一个简单的水着色器
水着色网上的示例有很多,但还是可以练习一下用来熟悉shader的api
模拟一个水面通常有以下几点:
(1)水的颜色根据深度变化
(2)水会移动,这个可以用flowmap,也可以用其他方法
(3)水有镜面反射,漫反射,水下折射
深度
unity shader里面水的表现水的深度需要采样一张深度图,深度图可以让画一张贴图,也可以让camera实时采样.
camera实时采样深度时,计算的深度时物体的屏幕坐标到camera屏幕坐标的距离
因此第一步:
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float4 screenPosition : TEXCOORD1; //屏幕坐标
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.screenPosition = ComputeScreenPos(o.vertex); //用ComputeScreenPos()这个方法计算顶点的屏幕坐标
return o;
}
有了这个物体的屏幕坐标后,就可以用api进行深度图的采样了.
需要注意的是,深度图的采样和2D纹理不同,是需要进行将正交投影转成透视投影的.
以下片元着色器的两种写法都正确:
float existingDepth01 = tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPosition)).r;
float existingDepth01 = tex2D(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPosition.xy / i.screenPosition.w)).r;
为什么要用xy坐标除以w坐标?可以参考learnOpenGL:https://learnopengl-cn.github.io/01 Getting started/08 Coordinate Systems/
这个投影矩阵将给定的平截头体范围映射到裁剪空间,除此之外还修改了每个顶点坐标的w值,从而使得离观察者越远的顶点坐标w分量越大。
被变换到裁剪空间的坐标都会在-w到w的范围之间(任何大于这个范围的坐标都会被裁剪掉)。OpenGL要求所有可见的坐标都落在-1.0到1.0范围内,作为顶点着色器最后的输出,
因此,一旦坐标在裁剪空间内之后,透视除法就会被应用到裁剪空间坐标
片元着色器代码:
fixed4 frag (v2f i) : SV_Target
{
float4 res;
fixed4 col = tex2D(_MainTex, i.uv);
float depth0 =tex2Dproj(_CameraDepthTexture,UNITY_PROJ_COORD(i.screenPosition) ).r; //透视投影
float depthLinear = LinearEyeDepth(depth0);
float depthDifference = depthLinear - i.screenPosition.w; //深度差值
float4 surfaceColor =depthDifference; //这一步输出的是深度的颜色
return surfaceColor;
}
这一步的颜色中,深度小的颜色深,深度大的颜色的浅,这个原因跟深度写入的算法有关.
接着为水上个颜色,用lerp插值,插值之前将diffrence转到0-1,原因是lerp公式是相当于return a + x *(b-a) ;
的.
float depthDifference = saturate( depthLinear - i.screenPosition.w); //深度差值
float4 surfaceColor =lerp(_ShallowColor,_DeepColor,depthDifference); //lerp插值
插值完后的效果:
顺便添加透明效果
Tags { "Queue" = "Transparent" "RenderType" = "Transparent"}
Blend SrcAlpha OneMinusSrcAlpha //设置透明混合为常规模式
ZWrite Off
岸边的波浪/泡沫
水边通常是有白色的波浪或泡沫的,首先需要做的是确定哪里是岸边
简单的思路就是 这个点的深度 > step 则是它是岸边部分,有泡沫.
接着是怎么表现这个部分有泡沫,可以添加一个泡沫纹理,=1的部分有这个纹理,=0的部分则没有.
先测试这个采样的噪声纹理代码:
_WaveNoise ("Wave Noise",2D) ="white" {}
struct v2f
{
float2 waveNosieUV :TEXCOORD2;
};
sampler2D _WaveNoise;
float4 _WaveNoise_ST;
fixed4 frag (v2f i) : SV_Target
{
float var_waveNoise = tex2D(_WaveNoise,i.waveNosieUV).r;
surfaceColor =var_waveNoise;
}
step操作:
_WaveRange("Wave Range",range(0,1) ) = 0.3
float waveStep = saturate(depthDifference /_WaveRange);
float waveNoise = var_waveNoise > waveStep ?1:0;
surfaceColor =waveNoise+ surfaceColor;
动画
接着,让水动起来
这里尝试flowmap的方法,这个方法是简单省性能的:https://catlikecoding.com/unity/tutorials/flow/texture-distortion/
float2 flowVector = tex2D(_FlowMap, IN.uv_MainTex).rg*2 - 1;
采样flowmap做流动方向的时候要*2-1 得到[-1,1]的方向向量,因为采样的时候采到的是[0,1]的二值,而向量方向是有负数的.
frac()可以让函数周期性地从0~1变化
uv = uv - flowVector * frac(Time.y)
这个意思就是采样主纹理用的uv坐标随着时间进行偏移,向哪偏就是flowVector决定,偏多少由frac(Time.y)决定,理解成积分就好
用frac()函数有跳变的问题,要过渡的话,有一个解决办法是再采样一个纹理,然后将两个纹理颜色进行lerp,采另一个纹理时frac()里面的系数要移半个相位
代码:
float2 FlowUV1( float2 uv, float2 flowVector, float time){
float progress = frac(time*0.1* _TimeSpeed);
return uv - flowVector * progress;
}
float2 FlowUV2( float2 uv, float2 flowVector, float time){
float progress = frac(time*0.1* _TimeSpeed+0.5);
return uv - flowVector * progress;
}
void surf (Input IN, inout SurfaceOutputStandard o)
{
// Albedo comes from a texture tinted by color
float2 flowVector = tex2D(_FlowMap, IN.uv_MainTex).rg*2 - 1;
float noise = tex2D(_FlowMap, IN.uv_MainTex).a;
//获得计算后的uv
float2 uv1 = FlowUV1(IN.uv_MainTex, flowVector , _Time.y);
float2 uv2 = FlowUV2(IN.uv_MainTex, flowVector , _Time.y);
//用uv采样主纹理,采两次
fixed4 c1 = tex2D (_MainTex, uv1) * _Color;
fixed4 c2 = tex2D (_MainTex, uv2) * _Color;
float p = frac(_Time.y*0.1* _TimeSpeed);
float lerpFractor = abs( (0.5-p) / 0.5 );
fixed4 c = lerp(c1,c2,lerpFractor);
o.Albedo = c.rgb;
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
这样采了uv之后就有基本的过渡了
测试这个flowMap:
接着,往之前写的shader里面加flowmap.
fixed4 frag (v2f i) : SV_Target
{
float4 res;
fixed4 col = tex2D(_MainTex, i.uv);
//
float depth0 =tex2Dproj(_CameraDepthTexture,UNITY_PROJ_COORD(i.screenPosition) ).r; //透视投影
float depthLinear = LinearEyeDepth(depth0);
float depthDifference = depthLinear - i.screenPosition.w; //深度差值
float depthDifference1 =saturate( depthDifference /_DepthMaxDistance);
float4 surfaceColor =lerp(_ShallowColor,_DeepColor,depthDifference1);
//flowmap部分
// floa3 dh = UnpackDerivativeHeight(tex2D(_MainTex))
float2 flowVector = tex2D(_FlowMap, i.flowmapUV).rg*2 - 1;
float2 fUV1 = FlowUV1(i.flowmapUV,flowVector,_Time.y);
float2 fUV2 = FlowUV2(i.flowmapUV,flowVector,_Time.y);
float p = frac(_Time.y*0.1* _TimeSpeed);
float lerpFractor = abs( (0.5-p) / 0.5 );
float f1 = tex2D(_WaveNoise,fUV1).r ;
float f2 = tex2D(_WaveNoise,fUV2).r;
float fnoise = lerp(f1,f2,lerpFractor);
//计算岸边泡沫
// float var_waveNoise = tex2D(_WaveNoise,i.waveNosieUV).r;
float waveStep = saturate(depthDifference /_WaveRange);
float waveNoise = 1- step(fnoise,waveStep);
surfaceColor = waveNoise + surfaceColor;
return surfaceColor;
}
这个时候岸边的波纹是可以动的。