0.本文示例代码地址
GitHub
1. 去掉 Default Shader 中的干扰项
重复上一篇文章提到的操作,新建一个场景 02_Simplest.unity, 去掉自带天空盒。在场景中创建一个 Cube、新建Material 文件 02_Simplest,然后新建 Unity Unlit Shader 并命名为 02_Simplest.shader并打开,用如下的代码覆盖 02_Simplest.shader
Shader "Shader_Examples/02_Simplest"
{
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float4 vert (float4 v : POSITION) : SV_POSITION
{
return UnityObjectToClipPos(v);
}
fixed4 frag () : SV_Target
{
return fixed4(1.0,0,0,1.0);
}
ENDCG
}
}
}
这是包含最简单的顶点和片元着色器的 shader 了,顶点着色函数只干一件事:进行坐标变换,将顶点的模型坐标(由MeshRender 组件提供)变换到裁剪空间坐标。这是顶点着色器最基本的任务。片元着色器只干一件事:返回一个 RGBA 颜色。
2. 顶点着色器的执行
我们给新建的 Cube 指定02_Simplest 进行渲染,可以看到整个 Cube 是红色的。我们先来看顶点着色器运行逻辑。如果了解渲染管线的话我们知道,在应用程序阶段,CPU 会将顶点数据准备好,提交给管线,在 Unity 中,这个工作由 MeshRender 来完成。我们假设这个 Cube 在 MeshRender 中由6个顶点表示(在 Unty 中实际并不是,原因先不管),那么在调用 DrawCall 渲染这个 Cube时,通过MeshRender 提交的顶点数据是一个长度为6的 Vector4 数组(为什么不用Vector3,涉及到具体的变换过程,齐次坐标,先不要考虑)。
执行的过程可以这样理解(我这段代码只是帮助理解,实际过程是GPU来处理的)
for (int i = 0; i < vertexList.length; i++)
{
float4 v = vertexList[i].POSITION;
vert(v);
}
划重点:顶点着色器是逐顶点执行的,针对提交的每个顶点,执行一次顶点着色函数。顶点着色函数将顶点坐标进行 MVP 变换后得到的裁剪空间坐标作为返回值,提供给下一阶段的片元着色器。
3. 片元着色器的执行
暂时可以把“片元”理解为”屏幕像素”(事实上他们差别巨大,但是此时姑且这么认为,有助于你理解片元着色器的工作)。先看片元着色函数的返回,是一个颜色值。就是说绘制了某个像素的颜色。事实上,片元着色器是逐片元执行的。也就是针对当前渲染图元覆盖的每一个像素,执行一遍。伪代码如下:
for (int i = 0; i < fragmentList.length;i++)
{
fixed4 red = new fixed4(1,0,0,1);
fragmentList.color = red;
}
可以简单的理解为:当片段着色器执行完成,屏幕上对应的像素立即变成红色。
如何计算 Cube 覆盖了哪些像素,这个过程叫“光栅化”,是GPU渲染流程的一个阶段,具体的算法暂不讨论。
4. 顶点着色器和片元着色器的数据传输
最简单的顶点着色器对每个顶点执行 MVP 变换,得到顶点在裁剪空间的坐标,作为顶点着色器的输出,也是片元着色器的输入。那么,我们的 Cube 只有6个顶点,只返回了6次,而片元着色器执行的次数却不是6次,那么它的输入是怎么得来的?
线性插值 得到。根据某一个片元中心位置(片元有大小)坐标距离覆盖它的三角形(顶点数组中的3个元素组成)的三个顶点的距离,加权计算得到(如 0.1p1 + 0.3p2 + 0.6*p3)。
不仅仅位置属性是插值计算得到,其它属性(例如法线、uv坐标等)也是这么计算得到的。
5. 顶点着色器的输入可以是哪些属性?
在我们这个最简单的 shader 里面,顶点着色函数的输入就是一个顶点位置
float4 vert (float4 v : POSITION) : SV_POSITION
通常顶点数据有很多,除了位置还有法线、切线、纹理坐标等,我们怎么知道这个参数 v 是用的位置的数据?因为我们给它制定了 POSITION 这个语义,意思就是将顶点数据中的 位置作为v传递给顶点着色函数,SV_POSITION 则表示,这个顶点着色函数的返回是一个裁剪空间的坐标。
顶点着色器必须包含 SV_POSITION 的输出,也就是说顶点着色器的输出里面必须包含顶点的裁剪空间坐标。否则后续阶段将无法知道这个顶点的位置,无法渲染出来。
6. 自定义输入输出结构
02_Simplest 这个 shader 里面,顶点着色函数只处理了位置信息,也只返回了位置信息,那当我们需要处理其它顶点数据信息时,怎么处理?我们可以自定义顶点数据结构,可以自定义字段,只要指定语义就可以了。比如
struct appdata
{
float4 vertex : POSITION; // 将顶点的位置信息作为vertext
float2 uv : TEXCOORD0; // 将顶点的第0组纹理坐标作为 uv
};
struct v2f
{
float2 uv : TEXCOORD0; // uv表示处理后的顶点纹理坐标
float4 vertex : SV_POSITION; // vertext 标示处理后的顶点坐标
};
有了上面的结构体定义,我们就可以在顶点着色函数和片元着色函数中如下使用:
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex); // 对顶点坐标进行变化
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
片元着色函数的 SV_Target 是啥?SV_Taget 表示,将这个片元输出的颜色,放到一个 RT(RenderTarget)中,这里先简单理解为放到一个缓冲区中。
目前 Unity Shader 中有哪些 语义呢?