Tutorial

WebGL 2.0 光线步进渲染原理详解

深入解析 WebGL 2.0 光线步进(Raymarching)技术,了解如何使用 SDF 符号距离场实现实时体积渲染,以及这种技术如何压榨 GPU 的极限性能。

🚀 volumeshadertest
12月3日
10 分钟

深入探索 WebGL 2.0 光线步进、SDF 和实时体积渲染

WebGL 2.0 解锁了强大的 GPU 功能集,使传统上需要原生 API 的高级渲染技术成为可能。其中,光线步进(Raymarching) 作为最富有表现力和数学优雅的方法之一脱颖而出,可以用最少的几何体渲染复杂场景。

本文将通过 WebGL 2.0 光线步进的核心概念,解释**符号距离场(SDF)**如何实现高效渲染,并展示这种技术如何从 GPU 中榨取惊人的性能。


1. 什么是光线步进?

光线步进是一种渲染方法,它不是将几何体投影到屏幕上,而是从相机逐像素发射一条光线,逐步穿过虚拟场景,评估距离,直到光线击中表面或达到最大范围。

1.1 光线步进的工作原理(可视化图表)

相机
  |
  | 光线
  v
  +--------------------------------- 屏幕像素
  |
  | 步进 1      (距离 d1)
  |
  |---------> 步进 2      (距离 d2)
  |
  |-----------------------> 步进 3 ...
  |
当距离 < ε 时到达表面

光线步进本质上是一个循环:

for (int i = 0; i < MAX_STEPS; i++) {
    float dist = sdf(currentPos);
    if (dist < EPSILON) hitSurface();
    currentPos += rayDir * dist;
}

关键要素是我们传入的 SDF 函数。


2. 什么是符号距离场(SDF)?

符号距离场 是一个返回以下值的函数:

> 0  物体外部距离  
= 0  正好在表面上  
< 0  物体内部距离  

2.1 SDF 图表

           外部
        +------------+
        |   d = 0.5  |
        |            |
内部    |     SDF    | 表面 d = 0
d < 0   |            |
        |   d = -0.3 |
        +------------+

2.2 示例:球体 SDF

float sdSphere(vec3 p, float r) {
    return length(p) - r;
}

通过将这些基本形状与布尔运算(并集、差集、交集)结合,你可以在不需要任何传统几何体的情况下构建复杂场景。


3. 为什么在 WebGL 2.0 中使用 SDF 光线步进?

3.1 优势

  • 不需要顶点缓冲区或网格 所有内容都在片段着色器中通过数学计算。

  • 无限的几何细节 SDF 定义平滑连续的表面。

  • 动态、可变形对象 用数学轻松动画化形状。

  • 紧凑的场景 完整的 3D 场景可能只需要几行 GLSL 代码。

3.2 使这成为可能的 WebGL 2.0 特性

  • 高精度浮点数
  • 完整的 GLSL ES 3.0 着色器支持
  • UBO(统一缓冲区)
  • 浮点纹理
  • 更灵活的循环和分支

这使得在 WebGL 1.0 中难以实现的稳定、高性能光线步进成为可能。


4. 逐步构建 WebGL 2.0 光线步进器

4.1 场景光线设置

为每个像素计算一条光线:

vec3 rayOrigin = cameraPos;
vec3 rayDir = normalize(uv.x * camRight + uv.y * camUp + camForward);

4.2 光线步进循环

float marchRay(vec3 ro, vec3 rd) {
    float totalDist = 0.0;

    for (int i = 0; i < MAX_STEPS; i++) {
        vec3 p = ro + rd * totalDist;
        float d = map(p);     // 你的 SDF 函数

        if (d < EPSILON) break;    // 击中
        if (totalDist > MAX_DISTANCE) break;  // 未击中

        totalDist += d;
    }
    return totalDist;
}

4.3 定义 SDF 场景

float map(vec3 p) {
    float s = sdSphere(p, 1.0);
    float box = sdBox(p - vec3(2.0, 0.0, 0.0), vec3(0.7));
    return min(s, box); // 并集
}

4.4 法线计算

使用梯度近似:

vec3 calcNormal(vec3 p) {
    const 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)
    ));
}

4.5 光照模型

简单的漫反射着色:

float diffuse = max(dot(normal, lightDir), 0.0);

5. 渲染体积(雾、云、烟雾)

SDF 光线步进可以扩展到体积渲染,其中每一步都贡献颜色和密度,而不是在表面停止。

5.1 体积图表

光线 →
[低密度] [中密度] [高密度] [不透明]
   +--------+---------+---------+-------+
   采样1    采样2     采样3     采样4

5.2 体积累积模型

vec3 color = vec3(0);
float absorption = 1.0;

for (int i = 0; i < STEPS; i++) {
    float density = computeDensity(p);
    vec3 sampleColor = density * vec3(0.6, 0.7, 1.0);

    color += sampleColor * absorption;
    absorption *= (1.0 - density * 0.1);

    p += rd * STEP_SIZE;
}

光线步进体积渲染更加昂贵,但 WebGL 2.0 的性能改进(特别是在移动 GPU 上)使实时云、雾和星云成为可能。


6. 性能优化技术

光线步进可能很重,因此优化至关重要。

6.1 关键技术

  1. 距离剔除 通过依赖准确的 SDF 距离跳过大的空白区域。

  2. 包围体积 测试光线是否进入存在 SDF 的区域。

  3. 自适应步长 在表面附近使用较小的步长,在开放空间中使用较大的步长。

  4. 提前退出 一旦击中表面就停止步进。

  5. 减少 MAX_STEPS 实时渲染的典型值范围是 64–128。

  6. 高效使用 WebGL 2.0 统一变量和缓冲区 将不变的值移出循环。


7. WebGL 2.0 项目的代码结构

目录布局示例:

shader/
  raymarch.vert
  raymarch.frag
src/
  webgl2-context.js
  camera.js
  renderer.js
index.html

典型的片段着色器部分:

// 1. 相机/光线设置
// 2. SDF 定义
// 3. 材质和光照函数
// 4. 光线步进循环
// 5. 最终颜色输出

8. 何时应该使用光线步进?

光线步进在以下情况下表现出色:

  • 无限平滑形状
  • 分形
  • 科幻场景
  • 符号距离软阴影
  • 程序化几何
  • 体积效果(云、雾、上帝之光)

在以下情况下应避免使用光线步进:

  • 许多详细的复杂网格
  • 逼真的蒙皮角色
  • 包含数千个对象的大型动态场景

光线步进最适合程序化世界风格化视觉效果


9. 结论

WebGL 2.0 光线步进仅使用数学和 GLSL 就将高级实时渲染带入浏览器。使用 SDF,你可以在不需要单个顶点缓冲区的情况下创建复杂的 3D 场景。尽管该技术在计算上很密集,但仔细的优化允许创建可与原生 GPU 应用程序相媲美的富有表现力的视觉效果。

光线步进代表了数学、艺术和 GPU 编程的独特交叉点,使其成为现代 WebGL 开发中最令人兴奋的技术之一。


这一切跟 Volume Shader BM Test 有什么关系

读完原理,回到工具本身。下面是把上面的理论对回到 Volume Shader BM Test 上的几个关键点。

为什么这个测试比普通 3D Demo 更狠

普通 WebGL Demo 画几千个多边形就够了。这个测试要求 GPU 对每一个像素跑一遍光线步进循环,通常每像素 1000+ 次迭代,且没有几何信息可以提前剔除。游戏里能跑满 60 FPS 的中端显卡,在 Heavy 模式下掉到 30 FPS 以下是常态。

为什么手机上 FPS 会掉

手机 SoC 一升温就会主动降频。前 10 秒看着很漂亮,之后 GPU 频率会被压下来。建议接上充电,先跑 Light 或 Medium,比较手机的时候看稳定性而不是峰值 FPS。详见手机 GPU 测试指南

为什么第一次跑会慢一点(Shader 编译)

页面第一次加载时浏览器要把 GLSL 编译成驱动可执行的二进制,这一步在某些驱动上要花几百毫秒,表现为开头的一次卡顿。丢掉第一次结果,看第二次。

怎样公平地比较结果

同一模式、浏览器标签在前台、同一电源状态。不同模式(Light / Medium / Heavy)的结果不能直接比,shader 负载不同。参考 FPS 结果解读

如果浏览器在用集显怎么办

带独显的笔记本上,Chrome / Edge 经常默认走集显。打开 chrome://gpu,看 GL_RENDERER 那一行。如果显示的是集显,去操作系统的图形设置里强制浏览器走独显。卡顿排查指南 里有详细步骤。


跑一次试试

理论看完了,直接跑一次基准测试,不到一分钟。

开始 Volume Shader BM Test →

相关指南

标签
WebGL光线步进体积渲染GPU性能