feat(render): implement Campaign AR and terrain fidelity

This commit is contained in:
Erik 2026-08-22 13:13:29 +02:00
parent 99cf26e00c
commit 7a5f96ede5
368 changed files with 50611 additions and 950 deletions

View file

@ -226,4 +226,148 @@ public class LandblockMeshTests
Assert.Equal(10.0f, atX48Y0.Position.Z);
Assert.Equal(0.0f, atX0Y48.Position.Z);
}
[Fact]
public void Build_NormalsMatchRetailIncidentFaceAverages_NotCentralDifferences()
{
// A deliberately non-planar surface makes retail's split-aware
// incident-plane average observably different from the former
// central-difference approximation.
var block = BuildFlatLandBlock();
for (int x = 0; x < LandblockMesh.HeightmapSide; x++)
for (int y = 0; y < LandblockMesh.HeightmapSide; y++)
block.Height[x * LandblockMesh.HeightmapSide + y] =
(byte)((x * x * 3 + y * y * 5 + x * y * 11 + x * 7 + y * 13) % 96);
const uint landblockX = 0xA9;
const uint landblockY = 0xB4;
var mesh = LandblockMesh.Build(
block,
landblockX,
landblockY,
IdentityHeightTable,
MakeContext(),
new Dictionary<uint, SurfaceInfo>());
// Independent geometry oracle: derive each polygon plane from the
// actual emitted positions/indices, accumulate it at the shared
// position, and normalize only after every incident polygon is seen.
var incidentNormalSums = new Dictionary<Vector3, Vector3>();
for (int i = 0; i < mesh.Indices.Length; i += 3)
{
Vector3 p0 = mesh.Vertices[mesh.Indices[i]].Position;
Vector3 p1 = mesh.Vertices[mesh.Indices[i + 1]].Position;
Vector3 p2 = mesh.Vertices[mesh.Indices[i + 2]].Position;
Vector3 planeNormal = Vector3.Normalize(Vector3.Cross(p1 - p0, p2 - p0));
AddNormal(incidentNormalSums, p0, planeNormal);
AddNormal(incidentNormalSums, p1, planeNormal);
AddNormal(incidentNormalSums, p2, planeNormal);
}
foreach (TerrainVertex vertex in mesh.Vertices)
{
Vector3 expected = Vector3.Normalize(incidentNormalSums[vertex.Position]);
AssertVectorNear(expected, vertex.Normal, 1e-6f);
Assert.InRange(vertex.Normal.Length(), 1f - 1e-6f, 1f + 1e-6f);
}
bool differsFromCentralDifferences = false;
for (int x = 0; x < LandblockMesh.HeightmapSide; x++)
{
for (int y = 0; y < LandblockMesh.HeightmapSide; y++)
{
int xL = Math.Max(x - 1, 0);
int xR = Math.Min(x + 1, LandblockMesh.HeightmapSide - 1);
int yD = Math.Max(y - 1, 0);
int yU = Math.Min(y + 1, LandblockMesh.HeightmapSide - 1);
float dx = (HeightAt(block, xR, y) - HeightAt(block, xL, y)) /
((xR - xL) * LandblockMesh.CellSize);
float dy = (HeightAt(block, x, yU) - HeightAt(block, x, yD)) /
((yU - yD) * LandblockMesh.CellSize);
Vector3 oldApproximation = Vector3.Normalize(new Vector3(-dx, -dy, 1f));
Vector3 position = new(
x * LandblockMesh.CellSize,
y * LandblockMesh.CellSize,
HeightAt(block, x, y));
Vector3 actual = mesh.Vertices.First(vertex => vertex.Position == position).Normal;
differsFromCentralDifferences |= Vector3.Distance(oldApproximation, actual) > 1e-4f;
}
}
Assert.True(
differsFromCentralDifferences,
"Synthetic terrain failed to distinguish retail incident-face averaging from central differences.");
}
[Theory]
[InlineData(0u, 0u)]
[InlineData(0xA9u, 0xB4u)]
public void Build_RetailNormalChange_PreservesExactSplitAwarePositionsAndIndices(
uint landblockX,
uint landblockY)
{
var block = BuildFlatLandBlock();
for (int x = 0; x < LandblockMesh.HeightmapSide; x++)
for (int y = 0; y < LandblockMesh.HeightmapSide; y++)
block.Height[x * LandblockMesh.HeightmapSide + y] =
(byte)((x * 17 + y * 29 + x * y * 3) % 80);
var mesh = LandblockMesh.Build(
block,
landblockX,
landblockY,
IdentityHeightTable,
MakeContext(),
new Dictionary<uint, SurfaceInfo>());
Assert.Equal(
Enumerable.Range(0, LandblockMesh.VerticesPerLandblock).Select(i => (uint)i),
mesh.Indices);
int vertexIndex = 0;
for (int cy = 0; cy < LandblockMesh.CellsPerSide; cy++)
{
for (int cx = 0; cx < LandblockMesh.CellsPerSide; cx++)
{
Vector3 bl = PositionAt(block, cx, cy);
Vector3 br = PositionAt(block, cx + 1, cy);
Vector3 tr = PositionAt(block, cx + 1, cy + 1);
Vector3 tl = PositionAt(block, cx, cy + 1);
Vector3[] expected = TerrainBlending.CalculateSplitDirection(
landblockX, (uint)cx, landblockY, (uint)cy) == CellSplitDirection.SWtoNE
? [bl, br, tr, bl, tr, tl]
: [bl, br, tl, br, tr, tl];
foreach (Vector3 position in expected)
Assert.Equal(position, mesh.Vertices[vertexIndex++].Position);
}
}
Assert.Equal(LandblockMesh.VerticesPerLandblock, vertexIndex);
}
private static float HeightAt(LandBlock block, int x, int y) =>
IdentityHeightTable[block.Height[x * LandblockMesh.HeightmapSide + y]];
private static Vector3 PositionAt(LandBlock block, int x, int y) => new(
x * LandblockMesh.CellSize,
y * LandblockMesh.CellSize,
HeightAt(block, x, y));
private static void AddNormal(
IDictionary<Vector3, Vector3> sums,
Vector3 position,
Vector3 normal)
{
sums.TryGetValue(position, out Vector3 sum);
sums[position] = sum + normal;
}
private static void AssertVectorNear(Vector3 expected, Vector3 actual, float epsilon)
{
Assert.InRange(actual.X, expected.X - epsilon, expected.X + epsilon);
Assert.InRange(actual.Y, expected.Y - epsilon, expected.Y + epsilon);
Assert.InRange(actual.Z, expected.Z - epsilon, expected.Z + epsilon);
}
}