标签: PRT

  • Games202 作业二 PRT实现

    Games202 作业二 PRT实现

    https://remoooo.com/202hw1
    上一篇
    下一篇
    img
    img
    img

    因为我也是菜鸡,所以不能确保所有都是正确的,希望大佬指正。

    知乎的公式有点丑陋,可以去:GitHub

    项目源代码:

    https://github.com/Remyuu/GAMES202-Homeworkgithub.com/Remyuu/GAMES202-Homework

    预计算球谐系数

    利用框架nori预先计算球谐函数系数。

    环境光照:计算cubemap每个像素的球谐系数

    ProjEnv::PrecomputeCubemapSH(images, width, height, channel); 使用黎曼积分的方法计算环境光球谐函数的系数。

    完整代码

    // TODO: here you need to compute light sh of each face of cubemap of each pixel
    // TODO: 此处你需要计算每个像素下cubemap某个面的球谐系数
    Eigen::Vector3f dir = cubemapDirs[i * width * height + y * width + x];
    int index = (y * width + x) * channel;
    Eigen::Array3f Le(images[i][index + 0], images[i][index + 1],
                      images[i][index + 2]);
    // 描述在球面坐标上当前的角度
    double theta = acos(dir.z());
    double phi = atan2(dir.y(), dir.x());
    // 遍历球谐函数的各个基函数
    for (int l = 0; l <= SHOrder; l++){
        for (int m = -l; m <= l; m++){
            float sh = sh::EvalSH(l, m, phi, theta);
            float delta = CalcArea((float)x, (float)y, width, height);
            SHCoeffiecents[l*(l+1)+m] += Le * sh * delta;
        }
    }
    C#

    分析

    球谐系数是球谐函数在一个球面上的投影,可以用于表示函数在球面上的分布。由于我们有RGB值有三个通道,因此我们会的球谐系数会存储为一个三维的向量。需要完善的部分:

    /// prt.cpp - PrecomputeCubemapSH()
    // TODO: here you need to compute light sh of each face of cubemap of each pixel
    // TODO: 此处你需要计算每个像素下cubemap某个面的球谐系数
    Eigen::Vector3f dir = cubemapDirs[i * width * height + y * width + x];
    int index = (y * width + x) * channel;
    Eigen::Array3f Le(images[i][index + 0], images[i][index + 1],
                      images[i][index + 2]);
    C#

    首先从六个 cubemap ( images 数组)的每个像素采样方向(一个三维向量,表示从中心到像素的方向),将方向转化为球面坐标( theta 和 phi )。

    接着将各个球面坐标传入 sh::EvalSH() 分别计算每个球谐函数(基函数)的实值 sh 。同时计算每个 cubemap 中每个像素所占据球面区域的比重 delta 。

    最后累加球谐系数,代码中我们可以对 cubemap 伤的所有像素进行累加,近似在原始计算球谐函数积分的操作。

    $$
    Ylm=∫ϕ=02π∫θ=0πf(θ,ϕ)Ylm(θ,ϕ)sin⁡(θ)dθdϕ
    $$

    其中:

    • θ 是天顶角,范围从 0 到 π; ϕ 是方位角,范围从 0 到 2pi 。
    • f(θ,ϕ) 是球面上某点的函数值。
    • Ylm 是球谐函数,它由相应的勒让德多项式 Plm 和一些三角函数组成。
    • l 是球谐函数的阶数; m 是球谐函数的序数,范围从 −l 到 l 。

    为了更加具体的让读者理解,这里写出代码中球谐函数离散形式的估计,即黎曼积分的方法来计算。

    $$
    Ylm=∑i=1Nf(θi,ϕi)Ylm(θi,ϕi)Δωi
    $$

    其中:

    • f(θi,ϕi) 是球面上某点的函数值。
    • Ylm(θi,ϕi) 是球谐函数在该点的值。
    • Δωi 是该点在球面上的微小区域或权重。
    • N 是所有离散点的总数。

    代码细节

    • 从 cubemap 获取RGB光照信息
    Eigen::Array3f Le(images[i][index + 0], images[i][index + 1],
                      images[i][index + 2]);
    C#

    channel 的值是3,对应于RGB三个通道。因此,index 就指向了某一像素的红色通道的位置,index + 1 指向绿色通道的位置,index + 2 指向蓝色通道的位置。

    • 将方向向量转换为球面坐标
    double theta = acos(dir.z());
    double phi = atan2(dir.y(), dir.x());
    C#

    theta 是正z轴到 dir 方向的夹角,而 phi 表示从正x轴到 dir 在xz平面上的投影的夹角。

    • 遍历球谐函数的各个基函数
    for (int l = 0; l <= SHOrder; l++){
        for (int m = -l; m <= l; m++){
            float sh = sh::EvalSH(l, m, phi, theta);
            float delta = CalcArea((float)x, (float)y, width, height);
            SHCoeffiecents[l*(l+1)+m] += Le * sh * delta;
        }
    }
    C#

    无阴影的漫反射项

    scene->getIntegrator()->preprocess(scene); 计算 Diffuse Unshadowed 的情况。化简渲染方程,将上节的球谐函数代入进一步计算为BRDF的球谐投影的系数。其中关键的函数是ProjectFunction。我们要为这个函数编写一个lambda表达式,用于计算传输函数项 。

    分析

    对于漫反射传输项,可以分为三种情况考虑:有阴影的无阴影的相互反射的

    首先考虑最简单的没有阴影的情况。我们有渲染方程

    其中,

    • 是入射辐射度。
    • 是几何函数,表面的微观特性和入射光的方向有关。
    • 是入射光方向。

    对于表面处处相等的漫反射表面,我们可以简化得到 Unshadowed 光照方程

    其中:

    • 是点 的漫反射出射辐射度。
    • 是表面法线。

    入射辐射度 和传输函数项 相互独立,因为前者代表场景中光源的贡献,后者表示表面如何响应入射的光线。因此将这两个部分独立处理。

    具体到运用球谐函数近似是,我们分别对这两项展开。前者的输入是光的入射方向,后者输入的是反射(或者出射方向),并且展开是两个系列的数组,因此我们使用名为查找表(Look-Up Table,简称LUT)的数据结构。

    auto shCoeff = sh::ProjectFunction(SHOrder, shFunc, m_SampleCount);
    C#

    其中,最重要的是上面这个函数 ProjectFunction 。我们要为这个函数编写一个Lambda表达式(shFunc)作为传参,表达式用于计算传输函数项 。

    ProjectFunction 函数传参:

    • 球谐阶数
    • 需要投影在基函数上的函数(我们需要编写的)
    • 采样数

    该函数会取Lambda函数返回的结果投影在基函数上得到系数,最后把各个样本系数累加并乘以权重,最后得出该顶点的最终系数。

    完整代码

    计算几何项,即传输函数项 。

    // prt.cpp
    ...
    double H = wi.normalized().dot(n.normalized()) / M_PI;
    if (m_Type == Type::Unshadowed){
        // TODO: here you need to calculate unshadowed transport term of a given direction
        // TODO: 此处你需要计算给定方向下的unshadowed传输项球谐函数值
        return (H > 0.0) ? H : 0.0;
    }
    C#

    总之最后的积分结果要记得除以 ,再传给 m_TransportSHCoeffs 。

    有阴影的漫反射项

    scene->getIntegrator()->preprocess(scene); 计算 Diffuse Shadowed 的情况。这一项多了一个可见项 。

    分析

    Visibility项()是一个非1即0的值,利用 bool rayIntersect(const Ray3f &ray) 函数,从顶点位置到采样方向反射一条射线,若击中物体,则认为被遮挡,有阴影,返回0;若射线未击中物体,则仍然返回 即可。

    完整代码

    // prt.cpp
    ...
    double H = wi.normalized().dot(n.normalized()) / M_PI;
    ...
    else{
        // TODO: here you need to calculate shadowed transport term of a given direction
        // TODO: 此处你需要计算给定方向下的shadowed传输项球谐函数值
        if (H > 0.0 && !scene->rayIntersect(Ray3f(v, wi.normalized())))
            return H;
        return 0.0;
    }
    C#

    总之最后的积分结果要记得除以 ,再传给 m_TransportSHCoeffs 。

    导出计算结果

    nori框架会生成的两个预计算结果的文件。

    添加运行参数:

    ./scenes/prt.xml

    在 prt.xml 中,需要做以下修改,就可以选择渲染的环境光cubemap。另外,模型、相机参数等也可自行修改。

    // prt.xml
    
    <!-- Render the visible surface normals -->
    <integrator type="prt">
        <string name="type" value="unshadowed" />
        <integer name="bounce" value="1" />
        <integer name="PRTSampleCount" value="100" />
    <!--        <string name="cubemap" value="cubemap/GraceCathedral" />-->
    <!--        <string name="cubemap" value="cubemap/Indoor" />-->
    <!--        <string name="cubemap" value="cubemap/Skybox" />-->
        <string name="cubemap" value="cubemap/CornellBox" />
    
    </integrator>
    C#

    其中,标签可选值:

    • type:unshadowed、shadowed、 interreflection
    • bounce:interreflection类型下的光线弹射次数(目前尚未实现)
    • PRTSampleCount:传输项每个顶点的采样数
    • cubemap:cubemap/GraceCathedral、cubemap/Indoor、cubemap/Skybox、cubemap/CornellBox
    img

    上图分别是GraceCathedral、Indoor、Skybox和CornellBox的 unshadowed 渲染结果,采样数是1。

    使用球谐系数着色

    将nori生成的文件手动拖到实时渲染框架中,并且对实时框架做一些改动。

    上一章计算完成后,将对应cubemap路径中的 light.txt 和 transport.txt 拷贝到实时渲染框架的cubemap文件夹中。

    预计算数据解析

    取消 engine.js 中88-114行的注释,这一段代码用于解析刚才添加进来的txt文件。

    // engine.js
    // file parsing
    ... // 把这块代码取消注释
    C#

    导入模型/创建并使用PRT材质Shader

    在materials文件夹下建立文件 PRTMaterial.js 。

    //PRTMaterial.js
    
    class PRTMaterial extends Material {
        constructor(vertexShader, fragmentShader) {
            super({
                'uPrecomputeL[0]': { type: 'precomputeL', value: null},
                'uPrecomputeL[1]': { type: 'precomputeL', value: null},
                'uPrecomputeL[2]': { type: 'precomputeL', value: null},
            }, 
            ['aPrecomputeLT'], 
            vertexShader, fragmentShader, null);
        }
    }
    
    async function buildPRTMaterial(vertexPath, fragmentPath) {
        let vertexShader = await getShaderString(vertexPath);
        let fragmentShader = await getShaderString(fragmentPath);
    
        return new PRTMaterial(vertexShader, fragmentShader);
    }
    C#

    然后在 index.html 里引入。