着色是根据材质属性(高光反射属性,漫反射属性等)、光源信息(如光源方向、辐照度等),使用等式去计算沿某个观察方向的出射度的过程。该等式即 光照模型(Lighting Model) 。
光照模型中包含不同的部分来计算光线经过物体表面后不同的方向
高光反射部分:表示光线在物体表面如何被反射
漫反射部分:表示光线在物体表面如何被折射,吸收和散射出表面
标准光照模型
标准光照模型将进入到摄像机内的光线分为4部分,每部分使用一种方法来计算其贡献度。
计算出反射方向后,由Phong模型计算高光反射部分:
M(gloss)是材质的光泽度,也成为反光度
Blinn模型提出一个简单的修改方法得到类似效果,避免计算反射方向,引入一个新的矢量,对入射方向和视角方向求和后归一化得到:
然后使用法线方向和新的矢量进行计算而不是反射方向和视角方向:
光照计算可以在顶点着色器中计算,称为逐顶点光照,也可以在片元着色器中计算,称为逐像素光照。 标准光照模型仅仅是一个经验模型,并不完全符合真实世界中的光照现象。这种模型的局限性在于:
不过由于易用性、计算速度和得到效果都比较好,被广泛应用。
漫反射光照模型实现
逐顶点漫反射实现
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Custom/Chapter6_DiffuseVertexLevel" {
Properties{
_Diffuse("DiffuseColor",Color) = (1.0,1.0,1.0,1.0)
}
SubShader{
Pass{
//定义光照模式,只有正确定光照模式,才能得到一些Unity内置光照变量
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//使用Unity的内置包含文件,使用其内置变量
#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;
o.pos = UnityObjectToClipPos(v.vertex);
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//通过模型到世界的转置逆矩阵计算得到世界空间内的顶点法向方向(v.normal存储的是模型空间内的顶点法线方向)
fixed3 worldNormal = normalize(mul(v.normal,(float3x3)unity_WorldToObject));
//得到世界空间内的光线方向
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
//根据Lambert定律计算漫反射 saturate函数将所得矢量或标量的值限定在[0,1]之间
fixed3 diffuse = _LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLight));
o.color = diffuse + ambient;
return o;
}
fixed4 frag(v2f i) :SV_Target{
return fixed4(i.color,1.0);
}
ENDCG
}
}
Fallback"Diffuse"
}
实现效果:
逐像素漫反射实现
Shader "Custom/Chapter6_DiffusePixelLevel" {
Properties{
_Diffuse("DiffuseColor",Color) = (1.0,1.0,1.0,1.0)
}
SubShader{
Pass{
//定义光照模式,只有正确定光照模式,才能得到一些Unity内置光照变量
Tags{ "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//使用Unity的内置包含文件,使用其内置变量
#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;
o.pos = UnityObjectToClipPos(v.vertex);
//存储世界空间下的法线,传递给片元着色器,
//只有顶点着色器才能接受来自模型空间的数据,因此需要先计算再传递给片元着色器
o.worldNormal =mul(v.normal,_World2Object);
return o;
}
fixed4 frag(v2f i) :SV_Target{
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLight = normalize(_WorldSpaceLightPos0);
fixed3 diffuse = _LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLight));
fixed3 color = diffuse + ambient;
return fixed4(color,1.0);
}
ENDCG
}
}
Fallback"Diffuse"
}
实现效果:
左侧为逐顶点实现效果,右侧为逐像素实现效果。可以看到,在逐顶点效果中,模型下部背光面与向光面的交界处会出现不平滑的锯齿过渡。而逐像素则交界处相对平滑。但以上两种均存在在光线无法到达的区域,模型外观通常全黑,没有明暗变化,使背光区域看起来像平面,为此提出一项改进技术——Half Lambert光照模型。
半兰伯特(Half Lambert)光照模型
广义半兰伯特光照模型计算公式:
半兰伯特没有使用max函数进行限制负值,对点积结果进行缩放后再做偏移。一般情况下,α和β的取值为0.5,0.5,这样将[-1,1]的结果映射到[0,1]。这样在原有兰伯特基础上,背光面取值为负的顶点在半兰伯特下,有了对应的到[0,0.5]的值,半兰伯特并没有物理基础,仅仅只是视觉加强技术而已。
半兰伯特实现:
fixed halfLambert = dot(worldNormal, worldLight)*0.5 + 0.5;
fixed3 diffuse = _LightColor0.rgb*_Diffuse.rgb*halfLambert;
实现效果:
高光反射光照模型实现
逐顶点高光反射实现
Shader "Custom/Chapter6_SpecularVertexLevel" {
Properties{
_Diffuse("DiffuseColor",Color)=(1,1,1,1)
_Specular("SpecualrColor",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;
o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(mul(v.normal,(float3x3)_World2Object));
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse = _LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLightDir));
//通过CG提供的reflect(i,n)提供的函数计算反射方向
fixed3 reflectDir = normalize(reflect(-worldLightDir,worldNormal));
//计算世界空间下的观察方向
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz-mul(_Object2World,v.vertex).xyz);
//计算高光反射部分
fixed3 specular = _LightColor0.rgb*_Specular.rgb*pow(saturate(dot(viewDir, reflectDir)), _Gloss);
o.color=ambient+diffuse+specular;
return o;
}
fixed4 frag(v2f i) :SV_Target{
return fixed4(i.color,1.0);
}
ENDCG
}
}
Fallback "Specular"
}
实现效果:
可以看出,逐顶点的高光反射在高光部分出现非常明显的锯齿不连续现象,由于高光计算部分pow()是非线性,因此顶点插值后的结果不理想。
逐像素高光反射实现
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Custom/Chapter6_SpecularPixelLevel" {
Properties{
_Diffuse("DiffuseColor",Color)=(1,1,1,1)
_Specular("SpecualrColor",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 worldNormal : TEXCOORD0;
fixed3 viewDir : TEXCOORD1;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = normalize(mul(v.normal,(float3x3)unity_WorldToObject));
//计算世界空间下的观察方向
o.viewDir = normalize(_WorldSpaceCameraPos.xyz-mul(unity_ObjectToWorld,v.vertex).xyz);
return o;
}
fixed4 frag(v2f i) :SV_Target{
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 worldNormal = i.worldNormal;
fixed3 diffuse = _LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal, worldLightDir));
//通过CG提供的reflect(i,n)提供的函数计算反射方向
fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
fixed3 viewDir = i.viewDir;
//计算高光反射部分
fixed3 specular = _LightColor0.rgb*_Specular.rgb*pow(saturate(dot(viewDir, reflectDir)), _Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
Fallback "Specular"
}
实现效果:
右侧为逐像素高光效果,高光边缘连续无明显锯齿现象
Blinn-Phong光照模型
Blinnn模型没有使用反射方向,引入新的变量 h 通过对光照方向i 和 视角方向 v 求和后归一化得到,
Blinn模型高光计算:
Blinn-Phong逐像素实现:
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Custom/Chapter6_SpecularBlinnPixelLevel" {
Properties{
_Diffuse("DiffuseColor",Color)=(1,1,1,1)
_Specular("SpecualrColor",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 worldNormal : TEXCOORD0;
fixed3 viewDir : TEXCOORD1;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = normalize(mul(v.normal,(float3x3)unity_WorldToObject));
//计算世界空间下的观察方向
o.viewDir = normalize(_WorldSpaceCameraPos.xyz-mul(unity_ObjectToWorld,v.vertex).xyz);
return o;
}
fixed4 frag(v2f i) :SV_Target{
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 worldNormal = i.worldNormal;
fixed3 diffuse = _LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal, worldLightDir));
fixed3 viewDir = i.viewDir;
fixed3 halfDir = normalize(worldLightDir+viewDir);
//计算高光反射部分
fixed3 specular = _LightColor0.rgb*_Specular.rgb*pow(saturate(dot(halfDir, worldNormal)), _Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
Fallback "Specular"
}
实现效果:
最右侧为逐像素Blinn-Phong效果,可以看出高光的光晕面积相对比Phong的要大一些。实际渲染中,大多数情况下选择Blinn-Phong模型。
在光照模型中,需要得到光源方向、视角方向等基本方向信息。通过自行计算的方式得到,如:
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
视角方向
o.viewDir = normalize(_WorldSpaceCameraPos.xyz-mul(unity_ObjectToWorld,v.vertex).xyz);
如果处理更加复杂的光照(如点光源和聚光灯),计算方向就是错误的。Unity提供了一些内置函数来计算这些信息。