Unity Shader - 实现简单水体 - 浅水到深水颜色控制

文章目录

  • 制作步骤
    • 准备好水体网格
    • 扰动水体网格
    • 添加水体网格色调,纹理
    • 放置海上放哨点(一些随便放的立方体)
    • 添加水的深浅透视效果
    • 添加水光效
    • 重构水顶点法线
  • 正交的相机的深度需要注意
  • 改进
  • Project
  • References

简单的模拟水的效果(3A游戏效果请绕开哦)

效果:



制作步骤

  • 准备好水体网格(网格脚本生成的,参考:Unity Shader - Noise 噪点图 - 实现简单山脉)
  • 扰动水体网格顶点(参考:Unity Shader - 使用Noise噪点图生成简单山脉(使用tex2Dlod控制顶点高度))
  • 添加水体网格色调,纹理
  • 放置海上放哨点(一些随便放的立方体)
  • 添加水的深浅透视效果
  • 添加水光效
  • 重构水顶点法线(参考:Unity Shader - 简单山脉 - 顶点着色器重构法线)

准备好水体网格

网格脚本生成的,参考:Unity Shader - Noise 噪点图 - 实现简单山脉
Unity Shader - 实现简单水体 - 浅水到深水颜色控制_第1张图片

扰动水体网格

参考:Unity Shader - 使用Noise噪点图生成简单山脉(使用tex2Dlod控制顶点高度)

我使用了:大波浪与小波浪的叠加

                float centerH = sin(_Time.y * _BigWaveLen + v.uv.x + v.uv.y) * _BigWaveAmplitude;
                centerH += tex2Dlod(_MainTex, float4(v.uv + float2(_Time.x * _SmallWaveSpeedX, _Time.x * _SmallWaveSpeedY), 0, 0)).r * _SmallWaveAmplitude;
                v.vertex.y = centerH;

添加水体网格色调,纹理

着了个色调,并对uv滚动动画

                fixed4 col = tex2D(_MainTex, i.uv + float2(_Time.y * _BigWaveLen * _UVSpeed, 0));
                col.rgb *= _MainColor.rgb;

放置海上放哨点(一些随便放的立方体)

Unity Shader - 实现简单水体 - 浅水到深水颜色控制_第2张图片

添加水的深浅透视效果

这个参考了unity内置的shader中,处理软粒子的思路:。

  • 获取深度纹理的view spacebuffViewZ
  • 获取当前片段的view spacefragViewZ
  • delta = buffViewZ - fragViewZ的深度插值,来控制水体的alpha透视,或lerp的颜色过渡。
  • 还可以使用我们下面实现代码中的fade来控制浅水到深度的效果,我这里就控制海水原来的颜色(combined.rgb + _ShallowColor.rgb * (_ShallowColor.a * 2))到浅水着色变量的颜色来过渡(combined.rgb)。
    combined.rgb = lerp(combined.rgb + _ShallowColor.rgb * (_ShallowColor.a * 2), combined.rgb, fade);
    如果需要你可以创建一个用个纹理:_ShallowToDeepTex的纹理然后:
var shallowToDeepCol = tex2D(_ShallowToDeepTex, i.uv);
combined.rgb = lerp(combined.rgb + shallowToDeepCol.rgb * (shallowToDeepCol.a * 2), combined.rgb, fade);

下面是参考的:unity内置的shader软粒子代码:

// Soft particles fragment function
#if defined(SOFTPARTICLES_ON) && defined(_FADING_ON)
#define fragSoftParticles(i) \
    if (SOFT_PARTICLE_NEAR_FADE > 0.0 || SOFT_PARTICLE_INV_FADE_DISTANCE > 0.0) \
    { \
        float sceneZ = LinearEyeDepth (SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projectedPosition))); \
        float fade = saturate (SOFT_PARTICLE_INV_FADE_DISTANCE * ((sceneZ - SOFT_PARTICLE_NEAR_FADE) - i.projectedPosition.z)); \
        ALBEDO_MUL *= fade; \
    }
#else
#define fragSoftParticles(i)
#endif

参考我手绘的一张图:
Unity Shader - 实现简单水体 - 浅水到深水颜色控制_第3张图片

下面我们的实现代码

				// vert
                #if DEEP_EFF
                o.projPos = ComputeScreenPos (o.pos);
                COMPUTE_EYEDEPTH(o.projPos.z);
                #endif

				// frag
                #if DEEP_EFF    // 水深效果
                // 深度效果
                // 这里参考unity内置的shader中,实现软例子:Soft particle的写法
                // 与背景深度远时,alpha比较高,里的近则alha低
                // 先是获取深度图的view space下的z值: buffViewZ
                float buffViewZ = LinearEyeD、epth (SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos)));
                // 再是获取片段的view space的z值:fragViewZ
                float fragViewZ = i.projPos.z;
                // 然后去他们之间的距离值做alpha的控制,与颜色lerp的变化
                float fade = saturate ((buffViewZ-fragViewZ) * _DeepFactor);
                combined.rgb = lerp(combined.rgb + _ShallowColor.rgb * (_ShallowColor.a * 2), combined.rgb, fade);
                combined.a *= fade;
                #endif

材质中暴露了一个DeepFactor参数,可以用于控制水深的系数。如下:

也暴露了一个浅水的颜色色调着色:

添加水光效

这个就是传统的经验光照模型:ambient + diffuse(HalfLambert) + specular(blinn phong)

				// frag
                #if LIGHTING_ON // 光照效果
                // ambient
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
                // diffuse
                half3 L = normalize(_WorldSpaceLightPos0.xyz);
                half3 N = normalize(i.normal);
                half LdotN = dot(L, N) * 0.5 + 0.5;
                fixed3 diffuse = col.rgb * LdotN;
                // specular
                half3 specular = 0;
                half3 V = normalize(_WorldSpaceCameraPos.xyz - i.wPos);
                half3 H = normalize(L + V);
                half HdotN = max(0, dot(H, N)); // blinn-phone
                specular = _LightColor0.rgb * pow(HdotN, _SpecularGlossy * 100) * _SpecularIntensity;
                // combined color
                fixed4 combined = fixed4(ambient + diffuse + specular, col.a);
                #else // LIGHTING_OFF
                fixed4 combined = col;
                #endif

Unity Shader - 实现简单水体 - 浅水到深水颜色控制_第4张图片

重构水顶点法线

参考:Unity Shader - 简单山脉 - 顶点着色器重构法线,这里不说了,参考的文章说得很清楚的。

                #if REBUILD_NORMAL // 重构法线,参考:https://blog.csdn.net/linjf520/article/details/104859710

                // reconstruct normals
                // 这个4x4数据也可以通过外部传入,可以节省顶点着色器ALU的计算量与L1 caches缓存量。
                const float4x4 offset_xz = {
                    {+1,+0, /* gap **/ +1,-1},  // 右下
                    {+0,-1, /* gap **/ -1,-1},  // 左下
                    {-1,+0, /* gap **/ -1,+1},  // 左上
                    {+0,+1, /* gap **/ +1,+1}   // 右上
                };
                // 默认向量也可以外部传入,因为上面的默认法线是可以调整的
                // 下面我讲默认法线初始化为:up
                float3 sumNormal = float3(0, 1, 0);
                float4 uv4 = 0;
                for (int i = 0; i < 4; i++) {
                    // 获取偏移数据
                    float4 uvs_offset = offset_xz[i];
                    // 获取偏移数据分别的高度
                    half h1 = 
                    sin(_Time.y * _BigWaveLen + v.uv.x + uvs_offset.x * _MainTex_TexelSize.x + v.uv.y + uvs_offset.y * _MainTex_TexelSize.y) * _BigWaveAmplitude;
                    uv4 = float4(v.uv + uvs_offset.xy * _MainTex_TexelSize.xy + float2(_Time.x * _SmallWaveSpeedX, _Time.x * _SmallWaveSpeedY), 0, 0);
                    h1 += tex2Dlod(_MainTex, uv4).r * _SmallWaveAmplitude;
                    half h2 = sin(_Time.y * _BigWaveLen + v.uv.x + uvs_offset.z * _MainTex_TexelSize.x + v.uv.y + uvs_offset.w * _MainTex_TexelSize.y) * _BigWaveAmplitude;
                    uv4 = float4(v.uv + uvs_offset.zw * _MainTex_TexelSize.xy + float2(_Time.x * _SmallWaveSpeedX, _Time.x * _SmallWaveSpeedY), 0, 0);
                    h2 += tex2Dlod(_MainTex, uv4).r * _SmallWaveAmplitude;
                    // 根据偏移方向,高度重构从当前顶点,指向附近偏移点的高度两个向量
                    float3 dir1 = float3(uvs_offset.x * _GridGap, h1 - centerH, uvs_offset.y * _GridGap);
                    float3 dir2 = float3(uvs_offset.z * _GridGap, h2 - centerH, uvs_offset.w * _GridGap);
                    // 根据平面两个向量(两个向量可以确定一个平面,如同:TBN当中的TB两个切向量)
                    // 叉乘得到平面的法线向量
                    float3 newNormal = (cross(dir1, dir2));
                    // 累加到混合向量中
                    sumNormal += newNormal;
                }
                // 均值混合
                sumNormal /= 5;
                o.normal = UnityObjectToWorldNormal(sumNormal);

                #else // REBUILD_NORMAL off
                o.normal = UnityObjectToWorldNormal(v.normal);
                #endif

Unity Shader - 实现简单水体 - 浅水到深水颜色控制_第5张图片

正交的相机的深度需要注意

unity底层在处理:相机的正交与透视投影模式下,分别对_CameraDepthTexture有不一样的编码过程(如果你使用的是_CameraDepthNormalsTexture就没这个问题),具体,可以参考我之前写过的一篇文章:Unity Shader - 获取BuiltIn深度纹理和自定义深度纹理的数据-只看:注意Unity正交相机中的深度纹理的编码 部分就好了。

因为我们使用的是_CameraDepthTexture,所以我们需要处理一下读取深度值的逻辑:

				// frag
				// jave.lin 2020.03.15
                #if DEEP_EFF || FOAM
                // unity_OrthoParams变量在:UnityShaderVariables.cginc有定义:
                // x = orthographic camera's width
                // y = orthographic camera's height
                // z = unused
                // w = 1.0 if camera is ortho, 0.0 if perspective
                // float4 unity_OrthoParams;

                float linearEyeDepth = 0;
                float linear01Depth = 0;
                // ortho - 处理正交的
                if (unity_OrthoParams.w == 1) {
                    float depth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos));
                    #if defined(UNITY_REVERSED_Z) // 正交需要处理这个宏定义,透视不用,估计后面unity版本升级后会处理正交的这个宏定义处理吧
                    depth = 1 - depth;
                    #endif

                    /* Linear01Depth的逆运算
                    // Z buffer to linear 0..1 depth
                    inline float Linear01Depth( float z )
                    {
                        return 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y);
                    }
                    Linear01Depth = 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y);
                    (_ZBufferParams.x * z + _ZBufferParams.y) * Linear01Depth = 1
                    (_ZBufferParams.x * z + _ZBufferParams.y) = 1/Linear01Depth
                    (_ZBufferParams.x * z) = 1/Linear01Depth - _ZBufferParams.y
                    z = (1/Linear01Depth - _ZBufferParams.y) / _ZBufferParams.x 
                    Linear01Depth == ndcZ
                    z = (1/ndcZ - _ZBufferParams.y) / _ZBufferParams.x 
                    */
                    linearEyeDepth = LinearEyeDepth (/*to ndcZ**/(1.0/depth - _ZBufferParams.y) /_ZBufferParams.x);
                    linear01Depth = (depth);
                } else { // perspective - 处理透视的
                    float depth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos));
                    linearEyeDepth = LinearEyeDepth (depth);
                    linear01Depth = Linear01Depth(depth);
                }
                
                #endif

                #if DEEP_EFF    // 水深效果
                // 深度效果
                // 这里参考unity内置的shader中,实现软例子:Soft particle的写法
                // 与背景深度远时,alpha比较高,里的近则alha低
                // 先是获取深度图的view space下的z值: buffViewZ
                // float buffViewZ = LinearEyeDepth (SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos)));
                float buffViewZ = linearEyeDepth;
                // 再是获取片段的view space的z值:fragViewZ
                float fragViewZ = i.projPos.z;
                // 然后去他们之间的距离值做alpha的控制,与颜色lerp的变化
                float fade = saturate ((buffViewZ-fragViewZ) * _DeepFactor);
                combined.rgb = lerp(combined.rgb + _ShallowColor.rgb * (_ShallowColor.a * 2), combined.rgb, fade);
                combined.a *= fade;
                #endif

注意上面代码中,i.projPos.z,可以改写为使用i.vertex.w即可,因为在project matrix中,m[4,3] == -1,就将model view space下的pos.w=-pos.z了。

正交下图效果:

改进

  • 添加水边Fresnel 反射
  • 添加透视
  • 添加折射
  • 添加水体背光透视
  • 水体网格面细分(Tessellation不用CPU端生成的方式)
  • 水体顶点分形、DFT快速傅里叶波动

后面有空再处理

Project

backup : UnityShader_SimpleWater_ShallowToDeepEffect_2018.3.0f2

References

  • Simple Water Shader in Unity
  • 网格脚本生成的,参考:Unity Shader - Noise 噪点图 - 实现简单山脉。
  • 扰动水体网格顶点,参考:Unity Shader - 使用Noise噪点图生成简单山脉(使用tex2Dlod控制顶点高度)。
  • 重构水顶点法线,参考:Unity Shader - 简单山脉 - 顶点着色器重构法线。
  • 【Unity Shader学习笔记】实现反射与折射模拟水面、使用grabPass与环境贴图 - 写完文章后,法线有个博主写得挺好的,推荐阅读。
  • Unity法线水,顺便利用CommandBuffer实现廉价的深度和截屏 - 这个后面可以尝试学习。
  • Unity海洋shader笔记①

你可能感兴趣的:(unity,unity-shader)