求包围盒体积_移动端rayMarching体积雾/体积云的尝试

本文介绍了在移动端使用RayMarching实现体积雾和体积云的技术细节,包括2dFractalNoise、优化策略(downSample、jitter等)以及针对移动平台的特定调整。通过在不同设备上的性能测试,展示优化后的体积雾效果,并分享了在资源有限的情况下如何达到高性能表现。
摘要由CSDN通过智能技术生成

9b5b0956b0ddfc53ea3a622a7116c834.png

本文谨代表本人的理解,旨在抛砖引玉,如有错误或建议,还望大佬们指教。

0、前言

提到体算法,主机端pc端一般采用rayMarching思想进行模拟。这方面可以查阅的资料已经很多了。本人在不久前参考这些文章,在unity中做出了简单的效果。

但毕竟从事手游开发,技术需要落地到移动平台。在移动端的实现和主机pc端有所不同,后续也许要做一些额外的优化。最终在手边最差的机器maliG51(麒麟710)上跑满了60帧,体积雾耗时2ms+-0.2ms,另外通过调参还有更多优化空间。

由于体积算法的文章已经有很多了,但在移动平台的实现还是空白。因此本文将不再过多赘述具体的算法细节,而是以在移动端跑满60帧为出发点,一切皆能省则省,阐述一下本人在移动端的实现与主机端pc端的差别,以及一些后续的优化方法。

抛砖引玉,还望大家多提意见。

1、先提出本人的结论

体积雾的实现采用:rayMarching+2dFractalNoise

优化采用:downSample+jitter+temporalFilter+blur+edgeResample

由于移动平台的一些限制,首先在形态模拟就无法得到云的细节,再加上一系列会优化后(后文均会提到),个人认为作为云来讲,形状和边缘细节损失比较大。不过用来模拟雾效,个人觉得效果还是不错的。

作为程序审美有限,最终配合大气散射,高度雾,做出了沙漠中黄沙滚滚的效果。并在mi10pro上跑满了60帧。我们先看视频:(没想到视频压缩这么厉害,具体的细节还是看图吧)

df9b3f9388da8c9af7912a88fa660345.png
mi10p: 散射+高度雾+双层体积云https://www.zhihu.com/video/1230312776129409024

a839d765fc1c7adf9e237f7345649a2c.gif
对比

257214ec095a78e8aa8749ecac163e73.png
一些细节

9cb36af968de8c1207de6f9f330477de.png
在天空中的样子(角度限定,没有细节,并不实用)

f661220e09f1ca899e7c0618c2e7a734.png
雾效

使用最费的沙尘暴效果进行测试。机器有限,仅测试了mi10pro和小米mix2华为nova4e。

mi10p作为新一年的旗舰机自然效果全开,体积雾也直接上了两层。大气散射+高度雾+双层体积雾。跑满60帧。其中体积雾耗时<1ms。

另外还测试了小米mix2华为nova4e。无散射+unity原生指数雾+单层体积雾。跑满60帧。其中在较低配的nova4e中体积雾耗时2ms+-0.2ms。

2、RayMarching的原理

由于rayMarching的文章已经比较多了,本文不再对其中的细节做过多的赘述。具体的思路以及流程,可以参考链接里的FMX演讲,仅有22分钟,言简意赅,非常值得观看。

FMX2017 Technical Directing Special: Real-time Volumetric Cloud Rendering​www.youtube.com

将talk简单的概括一下,就是从相机方向,每个像素向世界方向产生射线。射线在包围盒内,每隔一段距离进行采样与计算。直到射线总距离超过包围盒的最远距离或深度图的距离。至于云的形状,则采用Worley噪声作为大型,Perlin噪声修整边缘。(talk中穿插了循环噪声生成,后续还采用蓝噪声与TAA做步长优化)

3da6ce2c4a2ed16b732f44e5f6865992.png
FMX2017-rayMarching-noise

3、实现

3.1 拿到深度图

之前在知乎上看到了免费拿到深度图的方法。了解了一下后发现,只需要首尾帧调用如下方法就可以了,不需要每一帧重新set。此外,也如链接中所述,由于拿走了backBuffer。需要添加后处理相机,进行颜色输出与后处理渲染。

public void InitBuffer(Camera bufferCam)
{
    if(colorBufferRT == null)
    {
        colorBufferRT = RenderTexture.GetTemporary(bufferCam.pixelWidth, bufferCam.pixelHeight, 0);
        depthBufferRT = RenderTexture.GetTemporary(bufferCam.pixelWidth, bufferCam.pixelHeight, 24, RenderTextureFormat.Depth);
    }
    bufferCam.SetTargetBuffers(colorBufferRT.colorBuffer, depthBufferRT.depthBuffer);
}

public void ReleaseBuffer(Camera bufferCam)
{
    if (colorBufferRT != null)
    {
        RenderTexture.ReleaseTemporary(colorBufferRT);
        RenderTexture.ReleaseTemporary(depthBufferRT);
    }
    bufferCam.targetTexture = null;
}
BQ哥:unity自定义深度图(drawcall不翻倍使用深度图)​zhuanlan.zhihu.com
923f73dea6b5e9a4faa1a52956a3b9b1.png

3.2 Marching!

首先,从结论出发,我们已经把体算法用作雾效了。所以不能再和云的渲染一样,放到天空中,只用高度作为界限。我们可以在世界空间内定义aabb盒,使用射线与aabb盒求交。从而更方便的定义雾效的作用范围。另外,还可以加入雾效半径,通过限制rayMarching前进距离减少开销。

8c55cc817815fab6a48e7c22324f71ae.png
左:根据高度限制采样范围 | 右:世界空间定义aabb盒限制采样范围

使用slabMethod计算相交区域

Fast, Branchless Ray/Bounding Box Intersections​tavianator.com
39ba920827ff3950a24dff06efd70d45.png
inline float2 rayBoxDst(float3 boundsMin, float3 boundsMax, float3 rayOrigin, float3 invRaydir) {
    float3 t0 = (boundsMin - rayOrigin) * invRaydir;
    float3 t1 = (boundsMax - rayOrigin) * invRaydir;
    float3 tmin = min(t0, t1);
    float3 tmax = max(t0, t1);

    float dstA = max(max(tmin.x, tmin.y), tmin.z);
    float dstB = min(tmax.x, min(tmax.y, tmax.z));

    float dstToBox = max(0, dstA);
    float dstInsideBox = max(0, dstB - dstToBox);
    return float2(dstToBox, dstInsideBox);
}

根据深度图判断遮挡,根据采样半径限制距离

    float3 ro = _WorldSpaceCameraPos;
    float3 rd = normalize(rayXYZ);
    // 计算得到距包围盒的最短/最长距离
    float2 rayToContainerInfo = rayBoxDst(boundsMin, boundsMax, ro, 1 / rd);
    float dstToBox = rayToContainerInfo.x;
    float dstInsideBox = rayToContainerInfo.y;
    // 距离还需要考虑深度图
    float depthWithoutLinear = SAMPLE_DEPTH_TEXTURE(_MainDepthTex, uvDepth);
    float depth = LinearEyeDepth(depthWithoutLinear) * length(rayXYZ);
    // 前进到包围盒的最大距离or遇到了深度图
    float dstLimit = min(depth - dstToBox, dstInsideBox);
    // 限制距离相机半径以节省性能
    dstLimit = min(fogRadius - dstToBox, dstLimit);
    float3 entryPoint = ro + rd * dstToBox;

3.2 形状

*云和雾的分水岭

3.2.1 3d还是2d

一般来说,在模拟云层形状时,会采用3d噪声,通过采样3d噪声模拟云的形状,增加云在垂直方向的层次感。

这就带来了一些问题。首先不是所有es2.0都支持tex3D。不过这也不是大问题,因为3d噪声可以通过切片的形式使用tex2D采样切片。但不论是带宽限制,还是采样开销,移动端都架不住每个步长获取一个三维空间浓度信息。

因此在这里使用了一个通道的2d,只进行xz平面的形状模拟。如图:

1b6fa86fd19d49738548cce5601b986c.png
左:3d云(网图) | 右:我们的2d........

这里特地找到了迪士尼提供的3d噪声,用来对比只有一个通道的2d噪声。可以看到第一步就被甩了几条街...已经和云渐行渐远...更不用提地平线在不同大气高度模拟不同形状的云了。

3.2.2 Y轴怎么办

由于省掉3d噪声没有y轴信息,水平方向只能这样笔直的垂下来,像是柱形。为了解决这一现象,我们可以对y轴添加形状信息。这里使用了最简单的一元一次方程进行模拟。如此也避免了每个采样点额外采样一张高度ramap的操作。

float getHeightGradient(float heightPercent) {
    float heightGradientUp = HeightUpDownKB.x * heightPercent + HeightUpDownKB.y;
    float heightGradientDown = HeightUpDownKB.z * heightPercent;
    float heightGradient = saturate(heightGradientDown) * saturate(heightGradientUp);
    return heightGradient;
}

eadd56056a4ed588771d083a2cda98c5.png
在水平方向,使用两个简单的一元一次方程做剔除,得到圆滑的两端

3.2.3 分形噪声

在云雾的运动中可以采用分形噪声的思想,使用不同速度、不同频率的噪声叠加模拟流动效果。算是比较常见的做法了。

fractals, computer graphics, mathematics, shaders, demoscene and more​www.iquilezles.org
46008d3f4f963ae0cbcd97583ec0fc55.png

ebaf30e3ee98d8191a2c3140ef240885.gif
分形噪声

3.3 颜色

*移动端和主机pc端的第二处不同

这里先不看光照公式。实际上,上面的采样只得到了云在视方向的浓度。光照的计算还需要在上面的每个采样点,向光源方向采样,得到光源方向浓度信息,再结合光照公式计算光照强度。

由此可见,如果在计算中还向光源采样累加,那么复杂度将成倍增长。这显然不是我们希望看到的。

因此,这里将向光源的浓度直接离线写进了噪声图的g通道。在形状采样中直接读取g通道,模拟向光源方向的浓度。虽然无法与光照互动了,但省掉了很大一部分的开销。同时还可以留下b通道,供以后模拟heightmap使用。

可参考"添加raymarching"章节,这里是一个思路​blog.csdn.net
7351a31836ef5e4169e09bb85bad369a.png
while (dstTravelled < dstLimit) {
    float3 rayPos = entryPoint + rd * dstTravelled;
    float4 density4 = samplerDensity4(rayPos, boundsMin, boundsMax);
    if (density4.r > 0) {
        // 假装采样了...
        sumRG += FogLightingFunction(density4.r, density4.g);

07fde24af5daee73bc4d4e312ab1489b.png
选用什么光照模型,参考talk套公式就可以了

不过在最后,发现如果正常计算,阴影部分为黑色,会显得比较死板。使用两个颜色在光强结果中做差值。可以看到右图的暗部,实则为蓝紫色。

fixed4 getMixedFogColor(fixed sumR, fixed sumG) {
    //...
    float4 col = fixed4(sumR * CloudColor.rgb + (1 - sumR) * sumG * CloudSecondColor.rgb, sumG);
    return col;
}

83955bf3f3bba4e82d1852273c46d8a4.png
g通道模拟向光浓度

4、优化

至此,在省略了一堆操作后,体积雾的正常渲染就算是完成了。但是即使这样,在手机上还是开销很大的。因此还需要进一步优化。

4.1 步长

根据rayMarching的基本原理。每一次前进就是一次采样计算的开销。越密集的步长虽然会带来更好的效果,但同时也意味着更大的开销。

所以首先根本出发,增大步长以换取更好的性能。在这里,本文根据射线前进的距离增加步长,同样是使用了简单粗暴的一元一次方程。并限制最大采样次数以避免极端情况的出现。

while (dstTravelled < dstLimit) {
    float3 rayPos = entryPoint + rd * dstTravelled;
    float4 density4 = samplerDensity4(rayPos, boundsMin, boundsMax);

    if (density4.r > 0) {
        // ...

        // 最大有效采样次数,避免平视时穿透云层采样次数过多
        nowStepCount += 1;
        if (nowStepCount > MaxStepCount) break;
    }
    // 简单的一元一次方程,随采样距离增大步长
    stepSize = stepSizeKB.x * dstTravelled + stepSizeKB.y;
    dstTravelled += stepSize;
}

d2e338b1abcfb9467bef0e45b423f49b.png
根据射线前进距离增大步长,可以观察到明显的断层

4.2 抖动+时间滤波器

在和步长相关的操作中,这两者基本都是同时出现的。搭配使用,可以在一定程度上消除步长过大出现断层。

抖动(jitter)在这里的作用是屏幕像素每个像素,每一帧产生一个随机数,对采样的起始长度做叠加。比如第一帧少采一点,第二帧多采一点。然后使用时间滤波器(temporalFilter),对两帧做融合,得到平滑的颜色信息。

// 在前进前,先抖动一定距离
float dstTravelled = frac(sin(uv.x - uv.y + _Time.y) * 999999);
// ...
// rayMarching
while (...

注意这里的时间滤波器与时间抗锯齿(TAA)还是有所区别的。完全没有TAA那么高级。这里只是作用于体积雾效果,将雾效的采样抖动做融合,其他都没有考虑。因此,这里也在一定程度上造成了雾的涂抹感。

// 这一帧的颜色
float4 thisCol = tex2D(_MainTex, i.uv);
// 向周围采样,得到这一帧颜色区间
float4 thisMinCol = GetThisFrameMinCol(i.uv);
float4 thisMaxCol = GetThisFrameMaxCol(i.uv);
// 根据深度图,反推上一帧uv
float depth = tex2D(_MainDepthTex, i.uv).r;
float4 worldPos = mul(CurVPInverseMatrix, float4(i.uv * 2 - 1, depth, 1));
float4 lastClip = mul(PreVPMatrix, worldPos);
float2 uv = lastClip.xy / lastClip.w;
uv = uv * 0.5 + 0.5;
float2 velocity = i.uv - uv;
float2 lastUV = i.uv - velocity;
// 根据上一帧uv拿到这个像素上一帧的颜色信息,同时给定范围限制
float4 lastCol = tex2D(_LastTex, lastUV);
lastCol = clamp(lastCol, thisMinCol, thisMaxCol);
// 融合
thisCol = lerp(thisCol, lastCol, LastFrameLerp);
return thisCol;

c7ec98e96b5d8ec28c0d31ec45ea560d.png
左:无 | 中:jitter | 右:temporalFilter

4.3 降采样+边缘重绘

上文从rayMarching原理层面做了优化,然而最简单粗暴的,还是降采样。降到1/2就省了75%,十分可观。

然而低分辨率上采样时,会图片边缘,产生锯齿感。这时先对低分辨率的雾,或低分辨率的深度图做边缘检测。得到边缘信息回到高分辨率后再重绘。

不过边缘重绘在一些情况下带来了新的问题。比如当场景复杂度高时,边缘信息过多,高分辨的绘制还会带来额外开销。因此还有待测试。

Chapter 23. High-Speed, Off-Screen Particles​developer.nvidia.com
6caba220c8a59b9685455cb03c6487bf.png

4103d13e3232e6a1b148c82272a842bf.png
低分辨率查找边缘,高分辨率重绘

5. 体积云?体积雾

最后放到天空中,意外的发现,由于分形噪声的缘故,以及4.1中我们限制了采样步长。在某些角度的边缘居然意外的产生了层次感。然而截图也只是角度限定,由于云层较大较厚,和3d噪声还是差得远的。

1d198f3e47bae0321851c7d95a877f9b.png
太阳下方,由于步长限制,意外的出现了层次感(mi10p 小于1ms)

09b87c6a155dee0e273ae5962912b80c.png
换个角度,惨不忍睹,还不如shaderToy上的2d噪声

不过放在地上作为雾效,由于不需要云层那么厚,垂直方向的形态不需要很复杂,还是相对好很多的。

7fb37aa07f34fcc37491a0188cff8aed.png
充当雾效(mi10p 小于1ms)

或者直接像题图一样用作沙尘暴

5aedd5d312a15da7e53694fda78e33e9.png
题图沙尘暴(mi10p 小于1ms)

a3293b4fd74fade79d9b43d183aa5463.png
华为nova4e 无散射+原生雾+单层体积雾(2ms左右)

参考文章

FMX2017

地平线

unity自定义深度图

AABB盒求交

分形噪声

离屏软粒子

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值