diff --git a/src/engine/ChunkManager.ts b/src/engine/ChunkManager.ts index a72034a..935373e 100644 --- a/src/engine/ChunkManager.ts +++ b/src/engine/ChunkManager.ts @@ -17,7 +17,7 @@ export interface ChunkGPUData { const RESOLUTION = 129 export const CHUNK_SIZE = 512 export const HEIGHT_SCALE = 64 -const UNIFORM_BUFFER_SIZE = 128 // 64 (viewProj mat4) + 16 (worldOffset) + 16 (cameraPos) + 16 (fogParams) + 16 padding +const UNIFORM_BUFFER_SIZE = 144 // 64 (viewProj mat4) + 16 (worldOffset) + 16 (cameraPos) + 16 (fogParams) + 16 padding + 16 (biomeData) export default class ChunkManager { private device: GPUDevice @@ -100,6 +100,9 @@ export default class ChunkManager { const worldOffset = new Float32Array([cx * CHUNK_SIZE, 0, cz * CHUNK_SIZE, 0]) this.device.queue.writeBuffer(uniformBuffer, 64, worldOffset) + const biomeData = new Float32Array([biomeId ?? 0, 0, 0, 0]) + this.device.queue.writeBuffer(uniformBuffer, 128, biomeData) + const bindGroup = this.device.createBindGroup({ layout: this.bindGroupLayout, entries: [{ binding: 0, resource: { buffer: uniformBuffer } }], diff --git a/src/shaders/terrain.frag.wgsl b/src/shaders/terrain.frag.wgsl index 450cb78..390b88c 100644 --- a/src/shaders/terrain.frag.wgsl +++ b/src/shaders/terrain.frag.wgsl @@ -3,27 +3,44 @@ struct Uniforms { worldOffset: vec4, cameraPos: vec4, fogParams: vec4, + _pad: vec4, + biomeData: vec4, } @group(0) @binding(0) var uniforms: Uniforms; -@group(1) @binding(0) var terrainSampler: sampler; -@group(1) @binding(1) var grassTex: texture_2d; -@group(1) @binding(2) var rockTex: texture_2d; - struct FragInput { @location(0) normal: vec3, @location(1) uv: vec2, @location(2) worldPos: vec3, } +fn getBiomeColor(id: f32) -> vec3f { + let biome = i32(round(id)); + if (biome == 1) { return vec3f(0.86, 0.76, 0.42); } // Desert + if (biome == 2) { return vec3f(0.55, 0.50, 0.45); } // Mountains + if (biome == 3) { return vec3f(0.42, 0.60, 0.24); } // Valley + if (biome == 4) { return vec3f(0.22, 0.28, 0.14); } // Swamp + if (biome == 5) { return vec3f(0.20, 0.40, 0.12); } // Forest + return vec3f(0.34, 0.52, 0.18); // Grassland (default) +} + +fn getHeightBlendedColor(biomeId: f32, worldY: f32) -> vec3f { + let baseColor = getBiomeColor(biomeId); + let rockColor = vec3f(0.52, 0.48, 0.44); + let snowColor = vec3f(0.92, 0.93, 0.95); + + // Blend toward rock at mid-heights (>= 60 world units) + let rockBlend = clamp((worldY - 60.0) / 120.0, 0.0, 1.0); + let midColor = mix(baseColor, rockColor, rockBlend); + + // Blend toward snow at very high elevations (>= 300 world units) + let snowBlend = clamp((worldY - 300.0) / 150.0, 0.0, 1.0); + return mix(midColor, snowColor, snowBlend); +} + @fragment fn fs_main(f: FragInput) -> @location(0) vec4 { - let grassColor = textureSample(grassTex, terrainSampler, f.uv * 8.0).rgb; - let rockColor = textureSample(rockTex, terrainSampler, f.uv * 6.0).rgb; - - let slope = clamp(f.normal.y, 0.0, 1.0); - let blend = smoothstep(0.55, 0.80, slope); - let albedo = mix(rockColor, grassColor, blend); + let albedo = getHeightBlendedColor(uniforms.biomeData.x, f.worldPos.y); let lightDir = normalize(vec3(0.5, 1.2, 0.4)); let diffuse = max(dot(normalize(f.normal), lightDir), 0.0); diff --git a/src/shaders/terrain.vert.wgsl b/src/shaders/terrain.vert.wgsl index e2c07d9..a0a2d66 100644 --- a/src/shaders/terrain.vert.wgsl +++ b/src/shaders/terrain.vert.wgsl @@ -3,6 +3,8 @@ struct Uniforms { worldOffset: vec4, cameraPos: vec4, fogParams: vec4, + _pad: vec4, + biomeData: vec4, } @group(0) @binding(0) var uniforms: Uniforms; diff --git a/wasm/biome/biome_test.go b/wasm/biome/biome_test.go index ebf413b..0a7fac6 100644 --- a/wasm/biome/biome_test.go +++ b/wasm/biome/biome_test.go @@ -215,6 +215,30 @@ t.Errorf("vertical spike at (%d,%d): diff=%.4f > %.4f", row, col, diff, maxAllow } } +func TestBiomeNoiseProvidesVariety(t *testing.T) { + seed := 42 + biomesSeen := make(map[biome.BiomeType]bool) + var minTemp, maxTemp float64 = 1.0, 0.0 + for x := -10000.0; x <= 10000; x += 500 { + for z := -10000.0; z <= 10000; z += 500 { + temp, _ := biome.GetBiomeParams(x, z, seed) + if temp < minTemp { + minTemp = temp + } + if temp > maxTemp { + maxTemp = temp + } + biomesSeen[biome.GetBiomeAt(x, z, seed)] = true + } + } + if maxTemp-minTemp < 0.4 { + t.Errorf("temperature range too narrow: [%.3f, %.3f] - biome noise is not varying enough", minTemp, maxTemp) + } + if len(biomesSeen) < 3 { + t.Errorf("only %d biome types seen in 20k×20k area - expected at least 3", len(biomesSeen)) + } +} + func TestGaussianBiomeWeights_SumToOne(t *testing.T) { // Blending weights must sum to 1.0 at every climate point. testPoints := [][2]float64{ diff --git a/wasm/biome/registry.go b/wasm/biome/registry.go index 230580e..333327f 100644 --- a/wasm/biome/registry.go +++ b/wasm/biome/registry.go @@ -30,7 +30,7 @@ var DefaultBiomes = map[BiomeType]BiomeDefinition{ Lacunarity: 2.2, Persistence: 0.60, Amplitude: 1.0, - HeightMultiplier: 3.0, + HeightMultiplier: 10.0, }, Valley: { Name: "Valley", diff --git a/wasm/biome/selector.go b/wasm/biome/selector.go index df7fa57..d8276d2 100644 --- a/wasm/biome/selector.go +++ b/wasm/biome/selector.go @@ -23,17 +23,18 @@ func GetBiomeAt(worldX, worldZ float64, seed int) BiomeType { // values are used for smooth biome blending. func GetBiomeParams(worldX, worldZ float64, seed int) (temperature, humidity float64) { // Domain warping: shift sample coordinates to create non-linear biome boundaries. - wx := noise.FBm(worldX*warpNoiseScale, worldZ*warpNoiseScale, seed+1001, 3, warpNoiseScale, 2.0, 0.5) - wz := noise.FBm((worldX+1000)*warpNoiseScale, (worldZ+1000)*warpNoiseScale, seed+1001, 3, warpNoiseScale, 2.0, 0.5) + // Pass raw world coords; FBm handles frequency scaling internally. + wx := noise.FBm(worldX, worldZ, seed+1001, 3, warpNoiseScale, 2.0, 0.5) + wz := noise.FBm(worldX+500000, worldZ+500000, seed+1001, 3, warpNoiseScale, 2.0, 0.5) sx := worldX + wx*warpStrength sz := worldZ + wz*warpStrength // Temperature noise — seed offset to differ from terrain and humidity noise. - tempRaw := noise.FBm(sx*biomeNoiseScale, sz*biomeNoiseScale, seed+1000, 3, biomeNoiseScale, 2.0, 0.5) + tempRaw := noise.FBm(sx, sz, seed+1000, 3, biomeNoiseScale, 2.0, 0.5) temperature = (tempRaw + 1.0) * 0.5 - // Humidity noise — positionally offset to de-correlate from temperature. - humidRaw := noise.FBm((sx+5000)*biomeNoiseScale, (sz+5000)*biomeNoiseScale, seed+2000, 3, biomeNoiseScale, 2.0, 0.5) + // Humidity noise — large offset to de-correlate from temperature. + humidRaw := noise.FBm(sx+500000, sz+500000, seed+2000, 3, biomeNoiseScale, 2.0, 0.5) humidity = (humidRaw + 1.0) * 0.5 return }