VEX Engine
Retro looking game engine made in vulkan mostly because i wanted to understand it better.
Loading...
Searching...
No Matches
Mesh.cpp
1#include "components/Mesh.hpp"
2#include "components/ErrorUtils.hpp"
4
5#include <assimp/Importer.hpp>
6#include <assimp/scene.h>
7#include <assimp/postprocess.h>
8#include <assimp/version.h>
9#include <assimp/IOStream.hpp>
10#include <assimp/IOSystem.hpp>
11
12namespace vex {
13
14// Custom Assimp IO stream for VPK files
15class VPKAssimpStream : public Assimp::IOStream {
16private:
17 std::vector<char> m_buffer;
18 size_t m_position;
19
20public:
21 VPKAssimpStream(const char* data, size_t size)
22 : m_buffer(data, data + size), m_position(0) {}
23
24 ~VPKAssimpStream() override = default;
25
26 size_t Read(void* pvBuffer, size_t pSize, size_t pCount) override {
27 size_t bytesToRead = pSize * pCount;
28 size_t bytesAvailable = m_buffer.size() - m_position;
29
30 if (bytesToRead > bytesAvailable) {
31 bytesToRead = bytesAvailable;
32 }
33
34 if (bytesToRead > 0) {
35 memcpy(pvBuffer, m_buffer.data() + m_position, bytesToRead);
36 m_position += bytesToRead;
37 }
38
39 return bytesToRead / pSize; // Return number of items read
40 }
41
42 size_t Write(const void* pvBuffer, size_t pSize, size_t pCount) override {
43 return 0; // Read-only
44 }
45
46 aiReturn Seek(size_t pOffset, aiOrigin pOrigin) override {
47 size_t newPosition = m_position;
48
49 switch (pOrigin) {
50 case aiOrigin_SET: newPosition = pOffset; break;
51 case aiOrigin_CUR: newPosition = m_position + pOffset; break;
52 case aiOrigin_END: newPosition = m_buffer.size() + pOffset; break;
53 default: return aiReturn_FAILURE;
54 }
55
56 if (newPosition > m_buffer.size()) {
57 return aiReturn_FAILURE;
58 }
59
60 m_position = newPosition;
61 return aiReturn_SUCCESS;
62 }
63
64 size_t Tell() const override {
65 return m_position;
66 }
67
68 size_t FileSize() const override {
69 return m_buffer.size();
70 }
71
72 void Flush() override {}
73};
74
75// Custom Assimp IO system for VPK files
76class VPKAssimpIOSystem : public Assimp::IOSystem {
77private:
78 VirtualFileSystem* m_vfs;
79 std::string m_basePath;
80 std::string m_baseDir;
81
82public:
83 VPKAssimpIOSystem(VirtualFileSystem* vfs, const std::string& base_path)
84 : m_vfs(vfs), m_basePath(base_path) {
85 // Extract directory from base path
86 std::filesystem::path pathObj(base_path);
87 m_baseDir = pathObj.parent_path().string();
88 if (!m_baseDir.empty() && m_baseDir.back() != '/') {
89 m_baseDir += '/';
90 }
91 }
92
93 ~VPKAssimpIOSystem() override = default;
94
95 bool Exists(const char* pFile) const override {
96 std::string filePath(pFile);
97
98 log("Assimp checking if file exists: '%s'", filePath.c_str());
99
100 // First try the path as-is
101 if (m_vfs->file_exists(filePath)) {
102 log("File exists as-is: '%s'", filePath.c_str());
103 return true;
104 }
105
106 // Try relative to the base directory
107 std::string relativePath = m_baseDir + filePath;
108 if (m_vfs->file_exists(relativePath)) {
109 log("File exists relative to base: '%s'", relativePath.c_str());
110 return true;
111 }
112
113 // Try just the filename (for cases like "scene.bin")
114 std::filesystem::path pathObj(filePath);
115 std::string justFilename = pathObj.filename().string();
116 if (m_vfs->file_exists(justFilename)) {
117 log("File exists as filename only: '%s'", justFilename.c_str());
118 return true;
119 }
120
121 log(LogLevel::WARNING, "File not found: '%s'", filePath.c_str());
122 return false;
123 }
124
125 char getOsSeparator() const override {
126 return '/';
127 }
128
129 Assimp::IOStream* Open(const char* pFile, const char* pMode) override {
130 // Only support read mode
131 if (std::strstr(pMode, "r") == nullptr) {
132 return nullptr;
133 }
134
135 std::string filePath(pFile);
136 log("Assimp trying to open: '%s'", filePath.c_str());
137
138 std::string finalPath;
139
140 // Try different path resolutions in order:
141
142 // 1. Try as-is
143 if (m_vfs->file_exists(filePath)) {
144 finalPath = filePath;
145 log("Opening file as-is: '%s'", finalPath.c_str());
146 }
147 // 2. Try relative to base directory
148 else if (m_vfs->file_exists(m_baseDir + filePath)) {
149 finalPath = m_baseDir + filePath;
150 log("Opening file relative to base: '%s'", finalPath.c_str());
151 }
152 // 3. Try just the filename
153 else {
154 std::filesystem::path pathObj(filePath);
155 std::string justFilename = pathObj.filename().string();
156 if (m_vfs->file_exists(justFilename)) {
157 finalPath = justFilename;
158 log("Opening file as filename only: '%s'", finalPath.c_str());
159 } else {
160 log(LogLevel::ERROR, "Failed to find file: '%s'", filePath.c_str());
161 return nullptr;
162 }
163 }
164
165 // Read the file data into memory first
166 auto fileData = m_vfs->load_file(finalPath);
167 if (!fileData) {
168 log("Failed to load file data: '%s'", finalPath.c_str());
169 return nullptr;
170 }
171
172 log("Successfully loaded file: '%s' (%zu bytes)", finalPath.c_str(), fileData->size);
173
174 // Create stream from memory buffer
175 return new VPKAssimpStream(reinterpret_cast<const char*>(fileData->data.data()), fileData->size);
176 }
177
178 void Close(Assimp::IOStream* pFile) override {
179 delete pFile;
180 }
181};
182
183void MeshData::processScene(const aiScene* scene, const std::string& textureBaseDir) {
184 if (scene->mNumMeshes == 0) {
185 throw_error("Model contains no meshes");
186 }
187
188 submeshes.clear();
189 submeshes.resize(scene->mNumMeshes);
190
191 for (unsigned m = 0; m < scene->mNumMeshes; m++) {
192 log("Processing mesh %i...", m);
193 aiMesh* aiMesh = scene->mMeshes[m];
194 Submesh& submesh = submeshes[m];
195
196 submesh.vertices.resize(aiMesh->mNumVertices);
197 for (unsigned i = 0; i < aiMesh->mNumVertices; i++) {
198 submesh.vertices[i].position = {
199 aiMesh->mVertices[i].x,
200 aiMesh->mVertices[i].y,
201 aiMesh->mVertices[i].z
202 };
203
204 if(aiMesh->mNormals) {
205 submesh.vertices[i].normal = {
206 aiMesh->mNormals[i].x,
207 aiMesh->mNormals[i].y,
208 aiMesh->mNormals[i].z
209 };
210 }
211
212 if (aiMesh->mTextureCoords[0]) {
213 submesh.vertices[i].uv = {
214 aiMesh->mTextureCoords[0][i].x,
215 aiMesh->mTextureCoords[0][i].y
216 };
217 } else {
218 submesh.vertices[i].uv = glm::vec2(-100000.f);
219 }
220 }
221
222 submesh.indices.reserve(aiMesh->mNumFaces * 3);
223 for (unsigned i = 0; i < aiMesh->mNumFaces; i++) {
224 aiFace face = aiMesh->mFaces[i];
225 for (unsigned j = 0; j < face.mNumIndices; j++) {
226 submesh.indices.push_back(face.mIndices[j]);
227 }
228 }
229
230 if (aiMesh->mMaterialIndex >= 0) {
231 aiMaterial* material = scene->mMaterials[aiMesh->mMaterialIndex];
232 aiString texPath;
233 if (material->GetTexture(aiTextureType_DIFFUSE, 0, &texPath) == AI_SUCCESS) {
234 submesh.texturePath = textureBaseDir + texPath.C_Str();
235 }
236 }
237 }
238 }
239
240 void MeshData::loadFromRawFile(const std::string& relativePath) {
241 std::filesystem::path execDir = GetExecutableDir();
242 std::filesystem::path fullPath = execDir / relativePath;
243 std::string pathStr = fullPath.string();
244
245 log("Loading raw mesh from: %s", pathStr.c_str());
246
247 if (!std::filesystem::exists(fullPath)) {
248 handle_exception(std::runtime_error("Raw file does not exist: " + pathStr));
249 return;
250 }
251
252 Assimp::Importer importer;
253 const aiScene* scene = importer.ReadFile(pathStr,
254 aiProcess_Triangulate |
255 aiProcess_GenNormals |
256 aiProcess_FlipUVs);
257
258 if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) {
259 handle_exception(std::runtime_error("Assimp failed to load raw file: " + std::string(importer.GetErrorString())));
260 return;
261 }
262
263 std::filesystem::path meshFolder = fullPath;
264 meshFolder.remove_filename();
265
266 processScene(scene, meshFolder.string());
267
268 meshPath = pathStr;
269 }
270
271 void MeshData::loadFromFile(const std::string& path, VirtualFileSystem* vfs) {
272 log("Using Assimp version: %d.%d.%d", aiGetVersionMajor(), aiGetVersionMinor(), aiGetVersionRevision());
273 log("Creating assimp importer...");
274
275 auto importerPtr = std::make_unique<Assimp::Importer>();
276 Assimp::Importer& importer = *importerPtr;
277
278 std::string realPath = path;
279
280 #if NDEBUG
281 if (vfs) {
282 importer.SetIOHandler(new VPKAssimpIOSystem(vfs, realPath));
283 }
284 #endif
285
286 if (!vfs->file_exists(realPath)){
287 handle_exception(std::runtime_error("File: [" + realPath + "] doesnt exist"));
288 return;
289 }
290
291 const aiScene* scene = nullptr;
292
293 #if NDEBUG
294 auto fileData = vfs->load_file(realPath);
295 if (!fileData) throw_error("Failed to load file from VFS: " + realPath);
296
297 log("Loading from VFS memory buffer, size: %zu", fileData->size);
298
299 std::string extension = std::filesystem::path(realPath).extension().string();
300 scene = importer.ReadFileFromMemory(
301 fileData->data.data(),
302 fileData->size,
303 aiProcess_Triangulate | aiProcess_GenNormals | aiProcess_FlipUVs,
304 extension.c_str());
305 #else
306 scene = importer.ReadFile(realPath,
307 aiProcess_Triangulate | aiProcess_GenNormals | aiProcess_FlipUVs);
308 #endif
309
310 if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
311 handle_exception(std::runtime_error("Assimp failed to load file: " + std::string(importer.GetErrorString())));
312 return;
313 }
314
315 std::filesystem::path meshFolderPath(realPath);
316 meshFolderPath.remove_filename();
317
318 processScene(scene, meshFolderPath.string());
319
320 if (vfs) importer.SetIOHandler(nullptr);
321 meshPath = realPath;
322 }
323}
This file defines MeshData struct and all other structs needed for it.
This file defines VirtualFileSystem and VPKStream classes.
This class provides abstraction of file system needed for loading packed and unpacked assets.
std::unique_ptr< FileData > load_file(const std::string &virtual_path)
Loads a file into memory from the specified path.
bool file_exists(const std::string &virtual_path)
Checks if a file exists in the currently active file system mode.
std::filesystem::path VEX_EXPORT GetExecutableDir()
Retrieves the directory containing the current executable.
Definition PathUtils.cpp:37
void VEX_EXPORT throw_error(const std::string &msg)
Throws an error or terminates the application.
void VEX_EXPORT log(const char *fmt,...)
Logs a formatted message.
void VEX_EXPORT handle_exception(const std::exception &e)
Handles an exception based on build configuration.
void loadFromFile(const std::string &path, VirtualFileSystem *vfs)
Loads mesh data from a file using the Virtual File System.
Definition Mesh.cpp:271
void processScene(const aiScene *scene, const std::string &textureBaseDir)
Internal helper to convert an Assimp aiScene into VEX Submesh structures.
Definition Mesh.cpp:183
void loadFromRawFile(const std::string &relativePath)
Loads a mesh directly from the physical disk, bypassing the VFS.
Definition Mesh.cpp:240
Submesh structure for mesh data, its made like this cause some file formats hold multiple meshes in o...
Definition Mesh.hpp:33