summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsharpmantis <43401382+sharpmantis@users.noreply.github.com>2026-10-09 18:01:40 +0200
committerGitHub <noreply@github.com>2026-10-09 16:01:40 +0000
commit316c02f70a950010e4f724f4b3b71ebbd5b01a7f (patch)
tree84158b7fd16739d985d6dd5d3a78952d6eb90ce1
parent2884ad6de6a2374347b42c98cbf86de39578eb65 (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.cpp2
-rw-r--r--soh/soh/frame_interpolation.cpp60
-rw-r--r--soh/soh/frame_interpolation.h4
-rw-r--r--soh/src/code/z_skin.c3
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;