体渲染

前言:本课程的结构和方法

体积渲染(技术上,使用术语"参与介质"而不是"体积"可能更准确)是一个几乎和硬表面渲染一样庞大和复杂的主题。它有自己一套方程,实际上几乎可以看作是用来描述光如何与坚硬物质相互作用的方程的一种概括。对于那些对这些复杂数学公式不够熟悉的读者来说,可能会感到压力很大。

根据Scratchapixel上教授事物的方式,我们选择了一种"自下而上"的方式来教授体积渲染的挑战,也就是一种实践性的方法。与其从方程入手深入研究,我们将编写代码来渲染一个简单的体积球,并希望以一种直观的方式沿途解释事物。然后,我们将总结并在课程的最后将一切都形式化。

接下来的几节课将专门讨论体积渲染(这是一个广阔的主题)。在这个介绍性的课程中,我们将研究体积渲染和射线行进的基础知识。接下来的课程将专门探讨渲染体积的其他可能方法,全局照明应用于参与介质,多次散射,存储体积数据的格式,如OpenVDB等等。

如果您对体积渲染感兴趣,您可能还想查看有关模拟天空颜色的课程。天空可以看作一种体积。

介绍

在本课程的前两章中,我们的目标是学习如何渲染一个形状类似球体的体积,该球体受到单一光源的照明,背景色均匀。这将帮助我们初步直观地了解什么是体积,并介绍射线行进算法,用于渲染它们。

在这一特定章节中,我们将只渲染具有均匀密度的普通体积。我们将忽略由外部或体积内部的物体投射的阴影,以及如何渲染具有不同密度的体积。这些内容将在接下来的章节中进行研究。

与提供关于体积是什么以及用于渲染它们的方程的详细背景知识不同,让我们直接进行实现,然后从中推导出对体积渲染的更正式理解。

内部透射率、吸收、粒子密度和比尔定律

当光抵达我们的眼睛,是由物体反射的光或光源发出的光经过充满一些粒子的空间体积时,很可能会被吸收。体积中的粒子越多,体积就越不透明。基于这个简单的观察,我们可以建立与体积渲染相关的一些基本概念:吸收、传输以及体积的不透明度与其中包含的粒子密度之间的关系。暂时我们将考虑体积中的粒子密度是均匀的。

当光线朝着我们的眼睛穿过体积(这是形成我们眼中看到的物体图像的方式),其中的一些光将在穿过体积时被吸收。这种现象被称为吸收。我们感兴趣的是(目前),从背景透过体积传递的光的数量。我们称之为内部透射率(光线穿过体积时被吸收的量)。内部透射率可以被看作是一个值,从0(体积挡住了所有光线)到1(嗯,这是真空,所以所有光线都通过了)。

通过该体积传输的光量受到比尔-朗伯定律(或简称比尔定律)的控制。在比尔-朗伯定律中,密度的概念以吸收系数(以及散射系数,但我们将在本章后面介绍散射系数)来表达。您可以将其理解为"体积越密,吸收系数越高";正如您可以直观猜测的那样,吸收系数增加,体积变得更不透明。比尔-朗伯定律的方程如下:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> T = e x p ( − d i s t a n c e ∗ σ a ) = e ( − d i s t a n c e ∗ σ a ) T = exp(-distance * \sigma_a)=e^{(-distance * \sigma_a)} </math>T=exp(−distance∗σa)=e(−distance∗σa)

该定律规定了光线通过体积的内部透射率 <math xmlns="http://www.w3.org/1998/Math/MathML"> T T </math>T 与体积的吸收系数 <math xmlns="http://www.w3.org/1998/Math/MathML"> σ a \sigma_a </math>σa (希腊字母 sigma) 与光线穿过材料的距离(即光程)的乘积之间存在指数依赖关系。

这些系数的单位是倒数长度,即 <math xmlns="http://www.w3.org/1998/Math/MathML"> c m − 1 cm^{-1} </math>cm−1 或 <math xmlns="http://www.w3.org/1998/Math/MathML"> m − 1 m^{-1} </math>m−1 (这并不重要,这只是一个尺度问题)。这一点很重要,因为它有助于形成对这些系数所包含信息的直观认识。你可以将吸收系数(以及稍后我们将介绍的散射系数)看作是一个随机事件发生的概率或可能性,也就是说,在任何给定点/距离发生光子被吸收或散射的概率。

吸收系数和散射系数被认为表示概率密度(以防你想进一步研究这个主题)。由于它是概率,它不应超过1,但这取决于其所测量的单位。例如,如果你使用毫米,对于某种介质,你可能会得到0.2。但以厘米和米为单位表达,这将分别变为2和20。因此,实际上,你可以使用大于1的值。

系数与平均自由程之间的关系

吸收系数和散射系数的单位是长度的倒数非常重要,因为如果你取它们的倒数(吸收系数或散射系数的倒数),你会得到一个距离。这个距离被称为平均自由程,代表了随机事件发生的平均距离:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> mean free path = 1 σ s \text{mean free path} = { {1}\over{\sigma_s}} </math>mean free path=σs1

这个数值在模拟参与媒体中的多次散射中起着重要作用。查看有关次表面散射和高级体积渲染的教程,以了解更多关于这些非常酷的主题的信息。

图1:距离越大或密度越大,内部透射值就越低。

吸收系数或距离越大,T值越小。Beer-Lambert定律方程的结果在0-1的范围内。如果距离或吸收系数为0,方程结果为1。对于非常大的距离或密度,T接近0。对于固定距离,T随着吸收系数的增加而减小。对于固定吸收系数,T随着距离的增加而减小。光在介质中传播的距离越远,吸收就越多。介质中的粒子越多,吸收的光就越多。简单明了。您可以在图1中看到这种效应。

吸收介质和宝石。

吸收性介质是透明的(不是半透明的),但会使通过它看到的图像变暗(例如:啤酒、葡萄酒、宝石、有色玻璃)。

在均匀背景上渲染体积。

从这里开始很容易。假设我们有一块已知厚度和密度的体积块。假设厚度为10,密度为0.1。那么如果背景颜色(例如墙壁反射的光)是(xr,xg,xb),那么我们透过体积看到多少背景颜色是:

c++ 复制代码
vec3 background_color {xr, xg, xb};
float sigma_a = 0.1; // absorption coefficient
float distance = 10;
float T = exp(-distance * sigma_a);
vec3 background_color_through_volume = T * background_color;

散射

请注意,到目前为止,我们假设我们的体积是黑色的。换句话说,我们只是在我们的板块上加深了背景颜色。但是,体积不一定是黑色的。与实体物体一样,体积也会反射(或更准确地说是散射)光线。这就是为什么在晴天看云层时,你几乎可以看到云层的形状,就好像它是一个实体物体一样。体积还可以发光(比如蜡烛火焰),尽管在本章中我们将忽略发射。所以,让我们假设我们的体积板块有一定的颜色,比如(yr,yg,yb)。我们将忽略这种颜色的来源,稍后在本章中我们将解释它。目前,我们可以说我们的体积由于照亮它的光线(与实体物体类似,尽管不完全相同,但让我们用"反射"这个概念来理解)而具有一定的颜色。然后我们的代码变成了:

c++ 复制代码
vec3 background_color {xr, xg, xb}; 
float sigma_a= 0.1; 
float distance = 10; 
float T = exp(-distance * sigma_a); 
vec3 volume_color {yr, yg, yb}; 
vec3 background_color_through_volume = T * background_color + (1 - T) * volume_color; 

可以将其看作在Photoshop中使用 alpha 混合合并(A+B)图像的过程,例如将图像 B 添加到 A 上,其中 A 是背景图像(我们的蓝色墙壁),而 B 是具有透明通道的红色圆盘的图像。组合这两个图像的公式将如下所示:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> C = ( 1 − B . t r a n s p a r e n c y ) ∗ A + B . t r a n s p a r e n c y ∗ B C = (1 - B.transparency) * A + B.transparency * B </math>C=(1−B.transparency)∗A+B.transparency∗B

这里的透明度是 1 - 传输(也称为不透明度),B 是我们体积对象的颜色(被体积"反射"并朝向我们的眼睛/摄像机传播的光)。当我们讨论射线行进算法时,我们将再次涉及到这一点;目前,请记住这一点。

渲染第一个体积球

我们已经掌握了渲染第一张3D图像所需的一切。我们将使用到目前为止学到的知识来渲染一个球体,假设这个球体充满了一些粒子。我们将假设我们在一些背景上渲染我们的球体。这个原理非常简单。我们首先检查相机射线和球体之间是否有相交。如果没有相交,那么我们只需返回背景颜色。如果有相交,然后我们计算射线进入和离开球体表面的点。从那里,我们可以计算射线穿过球体的距离,并应用 Beer 定律来计算有多少光线透过了球体。我们目前假设由球体"反射"(散射)的光是均匀的。我们将在后面讨论光照。

图2:相机射线穿过体积对象。

图3:我们使用相机射线与体积对象的交点来计算相机射线沿着体积对象的不透明度。

技术上,我们不需要计算射线进入和离开球体的点来获取这两点之间的距离。我们只需要从tmax中减去tmin(射线沿着相机射线与球体相交的射线参数距离)。在下面的示例中,我们计算它们是为了强调我们关心的是这两点之间的距离。

c++ 复制代码
class Sphere : public Object 
{ 
public: 
    bool intersect(vec3, vec3, float, float) const { /* compute ray-sphere intersection */ } 
    float sigma_a{ 0.1 }; 
    vec3 scatter{ 0.8, 0.1, 0.5 }; 
    vec3 center{ 0, 0, -4 }; 
    float radius{ 1 }; 
}; 
 
void traceScene(vec3 ray_origin, vec3d ray_direction, const Sphere *sphere) 
{ 
    float t0, t1; 
    vec3 background_color { 0.572, 0.772, 0.921 }; 
    if (sphere->intersect(rayOrigin, rayDirection, t0, t1)) { 
        vec3 p1 = ray_origin + ray_direction * t0; 
        vec3 p2 = ray_origin + ray_direction * t1; 
        float distance = (p2 - p1).length();  // though you could simply do t1 - t0 
        float tranmission = exp(-distance * sphere->sigma_a); 
        return background_color * transmission + sphere->scatter * (1 - transmission); 
    } 
    else 
       return background_color; 
} 
 
void renderImage() 
{ 
    Sphere *sphere = new Sphere; 
    for (each row in the image) 
        for (each column in the image) 
            vec3 ray_dir = computeRay(col, row); 
            pixel_color = traceScene(ray_orig, ray_dir, sphere); 
            image_buffer[...] = pixel_color;  // store pixel color in image buffer 
 
    saveImage(image_buffer); 
    ... 
} 

毫不奇怪,随着密度增加,透射率接近于0,这意味着体积球的颜色占主导地位,而不是背景颜色。

你可以在上面的图像中看到,体积朝着球体的中心变得更加不透明(光线穿过球体的距离最大的地方)。你还可以看到随着密度的增加(随着σa的增加),整个球体变得更不透明。干得好!你刚刚渲染了你的第一个体积球,离成为一个体积渲染专家已经过了一半的路程。

让我们添加光照,内部散射

到目前为止,我们有了一个漂亮的体积球的图像,但光照呢?如果我们把光照射到一个体积对象上,我们可以看到那些直接暴露在光线下的体积部分比那些在阴影中的部分更亮。体积也会被光线照亮。我们如何处理这个问题? 原理非常简单。让我们想象一下,来自光源的光在穿过体积时的行程。随着光线穿过体积的行程,由于吸收,它的强度会减弱。毫不奇怪,光线在穿过体积后剩余多少能量是由 Beer 定律规定的。换句话说,如果我们知道光线穿过体积的距离,那么在该距离上的光线强度为:

c++ 复制代码
float light_intensity = 10; // just a number, it could be anything 
float T = exp(-distance_travelled_by_light * volume->absorption_coefficient); 
light_intensity_attenuation = T * ligth_intensity;

首先,光能量根据 Beer 定律随着穿过体积的距离而减少,这是相当合乎逻辑的。但还会发生其他事情:来自光源的光,如果最初没有朝向眼睛传播,也可以由于散射效应而被重定向到眼睛(至少部分如我们将看到的)。我们在这种情况下谈论的是散射效应。在散射中,光通过体积传播,由于散射事件而被重定向到眼睛。这个效应在图4中有所说明。散射事件是光子与构成介质/体积的粒子/原子之间的相互作用的结果。原子并没有吸收或反射光子(尽管这也可能发生),而是将光子"吐出",以与其入射方向不同的方向。我们将在接下来的章节中更多地了解这种现象。

图4:我们通过体积对象看到的光既来自背景物体(这里是蓝色)也来自光源。尽管光束在被光源发射时没有传向眼睛,但由于体积对象的内部散射效应,光在通过体积对象时会有一部分光被重定向到眼睛。

如果您查看图4,可以注意到到达眼睛的光(沿着图中以蓝色绘制的特定眼睛/摄像机射线)是来自背景(蓝色背景)的光和由于内部散射而向眼睛散射的光(黄色射线)的组合。

因此,我们如何考虑光源的贡献呢?我们需要"测量"沿着摄像机射线散射到眼睛的光的效应,这是内部散射的效应。问题在于,我们需要考虑沿着与球体相交的摄像机射线的整个部分的这种效应(图5)。我们需要在范围t0-t1上"积分"沿着摄像机射线散射到的光。

Figure 5: 我们需要沿着穿过体积物体的光线段集成由于内部散射而被重定向到眼睛的光。

要解决这个问题,我们将把穿过体积的相机射线部分划分为一定数量的段(如果你愿意,可以称之为样本),并计算到达每个这些段(样本)中心的光量,使用以下过程(请参见图6以获取概念的可视表示):

  • 然后,我们从该样本点发射一条射线(我们称其为X),以计算从样本点到球体边界的距离(我们称此点为P)。请注意,X始终在球体内部(我们的体积)而P始终是球体表面上的一个点。

  • 然后应用Beer定律来知道光能量在从P(这光线进入球体的地方)到X(这光线在其中向观察者散射的视线上的地方)的传输过程中衰减了多少。

图6:按照视线进行规律性的步进以估计积分,使用黎曼和。

图7:我们可以使用黎曼和来估计沿着摄像机的线散射的光量。思路是将曲线下的面积分成许多小矩形的和。每个矩形的高度由Li(x)给出,宽度dx由用户定义。

为了理解我们在解决的问题类型,我们需要看一下图6和图7。图6显示了沿着摄像机射线到来的入射光,正如您在图的底部部分所看到的,这是一个连续函数。让我们称这个函数为Li(x),其中x是在范围t0-t1内的摄像机射线上的任何点。我们需要计算的是曲线下的"面积"。在数学中,这是一个积分,我们可以写成:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> ∫ x = t 0 t 1 L i ( x ) d x \int_{x=t_0}^{t_1} Li(x) dx </math>∫x=t0t1Li(x)dx

正如我们刚才所说,积分的结果(一个数)被定义为曲线下(函数Li(x))的(净签名)面积,如图6所示。在我们的情况下,问题在于我们无法使用解析解来计算这个面积。但我们可以使用一个技巧来将这个面积近似为我们知道面积的简单形状------矩形(如图7所示)。我们以我们知道宽度(dx)和最终矩形的面积可以通过Li(x)乘以dx(x在区间中间)来计算,然后通过总结所有矩形的面积,我们得到了曲线下面积的近似值。Et voila!这种技术被称为黎曼和(近似一个我们不知道面积的形状的面积的想法可以追溯到古希腊)。

相关推荐
王哈哈^_^1 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie1 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic2 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿2 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具3 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
qq_390161773 小时前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test4 小时前
js下载excel示例demo
前端·javascript·excel
Yaml44 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事4 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶4 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json