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); } /// /// 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+). /// 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); } /// /// Shallow teal to deep navy, with alpha closing up as it deepens, so the /// shelved coast stays readable and open ocean does not. /// 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; } } }