光线追踪渲染实战(三):OpenGL 光线追踪,用 GPU 加速计算!

2023-10-27

项目代码仓库:
GitHub:https://github.com/AKGWSB/EzRT
gitee:https://gitee.com/AKGWSB/EzRT

前言

在 GPU 上实现光线追踪!

在这里插入图片描述

在这里插入图片描述



在差不多半年之前,我在 光线追踪渲染实战 中讨论并实现了简单的光线追踪,用 c++ 和多线程在 cpu 上最简单的计算,不过仅支持简单的三角形和球作为图元

时过境迁,摆弄这些精致的球体已不再新颖,为了高效遍历大量三角形,这篇 光线追踪加速遍历结构 学习并且讨论了 BVH 这种加速遍历数据结构,同时给出了 c++ 的简单实现和可视化的代码

如果说前两篇博客是 mc 中的木镐,石镐的话,今天我们就敲把铁镐,顺便把 GPU 放在火上烤一烤:

唔,大概说一下思路把:用的是 OpenGL 的 fragment shader 逐像素计算光追,然后三角形和 BVH 结构还有材质等信息,编码一下塞到纹理里面传给 shader,然后嗯算就完事了

hhh 核心部分很简单,就是把原来的 c++ 代码移植一下,搞到 GLSL 里面运行。emmm 这不是经典的大象塞进冰箱嘛。事实上为此我花费很多时间来解决各种百家争鸣的 bug

好在困难的时期已经过去了,翻过这座山… 好了。。。不扯闲话了,做好准备,我们要再一次进入波与粒的世界!

0. 前置知识

注:
该部分不涉及任何原理,代码讲解
涉猎该部分列出的前置知识,才能比较顺利的阅读本篇博客,以至于看到代码的时候不至于一头雾水 hhh

环境

visual studio 2019,数学库用 glm,然后 OpenGL 用 glew,窗口管理用 freeglut,图像读取用 SOIL2,关于怎么在 vs 上安装第三方库,可以使用 vcpkg 进行包管理,具体用法参考 我之前的博客

OpenGL

因为是基于 GLSL 的实现,那么首先对 OpenGL 得有一定的了解。常见的概念,比如绘制流程,着色器,坐标变换等。常见的操作,比如缓冲区对象,传送纹理,传递 uniform 一致变量等等。关于 OpenGL 的教程,最经典的莫过于 Learn OpenGL 了。当然也顺便推销下我自己的 专栏,尽管它费拉不堪…

GLSL

然后,片段着色器不是简单的输出重心插值后的顶点颜色。我们要在 fragment shader 中洋洋洒洒编写将近 114514e1919 行的代码来进行一个光的追,所以需要了解 GLSL 的语法规范,函数接口,uniform 变量,纹理,采样等操作

简单的几何学

和三角形求交,和轴对齐包围盒 AABB 长方体盒子求交,三角形重心插值等数学几何原理。和三角形求交的原理,和 AABB 求交…

路径追踪

一个点的颜色并不是由魔法确定的,而是通过渲染方程进行积分求解。每次积分逐像素递归求解光路直到碰到光源为止。原理和代码参考:光线追踪渲染实战

1. 布置画布

片段着色器会对每个像素进行一次计算,并且是在 GPU 上并行的,这正好满足我们的要求:并行与逐像素计算!我们用它来计算光线追踪的颜色!

利用片元着色器计算每个像素,首先我们绘制一个正方形铺满整个屏幕,作为我们的画布:

不需要任何的变换矩阵,直接将 6 个顶点固定到 NDC space,即 [-1, 1] 的范围,c++ 的代码很简单:

std::vector<vec3> square = {
    
	vec3(-1, -1, 0), vec3(1, -1, 0), vec3(-1, 1, 0), 
	vec3(1, 1, 0), vec3(-1, 1, 0), vec3(1, -1, 0) 
};

// 生成 vao vbo , 传送数据,绘制

...

顶点着色器直接传递顶点的 NDC 坐标作为像素的片元坐标,pix 变量的范围 [-1, 1],在片段着色器中尝试输出像素的坐标 pix 作为颜色。当看到如下的图像,说明画布有在正常工作:

2. 三角形数据传送到 shader

注:
这部分的代码相当无聊,我们是在为 shader 准备数据
如果您比较关心路径追踪的 GLSL 实现,那么大可跳过该部分

和以往的光线追踪程序不同,计算发生在 GPU 端。向 shader 中传递数据有很多种方式,uniform 数组,uniform buffer 等等,但是这些方式都存在数目限制,无法传送大量的数据

回想我们是怎么将图片传送到 GPU 上的,没错,纹理!我们通过 c++ 将模型数据读取到内存,然后以纹理的形式将数据传送到显存,然后在 shader 中对纹理进行采样就可以读出原来的数据!

我们的数据通常是以 数组 形式进行传送,比如三角形数组,BVH 二叉树数组,材质数组等等。这些数组都是一维的,以方便我们用 下标 指针进行访问和采样

可是众所周知一般的图片纹理是二维的,通过一个 0 ~ 1 范围的 uv 坐标进行采样而不是下标。暂且不谈把一维数组折叠到二维纹理这种奇技淫巧,显卡电路中焊死的硬件双线性插值过滤器也会破坏数据的准确性

出于这些原因,OpenGL 提供了一种较为原始的传递方式,叫做 Buffer Texture,即使用纹理作为通用数据缓冲区。它允许我们直接将内存中的二进制数据搬运到显存中,然后通过一种特殊的采样器,也就是 samplerBuffer 来访问。

和一般的 sampler2D 不同,samplerBuffer 将纹理的内容(即显存中的原始数据)视为一维数组,可以通过 下标直接索引 数据,并且不会使用任何过滤器这刚好满足我们的需要!

关于 Buffer Texture 的用法,可以参照 OpenGL Wiki ,或者下面的简单例子。假设我们有一个数组要传送:

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);

最后传送 0 号纹理到着色器:

glActiveTexture(GL_TEXTURE0);
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 分量有效。他们和内存数据的映射关系如下:

至此,我们已经掌握了在 GPU 上一次存取 12 字节(RGB32)内存数据的方法,但是我们的结构体不一定都是 12 字节对齐的

注:
也可以使用 GL_R32F 来每次读取一个 32 位浮点数,这样能够更加灵活的组织数据
但是显然一次读取一个 vec3 效率更高

以三角形和其材质为例,在 CPU 上 c++ 代码中他们的定义是这样的:

// 物体表面材质定义
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;  // 材质
};

注:
这里卖个关子…
算了,这里材质用的是 Disney 定义的 PBR 材质参数,下一章节中我们将实现 Disney principel’s BRDF,简单的说就是规范化的基于物理的渲染

编码三角形数据,我们创建结构体 Triangle_encoded,他们的数据类型都是 vec3 组成的,满足 12 字节对齐:

struct Triangle_encoded {
   
    vec3 p1, p2, p3;    // 顶点坐标
    vec3 n1, n2, n3;    // 顶点法线
    vec3 emissive;      // 自发光参数
    vec3 baseColor;     // 颜色
    vec3 param1;        // (subsurface, metallic, specular)
    vec3 param2;        // (specularTint, roughness, anisotropic)
    vec3 param3;        // (sheen, sheenTint, clearcoat)
    vec3 param4;        // (clearcoatGloss, IOR, transmission)
};

然后准备编码数据,这部分的 c++ 代码相当无聊,就是把数据倒腾来倒腾去:

// 读取三角形
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);

然后在 shader 中通过 texelFetch 解码数据,并且还原为原本的结构体,这部分的代码也相当无聊,同样是倒腾数据。只是注意访问纹理是以 vec3 为单位,编码后的三角形包含 12 个 vec3 向量,所以要通过下标计算每个下标对应的偏移,GLSL 代码如下:

#define SIZE_TRIANGLE   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;
}

然后尝试传输一个三角形和它的材质,在 shader 中输出它的颜色,顶点信息,或者任何属性以验证。如果成功读取颜色(或者其他数据,比如坐标)说明数据传输无误

3. 在 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 在三角形中:

代码也很简单,唯一值得注意的是如果视线方向 d 和三角形的法向 N 的方向相同,要翻转一下 N 以保证不管从正面还是背面击中三角形,我们都能获得正确的法向量。代码如下:

#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;
}

然后我们配置一下相机,相机位于 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);

然后用刚刚写的 hitArray 遍历数组,求交点的同时返回表面的颜色:

uniform int nTriangles;

...

HitResult res = hitArray(ray, 0, nTriangles-1);
if(res.isHit) fragColor = vec4(res.material.color, 1);

三角形… 已经看的足够多次了…

读取一个 Stanford Bunny 的 obj 模型再试一下:

ohhhh 虽然是暴力遍历 4000 多个三角形,但是因为 GPU 强大的并行能力,我们能够维持 4 ~ 5 的 FPS

4. 线性化 BVH 树

我们成功遍历三角形,但是我们需要更加高效的遍历,BVH 树是一个不错的选择。但是在 GLSL 中 没有指针 这一概念,我们需要将使用 指针 的树形结构改为使用 数组下标 作为指针的线性化二叉树。相信学过数据结构的童鞋都对此不陌生…

这是原来的 BVH 节点结构体,内容分为三部分,分别是左右孩子,AABB 碰撞盒,叶子节点信息,其中 AA 为极小点,BB 为极大点。因为不能用指针,只能用数组下标,我们将结构体改为:

// BVH 树节点
struct BVHNode {
   
    int left, right;    // 左右子树索引
    int n, index;       // 叶子节点信息               
    vec3 AA, BB;        // 碰撞盒
};

这里还引入了一个小变化:一个叶子节点可以保存多个三角形,n 表示该叶子节点的三角形数目,index 表示该节点第一个三角形,在 triangles 数组中的索引:

线性化二叉树也很简单,只需要每次创建节点的时候,将 new Node() 改为 push_back() 即插入数组,而下标的索引方式是照常的。

这里我们允许一个叶子包含 n 个三角形,每次创建节点直接 push_back,然后通过下标进行索引。使用最简单的二分法创建,每次将三角形数组对半分!创建 BVH 的代码如下:

注:
这里仅给出普通的二分建树代码
SAH 优化版本的代码在下文 “完整代码” 部分可以找到

// 构建 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;
}

至此我们拥有一个线性化的二叉树,接下来准备将它送入 GPU

5. BVH 数据传送到 shader

注:
该部分的代码也相当无聊,尽情跳过

和三角形数据类似,编码,然后传送:

struct BVHNode_encoded {
   
    vec3 childs;        // (left, right, 保留)
    vec3 leafInfo;      // (n, index, 保留)
    vec3 AA, BB;        
};

编码的过程太无聊了,不复制代码了。这里贴一下在 shader 中解码 BVHNode 的代码:

#define SIZE_BVHNODE    4

uniform samplerBuffer nodes;

// 获取第 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;
}

读取一个兔子,然后遍历所有节点,对于每个叶子节点,遍历它包含的所有三角形,看看兔子是否完整,以验证 BVH 是否传输正确:

注:
这一步还是暴力遍历,没有二分遍历 bvh,只是为了验证数据是否正确

投射光线 

...

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);
    }
}  

如果返回和 hitArray 一样的结果,说明我们 BVH 数据传输的没问题:

此外,在遍历之前,建议输出一下 1 号节点的左指针,不出意外应该是 2,将 node.left / 3 作为像素颜色输出,如果出现灰色说明正确,务必确保 整数下标 的传输无误!

6. 和 AABB 盒子求交

对于轴对齐包围盒,光线穿入穿出 xoy,xoz,yoz 平面,会有三组穿入点穿出点。如果找到一组穿入点穿出点,使得光线起点距离穿入点的距离 小于 光线起点距离穿出点的距离,即 t0 < t1 则说明命中

我们取 out 中最小的距离记作 t1,和 in 中最大的距离记作 t0,然后看是否 t1 > t0 如果满足等式,则说明命中:

对应的 GLSL 代码如下:

// 和 aabb 盒子求交,没有交点则返回 -1
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);
}

注:
n 即近交点 near,也就是 in
f 即远交点 far,也就是 out
因为 in 和 out 在 GLSL 中是关键字不能用作变量名

接下来测试一下。对于 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);

...

结果如下:

出现上述结果说明 BVH 的构建,传送和求交都没有问题

7. 非递归遍历 BVH 树

在 GPU 上面没有栈的概念,也不能执行递归程序,我们需要人为编写二叉树遍历的代码。对于 BVH 树,在和 节点求交 之后 ,我们总是查找它的左右子树,这相当于二叉树的 先序遍历 ,实现起来相对容易

维护一个栈来保存节点。首先将树根入栈,然后 while(!stack.empty()) 进行循环:

  1. 从栈中弹出节点 root
  2. 如果右树非空,将 root 的右子树压入栈中
  3. 如果左树非空,将 root 的左子树压入栈中

很简单,不过注意 先访问的节点后入栈 ,因为栈的存取顺序是相反的,这样保证下一次取栈顶元素,一定是先被访问的节点。这里摆烂了五毛特效糊一张非递归遍历时栈状态图:

如果不知道代码有没有 bug,可以上 leetcode 的第 144 题检验一下,这里是 传送门

至此我们掌握了非递归遍历二叉树的方法,在 GLSL 中没有 STL 的 stack,所以我们用数组和一个 sp 指针模拟栈。下面是遍历 BVH 查询 最近的 三角形的 GLSL 代码:

// 遍历 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
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

光线追踪渲染实战(三):OpenGL 光线追踪,用 GPU 加速计算! 的相关文章

  • glDrawElements 在 PyOpenGL 中绘制立方体

    我最近开始通过 Python 学习 OpenGL 这要归功于几个教程 尤其是 Nicolas P Rougier 的教程 http www labri fr perso nrougier teaching opengl http www l
  • VBO - 没有指数化的指数化

    我正在尝试将 VBO 与元素数组缓冲区一起用于我的三角形 如下所示 glBindBuffer GL ARRAY BUFFER g Buffer 0 glVertexPointer 3 GL FLOAT 0 BUFFER OFFSET 0 g
  • 如何以编程方式在 qml 中渲染 vtk 项目?

    到目前为止 我了解到我们在 QML 中有两个线程 我们的主应用程序线程和我们的 场景图 线程 http doc qt io qt 5 qtquick visualcanvas scenegraph html http doc qt io q
  • 静态链接库时出现 glew 链接器错误

    我正在尝试在 Visual Studio 2012 中构建一个 opengl 项目 我想静态包含 glew 库 因此我从源代码构建它并将生成的 glew32sd lib 复制到我的 lib 目录 我将此 lib 路径提供给 Visual S
  • 如何将 3D 图像输出到 3D 电视?

    我有一台 3D 电视 如果我不至少尝试让它显示我自己创作的漂亮 3D 图像 我就会逃避我的责任 作为一个极客 我之前已经完成了非常基本的 OpenGL 编程 因此我了解所涉及的概念 假设我可以为自己渲染一个简单的四面体或立方体并使其旋转一点
  • 使用 Opengl 绘制立方体 3D

    我想使用 OpenGL 绘制 3D 立方体这是我的代码如何纠正错误 float ver 8 3 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0
  • 法线在 openGL 中表现得很奇怪

    我一直在为 openGl 编写一个 obj 加载器 几何体加载得很好 但法线总是混乱的 我尝试在两个不同的程序中导出模型 但似乎没有任何效果 据我所知 这就是将法线放入 GL TRIANGLES 的方法 glNormal3fv norm1
  • QOpenGLFunctions 缺少重要的 OpenGL 函数

    QOpenGLFunctions 似乎缺少重要的函数 例如 glInvalidateFramebuffer 和 glMapBuffer 据我了解 QOpenGLFunctions 加载桌面 OpenGL 函数和 ES 函数的交集 如果是这样
  • OpenGL 和加载/读取 AoSoA(混合 SoA)格式的数据

    假设我有以下 AoSoA 格式的简化结构来表示顶点或点 struct VertexData float px 4 position x float py 4 position y 也就是说 每个实例VertexData存储4个顶点 我见过的
  • OpenGL:调试“单通道线框渲染”

    我正在尝试实现论文 单通道线框渲染 它看起来很简单 但它给了我所期望的厚暗值 论文没有给出计算海拔高度的确切代码 所以我按照自己认为合适的方式进行了操作 代码应该将三个顶点投影到视口空间中 获取它们的 高度 并将它们发送到片段着色器 片段着
  • glEnableVertexAttribArray 中“index”参数的含义以及(可能)OS X OpenGL 实现中的错误

    1 我是否正确理解 要使用顶点数组或VBO进行绘制 我需要所有属性在着色器程序链接之前调用glBindAttribLocation 或者在着色器程序成功链接后调用glGetAttribLocation 然后使用glVertexAttribP
  • 将像素传递给 glTexImage2D() 后会发生什么?

    例如 如果我创建一个像素数组 如下所示 int getPixels int pixels new int 10 pixels 0 1 pixels 1 0 pixels 1 1 etc glTexImage2D getPixels glTe
  • glut 库中缺少 glutInitContextVersion()

    我正在练习一些 opengl 代码 但是当我想通过以下方式强制 opengl 上下文使用特定版本的 opengl 时glutInitContextVersion 它编译过程失败并给出以下消息 使用未声明的标识符 glutInitContex
  • 如何在 GTX 560 及更高版本上使用 OpenGL 进行立体 3D?

    我正在使用在 Windows 7 上运行的开源触觉和 3D 图形库 Chai3D 我重写了该库以使用 Nvidia nvision 执行立体 3D 我将 OpenGL 与 GLUT 一起使用 并使用 glutInitDisplayMode
  • 将四元数旋转转换为旋转矩阵?

    基本上 给定一个四元数 qx qy qz qw 我如何将其转换为OpenGL旋转矩阵 我也对哪个矩阵行是 向上 向右 向前 等感兴趣 我有一个四元数的相机旋转 我需要在向量中 以下代码基于四元数 qw qx qy qz 其中顺序基于 Boo
  • GL_CULL_FACE使所有对象消失

    我正在尝试在 openGL3 3 中创建一些简单的多边形 我有两种类型的对象 具有以下属性 对象 1 10 个顶点 按顺序在下面列出 存储在GL ARRAY BUFFER并使用GL TRIANGLE FAN v x y z w v 0 0
  • GLSL NVidia 方形神器

    当 GLSL 着色器在以下 GPU 上生成不正确的图像时 我遇到了问题 GT 430 GT 770 GTX 570显卡760 但在这些上正常工作 英特尔高清显卡 2500英特尔高清4000英特尔4400显卡740MRadeon HD 631
  • 在 Linux 上运行我自己的程序的权限被拒绝? [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 我有Ubuntu 9 4 我已经构建了程序 一些基本的 OpenGL 该程序只是制作一个旋转的正方形 然后运行它并 sh blabla p
  • 三角形纹理映射OpenGL

    我正在开发一个使用 Marching Cubes 算法并将数据更改为 3D 模型的项目 现在我想在 OpenGL 中为我的 3D 模型使用纹理映射 我首先尝试了一个简单的示例 它将图片映射到三角形上 这是我的代码 int DrawGLSce
  • 如何使用边缘和内部镶嵌因子完成三角形面片镶嵌?

    I am just learning tessellation and i came across with below example for triangle patch tessellation but i am not sure h

随机推荐

  • Midjourney如何集成到自己(个人/企业)的平台(一)

    背景概述 目前Midjourney没有对外开放Api 想体验他们的服务只能在discord中进入他们的频道进行体验或者把他们的机器人拉入自己创建的服务器中 目前免费的已经无法体验了 需要使用就订阅 在网上搜索相应资料也是一知半解的 没有能照
  • tomcat 线程池和连接池

    在介绍如何配置tomcat线程池和连接池之前 先介绍一下线程池和连接池的原理 线程池的原理 其实线程池的原理很简单 类似于操作系统中的缓冲区的概念 它的流程如下 先启动若干数量的线程 并让这些线程都处于睡眠状态 当客户端有一个新请求时 就会
  • C# WPF并行计算两个矩阵

    并行计算两个矩阵 要求 编写一个WPF应用程序 利用数据并行计算两个矩阵 M N和N P 的乘积 得到一个M P的矩阵 1 在代码中用多任务通过调用某方法实现矩阵并行运算 在调用的参数中分别传递M N P的大小 2 程序中至少要测试3次有代
  • php7.3 "continue" targeting switch is equivalent to "break"

    php 7 3 版本导入Excel时报错 continue targeting switch is equivalent to break Did you mean to use continue 2 解决办法找到报错位置 将continu
  • interface{}类型通过json.Unmarshal之后的类型

    一句话总结 所有JSON数值类型一律解析为float64类型 需手动转换 对于map类型需判断是否为nil再转换为所需类型 interface 类型在json Unmarshal时 会自动将JSON转换为对应的数据类型 JSON的boole
  • 三星打印机显示无法连接服务器,三星打印机不能打印,提示“无法识别的USB设备”解决办法...

    打印机安装在电脑中之后出现无法识别的USB设备问题了 对于这个问题我们从几个方向来给各位排查无法识别的USB设备问题的解决办法 如图1 无法识别的USB设备 如图2 如果您USB设备没有被电脑识别 如下图所示 在电脑设备管理器里面会出现一个
  • OTA: Optimal Transport Assignment for Object Detection 原理与代码解读

    paper OTA Optimal Transport Assignment for Object Detection code https github com Megvii BaseDetection OTA 背景 标签分配 Label
  • SpringCloud @FeignClient 参数详解

    SpringCloud FeignClient 参数详解 今天因为工作中遇到FeignClient一个奇葩的bug 后面仔细研究了 找出了原因 那么刚好对FeignClient 这个注解总结一下 1 先看 FeignClient 源码 源码
  • Java集合框架——List接口的使用

    学习Java的同学注意了 学习过程中遇到什么问题或者想获取学习资源的话 欢迎加入Java学习交流群 群号码 183993990 我们一起学Java List集合代表一个有序集合 集合中每个元素都有其对应的顺序索引 List集合允许使用重复元
  • JVM(二)--对象已死?和引用问题

    JVM 二 对象已死 和引用问题 写在前面 java内存运行时区域的各个部分 其中程序计数器 虚拟机栈和本地方法栈3个区域随线程而生 随线程而灭 栈中的栈帧随着方法的进入和退出而有条不紊地执行者出栈和入栈操作 每一个栈帧中分配多少内存基本上
  • Error creating bean with name 'enableRedisKeyspaceNotificationsInitializer'报错处理

    服务器关闭后又重启 在上面启动web应用日志打印报错 Error creating bean with name enableRedisKeyspaceNotificationsInitializer 在网上搜了一下 发现答案很多都是 sp
  • 电信测试网速测试在线软件,宽带测速在线测网速(中国电信宽带测速官网)

    家里宽带怎么测试网速 我家是两兆宽带 可速度好像只有512的 请教一下 1 首先利用360进行测速 打开360主界面 点击 宽带测试器 2 进入宽带测速器后 软件就会自动开始进行网速测试 为了不影响网速的测试 可能需要把系统上 您好 1Mb
  • 关于unity打包安卓apk详细步骤

    小白的经验之谈 如有不足 欢迎指出 第一步 如果想要让Unity可以打包Apk 你需要先下载一个JDK7以上 包括7 的版本 并且必须是64位 安装时请记住您的安装路径 下面是分享的JAVA的jdk 链接 https pan baidu c
  • “阿里爸爸”上新!《2023阿里Java性能调优手册(实战参考)》

    为什么要学Java性能调优 编辑切换为居中 添加图片注释 不超过 140 字 可选 现在去学性能调优还有用么 编辑切换为居中 添加图片注释 不超过 140 字 可选 编辑切换为居中 添加图片注释 不超过 140 字 可选 编辑切换为居中 添
  • 变量的获取与设置:echo、变量设置规则、unset

    变量的获取与设置 echo 变量设置规则 unset echo命令 使用echo命令可以获取变量 但是在获取变量时 前面必须加上 或使用 variable 方式 例如 输出PATH的内容 当一个变量名称尚未设置时 默认的内容是 空 的 另外
  • Java 中的 JDK 介绍

    Java 开发工具包 JDK 是一个跨平台的软件开发环境 它提供了开发基于 Java 的软件应用程序和小程序所需的工具和库的集合 它是Java中使用的核心包 与JVM Java虚拟机 和JRE Java运行时环境 一起 初学者经常对 JRE
  • vscode的sql开发插件

    vscode的sql开发插件 Better Comments Error Lens SQLTools MySQL MariaDB SQL Formatter VSCODE连接数据库 执行sql Better Comments 注释美化插件
  • 2018年计算机专业考研,2018年计算机专业考研大纲解析

    2017年暑期几十年不遇的高温炙烤着紧张复习的同学 早早发布的2018年计算机考研大纲给大家带来一丝秋的凉意 考研大纲从考试目的到考试形式 再到四门课程的每一个知识点都没有任何变化 这符合文都老师们预测 也是广大同学们的福音 暑期奋战没有浪
  • IOS消息推送之APNS

    转自 http blog csdn net jiajiayouba article details 39926017 一 背景概述 1 环境配置 APNS Apple Push Notification Service 本文对推送相关概念不
  • 光线追踪渲染实战(三):OpenGL 光线追踪,用 GPU 加速计算!

    项目代码仓库 GitHub https github com AKGWSB EzRT gitee https gitee com AKGWSB EzRT 目录 前言 0 前置知识 1 布置画布 2 三角形数据传送到 shader 3 在 s