文章

Games202(6·作业2)

作业2

框架说明

框架包含两部分,nori的预计算部分,和webgl渲染部分

nori工程是一个开源的用C++编写的简单的教学光线跟踪器,在源码框架下,加入了prt.cpp文件

编译nori项目

steps:

  • 首先进入nori工程
  • mkdir创建目录,以便用于存放构建文件,
  • 进入目录后cmake命令根据CMakeLists.txt 文件(描述项目的构建规则),生成构建文件(例如sln等项目文件)
  • make命令编译链接项目
  • 运行可执行文件(参数为xml文件): ./Debug/nori.exe ../scenes/prt.xml

Error1:

*** No targets specified and no makefile found. Stop.

当在make命令时会出现这个错误,因为使用的vs2019及以上的版本时,cmake .. 调用的时vs自带的编译器MSVC, 它是没办法用mingw的make的

解决方式是用vs打开nori.sln编译链接生成nori.exe

Error2:

如下错误:

1
<lambda_9ed74708f63acbd4deb1a7dc36ea3ac3>::operator()

展开

因为MSVC 对于代码中的中文字符支持有问题(应该是会吞换行符),需要启用 utf-8 编译选项,在 prt/CMakeLists.txt 112 行添加:

target_compile_options(nori PUBLIC /utf-8) # MSVC unicode support

理解prt.xml文件

这里面定义了程序运行需要的一些参数,以便主程序自动识别xml里的参数并传给我们的程序

type表示漫反射传输项类型,bounce代表反弹次数,PRTSampleCount蒙特卡洛的采样数量,cubemap代表 cubemap的目录存储了六张贴图

运行(可视法线)

1741346335513

sh展开

将RE拆分为L和LT两部分,分别用sh展开,sh分为两部:投影(求出系数数组:球面上被投射函数 * Bi)(类似于向量在笛卡尔坐标3个方向的投影),重建(获得球面上被投射函数: 系数 * Bi)(近似还原)

预计算 L 投影到sh的coeffs

将在prt.cpp中的ProjEnv::PrecomputeCubemapSH()函数实现

1741346328376

想要使用球谐函数来表示环境光,就需要将环境光投影到球谐函数上来得到对应的系数,通过黎曼积分求和计算结果

代码解析

prt.cpp中包含了下面的函数

  • CalcArea(u,v,width,height)来计算cubemap上每个像素所代表的矩形区域投影到单位球面的面积(立体角)

PrecomputeCubemapSH函数

  • 首先创建cubemapDirs[]数组,计算cubemap的6张贴图上每个像素对应的单位方向向量,存储在里面
  • 仍通过3重嵌套循环每个像素,
    • 首先获取到像素方向 dir = cubemapDirs[i * width * height + y * width + x],即(前i个像素总量 + y的高度行数 * 像素宽度个 + x的宽度列数)
    • index根据xy找到在image环境贴图中的索引
    • Le环境光,在2维数组中,第i个贴图,index的位置,找到像素对应的环境光值
    • delta_w根据CalcArea求得像素的立体角
    • 获取基函数basic_sh_proj:利用EvalSH函数求得sh在某方向的基函数m(范围-l – l,因为l层有2l + 1个基函数),SHOrder表示sh的阶数
    • 通过GetIndex获取基函数在一维下的索引,并利用黎曼积分公式求得此基函数对应的系数

预计算 diffuse unshadowed LT 投影到sh的coeffs

对于无阴影来说,LT部分仅包含 Mdu=max(Nx·ωi,0),同L部分一样,想要使用sh来表示LT,就需要将LT投影到sh上来得到对应的系数

因为是采样求和,会考虑n_samples次,每次计算Mdu值,如果>0说明射线在半球上从而考虑它,

循环n_coeff系数次,把Mdu投影到sh上,获得本次result,乘以(权重/采样数)获得系数

代码实现

应在PRTIntegrator::preprocess(const Scene *scene)实现

  • 框架为我们完成了,调用PrecomputeCubemapSH计算L部分的系数存储在envCoeffs,创建了m_LightCoeffs数组并转移到其中
  • 框架为我们完成了,遍历每个顶点(着色点p),执行一次ProjectFunction,它将计算一系列的LT部分的系数
  • 框架为我们完成了,使用ProjectFunction,内部会根据采样数量来选取采样方向sample_side,会循环采样次(每个顶点发射数道wi),方向传递给shFun作为射线方向wi,根据shFunc计算的LT项投影到EvalSH()基函数,计算系数存储在shCoeff,
  • 每个顶点获得入射向量wi,如果是 Type::Unshadowed要在内部实现计算LT
  • 需要完成的部分:shFunc(lambda)(被投射函数) = 射线方向wi和法线点乘获得H,即Mdu,如果H > 0.0 ? H : 0(说明在法线半球中)
  • 框架为我们完成了,shCoeff最终会转移到m_TransportSHCoeffs中

另外incident 即wi表示入射 outgoing 即wo表示出射

预计算 diffuse shadowed LT 投影到sh的coeffs

有阴影的LT部分和无阴影的部分几乎一致,但要考虑V项(非0即1),Mdu = V(ωi)max(Nx·ωi,0)

在算法中,加入if判断当前射线是否和其他物体相交,如果没有相交,说明未被遮挡,即光源可以照射到因此并非阴影,考虑接下来的计算

代码实现

  • 框架为我们完成了:如果是 Type::shadowed要在内部实现,要在else部分实现计算LT
  • 需要完成的部分:不仅要看H是否>0,好要满足rayIntersect()从顶点位置到采样方向反射一条射线,如果h > 0 && 未被遮挡(无阴影)就 ? H : 0

预计算 diffuse inter-reflection LT 投影到sh的coeffs

LDI = LDS + ρ/π 积分 L^(x′, ωi) (1−V(ωi)) max(Nx·ωi,0)dωi

  • lds直接光照部分,我们在刚才预计算完成了,右边部分是间接光照部分,
  • 如果p点发射的光线有交点(1−V(ωi),之前直接光照部分,我们忽略了被遮挡的部分,这部分用来做间接光照),则在交点处求出重心坐标插值后的球谐系数,这个系数就表示间接光照的球谐系数
  • 根据简介光 乘以几何项Nx·ωi

代码实现

  • 要在prt.cpp文件中Type::Interreflection部分实现,和非多次bound一样,都需要遍历每个每个顶点(着色点p),求得系数存储到m_TransportSHCoeffs
  • 新建函数computeInterreflectionSH递归函数中实现计算所有间接光的球谐系数,接受顶点位置、法线、bound次数的传入
    • 返回条件为到达bounces的次数
    • 模仿ProjectFunction发射射线(根据sample射线次数,根据a,b,p,t四个值计算射线方向),即获得wi
    • 和之前一样依旧要看h > 0 (max部分)&& 被遮挡(是间接光照部分)(V部分),则可以获取到hit点处的信息Intersection(法线,索引,位置,),
    • nextBouncesCoeffs = 递归计算从hit位置间接光照的球谐系数
    • interpolateSH = 本次的球谐系数,插值:xzy分量 * bary中心坐标分量
    • 两次的系数会相加作为最终球谐系数 * (Nx·ωi )

编译预计算部分

完成了预计算部分,就是将GI部分展开到sh的系数求出,我们再次编译它, 时间有点就,这是debug结果(如果预计算部分正确的话),并且可以查看light.txt和transport.txt文件,这些就是计算的L和LT部分的系数

  • PRTIntegrator::Li(const Scene *scene, Sampler *sampler, const Ray3fray)实现了可视化球谐系数结果的方法,它可以让我们debug预计算部分

1741413124814

实时GI

webgl部分

要渲染GI回到webgl框架,

  • 创建新材质,PRTMaterial.js,构造函数文档攻速需要将uPrecomputeL[3]传给shader
  • 修改index.html
  • loadOBJ.js中加入PRTMaterial材质选项
  • engine.js添加使用PRTMaterial材质的mary模型(webgl工程还没有模型)
  • WebGLRenderer渲染器中给材质传入uniform 的uPrecomputeL[3]在prt中预计算的值(对于LT部分是aPrecomputeLT)
  • 编写对应的shader,
    • 在vs中,循环3次(rgb)每次对L和LT做点乘,获得vColor,
    • fs中很简单,把vColor输出就行

注意:

  • 框架nori部分,用cmake构建后,会将系数保存到 light.txt(光照球谐系数数据)和transport.txt(传输线球谐系数数据)两个文件(在prt/scenes/cubemap/indoor/)中
  • 在engin.js实现了读取分割并保存为Array的算法,它将数据储存在了两个全局变量precomputeL、precomputeLT数组中,
  • 我们需要将这部分代码取消注释——engin.js中的88-114行
  • 另外还需要将两个txt拷贝到webgl的(assets/cubemap/indoor/)中

运行

在右上的gui是可以切换场景的,对于CornellBox背景只需要简单改一下engine.js的路径支持

环境光球谐旋转

我们将使用简单的低阶SH快速旋转方法,以便在旋转环境光同时旋转sh,以便使得环境光和模型仍能够正确对应

sh的旋转性质:

  • R(f(x))=f(R(x))对原函数f(x)的旋转R()与直接旋转f(x)的自变量x是一样的
  • 可以分别在sh每层上面进行旋转,并且这个旋转是线性变化

推论:

根据旋转性质,sh某个vec ni在sh函数投影,得到系数函数p(ni),因此: M P(ni)=P(R(ni)), 对原函数P(ni)旋转M矩阵,与直接R(旋转函数)旋转自变量ni是一样的

对于sh函数的某层l,我们又知道每层共有2l + 1 个 vector,从-l —— l,对每个系数旋转:M[P(n−l), …, P(nl)] = [P(R(n−l)), …, P(R(nl))]

记A = [P(n−l), …, P(nl)],则M = [P(R(n−l)), …, P(R(nl))] A^−1

实现思路

  • A = 2l + 1 个vec n, 投影到sh 获得2l + 1 个向量P(ni),总计2l+ 1个2l+ 1,得到矩阵A
  • 给定旋转R,对所有n依次做旋转R(ni)、,投影道sh 获得2l + 1 个向量P(R(ni)), 总计2l+ 1个2l+ 1,得到矩阵S
  • 根据M = S A^−1,我们将得到M结果, 用M乘以li层的 vec, 就可以得到此层的 新系数
  • 对每层都这样做

注意I

  • SHEval()函数,可以帮助你轻松的获得一个三维向量在指定阶数,投影到球谐函数上的系数
  • 并将旋转后的precomputeL传入Shader

代码实现

  • 解开WebGLRenderer.js中的第53行代码注释,开启环境光旋转
  • 解开getRotationPrecomputeL的注释,以便将sh表示的预计算L传给材质
  • 实现tools.js中的函数
    • getRotationPrecomputeL
      • mat4.invert计算rotationMatrix的逆矩阵,保存为r,传输给computeSquareMatrix
      • 通过computeSquareMatrix,求得了2层的M旋转矩阵
      • 通过M * 每层的precompute_L,的到新系数,两者重新拼接为result数组
    • computeSquareMatrix_3by3是求第一阶的3x3旋转矩阵
      • 选取2l + 1 也就是3个 vec,
      • 利用SHEval函数求得每个vec对应的3个系数
      • 组成矩阵A
      • 利用rotationMatrix旋转3个 vec
      • 再次利用SHEval函数求得每个vec对应的3个旋转的系数
      • 再次组成矩阵S
      • math.multiply(S, A_inverse) S * A的逆矩阵,得到了3阶M 旋转矩阵
    • computeSquareMatrix_5by5求第二阶的5x5旋转矩阵
      • 5阶同理 ……
本文由作者按照 CC BY 4.0 进行授权
本页总访问量