自从以前看了clayman的博客后,就很想把自己代码里dx的effect框架替代掉。如今实习结束正好有短暂的自由时间,所以就ogre的material开刀吧。
山寨代码也是有学问的,如果你想搞明白这些代码的含义的话,最后之前就对这些已经有了一定的使用经历和认知,第一步先得自己想一下material系统的组成:
material应该包含的:1Material->n Techinque->(n Pass + extra params)->(n TextureUnit + m or 0 shaderUnit + n RenderStates(部分)),Pass里的TextureUnit又包含了这一组texture的相关信息和设置,TexcoordSet, AddressMode, FilterSetting等等, ShaderUnit则是对应于vs,fs以及新的gs,普通点也就最多两组。shaderUnit包含了具体的shader和其shaderParams,为何要把shader和shaderParams分开呢,不说设计优雅的考虑,简单的说就是方便,很多时候params可能会需要共享,譬如shaderA里用到mvpMat,和eyePos, diffuse,specular,可能shaderB也只用到这些,但是两个shader的内部计算并不相同。到此我们只分析了作为程序中的材质的结构,还需要考虑如果编写一个类似fx格式的material script,这也是另外一个大头,简单的说就是要做个脚本解释,然后给shader参数加上语义,减少大部分需要手动更新的shaderParams。鉴于我如今还没把这块扒完,等下次再写这块吧。
当你至少能闭着眼睛想透这些了,那就可以开始看ogre的material system了,先说句题外话,我读ogre的代码其实读挺久了。一直觉得很难读,开始觉得是自己水平不够,但后来实习才发现很多人都有这想法,甚至wolfgang对此也有批评,而自己曾经写了个olong引擎,接触过ipad编程的人应该对这些代码都比较熟悉~ogre里用了大量的设计模式,使得整个渲染流程从不同类里跳来跳去(SceneMgr这块看着最累了。。其实有很多地方看着都很累。。)个人觉得应该把render从SceneMgr里抽离出来做个RenderMgr,(记住这里的RenderMgr的概念和ogre的RenderSystem,后者是提供底层渲染API的接口,而前者是管理renderData并最终递交给RenderSystem,以前也见有别人博客讲过这个问题)让SceneMgr专门管理Scene。。
打住打住。。我是来写material的。。咳咳。。相当于渲染来说,ogre的material system还是相当易读的,也是因为这些模块相互关系几乎都是单向的,我们开始编写的时候可以先不考虑Technique,就是直接1 Material->n Pass->...当然甚至你都可以把multi pass去掉,就等同于1个pass,当然由于pass都代码实际也没多少,我们还是加上好了。
原始渲染的伪代码就是:
1for each pass
2{
3 setRenderStates(like alphe belnd, depth..)
4 for each texture unit
5 setTexSettings();
6 if(hasVertexShader())
7 {
8 bindVertexShader();
9 setShaderParams(vs);
10 }
11 if(hasFragmentShader())
12 {
13 bindFragmentShader();
14 setShaderParams(fs);
15 }
16 drawPrimitives();
17}
加入的RenderQueue也是一样,只是pass和对应的renderData我们是从renderqueue里取。
看到这里你会觉得,嘿,很简单嘛,但如果你看了ogre里关于shader参数的细节,你就知道这块还是比较复杂的。我们刚只说了一个运行过程,而建立params到constant registers的关系,以及如何编写autoConstants的代码都是魔鬼的细节~~对于dx,我们可以简单点,直接使用ID3DXConstantTable,它提供了set接口。但ogre则是直接从ID3DXConstantTable中解析参数。
设计这块的时候,脑子里先要对我整个过程有个清晰的认识:一方面是从shader里(我们这里假设是hlsl,asm先不考虑),另一方面则是解析参数读入材质脚本里定义的参数(named,autoNamed,或者index),最后将两者对应上。
0.类型解释
看过OgreGpuProgramParams.cpp/h的人都知道:GpuConstantDefinition,GpuNamedConstants,GpuLogicalIndexUse,GpuSharedParameters,GpuSharedParametersUsage,一堆的struct和class。。其实按照我们上面这个思路逐步加断点分析,代码也没那么繁琐。暂时不看GpuSharedParameters这块,大概看下GpuConstantDefinition,重点变成员是physicalIndex和logicalIndex,前者的解释是buffer中的起始地址,每个GpuProgramParameters里都包含了vector<int>和vector<float>两个buffer,他们存储了shader变量的值,而physicalIndex就是对应vector的索引。而logicalIndex则是我们后面将hlsl编译成asm后这些变量所绑定的寄存器id,而由于constant reg的size都是4,即一个register为4个float的大小,所以这些logicalIndex则不一定连续,譬如第一个uniform为float1那么第二个uniform的logicalIndex虽然是1,但实际跨过了4个float,而实际内存中我们的float(int同) buffer里,两个变量之间的则就空了3个float,这就是physicalIndex的作用,所以我们还需要建立一个从logicalIndex到physicalIndex的map,也就是两个GpuLogicalBuffersStructPtr对象(这两个对象和GpuNamedConstantsPtr在GpuProgram类和GpuProgramParameters是共享的,都是sharedPtr)
1.解析shader的变量
我们从再具体的读取代码来看,假设我们已经从脚本里读入了对应的shader,经过一堆调用后(略。。)在D3D9HLSLProgram的函数buildConstantDefinitions->processParamElement将每个参数的registerIndex,physicalIndex, type(原子类型,即float, int,),size都写入GpuLogicalBuffersStructPtr和GpuNamedConstantsPtr对象里(后者当然是只有highLevelShader里才有),并会为这个定义同样在插入带下标的一对键值,以便于如果我们传入的参数是数组(譬如float4x4[5]),则在程序里可以通过数组下标访问对应的数组变量。再constantDef建立好之后,将GpuNamedConstantsPtr传入到GpuProgramParameters对象里,然后向其中的两个vector添加上述对象大小的空间,在将两个LogicalToPhysicalMap的指针也传到shaderParams里,这样整个GpuProgramParameters几乎就完工了。
2.解析材质里定义的params,并将其与前者对应上
如今就差再把material script定义的param解析出来并与之前shader里的变量进行对应。材质脚本读出的每个变量的信息包含了param类型(named,auto_named,index,auto_index),name,type(float,int..),autoType(auto的才有),初始值等。我们根据其autoType在全局变量AutoConstantDictionary中查找是否有定义,然后调用set[Named]AutoConstant,在这里我们在会在我们之前从hlsl里解析出的GpuNamedConstantsPtr里查找是否有该名字的变量(毕竟shader里的变量才是最终有用的,甚至由于编译器优化的关系,由于shader编写者可能定义了一个变量而未实际使用,在编译shader后,这个变量实际是会被省略的,这样在GpuNamedConstantsPtr也就找不到了)然后调用_setRawAutoConstant将这个变量加入到这个GpuProgramParameters对象所持有的autoConstant中。
(ps:关于_getFloatConstantLogicalIndexUse的用途我没看懂,为何需要去在mFloatLogicalToPhysical里去查找这个logicalIndex是否存在呢,我觉得如果走到这一步肯定shader里肯定就是定义了的,因为这个logicalIndex是由mNamedConstants里取出,我觉得是没必要的。。)
3,更新autoParamsbuffer并最终在DP前进行更新
把上面的过程看懂了,下面就很好理解了。GpuProgramParameters::_updateAutoParams里就是对GpuProgramParameters对象所持有的autoConstant值进行更新,当然这是我们只是对GpuProgramParameters持有的buffer里的值做更新,最终在文章开头的伪代码中的setShaderParams(ogre里叫bindGpuProgramParamters)而这里的更新就轻而易举了,我们遍历logicalToPhysicalMap对每个param取出对应的logicalIndex,dataPointer,vector4ofCount,然后调用SetVertex/PixelShaderConstantF/I即可。
呼。。终于把这块写完了。。上述只是一个实现自己的material的基本思路。关于优化的话,我开头推荐的clayman的文章里有很多叙述。关于怎么写ogre script translator,等下一篇再说。。