|
| 1 | +/* This file is part of COVISE. |
| 2 | +
|
| 3 | + You can use it under the terms of the GNU Lesser General Public License |
| 4 | + version 2.1 or later, see lgpl-2.1.txt. |
| 5 | +
|
| 6 | + * License: LGPL 2+ */ |
| 7 | + |
| 8 | +#include "TeleportNavigationProvider.h" |
| 9 | +#include <OpenVRUI/coInteractionManager.h> |
| 10 | +#include <cmath> |
| 11 | +#include <cover/VRSceneGraph.h> |
| 12 | +#include <cover/coIntersection.h> |
| 13 | +#include <cover/coVRFileManager.h> |
| 14 | +#include <cover/coVRNavigationManager.h> |
| 15 | +#include <cover/coVRPluginSupport.h> |
| 16 | +#include <osg/MatrixTransform> |
| 17 | +#include <osg/PolygonOffset> |
| 18 | +#include <osg/Switch> |
| 19 | +#include <osg/Vec3> |
| 20 | +#include <osg/Matrix> |
| 21 | + |
| 22 | +using namespace opencover; |
| 23 | + |
| 24 | +static osg::Vec3 X = osg::Vec3(1, 0, 0); |
| 25 | +static osg::Vec3 UP = osg::Vec3(0, 0, 1); |
| 26 | + |
| 27 | +TeleportNavigationProvider::TeleportNavigationProvider() |
| 28 | + : coVRNavigationProvider("Teleport", nullptr) |
| 29 | + , interactionPoint(vrui::coInteraction::ButtonA, "ProbeMode", vrui::coInteraction::Navigation) |
| 30 | + , interactionTurn(vrui::coInteraction::ButtonB, "ProbeMode", vrui::coInteraction::Navigation) |
| 31 | + , triggerMouse(vrui::coInteraction::ButtonA, "MouseTeleport") |
| 32 | + , triggerWheel(vrui::coInteraction::Wheel, "MouseScroll") |
| 33 | +{ |
| 34 | + triggerMouse.setGroup(vrui::coInteraction::GroupNavigation); |
| 35 | + |
| 36 | + switch_ = new osg::Switch; |
| 37 | + cover->getObjectsRoot()->addChild(switch_); |
| 38 | + |
| 39 | + transform = new osg::MatrixTransform; |
| 40 | + switch_->addChild(transform); |
| 41 | + |
| 42 | + icon = coVRFileManager::instance()->loadFile("share/covise/icons/teleport_target.glb", nullptr, transform); |
| 43 | + |
| 44 | + setVisible(false); |
| 45 | +} |
| 46 | + |
| 47 | +TeleportNavigationProvider::~TeleportNavigationProvider() |
| 48 | +{ |
| 49 | +} |
| 50 | + |
| 51 | +void TeleportNavigationProvider::setEnabled(bool enabled) |
| 52 | +{ |
| 53 | + if (isEnabled() == enabled) |
| 54 | + return; |
| 55 | + |
| 56 | + coVRNavigationProvider::setEnabled(enabled); |
| 57 | + coIntersection::instance()->isectAllNodes(enabled); |
| 58 | + |
| 59 | + if (enabled) |
| 60 | + { |
| 61 | + vrui::coInteractionManager::the()->registerInteraction(&interactionPoint); |
| 62 | + vrui::coInteractionManager::the()->registerInteraction(&interactionTurn); |
| 63 | + vrui::coInteractionManager::the()->registerInteraction(&triggerMouse); |
| 64 | + vrui::coInteractionManager::the()->registerInteraction(&triggerWheel); |
| 65 | + } |
| 66 | + else |
| 67 | + { |
| 68 | + vrui::coInteractionManager::the()->unregisterInteraction(&interactionPoint); |
| 69 | + vrui::coInteractionManager::the()->unregisterInteraction(&interactionTurn); |
| 70 | + vrui::coInteractionManager::the()->unregisterInteraction(&triggerMouse); |
| 71 | + vrui::coInteractionManager::the()->unregisterInteraction(&triggerWheel); |
| 72 | + } |
| 73 | +} |
| 74 | + |
| 75 | +/** |
| 76 | + * Computes where to place the objects, based on where we want to teleport to. |
| 77 | + */ |
| 78 | +osg::Matrix computeNewObjectsTransform(const osg::Matrix &targetTransform, bool includeViewer = false) |
| 79 | +{ |
| 80 | + // We'll need this later. |
| 81 | + auto scaleTransform = cover->getObjectsScale()->getMatrix(); |
| 82 | + |
| 83 | + // The `referenceTransform` is location of the viewer's feet in the stage. |
| 84 | + // We use about 1.5m eye height for now. |
| 85 | + auto referenceTransform = osg::Matrix::scale(1000, 1000, 1000) * osg::Matrix::translate(0, 0, -1500); |
| 86 | + |
| 87 | + // If `includeViewer` is true, the viewer matrix is included in the |
| 88 | + // reference transform, which is useful when navigating with the mouse on a |
| 89 | + // 2D screen, for it places the camera, not the stage origin, into the |
| 90 | + // target location. In VR, we want to align the stage origin with the |
| 91 | + // teleport target (such that head-tracked orientation and offset are |
| 92 | + // ignored), so we do not include the viewer matrix. |
| 93 | + if (includeViewer) |
| 94 | + { |
| 95 | + referenceTransform = referenceTransform * cover->getViewerMat(); |
| 96 | + } |
| 97 | + |
| 98 | + // This equation aligns the "reference transform" (viewer's feet) with the |
| 99 | + // global targetTransform (normal left-to-right matrix order). |
| 100 | + // |
| 101 | + // ReferenceTransform = newObjectsTransform * scaleTransform * targetTransform |
| 102 | + // |
| 103 | + // We rearrange it to compute only the new objectsTransform: |
| 104 | + // |
| 105 | + // ~newObjectsTransform * ReferenceTransform = scaleTransform * targetTransform |
| 106 | + // ~newObjectsTransform = scaleTransform * targetTransform * ~ReferenceTransform |
| 107 | + // newObjectsTransform = ~(scaleTransform * targetTransform * ~ReferenceTransform) |
| 108 | + // |
| 109 | + // This is implemented below (notice the inverted order of operations |
| 110 | + // because of OSG's matrix structure): |
| 111 | + |
| 112 | + return osg::Matrix::inverse(osg::Matrix::inverse(referenceTransform) * targetTransform * scaleTransform); |
| 113 | +} |
| 114 | + |
| 115 | +bool TeleportNavigationProvider::update() |
| 116 | +{ |
| 117 | + bool enabledAndValid = isEnabledAndValid(); |
| 118 | + setVisible(enabledAndValid); |
| 119 | + |
| 120 | + if (!enabledAndValid) |
| 121 | + { |
| 122 | + return true; |
| 123 | + } |
| 124 | + |
| 125 | + // Adjust turn angle based on the mouse wheel |
| 126 | + turn_angle += triggerWheel.getWheelCount() * M_PI / 8; |
| 127 | + |
| 128 | + // Adjust turn angle based using pointer secondary action and swipe left/right |
| 129 | + if (interactionTurn.wasStarted()) |
| 130 | + { |
| 131 | + oldHandMatrix = cover->getPointerMat(); |
| 132 | + } |
| 133 | + else if (interactionTurn.isRunning()) |
| 134 | + { |
| 135 | + // See how the hand was moved and compute the angle it turned around the Z axis. |
| 136 | + auto handMatrix = cover->getPointerMat(); |
| 137 | + auto handMovmentSinceLastFrame = handMatrix * osg::Matrix::inverse(oldHandMatrix); |
| 138 | + auto c = handMovmentSinceLastFrame.getRotate() * X; |
| 139 | + auto angle_change = (c.y() || c.x()) ? atan2(c.y(), c.x()) : 0.f; |
| 140 | + |
| 141 | + // Turn the target location much more than the sweeping motion from the |
| 142 | + // pointer/hand. |
| 143 | + turn_angle += angle_change * 10.f; |
| 144 | + |
| 145 | + oldHandMatrix = handMatrix; |
| 146 | + } |
| 147 | + |
| 148 | + osg::Matrix currentObjectsTransform = cover->getObjectsXform()->getMatrix(); |
| 149 | + |
| 150 | + // Compute the current object's rotation around the Z axis, we want the |
| 151 | + // target angle to be the same as the current angle for `turn_angle = 0`. |
| 152 | + osg::Quat q = currentObjectsTransform.getRotate(); |
| 153 | + osg::Vec3 f = q * X; |
| 154 | + float current_angle = f.x() || f.y() ? -atan2(f.y(), f.x()) : 0.0; |
| 155 | + |
| 156 | + // The target angle around Z is the sum of the current angle and the |
| 157 | + // (relative) turn. |
| 158 | + float target_angle = current_angle + turn_angle; |
| 159 | + |
| 160 | + osg::Vec3 position = cover->getIntersectionHitPointWorld(); |
| 161 | + |
| 162 | + // The intersection position is relative to the world (stage), so we need |
| 163 | + // to turn it into object coordinates, as we attached the indicator to the object root. |
| 164 | + osg::Matrix objectsRootTransform = osg::computeWorldToLocal(cover->getObjectsRoot()->getParentalNodePaths().at(0)); |
| 165 | + |
| 166 | + auto targetTransform = osg::Matrix::rotate(target_angle, UP) * osg::Matrix::translate(position * objectsRootTransform + UP * 0.001f); |
| 167 | + |
| 168 | + // Set the indicator transform, scaling and rotating it (because the loaded |
| 169 | + // model is a unit circle and points in the wrong direction). |
| 170 | + float ringSize = 0.4; |
| 171 | + auto ringScale = osg::Matrix::scale(ringSize, ringSize, ringSize); |
| 172 | + transform->setMatrix(ringScale * osg::Matrix::rotate(M_PI / 2, UP) * targetTransform); |
| 173 | + |
| 174 | + bool mouseTriggered = triggerMouse.wasStopped(); |
| 175 | + bool pointerTriggered = interactionPoint.wasStopped(); |
| 176 | + |
| 177 | + if (mouseTriggered || pointerTriggered) |
| 178 | + { |
| 179 | + // Apply the movement transform |
| 180 | + cover->getObjectsXform()->setMatrix(computeNewObjectsTransform(targetTransform, mouseTriggered)); |
| 181 | + |
| 182 | + // Reset turn_angle for the next jump |
| 183 | + turn_angle = 0.f; |
| 184 | + } |
| 185 | + |
| 186 | + return true; |
| 187 | +} |
| 188 | + |
| 189 | +bool TeleportNavigationProvider::isEnabledAndValid() |
| 190 | +{ |
| 191 | + if (!isEnabled()) |
| 192 | + return false; |
| 193 | + |
| 194 | + if (!cover->getIntersectedNode()) |
| 195 | + return false; |
| 196 | + |
| 197 | + osg::Vec3 normal = cover->getIntersectionHitPointWorldNormal(); |
| 198 | + normal.normalize(); |
| 199 | + |
| 200 | + // Get and decompose the object's space transform |
| 201 | + osg::Matrix objectsRootTransform = osg::computeWorldToLocal(cover->getObjectsRoot()->getParentalNodePaths().at(0)); |
| 202 | + osg::Vec3f translation, scale; |
| 203 | + osg::Quat rotation, so; |
| 204 | + objectsRootTransform.decompose(translation, rotation, scale, so); |
| 205 | + |
| 206 | + // Rotate the normal into object space |
| 207 | + normal = rotation * normal; |
| 208 | + |
| 209 | + // If the normal in object space doesn't roughly point upwards, we don't |
| 210 | + // want to teleport there. |
| 211 | + if (normal * UP < 0.9f) |
| 212 | + return false; |
| 213 | + |
| 214 | + return true; |
| 215 | +} |
| 216 | + |
| 217 | +void TeleportNavigationProvider::setVisible(bool visible) |
| 218 | +{ |
| 219 | + if (visible) |
| 220 | + switch_->setAllChildrenOn(); |
| 221 | + else |
| 222 | + switch_->setAllChildrenOff(); |
| 223 | +} |
0 commit comments