MXVK Vulkan Framework 0.35.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) && parse_obj_index_value(fields[1], index.texcoord) && parse_obj_index_value(fields[2], index.normal) && index.position != 0;
111 }
112
113 template <typename T> [[nodiscard]] inline int resolve_obj_index(int index, const std::vector<T> &values) {
114 if (index > 0) {
115 return index - 1;
116 }
117 if (index < 0) {
118 return static_cast<int>(values.size()) + index;
119 }
120 return -1;
121 }
122
123 [[nodiscard]] inline std::string resolve_obj_path(const std::filesystem::path &base, const std::string &path) {
124 if (path.empty()) {
125 return {};
126 }
127 const std::filesystem::path value(path);
128 if (value.is_absolute()) {
129 return value.lexically_normal().string();
130 }
131 return (base / value).lexically_normal().string();
132 }
133
134 [[nodiscard]] inline std::string parse_mtl_texture_path(std::istream &stream) {
135 std::string path;
136 std::string token;
137 while (stream >> token) {
138 if (!token.empty() && token.front() == '-') {
139 if (token == "-blendu" || token == "-blendv" || token == "-cc" || token == "-clamp" || token == "-imfchan" || token == "-type" || token == "-bm" || token == "-boost" || token == "-texres") {
140 stream >> token;
141 } else if (token == "-mm") {
142 stream >> token >> token;
143 } else if (token == "-o" || token == "-s" || token == "-t") {
144 stream >> token >> token >> token;
145 }
146 continue;
147 }
148 if (!path.empty()) {
149 path += ' ';
150 }
151 path += token;
152 }
153 return path;
154 }
155
156 [[nodiscard]] inline bool load_mtl_file(const std::string &path, std::vector<OBJMaterial> &materials, std::string &error) {
157 std::ifstream file(path);
158 if (!file.is_open()) {
159 error = "could not open material library '" + path + "'";
160 return false;
161 }
162
163 std::vector<OBJMaterial> loaded_materials;
164 OBJMaterial *current = nullptr;
165 std::string line;
166 std::size_t line_number = 0;
167 while (std::getline(file, line)) {
168 ++line_number;
169 strip_obj_comment(line);
170 if (line.empty()) {
171 continue;
172 }
173
174 std::istringstream stream(line);
175 std::string tag;
176 stream >> tag;
177 if (tag == "newmtl") {
178 loaded_materials.emplace_back();
179 current = &loaded_materials.back();
180 std::getline(stream, current->name);
181 trim_obj_line(current->name);
182 if (current->name.empty()) {
183 error = "unnamed material at line " + std::to_string(line_number) + " in '" + path + "'";
184 return false;
185 }
186 continue;
187 }
188 if (current == nullptr) {
189 continue;
190 }
191
192 bool parsed = true;
193 if (tag == "Ka") {
194 parsed = static_cast<bool>(stream >> current->ambient[0] >> current->ambient[1] >> current->ambient[2]);
195 } else if (tag == "Kd") {
196 parsed = static_cast<bool>(stream >> current->diffuse[0] >> current->diffuse[1] >> current->diffuse[2]);
197 } else if (tag == "Ks") {
198 parsed = static_cast<bool>(stream >> current->specular[0] >> current->specular[1] >> current->specular[2]);
199 } else if (tag == "Ns") {
200 parsed = static_cast<bool>(stream >> current->shininess);
201 } else if (tag == "d") {
202 parsed = static_cast<bool>(stream >> current->dissolve);
203 } else if (tag == "Tr") {
204 float transparency = 0.0f;
205 parsed = static_cast<bool>(stream >> transparency);
206 current->dissolve = 1.0f - transparency;
207 } else if (tag == "illum") {
208 parsed = static_cast<bool>(stream >> current->illumination_model);
209 } else if (tag == "map_Kd") {
210 const std::string texture_path = parse_mtl_texture_path(stream);
211 if (!texture_path.empty()) {
212 current->diffuse_map = resolve_obj_path(std::filesystem::path(path).parent_path(), texture_path);
213 }
214 }
215
216 if (!parsed) {
217 error = "invalid '" + tag + "' value at line " + std::to_string(line_number) + " in '" + path + "'";
218 return false;
219 }
220 }
221
222 for (OBJMaterial &material : loaded_materials) {
223 for (float &component : material.ambient) {
224 component = std::clamp(component, 0.0f, 1.0f);
225 }
226 for (float &component : material.diffuse) {
227 component = std::clamp(component, 0.0f, 1.0f);
228 }
229 for (float &component : material.specular) {
230 component = std::clamp(component, 0.0f, 1.0f);
231 }
232 material.dissolve = std::clamp(material.dissolve, 0.0f, 1.0f);
233 }
234
235 materials = std::move(loaded_materials);
236 return true;
237 }
238
239 [[nodiscard]] inline std::array<float, 3> obj_face_normal(const OBJVertex &a, const OBJVertex &b, const OBJVertex &c) {
240 const float ux = b.position[0] - a.position[0];
241 const float uy = b.position[1] - a.position[1];
242 const float uz = b.position[2] - a.position[2];
243 const float vx = c.position[0] - a.position[0];
244 const float vy = c.position[1] - a.position[1];
245 const float vz = c.position[2] - a.position[2];
246 return {uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx};
247 }
248
249 [[nodiscard]] inline float obj_projected_area(const std::vector<OBJVertex> &face, int drop_axis) {
250 float area = 0.0f;
251 for (std::size_t index = 0; index < face.size(); ++index) {
252 const OBJVertex &a = face[index];
253 const OBJVertex &b = face[(index + 1) % face.size()];
254 area += a.position[(drop_axis + 1) % 3] * b.position[(drop_axis + 2) % 3] - b.position[(drop_axis + 1) % 3] * a.position[(drop_axis + 2) % 3];
255 }
256 return area;
257 }
258
259 [[nodiscard]] inline float obj_edge_cross(const OBJVertex &a, const OBJVertex &b, const OBJVertex &c, int drop_axis) {
260 const float ax = a.position[(drop_axis + 1) % 3];
261 const float ay = a.position[(drop_axis + 2) % 3];
262 const float bx = b.position[(drop_axis + 1) % 3];
263 const float by = b.position[(drop_axis + 2) % 3];
264 const float cx = c.position[(drop_axis + 1) % 3];
265 const float cy = c.position[(drop_axis + 2) % 3];
266 return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
267 }
268
269 [[nodiscard]] inline bool obj_point_in_triangle(const OBJVertex &point, const OBJVertex &a, const OBJVertex &b, const OBJVertex &c, int drop_axis, float winding) {
270 constexpr float OBJ_EPSILON = 1.0e-6f;
271 return obj_edge_cross(a, b, point, drop_axis) * winding >= -OBJ_EPSILON && obj_edge_cross(b, c, point, drop_axis) * winding >= -OBJ_EPSILON && obj_edge_cross(c, a, point, drop_axis) * winding >= -OBJ_EPSILON;
272 }
273
274 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) { triangles.push_back({{face[a], face[b], face[c]}, material_name, object_name}); }
275
276 inline void triangulate_obj_face(const std::vector<OBJVertex> &face, const std::string &material_name, const std::string &object_name, std::vector<OBJTriangle> &triangles) {
277 if (face.size() < 3) {
278 return;
279 }
280 if (face.size() == 3) {
281 append_obj_triangle(face, 0, 1, 2, material_name, object_name, triangles);
282 return;
283 }
284
285 const std::array<float, 3> normal = obj_face_normal(face[0], face[1], face[2]);
286 int drop_axis = 0;
287 if (std::fabs(normal[1]) > std::fabs(normal[0]) && std::fabs(normal[1]) >= std::fabs(normal[2])) {
288 drop_axis = 1;
289 } else if (std::fabs(normal[2]) > std::fabs(normal[0]) && std::fabs(normal[2]) > std::fabs(normal[1])) {
290 drop_axis = 2;
291 }
292
293 const float area = obj_projected_area(face, drop_axis);
294 if (std::fabs(area) <= 1.0e-6f) {
295 for (std::size_t index = 2; index < face.size(); ++index) {
296 append_obj_triangle(face, 0, index - 1, index, material_name, object_name, triangles);
297 }
298 return;
299 }
300 const float winding = area >= 0.0f ? 1.0f : -1.0f;
301
302 std::vector<std::size_t> remaining(face.size());
303 for (std::size_t index = 0; index < remaining.size(); ++index) {
304 remaining[index] = index;
305 }
306 while (remaining.size() > 3) {
307 bool clipped_ear = false;
308 for (std::size_t index = 0; index < remaining.size(); ++index) {
309 const std::size_t previous = remaining[(index + remaining.size() - 1) % remaining.size()];
310 const std::size_t current = remaining[index];
311 const std::size_t next = remaining[(index + 1) % remaining.size()];
312 if (obj_edge_cross(face[previous], face[current], face[next], drop_axis) * winding <= 1.0e-6f) {
313 continue;
314 }
315
316 bool contains_point = false;
317 for (const std::size_t test : remaining) {
318 if (test != previous && test != current && test != next && obj_point_in_triangle(face[test], face[previous], face[current], face[next], drop_axis, winding)) {
319 contains_point = true;
320 break;
321 }
322 }
323 if (contains_point) {
324 continue;
325 }
326
327 append_obj_triangle(face, previous, current, next, material_name, object_name, triangles);
328 remaining.erase(remaining.begin() + static_cast<std::ptrdiff_t>(index));
329 clipped_ear = true;
330 break;
331 }
332
333 if (!clipped_ear) {
334 for (std::size_t index = 2; index < remaining.size(); ++index) {
335 append_obj_triangle(face, remaining[0], remaining[index - 1], remaining[index], material_name, object_name, triangles);
336 }
337 return;
338 }
339 }
340 append_obj_triangle(face, remaining[0], remaining[1], remaining[2], material_name, object_name, triangles);
341 }
342
343 [[nodiscard]] inline bool load_obj_file(const std::string &path, OBJLoadResult &result, std::string &error) {
344 std::ifstream file(path);
345 if (!file.is_open()) {
346 error = "could not open file";
347 return false;
348 }
349
350 std::vector<std::array<float, 3>> positions;
351 std::vector<std::array<float, 2>> texcoords;
352 std::vector<std::array<float, 3>> normals;
353 OBJLoadResult loaded;
354 loaded.object_name = std::filesystem::path(path).stem().string();
355 std::string current_object = loaded.object_name;
356 std::string current_material;
357 std::string line;
358 std::size_t line_number = 0;
359
360 while (std::getline(file, line)) {
361 ++line_number;
362 strip_obj_comment(line);
363 if (line.empty()) {
364 continue;
365 }
366
367 std::istringstream stream(line);
368 std::string tag;
369 stream >> tag;
370 if (tag == "v") {
371 std::array<float, 3> position{};
372 if (!(stream >> position[0] >> position[1] >> position[2]) || !std::isfinite(position[0]) || !std::isfinite(position[1]) || !std::isfinite(position[2])) {
373 error = "invalid vertex at line " + std::to_string(line_number);
374 return false;
375 }
376 positions.push_back(position);
377 } else if (tag == "vt") {
378 std::array<float, 2> texcoord{};
379 if (!(stream >> texcoord[0] >> texcoord[1]) || !std::isfinite(texcoord[0]) || !std::isfinite(texcoord[1])) {
380 error = "invalid texture coordinate at line " + std::to_string(line_number);
381 return false;
382 }
383 texcoords.push_back(texcoord);
384 } else if (tag == "vn") {
385 std::array<float, 3> normal{};
386 if (!(stream >> normal[0] >> normal[1] >> normal[2]) || !std::isfinite(normal[0]) || !std::isfinite(normal[1]) || !std::isfinite(normal[2])) {
387 error = "invalid normal at line " + std::to_string(line_number);
388 return false;
389 }
390 normals.push_back(normal);
391 } else if (tag == "o") {
392 std::string name;
393 std::getline(stream, name);
394 trim_obj_line(name);
395 if (!name.empty()) {
396 current_object = name;
397 loaded.object_name = std::move(name);
398 }
399 } else if (tag == "mtllib") {
400 std::string library;
401 std::getline(stream, library);
402 trim_obj_line(library);
403 if (!library.empty() && loaded.material_library_path.empty()) {
404 loaded.material_library_path = resolve_obj_path(std::filesystem::path(path).parent_path(), library);
405 }
406 } else if (tag == "usemtl") {
407 std::getline(stream, current_material);
408 trim_obj_line(current_material);
409 } else if (tag == "f") {
410 std::vector<OBJVertex> face;
411 std::string token;
412 while (stream >> token) {
413 OBJIndex index;
414 if (!parse_obj_face_token(token, index)) {
415 error = "malformed face token '" + token + "' at line " + std::to_string(line_number);
416 return false;
417 }
418
419 const int position_index = resolve_obj_index(index.position, positions);
420 if (position_index < 0 || position_index >= static_cast<int>(positions.size())) {
421 error = "face position index out of range at line " + std::to_string(line_number);
422 return false;
423 }
424
425 OBJVertex vertex;
426 vertex.position = positions[static_cast<std::size_t>(position_index)];
427 if (index.texcoord != 0) {
428 const int texcoord_index = resolve_obj_index(index.texcoord, texcoords);
429 if (texcoord_index < 0 || texcoord_index >= static_cast<int>(texcoords.size())) {
430 error = "face texture index out of range at line " + std::to_string(line_number);
431 return false;
432 }
433 vertex.texcoord = texcoords[static_cast<std::size_t>(texcoord_index)];
434 }
435 if (index.normal != 0) {
436 const int normal_index = resolve_obj_index(index.normal, normals);
437 if (normal_index < 0 || normal_index >= static_cast<int>(normals.size())) {
438 error = "face normal index out of range at line " + std::to_string(line_number);
439 return false;
440 }
441 }
442 face.push_back(vertex);
443 }
444
445 if (face.size() < 3) {
446 error = "face has fewer than three vertices at line " + std::to_string(line_number);
447 return false;
448 }
449 triangulate_obj_face(face, current_material, current_object, loaded.triangles);
450 }
451 }
452
453 if (loaded.triangles.empty()) {
454 error = "no geometry found";
455 return false;
456 }
457 if (!loaded.material_library_path.empty() && !load_mtl_file(loaded.material_library_path, loaded.materials, error)) {
458 return false;
459 }
460
461 result = std::move(loaded);
462 return true;
463 }
464
465 } // namespace detail
466} // 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:31
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