Ambient+Diffuse

Diffuse Shader

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即可

案例

  1. 首先在顶点程序当中进行MVP变换

归一化顶点的法向量,通过UnityShaderLab内建的_WorldSpaceLightPos0

内建光照相关的变量

光参数以不同的方式传递给着色器,具体取决于使用哪个 渲染路径 以及着色器中使用哪个 Pass Tag 正向渲染(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可以访问 Unity文档 获取到顶点到光源的方向,并将其归一化

            v2f vert (appdata_base v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);

                float3 N=normalize(v.normal);
                float3 L=normalize(_WorldSpaceLightPos0);

                return o;
            }
  1. 将向量N与向量L进行点积计算,并使用saturate函数,使计算的点积映射到0~1之间 并将其值乘上光照颜色_LightColor0赋给COLOR

    float ndotl=saturate(dot(N,L));
    o.color=_LightColor0*ndotl;
    

image-20230417194808184

```

Diffuse

  1. 在片元着色器当中添加环境光照 在最后返回i.color后面加上_UNITY_LIGHTMODEL_AMBIENT_

               fixed4 frag (v2f i) : COLOR
               {
                   return i.color;//+UNITY_LIGHTMODEL_AMBIENT
               }
    

完整的DiffuseShader

在Window-Rendering-Lighting当中可以设置Environment Lighting 将Source改为Color,修改AmbientColor 就会影响模型的固定环境光照颜色

Lighting

这样就好了吗?🤔️

其实我们写的Diffuse Shader有一个非常致命的Bug,我们拿我们的Diffuse Shader和Unity的DiffuseShader进行对比,其实就能发现问题

与Unity的DiffuseShader对比

当我们将模型旋转一定角度时候,发现光照渲染的位置根本不正确 这是由于我们进行点积的法向量,和光照的方向的向量,不在同一个坐标系的原因导致的

计算错误的代码

v.normal是在模型坐标系,而_WorldSpaceLightPos0是世界坐标系 模型自身虽然旋转,但是v.normal相对于自身是没有变化的,_WorldSpaceLightPos0光照的方向也没有改变 所以模型即使旋转了,两者经过点积之后,还是和原来点积的结果一样,就导致了,光照计算后的结果是一致的

如何解决这个问🙋题❓

💡要在统一的坐标空间里计算法向量与光向量的点积 有两种方式

  1. 将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));
    
  2. 将_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着色器还是不一样

缩放模型

这是为什么呢❓

💡原因是经过非等比例缩放之后,顶点法线与切线不再垂直

顶点不垂直

img

上图中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
        }
    }
}