作者:王子饼干 ——镇江虹视游戏科技有限公司联合创始人、《多洛可小镇》制作人
原文链接:UnityShader入门精要笔记6:基础光照模型B
原文发布时间:2020年3月3日
文章类型:授权转载
路漫漫其修远兮,吾将上下而求索。——《离骚》
笔记当前使用的Unity版本:“2019.3.3”
笔记当前Unity最新的版本:“2020.1.0.Alpha 25”
在基础光照模型A中,我们编写兰伯特光照模型,以及更深切的体会了一下逐顶点和逐像素的区别,在本篇当中,我们首先要把之前的兰伯特漫反射光照模型的一些问题解决掉,然后再提出新的光照模型。
对于笔记有任何问题或者疑问,以及笔记中所出现的任何纰漏与错误,欢迎大家在评论区指出,谢谢~
我们先来看一张图哦
很像纯黑色的着色器计算,其实这个就是我们上一节中编写的两个着色器所渲染的两个模型的背光面,它是纯黑的~~~。非常的糟糕,事实上,在太阳底下,即使是背光面也会有一定的亮度,并不是完全纯黑的,但是半兰伯特光照模型却无法解决这点。
事实上,如果是纯黑色,也就是颜色值为0(一定要注意,在着色器当中,颜色值的分量范围是0-1,0是黑色,1是白色),我们可以回想一下计算兰伯特漫反射模型的公式
fixed3 diffuse = _LightColor0.xyz * max(0,dot(worldNormal,worldLightDir));
我们之前提到了,如果光源的方向夹角与模型表面发现的夹角超过90°,就会变成负值,为了不让它成为负值,我们必须要使用max函数把所有的值限定在0以上。那么很显然,模型的背光面的法线与光源的夹角是超过90°的,并且它们被max(0,x),既然是负值,最终结果肯定就是0了,于是我们在背光面就得到了纯黑色。
说实话,我并不知道如何解决,可能是我的水平还不够,单纯的从兰伯特光照模型上去解决这个问题可能难度太大了。但是有更多的trick技巧。(也就是通过一些视觉欺骗手段,让它看起来像是那么回事)
我们可以为整个模型表面都增加一点亮度,亮光的地方可以更亮,暗的地方也不至于纯黑,不久解决了吗,说做就做,我们接着上一节的
“UShaderMagicBook/PerFragmentLambertLight_0”
着色器来增加一些新的代码,读者可以在下面的链接中找到这个着色器的源代码。
529324416/UnityShaderMagicBookgithub.com/529324416/UnityShaderMagicBook
和上一节的代码几乎相同,我只增加了三行代码。第一行是在diffuse计算的下面
定义了一个trick,由于我并不是清楚这个“一点亮度”是多亮才合适,我们不妨自己来控制,所以我们写一个_Scale变量,让它来控制trick的亮度。由于这个是一个用户输入,所以不要忘记在材质面板和在你需要使用的CGPROGRAM块中声明和定义。如下图所示
当然啦,非常明显的是,颜色强度的调整是一个0-1之间的范围值,所以我们使用Range对象来作为它的类型。
最后,我们把这点亮度增加到模型的表面,再继续观察一下。
可以看到,当_Scale为0.2的时候,模型就有了一个更为合适的亮度值。
当然啦,其实官方提供了一个独特的变量来帮我们解决这个问题。那就是环境光(Ambient)。它也是内置在Unity渲染底层中的一个变量。即
UNITY_LIGHTMODEL_AMBIENT
表示环境光,一般来说,我们会增加环境光到漫反射或者高光反射的计算中,让它的亮面和暗面不会对比度那么强烈,操作起来也很简单,只需要把它直接加到diffuse上去即可。
还是刚才的代码,我们删掉刚才我们为trick增加的三行code。然后增加一行环境光的计算。
然后再回到Unity中观察结果
现在的效果就好了很多。
乘法是我们很早就会学习到的一种运算手段。如果单纯的两个标量相乘,还是非常好理解的,但是一旦乱七八糟的东西乘在一起,它们可能就具有了不同的意义,比如当一个矩阵乘以一个坐标的时候,就是一种变换。
而在Shader当中,我们经常会发现两种颜色乘在一起,那么两种颜色乘在一起代表什么呢?
其实稍微百度一下,我们就可以知道答案了,两种颜色相乘,其实就是用两种颜色作正片叠底。这个效果我们可以在ps中尝试一下。
我们先准备一张原始的图像,最好是色彩稍微丰富一些。
然后我们用蓝色去和它叠加在一起(用数学手段也就是乘在一起)
得到的效果还是非常明显的。整个画面都变成了蓝色调。
首先我们要知道,一般模型都有自己的颜色。虽然我们现在只能绘制出纯色,但是这种原理和手法我们一定要知道。为了改变模型的颜色,还是我们前面使用的那段代码,我们希望能够在一定程度上改变模型表面的颜色,于是我们在Properties语块增加一个颜色输入
不要忘记在CGPROGRAM块中声明
最后和diffuse乘在一起。
然后我们回到Unity中调整一下颜色并且观察其变化。
观察后发现环境光这部分并没有任何变化,因为环境光并没有和我们的颜色叠底,所以我们也让环境光和颜色叠一下。
然后再观察变化。
芜湖~,看起来还可以。
至于颜色相加是什么意义,大家完全可以自行百度,没有那么复杂。
前面说到了,我并不知道该怎么从兰伯特定律出发,去解决模型背面纯黑(纯黑会丢失背面的模型细节)的问题。但其实早在Valve开发《半条命》的时候,就提出了一种解决方法,被称为“半兰伯特光照模型”,也可以称为广义兰伯特模型。它的公式如下
和我前面所说的原生的trick很相似,它引入了两个新的算子,Alpha和Beta,其实也就是在避免计算出来的结果出现0值。自己创造函数其实非常有意思,有一些函数可以自己解出来,而有一些函数可以自己构造。这要求我们学好微积分,尤其是泰勒公式。由于这部分的代码比较简单。大家可以自己动手尝试一下,增加两个输入,然后按照这个计算方法,去改造一下之前的着色器代码。
漫反射是一种基础的材质,如果模型的表面更为光滑,它是有一定的反光能力的。我们可以尝试利用一种比较基础的高光反射模型来实现它。在此之前,我们需要先了解一下它的原理。
高光的反射并不是像漫反射一样,四面八方的,当我们从不同的角度观察某个光滑的对象的时候,高光的位置也在发生改变,简单点来说,我们观察模型的方向,也就是视角方向和物体反射光方向共同决定了高光的位置和强度。
我们需要先根据入射光方向和法线方向计算出反射光方向,然后通过反射光方向的和视角方向夹角的余弦值来决定高光的强度。
有了入射光和法线,我们可以很轻松的计算出反射方向,通过下面的公式
不过甚至不用自己计算,Unity提供了非常便利的函数 reflect,通过这个函数,可以直接计算出反射方向
(ps:由于这里的I指的是入射光方向,所以必须加一个负号,否则我们计算出来的反射角度就是反向的)
视角方向是针对模型的,所以我们必须先有模型的位置,然后用摄像机的位置坐标减去模型的位置坐标,以得到视角方向。
摄像机的位置也是内置于UnityCG.cginc中的一个常用的变量,每次开始渲染流程之前,Unity会把摄像机的世界坐标位置填充到变量_WorldSpaceCameraPos中,由于摄像机位置是世界空间中的,所以我们也需要把模型的坐标位置传入到世界坐标中。
这里采用的高光计算方式也是一种经验模型,即Phong光照模型。它通过以下的公式来计算高光
其中I是光源信息,r是反射方向,v是视角方向,gloss是光滑度。
有了以上的公式,和获取参数的手段,我们就可以开始编写着色器代码了。
Shader "UShaderMagicBook/PhongLight_0"{
Properties{
_BaseColor("Base Color",Color) = (1.0,1.0,1.0,1.0)
_Gloss("Gloss",Range(8,100)) = 20.0
}
SubShader{
pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex Vertex
#pragma fragment Pixel
#include "Lighting.cginc"
struct vertexInput{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct vertexOutput{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1; //需要模型的世界坐标位置,才可以计算视角方向
};
fixed4 _BaseColor;
float _Gloss;
vertexOutput Vertex(vertexInput v){
vertexOutput o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz; //由于vertex是一个4x4类型变量,所以我们取xyz
return o;
}
fixed4 Pixel(vertexOutput i):SV_TARGET{
fixed3 worldNormal = normalize(i.worldNormal); //别忘记标准化
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 reflectDir = reflect(-worldLightDir,worldNormal);
//注意reflect函数的第一个参数才是光源,第二个是法线
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
//视角方向可以通过内置的函数UnityWorldSpaceViewDir来计算,无需自己计算
//不要忘记标准化!!!
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _BaseColor.xyz; //先获取环境光
fixed3 diffuse = _LightColor0.xyz * _BaseColor.xyz * saturate(dot(worldNormal,worldLightDir));
//这里我们用了一个新的函数saturate,它的作用和max类似,也是把目标参数约束到0-1的范围内
fixed3 specular = _LightColor0.xyz * pow(saturate(dot(viewDir,reflectDir)),_Gloss);
//利用Phong模型中的高光公式来计算高光
return fixed4(specular + ambient + diffuse,1.0);
}
ENDCG
}
}
}
我们可以用刚才的这个着色器来初始化一个材质然后赋给一个模型观察一下效果。
仔细思考Phong光照模型的话,如果视角方向和反射方向的夹角超过90°,那么高光就会完全消失,但是事实并不是这样的。
上图的左图中,是Phong可以处理的情况,而到了右图的话,Phong光照模型就露出了“经验模型”的马脚,因为它和反射方向的夹角超过了90°,然而事实却是,即使超过了,我们应该还是可以看到高光的。
迫于这个问题,就有人提出了Blinn模型,Blinn模型抛弃了依赖夹角余弦值的反射方向,而使用半程向量来解决这个问题。
即光线与视线夹角一半方向上的一个单位向量。当半程向量与法线向量越接近时,镜面光分量就越大。
当观察向量与反射向量越接近,那么半角向量与法向量N越接近,观察者看到的镜面光成分越强。
这个半角向量就是光源方向和视角方向的相加后再标准化。
而Blinn光照模型也被称为Blinn-Phong光照模型。
Shader "UShaderMagicBook/BlinnPhongLight_0"{
Properties{
_BaseColor("Base Color",Color) = (1.0,1.0,1.0,1.0)
_Gloss("Gloss",Range(8.0,100.0)) = 20.0
}
SubShader{
pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex Vertex
#pragma fragment Pixel
#include "Lighting.cginc"
struct vertexInput{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct vertexOutput{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};
fixed4 _BaseColor;
float _Gloss;
vertexOutput Vertex(vertexInput v){
vertexOutput o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
return o;
}
fixed4 Pixel(vertexOutput i):SV_TARGET{
fixed3 worldNormal = normalize(i.worldNormal);
float3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(viewDir + worldLightDir);
//半角向量的计算
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _BaseColor.xyz;
fixed3 diffuse = _LightColor0.xyz * _BaseColor.xyz * saturate(dot(worldLightDir,worldNormal));
fixed3 specular = _LightColor0.xyz * pow(saturate(dot(halfDir,worldNormal)),_Gloss);
return fixed4(ambient + diffuse + specular,1.0);
}
ENDCG
}
}
}
我们用Blinn-Phong光照模型和Phong光照模型做一个对比
可以看见区别还是非常大的。
ok,那么到这里,我们简单的接触了一下几种比较经典的“经验模型”。我自己在编写这个笔记的时候也会有很多新的认识和发现。而不单纯的是把自己以前记得笔记再叙述一遍。希望这个笔记也会对大家有一定的帮助。另外,本节使用的所有代码都放到了我的github上,大家可以在下面的链接中找到。