前言

这个并不是技术分享的教程类博客,只是我记录的笔记而已。不,不要误会了!(虽然溜一遍下来发现好像也能看

纹理概念

纹理简介

纹理最初目的是使用一张图片控制模型的外观。在美术人员建模时会利用纹理展开技术把纹理映射坐标存储在每个顶点上。这意味着二位纹理坐标都代表着3D建模中的一个顶点。而这个坐标采用的是UV坐标(U为横轴V为纵轴)。纹理大小虽然多种多样,但是坐标都归一化到[0,1]范围内。但也有在这个范围外的,它们决定渲染引擎在超出这个范围时如何进行纹理采样。

纹理窗口简介

纹理窗口

Texure Type:纹理的类型,选择合适的类型可以让Unity为Shader传递正确的纹理,并对纹理进行优化;

Wrap Mode:范围超过[0,1]会如何处理(被平铺):

Repeat:重复之前的纹理(舍去整数部分继续采样,所以相当于重复来了一遍);

Clamp:超过1就按1来算,超过0就按0来算(这样就一直是1或者0的颜色了)

Filter Mode:当纹理因变化产生拉伸时会采用什么滤波模式。(滤波效果:Point<Bilinear<Trilinear,但是耗费性能也依次增大)

单张纹理实践

我们在光照时提到了物体的漫反射颜色。而我们通常会使用一张纹理来代替漫反射颜色。以下就是运用Blinn-Phong模型来计算光照的单张纹理:

Shader "Custom/Single Texture"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        //控制物体渲染的整体色调
        _MainTex ("Main Tex", 2D) = "white" {}
        //一个2D纹理,赋值为白色,从而它一开始就是个全白的纹理
        _Specular("Specular", Color)=(1,1,1,1)
        _Gloss ("Gloss", Range(8.0,256))=20
    }
    SubShader
    {
        pass{
            Tags{"LightMode"="ForwardBase"}
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            //在声明纹理变量的同时,需要声明 纹理名称_ST 这一float4变量
            //作用是存储该纹理的缩放和平移值
            //xy值为缩放值,zw为平移值,可以在纹理属性窗口中调节
            fixed4 _Specular;
            float _Gloss;

            struct a2v
            {
                float4 vertex:POSITION;
                float3 normal:NORMAL;
                float4 texcoord:TEXCOORD0;
                //将模型的第一组纹理坐标存储到该变量中
            };

            struct v2f
            {
                float4 pos:SV_POSITION;
                float3 worldNormal:TEXCOORD0;
                float3 worldPos:TEXCOORD1;
                float2 uv:TEXCOORD2;
                //使用该坐标进行纹理采样
            };

            v2f vert(a2v v)
            {
                v2f o;
                o.pos=UnityObjectToClipPos(v.vertex);
                o.worldNormal=UnityObjectToWorldNormal(v.normal);
                o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
                o.uv=v.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw;
                //由于纹理属性存在缩放和偏移值,所以让纹理坐标先乘以缩放值,再加上偏移值。这才是真正的纹理采样uv坐标
                //Unity对上述操作有个专门的宏:
                //o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
                return o;
            }

            fixed4 frag(v2f i):SV_Target
            {
                fixed3 worldNormal=normalize(i.worldNormal);
                fixed3 worldLightDir=normalize(UnityWorldSpaceLightDir(i.worldPos));
                fixed3 albedo=tex2D(_MainTex,i.uv).rgb*_Color.rgb;
                //对纹理进行采样,将返回的纹素值乘以物体的整体色调
                //albedo为材质的反射率
                fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
                fixed3 diffuse=_LightColor0.rgb*albedo*max(0,dot(worldNormal,worldLightDir));
                //通过反射率计算得到漫反射
                fixed3 viewDir=normalize(UnityWorldSpaceViewDir(i.worldPos));
                fixed3 halfDir=normalize(worldLightDir+viewDir);
                fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(max(0,dot(worldNormal,worldLightDir)),_Gloss);
                //常规方法拿下高光反射
                return fixed4(ambient+diffuse+specular,1.0);
            }
            ENDCG
            }
    }
    FallBack "Specular"
}

**o.uv=TRANSFORM_TEX(_MainTex,i.uv)**:将纹理坐标进行缩放和偏移

**tex2D(_MainTex,i.uv)**:根据uv内的坐标进行纹理采样,并返回纹素值

凹凸映射

概念

指使用一张纹理修改模型表面法线,让模型看起来“凹凸不平”但是并没有改变模型的顶点位置。实现这个方法的原理有两个:

高度纹理

在基础纹理上加了一张高度纹理。在高度图里存储强度值,颜色越浅月向外凸起。它可以非常直观地知道一个模型表面的凹凸情况,但计算很复杂,需要通过像素灰度值计算表面法线,因此需要消耗更多的性能。

法线纹理

概念

在基础纹理上加了一张法线纹理。法线纹理存储着表面的法线方向,而法线方向范围是[-1,1],像素范围是[0,1],所以需要做一个映射:

$$pixel=\frac {nromal+1}{2}$$

而反过来就是:

$$normal=pixel*2-1$$

那么问题来了,切线方向应该在哪个空间呢?下面有几个解决方案:

模型空间法线纹理

在模型空间下存储法线方向。法线纹理应该是五颜六色的,因为每个点存储的法线方向是各异的,经过映射后作为RGB颜色存储到纹理内也就是五颜六色的。使用模型空间实现简单,更加直观,不需要原始的法线切线等信息就可以计算。同时在纹理坐标的缝合处和尖锐的边角部分可见突变较少,可以提供平滑的边界。

切线空间的法线纹理

在切线空间下存储法线方向。在切线空间下,原点是该顶点本身,z轴是法线方向,x轴是切线方向,y轴通过法线和切线叉积而来,被称为副切线或副法线。切线空间下的法线纹理几乎全是浅蓝色,因为实际上大多数法线都与原模型的法线一致(即没有凹凸),也就是模型切线的垂直,即值为(0,0,1),经过映射后对应了RGB(0.5,0.5,1)浅蓝色。其它需要调整凹凸的只是对原法线作出略微修改(扰动),所以看起来也是蓝色。

切线空间看似复杂,但优点更多:它自由度高,模型空间下的法线纹理只能运用到该模型上,而切线空间下就可以运用到一个完全不同的网格上;它可以进行UV动画,可以移动一个纹理的UV坐标来实现凹凸移动的效果(如火山岩浆);它可以重用法线纹理,使用一张法线纹理就可以用到所有6个面;可以压缩,因为Z方向永远是正方向,所以可以通过XY直接推导,不需要多存储一个值。

凹凸映射的实践

切线空间下的计算

Shader "Custom/Normal Map In Tangent Space"{
Properties{
    _Color("Color Tint",Color)=(1,1,1,1)
    _MainTex("Main Tex",2D)="white"{}
    _BumpMap("Normal Map",2D)="bump"{}
    //bump是Unity内置的法线纹理,提供模型自带的法线信息
    _BumpScale("Bump Scale",float)=1.0
    //控制凹凸程度,为0时说明不会对光照产生任何影响
    _Specular("Specular",Color)=(1,1,1,1)
    _Gloss("Gloss",Range(8.0,256))=20
    }
SubShader{
        pass{
            Tags{"LightMode"="ForwardBase"}
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _BumpMap;
            float4 _BumpMap_ST;
            float _BumpScale;
            fixed4 _Specular;
            float _Gloss;
            
            struct a2v
            {
                float4 vertex:POSITION;
                float3 normal:NORMAL;
                float4 tangent:TANGENT;
                //将顶点的切线方向存储进tangent变量内,w分量用来存储副切线的方向性
                float4 texcoord:TEXCOORD0;
            };

            struct v2f
            {
                float4 pos:SV_POSITION;
                float4 uv:TEXCOORD0;
                //因为有两个纹理,所以该变量中xy存储_MainTex纹理坐标,zw存储_BumpMap纹理坐标
                float3 lightDir:TEXCOORD1;
                float3 viewDir:TEXCOORD2;
            };

            v2f vert(a2v v)
            {
                v2f o;
                o.pos=UnityObjectToClipPos(v.vertex);
                o.uv.xy=v.texcoord.xy*_MainTex_ST.xy+_BumpMap_ST.zw;
                o.uv.zw=v.texcoord.xy*_MainTex_ST.xy+_BumpMap_ST.zw;
                TANGENT_SPACE_ROTATION;
                //内置宏,得到从模型空间到切线空间的变换矩阵rotation
                o.lightDir=mul(rotation,ObjSpaceLightDir(v.vertex)).xyz; 
                //先拿模型空间下的光照方向,然后通过变换矩阵转到切线空间
                o.viewDir=mul(rotation,ObjSpaceViewDir(v.vertex)).xyz;
                //先拿模型空间下的视角方向,然后通过变换矩阵转到切线空间
                return o;
            }

            fixed4 frag(v2f i):SV_Target{
                fixed3 tangentLightDir=normalize(i.lightDir);
                fixed3 tangentViewDir=normalize(i.viewDir);
                fixed4 packedNormal=tex2D(_BumpMap,i.uv.zw);
                //对法线纹理进行纹理采样,此时返回的时经过映射后得到的像素值
                fixed3 tangentNormal;
                tangentNormal=UnpackNormal(packedNormal);
                //所以通过UnpackNormal映射回法线方向
                tangentNormal.xy*=_BumpScale;
                //通过乘以凹凸程度获取真实法线方向
                tangentNormal.z=sqrt(1.0-saturate(dot(tangentNormal.xy,tangentNormal.xy)));
                //通过切线空间xy值获得z值
                fixed3 albedo=tex2D(_MainTex,i.uv).rgb*_Color.rgb;
                fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
                fixed3 diffuse=_LightColor0.rgb*albedo*max(0,dot(tangentNormal,tangentLightDir));
                fixed3 halfDir=normalize(tangentLightDir+tangentViewDir);
                fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(max(0,dot(tangentNormal,halfDir)),_Gloss);
                return fixed4(ambient+diffuse+specular,1.0);
            }
            ENDCG
            }
        }
    FallBack "Specular"
    }

UnpackNormal:将像素值映射回法线(只有把法线纹理类型设置成Normal Map才能用)

TANGENT_SPACE_ROTATION:得到从模型空间到切线空间的变换矩阵rotation

这样就变成了凹凸不平的样子:

切线空间

世界空间计算

Shader"Custom/Normal Map In World Space"{
    Properties{
    _Color("Color Tint",Color)=(1,1,1,1)
    _MainTex("Main Tex",2D)="white"{}
    _BumpMap("Normal Map",2D)="bump"{}
    _BumpScale("Bump Scale",float)=1.0
    _Specular("Specular",Color)=(1,1,1,1)
    _Gloss("Gloss",Range(8.0,256))=20
    }
SubShader{
        pass{
            Tags{"LightMode"="ForwardBase"}
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _BumpMap;
            float4 _BumpMap_ST;
            float _BumpScale;
            fixed4 _Specular;
            float _Gloss;
            
            struct a2v
            {
                float4 vertex:POSITION;
                float3 normal:NORMAL;
                float4 tangent:TANGENT;
                float4 texcoord:TEXCOORD0;
            };

            struct v2f
            {
                float4 pos:SV_POSITION;
                float4 uv:TEXCOORD0;
                float4 TtoW0:TEXCOORD1;
                float4 TtoW1:TEXCOORD2;
                float4 TtoW2:TEXCOORD3;
                //这一坨是切线空间转化到世界空间的变换矩阵,由于寄存器存不了3x3大小的,所以只能这样
                //w用来存储世界空间下的顶点位置
            };

            v2f vert(a2v v)
            {
                v2f o;
                o.pos=UnityObjectToClipPos(v.vertex);
                o.uv.xy=v.texcoord.xy*_MainTex_ST.xy+_BumpMap_ST.zw;
                o.uv.zw=v.texcoord.xy*_MainTex_ST.xy+_BumpMap_ST.zw;
                
                float3 worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
                //世界空间下顶点位置
                fixed3 worldNormal=UnityObjectToWorldNormal(v.normal);
                //世界空间下法线
                fixed3 worldTangent=UnityObjectToWorldDir(v.tangent.xyz);
                //世界空间下切线
                fixed3 worldBinormal=cross(worldNormal,worldTangent)*v.tangent.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);
                //构建世界空间下坐标
                fixed3 lightDir=normalize(UnityWorldSpaceLightDir(worldPos));
                fixed3 viewDir=normalize(UnityWorldSpaceViewDir(worldPos));
                fixed3 bump=UnpackNormal(tex2D(_BumpMap,i.uv.zw));
                //法线方向(但是这里是切线空间下的)
                bump.xy*=_BumpScale;
                bump.z=sqrt(1.0-saturate(dot(bump.xy,bump.xy)));
                bump=normalize(half3(dot(i.TtoW0.xyz,bump),dot(i.TtoW1.xyz,bump),dot(i.TtoW2.xyz,bump)));
                //将法线变换到世界空间下
                fixed3 albedo=tex2D(_MainTex,i.uv).rgb*_Color.rgb;
                fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
                fixed3 diffuse=_LightColor0.rgb*albedo*max(0,dot(bump,lightDir));
                fixed3 halfDir=normalize(lightDir+viewDir);
                fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(max(0,dot(bump,halfDir)),_Gloss);
                return fixed4(ambient+diffuse+specular,1.0);
            }
            ENDCG
            }
        }
    FallBack "Specular"
    }

效果与使用切线空间大致相同。

渐变纹理

概念

运用一个存储表面属性的纹理,进行控制漫反射光照的方法。这种方法相比使用兰伯特模型,可以更加灵活地控制光照结果。由万能的V社在《军团要塞2》中首次应用并提出。

渐变纹理实践

Shader"Custom/Ramp Texture"
{
    Properties
    {
        _Color("Color Tint",Color)=(1,1,1,1)
        _RampTex("Ramp Tex",2D)="white"{}
        _Specular("Specular",Color)=(1,1,1,1)
        _Gloss("Gloss",Range(8.0,256))=20
        }
    SubShader{
        pass{
            Tags{"LightMode"="ForwardBase"}
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Color;
            sampler2D _RampTex;
            float4 _RampTex_ST;
            fixed4 _Specular;
            float _Gloss;

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

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

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

            fixed4 frag(v2f i):SV_Target
            {
                fixed3 worldNormal=normalize(i.worldNormal);
                fixed3 worldLightDir=normalize(UnityWorldSpaceLightDir(i.worldPos));
                fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
                fixed halfLambert=0.5*dot(worldNormal,worldLightDir)+0.5;
                //计算半兰伯特模型,因为此时半兰伯特范围也是[0,1],正好拿来做纹理采样
                //由于渐变纹理只有一个坐标(纵轴颜色不变),所以拿fixed采样
                fixed3 diffuseColor=tex2D(_RampTex,fixed2(halfLambert,halfLambert)).rgb*_Color.rgb;
                fixed3 diffuse=_LightColor0.rgb*diffuseColor;
                //将纹理采样的颜色和材质颜色相乘,得到最终漫反射颜色
                fixed3 viewDir=normalize(UnityWorldSpaceViewDir(i.worldPos));
                fixed3 halfDir=normalize(worldLightDir+viewDir);
                fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(max(0,dot(worldNormal,halfDir)),_Gloss);
                return fixed4(ambient+specular+diffuse,1.0);
            }
            ENDCG
        }
    }
    FallBack "Specular"
}

此时需要把Wrap Mode设置成Clamp模式,防止纹理采样因为浮点数精度产生的问题。(虽然半兰伯特范围是[0,1],但也会存在1.000001的情况,此时如果是repeat直接舍弃整数部分变成0.000001,就是黑色了。

遮罩纹理

概念

运用遮罩纹理保护某些区域免于修改,比如希望模型表面一些区域更亮,一些区域更暗,就可以使用一张遮罩纹理控制光照。

具体流程是:通过采样得到遮罩纹理的纹素值,使用某个通道的值(比如rgb中的r)与表面属性相乘。当通道值为0说明不受属性保护。

遮罩纹理实践

Shader "Custom/Chapter7-MaskTexture"
{
    Properties
    {
        _Color ("Color Tint", Color) = (1,1,1,1)
        _MainTex ("Main Tex", 2D) = "white" {}
        _BumpMap("Normal Map",2D)="white"{}
        _BumpScale("Bump Scale",Float)=1.0
        _SpecularMask("Specular Mask",2D)="white"{}
        //高光反射的遮罩纹理
        _SpecularScale("Specular Scale",Float)=1.0
        //遮罩影响度的系数
        _Specular("Specular",Color)=(1,1,1,1)
        _Gloss("Gloss",Range(8.0,256))=20
    }
    SubShader
    {
        pass{
        Tags { "LightMode"="ForwardBase" }

        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag

        #include "Lighting.cginc"

        fixed4 _Color;
        sampler2D _MainTex;
        float4 _MainTex_ST;
        //三个纹理共同使用一个纹理属性
        sampler2D _BumpMap;
        float _BumpScale;
        sampler2D _SpecularMask;
        float _SpecularScale;
        fixed4 _Specular;
        float _Gloss;

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

        struct v2f
        {
            float4 pos:SV_POSITION;
            float2 uv:TEXCOORD0;
            float3 lightDir:TEXCOORD1;
            float3 viewDir:TEXCOORD2;
        };

        v2f vert(a2v v)
        {
            v2f o;
            o.pos=UnityObjectToClipPos(v.vertex);
            o.uv.xy=v.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw;
            
            TANGENT_SPACE_ROTATION;
            o.lightDir=mul(rotation,ObjSpaceLightDir(v.vertex)).xyz;
            o.viewDir=mul(rotation,ObjSpaceViewDir(v.vertex)).xyz;
            return o;
        }

        fixed4 frag(v2f i):SV_Target
        {
            fixed3 tangentLightDir=normalize(i.lightDir);
            fixed3 tangentViewDir=normalize(i.viewDir);
            fixed3 tangentNormal=UnpackNormal(tex2D(_BumpMap,i.uv));
            tangentNormal.xy*=_BumpScale;
            tangentNormal.z=sqrt(1.0-saturate(dot(tangentNormal.xy,tangentNormal.xy)));
            fixed3 albedo=tex2D(_MainTex,i.uv).rgb*_Color.rgb;
            fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
            fixed3 diffuse=unity_LightColor0.rgb*albedo*max(0,dot(tangentNormal,tangentLightDir));
            fixed3 halfDir=normalize(tangentLightDir+tangentViewDir);
            fixed specularMask=tex2D(_SpecularMask,i.uv).r*_SpecularScale;
            //对遮罩纹理进行采样,将得到的掩码值乘上影响度
            fixed3 specular=unity_LightColor0.rgb*_Specular.rgb*pow(max(0,dot(tangentNormal,halfDir)),_Gloss)*specularMask;
            return fixed4(ambient+diffuse+specular,1.0);
        }
        ENDCG
    }
        }
    FallBack "Specular"
}