一.Unity的纹理概念
纹理最基础的目的:用一张图片控制模型的外观。使用纹理映射(texture mapping)技术,我们可以把一张图“粘”在模型表面,逐纹素(texel)(名字是为了和逐像素进行区分)的控制模型的颜色。
建模软件中利用纹理展开技术把纹理映射坐标存储在每个顶点上。如下图
UV坐标会被归一化到[0, 1]范围内。
OpenGL和DirectX在二维纹理空间的坐标有差异。OpenGL原点位于左下角,DirectX位于左上角,Unity默认OpenGL模式。
二.单张纹理.FilterMode属性
A.单张纹理应用
通常使用一张纹理来代替物体的漫反射颜色
纹理名_ST表示声明某个纹理的属性,name##_ST.xy 存储缩放值,name##_ST.zw存储偏移值。
结合之前的光照模型的单张纹理应用:
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
Shader "Single Texture" {
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;
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;
// Or just call the built-in function
// 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));
// Use the texture to sample the diffuse color
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
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, halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
要点1:tex2D函数表示对纹理进行采样,param1是需要被采样的纹理,param2是float2类型的纹理坐标,返回计算得到的纹素值。
要点2:TRASFORM_TEX实在UnityCG.cginc中定义的:
//Transform 2D UV by scale/bias property
//param1是顶点的纹理坐标
//param2是纹理的名称
#define TRANSFORM_TEX(tex,name)(tex.xy * name##_ST.xy + name##_ST.zw)
效果如下图:
B.纹理的属性
为了阐释纹理的属性,编写一个更为纯粹的纹理属性应用shader
Shader "TextureProperties" {
Properties {
_MainTex ("Main Tex", 2D) = "white" {}
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
struct a2v {
float4 vertex : POSITION;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 position : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert(a2v v) {
v2f o;
// Transform the vertex from object space to projection space
o.position = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed4 c = tex2D(_MainTex, i.uv);
return fixed4(c.rgb, 1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
用这个shader生成一个纹理图如下:
1.Wrap Mode属性
决定纹理坐标超过[0,1]范围后使用哪种平铺方式。
a.Repeat——重复纹理采样。
这种模式下,如果纹理的坐标超过了1,那么整数部分将会被舍弃,而直接使用小数部分进行采用。
b.Clamp模式时,超过的部分将会截取到边界值,从而形成一个条形结构。如下图:
2.FilterMode属性
决定了当纹理由于变换而产生拉伸时会使用哪种滤波模式。
三种模式:Point模式、Bilinear滤波、Trilinear滤波
a.Point模式使用了最近邻滤波,在放大或缩小时,它的采样像素数目通常只有一个,因此图像看起来有种像素风格的效果。
b.Bilinear滤波采用了线性滤波,对于每个目标像素,它会找4个邻近的像素,然后对他们进行线性插值混合后得到最终的像素,因此图像看起来像被模糊了。
c.Trilinear滤波几乎和Bilinear是一样的,只是Trilinear还会在多级渐远纹理(mipmapping)之间进行混合。
效果如下:
三.凹凸映射
目的:使用一张纹理来修改模型表面的发现。
方法:1.高度映射 : 使用高度纹理来模拟表面位移,后得到修改后的法线值。
2.法线映射:使用一张法线纹理直接存储表面法线。
1.法线映射
法线纹理中存储的就是表面的法线方向,由于法线的方向的分量范围在[-1,1],而像素的分量范围为[0,1],因此我们需要做一个映射:
pixel =0.5*normal + 0.5
实际使用:normal = pixel * 2 - 1
模型空间法线纹理:对于模型自带的表面法线,修改后存储在此纹理中。
切线空间法线纹理:对于模型的每个定点都有一个属于自己的切线空间,这个切线空间的原点就是该定点的本身,而z轴是定点的法线方向,x轴是切线方向,而y轴有法线和切线的叉积得到,也被称为是副切线或副法线。
左边的模型空间法线之所以是五颜六色的,因为所有法线所在坐标空间是在模型空间,x/y/z区间都在0到1之间,所以能显示彩色。
切线空间下的法线纹理几乎是浅蓝色,因为,每个法线方向所在的坐标空间是不一样的,即是表面各点各自的切线空间。这种法线纹理其实就是存储了每个点在各自的切线空间中的法线扰动方向。
2.实践代码
由于法线纹理存储的法线是切线空间下的方向,有两种选择:
1.在切线空间下计算光照,需要把光照方向、视角方向变换到切线空间。
2.都在世界空间进行光照计算。
下面是选择世界空间下计算光照模型的代码。
Shader "NormalMapWorldSpace" {
Properties {
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_BumpMap ("BumpMap",2D) = "white"{}
_BumpScale("BumpScale",Range(-2.0,2.0)) = 1.0
_Specular("Specular",Color)=(1,1,1,1)
_Diffuse("Diffuse",Color)=(1,1,1,1)
_Gloss("Gloss",Range(8,256)) = 40
}
SubShader {
Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex; float4 _MainTex_ST;
sampler2D _BumpMap; float4 _BumpMap_ST;
float _BumpScale;
fixed4 _Specular;
fixed4 _Diffuse;
float _Gloss;
struct a2v
{
float4 vertex:POSITION;
float3 normal:NORMAL;
float4 texcoord:TEXCOORD0;
float4 tangent:TANGENT;
};
struct v2f
{
float4 pos:SV_POSITION;
float4 TtoW0:TEXCOORD0;
float4 TtoW1:TEXCOORD1;
float4 TtoW2:TEXCOORD2;
float4 uv:TEXCOORD3;
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
float3 worldPos = mul(_Object2World,v.vertex).xyz;
float3 worldNormal = mul(v.normal,(float3x3)_World2Object);
//这里要怎样计算?有两种方式
float3 worldTangent = UnityObjectToWorldDir(v.tangent);
//float3 worldTangent = mul((float3x3)_Object2World,v.tagent);
//这里要怎样计算?
//叉积求得
float3 worldBionormal = cross(worldNormal,worldTangent) * v.tangent.w;
//计算了世界空间下顶点切线、副切线和法线的表示,并把它们按列摆放得到从切线空间到世界空间的变换矩阵
//把该矩阵的每一行分别存放在TtoW0、TtoW1、TtoW2中
//最后把世界空间下顶点位置x、y、z分量分别存储在这些变量的w分量中
o.TtoW0 = float4(worldTangent.x,worldBionormal.x,worldNormal.x,worldPos.x);
o.TtoW1 = float4(worldTangent.y,worldBionormal.y,worldNormal.y,worldPos.y);
o.TtoW2 = float4(worldTangent.z,worldBionormal.z,worldNormal.z,worldPos.z);
//o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
//o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
o.uv.xy = TRANSFORM_TEX(v.texcoord,_MainTex);
o.uv.zw = TRANSFORM_TEX(v.texcoord,_BumpMap);
return o;
}
fixed4 frag(v2f i):SV_Target
{
//i.uv.xy
fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo;
float3 worldPos = float3(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w);
float3 worldViewDir =normalize( UnityWorldSpaceViewDir(worldPos) );
float3 worldLightDir =normalize( UnityWorldSpaceLightDir(worldPos) );
//不需要这个值了,所有法线信息都是用bumpMap中的!!!包括漫反射的计算!
float3 worldNormal = normalize( float3(i.TtoW0.z,i.TtoW1.z,i.TtoW2.z) );
//UnpackNormal对法线纹理进行采样和解码(需要把法线纹理标识成Normal map)
fixed3 bump = UnpackNormal(tex2D(_BumpMap,i.uv.zw));
//倘若_BumpScale为0,那么bump=float3(0,0,1)就相当于入射表面没有任何扰动
bump.xy *= _BumpScale;
//需要保证经过处理的法线仍然是归一化的单位向量
//dot计算时,对应的分量相乘然后相加,dot(bump.xy,bump.xy)=x*x + y*y
bump.z = sqrt(1.0- saturate( dot(bump.xy,bump.xy) ));
//将法线从切线空间转换到世界空间,这里是在模拟bump左乘【切线空间到世界空间的转换矩阵】
//我潜意识中两种种错误的写法
//bump = normalize(float3(bump.x * i.TtoW0.xyz,bump.y*i.TtoW1.xyz,bump.z*i.TtoW2.xyz));
//bump = normalize(float3(i.TtoW0.xyz * bump,i.TtoW1.xyz * bump,
//正确写法,原来,单个向量默认相当于列向量
bump = normalize(half3(dot(i.TtoW0.xyz,bump),dot(i.TtoW1.xyz,bump),dot(i.TtoW2.xyz,bump)));
//计算漫发射时同样适用BumpMap中的法线信息!
fixed3 diffuse = _LightColor0.rgb * albedo* saturate(dot(bump,worldLightDir));
float3 halfDir = normalize(worldViewDir + worldLightDir);
//一定千万要注意,前面废了好大的劲,就是为了在这一步计算高光时能用上!!!
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(bump,halfDir)),_Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
基本思路:
由于我们只能在片元着色器中获得bumpMap的纹素信息,所以我们需要在片元着色器中使用切线空间到世界空间的转换矩阵。首先在顶点着色器中获得切线空间各个坐标轴在世界空间中的表示,副切线副通过转换好的worldNormal与worldTangent的叉积获得,注意方向靠v.tangent的w分量来确定。在v2f结构体声明了TtoW0、TtoW1、TtoW2三个纹理寄存器,用来分别将存放转换矩阵的每一行。由于需要在片元着色器用到worldPos来计算worldViewDir以及worldLightDir,所以需要向片元着色器传递该信息,传递方式为将该变量的三个分量分别存放到TtoW0、TtoW1、TtoW2的w分量中。
效果如下图
四.渐变纹理
之前计算漫反射光照,都是使用表面法线和光照方向的点积结果与材质的反射率相乘。渐变纹理是使用一张渐变纹理来控制漫反射光照。
Shader "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;
// Use the texture to sample the diffuse color
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(worldNormal, halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
利用半兰伯特构建float2变量,以此对一张渐变纹理进行采样。漫反射的颜色靠此方式获得。
效果图:
五.遮罩纹理
使用遮罩纹理的流程:通过采样得到的遮罩纹理的纹素值,然后使用其中某个(或某几个)通道的值(如texel.r)来与某种表面属性进行相乘,这样,当该通道的值为0时,可以保护表面不受该属性的影响。
遮罩纹理的使用是对高光效果进行遮罩,使用同一组uv坐标,对同一位置处的遮罩纹理进行采样,这里只使用了r分量。
世界空间下高光遮罩纹理实现:
Shader "MaskTexWorld" {
Properties {
_Color ("ColorTint", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Gloss ("Gloss", Range(8,256)) = 20
_BumpMap("BumpMap",2D) = "white"{}
_BumpScale("BumpScale",Range(-1,1)) = 1
_Specular("Specular",Color) = (1,1,1,1)
_SpecularMask("SpecularMask",2D) = "white"{}//高光反射遮罩纹理
_SpecularScale("SpecularScale",float) = 1.0//控制遮罩影响度的系数
}
SubShader {
Pass
{
Tags{"LgihtMode" = "ForwarBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
float _Gloss;
sampler2D _BumpMap;
float _BumpScale;
fixed4 _Specular;
sampler2D _SpecularMask;
float _SpecularScale;
struct a2v
{
float4 vertex:POSITION;
float3 normal:NORMAL;
//注意因为我们要使用tangent的w分量,所以这里的类型是float4
float4 texcoord:TEXCOORD0;
float4 tangent:TANGENT;
};
struct v2f
{
float4 pos:SV_POSITION;
float4 TtoW0:TEXCOORD0;
float4 TtoW1:TEXCOORD1;
float4 TtoW2:TEXCOORD2;
float2 uv:TEXCOORD3;
};
v2f vert(a2v v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
float3 worldNormal = normalize( UnityObjectToWorldNormal(v.normal) );
float3 worldTangent = normalize( UnityObjectToWorldDir(v.tangent.xyz) );
float3 worldBionormal = cross(worldNormal,worldTangent)*v.tangent.w;
float3 worldPos = mul(_Object2World,v.vertex);
o.TtoW0 = float4(worldTangent.x,worldBionormal.x,worldNormal.x,worldPos.x);
o.TtoW1 = float4(worldTangent.y,worldBionormal.y,worldNormal.y,worldPos.y);
o.TtoW2 = float4(worldTangent.z,worldBionormal.z,worldNormal.z,worldPos.z);
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
return o;
}
fixed4 frag(v2f i):SV_Target
{
float3 worldPos = float3(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w);
float3 worldLightDir =normalize(UnityWorldSpaceLightDir(worldPos));
float3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
float3 bump = UnpackNormal(tex2D(_BumpMap,i.uv));
bump.xy *= _BumpScale;
bump.z = sqrt(1-saturate(dot(bump.xy,bump.xy)));
float3 worldNormal = normalize( float3(dot(bump,i.TtoW0.xyz),dot(bump,i.TtoW1.xyz),dot(bump,i.TtoW2.xyz)) );
fixed3 albedo = tex2D(_MainTex,i.uv).rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(worldNormal,worldLightDir));
//通过uv对MaskTex对应位置进行采样
//所谓的 specularMask 实际上只利用采样纹理的一个分量通道,这里仅仅利用r
fixed specularMask = tex2D(_SpecularMask,i.uv).r * _SpecularScale;
fixed3 halfDir = normalize(worldLightDir + worldViewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal,halfDir)),_Gloss) * specularMask;
return fixed4(ambient + diffuse + specular,1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
效果如下