小T一边作自己的3D引擎,一边研究half-life2的图形引擎,留下点心得。
整体上half-life2给小T的感觉一是庞大,二就是难读,com方式组织引擎,小T也不敢妄加评价,只是小T觉得整个引擎非常的难读。小T只有借助调试器才勉强对half-life2的图形引擎有个大致的了解,至于怎么编译half-life2,怎么运行,以及怎样调试,小T就不详细的描述了,到网上随便一搜索就能找到一大堆,这里只是简单的看看他的图形系统。
当前的3D环境,想要提高渲染速度,一个最基本的方法就是要减少硬件的渲染状态的切换,今天小T就是讲hl2的渲染状态管理部分的。在hl2里面,渲染状态主要被放在了一个叫ShadowState_t的结构里面,这个结构对应了大多数的硬件渲染状态,渲染系统维护多个ShadowState_t结构,在必要的时候把ShadowState_t的内容真正的设置到硬件上面,从而减少硬件的状态切换。当然实际上却没有这么简单,上面只是个简单的介绍,下面开始详细的解释hl2怎么进行硬件状态管理。
hl2里面渲染状态管理的一个比较重要的类就是CTransitionTable,从名字上面看,这个类描述的是一种转换,他其实就是描述了,渲染过程中硬件渲染状态的转换流程,hl2把硬件状态的管理也放在了这个里面,举个例子来说可能比较清楚。
比如:整个场景里面有2个物品,第一个物品使用一组渲染状态,第二个物品使用另外一组渲染状态,我们先渲染第一个物品,设置成第一个物品的渲染状态,接着渲染第二个物品,设置第二个物品的渲染状态,这个就是正常的操作方式,如果第一个物品和第二个物品的渲染状态有相同的,那么有些设置renderstate的函数就不用调用,我们可以把渲染状态看作是一种状态机,从第一种状态转换到第二种状态,我们要作一些事情,如果我们能建立一张行动表,表里面记录状态转换的时候要执行的动作,那么我们就可以简化这种状态管理模型,这个就是CTransitionTable类完成的工作。
对于场景里面的每个materila(顺便说一句,hl2也是按照material的方式组织渲染动作的),都对应了一种渲染状态,这些渲染状态都被CTransitionTable类记录下来,每一个渲染状态对应一个唯一的id,那么material只用记录他对应的渲染状态id就能完成自己的渲染了。比如我们要渲染一个物品,我们告诉渲染系统我们使用的渲染状态id,渲染系统自动完成渲染状态的设置,自动完成渲染状态切换的最小化任务,然后我们调用一个drawprimitive就ok。实际上的操作也没有这么简单,我们再看看实际一点的东西。
hl2里面的渲染状态操作都被封装到了ShaderRenderState_t类里面
??struct ShaderRenderState_t
??{
???? RenderPassList_t m_Snapshots[4];//代表了一种渲染方式,缓冲4种渲染方式0=默认,1=带color数据,2=带alpha数据,3=color+alpha
????int m_Flags;???????????// 标志,
????VertexFormat_t m_VertexFormat;??// 顶点格式
????int m_VertexUsage;
??};
??struct RenderPassList_t
??{
????int m_nPassCount; ????????// 渲染pass的数目
????StateSnapshot_t m_Snapshot[MAX_RENDER_PASSES]; // 实际是一个short形式,就是渲染状态id
??};
解释下RenderPass这个东西,一个material在hl2里面渲染方式分成了4种,每种都对应了一个RenderPassList_t的类,而ShaderRenderState_t把这4种渲染方式都缓存了,嗯,举个例子,比如我现在想要按照默认的方式渲染一种material,我传递一个ShaderRenderState_t的结构给渲染系统,渲染系统根据我要求的渲染方式索引到正确的RenderPassList_t元素,使用里面记录的StateSnapshot_t数组里面的某个正确的id来获取到正确的渲染状态。
我们已经知道了渲染系统的客户端怎样要求渲染系统完成自己的渲染状态设置了,接下来看看渲染系统本身怎样完成这个工作,当渲染系统得到一个渲染状态id以后,就应该要获取到与之对应的渲染状态,并且正确的设置好这个状态,换句话说也就是要完成当前状态到新状态的切换,首先看看渲染系统怎么去找到正确的渲染状态,这就落在了IShaderAPI和CTransitionTable身上了,小T使用的是DX9的ShaderApi,所以这个任务是由CShaderAPIDX8这个类来完成了,在CShaderAPIDX8类里面有一个CTransitionTable类的数据成员m_TransitionTable,每当要获取到一个渲染状态id以后,ShaderApi就告诉m_TransitionTable要使用新的渲染状态了,并且传递新状态的id,到这里先停一下,一直小T都使用渲染状态这几个字在描述,那究竟hl2使用什么样子的数据结构来表示实际的渲染状态呢?ShadowState_t对应的是硬件状态,而还有一个东西对应了vs,ps的状态,他也是要在状态转换的时候进行设置的,渲染状态id对应的其实是SnapshotShaderState_t 结构:
??struct SnapshotShaderState_t
??{
????ShadowShaderState_t m_ShaderState;
????ShadowStateId_t m_ShadowStateId;
??};
??struct ShadowShaderState_t
?? {
????// The vertex + pixel shader group to use...
????VertexShader_t m_VertexShader;
????PixelShader_t m_PixelShader;
????// The static vertex + pixel shader indices
????int m_nStaticVshIndex;
????int m_nStaticPshIndex;
??};
??ShaderApi获得了新的渲染状态id以后,和当前的渲染状态id比较,如果不相同,则获取到ShadowStateId,如果也不相同,那就要设置新的ShadowState了,这个操作下面讲解。然后,ShaderApi保存当前的渲染状态id,保存ShadowStateId,同时设置ShadowShaderState,这些都设置完成了,硬件的渲染状态也就设置完成了,然后的任务就是调用DrawPrimitive等等函数完成绘制的时候了,这个绘制是由各个vs,ps完成的,关于这个部分,小T留到下次讲解。
??现在我们来看看shaderapi,怎么设置新的ShadowState,基本的方式就是获得两个状态之间的转换表,执行表里面规定的动作,而实际上也是这样进行的,那现在的重点就落在了转换表上面。
??CTransitionTable保存了当前渲染过程中所有可能的ShadowState(关于这些是怎么保存起来的,这些信息是怎么获取到了,下面讲解),然后再这些状态中间拉了一张网,每两个状态之间总有一个弧,同时保存了两个状态进行切换的时候要执行的操作,CTransitionTable的主要数据成员如下:
??CUtlVector m_ShadowStateList ; // 全部的ShadowState_t的vector,ShadowState的id作为这个vector的下标
??CUtlVector m_TransitionTable; // 状态转换表,TransitionList_t就代表了状态转换的操作.
??struct TransitionList_t
??{
????unsigned short m_FirstOperation;
????unsigned char m_NumOperations;
????unsigned char m_nOpCountInStateBlock;
????IDirect3DStateBlock9 *m_pStateBlock;
??};
??struct TransitionOp_t
??{
????ApplyStateFunc_t m_Op;
????int m_Argument;
??};
TransitionOp_t也放到了一个vector里面,而TransitionList_t的第一个和第二个成员能在这个vector里面寻址,从而定位到实际的操作,而TransitionOp_t定义的就是实际的操作,第一个成员是一个函数指针,执行操作就是调用那个函数指针,并且传递两个参数,一个是新的ShadowState_t,一个就是结构里面的另外一个成员。
总结下,CTransitionTable保存了全部的ShadowState_t,保存全部的TransitionOp,都是使用下表作为索引访问,再全部的ShadowState_t之间建立TransitionList,当要进行状态切换的时候执行状态之间定义的TransitionOp就完成了状态的切换了.那ShaderApi是怎么建立ShadowState_t表格和TransitionOP表格的呢?
ShadowState_t的表格是在material创建的时候完成创建的,创建了一个新的ShadowState_t了以后就会向转换表里面加入新的节点,并且设置好转换操作,而这动作却是借助Draw动作完成了。
下面结合源代码看看上面这些功能的具体实现。
先看看draw的流程,最常用操作就是建立一个DynamicMesh,然后使用一个MeshBuilder修改刚刚建立的DynamicMesh,然后调用mesh的Draw函数,我们就从这里开始。
void CDynamicMeshDX8::Draw( int firstIndex, int numIndices )
CBaseMeshDX8::DrawMesh(); // 调用自己类的函数,实际上是从父类继承来的
ShaderAPI()-DrawMesh( this ); // 调用CShaderAPIDX8的函数
m_pMaterial-DrawMesh( m_pRenderMesh ); // 转调用CMaterial的DrawMesh函数
ShaderSystem()-DrawElements( m_pShader, m_pShaderParams, &m_ShaderRenderState );//调用CShaderSystem函数
// 计算当前这次绘制操作的方式:普通?color?alpha?alpha+color?
int mod = pShader-ComputeModulationFlags( params );
g_pShaderAPI-SetDefaultState(); // 调用CShaderAPIDX8的函数
ShaderUtil()-SetDefaultState(); // 实际上回到了CMaterialSystem:SetDefaultState()
// 一系列的CShaderAPIDX8的函数,这些函数只是比较纪录,并不真正的修改硬件的状态
// 准备渲染,纪录当前的渲染操作,接下来的CurrentStateSnapshot()函数返回的就是当前渲染的状态id
PrepForShad