diff --git a/convert_model_to_binary.py b/convert_model_to_binary.py new file mode 100644 index 0000000..41f946d --- /dev/null +++ b/convert_model_to_binary.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +""" +Convert a text-based static mesh file (.txt) to binary format (.txt.bin). + +Usage: + python convert_model_to_binary.py [] + +If the output path is not given it is derived by appending ".bin" to the input path, +e.g. resources/w/firebox.txt -> resources/w/firebox.txt.bin + +Binary format (BSMF v1) -- all values little-endian: + + HEADER + 4 bytes magic "BSMF" + uint32 version (1) + uint32 numVertices + uint32 numTriangles + + VERTICES (numVertices entries): + 3 x float position x, y, z -- engine coordinate space + 3 x float normal x, y, z -- engine coordinate space + 2 x float UV u, v + + TRIANGLES (numTriangles entries): + 3 x uint32 vertex indices i0, i1, i2 + +The same Blender->engine axis swap applied by LoadFromTextFile02 is baked in here, +so the C++ binary loader can read coordinates directly without any post-processing: + engine_x = blender_y + engine_y = blender_z + engine_z = blender_x +""" + +import struct +import re +import sys +import os + + +def _parse_floats(text): + return [float(x) for x in re.findall(r'[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?', text)] + + +def _parse_ints(text): + return [int(x) for x in re.findall(r'[-]?\d+', text)] + + +def _swap_axes(x, y, z): + """Blender Y-up -> engine coordinate system (mirrors LoadFromTextFile02).""" + return y, z, x + + +def convert(input_path, output_path): + with open(input_path, 'r', encoding='utf-8') as f: + lines = [line.rstrip('\n') for line in f] + + idx = 0 + + def next_line(): + nonlocal idx + while idx < len(lines): + line = lines[idx] + idx += 1 + return line + raise EOFError("Unexpected end of file while parsing: " + input_path) + + # --- Vertices --- + while True: + line = next_line() + if '===Vertices' in line: + break + + m = re.search(r'\d+', line) + if not m: + raise ValueError("Could not parse vertex count from: " + line) + num_vertices = int(m.group()) + + positions = [] + normals = [] + uvs = [] + + for i in range(num_vertices): + line = next_line() + # V N: Pos(x, y, z) Norm(nx, ny, nz) UV(u, v) + nums = _parse_floats(line) + # nums[0] = vertex index (float-parsed), then 3 pos, 3 norm, 2 uv + if len(nums) < 9: + raise ValueError(f"Malformed vertex line {i}: {line}") + px, py, pz = _swap_axes(nums[1], nums[2], nums[3]) + nx, ny, nz = _swap_axes(nums[4], nums[5], nums[6]) + positions.append((px, py, pz)) + normals.append((nx, ny, nz)) + uvs.append((nums[7], nums[8])) + + # --- Triangles --- + while True: + line = next_line() + if '===Triangles' in line: + break + + m = re.search(r'\d+', line) + if not m: + raise ValueError("Could not parse triangle count from: " + line) + num_triangles = int(m.group()) + + triangles = [] + for i in range(num_triangles): + line = next_line() + ints = _parse_ints(line) + if len(ints) != 3: + raise ValueError(f"Malformed triangle line {i}: {line}") + triangles.append(tuple(ints)) + + # --- Write binary --- + with open(output_path, 'wb') as out: + out.write(b'BSMF') + out.write(struct.pack(' {output_path} ({out_size:,} bytes binary)") + print(f" Vertices: {num_vertices}, Triangles: {num_triangles}") + + +if __name__ == '__main__': + if len(sys.argv) < 2 or len(sys.argv) > 3: + print(f"Usage: {sys.argv[0]} []") + sys.exit(1) + + input_path = sys.argv[1] + output_path = sys.argv[2] if len(sys.argv) == 3 else input_path + '.bin' + convert(input_path, output_path) diff --git a/resources/config2/navigation2.json b/resources/config2/navigation2.json index d0cefc6..19bae36 100644 --- a/resources/config2/navigation2.json +++ b/resources/config2/navigation2.json @@ -9,20 +9,20 @@ "available": true, "polygon": [ [ - -200, - 200 + -100, + 100 ], [ - 200, - 200 + 100, + 100 ], [ - 200, - -200 + 100, + -100 ], [ - -200, - -200 + -100, + -100 ] ] } diff --git a/src/TextModel.cpp b/src/TextModel.cpp index eb9becd..90e847b 100644 --- a/src/TextModel.cpp +++ b/src/TextModel.cpp @@ -10,8 +10,22 @@ namespace ZL { + static std::unordered_map s_meshCache; + + static std::string CacheKey(const std::string& fileName) + { + if (fileName.size() > 4 && fileName.compare(fileName.size() - 4, 4, ".bin") == 0) + return fileName.substr(0, fileName.size() - 4); + return fileName; + } + VertexDataStruct LoadFromTextFile02(const std::string& fileName, const std::string& ZIPFileName) { + std::string key = CacheKey(fileName); + auto it = s_meshCache.find(key); + if (it != s_meshCache.end()) + return it->second; + VertexDataStruct result; std::istringstream f; @@ -181,10 +195,81 @@ namespace ZL std::cout << "Model loaded: " << numberVertices << " verts, " << numberTriangles << " tris." << std::endl; + s_meshCache[key] = result; return result; } + VertexDataStruct LoadModelFromBinFile(const std::string& fileName, const std::string& ZIPFileName) + { + std::string key = CacheKey(fileName); + auto it = s_meshCache.find(key); + if (it != s_meshCache.end()) + return it->second; + std::vector fileData = !ZIPFileName.empty() + ? readFileFromZIP(fileName, ZIPFileName) + : readFile(fileName); + if (fileData.size() < 16) + throw std::runtime_error("Binary mesh file is too short: " + fileName); + + const char* ptr = fileData.data(); + + if (ptr[0] != 'B' || ptr[1] != 'S' || ptr[2] != 'M' || ptr[3] != 'F') + throw std::runtime_error("Invalid magic bytes in binary mesh file: " + fileName); + ptr += 4; + + uint32_t version = *reinterpret_cast(ptr); ptr += 4; + if (version != 1) + throw std::runtime_error("Unsupported binary mesh version " + std::to_string(version) + ": " + fileName); + + uint32_t numVertices = *reinterpret_cast(ptr); ptr += 4; + uint32_t numTriangles = *reinterpret_cast(ptr); ptr += 4; + + const size_t expectedSize = 16 + + static_cast(numVertices) * 8 * sizeof(float) + + static_cast(numTriangles) * 3 * sizeof(uint32_t); + if (fileData.size() < expectedSize) + throw std::runtime_error("Binary mesh file is truncated: " + fileName); + + std::vector positions(numVertices); + std::vector normals(numVertices); + std::vector uvs(numVertices); + + for (uint32_t i = 0; i < numVertices; ++i) + { + positions[i](0) = *reinterpret_cast(ptr); ptr += 4; + positions[i](1) = *reinterpret_cast(ptr); ptr += 4; + positions[i](2) = *reinterpret_cast(ptr); ptr += 4; + normals[i](0) = *reinterpret_cast(ptr); ptr += 4; + normals[i](1) = *reinterpret_cast(ptr); ptr += 4; + normals[i](2) = *reinterpret_cast(ptr); ptr += 4; + uvs[i](0) = *reinterpret_cast(ptr); ptr += 4; + uvs[i](1) = *reinterpret_cast(ptr); ptr += 4; + } + + VertexDataStruct result; + result.PositionData.reserve(numTriangles * 3); + result.NormalData.reserve(numTriangles * 3); + result.TexCoordData.reserve(numTriangles * 3); + + for (uint32_t i = 0; i < numTriangles; ++i) + { + for (int k = 0; k < 3; ++k) + { + uint32_t idx = *reinterpret_cast(ptr); ptr += 4; + if (idx >= numVertices) + throw std::runtime_error("Triangle index out of range in: " + fileName); + result.PositionData.push_back(positions[idx]); + result.NormalData.push_back(normals[idx]); + result.TexCoordData.push_back(uvs[idx]); + } + } + + std::cout << "Binary model loaded: " << numVertices << " verts, " << numTriangles << " tris." << std::endl; + + s_meshCache[key] = result; + return result; + } } \ No newline at end of file diff --git a/src/TextModel.h b/src/TextModel.h index 41b88d0..f1236b5 100644 --- a/src/TextModel.h +++ b/src/TextModel.h @@ -7,4 +7,5 @@ namespace ZL { VertexDataStruct LoadFromTextFile02(const std::string& fileName, const std::string& ZIPFileName = ""); + VertexDataStruct LoadModelFromBinFile(const std::string& fileName, const std::string& ZIPFileName = ""); } \ No newline at end of file diff --git a/src/items/GameObjectLoader.cpp b/src/items/GameObjectLoader.cpp index b837166..3de1903 100644 --- a/src/items/GameObjectLoader.cpp +++ b/src/items/GameObjectLoader.cpp @@ -138,7 +138,10 @@ namespace ZL { // Load mesh try { - gameObj.mesh.data = LoadFromTextFile02(objData.meshPath, zipPath); + if (objData.meshPath.size() > 4 && objData.meshPath.compare(objData.meshPath.size() - 4, 4, ".bin") == 0) + gameObj.mesh.data = LoadModelFromBinFile(objData.meshPath, zipPath); + else + gameObj.mesh.data = LoadFromTextFile02(objData.meshPath, zipPath); } catch (const std::exception& e) { std::cerr << "GameObjectLoader: Failed to load mesh for '" << objData.name << "': " << e.what() << std::endl; @@ -211,7 +214,10 @@ namespace ZL { // Load mesh try { - intObj.mesh.data = LoadFromTextFile02(objData.meshPath, zipPath); + if (objData.meshPath.size() > 4 && objData.meshPath.compare(objData.meshPath.size() - 4, 4, ".bin") == 0) + intObj.mesh.data = LoadModelFromBinFile(objData.meshPath, zipPath); + else + intObj.mesh.data = LoadFromTextFile02(objData.meshPath, zipPath); } catch (const std::exception& e) { std::cerr << "GameObjectLoader: Failed to load mesh for interactive '" << objData.name << "': " << e.what() << std::endl;