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:
parent
0356a358b3
commit
d8e4911fcd
@ -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")
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user