Rendering/Primitives.cs

Utility class that loads two dev models (box and sphere) and creates tinted ModelRenderer GameObjects under a given parent. It provides factory methods to make spheres and boxes, and helpers to resize spheres by adjusting GameObject local scale based on model bounds.

File Access
namespace Coilgarden;

/// <summary>
/// Builds tinted primitive GameObjects under one parent, and remembers the models so each
/// view does not load them again.
/// <para>
/// The whole game is made of two shapes: a box and a sphere. Everything else is scale,
/// colour and lighting. That is the constraint the art direction is built around rather than
/// worked against - soft forms come from sphere shading and a warm key light, not from
/// modelled detail.
/// </para>
/// <para>
/// Pieces use the standard <b>lit</b> material with no override. Lighting is what turns a
/// sphere into a soft rounded form, and an unlit fill would throw that away for a flatness
/// nothing here wants. It does mean a tint renders slightly darker than authored, which the
/// palette already accounts for.
/// </para>
/// </summary>
public sealed class Primitives
{
	private const string BoxModelPath = "models/dev/box.vmdl";
	private const string SphereModelPath = "models/dev/sphere.vmdl";

	private readonly GameObject root;
	private readonly Model box;
	private readonly Model sphere;

	public Primitives( GameObject root )
	{
		this.root = root;

		box = Model.Load( BoxModelPath );
		sphere = Model.Load( SphereModelPath );
	}

	/// <summary>A sphere of the given diameter, centred on <paramref name="position"/>.</summary>
	public ModelRenderer Sphere( string name, Vector3 position, float diameter, Color tint ) =>
		Create( name, sphere, position, new Vector3( diameter, diameter, diameter ), tint );

	/// <summary>A box of the given size, centred on <paramref name="position"/>.</summary>
	public ModelRenderer Box( string name, Vector3 position, Vector3 size, Color tint ) =>
		Create( name, box, position, size, tint );

	/// <summary>Rescales an existing sphere in place, for anything that changes size per frame.</summary>
	public void Resize( ModelRenderer renderer, float diameter )
	{
		if ( !renderer.IsValid() ) return;

		renderer.GameObject.LocalScale = ScaleFor( sphere, new Vector3( diameter, diameter, diameter ) );
	}

	/// <summary>
	/// Rescales a sphere to different diameters per axis, turning it into an ellipsoid. This is
	/// how squash and stretch is done here - there is no deformation, just a non-uniform scale
	/// on a ball.
	/// </summary>
	public void Resize( ModelRenderer renderer, Vector3 diameters )
	{
		if ( !renderer.IsValid() ) return;

		renderer.GameObject.LocalScale = ScaleFor( sphere, diameters );
	}

	private ModelRenderer Create( string name, Model model, Vector3 position, Vector3 size, Color tint )
	{
		var obj = new GameObject( root, true, name );

		// Runtime visuals must never be written into the scene file.
		obj.Flags |= GameObjectFlags.NotSaved;
		obj.LocalPosition = position;
		obj.LocalScale = ScaleFor( model, size );

		var renderer = obj.Components.Create<ModelRenderer>();
		renderer.Model = model;
		renderer.Tint = tint;

		return renderer;
	}

	/// <summary>The dev models are whatever size they are, so scale them to the size asked for.</summary>
	private static Vector3 ScaleFor( Model model, Vector3 size )
	{
		var bounds = model.Bounds.Size;

		return new Vector3(
			bounds.x > 0f ? size.x / bounds.x : 1f,
			bounds.y > 0f ? size.y / bounds.y : 1f,
			bounds.z > 0f ? size.z / bounds.z : 1f );
	}
}