"""trackball_camera.py - An OpenGL Trackball Camera Class for Pyglet by Roger Allen, July 2008 roger@rogerandwendy.com A class for simple-minded 3d example apps. Usage: Initialize with a radius from the center/focus point: tbcam = TrackballCamera(5.0) After adjusting your projection matrix, set the modelview matrix. tbcam.update_modelview() On each primary mouse click, scale the x & y to [-1,1] and call: tbcam.mouse_roll(x,y,False) On each primary mouse drag, scale the x & y to [-1,1] and call: tbcam.mouse_roll(x,y) Mouse movements adjust the modelview projection matrix directly. """ __version__ = "1.0" # Code derived from the GLUT trackball.c, but now quite different and # customized for pyglet. # # I simply wanted an easy-to-use trackball camera for quick-n-dirty # opengl programs that I'd like to write. Finding none, I grabbed # the trackball.c code & started hacking. # # Originally implemented by Gavin Bell, lots of ideas from Thant Tessman # and the August '88 issue of Siggraph's "Computer Graphics," pp. 121-129. # and David M. Ciemiewicz, Mark Grossman, Henry Moreton, and Paul Haeberli # # Note: See the following for more information on quaternions: # # - Shoemake, K., Animating rotation with quaternion curves, Computer # Graphics 19, No 3 (Proc. SIGGRAPH'85), 245-254, 1985. # - Pletinckx, D., Quaternion calculus as a basic tool in computer # graphics, The Visual Computer 5, 2-13, 1989. # # Gavin Bell's code had this copyright notice: # (c) Copyright 1993, 1994, Silicon Graphics, Inc. # ALL RIGHTS RESERVED # Permission to use, copy, modify, and distribute this software for # any purpose and without fee is hereby granted, provided that the above # copyright notice appear in all copies and that both the copyright notice # and this permission notice appear in supporting documentation, and that # the name of Silicon Graphics, Inc. not be used in advertising # or publicity pertaining to distribution of the software without specific, # written prior permission. # # THE MATERIAL EMBODIED ON THIS SOFTWARE IS PROVIDED TO YOU "AS-IS" # AND WITHOUT WARRANTY OF ANY KIND, EXPRESS, IMPLIED OR OTHERWISE, # INCLUDING WITHOUT LIMITATION, ANY WARRANTY OF MERCHANTABILITY OR # FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT SHALL SILICON # GRAPHICS, INC. BE LIABLE TO YOU OR ANYONE ELSE FOR ANY DIRECT, # SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY # KIND, OR ANY DAMAGES WHATSOEVER, INCLUDING WITHOUT LIMITATION, # LOSS OF PROFIT, LOSS OF USE, SAVINGS OR REVENUE, OR THE CLAIMS OF # THIRD PARTIES, WHETHER OR NOT SILICON GRAPHICS, INC. HAS BEEN # ADVISED OF THE POSSIBILITY OF SUCH LOSS, HOWEVER CAUSED AND ON # ANY THEORY OF LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE # POSSESSION, USE OR PERFORMANCE OF THIS SOFTWARE. # # US Government Users Restricted Rights # Use, duplication, or disclosure by the Government is subject to # restrictions set forth in FAR 52.227.19(c)(2) or subparagraph # (c)(1)(ii) of the Rights in Technical Data and Computer Software # clause at DFARS 252.227-7013 and/or in similar or successor # clauses in the FAR or the DOD or NASA FAR Supplement. # Unpublished-- rights reserved under the copyright laws of the # United States. Contractor/manufacturer is Silicon Graphics, # Inc., 2011 N. Shoreline Blvd., Mountain View, CA 94039-7311. import math import copy from pyglet.gl import * # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # a little vector library that is misused in odd ways below. def v3add(src1, src2): return [ src1[0] + src2[0], src1[1] + src2[1], src1[2] + src2[2] ] def v3sub(src1, src2): return [ src1[0] - src2[0], src1[1] - src2[1], src1[2] - src2[2] ] def v3scale(v, scale): return [ v[0] * scale, v[1] * scale, v[2] * scale ] def v3dot(v1, v2): return v1[0]*v2[0] + v1[1]*v2[1] + v1[2]*v2[2] def v3cross(v1, v2): return [ (v1[1] * v2[2]) - (v1[2] * v2[1]), (v1[2] * v2[0]) - (v1[0] * v2[2]), (v1[0] * v2[1]) - (v1[1] * v2[0]) ] def v3length(v): return math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) def v3normalize(v): try: tmp = v3scale(v,1.0/v3length(v)) return tmp except ZeroDivisionError: return v # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Some quaternion routines def q_add(q1, q2): """Given two quaternions, add them together to get a third quaternion. Adding quaternions to get a compound rotation is analagous to adding translations to get a compound translation. When incrementally adding rotations, the first argument here should be the new rotation. """ t1 = v3scale(q1,q2[3]) t2 = v3scale(q2,q1[3]) t3 = v3cross(q2,q1) tf = v3add(t1,t2) tf = v3add(t3,tf) tf.append( q1[3] * q2[3] - v3dot(q1,q2) ) return tf # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def q_from_axis_angle(a, phi): # a is a 3-vector, q is a 4-vector """Computes a quaternion based on an axis (defined by the given vector) and an angle about which to rotate. The angle is expressed in radians. """ q = v3normalize(a) q = v3scale(q, math.sin(phi/2.0)) q.append(math.cos(phi/2.0)) return q # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def q_normalize(q): """Return a normalized quaternion""" mag = (q[0]*q[0] + q[1]*q[1] + q[2]*q[2] + q[3]*q[3]) if mag != 0: for i in range(4): q[i] /= mag; return q # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def q_matrix(q): """return the rotation matrix based on q""" m = [0.0]*16 m[0*4+0] = 1.0 - 2.0 * (q[1] * q[1] + q[2] * q[2]) m[0*4+1] = 2.0 * (q[0] * q[1] - q[2] * q[3]) m[0*4+2] = 2.0 * (q[2] * q[0] + q[1] * q[3]) m[0*4+3] = 0.0 m[1*4+0] = 2.0 * (q[0] * q[1] + q[2] * q[3]) m[1*4+1] = 1.0 - 2.0 * (q[2] * q[2] + q[0] * q[0]) m[1*4+2] = 2.0 * (q[1] * q[2] - q[0] * q[3]) m[1*4+3] = 0.0 m[2*4+0] = 2.0 * (q[2] * q[0] - q[1] * q[3]) m[2*4+1] = 2.0 * (q[1] * q[2] + q[0] * q[3]) m[2*4+2] = 1.0 - 2.0 * (q[1] * q[1] + q[0] * q[0]) m[2*4+3] = 0.0 m[3*4+0] = 0.0 m[3*4+1] = 0.0 m[3*4+2] = 0.0 m[3*4+3] = 1.0 return m # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def project_z(r, x, y): """Project an x,y pair onto a sphere of radius r OR a hyperbolic sheet if we are away from the center of the sphere. """ d = math.sqrt(x*x + y*y) if (d < r * 0.70710678118654752440): # Inside sphere z = math.sqrt(r*r - d*d) else: # On hyperbola t = r / 1.41421356237309504880 z = t*t / d return z # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # Trackball Camera Class # class TrackballCamera: # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def __init__(self, radius=1.0): """ initialize the camera, giving a radius from the focal point for the camera eye. Update focal point & up via the update_modelview call. """ # the quaternion storing the rotation self.rot_quat = [0,0,0,1] # the last mouse update self.last_x = None self.last_y = None # camera vars self.cam_eye = [0.,0.,radius] self.cam_focus = [0.,0.,0.] self.cam_up = [0.,1.,0.] # in add_quat routine, renormalize "sometimes" self.RENORMCOUNT = 97 self.count = 0 # Trackballsize should really be based on the distance from the center of # rotation to the point on the object underneath the mouse. That # point would then track the mouse as closely as possible. This is a # simple example, though, so that is left as an Exercise for the # Programmer. self.TRACKBALLSIZE = 0.8 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def mouse_roll(self, norm_mouse_x, norm_mouse_y, dragging=True): """When you click or drag the primary mouse button, scale the mouse x & y to the range [-1.0,1.0] and call this routine to roll the trackball and update the modelview matrix. The initial click should set dragging to False. """ if dragging: norm_mouse_quat = self._rotate(norm_mouse_x, norm_mouse_y) self.rot_quat = q_add(norm_mouse_quat,self.rot_quat) self.count += 1 if (self.count > self.RENORMCOUNT): self.rot_quat = q_normalize(self.rot_quat) self.count = 0 self.update_modelview() self.last_x = norm_mouse_x self.last_y = norm_mouse_y # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def mouse_zoom(self, norm_mouse_x, norm_mouse_y, dragging=True): """When you click or drag a secondary mouse button, scale the mouse x & y to the range [-1.0,1.0] and call this routine to change the trackball's camera radius and update the modelview matrix. The initial click should set dragging to False. """ if self.last_x: dx = norm_mouse_x - self.last_x dy = norm_mouse_y - self.last_y norm_mouse_r_delta = 20.0*math.sqrt(dx*dx+dy*dy) if dy > 0.0: norm_mouse_r_delta = -norm_mouse_r_delta if dragging: self.cam_eye[2] = self.cam_eye[2] + norm_mouse_r_delta if self.cam_eye[2] < 1.0: self.cam_eye[2] == 1.0 self.update_modelview() self.last_x = norm_mouse_x self.last_y = norm_mouse_y # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def update_modelview(self,cam_radius=None,cam_focus=None,cam_up=None): """Given a radius for the trackball camera, a focus-point 3-vector, another 3-vector the points 'up' combined with the current orientation of the trackball, update the GL_MODELVIEW matrix. """ if cam_radius: self.cam_eye[2] = cam_radius if cam_focus: self.cam_focus = cam_focus if cam_up: self.cam_up = cam_up glMatrixMode(GL_MODELVIEW) glLoadIdentity() gluLookAt( self.cam_eye[0],self.cam_eye[1],self.cam_eye[2], self.cam_focus[0],self.cam_focus[1],self.cam_focus[2], self.cam_up[0],self.cam_up[1],self.cam_up[2] ) # rotate this view by the current orientation m = self._matrix() mm = (GLfloat * len(m))(*m) # FIXME there is prob a better way... glMultMatrixf(mm) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def _matrix(self): """return the rotation matrix for the trackball""" return q_matrix(self.rot_quat) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def _rotate(self, norm_mouse_x, norm_mouse_y): """Pass the x and y coordinates of the last and current positions of the mouse, scaled so they are in the range [-1.0,1.0]. Simulate a track-ball. Project the points onto the virtual trackball, then figure out the axis of rotation, which is the cross product of LAST NEW and O LAST (O is the center of the ball, 0,0,0) Note: This is a deformed trackball-- is a trackball in the center, but is deformed into a hyperbolic sheet of rotation away from the center. This particular function was chosen after trying out several variations. """ # handle special case if (self.last_x == norm_mouse_x and self.last_y == norm_mouse_y): # Zero rotation return [ 0.0, 0.0, 0.0, 1.0] # # First, figure out z-coordinates for projection of P1 and P2 to # deformed sphere # last = [self.last_x, self.last_y, project_z(self.TRACKBALLSIZE,self.last_x,self.last_y)] new = [norm_mouse_x, norm_mouse_y, project_z(self.TRACKBALLSIZE,norm_mouse_x,norm_mouse_y)] # # Now, we want the cross product of LAST and NEW # aka the axis of rotation # a = v3cross(new,last) # # Figure out how much to rotate around that axis (phi) # d = v3sub(last,new) t = v3length(d) / (2.0*self.TRACKBALLSIZE) # Avoid problems with out-of-control values... if (t > 1.0): t = 1.0 if (t < -1.0): t = -1.0 phi = 2.0 * math.asin(t) return q_from_axis_angle(a,phi)