The blueprint has carried water since task 03 -- WBID per-pixel body id, WBTB body table, WSRF per-pixel surface level. Nothing ever drew it. Now the runtime does. No new water data: the blueprint is the authority on where water is and at what level, and this only reads it. WHY WATER IS ITS OWN MESH, not a block in the terrain field. The terrain is a Marching-Cubes iso-surface over ChunkData.Densities. Writing WATER into that field would not lay a sheet on top of the seabed -- it would move the iso-surface, fusing the sea into the terrain as if it were solid ground. A second surface is the only way water can sit at ITS level independent of the ground under it. So ChunkRenderer builds a water MeshInstance3D as a child of the chunk's terrain mesh. TRANSPARENCY IS SHIPPED, NOT DEFERRED (Step 2.4's cheap branch). Because water is a distinct MeshInstance3D it carries its own StandardMaterial3D, so alpha is one flag and Godot sorts transparent surfaces after opaque ones by itself. Zero mesher changes. Alpha runs 0.62 shallow -> 0.97 deep, so the shelved coast stays readable and open ocean closes up. WHICH DATA DROVE IT: WSRF for the level (per-pixel, quantised to 1/32768 raw ~ 7.7 mm, far under the 1 m voxel, and no table lookup), WBID for presence (it is the water stage's own classification output -- the same set the biome grid's Ocean/Lake pixels form by construction), WBTB as the fallback when a column is flagged wet but carries the WSRF no-water sentinel. A body the table does not know leaves the column DRY rather than guessing a level. Details worth keeping: - Water Y uses Constants.HEIGHT_SCALE, the SAME mapping as the terrain surface, named so the sheet and the seabed cannot drift apart if the vertical band is ever retuned. - Depth for shading comes from BLUEPRINT heights, not rendered geometry: the terrain render clamps its floor at Y=2, so every deep-ocean column would otherwise read as one flat ~36 m and the gradient would die exactly where the ocean gets interesting. - A cell is drawn if ANY corner is wet, flat at the highest wet level. Drawing onto a partly-dry cell is deliberate -- it carries the sheet under the shoreline where the opaque terrain hides it. Only-fully-wet cells retreat the waterline a metre and leave a dry gap around every coast and lake. - Non-metallic, roughness 0.35: the scene environment is minimal, and a metallic surface reads near-black when there is nothing to reflect. BlockRegistry gains WATER (id 11). Wire-safe: block IDs are never serialized -- the blueprint's section table stores heights, biome ordinals and water data, never block IDs, and chunks are not persisted yet. MEASURED on seed 1825907253: 241,415 water columns across 454 of 4096 chunks; surface Y 37.6..37.6 m (flat, = 0.15 x 251 -- the flat sea model, rendered); depth to 32.3 m, matching an independent read of the blueprint exactly. Both an ocean and a lake fall in the default view. NO REGRESSION to the 2D pipeline, verified section by section: a regeneration of the working seed is byte-identical to task 12's run in TCRV, TDTL, HGTS, BIOM, WBID, WBTB, WSRF, TOWN and all four road sections; only PRMS differs, and only in its write timestamp. All four snapshot PNGs md5-identical. No storms, no waves, no animation, no flow -- water at rest. Those are C2+. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
162 lines
6.2 KiB
C#
162 lines
6.2 KiB
C#
using Godot;
|
|
using IslaApocalypse.Core;
|
|
|
|
namespace IslaApocalypse.Client
|
|
{
|
|
// Notice this inherits from MeshInstance3D, which is Godot's visual 3D node!
|
|
public partial class ChunkRenderer : MeshInstance3D
|
|
{
|
|
public void RenderChunk(ChunkData data)
|
|
{
|
|
// 1. Pass the BlockIDs array to the generator, plus the per-column
|
|
// surface/biome/road data it needs to fade colours smoothly.
|
|
Mesh = MarchingCubes.GenerateMesh(
|
|
data.Densities, data.BlockIDs,
|
|
data.SurfaceHeights, data.ColumnBiomes, data.ColumnRoadMaterial,
|
|
Constants.ISO_LEVEL, Constants.VOXEL_SCALE);
|
|
|
|
|
|
StandardMaterial3D mat = new StandardMaterial3D();
|
|
|
|
// 2. THE FIX: Tell the material to use the colors we paint on the vertices!
|
|
mat.VertexColorUseAsAlbedo = true;
|
|
mat.Roughness = 0.8f;
|
|
mat.CullMode = BaseMaterial3D.CullModeEnum.Disabled;
|
|
|
|
MaterialOverride = mat;
|
|
|
|
Position = new Vector3(
|
|
data.ChunkPosition.X * Constants.CHUNK_SIZE_X * Constants.VOXEL_SCALE,
|
|
0,
|
|
data.ChunkPosition.Y * Constants.CHUNK_SIZE_Z * Constants.VOXEL_SCALE
|
|
);
|
|
|
|
// 3. Water at rest (task 13) — its own mesh, its own material.
|
|
BuildWaterSurface(data);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The water sheet, as a SEPARATE MeshInstance3D child.
|
|
///
|
|
/// Why separate rather than a block in the terrain field: the terrain is a
|
|
/// Marching-Cubes iso-surface over ChunkData.Densities. Writing water into
|
|
/// that field would not lay a sheet on top of the seabed — it would move the
|
|
/// iso-surface, fusing the water into the terrain as if the sea were solid
|
|
/// ground. A second surface is the only way to have water sit AT its own
|
|
/// level, independent of the ground beneath it.
|
|
///
|
|
/// It also makes translucency nearly free, which is why this task ships it
|
|
/// rather than deferring: a distinct MeshInstance3D carries its own
|
|
/// StandardMaterial3D, so alpha is one flag and Godot sorts transparent
|
|
/// surfaces after opaque ones on its own. No mesher change at all.
|
|
///
|
|
/// Water at rest is FLAT — one level per cell, taken from the blueprint's own
|
|
/// body levels. No waves, no displacement, no animation (those are C2+).
|
|
/// </summary>
|
|
private void BuildWaterSurface(ChunkData data)
|
|
{
|
|
if (!data.HasAnyWater) return; // most inland chunks: nothing to build
|
|
|
|
var st = new SurfaceTool();
|
|
st.Begin(Mesh.PrimitiveType.Triangles);
|
|
|
|
int cellsX = Constants.CHUNK_SIZE_X;
|
|
int cellsZ = Constants.CHUNK_SIZE_Z;
|
|
bool any = false;
|
|
|
|
for (int x = 0; x < cellsX; x++)
|
|
{
|
|
for (int z = 0; z < cellsZ; z++)
|
|
{
|
|
// A cell is drawn if ANY of its four corners is wet, and it is drawn
|
|
// FLAT at the deepest-standing level among those wet corners.
|
|
//
|
|
// Drawing on a partly-dry cell is deliberate: it carries the sheet
|
|
// right up under the shoreline, where the opaque terrain in front of
|
|
// it hides the submerged part. The alternative — only fully-wet
|
|
// cells — retreats the waterline a metre from shore and leaves a
|
|
// visible dry gap all the way around every coast and lake.
|
|
float level = Constants.NO_WATER;
|
|
float d00 = data.WaterSurfaceY[x, z];
|
|
float d10 = data.WaterSurfaceY[x + 1, z];
|
|
float d01 = data.WaterSurfaceY[x, z + 1];
|
|
float d11 = data.WaterSurfaceY[x + 1, z + 1];
|
|
if (d00 > level) level = d00;
|
|
if (d10 > level) level = d10;
|
|
if (d01 > level) level = d01;
|
|
if (d11 > level) level = d11;
|
|
if (level <= Constants.NO_WATER) continue;
|
|
|
|
// Depth for colour: the deepest wet corner of the cell, so a shelving
|
|
// coast reads as a gradient rather than a staircase of flat tiles.
|
|
float depth = Mathf.Max(
|
|
Mathf.Max(data.WaterDepthM[x, z], data.WaterDepthM[x + 1, z]),
|
|
Mathf.Max(data.WaterDepthM[x, z + 1], data.WaterDepthM[x + 1, z + 1]));
|
|
|
|
Color c = DepthColor(depth);
|
|
float s = Constants.VOXEL_SCALE;
|
|
|
|
var p00 = new Vector3(x * s, level, z * s);
|
|
var p10 = new Vector3((x + 1) * s, level, z * s);
|
|
var p01 = new Vector3(x * s, level, (z + 1) * s);
|
|
var p11 = new Vector3((x + 1) * s, level, (z + 1) * s);
|
|
|
|
// Two triangles, wound so the surface faces up.
|
|
AddVert(st, c, p00); AddVert(st, c, p01); AddVert(st, c, p11);
|
|
AddVert(st, c, p00); AddVert(st, c, p11); AddVert(st, c, p10);
|
|
any = true;
|
|
}
|
|
}
|
|
|
|
if (!any) return;
|
|
|
|
var water = new MeshInstance3D();
|
|
water.Mesh = st.Commit();
|
|
|
|
var mat = new StandardMaterial3D();
|
|
mat.VertexColorUseAsAlbedo = true;
|
|
// Vertex alpha is only honoured when the material is actually transparent.
|
|
mat.Transparency = BaseMaterial3D.TransparencyEnum.Alpha;
|
|
// Lighting note: the scene's environment is minimal (one default
|
|
// DirectionalLight3D, a blue ambient), so a metallic surface reads almost
|
|
// black — metal shows its surroundings, and there is little to show. Water
|
|
// is therefore non-metallic with moderate roughness: albedo stays visible
|
|
// under weak ambient, and it still catches a highlight when a light does
|
|
// hit it. Left deliberately in the same lighting world as the terrain so
|
|
// the two improve together when the scene gets proper lighting.
|
|
mat.Roughness = 0.35f;
|
|
mat.Metallic = 0.0f;
|
|
// Both faces: the camera starts above, but a player will swim under it.
|
|
mat.CullMode = BaseMaterial3D.CullModeEnum.Disabled;
|
|
water.MaterialOverride = mat;
|
|
|
|
// Child of this chunk renderer, so it inherits the chunk's world offset
|
|
// and dies with the chunk.
|
|
AddChild(water);
|
|
water.Position = Vector3.Zero;
|
|
water.Name = "Water";
|
|
}
|
|
|
|
private static void AddVert(SurfaceTool st, Color c, Vector3 p)
|
|
{
|
|
// SetColor/SetNormal must be set immediately before AddVertex — same rule
|
|
// the terrain mesher follows.
|
|
st.SetColor(c);
|
|
st.SetNormal(Vector3.Up);
|
|
st.AddVertex(p);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shallow teal to deep navy, with alpha closing up as it deepens, so the
|
|
/// shelved coast stays readable and open ocean does not.
|
|
/// </summary>
|
|
private static Color DepthColor(float depthMeters)
|
|
{
|
|
float t = Mathf.Clamp(depthMeters / Constants.WATER_DEEP_METERS, 0f, 1f);
|
|
t = t * t * (3f - 2f * t); // smoothstep: hold the shallows, then fall away
|
|
Color c = Constants.WATER_SHALLOW.Lerp(Constants.WATER_DEEP, t);
|
|
c.A = Mathf.Lerp(Constants.WATER_ALPHA_SHALLOW, Constants.WATER_ALPHA_DEEP, t);
|
|
return c;
|
|
}
|
|
}
|
|
}
|