Egg Octree

Yes, I’ve seen that case. I have a .bam terrain with 97792 vertices, 97410 tris. The result of bam2egg is even corrupted. The .egg only has half of the vertices. That terrain was built by triangles strips and flattened, without vertex normals.

Though for the other 307583 vertices, 203090 tris model, built NOT by triangles strips, with normals, it’s just fine.

How did you create your model ?

[UPDATE] :
to allow me to exclude some parts of the model in the octree process, I used my SceneGraphBrowser :
discourse.panda3d.org/viewtopic.php?t=3474

from pandac.PandaModules import *
import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject
from direct.gui.OnscreenText import OnscreenText
from direct.interval.IntervalGlobal import *
from direct.gui.DirectGui import DirectLabel
from direct.showbase import PythonUtil as PU
from direct.task import Task
from random import random
import sys, math
from SceneGraphBrowser import *

def createSceneGraphBrowser():
    # create SceneGraphBrowser and point it on the model
    global SGB
    SGB = SceneGraphBrowser(
        parent=None, # where to attach SceneGraphBrowser frame
        root=sourceNodePath, # display children under this root node
        command=nodeSelected, # user defined method, executed when a node get selected,
                              # with the selected node passed to it
        contextMenu=nodeRightClicked,
        # selectTag and noSelectTag are used to filter the selectable nodes.
        # The unselectable nodes will be grayed.
        # You should use only selectTag or noSelectTag at a time. Don't use both at the same time.
        #selectTag=['select'],   # only nodes which have the tag(s) are selectable. You could use multiple tags.
        noSelectTag=['noSelect','dontSelectMe'], # only nodes which DO NOT have the tag(s) are selectable. You could use multiple tags.
        # nodes which have exclusionTag wouldn't be displayed at all
        exclusionTag=['internal component'],
        frameSize=(1,1.6),
        font=None, titleScale=.05, itemScale=.035, itemTextScale=1.2, itemTextZ=0,
        rolloverColor=(1,.8,.2,1),
        collapseAll=0, # initial tree state
        suppressMouseWheel=1,  # 1 : blocks mouse wheel events from being sent to all other objects.
                               #     You can scroll the window by putting mouse cursor
                               #     inside the scrollable window.
                               # 0 : does not block mouse wheel events from being sent to all other objects.
                               #     You can scroll the window by holding down the modifier key
                               #     (defined below) while scrolling your wheel.
        modifier='control'  # shift/control/alt
        )

selected = None
selectedIval = None
excludedText = None
excludedIval = None

def renderFrame():
    base.graphicsEngine.renderFrame()
    base.graphicsEngine.renderFrame()

def nodeSelected(np): # don't forget to receive the selected node (np)
    global selected,selectedIval,excludedText,excludedIval
    if selected!=None:
       selected.hideBounds()
       selectedIval.finish()
       selected.clearRenderMode()
    if excludedIval!=None:
       if excludedIval.isPlaying():
          excludedIval.finish()
    if np.isStashed():
       excludedText = DirectLabel(text='THIS NODE IS EXCLUDED\nright-click it again to include it back',
                    scale=.06, pad=(.5,.5), frameColor=(1,1,1,.5),
                    text_fg=(0,0,0,1),sortOrder=2000)
       excludedIval = Sequence(
                excludedText.colorScaleInterval(5,Vec4(1,1,1,0),blendType='easeIn'),
                Func(excludedText.destroy)
                )
       excludedIval.start()
       return
    selected=np
    np.showTightBounds()
    selectedIval = Sequence(
             Func(np.setRenderModeWireframe,1),
             np.colorScaleInterval(.3,Vec4(1,0,0,1)),
             np.colorScaleInterval(.3,Vec4(1,1,1,1)),
             Func(np.setRenderModeFilled,1),
             np.colorScaleInterval(.3,Vec4(1,0,0,1)),
             np.colorScaleInterval(.3,Vec4(1,1,1,1)),
             Wait(.5)
             )
    selectedIval.loop()
    print np.getName(),'SELECTED'

def nodeRightClicked(np): # don't forget to receive the selected node (np)
    if result!=None:
       return
    if np.isStashed():
       np.unstash()
    else:
       np.stash()
    updateGrids(updateSpacing=0)



class EggOctree(object):

   def build(self, sourceNodePath, destNode, cellSize, collide=False):
      sourceNode=sourceNodePath.copyTo( sourceNodePath.getParent() )
      sourceNode.flattenStrong()

      print 'storing vertices & triangles.....'
      geomList=sourceNode.findAllMatches('**/+GeomNode').asList()
      if not geomList:
         return

      vpool=EggVertexPool('octV_0')

      collNumVtx=0
      collnumface=0
      # list for the vertices
      collVertices=[]
      collUniqueVtxIdx=[]
      # list for the vertices
      collNormals=[]
      # list for the vertices index
      collFaces=[]

      # append all vertex data into a complete list
      for collGeom in geomList:
          for gIdx in range(collGeom.node().getNumGeoms()):
              currentGeom=collGeom.node().getGeom(gIdx)
              collVtxData=currentGeom.getVertexData()
              numVtx=collVtxData.getNumRows()
              #=========================================================
              # create vertex reader
              collVtxReader=GeomVertexReader(collVtxData)
              # begin reading at vertex 0
              collVtxReader.setRow(0)
              # get the vertex position column
              column=-1
              columnName=''
              while ( columnName!='vertex' ):
                    column+=1
                    collVtxReader.setColumn(column)
                    columnName=str(collVtxReader.getColumn().getName())
              #=========================================================
              # create vertex normal reader
              collNormalReader=GeomVertexReader(collVtxData)
              # begin reading at vertex 0
              collNormalReader.setRow(0)
              # get the vertex normal column
              hasNormals=1
              column=-1
              columnName=''
              while ( columnName!='normal' ):
                    column+=1
                    collNormalReader.setColumn(column)
                    if collNormalReader.getColumn()==None:
                       hasNormals=0
                       break
                    else:
                       columnName=str(collNormalReader.getColumn().getName())
              for i in range(numVtx):
                  # respect each geomNode's transform which may be exist
                  vtx=sourceNode.getRelativePoint(collGeom,collVtxReader.getData3f())
                  collVertices.append(vtx)
                  eVtx=EggVertex()
                  eVtx.setPos(Point3D(vtx[0],vtx[1],vtx[2]))
                  if hasNormals:
                     normal=sourceNode.getRelativeVector(collGeom,collNormalReader.getData3f())
                     eVtx.setNormal(Vec3D(normal[0],normal[1],normal[2]))
                  collUniqueVtxIdx.append( vpool.createUniqueVertex(eVtx).getIndex() )
              for prim in range(currentGeom.getNumPrimitives()):
                  # store the vertices index for each triangle
                  collFaceData=currentGeom.getPrimitive(prim).decompose()
                  # get the triangle counts
                  numface=collFaceData.getNumFaces()
                  for i in range(0,numface*3,3):
                      # refer to the vertex data length list created earlier
                      vtxIdx1=collNumVtx+collFaceData.getVertex(i)
                      vtxIdx2=collNumVtx+collFaceData.getVertex(i+1)
                      vtxIdx3=collNumVtx+collFaceData.getVertex(i+2)
                      vtxIdx=(collUniqueVtxIdx[vtxIdx1],collUniqueVtxIdx[vtxIdx2],collUniqueVtxIdx[vtxIdx3])
                      collFaces.append(vtxIdx)
                  collnumface+=numface
              # add the current collected vertices count
              collNumVtx+=numVtx

      collNormals=None
      print 'NUM UNIQUE VTX :',vpool.getHighestIndex()+1

#       ############################################################
#       ################ DEBUG !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#       ED=EggData()
#       ED.addChild(vpool)
#       for idx in range(collnumface):
#           # Get the triangle center
#           poly=EggPolygon()
#           for i in range(3) :
#               poly.addVertex(vpool.getVertex(collFaces[idx][i]))
#           ED.addChild(poly)
#       ED.writeEgg(Filename(output_file+'_debug.egg'))
#       return
#       ################ DEBUG !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#       ############################################################

      # model's bounding box
      minB,maxB=sourceNode.getTightBounds()
      minBB = Vec3D(minB[0]+0.001, minB[1]+0.001, minB[2]+0.001)
      maxBB = Vec3D(maxB[0]+0.001, maxB[1]+0.001, maxB[2]+0.001)

      sourceNode.removeNode()

      cellXsize=cellSize[0]
      cellYsize=cellSize[1]
      cellZsize=cellSize[2]

      # Number of leaves x,y,z
      bboxSize = maxBB - minBB
      self.ncx = math.ceil(bboxSize.getX() / cellXsize)
      self.ncy = math.ceil(bboxSize.getY() / cellYsize)
      self.ncz = math.ceil(bboxSize.getZ() / cellZsize)

      # Depth of the tree x,y,z
      self.depthX = math.ceil(math.log(self.ncx) / math.log(2))
      self.depthY = math.ceil(math.log(self.ncy) / math.log(2))
      self.depthZ = math.ceil(math.log(self.ncz) / math.log(2))
      self.depth = max(self.depthX, self.depthY, self.depthZ)

      collVertices=None

      print self.ncz, self.ncy, self.ncx
      print 'initializing cells...'
      self.cells = [[[
            {'-':
               Point3D( minBB.getX() + x * cellXsize,
                        minBB.getY() + y * cellYsize,
                        minBB.getZ() + z * cellZsize),
            '+':
               Point3D( minBB.getX() + (x+1) * cellXsize,
                        minBB.getY() + (y+1) * cellYsize,
                        minBB.getZ() + (z+1) * cellZsize),
            'g':EggGroup('L_%d_%d_%d' % (x,y,z))}

            for z in range(self.ncz)]
            for y in range(self.ncy)]
            for x in range(self.ncx)]

      print 'Cell grid is %dx%dx%d' % (len(self.cells), len(self.cells[0]), len(self.cells[0][0]))

      if collide :
         for x in range(self.ncx) :
            for y in range(self.ncy) :
               for z in range(self.ncz) :
                  self.cells[x][y][z]['g'].addObjectType('barrier')

      print 'filling cells...'
      # Iterate on the triangles
      for idx in range(collnumface):
          # Get the triangle center
          polyCenter = Point3D(0,0,0)
          poly=EggPolygon()
          trie=collFaces[idx]
          for i in range(3) :
              vtx=vpool.getVertex( trie[i] )
              polyCenter +=vtx.getPos3()
              poly.addVertex(vtx)
          polyCenter /= 3.0
          # Add the triangle to the corresponding cell group
          cx = int(math.floor((polyCenter[0]-minBB[0]) / cellXsize))
          cy = int(math.floor((polyCenter[1]-minBB[1]) / cellYsize))
          cz = int(math.floor((polyCenter[2]-minBB[2]) / cellZsize))

          # doing this would yield uniform normals for all 3 vertices
          # so, preserve normals from the vertex data instead
          # (already done above after creating EggVertex)
#           if not hasNormals:
#              poly.recomputePolygonNormal()
          self.cells[cx][cy][cz]['g'].addChild(poly)

      # Add the vertex data
      destNode.addChild(vpool)

      print 'Started........'
      self.nleaves = self.recur(destNode, 0, 0,0,0)
      print self.nleaves, 'leaves added'

   def recur(self, node, depth, x, y, z) :
      if depth < self.depth :
         nnode = EggGroup('')
         delt = int(math.pow(2, self.depth - depth - 1))

         nchilds = 0

         nchilds += self.recur(nnode, depth+1, x, y, z)
         nchilds += self.recur(nnode, depth+1, x + delt, y, z)
         nchilds += self.recur(nnode, depth+1, x, y + delt, z)
         nchilds += self.recur(nnode, depth+1, x + delt, y + delt, z)

         nchilds += self.recur(nnode, depth+1, x, y, z + delt)
         nchilds += self.recur(nnode, depth+1, x + delt, y, z + delt)
         nchilds += self.recur(nnode, depth+1, x, y + delt, z + delt)
         nchilds += self.recur(nnode, depth+1, x + delt, y + delt, z + delt)

         if nchilds > 0 :
            node.addChild(nnode)
         return nchilds

      else :

         # We are in a leaf
         if x < self.ncx and y < self.ncy and z < self.ncz :
            node.addChild(self.cells[x][y][z]['g'])
            return 1
         else:
            return 0

eoct=EggOctree()

distX=4.
distY=4.
distZ=4.
result=None
inc=.1
adjusted=-1

grid=render.attachNewNode('grid')
grid.setLightOff(1)

LSgrid=LineSegs()
LSgrid.setThickness(1)

base.setBackgroundColor(.3,.7,1,1)
base.camLens.setNearFar(.1,100000)
loadingText=OnscreenText(text='LOADING.....',scale=.12,fg=(1,0,0,1),shadow=(0,0,0,1))
renderFrame()
loadingText.destroy()

offOnText=['OFF','ON']
offOnFG=[(1,0,0,1),(0,0,1,1)]
walkerTextParent=aspect2d.attachNewNode('')
walkerTextParent.setZ(-1.2)
walkerText = OnscreenText( parent=walkerTextParent,
        text='WALKING ON SCENEGRAPH HIERARCHY',pos=(0,.075),scale=.05,mayChange=1)
stepOnText = OnscreenText( parent=walkerTextParent,
        text='sdf',pos=(0,.02),scale=.05,mayChange=1)
gridPropsTextParent=aspect2d.attachNewNode('')
gridPropsTextParent.setZ(-1)
incText = OnscreenText( parent=gridPropsTextParent,
        text='increment : %.3f'%inc,pos=(0,.02),scale=.05,mayChange=1)
XYZtext=[]
XYZtext.append( OnscreenText( parent=gridPropsTextParent,
                text='X : %.3f'%distX,pos=(-.5,.075),scale=.05,mayChange=1) )
XYZtext.append( OnscreenText( parent=gridPropsTextParent,
                text='Y : %.3f'%distY,pos=(  0,.075),scale=.05,mayChange=1) )
XYZtext.append( OnscreenText( parent=gridPropsTextParent,
                text='Z : %.3f'%distZ,pos=( .5,.075),scale=.05,mayChange=1) )

def setupLights():
    ambientLight = AmbientLight( 'ambientLight' )
    ambientLight.setColor( Vec4(0.3, 0.3, 0.3, 1) )

    directionalLight = DirectionalLight( 'directionalLight1' )
    directionalLight.setDirection( Vec3( 1, 2,-1 ) )
    directionalLight.setColor( Vec4( .7, .7, .7, 1 ) )

    render.setLight(render.attachNewNode( ambientLight ))
    render.setLight(render.attachNewNode( directionalLight ))

def updateGrids(g=(1,1,1),inc=0,updateSpacing=1):
    global distX,distY,distZ
    TB1,TB2=sourceNodePath.getTightBounds()
    TB=TB2-TB1
    threshold=.1
    if updateSpacing:
       if g[0] and TB[0]>threshold:
          distX=PU.clampScalar(threshold,TB[0],distX+inc)
          XYZtext[0]['text']='X : %.3f'%distX
       if g[1] and TB[1]>threshold:
          distY=PU.clampScalar(threshold,TB[1],distY+inc)
          XYZtext[1]['text']='Y : %.3f'%distY
       if g[2] and TB[2]>threshold:
          distZ=PU.clampScalar(threshold,TB[2],distZ+inc)
          XYZtext[2]['text']='Z : %.3f'%distZ
    # remove all grids ###############################################
    for g in grid.findAllMatches('**/+GeomNode').asList():
        g.removeNode()
    # gridX ###############################################
    LSgrid.reset()
    LSgrid.setColor(1,1,0)
    for z in range(int(1+TB[2]/distZ)):
        for x in range(int(1+TB[0]/distX)):
            LSgrid.moveTo(x*distX,0,z*distZ)
            LSgrid.drawTo(x*distX,TB[1],z*distZ)
        LSgrid.moveTo(TB[0],0,z*distZ)
        LSgrid.drawTo(TB[0],TB[1],z*distZ)
    for x in range(int(1+TB[0]/distX)):
        LSgrid.moveTo(x*distX,0,TB[2])
        LSgrid.drawTo(x*distX,TB[1],TB[2])
    LSgrid.moveTo(TB[0],0,TB[2])
    LSgrid.drawTo(TB[0],TB[1],TB[2])
    grid.attachNewNode(LSgrid.create()).setPos(TB1)
    # gridY ###############################################
    LSgrid.reset()
    LSgrid.setColor(1,0,0)
    for z in range(int(1+TB[2]/distZ)):
        for y in range(int(1+TB[1]/distY)):
            LSgrid.moveTo(0,y*distY,z*distZ)
            LSgrid.drawTo(TB[0],y*distY,z*distZ)
        LSgrid.moveTo(0,TB[1],z*distZ)
        LSgrid.drawTo(TB[0],TB[1],z*distZ)
    for y in range(int(1+TB[1]/distY)):
        LSgrid.moveTo(0,y*distY,TB[2])
        LSgrid.drawTo(TB[0],y*distY,TB[2])
    LSgrid.moveTo(0,TB[1],TB[2])
    LSgrid.drawTo(TB[0],TB[1],TB[2])
    grid.attachNewNode(LSgrid.create()).setPos(TB1)
    # gridZ ###############################################
    LSgrid.reset()
    LSgrid.setColor(0,0,1)
    for x in range(int(1+TB[0]/distX)):
        for y in range(int(1+TB[1]/distY)):
            LSgrid.moveTo(x*distX,y*distY,0)
            LSgrid.drawTo(x*distX,y*distY,TB[2])
        LSgrid.moveTo(x*distX,TB[1],0)
        LSgrid.drawTo(x*distX,TB[1],TB[2])
    for y in range(int(1+TB[1]/distY)):
        LSgrid.moveTo(TB[0],y*distY,0)
        LSgrid.drawTo(TB[0],y*distY,TB[2])
    LSgrid.moveTo(TB[0],TB[1],0)
    LSgrid.drawTo(TB[0],TB[1],TB[2])
    grid.attachNewNode(LSgrid.create()).setPos(TB1)

def adjustX():
    global adjusted
    adjusted=0
    updateXYZtextScale()

def adjustY():
    global adjusted
    adjusted=1
    updateXYZtextScale()

def adjustZ():
    global adjusted
    adjusted=2
    updateXYZtextScale()

def updateXYZtextScale():
    for t in XYZtext:
        t['scale']=.05
        t['fg']=(0,0,0,1)
    XYZtext[adjusted]['scale']=.07
    XYZtext[adjusted]['fg']=(0,0,1,1)

def adjustGrid(dir):
    if adjusted==-1:
       print "CHOOSE X, Y, or Z"
       return
    adjustedGrid=[0,0,0]
    adjustedGrid[adjusted]=1
    if grid.isHidden():
       grid.show()
    updateGrids(adjustedGrid,dir*inc)

def adjustInc(Iinc):
    global inc
    inc=PU.clampScalar(.05,100,inc+Iinc)
    incText['text']='increment : %.3f'%inc

def toggleCollision():
    global collide
    collide=not collide
    collText['text']='COLLISION : '+offOnText[collide]
    collText['fg']=offOnFG[collide]

def toggleGridVis():
    if grid.isHidden():
       grid.show()
    else:
       grid.hide()

def toggleHierarchyWalk(status=None):
    global result,isWalkingOnHierarchy,selected
    if status==None:
       isWalkingOnHierarchy=not isWalkingOnHierarchy
    else:
       isWalkingOnHierarchy=status
    if isWalkingOnHierarchy:
       setMode(MODE_hierarchyWalk)
       if selected==None:
          if result==None:
             SGB.command(sourceNodePath)
             stepOnText['text']=sourceNodePath.getName()
          else:
             SGB.command(result)
             stepOnText['text']=result.getName()
       gridPropsTextParent.posInterval(.25,Point3(0,0,-1.2),blendType='easeIn').start()
       walkerTextParent.posInterval(.25,Point3(0,0,-1),blendType='easeIn').start()
       text=helpWalker+helpAllModes
       if result==None:
          text+=helpInclusion
       helpText['text']=text
    else:
       setMode(MODE_gridAdjustment)
       gridPropsTextParent.posInterval(.25,Point3(0,0,-1),blendType='easeIn').start()
       walkerTextParent.posInterval(.25,Point3(0,0,-1.2),blendType='easeIn').start()
       text=helpGrid+helpAllModes
       if result==None:
          text+=helpInclusion
       helpText['text']=text
    
def getChildIndex(np):
    children=np.getParent().getChildrenAsList()
    for c in children:
        if c==np:
           return children.index(np)

def gotoParent():
    if selected!=sourceNodePath and selected!=result:
       parent=selected.getParent()
       SGB.selectNode(parent)
       numChildren=parent.getParent().getNumChildren()
       selfIdx=getChildIndex(selected)
       if parent!=sourceNodePath and parent!=result:
          stepOnText['text']='child #%s of %s (num children : %s)'%(selfIdx+1,numChildren,parent.getNumChildren())

def gotoChild():
    numChildren=selected.getNumChildren()
    if numChildren:
       SGB.selectNode(selected.getChild(0))
       stepOnText['text']='child #%s of %s (num children : %s)'%(1,numChildren,selected.getNumChildren())

def gotoNextSibling():
    if selected==sourceNodePath or selected==result:
       return
    parent=selected.getParent()
    siblingIdx=getChildIndex(selected)+1
    if siblingIdx<parent.getNumChildren():
       SGB.selectNode(parent.getChild(siblingIdx))
       stepOnText['text']='child #%s of %s (num children : %s)'%(siblingIdx+1,parent.getNumChildren(),selected.getNumChildren())

def gotoPrevSibling():
    if selected==sourceNodePath or selected==result:
       return
    parent=selected.getParent()
    siblingIdx=getChildIndex(selected)-1
    if siblingIdx>-1:
       SGB.selectNode(parent.getChild(siblingIdx))
       stepOnText['text']='child #%s of %s (num children : %s)'%(siblingIdx+1,parent.getNumChildren(),selected.getNumChildren())

def setMode(mode):
    base.buttonThrowers[0].node().setPrefix(mode+':-> ')

def acceptKey(event,method,extraArgs=[]):
    for mode in MODES:
        DO.accept('%s:-> %s'%(mode,event),method,extraArgs)
        
def zoomNode(toNode=0):
    if toNode:
       if selected==None:
          return
       else:
          np=selected
    else:
       np=sourceNodePath
    if np.getBounds().isEmpty():
       return
    center=np.getBounds().getCenter()
    radius=np.getBounds().getRadius()

    camPivot.setPos(center)
    camPivot.setHpr(camera,0,0,0)
    camera.setPos(0,-radius*5,0)
    camera.lookAt(0,0,0)
    mat4=Mat4(camera.getMat())
    mat4.invertInPlace()
    base.mouseInterfaceNode.setMat(mat4)

def reviveModel():
    global result,selected
    if result==None:
       return
    if selected!=None:
       selected.hideBounds()
       selectedIval.finish()
       selected.clearRenderMode()
       selected=None
    loader.unloadModel(result)
    result.removeNode()
    result=None

    # revive the model
    sourceNodePath.unstash()
    # put a notification text
    waitText = DirectLabel(text='updating SceneGraphBrowser.....',
             scale=.045,text_fg=(0,0,0,1), frameColor=(1,1,1,.8), pad=(.5,.5),
             sortOrder=2000)
    renderFrame()
    waitText.destroy()
    # restore stashed nodes, so it will be found by the SGBrowser
    stashedNPs=sourceNodePath.findAllMatches('**/@@*')
    stashedNPs.unstash()
    # display the model's scenegraph
    SGB.setRoot(sourceNodePath)
    # stash them back
    stashedNPs.stash()
    stashedNPs.clear()
    toggleHierarchyWalk(0)
    print 'REVIVED !!!'

def splitModel():
    global result,selected
    waitText = DirectLabel(text='please wait.....',
             scale=.1, frameColor=(1,1,1,.7), pad=(1,.2), text_fg=(0,0,0,1),
             sortOrder=2000)
    renderFrame()
    waitText.destroy()

    ed = EggData()
    ed.setCoordinateSystem(0) # set it to Z-Up
    eoct.build(sourceNodePath, ed, Vec3D(distX, distY, distZ), collide=collide)
    print 'writing EGG...',
    if collide:
       ed.writeEgg(Filename(output_file+'_coll.egg'))
    else:
       ed.writeEgg(Filename(output_file+'.egg'))
    print 'done'

    if selected!=None:
       selected.hideBounds()
       selectedIval.finish()
       selected.clearRenderMode()
       selected=None
    sourceNodePath.stash()
    if result!=None:
       loader.unloadModel(result)
       result.removeNode()

    result = NodePath( loadEggData(ed) )
    result.setName(output_file)
    result.reparentTo(render)
    if collide:
       result.writeBamFile(output_file+'_coll.bam')
       result.setTransparency(0,10)
       for g in result.findAllMatches('**/+CollisionNode').asList():
           g.show()
    else:
       result.writeBamFile(output_file+'_White.bam')
       for g in result.findAllMatches('**/+GeomNode').asList():
           g.setColor(random(),random(),random(),1)
       result.writeBamFile(output_file+'_Colored.bam')

    waitText = DirectLabel(text='updating SceneGraphBrowser.....',
             scale=.045,text_fg=(0,0,0,1), frameColor=(1,1,1,.8), pad=(.5,.5),
             sortOrder=2000)
    renderFrame()
    waitText.destroy()
    SGB.setRoot(result)
#     result.ls()
    doneText = DirectLabel(text='DONE',
             scale=.1, frameColor=(1,1,1,.7), pad=(1,.2),text_fg=(0,0,0,1),
             sortOrder=2000)
    Sequence(
             doneText.colorScaleInterval(3,Vec4(1,1,1,.2)),
             Func(doneText.destroy)
             ).start()

    print '###########################################'
    print '###########################################'
    result.analyze()
    print '###########################################'
    print '###########################################'
    toggleHierarchyWalk(0)
    ed = None

DO=DirectObject()
MODE_hierarchyWalk='[walking around]'
MODE_gridAdjustment='[adjusting grid]'
isWalkingOnHierarchy=0

MODES=[MODE_gridAdjustment]
acceptKey('x',adjustX)
acceptKey('y',adjustY)
acceptKey('z',adjustZ)
acceptKey('arrow_right',adjustInc,[.05])
acceptKey('arrow_right-repeat',adjustInc,[.05])
acceptKey('arrow_left',adjustInc,[-.05])
acceptKey('arrow_left-repeat',adjustInc,[-.05])
acceptKey('arrow_up',adjustGrid,[1])
acceptKey('arrow_up-repeat',adjustGrid,[1])
acceptKey('arrow_down',adjustGrid,[-1])
acceptKey('arrow_down-repeat',adjustGrid,[-1])

MODES=[MODE_hierarchyWalk]
acceptKey('arrow_right',gotoNextSibling)
acceptKey('arrow_left',gotoPrevSibling)
acceptKey('arrow_up',gotoParent)
acceptKey('arrow_down',gotoChild)

MODES=[MODE_gridAdjustment,MODE_hierarchyWalk]
acceptKey('escape',sys.exit)
acceptKey('enter',splitModel)
acceptKey('r',reviveModel)
acceptKey('c',toggleCollision)
acceptKey('w',base.toggleWireframe)
acceptKey('g',toggleGridVis)
acceptKey('h',toggleHierarchyWalk)
acceptKey('s',base.screenshot)
acceptKey('a',zoomNode,[0])
acceptKey('f',zoomNode,[1])

helpGrid='''[ X/Y/Z ] : select X/Y/Z axis
[ arrow up/down ] : adjust cell size along selected axis
[ arrow left/right ] : adjust cell size increment

'''
helpWalker='''[ arrow up/down ] : go up to parent or down to children
[ arrow left/right ] : go to previous/next sibling

'''
helpAllModes='''[ ENTER ] : SPLIT NOW !!
[ R ] : revive model
[ C ] : toggle collision
[ W ] : toggle wireframe
[ G ] : toggle grid visibility
[ H ] : toggle grid adjustment / hierarchy walker
[ SPACE ] : toggle SceneGraphBrowser
[ A ] : zoom extent
[ F ] : zoom selected node'''
helpInclusion='''

RIGHT CLICK node in SceneGraphBrowser :
    toggle inclusion in octree process'''
helpText = OnscreenText(text=helpGrid+helpAllModes+helpInclusion,
         scale=.045,align=TextNode.ALeft,fg=(0,0,0,1),mayChange=1)
NodePath(helpText).setPos(render2d,-.995,0,.95)

setupLights()

##########################################################
##  THE INPUT FILE CAN BE ANY OF THE SUPPORTED FORMATS  ##
##########################################################
input_file='teapot.egg'#    <----- YOUR FILE
# input_file='panda.egg'#    <----- YOUR FILE
# input_file='RalphWorld.egg'#    <----- YOUR FILE
output_file=input_file[:input_file.rfind('.')]+'-split'
#=========================================================
collide=0
##########################################################
##########################################################

collText=OnscreenText(text='COLLISION : '+offOnText[collide],
         scale=.05,pos=(0,-.86),fg=offOnFG[collide],shadow=(0,0,0,1), mayChange=1)

sourceNodePath=loader.loadModel(input_file)
sourceNodePath.reparentTo(render)
sourceNodePath.analyze()

camPivot=render.attachNewNode('camPivot')
camera.reparentTo(camPivot)
camera.setPos(1,-1.2,1)
camera.lookAt(0,0,0)
zoomNode(0)
updateGrids()
waitText = DirectLabel(text='creating SceneGraphBrowser.....',
         scale=.045,text_fg=(0,0,0,1), frameColor=(1,1,1,.9), pad=(.5,.5),
         sortOrder=2000)
renderFrame()
waitText.destroy()

createSceneGraphBrowser()
acceptKey('space',SGB.toggleVisibility)
setMode(MODE_gridAdjustment)
run()


and don’t call me sir, I’m not bearded yet. :laughing:

My egg is modeled in Blender and exported with Chicken. I loaded it into Panda without problems (except it took quite a while to load).

I tried to convert it with your latest code but it was not possible, it doesn’t even load there. There is a warning in console: “Assertion failed: _current_index + sizeof(tempvar) <= _datagram->get_length() at line 277 of c:\temp\mkpr\built\include\datagramIterator.I”

With smaller terrain from Roaming Ralph it works fine.

Honestly I don’t know what’s going on there. My problem here is even more weird.
It’s my 97792 vertices, 97410 tris model, which is a single geom, a single primitive. After collecting the vertices & triangles, I stored the raw result to an .egg, but it couldn’t be loaded.
I’ve hunted it down and came up with this piece :
ynjh.panda3dprojects.com/debug_egg.zip
It’s the first 61448 triangles, but can’t be loaded. I doubt that 61448 is the triangle counts limit, but if I remove any of the , it’s loaded.
Does anyone know what’s wrong ?

hmm… my model has 50k poly count. does that mean its one of the reasons it cant be loaded as well?

till now i still cant figure out how to actually use the eggOctree lolx. but i found some ways to optimize my map and solved my ctrav-affected problem. good luck on ur problems guys :slight_smile:

[UPDATE] :

  1. I don’t use this original part anymore :
      # Loop on the vertices determine the bounding box
      minBB = Point3D()
      maxBB = Point3D()

      nverts = vpool.getHighestIndex()
      for iv in range(nverts + 1) :
         vtx = vpool[iv]
         vtxPos = vtx.getPos3()

         # Sets the X, Y or Z components
         i = 0
         for set in [Point3D.setX, Point3D.setY, Point3D.setZ]  :
            if vtxPos[i] < minBB[i] :
               set(minBB, vtxPos[i])

            if vtxPos[i] > maxBB[i] :
               set(maxBB, vtxPos[i])
            i += 1

      minBB -= Vec3D(0.001, 0.001, 0.001)
      maxBB += Vec3D(0.001, 0.001, 0.001)

There was a little noticable offset in the result, along Z. Now I use tight bounds, which gives acurate result :

      # model's bounding box
      minB,maxB=sourceNode.getTightBounds()
      minBB = Vec3D(minB[0]+0.001, minB[1]+0.001, minB[2]+0.001)
      maxBB = Vec3D(maxB[0]+0.001, maxB[1]+0.001, maxB[2]+0.001)
  1. improved vertices addition to the pool, now vertices are unique to avoid duplicated vertices.
  2. added scenegraph hierarchy walk, just like in pview, using arrow keys, in addition to the GUI.
  3. fixed 1 tiny bug in the SceneGraphBrowser GUI.
    discourse.panda3d.org/viewtopic.php?t=3474

The result of this octree is a single huge vertex pool and the octree groups of geoms. I’m still clueless about the failure involving poly counts. It failed on my 97792 vertices, 97410 tris model, but not on my 307583 vertices, 203090 tris model. It’s silent death, no spit on the console at all. I checked for maximum poly count for each octree’d geom, it isn’t excessive at all. Each geom’s poly counts is limited by the cell size, so using a small cell size should not fail, but it failed. So, it’s definitely something else beyond my reach :S
If someone could help, please [size=200]H E L P[/size] !!
Thank you.

I had some problem with the egg script.

  1. It used fix chunk cells ( so the polygons would not get divided as well when there is high density)
  2. It left lots of empty collision nodes in the egg
  3. And most importantly guessing the cell size is hard! I have models in meters and never know weather its 1, .1 or 100… that was a big hindrance!

This script is a little faster too and provides command line interface. Try it out!

Some ideas for future implementation:
Limit the number of the smallest cell - why divide further if you will never fit through the hole.
Write the collision system back into the source egg.
Porting it to c++ (why bother its already super fast)

#!/usr/bin/python
"""
                             _                 
                            | |                
  ___  __ _  __ _  ___   ___| |_ _ __ ___  ___ 
 / _ \/ _` |/ _` |/ _ \ / __| __| '__/ _ \/ _ \
|  __/ (_| | (_| | (_) | (__| |_| | |  __/  __/
 \___|\__, |\__, |\___/ \___|\__|_|  \___|\___|
       __/ | __/ | by treeform                            
      |___/ |___/                                                              
                              
This is a replacement of raytaller wonderful 
Egg Octree script many people had problem using it
( i always guessed wrong about the size of cells )
and it generated many "empty" branches which this 
one does not.  
original see : ( https://discourse.panda3d.org/viewtopic.php?t=2502 )
This script like the original also released under the WTFPL license.
Usage: egg-octreefy [args] [-o outfile.egg] infile.egg [infile.egg...]  
-h     display this 
-v     verbose
-l     list resulting egg file
-n     number of triangles per leaf (default 3) 
if outfile is not specified "infile"-octree.egg assumed
"""
import sys, getopt
import math
from pandac.PandaModules import *
global verbose,listResultingEgg,maxNumber
listResultingEgg = False
verbose = False
maxNumber = 3
    
def getCenter(vertexList):
    """ get a list of Polywraps and figure out their center """
    # Loop on the vertices determine the bounding box
    center = Point3D(0,0,0)
    i = False
    for vtx in vertexList:
        center += vtx.center
        i+=1
    if i:
        center /= i
    return center

def flatten(thing):
    """ get nested tuple structure like quadrents and flatten it """ 
    if type(thing) == tuple:
        for element in thing: 
            for thing in flatten(element):
                yield thing
    else:
        yield thing

def splitIntoQuadrants(vertexList,center):
    """
        +---+---+    +---+---+
        | 1 | 2 |    | 5 | 6 |
        +---+---+    +---+---+
        | 3 | 4 |    | 7 | 8 |
        +---+---+    +---+---+
        put all poly wraps into quadrents
    """
    quadrants = ((([],[]),
                 ([],[])),
                 (([],[]),
                  ([],[])))
    for vtx in vertexList:
        vtxPos = vtx.center
        x =  vtxPos[0] > center[0]
        y =  vtxPos[1] > center[1]
        z =  vtxPos[2] > center[2]
        quadrants[x][y][z].append(vtx)
    quadrants = flatten(quadrants)
    return quadrants

class Polywrap:
    """ 
        its a class that defines polygons center
        so that it does not have to be recomputed
    """
    polygon = None
    center = None
    
    def __str__(self):
        """ some visualization to aid debugging """
        return str(self.polygon.getNumVertices())+":"+str(self.center)
    
def genPolyWraps(group):
    """ generate a list of polywraps form a group of polygons """
    for polygon in iterChildren(group):
        if type(polygon) == EggPolygon:
            center = Vec3D()
            i = False
            for vtx in iterVertexes(polygon):
                center += vtx.getPos3()
                i += 1 
            if i : center /= i
            pw = Polywrap()
            pw.polygon = polygon
            pw.center = center
            yield pw 
          
def buildOctree(group):
    """ build an octree form a egg group """
    global verbose
    group.triangulatePolygons(0xff)
    polywraps = [i for i in genPolyWraps(group)]
    if verbose: print len(polywraps),"triangles"
    center = getCenter(polywraps)
    quadrants = splitIntoQuadrants(polywraps,center)
    eg = EggGroup('octree-root')
    for node in recr(quadrants):
        eg.addChild(node)
    return eg

def recr(quadrants,indent=0):
    """ 
        visit each quadrent and create octree there
        all the end consolidate all octrees into egg groups 
    """ 
    global verbose,maxNumber
    qs = [i for i in quadrants]
    if verbose: print "    "*indent,"8 quadrents have ",[len(i) for i in qs]," triangles"
    for quadrent in qs:
        if len(quadrent) == 0:
            if verbose: print "    "*indent," no triangles at this quadrent"
            continue
        elif len(quadrent) <= maxNumber:
            center = getCenter(quadrent)
            if verbose: print "    "*indent," triangle center", center, len(quadrent)
            eg = EggGroup('leaf %i tri'%len(quadrent))
            eg.addObjectType('barrier')
            for pw in quadrent:
                eg.addChild(pw.polygon)
                if eg.getFirstChild : yield eg
        else:
            eg = EggGroup('branch-%i'%indent)
            center = getCenter(quadrent)
            for node in recr(splitIntoQuadrants(quadrent,center),indent+1):
                eg.addChild(node)
            if eg.getFirstChild : yield eg
      
def iterChildren(eggNode):
    """ iterate all children of a node """
    try:
        child = eggNode.getFirstChild()
        while child:
            yield child
            child = eggNode.getNextChild()
    except:
        pass
    
def iterVertexes(eggNode):
    """ iterate all vertexes of polygon or polylist """
    try:
        index = eggNode.getHighestIndex()
        for i in xrange(index+1):
            yield eggNode.getVertex(i)
    except:
        index = eggNode.getNumVertices()
        for i in xrange(index):
            yield eggNode.getVertex(i)
        pass

def eggLs(eggNode,indent=0):
    """ list whats in our egg """
    if eggNode.__class__.__name__ != "EggPolygon":
        print " "*indent+eggNode.__class__.__name__+" "+eggNode.getName()
        for eggChildren in iterChildren(eggNode):
            eggLs(eggChildren,indent+1)
        
def eggStripTexture(eggNode):
    """ strip textures and materials """
    if eggNode.__class__ == EggPolygon:
        eggNode.clearTexture() 
        eggNode.clearMaterial()       
    else:
        for eggChildren in iterChildren(eggNode):
            eggStripTexture(eggChildren)
            
            
def octreefy(infile,outfile):
    """ 
        octreefy infile and write to outfile 
        using the buildOctree functions 
    """
    egg = EggData()
    egg.read(Filename(infile))
    eggStripTexture(egg)
    group = egg
    vertexPool = False
    # find the fist group and fine the first vertexPool
    # you might have to mess with this if your egg files 
    # are in odd format
    for child in iterChildren(egg):
        if type(child) == EggVertexPool:
            vertexPool = child
        if type(child) == EggGroup:
            group = child
    # if we have not found the vertexPool it must be inside 
    if not vertexPool: 
        for child in iterChildren(group):
            if type(child) == EggVertexPool:
                vertexPool = child
    if vertexPool and group:
        ed = EggData()
        ed.setCoordinateSystem(egg.getCoordinateSystem())
        ed.addChild(vertexPool)
        ed.addChild(buildOctree(group))
        if listResultingEgg: eggLs(ed)
        ed.writeEgg(Filename(outfile))
        
def main():
    """ interface to our egg octreefier """
    try:
        optlist, list = getopt.getopt(sys.argv[1:], 'hlvo:n:')
    except Exception,e:
        print e
        sys.exit(0)
    global verbose,listResultingEgg,maxNumber
    outfile = False
    for opt in optlist:
        if opt[0] == '-h':
            print __doc__
            sys.exit(0)
        if opt[0] == '-l':
            listResultingEgg = True
        if opt[0] == '-v':
            verbose = True
        if opt[0] == '-n':
            maxNumber = int(opt[1])
        if opt[0] == '-o':
            outfile = opt[1]
    if outfile and len(list) > 1:
        print "error can have an outfile and more then one infile"
        sys.exit(0)
        
    for file in list:
        if '.egg' in file:
            if verbose: print "processing",file
            if outfile:
                octreefy(file,outfile)
            else:
                octreefy(file,file.replace(".egg","-octree.egg"))
                  
if __name__ == "__main__":
    import os
    main()

hey treeform,
i desperatly tried to get sane results with your egg octee script.
for some models it produced output files, for some it didnt. and the files it produced did not show a thing when opened with pview or loaded elsewhise. contained some vertexpoll and groups but nothing that would draw at all.
i tried a whole bunch of models, blender chicken exports, x2egg, models with textures, and without.
it does not throw any errors or anything.
any chance you can fix it up so its working(again)?

I haven’t tried treeform’s.
Does shift-c in pview help, Thomas ?

shift-c shows the mesh. but it has many holes, especially smaller triangles are missing. many of them.
some models however wont get converted at all. it doesnt even create an outputfile.
i already asked treeform on irc to have a look at it, and if possible make it work for visible geometry,too. since octrees can be a major speedup and allows generic map optimisation like ->flatenStrong->octree. which is usefull in many cases.

could you provide the models you have problems with?

If there is no error at all most likely it seg faulted … so i would need your models to track that.

I ran some tests on it no holes or stuff found …

http://home.arcor.de/positiveelectron/files/octree-models.zip
one has holes, the other does not produce any output at all

Hey ^^:, I can’t for the life of me understand how to get a working model to work. How do you get this to work? Don’t point me to the 1st post, cuase I don’t get how to get it to from that lol.

if you use my script:

python egg-octreefy.py -o level-collision.egg level-visual.egg

Then you just load one file as visual object and the other as the collision object.

Cool it works. After a little testing I fine it only works on one object with in a .egg. Is there any way to for “loop” it to do all the objects in my map?

I’ve done some fiddling with treeforms version of the octree script. I made a bunch of changes, the most important being the originals assumption that you would only want to use an octree for collision detection - it now allows you to generate an octree keeping its materials/textures and without sticking ‘barrier’ on everything. Basically run it without the -c option for a rendering version, with the -c version for a collision detection version. An extra parameter - maximum recursion depth, has also been added. Its also gained a couple of minor bug fixes.

Whilst I have tested it on a bunch of stuff I don’t exactly have many meshes of the sort you would really throw at this kind of script lying around, so can’t be sure I’ve selected good default parameters, or that all bugs are toast. In case anyone is wondering why I’m playing with this I’ll let you guess what chickens next feature is going to be;-)

Get it here: http://thaines.net/content/panda3d/eggoctree.py

are you trying to use octrees on visual geometry? or why did you make those changes?

Not right now, but applying octrees is good for large levels, especially if you can set the far clipping plane close compared to the level size - i.e. in the tight and windy corridor type of fps. I’m trying to make Blender into a viable level editor - and it now has octree support for the output egg file, suitable for both visible geometry or collision detection geometry, at least in the current svn version. Might do a release tomorrow, but I want to clean up the manual first and not sure if I will get the time.

Im wondering if exporting my terrain.egg could benefit from octree. I have created a ~19000 poly terrain mesh, exported it out of blender chicken r60, and into ynjh_jo’s roaming ralph variation with gravity (discourse.panda3d.org/viewtopic.php?t=2924). Everything seems great, except that sometimes, on inclines mostly but not always, poor little ralph can “push” through the terrain and back again.

Can Egg Octree possibly be a soloution to this? Would using a geomipterrain be possible/better?

Egg occtrees do little for for visible geometry. I would only use them for collision only. If you want terrain use GeoMipMap.

To actually answer metel3d’s question, no - neither octrees nor the use of geomipterrain fix falling through the terrain - that is a failure of the collision system where you fall through a crack between the edges of polygons. The usual solution is to use multiple collision detection calls, so if one fails another will stop you falling through. (i.e. fire down multiple spaced out rays for instance.) I would use the geomipterrain to speed up basic rendering though.

Treeform: There are many situations where using octrees for your visible environment will result in a useful speed up - its dependent on the environment in question. This is especially the case for large dense environments where forward visibility is limited - there it could be the difference between playable and not.