光线跟踪(RayTracing)原理及c++实现

2024-03-08 05:32

本文主要是介绍光线跟踪(RayTracing)原理及c++实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Chapt1. Why to write a RayTracing Render

提到Computer Graphics,众所周知的是如OpenGL、Direct3D这样非常流行的光栅化渲染器。事实上,这些大部分应用于游戏制作的API主要为实时渲染(Real-time Rendering)而设置,而它们所采用的光栅化(Rasterization)的渲染方式,通过渲染大量的三角形(或者其他的几何图元种类(Primitive types)),是与本文介绍的光线跟踪相对的一种渲染方式。这种基于光栅化的渲染系统,往往只支持局部照明(Local Illumination)。局部照明在渲染几何图形的一个像素时,光照计算只能取得该像素的信息,而不能访问其他几何图形的信息。


图1.jpg

该图片出自《孤岛惊魂》,尽管看似水面显示出了远处山峰的倒影,却不能渲染植被、船骸等细节。

理论上,阴影(Shadow)、反射(Reflection)、折射(Refraction)均为全局照明(Global Illumination)效果,所以在实际应用中,栅格化渲染系统可以使用预处理(如阴影贴图(shadow mapping)、环境贴图(environment mapping))去模拟这些效果。

栅格化的最大优势是计算量比较小,适合实时渲染。相反,全局光照计算量大,一般也没有特殊硬件加速(通常只使用CPU而非GPU),所以只适合离线渲染(offline rendering),例如3D Studio Max、Maya等工具。其中一个支持全局光照的方法,称为光线追踪(ray tracing)。光线追踪能简单直接地支持阴影、反射、折射,实现起来亦非常容易。

Chapt2. Principles of RayTracing

由光源发出的光到达物体表面后,产生反射和折射,简单光照明模型和光透射模型模拟了这两种现象。在简单光照明模型中,反射被分为理想漫反射和镜面反射光,把透射光模型分为理想漫透射光和规则透射光。由广元发出的光成为直接光,物体对直接光的反射或折射成为直接反射和直接折射,相对的,把物体表面间对广德反射和折射成为间接光、间接反射、间接折射。光线在物体之间的传播方式是光线跟踪算法的基础。

最基本的光线跟踪算法是跟踪镜面反射和折射。从光源发出的光遇到物体的表面,发生反射和折射,光就改变方向,沿着反射方向和折射方向继续前进,知道遇到新的物体。但是光源发出光线,经过反射与折射,只有很少部分可以进入人的眼睛。因此实际光线跟踪算法的跟踪方向与光传播的方向是相反的(反向光线跟踪),称之为视线跟踪。由视点与像素(x,y)发出一根射线,与第一个物体相交后,在其反射与折射方向上进行跟踪,如图2所示


图2.gif

在光线跟踪算法中,我们有如下的四种光线:

  • 视线是由视点与像素(x,y)发出的射线;
  • 阴影测试线是物体表面上点与光源的连线;
  • 反射光线;
  • 折射光线

当光线 V与物体表面交于点P时,点P分为三部分,把这三部分光强相加,就是该条光线V在P点处的总的光强:

  • a) 由光源产生的直接的光线照射光强,是交点处的局部光强,可以由下式计算:

    式1.gif

  • b) 反射方向上由其它物体引起的间接光照光强,由IsKs' 计算,Is通过对反射光线的递归跟踪得到
  • c) 折射方向上由其它物体引起的间接光照光强,由ItKt' 计算,It通过对折射光线的递归跟踪得到

现在我们来讨论光线跟踪算法本身。我们将对一个由两个透明球和一个非透明物体组成的场景进行光线跟踪(图3)通过这个例子,可以把光线跟踪的基本过程解释清楚。


图3.gif

在我们的场景中,有一个点光源L,两个透明的球体O1与O2,一个不透明的物体O3。首先,从视点出发经过视屏一个像素点的视线E传播到达球体O1,与其交点为P1。从P1向光源L作一条阴影测试线S1,我们发现其间没有遮挡的物体,那么我们就用局部光照明模型计算光源对P1在其视线E的方向上的光强,作为该点的局部光强。同时我们还要跟踪该点处反射光线R1和折射光线T1,它们也对P1点的光强有贡献。在反射光线R1方向上,没有再与其他物体相交,那么就设该方向的光强为零,并结束这光线方向的跟踪。然后我们来对折射光线T1方向进行跟踪,来计算该光线的光强贡献。折射光线T1在物体O1内部传播,与O1相交于点P2,由于该点在物体内部,我们假设它的局部光强为零,同时,产生了反射光线R2和折射光线T2,在反射光线R2方向,我们可以继续递归跟踪下去计算它的光强,在这里就不再继续下去了。我们将继续对折射光线T2进行跟踪。T2与物体O3交于点P3,作P3与光源L的阴影测试线S3,没有物体遮挡,那么计算该处的局部光强,由于该物体是非透明的,那么我们可以继续跟踪反射光线R3方向的光强,结合局部光强,来得到P3处的光强。反射光线R3的跟踪与前面的过程类似,算法可以递归的进行下去。重复上面的过程,直到光线满足跟踪终止条件。这样我们就可以得到视屏上的一个象素点的光强,也就是它相应的颜色值。

上面的例子就是光线跟踪算法的基本过程,我们可以看出,光线跟踪算法实际上是光照明物理过程的近似逆过程,这一过程可以跟踪物体间的镜面反射光线和规则透射,模拟了理想表面的光的传播。

虽然在理想情况下,光线可以在物体之间进行无限的反射和折射,但是在实际的算法进行过程中,我们不可能进行无穷的光线跟踪,因而需要给出一些跟踪的终止条件。在算法应用的意义上,可以有以下的几种终止条件:

  • 该光线未碰到任何物体。
  • 该光线碰到了背景
  • 光线在经过许多次反射和折射以后,就会产生衰减,光线对于视点的光强贡献很小(小于某个设定值)
  • 光线反射或折射次数即跟踪深度大于一定值

Chapt3. Rasterization & RayTracing

了解了光线跟踪的原理之后,再来看一下在计算机上的实现。

光栅化渲染,简单地说,就是把大量三角形画到屏幕上。当中会采用深度缓冲(depth buffer, z-buffer),来解决多个三角形重叠时的前后问题。三角形数目影响效能,但三角形在屏幕上的总面积才是主要瓶颈。

光线追踪,简单地说,就是从摄影机的位置,通过影像平面上的像素位置(比较正确的说法是取样(sampling)位置),发射一束光线到场景,求光线和几何图形间最近的交点,再求该交点的著色。如果该交点的材质是反射性的,可以在该交点向反射方向继续追踪。光线追踪除了容易支持一些全局光照效果外,亦不局限于三角形作为几何图形的单位。任何几何图形,能与一束光线计算交点(intersection point),就能支持。


图4.png

上图显示了光线追踪的基本方式。要计算一点是否在阴影之内,也只须发射一束光线到光源,检测中间是否存在障碍物。

Chapt4. Source Code

1. Base Class

本例代码尝试使用基于物件(object-based)的方式编写

3D Vector
struct Vector {float x, y, z;Vector(float x_, float y_, float z_) : x(x_), y(y_), z(z_) {}Vector(const Vector& r) : x(r.x), y(r.y), z(r.z) {}float sqrLength() const {return x  x + y  y + z  z;}float length() const {return sqrt(sqrLength());}Vector operator+(const Vector& r) const {return Vector(x + r.x, y + r.y, z + r.z);}Vector operator-(const Vector& r) const {return Vector(x - r.x, y - r.y, z - r.z);}Vector operator(float v) const {return Vector(v  x, v  y, v  z);}Vector operator/(float v) const {float inv = 1 / v;return this  inv;}Vector normalize() const {float invlen = 1 / length();return this  invlen;}float dot(const Vector& r) const {return x  r.x + y  r.y + z  r.z;}Vector cross(const Vector& r) const {return Vector(-z  r.y + y  r.z,z  r.x - x  r.z,-y  r.x + x  r.y);}static Vector zero() {return Vector(0, 0, 0);}
};
inline Vector operator(float l, const Vector& r) {return r  l;}

这些类方法(如normalize、dot、cross等),如果传回Vector对象,都会传回一个新建构的Vector。这些三维向量的功能很简单,不在此详述。

Vector zero()用作常量,避免每次重新构建。值得一提,这些常量必需在prototype设定之后才能定义。

Ray

即为光线类,所谓光线(ray),从一点向某方向发射也。数学上可用参数函数(parametric function)表示:


式2.png

当中,o即发谢起点(origin),d为方向。在本文的例子里,都假设d为单位向量(unit vector),因此t为距离。实现如下

struct Ray {Vector origin, direction;Ray(const Vector& o, const Vector& d) : origin(o), direction(d) {}Vector getPoint(float t) const {return origin + t * direction;}
};
Sphere

球体(sphere)是其中一个最简单的立体几何图形。这里只考虑球体的表面(surface),中心点为c、半径为r的球体表面可用等式(equation)表示:


式3.png

如前文所述,需要计算光线和球体的最近交点。只要把光线x = r(t)代入球体等式,把该等式求解就是交点。为简化方程,设v=o - c,则:


式4.png

因为d为单位向量,所以二次方的系数可以消去。 t的二次方程式的解为


式5.png
struct Sphere : public Geometry {Vector center;float radius, sqrRadius;Sphere(const Vector& c, float r, Material m = NULL) :Geometry(m), center(c), radius(r), sqrRadius(r  r) {}IntersectResult intersect(const Ray& ray) const {Vector v = ray.origin - center;float a0 = v.sqrLength() - sqrRadius;float DdotV = ray.direction.dot(v);if (DdotV <= 0.0) {float discr = DdotV * DdotV - a0;if (discr >= 0.0) {float d = -DdotV - sqrt(discr);Vector p = ray.getPoint(d);Vector n = (p - center).normalize();return IntersectResult(this, d, p, n);}}return IntersectResult();}
};

这篇关于光线跟踪(RayTracing)原理及c++实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/786167

相关文章

Java+AI驱动实现PDF文件数据提取与解析

《Java+AI驱动实现PDF文件数据提取与解析》本文将和大家分享一套基于AI的体检报告智能评估方案,详细介绍从PDF上传、内容提取到AI分析、数据存储的全流程自动化实现方法,感兴趣的可以了解下... 目录一、核心流程:从上传到评估的完整链路二、第一步:解析 PDF,提取体检报告内容1. 引入依赖2. 封装

Java实现复杂查询优化的7个技巧小结

《Java实现复杂查询优化的7个技巧小结》在Java项目中,复杂查询是开发者面临的“硬骨头”,本文将通过7个实战技巧,结合代码示例和性能对比,手把手教你如何让复杂查询变得优雅,大家可以根据需求进行选择... 目录一、复杂查询的痛点:为何你的代码“又臭又长”1.1冗余变量与中间状态1.2重复查询与性能陷阱1.

python 线程池顺序执行的方法实现

《python线程池顺序执行的方法实现》在Python中,线程池默认是并发执行任务的,但若需要实现任务的顺序执行,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋... 目录方案一:强制单线程(伪顺序执行)方案二:按提交顺序获取结果方案三:任务间依赖控制方案四:队列顺序消

Redis实现分布式锁全过程

《Redis实现分布式锁全过程》文章介绍Redis实现分布式锁的方法,包括使用SETNX和EXPIRE命令确保互斥性与防死锁,Redisson客户端提供的便捷接口,以及Redlock算法通过多节点共识... 目录Redis实现分布式锁1. 分布式锁的基本原理2. 使用 Redis 实现分布式锁2.1 获取锁

Linux实现查看某一端口是否开放

《Linux实现查看某一端口是否开放》文章介绍了三种检查端口6379是否开放的方法:通过lsof查看进程占用,用netstat区分TCP/UDP监听状态,以及用telnet测试远程连接可达性... 目录1、使用lsof 命令来查看端口是否开放2、使用netstat 命令来查看端口是否开放3、使用telnet

使用SpringBoot+InfluxDB实现高效数据存储与查询

《使用SpringBoot+InfluxDB实现高效数据存储与查询》InfluxDB是一个开源的时间序列数据库,特别适合处理带有时间戳的监控数据、指标数据等,下面详细介绍如何在SpringBoot项目... 目录1、项目介绍2、 InfluxDB 介绍3、Spring Boot 配置 InfluxDB4、I

基于Java和FFmpeg实现视频压缩和剪辑功能

《基于Java和FFmpeg实现视频压缩和剪辑功能》在视频处理开发中,压缩和剪辑是常见的需求,本文将介绍如何使用Java结合FFmpeg实现视频压缩和剪辑功能,同时去除数据库操作,仅专注于视频处理,需... 目录引言1. 环境准备1.1 项目依赖1.2 安装 FFmpeg2. 视频压缩功能实现2.1 主要功

使用Python实现无损放大图片功能

《使用Python实现无损放大图片功能》本文介绍了如何使用Python的Pillow库进行无损图片放大,区分了JPEG和PNG格式在放大过程中的特点,并给出了示例代码,JPEG格式可能受压缩影响,需先... 目录一、什么是无损放大?二、实现方法步骤1:读取图片步骤2:无损放大图片步骤3:保存图片三、示php

使用Python实现一个简易计算器的新手指南

《使用Python实现一个简易计算器的新手指南》计算器是编程入门的经典项目,它涵盖了变量、输入输出、条件判断等核心编程概念,通过这个小项目,可以快速掌握Python的基础语法,并为后续更复杂的项目打下... 目录准备工作基础概念解析分步实现计算器第一步:获取用户输入第二步:实现基本运算第三步:显示计算结果进

Python多线程实现大文件快速下载的代码实现

《Python多线程实现大文件快速下载的代码实现》在互联网时代,文件下载是日常操作之一,尤其是大文件,然而,网络条件不稳定或带宽有限时,下载速度会变得很慢,本文将介绍如何使用Python实现多线程下载... 目录引言一、多线程下载原理二、python实现多线程下载代码说明:三、实战案例四、注意事项五、总结引