水的几种制作方式

Posted by Bob on April 3, 2019

ShaderGraph制作UV动画水

需要配置Lightweight Render Pipeline,使用ShaderGraph制作

水效果: image

属性: image

节点图如下: image

节点函数用途:

    // Shared Graph Node Functions
 
    void Unity_FresnelEffect_float(float3 Normal, float3 ViewDir, float Power, out float Out)
    {
        Out = pow((1.0 - saturate(dot(normalize(Normal), normalize(ViewDir)))), Power);
    }
 
    void Unity_Power_float(float A, float B, out float Out)
    {
        Out = pow(A, B);
    }
 
    void Unity_Lerp_float4(float4 A, float4 B, float4 T, out float4 Out)
    {
        Out = lerp(A, B, T);
    }
 
    void Unity_Multiply_float (float A, float B, out float Out)
    {
        Out = A * B;
    }
 
    void Unity_Multiply_float (float2 A, float2 B, out float2 Out)
    {
        Out = A * B;
    }
 
    void Unity_TilingAndOffset_float(float2 UV, float2 Tiling, float2 Offset, out float2 Out)
    {
        Out = UV * Tiling + Offset;
    }
 
    void Unity_NormalBlend_float(float3 A, float3 B, out float3 Out)
    {
        Out = normalize(float3(A.rg + B.rg, A.b * B.b));
    }

基于Depth水

深度图里存放了[0,1]范围的非线性分布的深度值,这些深度值来自NDC坐标。 在延迟渲染中,深度值默认已经渲染到G-buffer;而在前向渲染中,你需要去申请。

设置Camera.main.depthTextureMode = DepthTextureMode.Depth,表明了主摄像机渲染了深度图。在shader中通过_CameraDepthTexture获得深度值。

深度纹理中的深度值是d,Zndc对应了NDC坐标中的z分量的值(在摄像机推导中一文中推导),由下列公式计算可得:

image

由此可知LinearEyeDepth计算结果和zeye一致,它的取值范围就是视锥体深度范围,即[Near,Far]。


//LinearEyeDepth 负责把深度纹理的采样结果转换到视角空间下的深度值
// Z buffer to linear depth

inline float LinearEyeDepth( float z )
{

	return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w);

}

其中_ZBufferParams的定义如下:
double zc0, zc1;
// OpenGL would be this:
// zc0 = (1.0 - m_FarClip / m_NearClip) / 2.0;
// zc1 = (1.0 + m_FarClip / m_NearClip) / 2.0;
// D3D is this:
zc0 = 1.0 - m_FarClip / m_NearClip;// m_FarClip远裁剪平面
zc1 = m_FarClip / m_NearClip;
// now set _ZBufferParams with (zc0, zc1, zc0/m_FarClip, zc1/m_FarClip);

如果我们想要得到范围在[0, 1]之间的深度值,只需要把上面得到的结果除以Far即可。这样,0就表示该点与摄像机位于同一位置,1表示该点位于视锥体的远裁剪平面上。结果如下:

image

Linear01Depth 则会返回一个范围在[0, 1]的线性深度值,也就是我们上面得到的Z01。


// Z buffer to linear 0..1 depth (0 at eye, 1 at far plane)

inline float Linear01Depth( float z )
{
return 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y);
}

COMPUTE_EYEDEPTH(i):计算顶点在眼空间的深度(eye space depth),并输出到o中。当不渲染到深度纹理时,在顶点程序中使用它。

水效果: image

属性: image


Shader "Map/Sea" {
	Properties {
		//水

		_WaterAlphaTex ("WaterAlphaTex", 2D) = "black" {} 
		_WaterNormalTex ("WaterNormalTex", 2D) = "bump" {} 
		_WaterGradient ("WaterGradient", 2D) = "white" {} 
		_WaterNosieTex ("WaterNoise", 2D) = "white" {} 
		_WaterSpeed ("WaterSpeed", float) = 0.74 
		_Refract("Refract", float) = 0.07
		_Specular("Specular", float) = 1.86
		_Gloss("Gloss", float) = 0.71
		_SpecColor("SpecColor", color) = (1, 1, 1, 1)
		_CoastRange("CoastRange", vector) = (0.13, 1.53, 0.37, 0.78)

		//浪花

		_FoamTex("FoamTex", 2D) = "black" {}
		_FoamSpeed ("FoamSpeed", float) = -12.64 
		_FoamRange ("FoamRange", float) = 0.3 
		_FoamDelta("FoamDelta", float) = 2.43
		_FoamNoiseRange ("FoamNoiseRange", float) = 6.43
		
	}
	CGINCLUDE 
	fixed4 LightingSeaLight(SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten) {
		half3 halfVector = normalize(lightDir + viewDir);
		float diffFactor = max(0, dot(lightDir, s.Normal)) * 0.8 + 0.2;
		float nh = max(0, dot(halfVector, s.Normal));
		float spec = pow(nh, s.Specular * 128.0) * s.Gloss;
		fixed4 c;
		c.rgb = (s.Albedo * _LightColor0.rgb * diffFactor + _SpecColor.rgb * spec * _LightColor0.rgb) * (atten);
		c.a = s.Alpha + spec * _SpecColor.a;
		return c;
	}
	ENDCG
	SubShader {
		Tags { "RenderType"="Transparent" "Queue"="Transparent"}
		LOD 200

		GrabPass{}
		zwrite off
		
		CGPROGRAM
		#pragma surface surf SeaLight vertex:vert alpha noshadow
		#pragma target 3.0

		sampler2D _WaterAlphaTex;
		float4 _WaterAlphaTex_TexelSize;

		sampler2D _WaterGradient;
		sampler2D _WaterNormalTex;
		sampler2D _WaterNosieTex;
		sampler2D _FoamTex;

		sampler2D _CameraDepthTexture;
		sampler2D _GrabTexture;
		half4 _GrabTexture_TexelSize;
		
		float4 _CoastRange;
		half _WaterSpeed;
		fixed _Refract;
		half _Specular;
		fixed _Gloss;

		half _FoamNoiseRange;
		half _FoamSpeed;
		fixed _FoamDelta;
		half _FoamRange;
		

		struct Input {
			float2 uv_WaterAlphaTex;
			float2 uv_WaterNosieTex;
			float4 proj;
			float3 viewDir;
		};

		void vert (inout appdata_full v, out Input i) {
			UNITY_INITIALIZE_OUTPUT(Input, i);
			i.proj = ComputeScreenPos(UnityObjectToClipPos(v.vertex));
			COMPUTE_EYEDEPTH(i.proj.z);
		}

		fixed4 uvMove(sampler2D tex,float2 uv,float offset){
			fixed4 colorX = tex2D(tex, uv + offset + float2(_WaterSpeed*_Time.x,0));
			fixed4 colorXY = tex2D(tex, float2(1-uv.y,uv.x)  + offset + float2(_WaterSpeed*_Time.x,0));
			fixed4 color = (colorX+colorXY)/2;
			return color;
		}

		float coastDepth(float rang,float depth){
			return min(rang, depth)/rang;
		}

		fixed4 coastWaveColor(fixed delta,half deltaDepth,float offset,fixed4 noiseColor){
			fixed4 waveColor = tex2D(_FoamTex, float2(1-coastDepth(_CoastRange.z, deltaDepth)+_FoamRange*sin(_Time.x*_FoamSpeed+delta+noiseColor.r*_FoamNoiseRange),1)+offset);
			waveColor.rgb *= (1-(sin(_Time.x*_FoamSpeed+delta+noiseColor.r*_FoamNoiseRange)+1)/2)*noiseColor.r;
			return waveColor;
		}

		void surf (Input IN, inout SurfaceOutput o) {
			float2 uv = IN.proj.xy/IN.proj.w;
			#if UNITY_UV_STARTS_AT_TOP
			
			if(_WaterAlphaTex_TexelSize.y<0)
			uv.y = 1 - uv.y;
			
			#endif

			fixed4 waterAlpha = uvMove(_WaterAlphaTex,IN.uv_WaterAlphaTex,half2(0,0));

			float4 offsetColor = uvMove(_WaterNormalTex,IN.uv_WaterAlphaTex,half2(0,0));
			
			half2 offset = UnpackNormal(offsetColor).xy * _Refract;
			half deltaDepth = LinearEyeDepth(tex2Dproj (_CameraDepthTexture, IN.proj).r) - IN.proj.z;

			fixed4 wateNoiseColor = tex2D(_WaterNosieTex, IN.uv_WaterNosieTex);

			half4 seafloor = tex2D(_GrabTexture, uv+offset);
			fixed4 waterColor = tex2D(_WaterGradient, float2(coastDepth(_CoastRange.y, deltaDepth),1));
			
			fixed4 waveColorA = coastWaveColor(0,deltaDepth,offset,wateNoiseColor);
			fixed4 waveColorB = coastWaveColor(_FoamDelta,deltaDepth,offset,wateNoiseColor);
			
			half water_A = 1-coastDepth(_CoastRange.z, deltaDepth);
			half water_B = coastDepth(_CoastRange.w, deltaDepth);

			float4 normalColor = uvMove(_WaterNormalTex,IN.uv_WaterAlphaTex,offset);

			o.Normal = UnpackNormal(normalColor).xyz;
			
			o.Specular = _Specular;
			o.Gloss = _Gloss;
			o.Albedo = seafloor.rgb * (1 - water_B) + waterColor.rgb * water_B;
			o.Albedo = o.Albedo * (1 - waterAlpha.a*water_A) + waterAlpha.rgb * waterAlpha.a*water_A;
			o.Albedo += (waveColorA.rgb+waveColorB.rgb) * water_A; 

			//o.Albedo = water;
			
			o.Alpha = coastDepth(_CoastRange.x, deltaDepth);
		}
		ENDCG
	} 
	//FallBack "Diffuse"
}


基于Vertex Color水

波浪运动原理参见王者荣耀地图资源(随风飘摇的小草) 文中的正弦曲线公式。

切线空间 image

世界空间中worldPos(x,y,z),切线空间中pos’(tangent,binormal,normal),其中tangent、binormal、normal 相互垂直。tangent = (tx,ty,tz),binormal = (bx,by,bz),normal = (nx,ny,nz)。

image

世界空间变化到切线空间为变换矩阵M,切线空间变化到世界空间为变换矩阵MT

水效果: image

属性: image


Shader "Map/Sea"
{
	Properties
	{
		_FoamTex("FoamTex(R:waveFoam,G:coastFoam,B:waveDisturbance)", 2D) = "white" {}
		[Normal]_NormalTex("NormalTex", 2D) = "bump" {}
		_WaveMask ("WaveMask", 2D) = "white" {}
		_WaveTex("WaveTex", 2D) = "white" {}
		_Gradient("WaterGradient", 2D) = "white" {}
		_Sky("Sky", cube) = "" {}

		[Space]
		_WaveParams ("WaveParams(x:waveRange,y:waveOffset,z:waveDisturbance,w:foamDisturbance)", vector) = (0,0,0,0)
		_FoamParams("CoastFoamParams(x:fadeIn,y:fadeOut,z:width,w:alpha)", vector) = (0,0,0,0)
		_Speed("Speed(x:windSpeed,y:waveSpeed)", vector) = (0,0,0,0)

		[Space]
		_NormalScale ("NormalScale", range(0, 1)) = 1
		_Fresnel("Fresnel", float) = 0
		
		_Specular("Specular", float) = 0
		_Gloss("Gloss", float) = 0
		_FoamColor ("FoamColor", color) = (1,1,1,1)
		_SpecColor ("SpecColor", color) = (0.4,0.4,0.4,1)
		_LightDir("LightDir", vector) = (0, 0, 0, 0)
	}
	SubShader
	{
		Tags { "RenderType" = "Transparent" "Queue" = "Transparent" "IgnoreProjector" = "true" }
		LOD 100

		Pass
		{
			blend srcalpha oneminussrcalpha
			zwrite off
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma multi_compile_fog
			#pragma target 3.0
			
			#include "UnityCG.cginc"

			struct v2f
			{
				float2 uv_FoamTex : TEXCOORD0;
				float2 uv_NormalTex : TEXCOORD1;
				UNITY_FOG_COORDS(2)
				float4 TW0:TEXCOORD3;
				float4 TW1:TEXCOORD4;
				float4 TW2:TEXCOORD5;
				float4 vertex : SV_POSITION;
				float4 color : COLOR;
			};

			sampler2D _FoamTex;
			float4 _FoamTex_ST;

			sampler2D _WaveTex;
			sampler2D _WaveMask;

			half4 _Speed;
			
			fixed4 _WaveParams;

			half _NormalScale;

			half4 _FoamParams;

			sampler2D _Gradient;
			sampler2D _NormalTex;
			float4 _NormalTex_ST;

			half _Fresnel;

			samplerCUBE _Sky;

			half _Specular;
			fixed _Gloss;

			half4 _LightDir;
			half4 _SpecColor;

			fixed4 _FoamColor;
			
			//下面函数修改自Unity内置Shader "Unlit/SkyReflection Per Pixel"
			v2f vert (appdata_full v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);

				o.uv_FoamTex = TRANSFORM_TEX(v.texcoord, _FoamTex);
				o.uv_NormalTex = TRANSFORM_TEX(v.texcoord, _NormalTex);
				//o.uv_WaveTex = TRANSFORM_TEX(v.texcoord, _WaveTex);

				UNITY_TRANSFER_FOG(o,o.vertex);

				/*
				法线贴图中的法线向量在切线空间中:
				http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-13-normal-mapping/
				在点p处存在着一个切线空间,在此空间中,p的法线作为z轴,p的tangent作为x轴,p的binormal由normal和tangent叉乘而得,
				作为y轴。tangent由点所在的三角形中的uv方向确定(三角形中的uv值)
				*/

				//这里是计算顶点的世界坐标

				float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

				//得到顶点法线转换到世界空间的法线,得到切线空间的N

				fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);

				//得到顶点的切线转换到世界空间的切线,得到切线空间的T

				fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
				
				//计算方向(副切线的正负值)

				fixed tangentSign = v.tangent.w * unity_WorldTransformParams.w;

				//通过T和N的向量积,得到垂直这两个向量的向量,但是它的方向有两个,所以乘以上面得到的方向参数,得到最终的向量,得到切线空间的B

				fixed3 worldBinormal = cross(worldNormal, worldTangent) * tangentSign;

				//tangent space matrix:切线空间转世界空间

				o.TW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
				o.TW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
				o.TW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);

				o.color = v.color;

				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				//采样法线贴图

				fixed4 normalCol = (tex2D(_NormalTex, i.uv_NormalTex + fixed2(_Time.x*_Speed.x, 0)) + tex2D(_NormalTex, fixed2(_Time.x*_Speed.x + i.uv_NormalTex.y, i.uv_NormalTex.x))) / 2;
			
				half3 worldNormal = UnpackNormal(normalCol);

				//泡沫使用法线贴图的rg进行扰动

				half3 foam = tex2D(_FoamTex, i.uv_FoamTex + worldNormal.xy*_WaveParams.w).rgb;
				
				worldNormal = lerp(half3(0, 0, 1), worldNormal, _NormalScale);

				/*  
					transform normal from tangent to world space:
					通过向量计算,得到世界法线的方向,这里Surf方法里 worldNormal =  UnpackNormal(tex2D(_NormalTex, i.uv_NormalTex));
  					得到的是该顶点的切空间的法线方向,实际上,下面这个方法也等于
  					worldNormal = normalize(mul( float3x3(i.TW0.xyz, i.TW1.xyz.xyz, i.TW2.xyz.xyz),worldNormal));  
				*/

				worldNormal = normalize(fixed3(dot(i.TW0.xyz, worldNormal), dot(i.TW1.xyz, worldNormal), dot(i.TW2.xyz, worldNormal)));

				//根据顶点颜色r通道采样海水渐变

				//color.r:深度 color.g 浪花透明  color.b 海浪波纹透明 color.a 整体透明

				fixed4 col = tex2D(_Gradient, float2(i.color.r, 0.5));
				
				//采样反射天空盒

				half3 worldPos = half3(i.TW0.w, i.TW1.w, i.TW2.w);

				half3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
				half3 refl = reflect(-viewDir, worldNormal);

				half vdn = saturate(pow(dot(viewDir, worldNormal), _Fresnel));

				col.rgb = lerp(texCUBE(_Sky, refl), col.rgb, vdn);


				//计算海浪和岸边泡沫

				//顶点色海岸深度+海浪偏移+海浪范围*sin(时间*海浪速度 + 海浪扰动调整系数*海浪扰动)
				
				//正弦曲线公式参见:https://chenanbao.github.io/2018/10/25/%E9%9A%8F%E9%A3%8E%E9%A3%98%E6%91%87%E7%9A%84%E5%B0%8F%E8%8D%89/

				fixed wave1 = tex2D(_WaveTex, float2(i.color.r + _WaveParams.y + _WaveParams.x*sin(_Time.x*_Speed.y + _WaveParams.z*foam.b), 0)).r;
				fixed wave2 = tex2D(_WaveTex, float2(i.color.r + _WaveParams.y + _WaveParams.x*cos(_Time.x*_Speed.y + _WaveParams.z*foam.b), 0)).r;
				
				//基于顶点色海岸深度计算水深度透明度

				fixed waveAlpha = tex2D(_WaveMask, float2(i.color.r, 0)).r;

				//1 - (淡入-顶点色海岸深度)/淡入

				fixed sfadein = 1 - saturate((_FoamParams.x - i.color.r) / _FoamParams.x);

				//1 - (顶点色海岸深度-淡出)/宽度

				fixed sfadeout = 1 - saturate((i.color.r - _FoamParams.y) / _FoamParams.z);

				//(岸边泡沫颜色-海水渐变颜色)*(浪一+浪二)*水深度透明度*海浪泡沫法线*海浪波纹透明度

				col+= (_FoamColor - col)* (wave1 + wave2)*waveAlpha*foam.r*i.color.b;

				//(岸边泡沫颜色-海水渐变颜色)*淡入结果*淡出结果*海岸浪花透明度*浪花透明度

				col+= (_FoamColor - col)* sfadein*sfadeout *_FoamParams.w*foam.g*i.color.g;

				//计算高光

				half3 h = normalize(viewDir - normalize(_LightDir.xyz));
				fixed ndh = max(0, dot(worldNormal, h));

				col += _Gloss*pow(ndh, _Specular*128.0)*_SpecColor;
				
				// apply fog
				UNITY_APPLY_FOG(i.fogCoord, col);

				//应用顶点透明度

				col.a *= i.color.a;
				//col = i.color;
				return col;
			}
			ENDCG
		}
	}
}


参考资料:

depth

depth Project

normal-mapping tutorials

normal-mapping

normal maps

NormalMapWorldSpace

VertexPaint

CameraDepthTexture