本章你将学到一些与渲染管道,着色器,效果相关的概念.渲染管道作用是将3d场景绘制到2d图形,这样就可以在屏幕上显示了.渲染管道的一些阶段可以使用着色器,并使用效果来描述着色器和渲染管道阶段配置的组合.这个特性可以让你创建
自定义的效果,提高最终图形的效果.
渲染管道
为了让3d场景在2d屏幕上可见,场景必须被转换到2d图形.将3d绘制进一个2d图形称为渲染.图9-1显示了xna使用的渲染管道的抽象.
图 9-1 xna的渲染管道
3d场景中的物体是用网格(mesh)来描述,网格是顶点(还可能有索引)的集合.网格中的顶点有很多不同的属性,如位置,颜色,纹理坐标,如8章所述.
如图9-1所示,在渲染进程的开始,物体网格的顶点集合被发送到渲染管道,顶点集合将经过顶点处理阶段,光栅,像素处理.流程的最后,就产生很多准备储存进场景最终图形的像素.物体三角形为屏幕上的同一个像素进行排序,渲染管道的最后一个阶段-输出合并-用来决定哪个像是离摄影机最近.并将这些像素储存进最后的图形丢弃不可见的像素.判断是否可见的规则是摄影机与物体之间的距离,因此最近的物体可见,同时判断会被透明度信息所影响.
在老
版本的Directx和Opengl中,所有的渲染管道阶段都是固定的(预先定义好).这意味着效果的集合都是固定的,强制所有的游戏使用相同的渲染管道,只允许修改一些预定义的参数.导致大部分游戏风格都一样.
从directx8.1之后,通过创建一个被称为着色器的小程序使渲染管道编程变为可能.着色器允许你定义gpu编程阶段输入哪些数据及输出哪些数据,更重要的时每个阶段都有此过程.使用着色器你可以创建很多固定管道不能实现的游戏效果.
Xna中,你使用着色器来渲染任何物体.为了简化游戏开发xna提供了一些包含进程的着色器和效果的助手类.如,你可以使用SpriteBatch来绘制2d图元,BasicEffect来绘制3d模型.这2个类都可以使用透明度.如
他们的名字一样这些类只提供了基础的渲染.SpriteBatch来将渲染储存在
硬盘上的图形,并不会应用聚光灯,反射及图形涟漪效果.BasicEffect类可以使用基础光渲染3d世界.如果你想得的更好的效果
你需要写自己的着色器.
着色器
着色器是跑在gpu中的小程序,用来定义可编程内容管道如何接收xna程序的数据.着色器通常用HLSL来写.
有2个着色器:顶点着色器和像素着色器.光栅阶段在顶点和像素间被执行.
顶点着色器
用于顶点处理阶段,图9-1被顶点着色器调用.顶点着色器的最基本的任务是从xna程序中读取顶点的原始坐标,将其转换为2d屏幕坐标,准备进行下一阶段.另外,操作坐标的同时,你还可以处理各个顶点别的属性,如颜色,法线等.
顶点着色器可以执行很
多任务,如单纯的形变,骨骼动画,和粒子运动等.
光栅
在此阶段,你的gpu来检测每个三角形占据了屏幕的那些像素.所有的这些像素都将被发送到像素着色器,用于完成最后的处理.
图9-2显示了一个光栅的三角,占据了很多像素.特别注意:顶点的属性是在所有生成的像素中使用线性插值.
图9-2 三角行光栅化,灰色的网格表示产生的像素.
像素着色器
此着色器的主要任务是接收输入的像素,计算每个像素的最终颜色并将它们传递到输出合并.每个像素都可以为着色器提供多种数据,此数据由顶点着色器和光栅阶段线性插值所产生.这可以允许你根据光照条件来调整像素的颜色.如增加反射,执行凹凸贴图等.你也可以用像素着色器对一个完整的渲染过的场景进行后期处理.如,明亮度,对比度及颜色增强,饱和,模糊.
另外,像素着色器可以改变像素的深度.此深度用于输出合并时决定哪些像素需要绘制.默认深度指示源三角离摄影机多远.但是,如果你想影响输出合并,你可以指定自己的值.
高级着色语言
Xna使用微软的hlsl来原始支持着色.hlsl有一些内置的函数,包含数学操作,纹理访问及流程控制.hlsl支持的数据类型很像c,除了向量(vector),矩阵(matrix),采样器(sampler).
Hlsl数据类型
Hlsl支持多种不同的数据类型,包括,标量,向量,矩阵.表9-1显示hlsl标量数据类型.注意用现有的标量类型来创建向量和矩阵是可能的,如float2,float4,bool3x3,double2x2等.
表 9-1. Hlsl标量类型
类型 说明
bool true或false
int 32位有符合整数
half 16位浮点
float 32位浮点
double 64位浮点
另外的数据类型是采样器,用来从纹理采样.不同的采样器类型,如sampler1D,sampler2D,sampler3D,被用来用1d,2d,3d中采样.与采样器类型一起的还有一些状态,指明如何从纹理中采样,过滤器的使用,及纹理如何寻址.
采样器应该定义在hlsl程序的顶部,这里是2d纹理采样器的一个
例子:
// 声明输入的纹理
texture skyTexture;
// 声明从skyTexture纹理中采样的采样器
sampler2D skySampler = sampler_state
{
Texture = skyTexture;
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
AddressU = Wrap;
AddressV = Wrap;
AddressW = Wrap;
}
texture表示从哪个纹理采样(通过xna程序来传递纹理),纹理是只能通过使用采样器来读取.MinFilter,MagFilter,MipFilter是过滤器状态,AddressU,AddressV,AddressW是寻址状态.
注意:directsdk的文档包含完整的hlsl说明.你可以访问这个网站: http://msdn2.microsoft.com/en-us/library/bb509638.aspx.
固定与可变的输入参数
Hlsl有2种输入数据类型:
固定的输入数据:表示在顶点/像素着色处理过程中都是不变的.如渲染一棵树,它的纹理,世界矩阵和光照条件是不变的.固定输入数据是从你的xna应用中传递进着色器.
可变的输入数据:表示每个着色器执行时数据会发生变化.如树的渲染过程中,顶点着色器需要处理树所有的顶点.这表示顶点着色器的每个循环使用的顶点都不一样.与固定输入不同,定义可变输入数据类型需要使用语义(semantic),下面讨论.
语义(semantic)
语义是hlsl预定义的,用于将变量名和输入输出数据进行匹配(opengl是指定变量存放的寄存器).如,你的3d物体可能有一个float4储存3d位置,另外的一个float4用来储存2个纹理坐标.但你的顶点着色器如何明白哪一个是表示位置?
解决的方案是在顶点处理阶段添加一个POSITION0语义,用来匹配一个可变数据的位置属性,如:
float4 vertexPosition : POSITION0;
在所有的可变输入数据(从应用中获取的或渲染阶段中所传递)中,语义是必须的.如,从顶点输出的将在像素着色器中使用的数据必须定义语义.在"创建一个简单的着色器"中将看到语义的定义.
语义不是大小写敏感并且需要在变量后面使用":"来指定.表9-2和9-3显示了一些语义.
表9-2.输入的顶点着色语义
输入 描述 类型
POSITION[N] 在物体坐标系的顶点位置 float4
COLOR[n] 反射及镜面高光颜色 float4
NORMAL[n] 法线向量 float4
TEXCOORD[N] 纹理坐标 float4
TANGENT[n] 切线向量 float4
BIN
ORAML[n] 次法线向量 float4
BLENDINDICES[N] 骨骼混合索引 int4
BLENDWEIGHT[n] 骨骼混合宽度 float4
表9-3 顶点输出语义
输出 说明 类型
POSITION[N] 同一坐标系顶点的位置 float4(x,y,z,w)
COLOR[N] 反射或镜面高光颜色 float4
TEXCOORD[n] 纹理坐标 float4
FOG 顶点的雾 float
使用顶点着色的输入语言来接收可变数据.一些通用的语义如POSITION,COLOR,NORMAL和TEXTURE.如果顶点有切线和次法线需要使用TANGENT和BINORMAL语义,当你需要在效果中完成凹凸贴图时.当顶点连接到骨骼时需要使用BLENDINDICES和BLENDWEIGHT语义.骨骼用于网格顶点的变形(12章详解).
顶点着色输出必须的一个语义是POSITION.如果你需要传递其余数据到像素着色,应该使用TEXCOORD[N].
[n]是一个可选的数字,用来定义使用的资源序号.如,如果一个模型有3个纹理,TEXCOORD[n]的n可以是0,1,2;因此,TEXCOORD0,TEXCOORD1,TEXCOORD2都是有效的顶点着色语义.表9-4显示一些像素着色语义.
表9-4 像素着色语义
输入 说明 类型
COLOR[N] 反射或镜面高光颜色 float4
TEXCOORD[N] 纹理坐标 float4
COLOR[N] 输出的颜色 float4
DEPTH[N] 输出的深度 float
因为顶点着色在光栅阶段执行完成后,可用的输入语义是像素的颜色和一些纹理坐标.纹理坐标地址,纹理的位置与当前的像素匹配,并且这些坐标从顶点着色传递到像素着色.
像素着色最终输出的数据是像素颜色和深度,像素颜色是必须的,像素深度是可选的.
函数
Hlsl的语法像c,每个函数有一个声明和主体.方法声明包含方法名和返回值,及一些参数.返回值可能指定了语义.
下面是用于像素着色的入口方法:
float4 simplePS(float4 inputColor : COLOR0) : COLOR0
{
return inputColor * 0.5f;
}
因为simplePS是像素着色器的入口,参数必须指定一个语义.本例中simpleDS方法将接收颜色参数减弱一半后作为最后的颜色.注意方法的参数可以有其他的修饰符,如in,out,inout.用来定义输入,输出,输入/输出参数.
在"技术technique,途径pass,和效果effect"一节中我们将演示用于顶点和像素着色的入口方法.
Hlsl内置了一小组内置的方法.包含数学操作,纹理访问,及流程控制.这些方法不需要与gpu指令匹配.实际上,表9-5列出常用的hlsl方法.
表 9-5.常用的hlsl方法
方法 说明
Dot 返回2个向量的点乘
Cross 返回2个float 3d向量的叉乘
Lerp 2个值间执行线性插值,
算法:(1-s)*x+s*y
Mul 执行矩阵x乘以矩阵y
Normalize 归一化指定的浮点向量
Pow 返回x的y次幂
Reflect 根据给定的入射射线方向和表面法线返回反射向量
Refract 根据给定的入射射线方向和表面法线返回折射向量
Saturate 将值固定在[0,1]之间
Tex2d 执行2d纹理寻址
Tex3d 执行3d纹理寻址
创建一个简单的着色器
本节中你将使用hlsl创建第一个着色器.作为一个好
习惯,你应该在开始部分定义固定和可变变量:
//从应用中接收的矩阵-固定
//(world * view *
projection)
float4x4 matWVP : WorldViewProjection;
// 用于顶点输入的结构 - 可变
struct vertexInput
{
float4 position : POSITION0;
};
// 从顶点着色传递到像素着色的结果 - 可变
struct vertexOutput
{
float4 hposition : POSITION;
float3 color : COLOR0;
};
着色器使用的matWVP矩阵需要在xna应用中进行设置.这个世界-视野-投影矩阵由xna应用的摄影机创建.当顶点着色将3d位置转换到2d屏幕上时需要用到.
定义的vertexInput结构用来定义顶点着色需要的信息.如你所见,顶点着色可以处理包含位置数据的顶点.
vertexOutput结构定义数据类型,从顶点着色传递到光栅进行线性插值,然后传递到像素着色.顶点着色将被强制生成位置和颜色.
很重要的一点是顶点着色输出的顶点位置在像素着色中无法访问.光栅阶段时需要知道2d屏幕位置,可以这样想:在光栅阶段时顶点3d位置被消化了,所以在像素着色时就无法访问了.如果你的像素着色需要2d屏幕坐标,你应该使用额外的TEXCOORD[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对象来接收并最后被处理进一个pixedlInput对象.在SimpleVS方法,需要通过乘以matWVP矩阵来计算输出的2d屏幕位置.输出的顶点颜色被设为黄色,rgb(1,1,0).
接着,定义像素着色:
// 像素着色代码
float4 SimplePS(pixelInput IN) : COLOR0
{
return float4(IN.color.rgb, 1.0f);
}
此像素着色只是简单的将从顶点着色获取到的颜色返回.这个颜色将被用于最终的像素颜色.
不错,你现在定义了一个顶点着色和像素着色.但你还没有分别指定那个顶点着色及像素着色在将三角形渲染到屏幕上时使用.为此你需要指定需要使用那个技术(technique).
技术technique,途径pass,效果effect
在最基础的形式中,技术只是负责组合顶点着色与像素着色.下面的技术组合上面写过的顶点和像素着色:
technique basicTechnique
{
pass p0
{
VertexShader = compile vs_2_0 SimpleVS();
PixelShader = compile ps_2_0 SimplePS();
}
}
技术也定义了哪个着色需要用那个着色版本来编译.本例中都是使用2.0的着色模型.更高的版本有更多的指令和功能但需要gpu硬件来支持.
如你所见,2个着色都被封装在称为途径(pass)中.一个途径读入所有应该在技术中绘制的顶点,处理它们,处理生成的像素,并将其储存进背面缓冲,当所有的像素被处理完毕后它们会被显示到屏幕上.一些技术中可能你需要重复此步骤2次.因此一些技术将有2个途径,每一个途径均有一个顶点着色和像素着色.
为了一些更高级的效果,你需要使用多个技术.使用第一个技术将场景转换进一个图形中,另一个技术来处理此图形.
所有的着色和技术的组合称为效果(effect).一个简单的效果将包含一个顶点着色,一个像素着色和一个技术.高级效果(如阴影贴图或延迟渲染)将包含多个.
效果和技术是不同的,并且着色器使着色程序更容易的在不同的技术中重用,也可以创建不同的技术来面向低端和高端的gpu.
你将经常把相同效果的着色和技术放到同一个文件中,xna将每一个hlsl代码称为一个效果(effect).这将允许xna将效果作为游戏素材来处理,就像模型和纹理.所有的效果通过xna的内容管道来处理,生成可操控对象在运行时由内容管理器载入.
Effect类
此时,你已经完成了第一个完整效果并储存在.fx文件.这表示你可以关闭.fx文件并将其移动进你的xna工程.下一步在你的xna工程中载入用来渲染物体到屏幕上.在xna程序中,效果应该被载入进一个Effect类的实例中(正如一个图形被载入为一个Texture2D类的实例一样).这个Effect类允许你配置效果的固定参数,选择当前效果技术,及用来渲染的效果.下面的代码显示了如何载入及配置一个效果:
//xna Effect 对象
Effect effect;
//载入效果
effect = content.load<Effect>("/effects/simpleEffect");
//设置 技术
effect.CurrentTechnique = effect.Technique["basicTechnique"];
//配置 固定效果参数
effect.Parameters["matWVP"].SetValue(worldViewProjectionMatrix);
这些代码使用content.Load方法从hlsl代码文件中载入simpleEffect效果.然后指定那个效果的技术将被使用;本例中使用的是之前定义的basicTechnique技术.最后,设定了唯一的固定参数:matWVP.
下面的代码显示了如何使用载入的效果画出物体:
//首先 开始 效果
effect.Begin();
//记住 效果可以有多个pass
foreach(EffectPass pass in effect.CurrentTechnique.Passes){
pass.Begin();
//放入绘制代码
pass.End();
}
//最后,结束效果
effect.End();
为了绘制一个3d对象,你首先需要
开启准备使用的效果,然后遍历当前技术的所有途径.每一个pass,都需要开启pass,绘制物体,结束pass.最后,还需要结束效果.当通过CurrentTechnique来访问技术时,效果途径由类EffectPass来表示.如果你需要在pass开始之后改变效果参数,就需要调用Effect类的CommitChanges方法来更新变化.
前面的步骤显示了绘制一个模型的必要代码.当你从硬盘载入一个3d模型时,模型和它的效果都储存在ModelMesh对象中.
Effect助手类
当一个效果通过内容管理器载入时,你不知道它有什么参数或技术.为了修改效果的参数,你必须先确定效果里有什么参数.可能你配置了一个像这样的lightPosition参数:
effect.Paramters["lightPosition"].SetValue(new Vector3(0.0f,40.0f,0.0f));
在此代码中,当你修改了lightPosition参数的值时,效果的内部将产生一个lightPosition参数的查询.这会有
2个问题:查询此参数的计算需求及查询一个无效参数的可能.使用助手类可以避免这些问题.
为了简化自定义效果的操作,你可以为每个效果创建一个助手类.每个助手类将储存一个所有效果参数的引用,避免查询的代价.下面的代码显示了如何为效果参数储存一个引用及改变值:
EffectParameter param1 = effect.Parameters["lightPosition"];
// 渲染循环
{
param1.SetValue(new Vector3(0.0f,40.0f,0.0f));
//绘制模型
... ...
}
材质Materials
材质是你应该创建并储存用于配置效果参数的类.例如,你可以使用一个效果来渲染2个面,每个面应用一个纹理.本例中每个面的材质是它的纹理,可以通过效果参数的配置来渲染表面.如果2个表面共享了相同的材质,你可以设置想要的效果和材质,在一个序列中渲染2个面来避免效果或效果参数的变化.
下面是你需要创建的2个基础的材质类:
1 LightMaterial : 将储存用于光线计算(反射颜色,高光颜色,高光强度)的表面属性.
2 TextureMaterial : 此类将储存一个纹理贴图或网格(tile)用于将纹理应用到表面上.
你可以使用这2个基础的材质类来创建更复杂材质类型,如多纹理材质.
下面是LightMaterail完整代码:
public class LightMaterial
{
// 材质属性 - 反射或 镜面高光的颜色
Vector3 diffuseColor;
Vector3 specularColor;
// 镜面高光强度 (亮度)
float specularPower;
// 属性
public Vector3 DiffuseColor
{
get { return diffuseColor; }
set { diffuseColor = value; }
}
public Vector3 SpecularColor
{
get { return specularColor; }
set { specularColor = value; }
}
public float SpecularPower
{
get { return specularPower; }
set { specularPower = value; }
}
public LightMaterial (Vector3 diffuseColor, Vector3 specularColor, float specularPower)
{
this.diffuseColor = diffuseColor;
this.specularColor = specularColor;
this.specularPower = specularPower;
}
}
将光线的反射和高光颜色以xna的Vector3分别储存在LightMaterail类的diffuseColor和specularColor属性中.将光线强度(亮度)以float值储存在specularPower中.注意(x,y,z)分量表示颜色的rgb格式.你还需要用属性来存取光线的反射颜色,高光颜色及高光强度.
下面是TextureMaterial类的完整代码:
public class TextureMaterial
{
// 纹理
Texture2D texture;
// 纹理 UV 块
Vector2 uvTile;
// 属性
public Texture2D Texture
{
get { return texture; }
set { texture = value; }
}
public Vector2 UVTile
{
get { return uvTile; }
set { uvTile = value; }
}
public TextureMaterial(Texture2D texture, Vector2 uvTile)
{
this.texture = texture;
this.uvTile = uvTile;
}
}
用xna的Texture2D类来将一个纹理储存在TextureMaterial类的texture属性.纹理的uv块用于凹凸贴图,以xna的Vector2储存在uvTile属性.像LightMaterial类一样,你还需要创建属性来存取材质和uv块.
着色创作工具
在着色的开发中,通常需要修改你的着色,调整它的参数,使用不同的素材(模型,纹理等)来做测试.此过程可能会很慢并如果每次修改着色你都需要重编译并执行你的游戏就会非常郁闷.为了帮助着色的开发你可以使用着色创作工具.
其中一个最好的可用工具是
NVIDIA的FX Composer,网站(http://developer.nvidia.com).FX Composer是一个
跨平台的ide.支持一些着色语言包含hlsl,及多种类型素材格式,如COLLADA,FBX,X,3DS,OBJ.使用FX Composer可以在创作和修改实时查看着色的效果.FX Composer还包含场景管理和着色效率分析.
总结
本章,你学习了渲染管道各阶段,及如何用它们将3d场景输出到2d图形.
你也学习了如何创建着色(gpu阶段的编程)和着色,技术和效果之间的关系.
最后,你学习了xna中如何载入,配置,及使用效果.内容管道处理过的效果,你可以简单载入并使用它们来渲染你的3d物体.
现在你需要
复习一些基础的3d着色和效果的概念,你可以开始绘制一些3d模型.下一章,将创建更复杂的效果来渲染3d模型.每一个效果,你将创建一个新的将用于创建材质库效果助手类