在(一)中,我们输出的图像已经可以看出模型在光照下的基本样貌,但模型表面却缺少颜色细节,比如一面墙的表面应该是砖头的颜色。
一、基础纹理
我们通常会使用一张纹理来模拟物体的漫反射颜色,换而言之,我们需要使用纹理中的纹素颜色来代替漫反射光照模型中的漫反射系数。
本例中仍然使用Blinn-Phong光照模型。
Shader "Custom/Test1"
{
Properties
{
//用于控制纹理的整体颜色表现
_Color("Color",Color)=(1,1,1,1)
_MainTex("基础纹理",2D)="white"{}
_Specular("Specular",Color)=(1,1,1,1)
_Gloss("Gloss",Range(0,256))=20
}
SubShader
{
Pass
{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "UnityLightingCommon.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Specular;
fixed _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,_MainTex);
return o;
}
fixed4 frag(v2f i):SV_Target
{
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.rgb;
fixed3 worldNormal=normalize(i.worldNormal);
fixed3 worldLight=normalize(_WorldSpaceLightPos0.xyz);
//纹理采样
fixed3 texResult=tex2D(_MainTex,i.uv)*_Color;
fixed3 diffuse=_LightColor0*texResult
*saturate(dot(worldLight,worldNormal));
fixed3 viewDir=normalize(_WorldSpaceCameraPos-i.worldPos);
fixed3 halfDir=normalize(viewDir+worldLight);
fixed3 specular=_LightColor0.rgb*_Specular
*pow(saturate(dot(worldNormal,halfDir)),_Gloss);
return fixed4(ambient+diffuse+specular,1);
}
ENDCG
}
}
}
二、法线纹理/高度纹理:
可以看到模型表面已经是颜色分明,但此时新的问题又出现了,我们知道一面墙壁通常砖头是凹凸不平的,但只是使用基础纹理无法达到这样的效果,因此便有了法线/高度纹理,这种纹理可以在不改变模型顶点数的情况下营造出表面凹凸不平的错觉,这种方法也被称为达成凹凸映射。
2.1切线空间下计算:
Shader "Custom/Test1"
{
Properties
{
//用于控制纹理的整体颜色表现
_Color("Color",Color)=(1,1,1,1)
_MainTex("基础纹理",2D)="white"{}
_NormalTex("法线纹理",2D)="bump"{}
_BumpScale("凹凸缩放",float)=1.0
_Specular("Specular",Color)=(1,1,1,1)
_Gloss("Gloss",Range(0,256))=20
}
SubShader
{
Pass
{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "UnityLightingCommon.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _NormalTex;
float4 _NormalTex_ST;
half _BumpScale;
fixed4 _Specular;
fixed _Gloss;
struct a2v
{
float4 vertex:POSITION;
float3 normal:NORMAL;
//切线不同于法线,需要用4个分量表示,因为切线的w属性用于指明副切线的方向
float4 tangent:TANGENT;
float4 texcoord:TEXCOORD0;
};
struct v2f
{
float4 pos:SV_POSITION;
float3 tangentLightDir :TEXCOORD0;
float3 tangentViewDir:TEXCOORD1;
//使用float4,前两个值存放基础纹理坐标,后两个值存放法线纹理坐标
float4 uv:TEXCOORD3;
};
v2f vert(a2v v)
{
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
//内置宏,但注意变量名一定要和我写的一样
//用于得到世界到切线空间的变换矩阵
TANGENT_SPACE_ROTATION;
o.tangentLightDir=mul(rotation,ObjSpaceLightDir(v.vertex));
o.tangentViewDir=mul(rotation,ObjSpaceViewDir(v.vertex));
o.uv.xy=TRANSFORM_TEX(v.texcoord,_MainTex);
o.uv.zw=TRANSFORM_TEX(v.texcoord,_NormalTex);
return o;
}
fixed4 frag(v2f i):SV_Target
{
half4 rawNormal=tex2D(_NormalTex,i.uv.zw);
//使用内置函数将得到正确的法线映射,但由于unity会对法线贴图进行压缩,z分量需要自己计算
half3 tangentNormal=UnpackNormal(rawNormal);
//这样写可以控制法线在正常法线和贴图采样法线之间的过渡,从而实现控制凹凸
tangentNormal.xy*=_BumpScale;
tangentNormal.z=sqrt(1.0-
saturate(dot(tangentNormal.xy,tangentNormal.xy)));
half3 tangentLightDir=normalize(i.tangentLightDir);
half3 tangentViewDir=normalize(i.tangentViewDir);
//纹理采样
fixed3 texResult=tex2D(_MainTex,i.uv.xy)*_Color;
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.rgb;
fixed3 diffuse=_LightColor0*texResult
*saturate(dot(tangentLightDir,tangentNormal));
fixed3 halfDir=normalize(tangentViewDir+tangentLightDir);
fixed3 specular=_LightColor0.rgb*_Specular
*pow(saturate(dot(tangentNormal,halfDir)),_Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
}
2.2 世界空间下计算:
Shader "Custom/Test1"
{
Properties
{
//用于控制纹理的整体颜色表现
_Color("Color",Color)=(1,1,1,1)
_MainTex("基础纹理",2D)="white"{}
_NormalTex("法线纹理",2D)="bump"{}
_BumpScale("凹凸缩放",float)=1.0
_Specular("Specular",Color)=(1,1,1,1)
_Gloss("Gloss",Range(0,256))=20
}
SubShader
{
Pass
{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "UnityLightingCommon.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _NormalTex;
float4 _NormalTex_ST;
half _BumpScale;
fixed4 _Specular;
fixed _Gloss;
struct a2v
{
float4 vertex:POSITION;
float3 normal:NORMAL;
float4 tangent:TANGENT;
float4 texcoord:TEXCOORD0;
};
struct v2f
{
float4 pos:SV_POSITION;
//我们需要将切线空间到世界空间的变换矩阵传递到片元着色器
//由于片元着色器最大只支持float4大小的插值寄存器,因此我们将矩阵分成3行4列
//最后一列用于存储世界坐标,实际计算并不会参与
float4 matrix_TangentToObject0:TEXCOORD0;
float4 matrix_TangentToObject1:TEXCOORD1;
float4 matrix_TangentToObject2:TEXCOORD2;
float4 uv:TEXCOORD3;
};
v2f vert(a2v v)
{
v2f o;
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
float3 worldNormal=UnityObjectToWorldNormal(v.normal);
float3 worldTangent=UnityObjectToWorldDir(v.tangent);
//副切线由法线和切线的叉积得到,w用于表示副切线的方向
float3 worldBinormal=cross(worldNormal,worldTangent)*v.tangent.w;
//根据线性代数的知识,我们知道从切线空间到世界空间的变换矩阵由切线空间的三个坐标轴按列构成
//这三个坐标轴必须是在世界坐标下的表示
o.matrix_TangentToObject0=
float4(worldTangent.x,worldBinormal.x,worldNormal.x,worldPos.x);
o.matrix_TangentToObject1=
float4(worldTangent.y,worldBinormal.y,worldNormal.y,worldPos.y);
o.matrix_TangentToObject2=
float4(worldTangent.z,worldBinormal.z,worldNormal.z,worldPos.z);
//o.tangentLightDir=mul(rotation,ObjSpaceLightDir(v.vertex));
//o.tangentViewDir=mul(rotation,ObjSpaceViewDir(v.vertex));
o.uv.xy=TRANSFORM_TEX(v.texcoord,_MainTex);
o.uv.zw=TRANSFORM_TEX(v.texcoord,_NormalTex);
return o;
}
fixed4 frag(v2f i):SV_Target
{
half4 rawNormal=tex2D(_NormalTex,i.uv.zw);
//使用内置函数将得到正确的法线映射,但由于unity会对法线贴图进行压缩,z分量需要自己计算
half3 tangentNormal=UnpackNormal(rawNormal);
tangentNormal.xy*=_BumpScale;
tangentNormal.z=sqrt(1.0-
saturate(dot(tangentNormal.xy,tangentNormal.xy)));
//将采样得到的法线乘以变换矩阵
half3 worldNormal=normalize(half3(
dot(i.matrix_TangentToObject0.xyz,tangentNormal),
dot(i.matrix_TangentToObject1.xyz,tangentNormal),
dot(i.matrix_TangentToObject2.xyz,tangentNormal)));
float3 worldPos=float3(i.matrix_TangentToObject0.w,
i.matrix_TangentToObject1.w,i.matrix_TangentToObject2.w);
half3 worldLightDir=UnityWorldSpaceLightDir(worldPos);
half3 worldViewDir=UnityWorldSpaceViewDir(worldPos);
//纹理采样
fixed3 texResult=tex2D(_MainTex,i.uv.xy)*_Color;
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.rgb*texResult;
fixed3 diffuse=_LightColor0*texResult
*saturate(dot(worldLightDir,worldNormal));
fixed3 halfDir=normalize(worldViewDir+worldLightDir);
fixed3 specular=_LightColor0.rgb*_Specular
*pow(saturate(dot(worldNormal,halfDir)),_Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
}
计算结果与切线空间几乎没有任何差别。
关于高度纹理,书中并没有举例。
三、渐变纹理
在基础纹理中,我们通过纹理的颜色作为漫反射系数,在渐变纹理中,我们使用纹理的颜色直接表示漫反射计算结果,使用渐变纹理可以模拟插画风格的渲染。
Shader "Custom/Test1"
{
Properties
{
//用于控制纹理的整体颜色表现
_Color("Color",Color)=(1,1,1,1)
_RampTex("渐变纹理",2D)="white"{}
_Specular("Specular",Color)=(1,1,1,1)
_Gloss("Gloss",Range(0,256))=20
}
SubShader
{
Pass
{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "UnityLightingCommon.cginc"
fixed4 _Color;
sampler2D _RampTex;
float4 _RampTex_ST;
fixed4 _Specular;
fixed _Gloss;
struct a2v
{
float4 vertex:POSITION;
float3 normal:NORMAL;
float4 texcoord:TEXCOORD0;
};
struct v2f
{
float4 pos:SV_POSITION;
float3 worldNormal :TEXCOORD0;
float3 worldPos:TEXCOORD1;
};
v2f vert(a2v v)
{
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
fixed4 frag(v2f i):SV_Target
{
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.rgb;
fixed3 worldNormal=normalize(i.worldNormal);
fixed3 worldLight=normalize(_WorldSpaceLightPos0.xyz);
//我们使用半兰伯特来作为uv采样的坐标
//关于为何使用半兰伯特而不是模型自身的UV坐标
//这是因为有学术论文提到过这种技术,确实有公司使用,最终结果也符合预期
fixed halfLambert=0.5*dot(worldNormal,worldLight)+0.5;
fixed3 texResult=tex2D(_RampTex,halfLambert)*_Color;
fixed3 diffuse=_LightColor0*texResult;
fixed3 viewDir=normalize(_WorldSpaceCameraPos-i.worldPos);
fixed3 halfDir=normalize(viewDir+worldLight);
fixed3 specular=_LightColor0.rgb*_Specular
*pow(saturate(dot(worldNormal,halfDir)),_Gloss);
return fixed4(ambient+diffuse+specular,1);
}
ENDCG
}
}
}
以上为使用不同的渐变纹理产生的效果。
但请注意,右一模型在某些部位会出现小黑点,这是由于纹理采样的精度造成的,我们需要把渐变纹理的Wrap Mode设为Clamp模式。虽然半兰伯特计算方式最终结果会处于[0,1]之间,但是会有诸如1.0001的情况出现,而Repeat的采样会舍去整数使用小数,这恰好对应的纹理的黑色部分,而Clamp会对大于1的数强制取1。
四、遮罩纹理
遮罩纹理允许我们保护某些区域,在没有使用遮罩纹理之前,高光反射的参数是应用于所有像素,而遮罩纹理允许我们细腻的控制高光反射。除此之外,遮罩纹理还可以用于控制如何混合多张图片,比如地形制作中,对于草地,裸露土地如何混合等。
使用遮罩纹理控制高光反射:
计算过程和凹凸纹理的计算大体相同,只是在计算高光反射时将遮罩纹理的采样结果与高光反射的结果相乘,从而实现控制高光表现。
Shader "Custom/Test1"
{
Properties
{
//用于控制纹理的整体颜色表现
_Color("Color",Color)=(1,1,1,1)
_MainTex("基础纹理",2D)="white"{}
_NormalTex("法线纹理",2D)="bump"{}
_BumpScale("凹凸缩放",float)=1.0
_Specular("Specular",Color)=(1,1,1,1)
_SpecularMask("遮罩纹理",2D)="white"{}
_SpecularScale("遮罩缩放",float)=1.0
_Gloss("Gloss",Range(0,256))=20
}
SubShader
{
Pass
{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "UnityLightingCommon.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _NormalTex;
float4 _NormalTex_ST;
half _BumpScale;
fixed4 _Specular;
sampler2D _SpecularMask;
fixed _SpecularScale;
fixed _Gloss;
struct a2v
{
float4 vertex:POSITION;
float3 normal:NORMAL;
//切线不同于法线,需要用4个分量表示,因为切线的w属性用于指明副切线的方向
float4 tangent:TANGENT;
float4 texcoord:TEXCOORD0;
};
struct v2f
{
float4 pos:SV_POSITION;
float3 tangentLightDir :TEXCOORD0;
float3 tangentViewDir:TEXCOORD1;
//使用float4,前两个值存放基础纹理坐标,后两个值存放法线纹理坐标
float4 uv:TEXCOORD2;
};
v2f vert(a2v v)
{
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
TANGENT_SPACE_ROTATION;
o.tangentLightDir=mul(rotation,ObjSpaceLightDir(v.vertex));
o.tangentViewDir=mul(rotation,ObjSpaceViewDir(v.vertex));
o.uv.xy=TRANSFORM_TEX(v.texcoord,_MainTex);
o.uv.zw=TRANSFORM_TEX(v.texcoord,_NormalTex);
return o;
}
fixed4 frag(v2f i):SV_Target
{
half4 rawNormal=tex2D(_NormalTex,i.uv.zw);
//使用内置函数将得到正确的法线映射,但由于unity会对法线贴图进行压缩,z分量需要自己计算
half3 tangentNormal=UnpackNormal(rawNormal);
tangentNormal.xy*=_BumpScale;
tangentNormal.z=sqrt(1.0-
saturate(dot(tangentNormal.xy,tangentNormal.xy)));
half3 tangentLightDir=normalize(i.tangentLightDir);
half3 tangentViewDir=normalize(i.tangentViewDir);
//纹理采样
fixed3 texResult=tex2D(_MainTex,i.uv.xy)*_Color;
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.rgb;
fixed3 diffuse=_LightColor0*texResult
*saturate(dot(tangentLightDir,tangentNormal));
fixed3 halfDir=normalize(tangentViewDir+tangentLightDir);
//采样遮罩纹理,和基础纹理中一样,使用一个浮点数控制整体表现
fixed specularMask=tex2D(_SpecularMask,i.uv)*_SpecularScale;
fixed3 specular=_LightColor0.rgb*_Specular
*pow(saturate(dot(tangentNormal,halfDir)),_Gloss)*specularMask;
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
}
遮罩纹理有时候不止是用来控制高光,一般会根据需求,想控制啥就制作对应的遮罩就行。
五、纹理的采样
纹理的采样也是一个大坑,再UnityShader我们简简单单的使用内置函数就可以完成,但采样内部的原理却很复杂,不同的采样方式性能不同,表现结果也尽数不同,而采样的底层数学原理涉及到信号等等专业知识,可以参考B站大神的图形学专业课,有如何采样,如何抗锯齿等等。
比如Inspector面板上的值都是干啥的,B站大神的图形学专业课有涉及,大多数都是和纹理采样相关以及优化压缩的。