Skip to content

Commit

Permalink
add 'No Change' to unit scale option, and bug fix
Browse files Browse the repository at this point in the history
  • Loading branch information
matyalatte committed Jul 5, 2022
1 parent 76fd6cc commit c7bb276
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 33 deletions.
3 changes: 3 additions & 0 deletions blender_uasset_addon/bpy_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
def set_unit_scale(unit_scale):
"""Change unit scale to unit_scale."""
if isinstance(unit_scale, str):
if unit_scale == 'NONE':
return
if unit_scale not in UNIT:
raise RuntimeError(f'Unsupported unit. ({unit_scale})')
unit_scale = UNIT[unit_scale]
Expand Down Expand Up @@ -428,6 +430,7 @@ def join_meshes(meshes):
Returns:
mesh: joined mesh
"""
# deselect_all() need this?
if len(meshes) == 0:
return None
if len(meshes) == 1:
Expand Down
14 changes: 6 additions & 8 deletions blender_uasset_addon/import_uasset.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,15 +307,12 @@ def generate_mesh(amt, asset, materials, material_names, rescale=1.0,
# skinning
vg_names = [bone_names[vg] for vg in vertex_groups[i]]
joint = np.array(joints[i], dtype=np.uint32)
weight = np.array(weights[i], dtype=np.float32) / 255
weight = np.array(weights[i], dtype=np.float32) / 255 # Should we use float64 like normals?
bpy_util.skinning(section, vg_names, joint, weight)

# smoothing
norm = (np.array(normals[i], dtype=np.float32)) / 127 - 1
norm = norm / np.linalg.norm(norm, axis=1)[:, np.newaxis]

norm = bpy_util.flip_y_for_3d_vectors(norm)
bpy_util.smoothing(mesh_data, len(indice) // 3, norm, enable_smoothing=smoothing)
normal = np.array(normals[i], dtype=np.float64) / 127 - 1 # Got broken normals with float32
bpy_util.smoothing(mesh_data, len(indice) // 3, normal, enable_smoothing=smoothing)

if not keep_sections:
# join meshes
Expand Down Expand Up @@ -555,9 +552,10 @@ class ImportOptions(PropertyGroup):
unit_scale: EnumProperty(
name='Unit Scale',
items=(('CENTIMETERS', 'Centimeters', 'UE standard'),
('METERS', 'Meters', 'Blender standard')),
('METERS', 'Meters', 'Blender standard'),
('NONE', 'No Change', 'Use the current uint scale')),
description='Change unit scale to',
default='METERS'
default='NONE'
)

rescale: FloatProperty(
Expand Down
215 changes: 215 additions & 0 deletions blender_uasset_addon/unreal/animation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
"""Class for animations.
Notes:
It can't parse animation data yet.
It'll just get frame count, bone ids, and compressed binary.
"""
# Todo: Uncompress animation data.
from ..util import io_util as io


def read_unversioned_header(f):
"""Skip unversioned headers."""
offset = f.tell()
unv_head = io.read_uint8_array(f, 2)
is_last = unv_head[1] % 2 == 0
while is_last:
unv_head = io.read_uint8_array(f, 2)
is_last = unv_head[1] % 2 == 0
if f.tell() - offset > 100:
raise RuntimeError('Parse Failed. ')
size = f.tell() - offset
f.seek(offset)
headers = f.read(size)
return headers


def seek_skeleton(f, import_id):
"""Read binary data until find skeleton import ids."""
offset = f.tell()
buf = f.read(3)
size = io.get_size(f)
while True:
while buf != b'\xff' * 3:
if b'\xff' not in buf:
buf = f.read(3)
else:
buf = b''.join([buf[1:], f.read(1)])
if f.tell() == size:
raise RuntimeError('Skeleton id not found. This is an unexpected error.')
f.seek(-4, 1)
imp_id = -io.read_int32(f) - 1
if imp_id == import_id:
break
buf = f.read(3)
size = f.tell() - offset
f.seek(offset)
return f.read(size)


class UnkData:
"""Animation data."""
def __init__(self, unk, unk2, unk_int, unk_int2):
"""Constructor."""
self.unk = unk
self.unk2 = unk2
self.unk_int = unk_int
self.unk_int2 = unk_int2

@staticmethod
def read(f):
"""Read function."""
io.check(f.read(4), b'\x00\x02\x01\x05')
unk = f.read(8)
if unk[0] != b'\x80':
unk2 = f.read(27*io.read_uint32(f))
else:
unk2 = None
unk_int = io.read_uint32(f)
unk_int2 = io.read_uint32(f)
io.read_const_uint32(f, 4)
return UnkData(unk, unk2, unk_int, unk_int2)

def write(self, f):
"""Write function."""
f.write(b'\x00\x02\x01\x05')
f.write(self.unk)
if self.unk2 is not None:
io.write_uint32(f, len(self.unk2) // 27)
f.write(self.unk2)
io.write_uint32(f, self.unk_int)
io.write_uint32(f, self.unk_int2)
io.write_uint32(f, 4)


class RawAnimData:
"""Raw animation binary data."""
def __init__(self, size, unk, bone_count, unk_int, unk2, frame_count, fps, rest):
"""Constructor."""
self.size = size
self.unk = unk
self.bone_count = bone_count
self.unk_int = unk_int
self.unk2 = unk2
self.frame_count = frame_count
self.fps = fps
self.rest = rest

@staticmethod
def read(f, size):
"""Read function."""
offset = f.tell()
unk = f.read(8)
io.read_const_uint32(f, 3)
bone_count = io.read_uint16(f)
unk_int = io.read_uint16(f)
unk2 = f.read(8)
frame_count = io.read_uint32(f)
fps = io.read_uint32(f)
rest = f.read(size - (f.tell() - offset))
return RawAnimData(size, unk, bone_count, unk_int, unk2, frame_count, fps, rest)

def write(self, f):
"""Write function."""
f.write(self.unk)
io.write_uint32(f, 3)
io.write_uint16(f, self.bone_count)
io.write_uint16(f, self.unk_int)
f.write(self.unk2)
io.write_uint32(f, self.frame_count)
io.write_uint32(f, self.fps)
f.write(self.rest)


class AnimSequence:
"""Animation data."""
def __init__(self, uasset, unv_header, frame_count, bone_ids, notifies, guid, unk_ary, unk_int, raw_data, verbose):
"""Constructor."""
self.uasset = uasset
self.unv_header = unv_header
self.frame_count = frame_count
self.bone_ids = bone_ids
self.notifies = notifies
self.guid = guid
self.unk_ary = unk_ary
self.unk_int = unk_int
self.raw_data = raw_data
if verbose:
self.print()
# Todo: Remove this for released version
# with open(f'{uasset.file}.bin', 'wb') as f:
# self.raw_data.write(f)

@staticmethod
def read(f, uasset, verbose):
"""Read function."""
unv_header = read_unversioned_header(f)
frame_count = io.read_uint32(f)

def read_bone_id(f):
io.check(f.read(2), b'\x00\x03', f)
return io.read_uint32(f)

bone_count = io.read_uint32(f)
io.check(f.read(3), b'\x80\x03\x01')
bone_ids = [0] + [read_bone_id(f) for i in range(bone_count - 1)]

def get_skeleton_import(imports):
for imp, i in zip(imports, range(len(imports))):
if imp.class_name == 'Skeleton':
return imp, i
# Skip Notifies
skeleton_imp, import_id = get_skeleton_import(uasset.imports)

notifies = seek_skeleton(f, import_id)
io.read_null(f)
guid = f.read(16)
io.check(io.read_uint16(f), 1)
io.check(io.read_uint32_array(f, length=5), [1, 3, 0, 0, 2])
io.check(io.read_uint32_array(f), bone_ids)

io.check(f.read(2), b'\x00\x03', f)

unk_ary = [UnkData.read(f) for i in range(io.read_uint32(f))]

unk_int = io.read_uint32(f) # some offset?
raw_size = io.read_uint32(f)
io.read_const_uint32(f, raw_size)
raw_data = RawAnimData.read(f, raw_size)
return AnimSequence(uasset, unv_header, frame_count, bone_ids, notifies,
guid, unk_ary, unk_int, raw_data, verbose)

def write(self, f):
"""Write function."""
f.write(self.unv_header)
io.write_uint32(f, self.frame_count)

def write_bone_id(f, index):
f.write(b'\x00\x03')
io.write_uint32(f, index)

io.write_uint32(f, len(self.bone_ids))
f.write(b'\x80\x03\x01')
list(map(lambda i: write_bone_id(f, i), self.bone_ids[1:]))

f.write(self.notifies)
io.write_null(f)
f.write(self.guid)

io.write_uint16(f, 1)
io.write_uint32_array(f, [1, 3, 0, 0, 2])
io.write_uint32_array(f, self.bone_ids, with_length=True)
f.write(b'\x00\x03')
io.write_uint32(f, len(self.unk_ary))
list(map(lambda x: x.write(f), self.unk_ary))

io.write_uint32(f, self.unk_int)
io.write_uint32(f, self.raw_data.size)
io.write_uint32(f, self.raw_data.size)
self.raw_data.write(f)

def print(self):
"""Print meta data."""
print(f'frame count: {self.frame_count}')
print(f'use bone count: {len(self.bone_ids)}')
print(f'compressed data size: {self.raw_data.size}')
38 changes: 25 additions & 13 deletions blender_uasset_addon/unreal/buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,14 @@ def parse(self):
"""Parse buffer."""
def unpack(i):
mask8bit = 0xff
i = i ^ 0x80808080
x = i & mask8bit
y = (i >> 8) & mask8bit
z = (i >> 16) & mask8bit
return [x, y, z]

parsed = struct.unpack('<' + 'I' * 2 * self.size, self.buf)
normal = [unpack(parsed[i * 2 + 1] ^ 0x80808080) for i in range(self.size)]
normal = [unpack(parsed[i * 2 + 1]) for i in range(self.size)]
return normal

def import_from_blender(self, normal):
Expand Down Expand Up @@ -219,15 +220,20 @@ def write(f, vb):

def parse(self):
"""Parse buffer."""
def unpack(i):
mask8bit = 0xff
x = i & mask8bit
y = (i >> 8) & mask8bit
z = (i >> 16) & mask8bit
return [x, y, z]
uv_type = 'f' * self.use_float32 + 'e' * (not self.use_float32)
parsed = struct.unpack('<' + ('B' * 8 + uv_type * 2 * self.uv_num) * self.size, self.buf)
stride = 8 + 2 * self.uv_num
normals = [parsed[i * stride: i * stride + 8] for i in range(self.size)]
normal = [n[4:7] for n in normals]
# tangent = [n[0:4] for n in normals]
parsed = struct.unpack('<' + ('I' * 2 + uv_type * 2 * self.uv_num) * self.size, self.buf)
stride = 2 + 2 * self.uv_num
normals = [parsed[i * stride: i * stride + 1] for i in range(self.size)]
normal = [unpack(n) for n in normals]
texcoords = []
for j in range(self.uv_num):
texcoord = [parsed[i * stride + 8 + j * 2: i * stride + 8 + j * 2 + 2] for i in range(self.size)]
texcoord = [parsed[i * stride + 2 + j * 2: i * stride + 2 + j * 2 + 2] for i in range(self.size)]
texcoords.append(texcoord)
return normal, texcoords

Expand Down Expand Up @@ -318,16 +324,22 @@ def write(f, vb):

def parse(self):
"""Parse buffer."""
def unpack(i):
mask8bit = 0xff
x = i & mask8bit
y = (i >> 8) & mask8bit
z = (i >> 16) & mask8bit
return [x, y, z]
uv_type = 'f' * self.use_float32 + 'e' * (not self.use_float32)
parsed = struct.unpack('<' + ('B' * 8 + 'fff' + uv_type * 2 * self.uv_num) * self.size, self.buf)
stride = 11 + 2 * self.uv_num
normals = [parsed[i * stride: i * stride + 8] for i in range(self.size)]
normal = [n[4:7] for n in normals]
parsed = struct.unpack('<' + ('I' * 2 + 'fff' + uv_type * 2 * self.uv_num) * self.size, self.buf)
stride = 5 + 2 * self.uv_num
normals = [parsed[i * stride + 1] for i in range(self.size)]
normal = [unpack(n) for n in normals]
# tangent = [n[0:4] for n in normals]
position = [parsed[i * stride + 8: i * stride + 11] for i in range(self.size)]
position = [parsed[i * stride + 2: i * stride + 5] for i in range(self.size)]
texcoords = []
for j in range(self.uv_num):
texcoord = [parsed[i * stride + 11 + j * 2: i * stride + 11 + j * 2 + 2] for i in range(self.size)]
texcoord = [parsed[i * stride + 5 + j * 2: i * stride + 5 + j * 2 + 2] for i in range(self.size)]
texcoords.append(texcoord)
return normal, position, texcoords

Expand Down
8 changes: 2 additions & 6 deletions blender_uasset_addon/unreal/uasset.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ class UassetExport(c.LittleEndianStructure):
("unk", c.c_ubyte * 64),
]

MAIN_EXPORTS = ['SkeletalMesh', 'StaticMesh', 'Skeleton',
MAIN_EXPORTS = ['SkeletalMesh', 'StaticMesh', 'Skeleton', 'AnimSequence',
'Texture2D', 'TextureCube', 'Material', 'MaterialInstanceConstant']

def __init__(self):
Expand Down Expand Up @@ -309,12 +309,8 @@ def read_names(f, i):
list(map(lambda x, i: x.print(str(i)), self.imports, range(len(self.imports))))

paths = [n for n in self.name_list if n[0] == '/']
import_names = list(set([imp.name for imp in self.imports] + [imp.parent_dir for imp in self.imports]))
for imp in import_names:
if imp in paths:
paths.remove(imp)
paths = [p for p in paths if p.split('/')[-1] in self.file]
if len(paths) != 1:
print(paths)
raise RuntimeError('Failed to get asset path.')
self.asset_path = paths[0]

Expand Down
11 changes: 8 additions & 3 deletions blender_uasset_addon/unreal/uexp.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .mesh import StaticMesh, SkeletalMesh
from .skeleton import SkeletonAsset
from .texture import Texture
from .animation import AnimSequence


class Uexp:
Expand Down Expand Up @@ -60,10 +61,10 @@ def load(self, file, uasset, verbose=False):
if f.tell() + self.uasset.size != export.offset:
raise RuntimeError('Parse failed.')

if verbose:
print(f'{export.name} (offset: {f.tell()})')
print(f' size: {export.size}')
if export.ignore:
if verbose:
print(f'{export.name} (offset: {f.tell()})')
print(f' size: {export.size}')
export.read_uexp(f)

else:
Expand All @@ -77,6 +78,8 @@ def load(self, file, uasset, verbose=False):
self.skeleton = SkeletonAsset.read(f, self.version, self.name_list, verbose=verbose)
elif 'Texture' in self.asset_type:
self.texture = Texture.read(f, self.uasset, verbose=verbose)
elif self.asset_type == 'AnimSequence':
self.anim = AnimSequence.read(f, self.uasset, verbose=verbose)
self.unknown2 = f.read(export.offset + export.size - f.tell() - self.uasset.size)

offset = f.tell()
Expand Down Expand Up @@ -113,6 +116,8 @@ def save(self, file):
SkeletonAsset.write(f, self.skeleton)
elif 'Texture' in self.asset_type:
self.texture.write(f)
elif self.asset_type == 'AnimSequence':
self.anim.write(f)
else:
raise RuntimeError(f'Unsupported asset. ({self.asset_type})')
f.write(self.unknown2)
Expand Down
Loading

0 comments on commit c7bb276

Please sign in to comment.