21.水波效果具体实现

21.噪声-水波效果-具体实现


21.1 知识点

知识回顾:水波效果基本原理

水波效果指在计算机图形学中模拟水面波纹的视觉效果。其基本原理包括:

  • 可基于之前实现的带法线纹理的玻璃效果进行修改
  • 通过添加噪声法线纹理并结合 Shader 内置时间变量实现水波动态效果
  • 引入菲涅耳计算公式,以实现水面的光学特性

准备工作

导入纹理资源

导入噪声纹理和水面颜色纹理,并设置噪声纹理的配置,将其设置为从灰度生成的法线纹理。


复制Shader开发基础 Lesson40_高级纹理_渲染纹理_玻璃效果_带法线纹理实现的测试场景

新建Shader Lesson21_WaterWave 并删除其中无用代码 复制 Lesson40_GlassRefractionNormal 带法线纹理的玻璃效果

Shader "Unlit/Lesson21_WaterWave"
{
    Properties
    {
        //主纹理
        _MainTex("MainTex", 2D) = ""{}

        //  法线纹理
        _BumpMap("BumpMap", 2D) = ""{}

        //立方体纹理
        _Cube("Cubemap", Cube) = ""{}
        //折射程度 0~1 0代表完全反射(完全不折射)1代表完全折射(透明效果 相当于光全部进入了内部)
        _RefractAmount("RefractAmount", Range(0,1)) = 1


        //控制折射扭曲程度的变量
        _Distortion("Distortion", Range(0,10)) = 0
    }
    SubShader
    {
        //将渲染队列改为透明的 目的是让玻璃对象 滞后渲染
        //能够捕获到之前正确的屏幕图像
        Tags
        {
            "RenderType"="Opaque" "Queue"="Transparent"
        }
        //使用它来捕获当前屏幕内容 并存储到默认的渲染纹理变量中
        GrabPass {}

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

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;

            sampler2D _BumpMap; //法线纹理
            float4 _BumpMap_ST; //法线纹理的缩放和平移

            samplerCUBE _Cube;
            float _RefractAmount;
            //GrabPass默认存储的纹理变量 这个是规则
            sampler2D _GrabTexture;

            //控制折射扭曲程度的变量
            float _Distortion;

            struct v2f
            {
                //裁剪空间下的顶点坐标
                float4 pos:SV_POSITION;
                //用于存储从屏幕图像中采样的坐标(顶点相对于屏幕的位置)
                float4 grabPos:TEXCOORD0;
                //用于在颜色纹理中采样的UV坐标
                float4 uv:TEXCOORD1;

                // //世界空间下的反射向量
                // //我们将把反射向量的计算放在顶点着色器函数中 节约性能 表现效果也不会太差 肉眼几乎分辨不出来
                // float3 worldRefl:TEXCOORD2;

                //代表我们切线空间到世界空间的 变换矩阵的3行 三个变量的w存世界坐标
                float4 TtoW0:TEXCOORD2;
                float4 TtoW1:TEXCOORD3;
                float4 TtoW2:TEXCOORD4;
            };

            v2f vert(appdata_full appdata_full)
            {
                v2f v2f;
                //顶点坐标转换
                v2f.pos = UnityObjectToClipPos(appdata_full.vertex);

                //屏幕坐标转换相关的内容 注意传进去的是裁剪空间下的坐标
                v2f.grabPos = ComputeGrabScreenPos(v2f.pos);
                //uv坐标计算相关的内容
                v2f.uv.xy = appdata_full.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;


                //法线纹理的uv坐标计算
                v2f.uv.zw = appdata_full.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

                //计算反射光向量
                //1.计算世界空间下法线向量
                float3 worldNormal = UnityObjectToWorldNormal(appdata_full.normal);
                //2.世界空间下的顶点坐标
                fixed3 worldPos = mul(unity_ObjectToWorld, appdata_full.vertex).xyz;
                //3.计算视角方向 内部是用摄像机位置 - 世界坐标位置 
                fixed3 worldViewDir = UnityWorldSpaceViewDir(worldPos);

                // //4.计算反射向量
                // v2f.worldRefl = reflect(-worldViewDir, worldNormal);


                //把模型空间下的法线、切线转换到世界空间下
                // float3 worldNormal = UnityObjectToWorldNormal(appdata_base.normal);//上面已经计算了
                float3 worldTangent = UnityObjectToWorldDir(appdata_full.tangent);

                //计算副切线 计算叉乘结果后 垂直与切线和法线的向量有两条 通过乘以 切线当中的w,就可以确定是哪一条
                //cross是叉乘 appdata_full.tangent.w代表叉乘结果方向
                float3 worldBinormal = cross(normalize(worldTangent), normalize(worldNormal)) * appdata_full.tangent.w;

                //这个就是我们 切线空间到世界空间的 转换矩阵
                //       |    |     |
                //      切线 副切线 法线
                //       |    |     |
                //v2f.rotation = float3x3( worldTangent.x, worldBinormal.x,  worldNormal.x,
                //                          worldTangent.y, worldBinormal.y,  worldNormal.y,
                //                          worldTangent.z, worldBinormal.z,  worldNormal.z);
                v2f.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
                v2f.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
                v2f.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);


                return v2f;
            }

            fixed4 frag(v2f v2f):SV_TARGET
            {
                //世界空间下视角方向
                // fixed3 viewDir = normalize(UnityWorldSpaceViewDir(v2f.worldPos));
                float3 worldPos = float3(v2f.TtoW0.w, v2f.TtoW1.w, v2f.TtoW2.w);
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));


                //纹理采样函数tex2D
                //通过纹理采样函数 取出法线纹理贴图当中的数据
                float4 packedNormal = tex2D(_BumpMap, v2f.uv.zw);

                //利用内置的UnpackNormal函数对法线信息进行逆运算以及可能的解压
                //将我们取出来的法线数据 进行逆运算并且可能会进行解压缩的运算,最终得到切线空间下的法线数据
                float3 tangentNormal = UnpackNormal(packedNormal);

                // 没有处理凹凸程度 这一段注释
                // //乘以凹凸程度的系数
                // tangentNormal.xy *= _BumpScale;
                // tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));


                //把计算完毕后的切线空间下的法线转换到世界空间下
                // float3 worldNormal = mul(v2f.rotation, tangentNormal);

                //以下两种写法等价 但是可以一步到位
                //float3x3 rotation = float3x3(v2f.TtoW0.xyz, v2f.TtoW1.xyz, v2f.TtoW2.xyz );
                //float3 worldNormal = mul(rotation, tangentNormal);
                //本质 就是在进行矩阵运算
                float3 worldNormal = float3(dot(v2f.TtoW0.xyz, tangentNormal), dot(v2f.TtoW1.xyz, tangentNormal),
                                            dot(v2f.TtoW2.xyz, tangentNormal));


                //反射颜色相关的计算 会叠加主纹理颜色
                //对颜色纹理进行采样
                fixed4 mainTex = tex2D(_MainTex, v2f.uv);

                //通过世界法线以及视角方向 计算反射向量
                //我们需要在这里计算反射向量 因为这时才把法线纹理中的法线信息 算出来
                float3 refl = reflect(-viewDir, worldNormal);
                //将反射颜色和主纹理颜色进行叠加
                fixed4 reflColor = texCUBE(_Cube, refl) * mainTex;

                // 不直接从结构体中拿世界反射向量进行计算了 而是从法线纹理中采样得出
                // //将反射颜色和主纹理颜色进行叠加
                // fixed4 reflColor = texCUBE(_Cube, v2f.worldRefl) * mainTex;

                //折射相关的颜色
                //其实就是从我们抓取的 屏幕渲染纹理中进行采样 参与计算
                //抓取纹理中的颜色信息 相当于是这个玻璃对象后面的颜色


                //不使用自定义计算这是程度的反复噶了
                // //想要有折射效果 可以在采样grabColor之前 进行xy屏幕坐标的偏移
                // //可以自己定规则
                // //比如折射程度为1时 相当于没有偏移 折射程度越小 偏移越大
                // float2 offset = 1 - _RefractAmount;
                // //xy偏移一个位置 自定义的 这样当有一定折射但又不是完全折射时 向右偏移
                // v2f.grabPos.xy = v2f.grabPos.xy - offset / 100;


                //使用图形学前辈们通过实践总结出来的接近真实世界折射效果的方法
                //使用切线空间下法线的xy * 扭曲值得到一个偏移量
                //代表光线经过法线方向扰动后的偏移程度,确定光线折射的方向和强度
                float2 offset = tangentNormal.xy * _Distortion;

                //用偏移量offset和屏幕空间深度值v2f.grabPos.z相乘,模拟出真实的折射效果
                //  深度值越大(即距离相机越远),折射效果越明显。
                //  这样可以实现近大远小的效果,使得物体在不同深度上的折射效果有所差异。
                //  屏幕坐标.xy = offset * 屏幕坐标.z + 屏幕坐标.xy;
                v2f.grabPos.xy = offset * v2f.grabPos.z + v2f.grabPos.xy;


                //利用透视除法 将屏幕坐标转换到 0~1范围内 然后再进行采样
                fixed2 screenUV = v2f.grabPos.xy / v2f.grabPos.w;
                //从捕获的渲染纹理中进行采样 获取后面的颜色
                // 在 _RefractAmount 接近 1 时,玻璃材质会逐渐变得透明,展示出后方的图像,实现玻璃折射的视觉效果。
                fixed4 grabColor = tex2D(_GrabTexture, screenUV);

                //折射程度 0~1 0代表完全反射(完全不折射)1代表完全折射(透明效果 相当于光全部进入了内部)
                float4 color = reflColor * (1 - _RefractAmount) + grabColor * _RefractAmount;

                return color;
            }
            ENDCG
        }
    }
}

在场景中创建一个平面,并创建一个使用新建Shader的材质球并关联对应纹理,修改参数可以看到已经非常接近水波效果了

实现水波效果

主要步骤

  • 属性调整

    • 添加用于控制动态效果的 x 与 y 轴方向的速度属性(如 _WaveXSpeed 和 _WaveYSpeed)。
    • 删除玻璃 Shader 中的折射属性(_RefractAmount),改为后续菲涅耳效果所需的反射率参数(_FresnelScale),并修改相应的属性映射。
  • 片元着色器修改

    • 将原有的法线采样修改为扰动算法,对噪声法线纹理进行正负偏移采样,采样后进行归一化叠加。
    • 使用扰动后的法线替代原先的切线空间法线,并重新计算世界空间下的法线。
    • 主纹理采样时加入速度偏移,实现动态效果。
    • 采用菲涅耳近似公式处理最终的反射和折射混合效果。

添加用于控制动态效果的x和y轴方向的速度属性,将玻璃种的折射属性删除,改为之后菲涅耳效果会用到的反射率。并进行属性映射

Properties
{
    //主纹理
    _MainTex("MainTex", 2D) = ""{}
    // 法线纹理
    _BumpMap("BumpMap", 2D) = ""{}
    //立方体纹理
    _Cube("Cubemap", Cube) = ""{}

    /*//折射程度 0~1 0代表完全反射(完全不折射)1代表完全折射(透明效果 相当于光全部进入了内部)
    _RefractAmount("RefractAmount", Range(0,1)) = 1*/

    //控制折射扭曲程度的变量
    _Distortion("Distortion", Range(0,10)) = 0

    //控制水波水平和竖直速度偏移的属性
    _WaveXSpeed("WaveXSpeed", Range(-0.1, 0.1)) = 0.01
    _WaveYSpeed("WaveYSpeed", Range(-0.1, 0.1)) = 0.01
    //用于计算菲涅耳效果的系数
    _FresnelScale("FresnelScale", Range(0,1)) = 1
}
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap; //法线纹理
float4 _BumpMap_ST; //法线纹理的缩放和平移
samplerCUBE _Cube;

// float _RefractAmount;

//GrabPass默认存储的纹理变量 这个是规则
sampler2D _GrabTexture;
float _Distortion;//控制折射扭曲程度的变量

fixed _WaveXSpeed;//水波X速度
fixed _WaveYSpeed;//水波Y速度
float _FresnelScale;//计算菲涅耳效果的系数

片元着色器中,加入的一个水波移动速度,得到法线bump。bump是作为新的法线 代替之前的tangentNormal。新的法线也要得到世界法线。主纹理采样时也要加上速度。处理菲涅耳效应,复制Lesson32_FresnelBase中菲涅耳近似公式,用计算出来的系数处理最终的折射和反射相关内容

fixed4 frag(v2f v2f):SV_TARGET
{
    //世界空间下视角方向
    // fixed3 viewDir = normalize(UnityWorldSpaceViewDir(v2f.worldPos));
    float3 worldPos = float3(v2f.TtoW0.w, v2f.TtoW1.w, v2f.TtoW2.w);
    fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));


    // //纹理采样函数tex2D
    // //通过纹理采样函数 取出法线纹理贴图当中的数据
    // float4 packedNormal = tex2D(_BumpMap, v2f.uv.zw);
    //
    // //利用内置的UnpackNormal函数对法线信息进行逆运算以及可能的解压
    // //将我们取出来的法线数据 进行逆运算并且可能会进行解压缩的运算,最终得到切线空间下的法线数据
    // float3 tangentNormal = UnpackNormal(packedNormal);
    //
    // // 没有处理凹凸程度 这一段注释
    // // //乘以凹凸程度的系数
    // // tangentNormal.xy *= _BumpScale;
    // // tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
    //
    //
    // //把计算完毕后的切线空间下的法线转换到世界空间下
    // // float3 worldNormal = mul(v2f.rotation, tangentNormal);
    //
    // //以下两种写法等价 但是可以一步到位
    // //float3x3 rotation = float3x3(v2f.TtoW0.xyz, v2f.TtoW1.xyz, v2f.TtoW2.xyz );
    // //float3 worldNormal = mul(rotation, tangentNormal);
    // //本质 就是在进行矩阵运算
    // float3 worldNormal = float3(dot(v2f.TtoW0.xyz, tangentNormal), dot(v2f.TtoW1.xyz, tangentNormal),
    //                             dot(v2f.TtoW2.xyz, tangentNormal));


    //加入的一个水波移动速度的感觉
    //这个是图形学前辈们总结的一套简单的扰动算法 可以让水波、火焰、玻璃折射等 出现扰动感(动态感)
    float2 speed = _Time.y * float2(_WaveXSpeed, _WaveYSpeed);
    fixed3 bump1 = UnpackNormal(tex2D(_BumpMap, v2f.uv.zw + speed)).rgb;
    fixed3 bump2 = UnpackNormal(tex2D(_BumpMap, v2f.uv.zw - speed)).rgb;
    fixed3 bump = normalize(bump1 + bump2); //bump是作为新的法线 代替之前的tangentNormal

    //把计算完毕后的切线空间下的法线转换到世界空间下
    //float3x3 rotation = float3x3(v2f.TtoW0.xyz, v2f.TtoW1.xyz, v2f.TtoW2.xyz );
    //float3 worldNormal = mul(rotation, bump);
    //本质 就是在进行矩阵运算
    float3 worldNormal = float3(dot(v2f.TtoW0.xyz, bump), dot(v2f.TtoW1.xyz, bump),
                    dot(v2f.TtoW2.xyz, bump));


    //反射颜色相关的计算 会叠加主纹理颜色
    //对颜色纹理进行采样
    // fixed4 mainTex = tex2D(_MainTex, v2f.uv);
    fixed4 mainTex = tex2D(_MainTex, v2f.uv + speed);

    //通过世界法线以及视角方向 计算反射向量
    //我们需要在这里计算反射向量 因为这时才把法线纹理中的法线信息 算出来
    float3 refl = reflect(-viewDir, worldNormal);
    //将反射颜色和主纹理颜色进行叠加
    fixed4 reflColor = texCUBE(_Cube, refl) * mainTex;

    // 不直接从结构体中拿世界反射向量进行计算了 而是从法线纹理中采样得出
    // //将反射颜色和主纹理颜色进行叠加
    // fixed4 reflColor = texCUBE(_Cube, v2f.worldRefl) * mainTex;

    //折射相关的颜色
    //其实就是从我们抓取的 屏幕渲染纹理中进行采样 参与计算
    //抓取纹理中的颜色信息 相当于是这个玻璃对象后面的颜色


    //不使用自定义计算这是程度的反复噶了
    // //想要有折射效果 可以在采样grabColor之前 进行xy屏幕坐标的偏移
    // //可以自己定规则
    // //比如折射程度为1时 相当于没有偏移 折射程度越小 偏移越大
    // float2 offset = 1 - _RefractAmount;
    // //xy偏移一个位置 自定义的 这样当有一定折射但又不是完全折射时 向右偏移
    // v2f.grabPos.xy = v2f.grabPos.xy - offset / 100;


    //使用图形学前辈们通过实践总结出来的接近真实世界折射效果的方法
    //使用切线空间下法线的xy * 扭曲值得到一个偏移量
    //代表光线经过法线方向扰动后的偏移程度,确定光线折射的方向和强度
    // float2 offset = tangentNormal.xy * _Distortion;
    float2 offset = bump.xy * _Distortion;

    //用偏移量offset和屏幕空间深度值v2f.grabPos.z相乘,模拟出真实的折射效果
    //  深度值越大(即距离相机越远),折射效果越明显。
    //  这样可以实现近大远小的效果,使得物体在不同深度上的折射效果有所差异。
    //  屏幕坐标.xy = offset * 屏幕坐标.z + 屏幕坐标.xy;
    v2f.grabPos.xy = offset * v2f.grabPos.z + v2f.grabPos.xy;


    //利用透视除法 将屏幕坐标转换到 0~1范围内 然后再进行采样
    fixed2 screenUV = v2f.grabPos.xy / v2f.grabPos.w;
    //从捕获的渲染纹理中进行采样 获取后面的颜色
    // 在 _RefractAmount 接近 1 时,玻璃材质会逐渐变得透明,展示出后方的图像,实现玻璃折射的视觉效果。
    fixed4 grabColor = tex2D(_GrabTexture, screenUV);

    //折射程度 0~1 0代表完全反射(完全不折射)1代表完全折射(透明效果 相当于光全部进入了内部)
    // float4 color = reflColor * (1 - _RefractAmount) + grabColor * _RefractAmount;


    //利用schlick菲涅耳近似等式 计算菲涅耳反射率
    fixed fresnel = _FresnelScale + (1 - _FresnelScale) * pow(
        1 - dot(normalize(viewDir), normalize(worldNormal)), 5);
    //把以前的折射率_RefractAmount改成计算出来的菲涅耳折射率fresnel
    //菲涅耳折射率为1 完全反射 菲涅耳折射率为0 完全折射
    float4 color = reflColor * fresnel + grabColor * (1 - fresnel);


    return color;
}

最终效果

运行后,在场景中创建的平面上使用该 Shader 的材质球会呈现出明显的水波动态效果。


21.2 知识点代码

Lesson21_WaterWave.shader

Shader "Unlit/Lesson21_WaterWave"
{
    Properties
    {
        //主纹理
        _MainTex("MainTex", 2D) = ""{}
        // 法线纹理
        _BumpMap("BumpMap", 2D) = ""{}
        //立方体纹理
        _Cube("Cubemap", Cube) = ""{}
        /*//折射程度 0~1 0代表完全反射(完全不折射)1代表完全折射(透明效果 相当于光全部进入了内部)
        _RefractAmount("RefractAmount", Range(0,1)) = 1*/
        //控制折射扭曲程度的变量
        _Distortion("Distortion", Range(0,10)) = 0

        //控制水波水平和竖直速度偏移的属性
        _WaveXSpeed("WaveXSpeed", Range(-0.1, 0.1)) = 0.01
        _WaveYSpeed("WaveYSpeed", Range(-0.1, 0.1)) = 0.01
        //用于计算菲涅耳效果的系数
        _FresnelScale("FresnelScale", Range(0,1)) = 1
    }
    SubShader
    {
        //将渲染队列改为透明的 目的是让玻璃对象 滞后渲染
        //能够捕获到之前正确的屏幕图像
        Tags
        {
            "RenderType"="Opaque" "Queue"="Transparent"
        }
        //使用它来捕获当前屏幕内容 并存储到默认的渲染纹理变量中
        GrabPass {}

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

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _BumpMap; //法线纹理
            float4 _BumpMap_ST; //法线纹理的缩放和平移
            samplerCUBE _Cube;
            // float _RefractAmount;
            //GrabPass默认存储的纹理变量 这个是规则
            sampler2D _GrabTexture;
            float _Distortion; //控制折射扭曲程度的变量

            fixed _WaveXSpeed; //水波X速度
            fixed _WaveYSpeed; //水波Y速度
            float _FresnelScale; //计算菲涅耳效果的系数

            struct v2f
            {
                //裁剪空间下的顶点坐标
                float4 pos:SV_POSITION;
                //用于存储从屏幕图像中采样的坐标(顶点相对于屏幕的位置)
                float4 grabPos:TEXCOORD0;
                //用于在颜色纹理中采样的UV坐标
                float4 uv:TEXCOORD1;

                // //世界空间下的反射向量
                // //我们将把反射向量的计算放在顶点着色器函数中 节约性能 表现效果也不会太差 肉眼几乎分辨不出来
                // float3 worldRefl:TEXCOORD2;

                //代表我们切线空间到世界空间的 变换矩阵的3行 三个变量的w存世界坐标
                float4 TtoW0:TEXCOORD2;
                float4 TtoW1:TEXCOORD3;
                float4 TtoW2:TEXCOORD4;
            };

            v2f vert(appdata_full appdata_full)
            {
                v2f v2f;
                //顶点坐标转换
                v2f.pos = UnityObjectToClipPos(appdata_full.vertex);

                //屏幕坐标转换相关的内容 注意传进去的是裁剪空间下的坐标
                v2f.grabPos = ComputeGrabScreenPos(v2f.pos);
                //uv坐标计算相关的内容
                v2f.uv.xy = appdata_full.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;


                //法线纹理的uv坐标计算
                v2f.uv.zw = appdata_full.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

                //计算反射光向量
                //1.计算世界空间下法线向量
                float3 worldNormal = UnityObjectToWorldNormal(appdata_full.normal);
                //2.世界空间下的顶点坐标
                fixed3 worldPos = mul(unity_ObjectToWorld, appdata_full.vertex).xyz;
                //3.计算视角方向 内部是用摄像机位置 - 世界坐标位置 
                fixed3 worldViewDir = UnityWorldSpaceViewDir(worldPos);

                // //4.计算反射向量
                // v2f.worldRefl = reflect(-worldViewDir, worldNormal);


                //把模型空间下的法线、切线转换到世界空间下
                // float3 worldNormal = UnityObjectToWorldNormal(appdata_base.normal);//上面已经计算了
                float3 worldTangent = UnityObjectToWorldDir(appdata_full.tangent);

                //计算副切线 计算叉乘结果后 垂直与切线和法线的向量有两条 通过乘以 切线当中的w,就可以确定是哪一条
                //cross是叉乘 appdata_full.tangent.w代表叉乘结果方向
                float3 worldBinormal = cross(normalize(worldTangent), normalize(worldNormal)) * appdata_full.tangent.w;

                //这个就是我们 切线空间到世界空间的 转换矩阵
                //       |    |     |
                //      切线 副切线 法线
                //       |    |     |
                //v2f.rotation = float3x3( worldTangent.x, worldBinormal.x,  worldNormal.x,
                //                          worldTangent.y, worldBinormal.y,  worldNormal.y,
                //                          worldTangent.z, worldBinormal.z,  worldNormal.z);
                v2f.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
                v2f.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
                v2f.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);


                return v2f;
            }

            fixed4 frag(v2f v2f):SV_TARGET
            {
                //世界空间下视角方向
                // fixed3 viewDir = normalize(UnityWorldSpaceViewDir(v2f.worldPos));
                float3 worldPos = float3(v2f.TtoW0.w, v2f.TtoW1.w, v2f.TtoW2.w);
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));


                // //纹理采样函数tex2D
                // //通过纹理采样函数 取出法线纹理贴图当中的数据
                // float4 packedNormal = tex2D(_BumpMap, v2f.uv.zw);
                //
                // //利用内置的UnpackNormal函数对法线信息进行逆运算以及可能的解压
                // //将我们取出来的法线数据 进行逆运算并且可能会进行解压缩的运算,最终得到切线空间下的法线数据
                // float3 tangentNormal = UnpackNormal(packedNormal);
                //
                // // 没有处理凹凸程度 这一段注释
                // // //乘以凹凸程度的系数
                // // tangentNormal.xy *= _BumpScale;
                // // tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
                //
                //
                // //把计算完毕后的切线空间下的法线转换到世界空间下
                // // float3 worldNormal = mul(v2f.rotation, tangentNormal);
                //
                // //以下两种写法等价 但是可以一步到位
                // //float3x3 rotation = float3x3(v2f.TtoW0.xyz, v2f.TtoW1.xyz, v2f.TtoW2.xyz );
                // //float3 worldNormal = mul(rotation, tangentNormal);
                // //本质 就是在进行矩阵运算
                // float3 worldNormal = float3(dot(v2f.TtoW0.xyz, tangentNormal), dot(v2f.TtoW1.xyz, tangentNormal),
                //                             dot(v2f.TtoW2.xyz, tangentNormal));


                //加入的一个水波移动速度的感觉
                //这个是图形学前辈们总结的一套简单的扰动算法 可以让水波、火焰、玻璃折射等 出现扰动感(动态感)
                float2 speed = _Time.y * float2(_WaveXSpeed, _WaveYSpeed);
                fixed3 bump1 = UnpackNormal(tex2D(_BumpMap, v2f.uv.zw + speed)).rgb;
                fixed3 bump2 = UnpackNormal(tex2D(_BumpMap, v2f.uv.zw - speed)).rgb;
                fixed3 bump = normalize(bump1 + bump2); //bump是作为新的法线 代替之前的tangentNormal

                //把计算完毕后的切线空间下的法线转换到世界空间下
                //float3x3 rotation = float3x3(v2f.TtoW0.xyz, v2f.TtoW1.xyz, v2f.TtoW2.xyz );
                //float3 worldNormal = mul(rotation, bump);
                //本质 就是在进行矩阵运算
                float3 worldNormal = float3(dot(v2f.TtoW0.xyz, bump), dot(v2f.TtoW1.xyz, bump),
                             dot(v2f.TtoW2.xyz, bump));


                //反射颜色相关的计算 会叠加主纹理颜色
                //对颜色纹理进行采样
                // fixed4 mainTex = tex2D(_MainTex, v2f.uv);
                fixed4 mainTex = tex2D(_MainTex, v2f.uv + speed);

                //通过世界法线以及视角方向 计算反射向量
                //我们需要在这里计算反射向量 因为这时才把法线纹理中的法线信息 算出来
                float3 refl = reflect(-viewDir, worldNormal);
                //将反射颜色和主纹理颜色进行叠加
                fixed4 reflColor = texCUBE(_Cube, refl) * mainTex;

                // 不直接从结构体中拿世界反射向量进行计算了 而是从法线纹理中采样得出
                // //将反射颜色和主纹理颜色进行叠加
                // fixed4 reflColor = texCUBE(_Cube, v2f.worldRefl) * mainTex;

                //折射相关的颜色
                //其实就是从我们抓取的 屏幕渲染纹理中进行采样 参与计算
                //抓取纹理中的颜色信息 相当于是这个玻璃对象后面的颜色


                //不使用自定义计算这是程度的反复噶了
                // //想要有折射效果 可以在采样grabColor之前 进行xy屏幕坐标的偏移
                // //可以自己定规则
                // //比如折射程度为1时 相当于没有偏移 折射程度越小 偏移越大
                // float2 offset = 1 - _RefractAmount;
                // //xy偏移一个位置 自定义的 这样当有一定折射但又不是完全折射时 向右偏移
                // v2f.grabPos.xy = v2f.grabPos.xy - offset / 100;


                //使用图形学前辈们通过实践总结出来的接近真实世界折射效果的方法
                //使用切线空间下法线的xy * 扭曲值得到一个偏移量
                //代表光线经过法线方向扰动后的偏移程度,确定光线折射的方向和强度
                // float2 offset = tangentNormal.xy * _Distortion;
                float2 offset = bump.xy * _Distortion;

                //用偏移量offset和屏幕空间深度值v2f.grabPos.z相乘,模拟出真实的折射效果
                //  深度值越大(即距离相机越远),折射效果越明显。
                //  这样可以实现近大远小的效果,使得物体在不同深度上的折射效果有所差异。
                //  屏幕坐标.xy = offset * 屏幕坐标.z + 屏幕坐标.xy;
                v2f.grabPos.xy = offset * v2f.grabPos.z + v2f.grabPos.xy;


                //利用透视除法 将屏幕坐标转换到 0~1范围内 然后再进行采样
                fixed2 screenUV = v2f.grabPos.xy / v2f.grabPos.w;
                //从捕获的渲染纹理中进行采样 获取后面的颜色
                // 在 _RefractAmount 接近 1 时,玻璃材质会逐渐变得透明,展示出后方的图像,实现玻璃折射的视觉效果。
                fixed4 grabColor = tex2D(_GrabTexture, screenUV);

                //折射程度 0~1 0代表完全反射(完全不折射)1代表完全折射(透明效果 相当于光全部进入了内部)
                // float4 color = reflColor * (1 - _RefractAmount) + grabColor * _RefractAmount;


                //利用schlick菲涅耳近似等式 计算菲涅耳反射率
                fixed fresnel = _FresnelScale + (1 - _FresnelScale) * pow(
                    1 - dot(normalize(viewDir), normalize(worldNormal)), 5);
                //把以前的折射率_RefractAmount改成计算出来的菲涅耳折射率fresnel
                //菲涅耳折射率为1 完全反射 菲涅耳折射率为0 完全折射
                float4 color = reflColor * fresnel + grabColor * (1 - fresnel);


                return color;
            }
            ENDCG
        }
    }
}

Lesson21_噪声_水波效果_具体实现.cs

using UnityEngine;

public class Lesson21_噪声_水波效果_具体实现 : MonoBehaviour
{
    void Start()
    {
        #region 知识回顾 水波效果 基本原理

        //1.水波效果 是什么
        //水波效果指在计算机图形学中模拟水面波纹的视觉效果
        //2.水波效果 基本原理
        //水波效果可以基于我们之前实现的带法线纹理的玻璃效果进行修改
        //通过添加噪声法线纹理结合Shader内置时间变量实现水波动态效果
        //加入菲涅耳计算公式实现水面的光学特性

        #endregion

        #region 知识点一 准备工作

        //1.导入噪声纹理和水面颜色纹理,设置噪声纹理配置,将其设置为从灰度中生成的法线纹理
        //2.复制Shader开发基础 Lesson40_高级纹理_渲染纹理_玻璃效果_带法线纹理实现的测试场景
        //3.新建Shader Lesson21_WaterWave 并删除其中无用代码 复制 Lesson40_GlassRefractionNormal 带法线纹理的玻璃效果
        //4.在场景中创建一个平面,并创建一个使用新建Shader的材质球并关联对应纹理

        #endregion

        #region 知识点二 实现水波效果

        //1.属性相关
        //  1-1.添加用于控制动态效果的x和y轴方向的速度属性
        //      _WaveXSpeed、_WaveXSpeed
        //  1-2.将玻璃种的折射属性删除,改为之后菲涅耳效果会用到的反射率
        //      删除_RefractAmount,添加_FresnelScale
        //  修改对应的属性映射
        //2.修改片元着色器
        //  2-1.将原来的法线相关采样 修改为扰动算法(对噪声法线纹理进行 + - 偏移采样,采样后叠加单位化)
        //  2-2.修改折射相关计算,将对应法线换为新法线
        //  2-3.修改反射相关计算,世界空间下法线用扰动后发现进行计算,使用它计算反射相关内容
        //  2-4.处理菲涅耳效应,复制Lesson32_FresnelBase中菲涅耳近似公式,用计算出来的系数处理最终的折射和反射相关内容

        #endregion
    }
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏