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.
This commit is contained in:
sharpmantis
2026-10-09 18:01:40 +02:00
committed by GitHub
parent 2884ad6de6
commit 316c02f70a
4 changed files with 69 additions and 0 deletions
+2
View File
@@ -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++;
+60
View File
@@ -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;
+4
View File
@@ -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
+3
View File
@@ -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;