4

Unity法线水,顺便利用CommandBuffer实现廉价的深度和截屏

 3 years ago
source link: https://zhuanlan.zhihu.com/p/81672833
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Unity法线水,顺便利用CommandBuffer实现廉价的深度和截屏

最近其实做了好多东西,但是实在是忙啊

没有时间归纳和总结,先把最近做的这个东西拿出来和大家分享

后续逐步把所会的东西一点点分享出来

先放一个效果出来:

法线水最终效果

法线水其实和顶点偏移+曲面细分的波浪水本质是一样的

只是波浪的呈现方式不同,我们可以通过学习法线水的制作方式掌握大致的架构,

然后慢慢升级更多不同的效果

最后,请大家不要只收藏不点赞...

完整项目的Git链接:

我先把各个要做的功能列出来:

反光波浪 - 法线水顾名思义是用法线做的反光

岸边混合 - 就是根据深度利用地面颜色和水体产生混合,这样会有柔和的边缘混合,所以这里需要用到深度和截屏,

我们这里用Command buffer实现从相机中直接抽取颜色和深度缓冲

岸边波浪 - 这里也是根据深度,叠加一个或者多个波形,产生不断有海水拍打岸边的效果

泡沫 - 但是在某些情况下,是没有波浪的,比如内陆河,那么可能岸边带一点泡沫就够了,所以我们也要支持泡沫

水下折射 - 这个就要利用UV扭曲截屏的图像

基于Command buffer,从相机内抽取深度和颜色 - 大思路是绑定两个Command Buffer到相机上,

设置为全局纹理,是改自一个大哥写的文章,最后我会放出全部代码, 这个方法有一点小问题,

就是在物体比较少的时候,他本身的消耗就比直接取深度大,

因为unity经典管线深度图的获取成本实际上和场景的复杂度有直接关系, 所以也要酌情使用

一些小Trick - 中间会讲一些小Trick,

例如3张跨度比较大的纹理,利用UV缩放让不同距离的过渡更平滑,

一张纹理旋转UV多次采样当多张纹理使用(泡沫),利用插值实现布尔类型的开关

还有一些关于shader格式化的例子

还有哪些可以做的? -

我们这里可以重点说下波浪

法线水其实已经具备了所有的功能

如果我们想要效果更好的水,只需要在这个基础上修改即可,实际上我后面也试了下顶点偏移+曲面细分水,

之后有空在写一篇专门讲高级波浪的文章

顶点偏移+曲面细分水可以尝试用 Gwave波 或者FFT 实现

你只需要去掉法线的部分,改成顶点偏移就好了

而手机上,你根据深度加入一点点顶点偏移,不要曲面细分也是完全的可以的

这样可以做类似"权利游戏"海边波浪起伏的效果

碧蓝航线里那种海面的起伏也差不多可以这样做

而更高级的做法,我觉得最好的是Wave particles, 可以看这篇PPT:

http://advances.realtimerendering.com/s2016/Rendering%20rapids%20in%20Uncharted%204.pptx​advances.realtimerendering.com

法线水嘛,波浪肯定是法线做的,

之后着色因为仅仅是个平面, 所以可以直接用高光+水体颜色

高光可以用Phong实现, 你当然可以用你喜欢的方式实现

但是这里有2个小Trick

首先是缩放,我们用水体的时候,经常会缩放,但是我们并不喜欢水体上的纹理因为缩放拉伸

这个同样作用于制作各种全屏效果,例如迷雾

那我们就可以用这个代码乘以UV来保持缩放比例:

//通过世界到模型空间转换矩阵,拿到基向量的变化,进而得知缩放信息
//假如平面被缩放,我们要保证波浪不被跟着拉变形,所以需要这个信息
float3 recipObjScale = float3(length(unity_WorldToObject[0].xyz), length(unity_WorldToObject[1].xyz), length(unity_WorldToObject[2].xyz));
float3 objScale = 1.0 / recipObjScale;

下面我们先看一张法线图的效果:

左边是着色的, 右边是直接输出法线图的效果,我们想要的表面凹凸已经有了,

但是这样也太直白单调了

所以我们增加2张法线图

得到的结果是这样的:

肯定比上面要好看啦

但是单纯的法线叠加,有个问题,就是在缩放之后,会出现大量的重复感,

为什么会这样呢?因为我们吧相机拉远之后,tiling的大小并没有变换,

所以我们可以使用插值,根据摄像机距离水面的距离,来缩放uv

结果就可以在远景保持这样的效果:

贴一个完整的代码让大家可以在引擎内看效果,

我加上了详细的注释

之后就不在贴出完整代码了, 完整的代码放到文末

话说知乎的代码编辑器真不好用啊....

法线波浪完整的代码如下:

Shader "Custom/FX/FX-TestNormalWave"
{
    Properties
    {
		[Header(Color)]
		_WaterColor("Water Color 浅水区颜色", Color) = (0,0.3411765,0.6235294,1)
		_DeepWaterColor("Deep Water Color 深水区颜色", Color) = (0,0.3411765,0.6235294,1)
		_DepthTransparency("Depth Transparency 水体透明度", Range(0.5,10)) = 1.5
		_Fade("Fade 颜色混合后乘数,控制颜色强度", Range(0.1,5)) = 1

		[Space(50)]
		[Header(Light)]
		_Specular("Specular 高光强度", Range(0, 10)) = 1
		_LightWrapping("Light Wrapping", Float) = 0
		_Gloss("Gloss 高光范围", Range(0, 1)) = 0.55

		[Space(50)]
		[Header(Small Waves)]
		[NoScaleOffset] _SmallWavesTexture("Small Waves Texture", 2D) = "bump" {}
		_SmallWavesTiling("Small Waves Tiling", Float) = 1.5
		_SmallWavesSpeed("Small Waves Speed", Float) = 60
		_SmallWaveRrefraction("Small Wave Rrefraction", Range(0, 3)) = 1

		[Space(50)]
		[Header(Medium Waves)]
		[NoScaleOffset]_MediumWavesTexture("Medium Waves Texture", 2D) = "bump" {}
		_MediumWavesTiling("Medium Waves Tiling", Float) = 3
		_MediumWavesSpeed("Medium Waves Speed", Float) = -80
		_MediumWaveRefraction("Medium Wave Refraction", Range(0, 3)) = 2

		[Space(50)]
		[Header(Large Waves)]
		[NoScaleOffset]_LargeWavesTexture("Large Waves Texture", 2D) = "bump" {}
		_LargeWavesTiling("Large Waves Tiling", Float) = 0.5
		_LargeWavesSpeed("Large Waves Speed", Float) = 60
		_LargeWaveRefraction("Large Wave Refraction", Range(0, 3)) = 2.5

		[Space(50)]
		[Header(TilingDistance)]
		_MediumTilingDistance("Medium Tiling Distance", Float) = 200
		_LongTilingDistance("Long Tiling Distance", Float) = 500
		_DistanceTilingFade("Distance Tiling Fade", Float) = 1

    }
    SubShader
    {
	   Tags {
				"IgnoreProjector" = "True"
				"Queue" = "Transparent"
				"RenderType" = "Transparent"
			}

		GrabPass{ "_GrabTexture" }

        Pass
        {
			Tags {
				"LightMode" = "ForwardBase"
			}

			Blend SrcAlpha OneMinusSrcAlpha

			Cull Off
			ZWrite Off

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

			#include "UnityCG.cginc"
			#include "UnityPBSLighting.cginc"
			#include "UnityStandardBRDF.cginc"


			#pragma target 3.0

			uniform float4 _WaterColor;
			uniform float4 _DeepWaterColor;
			uniform float _DepthTransparency;
			uniform float _Fade;
			
			//自定义全局纹理, 深度和屏幕颜色
			//uniform sampler2D_float _LastDepthTexture;
			//uniform sampler2D_float _SceneColorTexture;
			uniform sampler2D _GrabTexture;
			uniform sampler2D_float _CameraDepthTexture;


			//波浪相关参数

			uniform sampler2D _SmallWavesTexture;
			uniform sampler2D _MediumWavesTexture;
			uniform sampler2D _LargeWavesTexture;

			uniform float _SmallWaveRrefraction;
			uniform float _SmallWavesSpeed;
			uniform float _SmallWavesTiling;


			uniform float _MediumWavesTiling;
			uniform float _MediumWavesSpeed;
			uniform float _MediumWaveRefraction;

			uniform float _LargeWaveRefraction;
			uniform float _LargeWavesTiling;
			uniform float _LargeWavesSpeed;

			//波浪距离
			uniform float _MediumTilingDistance;
			uniform float _DistanceTilingFade;
			uniform float _LongTilingDistance;

			//光照
			uniform float _Specular;
			uniform float _LightWrapping;
			uniform float _Gloss;


            struct VertexInput
            {
		float4 vertex : POSITION;
		float3 normal : NORMAL;
		float4 tangent : TANGENT;
		float2 texcoord0 : TEXCOORD0;
            };

            struct Interpolators
            {
		float4 pos : SV_POSITION;
		float2 uv : TEXCOORD0;
		float4 posWorld : TEXCOORD1;
		float3 normalDir : TEXCOORD2;
		float3 tangentDir : TEXCOORD3;
		float3 bitangentDir : TEXCOORD4;
		float4 screenPos : TEXCOORD5;
		float4 projPos : TEXCOORD6;
            };

	Interpolators vert (VertexInput v)
        {
		Interpolators i = (Interpolators)0;
		i.uv = v.texcoord0;
		i.normalDir = UnityObjectToWorldNormal(v.normal);
		i.tangentDir = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0.0)).xyz);
		i.bitangentDir = normalize(cross(i.normalDir, i.tangentDir) * v.tangent.w);

		i.posWorld = mul(unity_ObjectToWorld, v.vertex);

		i.pos = UnityObjectToClipPos(v.vertex);

		i.projPos = ComputeScreenPos(i.pos);
		COMPUTE_EYEDEPTH(i.projPos.z);
		i.screenPos = i.pos;
                return i;
        }

	float3 getWave(sampler2D tex, float3 objScale, Interpolators i,
float tiling, float speed, float refraction,
float _clampedDistance1, float _clampedDistance2) {

		//UV缩放设置
		float2 scale = objScale.rb*tiling;

		//根据时间和速度偏移UV
		float2 _smallWavesPanner = (i.uv + (float3((speed / scale), 0.0) * (_Time.r / 100.0)));

		float2 wavesUV = _smallWavesPanner * scale;

		//采样时, 根据时间偏移量和偏移缩放量采样
		float3 wavesTex = UnpackNormal(tex2D(tex, wavesUV));

		//return wavesTex;

		//第二次采样 uv 缩小20倍
		float3 wavesTex2 = UnpackNormal(tex2D(tex, wavesUV / 20.0));

		//类似之前的操作,采样uv被再次缩小,距离也同样
		float3 wavesTex3 = UnpackNormal(tex2D(tex, wavesUV / 60));

		//根据距离利用插值选择合适的UV
		return lerp(
			float3(0, 0, 1),
			lerp(lerp(wavesTex.rgb, wavesTex2.rgb, _clampedDistance1), wavesTex3.rgb, _clampedDistance2),
			lerp(lerp(refraction, refraction / 2.0, _clampedDistance1), (refraction / 8), _clampedDistance2));
		}

			
            fixed4 frag (Interpolators i) : SV_Target
            {
		//通过世界到模型空间转换矩阵,拿到基向量的变化,进而得知缩放信息
		//假如平面被缩放,我们要保证波浪不被跟着拉变形,所以需要这个信息
		float3 recipObjScale = float3(length(unity_WorldToObject[0].xyz), 
length(unity_WorldToObject[1].xyz), length(unity_WorldToObject[2].xyz));
		float3 objScale = 1.0 / recipObjScale;

#if UNITY_UV_STARTS_AT_TOP // OpenGL 和DX兼容设置
		float grabSign = -_ProjectionParams.x;
#else
		float grabSign = _ProjectionParams.x;
#endif

		i.normalDir = normalize(i.normalDir);
		i.screenPos = float4(i.screenPos.xy / i.screenPos.w, 0, 0);
		i.screenPos.y *= _ProjectionParams.x;

		//拿到切线变换,下面合并波浪法线要用
		float3x3 tangentTransform = float3x3(i.tangentDir, i.bitangentDir, i.normalDir);
		//视角方向,就是摄像机方向
		float3 viewDirection = normalize(_WorldSpaceCameraPos.xyz - i.posWorld.xyz);
				
		//算出距离相机的距离
		float _distance = distance(i.posWorld.rgb, _WorldSpaceCameraPos);
		//如果小于1 这个距离用平方缩小clamp距离
		float _clampedDistance1 = saturate(pow((_distance / _MediumTilingDistance), _DistanceTilingFade));
		float _clampedDistance2 = saturate(pow((_distance / _LongTilingDistance), _DistanceTilingFade));

/**小波浪相关设置**/
float3 _SmallWaveNormal = 
getWave(_SmallWavesTexture, objScale, i, _SmallWavesTiling, 
_SmallWavesSpeed, _SmallWaveRrefraction, _clampedDistance1, _clampedDistance2);
				
/**中级波浪相关设置**/
float3 _MediumWaveNormal = 
getWave(_MediumWavesTexture, objScale, i, 
_MediumWavesTiling, _MediumWavesSpeed, _MediumWaveRefraction, _clampedDistance1, _clampedDistance2);

/**大波浪相关设置**/
float3 _LargeWaveNormal = 
getWave(_LargeWavesTexture, objScale, i, 
_LargeWavesTiling, _LargeWavesSpeed, _LargeWaveRefraction, _clampedDistance1, _clampedDistance2);

	//合并波浪运算结果,偏转法线
	float3 normalWaveLocal = (_SmallWaveNormal + _MediumWaveNormal + _LargeWaveNormal);

        //这里可以输出法线看下法线效果
	//return float4(normalWaveLocal,1);

	//乘以切线空间矩阵,拿到真正的法线
	float3 normalDirection = normalize(mul(normalWaveLocal, tangentTransform));

	float3 lightDirection = normalize(_WorldSpaceLightPos0.xyz);
	float3 lightColor = _LightColor0.rgb;
	float3 halfDirection = normalize(viewDirection + lightDirection);

	float attenuation = 1;
	float3 attenColor = attenuation * _LightColor0.xyz;

	// Gloss:
	float gloss = _Gloss;
	float specPow = exp2(gloss * 10.0 + 1.0);

	// 根据法线算高光Specular:
	float NdotL = saturate(dot(normalDirection, lightDirection));
	float3 specularColor = (_Specular*_LightColor0.rgb);
	float3 directSpecular = attenColor * pow(max(0, dot(halfDirection, normalDirection)), specPow)*specularColor;
	
	float3 finalColor = directSpecular+ _WaterColor.rgb;
	fixed4 finalRGBA = fixed4(finalColor, 1);
	return finalRGBA;
        }
        ENDCG
        }
    }
}

计算深度 - 实现岸边颜色混合 和 折射

先看下目前的效果:

没有混合,非常生硬

从这里开始我们就开始逐段写出代码

法线完成后,我们就可以考虑根据深度和截屏的颜色来混合岸边

大家大致看下想法,参数我就不贴出来了

/**根据深度和坐标拿到当前的深度差**/
//屏幕深度
float sceneZ = max(0, 
LinearEyeDepth(UNITY_SAMPLE_DEPTH(tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos)))) - _ProjectionParams.g);

float partZ = max(0, i.projPos.z - _ProjectionParams.g);

//算出深度差, 这个就可以做很多事,比如岸边,根据深度颜色变化
float depthGap = sceneZ - partZ;

//深度乘数,用深度差和预设参数运算出一个乘数,方便下面的运算
float deepMultiplier = pow(saturate(depthGap / _DepthTransparency), _ShoreFade)*saturate(depthGap / _ShoreTransparency);

然后是折射:

//利用偏移抓取屏幕颜色的截图UV, 实现类似折射的效果
float2 sceneUVs = float2(1, grabSign) * i.screenPos.xy * 0.5 + 0.5
	+ lerp(
	((normalWaveLocal.rg*(_MediumWaveRefraction*0.02))*deepMultiplier),
	float2(0, 0), saturate(pow((distance(i.posWorld.rgb, _WorldSpaceCameraPos) / _RefractionDistance),
	_RefractionFalloff)));

//截屏用在这里
float4 sceneColor = tex2D(_GrabTexture, sceneUVs);

然后根据上面深度的内容,我们混合一个岸边的颜色出来:

//根据深度混合Deep color 和 water color
float3 _blendWaterColor = saturate(
_DeepWaterColor.rgb + sceneColor.rgb * saturate(_Fade - depthGap) * _WaterColor.rgb
);

你也可以分辨输出一下看看_blendWaterColor 具体的造型:

最后我们在颜色混合后根据颜色成熟插值一下岸边颜色:

float3 finalColor = directSpecular + _blendWaterColor;
fixed4 finalRGBA = fixed4(lerp(sceneColor.rgb, finalColor, deepMultiplier), 1);

此时如果你自己凑了几个参数,你应该实现了这样的效果:

泡沫

泡沫这里有一个小track,就是用一个旋转矩阵,旋转UV可以做出多层叠加一起的效果

和法线贴图一样,我们这里重复采样增加丰富度

然后泡沫的方法如下:

float3 getFoam(Interpolators i, float3 _blendWaterColor,float3 objScale, float depthGap, float3 normalWaveLocal) {

	//根据波浪法线和屏幕坐标采样, *0.5+0.5是为了归一化
	float2 _remap = (i.screenPos.rg + normalWaveLocal.rg)*0.5 + 0.5;
	float4 _ReflectionTex_var = tex2D(_ReflectionTex, TRANSFORM_TEX(_remap, _ReflectionTex));

	float _rotator_ang = 1.5708;
	float _rotator_spd = 1.0;
	float _rotator_cos = cos(_rotator_spd*_rotator_ang);
	float _rotator_sin = sin(_rotator_spd*_rotator_ang);
	float2 _rotator_piv = float2(0.5, 0.5);

	//旋转UV, uv * 2D旋转矩阵
	float2 _rotator = 
(mul(i.uv - _rotator_piv, float2x2(_rotator_cos, -_rotator_sin, _rotator_sin, _rotator_cos)) + _rotator_piv);

	//泡沫贴图的Tiling和物体缩放相乘拿到UV缩放比例
	float2 _FoamDivision = objScale.rb*_FoamTiling;

	//UV根据时间偏移具体量
	float3 _foamUVSpeed = (float3(_FoamSpeed / _FoamDivision, 0.0)*(_Time.r / 100.0));

	////旋转后的UV + uv偏移量,拿到最终UV
	float2 _FoamAdd = (_rotator + _foamUVSpeed);

	////UV * 缩放乘数
	float2 _foamUV = (_FoamAdd*_FoamDivision);
	float4 _foamTex1 = tex2D(_FoamTexture, _foamUV);

	float2 _FoamAdd2 = (i.uv + _foamUVSpeed);
	float2 _foamUV2 = (_FoamAdd2*_FoamDivision);
	float4 _foamTex2 = tex2D(_FoamTexture, _foamUV2);

	float2 _foamUV3 = (_FoamAdd*objScale.rb*_FoamTiling / 3.0);
	float4 _foamTex3 = tex2D(_FoamTexture, _foamUV3);

	float2 maxUV = (_FoamAdd2*_foamUV3);
	float4 _foamTex4 = tex2D(_FoamTexture, maxUV);

	//根据距离混合泡沫纹理的几种不同的UV
	float3 blendFoamRGB = lerp((_foamTex1.rgb - _foamTex2.rgb), (_foamTex3.rgb - _foamTex4.rgb),
		saturate(pow((distance(i.posWorld.rgb, _WorldSpaceCameraPos) / 20), 3)));
	//去色
	float3 foamRGBGray = (dot(blendFoamRGB, float3(0.3, 0.59, 0.11)) - _FoamContrast) / (1.0 - 2 * _FoamContrast);

	

	//根据深度混合颜色
	float3 foamRGB = foamRGBGray * _FoamColor.rgb *_FoamIntensity;

	float3 sqrtFoamRGB = (foamRGB*foamRGB);
	return lerp(_blendWaterColor, sqrtFoamRGB, _FoamVisibility);

}

大家可以在最后输出一下泡沫的方法,看下效果

泡沫覆盖了所有的海域,很好,下面我们在海浪上根据深度处理下泡沫的范围就可以做出贴边的海浪了

海浪我是在网上找一个大佬写的代码,遗憾的时候不太记得是哪里看的了

不过原理也非常简单

用一个纹理作为海浪的范围,然后用一个sin函数不断循环,依旧是两次采样做出叠加的海浪

然后根据深度控制海浪范围海浪

代码如下:

//岸边拍打的海浪相关业务
float3 getSurge(Interpolators i,float3 objScale, float depthGap)
{
//缩放UV
float2  surgeUVScale = objScale.rb / 200;
//噪波图
fixed4 noiseColor = tex2D(_NoiseTex, i.uv*objScale.rb / 5);
//第一个海浪
fixed4 surgeColor = tex2D(_SurgeTex, float2(1 - min(_Range, depthGap) / _Range + _SurgeRange * sin(_Time.x*_SurgeSpeed + noiseColor.r*_NoiseRange), 1)*surgeUVScale);
surgeColor.rgb *= (1 - (sin(_Time.x*_SurgeSpeed + noiseColor.r*_NoiseRange) + 1) / 2)*noiseColor.r;
//第二个海浪
fixed4 surgeColor2 = tex2D(_SurgeTex, float2(1 - min(_Range, depthGap) / _Range + _SurgeRange * sin(_Time.x*_SurgeSpeed + _SurgeDelta + noiseColor.r*_NoiseRange) + 0.5, 1)*surgeUVScale);
surgeColor2.rgb *= (1 - (sin(_Time.x*_SurgeSpeed + _SurgeDelta + noiseColor.r*_NoiseRange) + 1) / 2)*noiseColor.r;

//根据深度控制海浪范围
half surgeWave= 1 - min(_Range, depthGap) / _Range;
return (surgeColor.rgb + surgeColor2.rgb * _SurgeColor) * surgeWave;
}

但是这里还没完, 海浪有两种情况

一种是海边,那就是正常的海浪

但是还有一种是在河流中,那这时候我们只要根据深度输出泡沫而不要海浪

于是我们做一个单选框:

[Toggle]
_SurgeType("SurgeType",Float) = 1

在最后颜色混合的时候,根据插值混合下深度或者直接乘波浪

float3 finalColor = directSpecular + _blendWaterColor+ 
                        lerp(1-(saturate(depthGap / _FoamBlend)), surgeFinalColor, _SurgeType)* foamColor; //重点在这里

左边是勾上,右边是不勾,不过我适当调整了泡沫的参数,来调整亮度

这样整个海浪我们就都完成了

CommandBuffer的例子

我之前看一个大佬用CommandBuffer在经典管线下获取深度,来降低DC

Unity本身的深度获取方式太2了,当然你要用SRP就省事了,利用MRT就可以

我转念一想...干嘛不把颜色一起拿出来,于是我稍微改了一下,

也象征性的可以支持Post也求教诸位大佬有没有更好的方案

请大家无视Bloom的逗比写法, 比如最后那个:Graphics.Blit(dest,src);

我只是想试试post能不能顺利工作

如果你要应用这个,请把这个脚本挂到相机上

并且注释掉 GrabPass{ "_GrabTexture" }

同时替换水体shader里最上面两个纹理

打开 注释并且替换正文里的纹理引用

//自定义全局纹理, 深度和屏幕颜色
uniform sampler2D_float _LastDepthTexture;
uniform sampler2D_float _SceneColorTexture;

//uniform sampler2D _GrabTexture;
//uniform sampler2D_float _CameraDepthTexture;
......
float sceneZ = max(0, LinearEyeDepth(UNITY_SAMPLE_DEPTH(tex2Dproj(_LastDepthTexture, UNITY_PROJ_COORD(i.projPos)))) - _ProjectionParams.g);
......

//截屏用在这里
float4 sceneColor = tex2D(_SceneColorTexture, sceneUVs);

C#代码也可以去Git链接里看

完整代码和资源

如果你想支持延迟渲染,只要最后改一下各个参数的混合模式就行了

还有比如把float 改成fixed这种事,请大家根据喜好酌情修改吧


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK