1 #!/usr/local/bin/python
3 SPyRE -> Simple Pythonic Rendering Engine.
4 A simple OpenGL rendering engine
6 Spyre has five types of objects which together make up the engine.
9 Includes main loop, handles event queue, passing input data to
10 interface, maintaining viewport and tracking and displaying
14 Controls the primary camera, the engine, and the timekeeper in
15 response to user input.
18 Maintains eye position and direction of view, as well as projection
19 config (frustum or ortho). Different cameras are suited to different
20 interfaces, but MobileCamera is common.
23 Maintains lights, including fixed, mobile, and camera-mounted types;
24 and depth-cueing (fog) and some materials details.
27 Controls pace of engine run, can switch engine advancement on and
28 off. Also controls and monitors framerate.
31 In addition to the above five elements of the engine assembly, there are
32 displayable objects. These all descend from the Object class.
34 An ultra-simple example::
35 This is not tested. See scripts in demos directory for
37 =========================
39 import OpenGL.GLU as oglu
41 # create displayable object, subclassing Object
42 def Sphere(spyre.Object):
43 def __init__(self, diameter): self.rad = diameter/2.0
45 quad = oglu.gluNewQuadric()
46 oglu.gluSphere(quad, self.rad, 60, 60)
54 ===========================
55 @group Engine: Engine, EngineFullScreen
56 @group Timekeeper: TimeKeeper
57 @group Studios: StudioColorMat, Light, Sun, Bulb, Spot, NullStudio
58 @group Interfaces: Interface, BareBonesInterface, BasicInterface, PivotingInterface,
59 PanningInterface, PedestrianInterface, PedestrianInterfaceY, CursorKeyInterface
60 @group Camera: Camera, MobileCamera, MobileCameraFrustum, MobileCameraOrtho, BasicCamera,
61 BasicCameraOrtho, BasicCameraFrustum, PrecessingCamera, PrecessingCameraOrtho,
62 RovingCamera, RovingCameraFrustum, RovingCameraFrustumY, RovingCameraOrtho,
63 RovingCameraOrthoY, RovingCameraY, FrustumCam, OrthoCam
64 @group others: Object, Group
70 __url__
= 'http://pduel.sourceforge.net/'
71 __author__
= 'David Keeney <dkeeney@travelbyroad.net>'
72 __copyright__
= 'Copyright (C) 2000-2005 Erik Max Francis, 2004-2006 David Keeney'
83 from pygame
import display
, key
, init
, event
84 from pygame
import time
as pytime
85 from pygame
.locals import *
86 import OpenGL
.GL
as ogl
87 import OpenGL
.GLU
as oglu
89 # Some mathematical constants for convenience.
93 piOverOneEighty
= pi
/180
94 radiansToDegrees
= 1/piOverOneEighty
95 degreesToRadians
= piOverOneEighty
96 sqrtTwo
= math
.sqrt(2)
99 REDRAW_TIMER
= USEREVENT
+1
101 def cartesianToSpherical (x
, y
, z
):
102 """Convert cartesion coords (x,y,z) to spherical (rho,theta,phi)."""
104 rho
= math
.sqrt(x
*x
+ y
*y
+ z
*z
)
105 phi
= math
.asin(z
/rho
)
106 r
= rho
*math
.cos(phi
)
107 if r
< abs(y
): r
= abs(y
)
111 theta
= math
.asin(y
/r
)
113 theta
= math
.atan(y
/x
)
116 return rho
, theta
, phi
118 def sphericalToCartesian (rho
, theta
, phi
):
119 """Convert spherical (rho,theta,phi) to cartesion coords (x,y,z)."""
120 if abs(phi
) > piOverTwo
:
121 raise ValueError, phi
122 if abs(theta
) > twoPi
:
123 raise ValueError, theta
124 x
= rho
* math
.cos(phi
) * math
.cos(theta
)
125 y
= rho
* math
.cos(phi
) * math
.sin(theta
)
126 z
= rho
* math
.sin(phi
)
130 # Object #=====================================================================
132 class Object(object): # making class 'new-style'
134 """The fundamental object. Contains class variables for run time
135 and run (turn) counter.
137 All displayable objects should subclass from this. In addition to the
138 inherited method 'display', a displayable object may have an 'update' and
141 The class vars runTime and runTurn are maintained by the TimeKeeper instance.
143 @cvar runTime: how long has engine been running?
144 @cvar runTurn: how many frames have elapsed?
145 @cvar engine: makes engine accessible to displayable objects.
148 # reference to current engine
151 # list of opengl state dependant objects
152 opengl_state_dependent
= []
154 # these class vars track engine runtime (fictional)
156 # these are typically maintained/manipulated by a runTimer object
162 """Abstract initializer."""
163 if self
.__class
__ is Object
:
164 raise NotImplementedError
167 """Display the object using OpenGL This method must be overridden."""
168 raise NotImplementedError
171 ## various Object descendants from zoe are now in the zoeobj.py file
173 # Camera =====================================================================
175 ## ------ these are mixins to provide ortho/perspective to -------
176 ## ------ various cameras ----------------------------------------
178 class OrthoCam(object):
179 """An OrthoCam is a mixin that provides for ortho mode
180 in descendant cameras.
182 Each camera installed in an engine must inherit from either
183 FrustumCam or OrthoCam.
186 """Pushes ortho params/settings through to OpenGL """
187 ogl
.glMatrixMode(ogl
.GL_PROJECTION
)
189 ogl
.glOrtho(self
.left
, self
.right
,
190 self
.bottom
, self
.top
,
194 """Sets eye and center position in OpenGL """
195 ogl
.glMatrixMode(ogl
.GL_MODELVIEW
)
197 oglu
.gluLookAt(self
.eye
[0], self
.eye
[1], self
.eye
[2],
198 self
.center
[0], self
.center
[1], self
.center
[2],
199 self
.up
[0], self
.up
[1], self
.up
[2])
201 def shape(self
, left
, right
, bottom
, top
, near
=None, far
=None):
202 """Sets frustum shape members.
203 (left, top, near) and (right, bottom, near) map to the diametrical
204 opposite corners of the viewport, in x,y,z coords, modelview space.
206 near and far params are optional, and if ommitted, prior attribute
207 values remain. all params are floats.
221 class FrustumCam(OrthoCam
):
222 """A FrustumCam is a mixin that provides for frustum (perspective) mode
223 in descendant cameras.
225 Each camera installed in an engine must inherit from either
226 FrustumCam or OrthoCam.
229 """Pushes frustum params/settings through to OpenGL """
230 ogl
.glMatrixMode(ogl
.GL_PROJECTION
)
232 ogl
.glFrustum(self
.left
, self
.right
,
233 self
.bottom
, self
.top
,
236 frustum
= OrthoCam
.shape
238 def perspective(self
, fovy
, aspect
, near
, far
):
239 """Alternate way to specify frustum.
240 @param fovy: field of view angle, degrees
241 @param aspect: height/width (float)
242 @param near: near side of viewing box (float)
243 @param far: far side of viewing box (float)
245 # from Mesa source, Brian Paul
246 ymax
= near
* math
.tan(fovy
* pi
/ 360.0)
251 self
.frustum(xmin
, xmax
, ymin
, ymax
, near
, far
)
254 ## -- the cameras -------------------
256 class Camera(object):
257 """Abstract camera base class. A usable camera would inherit from
258 this, and from FrustumCam or OrthoCam.
260 Each engine must have at least one camera. If none are provided
261 by programmer, the interface will install its preferred camera.
263 Each camera tracks its own viewport, which is by default the
266 @ivar objects: Each camera keeps a list of displayable objects.
267 By default this is a reference to the engines list, and all
268 objects are displayed for each camera. This behavior can
269 be overriden such that each camera displays a subset of
271 @sort: __init__, setup, eye, center, zoomIn, zoomOut, setViewport,
272 displayViewport, displayBackground
273 @undocumented: object
276 def __init__(self
, engine
, eye
, center
, up
,
277 left
, right
, bottom
, top
, near
, far
):
278 """Initialize new camera.
279 @param engine: the engine
280 @param eye: x,y,z tuple with eye position
281 @param center: x,y,z tuple with center of view
282 @param up: x,y,z tuple pointing up
283 @param left: left edge of viewing box, in eyespace (float)
284 @param right: right edge of viewing box, in eyespace (float)
285 @param bottom: bottom edge of viewing box, in eyespace (float)
286 @param top: top edge of viewing box, in eyespace (float)
287 @param near: near edge of viewing box, in eyespace (float)
288 @param far: far edge of viewing box, in eyespace (float)"""
297 self
._center
= center
299 self
.background
= (0,0,0,0)
300 self
.fractViewport
= (0,0,1,1)
301 self
.viewport
= (0, 0, self
.engine
.width
, self
.engine
.height
)
302 self
.objects
= engine
.objects
307 """Overridable for things to be done before use, but after engine
308 and interface are initialized.
310 self
.setViewport(*self
.fractViewport
)
313 """Save state of initialization internally for later restoration."""
314 self
.__saveinit
= (self
._eye
, self
._center
,
315 self
.left
, self
.right
,
316 self
.bottom
, self
.top
,
319 def restoreInit(self
):
320 """Restore previously saved initialization state."""
321 stuff
= self
.__saveinit
323 self
._center
= stuff
[1]
324 self
.left
, self
.right
, self
.bottom
, self
.top
, self
.near
, \
328 """ methods to access center position property """
331 def _setCtr(self
, cntr
):
335 center
= property(_getCtr
, _setCtr
, None, "Track center position")
338 """ methods to access eye position property """
341 def _setEye(self
, eye
):
345 eye
= property(_getEye
, _setEye
, None, "Track eye position")
347 def zoomIn(self
, factor
=sqrtTwo
):
348 """Zoom in. Narrows the viewing box.
350 @param factor: ratio of old width to new width.
355 self
.bottom
/= factor
358 def zoomOut(self
, factor
=sqrtTwo
):
359 """Zoom out. Widens the viewing box.
361 @param factor: ratio of new width to old width.
366 self
.bottom
*= factor
370 """Refresh the position of the camera."""
373 def displayBackground(self
):
374 """Clear the display, or alternate prelim treatment.
375 Always clears depth buffer, clears color if background
376 attribute is set. This method can be overridden for
377 multiviewport effects.
379 bits
= ogl
.GL_DEPTH_BUFFER_BIT
381 ogl
.glClearColor(*self
.background
)
382 bits |
= ogl
.GL_COLOR_BUFFER_BIT
385 def displayViewport(self
):
386 """Push the viewport params to OpenGL."""
387 ogl
.glViewport(*self
.viewport
)
388 ogl
.glScissor(*self
.viewport
)
390 def setViewport(self
, left
, top
, right
, bottom
):
391 """Record desired viewport (left, top, right, bottom). Each value
392 is in range 0-1, representing fraction of window. The default
393 (0,0,1,1) is entire window.
395 self
.fractViewport
= (left
, top
, right
, bottom
)
396 assert(max(self
.fractViewport
)<=1)
397 assert(min(self
.fractViewport
)>=0)
398 w
, h
= self
.engine
.width
, self
.engine
.height
399 self
.viewport
= (int(left
*w
), int(top
*h
),
400 int((right
-left
)*w
), int((bottom
-top
)*h
))
403 class BasicCamera(Camera
):
404 """A basic camera views a center point from an eye position, with
405 a reference up vector that points to the top of the screen.
408 def __init__( self
, engine
, eye
,
409 center
= (0,0,0), up
= (0,1,0),
410 left
=None, right
=None,
411 bottom
=None, top
=None,
412 near
=None, far
=None):
413 if left
is None: left
= -5
414 if right
is None: right
= 5
415 if top
is None: top
= 5
416 if bottom
is None: bottom
= -5
417 if near
is None: near
= 1
418 if far
is None: far
= 11
419 Camera
.__init
__(self
, engine
, eye
, center
, up
,
420 left
, right
, bottom
, top
, near
, far
)
424 class BasicCameraFrustum(FrustumCam
, BasicCamera
):
428 class BasicCameraOrtho(OrthoCam
, BasicCamera
):
432 class PrecessingCamera(BasicCamera
):
433 """A precessing camera orbits around the z axis at a given
434 distance from it and height above the x-y plane."""
436 def __init__(self
, engine
, distance
, height
, rate
, \
437 center
=(0, 0, 0), up
=(0, 0, 1)):
438 BasicCamera
.__init
__(self
, engine
, None, center
, up
)
439 self
.distance
= distance
446 """Recalculate postion of eye """
447 self
.eye
= self
.distance
*math
.cos(self
.angle
), \
448 self
.distance
*math
.sin(self
.angle
), \
452 """Update calculated position """
453 self
.angle
+= self
.rate
456 elif self
.angle
>= twoPi
:
461 class PrecessingCameraOrtho(OrthoCam
, PrecessingCamera
):
462 """ A precessing camera with Orthographic viewing projection """
466 class MobileCamera(BasicCamera
):
467 """A mobile camera maintains a certain distance (rho) from the
468 origin, but is user controllable.
471 def __init__(self
, engine
, eyeRho
, center
=(0, 0, 0), up
=(0, 0, 1)):
472 """Initialize new camera.
473 @param engine: the engine
474 @param eyeRho: either a constant radius (float), or an eye position
476 @param center: center of view tuple (x,y,z)
478 BasicCamera
.__init
__(self
, engine
, None, center
, up
)
479 if type(eyeRho
) == type((0,)):
480 eyeVec
= la
.Vector(eyeRho
[0]-center
[0], \
481 eyeRho
[1]-center
[1], \
484 phi
= math
.asin(eyeVec
[2]/rho
)
485 r
= rho
* math
.cos(phi
)
486 theta
= math
.acos(eyeVec
[0]/r
)
501 def _setEye(self
, eye
):
502 """ methods to set eye position property. """
504 if type(eye
) == type((1,)):
505 self
.rho
, self
.theta
, self
.phi
= \
506 cartesianToSpherical(eye
[0]-ctr
[0],
512 eye
= property(_getEye
, _setEye
, "Maintain eye position, converting to spherical coords.")
516 there
= sphericalToCartesian(self
.rho
, self
.theta
, self
.phi
)
517 self
._eye
= (there
[0]+ctr
[0],
525 class MobileCameraOrtho(OrthoCam
, MobileCamera
):
526 """ A mobile camera with Orthographic viewing projection """
530 class MobileCameraFrustum(FrustumCam
, MobileCamera
):
531 """ A mobile camera with Frustum viewing projection """
535 class RovingCamera(BasicCamera
):
536 """A roving camera moves around the scene. Used with pedestrian interface. """
538 def __init__(self
, engine
, eye
=(2,2,2), center
=(0,0,0), up
=(0,0,1), elev
=None):
539 """Initialize new camera.
540 @param engine: the engine
541 @param eye: eye position tuple (x,y,z)
542 @param center: center of view type (x,y,z)
543 @param up: up vector tuple (x,y,z)
544 @param elev: (optional) function to give elevation z
545 for any x,y pair. defaults to 0
548 self
.saveCenter
= center
549 BasicCamera
.__init
__(self
, engine
, eye
, center
, up
)
552 self
.rho
= math
.sqrt((center
[0]-eye
[0])**2 +
553 (center
[1]-eye
[1])**2)
554 self
.theta
= math
.acos((center
[0]-eye
[0])/self
.rho
)
555 if eye
[1] > center
[1]:
556 self
.theta
= twoPi
-self
.theta
557 self
.camHeight
= eye
[2] - elev(eye
[0], eye
[1])
558 self
.ctrHeight
= center
[2] - elev(*center
[0:2])
564 """calculate center position from eye pos """
565 ex
, ey
, ez
= self
.eye
566 cx
= self
.rho
* math
.cos(self
.theta
) + ex
567 cy
= self
.rho
* math
.sin(self
.theta
) + ey
568 cz
= self
.elev(cx
, cy
) + self
.ctrHeight
569 self
.center
= (cx
, cy
, cz
)
575 class RovingCameraFrustum(FrustumCam
, RovingCamera
):
576 """ a roving camera with frustum projection """
580 class RovingCameraOrtho(OrthoCam
, RovingCamera
):
581 """ a roving camera with ortho projection """
585 class RovingCameraY(BasicCamera
):
586 """ A roving camera moves around the scene, panning on xz plane. Used with
587 PedestrianInterfaceY.
590 def __init__(self
, engine
, eye
=(2,2,2), center
=(0,0,0), up
=(0,0,1), elev
=None):
592 self
.saveCenter
= center
593 BasicCamera
.__init
__(self
, engine
, eye
, center
, up
)
596 self
.rho
= math
.sqrt((center
[0]-eye
[0])**2 +
597 (center
[2]-eye
[2])**2)
598 self
.theta
= math
.acos((center
[0]-eye
[0])/self
.rho
)
599 if eye
[2] < center
[2]:
600 self
.theta
= twoPi
- self
.theta
601 self
.camHeight
= eye
[1] - elev(eye
[0], eye
[2])
602 self
.ctrHeight
= center
[1] - elev(center
[0], center
[2])
608 """calculate center position from eye pos """
609 ex
, ey
, ez
= self
.eye
610 cx
= self
.rho
* math
.cos(self
.theta
) + ex
611 cz
= self
.rho
* math
.sin(self
.theta
) + ez
612 cy
= self
.elev(cx
, cz
) + self
.ctrHeight
613 self
.center
= (cx
, cy
, cz
)
619 class RovingCameraFrustumY(FrustumCam
, RovingCameraY
):
620 """RovingCameraY with Frustum """
624 class RovingCameraOrthoY(OrthoCam
, RovingCameraY
):
625 """RovingCameraY with Frustum """
629 # Interface ================================================================
631 class Interface(object):
632 """The interface encapsulates all the user interface behavior that
633 an engine can exhibit.
635 @cvar defaultCamera: preferred camera class and default __init__ params
638 defaultCamera
= BasicCameraOrtho
, ((5,5,5),)
640 def __init__(self
, engine
):
641 """Initialize new instance.
642 @param engine: the engine
645 engine
.interface
= self
646 self
.keyMap
= {} # mapping for keys and special keys
647 self
.buttons
= [] # list of buttons currently pressed
648 self
.lastButton
= None # last button pressed
649 self
.mouseMark
= None # last mouse down location
650 self
.visible
= 0 # is window currently visible
651 self
.joystickPosition
= [0,0]
654 """Setup interface after components all exist. """
655 if self
.engine
.camera
and not self
.engine
.cameras
:
656 self
.engine
.addCamera(self
.engine
.camera
)
657 if not self
.engine
.cameras
:
658 functor
, args
= self
.defaultCamera
659 cam
= functor(*((self
.engine
,) + args
))
660 self
.engine
.addCamera(cam
)
661 for c
in self
.engine
.cameras
:
664 def _mouse(self
, button
, state
, loc
):
665 """The mouse event wrapper. Private method: Override
666 mouseDown or mouseUp rather than this.
669 self
.buttons
.append(button
)
670 self
.lastButton
= button
672 self
.mouseDown(button
, loc
)
674 self
.buttons
.remove(button
)
675 self
.lastButton
= None
676 self
.mouseMark
= None
677 self
.mouseUp(button
, loc
)
679 def _motion(self
, loc
):
680 """The mouse motion wrapper. Private method: Override
681 mouseDrag or MouseMove rather than this.
684 self
.mouseDrag(self
.lastButton
, loc
)
688 def _joystickMotion(self
, axis
, val
):
689 """Joystick motion wrapper. Private method. Override
690 joystickMove rather than this.
693 self
.joystickPosition
[axis
] = val
694 self
.joystickMove(self
.joystickPosition
)
696 def _visibility(self
, visible
):
697 """The visibility wrapper."""
698 if visible
!= self
.visible
:
699 self
.visibilityChange(visible
)
700 self
.visible
= visible
702 def _entry(self
, entry
):
703 """The window entry wrapper."""
706 # The following should be overridden by subclasses.
708 def keyPressed(self
, key
):
709 """A key was pressed.
710 @param key: ascii value.
713 if self
.keyMap
.has_key(key
):
714 self
.keyMap
[key
](loc
)
716 def mouseDown(self
, button
, loc
):
717 """The specified mouse button was clicked while the mouse was at
719 @param button: which button was pressed
720 @param loc: x,y tuple with mouse position
722 self
.lastButton
= button
725 def mouseUp(self
, button
, loc
):
726 """The specified mouse button was released while the mouse was at
728 @param button: which button was pressed
729 @param loc: x,y tuple with mouse position
731 self
.lastButton
= None
732 self
.mouseMark
= None
734 def mouseMove(self
, loc
):
735 """The mouse moved with no buttons pressed."""
736 raise NotImplementedError('mouseMove')
738 def mouseDrag(self
, button
, loc
):
739 """The mouse was dragged to the specified location while the given
740 mouse button was held down.
742 raise NotImplementedError('mouseDrag')
744 def joystickMove(self
, pos
):
745 """The joystick moved to pos (0-1, 0-1)."""
746 raise NotImplementedError('joystickMove')
748 def joystickButtonDown(self
, button
):
749 """The specified joystick button was clicked.
750 @param button: which button was pressed
752 raise NotImplementedError('joystickDown')
754 def joystickButtonUp(self
, button
):
755 """The specified joystick button was released.
756 @param button: which button was released
758 raise NotImplementedError('joystickUp')
760 def joystickHatMove(self
, pos
):
761 """The joystick hat moved to pos (0-1, 0-1)."""
762 raise NotImplementedError('joystickHatMove')
764 def entryChange(self
, entered
):
765 """The mouse moved into or out of the window."""
766 raise NotImplementedError('entryChange');
768 def visibilityChange(self
, visible
):
769 """The window had a visibility chnage."""
770 raise NotImplementedError('visibilityChange');
773 class BareBonesInterface(Interface
):
774 """A bare-bones interface supports quitting, and resizing."""
776 def __init__(self
, engine
):
777 Interface
.__init
__(self
, engine
)
778 self
.keyMap
['q'] = self
.quit
779 self
.keyMap
['\033'] = self
.quit
780 self
.keyMap
[K_F12
] = self
.engine
.dumpStats
781 self
.keyMap
['>'] = self
.sizeUp
782 self
.keyMap
['<'] = self
.sizeDown
783 self
.keyMap
['r'] = self
.reset
786 """Handle all interface events (from Pygame events)
787 @param ev: pygame event
789 if ev
.type == VIDEORESIZE
:
791 elif ev
.type == KEYDOWN
:
793 key
= ev
.unicode.encode('ascii')
797 elif ev
.type == KEYUP
:
799 elif ev
.type == MOUSEMOTION
:
801 elif ev
.type == MOUSEBUTTONUP
:
802 self
.mouseUp(ev
.button
, ev
.pos
)
803 elif ev
.type == MOUSEBUTTONDOWN
:
804 self
.mouseDown(ev
.button
, ev
.pos
)
805 elif ev
.type == JOYAXISMOTION
:
806 self
._joystickMotion
(ev
.axis
, ev
.value
)
807 elif ev
.type == JOYBUTTONUP
:
808 self
.joystickButtonUp(ev
.button
)
809 elif ev
.type == JOYBUTTONDOWN
:
810 self
.joystickButtonDown(ev
.button
)
811 elif ev
.type == JOYHATMOTION
:
812 self
.joystickHatMove(ev
.value
)
813 elif ev
.type == QUIT
:
816 def quit(self
, empty
=None): self
.engine
.quit()
817 def reset(self
, empty
=None):
818 """Resets all cameras."""
819 for c
in self
.engine
.cameras
:
822 def sizeUp(self
, empty
):
823 """Increase window size."""
824 self
.engine
.reshape(self
.engine
.width
*2, self
.engine
.height
*2)
826 def sizeDown(self
, empty
):
827 """Decrease window size."""
828 self
.engine
.reshape(self
.engine
.width
/2, self
.engine
.height
/2)
830 def mouseMove(self
, loc
):
831 """The mouse moved with no buttons pressed."""
834 def mouseDrag(self
, button
, loc
):
835 """The mouse was dragged to the specified location while the given
836 mouse button was held down."""
839 def joystickMove(self
, pos
):
840 """The joystick moved to pos (-1-1, -1-1)."""
843 def joystickButtonDown(self
, button
):
844 """The specified joystick button was clicked.
845 @param button: which button was pressed
849 def joystickButtonUp(self
, button
):
850 """The specified joystick button was released.
851 @param button: which button was released
855 def joystickHatMove(self
, pos
):
856 """The joystick hat moved to pos (-1-1, -1-1)."""
859 def entryChange(self
, entered
):
860 """The mouse moved into or out of the window."""
863 def visibilityChange(self
, visible
):
864 """The window had a visibility chnage."""
868 class BasicInterface(BareBonesInterface
):
869 """A basic interface supports quitting, pausing, stepping,
870 zooming, and resizing.
873 def __init__(self
, engine
):
874 BareBonesInterface
.__init
__(self
, engine
)
875 self
.keyMap
[' '] = self
.toggle
876 self
.keyMap
['z'] = self
.zoomIn
877 self
.keyMap
['Z'] = self
.zoomOut
878 self
.keyMap
['\r'] = self
.step
880 def toggle(self
, empty
):
881 """Toggle engine progress on/off. """
882 self
.engine
.runTimer
.toggle()
884 def step(self
, empty
):
885 """Advance engine one step only. """
886 self
.engine
.runTimer
.step()
888 def zoomIn(self
, empty
):
889 """Zoom in on principle camera."""
890 self
.engine
.camera
.zoomIn()
891 self
.engine
.redisplay()
893 def zoomOut(self
, empty
):
894 """Zoom out on principle camera. """
895 self
.engine
.camera
.zoomOut()
896 self
.engine
.redisplay()
899 class PivotingInterface(BasicInterface
):
900 """A pivoting interface supports the basic interfaces and can be
901 pivoted around the center point with the mouse.
903 @cvar sensitivity: how sensitive is view to mouse motion
906 defaultCamera
= MobileCameraOrtho
, (5.0,)
909 def __init__(self
, engine
):
910 BasicInterface
.__init
__(self
, engine
)
912 def mouseDrag(self
, button
, loc
):
913 """Process mouse drag event.
914 @param loc: x,y tuple for new mouse loc
916 cam
= self
.engine
.camera
917 abT
= (1.0*loc
[0]/self
.engine
.width
)*cam
.right
, \
918 (-1.0*loc
[1]/self
.engine
.height
)*cam
.top
919 abS
= (1.0*self
.mouseMark
[0]/self
.engine
.width
)*cam
.right
, \
920 (-1.0*self
.mouseMark
[1]/self
.engine
.height
)*cam
.top
921 deltaX
= -self
.sensitivity
*(abT
[0]-abS
[0])
922 deltaY
= -self
.sensitivity
*(abT
[1]-abS
[1])
923 eyeVec
= la
.Vector(cam
.eye
[0]-cam
.center
[0], \
924 cam
.eye
[1]-cam
.center
[1], \
925 cam
.eye
[2]-cam
.center
[2])
926 upVec
= la
.Vector(*cam
.up
).unit()
927 leftVec
= eyeVec
.cross(upVec
).unit()
928 assert(leftVec
.dot(upVec
) < 0.0001)
929 assert(leftVec
.dot(eyeVec
) < 0.0001)
931 lVec
= leftVec
* deltaX
932 eyeVec
= eyeVec
+ lVec
933 upVec
= leftVec
.cross(eyeVec
).unit()
934 assert(upVec
.dot(leftVec
) < 0.0001)
935 assert(upVec
.dot(eyeVec
) < 0.0001)
937 uVec
= upVec
* deltaY
938 eyeVec
= eyeVec
+ uVec
939 eyeVec
= eyeVec
.unit() * cam
.rho
940 cam
.up
= (upVec
.x
, upVec
.y
, upVec
.z
)
942 # calculate spherical coords
943 cam
.rho
, cam
.theta
, cam
.phi
= \
944 cartesianToSpherical(eyeVec
.x
, eyeVec
.y
, eyeVec
.z
)
947 self
.engine
.redisplay()
950 class PanningInterface(PivotingInterface
):
951 """ A panning interface supports all the pivoting and basic
952 behavior, but the pivoting point can be moved with the keyboard.
954 @cvar increment: how far to move center per keystroke
959 def __init__(self
, engine
):
960 PivotingInterface
.__init
__(self
, engine
)
961 self
.keyMap
['d'] = self
.panPositiveX
962 self
.keyMap
['a'] = self
.panNegativeX
963 self
.keyMap
['w'] = self
.panPositiveY
964 self
.keyMap
['x'] = self
.panNegativeY
965 self
.keyMap
['e'] = self
.panPositiveZ
966 self
.keyMap
['c'] = self
.panNegativeZ
967 # self.keyMap['s'] = self.resetPan
969 def panPositiveX(self
, empty
=None):
970 cam
= self
.engine
.camera
971 x
= cam
.center
[0] + self
.increment
972 cam
.center
= x
, cam
.center
[1], cam
.center
[2]
974 def panNegativeX(self
, empty
=None):
975 cam
= self
.engine
.camera
976 x
= cam
.center
[0] - self
.increment
977 cam
.center
= x
, cam
.center
[1], cam
.center
[2]
979 def panPositiveY(self
, empty
=None):
980 cam
= self
.engine
.camera
981 y
= cam
.center
[1] + self
.increment
982 cam
.center
= cam
.center
[0], y
, cam
.center
[2]
984 def panNegativeY(self
, empty
=None):
985 cam
= self
.engine
.camera
986 y
= cam
.center
[1] - self
.increment
987 cam
.center
= cam
.center
[0], y
, cam
.center
[2]
989 def panPositiveZ(self
, empty
=None):
990 cam
= self
.engine
.camera
991 z
= cam
.center
[2] + self
.increment
992 cam
.center
= cam
.center
[0], cam
.center
[1], z
994 def panNegativeZ(self
, empty
=None):
995 cam
= self
.engine
.camera
996 z
= cam
.center
[2] - self
.increment
997 cam
.center
= cam
.center
[0], cam
.center
[1], z
999 def resetPan(self
, empty
=None):
1000 self
.engine
.camera
.center
= 0.0, 0.0, 0.0
1003 class PedestrianInterface(BasicInterface
):
1004 """A pedestrian interface moves the camera on a roughly horizontal plane.
1005 (constant z). The cursor keys move the viewpoint directly, and the camera
1008 @cvar rotate_inc: how far to rotate camera per keystroke.
1011 defaultCamera
= RovingCameraOrtho
, ()
1016 def __init__(self
, engine
):
1017 BasicInterface
.__init
__(self
, engine
)
1018 self
.stepSize
= self
.engine
.camera
.rho
* self
.increment
1019 self
.keyMap
[' '] = self
.forward
1020 self
.keyMap
['\b'] = self
.backward
1021 self
.keyMap
['l'] = self
.left
1022 self
.keyMap
['r'] = self
.right
1023 self
.keyMap
[K_UP
] = self
.forward
1024 self
.keyMap
[K_DOWN
] = self
.backward
1025 self
.keyMap
[K_LEFT
] = self
.left
1026 self
.keyMap
[K_RIGHT
] = self
.right
1027 key
.set_repeat(500, 30)
1029 def forward(self
, empty
=None):
1030 """Move camera forward."""
1031 self
.moveCam(self
.stepSize
)
1032 self
.engine
.camera
.calc()
1034 def backward(self
, empty
=None):
1035 """Move camera backward."""
1036 self
.moveCam(-self
.stepSize
)
1037 self
.engine
.camera
.calc()
1039 def moveCam(self
, inc
):
1040 cam
= self
.engine
.camera
1041 x
= cam
.eye
[0] + inc
* math
.cos(cam
.theta
)
1042 y
= cam
.eye
[1] + inc
* math
.sin(cam
.theta
)
1043 cam
.eye
= x
, y
, cam
.elev(x
, y
)+cam
.camHeight
1046 def left(self
, empty
=None):
1047 """Rotate camera left."""
1048 cam
= self
.engine
.camera
1049 cam
.theta
+= self
.rotate_inc
1050 if cam
.theta
> twoPi
:
1051 cam
.theta
= twoPi
- cam
.theta
1054 def right(self
, empty
=None):
1055 """Rotate camera right."""
1056 cam
= self
.engine
.camera
1057 cam
.theta
-= self
.rotate_inc
1059 cam
.theta
= twoPi
+ cam
.theta
1063 class PedestrianInterfaceY(PedestrianInterface
):
1064 """ PedestrianInterface for xz plane panning """
1066 def moveCam(self
, inc
):
1067 """Override moveCam to change plane of motion to x,z """
1068 cam
= self
.engine
.camera
1069 x
= cam
.eye
[0] + inc
* math
.cos(cam
.theta
)
1070 z
= cam
.eye
[2] - inc
* math
.sin(cam
.theta
)
1071 cam
.eye
= x
, cam
.elev(x
, z
)+cam
.camHeight
, z
1075 class CursorKeyInterface(PanningInterface
):
1077 """ A cursorkey interface supports all the pivoting and basic
1078 behavior, but the pivoting is done by cursor keys rather than
1084 def __init__(self
, engine
):
1085 PanningInterface
.__init
__(self
, engine
)
1086 self
.keyMap
[K_UP
] = self
.pivotUp
1087 self
.keyMap
[K_DOWN
] = self
.pivotDown
1088 self
.keyMap
[K_LEFT
] = self
.pivotLeft
1089 self
.keyMap
[K_RIGHT
] = self
.pivotRight
1090 self
.mouseLoc
= (0,0)
1091 key
.set_repeat(500, 50)
1093 def pivotUp(self
, empty
=None):
1094 ev
= event
.Event(MOUSEBUTTONDOWN
, pos
=(0,0), button
=1)
1096 ev
= event
.Event(MOUSEMOTION
,
1097 pos
=(self
.mouseLoc
[0],self
.mouseLoc
[1]+self
.increment
),
1101 ev
= event
.Event(MOUSEBUTTONUP
, pos
=(0,0), button
=1)
1104 def pivotDown(self
, empty
=None):
1105 ev
= event
.Event(MOUSEBUTTONDOWN
, pos
=(0,0), button
=1)
1107 ev
= event
.Event(MOUSEMOTION
,
1108 pos
=(self
.mouseLoc
[0],self
.mouseLoc
[1]-self
.increment
),
1112 ev
= event
.Event(MOUSEBUTTONUP
, pos
=(0,0), button
=1)
1115 def pivotLeft(self
, empty
=None):
1116 ev
= event
.Event(MOUSEBUTTONDOWN
, pos
=(0,0), button
=1)
1118 ev
= event
.Event(MOUSEMOTION
,
1119 pos
=(self
.mouseLoc
[0]-self
.increment
,self
.mouseLoc
[1]),
1123 ev
= event
.Event(MOUSEBUTTONUP
, pos
=(0,0), button
=1)
1126 def pivotRight(self
, empty
=None):
1127 ev
= event
.Event(MOUSEBUTTONDOWN
, pos
=(0,0), button
=1)
1129 ev
= event
.Event(MOUSEMOTION
,
1130 pos
=(self
.mouseLoc
[0]+self
.increment
,self
.mouseLoc
[1]),
1134 ev
= event
.Event(MOUSEBUTTONUP
, pos
=(0,0), button
=1)
1138 # Light ======================================
1140 class Light(object):
1142 """Implements each light. This class is concrete, so can be used
1143 to create light instances, but the subclasses may be more intuitive.
1145 You can create any number of lights, but only 8 can be on at a time.
1147 @cvar available_lights: keeps track of which light ids are available
1150 @sort: on, off, display, setPosition
1153 available_lights
= [ ogl
.GL_LIGHT7
, ogl
.GL_LIGHT6
, ogl
.GL_LIGHT5
, ogl
.GL_LIGHT4
,
1154 ogl
.GL_LIGHT3
, ogl
.GL_LIGHT2
, ogl
.GL_LIGHT1
, ogl
.GL_LIGHT0
]
1156 def __init__(self
, ambient
=None, diffuse
=None, specular
=None, position
=None,):
1157 """Initialize new instance.
1158 @param ambient: 4 tuple rgba for 'ambient' light
1159 @param diffuse: 4 tuple rgba for 'diffuse' light
1160 @param specular: 4 tuple rgba for 'specular' (imaged) light
1161 @param position: 4 tuple, x,y,z and opengl-peculiar 4th value
1164 if not ambient
: ambient
= (0.2, 0.2, 0.2, 1.0)
1165 if not diffuse
: diffuse
= (0.6, 0.6, 0.6, 1.0)
1166 if not specular
: specular
= (1.0, 1.0, 1.0, 1.0)
1167 if not position
: position
= (0.0, 0.0, 0.0, 0.0)
1168 self
.ambient
= ambient
1169 self
.diffuse
= diffuse
1170 self
.specular
= specular
1172 Light
.setPosition(self
, position
)
1174 if len(self
.available_lights
) > 0:
1178 """Turn light off on destruction."""
1182 """Turn light on """
1184 self
.id = self
.available_lights
.pop()
1188 """Turn light off """
1190 ogl
.glDisable(self
.id)
1191 self
.available_lights
.append(self
.id)
1195 def setPosition(self
, position
):
1196 """Change position value.
1197 @param position: 4 tuple with x,y,z,?
1199 assert(len(position
) == 4)
1200 self
.position
= position
1203 """Push light params through to OpenGL """
1205 ogl
.glLightfv(self
.id, ogl
.GL_POSITION
, self
.position
)
1207 ogl
.glEnable(self
.id)
1208 ogl
.glLightfv(self
.id, ogl
.GL_AMBIENT
, self
.ambient
)
1209 ogl
.glLightfv(self
.id, ogl
.GL_DIFFUSE
, self
.diffuse
)
1210 ogl
.glLightfv(self
.id, ogl
.GL_SPECULAR
, self
.specular
)
1215 """ updates light """
1220 """Implements light that stays at infinite distance """
1222 def __init__(self
, ambient
=None, diffuse
=None, specular
=None, direction
=None):
1223 """Initialize new instance.
1224 @param ambient: 4 tuple rgba for 'ambient' light
1225 @param diffuse: 4 tuple rgba for 'diffuse' light
1226 @param specular: 4 tuple rgba for 'specular' (imaged) light
1227 @param direction: 3 tuple, x,y,z direction light points
1229 Light
.__init
__(self
, ambient
, diffuse
, specular
, None)
1231 direction
= (0.0, 1.0, 0.0)
1232 self
.setDirection(direction
)
1234 def setPosition(self
, position
):
1235 """Disable setPosition. """
1238 def setDirection(self
, direction
):
1239 """Sets direction light shines. """
1240 assert(len(direction
) == 3)
1241 pos
= [ -1*x
for x
in direction
]
1247 """Implements point light """
1249 def __init__(self
, ambient
=None, diffuse
=None, specular
=None, position
=None,):
1250 """Initialize new instance.
1251 @param ambient: 4 tuple rgba for 'ambient' light
1252 @param diffuse: 4 tuple rgba for 'diffuse' light
1253 @param specular: 4 tuple rgba for 'specular' (imaged) light
1254 @param position: 3 tuple, x,y,z for location of light
1256 Light
.__init
__(self
, ambient
, diffuse
, specular
, None)
1258 position
= (0.0, 0.0, 0.0)
1259 self
.setPosition(position
)
1261 def setPosition(self
,position
):
1262 """Set position of light. """
1263 pos
= [ x
for x
in position
]
1269 """Implements a spotlight, with beam. """
1271 def __init__(self
, cutoff
, direction
, ambient
=None, diffuse
=None,
1272 specular
=None, position
=None,):
1273 """Initialize new instance.
1274 @param cutoff: angle in degrees for 'spot'
1275 @param direction: 3 tuple, x,y,z for direction light points
1276 @param ambient: 4 tuple rgba for 'ambient' light
1277 @param diffuse: 4 tuple rgba for 'diffuse' light
1278 @param specular: 4 tuple rgba for 'specular' (imaged) light
1279 @param position: 3 tuple, x,y,z for location of light
1281 Bulb
.__init
__(self
, ambient
, diffuse
, specular
, position
)
1282 self
.cutoff
= cutoff
1283 self
.setDirection(direction
)
1286 """Pushes light params through to OpenGL """
1288 ogl
.glLightf (self
.id, ogl
.GL_SPOT_CUTOFF
, self
.cutoff
);
1289 ogl
.glLightf (self
.id, ogl
.GL_SPOT_DIRECTION
, self
.direction
);
1292 def setDirection(self
, direction
):
1293 """Set Direction light points """
1294 di
= [ x
for x
in direction
[0:4] ]
1299 # Studio ======================================
1301 class NullStudio(object):
1302 """Manage environmental effects such as lights and fog. """
1303 def __init__(self
, engine
):
1309 def lightsOut(self
):
1310 """Disable all lighting """
1314 """Enable lighting """
1318 """Prep scene for lighting, set light models, colormaterial, etc... """
1321 def displayFixedLights(self
):
1322 """Push all light params through to OpenGL """
1325 def addFixedLight(self
,light
,position
=None):
1326 """Add a light to fixed lights array """
1329 def removeFixedLight(self
,light
):
1330 """Removes light from fixed lights array """
1333 def displayCamLights(self
):
1334 """Push all light params through to OpenGL """
1337 def addCamLight(self
,light
,position
=None):
1338 """Add a light to camera lights array """
1341 def removeCamLight(self
,light
):
1342 """Removes light from camera lights array """
1345 def displayMobileLights(self
):
1346 """Push all light params through to OpenGL """
1349 def addMobileLight(self
, light
, mover
=None, position
=None):
1350 """Add a light to mobile lights array. If mover
1351 provided, studio will call update method of mover
1352 periodically. Same light can be passed as both.
1353 @param light: light to be added
1354 @param mover: typically a 'group' that moves, and contains
1355 the light. Can be same as light.
1356 @param position: x,y,z tuple with initial light position
1360 def removeMobileLight(self
,light
):
1361 """Removes light from mobile lights array """
1364 def depthCueing(self
,status
=True,fmode
=None,
1365 density
=None,near
=None,far
=None):
1366 """Initialize depth cueing.
1367 @param status: is depth cueing enabled?
1368 @param fmode: GL_LINEAR, GL_EXP, or GL_EXP2
1369 @param density: float > 0, ignored for GL_LINEAR mode
1370 @param near: how near the camera does fog start?
1371 @param far: how far from the camera does fog extend?
1379 """Update the Mobile Lights (and whatever else) """
1383 """Commits the Mobile Lights (and whatever else) """
1387 class StudioColorMat(NullStudio
):
1388 """Manage environmental effects such as lights and fog """
1390 def __init__(self
, engine
, ambient
= (0.15, 0.15, 0.15, 1.0)):
1391 """Initialize new instance.
1392 @param engine: the engine
1393 @param ambient: 4 tuple, rgba for ambient light color
1395 self
.lightsFixed
= []
1397 self
.lightsMobile
= []
1398 self
.lightsMotion
= []
1399 self
.depthCue
= None
1400 self
.engine
= engine
1401 self
.ambient
= ambient
1402 self
.dirtyfog
= True
1407 def lightsOut(self
):
1408 """Disable all lighting """
1409 ogl
.glDisable(ogl
.GL_LIGHTING
)
1412 """Enable lighting """
1413 ogl
.glEnable(ogl
.GL_LIGHTING
)
1416 """Prep scene for lighting, set light models, colormaterial, etc... """
1417 if self
.lightsFixed
or self
.lightsCam
or self
.lightsMobile
:
1418 ogl
.glEnable(ogl
.GL_LIGHTING
)
1419 ogl
.glLightModelfv(ogl
.GL_LIGHT_MODEL_AMBIENT
, self
.ambient
)
1420 ogl
.glClearColor(0.0, 0.1, 0.1, 0.0)
1421 ogl
.glEnable(ogl
.GL_DEPTH_TEST
)
1422 ogl
.glShadeModel(ogl
.GL_SMOOTH
)
1423 ogl
.glEnable(ogl
.GL_COLOR_MATERIAL
)
1424 ogl
.glColorMaterial(ogl
.GL_FRONT_AND_BACK
, ogl
.GL_AMBIENT_AND_DIFFUSE
)
1425 if self
.lightsFixed
: # reset all lights to be refreshed
1426 for l
in self
.lightsFixed
:
1427 if l
.id: l
.dirty
= True
1429 for l
in self
.lightsCam
:
1430 if l
.id: l
.dirty
= True
1431 if self
.lightsMobile
:
1432 for l
in self
.lightsMobile
:
1433 if l
.id: l
.dirty
= True
1436 def displayFog(self
):
1437 """Initialize depth cueing. """
1439 ogl
.glFog(ogl
.GL_FOG_MODE
, self
.depthCue
[0])
1440 ogl
.glFog(ogl
.GL_FOG_DENSITY
, self
.depthCue
[1])
1441 ogl
.glFog(ogl
.GL_FOG_START
, self
.depthCue
[2])
1442 ogl
.glFog(ogl
.GL_FOG_END
, self
.depthCue
[3])
1443 ogl
.glEnable(ogl
.GL_FOG
)
1445 ogl
.glDisable(ogl
.GL_FOG
)
1446 self
.dirtyfog
= False
1448 def displayFixedLights(self
):
1449 """Push all light params through to OpenGL """
1451 if self
.lightsFixed
:
1452 ogl
.glMatrixMode(ogl
.GL_MODELVIEW
)
1453 for light
in self
.lightsFixed
:
1456 def addFixedLight(self
,light
,position
=None):
1457 """Add a light to fixed lights array """
1458 self
.lightsFixed
.append(light
)
1459 if position
: light
.setPosition(position
)
1461 def removeFixedLight(self
,light
):
1462 """Removes light from fixed lights array """
1464 self
.lightsFixed
.remove(light
)
1466 def displayCamLights(self
):
1467 """Push all light params through to OpenGL """
1469 ogl
.glMatrixMode(ogl
.GL_MODELVIEW
)
1470 ogl
.glLoadIdentity()
1471 for light
in self
.lightsCam
:
1474 def addCamLight(self
,light
,position
=None):
1475 """Add a light to camera lights array """
1476 self
.lightsCam
.append(light
)
1477 if position
: light
.setPosition(position
)
1479 def removeCamLight(self
,light
):
1480 """Removes light from camera lights array """
1482 self
.lightsCam
.remove(light
)
1484 def displayMobileLights(self
):
1485 """Push all light params through to OpenGL. """
1486 if self
.lightsMotion
:
1487 ogl
.glMatrixMode(ogl
.GL_MODELVIEW
)
1489 for mover
in self
.lightsMotion
:
1493 def addMobileLight(self
,light
,mover
=None,position
=None):
1494 """Add a light to mobile lights array. If mover
1495 provided, studio will call update method of mover
1496 periodically. Same light can be passed as both.
1497 @param light: light to be added
1498 @param mover: typically a 'group' that moves, and contains
1499 the light. Can be same as light.
1500 @param position: x,y,z tuple with initial light position
1502 self
.lightsMobile
.append(light
)
1503 self
.lightsMotion
.append(mover
)
1504 if position
: light
.setPosition(position
)
1506 def removeMobileLight(self
,light
):
1507 """Removes light from mobile lights array. Removes
1508 light and its mover.
1509 @param light: light to be removed
1511 i
= self
.lightsMobile
.index(light
);
1512 del self
.lightsMobile
[i
]
1513 del self
.lightsMotion
[i
]
1515 def depthCueing(self
,status
=True,fmode
=None,
1516 density
=None,near
=None,far
=None):
1517 """Initialize depth cueing.
1518 @param status: is depth cueing enabled?
1519 @param fmode: GL_LINEAR, GL_EXP, or GL_EXP2
1520 @param density: float > 0, ignored for GL_LINEAR mode
1521 @param near: how near the camera does fog start?
1522 @param far: how far from the camera does fog extend?
1525 if not self
.depthCue
:
1527 near
= self
.engine
.camera
.near
1529 far
= self
.engine
.camera
.far
1531 fmode
= ogl
.GL_LINEAR
1534 self
.depthCue
= (fmode
,density
,near
,far
)
1536 self
.depthCue
= None
1537 self
.dirtyfog
= True
1540 """Updates the Mobile Lights (and whatever else). """
1541 if self
.lightsMotion
:
1542 for li
in self
.lightsMotion
:
1549 """Push settings to OpenGL."""
1550 self
.displayMobileLights()
1551 self
.displayCamLights()
1552 self
.displayFixedLights()
1556 # Group #==========================================================================
1558 class Group(Object
):
1560 """A group is an object which holds a collection of other objects.
1561 It displays, updates, and commits all its contained objects in
1564 def __init__(self
, objects
=None):
1565 """Initialize new instance.
1566 @param objects: list of objects to be contained
1568 Object
.__init
__(self
)
1569 self
.objects
= [] # list of objects
1570 self
.commitableObjects
= [] # objects with commit method
1571 self
.updateableObjects
= [] # objects with update
1573 self
.extend(objects
)
1575 def append(self
, obj
):
1576 """Add an object."""
1577 self
.objects
.append(obj
)
1578 if hasattr(obj
,'commit'):
1579 self
.commitableObjects
.append(obj
)
1580 if hasattr(obj
,'update'):
1581 self
.updateableObjects
.append(obj
)
1583 def extend(self
, objects
):
1584 """Add a sequence of objects."""
1588 def remove(self
, obj
):
1589 """Remove an object."""
1590 if self
.objects
.count(obj
):
1591 self
.objects
.remove(obj
)
1592 if self
.commitableObjects
.count(obj
):
1593 self
.commitableObjects
.remove(obj
)
1594 if self
.updateableObjects
.count(obj
):
1595 self
.updateableObjects
.remove(obj
)
1598 """Draw the objects in the group """
1599 for obj
in self
.objects
:
1603 """Update each object in group."""
1604 for obj
in self
.updateableObjects
:
1608 """Commit each object in group. """
1609 for obj
in self
.commitableObjects
:
1613 # TimeKeeper #=====================================================================
1615 class TimeKeeper(Object
):
1616 """Keeps track of time and engine progress.
1617 Also has provisions for calling callback functions/bound-methods
1618 after predetermined delays. see addDelay()
1620 Does not have reference to engine, so same timekeeper class
1621 can track time for entire program, from engine to engine.
1623 @sort: iterate, addDelay, setup, step, toggle
1624 @ivar play: indicates whether update should run this frame,
1625 whether interface hasn't paused engine.
1628 def __init__(self
, fps
=24):
1629 """Initialize new instance.
1630 @param fps: frames per second, defaults to 24
1633 self
.createTime
= time
.time()
1634 self
.startTime
= None
1635 self
.display_interval
= 1.0/fps
# how many seconds between frames
1636 self
.play
= 1 # positive = play, 0 = paused, negative = update count
1637 self
.timePeriod
= 2 # period (s) to update frame rate
1638 self
.frameMark
, self
.timeMark
= 0l, time
.time() # marks for updating
1639 self
.frameRate
= 0.0 # current frame rate
1640 self
.timerEventID
= None
1642 def setup(self
, tid
):
1643 """Startup code to run after other objects are all constructed.
1644 @param tid: pygame event id for timer event.
1646 # adjust delayed timing to correct for engine setup delays
1647 self
.timerEventID
= tid
1649 diff
= time
.time() - self
.createTime
1651 for i
in range(len(self
.delayeds
)):
1652 d0
= self
.delayeds
[i
]
1653 d1
= (d0
[0]+diff
, d0
[1], d0
[2])
1654 self
.delayeds
[i
] = d1
1655 self
.createTime
= None
1657 self
.startTime
= time
.time()
1660 """Turn timer off."""
1661 pytime
.set_timer(self
.timerEventID
, 0) # off
1663 def set_timer(self
):
1664 """Enable timekeeper."""
1665 pytime
.set_timer(self
.timerEventID
, int(self
.display_interval
*1000)) # 1/24
1668 """Update frame measure. Called by engine once per frame."""
1676 elapsed
= now
- self
.timeMark
1677 Object
.runTime
= now
- self
.startTime
1678 if elapsed
>= self
.timePeriod
:
1679 self
.frameRate
= (Object
.runTurn
- self
.frameMark
)/elapsed
1680 self
.frameMark
= Object
.runTurn
1683 def step(self
, count
=2):
1684 """Set to only step the engine one frame."""
1688 """Toggle the engine between playing and pausing."""
1689 self
.play
= not self
.play
1691 def delayManager(self
):
1692 """Exec delayed tasks whose time has arrived, and maintain queue. """
1694 while len(self
.delayeds
) and self
.delayeds
[0] and self
.delayeds
[0][0] <= now
:
1695 temp
= self
.delayeds
.pop(0)
1696 func
, args
= temp
[1:]
1700 def addDelay(self
, delay
, func
, args
):
1701 """Adds a function to be be executed 'delay' ms later, with
1702 arguments 'args'. These callbacks are synchronized with the
1703 framerate, so that each is called at the first frame after delay
1704 is complete; Timing granularity depends on frame rate.
1706 @param delay: how many milliseconds to delay execution
1707 @param func: function to be called
1708 @param args: tuple of args to be passed to func when called
1710 tgt_time
= time
.time() + delay
/1000.0
1711 self
.delayeds
.append(tuple([tgt_time
,func
,args
]))
1712 self
.delayeds
.sort(lambda x
,y
: cmp(x
[0],y
[0]))
1715 # Engine #=====================================================================
1718 class EngineSuperShutdownException(Exception):
1719 """Exception that exits engine, and gets reraised. Can be used
1720 to signal program stop, in addition to engine stop.
1724 class EngineShutdownException(Exception):
1725 """Exception can be raised anywhere to end engine immediately. """
1728 class EngineReSetupException(Exception):
1729 """Exception can be raised anywhere to rerun engine setup code.
1730 raise whenever a new camera, studio or interface is installed while engine
1735 class EngineReInitException(Exception):
1736 """Exception can be raised anywhere to rerun engine initialization and setup code.
1737 raise whenever basic video config options change (basically window size) while
1743 class Engine(object):
1745 """The engine manages the window, handles high-level displaying
1748 defaultInterface
= PanningInterface
, ()
1749 defaultStudio
= NullStudio
, ()
1751 def __init__(self
, video
=(320,320)):
1752 self
.title
= 'spyre'
1753 self
.width
, self
.height
= video
1754 self
.mode
= OPENGL | DOUBLEBUF
1755 self
.background
= (0,0,0,0)
1757 self
.runTimer
= None
1758 self
.objects
= Group()
1759 self
.add
= self
.objects
.append
1760 self
.remove
= self
.objects
.remove
1761 self
.extend
= self
.objects
.extend
1764 self
.interface
= None
1767 Object
.engine
= self
1770 """Initialize video and other systems """
1772 display
.set_mode((self
.width
, self
.height
), self
.mode
)
1773 # regenerate objects that depend on opengl internal state
1774 for obj
in Object
.opengl_state_dependent
:
1780 """After all engines are done, call this. """
1784 """Setup the camera and interface if they're unspecified. """
1785 if self
.interface
is None:
1786 functor
, args
= self
.defaultInterface
1787 self
.interface
= functor(*((self
,) + args
))
1788 self
.interface
.setup()
1789 if self
.studio
is None:
1790 functor
, args
= self
.defaultStudio
1791 self
.studio
= functor(*((self
,) + args
))
1793 self
.studio
.displayCamLights()
1794 if self
.runTimer
is None:
1795 self
.runTimer
= TimeKeeper()
1796 self
.runTimer
.setup(REDRAW_TIMER
)
1797 Object
.engine
= self
1800 """Handle termination chores. """
1801 self
.runTimer
.shutdown()
1803 def addCamera(self
, cam
):
1804 """Add a camera (and viewport) to engine. """
1805 while self
.cameras
.count(cam
):
1806 self
.cameras
.remove(cam
)
1807 self
.cameras
.append(cam
)
1811 def removeCamera(self
, cam
):
1812 """Remove a camera (and viewport) from engine. """
1813 while self
.cameras
.count(cam
):
1814 self
.cameras
.remove(cam
)
1815 if self
.camera
== cam
:
1817 self
.camera
= self
.cameras
[0]
1822 """Update all objects, both studio and display."""
1823 self
.studio
.update()
1824 self
.objects
.update()
1827 """Commit all objects, both studio and display."""
1828 self
.studio
.commit()
1829 self
.objects
.commit()
1833 # enable scissor testing if multiple viewports
1835 if len(self
.cameras
) > 1:
1836 ogl
.glEnable(ogl
.GL_SCISSOR_TEST
)
1838 ogl
.glDisable(ogl
.GL_SCISSOR_TEST
)
1839 # for each camera, clear background and draw
1840 for cam
in self
.cameras
:
1841 cam
.displayViewport()
1842 cam
.displayBackground()
1846 self
.studio
.displayFixedLights()
1848 self
.studio
.displayMobileLights()
1849 cam
.objects
.display()
1850 # flush all pending drawing
1855 def redisplay(self
):
1856 """Register a redisplay."""
1860 """Clear the display."""
1861 ogl
.glViewport(0, 0, self
.width
, self
.height
)
1862 ogl
.glDisable(ogl
.GL_SCISSOR_TEST
)
1863 ogl
.glDisable(ogl
.GL_BLEND
)
1864 bits
= ogl
.GL_DEPTH_BUFFER_BIT
1866 ogl
.glClearColor(*self
.background
)
1867 bits |
= ogl
.GL_COLOR_BUFFER_BIT
1871 """Run update as apropos."""
1872 self
.redisplay() # default behaviour - necessary?
1873 if self
.runTimer
.play
:
1878 """Runs when the engine is idle."""
1883 def reshape(self
, width
, height
):
1884 """Resize the window."""
1885 self
.width
, self
.height
= width
, height
1887 raise EngineReInitException
1889 def event(self
, ev
):
1890 """Handles all user defined events"""
1893 def quit(self
, *args
):
1894 """ Mark the engine as shutting down. """
1895 raise EngineShutdownException
1897 def getAbsPos(self
, pos
):
1898 """Return absolute position of relative position 'pos'."""
1899 assert(ogl
.glGetIntegerv(ogl
.GL_MATRIX_MODE
) == ogl
.GL_MODELVIEW
)
1900 winPos
= oglu
.gluProject(*pos
)
1901 assert type(winPos
) is types
.TupleType
, type(winPos
)
1903 ogl
.glLoadIdentity()
1904 self
.camera
.viewMV()
1905 absPos
= oglu
.gluUnProject(*winPos
)
1906 assert type(absPos
) is types
.TupleType
, type(absPos
)
1911 """Set everything up and then execute the main loop."""
1914 if not hasattr(self
, 'window'):
1920 # run setup, then start main event loop
1921 # typically, this loop never repeats, but
1922 # is exited on first pass by EngineShutdownException
1926 self
.studio
.display()
1930 idler
= self
.idle().next
1931 except AttributeError:
1935 engevent
= self
.event
1936 maintainer
= self
.maintain
1937 displayer
= self
.display
1938 timer
= self
.runTimer
.iterate
1939 iface
= self
.interface
1940 ifevent
= iface
.event
1946 if ev
.type == NOEVENT
:
1949 elif ev
.type == ACTIVEEVENT
:
1953 elif ev
.type == REDRAW_TIMER
:
1954 event
.get(REDRAW_TIMER
) # clear queue
1955 #self.runTimer.iterate()
1962 elif ev
.type < USEREVENT
:
1963 #self.interface.event(ev)
1969 # if reSetup exception thrown, exit event loop
1971 except EngineReSetupException
:
1974 # if reInit exception is thrown, return to top of go()
1975 # and reinitialize and resetup everything
1976 except EngineReInitException
:
1979 # if shutdown exception thrown, run shutdown code, and
1981 except EngineShutdownException
:
1985 def dumpStats(self
, empty
=None):
1986 """Dump engine statistics. """
1987 cam
= self
.cameras
[0]
1988 print 'engine status '
1989 print '---------------'
1990 print ' camera eye '
1991 print ' ( %.4f %.4f %.4f ) ' % cam
.eye
1992 print ' camera center '
1993 print ' ( %.4f %.4f %.4f ) ' % cam
.center
1994 print ' camera viewbox bounds (l/r) '
1995 print ' ( %.4f %.4f ) ' % (cam
.left
, cam
.right
)
1996 print ' camera viewbox bounds (t/b) '
1997 print ' ( %.4f %.4f ) ' % (cam
.top
, cam
.bottom
)
1998 print ' camera viewbox bounds (n/f) '
1999 print ' ( %.4f %.4f ) ' % (cam
.near
, cam
.far
)
2002 class EngineFullScreen(Engine
):
2003 """Generic engine, but full screen. """
2005 defaultInterface
= PanningInterface
, ()
2006 defaultStudio
= NullStudio
, ()
2008 def __init__(self
, video
=(800, 600)):
2009 Engine
.__init
__(self
, video
)
2010 self
.mode
= self
.mode | FULLSCREEN
2012 def reshape(self
, width
, height
):
2013 """Disable reshape method. """