Unity Shader学习记录(三)

Unity Shader学习记录(三)

  前文中编写的Shader只有个漫反射颜色设置,渲染出来的模型是单调的纯色,而且虽然有点光照效果,但是看起来暗淡无光,总之就是不好看。在实际的游戏或者场景渲染中,模型的表面是多姿多彩的,光照也是复杂多变的,因此Shader往往需要具有处理这些数据的能力。


纹理贴图

  纹理和贴图都是在计算机图形图像中很常见的概念,简单说来它们指的就是一张图片,在渲染时根据模型的信息和贴图坐标匹配将图片上的像素渲染到对应的位置上。这样说起来似乎很简单,但对于计算机渲染来说,这样的一个步骤却包含了很多计算,所幸的是在编写Shader时并不需要考虑太多的细节,大部分计算都由硬件完成了,Shader所要做的就是把贴图坐标搞定。
  在前文中提到的漫反射模型能很轻松地应用到贴图渲染上,说白了所谓贴图渲染就是把图片里的像素颜色取出来,像是漫反射的颜色那样使用并且渲染到指定点上。这个过程仅仅比纯色漫反射计算多了一个步骤,那就是纹理采样。
  采样的概念来自数学

采样(sampling)也称取样,指把时间域或空间域的连续量转化成离散量的过程。

  而在计算机图形图像中,“采样”的概念是根据某种坐标信息从源数据中取得信息并且应用到目标数据上。
  一个添加了贴图的漫反射Shader编写如下

Shader "Custom/FinalTestShader" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
    }
    SubShader {
        Pass {
            Tags {"LightMode"="ForwardBase"}

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"

            float4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;

            struct a2v {
                float3 vertex : POSITION;
                float3 normal : NORMAL;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 worldNormal : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
            };

            v2f vert(a2v v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                return o;
            }

            fixed4 frag(v2f i) : SV_TARGET {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                fixed3 albedo = tex2D(_MainTex, i.uv) * _Color.rgb;
                fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(worldNormal, worldLightDir));
                return fixed4(ambient + diffuse, 1.0);
            }

            ENDCG
        }
    }
    FallBack "Diffuse"
}

  可以看得出来,大部分代码和纯色漫反射是一样的。
  在Properties段落里多了一个_MainTex字段,其类型被标记为2D,也就是指一个2D的纹理贴图,默认值为白色纯色图片。此后在代码里,使用一个sampler2D变量来指代这个2D纹理贴图,这个变量也被称为采样器。
  注意到除了采样器变量之外还有一个浮点四元组_MainTex_ST,这个四元组保存的是2D纹理的两组特殊参数,前两个是纹理的Tiling,也就是片重复数,它控制了XY两个轴向上的纹理采样速度,这个速度值越大,则对应轴向上每移动一个单位所对应的坐标移动距离越大,由于纹理本身有重复模式这个设定(Unity中在导入设置时可以看到相关设置),因此当坐标移动超过纹理范围后会根据重复模式的设定重新回到范围内。
  简单举例,如果重复模式为Repeat,则当任何一个轴向坐标超过1后会被裁掉整数部分,相当于重新回到坐标轴开头,这就会造成纹理在模型表面出现重复,像是瓦片一样排列起来。
  而四元组的后两个值是纹理的偏移参数,也就是说明采样从纹理贴图的哪个坐标开始。
  这个四元组可以自定义名称,但如果想要像代码中那样使用Unity自带的TRANSFORM_TEX宏来迅速计算uv坐标的话,那么必须按照“采样器名称_ST”这种格式来命名。
  随后最主要的几句代码就是在vert方法中的uv坐标计算

o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);

  还有frag方法中将原来的纯色漫反射替换为纹理采样的漫反射颜色

fixed3 albedo = tex2D(_MainTex, i.uv) * _Color.rgb;
fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(worldNormal, worldLightDir));

  注意到代码中有一个ambient的参数,这个是Unity的环境光参数,可以在项目设定中进行设置,使用它可以很轻松地更改全局光照颜色效果,不受场景光源的限制。
  经过这样的代码计算后,模型材质就会要求设置一张2D纹理贴图,如果不设置就是纯白,和原来的纯白色漫反射结果一样;加上贴图后模型便有了多样的变化。

闪亮的模型——高光

  现在Shader渲染了一张2D贴图到模型上,看起来有点游戏物体的意思了,但这个模型在场景中依然看起来很暗淡,没有什么亮眼的效果。使用漫反射模型虽然成功地让物体对光照产生了反应,但这个反应未免太单调太沉闷了一点,模型看起来就像是哑光材质的一样,多强的光线都照不出反光来。
  而现实生活中能找到大量的材质有很漂亮的反光效果,包括但不仅限于金属表面,光滑表面等情况;为了能够模拟出这种反光的效果来,需要用到另外一种光照模型,也就是“高光模型”。

高光是一种美术用语,指光源照射到物体然后反射到人的眼睛里时,物体上最亮的那个点就是高光,高光不是光,而是物体上最亮的部分。

  因此高光模型也是建立在光线的反射原理基础之上的,但这次不像漫反射那样考虑各个方向的反射,而是仅考虑一个方向的反射。物理上的镜面反射原理提到,光线在光滑表面的反射遵循简单的“入射角等于反射角”规律,这个所谓的入射角和反射角都是指入射/反射光线与平面法线的夹角,因此高光模型都需要跟顶点法线打交道。
  高光模型有各种各样的,但常用的高光模型有两种。这些高光模型都是经验模型,它们并不符合物理学上对反射光的定义和计算,仅仅是“看起来符合事实”的程度。
  第一种,Phong模型,基础高光模型,直接考虑入射光线的反射方向,并通过视角方向计算反射强度,定义计算公式如下:

cspecular=(clightmspecular)max(0,v^r^)mgloss

  其中 mgloss 为材质的“光泽度”,也可称为“反光度”,用于控制“高光区域”有多大,这个值越大,则高光区域越小,表示材质越光滑,反光情况更接近镜面。 v^ 则是视角向量,即从反射点指向摄像机的方向向量, r^ 是反射方向向量,它一般是通过计算得到的。

r^=2(n^l^)n^l^

  其中 n^ 是反射点的法向量, l^ 是指向光源的方向向量。
  第二种,Blinn模型,这种模型和Phong模型非常相似,但它没有使用物理上的反射光线方向,而是利用了视角与光源方向来计算得到一个中间方向,替代了反射方向参与计算,避免计算反射方向。
  其计算公式如下

cspecular=(clightmspecular)max(0,n^h^)mgloss

  而其中的 n^ 是法向量, h^ 则是通过计算得到的,其计算方式为

h^=v^+l^v^+l^

  可以看得出来,这样就避免了反射方向的计算。
  针对这两种模型,都放在一个Shader中展示,不同的部分会在代码中进行说明

Shader "Custom/FinalTestShader" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Specular ("Specular", Color) = (1,1,1,1)
        _Gloss ("Gloss", Range(1.0,128)) = 8.0
    }
    SubShader {
        Pass {
            Tags {"LightMode"="ForwardBase"}

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"

            float4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _Specular;
            float _Gloss;

            struct a2v {
                float3 vertex : POSITION;
                float3 normal : NORMAL;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 worldNormal : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
            };

            v2f vert(a2v v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                return o;
            }

            fixed4 frag(v2f i) : SV_TARGET {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                fixed3 albedo = tex2D(_MainTex, i.uv) * _Color.rgb;
                fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(worldNormal, worldLightDir));
                // Phong 模型
                fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
                // Blinn 模型
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);
                return fixed4(ambient + diffuse + specular, 1.0);
            }

            ENDCG
        }
    }
    FallBack "Diffuse"
}

  有了高光表达后,物体的表面光泽终于闪亮了起来,通过调整光泽度控制好反光的情况,物体看起来就像是有了金属材质那样,亮锃锃的。但与此同时也会有新的问题出现,有些物体的表面是有凹凸感的,比如砖石地面等情况;一般而言这种凹凸感可以通过非常精细的模型得以表达,换句话说既然物体表面凹凸不平,那么只要在模型上做出来不就好了。
  但这种做法极大地增加了模型的复杂度,用于静态渲染也许还可以,但是游戏的实时渲染往往是经受不住这样复杂的模型运算的,因此在游戏中都要求模型尽量简单,比如一堵墙,一段路面,最好就是简单几何体,或者只有寥寥几个平面组成的几何体。
  可这样一来凹凸感该如何体现呢?有了光照模型之后,这个答案其实并不复杂,所谓的凹凸感说穿了不就是物体表面的法线不停变化吗?既然如此在渲染平面时从外部文件信息中获取某个点的法线信息不就好了吗?
  事实上这也就是现在常见的一种渲染技术,“法线贴图”技术,原理简单,效果令人满意,下面就会解析这种技术在Shader中的实现。

猜你喜欢

转载自blog.csdn.net/soul900524/article/details/79425677