Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/engine/ChunkManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 } }],
Expand Down
37 changes: 27 additions & 10 deletions src/shaders/terrain.frag.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,44 @@ struct Uniforms {
worldOffset: vec4<f32>,
cameraPos: vec4<f32>,
fogParams: vec4<f32>,
_pad: vec4<f32>,
biomeData: vec4<f32>,
}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;

@group(1) @binding(0) var terrainSampler: sampler;
@group(1) @binding(1) var grassTex: texture_2d<f32>;
@group(1) @binding(2) var rockTex: texture_2d<f32>;

struct FragInput {
@location(0) normal: vec3<f32>,
@location(1) uv: vec2<f32>,
@location(2) worldPos: vec3<f32>,
}

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<f32> {
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<f32>(0.5, 1.2, 0.4));
let diffuse = max(dot(normalize(f.normal), lightDir), 0.0);
Expand Down
2 changes: 2 additions & 0 deletions src/shaders/terrain.vert.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ struct Uniforms {
worldOffset: vec4<f32>,
cameraPos: vec4<f32>,
fogParams: vec4<f32>,
_pad: vec4<f32>,
biomeData: vec4<f32>,
}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;

Expand Down
24 changes: 24 additions & 0 deletions wasm/biome/biome_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
2 changes: 1 addition & 1 deletion wasm/biome/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 6 additions & 5 deletions wasm/biome/selector.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down