一、基础概念
- 使用纹理映射(texture mapping)技术,可以把一张图“粘”在模型表面,逐纹素(texel)地控制模型的颜色
纹理映射坐标(UV坐标)
- 定义了该顶点在纹理中对应的2D坐标
- 使用(u, v)来表示,其中u为横向坐标,v为纵向坐标,通常被归一化在[0, 1]范围内
- OpenGL纹理空间的原点位于左下角,而DirectX中位于左上角,Unity使用的是符合OpenGL的
二、单张纹理
1.实践
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Custom/Chapter7-SingleTexture"
{
Properties{
_Color ("Color Tint", Color) = (1,1,1,1)
_MainTex ("Main 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 _MainTex;
float4 _MainTex_ST;
//需要使用 纹理名_ST 的方式来声明某个纹理的属性,ST是缩放和平移的缩写,可以让我们得到该纹理的缩放和平移值
//_MainTex_ST.xy存储的是缩放值,_MainTex_ST.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;
//或者这样写
//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));
//使用tex2D函数对纹理进行采样,第一个参数是需要被采样的纹理,第二个参数是float2类型的纹理坐标,返回计算得到的纹素值
//用采样结果和颜色属性的乘积来作为材质的反射率albedo
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xzy * 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, halfDir)), _Gloss);
return fixed4 (ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
2.纹理的属性
①纹理类型 Texture type
- Normal Map :法线纹理类型,用于存储切线空间下的法线信息,实现凹凸映射的效果
- Cubemap:立方体贴图类型,用于存储环境映射信息,实现反射折射等效果
②平铺模式 Wrap Mode
-
Repeat:纹理坐标超出[0,1]范围时,会不断重复纹理
-
Clamp:纹理坐标超出[0,1]范围时,会被截取到边缘值
-
Mirror:纹理坐标超出[0,1]范围时,会进行镜像重复
-
必须使用纹理的属性(例如 _MainTex_ST 变量)在Shader中对顶点纹理坐标进行相应的变换
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
//或者这样写
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
③滤波模式 Filter Mode
- Point:使用最邻近滤波,图像边缘清晰,但会出现马赛克现象(像素风)
- Bilinear:使用线性插值滤波,图像边缘平滑,但会损失一些细节
- Trilinear:使用三线性插值滤波,结合多级渐远纹理mipmap,图像边缘更平滑,细节更丰富,但性能开销大
④纹理尺寸和格式
- 纹理尺寸: 尽量使用 2 的幂次方大小的纹理,以优化性能和内存使用
- 纹理格式: 选择合适的纹理格式,平衡图像质量和内存使用
三、凹凸映射
- 两种方法:高度映射(height mapping)、法线映射(normal mapping)
1.高度纹理 Height map
- 使用一张高度图来实现凹凸映射,其中存储的时强度值,用于表示模型表面局部海拔高度
- 颜色越浅表示越向外凸起,越深表示越往里凹
- 好处是直观,缺点是计算更加复杂
2.法线纹理 Normal map
- 法线纹理中存储的是表面法线方向
- 由于法线方向分量范围在[-1,1],像素分量范围在[0,1],所以要做映射
- pixel = n o r m a l + 1 2 \frac{normal + 1}{2} 2normal+1
- 所以在shader中对法线纹理进行采样后,还需要对结果进行一次反映射的过程
- normal = pixel * 2 - 1
- 两种坐标系:
- 模型空间法线纹理
- 存储的是模型空间下的法线方向,五颜六色的,每个颜色代表特定的法线方向
- 切线空间法线纹理
- 存储切线空间下的法线方向
- 看起来几乎是全蓝色,因为存储的是相对法线方向(每个点相对于自身切线空间的法线扰动方向)
- 模型空间法线纹理
3.实践
- 需要在计算光照模型中统一各个方向矢量所在的坐标空间
- 有两种选择
- 在切线空间下计算光照
- 在世界空间下计算光照
①在切线空间下计算
- 基本思路:
- 在片元着色器中通过纹理采样得到切线空间下的法线
- 再与切线空间下的视角方向、光照方向等进行计算
- 最终得到光照结果
- 把光照方向和视角方向变换到切线方向下
- 在顶点着色器中将视角方向和光照方向从模型空间变换到切线空间中——需要得到模型空间到切线空间的变换矩阵
Shader "Custom/Chapter7-NormalMapTangentSpace"
{
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;
float3 lightDir : TEXCOORD1;
float 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; //uv.xy存储_MainTex的纹理坐标
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw; //uv.zw存储_BumpMap的纹理坐标
//unity内置宏来计算得到rotation矩阵,使从模型空间变换到切线空间
TANGENT_SPACE_ROTATION;
//LightDir从模型空间变换到切线空间
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
//ViewDir从模型空间变换到切线空间
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); //对_BumpMap进行采样,法线纹理中存储的是把法线映射过得到的像素值,因此需要反映射回来
fixed3 tangentNormal;
//将纹理mark为“法线贴图”,使用内置函数
tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale; //_BumpScale控制凹凸程度
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy))); //法线是矢量单位,因此可以由tangentNormal.xy计算
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"
}
②在世界空间下计算
- 基本思路:
- 在顶点着色器中计算从切线空间到世界空间的变换矩阵,传递给片元着色器
- 在片元着色器中把法线纹理中的法线方向从切线空间变换到世界空间
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "Custom/Chapter7-NormalMapWorldSpace"
{
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;
};
//TtoW0、TtoW1、TtoW2以此存储了从切线空间到世界空间的变换矩阵的每一行
//实际上对方向矢量的变换只需要3x3大小的矩阵就好,(每一行只用float3),但是为了充分使用,把世界空间下顶点位置存储在w分量中
v2f vert(a2v v){
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//使用了两张纹理,需要存储两个纹理坐标
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; //uv.xy存储_MainTex的纹理坐标
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw; //uv.zw存储_BumpMap的纹理坐标
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"
}
③法线纹理的类型
- 法线纹理通常用Normal Map标识
- 法线纹理的压缩
- unity会根据不同平台对法线纹理进行压缩,比如DXT5nm格式
- 这种压缩格式只存储法线纹理的xy分量,z分量可以通过xy分量推导出来
- Unity 提供了 UnpackNormal 函数来对法线纹理进行采样和解码
- Create from Grayscale
- 这个选框,用于从高度图生成法线纹理
- Bumpiness 用于控制凹凸程度
- Filtering 决定我们使用哪种方式来计算凹凸程度,有两种选项:Smooth 和 Sharp
- Smooth 使得生成的法线纹理会比较平滑
- Sharp 会使用 Sobel 滤波来生成法线,它会强调边缘信息
四、渐变纹理
- 用渐变纹理来控制漫反射光照的效果
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Custom/Chapter7-RampTexture"
{
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"
#include"UnityCG.cginc"
fixed4 _Color;
sampler2D _RampTex;
float4 _RampTex_ST; //_RampTex的纹理属性变量
fixed4 _Specular;
float _Gloss;
struct a2v{
float4 vertex : POSITION;
float4 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.xzy;
//用纹理去采样漫反射颜色
fixed halfLambert = 0.5* dot(worldNormal, worldLightDir) + 0.5;
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(halfDir, worldNormal)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
- 将渐变纹理贴上就可以出现效果
- 要把Wrap Mode设为Clamp模式,防止对纹理进行采样时由于浮点数精度而造成的问题
五、遮罩纹理 mask texture
- 遮罩允许我们保护某些区域,免于某些修改
- 流程
- 通过采样得到的遮罩纹理的纹素值
- 使用其中某(几)个通道的值,来与某种表面属性进行相乘
- 当该通道为0时,可以保护表面不受该属性的影响
1.实践
- 使用高光遮罩纹理,逐像素地控制模型表面的高光反射强度
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Custom/Chapter7-MaskTexture"
{
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
_SpecularMask ("Specular Mask", 2D) = "white"{} //gaoguang
_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; //为主纹理_MainTex、法线纹理_BumpMap和遮罩纹理_SpecularMask共同使用的纹理属性变量,修改主纹理的平铺系数和偏移系数会同时影响三个纹理的采样
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 = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));
fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
//得到遮罩的值
fixed specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularScale; //对遮罩纹理_SpecularMask进行采样
//用高光遮罩计算高光项
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss) * specularMask;
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}