·本专栏文章记录笔者阅读学习《Unity Shader入门精要》的感想与笔记,方便日后复习与查找
一、什么是纹理
1.纹理的定义
纹理就是一张用来控制模型外观的图片。使用纹理映射技术(texture mapping),我们可以把一张图贴在模型的表面,然后逐纹素(texel)地控制模型的颜色
2.UV坐标
纹理映射坐标一般是存储在每个顶点上的,这些坐标定义了顶点在纹理中相对应的2D坐标。而这个2D坐标就是(u,v),它们的范围归一化在【0,1】之间。(在这个范围之外的纹理坐标可以在纹理的图片设置中选择是Clamp截断在1或者0,还是Repeat截取小数部分)
3.纹理映射坐标的原点位置
- OpenGL中是左下角(Unity中使用的是这个风格的纹理坐标)
- DirectX中的左上角
关于纹理图片的相关设置的效果可以回去看之前的笔记:
二、单张纹理

这里我们采用的是基础光照模型(环境光+漫反射+高光反射)
不记得了就回顾之前的笔记:《Unity Shader入门精要》学习--初级篇--宝宝都能学会的基础光照模型
1.属性添加
2D类型是添加一个纹理属性用的,它的默认值是【 "字符" {}】
其他的属性按基础光照模型中需要的参数添加
2.CG中声明定义
2D类型的属性,在CG中对应sampler2D,而它还需要给他声明一个额外的float4的属性,即:_MainTex_ST。对任意一个纹理,它都需要有对应的这个属性,命名为:纹理名_ST
S表示Scale,缩放;T表示Translate,位移偏移
3.输入输出结构体数据的确定
- vertex:顶点位置,必须至少有这个才能进行后续的计算
- texcoord:TEXCOORD0把顶点的第一组纹理映射坐标传入给参数texcoord
- normal:然后是需要计算光照,所以需要传入顶点法线normal
- pos:一定要计算的顶点在裁剪空间中的坐标位置
- worldnormal:世界空间下顶点法线(因为是去片元着色器中进行逐像素计算)
- worldPos:世界空间下的顶点位置(通过它可以计算得到世界空间下的观察方向和光照方向)
- uv:用于存储经过缩放和位移之后的纹理坐标,方便我们后续在片元着色器中进行采样
4.顶点着色器中变换纹理坐标
v2f vert(a2v v){
v2f o;
o.pos = UnityObjectToClipPos(v.vertex); //老样子,把顶点坐标变换到裁剪空间中
//o.worldNormal = mul(v.normal, (float3x3)Unity_WorldToObject); //和上面的方法效果一样的
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;
}
其中o.uv是用来存储经过变换的顶点的纹理坐标的。可以手动缩放和偏移,也可以通过内置函数
- TRANSFORM_TEX(顶点纹理坐标,对应的纹理)
(图片和代码一样的,哪个看得清看哪个)
5.片元着色器中采用纹理影响基础颜色
fixed4 frag(v2f i) : SV_Target{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = UnityWorldSpaceLightDir(i.worldPos);
//对主纹理进行采样得到纹素颜色值
fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb;
//计算环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
//计算漫反射光
fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(worldNormal,worldLightDir));
//计算高光反射
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.pos));
fixed3 halfDir = normalize(viewDir + worldLightDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(halfDir,worldNormal)),_Gloss);
return fixed4(ambient+diffuse+specular, 1.0);
}
这里最关键的地方是反射率albedo的获取 = 纹理进行采样得到纹素值 * 模型颜色属性
- tex2D(采样的纹理,float2类型的纹理坐标) 用于得到纹素值(颜色值)
然后这里主要影响的也只是漫反射和环境光的部分(它们计算中都乘以了albedo)
其他的地方就是常规的光照计算了
完整代码:
Shader "Custom/MyShader/Chapter7_Texture/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"
#include "UnityCG.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 = mul(v.normal, (float3x3)Unity_WorldToObject); //和上面的方法效果一样的
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 = UnityWorldSpaceLightDir(i.worldPos);
//对主纹理进行采样得到纹素颜色值
fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb;
//计算环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
//计算漫反射光
fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(worldNormal,worldLightDir));
//计算高光反射
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.pos));
fixed3 halfDir = normalize(viewDir + worldLightDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(halfDir,worldNormal)),_Gloss);
return fixed4(ambient+diffuse+specular, 1.0);
}
ENDCG
}
}
Fallback "Specular"
}
三、凹凸纹理
凹凸纹理(bump mapping) 是指可以修改顶点法线,让模型表面实现看起来凹凸不平的效果的纹理贴图,它有两种常见的实现方式:
- 高度纹理(height map):
- 法线纹理(nromal map):
1.法线纹理的原理
1.1.法线纹理与法线的映射关系
因为纹理中存储顶点是颜色,范围在[-1,1];而法线则是一个矢量,范围在[0,1]之间,所以它们之间有这样的映射关系:
那采样得到pixel之后如何得到normal就可以推得是:
1.2.切线空间与模型空间存储法线
切线空间:是指由这个顶点为原点,这个顶点的切线(x轴)、法线(z轴)、副法线(y轴)组成的坐标空间。
- 副法线可以通过切线与法线叉乘得到
使用切线空间下的法线一般都是(0,0,1),所以映射到纹理中的颜色(上面的映射公式使用)会是(0,0,1),也就是蓝色的。因此,如果这个顶点的法线被扰动了,它就一定不是绿色的了。但是在模型空间中,它们的颜色可能就五颜六色了
模型空间存储法线的优点:
- 直观简单,实现简单
- 可见的图标缝隙小,提供更加平滑的边界。因为是同一坐标系下插值得到的
切线空间存储法线的优点:
- 自由度高,因此存储的是相对法线信息,所以可以应用到任意网格上
- 可以进行UV动画。通过移动UV坐标来实现,常用于水或者熔浆中
- 可以重用法线纹理。一张法线纹理可以应用在多个面上
- 可以压缩。法线纹理中的法线Z一定是正反向,所以我们可以只存储XY,Z通过叉乘得到
显然切线空间中的优点很多,所以一般我们都是存储在切线空间中的
2.法线纹理的应用
要使用法线纹理,我们需要知道采样得到的都是切线空间下的法线值,那要进行光照计算的的话,是还需要观察方向、光照方向的,而这些方向的计算是需要在同一个坐标空间中进行才能得到正确的结果。因此我们又分为两种流派①在切线空间中计算 ②在世界空间中计算
2.1.切线空间中计算

因为采样得到的法线已经是在切线空间当中了,所以我们的思路是把光照方向、观察方向转化到切线空间当中。我们可以直接在顶点着色器中就完成这个转化
2.1.1.属性添加
首先也是属性中添加相应的纹理
2.1.2.CG中声明属性定义
然后在CG片段中声明它们(记得添加_BumpMap_ST变量影响纹理的变换)
2.1.3.顶点/片元着色器的输入输出数据结构定义
a2v中:
- normal和tangent:接着因为我们需要计算从世界空间到切线空间的变换矩阵,所以需要获取法线和切线的信息。
- texcoord:同时获取顶点上的纹理坐标进行后续的变换与采样
v2f中:
- uv:来接受变换后的纹理坐标,注意它是float4类型的,xy用于存主纹理的纹理坐标,zw用来用法线纹理的纹理坐标
- lightDir:变换到切线空间中的光照方向
- viewDir:变换到切线空间中观察方向
2.1.4.顶点着色器中的计算
v2f vert(a2v v){
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//把主纹理和法向纹理贴图分别存入uv的xy和zw中
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
//要求出从模型空间到切线空间的变换矩阵,已经知道模型空间中顶点的法线,切线,那再知道它的副切线位置,就可以得到变换矩阵了
float3 binormal = cross(normalize(v.normal),normalize(v.tangent.xyz)) * v.tangent.w;
float3x3 rotation = float3x3(v.tangent.xyz,binormal, v.normal); //rotation是从模型空间变换到切线空间的变换
//TANGENT_SPACE_ROTATION; //或者直接用内置宏是一样的,同一也是rotation
o.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex)).xyz;
o.viewDir = mul(rotation,ObjSpaceViewDir(v.vertex)).xyz;
return o;
}
这里的uv来接受变换后的纹理坐标,注意它是float4类型的,xy用于存主纹理的纹理坐标,zw用来用法线纹理的纹理坐标
然后在已知切线空间下的标准正交基在模型空间下的表示的时候,把这个标准正交基每个横向排列,就是模型空间到切线空间的变换矩阵了。当然其实也可以不用手动求,直接调用UnityCG中内置的宏TANGENT_SPACE_ROTATION是一样的效果的(也是获取一个叫做rotation的,将矢量从模型空间变换到切线空间当中的矩阵,一般这个更常用)
其中Xm,Ym,Zm是切线空间的坐标轴在模型空间下的表示的向量值
不记得什么原理了可以回去看之前的笔记:《Unity Shader入门精要》学习--基础篇--骇人的空间变换
2.1.5.片元着色器中的计算
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 = UnpackNormal(packedNormal); //然后用内置函数对纹素值进行变换,变成切线空间下对应的法线值
tangentNormal.xy *= _BumpScale; //对得到的法线值的xy进行一个缩放
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy,tangentNormal.xy))); //对逆向映射回去的法线的xy乘以深度值之后,为了保证还是单位矢量,对z进行更新
//常规采样单个纹理并计算光照
fixed3 albedo = tex2D(_MainTex,i.uv.xy).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(tangentNormal,tangentLightDir));
fixed3 halfDir = normalize(tangentLightDir+tangentViewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(tangentNormal,halfDir)),_Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}
这里实现法线贴图的应用最关键的部分是在于
tex2D采样得到的是纹素值,然后通过内置函数【UnpackNormal(纹素)】进行映射(映射公式在上面有写过的:),不过要注意的是,要使用这个内置函数对这个法线纹理进行正确映射首先需要把这个法线纹理在图片设置中的类型设置为【Normal Map】
完整代码:
Shader "Custom/MyShader/Chapter7_Texture/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 "UnityCG.cginc"
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
fixed4 _MainTex_ST;
sampler2D _BumpMap;
fixed4 _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;
float3 viewDir : TEXCOORD2;
};
v2f vert(a2v v){
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//把主纹理和法向纹理贴图分别存入uv的xy和zw中
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
//要求出从模型空间到切线空间的变换矩阵,已经知道模型空间中顶点的法线,切线,那再知道它的副切线位置,就可以得到变换矩阵了
float3 binormal = cross(normalize(v.normal),normalize(v.tangent.xyz)) * v.tangent.w;
float3x3 rotation = float3x3(v.tangent.xyz,binormal, v.normal); //rotation是从模型空间变换到切线空间的变换
//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 = UnpackNormal(packedNormal); //然后用内置函数对纹素值进行变换,变成切线空间下对应的法线值
tangentNormal.xy *= _BumpScale; //对得到的法线值的xy进行一个缩放
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy,tangentNormal.xy))); //对逆向映射回去的法线的xy乘以深度值之后,为了保证还是单位矢量,对z进行更新
//常规采样单个纹理并计算光照
fixed3 albedo = tex2D(_MainTex,i.uv.xy).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(tangentNormal,tangentLightDir));
fixed3 halfDir = normalize(tangentLightDir+tangentViewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(tangentNormal,halfDir)),_Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
Fallback "Specular"
}
2.2.世界空间中计算

在世界空间中计算光照,那就意味着我们需要先对法线纹理进行采样,然后再映射得到其在切线空间下的值,然后再把它变换到世界空间下。这意味着我们需要先求得从切线空间变换到世界空间的变换矩阵。那我们首先需要计算得到切线空间的坐标轴在世界坐标下的表示,即worldNormal、worldTangent、worldBinormal,然后把它们作为分量,纵向放入矩阵中即可得到切线空间到法线空间的变换矩阵。
2.2.1.属性添加与定义
和切线空间中计算时候一样的
2.2.2.v2f输出结构体
a2v输入结构体和切线空间中计算时候一样的
- TtoW0,TtoW1,TtoW2:从切线空间到世界空间的变换矩阵的三个分量
2.2.3.顶点着色器中
v2f vert (appdata v){
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpTex_ST.xy + _BumpTex_ST.zw;
float3 worldPos = mul(unity_ObjectToWorld, v.vertex);
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
float3 worldBinormal = cross(worldNormal,worldTangent) * v.tangent.w;
//计算从切线空间变换到世界空间的变换矩阵的每一个分量 (已知切线空间的方向轴和轴心点在世界空间下的表示,所以对于每个轴它是按列排放的)
o.TtoW0 = fixed4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = fixed4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = fixed4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
return o;
}
其中最重要的是各个分量的保存方式:
每个分量的w值还正好顺便把顶点在世界空间中的位置给传了过去
2.2.4.片元着色器中
fixed4 frag (v2f i) : SV_Target
{
float3 worldPos = float3(i.TtoW0.z, i.TtoW1.z, i.TtoW2.z);
float3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos));
float3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
fixed3 bump = UnpackNormal(tex2D(_BumpTex,i.uv.zw)); //用内置函数对采样得到的纹素进行变换,得到
bump.xy *= _BumpScale;
bump.z = sqrt(1-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 albode = tex2D(_MainTex,i.uv.xy).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albode;
fixed3 diffuse = _LightColor0.rgb * albode * saturate(dot(bump,worldLightDir));
float3 halfDir = normalize(worldLightDir+worldViewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(halfDir,bump)),_Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}
其中最关键的就是
把采样映射后得到的法线值给变换到世界空间中,按矩阵乘法的定义来求得。
完整代码:
Shader "Custom/MyShader/Chapter7_Texture/NormalMapWorldSpace"
{
//在世界空间下使用法线纹理计算光照
//思路是在顶点着色器中计算好变换矩阵的各个分量
//然后在片元着色器中采样得到法线后变换到世界空间中进行计算
Properties
{
_Color ("Color Tint",Color) = (1,1,1,1)
_MainTex ("MainTex", 2D) = "white" {}
_BumpTex ("BumpTex", 2D) = "Bump"{}
_BumpScale("BumpScale", float) = 1.0
_Specular ("Specular", Color) = (1,1,1,1)
_Gloss ("Gloss",Range(8.0,256)) = 20.0
}
SubShader
{
Pass
{
Tags { "LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
float4 uv : TEXCOORD0;
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
};
float4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpTex;
float4 _BumpTex_ST;
float _BumpScale;
float4 _Specular;
float _Gloss;
v2f vert (appdata v){
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpTex_ST.xy + _BumpTex_ST.zw;
float3 worldPos = mul(unity_ObjectToWorld, v.vertex);
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
float3 worldBinormal = cross(worldNormal,worldTangent) * v.tangent.w;
//计算从切线空间变换到世界空间的变换矩阵的每一个分量 (已知切线空间的方向轴和轴心点在世界空间下的表示,所以对于每个轴它是按列排放的)
o.TtoW0 = fixed4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = fixed4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = fixed4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 worldPos = float3(i.TtoW0.z, i.TtoW1.z, i.TtoW2.z);
float3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos));
float3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
fixed3 bump = UnpackNormal(tex2D(_BumpTex,i.uv.zw)); //用内置函数对采样得到的纹素进行变换,得到
bump.xy *= _BumpScale;
bump.z = sqrt(1-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 albode = tex2D(_MainTex,i.uv.xy).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albode;
fixed3 diffuse = _LightColor0.rgb * albode * saturate(dot(bump,worldLightDir));
float3 halfDir = normalize(worldLightDir+worldViewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(halfDir,bump)),_Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
Fallback "Specular"
}


老实说我感觉它们看起来差不多
四、渐变纹理
人们发现纹理不止可以存储顶点的颜色值,还可以存储任意表面信息。比如用渐变纹理来控制漫反射光照。一种叫做冷到暖色调(Cool-to-warm- tones)的着色技术,可以保证物体的轮廓线更加明显,常用于制作卡通风格的渲染效果。
1.添加属性
这里只用到了一张渐变纹理
2.CG中声明属性变量
常规声明对应变量(注意贴图变量需要额外声明一个_材质名_ST)
3.输入输出结构体定义
常规在片元着色器中计算光照的搭配
4.顶点着色器中
常规变换顶点坐标,法线位置,以及UV
5.片元着色器中
fixed4 frag (v2f i) : SV_Target
{
//先求出三巨头:归一化的世界法线、光照方向、观察方向
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//用半兰伯特去对计算漫反射颜色
fixed halfLambert = dot(worldNormal,worldLightDir)*0.5 + 0.5; //这个数值肯定也是在【0,1】之间的
fixed3 diffuseColor = tex2D(_RampTex,fixed2(halfLambert,halfLambert)).rgb * _Color.rgb; //使用半兰伯特作为坐标去采样渐变纹理
fixed3 diffuse = _LightColor0.rgb * diffuseColor;
//正常计算高光反射
fixed3 halfDir = normalize(worldLightDir + worldViewDir);
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 specular = _LightColor0.rgb * _Specular * pow(saturate(dot(halfDir,worldNormal)),_Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}
其中最重要的地方是:
这里对渐变纹理的采样时一个斜线的方式进行采样
完整代码:
Shader "Custom/MyShader/Chapter7_Texture/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 "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD0;
float2 uv : TEXCOORD2;
};
float4 _Color;
sampler2D _RampTex;
float4 _RampTex_ST;
float4 _Specular;
float _Gloss;
v2f vert (appdata 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 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//用半兰伯特去对计算漫反射颜色
fixed halfLambert = dot(worldNormal,worldLightDir)*0.5 + 0.5; //这个数值肯定也是在【0,1】之间的
fixed3 diffuseColor = tex2D(_RampTex,fixed2(halfLambert,halfLambert)).rgb * _Color.rgb; //使用半兰伯特作为坐标去采样渐变纹理
fixed3 diffuse = _LightColor0.rgb * diffuseColor;
//正常计算高光反射
fixed3 halfDir = normalize(worldLightDir + worldViewDir);
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 specular = _LightColor0.rgb * _Specular * pow(saturate(dot(halfDir,worldNormal)),_Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
Fallback "Specular"
}
(我这个应该是猴头的平滑度不一样所以看起小有差别)
注意事项
我们需要把渐变纹理的Wrap Mode设置为Clamp,如果设置为repeat的话在采样并计算光照的时候不会采样到1.000001,然后截断为0.000001,在加入计算的时候会使得diffuse = 0,导致出现小黑点。
五、遮罩纹理
遮罩纹理主要是用于更细致地控制每个像素位置受光照的影响情况,保护某些位置免受修改(把这个位置的光照计算的颜色值乘以一个遮罩值)
1.添加属性
这全明星阵容了,把除了渐变纹理之外都邀请进来了(主纹理、法线纹理、遮罩纹理)
2.CG中声明属性变量
这里可以发现只声明了一个_MainTex_ST变量,这样设置的话那这个变量就会同时控制三个纹理的变换了
3.输入输出结构体定义
常规设置了(逐片元,切线空间中计算光照)
4.顶点着色器中
最重要的地方是把光照和观察方向从模型扣扣你赶紧变换到切线空间中。这里直接用了TANGENT_SPACE_ROTATION来获取模型空间到切线空间中的变换矩阵了
5.片元着色器中
fixed4 frag (v2f i) : SV_Target
{
fixed3 tangentLightDir = normalize(i.lightDir);
fixed3 tangentViewDir = normalize(i.viewDir);
fixed3 tangentNormal = UnpackNormal(tex2D(_BumpTex, i.uv));
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1-saturate(dot(tangentNormal.xy, tangentNormal.xy)));
fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(tangentNormal,tangentLightDir));
fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
//采样获取遮罩值
fixed specularMask = tex2D(_SpecularMask,i.uv).r * _SpecularScale;
//在计算完后乘以遮罩值来具体控制是否受影响
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(tangentNormal, halfDir)), _Gloss) * specularMask;
return fixed4(ambient + diffuse + specular, 1.0);
}
这里最重要的是
通过直接乘以一个数控制最后的光照结果来实现遮罩效果



可以看得出来还是有所不同的。如果遮罩图中采样到的纹素是0的话,那就会使得遮罩值是0,相乘进去得到的高光反射的值就是0 了,也就达到了控制这个地方不受高光反射的影响
当然,你这里还可以发现这个高光遮罩纹理在计算中只用到了R通道的值,那其他G B A的值不就浪费了嘛?事实上如果不充分利用的话确实浪费了,但是一般我们会在其他三个通道也存储不同的属性。例如R存储高光反射强度、G存储边缘光照强度、B存储高光反射指数、A存储自发光强度。这样就不会浪费了
六、总结
①纹理的基础功能其实就是用于给模型的每一个顶点进行上色的一张图片
②纹理上又可以存储的可以是由其他信息转化而成的颜色信息,例如法线的扰动、遮罩值、以及漫反射渐变量。这意味着我们可以对一个模型使用多张纹理贴图来使其获得更加丰富的效果。
③法线纹理的存储分为切线空间中与模型空间中,但是一般来说法线空间中进行存储的法线纹理更多,而且优点更多,可以高度复用且能够制作UV动画
④高度纹理是颜色越浅表示越凸起,越深表示越凹陷。所以它很直观,但是计算得到具体的法线值要更加复杂一些
⑤渐变纹理能够帮助我们给更加清晰明显地显示模型边缘轮廓,适合用于做卡通风格的渲染
⑥遮罩纹理则是能够帮助我们更细致地控制对每个像素颜色值的着色情况,同时可以保存更多的模型表面信息。
⑦在手动采样纹理的过程中,我们先在顶点着色器中得到经过偏移变换的纹理坐标值,然后再在片元着色器中进行采样,根据uv纹理坐标,得到具体的颜色值(纹素)。然后根据不同的纹理类型,可以对这个颜色值进行不同的处理。