A component that attaches and positions two horn model renderers to a skinned character head. It computes scalp deformation from morphs, finds placement by ray-triangle intersection against scalp geometry, solves fit/clearance, and updates horn GameObjects to follow the head bone each frame (or manually).
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);
}
}