feat: split native scale and translation channels
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Sys4;
|
||||
using Age.Engine.Vm;
|
||||
@@ -6,31 +7,36 @@ using Xunit;
|
||||
|
||||
public class GfxAnimationTests
|
||||
{
|
||||
// ---- Task 2: GfxState animation-channel data model (revised global-clock model, see engine-re.md) ----
|
||||
// ---- Native independent matrix channels + separate cyclic rotation (see engine-re.md). ----
|
||||
|
||||
[Fact]
|
||||
public void SetAnimTransform_RecordsTargetParamsAndEnables()
|
||||
public void ScaleAndTranslationChannels_AreIndependent()
|
||||
{
|
||||
var g = new GfxState();
|
||||
g.SetAnimTransform(0x1000, p1: 7, p2: 9, target: (100, 100, 100), normalized: true);
|
||||
g.SetScaleChannel(0x1000, delayMs: 7, durationMs: 900, percent: (200, 50, 100));
|
||||
g.SetTranslationChannel(0x1000, delayMs: 9, durationMs: 1200, target: (80, -25, 6));
|
||||
var o = g.TryGet(0x1000)!;
|
||||
Assert.Equal((100L, 100L, 100L), o.AnimTarget);
|
||||
Assert.Equal(7, o.AnimParam1);
|
||||
Assert.Equal(9, o.AnimParam2);
|
||||
Assert.True(o.AnimNormalized);
|
||||
Assert.True(o.AnimEnabled);
|
||||
Assert.Equal((2.0, 0.5, 1.0), o.ScaleTarget);
|
||||
Assert.Equal((80.0, -25.0, 6.0), o.TranslationTarget);
|
||||
Assert.Equal((7, 900), (o.ScaleDelayMs, o.ScaleDurationMs));
|
||||
Assert.Equal((9, 1200), (o.TranslationDelayMs, o.TranslationDurationMs));
|
||||
Assert.True(o.ScaleEnabled);
|
||||
Assert.True(o.TranslationEnabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StartAnim_SetsTargetDurationEnablesAndBumpsGeneration()
|
||||
public void RotationCycle_DoesNotOverwriteMatrixChannels()
|
||||
{
|
||||
var g = new GfxState();
|
||||
g.StartAnim(0x1000, durationTicks: 30, target: (0, 0, 5));
|
||||
g.SetScaleChannel(0x1000, 0, 100, (150, 150, 100));
|
||||
g.SetTranslationChannel(0x1000, 0, 100, (10, 20, 0));
|
||||
g.SetRotationCycle(0x1000, periodMs: 30, axis: (0, 0, 5));
|
||||
var o = g.TryGet(0x1000)!;
|
||||
Assert.Equal((0L, 0L, 5L), o.AnimTarget);
|
||||
Assert.Equal(30, o.AnimDurationTicks);
|
||||
Assert.True(o.AnimEnabled);
|
||||
Assert.Equal(1, o.AnimGeneration); // fresh object starts at 0, one anim-start -> 1
|
||||
Assert.Equal((1.5, 1.5, 1.0), o.ScaleTarget);
|
||||
Assert.Equal((10.0, 20.0, 0.0), o.TranslationTarget);
|
||||
Assert.Equal((0L, 0L, 5L), o.RotationAxis);
|
||||
Assert.Equal(30, o.RotationPeriodMs);
|
||||
Assert.True(o.RotationEnabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -52,29 +58,32 @@ public class GfxAnimationTests
|
||||
private static (int, Operand[]) Exit() => (0x2, System.Array.Empty<Operand>());
|
||||
|
||||
[Fact]
|
||||
public void SetAnimTransformAbs_DispatchRecordsChannel()
|
||||
public void MatrixOpcodeDispatch_RecordsSeparateChannels()
|
||||
{
|
||||
var t = T();
|
||||
// handle g[1]=0x1000; p1 g[2]=7; p2 g[3]=9; target g[4,5,6]=(800,500,0)
|
||||
// 0x220 sets translation; 0x21e sets scale percentages without overwriting it.
|
||||
var scene = ScriptAssembler.Assemble(t, "ANIM", new List<(int, Operand[])>
|
||||
{
|
||||
MovGI(1, 0x1000), MovGI(2, 7), MovGI(3, 9), MovGI(4, 800), MovGI(5, 500), MovGI(6, 0),
|
||||
(0x220, new[] { G(1), G(2), G(3), G(4), G(5), G(6) }),
|
||||
MovGI(4, 200), MovGI(5, 50), MovGI(6, 100),
|
||||
(0x21e, new[] { G(1), G(2), G(3), G(4), G(5), G(6) }),
|
||||
Exit(),
|
||||
}, System.Array.Empty<string>());
|
||||
var vm = new VirtualMachine(scene, t, new RecordingHost());
|
||||
vm.Run();
|
||||
var o = vm.Gfx.TryGet(0x1000)!;
|
||||
Assert.Equal((800L, 500L, 0L), o.AnimTarget);
|
||||
Assert.False(o.AnimNormalized);
|
||||
Assert.True(o.AnimEnabled);
|
||||
Assert.Equal((800.0, 500.0, 0.0), o.TranslationTarget);
|
||||
Assert.Equal((2.0, 0.5, 1.0), o.ScaleTarget);
|
||||
Assert.True(o.TranslationEnabled);
|
||||
Assert.True(o.ScaleEnabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnimStartAndClock_DispatchSetTargetDurationClockAndGeneration()
|
||||
public void RotationCycleAndClock_DispatchRemainSeparate()
|
||||
{
|
||||
var t = T();
|
||||
// anim-start(handle=0x1000, duration=30, target=(0,0,5)); set-anim-clock(400)
|
||||
// The legacy anim-start name is a rotation period+axis; 0x238 remains its own clock service.
|
||||
var scene = ScriptAssembler.Assemble(t, "ANIM", new List<(int, Operand[])>
|
||||
{
|
||||
MovGI(1, 0x1000), MovGI(2, 30), MovGI(3, 0), MovGI(4, 0), MovGI(5, 5), MovGI(6, 400),
|
||||
@@ -85,35 +94,48 @@ public class GfxAnimationTests
|
||||
var vm = new VirtualMachine(scene, t, new RecordingHost());
|
||||
vm.Run();
|
||||
var o = vm.Gfx.TryGet(0x1000)!;
|
||||
Assert.Equal((0L, 0L, 5L), o.AnimTarget);
|
||||
Assert.Equal(30, o.AnimDurationTicks);
|
||||
Assert.Equal(1, o.AnimGeneration);
|
||||
Assert.True(o.AnimEnabled);
|
||||
Assert.Equal((0L, 0L, 5L), o.RotationAxis);
|
||||
Assert.Equal(30, o.RotationPeriodMs);
|
||||
Assert.True(o.RotationEnabled);
|
||||
Assert.Equal((1.0, 1.0, 1.0), o.ScaleCurrent);
|
||||
Assert.Equal((0.0, 0.0, 0.0), o.TranslationCurrent);
|
||||
Assert.Equal(400, vm.Gfx.AnimClockDurationTicks);
|
||||
Assert.Equal(1, vm.Gfx.AnimClockGeneration);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SnapshotCarriesAnimStateForVisibleObject()
|
||||
public void SnapshotSamplesDelayedMatrixChannels_WithoutUsingZAsOpacity()
|
||||
{
|
||||
var t = T();
|
||||
// make object 0xA visible via set/draw-texture, then anim-start it toward (800,600,0) over 30 ticks.
|
||||
var scene = ScriptAssembler.Assemble(t, "ANIM", new List<(int, Operand[])>
|
||||
{
|
||||
MovGI(1, 0xA), MovGI(2, 4), MovGI(7, 0x25), MovGI(3, 800), MovGI(4, 600), MovGI(5, 0), MovGI(6, 0),
|
||||
(0x1f9, new[] { G(7), G(2), I(0) }), // set-texture resId 0x25 -> slot 4
|
||||
(0x1fb, new[] { G(1), G(2), I(0), I(0), G(3), G(4), G(5), G(6) }), // draw-texture: object 0xA visible
|
||||
MovGI(8, 30),
|
||||
(0x234, new[] { G(1), G(8), G(3), G(4), G(5) }), // anim-start(0xA, dur=30, (800,600,0))
|
||||
Exit(),
|
||||
}, System.Array.Empty<string>());
|
||||
var vm = new VirtualMachine(scene, t, new RecordingHost());
|
||||
vm.Run();
|
||||
var vis = vm.Gfx.SnapshotVisibleObjects();
|
||||
Assert.Single(vis);
|
||||
Assert.True(vis[0].Anim.Enabled);
|
||||
Assert.Equal((800L, 600L, 0L), (vis[0].Anim.TX, vis[0].Anim.TY, vis[0].Anim.TZ));
|
||||
Assert.Equal(30, vis[0].Anim.DurationTicks);
|
||||
Assert.Equal(1, vis[0].Anim.Generation);
|
||||
var g = new GfxState();
|
||||
g.SetSurface(4, 0x25, -1);
|
||||
g.BindDraw(0xA, 4, 0, 0, 100, 80, 120, 70);
|
||||
g.GetOrCreate(0xA).V18 = (100, 50, 0);
|
||||
g.SetScaleChannel(0xA, delayMs: 100, durationMs: 200, percent: (200, 50, 300));
|
||||
g.SetTranslationChannel(0xA, delayMs: 100, durationMs: 200, target: (40, -10, 99));
|
||||
g.SetRotationCycle(0xA, periodMs: 30, axis: (0, 0, 5));
|
||||
|
||||
var start = g.SnapshotVisibleObjects(1000).Single();
|
||||
Assert.Equal((1.0, 1.0, 1.0), (start.Transform.ScaleX, start.Transform.ScaleY, start.Transform.ScaleZ));
|
||||
Assert.Equal((0.0, 0.0, 0.0),
|
||||
(start.Transform.TranslateX, start.Transform.TranslateY, start.Transform.TranslateZ));
|
||||
|
||||
var held = g.SnapshotVisibleObjects(1100).Single();
|
||||
Assert.Equal((1.0, 1.0, 1.0), (held.Transform.ScaleX, held.Transform.ScaleY, held.Transform.ScaleZ));
|
||||
Assert.Equal((0.0, 0.0, 0.0),
|
||||
(held.Transform.TranslateX, held.Transform.TranslateY, held.Transform.TranslateZ));
|
||||
|
||||
var halfway = g.SnapshotVisibleObjects(1200).Single();
|
||||
Assert.Equal((1.5, 0.75, 2.0),
|
||||
(halfway.Transform.ScaleX, halfway.Transform.ScaleY, halfway.Transform.ScaleZ));
|
||||
Assert.Equal((20.0, -5.0, 49.5),
|
||||
(halfway.Transform.TranslateX, halfway.Transform.TranslateY, halfway.Transform.TranslateZ));
|
||||
Assert.Equal(255, halfway.Alpha);
|
||||
|
||||
var done = g.SnapshotVisibleObjects(1300).Single();
|
||||
Assert.Equal((2.0, 0.5, 3.0), (done.Transform.ScaleX, done.Transform.ScaleY, done.Transform.ScaleZ));
|
||||
Assert.Equal((40.0, -10.0, 99.0),
|
||||
(done.Transform.TranslateX, done.Transform.TranslateY, done.Transform.TranslateZ));
|
||||
Assert.True(done.Rotation.Enabled);
|
||||
Assert.Equal(255, done.Alpha); // scale-Z=3, translation-Z=99, rotation-axis-Z=5: still opaque
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ using System.Linq;
|
||||
|
||||
namespace Age.Engine.Model;
|
||||
|
||||
/// <summary>Per-object animation channel snapshot for the compositor (cluster 0x21c-0x243). Enabled = the
|
||||
/// object has an active anim channel; (TX,TY,TZ) = the transform target it animates toward; Normalized = the
|
||||
/// 0x21e ~percent variant; DurationTicks = the object's own duration (anim-start op2). The GLOBAL clock timebase
|
||||
/// (duration + generation) is read separately off <see cref="GfxState.AnimClockDurationTicks"/>. Generation bumps
|
||||
/// on each anim-start — the compositor re-triggers its wall-clock tween when it changes.</summary>
|
||||
public readonly record struct AnimState(bool Enabled, bool Normalized, long TX, long TY, long TZ,
|
||||
long DurationTicks, long Generation);
|
||||
/// <summary>The sampled native matrix channels carried to the compositor. Op 0x21e owns scale; op 0x220 owns
|
||||
/// translation. Z is retained for model fidelity even though the current 2D compositor uses X/Y only.</summary>
|
||||
public readonly record struct TransformState(double ScaleX, double ScaleY, double ScaleZ,
|
||||
double TranslateX, double TranslateY, double TranslateZ,
|
||||
double AnchorX, double AnchorY, double AnchorZ);
|
||||
|
||||
public readonly record struct RotationCycleState(bool Enabled, long PeriodMs, long AxisX, long AxisY, long AxisZ);
|
||||
|
||||
/// <summary>A renderable view of one visible gfx object — the host composites these in ascending-handle order
|
||||
/// (= the engine's z-order) each frame. Built by <see cref="GfxState.SnapshotVisibleObjects"/>; the surface
|
||||
@@ -20,7 +20,8 @@ public readonly record struct AnimState(bool Enabled, bool Normalized, long TX,
|
||||
/// vanish — the grey-background bug).</summary>
|
||||
public readonly record struct RenderObject(long Handle, long SurfaceResId, long ColorKey,
|
||||
int SrcX, int SrcY, int W, int H, int DstX, int DstY,
|
||||
AnimState Anim, int Alpha, long Tint, int TintStrength, BlendKind Blend);
|
||||
TransformState Transform, RotationCycleState Rotation,
|
||||
int Alpha, long Tint, int TintStrength, BlendKind Blend);
|
||||
|
||||
/// <summary>Host-agnostic model of the AGE native gfx command-buffer (reversed in
|
||||
/// docs/engine-re.md, gfx op-contract table). One registry maps an object handle to a GfxObject — the
|
||||
@@ -50,16 +51,22 @@ public sealed class GfxState
|
||||
public (int X, int Y, int W, int H) SrcRect;
|
||||
public bool Visible;
|
||||
|
||||
// ---- animation channel (cluster 0x21c-0x243; see docs/engine-re.md "sprite transform / ANIMATION").
|
||||
// 0x21e/0x220 set the transform directly (worker gfx_anim_set_channel@0x47eaa0: obj+0x3c=p1, +0x50=p2,
|
||||
// +0xac=target, +0x68=enable); 0x234 anim-start animates toward a target over the GLOBAL clock (0x238).
|
||||
// Passive: recorded here, interpolated by the Godot compositor over wall-clock. ----
|
||||
public (long X, long Y, long Z) AnimTarget;
|
||||
public long AnimParam1, AnimParam2;
|
||||
public bool AnimNormalized; // 0x21e (operands ~percent, /_DAT_00571c28) vs 0x220 (absolute)
|
||||
public bool AnimEnabled; // obj+0x68
|
||||
public long AnimDurationTicks; // 0x234 anim-start op2 (this object's duration; maxed into the clock)
|
||||
public long AnimGeneration; // bumped by anim-start (0x234); the compositor's per-object re-trigger
|
||||
// ---- independent one-shot matrix channels (gfx_object_apply_transform_channels@0x472f00). ----
|
||||
// Scale: current obj+0x6c, target obj+0xac, delay obj+0x3c, duration obj+0x50.
|
||||
public (double X, double Y, double Z) ScaleCurrent = (1, 1, 1), ScaleTarget = (1, 1, 1);
|
||||
public long ScaleDelayMs, ScaleDurationMs;
|
||||
public bool ScaleEnabled;
|
||||
// Translation: current obj+0x16c, target obj+0x1ac, delay obj+0x44, duration obj+0x58.
|
||||
public (double X, double Y, double Z) TranslationCurrent, TranslationTarget;
|
||||
public long TranslationDelayMs, TranslationDurationMs;
|
||||
public bool TranslationEnabled;
|
||||
// Shared matrix-channel start timestamp obj+0x34, seeded from frame-time ctx+0xb550.
|
||||
public long MatrixStartMs = -1;
|
||||
|
||||
// Op 0x234 is a separate cyclic rotation channel (period obj+0x228, axis obj+0x244..0x24c).
|
||||
public long RotationPeriodMs;
|
||||
public (long X, long Y, long Z) RotationAxis;
|
||||
public bool RotationEnabled;
|
||||
}
|
||||
|
||||
// ---- geometry/draw object store (V18/V24/draw bind, the compositor's input) ----
|
||||
@@ -75,10 +82,8 @@ public sealed class GfxState
|
||||
private readonly Dictionary<long, long> _fieldTable = new(); // ctx+0x46d14 (0x216); no family writer -> default 0
|
||||
public long CurrentObject { get; private set; }
|
||||
|
||||
// ---- GLOBAL animation clock (op 0x238 set-anim-clock; native ctx+0x51b7c total / +0x51b78 elapsed).
|
||||
// Non-blocking: the op only configures duration; the host advances elapsed per-frame and tweens all armed
|
||||
// objects over it (docs/engine-re.md, "anim_start/set_anim_clock decoded"). Generation bumps on each set so
|
||||
// the compositor resets its wall-clock elapsed. ----
|
||||
// ---- Separate global animation service clock (op 0x238; ctx+0x51b7c total / +0x51b78 elapsed).
|
||||
// Retained for its opcode family; 0x21e scale and 0x220 translation use frame-time directly instead. ----
|
||||
public long AnimClockDurationTicks { get; private set; }
|
||||
public long AnimClockGeneration { get; private set; }
|
||||
|
||||
@@ -188,35 +193,42 @@ public sealed class GfxState
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x21e/0x220 (set-anim-transform): record the transform target + two scalar params on the
|
||||
/// object and enable its animation channel. normalized = 0x21e (operands ~percent, /_DAT_00571c28);
|
||||
/// absolute = 0x220. Native worker gfx_anim_set_channel@0x47eaa0 sets obj+0x3c=p1, +0x50=p2, +0xac=target,
|
||||
/// +0x68=1.</summary>
|
||||
public void SetAnimTransform(long handle, long p1, long p2, (long X, long Y, long Z) target, bool normalized)
|
||||
/// <summary>Op 0x21e: normalized scale target (100 = identity), with independent delay/duration.</summary>
|
||||
public void SetScaleChannel(long handle, long delayMs, long durationMs, (long X, long Y, long Z) percent)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var o = GetOrCreate(handle);
|
||||
o.AnimParam1 = p1; o.AnimParam2 = p2; o.AnimTarget = target;
|
||||
o.AnimNormalized = normalized; o.AnimEnabled = true;
|
||||
o.ScaleDelayMs = delayMs; o.ScaleDurationMs = durationMs;
|
||||
o.ScaleTarget = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0);
|
||||
o.ScaleEnabled = durationMs > 0; o.MatrixStartMs = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x234 (anim-start): animate the object toward <paramref name="target"/> over the global
|
||||
/// clock; <paramref name="durationTicks"/> is this object's duration (native label_1235a maxes them into
|
||||
/// the clock). Bumps AnimGeneration — the compositor's per-object re-trigger.</summary>
|
||||
public void StartAnim(long handle, long durationTicks, (long X, long Y, long Z) target)
|
||||
/// <summary>Op 0x220: absolute translation target, with independent delay/duration.</summary>
|
||||
public void SetTranslationChannel(long handle, long delayMs, long durationMs, (long X, long Y, long Z) target)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var o = GetOrCreate(handle);
|
||||
o.AnimTarget = target; o.AnimDurationTicks = durationTicks;
|
||||
o.AnimEnabled = true; o.AnimGeneration++;
|
||||
o.TranslationDelayMs = delayMs; o.TranslationDurationMs = durationMs;
|
||||
o.TranslationTarget = target;
|
||||
o.TranslationEnabled = durationMs > 0; o.MatrixStartMs = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x238 (set-anim-clock): set the GLOBAL animation duration (game ticks) and bump the clock
|
||||
/// generation so the host resets its wall-clock elapsed. Non-blocking (the render loop advances it).</summary>
|
||||
/// <summary>Op 0x234: retain the cyclic rotation period and axis separately. Native interpolation uses
|
||||
/// frame-time ctx+0xb550 and rotates through 360 degrees per period; affine rendering is deferred.</summary>
|
||||
public void SetRotationCycle(long handle, long periodMs, (long X, long Y, long Z) axis)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var o = GetOrCreate(handle);
|
||||
o.RotationPeriodMs = periodMs; o.RotationAxis = axis; o.RotationEnabled = periodMs > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x238: set its separate global animation-service duration and reset marker.</summary>
|
||||
public void SetAnimClock(long durationTicks)
|
||||
{
|
||||
lock (_lock) { AnimClockDurationTicks = durationTicks; AnimClockGeneration++; }
|
||||
@@ -227,8 +239,8 @@ public sealed class GfxState
|
||||
public IReadOnlyList<RenderObject> SnapshotVisibleObjects() => SnapshotVisibleObjects(0);
|
||||
|
||||
/// <summary>Visible objects in ascending-handle order (= z-order), each with its source surface resolved
|
||||
/// and its active anim channels interpolated at <paramref name="nowMs"/> (the port of
|
||||
/// gfx_object_anim_interpolate). Position is the base V24 (a direct transform, ops 0x22f/0x229). The
|
||||
/// and its active channels interpolated at <paramref name="nowMs"/>. Position is the base V24 (a direct
|
||||
/// transform, ops 0x22f/0x229); scale and translation are independent one-shot matrix channels. The
|
||||
/// src-rect channel (0x239/0x231) selects the spritesheet cell; the color channel (0x232) ping-pongs the
|
||||
/// alpha/tint. Channel Start fields seed to nowMs on first sight.</summary>
|
||||
public IReadOnlyList<RenderObject> SnapshotVisibleObjects(long nowMs)
|
||||
@@ -280,17 +292,49 @@ public sealed class GfxState
|
||||
w = cellW; h = cellH;
|
||||
}
|
||||
|
||||
// One-shot matrix channels: hold current through delay, then linearly sample current -> target.
|
||||
if ((o.ScaleEnabled || o.TranslationEnabled) && o.MatrixStartMs < 0) o.MatrixStartMs = nowMs;
|
||||
var scale = SampleMatrixChannel(ref o.ScaleCurrent, o.ScaleTarget, o.ScaleDelayMs,
|
||||
o.ScaleDurationMs, o.MatrixStartMs, ref o.ScaleEnabled, nowMs);
|
||||
var translation = SampleMatrixChannel(ref o.TranslationCurrent, o.TranslationTarget,
|
||||
o.TranslationDelayMs, o.TranslationDurationMs,
|
||||
o.MatrixStartMs, ref o.TranslationEnabled, nowMs);
|
||||
if (!o.ScaleEnabled && !o.TranslationEnabled) o.MatrixStartMs = -1;
|
||||
|
||||
list.Add(new RenderObject(kv.Key, resId, ck, srcX, srcY, w, h,
|
||||
(int)o.V24.X, (int)o.V24.Y,
|
||||
new AnimState(o.AnimEnabled, o.AnimNormalized,
|
||||
o.AnimTarget.X, o.AnimTarget.Y, o.AnimTarget.Z,
|
||||
o.AnimDurationTicks, o.AnimGeneration),
|
||||
new TransformState(scale.X, scale.Y, scale.Z,
|
||||
translation.X, translation.Y, translation.Z,
|
||||
o.V18.X, o.V18.Y, o.V18.Z),
|
||||
new RotationCycleState(o.RotationEnabled, o.RotationPeriodMs,
|
||||
o.RotationAxis.X, o.RotationAxis.Y,
|
||||
o.RotationAxis.Z),
|
||||
alpha, tint, strength, blend));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
private static (double X, double Y, double Z) SampleMatrixChannel(
|
||||
ref (double X, double Y, double Z) current,
|
||||
(double X, double Y, double Z) target,
|
||||
long delayMs, long durationMs, long startMs, ref bool enabled, long nowMs)
|
||||
{
|
||||
if (!enabled || durationMs <= 0 || startMs < 0) return current;
|
||||
long elapsed = nowMs - startMs - delayMs;
|
||||
if (elapsed <= 0) return current;
|
||||
if (elapsed >= durationMs)
|
||||
{
|
||||
current = target;
|
||||
enabled = false;
|
||||
return current;
|
||||
}
|
||||
double t = (double)elapsed / durationMs;
|
||||
return (current.X + (target.X - current.X) * t,
|
||||
current.Y + (target.Y - current.Y) * t,
|
||||
current.Z + (target.Z - current.Z) * t);
|
||||
}
|
||||
|
||||
/// <summary>Ping-pong interpolation weight in [0,1] toward the target: 0 at cycle start, 1 at half-period.</summary>
|
||||
private static double PingPongWeight(long now, long start, long period)
|
||||
{
|
||||
|
||||
@@ -367,14 +367,14 @@ public sealed class VirtualMachine
|
||||
case "gfx-draw-color": // 0x203 (handle)(v)(alpha)(color) — static alpha/tint
|
||||
Gfx.SetObjectColor(Read(a[0]), GfxState.PackColor(Read(a[2]), Read(a[3]))); return pc + 1;
|
||||
// ---- sprite transform / animation cluster (docs/engine-re.md "0x21c-0x243 ... ANIMATION") ----
|
||||
case "set-anim-transform-abs": // 0x220 (handle)(p1)(p2)(x)(y)(z) — set transform directly
|
||||
Gfx.SetAnimTransform(Read(a[0]), Read(a[1]), Read(a[2]),
|
||||
(Read(a[3]), Read(a[4]), Read(a[5])), normalized: false); return pc + 1;
|
||||
case "set-anim-transform-norm": // 0x21e — same, operands are ~percent (/_DAT_00571c28)
|
||||
Gfx.SetAnimTransform(Read(a[0]), Read(a[1]), Read(a[2]),
|
||||
(Read(a[3]), Read(a[4]), Read(a[5])), normalized: true); return pc + 1;
|
||||
case "anim-start": // 0x234 (handle)(duration)(x)(y)(z) — animate toward target over the global clock
|
||||
Gfx.StartAnim(Read(a[0]), Read(a[1]), (Read(a[2]), Read(a[3]), Read(a[4]))); return pc + 1;
|
||||
case "set-anim-transform-abs": // 0x220 (handle)(delay)(duration)(tx)(ty)(tz)
|
||||
Gfx.SetTranslationChannel(Read(a[0]), Read(a[1]), Read(a[2]),
|
||||
(Read(a[3]), Read(a[4]), Read(a[5]))); return pc + 1;
|
||||
case "set-anim-transform-norm": // 0x21e (handle)(delay)(duration)(sx%)(sy%)(sz%)
|
||||
Gfx.SetScaleChannel(Read(a[0]), Read(a[1]), Read(a[2]),
|
||||
(Read(a[3]), Read(a[4]), Read(a[5]))); return pc + 1;
|
||||
case "anim-start": // 0x234 legacy name: (handle)(period)(axis x/y/z), cyclic rotation channel
|
||||
Gfx.SetRotationCycle(Read(a[0]), Read(a[1]), (Read(a[2]), Read(a[3]), Read(a[4]))); return pc + 1;
|
||||
case "set-anim-clock": // 0x238 (duration) — global, non-blocking (host advances it per-frame)
|
||||
Gfx.SetAnimClock(Read(a[0])); return pc + 1;
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user