From fb39c40431578195881ca2b194c80e268f8fecab Mon Sep 17 00:00:00 2001 From: Andrew Burnett Date: Fri, 25 Mar 2016 11:55:51 +0000 Subject: [PATCH 1/4] Add Transform fields --- unitypack/engine/component.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/unitypack/engine/component.py b/unitypack/engine/component.py index 3848904..cd14f01 100644 --- a/unitypack/engine/component.py +++ b/unitypack/engine/component.py @@ -10,4 +10,8 @@ class Behaviour(Component): class Transform(Component): - pass + position = field("m_LocalPosition") + rotation = field("m_LocalRotation") + scale = field("m_LocalScale") + parent = field("m_Father") + children = field("m_Children") From 7de39342a53f76583f03a0ded64018029e681c4d Mon Sep 17 00:00:00 2001 From: Andrew Burnett Date: Fri, 25 Mar 2016 20:50:23 +0000 Subject: [PATCH 2/4] Add SubMesh and VertexData classes --- unitypack/engine/__init__.py | 2 +- unitypack/engine/mesh.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/unitypack/engine/__init__.py b/unitypack/engine/__init__.py index 008291f..9e04bfc 100644 --- a/unitypack/engine/__init__.py +++ b/unitypack/engine/__init__.py @@ -4,7 +4,7 @@ ) from .audio import AudioClip, AudioSource, StreamedResource from .component import Behaviour, Component, Transform -from .mesh import Mesh, MeshFilter +from .mesh import Mesh, SubMesh, VertexData, MeshFilter from .movie import MovieTexture from .object import GameObject from .particle import EllipsoidParticleEmitter, MeshParticleEmitter, ParticleEmitter, ParticleSystem diff --git a/unitypack/engine/mesh.py b/unitypack/engine/mesh.py index fcdba3b..6f43aef 100644 --- a/unitypack/engine/mesh.py +++ b/unitypack/engine/mesh.py @@ -22,5 +22,21 @@ class Mesh(Object): vertex_data = field("m_VertexData") +class SubMesh(Object): + first_byte = field("firstByte") + first_vertex = field("firstVertex") + index_count = field("indexCount") + localAABB = field("localAABB") + topology = field("topology") + vertex_count = field("vertexCount") + + +class VertexData(Object): + channels = field("m_Channels") + current_channels = field("m_CurrentChannels") + data = field("m_DataSize") + vertex_count = field("m_VertexCount") + + class MeshFilter(Component): pass From 9d896bbcc3f18f8df8b3b74f9f8babceb335a242 Mon Sep 17 00:00:00 2001 From: Andrew Burnett Date: Sat, 26 Mar 2016 18:57:13 +0000 Subject: [PATCH 3/4] BinaryReader: add unsigned byte --- unitypack/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/unitypack/utils.py b/unitypack/utils.py index b647077..ebbae9b 100644 --- a/unitypack/utils.py +++ b/unitypack/utils.py @@ -48,6 +48,9 @@ def read_boolean(self) -> bool: def read_byte(self) -> int: return struct.unpack(self.endian + "b", self.read(1))[0] + def read_ubyte(self) -> int: + return struct.unpack(self.endian + "B", self.read(1))[0] + def read_int16(self) -> int: return struct.unpack(self.endian + "h", self.read(2))[0] From 10aa3dfcd9b775151a731886e0885f88b4fef1de Mon Sep 17 00:00:00 2001 From: Andrew Burnett Date: Sat, 26 Mar 2016 21:07:23 +0000 Subject: [PATCH 4/4] Add mesh OBJ export --- extract.py | 10 ++- unitypack/export.py | 196 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 unitypack/export.py diff --git a/extract.py b/extract.py index f32c7be..7271366 100755 --- a/extract.py +++ b/extract.py @@ -3,6 +3,7 @@ import pickle import sys import unitypack +from unitypack.export import OBJMesh from argparse import ArgumentParser from PIL import ImageOps from fsb5 import FSB5 @@ -57,8 +58,13 @@ def handle_asset(asset, handle_formats): write_to_file(d.name + ".cg", d.script) elif obj.type == "Mesh": - mesh_data = pickle.dumps(d._obj) - write_to_file(d.name + ".Mesh.pickle", mesh_data, mode="wb") + try: + mesh_data = OBJMesh(d).export() + write_to_file(d.name + ".obj", mesh_data, mode="w") + except NotImplementedError as e: + print("WARNING: Could not extract %r (%s)" % (d, e)) + mesh_data = pickle.dumps(d._obj) + write_to_file(d.name + ".Mesh.pickle", mesh_data, mode="wb") elif obj.type == "TextAsset": if isinstance(d.script, bytes): diff --git a/unitypack/export.py b/unitypack/export.py new file mode 100644 index 0000000..c9a42dd --- /dev/null +++ b/unitypack/export.py @@ -0,0 +1,196 @@ +from io import BytesIO +from .utils import BinaryReader + + +class OBJVector2: + def __init__(self, x = 0, y = 0): + self.x = x + self.y = y + + def read(self, buf): + self.x = buf.read_float() + self.y = buf.read_float() + return self + + def __str__(self): + return "%s %s" % (self.x, 1 - self.y) + + +class OBJVector3(OBJVector2): + def __init__(self, x = 0, y = 0, z = 0): + super().__init__(x, y) + self.z = z + + def read(self, buf): + super().read(buf) + self.z = buf.read_float() + return self + + def __str__(self): + return "%s %s %s" % (-self.x, self.y, self.z) + + +class OBJVector4(OBJVector3): + def __init__(self, x = 0, y = 0, z = 0, w = 0): + super().__init__(x, y, z) + self.w = w + + def read(self, buf): + super().read(buf) + self.w = buf.read_float() + return self + + def read_color(self, buf): + self.x = buf.read_ubyte() + self.y = buf.read_ubyte() + self.z = buf.read_ubyte() + self.w = buf.read_ubyte() + return self + + def __str__(self): + return "%s %s %s %s" % (self.x, self.y, self.z, self.w) + + +class MeshData: + def __init__(self, mesh): + self.mesh = mesh + self.indices = [] + self.triangles = [] + self.vertices = [] + self.normals = [] + self.colors = [] + self.uv1 = [] + self.uv2 = [] + self.uv3 = [] + self.uv4 = [] + self.tangents = [] + self.extract_indices() + self.extract_vertices() + + def extract_indices(self): + for sub in self.mesh.submeshes: + sub_indices = [] + sub_triangles = [] + buf = BinaryReader(BytesIO(self.mesh.index_buffer)) + buf.seek(sub.first_byte) + for i in range(0, sub.index_count): + sub_indices.append(buf.read_uint16()) + if not sub.topology: + sub_triangles.extend(sub_indices) + else: + raise NotImplementedError("(%s) topologies are not supported" % (self.mesh.name)) + + self.indices.append(sub_indices) + self.triangles.append(sub_triangles) + + def extract_vertices(self): + # unity 5+ has 8 channels (6 otherwise) + v5_channel_count = 8 + buf = BinaryReader(BytesIO(self.mesh.vertex_data.data)) + channels = self.mesh.vertex_data.channels + # actual streams attribute 'm_Streams' may only exist in unity 4, + # use of channel data alone seems to be sufficient + stream_count = self.get_num_streams(channels) + channel_count = len(channels) + + for s in range(0, stream_count): + for i in range(0, self.mesh.vertex_data.vertex_count): + for j in range(0, channel_count): + ch = None + if channel_count > 0: + ch = channels[j] + # format == 1, use half-floats (16 bit) + if ch["format"] == 1: + raise NotImplementedError("(%s) 16 bit floats are not supported" % (mesh.name)) + # read the appropriate vertex value into the correct list + if ch and ch["dimension"] > 0 and ch["stream"] == s: + if j == 0: + self.vertices.append(OBJVector3().read(buf)) + elif j == 1: + self.normals.append(OBJVector3().read(buf)) + elif j == 2: + self.colors.append(OBJVector4().read_color(buf)) + elif j == 3: + self.uv1.append(OBJVector2().read(buf)) + elif j == 4: + self.uv2.append(OBJVector2().read(buf)) + elif j == 5: + if channel_count == v5_channel_count: + self.uv3.append(OBJVector2().read(buf)) + else: + self.tangents.append(OBJVector4().read(buf)) + elif j == 6: # for unity 5+ + self.uv4.append(OBJVector2().read(buf)) + elif j == 7: # for unity 5+ + self.tangents.append(OBJVector4().read(buf)) + # TODO investigate possible alignment here, after each stream + + def get_num_streams(self, channels): + streams = [] + # scan the channel's stream value for distinct entries + for c in channels: + if c["stream"] not in streams: + streams.append(c["stream"]) + + return len(streams) + + +class OBJMesh: + def __init__(self, mesh): + if mesh.mesh_compression: + # TODO handle compressed meshes + raise NotImplementedError("(%s) compressed meshes are not supported" % (mesh.name)) + self.mesh_data = MeshData(mesh) + self.mesh = mesh + + @staticmethod + def face_str(indices, coords, normals): + ret = ["f "] + for i in indices[::-1]: + ret.append(str(i + 1)) + if coords or normals: + ret.append("/") + if coords: + ret.append(str(i + 1)) + if normals: + ret.append("/") + ret.append(str(i + 1)) + ret.append(" ") + ret.append("\n") + return "".join(ret) + + def export(self): + ret = [] + verts_per_face = 3 + normals = self.mesh_data.normals + tex_coords = self.mesh_data.uv1 + if not tex_coords: + tex_coords = self.mesh_data.uv2 + + for v in self.mesh_data.vertices: + ret.append("v %s\n" % (v)) + for v in normals: + ret.append("vn %s\n" % (v)) + for v in tex_coords: + ret.append("vt %s\n" % (v)) + ret.append("\n") + + # write group name and set smoothing to 1 + ret.append("g %s\n" % (self.mesh.name)) + ret.append("s 1\n") + + sub_count = len(self.mesh.submeshes) + for i in range(0, sub_count): + if sub_count == 1: + ret.append("usemtl %s\n" % (self.mesh.name)) + else: + ret.append("usemtl %s_%d\n" % (self.mesh.name, i)) + face_tri = [] + for t in self.mesh_data.triangles[i]: + face_tri.append(t) + if len(face_tri) == verts_per_face: + ret.append(self.face_str(face_tri, tex_coords, normals)) + face_tri = [] + ret.append("\n") + + return "".join(ret)