Volumetric Clouds – 体积云的做法

Last modified date

巧妙利用unity提供的Graphics.DrawMesh方法,配合基于uv偏移产生的立体光影效果,制作体积云。

这个Volumetric Cloud的做法简单来说,就是利用Graphics.DrawMesh的方法,将一个平面mesh垂直绘制n次,获得一个类似“千层糕”的立体结构。然后通过shader配合制作出云层的光影和飘动的变化效果。

首先简单介绍一下Unity提供的Graphics.DrawMesh方法:

DrawMesh可以在一帧里把一个Mesh绘制一次。这个Mesh可以接受或产生光影。可以被一个或多个Camera绘制。而且注意他并不是立即执行绘制的,他只是把绘制需求提交给渲染中心去处理,所以它不是实时执行的。这玩意儿适合用来绘制数量庞大,但没有交互需求的对象,例如游戏中无关紧要的远景。
可参考Unity官方文档:https://docs.unity3d.com/ScriptReference/Graphics.DrawMesh.html

然后开始一步一步说具体的做法:

先做一个四方连续的噪点图,噪点颗粒可以大一点,这样配合后面的云层效果会比较好。

然后通过shader做出UV平移效果。而且用2个方向,速度和尺寸都不同的图相乘,获得一个看上去效果很随机的图。

然后通过DrawMesh的方式,根据Y轴垂直绘制n次,获得一个“千层糕”。

计算绘制矩阵的时候注意把中心点设在原点。把绘制次数和高度参数暴露出来方便以后调试。

C#代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using WalkingFat;

namespace WalkingFat {
    public class DrawVolumetricCloud : MonoBehaviour {

        public int horizontalStackSize = 20;
        public float cloudHeight = 1f;
        public Mesh quadMesh;
        public Material cloudMaterial;
        float offset;

        public int layer;
        public Camera camera;
        private Matrix4x4 matrix;
        private Matrix4x4[] matrices;
        public bool castShadows = false;
        public bool useGpuInstancing = false;

        void Update()
        {

            cloudMaterial.SetFloat("_midYValue", transform.position.y);
            cloudMaterial.SetFloat("_cloudHeight", cloudHeight);

            offset = cloudHeight / horizontalStackSize / 2f;
            Vector3 startPosition = transform.position + (Vector3.up * (offset * horizontalStackSize / 2f));

            if (useGpuInstancing) // initialize matrix array
            {
                matrices = new Matrix4x4[horizontalStackSize];
            }

            for (int i = 0; i < horizontalStackSize; i++)
            {
                matrix = Matrix4x4.TRS(startPosition - (Vector3.up * offset * i), transform.rotation, transform.localScale);

                if (useGpuInstancing)
                {
                    matrices[i] = matrix; // build the matrices array if using GPU instancing
                }
                else
                {
                    Graphics.DrawMesh(quadMesh, matrix, cloudMaterial, layer, camera, 0, null, castShadows, false, false); // otherwise just draw it now
                }
            }

            if (useGpuInstancing) // draw the built matrix array
            {
                UnityEngine.Rendering.ShadowCastingMode shadowCasting = UnityEngine.Rendering.ShadowCastingMode.Off;
                if (castShadows)
                    shadowCasting = UnityEngine.Rendering.ShadowCastingMode.On;

                Graphics.DrawMeshInstanced(quadMesh, 0, cloudMaterial, matrices, horizontalStackSize, null, shadowCasting, false, layer, camera);

            }
        }
    }
}

然后继续写shader。通过Clip方法抠掉黑色区域,让云层镂空。

通过Y轴距离计算获得上下两端的衰减值,输入到Clip函数里,让上下两侧面积越来越少。基本上可以看到圆鼓鼓的云了。

关键代码:
// get value to taper top and bottom
float vFalloff = pow(saturate (abs(_midYValue - i.posWorld.y) / (_cloudHeight * 0.25)), _TaperPower);

现在才绘制了20遍,感觉层次感较粗糙。下图是绘制了100次的,看上去更圆润。

然后配上颜色,让上下暗,中间亮,这是为了配合后面制作光影效果。

以上工作就获得了体积感很强的云朵了。但是光影变化只有上下颜色渐变,不够真实。下面要继续做出光影变化。

让UV根据灯光的方向偏移一段距离,然后用初始的图剪掉偏移的图,可以获得两侧的“受光面”和“反光面”。并且这玩意儿还是根据灯光方向实时变化的,也没有用到Normal之类的复杂算法,性能开销很小。

配合上面的颜色渐变,就获得了一个跟随光影变化的体积云。

现在云层已经可以根据主光源的Y轴变化了,下面再通过视点方向和光源方向计算出次表面散射的的效果。

关键代码:
// Subsurface Scatterting
fixed3 sssCol = pow(saturate(dot(i.viewDir, i.lightDir)), _SssPower) * _SssStrength * _LightColor0;

放到场景里,配好颜色,看上去不错。

最后注意躁点贴图最好是压缩过的,他的粗糙颗粒感刚好可以营造出云层坑坑洼洼的起伏效果。

shader代码:
Shader "WalkingFat/VolumetricCloud"
{
    Properties
    {
        _NoiseTex ("Noise Texture", 2D) = "white" {}
        _MainColor ("Main Color", Color) = (1,1,1,1)

        _midYValue ("Mid Y Value", float) = 0.9
        _cloudHeight ("Cloud Height", float) = 2.95
        _TaperPower ("Taper Power", float) = 1.26
        _Cutoff ("Cutoff", range(0,1)) = 0.5

        _CloudSpeed ("Cloud Speed", float) = 0.5
        _NoiseScale1 ("Noise Scale 1", range (0.1, 2.0)) = 0.53
        _NoiseScale2 ("Noise Scale 2", range (0.1, 2.0)) = 1.32
        _CloudSize ("Cloud Size", range (0.01, 3.0)) = 0.2
        _CloudDirX ("Cloud Direction X", float) = 1
        _CloudDirZ ("Cloud Direction Z", float) = 0

        _SssPower ("SSS Power", range(0.01,10)) = 4.5
        _SssStrength ("SSS Strength", range(0.0,2.0)) = 0.22
        _OffsetDistance ("Offset Distance", range (0,1)) = 0.1
        _LitStrength ("Light Strength", range (0.1,20)) = 5
        _BackLitStrength ("BackLight Strength", range (0.1,20)) = 5
        _EdgeLitPower ("Edge Power", range (0.1,10)) = 1
        _EdgeLitStrength ("Edge Light Strength", range (0.1,20)) = 1

    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "LightMode"="ForwardBase"}
        LOD 100

        Pass
        {
            Cull Off

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog
            
            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float4 tangent : TANGENT;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv1 : TEXCOORD0;
                float2 uv2 : TEXCOORD1;
                float4 posWorld : TEXCOORD2;
                float3 lightDir : TEXCOORD3;  
                float3 viewDir  : TEXCOORD4;
                float3 tangentWorld  : TEXCOORD5;
                float2 uvOffset1 : TEXCOORD6;
                float2 uvOffset2 : TEXCOORD7;
                float2 uvOffset3 : TEXCOORD8;
                float2 uvOffset4 : TEXCOORD9;
                UNITY_FOG_COORDS(10)
            };

            sampler2D _NoiseTex;
            float4 _NoiseTex_ST, _MainColor;
            float _midYValue, _cloudHeight, _TaperPower, _Cutoff;
            float _CloudSpeed, _NoiseScale1, _NoiseScale2, _CloudSize, _CloudDirX, _CloudDirZ;
            float _SssPower, _SssStrength, _OffsetDistance, _LitStrength, _BackLitStrength, _EdgeLitPower, _EdgeLitStrength;
            
            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.posWorld = mul(unity_ObjectToWorld, v.vertex);
                o.lightDir = WorldSpaceLightDir( v.vertex );  
                o.viewDir = WorldSpaceViewDir( v.vertex );
                o.tangentWorld = UnityObjectToWorldDir(v.tangent.xyz);

                // uv panner
                float2 uv = TRANSFORM_TEX(v.uv, _NoiseTex);
//                o.uv2 = TRANSFORM_TEX(v.uv, _MainTex);
                float2 uvPanner1 = uv + _CloudSpeed * float2 (_CloudDirX + 0.02, _CloudDirZ + 0.02) * _Time * _NoiseScale1;
                float2 uvPanner2 = uv + _CloudSpeed * float2 (_CloudDirX - 0.02, _CloudDirZ - 0.02) * _Time * _NoiseScale2;
                o.uv1 = uvPanner1 * (_CloudSize + 0.13);
                o.uv2 = uvPanner2 * _CloudSize;

                // offset
                float3 litOffset = o.lightDir * o.tangentWorld * _OffsetDistance;
                o.uvOffset1 = o.uv1 + litOffset;
                o.uvOffset2 = o.uv2 + litOffset;

                o.uvOffset3 = o.uv1 - litOffset;
                o.uvOffset4 = o.uv2 - litOffset;

                UNITY_TRANSFER_FOG(o,o.pos);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {

                // sample the texture
                fixed4 texCol1 = tex2D(_NoiseTex, i.uv1);
                fixed4 texCol2 = tex2D(_NoiseTex, i.uv2);
                fixed4 col = texCol1 * texCol2;

                // get light value
                fixed4 texColOffset1 = tex2D(_NoiseTex, i.uvOffset1);
                fixed4 texColOffset2 = tex2D(_NoiseTex, i.uvOffset2);
                fixed light = saturate (texCol1.r + texCol2.r - texColOffset1.r - texColOffset2.r) * _LitStrength;
                // backlight value
                fixed4 texColOffset3 = tex2D(_NoiseTex, i.uvOffset3);
                fixed4 texColOffset4 = tex2D(_NoiseTex, i.uvOffset4);
                fixed backLight = saturate (texCol1.r + texCol2.r - texColOffset3.r - texColOffset4.r) * _BackLitStrength;

                fixed edgeLight = pow((1 - col.r), _EdgeLitPower) * _EdgeLitStrength;

                fixed finalLit = light + backLight + edgeLight;

                // get value to taper top and bottom
                float vFalloff = pow(saturate (abs(_midYValue - i.posWorld.y) / (_cloudHeight * 0.25)), _TaperPower);

                // Subsurface Scatterting
                fixed3 sssCol = pow(saturate(dot(i.viewDir, i.lightDir)), _SssPower) * _SssStrength * _LightColor0;

                clip(col.r - vFalloff - _Cutoff);

                // get cloud color
                fixed4 finalCol = lerp(_MainColor, _LightColor0, finalLit);

                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, finalCol);

                return finalCol;
            }
            ENDCG
        }
    }
}

Share

This site is protected by wp-copyrightpro.com