Skip to content

Instantly share code, notes, and snippets.

@Puyodead1
Created June 19, 2024 18:51
Show Gist options
  • Save Puyodead1/c2fe371fa980ea48ab7fcfbf560bf675 to your computer and use it in GitHub Desktop.
Save Puyodead1/c2fe371fa980ea48ab7fcfbf560bf675 to your computer and use it in GitHub Desktop.
QTQuick3D QSSG Mesh Converter

converts .mesh files created from QTQuick3D back to OBJ files.
This was created while reverse engineering a game that uses QT and has assets embedded

This script is very shitty, it works so its good enough

import argparse
import itertools
import math
import struct
from enum import Enum
from io import BytesIO
from os import path
from pathlib import Path
from typing import List
from binreader import BinaryReader
class Vector3(object):
x: float
y: float
z: float
def __init__(self, x, y, z) -> None:
self.x = x
self.y = y
self.z = z
def __str__(self) -> str:
return f"x={self.x}, y={self.y}, z={self.z}"
class Vector2(object):
x: float
y: float
def __init__(self, x, y) -> None:
self.x = x
self.y = y
def __str__(self) -> str:
return f"x={self.x}, y={self.y}"
class ComponentType(Enum):
UINT8 = 1
INT8 = 2
UINT16 = 3
INT16 = 4
UINT32 = 5
INT32 = 6
UINT64 = 7
INT64 = 8
FLOAT16 = 9
FLOAT32 = 10
FLOAT64 = 11
COMPONENT_TYPE_SIZES = {
ComponentType.UINT8: 1,
ComponentType.INT8: 1,
ComponentType.UINT16: 2,
ComponentType.INT16: 2,
ComponentType.UINT32: 4,
ComponentType.INT32: 4,
ComponentType.UINT64: 8,
ComponentType.INT64: 8,
ComponentType.FLOAT16: 2,
ComponentType.FLOAT32: 4,
ComponentType.FLOAT64: 8,
}
class DrawMode(Enum):
POINTS = 1
LINE_STRIP = 2
LINE_LOOP = 3
LINE = 4
TRIANGLE_STRIP = 5
TRIANGLE_FAN = 6
TRIANGLES = 7
class Winding(Enum):
CW = 1
CCW = 2
class VertexBufferEntry(object):
component_type = ComponentType.FLOAT32
component_count = 0
offset = 0
name = ""
class Size(object):
width: int
height: int
def __init__(self, width: int, height: int) -> None:
self.width = width
self.height = height
def __str__(self) -> str:
return f"width: {self.width}, height: {self.height}"
class SubsetBounds(object):
_min: Vector3
_max: Vector3
def __str__(self) -> str:
return f"min: {self.min}, max: {self.max}"
class Lod(object):
count = 0
offset = 0
distance = 0.0
class Subset(object):
raw_name_utf16 = b""
name_length = 0
bounds = SubsetBounds()
offset = 0
count = 0
lightmap_size_hint: Size
lod_count = 0
lods: List[Lod] = []
def __str__(self) -> str:
return f"Name: {self.raw_name_utf16}, Bounds: {self.bounds}, Offset: {self.offset}, Count: {self.count}, Lightmap size hint: {self.lightmap_size_hint}, Lod count: {self.lod_count}"
class MeshOffsetTracker(object):
start_offset = 0
byte_counter = 0
def __init__(self, offset: int) -> None:
self.start_offset = offset
def offset(self) -> int:
return self.start_offset + self.byte_counter
def align_advance(self, advance_amount: int) -> int:
self.advance(advance_amount)
alignment_amount = 4 - (self.byte_counter % 4)
self.byte_counter += alignment_amount
return alignment_amount
def advance(self, advance_amount: int) -> None:
self.byte_counter += advance_amount
def assert_component_type(component_type: int):
assert component_type in ComponentType._value2member_map_, "Invalid component type"
return ComponentType(component_type)
def assert_draw_mode(draw_mode: int):
assert draw_mode in DrawMode._value2member_map_, "Invalid draw mode"
return DrawMode(draw_mode)
def assert_winding(winding: int):
assert winding in Winding._value2member_map_, "Invalid winding"
return Winding(winding)
class Mesh(object):
version = 0
flags = 0
size = 0
draw_mode: DrawMode
winding: Winding
vertex_buffer: List[VertexBufferEntry] = []
index_buffer_data: BinaryReader
index_buffer_type: ComponentType
vertex_buffer_data: BinaryReader
vertex_buffer_size = 0
index_buffer_size = 0
@classmethod
def set_vertex_buffer_data(self, data: bytes, size: int) -> None:
self.vertex_buffer_data = BinaryReader(BytesIO(data))
self.vertex_buffer_size = size
@classmethod
def set_index_buffer_data(self, data: bytes, size: int) -> None:
self.index_buffer_data = BinaryReader(BytesIO(data))
self.index_buffer_size = size
def read_model(file_name):
with open(file_name, "rb") as f:
reader = BinaryReader(f)
mesh = Mesh()
offset_tracker = MeshOffsetTracker(0)
assert offset_tracker.offset() == reader.tell()
magic = reader.read_uint32()
mesh.version = reader.read_uint16()
mesh.flags = reader.read_uint16()
mesh.size = reader.read_uint32()
assert magic == 3365961549, "Invalid QSSG mesh"
assert mesh.version <= 7, "Invalid QSSG mesh version"
assert mesh.version >= 3, "Legacy QSSG mesh version is not supported"
target_buffer_entries_count = reader.read_uint32()
vertex_buffer_entries_count = reader.read_uint32()
stride = reader.read_uint32()
target_buffer_data_size = reader.read_uint32()
vertex_buffer_data_size = reader.read_uint32()
def has_seperate_target_buffer():
return mesh.version >= 7
def has_lightmap_size_hint():
return mesh.version >= 5
def has_lod_data_hint():
return mesh.version >= 6
print(f"Version: {mesh.version}")
print(f"Flags: {mesh.flags}")
print(f"Size: {mesh.size}")
print(f"Target buffer entries count: {target_buffer_entries_count}")
print(f"Vertex buffer entries count: {vertex_buffer_entries_count}")
print(f"Stride: {stride}")
print(f"Target buffer data size: {target_buffer_data_size}")
print(f"Vertex buffer data size: {vertex_buffer_data_size}")
if not has_seperate_target_buffer():
target_buffer_entries_count = 0
target_buffer_data_size = 0
index_buffer_type = reader.read_uint32()
mesh.index_buffer_type = assert_component_type(index_buffer_type)
index_buffer_data_offset = reader.read_uint32()
index_buffer_data_size = reader.read_uint32()
print(f"Index buffer component type: {mesh.index_buffer_type}")
print(f"Index buffer data offset: {index_buffer_data_offset}")
print(f"Index buffer data size: {index_buffer_data_size}")
target_count = reader.read_uint32()
subsets_count = reader.read_uint32()
print(f"Target count: {target_count}")
print(f"Subsets count: {subsets_count}")
joints_offset = reader.read_uint32()
joints_count = reader.read_uint32()
draw_mode = reader.read_uint32()
mesh.draw_mode = assert_draw_mode(draw_mode)
winding = reader.read_uint32()
mesh.winding = assert_winding(winding)
print(f"Joints offset: {joints_offset}")
print(f"Joints count: {joints_count}")
print(f"Draw mode: {mesh.draw_mode}")
print(f"Winding: {mesh.winding}")
offset_tracker.advance(16)
entries_byte_size = 0
print()
print("\t -- Vertex Buffer --")
print(f"\tentry count: {vertex_buffer_entries_count}")
for i in range(vertex_buffer_entries_count):
# print(f"\t\tvertex buffer entry {i}")
vbe = VertexBufferEntry()
name_offset = reader.read_uint32()
component_type = reader.read_uint32()
component_type = assert_component_type(component_type)
vbe.component_count = reader.read_uint32()
vbe.offset = reader.read_uint32()
vbe.component_type = component_type
mesh.vertex_buffer.append(vbe)
entries_byte_size += 16
align_amount = offset_tracker.align_advance(entries_byte_size)
if align_amount:
reader.read(align_amount)
# vertex buffer entry names
num_targets = 0
attr_names: List[bytes]
for entry in mesh.vertex_buffer:
name_length = reader.read_uint32()
offset_tracker.advance(struct.calcsize("I"))
name = reader.read(name_length)[: name_length - 1].decode()
entry.name = name
print(f"\t\tName: {entry.name}")
print(f"\t\tComponent type: {entry.component_type}")
print(f"\t\tComponent count: {entry.component_count}")
print(f"\t\tOffset: {entry.offset}")
print()
align_amount = offset_tracker.align_advance(name_length)
if align_amount:
reader.read(align_amount)
if num_targets > 0 or (not has_seperate_target_buffer() and entry.name.startswith("attr_t")):
# print("fucked")
# i do not give enough fucks about any of this
pass
vertex_buffer_data = reader.read(vertex_buffer_data_size)
align_amount = offset_tracker.align_advance(vertex_buffer_data_size)
if align_amount:
reader.read(align_amount)
mesh.set_vertex_buffer_data(vertex_buffer_data, vertex_buffer_data_size)
index_buffer_data = reader.read(index_buffer_data_size)
align_amount = offset_tracker.align_advance(index_buffer_data_size)
if align_amount:
reader.read(align_amount)
mesh.set_index_buffer_data(index_buffer_data, index_buffer_data_size)
subset_byte_size = 0
internal_subsets: List[Subset] = []
for i in range(subsets_count):
subset = Subset()
subset.count = reader.read_uint32()
subset.offset = reader.read_uint32()
min_x = reader.read_float()
min_y = reader.read_float()
min_z = reader.read_float()
max_x = reader.read_float()
max_y = reader.read_float()
max_z = reader.read_float()
name_offset = reader.read_uint32()
subset.name_length = reader.read_uint32()
subset.name_length = reader.read_uint32()
subset.bounds.min = Vector3(min_x, min_y, min_z)
subset.bounds.max = Vector3(max_x, max_y, max_z)
if has_lightmap_size_hint():
width = reader.read_uint32()
height = reader.read_uint32()
subset.lightmap_size_hint = Size(width, height)
if has_lod_data_hint():
subset.lod_count = reader.read_uint32()
subset_byte_size += 52 # v6
else:
subset_byte_size += 48 # v5
else:
subset.lightmap_size_hint = Size(0, 0)
subset_byte_size += 40 # v3 and v4
internal_subsets.append(subset)
align_amount = offset_tracker.align_advance(subset_byte_size)
if align_amount:
reader.read(align_amount)
for subset in internal_subsets:
subset.raw_name_utf16 = reader.read(subset.name_length * 2) # utf16-le
print(subset.raw_name_utf16.decode("utf-16-le"))
align_amount = offset_tracker.align_advance(subset.name_length * 2)
if align_amount:
reader.read(align_amount)
lod_byte_size = 0
subsets: List[Subset] = []
for subset in internal_subsets:
for i in range(subset.lod_count):
lod = Lod()
count = reader.read_uint32()
offset = reader.read_uint32()
distance = reader.read_float()
lod.count = count
lod.offset = offset
lod.distance = distance
subset.lods.append(lod)
print(f"Lod {i}/{subset.lod_count}")
lod_byte_size += 12
subsets.append(subset)
align_amount = offset_tracker.align_advance(lod_byte_size)
if align_amount:
reader.read(align_amount)
target_buffer: List[VertexBufferEntry] = []
target_buffer_data: bytes
# morph targets
if target_buffer_entries_count > 0:
if has_seperate_target_buffer():
entries_byte_size = 0
for i in range(target_buffer_entries_count):
vbe = VertexBufferEntry()
name_offset = reader.read_uint32()
component_type = reader.read_uint32()
component_type = assert_component_type(component_type)
vbe.component_count = reader.read_uint32()
vbe.offset = reader.read_uint32()
vbe.component_type = component_type
target_buffer.append(vbe)
entries_byte_size += 16
align_amount = offset_tracker.align_advance(entries_byte_size)
if align_amount:
reader.read(align_amount)
for entry in target_buffer:
name_length = reader.read_uint32()
offset_tracker.advance(struct.calcsize("I"))
name = reader.read(name_length - 1)
entry.name = name.decode("utf-8")
print(f" Name: {entry.name}")
align_amount = offset_tracker.align_advance(name_length)
if align_amount:
reader.read(align_amount)
target_buffer_data = reader.read(target_buffer_data_size)
else:
# remove target entries from vertex buffer entries
start_index = vertex_buffer_entries_count - target_buffer_entries_count
del mesh.vertex_buffer[start_index : start_index + target_buffer_entries_count]
vertex_count = vertex_buffer_data_size / stride
target_entry_tex_width = math.ceil(math.sqrt(vertex_count))
target_comp_stride = target_entry_tex_width * target_entry_tex_width * 4 * struct.calcsize("f")
num_comps = target_buffer_entries_count / num_targets
for i in range(target_buffer_entries_count):
entry = target_buffer[i]
dst_buf_index = (i // num_comps) * target_comp_stride + (i % num_comps) * (
target_comp_stride * num_targets
)
dst_buf = memoryview(target_buffer_data)[dst_buf_index:]
src_buf_index = entry.offset
src_buf = memoryview(mesh.vertex_buffer_data)[src_buf_index:]
for j in range(vertex_count):
dst_index = j * 4 * struct.calcsize("f")
src_index = j * stride
dst_buf[dst_index : dst_index + 3 * struct.calcsize("f")] = src_buf[
src_index : src_index + 3 * struct.calcsize("f")
]
entry.offset = i * target_comp_stride
# now we don't need to have redundant targetbuffer entries
start_index = num_comps
end_index = target_buffer_entries_count - (target_buffer_entries_count - num_comps)
del target_buffer[start_index:end_index]
return mesh
def read(reader: BinaryReader, t: ComponentType):
if t == ComponentType.UINT8:
return reader.read_uint()
elif t == ComponentType.INT8:
return reader.read_int()
elif t == ComponentType.UINT16:
return reader.read_uint16()
elif t == ComponentType.INT16:
return reader.read_int16()
elif t == ComponentType.UINT32:
return reader.read_uint32()
elif t == ComponentType.INT32:
return reader.read_int32()
elif t == ComponentType.UINT64:
return reader.read_uint64()
elif t == ComponentType.INT64:
return reader.read_int64()
elif t == ComponentType.FLOAT16:
return reader.read_float16()
elif t == ComponentType.FLOAT32:
return reader.read_float()
elif t == ComponentType.FLOAT64:
return reader.read_double()
else:
raise Exception(f"Unsupported component type: {t}")
class Vertex(object):
position: Vector3 = None
normal: Vector3 = None
uv: Vector2 = None
tangant: Vector3 = None
binormal: Vector2 = None
def __str__(self) -> str:
return f"Position: {self.position}, Normal: {self.normal}, UV: {self.uv}, Tangant: {self.tangant}, Binormal: {self.binormal}"
def mesh_to_obj(mesh: Mesh) -> str:
obj = "# Generated using QTQuick3D QSSG Mesh Converter by Puyodead1\nhttps:\/\/github.com\puyodead1\n"
vbo = mesh.vertex_buffer_data
vertex_size = sum(map(lambda x: COMPONENT_TYPE_SIZES[x.component_type] * x.component_count, mesh.vertex_buffer))
vertex_count = mesh.vertex_buffer_size // vertex_size
indicie_count = mesh.index_buffer_size // COMPONENT_TYPE_SIZES[mesh.index_buffer_type]
vertices: List[Vertex] = []
indicies: List[int] = tuple(read(mesh.index_buffer_data, mesh.index_buffer_type) for _ in range(indicie_count))
for i in range(vertex_count):
vertex = Vertex()
for entry in mesh.vertex_buffer:
t = Vector3 if entry.component_count == 3 else Vector2
a = tuple(read(vbo, entry.component_type) for _ in range(entry.component_count))
a = t(*a)
if entry.name == "attr_pos":
vertex.position = a
elif entry.name == "attr_norm":
vertex.normal = a
elif entry.name == "attr_uv0":
vertex.uv = a
elif entry.name == "attr_textan":
vertex.tangant = a
elif entry.name == "attr_binormal":
vertex.binormal = a
vertices.append(vertex)
for vertex in vertices:
obj += f"v {vertex.position.x} {vertex.position.y} {vertex.position.z}\n"
for vertex in vertices:
obj += f"vn {vertex.normal.x} {vertex.normal.y} {vertex.normal.z}\n"
for vertex in vertices:
obj += f"vt {vertex.uv.x} {vertex.uv.y}\n"
for i in range(0, len(indicies), 3):
d = indicies[i : i + 3]
obj += f"f {d[0] + 1}/{d[0] + 1}/{d[0] + 1} {d[1] + 1}/{d[1] + 1}/{d[1] + 1} {d[2] + 1}/{d[2] + 1}/{d[2] + 1}\n"
return obj
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("file", type=str)
args = parser.parse_args()
infile = Path(args.file)
mesh = read_model(infile)
obj = mesh_to_obj(mesh)
outfile = Path("converted", infile.name.split(".")[0] + ".obj")
outfile.parent.mkdir(exist_ok=True, parents=True)
with open(outfile, "w") as f:
f.write(obj)
print(f"Output written to {outfile}")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment