Unity Shader学习记录(二)
在进入真正能看到效果的Shader之前,需要一点数学基础,也就是向量和矩阵方面的数学知识,关于这些知识不予赘述,有大量的书籍资料以及网络文章可以学习,在此仅列举一些必要的概念方便查阅。
Shader中涉及到的数学知识包括
- 向量的运算
- 矩阵的定义
- 矩阵的运算
- 矩阵与向量之间的关系
- 矩阵与坐标系统之间的关系
具备一定的数学基础后,便可以正式编写能看出三维效果的Shader了。
视觉基础——光线
人眼要看到一样东西需要光进入人的眼睛,在现实生活中最常见的情况是物体在阳光或者人造光源下被人看见,而人们之所以能看到物体表面,也是来自于一种再普通不过的光学现象——漫反射。
物理中定义的漫反射是指在粗糙物体表面,光线会向各个不同的方向反射出去,看起来就暗淡,无法看到光源的像。而在计算机图形图像中,漫反射的含义就有所不同了,因为计算机定义的三维空间物体没有“粗糙”这个属性,它们甚至都不是物质,除非被赋予否则不存在物理性质;因此在计算机图形图像渲染中,“漫反射”是一种简单的“光照模型”,它描述了一个物体是怎样对光线产生反应的。
就像前文所编写的那个简单Shader那样,如果没有任何其它信息作用于一个物体,那么物体覆盖的范围内只会有单个颜色,根本产生不了三维物体的视觉效果。而要让物体能呈现出三维效果,那么按照现实世界的概念,首先需要一个光源来“照亮”它,其次它本身要能够对光线产生反应;为了达到这个目的,人们提出了许多不同的光照模型,它们各有各的特点和应用场景,而漫反射模型是其中最为普通也是最常见的一种。
图形图像学中的“漫反射模型”是对物理上的“漫反射现象”的一种简化和抽象,由于物理上漫反射的反射光线是完全随机的,因此可以视为在任意角度上看到的漫反射结果分布都是一致的,但是由于入射光线的角度问题会在不同的角度上产生不同的结果。
常见的漫反射模型有“兰伯特模型”和“半兰伯特模型”两种。
“兰伯特模型”认为,物体上任意一点反射的光线强度可以通过表示照亮该点的光线向量的反向投影到该点的法向量方向上来得到,这样简单地描述了一下物体表面反射光强度与光线照射方向的关系,在实际应用中就很好地模拟出了物体在类似阳光环境下的情况。
兰伯特漫反射模型的计算公式如下
其中
有了这个公式作为指导,基本的漫反射Shader就可以编写出来了。
Shader "Custom/FinalTestShader" {
Properties {
_Color ("Color", Color) = (1,1,1,1)
}
SubShader {
Pass {
Tags {"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
float4 _Color;
struct a2v {
float3 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_WorldToObject, v.vertex);
return o;
}
fixed4 frag(v2f i) : SV_TARGET {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = UnityWorldSpaceLightDir(i.worldPos);
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir));
return fixed4(diffuse, 1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
漫反射Shader解析
漫反射的Shader标签部分和前文的简单Shader都类似,不同的是在Pass中的Tags标签下声明光照渲染类型的语句不能去掉,因为在这里需要用到环境光的颜色,必须要声明了光照渲染类型后才能使用。
Tags {"LightMode"="ForwardBase"}
看到在CGPROGRAM关键字后面,除了定义可以设定的颜色数值之外,又添加了两个结构体定义,一个叫做a2v,另一个叫v2f。用这两个名字主要是方便理解和记忆,前者表示从应用到顶点流程,后者表示从顶点到片元流程。
struct a2v {
float3 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};
可以看到,直接在结构体中声明语义就可以替代掉原本需要在参数和返回值上都写上的语义,a2v结构中声明了POSITION语义,储存顶点坐标;同时也声明了NORMAL语义,储存顶点法线。
在vert方法中通过调用Unity自带的方法来计算顶点的裁剪坐标,法线的世界坐标表达以及顶点的世界坐标,并且储存到v2f结构体中,然后将这个结构体返回给frag方法。
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
return o;
}
可以看到为了计算出顶点在世界空间的坐标值,使用了一个矩阵来对顶点坐标进行转化,这个矩阵是Unity自带的,作用就是将向量坐标在世界空间和模型空间之间转化;在这里选择左乘来将顶点坐标从模型空间转化到世界空间。
需要注意的是,这些自带方法和矩阵有一部分是在Lighting.cginc文件中编写的,因此需要将它包括进Shader代码中才能使用,#include关键字不能少。
而frag方法则接受一个v2f结构作为输入,随后计算法线单位向量,指向光源的向量,最后通过兰伯特模型计算公式得到漫反射颜色,放入fixed4变量并且给定Alpha值为1(即完全不透明)后即可返回。
fixed4 frag(v2f i) : SV_TARGET {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = UnityWorldSpaceLightDir(i.worldPos);
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir));
return fixed4(diffuse, 1.0);
}
在这个例子中,漫反射的计算是在frag函数中完成的,这种叫做“逐片元渲染”,精细度比较高,类似于“逐像素操作”,效果比较好。而在实践中还有另一种方式,也就是在vert函数中计算漫反射结果,然后直接在frag函数中返回结果,这一般叫做“逐顶点渲染”。
两种方式的结果区别是很明显的,逐顶点渲染的结果比较粗糙,这是因为显示驱动在填充顶点之间的空白区域时使用的是线性插值,因此看起来会很“马赛克”,锯齿明显;而逐像素操作的结果不存在这个插值问题,因此很精细。
与之相对的,逐像素操作速度更慢,显卡压力更大,而逐顶点却很快,这是由于模型的顶点数总是远小于像素数的;因此在需要向效率妥协的时候,逐顶点渲染也许会是一个可行的选择。
至此一个最基础的漫反射Shader就完成了,这个Shader简单到只考虑了一个光源,因此效果是很单纯的,而且感觉很暗淡,因为物体也就受到一个平行光源的影响。
所谓“半兰伯特模型”是Valve公司提出的一种漫反射模型,不同于兰伯特模型中模拟真实物理情况将光线无法照到的地方设置为黑色的做法,半兰伯特模型将点积结果进行了缩放和偏移,让它始终大于零,由此得到一个能照亮本来无法被照亮的区域的模型。
半兰伯特的计算公式如下
通过这样的计算来让物体变亮,通常来说可以取
fixed4 frag(v2f i) : SV_TARGET {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = UnityWorldSpaceLightDir(i.worldPos);
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * (0.5 * dot(worldNormal, worldLightDir) + 0.5);
return fixed4(diffuse, 1.0);
}
这种漫反射模型适合用来强调环境中的一些物体或者让环境不用太多光源也能很亮。
下面将会为Shader添加纹理,高光模型,加入环境光表达等。