正确生成漫反射方向需用余弦加权半球采样:构造局部坐标系,用φ=2πξ₁、cosθ=√ξ₂生成方向,并归一化;能量衰减须含albedo、cosθ及1/pdf校正,pdf=cosθ/π,最终衰减为albedo;禁用均匀球面采样,因其pdf=1/(2π)导致能量不守恒。

漫反射方向怎么生成:别用均匀球面采样
直接在单位球面上均匀采样得到的方向,会导致 Lambert 漫反射模型的能量不守恒——视觉上物体会过亮,尤其在低粗糙度材质下更明显。正确做法是用余弦加权的半球采样:cosθ 分布(θ 是与法向夹角),让采样密度正比于 cosθ,才能匹配 Lambert 反射率定义。
实操建议:
- 设表面法向为
n,先构造局部坐标系:用orthonormal_basis(n)得到两个正交向量u、v - 生成两个 [0,1) 均匀随机数
ξ₁、ξ₂,计算:φ = 2 * M_PI * ξ₁cosθ = sqrt(ξ₂)sinθ = sqrt(1 - ξ₂)direction = u * cosφ * sinθ + v * sinφ * sinθ + n * cosθ - 注意:必须归一化
direction(即使数学上已单位化,浮点误差仍可能导致dot(direction, direction) != 1)
能量衰减怎么算:别漏掉几何项和 PDF 校正
漫反射光线打到新点后,接收到的能量不是简单乘个 albedo 就完事。路径追踪中,每次反弹都要按渲染方程做蒙特卡洛积分,衰减项包含三部分:albedo(反射率)、cosθ(入射角衰减)、1 / pdf(概率密度函数倒数)。
常见错误现象:diffuse surface looks too dark near edges 或 noise spikes on flat diffuse walls,往往就是漏了 1 / pdf。
立即学习“C++免费学习笔记(深入)”;
实操建议:
- 对上面生成的余弦加权方向,其 PDF(在半球上的概率密度)是
pdf = cosθ / π - 因此 BRDF × cosθ / pdf =
albedo / π × cosθ / (cosθ / π)=albedo—— 这才是无偏的权重 - 最终该次散射贡献为:
ray_color * albedo * attenuation,其中attenuation就是这个简化后的albedo,但前提是采样逻辑和 PDF 完全匹配
为什么不能直接用 random_in_unit_sphere()?
很多初学者抄代码时直接复用生成单位球内随机点的函数,然后归一化成方向,这本质是均匀球面采样。它在半球上的 PDF 是 1/(2π),而非正确的 cosθ/π。强行套用会引入系统性偏差:所有漫反射都多乘了约 2 倍的 cosθ,导致明暗过渡生硬、边缘发灰、收敛慢。
性能与兼容性影响:
- 均匀球采样虽快,但需要更高样本数才能压住噪点;余弦加权半球采样虽多两三个 sqrt/sin/cos,但收敛更快
- 如果后续要支持各向异性或 Oren-Nayar 等扩展模型,余弦加权是基础接口,硬塞球采样会破坏架构一致性
实际写 scatter() 函数时的关键检查点
C++ 中典型的材质 scatter 接口返回是否发生散射、新的光线、衰减系数。漫反射实现里最容易被忽略的是法向朝向和光线方向的一致性判断。
实操建议:
- 入射光线方向
ray.direction()是从相机指向交点的,所以真实入射方向是-ray.direction();确保dot(n, incident)> 0,否则说明交点在背面,应拒绝散射或翻转法向 - 生成的散射方向
scattered.direction()必须满足dot(n, scattered.direction()) > 0,否则光线会扎进物体内部,造成黑斑或 NaN -
attenuation应为albedo * (1.0f / M_PI)(Lambert BRDF 的常数项),而不是albedo * cosθ或其他变体 - 不要在
scatter()里做距离衰减(如1/r²)——那是ray_color()递归调用时由光线传播距离自然体现的
余弦加权采样的数学推导不难跳过,但漏掉 PDF 校正或法向朝向检查,调试时几乎无法通过肉眼区分是 BRDF 写错了还是采样错了。这两个地方一旦出问题,噪点模式会非常“有规律”地集中出现在斜面或阴影交界处。


















