diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index 1b4f62c475..2638720f80 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -1780,9 +1780,12 @@ void RunCommands(Gfx* Commands, int time, int step, int denom, int count) { ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIWidgets::ColorValues.at(themeColor)); for (int i = 0; i < count; i++) { time += step; + const float interpolationStep = (float)time / denom; std::unordered_map mtx_replacements = - (time == denom) ? std::unordered_map() : FrameInterpolation_Interpolate((float)time / denom); - intp->mInterpolationT = (float)time / denom; + (time == denom) ? std::unordered_map() : FrameInterpolation_Interpolate(interpolationStep); + // Ribbon vertices are shared by every frame of a tick: re-blend them before each run. + FrameInterpolation_UpdateRibbonHeads(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 a95efacb3e..b27e05df6f 100644 --- a/soh/soh/frame_interpolation.cpp +++ b/soh/soh/frame_interpolation.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -222,6 +223,19 @@ void record_mode(u8 mode) { } } +// Ribbon trails (sword slashes, boomerang, arrows) are vertices written on the CPU, one +// segment per sample of the weapon position. Only the newest end moves within a gameplay +// frame, so only it gets blended, sliding from the previous sample to the new one. +struct RibbonHead { + Vtx* dest = nullptr; + vector cur; + vector> pairs; // (moving vertex, vertex it starts from) + uint32_t frame = 0; +}; + +unordered_map ribbon_heads; +uint32_t record_frame; + Data& append(Op op) { auto& m = current_path.back()->ops[op]; current_path.back()->items.emplace_back(op, m.size()); @@ -486,8 +500,11 @@ void FrameInterpolation_StartRecord(void) { current_path.push_back(¤t_recording.root_path); matrix_depth = 0; new_mtx_depth = -1; + record_frame++; if (OTRGlobals::Instance->GetInterpolationFPS() != 20) { is_recording = true; + } else { + ribbon_heads.clear(); } } @@ -655,6 +672,51 @@ void FrameInterpolation_RecordSkinMatrixMtxFToMtx(MtxF* src, Mtx* dest) { FrameInterpolation_RecordMatrixMtxFToMtx(src, dest); } +void FrameInterpolation_RecordRibbonHead(const void* key, void* dest, u32 vtxCount, u32 pairCount, const s16* pairs) { + if (!is_recording) { + return; + } + + RibbonHead& head = ribbon_heads[key]; + head.dest = (Vtx*)dest; + head.cur.assign(head.dest, head.dest + vtxCount); + head.pairs.clear(); + for (u32 i = 0; i < pairCount; i++) { + head.pairs.emplace_back(pairs[2 * i], pairs[2 * i + 1]); + } + head.frame = record_frame; +} + +void FrameInterpolation_UpdateRibbonHeads(float step) { + for (auto it = ribbon_heads.begin(); it != ribbon_heads.end();) { + RibbonHead& head = it->second; + + // Not drawn this frame, the trail ended or stopped growing + if (head.frame != record_frame) { + it = ribbon_heads.erase(it); + continue; + } + + std::copy(head.cur.begin(), head.cur.end(), head.dest); + if (step < 1.0f) { + float w = 1.0f - step; + for (const auto& [dst, src] : head.pairs) { + const Vtx& from = head.cur[src]; + Vtx& to = head.dest[dst]; + for (int j = 0; j < 3; j++) { + to.v.ob[j] = (s16)(w * from.v.ob[j] + step * to.v.ob[j]); + } + // Colour carries the fade + for (int j = 0; j < 4; j++) { + to.v.cn[j] = (u8)(w * from.v.cn[j] + step * to.v.cn[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 8a68d4c3e2..135c174f56 100644 --- a/soh/soh/frame_interpolation.h +++ b/soh/soh/frame_interpolation.h @@ -58,6 +58,11 @@ void FrameInterpolation_RecordMatrixRotateAxis(f32 angle, Vec3f* axis, u8 mode); void FrameInterpolation_RecordSkinMatrixMtxFToMtx(MtxF* src, Mtx* dest); +// pairs holds pairCount (moving vertex, vertex it starts from) index pairs +void FrameInterpolation_RecordRibbonHead(const void* key, void* dest, u32 vtxCount, u32 pairCount, const s16* pairs); + +void FrameInterpolation_UpdateRibbonHeads(float step); + #ifdef __cplusplus } #endif diff --git a/soh/src/code/z_eff_blure.c b/soh/src/code/z_eff_blure.c index 56e5951495..22862a90ed 100644 --- a/soh/src/code/z_eff_blure.c +++ b/soh/src/code/z_eff_blure.c @@ -515,6 +515,13 @@ void EffectBlure_SetupSmooth(EffectBlure* this, GraphicsContext* gfxCtx) { CLOSE_DISPS(gfxCtx); } +// AddVertex sets a new sample's timer to 1. Only blend on frames that added one, +// otherwise a finished trail keeps shrinking its last segment. +static s32 EffectBlure_HeadIsNew(EffectBlure* this) { + return (this->numElements >= 2) && (this->elements[this->numElements - 1].state == 1) && + (this->elements[this->numElements - 1].timer == 1); +} + // original name: "SQ_NoInterpolate_disp" void EffectBlure_DrawElemNoInterpolation(EffectBlure* this, EffectBlureElement* elem, s32 index, GraphicsContext* gfxCtx) { @@ -601,6 +608,13 @@ void EffectBlure_DrawElemNoInterpolation(EffectBlure* this, EffectBlureElement* vtx[3].v.cn[2] = sp78.b; vtx[3].v.cn[3] = sp78.a; + if ((index == (s32)this->numElements - 2) && EffectBlure_HeadIsNew(this)) { + // Newest end (vtx[2], vtx[3]) slides from the previous sample (vtx[1], vtx[0]). + static const s16 headPairs[] = { 2, 1, 3, 0 }; + + FrameInterpolation_RecordRibbonHead(this, vtx, 4, 2, headPairs); + } + gSPVertex(POLY_XLU_DISP++, vtx, 4, 0); gSP2Triangles(POLY_XLU_DISP++, 0, 1, 2, 0, 0, 2, 3, 0); } @@ -768,6 +782,17 @@ void EffectBlure_DrawElemHermiteInterpolation(EffectBlure* this, EffectBlureElem vtx[j2].v.cn[3] = EffectSs_LerpU8(sp1A0.a, sp198.a, temp_f28); } + if ((index == (s32)this->numElements - 2) && EffectBlure_HeadIsNew(this)) { + // Every spline vertex depends on the moving end cross-section: collapse each onto + // the start of its edge (vtx[0] for the tip, vtx[1] for the base). + static const s16 headPairs[] = { + 2, 0, 4, 0, 6, 0, 8, 0, 10, 0, 12, 0, 14, 0, // tip edge + 3, 1, 5, 1, 7, 1, 9, 1, 11, 1, 13, 1, 15, 1, // base edge + }; + + FrameInterpolation_RecordRibbonHead(this, vtx, 16, 14, headPairs); + } + gSPVertex(POLY_XLU_DISP++, vtx, 16, 0); gSP2Triangles(POLY_XLU_DISP++, 0, 1, 3, 0, 0, 3, 2, 0); gSP2Triangles(POLY_XLU_DISP++, 2, 3, 5, 0, 2, 5, 4, 0); @@ -1061,6 +1086,13 @@ void EffectBlure_DrawSimple(EffectBlure* this2, GraphicsContext* gfxCtx) { } } + if (EffectBlure_HeadIsNew(this)) { + // Newest end (vtx[2], vtx[3]) slides from the previous sample (vtx[0], vtx[1]). + static const s16 headPairs[] = { 2, 0, 3, 1 }; + + FrameInterpolation_RecordRibbonHead(this, &vtx[(this->numElements - 2) * 4], 4, 2, headPairs); + } + EffectBlure_DrawSimpleVertices(gfxCtx, this, vtx); } } @@ -1155,6 +1187,13 @@ void EffectBlure_Draw(void* thisx, GraphicsContext* gfxCtx) { } } + if ((j >= 4) && EffectBlure_HeadIsNew(this)) { + // Newest pair slides from the previous pair (j - 4, j - 3). + const s16 headPairs[] = { (s16)(j - 2), (s16)(j - 4), (s16)(j - 1), (s16)(j - 3) }; + + FrameInterpolation_RecordRibbonHead(this, vtx, j, 2, headPairs); + } + j = 0; gSPVertex(POLY_XLU_DISP++, vtx, 32, 0);