Ambient+Diffuse
Diffuse是漫反射,它实际上包含2个部分,一个是Ambient环境光照,另外一个才是Diffuse 为什么需要环境光照? 在显示世界当中,光照射在环境上,环境又会反射到物体上,在计算机上,我们为了模拟物体的反光,所以我们就直接使用固定的环境光照
Diffuse光照模型与向量计算
normalized N是定点的法向量,normalized L,是从定点指向光源方向的向量 求这两个向量的点积,就可以知道,这两个向量他们之间的夹角,从点积的结果上看 y=dot(N,L)
1≥y>0
90°≥θ>0°
y=0
θ=90°
0>y≥-1
180°≥θ>90°
利用这个特性,我们就可以计算某一个顶点应该接收光照的强度
上图顶点所在的模型物体在空间做了一个方位的旋转,此时法向量N在世界空间当中被旋转了,从顶点指向光源的向量保持不动,这个时候两个向量形成的夹角大于90度,所以点积的结果是负值。应当不接受光照 在实际使用当中负数乘上一个颜色值是没有意义的,所以负数取0即可
案例
- 首先在顶点程序当中进行MVP变换
归一化顶点的法向量,通过UnityShaderLab内建的_WorldSpaceLightPos0
内建光照相关的变量
光参数以不同的方式传递给着色器,具体取决于使用哪个
以及着色器中使用哪个
正向渲染(ForwardBase和ForwardAdd pass types)
Name
Type
Value
_LightColor0 *(在 UnityLightingCommon.cginc 中申明)*
fixed4
光照颜色
_WorldSpaceLightPos0
float4
定向光:(世界空间方向,0)。其他灯光:(世界空间位置,1)。
unity_WorldToLight (declared in AutoLight.cginc)
float4x4
世界到光源矩阵. 使用 sample cookie & attenuation textures.
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0
float4
_(仅适用于ForwardBase pass)_前四个不重要点光源的世界空间位置。
unity_4LightAtten0
float4
_(仅适用于ForwardBase pass)_前四个不重要点光源的衰减因子。
unity_LightColor
half4[4]
_(仅适用于ForwardBase pass)_前四个非重要点光源的颜色。
unity_WorldToShadow
float4x4[4]
世界到阴影矩阵。一种用于Spot Lights,最多四种用于方向光级联( Directional light cascades)
关于更多的Build-in shader可以访问
获取到顶点到光源的方向,并将其归一化
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
float3 N=normalize(v.normal);
float3 L=normalize(_WorldSpaceLightPos0);
return o;
}
-
将向量N与向量L进行点积计算,并使用saturate函数,使计算的点积映射到0~1之间 并将其值乘上光照颜色_LightColor0赋给COLOR
float ndotl=saturate(dot(N,L)); o.color=_LightColor0*ndotl;
```
-
在片元着色器当中添加环境光照 在最后返回i.color后面加上_UNITY_LIGHTMODEL_AMBIENT_
fixed4 frag (v2f i) : COLOR { return i.color;//+UNITY_LIGHTMODEL_AMBIENT }
在Window-Rendering-Lighting当中可以设置Environment Lighting 将Source改为Color,修改AmbientColor 就会影响模型的固定环境光照颜色
这样就好了吗?🤔️
其实我们写的Diffuse Shader有一个非常致命的Bug,我们拿我们的Diffuse Shader和Unity的DiffuseShader进行对比,其实就能发现问题
当我们将模型旋转一定角度时候,发现光照渲染的位置根本不正确 这是由于我们进行点积的法向量,和光照的方向的向量,不在同一个坐标系的原因导致的
v.normal是在模型坐标系,而_WorldSpaceLightPos0是世界坐标系 模型自身虽然旋转,但是v.normal相对于自身是没有变化的,_WorldSpaceLightPos0光照的方向也没有改变 所以模型即使旋转了,两者经过点积之后,还是和原来点积的结果一样,就导致了,光照计算后的结果是一致的
如何解决这个问🙋题❓
💡要在统一的坐标空间里计算法向量与光向量的点积 有两种方式
-
将v.normal变换到世界坐标系,再和光向量进行点积
float3 N=normalize(v.normal); float3 L=normalize(_WorldSpaceLightPos0); N=mul(unity_ObjectToWorld,float4(N,0)).xyz; float ndotl=saturate(dot(N,L)); -
将_WorldSpaceLightPos0变换到模型自身的坐标系,再和法向量进行点积
float3 N=normalize(v.normal); float3 L=normalize(_WorldSpaceLightPos0); L=mul(unity_WorldToObject,float4(L,0)).xyz; float ndotl=saturate(dot(N,L));
现在效果就一样了…………吗❓
我们把模型进行非等比例的缩放,可以发现,我们的Diffuse着色器和Unity的Diffuse着色器还是不一样
这是为什么呢❓
💡原因是经过非等比例缩放之后,顶点法线与切线不再垂直
上图中x轴缩短为原来的1/2,法线n与切线p3-p2,不再垂直 对于切线它始终由p3-p2来表示,所以p2怎么变,都不会影响到切线 $$ 法线(\vec{n})与切线(\vec{t}):(p3-p2) $$ $$ 我们设unity_ObjectToWorld为M,(\vec{n})转世界空间矩阵为G $$ $$ 我们最终的目的是希望转换后的(\vec{t^{’}})与(\vec{n^{’}})依旧保持垂直关系 $$ $$ 所以就有:(M\vec{t}\cdot G\vec{n}=0) $$ 即M矩阵乘上切线,也就是变换后的模型切线,包括了上述的不等比缩放的情况,这样形成的矩阵与转世界空间矩阵G与法线n相乘的矩阵是正交的
🔔正交矩阵(Orthogonal matrix)是指矩阵的转置和其逆矩阵相等的矩阵,即A^T=A^(-1) 正交矩阵的性质有: 对于任意的两个向量x和y,都有x^Ty=0,即x和y是正交的
$$ 用矩阵的写法表示(\vec{t^{’}}\cdot \vec{n^{’}}=(MT)\cdot (GN)=0) (向量转成列矩阵形式,用其大写字母表示) $$ $$ 因为(\vec{t^{’}}\cdot \vec{n^{’}})用矩阵表示为(T^{T}N) $$ $$ 矩阵的性质 \left(AB\right)^{\mathrm {T} }=B^{\mathrm {T} }A^{\mathrm {T} } $$ $$ 注意因子反转的次序。以此可推出方阵_A_是可逆矩阵,当且仅当A^{T}是可逆矩阵,在这种情况下有 (A^{−1})^{T} = (A^{T})^{−1}。 $$ $$ 所以(T^{T}N=(MT)\cdot (GN)=(MT)^{T}(GN)=T^{T}M^{T}GN) $$ $$ 所以 (M^{T}G=I) $$ $$ 所以(G=(M^{T})^{-1}=(M^{-1})^{T}) (满足矩阵转置运算与逆运算可以交换顺序的性质) $$ 这里简单证明一下该性质 $$ 因为 ((MM^{-1})^{T}=(M^{-1})^{T}M^{T}=I) $$ $$ 所以 ((M^{-1})^{T}=(M^{T})^{-1}) $$ $$ 因为M=unity_ObjectToWorld,所以M^{-1}=unity_WorldToObject $$ 所以**G=**unity_WorldToObject的转置 可以用以下代码,进行转置后再和法向量相乘
transpose((float3x3)unity_WorldToObject)
Unity为我们封装了一个统一的函数_UnityObjectToWorldNormal(in float3 norm),_方便我们把模型法线转到世界空间
i.normal = UnityObjectToWorldNormal(v.normal);
它定义在UnityCG.ginc中,原码如下:
// Transforms normal from object to world space
inline float3 UnityObjectToWorldNormal( in float3 norm )
{
#ifdef UNITY_ASSUME_UNIFORM_SCALING
return UnityObjectToWorldDir(norm);
#else
// mul(IT_M, norm) => mul(norm, I_M) => {dot(norm, I_M.col0), dot(norm, I_M.col1), dot(norm, I_M.col2)}
return normalize(mul(norm, (float3x3)unity_WorldToObject));
#endif
}
// Transforms direction from object to world space
inline float3 UnityObjectToWorldDir( in float3 dir )
{
return normalize(mul((float3x3)unity_ObjectToWorld, dir));
}
它的实现原理正如我们推导的结果一样,只是它更加巧妙的使用
normalize(mul(norm, (float3x3)unity_WorldToObject));
代替了
normalize(mul(transpose((float3x3)unity_WorldToObject),normal));
从而提升了运算效率(省掉了转置计算) 我们修改之前的代码
float3 N=normalize(v.normal);
float3 L=normalize(_WorldSpaceLightPos0);
N=mul(unity_ObjectToWorld,float4(N,0)).xyz;
float ndotl=saturate(dot(N,L));
//修改为以下代码即可
float3 N=normalize(mul(v.normal,(float3x3)unity_WorldToObject));
float3 L=normalize(_WorldSpaceLightPos0);
float ndotl=saturate(dot(N,L));
修改后效果如下图所示
现在即使经过了不等比缩放,法线也能正确的进行坐标系的变换
改用片元着色器计算光照
顶点程序计算光照执行效率高,片元程序计算光照较慢但更细腻平滑,由于现代GPU硬件性能的提升 片元程序执行效率得到了极大的优化,现代的硬件处理都不成问题,所以一般来说都可以直接卸载片元着色器上
fixed4 frag (v2f i) : COLOR
{
float3 N=normalize(i.normal);
float3 L=normalize(_WorldSpaceLightPos0);
N=mul(float4(N,0),unity_WorldToObject).xyz;
float ndotl=saturate(dot(N,L));
return _LightColor0*ndotl+UNITY_LIGHTMODEL_AMBIENT;
}
ENDCG
完整代码
Shader "ShaderLearning/diffuse"
{
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct v2f
{
float4 vertex: POSITION;
float3 normal: TEXCOORD1;
fixed4 color : COLOR;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.normal=v.normal;
return o;
}
fixed4 frag (v2f i) : COLOR
{
float3 N=normalize(mul(i.normal,(float3x3)unity_WorldToObject));
float3 L=normalize(_WorldSpaceLightPos0);
float ndotl=saturate(dot(N,L));
return _LightColor0*ndotl+UNITY_LIGHTMODEL_AMBIENT;
}
ENDCG
}
}
}
修改建议🙋 normalize(mul(i.normal,(float3x3)unity_WorldToObject))替换为UnityObjectToWorldNormal(i.normal) 因为UnityObjectToWorldNormal,Unity官方已经为我们封装好了,其中对于等比例缩放和非等比例缩放,都有不同的处理方法,整体方案更加完善 替换后的完整代码
Shader "ShaderLearning/diffuse"
{
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct v2f
{
float4 vertex: POSITION;
float3 normal: TEXCOORD1;
fixed4 color : COLOR;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.normal=v.normal;
return o;
}
fixed4 frag (v2f i) : COLOR
{
float3 N=UnityObjectToWorldNormal(i.normal);
float3 L=normalize(_WorldSpaceLightPos0);
float ndotl=saturate(dot(N,L));
return _LightColor0*ndotl+UNITY_LIGHTMODEL_AMBIENT;
}
ENDCG
}
}
}


...