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;
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;
49 other.vertexBufferHandle = VK_NULL_HANDLE;
50 other.vertexBufferMemory = VK_NULL_HANDLE;
51 other.indexBufferHandle = VK_NULL_HANDLE;
52 other.indexBufferMemory = VK_NULL_HANDLE;
59 std::cout <<
"mxvk_model: " << message <<
'\n';
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) {
97 const size_t end = s.find_last_not_of(
" \t\r\n");
98 s = s.substr(begin, end - begin + 1);
102 const size_t commentPos = s.find(
'#');
103 if (commentPos != std::string::npos) {
104 s = s.substr(0, commentPos);
117 value = std::stoi(text, &consumed, 10);
118 }
catch (
const std::exception &) {
122 return consumed == text.size();
126 std::array<std::string, 3> fields{};
127 size_t fieldIndex = 0;
128 size_t fieldBegin = 0;
131 if (fieldIndex >= fields.size()) {
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) {
140 fieldBegin = slashPos + 1;
152 template <
typename T>
158 return static_cast<int>(values.size()) + objIndex;
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];
176 const float len = std::sqrt(normal.
x * normal.
x + normal.
y * normal.
y + normal.
z * normal.
z);
192 if (faceVertex->hasNormal) {
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;
202 [[nodiscard]]
float projectedArea2(
const std::vector<OBJFaceVertex> &face,
int dropAxis) {
204 for (
size_t i = 0; i < face.size(); ++i) {
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;
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);
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;
241 vertices.push_back(a.
vertex);
242 vertices.push_back(b.
vertex);
243 vertices.push_back(c.
vertex);
247 if (face.size() < 3) {
251 if (face.size() == 3) {
256 const Vec3 normal =
faceNormal(face[0].vertex, face[1].vertex, face[2].vertex);
258 if (std::fabs(normal.
y) > std::fabs(normal.
x) && std::fabs(normal.
y) >= std::fabs(normal.
z)) {
260 }
else if (std::fabs(normal.
z) > std::fabs(normal.
x) && std::fabs(normal.
z) > std::fabs(normal.
y)) {
264 float windingSign =
projectedArea2(face, dropAxis) >= 0.0f ? 1.0f : -1.0f;
266 for (
size_t i = 2; i < face.size(); ++i) {
272 std::vector<size_t> remaining(face.size());
273 for (
size_t i = 0; i < remaining.size(); ++i) {
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()];
284 const float cross =
edgeCross2(face[previous].vertex, face[current].vertex, face[next].vertex, dropAxis) * windingSign;
285 if (cross <= 1e-6f) {
289 bool containsPoint =
false;
290 for (
const size_t test : remaining) {
291 if (test == previous || test == current || test == next) {
295 face[previous].vertex,
296 face[current].vertex,
300 containsPoint =
true;
310 remaining.erase(remaining.begin() +
static_cast<std::ptrdiff_t
>(i));
316 for (
size_t i = 2; i < remaining.size(); ++i) {
317 appendOBJTriangle(vertices, face[remaining[0]], face[remaining[i - 1]], face[remaining[i]]);
323 appendOBJTriangle(vertices, face[remaining[0]], face[remaining[1]], face[remaining[2]]);
328 stream.next_in =
const_cast<Bytef *
>(
reinterpret_cast<const Bytef *
>(compressedData.data()));
329 stream.avail_in =
static_cast<uInt
>(compressedData.size());
331 if (inflateInit2(&stream, 15 + 32) != Z_OK) {
332 throw mxvk::Exception(
"MXModel::loadMXMODZ failed to initialize zlib inflater");
335 std::string output{};
336 std::array<char, 16384> buffer{};
339 while (result != Z_STREAM_END) {
340 stream.next_out =
reinterpret_cast<Bytef *
>(buffer.data());
341 stream.avail_out =
static_cast<uInt
>(buffer.size());
343 result = inflate(&stream, Z_NO_FLUSH);
344 if (result != Z_OK && result != Z_STREAM_END) {
346 throw mxvk::Exception(
"MXModel::loadMXMODZ failed to inflate compressed data");
349 const size_t producedBytes = buffer.size() -
static_cast<size_t>(stream.avail_out);
350 output.append(buffer.data(), producedBytes);
355 if (output.empty()) {
356 throw mxvk::Exception(
"MXModel::loadMXMODZ produced empty decompressed payload");
367 std::filesystem::path resolved(path);
368 if (resolved.is_absolute() || basePath.empty()) {
369 return resolved.lexically_normal().string();
372 return (std::filesystem::path(basePath) / resolved).lexically_normal().string();
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") {
383 }
else if (mapToken ==
"-mm") {
386 }
else if (mapToken ==
"-o" || mapToken ==
"-s" || mapToken ==
"-t") {
390 }
else if (mapToken ==
"-bm" || mapToken ==
"-boost" || mapToken ==
"-texres") {
396 if (!mapPath.empty()) {
405 const std::string &textureBasePath,
406 std::vector<MXMaterial> &materials) {
409 while (std::getline(file, line)) {
415 std::istringstream stream(line);
419 if (tag ==
"newmtl") {
420 materials.emplace_back();
421 current = &materials.back();
422 std::getline(stream, current->
name);
427 if (current ==
nullptr) {
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") {
445 if (!mapPath.empty()) {
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();
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();
465 return materialPath.filename().string();
469 const std::vector<MXMaterial> &materials) {
470 if (textureIndex <
static_cast<uint32_t
>(materials.size()) && !materials[textureIndex].name.empty()) {
471 return materials[textureIndex].name;
473 return "material_" + std::to_string(textureIndex);
477 const std::string &texturePath) {
478 if (texturePath.empty()) {
482 std::filesystem::path resolvedTexturePath(texturePath);
483 if (resolvedTexturePath.is_relative()) {
484 resolvedTexturePath = std::filesystem::absolute(resolvedTexturePath);
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);
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();
500 return resolvedTexturePath.lexically_normal().string();
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';
518 const std::string &sourcePath,
519 float positionScale) {
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{};
528 std::vector<TriBlock> triBlocks{};
529 TriBlock *current =
nullptr;
530 int sectionType = -1;
533 while (std::getline(file, line)) {
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 ==
'.';
543 if (isData && current !=
nullptr) {
548 switch (sectionType) {
550 if (stream >> x >> y >> z) {
551 current->pos.push_back({x * positionScale, y * positionScale, z * positionScale});
555 if (stream >> x >> y) {
556 current->uv.push_back({x, y});
560 if (stream >> x >> y >> z) {
561 current->nrm.push_back({x, y, z});
566 while (stream >> idx) {
567 current->fileIndices.push_back(idx);
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;
603 if (tag ==
"indices") {
609 if (triBlocks.empty()) {
610 throw mxvk::Exception(
"MXModel::loadMXMOD no geometry found in " + sourcePath);
614 for (
const TriBlock &blk : triBlocks) {
615 if (blk.pos.empty()) {
619 const uint32_t vertexBase =
static_cast<uint32_t
>(parsed.
vertices.size());
620 for (
size_t i = 0; i < blk.pos.size(); ++i) {
622 v.
pos[0] = blk.pos[i].x;
623 v.
pos[1] = blk.pos[i].y;
624 v.
pos[2] = blk.pos[i].z;
626 if (i < blk.uv.size()) {
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;
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);
645 for (uint32_t i = 0; i < static_cast<uint32_t>(blk.pos.size()); ++i) {
646 parsed.
indices.push_back(vertexBase + i);
658 throw mxvk::Exception(
"MXModel::loadMXMOD no renderable data found in " + sourcePath);
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);
672 for (
int i = 0; i < 3; ++i) {
675 for (
int i = 0; i < 2; ++i) {
678 for (
int i = 0; i < 3; ++i) {
686 if (verticesData.empty() || indicesData.empty()) {
690 std::vector<VKVertex> uniqueVertices{};
691 uniqueVertices.reserve(verticesData.size());
693 std::unordered_map<VKVertex, uint32_t, VKVertexHash> vertexMap{};
694 std::vector<uint32_t> remap(verticesData.size(), 0);
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;
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;
709 for (uint32_t &idx : indicesData) {
710 if (idx <
static_cast<uint32_t
>(remap.size())) {
715 verticesData = std::move(uniqueVertices);
723 logMXModelStep(
"load begin: " + path,
true);
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()),
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()),
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()),
752 throw mxvk::Exception(
"MXModel::load unsupported file format: " + 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);
766 if (objPath.empty()) {
769 if (verticesData.empty() || indicesData.empty()) {
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);
778 if (!objOutputPath.parent_path().empty()) {
779 std::filesystem::create_directories(objOutputPath.parent_path());
781 if (!mtlOutputPath.parent_path().empty()) {
782 std::filesystem::create_directories(mtlOutputPath.parent_path());
785 std::ofstream objFile(objOutputPath);
786 if (!objFile.is_open()) {
787 throw mxvk::Exception(
"MXModel::exportOBJ failed to open OBJ file: " + objOutputPath.string());
790 std::ofstream mtlFile(mtlOutputPath);
791 if (!mtlFile.is_open()) {
792 throw mxvk::Exception(
"MXModel::exportOBJ failed to open MTL file: " + mtlOutputPath.string());
795 objFile <<
"# Exported from MXVK MXModel\n";
796 objFile <<
"mtllib " << mtlReferencePath(objOutputPath.string(), mtlOutputPath.string()) <<
"\n\n";
798 for (
const VKVertex &vertex : verticesData) {
799 objFile <<
"v " << vertex.pos[0] <<
' ' << vertex.pos[1] <<
' ' << vertex.pos[2] <<
'\n';
803 for (
const VKVertex &vertex : verticesData) {
804 objFile <<
"vt " << vertex.texCoord[0] <<
' ' << vertex.texCoord[1] <<
'\n';
808 for (
const VKVertex &vertex : verticesData) {
809 objFile <<
"vn " << vertex.normal[0] <<
' ' << vertex.normal[1] <<
' ' << vertex.normal[2] <<
'\n';
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);
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;
826 const auto ensureOutputMaterial = [&outputMaterials, &hasOutputMaterial](
const std::string &name) {
827 if (hasOutputMaterial(name)) {
831 material.
name = name;
832 outputMaterials.push_back(material);
835 if (subMeshList.empty()) {
836 ensureOutputMaterial(
"material_0");
838 for (
const SubMesh &sm : subMeshList) {
839 const std::string materialName = !sm.materialName.empty()
841 : materialNameForTextureIndex(sm.textureIndex, outputMaterials);
842 ensureOutputMaterial(materialName);
846 for (
const MXMaterial &material : outputMaterials) {
848 outputMaterial.
map_kd = mtlTextureReferencePath(mtlOutputPath, outputMaterial.
map_kd);
849 writeMTLMaterial(mtlFile, outputMaterial);
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");
857 throw mxvk::Exception(
"MXModel::exportOBJ only triangle index ranges can be exported");
860 objFile <<
"usemtl " << materialName <<
'\n';
861 for (uint32_t i = 0; i <
indexCount; i += 3U) {
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");
868 const uint32_t objIndex = vertexIndex + 1U;
869 objFile <<
' ' << objIndex <<
'/' << objIndex <<
'/' << objIndex;
876 if (subMeshList.empty()) {
877 emitFaces(0,
static_cast<uint32_t
>(indicesData.size()),
"material_0");
879 for (
const SubMesh &sm : subMeshList) {
880 const std::string materialName = !sm.materialName.empty()
882 : materialNameForTextureIndex(sm.textureIndex, materialList);
883 emitFaces(sm.firstIndex, sm.indexCount, materialName);
889 const std::string &textureManifestPath,
890 const std::string &textureBasePath,
891 const std::string &objPath,
892 float positionScale) {
894 model.
load(modelPath, textureManifestPath, textureBasePath, positionScale);
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);
904 std::vector<Vec3> positions{};
905 std::vector<Vec2> texcoords{};
906 std::vector<Vec3> normals{};
908 verticesData.clear();
911 materialList.clear();
912 mtlLibraryPath.clear();
914 std::vector<VKVertex> currentVerts{};
915 std::string currentMaterialName{};
917 auto finalizeGroup = [&]() {
918 if (currentVerts.empty()) {
923 sm.firstIndex =
static_cast<uint32_t
>(indicesData.size());
924 sm.indexCount =
static_cast<uint32_t
>(currentVerts.size());
925 sm.materialName = currentMaterialName;
927 const uint32_t baseVertex =
static_cast<uint32_t
>(verticesData.size());
928 verticesData.insert(verticesData.end(), currentVerts.begin(), currentVerts.end());
930 for (uint32_t i = 0; i < sm.indexCount; ++i) {
931 indicesData.push_back(baseVertex + i);
934 subMeshList.push_back(sm);
935 currentVerts.clear();
939 while (std::getline(file, line)) {
945 std::istringstream stream(line);
953 if (stream >> x >> y >> z) {
954 positions.push_back({x * positionScale, y * positionScale, z * positionScale});
962 if (stream >> u >> v) {
963 texcoords.push_back({u, v});
972 if (stream >> x >> y >> z) {
973 normals.push_back({x, y, z});
978 if (tag ==
"mtllib") {
979 std::string mtlFile{};
980 std::getline(stream, 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();
988 mtlLibraryPath = (objPath.parent_path() / mtlPath).string();
993 if (tag ==
"g" || tag ==
"o") {
998 if (tag ==
"usemtl") {
999 std::string materialName{};
1000 std::getline(stream, materialName);
1002 if (!materialName.empty() && materialName != currentMaterialName) {
1004 currentMaterialName = materialName;
1013 std::vector<OBJFaceVertex> faceVerts{};
1014 std::string token{};
1015 while (stream >> token) {
1016 OBJIndex objIndex{};
1018 throw mxvk::Exception(
"MXModel::loadOBJ malformed face token '" + token +
"' in " + path);
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);
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;
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);
1035 faceVertex.vertex.texCoord[0] = texcoords[
static_cast<size_t>(texcoordIndex)].x;
1036 faceVertex.vertex.texCoord[1] = texcoords[
static_cast<size_t>(texcoordIndex)].y;
1039 if (objIndex.normal != 0) {
1041 if (normalIndex < 0 || normalIndex >=
static_cast<int>(normals.size())) {
1042 throw mxvk::Exception(
"MXModel::loadOBJ face normal index out of range in " + path);
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;
1050 faceVerts.push_back(faceVertex);
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);
1065 for (SubMesh &sm : subMeshList) {
1066 const auto it = materialIndices.find(sm.materialName);
1067 if (it != materialIndices.end()) {
1068 sm.textureIndex = it->second;
1073 if (verticesData.empty() || indicesData.empty()) {
1074 throw mxvk::Exception(
"MXModel::loadOBJ no geometry found in " + path);
1077 const bool hasNormals = !normals.empty();
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];
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);
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;
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);
1114 const MXMODParseResult parsed =
parseMXMODStream(file, path, positionScale);
1116 verticesData = parsed.vertices;
1117 indicesData = parsed.indices;
1118 subMeshList = parsed.subMeshes;
1119 materialList.clear();
1120 mtlLibraryPath.clear();
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);
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);
1137 std::istringstream stream(decompressedText);
1138 const MXMODParseResult parsed =
parseMXMODStream(stream, path, positionScale);
1140 verticesData = parsed.vertices;
1141 indicesData = parsed.indices;
1142 subMeshList = parsed.subMeshes;
1143 materialList.clear();
1144 mtlLibraryPath.clear();
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");
1155 if (verticesData.empty() || indicesData.empty()) {
1159 logMXModelStep(
"upload begin",
true);
1163 const VkDeviceSize vertexBufferSize =
sizeof(
VKVertex) * verticesData.size();
1164 const VkDeviceSize indexBufferSize =
sizeof(uint32_t) * indicesData.size();
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);
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);
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);
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);
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);
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);
1200 copyBuffer(device, commandPool, graphicsQueue, stagingVertexBuffer, vertexBufferHandle, vertexBufferSize);
1201 copyBuffer(device, commandPool, graphicsQueue, stagingIndexBuffer, indexBufferHandle, indexBufferSize);
1203 vkDestroyBuffer(device, stagingVertexBuffer,
nullptr);
1204 vkFreeMemory(device, stagingVertexMemory,
nullptr);
1205 vkDestroyBuffer(device, stagingIndexBuffer,
nullptr);
1206 vkFreeMemory(device, stagingIndexMemory,
nullptr);
1208 logMXModelStep(
"upload complete",
true);
1212 if (device == VK_NULL_HANDLE) {
1216 const bool hadBuffers = vertexBufferHandle != VK_NULL_HANDLE || indexBufferHandle != VK_NULL_HANDLE ||
1217 vertexBufferMemory != VK_NULL_HANDLE || indexBufferMemory != VK_NULL_HANDLE;
1219 logMXModelStep(
"teardown begin",
true);
1222 if (vertexBufferHandle != VK_NULL_HANDLE) {
1223 vkDestroyBuffer(device, vertexBufferHandle,
nullptr);
1224 vertexBufferHandle = VK_NULL_HANDLE;
1226 if (vertexBufferMemory != VK_NULL_HANDLE) {
1227 vkFreeMemory(device, vertexBufferMemory,
nullptr);
1228 vertexBufferMemory = VK_NULL_HANDLE;
1231 if (indexBufferHandle != VK_NULL_HANDLE) {
1232 vkDestroyBuffer(device, indexBufferHandle,
nullptr);
1233 indexBufferHandle = VK_NULL_HANDLE;
1235 if (indexBufferMemory != VK_NULL_HANDLE) {
1236 vkFreeMemory(device, indexBufferMemory,
nullptr);
1237 indexBufferMemory = VK_NULL_HANDLE;
1241 logMXModelStep(
"teardown complete",
true);
1246 if (cmd == VK_NULL_HANDLE || vertexBufferHandle == VK_NULL_HANDLE || indexBufferHandle == VK_NULL_HANDLE || indicesData.empty()) {
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);
1258 if (cmd == VK_NULL_HANDLE || index >= subMeshList.size() ||
1259 vertexBufferHandle == VK_NULL_HANDLE || indexBufferHandle == VK_NULL_HANDLE) {
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);
1268 const SubMesh &sm = subMeshList[index];
1276 void MXModel::createBuffer(VkDevice device, VkPhysicalDevice physicalDevice,
1277 VkDeviceSize size, VkBufferUsageFlags usage,
1278 VkMemoryPropertyFlags properties,
1279 VkBuffer &buffer, VkDeviceMemory &bufferMemory) {
1281 throw mxvk::Exception(
"MXModel::createBuffer cannot allocate zero-sized buffer");
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;
1290 if (vkCreateBuffer(device, &bufferInfo,
nullptr, &buffer) != VK_SUCCESS) {
1291 throw mxvk::Exception(
"MXModel::createBuffer failed to create VkBuffer");
1294 VkMemoryRequirements memRequirements{};
1295 vkGetBufferMemoryRequirements(device, buffer, &memRequirements);
1297 VkMemoryAllocateInfo allocInfo{};
1298 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
1299 allocInfo.allocationSize = memRequirements.size;
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");
1307 if (vkBindBufferMemory(device, buffer, bufferMemory, 0) != VK_SUCCESS) {
1308 throw mxvk::Exception(
"MXModel::createBuffer failed to bind memory");
1311 if (bufferMemory != VK_NULL_HANDLE) {
1312 vkFreeMemory(device, bufferMemory,
nullptr);
1313 bufferMemory = VK_NULL_HANDLE;
1315 if (buffer != VK_NULL_HANDLE) {
1316 vkDestroyBuffer(device, buffer,
nullptr);
1317 buffer = VK_NULL_HANDLE;
1323 uint32_t MXModel::findMemoryType(VkPhysicalDevice physicalDevice,
1324 uint32_t typeFilter, VkMemoryPropertyFlags properties) {
1325 VkPhysicalDeviceMemoryProperties memProperties{};
1326 vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);
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) {
1336 throw mxvk::Exception(
"MXModel::findMemoryType failed to find suitable memory type");
1339 void MXModel::copyBuffer(VkDevice device, VkCommandPool commandPool,
1340 VkQueue graphicsQueue,
1341 VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) {
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;
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");
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");
1365 VkBufferCopy copyRegion{};
1366 copyRegion.size = size;
1367 vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, ©Region);
1369 if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) {
1370 vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
1371 throw mxvk::Exception(
"MXModel::copyBuffer failed to end command buffer");
1374 VkSubmitInfo submitInfo{};
1375 submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
1376 submitInfo.commandBufferCount = 1;
1377 submitInfo.pCommandBuffers = &commandBuffer;
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");
1384 if (vkQueueWaitIdle(graphicsQueue) != VK_SUCCESS) {
1385 vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
1386 throw mxvk::Exception(
"MXModel::copyBuffer failed to wait for queue idle");
1389 vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
1392 void MXModel::loadMTL(
const std::string &path) {
1393 std::ifstream file(path);
1394 if (!file.is_open()) {
1398 parseMTLStream(file, std::filesystem::path(path).parent_path().
string(), materialList);
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);
1407 std::vector<std::string> lines{};
1409 while (std::getline(file, line)) {
1414 lines.push_back(line);
1417 bool isStructured =
false;
1418 bool isMtlLike =
false;
1419 for (
const std::string &ln : lines) {
1420 std::istringstream stream(ln);
1423 if (tag ==
"submesh" || tag ==
"texture_dir" || tag ==
"material_lib" || tag ==
"model" || tag ==
"texture") {
1424 isStructured =
true;
1427 if (tag ==
"newmtl") {
1433 materialList.clear();
1435 std::string manifestTextureBase = textureBasePath.empty()
1436 ? std::filesystem::path(path).parent_path().string()
1440 std::istringstream stream{};
1442 for (
const std::string &ln : lines) {
1448 }
else if (isStructured) {
1449 for (
const std::string &ln : lines) {
1450 std::istringstream stream(ln);
1454 if (tag ==
"texture_dir") {
1456 std::getline(stream, dir);
1459 manifestTextureBase =
resolveManifestPath(std::filesystem::path(path).parent_path().
string(), dir);
1464 if (tag ==
"material_lib") {
1465 std::string materialPath{};
1466 std::getline(stream, materialPath);
1468 if (!materialPath.empty()) {
1469 loadMTL(
resolveManifestPath(std::filesystem::path(path).parent_path().
string(), materialPath));
1474 if (tag ==
"texture") {
1475 std::string image{};
1476 if (stream >> image) {
1477 MXMaterial material{};
1478 material.name =
"material_" + std::to_string(materialList.size());
1480 materialList.push_back(material);
1485 if (tag ==
"submesh") {
1486 uint32_t subMeshIndex = 0;
1487 if (!(stream >> subMeshIndex) || subMeshIndex >= subMeshList.size()) {
1491 std::string materialOrTexture{};
1492 if (!(stream >> materialOrTexture)) {
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);
1504 }
catch (
const std::exception &) {
1507 subMeshList[subMeshIndex].materialName = materialOrTexture;
1511 for (
const std::string &ln : lines) {
1512 MXMaterial material{};
1513 material.name =
"material_" + std::to_string(materialList.size());
1515 materialList.push_back(material);
1519 if (materialList.empty()) {
1523 for (SubMesh &sm : subMeshList) {
1524 if (sm.materialName.empty()) {
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)
void trim(std::string &s)
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)
void stripTrailingComment(std::string &s)
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.
Parsed subset of Wavefront MTL material fields.
One indexed sub-range that can reference a dedicated texture slot.
std::size_t operator()(const VKVertex &v) const
Vertex payload consumed by MXVK model shaders.
std::vector< VKVertex > vertices
std::vector< uint32_t > indices
std::vector< SubMesh > subMeshes