第9章 渲染管线,着色和效果

9章 渲染管线,着色和效果

本章,你将学习一些关于渲染管线,着色和效果的概念。渲染管线负责渲染3D场景成2D图像,并绘制到屏幕。您可以用着色进行渲染管线的某些阶段的编程,并用效果来形容一个着色的组合和配置渲染管线的固定阶段。这灵活的允许你创建自定义视觉效果,改进最终图像的视觉样貌。

 

渲染管线

     要可视化一个3D场景到屏幕,场景要转换成2D图形。这个处理过程叫渲染。如图9-1

     

 

3D场景中的对象通过网格来描述,网格是它的顶点的集合(和索引,如果合适)。网格中的顶点可以有许多不同的属性,像位置,颜色,法线和纹理坐标。

渲染处理的开始,对象网格的顶点发送到渲染管线,这是顶点处理,光栅化和像素处理的地方。处理的结尾,生成许多像素,存储到场景的最终图像。因为一个对象的许多三角形可能竞争屏幕上相同的像素,渲染管线的最后阶段-合并输出-决定哪个像素更靠近相机。输出合并存储这些像素到最终图像并决定哪个像素被丢弃。这个决定基于相机和对象间的距离,因此只有最近的对象被显示,但这个决定也可能被透明度信息影响。在旧版DirectXOpenGL应用程序接口(APIs)中,所有渲染管线阶段是固定的(预排程序)。这意味着游戏程序员可用固定效果设置。这强制所有游戏用同样的渲染程序,只能改变几个预订参数。结果是游戏看起来都差不多。

DirectX8.1起,由于一个叫着色器的小程序诞生,编程渲染管线的一些部分成为可能。着色器允许你定义输入哪种数据和从每个图形处理单元的可编程阶段输出(GPU),更重要的,处理发生在每个阶段。使用着色器,你可以为游戏创建许多用固定管线不能做到的新效果。

XNA,你用着色器渲染一些对象到屏幕。为减轻游戏开发无需编程你自己的着色器,XNA提供一些包含一个着色器和效果基础设置的助手类。例如,你可用SpriteBatch类绘制2D精灵,BasicEffect类绘制3D模型。这两个类用透明于你的方式使用着色器。顾名思义,这些类只支持基础渲染。SpriteBatch类将渲染你保存在磁盘上的样子,但不添加任何射灯,反光,或荡漾效果。BasicEffect类可以用基础光照渲染你的3D世界。如果你想编程一些效果,你可以创建你自己的着色器。

 

着色器

着色器是一个在GPU内部执行并定义从XNA程序收到的数据如何在渲染管线的可编程阶段处理的小程序。着色器通常写进HLSL(高级着色语言)。

两个着色器一般应用:顶点着色和像素着色。光栅化在顶点着色和像素着色之间执行。

 

顶点着色

用于顶点处理阶段的着色器叫顶点着色器,见9-1。顶点着色器的基本任务是从你的XNA程序读取顶点的原始坐标,转换他们到2D屏幕坐标,并交给下一个阶段。额外的,处理坐标的同时,你也可以选择去处理顶点的其他属性,像颜色,法线等等。

顶点着色器允许你执行许多任务,固体变形(solids deforming),骨骼动画,粒子运动(particle motion

 

光栅化

在光栅化阶段,你的GPU确定每个三角形占有哪些屏幕像素。所有这些像素发送到像素着色器,允许你做一个处理的最后阶段。

9-2是光栅化三角形,生成许多像素。尤其注意顶点属性是所有生成的像素之间的线性插值。

 

 

像素着色器

像素着色器的主要任务是接受一个像素作为输入,计算像素的最终颜色,并传递到合并输出。每个像素可以提供多种数据的像素着色器,由你的顶点着色器生成并由光栅化成线性插值。这允许你的像素着色器依照光照条件调整像素的颜色,添加反射,执行凹凸贴图等。你也可以用像素着色器应用后处理效果在整个要渲染的场景,像亮度,色彩增强,饱和度和模糊。

额外的,像素着色器可以改变像素深度。这个深度用在输出合并时决定哪个像素被绘制哪个不被绘制。这个深度指示原始三角形离相机有多远。但是,如果你想影响输出合并的决定,你可以自己指定这个值。

 

高级着色语言HLSL

XNA通过微软的HLSL天生支持着色编程。HLSL有几个内建功能,数学运算,访问纹理,流量控制。HLSL支持的数据类型和C语言的类似,向量,矩阵,采样器

 

HLSL 数据类型

HLSL支持许多不同数据类型,包含标量(scalars),向量和矩阵。表9-1显示该语言含有的标量数据类型。注意可以为所有该语言包含的标量类型创建向量和矩阵,像float2float4bool3×3,等等。

类型         

Bool          true false

Int           32位有符号整型

Half          16位浮点数

Float         32位浮点数

Double        64位浮点数

 

HLSL中现有的其它数据类型是采样器(sampler)类型,用来从纹理采样数据。不同采样器类型,像,sampler1Dsampler2Dsampler3D分别用来采样1D2D3D纹理。与取样类型相关联的几个状态,指定要采样的纹理,用来过滤用,纹理如何编址。采样器要定义在HLSL代码文件的顶部。下面是2D纹理采样器的例子:

Texture skyTexture;

sampler2D skySampler = sampler_state

{

   Texture = skyTexture;// 代表要被采样的纹理,只能通过用采样器读取

   MinFilter = Linear;// 这三个是过滤状态(filtering states)

   MagFilter = Linear;

   MipFilter = Linear;

   AddressU = Wrap;// 地址状态(addressing states)

   AddressV = Wrap;

   AddressW = Wrap;

}

 

统一和变化的输入

      HLSL有两类输入数据类型:

统一(Uniform)输入数据:这是在完整输入数据的处理期间使所有着色器里的顶点/像素不发生变化的数据类型。例如,渲染一棵树时,它的纹理,世界矩阵和光照条件都不变。这种数据设置在您的XNA应用程序中。

变化(Varying)输入数据:这是在着色器每次执行时都变化的数据。例如,渲染一棵树时,顶点着色器要处理数的所有顶点。这意味着顶点携带的信息在每个顶点着色周期都被改变。和统一输入数据不同 ,你用语义(Semantics)声明变化输入数据。

 

语义(Semantics

语义是HLSL中用来映射输入和输出数据来使名字可变的预定义词。例如,3D对象的每个顶点可能有一个float4包含3D位置,另一个float4包含2个纹理坐标。你的顶点着色器怎么知道用哪个表示位置?

解决方案是添加POSITION0(前一个O是字母,后一个0是数字)语义到顶点处理阶段去映射每个顶点的位置属性到不同变量,如下:

Float4 vertexPosition:POSITION0;

这个语义对所有变化输入数据(从应用程序接收或在渲染阶段传递)是必须的。例如,所有从顶点着色器输出并将在像素着色器中使用的数据必须和语义相关。语义是不区分大小写且用冒号(:)指定在变量名之后。

输入顶点着色器语义

Input            描述                   类型

Position[n]      对象空间的顶点位置     float4

COLOR[n]         漫反射色和镜面反射色   float4

NORMAL[n]        法线向量               float4

TEXCOORD[n]      纹理坐标               float4

TANGENT[n]       切线向量               float4

BINORMAL[n]      副法线向量             float4

BLENDINDICES[n]  骨融合指数             int4

BLENDWEIGHT[n]   骨融合比重             float4

 

输出顶点着色器语义

Output           描述                   类型

Position[n]      同质空间的顶点位置     float4x,y,z,w

COLOR[n]         漫反射色和镜面反射色   float4

TEXCOORD[n]      纹理坐标               float4

FOG              顶点雾化               float

 

你为从顶点着色器接收的变化数据使用输入顶点着色器语义。最常用的语义如POSITIONCOLORNORMALTEXTURE。如果顶点有切线或副法线向量你才使用TANGENTBINORMAL语义,比如你想做些凹凸贴图。当顶点连接到骨头时采用BLENDINDICESBLENDWEIGHT

POSITION语义是顶点着色器唯一必要的输出。如果你想从顶点着色器传递其它数据到像素着色器,TEXCOORD[n]就用的到了。

[n]是定义要使用的资源数的可选整数。例如,一个模型有3个纹理,TEXCOORD语义的[n]可以是0,1,2;如TEXCOORD0TEXCOORD1TEXCOORD2.

 

像素着色器语义

Input            描述                   类型

COLOR[n]         漫反射色和镜面反射色   float4

TEXCOORD[n]      纹理坐标               float4

COLOR[n]         输出色                 float4

DEPTH[n]         输出深度               float

 

由于像素着色器是在光栅化阶段之后执行,可用的输入语义是像素颜色和一些纹理坐标。纹理坐标标注映射到当前像素的纹理位置,这些坐标可用于从顶点着色器改变数据到像素着色器。

从像素着色器输出的最终数据是像素颜色和深度,像素颜色的输出是必须的,深度是可选的。

 

方法

HLSL允许创建像C语言的语法的方法,每个方法有声明和定义。方法声明包含方法名和返回值,可能还有参数列表。返回类型可以使用和他关联的语义。下面是一个作为像素着色器入口点的方法:

Float4 simplePS(float4 inputColor: COLOR0):COLOR0

{

   Return inputColor * 0.5f;

}

 

因为这个方法用作像素着色器的入口点,它的参数要有一个语义关联。在这情况下,该方法把接收到的颜色参数乘以0.5来缩放并作为最终像素颜色返回。注意方法的参数可以有其它修饰符像inoutinout,用来定义输入,输出和输入/输出参数。

一小部分固有功能内建在HLSL中。有数学,纹理访问等。这些方法并不一定直接映射到GPU的汇编指令。事实上,其中许多方法被映射到GPU的汇编指令,他们很可能为这些任务提供最好的执行方式。

 

创建采样着色器

这节,你将把你学到的放到一起,创建第一个用HLSL的着色器。作为一个好习惯,你从声明固定和变化变量开始:

// 从应用程序接收的矩阵-固定(世界,视图,投影)

Float4×4 matWVP:WorldViewProjection;

// 用作顶点输入的结构-变化

Struct vertexInput

{

   Float4 position:POSITION0;

};

// 用来传递顶点着色输出到像素着色输入的结构-变化

Struct vertexOutput

{

   Float4 hposition:POSITION;

   Float3 color:COLOR0;

}

你的着色器预测matWVP矩阵将被XNA程序设置。由相机创建该矩阵。当你的顶点着色器变化3D位置到2D屏幕坐标这样做是需要的。

你用vertexInput结构定义顶点着色器可以预测到的信息。如你所见,这个顶点着色器将能出来所有包含位置数据的顶点。

你用vertexOutput结构定义从顶点着色器传递到光栅化器并经过线性插值到像素着色器的数据种类。你的顶点着色器将生成强制位置以及颜色。

这里一个重点是顶点着色器输出的顶点位置不能被像素着色器访问。这个2D屏幕位置是光栅化器必须的,但他被光栅化器消耗了。如果你的像素着色器需要这个2D屏幕位置,你要作为额外的TEXCOORDINATE[n]语义来传递。

接下来,它自己声明顶点着色器:

pixelInput simpleVS(vertexInput IN)

{

   pixelInput OUT;

   // 变换顶点位置

   OUT.hposition = mul(IN.position, matWVP);

   OUT.color = float3(1.0f,1.0f,0.0f);

   Return OUT;

}

顶点着色器在XNA程序每次渲染顶点时调用。这个顶点被你的着色器作为vertexInput对象接收并出来成pixelInput对象。在SimpleVS方法里,你把他乘以mtWVP矩阵来计算输出2D屏幕位置。输出顶点颜色设成黄,#010100.

接下来,你定义像素着色器:

Float4 simplePS(pixelInput IN):COLOR0

{

   Return float4(IN.color.rgb,1.0f);

}

该像素着色器简单返回从顶点处理阶段接收的颜色。这个颜色作为最终像素颜色。

 

技术,过程和效果

在其最基本的形式,一个技术并不比顶点着色和像素着色的组合多点什么。下面是一个用你刚定义的顶点着色器和像素着色器的技术:

Technique basicTechnique

{

   Pass p0

{

   vertexShader = compile vs_2_0 simpleVS();

   PixelShader = compile ps_2_0 simplePS();

}

}

一个即使也定义着色器要和哪个着色模型组合。在这情况,你为每个着色器用着色模型2.0.更高版本有更复杂的着色器和更多功能,但前提是GPU要支持。

如你所见,每个着色器都封装进过程(pass)。一个通道读入所有要用技术绘制的顶点,处理它们,处理返回的像素,并渲染这些像素到后备缓冲,等待所有像素被处理后发送到屏幕。有些技术,可能在同一帧你想为所有顶点做2次这个处理。为此,这个技术将有2个过程,顶点着色和像素着色都有。

为了一些高级效果,你可能想用多个技术。你将用第一个技术变换场景到临时的中间图像,该图像用作另一个技术的输入。

所遇着色器和效果的组合叫effect。一个简单的效果将包含一个顶点着色器,一个像素着色器,和一个技术。一个高级效果将包含多个这些部分。

效果,技术和着色器之间的区别有助于着色程序,使着色代码可以在不同技术里重用,也可以创建不同技术面向低端和高端GPU

一般你将所有着色器和技术依照相同效果保存在一个文件中,XNA调用每个HLSL代码文件一个effect。这允许你把效果当作游戏资源,就像模型和纹理。所有通过XNA内容管道处理,生成内容管理可以在运行时加载的可管理对象。

 

Effect

XNA程序,效果要被加载到Effect类的对象。Effect类允许你配置效果的固定参数,选择当前效果技术并用做渲染,代码如下:

Effect effect;

// 加载效果

Effect = content.Load<Effect>(“/effects/simpleEffect”);

// 设置技术

Effect.CurrentTechnique = lightEffectTechniques[“basicTechnique”];

// 配置固定效果参数

Effect.Parameters[“matWVP”].SetValue(worldViewProjectionMatrix);

该代码用内容管理器的content.Load方法从HLSL代码文件加载simpleEffect效果。之后定义哪个效果要被使用;在这情况下,用basicTechnique技术。最后,设置定义在HLSL代码文件:matWVP中的固定效果参数。

用法如下:

// 开始效果

Effect.Begin();

// 记住效果有很多通道

Foreach(EffectPass pass in effect.CurrentTechnique.Passes)

{

   Pass.Begin();

   // 这里是绘制代码

   Pass.End();

}

// 结束效果

Effect.End();

要绘制3D对象,你想要启动你要用作绘制对象的效果,然后遍历所选技术的所有过程。对于每个过程,你要启动过程,绘制对象,结束过程。最后,结束效果。效果通道由XNAEffectPass类负责。如果你想改变在启动一个过程后改变效果参数,你要调用Effect类的CommitChanges方法更新改变。

上面的步骤只用在你想用自定义效果绘制模型时。平时绘制模型,可以用存储在模型的ModelMesh对象中的效果。

 

效果助手(Effect Helper)类

当一个效果通过内容管理器加载后,你不知道它有什么参数或技术。同样,要修改一个效果参数,你要先在效果里查询这个效果,然后修改它。所以,你可以像这样配置一个叫lightPosition的效果参数:

Effect.Parameters[“lightPosition”].SetValue(new Vector3(0.0f,40.0f,0.0f));

当你修改lightPosition参数的值,一个查询是在内部产生。这会有2个问题:计算此参数的查询的开销是必须的,且可能查询的是无效的参数。使用助手类可以解决这个问题。

要简化自创效果的管理,你可以为每个效果创建一个唯一的助手类。每个效果助手类将存储所有效果参数的引用,避免查询参数的开销。代码如下:

EffectParameter param1 = effect.Parameters[“lightPosition”];

Param1.SetValue(new Vector3(0.0f,40.0f,0.0f));

 

材质

材质是你要创建来存储用作配置效果的参数的类。例如,你可以用一个应用纹理的效果渲染2个面。在此情况下,每个面的材质就是它的纹理,要配置渲染面的效果就用这个纹理。因此,如果2个面共享同样的材质,你可以设置渴望的效果和渴望的材质,并按顺序渲染每个面避免改变当前设置的效果或参数。

下面是你将创建的2个基础材质类:

LightMaterial:这个类将存储用作光照表面属性(镜面色,漫反射色,高光色)。

TextureMaterial:这个类将存储用作应用纹理到表面的纹理映射和贴图(tile)。

你可以用这2个基础材质类创建更复杂类型的材质,比如多纹理材质。如下是LightMaterial类的完整代码:

 

P255(或者直接看书的源代码)

 

你把光照的漫反射和镜面反射色以XNAVector3形式存储在LightMaterial类的diffuseColorspecularColor属性中。你存储光照的镜面反射强度为float值,存在specularPower属性中。注意向量的(x,y,z)分别代表颜色的(rgb)。如下是TextureMaterial类的完整代码:

 

P256

 

你存储纹理为XNATexture2D形式在TextureMaterial类的texture属性.纹理UV贴图用作凹凸贴图,以XNAVector2形式存储在uvTile属性。

 

着色创作工具

着色器开发期间,你要不断修改着色器,调整参数,并在不同资源(模型,纹理等)上测试。每次检验修改都要重编译和执行游戏,这个处理过程很慢也很累。为此你可以使用着色创作工具。

最好的工具之一是NVIDIAFX Composer,自己上网下去。这是一个IDE,支持HLSL在内的多种着色语言和FBXX等多种模型文件。你可以实时查看修改代码的结果。反正就是好东西。

posted @ 2009-09-05 10:38  conglele  阅读(1741)  评论(0编辑  收藏  举报