体渲染简明教程【NeRF基础】

体渲染(Volume Rendering)是一个几乎与硬表面渲染一样大和一样复杂的主题。 它有自己的一组方程,实际上几乎是用于描述光如何与硬物质相互作用的方程的概括。 对于那些不一定熟悉如此复杂的数学公式的读者来说,它们可能会让人不知所措。

在这里插入图片描述

推荐:用 NSDT设计器 快速搭建可编程3D场景。

正如我们在 Scratchapixel 上教学的方式一样,我们选择了“自下而上”的方法来应对教学体积渲染的挑战。 或者换句话说,这是一种实用的方法。 我们不会从方程开始深入研究它们,而是编写代码来渲染一个简单的体积球体,并以一种希望直观的方式解释整个过程。 然后我们将在课程结束时总结并形式化到目前为止所学到的所有内容。

几节课程将专门讨论体积渲染(这是一个很大的主题)。 在本入门课程中,我们将学习体积渲染和光线行进的基础知识。 接下来的课程将致力于其他可能的渲染体积的方法、应用于参与介质的全局照明、多重散射、用于存储体积数据的格式(例如 OpenVDB 等)。

1、体渲染简介

本课程前两章的目标是学习如何在均匀颜色的背景上渲染由单个光源照明的球体形状的体积。 这将帮助我们首先直观地了解体积是什么,并介绍我们将用于渲染它们的光线行进算法。

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

我们不再提供大量关于什么是体积以及用于渲染它们的方程的详细背景知识,而是直接深入实现并从中获得对体积渲染的更正式的理解。
在这里插入图片描述

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

由于光被物体反射或由光源发射而到达我们眼睛的光在穿过充满一些粒子的空间体积时很可能被吸收。 体积中的粒子越多,体积就越不透明。 从这个简单的观察中,我们可以得出一些与体积渲染相关的基本概念:吸收、透射以及体积的不透明程度与其所含粒子的密度之间的关系。 现在,我们将认为体积中包含的粒子的密度是均匀的。
在这里插入图片描述

当光沿着我们眼睛的方向穿过体积时(这就是我们所看到的物体的图像在我们的眼睛中形成的方式),其中一些光在穿过体积时会被体积吸收。 这种现象称为吸收。 我们(目前)感兴趣的是从背景穿过体积传输的光量。 我们谈论内部透射率(当光穿过体积时被体积吸收的光量)。 内部透射率可以看作是从 0(体积阻挡所有光)到 1(好吧,它是真空,所以所有光都透射)的值。

通过该体积传输的光量受比尔-朗伯定律(或简称比尔定律)管辖。 在比尔-朗伯定律中,密度的概念用吸收系数(和散射系数,但我们将在本章稍后介绍散射系数)来表达。 可以理解为“体积越密,吸收系数越高”; 正如您可以直观地猜测的那样,随着吸收系数的增加,体积变得更加不透明。 比尔-朗伯定律方程如下所示:
在这里插入图片描述

该定律指出,内部透射率之间存在指数依赖性,通过体积的光(T)与体积吸收系数(sigma_a)的乘积,以及光穿过材料的距离(即路径长度)。

这些系数的单位是距离的倒数或长度的倒数,例如cm^-1或者 m^-1 ,这很重要,因为它有助于直观地了解这些系数包含哪些信息。 如果你希望在任何给定点/距离发生随机事件(例如光子被吸收或散射),可以将吸收系数(以及我们稍后将介绍的散射系数)视为概率或可能性。

据说吸收和散射系数表示概率密度(如果你想对此主题进行更多研究)。 然而,由于它是一个不应超过 1 的概率,因此这取决于测量的单位。 例如,如果你使用毫米,则对于给定介质,你可能会得到 0.2。 但以厘米和米表示,则分别为 2 和 20。 因此,在实践中,没有什么可以阻止你使用大于 1 的值。

系数与平均自由路径之间的关系

吸收和散射系数的单位是长度的倒数这一事实很重要,因为如果取系数的倒数(1 除以散射系数的吸收),就会得到距离。 这个距离称为平均自由路径,表示随机事件发生的平均距离:
在这里插入图片描述

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

在这里插入图片描述

图1:距离越远或密度越大,内部透过率值越低。

吸收系数或距离越大,T 越小。比尔-朗伯定律方程返回 0-1 范围内的数字。 如果距离或吸收系数为 0,则方程返回 1。对于非常大的距离或密度,T 接近 0。对于固定距离,T 随着吸收系数的增加而减小。 对于固定的吸收系数,T 随着距离的增加而减小。 光在体积中传播得越远,被吸收的就越多。 体积中的粒子越多,吸收的光就越多。 简单的。 你可以在图 1 中看到此效果。

3、在均匀背景上渲染体块

从这里开始很容易。 想象一下,我们有一块厚度和密度已知的体积板。 分别说 10 和 0.1。 那么,如果背景颜色(例如我们正在看的墙壁反射的光)是(xr,xg,xb),那么我们通过体积看到的背景颜色有多少是:

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;

简单得不能再简单了。

4、散射

请注意,到目前为止我们都假设我们的体块是黑色的。 换句话说,无论我们的板在哪里,我们都只是使背景颜色变暗。 但体块不一定是这样。 像固体这样的体块也会反射(或更准确地说是散射)光。 这就是为什么当你在阳光明媚的日子看云时,你可以看到云的形状几乎就像是一个固体一样。 体块也可以发光(想象一下蜡烛火焰),我们只是为了完整性而提到这一点,但在本章中我们将忽略发光。

因此,我们假设我们的体块板具有特定的颜色(yr、yg、yb)。 我们暂时忽略该颜色的来源; 我们将在本章后面解释它。 我们只能说,直到那时,由于体块对象“反射”光(实际上不是,但让我们像固体对象一样使用“反射”的概念),我们的体块具有某种颜色,就像固体对象一样照亮它。 那么我们的代码就变成了:

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 中混合 (A+B) 图像的过程,例如使用 Alpha 混合。 假设你想在 A 上添加图像 B,其中 A 是背景图像(我们的蓝色墙壁),B 是带有透明通道的红色圆盘的图像。 组合这两个图像的公式为:

在这里插入图片描述

这里的透明度是 1 - 透射(也称为不透明度),B 是体块对象的颜色(被体块“反射”并向我们的眼睛/相机传播的光)。 当我们讨论光线行进算法时,我们会回到这一点; 现在,请记住这一点。

5、渲染我们的第一个体块球

我们拥有渲染第一个 3D 图像所需的一切。 我们将使用我们迄今为止所学到的知识来渲染一个我们假设充满一些粒子的球体。 我们假设我们正在某些背景上渲染球体。

原理很简单。 我们首先检查相机光线和球体之间的交点。 如果没有交集,那么我们只需返回背景颜色。 如果存在交点,我们就计算球体表面上光线进入和离开球体的点。 从那里,我们可以计算光线穿过球体的距离,并应用比尔定律来计算有多少光穿过球体。 我们现在假设球体“反射”(散射)的光是均匀的。 稍后我们将讨论照明。
在这里插入图片描述

图 2:穿过体块的相机光线。
在这里插入图片描述

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

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,这意味着体块球的颜色比背景的颜色占主导地位。
在这里插入图片描述

你可以在上面的图像中看到,体积朝着球体中心变得更加不透明(光线穿过球体的距离最大。你还可以看到,随着密度的增加(随着 sigma_a 的增加),球体整体变得更加不透明。

6、让我们添加光! 内散射

到目前为止,我们已经有了一个漂亮的体块球图像,但是照明呢? 如果我们将光照射到体积物体上,我们可以看到体块中更直接暴露在光线下的部分比阴影中的部分更亮。 体块也被灯光照亮。 我们如何解释这一点?

原理很简单。 让我们想象一下光源发出的光穿过体积的命运。 当它穿过体积时,其强度由于吸收而减弱。 毫不奇怪,光能在体积中传播一定距离后还剩下多少,是由比尔定律决定的。 换句话说,如果我们知道光穿过体积的距离,则该距离处的强度为:

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;

首先,根据比尔定律,光能在穿过体积时会减少。 这是非常符合逻辑的。 但还会发生其他情况:由于我们所说的散射效应,光源发出的光最初并未射向眼睛,但也可能被重定向到眼睛(至少是我们将看到的一部分)。

我们讨论的是内散射的特殊情况。 内散射是指光穿过某个体积,由于散射事件而被重定向到眼睛。 这种效应如图 4 所示。散射事件是光子与构成介质/体积的粒子/原子之间相互作用的结果。 原子不会被吸收或反射(这也可能发生),而是只是沿着与其入射方向不同的方向“吐出”光子。 我们将在接下来的章节中详细了解这种现象。

在这里插入图片描述

图 4:我们透过体块看到的光来自背景物体(此处为蓝色)以及光源。 尽管光源发出的光束没有传播到眼睛,但由于内散射效应,一些量的光在穿过体积物体时会被重定向到眼睛。

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

那么我们如何解释光源的贡献呢? 我们需要“测量”向眼睛散射的光(以及相机光线)作为内散射的效果。 问题是我们需要考虑沿着与球体相交的相机光线的整个部分的这种影响(图 5)。 我们需要“积分”沿着相机光线在 t0-t1 范围内散射的光。

在这里插入图片描述

图 5:我们需要对由于沿着穿过体块的光线部分进行内散射而被重定向到眼睛的光进行积分。

为了解决这个问题,我们将穿过体块的相机光线部分分成一定数量的片段(如果您愿意,我们的示例),并使用以下过程计算到达每个片段中心的光量(示例)(请参见图 6 以直观地了解该概念):

  • 我们从该样本点(我们称之为 X)向光源发射一条射线,以计算从样本点到球体边界(我们称之为点 P)的距离。 请注意,X 始终位于球体(我们的体积)内部,而 P 始终是球体表面上的点。
  • 然后应用比尔定律来了解光能从 P(光线进入球体的点)传播到 X(沿着眼睛光线的点,光线向观察者散射的点)时衰减了多少。
    在这里插入图片描述

图 6:以常规步骤沿着射线行进,使用黎曼和来估计积分。
在这里插入图片描述

图 7:我们可以使用黎曼和来估计代表沿相机散射的光量的曲线下面积。 这个想法是将曲线下的区域分解为小矩形的总和。 每个矩形的高度由 Li(x) 给出,宽度 dx 由用户定义。

为了了解我们在这里解决的问题类型,我们需要查看图 6 和图 7。图 6 显示了沿着相机光线到达的入射光,正如你在图底部看到的那样,它是一个连续函数。 我们将此函数称为 Li(x),其中 x 是沿包含在 t0-t1 范围内的摄像机光线的任意点。 我们需要计算的是曲线下方的“面积”。 在数学中,它是一个积分,我们可以写成:

在这里插入图片描述

正如我们刚才所说,积分的结果(数字)被定义为曲线下的(净符号)面积(函数 Li(x)),如图 6 所示。我们案例中的问题是我们无法使用解析解来计算该面积。 但我们可以使用一种技巧来近似该区域,将其分解为我们知道矩形面积的更简单的形状(如图 7 所示)。 我们沿着曲线以固定间隔对 Li(x) 进行采样,我们知道 (dx) 的宽度,然后可以将所得矩形的面积计算为 Li(x) 乘以 dx(x 位于间隔的中间)。 通过将所有矩形的面积相加,我们可以得到曲线下面积的近似值。 等等瞧! 这种技术被称为黎曼求和(用我们已知的面积来近似未知面积的形状的想法可以追溯到希腊人)。

那么它是如何转化为代码的呢?我们下次介绍。


原文链接:体渲染简明教程 — BimAnt

猜你喜欢

转载自blog.csdn.net/shebao3333/article/details/131888400