492 lines
15 KiB
Markdown
492 lines
15 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")`
|
|
- First line of shaders: `#version 300 es`, add `precision highp float;` in fragment shaders
|
|
- Vertex shader: `attribute` -> `in`, `varying` -> `out`
|
|
- Fragment shader: `varying` -> `in`, `gl_FragColor` -> custom output variable (must be declared before `void main()`, e.g. `out vec4 outColor;`), `texture2D()` -> `texture()`
|
|
- ShaderToy's `void mainImage(out vec4 fragColor, in vec2 fragCoord)` must be adapted to the standard `void main()` entry point
|
|
|
|
# CSG Boolean Operations
|
|
|
|
## Core Principles
|
|
|
|
CSG boolean operations are per-point value operations on two distance fields:
|
|
|
|
| Operation | Expression | Meaning |
|
|
|-----------|-----------|---------|
|
|
| Union | `min(d1, d2)` | Take nearest surface, keeping both shapes |
|
|
| Intersection | `max(d1, d2)` | Take farthest surface, keeping only the overlap |
|
|
| Subtraction | `max(d1, -d2)` | Cut d1 using the interior of d2 |
|
|
|
|
**Smooth booleans** (smooth min/max) introduce a blending band in the transition region. The parameter `k` controls the blend band width (larger = rounder, `k=0` degenerates to hard boolean). Multiple variants exist with different mathematical properties.
|
|
|
|
## Implementation Steps
|
|
|
|
### Step 1: Hard Boolean Operations
|
|
|
|
```glsl
|
|
float opUnion(float d1, float d2) { return min(d1, d2); }
|
|
float opIntersection(float d1, float d2) { return max(d1, d2); }
|
|
float opSubtraction(float d1, float d2) { return max(d1, -d2); }
|
|
```
|
|
|
|
### Step 2: Smooth Union (Polynomial Version)
|
|
|
|
```glsl
|
|
// k: blend radius, typical values 0.05~0.5
|
|
float opSmoothUnion(float d1, float d2, float k) {
|
|
float h = clamp(0.5 + 0.5 * (d2 - d1) / k, 0.0, 1.0);
|
|
return mix(d2, d1, h) - k * h * (1.0 - h);
|
|
}
|
|
```
|
|
|
|
### Step 3: Smooth Subtraction and Intersection (Polynomial Version)
|
|
|
|
```glsl
|
|
float opSmoothSubtraction(float d1, float d2, float k) {
|
|
float h = clamp(0.5 - 0.5 * (d2 + d1) / k, 0.0, 1.0);
|
|
return mix(d2, -d1, h) + k * h * (1.0 - h);
|
|
}
|
|
|
|
float opSmoothIntersection(float d1, float d2, float k) {
|
|
float h = clamp(0.5 - 0.5 * (d2 - d1) / k, 0.0, 1.0);
|
|
return mix(d2, d1, h) + k * h * (1.0 - h);
|
|
}
|
|
```
|
|
|
|
### Step 4: Quadratic Optimized Version (Recommended as Default)
|
|
|
|
```glsl
|
|
float smin(float a, float b, float k) {
|
|
float h = max(k - abs(a - b), 0.0);
|
|
return min(a, b) - h * h * 0.25 / k;
|
|
}
|
|
|
|
float smax(float a, float b, float k) {
|
|
float h = max(k - abs(a - b), 0.0);
|
|
return max(a, b) + h * h * 0.25 / k;
|
|
}
|
|
|
|
// Subtraction via smax
|
|
float sSub(float d1, float d2, float k) {
|
|
return smax(d1, -d2, k);
|
|
}
|
|
```
|
|
|
|
### Step 4b: Smooth Minimum Variant Library
|
|
|
|
Different smin implementations have different mathematical properties. Choose based on your needs:
|
|
|
|
| Variant | Rigid | Associative | Best For |
|
|
|---------|-------|-------------|----------|
|
|
| Quadratic (default above) | Yes | No | General use, fastest |
|
|
| Cubic | Yes | No | Smoother C2 transitions |
|
|
| Quartic | Yes | No | Highest quality blending |
|
|
| Exponential | No | Yes | Multi-body blending (order-independent) |
|
|
| Circular Geometric | Yes | Yes | Strict local blending |
|
|
|
|
**Rigid**: preserves original SDF shape outside the blend region (no under-estimation).
|
|
**Associative**: `smin(a, smin(b, c))` == `smin(smin(a, b), c)` — important when blending many objects where evaluation order varies.
|
|
|
|
```glsl
|
|
// --- Cubic Polynomial smin (C2 continuous, smoother transitions) ---
|
|
float sminCubic(float a, float b, float k) {
|
|
k *= 6.0;
|
|
float h = max(k - abs(a - b), 0.0) / k;
|
|
return min(a, b) - h * h * h * k * (1.0 / 6.0);
|
|
}
|
|
|
|
// --- Quartic Polynomial smin (C3 continuous, highest quality) ---
|
|
float sminQuartic(float a, float b, float k) {
|
|
k *= 16.0 / 3.0;
|
|
float h = max(k - abs(a - b), 0.0) / k;
|
|
return min(a, b) - h * h * h * (4.0 - h) * k * (1.0 / 16.0);
|
|
}
|
|
|
|
// --- Exponential smin (associative — order independent for multi-body blending) ---
|
|
float sminExp(float a, float b, float k) {
|
|
float r = exp2(-a / k) + exp2(-b / k);
|
|
return -k * log2(r);
|
|
}
|
|
|
|
// --- Circular Geometric smin (rigid + local + associative) ---
|
|
float sminCircle(float a, float b, float k) {
|
|
k *= 1.0 / (1.0 - sqrt(0.5));
|
|
return max(k, min(a, b)) - length(max(k - vec2(a, b), 0.0));
|
|
}
|
|
|
|
// --- Gradient-aware smin (carries material/color through blending) ---
|
|
// x = distance, yzw = material properties or color components
|
|
vec4 sminColor(vec4 a, vec4 b, float k) {
|
|
k *= 4.0;
|
|
float h = max(k - abs(a.x - b.x), 0.0) / (2.0 * k);
|
|
return vec4(
|
|
min(a.x, b.x) - h * h * k,
|
|
mix(a.yzw, b.yzw, (a.x < b.x) ? h : 1.0 - h)
|
|
);
|
|
}
|
|
|
|
// --- Smooth maximum from any smin variant ---
|
|
// smax(a, b, k) = -smin(-a, -b, k)
|
|
// Smooth subtraction: smax(d1, -d2, k)
|
|
// Smooth intersection: smax(d1, d2, k)
|
|
```
|
|
|
|
### Step 5: Basic SDF Primitives
|
|
|
|
```glsl
|
|
float sdSphere(vec3 p, float r) {
|
|
return length(p) - r;
|
|
}
|
|
|
|
float sdBox(vec3 p, vec3 b) {
|
|
vec3 d = abs(p) - b;
|
|
return length(max(d, 0.0)) + min(max(d.x, max(d.y, d.z)), 0.0);
|
|
}
|
|
|
|
float sdCylinder(vec3 p, float h, float r) {
|
|
vec2 d = abs(vec2(length(p.xz), p.y)) - vec2(r, h);
|
|
return min(max(d.x, d.y), 0.0) + length(max(d, 0.0));
|
|
}
|
|
```
|
|
|
|
### Step 6: CSG Composition for Scene Building
|
|
|
|
```glsl
|
|
float mapScene(vec3 p) {
|
|
float cube = sdBox(p, vec3(1.0));
|
|
float sphere = sdSphere(p, 1.2);
|
|
float cylX = sdCylinder(p.yzx, 2.0, 0.4);
|
|
float cylY = sdCylinder(p.xyz, 2.0, 0.4);
|
|
float cylZ = sdCylinder(p.zxy, 2.0, 0.4);
|
|
|
|
// (cube intersect sphere) - three cylinders = nut
|
|
float shape = opIntersection(cube, sphere);
|
|
float holes = opUnion(cylX, opUnion(cylY, cylZ));
|
|
return opSubtraction(shape, holes);
|
|
}
|
|
```
|
|
|
|
### Step 7: Smooth CSG Modeling for Organic Forms
|
|
|
|
```glsl
|
|
// Use different k values for different body parts: large k for major joints, small k for fine details
|
|
float mapCreature(vec3 p) {
|
|
float body = sdSphere(p, 0.5);
|
|
float head = sdSphere(p - vec3(0.0, 0.6, 0.3), 0.25);
|
|
float d = smin(body, head, 0.15); // large blend
|
|
|
|
float leg = sdCylinder(p - vec3(0.2, -0.5, 0.0), 0.3, 0.08);
|
|
d = smin(d, leg, 0.08); // medium blend
|
|
|
|
float eye = sdSphere(p - vec3(0.05, 0.75, 0.4), 0.05);
|
|
d = smax(d, -eye, 0.02); // small blend for subtraction
|
|
return d;
|
|
}
|
|
```
|
|
|
|
### Step 8: Ray Marching Main Loop
|
|
|
|
```glsl
|
|
float rayMarch(vec3 ro, vec3 rd, float maxDist) {
|
|
float t = 0.0;
|
|
for (int i = 0; i < MAX_STEPS; i++) {
|
|
vec3 p = ro + rd * t;
|
|
float d = mapScene(p);
|
|
if (d < SURF_DIST) return t;
|
|
t += d;
|
|
if (t > maxDist) break;
|
|
}
|
|
return -1.0;
|
|
}
|
|
```
|
|
|
|
### Step 9: Normal Calculation (Tetrahedral Sampling, 4 Samples More Efficient Than 6 with Central Differences)
|
|
|
|
```glsl
|
|
vec3 calcNormal(vec3 pos) {
|
|
vec2 e = vec2(0.001, -0.001);
|
|
return normalize(
|
|
e.xyy * mapScene(pos + e.xyy) +
|
|
e.yyx * mapScene(pos + e.yyx) +
|
|
e.yxy * mapScene(pos + e.yxy) +
|
|
e.xxx * mapScene(pos + e.xxx)
|
|
);
|
|
}
|
|
```
|
|
|
|
## Full Code Template
|
|
|
|
```glsl
|
|
// === CSG Boolean Operations - WebGL2 Full Template ===
|
|
// Note: When generating HTML with this template, pass iTime, iResolution, etc. via uniforms
|
|
|
|
#define MAX_STEPS 128
|
|
#define MAX_DIST 50.0
|
|
#define SURF_DIST 0.001
|
|
#define SMOOTH_K 0.1
|
|
|
|
// === Hard Boolean Operations ===
|
|
float opUnion(float d1, float d2) { return min(d1, d2); }
|
|
float opIntersection(float d1, float d2) { return max(d1, d2); }
|
|
float opSubtraction(float d1, float d2) { return max(d1, -d2); }
|
|
|
|
// === Smooth Boolean Operations (Quadratic Optimized) ===
|
|
float smin(float a, float b, float k) {
|
|
float h = max(k - abs(a - b), 0.0);
|
|
return min(a, b) - h * h * 0.25 / k;
|
|
}
|
|
|
|
float smax(float a, float b, float k) {
|
|
float h = max(k - abs(a - b), 0.0);
|
|
return max(a, b) + h * h * 0.25 / k;
|
|
}
|
|
|
|
// === SDF Primitives ===
|
|
float sdSphere(vec3 p, float r) {
|
|
return length(p) - r;
|
|
}
|
|
|
|
float sdBox(vec3 p, vec3 b) {
|
|
vec3 d = abs(p) - b;
|
|
return length(max(d, 0.0)) + min(max(d.x, max(d.y, d.z)), 0.0);
|
|
}
|
|
|
|
float sdCylinder(vec3 p, float h, float r) {
|
|
vec2 d = abs(vec2(length(p.xz), p.y)) - vec2(r, h);
|
|
return min(max(d.x, d.y), 0.0) + length(max(d, 0.0));
|
|
}
|
|
|
|
float sdEllipsoid(vec3 p, vec3 r) {
|
|
float k0 = length(p / r);
|
|
float k1 = length(p / (r * r));
|
|
return k0 * (k0 - 1.0) / k1;
|
|
}
|
|
|
|
float sdCapsule(vec3 p, vec3 a, vec3 b, float r) {
|
|
vec3 pa = p - a, ba = b - a;
|
|
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
|
|
return length(pa - ba * h) - r;
|
|
}
|
|
|
|
// === Scene Definition ===
|
|
float mapScene(vec3 p) {
|
|
// Rotation animation
|
|
float angle = iTime * 0.3;
|
|
float c = cos(angle), s = sin(angle);
|
|
p.xz = mat2(c, -s, s, c) * p.xz;
|
|
|
|
// Primitives
|
|
float cube = sdBox(p, vec3(1.0));
|
|
float sphere = sdSphere(p, 1.25);
|
|
float cylR = 0.45;
|
|
float cylX = sdCylinder(p.yzx, 2.0, cylR);
|
|
float cylY = sdCylinder(p.xyz, 2.0, cylR);
|
|
float cylZ = sdCylinder(p.zxy, 2.0, cylR);
|
|
|
|
// Hard boolean combination: nut = (cube intersect sphere) - three cylinders
|
|
float nut = opSubtraction(
|
|
opIntersection(cube, sphere),
|
|
opUnion(cylX, opUnion(cylY, cylZ))
|
|
);
|
|
|
|
// Organic spheres -- smooth union blending
|
|
float blob1 = sdSphere(p - vec3(1.8, 0.0, 0.0), 0.4);
|
|
float blob2 = sdSphere(p - vec3(-1.8, 0.0, 0.0), 0.4);
|
|
float blob3 = sdSphere(p - vec3(0.0, 1.8, 0.0), 0.4);
|
|
float blobs = smin(blob1, smin(blob2, blob3, 0.3), 0.3);
|
|
|
|
return smin(nut, blobs, 0.15);
|
|
}
|
|
|
|
// === Normal Calculation (Tetrahedral Sampling) ===
|
|
vec3 calcNormal(vec3 pos) {
|
|
vec2 e = vec2(0.001, -0.001);
|
|
return normalize(
|
|
e.xyy * mapScene(pos + e.xyy) +
|
|
e.yyx * mapScene(pos + e.yyx) +
|
|
e.yxy * mapScene(pos + e.yxy) +
|
|
e.xxx * mapScene(pos + e.xxx)
|
|
);
|
|
}
|
|
|
|
// === Ray Marching ===
|
|
float rayMarch(vec3 ro, vec3 rd) {
|
|
float t = 0.0;
|
|
for (int i = 0; i < MAX_STEPS; i++) {
|
|
vec3 p = ro + rd * t;
|
|
float d = mapScene(p);
|
|
if (d < SURF_DIST) return t;
|
|
t += d;
|
|
if (t > MAX_DIST) break;
|
|
}
|
|
return -1.0;
|
|
}
|
|
|
|
// === Soft Shadows ===
|
|
float calcSoftShadow(vec3 ro, vec3 rd, float k) {
|
|
float res = 1.0;
|
|
float t = 0.02;
|
|
for (int i = 0; i < 64; i++) {
|
|
float h = mapScene(ro + rd * t);
|
|
res = min(res, k * h / t);
|
|
t += clamp(h, 0.01, 0.2);
|
|
if (res < 0.001 || t > 20.0) break;
|
|
}
|
|
return clamp(res, 0.0, 1.0);
|
|
}
|
|
|
|
// === AO (Ambient Occlusion) ===
|
|
float calcAO(vec3 pos, vec3 nor) {
|
|
float occ = 0.0;
|
|
float sca = 1.0;
|
|
for (int i = 0; i < 5; i++) {
|
|
float h = 0.01 + 0.12 * float(i);
|
|
float d = mapScene(pos + h * nor);
|
|
occ += (h - d) * sca;
|
|
sca *= 0.95;
|
|
}
|
|
return clamp(1.0 - 3.0 * occ, 0.0, 1.0);
|
|
}
|
|
|
|
// === Main Function (WebGL2 Adapted) ===
|
|
out vec4 outColor;
|
|
void main() {
|
|
vec2 uv = (gl_FragCoord.xy - 0.5 * iResolution.xy) / iResolution.y;
|
|
|
|
// Camera
|
|
float camDist = 4.0;
|
|
float camAngle = 0.3;
|
|
vec3 ro = vec3(
|
|
camDist * cos(iTime * 0.2),
|
|
camDist * sin(camAngle),
|
|
camDist * sin(iTime * 0.2)
|
|
);
|
|
vec3 ta = vec3(0.0, 0.0, 0.0);
|
|
|
|
// Camera matrix
|
|
vec3 ww = normalize(ta - ro);
|
|
vec3 uu = normalize(cross(ww, vec3(0.0, 1.0, 0.0)));
|
|
vec3 vv = cross(uu, ww);
|
|
vec3 rd = normalize(uv.x * uu + uv.y * vv + 2.0 * ww);
|
|
|
|
// Background color
|
|
vec3 col = vec3(0.4, 0.5, 0.6) - 0.3 * rd.y;
|
|
|
|
// Ray marching
|
|
float t = rayMarch(ro, rd);
|
|
if (t > 0.0) {
|
|
vec3 pos = ro + rd * t;
|
|
vec3 nor = calcNormal(pos);
|
|
|
|
vec3 lightDir = normalize(vec3(0.8, 0.6, -0.3));
|
|
float dif = clamp(dot(nor, lightDir), 0.0, 1.0);
|
|
float sha = calcSoftShadow(pos + nor * 0.01, lightDir, 16.0);
|
|
float ao = calcAO(pos, nor);
|
|
float amb = 0.5 + 0.5 * nor.y;
|
|
|
|
vec3 mate = vec3(0.2, 0.3, 0.4);
|
|
col = vec3(0.0);
|
|
col += mate * 2.0 * dif * sha;
|
|
col += mate * 0.3 * amb * ao;
|
|
}
|
|
|
|
col = pow(col, vec3(0.4545));
|
|
outColor = vec4(col, 1.0);
|
|
}
|
|
```
|
|
|
|
## Common Variants
|
|
|
|
### Variant 1: Exponential Smooth Union
|
|
|
|
```glsl
|
|
float sminExp(float a, float b, float k) {
|
|
float res = exp(-k * a) + exp(-k * b);
|
|
return -log(res) / k;
|
|
}
|
|
```
|
|
|
|
### Variant 2: Smooth Operations with Color Blending
|
|
|
|
```glsl
|
|
// Returns blend factor for the caller to blend colors
|
|
float sminWithFactor(float a, float b, float k, out float blend) {
|
|
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
|
|
blend = h;
|
|
return mix(b, a, h) - k * h * (1.0 - h);
|
|
}
|
|
// float blend;
|
|
// float d = sminWithFactor(d1, d2, 0.1, blend);
|
|
// vec3 color = mix(color2, color1, blend);
|
|
|
|
// vec3 overload of smax
|
|
vec3 smax(vec3 a, vec3 b, float k) {
|
|
vec3 h = max(k - abs(a - b), 0.0);
|
|
return max(a, b) + h * h * 0.25 / k;
|
|
}
|
|
```
|
|
|
|
### Variant 3: Stepwise CSG Modeling (Architectural/Industrial)
|
|
|
|
```glsl
|
|
float sdBuilding(vec3 p) {
|
|
float walls = sdBox(p, vec3(1.0, 0.8, 1.0));
|
|
vec3 roofP = p;
|
|
roofP.y -= 0.8;
|
|
float roof = sdBox(roofP, vec3(1.2, 0.3, 1.2));
|
|
float d = opUnion(walls, roof);
|
|
|
|
// Cut windows (exploiting symmetry)
|
|
vec3 winP = abs(p);
|
|
winP -= vec3(1.01, 0.3, 0.4);
|
|
float window = sdBox(winP, vec3(0.1, 0.15, 0.12));
|
|
d = opSubtraction(d, window);
|
|
|
|
// Hollow out interior
|
|
float hollow = sdBox(p, vec3(0.95, 0.75, 0.95));
|
|
d = opSubtraction(d, hollow);
|
|
return d;
|
|
}
|
|
```
|
|
|
|
### Variant 4: Large-Scale Organic Character Modeling
|
|
|
|
```glsl
|
|
float mapCharacter(vec3 p) {
|
|
float body = sdEllipsoid(p, vec3(0.5, 0.4, 0.6));
|
|
float head = sdEllipsoid(p - vec3(0.0, 0.5, 0.5), vec3(0.25));
|
|
float d = smin(body, head, 0.2); // large k: wide blend
|
|
|
|
float ear = sdEllipsoid(p - vec3(0.3, 0.6, 0.3), vec3(0.15, 0.2, 0.05));
|
|
d = smin(d, ear, 0.08); // medium blend
|
|
|
|
float nostril = sdSphere(p - vec3(0.0, 0.4, 0.7), 0.03);
|
|
d = smax(d, -nostril, 0.02); // small k: fine sculpting
|
|
return d;
|
|
}
|
|
```
|
|
|
|
## Performance & Composition Tips
|
|
|
|
**Performance:**
|
|
- Bounding volume acceleration: use AABB/bounding spheres to skip distant sub-scenes, reducing `mapScene()` calls
|
|
- Tetrahedral sampling normals (4 samples) outperform central differences (6 samples)
|
|
- Step scaling `t += d * 0.9` can reduce overshoot penetration
|
|
- Prefer quadratic optimized smin/smax (fastest); use exponential version when extreme smoothness is needed
|
|
- `k` must not be zero (division by zero error); fall back to hard boolean when near zero
|
|
- For symmetric shapes, use `abs()` to fold coordinates and define only one side
|
|
|
|
**Composition techniques:**
|
|
- **+ Domain Repetition**: `mod()`/`fract()` for infinite repetition of CSG shapes (mechanical arrays, railings)
|
|
- **+ Procedural Displacement**: overlay noise displacement on SDF for surface detail
|
|
- **+ Procedural Texturing**: use smin blend factor to simultaneously blend material ID / color
|
|
- **+ 2D SDF**: equally applicable to 2D scenes (clouds, UI shape compositing)
|
|
- **+ Animation**: bind k values, positions, and radii to `iTime` for dynamic deformation
|
|
|
|
## Further Reading
|
|
|
|
Full step-by-step tutorials, mathematical derivations, and advanced usage in [reference](../reference/csg-boolean-operations.md)
|