Code/CharacterKit/CharacterTailPresentation.cs

Component that presents a detachable demon tail accessory on a character. Loads tail models and textures, creates and configures a SkinnedModelRenderer, simulates simple springy tail motion, applies pose/bone overrides each frame, and manages materials and skinning to match the character.

File AccessNative InteropNetworking
using System;

namespace CharacterEditor;

/// <summary>
/// An optional accessory with its own small skeleton. The body supplies only the
/// pelvis attachment: clothing fitting, human morphs and face deformers never run
/// over the tail. Its spring nodes and material copies are retained between frames.
/// </summary>
public sealed class CharacterTailPresentation : Component
{
	public const int MaximumBones = 10;
	public const string ModelDirectory = "models/characterkit/tails/";
	readonly int[] indices = new int[MaximumBones];
	readonly Transform[] rest = new Transform[MaximumBones];
	readonly Transform[] animationPose = new Transform[MaximumBones];
	readonly Vector3[] restOffsets = new Vector3[MaximumBones];
	readonly Vector3[] boneScales = new Vector3[MaximumBones];
	readonly Vector3[] motionAngles = new Vector3[MaximumBones];
	readonly Vector3[] angularVelocities = new Vector3[MaximumBones];
	readonly Dictionary<int, Material> materials = new();
	readonly Dictionary<int, Material> materialSources = new();
	SkinnedModelRenderer body;
	Model bodyModel, tailModel, boneLookupModel;
	Transform pelvisRest, spineRest;
	BoneCollection.Bone pelvisBone, spineBone;
	string pelvisName, spineName, skinKey;
	Vector3 attachment = new(-4.35f, 0, 37.6f);
	Vector3 lastRoot;
	Vector3 rootPosition, rootScale = Vector3.One;
	Rotation rootRotation, rootInverse;
	Rotation lastRotation;
	float lengthScale = 1, clock, pendingTime, renderTime, idleBlend;
	float silhouetteMargin = 3;
	float turnDeflection, liftDeflection;
	uint idleGestureSeed;
	bool graphInitialized;
	float animationTime;
	bool reset = true, animatedLastFrame, phaseInitialized, gestureSeedInitialized;
	bool hasPreviewTransform;
	bool previewSuspended, poseCacheReady, hasPosedBounds;
	Transform previewTransform;
	BBox posedBounds;
	ulong lastFrame = ulong.MaxValue;
	ulong graphFrame = ulong.MaxValue;
	bool worldGraphFresh;
	Color skinTint = Color.White;
	Texture skinTexture;

	public SkinnedModelRenderer Renderer { get; private set; }
	bool manualUpdate;
	public bool ManualUpdate
	{
		get => manualUpdate;
		set
		{
			manualUpdate = value;
			if (value && Renderer.IsValid()) Renderer.PlaybackRate = 0;
		}
	}
	public bool Animate { get; set; } = true;
	/// <summary>Weight of the additive movement response. Zero retains graph animation.</summary>
	public float InertiaStrength { get; set; } = 1;
	/// <summary>Native graph renderer. tail_action: 0 automatic, 1 quiet idle, 2–13 paired gestures.</summary>
	public void SetAction(CharacterTailAction action)
	{
		if (!Renderer.IsValid()) return;
		int value = Math.Clamp((int)action, 0, 13);
		Renderer.Parameters.Set("tail_action", value);
		// Manual ScenePanel updates can happen before the scene animation system
		// flushes renderer parameters, so also update the native graph immediately.
		Renderer.SceneModel?.SetAnimParameter("tail_action", value);
	}
	public float AnimationTime => animationTime;
	public int SimulatedSegments { get; private set; } = 7;
	public bool IsAnimating => Animate && Renderer.IsValid() && Renderer.Enabled;
	public static string ModelPath(string tip) => ModelDirectory + "demon_" + tip + ".vmdl";

	public static async System.Threading.Tasks.Task Load(CharacterAppearanceData data)
	{
		var model = await Model.LoadAsync(ModelPath(data.Tail.Tip));
		if (model is null || model.IsError)
			throw new InvalidOperationException("The selected demon tail mesh is unavailable.");
		string skinId = System.IO.Path.GetFileNameWithoutExtension(data.SkinPath);
		var color = await Texture.LoadAsync($"textures/characterkit/tails/{skinId}_{data.SkinStyle}_color.vtex");
		if (color is null || color.IsError)
			throw new InvalidOperationException("The selected tail skin is unavailable.");
		color.MarkUsed(1024);
		foreach (var map in new[] { "normal", "agenormal", "combinedmasks" })
		{
			var texture = await Texture.LoadAsync($"textures/characterkit/tails/tail_{map}.vtex");
			if (texture is null || texture.IsError)
				throw new InvalidOperationException("The demon tail skin map is unavailable: " + map);
			texture.MarkUsed(1024);
		}
	}

	/// <summary>Conservative posed-box test, so face-only previews can stay cached.</summary>
	public bool CouldBeVisible(CameraComponent camera, float aspect)
	{
		if (tailModel is null || !camera.IsValid() || !body.IsValid()) return false;
		if (!hasPosedBounds) return true;
		UpdateAttachment();
		float horizontal = MathF.Tan(camera.FieldOfView * MathF.PI / 360);
		float vertical = horizontal / MathF.Max(.1f, aspect);
		var inverse = camera.WorldRotation.Inverse;
		int outside = 31;
		// Use the posed shaft, including its extended tip, rather than the
		// curled asset bounds. A small margin covers the next animation step.
		var minimum = posedBounds.Mins - Vector3.One * 2;
		var maximum = posedBounds.Maxs + Vector3.One * 2;
		for (int corner = 0; corner < 8; corner++)
		{
			var point = new Vector3((corner & 1) == 0 ? minimum.x : maximum.x,
				(corner & 2) == 0 ? minimum.y : maximum.y, (corner & 4) == 0 ? minimum.z : maximum.z);
			var local = inverse * (ToWorld(point) - camera.WorldPosition);
			int mask = local.x < camera.ZNear ? 1 : 0;
			if (local.y > local.x * horizontal) mask |= 2;
			if (local.y < -local.x * horizontal) mask |= 4;
			if (local.z > local.x * vertical) mask |= 8;
			if (local.z < -local.x * vertical) mask |= 16;
			outside &= mask;
		}
		return outside == 0;
	}

	public void Configure(SkinnedModelRenderer target, CharacterAppearanceData data)
	{
		body = target;
		InitializeGestureSeed();
		if (!phaseInitialized)
		{
			clock = (idleGestureSeed & 0xffff) * .001f;
			phaseInitialized = true;
		}
		if (bodyModel != body.Model || boneLookupModel != body.Model)
		{
			boneLookupModel = bodyModel = body.Model;
			pelvisName = null;
			spineName = null;
			for (int i = 0; i < bodyModel.BoneCount; i++)
			{
				if (bodyModel.GetBoneName(i).Equals("pelvis", StringComparison.OrdinalIgnoreCase))
				{
					pelvisName = bodyModel.GetBoneName(i);
					pelvisBone = bodyModel.Bones.AllBones.First(b => b.Index == i);
					pelvisRest = bodyModel.GetBoneTransform(i);
				}
				if (bodyModel.GetBoneName(i).Equals("spine_0", StringComparison.OrdinalIgnoreCase))
				{
					spineName = bodyModel.GetBoneName(i);
					spineBone = bodyModel.Bones.AllBones.First(b => b.Index == i);
					spineRest = bodyModel.GetBoneTransform(i);
				}
			}
			reset = true;
			poseCacheReady = false;
		}

		var model = Model.Load(ModelPath(data.Tail.Tip));
		if (tailModel != model || !Renderer.IsValid() || indices[0] < 0)
		{
			if (!Renderer.IsValid()) Renderer = GameObject.Components.Create<SkinnedModelRenderer>();
			Renderer.Model = tailModel = model;
			Renderer.UseAnimGraph = true;
			// This accessory owns its clock so hidden previews and distant tails
			// can suspend. Advance evaluates the native graph once per accepted update.
			Renderer.PlaybackRate = 0;
			graphInitialized = false;
			Renderer.DeformationMode = SkinnedModelRenderer.DeformationModeType.None;
			Renderer.BoneMergeTarget = null;
			materials.Clear();
			materialSources.Clear();
			skinKey = null;
			for (int joint = 0; joint < MaximumBones; joint++)
			{
				indices[joint] = -1;
				string name = $"ck_tail_{joint:00}";
				for (int i = 0; i < model.BoneCount; i++)
					if (model.GetBoneName(i) == name)
					{
						indices[joint] = i;
						rest[joint] = model.GetBoneTransform(i);
						animationPose[joint] = rest[joint];
						break;
					}
			}
			reset = true;
		}

		var scale = .38f + .67f * data.Tail.Length;
		int segments = 3 + (int)MathF.Round(data.Tail.Length * 6);
		if (lengthScale != scale || SimulatedSegments != segments)
		{
			lengthScale = scale;
			SimulatedSegments = segments;
			reset = true;
		}
		for (int i = 0; i < MaximumBones; i++)
		{
			restOffsets[i] = i == 0 ? Vector3.Zero : rest[i - 1].Rotation.Inverse * (rest[i].Position - rest[i - 1].Position);
			boneScales[i] = rest[i].Scale * lengthScale;
		}
		poseCacheReady = false;
		InitializePoseCache();
		// Uniform joint scale avoids the bulges that anisotropic matrices create
		// between curved, blended joints. An authored radial morph cancels only
		// the girth change, leaving the shortening coherent along the whole mesh.
		if (Renderer.Morphs.Names.Contains("ck_tail_girth"))
			Renderer.Morphs.Set("ck_tail_girth", (1 / lengthScale - 1) / (1 / .38f - 1), 0);
		// Authored deltas include the shortest-tail compensation. Dividing by the
		// current uniform scale preserves girth and tip proportions. Additional bulk
		// is bounded on very short shafts to respect their tighter bend radius.
		if (Renderer.Morphs.Names.Contains("ck_tail_thickness"))
		{
			Renderer.Morphs.Set("ck_tail_thickness", data.Tail.ShaftBulk * .38f / lengthScale, 0);
			float size = data.Tail.TipScale;
			Renderer.Morphs.Set("ck_tail_tip_small", MathF.Max(0, 1 - size) * .76f / lengthScale, 0);
			Renderer.Morphs.Set("ck_tail_tip_large", MathF.Max(0, size - 1) * .38f / lengthScale, 0);
		}
		silhouetteMargin = MathF.Max(3 + 5 * data.Tail.Thickness, 6 * data.Tail.TipScale);
		Renderer.SceneModel?.Attributes.Set("TailTipColor", data.Tail.EffectiveTipColor(data));
		var root = AttachmentPoint(data);
		if (root != attachment)
		{
			attachment = root;
			reset = true;
		}
		ApplySkin(data);
		if (reset) Advance(0, false);
	}

	// The tail joins the upper sacrum, above the buttock lobes. Broad torso edits
	// move the root with the skin; local chest/leg/face edits do not move it.
	public static Vector3 AttachmentPoint(CharacterAppearanceData data) => CharacterTailAttachment.RestPoint(data);

	void ApplySkin(CharacterAppearanceData data)
	{
		var key = $"{data.SkinPath}|{data.SkinStyle}|{data.SkinColor}|{data.NormalizeSkinTone}|{data.Tail.CustomTipColor}|{data.Tail.Tip}|{data.Tail.EffectiveTipColor(data)}";
		if (key == skinKey) return;
		skinKey = key;
		// These authored tiles retain a clean patch of each selected body's skin.
		// They are baked once, not read back or reconstructed while playing.
		string skinId = System.IO.Path.GetFileNameWithoutExtension(data.SkinPath);
		skinTexture = Texture.Load($"textures/characterkit/tails/{skinId}_{data.SkinStyle}_color.vtex");
		skinTint = data.SkinColor;
		Material bodySurface = null;
		for (int i = 0; i < body.Materials.Count; i++)
		{
			var original = body.Materials.GetOriginal(i);
			if (original is null || !original.Name.Contains("/bodies/", StringComparison.OrdinalIgnoreCase)) continue;
			var source = UnifiedHuman.SkinMaterial(original, data.SkinPath, data.BaseShape is not null);
			bodySurface = body.Materials.GetOverride(i) ?? source;
			skinTint = SkinColorPresentation.MaterialTint(source, data.SkinColor, data.NormalizeSkinTone)
				* (data.BaseShape is not null ? UnifiedHuman.BodyToneTint(source, data.SkinPath) : Color.White);
			break;
		}
		for (int i = 0; i < Renderer.Materials.Count; i++)
		{
			var original = Renderer.Materials.GetOriginal(i);
			if (original is null) continue;
			if (original.Name.Contains("/legacy_hair/", StringComparison.OrdinalIgnoreCase))
			{
				// Native head-hair material and strand atlas, with one retained material
				// instance for this tuft. No new simulation, skeleton or renderer.
				if (!materials.TryGetValue(i, out var hair) || !materialSources.TryGetValue(i, out var hairSource) || hairSource != original)
				{
					materials[i] = hair = original.CreateCopy($"character-tail-fur-{Id}-{i}.vmat");
					materialSources[i] = original;
				}
				// Material tint is authored in sRGB; renderer hair tint is linear.
				var color = data.Tail.EffectiveTipColor(data);
				hair.Set("g_vColorTint", new Color(MathF.Pow(color.r, 1 / 2.2f), MathF.Pow(color.g, 1 / 2.2f), MathF.Pow(color.b, 1 / 2.2f)));
				Renderer.Materials.SetOverride(i, hair);
				continue;
			}
			bool tip = original.Name.Contains("tip", StringComparison.OrdinalIgnoreCase);
			if (tip && data.Tail.CustomTipColor)
			{
				Renderer.Materials.SetOverride(i, Material.Load("materials/characterkit/tails/demon_custom_tip.vmat"));
				continue;
			}
			// All solid tips use the same skin surface as the shaft unless recolored.
			var source = bodySurface ?? original;
			if (!materials.TryGetValue(i, out var material) || !materialSources.TryGetValue(i, out var previous) || previous != source)
			{
				materials[i] = material = source.CreateCopy($"character-tail-{Id}-{i}.vmat");
				materialSources[i] = source;
			}
			material.Set("g_tColor", skinTexture);
			// Start with the body's real skin shader settings, then replace UV-
			// dependent maps with authored tail tiles. This retains its native
			// subsurface/roughness response instead of a shiny generic material.
			material.Set("g_tNormal", Texture.Load("textures/characterkit/tails/tail_normal.vtex"));
			material.Set("g_tAgeNormal", Texture.Load("textures/characterkit/tails/tail_agenormal.vtex"));
			material.Set("g_tCombinedMasks", Texture.Load("textures/characterkit/tails/tail_combinedmasks.vtex"));
			material.Set("g_vColorTint", skinTint);
			Renderer.Materials.SetOverride(i, material);
		}
	}

	protected override void OnUpdate()
	{
		if (ManualUpdate || !Renderer.IsValid()) return;
		graphFrame = Application.FrameCount;
		worldGraphFresh = false;
		Renderer.PlaybackRate = 0;
		if (!Renderer.Enabled || !body.IsValid() || !body.Enabled || !Animate) return;
		float interval = 0;
		var camera = Scene.Camera;
		if (camera.IsValid())
		{
			var difference = body.WorldPosition - camera.WorldPosition;
			float distance = difference.Length;
			if (distance > 1600 || distance > 100 && Vector3.Dot(difference.Normal, camera.WorldRotation.Forward) < -.25f) return;
			interval = distance > 900 ? 1f / 15 : distance > 400 ? 1f / 30 : 0;
		}
		float elapsed = renderTime + Time.Delta;
		if (elapsed < interval || elapsed > .15f) return;
		var native = Renderer.SceneModel;
		if (native is null) return;
		native.ClearBoneOverrides();
		if (!graphInitialized)
		{
			// One-time phase variation before joining normal scene animation.
			native.PlaybackRate = 1;
			native.Update((idleGestureSeed & 0xffff) * .001f % 9);
			graphInitialized = true;
		}
		// The scene animation stage owns world playback. A throttled update
		// advances accumulated time once; no manual native Update follows it.
		Renderer.PlaybackRate = elapsed / MathF.Max(Time.Delta, .000001f);
		worldGraphFresh = true;
	}

	protected override void OnPreRender()
	{
		if (ManualUpdate || lastFrame == Application.FrameCount) return;
		lastFrame = Application.FrameCount;
		if (!Renderer.IsValid() || !Renderer.Enabled || !body.IsValid() || !body.Enabled)
		{
			reset = true;
			return;
		}
		float interval = 0;
		bool animate = Animate;
		var camera = Scene.Camera;
		if (camera.IsValid())
		{
			var difference = body.WorldPosition - camera.WorldPosition;
			float distance = difference.Length;
			if (distance > 2200 || distance > 100 && Vector3.Dot(difference.Normal, camera.WorldRotation.Forward) < -.25f)
			{
				reset = true;
				renderTime = 0;
				return;
			}
			interval = distance > 1600 ? .2f : distance > 900 ? 1f / 15 : distance > 400 ? 1f / 30 : 0;
			// At this size the silhouette matters, not secondary motion. Keep a
			// stable authored pose rather than repeatedly restarting a slow solver.
			if (distance > 1600) animate = false;
		}
		renderTime += Time.Delta;
		if (renderTime < interval) return;
		Advance(renderTime, animate);
		renderTime = 0;
	}

	/// <summary>ScenePanel previews call this explicitly; static cards stay frozen.</summary>
	public void TickPreview(float delta, bool animate, bool suspend = false)
	{
		ManualUpdate = true;
		Animate = animate;
		if (!body.IsValid()) return;
		if (suspend)
		{
			previewSuspended = true;
			return;
		}
		var transform = body.WorldTransform;
		if (!animate && !reset && !animatedLastFrame && hasPreviewTransform && previewTransform == transform) return;
		previewTransform = transform;
		hasPreviewTransform = true;
		Advance(delta, animate);
	}

	public void Advance(float delta, bool animate)
	{
		if (!body.IsValid() || !Renderer.IsValid() || Renderer.SceneModel is null || tailModel is null || indices[0] < 0) return;
		InitializeGestureSeed();
		InitializePoseCache();
		UpdateAttachment();
		var rotation = rootRotation;
		var position = rootPosition;
		if (!animate)
		{
			if (reset || animatedLastFrame || position != lastRoot || rotation != lastRotation)
			{
				ResetMotion();
				ApplyPose(true);
			}
			lastRoot = position;
			lastRotation = rotation;
			animatedLastFrame = false;
			previewSuspended = false;
			return;
		}
		bool interrupted = previewSuspended || delta > .15f;
		if (!animatedLastFrame || !previewSuspended && (position - lastRoot).LengthSquared > 400) reset = true;
		if (interrupted)
		{
			// A hidden preview or ordinary hitch must not snap a mid-gesture
			// tail back to its curled bind pose. Resume from the retained pose,
			// dropping stale motion input and missed time instead of that pose.
			delta = 0;
			turnDeflection = liftDeflection = 0;
			for (int i = 0; i <= SimulatedSegments; i++) angularVelocities[i] = Vector3.Zero;
		}
		if (!reset && !interrupted)
		{
			float dt = MathF.Max(.001f, delta);
			var localVelocity = rootInverse * ((position - lastRoot) / dt);
			var previousForward = rootInverse * lastRotation.Forward;
			float turnDelta = MathF.Atan2(previousForward.y, previousForward.x) * (180 / MathF.PI);
			float turnSpeed = turnDelta / dt;
			turnDeflection = Math.Clamp(turnSpeed * .08f + localVelocity.y * .14f, -32, 32);
			liftDeflection = Math.Clamp(localVelocity.x * .07f - localVelocity.z * .08f, -12, 12);
			for (int i = 1; i <= SimulatedSegments; i++)
			{
				float fraction = (float)i / SimulatedSegments;
				float envelope = fraction * fraction * (3 - 2 * fraction);
				motionAngles[i].y += turnDelta * .65f * envelope;
				ConstrainMotion(i, envelope);
			}
		}
		lastRoot = position;
		lastRotation = rotation;
		animatedLastFrame = true;
		previewSuspended = false;
		if (reset) ResetMotion();
		pendingTime = MathF.Min(.1f, pendingTime + Math.Clamp(delta, 0, .1f));
		const float step = 1f / 120;
		while (pendingTime >= step)
		{
			clock += step;
			Simulate(step);
			pendingTime -= step;
		}
		ApplyPose(false, Math.Clamp(delta, 0, .1f));
	}

	void UpdateAttachment()
	{
		var rotation = body.WorldRotation;
		var position = body.WorldPosition + rotation * attachment;
		if (pelvisName is not null && body.TryGetBoneTransform(pelvisBone, out var pelvis))
		{
			rotation = pelvis.Rotation * pelvisRest.Rotation.Inverse;
			position = pelvis.Position + rotation * ((attachment - pelvisRest.Position) * body.WorldScale);
			if (spineName is not null && body.TryGetBoneTransform(spineBone, out var spine))
			{
				var spineRotation = spine.Rotation * spineRest.Rotation.Inverse;
				var spinePosition = spine.Position + spineRotation * ((attachment - spineRest.Position) * body.WorldScale);
				position = Vector3.Lerp(position, spinePosition, .25f);
				rotation = Rotation.Slerp(rotation, spineRotation, .25f);
			}
		}
		GameObject.WorldTransform = new Transform(position, rotation, body.WorldScale);
		rootPosition = position;
		rootRotation = rotation;
		rootInverse = rotation.Inverse;
		rootScale = body.WorldScale;
	}

	Vector3 ToWorld(Vector3 point) => rootPosition + rootRotation * (point * rootScale);

	void ResetMotion()
	{
		for (int i = 0; i <= SimulatedSegments; i++)
		{
			motionAngles[i] = Vector3.Zero;
			angularVelocities[i] = Vector3.Zero;
		}
		pendingTime = 0;
		idleBlend = 0;
		turnDeflection = liftDeflection = 0;
		reset = false;
	}

	void Simulate(float step)
	{
		idleBlend = MathF.Min(1, idleBlend + step * 2);
		for (int i = 1; i <= SimulatedSegments; i++)
		{
			float fraction = (float)i / SimulatedSegments;
			float envelope = fraction * fraction * (3 - 2 * fraction);
			var target = new Vector3(liftDeflection, turnDeflection, 0) * envelope;
			// The base answers first; the softer tip follows and briefly settles
			// after a stop. Angular limits keep this from becoming a loose whip.
			float stiffness = 150 - 105 * fraction;
			float damping = 22 - 13 * fraction;
			angularVelocities[i] += ((target - motionAngles[i]) * stiffness - angularVelocities[i] * damping) * step;
			motionAngles[i] += angularVelocities[i] * step;
			ConstrainMotion(i, envelope);
		}
	}

	void ConstrainMotion(int index, float envelope)
	{
		float yawLimit = 36 * envelope, liftLimit = 16 * envelope;
		motionAngles[index].y = Math.Clamp(motionAngles[index].y, -yawLimit, yawLimit);
		motionAngles[index].x = Math.Clamp(motionAngles[index].x, -liftLimit, liftLimit);
		if (MathF.Abs(motionAngles[index].y) >= yawLimit && motionAngles[index].y * angularVelocities[index].y > 0)
			angularVelocities[index].y = 0;
		if (MathF.Abs(motionAngles[index].x) >= liftLimit && motionAngles[index].x * angularVelocities[index].x > 0)
			angularVelocities[index].x = 0;
	}

	Vector3 SampleMotion(float fraction)
	{
		float location = Math.Clamp(fraction, 0, 1) * SimulatedSegments;
		int index = Math.Min(SimulatedSegments - 1, (int)location);
		float t = location - index;
		var a = motionAngles[Math.Max(0, index - 1)];
		var b = motionAngles[index];
		var c = motionAngles[index + 1];
		var d = motionAngles[Math.Min(SimulatedSegments, index + 2)];
		return .5f * ((2 * b) + (-a + c) * t + (2 * a - 5 * b + 4 * c - d) * t * t + (-a + 3 * b - 3 * c + d) * t * t * t);
	}

	void InitializeGestureSeed()
	{
		if (gestureSeedInitialized) return;
		idleGestureSeed = unchecked((uint)Id.GetHashCode());
		gestureSeedInitialized = true;
	}

	void InitializePoseCache()
	{
		if (poseCacheReady) return;
		for (int i = 0; i < MaximumBones; i++)
			restOffsets[i] = i == 0 ? Vector3.Zero : rest[i - 1].Rotation.Inverse * (rest[i].Position - rest[i - 1].Position);
		poseCacheReady = true;
	}

	static void IncludePoint(ref Vector3 minimum, ref Vector3 maximum, Vector3 point)
	{
		minimum = new Vector3(MathF.Min(minimum.x, point.x), MathF.Min(minimum.y, point.y), MathF.Min(minimum.z, point.z));
		maximum = new Vector3(MathF.Max(maximum.x, point.x), MathF.Max(maximum.y, point.y), MathF.Max(maximum.z, point.z));
	}

	void ApplyPose(bool staticPose = false, float delta = 0)
	{
		var sceneModel = Renderer.SceneModel;
		bool sceneDriven = !ManualUpdate && graphFrame == Application.FrameCount;
		if (!staticPose)
		{
			if (!sceneDriven)
			{
				// ScenePanel previews have an explicit clock, independent of the
				// engine's world animation stage and the user's play-session time.
				sceneModel.Transform = GameObject.WorldTransform;
				sceneModel.ClearBoneOverrides();
				float advance = delta;
				if (!graphInitialized)
				{
					advance += (idleGestureSeed & 0xffff) * .001f % 9;
					graphInitialized = true;
				}
				sceneModel.PlaybackRate = 1;
				sceneModel.Update(advance);
				sceneModel.PlaybackRate = 0;
			}
			animationTime += delta;
			if (!sceneDriven || worldGraphFresh)
			{
				// Read in the animation stage's transform before moving the renderer
				// to this frame's newly animated pelvis attachment.
				var inverse = sceneModel.Transform.Rotation.Inverse;
				for (int i = 0; i < MaximumBones; i++)
				{
					var world = sceneModel.GetBoneWorldTransform(indices[i]);
					animationPose[i] = new Transform(Vector3.Zero, inverse * world.Rotation, Vector3.One);
				}
			}
		}
		sceneModel.Transform = GameObject.WorldTransform;
		var previous = new Transform(Vector3.Zero, Rotation.Identity, Vector3.One);
		var minimum = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
		var maximum = -minimum;
		float strength = float.IsFinite(InertiaStrength) ? Math.Clamp(InertiaStrength, 0, 1) : 1;
		for (int i = 0; i < MaximumBones; i++)
		{
			if (indices[i] < 0) continue;
			var rotation = rest[i].Rotation;
			if (!staticPose)
			{
				var motion = SampleMotion(i / (float)(MaximumBones - 1)) * strength;
				var animated = Rotation.Slerp(rest[i].Rotation, animationPose[i].Rotation, idleBlend);
				rotation = Rotation.FromYaw(motion.y) * Rotation.FromAxis(Vector3.Left, motion.x) * animated;
			}
			// FK preserves authored link lengths through graph crossfades and inertia.
			var point = i == 0 ? rest[0].Position * lengthScale
				: previous.Position + previous.Rotation * (restOffsets[i] * lengthScale);
			var transform = new Transform(point, rotation, boneScales[i]);
			// Keep the complete pose together. Direct world-bone writes do not
			// finalize the skinning palette used by the renderer.
			sceneModel.SetBoneOverride(indices[i], transform);
			IncludePoint(ref minimum, ref maximum, point);
			previous = transform;
		}
		var end = previous.Position + previous.Rotation * (restOffsets[MaximumBones - 1].Normal
			* (restOffsets[MaximumBones - 1].Length * lengthScale + 3));
		IncludePoint(ref minimum, ref maximum, end);
		posedBounds = new BBox(minimum - Vector3.One * silhouetteMargin, maximum + Vector3.One * silhouetteMargin);
		hasPosedBounds = true;
		sceneModel.LocalBounds = posedBounds;
		// Commit overrides through the native bone-update path, with graph time
		// paused. This publishes one coherent skinning palette without advancing
		// the authored animation again.
		float playbackRate = sceneModel.PlaybackRate;
		sceneModel.PlaybackRate = 0;
		sceneModel.Update(0);
		sceneModel.PlaybackRate = playbackRate;
	}
}

/// <summary>Optional action selection on the tail's independent native AnimGraph.</summary>
public enum CharacterTailAction
{
	Automatic, Idle, SwishLeft, SwishRight, FlickLeft, FlickRight, StretchLeft, StretchRight,
	SideCurlLeft, SideCurlRight, SWaveLeft, SWaveRight, CurlLiftLeft, CurlLiftRight
}