第07章 输入\输出与语义绑定
我们最大的弱点在于放弃。成功的必然之路就是不断的重来一次。
------ 托马斯·爱迪生
第三章从 GPU 运行原理和数据流程的角度阐述了顶点着色程序和片段着色程序的输入输出,即,应用程序(宿主程序)将图元信息(顶点位置、法向量、纹理坐标等)传递顶点着色程序;顶点着色程序基于图元信息进行坐标空间转换,运算得到的数据传递到片段着色程序中;片段着色程序还可以接受从应用程序中传递的纹理信息,将这些信息综合起来计算每个片段的颜色值,最后将这些颜色输送到真缓冲区(或颜色缓冲区)中。
这些是顶点着色程序和片段着色程序的基本功能和数据输入输出,实际上现在着色程序已经可以接受多种数据类型,并灵活的进行各种算法的处理,如,可以接受光源信息(光源位置、强度等)、材质信息(反射系数、折射系数等)、运动控制信息(纹理投影矩阵、顶点运动矩阵等),可以在顶点程序中计算光线的折射方向,并传递到片段程序中进行光照计算。
这一章节中,我们将讲解 Cg 语言通过何种机制确定数据类型和传递形式。读者要抱着如下几个问题阅读本章节:
1. 从应用程序传递到 GPU 的数据,分为图元信息数据(在 GPU 处理的基本数据如顶点位置信息等)和其他的离散数据(在 GPU 运行流程中不会发生变化,如材质对光的反射、折射信息),这两种输入数据如何区分?
2. 从应用程序传递到 GPU 中的图元信息如何区分类型,即,顶点程序怎么知道一个数据是位置数据,而不是法向量数据?
3. 顶点着色程序与片段着色程序之间的数据传递如何进行?
7.1 Cg 关键字
关键字是语言本身所保留的一个字符串集合,用于代表特定的含义,如前面所讲到的数据类型关键字 int 、float 等,以及结构体关键字 struct。 Cg 中的关键字很多都是照搬 C\C++ 中的关键字,不过 Cg 中也创造了一系列独特的关键字,这些关键字不但用于指定输入图元的数据含义(是位置信息,还是法向量信息),本质也则对应着这些图元数据存放的硬件资源(寄存器或者纹理),称之为语义词(Semantics),通常也根据其用法称之为绑定语义词(binding semantics)。
除语义词外,Cg 中还提供了三个关键字,in、out、inout,用于表示函数的输入参数的传递方式,称为输入\输出关键字,这组关键字可以和语义词合用表达硬件上不同的存储位置,即同一个语义词,使用 in 关键字修饰和 out 关键词修饰,表示的图形硬件上不同的寄存器。
Cg 语言还提供两个修饰符:uniform,用于指定变量的数据初始化方式;const 关键字的含义与 C\C++ 中相同,表示被修饰变量为常量变量。
下面将分别对上述的关键字进行详细阐述。这一章非常关键,尤其是语义词的使用方法和含义,再小的 Cg 程序都需要使用到语义词。
7.2 uniform
Cg 语言输入数据流分为两类(参见文献【3】 Program Inputs and Ooutputs):
1. Varying inputs, 即数据流输入图元信息的各种组成要素。从应用程序输入到 GPU 的数据除了顶点位置数据,还有顶点的法向量数据,纹理坐标数据等。Cg 语言提供了一组语义词,用以表明参数是由顶点的哪些数据初始化的。
2. Uniform inputs,表示一些与三维渲染有关的离散信息数据,这些数据通常由应用程序传入,并通常不会随着图元信息的变化而变化,如材质对光的反射信息、运动矩阵等。Uniform 修饰一个参数,表示该参数的值由外部应用程序初始化并传入;例如在参数列表中写:
uniform float brightness,
uniform float4x4 modelWorldProject
表示从“外部”传入一个 float 类型数据,和一个 4 阶矩阵。“外部”的含义通常是用 OpenGL 或者 DirectX 所编写的应用程序。
使用 Uniform 修饰的变量,除了数据来源不同外,与其他变量我是完全一样的。
需要注意的一点是:uniform 修饰的变量的值是从外部传入的,所以在 Cg 程序(顶点程序和片段程序)中通常使用 uniform 参数修饰函数形参,不容许声明一个用 uniform 修饰的局部变量!否则编译时会出现错误提示信息:
Error C5056:‘uniform’ not allowed on local variable
7.3 const
Cg 语言也提供 const 修饰符,与 C\C++ 中含义一样,被 const 所修饰的变量在初始化之后不能再去改变它的值。下面的例子程序中有一个声明为 const 的变量被赋值修改:
const float a = 1.0;
a = 2.0; // 错误
float b = a++; // 错误
编译时会出现错误提示信息:
error C1026: assignment to const variable.
const 修饰符与 uniform 修饰符是相互独立的,对一个变量既可以单独使用 const 或者 uniform,也可以同时使用。
7.4 输入\输出修饰符(in\out\inout)
参数传递是指:函数调用实参值初始化函数硬形参的过程。在 C\C++ 中,根据形参值的改变是否会导致实参值的改变,参数传递分为“值传递(pass-by-value)”和“引用传递(pass-by-reference)”。按值传递时,函数不会访问当前调用的实参,函数体处理的是实参的拷贝,也就是形参,所以形参值的改变不会影响是实参值;引用传递时,函数接收的是实参的存放地址,函数体中改变的是实参的值。C\C++ 采取指针机制构建引用传递,所以通常引用传递也称为“指针传递”。
Cg 语言中参数传递方式同样分为“值传递”和“引用传递”,但指针机制并不被 GPU 硬件所支持,所以 Cg 语言采用不同的语法修饰符来区别“值传递”和“引用传递”。这些修饰符分别为:
1. in: 修饰一个形参只是用于输入,进入函数体时被初始化,且该形参值的改变不会影响实参值,这是典型的值传递方式。
2. out: 修饰一个形参只是用于输出的,进入函数体时并没有被初始化,这种类型的形参一般是一个函数的运行结果。
3. inout: 修饰一个形参既用于输入也用于输出,这是典型的引用传递。
举例如下:
void muFunc(out float x); // 形参x, 只用于输出
void muFunc(inout float x); // 形参x, 既用于输入时初始化,也用于输出数据
void muFunc(in float x); // 形参x, 只用于输入
void muFunc(float x); // 等价与 in float x,这种用法和C\C++完全一致
也可以使用 return 语句来代替 out 修饰符的使用。输入\输出修饰符通常和语义词一起使用,表示顶点着色程序和片段着色程序的输入输出。
7.5 语义词(Semantic)与语义绑定(Binding Semantics)
语义词,表示输入图元的数据含义(是位置信息们还是法向量信息),也表明这些图元数据存放的硬件资源(寄存器或者纹理缓冲区)。顶点着色程序和片段着色程序中 Varying inputs 类型的输入,必须和一个语义词相绑定,这称之为绑定语义(Binding semantics)。
7.5.1 输入语义与输出语义的区别
语义概念的提出和图形流水线工作机制大有关系。从前面所讲的 GPU 处理流程中可以看出,一个阶段处理数据,然后传输给下一个阶段,那么每个阶段之间的接口是如何确定的呢?例如:顶点处理器的输入数据是处于模型空间的顶点数据(位置、法向量),输出的是投影坐标和光照颜色;片段处理器要讲光照颜色做为输入,问题是“片段处理器怎么知道光照颜色值的存放位置”?
在高级语言中(C\C++),数据从接口的一端流向另一端,是因为提供了数据存放的内存位置(通常是指针信息);由于 Cg 语言并不支持指针机制,且图形硬件处理过程中,数据通常暂存在寄存器中,故而在 Cg 语言中,通过引入语义绑定(binding semantics)机制,指定数据存放的位置,实际上就是将输入\输出数据和寄存器做一个映射关系(在 OpenGL Cg Profiles 中是这样的,但在 DirectX-based Cg profiles 中则并没有这种映射关系)。根据输入语义,图形处理器从某个寄存器取数据;然后再将处理好的数据,根据输出语义,放到指定的寄存器。
记住这一点:语义,是两个处理阶段(顶点序、片段程序)之间的输入\输出数据和寄存器之间的桥梁,同时语义通常也表示数据的含义,如 POSITION 一般表示参数中存放的数据是顶点位置。
语义,分为输入语义和输出语义;输入语义和输出语义是有区别的。虽然一些参数经常会使用相同的绑定语义词,例如:顶点 Shader 的输入参数,POSITION 指应用程序传入的顶点位置,而输出参数使用 POSITION 语义就表示要反馈给硬件光栅化的裁剪空间位置,光栅化把 POSITION 当成一个位置信息。虽然两个语义命名为 POSITION,但却对应着图形流水线上不同的寄存器。
说明:在 OpenGL Cg Profiles 中,语义绑定指定了输入\输出数据和图形硬件寄存器之间的对应关系;但是在 DirectX Cg Profiles 中,则并非如此。在文献【3】的第 25 页写到: In the OpenGL Cg Profiles, binding semantics implicitly specify the mapping of varying inputs to particular hardware. Howerver, in DirectX based Cg profiles there is no such implied mapping. 在文献【3】的第 260 页写到: Often, these predefined names correspond to the names of hardware registers or API respurces. |
---|
7.5.2 顶点着色程序的输入语义
图 13 所示的这组绑定语义关键字被 Cg 语言的所有 Vertex Profiles 所支持,一些 Profiles 支持额外的语义词。
语义词 POSITION 等价与 POSITION,其他的语义词也有类似的等价关系。为了说明语义词的含义,举例如下:
int float modelPos:POSITION
表示该参数中的数据是顶点位置坐标(通常位于模型空间),属于输入参数,语义词 POSITION 是输入语义,如果在 OpenGL 中则对应为接受应用程序传递的顶点数据的寄存器(图形硬件上)。
int float4 modelNormal:NORMAL
表示该参数中的数据是顶点法向量坐标(通常位于模型空间),属于输入参数,语义词 NORMAL 是输入语义,如果在 OpenGL 中则对应为接受应用程序传递的顶点法向量的寄存器(图形硬件上)。
注意,上面的参数都被声明为四元向量,通常我们在应用程序涉及的顶点位置和法向量都是三元向量,至于为什么要讲三元向量变为四元向量,又称齐次坐标,具体请看附录 A。顶点位置坐标传入顶点着色程序中转化为四元向量,最后一元数据为 1,而顶点法向量传入顶点着色程序中转化为四元向量,最后一元数据为 0。
7.5.3 顶点着色程序的输出语义
顶点程序的输出数据被传入到片段程序中,所以顶点着色程序的输出语义词,通常也是片段程序的输入语义词,不过语义词 POSITION 除外。
下面这些语义词使用于所有的 Cg Vertex Profiles 作为输出语义和 Cg Fragment Profiles 的输入语义:POSITION,PSIZE,FOG,COLOR0-COLOR1,TEXCOORD0-TEXCOORD7。
顶点着色程序必须声明一个输出变量,并绑定 POSITION 语义词,该变量中的数据将被用于,且只被用于光栅化!如果没有声明一个绑定 POSITION 语义词的输出变量,如下所示的代码:
void main_v(float4 position:POSITION, /*out float4 oposition:POSITION,*/ uniform float4x4 modelViewProj)
{
//oposition = mul(modelViewProj, position);
}
在使用 vp20 和 vp30 编译时会提示错误信息:error C6014: Required output 'HPOS' not written. 在使用 vs_2_0 和 vs_3_0 编译时会提示错误信息:error C6014:Required output 'POSITION' not written.
为了保持顶点程序输出语义和片段程序输入语义的一致性,通常使用相同的 struct 类型数据作为两者之间的传递,这是一种非常方便的写法,推荐使用。例如:
struct VertexIn
{
float4 position:POSITION;
float4 normal:NORMAL;
};
struct VertexScreen
{
float4 oPosition:POSITION;
float4 objectPos:TEXCOORD0;
float4 objectNormal:TEXCOORD1;
};
注意:当使用 struct 结构中的成员变量绑定语义时,需要注意到顶点着色程序中使用的 POSITION 语义词,是不会被片段程序所使用的。
如果需要从顶点着色程序向片段程序传递数据,例如顶点投影坐标、光照信息等,则可以声明另外的参数,绑定到 TEXCOORD 系列的语义词进行数据传递,实际上 TEXCOORD 系列的语义词通常被用于从顶点程序向片段程序之间传递数据。
当然,你也可以选择不使用 struct 结构,而直接在函数形参中进行语义绑定。无论使用何种方式,都要记住 Vertex Program 中的绑定语义(POSITION 除外)的输出形参的数据会传递到 Fragment Program 中绑定相同语义的输入形参中。
7.5.4 片段着色程序的输出语义
片段着色程序的输出语义词较少,通常是 COLOR。 这是因为片段着色程序运行完毕后,就基本到了 GPU 流水线的末端了。片段程序必须声明一个 out 向量(三元或者四元),绑定语义词 COLOR,这个值将被用做该片段的最终颜色值。例如:
void main_v(out float4 color:COLOR)
{
color.xyz = float3(1.0, 1.0, 1.0);
color.w = 1,0;
}
一些 fragment profiles 支持输出语义词 DEPTH,与它绑定的输出变量会设置片段的深度值;还有一些支持额外的颜色输出,可以用于多渲染目标(multiple render targets, MRTs)。
和顶点着色程序一样,片段着色程序也可以将输出对象放入一个结构体中。不过,这种做法未必方便,理由是:片段着色程序的输出对象少,最常用的就是颜色值(绑定输出语义词 COLOR),单独的一个向量没有必要放到结构体中。而顶点着色程序输出的对象很多,在有些光照或阴影计算中,往往要输出顶点的世界坐标、法向量、光的反射方向、折射方向、投影纹理坐标等数据,这些数据统一放到结构体中方便管理。
7.5.5 语义绑定方法
入口函数输入\输出数据的绑定语义有4 种方法(文献【3】第 260 页)
1. 绑定语义放在函数的参数列表的参数声明后面中:
[const][in|out|inout][:][=]
其中,const 作为可选项,修饰形参数据;in、out、inout 作为可选项,说明数据的类型;identifier 是必选项,形参变量名;一个冒号“:”加上一个绑定语义,是可选项;最后是初始化参数,是可选项。如下代码所示。形参列表中的参数一、参数而绑定到输入语义;参数三、参数四绑定到输出语义;尽管参数一和参数三的绑定语义词一样,但前者是输入语义,后者是输出语义,所以这两个参数数据所对应的硬件位置视不一样的。
void main_v(float4 positon_obj:POSITION, float3 normal_obj:NORMAL,
out float4 oPosition:POSITION, out float4 oColor:COLOR,
uniform float4x4 modelViewProj)
{
...............
}
2. 绑定语义可以放在结构体(struct)的成员变量后面:
struct
{
[:];
}
举例如下,结构 C2Elv_Outpu 中的 2 个成员变量分别绑定到语义 POSITION 和 COLOR,然后在 C2Elv_green 顶点程序入口函数中输出,所以 C2Elv_Outpu 中的语义是输出语义。
struct C2Elv_Output
{
float4 position:POSITION;
float3 color:COLOR;
}
C2Elv_Output C2Elv_green(float2 position:POSITION)
{
C2Elv_Output OUT;
OUT.position = float4(position, 0, 1);
OUT.color = float3(0,1,0);
return OUT;
}
3. 绑定语义可以放在函数声明的后面,其形式为:
()[:]
{
}
如下代码所示,顶点入口函数的声明后带有“COLOR”语义词,表示该函数需要反馈一个颜色值,所以函数的返回类型为 float4, 函数体也必须以 return 语句结束。
float4 main_v(float4 positon:POSITION,
out float4 oposition:POSITION,
uniform float4x4 modelViewProj):COLOR
{
oposition = mul(modelViewProj, positon);
float4 ocolor = float4(1.0, 0,0,0);
return oclolor;
}
4. 最后一种语义绑定的方法是,将绑定语义词放在全局非静态变量的声明后面。其形式为:
[:][=]
这种形式的结构很不紧凑,也不利于代码的维护和阅读,所以并不常见,不建议读者使用。事实上,我在学习和研究过程中也很少碰到这种形式。