dxc 1.9.0.5402 lowers float2/3/4 normalize(x) to x * Rsqrt(Dot(x, x)), and DXIL.rst defines Rsqrt as 1 / sqrt(src). A zero vector normalizes to NaN and a NaN component poisons every component. CultMath divided by max(length, 1e-20), which returned 0 for a zero vector and a huge finite component beside a NaN one. - math.normalize(float2/3/4) is now value * (1 / sqrt(dot(value, value))). - The HLSL mirror drops cultmath_normalize and its epsilon constant: it now equals the normalize intrinsic, and nothing consumed it. - HlslSemanticsTests pins zero and NaN normalize, the rsqrt bit pattern, and zero length/distance. design.md states the rule and notes that DXIL.rst's Rsqrt special-value table is a copy of Round's. - GameCult.Geometry's iso-surface gave degenerate triangles (an inside vertex on the isovalue) a zero normal through the old epsilon. It now guards them explicitly, with a test. - quaternion normalize keeps its identity fallback: quaternion is not an HLSL type. - Bump to 0.2.2 and rebuild the tracked Unity DLL and pdb. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
106 lines
3.5 KiB
C#
106 lines
3.5 KiB
C#
using System;
|
|
using FluentAssertions;
|
|
using NUnit.Framework;
|
|
|
|
namespace GameCult.Geometry.Tests
|
|
{
|
|
[TestFixture]
|
|
public sealed class IsoSurfaceTests
|
|
{
|
|
[TestCase(-1f)]
|
|
[TestCase(1f)]
|
|
public void Uniform_field_has_no_surface(float value)
|
|
{
|
|
var samples = Filled(2, value);
|
|
|
|
var mesh = CultGeometryIsoSurface.Extract(samples);
|
|
|
|
mesh.Positions.Should().BeEmpty();
|
|
mesh.Normals.Should().BeEmpty();
|
|
mesh.Indices.Should().BeEmpty();
|
|
}
|
|
|
|
[Test]
|
|
public void Plane_field_interpolates_to_the_isovalue_and_faces_outward()
|
|
{
|
|
var samples = new float[2, 2, 2];
|
|
for (var y = 0; y < 2; y++)
|
|
for (var z = 0; z < 2; z++)
|
|
{
|
|
samples[0, y, z] = -0.5f;
|
|
samples[1, y, z] = 0.5f;
|
|
}
|
|
|
|
var mesh = CultGeometryIsoSurface.Extract(samples);
|
|
|
|
mesh.TriangleCount.Should().BeGreaterThan(0);
|
|
for (var index = 0; index < mesh.Positions.Length; index += 3)
|
|
{
|
|
mesh.Positions[index].Should().BeApproximately(0.5f, 1e-6f);
|
|
}
|
|
|
|
for (var index = 0; index < mesh.Normals.Length; index += 3)
|
|
{
|
|
mesh.Normals[index].Should().BeApproximately(1f, 1e-6f);
|
|
mesh.Normals[index + 1].Should().BeApproximately(0f, 1e-6f);
|
|
mesh.Normals[index + 2].Should().BeApproximately(0f, 1e-6f);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Samples_on_the_isovalue_emit_finite_normals()
|
|
{
|
|
var samples = new float[2, 2, 2];
|
|
for (var y = 0; y < 2; y++)
|
|
for (var z = 0; z < 2; z++)
|
|
{
|
|
samples[0, y, z] = 0f;
|
|
samples[1, y, z] = 1f;
|
|
}
|
|
|
|
var mesh = CultGeometryIsoSurface.Extract(samples);
|
|
|
|
mesh.TriangleCount.Should().BeGreaterThan(0);
|
|
mesh.Normals.Should().OnlyContain(value => float.IsFinite(value));
|
|
}
|
|
|
|
[Test]
|
|
public void Extraction_is_deterministic_and_applies_origin_and_cell_size()
|
|
{
|
|
var samples = new float[2, 2, 2];
|
|
samples[0, 0, 0] = -1f;
|
|
samples[1, 0, 0] = 1f;
|
|
samples[0, 1, 0] = 1f;
|
|
samples[1, 1, 0] = 1f;
|
|
samples[0, 0, 1] = 1f;
|
|
samples[1, 0, 1] = 1f;
|
|
samples[0, 1, 1] = 1f;
|
|
samples[1, 1, 1] = 1f;
|
|
|
|
var first = CultGeometryIsoSurface.Extract(samples, origin: new CultVec3(10f, 20f, 30f), cellSize: 2f);
|
|
var second = CultGeometryIsoSurface.Extract(samples, origin: new CultVec3(10f, 20f, 30f), cellSize: 2f);
|
|
|
|
second.Positions.Should().Equal(first.Positions);
|
|
second.Normals.Should().Equal(first.Normals);
|
|
second.Indices.Should().Equal(first.Indices);
|
|
first.Positions.Should().OnlyContain(value => value >= 10f);
|
|
}
|
|
|
|
[Test]
|
|
public void Invalid_field_shape_is_rejected()
|
|
{
|
|
Action act = () => CultGeometryIsoSurface.Extract(new float[1, 2, 2]);
|
|
|
|
act.Should().Throw<ArgumentException>();
|
|
}
|
|
|
|
private static float[,,] Filled(int size, float value)
|
|
{
|
|
var samples = new float[size, size, size];
|
|
for (var x = 0; x < size; x++)
|
|
for (var y = 0; y < size; y++)
|
|
for (var z = 0; z < size; z++) samples[x, y, z] = value;
|
|
return samples;
|
|
}
|
|
}
|
|
}
|