概述
前面的一些博客里展示的一些Untiy Shader中,实际的顶点着色器和片元着色器代码被编写到了一个语法块CGPROGRAM
和ENDCG
中(例如这篇博客)。这个语法块告诉我们,在这里面编写的代码遵从CG的语法。
CG = C for graphics。顾名思义,可以理解为针对图形编程的C语言。实际上,Cg是NVIDIA为GPU编程设计开发的一门新的语言,但它保留了C语言大部分的语义,同时又考虑GPU的体系结构添加了一些新的语义和特性,让开发者能够方便地在图形编程领域进行开发。
从这篇文章开始,我学习的CG都是Untiy Shader中使用的一些语法,Unity Shader 的 CG和实际的CG有小小的区别,虽影响不大,但是仍需注意。
语言特性
数据类型
按照维基百科上的介绍,CG总共有六种数据大类,其中一些是针对GPU专门是设计的:
- float - 32bit浮点数
- half - 16bit浮点数
- int - 32bit整数
- fixed - 12bit定点数
- bool - 布尔值
- sampler* - 代表纹理物件
有时候,我们会看到这样的类型定义float4
,这其实是一种语法糖,相当于声明一个四元数组float [4]
。由于在图形学编程中,四元及其以下的数组会经常用到,所以CG专门定义这些额外的类型。
pragma关键字
看一下这段代码
// 代码片段 1.0
#pragma vertex vert
#pragma fragment frag
float4 vert(float4 v:POSITION):SV_POSITION{
return mul(UNITY_MATRIX_MVP, v);
}
fixed4 frag():SV_Target{
return fixed4(1.0,1.0,1.0,1.0);
}
这段代码片段中,#pragma vertex
用来表示指定顶点着色器,而#pragma fragment
用来指定片元着色器。即在实际的渲染流程中,顶点着色器的实现代码就是vert()
函数。片元着色器同理。
语义
通常,语义被放在一个冒号之后。如,在代码片段1.0中,POSITION
,SV_POSITION
,SV_Target
都是语义。每种语义表示的含义不同。当一个语义被放在一个变量之后,通常表示这个变量和GPU存储器(显存)中的一些数据相互关联。(关于显存中保存着什么数据,可以参考这篇博客里的一张图)。我们知道,CPU在调用DrawCall通知GPU渲染一个模型之前,会将一些渲染相关的数据从内存中拷贝到显存中。这些数据包括但不限于顶点坐标,顶点颜色,顶点对应的纹理坐标和纹理,等等)。
在上述代码片段中,POSITION语义表示将v与显存中的顶点坐标相关联。由于这里是函数的输入变量,于是每次调用该顶点着色器时,GPU会将对应的顶点坐标填充到这个变量v中。由于一个顶点坐标是四元数组(为什么是四元,请参考关键词齐次坐标系),如果v是float3类型的,那么会将该顶点坐标前三个数填充到变量中。float2和float同理。
而SV_POSITION表示顶点着色器vert()的输出是裁剪空间中的顶点坐标。在这篇博客中,我们知道顶点着色器之后还有一个曲面细分着色器,几何着色器等等。顶点着色器在输出顶点变换后的坐标后,还要经过曲面细分着色器和几何着色器的改动才会得到真正的裁剪空间下的顶点坐标。但是SV_POSITION语义指定顶点坐标系输出的顶点将不需要经过这些步骤,直接就是裁剪空间下的坐标,同时直接进入下一步的光栅化处理。
SV_Target 表示片元着色器frag()的输出直接输出到一个渲染目标(render target)中,这里的渲染目标是指GPU中的帧缓存。GPU提供给开发者的帧缓存一般有八个,我们可以改为使用SV_TargetN( 0 <= N < 8)来指定输出到哪个帧缓存。在这种情况下,SV_Target0和SV_Target是一样的效果。编写片元着色器代码时,一定要将片元着色器的输出关联到一个SV_Target中)。
现在,再来看一个更复杂的例子
// 代码片段 2.0
#pragma vertex vert
#pragma fragment frag
struct a2v{
float4 vertex: POSITION;
float3 normal: NORMAL;
float4 texcoord: TEXCOORD0;
};
float4 vert(a2v v): SV_POSITION{
return mul(UNITY_MATRIX_MVP, v.vertex);
}
fixed4 frag(): SV_Target{
return fixed4(1.0,1.0,1.0,1.0);
}
在这个例子中,vert的输入变量 v 并没有指定语义,那么顶点着色器怎么知道从哪里拿数据去填充这个 v 呢?注意,这个 v 实际上是我们定义的结构体 a2v 。而在a2v里面,已经定义了一个顶点变量,并且关联了语义POSITION。所以实际上这个 v 是有数据的。而且是三个数据的封装,除了顶点数据,NORMAL语义表示将模型空间的法线方向填充normal变量。TEXCOORD0表示用模型的第一套纹理坐标填充texcoord变量。
语义总结
- 顶点着色器输入(从应用阶段传递模型数据给顶点着色器)相关语义
语义 | 描述 |
---|---|
POSITION | 模型空间的顶点坐标,通常是float4类型 |
NORMAL | 顶点法线,通常是float3类型 |
TANGENT | 顶点切线,通常是float4类型 |
TEXCOORDn | 该顶点的纹理坐标,TEXCOORD0表示第一组纹理坐标,依此类推 |
COLOR | 顶点颜色,通常是fixed4或float4类型 |
- 顶点着色器输出/片元着色器输入(从顶点着色器传递给片元着色器)
语义 | 描述 |
---|---|
SV_POSITION | 裁剪空间中的顶点坐标,片元着色器的输入必须包含一个这样语义修饰的变量 |
COLOR0 | 顶点着色器输出第一组顶点颜色,通常不是必须的 |
COLOR1 | 顶点着色器输出第二组顶点颜色,通常不是必须的 |
TEXCOORDn,n∈[0,7] | 顶点着色器输出纹理坐标,通常不是必须的 |
- 片元着色器输出
语义 | 描述 |
---|---|
SV_Target | 输出值将会存储到一个渲染目标中。 |
现在,我们已经知道如何表示顶点着色器的输入和输出,现在就可以进入正式的Unity Shader编程了。