Editor/Effigy/ClothExport.cs

Editor utility that exports a cloth garment to a JSON-like string. It converts a PolyMesh plus capsule colliders and a skeleton into a serialized representation containing material, optional fabric settings, per-vertex pins, positions, UVs, triangle indices, per-vertex bone names (nearest capsule bone), and capsule definitions.

File Access
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;

namespace Effigy;

/// <summary>
/// A garment as live cloth: the file Marionette's GarmentCloth component simulates in the game.
///
/// WHY A SECOND EXPORT. A .clothing item is a skinned model - the shirt is baked into the pose
/// the drape left it in and then rides the bones rigidly. That is how every s&amp;box garment
/// works and it is fine for a fitted top; it is not a shirt hanging loose off a robot's
/// shoulders, which has to swing when he turns. That takes a simulation at runtime, and the
/// simulation needs things a model cannot carry: which bone each collider rides on and how thick
/// it is, and which bone each bit of cloth is leashed to.
///
/// EVERYTHING IN THE WEARER'S BIND POSE, in model space - the same space the garment was fitted
/// in. The component reads the model's bind transforms at start and turns each of these into an
/// offset from its bone, so the file does not care what pose the character is in when it loads.
///
/// JSON, WRITTEN BY HAND, for the reason ClothingDefinition gives: the kernel stays engine-free
/// and testable, and the game reads it with the engine's own deserialiser.
/// </summary>
public static class ClothExport
{
	/// <summary>
	/// How the cloth behaves at runtime, in the component's own terms (0..1 sliders, not
	/// compliances), so the file is the fabric the garment was given and the component has
	/// nothing to be set up on. See GarmentCloth.
	/// </summary>
	public sealed class Settings
	{
		public float Stiffness = 0.9f;
		public float Bend = 0.15f;
		public float Damping = 0.03f;
		public float Friction = 0.6f;
	}

	/// <summary>The Garment's Fabric and Stiffness as runtime settings. The numbers match the
	/// drape presets in ClothSim.WithFabric in spirit - denim holds its panels, stretch gives,
	/// leather is heavy - and are what looked right on a T-shirt, not measured anything.</summary>
	public static Settings FabricSettings( Fabric fabric, float stiffness )
	{
		stiffness = Math.Clamp( stiffness, 0f, 1f );

		return fabric switch
		{
			Fabric.Denim => new Settings { Stiffness = 0.6f + 0.4f * stiffness, Bend = 0.4f, Damping = 0.06f, Friction = 0.5f },
			Fabric.Leather => new Settings { Stiffness = 0.7f + 0.3f * stiffness, Bend = 0.6f, Damping = 0.1f, Friction = 0.4f },
			Fabric.Stretch => new Settings { Stiffness = 0.2f + 0.5f * stiffness, Bend = 0.05f, Damping = 0.02f, Friction = 0.75f },
			_ => new Settings { Stiffness = 0.4f + 0.6f * stiffness, Bend = 0.15f, Damping = 0.03f, Friction = 0.6f },
		};
	}

	/// <summary>
	/// Pin weights for a garment: 1 rides its bone exactly, 0 is free cloth. The top band of the
	/// garment - the collar and the shoulder yoke of a shirt, the waistband of trousers - is what
	/// holds a garment on a body, and free cloth there slides down the chest or bunches at the
	/// neck the moment the wearer runs. The band fades from pinned at the top to free at
	/// <paramref name="fraction"/> of the garment's height, so there is no crease where it ends.
	/// </summary>
	public static float[] PinTop( PolyMesh garment, float fraction )
	{
		var pins = new float[garment.VertexCount];

		if ( garment.VertexCount == 0 || fraction <= 0f )
			return pins;

		var top = garment.Positions.Max( p => p.z );
		var bottom = garment.Positions.Min( p => p.z );
		var band = MathF.Max( (top - bottom) * fraction, 1e-3f );

		for ( var i = 0; i < pins.Length; i++ )
		{
			var down = top - garment.Positions[i].z;
			var t = Math.Clamp( 1f - down / band, 0f, 1f );
			pins[i] = t * t * (3f - 2f * t);
		}

		return pins;
	}

	/// <param name="garment">The finished garment, in the wearer's model space.</param>
	/// <param name="capsules">What it hangs on - see BoneCapsules. Their Bone indexes <paramref name="skeleton"/>.</param>
	/// <param name="settings">The fabric. Null writes none, and the component uses its own.</param>
	/// <param name="pins">Per-vertex pin weight, see <see cref="PinTop"/>. Null pins nothing.</param>
	public static string Write( PolyMesh garment, IReadOnlyList<Capsule> capsules, Skeleton skeleton, string material,
		Settings settings = null, float[] pins = null )
	{
		string Name( int bone ) => bone >= 0 && bone < skeleton.Count ? skeleton.Bones[bone].Name : "";

		var usable = capsules.Where( c => c.Bone >= 0 && c.Bone < skeleton.Count ).ToList();
		var sb = new StringBuilder();
		var inv = CultureInfo.InvariantCulture;

		string F( float v ) => float.IsFinite( v ) ? v.ToString( "0.####", inv ) : "0";

		sb.Append( "{\n" );
		sb.Append( $"  \"Material\": \"{Escape( material ?? "" )}\",\n" );

		if ( settings is not null )
			sb.Append( $"  \"Fabric\": {{\"Stiffness\":{F( settings.Stiffness )},\"Bend\":{F( settings.Bend )},"
				+ $"\"Damping\":{F( settings.Damping )},\"Friction\":{F( settings.Friction )}}},\n" );

		if ( pins is not null && pins.Length == garment.VertexCount && pins.Any( w => w > 0f ) )
		{
			sb.Append( "  \"Pins\": [" );
			sb.Append( string.Join( ",", pins.Select( F ) ) );
			sb.Append( "],\n" );
		}

		// Per vertex: its bind position, a UV (the first corner that names one - a seam's two
		// sides differ, and cloth is simulated per vertex, not per corner) and the bone it is
		// leashed to, which is the bone of the capsule it lies nearest.
		var uvs = new Vec2[garment.VertexCount];

		foreach ( var face in garment.Faces )
		{
			if ( face.UVs is null )
				continue;

			for ( var c = 0; c < face.Count && c < face.UVs.Length; c++ )
				if ( uvs[face.Indices[c]].Equals( default( Vec2 ) ) )
					uvs[face.Indices[c]] = face.UVs[c];
		}

		sb.Append( "  \"Positions\": [" );
		sb.Append( string.Join( ",", garment.Positions.Select( p => $"{F( p.x )},{F( p.y )},{F( p.z )}" ) ) );
		sb.Append( "],\n" );

		sb.Append( "  \"UVs\": [" );
		sb.Append( string.Join( ",", uvs.Select( u => $"{F( u.x )},{F( u.y )}" ) ) );
		sb.Append( "],\n" );

		sb.Append( "  \"Triangles\": [" );
		var tris = new List<int>();

		foreach ( var face in garment.Faces )
			for ( var c = 1; c + 1 < face.Count; c++ )
			{
				tris.Add( face.Indices[0] );
				tris.Add( face.Indices[c] );
				tris.Add( face.Indices[c + 1] );
			}

		sb.Append( string.Join( ",", tris ) );
		sb.Append( "],\n" );

		sb.Append( "  \"VertexBones\": [" );
		sb.Append( string.Join( ",", garment.Positions.Select( p =>
		{
			var best = -1;
			var bestD = float.MaxValue;

			foreach ( var c in usable )
			{
				var d = (c.Closest( p ) - p).Length - c.Radius;

				if ( d < bestD )
				{
					bestD = d;
					best = c.Bone;
				}
			}

			return $"\"{Escape( Name( best ) )}\"";
		} ) ) );
		sb.Append( "],\n" );

		sb.Append( "  \"Capsules\": [" );
		sb.Append( string.Join( ",", usable.Select( c =>
			$"{{\"Bone\":\"{Escape( Name( c.Bone ) )}\",\"A\":[{F( c.A.x )},{F( c.A.y )},{F( c.A.z )}],"
			+ $"\"B\":[{F( c.B.x )},{F( c.B.y )},{F( c.B.z )}],\"Radius\":{F( c.Radius )}}}" ) ) );
		sb.Append( "]\n" );

		sb.Append( "}\n" );
		return sb.ToString();
	}

	static string Escape( string s ) => s.Replace( "\\", "\\\\" ).Replace( "\"", "\\\"" );
}