极简光线追踪入门

2023-11-09

0cc0a0421c71f05befff988801a7a13f.png

阅读本文不需要任何图形学基础,这里抛砖引玉,希望勾起读者对光线追踪的兴趣。

948c8090a094a06efb7dd5d0218ea1ac.png

前言

光线追踪原理简单,并且可以抛开图形学的一堆理论,单独提出来讲。

本文不需要任何图形学基础,看完本文后,将能够实现以下效果。

2d38c2333fe6c92c2366481566ce7f54.png

图1 光线追踪效果

3fef966e5f87da21f767f4d779f152e4.png

原理

光线追踪算法由Appel在1968年提出,是一种基于真实光线传播模拟的计算机三维图形渲染算法。能实现较真实的光影效果。但是由于其庞大的计算量,一般用于离线渲染中。随着硬件技术的提升,已经推出支持实时光线追踪渲染的GPU,如微软DXR和NVIDIA RTX。

光线追踪可简单分为正向光线追踪反向光线追踪。正向光线追踪算法从光源位置跟踪光子穿过场景的路径。反向光线追踪则从从观察者视角出发向场景发出光线,追踪光线的路径。

正向光线追踪中,由于从光源发出的粒子照射在物体上,折射光线不一定进入观察者视野,会产生大量无效计算。真正被接受使用的是反向光线追踪。

反向光线追踪如图2,假设屏幕内是真实的3D世界,显示器是个透明的玻璃。从相机,也就是视点出发,发射一条光线透过屏幕,射向3D场景中,击中球体,再追踪该光线击中球体后的反射、折射、吸收映射等等,最终综合计算出的结果,即为显示在屏幕上该点的像素(颜色)值。

a2a4d03f84e0d5ae33eb4c2910d932b5.png

图2 反向光线追踪原理

cb823d67637027914e6519bb0cf3dd98.png

实现

从基础篇原理可以看出,光线追踪算法是一个物理和数学模拟的过程。

最基础的模块包括:

        光线与物体的相交计算

        光照效果计算

此外,一些辅助模块:

        场景搭建

        图形显示

1.光线

首先介绍最难的也是最核心的光线与相交检测。

光线定义为从某一原点o,某一方向d,延申出的一条直线。可以得出光线方程:

p = o + t d (1)

t = 0  表示光线的原点,t可位于无限区间内,表示光线无限延申。

13867c7d8c6bcb7a9e1c0803cb843b10.png

图3 光线[3]

代码如下

class ray
{
public:
  ray() {}
  ray(const vec3& a, const vec3& b) { A = a; B = b; }
  vec3 origin() const { return A; }
  vec3 direction() const { return B; }
  vec3 point_at_parameter(float t) const { return A + t * B; } // 光线


  vec3 A; // 原点
  vec3 B; // 方向
};

2. 相交检测

光线与场景中几何体的相交检测,可以看成在一个3维空间,一条直线与一个三维几何体是否相交的问题。

三维几何体表面函数定义为:

f(x, y, z)= 0

f表示关于x, y, z的任意函数。f(x, y, z) < 0 表示表面的一侧, f(x, y, z) > 0,表示表面的另一侧。

过原点的半径为1的球体的方程为:

x ^2 + y ^ 2 + z ^ 2 - 1 = 0

0f94afe1ef5049e11d9f4b5de7d3fb7a.png

图4 球体[3]

用 p 表示点(x, y, z),为球面上的点, c表示球心坐标, r表示球体半径。

将球体方程改成向量形式为:

(p - c) * ( p - c) - r ^ 2 = 0 (2)

光线与几何体相交

将光线方程(1) 代入球体方程(2),得

(o + t * d - c ) - ( o + t * d - c) - r ^ 2 = 0

可得到关于 t 的二次方程

1b31f8abf7197b2439f965d2f8541b5c.png

二次方程的求解,初中数学学过的公式:

250d4b61ced4a9cdf5e9c7a6c074a6a0.png

二次方程分别包含一个解、两个解和无解。取决于判别式

b6cbf53faaba5a081427b8a2a12cecc8.png

对应的实际情况为光线将分别与球体相交一次、两次或者不相交。

e65ed4ed7c4f6c39eef99a4db5cb9980.png

图5 光线-球体相交检测

实现代码如下

bool sphere::hit(const ray& r, float t_min, float t_max, hit_record& rec) const {
    vec3 oc = r.origin() - center;
    float a = dot(r.direction(), r.direction());  
    float b = dot(oc, r.direction());
    float c = dot(oc, oc) - radius * radius;
    float discriminant = b * b - a * c;  // b ^2 - a c
    if (discriminant > 0) {
        float temp = (-b - sqrt(discriminant)) / a;
        if (temp < t_max && temp > t_min) {
            rec.t = temp;
            rec.p = r.point_at_parameter(rec.t);
            rec.normal = (rec.p - center) / radius;
            return true;
        }
        temp = (-b + sqrt(discriminant)) / a;
        if (temp < t_max && temp > t_min) {
            rec.t = temp;
            rec.p = r.point_at_parameter(rec.t);
            rec.normal = (rec.p - center) / radius;
            return true;
        }
    }
    return false;
}

此外,本文实现了光线与包围盒的碰撞检测,使用了slab检测算法,如下。

// slab法碰撞检测
bool AABB::hit(const ray& r, float t_min, float t_max, hit_record& rec) const {


    //vec3 min_pos; // AABB最小点坐标
    //vec3 max_pos; // AABB最大点坐标
    float delta = 0.0001;
    if ((abs(r.B.x()) < delta) && 
        ( r.A.x() > min_pos.x() || r.A.x() < max_pos.x()))
        return false;
    if ((abs(r.B.y()) < delta) && 
        (r.A.y() > min_pos.y() || r.A.y() < max_pos.y()))
        return false;
    if ((abs(r.B.z()) < delta) && 
        (r.A.z() > min_pos.z() || r.A.z() < max_pos.z()))
        return false;


    float tx_1 = (min_pos.x() - r.A.x()) / r.B.x(); // A 原点, B方向
    float tx_2 = (max_pos.x() - r.A.x()) / r.B.x();
    if (tx_1 > tx_2)
        std::swap(tx_1, tx_2);


    float ty_1 = (min_pos.y() - r.A.y()) / r.B.y();
    float ty_2 = (max_pos.y() - r.A.y()) / r.B.y();
    if (ty_1 > ty_2)
        std::swap(ty_1, ty_2);


    float tz_1 = (min_pos.z() - r.A.z()) / r.B.z();
    float tz_2 = (max_pos.z() - r.A.z()) / r.B.z();
    if (tz_1 > tz_2)
        std::swap(tz_1, tz_2);


    float t_min_max, t_max_min;
    t_min_max = std::max(tx_1, std::max(ty_1, tz_1)); // 三个小的交点中的最大,光线进入平面处(最靠近的平面)的最大t值 
    t_max_min = std::min(tx_2, std::min(ty_2, tz_2)); // 三个大的交点中的最小,光线离开平面处(最远离的平面)的最小t值


    if (t_min_max > t_min && t_max_min < t_max && t_min_max < t_max_min) // 范围检测
    {
        // 碰撞点的信息
        float t = t_min_max; 
        rec.t = t;
        rec.p = r.point_at_parameter(rec.t); 
        vec3 normal = rec.p;
        normal.make_unit_vector();
        rec.normal = normal;
        return true;
    }
    return false;
}

3.光照效果计算

光照效果,即光线击中物体时,击中点的颜色信息。常见的算法包括PBR、Phong、Blinn-Phong等。需要引入光源、物体材质,本文化繁为简,直接使用击中点的法线坐标做位颜色信息。

代码如下

rec.t = temp;
rec.p = r.point_at_parameter(rec.t);
//球体的法线即为球心指向碰撞点的方向向量。
rec.normal = (rec.p - center) / radius;

4.图形显示

图形显示这一块,最初思考用OpenGL和DirectX,觉得太麻烦想用OpenCV,最后发现,[1]这个教程中使用了ppm格式的图片(ImageMagick打开),简单方便,就拿来用了。本文的工程代码也修改自这本书。

ppm。ppm图片的数据由文件头和数据块组成。如下

    P3

    200 100

    255

    0 253 51

    1 253 51

    2 253 51

    3 253 51

文件头包含三行文本

第一行P3,表示PPM文件类型

第二行为图像的宽度和高度,200 x 100的图片

第三行为最大的相素值255

第四行开始为数据块,每行代表一个rgb颜色值。   

左上角是原点,写入顺序是从上到下,从左到右。代码如下

int main() {
        int nx = 200;
        int ny = 100;
        std::cout << "P3\n" << nx << " " << ny << "\n255\n";
        for (int j = ny-1; j >= 0; j--) {
            for (int i = 0; i < nx; i++) {
                float r = float(i) / float(nx);
                float g = float(j) / float(ny);
                float b = 0.2;
                int ir = int(255.99*r);
                int ig = int(255.99*g);
                int ib = int(255.99*b);
                std::cout << ir << " " << ig << " " << ib << "\n";
            }
        }
    }

这里cout将数据输出到控制台窗口,怎么变成ppm呢?

两种方法

1. 在工程属性界面,配置调试的命名参数如图

这么做的意思就是将控制台显示的内容重定向到image1.ppm文件

b444553a907c1f527eb291582cd77a3d.png

图6 visual studio工程设置

2. 编译工程生成exe文件

然后在exe的目录下,用cmd输入如下命令

6686822c153d76e3a2f7e9742b6332d9.png

图7 重定向输出

5.场景搭建

场景搭建即创建场景中包含的各个物体。

在一般的模型生成中,流行的做法是基于三角形绘制。所有的模型,人、车、建筑等等,都是很多个三角形做出。经典的射线检测即检测射线是否击中某个三角形。(感兴趣的读者可以自己三维中实现射线和三角形的碰撞检测)。

本文使用最基本的数学做法,一个球只需要知道球心和半径,一个AABB包围盒只需要知道最大点和最小点坐标。

球体代码

class sphere : public hitable {
public:
    sphere() {}
    sphere(vec3 cen, float r) : center(cen), radius(r) {};
    virtual bool hit(const ray& r, float tmin, float tmax, hit_record& rec) const;
    vec3 center; // 球心
    float radius; // 半径
};

包围盒代码

class AABB : public hitable {
public:
    AABB() {}
    AABB(vec3 min_pos, vec3 max_pos) : min_pos(min_pos), max_pos(max_pos) {};
    virtual bool hit(const ray& r, float tmin, float tmax, hit_record& rec) const;
    vec3 min_pos; // 最小点
    vec3 max_pos; // 最大点
};

需要记录下整个场景的所有物体的信息,方便每条射线击中检测。

hitable* list[3]; 
    list[0] = new sphere(vec3(1, 0, -1), 0.5);
    list[1] = new sphere(vec3(0, -1.5, -1), 1);
    list[2] = new AABB(vec3(1.0, 0.0, -0.5), vec3(3.0, 2.0, -0.8));
    // 场景中物体信息,这里只有两个球体、一个正方体
    hitable* world = new hitable_list(list,3 );

最后,对于每个像素生成一条射线和一个方向,遍历场景,进行碰撞检测,输出碰撞点颜色。

for (int j = ny - 1; j >= 0; j--) {
        for (int i = 0; i < nx; i++) {
            float u = float(i) / float(nx); // 当前像素点的UV坐标
            float v = float(j) / float(ny);
            vec3 direction(lower_left_corner + u * horizontal + v * vertical); //射线方向
            ray r(origin, direction);
            vec3 col = color(r, world); // 射线检测
            int ir = int(255.99 * col[0]);
            int ig = int(255.99 * col[1]);
            int ib = int(255.99 * col[2]);


            std::cout << ir << " " << ig << " " << ib << "\n";
        }
    }

工程源码放在个人github,感兴趣的可以自行下载。

链接:https://github.com/youxijunwuchen/RayTraceExample


d138ea76f41e000dcc20982b34db4782.png


光线追踪渲染管线介绍

光线追踪渲染光栅化渲染是相对的两个概念,现代的游戏引擎的渲染模块都是基于光栅化渲染搭建,常说的渲染管线,指的是光栅化渲染管线。

光栅化渲染渲染中,渲染是以物体为单位渲染。在光线追踪中,渲染以光线为单位。

本文的案例为阐述基本原理,一个点只生成一条光线,往一个方向射出,击中物体结束。

真正的光线追踪会在一个点往多个方向(一般以半球区域)射出多条光线,对于每一条光线,递归追踪其折射、反射,一直追踪至击中光源,或者返回出发点。如图8。

66f38de8a2db091d0df6ec58867455aa.png

图8 递归光线追踪[2]

计算视点光照的时候,将每条光线,每个路径上的光照贡献叠加。

光线追踪渲染管线如图

8c955617d75b3e7d06d5281dbaa3a9fe.png

图9 管线追踪渲染管线[4]

光线追踪的渲染程序从Ray Generation的Shader开始,随后开始遍历整个场景求交点,也就是工程中的for遍历world的过程。

当然,工程中都是在CPU中进行,光线追踪管线则在GPU中进行,即Intersection Shader中。

随后,进行有效性检测,图中的Any Hit中进行。由Any Hit Shader处理。

若没有击中任何物体,则调用MISS shader。进入下一条光线检测。

否则,当前光线与整个场景没有新的交点后,调用Closest Hit Shader进行着色。

463846f90435ad8a6f080e4d536c26a1.png

总结

本文介绍了光线追踪的基本概念,这里屏蔽了对光照的处理,相信读完本文对光线渲染会有个基础的了解。对全局光照感兴趣的读者可以去读经典书籍,如《Real-Time Rendering》、《全局光照技术》。对光线追踪感兴趣的可以阅读本文参考文献中的资料。

光线追踪技术一般用于离线渲染,烘焙生成光照贴图等。随着GPU的提升,2018年,NVIDIA突出第一款支持光线追踪实时渲染的GPU,也宣告光线追踪新的时代来临。

之前买的GTX 3060的显卡支持该技术,改天深入研究下实际应用效果~

参考资料

[1] RayTracing InOneWeekend https://github.com/RayTracing/InOneWeekend

[2] GAMES101,闫令琪, 现代计算机图形学入门 https://sites.cs.ucsb.edu/~lingqi/teaching/games101.html

[3] 光线追踪算法技术 清华大学出版社

[4] 光线追踪与实时渲染的未来 https://zhuanlan.zhihu.com/p/34851503

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

极简光线追踪入门 的相关文章

随机推荐

  • XML建模

    文章目录 思路 思路 把配置文件读到内存里并解析出来 gt 建立xml模型 有几个节点就创建几个模型 把他们的关系放到模型里 gt 对模型进行完善 gt 把解析出来的数据放到模型里 XML建模的具体文件 内附注释
  • Linux unit 测试工具,单元测试工具 CUnit 简介

    1 CUnit简介 1 1 CUnit简要描述 CUnit是一个编写 管理及运行c语言单元测试的系统 它使用一个简单的框架来构建测试结构 并为普通数据结构的测试提供丰富的断言 此外 CUnit为测试的运行和结果查看提供了许多不同的接口 包括
  • centos7 keepalived 离线安装

    两台服务器 master 10 214 130 100 slave 10 214 130 101 vip keepalived虚拟ip 10 214 130 102 1 下载 登陆官网 http www keepalived org dow
  • IDEA Maven 依赖分析插件Maven Helper

    IDEA 安装Maven Helper插件 1 打开setting 找到Plugins选项 安装Maven Helper 插件 如果有就跳过这一步 检索 Maven Helper 安装成功后 重新启动IDEA编辑器 2 使用Maven He
  • java 将pdf转word

    可以使用 Apache POI 库来实现将 PDF 转换为 Word 文档的功能 首先 需要将 Apache POI 库的依赖添加到项目中
  • 推荐几个很实用的编程网站

    目录 一 W3School 二 LeetCode 三 PythonTip 四 Codewars 五 Code Monkey 本文精选了有关代码 编程 Java Python SQL Git 和Ruby on Rails学习的网站 这些网站为
  • mac扩展屏,HIDPI

    2k 4k 均可开启 HIDPI
  • java.lang.RuntimeException: Canvas: trying to draw too large(277114284bytes) bitmap.

    java lang RuntimeException Canvas trying to draw too large 277114284bytes bitmap 今天运行一个小项目报错 E AndroidRuntime FATAL EXCE
  • 【PCL】得到一个点云中的最高值、最低值

    有一个点云 想得到它x y z三个轴上的最大值和最小值 可以用pcl getMinMax3D函数 在这儿 函数参数 1 点云 2 放最小值的容器 3 放最大值的容器 容器类型是点云中点的类型 正好有三个值 代码 Created by eth
  • 分布式事务-LCN

    2PC两阶段提交协议 分布式事务通常采用2PC协议 全称Two Phase Commitment Protocol 该协议主要为了解决在分布式数据库场景下 所有节点间数据一致性的问题 分布式事务通过2PC协议将提交分成两个阶段 阶段一为准备
  • 4. Redis高并发分布式锁实战---大厂生产级Redis高并发分布式锁实战

    分布式缓存技术Redis 1 手写分布式锁 2 Redis Lua 3 Redisson 4 redis分布式锁在集群中存在的问题 本文是按照自己的理解进行笔记总结 如有不正确的地方 还望大佬多多指点纠正 勿喷 课程内容 1 高并发场景秒杀
  • Elasticsearch 在kibana中对索引名称进行重命名

    问题 在实际的工作中 遇到已经将数据写入es 但是后边需要对这个索引进行重命名 如 test 20190122 test 20190121 需要重命名为test 2019 对于数据量比较少时 创建多个索引 需要创建多个分片 造成存储资源的浪
  • 正则表达式——Pattern.DOTALL

    项目测试过程中 测试发现短信内容无法正常解析成2个部分 代码如下 public static void main String args String testStr 13800000000 孔雀东南飞 五里一徘徊 n 十三能织素 十四学裁
  • 行式数据库与列式数据库的对比

    导语 随着大数据的发展 现在出现的列式存储和列式数据库 它与传统的行式数据库有很大区别的 正文 行式数据库是按照行存储的 行式数据库擅长随机读操作不适合用于大数据 像SQL server Oracle mysql等传统的是属于行式数据库范畴
  • C语言在控制台上实现鼠标操作的方法

    文章目录 了解windows库函数 了解句柄 实现思路与代码 在制作面向用户系统时 我们往往需要设置除输入参数外更为灵活的操作方式 例如鼠标点击 按键按下 无阻塞输入 等 同时 我们需要制作更为精美的 UI而不是简陋的黑白界面 然而 纯C语
  • 解决docker下nginx的多个站点互通问题

    系统为MAC 环境为docker mysql php nginx 问题描述 1 调试本地接口 方式为curl 请求 返回拒绝 业务上通过curl访问报错 Couldn t connect to server Failed to connec
  • git bash配置ssh 登录 Linux

    1 首先在 Linux 服务器上生成公钥和私钥文件 默认的存放目录在 ssh下 ssh keygen 可以将密码留空 这样之后就可以免密码登录 2 将私钥文件拷贝到本机 scp root 192 168 1 168 root ssh id
  • Ubuntu安装Protobuf,指定版本

    参考 https github com protocolbuffers protobuf readme https github com protocolbuffers protobuf blob v3 20 3 src README md
  • VirtualBox+WinDbg+Win7调试环境配置

    VirtualBox WinDbg Win7调试环境配置 火苗999 的博客 1 配置虚拟机串口如图 勾选启用串口 gt 端口选择COM1 gt 端口模式选择主机管道 gt 勾选创建管道 gt 端口文件位置输入 pipe com1 2 配置
  • 极简光线追踪入门

    阅读本文不需要任何图形学基础 这里抛砖引玉 希望勾起读者对光线追踪的兴趣 前言 光线追踪原理简单 并且可以抛开图形学的一堆理论 单独提出来讲 本文不需要任何图形学基础 看完本文后 将能够实现以下效果 图1 光线追踪效果 原理 光线追踪算法由