Concept · Rendering
Shaders & WGSL
Every pixel FloraForge puts on screen was computed by a shader — a small program that runs not on your CPU, but on the graphics card, thousands of copies at a time. This page explains what shaders are, how the GPU pipeline invokes them, and what their language looks like, using real code from the engine's sky renderer.
What a shader actually is
A shader is a function with a very unusual contract. You don't call it once — you hand it to the GPU, and the GPU calls it for you, in parallel, once for every item in some enormous set: every vertex of a mesh, every pixel of the screen, every cell of a grid. The shader never sees its neighbours and cannot share results with them; it gets one input, computes one output, and that isolation is exactly what lets the hardware run tens of thousands of copies simultaneously.
The name is historical — the first shaders computed shading, the colour of lit surfaces — but the idea outgrew the name decades ago. Modern shaders generate geometry, simulate water, animate clouds, and in FloraForge's case even build the terrain itself. What unites them is the execution model: one small pure function, mapped over a huge dataset, on hardware built for exactly that.
The pipeline: from triangles to pixels
Drawing-style shaders come in pairs, plugged into a fixed conveyor belt called the render pipeline. The engine submits raw vertices; the pipeline pushes them through both shader stages and the rasterizer between them:
The vertex shader answers one question: where does this point land on screen? It takes a vertex's 3D world position and multiplies it by the camera's view-projection matrix. The fragment shader answers the other: what colour is this pixel? For every pixel the rasterizer found inside a triangle, it combines lighting, fog, material colour and anything else the artist — or in FloraForge's case, the procedural rules — can express in code.
The language: WGSL
Shaders are written in special-purpose languages, and FloraForge uses
WGSL — the WebGPU Shading Language, the shader language
standardised alongside WebGPU.
It looks like a cousin of Rust: fn, let,
struct, type annotations after a colon. Three things stand out
against a general-purpose language:
- Vector math is built in.
vec3<f32>,mat4x4<f32>, dot products, swizzles likep.xyz— the things graphics code does constantly are part of the language, not a library. - Attributes wire it to the pipeline.
@vertexand@fragmentmark entry points;@builtin(position)and@location(n)declare exactly which value flows where between stages. - No recursion, no pointers into the heap, no surprises. The GPU must know every program's resource needs up front, so the language is deliberately closed — which is also what makes it safe to ship to a browser.
Here is a complete, real vertex shader — the one that draws FloraForge's sky. It uses a classic trick: instead of submitting a mesh, it generates a single triangle big enough to cover the whole screen, computing the three corners from nothing but the vertex's index:
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) ndc: vec2<f32>,
};
@vertex
fn vs_main(@builtin(vertex_index) id: u32) -> VertexOutput {
// Fullscreen triangle: 3 vertices covering the entire screen
let uv = vec2<f32>(f32((id << 1u) & 2u), f32(id & 2u));
var out: VertexOutput;
out.clip_position = vec4<f32>(uv * 2.0 - 1.0, 1.0, 1.0);
out.ndc = out.clip_position.xy;
return out;
}
The GPU calls vs_main three times with id = 0, 1, 2,
and the bit-twiddling on the index produces three corners that enclose the
screen. No vertex buffer needed at all. The VertexOutput struct is
the hand-off contract: clip_position goes to the rasterizer, and
ndc is interpolated and delivered to the fragment shader at every
pixel.
A fragment shader that paints clouds
The sky's fragment shader then runs once for every pixel on screen. The clouds you see in FloraForge aren't textures — they're computed in this shader from layered noise, fresh every frame. This excerpt is the noise stack it uses, a textbook example of how procedural graphics code reads:
// Cheap hash: turns a 2D coordinate into a pseudo-random 0–1 value.
fn hash2d(p: vec2<f32>) -> f32 {
var p3 = fract(vec3<f32>(p.x, p.y, p.x) * 0.1031);
p3 = p3 + dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
// Smooth noise: blend the hash values of the 4 surrounding grid corners.
fn noise2d(p: vec2<f32>) -> f32 {
let i = floor(p);
let f = fract(p);
let u = f * f * (3.0 - 2.0 * f);
let a = hash2d(i);
let b = hash2d(i + vec2<f32>(1.0, 0.0));
let c = hash2d(i + vec2<f32>(0.0, 1.0));
let d = hash2d(i + vec2<f32>(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
// Fractal Brownian motion: stack 5 octaves of noise, each finer and fainter.
fn fbm(p: vec2<f32>) -> f32 {
var val = 0.0;
var amp = 0.5;
var pos = p;
for (var i = 0; i < 5; i = i + 1) {
val = val + amp * noise2d(pos);
pos = pos * 2.0 + vec2<f32>(1.7, 9.2);
amp = amp * 0.5;
}
return val;
}
Read bottom-up, this is the recipe behind most procedural texture in games: a hash gives raw randomness, noise smooths it into rolling values, and fbm ("fractal Brownian motion") layers several scales of that noise so the result has both broad shapes and fine detail — exactly the structure of a real cloud bank. The same idea, in more dimensions, generates FloraForge's terrain (see Procedural Generation).
How shaders get their data
A shader can't read your program's variables — it lives on another processor with its own memory. Everything it needs must be shipped over explicitly and declared in the shader's interface. In WGSL those declarations name a group and a binding slot:
struct FrameUniform {
view_proj: mat4x4<f32>,
camera_position: vec4<f32>,
time: vec4<f32>,
// …shadow and projection params…
};
@group(0) @binding(0) var<uniform> frame: FrameUniform;
@group(1) @binding(0) var<uniform> material: MaterialUniform;
Group 0 holds what changes every frame (camera, clock); group 1 holds what changes per material (lighting, fog colours). That split is a deliberate performance pattern with its own page: Bind Groups.
The shaders in FloraForge
Every visual system in the engine is its own WGSL file in
src/renderer_wgpu/shaders/:
- terrain.wgsl — draws the ground: biome colours, shadows, fog.
- terrain_gen.wgsl — a compute shader that builds each chunk's mesh from height and moisture data.
- sky.wgsl — the gradient sky, sun, and procedural clouds quoted above.
- water.wgsl — the animated sea and river surface.
- instanced.wgsl — trees, shrubs and houses, drawn via instancing.
- shadows.wgsl — renders the sun's-eye-view depth map for shadowing.
- shrub_billboard.wgsl, hud.wgsl, thumbnail.wgsl, lighting.wgsl — distant-plant billboards, the HUD and minimap, herbarium thumbnails, and shared lighting helpers.
wgpu library translates them to Metal, Vulkan or Direct3D on
native platforms, and passes them straight through in the browser. One shader
codebase, every platform.