文章目录
在Unity Shader中实现漫反射光照模型
基本光照模型中漫反射部分的计算:
c
l
i
g
h
t
c_{light}
clight:入射光线的颜色和强度
m
d
i
f
f
u
s
e
m_{diffuse}
mdiffuse:材质的漫反射系数
n
\boldsymbol{n}
n:法线方向
I
\boldsymbol{I}
I :指向光源的单位矢量。
max():防止点积结果为负值
CG中的saturate函数:
参数:x,用于操作的标量或矢量,可以是float、float2、float3等类型。
描述:把x截取在[0, 1]范围内,如果x是一个矢量,那么会对它的每一个分量进行这样的操作。
逐顶点的漫反射光照
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unlit/6-2Shader" {
Properties {
// 得到并控制材质的漫反射颜色
_Diffuse ("Diffuse", Color) = (0.2, 0.4, 0.5, 0.6)
}
SubShader {
Pass {
Tags {
// 指明该Pass的光照模式
"LightMode" = "ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc" // 使用unity的一些内置变量
fixed4 _Diffuse; // 使用Properties语义块中声明的属性,要先定义
struct a2v {
float4 vertex : POSITION;
float3 normal : 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;
// Transform the normal from object space to world space
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
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"
}
逐像素光照
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
Shader "Unlit/DiffusePixelLevel" {
Properties {
// 得到并控制材质的漫反射颜色
_Diffuse ("Diffuse", Color) = (0.3, 0.6, 0.5, 0.6)
}
SubShader {
Pass {
Tags {
// 指明该Pass的光照模式
"LightMode" = "ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc" // 使用unity的一些内置变量
fixed4 _Diffuse; // 使用Properties语义块中声明的属性,要先定义
struct a2v {
float4 vertex : POSITION;
float3 normal : 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, (float3x3)unity_WorldToObject);
return o;
}
fixed4 frag (v2f i) : SV_Target {
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;
return fixed4 (color, 1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}
逐像素光照可以得到更加平滑的光照效果。
但是,即便使用了逐像素漫反射光照,在光照无法到达的区域,模型的外观通常是全黑的,没有任何明暗变化,这会使模型的背光区域看起来就像一个平面一样,失去了模型细节表现。
实际上我们可以通过添加环境光来得到非全黑的效果,但即便这样仍然无法解决背光面明暗一样的缺点。
为此,有一种改善技术被提出来,这就是半兰伯特(Half Lambert)光照模型。
半兰伯特模型
广义的半兰伯特光照模型公式:
与原兰伯特模型相比,半兰伯特光照模型没有使用max操作来防止
n
\boldsymbol{n}
n 和
I
\boldsymbol{I}
I 的点积为负值,而是对其结果进行了一个α倍的缩放再加上一个β大小的偏移。
绝大多数情况下,α和β的值均为0.5:
通过这样的方式,我们可以把
n
⋅
I
\boldsymbol{n} · \boldsymbol{I}
n⋅I 的结果范围从[-1, 1]映射到[0, 1]范围内。对于模型的背光面,在原兰伯特光照模型中点积结果将映射到0处;而在半兰伯特模型中,背光面也可以有明暗变化,不同的点积结果会映射到不同的值上。
需要注意的是,半兰伯特是没有任何物理依据的,它仅仅是一个视觉加强技术。
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
Shader "Unlit/HalfLambert" {
Properties {
// 得到并控制材质的漫反射颜色
_Diffuse ("Diffuse", Color) = (0.3, 0.6, 0.5, 0.6)
}
SubShader {
Pass {
Tags {
// 指明该Pass的光照模式
"LightMode" = "ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc" // 使用unity的一些内置变量
fixed4 _Diffuse; // 使用Properties语义块中声明的属性,要先定义
struct a2v {
float4 vertex : POSITION;
float3 normal : 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, (float3x3)unity_WorldToObject);
return o;
}
fixed4 frag (v2f i) : SV_Target {
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5;
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * halfLambert;
fixed3 color = ambient + diffuse;
return fixed4 (color, 1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}
在Unity Shader中实现高光反射光照模型
反射方向
r
\boldsymbol{r}
r 的计算:
CG提供了计算反射方向的函数reflect:
函数:reflect(i, n)
参数:
I
\boldsymbol{I}
I ,入射方向
n
\boldsymbol{n}
n ,法线方向
描述:当给定入射方向i和法线方向n时,reflect函数可以返回反射方向
逐顶点光照
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unlit/SpecularVertexLevel" {
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"
// 由于颜色属性的范围在0到1之间,因此对于_Diffuse和_Specular属性我们可以使用fixed精度的变量来存储它
// 而_Gloss的范围很大,因此我们使用float精度来存储。
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex : POSITION;
float4 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;
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz);
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"
}
使用逐顶点的方法得到的高光效果有比较大的问题,可以看出高光部分明显不平滑。这主要是因为,高光反射部分的计算是非线性的,而在顶点着色器中计算光照再进行插值的过程是线性的,破坏了原计算的非线性关系,就会出现较大的视觉问题。因此,我们就需要使用逐像素的方法来计算高光反射。
逐像素光照
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
Shader "Unlit/SpecularPixelLevel" {
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;
float4 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};
// 顶点着色器只需要计算世界空间下的法线方向和顶点坐标,并把它们传递给片元着色器即可
v2f vert (a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
// Transform the normal from object space to world space
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
fixed4 frag (v2f i) : SV_Target {
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 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
Fallback "Specular"
}
Blinn-Phong光照模型
Blinn模型没有使用反射方向,而是引入一个新的矢量
h
\boldsymbol{h}
h ,它是通过对视角方向
v
\boldsymbol{v}
v 和光照方向
I
\boldsymbol{I}
I 相加后再归一化得到的:
Blinn模型计算高光反射:
我们只需要修改片元着色器中对高光反射部分的计算代码:
可以看出,Blinn-Phong光照模型的高光反射部分看起来更大、更亮一些。
在实际渲染中,绝大多数情况我们都会选择Blinn-Phong光照模型。
需要再次提醒的是,这两种光照模型都是经验模型,也就是说,我们不应该认为Blinn-Phong模型是对“正确的”Phong模型的近似。
实际上,在一些情况下, Blinn-Phong模型更符合实验结果。
使用Unity内置的函数
需要注意的是,这些函数都没有保证得到的方向矢量是单位矢量,因此,我们需要在使用前把它们归一化。
Unity帮我们处理了不同种类光源的情况。计算光源方向的3个函数:WorldSpaceLightDir、UnityWorldSpaceLightDir和ObjSpace LightDir仅可用于前向渲染。这是因为只有在前向渲染时,这3个函数里使用的内置变量_WorldSpaceLightPos0等才会被正确赋值。
使用内置函数改写Unity Shader
使用这些内置函数来改写使用Blinn-Phong光照模型的Unity Shader: