Ogre材质解析代码初步分析(四)

来源:互联网 发布:普中科技单片机教程 编辑:程序博客网 时间:2024/05/16 15:42

本文参考:http://www.cnblogs.com/yzwalkman/archive/2013/01/08/2848831.html 尊重原创


“语义分析”的核心功能是在ScriptCompiler::convertToAST()函数中完成的。为了正确理解它,需要先来明确以下几个问题:

1.材质的格式

①命名格式:关键字+对象名+ “{” + 对象数据 + “}” 

比如材质脚本里面的“material”、“technique”、“pass” 

material  M{technique T(这个命名可以省略,则为“0”){pass P(这个命名可以省略,则为“0”){}}}

上面例子除了最外层的material 名字一定要存在之外,其他的命名是可以省略的,Ogre会按照先后顺序命名为索引序号,如上面的“0”。注意脚本的名字是全局唯一的,Ogre只会加载第一份同名的材质。

②派生继承关系符“:”,类似C++里面的继承关系,用于复杂父脚本的相关属性。子脚本也可以重载已定义的重名属性。

③变量定义符($)和变量赋值符(set)——前者表示引入一个变量对象,后者用以表示对此变量对象赋以指定值

④脚本引用标识符(import)——在一个脚本中要引用另一个脚本中定义的对象时,需要用import关键字导入相关脚本文件,如 import base_receiver from "pssm.material"


2.解析过程各个阶段的对应关系

“语义分析”的最终结果是生成AbstractNodeList,代码如下:

/** This enum holds the types of the possible abstract nodes */enum AbstractNodeType{ANT_UNKNOWN,ANT_ATOM,ANT_OBJECT,ANT_PROPERTY,ANT_IMPORT,ANT_VARIABLE_SET,ANT_VARIABLE_ACCESS};class AbstractNode;typedef SharedPtr<AbstractNode> AbstractNodePtr;typedef list<AbstractNodePtr>::type AbstractNodeList;typedef SharedPtr<AbstractNodeList> AbstractNodeListPtr;
与上一篇列举的ScriptToken与ConcreteNode比较一下可以发现,在ScriptNode阶段,左大括号(TID_LBRACE)与右大括号(TID_RBRACE)到了ConcreteNode阶段变为了CNT_LBRACE和CNT_RBRACE所标识的对象,而在AbstractNode阶段中则不再有相应的对象出现。这是因此为左右大括号,是为了定义数据单元的数据,在第三个阶段,相关的数据单元已由ANT_OBJECT所标识的对象来表示,所以大括号就无需单独表示出来。同样的道理,TID_COLON与CNT_COLON所标识的两个不同阶段的对象,相互对应。但到了第三阶段,代表继承含义的冒号(colon)已经随着派生对象(仍由ANT_OBJECT)的生成,而无需再存在,故而在AbstractNode中不再有相应的结点类型与之对应。另外,在第二阶段和第三阶段出现的由CNT_IMPORT和ANT_IMPORT标识出的对象,有对应关系,但在第一阶段中却并无相关对象与之对应。这是因为,import作为脚本的一个词素,在第一阶段中是被解释成了一个词(word),由TID_WORD所对应的结点与对应。关于import的处理过程,是很有意思的,后面会加以讨论。


AbstractNode是一个抽象类,以此为基类,Ogre定义了AtomAbstractNode、ObjectAbstractNode、PropertyAbstractNode、ImportAbstractNode、VariableAccessAbstractNode五个派生类,其type值分别为:ANT_ATOM、ANT_OBJ、ANT_PROPERTY、ANT_IMPORT、ANT_VARIABLE_ACCESS。Ogre根据第二阶段生成的ConcreteNodeList中的ConcreteNode对象,创建相应的AbstractNode的派生类对象,并将其保存在AbstractNodeList中。这一过程是由ScriptCompiler::convertToAST()函数启动的。其代码如下:

AbstractNodeListPtr ScriptCompiler::convertToAST(const Ogre::ConcreteNodeListPtr &nodes){AbstractTreeBuilder builder(this);AbstractTreeBuilder::visit(&builder, *nodes.get());return builder.getResult();}
AbstractTreeBuilder类是ScriptCompiler的内嵌类,定义如下:
class AbstractTreeBuilder{private:AbstractNodeListPtr mNodes;AbstractNode *mCurrent;ScriptCompiler *mCompiler;public:AbstractTreeBuilder(ScriptCompiler *compiler);const AbstractNodeListPtr &getResult() const;void visit(ConcreteNode *node);static void visit(AbstractTreeBuilder *visitor, const ConcreteNodeList &nodes);};
它的主要作用是生成相应的AbstractNode实体类对象,并将相应指针保存在内部的AbstractNodeList容器中。AbstractTreeBuilder又被定义为其宿主类ScriptCompiler的firend以便于对相关数据的访问。
具体过程请参考visit函数源代码。

经过以上三个阶段的处理,借由AbstractTreeBuilder类对象,Ogre终于“理解”了指定的脚本文件究竟包含了哪些脚本对象,以及这些脚本对象间的相互关系。需要强调的是,此时各脚本对象所包含的内部数据并未被加载。在Ogre中,脚本对象与脚本对象内部数据,这两个概念是严格区分的。脚本对象与这个脚本对象中的数据,对应着脚本文件中的“同一段”内容。但脚本对象是解析过程中必不可少的一个数据抽象,只有借助它,才能有一个完整的“语义分析”过程,或者说“语义分析”才有一个落脚点。而脚本对象中的数据,以材质(material)为例,是要参与到后期的渲流程中去的;material实际上是实体类(Entity)的一个属性,material数据一旦被解析出来,就完全脱离了“脚本”和脚本对象而独立存在了。因此,对于脚本所描述的材质对象,在Ogre中有两个对象类型与之对应,它们分别是ObjectAbstractNode和Material。

从以上的讨论可以看出Ogre实际上是——根据概念来组织代码,而非根据代码来组织概念。在很多情况下,我们往往会跳过AbstractNode这一层的抽象,而直接根据之前的解析结果,在代码中创建出相应的Material对象。如果不考虑对脚本中Import、set及派生关系的解析,则这种想法是可行的。可是,与多抽象出一个AbstractNode相比,这种做法会失掉很多的灵活性,同时也会使更多的代码纠缠在一起。如果待解析的脚本对象结构发生了变化,那么按照Ogre的做法,整个解析流程大的框架无需作任何修改,只要对AbstractNode的生成和脚本对象内部数据分析的过程作相应调整即可;而取消AbstractNode类,这种调整就会变得复杂的多。Ogre这种概念抽取,相互独立的方式体现了面向对象设计的精髓,根据概念来组织代码会使代码的组织结构更清晰,代码也更容易扩展和修改。不过事物总是有两面性,大量抽象出概念并依此构建代码也容易造成功能实现时,调用堆栈过深的情况。Ogre有时调用堆栈较深,与它的这种设计思想是有关系的。

在根据AbstractNode来创建相应的数据对象之前,还有一些工作要做:一是,有继承关系的脚本对象要将其基类对象的相关数据包含进来;二是,通过Import引入的脚本对象,在构建出相应的ImportAbstractNode对象后,还要做进一步的相应处理;三是,对于通过'$'定义的变量,要用实际的数据对象进行替换。

派生类对象获取其基类对象中的数据,由void ScriptCompiler::processObjects(Ogre::AbstractNodeList *nodes, const Ogre::AbstractNodeListPtr &top)函数来完成,具体参考Ogre源代码。

派生和导入,此次学习过程不需要了解太深,所以就略过了。



0 0
原创粉丝点击