Poodletooth-iLand/panda/direct/showbase/ShowBase.py
Master Jumblespeed d882959bfa switch to remote
2015-05-18 22:11:33 -04:00

2958 lines
114 KiB
Python
Executable file

"""Undocumented Module"""
__all__ = ['ShowBase', 'WindowControls']
# This module redefines the builtin import function with one
# that prints out every import it does in a hierarchical form
# Annoying and very noisy, but sometimes useful
#import VerboseImport
from pandac.PandaModules import *
# This needs to be available early for DirectGUI imports
import __builtin__
__builtin__.config = getConfigShowbase()
from direct.directnotify.DirectNotifyGlobal import *
from MessengerGlobal import *
from BulletinBoardGlobal import *
from direct.task.TaskManagerGlobal import *
from JobManagerGlobal import *
from EventManagerGlobal import *
from PythonUtil import *
from direct.showbase import PythonUtil
#from direct.interval.IntervalManager import ivalMgr
from direct.interval import IntervalManager
from InputStateGlobal import inputState
from direct.showbase.BufferViewer import BufferViewer
from direct.task import Task
from direct.directutil import Verify
from direct.showbase import GarbageReport
import EventManager
import math,sys,os
import Loader
import time
import gc
from direct.fsm import ClassicFSM
from direct.fsm import State
from direct.showbase import ExceptionVarDump
import DirectObject
import SfxPlayer
if __debug__:
from direct.directutil import DeltaProfiler
import OnScreenDebug
import AppRunnerGlobal
__builtin__.FADE_SORT_INDEX = 1000
__builtin__.NO_FADE_SORT_INDEX = 2000
# Now ShowBase is a DirectObject. We need this so ShowBase can hang
# hooks on messages, particularly on window-event. This doesn't
# *seem* to cause anyone any problems.
class ShowBase(DirectObject.DirectObject):
notify = directNotify.newCategory("ShowBase")
def __init__(self, fStartDirect = True, windowType = None):
self.__dev__ = config.GetBool('want-dev', __debug__)
__builtin__.__dev__ = self.__dev__
logStackDump = (config.GetBool('log-stack-dump', False) or
config.GetBool('client-log-stack-dump', False))
uploadStackDump = config.GetBool('upload-stack-dump', False)
if logStackDump or uploadStackDump:
ExceptionVarDump.install(logStackDump, uploadStackDump)
# Locate the directory containing the main program
self.mainDir = ExecutionEnvironment.getEnvironmentVariable("MAIN_DIR")
# The appRunner should have been created by the time ShowBase
# has been.
self.appRunner = AppRunnerGlobal.appRunner
#debug running multiplier
self.debugRunningMultiplier = 4
# Get the dconfig object
self.config = config
# Setup wantVerifyPdb as soon as reasonable:
Verify.wantVerifyPdb = self.config.GetBool('want-verify-pdb', 0)
# [gjeon] to disable sticky keys
if self.config.GetBool('disable-sticky-keys', 0):
storeAccessibilityShortcutKeys()
allowAccessibilityShortcutKeys(False)
self.printEnvDebugInfo()
vfs = VirtualFileSystem.getGlobalPtr()
self.nextWindowIndex = 1
self.__directStarted = False
self.__deadInputs = 0
# Store dconfig variables
self.sfxActive = self.config.GetBool('audio-sfx-active', 1)
self.musicActive = self.config.GetBool('audio-music-active', 1)
self.wantFog = self.config.GetBool('want-fog', 1)
self.wantRender2dp = self.config.GetBool('want-render2dp', 1)
self.screenshotExtension = self.config.GetString('screenshot-extension', 'jpg')
self.musicManager = None
self.musicManagerIsValid = None
self.sfxManagerList = []
self.sfxManagerIsValidList = []
self.wantStats = self.config.GetBool('want-pstats', 0)
self.wantTk = False
self.wantWx = False
# Fill this in with a function to invoke when the user "exits"
# the program by closing the main window.
self.exitFunc = None
# Add final-exit callbacks to this list. These will be called
# when sys.exit() is called, after Panda has unloaded, and
# just before Python is about to shut down.
self.finalExitCallbacks = []
Task.TaskManager.taskTimerVerbose = self.config.GetBool('task-timer-verbose', 0)
Task.TaskManager.extendedExceptions = self.config.GetBool('extended-exceptions', 0)
Task.TaskManager.pStatsTasks = self.config.GetBool('pstats-tasks', 0)
# Set up the TaskManager to reset the PStats clock back
# whenever we resume from a pause. This callback function is
# a little hacky, but we can't call it directly from within
# the TaskManager because he doesn't know about PStats (and
# has to run before libpanda is even loaded).
taskMgr.resumeFunc = PStatClient.resumeAfterPause
if self.__dev__:
self.__setupProfile()
# If the aspect ratio is 0 or None, it means to infer the
# aspect ratio from the window size.
# If you need to know the actual aspect ratio call base.getAspectRatio()
self.__configAspectRatio = ConfigVariableDouble('aspect-ratio', 0).getValue()
# This variable is used to see if the aspect ratio has changed when
# we get a window-event.
self.__oldAspectRatio = None
self.windowType = windowType
if self.windowType is None:
self.windowType = self.config.GetString('window-type', 'onscreen')
self.requireWindow = self.config.GetBool('require-window', 1)
# base.win is the main, or only window; base.winList is a list of
# *all* windows. Similarly with base.camList.
self.win = None
self.frameRateMeter = None
self.sceneGraphAnalyzerMeter = None
self.winList = []
self.winControls = []
self.mainWinMinimized = 0
self.mainWinForeground = 0
self.pipe = None
self.pipeList = []
self.mouse2cam = None
self.buttonThrowers = None
self.mouseWatcher = None
self.mouseWatcherNode = None
self.pointerWatcherNodes = None
self.mouseInterface = None
self.drive = None
self.trackball = None
self.texmem = None
self.showVertices = None
self.cam = None
self.cam2d = None
self.cam2dp = None
self.camera = None
self.camera2d = None
self.camera2dp = None
self.camList = []
self.camNode = None
self.camLens = None
self.camFrustumVis = None
self.direct = None
self.wxApp = None
self.tkRoot = None
# This is used for syncing multiple PCs in a distributed cluster
try:
# Has the cluster sync variable been set externally?
self.clusterSyncFlag = clusterSyncFlag
except NameError:
# Has the clusterSyncFlag been set via a config variable
self.clusterSyncFlag = self.config.GetBool('cluster-sync', 0)
self.hidden = NodePath('hidden')
self.graphicsEngine = GraphicsEngine.getGlobalPtr()
self.setupRender()
self.setupRender2d()
self.setupDataGraph()
if self.wantRender2dp:
self.setupRender2dp()
# This is a placeholder for a CollisionTraverser. If someone
# stores a CollisionTraverser pointer here, we'll traverse it
# in the collisionLoop task.
self.shadowTrav = 0
self.cTrav = 0
self.cTravStack = Stack()
# Ditto for an AppTraverser.
self.appTrav = 0
# This is the DataGraph traverser, which we might as well
# create now.
self.dgTrav = DataGraphTraverser()
# Maybe create a RecorderController to record and/or play back
# the user session.
self.recorder = None
playbackSession = self.config.GetString('playback-session', '')
recordSession = self.config.GetString('record-session', '')
if playbackSession:
self.recorder = RecorderController()
self.recorder.beginPlayback(Filename.fromOsSpecific(playbackSession))
elif recordSession:
self.recorder = RecorderController()
self.recorder.beginRecord(Filename.fromOsSpecific(recordSession))
if self.recorder:
# If we're either playing back or recording, pass the
# random seed into the system so each session will have
# the same random seed.
import random #, whrandom
seed = self.recorder.getRandomSeed()
random.seed(seed)
#whrandom.seed(seed & 0xff, (seed >> 8) & 0xff, (seed >> 16) & 0xff)
# Now that we've set up the window structures, assign an exitfunc.
self.oldexitfunc = getattr(sys, 'exitfunc', None)
sys.exitfunc = self.exitfunc
# Open the default rendering window.
if self.windowType != 'none':
props = WindowProperties.getDefault()
if (self.config.GetBool('read-raw-mice', 0)):
props.setRawMice(1)
self.openDefaultWindow(startDirect = False, props=props)
# The default is trackball mode, which is more convenient for
# ad-hoc development in Python using ShowBase. Applications
# can explicitly call base.useDrive() if they prefer a drive
# interface.
self.mouseInterface = self.trackball
self.useTrackball()
self.loader = Loader.Loader(self)
self.graphicsEngine.setDefaultLoader(self.loader.loader)
self.eventMgr = eventMgr
self.messenger = messenger
self.bboard = bulletinBoard
self.taskMgr = taskMgr
self.jobMgr = jobMgr
# Particle manager
self.particleMgr = None
self.particleMgrEnabled = 0
# Physics manager
self.physicsMgr = None
self.physicsMgrEnabled = 0
self.physicsMgrAngular = 0
self.createBaseAudioManagers()
self.createStats()
self.AppHasAudioFocus = 1
# Get a pointer to Panda's global ClockObject, used for
# synchronizing events between Python and C.
globalClock = ClockObject.getGlobalClock()
# Since we have already started up a TaskManager, and probably
# a number of tasks; and since the TaskManager had to use the
# TrueClock to tell time until this moment, make sure the
# globalClock object is exactly in sync with the TrueClock.
trueClock = TrueClock.getGlobalPtr()
globalClock.setRealTime(trueClock.getShortTime())
globalClock.tick()
# Now we can make the TaskManager start using the new globalClock.
taskMgr.globalClock = globalClock
# client CPU affinity is determined by, in order:
# - client-cpu-affinity-mask config
# - pcalt-# (# is CPU number, 0-based)
# - client-cpu-affinity config
# - auto-single-cpu-affinity config
affinityMask = self.config.GetInt('client-cpu-affinity-mask', -1)
if affinityMask != -1:
TrueClock.getGlobalPtr().setCpuAffinity(affinityMask)
else:
# this is useful on machines that perform better with each process
# assigned to a single CPU
autoAffinity = self.config.GetBool('auto-single-cpu-affinity', 0)
affinity = None
if autoAffinity and ('clientIndex' in __builtin__.__dict__):
affinity = abs(int(__builtin__.clientIndex))
else:
affinity = self.config.GetInt('client-cpu-affinity', -1)
if (affinity in (None, -1)) and autoAffinity:
affinity = 0
if affinity not in (None, -1):
# Windows XP supports a 32-bit affinity mask
TrueClock.getGlobalPtr().setCpuAffinity(1 << (affinity % 32))
# Make sure we're not making more than one ShowBase.
if 'base' in __builtin__.__dict__:
raise StandardError, "Attempt to spawn multiple ShowBase instances!"
__builtin__.base = self
__builtin__.render2d = self.render2d
__builtin__.aspect2d = self.aspect2d
__builtin__.pixel2d = self.pixel2d
__builtin__.render = self.render
__builtin__.hidden = self.hidden
__builtin__.camera = self.camera
__builtin__.loader = self.loader
__builtin__.taskMgr = self.taskMgr
__builtin__.jobMgr = self.jobMgr
__builtin__.eventMgr = self.eventMgr
__builtin__.messenger = self.messenger
__builtin__.bboard = self.bboard
# Config needs to be defined before ShowBase is constructed
#__builtin__.config = self.config
__builtin__.run = self.run
__builtin__.ostream = Notify.out()
__builtin__.directNotify = directNotify
__builtin__.giveNotify = giveNotify
__builtin__.globalClock = globalClock
__builtin__.vfs = vfs
__builtin__.cpMgr = ConfigPageManager.getGlobalPtr()
__builtin__.cvMgr = ConfigVariableManager.getGlobalPtr()
__builtin__.pandaSystem = PandaSystem.getGlobalPtr()
__builtin__.wantUberdog = base.config.GetBool('want-uberdog', 1)
if __debug__:
__builtin__.deltaProfiler = DeltaProfiler.DeltaProfiler("ShowBase")
__builtin__.onScreenDebug = OnScreenDebug.OnScreenDebug()
if self.wantRender2dp:
__builtin__.render2dp = self.render2dp
__builtin__.aspect2dp = self.aspect2dp
__builtin__.pixel2dp = self.pixel2dp
if __dev__:
ShowBase.notify.debug('__dev__ == %s' % __dev__)
else:
ShowBase.notify.info('__dev__ == %s' % __dev__)
# set up recording of Functor creation stacks in __dev__
PythonUtil.recordFunctorCreationStacks()
if __dev__ or self.config.GetBool('want-e3-hacks', False):
if self.config.GetBool('track-gui-items', True):
# dict of guiId to gui item, for tracking down leaks
self.guiItems = {}
# optionally restore the default gui sounds from 1.7.2 and earlier
if ConfigVariableBool('orig-gui-sounds', False).getValue():
from direct.gui import DirectGuiGlobals as DGG
DGG.setDefaultClickSound(self.loader.loadSfx("audio/sfx/GUI_click.wav"))
DGG.setDefaultRolloverSound(self.loader.loadSfx("audio/sfx/GUI_rollover.wav"))
# Now hang a hook on the window-event from Panda. This allows
# us to detect when the user resizes, minimizes, or closes the
# main window.
self.accept('window-event', self.windowEvent)
# Transition effects (fade, iris, etc)
import Transitions
self.transitions = Transitions.Transitions(self.loader)
if self.win:
# Setup the window controls - handy for multiwindow applications
self.setupWindowControls()
# Client sleep
sleepTime = self.config.GetFloat('client-sleep', 0.0)
self.clientSleep = 0.0
self.setSleep(sleepTime)
# Extra sleep for running 4+ clients on a single machine
# adds a sleep right after the main render in igloop
# tends to even out the frame rate and keeps it from going
# to zero in the out of focus windows
if base.config.GetBool('multi-sleep', 0):
self.multiClientSleep = 1
else:
self.multiClientSleep = 0
# Offscreen buffer viewing utility.
# This needs to be allocated even if the viewer is off.
self.bufferViewer = BufferViewer()
if self.wantRender2dp:
self.bufferViewer.setRenderParent(self.render2dp)
if self.windowType != 'none':
if fStartDirect: # [gjeon] if this is False let them start direct manually
self.__doStartDirect()
if self.config.GetBool('show-tex-mem', False):
if not self.texmem or self.texmem.cleanedUp:
self.toggleTexMem()
taskMgr.finalInit()
# Start IGLOOP
self.restart()
# add a collision traverser via pushCTrav and remove it via popCTrav
# that way the owner of the new cTrav doesn't need to hold onto the
# previous one in order to put it back
def pushCTrav(self, cTrav):
self.cTravStack.push(self.cTrav)
self.cTrav = cTrav
def popCTrav(self):
self.cTrav = self.cTravStack.pop()
def __setupProfile(self):
""" Sets up the Python profiler, if avaialable, according to
some Panda config settings. """
try:
import profile, pstats
except ImportError:
return
profile.Profile.bias = float(self.config.GetString("profile-bias","0"))
def f8(x):
return ("%"+"8.%df"%base.config.GetInt("profile-decimals",3)) % x
pstats.f8=f8
# temp; see ToonBase.py
def getExitErrorCode(self):
return 0
def printEnvDebugInfo(self):
"""
Print some information about the environment that we are running
in. Stuff like the model paths and other paths. Feel free to
add stuff to this.
"""
if self.config.GetBool('want-env-debug-info', 0):
print "\n\nEnvironment Debug Info {"
print "* model path:"
print getModelPath()
#print "* dna path:"
#print getDnaPath()
print "}"
def destroy(self):
""" Call this function to destroy the ShowBase and stop all
its tasks, freeing all of the Panda resources. Normally, you
should not need to call it explicitly, as it is bound to the
exitfunc and will be called at application exit time
automatically.
This function is designed to be safe to call multiple times."""
for cb in self.finalExitCallbacks[:]:
cb()
# [gjeon] restore sticky key settings
if self.config.GetBool('disable-sticky-keys', 0):
allowAccessibilityShortcutKeys(True)
taskMgr.destroy()
if getattr(self, 'musicManager', None):
self.musicManager.shutdown()
self.musicManager = None
for sfxManager in self.sfxManagerList:
sfxManager.shutdown()
self.sfxManagerList = []
if getattr(self, 'loader', None):
self.loader.destroy()
self.loader = None
if getattr(self, 'graphicsEngine', None):
self.graphicsEngine.removeAllWindows()
try:
self.direct.panel.destroy()
except:
pass
if hasattr(self, 'win'):
del self.win
del self.winList
del self.pipe
vfs = VirtualFileSystem.getGlobalPtr()
vfs.unmountAll()
def exitfunc(self):
"""
This should be assigned to sys.exitfunc to be called just
before Python shutdown. It guarantees that the Panda window
is closed cleanly, so that we free system resources, restore
the desktop and keyboard functionality, etc.
"""
self.destroy()
if self.oldexitfunc:
self.oldexitfunc()
def makeDefaultPipe(self, printPipeTypes = True):
"""
Creates the default GraphicsPipe, which will be used to make
windows unless otherwise specified.
"""
assert self.pipe == None
selection = GraphicsPipeSelection.getGlobalPtr()
if printPipeTypes:
selection.printPipeTypes()
self.pipe = selection.makeDefaultPipe()
if not self.pipe:
self.notify.error(
"No graphics pipe is available!\n"
"Your Config.prc file must name at least one valid panda display\n"
"library via load-display or aux-display.")
self.notify.info("Default graphics pipe is %s (%s)." % (
self.pipe.getType().getName(), self.pipe.getInterfaceName()))
self.pipeList.append(self.pipe)
def makeModulePipe(self, moduleName):
"""
Returns a GraphicsPipe from the indicated module,
e.g. 'pandagl' or 'pandadx9'. Does not affect base.pipe or
base.pipeList.
"""
selection = GraphicsPipeSelection.getGlobalPtr()
return selection.makeModulePipe(moduleName)
def makeAllPipes(self):
"""
Creates all GraphicsPipes that the system knows about and fill up
self.pipeList with them.
"""
shouldPrintPipes = 0
selection = GraphicsPipeSelection.getGlobalPtr()
selection.loadAuxModules()
# First, we should make sure the default pipe exists.
if self.pipe == None:
self.makeDefaultPipe()
# Now go through the list of known pipes, and make each one if
# we don't have one already.
numPipeTypes = selection.getNumPipeTypes()
for i in range(numPipeTypes):
pipeType = selection.getPipeType(i)
# Do we already have a pipe of this type on the list?
# This operation is n-squared, but presumably there won't
# be more than a handful of pipe types, so who cares.
already = 0
for pipe in self.pipeList:
if pipe.getType() == pipeType:
already = 1
if not already:
pipe = selection.makePipe(pipeType)
if pipe:
self.notify.info("Got aux graphics pipe %s (%s)." % (
pipe.getType().getName(), pipe.getInterfaceName()))
self.pipeList.append(pipe)
else:
self.notify.info("Could not make graphics pipe %s." % (
pipeType.getName()))
def openWindow(self, props = None, fbprops = None, pipe = None, gsg = None,
host = None, type = None, name = None, size = None,
aspectRatio = None, makeCamera = True, keepCamera = False,
scene = None, stereo = None, unexposedDraw = None,
callbackWindowDict = None, requireWindow = None):
"""
Creates a window and adds it to the list of windows that are
to be updated every frame.
props is the WindowProperties that describes the window.
type is either 'onscreen', 'offscreen', or 'none'.
If keepCamera is true, the existing base.cam is set up to
render into the new window.
If keepCamera is false but makeCamera is true, a new camera is
set up to render into the new window.
If unexposedDraw is not None, it specifies the initial value
of GraphicsWindow.setUnexposedDraw().
If callbackWindowDict is not None, a CallbackGraphicWindow is
created instead, which allows the caller to create the actual
window with its own OpenGL context, and direct Panda's
rendering into that window.
If requireWindow is true, it means that the function should
raise an exception if the window fails to open correctly.
"""
# Save this lambda here for convenience; we'll use it to call
# down to the underlying _doOpenWindow() with all of the above
# parameters.
func = lambda : self._doOpenWindow(
props = props, fbprops = fbprops, pipe = pipe, gsg = gsg,
host = host, type = type, name = name, size = size,
aspectRatio = aspectRatio, makeCamera = makeCamera,
keepCamera = keepCamera, scene = scene, stereo = stereo,
unexposedDraw = unexposedDraw,
callbackWindowDict = callbackWindowDict)
if self.win:
# If we've already opened a window before, this is just a
# pass-through to _doOpenWindow().
win = func()
self.graphicsEngine.openWindows()
return win
if type is None:
type = self.windowType
if requireWindow is None:
requireWindow = self.requireWindow
win = func()
# Give the window a chance to truly open.
self.graphicsEngine.openWindows()
if win != None and not win.isValid():
self.notify.info("Window did not open, removing.")
self.closeWindow(win)
win = None
if win == None and pipe == None:
# Try a little harder if the window wouldn't open.
self.makeAllPipes()
try:
self.pipeList.remove(self.pipe)
except ValueError:
pass
while self.win == None and self.pipeList:
self.pipe = self.pipeList[0]
self.notify.info("Trying pipe type %s (%s)" % (
self.pipe.getType(), self.pipe.getInterfaceName()))
win = func()
self.graphicsEngine.openWindows()
if win != None and not win.isValid():
self.notify.info("Window did not open, removing.")
self.closeWindow(win)
win = None
if win == None:
self.pipeList.remove(self.pipe)
if win == None:
self.notify.warning("Unable to open '%s' window." % (type))
if requireWindow:
# Unless require-window is set to false, it is an
# error not to open a window.
raise StandardError, 'Could not open window.'
else:
self.notify.info("Successfully opened window of type %s (%s)" % (
win.getType(), win.getPipe().getInterfaceName()))
return win
def _doOpenWindow(self, props = None, fbprops = None, pipe = None,
gsg = None, host = None, type = None, name = None,
size = None, aspectRatio = None,
makeCamera = True, keepCamera = False,
scene = None, stereo = None, unexposedDraw = None,
callbackWindowDict = None):
if pipe == None:
pipe = self.pipe
if pipe == None:
self.makeDefaultPipe()
pipe = self.pipe
if pipe == None:
# We couldn't get a pipe.
return None
if isinstance(gsg, GraphicsOutput):
# If the gsg is a window or buffer, it means to use the
# GSG from that buffer.
host = gsg
gsg = gsg.getGsg()
# If we are using DirectX, force a new GSG to be created,
# since at the moment DirectX seems to misbehave if we do
# not do this. This will cause a delay while all textures
# etc. are reloaded, so we should revisit this later if we
# can fix the underlying bug in our DirectX support.
if pipe.getType().getName().startswith('wdx'):
gsg = None
if type == None:
type = self.windowType
if props == None:
props = WindowProperties.getDefault()
if fbprops == None:
fbprops = FrameBufferProperties.getDefault()
if size != None:
# If we were given an explicit size, use it; otherwise,
# the size from the properties is used.
props = WindowProperties(props)
props.setSize(size[0], size[1])
if name == None:
name = 'window%s' % (self.nextWindowIndex)
self.nextWindowIndex += 1
win = None
flags = GraphicsPipe.BFFbPropsOptional
if type == 'onscreen':
flags = flags | GraphicsPipe.BFRequireWindow
elif type == 'offscreen':
flags = flags | GraphicsPipe.BFRefuseWindow
if callbackWindowDict:
flags = flags | GraphicsPipe.BFRequireCallbackWindow
if host:
assert host.isValid()
win = self.graphicsEngine.makeOutput(pipe, name, 0, fbprops,
props, flags, host.getGsg(), host)
elif gsg:
win = self.graphicsEngine.makeOutput(pipe, name, 0, fbprops,
props, flags, gsg)
else:
win = self.graphicsEngine.makeOutput(pipe, name, 0, fbprops,
props, flags)
if win == None:
# Couldn't create a window!
return None
if unexposedDraw is not None and hasattr(win, 'setUnexposedDraw'):
win.setUnexposedDraw(unexposedDraw)
if callbackWindowDict:
# If we asked for (and received) a CallbackGraphicsWindow,
# we now have to assign the callbacks, before we start
# trying to do anything with the window.
for callbackName in ['Events', 'Properties', 'Render']:
func = callbackWindowDict.get(callbackName, None)
if not func:
continue
setCallbackName = 'set%sCallback' % (callbackName)
setCallback = getattr(win, setCallbackName)
setCallback(PythonCallbackObject(func))
# We also need to set up the mouse/keyboard objects.
for inputName in callbackWindowDict.get('inputDevices', ['mouse']):
win.createInputDevice(inputName)
if hasattr(win, "requestProperties"):
win.requestProperties(props)
mainWindow = False
if self.win == None:
mainWindow = True
self.win = win
self.winList.append(win)
# Set up a 3-d camera for the window by default.
if keepCamera:
self.makeCamera(win, scene = scene, aspectRatio = aspectRatio,
stereo = stereo, useCamera = base.cam)
elif makeCamera:
self.makeCamera(win, scene = scene, aspectRatio = aspectRatio,
stereo = stereo)
messenger.send('open_window', [win, mainWindow])
if mainWindow:
messenger.send('open_main_window')
return win
def closeWindow(self, win, keepCamera = False, removeWindow = True):
"""
Closes the indicated window and removes it from the list of
windows. If it is the main window, clears the main window
pointer to None.
"""
win.setActive(False)
# First, remove all of the cameras associated with display
# regions on the window.
numRegions = win.getNumDisplayRegions()
for i in range(numRegions):
dr = win.getDisplayRegion(i)
# [gjeon] remove drc in base.direct.drList
if base.direct is not None:
for drc in base.direct.drList:
if drc.cam == dr.getCamera():
base.direct.drList.displayRegionList.remove(drc)
break
cam = NodePath(dr.getCamera())
dr.setCamera(NodePath())
if not cam.isEmpty() and \
cam.node().getNumDisplayRegions() == 0 and \
not keepCamera:
# If the camera is used by no other DisplayRegions,
# remove it.
if self.camList.count(cam) != 0:
self.camList.remove(cam)
# Don't throw away self.camera; we want to
# preserve it for reopening the window.
if cam == self.cam:
self.cam = None
if cam == self.cam2d:
self.cam2d = None
if cam == self.cam2dp:
self.cam2dp = None
cam.removeNode()
# [gjeon] remove winControl
for winCtrl in self.winControls:
if winCtrl.win == win:
self.winControls.remove(winCtrl)
break
# Now we can actually close the window.
if removeWindow:
self.graphicsEngine.removeWindow(win)
self.winList.remove(win)
mainWindow = False
if win == self.win:
mainWindow = True
self.win = None
if self.frameRateMeter:
self.frameRateMeter.clearWindow()
self.frameRateMeter = None
if self.sceneGraphAnalyzerMeter:
self.sceneGraphAnalyzerMeter.clearWindow()
self.sceneGraphAnalyzerMeter = None
messenger.send('close_window', [win, mainWindow])
if mainWindow:
messenger.send('close_main_window')
if not self.winList:
# Give the window(s) a chance to actually close before we
# continue.
base.graphicsEngine.renderFrame()
def openDefaultWindow(self, *args, **kw):
# Creates the main window for the first time, without being
# too particular about the kind of graphics API that is
# chosen. The suggested window type from the load-display
# config variable is tried first; if that fails, the first
# window type that can be successfully opened at all is
# accepted. Returns true on success, false otherwise.
#
# This is intended to be called only once, at application
# startup. It is normally called automatically unless
# window-type is configured to 'none'.
startDirect = kw.get('startDirect', True)
if 'startDirect' in kw:
del kw['startDirect']
self.openMainWindow(*args, **kw)
if startDirect:
self.__doStartDirect()
return self.win != None
def openMainWindow(self, *args, **kw):
"""
Creates the initial, main window for the application, and sets
up the mouse and render2d structures appropriately for it. If
this method is called a second time, it will close the
previous main window and open a new one, preserving the lens
properties in base.camLens.
The return value is true on success, or false on failure (in
which case base.win may be either None, or the previous,
closed window).
"""
keepCamera = kw.get('keepCamera', False)
success = 1
oldWin = self.win
oldLens = self.camLens
oldClearColorActive = None
if self.win != None:
# Close the previous window.
oldClearColorActive = self.win.getClearColorActive()
oldClearColor = VBase4(self.win.getClearColor())
oldClearDepthActive = self.win.getClearDepthActive()
oldClearDepth = self.win.getClearDepth()
oldClearStencilActive = self.win.getClearStencilActive()
oldClearStencil = self.win.getClearStencil()
self.closeWindow(self.win, keepCamera = keepCamera)
# Open a new window.
self.openWindow(*args, **kw)
if self.win == None:
self.win = oldWin
self.winList.append(oldWin)
success = 0
if self.win != None:
if isinstance(self.win, GraphicsWindow):
self.setupMouse(self.win)
self.makeCamera2d(self.win)
self.makeCamera2dp(self.win)
if oldLens != None:
# Restore the previous lens properties.
self.camNode.setLens(oldLens)
self.camLens = oldLens
if oldClearColorActive != None:
# Restore the previous clear properties.
self.win.setClearColorActive(oldClearColorActive)
self.win.setClearColor(oldClearColor)
self.win.setClearDepthActive(oldClearDepthActive)
self.win.setClearDepth(oldClearDepth)
self.win.setClearStencilActive(oldClearStencilActive)
self.win.setClearStencil(oldClearStencil)
flag = self.config.GetBool('show-frame-rate-meter', False)
if self.appRunner is not None and self.appRunner.allowPythonDev:
# In an allow_python_dev p3d application, we always
# start up with the frame rate meter enabled, to
# provide a visual reminder that this flag has been
# set.
flag = True
self.setFrameRateMeter(flag)
flag = self.config.GetBool('show-scene-graph-analyzer-meter', False)
self.setSceneGraphAnalyzerMeter(flag)
return success
def setSleep(self, amount):
"""
Sets up a task that calls python 'sleep' every frame. This is a simple
way to reduce the CPU usage (and frame rate) of a panda program.
"""
if (self.clientSleep == amount):
return
self.clientSleep = amount
if (amount == 0.0):
self.taskMgr.remove('clientSleep')
else:
# Spawn it after igloop (at the end of each frame)
self.taskMgr.remove('clientSleep')
self.taskMgr.add(self.sleepCycleTask, 'clientSleep', sort = 55)
def sleepCycleTask(self, task):
Thread.sleep(self.clientSleep)
#time.sleep(self.clientSleep)
return Task.cont
def setFrameRateMeter(self, flag):
"""
Turns on or off (according to flag) a standard frame rate
meter in the upper-right corner of the main window.
"""
if flag:
if not self.frameRateMeter:
self.frameRateMeter = FrameRateMeter('frameRateMeter')
self.frameRateMeter.setupWindow(self.win)
else:
if self.frameRateMeter:
self.frameRateMeter.clearWindow()
self.frameRateMeter = None
def setSceneGraphAnalyzerMeter(self, flag):
"""
Turns on or off (according to flag) a standard frame rate
meter in the upper-right corner of the main window.
"""
if flag:
if not self.sceneGraphAnalyzerMeter:
self.sceneGraphAnalyzerMeter = SceneGraphAnalyzerMeter('sceneGraphAnalyzerMeter', self.render.node())
self.sceneGraphAnalyzerMeter.setupWindow(self.win)
else:
if self.sceneGraphAnalyzerMeter:
self.sceneGraphAnalyzerMeter.clearWindow()
self.sceneGraphAnalyzerMeter = None
# [gjeon] now you can add more winControls after creating a showbase instance
def setupWindowControls(self, winCtrl=None):
if winCtrl is None:
winCtrl = WindowControls(
self.win, mouseWatcher=self.mouseWatcher,
cam=self.camera, camNode = self.camNode, cam2d=self.camera2d,
mouseKeyboard = self.dataRoot.find("**/*"))
self.winControls.append(winCtrl)
def setupRender(self):
"""
Creates the render scene graph, the primary scene graph for
rendering 3-d geometry.
"""
self.render = NodePath('render')
self.render.setAttrib(RescaleNormalAttrib.makeDefault())
self.render.setTwoSided(0)
self.backfaceCullingEnabled = 1
self.textureEnabled = 1
self.wireframeEnabled = 0
def setupRender2d(self):
"""
Creates the render2d scene graph, the primary scene graph for
2-d objects and gui elements that are superimposed over the
3-d geometry in the window.
"""
self.render2d = NodePath('render2d')
# Set up some overrides to turn off certain properties which
# we probably won't need for 2-d objects.
# It's probably important to turn off the depth test, since
# many 2-d objects will be drawn over each other without
# regard to depth position.
# We used to avoid clearing the depth buffer before drawing
# render2d, but nowadays we clear it anyway, since we
# occasionally want to put 3-d geometry under render2d, and
# it's simplest (and seems to be easier on graphics drivers)
# if the 2-d scene has been cleared first.
self.render2d.setDepthTest(0)
self.render2d.setDepthWrite(0)
self.render2d.setMaterialOff(1)
self.render2d.setTwoSided(1)
# The normal 2-d DisplayRegion has an aspect ratio that
# matches the window, but its coordinate system is square.
# This means anything we parent to render2d gets stretched.
# For things where that makes a difference, we set up
# aspect2d, which scales things back to the right aspect
# ratio.
aspectRatio = self.getAspectRatio()
self.aspect2d = self.render2d.attachNewNode(PGTop("aspect2d"))
self.aspect2d.setScale(1.0 / aspectRatio, 1.0, 1.0)
self.a2dBackground = self.aspect2d.attachNewNode("a2dBackground")
# It's important to know the bounds of the aspect2d screen.
self.a2dTop = 1.0
self.a2dBottom = -1.0
self.a2dLeft = -aspectRatio
self.a2dRight = aspectRatio
self.a2dTopCenter = self.aspect2d.attachNewNode("a2dTopCenter")
self.a2dTopCenterNs = self.aspect2d.attachNewNode("a2dTopCenterNS")
self.a2dBottomCenter = self.aspect2d.attachNewNode("a2dBottomCenter")
self.a2dBottomCenterNs = self.aspect2d.attachNewNode("a2dBottomCenterNS")
self.a2dLeftCenter = self.aspect2d.attachNewNode("a2dLeftCenter")
self.a2dLeftCenterNs = self.aspect2d.attachNewNode("a2dLeftCenterNS")
self.a2dRightCenter = self.aspect2d.attachNewNode("a2dRightCenter")
self.a2dRightCenterNs = self.aspect2d.attachNewNode("a2dRightCenterNS")
self.a2dTopLeft = self.aspect2d.attachNewNode("a2dTopLeft")
self.a2dTopLeftNs = self.aspect2d.attachNewNode("a2dTopLeftNS")
self.a2dTopRight = self.aspect2d.attachNewNode("a2dTopRight")
self.a2dTopRightNs = self.aspect2d.attachNewNode("a2dTopRightNS")
self.a2dBottomLeft = self.aspect2d.attachNewNode("a2dBottomLeft")
self.a2dBottomLeftNs = self.aspect2d.attachNewNode("a2dBottomLeftNS")
self.a2dBottomRight = self.aspect2d.attachNewNode("a2dBottomRight")
self.a2dBottomRightNs = self.aspect2d.attachNewNode("a2dBottomRightNS")
# Put the nodes in their places
self.a2dTopCenter.setPos(0, 0, self.a2dTop)
self.a2dTopCenterNs.setPos(0, 0, self.a2dTop)
self.a2dBottomCenter.setPos(0, 0, self.a2dBottom)
self.a2dBottomCenterNs.setPos(0, 0, self.a2dBottom)
self.a2dLeftCenter.setPos(self.a2dLeft, 0, 0)
self.a2dLeftCenterNs.setPos(self.a2dLeft, 0, 0)
self.a2dRightCenter.setPos(self.a2dRight, 0, 0)
self.a2dRightCenterNs.setPos(self.a2dRight, 0, 0)
self.a2dTopLeft.setPos(self.a2dLeft, 0, self.a2dTop)
self.a2dTopLeftNs.setPos(self.a2dLeft, 0, self.a2dTop)
self.a2dTopRight.setPos(self.a2dRight, 0, self.a2dTop)
self.a2dTopRightNs.setPos(self.a2dRight, 0, self.a2dTop)
self.a2dBottomLeft.setPos(self.a2dLeft, 0, self.a2dBottom)
self.a2dBottomLeftNs.setPos(self.a2dLeft, 0, self.a2dBottom)
self.a2dBottomRight.setPos(self.a2dRight, 0, self.a2dBottom)
self.a2dBottomRightNs.setPos(self.a2dRight, 0, self.a2dBottom)
# This special root, pixel2d, uses units in pixels that are relative
# to the window. The upperleft corner of the window is (0, 0),
# the lowerleft corner is (xsize, -ysize), in this coordinate system.
xsize, ysize = self.getSize()
self.pixel2d = self.render2d.attachNewNode(PGTop("pixel2d"))
self.pixel2d.setPos(-1, 0, 1)
if xsize > 0 and ysize > 0:
self.pixel2d.setScale(2.0 / xsize, 1.0, 2.0 / ysize)
def setupRender2dp(self):
"""
Creates a render2d scene graph, the secondary scene graph for
2-d objects and gui elements that are superimposed over the
2-d and 3-d geometry in the window.
"""
self.render2dp = NodePath('render2dp')
# Set up some overrides to turn off certain properties which
# we probably won't need for 2-d objects.
# It's probably important to turn off the depth test, since
# many 2-d objects will be drawn over each other without
# regard to depth position.
dt = DepthTestAttrib.make(DepthTestAttrib.MNone)
dw = DepthWriteAttrib.make(DepthWriteAttrib.MOff)
self.render2dp.setDepthTest(0)
self.render2dp.setDepthWrite(0)
self.render2dp.setMaterialOff(1)
self.render2dp.setTwoSided(1)
# The normal 2-d DisplayRegion has an aspect ratio that
# matches the window, but its coordinate system is square.
# This means anything we parent to render2d gets stretched.
# For things where that makes a difference, we set up
# aspect2d, which scales things back to the right aspect
# ratio.
aspectRatio = self.getAspectRatio()
self.aspect2dp = self.render2dp.attachNewNode(PGTop("aspect2dp"))
self.aspect2dp.node().setStartSort(16384)
self.aspect2dp.setScale(1.0 / aspectRatio, 1.0, 1.0)
# It's important to know the bounds of the aspect2d screen.
self.a2dpTop = 1.0
self.a2dpBottom = -1.0
self.a2dpLeft = -aspectRatio
self.a2dpRight = aspectRatio
self.a2dpTopCenter = self.aspect2dp.attachNewNode("a2dpTopCenter")
self.a2dpBottomCenter = self.aspect2dp.attachNewNode("a2dpBottomCenter")
self.a2dpLeftCenter = self.aspect2dp.attachNewNode("a2dpLeftCenter")
self.a2dpRightCenter = self.aspect2dp.attachNewNode("a2dpRightCenter")
self.a2dpTopLeft = self.aspect2dp.attachNewNode("a2dpTopLeft")
self.a2dpTopRight = self.aspect2dp.attachNewNode("a2dpTopRight")
self.a2dpBottomLeft = self.aspect2dp.attachNewNode("a2dpBottomLeft")
self.a2dpBottomRight = self.aspect2dp.attachNewNode("a2dpBottomRight")
# Put the nodes in their places
self.a2dpTopCenter.setPos(0, 0, self.a2dpTop)
self.a2dpBottomCenter.setPos(0, 0, self.a2dpBottom)
self.a2dpLeftCenter.setPos(self.a2dpLeft, 0, 0)
self.a2dpRightCenter.setPos(self.a2dpRight, 0, 0)
self.a2dpTopLeft.setPos(self.a2dpLeft, 0, self.a2dpTop)
self.a2dpTopRight.setPos(self.a2dpRight, 0, self.a2dpTop)
self.a2dpBottomLeft.setPos(self.a2dpLeft, 0, self.a2dpBottom)
self.a2dpBottomRight.setPos(self.a2dpRight, 0, self.a2dpBottom)
# This special root, pixel2d, uses units in pixels that are relative
# to the window. The upperleft corner of the window is (0, 0),
# the lowerleft corner is (xsize, -ysize), in this coordinate system.
xsize, ysize = self.getSize()
self.pixel2dp = self.render2dp.attachNewNode(PGTop("pixel2dp"))
self.pixel2dp.node().setStartSort(16384)
self.pixel2dp.setPos(-1, 0, 1)
if xsize > 0 and ysize > 0:
self.pixel2dp.setScale(2.0 / xsize, 1.0, 2.0 / ysize)
def setAspectRatio(self, aspectRatio):
""" Sets the global aspect ratio of the main window. Set it
to None to restore automatic scaling. """
self.__configAspectRatio = aspectRatio
self.adjustWindowAspectRatio(self.getAspectRatio())
def getAspectRatio(self, win = None):
# Returns the actual aspect ratio of the indicated (or main
# window), or the default aspect ratio if there is not yet a
# main window.
# If the config it set, we return that
if self.__configAspectRatio:
return self.__configAspectRatio
aspectRatio = 1
if win == None:
win = self.win
if win != None and win.hasSize() and win.getSbsLeftYSize() != 0:
aspectRatio = float(win.getSbsLeftXSize()) / float(win.getSbsLeftYSize())
else:
if win == None or not hasattr(win, "getRequestedProperties"):
props = WindowProperties.getDefault()
else:
props = win.getRequestedProperties()
if not props.hasSize():
props = WindowProperties.getDefault()
if props.hasSize() and props.getYSize() != 0:
aspectRatio = float(props.getXSize()) / float(props.getYSize())
if aspectRatio == 0:
return 1
return aspectRatio
def getSize(self, win = None):
# Returns the actual size of the indicated (or main
# window), or the default size if there is not yet a
# main window.
if win == None:
win = self.win
if win != None and win.hasSize():
return win.getXSize(), win.getYSize()
else:
if win == None or not hasattr(win, "getRequestedProperties"):
props = WindowProperties.getDefault()
else:
props = win.getRequestedProperties()
if not props.hasSize():
props = WindowProperties.getDefault()
if props.hasSize():
return props.getXSize(), props.getYSize()
def makeCamera(self, win, sort = 0, scene = None,
displayRegion = (0, 1, 0, 1), stereo = None,
aspectRatio = None, clearDepth = 0, clearColor = None,
lens = None, camName = 'cam', mask = None,
useCamera = None):
"""
Makes a new 3-d camera associated with the indicated window,
and creates a display region in the indicated subrectangle.
If stereo is True, then a stereo camera is created, with a
pair of DisplayRegions. If stereo is False, then a standard
camera is created. If stereo is None or omitted, a stereo
camera is created if the window says it can render in stereo.
If useCamera is not None, it is a NodePath to be used as the
camera to apply to the window, rather than creating a new
camera.
"""
# self.camera is the parent node of all cameras: a node that
# we can move around to move all cameras as a group.
if self.camera == None:
# We make it a ModelNode with the PTLocal flag, so that
# a wayward flatten operations won't attempt to mangle the
# camera.
self.camera = self.render.attachNewNode(ModelNode('camera'))
self.camera.node().setPreserveTransform(ModelNode.PTLocal)
__builtin__.camera = self.camera
self.mouse2cam.node().setNode(self.camera.node())
if useCamera:
# Use the existing camera node.
cam = useCamera
camNode = useCamera.node()
assert(isinstance(camNode, Camera))
lens = camNode.getLens()
cam.reparentTo(self.camera)
else:
# Make a new Camera node.
camNode = Camera(camName)
if lens == None:
lens = PerspectiveLens()
if aspectRatio == None:
aspectRatio = self.getAspectRatio(win)
lens.setAspectRatio(aspectRatio)
cam = self.camera.attachNewNode(camNode)
if lens != None:
camNode.setLens(lens)
if scene != None:
camNode.setScene(scene)
if mask != None:
if (isinstance(mask, int)):
mask = BitMask32(mask)
camNode.setCameraMask(mask)
if self.cam == None:
self.cam = cam
self.camNode = camNode
self.camLens = lens
self.camList.append(cam)
# Now, make a DisplayRegion for the camera.
if stereo is not None:
if stereo:
dr = win.makeStereoDisplayRegion(*displayRegion)
else:
dr = win.makeMonoDisplayRegion(*displayRegion)
else:
dr = win.makeDisplayRegion(*displayRegion)
dr.setSort(sort)
# By default, we do not clear 3-d display regions (the entire
# window will be cleared, which is normally sufficient). But
# we will if clearDepth is specified.
if clearDepth:
dr.setClearDepthActive(1)
if clearColor:
dr.setClearColorActive(1)
dr.setClearColor(clearColor)
dr.setCamera(cam)
return cam
def makeCamera2d(self, win, sort = 10,
displayRegion = (0, 1, 0, 1), coords = (-1, 1, -1, 1),
lens = None, cameraName = None):
"""
Makes a new camera2d associated with the indicated window, and
assigns it to render the indicated subrectangle of render2d.
"""
dr = win.makeMonoDisplayRegion(*displayRegion)
dr.setSort(sort)
# Enable clearing of the depth buffer on this new display
# region (see the comment in setupRender2d, above).
dr.setClearDepthActive(1)
# Make any texture reloads on the gui come up immediately.
dr.setIncompleteRender(False)
left, right, bottom, top = coords
# Now make a new Camera node.
if (cameraName):
cam2dNode = Camera('cam2d_' + cameraName)
else:
cam2dNode = Camera('cam2d')
if lens == None:
lens = OrthographicLens()
lens.setFilmSize(right - left, top - bottom)
lens.setFilmOffset((right + left) * 0.5, (top + bottom) * 0.5)
lens.setNearFar(-1000, 1000)
cam2dNode.setLens(lens)
# self.camera2d is the analog of self.camera, although it's
# not as clear how useful it is.
if self.camera2d == None:
self.camera2d = self.render2d.attachNewNode('camera2d')
camera2d = self.camera2d.attachNewNode(cam2dNode)
dr.setCamera(camera2d)
if self.cam2d == None:
self.cam2d = camera2d
return camera2d
def makeCamera2dp(self, win, sort = 20,
displayRegion = (0, 1, 0, 1), coords = (-1, 1, -1, 1),
lens = None, cameraName = None):
"""
Makes a new camera2dp associated with the indicated window, and
assigns it to render the indicated subrectangle of render2dp.
"""
dr = win.makeMonoDisplayRegion(*displayRegion)
dr.setSort(sort)
# Unlike render2d, we don't clear the depth buffer for
# render2dp. Caveat emptor.
if hasattr(dr, 'setIncompleteRender'):
dr.setIncompleteRender(False)
left, right, bottom, top = coords
# Now make a new Camera node.
if (cameraName):
cam2dNode = Camera('cam2dp_' + cameraName)
else:
cam2dNode = Camera('cam2dp')
if lens == None:
lens = OrthographicLens()
lens.setFilmSize(right - left, top - bottom)
lens.setFilmOffset((right + left) * 0.5, (top + bottom) * 0.5)
lens.setNearFar(-1000, 1000)
cam2dNode.setLens(lens)
# self.camera2d is the analog of self.camera, although it's
# not as clear how useful it is.
if self.camera2dp == None:
self.camera2dp = self.render2dp.attachNewNode('camera2dp')
camera2dp = self.camera2dp.attachNewNode(cam2dNode)
dr.setCamera(camera2dp)
if self.cam2dp == None:
self.cam2dp = camera2dp
return camera2dp
def setupDataGraph(self):
"""
Creates the data graph and populates it with the basic input
devices.
"""
self.dataRoot = NodePath('dataRoot')
# Cache the node so we do not ask for it every frame
self.dataRootNode = self.dataRoot.node()
# Now we have the main trackball & drive interfaces.
# useTrackball() and useDrive() switch these in and out; only
# one is in use at a given time.
self.trackball = NodePath(Trackball('trackball'))
self.drive = NodePath(DriveInterface('drive'))
self.mouse2cam = NodePath(Transform2SG('mouse2cam'))
# [gjeon] now you can create multiple mouse watchers to support multiple windows
def setupMouse(self, win, fMultiWin=False):
"""
Creates the structures necessary to monitor the mouse input,
using the indicated window. If the mouse has already been set
up for a different window, those structures are deleted first.
The return value is the ButtonThrower NodePath created for
this window.
If fMultiWin is true, then the previous mouse structures are
not deleted; instead, multiple windows are allowed to monitor
the mouse input. However, in this case, the trackball
controls are not set up, and must be set up by hand if
desired.
"""
if not fMultiWin and self.buttonThrowers != None:
for bt in self.buttonThrowers:
mw = bt.getParent()
mk = mw.getParent()
bt.removeNode()
mw.removeNode()
mk.removeNode()
bts, pws = self.setupMouseCB(win)
if fMultiWin:
return bts[0]
self.buttonThrowers = bts[:]
self.pointerWatcherNodes = pws[:]
self.mouseWatcher = self.buttonThrowers[0].getParent()
self.mouseWatcherNode = self.mouseWatcher.node()
if self.mouseInterface:
self.mouseInterface.reparentTo(self.mouseWatcher)
if self.recorder:
# If we have a recorder, the mouseWatcher belongs under a
# special MouseRecorder node, which may intercept the
# mouse activity.
mw = self.buttonThrowers[0].getParent()
mouseRecorder = MouseRecorder('mouse')
self.recorder.addRecorder(
'mouse', mouseRecorder.upcastToRecorderBase())
np = mw.getParent().attachNewNode(mouseRecorder)
mw.reparentTo(np)
# A special ButtonThrower to generate keyboard events and
# include the time from the OS. This is separate only to
# support legacy code that did not expect a time parameter; it
# will eventually be folded into the normal ButtonThrower,
# above.
mw = self.buttonThrowers[0].getParent()
self.timeButtonThrower = mw.attachNewNode(ButtonThrower('timeButtons'))
self.timeButtonThrower.node().setPrefix('time-')
self.timeButtonThrower.node().setTimeFlag(1)
# Tell the gui system about our new mouse watcher.
self.aspect2d.node().setMouseWatcher(mw.node())
self.aspect2dp.node().setMouseWatcher(mw.node())
self.pixel2d.node().setMouseWatcher(mw.node())
self.pixel2dp.node().setMouseWatcher(mw.node())
mw.node().addRegion(PGMouseWatcherBackground())
return self.buttonThrowers[0]
# [gjeon] this function is seperated from setupMouse to allow multiple mouse watchers
def setupMouseCB(self, win):
# For each mouse/keyboard device, we create
# - MouseAndKeyboard
# - MouseWatcher
# - ButtonThrower
# The ButtonThrowers are stored in a list, self.buttonThrowers.
# Given a ButtonThrower, one can access the MouseWatcher and
# MouseAndKeyboard using getParent.
#
# The MouseAndKeyboard generates mouse events and mouse
# button/keyboard events; the MouseWatcher passes them through
# unchanged when the mouse is not over a 2-d button, and passes
# nothing through when the mouse *is* over a 2-d button. Therefore,
# objects that don't want to get events when the mouse is over a
# button, like the driveInterface, should be parented to
# MouseWatcher, while objects that want events in all cases, like the
# chat interface, should be parented to the MouseAndKeyboard.
buttonThrowers = []
pointerWatcherNodes = []
for i in range(win.getNumInputDevices()):
name = win.getInputDeviceName(i)
mk = self.dataRoot.attachNewNode(MouseAndKeyboard(win, i, name))
mw = mk.attachNewNode(MouseWatcher("watcher%s" % (i)))
if win.getSideBySideStereo():
# If the window has side-by-side stereo enabled, then
# we should constrain the MouseWatcher to the window's
# DisplayRegion. This will enable the MouseWatcher to
# track the left and right halves of the screen
# individually.
mw.node().setDisplayRegion(win.getOverlayDisplayRegion())
mb = mw.node().getModifierButtons()
mb.addButton(KeyboardButton.shift())
mb.addButton(KeyboardButton.control())
mb.addButton(KeyboardButton.alt())
mb.addButton(KeyboardButton.meta())
mw.node().setModifierButtons(mb)
bt = mw.attachNewNode(ButtonThrower("buttons%s" % (i)))
if (i != 0):
bt.node().setPrefix('mousedev%s-' % (i))
mods = ModifierButtons()
mods.addButton(KeyboardButton.shift())
mods.addButton(KeyboardButton.control())
mods.addButton(KeyboardButton.alt())
mods.addButton(KeyboardButton.meta())
bt.node().setModifierButtons(mods)
buttonThrowers.append(bt)
if (win.hasPointer(i)):
pointerWatcherNodes.append(mw.node())
return buttonThrowers, pointerWatcherNodes
def enableSoftwareMousePointer(self):
"""
Creates some geometry and parents it to render2d to show
the currently-known mouse position. Useful if the mouse
pointer is invisible for some reason.
"""
mouseViz = render2d.attachNewNode('mouseViz')
lilsmiley = loader.loadModel('lilsmiley')
lilsmiley.reparentTo(mouseViz)
aspectRatio = self.getAspectRatio()
# Scale the smiley face to 32x32 pixels.
height = self.win.getSbsLeftYSize()
lilsmiley.setScale(
32.0 / height / aspectRatio,
1.0, 32.0 / height)
self.mouseWatcherNode.setGeometry(mouseViz.node())
def getAlt(self):
return self.mouseWatcherNode.getModifierButtons().isDown(
KeyboardButton.alt())
def getShift(self):
return self.mouseWatcherNode.getModifierButtons().isDown(
KeyboardButton.shift())
def getControl(self):
return self.mouseWatcherNode.getModifierButtons().isDown(
KeyboardButton.control())
def getMeta(self):
return self.mouseWatcherNode.getModifierButtons().isDown(
KeyboardButton.meta())
def addAngularIntegrator(self):
if not self.physicsMgrAngular:
self.physicsMgrAngular = 1
integrator = AngularEulerIntegrator()
self.physicsMgr.attachAngularIntegrator(integrator)
def enableParticles(self):
if not self.particleMgrEnabled:
if not self.particleMgr:
from direct.particles.ParticleManagerGlobal import particleMgr
self.particleMgr = particleMgr
self.particleMgr.setFrameStepping(1)
if not self.physicsMgr:
from PhysicsManagerGlobal import physicsMgr
self.physicsMgr = physicsMgr
integrator = LinearEulerIntegrator()
self.physicsMgr.attachLinearIntegrator(integrator)
self.particleMgrEnabled = 1
self.physicsMgrEnabled = 1
self.taskMgr.remove('manager-update')
self.taskMgr.add(self.updateManagers, 'manager-update')
def disableParticles(self):
if self.particleMgrEnabled:
self.particleMgrEnabled = 0
self.physicsMgrEnabled = 0
self.taskMgr.remove('manager-update')
def toggleParticles(self):
if self.particleMgrEnabled == 0:
self.enableParticles()
else:
self.disableParticles()
def isParticleMgrEnabled(self):
return self.particleMgrEnabled
def isPhysicsMgrEnabled(self):
return self.physicsMgrEnabled
def updateManagers(self, state):
dt = globalClock.getDt()
if (self.particleMgrEnabled == 1):
self.particleMgr.doParticles(dt)
if (self.physicsMgrEnabled == 1):
self.physicsMgr.doPhysics(dt)
return Task.cont
def createStats(self, hostname=None, port=None):
# You can specify pstats-host in your Config.prc or use ~pstats/~aipstats
# The default is localhost
if not self.wantStats:
return False
if PStatClient.isConnected():
PStatClient.disconnect()
# these default values match the C++ default values
if hostname is None:
hostname = ''
if port is None:
port = -1
PStatClient.connect(hostname, port)
return PStatClient.isConnected()
def addSfxManager(self, extraSfxManager):
# keep a list of sfx manager objects to apply settings to,
# since there may be others in addition to the one we create here
self.sfxManagerList.append(extraSfxManager)
newSfxManagerIsValid = (extraSfxManager!=None) and extraSfxManager.isValid()
self.sfxManagerIsValidList.append(newSfxManagerIsValid)
if newSfxManagerIsValid:
extraSfxManager.setActive(self.sfxActive)
def createBaseAudioManagers(self):
self.sfxPlayer = SfxPlayer.SfxPlayer()
sfxManager = AudioManager.createAudioManager()
self.addSfxManager(sfxManager)
self.musicManager = AudioManager.createAudioManager()
self.musicManagerIsValid=self.musicManager!=None \
and self.musicManager.isValid()
if self.musicManagerIsValid:
# ensure only 1 midi song is playing at a time:
self.musicManager.setConcurrentSoundLimit(1)
self.musicManager.setActive(self.musicActive)
# enableMusic/enableSoundEffects are meant to be called in response
# to a user request so sfxActive/musicActive represent how things
# *should* be, regardless of App/OS/HW state
def enableMusic(self, bEnableMusic):
# don't setActive(1) if no audiofocus
if self.AppHasAudioFocus and self.musicManagerIsValid:
self.musicManager.setActive(bEnableMusic)
self.musicActive = bEnableMusic
if bEnableMusic:
# This is useful when we want to play different music
# from what the manager has queued
messenger.send("MusicEnabled")
self.notify.debug("Enabling music")
else:
self.notify.debug("Disabling music")
def SetAllSfxEnables(self, bEnabled):
for i in range(len(self.sfxManagerList)):
if (self.sfxManagerIsValidList[i]):
self.sfxManagerList[i].setActive(bEnabled)
def enableSoundEffects(self, bEnableSoundEffects):
# don't setActive(1) if no audiofocus
if self.AppHasAudioFocus or (bEnableSoundEffects==0):
self.SetAllSfxEnables(bEnableSoundEffects)
self.sfxActive=bEnableSoundEffects
if bEnableSoundEffects:
self.notify.debug("Enabling sound effects")
else:
self.notify.debug("Disabling sound effects")
# enable/disableAllAudio allow a programmable global override-off
# for current audio settings. they're meant to be called when app
# loses audio focus (switched out), so we can turn off sound without
# affecting internal sfxActive/musicActive sound settings, so things
# come back ok when the app is switched back to
def disableAllAudio(self):
self.AppHasAudioFocus = 0
self.SetAllSfxEnables(0)
if self.musicManagerIsValid:
self.musicManager.setActive(0)
self.notify.debug("Disabling audio")
def enableAllAudio(self):
self.AppHasAudioFocus = 1
self.SetAllSfxEnables(self.sfxActive)
if self.musicManagerIsValid:
self.musicManager.setActive(self.musicActive)
self.notify.debug("Enabling audio")
# This function should only be in the loader but is here for
# backwards compatibility. Please do not add code here, add
# it to the loader.
def loadSfx(self, name):
return self.loader.loadSfx(name)
# This function should only be in the loader but is here for
# backwards compatibility. Please do not add code here, add
# it to the loader.
def loadMusic(self, name):
return self.loader.loadMusic(name)
def playSfx(
self, sfx, looping = 0, interrupt = 1, volume = None,
time = 0.0, node = None, listener = None, cutoff = None):
# This goes through a special player for potential localization
return self.sfxPlayer.playSfx(sfx, looping, interrupt, volume, time, node, listener, cutoff)
def playMusic(self, music, looping = 0, interrupt = 1, volume = None, time = 0.0):
if music:
if volume != None:
music.setVolume(volume)
# if interrupt was set to 0, start over even if it's
# already playing
if interrupt or (music.status() != AudioSound.PLAYING):
music.setTime(time)
music.setLoop(looping)
music.play()
def __resetPrevTransform(self, state):
# Clear out the previous velocity deltas now, after we have
# rendered (the previous frame). We do this after the render,
# so that we have a chance to draw a representation of spheres
# along with their velocities. At the beginning of the frame
# really means after the command prompt, which allows the user
# to interactively query these deltas meaningfully.
PandaNode.resetAllPrevTransform()
return Task.cont
def __dataLoop(self, state):
# traverse the data graph. This reads all the control
# inputs (from the mouse and keyboard, for instance) and also
# directly acts upon them (for instance, to move the avatar).
self.dgTrav.traverse(self.dataRootNode)
return Task.cont
def __ivalLoop(self, state):
# Execute all intervals in the global ivalMgr.
IntervalManager.ivalMgr.step()
return Task.cont
def initShadowTrav(self):
if not self.shadowTrav:
# set up the shadow collision traverser
self.shadowTrav = CollisionTraverser("base.shadowTrav")
self.shadowTrav.setRespectPrevTransform(False)
def __shadowCollisionLoop(self, state):
# run the collision traversal if we have a
# CollisionTraverser set.
if self.shadowTrav:
self.shadowTrav.traverse(self.render)
return Task.cont
def __collisionLoop(self, state):
# run the collision traversal if we have a
# CollisionTraverser set.
if self.cTrav:
self.cTrav.traverse(self.render)
if self.appTrav:
self.appTrav.traverse(self.render)
if self.shadowTrav:
self.shadowTrav.traverse(self.render)
messenger.send("collisionLoopFinished")
return Task.cont
def __audioLoop(self, state):
if (self.musicManager != None):
self.musicManager.update()
for x in self.sfxManagerList:
x.update()
return Task.cont
def __garbageCollectStates(self, state):
""" This task is started only when we have
garbage-collect-states set in the Config.prc file, in which
case we're responsible for taking out Panda's garbage from
time to time. This is not to be confused with Python's
garbage collection. """
TransformState.garbageCollect()
RenderState.garbageCollect()
return Task.cont
def __igLoop(self, state):
# We render the watch variables for the onScreenDebug as soon
# as we reasonably can before the renderFrame().
onScreenDebug.render()
if self.recorder:
self.recorder.recordFrame()
# Finally, render the frame.
self.graphicsEngine.renderFrame()
if self.clusterSyncFlag:
self.graphicsEngine.syncFrame()
if self.multiClientSleep:
time.sleep(0)
# We clear the text buffer for the onScreenDebug as soon
# as we reasonably can after the renderFrame().
onScreenDebug.clear()
if self.recorder:
self.recorder.playFrame()
if self.mainWinMinimized:
# If the main window is minimized, slow down the app a bit
# by sleeping here in igLoop so we don't use all available
# CPU needlessly.
# Note: this isn't quite right if multiple windows are
# open. We should base this on whether *all* windows are
# minimized, not just the main window. But it will do for
# now until someone complains.
time.sleep(0.1)
# Lerp stuff needs this event, and it must be generated in
# C++, not in Python.
throwNewFrame()
return Task.cont
def __igLoopSync(self, state):
# We render the watch variables for the onScreenDebug as soon
# as we reasonably can before the renderFrame().
onScreenDebug.render()
if self.recorder:
self.recorder.recordFrame()
self.cluster.collectData()
# Finally, render the frame.
self.graphicsEngine.renderFrame()
if self.clusterSyncFlag:
self.graphicsEngine.syncFrame()
if self.multiClientSleep:
time.sleep(0)
# We clear the text buffer for the onScreenDebug as soon
# as we reasonably can after the renderFrame().
onScreenDebug.clear()
if self.recorder:
self.recorder.playFrame()
if self.mainWinMinimized:
# If the main window is minimized, slow down the app a bit
# by sleeping here in igLoop so we don't use all available
# CPU needlessly.
# Note: this isn't quite right if multiple windows are
# open. We should base this on whether *all* windows are
# minimized, not just the main window. But it will do for
# now until someone complains.
time.sleep(0.1)
self.graphicsEngine.readyFlip()
self.cluster.waitForFlipCommand()
self.graphicsEngine.flipFrame()
# Lerp stuff needs this event, and it must be generated in
# C++, not in Python.
throwNewFrame()
return Task.cont
def restart(self,clusterSync=False,cluster=None):
self.shutdown()
# __resetPrevTransform goes at the very beginning of the frame.
self.taskMgr.add(
self.__resetPrevTransform, 'resetPrevTransform', sort = -51)
# give the dataLoop task a reasonably "early" sort,
# so that it will get run before most tasks
self.taskMgr.add(self.__dataLoop, 'dataLoop', sort = -50)
self.__deadInputs = 0
# spawn the ivalLoop with a later sort, so that it will
# run after most tasks, but before igLoop.
self.taskMgr.add(self.__ivalLoop, 'ivalLoop', sort = 20)
# make the collisionLoop task run before igLoop,
# but leave enough room for the app to insert tasks
# between collisionLoop and igLoop
self.taskMgr.add(self.__collisionLoop, 'collisionLoop', sort = 30)
if ConfigVariableBool('garbage-collect-states').getValue():
self.taskMgr.add(self.__garbageCollectStates, 'garbageCollectStates', sort = 46)
# give the igLoop task a reasonably "late" sort,
# so that it will get run after most tasks
self.cluster = cluster
if (not clusterSync or (cluster == None)):
self.taskMgr.add(self.__igLoop, 'igLoop', sort = 50)
else:
self.taskMgr.add(self.__igLoopSync, 'igLoop', sort = 50)
# the audioLoop updates the positions of 3D sounds.
# as such, it needs to run after the cull traversal in the igLoop.
self.taskMgr.add(self.__audioLoop, 'audioLoop', sort = 60)
self.eventMgr.restart()
def shutdown(self):
self.taskMgr.remove('audioLoop')
self.taskMgr.remove('igLoop')
self.taskMgr.remove('shadowCollisionLoop')
self.taskMgr.remove('collisionLoop')
self.taskMgr.remove('dataLoop')
self.taskMgr.remove('resetPrevTransform')
self.taskMgr.remove('ivalLoop')
self.taskMgr.remove('garbage_collect')
self.eventMgr.shutdown()
def getBackgroundColor(self, win = None):
"""
Returns the current window background color. This assumes
the window is set up to clear the color each frame (this is
the normal setting).
"""
if win == None:
win = self.win
return VBase4(win.getClearColor())
def setBackgroundColor(self, r = None, g = None, b = None, a = 0.0, win = None):
"""
Sets the window background color to the indicated value.
This assumes the window is set up to clear the color each
frame (this is the normal setting).
The color may be either a VBase3 or a VBase4, or a 3-component
tuple, or the individual r, g, b parameters.
"""
if g != None:
color = VBase4(r, g, b, a)
else:
arg = r
if isinstance(arg, VBase4):
color = arg
else:
color = VBase4(arg[0], arg[1], arg[2], a)
if win == None:
win = self.win
if win:
win.setClearColor(color)
def toggleBackface(self):
if self.backfaceCullingEnabled:
self.backfaceCullingOff()
else:
self.backfaceCullingOn()
def backfaceCullingOn(self):
if not self.backfaceCullingEnabled:
self.render.setTwoSided(0)
self.backfaceCullingEnabled = 1
def backfaceCullingOff(self):
if self.backfaceCullingEnabled:
self.render.setTwoSided(1)
self.backfaceCullingEnabled = 0
def toggleTexture(self):
if self.textureEnabled:
self.textureOff()
else:
self.textureOn()
def textureOn(self):
self.render.clearTexture()
self.textureEnabled = 1
def textureOff(self):
self.render.setTextureOff(100)
self.textureEnabled = 0
def toggleWireframe(self):
if self.wireframeEnabled:
self.wireframeOff()
else:
self.wireframeOn()
def wireframeOn(self):
self.render.setRenderModeWireframe(100)
self.render.setTwoSided(1)
self.wireframeEnabled = 1
def wireframeOff(self):
self.render.clearRenderMode()
render.setTwoSided(not self.backfaceCullingEnabled)
self.wireframeEnabled = 0
def disableMouse(self):
"""
Temporarily disable the mouse control of the camera, either
via the drive interface or the trackball, whichever is
currently in use.
"""
# We don't reparent the drive interface or the trackball;
# whichever one was there before will remain in the data graph
# and active. This way they won't lose button events while
# the mouse is disabled. However, we do move the mouse2cam
# object out of there, so we won't be updating the camera any
# more.
if self.mouse2cam:
self.mouse2cam.detachNode()
def enableMouse(self):
"""
Reverse the effect of a previous call to disableMouse().
useDrive() also implicitly enables the mouse.
"""
if self.mouse2cam:
self.mouse2cam.reparentTo(self.mouseInterface)
def silenceInput(self):
"""
This is a heavy-handed way of temporarily turning off
all inputs. Bring them back with reviveInput().
"""
if not self.__deadInputs:
self.__deadInputs = taskMgr.remove('dataLoop')
def reviveInput(self):
"""
Restores inputs after a previous call to silenceInput.
"""
if self.__deadInputs:
self.eventMgr.doEvents()
self.dgTrav.traverse(base.dataRootNode)
self.eventMgr.eventQueue.clear()
self.taskMgr.add(self.__dataLoop, 'dataLoop', sort = -50)
self.__deadInputs = 0
def setMouseOnNode(self, newNode):
if self.mouse2cam:
self.mouse2cam.node().setNode(newNode)
def changeMouseInterface(self, changeTo):
"""
Switch mouse action
"""
# Get rid of the prior interface:
self.mouseInterface.detachNode()
# Update the mouseInterface to point to the drive
self.mouseInterface = changeTo
self.mouseInterfaceNode = self.mouseInterface.node()
# Hookup the drive to the camera.
if self.mouseWatcher:
self.mouseInterface.reparentTo(self.mouseWatcher)
if self.mouse2cam:
self.mouse2cam.reparentTo(self.mouseInterface)
def useDrive(self):
"""
Switch mouse action to drive mode
"""
if self.drive:
self.changeMouseInterface(self.drive)
# Set the height to a good eyeheight
self.mouseInterfaceNode.reset()
self.mouseInterfaceNode.setZ(4.0)
def useTrackball(self):
"""
Switch mouse action to trackball mode
"""
if self.trackball:
self.changeMouseInterface(self.trackball)
def toggleTexMem(self):
""" Toggles a handy texture memory watcher. See TexMemWatcher
for more information. """
if self.texmem and not self.texmem.cleanedUp:
self.texmem.cleanup()
self.texmem = None
return
from direct.showutil.TexMemWatcher import TexMemWatcher
self.texmem = TexMemWatcher()
def toggleShowVertices(self):
""" Toggles a mode that visualizes vertex density per screen
area. """
if self.showVertices:
# Clean up the old mode.
self.showVertices.node().setActive(0)
dr = self.showVertices.node().getDisplayRegion(0)
base.win.removeDisplayRegion(dr)
self.showVertices.removeNode()
self.showVertices = None
return
dr = base.win.makeDisplayRegion()
dr.setSort(1000)
cam = Camera('showVertices')
cam.setLens(base.camLens)
# Set up a funny state to render only vertices.
override = 100000
t = NodePath('t')
t.setColor(1, 0, 1, 0.02, override)
t.setColorScale(1, 1, 1, 1, override)
t.setAttrib(ColorBlendAttrib.make(ColorBlendAttrib.MAdd, ColorBlendAttrib.OIncomingAlpha, ColorBlendAttrib.OOneMinusIncomingAlpha), override)
t.setAttrib(RenderModeAttrib.make(RenderModeAttrib.MPoint, 10), override)
t.setTwoSided(True, override)
t.setBin('fixed', 0, override)
t.setDepthTest(False, override)
t.setDepthWrite(False, override)
t.setLightOff(override)
t.setShaderOff(override)
t.setFogOff(override)
t.setAttrib(AntialiasAttrib.make(AntialiasAttrib.MNone), override)
t.setAttrib(RescaleNormalAttrib.make(RescaleNormalAttrib.MNone), override)
t.setTextureOff(override)
# Make the spots round, so there's less static in the display.
# This forces software point generation on many drivers, so
# it's not on by default.
if self.config.GetBool('round-show-vertices', False):
spot = PNMImage(256, 256, 1)
spot.renderSpot((1, 1, 1, 1), (0, 0, 0, 0), 0.8, 1)
tex = Texture('spot')
tex.load(spot)
tex.setFormat(tex.FAlpha)
t.setTexture(tex, override)
t.setAttrib(TexGenAttrib.make(TextureStage.getDefault(), TexGenAttrib.MPointSprite), override)
cam.setInitialState(t.getState())
cam.setCameraMask(~PandaNode.getOverallBit())
self.showVertices = self.cam.attachNewNode(cam)
dr.setCamera(self.showVertices)
def oobe(self, cam = None):
"""
Enable a special "out-of-body experience" mouse-interface
mode. This can be used when a "god" camera is needed; it
moves the camera node out from under its normal node and sets
the world up in trackball state. Button events are still sent
to the normal mouse action node (e.g. the DriveInterface), and
mouse events, if needed, may be sent to the normal node by
holding down the Control key.
This is different than useTrackball(), which simply changes
the existing mouse action to a trackball interface. In fact,
OOBE mode doesn't care whether useDrive() or useTrackball() is
in effect; it just temporarily layers a new trackball
interface on top of whatever the basic interface is. You can
even switch between useDrive() and useTrackball() while OOBE
mode is in effect.
This is a toggle; the second time this function is called, it
disables the mode.
"""
if cam is None:
cam = self.cam
# If oobeMode was never set, set it to false and create the
# structures we need to implement OOBE.
if not hasattr(self, 'oobeMode'):
self.oobeMode = 0
self.oobeCamera = self.hidden.attachNewNode('oobeCamera')
self.oobeCameraTrackball = self.oobeCamera.attachNewNode('oobeCameraTrackball')
self.oobeLens = PerspectiveLens()
self.oobeLens.setAspectRatio(self.getAspectRatio())
self.oobeLens.setNearFar(0.1, 10000.0)
self.oobeLens.setMinFov(40)
self.oobeTrackball = NodePath(Trackball('oobeTrackball'))
self.oobe2cam = self.oobeTrackball.attachNewNode(Transform2SG('oobe2cam'))
self.oobe2cam.node().setNode(self.oobeCameraTrackball.node())
self.oobeVis = loader.loadModel('models/misc/camera', okMissing = True)
if not self.oobeVis:
# Sometimes we have default-model-extension set to
# egg, but the file might be a bam file.
self.oobeVis = loader.loadModel('models/misc/camera.bam', okMissing = True)
if not self.oobeVis:
self.oobeVis = NodePath('oobeVis')
self.oobeVis.node().setFinal(1)
self.oobeVis.setLightOff(1)
self.oobeCullFrustum = None
self.accept('oobe-down', self.__oobeButton, extraArgs = [''])
self.accept('oobe-repeat', self.__oobeButton, extraArgs = ['-repeat'])
self.accept('oobe-up', self.__oobeButton, extraArgs = ['-up'])
if self.oobeMode:
# Disable OOBE mode.
if self.oobeCullFrustum != None:
# First, disable OOBE cull mode.
self.oobeCull(cam = cam)
if self.oobeVis:
self.oobeVis.reparentTo(self.hidden)
# Restore the mouse interface node, and remove the oobe
# trackball from the data path.
self.mouseInterfaceNode.clearButton(KeyboardButton.shift())
self.oobeTrackball.detachNode()
bt = self.buttonThrowers[0].node()
bt.setSpecificFlag(1)
bt.setButtonDownEvent('')
bt.setButtonRepeatEvent('')
bt.setButtonUpEvent('')
cam.reparentTo(self.camera)
#if cam == self.cam:
# self.camNode.setLens(self.camLens)
self.oobeCamera.reparentTo(self.hidden)
self.oobeMode = 0
bboard.post('oobeEnabled', False)
else:
bboard.post('oobeEnabled', True)
try:
cameraParent = localAvatar
except:
# Make oobeCamera be a sibling of wherever camera is now.
cameraParent = self.camera.getParent()
self.oobeCamera.reparentTo(cameraParent)
self.oobeCamera.clearMat()
# Make the regular MouseInterface node respond only when
# the shift button is pressed. And the oobe node will
# respond only when shift is *not* pressed.
self.mouseInterfaceNode.requireButton(KeyboardButton.shift(), True)
self.oobeTrackball.node().requireButton(KeyboardButton.shift(), False)
self.oobeTrackball.reparentTo(self.mouseWatcher)
# Set our initial OOB position to be just behind the camera.
mat = Mat4.translateMat(0, -10, 3) * self.camera.getMat(cameraParent)
mat.invertInPlace()
self.oobeTrackball.node().setMat(mat)
cam.reparentTo(self.oobeCameraTrackball)
# Temporarily disable button events by routing them
# through the oobe filters.
bt = self.buttonThrowers[0].node()
bt.setSpecificFlag(0)
bt.setButtonDownEvent('oobe-down')
bt.setButtonRepeatEvent('oobe-repeat')
bt.setButtonUpEvent('oobe-up')
# Don't change the camera lens--keep it with the original lens.
#if cam == self.cam:
# self.camNode.setLens(self.oobeLens)
if self.oobeVis:
self.oobeVis.reparentTo(self.camera)
self.oobeMode = 1
def __oobeButton(self, suffix, button):
if button.startswith('mouse'):
# Eat mouse buttons.
return
# Transmit other buttons.
messenger.send(button + suffix)
def oobeCull(self, cam = None):
"""
While in OOBE mode (see above), cull the viewing frustum as if
it were still attached to our original camera. This allows us
to visualize the effectiveness of our bounding volumes.
"""
if cam is None:
cam = self.cam
# First, make sure OOBE mode is enabled.
if not getattr(self, 'oobeMode', False):
self.oobe(cam = cam)
if self.oobeCullFrustum == None:
# Enable OOBE culling.
pnode = LensNode('oobeCull')
pnode.setLens(self.camLens)
pnode.showFrustum()
self.oobeCullFrustum = self.camera.attachNewNode(pnode)
# Tell the camera to cull from here instead of its own
# origin.
for c in base.camList:
c.node().setCullCenter(self.oobeCullFrustum)
if cam.node().isOfType(Camera):
cam.node().setCullCenter(self.oobeCullFrustum)
for c in cam.findAllMatches('**/+Camera'):
c.node().setCullCenter(self.oobeCullFrustum)
else:
# Disable OOBE culling.
for c in base.camList:
c.node().setCullCenter(NodePath())
if cam.node().isOfType(Camera):
cam.node().setCullCenter(self.oobeCullFrustum)
for c in cam.findAllMatches('**/+Camera'):
c.node().setCullCenter(NodePath())
self.oobeCullFrustum.removeNode()
self.oobeCullFrustum = None
def showCameraFrustum(self):
# Create a visible representation of the frustum.
self.removeCameraFrustum()
geom = self.camLens.makeGeometry()
if geom != None:
gn = GeomNode('frustum')
gn.addGeom(geom)
self.camFrustumVis = self.camera.attachNewNode(gn)
def removeCameraFrustum(self):
if self.camFrustumVis:
self.camFrustumVis.removeNode()
def screenshot(self, namePrefix = 'screenshot',
defaultFilename = 1, source = None,
imageComment=""):
""" Captures a screenshot from the main window or from the
specified window or Texture and writes it to a filename in the
current directory (or to a specified directory).
If defaultFilename is True, the filename is synthesized by
appending namePrefix to a default filename suffix (including
the filename extension) specified in the Config variable
screenshot-filename. Otherwise, if defaultFilename is False,
the entire namePrefix is taken to be the filename to write,
and this string should include a suitable filename extension
that will be used to determine the type of image file to
write.
Normally, the source is a GraphicsWindow, GraphicsBuffer or
DisplayRegion. If a Texture is supplied instead, it must have
a ram image (that is, if it was generated by
makeTextureBuffer() or makeCubeMap(), the parameter toRam
should have been set true). If it is a cube map texture as
generated by makeCubeMap(), namePrefix should contain the hash
mark ('#') character.
The return value is the filename if successful, or None if
there is a problem.
"""
if source == None:
source = self.win
if defaultFilename:
filename = GraphicsOutput.makeScreenshotFilename(namePrefix)
else:
filename = Filename(namePrefix)
if isinstance(source, Texture):
if source.getZSize() > 1:
saved = source.write(filename, 0, 0, 1, 0)
else:
saved = source.write(filename)
else:
saved = source.saveScreenshot(filename, imageComment)
if saved:
# Announce to anybody that a screenshot has been taken
messenger.send('screenshot', [filename])
return filename
return None
def saveCubeMap(self, namePrefix = 'cube_map_#.png',
defaultFilename = 0, source = None,
camera = None, size = 128,
cameraMask = PandaNode.getAllCameraMask()):
"""
Similar to screenshot(), this sets up a temporary cube map
Texture which it uses to take a series of six snapshots of the
current scene, one in each of the six cube map directions.
This requires rendering a new frame.
Unlike screenshot(), source may only be a GraphicsWindow,
GraphicsBuffer, or DisplayRegion; it may not be a Texture.
camera should be the node to which the cubemap cameras will be
parented. The default is the camera associated with source,
if source is a DisplayRegion, or base.camera otherwise.
The return value is the filename if successful, or None if
there is a problem.
"""
if source == None:
source = base.win
if camera == None:
if hasattr(source, "getCamera"):
camera = source.getCamera()
if camera == None:
camera = base.camera
if hasattr(source, "getWindow"):
source = source.getWindow()
rig = NodePath(namePrefix)
buffer = source.makeCubeMap(namePrefix, size, rig, cameraMask, 1)
if buffer == None:
raise StandardError, "Could not make cube map."
# Set the near and far planes from the default lens.
lens = rig.find('**/+Camera').node().getLens()
lens.setNearFar(base.camLens.getNear(), base.camLens.getFar())
# Now render a frame to fill up the texture.
rig.reparentTo(camera)
base.graphicsEngine.openWindows()
base.graphicsEngine.renderFrame()
tex = buffer.getTexture()
saved = self.screenshot(namePrefix = namePrefix,
defaultFilename = defaultFilename,
source = tex)
base.graphicsEngine.removeWindow(buffer)
rig.removeNode()
return saved
def saveSphereMap(self, namePrefix = 'spheremap.png',
defaultFilename = 0, source = None,
camera = None, size = 256,
cameraMask = PandaNode.getAllCameraMask(),
numVertices = 1000):
"""
This works much like saveCubeMap(), and uses the graphics
API's hardware cube-mapping ability to get a 360-degree view
of the world. But then it converts the six cube map faces
into a single fisheye texture, suitable for applying as a
static environment map (sphere map).
For eye-relative static environment maps, sphere maps are
often preferable to cube maps because they require only a
single texture and because they are supported on a broader
range of hardware.
The return value is the filename if successful, or None if
there is a problem.
"""
if source == None:
source = base.win
if camera == None:
if hasattr(source, "getCamera"):
camera = source.getCamera()
if camera == None:
camera = base.camera
if hasattr(source, "getWindow"):
source = source.getWindow()
# First, make an offscreen buffer to convert the cube map to a
# sphere map. We make it first so we can guarantee the
# rendering order for the cube map.
toSphere = source.makeTextureBuffer(namePrefix, size, size,
Texture(), 1)
# Now make the cube map buffer.
rig = NodePath(namePrefix)
buffer = toSphere.makeCubeMap(namePrefix, size, rig, cameraMask, 0)
if buffer == None:
base.graphicsEngine.removeWindow(toSphere)
raise StandardError, "Could not make cube map."
# Set the near and far planes from the default lens.
lens = rig.find('**/+Camera').node().getLens()
lens.setNearFar(base.camLens.getNear(), base.camLens.getFar())
# Set up the scene to convert the cube map. It's just a
# simple scene, with only the FisheyeMaker object in it.
dr = toSphere.makeMonoDisplayRegion()
camNode = Camera('camNode')
lens = OrthographicLens()
lens.setFilmSize(2, 2)
lens.setNearFar(-1000, 1000)
camNode.setLens(lens)
root = NodePath('buffer')
cam = root.attachNewNode(camNode)
dr.setCamera(cam)
fm = FisheyeMaker('card')
fm.setNumVertices(numVertices)
fm.setSquareInscribed(1, 1.1)
fm.setReflection(1)
card = root.attachNewNode(fm.generate())
card.setTexture(buffer.getTexture())
# Now render a frame. This will render out the cube map and
# then apply it to the the card in the toSphere buffer.
rig.reparentTo(camera)
base.graphicsEngine.openWindows()
base.graphicsEngine.renderFrame()
# One more frame for luck.
base.graphicsEngine.renderFrame()
saved = self.screenshot(namePrefix = namePrefix,
defaultFilename = defaultFilename,
source = toSphere.getTexture())
base.graphicsEngine.removeWindow(buffer)
base.graphicsEngine.removeWindow(toSphere)
rig.removeNode()
return saved
def movie(self, namePrefix = 'movie', duration = 1.0, fps = 30,
format = 'png', sd = 4, source = None):
"""
Spawn a task to capture a movie using the screenshot function.
- namePrefix will be used to form output file names (can include
path information (e.g. '/i/beta/frames/myMovie')
- duration is the length of the movie in seconds
- fps is the frame rate of the resulting movie
- format specifies output file format (e.g. png, bmp)
- sd specifies number of significant digits for frame count in the
output file name (e.g. if sd = 4, movie_0001.png)
- source is the Window, Buffer, DisplayRegion, or Texture from which
to save the resulting images. The default is the main window.
"""
globalClock.setMode(ClockObject.MNonRealTime)
globalClock.setDt(1.0/float(fps))
t = taskMgr.add(self._movieTask, namePrefix + '_task')
t.frameIndex = 0 # Frame 0 is not captured.
t.numFrames = int(duration * fps)
t.source = source
t.outputString = namePrefix + '_%0' + repr(sd) + 'd.' + format
t.setUponDeath(lambda state: globalClock.setMode(ClockObject.MNormal))
def _movieTask(self, state):
if state.frameIndex != 0:
frameName = state.outputString % state.frameIndex
self.notify.info("Capturing frame: " + frameName)
self.screenshot(namePrefix = frameName, defaultFilename = 0,
source = state.source)
state.frameIndex += 1
if state.frameIndex > state.numFrames:
return Task.done
else:
return Task.cont
def windowEvent(self, win):
if win == self.win:
properties = win.getProperties()
self.notify.info("Got window event: %s" % (repr(properties)))
if not properties.getOpen():
# If the user closes the main window, we should exit.
self.notify.info("User closed main window.")
if __dev__ and config.GetBool('auto-garbage-logging', 0):
GarbageReport.b_checkForGarbageLeaks()
self.userExit()
if properties.getForeground() and not self.mainWinForeground:
self.mainWinForeground = 1
elif not properties.getForeground() and self.mainWinForeground:
self.mainWinForeground = 0
if __dev__ and config.GetBool('auto-garbage-logging', 0):
GarbageReport.b_checkForGarbageLeaks()
if properties.getMinimized() and not self.mainWinMinimized:
# If the main window is minimized, throw an event to
# stop the music.
self.mainWinMinimized = 1
messenger.send('PandaPaused')
elif not properties.getMinimized() and self.mainWinMinimized:
# If the main window is restored, throw an event to
# restart the music.
self.mainWinMinimized = 0
messenger.send('PandaRestarted')
# If we have not forced the aspect ratio, let's see if it has
# changed and update the camera lenses and aspect2d parameters
self.adjustWindowAspectRatio(self.getAspectRatio())
# Temporary hasattr for old Pandas
if not hasattr(win, 'getSbsLeftXSize'):
self.pixel2d.setScale(2.0 / win.getXSize(), 1.0, 2.0 / win.getYSize())
self.pixel2dp.setScale(2.0 / win.getXSize(), 1.0, 2.0 / win.getYSize())
else:
self.pixel2d.setScale(2.0 / win.getSbsLeftXSize(), 1.0, 2.0 / win.getSbsLeftYSize())
self.pixel2dp.setScale(2.0 / win.getSbsLeftXSize(), 1.0, 2.0 / win.getSbsLeftYSize())
def adjustWindowAspectRatio(self, aspectRatio):
""" This function is normally called internally by
windowEvent(), but it may also be called to explicitly adjust
the aspect ratio of the render/render2d DisplayRegion, by a
class that has redefined these. """
if self.__configAspectRatio:
aspectRatio = self.__configAspectRatio
if aspectRatio != self.__oldAspectRatio:
self.__oldAspectRatio = aspectRatio
# Fix up some anything that depends on the aspectRatio
if self.camLens:
self.camLens.setAspectRatio(aspectRatio)
if aspectRatio < 1:
# If the window is TALL, lets expand the top and bottom
self.aspect2d.setScale(1.0, aspectRatio, aspectRatio)
self.a2dTop = 1.0 / aspectRatio
self.a2dBottom = - 1.0 / aspectRatio
self.a2dLeft = -1
self.a2dRight = 1.0
# Don't forget 2dp
self.aspect2dp.setScale(1.0, aspectRatio, aspectRatio)
self.a2dpTop = 1.0 / aspectRatio
self.a2dpBottom = - 1.0 / aspectRatio
self.a2dpLeft = -1
self.a2dpRight = 1.0
else:
# If the window is WIDE, lets expand the left and right
self.aspect2d.setScale(1.0 / aspectRatio, 1.0, 1.0)
self.a2dTop = 1.0
self.a2dBottom = -1.0
self.a2dLeft = -aspectRatio
self.a2dRight = aspectRatio
# Don't forget 2dp
self.aspect2dp.setScale(1.0 / aspectRatio, 1.0, 1.0)
self.a2dpTop = 1.0
self.a2dpBottom = -1.0
self.a2dpLeft = -aspectRatio
self.a2dpRight = aspectRatio
# Reposition the aspect2d marker nodes
self.a2dTopCenter.setPos(0, self.a2dTop, self.a2dTop)
self.a2dBottomCenter.setPos(0, self.a2dBottom, self.a2dBottom)
self.a2dLeftCenter.setPos(self.a2dLeft, 0, 0)
self.a2dRightCenter.setPos(self.a2dRight, 0, 0)
self.a2dTopLeft.setPos(self.a2dLeft, self.a2dTop, self.a2dTop)
self.a2dTopRight.setPos(self.a2dRight, self.a2dTop, self.a2dTop)
self.a2dBottomLeft.setPos(self.a2dLeft, self.a2dBottom, self.a2dBottom)
self.a2dBottomRight.setPos(self.a2dRight, self.a2dBottom, self.a2dBottom)
# Reposition the aspect2d marker nodes
self.a2dTopCenterNs.setPos(0, self.a2dTop, self.a2dTop)
self.a2dBottomCenterNs.setPos(0, self.a2dBottom, self.a2dBottom)
self.a2dLeftCenterNs.setPos(self.a2dLeft, 0, 0)
self.a2dRightCenterNs.setPos(self.a2dRight, 0, 0)
self.a2dTopLeftNs.setPos(self.a2dLeft, self.a2dTop, self.a2dTop)
self.a2dTopRightNs.setPos(self.a2dRight, self.a2dTop, self.a2dTop)
self.a2dBottomLeftNs.setPos(self.a2dLeft, self.a2dBottom, self.a2dBottom)
self.a2dBottomRightNs.setPos(self.a2dRight, self.a2dBottom, self.a2dBottom)
# Reposition the aspect2dp marker nodes
self.a2dpTopCenter.setPos(0, self.a2dpTop, self.a2dpTop)
self.a2dpBottomCenter.setPos(0, self.a2dpBottom, self.a2dpBottom)
self.a2dpLeftCenter.setPos(self.a2dpLeft, 0, 0)
self.a2dpRightCenter.setPos(self.a2dpRight, 0, 0)
self.a2dpTopLeft.setPos(self.a2dpLeft, self.a2dpTop, self.a2dpTop)
self.a2dpTopRight.setPos(self.a2dpRight, self.a2dpTop, self.a2dpTop)
self.a2dpBottomLeft.setPos(self.a2dpLeft, self.a2dpBottom, self.a2dpBottom)
self.a2dpBottomRight.setPos(self.a2dpRight, self.a2dpBottom, self.a2dpBottom)
# If anybody needs to update their GUI, put a callback on this event
messenger.send("aspectRatioChanged")
def userExit(self):
# The user has requested we exit the program. Deal with this.
if self.exitFunc:
self.exitFunc()
self.notify.info("Exiting ShowBase.")
self.finalizeExit()
def finalizeExit(self):
sys.exit()
# [gjeon] start wxPython
def startWx(self, fWantWx = True):
fWantWx = bool(fWantWx)
if self.wantWx != fWantWx:
self.wantWx = fWantWx
if self.wantWx:
self.spawnWxLoop()
def spawnWxLoop(self):
""" Call this method to hand the main loop over to wxPython.
This sets up a wxTimer callback so that Panda still gets
updated, but wxPython owns the main loop (which seems to make
it happier than the other way around). """
if self.wxApp:
# Don't do this twice.
return
initAppForGui()
import wx
# Create a new base.wxApp.
self.wxApp = wx.PySimpleApp(redirect = False)
if ConfigVariableBool('wx-main-loop', True):
# Put wxPython in charge of the main loop. It really
# seems to like this better; some features of wx don't
# work properly unless this is true.
# Set a timer to run the Panda frame 60 times per second.
wxFrameRate = ConfigVariableDouble('wx-frame-rate', 60.0)
self.wxTimer = wx.Timer(self.wxApp)
self.wxTimer.Start(1000.0 / wxFrameRate.getValue())
self.wxApp.Bind(wx.EVT_TIMER, self.__wxTimerCallback)
# wx is now the main loop, not us any more.
self.run = self.wxRun
self.taskMgr.run = self.wxRun
__builtin__.run = self.wxRun
if self.appRunner:
self.appRunner.run = self.wxRun
else:
# Leave Panda in charge of the main loop. This is
# friendlier for IDE's and interactive editing in general.
def wxLoop(task):
# First we need to ensure that the OS message queue is
# processed.
base.wxApp.Yield()
# Now do all the wxPython events waiting on this frame.
while base.wxApp.Pending():
base.wxApp.Dispatch()
return task.again
self.taskMgr.add(wxLoop, 'wxLoop')
def __wxTimerCallback(self, event):
if Thread.getCurrentThread().getCurrentTask():
# This happens when the wxTimer expires while igLoop is
# rendering. Ignore it.
return
self.taskMgr.step()
def wxRun(self):
""" This method replaces base.run() after we have called
spawnWxLoop(). Since at this point wxPython now owns the main
loop, this method is a call to wxApp.MainLoop(). """
if Thread.getCurrentThread().getCurrentTask():
# This happens in the p3d environment during startup.
# Ignore it.
return
self.wxApp.MainLoop()
def startTk(self, fWantTk = True):
fWantTk = bool(fWantTk)
if self.wantTk != fWantTk:
self.wantTk = fWantTk
if self.wantTk:
self.spawnTkLoop()
def spawnTkLoop(self):
""" Call this method to hand the main loop over to Tkinter.
This sets up a timer callback so that Panda still gets
updated, but Tkinter owns the main loop (which seems to make
it happier than the other way around). """
if self.tkRoot:
# Don't do this twice.
return
from Tkinter import tkinter
import Pmw
# Create a new Tk root.
self.tkRoot = Pmw.initialise()
__builtin__.tkroot = self.tkRoot
initAppForGui()
if ConfigVariableBool('tk-main-loop', True):
# Put Tkinter in charge of the main loop. It really
# seems to like this better; the GUI otherwise becomes
# largely unresponsive on Mac OS X unless this is true.
# Set a timer to run the Panda frame 60 times per second.
tkFrameRate = ConfigVariableDouble('tk-frame-rate', 60.0)
self.tkDelay = int(1000.0 / tkFrameRate.getValue())
self.tkRoot.after(self.tkDelay, self.__tkTimerCallback)
# wx is now the main loop, not us any more.
self.run = self.tkRun
self.taskMgr.run = self.tkRun
__builtin__.run = self.tkRun
if self.appRunner:
self.appRunner.run = self.tkRun
else:
# Leave Panda in charge of the main loop. This is
# friendlier for IDE's and interactive editing in general.
def tkLoop(task):
# Do all the tkinter events waiting on this frame
# dooneevent will return 0 if there are no more events
# waiting or 1 if there are still more.
# DONT_WAIT tells tkinter not to block waiting for events
while tkinter.dooneevent(tkinter.ALL_EVENTS | tkinter.DONT_WAIT):
pass
return task.again
self.taskMgr.add(tkLoop, 'tkLoop')
def __tkTimerCallback(self):
if not Thread.getCurrentThread().getCurrentTask():
self.taskMgr.step()
self.tkRoot.after(self.tkDelay, self.__tkTimerCallback)
def tkRun(self):
""" This method replaces base.run() after we have called
spawnTkLoop(). Since at this point Tkinter now owns the main
loop, this method is a call to tkRoot.mainloop(). """
if Thread.getCurrentThread().getCurrentTask():
# This happens in the p3d environment during startup.
# Ignore it.
return
self.tkRoot.mainloop()
def startDirect(self, fWantDirect = 1, fWantTk = 1, fWantWx = 0):
self.startTk(fWantTk)
self.startWx(fWantWx)
self.wantDirect = fWantDirect
if self.wantDirect:
from direct.directtools import DirectSession
base.direct.enable()
else:
__builtin__.direct = self.direct = None
def getRepository(self):
return None
def getAxes(self):
return loader.loadModel("models/misc/xyzAxis.bam")
def __doStartDirect(self):
if self.__directStarted:
return
self.__directStarted = False
# Start Tk, Wx and DIRECT if specified by Config.prc
fTk = self.config.GetBool('want-tk', 0)
fWx = self.config.GetBool('want-wx', 0)
# Start DIRECT if specified in Config.prc or in cluster mode
fDirect = (self.config.GetBool('want-directtools', 0) or
(self.config.GetString("cluster-mode", '') != ''))
# Set fWantTk to 0 to avoid starting Tk with this call
self.startDirect(fWantDirect = fDirect, fWantTk = fTk, fWantWx = fWx)
def run(self):
# This method runs the TaskManager when self.appRunner is
# None, which is to say, when we are not running from within a
# p3d file. When we *are* within a p3d file, the Panda
# runtime has to be responsible for running the main loop, so
# we can't allow the application to do it.
if self.appRunner is None or self.appRunner.dummy or \
(self.appRunner.interactiveConsole and not self.appRunner.initialAppImport):
self.taskMgr.run()
# A class to encapsulate information necessary for multiwindow support.
class WindowControls:
def __init__(
self, win, cam=None, camNode=None, cam2d=None, mouseWatcher=None,
mouseKeyboard=None, closeCmd=lambda: 0, grid=None):
self.win = win
self.camera = cam
if camNode is None and cam is not None:
camNode = cam.node()
self.camNode = camNode
self.camera2d = cam2d
self.mouseWatcher = mouseWatcher
self.mouseKeyboard = mouseKeyboard
self.closeCommand = closeCmd
self.grid = grid
def __str__(self):
s = "window = " + str(self.win) + "\n"
s += "camera = " + str(self.camera) + "\n"
s += "camNode = " + str(self.camNode) + "\n"
s += "camera2d = " + str(self.camera2d) + "\n"
s += "mouseWatcher = " + str(self.mouseWatcher) + "\n"
s += "mouseAndKeyboard = " + str(self.mouseKeyboard) + "\n"
return s