986 lines
45 KiB
Markdown
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)
|