Effigy.Tests/AtlasIdTests.cs
using System;
using Effigy;
using static Effigy.Tests.Report;
namespace Effigy.Tests;
/// <summary>
/// The atlas id, judged by the one thing it exists for: it must tell "same mesh, re-unwrapped" from
/// "same mesh, same atlas", because <see cref="MultiresSculpt.TopologyId"/> cannot.
/// </summary>
public static class AtlasIdTests
{
public static void Run()
{
Section( "atlas id: a re-unwrap is a different atlas" );
TestUnwrappingTheSameMeshTwiceGivesTheSameId();
TestAMarginChangeChangesTheId();
TestMovingAVertexDoesNotChangeTheId();
TestTopologyIdIgnoresTheAtlasAndAtlasIdDoesNot();
TestAnEmptyMeshReturnsAStableValue();
TestRasteriseIsVisibleAndHandsBackUsableBarycentrics();
}
static void TestUnwrappingTheSameMeshTwiceGivesTheSameId()
{
// Unwrapping is deterministic (UnwrapTests already asserts that), so two unwraps of the same
// mesh land on the same atlas and must therefore land on the same id. If this fails the id
// is hashing something unstable, and a cache keyed on it would re-render for no reason.
var a = Primitives.Box( 2, 2, 2 );
var b = Primitives.Box( 2, 2, 2 );
UVUnwrap.Unwrap( a );
UVUnwrap.Unwrap( b );
Check( "two unwraps of the same mesh give the same id", AtlasId.Of( a ) == AtlasId.Of( b ) );
}
static void TestAMarginChangeChangesTheId()
{
// The gutter between islands moves every island in the atlas, so the id has to change. This
// is the whole reason AtlasId exists.
var tight = Primitives.Box( 2, 2, 2 );
UVUnwrap.Unwrap( tight, margin: 0f );
var spaced = Primitives.Box( 2, 2, 2 );
UVUnwrap.Unwrap( spaced, margin: 0.05f );
Check( "a different island margin changes the id", AtlasId.Of( tight ) != AtlasId.Of( spaced ) );
}
static void TestMovingAVertexDoesNotChangeTheId()
{
// The id hashes UVs, not positions, so a parametric edit that moves geometry but leaves the
// atlas alone must not change it. That is what lets a cached canvas survive a normal edit.
var a = Primitives.Box( 2, 2, 2 );
UVUnwrap.Unwrap( a );
var moved = a.Clone();
moved.Positions[0] += new Vec3( 1, 2, 3 );
Check( "moving a vertex without touching UVs keeps the id", AtlasId.Of( a ) == AtlasId.Of( moved ) );
}
static void TestTopologyIdIgnoresTheAtlasAndAtlasIdDoesNot()
{
// THE POINT OF THE FILE. TopologyId ignores UVs, so it is the same across a margin change
// that rearranges the atlas. AtlasId must not be. A cache keyed on TopologyId alone would
// hand back a canvas built for the old atlas; keyed on both, it can tell.
var tight = Primitives.Box( 2, 2, 2 );
UVUnwrap.Unwrap( tight, margin: 0f );
var spaced = Primitives.Box( 2, 2, 2 );
UVUnwrap.Unwrap( spaced, margin: 0.05f );
Check( "TopologyId is unchanged by the margin change",
MultiresSculpt.TopologyId( tight ) == MultiresSculpt.TopologyId( spaced ) );
Check( "AtlasId is not", AtlasId.Of( tight ) != AtlasId.Of( spaced ) );
}
static void TestAnEmptyMeshReturnsAStableValue()
{
// An empty mesh has no UVs to hash, so the id is just the FNV offset basis. It must not
// throw and must be the same every call, or a document with an empty body breaks a cache
// check.
var a = AtlasId.Of( new PolyMesh() );
var b = AtlasId.Of( new PolyMesh() );
Check( "an empty mesh returns a stable value", a == b, $"{a} vs {b}" );
}
static void TestRasteriseIsVisibleAndHandsBackUsableBarycentrics()
{
// Rasterise is internal now so the paint dab can walk the same texels the bake walks. Called
// here, the fact that this line compiles is the proof of the visibility change; the
// barycentrics it hands back must reconstruct the texel centre, because that reconstruction
// is how a dab turns a texel into a 3D point to weight by distance.
var a = new Vec2( 0, 0 );
var b = new Vec2( 1, 0 );
var c = new Vec2( 0, 1 );
var count = 0;
NormalBake.Rasterise( a, b, c, 4, 4, ( x, y, wa, wb, wc ) =>
{
count++;
var centre = new Vec2( (x + 0.5f) / 4f, (y + 0.5f) / 4f );
var rebuilt = a * wa + b * wb + c * wc;
Check( $"texel ({x},{y}) barycentrics sum to one", MathF.Abs( wa + wb + wc - 1f ) < 1e-5f,
$"{wa} + {wb} + {wc} = {wa + wb + wc}" );
Check( $"texel ({x},{y}) barycentrics reconstruct the centre",
MathF.Abs( rebuilt.x - centre.x ) < 1e-5f && MathF.Abs( rebuilt.y - centre.y ) < 1e-5f,
$"({rebuilt.x}, {rebuilt.y}) vs ({centre.x}, {centre.y})" );
} );
// The lower-left half of a 4x4 map: every centre with x > 0, y > 0 and x + y <= 1, which is
// ten texels. The exact number is the point — it pins the top-left fill rule in place.
Check( "the triangle walks exactly ten texels", count == 10, $"{count}" );
}
}