zl程序教程

您现在的位置是:首页 >  其它

当前栏目

基于Object3D 实现光线追踪【100010749】

实现 基于 追踪
2023-09-11 14:17:49 时间

光线追踪作业

一、光线追踪

采蒙特卡罗路径追踪算法,通过多次采样从相机发出的光线并追踪其路径,计算路径上的发光、 反射、折射等带来的颜权重,最后求平均以求解物体表的渲染程,这个过程求解的渲染程结果 是偏的,缺点就是收敛速度极慢,采样数直接影响了渲染结果的质量,采样数的增加会导致渲染时 增加。且由于只追踪从相机发出的光线(即单向路径追踪,区分于双向路径追踪),导致当光源积 很时很难收敛,也难以模拟焦散等特征。

实现路径追踪的基础算法时主要参考了 smallpt,同时借助 SmallPT —— 99 代码光线追踪解析以 理解算法。实现了漫反射、镜反射与折射三种表材质类型。

光线追踪中到的求交逻辑除了参数曲外均基于前次 PA。

代码 include/pathtracer.cuh

二、景深、软阴影、抗锯与贴图

景深 原先的针孔相机模型中,相机(发出初始光线的位置)即为空间中个点;在模拟景深,即带光 圈的相机时,相机(发出初始光线的位置)可以是光圈位置的任何个点,所以通过指定光圈,随 机化地选择初始出射光线来模拟景深,为此另外定义个焦距参数。 在与相机所在位置的距离等于焦距的平上,物体应该清晰可,光圈中随机发出的光线需要满 这特征。采的法是,先在光圈内随机产个点作为光线的出射点,然后计算这个光线的向, 使得其能刚好射向焦点,出射点加向即构成完整的出射光线。 景深相机的代码 include/camera.cuh 的 getRay 部分

软阴影 通过对光源采样,被物体阻挡的部分并不是完全法采样到光照,是随被遮挡的程度逐渐加深 呈现出逐渐变深的阴影,因此可以得到带有渐变过渡的阴影。在实现中由于不定义光线,直接给物 体材质加上发光这特征,因此可以便地实现光源,即然实现了软阴影。 不附上具体代码,实现结合在 pt 当中

抗锯 参考 smallpt,实现的是 SSAA*4 抗锯,即将每个像素分为四个像素进采样,采样完毕后将四 个结果取平均作为对该像素本次采样的结果。SSAA 的优点是实现常简单,但相应的增加了数倍采样次 数,增加了渲染开销。 代码 main.cu 的 renderPixel 部分,每个像素都分成四个像素渲染再压缩

贴图 实现了平与球体类的纹理映射。对于平,采了指定区域平铺 + 拉伸的映射法,在创建平 时可以选择个平铺的基准点以及平铺的两个向向量,向的度同时也决定了材质的拉伸程度。对 于球体,采了墨卡托投影,直接通过交点处的法向换算出 uv 空间坐标。

在材质类中能够通过图创建并保存颜、发光、表材质类型与梯度信息四个矩阵,其中颜、 发光直接通过读 ppm 图的 rgb 数据得到;表材质的输图也为 ppm,判断每个像素上 RGB 中权 重最的值,如果 R 的权重则存为漫反射(默认),G 的权重则为反射,B 的权重则为折射;梯度 信息的输为灰度图,通过像素间的灰度变化(实际上为了计算便,只采了 R 通道)算出 u,v 向的 梯度(区间为 ),于计算凹凸贴图的法向,最终法向 的计算公式为

其中 是该点原来的法向

每次求交时,球体和平会计算出交点在 空间上的 uv 坐标,换算成材质类中的矩阵坐 标后取出对应点的材质信息,于后续计算。

材质主要代码 include/material.cuh include/plane.cuh 和 include/sphere.cuh 中有相关的映射逻辑(计算 uv 坐标),均在对应 类的 intersect 函数中 渲染效果

  1. 球的颜及凹凸贴图、平凹凸贴图(两侧砖墙)、表材质类型贴图(地) 2560x1440 每个像素采样 800 次 664 秒

景深,三个 100k 模型(材质分别为带颜的折射、带颜的反射、漫反射) 2560x1440 每个像素采样 800 次 9169 秒

三、参数曲解析法求交

参数曲线基于 PA3,实现了 Bspline 曲线和 Bezier 曲线,在此基础上增设旋转曲,将 xy 平上的曲 线绕 y 轴旋转以形成参数曲。光线与参数曲的求交采顿迭代法进,对参数曲的参数 , 与光线的参数 三个参数按照习题课 2 上的公式进迭代。

其中光线参数 的初值可由参数曲的包围盒与光线的相交测试得出, 的初值采暴撒点的 法给出,实现中通过定次数的尝试,每次随机成定义域内的 值并开始迭代,在迭代收敛、参 数超出定义域或是超出最迭代次数后退出迭代,并取所有相交结果中 最的作为最终与参数曲的 相交结果。

在展示时拿出的参数曲结果中,每隔些像素就会出现次光线交不上的情况,且每次渲染都是 样的结果,检查求交过程却找不到问题,卡了很久,最后想到的是写的伪随机算法,且随 机数种是和像素位置相关的,后修改成 curand 后果然解决了问题。教训是随机算法的随机性还是分 重要的。

相关代码 include/curve.cuh (两种参数曲线)及 include/revsurface.cuh , revsurface 的 intersect 包含了解析法求交的过程

渲染效果

全反射酒杯 1920x1080 每个像素采样 1000 次 9653 秒

四、GPU 并加速

由于串的版本中每个像素是独渲染的,具有天然的可并性,于是使 cuda 为每个像素发起 个 thread 进渲染。在将串程序改为 cuda 版本时主要遇到了以下困难

  • cuda 不允许深递归,原本的 pt 算法以及各种树的遍历都写成了递归的。采解决办法是, 对于 pt 算法,由于其层数不深,直接函数模板在编译时展开递归,对于树遍历则改成递归 版本
  • 沿前次 PA 中的框架,CPU 端创建的多态特性法很好地继承到 GPU(虚函数表丢失), 解决办法是直接在 GPU 端完成物体的构造
  • 量数据结构需要重构,stl 与库函数不可,先前 PA 中给的 vecmath 库不可直接使。

这些 均需要动完成移植,因此乎对所有算法相关的代码进了重构。由于时间有限完成仓促, 所以最后实现的版本存在些性能问题且可拓展性不佳 且由于实现的版本中 CPU 发起所有线程后就基本处于闲置状态,资源浪费很,因此加速效果也 没有特别理想,只到了纯 CPU 版的倍平(测试平台为 3700x+RTX2060s,CPU 版采 openmp 16 线 程)

五、光线求交加速

求交的算法加速主要分为两个部分,

是对于场景中的物体,利 AABB 包围盒及层次包围盒的形 式组织,形成树状结构以减少每次求交时需要计算的物体数。

是对于 mesh,利 kdtree 加速求交 过程。 AABB BVH

每个物体都有其 AABB 包围盒,包围盒由物体的最坐标(X,Y,Z 均最)及最坐标(X,Y,Z 均 最)确定,平较为特殊,本次只到了平于坐标平的平,采了指定某个区域为其包围盒的 做法,多个物体之间若要确定个包围盒,则由所有包围盒的最坐标和最坐标决定。

BVH 根据包围盒创建,每个结点包含了个包围盒与组物体的 id 及两个节点指针,标识该结点 的包围盒内的所有物体。先对于整个场景构造个包围盒作为根节点的包围盒,其物体 id 初始化为 包含所有物体。随后通过计算每个包围盒中点坐标(归化后)的 MortonCode,按 MortonCode 为键排序并均匀分割成两部分,两部分包含的物体构成该节点的两个节点,递归地构造整棵 BVH 树直 到每个叶结点都是单独的个物体(BVH 仅在 CPU 上构建次,因此这部分递归不需要展开)。

MortonCode 分割的思路参考了 NVIDA 官提供的 BVH 构建思路,原理为对点在空间中的位置进排序 并做出间隔距离最的划分,由于实现的场景中不包含太多物体(不像 mesh 样有那么多),这 样的构造已经够达成减少不必要求交的的。 在需要对场景求交时,遍历这棵树,剔除所有包围盒不与光线相交的结点,只对可能与光线相交的 物体求交。遍历采了依赖栈的递归法,以便在 GPU 上进。

相关代码 include/AABB.cuh

include/bvh.cuh include/objects.cuh 的 intersect 函数中包含了调 bvh 求交的过程 KD-Tree KD-Tree 加速仅针对 mesh 展开,每个包含 mesh 的物体都包含棵 kdtree,kdtree 实现了对于条 光线,判断其可能与哪些相交的接,以最化每次与 mesh 求交需要遍历的数。 kdtree 的节点定义为包含个包围盒和两个结点指针,同时留出个指针于保存叶结点包围盒 内的所有 id。

构造过程为读个 obj 件并保存所有三,从根节点开始,先构造个包裹所 有的包围盒并保存,再从 X 轴开始,树的每层轮流选择 X,Y,Z 中的个维度进划分,每次划分都让 该维度上分割点两侧的数尽量样多,这两部分分别构成两个节点,递归构造直到达到深度上限 或者每个结点的数于某个阈值。个如果穿过了分割点,则同时被包含在两个结点中。叶 结点中个数组保存该节点内的所有编号,内部结点不保存这个信息,因为求交最终只在叶 结点上进。

树的构造仅在创建场景时完成遍,故可以在 CPU 上进。 在需要对 mesh 求交时,遍历这棵树,剔除所有包围盒不与光线相交的结点,最终选出那些包围盒 与光线相交的叶结点,它们包含的所有与光线求交。遍历采了依赖栈的递归法,以便在 GPU 上进。

相关代码 include/AABB.cuh

include/kdtree.cuh mesh.cuh 的 intersect 函数中包含了调 kdtree 求交的过程

六、代码结构

主要类结构如下,到的数学类基于 Vecmath,此处不列出。每个类的完整代码均在同名头件中

其中右侧的类功能基本与前次 PA 中相同,Material 类中增加了 Texture 类以满贴图

Texture 类的主要功能是根据 uv 坐标返回个包含所有渲染需要的材质信息的数据结 构,该接定义如下

device__ void getMaterialAt(const float u, const float v,
MaterialFeature& ft) {
 if (u < 0 || u > 1 || v < 0 || v > 1) {
 // debug log
 printf("Incorrect u,v: %f %f\n", u, v);
 return;
 }
 int mapped_x = (int)(u * width);
 int mapped_y = (int)(v * height);
 int index = Image::index(mapped_x, mapped_y, width,
height);
 if (emission_map) {
 ft.emission = emission_map[index];
 }
 if (color_map) {
 ft.color = color_map[index];
 }
 if (type_map) {
 ft.type = type_map[index];
  }
 if (bump_map) {
 ft.gradientU = bump_map[index].y;
 ft.gradientV = bump_map[index].z;
 }
}

Threadresource 类来储存些多线程的必要资源,此处可以忽略

Camera 类增加了景深相关的参数

增加的参数为焦距 flength 与光圈 aperture

Camera 类保存了相机位置与图信息,其最主要的功能是产从某个像素点上发 出的光线,接定义如下

__device__ Ray getRay(const int x, const int y, const int sx,
const int sy, uint* Xi) const {
 double rp1 = 2 * rand(Xi),
 dx = rp1 < 1 ? sqrt(rp1) - 1 : 1 - sqrt(2 - rp1);
 double rp2 = 2 * rand(Xi),
 dy = rp2 < 1 ? sqrt(rp2) - 1 : 1 - sqrt(2 - rp2);
 Vector3f d = cx * (((sx + .5 + dx) / 2 + x) / w - .5) +
 cy * (((sy + .5 + dy) / 2 + y) / h - .5) +
 getDirection();
 if (flength == 0) {
 return Ray(getOrigin(), d);
 }
 else {
 d = normalize(d) * flength;
 double rand1 = rand(Xi) * 2 - 1.;
 double rand2 = rand(Xi) * 2 - 1.;
 Vector3f v1 = r1 * rand1 * aperture;
 Vector3f v2 = r2 * rand2 * aperture;
 Vector3f sp = getOrigin() + v1 + v2;
 Vector3f fp = d + getOrigin();
 d = fp - sp;
 return Ray(sp, d);
 }
}

AABB 类即为 AABB 包围盒,被应在物体、bvhtree 和 kdtree 上

其实现了与光线求交、与三形求交(于构建 kdtree)的法 KdTree 作为 Mesh 的成员变量,来加速 Mesh 求交

其求交逻辑主要如下,利个栈来实现递归的树遍历,其中 mark 数组来记录哪些 已经求过交,以避免重复的求交

__device__ bool intersect(const Ray& r, Hit& h, float
tmin, ThreadResource* thread_resource) const {
 BoolBitField *mark = thread_resource->_repeat_mark;
// pre allocated
 int mark_size = thread_resource->_mark_size;
 memset(mark, 0, sizeof(BoolBitField) * mark_size);
 bool result = false;
 KdNode* stack[MAX_KDTREE_DEPTH * 2];
 KdNode** stackPtr = stack;
 *(stackPtr++) = nullptr;
 if (!mainNode->getLeftChild() && !mainNode-
>getRightChild()) {
 intersect(r, h, tmin, mark, mark_size, mainNode,
result);
 }
 KdNode* node = mainNode;
 do {
 KdNode* lchild = node->getLeftChild();
 KdNode* rchild = node->getRightChild();
 bool lcNull = (lchild == nullptr);
 bool rcNull = (rchild == nullptr);
 bool intersectL = lcNull ? false : lchild-
>getBox().isIntersect(r);
 bool intersectR = rcNull ? false : rchild-
>getBox().isIntersect(r);
 if (intersectL && lchild->isLeaf()) {
 intersect(r, h, tmin, mark, mark_size,
lchild, result);
 }
 if (intersectR && rchild->isLeaf()) {
 intersect(r, h, tmin, mark, mark_size,
rchild, result);
 }
 bool traverseL = (intersectL && !lchild-
>isLeaf());
 bool traverseR = (intersectR && !rchild-
>isLeaf());
 if (!traverseL && !traverseR) {
 node = *(--stackPtr); //pop
 }
 else {
  node = (traverseL) ? lchild : rchild;
 if (traverseL && traverseR) {
 *(stackPtr++) = rchild; //push
 }
 }
 }
 while (node != nullptr);
 return result;
 }

所有物体都是虚基类 Object3D 的派类,Object3D 要求物体拥有 AABB 包围盒与材质的成员 变量,并要求物体必须实现统的求交接

Objects 类负责保存场景中的所有物体,控制其创建与回收,并负责在初始化场景时构建棵 bvhtree 并保存,于后续求交。在光追算法中对场景中物体求交时,仅调 Objects 的 bvhtree 的求交接

♻️ 资源

在这里插入图片描述

大小: 58.7MB
➡️ 资源下载:https://download.csdn.net/download/s1t16/87430272
注:如当前文章或代码侵犯了您的权益,请私信作者删除!