MXVK Vulkan Framework 0.24.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
mxvk_math_obj.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <array>
5#include <cmath>
6#include <cstddef>
7#include <exception>
8#include <filesystem>
9#include <fstream>
10#include <sstream>
11#include <string>
12#include <utility>
13#include <vector>
14
15namespace mxvk {
16
17 /**
18 * @brief Wavefront material data used by the software rasterizer.
19 *
20 * The diffuse color is used for solid triangle rendering. The remaining
21 * fields and resolved diffuse texture path are retained for applications
22 * that provide their own lit or textured pixel shader.
23 */
24 struct OBJMaterial {
25 std::string name;
26 std::array<float, 3> ambient{0.2f, 0.2f, 0.2f};
27 std::array<float, 3> diffuse{0.8f, 0.8f, 0.8f};
28 std::array<float, 3> specular{};
29 float shininess = 0.0f;
30 float dissolve = 1.0f;
32 std::string diffuse_map;
33 };
34
35 namespace detail {
36
37 struct OBJIndex {
38 int position = 0;
39 int texcoord = 0;
40 int normal = 0;
41 };
42
43 struct OBJVertex {
44 std::array<float, 3> position{};
45 std::array<float, 2> texcoord{};
46 };
47
48 struct OBJTriangle {
49 std::array<OBJVertex, 3> vertices{};
50 std::string material_name;
51 std::string object_name;
52 };
53
55 std::string object_name;
57 std::vector<OBJMaterial> materials;
58 std::vector<OBJTriangle> triangles;
59 };
60
61 inline void trim_obj_line(std::string &text) {
62 const std::size_t begin = text.find_first_not_of(" \t\r\n");
63 if (begin == std::string::npos) {
64 text.clear();
65 return;
66 }
67 const std::size_t end = text.find_last_not_of(" \t\r\n");
68 text = text.substr(begin, end - begin + 1);
69 }
70
71 inline void strip_obj_comment(std::string &text) {
72 const std::size_t comment = text.find('#');
73 if (comment != std::string::npos) {
74 text.erase(comment);
75 }
76 trim_obj_line(text);
77 }
78
79 [[nodiscard]] inline bool parse_obj_index_value(const std::string &text, int &value) {
80 if (text.empty()) {
81 value = 0;
82 return true;
83 }
84
85 std::size_t consumed = 0;
86 try {
87 value = std::stoi(text, &consumed, 10);
88 } catch (const std::exception &) {
89 return false;
90 }
91 return consumed == text.size();
92 }
93
94 [[nodiscard]] inline bool parse_obj_face_token(const std::string &token, OBJIndex &index) {
95 std::array<std::string, 3> fields{};
96 std::size_t field_index = 0;
97 std::size_t field_begin = 0;
98 while (true) {
99 if (field_index >= fields.size()) {
100 return false;
101 }
102 const std::size_t slash = token.find('/', field_begin);
103 fields[field_index++] = token.substr(field_begin, slash == std::string::npos ? std::string::npos : slash - field_begin);
104 if (slash == std::string::npos) {
105 break;
106 }
107 field_begin = slash + 1;
108 }
109
110 return parse_obj_index_value(fields[0], index.position) &&
111 parse_obj_index_value(fields[1], index.texcoord) &&
112 parse_obj_index_value(fields[2], index.normal) &&
113 index.position != 0;
114 }
115
116 template <typename T>
117 [[nodiscard]] inline int resolve_obj_index(int index, const std::vector<T> &values) {
118 if (index > 0) {
119 return index - 1;
120 }
121 if (index < 0) {
122 return static_cast<int>(values.size()) + index;
123 }
124 return -1;
125 }
126
127 [[nodiscard]] inline std::string resolve_obj_path(const std::filesystem::path &base, const std::string &path) {
128 if (path.empty()) {
129 return {};
130 }
131 const std::filesystem::path value(path);
132 if (value.is_absolute()) {
133 return value.lexically_normal().string();
134 }
135 return (base / value).lexically_normal().string();
136 }
137
138 [[nodiscard]] inline std::string parse_mtl_texture_path(std::istream &stream) {
139 std::string path;
140 std::string token;
141 while (stream >> token) {
142 if (!token.empty() && token.front() == '-') {
143 if (token == "-blendu" || token == "-blendv" || token == "-cc" ||
144 token == "-clamp" || token == "-imfchan" || token == "-type" ||
145 token == "-bm" || token == "-boost" || token == "-texres") {
146 stream >> token;
147 } else if (token == "-mm") {
148 stream >> token >> token;
149 } else if (token == "-o" || token == "-s" || token == "-t") {
150 stream >> token >> token >> token;
151 }
152 continue;
153 }
154 if (!path.empty()) {
155 path += ' ';
156 }
157 path += token;
158 }
159 return path;
160 }
161
162 [[nodiscard]] inline bool load_mtl_file(const std::string &path, std::vector<OBJMaterial> &materials, std::string &error) {
163 std::ifstream file(path);
164 if (!file.is_open()) {
165 error = "could not open material library '" + path + "'";
166 return false;
167 }
168
169 std::vector<OBJMaterial> loaded_materials;
170 OBJMaterial *current = nullptr;
171 std::string line;
172 std::size_t line_number = 0;
173 while (std::getline(file, line)) {
174 ++line_number;
175 strip_obj_comment(line);
176 if (line.empty()) {
177 continue;
178 }
179
180 std::istringstream stream(line);
181 std::string tag;
182 stream >> tag;
183 if (tag == "newmtl") {
184 loaded_materials.emplace_back();
185 current = &loaded_materials.back();
186 std::getline(stream, current->name);
187 trim_obj_line(current->name);
188 if (current->name.empty()) {
189 error = "unnamed material at line " + std::to_string(line_number) + " in '" + path + "'";
190 return false;
191 }
192 continue;
193 }
194 if (current == nullptr) {
195 continue;
196 }
197
198 bool parsed = true;
199 if (tag == "Ka") {
200 parsed = static_cast<bool>(stream >> current->ambient[0] >> current->ambient[1] >> current->ambient[2]);
201 } else if (tag == "Kd") {
202 parsed = static_cast<bool>(stream >> current->diffuse[0] >> current->diffuse[1] >> current->diffuse[2]);
203 } else if (tag == "Ks") {
204 parsed = static_cast<bool>(stream >> current->specular[0] >> current->specular[1] >> current->specular[2]);
205 } else if (tag == "Ns") {
206 parsed = static_cast<bool>(stream >> current->shininess);
207 } else if (tag == "d") {
208 parsed = static_cast<bool>(stream >> current->dissolve);
209 } else if (tag == "Tr") {
210 float transparency = 0.0f;
211 parsed = static_cast<bool>(stream >> transparency);
212 current->dissolve = 1.0f - transparency;
213 } else if (tag == "illum") {
214 parsed = static_cast<bool>(stream >> current->illumination_model);
215 } else if (tag == "map_Kd") {
216 const std::string texture_path = parse_mtl_texture_path(stream);
217 if (!texture_path.empty()) {
218 current->diffuse_map = resolve_obj_path(std::filesystem::path(path).parent_path(), texture_path);
219 }
220 }
221
222 if (!parsed) {
223 error = "invalid '" + tag + "' value at line " + std::to_string(line_number) + " in '" + path + "'";
224 return false;
225 }
226 }
227
228 for (OBJMaterial &material : loaded_materials) {
229 for (float &component : material.ambient) {
230 component = std::clamp(component, 0.0f, 1.0f);
231 }
232 for (float &component : material.diffuse) {
233 component = std::clamp(component, 0.0f, 1.0f);
234 }
235 for (float &component : material.specular) {
236 component = std::clamp(component, 0.0f, 1.0f);
237 }
238 material.dissolve = std::clamp(material.dissolve, 0.0f, 1.0f);
239 }
240
241 materials = std::move(loaded_materials);
242 return true;
243 }
244
245 [[nodiscard]] inline std::array<float, 3> obj_face_normal(const OBJVertex &a, const OBJVertex &b, const OBJVertex &c) {
246 const float ux = b.position[0] - a.position[0];
247 const float uy = b.position[1] - a.position[1];
248 const float uz = b.position[2] - a.position[2];
249 const float vx = c.position[0] - a.position[0];
250 const float vy = c.position[1] - a.position[1];
251 const float vz = c.position[2] - a.position[2];
252 return {uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx};
253 }
254
255 [[nodiscard]] inline float obj_projected_area(const std::vector<OBJVertex> &face, int drop_axis) {
256 float area = 0.0f;
257 for (std::size_t index = 0; index < face.size(); ++index) {
258 const OBJVertex &a = face[index];
259 const OBJVertex &b = face[(index + 1) % face.size()];
260 area += a.position[(drop_axis + 1) % 3] * b.position[(drop_axis + 2) % 3] -
261 b.position[(drop_axis + 1) % 3] * a.position[(drop_axis + 2) % 3];
262 }
263 return area;
264 }
265
266 [[nodiscard]] inline float obj_edge_cross(const OBJVertex &a, const OBJVertex &b, const OBJVertex &c, int drop_axis) {
267 const float ax = a.position[(drop_axis + 1) % 3];
268 const float ay = a.position[(drop_axis + 2) % 3];
269 const float bx = b.position[(drop_axis + 1) % 3];
270 const float by = b.position[(drop_axis + 2) % 3];
271 const float cx = c.position[(drop_axis + 1) % 3];
272 const float cy = c.position[(drop_axis + 2) % 3];
273 return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
274 }
275
276 [[nodiscard]] inline bool obj_point_in_triangle(const OBJVertex &point, const OBJVertex &a, const OBJVertex &b, const OBJVertex &c, int drop_axis, float winding) {
277 constexpr float OBJ_EPSILON = 1.0e-6f;
278 return obj_edge_cross(a, b, point, drop_axis) * winding >= -OBJ_EPSILON &&
279 obj_edge_cross(b, c, point, drop_axis) * winding >= -OBJ_EPSILON &&
280 obj_edge_cross(c, a, point, drop_axis) * winding >= -OBJ_EPSILON;
281 }
282
283 inline void append_obj_triangle(const std::vector<OBJVertex> &face, std::size_t a, std::size_t b, std::size_t c, const std::string &material_name, const std::string &object_name, std::vector<OBJTriangle> &triangles) {
284 triangles.push_back({{face[a], face[b], face[c]}, material_name, object_name});
285 }
286
287 inline void triangulate_obj_face(const std::vector<OBJVertex> &face, const std::string &material_name, const std::string &object_name, std::vector<OBJTriangle> &triangles) {
288 if (face.size() < 3) {
289 return;
290 }
291 if (face.size() == 3) {
292 append_obj_triangle(face, 0, 1, 2, material_name, object_name, triangles);
293 return;
294 }
295
296 const std::array<float, 3> normal = obj_face_normal(face[0], face[1], face[2]);
297 int drop_axis = 0;
298 if (std::fabs(normal[1]) > std::fabs(normal[0]) && std::fabs(normal[1]) >= std::fabs(normal[2])) {
299 drop_axis = 1;
300 } else if (std::fabs(normal[2]) > std::fabs(normal[0]) && std::fabs(normal[2]) > std::fabs(normal[1])) {
301 drop_axis = 2;
302 }
303
304 const float area = obj_projected_area(face, drop_axis);
305 if (std::fabs(area) <= 1.0e-6f) {
306 for (std::size_t index = 2; index < face.size(); ++index) {
307 append_obj_triangle(face, 0, index - 1, index, material_name, object_name, triangles);
308 }
309 return;
310 }
311 const float winding = area >= 0.0f ? 1.0f : -1.0f;
312
313 std::vector<std::size_t> remaining(face.size());
314 for (std::size_t index = 0; index < remaining.size(); ++index) {
315 remaining[index] = index;
316 }
317 while (remaining.size() > 3) {
318 bool clipped_ear = false;
319 for (std::size_t index = 0; index < remaining.size(); ++index) {
320 const std::size_t previous = remaining[(index + remaining.size() - 1) % remaining.size()];
321 const std::size_t current = remaining[index];
322 const std::size_t next = remaining[(index + 1) % remaining.size()];
323 if (obj_edge_cross(face[previous], face[current], face[next], drop_axis) * winding <= 1.0e-6f) {
324 continue;
325 }
326
327 bool contains_point = false;
328 for (const std::size_t test : remaining) {
329 if (test != previous && test != current && test != next &&
330 obj_point_in_triangle(face[test], face[previous], face[current], face[next], drop_axis, winding)) {
331 contains_point = true;
332 break;
333 }
334 }
335 if (contains_point) {
336 continue;
337 }
338
339 append_obj_triangle(face, previous, current, next, material_name, object_name, triangles);
340 remaining.erase(remaining.begin() + static_cast<std::ptrdiff_t>(index));
341 clipped_ear = true;
342 break;
343 }
344
345 if (!clipped_ear) {
346 for (std::size_t index = 2; index < remaining.size(); ++index) {
347 append_obj_triangle(face, remaining[0], remaining[index - 1], remaining[index], material_name, object_name, triangles);
348 }
349 return;
350 }
351 }
352 append_obj_triangle(face, remaining[0], remaining[1], remaining[2], material_name, object_name, triangles);
353 }
354
355 [[nodiscard]] inline bool load_obj_file(const std::string &path, OBJLoadResult &result, std::string &error) {
356 std::ifstream file(path);
357 if (!file.is_open()) {
358 error = "could not open file";
359 return false;
360 }
361
362 std::vector<std::array<float, 3>> positions;
363 std::vector<std::array<float, 2>> texcoords;
364 std::vector<std::array<float, 3>> normals;
365 OBJLoadResult loaded;
366 loaded.object_name = std::filesystem::path(path).stem().string();
367 std::string current_object = loaded.object_name;
368 std::string current_material;
369 std::string line;
370 std::size_t line_number = 0;
371
372 while (std::getline(file, line)) {
373 ++line_number;
374 strip_obj_comment(line);
375 if (line.empty()) {
376 continue;
377 }
378
379 std::istringstream stream(line);
380 std::string tag;
381 stream >> tag;
382 if (tag == "v") {
383 std::array<float, 3> position{};
384 if (!(stream >> position[0] >> position[1] >> position[2]) ||
385 !std::isfinite(position[0]) || !std::isfinite(position[1]) || !std::isfinite(position[2])) {
386 error = "invalid vertex at line " + std::to_string(line_number);
387 return false;
388 }
389 positions.push_back(position);
390 } else if (tag == "vt") {
391 std::array<float, 2> texcoord{};
392 if (!(stream >> texcoord[0] >> texcoord[1]) ||
393 !std::isfinite(texcoord[0]) || !std::isfinite(texcoord[1])) {
394 error = "invalid texture coordinate at line " + std::to_string(line_number);
395 return false;
396 }
397 texcoords.push_back(texcoord);
398 } else if (tag == "vn") {
399 std::array<float, 3> normal{};
400 if (!(stream >> normal[0] >> normal[1] >> normal[2]) ||
401 !std::isfinite(normal[0]) || !std::isfinite(normal[1]) || !std::isfinite(normal[2])) {
402 error = "invalid normal at line " + std::to_string(line_number);
403 return false;
404 }
405 normals.push_back(normal);
406 } else if (tag == "o") {
407 std::string name;
408 std::getline(stream, name);
409 trim_obj_line(name);
410 if (!name.empty()) {
411 current_object = name;
412 loaded.object_name = std::move(name);
413 }
414 } else if (tag == "mtllib") {
415 std::string library;
416 std::getline(stream, library);
417 trim_obj_line(library);
418 if (!library.empty() && loaded.material_library_path.empty()) {
419 loaded.material_library_path = resolve_obj_path(std::filesystem::path(path).parent_path(), library);
420 }
421 } else if (tag == "usemtl") {
422 std::getline(stream, current_material);
423 trim_obj_line(current_material);
424 } else if (tag == "f") {
425 std::vector<OBJVertex> face;
426 std::string token;
427 while (stream >> token) {
428 OBJIndex index;
429 if (!parse_obj_face_token(token, index)) {
430 error = "malformed face token '" + token + "' at line " + std::to_string(line_number);
431 return false;
432 }
433
434 const int position_index = resolve_obj_index(index.position, positions);
435 if (position_index < 0 || position_index >= static_cast<int>(positions.size())) {
436 error = "face position index out of range at line " + std::to_string(line_number);
437 return false;
438 }
439
440 OBJVertex vertex;
441 vertex.position = positions[static_cast<std::size_t>(position_index)];
442 if (index.texcoord != 0) {
443 const int texcoord_index = resolve_obj_index(index.texcoord, texcoords);
444 if (texcoord_index < 0 || texcoord_index >= static_cast<int>(texcoords.size())) {
445 error = "face texture index out of range at line " + std::to_string(line_number);
446 return false;
447 }
448 vertex.texcoord = texcoords[static_cast<std::size_t>(texcoord_index)];
449 }
450 if (index.normal != 0) {
451 const int normal_index = resolve_obj_index(index.normal, normals);
452 if (normal_index < 0 || normal_index >= static_cast<int>(normals.size())) {
453 error = "face normal index out of range at line " + std::to_string(line_number);
454 return false;
455 }
456 }
457 face.push_back(vertex);
458 }
459
460 if (face.size() < 3) {
461 error = "face has fewer than three vertices at line " + std::to_string(line_number);
462 return false;
463 }
464 triangulate_obj_face(face, current_material, current_object, loaded.triangles);
465 }
466 }
467
468 if (loaded.triangles.empty()) {
469 error = "no geometry found";
470 return false;
471 }
472 if (!loaded.material_library_path.empty() &&
473 !load_mtl_file(loaded.material_library_path, loaded.materials, error)) {
474 return false;
475 }
476
477 result = std::move(loaded);
478 return true;
479 }
480
481 } // namespace detail
482} // namespace mxvk
bool obj_point_in_triangle(const OBJVertex &point, const OBJVertex &a, const OBJVertex &b, const OBJVertex &c, int drop_axis, float winding)
std::string resolve_obj_path(const std::filesystem::path &base, const std::string &path)
bool parse_obj_face_token(const std::string &token, OBJIndex &index)
float obj_projected_area(const std::vector< OBJVertex > &face, int drop_axis)
bool load_mtl_file(const std::string &path, std::vector< OBJMaterial > &materials, std::string &error)
std::string parse_mtl_texture_path(std::istream &stream)
bool load_obj_file(const std::string &path, OBJLoadResult &result, std::string &error)
void trim_obj_line(std::string &text)
void triangulate_obj_face(const std::vector< OBJVertex > &face, const std::string &material_name, const std::string &object_name, std::vector< OBJTriangle > &triangles)
std::array< float, 3 > obj_face_normal(const OBJVertex &a, const OBJVertex &b, const OBJVertex &c)
void strip_obj_comment(std::string &text)
float obj_edge_cross(const OBJVertex &a, const OBJVertex &b, const OBJVertex &c, int drop_axis)
int resolve_obj_index(int index, const std::vector< T > &values)
bool parse_obj_index_value(const std::string &text, int &value)
void append_obj_triangle(const std::vector< OBJVertex > &face, std::size_t a, std::size_t b, std::size_t c, const std::string &material_name, const std::string &object_name, std::vector< OBJTriangle > &triangles)
Utilities for loading and saving PNG images.
Definition mxvk.hpp:30
Wavefront material data used by the software rasterizer.
std::array< float, 3 > ambient
std::string diffuse_map
std::array< float, 3 > diffuse
std::array< float, 3 > specular
std::vector< OBJMaterial > materials
std::vector< OBJTriangle > triangles
std::array< OBJVertex, 3 > vertices
std::array< float, 3 > position
std::array< float, 2 > texcoord