feat(render): implement Campaign AR and terrain fidelity
This commit is contained in:
parent
99cf26e00c
commit
7a5f96ede5
368 changed files with 50611 additions and 950 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue