最近在看《UnityShader入门精要》来进行Shader的入门学习。看完第六章的”Unity中的基础光照“后,对前面所讲的顶点着色器和片元着色器有了更透彻的理解,于是做了个小实验,一方面验证自己对着色器渲染原理的理解是否正确,另一方面想亲眼看看渲染的整个流程,加深记忆。
为了计算方便,实验场景尽量简单化
1.场景模型只要一个平行光,一个带漫反射Shader材质的胶囊和一个用于参照地平线的terrain;
2.平行光设置为沿x轴旋转正45度。这样一来,其向量可以表示为(0,-1,1),方便计算;
3.平行光强度设置为1,颜色为纯白,即(1,1,1,1),减少干扰;
4.环境光强度设置为0.2,可以明显看到效果。另外环境光强度在这里的范围是0~8,其实实际范围是0~3.445,由于这个问题导致先前计算一直出错,我也是醉了;
5.在胶囊上挂个脚本,只声明了”public color a;“,为了方面使用Unity自带取色器。
shader "Caddress Unity Shader/Diffuse Vertex-Level"
{
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 a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
fixed3 color : COLOR;
};
v2f vert(a2v v){
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(mul(v.normal,(float3x3)_World2Object));
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLight));
o.color = ambient + diffuse;
return o;
}
fixed frag(v2f i) : SV_Target{
return fixed4(i.color,1);
}
ENDCG
}
}
Fallback "Diffuse"
}
代码来自书的第6章第四节,逐顶点的漫反射光照模型
首先这里先介绍下顶点着色器和片元着色器,我们在屏幕上所看到的3D效果,其实只是各个像素的颜色协调出来的障眼法。程序员用代码在背后构建了一个数据流的三维世界,但要将这个世界展示在屏幕上,就需要利用里面的数据关联屏幕的像素颜色,让我们看到的效果和现实世界的一样。
顶点着色器
虽然叫着色器,但它和着色还没有太大的关系,因为你总不能对一个没有面积的点着色吧。顶点着色器只是让模型上的各个顶点携带相关的信息,在上面的shader里,我们让它携带顶点的位置坐标和顶点颜色两个信息。至于一个点怎么携带信息,你可以想象成其背后有个顶点类什么的。
片元着色器
片元着色器才是真正进行着色的地方,其返回值o.color就是该像素上的最终颜色。片元可以理解成像素,但像素只含有颜色信息,而片元还携带着深度,法线等信息。
由上面的代码可看出,Shader利用顶点的法线数据算好颜色后,就直接传给片元着色器去画,这种渲染方式比较简单,易于在Unity上做实验还原。如果是逐片元光照,那就是利用每个片元携带的法线来计算颜色了,计算量太大。
fixed3 worldNormal = normalize(mul(v.normal,(float3x3)_World2Object));
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
这两行得出了世界坐标下的法线和光源的向量,并做了归一化处理。我们在Scene下看到的就是世界空间。
dot(worldNormal,worldLight)
这里对两个数据进行求点积。点积的求法有两种,一种是坐标法,另一种是角度法。后面两种方法都会用到,那个方便来哪个。
diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLight));
这里求出了漫反射的颜色值,其中光源颜色和漫反射颜色我们都设成了白色(1,1,1),所以_LightColor0和_Diffuse各参数都是1,实际结果就是点积的结果,由于两个向量都是归一化,因此其结果就是两个向量夹角的cos值(0~1)。
o.color = ambient + diffuse;
得出的漫反射数据和环境光数据进行求和,由于我们把环境光强度设置为0.2,因此环境光各参数为1/3.445,即(0.058,0.058,0.058)。
为了方面计算,我们选用胶囊最顶的一个三角面来还原着色规律。如下图:
毋庸置疑,蓝色范围的左下角顶点向量方向为y轴,这里用夹角计算比较方便,其向量与光源方向的夹角是45°,余弦值为0.707。
因此o.color = (0.058,0.058,0.058)+ (0.707,0.707,0.707),
最后的结果再乘以255得到的rgb是:(195,195,195),
由于物体表面颜色为(255,0,0)纯红色,
因此在蓝色范围左下角顶点附近的像素颜色应为(195,0,0)
由侧视图可看出,半球体从y轴到x轴的弧线过渡一共分为8块,因此蓝色范围内的右顶点和上顶点的方向向量分别是y轴沿x方向旋转11.5°和y轴沿z方向旋转11.5度。(图的原地画错了,应该往上偏移一点,但结果没错)
上顶点方向往z轴偏移,使用角度计算比较方便,该向量与光源方向的夹角是(90 - 11.25)+ 45度,cos值为0.555。加上环境光的数据,其上顶点附近的rgb是(156,0,0)。
最难计算是右顶点了,由于空间几何学得不怎么好,找不到公式计算夹角,只能用坐标法计算了,我们假设网格一格单位为1,那上顶点的向量坐标就是(0,1/tan(11.5),1)。即(1,4.91,0)归一化后为(0.2,0.982,0)。另外,光源向量坐标也可以用这个网格单元的坐标系,只要方向对了就没问题,因此光源向量(0,-1,1)归一化为(0,-0.707,0.707)。
点积得到的结果是0.694,加上环境光后得到的(0.752,0,0),乘以255得到rgb(191,0,0)。
以上的点积结果应该都为负数,但在Shader背后会自动改为正数,这是因为只有当光源方向和法线方向相对的时候(即点积为负),才应该有光照,当两个向量点积只要大于等于0,无论数值多少,o.color都不会加上Diffuse的值。我们把光源强度调到最大
可以看出,当法线与光源方向垂直之后,或者方向一致后,颜色就只剩下环境光的效果了(0.058 * 255 = 14.79)。
说到光源强度,其范围是0~8没错。它的改变也会对物体光照效果有影响,其实就是_LightColor0的系数,当初设置为1也是为了方面计算。例如,之前蓝色范围的左下角顶点附近像素rgb为((0.058 + 0.707)* 255 = 195,0,0),如果将光照强度取1.3,则该像素rgb为((0.058 + 0.707 * 1.3)* 255,0,0)。即(249.16,0,0)
前面只验证了顶点附近的点,这是因为顶点被网格线遮挡了,而附近的点受另外两点颜色值的影响较小。为什么这么说,因为逐顶点光照的渲染是按照三角形中线定理来着色的,如果没有另外两个顶点的着色,那么这个三角形的颜色其实是该顶点沿着中线由100%~0%变化的。例如计算重心附近的颜色:
重心为各顶点中线交点处,重心到各个顶点距离为各个中线的2/3,因此重心附近的rgb应为顶点rgb的33%(1/3(195+156+191),0,0)。即(180.6,0,0)。存在一定误差。
由此可以看出,所谓渲染只是对某个像素按照Shader写的规则附上颜色而已。而光照是对模型原有的颜色进行线性加深,以此达到背光效果。至于纹理的渲染规则就不是简单的线性加深了,但渲染的原理不变:如果是逐顶点渲染,则按照三角形中线定理(纹理应该就不会使用这种方法了吧),如果是逐片元渲染就利用该片元上的信息计算出最终要在像素上显示的颜色。