Unity Shader分享unity3D技术分享Unity教程合集

【UnityShader_Ojors的脚印】Diffuse漫反射

2016-09-17  本文已影响137人  Ojors

在很多学习 Shader 的资料上,都是以标准光照模型开始,也就是常见的漫反射和高光反射光照模型,我的这系列也不列外,该篇先从漫反射开始。


漫反射是生活中很常见的一种光线反射现象,光线照射在物体表面时,会向各个方向进行散射。一个点的漫反射由入射光线的角度决定,入射角越大,视觉观感得到的光强越弱。

漫反射,截自维基百科

在 Shader 中,决定最终视觉效果的是物体表面的颜色、光照颜色、法向量和入射光线方向(注意这里的入射光线是从表面顶点指向光源)

漫反射,截自维基百科
物体表面颜色和光照颜色很容易理解,那么法向量和入射光线方向是干嘛用的呢?
上面谈到,漫反射由入射光线的角度决定,而这里决定的就是在屏幕显示时对应颜色的强弱,一个简单的例子就是,一个白色的物体,在光线直射的地方,白色会很亮,接近纯白,而在光线侧向照射的地方,白色就没那么白了,会带些许的阴影,在图形显示上颜色会变成灰色甚至是黑色。
这里所说的强弱,由法线和入射光线决定,两向量形成的夹角越小,则越接近直射效果,而当夹角大于或等于90°时,表面接受不到光照,呈黑色。衡量这个夹角,我们使用余弦值,即反射光线强度与表面法线和光源方向夹角的余弦值成正比。
于是有了如下的漫反射计算公式:
![][0]
[0]:http://latex.codecogs.com/png.latex?diffuse=(lightColorM_{diffuse})max(0,\overrightarrow{n}\cdot\overrightarrow{l})
其中,lightColor为光照颜色,Mdiffuse为材质颜色,n 和 l 分别为表面法向量和光源方向。我们可以这样理解,前面括号内的部分是要显示的目标颜色,后面的最大值计算,是为了得到要显示的颜色强度,而取最大值,是为了避免因为夹角大于 90° 出现负值的情况,我们在开发中,可以使用 saturate() 函数来代替最大值函数,该函数会把传进的数值截取到 [0,1] 之间。然后,这里为什么单两个向量点积就能得到余弦值呢?

我们知道,点积展开公式如下:
![][1]
[1]:http://latex.codecogs.com/png.latex?\overrightarrow{n}\cdot\overrightarrow{l}=\left|n\right|\left|l\right|cos\theta
在计算时,我们为了方便,会把两个向量做归一化处理,也就是模为1,由此以上点积公式就变成了:
![][2]
[2]:http://latex.codecogs.com/png.latex?\overrightarrow{n}\cdot\overrightarrow{l}=cos\theta


理论工作做完,就到了代码部分了:

逐顶点光照

逐顶点光照,顾名思义,就是对模型的顶点进行光照着色,这种方式在性能上占有,但是效果没有逐像素光照细腻

Shader "Ojors/DiffuseShader" {
    Properties {
        _Diffuse ("Diffuse", Color) = (1,1,1,1)
    }
    SubShader {
        Pass {
            Tags {"LightMode" = "ForwardBase"}

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"

            fixed4 _Diffuse;

            struct vertIn {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct vertOut {
                float4 pos : SV_POSITION;
                fixed3 color : COLOR;
            };

            vertOut vert (vertIn i) {
                vertOut o;
                o.pos = mul(UNITY_MATRIX_MVP, i.vertex);
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
                fixed3 worldNormal = normalize(mul(i.normal, (float3x3)unity_WorldToObject));
                fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));

                o.color = ambient + diffuse;
                return o;
            }

            fixed4 frag (vertOut i) : SV_Target {
                return fixed4(i.color, 1.0);
            }

            ENDCG
        }
    }
    FallBack "Diffuse"
}

对于上篇已经讲述过的内容,这里就不再赘述,我们只关注实现部分:
首先是属性:

Properties { 
    _Diffuse ("Diffuse", Color) = (1,1,1,1) 
}

这里定义了一个颜色属性,默认值是白色,表示使用该 Shader 的材质的颜色

#include "Lighting.cginc"

这里包含的光照文件里定义了我们需要获取的光照颜色,其实打开这个文件的话大家会发现,这里面定义了很多很实用的光照模型的计算函数,如果我们只是需要用到里面的变量,比如:_LightColor0,我们可以只包含 UnityLightingCommon.cginc 这个文件即可。

struct vertIn { 
    float4 vertex : POSITION; 
    float3 normal : NORMAL; 
}; 

struct vertOut { 
    float4 pos : SV_POSITION; 
    fixed3 color : COLOR; 
};

这里定义了两个结构体,一个是用于接收 Unity 外部输入的值,我们需要获取顶点以及表面法线;另一个是用于存储从顶点着色器到片段着色器需要传递的值,我们需要传递空间转换后的顶点坐标以及要输出的颜色。(注意:对于输入结构体和输出结构体顶点位置的语义,输入结构体我们使用 POSITION,而输出结构体我们使用 SV_POSITION,我查阅资料得知对于存储顶点的寄存器,输入和输出是不同的,所以我们一般会在两个结构体中对顶点定义不同的语义,但我在 Unity 5 中试过都用 SV_POSITION,其实并不会报错,我猜测可能是 Unity 为我们进行了处理,但为了兼容性和规范性,我们还是对输入和输出结构体定义不同的语义吧。)

vertOut vert (vertIn i) {
    vertOut o;
    o.pos = mul(UNITY_MATRIX_MVP, i.vertex);
    fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
    fixed3 worldNormal = normalize(mul(i.normal, (float3x3)unity_WorldToObject));
    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
    fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));

    o.color = ambient + diffuse;
    return o;
}

这里是逐顶点光照的计算过程:
首先要做的当然是顶点坐标的变换,然后提取环境光。
【这里要说一下什么是环境光:我们在现实中看到的所有物体,都受其他物体所反射的颜色的影响,比如一个场景:红色的地毯上摆放着一个茶几,茶几除了接收到自然光的照射外,还受到地毯所反射的颜色的影响,使其带有淡淡的红色。而环境光在 Unity 中可以通过 UNITY_LIGHTMODEL_AMBIENT 变量获取到】
然后是对法线进行从模型坐标系到世界坐标系的转换(只有在同一坐标空间下,点积才有意义),前面的篇章已经提到法线的变换,需要使用顶点变换矩阵的转置矩逆阵进行变换,在这里,我们使用法线右乘矩阵 unity_WorldToObject 来得到转置逆矩阵的效果(最近更新了Unity 5.4, 发现 _WorldToObject 矩阵命名变成了 unity_WorldToObject,这里需要注意!!!)
然后获取当前光照的方向:_WorldSpaceLightPos0
当然,上述操作都要把向量进行归一化处理。
最后就是进行漫反射的计算了,套用上面讲到的公式即可,返回跟环境光混合后的最终颜色。

fixed4 frag (vertOut i) : SV_Target {
    return fixed4(i.color, 1.0);
}

对于逐顶点光照,片段着色器的代码非常简单,只需要把顶点着色器计算得到的颜色结果输出即可。

逐像素光照

逐顶点光照因为是基于顶点来计算的,所以对于顶点数较少的模型,出来的效果可能不会很好,可能会产生锯齿,而逐像素光照可以解决锯齿问题,但因为是逐像素的计算,性能较逐顶点计算会较低,这里的取舍要看具体需要。
逐像素光照和逐顶点光照的计算方式其实大同小异,只是把顶点进行的光照计算移到了片段着色器中:

首先要说的是结构体:

struct vertIn {
    float4 vertex : POSITION;
    float3 normal : NORMAL;
};

struct vertOut {
    float4 pos : SV_POSITION;
    float3 worldNormal : TEXCOORD0;
};

这里的第二个结构体与逐顶点计算的稍微不同,worldNormal 量获取的是在世界坐标系下的法线,属于我们自定义的数据。对于自定义的数据,我们习惯把其语义定义成 TEXCOORD,但其实除了输入结构体中的特定数据、POSITION 和 SV_POSITION 外,输出结构体我们用什么语义都是可以的,其实质是 GPU 中的寄存器,但是为了规范书写,还是建议使用 TEXCOORD 来存储自定义数据。

然后是顶点着色器:

vertOut vert(vertIn i){
    vertOut o;
    o.pos = mul(UNITY_MATRIX_MVP, i.vertex);
    o.worldNormal = normalize(mul(i.normal, (float3x3)unity_WorldToObject));
    return o;
}

这里简单地把法线从模型坐标转换到了世界坐标。

片段着色器:

fixed4 frag(vertOut i) : SV_Target{
    fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
    fixed3 normal = normalize(i.worldNormal);
    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
    fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(normal, worldLightDir));
    fixed3 color = ambient+ diffuse;

    return fixed4(color, 1.0);
}

基本与前面讲到的相同。值得注意的一点是,法线到了片段着色器需要再一次进行归一化处理,原因是法线从顶点着色器到片段着色器的过程中,法线会进行插值处理,得到的法线不一定是单位长度的法线了,对后面的计算会产生影响。

Half Lambert(半兰伯特)光照模型

对于上述的光照,在背光面会产生一个问题,没有光照的地方会表现成纯黑,会失去一些模型的细节,而为了弥补这个缺陷,有了 Half Lambert 光照模型。(注意:该模型没有物理依据,只是一个视觉的加强技术)

Half Lambert 光照模型计算公式:
![][4]
[4]:http://latex.codecogs.com/png.latex?diffuse=(lightColorM_{diffuse})(\alpha*(\overrightarrow{n}\cdot\overrightarrow{l})+\beta)
原来的 max 部分被括号部分代替
其中:alpha 和 beta 分别代表阴影面和光照面的权重,alpha 越大,阴影面越大,alpha+beta = 1。

以逐像素光照为例,与上面的 Shader 对比,该光照模型区别的代码是片段着色器:

vertOut vert (vertIn i) {
    vertOut o;
    o.pos = mul(UNITY_MATRIX_MVP, i.vertex);
    fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
    fixed3 worldNormal = normalize(mul(i.normal, (float3x3)unity_WorldToObject));
    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
    fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * (dot(worldNormal, worldLightDir)*0.7+0.3);

    o.color = ambient + diffuse;
    return o;
}

关键一句:

fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * (dot(worldNormal, worldLightDir)*0.7+0.3);

可以看到,我这里把 alpha 设为 0.7,beta 设为0.3,看图对比就能看出区别:

板兰伯特光照(左) & 普通光照(右)

最后给出三个 Shader 的对比图:

Half Lambert(左)逐像素(中)逐顶点(右)
上一篇下一篇

猜你喜欢

热点阅读