目录
单张纹理代替漫反射颜色
美术人员在建模时通常会在软件中利用纹理展开的技术把纹理映射坐标(texture-mapping coordinates)存储在每个顶点上,这个坐标定义了该顶点在纹理中对应的2D坐标,通常用(u, v)表示。本次学习内容是将纹理图片映射到模型上,并利用特殊的纹理图片对渲染结果进行调整。
依旧使用Blinn-Phong光照模型计算。首先在Properties语义块中声明材质的参数:
Properties{
_Color("Color Tint", Color) = (1.0, 1.0, 1.0, 1.0)
_Specular("Specular", Color) = (1.0, 1.0, 1.0, 1.0)
_Gloss("Gloss", Range(8.0, 256)) = 20
_MainTex("Main Tex", 2D) = "white" {} // 2D是纹理属性的声明方式,white是内置纹理的名字
}
在SubShader的Pass块中赋予这些变量的数据类型,其中,纹理名_ST是固定的变量名称,储存的是纹理的缩放(Scale)和平移值(Translation),分别放在.xy和.zw里。
SubShader{
Pass{
Tags {"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
fixed4 _Specular;
float _Gloss;
sampler2D _MainTex;
float4 _MainTex_ST; // 纹理名_ST 是固定命名方式,表示纹理属性,ST是缩放(scale)和平移(translation),.xy是缩放值,.zw是平移值
......
接下来定义顶点着色器的输出和输入结构体:
struct a2v
{
float4 pos : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0; // 存储模型的第一组纹理坐标
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float4 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
在顶点着色其中计算顶点对应的纹理坐标:
v2f vert(a2v i)
{
v2f o;
o.pos = UnityObjectToClipPos(i.pos);
o.worldNormal = normalize(UnityObjectToWorldNormal(i.normal));
o.worldPos = mul(unity_ObjectToWorld, i.pos);
o.uv = i.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; //
// or
// o.uv = TRANSFORM_TEX(i.texcoord, _MainTex);
return o;
}
然后在片元着色器中用uv对纹理进行采样得到纹素值,即模型此处的颜色。
fixed4 frag(v2f i) : SV_Target
{
float3 worldNormal = i.worldNormal;
float4 worldPos = i.worldPos;
float3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
float3 worldView = normalize(_WorldSpaceCameraPos.xyz - worldPos);
float3 worldHalf = normalize(worldLight + worldView);
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb; // tex2D 采样_MainTex纹理,纹理坐标是i.uv,返回纹素值
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(worldNormal, worldLight));
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal, worldHalf)), _Gloss);
return fixed4((ambient + diffuse + specular), 1.0);
}
结果图像如下:
利用法线纹理进行凹凸映射 Bump Mapping
法线映射 Normal Mapping
在游戏中,我们经常希望物体的细节越多越好,但大量的顶点数会增加GPU开销。法线纹理(normal map)就是用来解决这个问题的,它可以在不增加模型顶点数的情况下为物体表面增加凹凸的细节和光照效果。
首先我们要知道法线纹理是什么。法线纹理储存的是模型表面的点在各自的切线空间下的法线。也就是说,我们根据语义NORMAL取到的是模型上顶点实际的法线(模型空间下),但在法线纹理中取到的是扰动后的(我们期待的)法线(切线空间下)。
那么,法线在法线纹理中是怎么储存的呢?我们知道,切线空间下一个未被扰动的法线的坐标表示是(0,0,1),也就是在xyz三个分量上的取值范围皆为[-1,1],而图片文件的RGBA四个通道的取值皆为[0,1],所以从纹理图片中读取法线坐标值时需要增加一步映射,即:
n
o
r
m
a
l
=
2
⋅
c
o
l
o
r
−
1
normal = 2 · color - 1
normal=2⋅color−1
一个未被扰动的法线(0,0,1)经过公式
c
o
l
o
r
=
n
o
r
m
a
l
+
1
2
color = \frac {normal + 1} {2}
color=2normal+1 逆映射到图片中的值为(0.5,0.5,1),这个rgb值为浅蓝色,所以大多数法线纹理都是浅蓝色为主色调的。
切线空间和世界空间的转换
现在我们知道了如何通过法线纹理取到我们需要的法线,但这个法线的值是在切线空间下的表示,而我们之前的学习中用于运算的法线都是从模型空间下转到世界空间下运算的。也就是说,我们需要一个变换矩阵,将切线空间下的法线变换到世界空间下(反过来也行,因为变换矩阵是正交矩阵,只要转置一下就是逆变换)。
通过语义NORMAL和TANGENT,我们能拿到模型空间下顶点的法线(normal)和切线(tangent)方向,那么模型空间下的副切线(bitangent)的方向可以由两者叉乘所得。而切线,副切线,法线向量又可以通过UnityObjectToWorldNormal()从模型空间变换为世界空间,而这三者对应的是切线空间的坐标系。有了切线空间坐标系在世界空间下的表示,我们就有了切线空间和世界空间的转换矩阵。
根据《Unity Shader入门精要》4.6章节,我们只需要将世界空间下的切线、副切线、法线向量按顺序纵向在矩阵中排列,就得到了一个3x3的转换矩阵,这个矩阵可以将切线空间中的向量转换为世界空间中的向量(点不行)。
M
T
a
n
g
e
n
t
S
p
a
c
e
2
W
o
r
l
d
S
p
a
c
e
=
(
∣
∣
∣
T
B
N
∣
∣
∣
)
M_{TangentSpace2WorldSpace} = \begin{pmatrix} | & | & |\\ T & B & N\\ | & | & | \end{pmatrix}
MTangentSpace2WorldSpace=
∣T∣∣B∣∣N∣
由于是正交矩阵,所以这个矩阵的转置就是世界空间的向量转换到切线空间的表示。
Shader编写
书上教了在切线空间和世界空间计算两种写法,切线空间是照着书写的,世界空间是自己写的,所以这里只放世界空间。
Properties部分声明纹理等属性:
Properties{
_MainTex("Main Tex", 2D) = "white" {}
_BumpMap("Bump Map", 2D) = "bump" {} // bump是Unity内置法线纹理,当没有提供任何法线纹理时,bump就对应了模型自带的法线信息
_BumpScale("Bump Scale", Float) = 1.0 // 法线扰动程度的大小
_Color("Color Tint", Color) = (1.0, 1.0, 1.0, 1.0)
_Specular("Specular", Color) = (1.0, 1.0, 1.0, 1.0)
_Gloss("Gloss", Range(8.0, 256)) = 20
}
Pass块中
SubShader{
Pass{
Tags {"LightModel" = "ForwardBase"}
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
fixed4 _Color;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 pos : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT; // TANGENT是顶点的切线方向。注意是float4,因为需要tangent.w来决定副切线的方向
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float4 worldPos : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldTangent : TEXCOORD2;
float3 worldBitangent : TEXCOORD3;
float4 uv : TEXCOORD4;
};
v2f vert(a2v i)
{
float3 bitangent = normalize(cross(normalize(i.normal), normalize(i.tangent.xyz)) * i.tangent.w);
v2f o;
o.pos = UnityObjectToClipPos(i.pos);
o.worldPos = mul(unity_ObjectToWorld, i.pos);
// 将TBN转换到世界空间
o.worldNormal = normalize(UnityObjectToWorldNormal(i.normal));
o.worldTangent = normalize(UnityObjectToWorldNormal(i.tangent.xyz));
o.worldBitangent = normalize(UnityObjectToWorldNormal(bitangent));
o.uv.xy = i.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = i.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
float3x3 rotation = transpose(float3x3(i.worldTangent, i.worldBitangent, i.worldNormal));
fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
// 将法线纹理中的值转换为向量值
fixed3 unpackedNormal = UnpackNormal(packedNormal);
unpackedNormal.xy *= _BumpScale;
unpackedNormal.z = sqrt(1 - saturate(dot(unpackedNormal.xy, unpackedNormal.xy))); // x2 + y2 + z2 = 1
// 若材质没有被设置为 normal map,则需手动计算法线
// unpackedNormal.xy = (2 * packedNormal.xy - 1) * _BumpScale;
// unpackedNormal.z = sqrt(1.0 - saturate(dot(unpackedNormal.xy, unpackedNormal.xy)));
float3 worldNormal = normalize(mul(rotation, unpackedNormal));
float3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
float3 worldView = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 worldHalf = normalize(worldView + worldLight);
fixed3 albedo = tex2D(_MainTex, i.uv.xy) * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(worldNormal, worldLight));
fixed3 specular = _LightColor0.rgb * pow(saturate(dot(worldHalf, worldNormal)), _Gloss);
return fixed4((ambient + diffuse + specular), 1);
}
ENDCG
}
}
效果如下:
遮罩纹理 Mask Texture
遮罩纹理常用于对模型表面颜色进行更精细的控制,比如控制光照的反射强度,或者是制作地形材质时混合多张图片(草地纹理、石子、泥土等)。通过采样得到遮罩纹理的纹素值,然后用其中某个通道的值(RGBA)与某种表面属性相乘,当该通道的值为0时,可以保护该表面不受属性影响。
下面是一个高光遮罩的shader代码,代码不难,直接全部贴过来了,比起凹凸映射的代码只增加了一个_SpecularMask,多了一个节省寄存器的操作:
Shader "Custom/Chapter7-MaskTexture"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Main Tex", 2D) = "white" {}
_BumpMap ("Bump Map", 2D) = "bump" {}
_BumpScale("Bump Scale", Range(-2, 2)) = 0
_SpecularMask("Specular Mask", 2D) = "white" {}
_SpecularScale("Specular Scale", Range(0, 10)) = 0
_Specular("Specular", Color) = (1, 1, 1, 1)
_Gloss("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
Pass
{
Tags {"LightModel" = "ForwardBase"}
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
sampler2D _SpecularMask;
float _BumpScale;
float _SpecularScale;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 pos : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float4 worldNormal : TEXCOORD1;
float4 worldTangent : TEXCOORD2;
float4 worldBitangent : TEXCOORD3;
float2 uv : TEXCOORD0;
};
v2f vert(a2v i)
{
v2f o;
o.pos = UnityObjectToClipPos(i.pos);
o.worldNormal.xyz = normalize(UnityObjectToWorldNormal(i.normal));
o.worldTangent.xyz = normalize(UnityObjectToWorldNormal(i.tangent.xyz));
o.worldBitangent.xyz = normalize(UnityObjectToWorldNormal((cross(i.normal, i.tangent.xyz) * i.tangent.w)));
o.uv = TRANSFORM_TEX(i.texcoord, _MainTex);
float3 worldPos = mul(unity_ObjectToWorld, i.pos);
o.worldTangent.w = worldPos.x; // 节省寄存器存储空间
o.worldBitangent.w = worldPos.y;
o.worldNormal.w = worldPos.z;
return o;
}
fixed4 frag(v2f i) : SV_TARGET
{
fixed3 worldPos = fixed3(i.worldTangent.w, i.worldBitangent.w, i.worldNormal.w);
fixed3x3 rotation = transpose(fixed3x3(i.worldTangent.xyz, i.worldBitangent.xyz, i.worldNormal.xyz));
fixed4 packedNormal = tex2D(_BumpMap, i.uv);
fixed3 unpackedNormal = UnpackNormal(packedNormal);
unpackedNormal.xy *= _BumpScale;
unpackedNormal.z = sqrt(1 - saturate(dot(unpackedNormal.xy, unpackedNormal.xy)));
fixed3 specularMask = tex2D(_SpecularMask, i.uv) * _SpecularScale;
fixed3 albedo = tex2D(_MainTex, i.uv) * _Color.rgb;
fixed3 worldNormal = normalize(mul(rotation, unpackedNormal));
fixed3 worldLight = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 worldView = normalize(UnityWorldSpaceViewDir(worldPos));
fixed3 worldHalf = normalize(worldView + worldLight);
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(worldNormal, worldLight));
fixed3 specular = _LightColor0.rgb * pow(saturate(dot(worldHalf, worldNormal)), _Gloss) * specularMask;
return fixed4((ambient + diffuse + specular), 1);
}
ENDCG
}
}
FallBack "Diffuse"
}
效果如下:
渐变纹理
渐变纹理用于控制漫反射光照效果。之前的学习中,我们使用的纹理表示的是物体的颜色,但其实纹理可以用于存储任意的表面属性。
这是一张可以实现卡通风格的纹理:
下面我们看一下如何使用它:
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
_Alpha("Alpha", Range(0, 1)) = 0.5
_Beta("Beta", Range(0, 1)) = 0.5
}
SubShader{
Pass{
Tags{"LightModel" = "ForwardBase"}
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag
fixed4 _Color;
sampler2D _RampTex;
float4 _RampTex_ST;
float4 _Specular;
float _Gloss;
float _Alpha;
float _Beta;
struct a2v {
float4 pos : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldPos : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float2 uv : TEXCOORD2;
};
v2f vert(a2v i)
{
v2f o;
o.pos = UnityObjectToClipPos(i.pos);
o.worldPos = mul(unity_ObjectToWorld, i.pos);
o.worldNormal = normalize(UnityObjectToWorldNormal(i.normal));
o.uv = TRANSFORM_TEX(i.texcoord, _RampTex);
return o;
}
fixed4 frag(v2f i) : SV_TARGET
{
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
fixed3 worldView = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 worldHalf = normalize(worldLight + worldView);
fixed halfLambert = _Alpha + dot(i.worldNormal, worldLight) * _Beta;
// 用halfLambert值作为uv值采样纹理(其实是一维的采样,因为图片纵向上的颜色相同)得到纠正后的漫反射值
fixed3 ramp = tex2D(_RampTex, fixed2(halfLambert, halfLambert)).rgb;
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * ramp;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Color.rbg;
fixed3 specular = _LightColor0.rgb * pow(saturate(dot(worldHalf, worldLight)), _Gloss);
return fixed4((diffuse + ambient + specular), 1);
}
ENDCG
}
}
FallBack "Diffuse"
}
效果如下:
坑点总结
- 为了增强记忆,每次写新的shader我都是新建后删除原有代码重头开始手打的,大多数时候写重复的部分更像是默写,这也导致了写到没完全理解的语句时会出现记忆不清的情况。这次就把Tags里面的“LightMode”记成了“LightModel”,导致改变光源方向的时候不会反映到物体的效果上。
- 像平时看代码一样,多去看看不熟悉的内置函数,关注输入和输出的数据类型,这次因为不知道UnityObjectToWorldNormal的输入参数只能是float3而我传了个float4进去而耽误了好一会儿功夫。