光线追踪渲染实战:蒙特卡洛路径追踪及其c++实现

于是我打算自己写一篇博客。以【把大象装进冰箱】进行比喻:如果说网络上的资料是【打开门,送大象进去,关门】的话,那本篇博客就是讨论【怎么装】的。比如冰箱门把手在哪里,朝哪个方向拉才能打开冰箱,要用香蕉还是鞭子把大象赶进去,怎么关上冰箱的门…

不多说批话,直接开始文本的内容!

在传统的计算机图形学中,光栅通常用于生成像素。因为光栅化后,我们只选择我们看到的图形,视野外的几何信息被切割,导致场景的整体信息丢失。然后它会导致一些渲染例如,水只能反映我们在屏幕上看到的像素:

此外,一些基于全球信息的渲染效果不能很好地运行,如全球光。在光栅管道下,我们通常需要通过各种类似的算法来模拟现实世界的光影现象,这是非常麻烦和有效的“还凑合”

超越传统图形流水线的光线跟踪是一种现代渲染。光跟踪是一种基于物理的渲染方法,通过模拟介质中光的真实性能来输出足够接近现实的图像。光从光源开始,通过各种反射折射进入相机:

从光源开始无数条到达相机的路线不太现实,从灯泡开始模拟光的传播,所以曲线拯救了国家,从相机的方向开始向场景投射射线,试图找出到达光源的可能路径:

本博客代码主要实现路径跟踪技术。路径跟踪技术是光跟踪的一个分支。通过将光投射到场景中,模拟光的行径,在物体和光源之间找到可行的路径,最后返回积累的颜色。

和光栅化不同,光线追踪的第一步是向世界空间中投射光线,这一步叫做RayCasting,对应位置的像素代表投射光遇到的实体,同时给相应位置的像素一定的颜色:

假设我们发射的光线击中一个点,我们将返回这个点的颜色作为最终像素的颜色。

那么如何描述一点颜色呢?我们介绍渲染方程↓

故事要从元和二年1986从2000年开始,科学家们首次提出渲染方程来描述一个点受到的光的影响。渲染像素的颜色是渲染方程的过程。一般意思是:一个点的光是由2部分是:

注意这个方向,允许这个点从它那里接收法向半球范围内方向的入射光:

如果我们想计算法向半球任何方向的入射光的积累,我们必须计算一个半球积分!以下是渲染方程的简化形式:

L(p)=E(p)∫L(q)cos(θ)dωiL\left(p\right)=E\left(p\right)\int_{}L\left(q\right)*cos(\theta)\\mathrm{d}\omega_{i}L(p)=E(p)+∫L(q)cos(θ)dωi

其中符号的解释如下:

L(x)→x点的光强度E(x)→x点发出的光q→从p点出发,方向为ωi的光命中了q点的物体θ→ωi与q点法向量的夹角\begin{array}{l}L(x)\rightarrowx\点的光强度\\E(x)\rightarrowx\点发出的光\\q\rightarrow从\p\点出发,方向为\\omega_{i}\的光命中了\q\点的物体\\\theta\rightarrow\omega_{i}\与\q\点法向量的夹角\end{array}L(x)→x点的光强度E(x)→x点发出的光q→从p点出发,方向为ωi的光命中了q点的物体θ→ωi与q点法向量的夹角

此外,∫\int_{}∫就是对法向半球入(出)射光线方向的积分。注意到渲染方程的递归形式,要想求解p点的光照我们必须先递归计算q点的光照值,如图:

递归式告诉我们:q点也反射了光到p点,反射光的强度等于q点的光强度乘以反射到p点的百分比cos(θ)

注:这里有个错误,我写的是θ是出射光wi和命中点q的法线的夹角,实际上渲染方程中,θ是出射光wi和出发点p点的法线的夹角,也就是Lambert’scosinelaw,或者说phong光照模型中的NdotL这个错误我在写完博客的半年之后才发现。。。不过因为我们用的是简化版的渲染方程,并非基于物理,即使你去掉这个cosine,也能够得到相对不错的结果,真正正确的伪代码应该是:

民科警告

对于p点的单位面积dp,光能够发射到q点的比率,取决于dp在入射光方向上面的投影面积:

了解了渲染方程的成分,就可以进行渲染方程的求解了。在高等数学中,积分的计算需要找到被积函数的原函数,和积分变量。可是渲染方程是一个困难积分,无法精确地计算其原函数,于是需要找寻其他方法对困难积分进行计算。

那么怎样计算一个困难积分的?我们引入蒙特卡洛方法↓

我更愿意称之为丢豆子。考虑最直观的情况,欲求区间上一函数的积分,我们往x区间上面丢豆子,并且计算豆子命中的位置的y的值,最后把他们加起来作为积分的估计:

丢豆子的过程称之为【采样】,如果我们使用均匀分布的x进行丢豆子,就能得到上图等宽度的柱状图近似。

事实上在实际问题中,豆子的位置不会总是服从均匀分布。那么每一个豆子的贡献,除了豆子命中位置的y值,还取决于豆子命中该位置的概率。蒙特卡洛方法允许我们使用x的任意的概率密度函数,来对积分进行估计。

假设采样x的概率密度函数为PDF(x)PDF(x)PDF(x),被积函数为f(x)f(x)f(x),那么x点采样的贡献就是:

f(x)PDF(x)\frac{f(x)}{PDF(x)}PDF(x)f(x)

所以积分计算就很简单了。首先按照产生一堆符合概率密度函数PDF分布的随机变量x,然后对每一个x都计算f(x)PDF(x)\frac{f(x)}{PDF(x)}PDF(x)f(x)最后求他们的均值即可。现在回过头来看刚刚的均匀分布的丢豆子,其中PDF(x)=1baPDF(x)=\frac{1}{b-a}PDF(x)=ba1,那么我们估计x2x^2x2的积分就可以这么计算:

可以看到仅5次采样就可以获得还不错的结果。我们和真实的积分值十分逼近了!采样的次数越多,差异就越少,所以蒙特卡洛方法可以做到对积分结果的无偏估计,这是好特性。

知晓了困难积分的近似求解方式,我们开始正式求解渲染方程↓

渲染方程是对于被求解点p的法向半球的积分,那么我们的随机变量就是在法向半球内的射线的方向,假设我们取法向半球内均匀分布的射线方向,那么就有PDF(x)=12πPDF(x)=\frac{1}{2\pi}PDF(x)=2π1,因为半球面积就是2π2\pi2π,如图:

于是有如下的伪代码:

pathTracing(p){ L=0.0 for(iinSAMPLE) { wi=random() //随机选择一个方向 if(wihitq) //射线wi击中q点 { cosine=dot(nq,wi) //q点法向量nq和wi的夹角余弦值 L+=cosine*pathTracing(q)/PDF(wi) } } returnL/SAMPLE //返回均值}其中SAMPLE就是我们的采样次数。那么问题就来了,这个递归的复杂度是指数,复杂度非常高带来的就是极大的计算资源的消耗,因为光线会有爆炸般的增长,以SAMPLE=100为例:

那么我们只有限制SAMPLE=1才能防止指数增长。而一次采样的估计肯定是不准确的,于是我们对每个像素,发射多条光线,然后平均他们的结果。每个像素的光线数目叫做SPP,即(SamplePrePixel),下图演示了SPP=3的情况,我们找寻了3条到光源的路径,并且平均他们的贡献:

注意只有在第一次采样时发射若干条光线,其余的时候我们只随机选取一个方向发射光线并且递归计算。那么伪代码就改成:

//追踪一条光线pathTracing(p){ L=0.0 wi=random() //随机选择一个方向 if(wihitq) //射线wi击中q点 { cosine=dot(nq,wi) //q点法向量nq和wi的夹角余弦值 L+=cosine*pathTracing(q)/PDF(wi) } returnL}//对一个像素投射SPP条光线L=0.0for(iinSPP){ wi=random() //随机选择一个方向 if(wihitq) //射线wi击中q点 { L+=pathTracing(q)/PDF(wi) }}L/=SPP这一步也没啥特别的,就是向每一个像素投射光线,然后求解渲染方程,没了。。。

渲染方程的伪代码有了,我们通过c++实现它↓

着手编写一个在Windows10下运行的x64程序,程序以图片的形式输出场景的渲染结果。我们以VusialStudio2019作为IDE,此外我们还需要额外的帮助库。

首先是数学运算库,我们需要一个能够表示三维向量,并且对向量进行加减乘除点积叉乘等操作的帮助库。你可以自己写一个简易的class,也可以使用现成的第三方库,这里我使用的是glm,它的网站在这里,你也可以从它的GitHub上面获取。此外,也可以通过vcpkg包管理工具来下载,只需要运行命令:

vcpkginstallglm如果在安装时遇到任何困难,可以参考我以前的博客:传送门①,传送门②

你可以使用任何流行的图像处理的库来进行图像输出,他们可以是Opencv,Qt,甚至是OpenGL,但是这里我使用非常轻量级的svpng。svpng不是一个c++的第三方库,它仅是一个inc文件:

你只需要把它放在你的工程目录下,然后再#include"svpng.inc"即可调用它。svpng就一个非常简单的功能,就可以帮我们保存png图像,调用svpng函数即可。函数的原型长这样:

voidsvpng(FILE*fp,unsignedw,unsignedh,constunsignedchar*img,intalpha)其中FILE是文件指针,w和h是图片的宽度和高度,img是图像的像素值数组,alpha是透明度,我们填0即可。通过如下的代码就可以将一个范围在[0,1]之间的double浮点数buffer输出到图片上:

//输出SRC数组中的数据到图像voidimshow(double*SRC){unsignedchar*image=newunsignedchar[WIDTH*HEIGHT*3];//图像bufferunsignedchar*p=image;double*S=SRC;//源数据FILE*fp;fopen_s(&fp,"image.png","wb");for(inti=0;i

随便在SRC中写点什么,比如输出xy的值作为rg通道。如果你看到如下的图片被生成,那么很成功!

如果找不到svpng.inc那么检查你的vs工程是否配置正确,将包含svpng的目录添加到vs的include目录:

光线追踪运算量巨大,单靠简单的单线程程序无法高效执行,但是因为每个光线的采样是相互独立的,于是我们可以利用多线程加速。VisualStudio有自带多线程加速的openmp库,无需自己手动下载,只需要引入:

#include//openmp多线程加速同时在项目设置中,允许vs使用多线程:

然后在需要并行执行的for循环之前加上:

omp_set_num_threads(50);//线程个数#pragmaompparallelforfor(){ ...}就可以享受多线程加速的福利了。此外,我墙裂建议你打开O2优化同时将运行模式调整到Release,以获取最大运行速度:

一切就绪?我们准备进入光与粒的世界↓

光线追踪的第一步是投射光线,我们模拟相机投影与成像的规则,指定一个[-1,1]范围内的投影平面和一个视点,然后根据输出图片的像素位置,计算其对应投影平面上的坐标,最后用坐标减去视点坐标,得到视线的方向向量,如图:

值得注意的是,图片的xy轴原点是在图片左上方,而实际投影我们需要一个在左下方的原点(即平面几何坐标系),所以y要做一次flip。此外,在世界坐标系下,我们确定相机的位置和投影平面的位置,让相机看向z轴负方向:

相机配置就绪,我们尝试输出相机的射线投射方向,其中imshow是上面编写的显示图片的函数:

double*image=newdouble[WIDTH*HEIGHT*3];memset(image,0.0,sizeof(double)*WIDTH*HEIGHT*3);double*p=image;for(inti=0;i

相机投射了光线,光线和场景物体相交,我们希望描述这一过程↓

在计算机图形学中通常使用三角形来描述任意形状的物体,因为三角形具有很好的几何特征,并且易于进行求交,裁剪等操作。在开始之前,我们先做一些规范化的定义:

假设我们用起点(start)和方向(direction)来描述一个射线:

//光线typedefstructRay{vec3startPoint=vec3(0,0,0);//起点vec3direction=vec3(0,0,0);//方向}Ray;在开始编写三角形类之前,我们先确定求交操作到底要返回那些信息:

那么首先我们有表面属性的定义,使用结构体能很好的帮我们组织数据,同时易于拓展:

//物体表面材质定义typedefstructMaterial{boolisEmissive=false;//是否发光vec3normal=vec3(0,0,0);//法向量vec3color=vec3(0,0,0);//颜色}Material;然后是光线求交结果的定义:

//光线求交结果typedefstructHitResult{boolisHit=false;//是否命中doubledistance=0.0f;//与交点的距离vec3hitPoint=vec3(0,0,0);//光线命中点Materialmaterial;//命中点的表面材质}HitResult;然后是三角形的class的定义:

classShape{public:Shape(){}virtualHitResultintersect(Rayray){returnHitResult();}};//三角形classTriangle:publicShape{public:Triangle(){}Triangle(vec3P1,vec3P2,vec3P3,vec3C){p1=P1,p2=P2,p3=P3;material.normal=normalize(cross(p2-p1,p3-p1));material.color=C;}vec3p1,p2,p3;//三顶点Materialmaterial;//材质//与光线求交HitResultintersect(Rayray){HitResultres;//...returnres;};};这里我墙裂建议使用虚函数+指针+继承的编程习惯,因为我们光线和任意图形求交,都有一致的返回结果,即HitResult结构体。我们使c++的指针特性,可以通过一套代码,完成多种复杂图元的求交。此外,在添加一种新图元的时候,主代码不需要任何的改动!(虽然现在我们只有三角形。。。

和三角形的求交分为两个步骤:首先判断光线和三角形所在平面是否有交点,然后再判断交点是否在三角形内部。思路很清晰,我们先来看光线与三角形所在平面相交的判断:

其中t表示了射线起点到交点的距离,如果t小于0那么表示三角形在摄像机之后!,然后开始判断点是否在三角形中。我们连接顶点与P点,然后判断连线与边的叉乘方向是否与法向量一致。如果三个顶点的判断都通过,说明P在三角形中,否则不在:

THE END
1.9个程序员接私活的网站推荐,最后一个和想象差太远了吧!猪八戒网是一个威客类可以在线接单子赚钱的网站,简单说就是需要找人做设计的雇主发布任务到猪八戒网上,然后一些服务商与个体威客可以去报价接单,完成单子后这个钱就可以赚到手了。 但猪八戒的服务商越来越正规化,个人要进去首先就需要交纳一大笔服务费用,并且抽成很高,只是个人的话,能力又不是非常突出是不太建议的https://blog.csdn.net/hackeroink/article/details/144419175
2.速看丨线上C++零基础寒假班,开课啦!速看丨线上C++零基础寒假班,开课啦!2024-12-13 14:17:18 来源: 慧明科技 北京举报 0 分享至 精彩内容早知道 特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。 Notice: The content above (including the pictures and videos if any) is https://www.163.com/dy/article/JJ9V0IDL0552AN36.html
3.C/C++兼职接单外包平台招聘其他后端开发接单云工网提供C/C++兼职接单招聘平台:数千名优秀在线全职兼职开发外包接单、电商、网站、APP等;C/C++3天免费试用,先体验后雇佣!https://www.yungong.com/c
4.淘宝运营策划书4、避免顾客查询订单,主动把底单/流水号发给顾客。 5、对于货到付款,请尽量先致电给顾客。 6、接到顾客投诉电话后,应给顾客以放心答复。 (四)网店美工 1、负责网店产品上传宝贝的文字编辑及上传宝贝的相关工作,图片拍摄制作。 2、根据主题需要完成店铺进行整体的美化(公告栏和促销栏图片设计)。 https://www.ruiwen.com/yunyingcehuashu/5419815.html
5.ps接单的有哪些网站?8. 天猫淘宝:通过淘宝店家发布商品和需求信息进行接单。请注意,这些网站可能存在不同的可靠性、安全性和合法性问题,请在接单前仔细阅读平台政策,并确保与对方进行明确的沟通和协商。同时,也要注意保护自身权益和安全。 有用(0) 回复 10331c 1、网上:猪八戒网,特微特网站,威客平台里面有知名的平面设计师,也https://wap.zol.com.cn/ask/x_25611856.html
6.C++进阶学习第三弹——菱形继承和虚拟继承——菱形继承的二义C++单继承 在C++中,单继承是指一个类只能继承自一个基类。这意味着派生类只能有一个直接的基类。 单继承的语法如下: 代码语言:javascript 复制 classBase{public:voidbaseFunction(){cout<<"Base function"<<endl;}};classDerived:publicBase{public:voidderivedFunction(){cout<<"Derived function"<<endl;}}; https://cloud.tencent.com/developer/article/2430694
7.qt5编写的单例模式Qt Quick application单例实现 上传者:IT8343时间:2023-06-25 C++设计模式--基于Qt4开源跨平台开发框架 C++设计模式--基于Qt4开源跨平台开发框架 上传者:gengkui9897时间:2019-01-06 QtC++设计模式代码 C++Qt设计模式课本的代码,网上找到贡献给大家,一起分享一起学习一起成长,希望对大家有用。。。 上传者https://www.iteye.com/resource/klsh496979112-10227758
8.C/C++学习笔记(2020.112021.5)51CTO博客C/C++语言的诞生 C CPlusPlus 标准库STL版本 HP(Hewlett-Packard) STL SGI (Silicon Graphics Computer System, Inc)STL STLport RW(Rouge Wave)实现版本 P.J.Plauger实现版本 非标准库 程序运行过程 预处理 生成汇编指令 生成二进制文件 生成linking文件 https://blog.51cto.com/u_15233215/6169893
9.C++单链接限速对象存储(OSS)C++单链接限速 更新时间:2024-11-27 13:30:36 产品详情 本文介绍如何在上传、下载文件(Object)时,通过在请求中携带限速参数并设置限速值,以保证其他应用的正常带宽。 注意事项 本文以华东1(杭州)外网Endpoint为例。如果您希望通过与OSS同地域的其他阿里云产品访问OSS,请使用内网Endpoint。关于OSS支持的Region与https://help.aliyun.com/document_detail/129783.html
10.世界500强所属国企事业单位高新企业的面谈机会别错过!2.熟悉Python,C/C++; 3.了解ITK/VTK, OpenCV; 4.了解Linux环境下的开发; 5.良好的英语读写及沟通能力; 6.良好的沟通协作及抗压能力。 生物力学实验研究员(贵阳花溪或北京朝阳) 1.参与骨科植入物的生物力学实验和人体步态分析。 2.跟踪、总结、复盘国外前沿研究 https://www.thepaper.cn/newsDetail_forward_17587878
11.手把手教你实现一个C++单链表C语言链表是一种数据结构,用于数据的存储。这篇文章主要为大家介绍了如何实现一个C++单链表,文中的示例代码讲解详细,感兴趣的小伙伴可以尝试一下+ 目录 GPT4.0+Midjourney绘画+国内大模型 会员永久免费使用!【 如果你想靠AI翻身,你先需要一个靠谱的工具!】 一. 节点声明 链表是一种数据结构,用于数据的存储。 如图,https://www.jb51.net/article/268764.htm
12.高效易用的C++单元测试框架:轻松构建高质量代码在上面的代码中我们定义了一个C++中的单元测试框架核心类UnitTestCore,它提供了注册测试用例、运行测试用例等功能。首先,它定义了一个静态函数GetInstance(),用于获取单例对象。这个函数使用了静态局部变量,保证了线程安全。接着,它定义了一个Run()函数,用于运行所有注册的测试用例。这个函数接受两个参数,分别是命令行https://www.jianshu.com/p/62b764141992
13.金三银四跳槽季,为了年后offer,我竟然撸完了这50场1000多道面经2. C++的new和malloc 3. C++重载 4. 进程间通信的互斥方法 5. 堆和栈的区别 6. 介绍进程和线程区别 7. linux内核 8. 介绍每种设计模式以及应用 9.单核cpu多线程 10. 指针有了解吗 11.文件软链接硬连接 二面 1. linux 怎么进shell 2. linux的swap分区 https://maimai.cn/article/detail?fid=1589559958&efid=HMnVzUsEaNASGCiWLCQB4Q
14.哈尔滨阿里巴巴笔试题目3.(6分)两个较长的单向链表a和b,为了找出及诶单noed满足node in a 并且node in b。请设计空间使用尽量小的算法(用c/c++,java 或者伪代码) 答案网上找的` 我们定义节点的距离为节点到链表开始所经过的节点数。如果两个链表长度相同,则相交节点其在两个链表上的距离一定相等。对于长度不同的两个链表,我们可https://www.yjbys.com/bishi/timu/787902.html
15.C++少儿编程轻松学写给中小学生的零基础教程本书由浅入深地引导读者学习C++编程,涉及变量、分支、循环、数组、字符串、函数等重要知识点,同时总结了程序调试技能、编程规范、考试经验等。本书包含丰富实用的代码示例,旨在帮助学生快速入门C++编程,并能够在案例的引导下,进一步夯实C++编程技能,轻松应对各类编程竞赛。除此之外,本书还提供了配套的题库供读者自行练http://epubit.com/bookDetails?id=UBb64008d82eaa
16.C++学习之路1建立一个基于单文档的MFC应用程序SCPortTest,所有步骤保持缺省状态。 ◆2. 添加类文件: 将SerialPort.h SerialPort.cpp两个类文件复制到工程文件夹中,用Project-Add to Project-Files命令将上述两个文件加入工程。并在SCPortTestView.h中将头文件SerialPort.h说明:#include "SerialPort.h"。 http://www.cppblog.com/asdjy123/archive/2012/09/06/189692.html
17.第35次CCFCSP认证(2024年9月22日)报名通知6.认证结束3个工作日后通过登陆http://cspro.org网站查看成绩,5个工作日后直接打印带红色公章成绩单。 7.作弊处理:认证现场除了有监考老师巡视外,CCF会派巡视员抽查各认证点。为发现CSP认证中的作弊现象,在评测过程中,CCF CSP技术委员会将采用两两比对的方法,对所有参加认证者的源程序进行比对,如发现源程序存在http://cst.hitwh.edu.cn/_t533/2024/0825/c371a186479/page.psp