From 352c9dd5ec105e385e0d428b6fea1347b5f1f4b2 Mon Sep 17 00:00:00 2001 From: Emma Broman Date: Tue, 14 Feb 2023 13:55:24 +0100 Subject: [PATCH] Touch module code cleanup (#2465) * Remove unused feature that allowed us to "pick" nodes using touch (it didn't really work and had some nasty hardcoded and costly implementation). Fixes Touch interaction picking refactor #1041 * General refactoring of code and removing redundant code * Make touch markers prettier (change default color and smoothen edges) * Add module property to control which renderable types are "directly touchable" * Add SGN property to control which individual nodes are "directly touchable" ("SupportsDirectInteraction") * Fix stuttering when zooming in closer than the orbitalnavigator allows --- data/assets/base_blank.asset | 1 + .../modules/touch/default_settings.asset | 19 + .../openspace/navigation/orbitalnavigator.h | 17 +- include/openspace/rendering/renderable.h | 3 + include/openspace/scene/scenegraphnode.h | 3 + modules/touch/include/touchinteraction.h | 74 +-- modules/touch/shaders/marker_fs.glsl | 20 +- modules/touch/src/directinputsolver.cpp | 7 + modules/touch/src/touchinteraction.cpp | 592 ++++++------------ modules/touch/src/touchmarker.cpp | 2 +- modules/touch/touchmodule.cpp | 117 +++- modules/touch/touchmodule.h | 16 +- src/navigation/orbitalnavigator.cpp | 26 +- src/rendering/renderable.cpp | 4 + src/scene/scenegraphnode.cpp | 55 +- 15 files changed, 464 insertions(+), 492 deletions(-) create mode 100644 data/assets/modules/touch/default_settings.asset diff --git a/data/assets/base_blank.asset b/data/assets/base_blank.asset index 00611bc658..40bfec6188 100644 --- a/data/assets/base_blank.asset +++ b/data/assets/base_blank.asset @@ -19,6 +19,7 @@ asset.require("util/launcher_images") -- Modules and component settings asset.require("modules/exoplanets/exoplanets") asset.require("modules/skybrowser/skybrowser") +asset.require("modules/touch/default_settings") asset.onInitialize(function () diff --git a/data/assets/modules/touch/default_settings.asset b/data/assets/modules/touch/default_settings.asset new file mode 100644 index 0000000000..1683bd3a03 --- /dev/null +++ b/data/assets/modules/touch/default_settings.asset @@ -0,0 +1,19 @@ +asset.onInitialize(function () + openspace.setPropertyValueSingle("Modules.Touch.EnableTouchInteraction", true) + + -- A list of renderable types that apply the "direct manipulation". Works best for + -- things with a sperical-ish shape and an intearction sphere of about the same size + -- as the bounding sphere. + -- Can also be set for each scene graph node using the "IsDirectlyTouchable" property + local directTouchList = { "RenderableGlobe" } + openspace.setPropertyValueSingle("Modules.Touch.DefaultDirectTouchRenderableTypes", directTouchList) +end) + +asset.meta = { + Name = "Touch Module Default Settings", + Version = "1.0", + Description = "Some default settings related to the touch module", + Author = "OpenSpace Team", + URL = "http://openspaceproject.com", + License = "MIT license" +} diff --git a/include/openspace/navigation/orbitalnavigator.h b/include/openspace/navigation/orbitalnavigator.h index 1fa4a95bfb..00e083d55b 100644 --- a/include/openspace/navigation/orbitalnavigator.h +++ b/include/openspace/navigation/orbitalnavigator.h @@ -129,9 +129,18 @@ public: bool hasZoomFriction() const; bool hasRollFriction() const; + double minAllowedDistance() const; + glm::dvec3 anchorNodeToCameraVector() const; glm::quat anchorNodeToCameraRotation() const; + /** + * Compute a camera position that pushed the camera position to + * a valid position over the anchor node, accounting for the + * minimal allowed distance + */ + glm::dvec3 pushToSurfaceOfAnchor(const glm::dvec3& cameraPosition) const; + private: struct CameraRotationDecomposition { glm::dquat localRotation = glm::dquat(1.0, 0.0, 0.0, 0.0); @@ -328,11 +337,11 @@ private: /** * Push the camera out to the surface of the object. * - * \return a position vector adjusted to be at least minHeightAboveGround meters + * \return a position vector adjusted to be at least _minimumAllowedDistance meters * above the actual surface of the object */ - glm::dvec3 pushToSurface(double minHeightAboveGround, - const glm::dvec3& cameraPosition, const glm::dvec3& objectPosition, + glm::dvec3 pushToSurface(const glm::dvec3& cameraPosition, + const glm::dvec3& objectPosition, const SurfacePositionHandle& positionHandle) const; /** @@ -351,7 +360,7 @@ private: * Calculates a SurfacePositionHandle given a camera position in world space. */ SurfacePositionHandle calculateSurfacePositionHandle(const SceneGraphNode& node, - const glm::dvec3 cameraPositionWorldSpace); + const glm::dvec3& cameraPositionWorldSpace) const; void resetIdleBehavior(); diff --git a/include/openspace/rendering/renderable.h b/include/openspace/rendering/renderable.h index 7bff19fafa..3d49bc20e1 100644 --- a/include/openspace/rendering/renderable.h +++ b/include/openspace/rendering/renderable.h @@ -33,6 +33,7 @@ #include #include #include +#include namespace ghoul { class Dictionary; } namespace ghoul::opengl { @@ -79,6 +80,8 @@ public: double boundingSphere() const; double interactionSphere() const; + std::string_view typeAsString() const; + virtual void render(const RenderData& data, RendererTasks& rendererTask); virtual void update(const UpdateData& data); diff --git a/include/openspace/scene/scenegraphnode.h b/include/openspace/scene/scenegraphnode.h index b933a27a75..15388e12d2 100644 --- a/include/openspace/scene/scenegraphnode.h +++ b/include/openspace/scene/scenegraphnode.h @@ -139,6 +139,8 @@ public: double reachFactor() const; double approachFactor() const; + bool supportsDirectInteraction() const; + SceneGraphNode* childNode(const std::string& identifier); const Renderable* renderable() const; @@ -204,6 +206,7 @@ private: properties::DoubleProperty _distFromCamToNode; properties::DoubleProperty _screenSizeRadius; properties::FloatProperty _visibilityDistance; + properties::BoolProperty _supportsDirectInteraction; // This variable is used for the rate-limiting of the screenspace positions (if they // are calculated when _computeScreenSpaceValues is true) diff --git a/modules/touch/include/touchinteraction.h b/modules/touch/include/touchinteraction.h index b492bbb5d6..44fd1c237b 100644 --- a/modules/touch/include/touchinteraction.h +++ b/modules/touch/include/touchinteraction.h @@ -41,7 +41,6 @@ #include //#define TOUCH_DEBUG_PROPERTIES -//#define TOUCH_DEBUG_NODE_PICK_MESSAGES namespace openspace { @@ -51,9 +50,9 @@ class SceneGraphNode; // Class used for keeping track of the recent average frame time class FrameTimeAverage { public: - //Update the circular buffer with the most recent frame time + // Update the circular buffer with the most recent frame time void updateWithNewFrame(double sample); - //Get the value of the most recent average frame time (seconds) + // Get the value of the most recent average frame time (seconds) double averageFrameTime() const; private: @@ -67,13 +66,11 @@ class TouchInteraction : public properties::PropertyOwner { public: TouchInteraction(); - // for interpretInteraction() - enum Type { + enum class InteractionType { ROTATION = 0, PINCH, PAN, ROLL, - PICK, ZOOM_OUT }; @@ -85,18 +82,19 @@ public: glm::dvec2 pan = glm::dvec2(0.0); }; - /* Main function call + /** + * Main function call * 1 Checks if doubleTap occured * 2 If the node in focus is large enough and all contact points have selected it, * calls directControl() function for direct-manipulation - * 3 Updates std::vector _selected (only if LMA successfully + * 3 Updates std::vector _selectedContactPoints (only if LMA successfully * converged, avoids interaction to snap on LMA fails) * 4 If directControl() wasn't called this frame, interpret the incoming * list and decide what type of interaction this frame should do * 5 Compute the new total velocities after interaction * 6 Evaluate if directControl should be called next frame- true if all contact points * select the same node and said node is larger than _nodeRadiusThreshold - */ + */ void updateStateFromInput(const std::vector& list, std::vector& lastProcessed); @@ -109,46 +107,43 @@ public: // Sets _tap to true, called if tap occured current frame (called from touchmodule) void tap(); - // Set touchactive as true from the touchmodule if incoming list isn't empty, used to - // void mouse input - void touchActive(bool active); - // Get & Setters - Camera* getCamera(); - const SceneGraphNode* getFocusNode(); - void setFocusNode(const SceneGraphNode* focusNode); void setCamera(Camera* camera); private: - /* Function that calculates the new camera state such that it minimizes the L2 error + /** + * Function that calculates the new camera state such that it minimizes the L2 error * in screenspace * between contact points and surface coordinates projected to clip space using LMA */ void directControl(const std::vector& list); - /* Traces each contact point into the scene as a ray - * if the ray hits a node, save the id, node and surface coordinates the cursor hit - * in the list _selected + /** + * Traces each contact point into the scene as a ray and find the intersection + * points on the surface of the current anchor node, if any. Saves the input id + * the node and surface coordinates the cursor hit */ - void findSelectedNode(const std::vector& list); + void updateNodeSurfacePoints(const std::vector& list); - /* Returns an int (ROT = 0, PINCH, PAN, ROLL, PICK) for what interaction to be used, - * depending on what input was gotten + /** + * Returns an enum for what interaction to be used, depending on what input was + * received */ - int interpretInteraction(const std::vector& list, + InteractionType interpretInteraction(const std::vector& list, const std::vector& lastProcessed); // Compute new velocity according to the interpreted action void computeVelocities(const std::vector& list, const std::vector& lastProcessed); - //Compute velocity based on double-tap for zooming + // Compute velocity based on double-tap for zooming double computeTapZoomDistance(double zoomGain); - //Compute coefficient for velocity decay to be applied in decceleration + // Compute coefficient for velocity decay to be applied in decceleration double computeConstTimeDecayCoefficient(double velocity); - /* Decelerate the velocities. Function is called in step() but is dereferenced from + /** + * Decelerate the velocities. Function is called in step() but is dereferenced from * frame time to assure same behaviour on all systems */ void decelerate(double dt); @@ -156,12 +151,13 @@ private: // Resets all properties that can be changed in the GUI to default void resetPropertiesToDefault(); + // Set all velocities to zero + void resetVelocities(); + Camera* _camera = nullptr; // Property variables - properties::StringProperty _origin; properties::BoolProperty _unitTest; - properties::BoolProperty _touchActive; properties::BoolProperty _disableZoom; properties::BoolProperty _disableRoll; properties::TriggerProperty _reset; @@ -170,10 +166,7 @@ private: properties::FloatProperty _touchScreenSize; properties::FloatProperty _tapZoomFactor; properties::FloatProperty _pinchZoomFactor; - properties::FloatProperty _nodeRadiusThreshold; properties::FloatProperty _rollAngleThreshold; - properties::FloatProperty _orbitSpeedThreshold; - properties::FloatProperty _spinSensitivity; properties::FloatProperty _zoomSensitivityExponential; properties::FloatProperty _zoomSensitivityProportionalDist; properties::FloatProperty _zoomSensitivityDistanceThreshold; @@ -184,11 +177,12 @@ private: properties::FloatProperty _centroidStillThreshold; properties::BoolProperty _panEnabled; properties::FloatProperty _interpretPan; - properties::FloatProperty _slerpTime; properties::Vec4Property _friction; - properties::FloatProperty _pickingRadiusMinimum; properties::FloatProperty _constTimeDecay_secs; + properties::BoolProperty _enableDirectManipulation; + properties::FloatProperty _directTouchDistanceThreshold; + #ifdef TOUCH_DEBUG_PROPERTIES struct DebugProperties : PropertyOwner { DebugProperties(); @@ -205,14 +199,13 @@ private: int stepVelUpdate = 0; #endif std::array _pinchInputs; + // Class variables VelocityStates _vel; VelocityStates _lastVel; VelocityStates _sensitivity; - double _projectionScaleFactor = 1.000004; - double _currentRadius = 1.0; - double _slerpdT = 10001.0; + bool _isWithinDirectTouchDistance = false; double _timeSlack = 0.0; std::chrono::milliseconds _time; bool _directTouchMode = false; @@ -220,12 +213,9 @@ private: bool _tap = false; bool _doubleTap = false; bool _zoomOutTap = false; - bool _lmSuccess = true; - std::vector _selected; - SceneGraphNode* _pickingSelected = nullptr; - DirectInputSolver _solver; + std::vector _selectedNodeSurfacePoints; + DirectInputSolver _directInputSolver; - glm::dquat _toSlerp = glm::dquat(1.0, 0.0, 0.0, 0.0); glm::vec2 _centroid = glm::vec2(0.f); FrameTimeAverage _frameTimeAvg; diff --git a/modules/touch/shaders/marker_fs.glsl b/modules/touch/shaders/marker_fs.glsl index 3815eb1b7d..9d9018a9b9 100644 --- a/modules/touch/shaders/marker_fs.glsl +++ b/modules/touch/shaders/marker_fs.glsl @@ -33,20 +33,28 @@ uniform vec3 color; Fragment getFragment() { - // calculate normal from texture coordinates + // Calculate normal from texture coordinates vec3 n; n.xy = gl_PointCoord.st * vec2(2.0, -2.0) + vec2(-1.0, 1.0); float mag = dot(n.xy, n.xy); - if (mag > 1.0) { - // kill pixels outside circle - discard; + + float edgeSmoothing = 1.0; + float w = 0.1; // wdith for smoothing + if (mag > 1.0 - w) { + // Kill pixels outside circle. Do a smoothstep for soft border + float t = (mag - (1.0-w)) / w; + edgeSmoothing = smoothstep(1.0, 0.0, t); + if (edgeSmoothing <= 0.0) { + discard; + } } n.z = sqrt(1.0 - mag); - - // calculate lighting + + // Calculate lighting vec3 light_dir = vec3(0.0, 0.0, 1.0); float diffuse = max(0.0, dot(light_dir, n)); float alpha = min(pow(sqrt(mag), thickness), opacity); + alpha *= edgeSmoothing; Fragment frag; frag.color = vec4(color * diffuse, alpha); diff --git a/modules/touch/src/directinputsolver.cpp b/modules/touch/src/directinputsolver.cpp index b0665ce5da..89360bfb3c 100644 --- a/modules/touch/src/directinputsolver.cpp +++ b/modules/touch/src/directinputsolver.cpp @@ -212,6 +212,13 @@ bool DirectInputSolver::solve(const std::vector& list, const std::vector& selectedBodies, std::vector* parameters, const Camera& camera) { + ZoneScopedN("Direct touch input solver") + + ghoul_assert( + selectedBodies.size() >= list.size(), + "Number of touch inputs must match the number of 'selected bodies'" + ); + int nFingers = std::min(static_cast(list.size()), 3); _nDof = std::min(nFingers * 2, 6); diff --git a/modules/touch/src/touchinteraction.cpp b/modules/touch/src/touchinteraction.cpp index dcce4ae840..63fb656faa 100644 --- a/modules/touch/src/touchinteraction.cpp +++ b/modules/touch/src/touchinteraction.cpp @@ -22,19 +22,13 @@ * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * ****************************************************************************************/ -#include - -#ifdef OPENSPACE_MODULE_GLOBEBROWSING_ENABLED -#include -#include -#endif - #include + #include +#include #include #include #include -#include #include #include #include @@ -68,12 +62,6 @@ namespace { constexpr std::string_view _loggerCat = "TouchInteraction"; - constexpr openspace::properties::Property::PropertyInfo OriginInfo = { - "Origin", - "Origin", - "" // @TODO Missing documentation - }; - constexpr openspace::properties::Property::PropertyInfo UnitTestInfo = { "UnitTest", "Take a unit test saving the LM data into file", @@ -95,13 +83,6 @@ namespace { "" // @TODO Missing documentation }; - constexpr openspace::properties::Property::PropertyInfo EventsInfo = { - "TouchEvents", - "True if we have a touch event", - "", - openspace::properties::Property::Visibility::Hidden - }; - constexpr openspace::properties::Property::PropertyInfo SetDefaultInfo = { "SetDefault", "Reset all properties to default", @@ -139,30 +120,12 @@ namespace { "sensitivity that will alter the pinch-zoom speed" }; - constexpr openspace::properties::Property::PropertyInfo DirectManipulationInfo = { - "DirectManipulationRadius", - "Radius a planet has to have to activate direct-manipulation", - "" // @TODO Missing documentation - }; - constexpr openspace::properties::Property::PropertyInfo RollThresholdInfo = { "RollThreshold", "Threshold for min angle for roll interpret", "" // @TODO Missing documentation }; - constexpr openspace::properties::Property::PropertyInfo OrbitSpinningThreshold = { - "OrbitThreshold", - "Threshold to activate orbit spinning in direct-manipulation", - "" // @TODO Missing documentation - }; - - constexpr openspace::properties::Property::PropertyInfo SpinningSensitivityInfo = { - "SpinningSensitivity", - "Sensitivity of spinning in direct-manipulation", - "" // @TODO Missing documentation - }; - constexpr openspace::properties::Property::PropertyInfo ZoomSensitivityExpInfo = { "ZoomSensitivityExp", "Sensitivity of exponential zooming in relation to distance from focus node", @@ -221,12 +184,6 @@ namespace { "" // @TODO Missing documentation }; - constexpr openspace::properties::Property::PropertyInfo SlerpTimeInfo = { - "SlerpTime", - "Time to slerp in seconds to new orientation with new node picking", - "" // @TODO Missing documentation - }; - constexpr openspace::properties::Property::PropertyInfo FrictionInfo = { "Friction", "Friction for different interactions (orbit, zoom, roll, pan)", @@ -249,6 +206,24 @@ namespace { "defaults to the surface of the current anchor." }; + constexpr openspace::properties::Property::PropertyInfo + EnableDirectManipulationInfo = + { + "EnableDirectManipulation", + "Enable direct manipulation", + "Decides whether the direct manipulation mode should be enabled or not. " + }; + + constexpr openspace::properties::Property::PropertyInfo + DirectManipulationThresholdInfo = + { + "DirectManipulationThreshold", + "Direct manipulation threshold", + "This threshold affects the distance from the interaction sphere at which the " + "direct manipulation interaction mode starts being active. The value is given " + "as a factor times the interaction sphere" + }; + // Compute coefficient of decay based on current frametime; if frametime has been // longer than usual then multiple decay steps may be applied to keep the decay // relative to user time @@ -266,22 +241,17 @@ namespace openspace { TouchInteraction::TouchInteraction() : properties::PropertyOwner({ "TouchInteraction" }) - , _origin(OriginInfo) , _unitTest(UnitTestInfo, false) - , _touchActive(EventsInfo, false) , _disableZoom(DisableZoomInfo, false) , _disableRoll(DisableRollInfo, false) , _reset(SetDefaultInfo) , _maxTapTime(MaxTapTimeInfo, 300, 10, 1000) , _deceleratesPerSecond(DecelatesPerSecondInfo, 240, 60, 300) - , _touchScreenSize(TouchScreenSizeInfo, 55.0f, 5.5f, 150.0f) + , _touchScreenSize(TouchScreenSizeInfo, 55.f, 5.5f, 150.f) , _tapZoomFactor(TapZoomFactorInfo, 0.2f, 0.f, 0.5f, 0.01f) , _pinchZoomFactor(PinchZoomFactorInfo, 0.01f, 0.f, 0.2f) - , _nodeRadiusThreshold(DirectManipulationInfo, 0.2f, 0.0f, 1.0f) , _rollAngleThreshold(RollThresholdInfo, 0.025f, 0.f, 0.05f, 0.001f) - , _orbitSpeedThreshold(OrbitSpinningThreshold, 0.005f, 0.f, 0.01f, 0.0001f) - , _spinSensitivity(SpinningSensitivityInfo, 0.25f, 0.f, 2.f, 0.01f) - , _zoomSensitivityExponential(ZoomSensitivityExpInfo, 1.03f, 1.0f, 1.1f) + , _zoomSensitivityExponential(ZoomSensitivityExpInfo, 1.03f, 1.f, 1.1f) , _zoomSensitivityProportionalDist(ZoomSensitivityPropInfo, 11.f, 5.f, 50.f) , _zoomSensitivityDistanceThreshold( ZoomSensitivityDistanceThresholdInfo, @@ -289,7 +259,7 @@ TouchInteraction::TouchInteraction() 0.01f, 0.25f ) - , _zoomBoundarySphereMultiplier(ZoomBoundarySphereMultiplierInfo, 1.001f, 0.01f, 10000.0f) + , _zoomBoundarySphereMultiplier(ZoomBoundarySphereMultiplierInfo, 1.001f, 0.01f, 10000.f) , _zoomInLimit(ZoomInLimitInfo, -1.0, 0.0, std::numeric_limits::max()) , _zoomOutLimit( ZoomOutLimitInfo, @@ -298,31 +268,25 @@ TouchInteraction::TouchInteraction() std::numeric_limits::max() ) , _inputStillThreshold(InputSensitivityInfo, 0.0005f, 0.f, 0.001f, 0.0001f) - // used to void wrongly interpreted roll interactions + // Used to void wrongly interpreted roll interactions , _centroidStillThreshold(StationaryCentroidInfo, 0.0018f, 0.f, 0.01f, 0.0001f) , _panEnabled(PanModeInfo, false) , _interpretPan(PanDeltaDistanceInfo, 0.015f, 0.f, 0.1f) - , _slerpTime(SlerpTimeInfo, 3.f, 0.1f, 5.f) , _friction( FrictionInfo, glm::vec4(0.025f, 0.025f, 0.02f, 0.001f), glm::vec4(0.f), glm::vec4(0.2f) ) - , _pickingRadiusMinimum( - { "Picking Radius", "Minimum radius for picking in NDC coordinates", "" }, - 0.1f, - 0.f, - 1.f - ) - , _constTimeDecay_secs(ConstantTimeDecaySecsInfo, 1.75f, 0.1f, 4.0f) + , _constTimeDecay_secs(ConstantTimeDecaySecsInfo, 1.75f, 0.1f, 4.f) , _pinchInputs({ TouchInput(0, 0, 0.0, 0.0, 0.0), TouchInput(0, 0, 0.0, 0.0, 0.0) }) , _vel{ glm::dvec2(0.0), 0.0, 0.0, glm::dvec2(0.0) } , _sensitivity{ glm::dvec2(0.08, 0.045), 12.0, 2.75, glm::dvec2(0.08, 0.045) } - // calculated with two vectors with known diff in length, then + // Calculated with two vectors with known diff in length, then // projDiffLength/diffLength. + , _enableDirectManipulation(EnableDirectManipulationInfo, true) + , _directTouchDistanceThreshold(DirectManipulationThresholdInfo, 5.f, 0.f, 10.f) { - addProperty(_touchActive); addProperty(_disableZoom); addProperty(_disableRoll); addProperty(_unitTest); @@ -332,10 +296,7 @@ TouchInteraction::TouchInteraction() addProperty(_touchScreenSize); addProperty(_tapZoomFactor); addProperty(_pinchZoomFactor); - addProperty(_nodeRadiusThreshold); addProperty(_rollAngleThreshold); - addProperty(_orbitSpeedThreshold); - addProperty(_spinSensitivity); addProperty(_zoomSensitivityExponential); addProperty(_zoomSensitivityProportionalDist); addProperty(_zoomSensitivityDistanceThreshold); @@ -347,25 +308,15 @@ TouchInteraction::TouchInteraction() addProperty(_centroidStillThreshold); addProperty(_panEnabled); addProperty(_interpretPan); - addProperty(_slerpTime); addProperty(_friction); - addProperty(_pickingRadiusMinimum); + + addProperty(_enableDirectManipulation); + addProperty(_directTouchDistanceThreshold); #ifdef TOUCH_DEBUG_PROPERTIES addPropertySubOwner(_debugProperties); #endif - _origin.onChange([this]() { - SceneGraphNode* node = sceneGraphNode(_origin.value()); - if (node) { - setFocusNode(node); - } - else { - LWARNING(fmt::format( - "Could not find a node in scenegraph called '{}'", _origin.value() - )); - } - }); _time = std::chrono::duration_cast( std::chrono::high_resolution_clock::now().time_since_epoch() ); @@ -378,97 +329,90 @@ TouchInteraction::TouchInteraction() void TouchInteraction::updateStateFromInput(const std::vector& list, std::vector& lastProcessed) { + size_t numFingers = list.size(); + #ifdef TOUCH_DEBUG_PROPERTIES - _debugProperties.nFingers = list.size(); + _debugProperties.nFingers = numFingers; #endif - OpenSpaceEngine::Mode mode = global::openSpaceEngine->currentMode(); - if (mode == OpenSpaceEngine::Mode::CameraPath || - mode == OpenSpaceEngine::Mode::SessionRecordingPlayback) - { + if (numFingers == 0) { + // No fingers, no input (note that this function should not even be called then) return; } if (_tap) { - // check for doubletap + // @TODO (2023-02-01, emmbr) This if is not triggered on every touch tap. + // Why? + + // Check for doubletap using namespace std::chrono; milliseconds timestamp = duration_cast( high_resolution_clock::now().time_since_epoch() ); if ((timestamp - _time).count() < _maxTapTime) { - LINFO("Double tap!"); _doubleTap = true; _tap = false; } _time = timestamp; } + // Code for lower-right corner double-tap to zoom-out - const glm::vec2 res = global::windowDelegate->currentWindowSize(); - const glm::vec2 pos = list[0].latestInput().screenCoordinates(res); + { + const glm::vec2 res = global::windowDelegate->currentWindowSize(); + const glm::vec2 pos = list[0].latestInput().screenCoordinates(res); - const float bottomCornerSizeForZoomTap_fraction = 0.08f; - const int zoomTapThresholdX = static_cast( - res.x * (1.f - bottomCornerSizeForZoomTap_fraction) - ); - const int zoomTapThresholdY = static_cast( - res.y * (1.f - bottomCornerSizeForZoomTap_fraction) - ); + const float bottomCornerSizeForZoomTap_fraction = 0.08f; + const int zoomTapThresholdX = static_cast( + res.x * (1.f - bottomCornerSizeForZoomTap_fraction) + ); + const int zoomTapThresholdY = static_cast( + res.y * (1.f - bottomCornerSizeForZoomTap_fraction) + ); - const bool isTapInLowerRightCorner = - (std::abs(pos.x) > zoomTapThresholdX && std::abs(pos.y) > zoomTapThresholdY); + const bool isTapInLowerRightCorner = + (std::abs(pos.x) > zoomTapThresholdX && std::abs(pos.y) > zoomTapThresholdY); - if (_doubleTap && isTapInLowerRightCorner) { - _zoomOutTap = true; - _tap = false; - _doubleTap = false; + if (_doubleTap && isTapInLowerRightCorner) { + _zoomOutTap = true; + _tap = false; + _doubleTap = false; + } } - size_t numFingers = list.size(); bool isTransitionBetweenModes = (_wasPrevModeDirectTouch != _directTouchMode); if (isTransitionBetweenModes) { - _vel.orbit = glm::dvec2(0.0); - _vel.zoom = 0.0; - _vel.roll = 0.0; - _vel.pan = glm::dvec2(0.0); + resetVelocities(); resetAfterInput(); } - if (_directTouchMode && _selected.size() > 0 && numFingers == _selected.size()) { + _directTouchMode = _enableDirectManipulation && + _isWithinDirectTouchDistance && + !_selectedNodeSurfacePoints.empty() && + numFingers == _selectedNodeSurfacePoints.size(); + + if (_directTouchMode) { #ifdef TOUCH_DEBUG_PROPERTIES _debugProperties.interactionMode = "Direct"; #endif directControl(list); } - if (_lmSuccess) { - findSelectedNode(list); - } - - if (!_directTouchMode) { + else { #ifdef TOUCH_DEBUG_PROPERTIES _debugProperties.interactionMode = "Velocities"; #endif computeVelocities(list, lastProcessed); } + if (_enableDirectManipulation && _isWithinDirectTouchDistance) { + updateNodeSurfacePoints(list); + } + _wasPrevModeDirectTouch = _directTouchMode; - // evaluates if current frame is in directTouchMode (will be used next frame) - _directTouchMode = - (_currentRadius > _nodeRadiusThreshold && _selected.size() == numFingers); } void TouchInteraction::directControl(const std::vector& list) { // Reset old velocities upon new interaction - _vel.orbit = glm::dvec2(0.0); - _vel.zoom = 0.0; - _vel.roll = 0.0; - _vel.pan = glm::dvec2(0.0); - - OpenSpaceEngine::Mode mode = global::openSpaceEngine->currentMode(); - if (mode == OpenSpaceEngine::Mode::CameraPath || - mode == OpenSpaceEngine::Mode::SessionRecordingPlayback) - { - return; - } + resetVelocities(); #ifdef TOUCH_DEBUG_PROPERTIES LINFO("DirectControl"); @@ -478,10 +422,10 @@ void TouchInteraction::directControl(const std::vector& list) std::vector par(6, 0.0); par[0] = _lastVel.orbit.x; // use _lastVel for orbit par[1] = _lastVel.orbit.y; - _lmSuccess = _solver.solve(list, _selected, &par, *_camera); - int nDof = _solver.nDof(); + bool lmSuccess = _directInputSolver.solve(list, _selectedNodeSurfacePoints, &par, *_camera); + int nDof = _directInputSolver.nDof(); - if (_lmSuccess && !_unitTest) { + if (lmSuccess && !_unitTest) { // If good values were found set new camera state _vel.orbit = glm::dvec2(par.at(0), par.at(1)); if (nDof > 2) { @@ -500,51 +444,38 @@ void TouchInteraction::directControl(const std::vector& list) // Reset velocities after setting new camera state _lastVel = _vel; - _vel.orbit = glm::dvec2(0.0); - _vel.zoom = 0.0; - _vel.roll = 0.0; - _vel.pan = glm::dvec2(0.0); + resetVelocities(); } else { - // prevents touch to infinitely be active (due to windows bridge case where event + // Prevents touch to infinitely be active (due to windows bridge case where event // doesn't get consumed sometimes when LMA fails to converge) resetAfterInput(); } } -void TouchInteraction::findSelectedNode(const std::vector& list) { - // trim list to only contain visible nodes that make sense - // @TODO (emmbr 2023-01-31) This hardcoded list should be removed and replaced by something - // else. Either a type of renderable that can always be directly manipulated, or a list - // that can be set in config/assets. Or both? - std::string selectables[31] = { - "Sun", "Mercury", "Venus", "Earth", "Mars", "Ceres", "Jupiter", "Saturn", "Uranus", - "Neptune", "Pluto", "Moon", "Titan", "Rhea", "Mimas", "Iapetus", "Enceladus", - "Dione", "Io", "Ganymede", "Europa", "Callisto", "NewHorizons", "Styx", "Nix", - "Kerberos", "Hydra", "Charon", "Tethys", "OsirisRex", "Bennu" - }; - std::vector selectableNodes; - for (SceneGraphNode* node : global::renderEngine->scene()->allSceneGraphNodes()) { - for (const std::string& name : selectables) { - if (node->identifier() == name) { - selectableNodes.push_back(node); - } - } +void TouchInteraction::updateNodeSurfacePoints(const std::vector& list) { + _selectedNodeSurfacePoints.clear(); + + const SceneGraphNode* anchor = + global::navigationHandler->orbitalNavigator().anchorNode(); + SceneGraphNode* node = sceneGraphNode(anchor->identifier()); + + // Check if current anchor is valid for direct touch + TouchModule* module = global::moduleEngine->module(); + + bool isDirectTouchRenderable = node->renderable() && + module->isDefaultDirectTouchType(node->renderable()->typeAsString()); + + if (!(node->supportsDirectInteraction() || isDirectTouchRenderable)) { + return; } glm::dquat camToWorldSpace = _camera->rotationQuaternion(); glm::dvec3 camPos = _camera->positionVec3(); - std::vector newSelected; - - // node & distance - std::pair currentlyPicked = { - nullptr, - std::numeric_limits::max() - }; - + std::vector surfacePoints; for (const TouchInputHolder& inputHolder : list) { - // normalized -1 to 1 coordinates on screen + // Normalized -1 to 1 coordinates on screen double xCo = 2 * (inputHolder.latestInput().x - 0.5); double yCo = -2 * (inputHolder.latestInput().y - 0.5); glm::dvec3 cursorInWorldSpace = camToWorldSpace * @@ -554,106 +485,37 @@ void TouchInteraction::findSelectedNode(const std::vector& lis size_t id = inputHolder.fingerId(); - for (SceneGraphNode* node : selectableNodes) { - double interactionSphereSquared = - node->interactionSphere() * node->interactionSphere(); - glm::dvec3 camToSelectable = node->worldPosition() - camPos; - double intersectionDist = 0.0; - const bool intersected = glm::intersectRaySphere( - camPos, - raytrace, - node->worldPosition(), - interactionSphereSquared, - intersectionDist - ); - if (intersected) { - glm::dvec3 intersectionPos = camPos + raytrace * intersectionDist; - glm::dvec3 pointInModelView = glm::inverse(node->worldRotationMatrix()) * - (intersectionPos - node->worldPosition()); + // Compute positions on anchor node, by checking if touch input + // intersect interaction sphere + double intersectionDist = 0.0; + const bool intersected = glm::intersectRaySphere( + camPos, + raytrace, + node->worldPosition(), + node->interactionSphere() * node->interactionSphere(), + intersectionDist + ); - // Add id, node and surface coordinates to the selected list - auto oldNode = std::find_if( - newSelected.begin(), - newSelected.end(), - [id](const DirectInputSolver::SelectedBody& s) { return s.id == id; } - ); - if (oldNode != newSelected.end()) { - const double oldNodeDist = glm::length( - oldNode->node->worldPosition() - camPos - ); - if (glm::length(camToSelectable) < oldNodeDist) { - // new node is closer, remove added node and add the new one - // instead - newSelected.pop_back(); - newSelected.push_back({ id, node, pointInModelView }); - } - } - else { - newSelected.push_back({ id, node, pointInModelView }); - } - } + if (intersected) { + glm::dvec3 intersectionPos = camPos + raytrace * intersectionDist; + glm::dvec3 pointInModelView = glm::inverse(node->worldRotationMatrix()) * + (intersectionPos - node->worldPosition()); - // Compute locations in view space to perform the picking - glm::dvec4 clip = glm::dmat4(_camera->projectionMatrix()) * - _camera->combinedViewMatrix() * - glm::vec4(node->worldPosition(), 1.0); - glm::dvec2 ndc = clip / clip.w; - - const bool isVisibleX = (ndc.x >= -1.0 && ndc.x <= 1.0); - const bool isVisibleY = (ndc.y >= -1.0 && ndc.y <= 1.0); - if (isVisibleX && isVisibleY) { - glm::dvec2 cursor = glm::dvec2(xCo, yCo); - - const double ndcDist = glm::length(ndc - cursor); - // We either want to select the object if it's bounding sphere as been - // touched (checked by the first part of this loop above) or if the touch - // point is within a minimum distance of the center - - // If the user touched the planet directly, this is definitely the one - // they are interested in => minimum distance - if (intersected) { -#ifdef TOUCH_DEBUG_NODE_PICK_MESSAGES - LINFOC( - node->identifier(), - "Picking candidate based on direct touch" - ); -#endif //#ifdef TOUCH_DEBUG_NODE_PICK_MESSAGES - currentlyPicked = std::pair( - node, - -std::numeric_limits::max() - ); - } - else if (ndcDist <= _pickingRadiusMinimum) { - // The node was considered due to minimum picking distance radius -#ifdef TOUCH_DEBUG_NODE_PICK_MESSAGES - LINFOC( - node->identifier(), - "Picking candidate based on proximity" - ); -#endif //#ifdef TOUCH_DEBUG_NODE_PICK_MESSAGES - const double dist = length(camToSelectable); - if (dist < currentlyPicked.second) { - currentlyPicked = std::make_pair(node, dist); - } - } - } + // Note that node is saved as the direct input solver was initially + // implemented to handle touch contact points on multiple nodes + surfacePoints.push_back({ id, node, pointInModelView }); } } - // If an item has been picked, it's in the first position of the vector now - if (SceneGraphNode* node = currentlyPicked.first) { - _pickingSelected = node; -#ifdef TOUCH_DEBUG_NODE_PICK_MESSAGES - LINFOC("Picking", "Picked node: " + _pickingSelected->identifier()); -#endif //#ifdef TOUCH_DEBUG_NODE_PICK_MESSAGES - } - - _selected = std::move(newSelected); + _selectedNodeSurfacePoints = std::move(surfacePoints); } -int TouchInteraction::interpretInteraction(const std::vector& list, +TouchInteraction::InteractionType +TouchInteraction::interpretInteraction(const std::vector& list, const std::vector& lastProcessed) { + ghoul_assert(!list.empty(), "Cannot interpret interaction of no input"); + glm::fvec2 lastCentroid = _centroid; _centroid = glm::vec2(0.f, 0.f); for (const TouchInputHolder& inputHolder : list) { @@ -664,7 +526,7 @@ int TouchInteraction::interpretInteraction(const std::vector& } _centroid /= static_cast(list.size()); - // see if the distance between fingers changed - used in pan interpretation + // See if the distance between fingers changed - used in pan interpretation double dist = 0; double lastDist = 0; TouchInput distInput = list[0].latestInput(); @@ -682,7 +544,7 @@ int TouchInteraction::interpretInteraction(const std::vector& glm::dvec2(distInput.x, distInput.y)); distInput = p; } - // find the slowest moving finger - used in roll interpretation + // Find the slowest moving finger - used in roll interpretation double minDiff = 1000.0; for (const TouchInputHolder& inputHolder : list) { const auto it = std::find_if( @@ -707,7 +569,7 @@ int TouchInteraction::interpretInteraction(const std::vector& minDiff = diff; } } - // find if all fingers angles are high - used in roll interpretation + // Find if all fingers angles are high - used in roll interpretation double rollOn = std::accumulate( list.begin(), list.end(), @@ -757,29 +619,26 @@ int TouchInteraction::interpretInteraction(const std::vector& #endif if (_zoomOutTap) { - return ZOOM_OUT; - } - else if (_doubleTap) { - return PICK; + return InteractionType::ZOOM_OUT; } else if (list.size() == 1) { - return ROTATION; + return InteractionType::ROTATION; } else { float avgDistance = static_cast(std::abs(dist - lastDist)); - // if average distance between 3 fingers are constant we have panning + // If average distance between 3 fingers are constant we have panning if (_panEnabled && (avgDistance < _interpretPan && list.size() == 3)) { - return PAN; + return InteractionType::PAN; } - // we have roll if one finger is still, or the total roll angles around the + // We have roll if one finger is still, or the total roll angles around the // centroid is over _rollAngleThreshold (_centroidStillThreshold is used to void // misinterpretations) else if (std::abs(minDiff) < _inputStillThreshold || (std::abs(rollOn) < 100.0 && normalizedCentroidDistance < _centroidStillThreshold)) { - return ROLL; + return InteractionType::ROLL; } else { const bool sameInput0 = _pinchInputs[0].holdsInput(list[0].latestInput()); @@ -791,7 +650,7 @@ int TouchInteraction::interpretInteraction(const std::vector& _pinchInputs[0] = TouchInputHolder(list[0].latestInput()); _pinchInputs[1] = TouchInputHolder(list[1].latestInput()); } - return PINCH; + return InteractionType::PINCH; } } } @@ -799,21 +658,21 @@ int TouchInteraction::interpretInteraction(const std::vector& void TouchInteraction::computeVelocities(const std::vector& list, const std::vector& lastProcessed) { - const int action = interpretInteraction(list, lastProcessed); const SceneGraphNode* anchor = global::navigationHandler->orbitalNavigator().anchorNode(); - if (!anchor) { + if (list.empty() || !anchor) { return; } + const InteractionType action = interpretInteraction(list, lastProcessed); + #ifdef TOUCH_DEBUG_PROPERTIES const std::map interactionNames = { { ROTATION, "Rotation" }, { PINCH, "Pinch" }, { PAN, "Pan" }, - { ROLL, "Roll" }, - { PICK, "Pick" } + { ROLL, "Roll" } }; _debugProperties.interpretedInteraction = interactionNames.at(action); @@ -821,8 +680,7 @@ void TouchInteraction::computeVelocities(const std::vector& li if (pinchConsecCt > 3) { LDEBUG(fmt::format( "PINCH gesture ended with {} drag distance and {} counts", - pinchConsecZoomFactor, - pinchConsecCt + pinchConsecZoomFactor, pinchConsecCt )); } pinchConsecCt = 0; @@ -835,7 +693,8 @@ void TouchInteraction::computeVelocities(const std::vector& li const float aspectRatio = static_cast(windowSize.x) / static_cast(windowSize.y); switch (action) { - case ROTATION: { // add rotation velocity + case InteractionType::ROTATION: { + // Add rotation velocity _vel.orbit += glm::dvec2(inputHolder.speedX() * _sensitivity.orbit.x, inputHolder.speedY() * _sensitivity.orbit.y); @@ -845,13 +704,13 @@ void TouchInteraction::computeVelocities(const std::vector& li ); break; } - case PINCH: { + case InteractionType::PINCH: { if (_disableZoom) { break; } - // add zooming velocity - dependant on distance difference between contact - // points this/first frame + // Add zooming velocity - dependant on distance difference between contact + // points this/first frame using namespace glm; const TouchInput& startFinger0 = _pinchInputs[0].firstInput(); const TouchInput& startFinger1 = _pinchInputs[1].firstInput(); @@ -876,12 +735,12 @@ void TouchInteraction::computeVelocities(const std::vector& li std::max(_touchScreenSize.value() * 0.1, 1.0); break; } - case ROLL: { + case InteractionType::ROLL: { if (_disableRoll) { break; } - // add global roll rotation velocity + // Add global roll rotation velocity double rollFactor = std::accumulate( list.begin(), list.end(), @@ -900,7 +759,7 @@ void TouchInteraction::computeVelocities(const std::vector& li _centroid.x, _centroid.y ); - // if's used to set angles 359 + 1 = 0 and 0 - 1 = 359 + // Ifs used to set angles 359 + 1 = 0 and 0 - 1 = 359 if (lastAngle > currentAngle + 1.5 * glm::pi()) { res += currentAngle + (2 * glm::pi() - lastAngle); } @@ -917,49 +776,24 @@ void TouchInteraction::computeVelocities(const std::vector& li _constTimeDecayCoeff.roll = computeConstTimeDecayCoefficient(_vel.roll); break; } - case PAN: { + case InteractionType::PAN: { if (!_panEnabled) { break; } - // add local rotation velocity + // Add local rotation velocity _vel.pan += glm::dvec2(inputHolder.speedX() * _sensitivity.pan.x, inputHolder.speedY() * _sensitivity.pan.y); double panVelocityAvg = glm::distance(_vel.pan.x, _vel.pan.y); _constTimeDecayCoeff.pan = computeConstTimeDecayCoefficient(panVelocityAvg); break; } - case PICK: { - // pick something in the scene as focus node - if (_pickingSelected) { - setFocusNode(_pickingSelected); - - // rotate camera to look at new focus, using slerp quat - glm::dvec3 camToFocus = _pickingSelected->worldPosition() - - _camera->positionVec3(); - glm::dvec3 forward = glm::normalize(_camera->viewDirectionWorldSpace()); - double angle = glm::angle(forward, camToFocus); - glm::dvec3 axis = glm::normalize(glm::cross(forward, camToFocus)); - _toSlerp.x = axis.x * sin(angle / 2.0); - _toSlerp.y = axis.y * sin(angle / 2.0); - _toSlerp.z = axis.z * sin(angle / 2.0); - _toSlerp.w = cos(angle / 2.0); - _slerpdT = 0.0; - } - else { - // zooms in to current if PICK interpret happened but only space was - // selected - _vel.zoom = computeTapZoomDistance(0.3); - _constTimeDecayCoeff.zoom = computeConstTimeDecayCoefficient(_vel.zoom); - } - break; - } - case ZOOM_OUT: { + case InteractionType::ZOOM_OUT: { if (_disableZoom) { break; } - // zooms out from current if triple tap occurred + // Zooms out from current if triple tap occurred _vel.zoom = computeTapZoomDistance(-1.0); _constTimeDecayCoeff.zoom = computeConstTimeDecayCoefficient(_vel.zoom); } @@ -973,23 +807,18 @@ double TouchInteraction::computeConstTimeDecayCoefficient(double velocity) { if (stepsToDecay > 0.0 && std::abs(velocity) > postDecayVelocityTarget) { return std::pow(postDecayVelocityTarget / std::abs(velocity), 1.0 / stepsToDecay); } - else { - return 1.0; - } + return 1.0; } double TouchInteraction::computeTapZoomDistance(double zoomGain) { const SceneGraphNode* anchor = global::navigationHandler->orbitalNavigator().anchorNode(); + if (!anchor) { return 0.0; } - double dist = glm::distance( - _camera->positionVec3(), - global::navigationHandler->orbitalNavigator().anchorNode()->worldPosition() - ); - + double dist = glm::distance(_camera->positionVec3(), anchor->worldPosition()); dist -= anchor->interactionSphere(); double newVelocity = dist * _tapZoomFactor; @@ -1002,21 +831,11 @@ double TouchInteraction::computeTapZoomDistance(double zoomGain) { // Main update call, calculates the new orientation and position for the camera depending // on _vel and dt. Called every frame void TouchInteraction::step(double dt, bool directTouch) { - OpenSpaceEngine::Mode mode = global::openSpaceEngine->currentMode(); - if (mode == OpenSpaceEngine::Mode::CameraPath || - mode == OpenSpaceEngine::Mode::SessionRecordingPlayback) - { - return; - } - using namespace glm; const SceneGraphNode* anchor = global::navigationHandler->orbitalNavigator().anchorNode(); - // since functions cant be called directly (TouchInteraction not a subclass of - // InteractionMode) - setFocusNode(global::navigationHandler->orbitalNavigator().anchorNode()); if (anchor && _camera) { // Create variables from current state dvec3 camPos = _camera->positionVec3(); @@ -1028,8 +847,7 @@ void TouchInteraction::step(double dt, bool directTouch) { const dvec3 camDirection = _camera->viewDirectionWorldSpace(); // Make a representation of the rotation quaternion with local and global - // rotations - // To avoid problem with lookup in up direction + // rotations. To avoid problem with lookup in up direction const dmat4 lookAtMat = lookAt( dvec3(0.0, 0.0, 0.0), directionToCenter, @@ -1039,9 +857,17 @@ void TouchInteraction::step(double dt, bool directTouch) { dquat localCamRot = inverse(globalCamRot) * _camera->rotationQuaternion(); const double interactionSphere = anchor->interactionSphere(); - const double distance = std::max(length(centerToCamera) - interactionSphere, 0.0); - _currentRadius = interactionSphere / - std::max(distance * _projectionScaleFactor, 1.0); + + // Check if camera is within distance for direct manipulation to be applicable + if (interactionSphere > 0.0 && _enableDirectManipulation) { + const double distance = + std::max(length(centerToCamera) - interactionSphere, 0.0); + const double maxDistance = interactionSphere * _directTouchDistanceThreshold; + _isWithinDirectTouchDistance = distance <= maxDistance; + } + else { + _isWithinDirectTouchDistance = false; + } { // Roll @@ -1053,12 +879,6 @@ void TouchInteraction::step(double dt, bool directTouch) { const dvec3 eulerAngles(_vel.pan.y * dt, _vel.pan.x * dt, 0.0); const dquat rotationDiff = dquat(eulerAngles); localCamRot = localCamRot * rotationDiff; - - // if we have chosen a new focus node - if (_slerpdT < _slerpTime) { - _slerpdT += 0.1 * dt; - localCamRot = slerp(localCamRot, _toSlerp, _slerpdT / _slerpTime); - } } { // Orbit (global rotation) @@ -1077,9 +897,10 @@ void TouchInteraction::step(double dt, bool directTouch) { dvec3(_camera->lookUpVectorCameraSpace()); const dmat4 lookAtMatrix = lookAt( - dvec3(0.0, 0.0, 0.0), + dvec3(0.0), directionToCenter, - lookUpWhenFacingCenter); + lookUpWhenFacingCenter + ); globalCamRot = normalize(quat_cast(inverse(lookAtMatrix))); } { @@ -1106,8 +927,10 @@ void TouchInteraction::step(double dt, bool directTouch) { // Because of heightmaps we need to ensure we don't go through the surface if (_zoomInLimit.value() < nodeRadius) { #ifdef TOUCH_DEBUG_PROPERTIES - LINFO(fmt::format("{}: Zoom In limit should be larger than anchor " - "center to surface, setting it to {}", _loggerCat, zoomInBounds)); + LINFO(fmt::format( + "Zoom In limit should be larger than anchor " + "center to surface, setting it to {}", zoomInBounds + )); #endif zoomInBounds = _zoomInLimit.value(); } @@ -1116,8 +939,8 @@ void TouchInteraction::step(double dt, bool directTouch) { // Make sure zoom in limit is not larger than zoom out limit if (zoomInBounds > _zoomOutLimit.value()) { LWARNING(fmt::format( - "{}: Zoom In Limit should be smaller than Zoom Out Limit", - _loggerCat, _zoomOutLimit.value() + "Zoom In Limit should be smaller than Zoom Out Limit", + _zoomOutLimit.value() )); } const double currentPosDistance = length(centerToCamera); @@ -1163,9 +986,10 @@ void TouchInteraction::step(double dt, bool directTouch) { } else if (currentPosViolatingZoomOutLimit) { #ifdef TOUCH_DEBUG_PROPERTIES - LINFOC("", fmt::format( - "{}: You are outside zoom out {} limit, only zoom in allowed", - _loggerCat, _zoomOutLimit.value())); + LINFO(fmt::format( + "You are outside zoom out {} limit, only zoom in allowed", + _zoomOutLimit.value() + )); #endif // Only allow zooming in if you are outside the zoom out limit if (newPosDistance < currentPosDistance) { @@ -1181,6 +1005,19 @@ void TouchInteraction::step(double dt, bool directTouch) { } decelerate(dt); + + // @TODO (emmbr, 2023-02-08) This is ugly, but for now prevents jittering + // when zooming in closer than the orbital navigator allows. Long term, we + // should make the touch interaction tap into the orbitalnavigator and let that + // do the updating of the camera, instead of handling them separately. Then we + // would keep them in sync and avoid duplicated camera updating code. + camPos = + global::navigationHandler->orbitalNavigator().pushToSurfaceOfAnchor(camPos); + + // @TODO (emmbr, 2023-02-08) with the line above, the ZoomInLimit might not be + // needed anymore. We should make it so that just the limit properties in the + // OrbitalNavigator is actually needed, and don't have duplicates + // Update the camera state _camera->setPositionVec3(camPos); _camera->setRotation(globalCamRot * localCamRot); @@ -1194,8 +1031,7 @@ void TouchInteraction::step(double dt, bool directTouch) { stepVelUpdate = 0; LINFO(fmt::format( "DistToFocusNode {} stepZoomVelUpdate {}", - length(centerToCamera), - _vel.zoom + length(centerToCamera), _vel.zoom )); } #endif @@ -1225,13 +1061,6 @@ void TouchInteraction::decelerate(double dt) { //Ensure the number of times to apply the decay coefficient is valid times = std::min(times, 1); - OpenSpaceEngine::Mode mode = global::openSpaceEngine->currentMode(); - if (mode == OpenSpaceEngine::Mode::CameraPath || - mode == OpenSpaceEngine::Mode::SessionRecordingPlayback) - { - return; - } - _vel.orbit *= computeDecayCoeffFromFrametime(_constTimeDecayCoeff.orbit, times); _vel.roll *= computeDecayCoeffFromFrametime(_constTimeDecayCoeff.roll, times); _vel.pan *= computeDecayCoeffFromFrametime(_constTimeDecayCoeff.pan, times); @@ -1244,15 +1073,11 @@ void TouchInteraction::resetAfterInput() { _debugProperties.nFingers = 0; _debugProperties.interactionMode = "None"; #endif - if (_directTouchMode && !_selected.empty() && _lmSuccess) { - double spinDelta = _spinSensitivity / global::windowDelegate->averageDeltaTime(); - if (glm::length(_lastVel.orbit) > _orbitSpeedThreshold) { - // allow node to start "spinning" after direct-manipulation finger is let go - _vel.orbit = _lastVel.orbit * spinDelta; - } - } - - _lmSuccess = true; + // @TODO (emmbr 2023-02-03) Bring back feature that allows node to spin when + // the direct manipulaiton finger is let go. Should implement this using the + // orbitalnavigator's friction values. This also implies passing velocities to + // the orbitalnavigator, instead of setting the camera directly as is currently + // done in this class. // Reset variables _lastVel.orbit = glm::dvec2(0.0); @@ -1264,8 +1089,7 @@ void TouchInteraction::resetAfterInput() { _pinchInputs[0].clearInputs(); _pinchInputs[1].clearInputs(); - _selected.clear(); - _pickingSelected = nullptr; + _selectedNodeSurfacePoints.clear(); } // Reset all property values to default @@ -1275,51 +1099,31 @@ void TouchInteraction::resetPropertiesToDefault() { _disableRoll.set(false); _maxTapTime.set(300); _deceleratesPerSecond.set(240); - _touchScreenSize.set(55.0f); + _touchScreenSize.set(55.f); _tapZoomFactor.set(0.2f); _pinchZoomFactor.set(0.01f); - _nodeRadiusThreshold.set(0.2f); _rollAngleThreshold.set(0.025f); - _orbitSpeedThreshold.set(0.005f); - _spinSensitivity.set(1.0f); _zoomSensitivityExponential.set(1.025f); _inputStillThreshold.set(0.0005f); _centroidStillThreshold.set(0.0018f); _interpretPan.set(0.015f); - _slerpTime.set(3.0f); _friction.set(glm::vec4(0.025f, 0.025f, 0.02f, 0.02f)); } +void TouchInteraction::resetVelocities() { + _vel.orbit = glm::dvec2(0.0); + _vel.zoom = 0.0; + _vel.roll = 0.0; + _vel.pan = glm::dvec2(0.0); +} + void TouchInteraction::tap() { _tap = true; } -void TouchInteraction::touchActive(bool active) { - _touchActive = active; -} - -// Get & Setters -Camera* TouchInteraction::getCamera() { - return _camera; -} - -const SceneGraphNode* TouchInteraction::getFocusNode() { - return global::navigationHandler->orbitalNavigator().anchorNode(); -} void TouchInteraction::setCamera(Camera* camera) { _camera = camera; } -void TouchInteraction::setFocusNode(const SceneGraphNode* focusNode) { - if (focusNode) { - global::navigationHandler->orbitalNavigator().setAnchorNode( - focusNode->identifier() - ); - } - else { - global::navigationHandler->orbitalNavigator().setAnchorNode(""); - } -} - void FrameTimeAverage::updateWithNewFrame(double sample) { if (sample > 0.0005) { _samples[_index++] = sample; diff --git a/modules/touch/src/touchmarker.cpp b/modules/touch/src/touchmarker.cpp index e6401232e2..8d6329a4a8 100644 --- a/modules/touch/src/touchmarker.cpp +++ b/modules/touch/src/touchmarker.cpp @@ -72,7 +72,7 @@ TouchMarker::TouchMarker() , _radiusSize(RadiusInfo, 30.f, 0.f, 100.f) , _opacity(OpacityInfo, 0.8f, 0.f, 1.f) , _thickness(ThicknessInfo, 2.f, 0.f, 4.f) - , _color(ColorInfo, glm::vec3(0.96f, 0.2f, 0.2f), glm::vec3(0.f), glm::vec3(1.f)) + , _color(ColorInfo, glm::vec3(1.f), glm::vec3(0.f), glm::vec3(1.f)) { addProperty(_visible); addProperty(_radiusSize); diff --git a/modules/touch/touchmodule.cpp b/modules/touch/touchmodule.cpp index dfa9ada612..ce9acf6364 100644 --- a/modules/touch/touchmodule.cpp +++ b/modules/touch/touchmodule.cpp @@ -28,44 +28,98 @@ #include #include #include +#include #include #include #include +#include +#include +#include using namespace TUIO; namespace { - constexpr openspace::properties::Property::PropertyInfo TouchActiveInfo = { - "TouchActive", - "True if we want to use touch input as 3D navigation", - "Use this if we want to turn on or off Touch input navigation. " - "Disabling this will reset all current touch inputs to the navigation." + constexpr std::string_view _loggerCat = "TouchModule"; + + constexpr openspace::properties::Property::PropertyInfo EnableTouchInfo = { + "EnableTouchInteraction", + "Enable Touch Interaction", + "Use this property to turn on/off touch input navigation in the 3D scene. " + "Disabling will reset all current touch inputs to the navigation." + }; + + constexpr openspace::properties::Property::PropertyInfo EventsInfo = { + "DetectedTouchEvent", + "Detected Touch Event", + "True when there is an active touch event", + openspace::properties::Property::Visibility::Hidden + }; + + constexpr openspace::properties::Property::PropertyInfo + DefaultDirectTouchRenderableTypesInfo = + { + "DefaultDirectTouchRenderableTypes", + "Default Direct Touch Renderable Types", + "A list of renderable types that will automatically use the \'direct " + "manipulation\' scheme when interacted with, keeping the finger on a static " + "position on the interaction sphere of the object when touching. Good for " + "relatively spherical objects.", + openspace::properties::Property::Visibility::AdvancedUser }; } namespace openspace { TouchModule::TouchModule() : OpenSpaceModule("Touch") - , _touchActive(TouchActiveInfo, true) + , _touchIsEnabled(EnableTouchInfo, true) + , _hasActiveTouchEvent(EventsInfo, false) + , _defaultDirectTouchRenderableTypes(DefaultDirectTouchRenderableTypesInfo) { addPropertySubOwner(_touch); addPropertySubOwner(_markers); - addProperty(_touchActive); - _touchActive.onChange([&] { + addProperty(_touchIsEnabled); + _touchIsEnabled.onChange([&]() { _touch.resetAfterInput(); _lastTouchInputs.clear(); }); + + _hasActiveTouchEvent.setReadOnly(true); + addProperty(_hasActiveTouchEvent); + + _defaultDirectTouchRenderableTypes.onChange([&]() { + _sortedDefaultRenderableTypes.clear(); + for (const std::string& s : _defaultDirectTouchRenderableTypes.value()) { + ghoul::TemplateFactory* fRenderable = + FactoryManager::ref().factory(); + + if (!fRenderable->hasClass(s)) { + LWARNING(fmt::format( + "In property 'DefaultDirectTouchRenderableTypes': '{}' is not a " + "registered renderable type. Ignoring", s + )); + continue; + } + + _sortedDefaultRenderableTypes.insert(s); + } + }); + addProperty(_defaultDirectTouchRenderableTypes); } TouchModule::~TouchModule() { // intentionally left empty } -void TouchModule::internalInitialize(const ghoul::Dictionary& /*dictionary*/){ +bool TouchModule::isDefaultDirectTouchType(std::string_view renderableType) const { + return _sortedDefaultRenderableTypes.find(std::string(renderableType)) != + _sortedDefaultRenderableTypes.end(); +} + +void TouchModule::internalInitialize(const ghoul::Dictionary&){ _ear.reset(new TuioEar()); global::callback::initializeGL->push_back([&]() { - LDEBUGC("TouchModule", "Initializing TouchMarker OpenGL"); + LDEBUG("Initializing TouchMarker OpenGL"); _markers.initialize(); #ifdef WIN32 // We currently only support one window of touch input internally @@ -78,7 +132,7 @@ void TouchModule::internalInitialize(const ghoul::Dictionary& /*dictionary*/){ }); global::callback::deinitializeGL->push_back([&]() { - LDEBUGC("TouchMarker", "Deinitialize TouchMarker OpenGL"); + LDEBUG("Deinitialize TouchMarker OpenGL"); _markers.deinitialize(); }); @@ -104,26 +158,37 @@ void TouchModule::internalInitialize(const ghoul::Dictionary& /*dictionary*/){ global::callback::preSync->push_back([&]() { - if (!_touchActive) { + if (!_touchIsEnabled) { + return; + } + + OpenSpaceEngine::Mode mode = global::openSpaceEngine->currentMode(); + if (mode == OpenSpaceEngine::Mode::CameraPath || + mode == OpenSpaceEngine::Mode::SessionRecordingPlayback) + { + // Reset everything, to avoid problems once we process inputs again + _lastTouchInputs.clear(); + _touch.resetAfterInput(); + clearInputs(); return; } _touch.setCamera(global::navigationHandler->camera()); - _touch.setFocusNode(global::navigationHandler->orbitalNavigator().anchorNode()); - if (processNewInput() && global::windowDelegate->isMaster()) { + bool gotNewInput = processNewInput(); + if (gotNewInput && global::windowDelegate->isMaster()) { _touch.updateStateFromInput(_touchPoints, _lastTouchInputs); } else if (_touchPoints.empty()) { _touch.resetAfterInput(); } - // update lastProcessed + // Update last processed touch inputs _lastTouchInputs.clear(); for (const TouchInputHolder& points : _touchPoints) { _lastTouchInputs.emplace_back(points.latestInput()); } - // calculate the new camera state for this frame + // Calculate the new camera state for this frame _touch.step(global::windowDelegate->deltaTime()); clearInputs(); }); @@ -145,14 +210,15 @@ bool TouchModule::processNewInput() { removeTouchInput(removal); } - // Set touch property to active (to void mouse input, mainly for mtdev bridges) - _touch.touchActive(!_touchPoints.empty()); + bool touchHappened = !_touchPoints.empty(); + _hasActiveTouchEvent = touchHappened; - if (!_touchPoints.empty()) { + // Set touch property to active (to void mouse input, mainly for mtdev bridges) + if (touchHappened) { global::interactionMonitor->markInteraction(); } - // Erase old input id's that no longer exists + // Erase old input ids that no longer exist _lastTouchInputs.erase( std::remove_if( _lastTouchInputs.begin(), @@ -168,7 +234,7 @@ bool TouchModule::processNewInput() { } ), _lastTouchInputs.end() - ); + ); if (_tap) { _touch.tap(); @@ -180,9 +246,11 @@ bool TouchModule::processNewInput() { if (_touchPoints.size() == _lastTouchInputs.size() && !_touchPoints.empty()) { + // @TODO (emmbr26, 2023-02-03) Looks to me like this code will always return + // true? That's a bit weird and should probably be investigated bool newInput = true; - // go through list and check if the last registrered time is newer than the one in - // lastProcessed (last frame) + // Go through list and check if the last registrered time is newer than the + // last processed touch inputs (last frame) std::for_each( _lastTouchInputs.begin(), _lastTouchInputs.end(), @@ -197,7 +265,8 @@ bool TouchModule::processNewInput() { if (!holder->isMoving()) { newInput = true; } - }); + } + ); return newInput; } else { diff --git a/modules/touch/touchmodule.h b/modules/touch/touchmodule.h index d217cd4972..7600a42c16 100644 --- a/modules/touch/touchmodule.h +++ b/modules/touch/touchmodule.h @@ -27,6 +27,8 @@ #include #include +#include +#include #include #include #include @@ -41,9 +43,15 @@ class Win32TouchHook; class TouchModule : public OpenSpaceModule { public: + constexpr static const char* Name = "Touch"; + TouchModule(); ~TouchModule(); + // Function to check if the given renderable type is one that should + // use direct maniuplation + bool isDefaultDirectTouchType(std::string_view renderableType) const; + protected: void internalInitialize(const ghoul::Dictionary& dictionary) override; @@ -64,7 +72,13 @@ private: std::vector _deferredRemovals; std::vector _lastTouchInputs; - properties::BoolProperty _touchActive; + properties::BoolProperty _touchIsEnabled; + properties::BoolProperty _hasActiveTouchEvent; + properties::StringListProperty _defaultDirectTouchRenderableTypes; + + // A sorted version of the list in the property + std::set _sortedDefaultRenderableTypes; + // contains an id and the Point that was processed last frame glm::ivec2 _webPositionCallback = glm::ivec2(0); #ifdef WIN32 diff --git a/src/navigation/orbitalnavigator.cpp b/src/navigation/orbitalnavigator.cpp index 276f02c0a4..fa6e0ec4af 100644 --- a/src/navigation/orbitalnavigator.cpp +++ b/src/navigation/orbitalnavigator.cpp @@ -539,6 +539,20 @@ glm::quat OrbitalNavigator::anchorNodeToCameraRotation() const { return glm::quat(invWorldRotation) * glm::quat(_camera->rotationQuaternion()); } + +glm::dvec3 OrbitalNavigator::pushToSurfaceOfAnchor( + const glm::dvec3& cameraPosition) const +{ + const SurfacePositionHandle posHandle = + calculateSurfacePositionHandle(*_anchorNode, cameraPosition); + + return pushToSurface( + cameraPosition, + _anchorNode->worldPosition(), + posHandle + ); +} + void OrbitalNavigator::resetVelocities() { _mouseStates.resetVelocities(); _joystickStates.resetVelocities(); @@ -703,7 +717,6 @@ void OrbitalNavigator::updateCameraStateFromStates(double deltaTime) { // Perform the vertical movements based on user input pose.position = translateVertically(deltaTime, pose.position, anchorPos, posHandle); pose.position = pushToSurface( - _minimumAllowedDistance, pose.position, anchorPos, posHandle @@ -992,6 +1005,10 @@ bool OrbitalNavigator::hasRollFriction() const { return _friction.roll; } +double OrbitalNavigator::minAllowedDistance() const { + return _minimumAllowedDistance; +} + OrbitalNavigator::CameraRotationDecomposition OrbitalNavigator::decomposeCameraRotationSurface(CameraPose cameraPose, const SceneGraphNode& reference) @@ -1473,8 +1490,7 @@ glm::dquat OrbitalNavigator::rotateHorizontally(double deltaTime, return mouseCameraRollRotation * globalCameraRotation; } -glm::dvec3 OrbitalNavigator::pushToSurface(double minHeightAboveGround, - const glm::dvec3& cameraPosition, +glm::dvec3 OrbitalNavigator::pushToSurface(const glm::dvec3& cameraPosition, const glm::dvec3& objectPosition, const SurfacePositionHandle& positionHandle) const { @@ -1495,7 +1511,7 @@ glm::dvec3 OrbitalNavigator::pushToSurface(double minHeightAboveGround, glm::sign(dot(actualSurfaceToCamera, referenceSurfaceOutDirection)); return cameraPosition + referenceSurfaceOutDirection * - glm::max(minHeightAboveGround - surfaceToCameraSigned, 0.0); + glm::max(_minimumAllowedDistance - surfaceToCameraSigned, 0.0); } glm::dquat OrbitalNavigator::interpolateRotationDifferential(double deltaTime, @@ -1520,7 +1536,7 @@ glm::dquat OrbitalNavigator::interpolateRotationDifferential(double deltaTime, SurfacePositionHandle OrbitalNavigator::calculateSurfacePositionHandle( const SceneGraphNode& node, - const glm::dvec3 cameraPositionWorldSpace) + const glm::dvec3& cameraPositionWorldSpace) const { ghoul_assert( glm::length(cameraPositionWorldSpace) > 0.0, diff --git a/src/rendering/renderable.cpp b/src/rendering/renderable.cpp index 6f063a4a4f..bfdae64f07 100644 --- a/src/rendering/renderable.cpp +++ b/src/rendering/renderable.cpp @@ -240,6 +240,10 @@ double Renderable::interactionSphere() const { return _interactionSphere; } +std::string_view Renderable::typeAsString() const { + return _renderableType; +} + SurfacePositionHandle Renderable::calculateSurfacePositionHandle( const glm::dvec3& targetModelSpace) const { diff --git a/src/scene/scenegraphnode.cpp b/src/scene/scenegraphnode.cpp index 44abce2c67..ed4dc79efe 100644 --- a/src/scene/scenegraphnode.cpp +++ b/src/scene/scenegraphnode.cpp @@ -160,25 +160,40 @@ namespace { openspace::properties::Property::Visibility::Developer }; + constexpr openspace::properties::Property::PropertyInfo + SupportsDirectInteractionInfo = + { + "SupportsDirectInteraction", + "Supports Direct Interaction", + "Only relevant when using touch interaction. If true, the \'direct " + "manipulation\' scheme will be used when interacting with this scene graph " + "node, meaning that the positions on the interaction sphere that intersects " + "with the touch points will directly follow the motion of the touch points. " + "Works best for objects that have an interaction sphere of about the same size " + "as the bounding sphere, and that are somewhat spherical. Note that using this " + "feature might significalty reduce the performance.", + openspace::properties::Property::Visibility::AdvancedUser + }; + struct [[codegen::Dictionary(SceneGraphNode)]] Parameters { - // The identifier of this scenegraph node. This name must be unique among all + // The identifier of this scene graph node. This name must be unique among all // scene graph nodes that are loaded in a specific scene. If a duplicate is // detected the loading of the node will fail, as will all childing that depend on // the node. The identifier must not contain any whitespaces or '.' std::string identifier; - // This names the parent of the currently specified scenegraph node. The parent + // This names the parent of the currently specified scene graph node. The parent // must already exist in the scene graph. If not specified, the node will be - // attached to the root of the scenegraph + // attached to the root of the scene graph std::optional parent [[codegen::annotation( - "If specified, this must be a name for another scenegraph node" + "If specified, this must be a name for another scene graph node" )]]; - // The renderable that is to be created for this scenegraph node. A renderable is - // a component of a scenegraph node that will lead to some visual result on the + // The renderable that is to be created for this scene graph node. A renderable is + // a component of a scene graph node that will lead to some visual result on the // screen. The specifics heavily depend on the 'Type' of the renderable. If no - // Renderable is specified, this scenegraph node is an internal node and can be + // Renderable is specified, this scene graph node is an internal node and can be // used for either group children, or apply common transformations to a group of // children std::optional renderable [[codegen::reference("renderable")]]; @@ -189,27 +204,30 @@ namespace { // [[codegen::verbatim(InteractionSphereInfo.description)]] std::optional interactionSphere; + // [[codegen::verbatim(SupportsDirectInteractionInfo.description)]] + std::optional supportsDirectInteraction; + struct Transform { - // This node describes a translation that is applied to the scenegraph node + // This node describes a translation that is applied to the scene graph node // and all its children. Depending on the 'Type' of the translation, this can // either be a static translation or a time-varying one std::optional translation [[codegen::reference("core_transform_translation")]]; - // This nodes describes a rotation that is applied to the scenegraph node and + // This nodes describes a rotation that is applied to the scene graph node and // all its children. Depending on the 'Type' of the rotation, this can either // be a static rotation or a time-varying one std::optional rotation [[codegen::reference("core_transform_rotation")]]; - // This node describes a scaling that is applied to the scenegraph node and + // This node describes a scaling that is applied to the scene graph node and // all its children. Depending on the 'Type' of the scaling, this can either // be a static scaling or a time-varying one std::optional scale [[codegen::reference("core_transform_scaling")]]; }; - // This describes a set of transformations that are applied to this scenegraph + // This describes a set of transformations that are applied to this scene graph // node and all of its children. There are only three possible values // corresponding to a 'Translation', a 'Rotation', and a 'Scale' std::optional transform; @@ -248,7 +266,7 @@ namespace { std::optional timeFrame [[codegen::reference("core_time_frame")]]; - // A tag or list of tags that can be used to reference to a group of scenegraph + // A tag or list of tags that can be used to reference to a group of scene graph // nodes. std::optional>> tag; @@ -258,7 +276,7 @@ namespace { std::optional name; // If this value is specified, this '/' separated URI specifies the location - // of this scenegraph node in a GUI representation, for instance + // of this scene graph node in a GUI representation, for instance // '/SolarSystem/Earth/Moon' std::optional path; @@ -266,12 +284,12 @@ namespace { std::optional description; // If this value is specified, GUI applications are incouraged to ignore this - // scenegraph node. This is most useful to trim collective lists of nodes and + // scene graph node. This is most useful to trim collective lists of nodes and // not display, for example, barycenters std::optional hidden; }; // Additional information that is passed to GUI applications. These are all hints - // and do not have any impact on the actual function of the scenegraph node + // and do not have any impact on the actual function of the scene graph node std::optional gui [[codegen::key("GUI")]]; }; #include "scenegraphnode_codegen.cpp" @@ -513,6 +531,7 @@ SceneGraphNode::SceneGraphNode() , _screenSizeRadius(ScreenSizeRadiusInfo, 0) , _visibilityDistance(VisibilityDistanceInfo, 6e10f) , _showDebugSphere(ShowDebugSphereInfo, false) + , _supportsDirectInteraction(SupportsDirectInteractionInfo, false) { addProperty(_computeScreenSpaceValues); addProperty(_screenSpacePosition); @@ -552,6 +571,8 @@ SceneGraphNode::SceneGraphNode() addProperty(_approachFactor); addProperty(_showDebugSphere); + + addProperty(_supportsDirectInteraction); } SceneGraphNode::~SceneGraphNode() {} @@ -1207,6 +1228,10 @@ double SceneGraphNode::approachFactor() const { return _approachFactor; } +bool SceneGraphNode::supportsDirectInteraction() const { + return _supportsDirectInteraction; +} + const Renderable* SceneGraphNode::renderable() const { return _renderable.get(); }