Volumetric Clouds – 体积云的做法
巧妙利用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
}
}
}