306 lines
12 KiB
Python
306 lines
12 KiB
Python
#!/usr/bin/env python
|
|
|
|
"""
|
|
Author: Josh Enes
|
|
Last Updated: 2015-03-13
|
|
|
|
This is a demo of Panda's portal-culling system. It demonstrates loading
|
|
portals from an EGG file, and shows an example method of selecting the
|
|
current cell using geoms and a collision ray.
|
|
"""
|
|
|
|
# Some config options which can be changed.
|
|
ENABLE_PORTALS = True # Set False to disable portal culling and see FPS drop!
|
|
DEBUG_PORTALS = False # Set True to see visually which portals are used
|
|
|
|
# Load PRC data
|
|
from panda3d.core import loadPrcFileData
|
|
if ENABLE_PORTALS:
|
|
loadPrcFileData('', 'allow-portal-cull true')
|
|
if DEBUG_PORTALS:
|
|
loadPrcFileData('', 'debug-portal-cull true')
|
|
loadPrcFileData('', 'window-title Portal Demo')
|
|
loadPrcFileData('', 'sync-video false')
|
|
loadPrcFileData('', 'show-frame-rate-meter true')
|
|
loadPrcFileData('', 'texture-minfilter linear-mipmap-linear')
|
|
|
|
# Import needed modules
|
|
import random
|
|
from direct.showbase.ShowBase import ShowBase
|
|
from direct.gui.OnscreenText import OnscreenText
|
|
from panda3d.core import PerspectiveLens, NodePath, LVector3, LPoint3, \
|
|
TexGenAttrib, TextureStage, TransparencyAttrib, CollisionTraverser, \
|
|
CollisionHandlerQueue, TextNode, CollisionRay, CollisionNode
|
|
|
|
|
|
def add_instructions(pos, msg):
|
|
"""Function to put instructions on the screen."""
|
|
return OnscreenText(text=msg, style=1, fg=(1, 1, 1, 1), shadow=(0, 0, 0, 1),
|
|
parent=base.a2dTopLeft, align=TextNode.ALeft,
|
|
pos=(0.08, -pos - 0.04), scale=.05)
|
|
|
|
def add_title(text):
|
|
"""Function to put title on the screen."""
|
|
return OnscreenText(text=text, style=1, pos=(-0.1, 0.09), scale=.08,
|
|
parent=base.a2dBottomRight, align=TextNode.ARight,
|
|
fg=(1, 1, 1, 1), shadow=(0, 0, 0, 1))
|
|
|
|
|
|
class Game(ShowBase):
|
|
"""Sets up the game, camera, controls, and loads models."""
|
|
def __init__(self):
|
|
ShowBase.__init__(self)
|
|
self.cellmanager = CellManager(self)
|
|
self.xray_mode = False
|
|
self.show_model_bounds = False
|
|
|
|
# Display instructions
|
|
add_title("Panda3D Tutorial: Portal Culling")
|
|
add_instructions(0.06, "[Esc]: Quit")
|
|
add_instructions(0.12, "[W]: Move Forward")
|
|
add_instructions(0.18, "[A]: Move Left")
|
|
add_instructions(0.24, "[S]: Move Right")
|
|
add_instructions(0.30, "[D]: Move Back")
|
|
add_instructions(0.36, "Arrow Keys: Look Around")
|
|
add_instructions(0.42, "[F]: Toggle Wireframe")
|
|
add_instructions(0.48, "[X]: Toggle X-Ray Mode")
|
|
add_instructions(0.54, "[B]: Toggle Bounding Volumes")
|
|
|
|
# Setup controls
|
|
self.keys = {}
|
|
for key in ['arrow_left', 'arrow_right', 'arrow_up', 'arrow_down',
|
|
'a', 'd', 'w', 's']:
|
|
self.keys[key] = 0
|
|
self.accept(key, self.push_key, [key, 1])
|
|
self.accept('shift-%s' % key, self.push_key, [key, 1])
|
|
self.accept('%s-up' % key, self.push_key, [key, 0])
|
|
self.accept('f', self.toggleWireframe)
|
|
self.accept('x', self.toggle_xray_mode)
|
|
self.accept('b', self.toggle_model_bounds)
|
|
self.accept('escape', __import__('sys').exit, [0])
|
|
self.disableMouse()
|
|
|
|
# Setup camera
|
|
lens = PerspectiveLens()
|
|
lens.setFov(60)
|
|
lens.setNear(0.01)
|
|
lens.setFar(1000.0)
|
|
self.cam.node().setLens(lens)
|
|
self.camera.setPos(-9, -0.5, 1)
|
|
self.heading = -95.0
|
|
self.pitch = 0.0
|
|
|
|
# Load level geometry
|
|
self.level_model = self.loader.loadModel('models/level')
|
|
self.level_model.reparentTo(self.render)
|
|
self.level_model.setTexGen(TextureStage.getDefault(),
|
|
TexGenAttrib.MWorldPosition)
|
|
self.level_model.setTexProjector(TextureStage.getDefault(),
|
|
self.render, self.level_model)
|
|
self.level_model.setTexScale(TextureStage.getDefault(), 4)
|
|
tex = self.loader.load3DTexture('models/tex_#.png')
|
|
self.level_model.setTexture(tex)
|
|
|
|
# Load cells
|
|
self.cellmanager.load_cells_from_model('models/cells')
|
|
# Load portals
|
|
self.cellmanager.load_portals_from_model('models/portals')
|
|
|
|
# Randomly spawn some models to test the portals
|
|
self.models = []
|
|
for dummy in range(0, 500):
|
|
pos = LPoint3((random.random() - 0.5) * 6,
|
|
(random.random() - 0.5) * 6,
|
|
random.random() * 7)
|
|
cell = self.cellmanager.get_cell(pos)
|
|
if cell is None: # skip if the random position is not over a cell
|
|
continue
|
|
dist = self.cellmanager.get_dist_to_cell(pos)
|
|
if dist > 1.5: # skip if the random position is too far from ground
|
|
continue
|
|
box = self.loader.loadModel('box')
|
|
box.setScale(random.random() * 0.2 + 0.1)
|
|
box.setPos(pos)
|
|
box.setHpr(random.random() * 360,
|
|
random.random() * 360,
|
|
random.random() * 360)
|
|
box.reparentTo(cell.nodepath)
|
|
self.models.append(box)
|
|
self.taskMgr.add(self.update, 'main loop')
|
|
|
|
def push_key(self, key, value):
|
|
"""Stores a value associated with a key."""
|
|
self.keys[key] = value
|
|
|
|
def update(self, task):
|
|
"""Updates the camera based on the keyboard input. Once this is
|
|
done, then the CellManager's update function is called."""
|
|
delta = globalClock.getDt()
|
|
move_x = delta * 3 * -self.keys['a'] + delta * 3 * self.keys['d']
|
|
move_z = delta * 3 * self.keys['s'] + delta * 3 * -self.keys['w']
|
|
self.camera.setPos(self.camera, move_x, -move_z, 0)
|
|
self.heading += (delta * 90 * self.keys['arrow_left'] +
|
|
delta * 90 * -self.keys['arrow_right'])
|
|
self.pitch += (delta * 90 * self.keys['arrow_up'] +
|
|
delta * 90 * -self.keys['arrow_down'])
|
|
self.camera.setHpr(self.heading, self.pitch, 0)
|
|
if ENABLE_PORTALS:
|
|
self.cellmanager.update()
|
|
return task.cont
|
|
|
|
def toggle_xray_mode(self):
|
|
"""Toggle X-ray mode on and off. This is useful for seeing the
|
|
effectiveness of the portal culling."""
|
|
self.xray_mode = not self.xray_mode
|
|
if self.xray_mode:
|
|
self.level_model.setColorScale((1, 1, 1, 0.5))
|
|
self.level_model.setTransparency(TransparencyAttrib.MDual)
|
|
else:
|
|
self.level_model.setColorScaleOff()
|
|
self.level_model.setTransparency(TransparencyAttrib.MNone)
|
|
|
|
def toggle_model_bounds(self):
|
|
"""Toggle bounding volumes on and off on the models."""
|
|
self.show_model_bounds = not self.show_model_bounds
|
|
if self.show_model_bounds:
|
|
for model in self.models:
|
|
model.showBounds()
|
|
else:
|
|
for model in self.models:
|
|
model.hideBounds()
|
|
|
|
|
|
class CellManager(object):
|
|
"""Creates a collision ray and collision traverser to use for
|
|
selecting the current cell."""
|
|
def __init__(self, game):
|
|
self.game = game
|
|
self.cells = {}
|
|
self.cells_by_collider = {}
|
|
self.cell_picker_world = NodePath('cell_picker_world')
|
|
self.ray = CollisionRay()
|
|
self.ray.setDirection(LVector3.down())
|
|
cnode = CollisionNode('cell_raycast_cnode')
|
|
self.ray_nodepath = self.cell_picker_world.attachNewNode(cnode)
|
|
self.ray_nodepath.node().addSolid(self.ray)
|
|
self.ray_nodepath.node().setIntoCollideMask(0) # not for colliding into
|
|
self.ray_nodepath.node().setFromCollideMask(1)
|
|
self.traverser = CollisionTraverser('traverser')
|
|
self.last_known_cell = None
|
|
|
|
def add_cell(self, collider, name):
|
|
"""Add a new cell."""
|
|
cell = Cell(self, name, collider)
|
|
self.cells[name] = cell
|
|
self.cells_by_collider[collider.node()] = cell
|
|
|
|
def get_cell(self, pos):
|
|
"""Given a position, return the nearest cell below that position.
|
|
If no cell is found, returns None."""
|
|
self.ray.setOrigin(pos)
|
|
queue = CollisionHandlerQueue()
|
|
self.traverser.addCollider(self.ray_nodepath, queue)
|
|
self.traverser.traverse(self.cell_picker_world)
|
|
self.traverser.removeCollider(self.ray_nodepath)
|
|
queue.sortEntries()
|
|
if not queue.getNumEntries():
|
|
return None
|
|
entry = queue.getEntry(0)
|
|
cnode = entry.getIntoNode()
|
|
try:
|
|
return self.cells_by_collider[cnode]
|
|
except KeyError:
|
|
raise Warning('collision ray collided with something '
|
|
'other than a cell: %s' % cnode)
|
|
|
|
def get_dist_to_cell(self, pos):
|
|
"""Given a position, return the distance to the nearest cell
|
|
below that position. If no cell is found, returns None."""
|
|
self.ray.setOrigin(pos)
|
|
queue = CollisionHandlerQueue()
|
|
self.traverser.addCollider(self.ray_nodepath, queue)
|
|
self.traverser.traverse(self.cell_picker_world)
|
|
self.traverser.removeCollider(self.ray_nodepath)
|
|
queue.sortEntries()
|
|
if not queue.getNumEntries():
|
|
return None
|
|
entry = queue.getEntry(0)
|
|
return (entry.getSurfacePoint(self.cell_picker_world) - pos).length()
|
|
|
|
def load_cells_from_model(self, modelpath):
|
|
"""Loads cells from an EGG file. Cells must be named in the
|
|
format "cell#" to be loaded by this function."""
|
|
cell_model = self.game.loader.loadModel(modelpath)
|
|
for collider in cell_model.findAllMatches('**/+GeomNode'):
|
|
name = collider.getName()
|
|
if name.startswith('cell'):
|
|
self.add_cell(collider, name[4:])
|
|
cell_model.removeNode()
|
|
|
|
def load_portals_from_model(self, modelpath):
|
|
"""Loads portals from an EGG file. Portals must be named in the
|
|
format "portal_#to#_*" to be loaded by this function, whereby the
|
|
first # is the from cell, the second # is the into cell, and * can
|
|
be anything."""
|
|
portal_model = loader.loadModel(modelpath)
|
|
portal_nodepaths = portal_model.findAllMatches('**/+PortalNode')
|
|
for portal_nodepath in portal_nodepaths:
|
|
name = portal_nodepath.getName()
|
|
if name.startswith('portal_'):
|
|
from_cell_id, into_cell_id = name.split('_')[1].split('to')
|
|
try:
|
|
from_cell = self.cells[from_cell_id]
|
|
except KeyError:
|
|
print ('could not load portal "%s" because cell "%s"'
|
|
'does not exist' % (name, from_cell_id))
|
|
continue
|
|
try:
|
|
into_cell = self.cells[into_cell_id]
|
|
except KeyError:
|
|
print ('could not load portal "%s" because cell "%s"'
|
|
'does not exist' % (name, into_cell_id))
|
|
continue
|
|
from_cell.add_portal(portal_nodepath, into_cell)
|
|
portal_model.removeNode()
|
|
|
|
def update(self):
|
|
"""Show the cell the camera is currently in and hides the rest.
|
|
If the camera is not in a cell, use the last known cell that the
|
|
camera was in. If the camera has not yet been in a cell, then all
|
|
cells will be hidden."""
|
|
camera_pos = self.game.camera.getPos(self.game.render)
|
|
for cell in self.cells:
|
|
self.cells[cell].nodepath.hide()
|
|
current_cell = self.get_cell(camera_pos)
|
|
if current_cell is None:
|
|
if self.last_known_cell is None:
|
|
return
|
|
self.last_known_cell.nodepath.show()
|
|
else:
|
|
self.last_known_cell = current_cell
|
|
current_cell.nodepath.show()
|
|
|
|
|
|
class Cell(object):
|
|
"""The Cell class is a handy way to keep an association between
|
|
all the related nodes and information of a cell."""
|
|
def __init__(self, cellmanager, name, collider):
|
|
self.cellmanager = cellmanager
|
|
self.name = name
|
|
self.collider = collider
|
|
self.collider.reparentTo(self.cellmanager.cell_picker_world)
|
|
self.collider.setCollideMask(1)
|
|
self.collider.hide()
|
|
self.nodepath = NodePath('cell_%s_root' % name)
|
|
self.nodepath.reparentTo(self.cellmanager.game.render)
|
|
self.portals = []
|
|
|
|
def add_portal(self, portal, cell_out):
|
|
"""Add a portal from this cell going into another one."""
|
|
portal.reparentTo(self.nodepath)
|
|
portal.node().setCellIn(self.nodepath)
|
|
portal.node().setCellOut(cell_out.nodepath)
|
|
self.portals.append(portal)
|
|
|
|
game = Game()
|
|
game.run()
|