[图形学] 实时体积水和泡沫的渲染
发布日期:2021-06-29 07:15:31 浏览次数:5 分类:技术文章

本文共 8664 字,大约阅读时间需要 28 分钟。

reference :《GPU Pro3》

Volumetric Real-Time Water and Foam Rendering Daniel Scherzer, Florian Bagar, and Oliver Mattausch

 

简介

        在过去十年内,模拟和渲染复杂的自然现象,如火、烟、云和流体等,一直是计算机图形学中一个活跃且多样化的研究领域。在这些现象中,水可能是最令人着迷和具有挑战性的问题,这是因为这是人们非常熟悉的一个现象。虽然水的视觉渲染质量在不断提高,我们距离了解真实水的所有物理特性还有很长的路要走,比如泡沫和水滴的形成以及它们与环境的相互作用。

        在本章中,我们提出了一种创建全动态多层实时水渲染的方法。这种方法可以表达水的体积特性和体积泡沫的物理形成,通过创建比以前实时方法更好的视觉效果。它基于一个非常快速的基于流体的模拟,使用Nvidia PhysX和OpenGL中的渲染进行硬件加速,因此可以轻松地以实时帧率运行。该算法占用的内存很小,易于实现并集成到现有的渲染引擎中。此外,我们的方法从美术的角度来看是高度可配置的,因此可以产生多种视觉外观,来帮助场景创建所需的氛围(见图3.1)。

        

       图3.1 提出的算法可以用多种方式表现水

模拟

         为了表现真实的水,我们首先必须模拟它的行为。Navier–Stokes方程的一个版本可以描述不可压缩流体的动态水,该方程对于流体运动应用了牛顿第二定律(动量守恒)以及质量守恒。这些方程式涉及了作用于流体的体积力(重力)和接触力(压力和应力)。这将会得到一个难以求解的非线性偏微分方程。

        Aswedonot需要一个精确的解决方案,但主要关注运行速度,我们可以得到一个近似的数值解。该问题域中的近似主要使用欧拉积分。(有更为准确的方法,例如Runge-Kutta或中点,但这些需要更多时间来评估)。

        平滑粒子流体动力学

        平滑粒子流体动力学(SPH)是模拟水的行为的一种强大而快速的方法[Desbrun和Gascuel 96]。这里的主要思想是通过粒子系统(将流体分为离散元素)来近似流体以计算其动态行为。每个粒子都具有质量和附加属性,例如位置、密度、速度和寿命。在经典粒子系统中,每个粒子仅基于其属性进行更新,为了速度会忽略粒子-粒子之间的相互作用。对于流体的模拟,由于存在接触力,这是不够的。每个粒子都可能潜在地影响所有其它粒子。如果我们使用数以万计的粒子,则导致计算复杂度大约为O(n2)——对于实际目的而言是很慢的。

struct FluidParticle{    Vector3 position ;    float density ;    Vector3 velocity ;    float lifetime ;    unsigned int id ; // unique number identifying the particle    float foam ;}

        清单3.1 表示单个粒子属性的结构

        在实践中,粒子根据它们之间的距离相互影响。因此,定义交互截止的距离是有意义的。在该距离内应用核函数来加权单个粒子彼此之间的影响。这有效地将计算复杂度降低到了O(n)。注意,这个截止距离也称为平滑长度,因为它给出了由核函数“平滑”属性的体积(因此称为平滑粒子流体动力学)。

        所有主流的物理引擎都已经包含了SPH的求解器。我们使用PhysX[Nvidia 11]由于它有硬件加速,但任何其它物理引擎也是可以的。通过将流体描述结构传递给物理引擎来创建流体模拟,物理引擎定义了SPH模拟的行为。模拟数据是一个非排序的3D点云,每个粒子都具有list 3.1所示的属性。请注意,在我们的使用中,粒子还包含泡沫参数,该参数按照3.2.2节中的描述进行更新。物理引擎通过应用SPH模拟更新并返回该点云。

        泡沫

        我们希望不仅可以模拟水颗粒,还可以模拟泡沫。泡沫是一种通过在液体中捕获气泡而形成的物质(见图3.4)。

        注意,这可以是流体表面上方的喷雾或气泡,但也可以在流体内部的深度,如瀑布的情况。泡沫的形成可以用韦伯数来描述,它被定义为动能与表面能量的比率:

        

       其中, ρ 是密度,v是液体和周围气体之间的相对速度,l是液滴直径,σ是表面张力。如果We较大,则动能大于表面能量,这导致水与周围的气体(空气)混合,结果会得到泡沫。出于我们的目的,我们使用粒径作为液滴直径,并使用表面张力的常数值。我们假设空气不移动,因此相对速度等于此时液体的速度。

        现在可以通过使用来自物理引擎的每个粒子的ρ和v来计算每个粒子的韦伯数。然后将该数字与用户定义的阈值进行比较,该阈值描述了该流体容易形成泡沫的程度。因此,颗粒可以分离成水颗粒和泡沫颗粒。因此,泡沫(从模拟的角度来看)并不是完全不同的现象,而只是水粒子的一种状态。

       有了所有这些模拟部件,我们现在必须解决如何渲染水的问题。

渲染

       使用扫描线渲染器以有效的方式渲染水是一个双重问题:首先,必须从模拟数据中提取水面;其次,该表面的阴影必须考虑水的体积特性。由于实时物理引擎通常限制为64K粒子,而电影中使用的离线系统通常使用数亿个粒子,因此我们的粒子尺寸会大很多;因此,我们必须格外小心,以提供可信的水面。

        

       图3.2 我们的分层水模型横截图:通过考虑每个像素处的泡沫Twf前面的水厚度Twb、泡沫厚度Tf和水的厚度来实现结果的体积外观。我们还将泡沫分成两个不同颜色的层(Tff),以获得更有趣的泡沫。有关各个图层的渲染,另请参见图3.5。

      Splatting

        从颗粒中提取表面的一种方法是使用行进立方体的方法。这具有许多的缺点:首先,由于预期水的表面连续变化,表面可能出现时间混叠;其次,行进立方体在计算上非常昂贵;第三,所得到的几何结构可能非常复杂,尤其是在结合泡沫效果时。一种避免这些缺点并且在当前硬件上运行良好的方法是splatting:想法是通过使用某种内核形状(例如:球体)将每个粒子splatting到深度(和厚度)缓冲区中(参加图3.2和3.6)。通过使用3D内核,我们创建了体积粒子。将粒子splatting到深度缓冲区(深度测试开启)会导致水面的屏幕空间近似(参见清单3.2)。当将splatting(加入球体半径,参见清单3.3)添加到厚度缓冲区时,会为每个像素创建一个近似的水厚度(我们稍后将用于对表面的着色)。

        以这种方式累积水厚度会得到一个可以接受的近似值,因为可以假设来自物理模拟的粒子在很大程度上是非重叠的。

FragShaderOutput FluidSplattingFP( float texcoord : TEXCOORD0, float eyeSpace : TEXCOORD1){    FragShaderOutput OUT;        // calculate eye-space normal from texture coordinate    float3 n;    n.xy = texCoord.y * float2(2.0f, -2.0f) + float(-1.0f, 1.0f);    float r2 = dot(n.xy, n.xy);    // kill pixels outside circle    if(r2 > 1.0f) discard;    // calculate radius    n.z = sqrt(1.0f - r2);        // positioin of this pixel on sphere in eye space    float eyeSpacePos = float4(eyeSpace.xyz + n * eyeSpace.w, 1.0f);    float clipSpacePos = mul(glstate.matrix.projection, eyeSpacePos);        // output eye-space depth    OUT.color = float4(eyeSpacePos.z, 0.0f, 0.0f, 0.0f);    OUT.depth = (clipSpacePos.z / clipSpacePos.w) * 0.5f + 0.5f;    return OUT;}

        清单3.2 用于将粒子数据作为球体Splatting深度纹理的像素着色器。

// calculate thickness with exponential falloff based on radiusfloat thickness = n.z * particleSize * 2.0f * exp(-r2 * 2.0f);OUT.color = float4(thickness, 0.0f, 0.0f, 0.0f);

        清单3.3 将粒子数据splatting到厚度纹理(注意:此着色器基于清单3.2中所示的着色器,并替换了它的最后8行代码。

        自适应屏幕空间曲率流过滤

        如果我们使用球形内核来展开粒子,直接渲染的结构往往难以令人信服(参见图3.3,左上图)。由于颗粒尺寸大,单个颗粒的球体几何形状清晰可见。使粒子更小并因此需要更多粒子来充分模拟场景不是一种选择,因为我们已经在最大的64k粒子附近操作了,并且更多粒子也会使得模拟和渲染更慢。另一种解决方案是以避免高曲率的方式平滑包含Splatting粒子的深度缓冲器。这是曲率流背后的思想[van der Laan et al. 09]。这里,表面沿法线向量移动,取决于表面的平均曲率:

        

        其中z是深度(在深度缓冲区读取到的),t是平滑时间不长,H是平均曲率。对于3D空间中的曲面,平均曲率定义如下:

        

        

        图3.3 直接将模拟中的粒子渲染为球体,产生类似果冻的水。通过使用水的厚度来减弱水的颜色(左上),可以改善水的阴影。通过应用屏幕空间曲率流过滤来迭代地平滑深度缓冲区来减小水的曲率,可以得到更令人信服的效果。

        其中是表面的单位法线。通过取x和y方向中视图空间位置p的导数之间的叉积来计算法线,从而得到单位法线[van der Laan et at.09]:

        

        其中:

        

        有限差分用于计算空间导数,Cx和Cy分别是x和y方向的视点参数,它们是根据视场和视口大小vx、vy的大小计算出来的,如公式(3.3)和(3.4)所示:

        

        将等式(3.2)中的单位法线代入等式(3.1),这能够允许H的导数,从而得到:

        

        其中:

        

        清单3.4中的GLSL着色器在屏幕空间中执行此操作。

        重复应用此滤波器的效果如图3.3所示,迭代过滤导致更大的平滑效果。如果我们想要随着观察者靠近和远离水时保持水的一定平滑程度,则必须针对每个像素自适应地调整迭代次数。我们发现迭代次数与眼睛距离间接成比例-靠近的水需要更多的迭代,远处的水需要更少的迭代。关于这一推导的细节可以在我们的论文中找到[Bagar et al. 10]

// samples for finite differencing (vsp = view space position)float depth = texRECT(depthMap, vsp.xy).x;float depth_d = texRECT(depthMap, vsp.xy + float2(0.0f, -1.0f)).x;float depth_l = texRECT(depthMap, vsp.xy + float2(-1.0f, 0.0f)).x;float depth_r = texRECT(depthMap, vsp.xy + float2(1.0f, 0.0f)).x;float depth_u = texRECT(depthMap, vsp.xy + float2(0.0f, 1.0f)).x;// derivatives (finite differencing)float dx = (0.5f * (depth_r - depth_l));float dy = (0.5f * (depth_u - depth_d));// second derivativesfloat dxx = (depth_l - 2.0f * depth + depth_r);float dyy = (depth_d - 2.0f * depth + depth_u);// constantsconst float dx2 = dx * dx;const float dy2 = dy * dy;const float Cx2 = Cx * Cx;const float Cy2 = Cy * Cy;// calculate curvaturefloat D = Cy2 * dx2 + Cx2 * dy2 + Cx2 * Cy2 * depth * depth;float H = Cy * dxx * D - Cy * dx * (Cy2 * dx * dxx + Cx2 * Cy2 * depth * dx)    + Cx * dyy * D - Cx * dy * (Cx2 * dy * dyy + Cx2 * Cy2 * depth * dy);H /= pow(D, 3.0f/2.0f);// curvature dependent shiftOUT.color = depth +epsilon * H;

        清单4.3 像素着色器代码,演示了屏幕空间曲率流过滤迭代步骤中的其中一步。

        泡沫和层次

        到目前为止,我们已经讨论了如何为水创建表面的问题。剩下的就是为这个表面提供逼真的阴影并增加泡沫效果。

        

        图3.4 由我们的渲染模型处理的三种泡沫形成情况:没有水的泡沫(左)、水上的泡沫(中间)和水中的泡沫(右)

        我们已经研究了泡沫产生的不同情况,并发现了一个像素的三个主要情况:没有水的泡沫、前面有水的泡沫和水中的泡沫(见图3.4)。我们忽略了更复杂的情况,例如多层泡沫,因为在实践中,与单个泡沫层相比的视觉差异可以忽略不计。这些例子可以演变为三层模型(见图3.2和图3.5)。

        

        图3.5 我们的水模型的各个层。背水层(左上)、泡沫层,两个用户定义的颜色,定义泡沫的外观(上部),前水层(左下)和反射加镜面高光(右下)。

        算法步骤

        为了将这一模型用于实践,我们的算法将水和泡沫颗粒分开并将它们使用不同的缓冲区进行splat。完整的算法在每个帧中执行以下步骤(参见图3.6):

        • 更新物理模拟

        • 将背景场景渲染为深度和颜色缓冲区

        • 通过将泡沫粒子splat到深度缓冲区中计算泡沫深度

        • 通过splat泡沫前水的粒子计算前水深度 

        • 通过应用自适应曲率流过滤计算滤波后的前水深度。

        

        图3.6 我们的方法中使用的缓冲区概述。Twf表示泡沫前面水的厚度,Twb表示泡沫后面水的厚度,Tf表示泡沫的厚度,Tff表示前泡沫层的厚度。

        • 计算厚度

            ◦ 泡沫Tf

            ◦ 泡沫前面的水 Twf

            ◦ 泡沫背后的水 Twb

            ◦ 前泡沫层 Tff

       • 执行体积合成

        我们对水粒子使用球形内核,并将球形内核与泡沫粒子的perlin噪声纹理相乘以获得更多的细节。平滑仅适用于最前面的水层,因为泡沫层后面的水表面无论如何都会被混淆。前泡沫层厚度Tff是人为构造的,其仅在泡沫深度后面的用户定义的距离内积累泡沫粒子。我们发现这样可以创造更有趣的泡沫。

        我们已经讨论了每个步骤背后的基础知识,除了最后一个步骤——体积合成。它建立在所有其它步骤之上。

        体积合成

        层的通常合成方式和体积合成之间的区别在于我们将每层的厚度考虑在内,以衰减视线。沿着视线合成到前面,我们有(图3.2):

        

        其中cfluid是水的颜色,cff和cfb是两种用户定义的颜色,它们混合在一起,为设计泡沫外观创造了更大的自由度。

        经过衰减后,我们计算前水面的高光,以及反射和折射(包括了菲涅尔项)。在这里,折射将整个水(前水层和后水层)视为一个体积,因此Cbackground从沿法线向量扰动的场景背景纹理中采样,使用Twb + Twf进行缩放[van der Laan et at .09]。

       图3.5 和3.7显示了合成中使用的各个步骤和颜色,清单3.5显示了GLSL像素着色器代码。

       

        图3.7 用户定义的颜色(cfluid, cff, cfb),以及来自组合步骤(Cbackground, Cwb, Cfoam, Cf , Cwf)(左)和最终着色效果(右)的结果颜色。

// surface properties ( v = view vector, h = half angle vector)float specular = pow(max(0.0, dot(normal, h)), fluidShiniess);// bias, scale, and power = user-defined parameters to tune// the Fresnel Termfloat fresnelTerm = bias + scale * pow(1.0 + dot(v, normal), power);float3 c_reflect = texCUBE(cubeMap, mul((float3x3)inView, reflect(-v, normal)));//...// attenuation factors (incl. user-defined falloff scales)float att_wb = saturate(exp(-t_wb * falloffScale));float att_foam = saturate(exp(-t_f *foamFalloffScale));float att_ff = saturate(exp(-t_ff * foamScale));float att_wf = saturate(exp(-t_wf * falloffScale));// composition (frag = fragment position in screen space)float3 c_background = texRECT(scene, frag.xy + normal.xy * (t_wb + t_wf));float3 c_wb = lerp(c_fluid, c_background, att_wb);float3 c_foam = lerp(c_fb, c_ff, att_ff);flaot3 c_f = lerp(c_foam, c_wb, att_foam);float3 c_wf = lerp(c_fluid, c_f, att_wf);// calculate factor to suppress specular highlights if foam// is the frontmost visual elementfloat spw = saturate(1.0f - att_wf + att_foam) * (1.0f - att_wf);//  combine with fresnel and specular highlightfloat3 surfaceColor = lerp(c_wf, c_reflect, min(fresnelTerm, spw)) + fluidSpecularColor * specular * spw;

         清单3.5 用于沿视线合成像素着色器:照明、依赖于厚度的衰减和最终颜色部分。

美术控制   

        我们在算法中引入了很多用户的可控参数,以允许美术产生多种视觉外观(见图3.1)。

        水的视觉外观由流体/水的颜色(cfluid)控制,其在组合期间根据不同的水厚度(twb,twf)和用户定义的衰减量程度组合。此外,用于控制菲涅尔方程近似的镜面反射颜色和镜面高光,以及菲涅尔偏差,比例和功率,都是由美术控制的。然后我们使用菲涅尔方程的近似来组合我们算法的反射和厚度相关衰减(参见清单3.5)。

        通过修改或替换相应场景的立方体环境贴图(cubeMap),美术可以影响反射本身,因为它直接与厚度相关的衰减混合,此外,它还描述了场景的静态照明。

        泡沫的视觉外观由泡沫的背面颜色(参见b)和泡沫的正面颜色(参见f)组成,它们根据前泡沫层的厚度(tff)混合(见图3.7)和用户定义的比例因子。同样,对于水,使用用户定义的衰减量可用于控制泡沫的总体不透明度,应该向美术开放的最后一个参数是韦伯数阈值。如前所述,该用户定义的阈值控制模拟流体是否容易形成泡沫。

总结

        我们提出了一种基于物理的水(流体)和泡沫渲染的算法,可以处理任意动态场景,允许水自由地交互并流到各处,也允许美术微调结果的外观。该算法在现代硬件上实时(平均16ms渲染时间)运行,并与渲染引擎(尤其是延迟着色)能很好地集成。

        有关所提方法的更多理论细节和详尽内容,请参阅文章[Bagar et al.10]和这项工作所依据的论文[Bagar 10]。

转载地址:https://blog.csdn.net/ZJU_fish1996/article/details/89036622 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:[OpenGL] 动态积雪效果
下一篇:[OpenGL] 屏幕空间反射效果

发表评论

最新留言

逛到本站,mark一下
[***.202.152.39]2024年04月26日 04时15分55秒