WEBGL Shader零基础教程(十四):光线追踪技术之路径追踪

  • 时间:2025-10-12 05:14 作者: 来源: 阅读:0
  • 扫一扫,手机访问
摘要:目录前言第一章 路径追踪基础认知:从 “规则追踪” 到 “随机采样”1.1 什么是路径追踪?1.2 传统光线追踪 vs 路径追踪:核心差异1.3 路径追踪的核心应用场景第二章 路径追踪核心原理:四步实现随机光线模拟2.1 核心流程拆解2.2 步骤 1:路径生成 —— 从相机到像素的初始光线2.2.1 核心数学逻辑(与传统光线追踪一致)2.3 步骤 2:随机采样 —— 光线反弹的方向选择2.3.1 核心数学工具:随机数生成2.3.2 基于

目录

前言

第一章 路径追踪基础认知:从 “规则追踪” 到 “随机采样”

1.1 什么是路径追踪?

1.2 传统光线追踪 vs 路径追踪:核心差异

1.3 路径追踪的核心应用场景

第二章 路径追踪核心原理:四步实现随机光线模拟

2.1 核心流程拆解

2.2 步骤 1:路径生成 —— 从相机到像素的初始光线

2.2.1 核心数学逻辑(与传统光线追踪一致)

2.3 步骤 2:随机采样 —— 光线反弹的方向选择

2.3.1 核心数学工具:随机数生成

2.3.2 基于材质的随机方向采样

2.4 步骤 3:辐射度积分 —— 计算路径的光照贡献

2.4.1 简化积分计算(零基础友好)

2.5 步骤 4:路径终止 —— 避免无限反弹的 “俄罗斯轮盘赌”

2.5.1 核心逻辑

2.5.2 Shader 实现

2.5.3 完整路径循环

第三章 WEBGL 实现路径追踪的限制与优化

3.1 WEBGL 的核心限制与解决方案

3.2 关键优化技巧(确保可运行帧率)

3.2.1 1. 减少采样与反弹次数

3.2.2 2. 优化随机采样质量

3.2.3 3. 降低计算复杂度

3.2.4 4. 后期处理降噪

第四章 实战案例:WEBGL Shader 实现路径追踪

4.1 案例 1:基础漫反射路径追踪(全局光照实现)

4.1.1 完整代码实现

4.1.2 运行效果与解析

4.2 案例 2:带软阴影与焦散的路径追踪(复杂光学效果)

4.2.1 完整代码实现(基于案例 1 扩展)

4.2.2 运行效果与解析

第五章 常见问题排查与进阶优化

5.1 常见问题排查

5.1.1 问题 1:画面噪点严重(无法接受)

5.1.2 问题 2:焦散效果不明显或无焦散

5.1.3 问题 3:性能卡顿(帧率<1fps)

5.2 进阶优化技巧(突破 WEBGL 性能瓶颈)

5.2.1 1. 重要性采样(减少采样数)

5.2.2 2. 空间划分加速(BVH 树)

5.2.3 3. 利用 WebGL 2.0 新特性



前言

在光线追踪技术体系中,“路径追踪(Path Tracing)” 是突破 “传统光线追踪局限”、实现 “影视级高真实感渲染” 的核心技术 —— 传统光线追踪(如 Whitted 风格)仅能模拟 “镜面反射、规则折射” 等理想光学效果,难以还原 “漫反射间接光照(如红色墙壁照亮白色桌面)、软阴影(如窗户投射的渐变阴影)、焦散(如阳光穿过水杯在地面形成的光斑)” 等复杂物理现象;而路径追踪通过 “蒙特卡洛随机采样” 与 “全局路径积分”,从根本上还原光线在场景中的随机传播规律,让渲染效果无限贴近真实世界。

对零基础开发者而言,路径追踪的难点并非语法(已在 GLSL ES 与实时光线追踪教程中覆盖),而是 “蒙特卡洛采样的数学逻辑”“路径生成与终止的策略” 以及 “WEBGL 环境下的性能平衡”。本文作为 WEBGL Shader 系列的第十五篇,将从 “基础认知→核心原理→WEBGL 实现→实战案例” 逐步拆解:先明确路径追踪的定义与价值,再深入讲解 “路径生成、随机采样、辐射度积分、路径终止” 四大核心步骤,最后通过两个可直接运行的 Three.js 案例(基础漫反射路径追踪、带软阴影与焦散的路径追踪),帮你掌握 “从随机光线到真实渲染” 的完整流程。

所有案例聚焦 “零基础友好”,跳过复杂的积分推导与硬件优化,聚焦 Shader 中 “路径追踪的核心逻辑”,确保你能边学边改,直观感受 “路径追踪对真实感的颠覆性提升”。掌握本文内容后,你将摆脱 “传统光线追踪的真实感天花板”,迈入影视级渲染的核心领域。

第一章 路径追踪基础认知:从 “规则追踪” 到 “随机采样”

在深入技术细节前,需先明确路径追踪的核心定位 —— 它是 “光线追踪的进阶形态”,本质是通过 “随机采样 + 积分计算” 解决传统光线追踪无法覆盖的 “全局光照与复杂光学效果” 问题。

1.1 什么是路径追踪?

路径追踪是指基于 “蒙特卡洛方法”,通过 “逆向生成随机光线路径”(从相机出发,每次反弹随机选择光线方向,最终连接到光源),计算每条路径的辐射度贡献,再通过积分逼近真实光照颜色的渲染技术。其核心逻辑源于 “辐射度传输理论”:物体表面某点的颜色,是所有能到达该点的光线路径的辐射度总和。

类比现实世界:阳光穿过窗户(随机折射)→照亮红色墙壁(漫反射随机方向)→反射光照亮白色桌面(漫反射)→最终光线进入人眼 —— 路径追踪会模拟这条 “随机路径”,并计算每个环节的光照贡献,最终得到桌面的颜色。

1.2 传统光线追踪 vs 路径追踪:核心差异

二者虽同属 “光线追踪” 体系,但在 “路径生成、光照覆盖、真实感、性能” 上存在本质区别,是零基础用户最易混淆的点,具体对比如下:


对比维度传统光线追踪(Whitted 风格)路径追踪(Path Tracing)
路径生成规则路径(镜面反射 / 折射方向固定,漫反射直接终止)随机路径(每次反弹随机选择方向,支持漫反射间接光照)
光照覆盖仅支持直接光照 + 理想间接光照(镜面 / 折射)天然支持全局光照(直接 + 多次漫反射 / 折射间接光照)
阴影效果硬阴影(光源视为点光源,无渐变)软阴影(光源视为区域光源,随机采样生成渐变阴影)
复杂光学效果不支持焦散、颜色 bleeding(颜色渗透)支持焦散(透明物体折射形成)、颜色 bleeding
真实感中(接近真实但缺乏细节)高(影视级真实感,可还原多数物理现象)
性能消耗中(与反弹次数正相关)高(与采样数、路径长度正相关,需降噪)
核心依赖几何相交检测(规则计算)蒙特卡洛采样(随机计算 + 积分逼近)
适用场景实时高真实感场景(如高端游戏)影视渲染、静态高真实感场景(如建筑可视化)


关键结论:传统光线追踪是 “实时优先” 的简化方案,路径追踪是 “真实感优先” 的精确方案;实际项目中常 “混合使用”(如实时场景用传统光线追踪,离线渲染用路径追踪),或通过 “降噪技术”(如 DLSS、AI 降噪)降低路径追踪的实时性能消耗。

1.3 路径追踪的核心应用场景

路径追踪虽性能消耗高,但在 “高真实感需求” 场景中不可替代,主要应用于以下领域:

影视动画:如《寻梦环游记》《疯狂动物城》,用路径追踪实现 “角色皮肤的次表面散射、场景的全局光照、透明材质的焦散”;建筑可视化:模拟 “不同时间段阳光穿过建筑的光照变化”“室内灯光的全局反射”,帮助设计师预览真实居住效果;产品设计:如汽车、珠宝设计,模拟 “产品在不同光照环境下的材质质感”(如金属的拉丝反射、宝石的折射焦散);虚拟制片(Virtual Production):实时生成 “电影级真实感的虚拟场景”,演员可与虚拟场景实时互动,减少后期合成工作量。

第二章 路径追踪核心原理:四步实现随机光线模拟

路径追踪的核心逻辑可拆解为 “路径生成→随机采样→辐射度积分→路径终止” 四步,每一步都需结合 “蒙特卡洛方法” 与 “光学物理规律”,是零基础用户必须掌握的核心内容。

2.1 核心流程拆解

路径追踪的完整流程(逆向追踪,从相机到光源)如下,每一步都需在 Shader 中实现:

输入:相机参数、像素坐标

处理:基于材质类型(漫反射/镜面/透明)生成随机方向

处理:累加每条路径的辐射度,平均后得到像素颜色

输出:多条路径的平均颜色

渲染结果:带全局光照、软阴影、焦散的真实场景

2.2 步骤 1:路径生成 —— 从相机到像素的初始光线

路径生成是路径追踪的起点,与传统光线追踪的 “光线生成” 逻辑一致,核心是 “为屏幕每个像素生成一条从相机出发的初始光线”,需明确光线的起点(origin) 和方向(direction)

2.2.1 核心数学逻辑(与传统光线追踪一致)

像素坐标转 NDC 坐标:将屏幕像素坐标(x∈[0, width],y∈[0, height])转换为 “标准化设备坐标(NDC)”(x∈[-1,1],y∈[-1,1]),公式如下:

glsl

vec2 ndc = 2.0 * vec2(gl_FragCoord.xy / uResolution) - 1.0;
ndc.y *= -1.0; // 翻转Y轴(屏幕坐标系Y向下,NDC坐标系Y向上)

其中uResolution是屏幕分辨率(如vec2(width, height))。

NDC 坐标转世界空间光线方向:通过 “相机的投影矩阵逆” 和 “视图矩阵逆”,将 NDC 坐标对应的 “相机近平面点” 转换为世界空间点,再与相机位置计算光线方向:

glsl

// 相机参数(CPU传递的uniform)
uniform vec3 uCameraPos;       // 相机世界空间位置
uniform mat4 uProjectionInverse; // 投影矩阵逆
uniform mat4 uViewInverse;       // 视图矩阵逆
 
// 步骤1:NDC坐标转世界空间点(近平面)
vec4 clipPos = vec4(ndc.x, ndc.y, -1.0, 1.0); // 近平面Z=-1
vec4 viewPos = uProjectionInverse * clipPos;   // 视图空间点
viewPos /= viewPos.w; // 透视除法
vec4 worldPos = uViewInverse * viewPos;       // 世界空间点
 
// 步骤2:计算初始光线方向(归一化)
vec3 rayDir = normalize(worldPos.xyz - uCameraPos);
Ray initialRay = Ray(uCameraPos, rayDir, 0.001, 1000.0); // tMin=0.001(避免自相交)

2.3 步骤 2:随机采样 —— 光线反弹的方向选择

随机采样是路径追踪与传统光线追踪的 “核心差异”,传统光线追踪的反弹方向固定(如镜面反射方向由reflect()函数确定),而路径追踪需 “基于材质类型随机选择方向”,以模拟真实世界中光线的随机传播(如漫反射表面的光线向各个方向散射)。

2.3.1 核心数学工具:随机数生成

路径追踪依赖 “高质量随机数” 生成采样方向,WEBGL Shader 中无原生随机函数,需通过 “哈希函数” 实现伪随机数:

glsl

// 2D伪随机函数(输入UV或坐标,输出[0,1)的随机值)
float random(vec2 uv) {
    return fract(sin(dot(uv, vec2(12.9898, 78.233))) * 43758.5453);
}
 
// 生成[0,1)的随机向量(用于方向采样)
vec3 randomVec3(vec2 uv) {
    float x = random(uv);
    float y = random(uv + vec2(0.1, 0.1));
    float z = random(uv + vec2(0.2, 0.2));
    return vec3(x, y, z);
}
2.3.2 基于材质的随机方向采样

不同材质的光线传播规律不同,需针对性设计采样策略:

漫反射材质(如墙面、纸张):光线与物体相交后,向 “半球面内随机方向” 散射,符合 “朗伯余弦定律”;

glsl

// 漫反射方向采样:在表面法线的半球面内生成随机方向
vec3 sampleDiffuseDirection(vec3 normal, vec2 uv) {
    // 生成单位球内的随机点(拒绝采样法)
    vec3 randomPoint;
    do {
        randomPoint = 2.0 * randomVec3(uv) - 1.0; // [-1,1]随机向量
    } while (dot(randomPoint, randomPoint) > 1.0); // 确保在单位球内
    randomPoint = normalize(randomPoint);
    // 确保方向在法线的半球面内(与法线同向)
    return dot(randomPoint, normal) > 0.0 ? randomPoint : -randomPoint;
}

镜面反射材质(如镜子、金属):光线主要沿 “镜面反射方向” 传播,加入少量随机偏移模拟 “磨砂金属” 效果;

glsl

// 镜面反射方向采样:镜面方向+随机偏移
vec3 sampleSpecularDirection(vec3 normal, vec3 incidentDir, vec2 uv, float roughness) {
    // 基础镜面反射方向
    vec3 specularDir = reflect(incidentDir, normal);
    // 加入随机偏移(粗糙度越大,偏移越明显)
    vec3 randomOffset = roughness * (2.0 * randomVec3(uv) - 1.0);
    specularDir = normalize(specularDir + randomOffset);
    // 确保方向在法线的半球面内
    return dot(specularDir, normal) > 0.0 ? specularDir : reflect(specularDir, normal);
}

透明材质(如玻璃、水):光线同时发生 “反射” 与 “折射”,需随机选择其中一种(基于菲涅尔效应),折射方向由 “斯涅尔定律” 确定;

glsl

// 透明材质方向采样:随机选择反射或折射
vec3 sampleTransparentDirection(vec3 normal, vec3 incidentDir, vec2 uv, float refractiveIndex, out bool isReflect) {
    // 菲涅尔效应:计算反射概率(入射角越大,反射概率越高)
    float cosTheta = clamp(dot(-incidentDir, normal), 0.0, 1.0);
    float fresnel = pow(1.0 - cosTheta, 5.0);
    float reflectProb = 0.1 + 0.9 * fresnel; // 基础反射概率0.1,叠加菲涅尔
 
    // 随机决定反射或折射
    isReflect = random(uv) < reflectProb;
    if (isReflect) {
        // 反射方向
        return reflect(incidentDir, normal);
    } else {
        // 折射方向(斯涅尔定律)
        float eta = dot(-incidentDir, normal) > 0.0 ? 1.0 / refractiveIndex : refractiveIndex; // 空气→玻璃或玻璃→空气
        vec3 refractedDir = refract(incidentDir, normal, eta);
        return refractedDir;
    }
}

2.4 步骤 3:辐射度积分 —— 计算路径的光照贡献

辐射度积分是路径追踪的 “核心数学逻辑”,核心是 “计算每条光线路径对像素颜色的贡献”,公式源于 “辐射度传输方程”:

其中:

:物体表面点沿出射方向的辐射度(颜色);:点的自发光辐射度(如光源);:双向反射分布函数(BRDF,描述材质的反射特性);:点沿入射方向的入射辐射度(来自其他物体或光源);:入射方向与表面法线的夹角余弦(漫反射衰减因子)。

2.4.1 简化积分计算(零基础友好)

对零基础用户,无需深入积分推导,只需掌握 “简化版积分逻辑”:每条路径的光照贡献 = 材质颜色 × 光源辐射度 × 方向衰减因子 × 路径权重,具体实现如下:

glsl

// 辐射度积分:计算当前路径段的光照贡献
vec3 calculateRadiance(HitResult hit, Ray ray, vec2 uv, out Ray nextRay) {
    vec3 radiance = vec3(0.0);
    vec3 normal = hit.normal;
    vec3 hitPos = hit.pos;
    vec3 incidentDir = -ray.direction; // 入射方向(从外部指向交点)
 
    // 1. 若击中光源,直接返回光源辐射度
    if (hit.sphere.isLight) {
        return hit.sphere.emission; // 光源自发光颜色
    }
 
    // 2. 基于材质类型采样下一个方向
    vec3 nextDir;
    bool isReflect = false;
    if (hit.sphere.type == DIFFUSE) {
        nextDir = sampleDiffuseDirection(normal, uv);
    } else if (hit.sphere.type == SPECULAR) {
        nextDir = sampleSpecularDirection(normal, incidentDir, uv, hit.sphere.roughness);
    } else if (hit.sphere.type == TRANSPARENT) {
        nextDir = sampleTransparentDirection(normal, incidentDir, uv, hit.sphere.refractiveIndex, isReflect);
    }
 
    // 3. 生成下一条光线(避免自相交)
    vec3 nextOrigin = hitPos + nextDir * 0.001; // 起点偏移
    nextRay = Ray(nextOrigin, nextDir, 0.001, ray.tMax);
 
    // 4. 计算当前路径段的贡献(简化版积分)
    float cosTheta = clamp(dot(nextDir, normal), 0.0, 1.0); // 方向衰减因子
    if (hit.sphere.type == DIFFUSE) {
        // 漫反射贡献:材质颜色 × cosTheta(朗伯定律)
        radiance = hit.sphere.albedo * cosTheta / PI; // PI=3.14159,归一化因子
    } else if (hit.sphere.type == SPECULAR) {
        // 镜面反射贡献:材质颜色 × 镜面强度
        radiance = hit.sphere.albedo * hit.sphere.specularStrength;
    } else if (hit.sphere.type == TRANSPARENT) {
        // 透明材质贡献:折射时颜色衰减小,反射时同镜面
        radiance = isReflect ? hit.sphere.albedo * 0.8 : hit.sphere.albedo * 0.95;
    }
 
    return radiance;
}

2.5 步骤 4:路径终止 —— 避免无限反弹的 “俄罗斯轮盘赌”

路径追踪的光线若无限反弹,会导致计算量无穷大,需通过 “俄罗斯轮盘赌(Russian Roulette)” 策略 “随机终止路径”,同时保证积分结果的无偏性(即平均颜色仍接近真实值)。

2.5.1 核心逻辑

设置终止概率:根据路径长度(反弹次数)设置 “终止概率”,反弹次数越多,终止概率越高(如第 1 次反弹终止概率 0.2,第 5 次反弹终止概率 0.8);随机决定是否终止:生成 [0,1) 的随机数,若小于终止概率,路径终止;否则,继续反弹,并将当前路径贡献乘以 “1/(1 - 终止概率)”(补偿终止概率的影响,确保无偏)。

2.5.2 Shader 实现

glsl

// 俄罗斯轮盘赌:决定路径是否终止
bool russianRoulette(int bounceCount, vec2 uv, out float weight) {
    // 反弹次数越多,终止概率越高
    float terminateProb;
    if (bounceCount == 0) terminateProb = 0.2; // 第1次反弹:20%终止概率
    else if (bounceCount == 1) terminateProb = 0.3;
    else if (bounceCount == 2) terminateProb = 0.4;
    else terminateProb = 0.8; // 第4次及以上:80%终止概率
 
    // 随机决定是否终止
    if (random(uv) < terminateProb) {
        weight = 0.0;
        return true; // 终止路径
    } else {
        weight = 1.0 / (1.0 - terminateProb); // 补偿权重,确保无偏
        return false; // 继续反弹
    }
}
2.5.3 完整路径循环

结合 “路径生成→随机采样→辐射度积分→路径终止”,实现完整的路径循环:

glsl

#define MAX_BOUNCES 5 // 最大反弹次数(避免极端情况)
 
vec3 tracePath(Ray initialRay, vec2 uv) {
    vec3 totalRadiance = vec3(0.0); // 总辐射度(像素颜色)
    vec3 pathWeight = vec3(1.0);    // 路径权重(累积贡献)
    Ray currentRay = initialRay;
 
    for (int bounce = 0; bounce < MAX_BOUNCES; bounce++) {
        // 步骤1:光线与场景相交检测
        HitResult hit = raySceneIntersect(currentRay);
        if (!hit.hit) {
            // 未击中任何物体,添加背景辐射度(如天空)
            totalRadiance += pathWeight * vec3(0.2, 0.3, 0.5); // 蓝色天空
            break;
        }
 
        // 步骤2:计算当前路径段的辐射度贡献
        Ray nextRay;
        vec3 radiance = calculateRadiance(hit, currentRay, uv + vec2(float(bounce), 0.0), nextRay);
        totalRadiance += pathWeight * radiance;
 
        // 步骤3:俄罗斯轮盘赌决定是否继续
        float weight;
        if (russianRoulette(bounce, uv + vec2(0.0, float(bounce)), weight)) {
            break; // 终止路径
        }
        pathWeight *= radiance * weight; // 更新路径权重
 
        // 步骤4:更新当前光线,准备下一次反弹
        currentRay = nextRay;
    }
 
    return totalRadiance;
}

第三章 WEBGL 实现路径追踪的限制与优化

WEBGL(尤其是 WebGL 1.0)对路径追踪的支持存在诸多限制(如性能弱、无原生随机函数),需针对性优化才能实现 “可接受的实时帧率”,本节讲解 WEBGL 环境下的核心限制与优化技巧。

3.1 WEBGL 的核心限制与解决方案

限制类型具体问题解决方案
性能弱WEBGL 运行在浏览器,GPU 并行计算能力有限,路径追踪的 “随机采样 + 多次反弹” 易导致帧率<5fps1. 降低采样数(每像素 1~8 次采样);2. 降低渲染分辨率(0.5 倍屏幕分辨率);3. 限制最大反弹次数(3~5 次)
无原生随机函数GLSL ES 无random()函数,伪随机数质量低易导致画面噪点严重1. 用 “纹理采样” 替代哈希函数(如加载噪声纹理,采样获取随机值);2. 结合像素坐标与时间生成随机种子,提升随机性
浮点数精度限制GLSL ES 的mediump精度(16 位)易导致积分计算误差,出现 “颜色断层”1. 关键计算(如辐射度积分、方向采样)用highp精度;2. 避免过小的权重值(如<0.001),减少精度损失
无循环展开优化WEBGL 对for循环的优化差,循环次数越多性能下降越明显1. 手动展开循环(如将MAX_BOUNCES=3的循环拆分为 3 个独立步骤);2. 用 “条件判断” 替代循环,减少分支开销

3.2 关键优化技巧(确保可运行帧率)

3.2.1 1. 减少采样与反弹次数

每像素低采样:路径追踪的 “噪点” 随采样数增加而减少,但采样数翻倍会导致性能减半,WEBGL 环境下建议每像素 1~4 次采样(可通过 “时间累积采样” 优化:每帧叠加 1 次采样,多帧后噪点减少);

glsl

// 时间累积采样:结合时间戳生成不同随机种子,每帧叠加1次采样
uniform float uTime; // 时间戳(CPU传递,每帧递增)
vec3 accumulateSamples(Ray initialRay, vec2 uv) {
    vec3 totalColor = vec3(0.0);
    const int sampleCount = 4; // 每像素4次采样
    for (int i = 0; i < sampleCount; i++) {
        // 生成不同随机种子(结合UV、采样索引、时间)
        vec2 sampleUV = uv + vec2(float(i) * 0.1, uTime * 0.01);
        totalColor += tracePath(initialRay, sampleUV);
    }
    return totalColor / float(sampleCount); // 平均采样结果,减少噪点
}

限制反弹次数:最大反弹次数设为 3~4 次,超过 4 次后间接光照贡献微弱,但性能消耗翻倍(多数场景中,3 次反弹已能覆盖 90% 的全局光照效果)。

3.2.2 2. 优化随机采样质量

噪声纹理采样:加载一张 “蓝噪声纹理”(Blue Noise Texture),通过采样纹理获取高质量随机值,减少噪点(蓝噪声比伪随机数的噪点分布更均匀,后期降噪更易);

glsl

uniform sampler2D uNoiseTexture; // 蓝噪声纹理(256x256)
vec3 randomVec3FromTexture(vec2 uv) {
    // 采样噪声纹理(RGB通道存储随机值)
    vec3 noise = texture2D(uNoiseTexture, uv * 8.0).rgb; // 8.0是纹理重复次数
    return 2.0 * noise - 1.0; // 转换为[-1,1]
}

分层采样:将像素划分为 4x4 的子像素,每个子像素生成 1 次采样,避免采样点集中导致的局部噪点(如uv = uv + vec2(i/4.0, j/4.0) / uResolution,i,j∈[0,3])。

3.2.3 3. 降低计算复杂度

简化材质模型:WEBGL 环境下仅支持 “漫反射、镜面反射、透明” 三种基础材质,避免复杂的 PBR 材质(如次表面散射、金属粗糙度模型);简化相交检测:场景中仅使用 “球体、平面” 等简单几何体,避免三角形网格(三角形相交检测公式复杂,性能消耗高);预计算常量:将 “PI、光源位置、材质参数” 等常量在 CPU 端预计算,传递给 Shader,避免 Shader 中反复计算(如const float PI = 3.1415926535;)。

3.2.4 4. 后期处理降噪

路径追踪的 “噪点” 是无法完全避免的,需通过 “后期处理” 降低噪点影响:

快速近似抗锯齿(FXAA):对画面中的高频噪声(如边缘锯齿)进行模糊,掩盖局部噪点;双边滤波(Bilateral Filter):在模糊的同时保留边缘细节,避免 “模糊过度导致画面发虚”;时间域降噪(Temporal Denoising):利用前几帧的渲染结果,与当前帧融合,减少单帧噪点(需存储前几帧的颜色与深度数据)。

第四章 实战案例:WEBGL Shader 实现路径追踪

本节通过两个递进的实战案例,覆盖路径追踪的核心应用场景 ——“基础漫反射路径追踪”(掌握核心流程)与 “带软阴影与焦散的路径追踪”(实现复杂光学效果)。案例基于 Three.js 的ShaderMaterial实现,代码完整可复制,注释详细,零基础用户可直接运行。

4.1 案例 1:基础漫反射路径追踪(全局光照实现)

需求:创建一个包含 “红色漫反射球体、白色漫反射球体、黄色光源球体” 的场景,实现路径追踪的核心流程,模拟 “红色球体照亮白色球体” 的漫反射间接光照(全局光照),对比传统光线追踪与路径追踪的差异。

4.1.1 完整代码实现

html

预览

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>路径追踪实战1:基础漫反射全局光照</title>
    <script src="https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.158.0/examples/jsm/controls/OrbitControls.js"></script>
    <style>body { margin: 0; overflow: hidden; }</style>
</head>
<body>
    <script>
        // 1. 基础环境搭建(Three.js仅用于画布与相机,核心渲染由Shader完成)
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        const renderer = new THREE.WebGLRenderer({ antialias: false });
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
        camera.position.set(0, 0, 15); // 相机位置:Z轴正方向
 
        // 2. 控制器(调整相机视角)
        const controls = new THREE.OrbitControls(camera, renderer.domElement);
        controls.enableDamping = true;
        controls.target.set(0, 0, 0);
 
        // 3. 加载蓝噪声纹理(优化随机采样)
        const noiseTexture = new THREE.TextureLoader().load('https://threejs.org/examples/textures/noise.png', (tex) => {
            tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
            tex.repeat.set(16, 16); // 纹理重复次数
        });
 
        // 4. 路径追踪ShaderMaterial(核心)
        const pathTracingMaterial = new THREE.ShaderMaterial({
            vertexShader: `
                // 顶点Shader:仅传递UV,全屏四边形渲染
                varying vec2 vUv;
                void main() {
                    vUv = uv;
                    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
                }
            `,
            fragmentShader: `
                precision highp float;
                #define MAX_BOUNCES 3 // 最大反弹次数
                #define SAMPLE_COUNT 4 // 每像素采样次数
                #define PI 3.141592653589793
 
                // -------------------------- 结构体定义 --------------------------
                // 光线结构体
                struct Ray {
                    vec3 origin;
                    vec3 direction;
                    float tMin;
                    float tMax;
                };
 
                // 球体结构体(支持漫反射材质与光源)
                struct Sphere {
                    vec3 center;
                    float radius;
                    vec3 albedo;    // 基础色(漫反射)
                    bool isLight;   // 是否为光源
                    vec3 emission;  // 光源自发光颜色
                };
 
                // 相交结果结构体
                struct HitResult {
                    bool hit;
                    float t;
                    vec3 pos;
                    vec3 normal;
                    Sphere sphere;
                };
 
                // -------------------------- Uniform参数(CPU传递) --------------------------
                uniform vec2 uResolution;   // 屏幕分辨率
                uniform vec3 uCameraPos;    // 相机位置(世界空间)
                uniform mat4 uProjectionInverse; // 投影矩阵逆
                uniform mat4 uViewInverse;       // 视图矩阵逆
                uniform Sphere uSpheres[3];      // 场景中的3个球体(2个物体+1个光源)
                uniform sampler2D uNoiseTexture; // 蓝噪声纹理
                uniform float uTime;        // 时间戳(用于累积采样)
 
                // -------------------------- 插值变量 --------------------------
                varying vec2 vUv;
 
                // -------------------------- 工具函数 --------------------------
                // 1. 从噪声纹理获取随机向量(高质量随机数)
                vec3 randomVec3(vec2 uv) {
                    vec3 noise = texture2D(uNoiseTexture, uv).rgb;
                    return 2.0 * noise - 1.0; // 转换为[-1,1]
                }
 
                // 2. 生成相机光线(从相机到当前像素)
                Ray createCameraRay(vec2 uv) {
                    // 像素坐标转NDC坐标
                    vec2 ndc = 2.0 * uv - 1.0;
                    ndc.y *= -1.0; // 翻转Y轴
 
                    // NDC转世界空间点(近平面)
                    vec4 clipPos = vec4(ndc.x, ndc.y, -1.0, 1.0);
                    vec4 viewPos = uProjectionInverse * clipPos;
                    viewPos /= viewPos.w;
                    vec4 worldPos = uViewInverse * viewPos;
 
                    // 计算光线方向
                    vec3 rayDir = normalize(worldPos.xyz - uCameraPos);
                    return Ray(
                        uCameraPos,
                        rayDir,
                        0.001,  // tMin=0.001,避免自相交
                        1000.0  // tMax=1000,最大追踪距离
                    );
                }
 
                // 3. 光线与球体相交检测
                HitResult raySphereIntersect(Ray ray, Sphere sphere) {
                    HitResult result;
                    result.hit = false;
 
                    vec3 oc = ray.origin - sphere.center;
                    float a = dot(ray.direction, ray.direction);
                    float b = 2.0 * dot(oc, ray.direction);
                    float c = dot(oc, oc) - sphere.radius * sphere.radius;
                    float discriminant = b * b - 4.0 * a * c;
 
                    if (discriminant > 0.0) {
                        float t1 = (-b - sqrt(discriminant)) / (2.0 * a);
                        float t2 = (-b + sqrt(discriminant)) / (2.0 * a);
                        float t = -1.0;
 
                        // 取在[tMin, tMax]范围内的最小t
                        if (t1 > ray.tMin && t1 < ray.tMax) {
                            t = t1;
                        } else if (t2 > ray.tMin && t2 < ray.tMax) {
                            t = t2;
                        }
 
                        if (t > 0.0) {
                            result.hit = true;
                            result.t = t;
                            result.pos = ray.origin + t * ray.direction;
                            result.normal = normalize(result.pos - sphere.center);
                            result.sphere = sphere;
                        }
                    }
 
                    return result;
                }
 
                // 4. 光线与场景相交检测(遍历所有球体)
                HitResult raySceneIntersect(Ray ray) {
                    HitResult closestHit;
                    closestHit.hit = false;
                    closestHit.t = ray.tMax;
 
                    for (int i = 0; i < 3; i++) {
                        HitResult hit = raySphereIntersect(ray, uSpheres[i]);
                        if (hit.hit && hit.t < closestHit.t) {
                            closestHit = hit;
                        }
                    }
 
                    return closestHit;
                }
 
                // 5. 漫反射方向采样(半球面随机方向)
                vec3 sampleDiffuseDirection(vec3 normal, vec2 uv) {
                    // 生成单位球内的随机点(拒绝采样法)
                    vec3 randomPoint;
                    do {
                        randomPoint = randomVec3(uv);
                    } while (dot(randomPoint, randomPoint) > 1.0);
                    randomPoint = normalize(randomPoint);
                    // 确保方向在法线的半球面内
                    return dot(randomPoint, normal) > 0.0 ? randomPoint : -randomPoint;
                }
 
                // 6. 俄罗斯轮盘赌(路径终止策略)
                bool russianRoulette(int bounceCount, vec2 uv, out float weight) {
                    float terminateProb;
                    if (bounceCount == 0) terminateProb = 0.2;
                    else if (bounceCount == 1) terminateProb = 0.3;
                    else terminateProb = 0.5;
 
                    // 从噪声纹理获取随机值,决定是否终止
                    float rand = texture2D(uNoiseTexture, uv).r;
                    if (rand < terminateProb) {
                        weight = 0.0;
                        return true;
                    } else {
                        weight = 1.0 / (1.0 - terminateProb);
                        return false;
                    }
                }
 
                // 7. 辐射度积分(计算当前路径段贡献)
                vec3 calculateRadiance(HitResult hit, Ray ray, vec2 uv, out Ray nextRay) {
                    vec3 radiance = vec3(0.0);
                    vec3 normal = hit.normal;
                    vec3 hitPos = hit.pos;
 
                    // 若击中光源,返回光源辐射度
                    if (hit.sphere.isLight) {
                        return hit.sphere.emission;
                    }
 
                    // 漫反射方向采样
                    vec3 nextDir = sampleDiffuseDirection(normal, uv);
                    // 生成下一条光线(避免自相交)
                    vec3 nextOrigin = hitPos + nextDir * 0.001;
                    nextRay = Ray(nextOrigin, nextDir, 0.001, ray.tMax);
 
                    // 漫反射辐射度贡献(朗伯定律)
                    float cosTheta = clamp(dot(nextDir, normal), 0.0, 1.0);
                    radiance = hit.sphere.albedo * cosTheta / PI;
 
                    return radiance;
                }
 
                // 8. 单条路径追踪
                vec3 traceSinglePath(Ray initialRay, vec2 uv) {
                    vec3 totalRadiance = vec3(0.0);
                    vec3 pathWeight = vec3(1.0);
                    Ray currentRay = initialRay;
 
                    for (int bounce = 0; bounce < MAX_BOUNCES; bounce++) {
                        // 光线与场景相交
                        HitResult hit = raySceneIntersect(currentRay);
                        if (!hit.hit) {
                            // 未击中物体,添加天空背景
                            totalRadiance += pathWeight * vec3(0.1, 0.2, 0.4);
                            break;
                        }
 
                        // 计算当前路径段贡献
                        Ray nextRay;
                        vec3 radiance = calculateRadiance(hit, currentRay, uv + vec2(float(bounce), 0.0), nextRay);
                        totalRadiance += pathWeight * radiance;
 
                        // 俄罗斯轮盘赌决定是否继续
                        float weight;
                        if (russianRoulette(bounce, uv + vec2(0.0, float(bounce)), weight)) {
                            break;
                        }
                        pathWeight *= radiance * weight;
 
                        // 更新当前光线
                        currentRay = nextRay;
                    }
 
                    return totalRadiance;
                }
 
                // 9. 多采样抗锯齿(每像素SAMPLE_COUNT次采样)
                vec3 multiSamplePathTracing() {
                    vec3 totalColor = vec3(0.0);
                    for (int i = 0; i < SAMPLE_COUNT; i++) {
                        // 生成不同采样点的UV(结合时间与索引,避免重复)
                        vec2 sampleUV = vUv + vec2(
                            (randomVec3(vUv + vec2(float(i), 0.0)).x) / uResolution.x,
                            (randomVec3(vUv + vec2(0.0, float(i))).y) / uResolution.y
                        ) + vec2(0.0, uTime * 0.001); // 时间偏移,累积采样
                        // 生成初始光线
                        Ray initialRay = createCameraRay(sampleUV);
                        // 追踪单条路径
                        totalColor += traceSinglePath(initialRay, sampleUV);
                    }
                    // 平均采样结果,减少噪点
                    return totalColor / float(SAMPLE_COUNT);
                }
 
                // -------------------------- 主函数 --------------------------
                void main() {
                    // 步骤1:多采样路径追踪
                    vec3 finalColor = multiSamplePathTracing();
 
                    // 步骤2:Gamma校正(匹配显示器输出)
                    finalColor = pow(finalColor, vec3(1.0 / 2.2));
 
                    // 步骤3:输出颜色(处理噪点:低于0.01的颜色设为0,减少杂色)
                    finalColor = max(finalColor, vec3(0.01));
                    gl_FragColor = vec4(finalColor, 1.0);
                }
            `,
            uniforms: {
                uResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
                uCameraPos: { value: camera.position },
                uProjectionInverse: { value: new THREE.Matrix4() },
                uViewInverse: { value: new THREE.Matrix4() },
                // 场景中的3个球体(2个漫反射物体+1个光源)
                uSpheres: {
                    value: [
                        // 球体1:红色漫反射物体(位于(-3, 0, 0),半径1.5)
                        {
                            center: new THREE.Vector3(-3, 0, 0),
                            radius: 1.5,
                            albedo: new THREE.Vector3(1.0, 0.2, 0.2),
                            isLight: false,
                            emission: new THREE.Vector3(0.0, 0.0, 0.0)
                        },
                        // 球体2:白色漫反射物体(位于(3, 0, 0),半径1.5)
                        {
                            center: new THREE.Vector3(3, 0, 0),
                            radius: 1.5,
                            albedo: new THREE.Vector3(0.9, 0.9, 0.9),
                            isLight: false,
                            emission: new THREE.Vector3(0.0, 0.0, 0.0)
                        },
                        // 球体3:黄色光源(位于(0, 4, 0),半径1.0,自发光)
                        {
                            center: new THREE.Vector3(0, 4, 0),
                            radius: 1.0,
                            albedo: new THREE.Vector3(1.0, 1.0, 1.0),
                            isLight: true,
                            emission: new THREE.Vector3(10.0, 8.0, 5.0) // 光源强度
                        }
                    ]
                },
                uNoiseTexture: { value: noiseTexture },
                uTime: { value: 0.0 } // 时间戳,每帧递增
            }
        });
 
        // 5. 创建全屏四边形(路径追踪渲染的载体)
        const fullscreenQuad = new THREE.Mesh(
            new THREE.PlaneGeometry(2, 2), // NDC坐标全屏四边形
            pathTracingMaterial
        );
        scene.add(fullscreenQuad);
 
        // 6. 渲染循环(更新时间戳与相机矩阵)
        function animate() {
            requestAnimationFrame(animate);
            controls.update();
 
            // 更新时间戳(用于累积采样)
            pathTracingMaterial.uniforms.uTime.value += 0.1;
 
            // 更新相机矩阵(投影逆矩阵、视图逆矩阵)
            pathTracingMaterial.uniforms.uCameraPos.value.copy(camera.position);
            pathTracingMaterial.uniforms.uProjectionInverse.value.copy(camera.projectionMatrix).invert();
            pathTracingMaterial.uniforms.uViewInverse.value.copy(camera.matrixWorld); // 视图逆矩阵=相机世界矩阵
 
            // 执行路径追踪渲染
            renderer.render(scene, camera);
        }
        animate();
 
        // 7. 窗口大小适配
        window.addEventListener('resize', () => {
            const width = window.innerWidth;
            const height = window.innerHeight;
            renderer.setSize(width, height);
            camera.aspect = width / height;
            camera.updateProjectionMatrix();
            pathTracingMaterial.uniforms.uResolution.value.set(width, height);
        });
    </script>
</body>
</html>
4.1.2 运行效果与解析

运行效果:  屏幕显示 3 个球体:红色漫反射球体、白色漫反射球体、黄色光源球体;白色球体靠近红色球体的一侧呈现 “淡淡的红色”(红色球体的漫反射间接光照),符合真实世界的 “颜色 bleeding” 效果;场景暗部(如球体下方)被间接光照填充,无纯黑区域,过渡自然;随着时间推移(uTime递增),画面噪点逐渐减少(时间累积采样效果)。 核心逻辑验证:  路径生成:从相机向每个像素发射初始光线,结合噪声纹理生成随机采样点;随机采样:漫反射球体相交后,在半球面内随机选择下一个方向,模拟漫反射散射;辐射度积分:计算每条路径的漫反射贡献,叠加光源与间接光照;路径终止:通过俄罗斯轮盘赌随机终止路径,避免无限反弹。

4.2 案例 2:带软阴影与焦散的路径追踪(复杂光学效果)

需求:在案例 1 的基础上,添加 “透明玻璃球体”,实现路径追踪的 “软阴影(光源随机采样)” 与 “焦散(玻璃折射形成的光斑)” 效果,模拟真实世界中 “阳光穿过玻璃在地面形成光斑” 的物理现象。

4.2.1 完整代码实现(基于案例 1 扩展)

html

预览

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>路径追踪实战2:软阴影与焦散</title>
    <script src="https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.158.0/examples/jsm/controls/OrbitControls.js"></script>
    <style>body { margin: 0; overflow: hidden; }</style>
</head>
<body>
    <script>
        // 1. 基础环境搭建(同案例1)
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        const renderer = new THREE.WebGLRenderer({ antialias: false });
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
        camera.position.set(0, 0, 20);
 
        // 2. 控制器(同案例1)
        const controls = new THREE.OrbitControls(camera, renderer.domElement);
        controls.enableDamping = true;
        controls.target.set(0, 0, 0);
 
        // 3. 加载蓝噪声纹理(同案例1)
        const noiseTexture = new THREE.TextureLoader().load('https://threejs.org/examples/textures/noise.png', (tex) => {
            tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
            tex.repeat.set(16, 16);
        });
 
        // 4. 路径追踪ShaderMaterial(扩展透明材质、软阴影、焦散)
        const pathTracingMaterial = new THREE.ShaderMaterial({
            vertexShader: `
                varying vec2 vUv;
                void main() {
                    vUv = uv;
                    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
                }
            `,
            fragmentShader: `
                precision highp float;
                #define MAX_BOUNCES 4 // 增加反弹次数,支持焦散
                #define SAMPLE_COUNT 8 // 增加采样数,减少焦散噪点
                #define PI 3.141592653589793
 
                // -------------------------- 结构体扩展(支持透明材质) --------------------------
                struct Ray { vec3 origin; vec3 direction; float tMin; float tMax; };
                struct Sphere {
                    vec3 center;
                    float radius;
                    vec3 albedo;
                    bool isLight;
                    vec3 emission;
                    int type; // 0=漫反射,1=透明
                    float refractiveIndex; // 透明材质折射率(玻璃=1.5)
                };
                struct HitResult { bool hit; float t; vec3 pos; vec3 normal; Sphere sphere; };
 
                // -------------------------- Uniform参数(扩展球体与光源) --------------------------
                uniform vec2 uResolution;
                uniform vec3 uCameraPos;
                uniform mat4 uProjectionInverse;
                uniform mat4 uViewInverse;
                uniform Sphere uSpheres[4]; // 4个球体(2个漫反射+1个透明+1个光源)
                uniform sampler2D uNoiseTexture;
                uniform float uTime;
                uniform vec3 uLightCenter; // 光源中心(用于软阴影采样)
                uniform float uLightRadius; // 光源半径(用于软阴影采样)
 
                varying vec2 vUv;
 
                // -------------------------- 工具函数(扩展透明材质、软阴影、焦散) --------------------------
                // 1. randomVec3()、createCameraRay()、raySphereIntersect() 同案例1,省略重复代码...
 
                // 2. 光线与场景相交检测(支持透明材质的法线反向)
                HitResult raySceneIntersect(Ray ray) {
                    HitResult closestHit;
                    closestHit.hit = false;
                    closestHit.t = ray.tMax;
 
                    for (int i = 0; i < 4; i++) {
                        HitResult hit = raySphereIntersect(ray, uSpheres[i]);
                        if (hit.hit && hit.t < closestHit.t) {
                            // 透明材质:若光线从内部穿出,法线反向
                            if (hit.sphere.type == 1) {
                                float dotDirNormal = dot(ray.direction, hit.normal);
                                if (dotDirNormal > 0.0) {
                                    hit.normal = -hit.normal;
                                }
                            }
                            closestHit = hit;
                        }
                    }
 
                    return closestHit;
                }
 
                // 3. 漫反射方向采样(同案例1)
                vec3 sampleDiffuseDirection(vec3 normal, vec2 uv) { /* 同案例1,省略 */ }
 
                // 4. 透明材质方向采样(反射+折射,支持焦散)
                vec3 sampleTransparentDirection(vec3 normal, vec3 incidentDir, vec2 uv, float refractiveIndex, out bool isReflect) {
                    // 菲涅尔效应:计算反射概率
                    float cosTheta = clamp(dot(-incidentDir, normal), 0.0, 1.0);
                    float fresnel = pow(1.0 - cosTheta, 5.0);
                    float reflectProb = 0.2 + 0.8 * fresnel; // 基础反射概率0.2
 
                    // 随机决定反射或折射
                    float rand = texture2D(uNoiseTexture, uv).r;
                    isReflect = rand < reflectProb;
                    if (isReflect) {
                        // 反射方向
                        return reflect(incidentDir, normal);
                    } else {
                        // 折射方向(斯涅尔定律)
                        float eta = dot(-incidentDir, normal) > 0.0 ? 1.0 / refractiveIndex : refractiveIndex;
                        vec3 refractedDir = refract(incidentDir, normal, eta);
                        return refractedDir;
                    }
                }
 
                // 5. 软阴影采样(光源视为区域光源,随机采样光源表面)
                bool sampleSoftShadow(vec3 hitPos, vec3 normal) {
                    // 随机采样光源表面的点
                    vec3 randomOffset = uLightRadius * randomVec3(hitPos.xy + vec2(uTime, 0.0));
                    vec3 lightSamplePos = uLightCenter + randomOffset;
 
                    // 生成阴影光线
                    vec3 shadowRayDir = normalize(lightSamplePos - hitPos);
                    Ray shadowRay = Ray(
                        hitPos + normal * 0.001,
                        shadowRayDir,
                        0.001,
                        distance(lightSamplePos, hitPos) - 0.001
                    );
 
                    // 检测阴影光线是否被遮挡(透明材质不遮挡,支持焦散)
                    HitResult shadowHit = raySceneIntersect(shadowRay);
                    if (shadowHit.hit) {
                        // 若遮挡物是透明材质,不视为阴影(允许光线穿过,形成焦散)
                        return shadowHit.sphere.type != 1;
                    }
                    return false;
                }
 
                // 6. 辐射度积分(扩展透明材质贡献)
                vec3 calculateRadiance(HitResult hit, Ray ray, vec2 uv, out Ray nextRay) {
                    vec3 radiance = vec3(0.0);
                    vec3 normal = hit.normal;
                    vec3 hitPos = hit.pos;
                    vec3 incidentDir = -ray.direction;
 
                    // 击中光源:返回光源辐射度
                    if (hit.sphere.isLight) {
                        return hit.sphere.emission;
                    }
 
                    // 漫反射材质
                    if (hit.sphere.type == 0) {
                        // 软阴影:计算直接光照贡献
                        vec3 directLight = vec3(0.0);
                        if (!sampleSoftShadow(hitPos, normal)) {
                            // 直接光照:光源辐射度 × 漫反射因子
                            vec3 lightDir = normalize(uLightCenter - hitPos);
                            float cosTheta = clamp(dot(normal, lightDir), 0.0, 1.0);
                            directLight = hit.sphere.albedo * cosTheta * uSpheres[3].emission / (4.0 * PI * pow(distance(hitPos, uLightCenter), 2.0));
                        }
 
                        // 间接光照:漫反射方向采样
                        vec3 nextDir = sampleDiffuseDirection(normal, uv);
                        vec3 nextOrigin = hitPos + nextDir * 0.001;
                        nextRay = Ray(nextOrigin, nextDir, 0.001, ray.tMax);
 
                        // 总辐射度 = 直接光照 + 间接光照贡献
                        float indirectFactor = clamp(dot(nextDir, normal), 0.0, 1.0);
                        radiance = directLight + hit.sphere.albedo * indirectFactor / PI;
                    }
                    // 透明材质(支持焦散)
                    else if (hit.sphere.type == 1) {
                        bool isReflect;
                        vec3 nextDir = sampleTransparentDirection(normal, incidentDir, uv, hit.sphere.refractiveIndex, isReflect);
                        vec3 nextOrigin = hitPos + nextDir * 0.001;
                        nextRay = Ray(nextOrigin, nextDir, 0.001, ray.tMax);
 
                        // 透明材质贡献:反射时同镜面,折射时颜色衰减小
                        radiance = isReflect ? hit.sphere.albedo * 0.8 : hit.sphere.albedo * 0.95;
                    }
 
                    return radiance;
                }
 
                // 7. 单条路径追踪(同案例1,增加焦散支持)
                vec3 traceSinglePath(Ray initialRay, vec2 uv) { /* 同案例1,省略重复代码,MAX_BOUNCES=4 */ }
 
                // 8. 多采样路径追踪(同案例1,SAMPLE_COUNT=8)
                vec3 multiSamplePathTracing() { /* 同案例1,省略重复代码 */ }
 
                // -------------------------- 主函数 --------------------------
                void main() {
                    vec3 finalColor = multiSamplePathTracing();
                    finalColor = pow(finalColor, vec3(1.0 / 2.2));
                    finalColor = max(finalColor, vec3(0.01));
                    gl_FragColor = vec4(finalColor, 1.0);
                }
            `,
            uniforms: {
                uResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
                uCameraPos: { value: camera.position },
                uProjectionInverse: { value: new THREE.Matrix4() },
                uViewInverse: { value: new THREE.Matrix4() },
                // 场景中的4个球体(2个漫反射+1个透明+1个光源)
                uSpheres: {
                    value: [
                        // 球体1:红色漫反射(同案例1)
                        { center: new THREE.Vector3(-4, 0, 0), radius: 1.5, albedo: new THREE.Vector3(1.0, 0.2, 0.2), isLight: false, emission: vec3(0), type: 0, refractiveIndex: 0.0 },
                        // 球体2:白色漫反射(地面,位于(0, -3, 0),半径2.5)
                        { center: new THREE.Vector3(0, -3, 0), radius: 2.5, albedo: new THREE.Vector3(0.9, 0.9, 0.9), isLight: false, emission: vec3(0), type: 0, refractiveIndex: 0.0 },
                        // 球体3:透明玻璃(位于(2, 0, 0),半径1.2,折射率1.5)
                        { center: new THREE.Vector3(2, 0, 0), radius: 1.2, albedo: new THREE.Vector3(0.95, 0.95, 0.95), isLight: false, emission: vec3(0), type: 1, refractiveIndex: 1.5 },
                        // 球体4:黄色区域光源(位于(0, 5, 0),半径1.2,自发光)
                        { center: new THREE.Vector3(0, 5, 0), radius: 1.2, albedo: vec3(1), isLight: true, emission: new THREE.Vector3(15.0, 12.0, 8.0), type: 0, refractiveIndex: 0.0 }
                    ]
                },
                uNoiseTexture: { value: noiseTexture },
                uTime: { value: 0.0 },
                uLightCenter: { value: new THREE.Vector3(0, 5, 0) }, // 光源中心
                uLightRadius: { value: 1.2 } // 光源半径(区域光源大小)
            }
        });
 
        // 5. 创建全屏四边形(同案例1)
        const fullscreenQuad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), pathTracingMaterial);
        scene.add(fullscreenQuad);
 
        // 6. 渲染循环(同案例1)
        function animate() {
            requestAnimationFrame(animate);
            controls.update();
 
            pathTracingMaterial.uniforms.uTime.value += 0.1;
            pathTracingMaterial.uniforms.uCameraPos.value.copy(camera.position);
            pathTracingMaterial.uniforms.uProjectionInverse.value.copy(camera.projectionMatrix).invert();
            pathTracingMaterial.uniforms.uViewInverse.value.copy(camera.matrixWorld);
 
            renderer.render(scene, camera);
        }
        animate();
 
        // 7. 窗口大小适配(同案例1)
        window.addEventListener('resize', () => {
            const width = window.innerWidth;
            const height = window.innerHeight;
            renderer.setSize(width, height);
            camera.aspect = width / height;
            camera.updateProjectionMatrix();
            pathTracingMaterial.uniforms.uResolution.value.set(width, height);
        });
    </script>
</body>
</html>
4.2.2 运行效果与解析

运行效果:  场景新增 “透明玻璃球体” 与 “黄色区域光源”,白色漫反射球体(地面)上出现 “玻璃球体投射的软阴影”(阴影边缘渐变,符合区域光源特性);地面上玻璃球体下方出现 “亮斑”(焦散效果),这是 “光线穿过玻璃球体折射后,在地面汇聚形成的光斑”,完全模拟真实玻璃的光学特性;红色漫反射球体的间接光照仍存在,白色地面靠近红色球体的一侧仍呈淡红色,全局光照效果正常。 核心技术验证:  软阴影:将光源视为 “区域光源”,随机采样光源表面点,生成多条阴影光线,实现渐变阴影;透明材质:通过 “菲涅尔效应” 随机选择反射或折射方向,折射光线穿过玻璃后继续传播,实现透明效果;焦散:折射光线在地面汇聚,通过多次反弹的辐射度积分,在地面形成亮斑,还原真实焦散现象。

第五章 常见问题排查与进阶优化

路径追踪的实现复杂度高,易出现 “噪点严重”“焦散效果不明显”“性能卡顿” 等问题,本节提供针对性的排查步骤与进阶优化技巧,帮助零基础用户解决实际开发中的难题。

5.1 常见问题排查

5.1.1 问题 1:画面噪点严重(无法接受)

现象:渲染结果存在大量随机噪点,尤其是暗部与焦散区域;原因 1:每像素采样数过低(如<4 次),蒙特卡洛采样的误差过大;  优化:增加采样数到 8~16 次,或启用 “时间累积采样”(每帧叠加 1 次采样,10 帧后噪点减少 90%); 原因 2:随机数质量低,导致采样方向分布不均匀;  排查:替换为 “蓝噪声纹理” 采样随机值,蓝噪声的噪点分布更均匀,后期降噪更易; 原因 3:反弹次数不足(如<3 次),间接光照贡献计算不完整;  优化:增加最大反弹次数到 4~5 次,尤其是焦散场景,需足够反弹次数让折射光线到达地面。

5.1.2 问题 2:焦散效果不明显或无焦散

现象:透明玻璃球体下方无亮斑,或焦散区域过暗;原因 1:透明材质的折射率设置错误(如≠1.5),导致折射方向计算偏差;  排查:玻璃的折射率设为 1.5~1.6,水的折射率设为 1.33,确保符合真实物理参数; 原因 2:阴影检测时将透明材质视为遮挡物,折射光线被阻断;  排查:在sampleSoftShadow()函数中,若遮挡物是透明材质(type==1),返回false(不视为阴影),允许折射光线穿过; 原因 3:采样数过低,焦散区域的噪点掩盖了亮斑;  优化:焦散场景需将采样数提升到 16~32 次,或使用 “焦散专用采样”(如重要性采样,优先采样光源方向)。

5.1.3 问题 3:性能卡顿(帧率<1fps)

现象:路径追踪渲染帧率极低,无法实时交互;原因 1:渲染分辨率过高(如 1920x1080),每像素采样数 × 分辨率的计算量过大;  优化:降低渲染分辨率到 0.5 倍(如 960x540),用 Three.js 的WebGLRenderTarget渲染到离屏目标,再放大到屏幕; 原因 2:循环未手动展开,WEBGL 对for循环的优化差;  排查:手动展开SAMPLE_COUNT=4的循环为 4 个独立步骤,避免for循环(如sample1 = ...; sample2 = ...; total = (sample1+sample2+...) /4); 原因 3:场景中物体数量过多(如>5 个球体),相交检测次数过多;  优化:限制场景物体数量为 3~4 个,仅保留核心物体(如 1 个透明 + 2 个漫反射 + 1 个光源)。

5.2 进阶优化技巧(突破 WEBGL 性能瓶颈)

5.2.1 1. 重要性采样(减少采样数)

传统 “均匀采样” 的效率低,“重要性采样” 通过 “优先采样对结果贡献大的方向”(如光源方向、镜面反射方向),减少采样数的同时降低噪点:

核心逻辑:对漫反射材质,优先采样光源方向;对透明材质,优先采样折射方向;Shader 实现

glsl

// 漫反射重要性采样:优先采样光源方向
vec3 sampleDiffuseImportance(vec3 normal, vec2 uv, vec3 lightCenter) {
    float lightProb = 0.7; // 70%概率采样光源方向,30%概率均匀采样
    if (random(uv) < lightProb) {
        // 采样光源方向
        vec3 lightDir = normalize(lightCenter - randomVec3(uv) * 0.1);
        return dot(lightDir, normal) > 0.0 ? lightDir : -lightDir;
    } else {
        // 均匀采样半球面
        return sampleDiffuseDirection(normal, uv);
    }
}
5.2.2 2. 空间划分加速(BVH 树)

对 “物体数量多” 的场景,用 “边界体积层次结构(BVH)” 对物体进行空间划分,减少相交检测的物体数量:

核心逻辑:将场景中的物体按空间位置分组,每个组用 “轴对齐包围盒(AABB)” 包裹,光线先与 AABB 相交检测,再与组内物体相交检测;WEBGL 实现:在 CPU 端构建 BVH 树,将 BVH 节点的 AABB 信息(最小点、最大点)传递给 Shader,Shader 中按 BVH 层次遍历物体,相交检测次数减少 50% 以上。

5.2.3 3. 利用 WebGL 2.0 新特性

升级到 WebGL 2.0,利用新特性提升路径追踪性能:

纹理缓冲对象(TBO):用 TBO 存储场景物体数据(如球体位置、半径),替代uniform数组,支持更多物体(>10 个);计算着色器(Compute Shader):用计算着色器并行处理 “路径生成”“随机采样” 步骤,突破片元 Shader 的性能限制;

  • 全部评论(0)
最新发布的资讯信息
【系统环境|】基于C++的药品购买系统设计和实现的详细项目实例(2025-10-12 05:26)
【系统环境|】upload漏洞审计报告思路(2025-10-12 05:25)
【系统环境|】WEBGL Shader零基础教程(十六):光线追踪技术之Metropolis光传输(2025-10-12 05:23)
【系统环境|】探秘提示工程架构师的提示工程文档规范体系(2025-10-12 05:22)
【系统环境|】WEBGL Shader零基础教程(十):顶点纹理拾取(2025-10-12 05:21)
【系统环境|】WEBGL Shader零基础教程(十一):高级光照(2025-10-12 05:20)
【系统环境|】WEBGL Shader零基础教程(十二):全局光照(GI)(2025-10-12 05:20)
【系统环境|】WEBGL Shader零基础教程(十四):光线追踪技术之路径追踪(2025-10-12 05:14)
【系统环境|】2FA验证器 验证码如何登录(2024-04-01 20:18)
【系统环境|】怎么做才能建设好外贸网站?(2023-12-20 10:05)
手机二维码手机访问领取大礼包
返回顶部