渲染
高级渲染与 Raymarching
WebGPU/WebGLShaderGLSL/WGSLRaymarchingSDF体积渲染光线追踪
直觉问题:为什么 Shader 能渲染出无限细节的场景?
Q1: 一个简单的全屏 Quad 如何渲染出复杂的 3D 场景?
Q2: 为什么电影中的云雾如此真实?GPU 如何模拟体积光照?
核心概念白话讲
SDF(Signed Distance Field,有向距离场)
定义:空间中任意点到物体表面的最短距离
基本 SDF 函数:
| 形状 | SDF 函数 |
|---|---|
| 球体 | length(p) - radius |
| 立方体 | max(abs(p) - size) |
| 圆柱 | max(length(p.xy) - radius, abs(p.z) - height) |
| 平面 | p.y |
布尔操作:
- 并集:
min(d1, d2) - 交集:
max(d1, d2) - 差集:
max(d1, -d2)
TIP
- SDF 支持无限细节,无需几何网格
- 适合程序化生成、流体、植被等复杂场景
Raymarching(光线步进)
核心思想:从相机发射光线,沿光线方向步进,找到物体交点
算法流程:
graph LR
A[发射光线<br/>origin, direction] --> B[步进<br/>p = p + d * ray]
B --> C{距离 < ε?}
C -->|是| D[找到交点<br/>计算光照]
C -->|否| E{步数 > max?}
E -->|是| F[未命中<br/>背景色}
E -->|否| B
步进循环:
for (int i = 0; i < MAX_STEPS; i++) {
float d = map(p);
if (d < EPSILON) break;
p += ray_dir * d;
if (t > MAX_DISTANCE) break;
}
Raymarching vs Ray Tracing:
- Ray Tracing:解析求交(三角形)
- Raymarching:数值求交(SDF)
体积渲染
原理:光线穿过体积,累加光照贡献
光传输方程:
体素化云:
- 3D 纹理:存储密度、颜色
- Raymarching:沿光线采样 3D 纹理
- 光照计算:Phase Function 计算散射
光照探针
原理:预计算场景光照,存储在探针中
Light Probe:
- 球谐函数(SH):低频光照(2 阶,9 个系数)
- 立方体贴图:高频光照(6 面,每面 32×32)
应用:
- GI(Global Illumination):间接光照
- 反射:环境反射
- AO:环境光遮蔽
光线追踪基础
DXR(DirectX Raytracing)/ Vulkan RT:
- Acceleration Structure:BVH(Bounding Volume Hierarchy)
- Ray Generation Shader:发射光线
- Intersection Shader:求交测试
- Any Hit / Closest Hit:命中处理
- Miss Shader:未命中处理
WARNING
- 光线追踪需要 RTX 系列显卡支持
- WebGPU 尚未原生支持光线追踪
原理与数学机制
Raymarching 数学推导
光线方程:
步进过程:
终止条件:
法线计算
数值梯度:
有限差分近似:
vec3 calcNormal(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)
));
}
体积云 Phase Function
Henyey-Greenstein Phase Function:
参数:
g = 0:各向同性散射g > 0:前向散射(云)g < 0:后向散射(烟雾)
光照计算
Blinn-Phong 光照:
软阴影:
球谐函数(SH)
2 阶球谐系数:
光照重建:
GLSL vs WGSL 代码对照
Raymarching 渲染
GLSL 版本
#version 300 es
precision highp float;
out vec4 frag_color;
in vec2 v_texcoord;
uniform float u_time;
uniform vec2 u_resolution;
const int MAX_STEPS = 100;
const float MAX_DIST = 100.0;
const float EPSILON = 0.001;
float sdSphere(vec3 p, float r) {
return length(p) - r;
}
float sdBox(vec3 p, vec3 b) {
vec3 q = abs(p) - b;
return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0);
}
float map(vec3 p) {
vec3 p1 = p - vec3(cos(u_time) * 2.0, 0.0, 0.0);
float d1 = sdSphere(p1, 1.0);
vec3 p2 = p - vec3(-cos(u_time) * 2.0, 0.0, 0.0);
float d2 = sdBox(p2, vec3(0.8));
return min(d1, d2);
}
vec3 calcNormal(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)
));
}
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 < EPSILON) break;
t += d;
if (t > MAX_DIST) break;
}
return t;
}
vec3 getLight(vec3 p, vec3 n, vec3 rd) {
vec3 lightPos = vec3(2.0, 5.0, -3.0);
vec3 lightDir = normalize(lightPos - p);
float diff = max(dot(n, lightDir), 0.0);
float amb = 0.1;
vec3 color = vec3(1.0, 0.5, 0.3);
return color * (diff + amb);
}
void main() {
vec2 uv = (v_texcoord - 0.5) * 2.0;
uv.x *= u_resolution.x / u_resolution.y;
vec3 ro = vec3(0.0, 0.0, -5.0);
vec3 rd = normalize(vec3(uv, 1.0));
float t = raymarch(ro, rd);
vec3 col = vec3(0.0);
if (t < MAX_DIST) {
vec3 p = ro + rd * t;
vec3 n = calcNormal(p);
col = getLight(p, n, rd);
}
frag_color = vec4(col, 1.0);
}
WGSL 版本
struct FragmentInput {
@location(0) texcoord: vec2<f32>,
}
struct FragmentOutput {
@location(0) color: vec4<f32>,
}
@group(0) @binding(0)
var<uniform> time: f32;
@group(0) @binding(1)
var<uniform> resolution: vec2<f32>;
const MAX_STEPS: i32 = 100;
const MAX_DIST: f32 = 100.0;
const EPSILON: f32 = 0.001;
fn sdSphere(p: vec3<f32>, r: f32) -> f32 {
return length(p) - r;
}
fn sdBox(p: vec3<f32>, b: vec3<f32>) -> f32 {
let q = abs(p) - b;
return length(max(q, vec3<f32>(0.0))) + min(max(q.x, max(q.y, q.z)), 0.0);
}
fn map(p: vec3<f32>) -> f32 {
let p1 = p - vec3<f32>(cos(time) * 2.0, 0.0, 0.0);
let d1 = sdSphere(p1, 1.0);
let p2 = p - vec3<f32>(-cos(time) * 2.0, 0.0, 0.0);
let d2 = sdBox(p2, vec3<f32>(0.8));
return min(d1, d2);
}
fn calcNormal(p: vec3<f32>) -> vec3<f32> {
let e = vec2<f32>(0.001, 0.0);
return normalize(vec3<f32>(
map(p + e.xyy) - map(p - e.xyy),
map(p + e.yxy) - map(p - e.yxy),
map(p + e.yyx) - map(p - e.yyx)
));
}
fn raymarch(ro: vec3<f32>, rd: vec3<f32>) -> f32 {
var t: f32 = 0.0;
for (var i: i32 = 0; i < MAX_STEPS; i++) {
let p = ro + rd * t;
let d = map(p);
if (d < EPSILON) { break; }
t = t + d;
if (t > MAX_DIST) { break; }
}
return t;
}
fn getLight(p: vec3<f32>, n: vec3<f32>, rd: vec3<f32>) -> vec3<f32> {
let lightPos = vec3<f32>(2.0, 5.0, -3.0);
let lightDir = normalize(lightPos - p);
let diff = max(dot(n, lightDir), 0.0);
let amb: f32 = 0.1;
let color = vec3<f32>(1.0, 0.5, 0.3);
return color * (diff + amb);
}
@fragment
fn fs_main(input: FragmentInput) -> FragmentOutput {
var output: FragmentOutput;
var uv = (input.texcoord - vec2<f32>(0.5, 0.5)) * 2.0;
uv.x = uv.x * resolution.x / resolution.y;
let ro = vec3<f32>(0.0, 0.0, -5.0);
let rd = normalize(vec3<f32>(uv, 1.0));
let t = raymarch(ro, rd);
var col = vec3<f32>(0.0);
if (t < MAX_DIST) {
let p = ro + rd * t;
let n = calcNormal(p);
col = getLight(p, n, rd);
}
output.color = vec4<f32>(col, 1.0);
return output;
}
差异点:
- WGSL 使用
const定义常量,需要显式类型标注 - WGSL 的
for循环变量需要显式类型声明var i: i32 - WGSL 的
break语法与 GLSL 相同
体积云渲染
GLSL 版本
#version 300 es
precision highp float;
out vec4 frag_color;
in vec2 v_texcoord;
uniform sampler3D u_cloud_texture;
uniform float u_time;
uniform vec2 u_resolution;
float hgPhase(float g, float cosTheta) {
return (1.0 - g * g) / (4.0 * 3.14159 * pow(1.0 + g * g - 2.0 * g * cosTheta, 1.5));
}
float raymarchCloud(vec3 ro, vec3 rd, float maxDist) {
float density = 0.0;
vec3 p = ro;
for (int i = 0; i < 64; i++) {
float d = texture(u_cloud_texture, p * 0.01 + vec3(u_time * 0.01)).r;
density += d * 0.01;
p += rd * 0.1;
if (length(p - ro) > maxDist) break;
}
return density;
}
void main() {
vec2 uv = (v_texcoord - 0.5) * 2.0;
uv.x *= u_resolution.x / u_resolution.y;
vec3 ro = vec3(0.0, 1.0, -5.0);
vec3 rd = normalize(vec3(uv, 1.0));
float density = raymarchCloud(ro, rd, 20.0);
vec3 col = vec3(density * 0.8, density * 0.85, density * 0.9);
frag_color = vec4(col, density * 0.5);
}
WGSL 版本
struct FragmentInput {
@location(0) texcoord: vec2<f32>,
}
struct FragmentOutput {
@location(0) color: vec4<f32>,
}
@group(0) @binding(0)
var cloud_texture: texture_3d<f32>;
@group(0) @binding(1)
var cloud_sampler: sampler;
@group(0) @binding(2)
var<uniform> time: f32;
@group(0) @binding(3)
var<uniform> resolution: vec2<f32>;
fn hgPhase(g: f32, cosTheta: f32) -> f32 {
return (1.0 - g * g) / (4.0 * 3.14159 * pow(1.0 + g * g - 2.0 * g * cosTheta, 1.5));
}
fn raymarchCloud(ro: vec3<f32>, rd: vec3<f32>, maxDist: f32) -> f32 {
var density: f32 = 0.0;
var p = ro;
for (var i: i32 = 0; i < 64; i++) {
let d = textureSample(cloud_texture, cloud_sampler, p * 0.01 + vec3<f32>(time * 0.01)).r;
density = density + d * 0.01;
p = p + rd * 0.1;
if (length(p - ro) > maxDist) { break; }
}
return density;
}
@fragment
fn fs_main(input: FragmentInput) -> FragmentOutput {
var output: FragmentOutput;
var uv = (input.texcoord - vec2<f32>(0.5, 0.5)) * 2.0;
uv.x = uv.x * resolution.x / resolution.y;
let ro = vec3<f32>(0.0, 1.0, -5.0);
let rd = normalize(vec3<f32>(uv, 1.0));
let density = raymarchCloud(ro, rd, 20.0);
let col = vec3<f32>(density * 0.8, density * 0.85, density * 0.9);
output.color = vec4<f32>(col, density * 0.5);
return output;
}
差异点:
- WGSL 使用
texture_3d<f32>定义 3D 纹理 - WGSL 使用
textureSample()采样 3D 纹理 - WGSL 的常量需要显式类型标注
常见误区与陷阱
-
Raymarching 步进过小
- 陷阱:
EPSILON过小导致性能暴跌 - 解决:根据场景尺度调整,通常
0.001-0.01
- 陷阱:
-
SDF 梯度计算错误
- 陷阱:步长
e过大导致法线不准确 - 解决:使用
0.001或自适应步长
- 陷阱:步长
-
体积渲染采样不足
- 陷阱:采样次数过少导致云体断层
- 解决:增加采样数或使用重要性采样
-
球谐函数系数错误
- 陷阱:系数归一化错误导致光照异常
- 解决:使用预计算的系数或参考标准公式
-
软阴影 Bias 过大
- 陷阱:Shadow Acne 或悬浮阴影
- 解决:根据表面曲率动态调整 Bias
-
光照探针更新不及时
- 陷阱:探针未随场景变化更新
- 解决:使用增量更新或背景更新
-
DXR/Vulkan RT 兼容性
- 陷阱:WebGPU 不支持光线追踪
- 解决:使用 Raymarching 模拟光线追踪
-
SDF 布尔操作混淆
- 陷阱:
min和max使用错误 - 解决:明确并集、交集、差集的数学定义
- 陷阱:
延伸阅读与自测
权威资料
- Raymarching Distance Fields - Inigo Quilez SDF 教程
- Painting with Math: A Gentle Study of Raymarching - Raymarching 深度学习
- Real-Time Volumetric Clouds - 实时体积云论文
- Spherical Harmonics - 球谐函数实现
开源实现参考
- Shadertoy - 在线 Shader 平台,大量 Raymarching 示例
- Three.js Volumetric Clouds - Three.js 体积云实现
- WebGPU Samples - WebGPU 官方示例
自测题
-
思考题: 为什么 Raymarching 可以渲染无限细节的场景?它与 Ray Tracing 的根本区别是什么?
-
对比题: SDF vs 传统的三角形网格,在内存占用、渲染质量、程序化生成上的区别是什么?
-
实践题: 如何实现自适应 Raymarching,根据距离动态调整步长?
-
扩展题: 体积云如何实现动态光照?Phase Function 如何影响云的视觉外观?
-
优化题: 如何优化 SDF 的布尔操作?是否可以使用距离场渐变加速?
参考资料获取时间: 2026-07-06,通过 web-search-prime_web_search_prime 工具检索。