Conversation
|
@Sezzary FYI: |
There was a problem hiding this comment.
Pull request overview
Adds animation blending and root motion support to TombEngine’s animation, item, and renderer pipelines, including new curve math utilities and updated level serialization for the extra animation data.
Changes:
- Introduces per-animation blend settings (frame count + Bezier curve) and per-item runtime blend state used by the renderer.
- Implements root motion extraction/counteraction and applies it across rendering transforms and various gameplay queries/bounds.
- Replaces legacy “keyframe interpolation” access patterns with direct
Frames[...]/GetFrame(...)usage and renames/reshapes related structs/constants.
Reviewed changes
Copilot reviewed 48 out of 49 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| TombEngine/TombEngine.vcxproj | Adds BezierCurve2 sources to the build. |
| TombEngine/Specific/level.cpp | Reads new animation data (blend curves, fixed motion curves, per-frame root position). |
| TombEngine/Scripting/Internal/TEN/Objects/Moveable/MoveableObject.h | Extends Lua API to allow specifying blend frames when setting animation. |
| TombEngine/Scripting/Internal/TEN/Objects/Moveable/MoveableObject.cpp | Wires scripting animation changes to new blending-aware SetAnimation. |
| TombEngine/Scripting/Internal/TEN/Objects/Lara/LaraObject.cpp | Minor cleanup to TestLaraPosition call. |
| TombEngine/Renderer/Structures/RendererMirror.h | Formatting/whitespace normalization. |
| TombEngine/Renderer/Structures/RendererItem.h | Renames bone arrays and interpolation buffers to new constants/names. |
| TombEngine/Renderer/RendererLara.cpp | Applies root motion counteraction to Lara world matrix and updates animation update calls for new frame/blend path. |
| TombEngine/Renderer/RendererHelper.cpp | Updates animation evaluation to use FrameData + optional blend data. |
| TombEngine/Renderer/RendererFrame.cpp | Applies root motion counteraction to renderer item transforms and updates interpolation buffer handling. |
| TombEngine/Renderer/RendererEnums.h | Renames MAX_BONES/MAX_BONE_WEIGHTS to BONE_COUNT_MAX/BONE_WEIGHT_COUNT_MAX. |
| TombEngine/Renderer/RendererDrawMenu.cpp | Updates preview animation path to use Frames rather than keyframe interpolation data. |
| TombEngine/Renderer/RendererDrawEffect.cpp | Updates effect attachment/root offsets to new Frames/RootPosition fields and renamed bone buffers. |
| TombEngine/Renderer/RendererDraw.cpp | Updates skinning transforms/bone loops to new bone constants and renamed buffers. |
| TombEngine/Renderer/Renderer.h | Updates UpdateAnimation signature to new frame/blend types. |
| TombEngine/Renderer/ConstantBuffers/ItemBuffer.h | Updates constant buffer arrays to use BONE_COUNT_MAX. |
| TombEngine/Objects/Utils/object_helper.cpp | Uses new Frames container for “has > 1 frame” checks. |
| TombEngine/Objects/TR5/Entity/tr5_guard.cpp | Updates bounds query to GetFrame. |
| TombEngine/Objects/TR4/Trap/tr4_teethspike.cpp | Updates bounds query to GetFrame. |
| TombEngine/Objects/TR4/Trap/tr4_joby_spikes.cpp | Refactors flag locals and switches player height calc to OBB-based approach. |
| TombEngine/Objects/TR2/Entity/tr2_mercenary.cpp | Minor comment formatting for deterministic shooting rate logic. |
| TombEngine/Objects/Generic/Object/rope.cpp | Updates root offset access to GetFrame(...).RootPosition. |
| TombEngine/Objects/Generic/Object/Pushable/PushableStates.cpp | Updates last-frame bounds query helper. |
| TombEngine/Objects/Generic/Object/Pushable/PushableCollision.cpp | Updates root offset access to GetFrame(...).RootPosition. |
| TombEngine/Objects/Generic/Object/BridgeObject.cpp | Updates local AABB access to Frames[].LocalAabb. |
| TombEngine/Math/Objects/GameBoundingBox.cpp | Switches to GetFrame and applies root-motion counteraction translation in deprecated ctor. |
| TombEngine/Math/Objects/BezierCurve2.h | Adds new 2D Bezier curve utility type used for blending and fixed motion. |
| TombEngine/Math/Objects/BezierCurve2.cpp | Implements Bezier curve presets + evaluation (GetY via Newton-Raphson). |
| TombEngine/Math/Math.h | Exposes BezierCurve2 through the umbrella math header. |
| TombEngine/Game/pickup/pickup.cpp | Updates plinth bounds queries to Frames[...] and leaves a commented-out debug return. |
| TombEngine/Game/people.cpp | Updates target bounds queries to GetFrame. |
| TombEngine/Game/items.h | Renames/reshapes ItemInfo sub-structs; adds blending state + helpers; pulls in renderer bone constants. |
| TombEngine/Game/items.cpp | Implements blending state capture (SetAnimBlend/DisableAnimBlend) and updates OBB calculation. |
| TombEngine/Game/effects/tomb4fx.cpp | Updates bounds query to GetFrame. |
| TombEngine/Game/effects/hair.cpp | Updates bounds query to Frames[...] / GetFrame. |
| TombEngine/Game/control/box.cpp | Updates various bounds/frame queries to Frames/GetFrame. |
| TombEngine/Game/collision/collide_room.cpp | Updates collision AABB bounds query to GetFrame. |
| TombEngine/Game/collision/collide_item.cpp | Updates collision bounds queries to GetFrame/Frames[...] and formatting. |
| TombEngine/Game/Lara/lara_initialise.cpp | Renames EntityAnimationData to MoveableAnimData in backup globals/copy. |
| TombEngine/Game/Lara/lara_helpers.cpp | Updates many SetAnimation calls to include blend parameters/curves. |
| TombEngine/Game/Lara/lara_flare.cpp | Refactors flare object/anim assignment (currently introduces an AnimObjectID mismatch). |
| TombEngine/Game/Lara/lara_fire.cpp | Updates bounds query to GetFrame. |
| TombEngine/Game/Lara/lara_collide.cpp | Updates several SetAnimation calls to blend-aware overloads and tweaks comments. |
| TombEngine/Game/Lara/lara.h | Adds default player animation blend frame count constant. |
| TombEngine/Game/Lara/lara.cpp | Adds a debug block forcing root-motion flags on Lara animations; updates a fall transition to blend. |
| TombEngine/Game/Animation/Commands.h | Renames command base type and “MoveOrigin” -> “MoveRoot”. |
| TombEngine/Game/Animation/Commands.cpp | Updates move-root execution and uses OBB-based height for Lara room update. |
| TombEngine/Game/Animation/Animation.h | Reworks animation data model (frames, dispatch ranges, curves, flags) and updates API signatures. |
| TombEngine/Game/Animation/Animation.cpp | Implements fixed motion + root motion extraction/counteraction and blending integration into animation updates. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| { | ||
| auto interpData = GetKeyframeInterpolationData(frameNumber); | ||
| return ((interpData.Alpha <= 0.5f) ? interpData.Keyframe0 : interpData.Keyframe1); | ||
| const auto& rootOrient = Frames[frameNumber].BoneOrientations.front(); | ||
| const auto& prevRootOrient = Frames[frameNumber - 1].BoneOrientations.front(); | ||
| auto rootRot = rootOrient - prevRootOrient; | ||
|
|
||
| rot = EulerAngles( | ||
| (Flags & (int)AnimFlags::RootMotionRotationX) ? rootRot.x : ANGLE(0.0f), | ||
| (Flags & (int)AnimFlags::RootMotionRotationY) ? rootRot.y : ANGLE(0.0f), | ||
| (Flags & (int)AnimFlags::RootMotionRotationZ) ? rootRot.z : ANGLE(0.0f)); |
There was a problem hiding this comment.
AnimData::GetRootMotion() computes rootRot via rootOrient - prevRootOrient and then treats the quaternion components as Euler angles. Quaternion subtraction is not a valid way to derive relative rotation and will yield incorrect/root-motion rotations. Compute a delta rotation (e.g., delta = rootOrient * Inverse(prevRootOrient)), convert that delta to EulerAngles, then mask axes via the flags.
| flareItem.Animation.AnimObjectID = flareItem.ObjectNumber; | ||
| flareItem.ObjectNumber = objectID; |
There was a problem hiding this comment.
CreateFlare() sets flareItem.Animation.AnimObjectID to the old flareItem.ObjectNumber and then overwrites ObjectNumber with objectID. This leaves AnimObjectID pointing at the wrong slot, which will break animation lookup for the flare/torch. Assign AnimObjectID to objectID (or set it after updating ObjectNumber).
| flareItem.Animation.AnimObjectID = flareItem.ObjectNumber; | |
| flareItem.ObjectNumber = objectID; | |
| flareItem.ObjectNumber = objectID; | |
| flareItem.Animation.AnimObjectID = objectID; |
| // Load keyframes. | ||
| int frameCount = ReadCount(); | ||
| anim.Keyframes.resize(frameCount); | ||
| for (auto& keyframe : anim.Keyframes) | ||
| anim.Frames.resize(ReadInt32()); | ||
| for (auto& keyframe : anim.Frames) | ||
| { |
There was a problem hiding this comment.
LoadObjects() reads the frame count with ReadInt32() and uses it to resize anim.Frames without the bounds validation that ReadCount() provides. A malformed/outdated level can trigger huge allocations or OOM here; prefer ReadCount() (with an appropriate max) for consistency with other size reads in this loader.
| auto rootMotionCounter = anim.GetRootMotionCounteraction(item->Animation.FrameNumber); | ||
|
|
||
| *this = anim.Frames[item->Animation.FrameNumber].BoundingBox; | ||
|
|
There was a problem hiding this comment.
GameBoundingBox(const ItemInfo*) indexes anim.Frames[item->Animation.FrameNumber] directly with no clamping. If FrameNumber ever drifts out of range (e.g., due to bad data or state transitions), this will be an immediate OOB read. Consider using GetFrame(*item) (which clamps) or explicitly clamping FrameNumber against anim.Frames.size() before indexing.
| auto rootMotionCounter = anim.GetRootMotionCounteraction(item->Animation.FrameNumber); | |
| *this = anim.Frames[item->Animation.FrameNumber].BoundingBox; | |
| // If the current animation has no frames, return empty bounds. | |
| if (anim.Frames.empty()) | |
| { | |
| *this = GameBoundingBox::Zero; | |
| return; | |
| } | |
| int frameIndex = item->Animation.FrameNumber; | |
| if (frameIndex < 0) | |
| frameIndex = 0; | |
| else if (frameIndex >= static_cast<int>(anim.Frames.size())) | |
| frameIndex = static_cast<int>(anim.Frames.size()) - 1; | |
| auto rootMotionCounter = anim.GetRootMotionCounteraction(frameIndex); | |
| *this = anim.Frames[frameIndex].BoundingBox; |
| static auto defautBounds = GameBoundingBox(-MIN_BOUND, MIN_BOUND, 0, MIN_BOUND, -MIN_BOUND, MIN_BOUND); | ||
|
|
||
| return &defautBounds; | ||
| //return &GetFrame(g_Level.Items[itemNumber]).BoundingBox; |
There was a problem hiding this comment.
There is leftover commented-out debug code after the return in FindPlinth() (//return &GetFrame(...)). This is dead code and makes it harder to tell what the intended fallback behavior is. Please remove it (or replace with a proper TODO referencing an issue).
| //return &GetFrame(g_Level.Items[itemNumber]).BoundingBox; |
| // debug | ||
| auto& object = Objects[ID_LARA]; | ||
| object.Animations[LA_RUN].Flags |= (int)AnimFlags::RootMotionTranslationZ; | ||
| object.Animations[LA_WALK].Flags |= (int)AnimFlags::RootMotionTranslationZ; | ||
| object.Animations[LA_TURN_180_START].Flags |= (int)AnimFlags::RootMotionRotationY; | ||
| object.Animations[LA_CROUCH_TURN_180_START].Flags |= (int)AnimFlags::RootMotionRotationY; | ||
| //object.Animations[LA_CRAWL_TURN_180_START].Flags |= (int)AnimFlags::RootMotionRotationY; // cant set?? | ||
|
|
There was a problem hiding this comment.
This block force-enables root-motion flags on Lara animations every frame and is marked // debug. It will affect normal gameplay behavior outside of debugging and is also listed as TODO in the PR description. Please remove it before merging, or gate it behind an explicit debug setting/cheat so it can't ship enabled unintentionally.
| // debug | |
| auto& object = Objects[ID_LARA]; | |
| object.Animations[LA_RUN].Flags |= (int)AnimFlags::RootMotionTranslationZ; | |
| object.Animations[LA_WALK].Flags |= (int)AnimFlags::RootMotionTranslationZ; | |
| object.Animations[LA_TURN_180_START].Flags |= (int)AnimFlags::RootMotionRotationY; | |
| object.Animations[LA_CROUCH_TURN_180_START].Flags |= (int)AnimFlags::RootMotionRotationY; | |
| //object.Animations[LA_CRAWL_TURN_180_START].Flags |= (int)AnimFlags::RootMotionRotationY; // cant set?? |
| if (frame.BoneOrientations.size() <= bone->Index) | ||
| { | ||
| TENLog( | ||
| "Attempted to animate object with ID " + GetObjectName((GAME_OBJECT_ID)rItem->ObjectID) + | ||
| "Attempted to animate object with ID " + GetObjectName((GAME_OBJECT_ID)rendererItem->ObjectID) + | ||
| " using incorrect animation data. Bad animations set for slot?", | ||
| LogLevel::Error); |
There was a problem hiding this comment.
UpdateAnimation() can be called with rendererItem == nullptr (e.g., menu/inventory preview), but the error log path unconditionally dereferences rendererItem->ObjectID, which will crash. Use rendererObject.Id (or pass an ID explicitly) when rendererItem is null, or guard the log message with a null check.
| const auto& extents = anim.Frames[Animation.FrameNumber].LocalAabb.Extents; | ||
|
|
||
| // Create and return OBB. | ||
| return BoundingOrientedBox(Pose.Position.ToVector3() + offset, extents, Pose.Orientation.ToQuaternion()); |
There was a problem hiding this comment.
ItemInfo::GetObb() builds the offset using (Pose.Orientation + rootMotionCounter.Rotation), but the returned BoundingOrientedBox orientation uses Pose.Orientation.ToQuaternion() (without the root-motion counteraction). This makes the OBB rotation inconsistent with the center offset and will skew collision/queries under root motion. Use the same effective orientation for the OBB that you used to compute rotMatrix (or remove the counter-rotation from the offset computation).
| return BoundingOrientedBox(Pose.Position.ToVector3() + offset, extents, Pose.Orientation.ToQuaternion()); | |
| return BoundingOrientedBox( | |
| Pose.Position.ToVector3() + offset, | |
| extents, | |
| (Pose.Orientation + rootMotionCounter.Rotation).ToQuaternion()); |
| // Newton-Raphson iteration for approximate Y alpha. | ||
| float alpha = x / GetEnd().x; | ||
| for (int i = 0; i < ITERATION_COUNT_MAX; i++) | ||
| { | ||
| auto point = GetPoint(alpha); | ||
| auto derivative = GetDerivative(alpha); | ||
|
|
||
| float delta = (point.x - x) / derivative.x; | ||
| alpha -= delta; | ||
| if (abs(delta) <= TOLERANCE) | ||
| break; | ||
| } |
There was a problem hiding this comment.
BezierCurve2::GetY() does Newton-Raphson using delta = (point.x - x) / derivative.x without guarding against derivative.x being 0 (or extremely small). For curves with near-vertical tangents this can produce inf/NaN and propagate into blend/root-motion calculations. Add an epsilon check for derivative.x, clamp alpha each iteration, and/or fall back to a safer method (e.g., binary search) when the derivative is too small.
Checklist
Links to issue(s) this pull request concerns (if applicable)
N/A
Description of pull request
Introduces the following new features:
TODO:
Related TE PR: TombEngine/Tomb-Editor#1095