Shader相关的知识可以说是学之不尽,在这里我仅仅是根据自身所学+经验总结一下Unity的shader相关知识,如有遗漏或者错误还请各位看官多多包涵,欢迎在评论区留言指正,有时间我会不断更新以及修正这些文章内容。
要完全理解和掌握shader,需要学习的东西太多,要写的话不知道写多少才算完结。这里我会从我对shader的理解来讲起,所以可能会和你们看过的shader书的思路不太一样,你们也不要觉得shader很难,我尽量以简单易懂的话来描述。
我之前总结过渲染管线在GPU里的流程是几何阶段->光栅化->像素处理阶段,Unity Shader能做的事情就是集中在几何阶段和像素处理阶段,这里我再把几何阶段和像素处理阶段的概念贴一下:
*几何阶段:顶点着色器,转换到剪裁空间,并映射到二维屏幕空间
*像素处理阶段:片段着色器着色,再进行alpha测试,模版测试,深度测试,混合等融合操作
这里我再贴一下Unity里最基本的shader代码(目前我讲解的shader都是Built-in管线的shader,至于URP的shader相关知识我会在后续文章中总结),到Unity编辑器里右键Create一个最基本的shader--Unlit shader:
代码如下(我加了些备注帮助理解):
Shader "Unlit/NewUnlitShader"
{
//属性信息,一般会在面板上显示
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" } //以不透明物体的方式渲染,默认渲染队列值为2000
LOD 100
Pass
{
//CGPROGRAM通常和ENDCG搭配使用,表示当前代码是CG语言
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
//模型数据,由CPU整理好传递过来
struct appdata
{
float4 vertex : POSITION; //模型顶点位置信息
float2 uv : TEXCOORD0; //模型纹理坐标信息
};
//v代表顶点着色器,f代表片段着色器,v2f表示顶点向片段传递的数据
struct v2f
{
float2 uv : TEXCOORD0; //之后会经过缩放偏移处理的纹理坐标信息
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION; //之后会经过空间转换后的顶点位置信息
};
sampler2D _MainTex;
float4 _MainTex_ST;
//顶点着色器,主要做顶点位置的空间转换
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex); //转换到剪裁空间
o.uv = TRANSFORM_TEX(v.uv, _MainTex); //对纹理坐标进行偏移缩放
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
//片段着色器,输出像素点的颜色
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv); //对纹理进行采样
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
看完代码之后相信你对shader有了初步的了解,接下来就是要讲一些概念性的东西了,为什么要把顶点信息转换到剪裁空间?剪裁空间又是个什么东东?
这里我们需要明白一件事情,那就是在游戏这个三维空间里,除了世界空间的坐标体系,还存在着其他的空间坐标体系,这些空间坐标体系大部分是以自身作为参考或者原点,比如模型空间,可以定位模型部件的一个相对位置,观察空间,以摄像机为原点,计算模型在观察空间的一个位置坐标。而我们最终都需要将这些物体、模型渲染到屏幕像素上,这里就有一个问题了,那我怎么知道这个模型在屏幕上是渲染全部还是只渲染一部分呢?这就不得不用到这个剪裁空间了,剪裁空间是由摄像机的视椎体决定的,在这个空间计算把摄像机看不到的部分剪掉不渲染,不用对所有物体全部渲染耗费性能。
因此这里面就会存在着这样几个空间转换(图来自Shader入门精要):
UnityObjectToClipPos做的事情就是把模型从模型空间坐标体系转换到剪裁空间的坐标体系,既然如此,那么这个空间转换是怎么做的呢?这里就需要用到矩阵了,看到矩阵先别跑,我知道对于很多数学不太好的同学看到矩阵就头疼,所以我在这里也尽量讲的通俗易懂,试着看一下我讲的你们是否能够理解吧。
关于Shader的矩阵你需要知道行矩阵和列矩阵,比如行矩阵:[1 2 3],列矩阵:
两个矩阵相乘,要看这两个矩阵尾、头数是否一样,一样表示可以相乘,不一样则不能相乘比如n行2列的矩阵和2行m列的矩阵相乘得到n×m的矩阵,相乘的结果就更简单了,其实就是把A矩阵的第一行数字和B矩阵的第一列数字相乘再相加,得到C矩阵第一行的第一个数字,A矩阵的第一行数字和B矩阵的第二列数字相乘再相加,得到C矩阵第一行的第二个数字...
以此类推直到A矩阵的第一行数字和B矩阵的其他列的每一列数字相乘再相加,得到C矩阵第一行的所有数字,再以此类推让A矩阵的第二行数字和B矩阵的所有列的每一列数字相乘再相加得到C矩阵的第二行所有数字...直到算出C矩阵,举个例子: X ,这是个2x3矩阵和3x2的矩阵相乘,那么得到的必定会是个2x2的矩阵:
X = =
是不是很简单呢?如果理解了话,其实我们看到矩阵的乘法就知道要做行乘以列再相加的运算即可
---------------------------------------------------------------------------------------------------------------------------------
在Unity里,向量或者点坐标的表示通常都是Vector3类型,所以我们在做运算的时候通常会使用1x3的行矩阵或者3x1的列矩阵,说到这里可能会有人要问了,为什么我在别的地方看到怎么都是四维矩阵相乘啊?先别急,让我们再想想这样一种情况,如果我给你一个Vector3类型,比如(1,1,1),你能告诉我这是一个方向矢量还是一个点吗?不能吧?如果说我让你把(1,1,1)给我往X轴平移一个单位,你能告诉我结果是什么吗?你也不好判断吧?因为你不知道它到底代指的是一个向量还是一个坐标点位置。
这个时候矩阵就能够帮助我们解决这个问题,怎么解决呢?利用矩阵的特性,我们将1x3的行矩阵或者3x1的列矩阵再多扩充一行或者一列,这里我用列矩阵打个比方:扩充之后变成了 和 ,为什么扩充之后会有0和1呢?我们不妨让这两个矩阵往X轴平移一个单位看看吧, X = , X =
向量最终得到的还是向量,而坐标点平移之后得到了正确的坐标位置,所以通过扩充0和1可以帮我们区分向量和坐标,因此我们在对坐标运算的时候通常会让原矩阵多扩充一行或者一列,扩充的值为1,计算的时候记住先缩放再旋转最后平移,简称“缩旋平”。
掌握了基本的矩阵运算之后,我们就知道在空间转换的时候如何利用矩阵来得到正确的结果。
---------------------------------------------------------------------------------------------------------------------------------
TRANSFORM_TEX(v.uv, _MainTex) :返回的是经过偏移缩放的UV值(float2类型),和_MainTex_ST搭配使用,_MainTex_ST的xy值表示纹理的缩放值,zw表示偏移值
下面这两个函数是等价的:
o.uv = TRANSFORM_TEX(v.uv,_MainTex);
o.uv = v.uv.xy * _MainTex_ST.xy + _MainTex_ST.zw;
最基本的Shader总结就先到这,后续让Shader给我们再上上强度(狗头)