mirror of
https://github.com/ZuneDev/MicrosoftIris.git
synced 2026-07-27 13:13:29 -07:00
Implement actual animation curves
This commit is contained in:
@@ -3,9 +3,21 @@ using System;
|
||||
namespace Microsoft.Iris.Render.OpenGL
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps a keyframe's <see cref="AnimationInterpolation"/> to an eased parameter.
|
||||
/// The original curves are evaluated in native code, so these are standard easings
|
||||
/// matching each curve's name/family (see logs/UIX.RenderApi.OpenGL/Implementation.md).
|
||||
/// Maps a keyframe's <see cref="AnimationInterpolation"/> to an eased factor in
|
||||
/// [0,1] for a normalized segment position <c>t</c>. The eased factor is then used
|
||||
/// to combine the two keyframe endpoint values (linear lerp, or slerp when
|
||||
/// <see cref="AnimationInterpolation.UseSphericalCombination"/> is set).
|
||||
///
|
||||
/// The formulas below were recovered from the original native Splash engine
|
||||
/// (UIXrender.dll) via Ghidra and are exact, not approximations — see
|
||||
/// logs/UIX.RenderApi.OpenGL/Implementation.md (2026-07-25 "Animation curve
|
||||
/// formulas RECOVERED from native").
|
||||
///
|
||||
/// NOTE: <see cref="EaseInInterpolation"/> and <see cref="EaseOutInterpolation"/>
|
||||
/// are value-space curves in the original — they build a computed intermediate
|
||||
/// control value between the endpoints and split the segment at <c>Handle</c>,
|
||||
/// so they cannot be reproduced exactly by a scalar factor fed to a straight
|
||||
/// A→B lerp. They are approximated here; see the TODO on those cases.
|
||||
/// </summary>
|
||||
internal static class AnimationEasing
|
||||
{
|
||||
@@ -14,21 +26,72 @@ namespace Microsoft.Iris.Render.OpenGL
|
||||
if (t <= 0f) return 0f;
|
||||
if (t >= 1f) return 1f;
|
||||
|
||||
return interpolation switch
|
||||
switch (interpolation)
|
||||
{
|
||||
LinearInterpolation => t,
|
||||
EaseInInterpolation => t * t,
|
||||
EaseOutInterpolation => t * (2f - t),
|
||||
SCurveInterpolation => t * t * (3f - 2f * t),
|
||||
SineInterpolation => 0.5f * (1f - (float)Math.Cos(Math.PI * t)),
|
||||
CosineInterpolation => 1f - (float)Math.Cos(t * (Math.PI / 2.0)),
|
||||
ExponentialInterpolation => (float)Math.Pow(2.0, 10.0 * (t - 1.0)),
|
||||
LogarithmicInterpolation => 1f - (float)Math.Pow(2.0, -10.0 * t),
|
||||
// Bezier control points are internal to the curve; smoothstep is a
|
||||
// reasonable stand-in until they can be read.
|
||||
BezierInterpolation => t * t * (3f - 2f * t),
|
||||
_ => t,
|
||||
};
|
||||
case LinearInterpolation:
|
||||
return t;
|
||||
|
||||
// f = sin(t·π/2) (ease-out shape)
|
||||
case SineInterpolation:
|
||||
return (float)Math.Sin(t * (Math.PI / 2.0));
|
||||
|
||||
// f = 1 − cos(t·π/2) (native: sin((t−1)·π/2) + 1; ease-in shape)
|
||||
case CosineInterpolation:
|
||||
return 1f - (float)Math.Cos(t * (Math.PI / 2.0));
|
||||
|
||||
// f = ExpEase(t, Weight)
|
||||
case ExponentialInterpolation exp:
|
||||
return (float)ExpEase(t, exp.Weight);
|
||||
|
||||
// f = ExpEase(t, 1/Weight) (reciprocal exponent of Exponential)
|
||||
case LogarithmicInterpolation log:
|
||||
return (float)ExpEase(t, 1.0 / log.Weight);
|
||||
|
||||
// Symmetric S built from the weighted-exponential ease.
|
||||
case SCurveInterpolation sc:
|
||||
return t < 0.5f
|
||||
? (float)(0.5 * ExpEase(2.0 * t, sc.Weight))
|
||||
: (float)(0.5 + 0.5 * ExpEase(2.0 * (t - 0.5), 1.0 / sc.Weight));
|
||||
|
||||
// Quintic Bézier (Bernstein degree 5) with control values
|
||||
// P0=0, P1=0, P2=cp1, P3=cp2, P4=1, P5=1.
|
||||
case BezierInterpolation bez:
|
||||
{
|
||||
double u = 1.0 - t;
|
||||
double t2 = t * t, t3 = t2 * t, t4 = t3 * t, t5 = t4 * t;
|
||||
double u2 = u * u, u3 = u2 * u;
|
||||
return (float)(10.0 * bez.ControlPoint1 * u3 * t2
|
||||
+ 10.0 * bez.ControlPoint2 * u2 * t3
|
||||
+ 5.0 * u * t4
|
||||
+ t5);
|
||||
}
|
||||
|
||||
// TODO: EaseIn/EaseOut are value-space in the original (they insert a
|
||||
// computed intermediate control value and split the segment at Handle;
|
||||
// see the log). A scalar factor cannot reproduce them exactly. As a
|
||||
// reasonable stand-in, use the first/second half of the weighted-exp
|
||||
// ease. Wiring the true value-space behavior needs changes in
|
||||
// GLKeyframeAnimation (compute the control value, pick the sub-segment).
|
||||
case EaseInInterpolation ein:
|
||||
return (float)ExpEase(t, ein.Weight);
|
||||
case EaseOutInterpolation eout:
|
||||
return (float)ExpEase(t, 1.0 / eout.Weight);
|
||||
|
||||
default:
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The native weighted-exponential ease (UIXrender.dll <c>FUN_310bbf90</c>):
|
||||
/// <c>(w^x − 1) / (w − 1)</c>, collapsing to the identity when <c>w == 1</c>.
|
||||
/// Underlies Exponential, Logarithmic and SCurve.
|
||||
/// </summary>
|
||||
private static double ExpEase(double x, double w)
|
||||
{
|
||||
if (w == 1.0)
|
||||
return x;
|
||||
return (Math.Pow(w, x) - 1.0) / (w - 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
// The in-process OpenGL render engine re-implements the Splash animation curve
|
||||
// evaluation that originally lived in native UIXrender.dll. To apply the real
|
||||
// curves it must read the interpolation parameters (Weight / Handle /
|
||||
// ControlPoint1 / ControlPoint2) that the original API keeps `internal`. Grant
|
||||
// the sibling assembly access rather than widening the original public surface.
|
||||
// See logs/UIX.RenderApi.OpenGL/Implementation.md (2026-07-25 curve recovery).
|
||||
[assembly: InternalsVisibleTo("UIX.RenderApi.OpenGL")]
|
||||
@@ -2,6 +2,89 @@
|
||||
|
||||
Reverse-chronological log (prepend new entries; never edit older ones).
|
||||
|
||||
## 2026-07-25 — Animation curve formulas RECOVERED from native (Ghidra)
|
||||
|
||||
Resolves the "unverifiable — the real curves/formulas run in native code" caveat
|
||||
from the entry below. The formulas are now **verified**, not assumed. Source:
|
||||
`UIXrender.dll` (the native Splash render engine) in the `ZuneDesktop` Ghidra
|
||||
project. All addresses below are in that image.
|
||||
|
||||
### How the pieces fit (verified end-to-end)
|
||||
- Managed `KeyframeAnimation.SendInterpolation` (UIX.renderapi.dll) does NOT compute
|
||||
curves; it sends a distinct, parameterized message per curve type to native
|
||||
`RemoteAnimation`. Opcodes (from `RemoteAnimation` Msg structs):
|
||||
8=SetEaseOut, 9=SetEaseIn, 10=SetBezier, 11=SetCosine, 12=SetSine,
|
||||
13=SetSCurve, 14=SetLogarithmic, 15=SetExponential, 16=SetLinear.
|
||||
Params carried: Exp/Log/SCurve → `flWeight`; EaseIn/Out → `flWeight`+`flHandle`;
|
||||
Bezier → `flHandle1`+`flHandle2` (= ControlPoint1/2); Sine/Cosine/Linear → none.
|
||||
Every message also carries `fSpherical` (→ `UseSphericalCombination`).
|
||||
- Native message dispatch table for the Animation class is at `0x311f9d90`
|
||||
(indexed by opcode). Handler[8..16] each allocate a small C++ "interpolation"
|
||||
object, store its params (weight @obj+0x10, handle/cp2 @obj+0x14), set a
|
||||
per-type vtable, and stash it in the keyframe array element (stride 0x18) at
|
||||
`keyframe+0x10`. Spherical flag → bit 0 of `obj+0xc`.
|
||||
- Each interpolation object's vtable slot 1 is its **evaluate** method with
|
||||
signature `eval(obj, float t, uint channelCount, float* A, float* B, float* out)`.
|
||||
`t` is the already-normalized segment fraction [0,1]; A/B are the two keyframe
|
||||
endpoint value vectors (up to 4 floats). Evaluate computes an eased factor `f`
|
||||
then calls the **combine** routine: `FUN_310bb984` = linear `out = (1-f)·A + f·B`
|
||||
(per component), or `FUN_310bba70` = **slerp** `out = (sin((1-f)Ω)·A + sin(fΩ)·B)/sinΩ`,
|
||||
`Ω = acos(dot(Â,B̂))`, normalized per channel count, lerp fallback when Ω≈0
|
||||
(used when `UseSphericalCombination`).
|
||||
|
||||
### The core weighted-exponential ease (`FUN_310bbf90`)
|
||||
```
|
||||
ExpEase(x, w) = (w == 1) ? x : (pow(w, x) - 1) / (w - 1)
|
||||
```
|
||||
This single function underlies Exponential, Logarithmic, and SCurve.
|
||||
|
||||
### Per-type eased factor `f(t)` (verified)
|
||||
- **Linear** (`FUN_310bbee0`): f = t
|
||||
- **Sine** (`FUN_310bc128`): f = sin(t · π/2) ← ease-out shape
|
||||
- **Cosine** (`FUN_310bc1a4`): f = sin((t−1)·π/2) + 1 = 1 − cos(t·π/2) ← ease-in shape
|
||||
- **Exponential(w)** (`FUN_310bbf1c`): f = ExpEase(t, w) (w = Weight, >0)
|
||||
- **Logarithmic(w)** (`FUN_310bbfdc`): f = ExpEase(t, 1/w) (reciprocal exponent)
|
||||
- **SCurve(w)** (`FUN_310bc05c`):
|
||||
t < 0.5 : f = 0.5 · ExpEase(2t, w)
|
||||
t ≥ 0.5 : f = 0.5 + 0.5 · ExpEase(2(t−0.5), 1/w) (symmetric S)
|
||||
- **Bezier(cp1, cp2)** (`FUN_310bc230`), u = 1−t — a **quintic Bézier** (Bernstein
|
||||
degree 5) easing with control values P0=0, P1=0, P2=cp1, P3=cp2, P4=1, P5=1:
|
||||
f = 10·cp1·u³·t² + 10·cp2·u²·t³ + 5·u·t⁴ + t⁵
|
||||
(`π` constant used by Sine/Cosine is the float `3.1415927`, not a double.)
|
||||
|
||||
### EaseIn / EaseOut are VALUE-SPACE, not scalar (`FUN_310bc3d0` / `0x310b8948`)
|
||||
These do NOT remap `t` and lerp straight A→B. They split the segment at time
|
||||
`handle` (h, 0<h<1) around a **computed intermediate control value** `mid`:
|
||||
```
|
||||
d = ExpEase(0.99, w)
|
||||
d = (1 − d) · h
|
||||
ctrl[] = (d / ((1−h)·0.01 + d)) · (B − A) // per component
|
||||
mid[] = A + ctrl // intermediate control value
|
||||
|
||||
if (t >= h): f = ExpEase((t−h)/(1−h), 1/w); combine(mid, B, f)
|
||||
else: f = t / h; combine(A, mid, f)
|
||||
```
|
||||
EaseOut (`0x310b8948`, vtable `0x3108b6f8`) is the mirror. Consequence for our
|
||||
renderer: EaseIn/EaseOut **cannot** be expressed as a scalar `Ease(interp, t)`
|
||||
fed to a plain A→B lerp — they need `mid` computed in value space and a
|
||||
sub-segment choice. Flagged in code; the scalar path handles the other 7 types
|
||||
exactly.
|
||||
|
||||
### Native vtables (for future reference)
|
||||
Linear `0x3108b678`, SCurve `0x3108b6a8`, Sine `0x3108b6b8`, Cosine `0x3108b6c8`,
|
||||
Bezier `0x3108b6d8`, Exponential `0x3108b688`, Logarithmic (shares ExpEase via
|
||||
1/w), EaseIn `0x3108b6e8`, EaseOut `0x3108b6f8`.
|
||||
|
||||
### Also confirmed while here
|
||||
- Keyframe time is **seconds** on the wire; native converts to ms via `×1000`
|
||||
then rounds to int (e.g. AddTimeEvent handler `0x310b853c`: `FUN_310e7d28(t*1000.0)`).
|
||||
Matches the existing time-unit assumption.
|
||||
- Interpolation belongs to a **segment**, keyed by keyframe index
|
||||
(`idxKeyframe = keyframeIndex-1` when !BackCompat, else `keyframeIndex`). Our
|
||||
evaluator currently reads `b.Interpolation` (the segment's END keyframe); native
|
||||
stores per keyframe slot — the exact start-vs-end association for BackCompat is
|
||||
still worth a targeted check but does not affect the formulas above.
|
||||
|
||||
## 2026-07-25 — Real keyframe animation evaluation
|
||||
|
||||
Replaced the no-op animation stubs with a working evaluator. `GLKeyframeAnimation`
|
||||
|
||||
Reference in New Issue
Block a user