Model optimization
This commit is contained in:
parent
be6a8cca9e
commit
d24748df1e
142
convert_model_to_binary.py
Normal file
142
convert_model_to_binary.py
Normal file
@ -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 <input.txt> [<output.bin>]
|
||||||
|
|
||||||
|
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('<I', 1))
|
||||||
|
out.write(struct.pack('<I', num_vertices))
|
||||||
|
out.write(struct.pack('<I', num_triangles))
|
||||||
|
|
||||||
|
for i in range(num_vertices):
|
||||||
|
out.write(struct.pack('<3f', *positions[i]))
|
||||||
|
out.write(struct.pack('<3f', *normals[i]))
|
||||||
|
out.write(struct.pack('<2f', *uvs[i]))
|
||||||
|
|
||||||
|
for tri in triangles:
|
||||||
|
out.write(struct.pack('<3I', *tri))
|
||||||
|
|
||||||
|
in_size = os.path.getsize(input_path)
|
||||||
|
out_size = os.path.getsize(output_path)
|
||||||
|
print(f"Converted: {input_path} ({in_size:,} bytes text) -> {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]} <input.txt> [<output.bin>]")
|
||||||
|
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)
|
||||||
@ -9,20 +9,20 @@
|
|||||||
"available": true,
|
"available": true,
|
||||||
"polygon": [
|
"polygon": [
|
||||||
[
|
[
|
||||||
-200,
|
-100,
|
||||||
200
|
100
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
200,
|
100,
|
||||||
200
|
100
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
200,
|
100,
|
||||||
-200
|
-100
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-200,
|
-100,
|
||||||
-200
|
-100
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,8 +10,22 @@
|
|||||||
namespace ZL
|
namespace ZL
|
||||||
{
|
{
|
||||||
|
|
||||||
|
static std::unordered_map<std::string, VertexDataStruct> 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)
|
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;
|
VertexDataStruct result;
|
||||||
std::istringstream f;
|
std::istringstream f;
|
||||||
|
|
||||||
@ -181,10 +195,81 @@ namespace ZL
|
|||||||
|
|
||||||
std::cout << "Model loaded: " << numberVertices << " verts, " << numberTriangles << " tris." << std::endl;
|
std::cout << "Model loaded: " << numberVertices << " verts, " << numberTriangles << " tris." << std::endl;
|
||||||
|
|
||||||
|
s_meshCache[key] = result;
|
||||||
return 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<char> 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<const uint32_t*>(ptr); ptr += 4;
|
||||||
|
if (version != 1)
|
||||||
|
throw std::runtime_error("Unsupported binary mesh version " + std::to_string(version) + ": " + fileName);
|
||||||
|
|
||||||
|
uint32_t numVertices = *reinterpret_cast<const uint32_t*>(ptr); ptr += 4;
|
||||||
|
uint32_t numTriangles = *reinterpret_cast<const uint32_t*>(ptr); ptr += 4;
|
||||||
|
|
||||||
|
const size_t expectedSize = 16
|
||||||
|
+ static_cast<size_t>(numVertices) * 8 * sizeof(float)
|
||||||
|
+ static_cast<size_t>(numTriangles) * 3 * sizeof(uint32_t);
|
||||||
|
if (fileData.size() < expectedSize)
|
||||||
|
throw std::runtime_error("Binary mesh file is truncated: " + fileName);
|
||||||
|
|
||||||
|
std::vector<Vector3f> positions(numVertices);
|
||||||
|
std::vector<Vector3f> normals(numVertices);
|
||||||
|
std::vector<Vector2f> uvs(numVertices);
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < numVertices; ++i)
|
||||||
|
{
|
||||||
|
positions[i](0) = *reinterpret_cast<const float*>(ptr); ptr += 4;
|
||||||
|
positions[i](1) = *reinterpret_cast<const float*>(ptr); ptr += 4;
|
||||||
|
positions[i](2) = *reinterpret_cast<const float*>(ptr); ptr += 4;
|
||||||
|
normals[i](0) = *reinterpret_cast<const float*>(ptr); ptr += 4;
|
||||||
|
normals[i](1) = *reinterpret_cast<const float*>(ptr); ptr += 4;
|
||||||
|
normals[i](2) = *reinterpret_cast<const float*>(ptr); ptr += 4;
|
||||||
|
uvs[i](0) = *reinterpret_cast<const float*>(ptr); ptr += 4;
|
||||||
|
uvs[i](1) = *reinterpret_cast<const float*>(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<const uint32_t*>(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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -7,4 +7,5 @@
|
|||||||
namespace ZL
|
namespace ZL
|
||||||
{
|
{
|
||||||
VertexDataStruct LoadFromTextFile02(const std::string& fileName, const std::string& ZIPFileName = "");
|
VertexDataStruct LoadFromTextFile02(const std::string& fileName, const std::string& ZIPFileName = "");
|
||||||
|
VertexDataStruct LoadModelFromBinFile(const std::string& fileName, const std::string& ZIPFileName = "");
|
||||||
}
|
}
|
||||||
@ -138,7 +138,10 @@ namespace ZL {
|
|||||||
|
|
||||||
// Load mesh
|
// Load mesh
|
||||||
try {
|
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) {
|
catch (const std::exception& e) {
|
||||||
std::cerr << "GameObjectLoader: Failed to load mesh for '" << objData.name << "': " << e.what() << std::endl;
|
std::cerr << "GameObjectLoader: Failed to load mesh for '" << objData.name << "': " << e.what() << std::endl;
|
||||||
@ -211,7 +214,10 @@ namespace ZL {
|
|||||||
|
|
||||||
// Load mesh
|
// Load mesh
|
||||||
try {
|
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) {
|
catch (const std::exception& e) {
|
||||||
std::cerr << "GameObjectLoader: Failed to load mesh for interactive '" << objData.name << "': " << e.what() << std::endl;
|
std::cerr << "GameObjectLoader: Failed to load mesh for interactive '" << objData.name << "': " << e.what() << std::endl;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user