实时渲染苛性水

在本文中,我将介绍使用WebGL和ThreeJS概括实时焦散计算的尝试。这是尝试的事实,这一点很重要,因为要找到一种在所有情况下都可以工作并提供60fps的解决方案非常困难,即使不是不可能的。但是您会看到,借助我的方法,您可以获得相当不错的结果。



什么是苛性的?



焦散是当光在水和空气的边界处从表面折射和反射时发生的光图案。



因为反射和折射是在水波上发生的,所以水在这里充当了动态透镜,从而产生了这些光的图案。





在这篇文章中,我们将重点讨论光折射引起的焦散,光折射通常在水下发生。



为了获得稳定的60fps,我们需要在图形卡(GPU)上进行计算,因此我们仅使用GLSL编写的着色器来计算焦散。



要计算它,我们需要:



  • 计算在水表面折射的光线(在GLSL中,这很容易,因为有内置函数
  • 使用相交算法计算这些射线与环境碰撞的点
  • 通过检查射线的会聚点来计算苛性亮度




WebGL上著名的水演示



我一直对这个Evan Wallace演示感到惊讶,该演示在WebGL上展示了视觉上逼真的水腐蚀性madebyevan.com/webgl-water





我建议阅读他的“中型”文章,其中介绍了如何使用轻型前网格和GLSL PD函数实时计算焦散。它的实现速度非常快,看起来非常不错,但是有一些缺点:它仅适用于立方体池和球形池球。如果将鲨鱼放在水下,则演示无法正常工作:在着色器中硬编码为在水下存在球形球。



他在水下放置了一个球体,因为计算折射光线与球体之间的交点是一项很容易的任务,它使用非常简单的数学方法。



这对演示很有好处,但是我想创建一个更通用的解决方案。 计算焦散,以便任何非结构化的网格(例如鲨鱼)都可以在池中。





现在让我们继续我的技术。在本文中,我将假定您已经了解使用栅格化进行3D渲染的基础知识,并且熟悉顶点着色器片段着色器如何协同工作以将图元(三角形)呈现到屏幕。



使用GLSL约束



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



  • 当前绘制的顶点的属性(位置:3D矢量,法线:3D矢量等)。我们可以传递我们的GPU属性,但是它们必须是内置的GLSL类型。
  • Uniform,即当前帧中整个当前渲染的网格的常量。这些可以是纹理,相机投影矩阵,照明方向等。它们必须具有内置类型:int,float,用于纹理的sampler2D,vec2,vec3,vec4,mat3,mat4。


但是,无法访问场景中存在的网格



这就是为什么只能使用简单的3D场景完成webgl-water演示的原因。计算折射光线与可以使用均匀表示的非常简单的形状的交点会更容易。对于球体,可以通过位置(3D矢量)和半径(浮点数)指定它,因此可以使用Uniform将该信息传递给着色器,并且计算交点需要非常简单的数学运算,可以在着色器中轻松快速地执行。



在着色器中执行的某些光线跟踪技术可在纹理中渲染网格,但在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



在大多数情况下,此技术效果很好。它可以与场景中的任何非结构化网格一起使用。



起初我以为我可以对水苛性碱使用类似的方法,即首先将水下环境渲染为纹理,然后使用此纹理来计算射线与环境之间的交点。... 我不仅渲染片段的深度,还渲染了片段在环境图中的位置。



这是创建环境贴图的结果:





信封贴图:XYZ位置存储在RGB通道中,深度存储在alpha通道中



如何计算光线与环境的交点



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



该算法的工作原理如下:



  • 阶段1:从光线与水面的交点开始
  • 第2阶段:使用折射函数计算折射
  • 阶段3:从当前位置沿折射射线的方向出发,即环境贴图的纹理中的一个像素。
  • 阶段4:将注册的环境深度(存储在环境纹理的当前像素中)与当前深度进行比较。如果环境的深度大于当前深度,那么我们需要继续前进,因此我们再次应用步骤3如果环境深度小于当前深度,则意味着光线在从环境纹理读取的位置与环境碰撞,并且我们发现与环境有交集。




当前深度小于环境深度:您需要继续前进





当前深度大于周围深度:我们发现了相交处



腐蚀性质地



找到交点后,我们可以使用Evan Wallace在他的文章中描述的技术来计算苛性亮度(和苛性亮度纹理)产生的纹理如下所示:





苛刻的亮度纹理(请注意,苛刻的效果在鲨鱼上不太重要,因为它更靠近水表面,从而减少了光线的会聚)



此纹理包含有关3D空间中每个点的光强度的信息。渲染完成的场景时,我们可以从苛性纹理读取此光强度,并获得以下结果:







可以在Github存储库中找到该技术的实现:github.com/martinRenou/threejs-caustics如果喜欢,给她一颗星星吧!



如果要查看焦散计算的结果,可以运行演示:martinrenou.github.io/threejs-caustics



关于此交集算法



这个决定高度依赖于环境纹理的分辨率。纹理越大,算法的精度越好,但是找到一个解决方案所花费的时间越长(在找到它之前,您需要计数和比较更多的像素)。



同样,只要不做太多次,就可以在着色器中读取纹理。在这里,我们创建了一个循环,该循环继续从纹理读取新像素,不建议这样做。



此外,WebGL中不允许while循环。(并且有充分的理由),因此我们需要在for循环中实现一种可由编译器扩展的算法。这意味着我们需要在编译时就知道一个循环终止条件,通常是“最大迭代”值,如果我们未在最大尝试次数内找到解决方案,这将迫使我们停止寻找解决方案。如果折射太重要,此限制会导致不正确的苛性结果。



我们的技术没有埃文·华莱士(Evan Wallace)建议的简化方法快,但是它比完整的射线追踪方法灵活得多,并且还可以用于实时渲染。但是,速度仍然取决于某些条件-光的方向,折射的亮度和环境纹理的分辨率。



结束演示审查



在本文中,我们着眼于计算水的腐蚀性,但是在演示中使用了其他技术。



渲染水面时,我们使用了天空盒纹理和立方体贴图来获取反射。我们还使用屏幕空间中的简单折射将折射应用于水表面(请参阅本文中有关屏幕空间中的反射和折射的信息),该技术在物理上是不正确的,但在视觉上令人信服且快速。我们还添加了色差,以提高真实感。



我们还有更多想法可以进一步改善方法,包括:



  • 苛性碱的色差:现在,我们将色差应用于水面,但是这种效果在水下苛性碱中也应可见。
  • 光在水中的散射。
  • 正如马丁·杰拉德(Martin Gerard)和艾伦·沃尔夫(Alan Wolf)在Twitter上所建议的那样,我们可以使用分层环境图(将其用作四叉树来查找相交点)来提高性能。他们还建议根据折射光线绘制环境图(假设它们是完全平坦的),这将使性能与照明的入射角度无关。


致谢



现实的,实时的可视化水这项工作是在开展QuantStack和资助ERDC



All Articles