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;
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;
40 other.vertexBufferHandle = VK_NULL_HANDLE;
41 other.vertexBufferMemory = VK_NULL_HANDLE;
42 other.indexBufferHandle = VK_NULL_HANDLE;
43 other.indexBufferMemory = VK_NULL_HANDLE;
50 std::cout <<
"mxvk_model: " << message <<
'\n';
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) {
88 const size_t end = s.find_last_not_of(
" \t\r\n");
89 s = s.substr(begin, end - begin + 1);
93 const size_t commentPos = s.find(
'#');
94 if (commentPos != std::string::npos) {
95 s = s.substr(0, commentPos);
108 value = std::stoi(text, &consumed, 10);
109 }
catch (
const std::exception &) {
113 return consumed == text.size();
117 std::array<std::string, 3> fields{};
118 size_t fieldIndex = 0;
119 size_t fieldBegin = 0;
122 if (fieldIndex >= fields.size()) {
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) {
131 fieldBegin = slashPos + 1;
141 template <
typename T> [[nodiscard]]
int resolveOBJIndex(
int objIndex,
const std::vector<T> &values) {
146 return static_cast<int>(values.size()) + objIndex;
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];
164 const float len = std::sqrt(normal.
x * normal.
x + normal.
y * normal.
y + normal.
z * normal.
z);
180 if (faceVertex->hasNormal) {
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;
190 [[nodiscard]]
float projectedArea2(
const std::vector<OBJFaceVertex> &face,
int dropAxis) {
192 for (
size_t i = 0; i < face.size(); ++i) {
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;
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);
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;
224 vertices.push_back(a.
vertex);
225 vertices.push_back(b.
vertex);
226 vertices.push_back(c.
vertex);
230 if (face.size() < 3) {
234 if (face.size() == 3) {
239 const Vec3 normal =
faceNormal(face[0].vertex, face[1].vertex, face[2].vertex);
241 if (std::fabs(normal.
y) > std::fabs(normal.
x) && std::fabs(normal.
y) >= std::fabs(normal.
z)) {
243 }
else if (std::fabs(normal.
z) > std::fabs(normal.
x) && std::fabs(normal.
z) > std::fabs(normal.
y)) {
247 float windingSign =
projectedArea2(face, dropAxis) >= 0.0f ? 1.0f : -1.0f;
249 for (
size_t i = 2; i < face.size(); ++i) {
255 std::vector<size_t> remaining(face.size());
256 for (
size_t i = 0; i < remaining.size(); ++i) {
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()];
267 const float cross =
edgeCross2(face[previous].vertex, face[current].vertex, face[next].vertex, dropAxis) * windingSign;
268 if (cross <= 1e-6f) {
272 bool containsPoint =
false;
273 for (
const size_t test : remaining) {
274 if (test == previous || test == current || test == next) {
277 if (
pointInProjectedTriangle(face[test].vertex, face[previous].vertex, face[current].vertex, face[next].vertex, dropAxis, windingSign)) {
278 containsPoint =
true;
288 remaining.erase(remaining.begin() +
static_cast<std::ptrdiff_t
>(i));
294 for (
size_t i = 2; i < remaining.size(); ++i) {
295 appendOBJTriangle(vertices, face[remaining[0]], face[remaining[i - 1]], face[remaining[i]]);
301 appendOBJTriangle(vertices, face[remaining[0]], face[remaining[1]], face[remaining[2]]);
306 stream.next_in =
const_cast<Bytef *
>(
reinterpret_cast<const Bytef *
>(compressedData.data()));
307 stream.avail_in =
static_cast<uInt
>(compressedData.size());
309 if (inflateInit2(&stream, 15 + 32) != Z_OK) {
310 throw mxvk::Exception(
"MXModel::loadMXMODZ failed to initialize zlib inflater");
313 std::string output{};
314 std::array<char, 16384> buffer{};
317 while (result != Z_STREAM_END) {
318 stream.next_out =
reinterpret_cast<Bytef *
>(buffer.data());
319 stream.avail_out =
static_cast<uInt
>(buffer.size());
321 result = inflate(&stream, Z_NO_FLUSH);
322 if (result != Z_OK && result != Z_STREAM_END) {
324 throw mxvk::Exception(
"MXModel::loadMXMODZ failed to inflate compressed data");
327 const size_t producedBytes = buffer.size() -
static_cast<size_t>(stream.avail_out);
328 output.append(buffer.data(), producedBytes);
333 if (output.empty()) {
334 throw mxvk::Exception(
"MXModel::loadMXMODZ produced empty decompressed payload");
345 std::filesystem::path resolved(path);
346 if (resolved.is_absolute() || basePath.empty()) {
347 return resolved.lexically_normal().string();
350 return (std::filesystem::path(basePath) / resolved).lexically_normal().string();
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") {
360 }
else if (mapToken ==
"-mm") {
363 }
else if (mapToken ==
"-o" || mapToken ==
"-s" || mapToken ==
"-t") {
367 }
else if (mapToken ==
"-bm" || mapToken ==
"-boost" || mapToken ==
"-texres") {
373 if (!mapPath.empty()) {
381 void parseMTLStream(std::istream &file,
const std::string &textureBasePath, std::vector<MXMaterial> &materials) {
384 while (std::getline(file, line)) {
390 std::istringstream stream(line);
394 if (tag ==
"newmtl") {
395 materials.emplace_back();
396 current = &materials.back();
397 std::getline(stream, current->
name);
402 if (current ==
nullptr) {
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") {
420 if (!mapPath.empty()) {
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();
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();
440 return materialPath.filename().string();
444 if (textureIndex <
static_cast<uint32_t
>(materials.size()) && !materials[textureIndex].name.empty()) {
445 return materials[textureIndex].name;
447 return "material_" + std::to_string(textureIndex);
451 if (texturePath.empty()) {
455 std::filesystem::path resolvedTexturePath(texturePath);
456 if (resolvedTexturePath.is_relative()) {
457 resolvedTexturePath = std::filesystem::absolute(resolvedTexturePath);
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);
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();
473 return resolvedTexturePath.lexically_normal().string();
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';
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{};
499 std::vector<TriBlock> triBlocks{};
500 TriBlock *current =
nullptr;
501 int sectionType = -1;
504 while (std::getline(file, line)) {
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 ==
'.';
514 if (isData && current !=
nullptr) {
519 switch (sectionType) {
521 if (stream >> x >> y >> z) {
522 current->pos.push_back({x * positionScale, y * positionScale, z * positionScale});
526 if (stream >> x >> y) {
527 current->uv.push_back({x, y});
531 if (stream >> x >> y >> z) {
532 current->nrm.push_back({x, y, z});
537 while (stream >> idx) {
538 current->fileIndices.push_back(idx);
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;
574 if (tag ==
"indices") {
580 if (triBlocks.empty()) {
581 throw mxvk::Exception(
"MXModel::loadMXMOD no geometry found in " + sourcePath);
585 for (
const TriBlock &blk : triBlocks) {
586 if (blk.pos.empty()) {
590 const uint32_t vertexBase =
static_cast<uint32_t
>(parsed.
vertices.size());
591 for (
size_t i = 0; i < blk.pos.size(); ++i) {
593 v.
pos[0] = blk.pos[i].x;
594 v.
pos[1] = blk.pos[i].y;
595 v.
pos[2] = blk.pos[i].z;
597 if (i < blk.uv.size()) {
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;
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);
616 for (uint32_t i = 0; i < static_cast<uint32_t>(blk.pos.size()); ++i) {
617 parsed.
indices.push_back(vertexBase + i);
629 throw mxvk::Exception(
"MXModel::loadMXMOD no renderable data found in " + sourcePath);
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);
643 for (
int i = 0; i < 3; ++i) {
646 for (
int i = 0; i < 2; ++i) {
649 for (
int i = 0; i < 3; ++i) {
657 if (verticesData.empty() || indicesData.empty()) {
661 std::vector<VKVertex> uniqueVertices{};
662 uniqueVertices.reserve(verticesData.size());
664 std::unordered_map<VKVertex, uint32_t, VKVertexHash> vertexMap{};
665 std::vector<uint32_t> remap(verticesData.size(), 0);
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;
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;
680 for (uint32_t &idx : indicesData) {
681 if (idx <
static_cast<uint32_t
>(remap.size())) {
686 verticesData = std::move(uniqueVertices);
694 logMXModelStep(
"load begin: " + path,
true);
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);
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);
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);
714 throw mxvk::Exception(
"MXModel::load unsupported file format: " + path);
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);
725 if (objPath.empty()) {
728 if (verticesData.empty() || indicesData.empty()) {
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);
735 if (!objOutputPath.parent_path().empty()) {
736 std::filesystem::create_directories(objOutputPath.parent_path());
738 if (!mtlOutputPath.parent_path().empty()) {
739 std::filesystem::create_directories(mtlOutputPath.parent_path());
742 std::ofstream objFile(objOutputPath);
743 if (!objFile.is_open()) {
744 throw mxvk::Exception(
"MXModel::exportOBJ failed to open OBJ file: " + objOutputPath.string());
747 std::ofstream mtlFile(mtlOutputPath);
748 if (!mtlFile.is_open()) {
749 throw mxvk::Exception(
"MXModel::exportOBJ failed to open MTL file: " + mtlOutputPath.string());
752 objFile <<
"# Exported from MXVK MXModel\n";
753 objFile <<
"mtllib " << mtlReferencePath(objOutputPath.string(), mtlOutputPath.string()) <<
"\n\n";
755 for (
const VKVertex &vertex : verticesData) {
756 objFile <<
"v " << vertex.pos[0] <<
' ' << vertex.pos[1] <<
' ' << vertex.pos[2] <<
'\n';
760 for (
const VKVertex &vertex : verticesData) {
761 objFile <<
"vt " << vertex.texCoord[0] <<
' ' << vertex.texCoord[1] <<
'\n';
765 for (
const VKVertex &vertex : verticesData) {
766 objFile <<
"vn " << vertex.normal[0] <<
' ' << vertex.normal[1] <<
' ' << vertex.normal[2] <<
'\n';
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);
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; }); };
779 const auto ensureOutputMaterial = [&outputMaterials, &hasOutputMaterial](
const std::string &name) {
780 if (hasOutputMaterial(name)) {
784 material.
name = name;
785 outputMaterials.push_back(material);
788 if (subMeshList.empty()) {
789 ensureOutputMaterial(
"material_0");
791 for (
const SubMesh &sm : subMeshList) {
792 const std::string materialName = !sm.materialName.empty() ? sm.materialName : materialNameForTextureIndex(sm.textureIndex, outputMaterials);
793 ensureOutputMaterial(materialName);
797 for (
const MXMaterial &material : outputMaterials) {
799 outputMaterial.
map_kd = mtlTextureReferencePath(mtlOutputPath, outputMaterial.
map_kd);
800 writeMTLMaterial(mtlFile, outputMaterial);
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");
808 throw mxvk::Exception(
"MXModel::exportOBJ only triangle index ranges can be exported");
811 objFile <<
"usemtl " << materialName <<
'\n';
812 for (uint32_t i = 0; i <
indexCount; i += 3U) {
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");
819 const uint32_t objIndex = vertexIndex + 1U;
820 objFile <<
' ' << objIndex <<
'/' << objIndex <<
'/' << objIndex;
827 if (subMeshList.empty()) {
828 emitFaces(0,
static_cast<uint32_t
>(indicesData.size()),
"material_0");
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);
837 void MXModel::exportOBJ(
const std::string &modelPath,
const std::string &textureManifestPath,
const std::string &textureBasePath,
const std::string &objPath,
float positionScale) {
839 model.load(modelPath, textureManifestPath, textureBasePath, positionScale);
840 model.exportOBJ(objPath);
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);
849 std::vector<Vec3> positions{};
850 std::vector<Vec2> texcoords{};
851 std::vector<Vec3> normals{};
853 verticesData.clear();
856 materialList.clear();
857 mtlLibraryPath.clear();
859 std::vector<VKVertex> currentVerts{};
860 std::string currentMaterialName{};
862 auto finalizeGroup = [&]() {
863 if (currentVerts.empty()) {
868 sm.firstIndex =
static_cast<uint32_t
>(indicesData.size());
869 sm.indexCount =
static_cast<uint32_t
>(currentVerts.size());
870 sm.materialName = currentMaterialName;
872 const uint32_t baseVertex =
static_cast<uint32_t
>(verticesData.size());
873 verticesData.insert(verticesData.end(), currentVerts.begin(), currentVerts.end());
875 for (uint32_t i = 0; i < sm.indexCount; ++i) {
876 indicesData.push_back(baseVertex + i);
879 subMeshList.push_back(sm);
880 currentVerts.clear();
884 while (std::getline(file, line)) {
890 std::istringstream stream(line);
898 if (stream >> x >> y >> z) {
899 positions.push_back({x * positionScale, y * positionScale, z * positionScale});
907 if (stream >> u >> v) {
908 texcoords.push_back({u, v});
917 if (stream >> x >> y >> z) {
918 normals.push_back({x, y, z});
923 if (tag ==
"mtllib") {
924 std::string mtlFile{};
925 std::getline(stream, 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();
933 mtlLibraryPath = (objPath.parent_path() / mtlPath).string();
938 if (tag ==
"g" || tag ==
"o") {
943 if (tag ==
"usemtl") {
944 std::string materialName{};
945 std::getline(stream, materialName);
947 if (!materialName.empty() && materialName != currentMaterialName) {
949 currentMaterialName = materialName;
958 std::vector<OBJFaceVertex> faceVerts{};
960 while (stream >> token) {
963 throw mxvk::Exception(
"MXModel::loadOBJ malformed face token '" + token +
"' in " + path);
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);
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;
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);
980 faceVertex.vertex.texCoord[0] = texcoords[
static_cast<size_t>(texcoordIndex)].x;
981 faceVertex.vertex.texCoord[1] = texcoords[
static_cast<size_t>(texcoordIndex)].y;
984 if (objIndex.normal != 0) {
986 if (normalIndex < 0 || normalIndex >=
static_cast<int>(normals.size())) {
987 throw mxvk::Exception(
"MXModel::loadOBJ face normal index out of range in " + path);
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;
995 faceVerts.push_back(faceVertex);
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);
1010 for (SubMesh &sm : subMeshList) {
1011 const auto it = materialIndices.find(sm.materialName);
1012 if (it != materialIndices.end()) {
1013 sm.textureIndex = it->second;
1018 if (verticesData.empty() || indicesData.empty()) {
1019 throw mxvk::Exception(
"MXModel::loadOBJ no geometry found in " + path);
1022 const bool hasNormals = !normals.empty();
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];
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);
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;
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);
1059 const MXMODParseResult parsed =
parseMXMODStream(file, path, positionScale);
1061 verticesData = parsed.vertices;
1062 indicesData = parsed.indices;
1063 subMeshList = parsed.subMeshes;
1064 materialList.clear();
1065 mtlLibraryPath.clear();
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);
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);
1082 std::istringstream stream(decompressedText);
1083 const MXMODParseResult parsed =
parseMXMODStream(stream, path, positionScale);
1085 verticesData = parsed.vertices;
1086 indicesData = parsed.indices;
1087 subMeshList = parsed.subMeshes;
1088 materialList.clear();
1089 mtlLibraryPath.clear();
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");
1098 if (verticesData.empty() || indicesData.empty()) {
1102 logMXModelStep(
"upload begin",
true);
1106 const VkDeviceSize vertexBufferSize =
sizeof(
VKVertex) * verticesData.size();
1107 const VkDeviceSize indexBufferSize =
sizeof(uint32_t) * indicesData.size();
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);
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);
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);
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);
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);
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);
1131 copyBuffer(device, commandPool, graphicsQueue, stagingVertexBuffer, vertexBufferHandle, vertexBufferSize);
1132 copyBuffer(device, commandPool, graphicsQueue, stagingIndexBuffer, indexBufferHandle, indexBufferSize);
1134 vkDestroyBuffer(device, stagingVertexBuffer,
nullptr);
1135 vkFreeMemory(device, stagingVertexMemory,
nullptr);
1136 vkDestroyBuffer(device, stagingIndexBuffer,
nullptr);
1137 vkFreeMemory(device, stagingIndexMemory,
nullptr);
1139 logMXModelStep(
"upload complete",
true);
1143 if (device == VK_NULL_HANDLE) {
1147 const bool hadBuffers = vertexBufferHandle != VK_NULL_HANDLE || indexBufferHandle != VK_NULL_HANDLE || vertexBufferMemory != VK_NULL_HANDLE || indexBufferMemory != VK_NULL_HANDLE;
1149 logMXModelStep(
"teardown begin",
true);
1152 if (vertexBufferHandle != VK_NULL_HANDLE) {
1153 vkDestroyBuffer(device, vertexBufferHandle,
nullptr);
1154 vertexBufferHandle = VK_NULL_HANDLE;
1156 if (vertexBufferMemory != VK_NULL_HANDLE) {
1157 vkFreeMemory(device, vertexBufferMemory,
nullptr);
1158 vertexBufferMemory = VK_NULL_HANDLE;
1161 if (indexBufferHandle != VK_NULL_HANDLE) {
1162 vkDestroyBuffer(device, indexBufferHandle,
nullptr);
1163 indexBufferHandle = VK_NULL_HANDLE;
1165 if (indexBufferMemory != VK_NULL_HANDLE) {
1166 vkFreeMemory(device, indexBufferMemory,
nullptr);
1167 indexBufferMemory = VK_NULL_HANDLE;
1171 logMXModelStep(
"teardown complete",
true);
1176 if (cmd == VK_NULL_HANDLE || vertexBufferHandle == VK_NULL_HANDLE || indexBufferHandle == VK_NULL_HANDLE || indicesData.empty()) {
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);
1188 if (cmd == VK_NULL_HANDLE || index >= subMeshList.size() || vertexBufferHandle == VK_NULL_HANDLE || indexBufferHandle == VK_NULL_HANDLE) {
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);
1197 const SubMesh &sm = subMeshList[index];
1205 void MXModel::createBuffer(VkDevice device, VkPhysicalDevice physicalDevice, VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer &buffer, VkDeviceMemory &bufferMemory) {
1207 throw mxvk::Exception(
"MXModel::createBuffer cannot allocate zero-sized buffer");
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;
1216 if (vkCreateBuffer(device, &bufferInfo,
nullptr, &buffer) != VK_SUCCESS) {
1217 throw mxvk::Exception(
"MXModel::createBuffer failed to create VkBuffer");
1220 VkMemoryRequirements memRequirements{};
1221 vkGetBufferMemoryRequirements(device, buffer, &memRequirements);
1223 VkMemoryAllocateInfo allocInfo{};
1224 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
1225 allocInfo.allocationSize = memRequirements.size;
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");
1233 if (vkBindBufferMemory(device, buffer, bufferMemory, 0) != VK_SUCCESS) {
1234 throw mxvk::Exception(
"MXModel::createBuffer failed to bind memory");
1237 if (bufferMemory != VK_NULL_HANDLE) {
1238 vkFreeMemory(device, bufferMemory,
nullptr);
1239 bufferMemory = VK_NULL_HANDLE;
1241 if (buffer != VK_NULL_HANDLE) {
1242 vkDestroyBuffer(device, buffer,
nullptr);
1243 buffer = VK_NULL_HANDLE;
1249 uint32_t MXModel::findMemoryType(VkPhysicalDevice physicalDevice, uint32_t typeFilter, VkMemoryPropertyFlags properties) {
1250 VkPhysicalDeviceMemoryProperties memProperties{};
1251 vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);
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) {
1261 throw mxvk::Exception(
"MXModel::findMemoryType failed to find suitable memory type");
1264 void MXModel::copyBuffer(VkDevice device, VkCommandPool commandPool, VkQueue graphicsQueue, VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) {
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;
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");
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");
1288 VkBufferCopy copyRegion{};
1289 copyRegion.size = size;
1290 vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, ©Region);
1292 if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) {
1293 vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
1294 throw mxvk::Exception(
"MXModel::copyBuffer failed to end command buffer");
1297 VkSubmitInfo submitInfo{};
1298 submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
1299 submitInfo.commandBufferCount = 1;
1300 submitInfo.pCommandBuffers = &commandBuffer;
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");
1307 if (vkQueueWaitIdle(graphicsQueue) != VK_SUCCESS) {
1308 vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
1309 throw mxvk::Exception(
"MXModel::copyBuffer failed to wait for queue idle");
1312 vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
1315 void MXModel::loadMTL(
const std::string &path) {
1316 std::ifstream file(path);
1317 if (!file.is_open()) {
1321 parseMTLStream(file, std::filesystem::path(path).parent_path().
string(), materialList);
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);
1330 std::vector<std::string> lines{};
1332 while (std::getline(file, line)) {
1337 lines.push_back(line);
1340 bool isStructured =
false;
1341 bool isMtlLike =
false;
1342 for (
const std::string &ln : lines) {
1343 std::istringstream stream(ln);
1346 if (tag ==
"submesh" || tag ==
"texture_dir" || tag ==
"material_lib" || tag ==
"model" || tag ==
"texture") {
1347 isStructured =
true;
1350 if (tag ==
"newmtl") {
1356 materialList.clear();
1358 std::string manifestTextureBase = textureBasePath.empty() ? std::filesystem::path(path).parent_path().string() : textureBasePath;
1361 std::istringstream stream{};
1363 for (
const std::string &ln : lines) {
1369 }
else if (isStructured) {
1370 for (
const std::string &ln : lines) {
1371 std::istringstream stream(ln);
1375 if (tag ==
"texture_dir") {
1377 std::getline(stream, dir);
1380 manifestTextureBase =
resolveManifestPath(std::filesystem::path(path).parent_path().
string(), dir);
1385 if (tag ==
"material_lib") {
1386 std::string materialPath{};
1387 std::getline(stream, materialPath);
1389 if (!materialPath.empty()) {
1390 loadMTL(
resolveManifestPath(std::filesystem::path(path).parent_path().
string(), materialPath));
1395 if (tag ==
"texture") {
1396 std::string image{};
1397 if (stream >> image) {
1398 MXMaterial material{};
1399 material.name =
"material_" + std::to_string(materialList.size());
1401 materialList.push_back(material);
1406 if (tag ==
"submesh") {
1407 uint32_t subMeshIndex = 0;
1408 if (!(stream >> subMeshIndex) || subMeshIndex >= subMeshList.size()) {
1412 std::string materialOrTexture{};
1413 if (!(stream >> materialOrTexture)) {
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);
1425 }
catch (
const std::exception &) {
1428 subMeshList[subMeshIndex].materialName = materialOrTexture;
1432 for (
const std::string &ln : lines) {
1433 MXMaterial material{};
1434 material.name =
"material_" + std::to_string(materialList.size());
1436 materialList.push_back(material);
1440 if (materialList.empty()) {
1444 for (SubMesh &sm : subMeshList) {
1445 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