高度雾或者垂直雾是体积雾的一重要分支,比如以下场景(真实图片,非特效),在很大场景中都有这样的需求。以下是找到的两个不错的参考实现。
其中有这篇文章的介绍了一种垂直雾的实现方法,思路、代码写的很详细了,网上有不少转载,这里也就不再说明了。
https://mp.weixin.qq.com/s/yfGNeyip5ebVIvBj-XBd4Q
http://www.sohu.com/a/231966965_667928
另外,还有就是以下这个牛B的插件
https://assetstore.unity.com/packages/vfx/shaders/fullscreen-camera-effects/volumetric-fog-mist-49858
下面是插件的其中一个设置可以设置雾在一个区域内。
通过这张图片引出我的需求,需要实现上面类似的区域垂直雾的效果,而且是可以支持多个区域。当时看到这个插件很开心,本想就是自己找的东西,期待的效果也是一样一样的。可看小半天源码,只能设置一个区域,不支持多个区域,一下子心凉了半截。
最终,无奈之下,只能又开始自己造轮子。思路如下:第一步实现第一参考中垂直雾的效果,再完成噪声和体积的处理。
第一个参考中使用的是屏幕后处理,由于不好处理区域,所以还是需要把方法改造为GrabPass的方法,通过一个Box定义一个垂直雾的区域,其Shader为ZWrite off,然后获取其_CameraDepthTexture,从而得到其对应点的世界坐标高度,(这里需要特别注意,这个世界坐标不是Box片段的,而是其后面GrabPass中获取的背景点坐标)。以下Shader的代码。
其中与参考1主要是获取屏幕射线的方法不同,参考1的核心方法(第二简化算法),一个特定的ViewPort Position和一个特定的深度值, 是能够唯一确定一个世界坐标的, 这就要求出Camera到屏幕上一点(对应的世界坐标)的一条射线,可以先求出透视矩阵中四个角的射线,再通过插件获取每一点的射线(即Camera到远投影面的一点的线段)。以上是在屏幕后Shader进行处理的。如下图的四个角所示。(为了更好理解本例,还请先看明白第一个参考)
所以以上方法,直接拿到本例的区域模型的Shader中是不行的,但核心当然也是要求出Camera到屏幕一点的射线,如下图所示
就是要求出Camera到P'点的射线,P点为区域模型上一点,可以求出其Z深度值,Z'为Camera的远投影面深度,就Shader的_ProjectionParams.z,Camera到P点的距离很好求出,这个就可以求出Camera到P'点的距离。可以参考以下的代码。其余的过程与参考一基本一致了。
Shader "Shader Forge/HeightFogBase1" {
Properties {
_MainTexture ("MainTexture", 2D) = "white" {}
_FogColor ("FogColor", Color) = (0.5,0.5,0.5,1)
_Start ("Start", Float ) = 0
_Density ("Density", Float ) = 0
}
SubShader {
Tags {
"IgnoreProjector"="True"
"Queue"="Transparent+1"
"RenderType"="Transparent"
}
GrabPass{ }
Pass {
Name "FORWARD"
Tags {
"LightMode"="ForwardBase"
}
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#define UNITY_PASS_FORWARDBASE
#include "UnityCG.cginc"
#pragma multi_compile_fwdbase_fullshadows
#pragma only_renderers d3d9 d3d11 glcore gles
#pragma target 3.0
uniform sampler2D _CameraDepthTexture;
uniform sampler2D _GrabTexture;
uniform float3 _CameraDir;
uniform float4x4 frustumCorners;
//第一个参考方案的获取屏幕射线的方法
float4 GetCameraRay( float4x4 frustumCorners , float2 vertex1 ){
int xx = (int)vertex1.x;
int yy = (int)vertex1.y;
int z = abs(3 - xx - 3*yy);
return frustumCorners[z];
}
uniform sampler2D _MainTexture; uniform float4 _MainTexture_ST;
uniform float _Start;
uniform float _Density;
uniform float4 _FogColor;
struct VertexInput {
float4 vertex : POSITION;
float2 texcoord0 : TEXCOORD0;
};
struct VertexOutput {
float4 pos : SV_POSITION;
float2 uv0 : TEXCOORD0;
float4 projPos : TEXCOORD1;
//float4 raydir: TEXCOORD2;
float4 posWorld : TEXCOORD3;
};
VertexOutput vert (VertexInput v) {
VertexOutput o = (VertexOutput)0;
o.uv0 = v.texcoord0;
o.pos = UnityObjectToClipPos( v.vertex );
o.projPos = ComputeScreenPos (o.pos);
o.posWorld = mul(unity_ObjectToWorld, v.vertex);
float3 viewDirection = normalize( o.posWorld.xyz - _WorldSpaceCameraPos.xyz);
//这里之前的插件的方式是不对的
// o.raydir.xyz = viewDirection * _ProjectionParams.z * distance(_WorldSpaceCameraPos.xyz, o.posWorld.xyz) / o.projPos.z;
// o.raydir.w = v.vertex.z;
COMPUTE_EYEDEPTH(o.projPos.z);
return o;
}
float4 frag(VertexOutput i) : COLOR {
//获取背景点的深度
float2 sceneUVs = (i.projPos.xy / i.projPos.w);
float depth = Linear01Depth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sceneUVs));
float3 viewDirection = normalize( i.posWorld.xyz - _WorldSpaceCameraPos.xyz);
//获取当前区域模型片段点的屏幕射线
float3 raydir = viewDirection * _ProjectionParams.z * distance(_WorldSpaceCameraPos.xyz, i.posWorld.xyz) / i.projPos.z;
//获取背景点的世界坐标
float3 worldPos = (depth * raydir).xyz + _WorldSpaceCameraPos;
float4 sceneColor = tex2D(_GrabTexture, sceneUVs);
float3 emissive = lerp(_FogColor.rgb, sceneColor.rgb, saturate(exp(worldPos.y -_Start)*_Density));
float3 finalColor = emissive;
//return fixed4(float3(1, worldPos.y, 0).rgb,1); //用于Debug显示高度值
return fixed4(finalColor.rgb,1);
}
ENDCG
}
}
FallBack "Diffuse"
CustomEditor "ShaderForgeMaterialInspector"
}
初步效果图如下:
但以上的效果在一些情况,比如四周都是封闭的还是不错的,但如图所示,其中一边是开放的,从侧面看时就不是很自然了,看不出体的感觉了。这个将在后续现实中再分享出来。
后续体积的实现参考链接