纹理贴图
本质上就是计算任何一个片元用贴图哪一个像素的颜色。
这里主要是算在环境光计算中,从下面代码可以看出。
代码使用上一节基于Phong光照模型
,为之添加单张纹理计算。
单张纹理贴图
添加的代码在行末注释第一个字符是+
号:
Shader "Custom/SingleTexture"
{
Properties{
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
_Specular ("Specular", Color) = (1, 1, 1, 1) //材质的高光反射颜色
_Gloss ("Gloss", Range(8.0, 256)) = 20 //材质的高光反射区域大小
_MainTex ("Main Tex", 2D) = "White" {} //+ 纹理贴图,默认纯白
_Color ("Color Tint", Color) = (1, 1, 1, 1) //+ 色调
}
SubShader{
Pass{
Tags{"LightMode" = "ForwardBase"}//定义了正确的LightMode,才能得到Unity内置的光照变量
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc" //使用内置库文件
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
sampler2D _MainTex; //+
float4 _MainTex_ST; //+ 前缀与纹理名一致,用于存放纹理的缩放与偏移
fixed4 _Color; //+
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 = mul(v.normal, (float3x3)unity_WorldToObject);//将模型法向量转化到世界坐标系下
o.worldPos = mul(v.vertex, unity_WorldToObject).xyz;//将顶点坐标转化到世界坐标系下,为了得到视角方向
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;//+ 先缩放后偏移,得到顶点在纹理图中的坐标
// 与上一句话同理 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target{
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;//+ 得到纹理颜色
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; //获取环境光
ambient *= albedo;//+ 作用到环境光
fixed3 worldNormal = normalize(i.worldNormal);//顶点着色器已经帮忙计算出来世界坐标系下的法向量了
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);//得到世界光源的光线向量(假设只有一个光源且使用平行光)
fixed3 diffuse = _LightColor0.rgb * albedo * (dot(worldNormal, worldLight) * 0.5 + 0.5);//+ 纹理颜色作用到漫反射
//上面计算了漫反射,下面计算高光反射
fixed3 reflectDir = normalize(reflect(worldLight, worldNormal));//反射方向
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);//向量的差,得到模型到摄像机的方向向量
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
fixed3 color = ambient + diffuse + specular;
return fixed4(color, 1.0);
}
ENDCG
}
}
}
主要工作就是在属性中获取到纹理文件,再用tex2D()
函数采样配合_MainTex_ST
参数得到每个片元的纹理坐标。
效果是这样的:
其中参数_MainTex_ST
本质上就是引用了下面这四个浮点数,
控制平铺(缩放)和偏移,可以看到将水平平铺设置为2,就水平方向贴了两次:
纹理的属性
纹理的使用代码不多,但是纹理有许多属性可以设置。
如平铺和多级渐远滤波之类的。
凹凸映射
也就是让贴图看起来凹凸不平
。
实质上就是通过计算表面的法线——朝向
从而得出光线对于这个面的映照结果。
一些空间
需要介绍一些空间
模型空间:一个三维空间:也就是一个3D建模自己的空间,比如建模了一个猪猪网格,猪猪的左前脚第一个脚趾头的最前面的顶点是这个空间的原点。然后其它网格顶点的坐标都能确定了。
色彩纹理UV图空间:一个二维空间:为了能够将纹理应用到建模出来的网格上,定义一个从模型空间映射到UV图空间的映射关系——纹理映射
。所有的模型网格顶点都能根据一个映射关系,落到UV图上的某个坐标上:。而UV图的每个坐标就是一个像素,如果是RGB三通道纹理图那么就直接得到每个顶点的颜色了。
模型空间的法线纹理UV图空间:一个二维空间:同上面的UV图一样,在某种映射下,三维模型网格的各个顶点会落到这个UV图的某个坐标上,取出这个坐标像素的RGB值
代表网格顶点的在模型空间下的法线方向XYZ
。
切线空间:一个三维空间:模型网格的每个顶点都有自己的切线空间,在这个空间中,顶点的切线方向是X轴,法线方向是Z轴,副切/法线是Y轴(切线和法线做叉积得到)。
切线空间的法线纹理UV图空间:一个二维空间:与上面各种UV图最大的不同就是,没有纹理映射
这一步操作,而是直接与色彩纹理UV图绑定,法线图RGB值
代表对于色彩图RGB
的同坐标像素
的法线方向
,而这个法线方向是在切线空间
下描述的。
这里最后两个空间可能会引起一些疑惑,感觉没有必要。
但是实际使用基本上都是使用切线空间的法线纹理,因为有更好的迁移性。
比如从一个猪猪模型导出的色彩和法线纹理图,用在这个猪猪上不会有问题,
而后者可以保证用到其它模型上,从光照法线上来说也不会有问题——可能会因为纹理映射而导致网格上有像素拉伸,但是光照计算基于模型本身的切线
,是完全正确的。
因此一些UV动画只能用后者实现。
法线纹理
上面基本上介绍完整了。
我们只需要学习使用切线空间下的纹理UV图
进行凹凸映射。
法线纹理直接说明了贴图纹理的每个坐标的各个顶点的空间朝向。
这样就能直接用来计算光照等信息。
注意一下法线纹理图是一张图片,所以每个像素代表一条法线,
而像素RGB三色
范围是[0,1],法线本质是一个单位向量,XYZ分量
范围是[-1,1],
因此这里有个偏移转化
下面具体实现,搞懂上面的几种空间就很容易得出大致流程:
在片元着色器中通过纹理采样得到切线空间下的法线,
然后再与切线空间下的视角方向、光照方向等进行计算,
得到最终的光照结果。
为此,我们首先需要再顶点着色器中把视角方向和光照方向从模型空间变换到切线空间中。
用到下面两张UV纹理图,一张是色彩,一张是法线纹理。
两张1024*1024的的纹理图,应该可以直接下载把:
因为涉及到从法线纹理中获取表面法线,而不需要在世界坐标系中计算光照,通通换成在切线空间中计算。
因此代码相对于上面的有很大的改动,
注释掉的代码在开头有个-
号,
添加的代码在行末注释中有个+
号,
改动的代码在行末注释中有个*
号。
Shader "Custom/NormalMapTexuture"
{
Properties{
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
_Specular ("Specular", Color) = (1, 1, 1, 1) //材质的高光反射颜色
_Gloss ("Gloss", Range(8.0, 256)) = 20 //材质的高光反射区域大小
_MainTex ("Main Tex", 2D) = "White" {} //纹理贴图,默认纯白
_Color ("Color Tint", Color) = (1, 1, 1, 1) //色调
_BumpMap("Normal Map", 2D) = "bump" {}//+ bump是Unity自带的法线纹理
_BumpScale("BumpScale", Float) = 1.0//+ 控制凹凸程度,为0时无凹凸
}
SubShader{
Pass{
Tags{"LightMode" = "ForwardBase"}//定义了正确的LightMode,才能得到Unity内置的光照变量
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc" //使用内置库文件
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
sampler2D _BumpMap; //+ 法线纹理图
float4 _BumpMap_ST; //+ 法线纹理的缩放与平移
float _BumpScale; //+ 凹凸程度
struct a2v{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT; //+ 得到顶点的切线信息
float4 texcoord : TEXCOORD0;
};
struct v2f{
float4 pos : SV_POSITION;
//- float3 worldNormal : TEXCOORD0; 不再需要世界坐标系中计算光照了,统统放在切线空间
//- float3 worldPos : TEXCOORD1;
float4 uv : TEXCOORD0; //* 法线纹理也有缩放和平移的能力,一起在这里储存
float3 lightDir : TEXCOORD1; //+ 切线空间中的光线方向
float3 viewDir : TEXCOORD2; //+ 切线空间中的视角方向
};
v2f vert(a2v v){
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//- o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
//- o.worldPos = mul(v.vertex, unity_WorldToObject).xyz;
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;//*
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;//+
TANGENT_SPACE_ROTATION;//+ 从模型空间转化到切线空间的矩阵,存放到rotation
o.lightDir = normalize(mul(rotation, ObjSpaceLightDir(v.vertex)).xyz);//+
o.viewDir = normalize(mul(rotation, ObjSpaceViewDir(v.vertex)).xyz);//+
return o;
}
fixed4 frag(v2f i) : SV_Target{
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
ambient *= albedo;
//不在需要在世界空间下计算漫反射和高光反射了
//- fixed3 worldNormal = normalize(i.worldNormal);
//- fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
//-fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * (dot(worldNormal, worldLight) * 0.5 + 0.5);
//-fixed3 reflectDir = normalize(reflect(worldLight, worldNormal));
//-fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
//-fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);//+
fixed3 tangentNormal;//+ 对上面的像素解包成法线向量
tangentNormal = UnpackNormal(packedNormal);//+
tangentNormal.xy *= _BumpScale;//+
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));//+
// 光照计算,漫反射放在切线空间中计算,考虑纹理中的法线方向,而不是顶点的法线方向。
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, i.lightDir));//+
// 高光反射放在切线空间中计算,考虑纹理中的法线方向,而不是顶点的法线方向。
fixed3 viewDir = normalize(i.lightDir + i.viewDir);//+
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, viewDir)), _Gloss);//+
fixed3 color = ambient + diffuse + specular;
return fixed4(color, 1.0);
}
ENDCG
}
}
}
作用法线纹理