From 3e7ef537998a2f19c7a72cb3af9d383d9720c54b Mon Sep 17 00:00:00 2001 From: crosstyan Date: Wed, 11 Mar 2026 11:37:45 +0800 Subject: [PATCH] Improve viewer camera controls and playback seeking --- src/OrbitCameraController.cpp | 17 ++- src/OrbitCameraController.hpp | 3 +- src/ViewerApp.cpp | 230 ++++++++++++++++++++++++++++++---- src/ViewerApp.hpp | 31 +++++ src/main.cpp | 2 +- 5 files changed, 253 insertions(+), 30 deletions(-) diff --git a/src/OrbitCameraController.cpp b/src/OrbitCameraController.cpp index e59a4e1..c96a6ea 100644 --- a/src/OrbitCameraController.cpp +++ b/src/OrbitCameraController.cpp @@ -13,6 +13,10 @@ constexpr float PitchLimit = 1.45f; constexpr float MinDistance = 0.05f; constexpr float OrbitSensitivity = 0.008f; constexpr float ZoomSensitivity = 0.15f; +constexpr Deg VerticalFieldOfView = 35.0_degf; +constexpr float FitPadding = 1.15f; +constexpr float DefaultYaw = 0.78539816f; +constexpr float DefaultPitch = 0.5f; } void OrbitCameraController::setViewport(const Vector2i& windowSize, const Vector2i& framebufferSize) { @@ -23,9 +27,9 @@ void OrbitCameraController::setViewport(const Vector2i& windowSize, const Vector void OrbitCameraController::setSceneBounds(const Vector3& center, float radius) { _pivot = center; _sceneRadius = Math::max(radius, 0.1f); - _distance = Math::max(_sceneRadius*2.5f, 0.5f); - _yaw = 0.0f; - _pitch = 0.35f; + _distance = Math::max((_sceneRadius/Math::sin(VerticalFieldOfView*0.5f))*FitPadding, 0.5f); + _yaw = DefaultYaw; + _pitch = DefaultPitch; } void OrbitCameraController::reset() { @@ -34,7 +38,8 @@ void OrbitCameraController::reset() { void OrbitCameraController::orbit(const Vector2& deltaPixels) { _yaw -= deltaPixels.x()*OrbitSensitivity; - _pitch = Math::clamp(_pitch - deltaPixels.y()*OrbitSensitivity, -PitchLimit, PitchLimit); + const float verticalDirection = _invertY ? 1.0f : -1.0f; + _pitch = Math::clamp(_pitch + verticalDirection*deltaPixels.y()*OrbitSensitivity, -PitchLimit, PitchLimit); } void OrbitCameraController::pan(const Vector2& deltaPixels) { @@ -50,7 +55,7 @@ void OrbitCameraController::zoom(float wheelDelta) { } Matrix4 OrbitCameraController::cameraTransform() const { - return Matrix4::lookAt(position(), _pivot, Vector3::yAxis()).invertedRigid(); + return Matrix4::lookAt(position(), _pivot, Vector3::yAxis()); } Vector3 OrbitCameraController::position() const { @@ -64,7 +69,7 @@ Vector3 OrbitCameraController::position() const { } float OrbitCameraController::panScale() const { - return 2.0f*_distance*Math::tan(35.0_degf/2.0f)/Float(_windowSize.y()); + return 2.0f*_distance*Math::tan(VerticalFieldOfView*0.5f)/Float(_windowSize.y()); } Vector3 OrbitCameraController::forward() const { diff --git a/src/OrbitCameraController.hpp b/src/OrbitCameraController.hpp index c94d6e6..c016be6 100644 --- a/src/OrbitCameraController.hpp +++ b/src/OrbitCameraController.hpp @@ -10,6 +10,7 @@ class OrbitCameraController { public: void setViewport(const Magnum::Vector2i& windowSize, const Magnum::Vector2i& framebufferSize); void setSceneBounds(const Magnum::Vector3& center, float radius); + void setInvertY(bool invertY) { _invertY = invertY; } void reset(); void orbit(const Magnum::Vector2& deltaPixels); @@ -33,7 +34,7 @@ private: float _yaw = 0.0f; float _pitch = 0.35f; float _sceneRadius = 1.0f; + bool _invertY = false; }; } - diff --git a/src/ViewerApp.cpp b/src/ViewerApp.cpp index b7b9393..3d2d0b8 100644 --- a/src/ViewerApp.cpp +++ b/src/ViewerApp.cpp @@ -6,10 +6,15 @@ #include #include +#include +#include +#include +#include #include #include +#include #include #include #include @@ -51,14 +56,16 @@ std::shared_ptr buildCpuMesh(const SimTK::PolygonalMesh& mesh) { } } + bool hasBounds = false; Range3D bounds; - bounds.min() = Vector3{std::numeric_limits::max()}; - bounds.max() = Vector3{-std::numeric_limits::max()}; cpu->vertices.reserve(positions.size()); for(std::size_t i = 0; i != positions.size(); ++i) { const Vector3 normal = normals[i].isZero() ? Vector3::yAxis() : normals[i].normalized(); cpu->vertices.push_back({positions[i], normal}); - bounds = Math::join(bounds, Range3D::fromSize(positions[i], {})); + const Range3D point = Range3D::fromSize(positions[i], {}); + if(hasBounds) bounds = Math::join(bounds, point); + else bounds = point; + hasBounds = true; } cpu->bounds = bounds; return cpu; @@ -68,6 +75,48 @@ Matrix4 makeFrameMatrix(const Vector3& translation, const Quaternion& rotation) return Matrix4::from(rotation.toMatrix(), translation); } +std::shared_ptr buildBoxMesh() { + auto cpu = std::make_shared(); + + const struct Face { + Vector3 normal; + Vector3 vertices[4]; + } faces[] = { + { Vector3::xAxis(), { + { 1.0f, -1.0f, -1.0f }, { 1.0f, 1.0f, -1.0f }, { 1.0f, 1.0f, 1.0f }, { 1.0f, -1.0f, 1.0f } + } }, + { -Vector3::xAxis(), { + { -1.0f, -1.0f, 1.0f }, { -1.0f, 1.0f, 1.0f }, { -1.0f, 1.0f, -1.0f }, { -1.0f, -1.0f, -1.0f } + } }, + { Vector3::yAxis(), { + { -1.0f, 1.0f, -1.0f }, { -1.0f, 1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f }, { 1.0f, 1.0f, -1.0f } + } }, + { -Vector3::yAxis(), { + { -1.0f, -1.0f, 1.0f }, { -1.0f, -1.0f, -1.0f }, { 1.0f, -1.0f, -1.0f }, { 1.0f, -1.0f, 1.0f } + } }, + { Vector3::zAxis(), { + { -1.0f, -1.0f, 1.0f }, { 1.0f, -1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f }, { -1.0f, 1.0f, 1.0f } + } }, + { -Vector3::zAxis(), { + { 1.0f, -1.0f, -1.0f }, { -1.0f, -1.0f, -1.0f }, { -1.0f, 1.0f, -1.0f }, { 1.0f, 1.0f, -1.0f } + } }, + }; + + cpu->vertices.reserve(24); + cpu->indices.reserve(36); + for(const Face& face: faces) { + const UnsignedInt base = UnsignedInt(cpu->vertices.size()); + for(const Vector3& position: face.vertices) cpu->vertices.push_back({position, face.normal}); + cpu->indices.insert(cpu->indices.end(), { + base + 0, base + 1, base + 2, + base + 0, base + 2, base + 3 + }); + } + + cpu->bounds = Range3D{{-1.0f, -1.0f, -1.0f}, {1.0f, 1.0f, 1.0f}}; + return cpu; +} + } ViewerApp::MeshDrawable::MeshDrawable(Object3D& object, @@ -91,11 +140,32 @@ void ViewerApp::MeshDrawable::draw(const Matrix4& transformation, Camera3D& came .draw(_mesh.mesh); } +ViewerApp::FlatDrawable::FlatDrawable(Object3D& object, + Shaders::FlatGL3D& shader, + GL::Mesh& mesh, + Color4 color, + DrawableGroup3D& drawables): + Drawable3D{object, &drawables}, + _shader{shader}, + _mesh{mesh}, + _color{color} {} + +void ViewerApp::FlatDrawable::draw(const Matrix4& transformation, Camera3D& camera) { + _shader + .setColor(_color) + .setTransformationProjectionMatrix(camera.projectionMatrix()*transformation) + .draw(_mesh); +} + ViewerApp::ViewerApp(const Arguments& arguments, CliOptions options): - Platform::Application{arguments, Configuration{}.setTitle("osim-magnum-viewer").setWindowFlags(Configuration::WindowFlag::Resizable)}, + Platform::Application{arguments, Configuration{} + .setTitle("osim-magnum-viewer") + .setSize({800, 600}, Configuration::DpiScalingPolicy::Physical) + .setWindowFlags(Configuration::WindowFlag::Resizable)}, _options{std::move(options)}, _sceneData{loadScene(_options.modelPath, _options.motionPath, _options.geometryDirs)}, _shader{Shaders::PhongGL{}}, + _flatShader{Shaders::FlatGL3D{}}, _isPlaying{!_options.startPaused}, _playbackSpeed{_options.initialSpeed} { @@ -112,14 +182,11 @@ ViewerApp::ViewerApp(const Arguments& arguments, CliOptions options): .setViewport(GL::defaultFramebuffer.viewport().size()); _orbit.setViewport(windowSize(), framebufferSize()); - const Range3D bounds = _sceneData.initialBounds; - const Vector3 center = (bounds.min() + bounds.max())*0.5f; - const float radius = Math::max((bounds.max() - bounds.min()).max(), 0.5f); - _orbit.setSceneBounds(center, radius); - _cameraObject.setTransformation(_orbit.cameraTransform()); + _orbit.setInvertY(!_options.reverseMouseY); if(!_sceneData.times.empty()) _currentTime = _sceneData.times.front(); createSceneObjects(); + resetDefaultCamera(); updateSceneAtCurrentTime(); _lastTick = std::chrono::steady_clock::now(); @@ -145,6 +212,11 @@ void ViewerApp::createSceneObjects() { auto markerCpu = buildCpuMesh(SimTK::PolygonalMesh::createSphereMesh(1.0, 1)); _markerMesh = uploadMesh(*markerCpu); _gpuMeshes.push_back(_markerMesh); + auto boxCpu = buildBoxMesh(); + _boxMesh = uploadMesh(*boxCpu); + _gpuMeshes.push_back(_boxMesh); + _groundGridMesh = MeshTools::compile(Primitives::grid3DWireframe({20, 20})); + _originWireBoxMesh = MeshTools::compile(Primitives::cubeWireframe()); for(const FrameTrack& frameTrack: _sceneData.frames) { auto frameObject = std::make_unique(&_scene); @@ -183,6 +255,52 @@ void ViewerApp::createSceneObjects() { _objectStorage.push_back(std::move(markerObject)); _drawableStorage.push_back(std::move(drawable)); } + + createReferenceObjects(); +} + +void ViewerApp::createReferenceObjects() { + const float radius = sceneRadius(); + + const float axisLength = Math::max(radius*0.6f, 0.35f); + const float axisHalfLength = axisLength*0.5f; + const float axisThickness = Math::max(radius*0.015f, 0.008f); + const float groundScale = Math::max(radius*2.5f, 1.5f); + const float originMarkerScale = Math::max(radius*0.04f, 0.03f); + + const struct AxisSpec { + Vector3 translation; + Vector3 scale; + Color4 color; + } axes[] = { + { { axisHalfLength, 0.0f, 0.0f }, { axisHalfLength, axisThickness, axisThickness }, { 0.92f, 0.28f, 0.28f, 1.0f } }, + { { 0.0f, axisHalfLength, 0.0f }, { axisThickness, axisHalfLength, axisThickness }, { 0.3f, 0.84f, 0.38f, 1.0f } }, + { { 0.0f, 0.0f, axisHalfLength }, { axisThickness, axisThickness, axisHalfLength }, { 0.36f, 0.58f, 0.92f, 1.0f } }, + }; + + for(const AxisSpec& axis: axes) { + auto object = std::make_unique(&_scene); + object->setTransformation(Matrix4::translation(axis.translation)*Matrix4::scaling(axis.scale)); + auto drawable = std::make_unique(*object, _shader, *_boxMesh, axis.color, _drawables); + _drawableStorage.push_back(std::move(drawable)); + _objectStorage.push_back(std::move(object)); + } + + auto originObject = std::make_unique(&_scene); + originObject->setTransformation(Matrix4::scaling(Vector3{originMarkerScale})); + auto originDrawable = std::make_unique( + *originObject, _flatShader, _originWireBoxMesh, Color4{0.82f, 0.84f, 0.88f, 1.0f}, _drawables); + _drawableStorage.push_back(std::move(originDrawable)); + _objectStorage.push_back(std::move(originObject)); + + auto groundObject = std::make_unique(&_scene); + groundObject->setTransformation( + Matrix4::rotationX(90.0_degf)* + Matrix4::scaling(Vector3{groundScale})); + auto groundDrawable = std::make_unique( + *groundObject, _flatShader, _groundGridMesh, Color4{0.42f, 0.45f, 0.48f, 1.0f}, _drawables); + _drawableStorage.push_back(std::move(groundDrawable)); + _objectStorage.push_back(std::move(groundObject)); } void ViewerApp::updatePlayback() { @@ -193,10 +311,16 @@ void ViewerApp::updatePlayback() { if(!_isPlaying || _sceneData.times.empty()) return; _currentTime += delta*_playbackSpeed; + const float startTime = _sceneData.times.front(); const float endTime = _sceneData.times.back(); + const float duration = endTime - startTime; if(_currentTime >= endTime) { - _currentTime = endTime; - _isPlaying = false; + if(_loopPlayback && duration > 0.0f) { + _currentTime = startTime + std::fmod(_currentTime - startTime, duration); + } else { + _currentTime = endTime; + _isPlaying = false; + } } } @@ -236,21 +360,84 @@ void ViewerApp::drawUi() { ImGui::TextUnformatted(_sceneData.modelName.c_str()); ImGui::TextUnformatted(_sceneData.motionName.c_str()); if(ImGui::Button(_isPlaying ? "Pause" : "Play")) { - if(!_isPlaying && !_sceneData.times.empty() && _currentTime >= _sceneData.times.back()) _currentTime = 0.0f; + if(!_isPlaying && !_sceneData.times.empty() && _currentTime >= _sceneData.times.back()) rewindPlayback(); _isPlaying = !_isPlaying; } ImGui::SameLine(); if(ImGui::Button("Rewind")) { - _currentTime = 0.0f; - _isPlaying = false; + rewindPlayback(); } ImGui::SliderFloat("Speed", &_playbackSpeed, 0.1f, 4.0f, "%.2fx"); + ImGui::Checkbox("Loop", &_loopPlayback); + if(ImGui::Checkbox("Reverse mouse Y", &_options.reverseMouseY)) { + _orbit.setInvertY(!_options.reverseMouseY); + } + if(!_sceneData.times.empty()) { + float seekTime = _currentTime; + if(ImGui::SliderFloat("Seek", &seekTime, _sceneData.times.front(), _sceneData.times.back(), "%.3f", + ImGuiSliderFlags_AlwaysClamp)) { + seekPlayback(seekTime); + } + } ImGui::Text("Time %.3f / %.3f", _currentTime, _sceneData.times.empty() ? 0.0f : _sceneData.times.back()); ImGui::Text("Frames %zu Markers %zu", _sceneData.frames.size(), _sceneData.markers.size()); - ImGui::TextUnformatted("LMB orbit RMB/MMB pan wheel zoom F frame"); + ImGui::TextUnformatted("LMB orbit Shift+LMB/RMB/MMB pan wheel zoom F frame"); ImGui::End(); } +float ViewerApp::sceneRadiusFromOrigin() const { + const Range3D bounds = _sceneData.initialBounds; + float radius = 0.5f; + for(const Vector3& corner: { + Vector3{bounds.min().x(), bounds.min().y(), bounds.min().z()}, + Vector3{bounds.min().x(), bounds.min().y(), bounds.max().z()}, + Vector3{bounds.min().x(), bounds.max().y(), bounds.min().z()}, + Vector3{bounds.min().x(), bounds.max().y(), bounds.max().z()}, + Vector3{bounds.max().x(), bounds.min().y(), bounds.min().z()}, + Vector3{bounds.max().x(), bounds.min().y(), bounds.max().z()}, + Vector3{bounds.max().x(), bounds.max().y(), bounds.min().z()}, + Vector3{bounds.max().x(), bounds.max().y(), bounds.max().z()}, + }) { + radius = Math::max(radius, corner.length()); + } + return radius; +} + +Vector3 ViewerApp::sceneCenter() const { + const Range3D bounds = _sceneData.initialBounds; + return (bounds.min() + bounds.max())*0.5f; +} + +float ViewerApp::sceneRadius() const { + const Range3D bounds = _sceneData.initialBounds; + return Math::max((bounds.max() - bounds.min()).length()*0.5f, 0.5f); +} + +void ViewerApp::seekPlayback(float time) { + if(_sceneData.times.empty()) { + _currentTime = 0.0f; + return; + } + + _currentTime = Math::clamp(time, _sceneData.times.front(), _sceneData.times.back()); + updateSceneAtCurrentTime(); +} + +void ViewerApp::resetDefaultCamera() { + _orbit.setSceneBounds({}, sceneRadiusFromOrigin()); + _cameraObject.setTransformation(_orbit.cameraTransform()); +} + +void ViewerApp::resetCameraToScene() { + _orbit.setSceneBounds(sceneCenter(), sceneRadius()); + _cameraObject.setTransformation(_orbit.cameraTransform()); +} + +void ViewerApp::rewindPlayback() { + seekPlayback(_sceneData.times.empty() ? 0.0f : _sceneData.times.front()); + _isPlaying = false; +} + void ViewerApp::drawEvent() { updatePlayback(); updateSceneAtCurrentTime(); @@ -292,19 +479,16 @@ void ViewerApp::keyPressEvent(KeyEvent& event) { switch(event.key()) { case Key::Space: - if(!_isPlaying && !_sceneData.times.empty() && _currentTime >= _sceneData.times.back()) _currentTime = 0.0f; + if(!_isPlaying && !_sceneData.times.empty() && _currentTime >= _sceneData.times.back()) rewindPlayback(); _isPlaying = !_isPlaying; event.setAccepted(); return; case Key::R: - _currentTime = 0.0f; - _isPlaying = false; + rewindPlayback(); event.setAccepted(); return; case Key::F: { - const Vector3 center = (_sceneData.initialBounds.min() + _sceneData.initialBounds.max())*0.5f; - const float radius = Math::max((_sceneData.initialBounds.max() - _sceneData.initialBounds.min()).max(), 0.5f); - _orbit.setSceneBounds(center, radius); + resetCameraToScene(); event.setAccepted(); return; } @@ -328,7 +512,9 @@ void ViewerApp::keyReleaseEvent(KeyEvent& event) { void ViewerApp::pointerPressEvent(PointerEvent& event) { if(_imgui.handlePointerPressEvent(event)) return; _lastPointerPosition = event.position(); - if(event.pointer() == Pointer::MouseLeft) _dragMode = DragMode::Orbit; + if(event.pointer() == Pointer::MouseLeft) { + _dragMode = (event.modifiers() & Modifier::Shift) ? DragMode::Pan : DragMode::Orbit; + } else if(event.pointer() == Pointer::MouseRight || event.pointer() == Pointer::MouseMiddle) _dragMode = DragMode::Pan; event.setAccepted(); } diff --git a/src/ViewerApp.hpp b/src/ViewerApp.hpp index 34529df..aa5efd4 100644 --- a/src/ViewerApp.hpp +++ b/src/ViewerApp.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,7 @@ struct CliOptions { std::vector geometryDirs; float initialSpeed = 1.0f; bool startPaused = false; + bool reverseMouseY = false; }; class ViewerApp: public Magnum::Platform::Application { @@ -99,12 +101,36 @@ private: Magnum::Color4 _color; }; + class FlatDrawable final: public Drawable3D { + public: + FlatDrawable(Object3D& object, + Magnum::Shaders::FlatGL3D& shader, + Magnum::GL::Mesh& mesh, + Magnum::Color4 color, + DrawableGroup3D& drawables); + + private: + void draw(const Magnum::Matrix4& transformation, Camera3D& camera) override; + + Magnum::Shaders::FlatGL3D& _shader; + Magnum::GL::Mesh& _mesh; + Magnum::Color4 _color; + }; + std::shared_ptr uploadMesh(const CpuMesh& mesh) const; void createSceneObjects(); + void createReferenceObjects(); void updatePlayback(); void updateSceneAtCurrentTime(); void drawUi(); std::size_t sampleIndexForTime(float time) const; + [[nodiscard]] float sceneRadiusFromOrigin() const; + [[nodiscard]] Magnum::Vector3 sceneCenter() const; + [[nodiscard]] float sceneRadius() const; + void seekPlayback(float time); + void resetDefaultCamera(); + void resetCameraToScene(); + void rewindPlayback(); CliOptions _options; LoadedScene _sceneData; @@ -118,6 +144,7 @@ private: DrawableGroup3D _drawables; Magnum::Shaders::PhongGL _shader; + Magnum::Shaders::FlatGL3D _flatShader; std::vector> _gpuMeshes; std::vector> _drawableStorage; std::vector> _objectStorage; @@ -125,8 +152,12 @@ private: std::vector _markers; std::shared_ptr _markerMesh; + std::shared_ptr _boxMesh; + Magnum::GL::Mesh _groundGridMesh; + Magnum::GL::Mesh _originWireBoxMesh; bool _isPlaying = false; + bool _loopPlayback = true; float _playbackSpeed = 1.0f; float _currentTime = 0.0f; std::chrono::steady_clock::time_point _lastTick; diff --git a/src/main.cpp b/src/main.cpp index 304ac13..625c4d5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,10 +13,10 @@ int main(int argc, char** argv) { cli.add_option("--geometry-dir", options.geometryDirs, "Additional geometry search directories"); cli.add_option("--speed", options.initialSpeed, "Initial playback speed")->check(CLI::PositiveNumber); cli.add_flag("--start-paused", options.startPaused, "Start paused at t=0"); + cli.add_flag("--reverse-mouse-y", options.reverseMouseY, "Reverse vertical orbit drag direction"); CLI11_PARSE(cli, argc, argv); osim_viewer::ViewerApp app({argc, argv}, std::move(options)); return app.exec(); } -