光线追踪 embree使用教程
embree基本使用教程
摘要
1.背景介绍
2.学习Embree需要的知识背景
2.1 必须需要了解光线追踪的原理
2.2 需要会用C++或者C语言
3.Embree介绍
3.1 embree是Intel开发的一个光线追踪内核
3.2 Embree版本
4.下载Embree
4.1 embree下载方法
4.2 安装TBB
5. 部署embree
5.1 需要的工具:
5.2 创建项目
6.使用Embree
6.1看看你部署好的embree能不能正常使用
6.2 开始实现最简单的测试代码
7.总结
附录
摘要
本教程包括embree的下载以及使用,并通过一个简单的示例介绍embree的基本使用方式,了解并掌握emrbee的使用。
1.背景介绍
光线追踪(Ray Tracing)在游戏中开始应用起来,Nvdia的RTX技术已经实现了在电脑上开启实时光线追踪(Real-time Ray Tracing)进行游戏,如游戏《控制》,而下一代的xbox、ps系列主机也将在硬件上支持实时光线追踪,相信光线追踪会开始普及并流行起来。与以往的光栅化(Rasterization)相比,使用了光线追踪技术渲染后的画面效果会非常逼真,大家感兴趣的话建议可以百度搜索一下光线追踪的对比视频感受一下,实时光线追踪会因为稳定一个可接受的帧数,牺牲一些画面效果,如果仅仅对一个静态场景进行光线追踪,不考虑渲染时间,最后渲染出一副图片,可以获得一张如照片般逼真的(Photo-realistic)图片。
2.学习Embree需要的知识背景
2.1 必须需要了解光线追踪的原理
光线追踪是计算机图形学中的一个领域,基本原理是很简单的,百度一下可以学习光线追踪的基本原理,但是如果你能更深入地了解光线追踪的话就更容易学习Embree了。推荐两个更深入学习光线追踪的途径:
(1)《Fundamentals of Computer Graphics》可以看第四章对光线追踪的基本介绍。
(2)《Ray Tracing in a Weekend》书中教你在不使用任何图形API的情况下用C++语言编写一个简易的光线追踪器。
这两本书都是英文书籍,但是语言都很简单,读起来再搭配翻译的话难度适中,尤其推荐第二本书,读完可以对实现光线追踪器有个具体的了解,对学习使用embree帮助很大。
2.2 需要会用C++或者C语言
因为Embree是用C语言的,用C++语言也可以。因为光线追踪是计算机图形学的内容,所以学习光线追踪的时候也会面临一个问题就是代码量很大,所以需要有个比较好的C++语言基础。
3.Embree介绍
3.1 embree是Intel开发的一个光线追踪内核
实际上我是把它理解为一个API(应用程序接口)。Embree是使用CPU进行运算的,并非显卡(如果这里有误请告诉我),可以发挥CPU的并行计算能力。总结:使用Embree可以帮助你实现光线追踪,而且可以是实时的,并行的(包括线程并行和数据并行)。类似的有Nvdia的Optix光线追踪内核,这个内核仅支持自家的显卡(如果这里有误请告诉我),并且用GPU进行运算,因为我百度出来有关于Optix的教程,但是没有找到关于Embree的教程,而且我学习Embree的时候也在起步阶段没有一个详细点的教程,自己摸索了很多后才好说真正部署好用上了,所以我花大篇幅写一个入门教程。
3.2 Embree版本
embree到我写教程的时候已经发展到了embree 3.8.0版本了,也是embree3版本,这是区别与embree2而言的,两个版本有不少变化,即API会有不同,如果你在GitHub上或者别的地方找到别人的代码,一定要区分该代码用的是embree2还是embree3,如果是embree2的话,那就仅仅学习代码的思路就好,代码已经不能用了,有能力的可以改写为embree3的版本。
4.下载Embree
(因为下载很慢,为了方便大家我分享出来,embree版本与本文版本一致:
链接: 下载地址
)
4.1 embree下载方法
embree官网:下载地址
下载地址:https://www.embree.org/downloads.html
embree支持windows、linux、mac平台,我的教程用的是windows平台,我也学习了在ubuntu平台下的使用,可能接下来再写一篇linux(Ubuntu)系统下使用embree的教程。接下来我会教你以最快最方便的方法用上embree。
图1 在embree官网里面下载windows版本的embree
(1)建议下载图1中的“embree-3.8.0-x64.vc14.msi”,下载后直接双击安装到你能找到的位置,建议默认:“C:\Program Files\Intel\Embree3 x64”。下载的时候注意x64对应你的系统是64位的,x86对应你的系统是32位的。
图2 安装embree的界面
(2)安装后不代表可以直接使用,需要添加环境变量。打开环境变量的设置界面,如果不知道怎么找到环境变量,可以先停下来百度一下再继续。
图3 打开系统环境变量
按照图3打开,可以编辑用户变量和系统变量:
*在用户变量的“Path”中添加你的embree安装路径,以上面的“C:\Program Files\Intel\Embree3 x64”为例子,结果如图4所示。(这步可以省略,以防万一最后做了)
图4 在用户变量中加入embree安装路径
*在系统变量的“Path”中加入embree的安装路径,以及安装目录下的bin和include文件夹(这步骤我添加的多了,不太严谨,也是以防万一,可以继续改进),结果如图5所示。
图5 在系统变量中加入embree安装路径
4.2 安装TBB
TBB是 Intel® Threading Building Blocks(线程构建基块),这是从源代码编译embree必不可少的,但是教程现在不是从源代码构建embree的方式使用embree,所以似乎是可以跳过的,但是我为了完整性也是以防万一,所以也将TBB一起构建了。前文提到Embree是支持线程并行的,TBB就是为线程并行提供支持的。
(1)TBB官网:下载地址
下载地址:https://github.com/intel/tbb/releases
图6 下载TBB
打开网址后下载“tbb-2020.1-win.zip”。注意图6红框的后面后缀win,如果有新的版本直接找win后缀的下载即可。
(2)下载后解压缩然后将解压后的文件夹添加到系统变量即可。没有双击安装的步骤。
图7 “tbb-2020.1-win.zip”解压后并被我重命名为TBB
图8 加入到系统变量的路径
如图7和图8,将TBB的路径加入到了系统变量中则等于安装好了TBB。
5. 部署embree
5.1 需要的工具:
(1)安装windows 10 的电脑。
(2)安装好了visual studio 2019 ,我用的是免费的community版本。
(3)安装好上文的embree。
(4)安装好上文的TBB。
5.2 创建项目
用visual studio 2019创建一个C++ 控制台项目
图9 创建一个C++ 控制台项目
(1)如图9创建一个C++项目,创建项目后,需要为项目附加embree,才能使用embree。
(2)打开项目,在vs2019最上面一栏找到“项目”——》“属性”。如图10。
图10 打开项目属性
(3)在属性页找到“c++”——》“常规”——》“附加包含目录”,添加你安装了embree下的include文件夹路径。如图11。
图11 添加“附加包含目录”
注意要将配置设置为x64平台,如图的11中的步骤1。
(4)在属性页找到“链接器”——》“常规”——》“附加库目录”,添加你安装了embree下的lib文件夹路径。
图12 添加“附加库目录”
(5)在属性页找到“链接器”——》“输入”——》“附加依赖项”,添加“embree3.dll”,直接写这个名字即可,不用写路径。如图13.
以上几步都很重要,完成以上几步后就可以开始测试能否用embree了。
6.使用Embree
embree安装目录下有个doc文件夹,里面放着readme.pdf,这是embree的API说明文档,要使用embree需要经常翻阅该API文档。
下面我介绍一个最简单的使用embree的代码,比官方示例的最简单代码还要简化。代码实现的功能是:在设备上创建一个只有一个三角形面片的三维场景,并且发射一条光线射向该场景,并判断光线有没有命中该场景下的三角形面片。这句话有可能读起来很绕,没关系,等学习完下面的示例后,记得再重新看我刚刚说的代码功能。以下代码用的是C++编程语言。
6.1看看你部署好的embree能不能正常使用
方法:写一个最简单的helloworld程序,然后尝试包含一下embree。
#include<iostream>#include<embree3/rtcore.h> //仅仅加入了这行代码进入helloworld程序中using namespace std;int main(){ cout<<"hello world"<<endl; return 0;}
注意要使用embree最基本的是用#include<embree3/rtcore.h>,可以理解为这条语句加载了embree API。上面的代码尝试运行一下,如果正常输出hello world的话,代表embree可以正常使用。需要注意,运行程序时候要如图14那样设定x64再用本地windows调试器运行程序。如果不是x64的话会报错。
图14 注意事项
6.2 开始实现最简单的测试代码
要实现的程序是判断一条光线是否击中场景中的几何体,这是光线追踪实现原理中最基本的一个步骤。所以我们需要创建一个几何体,然后创建一条光线,再使用函数function(场景,光线)来判断是否判断几何体和光线是否相交。
(1)创建几何体
这里目的是创建一个三角面片。这里简述一下embree中的创建几何体的步骤:
创建一个设备(device)——》创建该设备下的场景(scence)——》创建一个三角形面片(geometry)——》将该三角形面片添加入场景中。
*创建设备:
查阅API文档:
RTCDevice device = rtcNewDevice(NULL);
这步骤的目的请查阅API文档的“rtcNewDevice()”看详细介绍。RTCDevice是embree API中的数据类型,rtcNewDevice()是embree API中的函数,都不需要开发人员自己定义,也不需要理解是怎么实现的,只需要看API文档查看是怎么使用的就行了。
*创建场景:
RTCScene scene = rtcNewScene(device);
与上面同理,函数的解释和用法都在API文档里面。注意rtcNewScene()参数用了上面创建的device对象。代表再该device设备下创建了场景scene。
*创建三角形面片:
这里不是一两句代码的事情了,你有纸和笔的话可以拿出来,画一个三维坐标系。你想要创建一个三角形面片,你需要自己设计这个三角形三个顶点的具体三维坐标点。
RTCGeometry geom = rtcNewGeometry(device, RTC_GEOMETRY_TYPE_TRIANGLE);
首先创建一个设备device下的几何体geom,并指定几何体基本类型是三角形。(就是由一个个三角形面片组成的几何体)这个几何体geom对象还不是实体,需要我们往下看。
我这里定义一个三角形,三个顶点的坐标分别为:(0,0,0),(1,0,0),(0,1,0)。另外embree还需要设置三角形索引。
定义三角形前,需要创建各个顶点buffer,同时也需要创建索引buffer。
·创建三角形顶点缓存:
float* vertices = (float*)rtcSetNewGeometryBuffer(geom, RTC_BUFFER_TYPE_VERTEX, 0, RTC_FORMAT_FLOAT3, 3 * sizeof(float), 3);
该代码是创建了顶点的buffer,参数列表可以查阅API文档。大概意思是,为几何体geom创建一个顶点类型的buffer缓存,然后缓存类型是float3(三个float,对于坐标三个值),共设置三个3*float的大小。简单理解为我为三个坐标点创建了缓存,而且缓存大小刚好是三个坐标点的大小之和。
接下来还得创建所以缓存。
·创建索引缓存:
unsigned* indices = (unsigned*)rtcSetNewGeometryBuffer(geom, RTC_BUFFER_TYPE_INDEX, 0, RTC_FORMAT_UINT3, 3 * sizeof(unsigned), 1);
也是查看API文档。设置好了缓存,然后就可以定义各个顶点和索引了。
·定义顶点:
vertices[0] = 0.f; vertices[1] = 0.f; vertices[2] = 0.f;vertices[3] = 1.f; vertices[4] = 0.f; vertices[5] = 0.f;vertices[6] = 0.f; vertices[7] = 1.f; vertices[8] = 0.f;
三个三维坐标刚好就是九个float值,其实应该要我们开发者预先定义一个ver3的结构体,用来代表坐标点,但是为了代码最直接,所以直接逐个点给定义了。
·定义索引:
indices[0] = 0; indices[1] = 1; indices[2] = 2;
创建完了geom的顶点和三角形索引,我们就相当于定义好了几何体geom就只是一个三角形面片,定义好了几何体,我们要进行提交,相当于给几何体定型。
·提交几何体geom:
rtcCommitGeometry(geom);
·将几何体添加到场景scene中:
rtcAttachGeometry(scene, geom);
此时相当于我们的场景中有了一个定义好了具体位置的三角形,接下来我们计算光线与物体相交都是使用场景scene这个整体。
·已经添加入场景了,我们可以释放几何体geom:
rtcReleaseGeometry(geom);
(2)创建光线
完成第二步骤,创建一条光线。embree中已经有定义好的代表光线的结构体了:RTCRAY。可以查看API文档看RTCRAY中的成员函数。分别需要我们填写光线的起点坐标(向量),光线的方向坐标(向量),用于动态模糊功能的光线,光线射多远距离,光线的ID,Mask,Flags。另外有个记录碰撞信息的结构体RTCHit,记录碰撞点的信息。具体内容请查阅API文档。
但是我们真正要使用的是RTCRayHit结构体,它就是简单地包含了上面的RTCRay和RTCHit结构体。
·定义一条光线:
//定义光线起点rayhit.ray.org_x = 0;rayhit.ray.org_y = 0;rayhit.ray.org_z = -1;//定义光线方向rayhit.ray.dir_x = 0;rayhit.ray.dir_y = 0;rayhit.ray.dir_z = 1;//光线从0开始射向无限远方rayhit.ray.tnear = 0;rayhit.ray.tfar = std::numeric_limits<float>::infinity();rayhit.ray.mask = 0;rayhit.ray.flags = 0;//必须写默认值RTC_INVALID_GEOMETRY_IDrayhit.hit.geomID = RTC_INVALID_GEOMETRY_ID;rayhit.hit.instID[0] = RTC_INVALID_GEOMETRY_ID;
如上我们就创建了一条光线。
(3)光线与场景中物体求交
·定义上下文:
struct RTCIntersectContext context;rtcInitIntersectContext(&context);
具体作用请翻阅API文档,大致是用来记录求交点运算时候的上下文信息,创建方式基本是如上固定的。
·计算光线与物体交点:
rtcIntersect1(scene, &context, &rayhit);
得益于API,只需要一条语句。函数名后面的1代表是每次计算一条光线。光线的计算结果会保存在RTCRayHit结构体定义的rayhit,例如与光线相交的的表面法向量以及相交的几何体的ID号。
·判断光线与物体是否相交:
if (rayhit.hit.geomID != RTC_INVALID_GEOMETRY_ID){cout << "Found intersection on geometry " << rayhit.hit.geomID << "primitive " << rayhit.hit.primID << " at tfar=" << rayhit.ray.tfar << endl;}elsecout << "Did not find any intersection." << endl;
简单说明:求交运算后的rayhit,看看计算后的geomID(即相交物体的ID)是否等于默认值RTC_INVALID_GEOMETRY_ID (代表没有碰撞),若发生了碰撞,则会保存几何体的ID号,以此判断是否碰撞。并输出tfar代表在多元处发生碰撞。
(4)收尾工作
·释放资源:
rtcReleaseScene(scene);rtcReleaseDevice(device);
因为创建伴随而来就是空间,所以最后我们要释放空间,先释放场景scene,再释放设备device。
(5)执行
我预设的光线值是可以尝试碰撞的,执行结果如下图:
图15 执行结果:成功相交
可以自行去调整光线的方向向量或者其他参数,会出现没有成功相交的结果。
7.总结
可以说计算机图形学中的HelloWorld是画一个三角形,但是上面的程序还没有画出一个三角形,不过也足以简单入门了,但是距离运用embree还很远,还需要不断的学习和练习,如果你能跟着教程完成我以上的输出的话,那么恭喜你,embree你已经能用了,接下来就是利用API文档学习官方案例了。
上述的代码比官方案例中的最简那个minimal还要简单,方便大家更直观理解,请注意安装教程中的安装方式安装后,我们是没有官方示例代码的,只有已经可以运行的官方示例程序,如图16。
图16 官方示例程序
官方示例程序在embree安装目录下的bin文件夹可以找到,双击便可以运行,如果想看源代码的话,还需要去开头我们下载embree的网站上面下载open source(开源)版本。当然open source版本连embree都需要我们自己编译,强烈不推荐这种自己编译embree的方式,因为要用到cmake很麻烦,建议是安装本文教程部署好embree后再去下载开源版本学习官方示例的源代码即可。
编译embree很麻烦,有兴趣的可以联系我,我已经再windows和Ubuntu下编译成功,有时间我可能再写一篇再Ubuntu下编译embree源码的文章。
附录
本教程示例的完整代码:
#include<iostream>#include<embree3/rtcore.h>#include<limits>using namespace std;int main(){ RTCDevice device = rtcNewDevice(NULL); RTCScene scene = rtcNewScene(device); RTCGeometry geom = rtcNewGeometry(device, RTC_GEOMETRY_TYPE_TRIANGLE); float* vertices = (float*)rtcSetNewGeometryBuffer(geom, RTC_BUFFER_TYPE_VERTEX, 0, RTC_FORMAT_FLOAT3, 3 * sizeof(float), 3); unsigned* indices = (unsigned*)rtcSetNewGeometryBuffer(geom, RTC_BUFFER_TYPE_INDEX, 0, RTC_FORMAT_UINT3, 3 * sizeof(unsigned), 1); vertices[0] = 0.f; vertices[1] = 0.f; vertices[2] = 0.f; vertices[3] = 1.f; vertices[4] = 0.f; vertices[5] = 0.f; vertices[6] = 0.f; vertices[7] = 1.f; vertices[8] = 0.f; indices[0] = 0; indices[1] = 1; indices[2] = 2; rtcCommitGeometry(geom); rtcAttachGeometry(scene, geom); rtcReleaseGeometry(geom); rtcCommitScene(scene); struct RTCIntersectContext context; rtcInitIntersectContext(&context); struct RTCRayHit rayhit; rayhit.ray.org_x = 0; rayhit.ray.org_y = 0; rayhit.ray.org_z = -1; rayhit.ray.dir_x = 0; rayhit.ray.dir_y = 0; rayhit.ray.dir_z = 1; rayhit.ray.tnear = 0; rayhit.ray.tfar = std::numeric_limits<float>::infinity(); rayhit.ray.mask = 0; rayhit.ray.flags = 0; rayhit.hit.geomID = RTC_INVALID_GEOMETRY_ID; rayhit.hit.instID[0] = RTC_INVALID_GEOMETRY_ID; rtcIntersect1(scene, &context, &rayhit); if (rayhit.hit.geomID != RTC_INVALID_GEOMETRY_ID) { cout << "光线与几何体发生碰撞了,几何体ID是: " << rayhit.hit.geomID << " 发射碰撞的距离=" << rayhit.ray.tfar << endl; } else cout << "光线没有与物体发生碰撞。" << endl; rtcReleaseScene(scene); rtcReleaseDevice(device); return 0;}