feat(core): Phase B.3 — TerrainSurface (outdoor heightmap Z + cell ID)
Extracts the bilinear heightmap interpolation from GameWindow's inlined SampleTerrainZ into a reusable class. Also adds outdoor cell ID computation (8×8 grid of 24-unit cells, 0x0001..0x0040). First component of the physics collision engine. 6 new tests, all green. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
520589911b
commit
19aa8ce5d0
2 changed files with 175 additions and 0 deletions
79
src/AcDream.Core/Physics/TerrainSurface.cs
Normal file
79
src/AcDream.Core/Physics/TerrainSurface.cs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
using System;
|
||||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Outdoor terrain height resolver for a single landblock. Performs
|
||||
/// bilinear interpolation of the 9×9 heightmap grid to produce the
|
||||
/// ground Z at any (localX, localY) within the 192×192 landblock
|
||||
/// footprint. Also computes the outdoor cell ID for AC's position
|
||||
/// encoding.
|
||||
///
|
||||
/// <para>
|
||||
/// Algorithm ported from GameWindow.SampleTerrainZ (which was inlined
|
||||
/// and not reusable). The heightmap is indexed x-major:
|
||||
/// <c>heights[x * 9 + y]</c>; each byte is a lookup into
|
||||
/// <paramref name="heightTable"/> (256-entry float array from
|
||||
/// Region.LandDefs.LandHeightTable).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class TerrainSurface
|
||||
{
|
||||
private const int HeightmapSide = 9;
|
||||
private const float CellSize = 24f;
|
||||
private const int CellsPerSide = 8; // 192 / 24
|
||||
|
||||
private readonly byte[] _heights;
|
||||
private readonly float[] _heightTable;
|
||||
|
||||
public TerrainSurface(byte[] heights, float[] heightTable)
|
||||
{
|
||||
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));
|
||||
|
||||
_heights = heights;
|
||||
_heightTable = heightTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bilinear-interpolated terrain Z at (localX, localY) in
|
||||
/// landblock-local coordinates (0..192 range).
|
||||
/// </summary>
|
||||
public float SampleZ(float localX, float localY)
|
||||
{
|
||||
float fx = Math.Clamp(localX / CellSize, 0f, HeightmapSide - 1f);
|
||||
float fy = Math.Clamp(localY / CellSize, 0f, HeightmapSide - 1f);
|
||||
|
||||
int x0 = Math.Min((int)fx, HeightmapSide - 2);
|
||||
int y0 = Math.Min((int)fy, HeightmapSide - 2);
|
||||
int x1 = x0 + 1;
|
||||
int y1 = y0 + 1;
|
||||
float tx = fx - x0;
|
||||
float ty = fy - y0;
|
||||
|
||||
float h00 = _heightTable[_heights[x0 * HeightmapSide + y0]];
|
||||
float h10 = _heightTable[_heights[x1 * HeightmapSide + y0]];
|
||||
float h01 = _heightTable[_heights[x0 * HeightmapSide + y1]];
|
||||
float h11 = _heightTable[_heights[x1 * HeightmapSide + y1]];
|
||||
|
||||
float hx0 = h00 * (1 - tx) + h10 * tx;
|
||||
float hx1 = h01 * (1 - tx) + h11 * tx;
|
||||
return hx0 * (1 - ty) + hx1 * ty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute the outdoor cell ID for the given landblock-local position.
|
||||
/// Outdoor cells are an 8×8 grid of 24×24-unit cells numbered
|
||||
/// 0x0001..0x0040. Cell (0,0) at position (0,0) is 0x0001.
|
||||
/// </summary>
|
||||
public uint ComputeOutdoorCellId(float localX, float localY)
|
||||
{
|
||||
int cx = Math.Clamp((int)(localX / CellSize), 0, CellsPerSide - 1);
|
||||
int cy = Math.Clamp((int)(localY / CellSize), 0, CellsPerSide - 1);
|
||||
return (uint)(1 + cx * CellsPerSide + cy);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue