mirror of
https://github.com/HarbourMasters/Shipwright
synced 2026-10-15 08:20:50 -04:00
Interpolate ribbon trail vertices (sword slashes, arrows, ...) (#7335)
Weapon trails (EffectBlure) are CPU built vertex blocks, one segment per sample of the weapon position; matrix interpolation cannot touch them, so above 20 fps they pop segment by segment. A trail is a history: its sample indices shift every logical frame, so only its newest end has to be blended, sliding from the previous sample towards the current one. Only frames that appended a sample are recorded, otherwise a finished trail keeps collapsing its last segment.
This commit is contained in:
@@ -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*, MtxF> mtx_replacements =
|
||||
(time == denom) ? std::unordered_map<Mtx*, MtxF>() : FrameInterpolation_Interpolate((float)time / denom);
|
||||
intp->mInterpolationT = (float)time / denom;
|
||||
(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);
|
||||
intp->mInterpolationT = interpolationStep;
|
||||
wnd->DrawAndRunGraphicsCommands(Commands, mtx_replacements);
|
||||
intp->mInterpolationIndex++;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
@@ -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<Vtx> cur;
|
||||
vector<pair<s16, s16>> pairs; // (moving vertex, vertex it starts from)
|
||||
uint32_t frame = 0;
|
||||
};
|
||||
|
||||
unordered_map<const void*, RibbonHead> 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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user