文章目录

  • 原文学习
  • 前言
  • 一、前置条件
    • 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环境贴图
  • 完整代码

原文学习

GPU加速光线追踪

前言

  • 之前跟着上文作者的博客学习了蒙卡罗特路径追踪,在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 //长度12uniform 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] 的属性初始化 ...// 计算 AABBfor (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()) 进行循环(注意 先访问的节点后入栈 ,因为栈的存取顺序是相反的,这样保证下一次取栈顶元素,一定是先被访问的节点。):

    1. 从栈中弹出节点 root
    2. 如果右树非空,将 root 的右子树压入栈中
    3. 如果左树非空,将 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++) {// 随机出射方向 wivec3 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);
  • 加载出现问题:

    1. 图像有点暗,那是因为没有伽马矫正
    2. 图像是反的,待会采样的时候 flip 一下 y 就行了
    3. 图像很扭曲:待会我们用 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;
}

完整代码

原文章中,接下来用自己的方式整理一下原文章作者的代码思路。然后就用这个框架来添加东西。

光线追踪学习:GPU端光线追踪学习相关推荐

  1. 如何挑选深度学习 GPU?

    如何挑选深度学习 GPU? 深度学习是一个对计算有着大量需求的领域,从一定程度上来说,GPU的选择将从根本上决定深度学习的体验.因此,选择购买合适的GPU是一项非常重要的决策.那么2020年,如何选择 ...

  2. GPU 选择 深度学习 图像识别

    GPU 选择 深度学习 图像识别 1.显卡 1.1.Nvidia显卡分类 1.1.1 Geforce系列 1.1.2 Quadro系列 1.1.3 Tesla系列 1.2 GPU几个比较重要的参数 G ...

  3. 老黄投下新核弹:英伟达十年力作图灵架构,新GPU支持光线追踪

    夏乙 发自 凹非寺 量子位 出品 | 公众号 QbitAI "买得越多,省得越多." 今天早上,英伟达CEO黄仁勋,在温哥华的SIGGRAPH会议上,又喊出了他的GPU发布宣言,扔 ...

  4. 独家|让你的GPU为深度学习做好准备(附代码)

    作者:Saurabh Bodhe 翻译:陈振东 校对:车前子 本文约1000字,建议阅读5分钟. 本文讲述了使用NVIDIA官方工具搭建基于GPU的TensorFlow平台的教程. <在谷歌云平 ...

  5. 【移动端DL框架】当前主流的移动端深度学习框架一览

    大家好,继之前的12大深度学习开源框架之后,我们准备开通新的专栏<移动端DL框架>,这是第一篇文章,先来做一个总体的介绍,更多的细节可以关注以后的文章. 在这个专栏中,我们会介绍与移动端的 ...

  6. 含代码 | 支付宝如何优化移动端深度学习引擎?

    阿里妹导读:移动端深度学习在增强体验实时性.降低云端计算负载.保护用户隐私等方面具有天然的优势,在图像.语音.安全等领域具有越来越广泛的业务场景.考虑到移动端资源的限制,深度学习引擎的落地面临着性能. ...

  7. 深度学习-服务端训练+android客户端物体识别实战(caffe入门教程+mobilenet+ncnn+android)

    文章目录 背景 物体识别简介 自动驾驶 淘宝京东使用物体识别技术 公司业务需求 深度学习简介 深度学习的位置 深度学习概念 深度学习优势 深度学习基础知识 感知机 激活函数 多层感知机 卷积神经网络 ...

  8. 英伟达自动驾驶技术:用于自动驾驶汽车的端到端深度学习

    点上方蓝字计算机视觉联盟获取更多干货 在右上方 ··· 设为星标 ★,与你不见不散 仅作学术分享,不代表本公众号立场,侵权联系删除 转载于:机器之心 AI博士笔记系列推荐 周志华<机器学习> ...

  9. 教你如何挑选深度学习GPU

    教你如何挑选深度学习GPU 即将进入 2018 年,随着硬件的更新换代,越来越多的机器学习从业者又开始面临选择 GPU 的难题.正如我们所知,机器学习的成功与否很大程度上取决于硬件的承载能力.在今年 ...

最新文章

  1. Spring-JdbcTemplate(注入到spring容器)-01
  2. SAP Cloud Platform certificate trust下载和business role创建
  3. MySQL Workbench运行脚本
  4. linux下sqlmap安装教程,(转)Sqlmap官网下载与安装教程[windows/linux版本]
  5. 溢信服务转型之代理商技术培训
  6. leetcode第一刷_Unique Binary Search Trees
  7. oracle 安装包 下载、plsql 64位 安装包下载 、 plsql注册码
  8. vep加密视频转换为mp4提取破解录屏教程
  9. ElasticSearch分布式架构原理
  10. PSpice应用B-2
  11. Spring Cloud Gateway 服务网关的部署与使用详细介绍
  12. 组装一台计算机的配置,要不要自己动手组装一台电脑?一文告诉你答案!
  13. 求(2Y-4)²-4(Y-2)(3Y+7)≥0得解
  14. 扇区 物理块 逻辑块 flash 基础概念
  15. 设计模式_迭代器模式01
  16. java基础知识学习小总结(一)
  17. 解题:肯前必肯后,否后必否前
  18. 老鼠也会跟着音乐「蹦迪」,最喜欢Lady Gaga的歌|日本新研究
  19. Jquery3.x高版本支持IE8
  20. 被中国人误传了数千年的七句话 (转)

热门文章

  1. 微信公众号(订阅号)文章阅读数监控V0.1
  2. 分析流量对防御DDOS攻击有何价值?
  3. 2019年CSDN排名前10名大神
  4. 【计算机科学】【2020.05】基于深度学习的计算蛋白质结构预测
  5. html5导航栏向应折叠,超实用!网站导航栏设计形式总结
  6. 基于Java实现的Android拼图游戏设计
  7. DDD 战术模型之聚合
  8. ipcam的几个概念
  9. 百分位(percentile)是什么概念?怎么理解第95个百分位(95th percentile)
  10. combo box使用