# 前言
这周被项目主管拉过去谈了一次话,提到这一年多的的进步之类的问题,说我算是进步很大的了 —— 顿时感觉心情 UPUP 的,相比之前被老板说的没进步之类的话打击后,觉得平复多了。另外还谈到了 Shader 之类的问题,还问我感不感兴趣之类的?这当然回答 “是” 了,毕竟现在至少一般应用的话,自我感觉都是没问题的了,不过,我依然没有自大地说:我已经会了,有什么需要的吗?
毕竟,我目前只是算会用了而已,高级点的,恐怕也依然不足。而且最主要是,不清楚究竟会有什么样的要求?万一刚说自己会,然后叫你实现什么却又不行,那就尴尬了.... 所以保险起见,想想还是大成之后再透露吧。
最近一直在研究数学,于是决定暂且回到 Shader 上,这儿,就回顾一下基础,分析一下基本光照模型。
基本光照模型,一般由三种颜色合成:环境光、漫反射及镜面高光。
所以公式一般为:
下面会一个个解释。
# 环境光
# 简述
这里指的环境光并非物理意义上的那种 (因为目前而言,实时处理是一个几乎不可能的任务),而是指模仿这样的效果,简单使用一个设定好的固定颜色,并让所有物体都被它进行 “照明”,所以也被称作 “全局漫反射”。一般来说,都是直接将颜色与物体本身的颜色进行混合产生。
# 实现
而在 Unity 的 Shader 中,则是提供了一个内置变量 UNITY_LIGHTMODEL_AMBIENT 作为环境光颜色。而且这个颜色可以在 Unity 的编辑器 ->Lighting 中配置。
代码如下所示 (默认物体颜色为白色):
fixed4 frag (v2f i) : SV_Target
{
float4 color=float4(1,1,1,1);
float4 ambColor=color*UNITY_LIGHTMODEL_AMBIENT;
float4 finalColor=ambColor;
UNITY_APPLY_FOG(i.fogCoord, finalColor);
return finalColor;
}
![效果](/blogimages/oldpictures/2016.5.22-1_Ambient Color.JPG)
# 漫反射
# 简述
漫反射计算了光线与物体顶点的角度,然后对物体颜色做相应的衰减。
简单来说,它的概念就是:顶点法线与入射光线越角度越一致,那么颜色就越亮,反之则越暗。这个功能简单依靠点积实现,如果结果为负,则将其置为 0,即背光面的最终颜色将会变成 0 (所以如果只有漫反射的话,背光面就只会是黑的,因此才会有前面的环境光)。
公式也是很简单:
# 实现
在 Shader 中,因为需要用到法线,因此我们必须在定义的结构体上加上法线相关的语义绑定:
struct appdata
{
float4 vertex : POSITION;
float4 normal:NORMAL;
};
然后在顶点程序中,还需要将法线及顶点坐标转换至世界空间,并将其传入片段程序进行计算。当然这儿直接在顶点程序计算,然后通过插值获得最后结果也是可以的,不过从理论上来说,效果会比在片段中进行计算要差那么一点点 (如果是低模的,那就不止差一丁半点了)... 所以这儿我选择将数据传入片段程序,然后再行计算。
所以首先在传入片段程序的结构体 v2f 中添加两个参数,用于存放法线及光源方向:
float3 N:TEXCOORD0;
float3 L:TEXCOORD1;
然后在顶点计算中进行赋值:
float3 wPos=mul(_Object2World,v.vertex).xyz;
o.N=mul(float4(v.normal,0),_World2Object).xyz;
o.L=_WorldSpaceLightPos0.xyz;
需要注意的是对法线的转换,因为对法线的转换时不能使用与顶点一样的矩阵进行的,而是使用世界矩阵转置的逆矩阵进行,我们通过右乘世界矩阵的逆矩阵实现。至于原因的话,百度一下应该就可以找到比较详细解释,这只是提示一下而已。
然后在片段程序中,使用法线与光源方向计算漫反射,并添加到最后的颜色贡献中:
float4 diffColor=color*max(0,dot(normalize(i.N),normalize(i.L)))*_LightColor0;
float4 finalColor=ambColor+diffColor;
# 镜面反射
# 简述
一般在比较光滑的表面,我们都会注意到一些高光,并且随着我们视线角度的不同,都会产生一定的变化,而且,几乎所有物体都是具有这个属性的。比如说,现在盯着你的键盘看一下,每个按键上边,是不是都有一点油亮的感觉呢?而且低一下头、转一下头,这些发亮区域还会发生变化。
这就是镜面反射。
由此我们也可以清楚地知道,首先它肯定是与我们视线相关,其次,与物体表面的光滑程度相关,最后,当然就是与光线相关了。这东西其实也是属于经验公式,即并不完全符合物理,但却可以带来相似的视觉效果。刚才我们通过 “体验”,应当大约明白这是个什么样子了吧?
# 推导
先看一下图 (手绘的,不要在意细节):
相比其它的计算方式,镜面反射主要就是反射向量 R 计算起来比较麻烦一点。
因为很久以前听说,市面上挺流行考这个问题的,所以当年还是仔细研究过的,毕竟背公式什么的,就太 Low 了,要画个草图,就明白过程才是。那么,让我们来仔细推导一下吧,要知道这算还算是图形学里边比较简单的了。
在上面的那个简图中,我们可以把这三个方向向量看作两个三角形提取出来,如图所示:
那么,我们只需要先计算出这个 “大三角形” 的 “底” 的向量,无论是 R 到 L 还是 L 到 R 的方向,都可以通过 L 与其计算出最后的 R。那么,这儿我就设 L 到 R 方向的 “底” 为 K,那么。
OK,大致方向就是这样了,那么第一个就是计算光源 L 在法线 N 上的投影 (我们设其为 M),也就是这两个三角形的 “高”,之后即可通过 M-L 计算出其中一个三角形的 “底” 向量,将这个 “底” 乘以 2,就是光源到 R 的向量。首先,我们都知道,同时由于我们这儿计算的向量都是经过规范化的,如此,就可以直接略过了。
剩下的 就是 M 的长度,这时候,只需要再乘以法向量 N,就计算出向量 M 了。
然后
即最终公式为:
化简之后可得:
这就是经典的 Phong 模型计算公式了 —— 才怪 —— 要知道我们这才刚计算出反射向量呢,接下来就是将反射 R 实际运用起来了。
刚开始我们就提到过,镜面反射肯定与视线也是相关的,也就是我们看过去的方向和反射光线的方向,即取决于反射向量 R 与视线 V 的夹角,跟上边的漫反射 N 与 L 的关系有点类似:当两者方向越接近时越亮,反之则越暗,不过多了一个高光指数用于控制高亮区域大小之类的。
公式如下:
# 实现
# Phong 模型
如果已经明白了原理的话,那么实现起来,就相当简单了。
上面实现漫反射过程中,已经将光源方向机法线方向都传入了片段程序,因为镜面高光的实现还需要一个 “视线”,也就是相机的方向,在 Unity 中,则可由内置变量 “_worldSpaceCameraPos” 提供。
为了向片段程序再传入一个视线方向,所以得在结构体 v2f 中再加一个参数:
float3 V:TEXCOORD2;
然后在顶点程序中,因为主要计算在片段程序中,所以也是一句代码搞定:
o.V=_WorldSpaceCameraPos-wPos;
来到片段程序,首先将相机方向进行归一化处理,然后通过上述公式计算反射向量 R,并通过 Phong 公式计算最终镜面高光。最后,将所有的颜色加上去:
float3 V=normalize(i.V);
float3 R=normalize(2*N*(dot(N,L))-L)
float4 specColor=_SpColor*pow(saturate(dot(V,R)),_SpPower);
float4 finalColor=ambColor+diffColor+specColor;
效果:
这个光似乎显得太 “硬” 了... 所以可以在最后计算出来的镜面高光中,乘上一个值对其进行缩放:
float4 specColor=_SpColor.a*_SpColor*pow(saturate(dot(V,R)),_SpPower);
在这儿我使用了镜面颜色的 Alpha 值作为这个参数,之所以这样,跟 Unity5 内置的高光相比,显得太过难看,而且有些版本的镜面高光就有这个缩放参数。它可以将其柔和化。另外使用 Alpha 则是因为它在整个计算中根本没用,本着性能和废物利用的想法,就把它当作缩放参数来调整了。
效果:
另外,其实计算反射的方法,CG 中就已经内置了函数 “reflect (R,N)” 进行计算,主要注意传入的光源参数是指光源到顶点的方向就可以了。
# Blinn 模型
上面的是 Phong 模型,另外还有一个 Blinn 模型也是用来计算镜面高光的,公式如下:
可以稍微看看在下的拙图:
其中 H 是半角向量,指的是光源方向和视线的中间向量,除了其中点乘的两个参数换了,其它的跟 Phong 模型基本一致。不过由于半角向量的计算比 Phong 模型的反射向量 R 计算少了些计算步骤,所以性能上会好些,另外的区别自己比较下就知道了 (后面我会上源码,大家可以取消相应的注释进行测试)。
代码:
float4 specColor=_SpColor.a*_SpColor*pow(saturate(dot(N,normalize(L+V))),_SpPower);
其实我觉得 Unity5 内置的 Standard Shader 就是使用的 Blinn 模型,我还是上一张比较能看出区别的侧视图吧:
看出区别了吗?
# 点光源处理
最后的最后,就是对点光源的处理了。
之前那些处理过程,在 Unity 中都只针对第一个平行光源有效,所以为了支持点光源的话,还得多做一些处理。不过也都是些比较简单小改造。
在 Unity 中,点光源的渲染是通过在 Shader 中叫做 “ForwardAdd” 的第二个 Pass 进行,之前的我们的工作只能算是完成了第一个 Pass,也就是 “ForwardBase”。不过也不用担心,这两个 Pass 的内容区别也不是很大,只需要在前面的第一个 Pass 中进行些许修改,就可以挪到第二个 Pass 中了。这也是 Unity 的设定,而我们只需在 Pass 当先的 LightModel 设定好,那么它就可以工作了。
另外,我们要知道点光源与平行光源是不一样的!
- 最大的不同就是:点光源是有距离,而平行光在游戏中,是不算距离的。那么在此,计算点光源的时候,就得通过计算点光源的距离,而对其照明进行衰减。我们可以直接通过计算光源的距离,然后传入片段程序:
结构体 v2f 加入衰减值:
float atten:TEXCOORD3;
计算衰减 (注意与平行光计算的不同之处):
o.L=_WorldSpaceLightPos0.xyz-wPos;
o.atten=1/length(o.L);
- 还有一点需要注意的就是,第二个 Pass 当先还需要使用 Blend 命令使其与第一个 Pass 计算出来的结果混合。
Blend One One
效果:
# 源码
//Author:CWHISME
//Date:2016.5.22
//Decription:基本光照模型
Shader "CWH/Base Light Model"
{
Properties
{
_SpColor ("Specialer Color", COLOR) = (1,1,1,1)
_SpPower("Specialer Power",FLOAT)=1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
#include "Lighting.cginc"
uniform float4 _SpColor;
uniform float _SpPower;
struct appdata
{
float4 vertex : POSITION;
float3 normal:NORMAL;
};
struct v2f
{
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
float3 N:TEXCOORD0;
float3 L:TEXCOORD1;
float3 V:TEXCOORD2;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
float3 wPos=mul(_Object2World,v.vertex).xyz;
o.N=mul(float4(v.normal,0),_World2Object).xyz;
o.L=_WorldSpaceLightPos0.xyz;
o.V=_WorldSpaceCameraPos-wPos;
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float4 color=float4(1,1,1,1);
float4 ambColor=color*UNITY_LIGHTMODEL_AMBIENT;
float3 N=normalize(i.N);
float3 L=normalize(i.L);
float3 V=normalize(i.V);
float4 diffColor=color*max(0,dot(N,L))*_LightColor0;
float3 R=normalize(2*N*(dot(N,L))-L);
//float4 specColor=_SpColor.a*_SpColor*pow(saturate(dot(N,normalize(L+V))),_SpPower);
float4 specColor=_SpColor.a*_SpColor*pow(saturate(dot(V,R)),_SpPower);
float4 finalColor=ambColor+diffColor+specColor;
UNITY_APPLY_FOG(i.fogCoord, finalColor);
return finalColor;
}
ENDCG
}
//计算点光源
Pass
{
Tags{"LightMode"="ForwardAdd"}
Blend One One
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
uniform float4 _SpColor;
uniform float _SpPower;
struct appdata
{
float4 vertex : POSITION;
float3 normal:NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
float3 N:TEXCOORD0;
float3 L:TEXCOORD1;
float3 V:TEXCOORD2;
float atten:TEXCOORD3;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
float3 wPos=mul(_Object2World,v.vertex).xyz;
o.N=mul(float4(v.normal,0),_World2Object).xyz;
o.L=_WorldSpaceLightPos0.xyz-wPos;
o.atten=1/length(o.L);
o.V=_WorldSpaceCameraPos-wPos;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float4 color=float4(1,1,1,1);
float4 ambColor=color*UNITY_LIGHTMODEL_AMBIENT;
float3 N=normalize(i.N);
float3 L=normalize(i.L);
float3 V=normalize(i.V);
float4 diffColor=color*max(0,dot(N,L))*_LightColor0*i.atten;
float3 R=normalize(2*N*(dot(N,L))-L);
//float4 specColor=_SpColor.a*_SpColor*pow(saturate(dot(N,normalize(L+V))),_SpPower);
float4 specColor=_SpColor.a*_SpColor*pow(saturate(dot(V,R)),_SpPower)*i.atten;
float4 finalColor=ambColor+diffColor+specColor;
return finalColor;
}
ENDCG
}
}
}
好了,总算差不多完了!
写写停停,可不止花了一两天... 唉~时间啊....
这就是目前多数游戏中使用的最基本的光照模型了,稍微分析了下,也算是复习下基础了。