fix(scenery): #49 9×9 loop, per-spawn building check, triangle slope
Three fixes to match retail CLandBlock::get_land_scenes (0x00530460): 1. Loop bound: iterate 9×9 vertices (side_vertex_count=9), not 8×8 cells. Edge vertices (x=8 or y=8) produce valid spawns when the per-object displacement shifts the position back into [0, 192). Confirmed by named retail decomp do-while condition, WorldBuilder vertLength=9, ACViewer Terrain.Count=81, AC2D wTopo[9][9]. 2. Building suppression: check at the DISPLACED position's cell (CSortCell::has_building per spawn), not at the loop vertex index. Matches WorldBuilder buildingsGrid[gx2, gy2] pattern. 3. Slope filter: replace finite-difference gradient approximation with triangle-aware normal sampling via new static method TerrainSurface.SampleNormalZFromHeightmap. Picks the correct triangle via IsSplitSWtoNE, matching retail find_terrain_poly → polygon->plane.N.z and WorldBuilder's GetNormal(). Tests: 5 new tests for SampleNormalZFromHeightmap (flat=1.0, sloped<1, cross-validates with SampleSurface instance method) and DisplaceObject edge-vertex validity. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
17b4ffde12
commit
833d167ebc
4 changed files with 203 additions and 49 deletions
|
|
@ -198,6 +198,73 @@ public sealed class TerrainSurface
|
|||
return InterpolateZInTriangle(hBL, hBR, hTR, hTL, tx, ty, splitSWtoNE);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sample the terrain triangle's surface-normal Z component at (localX, localY)
|
||||
/// from a raw heightmap. Returns the upward component of the unit normal for
|
||||
/// the specific triangle the point lies in — flat ground returns 1.0, steeper
|
||||
/// slopes return smaller values. Used by <see cref="SceneryGenerator"/> for
|
||||
/// the retail slope filter (<c>CLandCell::find_terrain_poly → polygon.plane.N.z</c>).
|
||||
/// </summary>
|
||||
public static float SampleNormalZFromHeightmap(
|
||||
byte[] heights, float[] heightTable,
|
||||
uint landblockX, uint landblockY,
|
||||
float localX, float localY)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(heights);
|
||||
ArgumentNullException.ThrowIfNull(heightTable);
|
||||
if (heights.Length < 81)
|
||||
throw new ArgumentException("heights must have 81 entries", nameof(heights));
|
||||
if (heightTable.Length < 256)
|
||||
throw new ArgumentException("heightTable must have 256 entries", nameof(heightTable));
|
||||
|
||||
float fx = Math.Clamp(localX / CellSize, 0f, CellsPerSide - 0.001f);
|
||||
float fy = Math.Clamp(localY / CellSize, 0f, CellsPerSide - 0.001f);
|
||||
int cx = (int)fx;
|
||||
int cy = (int)fy;
|
||||
cx = Math.Clamp(cx, 0, CellsPerSide - 1);
|
||||
cy = Math.Clamp(cy, 0, CellsPerSide - 1);
|
||||
|
||||
float tx = fx - cx;
|
||||
float ty = fy - cy;
|
||||
|
||||
float hBL = heightTable[heights[cx * HeightmapSide + cy ]];
|
||||
float hBR = heightTable[heights[(cx+1) * HeightmapSide + cy ]];
|
||||
float hTR = heightTable[heights[(cx+1) * HeightmapSide + (cy+1)]];
|
||||
float hTL = heightTable[heights[cx * HeightmapSide + (cy+1)]];
|
||||
|
||||
bool splitSWtoNE = IsSplitSWtoNE(landblockX, (uint)cx, landblockY, (uint)cy);
|
||||
|
||||
float dzdx, dzdy;
|
||||
if (splitSWtoNE)
|
||||
{
|
||||
if (tx > ty)
|
||||
{
|
||||
dzdx = (hBR - hBL) / CellSize;
|
||||
dzdy = (hTR - hBR) / CellSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
dzdx = (hTR - hTL) / CellSize;
|
||||
dzdy = (hTL - hBL) / CellSize;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (tx + ty <= 1f)
|
||||
{
|
||||
dzdx = (hBR - hBL) / CellSize;
|
||||
dzdy = (hTL - hBL) / CellSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
dzdx = (hTR - hTL) / CellSize;
|
||||
dzdy = (hTR - hBR) / CellSize;
|
||||
}
|
||||
}
|
||||
|
||||
return 1f / MathF.Sqrt(dzdx * dzdx + dzdy * dzdy + 1f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pick the cell's triangle for the chosen diagonal and barycentric-
|
||||
/// interpolate Z. Single source of truth shared by both
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue