Effigy/Rig/HumanoidSample.cs
using System;
using System.Collections.Generic;
namespace Effigy;
/// <summary>
/// A blocky humanoid robot, one box per bone, for the "Make a Playermodel" tutorial to start from.
///
/// WHY A SAMPLE AT ALL. The playermodel lesson is about NAMES AND PARENTS - which bone hangs off
/// which, spelled the way citizen's animations expect. Nothing about it is about modelling, and
/// making the reader build twenty boxes first buries the one idea under forty minutes of typing
/// numbers into a primitive dialog. Offered alongside "I'll use my own model", so the lesson works
/// either way and neither route is the consolation prize.
///
/// WHY SEPARATE BOXES AND NOT ONE MESH. Each part is its own body, so Bone from Part can measure a
/// bone along it and pin it, which is the whole loop the lesson teaches. A merged humanoid is one
/// body with one bone and needs weight painting before anything bends.
///
/// WHY EVERY PART IS NAMED AFTER ITS BONE. A body takes its name from the feature that made it, and
/// Bone from Part names the bone after the body - so a part called `upperarm_L` becomes a bone
/// called `upperarm_L`, which <see cref="CitizenBoneMap.Playermodel"/> already knows how to place.
/// The reader never has to rename anything, and the one thing that can silently ruin a playermodel -
/// a misspelled bone - cannot happen on this route.
///
/// WHY EVERY PART IS LONGEST ALONG ITS BONE. <see cref="BoneFromBody.TryDerive"/> measures the
/// direction the vertices spread out along most, so a part that is wider than it is long yields a
/// bone across the limb rather than down it. That is why the torso boxes are taller than they are
/// wide and the robot reads as slightly slim: the shape is constrained by what the measurement can
/// see, not by taste. A cube has no longest axis at all and would be skipped outright.
///
/// UNITS ARE INCHES, the same as the rest of Effigy and of s&box. It stands in a T-pose facing +X
/// with the soles of its feet at z=0 and its hips at 31.5 - citizen's own pelvis height, because
/// citizen's walk decides how high the hips ride and a model built far from it floats or sinks.
/// </summary>
public static class HumanoidSample
{
/// <summary>One box: the bone name it carries, its size, and where its centre sits.</summary>
public readonly struct Part
{
public readonly string Name;
public readonly Vec3 Size;
public readonly Vec3 Centre;
public Part( string name, Vec3 size, Vec3 centre )
{
Name = name;
Size = size;
Centre = centre;
}
}
/// <summary>
/// The left side and the spine. The right side is this mirrored, built by <see cref="Parts"/>.
///
/// Written out rather than derived from a proportion table, because these numbers were tuned
/// against two things at once - the measurement above, and citizen's pelvis height - and a
/// formula that produced them would be a formula fitted to thirteen answers.
/// </summary>
static readonly Part[] Half =
{
new( "pelvis", new Vec3( 5f, 6.5f, 7f ), new Vec3( 0f, 0f, 31.5f ) ),
new( "spine_01", new Vec3( 4.5f, 6.5f, 7f ), new Vec3( 0f, 0f, 38.5f ) ),
new( "spine_02", new Vec3( 5f, 7f, 7.5f ), new Vec3( 0f, 0f, 45.75f ) ),
new( "chest", new Vec3( 6f, 7.5f, 8f ), new Vec3( 0f, 0f, 53.5f ) ),
new( "neck", new Vec3( 2.5f, 2.5f, 3.5f ), new Vec3( 0f, 0f, 59.25f ) ),
new( "head", new Vec3( 7f, 6.5f, 8.5f ), new Vec3( 0f, 0f, 65.25f ) ),
new( "clavicle_L", new Vec3( 3f, 5.5f, 2.5f ), new Vec3( 0f, 5.25f, 56f ) ),
new( "upperarm_L", new Vec3( 3f, 9.5f, 3f ), new Vec3( 0f, 12.75f, 56f ) ),
new( "forearm_L", new Vec3( 2.6f, 8.5f, 2.6f ), new Vec3( 0f, 21.75f, 56f ) ),
new( "hand_L", new Vec3( 1.5f, 5f, 3.2f ), new Vec3( 0f, 28.5f, 56f ) ),
new( "thigh_L", new Vec3( 4f, 4f, 13f ), new Vec3( 0f, 3.2f, 23.5f ) ),
new( "calf_L", new Vec3( 3.5f, 3.5f, 12.5f ), new Vec3( 0f, 3.2f, 10.75f ) ),
new( "foot_L", new Vec3( 8f, 3.5f, 3f ), new Vec3( 2.5f, 3.2f, 1.5f ) ),
};
/// <summary>
/// Every box, left side then right. `_L` is the MODEL's left, which is +Y - the side on your
/// left when you stand behind it, and the side citizen's `_L` bones are on.
/// </summary>
public static IReadOnlyList<Part> Parts
{
get
{
var parts = new List<Part>( Half.Length * 2 );
foreach ( var part in Half )
{
parts.Add( part );
if ( !part.Name.EndsWith( "_L", StringComparison.Ordinal ) )
continue;
var mirrored = part.Name[..^2] + "_R";
parts.Add( new Part( mirrored, part.Size,
new Vec3( part.Centre.x, -part.Centre.y, part.Centre.z ) ) );
}
return parts;
}
}
/// <summary>
/// The chains the lesson builds, each one a parent followed by its descendants in order.
///
/// ONE LIST, SHARED. The tutorial's IsDone checks read this, the headless test walks it to build
/// the rig the reader would build by hand, and the step text is written from it. Three copies of
/// a parent order is three chances for the document to teach something the test does not check.
///
/// The first name in each chain already exists by the time that chain is built - `chest` and
/// `pelvis` come from the spine - so a builder walking these must skip a bone it already made
/// rather than making a second one.
/// </summary>
public static IReadOnlyList<string[]> Chains { get; } = new[]
{
new[] { "pelvis", "spine_01", "spine_02", "chest", "neck", "head" },
new[] { "chest", "clavicle_L", "upperarm_L", "forearm_L", "hand_L" },
new[] { "chest", "clavicle_R", "upperarm_R", "forearm_R", "hand_R" },
new[] { "pelvis", "thigh_L", "calf_L", "foot_L" },
new[] { "pelvis", "thigh_R", "calf_R", "foot_R" },
};
/// <summary>
/// A studio holding the robot, already rebuilt.
///
/// BodyNames is written as well as Feature.Name. A body is named after its feature today, and
/// the names are the one thing this sample exists to get right - belt and braces is cheap here
/// and a silently renamed body would send the reader hunting a misspelling they never made.
/// </summary>
public static PartStudio Build()
{
var studio = new PartStudio();
foreach ( var part in Parts )
{
var primitive = studio.Add( new PrimitiveFeature { Name = part.Name } );
primitive.SizeX.Value = part.Size.x;
primitive.SizeY.Value = part.Size.y;
primitive.SizeZ.Value = part.Size.z;
primitive.Position.Value = part.Centre;
}
studio.Rebuild();
foreach ( var body in studio.Bodies )
{
var feature = studio.Features.Find( f => f.Id == body.FeatureId );
if ( feature?.Name is { Length: > 0 } name )
studio.BodyNames[body.Id] = name;
}
// Names are applied on the NEXT rebuild, so a caller that only ever reads Bodies would see
// the feature names it already had. Cheap, and it means Build returns a studio whose bodies
// and BodyNames agree.
studio.Rebuild();
return studio;
}
}