mesh/io/mesh_io.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "terraingen/mesh/io/mesh_io.h" | ||
| 2 | |||
| 3 | #include "terraingen/mesh/io/formats.h" | ||
| 4 | #include "terraingen/mesh/io/icq.h" | ||
| 5 | |||
| 6 | #include <cctype> | ||
| 7 | #include <cmath> | ||
| 8 | #include <fstream> | ||
| 9 | #include <stdexcept> | ||
| 10 | #include <string> | ||
| 11 | |||
| 12 | namespace terraingen { | ||
| 13 | |||
| 14 | namespace { | ||
| 15 | |||
| 16 | /// The extension of `path`, lower case. | ||
| 17 | 224 | std::string Extension(const std::filesystem::path& path) { | |
| 18 | 224 | std::string extension = path.extension().string(); | |
| 19 | 1127 | for (char& c : extension) { | |
| 20 | 903 | c = static_cast<char>(std::tolower(static_cast<unsigned char>(c))); | |
| 21 | } | ||
| 22 | 224 | return extension; | |
| 23 | } | ||
| 24 | |||
| 25 | /// Throws for no meshes or nodes, an empty or invalid mesh, mismatched normals, a node naming no | ||
| 26 | /// mesh, or a node scale or pose that cannot be written. | ||
| 27 | 77 | void CheckScene(const std::vector<SceneMesh>& meshes, const std::vector<SceneNode>& nodes) { | |
| 28 | 77 | if (meshes.empty() || nodes.empty()) { | |
| 29 | 1 | throw std::invalid_argument("A mesh scene needs meshes and nodes"); | |
| 30 | } | ||
| 31 | 512 | for (const SceneMesh& scene : meshes) { | |
| 32 | 438 | if (scene.mesh.positions.empty() || scene.mesh.triangles.empty()) { | |
| 33 | 1 | throw std::invalid_argument("Cannot write an empty mesh"); | |
| 34 | } | ||
| 35 | 437 | if (!scene.normals.empty() && scene.normals.size() != scene.mesh.positions.size()) { | |
| 36 | 1 | throw std::invalid_argument("Normals must be empty or one per vertex"); | |
| 37 | } | ||
| 38 | 436 | ValidateMesh(scene.mesh); | |
| 39 | } | ||
| 40 | 570 | for (const SceneNode& node : nodes) { | |
| 41 | 499 | if (node.mesh >= meshes.size()) { | |
| 42 | 1 | throw std::invalid_argument("A scene node names a mesh the scene does not have"); | |
| 43 | } | ||
| 44 | 1989 | for (const double s : {node.scale.x, node.scale.y, node.scale.z}) { | |
| 45 | 1492 | if (!(std::isfinite(s) && s != 0.0)) { | |
| 46 | 1 | throw std::invalid_argument("Node scales must be finite and not zero"); | |
| 47 | } | ||
| 48 | } | ||
| 49 | 497 | RequireRotation(node.pose.rotation); | |
| 50 | } | ||
| 51 | 71 | } | |
| 52 | |||
| 53 | } // namespace | ||
| 54 | |||
| 55 | namespace detail { | ||
| 56 | |||
| 57 | 284137 | Vec3d Placed(const SceneNode& node, const Vec3d& p) { | |
| 58 | 284137 | return node.pose.Apply({p.x * node.scale.x, p.y * node.scale.y, p.z * node.scale.z}); | |
| 59 | } | ||
| 60 | |||
| 61 | 41957 | Vec3d PlacedNormal(const SceneNode& node, const Vec3d& n) { | |
| 62 | 41957 | const Vec3d scaled{n.x / node.scale.x, n.y / node.scale.y, n.z / node.scale.z}; | |
| 63 | 41957 | const auto& r = node.pose.rotation; | |
| 64 | 41957 | const Vec3d rotated{r[0] * scaled.x + r[1] * scaled.y + r[2] * scaled.z, | |
| 65 | 41957 | r[3] * scaled.x + r[4] * scaled.y + r[5] * scaled.z, | |
| 66 | 83914 | r[6] * scaled.x + r[7] * scaled.y + r[8] * scaled.z}; | |
| 67 | 41957 | return rotated * (1.0 / Length(rotated)); | |
| 68 | } | ||
| 69 | |||
| 70 | 32 | void Save(const std::filesystem::path& path, const std::string& bytes) { | |
| 71 | 32 | std::ofstream file(path, std::ios::binary); | |
| 72 | 32 | file.write(bytes.data(), static_cast<std::streamsize>(bytes.size())); | |
| 73 | 32 | if (!file) { | |
| 74 | ✗ | throw std::runtime_error("Cannot write " + path.string()); | |
| 75 | } | ||
| 76 | 32 | } | |
| 77 | |||
| 78 | } // namespace detail | ||
| 79 | |||
| 80 | 123 | bool IsMeshPath(const std::filesystem::path& path) { | |
| 81 | 123 | const std::string extension = Extension(path); | |
| 82 | 146 | return extension == ".obj" || extension == ".gltf" || extension == ".glb" || | |
| 83 | 146 | extension == ".stl"; | |
| 84 | 123 | } | |
| 85 | |||
| 86 | 79 | void WriteScene(const std::filesystem::path& path, const std::vector<SceneMesh>& meshes, | |
| 87 | const std::vector<SceneNode>& nodes) { | ||
| 88 | 79 | if (!IsMeshPath(path)) { | |
| 89 | 4 | throw std::invalid_argument("Mesh files must end in .obj, .gltf, .glb or .stl: " + | |
| 90 | 6 | path.string()); | |
| 91 | } | ||
| 92 | 77 | CheckScene(meshes, nodes); | |
| 93 | 71 | const std::string extension = Extension(path); | |
| 94 | 71 | if (extension == ".obj") { | |
| 95 | 10 | detail::WriteObj(path, meshes, nodes); | |
| 96 | 61 | } else if (extension == ".stl") { | |
| 97 | 9 | detail::WriteStl(path, meshes, nodes); | |
| 98 | } else { | ||
| 99 | 52 | detail::WriteGltf(path, meshes, nodes); | |
| 100 | } | ||
| 101 | 71 | } | |
| 102 | |||
| 103 | 51 | void WriteMesh(const std::filesystem::path& path, const SceneMesh& mesh) { | |
| 104 | 263 | WriteScene(path, {mesh}, {SceneNode{}}); | |
| 105 | 102 | } | |
| 106 | |||
| 107 | 30 | SceneMesh ReadMesh(const std::filesystem::path& path) { | |
| 108 | 30 | const std::string extension = Extension(path); | |
| 109 | 30 | SceneMesh read; | |
| 110 | 30 | if (extension == ".obj") { | |
| 111 | 13 | read = detail::ReadObj(path); | |
| 112 | 17 | } else if (extension == ".gltf" || extension == ".glb") { | |
| 113 | 10 | read = detail::ReadGltf(path); | |
| 114 | 7 | } else if (extension == ".stl") { | |
| 115 | 4 | read = detail::ReadStl(path); | |
| 116 | 3 | } else if (extension == ".icq") { | |
| 117 | 2 | read.mesh = IcqToMesh(ReadIcq(path)); | |
| 118 | } else { | ||
| 119 | 2 | throw std::invalid_argument("Meshes are read from .obj, .gltf, .glb, .stl or .icq: " + | |
| 120 | 3 | path.string()); | |
| 121 | } | ||
| 122 | 26 | if (read.mesh.triangles.empty()) { | |
| 123 | 1 | throw std::runtime_error("No triangles in " + path.string()); | |
| 124 | } | ||
| 125 | 25 | ValidateMesh(read.mesh); | |
| 126 | 50 | return read; | |
| 127 | 35 | } | |
| 128 | |||
| 129 | } // namespace terraingen | ||
| 130 |