一个简单的水着色器

水着色网上的示例有很多,但还是可以练习一下用来熟悉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;
            }

这个时候岸边的波纹是可以动的。

posted @ 2022-01-09 19:46  melt00  阅读(156)  评论(0编辑  收藏  举报