Code/Cloth/GarmentCloth.cs

A component that simulates and renders a garment as position-based cloth. It loads a .cloth.json produced by Effigy, maps vertices to bones and capsules, runs a stable particle-based solver with distance and bend constraints, handles collision against body capsules, leash/pins/inertia, and builds a double-sided mesh for rendering in editor and runtime.

File AccessNative Interop
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Marionette;

/// <summary>
/// A garment worn as real cloth: it hangs off the wearer's shoulders and swings when they move,
/// instead of riding the bones stiffly the way a .clothing model does.
///
/// FED BY EFFIGY. Clothing > Publish > Make live cloth writes a .cloth.json: the garment in the
/// wearer's bind pose, the capsules it hangs on and which bone each capsule and each bit of cloth
/// belongs to. This reads it, puts the capsules on the live bones every frame, and steps the cloth
/// against them.
///
/// POSITION-BASED, the way game cloth is done: particles, distance constraints along every edge
/// and across every fold, collision against round capsules rather than triangles. It is stable at
/// any frame rate, which a spring simulation is not, and it costs a few milliseconds for a
/// T-shirt. It also runs in the editor, so a garment can be watched settling without pressing Play.
///
/// NO SELF-COLLISION, AND NO CLOTH-ON-CLOTH. The cloth collides with the body's capsules and
/// nothing else: a fold can pass through itself, and a jacket over a shirt passes through the
/// shirt. That is the usual first version of a game cloth solver and it is what this is; the
/// tool says so where it writes the file rather than letting a layered outfit be a surprise.
///
/// LEASHED. Every particle is held within Leash of where the garment was fitted relative to its
/// bone. Free cloth left behind by a teleport, a respawn or a frame hitch would otherwise be found
/// on the floor; the leash is the long-range attachment every shipped cloth solver has for that.
/// </summary>
[Title( "Garment Cloth" ), Category( "Marionette" ), Icon( "checkroom" )]
public sealed class GarmentCloth : Component, Component.ExecuteInEditor
{
	/// <summary>The character wearing it. Found on this object or its parents when left empty.</summary>
	[Property] public SkinnedModelRenderer Wearer { get; set; }

	/// <summary>The .cloth.json Effigy wrote, e.g. models/effigy/shirt.cloth.json.</summary>
	[Property] public string Cloth { get; set; }

	/// <summary>What it is drawn with. Empty uses the material the garment was given in Effigy.</summary>
	[Property] public Material Material { get; set; }

	/// <summary>Use the fabric the garment was given in Effigy. Off, the sliders below are used.</summary>
	[Property] public bool FabricFromFile { get; set; } = true;

	/// <summary>1 holds its length like canvas; low values let it stretch like jersey.</summary>
	[Property, Range( 0.05f, 1f )] public float Stiffness { get; set; } = 0.9f;

	/// <summary>How much it resists folding. Low hangs in soft folds, high stays in panels.</summary>
	[Property, Range( 0f, 1f )] public float Bend { get; set; } = 0.15f;

	/// <summary>Inches per second squared. Source's own gravity is 386.</summary>
	[Property] public float Gravity { get; set; } = 386f;

	/// <summary>Velocity lost per step. Higher is heavier, stiller cloth.</summary>
	[Property, Range( 0f, 0.5f )] public float Damping { get; set; } = 0.03f;

	/// <summary>How far any bit of cloth may swing from where it was fitted, in inches.</summary>
	[Property, Range( 0.5f, 24f )] public float Leash { get; set; } = 6f;

	/// <summary>How much of the wearer walking and turning the cloth feels. 0 is carried along
	/// as if sewn on and only swings with the limbs; 1 is left standing in the world and dragged
	/// after them by the leash.</summary>
	[Property, Range( 0f, 1f )] public float Inertia { get; set; } = 0.2f;

	/// <summary>How far off the capsules the cloth sits, so it does not z-fight the body.</summary>
	[Property, Range( 0f, 2f )] public float Skin { get; set; } = 0.25f;

	/// <summary>How much of a slide a contact takes away. High is a shirt that rests on a shoulder instead of sliding off it.</summary>
	[Property, Range( 0f, 1f )] public float Friction { get; set; } = 0.6f;

	[Property, Range( 1, 16 )] public int Iterations { get; set; } = 6;

	/// <summary>Hold the collar and the yoke on the body, the way Effigy marked them. Off, the
	/// whole garment is free cloth on a leash.</summary>
	[Property] public bool Pins { get; set; } = true;

	/// <summary>Draw the capsules the cloth hangs on.</summary>
	[Property] public bool ShowColliders { get; set; }

	sealed class ClothData
	{
		public string Material { get; set; }
		public FabricData Fabric { get; set; }
		public float[] Pins { get; set; }
		public float[] Positions { get; set; }
		public float[] UVs { get; set; }
		public int[] Triangles { get; set; }
		public string[] VertexBones { get; set; }
		public CapsuleData[] Capsules { get; set; }
	}

	sealed class FabricData
	{
		public float Stiffness { get; set; } = 0.9f;
		public float Bend { get; set; } = 0.15f;
		public float Damping { get; set; } = 0.03f;
		public float Friction { get; set; } = 0.6f;
	}

	sealed class CapsuleData
	{
		public string Bone { get; set; }
		public float[] A { get; set; }
		public float[] B { get; set; }
		public float Radius { get; set; }
	}

	// The loaded garment, in bone space.
	string _loaded;
	string[] _bones;
	int[] _vertexBone;
	float[] _pins;
	FabricData _fabric;
	Vector3[] _local;
	Vector2[] _uv;
	int[] _triangles;
	(int Bone, Vector3 A, Vector3 B, float Radius)[] _capsules;
	(int A, int B, float Length, bool Bend)[] _links;

	// The simulation, in world space.
	Vector3[] _x;
	Vector3[] _prev;
	Transform[] _boneWorld;
	(Vector3 A, Vector3 B, float Radius)[] _capsWorld;
	Transform? _frame;

	// The drawing.
	GameObject _drawn;
	Mesh _mesh;
	List<SimpleVertex> _vertices;

	protected override void OnEnabled()
	{
		Wearer ??= GetComponentInParent<SkinnedModelRenderer>( true, true )
			?? GetComponentInChildren<SkinnedModelRenderer>( true, true );
	}

	protected override void OnDisabled()
	{
		_drawn?.Destroy();
		_drawn = null;
		_mesh = null;
		_loaded = null;
		_frame = null;
	}

	// Pre-render, not update: the animation has posed the bones by then, so the capsules are where
	// the body is drawn this frame rather than where it was drawn last frame.
	protected override void OnPreRender()
	{
		if ( !Wearer.IsValid() || Wearer.Model is null || string.IsNullOrEmpty( Cloth ) )
			return;

		if ( _loaded != Cloth && !Load() )
			return;

		ReadBones();
		Carry();

		// A frame hitch in the editor can be a second long. Stepping a second of cloth at once is
		// how cloth explodes; stepping a thirtieth and letting the leash catch the rest is not.
		var dt = Math.Clamp( Time.Delta, 1f / 240f, 1f / 30f );
		const int substeps = 2;
		var h = dt / substeps;

		for ( var s = 0; s < substeps; s++ )
			Step( h );

		Draw();

		if ( ShowColliders )
			foreach ( var (a, b, r) in _capsWorld )
				Gizmo.Draw.LineCapsule( new Capsule( a, b, r ) );
	}

	bool Load()
	{
		OnDisabled();
		_loaded = Cloth;
		_boneWorld = null;
		_capsWorld = null;

		ClothData data;

		try
		{
			data = Json.Deserialize<ClothData>( FileSystem.Mounted.ReadAllText( Cloth ) );
		}
		catch ( Exception e )
		{
			Log.Warning( $"[GarmentCloth] could not read {Cloth}: {e.Message}" );
			return false;
		}

		if ( data?.Positions is null || data.Triangles is null || data.Capsules is null || data.Capsules.Length == 0 )
		{
			Log.Warning( $"[GarmentCloth] {Cloth} has no cloth in it - make it again with Make live cloth." );
			return false;
		}

		var model = Wearer.Model;
		var bones = new List<string>();
		var bind = new List<Transform>();

		int BoneIndex( string name )
		{
			var i = bones.IndexOf( name );

			if ( i >= 0 )
				return i;

			// A bone the model does not have (renamed since the cloth was made) still needs a
			// frame to hang in; the model's root is the honest fallback, and says so once.
			if ( string.IsNullOrEmpty( name ) || !model.Bones.HasBone( name ) )
			{
				if ( !string.IsNullOrEmpty( name ) )
					Log.Warning( $"[GarmentCloth] {model.Name} has no bone {name}; that part of the cloth hangs from the root." );

				name = model.Bones.Root?.Name ?? "";
				i = bones.IndexOf( name );

				if ( i >= 0 )
					return i;
			}

			bones.Add( name );
			bind.Add( model.GetBoneTransform( name ) );
			return bones.Count - 1;
		}

		static Vector3 V( float[] a, int i ) => new( a[i], a[i + 1], a[i + 2] );

		var count = data.Positions.Length / 3;
		_vertexBone = new int[count];
		_local = new Vector3[count];
		_uv = new Vector2[count];

		for ( var i = 0; i < count; i++ )
		{
			var bone = BoneIndex( data.VertexBones is not null && i < data.VertexBones.Length ? data.VertexBones[i] : "" );
			_vertexBone[i] = bone;
			_local[i] = bind[bone].PointToLocal( V( data.Positions, i * 3 ) );

			if ( data.UVs is not null && i * 2 + 1 < data.UVs.Length )
				_uv[i] = new Vector2( data.UVs[i * 2], data.UVs[i * 2 + 1] );
		}

		_capsules = data.Capsules.Select( c =>
		{
			var bone = BoneIndex( c.Bone );
			return (bone, bind[bone].PointToLocal( V( c.A, 0 ) ), bind[bone].PointToLocal( V( c.B, 0 ) ), c.Radius);
		} ).ToArray();

		_bones = bones.ToArray();
		_triangles = data.Triangles;
		_fabric = data.Fabric;
		_pins = data.Pins is { } p && p.Length == count ? p : null;
		_links = BuildLinks( data.Positions, _triangles );

		if ( Material is null && !string.IsNullOrEmpty( data.Material ) )
			Material = Material.Load( data.Material );

		// Start the cloth where it was fitted, on the bones as they are now.
		ReadBones();
		_x = new Vector3[count];
		_prev = new Vector3[count];

		for ( var i = 0; i < count; i++ )
			_x[i] = _prev[i] = _boneWorld[_vertexBone[i]].PointToWorld( _local[i] );

		return true;
	}

	/// <summary>Every edge, plus a link across every fold - the two corners opposite a shared
	/// edge. The edges hold the length; the cross links are what stops the cloth folding flat
	/// like paper, and Bend sets how hard they push.</summary>
	static (int, int, float, bool)[] BuildLinks( float[] positions, int[] tris )
	{
		Vector3 P( int i ) => new( positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2] );

		var links = new List<(int, int, float, bool)>();
		var edges = new Dictionary<(int, int), int>();

		void Edge( int a, int b, int opposite )
		{
			var key = a < b ? (a, b) : (b, a);

			if ( edges.TryGetValue( key, out var other ) )
			{
				if ( other >= 0 && other != opposite )
				{
					links.Add( (other, opposite, (P( other ) - P( opposite )).Length, true) );
					edges[key] = -1;
				}

				return;
			}

			edges[key] = opposite;
			links.Add( (a, b, (P( a ) - P( b )).Length, false) );
		}

		for ( var t = 0; t + 2 < tris.Length; t += 3 )
		{
			Edge( tris[t], tris[t + 1], tris[t + 2] );
			Edge( tris[t + 1], tris[t + 2], tris[t] );
			Edge( tris[t + 2], tris[t], tris[t + 1] );
		}

		return links.ToArray();
	}

	void ReadBones()
	{
		_boneWorld ??= new Transform[_bones?.Length ?? 0];

		if ( _bones is null )
			return;

		if ( _boneWorld.Length != _bones.Length )
			_boneWorld = new Transform[_bones.Length];

		for ( var b = 0; b < _bones.Length; b++ )
		{
			// Not animating (the editor, a ragdoll with no graph) still has a pose: the bind pose,
			// placed where the renderer is.
			_boneWorld[b] = Wearer.TryGetBoneTransform( _bones[b], out var tx )
				? tx
				: Wearer.WorldTransform.ToWorld( Wearer.Model.GetBoneTransform( _bones[b] ) );
		}

		_capsWorld ??= new (Vector3, Vector3, float)[_capsules.Length];

		for ( var c = 0; c < _capsules.Length; c++ )
		{
			var (bone, a, b, r) = _capsules[c];
			var world = _boneWorld[bone];
			_capsWorld[c] = (world.PointToWorld( a ), world.PointToWorld( b ), r * world.UniformScale);
		}
	}

	/// <summary>
	/// Move the cloth with the wearer before it is stepped. A simulation in world space knows
	/// nothing about the character walking off: the body slides forward through the shirt, the
	/// front is shoved along by the chest and the back is left hanging in the air behind them,
	/// clipping into the body. So the particles are carried in the wearer's frame, and Inertia is
	/// the fraction of the wearer's motion that is left out, which is the swing a walk gives it.
	/// </summary>
	void Carry()
	{
		var now = Wearer.WorldTransform;

		if ( _frame is { } was )
		{
			var carry = 1f - Inertia;

			for ( var i = 0; i < _x.Length; i++ )
			{
				var to = now.PointToWorld( was.PointToLocal( _x[i] ) );
				_x[i] += (to - _x[i]) * carry;

				to = now.PointToWorld( was.PointToLocal( _prev[i] ) );
				_prev[i] += (to - _prev[i]) * carry;
			}
		}

		_frame = now;
	}

	// The fabric in use: the file's when it has one and FabricFromFile is on, else the sliders.
	float StiffnessNow => FabricFromFile && _fabric is { } f ? f.Stiffness : Stiffness;
	float BendNow => FabricFromFile && _fabric is { } f ? f.Bend : Bend;
	float DampingNow => FabricFromFile && _fabric is { } f ? f.Damping : Damping;
	float FrictionNow => FabricFromFile && _fabric is { } f ? f.Friction : Friction;

	void Step( float h )
	{
		var gravity = Vector3.Down * Gravity * h * h;
		var keep = 1f - DampingNow;
		var stiffness = StiffnessNow;

		for ( var i = 0; i < _x.Length; i++ )
		{
			var velocity = (_x[i] - _prev[i]) * keep;
			_prev[i] = _x[i];
			_x[i] += velocity + gravity;
		}

		var bend = BendNow * BendNow;

		for ( var it = 0; it < Iterations; it++ )
		{
			foreach ( var (a, b, rest, isBend) in _links )
			{
				var d = _x[a] - _x[b];
				var len = d.Length;

				if ( len < 1e-6f )
					continue;

				var k = isBend ? bend : stiffness;
				var correction = d * ((len - rest) / len * 0.5f * k);
				_x[a] -= correction;
				_x[b] += correction;
			}
		}

		// Once per step, after the links: the position the step ends on is the one that has to
		// be off the body, and doing it per iteration was most of the frame.
		Collide();
		Tether();
		Pin();
	}

	/// <summary>
	/// Hold the pinned band where the garment was fitted, relative to its bone. A weight of 1 is
	/// sewn to the bone; in between is a soft pull, so the yoke of a shirt stays on the shoulders
	/// and the body of it hangs free from there. The velocity goes with it, or the band jitters
	/// between the pin and the fall.
	/// </summary>
	void Pin()
	{
		if ( !Pins || _pins is null )
			return;

		for ( var i = 0; i < _x.Length; i++ )
		{
			var w = _pins[i];

			if ( w <= 0f )
				continue;

			var home = _boneWorld[_vertexBone[i]].PointToWorld( _local[i] );
			var pull = (home - _x[i]) * w;
			_x[i] += pull;
			_prev[i] += pull;
		}
	}

	void Collide()
	{
		var friction = FrictionNow;

		for ( var i = 0; i < _x.Length; i++ )
		{
			foreach ( var (a, b, radius) in _capsWorld )
			{
				var ab = b - a;
				var lenSq = ab.LengthSquared;
				var t = lenSq < 1e-8f ? 0f : Math.Clamp( Vector3.Dot( _x[i] - a, ab ) / lenSq, 0f, 1f );
				var core = a + ab * t;
				var d = _x[i] - core;
				var dist = d.Length;
				var want = radius + Skin;

				if ( dist >= want )
					continue;

				var normal = dist > 1e-5f ? d / dist : Vector3.Up;
				var pushed = core + normal * want;

				// Friction: a contact keeps little of the slide it did this step, so a shirt rests
				// on a shoulder instead of sliding off it.
				var moved = pushed - _prev[i];
				var slide = moved - normal * Vector3.Dot( moved, normal );
				_x[i] = pushed - slide * friction;
			}
		}
	}

	void Tether()
	{
		for ( var i = 0; i < _x.Length; i++ )
		{
			var home = _boneWorld[_vertexBone[i]].PointToWorld( _local[i] );
			var d = _x[i] - home;
			var len = d.Length;

			if ( len > Leash )
			{
				_x[i] = home + d * (Leash / len);

				// Take the velocity with it, or the particle snaps straight back out next step.
				_prev[i] = _x[i];
			}
		}
	}

	/// <summary>
	/// Stream the particles into the mesh. Both sides are drawn - a shirt is seen from inside at
	/// the collar and the hem - so every vertex is written twice, the back copy with its normal
	/// turned round and its triangles wound the other way.
	/// </summary>
	void Draw()
	{
		var n = _x.Length;
		var normals = new Vector3[n];

		for ( var t = 0; t + 2 < _triangles.Length; t += 3 )
		{
			var a = _triangles[t];
			var b = _triangles[t + 1];
			var c = _triangles[t + 2];
			var face = Vector3.Cross( _x[b] - _x[a], _x[c] - _x[a] );
			normals[a] += face;
			normals[b] += face;
			normals[c] += face;
		}

		if ( !_drawn.IsValid() )
			Build();

		var space = _drawn.WorldTransform;
		var bounds = BBox.FromPositionAndSize( space.PointToLocal( _x[0] ), 1f );

		for ( var i = 0; i < n; i++ )
		{
			var p = space.PointToLocal( _x[i] );
			var normal = space.Rotation.Inverse * normals[i].Normal;
			var tangent = Vector3.Cross( normal, Vector3.Up ).Normal;

			_vertices[i] = new SimpleVertex( p, normal, tangent, _uv[i] );
			_vertices[i + n] = new SimpleVertex( p, -normal, tangent, _uv[i] );
			bounds = bounds.AddPoint( p );
		}

		_mesh.SetVertexBufferData( _vertices, 0 );
		_mesh.Bounds = bounds;
	}

	void Build()
	{
		var n = _x.Length;
		_vertices = new List<SimpleVertex>( new SimpleVertex[n * 2] );

		var indices = new List<int>( _triangles.Length * 2 );
		indices.AddRange( _triangles );

		for ( var t = 0; t + 2 < _triangles.Length; t += 3 )
		{
			indices.Add( _triangles[t] + n );
			indices.Add( _triangles[t + 2] + n );
			indices.Add( _triangles[t + 1] + n );
		}

		_mesh = new Mesh( Material ?? Material.Load( "materials/default.vmat" ) );
		_mesh.CreateVertexBuffer<SimpleVertex>( _vertices.Count, _vertices );
		_mesh.CreateIndexBuffer( indices.Count, indices );

		// A child that is never saved: the cloth is rebuilt from the file every time, and a
		// scene that serialised the generated model would carry a stale copy of it forever.
		_drawn = new GameObject( GameObject, true, "cloth" );
		_drawn.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.Hidden;

		var renderer = _drawn.Components.Create<ModelRenderer>();
		renderer.Model = Model.Builder.AddMesh( _mesh ).Create();
	}
}