文章目录
- 原文学习
- 前言
- 一、前置条件
- 1.内容
- 2.难点
- 二、前置代码(sheder和三角形等设置)
- 1.画面渲染
- 2.Shader的使用
- 3.材质信息
- 4.在 shader 中进行三角形求交
- 5.相机配置
- 三、使用线性化的BVH树进行优化
- 1. 构建BVH
- 2. BVH 数据传送到 shader
- 3. 和 AABB 盒子求交
- 4. 非递归遍历 BVH 树
- 四、开始光线追踪
- 1. 原理
- 2. 辅助函数
- 3. pathTracing 的实现
- 4. 多帧混合与后处理
- 5. 增加HDR环境贴图
- 完整代码
前言
- 之前跟着上文作者的博客学习了蒙卡罗特路径追踪,在CPU端模拟实现光追效果图片。但是渲染消耗过大,如果想要实现的更好效果需要做到使用BVH加速遍历效果以及在GPU端实现光线追踪。
- 大概思路就是将OPENGL中的片段着色器逐个像素的计算光追,然后将三角形信息以及BVH加速效果和光线投射技术实现到shader中。而shader中的color使用光线投射技术。
- 接下来继学习作者的GPU端实现光线追踪的效果。
一、前置条件
1.内容
- OpenGL
- GLSL
- 路径追踪:一个点的颜色是通过渲染方程进行积分求解。每次积分逐像素递归求解光路直到碰到光源为止。
- BVH加速盒
2.难点
- shader中的信息交流
- BVH加速在shader中不能使用指针技术,所以只能用线性二叉树的方式来实现BVH
二、前置代码(sheder和三角形等设置)
1.画面渲染
- 上下为[-1,1]的画面中:
2.Shader的使用
- 我们的数据通常是以 数组 形式进行传送,比如三角形数组,BVH 二叉树数组,材质数组等等。这些数组都是一维的,以方便我们用 下标 指针进行访问和采样。
- 这里使用的是Buffer Texture:它允许我们直接将内存中的二进制数据搬运到显存中,然后通过一种特殊的采样器,也就是 samplerBuffer 来访问。
和一般的 sampler2D 不同,samplerBuffer 将纹理的内容(即显存中的原始数据)视为一维数组,可以通过 下标直接索引 数据,并且不会使用任何过滤器这刚好满足我们的需要!
Buffer Texture 的使用方式如下(示例):
int n; // 数组大小
float triangles[];
//创建一个缓冲区对象,叫做 texture buffer object,简称 tbo,这可以类比为显存中开辟
GLuint tbo;
glGenBuffers(1, &tbo);
glBindBuffer(GL_TEXTURE_BUFFER, tbo);
glBufferData(GL_TEXTURE_BUFFER,
n * sizeof(float), &your_data[0], GL_STATIC_DRAW);//然后将数据塞进缓冲区中:
//随后创建一块纹理,注意这时的纹理类型应该为 GL_TEXTURE_BUFFER 这表示我们开辟的不是图像纹理而是数据缓冲区纹理:
GLuint tex;
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_BUFFER, tex);
//用 glTexBuffer 将 tbo 中的数据关联到 texture buffer
//这里我们使用 GL_RGB32F 的格式,这样一次访问可以取出一个 vec3 向量的数据。
//采样器的返回值有 RGB 三个通道,每个通道都是 32 位的浮点数:
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGB32F, tbo);
glActiveTexture(GL_TEXTURE0);//最后传送 0 号纹理到着色器:
glUniform1i(glGetUniformLocation(program, "triangles"), 0);
在着色器端使用 texelFetch 和一个整数下标 index 进行 samplerBuffer 类型的纹理的查询:
uniform samplerBuffer triangles;
...
int index = xxx
vec3 data = texelFetch(triangles, index).xyz;
- 这里的数据格式 GL_RGB32F 指的是一个下标(一次采样)能读取到多少数据,即一格数据的单位。一个下标将会索引三个 32 位的浮点数,并且返回一个 vec4,但是仅有 rgb 分量有效。他们和内存数据的映射关系如下:
也可以使用 GL_R32F 来每次读取一个 32 位浮点数,这样能够更加灵活的组织数据但是显然一次读取一个 vec3 效率更高
3.材质信息
- 迪士尼材质原则
//迪士尼规范
// 物体表面材质定义
struct Material {
vec3 emissive = vec3(0, 0, 0); // 作为光源时的发光颜色
vec3 baseColor = vec3(1, 1, 1);
float subsurface = 0.0;
float metallic = 0.0;
float specular = 0.0;
float specularTint = 0.0;
float roughness = 0.0;
float anisotropic = 0.0;
float sheen = 0.0;
float sheenTint = 0.0;
float clearcoat = 0.0;
float clearcoatGloss = 0.0;
float IOR = 1.0;
float transmission = 0.0;
};
// 三角形定义
struct Triangle {
vec3 p1, p2, p3; // 顶点坐标
vec3 n1, n2, n3; // 顶点法线
Material material; // 材质
};
- 编码:
// 读取三角形
std::vector<Triangle> triangles;
readObj()
int nTriangles = triangles.size();
...
// 编码 三角形, 材质
std::vector<Triangle_encoded> triangles_encoded(nTriangles);
for (int i = 0; i < nTriangles; i++) {
Triangle& t = triangles[i];
Material& m = t.material;
// 顶点位置
triangles_encoded[i].p1 = t.p1;
triangles_encoded[i].p2 = t.p2;
triangles_encoded[i].p3 = t.p3;
// 顶点法线
triangles_encoded[i].n1 = t.n1;
triangles_encoded[i].n2 = t.n2;
triangles_encoded[i].n3 = t.n3;
// 材质
triangles_encoded[i].emissive = m.emissive;
triangles_encoded[i].baseColor = m.baseColor;
triangles_encoded[i].param1 = vec3(m.subsurface, m.metallic, m.specular);
triangles_encoded[i].param2 = vec3(m.specularTint, m.roughness, m.anisotropic);
triangles_encoded[i].param3 = vec3(m.sheen, m.sheenTint, m.clearcoat);
triangles_encoded[i].param4 = vec3(m.clearcoatGloss, m.IOR, m.transmission);
}
- 利用 texture buffer 传送到 shader 中,这里创建 texture buffer object,然后将数据导入 tbo,然后创建纹理,将 tbo 和纹理绑定:
GLuint trianglesTextureBuffer;//创建数据缓冲区纹理
GLuint tbo0;//缓冲区对象
glGenBuffers(1, &tbo0);
glBindBuffer(GL_TEXTURE_BUFFER, tbo0);//绑定缓冲区对象
glBufferData(GL_TEXTURE_BUFFER, triangles_encoded.size() * sizeof(Triangle_encoded),
&triangles_encoded[0], GL_STATIC_DRAW);//将数据放入缓冲区
glGenTextures(1, &trianglesTextureBuffer);
glBindTexture(GL_TEXTURE_BUFFER, trianglesTextureBuffer);//绑定缓冲区纹理
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGB32F, tbo0);//用 glTexBuffer 将 tbo 中的数据关联到 texture buffer
- 在shader中解码数据:
#define SIZE_TRIANGLE 12 //长度12
uniform samplerBuffer triangles;
...
// 获取第 i 下标的三角形
Triangle getTriangle(int i) {
int offset = i * SIZE_TRIANGLE;
Triangle t;
// 顶点坐标
t.p1 = texelFetch(triangles, offset + 0).xyz;
t.p2 = texelFetch(triangles, offset + 1).xyz;
t.p3 = texelFetch(triangles, offset + 2).xyz;
// 法线
t.n1 = texelFetch(triangles, offset + 3).xyz;
t.n2 = texelFetch(triangles, offset + 4).xyz;
t.n3 = texelFetch(triangles, offset + 5).xyz;
return t;
}
// 获取第 i 下标的三角形的材质
Material getMaterial(int i) {
Material m;
int offset = i * SIZE_TRIANGLE;
vec3 param1 = texelFetch(triangles, offset + 8).xyz;
vec3 param2 = texelFetch(triangles, offset + 9).xyz;
vec3 param3 = texelFetch(triangles, offset + 10).xyz;
vec3 param4 = texelFetch(triangles, offset + 11).xyz;
m.emissive = texelFetch(triangles, offset + 6).xyz;
m.baseColor = texelFetch(triangles, offset + 7).xyz;
m.subsurface = param1.x;
m.metallic = param1.y;
m.specular = param1.z;
m.specularTint = param2.x;
m.roughness = param2.y;
m.anisotropic = param2.z;
m.sheen = param3.x;
m.sheenTint = param3.y;
m.clearcoat = param3.z;
m.clearcoatGloss = param4.x;
m.IOR = param4.y;
m.transmission = param4.z;
return m;
}
4.在 shader 中进行三角形求交
- 定义:
// 光线
struct Ray {
vec3 startPoint;
vec3 direction;
};
// 光线求交结果
struct HitResult {
bool isHit; // 是否命中
bool isInside; // 是否从内部命中
float distance; // 与交点的距离
vec3 hitPoint; // 光线命中点
vec3 normal; // 命中点法线
vec3 viewDir; // 击中该点的光线的方向
Material material; // 命中点的表面材质
};
- 求交方式大体和之前的光线投射相似:首先是求解光线和三角形所在平面的距离 t,有了距离顺势求出交点 P。求出交点之后,判断交点是否在三角形内。这里通过叉乘的方向和法相是否同向来判断。如果三次叉乘都和 N 同向,说明 P 在三角形中
#define INF 114514.0
// 光线和三角形求交
HitResult hitTriangle(Triangle triangle, Ray ray) {
HitResult res;
res.distance = INF;
res.isHit = false;
res.isInside = false;
vec3 p1 = triangle.p1;
vec3 p2 = triangle.p2;
vec3 p3 = triangle.p3;
vec3 S = ray.startPoint; // 射线起点
vec3 d = ray.direction; // 射线方向
vec3 N = normalize(cross(p2-p1, p3-p1)); // 法向量
// 从三角形背后(模型内部)击中
if (dot(N, d) > 0.0f) {
N = -N;
res.isInside = true;
}
// 如果视线和三角形平行
if (abs(dot(N, d)) < 0.00001f) return res;
// 距离
float t = (dot(N, p1) - dot(S, N)) / dot(d, N);
if (t < 0.0005f) return res; // 如果三角形在光线背面
// 交点计算
vec3 P = S + d * t;
// 判断交点是否在三角形中
vec3 c1 = cross(p2 - p1, P - p1);
vec3 c2 = cross(p3 - p2, P - p2);
vec3 c3 = cross(p1 - p3, P - p3);
bool r1 = (dot(c1, N) > 0 && dot(c2, N) > 0 && dot(c3, N) > 0);
bool r2 = (dot(c1, N) < 0 && dot(c2, N) < 0 && dot(c3, N) < 0);
// 命中,封装返回结果
if (r1 || r2) {
res.isHit = true;
res.hitPoint = P;
res.distance = t;
res.normal = N;
res.viewDir = d;
// 根据交点位置插值顶点法线
float alpha = (-(P.x-p2.x)*(p3.y-p2.y) + (P.y-p2.y)*(p3.x-p2.x)) / (-(p1.x-p2.x-0.00005)*(p3.y-p2.y+0.00005) + (p1.y-p2.y+0.00005)*(p3.x-p2.x+0.00005));
float beta = (-(P.x-p3.x)*(p1.y-p3.y) + (P.y-p3.y)*(p1.x-p3.x)) / (-(p2.x-p3.x-0.00005)*(p1.y-p3.y+0.00005) + (p2.y-p3.y+0.00005)*(p1.x-p3.x+0.00005));
float gama = 1.0 - alpha - beta;
vec3 Nsmooth = alpha * triangle.n1 + beta * triangle.n2 + gama * triangle.n3;
Nsmooth = normalize(Nsmooth);
res.normal = (res.isInside) ? (-Nsmooth) : (Nsmooth);
}
return res;
}
然后我们编写一个函数,暴力遍历三角形数组进行求交,返回最近的交点:
#define INF 114514.0
// 暴力遍历数组下标范围 [l, r] 求最近交点
HitResult hitArray(Ray ray, int l, int r) {
HitResult res;
res.isHit = false;
res.distance = INF;
for(int i=l; i<=r; i++) {
Triangle triangle = getTriangle(i);
HitResult r = hitTriangle(triangle, ray);
if(r.isHit && r.distance<res.distance) {
res = r;
res.material = getMaterial(i);
}
}
return res;
}
5.相机配置
- 相机位于 vec3(0, 0, 4),看向 z 轴负方向,根据画布像素的 NDC 坐标来投射射线。这里投影平面长宽均为 2.0,而 zNear 为 2.0,这保证了 50° 左右的视场角:
Ray ray;
ray.startPoint = vec3(0, 0, 4);
vec3 dir = vec3(pix.xy, 2) - ray.startPoint;
ray.direction = normalize(dir);
三、使用线性化的BVH树进行优化
1. 构建BVH
虽然可以成功遍历三角形,但是我们需要更加高效的遍历,需要使用到。但是在 GLSL 中 没有指针 这一概念,我们需要将使用 指针 的树形结构改为使用 数组下标 作为指针的线性化二叉树。(计算下标来代替指针)
- 原来的 BVH 节点结构体,内容分为三部分,分别是左右孩子,AABB 碰撞盒,叶子节点信息,其中 AA 为极小点,BB 为极大点。因为不能使用指针 所以只能用数组下标。
// BVH 树节点
//这里还引入了一个小变化:一个叶子节点可以保存多个三角形
//n 表示该叶子节点的三角形数目,index 表示该节点第一个三角形
struct BVHNode {
int left, right; // 左右子树索引
int n, index; // 叶子节点信息
vec3 AA, BB; // 碰撞盒
};
线性化二叉树也很简单,只需要每次创建节点的时候,将 new Node() 改为 push_back() 即插入数组,而下标的索引方式是照常的。
// 构建 BVH
int buildBVH(std::vector<Triangle>& triangles, std::vector<BVHNode>& nodes, int l, int r, int n) {
if (l > r) return 0;
// 注:
// 此处不可通过指针,引用等方式操作,必须用 nodes[id] 来操作
// 因为 std::vector<> 扩容时会拷贝到更大的内存,那么地址就改变了
// 而指针,引用均指向原来的内存,所以会发生错误
nodes.push_back(BVHNode());
int id = nodes.size() - 1; // 注意: 先保存索引
nodes[id] 的属性初始化 ...
// 计算 AABB
for (int i = l; i <= r; i++) {
... // 遍历三角形 计算 AABB
}
// 不多于 n 个三角形 返回叶子节点
if ((r - l + 1) <= n) {
nodes[id].n = r - l + 1;
nodes[id].index = l;
return id;
}
// 否则递归建树
// 按 x,y,z 划分数组
std::sort(...)
// 递归
int mid = (l + r) / 2;
int left = buildBVH(triangles, nodes, l, mid, n);
int right = buildBVH(triangles, nodes, mid + 1, r, n);
nodes[id].left = left;
nodes[id].right = right;
return id;
}
2. BVH 数据传送到 shader
struct BVHNode_encoded {
vec3 childs; // (left, right, 保留)
vec3 leafInfo; // (n, index, 保留)
vec3 AA, BB;
};
shader 中解码 BVHNode 的代码
// 获取第 i 下标的 BVHNode 对象
BVHNode getBVHNode(int i) {
BVHNode node;
// 左右子树
int offset = i * SIZE_BVHNODE;
ivec3 childs = ivec3(texelFetch(nodes, offset + 0).xyz);
ivec3 leafInfo = ivec3(texelFetch(nodes, offset + 1).xyz);
node.left = int(childs.x);
node.right = int(childs.y);
node.n = int(leafInfo.x);
node.index = int(leafInfo.y);
// 包围盒
node.AA = texelFetch(nodes, offset + 2).xyz;
node.BB = texelFetch(nodes, offset + 3).xyz;
return node;
}
投射光线
...
for(int i=0; i<nNodes; i++) {
BVHNode node = getBVHNode(i);
if(node.n>0) {
int L = node.index;
int R = node.index + node.n - 1;
HitResult res = hitArray(ray, L, R);
if(res.isHit) fragColor = vec4(res.material.color, 1);
}
}
3. 和 AABB 盒子求交
- 对于轴对齐包围盒,光线穿入穿出 xoy,xoz,yoz 平面,会有三组穿入点穿出点。如果找到一组穿入点穿出点,使得光线起点距离穿入点的距离 小于 光线起点距离穿出点的距离,即
t0 < t1
则说明命中
取 out 中最小的距离记作 t1,和 in 中最大的距离记作 t0,然后看是否 t1 > t0 如果满足等式,则说明命中: - GLSL求交代码(n 即近交点 near,也就是 in
f 即远交点 far,也就是 out)
// 和 aabb 盒子求交,没有交点则返回 -1
//n 即近交点 near,也就是 in,f 即远交点 far,也就是 out
float hitAABB(Ray r, vec3 AA, vec3 BB) {
vec3 invdir = 1.0 / r.direction;
vec3 f = (BB - r.startPoint) * invdir;
vec3 n = (AA - r.startPoint) * invdir;
vec3 tmax = max(f, n);
vec3 tmin = min(f, n);
float t1 = min(tmax.x, min(tmax.y, tmax.z));
float t0 = max(tmin.x, max(tmin.y, tmin.z));
return (t1 >= t0) ? ((t0 > 0.0) ? (t0) : (t1)) : (-1);
}
- 测试代码:对于 BVH 的根节点(1 号节点)我们分别和其左右子树求交,如果左子树命中则返回红色,右子树命中则返回绿色,两个都命中则返回黄色:
...
BVHNode node = getBVHNode(1);
BVHNode left = getBVHNode(node.left);
BVHNode right = getBVHNode(node.right);
float r1 = hitAABB(ray, left.AA, left.BB);
float r2 = hitAABB(ray, right.AA, right.BB);
vec3 color;
if(r1>0) color = vec3(1, 0, 0);
if(r2>0) color = vec3(0, 1, 0);
if(r1>0 && r2>0) color = vec3(1, 1, 0);
...
4. 非递归遍历 BVH 树
- 因为在GPU 上面没有栈的概念,也不能执行递归程序,所以要认为写出 BVH二叉树的遍历代码,自定义栈。
对于 BVH 树,在和 根 节点求交 之后 ,我们总是查找它的左右子树,这相当于二叉树的先序遍历
- 通过维护一个栈来保存节点。首先将树根入栈,然后
while(!stack.empty())
进行循环(注意 先访问的节点后入栈 ,因为栈的存取顺序是相反的,这样保证下一次取栈顶元素,一定是先被访问的节点。):
- 从栈中弹出节点 root
- 如果右树非空,将 root 的右子树压入栈中
- 如果左树非空,将 root 的左子树压入栈中
- 通过使用数组与下标来模拟栈来完成BVH盒子求交操作。
遍历 BVH 求交
// 遍历 BVH 求交
HitResult hitBVH(Ray ray) {
HitResult res;
res.isHit = false;
res.distance = INF;
// 栈
int stack[256];
int sp = 0;
stack[sp++] = 1;
while(sp>0) {
int top = stack[--sp];
BVHNode node = getBVHNode(top);
// 是叶子节点,遍历三角形,求最近交点
if(node.n>0) {
int L = node.index;
int R = node.index + node.n - 1;
HitResult r = hitArray(ray, L, R);
if(r.isHit && r.distance<res.distance) res = r;
continue;
}
// 和左右盒子 AABB 求交
float d1 = INF; // 左盒子距离
float d2 = INF; // 右盒子距离
if(node.left>0) {
BVHNode leftNode = getBVHNode(node.left);
d1 = hitAABB(ray, leftNode.AA, leftNode.BB);
}
if(node.right>0) {
BVHNode rightNode = getBVHNode(node.right);
d2 = hitAABB(ray, rightNode.AA, rightNode.BB);
}
// 在最近的盒子中搜索
if(d1>0 && d2>0) {
if(d1<d2) { // d1<d2, 左边先
stack[sp++] = node.right;
stack[sp++] = node.left;
} else { // d2<d1, 右边先
stack[sp++] = node.left;
stack[sp++] = node.right;
}
} else if(d1>0) { // 仅命中左边
stack[sp++] = node.left;
} else if(d2>0) { // 仅命中右边
stack[sp++] = node.right;
}
}
return res;
}
这里通过交点的距离判断,优先查找近的盒子,能够大大加速。将原来的暴力查找的 hitArray 换成新的 hitBVH 函数
四、开始光线追踪
1. 原理
渲染方程:
因为光路可逆,沿着 wi 方向 射入 p 点的光的能量,等于从 q 点出发,沿着 wi 方向 射出 的光的能量:
伪代码
每次递归的返回结果都乘以了 f_r * cosine / pdf
,但是对于 shader 中没有递归,可以用循环代替。变量 history 来记录每次递归,返回结果的累乘。
给定一个点 p 的表面信息,即 HitResult 结构体,一个入射光线方向 viewDir 和一个最大弹射次数,然后通过 pathTracing 函数求解 p 点的颜色:
投射光线
...
// primary hit
HitResult firstHit = hitBVH(ray);
vec3 color;
if(!firstHit.isHit) {
color = vec3(0);
} else {
vec3 Le = firstHit.material.emissive;
int maxBounce = 2;
vec3 Li = pathTracing(firstHit, maxBounce);
color = Le + Li;
}
fragColor = vec4(color, 1.0);
2. 辅助函数
一共需要用到3个辅助函数
1. 0 ~ 1 **均匀分布的随机数**的函数
2. 生成**半球均匀分布的随机向量**的函数
3. 任意向量投影到 **法向半球** 的函数
- 首先是 0 ~ 1 均匀分布的随机数:要一个 uniform uint 变量frameCounter (帧计数器)做随机种子,同时还需要 width,height 和当前屏幕像素的 NDC 坐标pix 变量。
uniform uint frameCounter;
uint seed = uint(
uint((pix.x * 0.5 + 0.5) * width) * uint(1973) +
uint((pix.y * 0.5 + 0.5) * height) * uint(9277) +
uint(frameCounter) * uint(26699)) | uint(1);
uint wang_hash(inout uint seed) {
seed = uint(seed ^ uint(61)) ^ uint(seed >> uint(16));
seed *= uint(9);
seed = seed ^ (seed >> 4);
seed *= uint(0x27d4eb2d);
seed = seed ^ (seed >> 15);
return seed;
}
float rand() {
return float(wang_hash(seed)) / 4294967296.0;
}
- 半球均匀分布代码引自 PBRT 13.6ξ 1 和ξ 2 是0-1分布的随机数
// 半球均匀采样
vec3 SampleHemisphere() {
float z = rand();
float r = max(0, sqrt(1.0 - z*z));
float phi = 2.0 * PI * rand();
return vec3(r * cos(phi), r * sin(phi), z);
}
这里半球的 “上方向” 是 z 轴,需要做一次投影来对应到法向半球的法线 N 方向。该部分的代码引自 GPU Path Tracing in Unity – Part 2
// 将向量 v 投影到 N 的法向半球
vec3 toNormalHemisphere(vec3 v, vec3 N) {
vec3 helper = vec3(1, 0, 0);
if(abs(N.x)>0.999) helper = vec3(0, 0, 1);
vec3 tangent = normalize(cross(N, helper));
vec3 bitangent = normalize(cross(N, tangent));
return v.x * tangent + v.y * bitangent + v.z * N;
}
3. pathTracing 的实现
这里我们仅实现漫反射
半球面积为 2 π,这里我们取漫反射的概率密度函数 pdf 为 1 / 2 π ,此外关于 f_r (这里 f_r 实际上是 BRDF,即双向反射分布函数
函数 BRDF(p, wi, wo) 的值,描述了光从 wi 射入 p 点,散射后有多少光能从 wo 射出一个结论是漫反射的 BRDF 就是颜色值除以 pi)这里我们取表面颜色除以 π ,这里姑且看作一个常数
// 路径追踪
vec3 pathTracing(HitResult hit, int maxBounce) {
vec3 Lo = vec3(0); // 最终的颜色
vec3 history = vec3(1); // 递归积累的颜色
for(int bounce=0; bounce<maxBounce; bounce++) {
// 随机出射方向 wi
vec3 wi = toNormalHemisphere(SampleHemisphere(), hit.normal);
// 漫反射: 随机发射光线
Ray randomRay;
randomRay.startPoint = hit.hitPoint;
randomRay.direction = wi;
HitResult newHit = hitBVH(randomRay);
float pdf = 1.0 / (2.0 * PI); // 半球均匀采样概率密度
float cosine_o = max(0, dot(-hit.viewDir, hit.normal)); // 入射光和法线夹角余弦
float cosine_i = max(0, dot(randomRay.direction, hit.normal)); // 出射光和法线夹角余弦
vec3 f_r = hit.material.baseColor / PI; // 漫反射 BRDF
// 未命中
if(!newHit.isHit) {
break;
}
// 命中光源积累颜色
vec3 Le = newHit.material.emissive;
Lo += history * Le * f_r * cosine_i / pdf;
// 递归(步进)
hit = newHit;
history *= f_r * cosine_i / pdf; // 累积颜色
}
return Lo;
}
运行后的结果非常嘈杂,这是因为我们要将每一帧的结果 累加 作为积分的值,而不是单独的取每一个离散的采样,为此需要混合多个帧的绘制结果
4. 多帧混合与后处理
- 使用 defer render 延迟渲染管线
- 需要维护一块纹理 lastFrame 来保存上一帧的图像,同时为了对输出进行后处理(比如伽马矫正,色调映射),我们需要实现一个简单管线:
这里封装一个 RenderPass 类,其中 colorAttachments 是要传入下一 pass 的纹理 id,这些纹理将作为帧缓冲的颜色附件。然后每个 pass 直接调用 draw 就行,其中 texPassArray 是 上一个 pass 的 colorAttachments
class RenderPass {
public:
std::vector<GLuint> colorAttachments;
// 其他属性 ...
void bindData(bool finalPass = false) {
}
void draw(std::vector<GLuint> texPassArray = {}) {
}
};
完成渲染管线后,在pass1的片元着色器增加多帧的混合效果
uniform sampler2D lastFrame;
...
// 和上一帧混合
vec3 lastColor = texture2D(lastFrame, pix.xy*0.5+0.5).rgb;
color = mix(lastColor, color, 1.0/float(frameCounter+1));
5. 增加HDR环境贴图
一般的图片亮度拉满也就 255,但是 HDR 亮度是整个浮点数范围,能够较好的表示现实中的光照,所以用来做环境贴图
- 首先可以在 ploy heaven 上面下载到 HDR 贴图:
- 然后我们需要读取 HDR 图片,SOIL 显然是读不了的(其实有伪 HDR,是通过 RGBE 或者 RGBdivA,RGBdivA2 来实现的,不过似乎有一个 A 通道始终为 128 的 BUG 所以无法使用
这里我们选择一个轻量级的库:HDR Image Loader,它无需安装,只需要 include 一下就可用。它的代码在 这里
#include "lib/hdrloader.h"
...
// hdr 全景图
HDRLoaderResult hdrRes;
bool r = HDRLoader::load("./skybox/sunset.hdr", hdrRes);
GLuint hdrMap = 创建一张纹理()
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, hdrRes.width, hdrRes.height, 0, GL_RGB, GL_FLOAT, hdrRes.cols);
- 加载出现问题:
- 图像有点暗,那是因为没有伽马矫正
- 图像是反的,待会采样的时候 flip 一下 y 就行了
- 图像很扭曲:待会我们用 spherical coord 采样就正常了
我们给定一个向量 v,将其转为采样 HDR图的 纹理坐标 uv,代码参考 stack overflow
// 将三维向量 v 转为 HDR map 的纹理坐标 uv
vec2 SampleSphericalMap(vec3 v) {
vec2 uv = vec2(atan(v.z, v.x), asin(v.y));
uv /= vec2(2.0 * PI, PI);
uv += 0.5;
uv.y = 1.0 - uv.y;
return uv;
}
然后采样HDR贴图
// 获取 HDR 环境颜色
vec3 sampleHdr(vec3 v) {
vec2 uv = SampleSphericalMap(normalize(v));
vec3 color = texture2D(hdrMap, uv).rgb;
//color = min(color, vec3(10));
return color;
}
原作者写的有关HDR亮度的注意的地方
然后将 main 函数中,primary ray 的 miss 的处理中,color = vec3(0)
换为:
color = sampleHdr(ray.direction);
此外,pathTracing 中,ray miss 的时候也要处理:
// 未命中
if(!newHit.isHit) {
vec3 skyColor = sampleHdr(randomRay.direction);
Lo += history * skyColor * f_r * cosine_i / pdf;
break;
}
完整代码
原文章中,接下来用自己的方式整理一下原文章作者的代码思路。然后就用这个框架来添加东西。