您的位置:首页 > 其它

翻译:Panda3D Manual/V. Programming with Panda/A. The Scene Graph

2008-04-15 18:15 1246 查看
场景组织
Scene Graph节点树
很多简单3维引擎都使用一个模型列表(list)来组织场景,渲染时首先分配(或从磁盘载入)一个3维模型然后插入模型列表。在没有插入列表之前,模型对渲染器(render)来说是“不可见”的。
Panda3D的场景组织要复杂一些,它使用树结构而不是列表。同理,插入到树里之前,物体对渲染器不可见。
这棵树由PandaNode类的对象构成,该类作为ModelNode、GeomNode、LightNode等许多类的超类。本手册里,我们通常把这些类的对象笼统地称为节点(node)。这棵树的根节点叫render。(注意:还有其他根节点用于别的用途)
Panda3D这棵“渲染树”被命名为scene graph。
Scene Graph层级结构
下面列出关于scene graph层次组织的几点重要信息:
1.由你决定将节点插入树的哪个位置,根据插入的位置,你的“树”可以纵向生长也可以横向生长。
2.节点的位置通过相对其父节点的位置来指定。例如,一顶3维模型帽子戴在人头顶5个单位处。将帽子作为头的孩子,它的位置设成(0,0,5)。
3.父节点的渲染属性(rendering attribute)会传给孩子节点。例如指定一个节点的以雾效果来渲染,则它的孩子节点也将渲染雾,除非你明确地覆盖(override)子节点的属性。
4.Panda3D为树上的每个节点生成包围盒(bounding box)。好的分层结构可以加快视棱体(frustum)和遮挡剔除(occlusion culling)。如果整条树枝都处在视棱体外,就没必要检查它的孩子。
初学者通常把树完全平铺——所有的东西都直接插入根节点下。一开始这么设计挺好,渐渐地你需要增加树的深度。但是,除非有充分理由,不然不要让树的结构变得复杂。
NodePath
Panda3D提供了一个辅助性的类NodePath,它包含一个节点的指针以及一些管理信息。后面我们会解释管理信息的作用。Panda设计者的意图是把NodePath当作节点的句柄。任何创建节点的函数都返回一个关联该新节点的NodePath。
NodePath不是节点的指针,它是节点的“句柄”。从概念上只是人为的区分,实际上是同一个东西。但有些API函数需要你传入NodePath,有些函数则需要传入节点的指针。为此,尽管两者在概念上没有分别,你还是得知道同时存在这两样东西。
你 可以通过nodePath.node()将NodePath转化成“常规”指针,但却无法从指针转化成NodePath。因为有时候需要 NodePath,有时候又需要节点的指针,所以你最好保留NodePath而不是节点的指针。当传递参数时,你可以传入NodePath,也可以随时根 据需要把NodePath转化为指针。
NodePath方法与节点方法(NodePath-Methods and Node-Methods
很多NodePath的方法适用任何类型的节点,但某些类型的节点如LODNode和Camera(举例)拥有自己独有的方法,因此在调用这些方法时要先转成该类型的指针。
例如:
#调用节点的一般方法:
myNodePath.setPos(x,y,z)
myNodePath.setColor(banana)

#调用LODNODE节点独有方法:
myNodePath.node().addSwitch(1000, 100)
myNodePath.node().setCenter(Point(0, 5, 0))

#调用CAMERA节点的特定方法:
myNodePath.node().setLens(PerspectiveLens())
myNodePath.node().getCameraMask()
记住:当你调用NodePath方法时,实际上是对它所指向的节点进行操作。
上例中,首先将NodePath转换为节点,然后再调用节点方法。这是我们推荐的做法。

Scene Graph 操作(Scene Graph Manipulations
默认的Scene Graph
默认情况下,Panda3D有2种不同的scene graph,根节点分别是renderrender2d
大部分时候我们使用render,场景里的物体必须以render(或者其他以render为父节点的节点)为父节点。
render2d用于在屏幕上显示2维GUI元素,如文本和按钮。所有render2d的子节点都在3维场景之上渲染,就像在屏幕上作画一般。
render2d使用左下角(-1,0,-1)到右上角(1,0,1)的坐标区域,是个正方形。对于不是正方形的GUI,我们有一个render2d的子节点aspect2d,非正方形元素以它为父节点,可以设置长宽比。大部分情况下,GUI元素以aspect2d为父节点。
最后,还有一种顶层节点hidden。它是一个没有设置渲染属性普通节点,以它为父节点的物体将不被渲染。旧的Panda3D代码需要使用hidden来从scene graph中移除节点。但现在不需要这么做了,nodePath.detachNode()移除某个节点。
模型载入
你可以从文件名载入模型,利用Panda文件名语法找到模型的egg和bam文件。很多例子里都没有给出文件的扩展名,Panda3D将寻找扩展名为.egg或.bam的文件。
myNodePath = loader.loadModel('my/path/to/models/myModel.egg')
载入同一个模型的拷贝,避免每次从磁盘读取:
teapot1 = loader.loadModelCopy('teapot.egg')
teapot2 = loader.loadModelCopy('teapot.egg')
teapot3 = loader.loadModelCopy('teapot.egg')
第一次调用loadModelCopy方法时,模型被保存到内存,之后从内存直接载入,避免从磁盘读取。
以上调用适合加载静态模型;载入动画模型请参考Loading Actors and Animations一节。
Reparenting nodes and models
myModel.reparentTo (render)将模型插入到render父节点下,以便在场景里显示出来。移除模型使用myModel.detachNode()。一旦你对场景组织熟 悉之后,就会发现向下生长的“树”有很多优点,新节点不光可以插入render节点下。有时,创建一个空节点有利于把几个模型组成一组:
dummyNode = render.attachNewNode("Dummy Node Name")
myModel.reparentTo(dummyNode)
myOtherModel.reparentTo(dummyNode)
因 为节点从父节点继承位置,因此你在插入节点时会无意中改变节点在场景中的位置,要避免这种情况,需要使用myModel.wrtReparentTo (newParent) 方法,wrt前缀表示with respect to,它自动将模型坐标变换到父节点的坐标系,使模型的世界坐标保持不变。wrtReparentTo使用浮点矩阵运算,因此会产生误差,对同一节点频繁 使用该方法将使误差累积到一定程度,物体外观发生细微改变。初学者很容易滥用这个方法,我们应该谨慎使用。
普通状态改变(Common State Changes
Panda3D默认的坐标系是X指向右,Y指向屏幕里,Z指向天空,物体的旋转通常用三个欧拉角Heading、Pitch和Roll描述,角以度为单位。喜欢用四元数的人可用 setQuat() 方法指定旋转量。
节点位置:
myNodePath.setPos(X,Y,Z) #位置
myNodePath.setHpr(H,P,R) #旋转
节点缩放:
myNodePath.setScale(uniform)
myNodePath.setScale(SX,SY,SZ)
单个方向的位置、旋转和缩放:
myNodePath.setX(X)
myNodePath.setY(Y)
myNodePath.setZ(Z)
myNodePath.setH(H)
myNodePath.setP(P)
myNodePath.setR(R)
myNodePath.setSx(SX)
myNodePath.setSy(SY)
myNodePath.setSz(SZ)
或一次性设定:
myNodePath.setPosHprScale(X,Y,Z,H,P,R,SX,SY,SZ)
查询位置或单个坐标:
myNodePath.getPos()
myNodePath.getX()
myNodePath.getY()
myNodePath.getZ()
自定义一对键值来保存信息:
myNodePath.setTag('Key', 'Value')
print myNodePath.getTag('Key') #返回 'Value'
设定或查询相对于另一节点的位置
myNodePath.setPos(otherNodePath, X, Y, Z) #相对otherNodePath的(X, Y, Z)位置
myNodePath.getPos(otherNodePath)
沿X轴移动3个单位可这样写:
myNodePath.setPos(myNodePath, 3, 0, 0)
设定和查询相对位置是Panda场景组织的一个强大特性。
lookAt()方法旋转模型使之朝向另外的物体。
myNodePath.lookAt(otherObject)#myNodePath的Y轴正向指向otherObject
颜 色设置方法myNodePath.setColor(R,G,B,A),颜色值为0到1的浮点数,0为黑,1为白。如果模型有贴图,则设置颜色可能看不出 变化。将颜色设成白色可以恢复贴图的本来面目,但最好用myNodePath.clearColor()来清除当前的颜色设定。第四个分量alpha代表 透明度,1为完全不透明,要使alpha值生效,必须首先打开透明度,用myNodePath.setTransparency (TransparencyAttrib.MAlpha)方法,关闭透明度myNodePath.setTransparency (TransparencyAttrib.MNone)。不到万不得已不要打开透明度,因为该模式渲染代价高。如果模型的顶点有颜色,则用 myNodePath.setColorScale(R,G,B,A)使原有颜色与该颜色相乘。一个典型应用就是将setColorScale()应用到 render节点来使整个场景变暗,达到淡出效果。
因为alpha很重要,因此提供myNodePath.setAlphaScale(SA)方法来单独地缩放alpha值。
可以暂时不渲染某个物体:
myNodePath.hide() #隐藏
myNodePath.show() #显示
此时,myNodePath的子节点也跟着隐藏或显示。
操纵模型的一部分(Manipulating a Piece of a Model
每 个载入的模型都成为渲染树上一个ModelNode节点,在它之下有一个或多个GeomNode,它们才真正包含模型的多边形数据,如果要对模型的某一部 分进行操作,比如对某部分更换纹理贴图,你需要获得该GeomNode的指针。在这之前你得保证那部分模型在一个单独的GeomNode里,Panda的 优化机制会合并模型的分离部分,因此要注意了。
动画(骨骼动画)模型
对于动画(骨骼动画)模型,我们通过Actor接口载入——Panda将合并模型使节点最少,为了标示一个独立的部分,应该使用 egg-optchar (optimize character)程序。例如:
egg-optchar -d outputDir -flag Sphere01=theHead modelFile.egg anim1.egg anim2.egg#在建模程序里标签为“Sphere01”的模型的头部将保存分离。
注意必须给egg-optchar同时提供模型文件和它所有的动画文件,egg-optchar输出到-d指示的文件夹。-flag参数保证Panda不重新排列指定的多边形集合,并且为这个多边形集合分配一个名字,一旦给几何体贴上标签,就可以从标签获得指针:
myModelsHead = myModel.find("**/theHead")
这样你可以对模型的头部进行单独操作,例如使用setPos移动位置,用setTexture改变贴图,可以像对待节点一样对它进行操作。
非动画(环境)模型
对于静态(环境)模型,由于不包含骨骼或动画,Panda载入时并不进行优化,因此建模时的独立部分仍然是独立的,但Panda不保证它不合并节点,除非你告诉它不要合并。保护节点的办法是在egg文件里插入<Model>标志。
搜索Scene Graph(Searching the Scene Graph
nodePath.ls()方法列出此节点所有的子节点,包括子节点的子节点,直到输出整棵子树,它还列出每个节点的变换和渲染属性(Render Attribute)。该方法在交互运行python时很有用,是检查scene graph的好办法。
find()和findAllMatches()方法分别返回一个NodePath和一个NodePathCollection。它们以一个path字符串为参数,根据名字或类型来搜索。最简单的方式是path由一系列用斜杠分开节点名字组成,还可以包括下面这些符号:
* 一个节点
** 0个或多个节点
+typename 该类型或从该类型派生的节点
-typename 当且仅当该类型的节点
=tag 拥有这个标签的节点
=tag=value 有这个标签且标签的值等于value的节点
标准的文件名字符,如*、?和a-z都可用。在节点名前加@@表明该节点是个贮藏(stashed)节点,贮藏节点不被返回,@@*表示任何贮藏节点。
参数还可以接着控制标志,在参数后加一个分号,后面至少接一个标志,标志间不加空格或标点。
-h 不返回隐藏节点
+h 返回隐藏节点
-s 不返回贮藏节点除非用@@指明
+s 没有@@指明也返回贮藏节点
-i 节点名字区分大小写
+i 节点名字大小不敏感,但只对名字如此,类型和标签仍然区分大小写
默认的标志为+h-s-i
find()方法搜索一个匹配的节点,若有多个匹配则返回最近的那个,若没有匹配的节点则返回一个空的NodePath。findAllMatches()返回所有匹配的节点,最近放在第一个。
nodePath.find("<Path>")
nodePath.findAllMatches("<Path>")
例子:
nodePath.find("house/door") #查找名为door的节点,该节点是house节点的子节点,而house是搜索开始节点的子节点
nodePath.find("**/red*") #查找任何位置(开始节点下)以red开头的节点
getParent() 方法返回父节点的NodePath,getChildren() 返回当前节点的子节点的NodePathCollection,getChildrenAsList() 以列表形式返回子节点。
例子:
for child in nodePath.getChildrenAsList():
print child

#搜索Scene Graph直到找到某个节点
while nodePath.getParent()!=someAncestor:
nodePath=nodePath.getParent()
nodePath=nodePath.getParent()
渲染属性(Render Attributes
Panda 使用一个属性集来决定几何体的渲染方式,整个属性集被称为RenderState,决定了物体的颜色、纹理、光照等属性。这些属性都可以保存在scene graph任何一个节点里,对节点设置的属性也会应用到它的所有子节点。(除非你在effect里override,那是另一种高级应用)
也可以直接给节点分配属性:
nodePath.node().setAttrib(attributeObject)
大多数情况下,一些常用的属性的设置都有更方便更直接的方法(如nodePath.setFog()),也有对应的移除属性的方法(nodePath.clearFog())。下面列出Panda3D目前所有渲染属性的设置方法:
AlphaTestAttrib -
ClipPlaneAttrib nodePath.setClipPlane(planeNode)
ColorAttrib nodePath.setColor(r, g, b, a)
ColorBlendAttrib -
ColorScaleAttrib nodePath.setColorScale(r, g, b, a)
ColorWriteAttrib -
CullBinAttrib nodePath.setBin('binName', order)
CullFaceAttrib nodePath.setTwoSided(flag)
DepthOffsetAttrib -
DepthTestAttrib nodePath.setDepthTest(flag)
DepthWriteAttrib nodePath.setDepthWrite(flag)
FogAttrib nodePath.setFog(fog);
LightAttrib nodePath.setLight(light);
MaterialAttrib nodePath.setMaterial(material)
RenderModeAttrib nodePath.setRenderMode(RenderModeAttrib.Mode)
ShaderAttrib nodePath.setShader(shader);
StencilAttrib -
TexGenAttrib nodePath.setTexGen(stage, TexGenAttrib.Mode);
TexMatrixAttrib nodePath.setTexTransform(TransformState.make(mat));
TextureAttrib nodePath.setTexture(tex);
TransparencyAttrib nodePath.setTransparency(TransparencyAttrib.Mode)
Alpha检测属性(Alpha Test Attribute
Alpha 检测属性用于控制节点的某个部分是否基于纹理的alpha值来渲染,在渲染复杂形状物体时非常有用。该检测与alpha透明值不同,如果你对一个渲染到 color buffer的节点设置alpha检测属性,结果可能很奇怪。所有通过alpha检测的像素将被渲染,包括它们的透明度,而检测失败的像素不被渲染。记 住,设置适当的属性来override掉从上层节点继承的alpha检测。下面的例子建立一个属性,只渲染alpha值低于1/4的物体:
lowPassFilter = AlphaTestAttrib.make(RenderAttrib.MLess,0.25)
将该属性添加到一个节点:
NodePath.node().setAttrib(lowPassFilter)
Stencil检测/写入属性(Stencil Test/Write Attribute
StencilAttrib 用于检测和写入stencil buffer,两项工作在单个StencilAttrib同时进行。除了大家熟知的color buffer和depth buffer,stencil buffer作为一个辅助性buffer提供每个像素的掩码,让渲染管线有选择地进行渲染。典型的stencil应用有binary masking,shadowing和planar reflections。使用stencil buffer的目的通常是阻止一些物体渲染到color buffer。它的作用就像一个不可见的屏障,打开或关闭场景中物体的color buffer渲染。可以把它想象成蒙在场景上的镂空卡纸。在stencil比较期间,StencilAttrib的参考值与保存在stencil buffer里的值比较。例如:使用比较函数StencilAttrib.SCFGreaterThan,参考值 r=1,Sp为像素P在stencil buffer里的值,当r > Sp时P通过检测。
stencil buffer默认是关闭的,要打开它就必须在config.prc文件中加入这行:
framebuffer-stencil #t
StencilAttrib由构造函数唯一定义,下面的代码创建一个属性,只渲染stencil buffer值为1的物体,而且不改变stencil buffer的值:
stencilReader = StencilAttrib.make(1,StencilAttrib.SCFEqual,StencilAttrib.SOKeep,StencilAttrib.SOKeep,StencilAttrib.SOKeep,0,1,0)
第一参数为布尔值,如果为0,则对StencilAttrib不做处理。第二个参数 是使用的比较函数,在这里是等于函数。后三个参数表示根据比较结果stencil buffer将发生的变化。在这里三个keep函数表明不改变stencil buffer的值。再下来是用于比较的参考值,它在传到比较函数前先和一个掩码进行按位与,由于我们只是读取stencil buffer的值,因此给读、写掩码分别传值1、0,就是最后两个参数。
接下来建立一个写入stencil buffer的属性,在建立一个effect时大概都要一前一后使用这两个属性。
constantOneStencil = StencilAttrib.make(1,StencilAttrib.SCFAlways,StencilAttrib.SOZero,StencilAttrib.SOReplace,StencilAttrib.SOReplace,1,0,1)
首 先第一参数还是使这个属性生效,接下来的比较函数是Always,说明检测都通过。下来的参数是检测失败后对stencil buffer的操作(在这里不会出现)——将stencil buffer的值清0。下一个参数指定stencil检测通过而depth检测失败时的操作,后面是stencil和depth检测都通过的操作。这次我 们不管通过depth测试与否都要设置stencil buffer的值,因此后两个操作都设为Replace。参考值为1,掩码设为读0写1。
现在把属性加到节点上,让场景实现蒙上镂空卡纸后的effect:
cm = CardMaker("cardmaker")
cm.setFrame(-.5,.5,-.5,.5)
viewingSquare = render2d.attachNewNode(cm.generate())
viewingSquare.node().setAttrib(constanOneStencil)
view = loader.loadModel("models/Panda")
view.setScale(3)
view.node().setAttrib(stencilReader)
实例化(Instancing
在歌舞剧A Chorus Line中,50个外貌相似的女子一字排开表演整齐的踢踏舞。如果要在Panda3D中实现这个场面,我们可以这样做:
for i in range(50):
dancer = Actor.Actor("chorus-line-dancer.egg", {"kick":"kick.egg"})
dancer.loop("kick")
dancer.setPos(i*5,0,0)
dancer.reparentTo(render)

看看我们建立的scene graph:



这样做没错,但代价有点高。模型的动画要进行逐顶点的矩阵运算,所以这里要计算50个模型的动画,而它们实际上完全一样,只需计算一次就够了。实例化(instancing)技术可以避免重复计算。该技术的思路是,只建立一个dancer而不是50个,引擎只需更新一个dancer的动画。只不过要渲染50次,然后插入到50个不同位置:
dancer = Actor.Actor("chorus-line-dancer.egg", {"kick":"kick.egg"})
dancer.loop("kick")
dancer.setPos(0,0,0)
for i in range(50):
placeholder = render.attachNewNode("Dancer-Placeholder")
placeholder.setPos(i*5,0,0)
dancer.instanceTo(placeholder)
此时scene graph变成:



它不再是一棵树,而是一个直接的链式闭合结构,但renderer(渲染器)仍然按照循环的树遍历算法来遍历这个结构。因此,在终点的dancer节点处结束50次遍历。下面是深度优先遍历图,注意不是scene graph图,而是renderer遍历scene graph的路径图:



我们可以看到renderer访问dancer 50次,renderer并不知道它访问的是同一个节点,对它来说,访问同一个节点50次和访问50个不同节点没什么区别。
我们有50个placeholder节点,全都称为dummy节点,它们不包含真正的多边形模型,只是用与场景组织的对象。可以把placeholder看作dancer站立的舞台。
dancer位于相对于父节点(0,0,0)的位置,当renderer遍历placeholder1子树,dancer的位置就与placeholder1关联,当renderer遍历placeholder2子树,dancer的位置就与placeholder2关联,虽然dancer的位置固定在(0,0,0),它将出现在场景中的不同地方。
高级实例化
我们进一步改进程序:
dancer = Actor.Actor("chorus-line-dancer.egg", {"kick":"kick.egg"})
dancer.loop("kick")
dancer.setPos(0,0,0)
chorusline = NodePath()
for i in range(50):
placeholder = chorusline.attachNewNode("Dancer-Placeholder")
placeholder.setPos(i*5,0,0)
dancer.instanceTo(placeholder)
与之前不同的是,我们没有把50个placeholde放到render下,而是把它们放在一个dummy节点chorusline下,因此dancer不再是scene graph的一部分。现在,让我们:
for i in range(3):
placeholder = render.attachNewNode("Line-Placeholder")
placeholder.setPos(0,i*10,0)
chorusline.instanceTo(placeholder)
此刻scene graph变成:




当renderer使用树遍历算法遍历scene graph时,会遇到3个主要子树(以line-placeholder为根),每个子树包含50个placeholder和50个dancer,总共150个dancer。
关于实例化的重要提醒
实例化在模型和动画时节约了cpu运算,但它并不能改变renderer仍然要渲染模型150次的事实。如果dancer是1000个多边形模型,那么总共就要渲染150,000个多边形。而且,每个实例(instance)都有自己的bounding box,每个都独立地进行occlusion cull和frustum cull。
NodePath:一个节点指针 + 一个唯一的实例ID
即使我们有dancer模型的指针,但却无法回答“dancer在哪里”的问题。因为dancer不止在一个地方,它在150个地方。因此,节点指针没办法检索网络变换。
因 为“物体在哪里”是个很基本的问题,所以得不到答案会给我们造成诸多不便。很多有用的查询方法不能用于实例化,例如,不能取回一个节点的父节点,不能确定 节点的全局颜色或其他全局属性。所有查询方法都不管用,因为一个节点可能出现在很多地方,有很多种颜色,很多个父节点。但这些查询都是必不可少的。
解决办法基于以下考虑:如果有一个dancer模型的指针,以及一个唯一的ID来区别150个实例,那么就能查询某个实例。
之前,我们知道一个NodePath包含一个指向节点的指针和一些管理信息。管理信息的目的就是为了区别每个实例。现在你知道panda不提供Node::getNetTransform方法但提供了NodePath::getNetTransform方法的原因了。
为了理解NodePath如何取得它的名字,设想一下怎样唯一地标识一个实例。150个dancer,每一个都对应一个单独的scene graph路径(path),从根节点到dancer每一条可能的path都有一个dancer实例。换句话说,为了区分每个实例,需要一个从叶子节点到根节点的节点链表。
nodepath的管理信息就是一个节点链表,可以使用NodePath::node(i)方法从链表获取节点,node(0)作为第一个节点代表该NodePath指向的节点。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: