40.玻璃效果带法线纹理实现

  1. 40.高级纹理-渲染纹理-玻璃效果-带法线纹理实现
    1. 40.1 知识点
      1. 什么是带法线纹理的玻璃效果
      2. 带法线纹理的玻璃效果实现
        1. 主要步骤
        2. 创建Shader Lesson40_GlassRefractionNormal 复制Lesson39_高级纹理_渲染纹理_玻璃效果_基础实现的Lesson39_GlassBase
        3. 将Lesson20_标准光照着色器_标准漫反射中Lesson20_BumpedDiffuseShader中关于法线相关的计算整合进来,可以逐步复制Lesson20_BumpedDiffuseShade相关代码
          1. 添加法线纹理属性以及映射
          2. v2f结构体中,把uv改成float4,同时添加三个代表切线空间到世界空间变换矩阵变量。注意TEXCOORD序号
          3. 顶点函数中对法线纹理的uv坐标进行计算,同时计算切线和副切线(法线已经计算了),构造切线空间到世界空间的变换矩阵。注意,参数要改成appdata_full,否则没有appdata_full.tangent变量,无法得到副切线
          4. 片元函数准备好世界空间下的视角方向。对法线纹理进行采样。由于没有处理凹凸程度,对凹凸程度相关代码注释。把计算完毕后的切线空间下的法线转换到世界空间下,得到法线纹理在世界的法线。世界视角方向和世界法线等一下计算反射向量有用。
        4. 修改反射向量计算规则,由于法线需要从法线纹理中获取,而不是在顶点着色器中计算,因此需要将反射向量的计算放入到片元着色器中
          1. 结构体中注释掉世界空间下的反射向量,因为不需要在顶点函数计算了,要在片元函数计算
          2. 顶点函数注释掉计算反射向量的代码
          3. 片元函数不直接从结构体中拿世界反射向量进行计算了,而是通过世界法线以及视角方向计算反射向量。通过反射向量采样得到反射颜色并和主纹理颜色进行叠加
        5. 创建shader和材质赋值并查看效果。修改折射程度可以看效果。虽然我们在这前当中没有去处理光照的效果,但是看起来其实就是有对应的凹凸效果。之前我们的凹效果是由光照带来的,但现在这凹凸效果其实因为我们用法线去处理反射相关的向量。在反射向量的这个采样后,取出来反射颜色也能给它带来凹凸感。由于有法线产生的凹凸不平的效果,就不会像上节课一样是镜面反射了。
        6. 但是现在仍然存在问题,因为现在是凹凸不平的效果,但是折射算法还是和以前一样。正常来说折射过来的光线要有扭曲感,里面的球体不应该是圆形的,而是扭曲的。为了实现更真实的折射效果,我们要利用切线空间法线来计算折射偏移
      3. 利用切线空间法线来计算折射偏移
        1. 主要步骤
        2. 加入一个控制折射扭曲程度的新属性_Distortion 和映射取值范围可以大一些
        3. 使用切线空间法线计算偏移值。这种计算方法基于图形学领域长期的实践总结,接近真实的折射效果。首先,使用切线空间下的法线的 x 和 y 分量乘以扭曲系数 _Distortion 得到偏移量:float2 offset = tangentNormal.xy * _Distortion;。这个偏移量表示光线经过法线方向扰动后的偏移程度,用来确定光线折射的方向和强度。接着,将屏幕坐标的 x 和 y 分量加上偏移值与深度值的乘积:屏幕坐标.xy = offset * 屏幕坐标.z + 屏幕坐标.xy;。通过偏移量与屏幕空间深度值相乘,可以模拟出真实的折射效果。深度值越大(即距离相机越远),折射效果越明显,从而实现近大远小的视觉效果,使得物体在不同深度下的折射效果有所变化
        4. 修改扭曲程度,查看效果
    2. 40.2 知识点代码
      1. Lesson40_高级纹理_渲染纹理_玻璃效果_带法线纹理实现.cs
      2. Lesson40_GlassRefractionNormal.shader

40.高级纹理-渲染纹理-玻璃效果-带法线纹理实现


40.1 知识点

什么是带法线纹理的玻璃效果

在上节课中,我们实现了一个基础的玻璃效果,但这个效果缺少法线纹理的支持。法线纹理指的是模型上带有的法线贴图,用来模拟表面细节的凹凸效果。添加法线纹理后的玻璃折射效果会更复杂,因为法线贴图的凹凸会影响光线的折射路径。

例如,如果玻璃表面有凹凸的格子效果,当我们透过它观察时,玻璃的折射将更加不规则,呈现出不同角度的扭曲效果。为了实现这种真实的凹凸感,光线折射的计算方式需要相应调整。因此,带法线纹理的玻璃效果即通过法线贴图,增加表面细节,为玻璃材质带来更真实的扭曲和折射体验。

带法线纹理的玻璃效果实现

主要步骤

  1. 创建 Shader Lesson40_GlassRefractionNormal 复制 Lesson39_高级纹理_渲染纹理_玻璃效果_基础实现 的 Shader
  2. Lesson20_标准光照着色器_标准漫反射Lesson20_BumpedDiffuseShader 中关于法线相关的计算整合进来。注意:不再需要 _BumpScale 来控制凹凸程度,我们默认法线凹凸程度最大化
  3. 修改反射向量计算规则。由于法线需要从法线纹理中获取,因此需要将反射向量的计算放入到片元着色器中

创建Shader Lesson40_GlassRefractionNormal 复制Lesson39_高级纹理_渲染纹理_玻璃效果_基础实现的Lesson39_GlassBase

Shader "Unlit/Lesson40_GlassRefractionNormal"
{
    Properties
    {
        //主纹理
        _MainTex("MainTex", 2D) = ""{}
        //立方体纹理
        _Cube("Cubemap", Cube) = ""{}
        //折射程度 0~1 0代表完全反射(完全不折射)1代表完全折射(透明效果 相当于光全部进入了内部)
        _RefractAmount("RefractAmount", 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;
            samplerCUBE _Cube;
            float _RefractAmount;

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

            struct v2f
            {
                //裁剪空间下的顶点坐标
                float4 pos:SV_POSITION; 
                //用于存储从屏幕图像中采样的坐标(顶点相对于屏幕的位置)
                float4 grabPos:TEXCOORD0;
                //用于在颜色纹理中采样的UV坐标
                float2 uv:TEXCOORD1;
                //世界空间下的反射向量
                //我们将把反射向量的计算放在顶点着色器函数中 节约性能 表现效果也不会太差 肉眼几乎分辨不出来
                float3 worldRefl:TEXCOORD2;
            };

            v2f vert(appdata_base appdata_base)
            {
                v2f v2f;
                //顶点坐标转换
                v2f.pos = UnityObjectToClipPos(appdata_base.vertex);
                
                //屏幕坐标转换相关的内容 注意传进去的是裁剪空间下的坐标
                v2f.grabPos = ComputeGrabScreenPos(v2f.pos);
                //uv坐标计算相关的内容
                v2f.uv.xy = appdata_base.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                
                //计算反射光向量
                //1.计算世界空间下法线向量
                float3 worldNormal = UnityObjectToWorldNormal(appdata_base.normal);
                //2.世界空间下的顶点坐标
                fixed3 worldPos = mul(unity_ObjectToWorld, appdata_base.vertex).xyz;
                //3.计算视角方向 内部是用摄像机位置 - 世界坐标位置 
                fixed3 worldViewDir = UnityWorldSpaceViewDir(worldPos);
                //4.计算反射向量
                v2f.worldRefl = reflect(-worldViewDir, worldNormal);

                return v2f;
            }

            fixed4 frag(v2f v2f):SV_TARGET
            {
                //反射颜色相关的计算 会叠加主纹理颜色
                //对颜色纹理进行采样
                fixed4 mainTex = tex2D(_MainTex, v2f.uv);
                //将反射颜色和主纹理颜色进行叠加
                fixed4 reflColor = texCUBE(_Cube, v2f.worldRefl) * mainTex;

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

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

                //利用透视除法 将屏幕坐标转换到 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
        }
    }
}

将Lesson20_标准光照着色器_标准漫反射中Lesson20_BumpedDiffuseShader中关于法线相关的计算整合进来,可以逐步复制Lesson20_BumpedDiffuseShade相关代码

添加法线纹理属性以及映射
Properties
{
    //主纹理
    _MainTex("MainTex", 2D) = ""{}
    
    //  法线纹理
    _BumpMap("BumpMap", 2D) = ""{}
    
    //立方体纹理
    _Cube("Cubemap", Cube) = ""{}
    //折射程度 0~1 0代表完全反射(完全不折射)1代表完全折射(透明效果 相当于光全部进入了内部)
    _RefractAmount("RefractAmount", Range(0,1)) = 1
}
sampler2D _MainTex;
float4 _MainTex_ST;

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

samplerCUBE _Cube;
float _RefractAmount;
//GrabPass默认存储的纹理变量 这个是规则
sampler2D _GrabTexture;
v2f结构体中,把uv改成float4,同时添加三个代表切线空间到世界空间变换矩阵变量。注意TEXCOORD序号
struct v2f
{
    //裁剪空间下的顶点坐标
    float4 pos:SV_POSITION;
    //用于存储从屏幕图像中采样的坐标(顶点相对于屏幕的位置)
    float4 grabPos:TEXCOORD0;
    //用于在颜色纹理中采样的UV坐标
    float4 uv:TEXCOORD1;
    //世界空间下的反射向量
    //我们将把反射向量的计算放在顶点着色器函数中 节约性能 表现效果也不会太差 肉眼几乎分辨不出来
    float3 worldRefl:TEXCOORD2;

    //代表我们切线空间到世界空间的 变换矩阵的3行 三个变量的w存世界坐标
    float4 TtoW0:TEXCOORD3;
    float4 TtoW1:TEXCOORD4;
    float4 TtoW2:TEXCOORD5;
};
顶点函数中对法线纹理的uv坐标进行计算,同时计算切线和副切线(法线已经计算了),构造切线空间到世界空间的变换矩阵。注意,参数要改成appdata_full,否则没有appdata_full.tangent变量,无法得到副切线
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);
    //将反射颜色和主纹理颜色进行叠加
    fixed4 reflColor = texCUBE(_Cube, v2f.worldRefl) * mainTex;

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

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

    //利用透视除法 将屏幕坐标转换到 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;
}

修改反射向量计算规则,由于法线需要从法线纹理中获取,而不是在顶点着色器中计算,因此需要将反射向量的计算放入到片元着色器中

结构体中注释掉世界空间下的反射向量,因为不需要在顶点函数计算了,要在片元函数计算
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;

    //利用透视除法 将屏幕坐标转换到 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;
}

创建shader和材质赋值并查看效果。修改折射程度可以看效果。虽然我们在这前当中没有去处理光照的效果,但是看起来其实就是有对应的凹凸效果。之前我们的凹效果是由光照带来的,但现在这凹凸效果其实因为我们用法线去处理反射相关的向量。在反射向量的这个采样后,取出来反射颜色也能给它带来凹凸感。由于有法线产生的凹凸不平的效果,就不会像上节课一样是镜面反射了。



但是现在仍然存在问题,因为现在是凹凸不平的效果,但是折射算法还是和以前一样。正常来说折射过来的光线要有扭曲感,里面的球体不应该是圆形的,而是扭曲的。为了实现更真实的折射效果,我们要利用切线空间法线来计算折射偏移

利用切线空间法线来计算折射偏移

主要步骤

  1. 加入控制折射扭曲程度的新属性
    新增一个名为 _Distortion 的属性,用于控制折射的扭曲程度。该属性的取值范围可以较大,以便更灵活地调整折射效果。

  2. 利用切线空间下法线来计算偏移值
    使用以下两行关键代码计算偏移量:

    • float2 offset = tangentNormal.xy * _Distortion;
      通过切线空间下法线的 xy 分量与扭曲系数 _Distortion 相乘,得到一个偏移量。这个偏移量表示光线经过法线方向扰动后的偏移程度,进而决定折射光线的方向和强度。

    • 屏幕坐标.xy = offset * 屏幕坐标.z + 屏幕坐标.xy;
      将偏移量与屏幕空间的深度值(屏幕坐标.z)相乘,用以模拟真实的折射效果。深度值越大(即离相机越远),折射效果越明显。通过这种方式,能够实现类似“近大远小”的视觉效果,使得物体在不同深度上表现出不同的折射效果。

  • 这种计算方法源于图形学前辈们的实践总结,旨在尽可能逼近真实世界中的折射现象。

加入一个控制折射扭曲程度的新属性_Distortion 和映射取值范围可以大一些

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
}
sampler2D _MainTex;
float4 _MainTex_ST;

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

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

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

使用切线空间法线计算偏移值。这种计算方法基于图形学领域长期的实践总结,接近真实的折射效果。首先,使用切线空间下的法线的 x 和 y 分量乘以扭曲系数 _Distortion 得到偏移量:float2 offset = tangentNormal.xy * _Distortion;。这个偏移量表示光线经过法线方向扰动后的偏移程度,用来确定光线折射的方向和强度。接着,将屏幕坐标的 x 和 y 分量加上偏移值与深度值的乘积:屏幕坐标.xy = offset * 屏幕坐标.z + 屏幕坐标.xy;。通过偏移量与屏幕空间深度值相乘,可以模拟出真实的折射效果。深度值越大(即距离相机越远),折射效果越明显,从而实现近大远小的视觉效果,使得物体在不同深度下的折射效果有所变化

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;
}

修改扭曲程度,查看效果



40.2 知识点代码

Lesson40_高级纹理_渲染纹理_玻璃效果_带法线纹理实现.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson40_高级纹理_渲染纹理_玻璃效果_带法线纹理实现 : MonoBehaviour
{
    void Start()
    {
        #region 知识点一 什么是带法线纹理的玻璃效果

        // 在上节课中,我们实现了一个基础的玻璃效果
        // 但这个效果缺少法线纹理的支持
        // 法线纹理指的是模型上带有的法线贴图,用来模拟表面细节的凹凸效果。
        // 添加法线纹理后的玻璃折射效果会更复杂,因为法线贴图的凹凸会影响光线的折射路径。
        
        // 例如,如果玻璃表面有凹凸的格子效果,当我们透过它观察时,玻璃的折射将更加不规则,呈现出不同角度的扭曲效果。
        // 为了实现这种真实的凹凸感,光线折射的计算方式需要相应调整。
        // 因此,带法线纹理的玻璃效果即通过法线贴图,增加表面细节,为玻璃材质带来更真实的扭曲和折射体验。
        
        #endregion

        #region 知识点二 带法线纹理的玻璃效果实现

        //1.创建Shader Lesson40_GlassRefractionNormal 复制Lesson39_高级纹理_渲染纹理_玻璃效果_基础实现的Shader

        //2.将Lesson20_标准光照着色器_标准漫反射中Lesson20_BumpedDiffuseShader中关于法线相关的计算整合进来
        //  注意:不再需要_BumpScale来控制凹凸程度,我们默认法线凹凸程度最大化

        //3.修改反射向量计算规则
        //  由于法线需要从法线纹理中获取
        //  因此需要将反射向量的计算放入到片元着色器中

        #endregion

        #region 知识点三 利用切线空间法线来计算折射偏移

        //1.加入一个控制折射扭曲程度的新属性_Distortion 取值范围可以大一些
        //2.利用切线空间下法线来计算偏移值
        //  加入两行关键代码
        //  第一行
        //  float2 offset = tangentNormal.xy * _Distortion ;
        //  使用切线空间下法线的xy * 扭曲值得到一个偏移量 
        //  代表光线经过法线方向扰动后的偏移程度,确定光线折射的方向和强度

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

        //  这种计算方式是
        //  图形学前辈们通过实践总结出来的接近真实世界折射效果的方法

        #endregion
    }
}

Lesson40_GlassRefractionNormal.shader

Shader "Unlit/Lesson40_GlassRefractionNormal"
{
    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
        }
    }
}


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

×

喜欢就点赞,疼爱就打赏