Files
skills/shader-dev/techniques/voxel-rendering.md
shihao 6487becf60 Initial commit: add all skills files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:52:49 +08:00

986 lines
45 KiB
Markdown

## WebGL2 Adaptation Requirements
The code templates in this document use ShaderToy GLSL style. When generating standalone HTML pages, you must adapt for WebGL2:
- Use `canvas.getContext("webgl2")` **(required! WebGL1 does not support in/out keywords)**
- Shader first line: `#version 300 es`, add `precision highp float;` to fragment shader
- **IMPORTANT: #version must be the very first line of the shader! No characters before it (including blank lines/comments/Unicode BOM)**
- Vertex shader: `attribute``in`, `varying``out`
- Fragment shader: `varying``in`, `gl_FragColor` → custom `out vec4 fragColor`, `texture2D()``texture()`
- ShaderToy's `void mainImage(out vec4 fragColor, in vec2 fragCoord)` needs to be adapted to the standard `void main()` entry point
### WebGL2 Full Adaptation Example
```glsl
// === Vertex Shader ===
const vertexShaderSource = `#version 300 es
in vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
}`;
// === Fragment Shader ===
const fragmentShaderSource = `#version 300 es
precision highp float;
uniform float iTime;
uniform vec2 iResolution;
// IMPORTANT: Important: WebGL2 must declare the output variable!
out vec4 fragColor;
// ... other functions ...
void main() {
// IMPORTANT: Use gl_FragCoord.xy instead of fragCoord
vec2 fragCoord = gl_FragCoord.xy;
vec3 col = vec3(0.0);
// ... rendering logic ...
// IMPORTANT: Write to fragColor, not gl_FragColor!
fragColor = vec4(col, 1.0);
}`;
```
**IMPORTANT: Common GLSL compile errors:**
- `in/out storage qualifier supported in GLSL ES 3.00 only` → Check that you are using `getContext("webgl2")` and `#version 300 es`
- `#version directive must occur on the first line` → Check that the shader string starts with #version, with no characters before it
- **IMPORTANT: GLSL reserved words**: `cast`, `class`, `template`, `namespace`, `union`, `enum`, `typedef`, `sizeof`, `input`, `output`, `filter`, `image`, `sampler`, `fixed`, `volatile`, `public`, `static`, `extern`, `external`, `interface`, `long`, `short`, `double`, `half`, `unsigned`, `superp`, `inline`, `noinline`, etc. are all GLSL reserved words and **must never be used as variable or function names**! Common pitfall: naming a function `cast` for ray casting → compile failure. **Use compound names like `castRay`, `castShadow`, `shootRay` instead**.
- **IMPORTANT: GLSL strict typing**: float/int cannot be mixed. `if (x > 0)` for int, `if (y < 0.0)` for float. Comparing ivec3 members to float requires explicit conversion: `float(c.y) < height`. When getVoxel returns int, compare with `> 0` not `> 0.0`. Function parameter types must match exactly.
- **IMPORTANT: Vector dimension mismatch (vec2 vs vec3)**: `p.xz` returns `vec2` and **must never** be added to `vec3` or passed to functions expecting `vec3` parameters (e.g., `fbm(vec3)`, `noise(vec3)`)! Common error: `fbm(p.xz * 0.08 + vec3(...))``vec2 + vec3` compile failure. **Fix**: either use a `vec2` version of noise/fbm, or construct a full vec3: `fbm(vec3(p.xz * 0.08, p.y * 0.05))`. Similarly, `vec2` only has `.x`/`.y`, cannot access `.z`/`.w`.
- **IMPORTANT: length() / floating-point precision**: `length(ivec2)` must first convert to `vec2`: `length(vec2(d))`. Exact floating-point equality comparison almost never works; use range comparison: `floor(p.y) == floor(height)`
# Voxel Rendering Skill
## Use Cases
- Rendering discrete volumetric data on regular 3D grids (Minecraft-style worlds, medical volume data, architectural voxel models)
- Pixel-accurate block/cube scenes
- "Block art", "3D pixel art", "low-poly voxel" visual styles
- Real-time voxel scenes in pure fragment shader environments like ShaderToy
- Advanced lighting effects including shadows, AO, and global illumination
## Core Principles
The core of voxel rendering is the **DDA (Digital Differential Analyzer) ray traversal algorithm**: cast a ray from the camera through each pixel, stepping through the 3D grid cell by cell along the ray direction until hitting an occupied voxel.
For ray `P(t) = rayPos + t * rayDir`, DDA maintains:
- **`mapPos`** = `floor(rayPos)`: current grid coordinate (integer)
- **`deltaDist`** = `abs(1.0 / rayDir)`: t cost to cross one cell
- **`sideDist`** = `(sign(rayDir) * (mapPos - rayPos) + sign(rayDir) * 0.5 + 0.5) * deltaDist`: t distance to the next boundary on each axis
Each step advances along the axis with the smallest `sideDist`, updating `sideDist += deltaDist` and `mapPos += rayStep`.
Normal on hit: `normal = -mask * rayStep`
Face UV is obtained by projecting the hit point onto the two tangent axes of the hit face.
## Implementation Steps
### Step 1: Camera Ray Construction
```glsl
vec2 screenPos = (fragCoord.xy / iResolution.xy) * 2.0 - 1.0;
vec3 cameraDir = vec3(0.0, 0.0, 0.8); // Focal length; larger = narrower FOV
vec3 cameraPlaneU = vec3(1.0, 0.0, 0.0);
vec3 cameraPlaneV = vec3(0.0, 1.0, 0.0) * iResolution.y / iResolution.x;
vec3 rayDir = cameraDir + screenPos.x * cameraPlaneU + screenPos.y * cameraPlaneV;
vec3 rayPos = vec3(0.0, 2.0, -12.0);
```
### Step 2: DDA Initialization
```glsl
ivec3 mapPos = ivec3(floor(rayPos));
vec3 rayStep = sign(rayDir);
vec3 deltaDist = abs(1.0 / rayDir); // When ray is normalized, equivalent to abs(1.0/rd), no length() needed
vec3 sideDist = (sign(rayDir) * (vec3(mapPos) - rayPos) + (sign(rayDir) * 0.5) + 0.5) * deltaDist;
```
### Step 3: DDA Traversal Loop (Branchless Version)
```glsl
#define MAX_RAY_STEPS 64
bvec3 mask;
for (int i = 0; i < MAX_RAY_STEPS; i++) {
if (getVoxel(mapPos)) break;
// Branchless axis selection
mask = lessThanEqual(sideDist.xyz, min(sideDist.yzx, sideDist.zxy));
sideDist += vec3(mask) * deltaDist;
mapPos += ivec3(vec3(mask)) * ivec3(rayStep);
}
```
Alternative form (step version):
```glsl
vec3 mask = step(sideDist.xyz, sideDist.yzx) * step(sideDist.xyz, sideDist.zxy);
sideDist += mask * deltaDist;
mapPos += mask * rayStep;
```
### Step 4: Voxel Occupancy Function
```glsl
// Basic version: solid block (most common; use this when user asks for "voxel cube")
// IMPORTANT: Important: getVoxel receives ivec3, but all internal calculations must use float!
bool getVoxel(ivec3 c) {
vec3 p = vec3(c) + vec3(0.5); // ivec3 → vec3 conversion (required!)
float d = sdBox(p, vec3(6.0)); // Solid 12x12x12 cube
return d < 0.0;
}
// Advanced version: SDF boolean operations (sphere carved from box = only corners remain)
bool getVoxelCarved(ivec3 c) {
vec3 p = vec3(c) + vec3(0.5);
float d = max(-sdSphere(p, 7.5), sdBox(p, vec3(6.0))); // box ∩ ¬sphere
return d < 0.0;
}
// Advanced version: height map terrain with material IDs
// IMPORTANT: Key: all comparisons must use float! c.y is int and must be converted to float for comparison
// IMPORTANT: Important: must use range comparison, not exact equality (floating-point precision issues)
int getVoxelMaterial(ivec3 c) {
vec3 p = vec3(c); // ivec3 → vec3 conversion (required!)
float groundHeight = getTerrainHeight(p.xz); // p.xz is vec2, passes float parameters
if (float(c.y) < groundHeight) return 1; // int → float comparison
if (float(c.y) < groundHeight + 4.0) return 7; // int → float comparison
return 0;
}
// Pure float version (simpler, recommended):
int getVoxelMaterial(vec3 c) {
float groundHeight = getTerrainHeight(c.xz);
// IMPORTANT: Use range comparison, never exact equality!
if (c.y >= groundHeight && c.y < groundHeight + 1.0) return 1; // Grass top layer
if (c.y >= groundHeight - 3.0 && c.y < groundHeight) return 2; // Dirt layer
if (c.y < groundHeight - 3.0) return 3; // Stone layer
return 0;
}
// Advanced version: mountain terrain (height-based coloring: grass green → rock gray → snow white)
// IMPORTANT: Key 1: color thresholds must be based on heightRatio (normalized height 0~1), not absolute height!
// IMPORTANT: Key 2: maxH must match the actual maximum return value of getMountainHeight!
// If getMountainHeight returns at most 15.0, maxH must be 15.0, not arbitrarily 20.0
// IMPORTANT: Key 3: threshold spacing must be large enough (at least 0.2), otherwise color bands are too narrow to see
// IMPORTANT: Key 4: grass area typically covers the largest terrain area (low elevation); set grass threshold high (0.4) to ensure green is clearly visible
float maxH = 15.0; // IMPORTANT: Must equal the actual max value of getMountainHeight!
int getMountainVoxel(vec3 c) {
float height = getMountainHeight(c.xz); // Returns 0 ~ maxH
if (c.y > height) return 0; // Air
float heightRatio = c.y / maxH; // Normalize to 0~1
// IMPORTANT: Thresholds from low to high: grass < 0.4, rock 0.4~0.7, snow > 0.7
if (heightRatio < 0.4) return 1; // Grass (green) — largest area
if (heightRatio < 0.7) return 2; // Rock (gray)
return 3; // Snow cap (white)
}
// IMPORTANT: Corresponding material colors must have sufficient saturation and clear contrast:
// mat==1: vec3(0.25, 0.55, 0.15) Grass green (saturated green, must not be grayish!)
// mat==2: vec3(0.5, 0.45, 0.4) Rock gray-brown
// mat==3: vec3(0.92, 0.93, 0.96) Snow white
// IMPORTANT: Lighting must not be too bright or it washes out colors! Sun intensity ≤ 2.0, sky light ≤ 1.0
// IMPORTANT: Gamma correction pow(col, vec3(0.4545)) brightens dark colors and reduces saturation;
// if colors look grayish-white, make grass green more saturated: vec3(0.2, 0.5, 0.1)
// IMPORTANT: Rotating objects: to rotate a voxel object, apply inverse rotation to the sample point in getVoxel!
// Do not rotate the camera to simulate object rotation (that only changes the viewpoint)
bool getVoxelRotating(ivec3 c) {
vec3 p = vec3(c) + vec3(0.5);
// Rotate around Y axis: apply inverse rotation to sample point
float angle = -iTime; // Negative sign = inverse transform
float s = sin(angle), co = cos(angle);
p.xz = vec2(p.x * co - p.z * s, p.x * s + p.z * co);
float d = sdBox(p, vec3(6.0)); // Rotated solid cube
return d < 0.0;
}
```
### Step 5: Face Shading (Normal + Base Color)
```glsl
vec3 normal = -vec3(mask) * rayStep;
vec3 color;
if (mask.x) color = vec3(0.5); // Side faces darkest
if (mask.y) color = vec3(1.0); // Top face brightest
if (mask.z) color = vec3(0.75); // Front/back faces medium
fragColor = vec4(color, 1.0);
```
### Step 6: Precise Hit Position and Face UV
```glsl
float t = dot(sideDist - deltaDist, vec3(mask));
vec3 hitPos = rayPos + rayDir * t;
vec3 uvw = hitPos - vec3(mapPos);
vec2 uv = vec2(dot(vec3(mask) * uvw.yzx, vec3(1.0)),
dot(vec3(mask) * uvw.zxy, vec3(1.0)));
```
### Step 7: Neighbor Voxel AO
```glsl
float vertexAo(vec2 side, float corner) {
return (side.x + side.y + max(corner, side.x * side.y)) / 3.0;
}
vec4 voxelAo(vec3 pos, vec3 d1, vec3 d2) {
vec4 side = vec4(
getVoxel(pos + d1), getVoxel(pos + d2),
getVoxel(pos - d1), getVoxel(pos - d2));
vec4 corner = vec4(
getVoxel(pos + d1 + d2), getVoxel(pos - d1 + d2),
getVoxel(pos - d1 - d2), getVoxel(pos + d1 - d2));
vec4 ao;
ao.x = vertexAo(side.xy, corner.x);
ao.y = vertexAo(side.yz, corner.y);
ao.z = vertexAo(side.zw, corner.z);
ao.w = vertexAo(side.wx, corner.w);
return 1.0 - ao;
}
// Bilinear interpolation
vec4 ambient = voxelAo(mapPos - rayStep * mask, mask.zxy, mask.yzx);
float ao = mix(mix(ambient.z, ambient.w, uv.x), mix(ambient.y, ambient.x, uv.x), uv.y);
ao = pow(ao, 1.0 / 3.0); // Gamma correction to control AO intensity
```
### Step 8: DDA Shadow Ray
```glsl
// IMPORTANT: Shadow steps must be capped at 16; total main ray + shadow ray steps should not exceed 80
#define MAX_SHADOW_STEPS 16
float castShadow(vec3 ro, vec3 rd) {
vec3 pos = floor(ro);
vec3 ri = 1.0 / rd;
vec3 rs = sign(rd);
vec3 dis = (pos - ro + 0.5 + rs * 0.5) * ri;
for (int i = 0; i < MAX_SHADOW_STEPS; i++) {
if (getVoxel(ivec3(pos))) return 0.0;
vec3 mm = step(dis.xyz, dis.yzx) * step(dis.xyz, dis.zxy);
dis += mm * rs * ri;
pos += mm * rs;
}
return 1.0;
}
vec3 sundir = normalize(vec3(-0.5, 0.6, 0.7));
float shadow = castShadow(hitPos + normal * 0.01, sundir);
float diffuse = max(dot(normal, sundir), 0.0) * shadow;
```
## Complete Code Template
```glsl
// === Voxel Rendering - Complete ShaderToy Template ===
// Includes: DDA traversal, face shading, neighbor AO, hard shadows
// IMPORTANT: Performance critical: SwiftShader software renderer (headless browser evaluation environment) cannot handle too many loop iterations
// Default 64+16=80 steps, suitable for most scenes. Simple scenes (single cube) can increase to 96+24
// Multi-building/character/Minecraft scenes must keep 64+16 or lower!
#define MAX_RAY_STEPS 64
#define MAX_SHADOW_STEPS 16
#define GRID_SIZE 16.0
// ---- Math Utilities ----
float sdSphere(vec3 p, float r) { return length(p) - r; }
float sdBox(vec3 p, vec3 b) {
vec3 d = abs(p) - b;
return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0));
}
float hash31(vec3 n) { return fract(sin(dot(n, vec3(1.0, 113.0, 257.0))) * 43758.5453); }
vec2 rotate2d(vec2 v, float a) {
float s = sin(a), c = cos(a);
return vec2(v.x * c - v.y * s, v.y * c + v.x * s);
}
// ---- Voxel Scene Definition ----
// IMPORTANT: Default solid cube. Use sdBox for "voxel cube"; add SDF boolean ops for carved/sculpted shapes
int getVoxel(vec3 c) {
vec3 p = c + 0.5;
float d = sdBox(p, vec3(6.0)); // Solid 12x12x12 block
if (d < 0.0) {
if (p.y < -3.0) return 2;
return 1;
}
return 0;
}
// ---- Neighbor AO ----
float getOccupancy(vec3 c) { return float(getVoxel(c) > 0); }
float vertexAo(vec2 side, float corner) {
return (side.x + side.y + max(corner, side.x * side.y)) / 3.0;
}
vec4 voxelAo(vec3 pos, vec3 d1, vec3 d2) {
vec4 side = vec4(
getOccupancy(pos + d1), getOccupancy(pos + d2),
getOccupancy(pos - d1), getOccupancy(pos - d2));
vec4 corner = vec4(
getOccupancy(pos + d1 + d2), getOccupancy(pos - d1 + d2),
getOccupancy(pos - d1 - d2), getOccupancy(pos + d1 - d2));
vec4 ao;
ao.x = vertexAo(side.xy, corner.x);
ao.y = vertexAo(side.yz, corner.y);
ao.z = vertexAo(side.zw, corner.z);
ao.w = vertexAo(side.wx, corner.w);
return 1.0 - ao;
}
// ---- DDA Traversal Core ----
struct HitInfo {
bool hit;
float t;
vec3 pos;
vec3 normal;
vec3 mapPos;
vec2 uv;
int mat;
};
HitInfo castRay(vec3 ro, vec3 rd, int maxSteps) {
HitInfo info;
info.hit = false;
info.t = 0.0;
vec3 mapPos = floor(ro);
vec3 rayStep = sign(rd);
vec3 deltaDist = abs(1.0 / rd);
vec3 sideDist = (rayStep * (mapPos - ro) + rayStep * 0.5 + 0.5) * deltaDist;
vec3 mask = vec3(0.0);
for (int i = 0; i < maxSteps; i++) {
int vox = getVoxel(mapPos);
if (vox > 0) {
info.hit = true;
info.mat = vox;
info.normal = -mask * rayStep;
info.mapPos = mapPos;
info.t = dot(sideDist - deltaDist, mask);
info.pos = ro + rd * info.t;
vec3 uvw = info.pos - mapPos;
info.uv = vec2(dot(mask * uvw.yzx, vec3(1.0)),
dot(mask * uvw.zxy, vec3(1.0)));
return info;
}
mask = step(sideDist.xyz, sideDist.yzx) * step(sideDist.xyz, sideDist.zxy);
sideDist += mask * deltaDist;
mapPos += mask * rayStep;
}
return info;
}
// ---- Shadow Ray ----
// IMPORTANT: Shadow steps at 16 (combined with main ray 64 = 80, within SwiftShader safe range)
float castShadow(vec3 ro, vec3 rd) {
vec3 pos = floor(ro);
vec3 ri = 1.0 / rd;
vec3 rs = sign(rd);
vec3 dis = (pos - ro + 0.5 + rs * 0.5) * ri;
for (int i = 0; i < MAX_SHADOW_STEPS; i++) {
// IMPORTANT: getVoxel returns int; comparison must use int constant (0), not float (0.0)
if (getVoxel(pos) > 0) return 0.0;
vec3 mm = step(dis.xyz, dis.yzx) * step(dis.xyz, dis.zxy);
dis += mm * rs * ri;
pos += mm * rs;
}
return 1.0;
}
// ---- Material Colors ----
// IMPORTANT: Texture coloring key: "low saturation" does not mean "near white/gray"!
// Low saturation = colorful but not vivid, must retain clear hue differences (e.g., brick red 0.55,0.35,0.3 not gray-white 0.8,0.8,0.8)
// Brick/stone textures: use UV periodic patterns (mortar lines = dark lines), never use solid colors!
vec3 getMaterialColor(int mat, vec2 uv) {
vec3 col = vec3(0.6);
if (mat == 1) col = vec3(0.7, 0.7, 0.75);
if (mat == 2) col = vec3(0.4, 0.55, 0.3);
float checker = mod(floor(uv.x * 4.0) + floor(uv.y * 4.0), 2.0);
col *= 0.85 + 0.15 * checker;
return col;
}
// ---- Brick/Stone Texture Coloring (use this to replace getMaterialColor when user requests "brick texture") ----
// IMPORTANT: Key: brick texture = UV periodic pattern (staggered rows + mortar dark lines), not solid color!
vec3 getBrickColor(vec2 uv, vec3 baseColor, vec3 mortarColor) {
vec2 brickUV = uv * vec2(4.0, 8.0);
float row = floor(brickUV.y);
brickUV.x += mod(row, 2.0) * 0.5; // Staggered row offset
vec2 f = fract(brickUV);
float mortar = step(f.x, 0.06) + step(f.y, 0.08); // Mortar joints
mortar = clamp(mortar, 0.0, 1.0);
float noise = fract(sin(dot(floor(brickUV), vec2(12.9898, 78.233))) * 43758.5453);
vec3 brickVariation = baseColor * (0.85 + 0.3 * noise); // Slight color variation per brick
return mix(brickVariation, mortarColor, mortar);
}
// Usage example (maze walls):
// if (mat == 1) col = getBrickColor(uv, vec3(0.55, 0.35, 0.3), vec3(0.4, 0.38, 0.35)); // Brick red + mortar
// if (mat == 2) col = getBrickColor(uv, vec3(0.5, 0.48, 0.42), vec3(0.35, 0.33, 0.3)); // Gray stone brick
// ---- Main Function ----
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 screenPos = (fragCoord.xy / iResolution.xy) * 2.0 - 1.0;
screenPos.x *= iResolution.x / iResolution.y;
vec3 ro = vec3(0.0, 2.0 * sin(iTime * 0.5), -12.0);
vec3 forward = vec3(0.0, 0.0, 0.8);
vec3 rd = normalize(forward + vec3(screenPos, 0.0));
ro.xz = rotate2d(ro.xz, iTime * 0.3);
rd.xz = rotate2d(rd.xz, iTime * 0.3);
vec3 sunDir = normalize(vec3(-0.5, 0.6, 0.7));
vec3 skyColor = vec3(0.6, 0.75, 0.9);
HitInfo hit = castRay(ro, rd, MAX_RAY_STEPS);
vec3 col;
if (hit.hit) {
vec3 matCol = getMaterialColor(hit.mat, hit.uv);
vec3 mask = abs(hit.normal);
vec4 ambient = voxelAo(hit.mapPos, mask.zxy, mask.yzx);
float ao = mix(
mix(ambient.z, ambient.w, hit.uv.x),
mix(ambient.y, ambient.x, hit.uv.x),
hit.uv.y);
ao = pow(ao, 0.5);
float shadow = castShadow(hit.pos + hit.normal * 0.01, sunDir);
float diff = max(dot(hit.normal, sunDir), 0.0);
float sky = 0.5 + 0.5 * hit.normal.y;
vec3 lighting = vec3(0.0);
// IMPORTANT: Mountain/terrain scenes: sun light ≤ 2.0, sky light ≤ 1.0; too bright washes out material color differences
lighting += 2.0 * diff * vec3(1.0, 0.95, 0.8) * shadow;
lighting += 1.0 * sky * skyColor;
lighting *= ao;
col = matCol * lighting;
// IMPORTANT: Fog: coefficient should not be too large, otherwise nearby objects get swallowed into pure sky color
// 0.0002 suits GRID_SIZE=16 scenes; use smaller coefficients for larger scenes
float fog = 1.0 - exp(-0.0002 * hit.t * hit.t);
col = mix(col, skyColor, clamp(fog, 0.0, 0.7)); // Clamp prevents objects from disappearing entirely
} else {
col = skyColor - rd.y * 0.2;
}
col = pow(clamp(col, 0.0, 1.0), vec3(0.4545));
fragColor = vec4(col, 1.0);
}
```
## Common Variants
### Variant 1: Glowing Voxels (Glow Accumulation)
Accumulate distance-based glow values during DDA traversal; produces semi-transparent glow even on miss.
```glsl
float glow = 0.0;
for (int i = 0; i < MAX_RAY_STEPS; i++) {
float d = sdSomeShape(vec3(mapPos));
glow += 0.015 / (0.01 + d * d);
if (d < 0.0) break;
// ... normal DDA stepping ...
}
vec3 col = baseColor + glow * vec3(0.4, 0.6, 1.0);
```
### Variant 2: Rounded Voxels (Intra-voxel SDF Refinement)
After DDA hit, perform SDF ray march inside the voxel to render rounded blocks.
```glsl
float id = hash31(mapPos);
float w = 0.05 + 0.35 * id;
float sdRoundedBox(vec3 p, float w) {
return length(max(abs(p) - 0.5 + w, 0.0)) - w;
}
vec3 localP = hitPos - mapPos - 0.5;
for (int j = 0; j < 6; j++) {
float h = sdRoundedBox(localP, w);
if (h < 0.025) break;
localP += rd * max(0.0, h);
}
```
### Variant 3: Hybrid SDF-Voxel Traversal
SDF sphere-tracing with large steps at distance, switching to precise DDA near the surface.
```glsl
#define VOXEL_SIZE 0.0625
#define SWITCH_DIST (VOXEL_SIZE * 1.732)
bool useVoxel = false;
for (int i = 0; i < MAX_STEPS; i++) {
vec3 pos = ro + rd * t;
float d = mapSDF(useVoxel ? voxelCenter : pos);
if (!useVoxel) {
t += d;
if (d < SWITCH_DIST) { useVoxel = true; voxelPos = getVoxelPos(pos); }
} else {
if (d < 0.0) break;
if (d > SWITCH_DIST) { useVoxel = false; t += d; continue; }
vec3 exitT = (voxelPos - ro * ird + ird * VOXEL_SIZE * 0.5);
// ... select minimum axis to advance ...
}
}
```
### Variant 4: Voxel Cone Tracing
Build multi-level mipmaps, cast cone-shaped rays from hit points for global illumination.
```glsl
vec4 traceCone(vec3 origin, vec3 dir, float coneRatio) {
vec4 light = vec4(0.0);
float t = 1.0;
for (int i = 0; i < 58; i++) {
vec3 sp = origin + dir * t;
float diameter = max(1.0, t * coneRatio);
float lod = log2(diameter);
vec4 sample = voxelFetch(sp, lod);
light += sample * (1.0 - light.w);
t += diameter;
}
return light;
}
```
### Variant 5: PBR Lighting + Multi-Bounce Reflection
GGX BRDF replacing Lambert, with metallic/roughness parameters; cast a second DDA ray for reflections.
```glsl
float ggxDiffuse(float NoL, float NoV, float LoH, float roughness) {
float FD90 = 0.5 + 2.0 * roughness * LoH * LoH;
float a = 1.0 + (FD90 - 1.0) * pow(1.0 - NoL, 5.0);
float b = 1.0 + (FD90 - 1.0) * pow(1.0 - NoV, 5.0);
return a * b / 3.14159;
}
vec3 rd2 = reflect(rd, normal);
HitInfo reflHit = castRay(hitPos + normal * 0.001, rd2, 64);
vec3 reflColor = reflHit.hit ? shade(reflHit) : skyColor;
float fresnel = 0.04 + 0.96 * pow(1.0 - max(dot(normal, -rd), 0.0), 5.0);
col += fresnel * reflColor;
```
### Variant 6: Voxel Water Scene (Water + Underwater Voxels)
Water surface ripple reflections, underwater refraction, sand and seaweed for a complete water scene.
```glsl
float waterY = 0.0;
// Underwater voxel scene definition (sand + seaweed)
// IMPORTANT: All coordinate operations must use correct vector dimensions!
// c.xz returns vec2, only has .x/.y components, cannot use .z!
int getVoxel(vec3 c) {
float sandHeight = -3.0 + 0.5 * sin(c.x * 0.3) * cos(c.z * 0.4);
if (c.y < sandHeight) return 1; // Sand interior
if (c.y < sandHeight + 1.0) return 2; // Sand surface
// Seaweed: only grows underwater, above sand
float grassHash = fract(sin(dot(floor(c.xz), vec2(12.9898, 78.233))) * 43758.5453);
// IMPORTANT: floor(c.xz) is vec2; the second argument to dot() must also be vec2
if (grassHash > 0.85 && c.y >= sandHeight + 1.0 && c.y < sandHeight + 1.0 + 3.0 * grassHash) {
return 3; // Seaweed
}
return 0;
}
// Handle water surface in main rendering
float tWater = (waterY - ro.y) / rd.y;
bool hitWater = tWater > 0.0 && (tWater < hit.t || !hit.hit);
if (hitWater) {
vec3 waterPos = ro + rd * tWater;
vec3 waterNormal = vec3(0.0, 1.0, 0.0);
// IMPORTANT: waterPos.xz is vec2; access with .x/.y (not .x/.z)
vec2 waveXZ = waterPos.xz; // vec2: waveXZ.x = worldX, waveXZ.y = worldZ
waterNormal.x += 0.05 * sin(waveXZ.x * 3.0 + iTime);
waterNormal.z += 0.05 * cos(waveXZ.y * 2.0 + iTime * 0.7);
waterNormal = normalize(waterNormal);
float fresnel = 0.04 + 0.96 * pow(1.0 - max(dot(waterNormal, -rd), 0.0), 5.0);
// Reflection
vec3 reflDir = reflect(rd, waterNormal);
HitInfo reflHit = castRay(waterPos + waterNormal * 0.01, reflDir, 64);
vec3 reflCol = reflHit.hit ? getMaterialColor(reflHit.mat, reflHit.uv) : skyColor;
// Refraction (underwater voxels: sand, seaweed)
vec3 refrDir = refract(rd, waterNormal, 1.0 / 1.33);
HitInfo refrHit = castRay(waterPos - waterNormal * 0.01, refrDir, 64);
vec3 refrCol;
if (refrHit.hit) {
vec3 matCol = getMaterialColor(refrHit.mat, refrHit.uv);
float underwaterDist = length(refrHit.pos - waterPos);
refrCol = mix(matCol, vec3(0.0, 0.15, 0.3), 1.0 - exp(-0.1 * underwaterDist));
} else {
refrCol = vec3(0.0, 0.1, 0.3);
}
col = mix(refrCol, reflCol, fresnel);
col = mix(col, vec3(0.0, 0.3, 0.5), 0.2);
}
```
### Variant 7: Rotating Voxel Objects
Rotate voxel objects as a whole. Core: apply inverse rotation to sample points in getVoxel.
```glsl
// IMPORTANT: Correct way to rotate objects: apply inverse rotation to sample coordinates in getVoxel
// Wrong approach: only rotate the camera (that just changes the viewpoint, not the object)
int getVoxel(vec3 c) {
vec3 p = c + 0.5;
// Rotate around Y axis
float angle = -iTime * 0.5;
float s = sin(angle), co = cos(angle);
p.xz = vec2(p.x * co - p.z * s, p.x * s + p.z * co);
// Can also rotate around multiple axes:
// p.yz = vec2(p.y * co2 - p.z * s2, p.y * s2 + p.z * co2); // X axis rotation
float d = sdBox(p, vec3(6.0));
if (d < 0.0) return 1;
return 0;
}
```
### Variant 8: Indoor/Cave/Enclosed Scenes (Point Lights + High Ambient Lighting)
Indoor, cave, underground, sci-fi base, and other enclosed or semi-enclosed scenes require point lights and high ambient lighting.
```glsl
// IMPORTANT: Key points for enclosed/semi-enclosed scenes (caves, interiors, sci-fi bases, mazes, etc.):
// 1. Camera must be placed inside the cavity (a position where getVoxel returns 0)
// 2. Must use point lights, not just directional light (directional light blocked by walls/ceiling = total darkness!)
// 3. Ambient light must be high enough (at least 0.2-0.3) to prevent scene from being too dark to see details
// 4. Can use multiple point lights + emissive voxels to simulate torches/fluorescence/holographic displays
// 5. Sci-fi scene metallic walls need bright enough light sources to show reflections
// 6. Emissive elements (holographic screens, indicator lights, magic circles) use emissive materials: add emissive color directly to lighting
// Cave scene: cavity = area where getVoxel returns 0
// IMPORTANT: Cave/terrain noise functions must respect vector dimensions!
// p.xz is vec2; if noise/fbm function takes vec3, construct a full vec3:
// Correct: fbm(vec3(p.xz, p.y * 0.5)) or use vec2 version of noise
// Wrong: fbm(p.xz + vec3(...)) ← vec2 + vec3 compile failure!
int getVoxel(vec3 c) {
float cave = sdSphere(c + 0.5, 12.0);
// IMPORTANT: For noise-carved detail, use c's components directly (all float)
cave += 2.0 * sin(c.x * 0.3) * sin(c.y * 0.4) * sin(c.z * 0.35);
if (cave > 0.0) return 1; // Rock wall
return 0; // Cavity (camera goes here)
}
// Point light attenuation
vec3 pointLightPos = vec3(0.0, 3.0, 0.0);
vec3 toLight = pointLightPos - hit.pos;
float lightDist = length(toLight);
vec3 lightDir = toLight / lightDist;
float attenuation = 1.0 / (1.0 + 0.1 * lightDist + 0.01 * lightDist * lightDist);
float diff = max(dot(hit.normal, lightDir), 0.0);
float shadow = castShadow(hit.pos + hit.normal * 0.01, lightDir);
vec3 lighting = vec3(0.0);
// IMPORTANT: High ambient light to prevent total darkness (required for enclosed scenes! at least 0.2)
lighting += vec3(0.25, 0.22, 0.2); // Warm ambient light
lighting += 3.0 * diff * attenuation * vec3(1.0, 0.8, 0.5) * shadow; // Point light
// Multiple torches/emissive objects (use sin for flicker animation)
vec3 torch1 = vec3(5.0, 2.0, 3.0);
vec3 torch2 = vec3(-4.0, 1.0, -5.0);
float flicker1 = 0.8 + 0.2 * sin(iTime * 5.0 + 1.0);
float flicker2 = 0.8 + 0.2 * sin(iTime * 4.3 + 2.7);
lighting += calcPointLight(hit.pos, hit.normal, torch1, vec3(1.0, 0.6, 0.2)) * flicker1;
lighting += calcPointLight(hit.pos, hit.normal, torch2, vec3(0.2, 1.0, 0.5)) * flicker2;
// Emissive materials (holographic displays, fluorescent moss, indicator lights, magic circles, etc.)
// IMPORTANT: Emissive colors are added directly to lighting, unaffected by shadows
if (hit.mat == 2) {
lighting += vec3(0.1, 0.4, 0.15); // Fluorescent moss (faint green)
}
if (hit.mat == 3) {
float pulse = 0.7 + 0.3 * sin(iTime * 2.0);
lighting += vec3(0.2, 0.6, 1.0) * pulse; // Blue pulse light
}
col = matCol * lighting;
```
### Variant 9: Voxel Character Animation
Simple voxel character animation using time-driven offsets and rotations.
```glsl
// IMPORTANT: Voxel character animation core approach:
// 1. Split the character into multiple body parts (head, torso, left arm, right arm, left leg, right leg)
// 2. Each part is an sdBox with independent offset/rotation parameters
// 3. iTime drives limb swinging (sin/cos periodic motion)
// 4. Combine all parts using SDF min()
// IMPORTANT: SwiftShader performance critical: character function is called at every DDA step!
// Must add AABB bounding box check in getVoxel: first check if c is near the character,
// skip sdBox calculations for that character if not nearby. Otherwise frame timeout → black screen
// Reduce MAX_RAY_STEPS to 64, MAX_SHADOW_STEPS to 16
int getCharacter(vec3 p, vec3 charPos, float animPhase) {
vec3 lp = p - charPos;
float limbSwing = sin(iTime * 4.0 + animPhase) * 0.5;
// Torso
float body = sdBox(lp - vec3(0, 3, 0), vec3(1.5, 2.0, 1.0));
// Head
float head = sdBox(lp - vec3(0, 6, 0), vec3(1.2, 1.2, 1.2));
// Arm swing (offset y coordinate around shoulder joint to simulate rotation)
vec3 armOffset = vec3(0, limbSwing * 2.0, limbSwing);
float leftArm = sdBox(lp - vec3(-2.5, 3, 0) - armOffset, vec3(0.5, 2.0, 0.5));
float rightArm = sdBox(lp - vec3(2.5, 3, 0) + armOffset, vec3(0.5, 2.0, 0.5));
// Alternating leg swing
vec3 legOffset = vec3(0, 0, limbSwing * 1.5);
float leftLeg = sdBox(lp - vec3(-0.7, 0, 0) - legOffset, vec3(0.5, 1.5, 0.5));
float rightLeg = sdBox(lp - vec3(0.7, 0, 0) + legOffset, vec3(0.5, 1.5, 0.5));
float d = min(body, min(head, min(leftArm, min(rightArm, min(leftLeg, rightLeg)))));
if (d < 0.0) {
if (head < 0.0) return 10; // Head (skin color)
if (leftArm < 0.0 || rightArm < 0.0) return 11; // Arms
return 12; // Torso/legs
}
return 0;
}
// Combine scene + characters in getVoxel
// IMPORTANT: Must add AABB bounding box early exit! Character sdBox calculations are expensive
int getVoxel(vec3 c) {
// Scene (floor, walls, etc.)
int scene = getSceneVoxel(c);
if (scene > 0) return scene;
// IMPORTANT: AABB check: only call getCharacter near the character
// Character 1: warrior (at position (5,0,0)), bounding box ±5 cells
if (abs(c.x - 5.0) < 5.0 && c.y >= 0.0 && c.y < 10.0 && abs(c.z) < 5.0) {
int char1 = getCharacter(c, vec3(5, 0, 0), 0.0);
if (char1 > 0) return char1;
}
// Character 2: mage (at position (-5,0,3)), bounding box ±5 cells
if (abs(c.x + 5.0) < 5.0 && c.y >= 0.0 && c.y < 10.0 && abs(c.z - 3.0) < 5.0) {
int char2 = getCharacter(c, vec3(-5, 0, 3), 3.14);
if (char2 > 0) return char2;
}
return 0;
}
```
### Variant 10: Waterfall / Flowing Water Particle Effects
Dynamic waterfall, splash particles, water mist effects. Core: time-offset noise simulates water flow, hashed particles simulate splashes, exponential decay simulates mist.
```glsl
// IMPORTANT: Key points for waterfall/flowing water/particle effects:
// 1. Waterfall stream: noise + iTime vertical offset simulates water column flowing down
// 2. Splash particles: hash-distributed voxels at the bottom, positions change with iTime to simulate splashing
// 3. Water mist: semi-transparent accumulation (reduced alpha) or density field at the bottom simulates mist diffusion
// 4. Waterfall must have a clear high point (cliff/rock wall) and low point (pool), drop ≥ 10 cells
// 5. Water stream material uses light blue-white + brightness flicker to simulate flowing water feel
float hash21(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
int getVoxel(vec3 c) {
// Cliff rock walls (both sides + back)
if (c.x < -5.0 || c.x > 5.0) {
if (c.y < 15.0 && c.z > -3.0 && c.z < 3.0) return 1; // Rock
}
if (c.z > 2.0 && c.y < 15.0 && abs(c.x) < 6.0) return 1; // Back wall
// Cliff top platform
if (c.y >= 13.0 && c.y < 15.0 && c.z > -1.0 && c.z < 3.0 && abs(c.x) < 5.0) return 1;
// Bottom pool floor
if (c.y < -2.0 && abs(c.x) < 8.0 && c.z > -6.0 && c.z < 3.0) return 2; // Pool bottom
// IMPORTANT: Waterfall stream: narrow band x ∈ [-2, 2], falling from y=13 to y=0
// Use iTime offset on y-coordinate noise to simulate downward water flow
if (abs(c.x) < 2.0 && c.y >= 0.0 && c.y < 13.0 && c.z > -1.0 && c.z < 1.0) {
float flowNoise = hash21(vec2(floor(c.x), floor(c.y - iTime * 8.0)));
if (flowNoise > 0.25) return 3; // Water (gaps simulate translucent water curtain)
}
// IMPORTANT: Splash particles: bottom y ∈ [-1, 3], x ∈ [-4, 4]
// Use hash + iTime to generate randomly bouncing voxel particles
if (c.y >= -1.0 && c.y < 3.0 && abs(c.x) < 4.0 && c.z > -3.0 && c.z < 2.0) {
float t = iTime * 3.0;
float particleHash = hash21(vec2(floor(c.x * 2.0), floor(c.z * 2.0) + floor(t)));
float yOffset = fract(t + particleHash) * 3.0; // Particle upward trajectory
if (abs(c.y - yOffset) < 0.6 && particleHash > 0.7) return 4; // Splash particle
}
// IMPORTANT: Water mist: bottom y ∈ [-1, 2], wider range than splashes
// Density decreases with height and distance from waterfall center
if (c.y >= -1.0 && c.y < 2.0 && abs(c.x) < 6.0 && c.z > -5.0 && c.z < 3.0) {
float distFromCenter = length(vec2(c.x, c.z));
float mistDensity = exp(-0.15 * distFromCenter) * exp(-0.5 * max(c.y, 0.0));
float mistNoise = hash21(vec2(floor(c.x * 0.5 + iTime * 0.5), floor(c.z * 0.5)));
if (mistNoise < mistDensity * 0.8) return 5; // Water mist
}
return 0;
}
// Material colors
vec3 getMaterialColor(int mat, vec2 uv) {
if (mat == 1) return vec3(0.45, 0.4, 0.35); // Rock
if (mat == 2) return vec3(0.35, 0.3, 0.25); // Pool bottom
if (mat == 3) { // Water stream (shimmering blue-white)
float shimmer = 0.8 + 0.2 * sin(uv.y * 20.0 + iTime * 10.0);
return vec3(0.6, 0.8, 1.0) * shimmer;
}
if (mat == 4) return vec3(0.85, 0.92, 1.0); // Splash (bright white)
if (mat == 5) return vec3(0.7, 0.82, 0.9); // Water mist (pale blue-white)
return vec3(0.5);
}
// IMPORTANT: Water mist material needs special lighting: high emissive + translucent feel
// During shading:
if (hit.mat == 5) {
lighting += vec3(0.4, 0.5, 0.6); // Water mist emissive (unaffected by shadows)
}
// Camera: side angle slightly elevated, showing the full waterfall (top to bottom + bottom splashes and mist)
// ro = vec3(12.0, 10.0, -10.0), lookAt = vec3(0.0, 6.0, 0.0)
```
### Variant 11: Multi-Building / Town / Minecraft-Style Scenes (Multi-Structure Town Composition)
Towns, villages, Minecraft-style worlds, and other scenes requiring multiple discrete structures (houses, trees, lampposts, etc.) placed on the ground.
**IMPORTANT: "Minecraft-like voxel scene" = multi-building scene; must follow the performance constraints of this template!**
```glsl
// IMPORTANT: Key points for multi-building scenes:
// 1. Define the ground first (height map or flat plane), ensure ground getVoxel returns correct material
// 2. Each building uses an independent helper function, receiving local coordinates, returning material ID
// 3. In getVoxel, check each building sequentially (using offset coordinates), return on first hit
// 4. Camera must be outside the scene facing the center, far enough to see the full view
// 5. IMPORTANT: Building coordinate ranges must be within DDA traversal range (MAX_RAY_STEPS * cell ≈ reachable distance)
// 6. IMPORTANT: Scene range should not be too large! Concentrate all buildings within -20~20 range, camera 30-50 cells away
// 7. IMPORTANT: SwiftShader performance critical: getVoxel must have AABB bounding box early exit!
// Above ground (c.y > 0), check AABB range first; return 0 immediately if outside building area
// Otherwise every DDA step checks all buildings → frame timeout → black screen / only sky renders
// 8. IMPORTANT: MAX_RAY_STEPS reduced to 64, MAX_SHADOW_STEPS to 16 (complex getVoxel requires lower step counts)
// Single house: width w, depth d, height h, with triangular roof
int makeHouse(vec3 p, float w, float d, float h, int wallMat, int roofMat) {
// Walls
if (p.x >= 0.0 && p.x < w && p.z >= 0.0 && p.z < d && p.y >= 0.0 && p.y < h) {
return wallMat;
}
// Triangular roof: starts from wall top, x range narrows by 1 per level
float roofY = p.y - h;
float roofInset = roofY; // Inset by 1 cell per level
if (roofY >= 0.0 && roofY < w * 0.5
&& p.x >= roofInset && p.x < w - roofInset
&& p.z >= 0.0 && p.z < d) {
return roofMat;
}
return 0;
}
// Tree: trunk + spherical canopy
int makeTree(vec3 p, float trunkH, float crownR, int trunkMat, int leafMat) {
// Trunk (1x1 column)
if (p.x >= -0.5 && p.x < 0.5 && p.z >= -0.5 && p.z < 0.5
&& p.y >= 0.0 && p.y < trunkH) {
return trunkMat;
}
// Spherical canopy
vec3 crownCenter = vec3(0.0, trunkH + crownR * 0.5, 0.0);
if (length(p - crownCenter) < crownR) {
return leafMat;
}
return 0;
}
// Lamppost: thin pole + glowing top block
int makeLamp(vec3 p, float h, int poleMat, int lightMat) {
if (p.x >= -0.3 && p.x < 0.3 && p.z >= -0.3 && p.z < 0.3
&& p.y >= 0.0 && p.y < h) {
return poleMat; // Pole
}
if (p.x >= -0.5 && p.x < 0.5 && p.z >= -0.5 && p.z < 0.5
&& p.y >= h && p.y < h + 1.0) {
return lightMat; // Lamp head (emissive)
}
return 0;
}
int getVoxel(vec3 c) {
// 1. Ground (y < 0 is underground, y == 0 layer is surface)
if (c.y < -1.0) return 0;
if (c.y < 0.0) return 1; // Ground (dirt/grass)
// 2. Road (along z direction, x range -2~2)
if (c.y < 1.0 && abs(c.x) < 2.0) return 2; // Road surface
// IMPORTANT: AABB bounding box early exit (required for SwiftShader!)
// All buildings are within x:-15~15, y:0~12, z:-5~15
// Return 0 immediately outside this range, avoiding per-building checks
if (c.x < -15.0 || c.x > 15.0 || c.y > 12.0 || c.z < -5.0 || c.z > 15.0) return 0;
// 3. Place buildings (each with offset coordinates)
// IMPORTANT: House width/height must be ≥ 5 cells, otherwise they look like dots from far away! Use bright material colors
int m;
// House A: position (5, 0, 3), width 6, depth 5, height 5
m = makeHouse(c - vec3(5.0, 0.0, 3.0), 6.0, 5.0, 5.0, 3, 4);
if (m > 0) return m;
// House B: position (-10, 0, 2), width 7, depth 5, height 5
m = makeHouse(c - vec3(-10.0, 0.0, 2.0), 7.0, 5.0, 5.0, 5, 4);
if (m > 0) return m;
// Tree: position (0, 0, 8)
m = makeTree(c - vec3(0.0, 0.0, 8.0), 4.0, 2.5, 6, 7);
if (m > 0) return m;
// Lamppost: position (3, 0, 0)
m = makeLamp(c - vec3(3.0, 0.0, 0.0), 5.0, 8, 9);
if (m > 0) return m;
return 0;
}
// IMPORTANT: Camera setup: must be far enough to overlook the entire town
// Recommended: ro = vec3(0, 15, -35), looking at scene center vec3(0, 3, 5)
vec3 ro = vec3(0.0, 15.0, -35.0);
vec3 lookAt = vec3(0.0, 3.0, 5.0);
vec3 forward = normalize(lookAt - ro);
vec3 right = normalize(cross(forward, vec3(0, 1, 0)));
vec3 up = cross(right, forward);
vec3 rd = normalize(forward * 0.8 + right * screenPos.x + up * screenPos.y);
// IMPORTANT: Sunset/side-lit scene key: when light comes from the side or at low angle, building fronts may be completely backlit turning into black silhouettes!
// Must satisfy all: (1) ambient light ≥ 0.3 (prevent backlit faces from going black); (2) house walls use bright materials (e.g., light yellow 0.85,0.75,0.55)
// (3) house dimensions must not be too small (width/height ≥ 5 cells), otherwise they look like dots from far away
vec3 sunDir = normalize(vec3(-0.8, 0.3, 0.5)); // Sunset low angle
vec3 sunColor = vec3(1.0, 0.6, 0.3); // Warm orange
vec3 ambientColor = vec3(0.35, 0.3, 0.4); // IMPORTANT: High ambient light (≥0.3) to prevent silhouettes
// lighting = ambientColor + diff * sunColor * shadow;
```
## Performance & Composition
**Performance Tips:**
- Early exit: break immediately when `mapPos` exceeds scene bounds
- Shadow ray steps of 16-24 are sufficient
- Use SDF sphere-tracing with large steps in open areas, switch to DDA near surfaces
- Material queries, AO, normals, etc. are only computed after hit
- Replace procedural voxel queries with `texelFetch` texture sampling
- Multi-frame accumulation + reprojection for low-noise results
- **IMPORTANT: MAX_RAY_STEPS defaults to 64, MAX_SHADOW_STEPS defaults to 16 (total 80)**. Only simple scenes (single cube/sphere) can increase to 96+24. Multi-building/Minecraft/character scenes with complex getVoxel must keep 64+16 or lower, otherwise SwiftShader frame timeout → only sky background renders
**Composition Tips:**
- **Procedural noise terrain**: use FBM/Perlin noise height maps inside `getVoxel()`
- **SDF procedural modeling**: use SDF boolean operations inside `getVoxel()` to define shapes
- **Texture mapping**: after hit, sample 16x16 pixel textures using face UV * 16
- **Atmospheric scattering / volumetric fog**: accumulate medium density during DDA traversal
- **Water surface rendering**: Fresnel reflection/refraction on a specific Y plane (see Variant 6 above)
- **Global illumination**: cone tracing or Monte Carlo hemisphere sampling
- **Temporal reprojection**: multi-frame accumulation + previous frame reprojection for anti-aliasing and denoising
## Common Errors
1. **GLSL reserved words causing compile failure**: `cast`, `class`, `template`, `namespace`, `input`, `output`, `filter`, `image`, `sampler`, `half`, `fixed`, etc. are GLSL reserved words and **must never be used as variable or function names**. Use compound names: `castRay`, `castShadow`, `shootRay`, `spellEffect` (not `cast`)
2. **Enclosed/semi-enclosed scene total darkness**: caves, interiors, sci-fi bases, mazes, and other enclosed scenes cannot rely solely on directional light (completely blocked by walls/ceiling); must use point lights + high ambient light (≥0.2) + emissive materials (see Variant 8)
3. **Camera inside voxel causing rendering anomalies**: cave/indoor scene camera origin must be inside the cavity (where getVoxel returns 0), otherwise the first DDA step hits immediately = scene invisible
4. **Complex getVoxel causing SwiftShader black screen (most common with Minecraft-style/town/character/multi-building scenes!)**: getVoxel is called once per DDA step; if it contains multiple buildings/characters/terrain+trees without early exit, frame timeout → only sky background renders. **Must do all of**: (1) AABB bounding box early exit (check coordinate range first, return 0 immediately outside building area); (2) MAX_RAY_STEPS ≤ 64, MAX_SHADOW_STEPS ≤ 16; (3) scene range within ±20 cells. **Minecraft-style scene = multi-building scene**; must follow this rule (see Variant 9, 11 template code)
5. **vec2/vec3 dimension mismatch causing compile failure**: `p.xz` returns `vec2` and cannot be passed directly to noise/fbm functions expecting `vec3` parameters or used in operations with `vec3`. Use `vec3(p.xz, val)` to construct a full vec3, or use vec2 versions of functions
6. **Mountain/terrain height-based coloring invisible**: (1) `maxH` must equal the actual max return value of the terrain noise function (don't arbitrarily use 20.0); (2) grass threshold at 0.4 (largest area ensures green is visible), rock 0.4~0.7, snow >0.7; (3) grass green must be saturated enough `vec3(0.25, 0.55, 0.15)` not grayish; (4) sun intensity ≤2.0, sky light ≤1.0, too bright washes out colors; (5) gamma correction reduces saturation, pre-compensate material colors (see Step 4 mountain terrain template)
7. **Waterfall/flowing water effect lacks recognizability**: waterfall must have a clear cliff drop (≥10 cells), visible water column (noise + iTime offset), bottom splash particles (hash random bouncing), and mist (exponential decay density field). Just a gradient color block is not a waterfall! See Variant 10 complete template
8. **"Low saturation coloring" becomes pure white/gray**: low saturation ≠ near white! Low saturation means colors are not vivid but still have clear hue (e.g., brick red `vec3(0.55, 0.35, 0.3)` not gray-white `vec3(0.8, 0.8, 0.8)`). Brick/stone textures must use UV periodic patterns (staggered rows + mortar dark lines), not solid colors. See the `getBrickColor` function in the complete template
9. **Sunset/side-lit scene buildings become black silhouettes**: when low-angle light (sunset/dawn) illuminates from the side, building fronts are completely backlit → pure black silhouettes with no visible detail. Must: (1) ambient light ≥ 0.3; (2) walls use bright materials (light yellow, off-white) not dark colors; (3) buildings large enough (width/height ≥ 5 cells). See Variant 11 sunset scene code
## Further Reading
For full step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/voxel-rendering.md)