美文网首页Unity商店资源推广
TOD的云层渲染和Flare遮挡

TOD的云层渲染和Flare遮挡

作者: 恶毒的狗 | 来源:发表于2020-04-17 10:37 被阅读0次

    关于TOD

    前项目在模仿塞尔达的昼夜变换效果时用过 Time Of Day 这个Unity插件。

    本文记录一下 TOD 的云层渲染方式,以及如何让 TOD 的云层遮挡太阳光的 Flare

    TOD渲染顺序

    image

    以上图的白天为例,TOD的主要渲染顺序如下:

    1. 绘制太阳 (Background+30)
    2. 绘制大气层 (Background+50)
    3. 绘制云层 (Geometry+530)

    其中,太阳和大气层都是实体渲染,按照实体渲染从近往远的优化策略,这里 RenderQueue 放在 Background 似乎不太合理,我们游戏中会把他们的渲染顺序调整到 Geometry 的后面。

    关于太阳和大气层的渲染,特别是大气层的渲染,内容还不少,日后慢慢写,本文主要是关于云层渲染的。

    TOD的云层渲染

    大气层的模型是一个球面,而云层的模型是一个半球面,因为云层不会出现在地平线之下:

    image

    云层渲染的基本原理比较简单,就是根据一张密度图计算云层的形状,贴在这个半球面上,然后和大气层做Alpha混合。

    作者在这个插件的主页说这个云层是 Semi-volumetric 的,从代码上看,这里的半体积其实就是在 yz 两个方向计算云的密度,这样计算很快,虽然不是真正的 体积云,但是效果不错,移动设备也能跑得动。

    云层的密度图

    云层的形状主要根据作者提供的一张密度图来计算,密度图的4个通道分别是不同频率的噪声图,如下:

    image

    在介绍密度计算之前,我们首先需要处理好半球面上的点到密度图的UV映射问题。

    云层的UV计算

    根据 TOD 的实现机制,摄像机刚好位于球面的中心,我们可以用模型空间下 归一化viewDir 来映射密度图的纹理坐标。

    这里 viewDir 计算方式如下:

    o.viewDir  = normalize(v.vertex.xyz);
    

    考虑下面这种最简单的映射方式:用 viewDir 的水平面 XZ坐标 直接映射纹理的 UV

    image

    可以看到,云层越接近水平线,拉伸的就越厉害,因为均匀变化的水平坐标XZ对应的球面跨度变化是不均匀的。

    要得到正确的结果,我们可以参考 这篇文章 来做球面点到纹理二维坐标的转换,不过作者用了一个计算量更小的方式:

    inline float3 CloudPosition(float3 viewDir, float3 offset)
    {
        float mult = 1.0 / lerp(0.1, 1.0, viewDir.y);
        return (float3(viewDir.x * mult + offset.x, 0, viewDir.z * mult + offset.z)) / TOD_CloudSize;
    }
    

    这里就是把 viewDir.y 考虑进去,这个值越小越接近水平线,XZ 平面上的跨度就越大,效果如下:

    image

    云层的密度计算

    正确计算UV后,我们就可以在像素着色器计算密度了。

    这里先不考虑N层噪声的叠加,我们看一下一层噪声的代码:

    inline half3 CloudLayerDensity(sampler2D densityTex, float4 uv, float3 viewDir)
    {
        half3 density = 0;
        half4 n = tex2D(densityTex, uv.xy);
    
        // Density when marching in up direction
        density.y += n.r;
    
        // Density when marching in view direction
        density.z += n.r;
    
        // Coverage
        density.yz = (density.yz - TOD_CloudCoverage) * half2(TOD_CloudAttenuation, TOD_CloudDensity);
    
        // Opacity
        density.x = saturate(density.z);
    
        return density;
    }
    

    这里 yz 两个方向的密度都取密度图的R通道,TOD_CloudCoverage 用于裁剪云朵的范围,TOD_CloudDensity 可以控制云朵边缘的透明程度,TOD_CloudAttenuation 可以控制云朵的颜色衰减。

    云层的颜色计算

    得到密度后,云层的颜色计算代码如下:

    inline half4 CloudLayerColor(sampler2D densityTex, float4 uv, float4 color, float3 viewDir, float3 lightDir, float3 lightCol)
    {
        half3 density = CloudLayerDensity(densityTex, uv, viewDir);
    
        half4 res = 0;
        res.a = density.x;
        res.rgb = 1.0 - density.y;
    
        res *= color;
    
        return res;
    }
    

    代码很简单,不啰嗦。

    云层的渲染分级

    上面的代码只是云层最基础的计算,TOD 针对云层的渲染质量分了三级:

    • TOD_CloudQualityType.Low
      • 一层噪声
    • TOD_CloudQualityType.Medium
      • 四层噪声叠加
    • TOD_CloudQualityType.High
      • 四层噪声叠加
      • 米氏散射叠加

    这里就不贴代码了,有兴趣的话可以去支持一下作者。

    Flare遮挡

    下面回到项目中实际遇到的问题,如下图所示,太阳其实已经被云层遮挡了,但是 Flare 还是显示了出来:

    image

    要解决这个问题其实也简单,首先我们需要定义自己的 LensFlare 的渲染,如下图:

    image

    然后,在像素着色器部分,我们需要计算出太阳所在位置的云密度,进而计算出 影衰减 并应用到 Flare 上,主要代码如下:

    half4 frag (v2f i) : SV_Target
    {
        half4 texColor = tex2D(_FlareTexture, i.uv);
    
        float3 skyPos = normalize(mul((float3x3)TOD_World2Sky, TOD_SunWorldPos));
        float4 cloudUV = CloudUV(skyPos);
        float cloudShadowAtten = TOD_CloudOpacity * CloudShadow(TOD_CloudTexture, cloudUV);
        cloudShadowAtten = 1 - cloudShadowAtten;
    
        texColor.rgb = texColor.rgb * cloudShadowAtten;
        return texColor * i.color;
    }
    

    最后放一张动图:

    image

    个人主页

    本文的个人主页链接:https://baddogzz.github.io/2020/04/16/Tod-Cloud/

    好了,拜拜!

    相关文章

      网友评论

        本文标题:TOD的云层渲染和Flare遮挡

        本文链接:https://www.haomeiwen.com/subject/boqevhtx.html