319 lines
9.1 KiB
Markdown
319 lines
9.1 KiB
Markdown
## WebGL2 Adaptation Requirements
|
|
|
|
**IMPORTANT: GLSL Type Strictness Warning**:
|
|
- GLSL is a strongly typed language with **no `string` type**; using string types is forbidden
|
|
- Common illegal types: `string`, `int` (can only use `int` literals, cannot declare variable types as `int`)
|
|
- vec2/vec3/vec4 cannot be implicitly converted between each other; explicit construction is required
|
|
- Float precision: `highp float` (recommended), `mediump float`, `lowp float`
|
|
|
|
The code templates in this document use ShaderToy GLSL style. When generating standalone HTML pages, you must adapt for WebGL2:
|
|
|
|
- Use `canvas.getContext("webgl2")`
|
|
- Shader first line: `#version 300 es`, add `precision highp float;` in fragment shader
|
|
- 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)` must be adapted to the standard `void main()` entry point
|
|
|
|
# SDF Normal Estimation
|
|
|
|
## Use Cases
|
|
|
|
- Lighting calculations in raymarching rendering pipelines (diffuse, specular, Fresnel, etc.)
|
|
- Any 3D scene based on SDF distance fields (fractals, parametric surfaces, boolean geometry, procedural terrain)
|
|
- Edge detection and contour rendering (Laplacian value as a byproduct of normal sampling)
|
|
- Prerequisite for ambient occlusion (AO) computation
|
|
|
|
## Core Principles
|
|
|
|
The gradient of an SDF `nabla f(p)` points in the direction of fastest distance increase, which is the outward surface normal. Numerical differentiation approximates the gradient:
|
|
|
|
$$\vec{n} = \text{normalize}\left(\nabla f(p)\right)$$
|
|
|
|
Three main strategies:
|
|
|
|
| Method | Samples | Accuracy | Recommendation |
|
|
|--------|---------|----------|----------------|
|
|
| Forward difference | 4 | O(epsilon) | Simple scenes |
|
|
| Central difference | 6 | O(epsilon^2) | When symmetry is needed |
|
|
| **Tetrahedron method** | **4** | **Between the two** | **Preferred** |
|
|
|
|
Key parameter epsilon: commonly `0.0005 ~ 0.001`; for advanced scenes, multiply by ray distance `t` for adaptive scaling.
|
|
|
|
## Implementation Steps
|
|
|
|
### Step 1: Define SDF Scene Function
|
|
|
|
```glsl
|
|
float map(vec3 p) {
|
|
float d = length(p) - 1.0; // unit sphere
|
|
return d;
|
|
}
|
|
```
|
|
|
|
### Step 2: Choose Differentiation Method
|
|
|
|
#### Method A: Forward Difference -- 4 Samples
|
|
|
|
```glsl
|
|
const float EPSILON = 1e-3;
|
|
|
|
vec3 getNormal(vec3 p) {
|
|
vec3 n;
|
|
n.x = map(vec3(p.x + EPSILON, p.y, p.z));
|
|
n.y = map(vec3(p.x, p.y + EPSILON, p.z));
|
|
n.z = map(vec3(p.x, p.y, p.z + EPSILON));
|
|
return normalize(n - map(p));
|
|
}
|
|
```
|
|
|
|
#### Method B: Central Difference -- 6 Samples
|
|
|
|
```glsl
|
|
vec3 getNormal(vec3 p) {
|
|
vec2 o = vec2(0.001, 0.0);
|
|
return normalize(vec3(
|
|
map(p + o.xyy) - map(p - o.xyy),
|
|
map(p + o.yxy) - map(p - o.yxy),
|
|
map(p + o.yyx) - map(p - o.yyx)
|
|
));
|
|
}
|
|
```
|
|
|
|
#### Method C: Tetrahedron Method -- 4 Samples (Recommended)
|
|
|
|
```glsl
|
|
// Classic tetrahedron method, coefficient 0.5773 ~ 1/sqrt(3)
|
|
vec3 calcNormal(vec3 pos) {
|
|
float eps = 0.0005;
|
|
vec2 e = vec2(1.0, -1.0) * 0.5773;
|
|
return normalize(
|
|
e.xyy * map(pos + e.xyy * eps) +
|
|
e.yyx * map(pos + e.yyx * eps) +
|
|
e.yxy * map(pos + e.yxy * eps) +
|
|
e.xxx * map(pos + e.xxx * eps)
|
|
);
|
|
}
|
|
```
|
|
|
|
### Step 3: Apply to Lighting
|
|
|
|
```glsl
|
|
vec3 pos = ro + rd * t; // hit point
|
|
vec3 nor = calcNormal(pos); // surface normal
|
|
|
|
vec3 lightDir = normalize(vec3(1.0, 4.0, -4.0));
|
|
float diff = max(dot(nor, lightDir), 0.0);
|
|
vec3 col = vec3(0.8) * diff;
|
|
```
|
|
|
|
## Complete Code Template
|
|
|
|
```glsl
|
|
// SDF Normal Estimation — Complete ShaderToy Template
|
|
|
|
#define MAX_STEPS 128
|
|
#define MAX_DIST 100.0
|
|
#define SURF_DIST 0.001
|
|
#define NORMAL_METHOD 2 // 0=forward diff, 1=central diff, 2=tetrahedron
|
|
|
|
// ---- SDF Scene Definition ----
|
|
float map(vec3 p) {
|
|
float sphere = length(p - vec3(0.0, 1.0, 0.0)) - 1.0;
|
|
float ground = p.y;
|
|
return min(sphere, ground);
|
|
}
|
|
|
|
// ---- Normal Estimation ----
|
|
|
|
vec3 normalForward(vec3 p) {
|
|
float eps = 0.001;
|
|
float d = map(p);
|
|
return normalize(vec3(
|
|
map(p + vec3(eps, 0.0, 0.0)),
|
|
map(p + vec3(0.0, eps, 0.0)),
|
|
map(p + vec3(0.0, 0.0, eps))
|
|
) - d);
|
|
}
|
|
|
|
vec3 normalCentral(vec3 p) {
|
|
vec2 e = vec2(0.001, 0.0);
|
|
return normalize(vec3(
|
|
map(p + e.xyy) - map(p - e.xyy),
|
|
map(p + e.yxy) - map(p - e.yxy),
|
|
map(p + e.yyx) - map(p - e.yyx)
|
|
));
|
|
}
|
|
|
|
vec3 normalTetra(vec3 p) {
|
|
float eps = 0.0005;
|
|
vec2 e = vec2(1.0, -1.0) * 0.5773;
|
|
return normalize(
|
|
e.xyy * map(p + e.xyy * eps) +
|
|
e.yyx * map(p + e.yyx * eps) +
|
|
e.yxy * map(p + e.yxy * eps) +
|
|
e.xxx * map(p + e.xxx * eps)
|
|
);
|
|
}
|
|
|
|
vec3 calcNormal(vec3 p) {
|
|
#if NORMAL_METHOD == 0
|
|
return normalForward(p);
|
|
#elif NORMAL_METHOD == 1
|
|
return normalCentral(p);
|
|
#else
|
|
return normalTetra(p);
|
|
#endif
|
|
}
|
|
|
|
// ---- Raymarching ----
|
|
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 = map(p);
|
|
if (d < SURF_DIST || t > MAX_DIST) break;
|
|
t += d;
|
|
}
|
|
return t;
|
|
}
|
|
|
|
// ---- Main Function ----
|
|
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
|
|
|
|
vec3 ro = vec3(0.0, 2.0, -5.0);
|
|
vec3 rd = normalize(vec3(uv, 1.5));
|
|
|
|
float t = raymarch(ro, rd);
|
|
vec3 col = vec3(0.0);
|
|
|
|
if (t < MAX_DIST) {
|
|
vec3 pos = ro + rd * t;
|
|
vec3 nor = calcNormal(pos);
|
|
|
|
vec3 sunDir = normalize(vec3(0.8, 0.4, -0.6));
|
|
float diff = clamp(dot(nor, sunDir), 0.0, 1.0);
|
|
float amb = 0.5 + 0.5 * nor.y;
|
|
vec3 ref = reflect(rd, nor);
|
|
float spec = pow(clamp(dot(ref, sunDir), 0.0, 1.0), 16.0);
|
|
|
|
col = vec3(0.18) * amb + vec3(1.0, 0.95, 0.85) * diff + vec3(0.5) * spec;
|
|
} else {
|
|
col = vec3(0.5, 0.7, 1.0) - 0.5 * rd.y;
|
|
}
|
|
|
|
col = pow(col, vec3(0.4545));
|
|
fragColor = vec4(col, 1.0);
|
|
}
|
|
```
|
|
|
|
## Common Variants
|
|
|
|
### Variant 1: NuSan Reverse-Offset Forward Difference
|
|
|
|
```glsl
|
|
// Reverse-offset forward difference
|
|
vec2 noff = vec2(0.001, 0.0);
|
|
vec3 normal = normalize(
|
|
map(pos) - vec3(
|
|
map(pos - noff.xyy),
|
|
map(pos - noff.yxy),
|
|
map(pos - noff.yyx)
|
|
)
|
|
);
|
|
```
|
|
|
|
### Variant 2: Adaptive Epsilon (Distance Scaling)
|
|
|
|
```glsl
|
|
// Adaptive epsilon based on ray distance
|
|
vec3 calcNormal(vec3 pos, float t) {
|
|
float precis = 0.001 * t;
|
|
vec2 e = vec2(1.0, -1.0) * precis;
|
|
return normalize(
|
|
e.xyy * map(pos + e.xyy) +
|
|
e.yyx * map(pos + e.yyx) +
|
|
e.yxy * map(pos + e.yxy) +
|
|
e.xxx * map(pos + e.xxx)
|
|
);
|
|
}
|
|
```
|
|
|
|
### Variant 3: Large Epsilon for Rounding / Anti-Aliasing
|
|
|
|
```glsl
|
|
// Large epsilon for rounding / anti-aliasing
|
|
vec3 getNormal(vec3 p) {
|
|
vec2 e = vec2(0.015, -0.015); // intentionally large epsilon
|
|
return normalize(
|
|
e.xyy * map(p + e.xyy) +
|
|
e.yyx * map(p + e.yyx) +
|
|
e.yxy * map(p + e.yxy) +
|
|
e.xxx * map(p + e.xxx)
|
|
);
|
|
}
|
|
```
|
|
|
|
### Variant 4: Anti-Inlining Loop
|
|
|
|
```glsl
|
|
// Anti-inlining loop — reduces compile time for complex SDFs
|
|
#define ZERO (min(iFrame, 0))
|
|
|
|
vec3 calcNormal(vec3 p, float t) {
|
|
vec3 n = vec3(0.0);
|
|
for (int i = ZERO; i < 4; i++) {
|
|
vec3 e = 0.5773 * (2.0 * vec3(
|
|
(((i + 3) >> 1) & 1),
|
|
((i >> 1) & 1),
|
|
(i & 1)
|
|
) - 1.0);
|
|
n += e * map(p + e * 0.001 * t);
|
|
}
|
|
return normalize(n);
|
|
}
|
|
```
|
|
|
|
### Variant 5: Normal + Edge Detection
|
|
|
|
```glsl
|
|
// Central difference + Laplacian edge detection
|
|
float edge = 0.0;
|
|
vec3 normal(vec3 p) {
|
|
vec3 e = vec3(0.0, det * 5.0, 0.0);
|
|
|
|
float d1 = de(p - e.yxx), d2 = de(p + e.yxx);
|
|
float d3 = de(p - e.xyx), d4 = de(p + e.xyx);
|
|
float d5 = de(p - e.xxy), d6 = de(p + e.xxy);
|
|
float d = de(p);
|
|
|
|
edge = abs(d - 0.5 * (d2 + d1))
|
|
+ abs(d - 0.5 * (d4 + d3))
|
|
+ abs(d - 0.5 * (d6 + d5));
|
|
edge = min(1.0, pow(edge, 0.55) * 15.0);
|
|
|
|
return normalize(vec3(d1 - d2, d3 - d4, d5 - d6));
|
|
}
|
|
```
|
|
|
|
## Performance & Composition
|
|
|
|
**Performance**:
|
|
- Default to tetrahedron method (4 samples, better accuracy than forward difference)
|
|
- Only switch to central difference (6 samples) when jagged normal artifacts appear
|
|
- Use anti-inlining loop (Variant 4) for complex SDFs to avoid compile time explosion
|
|
- Epsilon recommended `0.0005 ~ 0.001`; best practice is adaptive `eps * t`
|
|
- Too small (< 1e-5) produces floating-point noise; too large (> 0.05) loses detail
|
|
- Reuse SDF sampling results when multiple types of information are needed at the same position (e.g., Variant 5)
|
|
|
|
**Common combinations**:
|
|
- **Normal + Soft Shadow**: `calcSoftShadow(pos + nor * 0.01, sunDir, 16.0)` -- normal offset at start point to avoid self-intersection
|
|
- **Normal + AO**: Multi-step SDF sampling along the normal to estimate occlusion
|
|
- **Normal + Fresnel**: `pow(clamp(1.0 + dot(nor, rd), 0.0, 1.0), 5.0)`
|
|
- **Normal + Bump Mapping**: Overlay texture gradient perturbation on SDF normals
|
|
- **Normal + Triplanar Mapping**: Use `abs(nor)` components as triplanar blend weights
|
|
|
|
## Further Reading
|
|
|
|
For complete step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/normal-estimation.md)
|