Initial native OpenSim viewer prototype
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
#include "OpenSimLoader.hpp"
|
||||
|
||||
#include <OpenSim/Common/Storage.h>
|
||||
#include <OpenSim/Simulation/Model/Appearance.h>
|
||||
#include <OpenSim/Simulation/Model/Geometry.h>
|
||||
#include <OpenSim/Simulation/Model/Marker.h>
|
||||
#include <OpenSim/Simulation/Model/Model.h>
|
||||
#include <OpenSim/Simulation/Model/ModelVisualizer.h>
|
||||
#include <OpenSim/Simulation/StatesTrajectory.h>
|
||||
#include <SimTKcommon/internal/PolygonalMesh.h>
|
||||
|
||||
#include <Magnum/Math/Functions.h>
|
||||
#include <Magnum/Math/Matrix4.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
using namespace Magnum;
|
||||
|
||||
namespace osim_viewer {
|
||||
|
||||
namespace {
|
||||
|
||||
Vector3 toVector3(const SimTK::Vec3& value) {
|
||||
return {Float(value[0]), Float(value[1]), Float(value[2])};
|
||||
}
|
||||
|
||||
Quaternion toQuaternion(const SimTK::Rotation& rotation) {
|
||||
const SimTK::Quaternion quat = rotation.convertRotationToQuaternion();
|
||||
return Quaternion{{Float(quat[1]), Float(quat[2]), Float(quat[3])}, Float(quat[0])}.normalized();
|
||||
}
|
||||
|
||||
Color4 toColor(const OpenSim::Appearance& appearance) {
|
||||
const SimTK::Vec3 color = appearance.get_color();
|
||||
return {Float(color[0]), Float(color[1]), Float(color[2]), Float(appearance.get_opacity())};
|
||||
}
|
||||
|
||||
Range3D scaledBounds(const Range3D& bounds, const Vector3& scale) {
|
||||
Range3D out;
|
||||
out.min() = Vector3{std::numeric_limits<Float>::max()};
|
||||
out.max() = Vector3{-std::numeric_limits<Float>::max()};
|
||||
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()},
|
||||
}) {
|
||||
out = Math::join(out, Range3D::fromSize(corner*scale, {}));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Range3D transformBounds(const Range3D& bounds, const Matrix4& transform) {
|
||||
Range3D out;
|
||||
out.min() = Vector3{std::numeric_limits<Float>::max()};
|
||||
out.max() = Vector3{-std::numeric_limits<Float>::max()};
|
||||
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()},
|
||||
}) {
|
||||
out = Math::join(out, Range3D::fromSize(transform.transformPoint(corner), {}));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
struct MeshCache {
|
||||
std::unordered_map<std::string, std::shared_ptr<CpuMesh>> meshes;
|
||||
std::shared_ptr<CpuMesh> unitSphere;
|
||||
std::shared_ptr<CpuMesh> unitCylinder;
|
||||
std::shared_ptr<CpuMesh> unitBrick;
|
||||
};
|
||||
|
||||
std::shared_ptr<CpuMesh> buildCpuMesh(const SimTK::PolygonalMesh& mesh) {
|
||||
auto cpu = std::make_shared<CpuMesh>();
|
||||
std::vector<Vector3> positions;
|
||||
positions.reserve(mesh.getNumVertices());
|
||||
for(int i = 0; i != mesh.getNumVertices(); ++i) positions.push_back(toVector3(mesh.getVertexPosition(i)));
|
||||
|
||||
std::vector<Vector3> normals(positions.size(), Vector3{0.0f});
|
||||
for(int face = 0; face != mesh.getNumFaces(); ++face) {
|
||||
const int faceVertexCount = mesh.getNumVerticesForFace(face);
|
||||
if(faceVertexCount < 3) continue;
|
||||
const int a = mesh.getFaceVertex(face, 0);
|
||||
for(int i = 1; i < faceVertexCount - 1; ++i) {
|
||||
const int b = mesh.getFaceVertex(face, i);
|
||||
const int c = mesh.getFaceVertex(face, i + 1);
|
||||
const Vector3 ab = positions[b] - positions[a];
|
||||
const Vector3 ac = positions[c] - positions[a];
|
||||
const Vector3 faceNormal = Math::cross(ab, ac);
|
||||
normals[a] += faceNormal;
|
||||
normals[b] += faceNormal;
|
||||
normals[c] += faceNormal;
|
||||
cpu->indices.push_back(Magnum::UnsignedInt(a));
|
||||
cpu->indices.push_back(Magnum::UnsignedInt(b));
|
||||
cpu->indices.push_back(Magnum::UnsignedInt(c));
|
||||
}
|
||||
}
|
||||
|
||||
Range3D bounds;
|
||||
bounds.min() = Vector3{std::numeric_limits<Float>::max()};
|
||||
bounds.max() = Vector3{-std::numeric_limits<Float>::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], {}));
|
||||
}
|
||||
cpu->bounds = bounds;
|
||||
return cpu;
|
||||
}
|
||||
|
||||
std::shared_ptr<CpuMesh> sphereMesh(MeshCache& cache) {
|
||||
if(!cache.unitSphere) {
|
||||
cache.unitSphere = buildCpuMesh(SimTK::PolygonalMesh::createSphereMesh(1.0, 2));
|
||||
}
|
||||
return cache.unitSphere;
|
||||
}
|
||||
|
||||
std::shared_ptr<CpuMesh> cylinderMesh(MeshCache& cache) {
|
||||
if(!cache.unitCylinder) {
|
||||
cache.unitCylinder = buildCpuMesh(SimTK::PolygonalMesh::createCylinderMesh(SimTK::YAxis, 1.0, 1.0, 2));
|
||||
}
|
||||
return cache.unitCylinder;
|
||||
}
|
||||
|
||||
std::shared_ptr<CpuMesh> brickMesh(MeshCache& cache) {
|
||||
if(!cache.unitBrick) {
|
||||
cache.unitBrick = buildCpuMesh(SimTK::PolygonalMesh::createBrickMesh(SimTK::Vec3(1.0, 1.0, 1.0), 1));
|
||||
}
|
||||
return cache.unitBrick;
|
||||
}
|
||||
|
||||
std::shared_ptr<CpuMesh> resolveMeshFile(MeshCache& cache, const OpenSim::Model& model, const std::string& meshFile) {
|
||||
bool isAbsolute = false;
|
||||
SimTK::Array_<std::string> attempts;
|
||||
if(!OpenSim::ModelVisualizer::findGeometryFile(model, meshFile, isAbsolute, attempts) || attempts.empty()) {
|
||||
throw std::runtime_error("Unable to locate geometry file: " + meshFile);
|
||||
}
|
||||
|
||||
const std::string resolved = attempts.back();
|
||||
if(auto found = cache.meshes.find(resolved); found != cache.meshes.end()) return found->second;
|
||||
|
||||
SimTK::PolygonalMesh mesh;
|
||||
mesh.loadFile(resolved);
|
||||
auto cpu = buildCpuMesh(mesh);
|
||||
cache.meshes.emplace(resolved, cpu);
|
||||
return cpu;
|
||||
}
|
||||
|
||||
Vector3 geometryScale(const OpenSim::Geometry& geometry) {
|
||||
const SimTK::Vec3 scale = geometry.get_scale_factors();
|
||||
return {Float(scale[0]), Float(scale[1]), Float(scale[2])};
|
||||
}
|
||||
|
||||
GeometrySpec makeGeometrySpec(MeshCache& cache, const OpenSim::Model& model, const OpenSim::Geometry& geometry) {
|
||||
GeometrySpec spec;
|
||||
spec.name = geometry.getName();
|
||||
spec.color = toColor(geometry.get_Appearance());
|
||||
spec.scale = geometryScale(geometry);
|
||||
|
||||
if(const auto* mesh = dynamic_cast<const OpenSim::Mesh*>(&geometry)) {
|
||||
spec.mesh = resolveMeshFile(cache, model, mesh->getGeometryFilename());
|
||||
} else if(const auto* sphere = dynamic_cast<const OpenSim::Sphere*>(&geometry)) {
|
||||
spec.mesh = sphereMesh(cache);
|
||||
spec.scale *= Vector3{Float(sphere->get_radius())};
|
||||
} else if(const auto* cylinder = dynamic_cast<const OpenSim::Cylinder*>(&geometry)) {
|
||||
spec.mesh = cylinderMesh(cache);
|
||||
spec.scale *= Vector3{Float(cylinder->get_radius()), Float(cylinder->get_half_height()), Float(cylinder->get_radius())};
|
||||
} else if(const auto* brick = dynamic_cast<const OpenSim::Brick*>(&geometry)) {
|
||||
spec.mesh = brickMesh(cache);
|
||||
const SimTK::Vec3 half = brick->get_half_lengths();
|
||||
spec.scale *= Vector3{Float(half[0]), Float(half[1]), Float(half[2])};
|
||||
} else if(const auto* ellipsoid = dynamic_cast<const OpenSim::Ellipsoid*>(&geometry)) {
|
||||
spec.mesh = sphereMesh(cache);
|
||||
const SimTK::Vec3 radii = ellipsoid->get_radii();
|
||||
spec.scale *= Vector3{Float(radii[0]), Float(radii[1]), Float(radii[2])};
|
||||
} else {
|
||||
throw std::runtime_error("Unsupported geometry type: " + geometry.getConcreteClassName());
|
||||
}
|
||||
|
||||
return spec;
|
||||
}
|
||||
|
||||
Matrix4 frameMatrix(const Magnum::Vector3& translation, const Magnum::Quaternion& rotation) {
|
||||
return Matrix4::from(rotation.toMatrix(), translation);
|
||||
}
|
||||
|
||||
std::vector<std::string> geometrySearchDirsForModel(const std::string& modelPath,
|
||||
const std::vector<std::string>& explicitDirs) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
std::set<std::string> unique;
|
||||
std::vector<std::string> ordered;
|
||||
const auto addIfDirectory = [&](const fs::path& path) {
|
||||
if(path.empty()) return;
|
||||
std::error_code ec;
|
||||
if(!fs::is_directory(path, ec)) return;
|
||||
|
||||
const std::string normalized = fs::weakly_canonical(path, ec).string();
|
||||
const std::string& key = ec ? path.string() : normalized;
|
||||
if(unique.insert(key).second) ordered.push_back(key);
|
||||
};
|
||||
|
||||
for(const std::string& dir: explicitDirs) addIfDirectory(fs::path{dir});
|
||||
|
||||
const fs::path modelDir = fs::absolute(fs::path{modelPath}).parent_path();
|
||||
addIfDirectory(modelDir);
|
||||
addIfDirectory(modelDir/"Geometry");
|
||||
if(!modelDir.empty()) addIfDirectory(modelDir.parent_path()/"Geometry");
|
||||
|
||||
#ifdef OSIM_VIEWER_SOURCE_DIR
|
||||
addIfDirectory(fs::path{OSIM_VIEWER_SOURCE_DIR}/"Geometry");
|
||||
#endif
|
||||
|
||||
return ordered;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
LoadedScene loadScene(const std::string& modelPath,
|
||||
const std::string& motionPath,
|
||||
const std::vector<std::string>& geometryDirs) {
|
||||
for(const std::string& dir: geometrySearchDirsForModel(modelPath, geometryDirs)) {
|
||||
OpenSim::ModelVisualizer::addDirToGeometrySearchPaths(dir);
|
||||
}
|
||||
|
||||
OpenSim::Model model{modelPath};
|
||||
auto& coords = model.updCoordinateSet();
|
||||
for(int i = 0; i != coords.getSize(); ++i) coords.get(i).setDefaultLocked(false);
|
||||
model.initSystem();
|
||||
|
||||
OpenSim::Storage motion{motionPath};
|
||||
if(motion.isInDegrees()) model.getSimbodyEngine().convertDegreesToRadians(motion);
|
||||
|
||||
OpenSim::Storage motionAsStates;
|
||||
model.formStateStorage(motion, motionAsStates, false);
|
||||
const OpenSim::StatesTrajectory trajectory = OpenSim::StatesTrajectory::createFromStatesStorage(model, motionAsStates, true, true);
|
||||
|
||||
MeshCache meshCache;
|
||||
LoadedScene scene;
|
||||
scene.modelName = model.getName();
|
||||
scene.motionName = motion.getName().empty() ? motionPath : motion.getName();
|
||||
|
||||
std::vector<const OpenSim::PhysicalFrame*> framePtrs;
|
||||
for(const OpenSim::PhysicalFrame& frame: model.getComponentList<OpenSim::PhysicalFrame>()) {
|
||||
if(frame.getProperty_attached_geometry().size() == 0) continue;
|
||||
|
||||
FrameTrack track;
|
||||
track.name = frame.getName();
|
||||
for(int i = 0; i != frame.getProperty_attached_geometry().size(); ++i) {
|
||||
const OpenSim::Geometry& geometry = frame.get_attached_geometry(i);
|
||||
if(!geometry.get_Appearance().get_visible()) continue;
|
||||
try {
|
||||
track.geometries.push_back(makeGeometrySpec(meshCache, model, geometry));
|
||||
} catch(const std::exception& error) {
|
||||
std::cerr << "Warning: skipping geometry '" << geometry.getName()
|
||||
<< "' on frame '" << frame.getName() << "': "
|
||||
<< error.what() << '\n';
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if(track.geometries.empty()) continue;
|
||||
|
||||
framePtrs.push_back(&frame);
|
||||
scene.frames.push_back(std::move(track));
|
||||
}
|
||||
|
||||
std::vector<const OpenSim::Marker*> markerPtrs;
|
||||
const OpenSim::MarkerSet& markers = model.getMarkerSet();
|
||||
for(int i = 0; i != markers.getSize(); ++i) {
|
||||
const auto& marker = markers.get(i);
|
||||
MarkerTrack track;
|
||||
track.name = marker.getName();
|
||||
track.color = {0.92f, 0.2f, 0.2f, 1.0f};
|
||||
markerPtrs.push_back(&marker);
|
||||
scene.markers.push_back(std::move(track));
|
||||
}
|
||||
|
||||
scene.times.reserve(trajectory.getSize());
|
||||
for(FrameTrack& frame: scene.frames) {
|
||||
frame.translations.reserve(trajectory.getSize());
|
||||
frame.rotations.reserve(trajectory.getSize());
|
||||
}
|
||||
for(MarkerTrack& marker: scene.markers) marker.positions.reserve(trajectory.getSize());
|
||||
|
||||
scene.initialBounds.min() = Vector3{std::numeric_limits<Float>::max()};
|
||||
scene.initialBounds.max() = Vector3{-std::numeric_limits<Float>::max()};
|
||||
|
||||
for(std::size_t sample = 0; sample != trajectory.getSize(); ++sample) {
|
||||
const SimTK::State& state = trajectory[sample];
|
||||
scene.times.push_back(Float(state.getTime()));
|
||||
model.realizeDynamics(state);
|
||||
|
||||
for(std::size_t i = 0; i != framePtrs.size(); ++i) {
|
||||
const SimTK::Transform xform = framePtrs[i]->getTransformInGround(state);
|
||||
const Vector3 translation = toVector3(xform.T());
|
||||
const Quaternion rotation = toQuaternion(xform.R());
|
||||
scene.frames[i].translations.push_back(translation);
|
||||
scene.frames[i].rotations.push_back(rotation);
|
||||
|
||||
if(sample == 0) {
|
||||
const Matrix4 world = frameMatrix(translation, rotation);
|
||||
for(const GeometrySpec& geometry: scene.frames[i].geometries) {
|
||||
const Range3D scaled = scaledBounds(geometry.mesh->bounds, geometry.scale);
|
||||
scene.initialBounds = Math::join(scene.initialBounds, transformBounds(scaled, world));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(std::size_t i = 0; i != markerPtrs.size(); ++i) {
|
||||
const Vector3 position = toVector3(markerPtrs[i]->getLocationInGround(state));
|
||||
scene.markers[i].positions.push_back(position);
|
||||
if(sample == 0) scene.initialBounds = Math::join(scene.initialBounds, Range3D::fromSize(position, {}));
|
||||
}
|
||||
}
|
||||
|
||||
if(scene.frames.empty() && scene.markers.empty()) {
|
||||
throw std::runtime_error("The model did not produce any renderable body geometry or markers.");
|
||||
}
|
||||
|
||||
return scene;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include <Magnum/Math/Color.h>
|
||||
#include <Magnum/Math/Range.h>
|
||||
#include <Magnum/Math/Vector3.h>
|
||||
#include <Magnum/Math/Quaternion.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace osim_viewer {
|
||||
|
||||
struct CpuMesh {
|
||||
struct Vertex {
|
||||
Magnum::Vector3 position;
|
||||
Magnum::Vector3 normal;
|
||||
};
|
||||
|
||||
std::vector<Vertex> vertices;
|
||||
std::vector<Magnum::UnsignedInt> indices;
|
||||
Magnum::Range3D bounds;
|
||||
};
|
||||
|
||||
struct GeometrySpec {
|
||||
std::shared_ptr<CpuMesh> mesh;
|
||||
Magnum::Vector3 scale{1.0f};
|
||||
Magnum::Color4 color{1.0f};
|
||||
std::string name;
|
||||
};
|
||||
|
||||
struct FrameTrack {
|
||||
std::string name;
|
||||
std::vector<GeometrySpec> geometries;
|
||||
std::vector<Magnum::Vector3> translations;
|
||||
std::vector<Magnum::Quaternion> rotations;
|
||||
};
|
||||
|
||||
struct MarkerTrack {
|
||||
std::string name;
|
||||
Magnum::Color4 color{1.0f};
|
||||
std::vector<Magnum::Vector3> positions;
|
||||
};
|
||||
|
||||
struct LoadedScene {
|
||||
std::string modelName;
|
||||
std::string motionName;
|
||||
std::vector<float> times;
|
||||
std::vector<FrameTrack> frames;
|
||||
std::vector<MarkerTrack> markers;
|
||||
Magnum::Range3D initialBounds;
|
||||
};
|
||||
|
||||
LoadedScene loadScene(const std::string& modelPath,
|
||||
const std::string& motionPath,
|
||||
const std::vector<std::string>& geometryDirs);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
#include "OrbitCameraController.hpp"
|
||||
|
||||
#include <Magnum/Math/Angle.h>
|
||||
#include <Magnum/Math/Functions.h>
|
||||
|
||||
using namespace Magnum;
|
||||
using namespace Math::Literals;
|
||||
|
||||
namespace osim_viewer {
|
||||
|
||||
namespace {
|
||||
constexpr float PitchLimit = 1.45f;
|
||||
constexpr float MinDistance = 0.05f;
|
||||
constexpr float OrbitSensitivity = 0.008f;
|
||||
constexpr float ZoomSensitivity = 0.15f;
|
||||
}
|
||||
|
||||
void OrbitCameraController::setViewport(const Vector2i& windowSize, const Vector2i& framebufferSize) {
|
||||
_windowSize = {Math::max(windowSize.x(), 1), Math::max(windowSize.y(), 1)};
|
||||
_framebufferSize = {Math::max(framebufferSize.x(), 1), Math::max(framebufferSize.y(), 1)};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void OrbitCameraController::reset() {
|
||||
setSceneBounds(_pivot, _sceneRadius);
|
||||
}
|
||||
|
||||
void OrbitCameraController::orbit(const Vector2& deltaPixels) {
|
||||
_yaw -= deltaPixels.x()*OrbitSensitivity;
|
||||
_pitch = Math::clamp(_pitch - deltaPixels.y()*OrbitSensitivity, -PitchLimit, PitchLimit);
|
||||
}
|
||||
|
||||
void OrbitCameraController::pan(const Vector2& deltaPixels) {
|
||||
const Vector3 panRight = right();
|
||||
const Vector3 panUp = up();
|
||||
const float scale = panScale();
|
||||
_pivot += (-panRight*deltaPixels.x() + panUp*deltaPixels.y())*scale;
|
||||
}
|
||||
|
||||
void OrbitCameraController::zoom(float wheelDelta) {
|
||||
const float factor = std::exp(-wheelDelta*ZoomSensitivity);
|
||||
_distance = Math::max(_distance*factor, MinDistance);
|
||||
}
|
||||
|
||||
Matrix4 OrbitCameraController::cameraTransform() const {
|
||||
return Matrix4::lookAt(position(), _pivot, Vector3::yAxis()).invertedRigid();
|
||||
}
|
||||
|
||||
Vector3 OrbitCameraController::position() const {
|
||||
const float cosPitch = std::cos(_pitch);
|
||||
const Vector3 offset{
|
||||
_distance*cosPitch*std::sin(_yaw),
|
||||
_distance*std::sin(_pitch),
|
||||
_distance*cosPitch*std::cos(_yaw)
|
||||
};
|
||||
return _pivot + offset;
|
||||
}
|
||||
|
||||
float OrbitCameraController::panScale() const {
|
||||
return 2.0f*_distance*Math::tan(35.0_degf/2.0f)/Float(_windowSize.y());
|
||||
}
|
||||
|
||||
Vector3 OrbitCameraController::forward() const {
|
||||
return (_pivot - position()).normalized();
|
||||
}
|
||||
|
||||
Vector3 OrbitCameraController::right() const {
|
||||
return Math::cross(forward(), Vector3::yAxis()).normalized();
|
||||
}
|
||||
|
||||
Vector3 OrbitCameraController::up() const {
|
||||
return Math::cross(right(), forward()).normalized();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <Magnum/Math/Matrix4.h>
|
||||
#include <Magnum/Math/Vector2.h>
|
||||
#include <Magnum/Math/Vector3.h>
|
||||
|
||||
namespace osim_viewer {
|
||||
|
||||
class OrbitCameraController {
|
||||
public:
|
||||
void setViewport(const Magnum::Vector2i& windowSize, const Magnum::Vector2i& framebufferSize);
|
||||
void setSceneBounds(const Magnum::Vector3& center, float radius);
|
||||
void reset();
|
||||
|
||||
void orbit(const Magnum::Vector2& deltaPixels);
|
||||
void pan(const Magnum::Vector2& deltaPixels);
|
||||
void zoom(float wheelDelta);
|
||||
|
||||
[[nodiscard]] Magnum::Matrix4 cameraTransform() const;
|
||||
[[nodiscard]] Magnum::Vector3 position() const;
|
||||
[[nodiscard]] Magnum::Vector3 pivot() const { return _pivot; }
|
||||
|
||||
private:
|
||||
[[nodiscard]] float panScale() const;
|
||||
[[nodiscard]] Magnum::Vector3 forward() const;
|
||||
[[nodiscard]] Magnum::Vector3 right() const;
|
||||
[[nodiscard]] Magnum::Vector3 up() const;
|
||||
|
||||
Magnum::Vector2i _windowSize{1280, 720};
|
||||
Magnum::Vector2i _framebufferSize{1280, 720};
|
||||
Magnum::Vector3 _pivot{0.0f};
|
||||
float _distance = 5.0f;
|
||||
float _yaw = 0.0f;
|
||||
float _pitch = 0.35f;
|
||||
float _sceneRadius = 1.0f;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
#include "ViewerApp.hpp"
|
||||
|
||||
#include <SimTKcommon/internal/PolygonalMesh.h>
|
||||
|
||||
#include <Corrade/Containers/ArrayViewStl.h>
|
||||
|
||||
#include <Magnum/GL/DefaultFramebuffer.h>
|
||||
#include <Magnum/GL/Renderer.h>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
|
||||
using namespace Magnum;
|
||||
using namespace Math::Literals;
|
||||
|
||||
namespace osim_viewer {
|
||||
|
||||
namespace {
|
||||
|
||||
Vector3 toVector3(const SimTK::Vec3& value) {
|
||||
return {Float(value[0]), Float(value[1]), Float(value[2])};
|
||||
}
|
||||
|
||||
std::shared_ptr<CpuMesh> buildCpuMesh(const SimTK::PolygonalMesh& mesh) {
|
||||
auto cpu = std::make_shared<CpuMesh>();
|
||||
std::vector<Vector3> positions;
|
||||
positions.reserve(mesh.getNumVertices());
|
||||
for(int i = 0; i != mesh.getNumVertices(); ++i) positions.push_back(toVector3(mesh.getVertexPosition(i)));
|
||||
|
||||
std::vector<Vector3> normals(positions.size(), Vector3{0.0f});
|
||||
cpu->indices.reserve(std::size_t(mesh.getNumFaces())*3);
|
||||
for(int face = 0; face != mesh.getNumFaces(); ++face) {
|
||||
const int faceVertexCount = mesh.getNumVerticesForFace(face);
|
||||
if(faceVertexCount < 3) continue;
|
||||
|
||||
const int a = mesh.getFaceVertex(face, 0);
|
||||
for(int i = 1; i < faceVertexCount - 1; ++i) {
|
||||
const int b = mesh.getFaceVertex(face, i);
|
||||
const int c = mesh.getFaceVertex(face, i + 1);
|
||||
const Vector3 normal = Math::cross(positions[b] - positions[a], positions[c] - positions[a]);
|
||||
normals[a] += normal;
|
||||
normals[b] += normal;
|
||||
normals[c] += normal;
|
||||
cpu->indices.push_back(Magnum::UnsignedInt(a));
|
||||
cpu->indices.push_back(Magnum::UnsignedInt(b));
|
||||
cpu->indices.push_back(Magnum::UnsignedInt(c));
|
||||
}
|
||||
}
|
||||
|
||||
Range3D bounds;
|
||||
bounds.min() = Vector3{std::numeric_limits<Float>::max()};
|
||||
bounds.max() = Vector3{-std::numeric_limits<Float>::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], {}));
|
||||
}
|
||||
cpu->bounds = bounds;
|
||||
return cpu;
|
||||
}
|
||||
|
||||
Matrix4 makeFrameMatrix(const Vector3& translation, const Quaternion& rotation) {
|
||||
return Matrix4::from(rotation.toMatrix(), translation);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ViewerApp::MeshDrawable::MeshDrawable(Object3D& object,
|
||||
Shaders::PhongGL& shader,
|
||||
GpuMesh& mesh,
|
||||
Color4 color,
|
||||
DrawableGroup3D& drawables):
|
||||
Drawable3D{object, &drawables},
|
||||
_shader{shader},
|
||||
_mesh{mesh},
|
||||
_color{color} {}
|
||||
|
||||
void ViewerApp::MeshDrawable::draw(const Matrix4& transformation, Camera3D& camera) {
|
||||
_shader
|
||||
.setLightPositions({{4.0f, 6.0f, 4.0f, 0.0f}})
|
||||
.setAmbientColor(Color4{_color.rgb()*0.35f, _color.a()})
|
||||
.setDiffuseColor(_color)
|
||||
.setTransformationMatrix(transformation)
|
||||
.setNormalMatrix(transformation.normalMatrix())
|
||||
.setProjectionMatrix(camera.projectionMatrix())
|
||||
.draw(_mesh.mesh);
|
||||
}
|
||||
|
||||
ViewerApp::ViewerApp(const Arguments& arguments, CliOptions options):
|
||||
Platform::Application{arguments, Configuration{}.setTitle("osim-magnum-viewer").setWindowFlags(Configuration::WindowFlag::Resizable)},
|
||||
_options{std::move(options)},
|
||||
_sceneData{loadScene(_options.modelPath, _options.motionPath, _options.geometryDirs)},
|
||||
_shader{Shaders::PhongGL{}},
|
||||
_isPlaying{!_options.startPaused},
|
||||
_playbackSpeed{_options.initialSpeed} {
|
||||
|
||||
_imgui = ImGuiIntegration::Context{Vector2{windowSize()} / dpiScaling(), windowSize(), framebufferSize()};
|
||||
|
||||
GL::Renderer::enable(GL::Renderer::Feature::DepthTest);
|
||||
GL::Renderer::enable(GL::Renderer::Feature::FaceCulling);
|
||||
GL::Renderer::setBlendEquation(GL::Renderer::BlendEquation::Add, GL::Renderer::BlendEquation::Add);
|
||||
GL::Renderer::setBlendFunction(GL::Renderer::BlendFunction::SourceAlpha, GL::Renderer::BlendFunction::OneMinusSourceAlpha);
|
||||
GL::Renderer::setClearColor(0x1e2329_rgbf);
|
||||
|
||||
_camera.setAspectRatioPolicy(SceneGraph::AspectRatioPolicy::Extend);
|
||||
_camera.setProjectionMatrix(Matrix4::perspectiveProjection(35.0_degf, Vector2{windowSize()}.aspectRatio(), 0.01f, 500.0f))
|
||||
.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());
|
||||
if(!_sceneData.times.empty()) _currentTime = _sceneData.times.front();
|
||||
|
||||
createSceneObjects();
|
||||
updateSceneAtCurrentTime();
|
||||
|
||||
_lastTick = std::chrono::steady_clock::now();
|
||||
setMinimalLoopPeriod(16.0_msec);
|
||||
}
|
||||
|
||||
std::shared_ptr<ViewerApp::GpuMesh> ViewerApp::uploadMesh(const CpuMesh& mesh) const {
|
||||
auto gpu = std::make_shared<GpuMesh>();
|
||||
gpu->bounds = mesh.bounds;
|
||||
gpu->vertexBuffer.setData(Containers::arrayView(mesh.vertices));
|
||||
gpu->indexBuffer.setData(Containers::arrayView(mesh.indices));
|
||||
gpu->mesh.setCount(Int(mesh.indices.size()))
|
||||
.setPrimitive(GL::MeshPrimitive::Triangles)
|
||||
.addVertexBuffer(gpu->vertexBuffer, 0,
|
||||
Shaders::PhongGL::Position{},
|
||||
Shaders::PhongGL::Normal{})
|
||||
.setIndexBuffer(gpu->indexBuffer, 0, GL::MeshIndexType::UnsignedInt);
|
||||
return gpu;
|
||||
}
|
||||
|
||||
void ViewerApp::createSceneObjects() {
|
||||
std::unordered_map<const CpuMesh*, std::shared_ptr<GpuMesh>> meshMap;
|
||||
auto markerCpu = buildCpuMesh(SimTK::PolygonalMesh::createSphereMesh(1.0, 1));
|
||||
_markerMesh = uploadMesh(*markerCpu);
|
||||
_gpuMeshes.push_back(_markerMesh);
|
||||
|
||||
for(const FrameTrack& frameTrack: _sceneData.frames) {
|
||||
auto frameObject = std::make_unique<Object3D>(&_scene);
|
||||
SceneFrame frame;
|
||||
frame.object = frameObject.get();
|
||||
|
||||
for(const GeometrySpec& geometry: frameTrack.geometries) {
|
||||
std::shared_ptr<GpuMesh> gpuMesh;
|
||||
if(const auto found = meshMap.find(geometry.mesh.get()); found != meshMap.end()) {
|
||||
gpuMesh = found->second;
|
||||
} else {
|
||||
gpuMesh = uploadMesh(*geometry.mesh);
|
||||
meshMap.emplace(geometry.mesh.get(), gpuMesh);
|
||||
_gpuMeshes.push_back(gpuMesh);
|
||||
}
|
||||
|
||||
auto geometryObject = std::make_unique<Object3D>(frame.object);
|
||||
geometryObject->setTransformation(Matrix4::scaling(geometry.scale));
|
||||
auto drawable = std::make_unique<MeshDrawable>(*geometryObject, _shader, *gpuMesh, geometry.color, _drawables);
|
||||
|
||||
frame.geometries.push_back({geometryObject.get(), geometry.color});
|
||||
_objectStorage.push_back(std::move(geometryObject));
|
||||
_drawableStorage.push_back(std::move(drawable));
|
||||
}
|
||||
|
||||
_frames.push_back(frame);
|
||||
_objectStorage.push_back(std::move(frameObject));
|
||||
}
|
||||
|
||||
constexpr Float MarkerRadius = 0.01f;
|
||||
for(const MarkerTrack& markerTrack: _sceneData.markers) {
|
||||
auto markerObject = std::make_unique<Object3D>(&_scene);
|
||||
markerObject->setTransformation(Matrix4::scaling(Vector3{MarkerRadius}));
|
||||
auto drawable = std::make_unique<MeshDrawable>(*markerObject, _shader, *_markerMesh, markerTrack.color, _drawables);
|
||||
_markers.push_back({markerObject.get(), markerTrack.color});
|
||||
_objectStorage.push_back(std::move(markerObject));
|
||||
_drawableStorage.push_back(std::move(drawable));
|
||||
}
|
||||
}
|
||||
|
||||
void ViewerApp::updatePlayback() {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const float delta = std::chrono::duration<float>(now - _lastTick).count();
|
||||
_lastTick = now;
|
||||
|
||||
if(!_isPlaying || _sceneData.times.empty()) return;
|
||||
|
||||
_currentTime += delta*_playbackSpeed;
|
||||
const float endTime = _sceneData.times.back();
|
||||
if(_currentTime >= endTime) {
|
||||
_currentTime = endTime;
|
||||
_isPlaying = false;
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t ViewerApp::sampleIndexForTime(const float time) const {
|
||||
if(_sceneData.times.size() < 2) return 0;
|
||||
auto it = std::lower_bound(_sceneData.times.begin(), _sceneData.times.end(), time);
|
||||
if(it == _sceneData.times.begin()) return 0;
|
||||
if(it == _sceneData.times.end()) return _sceneData.times.size() - 2;
|
||||
return std::size_t(std::distance(_sceneData.times.begin(), it) - 1);
|
||||
}
|
||||
|
||||
void ViewerApp::updateSceneAtCurrentTime() {
|
||||
if(_sceneData.times.empty()) return;
|
||||
|
||||
const std::size_t sample = sampleIndexForTime(_currentTime);
|
||||
const std::size_t next = Math::min(sample + 1, _sceneData.times.size() - 1);
|
||||
const float t0 = _sceneData.times[sample];
|
||||
const float t1 = _sceneData.times[next];
|
||||
const float alpha = (next == sample || t1 <= t0) ? 0.0f : (_currentTime - t0)/(t1 - t0);
|
||||
|
||||
for(std::size_t i = 0; i != _frames.size(); ++i) {
|
||||
const Vector3 translation = Math::lerp(_sceneData.frames[i].translations[sample], _sceneData.frames[i].translations[next], alpha);
|
||||
const Quaternion rotation = Math::slerp(_sceneData.frames[i].rotations[sample], _sceneData.frames[i].rotations[next], alpha).normalized();
|
||||
_frames[i].object->setTransformation(makeFrameMatrix(translation, rotation));
|
||||
}
|
||||
|
||||
for(std::size_t i = 0; i != _markers.size(); ++i) {
|
||||
const Vector3 position = Math::lerp(_sceneData.markers[i].positions[sample], _sceneData.markers[i].positions[next], alpha);
|
||||
_markers[i].object->setTransformation(Matrix4::translation(position)*Matrix4::scaling(Vector3{0.01f}));
|
||||
}
|
||||
}
|
||||
|
||||
void ViewerApp::drawUi() {
|
||||
ImGui::SetNextWindowPos(ImVec2(12.0f, 12.0f), ImGuiCond_Once);
|
||||
ImGui::SetNextWindowSize(ImVec2(360.0f, 0.0f), ImGuiCond_Once);
|
||||
ImGui::Begin("Playback");
|
||||
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;
|
||||
_isPlaying = !_isPlaying;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if(ImGui::Button("Rewind")) {
|
||||
_currentTime = 0.0f;
|
||||
_isPlaying = false;
|
||||
}
|
||||
ImGui::SliderFloat("Speed", &_playbackSpeed, 0.1f, 4.0f, "%.2fx");
|
||||
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::End();
|
||||
}
|
||||
|
||||
void ViewerApp::drawEvent() {
|
||||
updatePlayback();
|
||||
updateSceneAtCurrentTime();
|
||||
_cameraObject.setTransformation(_orbit.cameraTransform());
|
||||
|
||||
GL::defaultFramebuffer.clear(GL::FramebufferClear::Color | GL::FramebufferClear::Depth);
|
||||
_camera.draw(_drawables);
|
||||
|
||||
_imgui.newFrame();
|
||||
if(ImGui::GetIO().WantTextInput && !isTextInputActive()) startTextInput();
|
||||
else if(!ImGui::GetIO().WantTextInput && isTextInputActive()) stopTextInput();
|
||||
|
||||
drawUi();
|
||||
_imgui.updateApplicationCursor(*this);
|
||||
GL::Renderer::enable(GL::Renderer::Feature::Blending);
|
||||
GL::Renderer::enable(GL::Renderer::Feature::ScissorTest);
|
||||
GL::Renderer::disable(GL::Renderer::Feature::FaceCulling);
|
||||
GL::Renderer::disable(GL::Renderer::Feature::DepthTest);
|
||||
_imgui.drawFrame();
|
||||
GL::Renderer::enable(GL::Renderer::Feature::DepthTest);
|
||||
GL::Renderer::enable(GL::Renderer::Feature::FaceCulling);
|
||||
GL::Renderer::disable(GL::Renderer::Feature::ScissorTest);
|
||||
GL::Renderer::disable(GL::Renderer::Feature::Blending);
|
||||
|
||||
swapBuffers();
|
||||
redraw();
|
||||
}
|
||||
|
||||
void ViewerApp::viewportEvent(ViewportEvent& event) {
|
||||
GL::defaultFramebuffer.setViewport({{}, event.framebufferSize()});
|
||||
_imgui.relayout(Vector2{event.windowSize()} / event.dpiScaling(), event.windowSize(), event.framebufferSize());
|
||||
_camera.setProjectionMatrix(Matrix4::perspectiveProjection(35.0_degf, Vector2{event.windowSize()}.aspectRatio(), 0.01f, 500.0f))
|
||||
.setViewport(event.framebufferSize());
|
||||
_orbit.setViewport(event.windowSize(), event.framebufferSize());
|
||||
}
|
||||
|
||||
void ViewerApp::keyPressEvent(KeyEvent& event) {
|
||||
if(_imgui.handleKeyPressEvent(event)) return;
|
||||
|
||||
switch(event.key()) {
|
||||
case Key::Space:
|
||||
if(!_isPlaying && !_sceneData.times.empty() && _currentTime >= _sceneData.times.back()) _currentTime = 0.0f;
|
||||
_isPlaying = !_isPlaying;
|
||||
event.setAccepted();
|
||||
return;
|
||||
case Key::R:
|
||||
_currentTime = 0.0f;
|
||||
_isPlaying = false;
|
||||
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);
|
||||
event.setAccepted();
|
||||
return;
|
||||
}
|
||||
case Key::LeftBracket:
|
||||
_playbackSpeed = Math::max(_playbackSpeed*0.5f, 0.1f);
|
||||
event.setAccepted();
|
||||
return;
|
||||
case Key::RightBracket:
|
||||
_playbackSpeed = Math::min(_playbackSpeed*2.0f, 4.0f);
|
||||
event.setAccepted();
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ViewerApp::keyReleaseEvent(KeyEvent& event) {
|
||||
if(_imgui.handleKeyReleaseEvent(event)) return;
|
||||
}
|
||||
|
||||
void ViewerApp::pointerPressEvent(PointerEvent& event) {
|
||||
if(_imgui.handlePointerPressEvent(event)) return;
|
||||
_lastPointerPosition = event.position();
|
||||
if(event.pointer() == Pointer::MouseLeft) _dragMode = DragMode::Orbit;
|
||||
else if(event.pointer() == Pointer::MouseRight || event.pointer() == Pointer::MouseMiddle) _dragMode = DragMode::Pan;
|
||||
event.setAccepted();
|
||||
}
|
||||
|
||||
void ViewerApp::pointerReleaseEvent(PointerEvent& event) {
|
||||
if(_imgui.handlePointerReleaseEvent(event)) return;
|
||||
_dragMode = DragMode::None;
|
||||
_lastPointerPosition.reset();
|
||||
}
|
||||
|
||||
void ViewerApp::pointerMoveEvent(PointerMoveEvent& event) {
|
||||
if(_imgui.handlePointerMoveEvent(event)) return;
|
||||
if(_dragMode == DragMode::None || !_lastPointerPosition) return;
|
||||
|
||||
const Vector2 delta = event.position() - *_lastPointerPosition;
|
||||
_lastPointerPosition = event.position();
|
||||
if(_dragMode == DragMode::Orbit) _orbit.orbit(delta);
|
||||
else if(_dragMode == DragMode::Pan) _orbit.pan(delta);
|
||||
event.setAccepted();
|
||||
}
|
||||
|
||||
void ViewerApp::scrollEvent(ScrollEvent& event) {
|
||||
if(_imgui.handleScrollEvent(event)) {
|
||||
event.setAccepted();
|
||||
return;
|
||||
}
|
||||
_orbit.zoom(event.offset().y());
|
||||
event.setAccepted();
|
||||
}
|
||||
|
||||
void ViewerApp::textInputEvent(TextInputEvent& event) {
|
||||
if(_imgui.handleTextInputEvent(event)) return;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
#pragma once
|
||||
|
||||
#include "OpenSimLoader.hpp"
|
||||
#include "OrbitCameraController.hpp"
|
||||
|
||||
#include <Magnum/GL/Buffer.h>
|
||||
#include <Magnum/GL/Mesh.h>
|
||||
#include <Magnum/ImGuiIntegration/Context.hpp>
|
||||
#include <Magnum/Math/Color.h>
|
||||
#include <Magnum/Math/Matrix4.h>
|
||||
#include <Magnum/Math/Time.h>
|
||||
#include <Magnum/SceneGraph/Camera.h>
|
||||
#include <Magnum/SceneGraph/Drawable.h>
|
||||
#include <Magnum/SceneGraph/MatrixTransformation3D.h>
|
||||
#include <Magnum/SceneGraph/Object.h>
|
||||
#include <Magnum/SceneGraph/Scene.h>
|
||||
#include <Magnum/Shaders/PhongGL.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifdef CORRADE_TARGET_ANDROID
|
||||
#include <Magnum/Platform/AndroidApplication.h>
|
||||
#elif defined(CORRADE_TARGET_EMSCRIPTEN)
|
||||
#include <Magnum/Platform/EmscriptenApplication.h>
|
||||
#else
|
||||
#include <Magnum/Platform/Sdl2Application.h>
|
||||
#endif
|
||||
|
||||
namespace osim_viewer {
|
||||
|
||||
struct CliOptions {
|
||||
std::string modelPath;
|
||||
std::string motionPath;
|
||||
std::vector<std::string> geometryDirs;
|
||||
float initialSpeed = 1.0f;
|
||||
bool startPaused = false;
|
||||
};
|
||||
|
||||
class ViewerApp: public Magnum::Platform::Application {
|
||||
public:
|
||||
explicit ViewerApp(const Arguments& arguments, CliOptions options);
|
||||
|
||||
void drawEvent() override;
|
||||
void viewportEvent(ViewportEvent& event) override;
|
||||
void keyPressEvent(KeyEvent& event) override;
|
||||
void keyReleaseEvent(KeyEvent& event) override;
|
||||
void pointerPressEvent(PointerEvent& event) override;
|
||||
void pointerReleaseEvent(PointerEvent& event) override;
|
||||
void pointerMoveEvent(PointerMoveEvent& event) override;
|
||||
void scrollEvent(ScrollEvent& event) override;
|
||||
void textInputEvent(TextInputEvent& event) override;
|
||||
|
||||
private:
|
||||
using Object3D = Magnum::SceneGraph::Object<Magnum::SceneGraph::MatrixTransformation3D>;
|
||||
using Scene3D = Magnum::SceneGraph::Scene<Magnum::SceneGraph::MatrixTransformation3D>;
|
||||
using Camera3D = Magnum::SceneGraph::Camera3D;
|
||||
using Drawable3D = Magnum::SceneGraph::Drawable3D;
|
||||
using DrawableGroup3D = Magnum::SceneGraph::DrawableGroup3D;
|
||||
|
||||
struct GpuMesh {
|
||||
Magnum::GL::Mesh mesh;
|
||||
Magnum::GL::Buffer vertexBuffer;
|
||||
Magnum::GL::Buffer indexBuffer;
|
||||
Magnum::Range3D bounds;
|
||||
};
|
||||
|
||||
struct SceneGeometry {
|
||||
Object3D* object{};
|
||||
Magnum::Color4 color{1.0f};
|
||||
};
|
||||
|
||||
struct SceneFrame {
|
||||
Object3D* object{};
|
||||
std::vector<SceneGeometry> geometries;
|
||||
};
|
||||
|
||||
struct SceneMarker {
|
||||
Object3D* object{};
|
||||
Magnum::Color4 color{1.0f};
|
||||
};
|
||||
|
||||
class MeshDrawable final: public Drawable3D {
|
||||
public:
|
||||
MeshDrawable(Object3D& object,
|
||||
Magnum::Shaders::PhongGL& shader,
|
||||
GpuMesh& mesh,
|
||||
Magnum::Color4 color,
|
||||
DrawableGroup3D& drawables);
|
||||
|
||||
private:
|
||||
void draw(const Magnum::Matrix4& transformation, Camera3D& camera) override;
|
||||
|
||||
Magnum::Shaders::PhongGL& _shader;
|
||||
GpuMesh& _mesh;
|
||||
Magnum::Color4 _color;
|
||||
};
|
||||
|
||||
std::shared_ptr<GpuMesh> uploadMesh(const CpuMesh& mesh) const;
|
||||
void createSceneObjects();
|
||||
void updatePlayback();
|
||||
void updateSceneAtCurrentTime();
|
||||
void drawUi();
|
||||
std::size_t sampleIndexForTime(float time) const;
|
||||
|
||||
CliOptions _options;
|
||||
LoadedScene _sceneData;
|
||||
|
||||
Magnum::ImGuiIntegration::Context _imgui{Magnum::NoCreate};
|
||||
OrbitCameraController _orbit;
|
||||
|
||||
Scene3D _scene;
|
||||
Object3D _cameraObject{&_scene};
|
||||
Camera3D _camera{_cameraObject};
|
||||
DrawableGroup3D _drawables;
|
||||
|
||||
Magnum::Shaders::PhongGL _shader;
|
||||
std::vector<std::shared_ptr<GpuMesh>> _gpuMeshes;
|
||||
std::vector<std::unique_ptr<Drawable3D>> _drawableStorage;
|
||||
std::vector<std::unique_ptr<Object3D>> _objectStorage;
|
||||
std::vector<SceneFrame> _frames;
|
||||
std::vector<SceneMarker> _markers;
|
||||
|
||||
std::shared_ptr<GpuMesh> _markerMesh;
|
||||
|
||||
bool _isPlaying = false;
|
||||
float _playbackSpeed = 1.0f;
|
||||
float _currentTime = 0.0f;
|
||||
std::chrono::steady_clock::time_point _lastTick;
|
||||
|
||||
std::optional<Magnum::Vector2> _lastPointerPosition;
|
||||
enum class DragMode {
|
||||
None,
|
||||
Orbit,
|
||||
Pan,
|
||||
} _dragMode = DragMode::None;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#include "ViewerApp.hpp"
|
||||
|
||||
#include <CLI/App.hpp>
|
||||
#include <CLI/Config.hpp>
|
||||
#include <CLI/Formatter.hpp>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
osim_viewer::CliOptions options;
|
||||
|
||||
CLI::App cli{"Read-only OpenSim viewer"};
|
||||
cli.add_option("model", options.modelPath, "Path to model .osim")->required()->check(CLI::ExistingFile);
|
||||
cli.add_option("motion", options.motionPath, "Path to motion .mot")->required()->check(CLI::ExistingFile);
|
||||
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");
|
||||
|
||||
CLI11_PARSE(cli, argc, argv);
|
||||
|
||||
osim_viewer::ViewerApp app({argc, argv}, std::move(options));
|
||||
return app.exec();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user