diff --git a/src/data/maps/core/ElevationMapGenerator.ts b/src/data/maps/core/ElevationMapGenerator.ts index 1a28aa17..6b84bea8 100644 --- a/src/data/maps/core/ElevationMapGenerator.ts +++ b/src/data/maps/core/ElevationMapGenerator.ts @@ -204,9 +204,50 @@ function paintRamp( } } +/** Beach zone width around water features */ +const BEACH_WIDTH = 4; + +/** + * Paint a beach zone around a circular water area + * Creates a LOW elevation ring that provides walkable access to water + */ +function paintBeachCircle( + grid: GenerationGrid, + cx: number, + cy: number, + waterRadius: number +): void { + const innerR2 = waterRadius * waterRadius; + const outerRadius = waterRadius + BEACH_WIDTH; + const outerR2 = outerRadius * outerRadius; + + for (let y = Math.floor(cy - outerRadius); y <= Math.ceil(cy + outerRadius); y++) { + for (let x = Math.floor(cx - outerRadius); x <= Math.ceil(cx + outerRadius); x++) { + if (y >= 0 && y < grid.height && x >= 0 && x < grid.width) { + const dx = x - cx; + const dy = y - cy; + const distSq = dx * dx + dy * dy; + + // Only paint in the beach ring (outside water, inside outer radius) + if (distSq > innerR2 && distSq <= outerR2) { + // Don't overwrite water, ramps, or existing low ground + if (!grid.ramps[y][x] && + grid.features[y][x] !== 'water_deep' && + grid.features[y][x] !== 'water_shallow' && + grid.elevation[y][x] > ELEVATION.LOW) { + grid.elevation[y][x] = ELEVATION.LOW; + grid.features[y][x] = 'none'; // Clear any features + } + } + } + } + } +} + /** * Paint a feature (water, forest, etc.) on a circular area * Water features set elevation to ELEVATION.WATER - water surface is always level + * Shallow water also creates a beach zone around it for walkable access */ function paintFeatureCircle( grid: GenerationGrid, @@ -218,6 +259,12 @@ function paintFeatureCircle( const r2 = radius * radius; // Water surface is always at the same level (deep vs shallow is depth BELOW surface) const isWater = feature === 'water_deep' || feature === 'water_shallow'; + const isShallowWater = feature === 'water_shallow'; + + // Paint beach zone around shallow water first (so water overwrites it in the center) + if (isShallowWater) { + paintBeachCircle(grid, cx, cy, radius); + } for (let y = Math.floor(cy - radius); y <= Math.ceil(cy + radius); y++) { for (let x = Math.floor(cx - radius); x <= Math.ceil(cx + radius); x++) { @@ -239,9 +286,47 @@ function paintFeatureCircle( } } +/** + * Paint a beach zone around a rectangular water area + * Creates a LOW elevation border that provides walkable access to water + */ +function paintBeachRect( + grid: GenerationGrid, + x: number, + y: number, + width: number, + height: number +): void { + // Beach extends BEACH_WIDTH cells outside the water rect + const beachX = x - BEACH_WIDTH; + const beachY = y - BEACH_WIDTH; + const beachWidth = width + BEACH_WIDTH * 2; + const beachHeight = height + BEACH_WIDTH * 2; + + for (let py = Math.floor(beachY); py < Math.ceil(beachY + beachHeight); py++) { + for (let px = Math.floor(beachX); px < Math.ceil(beachX + beachWidth); px++) { + if (py >= 0 && py < grid.height && px >= 0 && px < grid.width) { + // Only paint cells outside the water rect + const inWaterRect = px >= x && px < x + width && py >= y && py < y + height; + if (!inWaterRect) { + // Don't overwrite water, ramps, or existing low ground + if (!grid.ramps[py][px] && + grid.features[py][px] !== 'water_deep' && + grid.features[py][px] !== 'water_shallow' && + grid.elevation[py][px] > ELEVATION.LOW) { + grid.elevation[py][px] = ELEVATION.LOW; + grid.features[py][px] = 'none'; // Clear any features + } + } + } + } + } +} + /** * Paint a feature on a rectangular area * Water features set elevation to ELEVATION.WATER - water surface is always level + * Shallow water also creates a beach zone around it for walkable access */ function paintFeatureRect( grid: GenerationGrid, @@ -253,6 +338,12 @@ function paintFeatureRect( ): void { // Water surface is always at the same level (deep vs shallow is depth BELOW surface) const isWater = feature === 'water_deep' || feature === 'water_shallow'; + const isShallowWater = feature === 'water_shallow'; + + // Paint beach zone around shallow water first (so water overwrites it in the center) + if (isShallowWater) { + paintBeachRect(grid, x, y, width, height); + } for (let py = Math.floor(y); py < Math.ceil(y + height); py++) { for (let px = Math.floor(x); px < Math.ceil(x + width); px++) { diff --git a/src/rendering/EnvironmentManager.ts b/src/rendering/EnvironmentManager.ts index 92506f4f..6b3a8c95 100644 --- a/src/rendering/EnvironmentManager.ts +++ b/src/rendering/EnvironmentManager.ts @@ -351,9 +351,10 @@ export class EnvironmentManager { } // Determine water colors based on biome + // Shallow color is darker and closer to deep to create more subtle transitions const isLava = this.biome.name === 'Volcanic'; - const shallowColor = isLava ? new THREE.Color(0x802000) : new THREE.Color(0x40a0c0); - const deepColor = isLava ? new THREE.Color(0x400800) : new THREE.Color(0x004488); + const shallowColor = isLava ? new THREE.Color(0x802000) : new THREE.Color(0x1a6080); + const deepColor = isLava ? new THREE.Color(0x400800) : new THREE.Color(0x0a3050); // Calculate sun direction from directional light this._tempSunDirection.copy(this.directionalLight.position).normalize(); diff --git a/src/rendering/tsl/WaterMaterial.ts b/src/rendering/tsl/WaterMaterial.ts index 6138a5c7..7f96cdb4 100644 --- a/src/rendering/tsl/WaterMaterial.ts +++ b/src/rendering/tsl/WaterMaterial.ts @@ -196,13 +196,26 @@ export class TSLWaterMaterial { let depthFactor: ShaderNodeObject; if (hasWaterDataAttr) { const waterData = attribute('aWaterData', 'vec3'); - depthFactor = waterData.y; // isDeep component + depthFactor = waterData.y; // isDeep component (0 or 1) } else { depthFactor = float(0.5); // Fallback to mid-blend } - // Base color blend between shallow and deep - const baseColor = mix(this.uShallowColor, this.uDeepColor, depthFactor); + // Add noise-based edge softening to break up the harsh shallow/deep boundary + // This creates a more natural, organic transition between water depths + const noiseScale = 0.15; // Controls the scale of the noise pattern + const noisePhase1 = worldPos.x.mul(noiseScale).add(worldPos.z.mul(noiseScale * 1.3)); + const noisePhase2 = worldPos.x.mul(noiseScale * 0.7).sub(worldPos.z.mul(noiseScale * 0.9)); + const edgeNoise = sin(noisePhase1.mul(8.0).add(scaledTime.mul(0.1))) + .mul(cos(noisePhase2.mul(6.0).sub(scaledTime.mul(0.08)))) + .mul(0.3); // Noise amplitude + + // Blend the depth factor with noise to soften edges + // Clamp to ensure we stay in 0-1 range + const softDepthFactor = clamp(depthFactor.add(edgeNoise), 0.0, 1.0); + + // Base color blend between shallow and deep with softened transition + const baseColor = mix(this.uShallowColor, this.uDeepColor, softDepthFactor); // Add subtle wave-based color variation (purely aesthetic, not lighting) const wavePhase = worldPos.x.mul(0.1).add(worldPos.z.mul(0.15)).add(scaledTime.mul(0.3));