麒麟子

~~

导航

<2024年11月>
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567

统计

常用链接

留言簿(12)

随笔分类

随笔档案

Friends

WebSites

积分与排名

最新随笔

最新评论

阅读排行榜

评论排行榜

#

优化3D图形流水线

转自:http://hi.baidu.com/freedomknightduzhi/blog/item/7e401a9b2521eeb3c9eaf4f1.html
在使用NVIDIA PerfHUD 5 Launcher的时候,明显发现现在的CPU时间和GPU时间不均衡,于是考虑优化。
下面是参考NVIDIA的OGP开始总结。
优化代码通常是找出瓶颈,对瓶颈进行优化,这里暂不考虑CPU内部的优化方法,主要记录CPU->GPU的3D渲染流水线的瓶颈查出方法以及优化手段。
若仅希望进行CPU方面的优化,可使用一些辅助工具,如Inter的Intel(R) VTune(TM) Performance Analyzer,Intel(R) Thread Profiler 3.1,AMD的CodeAnalyst等。
进行优化的步骤如上面所说:1:找出瓶颈,2:对其优化。
最通用也最有效的找出瓶颈的方法当然是找到核心函数,降低它的时钟周期和负荷,看是否对程序性能有大的影响。优化的手段多是拆东补西而已,即,将影响性能的瓶颈中的任务分配给其他较空闲的部分进行处理,来平衡整体所消耗的时间。
那么来看一下图形渲染流水线大致过程。
1:系统CPU从内存中读取几何顶点 -> 输送到GPU显存 -> 输送到GPU高速顶点缓冲区 -> GPU顶点着色 -> GPU建立三角型 -> GPU矩阵变换 -> GPU光栅化 -> 3
2:系统CPU从内存中读取纹理信息 -> 输送到GPU显存 -> 输送到GPU高速纹理缓冲区( DX10.0以后可与顶点缓冲共同,不再强制区分 ) -> 3
3:片段着色光栅化 -> 输出GPU后台缓冲进行渲染。
那么,很简单的有几大模块在其中可能存在着瓶颈的限制。

1:CPU本身逻辑计算能力的限制。

2:CPU到GPU显存AGP传输能力的限制
(1)顶点
(2)纹理
3:GPU显存到高速缓冲区的传输带宽限制
(1)纹理传输带宽限制     (显存->高速缓冲区)
(2)光栅化完毕后的桢传输带宽限制 (高速缓冲区->显存)
注:这里不考虑 顶点 传输的带宽限制,因为这个限制极小
4:GPU高速缓冲区内部处理能力的限制。
(1)顶点变换着色处理能力限制。
(2)顶点最大数量支持限制。
(3)三角型建立限制。
(4)光栅化限制。
(5)象素着色限制。
5:内存过小限制。
6:显卡显存过小,以及其他硬件Caps限制。

上述就是常见3D图形渲染流水线中的瓶颈限制,那么我们下一步去一一确定,可能是哪方面的瓶颈。简单的方法是检测FPS。
注意1:许多瓶颈可能由于硬件更变而更变。
注意2:Debug模式和Release模式的瓶颈表现未必相同。
注意3:查看FPS时候一定关闭垂直同步。
1:改变色深,16bit,32bit,这个是直接影响 桢渲染缓冲 的大小的,若修改了此项之后,FPS有较大变化,则是由于3.2 桢传输带宽限制。
注:这里需要改变所有渲染对象的色深。
2:改变纹理大小尺寸,改变纹理过滤方式,若修改了此项之后,FPS有较大变化,则是由于3.1 纹理传输带宽的限制 或 2.2 纹理AGP传输能力限制。
注:纹理过滤方式中,点过滤速度 > 线性过滤速度 > 三角面过滤速度 > 各向异性过滤速度 若改变纹理过滤方式就将FPS提高了,则是3.1 纹理传输带宽的限制。这步是将纹理数据从显存运输到GPU高速纹理缓冲区的过程。
3:改变桌面分辨率,若修改了此项之后,FPS有较大变化,则是由于 4.4 光栅化限制 或是 4.5 象素着色Shader限制。
此时减少 PixelShader指令数量,若修改了此项之后,FPS有较大变化,则是由于 4.5 象素着色Shader限制,若没有较大变化,则是由于 4.4 光栅化限制。
4:减少 VertexShader 指令数量,若修改了此项之后,FPS有较大变化,则是由于 4.1 顶点变换着色处理能力限制。
5:减少顶点数量和AGP传输速率,若修改了此项之后,FPS有较大变化,则是由于 4.2 顶点最大数量支持限制 或 2.1 顶点AGP传输能力限制。
6:若以上都不是,则是 1.0 CPU逻辑计算能力限制。
注:该项也可根据NVIDIA PerfHUD来检测CPU和GPU的空闲时间来判定,若GPU空闲时间过多,则说明是由于CPU计算能力或AGP传输能力导致。
该项也可用简单的更换CPU,而不更换GPU的方式来检测判定。
7:看资源管理器,CPU占用率,内存占用率,可以知道是否是由于1.0 CPU本身逻辑计算能力的限制 或是 5.0内存过小限制。
8:看DX SDK自带的CapsViewer可以知道显卡的支持性,以获得更多更准确的判定。
9:在BIOS中更变APGP为1X模式,若修改了此项之后,FPS有较大变化,则是由于2.1 或 2.2 AGP传输能力限制。
10:降低GPU配置进行检测判定,此时要注意两项,一是降低GPU的运行频率,一是降低GPU显存性能和大小,可以确定GPU方面的问题大致所在。
11:删除一些游戏中涉及的 物理,AI,逻辑 等占用大量CPU效率的代码以获得更强的针对性。
12:对角色,地形,静态模型,阴影 等设置渲染开关,以更明确的确定问题所在。

优化方法:
一:整体优化。
1:减少小批量作业
(1)让一个顶点缓冲中更多顶点。(1024点以上较适合)
(2)少Draw。(尽量一次性多渲染些三角形,减少渲染次数)
(3)尽量将多个尺寸小的纹理文件合并为一个尺寸大的纹理文件,减少零碎的小纹理文件数量。
(4)使用VertexShader将一些关系紧密的几何体打包在一起。(VS2.0就已经存在256个4D向量常数)
2:逻辑排序优化
(1)尽量在逻辑层将顶点进行一定的排序以减少在GPU高速缓冲区中的重新排布。
(2)尽量将渲染对象在逻辑层按照深度由屏幕->内部排序,减少不必要的深度拣选。
(3)尽量使用索引条带或索引列表
(4)根据渲染状态和渲染对象对纹理进行基本排序
3:减少不必要的渲染(CPU层的基本二分四叉八叉这里不再强调)
(1)在多Pass渲染时,在第一个渲染Pass上对每个渲染对象加以咨询,当第一个Pass中该渲染对象渲染象素量达不到指定标准,则后续Pass不再对其进行渲染。
(2)对一些重复渲染(如太阳眩光特效)需要进行计数,达到指定数量即停止渲染或进行分布式渲染。
(3)对一些复杂的模型设置基本的包围盒判定其渲染必要性。
4:减少线程锁定导致的不必要等待
(1)CPU Lock了一个资源,等待GPU进行渲染,此时常见做法有等待GPU渲染,中间期间CPU经常处于Idle空闲状态,建议此时给CPU其他的事情做,如为下一个资源做好基本准备或进行逻辑处理。
5:减少或平均分布CPU压力(实际上,大部分程序是CPU逻辑计算限制的)
(1)CPU压力重点在以下方面可能存在: AI,IO,网络,复杂逻辑,这些部分可进行CPU瓶颈测试以确定优化方向。
(2)优化方针:宁可GPU忙碌也要CPU减压。
(3)使用文章开始时我提到的一些工具去查找CPU中不必要的汇编空循环以及不必要的CPU空闲。
二:局部优化。
6:AGP传输瓶颈
(1)当过多数据通过AGP8X从CPU内存传递到GPU显存时,我们可以选择以下方式优化。
   [1]减小顶点个数
   [2]减少动态顶点个数,使用VertexShader动画替代。
   [3]正确使用API,设置正确参数,避免动态顶点和纹理缓冲区的创建管理。
   [4]根据硬件配置属性确定适合的 桢缓冲,纹理缓冲,静态顶点缓冲 的大小。
(2)避免使用无序或不规则数据传输。
   [1]顶点数量尺寸应当是32的整数倍。(可使用顶点压缩,再在VertexShader中对顶点数据进行解压缩)
   [2]确保顶点的有序性。(在CPU逻辑层对其进行排序后传输,NVTriStrip这个工具可以帮我们生成优化的高效的有序的Mesh顶点数据)
(3)具体到API层面的几何Mesh传输
   [1]对于静态几何体,创建 只写的顶点缓冲,且,仅写入一次。
   [2]对于动态几何体,在程序初始创建一个动态顶点缓冲,之后每桢初始锁定DISCARD,进行NOOVEWRITE而不要进行DISCARD,DISCARD的耗时不是NOOVEWRITE可比的。
   [3]基本原则,少创建缓冲区,多对其进行重复使用,减少锁定次数。
7:顶点变换传输处理瓶颈(由于GPU有强大的顶点处理能力,一般在顶点变换方面不会有瓶颈出现,但假若出现了。。)
(1)顶点太多
   [1]使用细节Lod,一般起用2-3级Lod就足够了。
(2)顶点处理过于复杂
   [1]减少灯光数量,降低灯光复杂度(方向平行光效率 > 点光源效率 > 聚光灯效率 )
   [2]减少顶点着色器指令数量,避免128条以上指令,避免大量的分支指令
   [3]对顶点进行CPU层逻辑排序
   [4]能在CPU中进行计算的在CPU中进行计算,传递常量给GPU
   [5]减少和避免CG/HLSL之中的 mov 指令。即使使用了,也要重点注意。
8:大部分情况下 4.3 三角形建立限制 以及 4.4 光栅化限制 是不会成为瓶颈的,但,当三角形数量过多或者光栅化时每个三角形顶点数据过于复杂时可能会出现这种瓶颈,此时减少三角形总数,使用VS或减少Z-cull三角都是有效的方法。
9:象素着色器的瓶颈(在DX7之前,全是固定渲染管道,一般来说传输量和着色器之间的计算是均衡的,但是DX8开始可编程流水管道开始,PixelShader的计算量开始增幅,数据传输量通常相对来说比较小了。)
(1)需处理的纹理片段过多过大
   [1]在CPU层按照 屏幕->向内 Z-Buffer的顺序排序传入,并按照这个顺序进行渲染。
   [2]多Pass渲染时,考虑在第一个渲染Pass中关闭特效并让第一个Pass负责Z-buffer的处理。这样的话,后续Pass中可以避免渲染不要的纹理片段。
(2)每个纹理片段的处理过于复杂
   [1]大段的长着色器指令将会很大降低效率,尝试减少着色器指令长度
   [2]使用向量操作,并行co-issuing来减少指令数量。
   [3]混合使用配对的简单的texture和combiner组合指令。
   [4]使用Alpha混合器提高性能。
   [5]考虑对阴影也进行Lod计算。
   [6]在DX10开始,考虑将顶点缓冲移做象素缓冲进行使用。
(3)额外的优化方法
   [1]使用fx_12精度
   [2]使用fp16指令
   [3]使用Pixel_Shader2.0的时候开启ps_2_a描述开关
   [4]减少寄存器的临时存取
   [5]减少不必要的精度要求
   [6]尽量使用低版本的Shader(但避免使用VS1.0,已经被VS3.0抛弃了)
10:纹理贴图导致的瓶颈
(1)优化方法。
   [1]纹理过滤时避免使用 三角面性过滤 和 各相异性过滤,特殊需求除外,一般线性过滤已经可以做的很好。
   [2]即使使用各相异性过滤,也要降低相异性比率。使用了各相异性过滤的话,则可以尽量减少三角面性过滤。
   [3]降低纹理分辨率,避免使用不必要的高分辨率纹理。
   [4]降低纹理色深,例如环境纹理,阴影纹理这些,尽量使用16位。
   [5]建议进行纹理压缩,例如DXT格式就可以有效压缩纹理,并且GPU对DXT格式支持很好。
   [6]避免使用非二次方的纹理资源。
   [7]在进行纹理锐化的时候,避免使用负值的Lod进行锐化,会导致远处失真,尽量使用各相异性过滤进行锐化
   [8]对于动态纹理,一般建议用 D3DUSAGE_DYNAMIC D3DPOOL_DEAFAULT 进行创建缓冲,使用 D3DLOCK_DISCARD 进行锁定,尽量做到一次锁定多次使用,不要频繁解锁,另外,永远不要读这样的纹理。
11:桢缓冲导致的瓶颈
(1)优化方法
   [1]尽量关闭Z-write,一般来说,在一个渲染Pass中就可以进行完整的Z-buffer处理,在后续的Pass中就应当关闭Z-write,不用担心,即使需要Alpha混合的对象也不再需要开启Z-write了。
   [2]尽量开始AlphaTest,实际上这个操作会提高效率,而非降低。
   [3]避免使用浮点桢缓存。
   [4]若没有启用模版深度缓冲的话,使用16位的Zbuffer就可以了。
   [5]避免使用RendToTexture,或者可能的去减少Rend的尺寸。
对于现在可编程流水管线来说,这意味着我们有更大的自由度实现更多的特效,但也有了更多的瓶颈和更多的复杂度,我们遇到问题要正确的获取瓶颈所在,开动脑筋进行优化,平衡各环节间的负载。让各环节不过载不空闲。

更多信息希望您查看Nvidia的《GPU_Programming_Guide》,翻译成中文则是《GPU编程精粹》。以上。

posted @ 2010-01-29 13:48 麒麟子 阅读(1954) | 评论 (2)编辑 收藏

优化3D图形渲染通道负载

优化3D图形渲染通道负载

http://www.itjiaocheng.com/jiaocheng/pingmiansheji/AutoCAD/texiaojiqiao/2009/0520/23435.html

  

一般来说, 定位渲染通道瓶颈的方法就是改变渲染通道每个步骤的工作量, 如果吞吐量也改变了, 那个步骤就是瓶颈.。找到了瓶颈就要想办法消除瓶颈, 可以减少该步骤的工作量, 增加其他步骤的工作量。

   一般在光栅化之前的瓶颈称作”transform bound”, 三角形设置处理后的瓶颈称作”fill bound”定位瓶颈的办法:

   1.改变帧缓冲或者渲染目标(Render Target)的颜色深度(16 到32 位), 如果帧速改变了, 那么瓶颈应该在帧缓冲(RenderTarget)的填充率上。

   2.否则试试改变贴图大小和贴图过滤设置, 如果帧速变了,那么瓶颈应该是在贴图这里。

   3.否则改变分辨率.如果帧速改变了, 那么改变一下pixel shader的指令数量, 如果帧速变了, 那么瓶颈应该就是pixel shader. 否则瓶颈就在光栅化过程中。

   4.否则, 改变顶点格式的大小, 如果帧速改变了, 那么瓶颈应该在显卡带宽上。

   5.如果以上都不是, 那么瓶颈就在CPU这一边。

   优化方法36条:

   1.尽量减少无用的顶点数据, 比如贴图坐标, 如果有Object使用2组有的使用1组, 那么不 要将他们放在一个vertex buffer中, 这样可以减少传输的数据量。

   2.使用多个streamsource, 比如SkinMesh渲染, 可以把顶点坐标和法线这些每一帧都要修改的数据放在一个动态VB中, 其它不需要修改的(如贴图坐标)放到一个静态VB中, 这样就减少了数据传输量。

   3.尽量使用16位的索引缓冲,避免32位的. 一方面浪费带宽, 一方面也不是所有的显卡都支持32位的索引缓冲。

   4.可以考虑使用vertex shader来计算静态VB中的数据.比如SkinMesh的顶点可以放到vectex shader中计算, 这样就可以避免每一帧都从AGP内存中向显存传送数据. 这样也可以使用静态VB了。

   5.坚决避免使用Draw**UP一族的函数来绘制多边形。

   6.在设计程序之前好好规划一下显卡内存的使用, 确保framebuffer, 贴图, 静态VB能够正好放入显卡的本地内存中。

   7.尽量使顶点格式大小是32字节的倍数.可以考虑使用压缩过的顶点格式然后用vertex shader去解. 或者留下冗余的部分, 使顶点大小刚好使32字节的倍数。

   8.顶点在顶点缓冲中的顺序尽量符合绘制的顺序, 考虑使用strips来代替list。

   9.如果可能尽量多的使用static vertex buffer代替dynamic vertex buffer。

   10.动态VB使用DISCARD参数来lock更新, 使用NOOVERWR99vE来添加.尽量不要使用不带参数的lock调用(0)。

   11.尽量减少lock的次数, 有些东西并不一定非要每一帧都更新VB, 比如人物动画一般每秒钟更新30次VB基本上就够了。

   12.如果是因为需要绘制的顶点数据太多了可以考虑使用LOD, 但是现在的显卡的绘制能力都很强劲, 所以需要权衡一下LOD是否能够带来相应的好处, 如果过分的强化LOD很可能将瓶颈转移到CPU这边。

   13.避免过多的顶点计算,比如过多的光源, 过于复杂的光照计算(复杂的光照模型), 纹理自动生成的开启也会增加顶点的计算量. 如果贴图坐标变换矩阵不是单位矩阵, 也会造成顶点计算量的增加, 所以如果纹理变换已经结束, 记得要将纹理变换矩阵设为单位矩阵同时调整贴图坐标。

   14.避免Vertex shader指令数量太多或者分支过多, 尽量减少vertex shader的长度和复杂程度. 尽量使用swizzling代替mov。

   15.如果图象质量方面的计算(pixel shader)范围很大, 并且很复杂, 可以考虑试试全屏反走样。说不定更快。

   16.尽量按照front – back的顺序来绘制。

   17.在shader中判断Z值可以避免绘制不可见的象素, 但是nvidia建议简单的shader不要这么做.(Don't do this in a simple shader)。

   18.如果可能, 尽量使用vertex shader来代替pixel shader.将计算从逐象素变成逐顶点。

   19.尽量降低贴图的大小.过大的贴图可能造成贴图cache过载, 从而导致贴图cache命中降低.过大的贴图会导致显存过载, 这时候贴图是从系统内存中取的。

   20.只要可能就用16位色的贴图, 如环境贴图或者shadow map.它们用32位色的贴图实在是浪费。

   21.考虑使用DXT 贴图压缩。

   22.如果可能,使用简单的贴图过滤或者mip map, 除非必要否则尽量不要使用三线过滤和各项异性过滤. light map 和环境贴图基本上都不需要使用它们。

   23.只有真正需要修改的贴图才使用Dynamic, 并且使用DISCRAD和WR99vEONLY来lock。

   24.太多的帧缓冲读写可以考虑关闭Z-Writes如有些多pass的渲染中的后续pass或者粒子系统等半透明几何物体(如果可以)。

   25.可能的话尽量使用alpha test代替alpha blending。

   26.如果不需要stencil buffer就尽量使用16位的Z buffer。

   27.减小RenderTarget 贴图的大小, 如shadow map 环境贴图. 可能根本不需要那么大效果就很好。

   28.Stencil 和Z buffer 尽量一起clear. 他们本来就是一块缓冲。

   29.尽量减少渲染状态的切换, 尽量一次画尽可能多的多边形。(根据显卡性能决定最多画多少, 不过一般再多也不会多到哪里去。 除非你根本不需要贴图和渲染状态的切换)。

   30.尽量使用shader来代替Fixed Pipeline。

   31.尽量使用shader来实现来取代Multipass渲染效果。

   32.尽量优先先建立重要的资源, 如Render target, shaders, 贴图, VB, IB等等.以免显存过载的时候它们被创建到系统内存中。

   33.坚决不要在渲染循环中调用创建资源。

   34.按照shader和贴图分组后再渲染.先按照shaders分组再按贴图。

   35.Color Stencil Z buffer尽量在一次Clear调用中清除。

   36.一个Vertex buffer 的大小在2M-4M之间最好。(中国软件)

posted @ 2010-01-29 13:43 麒麟子 阅读(1260) | 评论 (0)编辑 收藏

关于骨骼动画及微软示例Skinned Mesh的解析

 

原文链接:http://www.gameres.com/document.asp?TopicID=87707

这是我自个写的,第一次发. 没想到这个贴子编辑器极差. 原文是有字体字色的.现在只能清一色了.
版主,发贴的编辑器太难用! 你有必要向上反映一下. 下面的字体是我敲html标记加上的,大家凑和看.

 

关于骨骼动画及微软示例Skinned Mesh的解析

骨骼动画是D3D的一个重要应用。尽管微软DXSDK提供了示例Skinned Mesh,但由于涉及众多概念和技术细节,示例相对于初学者非常复杂,难以看懂。在此,提供一些重要问题评论,以使初学者走出迷局,顺利上手。文中所述都是参照各种资料加上自己的理解,也有可能出些偏差,有则回贴拍砖,无则权当一笑。


一 骨骼动画原理
原理方面在网上资料比较多,大家都基本明白。在此说一下重点:
总体上,绝大部分动画实现原理一致,就是“提供一种机制,描述各顶点位置随时间的变化”。有三种方法:
1.1 关节动画:由于大部分运动,都是皮肤随骨骼在动,皮肤相对于它的骨骼本身并没有发生运动,所以只要描述清楚骨骼的运动就行了。用矩阵描述各个骨骼的相对于父骨骼运动。(大多运动都是旋转型) 易知,从子骨骼用矩阵乘法累积到最顶层根骨骼,就可以得到每个子骨骼相对于世界坐标系的转换矩阵。
  这种动画,只须用普通Mesh保存最初始的各顶点坐标,以及一系列后续时刻所对应的各骨骼的运动矩阵。不用保存每时刻的顶点数据,节省了大量存储空间。而且比较灵活,可以利用关键帧插值运算,便于通过运算调节动作。缺点是在两段骨骼交接处,容易产生裂缝,影响效果。

1.2 渐变动画:通过保存一系列时刻的顶点坐标来完成动画。虽然比较逼真,但占用大量空间,灵活性也不高。

1.3 骨骼蒙皮动画(skinned Mesh)
  相当于上面两方法的折中。现在比较流行。
  在关节动画的基础上,利用顶点混合(Vertex Blend)技术,对于关节附近的顶点,由影响这些顶点的两段(或多段)骨骼运动,分别赋以权值,共同决定顶点位置。相当于在骨骼关节上动态蒙皮,有效解决了裂缝问题。

  这里,引入一个D3D技术概念:“Vertex Blending”---顶点混合技术。比如说,你肯定用过SetTransform(D3DTS_WORLD,....),但SetTransform(D3DTS_WORLDMATRIX(i),....)是不是很奇怪?这个问题后文会讲到。 你也可以在微软的DXSDK的帮助文件中搜索“Geometry Blending”主题,有裂缝及其解决办法图示。

 

二 X文件如何保存骨骼动画

理解X文件格式,对用好相关的DX函数是非常重要的。

不含动画的普通X文件,有一个Mesh单元,保存了各顶点信息、各三角面的索引信息、材质种类及定义等。

动画X文件,则在这个单元中增加了“各骨骼蒙皮信息”、“骨骼层次及结构信息”、“各时刻骨骼矩阵信息”等。

2.1 网格蒙皮信息:首先,在Mesh{}单元中,在原有的普通网格顶点数据基础上,新增了XSkinMeshHeader{}结构,以及多个SkinWeights{}结构。用以描述各个骨骼的蒙皮信息。

其中,XSkinMeshHeader是总括,举一实例,如下:

XSkinMeshHeader
{
2,//一个顶点可以受到骨骼影响的最大骨骼数,可用于计算共同作用时减少遍历次数
4,//一个三角面可以受到骨骼影响的最大骨骼数。这个数字对硬件顶点混合计算提出了基本要求。
35 //当前Mesh的骨骼总数。
}

由于每个骨骼的蒙皮信息都需要用SkinWeights结构去描述,所以有多少块骨骼,在Mesh中就有多少个SkinWeights对象。
注意,一般把SkinWeights视作Mesh的一部分。这种Mesh又称Skinned Mesh (蒙皮网格)

SkinWeights 结构如下:
{
  STRING      transformNodeName;      //骨骼名
  DWORD       nWeights;               //权重数组的元素个数,即该骨骼相关的顶点个数
  array DWORD vertexIndices[nWeights];//受该骨骼控制的顶点索引,实际上定义了该骨骼的蒙皮
  array float weights[nWeights];      //蒙皮各顶点的受本骨骼影响的权值
  Matrix4x4   matrixOffset;           //骨骼偏移矩阵,用来从初始Mesh坐标,反向计算顶点在子骨骼坐标系中的初始坐标。
}
在有的书中,把上面的matrixOffset叫骨骼权重矩阵,是不恰当的。应该称为骨骼偏移矩阵比较合适。

[问题] 在整个动画过程中,子骨骼运动矩阵的数值是不断变化的。上面的骨骼偏移矩阵变化吗?有没有必要重新计算?它在什么时候使用?
答:各骨骼的偏移矩阵matrixOffset专门用来从原始Mesh数据计算出各顶点相对于骨骼坐标系的原始坐标。在绘制前,把它与当前变换矩阵相乘,就可以得到该骨骼的当前的最终变换矩阵。 总之,骨骼偏移矩阵是与原始Mesh顶点数值相关联的,在整个动画过程中是不变的,也不应该变。在动画过程中变化是当前骨骼变换矩阵,可由.X中的AnimatonKey中的各时刻矩阵得到。这个矩阵乘法在示例中的对应代码如下:
D3DXMatrixMultiply( &matTemp, &pMeshContainer->pBoneOffsetMatrices[iMatrixIndex], pMeshContainer->ppBoneMatrixPtrs[iMatrixIndex] );

即,D3DXMatrixMultiply(输出最终世界矩阵, 该骨骼的偏移矩阵, 该骨骼的变换矩阵)


2.2 骨骼层次信息

在X文件中,Frame是基本的组成单元。又称框架Frame。 一个.x可以有多个Frame。(注意此处的Frame不是帧,与帧没什么关系)

框架Frame允许嵌套,这样就存在父子框架了。而并列的框架,称为兄弟框架。这两种关系组合在一起,即可以纵深,又可以并列,形成一种层次结构。这种结构,可用二叉树描述。

每个框架结构的最前面,有一个FrameTransformMatrix矩阵数据,描述了该框架相对于父框架的变换矩阵。也就是说,该框架中的坐标,与该矩阵相乘,可转换为父框架坐标系的坐标。
这种层次结构,使得X文件能描述许多复杂的物体。如地形场景。

在骨骼动画文件中,框架结构可直接拿来描述人物骨骼的层次结构。框架的名字通常为对应的骨骼名。
如“左上臂->左前臂->手掌->手指”就形成一个父子骨骼链。而左上臂与右上臂是并行关系。

数据示例: D:\D9XSDK\Samples\Media\tiny.x

Frame ...{
  .....

  Frame Bip01_R_Calf { //子骨骼
      
       FrameTransformMatrix {
        1.000000,-0.000691,-0.000000,0.000000,0.000691,1.000000,-0.000000,0.000000,0.000000,0.000000,1.000000,0.000000,119.231522,0.000021,-0.000011,1.000000;;
       }

        Frame Bip01_R_Foot {//--孙子骨骼
      
        FrameTransformMatrix {
         0.988831,0.124156,0.082452,0.000000,-0.122246,0.992109,-0.027835,0.000000,-0.085257,0.017445,0.996206,0.000000,119.231476,-0.000039,0.000023,1.000000;;
        }

        ....缩进
    }
}

[问题]查看示例tiny.x文件,发现只有根框架下有一个Mesh,包含了所有顶点信息。其它各个Frame都没有Mesh数据。怎么理解?
答: 一般来说,每个动画文件只有一个Mesh网格,包含物体所有顶点信息。
     其它Frame,只是借用来描述各骨骼的层次信息,没必要再定义骨骼网格。每块骨骼对应的蒙皮顶点信息,由根Mesh中的相应骨骼的SkinWeights中蒙皮顶点索引描述的。在动画过程中,各个顶点的新坐标,要借助SkinWeights中的顶点索引来进行重新计算。

2.3 动画信息:
由一系列AnimatonKey组成,数据示例如下:

  AnimationKey {
   4;--动画类型 4表示矩阵
   62; --动画帧数,即下面矩阵个数
   0;16;1.000000,-0.000691,-0.000000,0.000000,0.000691,1.000000,0.000000,0.000000,0.000000,-0.000000,1.000000,0.000000,119.231514,-0.000005,0.000001,1.000000;;,
   80;16;0.992696,-0.120646,-0.000000,0.000000,0.120646,0.992696,0.000000,0.000000,-0.000000,-0.000000,1.000000,0.000000,119.231514,0.000002,-0.000002,1.000000;;,

   ..上面红数字表示时刻tick,兰数字表示数值的个数。
   ...其它各时刻矩阵...

   { Bip01_R_Calf }--对应的骨骼对象引用
  }


注意:
(1)每块骨骼都有一个AnimationKey{}.
(2)在上面数据结构中,主要保存了各典型时刻的该骨骼相对于父的变换矩阵.
(3)在0时刻的矩阵,与该骨骼对应的前面的Frame所对应的矩阵是相同的。如Frame Bip01_R_Calf{}中的变换矩阵,与Bip01_R_Calf所对应的AnimationKey 的第0时刻矩阵是一样的。这说明,在以后动画运行时,DX会提供一种功能,用AnimatonKey中的对应数据刷新初始的变换矩阵(也可能启用关键帧插值算法)。这个功能对应于示例中的m_pAnimController->SetTime(...)语句。

三 怎样从X文件加载骨骼动画信息?
3.1 负责加载的函数:
  可能有多种加载方式,在此以SDK中的示例为准,叙述一种标准加载方式,需要用到DX函数D3DXLoadMeshHierarchyFromX(),函数字面意思是读取Mesh层次信息。
HRESULT WINAPI
    D3DXLoadMeshHierarchyFromX(
        LPCSTR Filename,                 //.x文件名
        DWORD MeshOptions,               //Mesh选项,一般选D3DXMESH_MANAGED
        LPDIRECT3DDEVICE9 pD3DDevice,    //指向D3D设备Device
        LPD3DXALLOCATEHIERARCHY pAlloc,  //自定义数据容器
        LPD3DXLOADUSERDATA pUserDataLoader,  //一般选NULL
        LPD3DXFRAME *ppFrameHierarchy,       //返回根Frame指针,指向代表整个骨架的Frame层次结构
        LPD3DXANIMATIONCONTROLLER *ppAnimController //返回相应的动画控制器
);

这个函数后面的两个输出参数很重要,也很好理解,但输入参数中的自定义数据容器是怎么回事呢?
原来,鉴于动画数据的复杂性,需要你配合完成加载过程。比如你是否用到自定义扩展结构,Mesh等数据保存在哪里,怎样使用户自己创建容器,自己决定卸载等等。
DX提供了ID3DXALLOCATEHIERARCHY接口,提供了这个自定义的机会,你重载这个接口的虚函数,在加载过程中,它就像回调函数那样运作。

你需要像下面这样建立一个自定义数据容器类:
class CAllocateHierarchy: public ID3DXAllocateHierarchy
{
public:
    STDMETHOD(CreateFrame)(THIS_ LPCTSTR Name, LPD3DXFRAME *ppNewFrame);
    STDMETHOD(CreateMeshContainer)(THIS_ LPCTSTR Name, LPD3DXMESHDATA pMeshData,
                            LPD3DXMATERIAL pMaterials, LPD3DXEFFECTINSTANCE pEffectInstances, DWORD NumMaterials,
                            DWORD *pAdjacency, LPD3DXSKININFO pSkinInfo,
                            LPD3DXMESHCONTAINER *ppNewMeshContainer);
    STDMETHOD(DestroyFrame)(THIS_ LPD3DXFRAME pFrameToFree);
    STDMETHOD(DestroyMeshContainer)(THIS_ LPD3DXMESHCONTAINER pMeshContainerBase);
    CAllocateHierarchy(CMyD3DApplication *pApp) :m_pApp(pApp) {}
public:
    CMyD3DApplication* m_pApp;
};

[问题]上面的STDMETHOD是什么意思?
答:相当于virtual   HRESULT   __stdcall 的宏。<评论> 因为这种类要与D3D的COM接口打交道,不仅仅在C++内部使用,所以,所有类方法必须做成stdcall的,可对外开放的。
#define   STDMETHOD(method)               virtual   HRESULT   STDMETHODCALLTYPE   method  
#define   STDMETHODCALLTYPE               __stdcall  
这样当写一个函数STDMETHOD(op1(int   i))      
展开后成为:     virtual   HRESULT   __stdcall   op1(int   i);  

3.2 自定义数据容器以及具体的读取过程:
根据.X文件,在加载过程中,主要有两方面数据需要保存,一个是骨架Frame信息,一个是网格蒙皮Mesh信息。这两个信息保存在如下结构中。

框架信息(对应于骨骼)
typedef struct _D3DXFRAME
{
    LPSTR                   Name;
    D3DXMATRIX              TransformationMatrix; //本骨骼的转换矩阵

    LPD3DXMESHCONTAINER     pMeshContainer;       //本骨骼所对应Mesh数据

    struct _D3DXFRAME       *pFrameSibling;       //兄弟骨骼
    struct _D3DXFRAME       *pFrameFirstChild;    //子骨骼
} D3DXFRAME, *LPD3DXFRAME;

自定义数据容器,其数据来源由上面接口的CreateMeshContainer()函数提供
typedef struct _D3DXMESHCONTAINER
{
    LPSTR                   Name;       //容器名
    D3DXMESHDATA            MeshData;   //Mesh数据,可创建SkinMesh取代这个Mesh
    LPD3DXMATERIAL          pMaterials; //材质数组
    LPD3DXEFFECTINSTANCE    pEffects;  
    DWORD                   NumMaterials;//材质数
    DWORD*                  pAdjacency;  //邻接三角形数组
    LPD3DXSKININFO          pSkinInfo;   //蒙皮信息,其中含.x中的各个skinweight蒙皮顶点索引及各骨骼偏移矩阵等。
    struct _D3DXMESHCONTAINER *pNextMeshContainer;
} D3DXMESHCONTAINER, *LPD3DXMESHCONTAINER;


[评论]
.在动画文件中,框架通常用来描述骨骼。可以把Frame视做骨骼,所以不细加区分。
.在上面D3DXFRAME结构中,pFrameSibling, pFrameFirstChild两个指针,常用于递归函数中,遍历整个骨架。
.在D3DXFRAME结构中有一个pMeshContainer指针,难道框架与Mesh是一一对应的吗?
有一个框架(骨骼)就有一个Mesh吗?怎么.X文件中只有一个Mesh?难道加载时拆开存放?
答:从D3DXFrame结构上看,每个Frame都有一个pMeshContainer指针。这就有三种解释:
   第一种,加载到内存后所有的pMeshContainer都指向同一个全局Mesh
   第二种,加载到内存后,只有一个主框架的pMeshContainer不为空,其它Frame的pMeshContainer均为NULL,因为在.X中,它们没有定义自己的Mesh
   第三种,加载到内存后,D3D将Mesh拆分,分开到各骨骼所对应的Frame,每个Frame都有自己的Mesh。
   这个问题我以前也不是很清楚,通过查看示例源码及跟踪发现,正确解释应该是第2种。唯一的一个全局Mesh存放在Frame "body"下的无名Frame中。而其它Frame由于没有自己专门的Mesh而指向NULL. 应该大致如此。这个问题之所以让人困绕,是因为从后续代码上看,在渲染DrawFrame时,是遍历每一个frame分别绘制它们对应的Mesh. 如果对应于同一个mesh,就绘制多遍。如果对应各自mesh,那么变换矩阵怎么组织运算等等。所以,根据第二种解释,由于只有一个pMeshContainer不为NULL,所以参与绘制及蒙皮的只有这一个MeshContainer,人体所有顶点数据及蒙皮信息都在这个mesh中。
所以,读取tiny.x文件后,会产生多个D3DXFRAME对象,但只有一个D3DXMESHCONTAINER对象。

在示例代码的CMyD3DApplication::InitDeviceObjects()中,有:
    hr = D3DXLoadMeshHierarchyFromX(strMeshPath, D3DXMESH_MANAGED, m_pd3dDevice, &Alloc, NULL, &m_pFrameRoot, &m_pAnimController);
    if (FAILED(hr))
        return hr;
其中的Alloc是就自定义的数据容器对象。m_pFrameRoot是根骨骼,对遍历很重要。m_pAnimController是动画控制器,对刷新矩阵很重要。

你在运行完这句话后,下一个断点,观察m_pFrameRoot,会发现如下内容:

m_pFrameRoot 0x00c59380 {Name=0x00c53630 "Scene_Root" .....} //根框架
pMeshContainer 0x00000000
pFrameSibling 0x00000000
pFrameFirstChild 0x00c59428 {Name=0x00c53ca8 "body" pMeshContainer=0x00000000...}//子框架 骨骼body
   +---  pMeshContainer 0x00000000
           +---  pFrameSibling 0x01419f00 {Name=0x00c5ffd8 "Box01" pMeshContainer=0x00000000 ...}//兄弟框架
      +---  pFrameFirstChild 0x00c594d0 {Name=0x00000000 pMeshContainer=0x00c59828 //子框架---该框架就是.x中含有唯一全局Mesh的无名框架


可见,在内存中的Frame布局是与.x中一一对应的。除了pFrameFirstChild 0x00c594d0这个地方的Frame中的pMeshContainer不为空,其它框架的这个mesh指针都是空值。
另外一点可以看出,并不是每个Frame都对就一块骨骼,有的是别的用途。也就是说Frame对象的个数可能多于骨骼数。

3.3 分析CAllocateHierarchy类
下面继续研究自定义数据容器CAllocateHierarchy,顾名思义,该类是在加载过程中自行分配层次数据空间。它有4个成员,都是重载D3D的接口虚函数。
它的成员CreateFrame()是用来创建D3DXFrame对象的,而CreateMeshContainer()是用来创建Mesh数据对象的。你可以在这两个函数中下断点,发现CreateFrame会运行多次,而CreateMeshContainer只运行一次,再次验证了上面的说法。

值得注意的是,示例对上面的D3DXFRAME,D3DXMESHCONTAINER两个结构做了扩展,分别代之以D3DXFRAME_DERIVED结构和D3DXMESHCONTAINER_DERIVED结构,以集中存储数据方便程序处理。

CreateFrame()处理比较简单,你只是new一个Frame对象空间,填入传进来的Name,其它内容由DX负责维护填充。

CreateMeshContainer()较为复杂。它的任务一是保存传入的网格数据数据,二是根据这些数据及蒙皮信息调用GenerateSkinnedMesh()函数生成蒙皮网格。只有这个新的BlendMesh才能在Render()时支持顶点混合,完成蒙皮的显示。在D3DXMESHCONTAINER_DERIVED结构中,用pOrigMesh保存旧的Mesh普通网格信息。而Meshdata.Mesh则指向新产生的BlendMesh

在这个函数中,多次用到了AddRef(),对COM不熟悉的新手容易困惑。D3D是COM组件,它在服务进程中运行,而不在当前的客户进程中。在DX组件运行过程中,要创建一系列接口对象,如CreateDevice()返回接口指针,这些接口及其占用内存什么时候释放,要通过“引用计数”的技术来解决。AddRef()给这个接口指针的计数加1,而Release()会将之减1。一旦减到0,表示没有客户使用了,相关的接口就释放了。 由此可知,每次调用Rlease()后,并不一定会释放内存,而是当引用计数归0时释放内存。
这样,对接口指针的使用,就像维护堆栈的平衡一样,要仔细,而且按照某种约定规则使用。

但平时D3D编程中,怎么不用AddRef()呢?这是由于一个接口指针,如ID3DDevice,或VertexBuf指针,都是D3DXCreate出来的,在Create时候,在内部已经事先AddRef()了,你就不需要再做这工作了。只要你在不用时,调用 p指针->Relase()就释放了。一般编程,特别是小型示例程序,都是初始化时建立一次,关闭时释放,都遵守了这种约定,所以不存在这种问题。

但在CreateMeshContainer()函数中,以多种方式使用了指针,在局部指针变量中来回传递,所以问题复杂化了。在COM编程中约定,任何时候地接口指针赋值(复制),都要AddRef(),在指针变量结束生命期前,再Release(). 但许多程序员都不是严格这么做。因为在局部变量用完就废了,先AddRef()增加计数再Release()减少,和直接使用最后是等效的。几乎是多此一举。这与编程习惯有关系。一旦引用计数不对,如果没有统一的习惯,不好排查。在CreateMeshContainer()中,对接口指针的使用有三种方式,例举如下:

方式一:不使用AddRef()。和普通指针一样,临时变量是左值,接口指针是右值,直接赋值使用。如:
        pMesh = pMeshData->pMesh;
        这是由于pMesh是局部变量,它只是临时引用一下,没必要为它先AddRef(),后Release()。

方式二:隐式的使用AddRef()。 由于用到了一些内部有AddRef()动作的函数,就要按照COM约定,在子程序结束前Release()
        pMesh->GetDevice(&pd3dDevice);//此处d3d设备引用计数已经加1
        ....
        SAFE_RELEASE(pd3dDevice);//--此处将引用计数减1,并不是真的释放d3d设备
        在本例中,pd3dDevice在GetDevice()中已经Addref()过了,所以,在退出CreateMeshContainer()前,必须pd3dDevice->Release()

方式三:显式的使用AddRef()。 如果一个指针值,不是由D3DXCreate出来的,而是通过赋值方式复制给一个全局变量或长期变量的。 所以,可以通过AddRef()的方式来延迟该对象的释放。因为,如果不AddRef(),极有可能在函数返回该对象就可能释放了。它就像一个加油站,使得传入对象的寿命延长至自己控制范围内。用了AddRef(),就要在相关的Destroy中添加Release()。

在本函数,有三处这样的语句:
        pMeshContainer->MeshData.pMesh = pMesh;
        pMeshContainer->MeshData.Type = D3DXMESHTYPE_MESH;
        pMesh->AddRef();
         ....
        pMeshContainer->pSkinInfo = pSkinInfo;
        pSkinInfo->AddRef();

        pMeshContainer->pOrigMesh = pMesh;
        pMesh->AddRef();
         ....

        将来在DestroyMeshContainer()中,要释放这些指针:
        ....
        SAFE_RELEASE( pMeshContainer->MeshData.pMesh );
        SAFE_RELEASE( pMeshContainer->pSkinInfo );
        SAFE_RELEASE( pMeshContainer->pOrigMesh );

        由于这些指针值的创建、更改等都是用户自己经营的,所以务必要加前后吻合,在CreateMeshContainer()中AddRef(),在DestroyMeshContainer()中Release().


再来看数据的保存部分。
在CreateMeshContainer()的传入参数中,有pMeshData,pMaterials,pEffectInstances,NumMaterials,pAdjacency,pSkinInfo
你需要把这些数据保存到自己的D3DXMESHCONTAINER对象中。并且其中的所有数组所需的空间都要在全局堆中new出来。所以在该代码中,有如下new:
pMeshContainer = new D3DXMESHCONTAINER_DERIVED;//自定义的扩展数据容器对象
memset(pMeshContainer, 0, sizeof(D3DXMESHCONTAINER_DERIVED));//初始化pMeshContainer,清0
    ...
pMeshContainer->pMaterials = new D3DXMATERIAL[pMeshContainer->NumMaterials];//准备保存材质
pMeshContainer->ppTextures = new LPDIRECT3DTEXTURE9[pMeshContainer->NumMaterials];//准备创建纹理对象。它声明在扩展部分。
pMeshContainer->pAdjacency = new DWORD[NumFaces*3];//准备保存邻接三角形数组,NumFaces = pMesh->GetNumFaces();

然后,对数据进行memcpy保存。pEffectInstances由于在绘制中不需要,并没进行保存。对于没有贴图的赋以默认材质属性。
值得注意的是,所有这些new,必须在DestroyMeshContainer()时进行delete.

接下来的处理中,如果发现Mesh的FVF中没有法向量,要用CloneMeshFVF()重建Mesh,计算顶点平均法向量。以备光照处理。

最后,我们看看蒙皮信息pSkinInfo的处理。这是重头戏。
如果发现pSkinInfo!=NULL,就准备着手从各个蒙皮骨骼信息创建SkinMesh.
首先,用扩展容器结构D3DXMESHCONTAINER_DERIVED中的各属性保存原Mesh指针值,pMeshContainer->pOrigMesh = pMesh, 因为接下来我们要创建SkinMesh替代原Mesh.然后,把SkinInfo中的各骨骼的偏移矩阵保存到pMeshContainer->pBoneOffsetMatrices中
      cBones = pSkinInfo->GetNumBones();
      pMeshContainer->pBoneOffsetMatrices = new D3DXMATRIX[cBones];
      .....
     每个“骨骼偏移矩阵”pBoneOffsetMatrices,在将来DrawMeshContainer()中是必须要用的。因为原始Mesh中的顶点数据乘以“骨骼偏移矩阵”,再乘以“变换矩阵”,才能求得各骨骼顶点在世界坐标系中的坐标。 即:
    骨骼上各点在世界坐标系中的新坐标=初始网格中的各点坐标*骨骼偏移矩阵*骨骼当前的变换矩阵
    其中,“初始网格中的各点坐标*骨骼偏移矩阵” = 骨骼上各点初始时刻在该骨骼坐标系中的局部坐标

做了以上工作后,调用GenerateSkinnedMesh(pMeshContainer),创建SkinMesh. 接下来,我们看看GenerateSkinnedMesh()做了哪些工作。

3.4 怎样生成蒙皮网格SkinMesh? GenerateSkinnedMesh()分析

由于要重定义pMeshContainer->MeshData.pMesh,所以先SAFE_RELEASE( pMeshContainer->MeshData.pMesh ); 释放原pMesh

在这个函数中,是根据当前绘图方式设置进行加载数据的。因为顶点混合,有无索引的顶点混合,有含索引的顶点混合,所使用的函数和对应的SkinMesh数据内容也有所不同。
在示例中,自定义了枚举m_SkinningMethod,主要分为D3DNONINDEXED和D3DINDEXED,以有纯软件渲染等。运行示例后,你可以选择菜单中的Options选择不同的渲染方式。

我们着重分析一下带索引的蒙皮网格。在程序中,就是D3DINDEXED相关的部分。
if (m_SkinningMethod == D3DINDEXED){ ....}

注意! 示例默认工作在D3DNONINDEXED下,如果要跟踪D3DINDEXED部分的代码,必须选择菜单中的Options选择indexed!


最主要的,要通过DX的ConvertToIndexedBlendedMesh()函数,生成支持“索引顶点混合”的SkinMesh.有关索引顶点混合的技术,你可以在DXSDK帮助文件中搜索“Indexed Vertex Blending”主题,对着英文和插图将就看,确有收获。

要想用硬件对顶点进行混合,那么参与混合者不能太多。也就是说同时影响一个顶点的骨骼数不能多。我们假定一个顶点最多同时受4个骨骼的影响(也就是同时最多有4个骨骼矩阵参与加权求和),那么同时影响一个三角形面的骨骼数最多就是3*4=12个。
我们用NumMaxFaceInfl表示影响一个三角面的最多骨骼矩阵数,那么,通过调用pSkinInfo->GetMaxFaceInfluences()获取这个数值,一般也就3-4。如果这个数值太大,我们强制使用NumMaxFaceInfl = min(NumMaxFaceInfl, 12);来最多取值12。

用NumMaxFaceInfl 这个数值干什么呢? 我们用来它分析当前的显卡倒底行不行。

if (m_d3dCaps.MaxVertexBlendMatrixIndex + 1 < NumMaxFaceInfl)//如果显卡达不到该要求
{
      //很奇怪。2005年底买的GeForce 6600GT显卡,竟然m_d3dCaps.MaxVertexBlendMatrixIndex=0, 不支持索引顶点混合!是驱动问题还是怎么了?
      //但它支持非索引混合。或者,也许要用HLSL支持混合。看起来,3D编程要多考虑。
       ..
      pMeshContainer->UseSoftwareVP = true;//用软件渲染顶点。显然不实用。
}
else
{
      pMeshContainer->NumPaletteEntries = min( ( m_d3dCaps.MaxVertexBlendMatrixIndex + 1 ) / 2,
                                                     pMeshContainer->pSkinInfo->GetNumBones() );//--什么意思?
      pMeshContainer->UseSoftwareVP = false;//采用硬件顶点混合。
      Flags |= D3DXMESH_MANAGED;
}

[评论]在上面有一行代码:
     pMeshContainer->NumPaletteEntries = min( ( m_d3dCaps.MaxVertexBlendMatrixIndex + 1 ) / 2,pMeshContainer->pSkinInfo->GetNumBones() );
尽管作者加了大段注释,还是让人一头雾水。其实,我们做一个实验,反尔更能理解它的用途。
第一步,你在这句话后面下一个断点,看一下在你机器上这个数值。我的ATI 9550显卡机器上是19。比tiny.x中的骨骼数35少很多。
第二步,你将上面=右边瞎填一个大于4的数字,比如6。编译后照样运行。而且效果上几乎看不出任何差别。
为什么会这样呢? 我们在绘制代码部分,看看这个数值起什么作用。
在DrawMeshContainer()代码中,我们查找D3DINDEXED相关的部分。在mesh各子集的DrawSubset()之前,有如下代码:
      for (iAttrib = 0; iAttrib < pMeshContainer->NumAttributeGroups; iAttrib++)
      {
                // first calculate all the world matrices
                for (iPaletteEntry = 0; iPaletteEntry < pMeshContainer->NumPaletteEntries; ++iPaletteEntry)
                {
                    iMatrixIndex = pBoneComb[iAttrib].BoneId[iPaletteEntry];
                    if (iMatrixIndex != UINT_MAX)
                    {
                        D3DXMatrixMultiply( &matTemp, &pMeshContainer->pBoneOffsetMatrices[iMatrixIndex], pMeshContainer->ppBoneMatrixPtrs[iMatrixIndex] );
                        m_pd3dDevice->SetTransform( D3DTS_WORLDMATRIX( iPaletteEntry ), &matTemp );
                    }
                }
       ...
      }
下面仔细评估一下这些代码.
先注意看其中奇怪的D3DTS_WORLDMATRIX()宏,我们以前还没这样用过。它是做什么用的呢?通过查DXSDK帮助,我们在Geometry Blending主题中找到相关说明,并在"Indexed Vertex Blending"主题中给出了内部实现原理。原来,当你用m_pd3dDevice->SetRenderState(D3DRS_INDEXEDVERTEXBLENDENABLE, TRUE);开启了索引顶点混合后,在硬件上就启用了“palette of matrices”,即矩阵寄存器组,它最多支持同时256个索引。就像过去用256色调色板来表现彩色一样。D3DTS_WORLDMATRIX()宏就是有256-511这256个数表示矩阵索引号。

这些矩阵参与如下计算:

V最终顶点位置=V*M[索引值1]*权重1 + V*M[索引值2]*权重2 + ....+V*M[索引n]*(1-其它权重和)

这个公式的来源,相信大家在众多资料上见过,不赘述。 当然,我们也可以用程序完成这个蒙皮计算过程,但逐个读顶点却很麻烦。现在是由硬件代劳了。我们只设矩阵就行了。
我们用m_pd3dDevice->SetTransform( D3DTS_WORLDMATRIX( iPaletteEntry ), &matTemp );这种方式设定各索引对应的矩阵。

那么权重呢?我们怎么设?原来在上面所说的DX提供的ConvertToIndexedBlendedMesh()函数中,生成SkinMesh时,各网格顶点格式FVF已经有变化了,增加了新格式,D3DFVF_XYZB2,D3DFVF_LASTBETA_UBYTE4,用以记录顶点对应的权重值以及矩阵索引。如下
struct VERTEX
{
    float x,y,z;
    float weight;
    DWORD matrixIndices;
    float normal[3];
};
#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZB2 | D3DFVF_LASTBETA_UBYTE4 |D3DFVF_NORMAL);

D3DFVF_LASTBETA_UBYTE4对应于DWORD数值,用于矩阵索引时,每个字节表示一个索引,最多可以允许4个索引,同时有4个矩阵参于该点的混合。如果一次绘制中涉及了9块骨骼矩阵,你可以把这9个矩阵全部用SetTransform设置到矩阵寄存器中,但每个顶点在渲染时,最多使用其中的4个。由此可知,pMeshContainer->NumPaletteEntries这个数值,确定了一趟DrawSubset绘制所用到的矩阵个数,个数越多,在一趟绘制中就可以纳入的更多顶点。所以,当我们减少pMeshContainer->NumPaletteEntries这个数值时,pMeshContainer->NumAttributeGroups数值就会增加。也就是说,一趟绘制中所允许涉及的骨骼数越少,那么子集的数量NumAttributeGroups就会增加,需要多绘几趟。
你可以在此下断点观察,当NumPaletteEntries=19时,NumAttributeGroups=3 当NumPaletteEntries=6时,NumAttributeGroups=12 当NumPaletteEntries=4时,NumAttributeGroups=31,几乎和无索引时的分组一样多了。

顶点中的权重weight存放了它当前骨骼的权重。(一个顶点对应的多个骨骼权重怎么存放?是不是在当前子集中有多个同样的顶点,权重不同,对应的矩阵索引不同,然后混合)


由上所述,ConvertToIndexedBlendedMesh()是一个很重要函数,由DX自动将Mesh顶点分组成多个子集,以便DrawSubset. 你必须把它的返回参数都记录下来,在绘制时使用。

 

四. 怎样绘制显示动画?

DrawFrame()用来绘制整个X框架。它遍历各个框架,找到Mesh不为空的进行绘制。(其实整个.x中通常只有一个不为空,见上文所述)
DrawMeshContainer()是绘制函数。

4.1 怎样开启顶点混合?
注意应用有关的Vertex Blending技术。如在索引方式的绘制中,
m_pd3dDevice->SetRenderState(D3DRS_VERTEXBLEND, pMeshContainer->NumInfl - 1);
其实是设定了D3DVBF_2WEIGHTS或D3DVBF_3WEIGHTS
注意要m_pd3dDevice->SetRenderState(D3DRS_INDEXEDVERTEXBLENDENABLE, TRUE);

4.2 矩阵的刷新:
首先,在FrameMove()调用m_pAnimController->SetTime()设置当前时间(或在DX9.0c中用AdvanceTime()设置时间差),从而刷新各个pFrame->TransformationMatrix,即骨骼转换矩阵
其次,调用UpdateFrameMatrices()做乘法累积,计算出各骨骼坐标系到根世界转换矩阵。
最后,在绘制前,将该转换矩阵左乘偏移矩阵,得到最终的转换矩阵。
      D3DXMatrixMultiply( &matTemp, &pMeshContainer->pBoneOffsetMatrices[iMatrixIndex], pMeshContainer->ppBoneMatrixPtrs[iMatrixIndex] );

由此可见,你如果注释掉了m_pAnimController->SetTime,画面肯定停了。

4.3 绘制输出 是在DrawMeshContainer()中,调用SkinMesh的DrawSubset进行绘制。一些细节内容如D3DTS_WORLDMATRIX(),在上面已经有说明,不再罗嗦。

 

4.4 关于示例中多种绘制方式分析
在示例中,用到了多种渲染方式,包括传统的非索引顶点混合,还有新兴的HLSL方式。而且我发现,ATI RADEON 9550 显卡MaxVertexBlendMatrixIndex=37,而价格更高的Gefoce 6600GT MaxVertexBlendMatrixIndex竟然为0,不支持index vertex blending!
所以,还是有必要分析一下该示例中各种vertex blending方式的处理,以便掌握多种绘制方式适应不同显卡。
经测试,示例中所涉及的多种方式,由慢到快,依次是以下几种:
    SOFTWARE,
    D3DNONINDEXED,
    D3DINDEXED,
    D3DINDEXEDVS,
    D3DINDEXEDHLSLVS,

从最慢的SW到最快的HLSL,大约相差20%,有时会大到40%。 差别不是特别悬殊的原因,主要是顶点混合并不是瓶颈。

关于顶点处理方式,是在创建D3D设备时指定的。共有三种方式:
   D3DCREATE_SOFTWARE_VERTEXPROCESSING 软件顶点运算  (简记 sw vp)
   D3DCREATE_HARDWARE_VERTEXPROCESSING 硬件顶点运算。必须有这项才支持有HAL (简记 hw vp)
   D3DCREATE_MIXED_VERTEXPROCESSING 混合顶点运算,即硬件+软件 (简记 mixed vp)

一旦用D3DCREATE_HARDWARE_VERTEXPROCESSING方式创建设备,就只能在硬件方式下进行顶点处理。如果调用m_pd3dDevice->SetSoftwareVertexProcessing(TRUE)来切换到软件顶点处理,HRESULT会返回失败。
  所以,如果你对客户的显卡没有足够的信息,就用D3DCREATE_MIXED_VERTEXPROCESSING方式创建设备。它默认工作方式是HAL。一旦发现进行某种绘制时硬件能力不够,就可以调用调用m_pd3dDevice->SetSoftwareVertexProcessing(TRUE)切换到软件模式。在示例中就是这么做的,启动示例后,运行在mixed模式下。

在Gefoce6600GT显卡中,由于D3DINDEXED方式不支持,采用了软件混合方式,在这种方式下速度甚至比SOFTWARE慢。HLSL还好,还是最快。

要确定设备的硬件顶点处理能力,可以参考D3DCAPS9结构的VertexProcessingCaps成员。可以获取下列属性
MaxActiveLights,MaxUserClipPlanes,MaxVertexBlendMatrices,MaxStreams,MaxVertexIndex

(1)D3DNONINDEXED方式:

首先看GenerateSkinnedMesh()中怎样创建蒙皮网格的。
这种方式下,用ConvertToBlendedMesh()建立蒙皮网格,而不是ConvertToIndexBlendedMesh()

为了绘制蒙皮,在这个函数中对Mesh各子集的顶点再次进行的分组。分组的标准是各顶点(或三角面)所涉及的骨骼矩阵个数不超过pMeshContainer->NumInfl个。(这个数字是由在ConvertToBlendedMesh()时,由参数pMaxFaceInfl返回的)。一个Mesh子集可能被拆开成多个分组。 最后,分组的属性保存在pBoneCombinationBuf中,如子集ID,该子集的各骨骼ID,起始三角面,三角面个数等供绘制时使用,分组的个数保存在pMeshContainer->NumAttributeGroups中。

接下来检查每个分组所涉及的骨骼数,是不是超过硬件允许的最大混合矩阵数---MaxVertexBlendMatrices。如果超过了就把所有分组截为两大部分,前一部分用硬件混合,后一部分采用软件混合。而且,一旦发现有需要软件混合,要采用CloneMeshFVF(D3DXMESH_SOFTWAREPROCESSING|...)的方式重新生成网格。

再来看绘制部分DrawMeshContainer()

用pBoneComb指向骨骼分组属性,扫描各分组。找出其中骨骼数满足硬件性能的用进行绘制。
然后开启软件顶点渲染m_pd3dDevice->SetSoftwareVertexProcessing(TRUE),对那些骨骼数超出硬件性能的进行绘制。
SetSoftwareVertexProcessing()需要当前d3d设备以D3DCREATE_MIXED_VERTEXPROCESSING方式创建。

(2)D3DINDEXED,这种方式上面分析过了,从略。用pMeshContainer->UseSoftwareVP表示是否采用软件绘制。
值得注意的是在这种方式下,一旦硬件性能不足,会彻底使用软件顶点渲染,而不是像上面一样拆为两部分。

(3)D3DINDEXEDVS,D3DINDEXEDHLSLVS
这种情况下使用了着色器和高级着色语言。超出本文主旨,讨论从略。

(4)SOFTWARE--软件方式? 让人有些迷惑,与上面的m_pd3dDevice->SetSoftwareVertexProcessing(TRUE)有何区别?

从代码看,这种方式下反而比较简单。GenerateSkinnedMesh()中,
先直接从原始Mesh克隆一个Mesh,然后读取它的材质属性数组。开辟一个空间m_pBoneMatrices,用以存放各块骨骼的转换矩阵。

在绘制时,从pMeshContainer中的变换矩阵乘以偏移矩阵,放在pBoneMatrices中。把这个矩阵数组,以原Mesh的顶点作为源顶点,以新克隆的MeshData.pMesh做为目标顶点,调用pSkinInfo->UpdateSkinnedMesh(),用软件方式计算各骨骼顶点的新位置(相当于软件计算方式蒙皮)。

然后调用MeshData.pMesh->DrawSubset()绘制。

可见,在SOFTWARE方式下,最终顶点的渲染还是HAL方式的,只不过蒙皮计算是由软件完成的。它和上面的m_pd3dDevice->SetSoftwareVertexProcessing(TRUE)直接设置软件顶点渲染还是有区别的。

posted @ 2009-11-16 00:47 麒麟子 阅读(1999) | 评论 (2)编辑 收藏

Shader Model 4.0 全新架构


Shader Model4.0统一渲染架构  
微软的DirectX 9.0c距今离它的诞生已经有2年的光景,DX9.0c给我们带来了全新的Shader Model3.0技术,也使得3D画面较以往了有了质的突破,DirectX 9.0c是截至至今微软历史上寿命最长的一代API,而图形技术的发展是不会停下脚步的,2006年微软发布了全新的DirectX 10.0,仅从版本上看比9.0c相差一级,但是DirectX 10.0带给我们的又将是一个全新的概念。

 

  在微软发布DX10.0后,NVIDIA积极响应,发布了完全符合DirectX 10.0的通用Shader架构图形处理器G80,也标志着DX9.0c将会逐步被DX10.0替代。相对DirectX 9.0c中的SM3.0,在Shader Model 4.0中微软引入了统一着色架构,这才是DX10最大的改变。我们都知道,微软在DirectX 9中引入的了2.0/2.X/3.0三个版本的Vertex Shader(顶点着色引擎)以及Pixel Shader(像素着色引擎)。其中支持2.0版的着色引擎是DirectX 9的GPU的最低标准,而当前主流的显卡已经都硬件支持加入了拥有更多高级处理功能的3.0版本着色引擎。

    不过,即便是DirectX 9.0c,对于功能相仿Vertex Shader、Pixel Shader来说,目前图形芯片厂商仍需要在GPU中划分两个区域来存放Vertex Shader阵列和Pixel Shader贴图流水线。这无疑是一种资源冗余,而且这也加重GPU的设计难度及成本。当DirectX 10把渲染流程更细分为Vertex Shader、Geometry Shader及Pixel Shader,这个情况将会更为明显。而DX10.0的诞生就将这2种渲染整合在了一起!


SM4.0较SM3.0的改进
 而在DirectX 10中引入了统一渲染架,通过一个整合Vertex Shader、 Pixel Shader的可编程整合光影处理器来完成目前Vertex Shader、Pixel Shader所有的工作。所谓统一渲染架构,最容易的理解方式就是Shader单元不再分离,显示核心不再为Shader类型不同而配置不同类型的Shader单元,对于主流的显示核心,Pixel Shader单元以及vertex Shader单元的概念都应该已经非常熟悉了,而在统一渲染架构中这两种Shader单元将不再分离,转而所有的Shader单元都可以为需要处理的数据进行处理,不管和是Pixel Shader数据还是Vertex Shader数据。

     而调配哪几组Shader单元负责处理什么数据或者进行什么样子类型的计算,则由一个被称为small sets of instructions(SSI)的部分来控制。这样在硬件上,设计者就无需为不同的着色引擎设计不同的执行单元,只要按照所对应的接口以及操作方式全部融为一体,仅设置一种独立的Shader执行单元。这意味着GPU厂家可以用更小的核心来实现现在需要用8000万甚至更多晶体管才能实现的功能!

  相比原先的Shader Model 3.0,Shader Model 4.0最大指令数从512条增加到了64000条;临时暂存器数量也从原先的32个增加到惊人的4096个;允许同时对128个Texture进行操作(Shader Model 3.0只允许16个);材质texture格式变为硬件支持的RGBE格式,其中的"E"是Exponent的省略,是RGB共同的说明,这在HDR的处理上有很大的作用,摒弃了以往需要专门decoding处理HDR渲染的流程。 另外,对于纹理的尺寸Shader Model4.0也有惊人的提升,8192x8192的最高纹理分辩率比原先最高2048x2048的分辩率要高出4倍。G80图形核心对以上规格都给予了完整的硬件支持。

Shader Model4.0新特性
 Shader Model 4.0另一个重大变化就是在VS和PS之间引入了一个新的可编程图形层----几何着色器(Geometry Shader)。原来的Vertex Shader和Pixel Shader只是对逐个顶点或像素进行处理,而新的Geometry Shader可以批量进行几何处理,快速的把模型类似的顶点结合起来进行运算。虽然其操作不会象Vertex Shader那样完整,只是处理器单个顶点的相关函数操作,但是这种操作却可以确定整个模型的物理形状。这将大大加速处理器速度,因为其它Shader单元将不再去需要判定数据所存在的位置,而只是需要简单的为特定区域进行操作就可以了。

 

  Geometry Shader可以把点、线、三角等多边形联系起来快速处理、同时创造新的多边形,在很短时间内直接分配给其他Shader和显存而无需经过CPU,烟雾、爆炸等复杂图象不再需要CPU来处理。从而极大的提高了CPU速度和显卡速度。游戏图象中可以出现许多精细场景,如不锈钢茶壶上清楚的反射出周围物体、超精细的人物皮肤等。

  为了最大程度的发挥Geometry Shader的威力,DX10硬件还专门设置了一个名为流输出层(Stream Output State)的部件来配合它使用。这个层的功能是将Vertex Shader和Pixel Shader处理完成的数据输出给用户,由用户进行处理后再反馈给流水线继续处理。我们可以通过Stream Out把GPU拆成两段,只利用前面的一段几何运算单元。对某些科学研究,也许可以通过stream out来利用GPU的数学运算能力,等于在CPU之外又平白多得了一个数学协处理器。举个例子,Doom3常用的Stencil shadow,因为CPU负担很重,广受批评。但是因为GS可以计算轮廓线, 还可以动态插入新的多边形,有了Stream out之后,Shadow volume的生成就可以放到GPU端进行,实现Stencil shadow的硬件化,这将大大降低CPU占用。

统一着色架构
在以前的DirectX版本中,像素着色器因为受到常量寄存器、可用指令和总体流程可的限制总是运行在顶点着色器之后,因此程序员必须学会怎样分别去利用好顶点和像素着色器的权限。Shader model 4.0则带来了与以往不同的统一着色架构,在DirectX 10基础上进行游戏开发,程序员不需要在避免着色冲突限制上花费时间,所有的统一架构着色器都能够使用GPU可以用的全部资源。

 

  Shader model 4.0在着色器程序可用资源的提升方面让人激动,在以往的DirectX下,开发者不得不仔细计算可用的寄存器资源,而在DirectX 10中,这些问题都不复存在,如上表所示,总体上DirectX 10提供了超过10倍的DirectX 9可用资源。

更多的纹理和渲染
Shader Model 4.0支持纹理队列集,把开发者从繁重的拼接纹理图集的工作中解放出来,并能够在每个着色器上使用更多的特殊纹理实现更好的视觉效果。

  在Shader Model 4.0之前,过高的开销使在一个着色器操作上使用多个特殊纹理的操作基本无法实现。为了解决这个问题,开发把许多小的分散的纹理拼接成一个大的纹理;在运行层中,着色器也需要进行额外的地址运算以便在拼接纹理图集中找到特定的纹理。纹理图集方式存在两个明显的缺点:首先小纹理之间的分界线回导致过滤操作错误;然后,DirectX 9的4096*4096纹理尺寸限制也是纹理图集的总体规模受到局限。纹理队列集能够解决所有问题,它能够使用队列格式存储纹理,每个队列能存储512同尺寸个纹理,最大的可用纹理尺寸也提升到8192*8192。为了促进这种应用,每个着色器可以操作的最大纹理数也提高到了128个,8倍于DirectX 9。

  更多的渲染对象
  多重渲染对象是DirectX 9时代的一个流行特性,它允许每个像素着色周期输出4个不同的渲染结果,从而高效率的在一个周期内渲染一个场景的4遍。在DirectX 10中,渲染对象的数目提高到8,着极大的提高了着色器能实现的场景复杂程度,延迟渲染和其它一些图像空间优化算法将广泛的从中受益。

两种新的HDR格式
 两种新的HDR格式
  HDR(High dynamic range rendering)从支持浮点色彩格式的DirectX 9时代开始流行。不幸的是浮点格式比整数格式占用更多的寄存器空间而限制了其性能的发挥。如典型的FP16格式的每个色彩数据需要占用16bits,这两倍于整数格式的空间占用。

 

 

  DirectX 10的新HDR格式能够在和FP16实现同样动态范围的前提下只占用50%的存储空间。第一种格式为R11G11B10,它使用11-bits的红色和绿色以及10-bits的蓝色来优化存储空间;第二种格式是使用一个5-bits共享首位存储所有色彩然后每个色彩拥有9-bits尾址,这些简化的方法在HDR品质上和标准的FP16几乎没有差别。在最高级别的HDR方面,DirectX 10支持FP32的HDR,这可以用于科学计算等对计算精度较高的应用程序。

  很显然,DirectX 10.0全新的Shader Model4.0对于消费者来说是一场全新的视觉革命,更逼真的3D游戏画面、流畅的高清视频回放是微软、显卡厂商推动技术发展的动力之源,在不远的将来我们就会体会到全新的DX10、SM4.0给我们带来的饕餮大餐。





posted @ 2009-10-16 17:47 麒麟子 阅读(2425) | 评论 (2)编辑 收藏

IDirect3DDevice9::SetClipPlane

突然看到这个函数。
HRESULT SetClipPlane(
  DWORD Index,
  CONST float * pPlane
);

虽然DX SDK上面有,但还是有很多朋友不喜欢看那些拉丁字母,我也顺便就记录一下吧。

参数:
第一个是索引,不用说了。

第二个是存着 A B C D的数组。
这个数组最后会用来构建 Ax+By+Cz+Dw = 0;平面。

然后顶点会根据自已的位置(x,y,z,w)来进行判断。如果Ax+By+Cz+Dw >= 0。则表示在平面前方,保留。反之则在后方,被裁剪掉。

值得注意的时,在固定管线使用平面裁剪的时候,是在世界坐标系中处理的。


而用SHADER的时候,是在裁剪空间中处理的。(即顶点输出的时候的坐标系)
貌似还是太抽象。比如顶点输入坐标是pos   此时的坐标变换阵是WVP,则 Output.pos = mul(pos,WVP);  那么,此时的裁剪空间就是Output.pos对应的坐标系空间。


另外,默认情况下D3DRS_CLIPPLANEENABLE 是没有打开的,应该在SetRenderState中手工打开。

值得注意的是:D3DXPLANE进行矩阵变换的时候,要将需要乘的那个矩阵进行求逆和转置,再相乘。SDK中代码如下
D3DXPLANE   planeNew;
D3DXPLANE   plane(
0,1,1,0);
D3DXPlaneNormalize(
&plane, &plane);

D3DXMATRIX  matrix;
D3DXMatrixScaling(
&matrix, 1.0f,2.0f,3.0f); 
D3DXMatrixInverse(
&matrix, NULL, &matrix);
D3DXMatrixTranspose(
&matrix, &matrix);
D3DXPlaneTransform(
&planeNew, &plane, &matrix);


上面的D3DXPLANE plane(0,1,1,0)如果你觉得不直观的话,DX提供了以下一些生成PLANE的函数
D3DXPLANE * D3DXPlaneFromPoints(
  D3DXPLANE 
* pOut,
  CONST D3DXVECTOR3 
* pV1,
  CONST D3DXVECTOR3 
* pV2,
  CONST D3DXVECTOR3 
* pV3
);

上面的PV1 PV2 PV3则是平面上的三个点。这个函数可以很容易地求得一个三角形所在的平面。

D3DXPLANE * D3DXPlaneFromPointNormal(
  D3DXPLANE 
* pOut,
  CONST D3DXVECTOR3 
* pPoint,
  CONST D3DXVECTOR3 
* pNormal
);

 

pPoint为平面上的一个点。 pNormal是平面的法线方向。
比如,你想创建一个水平平面,并且朝上。 则可以将pPoint传入0,0,0  而pNormal传入0,1,0即可。

posted @ 2009-10-11 16:34 麒麟子 阅读(2097) | 评论 (0)编辑 收藏

仅列出标题
共38页: First 17 18 19 20 21 22 23 24 25 Last