15 static bool ends_with(
const std::string &s,
const std::string &suf) {
16 return s.size() >= suf.size() && s.compare(s.size() - suf.size(), suf.size(), suf) == 0;
21 std::vector<VKVertex> unique;
22 std::unordered_map<VKVertex, uint32_t, VKVertexHash> map;
23 std::vector<uint32_t> newIdx;
27 auto it = map.find(v);
28 if (it != map.end()) {
29 newIdx.push_back(it->second);
31 uint32_t idx =
static_cast<uint32_t
>(unique.size());
34 newIdx.push_back(idx);
41 std::cout <<
">> MXModel::compressIndices: " << before <<
" verts -> "
54 std::ifstream f(path);
56 throw mx::Exception(
"MXModel::load: failed to open file: " + path);
58 std::vector<Vec3> positions;
59 std::vector<Vec2> texcoords;
60 std::vector<Vec3> normals;
67 std::vector<VKVertex> currentVerts;
68 uint32_t currentTexIdx = 0;
69 std::string currentMtlName;
73 auto finalizeGroup = [&]() {
74 if (currentVerts.empty())
return;
77 sm.
indexCount =
static_cast<uint32_t
>(currentVerts.size());
80 for (
auto &v : currentVerts)
87 while (std::getline(f, line)) {
88 if (line.empty() || line[0] ==
'#')
continue;
90 std::istringstream s(line);
97 positions.push_back({x * positionScale, y * positionScale, z * positionScale});
98 }
else if (tag ==
"vt") {
101 texcoords.push_back({u, v});
102 }
else if (tag ==
"vn") {
104 if (s >> x >> y >> z)
105 normals.push_back({x, y, z});
106 }
else if (tag ==
"mtllib") {
109 std::filesystem::path objDir = std::filesystem::path(path).parent_path();
112 }
else if (tag ==
"g" || tag ==
"o" || tag ==
"usemtl") {
114 currentTexIdx =
static_cast<uint32_t
>(
subMeshes_.size());
115 if (tag ==
"usemtl") {
118 }
else if (tag ==
"f") {
119 std::vector<VKVertex> faceVerts;
122 int vi = 0, ti = 0, ni = 0;
123 if (sscanf(token.c_str(),
"%d/%d/%d", &vi, &ti, &ni) == 3) {
124 }
else if (sscanf(token.c_str(),
"%d//%d", &vi, &ni) == 2) {
126 }
else if (sscanf(token.c_str(),
"%d/%d", &vi, &ti) == 2) {
129 sscanf(token.c_str(),
"%d", &vi);
135 int idx = vi > 0 ? vi - 1 :
static_cast<int>(positions.size()) + vi;
136 if (idx >= 0 && idx <
static_cast<int>(positions.size())) {
137 vtx.
pos[0] = positions[idx].x;
138 vtx.
pos[1] = positions[idx].y;
139 vtx.
pos[2] = positions[idx].z;
143 int idx = ti > 0 ? ti - 1 :
static_cast<int>(texcoords.size()) + ti;
144 if (idx >= 0 && idx <
static_cast<int>(texcoords.size())) {
150 int idx = ni > 0 ? ni - 1 :
static_cast<int>(normals.size()) + ni;
151 if (idx >= 0 && idx <
static_cast<int>(normals.size())) {
152 vtx.
normal[0] = normals[idx].x;
153 vtx.
normal[1] = normals[idx].y;
154 vtx.
normal[2] = normals[idx].z;
157 faceVerts.push_back(vtx);
159 for (
size_t i = 2; i < faceVerts.size(); ++i) {
160 currentVerts.push_back(faceVerts[0]);
161 currentVerts.push_back(faceVerts[i - 1]);
162 currentVerts.push_back(faceVerts[i]);
173 throw mx::Exception(
"MXModel::load: no vertices found in " + path);
185 bool hasNormals = !normals.empty();
187 for (
size_t i = 0; i + 2 <
vertices_.size(); i += 3) {
194 float nx = ay * bz - az * by;
195 float ny = az * bx - ax * bz;
196 float nz = ax * by - ay * bx;
197 float len = std::sqrt(nx*nx + ny*ny + nz*nz);
198 if (len > 0.0f) { nx /= len; ny /= len; nz /= len; }
199 for (
int j = 0; j < 3; ++j) {
209 for (uint32_t i = 0; i < static_cast<uint32_t>(
vertices_.size()); ++i)
213 std::cout <<
">> MXModel::load (OBJ): " << std::filesystem::path(path).filename().string()
214 <<
" - " <<
subMeshes_.size() <<
" sub-mesh(es) [OK]\n";
222 throw mx::Exception(
"MXModel::load: failed to read file: " + path);
225 std::ifstream f(path);
227 throw mx::Exception(
"MXModel::load: failed to open file: " + path);
228 std::ostringstream ss;
233 if (text.size() >= 3 &&
234 (
unsigned char)text[0] == 0xEF &&
235 (
unsigned char)text[1] == 0xBB &&
236 (
unsigned char)text[2] == 0xBF) {
240 std::istringstream file(text);
241 auto trim = [](std::string &s) {
242 size_t b = s.find_first_not_of(
" \t\r\n");
243 if (b == std::string::npos) { s.clear();
return; }
244 size_t e = s.find_last_not_of(
" \t\r\n");
245 s = s.substr(b, e - b + 1);
248 std::vector<Vec3> pos;
249 std::vector<Vec2> uv;
250 std::vector<Vec3> nrm;
251 std::vector<uint32_t> fileIndices;
257 while (std::getline(file, line)) {
258 if (line.empty())
continue;
259 size_t commentPos = line.find(
'#');
260 if (commentPos != std::string::npos) line = line.substr(0, commentPos);
262 if (line.empty())
continue;
265 if (line.size() >= 3 &&
266 (
unsigned char)line[0] == 0xEF &&
267 (
unsigned char)line[1] == 0xBB &&
268 (
unsigned char)line[2] == 0xBF) {
271 if (line.empty())
continue;
274 std::istringstream s(line);
277 char c = line[line.find_first_not_of(
" \t")];
278 bool isData = (c >=
'0' && c <=
'9') || c ==
'-' || c ==
'+' || c ==
'.';
281 float x = 0, y = 0, z = 0;
284 if (s >> x >> y >> z)
285 pos.push_back({x * positionScale, y * positionScale, z * positionScale});
289 uv.push_back({x, y});
292 if (s >> x >> y >> z)
293 nrm.push_back({x, y, z});
297 while (s >> idx) fileIndices.push_back(idx);
307 if (tag.empty())
continue;
310 uint32_t st = 0, ti = 0;
316 if (tag ==
"vert") { s >> count; type = 0;
continue; }
317 if (tag ==
"tex") { s >> count; type = 1;
continue; }
318 if (tag ==
"norm") { s >> count; type = 2;
continue; }
319 if (tag ==
"indices") { s >> count; type = 5;
continue; }
323 throw mx::Exception(
"MXModel::load: no vertices found in " + path);
326 int vcount =
static_cast<int>(pos.size());
330 for (
int i = 0; i < vcount; ++i) {
335 if (i < (
int)uv.size()) {
339 if (i < (
int)nrm.size()) {
347 if (!fileIndices.empty()) {
349 std::cout <<
">> MXModel: " << vcount <<
" verts, " <<
indices_.size() <<
" indices from file\n";
353 for (uint32_t i = 0; i < static_cast<uint32_t>(vcount); ++i)
366 std::cout <<
">> MXModel::load: " << std::filesystem::path(path).filename().string() <<
" - [OK]\n";
370 VkCommandPool commandPool, VkQueue graphicsQueue) {
371 std::cout <<
">> MXModel::upload: uploading " <<
vertices_.size()
372 <<
" vertices, " <<
indices_.size() <<
" indices to GPU...\n";
379 VkBuffer stagingVertexBuffer;
380 VkDeviceMemory stagingVertexBufferMemory;
382 VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
383 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
384 stagingVertexBuffer, stagingVertexBufferMemory);
387 VkBuffer stagingIndexBuffer;
388 VkDeviceMemory stagingIndexBufferMemory;
390 VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
391 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
392 stagingIndexBuffer, stagingIndexBufferMemory);
395 void *vertexData =
nullptr;
396 vkMapMemory(device, stagingVertexBufferMemory, 0, vertexBufferSize, 0, &vertexData);
397 memcpy(vertexData,
vertices_.data(),
static_cast<size_t>(vertexBufferSize));
398 vkUnmapMemory(device, stagingVertexBufferMemory);
401 void *indexData =
nullptr;
402 vkMapMemory(device, stagingIndexBufferMemory, 0, indexBufferSize, 0, &indexData);
403 memcpy(indexData,
indices_.data(),
static_cast<size_t>(indexBufferSize));
404 vkUnmapMemory(device, stagingIndexBufferMemory);
408 VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
409 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
414 VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
415 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
423 vkDestroyBuffer(device, stagingVertexBuffer,
nullptr);
424 vkFreeMemory(device, stagingVertexBufferMemory,
nullptr);
425 vkDestroyBuffer(device, stagingIndexBuffer,
nullptr);
426 vkFreeMemory(device, stagingIndexBufferMemory,
nullptr);
427 std::cout <<
">> MXModel::upload: GPU buffers created, staging buffers released [OK]\n";
432 std::cout <<
">> MXModel::cleanup: destroying vertex and index buffers\n";
448 std::cout <<
">> MXModel::draw: skipped (no buffers)\n";
453 VkDeviceSize offsets[] = { 0 };
454 vkCmdBindVertexBuffers(cmd, 0, 1, buffers, offsets);
455 vkCmdBindIndexBuffer(cmd,
indexBuffer_, 0, VK_INDEX_TYPE_UINT32);
456 vkCmdDrawIndexed(cmd,
indexCount(), 1, 0, 0, 0);
461 std::cout <<
">> MXModel::drawSubMesh: skipped (no buffers)\n";
465 std::cout <<
">> MXModel::drawSubMesh: index " << index <<
" out of range (" <<
subMeshes_.size() <<
" sub-meshes)\n";
470 VkDeviceSize offsets[] = { 0 };
471 vkCmdBindVertexBuffers(cmd, 0, 1, buffers, offsets);
472 vkCmdBindIndexBuffer(cmd,
indexBuffer_, 0, VK_INDEX_TYPE_UINT32);
475 vkCmdDrawIndexed(cmd, sm.indexCount, 1, sm.firstIndex, 0, 0);
479 VkDeviceSize size, VkBufferUsageFlags usage,
480 VkMemoryPropertyFlags properties,
481 VkBuffer &buffer, VkDeviceMemory &bufferMemory) {
482 VkBufferCreateInfo bufferInfo{};
483 bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
484 bufferInfo.size = size;
485 bufferInfo.usage = usage;
486 bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
488 if (vkCreateBuffer(device, &bufferInfo,
nullptr, &buffer) != VK_SUCCESS)
490 std::cout <<
">> MXModel::createBuffer: allocated " << size <<
" bytes\n";
492 VkMemoryRequirements memRequirements;
493 vkGetBufferMemoryRequirements(device, buffer, &memRequirements);
495 VkMemoryAllocateInfo allocInfo{};
496 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
497 allocInfo.allocationSize = memRequirements.size;
498 allocInfo.memoryTypeIndex =
findMemoryType(physicalDevice, memRequirements.memoryTypeBits, properties);
500 if (vkAllocateMemory(device, &allocInfo,
nullptr, &bufferMemory) != VK_SUCCESS)
501 throw mx::Exception(
"MXModel: Failed to allocate buffer memory!");
503 vkBindBufferMemory(device, buffer, bufferMemory, 0);
507 uint32_t typeFilter, VkMemoryPropertyFlags properties) {
508 VkPhysicalDeviceMemoryProperties memProperties;
509 vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);
510 for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) {
511 if ((typeFilter & (1 << i)) &&
512 (memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
516 throw mx::Exception(
"MXModel: Failed to find suitable memory type!");
520 VkQueue graphicsQueue,
521 VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) {
522 std::cout <<
">> MXModel::copyBuffer: copying " << size <<
" bytes to device-local memory\n";
523 VkCommandBufferAllocateInfo allocInfo{};
524 allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
525 allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
526 allocInfo.commandPool = commandPool;
527 allocInfo.commandBufferCount = 1;
529 VkCommandBuffer commandBuffer;
530 vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer);
532 VkCommandBufferBeginInfo beginInfo{};
533 beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
534 beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
535 vkBeginCommandBuffer(commandBuffer, &beginInfo);
537 VkBufferCopy copyRegion{};
538 copyRegion.size = size;
539 vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, ©Region);
541 vkEndCommandBuffer(commandBuffer);
543 VkSubmitInfo submitInfo{};
544 submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
545 submitInfo.commandBufferCount = 1;
546 submitInfo.pCommandBuffers = &commandBuffer;
548 vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);
549 vkQueueWaitIdle(graphicsQueue);
551 vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
555 std::ifstream f(path);
557 std::cerr <<
">> MXModel::loadMTL: warning - cannot open: " << path <<
"\n";
563 while (std::getline(f, line)) {
564 if (line.empty() || line[0] ==
'#')
continue;
565 std::istringstream s(line);
569 if (tag ==
"newmtl") {
573 }
else if (!current) {
575 }
else if (tag ==
"Ka") {
576 s >> current->
ka[0] >> current->
ka[1] >> current->
ka[2];
577 }
else if (tag ==
"Kd") {
578 s >> current->
kd[0] >> current->
kd[1] >> current->
kd[2];
579 }
else if (tag ==
"Ks") {
580 s >> current->
ks[0] >> current->
ks[1] >> current->
ks[2];
581 }
else if (tag ==
"Ns") {
583 }
else if (tag ==
"d") {
585 }
else if (tag ==
"illum") {
587 }
else if (tag ==
"map_Kd") {
591 std::cout <<
">> MXModel::loadMTL: loaded " <<
materials_.size()
592 <<
" material(s) from " << path <<
"\n";
General-purpose exception class for the mx library.
std::string mtlLibPath_
Path to .mtl file from OBJ mtllib.
uint32_t indexCount() const
static void createBuffer(VkDevice device, VkPhysicalDevice physicalDevice, VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer &buffer, VkDeviceMemory &bufferMemory)
Allocate a Vulkan buffer with the specified usage and memory flags.
void compressIndices()
Deduplicate vertices and rebuild the index list.
std::vector< uint32_t > indices_
Parsed index data.
std::vector< SubMesh > subMeshes_
Sub-mesh ranges (one per group).
VkDeviceMemory indexBufferMemory_
void loadOBJ(const std::string &path, float positionScale)
Load a Wavefront OBJ file.
std::vector< VKVertex > vertices_
Parsed vertex data.
void drawSubMesh(VkCommandBuffer cmd, size_t index) const
Draw a single sub-mesh (binds vertex/index buffers and issues indexed draw).
static uint32_t findMemoryType(VkPhysicalDevice physicalDevice, uint32_t typeFilter, VkMemoryPropertyFlags properties)
Find a memory type index satisfying typeFilter and properties.
void loadMXMOD(const std::string &path, float positionScale)
Load an MXMOD format file.
void upload(VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue)
Upload parsed geometry to device-local Vulkan buffers.
void draw(VkCommandBuffer cmd) const
Record a bind + indexed draw into a command buffer.
uint32_t textureIndex_
Sub-mesh texture index.
std::vector< MXMaterial > materials_
Parsed MTL materials.
void cleanup(VkDevice device)
Free all Vulkan buffer resources.
void load(const std::string &path, float positionScale=1.0f)
Parse a model file (.mxmod, .mxmod.z, or .obj) and populate the vertex/index vectors.
VkDeviceMemory vertexBufferMemory_
void loadMTL(const std::string &path)
Parse a Wavefront .mtl file and populate materials_.
static void copyBuffer(VkDevice device, VkCommandPool commandPool, VkQueue graphicsQueue, VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size)
Copy bytes from one Vulkan buffer to another via a staging command.
std::string decompressString(void *data, uLong size_)
Decompress a zlib-compressed data block.
std::vector< char > readFile(const std::string &filename)
Read an entire file into a byte vector.
static bool ends_with(const std::string &s, const std::string &suf)
Parsed Wavefront MTL material properties.
float ks[3]
Specular colour.
int illum
Illumination model.
float ka[3]
Ambient colour.
float ns
Specular exponent.
std::string map_kd
Diffuse texture filename.
float d
Opacity (dissolve).
float kd[3]
Diffuse colour.
std::string name
Material name (newmtl).
A range of indices within the shared index buffer, with its own texture.
std::string materialName
Material name from OBJ usemtl directive.
uint32_t textureIndex
Texture slot for this sub-mesh.
uint32_t indexCount
Number of indices in this sub-mesh.
uint32_t firstIndex
Offset into the index buffer.
Per-vertex data uploaded to the Vulkan vertex buffer.
float normal[3]
Surface normal vector.
float texCoord[2]
UV texture coordinates.
float pos[3]
3-D position (x, y, z).
Vulkan 3-D model loader and GPU buffer manager.