CharacterKit/CharacterHornPresentation.cs

Component that attaches and positions two horn models to a character head. It computes scalp points and envelope from morphs, builds/updates ModelRenderer instances for left/right horns, fits them against head geometry, and updates attachment transform each frame unless ManualUpdate is set.

Native Interop
using System;

namespace CharacterEditor;
/// <summary>Two rigid head accessories. Head pose following is the only frame-time work.</summary>
public sealed class CharacterHornPresentation : Component
{
	SkinnedModelRenderer body;
	Model bodyModel;
	BoneCollection.Bone head;
	Transform headRest;
	readonly ModelRenderer[] renderers = new ModelRenderer[2];
	public IReadOnlyList<ModelRenderer> Renderers => renderers;
	public bool ManualUpdate { get; set; }
	public float MinimumClearance { get; private set; }

	public void Configure(SkinnedModelRenderer target, CharacterAppearanceData appearance)
	{
		body = target;
		if (bodyModel != body.Model)
		{
			bodyModel = body.Model;
			head = bodyModel.Bones.AllBones.FirstOrDefault(b => b.Name.Equals("head", StringComparison.OrdinalIgnoreCase));
			if (head is null)
				return;
			headRest = bodyModel.GetBoneTransform(head.Index);
		}

		var points = HornScalp.Points.ToArray();
		foreach (var morph in HornScalp.Morphs)
		{
			float weight = body.Morphs.Get(morph.Name);
			if (appearance.BaseShape is null && morph.Name.StartsWith("ce_human"))
				weight = morph.Name == "ce_" + System.IO.Path.GetFileNameWithoutExtension(appearance.SkinPath) ? 1 : 0;
			if (MathF.Abs(weight) < .00001f)
				continue;
			for (int i = 0; i < points.Length; i++)
				points[i] += morph.Delta[i] * weight;
		}

		CharacterDeformation.ShapeAttachmentPoints(points, appearance);
		var envelope = HornHeadEnvelope.Points.ToArray();
		foreach (var morph in HornHeadEnvelope.Morphs)
		{
			float weight = body.Morphs.Get(morph.Name);
			if (appearance.BaseShape is null && morph.Name.StartsWith("ce_human"))
				weight = morph.Name == "ce_" + System.IO.Path.GetFileNameWithoutExtension(appearance.SkinPath) ? 1 : 0;
			if (MathF.Abs(weight) < .00001f)
				continue;
			for (int i = 0; i < envelope.Length; i++)
				envelope[i] += morph.Delta[i] * weight;
		}

		CharacterDeformation.ShapeAttachmentPoints(envelope, appearance);
		var planes = HornClearance.Planes(envelope);
		MinimumClearance = float.MaxValue;
		for (int side = 0; side < 2; side++)
		{
			var settings = side == 0 || appearance.Horns.Linked ? appearance.Horns.Left : appearance.Horns.Right;
			if (settings.Family == "none")
			{
				if (renderers[side].IsValid())
					renderers[side].GameObject.Enabled = false;
				continue;
			}

			if (!renderers[side].IsValid())
			{
				var go = new GameObject
				{
					Parent = GameObject,
					Name = side == 0 ? "Left horn" : "Right horn",
					NetworkMode = NetworkMode.Never
				};
				go.Flags |= GameObjectFlags.NotSaved;
				renderers[side] = go.Components.Create<ModelRenderer>();
			}

			var renderer = renderers[side];
			renderer.GameObject.Enabled = true;
			renderer.Model = CharacterHornMesh.Get(settings, side == 1);
			renderer.MaterialOverride = null;
			renderer.SceneObject.Attributes.Set("HornBase", settings.Color);
			renderer.SceneObject.Attributes.Set("HornTip", settings.TipColor);
			renderer.SceneObject.Attributes.Set("HornRoughness", settings.Roughness);
			renderer.SceneObject.Attributes.Set("HornRidges", settings.Ridges);
			renderer.SceneObject.Attributes.Set("HornTipCoverage", settings.TipCoverage);
			renderer.SceneObject.Attributes.Set("HornBlendSoftness", settings.BlendSoftness);
			float sign = side == 0 ? 1 : -1;
			var direction = new Vector3(1.6f - 2.4f * settings.Placement, sign * (.42f + .65f * settings.Spacing), 1).Normal;
			var center = new Vector3(0, 0, 64);
			var normal = direction;
			var point = center + direction * 4.6f;
			float closest = float.MaxValue;
			for (int t = 0; t < HornScalp.Triangles.Length; t += 3)
			{
				var a = points[HornScalp.Triangles[t]];
				var b = points[HornScalp.Triangles[t + 1]];
				var c = points[HornScalp.Triangles[t + 2]];
				if (RayTriangle(center, direction, a, b, c, out float distance) && distance < closest)
				{
					closest = distance;
					point = center + direction * distance;
					normal = Vector3.Cross(b - a, c - a).Normal;
					if (Vector3.Dot(normal, direction) < 0)
						normal = -normal;
				}
			}

			var fit = HornClearance.Solve(settings, side == 1, point, normal, planes);
			renderer.LocalPosition = fit.Position;
			renderer.LocalRotation = fit.Rotation;
			MinimumClearance = MathF.Min(MinimumClearance, fit.Clearance);
		}

		UpdateAttachment();
	}

	static bool RayTriangle(Vector3 origin, Vector3 direction, Vector3 a, Vector3 b, Vector3 c, out float distance)
	{
		distance = 0;
		var e1 = b - a;
		var e2 = c - a;
		var p = Vector3.Cross(direction, e2);
		float determinant = Vector3.Dot(e1, p);
		if (MathF.Abs(determinant) < .000001f)
			return false;
		float inverse = 1 / determinant;
		var s = origin - a;
		float u = Vector3.Dot(s, p) * inverse;
		if (u < 0 || u > 1)
			return false;
		var q = Vector3.Cross(s, e1);
		float v = Vector3.Dot(direction, q) * inverse;
		if (v < 0 || u + v > 1)
			return false;
		distance = Vector3.Dot(e2, q) * inverse;
		return distance > 0;
	}

	protected override void OnPreRender()
	{
		if (!ManualUpdate)
			UpdateAttachment();
	}

	public void TickPreview()
	{
		ManualUpdate = true;
		UpdateAttachment();
	}

	public void UpdateAttachment()
	{
		if (!body.IsValid() || head is null || !body.TryGetBoneTransform(head, out var pose))
			return;
		var rotation = pose.Rotation * headRest.Rotation.Inverse;
		GameObject.WorldTransform = new Transform(pose.Position - rotation * (headRest.Position * body.WorldScale), rotation, body.WorldScale);
	}
}