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_model.cpp
Go to the documentation of this file.
1#include "mxvk/mxvk_model.hpp"
2
3#include <cmath>
4
5#include <algorithm>
6#include <array>
7#include <cstddef>
8#include <exception>
9#include <filesystem>
10#include <fstream>
11#include <iostream>
12#include <sstream>
13
14#include <zlib.h>
15
16namespace mxvk {
17
18 MXModel::MXModel(MXModel &&other) noexcept
19 : verticesData(std::move(other.verticesData)),
20 indicesData(std::move(other.indicesData)),
21 subMeshList(std::move(other.subMeshList)),
22 materialList(std::move(other.materialList)),
23 mtlLibraryPath(std::move(other.mtlLibraryPath)),
24 vertexBufferHandle(other.vertexBufferHandle),
25 vertexBufferMemory(other.vertexBufferMemory),
26 indexBufferHandle(other.indexBufferHandle),
27 indexBufferMemory(other.indexBufferMemory) {
28 other.vertexBufferHandle = VK_NULL_HANDLE;
29 other.vertexBufferMemory = VK_NULL_HANDLE;
30 other.indexBufferHandle = VK_NULL_HANDLE;
31 other.indexBufferMemory = VK_NULL_HANDLE;
32 }
33
34 MXModel &MXModel::operator=(MXModel &&other) noexcept {
35 if (this == &other) {
36 return *this;
37 }
38
39 verticesData = std::move(other.verticesData);
40 indicesData = std::move(other.indicesData);
41 subMeshList = std::move(other.subMeshList);
42 materialList = std::move(other.materialList);
43 mtlLibraryPath = std::move(other.mtlLibraryPath);
44 vertexBufferHandle = other.vertexBufferHandle;
45 vertexBufferMemory = other.vertexBufferMemory;
46 indexBufferHandle = other.indexBufferHandle;
47 indexBufferMemory = other.indexBufferMemory;
48
49 other.vertexBufferHandle = VK_NULL_HANDLE;
50 other.vertexBufferMemory = VK_NULL_HANDLE;
51 other.indexBufferHandle = VK_NULL_HANDLE;
52 other.indexBufferMemory = VK_NULL_HANDLE;
53 return *this;
54 }
55
56 namespace {
57 void logMXModelStep(const std::string &message, bool important = false) {
58 if (important) {
59 std::cout << "mxvk_model: " << message << '\n';
60 }
61 }
62
63 struct Vec2 {
64 float x{};
65 float y{};
66 };
67
68 struct Vec3 {
69 float x{};
70 float y{};
71 float z{};
72 };
73
75 std::vector<VKVertex> vertices{};
76 std::vector<uint32_t> indices{};
77 std::vector<SubMesh> subMeshes{};
78 };
79
82 bool hasNormal = false;
83 };
84
85 struct OBJIndex {
86 int position = 0;
87 int texcoord = 0;
88 int normal = 0;
89 };
90
91 void trim(std::string &s) {
92 const size_t begin = s.find_first_not_of(" \t\r\n");
93 if (begin == std::string::npos) {
94 s.clear();
95 return;
96 }
97 const size_t end = s.find_last_not_of(" \t\r\n");
98 s = s.substr(begin, end - begin + 1);
99 }
100
101 void stripTrailingComment(std::string &s) {
102 const size_t commentPos = s.find('#');
103 if (commentPos != std::string::npos) {
104 s = s.substr(0, commentPos);
105 }
106 trim(s);
107 }
108
109 [[nodiscard]] bool parseOBJIndexValue(const std::string &text, int &value) {
110 if (text.empty()) {
111 value = 0;
112 return true;
113 }
114
115 size_t consumed = 0;
116 try {
117 value = std::stoi(text, &consumed, 10);
118 } catch (const std::exception &) {
119 return false;
120 }
121
122 return consumed == text.size();
123 }
124
125 [[nodiscard]] bool parseOBJFaceToken(const std::string &token, OBJIndex &index) {
126 std::array<std::string, 3> fields{};
127 size_t fieldIndex = 0;
128 size_t fieldBegin = 0;
129
130 while (true) {
131 if (fieldIndex >= fields.size()) {
132 return false;
133 }
134
135 const size_t slashPos = token.find('/', fieldBegin);
136 fields[fieldIndex++] = token.substr(fieldBegin, slashPos == std::string::npos ? std::string::npos : slashPos - fieldBegin);
137 if (slashPos == std::string::npos) {
138 break;
139 }
140 fieldBegin = slashPos + 1;
141 }
142
143 if (!parseOBJIndexValue(fields[0], index.position) ||
144 !parseOBJIndexValue(fields[1], index.texcoord) ||
145 !parseOBJIndexValue(fields[2], index.normal)) {
146 return false;
147 }
148
149 return index.position != 0;
150 }
151
152 template <typename T>
153 [[nodiscard]] int resolveOBJIndex(int objIndex, const std::vector<T> &values) {
154 if (objIndex > 0) {
155 return objIndex - 1;
156 }
157 if (objIndex < 0) {
158 return static_cast<int>(values.size()) + objIndex;
159 }
160 return -1;
161 }
162
163 [[nodiscard]] Vec3 faceNormal(const VKVertex &a, const VKVertex &b, const VKVertex &c) {
164 const float ax = b.pos[0] - a.pos[0];
165 const float ay = b.pos[1] - a.pos[1];
166 const float az = b.pos[2] - a.pos[2];
167 const float bx = c.pos[0] - a.pos[0];
168 const float by = c.pos[1] - a.pos[1];
169 const float bz = c.pos[2] - a.pos[2];
170
171 Vec3 normal{
172 ay * bz - az * by,
173 az * bx - ax * bz,
174 ax * by - ay * bx,
175 };
176 const float len = std::sqrt(normal.x * normal.x + normal.y * normal.y + normal.z * normal.z);
177 if (len > 0.0f) {
178 normal.x /= len;
179 normal.y /= len;
180 normal.z /= len;
181 }
182 return normal;
183 }
184
186 if (a.hasNormal && b.hasNormal && c.hasNormal) {
187 return;
188 }
189
190 const Vec3 normal = faceNormal(a.vertex, b.vertex, c.vertex);
191 for (OBJFaceVertex *faceVertex : {&a, &b, &c}) {
192 if (faceVertex->hasNormal) {
193 continue;
194 }
195 faceVertex->vertex.normal[0] = normal.x;
196 faceVertex->vertex.normal[1] = normal.y;
197 faceVertex->vertex.normal[2] = normal.z;
198 faceVertex->hasNormal = true;
199 }
200 }
201
202 [[nodiscard]] float projectedArea2(const std::vector<OBJFaceVertex> &face, int dropAxis) {
203 float area = 0.0f;
204 for (size_t i = 0; i < face.size(); ++i) {
205 const VKVertex &a = face[i].vertex;
206 const VKVertex &b = face[(i + 1) % face.size()].vertex;
207 const float ax = a.pos[(dropAxis + 1) % 3];
208 const float ay = a.pos[(dropAxis + 2) % 3];
209 const float bx = b.pos[(dropAxis + 1) % 3];
210 const float by = b.pos[(dropAxis + 2) % 3];
211 area += ax * by - bx * ay;
212 }
213 return area;
214 }
215
216 [[nodiscard]] float edgeCross2(const VKVertex &a, const VKVertex &b, const VKVertex &c, int dropAxis) {
217 const float ax = a.pos[(dropAxis + 1) % 3];
218 const float ay = a.pos[(dropAxis + 2) % 3];
219 const float bx = b.pos[(dropAxis + 1) % 3];
220 const float by = b.pos[(dropAxis + 2) % 3];
221 const float cx = c.pos[(dropAxis + 1) % 3];
222 const float cy = c.pos[(dropAxis + 2) % 3];
223 return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
224 }
225
226 [[nodiscard]] bool pointInProjectedTriangle(const VKVertex &point,
227 const VKVertex &a,
228 const VKVertex &b,
229 const VKVertex &c,
230 int dropAxis,
231 float windingSign) {
232 constexpr float epsilon = 1e-6f;
233 const float ab = edgeCross2(a, b, point, dropAxis) * windingSign;
234 const float bc = edgeCross2(b, c, point, dropAxis) * windingSign;
235 const float ca = edgeCross2(c, a, point, dropAxis) * windingSign;
236 return ab >= -epsilon && bc >= -epsilon && ca >= -epsilon;
237 }
238
239 void appendOBJTriangle(std::vector<VKVertex> &vertices, OBJFaceVertex a, OBJFaceVertex b, OBJFaceVertex c) {
240 assignNormalIfMissing(a, b, c);
241 vertices.push_back(a.vertex);
242 vertices.push_back(b.vertex);
243 vertices.push_back(c.vertex);
244 }
245
246 void triangulateOBJFace(const std::vector<OBJFaceVertex> &face, std::vector<VKVertex> &vertices) {
247 if (face.size() < 3) {
248 return;
249 }
250
251 if (face.size() == 3) {
252 appendOBJTriangle(vertices, face[0], face[1], face[2]);
253 return;
254 }
255
256 const Vec3 normal = faceNormal(face[0].vertex, face[1].vertex, face[2].vertex);
257 int dropAxis = 0;
258 if (std::fabs(normal.y) > std::fabs(normal.x) && std::fabs(normal.y) >= std::fabs(normal.z)) {
259 dropAxis = 1;
260 } else if (std::fabs(normal.z) > std::fabs(normal.x) && std::fabs(normal.z) > std::fabs(normal.y)) {
261 dropAxis = 2;
262 }
263
264 float windingSign = projectedArea2(face, dropAxis) >= 0.0f ? 1.0f : -1.0f;
265 if (std::fabs(projectedArea2(face, dropAxis)) <= 1e-6f) {
266 for (size_t i = 2; i < face.size(); ++i) {
267 appendOBJTriangle(vertices, face[0], face[i - 1], face[i]);
268 }
269 return;
270 }
271
272 std::vector<size_t> remaining(face.size());
273 for (size_t i = 0; i < remaining.size(); ++i) {
274 remaining[i] = i;
275 }
276
277 while (remaining.size() > 3) {
278 bool clippedEar = false;
279 for (size_t i = 0; i < remaining.size(); ++i) {
280 const size_t previous = remaining[(i + remaining.size() - 1) % remaining.size()];
281 const size_t current = remaining[i];
282 const size_t next = remaining[(i + 1) % remaining.size()];
283
284 const float cross = edgeCross2(face[previous].vertex, face[current].vertex, face[next].vertex, dropAxis) * windingSign;
285 if (cross <= 1e-6f) {
286 continue;
287 }
288
289 bool containsPoint = false;
290 for (const size_t test : remaining) {
291 if (test == previous || test == current || test == next) {
292 continue;
293 }
294 if (pointInProjectedTriangle(face[test].vertex,
295 face[previous].vertex,
296 face[current].vertex,
297 face[next].vertex,
298 dropAxis,
299 windingSign)) {
300 containsPoint = true;
301 break;
302 }
303 }
304
305 if (containsPoint) {
306 continue;
307 }
308
309 appendOBJTriangle(vertices, face[previous], face[current], face[next]);
310 remaining.erase(remaining.begin() + static_cast<std::ptrdiff_t>(i));
311 clippedEar = true;
312 break;
313 }
314
315 if (!clippedEar) {
316 for (size_t i = 2; i < remaining.size(); ++i) {
317 appendOBJTriangle(vertices, face[remaining[0]], face[remaining[i - 1]], face[remaining[i]]);
318 }
319 return;
320 }
321 }
322
323 appendOBJTriangle(vertices, face[remaining[0]], face[remaining[1]], face[remaining[2]]);
324 }
325
326 [[nodiscard]] std::string inflateCompressedText(const std::vector<unsigned char> &compressedData) {
327 z_stream stream{};
328 stream.next_in = const_cast<Bytef *>(reinterpret_cast<const Bytef *>(compressedData.data()));
329 stream.avail_in = static_cast<uInt>(compressedData.size());
330
331 if (inflateInit2(&stream, 15 + 32) != Z_OK) {
332 throw mxvk::Exception("MXModel::loadMXMODZ failed to initialize zlib inflater");
333 }
334
335 std::string output{};
336 std::array<char, 16384> buffer{};
337
338 int result = Z_OK;
339 while (result != Z_STREAM_END) {
340 stream.next_out = reinterpret_cast<Bytef *>(buffer.data());
341 stream.avail_out = static_cast<uInt>(buffer.size());
342
343 result = inflate(&stream, Z_NO_FLUSH);
344 if (result != Z_OK && result != Z_STREAM_END) {
345 inflateEnd(&stream);
346 throw mxvk::Exception("MXModel::loadMXMODZ failed to inflate compressed data");
347 }
348
349 const size_t producedBytes = buffer.size() - static_cast<size_t>(stream.avail_out);
350 output.append(buffer.data(), producedBytes);
351 }
352
353 inflateEnd(&stream);
354
355 if (output.empty()) {
356 throw mxvk::Exception("MXModel::loadMXMODZ produced empty decompressed payload");
357 }
358
359 return output;
360 }
361
362 [[nodiscard]] std::string resolveManifestPath(const std::string &basePath, const std::string &path) {
363 if (path.empty()) {
364 return {};
365 }
366
367 std::filesystem::path resolved(path);
368 if (resolved.is_absolute() || basePath.empty()) {
369 return resolved.lexically_normal().string();
370 }
371
372 return (std::filesystem::path(basePath) / resolved).lexically_normal().string();
373 }
374
375 [[nodiscard]] std::string parseMTLTexturePath(std::istream &stream) {
376 std::string mapPath{};
377 std::string mapToken{};
378 while (stream >> mapToken) {
379 if (!mapToken.empty() && mapToken[0] == '-') {
380 if (mapToken == "-blendu" || mapToken == "-blendv" || mapToken == "-cc" ||
381 mapToken == "-clamp" || mapToken == "-imfchan" || mapToken == "-type") {
382 stream >> mapToken;
383 } else if (mapToken == "-mm") {
384 stream >> mapToken;
385 stream >> mapToken;
386 } else if (mapToken == "-o" || mapToken == "-s" || mapToken == "-t") {
387 stream >> mapToken;
388 stream >> mapToken;
389 stream >> mapToken;
390 } else if (mapToken == "-bm" || mapToken == "-boost" || mapToken == "-texres") {
391 stream >> mapToken;
392 }
393 continue;
394 }
395
396 if (!mapPath.empty()) {
397 mapPath += ' ';
398 }
399 mapPath += mapToken;
400 }
401 return mapPath;
402 }
403
404 void parseMTLStream(std::istream &file,
405 const std::string &textureBasePath,
406 std::vector<MXMaterial> &materials) {
407 MXMaterial *current = nullptr;
408 std::string line{};
409 while (std::getline(file, line)) {
411 if (line.empty()) {
412 continue;
413 }
414
415 std::istringstream stream(line);
416 std::string tag{};
417 stream >> tag;
418
419 if (tag == "newmtl") {
420 materials.emplace_back();
421 current = &materials.back();
422 std::getline(stream, current->name);
423 trim(current->name);
424 continue;
425 }
426
427 if (current == nullptr) {
428 continue;
429 }
430
431 if (tag == "Ka") {
432 stream >> current->ka[0] >> current->ka[1] >> current->ka[2];
433 } else if (tag == "Kd") {
434 stream >> current->kd[0] >> current->kd[1] >> current->kd[2];
435 } else if (tag == "Ks") {
436 stream >> current->ks[0] >> current->ks[1] >> current->ks[2];
437 } else if (tag == "Ns") {
438 stream >> current->ns;
439 } else if (tag == "d") {
440 stream >> current->d;
441 } else if (tag == "illum") {
442 stream >> current->illum;
443 } else if (tag == "map_Kd") {
444 const std::string mapPath = parseMTLTexturePath(stream);
445 if (!mapPath.empty()) {
446 current->map_kd = resolveManifestPath(textureBasePath, mapPath);
447 }
448 }
449 }
450 }
451
452 [[nodiscard]] std::string mtlReferencePath(const std::string &objPath, const std::string &mtlPath) {
453 const std::filesystem::path objDir = std::filesystem::path(objPath).parent_path();
454 const std::filesystem::path materialPath(mtlPath);
455 if (objDir.empty()) {
456 return materialPath.filename().string();
457 }
458
459 std::error_code ec{};
460 const std::filesystem::path relative = std::filesystem::relative(materialPath, objDir, ec);
461 if (!ec && !relative.empty()) {
462 return relative.string();
463 }
464
465 return materialPath.filename().string();
466 }
467
468 [[nodiscard]] std::string materialNameForTextureIndex(uint32_t textureIndex,
469 const std::vector<MXMaterial> &materials) {
470 if (textureIndex < static_cast<uint32_t>(materials.size()) && !materials[textureIndex].name.empty()) {
471 return materials[textureIndex].name;
472 }
473 return "material_" + std::to_string(textureIndex);
474 }
475
476 [[nodiscard]] std::string mtlTextureReferencePath(const std::filesystem::path &mtlPath,
477 const std::string &texturePath) {
478 if (texturePath.empty()) {
479 return {};
480 }
481
482 std::filesystem::path resolvedTexturePath(texturePath);
483 if (resolvedTexturePath.is_relative()) {
484 resolvedTexturePath = std::filesystem::absolute(resolvedTexturePath);
485 }
486
487 std::filesystem::path mtlDir = mtlPath.parent_path();
488 if (mtlDir.empty()) {
489 mtlDir = std::filesystem::current_path();
490 } else if (mtlDir.is_relative()) {
491 mtlDir = std::filesystem::absolute(mtlDir);
492 }
493
494 std::error_code ec{};
495 const std::filesystem::path relative = std::filesystem::relative(resolvedTexturePath, mtlDir, ec);
496 if (!ec && !relative.empty()) {
497 return relative.string();
498 }
499
500 return resolvedTexturePath.lexically_normal().string();
501 }
502
503 void writeMTLMaterial(std::ostream &out, const MXMaterial &material) {
504 out << "newmtl " << material.name << '\n';
505 out << "Ka " << material.ka[0] << ' ' << material.ka[1] << ' ' << material.ka[2] << '\n';
506 out << "Kd " << material.kd[0] << ' ' << material.kd[1] << ' ' << material.kd[2] << '\n';
507 out << "Ks " << material.ks[0] << ' ' << material.ks[1] << ' ' << material.ks[2] << '\n';
508 out << "Ns " << material.ns << '\n';
509 out << "d " << material.d << '\n';
510 out << "illum " << material.illum << '\n';
511 if (!material.map_kd.empty()) {
512 out << "map_Kd " << material.map_kd << '\n';
513 }
514 out << '\n';
515 }
516
517 [[nodiscard]] MXMODParseResult parseMXMODStream(std::istream &file,
518 const std::string &sourcePath,
519 float positionScale) {
520 struct TriBlock {
521 uint32_t textureIndex = 0;
522 std::vector<Vec3> pos{};
523 std::vector<Vec2> uv{};
524 std::vector<Vec3> nrm{};
525 std::vector<uint32_t> fileIndices{};
526 };
527
528 std::vector<TriBlock> triBlocks{};
529 TriBlock *current = nullptr;
530 int sectionType = -1;
531
532 std::string line{};
533 while (std::getline(file, line)) {
535 if (line.empty()) {
536 continue;
537 }
538
539 std::istringstream stream(line);
540 const char c = line[line.find_first_not_of(" \t")];
541 const bool isData = (c >= '0' && c <= '9') || c == '-' || c == '+' || c == '.';
542
543 if (isData && current != nullptr) {
544 float x = 0.0f;
545 float y = 0.0f;
546 float z = 0.0f;
547
548 switch (sectionType) {
549 case 0:
550 if (stream >> x >> y >> z) {
551 current->pos.push_back({x * positionScale, y * positionScale, z * positionScale});
552 }
553 break;
554 case 1:
555 if (stream >> x >> y) {
556 current->uv.push_back({x, y});
557 }
558 break;
559 case 2:
560 if (stream >> x >> y >> z) {
561 current->nrm.push_back({x, y, z});
562 }
563 break;
564 case 5: {
565 uint32_t idx = 0;
566 while (stream >> idx) {
567 current->fileIndices.push_back(idx);
568 }
569 } break;
570 default:
571 break;
572 }
573
574 continue;
575 }
576
577 std::string tag{};
578 stream >> tag;
579 if (tag == "tri") {
580 uint32_t surfaceType = 0;
581 uint32_t textureIndex = 0;
582 stream >> surfaceType >> textureIndex;
583 static_cast<void>(surfaceType);
584 triBlocks.emplace_back();
585 current = &triBlocks.back();
586 current->textureIndex = textureIndex;
587 sectionType = -1;
588 continue;
589 }
590
591 if (tag == "vert") {
592 sectionType = 0;
593 continue;
594 }
595 if (tag == "tex") {
596 sectionType = 1;
597 continue;
598 }
599 if (tag == "norm") {
600 sectionType = 2;
601 continue;
602 }
603 if (tag == "indices") {
604 sectionType = 5;
605 continue;
606 }
607 }
608
609 if (triBlocks.empty()) {
610 throw mxvk::Exception("MXModel::loadMXMOD no geometry found in " + sourcePath);
611 }
612
613 MXMODParseResult parsed{};
614 for (const TriBlock &blk : triBlocks) {
615 if (blk.pos.empty()) {
616 continue;
617 }
618
619 const uint32_t vertexBase = static_cast<uint32_t>(parsed.vertices.size());
620 for (size_t i = 0; i < blk.pos.size(); ++i) {
621 VKVertex v{};
622 v.pos[0] = blk.pos[i].x;
623 v.pos[1] = blk.pos[i].y;
624 v.pos[2] = blk.pos[i].z;
625
626 if (i < blk.uv.size()) {
627 v.texCoord[0] = blk.uv[i].x;
628 v.texCoord[1] = blk.uv[i].y;
629 }
630 if (i < blk.nrm.size()) {
631 v.normal[0] = blk.nrm[i].x;
632 v.normal[1] = blk.nrm[i].y;
633 v.normal[2] = blk.nrm[i].z;
634 }
635
636 parsed.vertices.push_back(v);
637 }
638
639 const uint32_t firstIndex = static_cast<uint32_t>(parsed.indices.size());
640 if (!blk.fileIndices.empty()) {
641 for (uint32_t idx : blk.fileIndices) {
642 parsed.indices.push_back(vertexBase + idx);
643 }
644 } else {
645 for (uint32_t i = 0; i < static_cast<uint32_t>(blk.pos.size()); ++i) {
646 parsed.indices.push_back(vertexBase + i);
647 }
648 }
649
650 SubMesh sm{};
651 sm.firstIndex = firstIndex;
652 sm.indexCount = static_cast<uint32_t>(parsed.indices.size()) - firstIndex;
653 sm.textureIndex = blk.textureIndex;
654 parsed.subMeshes.push_back(sm);
655 }
656
657 if (parsed.vertices.empty() || parsed.indices.empty()) {
658 throw mxvk::Exception("MXModel::loadMXMOD no renderable data found in " + sourcePath);
659 }
660
661 return parsed;
662 }
663 } // namespace
664
665 std::size_t VKVertexHash::operator()(const VKVertex &v) const {
666 std::size_t seed = 0;
667 auto combine = [&seed](float value) {
668 std::hash<float> hasher;
669 seed ^= hasher(value) + 0x9e3779b9u + (seed << 6u) + (seed >> 2u);
670 };
671
672 for (int i = 0; i < 3; ++i) {
673 combine(v.pos[i]);
674 }
675 for (int i = 0; i < 2; ++i) {
676 combine(v.texCoord[i]);
677 }
678 for (int i = 0; i < 3; ++i) {
679 combine(v.normal[i]);
680 }
681
682 return seed;
683 }
684
686 if (verticesData.empty() || indicesData.empty()) {
687 return;
688 }
689
690 std::vector<VKVertex> uniqueVertices{};
691 uniqueVertices.reserve(verticesData.size());
692
693 std::unordered_map<VKVertex, uint32_t, VKVertexHash> vertexMap{};
694 std::vector<uint32_t> remap(verticesData.size(), 0);
695
696 for (size_t i = 0; i < verticesData.size(); ++i) {
697 const auto it = vertexMap.find(verticesData[i]);
698 if (it != vertexMap.end()) {
699 remap[i] = it->second;
700 continue;
701 }
702
703 const uint32_t nextIndex = static_cast<uint32_t>(uniqueVertices.size());
704 uniqueVertices.push_back(verticesData[i]);
705 vertexMap.emplace(verticesData[i], nextIndex);
706 remap[i] = nextIndex;
707 }
708
709 for (uint32_t &idx : indicesData) {
710 if (idx < static_cast<uint32_t>(remap.size())) {
711 idx = remap[idx];
712 }
713 }
714
715 verticesData = std::move(uniqueVertices);
716 }
717
718 void MXModel::load(const std::string &path, float positionScale) {
719 if (path.empty()) {
720 throw mxvk::Exception("MXModel::load path is empty");
721 }
722
723 logMXModelStep("load begin: " + path, true);
724
725 if (path.ends_with(".obj")) {
726 loadOBJ(path, positionScale);
727 logMXModelStep("load complete (.obj): vertices=" + std::to_string(verticesData.size()) +
728 ", indices=" + std::to_string(indicesData.size()) +
729 ", submeshes=" + std::to_string(subMeshList.size()),
730 true);
731 return;
732 }
733
734 if (path.ends_with(".mxmod")) {
735 loadMXMOD(path, positionScale);
736 logMXModelStep("load complete (.mxmod): vertices=" + std::to_string(verticesData.size()) +
737 ", indices=" + std::to_string(indicesData.size()) +
738 ", submeshes=" + std::to_string(subMeshList.size()),
739 true);
740 return;
741 }
742
743 if (path.ends_with(".mxmod.z")) {
744 loadMXMODZ(path, positionScale);
745 logMXModelStep("load complete (.mxmod.z): vertices=" + std::to_string(verticesData.size()) +
746 ", indices=" + std::to_string(indicesData.size()) +
747 ", submeshes=" + std::to_string(subMeshList.size()),
748 true);
749 return;
750 }
751
752 throw mxvk::Exception("MXModel::load unsupported file format: " + path);
753 }
754
755 void MXModel::load(const std::string &path,
756 const std::string &textureManifestPath,
757 const std::string &textureBasePath,
758 float positionScale) {
759 load(path, positionScale);
760 if (!textureManifestPath.empty()) {
761 loadTextureManifest(textureManifestPath, textureBasePath);
762 }
763 }
764
765 void MXModel::exportOBJ(const std::string &objPath, const std::string &mtlPath) const {
766 if (objPath.empty()) {
767 throw mxvk::Exception("MXModel::exportOBJ objPath is empty");
768 }
769 if (verticesData.empty() || indicesData.empty()) {
770 throw mxvk::Exception("MXModel::exportOBJ requires loaded geometry");
771 }
772
773 const std::filesystem::path objOutputPath(objPath);
774 const std::filesystem::path mtlOutputPath = mtlPath.empty()
775 ? objOutputPath.parent_path() / objOutputPath.stem().concat(".mtl")
776 : std::filesystem::path(mtlPath);
777
778 if (!objOutputPath.parent_path().empty()) {
779 std::filesystem::create_directories(objOutputPath.parent_path());
780 }
781 if (!mtlOutputPath.parent_path().empty()) {
782 std::filesystem::create_directories(mtlOutputPath.parent_path());
783 }
784
785 std::ofstream objFile(objOutputPath);
786 if (!objFile.is_open()) {
787 throw mxvk::Exception("MXModel::exportOBJ failed to open OBJ file: " + objOutputPath.string());
788 }
789
790 std::ofstream mtlFile(mtlOutputPath);
791 if (!mtlFile.is_open()) {
792 throw mxvk::Exception("MXModel::exportOBJ failed to open MTL file: " + mtlOutputPath.string());
793 }
794
795 objFile << "# Exported from MXVK MXModel\n";
796 objFile << "mtllib " << mtlReferencePath(objOutputPath.string(), mtlOutputPath.string()) << "\n\n";
797
798 for (const VKVertex &vertex : verticesData) {
799 objFile << "v " << vertex.pos[0] << ' ' << vertex.pos[1] << ' ' << vertex.pos[2] << '\n';
800 }
801 objFile << '\n';
802
803 for (const VKVertex &vertex : verticesData) {
804 objFile << "vt " << vertex.texCoord[0] << ' ' << vertex.texCoord[1] << '\n';
805 }
806 objFile << '\n';
807
808 for (const VKVertex &vertex : verticesData) {
809 objFile << "vn " << vertex.normal[0] << ' ' << vertex.normal[1] << ' ' << vertex.normal[2] << '\n';
810 }
811 objFile << '\n';
812
813 std::vector<MXMaterial> outputMaterials = materialList;
814 for (size_t i = 0; i < outputMaterials.size(); ++i) {
815 if (outputMaterials[i].name.empty()) {
816 outputMaterials[i].name = "material_" + std::to_string(i);
817 }
818 }
819
820 const auto hasOutputMaterial = [&outputMaterials](const std::string &name) {
821 return std::any_of(outputMaterials.begin(), outputMaterials.end(), [&name](const MXMaterial &material) {
822 return material.name == name;
823 });
824 };
825
826 const auto ensureOutputMaterial = [&outputMaterials, &hasOutputMaterial](const std::string &name) {
827 if (hasOutputMaterial(name)) {
828 return;
829 }
830 MXMaterial material{};
831 material.name = name;
832 outputMaterials.push_back(material);
833 };
834
835 if (subMeshList.empty()) {
836 ensureOutputMaterial("material_0");
837 } else {
838 for (const SubMesh &sm : subMeshList) {
839 const std::string materialName = !sm.materialName.empty()
840 ? sm.materialName
841 : materialNameForTextureIndex(sm.textureIndex, outputMaterials);
842 ensureOutputMaterial(materialName);
843 }
844 }
845
846 for (const MXMaterial &material : outputMaterials) {
847 MXMaterial outputMaterial = material;
848 outputMaterial.map_kd = mtlTextureReferencePath(mtlOutputPath, outputMaterial.map_kd);
849 writeMTLMaterial(mtlFile, outputMaterial);
850 }
851
852 const auto emitFaces = [&](uint32_t firstIndex, uint32_t indexCount, const std::string &materialName) {
853 if (firstIndex > indicesData.size() || indexCount > indicesData.size() - firstIndex) {
854 throw mxvk::Exception("MXModel::exportOBJ submesh index range is out of bounds");
855 }
856 if ((indexCount % 3U) != 0U) {
857 throw mxvk::Exception("MXModel::exportOBJ only triangle index ranges can be exported");
858 }
859
860 objFile << "usemtl " << materialName << '\n';
861 for (uint32_t i = 0; i < indexCount; i += 3U) {
862 objFile << "f";
863 for (uint32_t j = 0; j < 3U; ++j) {
864 const uint32_t vertexIndex = indicesData[firstIndex + i + j];
865 if (vertexIndex >= static_cast<uint32_t>(verticesData.size())) {
866 throw mxvk::Exception("MXModel::exportOBJ vertex index is out of bounds");
867 }
868 const uint32_t objIndex = vertexIndex + 1U;
869 objFile << ' ' << objIndex << '/' << objIndex << '/' << objIndex;
870 }
871 objFile << '\n';
872 }
873 objFile << '\n';
874 };
875
876 if (subMeshList.empty()) {
877 emitFaces(0, static_cast<uint32_t>(indicesData.size()), "material_0");
878 } else {
879 for (const SubMesh &sm : subMeshList) {
880 const std::string materialName = !sm.materialName.empty()
881 ? sm.materialName
882 : materialNameForTextureIndex(sm.textureIndex, materialList);
883 emitFaces(sm.firstIndex, sm.indexCount, materialName);
884 }
885 }
886 }
887
888 void MXModel::exportOBJ(const std::string &modelPath,
889 const std::string &textureManifestPath,
890 const std::string &textureBasePath,
891 const std::string &objPath,
892 float positionScale) {
893 MXModel model{};
894 model.load(modelPath, textureManifestPath, textureBasePath, positionScale);
895 model.exportOBJ(objPath);
896 }
897
898 void MXModel::loadOBJ(const std::string &path, float positionScale) {
899 std::ifstream file(path);
900 if (!file.is_open()) {
901 throw mxvk::Exception("MXModel::loadOBJ failed to open file: " + path);
902 }
903
904 std::vector<Vec3> positions{};
905 std::vector<Vec2> texcoords{};
906 std::vector<Vec3> normals{};
907
908 verticesData.clear();
909 indicesData.clear();
910 subMeshList.clear();
911 materialList.clear();
912 mtlLibraryPath.clear();
913
914 std::vector<VKVertex> currentVerts{};
915 std::string currentMaterialName{};
916
917 auto finalizeGroup = [&]() {
918 if (currentVerts.empty()) {
919 return;
920 }
921
922 SubMesh sm{};
923 sm.firstIndex = static_cast<uint32_t>(indicesData.size());
924 sm.indexCount = static_cast<uint32_t>(currentVerts.size());
925 sm.materialName = currentMaterialName;
926
927 const uint32_t baseVertex = static_cast<uint32_t>(verticesData.size());
928 verticesData.insert(verticesData.end(), currentVerts.begin(), currentVerts.end());
929
930 for (uint32_t i = 0; i < sm.indexCount; ++i) {
931 indicesData.push_back(baseVertex + i);
932 }
933
934 subMeshList.push_back(sm);
935 currentVerts.clear();
936 };
937
938 std::string line{};
939 while (std::getline(file, line)) {
941 if (line.empty()) {
942 continue;
943 }
944
945 std::istringstream stream(line);
946 std::string tag{};
947 stream >> tag;
948
949 if (tag == "v") {
950 float x = 0.0f;
951 float y = 0.0f;
952 float z = 0.0f;
953 if (stream >> x >> y >> z) {
954 positions.push_back({x * positionScale, y * positionScale, z * positionScale});
955 }
956 continue;
957 }
958
959 if (tag == "vt") {
960 float u = 0.0f;
961 float v = 0.0f;
962 if (stream >> u >> v) {
963 texcoords.push_back({u, v});
964 }
965 continue;
966 }
967
968 if (tag == "vn") {
969 float x = 0.0f;
970 float y = 0.0f;
971 float z = 0.0f;
972 if (stream >> x >> y >> z) {
973 normals.push_back({x, y, z});
974 }
975 continue;
976 }
977
978 if (tag == "mtllib") {
979 std::string mtlFile{};
980 std::getline(stream, mtlFile);
981 trim(mtlFile);
982 if (!mtlFile.empty()) {
983 const std::filesystem::path objPath(path);
984 std::filesystem::path mtlPath(mtlFile);
985 if (mtlPath.is_absolute()) {
986 mtlPath = mtlPath.filename();
987 }
988 mtlLibraryPath = (objPath.parent_path() / mtlPath).string();
989 }
990 continue;
991 }
992
993 if (tag == "g" || tag == "o") {
994 finalizeGroup();
995 continue;
996 }
997
998 if (tag == "usemtl") {
999 std::string materialName{};
1000 std::getline(stream, materialName);
1001 trim(materialName);
1002 if (!materialName.empty() && materialName != currentMaterialName) {
1003 finalizeGroup();
1004 currentMaterialName = materialName;
1005 }
1006 continue;
1007 }
1008
1009 if (tag != "f") {
1010 continue;
1011 }
1012
1013 std::vector<OBJFaceVertex> faceVerts{};
1014 std::string token{};
1015 while (stream >> token) {
1016 OBJIndex objIndex{};
1017 if (!parseOBJFaceToken(token, objIndex)) {
1018 throw mxvk::Exception("MXModel::loadOBJ malformed face token '" + token + "' in " + path);
1019 }
1020
1021 OBJFaceVertex faceVertex{};
1022 const int positionIndex = resolveOBJIndex(objIndex.position, positions);
1023 if (positionIndex < 0 || positionIndex >= static_cast<int>(positions.size())) {
1024 throw mxvk::Exception("MXModel::loadOBJ face position index out of range in " + path);
1025 }
1026 faceVertex.vertex.pos[0] = positions[static_cast<size_t>(positionIndex)].x;
1027 faceVertex.vertex.pos[1] = positions[static_cast<size_t>(positionIndex)].y;
1028 faceVertex.vertex.pos[2] = positions[static_cast<size_t>(positionIndex)].z;
1029
1030 if (objIndex.texcoord != 0) {
1031 const int texcoordIndex = resolveOBJIndex(objIndex.texcoord, texcoords);
1032 if (texcoordIndex < 0 || texcoordIndex >= static_cast<int>(texcoords.size())) {
1033 throw mxvk::Exception("MXModel::loadOBJ face texture index out of range in " + path);
1034 }
1035 faceVertex.vertex.texCoord[0] = texcoords[static_cast<size_t>(texcoordIndex)].x;
1036 faceVertex.vertex.texCoord[1] = texcoords[static_cast<size_t>(texcoordIndex)].y;
1037 }
1038
1039 if (objIndex.normal != 0) {
1040 const int normalIndex = resolveOBJIndex(objIndex.normal, normals);
1041 if (normalIndex < 0 || normalIndex >= static_cast<int>(normals.size())) {
1042 throw mxvk::Exception("MXModel::loadOBJ face normal index out of range in " + path);
1043 }
1044 faceVertex.vertex.normal[0] = normals[static_cast<size_t>(normalIndex)].x;
1045 faceVertex.vertex.normal[1] = normals[static_cast<size_t>(normalIndex)].y;
1046 faceVertex.vertex.normal[2] = normals[static_cast<size_t>(normalIndex)].z;
1047 faceVertex.hasNormal = true;
1048 }
1049
1050 faceVerts.push_back(faceVertex);
1051 }
1052
1053 triangulateOBJFace(faceVerts, currentVerts);
1054 }
1055
1056 finalizeGroup();
1057
1058 if (!mtlLibraryPath.empty()) {
1059 loadMTL(mtlLibraryPath);
1060 std::unordered_map<std::string, uint32_t> materialIndices{};
1061 for (uint32_t i = 0; i < static_cast<uint32_t>(materialList.size()); ++i) {
1062 materialIndices.emplace(materialList[i].name, i);
1063 }
1064
1065 for (SubMesh &sm : subMeshList) {
1066 const auto it = materialIndices.find(sm.materialName);
1067 if (it != materialIndices.end()) {
1068 sm.textureIndex = it->second;
1069 }
1070 }
1071 }
1072
1073 if (verticesData.empty() || indicesData.empty()) {
1074 throw mxvk::Exception("MXModel::loadOBJ no geometry found in " + path);
1075 }
1076
1077 const bool hasNormals = !normals.empty();
1078 if (!hasNormals) {
1079 for (size_t i = 0; i + 2 < verticesData.size(); i += 3) {
1080 const float ax = verticesData[i + 1].pos[0] - verticesData[i].pos[0];
1081 const float ay = verticesData[i + 1].pos[1] - verticesData[i].pos[1];
1082 const float az = verticesData[i + 1].pos[2] - verticesData[i].pos[2];
1083 const float bx = verticesData[i + 2].pos[0] - verticesData[i].pos[0];
1084 const float by = verticesData[i + 2].pos[1] - verticesData[i].pos[1];
1085 const float bz = verticesData[i + 2].pos[2] - verticesData[i].pos[2];
1086
1087 float nx = ay * bz - az * by;
1088 float ny = az * bx - ax * bz;
1089 float nz = ax * by - ay * bx;
1090 const float len = std::sqrt(nx * nx + ny * ny + nz * nz);
1091 if (len > 0.0f) {
1092 nx /= len;
1093 ny /= len;
1094 nz /= len;
1095 }
1096
1097 for (int j = 0; j < 3; ++j) {
1098 verticesData[i + static_cast<size_t>(j)].normal[0] = nx;
1099 verticesData[i + static_cast<size_t>(j)].normal[1] = ny;
1100 verticesData[i + static_cast<size_t>(j)].normal[2] = nz;
1101 }
1102 }
1103 }
1104
1106 }
1107
1108 void MXModel::loadMXMOD(const std::string &path, float positionScale) {
1109 std::ifstream file(path);
1110 if (!file.is_open()) {
1111 throw mxvk::Exception("MXModel::loadMXMOD failed to open file: " + path);
1112 }
1113
1114 const MXMODParseResult parsed = parseMXMODStream(file, path, positionScale);
1115
1116 verticesData = parsed.vertices;
1117 indicesData = parsed.indices;
1118 subMeshList = parsed.subMeshes;
1119 materialList.clear();
1120 mtlLibraryPath.clear();
1121
1123 }
1124
1125 void MXModel::loadMXMODZ(const std::string &path, float positionScale) {
1126 std::ifstream file(path, std::ios::binary);
1127 if (!file.is_open()) {
1128 throw mxvk::Exception("MXModel::loadMXMODZ failed to open file: " + path);
1129 }
1130
1131 std::vector<unsigned char> compressedData((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
1132 if (compressedData.empty()) {
1133 throw mxvk::Exception("MXModel::loadMXMODZ empty compressed file: " + path);
1134 }
1135
1136 const std::string decompressedText = inflateCompressedText(compressedData);
1137 std::istringstream stream(decompressedText);
1138 const MXMODParseResult parsed = parseMXMODStream(stream, path, positionScale);
1139
1140 verticesData = parsed.vertices;
1141 indicesData = parsed.indices;
1142 subMeshList = parsed.subMeshes;
1143 materialList.clear();
1144 mtlLibraryPath.clear();
1145
1147 }
1148
1149 void MXModel::upload(VkDevice device, VkPhysicalDevice physicalDevice,
1150 VkCommandPool commandPool, VkQueue graphicsQueue) {
1151 if (device == VK_NULL_HANDLE || physicalDevice == VK_NULL_HANDLE ||
1152 commandPool == VK_NULL_HANDLE || graphicsQueue == VK_NULL_HANDLE) {
1153 throw mxvk::Exception("MXModel::upload requires valid Vulkan handles");
1154 }
1155 if (verticesData.empty() || indicesData.empty()) {
1156 throw mxvk::Exception("MXModel::upload requires loaded geometry");
1157 }
1158
1159 logMXModelStep("upload begin", true);
1160
1161 cleanup(device);
1162
1163 const VkDeviceSize vertexBufferSize = sizeof(VKVertex) * verticesData.size();
1164 const VkDeviceSize indexBufferSize = sizeof(uint32_t) * indicesData.size();
1165
1166 VkBuffer stagingVertexBuffer = VK_NULL_HANDLE;
1167 VkDeviceMemory stagingVertexMemory = VK_NULL_HANDLE;
1168 createBuffer(device, physicalDevice, vertexBufferSize,
1169 VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
1170 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
1171 stagingVertexBuffer, stagingVertexMemory);
1172
1173 VkBuffer stagingIndexBuffer = VK_NULL_HANDLE;
1174 VkDeviceMemory stagingIndexMemory = VK_NULL_HANDLE;
1175 createBuffer(device, physicalDevice, indexBufferSize,
1176 VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
1177 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
1178 stagingIndexBuffer, stagingIndexMemory);
1179
1180 void *vertexData = nullptr;
1181 vkMapMemory(device, stagingVertexMemory, 0, vertexBufferSize, 0, &vertexData);
1182 std::memcpy(vertexData, verticesData.data(), static_cast<size_t>(vertexBufferSize));
1183 vkUnmapMemory(device, stagingVertexMemory);
1184
1185 void *indexData = nullptr;
1186 vkMapMemory(device, stagingIndexMemory, 0, indexBufferSize, 0, &indexData);
1187 std::memcpy(indexData, indicesData.data(), static_cast<size_t>(indexBufferSize));
1188 vkUnmapMemory(device, stagingIndexMemory);
1189
1190 createBuffer(device, physicalDevice, vertexBufferSize,
1191 VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
1192 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
1193 vertexBufferHandle, vertexBufferMemory);
1194
1195 createBuffer(device, physicalDevice, indexBufferSize,
1196 VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
1197 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
1198 indexBufferHandle, indexBufferMemory);
1199
1200 copyBuffer(device, commandPool, graphicsQueue, stagingVertexBuffer, vertexBufferHandle, vertexBufferSize);
1201 copyBuffer(device, commandPool, graphicsQueue, stagingIndexBuffer, indexBufferHandle, indexBufferSize);
1202
1203 vkDestroyBuffer(device, stagingVertexBuffer, nullptr);
1204 vkFreeMemory(device, stagingVertexMemory, nullptr);
1205 vkDestroyBuffer(device, stagingIndexBuffer, nullptr);
1206 vkFreeMemory(device, stagingIndexMemory, nullptr);
1207
1208 logMXModelStep("upload complete", true);
1209 }
1210
1211 void MXModel::cleanup(VkDevice device) {
1212 if (device == VK_NULL_HANDLE) {
1213 return;
1214 }
1215
1216 const bool hadBuffers = vertexBufferHandle != VK_NULL_HANDLE || indexBufferHandle != VK_NULL_HANDLE ||
1217 vertexBufferMemory != VK_NULL_HANDLE || indexBufferMemory != VK_NULL_HANDLE;
1218 if (hadBuffers) {
1219 logMXModelStep("teardown begin", true);
1220 }
1221
1222 if (vertexBufferHandle != VK_NULL_HANDLE) {
1223 vkDestroyBuffer(device, vertexBufferHandle, nullptr);
1224 vertexBufferHandle = VK_NULL_HANDLE;
1225 }
1226 if (vertexBufferMemory != VK_NULL_HANDLE) {
1227 vkFreeMemory(device, vertexBufferMemory, nullptr);
1228 vertexBufferMemory = VK_NULL_HANDLE;
1229 }
1230
1231 if (indexBufferHandle != VK_NULL_HANDLE) {
1232 vkDestroyBuffer(device, indexBufferHandle, nullptr);
1233 indexBufferHandle = VK_NULL_HANDLE;
1234 }
1235 if (indexBufferMemory != VK_NULL_HANDLE) {
1236 vkFreeMemory(device, indexBufferMemory, nullptr);
1237 indexBufferMemory = VK_NULL_HANDLE;
1238 }
1239
1240 if (hadBuffers) {
1241 logMXModelStep("teardown complete", true);
1242 }
1243 }
1244
1245 void MXModel::draw(VkCommandBuffer cmd) const {
1246 if (cmd == VK_NULL_HANDLE || vertexBufferHandle == VK_NULL_HANDLE || indexBufferHandle == VK_NULL_HANDLE || indicesData.empty()) {
1247 return;
1248 }
1249
1250 const VkBuffer buffers[] = {vertexBufferHandle};
1251 const VkDeviceSize offsets[] = {0};
1252 vkCmdBindVertexBuffers(cmd, 0, 1, buffers, offsets);
1253 vkCmdBindIndexBuffer(cmd, indexBufferHandle, 0, VK_INDEX_TYPE_UINT32);
1254 vkCmdDrawIndexed(cmd, indexCount(), 1, 0, 0, 0);
1255 }
1256
1257 void MXModel::drawSubMesh(VkCommandBuffer cmd, size_t index) const {
1258 if (cmd == VK_NULL_HANDLE || index >= subMeshList.size() ||
1259 vertexBufferHandle == VK_NULL_HANDLE || indexBufferHandle == VK_NULL_HANDLE) {
1260 return;
1261 }
1262
1263 const VkBuffer buffers[] = {vertexBufferHandle};
1264 const VkDeviceSize offsets[] = {0};
1265 vkCmdBindVertexBuffers(cmd, 0, 1, buffers, offsets);
1266 vkCmdBindIndexBuffer(cmd, indexBufferHandle, 0, VK_INDEX_TYPE_UINT32);
1267
1268 const SubMesh &sm = subMeshList[index];
1269 if (sm.indexCount == 0) {
1270 return;
1271 }
1272
1273 vkCmdDrawIndexed(cmd, sm.indexCount, 1, sm.firstIndex, 0, 0);
1274 }
1275
1276 void MXModel::createBuffer(VkDevice device, VkPhysicalDevice physicalDevice,
1277 VkDeviceSize size, VkBufferUsageFlags usage,
1278 VkMemoryPropertyFlags properties,
1279 VkBuffer &buffer, VkDeviceMemory &bufferMemory) {
1280 if (size == 0) {
1281 throw mxvk::Exception("MXModel::createBuffer cannot allocate zero-sized buffer");
1282 }
1283
1284 VkBufferCreateInfo bufferInfo{};
1285 bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
1286 bufferInfo.size = size;
1287 bufferInfo.usage = usage;
1288 bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
1289
1290 if (vkCreateBuffer(device, &bufferInfo, nullptr, &buffer) != VK_SUCCESS) {
1291 throw mxvk::Exception("MXModel::createBuffer failed to create VkBuffer");
1292 }
1293
1294 VkMemoryRequirements memRequirements{};
1295 vkGetBufferMemoryRequirements(device, buffer, &memRequirements);
1296
1297 VkMemoryAllocateInfo allocInfo{};
1298 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
1299 allocInfo.allocationSize = memRequirements.size;
1300
1301 try {
1302 allocInfo.memoryTypeIndex = findMemoryType(physicalDevice, memRequirements.memoryTypeBits, properties);
1303 if (vkAllocateMemory(device, &allocInfo, nullptr, &bufferMemory) != VK_SUCCESS) {
1304 throw mxvk::Exception("MXModel::createBuffer failed to allocate memory");
1305 }
1306
1307 if (vkBindBufferMemory(device, buffer, bufferMemory, 0) != VK_SUCCESS) {
1308 throw mxvk::Exception("MXModel::createBuffer failed to bind memory");
1309 }
1310 } catch (...) {
1311 if (bufferMemory != VK_NULL_HANDLE) {
1312 vkFreeMemory(device, bufferMemory, nullptr);
1313 bufferMemory = VK_NULL_HANDLE;
1314 }
1315 if (buffer != VK_NULL_HANDLE) {
1316 vkDestroyBuffer(device, buffer, nullptr);
1317 buffer = VK_NULL_HANDLE;
1318 }
1319 throw;
1320 }
1321 }
1322
1323 uint32_t MXModel::findMemoryType(VkPhysicalDevice physicalDevice,
1324 uint32_t typeFilter, VkMemoryPropertyFlags properties) {
1325 VkPhysicalDeviceMemoryProperties memProperties{};
1326 vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);
1327
1328 for (uint32_t i = 0; i < memProperties.memoryTypeCount; ++i) {
1329 const bool typeMatches = (typeFilter & (1u << i)) != 0u;
1330 const bool flagsMatch = (memProperties.memoryTypes[i].propertyFlags & properties) == properties;
1331 if (typeMatches && flagsMatch) {
1332 return i;
1333 }
1334 }
1335
1336 throw mxvk::Exception("MXModel::findMemoryType failed to find suitable memory type");
1337 }
1338
1339 void MXModel::copyBuffer(VkDevice device, VkCommandPool commandPool,
1340 VkQueue graphicsQueue,
1341 VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) {
1342 if (size == 0) {
1343 return;
1344 }
1345
1346 VkCommandBufferAllocateInfo allocInfo{};
1347 allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
1348 allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
1349 allocInfo.commandPool = commandPool;
1350 allocInfo.commandBufferCount = 1;
1351
1352 VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
1353 if (vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer) != VK_SUCCESS) {
1354 throw mxvk::Exception("MXModel::copyBuffer failed to allocate command buffer");
1355 }
1356
1357 VkCommandBufferBeginInfo beginInfo{};
1358 beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
1359 beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
1360 if (vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) {
1361 vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
1362 throw mxvk::Exception("MXModel::copyBuffer failed to begin command buffer");
1363 }
1364
1365 VkBufferCopy copyRegion{};
1366 copyRegion.size = size;
1367 vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, &copyRegion);
1368
1369 if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) {
1370 vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
1371 throw mxvk::Exception("MXModel::copyBuffer failed to end command buffer");
1372 }
1373
1374 VkSubmitInfo submitInfo{};
1375 submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
1376 submitInfo.commandBufferCount = 1;
1377 submitInfo.pCommandBuffers = &commandBuffer;
1378
1379 if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE) != VK_SUCCESS) {
1380 vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
1381 throw mxvk::Exception("MXModel::copyBuffer failed to submit command buffer");
1382 }
1383
1384 if (vkQueueWaitIdle(graphicsQueue) != VK_SUCCESS) {
1385 vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
1386 throw mxvk::Exception("MXModel::copyBuffer failed to wait for queue idle");
1387 }
1388
1389 vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
1390 }
1391
1392 void MXModel::loadMTL(const std::string &path) {
1393 std::ifstream file(path);
1394 if (!file.is_open()) {
1395 return;
1396 }
1397
1398 parseMTLStream(file, std::filesystem::path(path).parent_path().string(), materialList);
1399 }
1400
1401 void MXModel::loadTextureManifest(const std::string &path, const std::string &textureBasePath) {
1402 std::ifstream file(path);
1403 if (!file.is_open()) {
1404 throw mxvk::Exception("MXModel::loadTextureManifest failed to open file: " + path);
1405 }
1406
1407 std::vector<std::string> lines{};
1408 std::string line{};
1409 while (std::getline(file, line)) {
1411 if (line.empty()) {
1412 continue;
1413 }
1414 lines.push_back(line);
1415 }
1416
1417 bool isStructured = false;
1418 bool isMtlLike = false;
1419 for (const std::string &ln : lines) {
1420 std::istringstream stream(ln);
1421 std::string tag{};
1422 stream >> tag;
1423 if (tag == "submesh" || tag == "texture_dir" || tag == "material_lib" || tag == "model" || tag == "texture") {
1424 isStructured = true;
1425 break;
1426 }
1427 if (tag == "newmtl") {
1428 isMtlLike = true;
1429 break;
1430 }
1431 }
1432
1433 materialList.clear();
1434
1435 std::string manifestTextureBase = textureBasePath.empty()
1436 ? std::filesystem::path(path).parent_path().string()
1437 : textureBasePath;
1438
1439 if (isMtlLike) {
1440 std::istringstream stream{};
1441 std::string text{};
1442 for (const std::string &ln : lines) {
1443 text += ln;
1444 text += '\n';
1445 }
1446 stream.str(text);
1447 parseMTLStream(stream, manifestTextureBase, materialList);
1448 } else if (isStructured) {
1449 for (const std::string &ln : lines) {
1450 std::istringstream stream(ln);
1451 std::string tag{};
1452 stream >> tag;
1453
1454 if (tag == "texture_dir") {
1455 std::string dir{};
1456 std::getline(stream, dir);
1457 trim(dir);
1458 if (!dir.empty()) {
1459 manifestTextureBase = resolveManifestPath(std::filesystem::path(path).parent_path().string(), dir);
1460 }
1461 continue;
1462 }
1463
1464 if (tag == "material_lib") {
1465 std::string materialPath{};
1466 std::getline(stream, materialPath);
1467 trim(materialPath);
1468 if (!materialPath.empty()) {
1469 loadMTL(resolveManifestPath(std::filesystem::path(path).parent_path().string(), materialPath));
1470 }
1471 continue;
1472 }
1473
1474 if (tag == "texture") {
1475 std::string image{};
1476 if (stream >> image) {
1477 MXMaterial material{};
1478 material.name = "material_" + std::to_string(materialList.size());
1479 material.map_kd = resolveManifestPath(manifestTextureBase, image);
1480 materialList.push_back(material);
1481 }
1482 continue;
1483 }
1484
1485 if (tag == "submesh") {
1486 uint32_t subMeshIndex = 0;
1487 if (!(stream >> subMeshIndex) || subMeshIndex >= subMeshList.size()) {
1488 continue;
1489 }
1490
1491 std::string materialOrTexture{};
1492 if (!(stream >> materialOrTexture)) {
1493 continue;
1494 }
1495
1496 try {
1497 size_t consumed = 0;
1498 const unsigned long value = std::stoul(materialOrTexture, &consumed, 10);
1499 if (consumed == materialOrTexture.size()) {
1500 subMeshList[subMeshIndex].textureIndex = static_cast<uint32_t>(value);
1501 subMeshList[subMeshIndex].materialName = materialNameForTextureIndex(subMeshList[subMeshIndex].textureIndex, materialList);
1502 continue;
1503 }
1504 } catch (const std::exception &) {
1505 }
1506
1507 subMeshList[subMeshIndex].materialName = materialOrTexture;
1508 }
1509 }
1510 } else {
1511 for (const std::string &ln : lines) {
1512 MXMaterial material{};
1513 material.name = "material_" + std::to_string(materialList.size());
1514 material.map_kd = resolveManifestPath(manifestTextureBase, ln);
1515 materialList.push_back(material);
1516 }
1517 }
1518
1519 if (materialList.empty()) {
1520 return;
1521 }
1522
1523 for (SubMesh &sm : subMeshList) {
1524 if (sm.materialName.empty()) {
1525 sm.materialName = materialNameForTextureIndex(sm.textureIndex, materialList);
1526 }
1527 }
1528 }
1529
1530} // namespace mxvk
MXModel()=default
uint32_t indexCount() const
void draw(VkCommandBuffer cmd) const
Record one indexed draw for the full mesh.
void cleanup(VkDevice device)
Release owned GPU buffers.
void upload(VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue)
Upload parsed geometry to device-local GPU buffers.
MXModel & operator=(const MXModel &)=delete
void compressIndices()
Remove duplicate vertices and remap indices.
void drawSubMesh(VkCommandBuffer cmd, size_t index) const
Record one indexed draw for a sub-mesh.
void load(const std::string &path, float positionScale=1.0f)
Parse model data from disk into CPU-side arrays.
void exportOBJ(const std::string &objPath, const std::string &mtlPath="") const
Export the loaded model as Wavefront OBJ plus MTL.
Vulkan mesh loader and GPU buffer manager for MXVK.
bool parseOBJIndexValue(const std::string &text, int &value)
void assignNormalIfMissing(OBJFaceVertex &a, OBJFaceVertex &b, OBJFaceVertex &c)
float edgeCross2(const VKVertex &a, const VKVertex &b, const VKVertex &c, int dropAxis)
MXMODParseResult parseMXMODStream(std::istream &file, const std::string &sourcePath, float positionScale)
bool pointInProjectedTriangle(const VKVertex &point, const VKVertex &a, const VKVertex &b, const VKVertex &c, int dropAxis, float windingSign)
std::string parseMTLTexturePath(std::istream &stream)
std::string materialNameForTextureIndex(uint32_t textureIndex, const std::vector< MXMaterial > &materials)
float projectedArea2(const std::vector< OBJFaceVertex > &face, int dropAxis)
std::string resolveManifestPath(const std::string &basePath, const std::string &path)
void appendOBJTriangle(std::vector< VKVertex > &vertices, OBJFaceVertex a, OBJFaceVertex b, OBJFaceVertex c)
void writeMTLMaterial(std::ostream &out, const MXMaterial &material)
void logMXModelStep(const std::string &message, bool important=false)
std::string mtlTextureReferencePath(const std::filesystem::path &mtlPath, const std::string &texturePath)
std::string inflateCompressedText(const std::vector< unsigned char > &compressedData)
std::string mtlReferencePath(const std::string &objPath, const std::string &mtlPath)
void parseMTLStream(std::istream &file, const std::string &textureBasePath, std::vector< MXMaterial > &materials)
bool parseOBJFaceToken(const std::string &token, OBJIndex &index)
Vec3 faceNormal(const VKVertex &a, const VKVertex &b, const VKVertex &c)
int resolveOBJIndex(int objIndex, const std::vector< T > &values)
void triangulateOBJFace(const std::vector< OBJFaceVertex > &face, std::vector< VKVertex > &vertices)
Utilities for loading and saving PNG images.
Definition mxvk.hpp:30
Parsed subset of Wavefront MTL material fields.
std::string map_kd
std::string name
One indexed sub-range that can reference a dedicated texture slot.
uint32_t indexCount
uint32_t textureIndex
uint32_t firstIndex
std::size_t operator()(const VKVertex &v) const
Vertex payload consumed by MXVK model shaders.
float normal[3]
float texCoord[2]