mirror of
https://github.com/ZuneDev/MicrosoftIris.git
synced 2026-07-27 13:13:29 -07:00
Implement keyframed animations
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Iris.Render.OpenGL
|
||||
{
|
||||
/// <summary>
|
||||
/// A resolved animation value as up to four float channels plus its logical type.
|
||||
/// Reads keyframe inputs through the public <see cref="AnimationInput.TryGetConstantValue"/>
|
||||
/// accessor and folds <see cref="BinaryOperation"/> expressions over their public operands.
|
||||
/// Object/continuous inputs (whose value depends on a live render object) are not
|
||||
/// resolvable here and report failure so the caller can hold the previous value.
|
||||
/// </summary>
|
||||
internal readonly struct AnimValue
|
||||
{
|
||||
public readonly AnimationInputType Type;
|
||||
public readonly float X, Y, Z, W;
|
||||
|
||||
public AnimValue(AnimationInputType type, float x, float y, float z, float w)
|
||||
{
|
||||
Type = type;
|
||||
X = x;
|
||||
Y = y;
|
||||
Z = z;
|
||||
W = w;
|
||||
}
|
||||
|
||||
public static bool TryRead(AnimationInput input, out AnimValue value)
|
||||
{
|
||||
value = default;
|
||||
if (input == null)
|
||||
return false;
|
||||
|
||||
if (input is BinaryOperation op)
|
||||
{
|
||||
if (!TryRead(op.LeftOperand, out AnimValue l) || !TryRead(op.RightOperand, out AnimValue r))
|
||||
return false;
|
||||
value = op.Operation == BinaryOpCode.Multiply
|
||||
? new AnimValue(l.Type, l.X * r.X, l.Y * r.Y, l.Z * r.Z, l.W * r.W)
|
||||
: new AnimValue(l.Type, l.X + r.X, l.Y + r.Y, l.Z + r.Z, l.W + r.W);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!input.TryGetConstantValue(out object raw) || raw == null)
|
||||
return false;
|
||||
|
||||
switch (raw)
|
||||
{
|
||||
case float f: value = new AnimValue(input.InputType, f, 0f, 0f, 0f); return true;
|
||||
case Vector2 v2: value = new AnimValue(input.InputType, v2.X, v2.Y, 0f, 0f); return true;
|
||||
case Vector3 v3: value = new AnimValue(input.InputType, v3.X, v3.Y, v3.Z, 0f); return true;
|
||||
case Vector4 v4: value = new AnimValue(input.InputType, v4.X, v4.Y, v4.Z, v4.W); return true;
|
||||
case Quaternion q: value = new AnimValue(input.InputType, q.X, q.Y, q.Z, q.W); return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Interpolate between two values by t∈[0,1]; quaternions use slerp.</summary>
|
||||
public static AnimValue Lerp(AnimValue a, AnimValue b, float t, bool spherical)
|
||||
{
|
||||
if (a.Type == AnimationInputType.Quaternion && (spherical || b.Type == AnimationInputType.Quaternion))
|
||||
return Slerp(a, b, t);
|
||||
return new AnimValue(
|
||||
a.Type,
|
||||
a.X + (b.X - a.X) * t,
|
||||
a.Y + (b.Y - a.Y) * t,
|
||||
a.Z + (b.Z - a.Z) * t,
|
||||
a.W + (b.W - a.W) * t);
|
||||
}
|
||||
|
||||
private static AnimValue Slerp(AnimValue a, AnimValue b, float t)
|
||||
{
|
||||
float dot = a.X * b.X + a.Y * b.Y + a.Z * b.Z + a.W * b.W;
|
||||
float bx = b.X, by = b.Y, bz = b.Z, bw = b.W;
|
||||
if (dot < 0f)
|
||||
{
|
||||
dot = -dot;
|
||||
bx = -bx; by = -by; bz = -bz; bw = -bw;
|
||||
}
|
||||
|
||||
float wa, wb;
|
||||
if (dot > 0.9995f)
|
||||
{
|
||||
wa = 1f - t;
|
||||
wb = t;
|
||||
}
|
||||
else
|
||||
{
|
||||
float theta = (float)Math.Acos(dot);
|
||||
float sin = (float)Math.Sin(theta);
|
||||
wa = (float)Math.Sin((1f - t) * theta) / sin;
|
||||
wb = (float)Math.Sin(t * theta) / sin;
|
||||
}
|
||||
return new AnimValue(
|
||||
AnimationInputType.Quaternion,
|
||||
a.X * wa + bx * wb,
|
||||
a.Y * wa + by * wb,
|
||||
a.Z * wa + bz * wb,
|
||||
a.W * wa + bw * wb);
|
||||
}
|
||||
|
||||
public float AsFloat() => X;
|
||||
public Vector2 AsVector2() => new Vector2(X, Y);
|
||||
public Vector3 AsVector3() => new Vector3(X, Y, Z);
|
||||
public Vector4 AsVector4() => new Vector4(X, Y, Z, W);
|
||||
|
||||
public float GetChannel(int index) => index switch { 0 => X, 1 => Y, 2 => Z, _ => W };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
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).
|
||||
/// </summary>
|
||||
internal static class AnimationEasing
|
||||
{
|
||||
public static float Ease(AnimationInterpolation? interpolation, float t)
|
||||
{
|
||||
if (t <= 0f) return 0f;
|
||||
if (t >= 1f) return 1f;
|
||||
|
||||
return interpolation switch
|
||||
{
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Iris.Render.OpenGL
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes a resolved <see cref="AnimValue"/> onto a target render object's named
|
||||
/// animatable property, honoring an optional channel mask. Property names/types match
|
||||
/// the original render objects (Position/Size/Scale/Alpha/Rotation/… on visuals,
|
||||
/// CameraEye/At/Up/Zn on cameras, Offset/ColorMask on gradients, dynamic on effects).
|
||||
/// </summary>
|
||||
internal static class AnimationTargetApplier
|
||||
{
|
||||
public static void Apply(IAnimatable target, string property, string? mask, AnimValue value)
|
||||
{
|
||||
// Effects use dynamic (custom) properties with no readable current value, so
|
||||
// they only support a full write (masks are not applied to them).
|
||||
if (target is GLEffect effect)
|
||||
{
|
||||
ApplyToEffect(effect, property, value);
|
||||
return;
|
||||
}
|
||||
|
||||
AnimationTypeMask channelMask = AnimationTypeMask.FromString(mask);
|
||||
float[] channels;
|
||||
if (channelMask.ChannelCount == 0)
|
||||
{
|
||||
channels = new[] { value.X, value.Y, value.Z, value.W };
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!TryGetChannels(target, property, out channels))
|
||||
return;
|
||||
for (int i = 0; i < channelMask.ChannelCount; i++)
|
||||
{
|
||||
AnimationTypeChannel ch = channelMask[i];
|
||||
if (ch != AnimationTypeChannel.O)
|
||||
channels[(int)ch - 1] = value.GetChannel(i);
|
||||
}
|
||||
}
|
||||
|
||||
SetChannels(target, property, channels);
|
||||
}
|
||||
|
||||
private static void ApplyToEffect(GLEffect effect, string property, AnimValue v)
|
||||
{
|
||||
switch (v.Type)
|
||||
{
|
||||
case AnimationInputType.Float: effect.SetProperty(property, v.AsFloat()); break;
|
||||
case AnimationInputType.Vector2: effect.SetProperty(property, v.AsVector2()); break;
|
||||
case AnimationInputType.Vector3: effect.SetProperty(property, v.AsVector3()); break;
|
||||
default: effect.SetProperty(property, v.AsVector4()); break;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetChannels(IAnimatable target, string property, out float[] channels)
|
||||
{
|
||||
channels = new float[4];
|
||||
switch (target)
|
||||
{
|
||||
case GLVisual visual:
|
||||
switch (property)
|
||||
{
|
||||
case "Position": Store(channels, visual.Position); return true;
|
||||
case "Size": channels[0] = visual.Size.X; channels[1] = visual.Size.Y; return true;
|
||||
case "Scale": Store(channels, visual.Scale); return true;
|
||||
case "Alpha": channels[0] = visual.Alpha; return true;
|
||||
case "CenterPoint": Store(channels, visual.CenterPoint); return true;
|
||||
case "Rotation":
|
||||
Store(channels, visual.Rotation.Axis);
|
||||
channels[3] = visual.Rotation.Angle;
|
||||
return true;
|
||||
default: return false;
|
||||
}
|
||||
case GLCamera camera:
|
||||
switch (property)
|
||||
{
|
||||
case "CameraEye": Store(channels, camera.Eye); return true;
|
||||
case "CameraAt": Store(channels, camera.At); return true;
|
||||
case "CameraUp": Store(channels, camera.Up); return true;
|
||||
case "CameraZn": channels[0] = camera.Zn; return true;
|
||||
default: return false;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetChannels(IAnimatable target, string property, float[] c)
|
||||
{
|
||||
switch (target)
|
||||
{
|
||||
case GLVisual visual:
|
||||
switch (property)
|
||||
{
|
||||
case "Position": visual.Position = new Vector3(c[0], c[1], c[2]); break;
|
||||
case "Size": visual.Size = new Vector2(c[0], c[1]); break;
|
||||
case "Scale": visual.Scale = new Vector3(c[0], c[1], c[2]); break;
|
||||
case "Alpha": visual.Alpha = c[0]; break;
|
||||
case "CenterPoint": visual.CenterPoint = new Vector3(c[0], c[1], c[2]); break;
|
||||
case "Rotation": visual.Rotation = new AxisAngle(new Vector3(c[0], c[1], c[2]), c[3]); break;
|
||||
case "Orientation": visual.Rotation = QuaternionToAxisAngle(c[0], c[1], c[2], c[3]); break;
|
||||
}
|
||||
break;
|
||||
case GLCamera camera:
|
||||
switch (property)
|
||||
{
|
||||
case "CameraEye": camera.Eye = new Vector3(c[0], c[1], c[2]); break;
|
||||
case "CameraAt": camera.At = new Vector3(c[0], c[1], c[2]); break;
|
||||
case "CameraUp": camera.Up = new Vector3(c[0], c[1], c[2]); break;
|
||||
case "CameraZn": camera.Zn = c[0]; break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Store(float[] channels, Vector3 v)
|
||||
{
|
||||
channels[0] = v.X;
|
||||
channels[1] = v.Y;
|
||||
channels[2] = v.Z;
|
||||
}
|
||||
|
||||
private static AxisAngle QuaternionToAxisAngle(float x, float y, float z, float w)
|
||||
{
|
||||
w = w < -1f ? -1f : (w > 1f ? 1f : w);
|
||||
float angle = 2f * (float)Math.Acos(w);
|
||||
float s = (float)Math.Sqrt(Math.Max(0f, 1f - w * w));
|
||||
Vector3 axis = s < 1e-4f ? new Vector3(0f, 0f, 1f) : new Vector3(x / s, y / s, z / s);
|
||||
return new AxisAngle(axis, angle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,14 @@ using System.Collections.Generic;
|
||||
namespace Microsoft.Iris.Render.OpenGL
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared state machine for animations: play/pause/reset, repeat counting and the
|
||||
/// async-notify event. Target property interpolation is intentionally minimal here
|
||||
/// (see <see cref="GLKeyframeAnimation"/>); full per-frame evaluation is a stage-3 TODO.
|
||||
/// Shared state machine for animations: play/pause/reset, repeat and the async-notify
|
||||
/// event. Time evaluation lives in the concrete subclasses (see <see cref="GLKeyframeAnimation"/>).
|
||||
/// </summary>
|
||||
public abstract class GLAnimation : SharedRenderObject, IAnimation
|
||||
{
|
||||
public int RepeatCount { get; set; }
|
||||
public bool IsPlaying { get; private set; }
|
||||
public bool IsActive { get; private set; }
|
||||
public bool IsPlaying { get; protected set; }
|
||||
public bool IsActive { get; protected set; }
|
||||
public bool AutoReset { get; set; }
|
||||
public AnimationResetBehavior ResetBehavior { get; set; } = AnimationResetBehavior.LeaveCurrent;
|
||||
|
||||
@@ -23,7 +22,11 @@ namespace Microsoft.Iris.Render.OpenGL
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
public virtual void Pause() => IsPlaying = false;
|
||||
public virtual void Pause()
|
||||
{
|
||||
if (IsActive)
|
||||
IsPlaying = false;
|
||||
}
|
||||
|
||||
public virtual void Reset()
|
||||
{
|
||||
@@ -41,10 +44,15 @@ namespace Microsoft.Iris.Render.OpenGL
|
||||
|
||||
protected void RaiseAsyncNotify(int cookie) => AsyncNotifyEvent?.Invoke(cookie);
|
||||
|
||||
/// <summary>Advance internal time. Called by the animation system each pulse.</summary>
|
||||
internal virtual void Advance(int advanceMs) { }
|
||||
/// <summary>Advance internal time by <paramref name="advanceMs"/>. Driven by the system's pulse.</summary>
|
||||
internal abstract void Advance(int advanceMs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggregates child animations and drives them together. The public API exposes no way
|
||||
/// to add members (IAnimationGroup has no members beyond IAnimation), so membership is
|
||||
/// only available internally; kept for lifecycle parity.
|
||||
/// </summary>
|
||||
public sealed class GLAnimationGroup : GLAnimation, IAnimationGroup
|
||||
{
|
||||
private readonly List<GLAnimation> m_members = new List<GLAnimation>();
|
||||
@@ -52,15 +60,30 @@ namespace Microsoft.Iris.Render.OpenGL
|
||||
public override void Play()
|
||||
{
|
||||
base.Play();
|
||||
foreach (var a in m_members)
|
||||
foreach (GLAnimation a in m_members)
|
||||
a.Play();
|
||||
}
|
||||
|
||||
public override void Pause()
|
||||
{
|
||||
base.Pause();
|
||||
foreach (GLAnimation a in m_members)
|
||||
a.Pause();
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
foreach (GLAnimation a in m_members)
|
||||
a.Reset();
|
||||
}
|
||||
|
||||
internal void Add(GLAnimation animation) => m_members.Add(animation);
|
||||
|
||||
internal override void Advance(int advanceMs)
|
||||
{
|
||||
foreach (var a in m_members)
|
||||
foreach (GLAnimation a in m_members)
|
||||
if (a.IsPlaying)
|
||||
a.Advance(advanceMs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,20 @@ namespace Microsoft.Iris.Render.OpenGL
|
||||
private readonly List<GLAnimation> m_animations = new List<GLAnimation>();
|
||||
private bool m_paused;
|
||||
|
||||
private bool m_backCompat;
|
||||
|
||||
public int UpdatesPerSecond { get; set; } = 60;
|
||||
public float SpeedAdjustment { get; set; } = 1f;
|
||||
public bool BackCompat { set { /* compatibility flag; no behavioral change */ } }
|
||||
|
||||
// When set, keyframe 0 is not auto-populated with the initial value (matching the
|
||||
// original AnimationSystem.BackCompat behavior).
|
||||
public bool BackCompat { set => m_backCompat = value; }
|
||||
|
||||
public IKeyframeAnimation CreateKeyframeAnimation(object objUser, AnimationInput initialValue)
|
||||
{
|
||||
var a = new GLKeyframeAnimation(initialValue);
|
||||
if (!m_backCompat)
|
||||
a.AddInitialKeyframe();
|
||||
m_animations.Add(a);
|
||||
return a;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,21 @@ using System.Collections.Generic;
|
||||
namespace Microsoft.Iris.Render.OpenGL
|
||||
{
|
||||
/// <summary>
|
||||
/// Keyframe animation. Stores keyframes, targets and events and runs the play-state
|
||||
/// machine. Smooth per-frame evaluation and target property mutation are a stage-3
|
||||
/// TODO; today it drives lifecycle/events so higher layers sequence correctly.
|
||||
/// Time-driven keyframe animation. On each pulse it advances its clock, finds the
|
||||
/// surrounding keyframes, eases and interpolates between their (constant) values and
|
||||
/// writes the result onto every target property. Supports repeat/auto-reset/reset
|
||||
/// behavior and additive/multiplicative Reference/Scale inputs.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Keyframe times are in seconds. Matching the original, keyframe 0 at t=0 holds the
|
||||
/// initial value (added here unless the system is in BackCompat mode). Stage/time/
|
||||
/// progress/value events are recorded but not yet dispatched — see the log for why
|
||||
/// (their targets require the render-internal IActivatableObject).
|
||||
/// </remarks>
|
||||
public sealed class GLKeyframeAnimation : GLAnimation, IKeyframeAnimation
|
||||
{
|
||||
private static readonly LinearInterpolation s_defaultInterpolation = new LinearInterpolation();
|
||||
|
||||
private readonly struct Target
|
||||
{
|
||||
public readonly IAnimatable Object;
|
||||
@@ -25,23 +34,32 @@ namespace Microsoft.Iris.Render.OpenGL
|
||||
private readonly List<AnimationKeyframe> m_keyframes = new List<AnimationKeyframe>();
|
||||
private readonly List<Target> m_targets = new List<Target>();
|
||||
private readonly List<AnimationEvent> m_events = new List<AnimationEvent>();
|
||||
private readonly AnimationInput m_initialValue;
|
||||
|
||||
private float m_timeSec;
|
||||
private int m_loopsCompleted;
|
||||
|
||||
public GLKeyframeAnimation(AnimationInput initialValue)
|
||||
{
|
||||
InitialValue = initialValue;
|
||||
m_initialValue = initialValue;
|
||||
Type = initialValue.InputType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seed keyframe 0 (t=0) with the initial value. The animation system calls this
|
||||
/// unless it is in BackCompat mode, matching the original renderer's constructor.
|
||||
/// </summary>
|
||||
internal void AddInitialKeyframe()
|
||||
=> m_keyframes.Add(new AnimationKeyframe(0f, m_initialValue, s_defaultInterpolation));
|
||||
|
||||
public int KeyframeCount => m_keyframes.Count;
|
||||
public AnimationInput InitialValue { get; }
|
||||
public AnimationInput InitialValue => m_keyframes.Count > 0 ? m_keyframes[0].Value : m_initialValue;
|
||||
public AnimationInput Reference { get; set; } = null!;
|
||||
public AnimationInput Scale { get; set; } = null!;
|
||||
public AnimationInputType Type { get; }
|
||||
|
||||
public void AddKeyframe(AnimationKeyframe keyframe) => m_keyframes.Add(keyframe);
|
||||
|
||||
public AnimationKeyframe GetKeyframe(int keyframeIndex) => m_keyframes[keyframeIndex];
|
||||
|
||||
public void SetKeyframe(int keyframeIndex, AnimationKeyframe keyframe) => m_keyframes[keyframeIndex] = keyframe;
|
||||
|
||||
public void AddTarget(IAnimatable targetObject, string targetProperty)
|
||||
@@ -62,5 +80,183 @@ namespace Microsoft.Iris.Render.OpenGL
|
||||
public void AddValueEvent(ValueEventCondition condition, AnimationInput reference, AnimationEvent animationEvent) => m_events.Add(animationEvent);
|
||||
public void RemoveEvent(AnimationEvent animationEvent) => m_events.Remove(animationEvent);
|
||||
public void RemoveAllEvents() => m_events.Clear();
|
||||
|
||||
// ---- Lifecycle -----------------------------------------------------------
|
||||
public override void Play()
|
||||
{
|
||||
// Replaying after a completed run restarts from the top.
|
||||
if (IsActive && !IsPlaying && m_timeSec >= Duration)
|
||||
{
|
||||
m_timeSec = 0f;
|
||||
m_loopsCompleted = 0;
|
||||
}
|
||||
base.Play();
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
m_timeSec = 0f;
|
||||
m_loopsCompleted = 0;
|
||||
ApplyResetBehavior();
|
||||
}
|
||||
|
||||
public override void InstantAdvance(float advanceTime)
|
||||
{
|
||||
if (advanceTime > 0f)
|
||||
AdvanceBy(advanceTime);
|
||||
}
|
||||
|
||||
public override void InstantFinish()
|
||||
{
|
||||
float duration = Duration;
|
||||
m_timeSec = duration;
|
||||
ApplyAt(duration);
|
||||
IsPlaying = false;
|
||||
m_loopsCompleted = RepeatCount < 0 ? 0 : RepeatCount;
|
||||
if (AutoReset)
|
||||
Reset();
|
||||
}
|
||||
|
||||
internal override void Advance(int advanceMs)
|
||||
{
|
||||
if (IsPlaying && advanceMs > 0)
|
||||
AdvanceBy(advanceMs / 1000f);
|
||||
}
|
||||
|
||||
// ---- Evaluation ----------------------------------------------------------
|
||||
/// <summary>Total animation length in seconds (largest keyframe time).</summary>
|
||||
private float Duration
|
||||
{
|
||||
get
|
||||
{
|
||||
float max = 0f;
|
||||
foreach (AnimationKeyframe k in m_keyframes)
|
||||
if (k.Time > max)
|
||||
max = k.Time;
|
||||
return max;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsInfinite => RepeatCount < 0;
|
||||
|
||||
private void AdvanceBy(float dt)
|
||||
{
|
||||
float duration = Duration;
|
||||
m_timeSec += dt;
|
||||
|
||||
if (duration <= 0f)
|
||||
{
|
||||
ApplyAt(0f);
|
||||
Complete();
|
||||
return;
|
||||
}
|
||||
|
||||
while (m_timeSec >= duration)
|
||||
{
|
||||
if (IsInfinite || m_loopsCompleted < RepeatCount)
|
||||
{
|
||||
m_timeSec -= duration;
|
||||
m_loopsCompleted++;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_timeSec = duration;
|
||||
ApplyAt(duration);
|
||||
Complete();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ApplyAt(m_timeSec);
|
||||
}
|
||||
|
||||
private void Complete()
|
||||
{
|
||||
IsPlaying = false;
|
||||
if (AutoReset)
|
||||
Reset();
|
||||
}
|
||||
|
||||
private void ApplyResetBehavior()
|
||||
{
|
||||
switch (ResetBehavior)
|
||||
{
|
||||
case AnimationResetBehavior.SetInitialValue:
|
||||
ApplyAt(0f);
|
||||
break;
|
||||
case AnimationResetBehavior.SetFinalValue:
|
||||
ApplyAt(Duration);
|
||||
break;
|
||||
// LeaveCurrent: nothing to do.
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyAt(float time)
|
||||
{
|
||||
if (m_keyframes.Count == 0 || m_targets.Count == 0)
|
||||
return;
|
||||
if (!SampleValue(time, out AnimValue value))
|
||||
return;
|
||||
|
||||
value = ApplyReferenceAndScale(value);
|
||||
|
||||
foreach (Target target in m_targets)
|
||||
AnimationTargetApplier.Apply(target.Object, target.Property, target.Mask, value);
|
||||
}
|
||||
|
||||
private bool SampleValue(float time, out AnimValue value)
|
||||
{
|
||||
value = default;
|
||||
|
||||
// Keyframes are authored in time order; sort defensively so out-of-order
|
||||
// additions still evaluate correctly.
|
||||
m_keyframes.Sort((a, b) => a.Time.CompareTo(b.Time));
|
||||
|
||||
if (time <= m_keyframes[0].Time)
|
||||
return AnimValue.TryRead(m_keyframes[0].Value, out value);
|
||||
|
||||
AnimationKeyframe last = m_keyframes[m_keyframes.Count - 1];
|
||||
if (time >= last.Time)
|
||||
return AnimValue.TryRead(last.Value, out value);
|
||||
|
||||
for (int i = 0; i < m_keyframes.Count - 1; i++)
|
||||
{
|
||||
AnimationKeyframe a = m_keyframes[i];
|
||||
AnimationKeyframe b = m_keyframes[i + 1];
|
||||
if (time < a.Time || time > b.Time)
|
||||
continue;
|
||||
|
||||
float span = b.Time - a.Time;
|
||||
float localT = span > 0f ? (time - a.Time) / span : 1f;
|
||||
float eased = AnimationEasing.Ease(b.Interpolation, localT);
|
||||
|
||||
bool haveA = AnimValue.TryRead(a.Value, out AnimValue va);
|
||||
bool haveB = AnimValue.TryRead(b.Value, out AnimValue vb);
|
||||
if (haveA && haveB)
|
||||
{
|
||||
bool spherical = b.Interpolation?.UseSphericalCombination ?? false;
|
||||
value = AnimValue.Lerp(va, vb, eased, spherical);
|
||||
return true;
|
||||
}
|
||||
// If only one endpoint is readable (e.g. the other is object-relative),
|
||||
// hold that endpoint rather than skipping the whole frame.
|
||||
if (haveA) { value = va; return true; }
|
||||
if (haveB) { value = vb; return true; }
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private AnimValue ApplyReferenceAndScale(AnimValue value)
|
||||
{
|
||||
// Effective = Reference + Scale * value (both optional). The exact native
|
||||
// combination is unverified; this is the conventional interpretation.
|
||||
if (Scale != null && AnimValue.TryRead(Scale, out AnimValue s))
|
||||
value = new AnimValue(value.Type, value.X * s.X, value.Y * s.Y, value.Z * s.Z, value.W * s.W);
|
||||
if (Reference != null && AnimValue.TryRead(Reference, out AnimValue r))
|
||||
value = new AnimValue(value.Type, value.X + r.X, value.Y + r.Y, value.Z + r.Z, value.W + r.W);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,10 @@ namespace Microsoft.Iris.Render.OpenGL
|
||||
/// <summary>Create an OpenGL render engine for the given Iris engine info.</summary>
|
||||
public static IRenderEngine CreateEngine(IrisEngineInfo engineInfo, IRenderHost renderHost)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(engineInfo);
|
||||
ArgumentNullException.ThrowIfNull(renderHost);
|
||||
// Explicit null checks (not ArgumentNullException.ThrowIfNull, which is net6+
|
||||
// and does not exist on the net48 target).
|
||||
if (engineInfo == null) throw new ArgumentNullException(nameof(engineInfo));
|
||||
if (renderHost == null) throw new ArgumentNullException(nameof(renderHost));
|
||||
return new GLRenderEngine(engineInfo, renderHost);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,16 @@ namespace Microsoft.Iris.Render
|
||||
|
||||
public AnimationInputType InputType => this.m_inputType;
|
||||
|
||||
// Public read accessor so an out-of-assembly animation engine can evaluate
|
||||
// keyframe values without reaching internal state. Only constant inputs carry a
|
||||
// directly-readable value; expression/object inputs return false (a consumer can
|
||||
// still walk BinaryOperation's public operands, and read the leaves this way).
|
||||
public virtual bool TryGetConstantValue(out object value)
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
internal AnimationInputType SourceType => this.m_sourceType;
|
||||
|
||||
internal AnimationTypeMask SourceMask => this.m_sourceMask;
|
||||
|
||||
@@ -74,6 +74,12 @@ namespace Microsoft.Iris.Render
|
||||
|
||||
internal object RawValue => this.m_inputValue;
|
||||
|
||||
public override bool TryGetConstantValue(out object value)
|
||||
{
|
||||
value = this.m_inputValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
private object ApplyMask(AnimationTypeMask mask, object value)
|
||||
{
|
||||
object obj = null;
|
||||
|
||||
@@ -2,6 +2,55 @@
|
||||
|
||||
Reverse-chronological log (prepend new entries; never edit older ones).
|
||||
|
||||
## 2026-07-25 — Real keyframe animation evaluation
|
||||
|
||||
Replaced the no-op animation stubs with a working evaluator. `GLKeyframeAnimation`
|
||||
now advances a clock, finds the surrounding keyframes, eases + interpolates their
|
||||
values and writes the result onto every target property. Repeat, auto-reset and
|
||||
reset-behavior are implemented; Reference/Scale are applied as `ref + scale*value`.
|
||||
|
||||
New files: `Animation/AnimValue.cs` (resolve/lerp/slerp values),
|
||||
`Animation/AnimationEasing.cs` (curve → eased t), `Animation/AnimationTargetApplier.cs`
|
||||
(write value to a named property with channel masking). `GLAnimation` gained
|
||||
protected play-state setters + an abstract `Advance`.
|
||||
|
||||
**One UIX.RenderApi change (approved by the human — "public accessors"):** added
|
||||
`public virtual bool AnimationInput.TryGetConstantValue(out object)` (false by default)
|
||||
and an override on `ConstantAnimationInput` returning its masked value. This is the
|
||||
only supported way for a separate assembly to read keyframe values — the payload was
|
||||
`internal`, and the original animation *engine* lives inside UIX.RenderApi so it never
|
||||
needed a public accessor. `BinaryOperation` already exposes its operands publicly, so
|
||||
expression inputs (relative keyframes) fold over `TryGetConstantValue` leaves; no
|
||||
reflection is used anywhere.
|
||||
|
||||
Confirmed against the UIX consumer (`AnimationManager`, `KeyframeAnimation`):
|
||||
- UIX sets `BackCompat = true`, so keyframe 0 is NOT auto-populated with the initial
|
||||
value (we honor the flag; the auto-keyframe only happens when BackCompat is false).
|
||||
- UIX drives `PulseTimeAdvance` itself, so the render loop must NOT pulse animations.
|
||||
- Keyframe 0 at t=0 = initial value (original behavior, used when !BackCompat).
|
||||
|
||||
Documented assumptions (unverifiable — the real curves/formulas run in native code;
|
||||
logged per the CLAUDE.md unknowns procedure):
|
||||
- Time units: pulse is milliseconds (`nAdvanceMs`), keyframe times / InstantAdvance are
|
||||
seconds. Our conversion matches both.
|
||||
- RepeatCount: 0 = play once, N>0 = N extra loops (N+1 total), <0 = infinite.
|
||||
- Easing shapes are standard curves matched to each interpolation class's name
|
||||
(Bezier falls back to smoothstep since its control points are internal).
|
||||
- Reference/Scale combine as `reference + scale*value`.
|
||||
|
||||
Known gap (NOT done — needs a decision): stage/time/progress/value **event dispatch**.
|
||||
`AnimationEvent`'s ctor hard-casts its target to the render-internal
|
||||
`IActivatableObject`, which an external animation object cannot implement, so UIX's
|
||||
`AnimationProxy` (which registers `AnimationEvent(anim, "AsyncNotify", …)` for
|
||||
Complete/Reset) can't target our animations, and completion/reset notifications don't
|
||||
flow back. Resolving this needs `IActivatableObject` made public + implemented on our
|
||||
animation objects (+ an in-process activation dispatch), a larger change than the value
|
||||
accessor. Events are stored today but not fired. Flagged to the human.
|
||||
|
||||
Validation: compiled the whole project against the prebuilt `UIX.RenderApi.dll` + a
|
||||
one-method harness shim for the new `TryGetConstantValue` (the prebuilt DLL predates it)
|
||||
— build succeeded.
|
||||
|
||||
## 2026-07-25 — Input-event translation (Silk.NET.Input)
|
||||
|
||||
Wired real keyboard/mouse input via `GLInputTranslator`, created by the engine on
|
||||
|
||||
Reference in New Issue
Block a user