Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
7f74a97
Write baked frames and real planes to TEN level
Sezzary Dec 14, 2025
5eaab58
Merge branch 'develop' into sezz/write-baked-frames
Sezzary Dec 15, 2025
e03e66a
Revert "Write baked frames and real planes to TEN level"
Sezzary Dec 15, 2025
fd80fb5
Only write baked frames
Sezzary Dec 15, 2025
6dabf84
Update Structs.cs
Sezzary Dec 15, 2025
1c30d55
Merge branch 'develop' into sezz/write-baked-frames
Lwmte Mar 15, 2026
93d3ada
UI changes for blend curves and root motion, move frame baking to Wad.cs
Lwmte Mar 16, 2026
60c439e
WIP
Lwmte Mar 16, 2026
39745e7
Revert "WIP"
Lwmte Mar 16, 2026
e332f9c
Change window style
Lwmte Mar 16, 2026
1b4c8e8
Preview animation blending
Lwmte Mar 16, 2026
02cf3a1
Adjust UI
Lwmte Mar 17, 2026
0e60753
Fix button position
Lwmte Mar 17, 2026
cee6baf
Increase state change row height
Lwmte Mar 17, 2026
4cd741b
Fix button style
Lwmte Mar 17, 2026
9fd7201
Write root motion bitmask properly
Lwmte Mar 17, 2026
896b576
Fixups
Sezzary Mar 17, 2026
aa55553
Fix name mismatches causing crash in state changes panel
Sezzary Mar 17, 2026
7540246
Partial fix for clamping `Next low frame`
Sezzary Mar 17, 2026
89d84a5
Remove X root motion checkbox
Sezzary Mar 17, 2026
d5389bf
Fixed issues with state change list
Lwmte Mar 18, 2026
e9ca36a
Remove unused field
Lwmte Mar 18, 2026
30f8509
Fixed mistake with array filling
Lwmte Mar 27, 2026
092cae2
Fixed incorrect bounding box conversions
Lwmte Mar 27, 2026
cad7358
Fixed incorrect flag
Lwmte Mar 28, 2026
0adca4b
Restore draw skin menu entry for animation editor
Lwmte Mar 28, 2026
4436613
Address Copilot comments part 1
Lwmte Mar 28, 2026
c50dab0
Address Copilot comments part 2
Lwmte Mar 28, 2026
905956a
Update Wad.cs
Lwmte Mar 28, 2026
04413b1
Remove unnecessary comment
Lwmte Mar 28, 2026
1971719
Merge branch 'develop' into sezz/write-baked-frames
Lwmte Apr 4, 2026
82934fd
Fixed incorrect preset assignment
Lwmte Apr 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 36 additions & 11 deletions TombLib/TombLib.Forms/Controls/BezierCurveEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Numerics;
using System.Windows.Forms;
using TombLib.Types;
Expand Down Expand Up @@ -71,18 +72,13 @@ public void UpdateUI()

private void AdjustHandlesForLinearCurve()
{
if (_bezierCurve.StartHandle == _bezierCurve.Start)
{

_controlPoints[0] = new Vector2(0, Height);
_controlPoints[1] = new Vector2(Width / 3.0f, Height * 2.0f / 3.0f);
}
if (_bezierCurve.StartHandle != _bezierCurve.Start || _bezierCurve.EndHandle != _bezierCurve.End)
return;

if (_bezierCurve.EndHandle == _bezierCurve.End)
{
_controlPoints[2] = new Vector2(2 * Width / 3.0f, Height / 3.0f);
_controlPoints[3] = new Vector2(Width, 0);
}
_controlPoints[0] = new Vector2(0, Height);
_controlPoints[1] = new Vector2(Width / 3.0f, Height * 2.0f / 3.0f);
_controlPoints[2] = new Vector2(2 * Width / 3.0f, Height / 3.0f);
_controlPoints[3] = new Vector2(Width, 0);
}

private Vector2 TransformToBezier(Vector2 point)
Expand Down Expand Up @@ -235,6 +231,35 @@ protected override void OnMouseDoubleClick(MouseEventArgs e)
}
}

public static void DrawPreview(Graphics g, Rectangle rect, BezierCurve2 curve, int padding = 2)
{
int x = rect.X + padding;
int y = rect.Y + padding;
int w = rect.Width - padding * 2;
int h = rect.Height - padding * 2;

if (w <= 0 || h <= 0)
return;

using (var pen = new Pen(Colors.LightText, 1.0f))
{
pen.StartCap = LineCap.Round;
pen.EndCap = LineCap.Round;

int steps = Math.Max(w / 2, 8);
var points = new PointF[steps + 1];
for (int i = 0; i <= steps; i++)
{
float alpha = (float)i / steps;
var p = curve.GetPoint(alpha);
points[i] = new PointF(x + p.X * w, y + (1.0f - p.Y) * h);
}

g.SmoothingMode = SmoothingMode.AntiAlias;
g.DrawLines(pen, points);
}
}

protected override void OnResize(EventArgs e)
{
base.OnResize(e);
Expand Down
73 changes: 37 additions & 36 deletions TombLib/TombLib/LevelData/Compilers/TombEngine/Structs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,6 @@ public void Write(BinaryWriterEx writer)
foreach (var animation in Animations)
{
writer.Write(animation.StateID);
writer.Write(animation.Interpolation);
writer.Write(animation.FrameEnd);
writer.Write(animation.NextAnimation);
writer.Write(animation.NextFrame);
Expand Down Expand Up @@ -701,22 +700,10 @@ public void Write(BinaryWriterEx writer)
writer.Write(fixedMotionCurveZ.StartHandle);
writer.Write(fixedMotionCurveZ.EndHandle);

if (animation.KeyFrames.Count == 0)
{
var defaultKeyFrame = new TombEngineKeyFrame();
defaultKeyFrame.BoneOrientations = new List<Quaternion>(Enumerable.Repeat(Quaternion.Identity, NumMeshes));

writer.Write(1);
defaultKeyFrame.Write(writer);
}
else
{
writer.Write(animation.KeyFrames.Count);
foreach (var keyFrame in animation.KeyFrames)
{
keyFrame.Write(writer);
}
}
// Write pre-baked frames.
writer.Write(animation.InterpolatedFrames.Count);
foreach (var frame in animation.InterpolatedFrames)
frame.Write(writer);

writer.Write(animation.StateChanges.Count);
foreach (var stateChange in animation.StateChanges)
Expand All @@ -725,9 +712,9 @@ public void Write(BinaryWriterEx writer)
writer.Write(stateChange.FrameLow);
writer.Write(stateChange.FrameHigh);
writer.Write(stateChange.NextAnimation);
writer.Write(stateChange.NextFrameLow);
writer.Write(stateChange.NextFrameHigh);
writer.Write(stateChange.BlendFrameCount);
writer.Write(stateChange.NextLowFrame);
writer.Write(stateChange.NextHighFrame);
writer.Write(stateChange.BlendFrames);
writer.Write(stateChange.BlendCurve.Start);
writer.Write(stateChange.BlendCurve.End);
writer.Write(stateChange.BlendCurve.StartHandle);
Expand All @@ -747,7 +734,7 @@ public void Write(BinaryWriterEx writer)
}
}

writer.Write(animation.Flags);
writer.Write((int)animation.RootMotion.Flags);
}
}
}
Expand All @@ -759,9 +746,9 @@ public struct TombEngineStateChange
public int FrameLow;
public int FrameHigh;
public int NextAnimation;
public int NextFrameLow;
public int NextFrameHigh;
public int BlendFrameCount;
public int NextLowFrame;
public int NextHighFrame;
public int BlendFrames;
public BezierCurve2 BlendCurve;
}

Expand All @@ -778,10 +765,11 @@ public struct TombEngineAnimation
public Vector3 VelocityStart;
public Vector3 VelocityEnd;
public List<TombEngineKeyFrame> KeyFrames;
public List<TombEngineKeyFrame> InterpolatedFrames;
public List<TombEngineStateChange> StateChanges;
public int NumAnimCommands;
public List<object> CommandData;
public int Flags;
public WadAnimRootMotionSettings RootMotion;
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
Expand All @@ -793,17 +781,8 @@ public class TombEngineKeyFrame

public void Write(BinaryWriterEx writer)
{
var center = new Vector3(
BoundingBox.X1 + BoundingBox.X2,
BoundingBox.Y1 + BoundingBox.Y2,
BoundingBox.Z1 + BoundingBox.Z2) / 2;
var extents = new Vector3(
BoundingBox.X2 - BoundingBox.X1,
BoundingBox.Y2 - BoundingBox.Y1,
BoundingBox.Z2 - BoundingBox.Z1) / 2;

writer.Write(center);
writer.Write(extents);
writer.Write(BoundingBox.Center);
writer.Write(BoundingBox.Extents);
writer.Write(RootOffset);

writer.Write(BoneOrientations.Count);
Expand All @@ -820,6 +799,28 @@ public struct TombEngineBoundingBox
public short Y2;
public short Z1;
public short Z2;

public Vector3 Center
{
get
{
return new Vector3(
X1 + X2,
Y1 + Y2,
Z1 + Z2) / 2;
}
}

public Vector3 Extents
{
get
{
return new Vector3(
X2 - X1,
Y2 - Y1,
Z2 - Z1) / 2;
}
}
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
Expand Down
63 changes: 59 additions & 4 deletions TombLib/TombLib/LevelData/Compilers/TombEngine/Wad.cs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ public void ConvertWad2DataToTombEngine()
newAnimation.VelocityStart = new Vector3(oldAnimation.StartLateralVelocity, 0, oldAnimation.StartVelocity);
newAnimation.VelocityEnd = new Vector3(oldAnimation.EndLateralVelocity, 0, oldAnimation.EndVelocity);
newAnimation.KeyFrames = new List<TombEngineKeyFrame>();
newAnimation.InterpolatedFrames = new List<TombEngineKeyFrame>();
newAnimation.StateChanges = new List<TombEngineStateChange>();
newAnimation.NumAnimCommands = oldAnimation.AnimCommands.Count;
newAnimation.CommandData = new List<object>();
Expand Down Expand Up @@ -314,6 +315,10 @@ public void ConvertWad2DataToTombEngine()
newAnimation.KeyFrames.Add(newFrame);
}

// Bake interpolated frames from keyframes and pass root motion settings.
BakeInterpolatedFrames(newAnimation, oldMoveable.Meshes.Count());
newAnimation.RootMotion = oldAnimation.RootMotion;

// Add anim commands
foreach (var command in oldAnimation.AnimCommands)
{
Expand All @@ -325,7 +330,7 @@ public void ConvertWad2DataToTombEngine()
newAnimation.CommandData.Add(new Vector3(command.Parameter1, command.Parameter2, command.Parameter3));

break;
case WadAnimCommandType.SetJumpDistance:
newAnimation.CommandData.Add(2);

Expand Down Expand Up @@ -387,9 +392,9 @@ public void ConvertWad2DataToTombEngine()
newStateChange.FrameLow = unchecked((int)(dispatch.InFrame));
newStateChange.FrameHigh = unchecked((int)(dispatch.OutFrame));
newStateChange.NextAnimation = checked((int)(dispatch.NextAnimation));
newStateChange.NextFrameLow = (int)dispatch.NextFrameLow;
newStateChange.NextFrameHigh = (int)dispatch.NextFrameHigh;
newStateChange.BlendFrameCount = (int)dispatch.BlendFrameCount;
newStateChange.NextLowFrame = (int)dispatch.NextLowFrame;
newStateChange.NextHighFrame = (int)dispatch.NextHighFrame;
newStateChange.BlendFrames = (int)dispatch.BlendFrames;
newStateChange.BlendCurve = dispatch.BlendCurve.Clone();

newAnimation.StateChanges.Add(newStateChange);
Expand Down Expand Up @@ -540,6 +545,56 @@ public void ConvertWad2DataToTombEngine()
}
}

private static void BakeInterpolatedFrames(TombEngineAnimation animation, int meshCount)
{
// Write dummy frame if no keyframes exist.
if (animation.KeyFrames.Count == 0)
{
var defaultKeyFrame = new TombEngineKeyFrame();
defaultKeyFrame.BoneOrientations = new List<Quaternion>(Enumerable.Repeat(Quaternion.Identity, meshCount));
animation.InterpolatedFrames.Add(defaultKeyFrame);
return;
}

float alphaStep = 1.0f / (float)Math.Max(1, animation.Interpolation);
for (int i = 0; i < animation.KeyFrames.Count; i++)
{
var currentKeyframe = animation.KeyFrames[i];
animation.InterpolatedFrames.Add(currentKeyframe);

if (i == (animation.KeyFrames.Count - 1))
break;

var nextKeyframe = animation.KeyFrames[i + 1];

// Add interpolated frames between keyframes.
for (int j = 1; j < animation.Interpolation; j++)
{
float alpha = alphaStep * j;

var center = Vector3.Lerp(currentKeyframe.BoundingBox.Center, nextKeyframe.BoundingBox.Center, alpha);
var extents = Vector3.Lerp(currentKeyframe.BoundingBox.Extents, nextKeyframe.BoundingBox.Extents, alpha);
var rootPos = Vector3.Lerp(currentKeyframe.RootOffset, nextKeyframe.RootOffset, alpha);

var boneOrients = new List<Quaternion>();
for (int k = 0; k < currentKeyframe.BoneOrientations.Count; k++)
boneOrients.Add(Quaternion.Slerp(currentKeyframe.BoneOrientations[k], nextKeyframe.BoneOrientations[k], alpha));

var frame = new TombEngineKeyFrame();
frame.BoundingBox = new TombEngineBoundingBox();
frame.BoundingBox.X1 = (short)(center.X - extents.X);
frame.BoundingBox.X2 = (short)(center.X + extents.X);
frame.BoundingBox.Y1 = (short)(center.Y - extents.Y);
frame.BoundingBox.Y2 = (short)(center.Y + extents.Y);
frame.BoundingBox.Z1 = (short)(center.Z - extents.Z);
frame.BoundingBox.Z2 = (short)(center.Z + extents.Z);
frame.RootOffset = rootPos;
frame.BoneOrientations = boneOrients;
animation.InterpolatedFrames.Add(frame);
}
}
}

public static short ToTrAngle(float angle)
{
double result = Math.Round(angle * (65536.0f / 360.0f));
Expand Down
2 changes: 1 addition & 1 deletion TombLib/TombLib/LevelData/Compilers/Wad.cs
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ public void ConvertWad2DataToTrData(Level l)
newAnimDispatch.Low = unchecked((ushort)(dispatch.InFrame + newAnimation.FrameStart));
newAnimDispatch.High = unchecked((ushort)(dispatch.OutFrame + newAnimation.FrameStart));
newAnimDispatch.NextAnimation = checked((ushort)(dispatch.NextAnimation + lastAnimation));
newAnimDispatch.NextFrame = dispatch.NextFrameLow;
newAnimDispatch.NextFrame = dispatch.NextLowFrame;

_animDispatches.Add(newAnimDispatch);
lastAnimDispatch++;
Expand Down
1 change: 0 additions & 1 deletion TombLib/TombLib/Types/BezierCurve2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,6 @@ public override int GetHashCode()
first.EndHandle == second.EndHandle;
}


public static bool operator !=(BezierCurve2 first, BezierCurve2 second) => !(first == second);
}
}
10 changes: 5 additions & 5 deletions TombLib/TombLib/Wad/Tr4Wad/Tr4WadOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ internal static WadMoveable ConvertTr4MoveableToWadMoveable(Wad2 wad, Tr4Wad old
ad.InFrame = (ushort)(wadAd.Low - newFrameStart);
ad.OutFrame = (ushort)(wadAd.High - newFrameStart);
ad.NextAnimation = (ushort)((wadAd.NextAnimation - oldMoveable.AnimationIndex) % numAnimations);
ad.NextFrameLow = (ushort)wadAd.NextFrame;
ad.NextLowFrame = (ushort)wadAd.NextFrame;

sc.Dispatches.Add(ad);
}
Expand Down Expand Up @@ -535,17 +535,17 @@ internal static WadMoveable ConvertTr4MoveableToWadMoveable(Wad2 wad, Tr4Wad old
if (animDispatch.NextAnimation > short.MaxValue)
{
animDispatch.NextAnimation = 0;
animDispatch.NextFrameLow = 0;
animDispatch.NextLowFrame = 0;
continue;
}

if (frameBases[newMoveable.Animations[animDispatch.NextAnimation]][0] != 0)
{
// HACK: In some cases dispatches have invalid NextFrame.
// From tests it seems that's ok to make NextFrame equal to max frame number.
animDispatch.NextFrameLow -= frameBases[newMoveable.Animations[animDispatch.NextAnimation]][0];
if (animDispatch.NextFrameLow > frameBases[newMoveable.Animations[animDispatch.NextAnimation]][1])
animDispatch.NextFrameLow = frameBases[newMoveable.Animations[animDispatch.NextAnimation]][1];
animDispatch.NextLowFrame -= frameBases[newMoveable.Animations[animDispatch.NextAnimation]][0];
if (animDispatch.NextLowFrame > frameBases[newMoveable.Animations[animDispatch.NextAnimation]][1])
animDispatch.NextLowFrame = frameBases[newMoveable.Animations[animDispatch.NextAnimation]][1];
}
stateChange.Dispatches[j] = animDispatch;
}
Expand Down
4 changes: 2 additions & 2 deletions TombLib/TombLib/Wad/TrLevels/TrLevelOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ public static WadMoveable ConvertTrLevelMoveableToWadMoveable(Wad2 wad, TrLevel
ad.InFrame = (ushort)(wadAd.Low - oldAnimation.FrameStart);
ad.OutFrame = (ushort)(wadAd.High - oldAnimation.FrameStart);
ad.NextAnimation = (ushort)((wadAd.NextAnimation - oldMoveable.Animation) % numAnimations);
ad.NextFrameLow = (ushort)wadAd.NextFrame;
ad.NextLowFrame = (ushort)wadAd.NextFrame;

sc.Dispatches.Add(ad);
}
Expand Down Expand Up @@ -575,7 +575,7 @@ public static WadMoveable ConvertTrLevelMoveableToWadMoveable(Wad2 wad, TrLevel
{
WadAnimDispatch animDispatch = stateChange.Dispatches[J];
if (frameBases[newMoveable.Animations[animDispatch.NextAnimation]] != 0)
animDispatch.NextFrameLow = (ushort)(animDispatch.NextFrameLow - frameBases[newMoveable.Animations[animDispatch.NextAnimation]]);
animDispatch.NextLowFrame = (ushort)(animDispatch.NextLowFrame - frameBases[newMoveable.Animations[animDispatch.NextAnimation]]);
stateChange.Dispatches[J] = animDispatch;
}
}
Expand Down
1 change: 1 addition & 0 deletions TombLib/TombLib/Wad/Wad2Chunks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ public static class Wad2Chunks
/********/public static readonly ChunkId Animation3 = ChunkId.FromString("W2Ani3");
/**********/public static readonly ChunkId AnimationVelocities = ChunkId.FromString("W2AniV");
/**********/public static readonly ChunkId AnimationName = ChunkId.FromString("W2AnmName");
/**********/public static readonly ChunkId AnimationRootMotion = ChunkId.FromString("W2AniRM");
/**********/public static readonly ChunkId StateChanges = ChunkId.FromString("W2StChs");
/************/public static readonly ChunkId StateChange = ChunkId.FromString("W2StCh");
/**************/public static readonly ChunkId Dispatches = ChunkId.FromString("W2Disps");
Expand Down
Loading