acdream/src/AcDream.Core/Terrain/TerrainVertex.cs
Erik e0dfecdf23 feat(core+app): per-cell terrain texture blending (Phase 3c.4)
The visual-win commit that wires up the Phase 3c.1/.2/.3 building blocks:
Holtburg's terrain now uses AC's real per-cell texture-merge blend
(base + up to 3 terrain overlays + up to 2 road overlays, with alpha
masks from the alpha atlas) instead of the flat per-vertex single-layer
atlas lookup that preceded it.

Geometry rewrite:
  - New TerrainVertex struct (40 bytes): Position(vec3) + Normal(vec3) +
    Data0..3 (4x uint32 packed blend recipe)
  - LandblockMesh.Build is now cell-based: iterates 8x8 cells instead of
    the old 9x9 vertex grid, emits 6 vertices per cell (two triangles),
    384 total vertices per landblock
  - For each cell: extract 4-corner terrain/road values → GetPalCode →
    BuildSurface (cached across landblocks via a shared surfaceCache) →
    FillCellData → split direction from CalculateSplitDirection → emit
    6 vertices in the exact gl_VertexID % 6 order WorldBuilder's vertex
    shader expects
  - Per-vertex normals preserved via Phase 3b central-difference
    precomputation on the 9x9 heightmap, interpolated smoothly across
    the cell (we deliberately didn't adopt WorldBuilder's dFdx/dFdy
    flat-shade approach — Phase 3a/3b user-tuned lighting was worth
    keeping)

Renderer rewrite:
  - TerrainRenderer VAO: vec3 Position, vec3 Normal, 4x uvec4 byte
    attributes for Data0..3. The uvec4-of-bytes read pattern matches
    Landscape.vert so the ported shader math stays byte-for-byte
    identical to WorldBuilder's.
  - Binds both atlases: terrain atlas on unit 0 (uTerrain), alpha atlas
    on unit 1 (uAlpha)

Shader rewrite (ports of WorldBuilder Landscape.vert/.frag, trimmed):
  - terrain.vert: unpacks the 4 data bytes + rotation bits, derives the
    cell corner from gl_VertexID % 6 + splitDir, rotates the cell-local
    UV per overlay's rotation field, and computes world-space normal
    for the fragment shader
  - terrain.frag: maskBlend3 three-layer alpha-weighted composite for
    terrain overlays, inverted-alpha road combine, final composite
    base * (1-ovlA)*(1-rdA) + ovl * ovlA*(1-rdA) + road * rdA. Phase
    3a/3b directional lighting applied on top (SUN_DIR, AMBIENT=0.25,
    DIFFUSE=0.75, in sync with mesh.frag).
  - Editor uniforms (grid, brush, unwalkable slopes) deliberately
    omitted — not applicable to a game client
  - Per-texture tiling factor hardcoded to 1.0 for now (WorldBuilder
    reads it from uTexTiling[36] uploaded from the dats); one tile per
    cell = 8 tiles per landblock-side, slightly coarser than the old
    ~2x-per-cell tiling. Tunable via the TILE constant if needed.

TerrainAtlas grew parallel TCode/RCode lists (CornerAlphaTCodes,
SideAlphaTCodes, RoadAlphaRCodes) so TerrainBlendingContext can be
built without the mesh loader touching the dats directly.

GameWindow builds a TerrainBlendingContext once, shares a Dictionary
<uint, SurfaceInfo> surfaceCache across all 9 landblocks. Output:
"terrain: 137 unique palette codes across 9 landblocks" — avg ~15
unique per landblock, cache reuse healthy.

LandblockMeshTests rewritten for 384-vertex layout. 77/77 tests green.
Visual smoke run launches clean: no shader compile/link errors, no
GL warnings, terrain renders to the screen.

User visual verification is the final acceptance gate for Phase 3c.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 14:02:15 +02:00

28 lines
1.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Numerics;
namespace AcDream.Core.Terrain;
/// <summary>
/// Per-vertex terrain geometry + blend data. Terrain uses a cell-based
/// layout: 64 cells per landblock, 6 vertices per cell (two triangles),
/// 384 vertices per landblock total. All 6 vertices in a cell carry
/// identical <c>Data0..3</c> (the cell's blend recipe from
/// <see cref="TerrainBlending.FillCellData"/>); the vertex shader derives
/// which of the 4 cell corners a given vertex represents from
/// <c>gl_VertexID % 6</c> plus the split direction bit.
///
/// Normal is stored per vertex via Phase 3b's central-difference scheme on
/// the 9×9 heightmap — this lets the fragment shader interpolate a smooth
/// normal across triangles (softer than WorldBuilder's <c>dFdx</c>/<c>dFdy</c>
/// flat-shaded approach). UVs are derived from the corner index in the
/// vertex shader — not stored here.
///
/// Size: 12 (position) + 12 (normal) + 4*4 (Data0..3) = 40 bytes.
/// </summary>
public readonly record struct TerrainVertex(
Vector3 Position,
Vector3 Normal,
uint Data0,
uint Data1,
uint Data2,
uint Data3);