返回知识库

渲染

高级渲染与 Raymarching

高级渲染与 Raymarching 封面
WebGPU/WebGLShaderGLSL/WGSLRaymarchingSDF体积渲染光线追踪

直觉问题:为什么 Shader 能渲染出无限细节的场景?

Q1: 一个简单的全屏 Quad 如何渲染出复杂的 3D 场景?

Q2: 为什么电影中的云雾如此真实?GPU 如何模拟体积光照?


核心概念白话讲

SDF(Signed Distance Field,有向距离场)

定义:空间中任意点到物体表面的最短距离

SDF(p)={d,p 在物体外d,p 在物体内\text{SDF}(p) = \begin{cases} d, & p \text{ 在物体外} \\ -d, & p \text{ 在物体内} \end{cases}

基本 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)

体积渲染

原理:光线穿过体积,累加光照贡献

光传输方程

Lout=LineσdΔt+σsLscatter(1eσdΔt)L_{out} = L_{in} \cdot e^{-\sigma_d \cdot \Delta t} + \sigma_s \cdot L_{scatter} \cdot (1 - e^{-\sigma_d \cdot \Delta t})

体素化云

  1. 3D 纹理:存储密度、颜色
  2. Raymarching:沿光线采样 3D 纹理
  3. 光照计算: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 数学推导

光线方程

p(t)=origin+tdirectionp(t) = \text{origin} + t \cdot \text{direction}

步进过程

pi+1=pi+directionSDF(pi)p_{i+1} = p_i + \text{direction} \cdot \text{SDF}(p_i)

终止条件

SDF(p)<ϵt>maxDistance\text{SDF}(p) < \epsilon \quad \text{或} \quad t > \text{maxDistance}

法线计算

数值梯度

n=[SDFxSDFySDFz]n = \begin{bmatrix} \frac{\partial \text{SDF}}{\partial x} \\ \frac{\partial \text{SDF}}{\partial y} \\ \frac{\partial \text{SDF}}{\partial z} \end{bmatrix}

有限差分近似

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

P(θ)=1g24π(1+g22gcosθ)3/2P(\theta) = \frac{1 - g^2}{4\pi (1 + g^2 - 2g \cos \theta)^{3/2}}

参数

  • g = 0:各向同性散射
  • g > 0:前向散射(云)
  • g < 0:后向散射(烟雾)

光照计算

Blinn-Phong 光照

L=kdmax(0,nl)+ksmax(0,nh)shininessL = k_d \cdot \max(0, n \cdot l) + k_s \cdot \max(0, n \cdot h)^{shininess}

软阴影

shadow=min(1.0,kdheight)\text{shadow} = \min(1.0, k \cdot \frac{d}{\text{height}})

球谐函数(SH)

2 阶球谐系数

SH0=0.282095SH_0 = 0.282095 SH1=0.488603ySH_1 = 0.488603 \cdot y SH2=0.488603zSH_2 = 0.488603 \cdot z SH3=0.488603xSH_3 = 0.488603 \cdot x

光照重建

L=i=08ciSHiL = \sum_{i=0}^{8} c_i \cdot SH_i

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 的常量需要显式类型标注

常见误区与陷阱

  1. Raymarching 步进过小

    • 陷阱:EPSILON 过小导致性能暴跌
    • 解决:根据场景尺度调整,通常 0.001-0.01
  2. SDF 梯度计算错误

    • 陷阱:步长 e 过大导致法线不准确
    • 解决:使用 0.001 或自适应步长
  3. 体积渲染采样不足

    • 陷阱:采样次数过少导致云体断层
    • 解决:增加采样数或使用重要性采样
  4. 球谐函数系数错误

    • 陷阱:系数归一化错误导致光照异常
    • 解决:使用预计算的系数或参考标准公式
  5. 软阴影 Bias 过大

    • 陷阱:Shadow Acne 或悬浮阴影
    • 解决:根据表面曲率动态调整 Bias
  6. 光照探针更新不及时

    • 陷阱:探针未随场景变化更新
    • 解决:使用增量更新或背景更新
  7. DXR/Vulkan RT 兼容性

    • 陷阱:WebGPU 不支持光线追踪
    • 解决:使用 Raymarching 模拟光线追踪
  8. SDF 布尔操作混淆

    • 陷阱:minmax 使用错误
    • 解决:明确并集、交集、差集的数学定义

延伸阅读与自测

权威资料

开源实现参考

自测题

  1. 思考题: 为什么 Raymarching 可以渲染无限细节的场景?它与 Ray Tracing 的根本区别是什么?

  2. 对比题: SDF vs 传统的三角形网格,在内存占用、渲染质量、程序化生成上的区别是什么?

  3. 实践题: 如何实现自适应 Raymarching,根据距离动态调整步长?

  4. 扩展题: 体积云如何实现动态光照?Phase Function 如何影响云的视觉外观?

  5. 优化题: 如何优化 SDF 的布尔操作?是否可以使用距离场渐变加速?


参考资料获取时间: 2026-07-06,通过 web-search-prime_web_search_prime 工具检索。