zifa2003293 发表于 2022-11-18 17:46

《Unity Shader入门精要》笔记(三十)

本文为《Unity Shader入门精要》第十五章《使用噪声》的第二节内容《水波效果》。
本文相关代码,详见:

原书代码,详见原作者github:
<hr/>1. 概念原理

1.1 使用噪声纹理

模拟实时水面的过程中,通常会使用噪声纹理作为一个高度图,以不断修改水面的法线方向。为了让水面流动起来,会以时间变量作为偏移对噪声纹理进行采样。得到法线后,再进行正常的反射+折射计算,得到最后的水面波动效果。

1.2 菲涅尔反射

案例中使用了菲尼尔反射的水面效果,所以除了修改水面的法线方向外,还需要模拟反射和折射的效果。另外会使用第10章提到的菲涅尔系数冬天来决定反射和折射的混合程度。
// v:视角方向
// n:法线方向
// 夹角越小,fresnel值越小,反射越弱,折射越强
fresnel = pow(1 - max(0, v·n), 4)

1.3 模拟反射

案例中将使用一张立方体纹理(CubMap)作为环境纹理,模拟水面反射周围物体的效果。

1.4 模拟折射

使用GrabPass获取当前屏幕的渲染纹理,并使用切线空间下的法线对像素的屏幕坐标进行偏移,使用偏移后的坐标对渲染纹理进行屏幕采样,从而模拟折射的效果。

2.案例:水波效果的实现

2.1 效果预览

本案例实现效果如下:



2.2 准备工作

完成如下准备工作:

[*]新建名为Scene_15_2的场景,并去掉天空盒;
[*]搭建水波效果的测试场景,构建一个由6面墙围成的封闭房间,房间中防止一个平面来模拟水面,详情可参考文章开头链接里的工程;
[*]新建名为WaterWaveMat的材质,并赋给上一步的平面(水面);
[*]新建名为Chapter15-WaterWave的Unity Shader,并赋给上一步的材质;
[*]使用10.1.2节中实现的创建立方体纹理的脚本创建一个立方体纹理,用于得到本场景适用的环境纹理:

[*]在Project创建一张名为Wave_Cubemap的立方体纹理,创建方法右键-Create-Legacy-Cubemap;
[*]点击菜单栏GameObject-Render into CubeMap,在弹出的窗口中,将场景中的点光源拖入Render From Position属性中,将上一步新建的立方体纹理拖入Cubemap属性中;
[*]点击Render!按钮,可将周围环境渲染到立方体纹理中;

[*]保存场景。

在渲染立方体纹理时,需要注意的是:为了得到比较亮丽的效果,可适当将点光源强度加强,渲染后再重置强度数值。



2.3 编写Shader:Chapter15-WaterWave

编写如下代码:
Shader "Unity Shaders Book/Chapter 15/Water Wave"
{
    Properties
    {
      // 控制水面颜色
      _Color ("Main Color", Color) = (0.0, 0.15, 0.115, 1.0)
      // 水面博文材质纹理,默认为白色纹理
      _MainTex ("Base (RGB)", 2D) = "white" {}
      // 由噪声纹理生成的法线纹理
      _WaveMap ("Wave Map", 2D) = "bump" {}
      // 用于模拟反射的立方体纹理
      _Cubemap ("Environment Map", Cube) = "SkyBox" {}
      // 法线纹理在X方向上的平移速度
      _WaveXSpeed ("Wave Horizontal Speed", Range(-0.1, 0.1)) = 0.01
      // 法线纹理在Y方向上的平移速度
      _WaveYSpeed ("Wave Vertical Speed", Range(-0.1, 0.1)) = 0.01
      // 控制模拟折射时图像的扭曲程度
      _Distortion ("Distortion", Range(0, 100)) = 10
    }

    SubShader
    {
      Tags
      {
            // 确保水面被渲染时,其他所有不透明物体都已经被渲染到屏幕上
            // 达到透过水面看其他物体的效果
            "Queue" = "Transparent"
            // 为了在使用着色器替换(Shader Replacement)时,水面在需要时被正确替换
            "RenderType" = "Opaque"
      }

      // 通过关键词GrabPass,定义一个获取屏幕图像的Pass
      GrabPass
      {
            // 抓取到的屏幕图像存如这个变量名定义的纹理中
            "_RefractionTex"
      }

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

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #pragma multi_compile_fwdbase

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

            struct appdata
            {
                float4 vertex : POSITION;
                float4 texcoord : TEXCOORD0;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                // 当前片元对应屏幕图像中的位置
                float4 scrPos : TEXCOORD0;
                // uv是4维向量,xy存储水面波纹纹理的uv值,zw存储噪声法线纹理的uv值
                float4 uv : TEXCOORD1;
                // 使用3个4维向量存储切线空间转世界空间的3X3的转换矩阵
                // 为了充分利用存储变量,用它们的w分量存储当前片元在世界空间中的位置
                float4 TtoW0 : TEXCOORD2;
                float4 TtoW1 : TEXCOORD3;
                float4 TtoW2 : TEXCOORD4;
            };

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _WaveMap;
            float4 _WaveMap_ST;
            // 立方体纹理,用于周围环境的发射
            samplerCUBE _Cubemap;
            fixed _WaveXSpeed;
            fixed _WaveYSpeed;
            float _Distortion;
            sampler2D _RefractionTex;
            // 折射纹理的纹素大小,做屏幕图像采样偏移时会用到这个值
            float4 _RefractionTex_TexelSize;

            v2f vert (appdata v)
            {
                v2f o;

                o.pos = UnityObjectToClipPos(v.vertex);
                // 使用内置的ComputeGrabScreenPos函数,得到当前顶点对应屏幕空间的坐标
                o.scrPos = ComputeGrabScreenPos(o.pos);

                // 水面波纹纹理的uv值
                o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
                // 噪声法线纹理的uv值
                o.uv.zw = TRANSFORM_TEX(v.texcoord, _WaveMap);

                // 当前顶点在世界空间下的坐标
                float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                // 使用内置的UnityObjectToWorldNormal函数,得到世界空间的法线方向
                fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
                // 使用内置的UnityObjectToWorldDir函数,得到世界空间的切线方向
                fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
                // 由法线方向和切线方向叉乘得到副法线的方向,
                // 因为垂直于平面的射线有两个方向,切线方向的w分量决定了叉乘后唯一方向
                fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;

                // 构建切线空间转世界空间的矩阵
                // 将矩阵的每一行分别存入TtoW0、TtoW1和TtoW2的x、y、z分量中
                // 数学方法:
                // 得到切线空间下的3个坐标轴(x、y、z轴分别对应切线、副切线和法线的方向)在世界空间下的表示,
                // 把它们依次按列组成一个变换矩阵,即可得到切线空间转世界空间的矩阵
                // 为了把三个变量的w分量用起来,将世界空间的顶点左边存入其中
                o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
                o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
                o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // 片元在世界空间的坐标
                float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
                // 使用内置的UnityWorldSpaceViewDir,得到世界空间的视角方向
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));

                // 通过时间变量及UV偏移速度,得到噪声法线纹理的uv偏移量
                float2 waveUvOffset = _Time.y * float2(_WaveXSpeed, _WaveYSpeed);
                // 对噪声法线纹理进行两次采样,是为了模拟两层交叉的水波波动的效果
                fixed3 bump1 = UnpackNormal(tex2D(_WaveMap, i.uv.zw + waveUvOffset)).rgb;
                fixed3 bump2 = UnpackNormal(tex2D(_WaveMap, i.uv.zw - waveUvOffset)).rgb;
                // 对两次采样结果相加并归一化后得到切线空间下的法线方向
                fixed3 bump = normalize(bump1 + bump2);

                // 基于法线方向的xy分量乘以折射程度系数,再乘以折射纹理的纹素大小,得到折射纹理的采样偏移量
                float2 refractOffset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
                // 采样偏移量乘以屏幕空间的z值,是为了模拟深度越大,折射程度越大的效果
                i.scrPos.xy = i.scrPos.xy + refractOffset * i.scrPos.z;
                // 对屏幕空间坐标做透视除法后,对折射纹理进行采样
                fixed3 refractColor = tex2D(_RefractionTex, i.scrPos.xy / i.scrPos.w).rgb;

                // 使用转换矩阵,将法线由切线空间转到世界空间
                bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
                // 基于uv偏移量,对水面波纹纹理采样
                fixed4 texColor = tex2D(_MainTex, i.uv.xy + waveUvOffset);
                // 基于世界空间的视角方向和法线方向,使用内置的reflect函数得到反射方向
                // (注意:视角方向要取反,由片元向外的方向)
                fixed3 reflectDir = reflect(-viewDir, bump);
                // 基于反射方向,对立方体纹理进行采样,采样结果乘以波纹的颜色,再乘以叠加的颜色
                fixed3 reflectColor = texCUBE(_Cubemap, reflectDir).rgb * texColor.rgb * _Color.rgb;
               
                // 根据视角方向和法线方向的夹角大小,计算菲涅尔系数
                fixed fresnel = pow(1 - saturate(dot(viewDir, bump)), 4);
                // 根据菲涅尔系数,将反射颜色和折射颜色进行混合,得到最终的颜色
                fixed3 finalColor = reflectColor * fresnel + refractColor * (1.0 - fresnel);

                return fixed4(finalColor, 1.0);
            }
            ENDCG
      }
    }

    // 不投射阴影,设为其他的话,会让水面投射阴影,看起来水面不亮堂
    Fallback Off
}

2.4 配置材质

配置如下材质,可得到最终效果,配置过程中涉及的水波纹理和噪声法线纹理可在文章开头链接的工程中找到。



以上是本次笔记的所有内容,下一篇笔记我们将学习《再谈全局雾效》的相关知识。

<hr/>
写在最后

本文内容会同步发在笔者的公众号上,欢迎大家关注交流!
公众号:程序员叨叨叨(ID:i_coder)
页: [1]
查看完整版本: 《Unity Shader入门精要》笔记(三十)