octreefy optimize collisions

Where to find correct code for octreefy a panda egg file for collision optimization?
Some code on the forum is either not accessible or not working properly.

There are a few different versions rattling around. I personally like the version I wrote, but it works on loaded models (node paths) not eggs:
https://github.com/Craig-Macomber/Panda3D-Terrain-System/blob/master/collisionUtil.py

General use is something like:

    map.clearModelNodes()
    
    col=collisionUtil.rebuildGeomNodesToColPolys(map)
    col=collisionUtil.colTree(col)
    col.setCollideMask(collisionUtil.groundMask)
    
    map.flattenMedium()

    map2=NodePath("MAP")
    map.reparentTo(map2)
    col.reparentTo(map2)

thank you Craig, your code seems to meet my needs.
Honestly I still don’t get the use of clearModelNodes() and flattenMedium() .Could you please elaborate?

What about:

#for geometry only
map = loader.loadModel("model.egg")
map.reparentTo(render)
map.clearModelNodes() #use??
map.flattenMedium()   #why not flattenStrong?

#for collision only
col=collisionUtil.rebuildGeomNodesToColPolys(map)
col.flattenStrong()   #should this help???
col=collisionUtil.colTree(col)
col.setCollideMask(collisionUtil.groundMask)
col.reparentTo(map)

Here’s the version I’ve been using. It works with egg files. It’s know as “by treeform, modified by lethe”:

#!/usr/bin/python
"""
                             _                 
                            | |               
  ___  __ _  __ _  ___   ___| |_ _ __ ___  ___
 / _ \/ _` |/ _` |/ _ \ / __| __| '__/ _ \/ _ \
|  __/ (_| | (_| | (_) | (__| |_| | |  __/  __/
 \___|\__, |\__, |\___/ \___|\__|_|  \___|\___|
       __/ | __/ | by treeform, modified by lethe
      |___/ |___/                                                             
                             
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 512, 3 if -c)
-r     maximum recursion depth (default 4, 32 if -c)
-c     prepares the output for collision use rather than rendering
if outfile is not specified "infile"-octree.egg assumed
"""

import sys, getopt
import math
from pandac.PandaModules import *


global verbose, listResultingEgg, maxNumber, maxRec, prepCollide

verbose = False
listResultingEgg = False
maxNumber = -1
maxRec = -1
prepCollide = False


def getCenter(polywrapList):
  """Given a list of PolyWrap-s this calculates their centre (mean), as their centre is the mean of their vertices this is a weighted average of the vertices. (When faces are not triangulated, un-weighted if they are.)"""
  # Loop on the PolyWrap-s to determine the meany...
  center = Point3D(0,0,0)
  for pw in polywrapList:
    center += pw.center
  
  num = len(polywrapList)
  if num>0: center /= float(num)
  
  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(polywrapList,center):
  """ put all poly wraps into quadrents """
  quadrants = ((([],[]),
                ([],[])),
               (([],[]),
                ([],[])))
  for pw in polywrapList:
    pwPos = pw.center
    x =  pwPos[0] > center[0]
    y =  pwPos[1] > center[1]
    z =  pwPos[2] > center[2]
    quadrants[x][y][z].append(pw)
  quadrants = flatten(quadrants)
  return quadrants


class Polywrap:
  """
  Simple wrapper for a polygon that also stores the
  polygons vertex mean, 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 from a group of polygons, which could be in sub-groups """
  for polygon in iterGroupChildren(group):
    if type(polygon) == EggPolygon:
      center = Vec3D()
      num = 0
      for vtx in iterVertexes(polygon):
        center += vtx.getPos3()
        num += 1
      if num>0: center /= float(num)
      
      pw = Polywrap()
      pw.polygon = polygon
      pw.center = center
      yield pw


def buildOctree(group):
    """ build an octree form a egg group """
    global verbose, prepCollide
    if prepCollide: group.triangulatePolygons(0xff)
    polywraps = [i for i in genPolyWraps(group)]
    if verbose: print len(polywraps),"polygons"
    
    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 = 1):
  """
  visit each quadrent and create octree there
  all the end consolidate all octrees into egg groups
  """
  global verbose, maxNumber, maxRec, prepCollide
  qs = [i for i in quadrants]
  if verbose: print "  "*indent,"8 quadrents have ",[len(i) for i in qs]," polygons"
  
  for i, quadrent in enumerate(qs):
    if len(quadrent) == 0:
      if verbose: print "  "*indent," no polygons in quadrent"
      continue
    elif (len(quadrent) <= maxNumber) or (indent >= maxRec):
      center = getCenter(quadrent)
      if verbose: print "  "*indent," triangle center", center, len(quadrent)
      eg = EggGroup('leaf %i-%i (%i tri)'%(indent,i,len(quadrent)))
      if prepCollide: eg.addObjectType('barrier')
      for pw in quadrent:
        eg.addChild(pw.polygon)
      if eg.getFirstChild : yield eg
    else:
      eg = EggGroup('branch-%i-%i (%i tri)'%(indent,i,len(quadrent)))
      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 iterGroupChildren(eggNode):
  """ iterate all children of groups under a node that are not themselves groups"""
  for child in iterChildren(eggNode):
    if type(child) == EggGroup:
      for i in iterGroupChildren(child):
        yield i
    else:
      yield child


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
  """
  global verbose, maxNumber, maxRec, prepCollide
  egg = EggData()
  egg.read(Filename(infile))
  if prepCollide: eggStripTexture(egg)
  group = egg
  vertexPool = False
  comment = None
  if verbose:
    print 'Input:'
    eggLs(egg)

  # find the fist group and find 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 type(child) == EggComment:
      comment = 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()

    # More the textures and materials from the old egg file if needed - this wrecks the old file...
    if not prepCollide:
      for child in iterChildren(egg):
        if type(child) in [EggMaterial,EggTexture]:
          ed.addChild(child)

    ed.setCoordinateSystem(egg.getCoordinateSystem())

    com = 'File has been octree-ed (-n %i -r %i%s)'%(maxNumber,maxRec,['',' -c'][prepCollide])
    if comment:
      ed.addChild(EggComment('',com+'; '+comment.getComment()))
    else:
      ed.addChild(EggComment('',com))
    
    ed.addChild(vertexPool)
    ed.addChild(buildOctree(group))
    if listResultingEgg: eggLs(ed)
    ed.writeEgg(Filename(outfile))
  else:
    print 'Could not find vertexPool or group'


def main():
  """ interface to our egg octreefier """
  try:
    optlist, list = getopt.getopt(sys.argv[1:], 'hlvo:n:r:c')
  except Exception,e:
    print e
    sys.exit(0)

  global verbose, listResultingEgg, maxNumber, maxRec, prepCollide
  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] == '-r':
      maxRec = int(opt[1])
    if opt[0] == '-c':
      prepCollide = True
    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)

  if maxNumber==-1:
    if prepCollide: maxNumber = 3
    else: maxNumber = 512
  if maxRec==-1:
    if prepCollide: maxRec = 32
    else: maxRec = 4

  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()

@wezu: I tried this already but for some reason (certainly due to the original egg file structure) it doesn’t generate a correct octree…

They arn’t important for the collision stuff, its just part of my rendering optimization stuff that happened to be mixed in with my collision generation code.

I wanted to share the script that I adjusted slightly to run with python3:

"""
                              _                 
                             | |               
   ___  __ _  __ _  ___   ___| |_ _ __ ___  ___
  / _ \/ _` |/ _` |/ _ \ / __| __| '__/ _ \/ _ \ 
 |  __/ (_| | (_| | (_) | (__| |_| | |  __/  __/
  \___|\__, |\__, |\___/ \___|\__|_|  \___|\___|
        __/ | __/ | by treeform, modified by lethe
       |___/ |___/                                                             
                             
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 512, 3 if -c)
-r     maximum recursion depth (default 4, 32 if -c)
-c     prepares the output for collision use rather than rendering
if outfile is not specified "infile"-octree.egg assumed
"""

import sys, getopt
import math
from panda3d.core import *
from panda3d.egg import *

global verbose, listResultingEgg, maxNumber, maxRec, prepCollide

verbose = False
listResultingEgg = False
maxNumber = -1
maxRec = -1
prepCollide = False


def getCenter(polywrapList):
  """Given a list of PolyWrap-s this calculates their centre (mean), as their centre is the mean of their vertices this is a weighted average of the vertices. (When faces are not triangulated, un-weighted if they are.)"""
  # Loop on the PolyWrap-s to determine the meany...
  center = Point3D(0,0,0)
  for pw in polywrapList:
    center += pw.center
  
  num = len(polywrapList)
  if num>0: center /= float(num)
  
  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(polywrapList,center):
  """ put all poly wraps into quadrents """
  quadrants = ((([],[]),
                ([],[])),
               (([],[]),
                ([],[])))
  for pw in polywrapList:
    pwPos = pw.center
    x =  pwPos[0] > center[0]
    y =  pwPos[1] > center[1]
    z =  pwPos[2] > center[2]
    quadrants[x][y][z].append(pw)
  quadrants = flatten(quadrants)
  return quadrants


class Polywrap:
  """
  Simple wrapper for a polygon that also stores the
  polygons vertex mean, 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 from a group of polygons, which could be in sub-groups """
  for polygon in iterGroupChildren(group):
    if type(polygon) == EggPolygon:
      center = Vec3D()
      num = 0
      for vtx in iterVertexes(polygon):
        center += vtx.getPos3()
        num += 1
      if num>0: center /= float(num)
      
      pw = Polywrap()
      pw.polygon = polygon
      pw.center = center
      yield pw


def buildOctree(group):
    """ build an octree form a egg group """
    global verbose, prepCollide
    if prepCollide: group.triangulatePolygons(0xff)
    polywraps = [i for i in genPolyWraps(group)]
    if verbose: print (len(polywraps),"polygons")
    
    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 = 1):
  """
  visit each quadrent and create octree there
  all the end consolidate all octrees into egg groups
  """
  global verbose, maxNumber, maxRec, prepCollide
  qs = [i for i in quadrants]
  if verbose: print ("  "*indent,"8 quadrents have ",[len(i) for i in qs]," polygons")
  
  for i, quadrent in enumerate(qs):
    if len(quadrent) == 0:
      if verbose: print ("  "*indent," no polygons in quadrent")
      continue
    elif (len(quadrent) <= maxNumber) or (indent >= maxRec):
      center = getCenter(quadrent)
      if verbose: print ("  "*indent," triangle center", center, len(quadrent))
      eg = EggGroup('leaf %i-%i (%i tri)'%(indent,i,len(quadrent)))
      if prepCollide: eg.addObjectType('barrier')
      for pw in quadrent:
        eg.addChild(pw.polygon)
      if eg.getFirstChild : yield eg
    else:
      eg = EggGroup('branch-%i-%i (%i tri)'%(indent,i,len(quadrent)))
      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 iterGroupChildren(eggNode):
  """ iterate all children of groups under a node that are not themselves groups"""
  for child in iterChildren(eggNode):
    if type(child) == EggGroup:
      for i in iterGroupChildren(child):
        yield i
    else:
      yield child


def iterVertexes(eggNode):
  """ iterate all vertexes of polygon or polylist """
  try:
    index = eggNode.getHighestIndex()
    for i in range(index+1):
      yield eggNode.getVertex(i)
  except:
    index = eggNode.getNumVertices()
    for i in range(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
  """
  global verbose, maxNumber, maxRec, prepCollide
  egg = EggData()
  egg.read(Filename(infile))
  if prepCollide: eggStripTexture(egg)
  group = egg
  vertexPool = False
  comment = None
  if verbose:
    print ('Input:')
    eggLs(egg)

  # find the fist group and find 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 type(child) == EggComment:
      comment = 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()

    # More the textures and materials from the old egg file if needed - this wrecks the old file...
    if not prepCollide:
      for child in iterChildren(egg):
        if type(child) in [EggMaterial,EggTexture]:
          ed.addChild(child)

    ed.setCoordinateSystem(egg.getCoordinateSystem())

    com = 'File has been octree-ed (-n %i -r %i%s)'%(maxNumber,maxRec,['',' -c'][prepCollide])
    if comment:
      ed.addChild(EggComment('',com+'; '+comment.getComment()))
    else:
      ed.addChild(EggComment('',com))
    
    ed.addChild(vertexPool)
    ed.addChild(buildOctree(group))
    if listResultingEgg: eggLs(ed)
    ed.writeEgg(Filename(outfile))
  else:
    print ('Could not find vertexPool or group')


def main():
  """ interface to our egg octreefier """
  try:
    optlist, list = getopt.getopt(sys.argv[1:], 'hlvo:n:r:c')
  except Exception as error:
    print (error)
    sys.exit(0)

  global verbose, listResultingEgg, maxNumber, maxRec, prepCollide
  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] == '-r':
      maxRec = int(opt[1])
    if opt[0] == '-c':
      prepCollide = True
    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)

  if maxNumber==-1:
    if prepCollide: maxNumber = 3
    else: maxNumber = 512
  if maxRec==-1:
    if prepCollide: maxRec = 32
    else: maxRec = 4

  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()

Also see this script, which operates on a loaded model and doesn’t require egg files: