Fixed GLViewWidget Gimbal Lock with Quaternion Parametrization (#909)

* Fixed GLViewWidget Gimbal Lock with Quaternion Parametrization

* fixed rotation name

* fixes extraneous pos argument and restore compatibility

* remove redundant getAxisAngle() in rotation (for Qt4 compatibility)

* GLViewWidget takes rotationMethod parameter

* Avoid merge conflict

* Fix docstring

Co-authored-by: Ogi Moore <ognyan.moore@gmail.com>
This commit is contained in:
Yoonyoung (Jamie) Cho 2020-12-25 00:45:02 -05:00 committed by GitHub
parent 0356a358b3
commit d8e4911fcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -10,18 +10,26 @@ from .. import functions as fn
ShareWidget = None
class GLViewWidget(QtOpenGL.QGLWidget):
"""
Basic widget for displaying 3D data
def __init__(self, parent=None, devicePixelRatio=None, rotationMethod='euler'):
"""
Basic widget for displaying 3D data
- Rotation/scale controls
- Axis/grid display
- Export options
High-DPI displays: Qt5 should automatically detect the correct resolution.
For Qt4, specify the ``devicePixelRatio`` argument when initializing the
widget (usually this value is 1-2).
"""
def __init__(self, parent=None, devicePixelRatio=None):
================ ==============================================================
**Arguments:**
parent (QObject, optional): Parent QObject. Defaults to None.
devicePixelRatio (float, optional): High-DPI displays Qt5 should automatically
detect the correct resolution. For Qt4, specify the
``devicePixelRatio`` argument when initializing the widget
(usually this value is 1-2). Defaults to None.
rotationMethod (str): Mechanimsm to drive the rotation method, options are
'euler' and 'quaternion'. Defaults to 'euler'.
================ ==============================================================
"""
global ShareWidget
if ShareWidget is None:
@ -31,8 +39,21 @@ class GLViewWidget(QtOpenGL.QGLWidget):
QtOpenGL.QGLWidget.__init__(self, parent, ShareWidget)
self.setFocusPolicy(QtCore.Qt.ClickFocus)
if rotationMethod not in {"euler", "quaternion"}:
raise RuntimeError("Rotation method should be either 'euler' or 'quaternion'")
self.opts = {
'devicePixelRatio': devicePixelRatio
'center': Vector(0,0,0), ## will always appear at the center of the widget
'rotation' : QtGui.QQuaternion(1,0,0,0), ## camera rotation (quaternion:wxyz)
'distance': 10.0, ## distance of camera from center
'fov': 60, ## horizontal field of view in degrees
'elevation': 30, ## camera's angle of elevation in degrees
'azimuth': 45, ## camera's azimuthal angle in degrees
## (rotation around z-axis 0 points along x-axis)
'viewport': None, ## glViewport params; None == whole widget
'devicePixelRatio': devicePixelRatio,
'rotationMethod': rotationMethod
}
self.reset()
self.items = []
@ -162,8 +183,12 @@ class GLViewWidget(QtOpenGL.QGLWidget):
def viewMatrix(self):
tr = QtGui.QMatrix4x4()
tr.translate( 0.0, 0.0, -self.opts['distance'])
tr.rotate(self.opts['elevation']-90, 1, 0, 0)
tr.rotate(self.opts['azimuth']+90, 0, 0, -1)
if self.opts['rotationMethod'] == 'quaternion':
tr.rotate(self.opts['rotation'])
else:
# default rotation method
tr.rotate(self.opts['elevation']-90, 1, 0, 0)
tr.rotate(self.opts['azimuth']+90, 0, 0, -1)
center = self.opts['center']
tr.translate(-center.x(), -center.y(), -center.z())
return tr
@ -251,36 +276,52 @@ class GLViewWidget(QtOpenGL.QGLWidget):
glMatrixMode(GL_MODELVIEW)
glPopMatrix()
def setCameraPosition(self, pos=None, distance=None, elevation=None, azimuth=None):
def setCameraPosition(self, pos=None, distance=None, elevation=None, azimuth=None, rotation=None):
if pos is not None:
self.opts['center'] = pos
if distance is not None:
self.opts['distance'] = distance
if elevation is not None:
self.opts['elevation'] = elevation
if azimuth is not None:
self.opts['azimuth'] = azimuth
if rotation is not None:
# set with quaternion
self.opts['rotation'] = rotation
else:
# set with elevation-azimuth, restored for compatibility
eu = self.opts['rotation'].toEulerAngles()
if azimuth is not None:
eu.setZ(-azimuth-90)
if elevation is not None:
eu.setX(elevation-90)
self.opts['rotation'] = QtGui.QQuaternion.fromEulerAngles(eu)
self.update()
def cameraPosition(self):
"""Return current position of camera based on center, dist, elevation, and azimuth"""
center = self.opts['center']
dist = self.opts['distance']
elev = self.opts['elevation'] * np.pi/180.
azim = self.opts['azimuth'] * np.pi/180.
pos = Vector(
center.x() + dist * np.cos(elev) * np.cos(azim),
center.y() + dist * np.cos(elev) * np.sin(azim),
center.z() + dist * np.sin(elev)
)
if self.opts['rotationMethod'] == "quaternion":
pos = center - self.opts['rotation'].rotatedVector( Vector(0,0,dist) )
else:
# using 'euler' rotation method
elev = self.opts['elevation'] * np.pi / 180
azim = self.opts['azimuth'] * np.pi / 180
pos = Vector(
center.x() + dist * np.cos(elev) * np.cos(azim),
center.y() + dist * np.cos(elev) * np.sin(azim),
center.z() + dist * np.sin(elev)
)
return pos
def orbit(self, azim, elev):
"""Orbits the camera around the center position. *azim* and *elev* are given in degrees."""
self.opts['azimuth'] += azim
self.opts['elevation'] = np.clip(self.opts['elevation'] + elev, -90, 90)
if self.opts['rotationMethod'] == 'quaternion':
q = QtGui.QQuaternion.fromEulerAngles(
elev, -azim, 0
) # rx-ry-rz
q *= self.opts['rotation']
self.opts['rotation'] = q
else: # default euler rotation method
self.opts['azimuth'] += azim
self.opts['elevation'] = np.clip(self.opts['elevation'] + elev, -90, 90)
self.update()
def pan(self, dx, dy, dz, relative='global'):
@ -326,16 +367,29 @@ class GLViewWidget(QtOpenGL.QGLWidget):
self.opts['center'] = self.opts['center'] + xVec * xScale * dx + yVec * xScale * dy + zVec * xScale * dz
elif relative == 'view':
# pan in plane of camera
elev = np.radians(self.opts['elevation'])
azim = np.radians(self.opts['azimuth'])
fov = np.radians(self.opts['fov'])
dist = (self.opts['center'] - self.cameraPosition()).length()
fov_factor = np.tan(fov / 2) * 2
scale_factor = dist * fov_factor / self.width()
z = scale_factor * np.cos(elev) * dy
x = scale_factor * (np.sin(azim) * dx - np.sin(elev) * np.cos(azim) * dy)
y = scale_factor * (np.cos(azim) * dx + np.sin(elev) * np.sin(azim) * dy)
self.opts['center'] += QtGui.QVector3D(x, -y, z)
if self.opts['rotationMethod'] == 'quaternion':
# obtain basis vectors
qc = self.opts['rotation'].conjugated()
xv = qc.rotatedVector( Vector(1,0,0) )
yv = qc.rotatedVector( Vector(0,1,0) )
zv = qc.rotatedVector( Vector(0,0,1) )
scale_factor = self.pixelSize( self.opts['center'] )
# apply translation
self.opts['center'] += scale_factor * (xv*-dx + yv*dy + zv*dz)
else: # use default euler rotation method
elev = np.radians(self.opts['elevation'])
azim = np.radians(self.opts['azimuth'])
fov = np.radians(self.opts['fov'])
dist = (self.opts['center'] - self.camerPosition()).length()
fov_factor = np.tan(fov / 2) * 2
scale_factor = dist * fov_factor / self.width()
z = scale_factor * np.cos(elev) * dy
x = scale_factor * (np.sin(azim) * dx - np.sin(elev) * np.cos(azim) * dy)
y = scale_factor * (np.cos(azim) * dx + np.sin(elev) * np.sin(azim) * dy)
self.opts['center'] += QtGui.QVector3D(x, -y, z)
else:
raise ValueError("relative argument must be global, view, or view-upright")