diff options
| author | sharpmantis <43401382+sharpmantis@users.noreply.github.com> | 2026-10-09 18:01:40 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-10-09 16:01:40 +0000 |
| commit | 316c02f70a950010e4f724f4b3b71ebbd5b01a7f (patch) | |
| tree | 84158b7fd16739d985d6dd5d3a78952d6eb90ce1 | |
| parent | 2884ad6de6a2374347b42c98cbf86de39578eb65 (diff) | |
Interpolate skinned limb vertices (horse body and legs) (#7282)
Matrix interpolation cannot cover SKIN_LIMB_TYPE_ANIMATED limbs: their
vertices are deformed on the CPU and referenced through
gSPSegment(0x08, ...) without any recorded matrix, so recorded matrix
interpolation never moves them.
Record each skinned limb's pose (double-buffered, keyed by the vertex
entry) and blend the previous and current poses for every displayed
frame, right before the graphics commands are run. Ties the blend to a
logical-frame counter so a limb that was skipped for a frame falls back
to its exact pose instead of blending against a stale one.
| -rw-r--r-- | soh/soh/OTRGlobals.cpp | 2 | ||||
| -rw-r--r-- | soh/soh/frame_interpolation.cpp | 60 | ||||
| -rw-r--r-- | soh/soh/frame_interpolation.h | 4 | ||||
| -rw-r--r-- | soh/src/code/z_skin.c | 3 |
4 files changed, 69 insertions, 0 deletions
diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index 2638720f8..d336c3c5b 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -1785,6 +1785,8 @@ void RunCommands(Gfx* Commands, int time, int step, int denom, int count) { (time == denom) ? std::unordered_map<Mtx*, MtxF>() : FrameInterpolation_Interpolate(interpolationStep); // Ribbon vertices are shared by every frame of a tick: re-blend them before each run. FrameInterpolation_UpdateRibbonHeads(interpolationStep); + // Skinned vertices are shared by every displayed frame, so blend them before each run + FrameInterpolation_UpdateSkinnedVertices(interpolationStep); intp->mInterpolationT = interpolationStep; wnd->DrawAndRunGraphicsCommands(Commands, mtx_replacements); intp->mInterpolationIndex++; diff --git a/soh/soh/frame_interpolation.cpp b/soh/soh/frame_interpolation.cpp index b27e05df6..811f443ab 100644 --- a/soh/soh/frame_interpolation.cpp +++ b/soh/soh/frame_interpolation.cpp @@ -234,6 +234,18 @@ struct RibbonHead { }; unordered_map<const void*, RibbonHead> ribbon_heads; + +// Skinned limbs (horse body and legs) have their vertices rewritten on the CPU each +// gameplay frame, so matrix interpolation can't move them. Keep the last two poses and +// write a blend of them into the game's buffer before each displayed frame. +struct SkinnedLimb { + Vtx* dest = nullptr; + vector<Vtx> prev; + vector<Vtx> cur; + uint32_t frame = 0; +}; + +unordered_map<const void*, SkinnedLimb> skinned_limbs; uint32_t record_frame; Data& append(Op op) { @@ -505,6 +517,7 @@ void FrameInterpolation_StartRecord(void) { is_recording = true; } else { ribbon_heads.clear(); + skinned_limbs.clear(); } } @@ -717,6 +730,53 @@ void FrameInterpolation_UpdateRibbonHeads(float step) { } } +void FrameInterpolation_RecordSkinnedLimb(const void* key, void* dest, u32 vtxCount) { + if (!is_recording) { + return; + } + + SkinnedLimb& limb = skinned_limbs[key]; + // Only blend against a pose from the frame right before this one + if (limb.frame + 1 == record_frame && limb.cur.size() == vtxCount) { + limb.prev.swap(limb.cur); + } else { + limb.prev.clear(); + } + limb.dest = (Vtx*)dest; + limb.cur.assign(limb.dest, limb.dest + vtxCount); + limb.frame = record_frame; +} + +void FrameInterpolation_UpdateSkinnedVertices(float step) { + for (auto it = skinned_limbs.begin(); it != skinned_limbs.end();) { + SkinnedLimb& limb = it->second; + + // Not drawn this frame, its buffer may be freed + if (limb.frame != record_frame) { + it = skinned_limbs.erase(it); + continue; + } + + if (limb.prev.empty() || step >= 1.0f) { + std::copy(limb.cur.begin(), limb.cur.end(), limb.dest); + } else { + float w = 1.0f - step; + for (size_t i = 0; i < limb.cur.size(); i++) { + const Vtx& p = limb.prev[i]; + const Vtx& c = limb.cur[i]; + Vtx& d = limb.dest[i]; + d = c; + for (int j = 0; j < 3; j++) { + d.n.ob[j] = (s16)(w * p.n.ob[j] + step * c.n.ob[j]); + d.n.n[j] = (s8)(w * p.n.n[j] + step * c.n.n[j]); + } + } + } + + ++it; + } +} + // https://stackoverflow.com/questions/1148309/inverting-a-4x4-matrix static bool invert_matrix(const float m[16], float invOut[16]) { float inv[16], det; diff --git a/soh/soh/frame_interpolation.h b/soh/soh/frame_interpolation.h index 135c174f5..7143931b9 100644 --- a/soh/soh/frame_interpolation.h +++ b/soh/soh/frame_interpolation.h @@ -63,6 +63,10 @@ void FrameInterpolation_RecordRibbonHead(const void* key, void* dest, u32 vtxCou void FrameInterpolation_UpdateRibbonHeads(float step); +void FrameInterpolation_RecordSkinnedLimb(const void* key, void* dest, u32 vtxCount); + +void FrameInterpolation_UpdateSkinnedVertices(float step); + #ifdef __cplusplus } #endif diff --git a/soh/src/code/z_skin.c b/soh/src/code/z_skin.c index c8d0ce8df..fa53169b8 100644 --- a/soh/src/code/z_skin.c +++ b/soh/src/code/z_skin.c @@ -1,4 +1,5 @@ #include "global.h" +#include "soh/frame_interpolation.h" MtxF gSkinLimbMatrices[60]; // holds matrices for each limb of the skeleton currently being drawn @@ -126,6 +127,8 @@ void Skin_ApplyLimbModifications(GraphicsContext* gfxCtx, Skin* skin, s32 limbIn vtxBuf, &vtxPoint); } + FrameInterpolation_RecordSkinnedLimb(vtxEntry, vtxBuf, data->totalVtxCount); + gSPSegment(POLY_OPA_DISP++, 0x08, vtxEntry->buf[vtxEntry->index]); vtxEntry->index = (vtxEntry->index == 0) ? 1 : 0; |
