《用余生学习光线追踪》2.光照散射与重要性采样

本文详细介绍了一个基于《RayTracinginOneWeekend》系列书籍的光线追踪项目。内容涵盖从基础概念到高级技巧,如射线、相机、抗锯齿、材质模拟、纹理映射等,并深入探讨了概率论在光照散射中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本项目参考自《Ray Tracing in One Weekend》系列
项目链接:https://github.com/maijiaquan/ray-tracing-with-imgui

目录:
《用两天学习光线追踪》1.项目介绍和ppm图片输出
《用两天学习光线追踪》2.射线、简单相机和背景输出
《用两天学习光线追踪》3.球体和表面法向量
《用两天学习光线追踪》4.封装成类
《用两天学习光线追踪》5.抗锯齿
《用两天学习光线追踪》6.漫反射材质
《用两天学习光线追踪》7.反射向量和金属材质
《用两天学习光线追踪》8.折射向量和电介质
《用两天学习光线追踪》9.可放置相机
《用两天学习光线追踪》10.散焦模糊

《用一周学习光线追踪》1.动态模糊
《用一周学习光线追踪》2.BVH树、AABB相交检测
《用一周学习光线追踪》3.纯色纹理和棋盘纹理
《用一周学习光线追踪》4.柏林噪声
《用一周学习光线追踪》5.球面纹理贴图
《用一周学习光线追踪》6.光照和轴对齐矩形
《用一周学习光线追踪》7.长方体和平移旋转

《用余生学习光线追踪》1.蒙特卡洛积分与概率密度函数PDF
《用余生学习光线追踪》2.光照散射与重要性采样
《用余生学习光线追踪》3.生成球坐标随机方向
《用余生学习光线追踪》4.直接对光照采样


球面各方向上的蒙特卡洛积分

Ray Tracer需要选取随机方向,方向可以定义为单位圆上的一个点。我们需要定义一个二维的PDF,假设我们要对任意方向求解以下积分:
∫ c o s 2 ( θ ) \int cos^2(\theta) cos2(θ)

如果要用蒙特卡洛算法来求解积分,则我们要计算 c o s 2 ( θ ) / p ( d i r e c t i o n ) cos^2(\theta) / p(direction) cos2(θ)/p(direction),可以用基于极坐标的参数 ( θ , ϕ ) (\theta, \phi) (θ,ϕ)来定义方向。

创建一个单位球上的点的函数如下:

vec3 random_on_unit_sphere() {
    vec3 p;
    do {
        p = 2.0*vec3(random_double(),random_double(),random_double()) - vec3(1,1,1);
    } while (dot(p,p) >= 1.0);
    return unit_vector(p);
}

这些单位球上的随机点的PDF,就是 1 / a r e a 1/area 1/area,其中area为单位球的面积,也就是 1 / ( 4 π ) 1/(4\pi) 1/(4π),如果 θ \theta θ是球心到球面上一点连线与z轴正方向的夹角,则 c o s ( θ ) cos(\theta) cos(θ)为该单位向量的z轴上的分量。

写成代码如下:

inline float pdf(const vec3& p) {
    return 1 / (4*M_PI);
}

int main() {
    int N = 1000000;
    float sum;
    for (int i = 0; i < N; i++) {
            vec3 d = random_on_unit_sphere();
            float cosine_squared = d.z()*d.z();
            sum += cosine_squared / pdf(d);
    }
    std::cout << "I =" << sum/N << "\n";
}

上述积分的解析解为 4 3 π \frac{4}{3} \pi 34π

备注:solid angle 和 area 和 direction 是一回事。


光照散射

接下来,我们会大幅度修改光照的算法。之前的光照都是直接光线和物体表面散射得到的,并没有用到概率论,接下来把概率论用上。首先,我们去假设光线是否被吸收了:

  • 光被散射(没被吸收)的概率为 A A A

  • 光被吸收的概率为 1 − A 1-A 1A

A代表albedo,也就是我们所说的反射率
我们将会使用波长来代替RGB,RGB对应长、中、短波长。如果光线确实发生散射了,那就会有一个方向性的概率分布,我们将其定义成关于立体角(solid angle)的散射pdf(scattering pdf),数学表示为 s ( d i r e c t i o n ) s(direction) s(direction),散射pdf会随着方向而变化。根据散射概率、散射pdf和颜色函数,颜色函数定义如下:
C o l o r = ∫ A ⋅ s ( d i r e c t i o n ) ⋅ c o l o r ( d i r e c t i o n ) Color = \int A \cdot s(direction) \cdot color(direction) Color=As(direction)color(direction)

注意,A、s()可能会随着观察的方向和物体表面的位置不同而变化。
套用蒙特卡洛积分公式,得到估算公式如下:
C o l o r = ( A ⋅ s ( d i r e c t i o n ) ⋅ c o l o r ( d i r e c t i o n ) ) / p ( d i r e c t i o n ) (0) Color = (A \cdot s(direction) \cdot color(direction)) / p(direction) \tag{0} Color=(As(direction)color(direction))/p(direction)(0)
其中, p ( d i r e c t i o n ) p(direction) p(direction)为随机生成的方向的pdf。
对于兰伯特材质来说,我们已经隐性地套用过这个公式了:我们假设其 p ( ) p() p()是一个cosine函数。s()跟 c o s ( θ ) cos(\theta) cos(θ)成正比,其中 θ \theta θ 为相对于法线表面的夹角。注意,所有的pdf在积分区间内的积分结果为1。当 cos ⁡ ( θ ) < 0 \cos(\theta)<0 cos(θ)<0的时候, s ( d i r e c t i o n ) = 0 s(direction) = 0 s(direction)=0,半球的cos的积分值为 π \pi π
在球坐标系中,有公式:
δ A = s i n ( θ ) δ θ δ ϕ \delta A = sin(\theta) \delta \theta \delta \phi δA=sin(θ)δθδϕ
于是有:
A r e a = ∫ 0 2 π ∫ 0 π / 2 c o s ( θ ) s i n ( θ ) δ θ δ ϕ = 2 π ⋅ 1 2 = π Area = \int_{0}^{2 \pi} \int_{0}^{\pi / 2} cos(\theta)sin(\theta) \delta \theta \delta \phi = 2 \pi \cdot \frac{1}{2} = \pi Area=02π0π/2cos(θ)sin(θ)δθδϕ=2π21=π
因此,对于一个兰伯特表面,散射pdf为:
s ( d i r e c t i o n ) = c o s ( θ ) / π (1) s(direction) = cos(\theta) / \pi \tag{1} s(direction)=cos(θ)/π(1)
如果采样pdf也是用这个函数,即
p ( d i r e c t i o n ) = c o s ( θ ) / π (2) p(direction) = cos(\theta) / \pi \tag{2} p(direction)=cos(θ)/π(2)
则s(direction)和p(direction)相互抵消后有:
C o l o r = A ∗ c o l o r ( d i r e c t i o n ) Color = A * color(direction) Color=Acolor(direction)
这就是原来的color函数。
现在有了概率论,我们可以朝着更加重要的方向(例如光源的方向)额外发射光线。
有些教材里面会通过双向反射分布函数(bidirectional reflectance distribution function,简称BRDF)描述反射:
B R D F = A ∗ s ( d i r e c t i o n ) / c o s ( θ ) BRDF = A * s(direction) / cos(\theta) BRDF=As(direction)/cos(θ)
以兰伯特材质为例,BRDF公式为:
B R D F = A / π BRDF = A / \pi BRDF=A/π


光照散射与重要性采样

上一节的蒙特卡洛算法求解积分的步骤如下:

  1. 找到在区间 [ a , b ] [a,b] [a,b]内被积函数 f ( x ) f(x) f(x)
  2. 在区间 [ a , b ] [a,b] [a,b]内挑选一个合适的非零的概率密度函数 p p p
  3. 生成一堆满足上述概率密度函数 p p p分布的随机数 r r r,从而得到一堆 f ( r ) / p ( r ) f(r)/p(r) f(r)/p(r),求这堆 f ( r ) / p ( r ) f(r)/p(r) f(r)/p(r)的平均值

接下来两节,目标都是向着光源的位置,发送一堆额外的光线,从而降噪。

假设我们可以用一个概率密度函数 p L i g h t ( d i r e c t i o n ) pLight(direction) pLight(direction)生成一堆额外向着光源的光线。

假设我们有一个跟s有关的概率密度函数 p S u r f a c e ( d i r e c t i o n ) pSurface(direction) pSurface(direction)
两个pdf的线性求和,仍然是pdf,例如:
p ( d i r e c t i o n ) = 0.5 ⋅ p L i g h t ( d i r e c t i o n ) + 0.5 ∗ ⋅ p S u r f a c e ( d i r e c t i o n ) p(direction) = 0.5 \cdot pLight(direction) + 0.5* \cdot pSurface(direction) p(direction)=0.5pLight(direction)+0.5pSurface(direction)
只要权重是整数且权重之和为1,任意的pdf求和结果仍然是pdf。
我们希望能够找到一个pdf,使得 s ( d i r e c t i o n ) ∗ c o l o r ( d i r e c t i o n ) s(direction)*color(direction) s(direction)color(direction)增大的同时,这个pdf也同样增大。对于漫反射表面来说,这也是在猜测哪里会有更大的 c o l o r ( d i r e c t i o n ) color(direction) color(direction)值。

为了降噪,我们需要会构造一个能发射更多光线到光源的pdf。
回忆一下蒙特卡洛积分公式:
∫ f ( x ) ≈ f ( r ) / p ( r ) \int f(x) \approx f(r)/p(r) f(x)f(r)/p(r)
对于Lambertian材质,我们定义:
p ( d i r e c t i o n ) = c o s ( θ ) / π p(direction) = cos(\theta) / \pi p(direction)=cos(θ)/π

接下来,修改material类的scatter函数,新增函数scattering_pdf()

class material  {
    public:
        virtual bool scatter(const ray& r_in,
            const hit_record& rec, vec3& albedo, ray& scattered, float& pdf) const {
            return false;
        }
        virtual float scattering_pdf(const ray& r_in, const hit_record& rec,
                                     const ray& scattered) const {
            return 0;
        }
        virtual vec3 emitted(float u, float v, const vec3& p) const {
            return vec3(0,0,0);
        }
};

修改lambertian类,scatter函数新增了计算pdf的功能。

class lambertian : public material {
    public:
        lambertian(texture *a) : albedo(a) {}
        float scattering_pdf(const ray& r_in,
            const hit_record& rec, const ray& scattered) const {
            float cosine = dot(rec.normal, unit_vector(scattered.direction()));
            if (cosine < 0)
                return 0;
            return cosine / M_PI;
        }
        bool scatter(const ray& r_in,
            const hit_record& rec, vec3& alb, ray& scattered, float& pdf) const {
            vec3 target = rec.p + rec.normal + random_in_unit_sphere();
            scattered = ray(rec.p, unit_vector(target-rec.p), r_in.time());
            alb = albedo->value(rec.u, rec.v, rec.p);
            pdf = dot(rec.normal, scattered.direction()) / M_PI;
            return true;
        }
        texture *albedo;
};

color函数修改如下:

vec3 color(const ray& r, hittable *world, int depth) {
    hit_record rec;
    if (world->hit(r, 0.001, MAXFLOAT, rec)) {
        ray scattered;
        vec3 attenuation;
        vec3 emitted = rec.mat_ptr->emitted(rec.u, rec.v, rec.p);
        float pdf;
        vec3 albedo;
        if (depth < 50 && rec.mat_ptr->scatter(r, rec, albedo, scattered, pdf)) {
            return emitted + albedo*rec.mat_ptr->scattering_pdf(r, rec, scattered)
                *color(scattered, world, depth+1) / pdf;
        }
        else
            return emitted;
    }
    else
        return vec3(0,0,0);
}

其中,color的计算来源于公式一和公式二。
渲染结果和之前一样,没有区别。

接下来,根据经验,换一种采样策略。选取命中表面上的一个半球的某个方向作为散射方向,则有:
p ( d i r e c t i o n ) = 1 / 2 π p(direction) = 1/2\pi p(direction)=1/2π

scatter函数换成:

bool scatter(const ray& r_in,
    const hit_record& rec, vec3& alb, ray& scattered, float& pdf) const {
    vec3 direction;
    do {
        direction = random_in_unit_sphere();
    } while (dot(direction, rec.normal) < 0);
    scattered = ray(rec.p, unit_vector(direction), r_in.time());
    alb = albedo->value(rec.u, rec.v, rec.p);
    pdf = 0.5 / M_PI;
    return true;
}

结果如下:

在这里插入图片描述

左box前面的颜色似乎更加均匀了。这究竟是bug还是feature?


参考资料:
1.Ray Tracing: The Rest of Your Life

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值