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
|
ShareWidget = None
|
||||||
|
|
||||||
class GLViewWidget(QtOpenGL.QGLWidget):
|
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
|
- Rotation/scale controls
|
||||||
- Axis/grid display
|
- Axis/grid display
|
||||||
- Export options
|
- Export options
|
||||||
|
|
||||||
High-DPI displays: Qt5 should automatically detect the correct resolution.
|
================ ==============================================================
|
||||||
For Qt4, specify the ``devicePixelRatio`` argument when initializing the
|
**Arguments:**
|
||||||
widget (usually this value is 1-2).
|
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
|
||||||
def __init__(self, parent=None, devicePixelRatio=None):
|
``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
|
global ShareWidget
|
||||||
|
|
||||||
if ShareWidget is None:
|
if ShareWidget is None:
|
||||||
|
@ -31,8 +39,21 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
||||||
QtOpenGL.QGLWidget.__init__(self, parent, ShareWidget)
|
QtOpenGL.QGLWidget.__init__(self, parent, ShareWidget)
|
||||||
|
|
||||||
self.setFocusPolicy(QtCore.Qt.ClickFocus)
|
self.setFocusPolicy(QtCore.Qt.ClickFocus)
|
||||||
|
|
||||||
|
if rotationMethod not in {"euler", "quaternion"}:
|
||||||
|
raise RuntimeError("Rotation method should be either 'euler' or 'quaternion'")
|
||||||
|
|
||||||
self.opts = {
|
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.reset()
|
||||||
self.items = []
|
self.items = []
|
||||||
|
@ -162,8 +183,12 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
||||||
def viewMatrix(self):
|
def viewMatrix(self):
|
||||||
tr = QtGui.QMatrix4x4()
|
tr = QtGui.QMatrix4x4()
|
||||||
tr.translate( 0.0, 0.0, -self.opts['distance'])
|
tr.translate( 0.0, 0.0, -self.opts['distance'])
|
||||||
tr.rotate(self.opts['elevation']-90, 1, 0, 0)
|
if self.opts['rotationMethod'] == 'quaternion':
|
||||||
tr.rotate(self.opts['azimuth']+90, 0, 0, -1)
|
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']
|
center = self.opts['center']
|
||||||
tr.translate(-center.x(), -center.y(), -center.z())
|
tr.translate(-center.x(), -center.y(), -center.z())
|
||||||
return tr
|
return tr
|
||||||
|
@ -251,36 +276,52 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
||||||
glMatrixMode(GL_MODELVIEW)
|
glMatrixMode(GL_MODELVIEW)
|
||||||
glPopMatrix()
|
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:
|
if pos is not None:
|
||||||
self.opts['center'] = pos
|
self.opts['center'] = pos
|
||||||
if distance is not None:
|
if distance is not None:
|
||||||
self.opts['distance'] = distance
|
self.opts['distance'] = distance
|
||||||
if elevation is not None:
|
if rotation is not None:
|
||||||
self.opts['elevation'] = elevation
|
# set with quaternion
|
||||||
if azimuth is not None:
|
self.opts['rotation'] = rotation
|
||||||
self.opts['azimuth'] = azimuth
|
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()
|
self.update()
|
||||||
|
|
||||||
def cameraPosition(self):
|
def cameraPosition(self):
|
||||||
"""Return current position of camera based on center, dist, elevation, and azimuth"""
|
"""Return current position of camera based on center, dist, elevation, and azimuth"""
|
||||||
center = self.opts['center']
|
center = self.opts['center']
|
||||||
dist = self.opts['distance']
|
dist = self.opts['distance']
|
||||||
elev = self.opts['elevation'] * np.pi/180.
|
if self.opts['rotationMethod'] == "quaternion":
|
||||||
azim = self.opts['azimuth'] * np.pi/180.
|
pos = center - self.opts['rotation'].rotatedVector( Vector(0,0,dist) )
|
||||||
|
else:
|
||||||
pos = Vector(
|
# using 'euler' rotation method
|
||||||
center.x() + dist * np.cos(elev) * np.cos(azim),
|
elev = self.opts['elevation'] * np.pi / 180
|
||||||
center.y() + dist * np.cos(elev) * np.sin(azim),
|
azim = self.opts['azimuth'] * np.pi / 180
|
||||||
center.z() + dist * np.sin(elev)
|
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
|
return pos
|
||||||
|
|
||||||
def orbit(self, azim, elev):
|
def orbit(self, azim, elev):
|
||||||
"""Orbits the camera around the center position. *azim* and *elev* are given in degrees."""
|
"""Orbits the camera around the center position. *azim* and *elev* are given in degrees."""
|
||||||
self.opts['azimuth'] += azim
|
if self.opts['rotationMethod'] == 'quaternion':
|
||||||
self.opts['elevation'] = np.clip(self.opts['elevation'] + elev, -90, 90)
|
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()
|
self.update()
|
||||||
|
|
||||||
def pan(self, dx, dy, dz, relative='global'):
|
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
|
self.opts['center'] = self.opts['center'] + xVec * xScale * dx + yVec * xScale * dy + zVec * xScale * dz
|
||||||
elif relative == 'view':
|
elif relative == 'view':
|
||||||
# pan in plane of camera
|
# pan in plane of camera
|
||||||
elev = np.radians(self.opts['elevation'])
|
|
||||||
azim = np.radians(self.opts['azimuth'])
|
if self.opts['rotationMethod'] == 'quaternion':
|
||||||
fov = np.radians(self.opts['fov'])
|
# obtain basis vectors
|
||||||
dist = (self.opts['center'] - self.cameraPosition()).length()
|
qc = self.opts['rotation'].conjugated()
|
||||||
fov_factor = np.tan(fov / 2) * 2
|
xv = qc.rotatedVector( Vector(1,0,0) )
|
||||||
scale_factor = dist * fov_factor / self.width()
|
yv = qc.rotatedVector( Vector(0,1,0) )
|
||||||
z = scale_factor * np.cos(elev) * dy
|
zv = qc.rotatedVector( Vector(0,0,1) )
|
||||||
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)
|
scale_factor = self.pixelSize( self.opts['center'] )
|
||||||
self.opts['center'] += QtGui.QVector3D(x, -y, z)
|
|
||||||
|
# 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:
|
else:
|
||||||
raise ValueError("relative argument must be global, view, or view-upright")
|
raise ValueError("relative argument must be global, view, or view-upright")
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user