Effigy.Tests/FabricMaterialTests.cs

Unit test class for FabricMaterial. It runs a suite of deterministic checks that fabric presets produce valid color/normal/roughness maps, normals point outward and are unit-length, plaid shows the second color, repeated calls with same seed produce identical tiles, tiles wrap at edges, and generated vmat source contains expected flags.

using System;
using System.Linq;

namespace Effigy.Tests;

/// <summary>The fabric tiles: every preset makes a full set, the normals point out, plaid shows
/// its second colour, and the same settings make the same pixels every time.</summary>
public static class FabricMaterialTests
{
	public static void Run()
	{
		Report.Section( "fabric: every preset writes a usable tile" );

		var blue = new Vec3( 0.2f, 0.3f, 0.6f );
		var cream = new Vec3( 0.9f, 0.88f, 0.8f );
		var n = FabricMaterial.Size * FabricMaterial.Size;

		foreach ( var preset in FabricMaterial.Presets )
		{
			var (color, normal, rough) = FabricMaterial.Maps( preset, blue, cream, 6f, 0.2f );

			var full = color.Length == n * 4 && normal.Length == n * 4 && rough.Length == n * 4;
			var outward = Enumerable.Range( 0, n ).All( i => normal[i * 4 + 2] > 100 );
			var unit = Enumerable.Range( 0, n ).Take( 4096 ).All( i =>
			{
				float x = normal[i * 4] / 127.5f - 1f, y = normal[i * 4 + 1] / 127.5f - 1f, z = normal[i * 4 + 2] / 127.5f - 1f;
				return MathF.Abs( x * x + y * y + z * z - 1f ) < 0.05f;
			} );
			var textured = Enumerable.Range( 0, n ).Select( i => (int)color[i * 4 + 2] ).Distinct().Count() > 4;

			Report.Check( $"{preset}: full maps, outward unit normals, some texture", full && outward && unit && textured );
		}

		var (plaid, _, _) = FabricMaterial.Maps( "Plaid", blue, cream, 4f, 0f );
		var pale = Enumerable.Range( 0, n ).Count( i => plaid[i * 4] > 180 && plaid[i * 4 + 2] > 160 );
		Report.Check( "plaid shows the second colour over about a quarter of the tile", pale > n / 6 && pale < n / 2, $"{pale * 100 / n}%" );

		var (a, _, _) = FabricMaterial.Maps( "Denim", blue, cream, 8f, 0.3f, 5 );
		var (b, _, _) = FabricMaterial.Maps( "Denim", blue, cream, 8f, 0.3f, 5 );
		Report.Check( "the same settings make the same tile", a.SequenceEqual( b ) );

		// The tile wraps: the column at x = 0 continues from x = size - 1, so a row's jump across
		// the edge is no bigger than a typical jump inside it.
		var (canvas, _, _) = FabricMaterial.Maps( "Canvas", blue, cream, 4f, 0f );
		var size = FabricMaterial.Size;
		var edge = 0; var inner = 0;
		for ( var y = 0; y < size; y++ )
		{
			edge += Math.Abs( canvas[(y * size) * 4] - canvas[(y * size + size - 1) * 4] );
			inner += Math.Abs( canvas[(y * size + size / 2) * 4] - canvas[(y * size + size / 2 - 1) * 4] );
		}
		Report.Check( "the tile wraps at its edge", edge <= inner * 2 + size, $"edge {edge}, inner {inner}" );

		var vmat = FabricMaterial.VmatSource( "Fleece", "c.png", "n.png", "r.png" );
		Report.Check( "fleece asks Complex for cloth shading", vmat.Contains( "F_CLOTH_SHADING 1" ) && vmat.Contains( "complex.vfx" ) );
		Report.Check( "satin asks for anisotropic gloss", FabricMaterial.VmatSource( "Satin", "c", "n", "r" ).Contains( "F_ANISOTROPIC_GLOSS 1" ) );
		Report.Check( "denim asks for neither", !FabricMaterial.VmatSource( "Denim", "c", "n", "r" ).Contains( "F_" ) );
	}
}