|
第三部分、UV仿射贴图、材质混合、文件操作
http://bbs.gameres.com/showthread.asp?threadid=94927
第四部分解决方案
http://pickup.mofile.com/7747729412122892
第四部分、实时渲染技术
该主题将包含以下内容:标准视景体的构造、1/W线性插值、AABB包围盒、碰撞检测、Alpha通道、雾化、基础物理模型,以及一个用于演示以上内容的例子。
我阅读了各部分当前已经积累的一些概念性东西,并着手完成了所有细节工作。其中Alpha通道、全局顶点雾、基础物理模型是在考察了相关需求之后,完成从最初的构思到最终呈现,并且尽量提供较高的性能,以期取得良好的结果。Alpha通道和顶点雾都使用了256分辨率的Alpha深度值,这个约定使得Alpha运算过程能够获得最高效的实时性能。否则,实现顶点雾的效果将变得不可行,这样从图像呈现的角度,游戏的可玩性将大打折扣。


图4-1、第二个场景不采用全局顶点雾,场景突然中止在远裁剪面
最近我在思索很多的主题,其一、对多边形的利用,对贴图和效果的呈现,应该要简化到什么程度,才能体现出足够宏大的场景,比如说,也许多边形数可能不多,但是却占据了广大的空间,或者物体很小,但多边形数很多,应该是用多少多边形才足够并且不会影响速度。其二、或者立刻停止软件渲染的研究工作,使用硬件来加速。计算机的内核处理速度将会越来越快,软件渲染将会在未来被重新考虑并工作得越来越好,并且有利于表达出一些奇异的物体组织模式和渲染效果,而不必借助于硬件的升级。通常,硬件将只满足大多数的工业需求。其三、底层的数学演算和基于充满激情的情感逻辑推演之间应该架起怎样的一座桥梁。比如,如果利用可行的一套数学模型描述关于什么是幽默和悲伤的感受。我知道它们属于不同的概念层次。但是为什么不能说:5是个很有趣的家伙。我是说,在游戏开发过程当中,什么将占据主导作用?真实光照模型的数学公式或者是“把颜色的数值降下来,它们太远了”这样的描述情节,碰撞物相互之间的反馈力到真实物理世界的等比例建模或者“碰上了,让物体退回5个像素吧”。我们旨在建立一个有趣的,带有幽默感的可预期的假想世界还是建立一个现实生活空间的缩影?最终,我通过玩大量的游戏时发现,细节的感动通常更能让玩家沉迷。比如《格兰蒂亚》里面的小型物体因为玩家的碰撞而摆动,但是小型物体只是播放了摆动的动画和声音,而没有真正和玩家的身体各部件建立碰撞关系。而在射击游戏中(第一人称或者第三人称),简单而高效的操作按钮将使玩家立刻判断出它是否值得玩,而不是给更多的自由度,这种毫无节制地膨胀的技术把戏。
上述的论调看似我一直在力求逃避严谨的数学演算的问题,有一点。在我看来,数学演算系统在能够用5到10个公式计算出“幽默指数”之前,我大量地使用了情感机制。
一、标准视景体的建立
很多的书籍试图将我们将要获得的视景体空间描述成一个立方体,这实际上是不可行的,甚至到多边形描绘阶段,如果这个多边形附带有贴图坐标,也是未经透视的。
标准视景体依然是个锥形体,只不过它在Z方向上的每个截面都同相应的W值对应了起来。W=z/d,有时候我使用w作为“未经变化的”z值使用,计算顶点雾的时候我就是通过这个数值乘于d来和全局雾区间进行比较的。
另外一个需要关注的问题是d的取值,大量的研究总是假定d的取值始终为1,理由有二:其一、w=z/d=z,这样w和x、y、z的分量的比较工作将更为直接,当前视景体视场总是以90度为基准,这使得通过缩放到基准的最大x、y值和xy截面的z值相等

尽管如此,我依然使用了可变的d值,因为它使得代码在逻辑上讲是准确的,这样在所有的向量分量都要采用d作为分母。

上述式子我透露了一个信息,z方向的xy截面区域将和相应的w取值进行比较,以确定是否位于视景体内。Xmax应该是x可能的最大取值。

图4-2、不同的xy截面,所确定的x或者y方向的视景体区间是不同的
我曾经对于标准化视景体是否必要表示怀疑。直到我将它用于物体包围盒地剔除工作的时候才打消了这个念头。基本上,它将视景体的一切信息都存放在了W分量上了。其它分量为了和W对齐都做了一定的缩放工作。

Zoomy和Zoomx都可以理解,关键在于Zoomz,每个截面的w取值都是不等的。但是以下事实可以作为比较工具:

图4-3、关于变换后的z和w比较的问题
如图,当per=0时,w和近裁剪面一致(不考虑d的影响),而z值为0,当per=1.0时,w和远裁剪面一致,相应的z值为w,当per落在0到1.0的内区间时,
z-w=nc*per-nc=nc*(per-1)<0,即z<0。而当nc=0且z=0时w=z,或者z=fc时,per=1.0,w=z。
这是一个有趣的现象,尽管标准化视景体没有给出z和w的直接大小对应关系,但是它在客观上维护了这种大小对应关系。正确理解它在更深入的使用中将会帮助很大。
没有一本书籍写出比以上验证过程更接近真相的描述了,最后给出了期望的视景体变换矩阵:

二、透视纹理修正
当我完成了对纹理进行透视修正的代码工作之后,我再也不想看到以前那样子的纹理映射效果了,我竟然忍受了它们数天的时间。并且我觉得如果有人重新发明纹理映射算法的话,它首先考虑的是经过透视变换的纹理映射坐标而不是直接对它进行图像空间插值。只是我对Gouraud着色概念的“剽窃”影响了个人的思维。

图4-4、对透视投影进行线性插值造成空间扭曲,两个面产生交叠

图4-5、z缓存使用线性插值得到的深度信息,必然造成部分平行面交叠

图4-6、通过正确的深度信息获取,该问题被解决(使用1/w作为插值对象,后面提到)
因为纹理贴图是定义在视景体空间的,而x、y方向的值都被透视变换了,因此只需将纹理贴图进行同样的透视变换,就能够得到纹理贴图坐标在图像空间的线性插值坐标,并回乘关联的透视因子w就能够得到实际采样的纹理坐标。问题是,怎么得到关联的w的数值。这利用了1/w是关于图像空间线性变化的事实。现在变成了验证1/w在图像空间是否线性变化的问题了。
如前面的所有难题一样,我首先采用了数学的办法对其进行证明,并把式子写得越来越复杂,但这并不奏效。后来我经过反复论证,使用了一种极简的办法,即认为1/w是关于空间直线y=1在图像空间的投影,论证如下:


通过以上事实,就获得了投影到图像空间的纹理贴图坐标的线性插值过程,以及对应的1/w线性插值过程,该坐标除于1/w就得到了视景体空间的坐标。
唯一值得注意的问题是,检验1/w是否落在近裁剪面以外的区间。这需要和d/nearclip进行比较,当d=1时,nearclip小于1将导致1/w可行的区间大于1,这本来不是什么问题,但是实践中我们通常设置了1/w深度缓冲的偏移量为1,这样前一个循环节的深度缓冲信息可能无法使当前循环节正常通过检测。这个时候只需nearclip设置不小于1的情况。假如d=100或者更大的数值,为了保持深度缓冲区间为0到1的特性,nearclip将需要大于100!这是不可接受的,因此固然引入d值在逻辑上有效,但是基于这个原因,不应当更改这个数值。或者使用更小的取值,如0.5,这样nearclip就可以推移到0.5,但是有谁会关心这半个单位量的距离呢?

图4-7、因为过大的d(这里是100)值导致后续桢的一部分像素无法通过检测
三、经过改良的流水线,对象表映射和Alpha通道
随着对物体个数的量的需求的增加,对象拷贝传入流水线开始产生瓶颈,因为必然有大部分对象因为剔除的关系而完全不会被访问到细节信息。之前总是假定渲染对象都是在视景体内的,但是现在的引擎引入了包围盒层次,就不应该再这样做。况且,以后还要使用到多次渲染呢(在一个桢缓存里面渲染多个子画面或者多个层次画面,用于不同视景区间的物体呈现,这广泛用于图形界面和其他类似应用)。
但是什么属性需要放在对象表映射结构上呢?直接的回答就是:在流水线深入对象细节之前的一切必须信息。如包围盒区间、位置、方位等,在我试图把它们全部枚举出来之前赶快来看一下这个动人的结构:
typedef struct
{
OBJ* obj;
EULER E;
VECTOR3 pos;
VECTOR3 move;
MATRIX m;
COLLIDEBOX collidebox;
COLLIDEBOX aabb;
int models_size;
int randid;
float fallspeed;
float speed;
bool landing;
}REFOBJ;
有一个move向量,这用于简单的物理系统,当判断动作不可行或者需要额外动作(物体下落、被迫回退等)时进行修正,最后附加在pos上,这时候物体才真正做出反应。Collidebox存放对象模型的碰撞盒,这是读入模型之后就定义好的了,aabb在游戏逻辑需要的情况下,将重新计算当前旋转量下的轴对齐包围盒(Axis Align Bound Box)。为了避免过度的运算开支,我使用了变换后的Collidebox顶点计算一个AABB而不是针对所有顶点,这在控制对象是人物并只做水平方向的旋转的情况底下很可靠。毕竟很少有人为一个横着放的长杆子着手编写游戏。如果它不是人物或者拟人的什么其他东西的话,则可以在游戏逻辑上建议实时转换顶点的aabb,为了性能考虑,可以加入一个忙碌拒绝策略,即每个循环将只处理部分物体的aabb转换。旋转量过大的物体获得最高的优先权。而至于场景对象的包围盒,为了开启动态的地形跟踪算法,包围盒将进行细分,在加上场景对象绝大多数情况下不做旋转的动作,这样通过初始的Collidebox顶点转换的AABB就可以直接拿来用了。Randid标识符用于碰撞排除,即物体A在遍历所有可能碰撞对象的时候将不考虑自身。Fallspeed是用在物理模型的下落速度。Speed是物体的运动速度,landing标记物体是否处于着陆状态,若是则拒绝为Fallspeed添加重力加速度并使其为0。
当前我使用STL的vector用于仿造数组编程,我可不想在指针上面懂什么脑筋,而且该映射结构已经很简短了,无需考虑将对象映射结构的指针放入流水线渲染表里。不过其映射的对象则使用了指针,当写到这里的时候我想到我完成了一个模型统一读取模块,该模块将对各个放置到对象表的对象进行命名,并利用map进行键值映射,但是因为这个命名可以由用户编程界面定义,可能存在的重名问题将导致不可知的结果。
经过修改的流水线体:
typedef struct
{
map<string,OBJ> objects;
vector<REFOBJ> renders;
vector<BITMAPFILE> bitmaps;
TRIANGLE alphachannel[30000];
}PIPELINE;
其中包含了对象表,渲染表,位图表和Alpha通道。对象表和位图表在一个合适的关卡中应当只加载一次,渲染表将每桢清空然后投入新的对象映射。Alpha通道存放了所有透明多边形,避免和普通多边形混杂在一起,把普通多边形的渲染拒绝掉,采用这种策略的一个显而易见的用场是,绘制完所有的普通多边形,排序Alpha通道的多边形,最后再绘制透明多边形。如果一个透明多边形可见,那么它要么直接在所有普通多边形的前面,要么可能被其它透明多边形遮挡,对于前者,这没有问题,所有的多边形都被绘制完毕了,而对于后者,这应该也不是问题,因为已经对Alpha通道执行排序了。当然这种多边形排序内在的交叠情况导致排序很难完全正确完成,除非执行的是像素级别的排序或者类似的其他扫描线检测技术。但是它们是如此的透明,以至效果还不错。
对透明光栅化没有什么值得说的,因为它使用了256的分辨率,和颜色分辨率对齐起来,一切运算结果都存放到了一张二维表格上。再也没有比这速度更快的办法了,要么就不要用它。
bmpB=alphatable[bmpB][alpha]+alphatable[*surbuf][invalpha];
bmpG=alphatable[bmpG][alpha]+alphatable[*(surbuf+1)][invalpha];
bmpR=alphatable[bmpR][alpha]+alphatable[*(surbuf+2)][invalpha];
上面这段Alpha混合的代码段我曾经打算利用它来炫耀,但是为了避免被别人指责无知我就立刻放弃了该想法。
注意一个事实:插值过程永远不会令类似色彩或者位移或者Alpha或者其他任意数值越界,若存在,则一定是代码写错了,任何优秀的输入数据和错误过滤都无法挽救糟糕的代码错误,所以放弃这种不必要的检测。

图4-8、经典回归:一个透明材质的茶壶
最后,有一种方式可以适当补救深度排序引起的多边形交叠情况(这是我在刚完成这篇文档的大部分的时候想到的),因为多边形交叠可能性的存在,更后面渲染的多边形因为比之前的多边形深度要大,所以被拒绝渲染,而事实上因为透明材质的特性,这些像素信息应该被渲染到屏幕上的。这时候可以考虑这样一种策略,不更新深度缓存,而是渲染所有比不透明物体近的像素,因为产生交叠的多边形通常属于同一个物体所有,它们拥有相同的透明值,而不同物体间的交叠仅在两个物体都拥有透明材质时才发生交叠,但是这种情况可以在关卡设计时避免。还有就是当同一个物体的多边形交叠,且交叠的多边形的光照色彩差异较大,这也会有问题,原本是事实在后部的透明多边形遗留更少的色彩信息,现在则是事实位于前部的透明多边形遗留更少的色彩信息,但是没有什么比什么都不争取就放弃更令人沮丧了。


图4-9、深度缓存更新和不更新渲染的差异,第二图近似地实现了真实效果
四、起点误差
我曾经试着利用扩展Bresenham算法对起点误差进行估价,但是在多边形光栅化内无法完成,因为它结合了两个轴的修正逻辑,而不是分开处理。最后我仅仅是在画第一个点之前就累积了一个标准误差,使得它在合适的时刻就修正被动变化量,而不会等到所有扫描线循环完毕,少放了一个误差值。扩展Bresenham则对首尾段进行平分。如上面所述,这在没有分支判断的情况下无法完成,要知道,这需要双倍的代码。
五、包围盒层次和碰撞检测
在大量的书籍中,这两者都是分别被分作两章并展开漫长的讨论的,而在这里,仅仅是一个小节的信息量。这说明了两个问题:一、我使用了极为简练的语言试图描述各种错综复杂的技术和逻辑关系。二、为了对付游戏编程,我们是否被日益膨胀的技术信息蒙蔽了双眼。也许,希望就在转角?
这方面的工作都是我在需求的驱使下阅读一些概念性的讲述后完成全部细节的(其实,这句话也适用于之前论述到的所有方面),但已经足够满足当前的一部分需求。如果可能开发一个应有尽有的引擎系统,那么就不会再有层出不穷的A引擎B引擎诞生。除非为了逗趣。
之所以细节上的建立可以重头做起,关键是这些概念描述的主体是否是公开的客观的,如果是的话,重新观察和思量一番也无妨。比如包围盒层次用于物体剔除是基于这样一个事实:我看着眼前的桌子和椅子,还有上面的水果篮,我知道柜子底下还躲着一只猫,可是柜子在哪呢?它在我身后,我必定看不到它,更别提那只行事诡秘的猫了。

图4-10、可视化的包围盒
我在物体模型加载的时候处理了模型的包围盒和内部几何体的包围盒,关于这方面的细节没什么可说的。它是2D画面中包围框的扩展,依然是轴对齐的,只不过多了一个维度。对这种类型数据的处理并没有随着新的维度的增加而难度有所加大,它依然可以被压缩到三个一维的区间进行处理。仅当多个轴向之间需要互动的时候这种难度上的变化才体现出来。一个例子是:当物体A部分和物体B在垂直于x轴的面上相交,那么它也应该在y轴向和z轴向和物体B都相交,这样它除了沿反方向退出外什么也做不了。

图4-11、物体A和物体B的某个面浅相交,玩家期望它能在B表面漫游
当物体A和物体B浅相交的时候我们会认为它们之间碰撞是那么地不明显,就好像根本没发生一样,我们不期望继续往前冲撞过去,至少可以让我们绕开,这意味着除了相交面的法线负方向我们不指望外,其他的方向我们还应该能够保留自由度。这样的需求使得三个原本逻辑分离的一维事件重新统一在立体空间了。
注意:我在引擎中加入了浅相交的滑行策略,这对于提高可玩性提供了极大的帮助。回想我们在玩一款游戏,我们会尝试四处跑动,并在撞上墙壁后还期望能够从碰撞的地方缓缓地改变朝向接着往更好的地方跑去。缺失的滑行策略将导致这样的问题:玩家撞上墙壁,在cpu中高速运转的碰撞系统反馈给对象控制器,不可以往前,因为在这个方向撞上了,不可以往左往右往上往下,理由一样,只能往后退了。
从原理上来讲,上述过于严肃的判断策略是正确的,但是我们还是需要滑行策略,关键在于我们并不想在物体撞上表面后给一个回退量,第一、需要额外的代码,但是目前的碰撞系统的代码简洁而高效。第二、假如玩家不幸处在两个碰撞盒之间,那么就会产生无休止的回退过程。第三、是突然的回退量还是缓慢供给的回退量,突然的回退量要给多少,往哪个矢量方向,陷入另一个碰撞盒怎么办,缓慢的回退量看似可行,但考虑这样一个问题:玩家碰上墙壁了,试图沿着墙壁缓慢斜行,然后绕开它,这个时候就会产生回退、碰撞、回退、碰撞的过程,如果相机此刻定位在玩家身上,那么整个画面就开始颤动了。如果游戏角色的跑动不是基于统一的地图平面而是基于地形系统的话,这个问题就会更让人难于接受了。角色时时刻刻都处在回退值的干扰之下而不得安宁。
另一个问题是梯度爬行策略。注意,关于碰撞检测和物理系统的大量用词都是我捏造的,因为我没有也不必去考察其它的实现方案,所以使用自己定义的名称并不奇怪。
 
图4-12、汽车试图沿着梯度上升,结果,它做到了!
不应该让玩家处于永恒不变的平面,梯度爬行策略是一个简单的地形跟踪模型,它需要满足以下条件:

1、A半高位超过B顶面
2、A在B的垂直面上处于滑行状态
3、A底部在B顶部以下
其中条件1可以指定一个期望的阀值,默认情况下为0.5,条件2对滑行状态的判定也可以指定小于0.5的阀值,默认情况下为0.2,太大的阀值看起来不真实(A陷入B太深了,根本动弹不得)。
碰撞检测过程分解:

①、A半高和B半高比较。
If(A>=B)
正方向反馈力。
If(A<B)
负方向反馈力。

②、A区间和B区间是否交叠
If(Min(A,B)的顶点位于Max(A,B)区间)
区间交叠
If(区间交叠)
使用①力反馈
Else
退出

④、A相对于两端点n分面和B的两端点的比较
If(A底分面在B顶端点之上||A顶分面在B底端点之下)
浅相交

⑤、滑行策略:浮动平面
If(浅相交在Y轴向)
浮动平面;消除X、Y轴向力反馈

⑥、滑动策略:边接触
If(浅相交在X、Y轴向)
边接触;消除非浅相交轴向力反馈

⑦、梯度爬行策略(梯度跳跃)
If(A半高大于B顶端点&&A对B在Y轴向边接触&&A底端点小于B顶端点)
梯度跳跃(信息传递给力反馈W分量)

⑧、Y滑行策略
If((!梯度跳跃)&&边接触)
Y滑行策略

⑨、浮动修正
If(A半高>B半高&&A底端点<B顶端点&&A顶端点>B顶端点&&(!边接触))
浮动修正(信息传递给力反馈W分量)

⑩、高度上升
If(梯度跳跃||浮动修正)
y++
六、全局顶点雾
 
图4-13、富于诗意的全局顶点雾世界
①顶点雾浓度

全局雾浓度等于z在fog.length区间的占位(256分辨率)
fog.length=(f-n)*fog.percnet
fog.alpha=max(min((z-fog.length)/fog.length,1),0)*0xFF
②雾化混合
Gouraud::fog=Alpha(fog.color,vertex.color,fog.alpha)
GouraudAlpha::fog=Alpha(vertex.color,screen.color,vertex.alpha)
->Gouraud::fog
Texture::fog=Mix(vertex.color,bmf.color)
->Gouraud::fog
TextureAlpha::fog=Modulate(vertex.color,bmf.color)
->GouraudAlpha::fog
③“贴图->雾化”和“雾化->贴图”的区别
雾化后贴图,贴图信息将不受雾化影响。
应该使用“贴图->雾化”过程
④“Alpha->雾化”和“雾化->Alpha”的区别
先雾化再Alpha将导致雾颜色透明化,若雾颜色和背景色一致,则没有问题。
Gouraud不处理贴图信息,可事先计算顶点雾颜色,为避免GouraudAlpha为了先处理Alpha而对雾浓度插值的开销,雾颜色和背景色将一致化。
TextureAlpha无论如何都要进行雾浓度插值,故应该给出合适的雾化顺序,即“Alpha->雾化”
⑤雾化和光照的区别
雾化使用Alpha混合,有限定域
光照使用逐一材质调制和求和,无限定域,最终渲染时被截断。
七、使用玩具车在趣味的场景中进行跳跃和跑动的演示程序
打开Release目录下的JumpCar.exe开始玩。
|
|