源网址链接

工作原理

课程初始,我们会解释怎样把一个三维场景只作为可视化的二维图片。一旦我们理解了这个过程和过程的参与制,就能运用电脑来简单的模拟一个“人造”图片。这部分更像是一个前端CG技术的理论根基。

在课程的第二部分,我们会介绍光线追踪代数和解释,它在小空间中怎样运行的?我们收到了各界人士寄来的邮件,询问我们为什么如此关注光线追踪,而不是其他代数理论?事实上并非如此。我们之所以选择关注光追,只是简单的因为它是最直接的模拟物体成像现象的代数理论。同理,我们相信,在写出创作简单图片的程序时,光追是相对于其他技术而言最好的选择。

我们会从光追代数基础开始。但是,一旦我们了解了所有的信息,就要动手制作一个扫描线渲染器来作为例子。我们会展示方法。

图片是怎么产生的?

制作一张图片,我们首先需要一个二维平面(该平面应有面积,而不是一个点),那么我们就能想象一个顶点在眼睛处、高与视线(就是连接眼睛和观察的物体的线)平行的金字塔,而它的切面(或是截面、视平面)就是我们观察的图像,你可以把它看作画家手中的画布。视平面是计算机图形学的概念,我们把它作为把三维场景投影的目标二维平面。很显然,我们刚刚讲述的只是众多不同创建图片方法中最基础的概念之一。例如,在摄影时,视平面也就是影片的幕布。

透视投影

想象我们要在空白的画布上绘制一个立方体,而描述透视原理最简单的方法就是从立方体的每个顶点上向眼睛连线。在每条线和眼前的画布交汇之处做上记号,也就在画布上投射出了物体的形状。举例来说,C0点是立方体的某个顶点,它又连接着C1、C2、C3三个点。把这四个点投射到画布上受,就得到了C0'、C1'、C2'、C3'。当C0-C1定义了一条边时,便连接C0'和C1'。对C0、C2重复,就有了C0'-C2'。

对立方体的其他边重复此行为,画布上最后就有了此立方体的二维象征,这就完成了第一个透视投影的图片。如果对每个场景中的物体重复此过程,就得到了特定观察点的场景图片。这只是15世纪的画家们理解透视投影的起点。

光和颜色

我们已经知道了如何在二维平面上绘制三维物体的轮廓,是时候加点颜色来完成这张图了。

快速总结我们刚刚学到的:我们能用两步绘制一个三维场景——首先将三维形状投影到视平面,这步只需要连接物体和眼睛。然后在画布上连接各个线段与视平面的交点,就有了物体轮廓。或许你发现了,这是个几何过程。第二步就是向图片的骨架上添加颜色。

场景中物体的颜色和明度大体是由光线和物体的材质的交互决定的。光线由光子(电磁的粒子)组成的,它们像沿直线运动的声波一样携带能量和波动,光子是由多种的光源发射的,最典型的例子就是太阳。如果一组光子击中了某物体,可能发生三件事:光子被吸收、反射或透过物体。光子被吸收、反射、透过的百分比因材质而异,也决定了该物体在场景中的外观。但是,所有材质的共同点就是,光子在被材质吸收、反射、透过的前后的总数总是相等。也就是说,如果有100个光子到达了物体表面的某一点,假设有60个被吸收了,40个被反射了,总数依旧是100。此时必定不会有70个被吸收而60个被反射,或20个被吸收50个被反射。

科学上讲,材质只被分为两类:被称作导体的金属和绝缘体。绝缘体,包括玻璃、塑料、木头、水等。这些材质都有绝缘的属性(但纯水是导体)。注意,绝缘体材质既可以是透明也能是不透明的——下图中的玻璃球和塑料球都是绝缘体材质。实际上,所有材质在特定电磁辐射下或多或少都是透明的。例如X光就能穿透人体。

同一物品也可以被多层材质或材质的组合制成,例如不透明物体(比如木头)可以有清漆制成的透明外壳包裹,这样使得它同时拥有漫反射和闪耀的外观,就像下图中的彩色玻璃球。

现在思考下不透明且漫反射的物体,为了简化情况,我们假定物体的颜色负责了吸收光子的过程。首先,白光是由红色、蓝色、绿色光子构成的,所以如果一束白光击中了红色的物体,那么吸收过程就会过滤(或吸收)绿色和蓝色的光子,由于物体未吸收红色光子,它们就被反射了。这就是这个物体看起来是红色的原因。而现在我们能看到这个物体的根本原因,则是一些被反射的红色光子到达了我们的眼睛。所有被照亮的物体或表面,都会向各个方向放射光线,只有那些垂直进入眼睛的光线才能被看到。我们的眼睛由无数感光细胞组成,它们负责把光信号转变为神经信号。之后我们的脑子就能用这些信号转译为不同的阴影和色调(我不确定具体流程)。这是极度简化后的描述,在这个色彩课程中会有更多细节(https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics)

如同透视投影的概念,人类花费许多时间才理解光。希腊人发展了一个视觉理论:物体是由眼睛中发出的光被看到的。一位阿拉伯科学家海什木(Ibn al-Haytham ,c. 965-1039)是第一个提出“是阳光使我们看得见东西”的人。沿直线运动的细小粒子流被从物体反射到我们的眼睛里,组成了图像。现在,让我们看看怎么用计算机模拟大自然吧!

小空间中的光追代数学

海什木描述的现象解释了为何我们能看见物体,基于他的观测得出了两点有趣的结论:首先,没有光我们就什么都看不见;其次,没有环境中的物体,我们也看不见光。如果我们在星际间漫游,就会发生这种现象:即使光子仍在周围的空间中存在,如果周围什么都没有,眼前就是一片黑暗。

前向追踪

如果我们要试着在电脑中生成图片时模拟光与物体的交互过程,就必须要注意另一种物理现象。与物体反射的光线总数相比,到达我们眼睛上的光线只是有限的一小部分。举例来说,想象一个一次只发射一个光子的光源,让我们测试下这个光子会经历什么?它会首先沿着直线运动,直到撞击到了物体表面。忽略光子可能被吸收,假定它会被反射到随即方向。如果它集中了我们眼球的表面,我们也就“看见”了反射了光子的那个点。

现在,再看计算机图形学中的情况。首先,我们用一张由像素组成的图片取代了眼睛。那么,被发射的光子就会击中视平面上众多像素中的其中一个,增加这个点处的亮度到大于零的值。这个过程会被重复无数次,直到所有的像素都计算完成,一张电脑生成的图片也就被创造出来了。由于我们是按照“自光源到观察者”的顺序循着光子的轨迹,这个技术故被叫做“前向光线追踪”。

但是,你有没有发现此方法的潜在问题?

它的问题有这些:在我们的例子里,假定了被反射的光子总是会击中眼球表面,但实际上,光线可能被反射到任意可能的方向,每束光线都仅有极小、极小的可能性真正的击中眼睛。或许需要从光源发车数以亿计的光子才能找到那个唯一的击中眼睛的光子。这就是现实:无数的光子以光速沿着各个方向运动。综上所述,对计算机世界而言,在场景中模拟如此多的光子和物体的交互根本就不是可行的解决方案。

所以你会想:“我们真的需要在随即方向上发射光子吗?既然眼睛位置为已知,为什么不只按照眼睛的方位发射光子,再观察它通过了视平面的哪个像素呢?”这显然是优化方法之一,但是我们只能对特定材质使用这个方法。在之后的关于“光和物质相互作用”的课程中我们会知道,对漫反射材质而言定向性并不重要,这是因为击中漫反射表面的光子,可以被反射到在以接触点处的法线为中心的半球内的任意方向。但是,如果表面是镜面,且不具有漫反射特性,光线就只能被反射到精确的方向,也就是对称后的方向(之后我们会学习怎么计算)。对此类表面而言,光子会沿着既定地对称后方向反射,我们不能人为地改变光子的方向,这意味着这个解决方案也并非完美。

即使我们决定只在由漫反射物体组成的场景中使用这个方法,依然面临着一个主要问题。我们能够把光的发射可视化成喷洒光线(或者说小颜料滴)到物体表面般,但如果喷雾不够稠密,一些区域就不会被均匀地照亮。

想象一下,用白色马克笔在黑纸上画出白点来组成 茶壶(其中每个白点代表一个光子)。正如下图所示,只有有限的光子和茶壶发生碰撞,所以遗留下许多未绘制的区域。当白点持续增加,光子的密度也同时增加,直到茶壶已经“几乎”完全被光子发改了,也就更易于识别。

但是发射1000个光子或更多,也不能真正的保证我们的物体表米娜已经被光子完全覆盖。这是此技术的主要短板。也就是说,我们我们不得不让这个程序一直运作,直到喷射到表面的的光子足够多,能够精确的表现这个物体。这意味着我们需要在渲染时一直盯着这个物体,来决定何时停止这个程序。在量产的环境下,这显然是不可能的。另外,可以预见的是,光追中最耗算力的任务就是寻找光线-几何之间的碰撞交互。在光源处新建光子并非难事,但是寻找它们在场景中的变换将非常耗力。

结论:前向光追(或称光源追踪,因为光源发射光线)使得在计算机中模拟光的移动理论上可行。但是,这个方法,恰如前文所述,并非最高效或可行的。在一篇1980年影响深远的、名为“一个改进后的遮蔽显示用光照模型”,特纳·惠特德(最早的计算机图形学研究者之一)写道:“在常见的光线追踪方法中,从光源出发的光线在击中观察者之前都会被追踪轨迹,但由于只有一小部分会集中观察者,就造成了浪费。在阿波罗建议的第二个方法中,光线则会被反方向追踪:从观察者到场景中的物体。”现在我们就看看特纳·惠特德提到的另一种方法。

逆向追踪

我们不再按照从光源到感受体(例如眼睛)的顺序来接受光线,而是逆向的,从感受体到物体。由于这与自然的光线方向恰恰相反,所以宜作“逆向光追”或“眼光光追”(因为就好像从眼中发出了光线一样)。此方法提供了一个针对前向光追弊端的便利解决方案。由于我们的模拟不可能像大自然一样高效且完美,所以必须有所妥协,从眼睛处向场景中追踪光线:当从眼睛出发的线,击中了一个物体的某个点时,再从该点发射一条线(称作“光线”(light ray)或“影线”(shadow ray))计算该点接受了多少从光源而来的光能。有时,这条“光线”会被场景中的其他物体阻塞,这意味着此“光线”的原点将在阴影中,没有接收到任何光线。这也是我们不把这些线叫做“光线”,而是“影线”的原因。在计算机图形学语境中,自眼睛发射至场景中的射线为“始线”、“视线”或“摄像机线”。

在此课程中,我们把从光源出发的光追描述为前向光追,把从摄像机出发的光追称为逆向光追,但是一些作者会用不同的讲法:把从摄像机出发的光追称为前向,这是因为它是计算机图形学中最常见的路径追踪技术。为了避免混淆,你也可以最精确的称呼“光和眼追踪”。这个名词在双向路径追踪的语境中更加常见。

结论

在计算机图形学中,不论是从光源还是眼睛处发射射线,都叫做路径追踪。“光线追踪”有时也可以通用,但路径追踪的概念暗示了计算机沿着光源和摄像机间的路径并生成图片的机制。通过这物理仿真的方式,我们可以简单的模拟出诸如焦散和来自场景中其他平面的光照(间接光照)等的视觉效果。

执行光追算法

经过前文的铺垫,我们终于准备好写我们的第一个光线追踪器啦!你大概已经猜到了光线追踪的算法原理了吧。

首先,要明确的是,自然界的光线传播,是无数的从光源发射出的光线在碰到人眼之前的反弹。而光追则是对自然界的优雅的模仿——除了跟踪光的路径的方向与真实恰恰相反以外,完全是完美的自然模拟器。

光追算法会使用一个像素组成的图片,其上的每个像素都发射一条射入场景中的原初射线,其方向由连接眼睛和该像素中间点的线段方向确认。一旦我们有了原初射线的方向,就可以检查它是否和场景中的任一物体接触。在一些情况下,原初射线不止和单一物体接触,只是就要选择接触点距离人眼最近的物体,然后自接触点向光源发射一条阴影线。如果阴影线直到碰到光源都未曾碰触任何物体,那么物体上的该点就是被照亮的,否则就是在阴影中。

如果我们对每个像素都重复此行为,就获得了一个此三维场景的二维概括。

这是一段描述这一算法的伪代码。

for (int j = 0; j < imageHeight; ++j) { for (int i = 0; i < imageWidth; ++i) { // compute primary ray directionRay primRay; computePrimRay(i, j, &primRay); // shoot prim ray in the scene and search for intersectionPoint pHit; Normal nHit; float minDist = INFINITY; Object object = NULL; for (int k = 0; k < objects.size(); ++k) { if (Intersect(objects[k], primRay, &pHit, &nHit)) { float distance = Distance(eyePosition, pHit); if (distance < minDistance) { object = objects[k]; minDistance = distance; // update min distance } } } if (object != NULL) { // compute illuminationRay shadowRay; shadowRay.direction = lightPosition - pHit; bool isShadow = false; for (int k = 0; k < objects.size(); ++k) { if (Intersect(objects[k], shadowRay)) { isInShadow = true; break; } } } if (!isInShadow) pixels[i][j] = object->color * light.brightness; else pixels[i][j] = 0; }
} 

光追的美妙之处之一就是,它仅需要短短几行代码,任何人都能在200行内写出基础的光线追踪器。而其他的算法,例如扫描线渲染器,则需要更多的精力来介绍。

此技术最早在1969年由亚瑟·阿波罗的“一些机器渲染集合体的技术”论文中被阐释。如果这个算法如此精妙,为什么我们不用它替换其他的渲染算法?最主要的原因是,在当时(某种程度上今天也一样)它的速度太慢了。正如阿波罗在他的论文中写的:

此方法非常花费时间,通常需要几千倍的线框绘制的时间才能得到有用的结果。其中大约一半的时间花费在场景和投影中的点和点之间的通信。

换句话来说,它很慢——或者用吉姆·卡吉雅(计算机图形学史上最具影响力的研究者之一)的话讲“光追并不慢,慢的是电脑。”寻找射线和几何体间的碰撞花费的时间是巨量的。数十年来,光追的主要弱点都是速度。即使和其他的技术相比,例如z缓存算法,光追仍然非常耗时。虽然今天我们有了更快的电脑,旧日需要一个小时才能计算出的图片如今只需几分钟了,但是实时和可交互的光追渲染仍然是炙手可热的话题。

总结来讲,渲染路径可以被看作先后两个过程:首先确定某点是否在某像素可见(可见性部分),然后对该点着色(着色部分)。不幸的是,这两部分都需要昂贵和耗时的射线-几何体碰撞检测。算法的优雅且强大需要我们用渲染时间和精确性来交换,反之亦然。自从阿波罗发表了他的论文,关于加速射线-物体碰撞检测的方法已经有了大量研究。通过结合新的加速方法和新的计算机技术,在渲染软件中使用光追技术正越发简便。

添加反射和折射

光追的另一优点是,通过扩展光的传播的方式,我们可以非常简单的模拟类似反射和折射的效果,它们都是模拟玻璃、镜子材质的便利方法。在1979年的一篇名为“一个改进后的色显示发光模型”论文中,特纳·怀特第一次描述了怎样把阿波罗的光追算法扩展得更先进,包括了反射和折射的计算。

在光学中,反射和折射都是常见的现象,我们之后有一整个课程集中于折射和反射,现在先快速了解下怎么模拟它们。我们以玻璃球来举例,因为其中既有折射也有反射。只要我们知道与玻璃球碰撞的射线的方向,计算结果就更容易了。折射和反射方向取决于碰撞位置的法线方向射线的初始方向(原初射线)两者。计算折射方向还需要明确材质的折射率。虽然我们早前说过:光线的轨迹是直线,但正是因为光线被弯曲了,我们才能看到折射。光子击中的媒介不同,折射率就不同,弯曲的方向就不同。这其中的科学道理,我们之后会更深入的进行讲解。只要我们记住折射和反射这两种效果都取决于法线向量方向和入射光线的方向就可以了。而折射又取决于材质的折射率。

相似的,我们必须也意识到这个事实:如同玻璃球这样的物品中,折射和反射是同时存在的,我们需要对它表面上给定的点做以上两种计算。但是,怎样将这两者合二为一?是不是取反射结果的50%加上折射结果的50%?不幸的是,事实远非那样简单。数值的混合取决于主光线(或观察方向)与物体法线和折射率之间的夹角。 不过幸运的是,有一个公式可以精确计算出每个数值应如何混合。该方程称为菲涅耳方程。为了简洁起见,我们目前仅需要知道它的存在,并且在将来确定混合值时将很有用。

因此,让我们回顾一下。惠特德算法如何工作?首先从眼睛和与场景中的物体最近的相交处(如果有的话)发出主光线。如果射线击中的对象不是漫反射或不透明材质的对象,则必须进行额外的计算工作。为了计算该点(例如玻璃球)上的最终颜色,你需要计算反射色和折射色并将它们混合在一起。请记住,我们分三个步骤进行操作:计算反射颜色,计算折射颜色,然后应用菲涅耳公式。

  • 首先,我们计算反射方向。 为此,我们需要两个值:相交点的法线方向和主光线的方向。 一旦获得反射方向,便会朝该方向发射新射线。回到我们的旧示例,假设反射射线击中了红色球体。使用阿波罗的算法,我们通过向光源发射影线来得出有多少光线到达红色球体上的该点。 这样就获得了一种颜色(如果有阴影即为黑色),然后将其乘以光强度,并返回到玻璃球的表面。
  • 现在我们对折射进行相同的操作。 请注意,由于光线穿过玻璃球,因此称为透射光线(光线已从球体的一侧传播到另一侧)。 为了计算透射方向,我们需要击中点处的法线方向、主光线方向和材料的折射率(在此示例中,玻璃材料的折射率约是1.5)。 计算出新的方向后,折射光沿其路线继续到达玻璃球的另一侧。 在新的位置,由于介质发生了改变(由玻璃变为空气),所以光线再一次被折射。 如图所示,光线进入和离开玻璃物体时方向都会发生变化。每当介质发生变化时——也就是光线从一种离开由进入另一种时、且两种介质折射率不相等时,折射都会发生。你可能已经知道:空气的折射率非常接近1,玻璃的折射率约为1.5。折射具有使光线稍微弯曲的作用。 这个过程就是使物体在被透视时看起来出现偏移的原因。现在想象一下,当折射光线离开玻璃球后击中了绿色的球体。于是我们再次计算绿色球体与折射射线交点处的局部亮度(通过发射影线)。然后将颜色(如果有阴影即为黑色)乘以光强度,并返回到玻璃球的表面。
  • 最后,我们计算菲涅耳方程。需要的参数包括玻璃球的折射率、原射线和击中点的法线之间的夹角。将这二者点乘(我们将在后面解释),菲涅耳方程会返回两者混合后的值。

以下是一些进一步解释其原理的伪代码:

// compute reflection color
color reflectionCol = computeReflectionColor();
// compute refraction color
color refractionCol = computeRefractionColor();
float Kr; // reflection mix value
float Kt; // refraction mix value
fresnel(refractiveIndex, normalHit, primaryRayDirection, &Kr, &Kt);
// mix the two
color glassBallColorAtHit = Kr * reflectionColor + (1-Kr) * refractionColor; 

最后,此算法的美妙之处在于:它是递归的(在某种程度上也是一种诅咒!)。到目前为止我们仅研究了有限的情况:反射射线射向红色的不透明球体,而折射射线射向绿色的不透明且漫反射的球体。但是,我们未来会想象红色和绿色的球体也是玻璃球。为了计算反射和折射光线返回的颜色,我们必须对原始玻璃球使用的红色和绿色球体遵循相同的过程。这是射线跟踪算法的严重缺陷,在某些情况下实际上可能是噩梦。想象一下,我们的相机在一个只有反光面的盒子里。理论上光线会被困在盒中并将无尽地在盒子的墙壁反弹(或直到你停止模拟)。因此,我们必须人为的设置限制,以阻止光线相互影响,从而无限地递归。每当射线被反射或折射时,其深度都会增加。而当射线深度大于最大递归深度时,就直接停止递归过程。

写个基本的光线追踪器

我们收到了很多来自读者的电子邮件询问:“既然如果制作光追很简单,您能不能给我们提供一个真实的例子?” 那并非我计划之中的事(因为我的想法是逐步编写渲染器),但是我们花了几个小时编写了一个简易的光线跟踪器,大约有300行代码。 尽管它的性能不尽如人意,但我们只是想证明:在了解这些技术原理后实施它们并不困难。源代码开放下载,但我们现在和将来都不会花时间解释它。 它写得相当快,因此有相当的改进余地。此版本的光线跟踪器中,我们为了使变得光可见(是个球体),于是将反射绘制在反射球上。 当玻璃球完全透明(白色)时,它们会难以被观察,所以在我们的示例中,我们将它们略微着色(红色)。 在现实世界中,透明玻璃是否看起来明显往往取决于环境(你有没有撞上过玻璃门?)。 请注意生成的图像并不完全准确:透明红色球体下的阴影不应完全不透明。我们将在以后的课程中学习如何轻松纠正这种视觉误差。我们还实现了其他功能,例如假菲涅耳(使用饰面比)和折射。 所有这些事情都将在以后进行研究,因此,如果您目前不清楚它们,请不要担心。 至少,您现在有一个小程序可以玩。

要编译程序,请将源代码下载到硬盘驱动器上。需要c ++编译器(我们在Linux下使用gcc)。 该程序不需要任何特殊的东西即可进行编译。 使用Linux Shell,然后在文件所在的位置键入以下命令:

c++ -O3 -o raytracer raytracer.cpp

要创建图像,请在外环境中键入. /raytracer 来运行该程序。等待几秒钟。 程序返回时,磁盘上应该有一个名为 untitled.ppm 的文件。 您可以使用Photoshop、Preview(在Mac上)或Gimp(在Linux上)打开此文件。 或者检查专门用于读取和显示PPM图像的课程。

以下这段伪代码是实现经典递归光线跟踪算法的可能方式之一:

#define MAX_RAY_DEPTH 3 color Trace(const Ray &ray, int depth)
{ Object *object = NULL; float minDist = INFINITY; Point pHit; Normal nHit; for (int k = 0; k < objects.size(); ++k) { if (Intersect(objects[k], ray, &pHit, &nHit)) { // ray origin = eye position of it's the prim rayfloat distance = Distance(ray.origin, pHit); if (distance < minDistance) { object = objects[i]; minDistance = distance; } } } if (object == NULL) return 0; // if the object material is glass, split the ray into a reflection// and a refraction ray.if (object->isGlass && depth < MAX_RAY_DEPTH) { // compute reflectionRay reflectionRay; reflectionRay = computeReflectionRay(ray.direction, nHit); // recursecolor reflectionColor = Trace(reflectionRay, depth + 1); Ray refractioRay; refractionRay = computeRefractionRay( object->indexOfRefraction, ray.direction, nHit); // recursecolor refractionColor = Trace(refractionRay, depth + 1); float Kr, Kt; fresnel( object->indexOfRefraction, nHit, ray.direction, &Kr, &Kt); return reflectionColor * Kr + refractionColor * (1-Kr); } // object is a diffuse opaque object        // compute illuminationRay shadowRay; shadowRay.direction = lightPosition - pHit; bool isShadow = false; for (int k = 0; k < objects.size(); ++k) { if (Intersect(objects[k], shadowRay)) { // hit point is in shadow so just returnreturn 0; } } // point is illuminatedreturn object->color * light.brightness;
} // for each pixel of the image
for (int j = 0; j < imageHeight; ++j) { for (int i = 0; i < imageWidth; ++i) { // compute primary ray directionRay primRay; computePrimRay(i, j, &primRay); pixels[i][j] = Trace(primRay, 0); }
} 

最短的光线追踪器

许多年前,研究员保罗·赫克伯特(Paul Heckbert)编写了一种射线追踪器,可以“适用于商务卡上”。 这个想法是:用C / C ++编写一个最小的光线跟踪器,它很短到可以在名片的背面打印出来(有关此想法的更多信息,请参见他在Graphics Gems IV中写的一篇文章)。 从那时起,许多程序员都尝试了这种编码练习。 在下方,您可以找到由安德鲁·肯斯勒(Andrew Kensler)编写的版本。 左图是他的程序的结果。 注意景深效果(物体在距离上变得模糊)。 能够用很少的几行代码创建一个相当复杂的图像,这真是太神奇了。

#include <stdlib.h>   // card > aek.ppm
#include <stdio.h>
#include <math.h>
typedef int i;typedef float f;struct v{f x,y,z;v operator+(v r){return v(x+r.x,y+r.y,z+r.z);}v operator*(f r){return v(x*r,y*r,z*r);}f operator%(v r){return x*r.x+y*r.y+z*r.z;}v(){}v operator^(v r){return v(y*r.z-z*r.y,z*r.x-x*r.z,x*r.y-y*r.x);}v(f a,f b,f c){x=a;y=b;z=c;}v operator!(){return*this*(1/sqrt(*this%*this));}};i G[]={247570,280596,280600,249748,18578,18577,231184,16,16};f R(){return(f)rand()/RAND_MAX;}i T(v o,v d,f&t,v&n){t=1e9;i m=0;f p=-o.z/d.z;if(.01<p)t=p,n=v(0,0,1),m=1;for(i k=19;k--;)for(i j=9;j--;)if(G[j]&1<<k){v p=o+v(-k,0,-j-4);f b=p%d,c=p%p-1,q=b*b-c;if(q>0){f s=-b-sqrt(q);if(s<t&&s>.01)t=s,n=!(p+d*t),m=2;}}return m;}v S(v o,v d){f t;v n;i m=T(o,d,t,n);if(!m)return v(.7,.6,1)*pow(1-d.z,4);v h=o+d*t,l=!(v(9+R(),9+R(),16)+h*-1),r=d+n*(n%d*-2);f b=l%n;if(b<0||T(h,l,t,n))b=0;f p=pow(l%r*(b>0),99);if(m&1){h=h*.2;return((i)(ceil(h.x)+ceil(h.y))&1?v(3,1,1):v(3,3,3))*(b*.2+.1);}return v(p,p,p)+S(h,r)*.5;}i main(){printf("P6 512 512 255 ");v g=!v(-6,-16,0),a=!(v(0,0,1)^g)*.002,b=!(g^a)*.002,c=(a+b)*-256+g;for(i y=512;y--;)for(i x=512;x--;){v p(13,13,13);for(i r=64;r--;){v t=a*(R()-.5)*99+b*(R()-.5)*99;p=S(v(17,16,8)+t,!(t*-1+(a*(R()+x)+b*(y+R())+c)*16))*3.5+p;}printf("%c%c%c",(i)p.x,(i)p.y,(i)p.z);}}

要运行该程序,请将代码复制/粘贴到文本文件中(将文件重命名为minray.cpp或您喜欢的任何名称),然后编译代码(c++ -O3 -o min ray minray.cpp or clang++ -O3 -o min ray minray.cpp,如果您希望使用clang编译器),并使用命令行运行它:min ray> minray.ppm。 与其将最终的图像数据写到磁盘上(这会使代码更长),不如将数据写到标准输出(您正在从中运行程序的外壳)中,我们可以将其重定向(使用符号>)到文件中。可以使用Photoshop读取PPM文件。

在此处提供该程序仅是为了表明可以用非常少的代码行实现光线跟踪算法。本节的下一课将解释代码中使用的许多技术。

源代码

点我下载

A very basic raytracer example.

Instructions to compile this program:
c++ -o raytracer -O3 -Wall raytracer.cpp

#include <cstdlib>
#include <cstdio>
#include <cmath>
#include <fstream>
#include <vector>
#include <iostream>
#include <cassert> #if defined __linux__ || defined __APPLE__
// "Compiled for Linux
#else
// Windows doesn't define these values by default, Linux does
#define M_PI 3.141592653589793
#define INFINITY 1e8
#endif template<typename T>
class Vec3
{
public: T x, y, z; Vec3() : x(T(0)), y(T(0)), z(T(0)) {} Vec3(T xx) : x(xx), y(xx), z(xx) {} Vec3(T xx, T yy, T zz) : x(xx), y(yy), z(zz) {} Vec3& normalize() { T nor2 = length2(); if (nor2 > 0) { T invNor = 1 / sqrt(nor2); x *= invNor, y *= invNor, z *= invNor; } return *this; } Vec3<T> operator * (const T &f) const { return Vec3<T>(x * f, y * f, z * f); } Vec3<T> operator * (const Vec3<T> &v) const { return Vec3<T>(x * v.x, y * v.y, z * v.z); } T dot(const Vec3<T> &v) const { return x * v.x + y * v.y + z * v.z; } Vec3<T> operator - (const Vec3<T> &v) const { return Vec3<T>(x - v.x, y - v.y, z - v.z); } Vec3<T> operator + (const Vec3<T> &v) const { return Vec3<T>(x + v.x, y + v.y, z + v.z); } Vec3<T>& operator += (const Vec3<T> &v) { x += v.x, y += v.y, z += v.z; return *this; } Vec3<T>& operator *= (const Vec3<T> &v) { x *= v.x, y *= v.y, z *= v.z; return *this; } Vec3<T> operator - () const { return Vec3<T>(-x, -y, -z); } T length2() const { return x * x + y * y + z * z; } T length() const { return sqrt(length2()); } friend std::ostream & operator << (std::ostream &os, const Vec3<T> &v) { os << "[" << v.x << " " << v.y << " " << v.z << "]"; return os; }
}; typedef Vec3<float> Vec3f; class Sphere
{
public: Vec3f center;                           /// position of the sphere float radius, radius2;                  /// sphere radius and radius^2 Vec3f surfaceColor, emissionColor;      /// surface color and emission (light) float transparency, reflection;         /// surface transparency and reflectivity Sphere( const Vec3f &c, const float &r, const Vec3f &sc, const float &refl = 0, const float &transp = 0, const Vec3f &ec = 0) : center(c), radius(r), radius2(r * r), surfaceColor(sc), emissionColor(ec), transparency(transp), reflection(refl) { /* empty */ } 

Compute a ray-sphere intersection using the geometric solution

 bool intersect(const Vec3f &rayorig, const Vec3f &raydir, float &t0, float &t1) const { Vec3f l = center - rayorig; float tca = l.dot(raydir); if (tca < 0) return false; float d2 = l.dot(l) - tca * tca; if (d2 > radius2) return false; float thc = sqrt(radius2 - d2); t0 = tca - thc; t1 = tca + thc; return true; }
}; 

This variable controls the maximum recursion depth

#define MAX_RAY_DEPTH 5 float mix(const float &a, const float &b, const float &mix)
{ return b * mix + a * (1 - mix);
} 

This is the main trace function. It takes a ray as argument (defined by its origin and direction). We test if this ray intersects any of the geometry in the scene. If the ray intersects an object, we compute the intersection point, the normal at the intersection point, and shade this point using this information. Shading depends on the surface property (is it transparent, reflective, diffuse). The function returns a color for the ray. If the ray intersects an object that is the color of the object at the intersection point, otherwise it returns the background color.

Vec3f trace( const Vec3f &rayorig, const Vec3f &raydir, const std::vector<Sphere> &spheres, const int &depth)
{ //if (raydir.length() != 1) std::cerr << "Error " << raydir << std::endl;float tnear = INFINITY; const Sphere* sphere = NULL; // find intersection of this ray with the sphere in the scenefor (unsigned i = 0; i < spheres.size(); ++i) { float t0 = INFINITY, t1 = INFINITY; if (spheres[i].intersect(rayorig, raydir, t0, t1)) { if (t0 < 0) t0 = t1; if (t0 < tnear) { tnear = t0; sphere = &spheres[i]; } } } // if there's no intersection return black or background colorif (!sphere) return Vec3f(2); Vec3f surfaceColor = 0; // color of the ray/surfaceof the object intersected by the ray Vec3f phit = rayorig + raydir * tnear; // point of intersection Vec3f nhit = phit - sphere->center; // normal at the intersection point nhit.normalize(); // normalize normal direction // If the normal and the view direction are not opposite to each other// reverse the normal direction. That also means we are inside the sphere so set// the inside bool to true. Finally reverse the sign of IdotN which we want// positive.float bias = 1e-4; // add some bias to the point from which we will be tracing bool inside = false; if (raydir.dot(nhit) > 0) nhit = -nhit, inside = true; if ((sphere->transparency > 0 || sphere->reflection > 0) && depth < MAX_RAY_DEPTH) { float facingratio = -raydir.dot(nhit); // change the mix value to tweak the effectfloat fresneleffect = mix(pow(1 - facingratio, 3), 1, 0.1); // compute reflection direction (not need to normalize because all vectors// are already normalized)Vec3f refldir = raydir - nhit * 2 * raydir.dot(nhit); refldir.normalize(); Vec3f reflection = trace(phit + nhit * bias, refldir, spheres, depth + 1); Vec3f refraction = 0; // if the sphere is also transparent compute refraction ray (transmission)if (sphere->transparency) { float ior = 1.1, eta = (inside) ? ior : 1 / ior; // are we inside or outside the surface? float cosi = -nhit.dot(raydir); float k = 1 - eta * eta * (1 - cosi * cosi); Vec3f refrdir = raydir * eta + nhit * (eta *  cosi - sqrt(k)); refrdir.normalize(); refraction = trace(phit - nhit * bias, refrdir, spheres, depth + 1); } // the result is a mix of reflection and refraction (if the sphere is transparent)surfaceColor = ( reflection * fresneleffect + refraction * (1 - fresneleffect) * sphere->transparency) * sphere->surfaceColor; } else { // it's a diffuse object, no need to raytrace any furtherfor (unsigned i = 0; i < spheres.size(); ++i) { if (spheres[i].emissionColor.x > 0) { // this is a lightVec3f transmission = 1; Vec3f lightDirection = spheres[i].center - phit; lightDirection.normalize(); for (unsigned j = 0; j < spheres.size(); ++j) { if (i != j) { float t0, t1; if (spheres[j].intersect(phit + nhit * bias, lightDirection, t0, t1)) { transmission = 0; break; } } } surfaceColor += sphere->surfaceColor * transmission * std::max(float(0), nhit.dot(lightDirection)) * spheres[i].emissionColor; } } } return surfaceColor + sphere->emissionColor;
} 

Main rendering function. We compute a camera ray for each pixel of the image trace it and return a color. If the ray hits a sphere, we return the color of the sphere at the intersection point, else we return the background color.

void render(const std::vector<Sphere> &spheres)
{ unsigned width = 640, height = 480; Vec3f *image = new Vec3f[width * height], *pixel = image; float invWidth = 1 / float(width), invHeight = 1 / float(height); float fov = 30, aspectratio = width / float(height); float angle = tan(M_PI * 0.5 * fov / 180.); // Trace raysfor (unsigned y = 0; y < height; ++y) { for (unsigned x = 0; x < width; ++x, ++pixel) { float xx = (2 * ((x + 0.5) * invWidth) - 1) * angle * aspectratio; float yy = (1 - 2 * ((y + 0.5) * invHeight)) * angle; Vec3f raydir(xx, yy, -1); raydir.normalize(); *pixel = trace(Vec3f(0), raydir, spheres, 0); } } // Save result to a PPM image (keep these flags if you compile under Windows)std::ofstream ofs("./untitled.ppm", std::ios::out | std::ios::binary); ofs << "P6\n" << width << " " << height << "\n255\n"; for (unsigned i = 0; i < width * height; ++i) { ofs << (unsigned char)(std::min(float(1), image[i].x) * 255) << (unsigned char)(std::min(float(1), image[i].y) * 255) << (unsigned char)(std::min(float(1), image[i].z) * 255); } ofs.close(); delete [] image;
} 

In the main function, we will create the scene which is composed of 5 spheres and 1 light (which is also a sphere). Then, once the scene description is complete we render that scene, by calling the render() function.

int main(int argc, char **argv)
{ srand48(13); std::vector<Sphere> spheres; // position, radius, surface color, reflectivity, transparency, emission colorspheres.push_back(Sphere(Vec3f( 0.0, -10004, -20), 10000, Vec3f(0.20, 0.20, 0.20), 0, 0.0)); spheres.push_back(Sphere(Vec3f( 0.0,      0, -20),     4, Vec3f(1.00, 0.32, 0.36), 1, 0.5)); spheres.push_back(Sphere(Vec3f( 5.0,     -1, -15),     2, Vec3f(0.90, 0.76, 0.46), 1, 0.0)); spheres.push_back(Sphere(Vec3f( 5.0,      0, -25),     3, Vec3f(0.65, 0.77, 0.97), 1, 0.0)); spheres.push_back(Sphere(Vec3f(-5.5,      0, -15),     3, Vec3f(0.90, 0.90, 0.90), 1, 0.0)); // lightspheres.push_back(Sphere(Vec3f( 0.0,     20, -30),     3, Vec3f(0.00, 0.00, 0.00), 0, 0.0, Vec3f(3))); render(spheres); return 0;
} 

【自翻】光线追踪的简介:创作3D图片的简单方法相关推荐

  1. C#生成条形码图片的简单方法

    这篇文章主要介绍了C#生成条形码图片的简单方法,实例分析了了条形码图片的生成原理与实现方法,具有一定参考借鉴价值,需要的朋友可以参考下 本文实例讲述了C#生成条形码图片的简单方法.分享给大家供大家参考 ...

  2. 把视频解码为本地图片的简单方法

    在网上查了一下把视频解码为本地图片的方法,一般都是要下载安装专业的视频处理软件来做,而且专业的视频处理软件并不是免费的,下载和安装非常费时,占用的内存空间也比较大.还有用opencv库编写代码,也可以 ...

  3. 易语言 base64转图片的简单方法

    介绍 验证码图片地址: data:image/png;base64.... 目的 将地址转换成图片 方法 需要使用精易模块 也可以使用精易模块的 编码_Base64转图片 命令 编码_Base64转图 ...

  4. 横向排列图片的简单方法

    描述:图片横向排列,出现横向滚动条.最外层div根据图片的宽度 设定固定长度 ,包含了图片的底下的说明描述 css样式: /* 包裹图片div的盒子 */ .ul-box{/*white-space: ...

  5. PS图片操作简单方法:

    PS如何给图片添加文字:https://jingyan.baidu.com/article/4b07be3c2e40e508b280f317.html 如何使用PS在模糊的图片上输入清晰的文字:htt ...

  6. Javascript实现鼠标替换图片的简单方法

    <!DOCTYPE html> <html> <head><title>A Simple Rollover</title><link ...

  7. html图片 3d切换特效,一款基于css3的3D图片翻页切换特效

    今天给大家分享一款基于css3的3D图片翻页切换特效.单击图片下方的滑块会切换上方的图片.动起你的鼠标试试吧,效果图如下: 实现的代码. html代码: Bedouin Blue-green Dram ...

  8. ❤表白❤相册——动态3D图片墙

    ❤表白❤相册--动态3D图片墙 一.前言 1.相册的意义什么 答:相册在于记忆.回忆.生活.旅游等的记录,在于可以保留生命中的精彩一瞬间,感动一刹那,让时间永久定格在那一时刻.时间一去不复返,珍藏的记 ...

  9. 实现3d图片移动_「3D建模」什么是动画和角色设计的3D索具?

    3D建模设计软件 maya.zbrush.3Dmax 索具如何工作? 索具是较大动画过程的一部分. 创建3D模型后,将构建代表骨骼结构的一系列骨骼.例如,在一个角色中,可能有一组背部骨骼,脊柱和头部骨 ...

最新文章

  1. Docker基本使用命令
  2. beatsx三闪红灯是什么意思_周迅感情亮红灯?真离了!?亮红灯英文是red light ?red 对了,但不用 light!...
  3. easyui datagrid 返回数据正确 fit='true' 时不显示内容
  4. python视图函数是什么意思_Flask初学者:视图函数
  5. 南风表情包小程序完整版源码 后台API+前端
  6. webpack的多文件打包问题
  7. 面向对象开发方法概述
  8. 孙宇晨终于和巴菲特共进晚餐 还赠送数字币作为见面礼
  9. centos 6.4 更新源地址
  10. 尚硅谷JVM笔记(宋红康主讲)
  11. 解放生产力「GitHub 热点速览 v.21.51」
  12. 最新版PandoraBox潘多拉安装adbyby去广告插件图文详细教程!!
  13. No virtual method setOutputFile Ljava/io/File V in class Landroid/media/MediaRecorder
  14. 金代文化是中华民族文化的重要组成部分
  15. 张正友畸变矫正C++代码
  16. Flutter中的多选按钮组件Checkbox
  17. 栈——栈的基本概念和基本操作
  18. Android Studio GIT 仓库地址 变更 方法
  19. P1873 砍树(二分答案)
  20. 怪物猎人X护石系统详解 护石任务数据向分析

热门文章

  1. php注册登录描述,基于PHP实现用户登录注册功能的详细教程
  2. 以核心素养为导向的计算机教学方式,《核心素养导向的课堂教学》导读
  3. matlab频率域滤波器,频率域滤波的MATLAB设计与实现_课程设计
  4. 零散专题32 生成PDF
  5. 抖音变现模式?80%的人都不知道的秘密,三类更适合玩私域的产品
  6. 代码主题darcula_仿IntelliJ Darcula的Swing主题FlatLaf使用方法
  7. 电脑同时连接有线和无线网络怎么设置有线网络优先
  8. 牛客网题源(JavaScript)
  9. php silk v3 decoder,小程序API录音后Silk格式转码MP3
  10. 从0开始制作H5直播源码的教程