Skip to content

Commit 64d5590

Browse files
committed
feat: enhance terrain rendering with biome map integration and color definitions
1 parent 3911e51 commit 64d5590

File tree

6 files changed

+173
-18
lines changed

6 files changed

+173
-18
lines changed

games/tribe2/src/components/game-world-controller.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export const GameWorldController: React.FC<GameWorldControllerProps> = ({ mode,
9696
await renderer.initTerrain(
9797
webgpuCanvas,
9898
gameStateRef.current.heightMap,
99+
gameStateRef.current.biomeMap,
99100
gameStateRef.current.mapDimensions,
100101
HEIGHT_MAP_RESOLUTION,
101102
);

games/tribe2/src/context/webgpu-renderer-context.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { createContext, useContext, ReactNode, useCallback, useRef } from 'react';
22
import { Vector2D } from '../game/types/math-types';
3+
import { BiomeType } from '../game/types/world-types';
34
import { Vector3D, WebGPUTerrainState } from '../game/types/rendering-types';
45
import { initWebGPUTerrain, renderWebGPUTerrain } from '../game/renderer/webgpu-renderer';
56
import { HEIGHT_SCALE, TERRAIN_DISPLACEMENT_FACTOR } from '../game/constants/rendering-constants';
@@ -8,6 +9,7 @@ interface WebGpuRendererContextType {
89
initTerrain: (
910
canvas: HTMLCanvasElement,
1011
heightMap: number[][],
12+
biomeMap: BiomeType[][],
1113
mapDimensions: { width: number; height: number },
1214
cellSize: number,
1315
lighting?: { lightDir?: Vector3D; heightScale?: number; ambient?: number; displacementFactor?: number },
@@ -25,11 +27,12 @@ export const WebGpuRendererProvider: React.FC<{ children: ReactNode }> = ({ chil
2527
async (
2628
canvas: HTMLCanvasElement,
2729
heightMap: number[][],
30+
biomeMap: BiomeType[][],
2831
mapDimensions: { width: number; height: number },
2932
cellSize: number,
3033
lighting?: { lightDir?: Vector3D; heightScale?: number; ambient?: number; displacementFactor?: number },
3134
) => {
32-
const gpuState = await initWebGPUTerrain(canvas, heightMap, mapDimensions, cellSize, lighting);
35+
const gpuState = await initWebGPUTerrain(canvas, heightMap, biomeMap, mapDimensions, cellSize, lighting);
3336
webGpuStateRef.current = gpuState;
3437
},
3538
[],

games/tribe2/src/game/constants/rendering-constants.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,21 @@ export const WATER_FOAM_COLOR = { r: 0.9, g: 0.95, b: 1.0 };
2424
/** The width of the foam at the shoreline, as a normalized value (0-1). */
2525
export const WATER_FOAM_WIDTH = 0.03;
2626

27+
/** The color of ground terrain. */
28+
export const GROUND_COLOR = { r: 0.4, g: 0.3, b: 0.2 };
29+
30+
/** The color of sand terrain. */
31+
export const SAND_COLOR = { r: 0.8, g: 0.7, b: 0.5 };
32+
33+
/** The color of grass terrain. */
34+
export const GRASS_COLOR = { r: 0.25, g: 0.6, b: 0.3 };
35+
36+
/** The color of rock terrain. */
37+
export const ROCK_COLOR = { r: 0.5, g: 0.5, b: 0.5 };
38+
39+
/** The color of snow terrain. */
40+
export const SNOW_COLOR = { r: 0.95, g: 0.98, b: 1.0 };
41+
2742
// Viewport and Camera Constants
2843
/** The amount the zoom level changes per mouse scroll event. */
2944
export const ZOOM_SPEED = 0.1;

games/tribe2/src/game/renderer/shaders/terrain.wgsl

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,20 @@ struct Uniforms {
99
c2: vec4f,
1010
// c3: ambient, waterLevel, time, displacementFactor
1111
c3: vec4f,
12+
// c4: GROUND.rgb, SAND.r
13+
c4: vec4f,
14+
// c5: SAND.gb, GRASS.rg
15+
c5: vec4f,
16+
// c6: GRASS.b, ROCK.rgb
17+
c6: vec4f,
18+
// c7: SNOW.rgb, padding
19+
c7: vec4f,
1220
};
1321

1422
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
1523
@group(0) @binding(1) var heightTex: texture_2d<f32>;
1624
@group(0) @binding(2) var heightSampler: sampler;
25+
@group(0) @binding(3) var biomeTex: texture_2d<f32>;
1726

1827
struct VSOut {
1928
@builtin(position) position: vec4f,
@@ -31,11 +40,47 @@ fn vs_main(@builtin(vertex_index) vid: u32) -> VSOut {
3140
return out;
3241
}
3342

34-
fn base_color(h: f32) -> vec3f {
35-
// Simple green gradient by height
36-
let low = vec3f(0.05, 0.2, 0.08);
37-
let high = vec3f(0.25, 0.6, 0.3);
38-
return mix(low, high, clamp(h, 0.0, 1.0));
43+
// Decodes biome colors from the uniform buffer with interpolation
44+
// biomeValue is a normalized float [0, 1] from the biome texture
45+
fn getBiomeColor(biomeValue: f32) -> vec3f {
46+
// Scale back to biome ID range [0, 4]
47+
let scaledValue = biomeValue * 4.0;
48+
49+
// Get integer biome IDs for interpolation
50+
let biomeId0 = u32(floor(scaledValue));
51+
let biomeId1 = u32(ceil(scaledValue));
52+
let t = fract(scaledValue); // Interpolation factor
53+
54+
// Get colors for both biomes
55+
var color0: vec3f;
56+
var color1: vec3f;
57+
58+
if (biomeId0 == 0u) {
59+
color0 = uniforms.c4.xyz; // Ground
60+
} else if (biomeId0 == 1u) {
61+
color0 = vec3f(uniforms.c4.w, uniforms.c5.x, uniforms.c5.y); // Sand
62+
} else if (biomeId0 == 2u) {
63+
color0 = vec3f(uniforms.c5.z, uniforms.c5.w, uniforms.c6.x); // Grass
64+
} else if (biomeId0 == 3u) {
65+
color0 = uniforms.c6.yzw; // Rock
66+
} else {
67+
color0 = uniforms.c7.xyz; // Snow
68+
}
69+
70+
if (biomeId1 == 0u) {
71+
color1 = uniforms.c4.xyz; // Ground
72+
} else if (biomeId1 == 1u) {
73+
color1 = vec3f(uniforms.c4.w, uniforms.c5.x, uniforms.c5.y); // Sand
74+
} else if (biomeId1 == 2u) {
75+
color1 = vec3f(uniforms.c5.z, uniforms.c5.w, uniforms.c6.x); // Grass
76+
} else if (biomeId1 == 3u) {
77+
color1 = uniforms.c6.yzw; // Rock
78+
} else {
79+
color1 = uniforms.c7.xyz; // Snow
80+
}
81+
82+
// Interpolate between the two biome colors
83+
return mix(color0, color1, t);
3984
}
4085

4186
// Cartoon water effect - slow, peaceful, zoom-dependent
@@ -54,8 +99,6 @@ fn cartoon_water(worldPos: vec2f, depth: f32, time: f32, zoom: f32) -> vec4f {
5499
var waterColor = mix(shallowColor, deepColor, depthFactor);
55100

56101
// Zoom-dependent detail level
57-
// At low zoom (zoomed out), water is flat and simple
58-
// At high zoom (zoomed in), water shows wave patterns
59102
let detailLevel = clamp((zoom - 0.5) / 2.5, 0.0, 1.0); // 0 at zoom=0.5, 1 at zoom=3.0
60103

61104
if (detailLevel > 0.01) {
@@ -198,7 +241,10 @@ fn fs_main(@builtin(position) fragCoord: vec4f) -> @location(0) vec4f {
198241
let n_r = normalize(vec3f(-dzdx_r, -dzdy_r, 1.0));
199242
let ndl_r = max(dot(n_r, lightDir), 0.0);
200243
let lighting_r = ambient + (1.0 - ambient) * ndl_r;
201-
let terrainColor = base_color(h_refracted) * lighting_r;
244+
245+
// Get biome color for refracted terrain with interpolation
246+
let biomeValue_r = textureSampleLevel(biomeTex, heightSampler, refracted_uv, 0.0).x;
247+
let terrainColor = getBiomeColor(biomeValue_r) * lighting_r;
202248

203249
// Get cartoon water color
204250
let waterCol = cartoon_water(wrapped, waterDepth, time, zoom);
@@ -215,7 +261,8 @@ fn fs_main(@builtin(position) fragCoord: vec4f) -> @location(0) vec4f {
215261
return vec4f(finalColor, 1.0);
216262
} else if (waterDepth > -shorelineWidth) {
217263
// This fragment is just above water (wet shore)
218-
let terrainColor = base_color(h) * lighting;
264+
let biomeValue = textureSampleLevel(biomeTex, heightSampler, uv, 0.0).x;
265+
let terrainColor = getBiomeColor(biomeValue) * lighting;
219266

220267
// Calculate how close we are to water level
221268
let wetFactor = clamp(1.0 + (waterDepth / shorelineWidth), 0.0, 1.0);
@@ -227,7 +274,8 @@ fn fs_main(@builtin(position) fragCoord: vec4f) -> @location(0) vec4f {
227274
return vec4f(darkenedColor, 1.0);
228275
} else {
229276
// Above water - render terrain normally
230-
let color = base_color(h) * lighting;
277+
let biomeValue = textureSampleLevel(biomeTex, heightSampler, uv, 0.0).x;
278+
let color = getBiomeColor(biomeValue) * lighting;
231279
return vec4f(color, 1.0);
232280
}
233281
}

games/tribe2/src/game/renderer/webgpu-renderer.ts

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@ import terrainShaderWGSL from './shaders/terrain.wgsl?raw';
22
import { Vector2D } from '../../game/types/math-types';
33
import { WebGPUTerrainState, Vector3D } from '../types/rendering-types';
44
import { WATER_LEVEL } from '../constants/world-constants';
5-
import { TERRAIN_DISPLACEMENT_FACTOR } from '../constants/rendering-constants';
5+
import {
6+
TERRAIN_DISPLACEMENT_FACTOR,
7+
GROUND_COLOR,
8+
SAND_COLOR,
9+
GRASS_COLOR,
10+
ROCK_COLOR,
11+
SNOW_COLOR,
12+
} from '../constants/rendering-constants';
13+
import { BiomeType } from '../types/world-types';
614

715
function isWebGPUSupported() {
816
return typeof navigator !== 'undefined' && 'gpu' in navigator;
@@ -23,9 +31,46 @@ function flattenHeightMapToR8(heightMap: number[][]): { data: Uint8Array; width:
2331
return { data, width: w, height: h };
2432
}
2533

34+
// Flattens the biome map to a Uint8Array where each biome ID [0-4] is
35+
// normalized to the [0-255] range for use in an r8unorm texture.
36+
function flattenBiomeMapToR8Unorm(biomeMap: BiomeType[][]): { data: Uint8Array; width: number; height: number } {
37+
const h = biomeMap.length;
38+
const w = biomeMap[0]?.length ?? 0;
39+
const data = new Uint8Array(w * h);
40+
let i = 0;
41+
for (let y = 0; y < h; y++) {
42+
const row = biomeMap[y];
43+
for (let x = 0; x < w; x++) {
44+
let biomeValue = 0; // Default to GROUND
45+
switch (row[x]) {
46+
case BiomeType.GROUND:
47+
biomeValue = 0;
48+
break;
49+
case BiomeType.SAND:
50+
biomeValue = 1;
51+
break;
52+
case BiomeType.GRASS:
53+
biomeValue = 2;
54+
break;
55+
case BiomeType.ROCK:
56+
biomeValue = 3;
57+
break;
58+
case BiomeType.SNOW:
59+
biomeValue = 4;
60+
break;
61+
}
62+
// Normalize the value from [0, 4] to [0, 255] for r8unorm texture.
63+
// The shader will receive this as a float in [0.0, 1.0].
64+
data[i++] = Math.round((biomeValue / 4.0) * 255);
65+
}
66+
}
67+
return { data, width: w, height: h };
68+
}
69+
2670
export async function initWebGPUTerrain(
2771
canvas: HTMLCanvasElement,
2872
heightMap: number[][],
73+
biomeMap: BiomeType[][],
2974
mapDimensions: { width: number; height: number },
3075
cellSize: number,
3176
lighting?: { lightDir?: Vector3D; heightScale?: number; ambient?: number; displacementFactor?: number },
@@ -52,6 +97,7 @@ export async function initWebGPUTerrain(
5297
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
5398
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
5499
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
100+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
55101
],
56102
});
57103

@@ -64,28 +110,43 @@ export async function initWebGPUTerrain(
64110
primitive: { topology: 'triangle-list' },
65111
});
66112

67-
// Uniform buffer (4 vec4 = 64 bytes)
68-
const uniformBufferSize = 4 * 4 * 4;
113+
// Uniform buffer (8 vec4 = 128 bytes)
114+
const uniformBufferSize = 8 * 4 * 4;
69115
const uniformBuffer = device.createBuffer({
70116
size: uniformBufferSize,
71117
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
72118
});
73119

74120
// Height map texture
75-
const { data, width: gridW, height: gridH } = flattenHeightMapToR8(heightMap);
121+
const { data: heightData, width: gridW, height: gridH } = flattenHeightMapToR8(heightMap);
76122
const heightTexture = device.createTexture({
77123
size: { width: gridW, height: gridH },
78124
format: 'r8unorm',
79125
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
80126
});
81127
device.queue.writeTexture(
82128
{ texture: heightTexture },
83-
data.buffer,
84-
{ offset: data.byteOffset, bytesPerRow: gridW, rowsPerImage: gridH },
129+
heightData.buffer,
130+
{ offset: heightData.byteOffset, bytesPerRow: gridW, rowsPerImage: gridH },
85131
{ width: gridW, height: gridH },
86132
);
87133
const heightTextureView = heightTexture.createView();
88134

135+
// Biome map texture
136+
const { data: biomeData } = flattenBiomeMapToR8Unorm(biomeMap);
137+
const biomeTexture = device.createTexture({
138+
size: { width: gridW, height: gridH },
139+
format: 'r8unorm',
140+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
141+
});
142+
device.queue.writeTexture(
143+
{ texture: biomeTexture },
144+
biomeData.buffer,
145+
{ offset: biomeData.byteOffset, bytesPerRow: gridW, rowsPerImage: gridH },
146+
{ width: gridW, height: gridH },
147+
);
148+
const biomeTextureView = biomeTexture.createView();
149+
89150
const sampler = device.createSampler({
90151
addressModeU: 'repeat',
91152
addressModeV: 'repeat',
@@ -99,6 +160,7 @@ export async function initWebGPUTerrain(
99160
{ binding: 0, resource: { buffer: uniformBuffer } },
100161
{ binding: 1, resource: heightTextureView },
101162
{ binding: 2, resource: sampler },
163+
{ binding: 3, resource: biomeTextureView },
102164
],
103165
});
104166

@@ -113,6 +175,8 @@ export async function initWebGPUTerrain(
113175
sampler,
114176
heightTexture,
115177
heightTextureView,
178+
biomeTexture,
179+
biomeTextureView,
116180
gridSize: { width: gridW, height: gridH },
117181
mapDimensions,
118182
cellSize,
@@ -158,7 +222,7 @@ export function renderWebGPUTerrain(
158222
const canvasWidth = canvas.width;
159223
const canvasHeight = canvas.height;
160224

161-
const u = new Float32Array(16);
225+
const u = new Float32Array(32);
162226
// c0: center.x, center.y, zoom, cellSize
163227
u[0] = center.x;
164228
u[1] = center.y;
@@ -181,6 +245,28 @@ export function renderWebGPUTerrain(
181245
u[14] = time;
182246
u[15] = displacementFactor;
183247

248+
// c4-c7: Biome colors
249+
// c4: GROUND.rgb, SAND.r
250+
u[16] = GROUND_COLOR.r;
251+
u[17] = GROUND_COLOR.g;
252+
u[18] = GROUND_COLOR.b;
253+
u[19] = SAND_COLOR.r;
254+
// c5: SAND.gb, GRASS.rg
255+
u[20] = SAND_COLOR.g;
256+
u[21] = SAND_COLOR.b;
257+
u[22] = GRASS_COLOR.r;
258+
u[23] = GRASS_COLOR.g;
259+
// c6: GRASS.b, ROCK.rgb
260+
u[24] = GRASS_COLOR.b;
261+
u[25] = ROCK_COLOR.r;
262+
u[26] = ROCK_COLOR.g;
263+
u[27] = ROCK_COLOR.b;
264+
// c7: SNOW.rgb, padding
265+
u[28] = SNOW_COLOR.r;
266+
u[29] = SNOW_COLOR.g;
267+
u[30] = SNOW_COLOR.b;
268+
u[31] = 0.0;
269+
184270
device.queue.writeBuffer(uniformBuffer, 0, u.buffer);
185271

186272
const pass = encoder.beginRenderPass({

games/tribe2/src/game/types/rendering-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export interface WebGPUTerrainState {
2121
sampler: GPUSampler;
2222
heightTexture: GPUTexture;
2323
heightTextureView: GPUTextureView;
24+
biomeTexture: GPUTexture;
25+
biomeTextureView: GPUTextureView;
2426
gridSize: { width: number; height: number };
2527
mapDimensions: { width: number; height: number };
2628
cellSize: number; // world units per height texel (HEIGHT_MAP_RESOLUTION)

0 commit comments

Comments
 (0)