portal_culling.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. #!/usr/bin/env python
  2. """
  3. Author: Josh Enes
  4. Last Updated: 2015-03-13
  5. This is a demo of Panda's portal-culling system. It demonstrates loading
  6. portals from an EGG file, and shows an example method of selecting the
  7. current cell using geoms and a collision ray.
  8. """
  9. # Some config options which can be changed.
  10. ENABLE_PORTALS = True # Set False to disable portal culling and see FPS drop!
  11. DEBUG_PORTALS = False # Set True to see visually which portals are used
  12. # Load PRC data
  13. from panda3d.core import loadPrcFileData
  14. if ENABLE_PORTALS:
  15. loadPrcFileData('', 'allow-portal-cull true')
  16. if DEBUG_PORTALS:
  17. loadPrcFileData('', 'debug-portal-cull true')
  18. loadPrcFileData('', 'window-title Portal Demo')
  19. loadPrcFileData('', 'sync-video false')
  20. loadPrcFileData('', 'show-frame-rate-meter true')
  21. loadPrcFileData('', 'texture-minfilter linear-mipmap-linear')
  22. # Import needed modules
  23. import random
  24. from direct.showbase.ShowBase import ShowBase
  25. from direct.gui.OnscreenText import OnscreenText
  26. from panda3d.core import PerspectiveLens, NodePath, LVector3, LPoint3, \
  27. TexGenAttrib, TextureStage, TransparencyAttrib, CollisionTraverser, \
  28. CollisionHandlerQueue, TextNode, CollisionRay, CollisionNode
  29. def add_instructions(pos, msg):
  30. """Function to put instructions on the screen."""
  31. return OnscreenText(text=msg, style=1, fg=(1, 1, 1, 1), shadow=(0, 0, 0, 1),
  32. parent=base.a2dTopLeft, align=TextNode.ALeft,
  33. pos=(0.08, -pos - 0.04), scale=.05)
  34. def add_title(text):
  35. """Function to put title on the screen."""
  36. return OnscreenText(text=text, style=1, pos=(-0.1, 0.09), scale=.08,
  37. parent=base.a2dBottomRight, align=TextNode.ARight,
  38. fg=(1, 1, 1, 1), shadow=(0, 0, 0, 1))
  39. class Game(ShowBase):
  40. """Sets up the game, camera, controls, and loads models."""
  41. def __init__(self):
  42. ShowBase.__init__(self)
  43. self.cellmanager = CellManager(self)
  44. self.xray_mode = False
  45. self.show_model_bounds = False
  46. # Display instructions
  47. add_title("Panda3D Tutorial: Portal Culling")
  48. add_instructions(0.06, "[Esc]: Quit")
  49. add_instructions(0.12, "[W]: Move Forward")
  50. add_instructions(0.18, "[A]: Move Left")
  51. add_instructions(0.24, "[S]: Move Right")
  52. add_instructions(0.30, "[D]: Move Back")
  53. add_instructions(0.36, "Arrow Keys: Look Around")
  54. add_instructions(0.42, "[F]: Toggle Wireframe")
  55. add_instructions(0.48, "[X]: Toggle X-Ray Mode")
  56. add_instructions(0.54, "[B]: Toggle Bounding Volumes")
  57. # Setup controls
  58. self.keys = {}
  59. for key in ['arrow_left', 'arrow_right', 'arrow_up', 'arrow_down',
  60. 'a', 'd', 'w', 's']:
  61. self.keys[key] = 0
  62. self.accept(key, self.push_key, [key, 1])
  63. self.accept('shift-%s' % key, self.push_key, [key, 1])
  64. self.accept('%s-up' % key, self.push_key, [key, 0])
  65. self.accept('f', self.toggleWireframe)
  66. self.accept('x', self.toggle_xray_mode)
  67. self.accept('b', self.toggle_model_bounds)
  68. self.accept('escape', __import__('sys').exit, [0])
  69. self.disableMouse()
  70. # Setup camera
  71. lens = PerspectiveLens()
  72. lens.setFov(60)
  73. lens.setNear(0.01)
  74. lens.setFar(1000.0)
  75. self.cam.node().setLens(lens)
  76. self.camera.setPos(-9, -0.5, 1)
  77. self.heading = -95.0
  78. self.pitch = 0.0
  79. # Load level geometry
  80. self.level_model = self.loader.loadModel('models/level')
  81. self.level_model.reparentTo(self.render)
  82. self.level_model.setTexGen(TextureStage.getDefault(),
  83. TexGenAttrib.MWorldPosition)
  84. self.level_model.setTexProjector(TextureStage.getDefault(),
  85. self.render, self.level_model)
  86. self.level_model.setTexScale(TextureStage.getDefault(), 4)
  87. tex = self.loader.load3DTexture('models/tex_#.png')
  88. self.level_model.setTexture(tex)
  89. # Load cells
  90. self.cellmanager.load_cells_from_model('models/cells')
  91. # Load portals
  92. self.cellmanager.load_portals_from_model('models/portals')
  93. # Randomly spawn some models to test the portals
  94. self.models = []
  95. for dummy in range(0, 500):
  96. pos = LPoint3((random.random() - 0.5) * 6,
  97. (random.random() - 0.5) * 6,
  98. random.random() * 7)
  99. cell = self.cellmanager.get_cell(pos)
  100. if cell is None: # skip if the random position is not over a cell
  101. continue
  102. dist = self.cellmanager.get_dist_to_cell(pos)
  103. if dist > 1.5: # skip if the random position is too far from ground
  104. continue
  105. box = self.loader.loadModel('box')
  106. box.setScale(random.random() * 0.2 + 0.1)
  107. box.setPos(pos)
  108. box.setHpr(random.random() * 360,
  109. random.random() * 360,
  110. random.random() * 360)
  111. box.reparentTo(cell.nodepath)
  112. self.models.append(box)
  113. self.taskMgr.add(self.update, 'main loop')
  114. def push_key(self, key, value):
  115. """Stores a value associated with a key."""
  116. self.keys[key] = value
  117. def update(self, task):
  118. """Updates the camera based on the keyboard input. Once this is
  119. done, then the CellManager's update function is called."""
  120. delta = base.clock.dt
  121. move_x = delta * 3 * -self.keys['a'] + delta * 3 * self.keys['d']
  122. move_z = delta * 3 * self.keys['s'] + delta * 3 * -self.keys['w']
  123. self.camera.setPos(self.camera, move_x, -move_z, 0)
  124. self.heading += (delta * 90 * self.keys['arrow_left'] +
  125. delta * 90 * -self.keys['arrow_right'])
  126. self.pitch += (delta * 90 * self.keys['arrow_up'] +
  127. delta * 90 * -self.keys['arrow_down'])
  128. self.camera.setHpr(self.heading, self.pitch, 0)
  129. if ENABLE_PORTALS:
  130. self.cellmanager.update()
  131. return task.cont
  132. def toggle_xray_mode(self):
  133. """Toggle X-ray mode on and off. This is useful for seeing the
  134. effectiveness of the portal culling."""
  135. self.xray_mode = not self.xray_mode
  136. if self.xray_mode:
  137. self.level_model.setColorScale((1, 1, 1, 0.5))
  138. self.level_model.setTransparency(TransparencyAttrib.MDual)
  139. else:
  140. self.level_model.setColorScaleOff()
  141. self.level_model.setTransparency(TransparencyAttrib.MNone)
  142. def toggle_model_bounds(self):
  143. """Toggle bounding volumes on and off on the models."""
  144. self.show_model_bounds = not self.show_model_bounds
  145. if self.show_model_bounds:
  146. for model in self.models:
  147. model.showBounds()
  148. else:
  149. for model in self.models:
  150. model.hideBounds()
  151. class CellManager(object):
  152. """Creates a collision ray and collision traverser to use for
  153. selecting the current cell."""
  154. def __init__(self, game):
  155. self.game = game
  156. self.cells = {}
  157. self.cells_by_collider = {}
  158. self.cell_picker_world = NodePath('cell_picker_world')
  159. self.ray = CollisionRay()
  160. self.ray.setDirection(LVector3.down())
  161. cnode = CollisionNode('cell_raycast_cnode')
  162. self.ray_nodepath = self.cell_picker_world.attachNewNode(cnode)
  163. self.ray_nodepath.node().addSolid(self.ray)
  164. self.ray_nodepath.node().setIntoCollideMask(0) # not for colliding into
  165. self.ray_nodepath.node().setFromCollideMask(1)
  166. self.traverser = CollisionTraverser('traverser')
  167. self.last_known_cell = None
  168. def add_cell(self, collider, name):
  169. """Add a new cell."""
  170. cell = Cell(self, name, collider)
  171. self.cells[name] = cell
  172. self.cells_by_collider[collider.node()] = cell
  173. def get_cell(self, pos):
  174. """Given a position, return the nearest cell below that position.
  175. If no cell is found, returns None."""
  176. self.ray.setOrigin(pos)
  177. queue = CollisionHandlerQueue()
  178. self.traverser.addCollider(self.ray_nodepath, queue)
  179. self.traverser.traverse(self.cell_picker_world)
  180. self.traverser.removeCollider(self.ray_nodepath)
  181. queue.sortEntries()
  182. if not queue.getNumEntries():
  183. return None
  184. entry = queue.getEntry(0)
  185. cnode = entry.getIntoNode()
  186. try:
  187. return self.cells_by_collider[cnode]
  188. except KeyError:
  189. raise Warning('collision ray collided with something '
  190. 'other than a cell: %s' % cnode)
  191. def get_dist_to_cell(self, pos):
  192. """Given a position, return the distance to the nearest cell
  193. below that position. If no cell is found, returns None."""
  194. self.ray.setOrigin(pos)
  195. queue = CollisionHandlerQueue()
  196. self.traverser.addCollider(self.ray_nodepath, queue)
  197. self.traverser.traverse(self.cell_picker_world)
  198. self.traverser.removeCollider(self.ray_nodepath)
  199. queue.sortEntries()
  200. if not queue.getNumEntries():
  201. return None
  202. entry = queue.getEntry(0)
  203. return (entry.getSurfacePoint(self.cell_picker_world) - pos).length()
  204. def load_cells_from_model(self, modelpath):
  205. """Loads cells from an EGG file. Cells must be named in the
  206. format "cell#" to be loaded by this function."""
  207. cell_model = self.game.loader.loadModel(modelpath)
  208. for collider in cell_model.findAllMatches('**/+GeomNode'):
  209. name = collider.getName()
  210. if name.startswith('cell'):
  211. self.add_cell(collider, name[4:])
  212. cell_model.removeNode()
  213. def load_portals_from_model(self, modelpath):
  214. """Loads portals from an EGG file. Portals must be named in the
  215. format "portal_#to#_*" to be loaded by this function, whereby the
  216. first # is the from cell, the second # is the into cell, and * can
  217. be anything."""
  218. portal_model = loader.loadModel(modelpath)
  219. portal_nodepaths = portal_model.findAllMatches('**/+PortalNode')
  220. for portal_nodepath in portal_nodepaths:
  221. name = portal_nodepath.getName()
  222. if name.startswith('portal_'):
  223. from_cell_id, into_cell_id = name.split('_')[1].split('to')
  224. try:
  225. from_cell = self.cells[from_cell_id]
  226. except KeyError:
  227. print ('could not load portal "%s" because cell "%s"'
  228. 'does not exist' % (name, from_cell_id))
  229. continue
  230. try:
  231. into_cell = self.cells[into_cell_id]
  232. except KeyError:
  233. print ('could not load portal "%s" because cell "%s"'
  234. 'does not exist' % (name, into_cell_id))
  235. continue
  236. from_cell.add_portal(portal_nodepath, into_cell)
  237. portal_model.removeNode()
  238. def update(self):
  239. """Show the cell the camera is currently in and hides the rest.
  240. If the camera is not in a cell, use the last known cell that the
  241. camera was in. If the camera has not yet been in a cell, then all
  242. cells will be hidden."""
  243. camera_pos = self.game.camera.getPos(self.game.render)
  244. for cell in self.cells:
  245. self.cells[cell].nodepath.hide()
  246. current_cell = self.get_cell(camera_pos)
  247. if current_cell is None:
  248. if self.last_known_cell is None:
  249. return
  250. self.last_known_cell.nodepath.show()
  251. else:
  252. self.last_known_cell = current_cell
  253. current_cell.nodepath.show()
  254. class Cell(object):
  255. """The Cell class is a handy way to keep an association between
  256. all the related nodes and information of a cell."""
  257. def __init__(self, cellmanager, name, collider):
  258. self.cellmanager = cellmanager
  259. self.name = name
  260. self.collider = collider
  261. self.collider.reparentTo(self.cellmanager.cell_picker_world)
  262. self.collider.setCollideMask(1)
  263. self.collider.hide()
  264. self.nodepath = NodePath('cell_%s_root' % name)
  265. self.nodepath.reparentTo(self.cellmanager.game.render)
  266. self.portals = []
  267. def add_portal(self, portal, cell_out):
  268. """Add a portal from this cell going into another one."""
  269. portal.reparentTo(self.nodepath)
  270. portal.node().setCellIn(self.nodepath)
  271. portal.node().setCellOut(cell_out.nodepath)
  272. self.portals.append(portal)
  273. game = Game()
  274. game.run()