简介:
介绍了Unity Shader入门精要中初级篇包含的所有代码,通过详细拆解代码,一步一步揭晓Shader的原理。
Shader "MyShaderName" { //着色器名字
Properties {
// 属性
}
SubShader {
// 针对显卡A的SubShader
Pass {
// 设置渲染状态和标签
// 开始CG代码片段
CGPROGRAM
// 该代码片段的编译指令,例如:
#pragma vertex vert
#pragma fragment frag
// CG代码写在这里
ENDCG //结束CG代码
// 其他设置
}
// 其他需要的Pass
}
SubShader {
// 针对显卡B的SubShader
}
// 上述SubShader都失败后用于回调的Unity Shader
Fallback "VertexLit"
}
说人话:
Shader "MyShaderName" {}
•第一行是着色器的名字,用大括号{} 包裹后续所有的Shader代码,关键字 Shader。
Properties {
// 属性
}
•紧接着是Shader的属性,用大括号{} 包裹后续所有的属性代码,关键字 Properties。
SubShader {}
•代表子着色器,每个Shader都会包含至少一个SubShader,Unity显示物体时,回去检测这些SubShader,选择一个能够在当前显卡允许的SubShader。
Pass{}
•Pass命令包含渲染状态设置命令的列表,可以有多个Pass。
CGPROGRAM
//CG代码
ENDCG
•CG代码被包含在其中。
#pragma vertex vert
#pragma fragment frag
•该代码片段的编译指令,告诉计算机哪个是顶点着色器(vertex),哪个是片元着色器(fragment)。vert和frag通常分别是它们的名字,也可以更改。
Fallback "VertexLit"
• 上述SubShader都失败后用于回调的Unity Shader
最简单的顶点/片元着色器
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
// 定义一个名为 "Simple Shader" 的着色器
Shader "Unity Shaders Book/Chapter 5/Simple Shader" {
// 定义属性
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1) // 颜色属性
}
// 定义子着色器
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert // 顶点着色器入口点
#pragma fragment frag // 片段着色器入口点
uniform fixed4 _Color; // 颜色变量
// 定义输入结构体
struct a2v {
float4 vertex : POSITION; // 顶点位置
float3 normal : NORMAL; // 法线
float4 texcoord : TEXCOORD0; // 纹理坐标
};
// 定义输出结构体
struct v2f {
float4 pos : SV_POSITION; // 顶点位置
fixed3 color : COLOR0; // 颜色
};
// 顶点着色器函数
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex); // 将顶点位置转换为裁剪空间
o.color = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5); // 计算颜色
return o;
}
// 片元着色器函数
fixed4 frag(v2f i) : SV_Target {
fixed3 c = i.color;
c *= _Color.rgb; // 使用颜色属性调整颜色
return fixed4(c, 1.0); // 返回最终颜色
}
ENDCG
}
}
}
说人话:
Shader "Unity Shaders Book/Chapter 5/Simple Shader" {}
• 第一行表示Shader的名字,”/“用来分隔层次结构。
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1) // 颜色属性
}
•Properties语义块中,声明了一个属性_Color,它的类型是Color,初始值是(1.0,1.0,1.0,1.0),对应白色。为了在CG代码中可以访问它,我们还需要在CG代码片段中提前定义一个新的变量,这个变量的名称和类型必须与Properties语义块中的属性定义相匹配。
•在片元着色器中使用了该属性。
ShaderLab属性类型 | CG变量类型 |
---|---|
Color,Vector | float4,half4,fixed4 |
Range,Float | float,half,fixed |
2D | sampler2D |
Cube | samplerCube |
3D | sampler3D |
SubShader { Pass {} }
•定义子着色器和Pass语句块。Pass命令包含渲染状态设置命令的列表。
CGPROGRAM
ENDCG
•CG代码被包含在其中。所以 CGPROGRAM通常在Pass语句块的开头, ENDCG在Pass语句块结尾。
#pragma vertex vert
#pragma fragment frag
•它们将告诉Unity,哪个函数包含了顶点着色器的代码,哪个函数包含了片元着色器的代码。更通用的编译指令表示如下: #pragma vertex name
#pragma fragment name
•其中name就是我们指定的函数名,这两个函数的名字不一定是vert和frag,它们可以是任意自定义的合法函数名,但我们一般使用vert和frag来定义这两个函数,因为它们很直观。
uniform fixed4 _Color; // 颜色变量
•属性中有_Color在Pass中也要声明Pass。uniform可省略,fixed4表示是一个四维矢量。
// 定义输入结构体
struct a2v {
float4 vertex : POSITION; // 顶点位置
float3 normal : NORMAL; // 法线
float4 texcoord : TEXCOORD0; // 纹理坐标
};
•vertex包含这个顶点的位置,这是通过POSITION语义指定的,返回值是一个float4类型变量,它是该顶点在裁剪空间中的位置。这些语义将告诉系统用户需要哪些输入值,以及用户的输出是什么。
•POSITON语义告诉Unity,用模型空间顶点坐标填充vertex变量。
•NORMAL告诉Unity,用模型空间顶点坐标填充normal变量。
•TECCOORD0告诉Unity,用模型的第一套纹理坐标填充texcoord。
•a表示应用(application), v表示顶点着色器(vertex shader), a2v的意思就是把数据从应用阶段传递到顶点着色器中。
// 定义输出结构体
struct v2f {
float4 pos : SV_POSITION; // 顶点位置
fixed3 color : COLOR0; // 颜色
};
•SV_POSITION将告诉Unity,pos里包含了顶点在裁剪空间的位置信息。
•COLORO语义可以用来存储颜色信息。
// 顶点着色器函数
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex); // 将顶点位置转换为裁剪空间
o.color = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5); // 计算颜色
return o;
}
•声明输出结构体 v2f o。 参数a2v v是输入结构体。
•vert函数的返回值是v2f类型的。传入参数a2v v用于进行顶点和片元的信息传递。
•o.pos = UnityObjectToClipPos(v.vertex) 将顶点位置转换为裁剪空间。
•v.normal包含顶点的法线方向,其分量范围在[-1.0,1.0],将法线归一化后,法线有可能是正向和反向,所以分量范围是[-1.0,1.0]。
•o.color = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5); 把分量范围映射到[0.0,1.0] ,也就是将范围[-1.0,1.0]乘以0.5在加上0.5,映射到[0.0,1.0] 。
•return o 顶点着色器最重要的是返回裁剪空间中的顶点信息。受到结构体v2f中的语义控制
float4 pos : SV_POSITION; // 顶点位置 fixed3 color : COLOR0; // 颜色 SV表示系统数值语义用SV_POSITION语义去修饰顶点着色器的输出变量pos,那么就表示pos包含了可用于光栅化的变换后的顶点坐标(即齐次裁剪空间中的坐标)。通常SV表示输出。
语义 | 描述 |
---|---|
POSITION | 模型空间中的顶点位置,通常是float4类型 |
NORMAL | 顶点法线,通常是float3类型 |
TANGENT | 顶点切线,通常是float4类型 |
TEXCOORDn,TEXCOORD0,TEXCOORD1 | 该顶点的纹理坐标,TEXCOORD0表示第一组纹理坐标。 |
COLOR | 顶点颜色,通常是fixed4或float4类型 |
// 片元着色器函数
fixed4 frag(v2f i) : SV_Target {
fixed3 c = i.color;
c *= _Color.rgb; // 使用颜色属性调整颜色
return fixed4(c, 1.0); // 返回最终颜色
}
•参数 v2f i,输入结构体 i。同时也是顶点着色器的输出结构体。
•SV_Target也是HLSL中的一个系统语义,它等同于告诉渲染器,把用户的输出颜色存储到一个渲染目标(render target)中,这里将输出到默认的帧缓存中。
•fixed3 c = i.color 用fixed类型的变量c获得输出结构体的颜色。
•c *= _Color.rgb 使用颜色属性调整颜色。rgb表示红绿蓝,是fixed 3类型的。
•return fixed4(c, 1.0) ,返回值是一个fixded4类型变量。受到SV_Target语义的控制,表示渲染的颜色。
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unity Shaders Book/Chapter 6/Diffuse Vertex-Level" {
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Diffuse;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
fixed3 color : COLOR;
};
v2f vert(a2v v) {
v2f o;
// Transform the vertex from object space to projection space
o.pos = UnityObjectToClipPos(v.vertex);
// Get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// Transform the normal from object space to world space
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
// Get the light direction in world space
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
// Compute diffuse term
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));
o.color = ambient + diffuse;
return o;
}
fixed4 frag(v2f i) : SV_Target {
return fixed4(i.color, 1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
说人话:
Shader "Unity Shaders Book/Chapter 6/Diffuse Vertex-Level" {}
•为这个Shader起一个名字。
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
}
•为了得到并且控制材质的漫反射颜色,我们首先在Shader的Properties语义块中声明了一个Color类型的属性,并把它的初始值设为白色。
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
}
}
•在SubShader语义块中定义了一个Pass语义块。这是因为顶点/片元着色器的代码需要写在Pass语义块,而非SubShader语义块中。而且,我们在Pass的第一行指明了该Pass的光照模式。
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
•使用CGPROGRAM和ENDCG来包围CG代码片,以定义最重要的顶点着色器和片元着色器代码。首先,我们使用#pragma指令来告诉Unity,我们定义的顶点着色器和片元着色器叫什么名字。在本例中,它们的名字分别是vert和frag。
#include "Lighting.cginc"
•为了使用Unity内置的一些变量,如后面要讲到的_LightColor0,还需要包含进Unity的内置文件Lighting.cginc。
fixed4 _Diffuse;
•为了在Shader中使用Properties语义块中声明的属性,我们需要定义一个和该属性类型相匹配的变量:
•由于颜色属性的范围在0到1之间,因此我们可以使用fixed精度的变量来存储它。
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
fixed3 color : COLOR;
};
•然后,我们定义了顶点着色器的输入和输出结构体(输出结构体同时也是片元着色器的输入结构体)。
v2f vert(a2v v) {
v2f o;
// Transform the vertex from object space to projection space
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// Get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// Transform the normal fram object space to world space
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)_World2Object));
// Get the light direction in world space
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
// Compute diffuse term
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,
worldLight));
o.color = ambient + diffuse;
return o;
}
•在第一行,我们首先定义了返回值o。我们已经重复过很多次,顶点着色器最基本的任务就是把顶点位置从模型空间转换到裁剪空间中。
• o.pos = mul(UNITY_MATRIX_MVP, v.vertex) 因此我们需要使用Unity内置的模型世界投影矩阵UNITY_MATRIX_MVP来完成这样的坐标变换。或者使用o.pos = UnityObjectToClipPos(v.vertex) 将顶点位置转换为裁剪空间
• fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz 接下来,我们通过Unity的内置变量UNITY_LIGHTMODEL_AMBIENT得到了环境光部分。
•fixed3 worldNormal = normalize(mul(v.normal, (float3x3)World2Object)) 因为a2v得到的顶点法线是位于模型空间下的,World2Object是模型到空间的逆矩阵。通过模型空间法线和模型到空间的逆矩阵相乘将法线转换到世界空间中。法线是一个三维矢量,因此我们只需要截取_World2Object的前三行前三列即可。
•在得到了世界空间中的法线和光源方向后,我们需要对它们进行归一化操作。
•fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz) 获得指向光源的单位矢量。
• 漫反射计算公式 clight:入射光线颜色和强度 mdiffuse:材质的漫反射系数 n:表面法线 I:光源方向
•fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,
worldLight)) 在得到它们点积的结果后,我们需要防止这个结果为负值。为此,我们使用了saturate函数。saturate函数是CG提供的一种函数,它的作用是可以把参数截取到[0, 1]的范围内。最后,再与光源的颜色和强度以及材质的漫反射颜色相乘即可得到最终的漫反射光照部分。
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unity Shaders Book/Chapter 6/Diffuse Pixel-Level" {
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Diffuse;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
};
v2f vert(a2v v) {
v2f o;
// Transform the vertex from object space to projection space
o.pos = UnityObjectToClipPos(v.vertex);
// Transform the normal from object space to world space
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
return o;
}
fixed4 frag(v2f i) : SV_Target {
// Get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// Get the normal in world space
fixed3 worldNormal = normalize(i.worldNormal);
// Get the light direction in world space
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
// Compute diffuse term
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
fixed3 color = ambient + diffuse;
return fixed4(color, 1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
说人话:
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
};
•修改顶点着色器的输出结构体v2f。从fixed3 color : COLOR修改为float3 worldNormal : TEXCOORD0。
v2f vert(a2v v) {
v2f o;
// Transform the vertex from object space to projection space
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// Transform the normal fram object space to world space
o.worldNormal = mul(v.normal, (float3x3)_World2Object);
return o;
}
•顶点着色器不需要计算光照模型,只需要把世界空间下的法线传递给片元着色器即可。在片元着色器中计算光照模型。只有顶点着色器可以得到顶点的法线以及顶点相关的位置信息,片元着色器得不到顶点,所以需要顶点着色器中进行计算然后传递。
•v2f o 声明输出结构体。
•o.pos = mul(UNITY_MATRIX_MVP, v.vertex) 计算世界空间下顶点位置。
• o.worldNormal = mul(v.normal, (float3x3)_World2Object) 计算顶点世界法线位置。
fixed4 frag(v2f i) : SV_Target {
// Get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// Get the normal in world space
fixed3 worldNormal = normalize(i.worldNormal);
// Get the light direction in world space
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
// Compute diffuse term
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,
worldLightDir));
fixed3 color = ambient + diffuse;
return fixed4(color, 1.0);
}
•片元着色器需要计算漫反射光照模型。
•fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz 通过内置函数获得自发光。
•fixed3 worldNormal = normalize(i.worldNormal) 归一化世界法线。
•fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz) 归一化世界光源方向。
• fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,
worldLightDir)) 根据光照模型计算漫反射。
•fixed3 color = ambient + diffuse 得到自发光和漫反射的颜色。
•逐顶点和逐像素的区别:
逐顶点光照是在 顶点shader的时候计算顶点光照颜色,然后到片元着色的时候通过插值,得到片元的光照计算的结果。逐像素光照是在片元着色的时候做光照计算,逐像素光照性能开销大,但是效果好,逐顶点光照,性能开销小,效果比逐像素差。
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unity Shaders Book/Chapter 6/Specular Vertex-Level" {
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
_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 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
fixed3 color : COLOR;
};
v2f vert(a2v v) {
v2f o;
// Transform the vertex from object space to projection space
o.pos = UnityObjectToClipPos(v.vertex);
// Get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// Transform the normal from object space to world space
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
// Get the light direction in world space
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
// Compute diffuse term
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
// Get the reflect direction in world space
fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
// Get the view direction in world space
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz);
// Compute specular term
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
o.color = ambient + diffuse + specular;
return o;
}
fixed4 frag(v2f i) : SV_Target {
return fixed4(i.color, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
•高光反射计算公式
clight:入射光线的颜色和强度 mspecluar:材质的高光反射系数 v:视角方向 r:反射方向
•反射方向r由表面法线n和光源方向I计算得到。可推导而成
•CG提供了计算反射方向的函数reflect 函数:reflect(i, n)
参数:i,入射方向;n,法线方向。可以是float、float2、float3等类型。
描述:当给定入射方向i和法线方向n时,reflect函数可以返回反射方向。图6.9给出了参数和返回值之间的关系。
说人话:
Shader "Unity Shaders Book/Chapter 6/Specular Vertex-Level" { }
•首先,我们需要为这个Shader起一个名字。
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
•为了在材质面板中能够方便地控制高光反射属性,我们在Shader的Properties语义块中声明了三个属性。其中,新添加的Specular用于控制材质的高光反射颜色,而Gloss用于控制高光区域的大小。
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
}
}
•我们在SubShader语义块中定义了一个Pass语义块。这是因为顶点/片元着色器的代码需要写在Pass语义块,而非SubShader语义块中。而且,我们在Pass的第一行指明了该Pass的光照模式。
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
•使用CGPROGRAM和ENDCG来包围CG代码片,以定义最重要的顶点着色器和片元着色器代码。首先,我们使用#pragma指令来告诉Unity,我们定义的顶点着色器和片元着色器叫什么名字。在本例中,它们的名字分别是vert和frag。
#include "Lighting.cginc"
•为了使用Unity内置的一些变量,如_LightColor0,还需要包含进Unity的内置文件Lighting.cginc。
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
•为了在Shader中使用Properties语义块中声明的属性,我们需要定义和这些属性类型相匹配的变量
•由于颜色属性的范围在0到1之间,因此对于_Diffuse和_Specular属性我们可以使用fixed精度的变量来存储它。而_Gloss的范围很大,因此我们使用float精度来存储。
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
fixed3 color : COLOR;
};
•然后,我们定义了顶点着色器的输入和输出结构体(输出结构体同时也是片元着色器的输入结构体)。
v2f vert(a2v v) {
v2f o;
// Transform the vertex from object space to projection space
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// Get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// Transform the normal fram object space to world space
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)_World2Object));
// Get the light direction in world space
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
// Compute diffuse term
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
// Get the reflect direction in world space
fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
// Get the view direction in world space
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(_Object2World, v.vertex).xyz);
// Compute specular term
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
o.color = ambient + diffuse + specular;
return o;
}
•在顶点着色器中,我们计算了包含高光反射的光照模型。
•漫反射部分的计算和6.4节中的代码完全一致。
• fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal)) 我们首先计算了入射光线方向关于表面法线的反射方向reflectDir。由于CG的reflect函数的入射方向要求是由光源指向交点处的,因此我们需要对worldLightDir取反后再传给reflect函数。worldLightDir是交点处指向光源。
•fixed3 viewDir = normalize(WorldSpaceCameraPos.xyz - mul(Object2World, v.vertex).xyz) 然后,我们通过_WorldSpaceCameraPos得到了世界空间中的摄像机位置,再通过和顶点世界空间位置相减即可得到世界空间下的视角方向,这是单位视角方向。
• fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss) 得到了所有的4个参数,代入公式即可得到高光反射的光照部分。最后,再和环境光、漫反射光相加存储到最后的颜色中。
fixed4 frag(v2f i) : SV_Target {
return fixed4(i.color, 1.0);
}
•片元着色器的代码非常简单,我们只需要直接返回顶点颜色即可。
Fallback "Specular"
•)最后,我们需要把这个Unity Shader的回调Shader设置为内置的Specular。
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unity Shaders Book/Chapter 6/Specular Pixel-Level" {
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
_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 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};
v2f vert(a2v v) {
v2f o;
// Transform the vertex from object space to projection space
o.pos = UnityObjectToClipPos(v.vertex);
// Transform the normal from object space to world space
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
// Transform the vertex from object spacet to world space
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
fixed4 frag(v2f i) : SV_Target {
// Get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
// Compute diffuse term
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
// Get the reflect direction in world space
fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
// Get the view direction in world space
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
// Compute specular term
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
说人话:
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};
•修改顶点着色器的输出结构体v2f。修改为世界法线和世界坐标。语义为第一组纹理坐标和第二组纹理坐标。
v2f vert(a2v v) {
v2f o;
// Transform the vertex from object space to projection space
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// Transform the normal fram object space to world space
o.worldNormal = mul(v.normal, (float3x3)_World2Object);
// Transform the vertex from object space to world space
o.worldPos = mul(_Object2World, v.vertex).xyz;
return o;
}
•顶点着色器只需要计算世界空间下的法线方向和顶点坐标,并把它们传递给片元着色器即可。
fixed4 frag(v2f i) : SV_Target {
// Get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
// Compute diffuse term
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
// Get the reflect direction in world space
fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
// Get the view direction in world space
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
// Compute specular term
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
•片元着色器需要计算关键的光照模型。
•Blinn模型没有使用反射方向,而是引入一个新的矢量h,它是通过对视角方向v和光照方向I相加后再归一化得到的。即
而Blinn模型计算高光反射的公式如下:
将逐像素高光反射SHader中的片元着色器中对高光反射部分的计算代码进行修改。
fixed4 frag(v2f i) : SV_Target {
...
// Get the view direction in world space
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
// Get the half direction in world space
fixed3 halfDir = normalize(worldLightDir + viewDir);
// Compute specular term
fixed3 specular=_LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
•fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz) 得到摄像机视角。
•fixed3 halfDir = normalize(worldLightDir + viewDir) 得到新的矢量h。通过将世界光照方向和摄像机视角方向归一化得到的。
• fixed3 specular=_LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss) 按照Blinn-Phong 光照模型公式进行计算得到高光反射。
UnityCG.cginc中一些常用的帮助函数:
函数名 | 描述 |
---|---|
float3 WorldSpaceViewDir (float4 v) | 输入一个模型空间中的顶点位置,返回世界空间中从该点到摄像机的观察方向,内部实现使用UnityWorldSpaceViewDir函数 |
float3 UnityWorldSpaceViewDir (float4 v) | 输入一个世界空间中的顶点位置,返回世界空间中从该点到摄像机的观察方向 |
float3 ObjSpaceViewDir (float4 v) | 输入一个模型空间中的顶点位置,返回模型空间中从该点到摄像机的观察方向 |
float3 WorldSpaceLightDir (float4 v) | 仅可用于前向渲染中,输入一个模型空间中的顶点位置,返回世界空间中从该点到光源的光照方向。内部实现使用了UnityWorldSpaceLightDir函数,没有被归一化 |
float3 UnityWorldSpaceLightDir (float4 v) | 仅可用于前向渲染中,输入一个世界空间中的顶点位置,返回世界空间中从该点到光源的光照方向。没有被归一化 |
float3 ObjSpaceLightDir (float4 v) | 仅可用于前向渲染中,输入一个模型空间中的顶点位置,返回模型空间中从该点到光源的光照方向。没有被归一化 |
float3 UnityObjectToWorldNormal (float3 norm) | 把法线方向从模型空间转换到世界空间中 |
float3 UnityObjectToWorldDir (in float3 dir) | 把方向矢量从模型空间转换到世界空间中 |
float3 UnityWorldToWorldDir (float3 dir) | 把方向矢量从世界空间转换到模型空间中 |
•需要注意的是,这些函数都没有保证得到的方向矢量是单位矢量,因此,我们需要在使用前把它们归一化。
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unity Shaders Book/Chapter 6/Blinn-Phong Use Built-in Functions" {
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(1.0, 500)) = 20
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float4 worldPos : TEXCOORD1;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
// Use the build-in funtion to compute the normal in world space
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNormal);
// Use the build-in funtion to compute the light direction in world space
// Remember to normalize the result
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
// Use the build-in funtion to compute the view direction in world space
// Remember to normalize the result
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"
}
说人话:
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
// Use the build-in funtion to compute the normal in world space
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
return o;
}
•o.pos = UnityObjectToClipPos(v.vertex) 把顶点位置从模型空间转换到裁剪空间。
o.worldNormal = UnityObjectToWorldNormal(v.normal) 把法线方向从模型空间转换到世界空间中。
fixed4 frag(v2f i) : SV_Target {
...
fixed3 worldNormal = normalize(i.worldNormal);
// Use the build-in function to compute the light direction in world space
// Remember to normalize the result
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
...
// Use the build-in function to compute the view direction in world space
// Remember to normalize the result
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
...
}
•在片元着色器中,我们使用内置的UnityWorldSpaceLightDir函数和UnityWorldSpaceView Dir函数来分别计算世界空间的光照方向和视角方向。
• fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)) 输入一个世界空间的顶点位置,返回世界空间中从该点到光源的光照方向,没有被归一化。
•fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)) 输入一个世界空间中的顶点位置,返回世界空间中从该点到摄像机的观察方向。没有被归一化。
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unity Shaders Book/Chapter 7/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"
}
说人话:
Shader "Unity Shaders Book/Chapter 7/Single Texture" {
•首先,我们需要为这个Unity Shader起一个名字。
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
}
•为了使用纹理,我们需要在Properties语义块中添加一个纹理属性。
•MainTex的纹理,在3.3.2节中,我们已经知道2D是纹理属性的声明方式。我们使用一个字符串后跟一个花括号作为它的初始值,“white”是内置纹理的名字,也就是一个全白的纹理。为了控制物体的整体色调,我们还声明了一个Color属性。
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
}
}
•然后,我们在SubShader语义块中定义了一个Pass语义块。而且,我们在Pass的第一行指明了该Pass的光照模式。
•LightMode标签是Pass标签中的一种,它用于定义该Pass在Unity的光照流水线中的角色。
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
•接着,我们使用CGPROGRAM和ENDCG来包围住CG代码片,以定义最重要的顶点着色器和片元着色器代码。首先,我们使用#pragma指令来告诉Unity,我们定义的顶点着色器和片元着色器叫什么名字。在本例中,它们的名字分别是vert和frag。
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Specular;
float _Gloss;
•我们需要在CG代码片中声明和上述属性类型相匹配的变量,以便和材质面板中的属性建立联系。
•与其他属性类型不同的是,我们还需要为纹理类型的属性声明一个float4类型的变量MainTex_ST。其中,MainTex_ST的名字不是任意起的。在Unity中,我们需要使用纹理名ST的方式来声明某个纹理的属性。其中,ST是缩放(scale)和平移(translation)的缩写。MainTex_ST可以让我们得到该纹理的缩放和平移(偏移)值,MainTex_ST.xy存储的是缩放值,而MainTex_ST.zw存储的是偏移值。这些值可以在材质面板的纹理属性中调节。
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;
};
•接下来,我们需要定义顶点着色器的输入和输出结构体。
•在上面的代码中,我们首先在a2v结构体中使用TEXCOORD0语义声明了一个新的变量texcoord,这样Unity就会将模型的第一组纹理坐标存储到该变量中。然后,我们在v2f结构体中添加了用于存储纹理坐标的变量uv,以便在片元着色器中使用该坐标进行纹理采样。
v2f vert(a2v v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(_Object2World, 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;
}
•然后,我们定义了顶点着色器。
• o.uv = v.texcoord.xy * MainTex_ST.xy + MainTex_ST.zw 在顶点着色器中,我们使用纹理的属性值MainTex_ST来对顶点纹理坐标(v.texcoord.xy )进行变换,得到最终的纹理坐标。计算过程是,首先使用缩放属性MainTex_ST.xy对顶点纹理坐标(v.texcoord.xy ))进行缩放,然后再使用偏移属性MainTex_ST.zw对结果进行偏移。Unity提供了一个内置宏TRANSFORM_TEX来帮我们计算上述过程。TRANSFORM_TEX是在UnityCG.cginc中定义的。也就是o.uv = TRANSFORM_TEX(v.texcoord, _MainTex)。
// Transforms 2D UV by scale/bias property
#define TRANSFORM_TEX(tex, name) (tex.xy * name##_ST.xy + name##_ST.zw)
它接受两个参数,第一个参数是顶点纹理坐标,第二个参数是纹理名,在它的实现中,将利用纹理名_ST的方式来计算变换后的纹理坐标。
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);
}
•我们还需要实现片元着色器,并在计算漫反射时使用纹理中的纹素值。
•上面的代码首先计算了世界空间下的法线方向和光照方向。然后,使用CG的tex2D函数对纹理进行采样。它的第一个参数是需要被采样的纹理,第二个参数是一个float2类型的纹理坐标,它将返回计算得到的纹素值。我们使用采样结果和颜色属性_Color的乘积来作为材质的反射率albedo,并把它和环境光照相乘得到环境光部分。随后,我们使用albedo来计算漫反射光照的结果,并和环境光照、高光反射光照相加后返回。
• fixed3 worldNormal = normalize(i.worldNormal); 计算世界空间下法线方向
•fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));计算世界空间下光照方向
• fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb; 使用CG的tex2D函数对纹理进行采样。使用采样结果和颜色属性Color的乘积来作为材质的反射率albedo。
•fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; 并把它和环境光照相乘得到环境光部分
• fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));随后,我们使用albedo来计算漫反射光照的结果
•return fixed4(ambient + diffuse + specular, 1.0); 并和环境光照、高光反射光照相加后返回。
Fallback "Specular"
•最后,我们为该Shader设置了合适的Fallback。
由于法线方向的分量范围在[-1, 1],而像素的分量范围为[0, 1],因此我们需要做一个映射,通常使用的映射就是
这就要求,我们在Shader中对法线纹理进行纹理采样后,还需要对结果进行一次反映射的过程,以得到原先的法线方向。反映射的过程实际就是使用上面映射函数的逆函数:
normal=pixel ×2-1
1,在切线空间下计算
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unity Shaders Book/Chapter 7/Normal Map In Tangent Space" {
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;
float3 viewDir : TEXCOORD2;
};
// Unity doesn't support the 'inverse' function in native shader
// so we write one by our own
// Note: this function is just a demonstration, not too confident on the math or the speed
// Reference: http://answers.unity3d.com/questions/218333/shader-inversefloat4x4-function.html
float4x4 inverse(float4x4 input) {
#define minor(a,b,c) determinant(float3x3(input.a, input.b, input.c))
float4x4 cofactors = float4x4(
minor(_22_23_24, _32_33_34, _42_43_44),
-minor(_21_23_24, _31_33_34, _41_43_44),
minor(_21_22_24, _31_32_34, _41_42_44),
-minor(_21_22_23, _31_32_33, _41_42_43),
-minor(_12_13_14, _32_33_34, _42_43_44),
minor(_11_13_14, _31_33_34, _41_43_44),
-minor(_11_12_14, _31_32_34, _41_42_44),
minor(_11_12_13, _31_32_33, _41_42_43),
minor(_12_13_14, _22_23_24, _42_43_44),
-minor(_11_13_14, _21_23_24, _41_43_44),
minor(_11_12_14, _21_22_24, _41_42_44),
-minor(_11_12_13, _21_22_23, _41_42_43),
-minor(_12_13_14, _22_23_24, _32_33_34),
minor(_11_13_14, _21_23_24, _31_33_34),
-minor(_11_12_14, _21_22_24, _31_32_34),
minor(_11_12_13, _21_22_23, _31_32_33)
);
#undef minor
return transpose(cofactors) / determinant(input);
}
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
///
/// Note that the code below can handle both uniform and non-uniform scales
///
// Construct a matrix that transforms a point/vector from tangent space to world space
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
/*
float4x4 tangentToWorld = float4x4(worldTangent.x, worldBinormal.x, worldNormal.x, 0.0,
worldTangent.y, worldBinormal.y, worldNormal.y, 0.0,
worldTangent.z, worldBinormal.z, worldNormal.z, 0.0,
0.0, 0.0, 0.0, 1.0);
// The matrix that transforms from world space to tangent space is inverse of tangentToWorld
float3x3 worldToTangent = inverse(tangentToWorld);
*/
//wToT = the inverse of tToW = the transpose of tToW as long as tToW is an orthogonal matrix.
float3x3 worldToTangent = float3x3(worldTangent, worldBinormal, worldNormal);
// Transform the light and view dir from world space to tangent space
o.lightDir = mul(worldToTangent, WorldSpaceLightDir(v.vertex));
o.viewDir = mul(worldToTangent, WorldSpaceViewDir(v.vertex));
///
/// Note that the code below can only handle uniform scales, not including non-uniform scales
///
// Compute the binormal
// float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w;
// // Construct a matrix which transform vectors from object space to tangent space
// float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);
// Or just use the built-in macro
// TANGENT_SPACE_ROTATION;
//
// // Transform the light direction from object space to tangent space
// o.lightDir = mul(rotation, normalize(ObjSpaceLightDir(v.vertex))).xyz;
// // Transform the view direction from object space to tangent space
// o.viewDir = mul(rotation, normalize(ObjSpaceViewDir(v.vertex))).xyz;
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 tangentLightDir = normalize(i.lightDir);
fixed3 tangentViewDir = normalize(i.viewDir);
// Get the texel in the normal map
fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
fixed3 tangentNormal;
// If the texture is not marked as "Normal map"
// tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;
// tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
// Or mark the texture as "Normal map", and use the built-in funciton
tangentNormal = UnpackNormal(packedNormal);
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);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
说人话:
Shader "Unity Shaders Book/Chapter 7/Normal Map In Tangent Space" { }
•首先,我们为该Unity Shader定义一个名字。
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
}
•然后,我们在Properties语义块中添加了法线纹理的属性,以及用于控制凹凸程度的属性。
•对于法线纹理BumpMap,我们使用"bump"作为它的默认值。"bump"是Unity内置的法线纹理,当没有提供任何法线纹理时,"bump"就对应了模型自带的法线信息。
•BumpScale则是用于控制凹凸程度的,当它为0时,意味着该法线纹理不会对光照产生任何影响。
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
}
}
•我们在SubShader语义块中定义了一个Pass语义块,并且在Pass的第一行指明了该Pass的光照模式。
•LightMode标签是Pass标签中的一种,它用于定义该Pass在Unity的光照流水线中的角色。
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
•)接着,我们使用CGPROGRAM和ENDCG来包围住CG代码片,以定义最重要的顶点着色器和片元着色器代码。首先,我们使用#pragma指令来告诉Unity,我们定义的顶点着色器和片元着色器叫什么名字。在本例中,它们的名字分别是vert和frag。
#include "Lighting.cginc"
•为了使用Unity内置的一些变量,如_LightColor0,还需要包含进Unity的内置文件Lighting.cginc。
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
fixed4 _Specular;
float _Gloss;
•为了和Properties语义块中的属性建立联系,我们在CG代码块中声明了和上述属性类型匹配的变量。
•为了得到该纹理的属性(平铺和偏移系数),我们为_MainTex和_BumpMap定义了_MainTex_ST和_BumpMap_ST变量。
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
•我们已经知道,切线空间是由顶点法线和切线构建出的一个坐标空间,因此我们需要得到顶点的切线信息。为此,我们修改顶点着色器的输入结构体a2v。
•我们使用TANGENT语义来描述float4类型的tangent变量,以告诉Unity把顶点的切线方向填充到tangent变量中。需要注意的是,和法线方向normal不同,tangent的类型是float4,而非float3,这是因为我们需要使用tangent.w分量来决定切线空间中的第三个坐标轴——副切线的方向性。
struct v2f {
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float3 lightDir: TEXCOORD1;
float3 viewDir : TEXCOORD2;
};
•我们需要在顶点着色器中计算切线空间下的光照和视角方向,因此我们在v2f结构体中添加了两个变量来存储变换后的光照和视角方向。
•由于我们使用了两张纹理,因此需要存储两个纹理坐标。为此,我们把v2f中的uv变量的类型定义为float4类型。
v2f vert(a2v v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
// Compute the binormal
// float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) *
v.tangent.w;
// // Construct a matrix which transform vectors from object space to tangent space
// float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);
// Or just use the built-in macro
TANGENT_SPACE_ROTATION;
// Transform the light direction from object space to tangent space
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
// Transform the view direction from object space to tangent space
o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;
return o;
}
• o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
其中xy分量存储了MainTex的纹理坐标,而zw分量存储了BumpMap的纹理坐标(实际上,MainTex和BumpMap通常会使用同一组纹理坐标,出于减少插值寄存器的使用数目的目的,我们往往只计算和存储一个纹理坐标即可)。
• TANGENT_SPACE_ROTATION;然后,我们把模型空间下切线方向、副切线方向和法线方向按行排列来得到从模型空间到切线空间的变换矩阵rotation。需要注意的是,在计算副切线时我们使用v.tangent.w和叉积结果进行相乘,这是因为和切线与法线方向都垂直的方向有两个,而w决定了我们选择其中哪一个方向。Unity也提供了一个内置宏TANGENT_SPACE_ROTATION(在UnityCG.cginc中被定义)来帮助我们直接计算得到rotation变换矩阵,它的实现和上述代码完全一样。
• o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;
然后,我们使用Unity的内置函数ObjSpaceLightDir和ObjSpaceViewDir来得到模型空间下的光照和视角方向,再利用变换矩阵rotation把它们从模型空间变换到切线空间中。
fixed4 frag(v2f i) : SV_Target {
fixed3 tangentLightDir = normalize(i.lightDir);
fixed3 tangentViewDir = normalize(i.viewDir);
// Get the texel in the normal map
fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
fixed3 tangentNormal;
// If the texture is not marked as "Normal map"
// tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;
// tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
// Or mark the texture as "Normal map", and use the built-in funciton
tangentNormal = UnpackNormal(packedNormal);
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);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal,
halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
•)由于我们在顶点着色器中完成了大部分工作,因此片元着色器中只需要采样得到切线空间下的法线方向,再在切线空间下进行光照计算即可。
• fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
在上面的代码中,我们首先利用tex2D对法线纹理BumpMap进行采样。正如本节一开头所讲的,法线纹理中存储的是把法线经过映射后得到的像素值,因此我们需要把它们反映射回来。如果我们没有在Unity里把该法线纹理的类型设置成Normal map(详见7.2.4节),就需要在代码中手动进行这个过程。
• tangentNormal = UnpackNormal(packedNormal);
我们首先把packedNormal的xy分量按之前提到的公式映射回法线方向。
•tangentNormal.xy *= _BumpScale;
然后乘以BumpScale(控制凹凸程度)来得到tangentNormal的xy分量。
• tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
由于法线都是单位矢量,因此tangentNormal.z分量可以由tangentNormal.xy计算而得。由于我们使用的是切线空间下的法线纹理,因此可以保证法线方向的z分量为正。
•在Unity中,为了方便Unity对法线纹理的存储进行优化,我们通常会把法线纹理的纹理类型标识成Normal map, Unity会根据平台来选择不同的压缩方法。这时,如果我们再使用上面的方法来计算就会得到错误的结果,因为此时_BumpMap的rgb分量并不再是切线空间下法线方向的xyz值了。在7.2.4节中,我们会具体解释。在这种情况下,我们可以使用Unity的内置函数UnpackNormal来得到正确的法线方向。
Fallback "Specular"
•最后,我们为该Unity Shader设置合适的Fallback。
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unity Shaders Book/Chapter 7/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"
}
说人话:
Shader "Unity Shaders Book/Chapter 7/Ramp Texture" {
•我们需要为这个Shader起一个名字。
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
}
•在Properties语义块中声明一个纹理属性来存储渐变纹理。
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
}
}
•然后,我们在SubShader语义块中定义了一个Pass语义块,并在Pass的第一行指明了该Pass的光照模式。
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
•然后,我们使用CGPROGRAM和ENDCG来包围住CG代码片,以定义最重要的顶点着色器和片元着色器代码。我们使用#pragma指令来告诉Unity,我们定义的顶点着色器和片元着色器叫什么名字。在本例中,它们的名字分别是vert和frag。
#include "Lighting.cginc"
•为了使用Unity内置的一些变量,如_LightColor0,还需要包含进Unity的内置文件Lighting.cginc。
fixed4 _Color;
sampler2D _RampTex;
float4 _RampTex_ST;
fixed4 _Specular;
float _Gloss;
•随后,我们需要定义和Properties中各个属性类型相匹配的变量。
•我们为渐变纹理_RampTex定义了它的纹理属性变量_RampTex_ST。
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 = mul(UNITY_MATRIX_MVP, v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(_Object2World, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _RampTex);
return o;
}
•定义顶点着色器函数。
•TRANSFORM_TEX(v.texcoord, _RampTex)使用了内置的TRANSFORM_TEX宏来计算经过平铺和偏移后的纹理坐标。
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);
}
•定义片元着色器函数。
•fixed halfLambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;
在上面的代码中,我们使用6.4.3节中提到的半兰伯特模型,通过对法线方向和光照方向的点积做一次0.5倍的缩放以及一个0.5大小的偏移来计算半兰伯特部分halfLambert。这样,我们得到的halfLambert的范围被映射到了[0,1]之间。
•fixed3 diffuseColor = tex2D(_RampTex, fixed2(halfLambert, halfLambert)).rgb *Color.rgb;
之后,我们使用halfLambert来构建一个纹理坐标,并用这个纹理坐标对渐变纹理RampTex进行采样。由于RampTex实际就是一个一维纹理(它在纵轴方向上颜色不变),因此纹理坐标的u和v方向我们都使用了halfLambert。然后,把从渐变纹理采样得到的颜色和材质颜色_Color相乘,得到最终的漫反射颜色。
•剩下的代码就是计算高光反射和环境光,并把它们的结果进行相加。相信读者已经对这些步骤非常熟悉了。
Fallback "Specular"
•最后,我们为该Unity Shader设置合适的Fallback。
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unity Shaders Book/Chapter 7/Mask Texture" {
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" {}
_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;
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);
// Get the mask value
fixed specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularScale;
// Compute specular term with the specular mask
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss) * specularMask;
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
说人话:
Shader "Unity Shaders Book/Chapter 7/Mask Texture" {
•首先,我们需要为这个Shader起一个名字。
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" {}
_SpecularScale ("Specular Scale", Float) = 1.0
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
•需要在Properties语义块中声明更多的变量来控制高光反射。上面属性中的SpecularMask即是我们需要使用的高光反射遮罩纹理,SpecularScale则是用于控制遮罩影响度的系数。
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
}
}
•然后,我们在SubShader语义块中定义了一个Pass语义块,并在Pass的第一行指明了该Pass的光照模式。
•LightMode标签是Pass标签中的一种,它用于定义该Pass在Unity的光照流水线中的角色。
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
•我们使用CGPROGRAM和ENDCG来包围住CG代码片,以定义最重要的顶点着色器和片元着色器代码。我们使用#pragma指令来告诉Unity,我们定义的顶点着色器和片元着色器叫什么名字。在本例中,它们的名字分别是vert和frag。
#include "Lighting.cginc"
•为了使用Unity内置的一些变量,如_LightColor0,还需要包含进Unity的内置文件Lighting.cginc。
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float _BumpScale;
sampler2D _SpecularMask;
float _SpecularScale;
fixed4 _Specular;
float _Gloss;
•随后,我们需要定义和Properties中各个属性类型相匹配的变量。
•我们为主纹理MainTex、法线纹理BumpMap和遮罩纹理SpecularMask定义了它们共同使用的纹理属性变量MainTex_ST。这意味着,在材质面板中修改主纹理的平铺系数和偏移系数会同时影响3个纹理的采样。使用这种方式可以让我们节省需要存储的纹理坐标数目,如果我们为每一个纹理都使用一个单独的属性变量TextureName_ST,那么随着使用的纹理数目的增加,我们会迅速占满顶点着色器中可以使用的插值寄存器。而很多时候,我们不需要对纹理进行平铺和位移操作,或者很多纹理可以使用同一种平铺和位移操作,此时我们就可以对这些纹理使用同一个变换后的纹理坐标进行采样。
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 = mul(UNITY_MATRIX_MVP, 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;
}
• TANGENT_SPACE_ROTATION;
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;在顶点着色器中,我们对光照方向和视角方向进行了坐标空间的变换,把它们从模型空间变换到了切线空间中,以便在片元着色器中和法线进行光照运算。
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);
// Get the mask value
fixed specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularScale;
// Compute specular term with the specular mask
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal,
halfDir)), _Gloss) * specularMask;
return fixed4(ambient + diffuse + specular, 1.0);
}
•使用遮罩纹理的地方是片元着色器。我们使用它来控制模型表面的高光反射强度。
•环境光照和漫反射光照和之前使用过的代码完全一样。
• fixed specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularScale;
在计算高光反射时,我们首先对遮罩纹理SpecularMask进行采样。由于本书使用的遮罩纹理中每个纹素的rgb分量其实都是一样的,表明了该点对应的高光反射强度,在这里我们选择使用r分量来计算掩码值。然后,我们用得到的掩码值和SpecularScale相乘,一起来控制高光反射的强度。
•需要说明的是,我们使用的这张遮罩纹理其实有很多空间被浪费了——它的rgb分量存储的都是同一个值。在实际的游戏制作中,我们往往会充分利用遮罩纹理中的每一个颜色通道来存储不同的表面属性。