Files
skills/shader-dev/techniques/csg-boolean-operations.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

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)