Skip to content

Commit

Permalink
Make the model warper write warped meshes as OBJ to disk (#889)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamkewley committed Jun 17, 2024
1 parent ad537f2 commit f0c505d
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ class osc::UndoableModelStatePair::Impl final {
// crete a new commit graph that contains a backup of the given model
explicit Impl(std::unique_ptr<OpenSim::Model> m) :
m_Scratch{std::move(m)},
m_MaybeFilesystemLocation{TryFindInputFile(m_Scratch.getModel())}
m_MaybeFilesystemLocation{TryFindInputFile(m_Scratch.getModel()).value_or("")}
{
std::stringstream ss;
if (!m_MaybeFilesystemLocation.empty())
Expand Down
33 changes: 31 additions & 2 deletions src/OpenSimCreator/Documents/ModelWarper/CachedModelWarper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@
#include <OpenSimCreator/Documents/Model/BasicModelStatePair.h>
#include <OpenSimCreator/Documents/ModelWarper/ModelWarpDocument.h>
#include <OpenSimCreator/Utils/SimTKHelpers.h>
#include <oscar/Formats/OBJ.h>
#include <oscar/Platform/Log.h>
#include <oscar/Utils/Assertions.h>

#include <filesystem>
#include <fstream>
#include <map>
#include <memory>
#include <optional>
Expand All @@ -21,7 +24,7 @@ using namespace osc::mow;

namespace
{
std::unique_ptr<InMemoryMesh> WarpMesh(
std::unique_ptr<OpenSim::Geometry> WarpMesh(
ModelWarpDocument const& document,
OpenSim::Model const& model,
SimTK::State const& state,
Expand All @@ -35,7 +38,33 @@ namespace
compiled->warpInPlace(vertices);
mesh.set_vertices(vertices);
mesh.recalculate_normals();
return std::make_unique<InMemoryMesh>(mesh);

if (document.getShouldWriteWarpedMeshesToDisk()) {
// the mesh should be written to disk in an appropriate mesh file format
// and the resulting warped `OpenSim::Model` should link to the on-disk
// data via an `OpenSim::Mesh`

// figure out and prepare where the mesh data should be written
const auto warpedMeshesDir = document.getWarpedMeshesOutputDirectory();
OSC_ASSERT(warpedMeshesDir && "cannot figure out where to write warped mesh data: this will only work if the osim file was loaded from disk");
const auto meshLocationAbsPath = std::filesystem::weakly_canonical(*warpedMeshesDir / GetMeshFileName(inputMesh));
std::filesystem::create_directories(meshLocationAbsPath.parent_path()); // ensure parent directories are created

// write mesh data to disk as a Wavefront OBJ file
{
std::ofstream objStream{meshLocationAbsPath, std::ios::trunc};
objStream.exceptions(std::ios::badbit | std::ios::failbit);
write_as_obj(objStream, mesh, ObjMetadata{"osc-model-warper"});
}

// return an `OpenSim::Mesh` thank refers to the OBJ file
auto rv = std::make_unique<OpenSim::Mesh>();
rv->set_mesh_file(meshLocationAbsPath.string()); // TODO: should be relative-ized, where reasonable
return rv;
}
else {
return std::make_unique<InMemoryMesh>(mesh);
}
}

void OverwriteGeometry(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,15 @@ namespace osc::mow
bool getShouldDefaultMissingFrameWarpsToIdentity() const { return m_ShouldDefaultMissingFrameWarpsToIdentity; }
void setShouldDefaultMissingFrameWarpsToIdentity(bool v) { m_ShouldDefaultMissingFrameWarpsToIdentity = v; }

bool getShouldWriteWarpedMeshesToDisk() const { return m_ShouldWriteWarpedMeshesToDisk; }
void setShouldWriteWarpedMeshesToDisk(bool v) { m_ShouldWriteWarpedMeshesToDisk = v; }

std::filesystem::path getWarpedMeshesOutputDirectory() const { return m_WarpedMeshesOutputDirectory; }

private:
float m_WarpBlendingFactor = 1.0f;
bool m_ShouldDefaultMissingFrameWarpsToIdentity = false;
bool m_ShouldWriteWarpedMeshesToDisk = false;
std::filesystem::path m_WarpedMeshesOutputDirectory = "WarpedGeometry";
};
}
24 changes: 24 additions & 0 deletions src/OpenSimCreator/Documents/ModelWarper/ModelWarpDocument.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,30 @@ void osc::mow::ModelWarpDocument::setWarpBlendingFactor(float v)
m_ModelWarpConfig.upd()->setWarpBlendingFactor(v);
}

bool osc::mow::ModelWarpDocument::getShouldWriteWarpedMeshesToDisk() const
{
return m_ModelWarpConfig->getShouldWriteWarpedMeshesToDisk();
}

void osc::mow::ModelWarpDocument::setShouldWriteWarpedMeshesToDisk(bool v)
{
m_ModelWarpConfig.upd()->setShouldWriteWarpedMeshesToDisk(v);
}

std::optional<std::filesystem::path> osc::mow::ModelWarpDocument::getWarpedMeshesOutputDirectory() const
{
const auto osimFileLocation = getOsimFileLocation();
if (not osimFileLocation) {
return std::nullopt;
}
return std::filesystem::weakly_canonical(osimFileLocation->parent_path() / m_ModelWarpConfig->getWarpedMeshesOutputDirectory());
}

std::optional<std::filesystem::path> osc::mow::ModelWarpDocument::getOsimFileLocation() const
{
return TryFindInputFile(m_ModelState->getModel());
}

std::vector<ValidationCheckResult> osc::mow::ModelWarpDocument::implValidate() const
{
std::vector<ValidationCheckResult> rv;
Expand Down
8 changes: 8 additions & 0 deletions src/OpenSimCreator/Documents/ModelWarper/ModelWarpDocument.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <oscar/Utils/CopyOnUpdPtr.h>

#include <filesystem>
#include <optional>
#include <vector>

namespace OpenSim { class Mesh; }
Expand Down Expand Up @@ -49,6 +50,13 @@ namespace osc::mow
float getWarpBlendingFactor() const;
void setWarpBlendingFactor(float);

bool getShouldWriteWarpedMeshesToDisk() const;
void setShouldWriteWarpedMeshesToDisk(bool);

std::optional<std::filesystem::path> getWarpedMeshesOutputDirectory() const;

std::optional<std::filesystem::path> getOsimFileLocation() const;

ValidationCheckState state() const;

// only checks reference equality by leaning on the copy-on-write behavior
Expand Down
2 changes: 1 addition & 1 deletion src/OpenSimCreator/Graphics/SimTKMeshLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ namespace rgs = std::ranges;

namespace
{
constexpr auto c_supported_mesh_extensions = std::to_array({"obj"sv, "vtp"sv, "stl"sv});
constexpr auto c_supported_mesh_extensions = std::to_array({"obj"sv, "vtp"sv, "stl"sv, "stla"sv});

struct OutputMeshMetrics {
size_t numVertices = 0;
Expand Down
8 changes: 6 additions & 2 deletions src/OpenSimCreator/UI/ModelWarper/UIState.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ void osc::mow::UIState::actionWarpModelAndOpenInModelEditor()
return;
}

auto m = m_ModelWarper.warp(*m_Document);
m_TabHost->add_and_select_tab<ModelEditorTab>(*api, std::make_unique<UndoableModelStatePair>(m->getModel()));
// create a copy of the document so that we can apply export-specific
// configuration changes to it
ModelWarpDocument copy{*m_Document};
copy.setShouldWriteWarpedMeshesToDisk(true); // required for OpenSim to be able to load the warped model correctly
auto warpedModelStatePair = m_ModelWarper.warp(copy);
m_TabHost->add_and_select_tab<ModelEditorTab>(*api, std::make_unique<UndoableModelStatePair>(warpedModelStatePair->getModel()));
}
17 changes: 11 additions & 6 deletions src/OpenSimCreator/Utils/OpenSimHelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -740,16 +740,15 @@ bool osc::HasInputFileName(const OpenSim::Model& m)
return !name.empty() && name != "Unassigned";
}

std::filesystem::path osc::TryFindInputFile(const OpenSim::Model& m)
std::optional<std::filesystem::path> osc::TryFindInputFile(const OpenSim::Model& m)
{
if (!HasInputFileName(m)) {
return {};
if (not HasInputFileName(m)) {
return std::nullopt;
}

std::filesystem::path p{m.getInputFileName()};

if (!std::filesystem::exists(p)) {
return {};
if (not std::filesystem::exists(p)) {
return std::nullopt;
}

return p;
Expand Down Expand Up @@ -780,6 +779,12 @@ std::optional<std::filesystem::path> osc::FindGeometryFileAbsPath(
return std::optional<std::filesystem::path>{std::filesystem::weakly_canonical({attempts.back()})};
}

std::string osc::GetMeshFileName(const OpenSim::Mesh& mesh)
{
std::filesystem::path p{mesh.get_mesh_file()};
return p.filename().string();
}

bool osc::ShouldShowInUI(const OpenSim::Component& c)
{
if (dynamic_cast<const OpenSim::PathWrapPoint*>(&c)) {
Expand Down
5 changes: 4 additions & 1 deletion src/OpenSimCreator/Utils/OpenSimHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -497,14 +497,17 @@ namespace osc
// returns a non-empty path if the given model has an input file name that exists on the user's filesystem
//
// otherwise, returns an empty path
std::filesystem::path TryFindInputFile(const OpenSim::Model&);
std::optional<std::filesystem::path> TryFindInputFile(const OpenSim::Model&);

// returns the absolute path to the given mesh component, if found (otherwise, std::nullptr)
std::optional<std::filesystem::path> FindGeometryFileAbsPath(
const OpenSim::Model&,
const OpenSim::Mesh&
);

// returns the filename part of the `mesh_file` property (e.g. `C:\Users\adam\mesh.obj` returns `mesh.obj`)
std::string GetMeshFileName(const OpenSim::Mesh&);

// returns `true` if the component should be shown in the UI
//
// this uses heuristics to determine whether the component is something the UI should be
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,42 +23,42 @@ namespace
}
}

TEST(ModelWarpingDocument, CanDefaultConstruct)
TEST(ModelWarpDocument, CanDefaultConstruct)
{
ASSERT_NO_THROW({ ModelWarpDocument{}; });
}

TEST(ModelWarpingDocument, CanConstructFromPathToOsim)
TEST(ModelWarpDocument, CanConstructFromPathToOsim)
{
ASSERT_NO_THROW({ ModelWarpDocument{GetFixturesDir() / "blank.osim"}; });
}

TEST(ModelWarpingDocument, ConstructorThrowsIfGivenInvalidOsimPath)
TEST(ModelWarpDocument, ConstructorThrowsIfGivenInvalidOsimPath)
{
ASSERT_THROW({ ModelWarpDocument{std::filesystem::path{"bs.osim"}}; }, std::exception);
}

TEST(ModelWarpingDocument, AfterConstructingFromBasicOsimFileTheReturnedModelContainsExpectedComponents)
TEST(ModelWarpDocument, AfterConstructingFromBasicOsimFileTheReturnedModelContainsExpectedComponents)
{
ModelWarpDocument const doc{GetFixturesDir() / "onebody.osim"};
doc.model().getComponent("bodyset/some_body");
}

TEST(ModelWarpingDocument, DefaultConstructedIsInAnOKState)
TEST(ModelWarpDocument, DefaultConstructedIsInAnOKState)
{
// i.e. it is possible to warp a blank model
ModelWarpDocument const doc;
ASSERT_EQ(doc.state(), ValidationCheckState::Ok);
}

TEST(ModelWarpingDocument, BlankOsimFileIsInAnOKState)
TEST(ModelWarpDocument, BlankOsimFileIsInAnOKState)
{
// a blank document is also warpable (albeit, trivially)
ModelWarpDocument const doc{GetFixturesDir() / "blank.osim"};
ASSERT_EQ(doc.state(), ValidationCheckState::Ok);
}

TEST(ModelWarpingDocument, OneBodyIsInAnOKState)
TEST(ModelWarpDocument, OneBodyIsInAnOKState)
{
// the onebody example isn't warpable, because it can't figure out how to warp
// the offset frame in it (the user _must_ specify that they want to ignore it, or
Expand All @@ -67,15 +67,15 @@ TEST(ModelWarpingDocument, OneBodyIsInAnOKState)
ASSERT_EQ(doc.state(), ValidationCheckState::Error);
}

TEST(ModelWarpingDocument, SparselyNamedPairedIsInAnOKState)
TEST(ModelWarpDocument, SparselyNamedPairedIsInAnOKState)
{
// the landmarks in this example are sparesely named, but fully paired, and the
// model contains no PhysicalOffsetFrames to worry about, so it's fine
ModelWarpDocument const doc{GetFixturesDir() / "SparselyNamedPaired" / "model.osim"};
ASSERT_EQ(doc.state(), ValidationCheckState::Ok);
}

TEST(ModelWarpingDocument, SimpleUnnamedIsInAnErrorState)
TEST(ModelWarpDocument, SimpleUnnamedIsInAnErrorState)
{
// the model is simple, and has landmarks on the source mesh, but there is no
// destination mesh/landmarks, and the user hasn't specified any overrides
Expand All @@ -84,7 +84,7 @@ TEST(ModelWarpingDocument, SimpleUnnamedIsInAnErrorState)
ASSERT_EQ(doc.state(), ValidationCheckState::Error);
}

TEST(ModelWarpingDocument, SimpleIsInAnErrorState)
TEST(ModelWarpDocument, SimpleIsInAnErrorState)
{
// the model is simple, and has named landmarks on the source mesh, but there
// is no destination mesh/landmarks, and the user hasn't specified any overrides
Expand All @@ -93,43 +93,43 @@ TEST(ModelWarpingDocument, SimpleIsInAnErrorState)
ASSERT_EQ(doc.state(), ValidationCheckState::Error);
}

TEST(ModelWarpingDocument, PairedIsInAnOKState)
TEST(ModelWarpDocument, PairedIsInAnOKState)
{
// the model is simple and has fully paired meshes+landmarks: it can be warped
ModelWarpDocument const doc{GetFixturesDir() / "Paired" / "model.osim"};
ASSERT_EQ(doc.state(), ValidationCheckState::Ok);
}

TEST(ModelWarpingDocument, MissingSourceLMsIsInAnErrorState)
TEST(ModelWarpDocument, MissingSourceLMsIsInAnErrorState)
{
// the model is simple, has source+destination meshes, but is missing landmark
// data for a source mesh: unwarpable
ModelWarpDocument const doc{GetFixturesDir() / "MissingSourceLMs" / "model.osim"};
ASSERT_EQ(doc.state(), ValidationCheckState::Error);
}

TEST(ModelWarpingDocument, MissingDestinationLMsIsInAnErrorState)
TEST(ModelWarpDocument, MissingDestinationLMsIsInAnErrorState)
{
// the model is simple, has source+destination meshes, but is missing landmark
// data for a destination mesh: unwarpable
ModelWarpDocument const doc{GetFixturesDir() / "MissingDestinationLMs" / "model.osim"};
ASSERT_EQ(doc.state(), ValidationCheckState::Error);
}

TEST(ModelWarpingDocument, PofPairedIsInAnErrorState)
TEST(ModelWarpDocument, PofPairedIsInAnErrorState)
{
// the model has fully-paired meshes (good), but contains `PhysicalOffsetFrame`s
// that haven't been explicitly handled by the user (ignored, least-squares fit, etc.)
ModelWarpDocument const doc{GetFixturesDir() / "PofPaired" / "model.osim"};
ASSERT_EQ(doc.state(), ValidationCheckState::Error);
}

TEST(ModelWarpingDocument, WarpBlendingFactorInitiallyOne)
TEST(ModelWarpDocument, WarpBlendingFactorInitiallyOne)
{
ASSERT_EQ(ModelWarpDocument{}.getWarpBlendingFactor(), 1.0f);
}

TEST(ModelWarpingDocument, WarpBlendingFactorClampedBetweenZeroAndOne)
TEST(ModelWarpDocument, WarpBlendingFactorClampedBetweenZeroAndOne)
{
ModelWarpDocument doc;
ASSERT_EQ(doc.getWarpBlendingFactor(), 1.0f);
Expand All @@ -140,3 +140,54 @@ TEST(ModelWarpingDocument, WarpBlendingFactorClampedBetweenZeroAndOne)
doc.setWarpBlendingFactor(1.0f);
ASSERT_EQ(doc.getWarpBlendingFactor(), 1.0f);
}

TEST(ModelWarpDocument, getShouldWriteWarpedMeshesToDisk_InitiallyFalse)
{
// this might be important, because the UI performs _much_ better if it doesn't
// have to write the warped meshes to disk. So it should be an explicit operation
// when the caller (e.g. the export process) actually needs this behavior (e.g.
// because OpenSim is going to expect on-disk mesh data)
ASSERT_FALSE(ModelWarpDocument{}.getShouldWriteWarpedMeshesToDisk());
}

TEST(ModelWarpDocument, setShouldWriteWarpedMeshesToDisk_CanBeUsedToSetBehaviorToTrue)
{
ModelWarpDocument doc;

ASSERT_FALSE(doc.getShouldWriteWarpedMeshesToDisk());
doc.setShouldWriteWarpedMeshesToDisk(true);
ASSERT_TRUE(doc.getShouldWriteWarpedMeshesToDisk());
}

TEST(ModelWarpDocument, setShouldWriteWarpedMeshesToDisk_ChangesEquality)
{
ModelWarpDocument a;
ModelWarpDocument b = a;
ASSERT_EQ(a, b);
b.setShouldWriteWarpedMeshesToDisk(true);
ASSERT_NE(a, b);
}

TEST(ModelWarpDocument, getWarpedMeshesOutputDirectory_ReturnsNulloptWhenNoOsimProvided)
{
ASSERT_EQ(ModelWarpDocument{}.getWarpedMeshesOutputDirectory(), std::nullopt);
}

TEST(ModelWarpDocument, getWarpedMeshesOutputDirectory_ReturnsNonNulloptWhenOsimProvied)
{
const std::filesystem::path fileLocation = GetFixturesDir() / "blank.osim";
const ModelWarpDocument doc{fileLocation};
ASSERT_NE(ModelWarpDocument{}.getWarpedMeshesOutputDirectory(), std::nullopt);
}

TEST(ModelWarpDocument, getOsimFileLocation_ReturnsNulloptOnDefaultConstruction)
{
ASSERT_EQ(ModelWarpDocument{}.getOsimFileLocation(), std::nullopt);
}

TEST(ModelWarpDocument, getOsimFileLocation_ReturnsProvidedOsimFileLocationWHenConstructedFromPath)
{
const std::filesystem::path fileLocation = GetFixturesDir() / "blank.osim";
const ModelWarpDocument doc{fileLocation};
ASSERT_EQ(doc.getOsimFileLocation(), fileLocation);
}

0 comments on commit f0c505d

Please sign in to comment.