feat: add affine rotation rendering and timeline diagnostics
This commit is contained in:
@@ -2,13 +2,17 @@ using System.Linq;
|
||||
|
||||
namespace Age.Engine.Model;
|
||||
|
||||
/// <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>
|
||||
/// <summary>The sampled native one-shot channels carried to the compositor: op 0x21e scale, op 0x21f
|
||||
/// axis-angle rotation, and op 0x220 translation. Z is retained through full 4x4 composition.</summary>
|
||||
public readonly record struct TransformState(double ScaleX, double ScaleY, double ScaleZ,
|
||||
double TranslateX, double TranslateY, double TranslateZ,
|
||||
double AnchorX, double AnchorY, double AnchorZ);
|
||||
double AnchorX, double AnchorY, double AnchorZ,
|
||||
double RotationAxisX = 0, double RotationAxisY = 0,
|
||||
double RotationAxisZ = 0, double RotationAngleDegrees = 0);
|
||||
|
||||
public readonly record struct RotationCycleState(bool Enabled, long PeriodMs, long AxisX, long AxisY, long AxisZ);
|
||||
public readonly record struct RotationCycleState(bool Enabled, long PeriodMs,
|
||||
double AxisX, double AxisY, double AxisZ,
|
||||
double AngleDegrees = 0);
|
||||
|
||||
/// <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
|
||||
@@ -60,6 +64,10 @@ public sealed class GfxState
|
||||
public (double X, double Y, double Z) TranslationCurrent, TranslationTarget;
|
||||
public long TranslationDelayMs, TranslationDurationMs;
|
||||
public bool TranslationEnabled;
|
||||
public (double X, double Y, double Z, double Angle) RotationCurrent;
|
||||
public (double X, double Y, double Z, double Angle) RotationTarget;
|
||||
public long RotationDelayMs, RotationDurationMs;
|
||||
public bool RotationChannelEnabled;
|
||||
// Shared matrix-channel start timestamp obj+0x34, seeded from frame-time ctx+0xb550.
|
||||
public long MatrixStartMs = -1;
|
||||
|
||||
@@ -67,6 +75,7 @@ public sealed class GfxState
|
||||
public long RotationPeriodMs;
|
||||
public (long X, long Y, long Z) RotationAxis;
|
||||
public bool RotationEnabled;
|
||||
public long RotationStartMs = -1;
|
||||
}
|
||||
|
||||
// ---- geometry/draw object store (V18/V24/draw bind, the compositor's input) ----
|
||||
@@ -217,6 +226,19 @@ public sealed class GfxState
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x21f: delayed one-shot axis-angle rotation target, sharing obj+0x34's start timestamp.</summary>
|
||||
public void SetRotationChannel(long handle, long delayMs, long durationMs,
|
||||
(long X, long Y, long Z) axis, long angleDegrees)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var o = GetOrCreate(handle);
|
||||
o.RotationDelayMs = delayMs; o.RotationDurationMs = durationMs;
|
||||
o.RotationTarget = (axis.X, axis.Y, axis.Z, angleDegrees);
|
||||
o.RotationChannelEnabled = durationMs > 0; o.MatrixStartMs = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
@@ -225,6 +247,7 @@ public sealed class GfxState
|
||||
{
|
||||
var o = GetOrCreate(handle);
|
||||
o.RotationPeriodMs = periodMs; o.RotationAxis = axis; o.RotationEnabled = periodMs > 0;
|
||||
o.RotationStartMs = -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,22 +322,35 @@ public sealed class GfxState
|
||||
}
|
||||
|
||||
// One-shot matrix channels: hold current through delay, then linearly sample current -> target.
|
||||
if ((o.ScaleEnabled || o.TranslationEnabled) && o.MatrixStartMs < 0) o.MatrixStartMs = nowMs;
|
||||
if ((o.ScaleEnabled || o.RotationChannelEnabled || 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 rotation = SampleRotationChannel(ref o.RotationCurrent, o.RotationTarget,
|
||||
o.RotationDelayMs, o.RotationDurationMs,
|
||||
o.MatrixStartMs, ref o.RotationChannelEnabled, 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;
|
||||
if (!o.ScaleEnabled && !o.RotationChannelEnabled && !o.TranslationEnabled) o.MatrixStartMs = -1;
|
||||
|
||||
double cycleAngle = 0;
|
||||
if (o.RotationEnabled && o.RotationPeriodMs > 0)
|
||||
{
|
||||
if (o.RotationStartMs < 0) o.RotationStartMs = nowMs;
|
||||
long elapsed = System.Math.Max(0, nowMs - o.RotationStartMs);
|
||||
cycleAngle = ((elapsed % o.RotationPeriodMs) * 360) / o.RotationPeriodMs;
|
||||
}
|
||||
|
||||
list.Add(new RenderObject(kv.Key, resId, ck, srcX, srcY, w, h,
|
||||
(int)o.V24.X, (int)o.V24.Y,
|
||||
new TransformState(scale.X, scale.Y, scale.Z,
|
||||
translation.X, translation.Y, translation.Z,
|
||||
o.V18.X, o.V18.Y, o.V18.Z),
|
||||
o.V18.X, o.V18.Y, o.V18.Z,
|
||||
rotation.X, rotation.Y, rotation.Z, rotation.Angle),
|
||||
new RotationCycleState(o.RotationEnabled, o.RotationPeriodMs,
|
||||
o.RotationAxis.X, o.RotationAxis.Y,
|
||||
o.RotationAxis.Z),
|
||||
o.RotationAxis.Z, cycleAngle),
|
||||
alpha, tint, strength, blend));
|
||||
}
|
||||
return list;
|
||||
@@ -341,6 +377,22 @@ public sealed class GfxState
|
||||
current.Z + (target.Z - current.Z) * t);
|
||||
}
|
||||
|
||||
private static (double X, double Y, double Z, double Angle) SampleRotationChannel(
|
||||
ref (double X, double Y, double Z, double Angle) current,
|
||||
(double X, double Y, double Z, double Angle) 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,
|
||||
current.Angle + (target.Angle - current.Angle) * 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)
|
||||
{
|
||||
|
||||
54
engine/Age.Engine/Model/SoftwareAffineRasterizer.cs
Normal file
54
engine/Age.Engine/Model/SoftwareAffineRasterizer.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
namespace Age.Engine.Model;
|
||||
|
||||
/// <summary>Nearest-neighbour inverse-mapped RGBA8 affine compositor used by the Godot host and pure tests.</summary>
|
||||
public static class SoftwareAffineRasterizer
|
||||
{
|
||||
public static void BlitRgba(byte[] dst, int dstW, int dstH, byte[] src, int srcW, int srcH,
|
||||
int srcX, int srcY, int width, int height, Affine2D localToDest,
|
||||
long tint, float tintStrength, float opacity)
|
||||
{
|
||||
if (width <= 0 || height <= 0 || !localToDest.TryInverse(out var inv)) return;
|
||||
Bounds(localToDest, width, height, dstW, dstH, out int x0, out int y0, out int x1, out int y1);
|
||||
int istr = (int)(System.Math.Clamp(tintStrength, 0f, 1f) * 255);
|
||||
int ia = (int)(System.Math.Clamp(opacity, 0f, 1f) * 255);
|
||||
int tr=(int)(tint>>16&255), tg=(int)(tint>>8&255), tb=(int)(tint&255);
|
||||
for (int y=y0; y<y1; y++) for (int x=x0; x<x1; x++)
|
||||
{
|
||||
var p = inv.Apply(x + 0.5, y + 0.5);
|
||||
int u=(int)System.Math.Floor(p.X), v=(int)System.Math.Floor(p.Y);
|
||||
if ((uint)u >= (uint)width || (uint)v >= (uint)height) continue;
|
||||
int si=((srcY+v)*srcW+(srcX+u))*4, di=(y*dstW+x)*4;
|
||||
int sa=src[si+3]*ia/255; if(sa==0) continue;
|
||||
int sr=(src[si]*(255-istr)+tr*istr)/255;
|
||||
int sg=(src[si+1]*(255-istr)+tg*istr)/255;
|
||||
int sb=(src[si+2]*(255-istr)+tb*istr)/255;
|
||||
Blend(dst,di,sr,sg,sb,sa);
|
||||
}
|
||||
}
|
||||
|
||||
public static void FillRgba(byte[] dst, int dstW, int dstH, int width, int height,
|
||||
Affine2D localToDest, long color, float opacity)
|
||||
{
|
||||
if (width <= 0 || height <= 0 || !localToDest.TryInverse(out var inv)) return;
|
||||
Bounds(localToDest,width,height,dstW,dstH,out int x0,out int y0,out int x1,out int y1);
|
||||
int a=(int)(System.Math.Clamp(opacity,0f,1f)*255); if(a==0)return;
|
||||
int r=(int)(color>>16&255),g=(int)(color>>8&255),b=(int)(color&255);
|
||||
for(int y=y0;y<y1;y++)for(int x=x0;x<x1;x++){
|
||||
var p=inv.Apply(x+.5,y+.5);
|
||||
if(p.X>=0&&p.X<width&&p.Y>=0&&p.Y<height) Blend(dst,(y*dstW+x)*4,r,g,b,a);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Bounds(Affine2D m,int w,int h,int dw,int dh,out int x0,out int y0,out int x1,out int y1)
|
||||
{
|
||||
var a=m.Apply(0,0);var b=m.Apply(w,0);var c=m.Apply(0,h);var d=m.Apply(w,h);
|
||||
x0=System.Math.Max(0,(int)System.Math.Floor(System.Math.Min(System.Math.Min(a.X,b.X),System.Math.Min(c.X,d.X))));
|
||||
y0=System.Math.Max(0,(int)System.Math.Floor(System.Math.Min(System.Math.Min(a.Y,b.Y),System.Math.Min(c.Y,d.Y))));
|
||||
x1=System.Math.Min(dw,(int)System.Math.Ceiling(System.Math.Max(System.Math.Max(a.X,b.X),System.Math.Max(c.X,d.X))));
|
||||
y1=System.Math.Min(dh,(int)System.Math.Ceiling(System.Math.Max(System.Math.Max(a.Y,b.Y),System.Math.Max(c.Y,d.Y))));
|
||||
}
|
||||
private static void Blend(byte[] d,int i,int r,int g,int b,int a){
|
||||
d[i]=(byte)((r*a+d[i]*(255-a))/255);d[i+1]=(byte)((g*a+d[i+1]*(255-a))/255);
|
||||
d[i+2]=(byte)((b*a+d[i+2]*(255-a))/255);d[i+3]=(byte)System.Math.Min(255,d[i+3]+a);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,74 @@
|
||||
namespace Age.Engine.Model;
|
||||
|
||||
/// <summary>The axis-aligned 2D reduction of AGE's row-vector object matrix. Native composition is
|
||||
/// T(-anchor) * scale * middle(rotation) * translation * T(anchor). With rotation deferred, a point is
|
||||
/// therefore anchor + (point-anchor)*scale + translation; Z remains a retained 3D channel, not opacity.</summary>
|
||||
/// <summary>A projected row-vector affine transform: x'=x*XX+y*YX+TX, y'=x*XY+y*YY+TY.</summary>
|
||||
public readonly record struct Affine2D(double XX, double XY, double YX, double YY, double TX, double TY)
|
||||
{
|
||||
public (double X, double Y) Apply(double x, double y)
|
||||
=> (x * XX + y * YX + TX, x * XY + y * YY + TY);
|
||||
|
||||
public Affine2D FromLocalOrigin(double worldX, double worldY)
|
||||
{
|
||||
var p = Apply(worldX, worldY);
|
||||
return new(XX, XY, YX, YY, p.X, p.Y);
|
||||
}
|
||||
|
||||
public bool TryInverse(out Affine2D inverse)
|
||||
{
|
||||
double det = XX * YY - XY * YX;
|
||||
if (System.Math.Abs(det) < 1e-12) { inverse = default; return false; }
|
||||
double xx = YY / det, xy = -XY / det, yx = -YX / det, yy = XX / det;
|
||||
inverse = new(xx, xy, yx, yy, -(TX * xx + TY * yx), -(TX * xy + TY * yy));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Exact 2D projection of AGE's row-vector retained-object matrix. Native call order is anchored
|
||||
/// scale, one-shot axis-angle rotation, translation, then separately anchored cyclic rotation. The adjacent
|
||||
/// anchor translations cancel, yielding T(-a)*S*R1*T*Rcycle*T(+a). Z is projected away only afterward.</summary>
|
||||
public static class Transform2DMath
|
||||
{
|
||||
public static (double X, double Y) Apply(double x, double y, TransformState transform)
|
||||
=> (transform.AnchorX + (x - transform.AnchorX) * transform.ScaleX + transform.TranslateX,
|
||||
transform.AnchorY + (y - transform.AnchorY) * transform.ScaleY + transform.TranslateY);
|
||||
public static Affine2D Build(TransformState t, RotationCycleState cycle = default)
|
||||
{
|
||||
double[] m = Identity();
|
||||
m = Mul(m, Translation(-t.AnchorX, -t.AnchorY, -t.AnchorZ));
|
||||
m = Mul(m, Scale(t.ScaleX, t.ScaleY, t.ScaleZ));
|
||||
m = Mul(m, AxisAngle(t.RotationAxisX, t.RotationAxisY, t.RotationAxisZ, t.RotationAngleDegrees));
|
||||
m = Mul(m, Translation(t.TranslateX, t.TranslateY, t.TranslateZ));
|
||||
if (cycle.Enabled) m = Mul(m, AxisAngle(cycle.AxisX, cycle.AxisY, cycle.AxisZ, cycle.AngleDegrees));
|
||||
m = Mul(m, Translation(t.AnchorX, t.AnchorY, t.AnchorZ));
|
||||
return new(m[0], m[1], m[4], m[5], m[12], m[13]);
|
||||
}
|
||||
|
||||
public static (double X, double Y) Apply(double x, double y, TransformState transform,
|
||||
RotationCycleState cycle = default)
|
||||
=> Build(transform, cycle).Apply(x, y);
|
||||
|
||||
private static double[] Identity() => new double[] { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 };
|
||||
private static double[] Scale(double x, double y, double z)
|
||||
=> new double[] { x,0,0,0, 0,y,0,0, 0,0,z,0, 0,0,0,1 };
|
||||
private static double[] Translation(double x, double y, double z)
|
||||
=> new double[] { 1,0,0,0, 0,1,0,0, 0,0,1,0, x,y,z,1 };
|
||||
|
||||
private static double[] AxisAngle(double x, double y, double z, double degrees)
|
||||
{
|
||||
double len = System.Math.Sqrt(x*x + y*y + z*z);
|
||||
if (len < 1e-12 || System.Math.Abs(degrees) < 1e-12) return Identity();
|
||||
x /= len; y /= len; z /= len;
|
||||
double r = degrees * System.Math.PI / 180.0, c = System.Math.Cos(r), s = System.Math.Sin(r), q = 1-c;
|
||||
return new double[] {
|
||||
x*x*q+c, x*y*q+z*s, x*z*q-y*s, 0,
|
||||
x*y*q-z*s, y*y*q+c, y*z*q+x*s, 0,
|
||||
x*z*q+y*s, y*z*q-x*s, z*z*q+c, 0,
|
||||
0,0,0,1
|
||||
};
|
||||
}
|
||||
|
||||
private static double[] Mul(double[] a, double[] b)
|
||||
{
|
||||
var o = new double[16];
|
||||
for (int row=0; row<4; row++)
|
||||
for (int col=0; col<4; col++)
|
||||
for (int k=0; k<4; k++) o[row*4+col] += a[row*4+k] * b[k*4+col];
|
||||
return o;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,6 +373,9 @@ public sealed class VirtualMachine
|
||||
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 "set-anim-rotation-axis-angle": // 0x21f (handle)(delay)(duration)(axis x/y/z)(angle deg)
|
||||
Gfx.SetRotationChannel(Read(a[0]), Read(a[1]), Read(a[2]),
|
||||
(Read(a[3]), Read(a[4]), Read(a[5])), Read(a[6])); 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)
|
||||
|
||||
Reference in New Issue
Block a user