Some work.
[krufty_fps.git] / spyre.py
blob214efd6c6c746ffddf89c94d0275dbe4754ad78b
1 #!/usr/local/bin/python
2 """
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.
8 Engine::
9 Includes main loop, handles event queue, passing input data to
10 interface, maintaining viewport and tracking and displaying
11 objects.
13 Interface::
14 Controls the primary camera, the engine, and the timekeeper in
15 response to user input.
17 Camera::
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.
22 Studio::
23 Maintains lights, including fixed, mobile, and camera-mounted types;
24 and depth-cueing (fog) and some materials details.
26 TimeKeeper::
27 Controls pace of engine run, can switch engine advancement on and
28 off. Also controls and monitors framerate.
30 Object::
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
36 tested code.
37 =========================
38 import spyre
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
44 def display(self):
45 quad = oglu.gluNewQuadric()
46 oglu.gluSphere(quad, self.rad, 60, 60)
48 sph = Sphere()
50 engine = Engine()
51 engine.add(sph)
53 engine.go()
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
65 @undocumented: Rect
66 """
68 __program__ = 'SPyRE'
69 __version__ = '0.7.1'
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'
73 __license__ = 'LGPL'
76 import math
77 import sys
78 import types
79 import time
81 import la
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.
90 pi = math.pi
91 twoPi = 2*pi
92 piOverTwo = pi/2
93 piOverOneEighty = pi/180
94 radiansToDegrees = 1/piOverOneEighty
95 degreesToRadians = piOverOneEighty
96 sqrtTwo = math.sqrt(2)
98 # events
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)
108 assert(y/r <= 1)
109 assert(y/r >= -1)
110 if x == 0:
111 theta = math.asin(y/r)
112 else:
113 theta = math.atan(y/x)
114 if (x < 0):
115 theta += pi
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)
127 return x, y, z
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
139 'commit' methods.
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
149 engine = None
151 # list of opengl state dependant objects
152 opengl_state_dependent = []
154 # these class vars track engine runtime (fictional)
155 # and turn counts
156 # these are typically maintained/manipulated by a runTimer object
158 runTime = 0.0
159 runTurn = 0
161 def __init__(self):
162 """Abstract initializer."""
163 if self.__class__ is Object:
164 raise NotImplementedError
166 def display(self):
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.
185 def viewProj(self):
186 """Pushes ortho params/settings through to OpenGL """
187 ogl.glMatrixMode(ogl.GL_PROJECTION)
188 ogl.glLoadIdentity()
189 ogl.glOrtho(self.left, self.right,
190 self.bottom, self.top,
191 self.near, self.far)
193 def viewMV(self):
194 """Sets eye and center position in OpenGL """
195 ogl.glMatrixMode(ogl.GL_MODELVIEW)
196 ogl.glLoadIdentity()
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.
209 if near != None:
210 self.near = near
211 if far != None:
212 self.far = far
213 self.left = left
214 self.right = right
215 self.bottom = bottom
216 self.top = top
218 ortho = shape
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.
228 def viewProj(self):
229 """Pushes frustum params/settings through to OpenGL """
230 ogl.glMatrixMode(ogl.GL_PROJECTION)
231 ogl.glLoadIdentity()
232 ogl.glFrustum(self.left, self.right,
233 self.bottom, self.top,
234 self.near, self.far)
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)
247 ymin = -ymax
248 xmin = ymin * aspect
249 xmax = ymax * aspect
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
264 size of the window.
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
270 displayable objects.
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)"""
289 self.engine = engine
290 self.left = left
291 self.right = right
292 self.bottom = bottom
293 self.top = top
294 self.near = near
295 self.far = far
296 self._eye = eye
297 self._center = center
298 self.up = up
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
303 self.saveInit()
304 self.dirty = True
306 def setup(self):
307 """Overridable for things to be done before use, but after engine
308 and interface are initialized.
310 self.setViewport(*self.fractViewport)
312 def saveInit(self):
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,
317 self.near, self.far)
319 def restoreInit(self):
320 """Restore previously saved initialization state."""
321 stuff = self.__saveinit
322 self._eye = stuff[0]
323 self._center = stuff[1]
324 self.left, self.right, self.bottom, self.top, self.near, \
325 self.far = stuff[2:]
327 def _getCtr(self):
328 """ methods to access center position property """
329 return self._center
331 def _setCtr(self, cntr):
332 self._center = cntr
333 self.dirty = True
335 center = property(_getCtr, _setCtr, None, "Track center position")
337 def _getEye(self):
338 """ methods to access eye position property """
339 return self._eye
341 def _setEye(self, eye):
342 self._eye = eye
343 self.dirty = True
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.
352 self.left /= factor
353 self.right /= factor
354 self.top /= factor
355 self.bottom /= factor
356 self.dirty = True
358 def zoomOut(self, factor=sqrtTwo):
359 """Zoom out. Widens the viewing box.
361 @param factor: ratio of new width to old width.
363 self.left *= factor
364 self.right *= factor
365 self.top *= factor
366 self.bottom *= factor
367 self.dirty = True
369 def refresh(self):
370 """Refresh the position of the camera."""
371 pass
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
380 if self.background:
381 ogl.glClearColor(*self.background)
382 bits |= ogl.GL_COLOR_BUFFER_BIT
383 ogl.glClear(bits)
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)
421 self.saveInit()
424 class BasicCameraFrustum(FrustumCam, BasicCamera):
425 pass
428 class BasicCameraOrtho(OrthoCam, BasicCamera):
429 pass
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
440 self.height = height
441 self.rate = rate
442 self.angle = 0.0
443 self.calc()
445 def calc(self):
446 """Recalculate postion of eye """
447 self.eye = self.distance*math.cos(self.angle), \
448 self.distance*math.sin(self.angle), \
449 self.height
451 def refresh(self):
452 """Update calculated position """
453 self.angle += self.rate
454 if self.angle < 0:
455 self.angle += twoPi
456 elif self.angle >= twoPi:
457 self.angle -= twoPi
458 self.calc()
461 class PrecessingCameraOrtho(OrthoCam, PrecessingCamera):
462 """ A precessing camera with Orthographic viewing projection """
463 pass
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
475 tuple (x,y,z)
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], \
482 eyeRho[2]-center[2])
483 rho = eyeVec.norm()
484 phi = math.asin(eyeVec[2]/rho)
485 r = rho * math.cos(phi)
486 theta = math.acos(eyeVec[0]/r)
487 else:
488 rho = eyeRho
489 theta = 0
490 phi = 0
491 self.rho = rho
492 self.theta = theta
493 self.phi = phi
494 self.center = center
495 self.calc()
496 self.saveInit()
498 def _getEye(self):
499 return self._eye
501 def _setEye(self, eye):
502 """ methods to set eye position property. """
503 ctr = self.center
504 if type(eye) == type((1,)):
505 self.rho, self.theta, self.phi = \
506 cartesianToSpherical(eye[0]-ctr[0],
507 eye[1]-ctr[1],
508 eye[2]-ctr[2],)
509 self.calc()
510 self.dirty = True
512 eye = property(_getEye, _setEye, "Maintain eye position, converting to spherical coords.")
514 def calc(self):
515 ctr = self.center
516 there = sphericalToCartesian(self.rho, self.theta, self.phi)
517 self._eye = (there[0]+ctr[0],
518 there[1]+ctr[1],
519 there[2]+ctr[2])
521 def refresh(self):
522 self.calc()
525 class MobileCameraOrtho(OrthoCam, MobileCamera):
526 """ A mobile camera with Orthographic viewing projection """
527 pass
530 class MobileCameraFrustum(FrustumCam, MobileCamera):
531 """ A mobile camera with Frustum viewing projection """
532 pass
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
547 self.saveEye = eye
548 self.saveCenter = center
549 BasicCamera.__init__(self, engine, eye, center, up)
550 if elev == None:
551 elev = lambda x,y: 0
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])
559 self.elev = elev
560 self.calc()
561 self.saveInit()
563 def calc(self):
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)
571 def refresh(self):
572 self.calc()
575 class RovingCameraFrustum(FrustumCam, RovingCamera):
576 """ a roving camera with frustum projection """
577 pass
580 class RovingCameraOrtho(OrthoCam, RovingCamera):
581 """ a roving camera with ortho projection """
582 pass
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):
591 self.saveEye = eye
592 self.saveCenter = center
593 BasicCamera.__init__(self, engine, eye, center, up)
594 if elev == None:
595 elev = lambda x,y: 0
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])
603 self.elev = elev
604 self.calc()
605 self.saveInit()
607 def calc(self):
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)
615 def refresh(self):
616 self.calc()
619 class RovingCameraFrustumY(FrustumCam, RovingCameraY):
620 """RovingCameraY with Frustum """
621 pass
624 class RovingCameraOrthoY(OrthoCam, RovingCameraY):
625 """RovingCameraY with Frustum """
626 pass
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
644 self.engine = 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]
653 def setup(self):
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:
662 c.setup()
664 def _mouse(self, button, state, loc):
665 """The mouse event wrapper. Private method: Override
666 mouseDown or mouseUp rather than this.
668 if state:
669 self.buttons.append(button)
670 self.lastButton = button
671 self.mouseMark = loc
672 self.mouseDown(button, loc)
673 else:
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.
683 if self.lastButton:
684 self.mouseDrag(self.lastButton, loc)
685 else:
686 self.mouseMove(loc)
688 def _joystickMotion(self, axis, val):
689 """Joystick motion wrapper. Private method. Override
690 joystickMove rather than this.
692 if axis > 1: return
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."""
704 pass
706 # The following should be overridden by subclasses.
708 def keyPressed(self, key):
709 """A key was pressed.
710 @param key: ascii value.
712 loc = self.mouseMark
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
718 the given location.
719 @param button: which button was pressed
720 @param loc: x,y tuple with mouse position
722 self.lastButton = button
723 self.mouseMark = loc
725 def mouseUp(self, button, loc):
726 """The specified mouse button was released while the mouse was at
727 the given location.
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
785 def event(self, ev):
786 """Handle all interface events (from Pygame events)
787 @param ev: pygame event
789 if ev.type == VIDEORESIZE:
790 pass
791 elif ev.type == KEYDOWN:
792 if ev.key < 256:
793 key = ev.unicode.encode('ascii')
794 else:
795 key = ev.key
796 self.keyPressed(key)
797 elif ev.type == KEYUP:
798 pass
799 elif ev.type == MOUSEMOTION:
800 self._motion(ev.pos)
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:
814 self.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:
820 c.restoreInit()
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."""
832 pass
834 def mouseDrag(self, button, loc):
835 """The mouse was dragged to the specified location while the given
836 mouse button was held down."""
837 pass
839 def joystickMove(self, pos):
840 """The joystick moved to pos (-1-1, -1-1)."""
841 pass
843 def joystickButtonDown(self, button):
844 """The specified joystick button was clicked.
845 @param button: which button was pressed
847 pass
849 def joystickButtonUp(self, button):
850 """The specified joystick button was released.
851 @param button: which button was released
853 pass
855 def joystickHatMove(self, pos):
856 """The joystick hat moved to pos (-1-1, -1-1)."""
857 pass
859 def entryChange(self, entered):
860 """The mouse moved into or out of the window."""
861 pass
863 def visibilityChange(self, visible):
864 """The window had a visibility chnage."""
865 pass
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,)
907 sensitivity = 2
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)
930 if deltaX:
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)
936 if deltaY:
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)
945 self.mouseMark = loc
946 cam.refresh()
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
957 increment = 0.1
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
1006 indirectly.
1008 @cvar rotate_inc: how far to rotate camera per keystroke.
1011 defaultCamera = RovingCameraOrtho, ()
1013 increment = 0.05
1014 rotate_inc = 0.02
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
1044 cam.calc()
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
1052 cam.calc()
1054 def right(self, empty=None):
1055 """Rotate camera right."""
1056 cam = self.engine.camera
1057 cam.theta -= self.rotate_inc
1058 if cam.theta < 0:
1059 cam.theta = twoPi + cam.theta
1060 cam.calc()
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
1072 cam.calc()
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
1079 by mouse.
1082 increment = 10
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)
1095 event.post(ev)
1096 ev = event.Event(MOUSEMOTION,
1097 pos=(self.mouseLoc[0],self.mouseLoc[1]+self.increment),
1098 rel=(0,0),
1099 buttons=(1))
1100 event.post(ev)
1101 ev = event.Event(MOUSEBUTTONUP, pos=(0,0), button=1)
1102 event.post(ev)
1104 def pivotDown(self, empty=None):
1105 ev = event.Event(MOUSEBUTTONDOWN, pos=(0,0), button=1)
1106 event.post(ev)
1107 ev = event.Event(MOUSEMOTION,
1108 pos=(self.mouseLoc[0],self.mouseLoc[1]-self.increment),
1109 rel=(0,0),
1110 buttons=(1))
1111 event.post(ev)
1112 ev = event.Event(MOUSEBUTTONUP, pos=(0,0), button=1)
1113 event.post(ev)
1115 def pivotLeft(self, empty=None):
1116 ev = event.Event(MOUSEBUTTONDOWN, pos=(0,0), button=1)
1117 event.post(ev)
1118 ev = event.Event(MOUSEMOTION,
1119 pos=(self.mouseLoc[0]-self.increment,self.mouseLoc[1]),
1120 rel=(0,0),
1121 buttons=(1))
1122 event.post(ev)
1123 ev = event.Event(MOUSEBUTTONUP, pos=(0,0), button=1)
1124 event.post(ev)
1126 def pivotRight(self, empty=None):
1127 ev = event.Event(MOUSEBUTTONDOWN, pos=(0,0), button=1)
1128 event.post(ev)
1129 ev = event.Event(MOUSEMOTION,
1130 pos=(self.mouseLoc[0]+self.increment,self.mouseLoc[1]),
1131 rel=(0,0),
1132 buttons=(1))
1133 event.post(ev)
1134 ev = event.Event(MOUSEBUTTONUP, pos=(0,0), button=1)
1135 event.post(ev)
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
1148 for use.
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
1163 self.id = None
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
1171 ## attenuation?
1172 Light.setPosition(self, position)
1173 self.dirty = False
1174 if len(self.available_lights) > 0:
1175 self.on()
1177 def __del__(self):
1178 """Turn light off on destruction."""
1179 self.off()
1181 def on(self):
1182 """Turn light on """
1183 if not self.id:
1184 self.id = self.available_lights.pop()
1185 self.dirty = True
1187 def off(self):
1188 """Turn light off """
1189 if self.id:
1190 ogl.glDisable(self.id)
1191 self.available_lights.append(self.id)
1192 self.id = None
1193 self.dirty = False
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
1202 def display(self):
1203 """Push light params through to OpenGL """
1204 if self.id:
1205 ogl.glLightfv(self.id, ogl.GL_POSITION, self.position)
1206 if self.dirty:
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)
1211 # attenuation ?
1212 self.dirty = False
1214 def update(self):
1215 """ updates light """
1216 pass
1219 class Sun(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)
1230 if not direction:
1231 direction = (0.0, 1.0, 0.0)
1232 self.setDirection(direction)
1234 def setPosition(self, position):
1235 """Disable setPosition. """
1236 pass
1238 def setDirection(self, direction):
1239 """Sets direction light shines. """
1240 assert(len(direction) == 3)
1241 pos = [ -1*x for x in direction ]
1242 pos.append(1.0)
1243 self.position = pos
1246 class Bulb(Light):
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)
1257 if not position:
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 ]
1264 pos.append(0.0)
1265 self.position = pos
1268 class Spot(Bulb):
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)
1285 def display(self):
1286 """Pushes light params through to OpenGL """
1287 if self.id:
1288 ogl.glLightf (self.id, ogl.GL_SPOT_CUTOFF, self.cutoff);
1289 ogl.glLightf (self.id, ogl.GL_SPOT_DIRECTION, self.direction);
1290 Bulb.display(self)
1292 def setDirection(self, direction):
1293 """Set Direction light points """
1294 di = [ x for x in direction[0:4] ]
1295 di.append(1.0)
1296 self.direction = di
1299 # Studio ======================================
1301 class NullStudio(object):
1302 """Manage environmental effects such as lights and fog. """
1303 def __init__(self, engine):
1304 pass
1306 def setup(self):
1307 pass
1309 def lightsOut(self):
1310 """Disable all lighting """
1311 pass
1313 def lightsOK(self):
1314 """Enable lighting """
1315 pass
1317 def init(self):
1318 """Prep scene for lighting, set light models, colormaterial, etc... """
1319 pass
1321 def displayFixedLights(self):
1322 """Push all light params through to OpenGL """
1323 pass
1325 def addFixedLight(self,light,position=None):
1326 """Add a light to fixed lights array """
1327 pass
1329 def removeFixedLight(self,light):
1330 """Removes light from fixed lights array """
1331 pass
1333 def displayCamLights(self):
1334 """Push all light params through to OpenGL """
1335 pass
1337 def addCamLight(self,light,position=None):
1338 """Add a light to camera lights array """
1339 pass
1341 def removeCamLight(self,light):
1342 """Removes light from camera lights array """
1343 pass
1345 def displayMobileLights(self):
1346 """Push all light params through to OpenGL """
1347 pass
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
1358 pass
1360 def removeMobileLight(self,light):
1361 """Removes light from mobile lights array """
1362 pass
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?
1373 pass
1375 def display(self):
1376 pass
1378 def update(self):
1379 """Update the Mobile Lights (and whatever else) """
1380 pass
1382 def commit(self):
1383 """Commits the Mobile Lights (and whatever else) """
1384 pass
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 = []
1396 self.lightsCam = []
1397 self.lightsMobile = []
1398 self.lightsMotion = []
1399 self.depthCue = None
1400 self.engine = engine
1401 self.ambient = ambient
1402 self.dirtyfog = True
1404 def init(self):
1405 pass
1407 def lightsOut(self):
1408 """Disable all lighting """
1409 ogl.glDisable(ogl.GL_LIGHTING)
1411 def lightsOK(self):
1412 """Enable lighting """
1413 ogl.glEnable(ogl.GL_LIGHTING)
1415 def setup(self):
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
1428 if self.lightsCam:
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
1434 self.displayFog();
1436 def displayFog(self):
1437 """Initialize depth cueing. """
1438 if self.depthCue:
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)
1444 else:
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:
1454 light.display()
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 """
1463 light.off()
1464 self.lightsFixed.remove(light)
1466 def displayCamLights(self):
1467 """Push all light params through to OpenGL """
1468 if self.lightsCam:
1469 ogl.glMatrixMode(ogl.GL_MODELVIEW)
1470 ogl.glLoadIdentity()
1471 for light in self.lightsCam:
1472 light.display()
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 """
1481 light.off()
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)
1488 ogl.glPushMatrix()
1489 for mover in self.lightsMotion:
1490 mover.display()
1491 ogl.glPopMatrix()
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?
1524 if status:
1525 if not self.depthCue:
1526 if near == None:
1527 near = self.engine.camera.near
1528 if far == None:
1529 far = self.engine.camera.far
1530 if fmode == None:
1531 fmode = ogl.GL_LINEAR
1532 if density == None:
1533 density = 1
1534 self.depthCue = (fmode,density,near,far)
1535 else:
1536 self.depthCue = None
1537 self.dirtyfog = True
1539 def update(self):
1540 """Updates the Mobile Lights (and whatever else). """
1541 if self.lightsMotion:
1542 for li in self.lightsMotion:
1543 if li:
1544 li.update()
1545 if self.dirtyfog:
1546 self.displayFog()
1548 def display(self):
1549 """Push settings to OpenGL."""
1550 self.displayMobileLights()
1551 self.displayCamLights()
1552 self.displayFixedLights()
1553 self.dirty = False
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
1562 sequence."""
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
1572 if objects:
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."""
1585 for obj in objects:
1586 self.append(obj)
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)
1597 def display(self):
1598 """Draw the objects in the group """
1599 for obj in self.objects:
1600 obj.display()
1602 def update(self):
1603 """Update each object in group."""
1604 for obj in self.updateableObjects:
1605 obj.update()
1607 def commit(self):
1608 """Commit each object in group. """
1609 for obj in self.commitableObjects:
1610 obj.commit()
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
1632 self.delayeds = []
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
1648 if self.createTime:
1649 diff = time.time() - self.createTime
1650 assert(diff>=0)
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
1656 self.set_timer()
1657 self.startTime = time.time()
1659 def shutdown(self):
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
1667 def iterate(self):
1668 """Update frame measure. Called by engine once per frame."""
1669 if self.play < 0:
1670 self.play += 1
1671 if self.delayeds:
1672 self.delayManager()
1673 if self.play:
1674 Object.runTurn += 1
1675 now = time.time()
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
1681 self.timeMark = now
1683 def step(self, count=2):
1684 """Set to only step the engine one frame."""
1685 self.play = -count
1687 def toggle(self):
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. """
1693 now = time.time()
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:]
1697 func(*args)
1698 now = time.time()
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.
1722 pass
1724 class EngineShutdownException(Exception):
1725 """Exception can be raised anywhere to end engine immediately. """
1726 pass
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
1731 is running.
1733 pass
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
1738 engine is running.
1740 pass
1743 class Engine(object):
1745 """The engine manages the window, handles high-level displaying
1746 and updating."""
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
1762 self.cameras = []
1763 self.camera = None
1764 self.interface = None
1765 self.studio = None
1766 self.dirty = True
1767 Object.engine = self
1769 def init(self):
1770 """Initialize video and other systems """
1771 self.window = \
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:
1775 obj.regenerate()
1776 if self.studio:
1777 self.studio.init()
1779 def unInit(self):
1780 """After all engines are done, call this. """
1781 pass
1783 def setup(self):
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))
1792 self.studio.setup()
1793 self.studio.displayCamLights()
1794 if self.runTimer is None:
1795 self.runTimer = TimeKeeper()
1796 self.runTimer.setup(REDRAW_TIMER)
1797 Object.engine = self
1799 def shutdown(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)
1808 if not self.camera:
1809 self.camera = 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:
1816 if self.cameras:
1817 self.camera = self.cameras[0]
1818 else:
1819 self.camera = None
1821 def update(self):
1822 """Update all objects, both studio and display."""
1823 self.studio.update()
1824 self.objects.update()
1826 def commit(self):
1827 """Commit all objects, both studio and display."""
1828 self.studio.commit()
1829 self.objects.commit()
1831 def display(self):
1832 """Display."""
1833 # enable scissor testing if multiple viewports
1834 self.clear()
1835 if len(self.cameras) > 1:
1836 ogl.glEnable(ogl.GL_SCISSOR_TEST)
1837 else:
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()
1843 cam.viewProj()
1844 cam.viewMV()
1845 if cam.dirty:
1846 self.studio.displayFixedLights()
1847 cam.dirty = False
1848 self.studio.displayMobileLights()
1849 cam.objects.display()
1850 # flush all pending drawing
1851 ogl.glFlush()
1852 display.flip()
1853 self.dirty = False
1855 def redisplay(self):
1856 """Register a redisplay."""
1857 self.dirty = True
1859 def clear(self):
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
1865 if self.background:
1866 ogl.glClearColor(*self.background)
1867 bits |= ogl.GL_COLOR_BUFFER_BIT
1868 ogl.glClear(bits)
1870 def maintain(self):
1871 """Run update as apropos."""
1872 self.redisplay() # default behaviour - necessary?
1873 if self.runTimer.play:
1874 self.update()
1875 self.commit()
1877 def idle(self):
1878 """Runs when the engine is idle."""
1879 while 1:
1880 pytime.wait(10)
1881 yield None
1883 def reshape(self, width, height):
1884 """Resize the window."""
1885 self.width, self.height = width, height
1886 self.redisplay()
1887 raise EngineReInitException
1889 def event(self, ev):
1890 """Handles all user defined events"""
1891 pass
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)
1902 ogl.glPushMatrix()
1903 ogl.glLoadIdentity()
1904 self.camera.viewMV()
1905 absPos = oglu.gluUnProject(*winPos)
1906 assert type(absPos) is types.TupleType, type(absPos)
1907 ogl.glPopMatrix()
1908 return absPos
1910 def go(self):
1911 """Set everything up and then execute the main loop."""
1912 try:
1913 while 1:
1914 if not hasattr(self, 'window'):
1915 init()
1916 self.init()
1918 try:
1919 while 1:
1920 # run setup, then start main event loop
1921 # typically, this loop never repeats, but
1922 # is exited on first pass by EngineShutdownException
1923 self.setup()
1924 self.update()
1925 self.commit()
1926 self.studio.display()
1927 self.display()
1929 try:
1930 idler = self.idle().next
1931 except AttributeError:
1932 idler = self.idle
1934 # optimization
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
1942 try:
1943 # main event loop
1944 while 1:
1945 ev = event.poll()
1946 if ev.type == NOEVENT:
1947 #self.idle()
1948 idler()
1949 elif ev.type == ACTIVEEVENT:
1950 if (ev.state):
1951 #self.redisplay()
1952 self.dirty = True
1953 elif ev.type == REDRAW_TIMER:
1954 event.get(REDRAW_TIMER) # clear queue
1955 #self.runTimer.iterate()
1956 timer()
1957 #self.maintain()
1958 maintainer()
1959 if self.dirty:
1960 # self.display()
1961 displayer()
1962 elif ev.type < USEREVENT:
1963 #self.interface.event(ev)
1964 ifevent(ev)
1965 else:
1966 #self.event(ev)
1967 engevent(ev)
1969 # if reSetup exception thrown, exit event loop
1970 # to setup loop
1971 except EngineReSetupException:
1972 pass
1974 # if reInit exception is thrown, return to top of go()
1975 # and reinitialize and resetup everything
1976 except EngineReInitException:
1977 pass
1979 # if shutdown exception thrown, run shutdown code, and
1980 # exit runloop
1981 except EngineShutdownException:
1982 self.shutdown()
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. """
2014 pass