您的位置:首页 > 其它

3D绘图程序设计之shader学习总结(二)

2014-07-19 00:25 141 查看
经过上一篇的3D渲染管线的学习之后,对shader的学习有很大帮助,接下来就正式学习shader,进入GPU图形编程的世界(啊,说得有些夸张,其实就是做些渲染方面的特效以及图像处理方面的效果)。废话不多说,首先先大致介绍下GPU以及shader的版本。

1.GPU及Shader版本简介

GPU简介

GPU拥有强大的并行计算以及进行四维向量浮点数运算的能力。并行计算也就是数据之间没有依懒性,相互之间都是独立的,比如在进行顶点数据计算时,各数据之间是相互独立的,就可进行并行计算,因此GPU一次能同时处理很多个顶点;而CPU往往处理的数据之间会有依赖,比如要等这些数据处理好之后,才能处理后面的数据。

早期,GPU只用于固定渲染管线的绘制流程(上一篇正是在介绍固定渲染管线),以及填充画面上各个像素的颜色;而模型顶点数据的处理(包括三角形、多边形的坐标变换)、光照计算等还是由CPU进行处理,这岂不是浪费了GPU那么强大的并行计算能力吗。随着硬件发展之快速,GPU已可支持计算顶点数据以及光照等,并且还能支持可程序化的渲染流程(Programing pipline),也就是shader,以及能对纹理等进行操作。目前PC上大家所熟悉的NVIDIA Geforce系列和ATI Radeon系列的显卡在早期的型号并没有像现在能很好的支持上面的这些功能。

Shader版本简介

这里以DirectX3D中的shader版本标准来说明。目前的主要版本有4个,最高的版本为Shader Model 4.0。Shader Model 1.0的vertex和pixel shader不支持流程控制,即if-else和循环for-loop,而且也基本上是汇编语言来编写shader。到了Model 2.0,还是不能真正支持流程控制,但对代码长度以及指令数量的支持有了大幅增加,并且可使用HLSL(High Level Shading Language)和CG(C for Graphic)等高级语言来编写shader。等到了Model
3.0,才是正式的支持流程控制。Shader Model 4.0 可以说是一个重大的标准更新,大幅增加了很多功能,DirectX10/11已能支持4.0。不过一般2.0和3.0已能满足大部分的开发需求。

shader虽然已能支持流程控制,但还是尽量少用流程控制语句,毕竟GPU是处理并行计算的,加入流程控制可能会破坏其并行性,很消耗性能,尤其是当前移动端的GPU,更需要考虑这些问题。不过,shader在被编译为GPU所能执行的指令序列时,编译器(compiler)可能会以一定的标准避开流程控制,比如for循环,将被编译为多条语句,只要循环的条件限制是常量并且不是通过shader内部计算的值得到的,就可以。If语句也同样可以,具体的标准还是到网上查查吧。

2.编写Shader使用的开发环境

我在《3D绘图程序设计》这本书上看到是使用NVIDIA FX Composer来编写并调试shader的,主要支持HLSL和CG,该软件主要提供了能及时查看shader运行效果的功能,而GLSL需要Render Monkey来编写并查看效果。这里就选用了Fx Composer来编写。

在安装Fx Composer时,尤其是win8,很坑呀。安装时,会到一个界面是选择安装的组件,要把Shader Debugging Plugin这个取消,否则在启动时,就会报错。如果取消了这个插件,启动还是有问题,那就试试装Cg Toolkit这个工具包,该工具包中会带有关CG开发的帮助文档,CG的API以及数学运算库都能从这里查到。

3.第一个Shader程序

说了这么半天,终于可以编写第一个shader啦,那就叫它HelloShader吧!还是先贴代码吧

HelloShader.fx

uniform float4x4 WorldViewProj : WorldViewProjection;  //模型观察投影矩阵

float4 mainVS(float3 pos : POSITION) : POSITION{
return mul(float4(pos.xyz, 1.0), WorldViewProj);
}

float4 mainPS() : COLOR {
return float4(1.0, 1.0, 1.0, 1.0);
}

technique technique0 {
pass p0 {
CullMode = None;
VertexShader = compile vs_3_0 mainVS();
PixelShader = compile ps_3_0 mainPS();
}
}

HLSL的shader的结构大致就是这样。首先,看到一个程序,就会先找它的执行入口在哪,而shader中没有明确规定哪个就是入口,需要自己去定义,而且是定义vertex shader和 pixel shader各自的入口,就是在technique定义体内的Vertex和PixelShader后面的函数名,函数名可以自己定义,vs_3_0是shader model的版本,即3.0。

CullMode就是设置背面消隐的方式,具体宏定义如下:

(1)None为完全禁用背面消隐

(2)CW为只对顺时针绕序的三角形进行消隐

(3)CCW为只对逆时针绕序的三角形进行消隐

代码第一行定义的变量WordViewProj在mainPS中有用到,但是这个变量是在哪儿赋值的呢。实际上该变量是由CPU计算好,再传过来的。这个过程也就涉及到了CPU与GPU的交互,shader是由CPU告诉GPU去执行的,DirectX中会有相应的API去执行这个指令,比如SetVertexShader和SetPixelShader用来告诉GPU去执行哪一组shader。而且CPU会将模型的顶点数据、纹理信息等传递给GPU,这样GPU就能读取这些数据。在Vertex Shader中,要读取数据,还可以从一组称为常量寄存器(Constant
Registers)的空间中读取数据,CPU将数据放到常量寄存器中,就可供Vertex Shader使用啦。而且,存放在常量寄存器的数值是全局的,也是只读的,Vertex Shader并不能改变其值。

声明为uniform的变量就存放在常量寄存器中,在HLSL中,uniform关键字可以被省略,而在GLSL中,就不能省略了。因此,上面的WorldViewProj变量也就存放在了常量寄存器中,这是个转换矩阵,在Fx Composer中,会自动将该转换矩阵的内容传入后面声明的WorldViewProjection所代表的寄存器中。WorldViewProjection即模型观察投影矩阵。

在WorldViewProj变量前面的关键词那肯定就是类型了,终于说到类型这么基本又关键的问题啦。看到float4x4这个类型,感觉有些奇怪,和其他的语言有所不同,该类型代表4x4的一个矩阵,由4个四维向量组成。它为啥要设计成这样的类型呢,实际上这正是用到了GPU计算四维向量的优势,也就是GPU可以通过一条指令完成4个float类型数值的计算。

下面的mainVS函数就是Vertex Shader的入口,返回值是float4类型的一个四维向量。该函数的参数后面有个POSITION,这语法那就更与其他语言不同了,它指明了所传入的参数为顶点的位置,shader在读取pos变量时,会到顶点处理单元中存放位置的寄存器读取数据。该函数返回值后面也有个POSITION字段,这个和参数中的POSITION所代表的意义不同,它代表的是屏幕坐标系中的位置。函数内通过顶点坐标与转换矩阵相乘后,就得到了屏幕坐标系中的坐标。

在Vertex Shader中处理好的数据,之后,就会供Pixel Shader使用。mainPS函数中的返回值后面用的是COLOR字段,在此是指framebuffer上的颜色,也就是该返回值会被放在GPU像素处理单元中代表画面颜色的寄存器中,用来更新画面的颜色。函数内的实现就很简单了,返回了白色,改变其中的值,就会看到不同的颜色效果,在shader中,用到的颜色的取值范围是0-1。





颜色值为(1,1,1,1) 颜色值为(0.5,1,1,1)

4.在shader中定义结构体

在第一个shader程序中,两个函数的参数都只用到了一个单独的变量,为了更方便传入更多的参数,就用到了自定义结构体。还是先贴出例子吧

NormalColor.fx

uniform float4x4 WorldViewProj : WorldViewProjection;

struct VS_INPUT
{
float4 pos : POSITION;   //位置
float3 normal : NORMAL;  //法线
};

struct VS_OUTPUT
{
float4 pos : POSITION;  //转换到屏幕坐标的位置
float4 color : COLOR;   //顶点颜色
};

VS_OUTPUT mainVS(VS_INPUT i)
{
VS_OUTPUT o;
o.pos = mul(float4(i.pos.xyz, 1.0), WorldViewProj);
o.color = float4(i.normal.xyz, 1.0);
return o;
}

float4 mainPS(VS_OUTPUT i) : COLOR {
return i.color;
}

technique technique0 {
pass p0 {
CullMode = None;
VertexShader = compile vs_3_0 mainVS();
PixelShader = compile ps_3_0 mainPS();
}
}
这个shader的效果是将顶点的法线作为颜色输出,当模型顶点的法线方向不一致时,就会出现有趣的现象。下面简单说明一下不同的地方。

这里会看到很类似于C++等一些语言的结构体定义,VS_INPUT和VS_OUTPUT分别作为mainVS的输入和输出参数,在VS_INPUT中出现了NORMAL的字段,该字段代表在读取normal变量时,会到顶点处理单元中存放法线的寄存器中读取数据。在VS_OUTPUT中的COLOR字段,我想应该是将该值存储在经过转换后的顶点颜色寄存器中。

在Pixel Shader中,将VS_OUTPUT作为输入参数,就不需要再定义一个结构体,要注意的是,Pixel Shader并不能获取经过计算转换到屏幕坐标系上的位置,要想读取转换后的顶点位置,就要在Vertex Shader中将转换后的位置放到可以让Pixel Shader读取的寄存器,比如可以将数据放到贴图坐标字段中(TEXCOORD0等)。而COLOR字段,应该是可以被Pixel Shader读取的。

下面就看一下显示效果吧



茶壶的法线方向不同,平面的法线相同

好了,就说这么多吧。 这一篇主要是shader的一些基础。下一篇再接着学习光照计算。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: