From 877cf3c1f88820d9b6c9751c9ab792d40db74d66 Mon Sep 17 00:00:00 2001 From: Nolan Kramer Date: Thu, 8 May 2025 17:54:23 -0700 Subject: [PATCH 1/3] Add Turntable controller source and runner --- examples/app/CMakeLists.txt | 1 + .../app/vsgturntablecamera/CMakeLists.txt | 15 + examples/app/vsgturntablecamera/Turntable.cpp | 640 ++++++++++++++++++ examples/app/vsgturntablecamera/Turntable.hpp | 152 +++++ .../vsgturntablecamera/vsgturntablecamera.cpp | 414 +++++++++++ 5 files changed, 1222 insertions(+) create mode 100644 examples/app/vsgturntablecamera/CMakeLists.txt create mode 100644 examples/app/vsgturntablecamera/Turntable.cpp create mode 100644 examples/app/vsgturntablecamera/Turntable.hpp create mode 100644 examples/app/vsgturntablecamera/vsgturntablecamera.cpp diff --git a/examples/app/CMakeLists.txt b/examples/app/CMakeLists.txt index 1df0a79d..eb229241 100644 --- a/examples/app/CMakeLists.txt +++ b/examples/app/CMakeLists.txt @@ -8,6 +8,7 @@ add_subdirectory(vsgrendertotexturearray) add_subdirectory(vsgscreenshot) add_subdirectory(vsgoffscreenshot) add_subdirectory(vsgsubpass) +add_subdirectory(vsgturntablecamera) add_subdirectory(vsgskybox) add_subdirectory(vsgviewer) add_subdirectory(vsgdeviceselection) diff --git a/examples/app/vsgturntablecamera/CMakeLists.txt b/examples/app/vsgturntablecamera/CMakeLists.txt new file mode 100644 index 00000000..a0b0c96d --- /dev/null +++ b/examples/app/vsgturntablecamera/CMakeLists.txt @@ -0,0 +1,15 @@ +set(SOURCES + Turntable.cpp + vsgturntablecamera.cpp +) + +add_executable(vsgturntablecamera ${SOURCES}) + +target_link_libraries(vsgturntablecamera vsg::vsg) + +if (vsgXchange_FOUND) + target_compile_definitions(vsgturntablecamera PRIVATE vsgXchange_FOUND) + target_link_libraries(vsgturntablecamera vsgXchange::vsgXchange) +endif() + +install(TARGETS vsgturntablecamera RUNTIME DESTINATION bin) diff --git a/examples/app/vsgturntablecamera/Turntable.cpp b/examples/app/vsgturntablecamera/Turntable.cpp new file mode 100644 index 00000000..6a3d1992 --- /dev/null +++ b/examples/app/vsgturntablecamera/Turntable.cpp @@ -0,0 +1,640 @@ +#include "Turntable.hpp" + +Turntable::Turntable(vsg::ref_ptr camera) : + _camera(camera), + _lookAt(camera->viewMatrix.cast()), + _keyboard(vsg::Keyboard::create()) +{ + if (!_lookAt) + { + _lookAt = new vsg::LookAt; + } +} + +/// compute non dimensional window coordinate (-1,1) from event coords +vsg::dvec2 Turntable::ndc(const vsg::PointerEvent& event) +{ + auto renderArea = _camera->getRenderArea(); + auto [x, y] = cameraRenderAreaCoordinates(event); + + double aspectRatio = static_cast(renderArea.extent.width) / static_cast(renderArea.extent.height); + vsg::dvec2 v( + (renderArea.extent.width > 0) ? (static_cast(x - renderArea.offset.x) / static_cast(renderArea.extent.width) * 2.0 - 1.0) * aspectRatio : 0.0, + (renderArea.extent.height > 0) ? static_cast(y - renderArea.offset.y) / static_cast(renderArea.extent.height) * 2.0 - 1.0 : 0.0); + return v; +} + +/// compute turntable coordinate from event coords +vsg::dvec3 Turntable::ttc(const vsg::PointerEvent& event) +{ + vsg::dvec2 v = ndc(event); + + // Compute the z coordinate for projection onto a sphere + double x2 = v.x * v.x; + double y2 = v.y * v.y; + double r2 = x2 + y2; + + double z; + if (r2 < 0.5) // Inside sphere + { + z = sqrt(1.0 - r2); + } else { // On hyperbola + z = 0.5 / sqrt(r2); + } + + return vsg::dvec3(v.x, v.y, z); +} + +void Turntable::apply(vsg::KeyPressEvent& keyPress) +{ + if (_keyboard) keyPress.accept(*_keyboard); + + if (!_hasKeyboardFocus || keyPress.handled || !eventRelevant(keyPress)) return; + + if (auto itr = keyViewpointMap.find(keyPress.keyBase); itr != keyViewpointMap.end()) + { + _previousTime = keyPress.time; + + setViewpoint(itr->second.lookAt, itr->second.duration); + + keyPress.handled = true; + } +} + +void Turntable::apply(vsg::KeyReleaseEvent& keyRelease) +{ + if (_keyboard) keyRelease.accept(*_keyboard); +} + +void Turntable::apply(vsg::FocusInEvent& focusIn) +{ + if (_keyboard) focusIn.accept(*_keyboard); +} + +void Turntable::apply(vsg::FocusOutEvent& focusOut) +{ + if (_keyboard) focusOut.accept(*_keyboard); +} + +void Turntable::apply(vsg::ButtonPressEvent& buttonPress) +{ + if (buttonPress.handled || !eventRelevant(buttonPress)) + { + _hasKeyboardFocus = false; + return; + } + + _hasPointerFocus = _hasKeyboardFocus = withinRenderArea(buttonPress); + _lastPointerEventWithinRenderArea = _hasPointerFocus; + + if (buttonPress.mask & rotateButtonMask) + _updateMode = ROTATE; + else if (buttonPress.mask & panButtonMask) + _updateMode = PAN; + else if (buttonPress.mask & zoomButtonMask) + _updateMode = ZOOM; + else + _updateMode = INACTIVE; + + if (_hasPointerFocus) buttonPress.handled = true; + + _zoomPreviousRatio = 0.0; + _pan.set(0.0, 0.0); + _rotateAngle = 0.0; + + _previousPointerEvent = &buttonPress; +} + +void Turntable::apply(vsg::ButtonReleaseEvent& buttonRelease) +{ + if (buttonRelease.handled || !eventRelevant(buttonRelease)) return; + + if (!windowOffsets.empty() && windowOffsets.count(buttonRelease.window) == 0) return; + + if (supportsThrow) _thrown = _previousPointerEvent && (buttonRelease.time == _previousPointerEvent->time); + + _lastPointerEventWithinRenderArea = withinRenderArea(buttonRelease); + _hasPointerFocus = false; + + _previousPointerEvent = &buttonRelease; +} + +void Turntable::apply(vsg::MoveEvent& moveEvent) +{ + if (!eventRelevant(moveEvent)) return; + + _lastPointerEventWithinRenderArea = withinRenderArea(moveEvent); + + if (moveEvent.handled || !_hasPointerFocus) return; + + vsg::dvec2 new_ndc = ndc(moveEvent); + vsg::dvec3 new_ttc = ttc(moveEvent); + + if (!_previousPointerEvent) _previousPointerEvent = &moveEvent; + + vsg::dvec2 prev_ndc = ndc(*_previousPointerEvent); + vsg::dvec3 prev_ttc = ttc(*_previousPointerEvent); + +#if 1 +vsg::dvec2 control_ndc = new_ndc; +vsg::dvec3 control_ttc = new_ttc; +#else +vsg::dvec2 control_ndc = (new_ndc + prev_ndc) * 0.5; +vsg::dvec3 control_ttc = (new_ttc + prev_ttc) * 0.5; +#endif + + double dt = std::chrono::duration(moveEvent.time - _previousPointerEvent->time).count(); + _previousDelta = dt; + + double scale = 1.0; + //if (_previousTime > _previousPointerEvent->time) scale = std::chrono::duration(moveEvent.time - _previousTime).count() / dt; + // scale *= 2.0; + + _previousTime = moveEvent.time; + + if (moveEvent.mask & rotateButtonMask) + { + _updateMode = ROTATE; + + moveEvent.handled = true; + + // Calculate deltas for horizontal and vertical rotations + vsg::dvec2 delta = control_ndc - prev_ndc; + + double rotationFactor = 1.0; + if (delta.x != 0.0) + { + // Horizontal rotation around the up axis (Y) + double rotateAngle = -delta.x * rotationFactor; + vsg::dvec3 rotateAxis(0.0, 1.0, 0.0); + rotate(rotateAngle * scale, rotateAxis); + } + + if (delta.y != 0.0) + { + // Vertical rotation around the local side vector + vsg::dvec3 lookVector = vsg::normalize(_lookAt->center - _lookAt->eye); + vsg::dvec3 sideVector = vsg::normalize(vsg::cross(lookVector, _lookAt->up)); + + // The negative sign makes upward mouse movement tilt the camera up + double rotateAngle = -delta.y * rotationFactor; + rotate(rotateAngle * scale, sideVector); + } + } + else if (moveEvent.mask & panButtonMask) + { + _updateMode = PAN; + moveEvent.handled = true; + vsg::dvec2 delta = control_ndc - prev_ndc; + _pan = delta; + pan(delta * scale); + } + else if (moveEvent.mask & zoomButtonMask) + { + _updateMode = ZOOM; + moveEvent.handled = true; + vsg::dvec2 delta = control_ndc - prev_ndc; + if (delta.y != 0.0) + { + _zoomPreviousRatio = zoomScale * 2.0 * delta.y; + zoom(_zoomPreviousRatio * scale); + } + } + + _thrown = false; + _previousPointerEvent = &moveEvent; +} + +void Turntable::apply(vsg::ScrollWheelEvent& scrollWheel) +{ + if (scrollWheel.handled || !eventRelevant(scrollWheel) || !_lastPointerEventWithinRenderArea) return; + + scrollWheel.handled = true; + + zoom(scrollWheel.delta.y * 0.1); +} + +void Turntable::apply(vsg::TouchDownEvent& touchDown) +{ + if (!eventRelevant(touchDown)) return; + + _previousTouches[touchDown.id] = &touchDown; + switch (touchDown.id) + { + case 0: { + if (_previousTouches.size() == 1) + { + vsg::ref_ptr w = touchDown.window; + vsg::ref_ptr evt = vsg::ButtonPressEvent::create( + w, + touchDown.time, + touchDown.x, + touchDown.y, + touchMappedToButtonMask, + touchDown.id); + apply(*evt.get()); + } + break; + } + case 1: { + _prevZoomTouchDistance = 0.0; + if (touchDown.id == 0 && _previousTouches.count(1)) + { + const auto& prevTouch1 = _previousTouches[1]; + auto a = std::abs(static_cast(prevTouch1->x) - touchDown.x); + auto b = std::abs(static_cast(prevTouch1->y) - touchDown.y); + if (a > 0 || b > 0) + _prevZoomTouchDistance = sqrt(a * a + b * b); + } + break; + } + } +} + +void Turntable::apply(vsg::TouchUpEvent& touchUp) +{ + if (!eventRelevant(touchUp)) return; + + if (touchUp.id == 0 && _previousTouches.size() == 1) + { + vsg::ref_ptr w = touchUp.window; + vsg::ref_ptr evt = vsg::ButtonReleaseEvent::create( + w, + touchUp.time, + touchUp.x, + touchUp.y, + touchMappedToButtonMask, + touchUp.id); + apply(*evt.get()); + } + _previousTouches.erase(touchUp.id); +} + +void Turntable::apply(vsg::TouchMoveEvent& touchMove) +{ + if (!eventRelevant(touchMove)) return; + + vsg::ref_ptr w = touchMove.window; + switch (_previousTouches.size()) + { + case 1: { + // Rotate + vsg::ref_ptr evt = vsg::MoveEvent::create( + w, + touchMove.time, + touchMove.x, + touchMove.y, + touchMappedToButtonMask); + apply(*evt.get()); + break; + } + case 2: { + if (touchMove.id == 0 && _previousTouches.count(0)) + { + // Zoom + const auto& prevTouch1 = _previousTouches[1]; + auto a = std::abs(static_cast(prevTouch1->x) - touchMove.x); + auto b = std::abs(static_cast(prevTouch1->y) - touchMove.y); + if (a > 0 || b > 0) + { + auto touchZoomDistance = sqrt(a * a + b * b); + if (_prevZoomTouchDistance && touchZoomDistance > 0) + { + auto zoomLevel = touchZoomDistance / _prevZoomTouchDistance; + if (zoomLevel < 1) + zoomLevel = -(1 / zoomLevel); + zoomLevel *= 0.1; + zoom(zoomLevel); + } + _prevZoomTouchDistance = touchZoomDistance; + } + } + break; + } + } + _previousTouches[touchMove.id] = &touchMove; +} + +void Turntable::apply(vsg::FrameEvent& frame) +{ + // Handle keyboard input when focused + if (_hasKeyboardFocus && _keyboard) + { + auto times2speed = [](std::pair duration) -> double { + if (duration.first <= 0.0) return 0.0; + double speed = duration.first >= 1.0 ? 1.0 : duration.first; + + if (duration.second > 0.0) + { + // key has been released so slow down + speed -= duration.second; + return speed > 0.0 ? speed : 0.0; + } + else + { + // key still pressed so return speed based on duration of press + return speed; + } + }; + + double speed = 0.0; + vsg::dvec3 move(0.0, 0.0, 0.0); + if ((speed = times2speed(_keyboard->times(moveLeftKey))) != 0.0) move.x += -speed; + if ((speed = times2speed(_keyboard->times(moveRightKey))) != 0.0) move.x += speed; + if ((speed = times2speed(_keyboard->times(moveUpKey))) != 0.0) move.y += speed; + if ((speed = times2speed(_keyboard->times(moveDownKey))) != 0.0) move.y += -speed; + if ((speed = times2speed(_keyboard->times(moveForwardKey))) != 0.0) move.z += speed; + if ((speed = times2speed(_keyboard->times(moveBackwardKey))) != 0.0) move.z += -speed; + + vsg::dvec3 rot(0.0, 0.0, 0.0); + // For turntable, we primarily care about the horizontal rotation (around up vector) + if ((speed = times2speed(_keyboard->times(turnLeftKey))) != 0.0) rot.x += speed; + if ((speed = times2speed(_keyboard->times(turnRightKey))) != 0.0) rot.x -= speed; + // Vertical rotation (tilt) + if ((speed = times2speed(_keyboard->times(pitchUpKey))) != 0.0) rot.y += speed; + if ((speed = times2speed(_keyboard->times(pitchDownKey))) != 0.0) rot.y -= speed; + // Typically turntable doesn't have roll, but keeping minimal functionality + if ((speed = times2speed(_keyboard->times(rollLeftKey))) != 0.0) rot.z -= speed * 0.2; // Reduced influence + if ((speed = times2speed(_keyboard->times(rollRightKey))) != 0.0) rot.z += speed * 0.2; // Reduced influence + + if (rot || move) + { + double scale = std::chrono::duration(frame.time - _previousTime).count(); + double scaleTranslation = scale * 0.2 * vsg::length(_lookAt->center - _lookAt->eye); + double scaleRotation = scale * 0.5; + + vsg::dvec3 upVector = _lookAt->up; + vsg::dvec3 lookVector = vsg::normalize(_lookAt->center - _lookAt->eye); + vsg::dvec3 sideVector = vsg::normalize(vsg::cross(lookVector, upVector)); + + // For turntable, we primarily translate in the horizontal plane + vsg::dvec3 horizontalMove = sideVector * (scaleTranslation * move.x) + + vsg::cross(upVector, sideVector) * (scaleTranslation * move.z); + // Vertical movement is still allowed + vsg::dvec3 verticalMove = upVector * (scaleTranslation * move.y); + vsg::dvec3 delta = horizontalMove + verticalMove; + + // Store the original distance for maintaining orbit radius + double distanceFromCenter = vsg::length(_lookAt->eye - _lookAt->center); + + // Get the global up vector (assuming Z-up, change if needed) + vsg::dvec3 globalUp(0.0, 0.0, 1.0); + + // Apply rotation with gimbal lock avoidance + applyRotationWithGimbalAvoidance(rot, scaleRotation, globalUp); + + // Apply translation + _lookAt->eye += delta; + _lookAt->center += delta; + + _thrown = false; + } + } + + if (_endLookAt) + { + double timeSinceOfAnimation = std::chrono::duration(frame.time - _startTime).count(); + if (timeSinceOfAnimation < _animationDuration) + { + double r = vsg::smoothstep(0.0, 1.0, timeSinceOfAnimation / _animationDuration); + + _lookAt->eye = vsg::mix(_startLookAt->eye, _endLookAt->eye, r); + _lookAt->center = vsg::mix(_startLookAt->center, _endLookAt->center, r); + + double angle = acos(vsg::dot(_startLookAt->up, _endLookAt->up) / (vsg::length(_startLookAt->up) * vsg::length(_endLookAt->up))); + if (angle > 1.0e-6) + { + auto rotation = vsg::rotate(angle * r, vsg::normalize(vsg::cross(_startLookAt->up, _endLookAt->up))); + _lookAt->up = rotation * _startLookAt->up; + } + else + { + _lookAt->up = _endLookAt->up; + } + } + else + { + _lookAt->eye = _endLookAt->eye; + _lookAt->center = _endLookAt->center; + _lookAt->up = _endLookAt->up; + + _endLookAt = nullptr; + _animationDuration = 0.0; + } + } + else if (_thrown) + { + double scale = _previousDelta > 0.0 ? std::chrono::duration(frame.time - _previousTime).count() / _previousDelta : 0.0; + switch (_updateMode) + { + case (ROTATE): + // For turntable thrown rotation, avoid gimbal lock + { + vsg::dvec3 globalUp(0.0, 0.0, 1.0); // Assuming Z-up + vsg::dvec3 lookVector = vsg::normalize(_lookAt->center - _lookAt->eye); + + // Decompose rotation into horizontal and vertical components + double verticalComponent = vsg::dot(_rotateAxis, globalUp); + + // Create rotation vectors that will help avoid gimbal lock + vsg::dvec3 horizontalAxis, verticalAxis; + computeRotationAxesWithGimbalAvoidance(horizontalAxis, verticalAxis, lookVector, globalUp); + + // Apply horizontal rotation + double horizontalAngle = _rotateAngle * (1.0 - abs(verticalComponent)) * scale; + rotate(horizontalAngle, horizontalAxis); + + // Apply vertical rotation + double verticalAngle = _rotateAngle * verticalComponent * scale; + rotate(verticalAngle, verticalAxis); + } + break; + case (PAN): + // For turntable, we primarily pan in the horizontal plane + { + vsg::dvec3 globalUp(0.0, 0.0, 1.0); + vsg::dvec3 pan3D(_pan.x, _pan.y, 0.0); + vsg::dvec3 horizontalPan = pan3D - globalUp * vsg::dot(pan3D, globalUp); + auto scaled = horizontalPan * scale + globalUp * vsg::dot(pan3D, globalUp) * scale * 0.5; + pan(vsg::dvec2(scaled.x, scaled.y)); + } + break; + case (ZOOM): + zoom(_zoomPreviousRatio * scale); + break; + default: + break; + } + } + + _previousTime = frame.time; +} + +// Method implementing gimbal lock avoidance for turntable controls +void Turntable::applyRotationWithGimbalAvoidance(const vsg::dvec3& rot, double scaleRotation, const vsg::dvec3& globalUp) +{ + // Get current view vectors + vsg::dvec3 lookVector = vsg::normalize(_lookAt->center - _lookAt->eye); + + // Compute the rotation axes with gimbal lock avoidance + vsg::dvec3 horizontalAxis, verticalAxis; + computeRotationAxesWithGimbalAvoidance(horizontalAxis, verticalAxis, lookVector, globalUp); + + // Apply horizontal rotation (around a modified up vector that avoids gimbal lock) + double horizontalAngle = rot.x * scaleRotation; + rotate(horizontalAngle, horizontalAxis); + + // Apply vertical rotation (around the side vector) + double verticalAngle = rot.y * scaleRotation; + rotate(verticalAngle, verticalAxis); + + // Apply minimal roll if specified + if (rot.z != 0.0) + { + double rollAngle = rot.z * scaleRotation; + rotate(rollAngle, lookVector); + } +} + +// Implementation of gimbal lock avoidance algorithm +void Turntable::computeRotationAxesWithGimbalAvoidance(vsg::dvec3& horizontalAxis, vsg::dvec3& verticalAxis, + const vsg::dvec3& lookVector, const vsg::dvec3& globalUp) +{ + // 1. Compute how close we are to gimbal lock + double viewVerticalAlignment = abs(vsg::dot(lookVector, globalUp)); + + // The closer viewVerticalAlignment is to 1.0, the closer we are to gimbal lock + + // 2. Compute the rotation axes: + + // 2.1. Standard turntable axes + vsg::dvec3 standardUp = globalUp; // Standard up vector for horizontal rotation + vsg::dvec3 standardSide = vsg::normalize(vsg::cross(lookVector, standardUp)); // Standard side vector for vertical rotation + + // 2.2. Rotated axes + // Cross product of global up and look direction gives us an axis perpendicular to both + vsg::dvec3 rotatedSide = vsg::normalize(vsg::cross(globalUp, lookVector)); + // Another cross product gives us a "rotated horizon" axis + vsg::dvec3 rotatedUp = vsg::normalize(vsg::cross(rotatedSide, lookVector)); + + // 3. Blend between the standard and rotated axes based on gimbal lock severity + + // This blending factor increases as we approach gimbal lock + // Using a smooth curve that increases more rapidly as we approach gimbal lock + double blendFactor = vsg::smoothstep(0.7, 0.98, viewVerticalAlignment); + + // Blend the up vectors (horizontal rotation axis) + horizontalAxis = vsg::normalize(vsg::mix(standardUp, rotatedUp, blendFactor)); + + // For vertical rotation, we blend between standard side vector and a computed side vector + // that's based on our current blended up vector (to ensure orthogonality) + vsg::dvec3 blendedSide = vsg::normalize(vsg::cross(lookVector, horizontalAxis)); + verticalAxis = vsg::normalize(vsg::mix(standardSide, blendedSide, blendFactor)); + + // Ensure axes are normalized + horizontalAxis = vsg::normalize(horizontalAxis); + verticalAxis = vsg::normalize(verticalAxis); +} + +void Turntable::rotate(double angle, const vsg::dvec3& axis) +{ + if (abs(angle) < 1e-10 || vsg::length(axis) < 1e-10) + return; + + // Create rotation matrix around the provided axis + vsg::dmat4 matrix = vsg::translate(_lookAt->center) * + vsg::rotate(angle, vsg::normalize(axis)) * + vsg::translate(-_lookAt->center); + + // Apply rotation + _lookAt->eye = matrix * _lookAt->eye; + _lookAt->up = vsg::normalize(matrix * (_lookAt->eye + _lookAt->up) - matrix * _lookAt->eye); +} + +void Turntable::zoom(double ratio) +{ + vsg::dvec3 lookVector = _lookAt->center - _lookAt->eye; + _lookAt->eye = _lookAt->eye + lookVector * ratio; +} + +void Turntable::pan(const vsg::dvec2& delta) +{ + vsg::dvec3 lookVector = _lookAt->center - _lookAt->eye; + vsg::dvec3 lookNormal = normalize(lookVector); + vsg::dvec3 upNormal = _lookAt->up; + vsg::dvec3 sideNormal = cross(lookNormal, upNormal); + + double distance = vsg::length(lookVector); + distance *= 0.25; + + vsg::dvec3 translation = sideNormal * (-delta.x * distance) + upNormal * (delta.y * distance); + + _lookAt->eye = _lookAt->eye + translation; + _lookAt->center = _lookAt->center + translation; +} + +std::pair Turntable::cameraRenderAreaCoordinates(const vsg::PointerEvent& pointerEvent) const +{ + if (!pointerEvent.window) return {0, 0}; + + auto itr = windowOffsets.find(pointerEvent.window); + if (itr != windowOffsets.end()) + { + auto coords = itr->second; + return {pointerEvent.x + coords.x, pointerEvent.y + coords.y}; + } + + return {pointerEvent.x, pointerEvent.y}; +} + +bool Turntable::withinRenderArea(const vsg::PointerEvent& pointerEvent) const +{ + if (!_camera || !_camera->viewportState) return false; + + auto [x, y] = cameraRenderAreaCoordinates(pointerEvent); + + const auto& viewport = _camera->viewportState->getViewport(); + return (x >= viewport.x && x < viewport.x + viewport.width && + y >= viewport.y && y < viewport.y + viewport.height); +} + +bool Turntable::eventRelevant(const vsg::WindowEvent& event) const +{ + if (windowOffsets.empty()) return true; + + return (windowOffsets.count(event.window) > 0); +} + +void Turntable::addWindow(vsg::ref_ptr window, const vsg::ivec2& offset) +{ + windowOffsets[vsg::observer_ptr(window)] = offset; +} + +void Turntable::addKeyViewpoint(vsg::KeySymbol key, vsg::ref_ptr lookAt, double duration) +{ + keyViewpointMap[key].lookAt = lookAt; + keyViewpointMap[key].duration = duration; +} + +void Turntable::setViewpoint(vsg::ref_ptr lookAt, double duration) +{ + if (!lookAt) return; + + _thrown = false; + + if (duration == 0.0) + { + _lookAt->eye = lookAt->eye; + _lookAt->center = lookAt->center; + _lookAt->up = lookAt->up; + + _startLookAt = nullptr; + _endLookAt = nullptr; + _animationDuration = 0.0; + } else { + _startTime = _previousTime; + _startLookAt = vsg::LookAt::create(*_lookAt); + _endLookAt = lookAt; + _animationDuration = duration; + } +} diff --git a/examples/app/vsgturntablecamera/Turntable.hpp b/examples/app/vsgturntablecamera/Turntable.hpp new file mode 100644 index 00000000..8a0fbc27 --- /dev/null +++ b/examples/app/vsgturntablecamera/Turntable.hpp @@ -0,0 +1,152 @@ +#pragma once + +#include + +class Turntable : public vsg::Inherit +{ + public: + explicit Turntable(vsg::ref_ptr camera); + + /// compute non dimensional window coordinate (-1,1) from event coords + vsg::dvec2 ndc(const vsg::PointerEvent& event); + + /// compute turntable coordinate from event coords + vsg::dvec3 ttc(const vsg::PointerEvent& event); + + void apply(vsg::KeyPressEvent& keyPress) override; + void apply(vsg::KeyReleaseEvent& keyRelease) override; + void apply(vsg::FocusInEvent& focusIn) override; + void apply(vsg::FocusOutEvent& focusOut) override; + void apply(vsg::ButtonPressEvent& buttonPress) override; + void apply(vsg::ButtonReleaseEvent& buttonRelease) override; + void apply(vsg::MoveEvent& moveEvent) override; + void apply(vsg::ScrollWheelEvent& scrollWheel) override; + void apply(vsg::TouchDownEvent& touchDown) override; + void apply(vsg::TouchUpEvent& touchUp) override; + void apply(vsg::TouchMoveEvent& touchMove) override; + void apply(vsg::FrameEvent& frame) override; + + virtual void rotate(double angle, const vsg::dvec3& axis); + virtual void zoom(double ratio); + virtual void pan(const vsg::dvec2& delta); + + std::pair cameraRenderAreaCoordinates(const vsg::PointerEvent& pointerEvent) const; + bool withinRenderArea(const vsg::PointerEvent& pointerEvent) const; + bool eventRelevant(const vsg::WindowEvent& event) const; + + /// list of windows that this Trackball should respond to events from, and the points xy offsets to apply + std::map, vsg::ivec2> windowOffsets; + + /// add a Window to respond events for, with mouse coordinate offset to treat all associated windows + void addWindow(vsg::ref_ptr window, const vsg::ivec2& offset = {}); + + /// add Key to Viewpoint binding using a LookAt to define the viewpoint + void addKeyViewpoint(vsg::KeySymbol key, vsg::ref_ptr lookAt, double duration = 1.0); + + /// set the LookAt viewport to the specified lookAt, animating the movements from the current lookAt to the new one. + /// A value of 0.0 instantly moves the lookAt to the new value. + void setViewpoint(vsg::ref_ptr lookAt, double duration = 1.0); + + struct Viewpoint + { + vsg::ref_ptr lookAt; + double duration = 0.0; + }; + + /// container that maps key symbol bindings with the Viewpoint that should move the LookAt to when pressed. + std::map keyViewpointMap; + + /// Key that turns the view left around the eye points + vsg::KeySymbol turnLeftKey = vsg::KEY_a; + + /// Key that turns the view right around the eye points + vsg::KeySymbol turnRightKey = vsg::KEY_d; + + /// Key that pitches up the view around the eye point + vsg::KeySymbol pitchUpKey = vsg::KEY_w; + + /// Key that pitches down the view around the eye point + vsg::KeySymbol pitchDownKey = vsg::KEY_s; + + /// Key that rools the view anti-clockwise/left + vsg::KeySymbol rollLeftKey = vsg::KEY_q; + + /// Key that rolls the view clockwise/right + vsg::KeySymbol rollRightKey = vsg::KEY_e; + + /// Key that moves the view forward + vsg::KeySymbol moveForwardKey = vsg::KEY_o; + + /// Key that moves the view backwards + vsg::KeySymbol moveBackwardKey = vsg::KEY_i; + + /// Key that moves the view left + vsg::KeySymbol moveLeftKey = vsg::KEY_Left; + + /// Key that moves the view right + vsg::KeySymbol moveRightKey = vsg::KEY_Right; + + /// Key that moves the view upward + vsg::KeySymbol moveUpKey = vsg::KEY_Up; + + /// Key that moves the view downward + vsg::KeySymbol moveDownKey = vsg::KEY_Down; + + /// Button mask value used to enable rotating of the view, defaults to left mouse button + vsg::ButtonMask rotateButtonMask = vsg::BUTTON_MASK_1; + + /// Button mask value used to enable panning of the view, defaults to middle mouse button + vsg::ButtonMask panButtonMask = vsg::BUTTON_MASK_2; + + /// Button mask value used to enable zooming of the view, defaults to right mouse button + vsg::ButtonMask zoomButtonMask = vsg::BUTTON_MASK_3; + + /// Button mask value used used for touch events + vsg::ButtonMask touchMappedToButtonMask = vsg::BUTTON_MASK_1; + + /// Scale for controlling how rapidly the view zooms in/out. Positive value zooms in when mouse moves downwards + double zoomScale = 1.0; + + /// Toggle on/off whether the view should continue moving when the mouse buttons are released while the mouse is in motion. + bool supportsThrow = true; + + protected: + vsg::ref_ptr _camera; + vsg::ref_ptr _lookAt; + + bool _hasKeyboardFocus = false; + bool _hasPointerFocus = false; + bool _lastPointerEventWithinRenderArea = false; + + enum UpdateMode + { + INACTIVE = 0, + ROTATE, + PAN, + ZOOM + }; + UpdateMode _updateMode = INACTIVE; + double _zoomPreviousRatio = 0.0; + vsg::dvec2 _pan; + double _rotateAngle = 0.0; + vsg::dvec3 _rotateAxis; + + vsg::time_point _previousTime; + vsg::ref_ptr _previousPointerEvent; + double _previousDelta = 0.0; + double _prevZoomTouchDistance = 0.0; + bool _thrown = false; + + vsg::time_point _startTime; + vsg::ref_ptr _startLookAt; + vsg::ref_ptr _endLookAt; + std::map> _previousTouches; + + vsg::ref_ptr _keyboard; + + double _animationDuration = 0.0; + + void applyRotationWithGimbalAvoidance(const vsg::dvec3& rot, double scaleRotation, const vsg::dvec3& globalUp); + void computeRotationAxesWithGimbalAvoidance(vsg::dvec3& horizontalAxis, vsg::dvec3& verticalAxis, + const vsg::dvec3& lookVector, const vsg::dvec3& globalUp); +}; \ No newline at end of file diff --git a/examples/app/vsgturntablecamera/vsgturntablecamera.cpp b/examples/app/vsgturntablecamera/vsgturntablecamera.cpp new file mode 100644 index 00000000..0a64a6d8 --- /dev/null +++ b/examples/app/vsgturntablecamera/vsgturntablecamera.cpp @@ -0,0 +1,414 @@ +#include + +#include "Turntable.hpp" + +#ifdef vsgXchange_FOUND +# include +#endif + +#include +#include +#include +#include + +vsg::ref_ptr createTextureQuad(vsg::ref_ptr sourceData, vsg::ref_ptr options) +{ + auto builder = vsg::Builder::create(); + builder->options = options; + + vsg::StateInfo state; + state.image = sourceData; + state.lighting = false; + + vsg::GeometryInfo geom; + geom.dy.set(0.0f, 0.0f, 1.0f); + geom.dz.set(0.0f, -1.0f, 0.0f); + + return builder->createQuad(geom, state); +} + +void enableGenerateDebugInfo(vsg::ref_ptr options) +{ + auto shaderHints = vsg::ShaderCompileSettings::create(); + shaderHints->generateDebugInfo = true; + + auto& text = options->shaderSets["text"] = vsg::createTextShaderSet(options); + text->defaultShaderHints = shaderHints; + + auto& flat = options->shaderSets["flat"] = vsg::createFlatShadedShaderSet(options); + flat->defaultShaderHints = shaderHints; + + auto& phong = options->shaderSets["phong"] = vsg::createPhongShaderSet(options); + phong->defaultShaderHints = shaderHints; + + auto& pbr = options->shaderSets["pbr"] = vsg::createPhysicsBasedRenderingShaderSet(options); + pbr->defaultShaderHints = shaderHints; +} + +int main(int argc, char** argv) +{ + try + { + // set up defaults and read command line arguments to override them + vsg::CommandLine arguments(&argc, argv); + + // if we want to redirect std::cout and std::cerr to the vsg::Logger call vsg::Logger::redirect_stdout() + if (arguments.read({"--redirect-std", "-r"})) vsg::Logger::instance()->redirect_std(); + + // set up vsg::Options to pass in filepaths, ReaderWriters and other IO related options to use when reading and writing files. + auto options = vsg::Options::create(); + options->sharedObjects = vsg::SharedObjects::create(); + options->fileCache = vsg::getEnv("VSG_FILE_CACHE"); + options->paths = vsg::getEnvPaths("VSG_FILE_PATH"); + +#ifdef vsgXchange_all + // add vsgXchange's support for reading and writing 3rd party file formats + options->add(vsgXchange::all::create()); +#endif + + arguments.read(options); + + if (uint32_t numOperationThreads = 0; arguments.read("--ot", numOperationThreads)) options->operationThreads = vsg::OperationThreads::create(numOperationThreads); + + auto windowTraits = vsg::WindowTraits::create(); + windowTraits->windowTitle = "vsgviewer"; + windowTraits->debugLayer = arguments.read({"--debug", "-d"}); + windowTraits->apiDumpLayer = arguments.read({"--api", "-a"}); + windowTraits->synchronizationLayer = arguments.read("--sync"); + bool reportAverageFrameRate = arguments.read("--fps"); + if (arguments.read("--double-buffer")) windowTraits->swapchainPreferences.imageCount = 2; + if (arguments.read("--triple-buffer")) windowTraits->swapchainPreferences.imageCount = 3; // default + if (arguments.read("--IMMEDIATE")) { windowTraits->swapchainPreferences.presentMode = VK_PRESENT_MODE_IMMEDIATE_KHR; } + if (arguments.read("--FIFO")) windowTraits->swapchainPreferences.presentMode = VK_PRESENT_MODE_FIFO_KHR; + if (arguments.read("--FIFO_RELAXED")) windowTraits->swapchainPreferences.presentMode = VK_PRESENT_MODE_FIFO_RELAXED_KHR; + if (arguments.read("--MAILBOX")) windowTraits->swapchainPreferences.presentMode = VK_PRESENT_MODE_MAILBOX_KHR; + if (arguments.read({"-t", "--test"})) + { + windowTraits->swapchainPreferences.presentMode = VK_PRESENT_MODE_IMMEDIATE_KHR; + windowTraits->fullscreen = true; + reportAverageFrameRate = true; + } + if (arguments.read({"--st", "--small-test"})) + { + windowTraits->swapchainPreferences.presentMode = VK_PRESENT_MODE_IMMEDIATE_KHR; + windowTraits->width = 192, windowTraits->height = 108; + windowTraits->decoration = false; + reportAverageFrameRate = true; + } + + bool multiThreading = arguments.read("--mt"); + if (arguments.read({"--fullscreen", "--fs"})) windowTraits->fullscreen = true; + if (arguments.read({"--window", "-w"}, windowTraits->width, windowTraits->height)) { windowTraits->fullscreen = false; } + if (arguments.read({"--no-frame", "--nf"})) windowTraits->decoration = false; + if (arguments.read("--or")) windowTraits->overrideRedirect = true; + auto maxTime = arguments.value(std::numeric_limits::max(), "--max-time"); + + if (arguments.read("--d32")) windowTraits->depthFormat = VK_FORMAT_D32_SFLOAT; + if (arguments.read("--sRGB")) windowTraits->swapchainPreferences.surfaceFormat = {VK_FORMAT_B8G8R8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}; + if (arguments.read("--RGB")) windowTraits->swapchainPreferences.surfaceFormat = {VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}; + + arguments.read("--screen", windowTraits->screenNum); + arguments.read("--display", windowTraits->display); + arguments.read("--samples", windowTraits->samples); + if (int log_level = 0; arguments.read("--log-level", log_level)) vsg::Logger::instance()->level = vsg::Logger::Level(log_level); + auto numFrames = arguments.value(-1, "-f"); + auto pathFilename = arguments.value("", "-p"); + auto loadLevels = arguments.value(0, "--load-levels"); + auto maxPagedLOD = arguments.value(0, "--maxPagedLOD"); + auto horizonMountainHeight = arguments.value(0.0, "--hmh"); + auto nearFarRatio = arguments.value(0.001, "--nfr"); + if (arguments.read("--rgb")) options->mapRGBtoRGBAHint = false; + + bool depthClamp = arguments.read({"--dc", "--depthClamp"}); + if (depthClamp) + { + std::cout << "Enabled depth clamp." << std::endl; + auto deviceFeatures = windowTraits->deviceFeatures = vsg::DeviceFeatures::create(); + deviceFeatures->get().samplerAnisotropy = VK_TRUE; + deviceFeatures->get().depthClamp = VK_TRUE; + } + + vsg::ref_ptr resourceHints; + if (auto resourceHintsFilename = arguments.value("", "--rh")) + { + resourceHints = vsg::read_cast(resourceHintsFilename, options); + } + + if (auto outputResourceHintsFilename = arguments.value("", "--orh")) + { + if (!resourceHints) resourceHints = vsg::ResourceHints::create(); + vsg::write(resourceHints, outputResourceHintsFilename, options); + return 0; + } + + if (arguments.read({"--shader-debug-info", "--sdi"})) + { + enableGenerateDebugInfo(options); + windowTraits->deviceExtensionNames.push_back(VK_KHR_SHADER_NON_SEMANTIC_INFO_EXTENSION_NAME); + } + + if (int log_level = 0; arguments.read("--log-level", log_level)) vsg::Logger::instance()->level = vsg::Logger::Level(log_level); + auto logFilename = arguments.value("", "--log"); + + vsg::ref_ptr instrumentation; + if (arguments.read({"--gpu-annotation", "--ga"}) && vsg::isExtensionSupported(VK_EXT_DEBUG_UTILS_EXTENSION_NAME)) + { + windowTraits->debugUtils = true; + + auto gpu_instrumentation = vsg::GpuAnnotation::create(); + if (arguments.read("--name")) + gpu_instrumentation->labelType = vsg::GpuAnnotation::SourceLocation_name; + else if (arguments.read("--className")) + gpu_instrumentation->labelType = vsg::GpuAnnotation::Object_className; + else if (arguments.read("--func")) + gpu_instrumentation->labelType = vsg::GpuAnnotation::SourceLocation_function; + + instrumentation = gpu_instrumentation; + } + else if (arguments.read({"--profiler", "--pr"})) + { + // set Profiler options + auto settings = vsg::Profiler::Settings::create(); + arguments.read("--cpu", settings->cpu_instrumentation_level); + arguments.read("--gpu", settings->gpu_instrumentation_level); + arguments.read("--log-size", settings->log_size); + + // create the profiler + instrumentation = vsg::Profiler::create(settings); + } + + vsg::Affinity affinity; + uint32_t cpu = 0; + while (arguments.read("-c", cpu)) + { + affinity.cpus.insert(cpu); + } + + // should animations be automatically played + auto autoPlay = !arguments.read({"--no-auto-play", "--nop"}); + + if (arguments.errors()) return arguments.writeErrorMessages(std::cerr); + + if (argc <= 1) + { + std::cout << "Please specify a 3d model or image file on the command line." << std::endl; + return 1; + } + + auto group = vsg::Group::create(); + + vsg::Path path; + + // read any vsg files + for (int i = 1; i < argc; ++i) + { + vsg::Path filename = arguments[i]; + path = vsg::filePath(filename); + + auto object = vsg::read(filename, options); + if (auto node = object.cast()) + { + group->addChild(node); + } + else if (auto data = object.cast()) + { + if (auto textureGeometry = createTextureQuad(data, options)) + { + group->addChild(textureGeometry); + } + } + else if (object) + { + std::cout << "Unable to view object of type " << object->className() << std::endl; + } + else + { + std::cout << "Unable to load file " << filename << std::endl; + } + } + + if (group->children.empty()) + { + return 1; + } + + vsg::ref_ptr vsg_scene; + if (group->children.size() == 1) + vsg_scene = group->children[0]; + else + vsg_scene = group; + + // create the viewer and assign window(s) to it + auto viewer = vsg::Viewer::create(); + auto window = vsg::Window::create(windowTraits); + if (!window) + { + std::cout << "Could not create window." << std::endl; + return 1; + } + + viewer->addWindow(window); + + // compute the bounds of the scene graph to help position camera + vsg::ComputeBounds computeBounds; + vsg_scene->accept(computeBounds); + vsg::dvec3 centre = (computeBounds.bounds.min + computeBounds.bounds.max) * 0.5; + double radius = vsg::length(computeBounds.bounds.max - computeBounds.bounds.min) * 0.6; + + // set up the camera + auto lookAt = vsg::LookAt::create(centre + vsg::dvec3(0.0, -radius * 3.5, 0.0), centre, vsg::dvec3(0.0, 0.0, 1.0)); + + vsg::ref_ptr perspective; + auto ellipsoidModel = vsg_scene->getRefObject("EllipsoidModel"); + if (ellipsoidModel) + { + perspective = vsg::EllipsoidPerspective::create(lookAt, ellipsoidModel, 30.0, static_cast(window->extent2D().width) / static_cast(window->extent2D().height), nearFarRatio, horizonMountainHeight); + } + else + { + perspective = vsg::Perspective::create(30.0, static_cast(window->extent2D().width) / static_cast(window->extent2D().height), nearFarRatio * radius, radius * 4.5); + } + + auto camera = vsg::Camera::create(perspective, lookAt, vsg::ViewportState::create(window->extent2D())); + + // add close handler to respond to the close window button and pressing escape + viewer->addEventHandler(vsg::CloseHandler::create(viewer)); + + auto cameraAnimation = vsg::CameraAnimationHandler::create(camera, pathFilename, options); + viewer->addEventHandler(cameraAnimation); + if (autoPlay && cameraAnimation->animation) + { + cameraAnimation->play(); + + if (reportAverageFrameRate && maxTime == std::numeric_limits::max()) + { + maxTime = cameraAnimation->animation->maxTime(); + } + } + + // Use our Turntable camera controller to control the camera + auto cameraController = Turntable::create(camera); + viewer->addEventHandler(cameraController); + + // if required preload specific number of PagedLOD levels. + if (loadLevels > 0) + { + vsg::LoadPagedLOD loadPagedLOD(camera, loadLevels); + + auto startTime = vsg::clock::now(); + + vsg_scene->accept(loadPagedLOD); + + auto time = std::chrono::duration(vsg::clock::now() - startTime).count(); + std::cout << "No. of tiles loaded " << loadPagedLOD.numTiles << " in " << time << "ms." << std::endl; + } + + auto commandGraph = vsg::createCommandGraphForView(window, camera, vsg_scene); + viewer->assignRecordAndSubmitTaskAndPresentation({commandGraph}); + + if (instrumentation) viewer->assignInstrumentation(instrumentation); + + if (multiThreading) + { + viewer->setupThreading(); + + if (affinity) + { + auto cpu_itr = affinity.cpus.begin(); + + // set affinity of main thread + if (cpu_itr != affinity.cpus.end()) + { + std::cout << "vsg::setAffinity() " << *cpu_itr << std::endl; + vsg::setAffinity(vsg::Affinity(*cpu_itr++)); + } + + for (auto& thread : viewer->threads) + { + if (thread.joinable() && cpu_itr != affinity.cpus.end()) + { + std::cout << "vsg::setAffinity(" << thread.get_id() << ") " << *cpu_itr << std::endl; + vsg::setAffinity(thread, vsg::Affinity(*cpu_itr++)); + } + } + } + } + else if (affinity) + { + std::cout << "vsg::setAffinity("; + for (auto cpu_num : affinity.cpus) + { + std::cout << " " << cpu_num; + } + std::cout << " )" << std::endl; + + vsg::setAffinity(affinity); + } + + viewer->compile(resourceHints); + + if (maxPagedLOD > 0) + { + // set targetMaxNumPagedLODWithHighResSubgraphs after Viewer::compile() as it will assign any DatabasePager if required. + for (auto& task : viewer->recordAndSubmitTasks) + { + if (task->databasePager) task->databasePager->targetMaxNumPagedLODWithHighResSubgraphs = maxPagedLOD; + } + } + + if (autoPlay) + { + // find any animation groups in the loaded scene graph and play the first animation in each of the animation groups. + auto animationGroups = vsg::visit(vsg_scene).animationGroups; + for (auto ag : animationGroups) + { + if (!ag->animations.empty()) viewer->animationManager->play(ag->animations.front()); + } + } + + viewer->start_point() = vsg::clock::now(); + + // rendering main loop + while (viewer->advanceToNextFrame() && (numFrames < 0 || (numFrames--) > 0) && (viewer->getFrameStamp()->simulationTime < maxTime)) + { + // pass any events into EventHandlers assigned to the Viewer + viewer->handleEvents(); + + viewer->update(); + + viewer->recordAndSubmit(); + + viewer->present(); + } + + if (reportAverageFrameRate) + { + auto fs = viewer->getFrameStamp(); + double fps = static_cast(fs->frameCount) / std::chrono::duration(vsg::clock::now() - viewer->start_point()).count(); + std::cout << "Average frame rate = " << fps << " fps" << std::endl; + } + + if (auto profiler = instrumentation.cast()) + { + instrumentation->finish(); + if (logFilename) + { + std::ofstream fout(logFilename); + profiler->log->report(fout); + } + else + { + profiler->log->report(std::cout); + } + } + } + catch (const vsg::Exception& ve) + { + for (int i = 0; i < argc; ++i) std::cerr << argv[i] << " "; + std::cerr << "\n[Exception] - " << ve.message << " result = " << ve.result << std::endl; + return 1; + } + + // clean up done automatically thanks to ref_ptr<> + return 0; +} From 50865bfd516fd786d526725670f107d30edbe89c Mon Sep 17 00:00:00 2001 From: Nolan Kramer Date: Thu, 8 May 2025 18:03:03 -0700 Subject: [PATCH 2/3] Add license mit --- examples/app/vsgturntablecamera/Turntable.cpp | 25 +++++++++++++++++++ examples/app/vsgturntablecamera/Turntable.hpp | 25 +++++++++++++++++++ .../vsgturntablecamera/vsgturntablecamera.cpp | 25 +++++++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/examples/app/vsgturntablecamera/Turntable.cpp b/examples/app/vsgturntablecamera/Turntable.cpp index 6a3d1992..fbcbf0a9 100644 --- a/examples/app/vsgturntablecamera/Turntable.cpp +++ b/examples/app/vsgturntablecamera/Turntable.cpp @@ -1,3 +1,28 @@ +/** + * MIT License + +Copyright (c) [2025] [Nolan Kramer] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + * + */ + #include "Turntable.hpp" Turntable::Turntable(vsg::ref_ptr camera) : diff --git a/examples/app/vsgturntablecamera/Turntable.hpp b/examples/app/vsgturntablecamera/Turntable.hpp index 8a0fbc27..802badac 100644 --- a/examples/app/vsgturntablecamera/Turntable.hpp +++ b/examples/app/vsgturntablecamera/Turntable.hpp @@ -1,3 +1,28 @@ +/** + * MIT License + +Copyright (c) [2025] [Nolan Kramer] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + * + */ + #pragma once #include diff --git a/examples/app/vsgturntablecamera/vsgturntablecamera.cpp b/examples/app/vsgturntablecamera/vsgturntablecamera.cpp index 0a64a6d8..5a6430f5 100644 --- a/examples/app/vsgturntablecamera/vsgturntablecamera.cpp +++ b/examples/app/vsgturntablecamera/vsgturntablecamera.cpp @@ -1,3 +1,28 @@ +/** + * MIT License + +Copyright (c) [2025] [Nolan Kramer] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + * + */ + #include #include "Turntable.hpp" From cf79ac567d845fea4fa50ed580551faf2467f6a8 Mon Sep 17 00:00:00 2001 From: Nolan Kramer Date: Wed, 14 May 2025 10:58:32 -0700 Subject: [PATCH 3/3] Rotation fix: use std::abs() --- examples/app/vsgturntablecamera/Turntable.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/app/vsgturntablecamera/Turntable.cpp b/examples/app/vsgturntablecamera/Turntable.cpp index fbcbf0a9..81f9d988 100644 --- a/examples/app/vsgturntablecamera/Turntable.cpp +++ b/examples/app/vsgturntablecamera/Turntable.cpp @@ -466,7 +466,7 @@ void Turntable::apply(vsg::FrameEvent& frame) computeRotationAxesWithGimbalAvoidance(horizontalAxis, verticalAxis, lookVector, globalUp); // Apply horizontal rotation - double horizontalAngle = _rotateAngle * (1.0 - abs(verticalComponent)) * scale; + double horizontalAngle = _rotateAngle * (1.0 - std::abs(verticalComponent)) * scale; rotate(horizontalAngle, horizontalAxis); // Apply vertical rotation @@ -526,7 +526,7 @@ void Turntable::computeRotationAxesWithGimbalAvoidance(vsg::dvec3& horizontalAxi const vsg::dvec3& lookVector, const vsg::dvec3& globalUp) { // 1. Compute how close we are to gimbal lock - double viewVerticalAlignment = abs(vsg::dot(lookVector, globalUp)); + double viewVerticalAlignment = std::abs(vsg::dot(lookVector, globalUp)); // The closer viewVerticalAlignment is to 1.0, the closer we are to gimbal lock @@ -563,7 +563,7 @@ void Turntable::computeRotationAxesWithGimbalAvoidance(vsg::dvec3& horizontalAxi void Turntable::rotate(double angle, const vsg::dvec3& axis) { - if (abs(angle) < 1e-10 || vsg::length(axis) < 1e-10) + if (std::abs(angle) < 1e-10 || vsg::length(axis) < 1e-10) return; // Create rotation matrix around the provided axis