模拟真实的光照环境:
高光反射(specular):表示物体表面是如何反射光线的
漫反射(diffuse):表示有多少光线被折射、吸收和散射出表面
直接光照(direct light):直接从光源发射出来经过物体表面一次发射后进入摄像机的光线
基本方法:把进入到摄像机的光线分为4部分,每一部分单独计算贡献度
环境光:一般是一个全局变量,即场景中的所有物体都使用同一个环境光
c a m b i e n t = g a m b i e n t c_{ambient} = g_{ambient} cambient=gambient
自发光:直接使用该材质的自发光颜色
c e m i s s i v e = m e m i s s i v e c_{emissive} = m_{emissive} cemissive=memissive
漫反射:符合兰伯特定律(Lambert’s Law):反射光线的强度与表面法线和光源方向的夹角的余弦值成正比
c d i f f u s e = ( c l i g h t ⋅ m d i f f u s e ) m a x ( 0 , n ⃗ ⋅ l ⃗ ) c_{diffuse} = (c_{light}·m_{diffuse})max(0,\vec{n}·\vec{l}) cdiffuse=(clight⋅mdiffuse)max(0,n⋅l)
高光反射:
需要知道的信息:表面法线 n ⃗ \vec{n} n、视角方向 v ⃗ \vec{v} v、光源方向 l ⃗ \vec{l} l、反射方向 r ⃗ \vec{r} r
r ⃗ = 2 ( n ⃗ ⋅ l ⃗ ) n ⃗ − l ⃗ \vec{r} = 2(\vec{n}·\vec{l})\vec{n} - \vec{l} r=2(n⋅l)n−l
Phong模型:
c s p e c u l a r = ( c l i g h t ⋅ m s p e c u l a r ) m a x ( 0 , v ⃗ ⋅ r ⃗ ) m g l o s s c_{specular} = (c_{light}·m_{specular})max(0,\vec{v}·\vec{r})^{m_{gloss}} cspecular=(clight⋅mspecular)max(0,v⋅r)mgloss
Blinn模型:
为了避免计算反射方向 r ⃗ \vec{r} r,引入一个新的矢量 h ⃗ \vec{h} h, h ⃗ \vec{h} h是由视角方向 v ⃗ \vec{v} v与光源方向 l ⃗ \vec{l} l取平均再归一化得到的,即
h ⃗ = v ⃗ + l ⃗ ∣ v ⃗ + l ∣ ⃗ \vec{h} = \frac{\vec{v}+\vec{l}}{\vec{|v}+\vec{l|}} h=∣v+l∣v+l
然后使用 n ⃗ \vec{n} n和 h ⃗ \vec{h} h之间的夹角进行计算
c s p e c u l a r = ( c l i g h t ⋅ m s p e c u l a r ) m a x ( 0 , n ⃗ ⋅ h ⃗ ) m g l o s s c_{specular} = (c_{light}·m_{specular})max(0,\vec{n}·\vec{h})^{m_{gloss}} cspecular=(clight⋅mspecular)max(0,n⋅h)mgloss
如果摄像机和光源距离模型足够远的情况下,我们可以认为 v ⃗ \vec{v} v
和 l ⃗ \vec{l} l是定值,所以 h ⃗ \vec{h} h是一个常量,此时Blinn模型会快于Phong模型
逐顶点光照(per-vertex lighting):在顶点着色器中使用基本光照模型
逐像素光照(per-pixel lighting):在片元着色器中使用基本光照模型
Unity中的环境光与自发光:
在Shader中,只需要通过内置变量
UNITY_LIGHTMODEL_AMBIENT
就可以直接得到环境光的信息
大多数物体是没有自发光特性的,所以不用去计算;如果非要计算自发光的话,就在片元着色器输出最后颜色之前,将材质的自发光颜色加进去就可以
逐顶点光照:
Shader内容为:
Shader "Custom/Chapter6-DiffuseVertexLevel" {
Properties{
//声明一个颜色属性:白色
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
}
SubShader{
Pass{
//定义该Pass在渲染中的角色
Tags{"LightMode" = "ForwardBase"}
//以下为Cg代码片,ENDCG结束
CGPROGRAM
#pragma vertex vert//顶点着色器
#pragma fragment frag//片元着色器
//包含内置的光照模型
#include "Lighting.cginc"
//使用定义的属性必须定义一个与属性相匹配的变量
//由于颜色范围在(0,1),所以使用fixed精度的变量
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;
////将法线方向从模型空间变换到世界空间
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
//世界空间中的光线方向
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
//按照公式计算漫反射 saturate在这里等同于max
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
}
}
//回调Shader设置为内置Diffuse
Fallback "Diffuse"
}
与原来代码相比:
UNITY_MATRIX_MVP
与顶点坐标的点乘替换为UnityObjectToClipPos
函数,将顶点坐标从模型空间(object space)变换到裁剪空间(clipping space),更直观更好理解逐像素光照:
Shader "Custom/Chapter6-DiffusePixelLevel" {
Properties{
//声明一个颜色属性:白色
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
}
SubShader{
Pass{
//定义该Pass在渲染中的角色
Tags{"LightMode" = "ForwardBase"}
//以下为Cg代码片,ENDCG结束
CGPROGRAM
#pragma vertex vert//顶点着色器
#pragma fragment frag//片元着色器
//包含内置的光照模型
#include "Lighting.cginc"
//使用定义的属性必须定义一个与属性相匹配的变量
//由于颜色范围在(0,1),所以使用fixed精度的变量
fixed4 _Diffuse;
//定义结构
struct a2v{
float4 vertex:POSITION;//顶点位置
float3 normal:NORMAL;//顶点法线
};
struct v2f{
float4 pos:SV_POSITION;
fixed3 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 worldLight = normalize(_WorldSpaceLightPos0.xyz);
//按照公式计算漫反射 saturate在这里等同于max
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLight));
//最终颜色值等于环境光加漫反射
fixed3 color = ambient + diffuse;
return fixed4(color,1.0);
}
ENDCG
}
}
//回调Shader设置为内置Diffuse
Fallback "Diffuse"
}
逐像素光照可以达到更加平滑的光照效果,但是有一个问题存在:在光照无法到达的区域,模型一般是黑的,没有任何明暗变化。这可以通过添加环境光来改善,但是仍然有背光面的存在。因此,半兰伯特光照模型被提出
半兰伯特光照模型:Half Lambert
为了解决上述问题,Valve公司在开发《半条命》的时候提出了一种新技术,在原Lambert模型上进行了修改
c d i f f u s e = ( c l i g h t ⋅ m d i f f u s e ) ( α ( n ⃗ ⋅ l ⃗ ) + β ) c_{diffuse} = (c_{light}·m_{diffuse})(\alpha(\vec{n}·\vec{l})+\beta) cdiffuse=(clight⋅mdiffuse)(α(n⋅l)+β)
与原模型相比,半兰伯特光照模型并没有使用max来限定 n ⃗ \vec{n} n, l ⃗ \vec{l} l为(0,1),而是对其结果进行一个 α \alpha α倍的缩放在进行一个 β \beta β的偏移,一般默认为0.5
c d i f f u s e = ( c l i g h t ⋅ m d i f f u s e ) ( 0.5 ( n ⃗ ⋅ l ⃗ ) + 0.5 ) c_{diffuse} = (c_{light}·m_{diffuse})(0.5(\vec{n}·\vec{l})+0.5) cdiffuse=(clight⋅mdiffuse)(0.5(n⋅l)+0.5)
m a x ( 0 , n ⃗ ⋅ l ⃗ ) max(0,\vec{n}·\vec{l}) max(0,n⋅l),当 n ⃗ \vec{n} n· l ⃗ \vec{l} l为负值时,即模型的背光面,点积值都变为了0,这样就导致没有明暗变化,而 α ( n ⃗ ⋅ l ⃗ ) + β \alpha(\vec{n}·\vec{l})+\beta α(n⋅l)+β将点积结果映射到了不同的值上
半兰伯特光照模型是没有任何物理依据的,只是一种视觉加强技术
在Shader代码中按公式修改漫反射计算部分即可
逐顶点光照:
公式回忆:
c s p e c u l a r = ( c l i g h t ⋅ m s p e c u l a r ) m a x ( 0 , v ⃗ ⋅ r ⃗ ) m g l o s s c_{specular} = (c_{light}·m_{specular})max(0,\vec{v}·\vec{r})^{m_{gloss}} cspecular=(clight⋅mspecular)max(0,v⋅r)mgloss
r ⃗ = 2 ( n ⃗ ⋅ l ⃗ ) n ⃗ − l ⃗ \vec{r} = 2(\vec{n}·\vec{l})\vec{n} - \vec{l} r=2(n⋅l)n−l
在Cg中提供了反射方向 r ⃗ \vec{r} r的计算函数reflect(i, n)
,i
为入射方向,一般为光源方向的相反方向
可以观察到高光部分不平滑,主要的原因是高光的计算是非线性的,而逐顶点的计算是线性的,这破坏了原计算的非线性关系,就出现以上问题
逐像素光照:
顶点着色器只需要计算世界空间下的法线方向和顶点坐标,然后传递给片元着色器即可
Blinn模型没有使用反射方向 r ⃗ \vec{r} r,而是定义了一个新的矢量 h ⃗ \vec{h} h
h ⃗ = v ⃗ + l ⃗ ∣ v ⃗ + l ∣ ⃗ \vec{h} = \frac{\vec{v}+\vec{l}}{\vec{|v}+\vec{l|}} h=∣v+l∣v+l
c s p e c u l a r = ( c l i g h t ⋅ m s p e c u l a r ) m a x ( 0 , n ⃗ ⋅ h ⃗ ) m g l o s s c_{specular} = (c_{light}·m_{specular})max(0,\vec{n}·\vec{h})^{m_{gloss}} cspecular=(clight⋅mspecular)max(0,n⋅h)mgloss
按照公式将高光计算部分修改即可
//计算h
fixed3 h = normalize(worldLight + viewDir);
//计算高光
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal, h)), _Gloss);
可以看出:Blinn-Phong模型的高光反射部分更大更亮一点,大多数情况下,在渲染的过程中都会选择此模型。