水焦散的实时渲染

2020-08-30 20:31:32

在本文中,我提出了使用WebGL和ThreeJS实时推广焦散计算的尝试。这是一种尝试,这一点很重要,找到一种在所有情况下都能很好地工作并以60fps运行的解决方案是困难的,如果不是不可能的话。但是你会看到,使用这种技术我们可以得到相当不错的结果。

焦散是光从表面(在我们的例子中是空气/水界面)折射和反射时出现的光图案。

由于水波的反射和折射,水就像一个动态放大镜,创造了那些光图案。

在这篇文章中,我们主要关注由于光线折射而产生的焦散,所以主要是在水下发生的情况。

为了获得稳定的60fps,我们需要在显卡(GPU)上计算它们,所以我们将完全使用用GLSL编写的着色器来计算它们。

计算水面上的折射光线(在GLSL中很简单,因为提供了一个内置函数)。

我总是对埃文·华莱士(Evan Wallace)的这个演示感到惊讶,它使用WebGL:madebyevan.com/webGL-water展示了视觉上令人信服的水焦散。

我非常推荐阅读他的Medium文章,其中解释了如何使用光线前网格和偏导数GLSL函数实时计算它们。他的实现速度非常快,而且非常好看,但它也有一些缺点:它只适用于立方池和池中的球形球。您不能将鲨鱼放到水下并期望演示能够工作,这仅仅是因为它在着色器中是硬编码的,即它在水下是一个球体。

把球体放在水下的原因是,计算折射光线和球体之间的交点很简单,而且涉及到非常简单的数学问题。

所有这些对于演示来说都很好,但我想要一个更通用的焦散计算解决方案,这样任何类型的非结构化网格都可以像鲨鱼一样躺在池子里。

现在,让我们开始我们的方法吧。在本文中,我希望您已经了解了使用光栅化进行3D渲染的基础知识,以及顶点着色器和片段着色器如何协同工作在屏幕上绘制基本体(三角形)。

在以GLSL(OpenGL着色语言)编写的着色器中,您只能访问有关场景的有限数量的信息,例如:

当前正在绘制的顶点的属性(位置:3D矢量、法线:3D矢量等)。您可以将自己的属性传递给GPU,但它需要具有GLSL内置类型。

在当前帧,对于当前正在绘制的整个网格而言,U形是恒定的。它可以是纹理、相机投影矩阵、灯光方向等。它必须具有内置类型:int、Float、Sampler2D(纹理)、Vector 2、Vector 3、Vector 4、mat3、mat4。

但是没有访问场景中存在的网格的方法。

这就是为什么WebGL-water演示只能用简单的3D场景制作的原因。可以更容易地计算折射光线和可以使用制服表示的非常简单的形状之间的交点。对于球体,它可以由位置(3D向量)和半径(浮点)定义,因此可以使用制服将此信息传递给着色器,并且交点计算涉及非常简单的数学运算,可以在着色器中轻松快速地执行。

在着色器中执行的某些光线跟踪技术通过纹理传递网格,但这超出了2020年使用WebGL进行实时渲染的范围。我们必须牢记,我们希望每秒计算60张图像,使用大量光线才能获得令人满意的结果。如果我们使用256x256=65536光线计算焦散,这意味着每秒运行大量的交集计算(这也取决于场景中的网格数)。

我们需要找到一种方法,将水下环境表现为制服,并计算交叉点,同时保持良好的表现。

当涉及到动态阴影计算时,一种众所周知的技术是阴影映射。它通常在电子游戏中使用,它看起来不错,而且速度很快。

从灯光角度看,3D场景首先在纹理中渲染。该纹理将包含所有碎片深度(光源和碎片之间的距离),而不是包含碎片颜色。此纹理称为阴影贴图。

然后,在渲染3D场景时使用阴影贴图。当在屏幕上绘制碎片时,我们可以从阴影图中知道是否有另一个碎片在光源和我们当前的碎片之间。如果是这样的话,我们知道我们的碎片在阴影中,我们应该把它画得更暗一点。

您可以在这个优秀的opengl教程中阅读更多关于阴影贴图的内容,并找到精美的插图:www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping.。

您还可以在此处找到使用ThreeJS的实时示例(按“t”在左下角显示阴影贴图):threejs.org/examples/?q=shadowm#webgl_shadowmap.。

这项技术在大多数情况下都工作得很好。它可以处理场景中的任何类型的非结构化网格。

我的第一个想法是,我可以对水焦散执行类似的方法,这意味着首先在一个纹理中渲染水下环境,然后使用这个纹理来计算光线和环境之间的交点。我不仅渲染碎片深度,还渲染环境贴图中的碎片位置。

现在我有了水下环境贴图,我需要计算折射光线和环境之间的交点。

第一步:从光线和水面的交点开始。

步骤3:从折射光线方向的当前位置移动环境贴图纹理的一个像素。

步骤4:将注册的环境深度(存储在当前环境纹理像素中)与当前深度进行比较。如果环境深度大于当前深度,则意味着我们需要更进一步,因此我们再次应用步骤3。如果环境深度小于当前深度,则意味着光线在您从环境纹理读取的位置击中环境,您找到了与环境的交点。

一旦找到交叉点,我们就可以使用埃文·华莱士(Evan Wallace)在他的文章中解释的技术来计算焦散强度(和焦散强度纹理)。生成的纹理如下所示:

此纹理包含3D空间每个点的灯光强度信息。然后,我们可以在渲染最终场景时从焦散纹理中读取该灯光强度,并获得以下结果:

您可以在以下Github存储库中找到该技术的实现:github.com/martinrenou/triejs-corsitics。如果你喜欢,就给它一颗星!

如果要实时查看焦散计算的结果,可以尝试此演示:martinrenou.github.io/triejs-corbtics。

此解决方案在很大程度上取决于环境纹理分辨率。纹理越大,算法的精度就越高,但找到解决方案所需的时间就越长(在找到解决方案之前需要读取和比较的像素越多)。

另外,在着色器中读取纹理是可以的,只要你不做太多次,这里我们正在做一个循环,不断地从纹理读取新的像素,这是不推荐的。

此外,在WebGL中禁止While循环(这是有充分理由的),因此我们需要使我们的算法成为可由编译器展开的for循环。这意味着我们的循环需要一个在编译时已知的结束条件,通常是一个“最大迭代”值,如果在最大尝试次数后仍未找到交叉点,则强制停止查找交叉点。如果折射太重要,此限制会导致错误的焦散结果。

我们的方法没有Evan Wallace建立的简化方法那么快,但它比全面的光线跟踪方法更容易处理,并且可以用于实时渲染。但是,速度仍然取决于某些条件,如灯光方向、折射强度和环境纹理分辨率。

本文的重点是水焦散计算,但本演示中还使用了其他技术。

对于水面渲染,我们使用了天盒纹理和立方体贴图来获得一些反射。我们还使用简单的屏幕空间折射在水面上应用了折射(参见这篇关于屏幕空间反射和折射的文章),这种技术在物理上并不正确,但在视觉上很吸引人,而且速度很快。此外,我们增加了色差以增加真实感。

焦散上的色差:我们目前在水面上应用色差,但这种效果在水下焦散上也应该是可见的。

这项关于水的实时和逼真可视化的工作是由QuantStack领导的,并由ERDC创立。

我叫马丁·雷诺,我是QuantStack的一名科学软件工程师。在加入QuantStack之前,我曾就读于法国图卢兹的航空航天工程学院SUPAERO。我还在法国巴黎的Logilab和英国剑桥的EnThink工作过。作为QuantStack的一名开源开发人员,我参与了各种项目,从C++中的Xtensor和Xeus-python到Python和Javascript/Tyescript中的ipyfollet和bqlot。