【UnityShader_Ojors的脚印】Diffuse漫反射光照

在很多学习 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(左)逐像素(中)逐顶点(右)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容