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