import/obj.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // SPDX-License-Identifier: BSD-3-Clause | ||
| 2 | #include "chrono_batch_render/import/obj.h" | ||
| 3 | |||
| 4 | #include <algorithm> | ||
| 5 | #include <cctype> | ||
| 6 | #include <cmath> | ||
| 7 | #include <map> | ||
| 8 | #include <set> | ||
| 9 | #include <span> | ||
| 10 | #include <sstream> | ||
| 11 | #include <string> | ||
| 12 | #include <unordered_map> | ||
| 13 | #include <utility> | ||
| 14 | |||
| 15 | #include <fmt/format.h> | ||
| 16 | |||
| 17 | #include <tiny_obj_loader.h> | ||
| 18 | |||
| 19 | #include "chrono_batch_render/util/error.h" | ||
| 20 | #include "chrono_batch_render/util/log.h" | ||
| 21 | #include "chrono_batch_render/util/profiler.h" | ||
| 22 | #include "chrono_batch_render/util/text.h" | ||
| 23 | |||
| 24 | namespace chrono::batch_render { | ||
| 25 | namespace { | ||
| 26 | |||
| 27 | 26 | std::string ToIdentifier(std::string_view text) { | |
| 28 | 26 | std::string identifier; | |
| 29 | 129 | for (const unsigned char c : text) { | |
| 30 | 206 | identifier += std::isalnum(c) ? char(c) : '_'; | |
| 31 | } | ||
| 32 | 26 | if (!identifier.empty() && std::isdigit((unsigned char)identifier.front())) { | |
| 33 | ✗ | identifier.insert(0, "_"); | |
| 34 | } | ||
| 35 | 26 | return identifier; | |
| 36 | ✗ | } | |
| 37 | |||
| 38 | 2 | class ChildNames { | |
| 39 | public: | ||
| 40 | 26 | std::string Take(std::string_view name, std::string_view fallback) { | |
| 41 | 26 | std::string identifier = ToIdentifier(name); | |
| 42 | 26 | if (!IsIdentifier(identifier)) { | |
| 43 | ✗ | identifier = std::string(fallback); | |
| 44 | } | ||
| 45 | 26 | std::string taken = identifier; | |
| 46 | 26 | for (int n = 2; !m_taken.insert(taken).second; ++n) { | |
| 47 | ✗ | taken = fmt::format("{}_{}", identifier, n); | |
| 48 | } | ||
| 49 | 26 | return taken; | |
| 50 | 26 | } | |
| 51 | |||
| 52 | private: | ||
| 53 | std::set<std::string> m_taken; | ||
| 54 | }; | ||
| 55 | |||
| 56 | 6 | Float3 Cross(Float3 a, Float3 b) { | |
| 57 | 6 | return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x}; | |
| 58 | } | ||
| 59 | |||
| 60 | 6 | Float3 Normalize(Float3 v) { | |
| 61 | 6 | const float length = std::sqrt(v.x * v.x + v.y * v.y + v.z * v.z); | |
| 62 | 6 | return length > 0 ? Float3{v.x / length, v.y / length, v.z / length} : Float3{0, 0, 1}; | |
| 63 | } | ||
| 64 | |||
| 65 | class MeshBuilder { | ||
| 66 | public: | ||
| 67 | 14 | MeshBuilder(const tinyobj::attrib_t& attributes, const std::filesystem::path& file) | |
| 68 | 14 | : m_attributes(attributes), m_file(file), | |
| 69 | 14 | m_colors(!attributes.colors.empty() && | |
| 70 | 14 | attributes.colors.size() == attributes.vertices.size()) {} | |
| 71 | |||
| 72 | 17 | void AddFace(std::span<const tinyobj::index_t> corners, uint32_t smoothing) { | |
| 73 | 17 | m_desc.faceVertexCounts.push_back(int32_t(corners.size())); | |
| 74 | 17 | m_smoothing.push_back(smoothing); | |
| 75 | 73 | for (const tinyobj::index_t& corner : corners) { | |
| 76 | 57 | m_desc.faceVertexIndices.push_back(Point(corner.vertex_index)); | |
| 77 | 56 | AddNormal(corner.normal_index); | |
| 78 | 56 | AddSt(corner.texcoord_index); | |
| 79 | } | ||
| 80 | 16 | } | |
| 81 | |||
| 82 | 13 | Mesh::Desc Finish(std::string material) && { | |
| 83 | 13 | if (!m_normals) { | |
| 84 | 10 | m_desc.normals = SmoothNormals(); | |
| 85 | } | ||
| 86 | 13 | m_desc.normalsInterpolation = Interpolation::FaceVarying; | |
| 87 | 13 | if (m_st) { | |
| 88 | 6 | m_desc.primvars.push_back(Primvar::Create("st", Interpolation::FaceVarying, m_stValues, | |
| 89 | 3 | std::move(m_stIndices))); | |
| 90 | } | ||
| 91 | 13 | if (m_colors) { | |
| 92 | 4 | m_desc.primvars.push_back( | |
| 93 | 4 | Primvar::Create("displayColor", Interpolation::Vertex, m_colorValues)); | |
| 94 | } | ||
| 95 | 13 | if (!material.empty()) { | |
| 96 | 24 | m_desc.materialBinding = {std::move(material)}; | |
| 97 | } | ||
| 98 | 13 | m_desc.subdivisionScheme = SubdivisionScheme::None; | |
| 99 | 13 | return std::move(m_desc); | |
| 100 | 8 | } | |
| 101 | |||
| 102 | 13 | uint32_t FaceCount() const { | |
| 103 | 13 | return uint32_t(m_desc.faceVertexCounts.size()); | |
| 104 | } | ||
| 105 | |||
| 106 | 13 | uint32_t PointCount() const { | |
| 107 | 13 | return uint32_t(m_desc.points.size()); | |
| 108 | } | ||
| 109 | |||
| 110 | private: | ||
| 111 | 81 | void Check(int index, size_t count, std::string_view what) const { | |
| 112 | 81 | if (index < 0 || size_t(index) >= count) { | |
| 113 | ✗ | throw Error("Reading \"{}\" as OBJ: a face uses {} {} of {}", m_file.string(), what, | |
| 114 | 2 | index + 1, count); | |
| 115 | } | ||
| 116 | 80 | } | |
| 117 | |||
| 118 | 57 | int32_t Point(int vertex) { | |
| 119 | 57 | const std::vector<tinyobj::real_t>& v = m_attributes.vertices; | |
| 120 | 57 | Check(vertex, v.size() / 3, "vertex"); | |
| 121 | 56 | const auto [found, added] = m_points.try_emplace(vertex, int32_t(m_desc.points.size())); | |
| 122 | 56 | if (added) { | |
| 123 | 49 | m_desc.points.push_back({v[3 * vertex], v[3 * vertex + 1], v[3 * vertex + 2]}); | |
| 124 | 49 | if (m_colors) { | |
| 125 | 7 | const std::vector<tinyobj::real_t>& c = m_attributes.colors; | |
| 126 | 7 | m_colorValues.push_back({c[3 * vertex], c[3 * vertex + 1], c[3 * vertex + 2]}); | |
| 127 | } | ||
| 128 | } | ||
| 129 | 56 | return found->second; | |
| 130 | } | ||
| 131 | |||
| 132 | 56 | void AddNormal(int normal) { | |
| 133 | 56 | if (normal < 0) { | |
| 134 | 44 | m_normals = false; | |
| 135 | } | ||
| 136 | 56 | if (!m_normals) { | |
| 137 | return; | ||
| 138 | } | ||
| 139 | 12 | const std::vector<tinyobj::real_t>& n = m_attributes.normals; | |
| 140 | 24 | Check(normal, n.size() / 3, "normal"); | |
| 141 | 12 | m_desc.normals.push_back({n[3 * normal], n[3 * normal + 1], n[3 * normal + 2]}); | |
| 142 | } | ||
| 143 | |||
| 144 | 56 | void AddSt(int texcoord) { | |
| 145 | 56 | if (texcoord < 0) { | |
| 146 | 44 | m_st = false; | |
| 147 | } | ||
| 148 | 56 | if (!m_st) { | |
| 149 | 44 | return; | |
| 150 | } | ||
| 151 | 12 | const std::vector<tinyobj::real_t>& t = m_attributes.texcoords; | |
| 152 | 12 | Check(texcoord, t.size() / 2, "texture coordinate"); | |
| 153 | 12 | const auto [found, added] = m_stSlots.try_emplace(texcoord, int32_t(m_stValues.size())); | |
| 154 | 12 | if (added) { | |
| 155 | 12 | m_stValues.push_back({t[2 * texcoord], t[2 * texcoord + 1]}); | |
| 156 | } | ||
| 157 | 12 | m_stIndices.push_back(found->second); | |
| 158 | } | ||
| 159 | |||
| 160 | 10 | std::vector<Float3> SmoothNormals() const { | |
| 161 | 10 | if (std::ranges::all_of(m_smoothing, [](uint32_t group) { return group == 0; })) { | |
| 162 | 9 | return {}; | |
| 163 | } | ||
| 164 | 1 | const std::vector<Float3>& points = m_desc.points; | |
| 165 | 1 | const std::vector<int32_t>& indices = m_desc.faceVertexIndices; | |
| 166 | 1 | std::vector<Float3> faces; | |
| 167 | 1 | std::map<std::pair<int32_t, uint32_t>, Float3> sums; | |
| 168 | 1 | size_t corner = 0; | |
| 169 | 3 | for (size_t face = 0; face < m_smoothing.size(); ++face) { | |
| 170 | 2 | const size_t count = size_t(m_desc.faceVertexCounts[face]); | |
| 171 | 2 | Float3 area; | |
| 172 | 8 | for (size_t i = 0; i < count; ++i) { | |
| 173 | 6 | const Float3 a = points[size_t(indices[corner + i])]; | |
| 174 | 6 | const Float3 b = points[size_t(indices[corner + (i + 1) % count])]; | |
| 175 | 6 | const Float3 edge = Cross(a, b); | |
| 176 | 6 | area = {area.x + edge.x, area.y + edge.y, area.z + edge.z}; | |
| 177 | } | ||
| 178 | 2 | faces.push_back(area); | |
| 179 | 2 | if (m_smoothing[face] != 0) { | |
| 180 | 8 | for (size_t i = 0; i < count; ++i) { | |
| 181 | 6 | Float3& sum = sums[{indices[corner + i], m_smoothing[face]}]; | |
| 182 | 6 | sum = {sum.x + area.x, sum.y + area.y, sum.z + area.z}; | |
| 183 | } | ||
| 184 | } | ||
| 185 | 2 | corner += count; | |
| 186 | } | ||
| 187 | 1 | std::vector<Float3> normals; | |
| 188 | 1 | normals.reserve(indices.size()); | |
| 189 | corner = 0; | ||
| 190 | 3 | for (size_t face = 0; face < m_smoothing.size(); ++face) { | |
| 191 | 2 | const size_t count = size_t(m_desc.faceVertexCounts[face]); | |
| 192 | 8 | for (size_t i = 0; i < count; ++i) { | |
| 193 | 6 | normals.push_back(Normalize( | |
| 194 | 6 | m_smoothing[face] == 0 ? faces[face] | |
| 195 | 12 | : sums.at({indices[corner + i], m_smoothing[face]}))); | |
| 196 | } | ||
| 197 | 2 | corner += count; | |
| 198 | } | ||
| 199 | 1 | return normals; | |
| 200 | 1 | } | |
| 201 | |||
| 202 | const tinyobj::attrib_t& m_attributes; | ||
| 203 | const std::filesystem::path& m_file; | ||
| 204 | Mesh::Desc m_desc; | ||
| 205 | std::unordered_map<int, int32_t> m_points; | ||
| 206 | std::unordered_map<int, int32_t> m_stSlots; | ||
| 207 | std::vector<Float2> m_stValues; | ||
| 208 | std::vector<int32_t> m_stIndices; | ||
| 209 | std::vector<Float3> m_colorValues; | ||
| 210 | std::vector<uint32_t> m_smoothing; | ||
| 211 | bool m_normals = true; | ||
| 212 | bool m_st = true; | ||
| 213 | bool m_colors = false; | ||
| 214 | }; | ||
| 215 | |||
| 216 | 16 | std::string_view ScalarChannel(const tinyobj::texture_option_t& option) { | |
| 217 | 16 | switch (option.imfchan) { | |
| 218 | ✗ | case 'g': | |
| 219 | ✗ | return "g"; | |
| 220 | ✗ | case 'b': | |
| 221 | ✗ | return "b"; | |
| 222 | 16 | default: | |
| 223 | 16 | return "r"; | |
| 224 | } | ||
| 225 | } | ||
| 226 | |||
| 227 | 13 | std::string Text(const std::vector<std::byte>& bytes) { | |
| 228 | 13 | return {reinterpret_cast<const char*>(bytes.data()), bytes.size()}; | |
| 229 | } | ||
| 230 | |||
| 231 | ✗ | class SourceMaterials : public tinyobj::MaterialReader { | |
| 232 | public: | ||
| 233 | 8 | explicit SourceMaterials(const ImportSource& source) : m_source(source) {} | |
| 234 | |||
| 235 | 5 | bool operator()(const std::string& name, std::vector<tinyobj::material_t>* materials, | |
| 236 | std::map<std::string, int>* ids, std::string* warning, | ||
| 237 | std::string* error) override { | ||
| 238 | 5 | FileBytes bytes; | |
| 239 | 5 | try { | |
| 240 | 10 | bytes = m_source.Read(name); | |
| 241 | ✗ | } catch (const Error& missing) { | |
| 242 | ✗ | *warning += std::string(missing.what()) + "\n"; | |
| 243 | ✗ | return false; | |
| 244 | ✗ | } | |
| 245 | 5 | std::istringstream text(Text(*bytes)); | |
| 246 | 5 | tinyobj::LoadMtl(ids, materials, &text, warning, error); | |
| 247 | 5 | return true; | |
| 248 | 10 | } | |
| 249 | |||
| 250 | private: | ||
| 251 | const ImportSource& m_source; | ||
| 252 | }; | ||
| 253 | |||
| 254 | struct LookTexture { | ||
| 255 | std::string input; | ||
| 256 | std::string output; | ||
| 257 | size_t placement = 0; | ||
| 258 | UsdUVTexture::Desc texture; | ||
| 259 | }; | ||
| 260 | |||
| 261 | 6 | struct Look { | |
| 262 | UsdPreviewSurface::Desc surface; | ||
| 263 | std::vector<LookTexture> textures; | ||
| 264 | std::vector<TexturePlacement::Desc> placements; | ||
| 265 | }; | ||
| 266 | |||
| 267 | 6 | void DefineLook(Scene& scene, std::string_view at, const Look& look) { | |
| 268 | 6 | const std::string root(at); | |
| 269 | 6 | const Prim material = scene.Define<Material>(root); | |
| 270 | 6 | const Prim surface = scene.Define<UsdPreviewSurface>(root + "/Surface", look.surface); | |
| 271 | 6 | material.Connect("surface", surface, "surface"); | |
| 272 | 6 | std::vector<Prim> placements; | |
| 273 | 8 | for (size_t index = 0; index < look.placements.size(); ++index) { | |
| 274 | 2 | const std::string name = index == 0 ? "Placement" : fmt::format("Placement_{}", index + 1); | |
| 275 | 2 | placements.push_back( | |
| 276 | 4 | scene.Define<TexturePlacement>(root + "/" + name, look.placements[index])); | |
| 277 | 2 | } | |
| 278 | 8 | for (const LookTexture& texture : look.textures) { | |
| 279 | 2 | std::string name = texture.input; | |
| 280 | 2 | name[0] = char(std::toupper(static_cast<unsigned char>(name[0]))); | |
| 281 | 2 | const Prim read = | |
| 282 | 4 | scene.Define<UsdUVTexture>(root + "/" + name + "Texture", texture.texture); | |
| 283 | 2 | read.Connect("st", placements[texture.placement], "st"); | |
| 284 | 2 | surface.Connect(texture.input, read, texture.output); | |
| 285 | 2 | } | |
| 286 | 12 | } | |
| 287 | |||
| 288 | class SurfaceBuilder { | ||
| 289 | public: | ||
| 290 | 6 | SurfaceBuilder(const tinyobj::material_t& material, std::string_view path, | |
| 291 | const ImportSource& source, ImportPlan& plan) | ||
| 292 | 6 | : m_material(material), m_path(path), m_source(source), m_plan(plan) {} | |
| 293 | |||
| 294 | 6 | Look Build() && { | |
| 295 | 6 | const tinyobj::material_t& m = m_material; | |
| 296 | 6 | UsdPreviewSurface::Desc& surface = m_look.surface; | |
| 297 | 6 | surface.diffuseColor = {m.diffuse[0], m.diffuse[1], m.diffuse[2]}; | |
| 298 | 6 | Map("diffuseColor", "map_Kd", m.diffuse_texname, m.diffuse_texopt); | |
| 299 | 6 | surface.emissiveColor = {m.emission[0], m.emission[1], m.emission[2]}; | |
| 300 | 6 | Map("emissiveColor", "map_Ke", m.emissive_texname, m.emissive_texopt); | |
| 301 | 6 | surface.opacity = m.dissolve; | |
| 302 | 6 | Map("opacity", "map_d", m.alpha_texname, m.alpha_texopt, | |
| 303 | 6 | m.alpha_texname == m.diffuse_texname ? "a" : ScalarChannel(m.alpha_texopt)); | |
| 304 | 6 | surface.roughness = m.roughness > 0 ? m.roughness | |
| 305 | 6 | : m.shininess > 0 ? std::pow(2 / (m.shininess + 2), 0.25f) | |
| 306 | : 0.5f; | ||
| 307 | 12 | Map("roughness", "map_Pr", m.roughness_texname, m.roughness_texopt, | |
| 308 | 6 | ScalarChannel(m.roughness_texopt)); | |
| 309 | 6 | surface.metallic = m.metallic; | |
| 310 | 12 | Map("metallic", "map_Pm", m.metallic_texname, m.metallic_texopt, | |
| 311 | 6 | ScalarChannel(m.metallic_texopt)); | |
| 312 | 6 | if (m.ior > 1) { | |
| 313 | ✗ | surface.ior = m.ior; | |
| 314 | } | ||
| 315 | 6 | if (m.clearcoat_thickness > 0) { | |
| 316 | ✗ | surface.clearcoat = m.clearcoat_thickness; | |
| 317 | ✗ | surface.clearcoatRoughness = m.clearcoat_roughness; | |
| 318 | } | ||
| 319 | 6 | if (Map("normal", "norm", m.normal_texname, m.normal_texopt)) { | |
| 320 | ✗ | surface.normalStrength = m.normal_texopt.bump_multiplier; | |
| 321 | 6 | } else if (Map("normal", "map_Bump", m.bump_texname, m.bump_texopt)) { | |
| 322 | ✗ | Log(LogLevel::Debug, "{} reads its map_Bump \"{}\" as a normal map", m_path, | |
| 323 | m.bump_texname); | ||
| 324 | ✗ | surface.normalStrength = m.bump_texopt.bump_multiplier; | |
| 325 | } | ||
| 326 | 6 | Ignore("map_Ka", m.ambient_texname); | |
| 327 | 6 | Ignore("map_Ks", m.specular_texname); | |
| 328 | 6 | Ignore("map_Ns", m.specular_highlight_texname); | |
| 329 | 6 | Ignore("disp", m.displacement_texname); | |
| 330 | 6 | Ignore("refl", m.reflection_texname); | |
| 331 | 6 | Ignore("map_Ps", m.sheen_texname); | |
| 332 | 6 | if (m.sheen > 0) { | |
| 333 | ✗ | m_ignored.push_back("Ps"); | |
| 334 | } | ||
| 335 | 6 | if (m.anisotropy > 0) { | |
| 336 | ✗ | m_ignored.push_back("aniso"); | |
| 337 | } | ||
| 338 | 6 | if (!m_ignored.empty()) { | |
| 339 | ✗ | Log(LogLevel::Debug, "{} ignores {}, which UsdPreviewSurface has no input for", m_path, | |
| 340 | ✗ | Join(m_ignored)); | |
| 341 | } | ||
| 342 | 6 | return std::move(m_look); | |
| 343 | } | ||
| 344 | |||
| 345 | private: | ||
| 346 | 42 | bool Map(std::string_view input, std::string_view statement, const std::string& name, | |
| 347 | const tinyobj::texture_option_t& option, std::string_view output = "rgb") { | ||
| 348 | 42 | if (name.empty()) { | |
| 349 | return false; | ||
| 350 | } | ||
| 351 | 5 | std::string file; | |
| 352 | 5 | if (FileBytes bytes = m_source.Find(name)) { | |
| 353 | 2 | file = m_plan.AddFile(name, std::move(bytes)); | |
| 354 | } else { | ||
| 355 | 3 | const std::filesystem::path located = m_source.Locate(name); | |
| 356 | 3 | if (!std::filesystem::exists(located)) { | |
| 357 | 3 | Log(LogLevel::Warning, "{}'s {} \"{}\" doesn't exist; {} stays untextured", m_path, | |
| 358 | 3 | statement, located.string(), input); | |
| 359 | 3 | return false; | |
| 360 | } | ||
| 361 | ✗ | file = located.generic_string(); | |
| 362 | 3 | } | |
| 363 | 2 | const bool color = input == "diffuseColor" || input == "emissiveColor"; | |
| 364 | 2 | LookTexture texture{ | |
| 365 | 2 | .input = std::string(input), .output = std::string(output), .placement = Place(option)}; | |
| 366 | 2 | texture.texture.file = std::move(file); | |
| 367 | 2 | texture.texture.sourceColorSpace = color ? TextureColorSpace::SRGB : TextureColorSpace::Raw; | |
| 368 | 2 | if (option.clamp) { | |
| 369 | 1 | texture.texture.wrapS = TextureWrapMode::Clamp; | |
| 370 | 1 | texture.texture.wrapT = TextureWrapMode::Clamp; | |
| 371 | } | ||
| 372 | 2 | if (input == "normal") { | |
| 373 | ✗ | texture.texture.scale = {2, 2, 2, 1}; | |
| 374 | ✗ | texture.texture.bias = {-1, -1, -1, 0}; | |
| 375 | } | ||
| 376 | 2 | m_look.textures.push_back(std::move(texture)); | |
| 377 | 2 | return true; | |
| 378 | 7 | } | |
| 379 | |||
| 380 | 2 | size_t Place(const tinyobj::texture_option_t& option) { | |
| 381 | 2 | const TexturePlacement::Desc placement{ | |
| 382 | 2 | .uvScale = {option.scale[0], option.scale[1]}, | |
| 383 | 2 | .uvOffset = {option.origin_offset[0], option.origin_offset[1]}}; | |
| 384 | 2 | std::vector<TexturePlacement::Desc>& placements = m_look.placements; | |
| 385 | 2 | const auto found = std::ranges::find_if(placements, [&](const TexturePlacement::Desc& p) { | |
| 386 | 1 | return p.uvScale == placement.uvScale && p.uvOffset == placement.uvOffset; | |
| 387 | }); | ||
| 388 | 2 | if (found != placements.end()) { | |
| 389 | ✗ | return size_t(found - placements.begin()); | |
| 390 | } | ||
| 391 | 2 | placements.push_back(placement); | |
| 392 | 2 | return placements.size() - 1; | |
| 393 | 2 | } | |
| 394 | |||
| 395 | 36 | void Ignore(std::string_view statement, const std::string& name) { | |
| 396 | 6 | if (!name.empty()) { | |
| 397 | ✗ | m_ignored.emplace_back(statement); | |
| 398 | } | ||
| 399 | } | ||
| 400 | |||
| 401 | const tinyobj::material_t& m_material; | ||
| 402 | std::string m_path; | ||
| 403 | const ImportSource& m_source; | ||
| 404 | ImportPlan& m_plan; | ||
| 405 | Look m_look; | ||
| 406 | std::vector<std::string> m_ignored; | ||
| 407 | }; | ||
| 408 | |||
| 409 | 8 | struct ObjData { | |
| 410 | tinyobj::attrib_t attributes; | ||
| 411 | std::vector<tinyobj::shape_t> shapes; | ||
| 412 | std::vector<tinyobj::material_t> materials; | ||
| 413 | }; | ||
| 414 | |||
| 415 | 9 | ObjData Parse(const ImportSource& source) { | |
| 416 | 9 | const std::filesystem::path& file = source.file; | |
| 417 | 17 | std::istringstream text(Text(*source.ReadEntry())); | |
| 418 | 8 | SourceMaterials reader(source); | |
| 419 | 8 | ObjData data; | |
| 420 | 8 | std::string warnings; | |
| 421 | 8 | std::string errors; | |
| 422 | 8 | if (!tinyobj::LoadObj(&data.attributes, &data.shapes, &data.materials, &warnings, &errors, | |
| 423 | &text, &reader, false, false)) { | ||
| 424 | ✗ | throw Error("Reading \"{}\" as OBJ: {}", file.string(), errors); | |
| 425 | } | ||
| 426 | 8 | const std::string messages = warnings + errors; | |
| 427 | 19 | for (const std::string_view warning : Split(messages, '\n')) { | |
| 428 | 11 | if (!warning.empty()) { | |
| 429 | 4 | Log(LogLevel::Warning, "reading \"{}\": {}", file.string(), warning); | |
| 430 | } | ||
| 431 | ✗ | } | |
| 432 | 8 | Log(LogLevel::Debug, "\"{}\" has {} shapes, {} materials and {} vertices", file.string(), | |
| 433 | 8 | data.shapes.size(), data.materials.size(), data.attributes.vertices.size() / 3); | |
| 434 | 16 | return data; | |
| 435 | 8 | } | |
| 436 | |||
| 437 | 8 | std::vector<std::string> DefineMaterials(ImportPlan& plan, std::string_view path, | |
| 438 | const ObjData& data, const ImportSource& source, | ||
| 439 | ImportContext& context, ChildNames& children) { | ||
| 440 | 8 | std::vector<std::string> looks; | |
| 441 | 8 | if (data.materials.empty()) { | |
| 442 | return looks; | ||
| 443 | } | ||
| 444 | 5 | const std::string scope = fmt::format("{}/{}", path, children.Take("Looks", "Looks")); | |
| 445 | 10 | plan.Define<Scope>(scope); | |
| 446 | 5 | ChildNames names; | |
| 447 | 11 | for (const tinyobj::material_t& material : data.materials) { | |
| 448 | 6 | context.Report("materials", looks.size(), data.materials.size()); | |
| 449 | 6 | const std::string look = fmt::format("{}/{}", scope, names.Take(material.name, "Material")); | |
| 450 | 24 | plan.Add(look, [built = SurfaceBuilder(material, look, source, plan).Build()]( | |
| 451 | 6 | Scene& scene, std::string_view at) { DefineLook(scene, at, built); }); | |
| 452 | 6 | Log(LogLevel::Debug, "read OBJ material \"{}\" for {}", material.name, look); | |
| 453 | 6 | looks.push_back(look); | |
| 454 | 6 | } | |
| 455 | 5 | context.Report("materials", looks.size(), data.materials.size()); | |
| 456 | 5 | return looks; | |
| 457 | 10 | } | |
| 458 | |||
| 459 | 8 | void DefineMeshes(ImportPlan& plan, std::string_view path, const ObjData& data, | |
| 460 | std::span<const std::string> looks, const ImportSource& source, | ||
| 461 | ImportContext& context, ChildNames& children) { | ||
| 462 | 8 | const std::vector<tinyobj::shape_t>& shapes = data.shapes; | |
| 463 | 21 | for (size_t index = 0; index < shapes.size(); ++index) { | |
| 464 | 14 | context.Report("shapes", index, shapes.size()); | |
| 465 | 14 | const tinyobj::shape_t& shape = shapes[index]; | |
| 466 | 14 | if (!shape.lines.indices.empty() || !shape.points.indices.empty()) { | |
| 467 | ✗ | Log(LogLevel::Warning, | |
| 468 | "\"{}\" skips the lines and points of shape \"{}\", which import as nothing yet", | ||
| 469 | ✗ | source.file.string(), shape.name); | |
| 470 | } | ||
| 471 | 14 | if (shape.mesh.num_face_vertices.empty()) { | |
| 472 | 12 | continue; | |
| 473 | } | ||
| 474 | 14 | MeshBuilder builder(data.attributes, source.file); | |
| 475 | 14 | std::map<int, std::vector<int32_t>> faces; | |
| 476 | 14 | size_t corner = 0; | |
| 477 | 30 | for (size_t face = 0; face < shape.mesh.num_face_vertices.size(); ++face) { | |
| 478 | 17 | const size_t count = shape.mesh.num_face_vertices[face]; | |
| 479 | 17 | const int material = | |
| 480 | 17 | face < shape.mesh.material_ids.size() ? shape.mesh.material_ids[face] : -1; | |
| 481 | 17 | const bool bound = material >= 0 && size_t(material) < looks.size(); | |
| 482 | 17 | faces[bound ? material : -1].push_back(int32_t(face)); | |
| 483 | 17 | const uint32_t smoothing = face < shape.mesh.smoothing_group_ids.size() | |
| 484 | 17 | ? shape.mesh.smoothing_group_ids[face] | |
| 485 | 17 | : 0; | |
| 486 | 17 | builder.AddFace(std::span(shape.mesh.indices).subspan(corner, count), smoothing); | |
| 487 | 16 | corner += count; | |
| 488 | } | ||
| 489 | 13 | const bool single = faces.size() <= 1; | |
| 490 | 13 | const int only = faces.empty() ? -1 : faces.begin()->first; | |
| 491 | 14 | const std::string mesh = fmt::format("{}/{}", path, children.Take(shape.name, "Mesh")); | |
| 492 | 13 | const uint32_t faceCount = builder.FaceCount(); | |
| 493 | 13 | const uint32_t pointCount = builder.PointCount(); | |
| 494 | 13 | plan.Define<Mesh>( | |
| 495 | 26 | mesh, std::move(builder).Finish(single && only >= 0 ? looks[size_t(only)] : "")); | |
| 496 | 13 | Log(LogLevel::Debug, "read OBJ shape \"{}\" for {} with {} faces and {} points", shape.name, | |
| 497 | mesh, faceCount, pointCount); | ||
| 498 | 13 | if (single) { | |
| 499 | 12 | continue; | |
| 500 | } | ||
| 501 | 1 | ChildNames subsets; | |
| 502 | 4 | for (auto& [material, indices] : faces) { | |
| 503 | 3 | if (material < 0) { | |
| 504 | 1 | continue; | |
| 505 | } | ||
| 506 | 2 | const std::string subset = fmt::format( | |
| 507 | 2 | "{}/{}", mesh, subsets.Take(data.materials[size_t(material)].name, "Subset")); | |
| 508 | 8 | plan.Define<GeomSubset>(subset, {.materialBinding = {looks[size_t(material)]}, | |
| 509 | .indices = std::move(indices), | ||
| 510 | 2 | .familyName = "materialBind"}); | |
| 511 | 2 | } | |
| 512 | 14 | } | |
| 513 | 7 | context.Report("shapes", shapes.size(), shapes.size()); | |
| 514 | 13 | } | |
| 515 | |||
| 516 | 3 | class ObjImporter final : public Importer { | |
| 517 | public: | ||
| 518 | 18 | std::string Name() const override { | |
| 519 | 18 | return "OBJ"; | |
| 520 | } | ||
| 521 | |||
| 522 | 3 | std::vector<std::string> Extensions() const override { | |
| 523 | 3 | return {".obj"}; | |
| 524 | } | ||
| 525 | |||
| 526 | 9 | ImportPlan Read(std::string_view path, const ImportSource& source, | |
| 527 | ImportContext& context) override { | ||
| 528 | 9 | CHRONO_BATCH_RENDER_ZONE("Import/Obj"); | |
| 529 | 9 | context.Report("parsing", 0, 1); | |
| 530 | 9 | const ObjData data = Parse(source); | |
| 531 | 8 | context.Report("parsing", 1, 1); | |
| 532 | |||
| 533 | 8 | const double half = std::sqrt(0.5); | |
| 534 | 8 | const float units = context.MetersPerUnit().value_or(1); | |
| 535 | 8 | const bool yUp = context.Up().value_or(UpAxis::Y) == UpAxis::Y; | |
| 536 | 8 | ImportPlan plan; | |
| 537 | 17 | plan.Define<Xform>(std::string(path), {.orient = yUp ? Quatd{half, 0, 0, half} : Quatd{}, | |
| 538 | .scale = {units, units, units}}); | ||
| 539 | 8 | ChildNames children; | |
| 540 | 8 | const std::vector<std::string> looks = | |
| 541 | 8 | DefineMaterials(plan, path, data, source, context, children); | |
| 542 | 8 | DefineMeshes(plan, path, data, looks, source, context, children); | |
| 543 | 7 | return plan; | |
| 544 | 20 | } | |
| 545 | }; | ||
| 546 | |||
| 547 | } | ||
| 548 | |||
| 549 | 3 | std::shared_ptr<Importer> CreateObjImporter() { | |
| 550 | 3 | return std::make_shared<ObjImporter>(); | |
| 551 | } | ||
| 552 | |||
| 553 | } | ||
| 554 |