chrono_batch_render/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
321 / 354
90.7%
Branches:
263 / 343
76.7%
Functions:
28 / 28
100.0%

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
2/2
✓ Branch 0 taken 103 times.
✓ Branch 1 taken 26 times.
129 for (const unsigned char c : text) {
30
1/2
✓ Branch 0 taken 103 times.
✗ Branch 1 not taken.
206 identifier += std::isalnum(c) ? char(c) : '_';
31 }
32
2/4
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 26 times.
26 if (!identifier.empty() && std::isdigit((unsigned char)identifier.front())) {
33
0/1
✗ Branch 1 not taken.
✗ 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
2/3
✓ Branch 1 taken 26 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 26 times.
26 if (!IsIdentifier(identifier)) {
43
0/1
✗ Branch 1 not taken.
✗ identifier = std::string(fallback);
44 }
45
1/1
✓ Branch 1 taken 26 times.
26 std::string taken = identifier;
46
2/3
✓ Branch 1 taken 26 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 26 times.
26 for (int n = 2; !m_taken.insert(taken).second; ++n) {
47
0/1
✗ Branch 1 not taken.
✗ 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
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 const float length = std::sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
62
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
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
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 12 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
14 m_colors(!attributes.colors.empty() &&
70
3/4
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
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
2/2
✓ Branch 0 taken 57 times.
✓ Branch 1 taken 16 times.
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
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 3 times.
13 if (!m_normals) {
84 10 m_desc.normals = SmoothNormals();
85 }
86 13 m_desc.normalsInterpolation = Interpolation::FaceVarying;
87
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 10 times.
13 if (m_st) {
88
1/1
✓ Branch 1 taken 3 times.
6 m_desc.primvars.push_back(Primvar::Create("st", Interpolation::FaceVarying, m_stValues,
89
1/1
✓ Branch 1 taken 3 times.
3 std::move(m_stIndices)));
90 }
91
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 11 times.
13 if (m_colors) {
92 4 m_desc.primvars.push_back(
93
2/2
✓ Branch 1 taken 2 times.
✓ Branch 4 taken 2 times.
4 Primvar::Create("displayColor", Interpolation::Vertex, m_colorValues));
94 }
95
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 5 times.
13 if (!material.empty()) {
96
3/5
✓ Branch 1 taken 8 times.
✓ Branch 3 taken 8 times.
✓ Branch 4 taken 8 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
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
3/4
✓ Branch 0 taken 81 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 80 times.
81 if (index < 0 || size_t(index) >= count) {
113 ✗ throw Error("Reading \"{}\" as OBJ: a face uses {} {} of {}", m_file.string(), what,
114
2/2
✓ Branch 2 taken 1 time.
✓ Branch 5 taken 1 time.
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
2/2
✓ Branch 1 taken 49 times.
✓ Branch 2 taken 7 times.
56 const auto [found, added] = m_points.try_emplace(vertex, int32_t(m_desc.points.size()));
122
2/2
✓ Branch 0 taken 49 times.
✓ Branch 1 taken 7 times.
56 if (added) {
123 49 m_desc.points.push_back({v[3 * vertex], v[3 * vertex + 1], v[3 * vertex + 2]});
124
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 42 times.
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
2/2
✓ Branch 0 taken 44 times.
✓ Branch 1 taken 12 times.
56 if (normal < 0) {
134 44 m_normals = false;
135 }
136
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 44 times.
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
2/2
✓ Branch 0 taken 44 times.
✓ Branch 1 taken 12 times.
56 if (texcoord < 0) {
146 44 m_st = false;
147 }
148
2/2
✓ Branch 0 taken 44 times.
✓ Branch 1 taken 12 times.
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
1/2
✓ Branch 1 taken 12 times.
✗ Branch 2 not taken.
12 const auto [found, added] = m_stSlots.try_emplace(texcoord, int32_t(m_stValues.size()));
154
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
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
2/2
✓ Branch 1 taken 9 times.
✓ Branch 2 taken 1 time.
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
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 time.
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
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2 times.
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
1/1
✓ Branch 1 taken 2 times.
2 faces.push_back(area);
179
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (m_smoothing[face] != 0) {
180
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2 times.
8 for (size_t i = 0; i < count; ++i) {
181
1/1
✓ Branch 1 taken 6 times.
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/1
✓ Branch 1 taken 1 time.
1 normals.reserve(indices.size());
189 corner = 0;
190
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 time.
3 for (size_t face = 0; face < m_smoothing.size(); ++face) {
191 2 const size_t count = size_t(m_desc.faceVertexCounts[face]);
192
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2 times.
8 for (size_t i = 0; i < count; ++i) {
193
1/1
✓ Branch 2 taken 6 times.
6 normals.push_back(Normalize(
194
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 m_smoothing[face] == 0 ? faces[face]
195
1/1
✓ Branch 1 taken 6 times.
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
1/3
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
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
2/3
✓ Branch 1 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
10 bytes = m_source.Read(name);
241
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ } catch (const Error& missing) {
242
0/2
✗ Branch 2 not taken.
✗ Branch 5 not taken.
✗ *warning += std::string(missing.what()) + "\n";
243 ✗ return false;
244 ✗ }
245
2/4
✓ Branch 1 taken 5 times.
✓ Branch 4 taken 5 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
5 std::istringstream text(Text(*bytes));
246
1/1
✓ Branch 1 taken 5 times.
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
2/2
✓ Branch 1 taken 6 times.
✓ Branch 4 taken 6 times.
6 const Prim surface = scene.Define<UsdPreviewSurface>(root + "/Surface", look.surface);
271
1/1
✓ Branch 1 taken 6 times.
6 material.Connect("surface", surface, "surface");
272 6 std::vector<Prim> placements;
273
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 6 times.
8 for (size_t index = 0; index < look.placements.size(); ++index) {
274
5/5
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
✓ Branch 3 taken 1 time.
✓ Branch 6 taken 1 time.
✓ Branch 9 taken 2 times.
2 const std::string name = index == 0 ? "Placement" : fmt::format("Placement_{}", index + 1);
275 2 placements.push_back(
276
4/4
✓ Branch 1 taken 2 times.
✓ Branch 4 taken 2 times.
✓ Branch 7 taken 2 times.
✓ Branch 10 taken 2 times.
4 scene.Define<TexturePlacement>(root + "/" + name, look.placements[index]));
277 2 }
278
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 6 times.
8 for (const LookTexture& texture : look.textures) {
279
1/1
✓ Branch 1 taken 2 times.
2 std::string name = texture.input;
280
1/1
✓ Branch 1 taken 2 times.
2 name[0] = char(std::toupper(static_cast<unsigned char>(name[0])));
281 2 const Prim read =
282
3/3
✓ Branch 1 taken 2 times.
✓ Branch 4 taken 2 times.
✓ Branch 7 taken 2 times.
4 scene.Define<UsdUVTexture>(root + "/" + name + "Texture", texture.texture);
283
1/1
✓ Branch 1 taken 2 times.
2 read.Connect("st", placements[texture.placement], "st");
284
1/1
✓ Branch 1 taken 2 times.
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
1/1
✓ Branch 2 taken 6 times.
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
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 4 times.
6 m.alpha_texname == m.diffuse_texname ? "a" : ScalarChannel(m.alpha_texopt));
304
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 surface.roughness = m.roughness > 0 ? m.roughness
305
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
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
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (m.ior > 1) {
313 ✗ surface.ior = m.ior;
314 }
315
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (m.clearcoat_thickness > 0) {
316 ✗ surface.clearcoat = m.clearcoat_thickness;
317 ✗ surface.clearcoatRoughness = m.clearcoat_roughness;
318 }
319
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
6 if (Map("normal", "norm", m.normal_texname, m.normal_texopt)) {
320 ✗ surface.normalStrength = m.normal_texopt.bump_multiplier;
321
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
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
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 Ignore("map_Ka", m.ambient_texname);
327
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 Ignore("map_Ks", m.specular_texname);
328
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 Ignore("map_Ns", m.specular_highlight_texname);
329
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 Ignore("disp", m.displacement_texname);
330
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 Ignore("refl", m.reflection_texname);
331
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 Ignore("map_Ps", m.sheen_texname);
332
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (m.sheen > 0) {
333 ✗ m_ignored.push_back("Ps");
334 }
335
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (m.anisotropy > 0) {
336 ✗ m_ignored.push_back("aniso");
337 }
338
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (!m_ignored.empty()) {
339
0/1
✗ Branch 2 not taken.
✗ 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
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 37 times.
42 if (name.empty()) {
349 return false;
350 }
351
1/1
✓ Branch 1 taken 5 times.
5 std::string file;
352
3/3
✓ Branch 1 taken 5 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 3 times.
5 if (FileBytes bytes = m_source.Find(name)) {
353
2/5
✓ Branch 1 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
2 file = m_plan.AddFile(name, std::move(bytes));
354 } else {
355
1/1
✓ Branch 1 taken 3 times.
3 const std::filesystem::path located = m_source.Locate(name);
356
2/3
✓ Branch 1 taken 3 times.
✓ Branch 3 taken 3 times.
✗ Branch 4 not taken.
3 if (!std::filesystem::exists(located)) {
357
1/1
✓ Branch 1 taken 3 times.
3 Log(LogLevel::Warning, "{}'s {} \"{}\" doesn't exist; {} stays untextured", m_path,
358
1/1
✓ Branch 1 taken 3 times.
3 statement, located.string(), input);
359 3 return false;
360 }
361
0/1
✗ Branch 1 not taken.
✗ file = located.generic_string();
362 3 }
363
3/4
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 time.
2 const bool color = input == "diffuseColor" || input == "emissiveColor";
364
1/1
✓ Branch 1 taken 2 times.
2 LookTexture texture{
365
4/4
✓ Branch 1 taken 2 times.
✓ Branch 4 taken 2 times.
✓ Branch 7 taken 2 times.
✓ Branch 10 taken 2 times.
2 .input = std::string(input), .output = std::string(output), .placement = Place(option)};
366 2 texture.texture.file = std::move(file);
367
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
2 texture.texture.sourceColorSpace = color ? TextureColorSpace::SRGB : TextureColorSpace::Raw;
368
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
2 if (option.clamp) {
369 1 texture.texture.wrapS = TextureWrapMode::Clamp;
370 1 texture.texture.wrapT = TextureWrapMode::Clamp;
371 }
372
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (input == "normal") {
373 ✗ texture.texture.scale = {2, 2, 2, 1};
374 ✗ texture.texture.bias = {-1, -1, -1, 0};
375 }
376
1/1
✓ Branch 1 taken 2 times.
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/2
✓ Branch 1 taken 2 times.
✓ Branch 4 taken 2 times.
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/4
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
1 return p.uvScale == placement.uvScale && p.uvOffset == placement.uvOffset;
387 });
388
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (found != placements.end()) {
389 ✗ return size_t(found - placements.begin());
390 }
391
1/1
✓ Branch 1 taken 2 times.
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/12
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 6 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 6 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 6 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 6 times.
✗ Branch 10 not taken.
✓ Branch 11 taken 6 times.
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
1/1
✓ Branch 1 taken 8 times.
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
3/4
✓ Branch 2 taken 8 times.
✓ Branch 5 taken 8 times.
✓ Branch 7 taken 8 times.
✗ Branch 8 not taken.
17 std::istringstream text(Text(*source.ReadEntry()));
418 8 SourceMaterials reader(source);
419 8 ObjData data;
420
1/1
✓ Branch 1 taken 8 times.
8 std::string warnings;
421 8 std::string errors;
422
2/3
✓ Branch 1 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
8 if (!tinyobj::LoadObj(&data.attributes, &data.shapes, &data.materials, &warnings, &errors,
423 &text, &reader, false, false)) {
424
0/2
✗ Branch 2 not taken.
✗ Branch 5 not taken.
✗ throw Error("Reading \"{}\" as OBJ: {}", file.string(), errors);
425 }
426
1/1
✓ Branch 1 taken 8 times.
8 const std::string messages = warnings + errors;
427
3/3
✓ Branch 1 taken 8 times.
✓ Branch 3 taken 11 times.
✓ Branch 4 taken 8 times.
19 for (const std::string_view warning : Split(messages, '\n')) {
428
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 9 times.
11 if (!warning.empty()) {
429
2/2
✓ Branch 1 taken 2 times.
✓ Branch 4 taken 2 times.
4 Log(LogLevel::Warning, "reading \"{}\": {}", file.string(), warning);
430 }
431 ✗ }
432
1/1
✓ Branch 1 taken 8 times.
8 Log(LogLevel::Debug, "\"{}\" has {} shapes, {} materials and {} vertices", file.string(),
433
1/1
✓ Branch 1 taken 8 times.
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
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 3 times.
8 if (data.materials.empty()) {
442 return looks;
443 }
444
2/2
✓ Branch 1 taken 5 times.
✓ Branch 4 taken 5 times.
5 const std::string scope = fmt::format("{}/{}", path, children.Take("Looks", "Looks"));
445
1/1
✓ Branch 1 taken 5 times.
10 plan.Define<Scope>(scope);
446 5 ChildNames names;
447
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 5 times.
11 for (const tinyobj::material_t& material : data.materials) {
448
1/1
✓ Branch 1 taken 6 times.
6 context.Report("materials", looks.size(), data.materials.size());
449
2/2
✓ Branch 1 taken 6 times.
✓ Branch 4 taken 6 times.
6 const std::string look = fmt::format("{}/{}", scope, names.Take(material.name, "Material"));
450
5/6
✗ Branch 1 not taken.
✓ Branch 7 taken 6 times.
✓ Branch 10 taken 6 times.
✓ Branch 13 taken 6 times.
✓ Branch 16 taken 6 times.
✓ Branch 19 taken 6 times.
24 plan.Add(look, [built = SurfaceBuilder(material, look, source, plan).Build()](
451 6 Scene& scene, std::string_view at) { DefineLook(scene, at, built); });
452
1/1
✓ Branch 1 taken 6 times.
6 Log(LogLevel::Debug, "read OBJ material \"{}\" for {}", material.name, look);
453
1/1
✓ Branch 1 taken 6 times.
6 looks.push_back(look);
454 6 }
455
1/1
✓ Branch 1 taken 5 times.
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
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 7 times.
21 for (size_t index = 0; index < shapes.size(); ++index) {
464 14 context.Report("shapes", index, shapes.size());
465
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 const tinyobj::shape_t& shape = shapes[index];
466
2/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 14 times.
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
0/1
✗ Branch 2 not taken.
✗ source.file.string(), shape.name);
470 }
471
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 14 times.
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
2/2
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 13 times.
30 for (size_t face = 0; face < shape.mesh.num_face_vertices.size(); ++face) {
478
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 const size_t count = shape.mesh.num_face_vertices[face];
479 17 const int material =
480
3/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✓ Branch 3 taken 6 times.
17 face < shape.mesh.material_ids.size() ? shape.mesh.material_ids[face] : -1;
481
3/4
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 11 times.
17 const bool bound = material >= 0 && size_t(material) < looks.size();
482
2/2
✓ Branch 1 taken 17 times.
✓ Branch 4 taken 17 times.
17 faces[bound ? material : -1].push_back(int32_t(face));
483
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 const uint32_t smoothing = face < shape.mesh.smoothing_group_ids.size()
484
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 ? shape.mesh.smoothing_group_ids[face]
485 17 : 0;
486
1/1
✓ Branch 1 taken 16 times.
17 builder.AddFace(std::span(shape.mesh.indices).subspan(corner, count), smoothing);
487 16 corner += count;
488 }
489
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 const bool single = faces.size() <= 1;
490
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 const int only = faces.empty() ? -1 : faces.begin()->first;
491
2/2
✓ Branch 1 taken 13 times.
✓ Branch 4 taken 13 times.
14 const std::string mesh = fmt::format("{}/{}", path, children.Take(shape.name, "Mesh"));
492
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 5 times.
13 const uint32_t faceCount = builder.FaceCount();
493 13 const uint32_t pointCount = builder.PointCount();
494
2/2
✓ Branch 1 taken 13 times.
✓ Branch 4 taken 13 times.
13 plan.Define<Mesh>(
495
5/5
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 5 times.
✓ Branch 3 taken 8 times.
✓ Branch 6 taken 5 times.
✓ Branch 9 taken 13 times.
26 mesh, std::move(builder).Finish(single && only >= 0 ? looks[size_t(only)] : ""));
496
1/1
✓ Branch 1 taken 13 times.
13 Log(LogLevel::Debug, "read OBJ shape \"{}\" for {} with {} faces and {} points", shape.name,
497 mesh, faceCount, pointCount);
498
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 1 time.
13 if (single) {
499 12 continue;
500 }
501 1 ChildNames subsets;
502
4/4
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 1 time.
4 for (auto& [material, indices] : faces) {
503
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2 times.
3 if (material < 0) {
504 1 continue;
505 }
506
1/1
✓ Branch 1 taken 2 times.
2 const std::string subset = fmt::format(
507
2/2
✓ Branch 1 taken 2 times.
✓ Branch 4 taken 2 times.
2 "{}/{}", mesh, subsets.Take(data.materials[size_t(material)].name, "Subset"));
508
3/3
✓ Branch 1 taken 2 times.
✓ Branch 4 taken 2 times.
✓ Branch 7 taken 2 times.
8 plan.Define<GeomSubset>(subset, {.materialBinding = {looks[size_t(material)]},
509 .indices = std::move(indices),
510
1/1
✓ Branch 1 taken 2 times.
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
4/5
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 7 times.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✓ Branch 6 taken 2 times.
9 CHRONO_BATCH_RENDER_ZONE("Import/Obj");
529
1/1
✓ Branch 1 taken 9 times.
9 context.Report("parsing", 0, 1);
530
1/1
✓ Branch 1 taken 8 times.
9 const ObjData data = Parse(source);
531
1/1
✓ Branch 1 taken 8 times.
8 context.Report("parsing", 1, 1);
532
533 8 const double half = std::sqrt(0.5);
534
3/3
✓ Branch 1 taken 8 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 6 times.
8 const float units = context.MetersPerUnit().value_or(1);
535
3/3
✓ Branch 1 taken 8 times.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 7 times.
8 const bool yUp = context.Up().value_or(UpAxis::Y) == UpAxis::Y;
536 8 ImportPlan plan;
537
2/2
✓ Branch 1 taken 8 times.
✓ Branch 4 taken 8 times.
17 plan.Define<Xform>(std::string(path), {.orient = yUp ? Quatd{half, 0, 0, half} : Quatd{},
538 .scale = {units, units, units}});
539
1/1
✓ Branch 1 taken 8 times.
8 ChildNames children;
540 8 const std::vector<std::string> looks =
541
1/1
✓ Branch 1 taken 8 times.
8 DefineMaterials(plan, path, data, source, context, children);
542
1/1
✓ Branch 1 taken 7 times.
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
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
3 return std::make_shared<ObjImporter>();
551 }
552
553 }
554