“材质(material)”,材质是一个很基本的术语,表示你的物体对光的反射方式。但是对它的实现并不像其解释这样简单,我们会用这一章节进行详细地介绍Ogre中相关的技术。
我们刚才提及过,材质定义了物体对光线反射的处理方法。这里暗示了材质的表现与光源的类型相关:聚光(Spotlights)、点光源(point lights)以及有向光(directional lights)对材质的表现有着完全不同的影响。简单来讲,它们都是对物理世界真实光源类型的模拟,所以材质对它们的反射也遵守着其在物理世界中的规则。
注意:虽然在Ogre里面,材质定义了物体如何反射光线的方法,但实际上并没有真的把光反射到附近的物体上。换句话说,场景中物体的辐射和反射属性并没有给场景增加任何光照。这是因为Ogre(以及目前绝大多数的即时渲染算法中)使用了局部辐射的算法来处理光照效果。
为了帮助大家更好的理解局部辐射,在这里讲解一下与其相对的全局辐射算法的概念。所谓全局辐射光照模型,指的是通过光线跟踪等技术,计算场景中所有包括反射、辐射等所有光线的照明结果的算法模型。虽然全局光照模型可以产生和真实照片媲美的渲染结果,但因为其算法相对比较消耗时间,所以目前仍然无法应用于即时渲染领域(即时运算的3D应用程序,至少要每秒产生30帧画面)。所以全局光照模型现在只活跃于用来产生照片质量动画的“离线”渲染工具中,比如Pixar’s RenderMan(http://www.pixar.com),Mental ray(http://www.mentalimages.com,包括3D Studio Max 等商业软件所使用的工具),POV-Ray(http://www.povray.org,一个开源的光线跟踪渲染工具)以及Aqsis(一个免费开源的符合RenderMan标准的离线渲染器)中。
材质的101[1]报告
Ogre是一个硬件加速的事实3D渲染引擎。换句话说,它尽可能的使用硬件(GPU)来处理更多的渲染过程。而对于硬件来说,不论多么复杂的材质,最终渲染到屏幕的结果只有一个类型,那就是颜色。而Ogre作为一个典型的渲染引擎,把物体上的颜色分解成四种不同的光照作用:环境反射(Ambient)、漫反射(Diffuse)、放射(Emissive)以及镜面反射(Specular)。
物体着色的基础
在前一个章节中,我们介绍了Ogre中光源的类型。而在Ogre所采用的局部辐射度模型的着色算法中,对物体颜色产生影响的除了光源本身的属性之外,还有观察的角度以及物体本身的颜色(在实际处理中等同于的是摄像机的角度和物体所用材质属性)。
下面列举了所有四种影响最终颜色的材质属性,以及它们的含义。
·环境反射:近似的模拟了场景中的全局辐射;也就是用来近似模拟所有光在场景中不断散射的结果。材质中有相应的属性来代表这种环境反射颜色。
·漫反射:这种颜色是接收到直接从光源发射的光之后产生的,“漫反射”这个词来源于现实世界,描述光被物体反射到各个方向的效果(换句话说就是散射)。
·放射:指的是自发光物体所拥有的颜色。这里有一个有趣的话题,因为在局部辐射光照模型中,放射光只能照亮自己却不能对周围任何物体产生影响,有时候这种效果却可以让人觉得极其虚幻(设想一下,在你的房间里有一个灼热的物体,却不会照亮周围任何东西,你就会了解这种感觉了)。
·镜面反射:描述了物体对被光照后的“高光”效果。设想一下假如你有一个红色皮球,而且擦得光亮。现在把它放在一个高瓦度的灯泡下面,你就会在上面看到一个被称为镜面高光的亮斑。这是因为光线被光滑的表面直接反射到你的眼睛中的缘故。
在这里镜面反射的高光的颜色是可以被用户定义的。这是因为在现实世界中,和环境光、漫反射以及辐射光不同,镜面反射高光的颜色除了材质自身和光的颜色之外也受到周围其他的一些因素的影响。所以这里允许通过手动调整,使用较低的代价来达到对真实世界的简单模拟。另外我们也可以手动调整镜面高光的反射能量,简单的说就是可以用参数来控制红色皮球上“光斑”的大小,同时调节物体发亮的程度:能量大的时候,光斑变得又小又亮。
图6-1:在3D Studio Max中对反射高光的设置对话框以及相应的材质结果
图6-1展示了在3D Studio Max软件中材质编辑中的镜面高光的属性调整对话框,同时也有调整之后的渲染结果。其中“高光级别”参数等同于Ogre中“specular power(镜面能量)”的概念。而另外的“光泽度”等同于Ogre中“shininess specular(镜面亮度)”参数。镜面能量决定能反射回来光的总量(表现为曲线的高度),另外的镜面亮度定义了高光如何被“展开”(越高的数值曲线越光滑)。图6-1(a)描述了一个胶皮球被一个光源照亮之后镜面高光的效果。图6-1(b)拥有一样镜面能量,但是调整镜面亮度到一个小一点的值,最后在图6-1(c)中和图6-1(a)同样镜面亮度的前提下,减少了镜面能量的数值。
这些基础的着色参数在Ogre中是总是可用的,不论你的硬件是否支持几何转换和光照处理(T&L),这些参数都是硬件加速所需要最基本的数据。如果在材质中提供了多种技术方案(Technique在我们后面的“技术和方案”章节中会具体介绍)以供选择的时候,至少对于其中使用固定功能着色(fixed-function shading)管线的来说这些参数仍然是必要的。
纹理贴图(Texture Mapping)
大部分的图形硬件都会至少支持一两个纹理处理单元(Texture units)。这些单元允许你的程序把纹理(Texture)通过一个独立的坐标系映射到一个物体上。这里的纹理不仅仅限于“2D图片”的概念,也可以有MIP-map 细节度的数据、立方体(Cube)纹理或者体积纹理(Volume)等等。在构造纹理的时候,可以使用的2D图片包括GIF,PNG或者网页上面使用的JPG图片格式,这些图片通常可以从屏幕截图或者数码相机以及拍照手机中得到。简单的说,使用了纹理贴图,就等于用一个2D图片包裹了3D物体,进而和物体表面的漫反射项进行混合(或者替代)来产生最终的颜色效果。
提示:纹理贴图的制造通常可以被分成两个步骤。首先是艺术部分:美工使用3D模型工具来调整的纹理坐标(一般被称为UV坐标系)到模型的顶点上,进而产生一系列UV坐标。然后是程序部分:这些UV坐标被传递到图形硬件上把模型顶点和纹理坐标关联起来。
前面提到的一些技术,除了图形硬件上的几何转换和光照处理(T&L),我们还提到了固定功能管道(fixed-function pipeline),这里面所谓的“固定(fixed)”的意思就是不可以编程的;你告知硬件物体的顶点和图片上像素的对应关系,然后就交给硬件就进行下面的工作:把物体的顶点信息映射到世界空间中去,然后结合纹理光栅化顶点的数据,最后绘制到屏幕上。
可编程着色技术(Programmable Shading)
可编程图形管线(programmable graphics pipeline)的出现,是到目前为止实时渲染3D图形加速技术中最大的飞跃。这意味着,简单的对每一顶点(顶点着色)和对每一像素(像素着色或者称为片断着色)的运算可以放到程序片段中来进行,这些程序既可以使用高级语言也可以通过GPU的汇编语言来实现,这样就能用我们所写的程序来取代之前固定功能管道所做的事情。注意,我在这里用到了“能取代”这个词,而不是“必须取代”。你仍然可以坚持使用固定功能管道,即便是在今天GEFORCE 7950 GTX2(在我写这文章时候市场上最强的图形卡)上面。如果决定了采用可编程着色语言,那么你就可以在渲染通路重处理任何顶点以及它们之间的片断数据。纹理在这个时候成为了程序的输入数据,需要说明一下,这里的输入数据可以是任何你需要的东西。因为如果你足够聪明,GPU对你而言只不过是一个高速的并行计算引擎而以,你甚至可以用它完成你任何希望的计算工作(比如人工智能的计算,在游戏编程精粹4中有相应的文章)。
Ogre支持所有风格的GPU着色语言,其中包括低级的汇编和诸如Cg,GLSL和HLSL这种高级语言。然而“支持”这个词同时意味着Ogre并没有为你做什么具体的事情;你仍然要靠自己双手去写这些着色程序,不过还好Ogre提供了一个灵活并且可变的框架来帮助你在GPU上进行着色工作。你会在之后的“材质和可编程管线”章节中了解这个框架的具体信息。
材质和程序设计
在上一个章中,我们介绍了Ogre对物体中的定义的实体和几何模型之间的关系。同时我们也知道需要通过材质来让它们看起来更像现实世界中的样子。在更进一步了解Ogre材质机理之前,我们还要了解一些更高级的编程概念。
批次(Batching)
Ogre最基本的渲染单元被称为可渲染对象(Renderable),它们通过不同的渲染状态被分类传递到渲染队列中去。相同渲染状态的可渲染对象只需要一次的绘图操作,也就是说它们在一个绘图批次中完成了渲染过程。当渲染到不同的材质或者模型的时候都会导致渲染状态的改变,而每次改变都会引起新的绘图操作(新的绘图批次)。绘图操作都是一个耗费时间的过程,如同画家在绘制新作品前要清洗调色板一样,图形硬件也需要进行顶点和纹理的更新操作。为了渲染效率的提高,程序设计者要尽量减少渲染状态的改变,换句话说也就是减少绘图批次的数量。
注意:通常而言,在你设定了渲染场景之后,所有的纹理都会被一次性的载入显存,除非你的纹理占用了过多的显存,否则不会在每一帧都传递纹理数据到显存中去。因此,你需要尽量的谨慎的处理纹理的使用:一旦确定纹理已经在显存,在它失效之前就不要再尝试去重载它。
Ogre引擎在力所能及的范围内尽量把相同渲染状态的物体一起渲染,进而减少渲染状态改变。然而即便如此,Ogre仍然尊重用户设置的Renderable整体(也就是说Renderable是处理批次的最小原子结构),并不进行拆分。举例来说,如果你有一个拥有20个使用相同材质的片段组成的模型,你应该把它们合并成一个Renderable对象而不分割成二十个不同的小对象批次:这样就可以省略掉系统检查这些片断是否是一个相同的渲染状态的步骤,从而可以一次处理完毕。在渲染的每一帧中,都会有一个明确的可以处理批次数量的上限,但是对这方面的讨论超出了本书所介绍的范围。如果你希望得到更多的信息,可以去查阅NVIDIA的工程师Matthias Wloka在CGD(游戏开发者大会)上发表的相关论文[1]。在这本书的后面,你会学到如何在代码中控制渲染状态的改变。为了达到以上的目的,你可能会需要使用3D模型工具来修改你所用的模型。
材质克隆
在Ogre中材质是被所有引用所共享的。当你从材质管理器重得到了一个指向材质的指针的时候,其他的使用相同材质对象也在处理一样的指针。这意味着如果你希望改变某个物体上面材质的任何属性的话,你都要把这个材质为它单独克隆一份,否则这个改变就会同时影响到所有使用相同材质的物体。在渲染通道中都需要再产生一个完全不同的材质。不过这里也有一种可以替代的方法存在,把材质作为颜色数据,通过改变可编程GPU着色的参数来实现不同材质的效果。这样就可以避免产生一个新的批次。
GPU着色
GPU(图形处理器)在设计之初就有着明确的目标,提供高性能的并行向量计算以供针对3D图形处理使用。而具体如何使用和怎么使用的权力都交给了用户。GPU的存在是为了减少CPU(中央处理器)的工作压力,使用者负责分配具体的计算工作给它们。需要指出的是,GPU在其专署领域的运算速率大大的高于CPU,而且似乎在未来一个时期内这个差距仍然会增加。因此,你需要有计划的开始在你设计中的管线加入可编程着色技术,尤其是当你将要计划使用实时阴影技术或者更高级的渲染到纹理(Render-to-Texture)技术的时候更需要这方面的支援。
技术(Technique) 和方案(Scheme)
可以说技术和方案是Ogre引擎材质中最强大和活跃的两个特性。在Ogre中,每个材质中都至少包含了一种“技术”实现,这种实现允许你对不同性能显示卡和硬件平台使用不同的材质属性组合。简而言之,技术就是“一种对物体的渲染方法”。通常来说对具体适用哪个渲染技术是由Ogre引擎自动甄选出来的(根据硬件性能、方案以及细节等级等信息),但是如果你希望的话也可以在代码中完全控制这个过程。
“方案”是Ogre使用的高级话题之一,事实上它是一个渲染技术集合的描述。举例来说,你可能有三个不同的技术方案:高质量,中等质量,低质量。在游戏运行的时候,允许用户通过选择这三个方案中的任意一个来确定在游戏中具体使用的渲染技术集合。
Ogre在渲染的时候,会有一个自动甄选所需渲染技术的固定流程:首先过滤掉那些不在当前方案中的所有技术(默认情况下当前方案是“Default”);然后选择适配当前细节等级(LoD)的那些;最后在剩下的当中挑选当前硬件环境中可以执行的最优技术(最好效果的)。当Ogre找不到任何一个可以使用的渲染技术时,就会把物体渲染成单调的白色表面。换句话说,如果你看到了一片雪白,就要检讨一下你对材质的配置了。另外在默认的情况下,材质中所有技术的细节等级(LoD)都被设置成为0,也就是最高的细节等级。换句话说,Ogre总是在尽可能的帮助你选择最优材质技术。
似乎技术和方案会带来很多复杂的处理细节。但在实际的执行过程中,你只要在材质脚本中提供了充足的内容,Ogre就会接替你来管理这些琐碎的细节。当然如果你喜欢,也可以用代码完成脚本所进行的工作。
材质细节等级(Material LoD)
计算机图形学中的细节等级(Level of detail)这个术语经常是用来描述几何体复杂度等级和摄像机距离的关系。同样的,这种描述也可以类似的用在材质上。例如说,你可以通过脚本把当前材质定义成多层纹理(用不同的方法来混合它们),并且同时拥有顶点和片断两种GPU着色程序。就算在一个很近的距离,也能在你的模型上面表现出“无与伦比”的漂亮材质细节。然而,当这个模型在屏幕上缩小到12像素的时候(就是说距离变远了),你认为这样做还值得么?
答案明显是不!解决这个问题的方法就是使用材质细节等级(LoD)管理。继续我们上面的例子,好地解决方案是利用Ogre可以让你在不同的细节等级中使用不同的技术实现,进而更有效的利用当前GPU的资源。为了实现这个效果,你需要首先定义各个细节等级所对应的实际距离,然后把相应的技术索引到这些等级中去。这里鼓励你在每个细节等级中尽可能多的设置不同的技术实现,以供给不同的方案以及硬件选择出最适用的实现。
材质的组成
在下图6-2中,展示了Ogre的材质之中各种组成成分之间的关系。一份完整的材质至少有一种技术实现,每种技术实现中至少要有一个渲染通路。在图示中,我们看到材质中包含了N种的技术实现,而在真正的渲染时,只会有一种技术被激活并进入渲染过程(选择激活技术的工作一般交给Ogre自动完成)。如果我们激活了技术0,那么技术0中所有通路都会被传到图形硬件中去依次渲染(换句话说,Ogre渲染了激活技术中的所有通路)。
图6-2:在Ogre中,材质,技术以及通路之间的关系
通路(Pass)
在Ogre中通路是最基本的渲染单位,同时也是可渲染对象(Renderable)用来标示自己渲染状态的基本单元。每个可渲染对象都会有自己的材质,Ogre在材质中甄选出最适合当前应用的技术实现。然后把当前技术中所有的“通路”依次放入图形硬件的渲染通路中。顾名思义,Ogre材质中的“通路”对应于图形硬件中“渲染通路”的概念。也就是说当前技术中如果包含了3个通路,那么在绘制是用这个材质的模型的时候,在每一帧就要进行3次渲染。
在实际的使用中,通路里面还有“纹理单元(texture unit)”的定义,你可以在一个通路中定义任意数量的纹理单元,当然一个不用也是没问题的。
纹理单元(Texture Unit)
在Ogre对材质的定义中,纹理单元的概念对应于图形硬件中的纹理采样(texture sampler)。为了运行Ogre程序,至少需要一个硬件纹理采样支持。不过这并不是什么大问题,因为现代的图形硬件基本上都会有多个纹理采样,因此我们可以在一次渲染通路的执行中,同时处理多个纹理单元。
顾名思义,纹理单元里都会包含一张纹理。你可以直接用硬盘中的图片文件,也可以通过实时的渲染来得到,甚至可以通过一个视频流来动态生成纹理图案。在Ogre中并没有对通路中纹理单元的数量进行限制,这是因为Ogre能根据图形硬件能力动态拆分通路(这里假设没有使用硬件着色程序)。具体点说,如果你的图形硬件只能同时处理4个纹理采样,但是应用程序却使用了一个6纹理单元的通路。这时候Ogre会自动的把这个6纹理通路拆分成两个分别两次进行渲染,不过虽然最后的渲染结果和预期的一样,但是仍然是通过两次渲染通路来实现的,对效率的影响不言自明。
最后需要注意的是,纹理在被真正的抛弃之前,都会一直存在显存中,而并不是每帧从内存传递到显存。但是如果纹理数量太多或者体积过大的时候,硬件无法同时处理所有纹理,效率会严重降低。
纹理压缩
很多现代图形硬件都会支持压缩纹理(比如DXTC方案),不过Ogre只是简单的载入纹理然后把它传到图形硬件上,并没有执行纹理的任何压缩过程。如果需要纹理压缩的支持,就需要离线进行图片的压缩工作(多数情况是把图片转换成压缩的DDS格式),然后把这些预压缩的纹理交给程序使用。如果你的硬件不支持压缩图片格式,Ogre会帮助你在程序运行的时候进行解压缩,然后再交给硬件使用。
视频流
虽然Ogre并没有在纹理单元中内置对视频流的支持,不过因为纹理单元可以支持外部数据源的处理,所以通过这个这个简单的机理可以帮助实现对视频流的支持。Ogre社区已经提供了一个针对Theora(http://www.theora.org)这个视频流处理器为基础的插件。通过这个插件你可以把任何你所喜欢的流加入到你的Ogre程序中,甚至可以直接从实况电视转播中得到视频源:任何你能想象得到的应用都能帮你实现。
实体(Entity)
实体是Ogre中比较神奇和复杂的概念之一。在每个实体中都包含着一些子实体(SubEntity)的实现,这些子实体是真正的可渲染对象,它们维护着具体的材质特性。而每个子实体又和一个子模型(SubMesh)对应(通常来源于3D模型工具建立的模型)。概括的来说,实体和子实体是物体渲染特性的入口,而模型(Mesh)与子模型是物体结构特性(几何体数据)的入口。
材质的例子
为了便于大家更好的理解,我们会通过讲解一些简单的例子来继续这个章节。这里大多是从Ogre的演示程序中找到的例子,不过也有我写的的几个材质脚本的最简单实现。
材质与固定功能管道(Fixed-Function Pipeline)
代码6-1展示了一个最简单的材质脚本,里面包含一个最简单的技术实现,技术实现里只有一个通路。如果你用它对你的物体进行渲染,会得到一个灰色的结果。
代码6-1:宇宙中最简单的Ogre材质
material VerySimple
{
technique
{
pass
{
diffuse 0.5 0.5 0.5
}
}
}
上面代码6-1中定义的材质,并没有什么特殊漂亮的地方,但是对于我们用来介绍Ogre材质脚本层次和结构来说已经足够了。通过它你可以直观地了解到脚本内部的嵌套关系:材质包含了技术,而技术包含了通路。通路中定义了漫反射参数。
现在让我们看一个更复杂一点的例子,在下面代码6-2中,我们在材质中建立了两个技术,而且提供了更多的固定功能着色(Fixed-function shading)的控制。
代码 6-2:对代码6-1的简单扩充
material NotQuiteAsSimple
{
technique
{
pass
{
diffuse 0.5 0.5 0.5
ambient 0.1 0.2 0.3
specular 0.8 0.8 0.8 68
texture_unit
{
texture ReallyCool.jpg
colour_op modulate
}
}
}
technique
{
pass
{
diffuse 0.5 0.5 0.5
}
}
}
代码6-2已经很接近在实际中使用的材质定义了。在其中第一个技术的通路里面使用了texture_unit标记来定义所使用的纹理贴图,这里假设有一张叫做ReallyCool.jpg的图片。在这个定义里面有趣的部分是定义如何把纹理的像素点混合到已经存在的颜色上:colour_op modulate这段脚本让Ogre把纹理上面的颜色数据和当前像素上的颜色数据(之前颜色混合的结果)相乘。在我们的例子中,所谓的当前颜色数据,其实指的就是程序中对局部光照模型混合的结果,具体点来说就是在这个相同的通路中对环境光,漫反射以及镜面反射采样的计算结果。
代码6-2也是我们第一次在材质脚本中加入一个用来“垫底”的技术实现。如果程序不幸的运行在一个不能支持纹理数据的硬件环境上的时候,第二个备用的技术实现就会被系统拿出来工作,把你的模型渲染成单一的灰色,虽然不怎么好看,但至少保证了程序安全运行。因为我们定义的两个技术都被系统默认的设置成相同的“方案”(默认的“Default”方案)以及相同的LoD(细节等级:0),所以的这个“垫底”才能良好的被运行。
注意:在你造成上面的问题之前,可能还有更严重的问题会出现。如果你的硬件真的很“老”,以至于无法支持纹理贴图属性,这时候甚至基本的3D API都不能运行(不论是DirectX还是OpenGL),进而导致Ogre不能启动。虽然Ogre是个强大的引擎,但是不能帮你升级硬件。
代码6-3是一个真正在Ogre例子里面使用的材质脚本——Example.material。你在Ogre目录下面的Samples/Media/materials/scripts 子目录下面可以找到这个文件。
代码6-3:一个定义纹理贴图的材质脚本
material Examples/EnvMappedRustySteel
{
technique
{
pass
{
texture_unit Diffuse
{
texture RustySteel.jpg
}
texture_unit Environment
{
texture spheremap.png
colour_op_ex add src_texture src_current
colour_op_multipass_fallback one one
env_map spherical
}
}
}
}
上面代码和之前最大的不同就是增加了材质的命名:Examples/EnvMappedRustySteel。虽然看起来似乎好像存在着一个目录结构,但事实上并不真的存在任何层级关系,这样命名只不过是一个为了更方便处理的习惯罢了:Ogre系统只是简单的认为,在material标签右面相同行的所有字母都是这个材质的名字。这个名字在整个程序中必是唯一的。因为在Ogre中对于材质的名称并没有一个类似“命名空间”的概念:就算在不同的资源载入的材质,它们的名字也是同时存在一个空间的。如果你真的希望可以用一样的名字命名不同的材质,就最好遵从我们上面提到的习惯,用类似路径名的方法命名材质,其中路径就可以替代命名空间的实现。
Ogre会给所有材质之中的元素(包括技术,通路以及纹理单元)提供一个默认的名字。还是用我们之前的脚本作为例子,在材质Examples/EnvMappedRustySteel中,里面的唯一的技术实现被命名为“0”,而通路因为一样的原因得到了“0”这个名字。两个纹理单元分别被命名为“0”和“1”。这个名称是和他在材质脚本中的顺序相匹配的。当你要通过“继承”的方式扩充脚本的时候(后面回讲到具体细节),有必要给这些元素手动命名,以便实现扩充功能的作用。相对于材质本身,Ogre并没有要求这些元素名称具有全局唯一性(当然,在它们自己的容器中,还是需要有不同的名字来辨别的)。
在代码6-3中,第二个纹理单元被指定成为一个球体环境贴图(通过env_map标记来实现)。环境贴图是一种用很低的代价实现的模拟反射表面的效果,其中并没有真的使用光线跟踪算法。再加上前面使用的“生锈的钢铁”纹理,最后混合上环境贴图的效果。最后能产生什么样子呢?那就要参考下面的几幅截图和贴图来了解了。
图6-3:RustySteel.jpg图片(生锈的钢铁表面)
图6-4:spheremap.png图片以及球体上的环境贴图
图6-5:Ogre中Demo_EnvMapping演示程序最终渲染结果
材质的继承(Material Inheritance)
Ogre提供了体面的纹理继承机制,可以帮助我们更简单的改变纹理的某些部分。比如在前面我们定义了材质脚本Examples/EnvMappedRustySteel,如果我们需要换成压缩纹理。这时候既不用在原脚本上修改,也不用重新写另外一个。通过材质脚本的继承机制,我们可从原有的脚本中派生出新的纹理出来。
不过在Ogre的代码和文档中把这种行为称为材质拷贝(material copying),这种称为揭示了“材质继承”与面向对象中的继承概念的区别。Ogre并没有对材质脚本实现面向对象的继承关系;也就是说在代码中Ogre系统名没有真正的使用原是材质进行继承,而只不过是简单的对原始脚本的属性进行了拷贝,其结果是:改变父脚本的属性不会影响子脚本。
虽然不怎么地道,但是确实是如继承一样方便的方法。你可以建立一个新的材质,把已经存在的材质作为基础,拷贝所有旧材质的属性到新材质里面,并对其中的属性做轻微的调整。这样做既可以减少工作量也可以让脚本看起来更清晰简单。
命名的重要性
虽然大多数情况可以依赖系统对材质中成员的默认名称。但如果你使用了继承,并且准备在新的材质中增加技术,通道或者新的纹理单元。这时候就要考虑手动对程序进行命名了,这是因为需要明确的区分父材质和子材质(也就是原材质脚本和目标材质脚本)中各元素的名称。如果你在之前的材质中使用了称为“Primary”的技术,在扩充的拷贝材质中就不能增加一个被称为“Primary”的技术,这是因为相同的名称会覆盖原材质的属性。
纹理覆盖(Texture Aliases)
在很多情况下,派生材质的目的可能只是需要改变已有脚本中的纹理图片。这时候可以通过简单的纹理覆盖机制来实现。下面的代码片断展示了如何通过纹理覆盖机制把父材质中的图片换成相应的压缩格式。
material Examples/EnvMappedCompressedRustySteel : Examples/EnvMappedRustySteel
{
set_texture_alias Diffuse rustySteel.dds
set_texture_alias Environment aphereMap.dds
}
上面代码实现了我们需要的功能,现在Examples/EnvMappedCompressedRustySteel的使用的纹理已经不同于Examples/EnvMappedRustySteel,其他属性仍然继承了下来。一目了然,这种方法比麻烦的拷贝粘贴方法要好上很多。
我们再来看一个稍微特别一点的材质实现,下面的材质脚本是Ogre演示程序中真实使用的例子。
代码6-4:在Sampls/Media/materials/scripts/Example.material中使用的材质纹理效果
material Examples/TextureEffect4
{
technique
{
pass
{
ambient 0.3 0.3 0.3
scene_blend colour_blend
cull_hardware none
cull_software none
texture_unit
{
texture Water02.jpg
scrool_anim 0.01 0.01
}
}
}
}
虽然图6-6所展示的水纹是有流动的,但是在书中的静态图片却看不出来实际效果。如果你运行了在程序中调用这个材质就能更好地看到这个效果了,但如果现在手边没有的话,那么就来想象一下纹理单元中的纹理卷轴的效果,这就是代码6-4所能实现的。
图6-6:在Demo_TextureFX演示程序中把当前纹理改成Examples/TextureEffect4之后的效果
在默认的情况下,Ogre会使用硬件拣选来过滤掉逆时针的表面(也就是说图形硬件只渲染那些边是面向摄像机顺时针排列的三角形)。而有一些场景管理器只会渲染那些法线面向摄像机的三角形。当我们希望平面的两个面在场景中都可见的时候,就需要关掉这两种拣选方式,通过脚本中所用的cull_hardware none和cull_software none两行来实现。
在图6-6中我们可以看到背景的蓝天白云透过半透明的材质被展现出来。这是材质和场景之中已经有的颜色进行混合的结果;因为纹理图片本身并没有透明效果(也就是没有自己的Alpha通道),所以使用了:scene_blend colour_blend来实现了简单的混合效果。如果使用了有Alpha通道的透明效果纹理,则可以使用:scene_blend alpha_blend来得到更好的效果。
材质和可编程渲染管线
在之前所提及的所有材质都使用了固定的渲染管线。而现在我们要接触更复杂一些的可编程渲染管线。虽然之前所有的脚本也都可以在可编程的管线中正常使用,但是当我们调用了着色程序之后,其中大部分的设置都会被忽略掉。
在进行Ogre材质的GPU程序设计的时候有三件事情需要关心:程序本身、程序声明以及在材质中的使用。在这里首先让我们看一个简单一些GPU程序产生材质的例子:一个在Ogre的某个Demo程序中使用的硬铁皮表面材质。
代码6-5:在文件Samples/Media/materials/programs/Example.cg中的Cg程序
/*
Single-weight-per-vertex hardware skinning, 2 lights
The trouble with vertex programs is they're not general purpose, but
fixed function hardware skinning is very poorly supported
*/
void hardwareSkinningOneWeight_vp(
float4 position : POSITION,
float3 normal : NORMAL,
float2 uv : TEXCOORD0,
float blendIdx : BLENDINDICES,
out float4 oPosition : POSITION,
out float2 oUv : TEXCOORD0,
out float4 colour : COLOR,
// Support up to 24 bones of float3x4
// vs_1_1 only supports 96 params so more than this is not feasible
uniform float3x4 worldMatrix3x4Array[24],
uniform float4x4 viewProjectionMatrix,
uniform float4 lightPos[2],
uniform float4 lightDiffuseColour[2],
uniform float4 ambient)
{
// transform by indexed matrix
float4 blendPos = float4(mul(worldMatrix3x4Array[blendIdx], position).xyz, 1.0);
// view / projection
oPosition = mul(viewProjectionMatrix, blendPos);
// transform normal
float3 norm = mul((float3x3)worldMatrix3x4Array[blendIdx], normal);
// Lighting - support point and directional
float3 lightDir0 = normalize(
lightPos[0].xyz - (blendPos.xyz * lightPos[0].w));
float3 lightDir1 = normalize(
lightPos[1].xyz - (blendPos.xyz * lightPos[1].w));
oUv = uv;
colour = ambient +
(saturate(dot(lightDir0, norm)) * lightDiffuseColour[0]) +
(saturate(dot(lightDir1, norm)) * lightDiffuseColour[1]);
}
就算你不了解6-5这个程序具体是什么意思也不要紧,现在你只要简单的注意一下用黑体字标明的函数名称就好了。另外,对于储存GPU程序的文件可以任意命名,如果你喜欢甚至可以改变扩展名。一会儿你就能了解到,这个文件的名字对于Ogre系统来说没有任何意义。
注意:在这本书里面并没有要讲解GPU程序设计的计划。这里所列出的所有GPU程序代码都只是为了作为实际的例子,如果你不理解这些代码的意思又对其有兴趣,就需要找一些资料来学习一下相关的知识了。
一旦你完成了上面的程序定义,你接下来需要做的工作是建立一个程序声明(declaration)。这种声名被用来给材质脚本中使用,下面代码6-6就是对上面程序的声明。
代码 6-6:在Samples/Media/materials/scripts/Examples.program中的程序声明
//Basic hardware skinning using one indexed weight per vertex
vertex_program Ogre/HardwareSkinningOneWeight cg
{
source Example_Basic.cg
entry_point hardwareSkinningOneWeight_vp
profiles vs_1_1 arbvp1
includes_skeletal_animation true
}
代 码6-6中列出了叫做Ogre/HardwareSkinningOneWeight的顶点程序声明。首先它声明了这个程序使用的是cg语言,进而告知 Ogre需要调用Cg运行时库通过来硬件渲染管线来处理程序。并且这个声明也告诉了Ogre从Example_Base.cg(我们之前定义的代码 6-5)中可以找到程序源代码。对于一个Cg程序来说,它需要一个程序入口点,这样Cg就可以从它来运行整个程序了(Cg是一种类似C的语言,它支持结构 化的代码设计)。我们通过entry_point标签来把它定义到hardwareSkinningOneWeight_vp函数上(在真实的代码中可能 同时有很多函数)。(另外:HLSL程序也需要声明一个入口点来进入程序,而汇编和GLSL语言就没有这个需要:其中GLSL使用main函数作为入口 点,汇编程序的运行只是简单的从顶部运行到底部。)
同时在代码中也提供了profiles(描述)属性,用来把这个程序 的profiles信息交给Ogre和Cg以便运行时使用。Ogre通过这个属性信息来做技术的匹配工作:如果特殊的硬件集合无法支持现在所提供的 profiles信息,Ogre就会再选择下一个要求较低的技术来匹配,直到找到能用的为止。Cg程序也使用这个profiles信息;汇编程序使用一个 类似的机制来处理,被称为syntax;DirectX HLSL使用target来定义GPU性能需求。这些不同的名词其实都讲的一样的东西,而且可以使用一样的值(举例来说,现在的设置对汇编程序的 syntax同样有效)。GLSL程序直接编译成本地GPU机器码,所以不需要这种参数。
最后,在这个例子中,声明了在 Ogre中硬件加速骨骼动画的支持,Ogre将要使用这个程序代替软件蒙皮通路来提供给蒙皮使用(我们将要在第九章了解到更多的关于Ogre动画的相关细 节)。只有当你确定需要这个功能的时候再打开它,否则就把这行从程序里面去掉。
代码6-7:就是使用上面提到的硬件蒙皮 程序的材质,你可以在文件中找到
material Examples/Robot
{
// Hardware skinning techniique
technique
{
pass
{
vertex_program_ref Ogre/HardwareSkinningOneWeight
{
param_named_auto worldMatrix3x4Array[0] world_matrix_array_3x4
param_named_auto viewProjectionMatrix viewproj_matrix
param_named_auto lightPos[0] light_position 0
param_named_auto lightPos[1] light_position 1
param_named_auto lightDiffuseColour[0] light_diffuse_colour 0
param_named_auto lightDiffuseColour[1] light_diffuse_colour 1
param_named_auto ambient ambient_light_colour
}
// alternate shadow caster program
shadow_caster_vertex_program_ref Ogre/HardwareSkinningOneWeightShadowCaster
{
param_named_auto worldMatrix3x4Array[0] world_matrix_array_3x4
param_named_auto viewProjectionMatrix viewproj_matrix
param_named_auto ambient ambient_light_colour
}
texture_unit
{
texture r2skin.jpg
}
}
}
// Software blending technique
technique
{
pass
{
texture_unit
{
texture r2skin.jpg
}
}
}
}
代码6-7 中主要介绍了GPU程序中常用的参数。整个材质脚本中包含了两个不同的技术实现,第一个就是硬件蒙皮用的技术;当硬件无法支持上面的例程的时候将会载入第 二个使用固定功能管线的技术,在这里Ogre把固定管线的软件蒙皮作为整个材质的备用“垫底”的技术实现。
注意:虽然两 种技术都把r2skin.jpg作为纹理使用。不过其中的区别是,当使用可编程渲染管线的技术实现,这个纹理可被GPU程序作为参数使用(通过 TEXCOORDO输入参数语义),而固定渲染管线忽略了这项功能。
而上面的材质中到底处理了哪些事情呢?在第 一个技术中的第一个通路里面,我们告诉了Ogre我们希望使用shado_caster_vertex_program_ref这个GPU顶点程序(我们 之前在程序中声明的,当你阅读完第九章节之后可以了解更多细节)。后面7行告诉Ogre自动分配五个“uniform”参数到顶点着色程序(基于程序参数 的语义,硬件来自动处理这些参数的输入和输出)。
GPU程序参数类型
为 了进一步了解Ogre材质,我们必须在这里介绍一些Ogre中GPU程序所使用的不同的参数类型,在GPU程序里面这些参数通常被称为 Constant(常量);这是因为从GPU程序的角度来看,这些参数对它们而言事实上就是起到了常量的作用。所以在Ogre程序中谈及GPU程序时候经 常会交替使用Parameter(参数)和Constant(常量)两个术语。
我们首先要知道GPU程序中分为两种不同 的参数类型,依据不同的索引方式,分别是位置索引(Indexed)和名称定位(Named)。而每种参数根据不同的设置方法又分为自动和手动两种。所以 就像我们之前脚本中所使用的一样,细分下来一共有四种不同的参数,它们分别是param_indexed(手动索引)、param_name(手动名 称)、param_indexed_auto(自动索引)以及param_named_auto(自动名称)。
在汇编图 形着色语言中,唯一能设置的参数种类就是位置索引,所以提到位置索引的参数设置方法十有八九也是在谈论汇编语言的程序。其实位置索引依赖于一个很简单的前 提:所有参数在硬件上的存储地址都是固定的,而所谓索引就是直接从硬件地址中找出相应的参数。在这里举个简单的例子,如果我们有一个以四个浮点数(16字 节)为最小储存单位的硬件,第一个参数的索引值默认是0,而第二个和后面的参数的索引值则和前面的参数长度有关,如果我们在所引为0的地方放了一个16字 节的float4类型(四个float)数据,那么下一个参数的索引就是1,我们在所引为0的地方放入一个64字节的matrix4x4类型数据(相当于 4个float4),那么下一个参数的索引值就是4。
图6-7:分别展示了在同一块储存区域中5块相同大小 数据和两块不同大小数据的储存方式和索引地址
相对于位置索引的参数,名称定位的设置方式要简单且清晰很多。只需要在材质 和GPU程序中使用相同的参数名称,Ogre就会帮你把它们关联起来。虽然方面,但也有其局限性,因为这些通过命名定位的参数只能在高级着色语言中使用, 即Cg,HLSL和GLSL。而在汇编程序中无法支持这种参数方式。
对于上面提到的两种参数,Ogre在传递值的时候又 会分为自动和手动两种处理方式。它们的区别在于Ogre是否自动为你寻找并匹配参数所对应的值。Ogre可以帮你处理大量不类型的参数,并把它们的“自动 值”传递到图形硬件中的着色程序中。如果你希望了解所Ogre所提供所有“自动值”的类型,请参考本书附录B的列表。
不 过仍然后很多时候需要你通过手动的方式向着色程序传递参数,对于名称定位的参数类型(param_named),可以通过 GpuProgramParameters::setConstant()方法来传递参数。而对于位置索引的类型(param_indexed),则可以 通过GpuProgramParameters::setNamedConstant()方法,例如:
GpuProgramParametersSharedPtr params = entity->getSubEntity(0)->getMaterial()->
getTechnique(0)->getPass(0)->getVertexProgram().createParameters();
params->setNamedConstant(“ambient”, ColourValue(0.5, 0.5 0.5 1.0));
虽然上面只写了顶点着色程序的方法,不过对 于片断着色程序(也有人称之为像素着色程序)而言,也有类似的方法和机制帮助你完成参数的传值过程。
定制的自动参数
通 过Renderable(可渲染对象)的接口,你可以实现把CPU应程序中的参数变量,绑定到相应的GPU程序参数上面;这么做的好处就是当这些参数的值 改变的时候,不需要你手动通过setNameConstant()方法去通知GPU程序,取而代之的是这个过程将交给程序自动完成。为了实现这种神奇的功 能,首先要在材质脚本中进行相应的配置(参照附录B),然后在程序中通过setCustomParameter()这个Renderable类型的方法来 把参数绑定到具体变量。
举例来说,假如我们要在之前的代码6-7中绑定一个程序中的变量到CPU程序中投影算法的环境光 (ambient)参数上。我们首先需要做的是在材质脚本中把自动参数的指定从ambient_light_colour改成custom(下面的黑体 字)。
//改变投影参数
shadow_caster_vertex_program_ref Ogre/HardwareSkinningOneWeightShadowCaster
{
param_named_auto worldMatrix3x4Array[0] world_matrix_array_3x4
param_named_auto viewProjectionMatrix viewproj_matrix
param_named_auto ambient custom 12
}
后 面额外的参数(“12”)是用来传递给Ogre程序来辨认到底是绑定到哪个参数上面。被绑定到GPU程序中的变量类型只能是4-vector(四元向量) 类型(换句话说,就是Ogre中的Vector类),但是从CPU到GPU传递数据具体的含义可以任你确定,比如在我们现在的例子中,我们认为这个定制的 参数的意义是RGBA各式的颜色值。
Vector4 myColor;
entity->getSubEntity(0)->setCustomParameter(12, myColor);
现在,当GPU程序开始执行渲染到这个通路的时候,Ogre会自动的提供当前的 myColor变量的值作为参数。而我们在脚本中使用的索引(“12”)的唯一目的就是为了查询绑定参数,需要注意的是,这个索引值不可以为0。
比 较复杂的例子:视差帖图
我将要通过一个高级一点的例子作为这个章节的结尾,整个程序中既包含顶点程序也包含片断程序。视差帖图 (Offset Mapping)是一种用来在不影响模型结构的情况下,在3D空间中加入贴图表面视觉凹凸度的方法。图片6-8以及图片6-9展示了实际的效果。注意,这 些在纹理表面上增加的深度效果并不会受到观察角度的影响而产生变化。
图6-8:视差贴图在Demo_Dot3Bump演示 中的效果
图6-9:从另外的角度观察视差贴图
代 码6-8提供了Demo_Dot3Bump中使用的视差贴图例子的材质脚本。
代码6-8:在Samples/Media /materials/scripts/OffsetMapping.material中的GPU程序声明
// Bump map with Parallax offset vertex program, support for this is required
vertex_program Examples/OffsetMappingVP cg
{
source OffsetMapping.cg
entry_point main_vp
profiles vs_1_1 arbvp1
}
// Bump map with parallax fragment program
fragment_program Examples/OffsetMappingFP cg
{
source OffsetMapping.cg
entry_point main_fp
profiles ps_2_0 arbfp1
}
// Bump map with parallax fragment program
fragment_program Examples/OffsetMappingPS asm
{
source OffsetMapping_specular.asm
// sorry, only for ps_1_4 and above:)
syntax ps_1_4
}
material Examples/OffsetMapping/Specular
{
// This is the preferred technique which uses both vertex and
// fragment programs, supports coloured lights
technique
{
// do the lighting and bump mapping with parallax pass
pass
{
// Vertex program reference
vertex_program_ref Examples/OffsetMappingVP
{
param_named_auto lightPosition light_position_object_space 0
param_named_auto eyePosition camera_position_object_space
param_named_auto worldViewProj worldviewproj_matrix
}
// Fragment program
fragment_program_ref Examples/OffsetMappingFP
{
param_named_auto lightDiffuse light_diffuse_colour 0
param_named_auto lightSpecular light_specular_colour 0
// Parallax Height scale and bias
param_named scaleBias float4 0.04 -0.02 1 0
}
// Normal + height(alpha) map
texture_unit
{
texture rockwall_NH.tga
tex_coord_set 0
}
// Base diffuse texture map
texture_unit
{
texture rockwall.tga
tex_coord_set 1
}
}
}
// This is the preferred technique which uses both vertex and
// fragment programs, supports coloured lights
technique
{
// do the lighting and bump mapping with parallax pass
pass
{
// Vertex program reference
vertex_program_ref Examples/OffsetMappingVP
{
param_named_auto lightPosition light_position_object_space 0
param_named_auto eyePosition camera_position_object_space
param_named_auto worldViewProj worldviewproj_matrix
}
// Fragment program
fragment_program_ref Examples/OffsetMappingPS
{
param_indexed_auto 0 light_diffuse_colour 0
param_indexed_auto 1 light_specular_colour 0
// Parallax Height scale and bias
param_indexed 2 float4 0.04 -0.02 1 0
}
// Normal + height(alpha) map
texture_unit
{
texture rockwall_NH.tga
tex_coord_set 0
}
// Base diffuse texture map
texture_unit
{
texture rockwall.tga
tex_coord_set 1
}
}
}
// Simple no-shader fallback
technique
{
pass
{
// Base diffuse texture map
texture_unit
{
texture rockwall.tga
}
}
}
}
在 上面代码中不仅有材质脚本本身,同时提供了被材质脚本所引用的所有着色程序的声明。其中包含了一个顶点着色程序声明 (Examples/PffsetMappingVP)和两个片断着色程序声明(Examples/OffsetMappingFP和Examples /OffsetMappingPS)。在这里需要注意一点,我们在同一个材质脚本中使用了两个不同的片断着色程序声明,这是为了适配不同性能的图形硬件。 对于高于2.0顶点着色版本的图形硬件可以很顺利的执行Examples/OffsetMappingFP这个片断着色程序。而对于低于这个版本且高于 1.4顶点渲染版本的图形硬件只能是用Examples/OffsetMappingPS来实现类似的效果。这两个顶点程序被不同的技术所引用,在具体的 执行中Ogre会帮助我们根据硬件的能力作出选择。在下面的代码6-9种我们可以看到这些程序的Cg语言版本。而对于 Examples/OffsetMappingPS程序,你可以在6-10里面看到它的汇编代码。
代码 6-9:Cg语言实现的顶点和片断着色程序,可以在Samples/Media/materials/programs /OffsetMapping.cg文件中找到相应的代码
/* Bump mapping with Parallax offset vertex program
In this program, we want to calculate the tangent space light end eye vectors
which will get passed to the fragment program to produce the per-pixel bump map
with parallax offset effect.
*/
/* Vertex program that moves light and eye vectors into texture tangent space at vertex */
void main_vp(float4 position : POSITION,
float3 normal : NORMAL,
float2 uv : TEXCOORD0,
float3 tangent : TEXCOORD1,
// outputs
out float4 oPosition : POSITION,
out float2 oUv : TEXCOORD0,
out float3 oLightDir : TEXCOORD1, // tangent space
out float3 oEyeDir : TEXCOORD2, // tangent space
out float3 oHalfAngle : TEXCOORD3, //
// parameters
uniform float4 lightPosition, // object space
uniform float3 eyePosition, // object space
uniform float4x4 worldViewProj)
{
// calculate output position
oPosition = mul(worldViewProj, position);
// pass the main uvs straight through unchanged
oUv = uv;
// calculate tangent space light vector
// Get object space light direction
float3 lightDir = normalize(lightPosition.xyz - (position * lightPosition.w));
float3 eyeDir = eyePosition - position.xyz;
// Calculate the binormal (NB we assume both normal and tangent are
// already normalised)
// NB looks like nvidia cross params are BACKWARDS to what you'd expect
// this equates to NxT, not TxN
float3 binormal = cross(tangent, normal);
// Form a rotation matrix out of the vectors
float3x3 rotation = float3x3(tangent, binormal, normal);
// Transform the light vector according to this matrix
lightDir = normalize(mul(rotation, lightDir));
eyeDir = normalize(mul(rotation, eyeDir));
oLightDir = lightDir;
oEyeDir = eyeDir;
oHalfAngle = normalize(eyeDir + lightDir);
}
// General functions
// Expand a range-compressed vector
float3 expand(float3 v)
{
return (v - 0.5) * 2;
}
void main_fp(float2 uv : TEXCOORD0,
float3 lightDir : TEXCOORD1,
float3 eyeDir : TEXCOORD2,
float3 halfAngle : TEXCOORD3,
uniform float3 lightDiffuse,
uniform float3 lightSpecular,
uniform float4 scaleBias,
uniform sampler2D normalHeightMap,
uniform sampler2D diffuseMap,
out float4 oColor : COLOR)
{
// get the height using the tex coords
float height = tex2D(normalHeightMap, uv).a;
// scale and bias factors
float scale = scaleBias.x;
float bias = scaleBias.y;
// calculate displacement
float displacement = (height * scale) + bias;
float3 uv2 = float3(uv, 1);
// calculate the new tex coord to use for normal and diffuse
float2 newTexCoord = ((eyeDir * displacement) + uv2).xy;
// get the new normal and diffuse values
float3 normal = expand(tex2D(normalHeightMap, newTexCoord).xyz);
float3 diffuse = tex2D(diffuseMap, newTexCoord).xyz;
float3 specular = pow(saturate(dot(normal, halfAngle)), 32) * lightSpecular;
float3 col = diffuse * saturate(dot(normal, lightDir)) * lightDiffuse + specular;
oColor = float4(col, 1);
}
代 码6-10:为了配合低等级图形硬件而实现的汇编片断着色语言,可以在Samples/Media/materials/programs /OffsetMapping_specular.asm文件中找到相应代码
// Pixel Shader for doing bump mapping with parallax plus diffuse and specular lighting by nfz
// uv TEXCOORD0
// lightDir TEXCOORD1
// eyeDir TEXCOORD2
// half TEXCOORD3
// lightDiffuse c0
// lightSpecular c1
// Parallax scale and bias c2
// normal/height map texunit 0 - height map in alpha channel
// diffuse texture texunit 1
ps.1.4
texld r0, t0 // get height
texcrd r2.xyz, t0 // get uv coordinates
texcrd r3.xyz, t2 // get eyedir vector
mad r0.xyz, r0.a, c2.x, c2.y // displacement = height * scale + bias
mad r2.xyz, r3, r0, r2 // newtexcoord = eyedir * displacement + uv
phase
texld r0, r2.xyz // get normal N using newtexcoord
texld r1, r2.xyz // get diffuse texture using newtexcoord
texcrd r4.xyz, t1 // get lightdir vector
texcrd r5.xyz, t3 // get half angle vector
dp3_sat r5.rgb, r0_bx2, r5 // N dot H - spec calc
dp3_sat r4.rgb, r0_bx2, r4 // N dot L - diffuse calc
+ mul r5.a, r5.r, r5.r
mul r0.rgb, r4, r1 // colour = diffusetex * N dot L
+ mul r5.a, r5.a, r5.a
mul r5.rgb, r5.a, r5.a
mul r5.rgb, r5, r5
mul r5.rgb, r5, r5
mul r5.rgb, r5, c1 // specular = (N dot H)^32 * specularlight
mad r0.rgb, r0, c0, r5 // colour = diffusetex * (N dot L)* diffuselight + specular
+ mov r0.a, c2.b
这 里的可编程通路中只支持了一个光源的处理(通常指的事场景中第一个光源)。顶点程序支持三个自动提交参数:光源位置,眼睛位置,和世界-视点-投影变换矩 阵(这三个参数都传给了main_vp函数)。切线数据被作为顶点的数据元素(被编码成3D 纹理的UVW格式),与顶点位置、法线以及标准的UV贴图坐标被作为前四个参数传递给顶点程序。切线数据通常在离线产生的文件中得到,比如通过导出的模型 文件或者转换之后的XML文件;你也可以通过调用Mesh::buildTangentVectors()在程序中动态生成这些数据(在自动生成模型的算 法中经常用到)。main_vp函数提供相应的返回值会作为片断程序的输入参数提供,其中包括顶点坐标和四个贴图坐标(包括贴图UV坐标,灯光数据,眼睛 数据以及切线空间的半三维向量(half 3-vectors))。
Ogre自动提供给片断程序的参数包括环境光和镜面 光的颜色,并且你的应用程序可以把高光信息和偏移数据放在一个四元向量中传递给片断程序(如果你没有传入,将使用默认数据)。每个片段的实例还能得到之前 在顶点程序中输出的数据作为额外参数(这些参数通过之前的main_vp输出得到)。最终从判断程序中输出的结果就是最后计算出来的像素颜色,并把这个结 果储存在相应的帧缓存中。
而其中的汇编和Cg的片断程序执行了相同的工作;这么做的原因是在把纹理坐标通过另外一张纹理 调整的时候,早期版本的像素着色技术不能支使用持附属纹理读取。所以我们需要在PS1.4版本使用汇编程序来代替Cg。这样做的好处是可以让程序在支持 DirectX8.1级别的硬件上运行(比如Radeon 8500等)。
注意:你可能注意到了通过纹理传递任意数据类 型到片断程序。这种使用方法并不只是Ogre所专有的;对于GPU编程来说这是一个很常用的储存传递匿名数据的方法(不论是从CPU转递数据到顶点程序还 是从顶点程序传递到片断程序,或者是我们这样从CPU传递到片断程序)。
结语
Ogre的材质系统是非常灵活的,会有很多 种不同的方法来管理这些在你的场景中渲染属性。这一章节的目标就是让你熟悉Ogre如何管理自己的材质,以及在你的程序中如何创建并管理者这些。不过这仍 然仍无法在这一个章节中把所有Ogre材质的属性和指令的细节介绍清楚。虽然这是一项很费劲并让人厌烦的工作,但是这本书仍然在附录B中提供了材质脚本中 所有的指令和参数的列表。你可以用这章节学过的知识来尝试构建你需要的材质效果,当你需要知道所有材质脚本的属性和指令的时候,附录B就是你最好的快速参 考手册。
在下一章节中,你会学到关于Ogre的一个子系统,一个可以用来帮助你的程序来找到可以使用的材质脚本的系统:Ogre 的资源管理系统。
来源: 中国网游开发中心 编辑: 张喆