2768 lines
94 KiB
Python
2768 lines
94 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
'''
|
||
|
ZeroEngine .msh model format.
|
||
|
|
||
|
Refer to
|
||
|
schlechtwetterfront.github.io/ze_filetypes/msh.html
|
||
|
for more information regarding the file format.
|
||
|
'''
|
||
|
import msh2_crc
|
||
|
reload(msh2_crc)
|
||
|
import itertools
|
||
|
import struct
|
||
|
import math
|
||
|
import logging
|
||
|
import json
|
||
|
import os
|
||
|
|
||
|
|
||
|
MODEL_TYPES = {'null': 0,
|
||
|
'geodynamic': 1,
|
||
|
'cloth': 2,
|
||
|
'bone': 3,
|
||
|
'geobone': 3,
|
||
|
'geostatic': 4,
|
||
|
'geoshadow': 6}
|
||
|
|
||
|
MODEL_TYPES_INT = ['null',
|
||
|
'geodynamic',
|
||
|
'cloth',
|
||
|
'bone',
|
||
|
'geostatic',
|
||
|
'',
|
||
|
'geoshadow']
|
||
|
|
||
|
|
||
|
class MSH2Error(Exception):
|
||
|
def __init__(self, value):
|
||
|
self.parameter = value
|
||
|
|
||
|
def __str__(self):
|
||
|
return repr(self.parameter)
|
||
|
|
||
|
|
||
|
class Packer(object):
|
||
|
def pad_string(self, string):
|
||
|
'''Pads the given string with \x00s to fill a multiple of 4 length.'''
|
||
|
if string == '':
|
||
|
return ''
|
||
|
i = 0
|
||
|
while len(string) >= i:
|
||
|
i += 4
|
||
|
return string.ljust(i, '\x00')
|
||
|
|
||
|
def null_terminate(self, string):
|
||
|
'''Null-terminates the given string.'''
|
||
|
return string + '\x00'
|
||
|
|
||
|
def pack_long_chunk(self, header, number):
|
||
|
'''Chunk with only one long int.'''
|
||
|
data = [header]
|
||
|
data.append(struct.pack('<L', 4))
|
||
|
data.append(struct.pack('<L', number))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack_string_chunk(self, header, string):
|
||
|
'''String Chunk(NAME, TX0D...) packer.'''
|
||
|
data = [header]
|
||
|
comp_str = self.pad_string(string.encode('ascii'))
|
||
|
data.append(struct.pack('<L', len(comp_str)))
|
||
|
data.append(comp_str)
|
||
|
return ''.join(data)
|
||
|
|
||
|
def sum_seq(self, seq, start=0, end=None):
|
||
|
'''Sums the sequence items from start to end.
|
||
|
If end is None it will sum all items from start.'''
|
||
|
if end:
|
||
|
tosum = seq[start:end]
|
||
|
else:
|
||
|
tosum = seq[start:]
|
||
|
tosum = [len(item) for item in tosum]
|
||
|
return sum(tosum)
|
||
|
|
||
|
|
||
|
class Msh(Packer):
|
||
|
# is_shadowvolume bool
|
||
|
# info Info
|
||
|
# materials Material[]
|
||
|
# models Model[]
|
||
|
# animation Animation
|
||
|
def __init__(self):
|
||
|
self.info = None
|
||
|
self.has_shadowvolume = False
|
||
|
self.models = []
|
||
|
self.materials = []
|
||
|
self.animation = None
|
||
|
self.classname = 'Msh'
|
||
|
self.modulepath = ''
|
||
|
|
||
|
def dump(self, filepath):
|
||
|
'''Dumps .msh file information into a text file.'''
|
||
|
with open(filepath, 'w') as fh:
|
||
|
self.info.dump(fh)
|
||
|
self.materials.dump(fh)
|
||
|
self.models.dump(fh)
|
||
|
return True
|
||
|
|
||
|
def save(self, filepath):
|
||
|
with open(filepath, 'wb') as fh:
|
||
|
fh.write(self.pack())
|
||
|
|
||
|
def save_json(self, filepath):
|
||
|
'''Saves the .msh in JSON format.'''
|
||
|
data = {
|
||
|
'info': self.info.get_json(),
|
||
|
'models': self.models.get_json(),
|
||
|
'materials': self.materials.get_json(),
|
||
|
'has_shadowvolume': self.has_shadowvolume,
|
||
|
'animation': None,
|
||
|
}
|
||
|
with open(filepath, 'wb') as fh:
|
||
|
fh.write(json.dumps(data, indent=4, separators=(',', ': ')))
|
||
|
return True
|
||
|
|
||
|
def save_segmented_json(self, folder, name=None):
|
||
|
# If no name argument was passed assume that a full path was passed.
|
||
|
if not name:
|
||
|
folder, name = os.path.dirname(folder), os.path.basename(folder)
|
||
|
name = name.split('.')[0]
|
||
|
if not os.path.isdir(folder):
|
||
|
os.mkdir(folder)
|
||
|
with open(os.path.join(folder, name + '.txt'), 'w') as fh:
|
||
|
fh.write(json.dumps({'has_shadowvolume': self.has_shadowvolume}))
|
||
|
with open(os.path.join(folder, name + ' nfo.txt'), 'w') as fh:
|
||
|
fh.write(json.dumps(self.info.get_json(), indent=4, separators=(',', ': ')))
|
||
|
for model in self.models:
|
||
|
model.save_segmented_json(folder, name)
|
||
|
for material in self.materials:
|
||
|
with open(os.path.join(folder, '{0} mtl {1}.txt'.format(name, material.name)), 'wb') as fh:
|
||
|
fh.write(json.dumps(material.get_json(), indent=4, separators=(',', ': ')))
|
||
|
|
||
|
@classmethod
|
||
|
def load_segmented_json(cls, folder, name):
|
||
|
model_start = '{0} mdl '.format(name)
|
||
|
material_start = '{0} mtl '.format(name)
|
||
|
msh = cls()
|
||
|
info_data = ''
|
||
|
with open(os.path.join(folder, '{0}.txt'.format(name)), 'r') as fh:
|
||
|
msh.has_shadowvolume = json.loads(fh.read())['has_shadowvolume']
|
||
|
with open(os.path.join(folder, '{0} nfo.txt'.format(name)), 'r') as fh:
|
||
|
info_data = json.loads(fh.read())
|
||
|
msh.info = SceneInfo.from_json(info_data)
|
||
|
materials = MaterialCollection(msh)
|
||
|
models = ModelCollection(msh)
|
||
|
for filename in os.listdir(folder):
|
||
|
if filename.startswith(model_start) and (not ' seg ' in filename):
|
||
|
models.add(Model.load_segmented_json(folder, filename.split('.')[0]))
|
||
|
elif filename.startswith(material_start):
|
||
|
with open(os.path.join(folder, filename), 'r') as fh:
|
||
|
materials.add(Material.from_json(json.loads(fh.read())))
|
||
|
msh.models = models
|
||
|
# Re-order models so that parents appear before their children.
|
||
|
models_map = {}
|
||
|
models = []
|
||
|
for model in msh.models:
|
||
|
if model.parent_name:
|
||
|
if not models_map.get(model.parent_name, None):
|
||
|
models_map[model.parent_name] = [model]
|
||
|
else:
|
||
|
models_map[model.parent_name].append(model)
|
||
|
else:
|
||
|
models.append(model)
|
||
|
new_models = []
|
||
|
for model in models:
|
||
|
new_models.extend(msh.get_children_ordered(model, models_map))
|
||
|
msh.models.replace(new_models)
|
||
|
msh.materials = materials
|
||
|
msh.animation = Animation(None, 'empty')
|
||
|
msh.set_indices()
|
||
|
return msh
|
||
|
|
||
|
def get_children_ordered(self, model, children_map):
|
||
|
models = []
|
||
|
models.append(model)
|
||
|
children = children_map.get(model.name, [])
|
||
|
for child in children:
|
||
|
models.extend(self.get_children_ordered(child, children_map))
|
||
|
return models
|
||
|
|
||
|
@classmethod
|
||
|
def load_json(cls, filepath):
|
||
|
data = ''
|
||
|
with open(filepath, 'rb') as fh:
|
||
|
data = fh.read()
|
||
|
data = json.loads(data)
|
||
|
msh = cls()
|
||
|
msh.info = SceneInfo.from_json(data['info'], msh)
|
||
|
msh.models = ModelCollection.from_json(data['models'], msh)
|
||
|
msh.materials = MaterialCollection.from_json(data['materials'], msh)
|
||
|
msh.has_shadowvolume = data['has_shadowvolume']
|
||
|
msh.animation = Animation(None, 'empty')
|
||
|
msh.set_indices()
|
||
|
return msh
|
||
|
|
||
|
def set_indices(self):
|
||
|
'''Set material and model indices (ie after json load).'''
|
||
|
for index, material in enumerate(self.materials):
|
||
|
material.index = index
|
||
|
for index, model in enumerate(self.models):
|
||
|
model.index = index
|
||
|
for seg in model.segments:
|
||
|
seg.material = self.get_mat_by_name(seg.mat_name)
|
||
|
|
||
|
def get_mat_by_name(self, name):
|
||
|
'''Get Material object by name.'''
|
||
|
dct = self.materials.get_matname_dict()
|
||
|
return dct[name]
|
||
|
|
||
|
def get_mat_by_index(self, index):
|
||
|
'''Get Material object by index into the colection.'''
|
||
|
return self.materials[index]
|
||
|
|
||
|
def pack_CL1L(self):
|
||
|
data = ['CL1L']
|
||
|
data.append(struct.pack('<L', 0))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack(self):
|
||
|
'''Packs data to .msh file structure and returns the data as a string.'''
|
||
|
data = ['HEDR',
|
||
|
'hedr_len']
|
||
|
# First pack the MSH2 header with all its children chunks.
|
||
|
msh2_data = ['MSH2', 'len']
|
||
|
msh2_data.append(self.info.pack())
|
||
|
msh2_data.append(self.materials.pack())
|
||
|
msh2_data.append(self.models.pack())
|
||
|
#msh2_data[1] = struct.pack('<L', len(msh2_data[2]) + len(msh2_data[3]) + len(msh2_data[4]))
|
||
|
msh2_data[1] = struct.pack('<L', self.sum_seq(msh2_data, 2))
|
||
|
# Finished MSH2.
|
||
|
data.append(''.join(msh2_data))
|
||
|
# HEDR only has MSH2 and the animation chunks as children(and sometimes SHVO).
|
||
|
# We don't pack SHVO here so on to animation chunks.
|
||
|
data.append(self.animation.pack())
|
||
|
data.append(self.pack_CL1L())
|
||
|
# sum length of msh2 data + length of animation chunks.
|
||
|
data[1] = struct.pack('<L', self.sum_seq(data, 2))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def repack(self):
|
||
|
'''Will use stored binary data of cloth and shadow chunks instead
|
||
|
of recalculating it(not possible at the moment).'''
|
||
|
data = ['HEDR',
|
||
|
'hedr_len']
|
||
|
# First pack the MSH2 header with all its children chunks.
|
||
|
msh2_data = ['MSH2', 'len']
|
||
|
msh2_data.append(self.info.pack())
|
||
|
msh2_data.append(self.materials.pack())
|
||
|
msh2_data.append(self.models.repack())
|
||
|
#msh2_data[1] = struct.pack('<L', len(msh2_data[2]) + len(msh2_data[3]) + len(msh2_data[4]))
|
||
|
msh2_data[1] = struct.pack('<L', self.sum_seq(msh2_data, 2))
|
||
|
# Finished MSH2.
|
||
|
data.append(''.join(msh2_data))
|
||
|
# HEDR only has MSH2 and the animation chunks as children(and sometimes SHVO).
|
||
|
# We don't pack SHVO here so on to animation chunks.
|
||
|
if self.animation:
|
||
|
data.append(self.animation.pack())
|
||
|
data.append(self.pack_CL1L())
|
||
|
# sum length of msh2 data + length of animation chunks.
|
||
|
data[1] = struct.pack('<L', self.sum_seq(data, 2))
|
||
|
return ''.join(data)
|
||
|
|
||
|
|
||
|
class SceneInfo(Packer):
|
||
|
def __init__(self, msh=None):
|
||
|
# Reference to Msh.
|
||
|
if msh:
|
||
|
self.msh = msh
|
||
|
else:
|
||
|
self.msh = None
|
||
|
self.name = ''
|
||
|
self.classname = 'SceneInfo'
|
||
|
self.frame_range = 0, 100
|
||
|
self.fps = 29.97
|
||
|
self.scale = 1.0, 1.0, 1.0
|
||
|
self.bbox = BBox()
|
||
|
|
||
|
def dump(self, fileh=None):
|
||
|
'''Dump information to open filehandler fileh.'''
|
||
|
if fileh:
|
||
|
fileh.write('--- SceneInfo ---\n')
|
||
|
fileh.write('\tSceneName: {0}\n'.format(self.name))
|
||
|
fileh.write('\tFrameRange: {0}-{1}\n'.format(*self.frame_range))
|
||
|
fileh.write('\tFPS: {0}\n'.format(self.fps))
|
||
|
|
||
|
def get_json(self):
|
||
|
data = {
|
||
|
'name': self.name,
|
||
|
'frame_range': self.frame_range,
|
||
|
'fps': self.fps,
|
||
|
'scale': self.scale,
|
||
|
'bbox': self.bbox.get_json()
|
||
|
}
|
||
|
return data
|
||
|
|
||
|
@staticmethod
|
||
|
def from_json(data, msh=None):
|
||
|
info = SceneInfo()
|
||
|
info.msh = msh
|
||
|
info.name = data['name']
|
||
|
info.frame_range = data['frame_range']
|
||
|
info.fps = data['fps']
|
||
|
info.scale = data['scale']
|
||
|
info.bbox = BBox.from_json(data['bbox'])
|
||
|
return info
|
||
|
|
||
|
def pack(self):
|
||
|
'''Packs the scene information data.'''
|
||
|
data = ['SINF']
|
||
|
data.append('size_ind')
|
||
|
data.append(self.pack_NAME())
|
||
|
data.append(self.pack_FRAM())
|
||
|
data.append(self.bbox.pack())
|
||
|
data[1] = struct.pack('<L', len(data[2]) + len(data[3]) + len(data[4]))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack_NAME(self):
|
||
|
'''Packs the scene name.'''
|
||
|
return self.pack_string_chunk('NAME', self.name)
|
||
|
|
||
|
def pack_FRAM(self):
|
||
|
'''Packs frame range and FPS into the FRAM chunk.'''
|
||
|
data = ['FRAM']
|
||
|
data.append(struct.pack('<L', 12))
|
||
|
data.append(struct.pack('<L', self.frame_range[0]))
|
||
|
data.append(struct.pack('<L', self.frame_range[1]))
|
||
|
data.append(struct.pack('<f', self.fps))
|
||
|
return ''.join(data)
|
||
|
|
||
|
|
||
|
class Material(Packer):
|
||
|
# name: string
|
||
|
# index: int
|
||
|
# tex_0: string.tga
|
||
|
# tex_1: string.tga
|
||
|
# tex_2: string.tga
|
||
|
# tex_3: string.tga
|
||
|
# attr_0: int(0-255)
|
||
|
# attr_1: int(0-255)
|
||
|
# attr_2: int(0-255)
|
||
|
# attr_3: int(0-255)
|
||
|
# diff_color: (red, green, blue, alpha) floats
|
||
|
# ambt_color: (red, green, blue, alpha) floats
|
||
|
# spec_color: (red, green, blue, alpha) floats
|
||
|
# gloss: float
|
||
|
def __init__(self, coll=None):
|
||
|
# Reference to MaterialCollection
|
||
|
if coll:
|
||
|
self.collection = coll
|
||
|
else:
|
||
|
self.collection = None
|
||
|
self.name = ''
|
||
|
self.classname = 'Material'
|
||
|
self.index = 0
|
||
|
self.tex0 = None
|
||
|
self.tex1 = None
|
||
|
self.tex2 = None
|
||
|
self.tex3 = None
|
||
|
self.flags = [['specular', False, 128],
|
||
|
['additive', False, 64],
|
||
|
['perpixel', False, 32],
|
||
|
['hard', False, 16],
|
||
|
['double', False, 8],
|
||
|
['single', False, 4],
|
||
|
['glow', False, 2],
|
||
|
['emissive', False, 1]]
|
||
|
self.render_type = 0
|
||
|
self.data0 = 0
|
||
|
self.data1 = 0
|
||
|
self.diff_color = Color()
|
||
|
self.ambt_color = Color()
|
||
|
self.spec_color = Color()
|
||
|
self.gloss = 70.0
|
||
|
|
||
|
def dump(self, fh):
|
||
|
'''Dump information to open filehandler fileh.'''
|
||
|
fh.write('\t--- Material ---\n')
|
||
|
fh.write('\t\tName: {0}\n'.format(self.name))
|
||
|
fh.write('\t\tTex0: {0}\n'.format(self.tex0))
|
||
|
fh.write('\t\tTex1: {0}\n'.format(self.tex1))
|
||
|
fh.write('\t\tTex2: {0}\n'.format(self.tex2))
|
||
|
fh.write('\t\tTex3: {0}\n'.format(self.tex3))
|
||
|
fh.write('\t\tFlags\n')
|
||
|
for flag in self.flags:
|
||
|
fh.write('\t\t\t{0}: {1}\n'.format(flag[0], flag[1]))
|
||
|
fh.write('\n')
|
||
|
fh.write('\t\tRenderType: {0}\n'.format(self.render_type))
|
||
|
fh.write('\t\tData0: {0}\n'.format(self.data0))
|
||
|
fh.write('\t\tData1: {0}\n'.format(self.data1))
|
||
|
fh.write('\t\tDiffuse: {0}\n'.format(self.diff_color))
|
||
|
fh.write('\t\tSpecular: {0}\n'.format(self.spec_color))
|
||
|
fh.write('\t\tAmbient: {0}\n'.format(self.ambt_color))
|
||
|
fh.write('\t\tGloss: {0}\n'.format(self.gloss))
|
||
|
|
||
|
def get_json(self):
|
||
|
data = {
|
||
|
'name': self.name,
|
||
|
'index': self.index,
|
||
|
'tex0': self.tex0,
|
||
|
'tex1': self.tex1,
|
||
|
'tex2': self.tex2,
|
||
|
'tex3': self.tex3,
|
||
|
'flags': self.flags,
|
||
|
'render_type': self.render_type,
|
||
|
'data0': self.data0,
|
||
|
'data1': self.data1,
|
||
|
'diffuse': self.diff_color.get_json(),
|
||
|
'specular': self.spec_color.get_json(),
|
||
|
'ambient': self.ambt_color.get_json(),
|
||
|
'gloss': self.gloss,
|
||
|
}
|
||
|
return data
|
||
|
|
||
|
@staticmethod
|
||
|
def from_json(data, collection=None):
|
||
|
material = Material()
|
||
|
material.collection = collection
|
||
|
material.name = data['name']
|
||
|
material.index = data['index']
|
||
|
material.tex0 = data['tex0']
|
||
|
material.tex1 = data['tex1']
|
||
|
material.tex2 = data['tex2']
|
||
|
material.tex3 = data['tex3']
|
||
|
material.flags = data['flags']
|
||
|
material.render_type = data['render_type']
|
||
|
material.data0 = data['data0']
|
||
|
material.data1 = data['data1']
|
||
|
material.diff_color = Color.from_json(data['diffuse'])
|
||
|
material.spec_color = Color.from_json(data['specular'])
|
||
|
material.ambt_color = Color.from_json(data['ambient'])
|
||
|
material.gloss = data['gloss']
|
||
|
return material
|
||
|
|
||
|
@property
|
||
|
def ATRB(self):
|
||
|
'''Sum ATRB byte value.'''
|
||
|
val = 0
|
||
|
for flag in self.flags:
|
||
|
if flag[1]:
|
||
|
val += flag[2]
|
||
|
return val
|
||
|
|
||
|
def pack(self):
|
||
|
'''Packs the material into a MATD chunk.'''
|
||
|
data = ['MATD']
|
||
|
data.append('size_indicator')
|
||
|
data.append(self.pack_NAME())
|
||
|
data.append(self.pack_DATA())
|
||
|
data.append(self.pack_ATRB())
|
||
|
data.append(self.pack_textures())
|
||
|
data[1] = struct.pack('<L', len(data[2]) + len(data[3]) + len(data[4]) + len(data[5]))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def flags_from_int(self, val):
|
||
|
'''Unpacks an int indicating the material flags into the
|
||
|
single flags.'''
|
||
|
# Decodes an int into flags.
|
||
|
place0 = val
|
||
|
flags = [('specular', True, 128),
|
||
|
('additive', True, 64),
|
||
|
('perpixel', True, 32),
|
||
|
('hard', True, 16),
|
||
|
('double', True, 8),
|
||
|
('single', True, 4),
|
||
|
('glow', True, 2),
|
||
|
('emissive', True, 1)]
|
||
|
new = place0
|
||
|
for index, flag in enumerate(flags):
|
||
|
new -= flag[2]
|
||
|
if new < 0:
|
||
|
flags[index] = flag[0], False, flag[2]
|
||
|
new += flag[2]
|
||
|
return flags
|
||
|
|
||
|
def pack_NAME(self):
|
||
|
'''Packs the materials name.'''
|
||
|
return self.pack_string_chunk('NAME', self.name)
|
||
|
|
||
|
def pack_DATA(self):
|
||
|
'''Packs the material's shader/color data.'''
|
||
|
data = ['DATA']
|
||
|
data.append(struct.pack('<L', 52))
|
||
|
data.append(self.diff_color.pack('f'))
|
||
|
data.append(self.spec_color.pack('f'))
|
||
|
data.append(self.ambt_color.pack('f'))
|
||
|
data.append(struct.pack('<f', self.gloss))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack_ATRB(self):
|
||
|
'''Packs the material render attributes.'''
|
||
|
data = ['ATRB']
|
||
|
data.append(struct.pack('<L', 4))
|
||
|
data.append(struct.pack('<B', self.ATRB))
|
||
|
data.append(struct.pack('<B', self.render_type))
|
||
|
data.append(struct.pack('<B', self.data0))
|
||
|
data.append(struct.pack('<B', self.data1))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack_textures(self):
|
||
|
'''Packs up to 4 textures in TX0D/TX1D/TX2D/TX3D chunks.'''
|
||
|
data = []
|
||
|
if self.tex0:
|
||
|
data.append(self.pack_string_chunk('TX0D', self.tex0))
|
||
|
if self.tex1:
|
||
|
data.append(self.pack_string_chunk('TX1D', self.tex1))
|
||
|
if self.tex2:
|
||
|
data.append(self.pack_string_chunk('TX2D', self.tex2))
|
||
|
if self.tex3:
|
||
|
data.append(self.pack_string_chunk('TX3D', self.tex3))
|
||
|
return ''.join(data)
|
||
|
|
||
|
|
||
|
class MaterialCollection(Packer):
|
||
|
def __init__(self, msh=None, materials=None):
|
||
|
# Reference to Msh.
|
||
|
if msh:
|
||
|
self.msh = msh
|
||
|
else:
|
||
|
self.msh = None
|
||
|
self.classname = 'MaterialCollection'
|
||
|
if materials:
|
||
|
self.materials = materials
|
||
|
else:
|
||
|
self.materials = []
|
||
|
|
||
|
def dump(self, fh):
|
||
|
'''Dump information to open filehandler fileh.'''
|
||
|
fh.write('--- MaterialCollection ---\n')
|
||
|
fh.write('\tNumMaterials: {0}\n'.format(len(self.materials)))
|
||
|
for mat in self.materials:
|
||
|
mat.dump(fh)
|
||
|
|
||
|
def get_json(self):
|
||
|
data = {
|
||
|
'materials': [mat.get_json() for mat in self.materials],
|
||
|
}
|
||
|
return data
|
||
|
|
||
|
@staticmethod
|
||
|
def from_json(data, msh=None):
|
||
|
coll = MaterialCollection()
|
||
|
coll.msh = msh
|
||
|
for material_data in data['materials']:
|
||
|
coll.materials.append(Material.from_json(material_data))
|
||
|
return coll
|
||
|
|
||
|
def add(self, material):
|
||
|
'''Add Material to the collection and set
|
||
|
collection attribute.'''
|
||
|
material.collection = self
|
||
|
self.materials.append(material)
|
||
|
|
||
|
def remove(self, index):
|
||
|
'''Remove material at index index.'''
|
||
|
del self.materials[index]
|
||
|
|
||
|
def replace(self, materials):
|
||
|
'''Replace internal materials list.'''
|
||
|
self.materials = materials
|
||
|
|
||
|
def get_matname_dict(self):
|
||
|
'''Returns dict of 'materialname': material pairs.'''
|
||
|
matnamedict = {}
|
||
|
for mat in self.materials:
|
||
|
matnamedict[mat.name] = mat
|
||
|
return matnamedict
|
||
|
|
||
|
def assign_indices(self):
|
||
|
'''Assigns index attribute for every material.'''
|
||
|
for index, mat in enumerate(self.materials):
|
||
|
mat.index = index
|
||
|
|
||
|
def __str__(self):
|
||
|
return str(self.materials)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return str(self.materials)
|
||
|
|
||
|
def __getitem__(self, key):
|
||
|
return self.materials[key]
|
||
|
|
||
|
def __len__(self):
|
||
|
return len(self.materials)
|
||
|
|
||
|
def pack(self):
|
||
|
'''Packs the material list + materials.'''
|
||
|
data = ['MATL', 'size']
|
||
|
data.append(struct.pack('<L', len(self.materials)))
|
||
|
for material in self.materials:
|
||
|
data.append(material.pack())
|
||
|
data[1] = struct.pack('<L', self.sum_seq(data, 3) + 4)
|
||
|
return ''.join(data)
|
||
|
|
||
|
|
||
|
class Model(Packer):
|
||
|
collprim_by_index = {0: 'Sphere',
|
||
|
1: 'Sphere',
|
||
|
2: 'Cylinder',
|
||
|
4: 'Cube'}
|
||
|
collprim_by_name = {'Sphere': 0,
|
||
|
'Cylinder': 2,
|
||
|
'Cube': 4}
|
||
|
|
||
|
def __init__(self, collection=None):
|
||
|
# Ref to ModelCollection.
|
||
|
if collection:
|
||
|
self.collection = collection
|
||
|
else:
|
||
|
self.collection = None
|
||
|
self.name = 'model'
|
||
|
self.classname = 'Model'
|
||
|
self.parent_name = None
|
||
|
self.index = 0
|
||
|
self.model_type = 'geostatic'
|
||
|
self.vis = 0
|
||
|
self.segments = SegmentCollection(self)
|
||
|
self.collprim = False
|
||
|
self.cloth_collprim = False
|
||
|
self.primitive = 4, 0.0, 0.0, 0.0
|
||
|
self.deformers = []
|
||
|
self.deformer_indices = []
|
||
|
self.bbox = BBox()
|
||
|
self.transform = Transform()
|
||
|
self.bone = None
|
||
|
self.msh = None
|
||
|
|
||
|
def get_json(self):
|
||
|
data = {
|
||
|
'name': self.name,
|
||
|
'parent': self.parent_name,
|
||
|
'model_type': self.model_type,
|
||
|
'visible': not self.vis, # vis == hidden
|
||
|
'segments': self.segments.get_json(),
|
||
|
'collprim': self.collprim,
|
||
|
'primitive': self.primitive,
|
||
|
'num_deformers': len(self.deformers),
|
||
|
'deformers': self.deformers,
|
||
|
'bbox': self.bbox.get_json(),
|
||
|
'transform': self.transform.get_json(),
|
||
|
}
|
||
|
return data
|
||
|
|
||
|
def save_segmented_json(self, folder, name):
|
||
|
data = self.get_json()
|
||
|
del data['segments']
|
||
|
with open(os.path.join(folder, '{0} mdl {1}.txt'.format(name, self.name)), 'w') as fh:
|
||
|
fh.write(json.dumps(data, indent=4, separators=(',', ': ')))
|
||
|
for index, segment in enumerate(self.segments):
|
||
|
with open(os.path.join(folder, '{0} mdl {1} seg {2:0>3}.txt'.format(name, self.name, index)), 'w') as fh:
|
||
|
fh.write(json.dumps(segment.get_json(), indent=4, separators=(',', ': ')))
|
||
|
|
||
|
@classmethod
|
||
|
def load_segmented_json(cls, folder, name):
|
||
|
logging.info('LOADING SEGMENTED MODEL %s %s', folder, name)
|
||
|
with open(os.path.join(folder, '{0}.txt'.format(name)), 'r') as fh:
|
||
|
model = Model.from_json(json.loads(fh.read()))
|
||
|
seg_start = '{0} seg '.format(name)
|
||
|
segments = SegmentCollection(model)
|
||
|
for filename in os.listdir(folder):
|
||
|
if filename.startswith(seg_start):
|
||
|
with open(os.path.join(folder, filename), 'r') as fh:
|
||
|
data = json.loads(fh.read())
|
||
|
if data['type'] == 'SegmentGeometry':
|
||
|
segments.add(SegmentGeometry.from_json(data))
|
||
|
elif data['type'] == 'ClothGeometry':
|
||
|
segments.add(ClothGeometry.from_json(data))
|
||
|
elif data['type'] == 'ShadowGeometry':
|
||
|
segments.add(ShadowGeometry.from_json(data))
|
||
|
model.segments = segments
|
||
|
return model
|
||
|
|
||
|
@staticmethod
|
||
|
def from_json(data, collection=None):
|
||
|
model = Model()
|
||
|
model.collection = collection
|
||
|
model.name = data['name']
|
||
|
model.parent_name = data['parent']
|
||
|
model.model_type = data['model_type']
|
||
|
model.vis = data.get('visible', False)
|
||
|
model.vis = data.get('vis', model.vis) # Older format used that.
|
||
|
# In case this is loaded from a segmented json.
|
||
|
if data.get('segments', None):
|
||
|
model.segments = SegmentCollection.from_json(data['segments'], model)
|
||
|
model.collprim = data['collprim']
|
||
|
model.primitive = data['primitive']
|
||
|
model.deformers = data['deformers']
|
||
|
model.bbox = BBox.from_json(data['bbox'])
|
||
|
model.transform = Transform.from_json(data['transform'])
|
||
|
return model
|
||
|
|
||
|
def get_collprim_name(self):
|
||
|
return self.collprim_by_index[self.primitive[0]]
|
||
|
|
||
|
def get_collprim_index(self, name):
|
||
|
return self.collprim_by_name[name]
|
||
|
|
||
|
def dump(self, fh):
|
||
|
'''Dump information to open filehandler fileh.'''
|
||
|
fh.write('\t--- Model ---\n')
|
||
|
fh.write('\t\tName: {0}\n'.format(self.name))
|
||
|
fh.write('\t\tParent: {0}\n'.format(self.parent_name))
|
||
|
fh.write('\t\tType: {0}\n'.format(self.model_type))
|
||
|
fh.write('\t\tVisible: {0}\n'.format(self.vis))
|
||
|
fh.write('\t\tPos: {0}, {1}, {2}\n'.format(*self.transform.translation))
|
||
|
fh.write('\t\tRot: {0}, {1}, {2}, {3}\n'.format(*self.transform.rotation))
|
||
|
fh.write('\t\tScl: {0}, {1}, {2}\n'.format(*self.transform.scale))
|
||
|
if self.collprim:
|
||
|
fh.write('\t\tCollprim: {0}, {1}, {2}, {3}\n'.format(*self.primitive))
|
||
|
if 'geo' in self.model_type or self.model_type == 'cloth':
|
||
|
self.segments.dump(fh)
|
||
|
if self.deformers:
|
||
|
fh.write('\t\tDeformers:\n')
|
||
|
for deform in self.deformers:
|
||
|
fh.write('\t\t\t{0}\n'.format(deform))
|
||
|
|
||
|
def set_deformers_from_indices(self):
|
||
|
'''Sets deformers attribute to actual model names.'''
|
||
|
try:
|
||
|
for ind in self.deformer_indices:
|
||
|
self.deformers.append(self.collection[ind - 1].name)
|
||
|
except IndexError:
|
||
|
raise MSH2Error('Check C:\dump.dump!')
|
||
|
|
||
|
def pack(self):
|
||
|
'''Packs the MODL chunk. This should be used to retrieve the model in packed form.'''
|
||
|
data = ['MODL', 'sizeind']
|
||
|
data.append(self.pack_MTYP())
|
||
|
data.append(self.pack_MNDX())
|
||
|
data.append(self.pack_NAME())
|
||
|
if self.parent_name:
|
||
|
data.append(self.pack_PRNT())
|
||
|
data.append(self.pack_FLGS())
|
||
|
data.append(self.transform.pack())
|
||
|
if 'geo' in self.model_type or self.model_type == 'cloth':
|
||
|
data.append(self.pack_GEOM())
|
||
|
if self.collprim:
|
||
|
data.append(self.pack_SWCI())
|
||
|
data[1] = struct.pack('<L', self.sum_seq(data, 2))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack_MTYP(self):
|
||
|
'''Packs the model type chunk.'''
|
||
|
return self.pack_long_chunk('MTYP', MODEL_TYPES[self.model_type])
|
||
|
|
||
|
def pack_MNDX(self):
|
||
|
'''Packs the model index chunk.'''
|
||
|
return self.pack_long_chunk('MNDX', self.index)
|
||
|
|
||
|
def pack_NAME(self):
|
||
|
'''Packs the model's name.'''
|
||
|
return self.pack_string_chunk('NAME', self.name)
|
||
|
|
||
|
def pack_PRNT(self):
|
||
|
'''Packs the model's parent's name.'''
|
||
|
return self.pack_string_chunk('PRNT', self.parent_name)
|
||
|
|
||
|
def pack_FLGS(self):
|
||
|
'''Packs the visibility.'''
|
||
|
return self.pack_long_chunk('FLGS', self.vis)
|
||
|
|
||
|
def pack_GEOM(self):
|
||
|
data = ['GEOM']
|
||
|
data.append('size_indic')
|
||
|
data.append(self.bbox.pack())
|
||
|
data.append(self.segments.pack())
|
||
|
if self.deformers:
|
||
|
data.append(self.pack_ENVL())
|
||
|
data[1] = struct.pack('<L', self.sum_seq(data, 2))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack_ENVL(self):
|
||
|
data = ['ENVL']
|
||
|
data.append(struct.pack('<L', len(self.deformers) * 4 + 4))
|
||
|
data.append(struct.pack('<L', len(self.deformers)))
|
||
|
for deformer in self.deformers:
|
||
|
index = self.collection.get_index(deformer)
|
||
|
data.append(struct.pack('<L', index))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack_SWCI(self):
|
||
|
'''Packs the collision primitive chunk.'''
|
||
|
data = ['SWCI']
|
||
|
data.append(struct.pack('<L', 16))
|
||
|
data.append(struct.pack('<Lfff', *self.primitive))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def repack(self):
|
||
|
'''Repacks the MODL chunk. This should be used to retrieve the model in packed form.'''
|
||
|
data = ['MODL', 'sizeind']
|
||
|
data.append(self.pack_MTYP())
|
||
|
data.append(self.pack_MNDX())
|
||
|
data.append(self.pack_NAME())
|
||
|
if self.parent_name:
|
||
|
data.append(self.pack_PRNT())
|
||
|
data.append(self.pack_FLGS())
|
||
|
data.append(self.transform.pack())
|
||
|
if 'geo' in self.model_type or self.model_type == 'cloth':
|
||
|
data.append(self.repack_GEOM())
|
||
|
if self.collprim:
|
||
|
data.append(self.pack_SWCI())
|
||
|
data[1] = struct.pack('<L', self.sum_seq(data, 2))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def repack_GEOM(self):
|
||
|
data = ['GEOM']
|
||
|
data.append('size_indic')
|
||
|
data.append(self.bbox.pack())
|
||
|
data.append(self.segments.repack())
|
||
|
if self.deformers:
|
||
|
data.append(self.pack_ENVL())
|
||
|
data[1] = struct.pack('<L', self.sum_seq(data, 2))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def get_vertex_by_index(self, vertex_index):
|
||
|
'''Get a Vertex from an index that is not relative to a segment.
|
||
|
Basically combines all vertices of all segments into one list to array into.'''
|
||
|
offset = 0
|
||
|
for segment in self.segments:
|
||
|
if vertex_index < len(segment.vertices) + offset:
|
||
|
return segment.vertices[vertex_index - offset]
|
||
|
offset += len(segment.vertices)
|
||
|
|
||
|
|
||
|
class ModelCollection(object):
|
||
|
def __init__(self, msh=None, models=None):
|
||
|
# Ref to Msh.
|
||
|
if msh:
|
||
|
self.msh = msh
|
||
|
else:
|
||
|
self.msh = None
|
||
|
self.classname = 'ModelCollection'
|
||
|
if models:
|
||
|
self.models = models
|
||
|
else:
|
||
|
self.models = []
|
||
|
self.msh = None
|
||
|
|
||
|
def get_json(self):
|
||
|
data = {
|
||
|
'models': [m.get_json() for m in self.models]
|
||
|
}
|
||
|
return data
|
||
|
|
||
|
@staticmethod
|
||
|
def from_json(data, msh=None):
|
||
|
coll = ModelCollection()
|
||
|
coll.msh = msh
|
||
|
for model_data in data['models']:
|
||
|
coll.models.append(Model.from_json(model_data, coll))
|
||
|
return coll
|
||
|
|
||
|
def dump(self, fh):
|
||
|
'''Dump information to open filehandler fh.'''
|
||
|
fh.write('--- ModelCollection ---\n')
|
||
|
fh.write('\tNumModels: {0}\n'.format(len(self.models)))
|
||
|
for model in self.models:
|
||
|
model.dump(fh)
|
||
|
|
||
|
def add(self, model):
|
||
|
'''Add Model object and set its collection
|
||
|
attribute.'''
|
||
|
model.collection = self
|
||
|
self.models.append(model)
|
||
|
|
||
|
def remove(self, index):
|
||
|
del self.models[index]
|
||
|
|
||
|
def remove_multi(self, models):
|
||
|
'''Compares names between internal list and
|
||
|
models. If name of internal Model equals one in
|
||
|
models, remove the Model.'''
|
||
|
for index, model in enumerate(self.models):
|
||
|
if model.name in models:
|
||
|
del self.models[index]
|
||
|
|
||
|
def replace(self, new_models):
|
||
|
'''Replace internal model list with new_models.'''
|
||
|
self.models = new_models
|
||
|
|
||
|
def assign_indices(self):
|
||
|
'''Assign index attribute for every Model.'''
|
||
|
for index, model in enumerate(self.models):
|
||
|
model.index = index + 1
|
||
|
|
||
|
def assign_parents(self):
|
||
|
'''Assign collection attribute for every Model.'''
|
||
|
for model in self.models:
|
||
|
model.collection = self
|
||
|
|
||
|
def assign_cloth_collisions(self):
|
||
|
'''Assign collision data to every cloth that has collisions referenced.'''
|
||
|
cloth_collision_primitives = {}
|
||
|
for model in self.models:
|
||
|
if model.cloth_collprim:
|
||
|
cloth_collision_primitives[model.name] = model
|
||
|
|
||
|
for model in self.models:
|
||
|
for segment in model.segments:
|
||
|
if isinstance(segment, ClothGeometry):
|
||
|
for collision in segment.collisions:
|
||
|
collision.primitive_type = cloth_collision_primitives[collision.name].primitive[0]
|
||
|
collision.primitive_data = cloth_collision_primitives[collision.name].primitive[1:]
|
||
|
|
||
|
def get_index(self, modelname):
|
||
|
'''Get model.index for model with name modelname.'''
|
||
|
for model in self.models:
|
||
|
if model.name == modelname:
|
||
|
logging.debug('ModelCollection.get_index: {0} - {1}'.format(model.name, model.index))
|
||
|
return model.index
|
||
|
return 0
|
||
|
|
||
|
def by_name(self, name):
|
||
|
'''Get model by name.'''
|
||
|
for model in self.models:
|
||
|
if model.name == name:
|
||
|
return model
|
||
|
|
||
|
def get_names_list(self):
|
||
|
'''Get list of model names.'''
|
||
|
return [mdl.name for mdl in self.models]
|
||
|
|
||
|
def __str__(self):
|
||
|
return str(self.models)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return str(self.models)
|
||
|
|
||
|
def __getitem__(self, key):
|
||
|
return self.models[key]
|
||
|
|
||
|
def __len__(self):
|
||
|
return len(self.models)
|
||
|
|
||
|
def pack(self):
|
||
|
data = [model.pack() for model in self.models]
|
||
|
return ''.join(data)
|
||
|
|
||
|
def repack(self):
|
||
|
data = [model.repack() for model in self.models]
|
||
|
return ''.join(data)
|
||
|
|
||
|
|
||
|
class SegmentCollection(object):
|
||
|
def __init__(self, model=None, segments=None):
|
||
|
# Reference to Model.
|
||
|
if model:
|
||
|
self.model = model
|
||
|
else:
|
||
|
self.model = None
|
||
|
self.classname = 'SegmentCollection'
|
||
|
if segments:
|
||
|
self.segments = segments
|
||
|
else:
|
||
|
self.segments = []
|
||
|
self.msh = None
|
||
|
|
||
|
def get_json(self):
|
||
|
data = {
|
||
|
'segments': [seg.get_json() for seg in self.segments],
|
||
|
}
|
||
|
return data
|
||
|
|
||
|
@staticmethod
|
||
|
def from_json(data, model=None):
|
||
|
coll = SegmentCollection()
|
||
|
coll.model = model
|
||
|
for segment_data in data['segments']:
|
||
|
if segment_data['type'] == 'SegmentGeometry':
|
||
|
coll.segments.append(SegmentGeometry.from_json(segment_data, coll))
|
||
|
elif segment_data['type'] == 'ClothGeometry':
|
||
|
coll.segments.append(ClothGeometry.from_json(segment_data, coll))
|
||
|
elif segment_data['type'] == 'ShadowGeometry':
|
||
|
coll.segments.append(ShadowGeometry.from_json(segment_data, coll))
|
||
|
return coll
|
||
|
|
||
|
def dump(self, fh):
|
||
|
'''Dump information to open filehandler fileh.'''
|
||
|
fh.write('\t\t--- SegmentCollection ---\n')
|
||
|
for seg in self.segments:
|
||
|
seg.dump(fh)
|
||
|
|
||
|
def num_vertices(self):
|
||
|
numv = 0
|
||
|
for segm in self.segments:
|
||
|
numv += len(segm.vertices)
|
||
|
return numv
|
||
|
|
||
|
def add(self, segment):
|
||
|
'''Adds SegmentGeometry, ClothGeometry or ShadowGeometry
|
||
|
to the collection and sets the collection attribute.'''
|
||
|
segment.collection = self
|
||
|
self.segments.append(segment)
|
||
|
|
||
|
def remove(self, index):
|
||
|
'''Removes Geometry at index index.'''
|
||
|
del self.segments[index]
|
||
|
|
||
|
def split(self, seg_index, poly_mat_inds, mat_names):
|
||
|
'''Splits the segment with index == seg_index.
|
||
|
poly_mat_inds is a list where the index represents
|
||
|
the index of the face/polygon and the content of the
|
||
|
list item represents the material index.
|
||
|
mat_names is a list of material names.
|
||
|
This function splits the segment into multiple
|
||
|
segments(one per material). The result segments
|
||
|
will be used as the .segments of this SegmentCollection.'''
|
||
|
polies_per_material = self._polies_per_material(poly_mat_inds)
|
||
|
# Segment to split.
|
||
|
segm = self.segments[seg_index]
|
||
|
segments = []
|
||
|
# Loop through materials.
|
||
|
for mat_index, mat_poly_indices in enumerate(polies_per_material):
|
||
|
if not mat_poly_indices:
|
||
|
continue
|
||
|
seg = SegmentGeometry(self)
|
||
|
facecoll = FaceCollection(seg)
|
||
|
vertcoll = VertexCollection(seg)
|
||
|
vertcoll.set_flags_from_vertcoll(segm.vertices)
|
||
|
# Loop through face indices.
|
||
|
for local_face_index, face_index in enumerate(mat_poly_indices):
|
||
|
master_face = segm.faces[face_index]
|
||
|
new_face = Face()
|
||
|
for vert_index in master_face.vertices:
|
||
|
# Add the vertex. As the vertex now holds UV, weights etc
|
||
|
# its not necessary anymore to do all the checking if
|
||
|
# the UV etc lists should be split, too.
|
||
|
vertcoll.add(segm.vertices[vert_index])
|
||
|
# Add last item in vertex collection to face.
|
||
|
new_face.add(len(vertcoll) - 1)
|
||
|
facecoll.add(new_face)
|
||
|
seg.vertices = vertcoll
|
||
|
seg.faces = facecoll
|
||
|
seg.mat_name = mat_names[mat_index]
|
||
|
seg.mat = self.msh.get_mat_by_name(segm.mat_name)
|
||
|
segments.append(seg)
|
||
|
self.segment = []
|
||
|
self.segments = segments
|
||
|
|
||
|
def assign_materials(self, material_coll):
|
||
|
'''Assign material object from material name.'''
|
||
|
mat_name_dict = material_coll.get_matname_dict()
|
||
|
for segment in self.segments:
|
||
|
segment.material = mat_name_dict[segment.mat_name]
|
||
|
|
||
|
def _polies_per_material(self, poly_inds):
|
||
|
'''Creates a list where each item represents a material and holds
|
||
|
all polygon indices mapped to that material.'''
|
||
|
largest_index = self._mat_largest_ind(poly_inds)
|
||
|
# Create sub-lists for every material.
|
||
|
list_ = [[] for n in xrange(largest_index + 1)]
|
||
|
for index, el in enumerate(poly_inds):
|
||
|
# Now add the polygon indices to the according mat list.
|
||
|
list_[el].append(index)
|
||
|
return list_
|
||
|
|
||
|
def _mat_largest_ind(self, inds):
|
||
|
'''Largest material index. Equals number of materials.'''
|
||
|
int_ = 0
|
||
|
for el in inds:
|
||
|
if el > int_:
|
||
|
int_ = el
|
||
|
return int_
|
||
|
|
||
|
def __str__(self):
|
||
|
return str(self.segments)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return str(self.segments)
|
||
|
|
||
|
def __getitem__(self, key):
|
||
|
return self.segments[key]
|
||
|
|
||
|
def __len__(self):
|
||
|
return len(self.segments)
|
||
|
|
||
|
def pack(self):
|
||
|
data = []
|
||
|
for segment in self.segments:
|
||
|
data.append(segment.pack())
|
||
|
return ''.join(data)
|
||
|
|
||
|
def repack(self):
|
||
|
data = [segment.repack() for segment in self.segments]
|
||
|
return ''.join(data)
|
||
|
|
||
|
|
||
|
class SegmentGeometry(Packer):
|
||
|
# mat_name: string
|
||
|
# mat_index: int
|
||
|
# vert_list: list (Vertex)
|
||
|
# face_list: list (Face)
|
||
|
def __init__(self, collection=None):
|
||
|
#Reference to SegmentCollection.
|
||
|
if collection:
|
||
|
self.collection = collection
|
||
|
else:
|
||
|
self.collection = None
|
||
|
self.classname = 'SegmentGeometry'
|
||
|
self.mat_name = None
|
||
|
self.material = None
|
||
|
self.vertices = None
|
||
|
self.faces = None
|
||
|
self.msh = None
|
||
|
self.index_map = None
|
||
|
|
||
|
def get_json(self):
|
||
|
data = {
|
||
|
'type': self.classname,
|
||
|
'material': self.material.name.encode('utf-8'),
|
||
|
'num_vertices': len(self.vertices),
|
||
|
'vertices': self.vertices.get_json(),
|
||
|
'num_faces': len(self.faces),
|
||
|
'faces': self.faces.get_json(),
|
||
|
}
|
||
|
return data
|
||
|
|
||
|
@staticmethod
|
||
|
def from_json(data, collection=None):
|
||
|
geo = SegmentGeometry()
|
||
|
geo.collection = collection
|
||
|
geo.mat_name = data['material'].encode('utf-8')
|
||
|
geo.vertices = VertexCollection.from_json(data['vertices'], geo)
|
||
|
geo.faces = FaceCollection.from_json(data['faces'], geo)
|
||
|
return geo
|
||
|
|
||
|
def clear_doubles(self):
|
||
|
'''Clears Vertices appearing more than once.'''
|
||
|
self.index_map = dict()
|
||
|
new_vertices = []
|
||
|
# Use a set to quickly check if we have a duplicate.
|
||
|
new_vertices_set = set()
|
||
|
len_new_verts = 0
|
||
|
|
||
|
num_cleared_vertices = 0
|
||
|
|
||
|
for index, vert in enumerate(self.vertices):
|
||
|
if vert in new_vertices_set:
|
||
|
index_original = new_vertices.index(vert)
|
||
|
# Insert into indices map.
|
||
|
self.index_map[index] = index_original
|
||
|
|
||
|
num_cleared_vertices += 1
|
||
|
else:
|
||
|
new_vertices.append(vert)
|
||
|
new_vertices_set.add(vert)
|
||
|
self.index_map[index] = len_new_verts
|
||
|
len_new_verts += 1
|
||
|
self.vertices.vertices = new_vertices
|
||
|
|
||
|
logging.info('Cleared %s doubles.', num_cleared_vertices)
|
||
|
|
||
|
def dump(self, fh):
|
||
|
'''Dump information to open filehandler fileh.'''
|
||
|
fh.write('\t\t\t--- SegmentGeometry ---\n')
|
||
|
fh.write('\t\t\t\tMaterial: {0}\n'.format(self.material))
|
||
|
fh.write('\t\t\t\tVertices: {0}\n'.format(len(self.vertices)))
|
||
|
fh.write('\t\t\t\tFaces: {0}\n'.format(len(self.faces or '')))
|
||
|
fh.write('\t\t\t\tUVed: {0}\n'.format(self.vertices.uved))
|
||
|
fh.write('\t\t\t\tColored: {0}\n'.format(self.vertices.colored))
|
||
|
fh.write('\t\t\t\tWeighted: {0}\n'.format(self.vertices.weighted))
|
||
|
fh.write(str(self.index_map))
|
||
|
|
||
|
def pack_MATI(self):
|
||
|
return self.pack_long_chunk('MATI', self.material.index)
|
||
|
|
||
|
def pack(self):
|
||
|
'''Packs the SEGM chunk. This should be used to retrieve the segment in packed form.'''
|
||
|
data = ['SEGM']
|
||
|
data.append('size')
|
||
|
data.append(self.pack_MATI())
|
||
|
data.append(self.vertices.pack())
|
||
|
data.append(self.faces.pack())
|
||
|
data[1] = struct.pack('<L', self.sum_seq(data, 2))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def repack(self):
|
||
|
return self.pack()
|
||
|
|
||
|
|
||
|
class ShadowGeometry(Packer):
|
||
|
def __init__(self, collection=None):
|
||
|
if collection:
|
||
|
self.collection = collection
|
||
|
else:
|
||
|
self.collection = None
|
||
|
self.classname = 'ShadowGeometry'
|
||
|
self.data = ''
|
||
|
self.positions = []
|
||
|
self.edges = []
|
||
|
|
||
|
def get_json(self):
|
||
|
data = {
|
||
|
'type': self.classname,
|
||
|
'num_positions': len(self.positions),
|
||
|
'positions': self.positions,
|
||
|
'num_edges': len(self.edges),
|
||
|
'edges': self.edges,
|
||
|
}
|
||
|
return data
|
||
|
|
||
|
@staticmethod
|
||
|
def from_json(data, collection=None):
|
||
|
geo = ShadowGeometry()
|
||
|
geo.collection = collection
|
||
|
geo.positions = data['positions']
|
||
|
geo.edges = data['edges']
|
||
|
return geo
|
||
|
|
||
|
def dump(self, fh):
|
||
|
'''Dump information to open filehandler fileh.'''
|
||
|
fh.write('\t\t\t--- ShadowGeometry ---\n')
|
||
|
fh.write('\t\t\t\tNo data available.\n')
|
||
|
|
||
|
def repack(self):
|
||
|
data = ['SHDW', 'size', self.data]
|
||
|
data[1] = struct.pack('<L', len(self.data))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack(self):
|
||
|
data = ['SHDW', 'size']
|
||
|
data.append(struct.pack('<L', len(self.positions)))
|
||
|
for pos in self.positions:
|
||
|
data.append(struct.pack('<fff', *pos))
|
||
|
data.append(struct.pack('<L', len(self.edges)))
|
||
|
for edge in self.edges:
|
||
|
data.append(struct.pack('<HHHH', *edge))
|
||
|
data[1] = struct.pack('<L', 4 + 4 + 6 * len(self.edges) + 12 * len(self.positions))
|
||
|
return ''.join(data)
|
||
|
|
||
|
|
||
|
class ClothCollision(Packer):
|
||
|
def __init__(self, cloth_geo=None):
|
||
|
self.cloth = cloth_geo
|
||
|
self.name = ''
|
||
|
self.parent = ''
|
||
|
# 0: Sphere, 1: Cylinder
|
||
|
self.primitive_type = 0
|
||
|
self.primitive_data = 4, 4, 4
|
||
|
|
||
|
def get_json(self):
|
||
|
data = {
|
||
|
'name': self.name,
|
||
|
'parent': self.parent,
|
||
|
'primitive_type': self.primitive_type,
|
||
|
'primitive_data': self.primitive_data,
|
||
|
}
|
||
|
return data
|
||
|
|
||
|
@classmethod
|
||
|
def from_json(cls, data, cloth=None):
|
||
|
cc = cls()
|
||
|
cc.cloth = cloth
|
||
|
cc.name = data['name']
|
||
|
cc.parent = data['parent']
|
||
|
cc.primitive_type = data['primitive_type']
|
||
|
cc.primitive_data = data['primitive_data']
|
||
|
return cc
|
||
|
|
||
|
def pack(self):
|
||
|
data = [self.name + '\x00',
|
||
|
self.parent + '\x00',
|
||
|
struct.pack('<L', self.primitive_type),
|
||
|
struct.pack('<fff', *self.primitive_data)]
|
||
|
return ''.join(data)
|
||
|
|
||
|
|
||
|
class ClothGeometry(Packer):
|
||
|
def __init__(self, collection=None):
|
||
|
if collection:
|
||
|
self.collection = collection
|
||
|
else:
|
||
|
self.collection = None
|
||
|
self.classname = 'ClothGeometry'
|
||
|
self.texture = 'no_texture'
|
||
|
self.vertices = None
|
||
|
self.faces = None
|
||
|
self.stretch = ''
|
||
|
self.stretch_constraints = set()
|
||
|
self.cross = ''
|
||
|
self.cross_constraints = set()
|
||
|
self.bend = ''
|
||
|
self.bend_constraints = set()
|
||
|
self.collision = ''
|
||
|
self.collisions = []
|
||
|
|
||
|
def get_json(self):
|
||
|
data = {
|
||
|
'type': self.classname,
|
||
|
'texture': self.texture,
|
||
|
'num_vertices': len(self.vertices),
|
||
|
'vertices': self.vertices.get_json(),
|
||
|
'num_faces': len(self.faces),
|
||
|
'faces': self.faces.get_json(),
|
||
|
'stretch': self.stretch_constraints,
|
||
|
'cross': self.cross_constraints,
|
||
|
'bend': self.bend_constraints,
|
||
|
'collisions': [c.get_json() for c in self.collisions],
|
||
|
}
|
||
|
return data
|
||
|
|
||
|
@staticmethod
|
||
|
def from_json(data, collection=None):
|
||
|
geo = ClothGeometry()
|
||
|
geo.collection = collection
|
||
|
geo.vertices = ClothVertexCollection.from_json(data['vertices'], geo)
|
||
|
geo.faces = FaceCollection.from_json(data['faces'], geo)
|
||
|
geo.stretch_constraints = data['stretch']
|
||
|
geo.cross_constraints = data['cross']
|
||
|
geo.bend_constraints = data['bend']
|
||
|
geo.collisions = [ClothCollision.from_json(collision, geo) for collision in data['collisions']]
|
||
|
return geo
|
||
|
|
||
|
def dump(self, fh):
|
||
|
'''Dump information to open filehandler fileh.'''
|
||
|
fh.write('\t\t\t--- ClothGeometry ---\n')
|
||
|
fh.write('\t\t\t\tVertices: {0}\n'.format(len(self.vertices)))
|
||
|
fh.write('\t\t\t\tFaces: {0}\n'.format(len(self.faces)))
|
||
|
|
||
|
def assign_parents(self):
|
||
|
'''Assign segment attribute for every collection.'''
|
||
|
self.vertices.segment = self
|
||
|
self.faces.segment = self
|
||
|
|
||
|
def create_constraints(self):
|
||
|
''' Creates constraints between faces and vertices so the cloth simulation can keep the
|
||
|
cloth's shape
|
||
|
'''
|
||
|
def is_fixed(idx):
|
||
|
return self.vertices[idx].is_fixed
|
||
|
|
||
|
# Note that the Pandemic exporter does _not_ constrain fixed points to each other (which
|
||
|
# really doesn't make any sense). So we don't do it either
|
||
|
for face in self.faces:
|
||
|
# p1 ┌────┐ p2
|
||
|
# │ │
|
||
|
# │ │
|
||
|
# p4 └────┘ p3
|
||
|
|
||
|
last_vert_idx = face.vertices[-1]
|
||
|
|
||
|
# Stretch constraints, along the boundary of the polygon
|
||
|
# p1-p2, p2-p3, p3-p4, p4-p1
|
||
|
# This allows for any amount of sides to the polygon
|
||
|
for vert_idx in face.vertices:
|
||
|
both_fixed = is_fixed(vert_idx) and is_fixed(last_vert_idx)
|
||
|
|
||
|
if both_fixed:
|
||
|
continue
|
||
|
|
||
|
# Check both possible orders, we only want one connection between two vertices
|
||
|
a_b_exists = (last_vert_idx, vert_idx) in self.stretch_constraints
|
||
|
b_a_exists = (vert_idx, last_vert_idx) in self.stretch_constraints
|
||
|
|
||
|
if not (a_b_exists or b_a_exists):
|
||
|
self.stretch_constraints.add((last_vert_idx, vert_idx))
|
||
|
|
||
|
last_vert_idx = vert_idx
|
||
|
|
||
|
# Cross constraints, diagonally across _quads_
|
||
|
# p1-p3, p2-p4
|
||
|
# Only works for quads currently
|
||
|
if len(face.vertices) == 4:
|
||
|
pair_1 = (face.vertices[0], face.vertices[2])
|
||
|
pair_2 = (face.vertices[1], face.vertices[3])
|
||
|
|
||
|
# Only add constraint if either or both vertices are dynamic
|
||
|
if not (is_fixed(pair_1[0]) and is_fixed(pair_1[1])):
|
||
|
self.cross_constraints.add(pair_1)
|
||
|
|
||
|
if not (is_fixed(pair_2[0]) and is_fixed(pair_2[1])):
|
||
|
self.cross_constraints.add(pair_2)
|
||
|
|
||
|
# Bend constraints
|
||
|
# Supposed to keep the overall shape of the cloth from overbending
|
||
|
#
|
||
|
# p1 p2 p3
|
||
|
# ┌─────┬─────┐
|
||
|
# │ │ │
|
||
|
# │ p8 │ p9 │ p4
|
||
|
# ├─────┼─────┤
|
||
|
# │ │ │
|
||
|
# │ │ │
|
||
|
# └─────┴─────┘
|
||
|
# p7 p6 p5
|
||
|
#
|
||
|
# p1-p3, p3-p5, p5-p7, p7-p1, p8-p4, p2-p6
|
||
|
# Connects all faces in this pattern
|
||
|
for face2 in self.faces:
|
||
|
if face is face2:
|
||
|
continue
|
||
|
|
||
|
# Vertices shared between the current two faces
|
||
|
shared_vertices = [v for v in face2.vertices if v in face.vertices]
|
||
|
|
||
|
if not shared_vertices:
|
||
|
continue
|
||
|
|
||
|
shared_vertex = shared_vertices[0]
|
||
|
|
||
|
# get_connections gets all vertices in a face that connect to the given one. So in
|
||
|
# the drawing above if we pass p8 to get_connections of the top-left face we will
|
||
|
# get [p1, p9]
|
||
|
# Shared vertices will be filtered out, so p1 remains
|
||
|
connections1 = []
|
||
|
for connection_vertex in face.get_connections(shared_vertex):
|
||
|
if connection_vertex not in shared_vertices:
|
||
|
connections1.append(connection_vertex)
|
||
|
|
||
|
# Same as above, assuming we pass p8 of the bottom-left face we will get [p9, p7]
|
||
|
# Shared vertices will be filtered out, so p7 remains
|
||
|
connections2 = []
|
||
|
for connection_vertex in face2.get_connections(shared_vertex):
|
||
|
if connection_vertex not in shared_vertices:
|
||
|
connections2.append(connection_vertex)
|
||
|
|
||
|
# Zip the two resulting lists ([p1] and [p7]) creating pair p1-p7
|
||
|
for vert_idx_1, vert_idx_2 in zip(connections1, connections2):
|
||
|
# Again, a constraint between two fixed points does not make sense. In this case
|
||
|
# we need to consider the case of fixed-dynamic _spanning_ a fixed point needs
|
||
|
# to be considered, too (e.g. p1-p7 where p1 and p8 are fixed). These should be
|
||
|
# kept
|
||
|
both_fixed = is_fixed(vert_idx_1) and is_fixed(vert_idx_2)
|
||
|
|
||
|
if both_fixed:
|
||
|
continue
|
||
|
|
||
|
a_b_exists = (vert_idx_1, vert_idx_2) in self.bend_constraints
|
||
|
b_a_exists = (vert_idx_2, vert_idx_1) in self.bend_constraints
|
||
|
|
||
|
if not (a_b_exists or b_a_exists):
|
||
|
self.bend_constraints.add((vert_idx_1, vert_idx_2))
|
||
|
|
||
|
def pack_stretch(self):
|
||
|
data = ['SPRS', struct.pack('<LL', 4 + 4 * len(self.stretch_constraints), len(self.stretch_constraints))]
|
||
|
for item in self.stretch_constraints:
|
||
|
data.append(struct.pack('<HH', *item))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def repack_stretch(self):
|
||
|
data = ['SPRS', 'size']
|
||
|
# Just append the data we read when this .msh was loaded.
|
||
|
data.append(self.stretch)
|
||
|
data[1] = struct.pack('<L', len(self.stretch))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack_cross(self):
|
||
|
data = ['CPRS', struct.pack('<LL', 4 + 4 * len(self.cross_constraints), len(self.cross_constraints))]
|
||
|
for item in self.cross_constraints:
|
||
|
data.append(struct.pack('<HH', *item))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def repack_cross(self):
|
||
|
data = ['CPRS', 'size']
|
||
|
data.append(self.cross)
|
||
|
data[1] = struct.pack('<L', len(self.cross))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack_bend(self):
|
||
|
data = ['BPRS', struct.pack('<LL', 4 + 4 * len(self.bend_constraints), len(self.bend_constraints))]
|
||
|
for item in self.bend_constraints:
|
||
|
data.append(struct.pack('<HH', *item))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def repack_bend(self):
|
||
|
data = ['BPRS', 'size']
|
||
|
data.append(self.bend)
|
||
|
data[1] = struct.pack('<L', len(self.cross))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack_collision(self):
|
||
|
data = ['COLL', 'size', struct.pack('<L', len(self.collisions))]
|
||
|
# Length of size indicator = 4.
|
||
|
size = 4
|
||
|
for coll in self.collisions:
|
||
|
data.append(coll.pack())
|
||
|
size += len(data[-1])
|
||
|
# Make the length of the chunk a multiple of 4.
|
||
|
while size % 4 != 0:
|
||
|
size += 1
|
||
|
data.append('\x00')
|
||
|
data[1] = struct.pack('<L', size)
|
||
|
return ''.join(data)
|
||
|
|
||
|
def repack_collision(self):
|
||
|
data = ['COLL', 'size']
|
||
|
data.append(self.collision)
|
||
|
data[1] = struct.pack('<L', len(self.collision))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack(self):
|
||
|
data = ['CLTH', 'size']
|
||
|
if self.texture:
|
||
|
data.append(self.pack_string_chunk('CTEX', self.texture))
|
||
|
else:
|
||
|
data.append(self.pack_string_chunk('CTEX', 'no_texture'))
|
||
|
data.append(self.vertices.pack_pos())
|
||
|
data.append(self.vertices.pack_uvs())
|
||
|
data.append(self.vertices.pack_fixed())
|
||
|
data.append(self.vertices.pack_weights())
|
||
|
data.append(self.faces.pack_cloth())
|
||
|
data.append(self.pack_stretch())
|
||
|
data.append(self.pack_cross())
|
||
|
data.append(self.pack_bend())
|
||
|
data.append(self.pack_collision())
|
||
|
data[1] = struct.pack('<L', self.sum_seq(data, 2))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def repack(self):
|
||
|
data = ['CLTH', 'size']
|
||
|
if self.texture:
|
||
|
data.append(self.pack_string_chunk('CTEX', self.texture))
|
||
|
else:
|
||
|
data.append(self.pack_string_chunk('CTEX', 'no_texture'))
|
||
|
data.append(self.vertices.pack_pos())
|
||
|
data.append(self.vertices.pack_uvs())
|
||
|
data.append(self.vertices.pack_fixed())
|
||
|
data.append(self.vertices.pack_weights())
|
||
|
data.append(self.faces.pack_cloth())
|
||
|
data.append(self.repack_stretch())
|
||
|
data.append(self.repack_cross())
|
||
|
data.append(self.repack_bend())
|
||
|
data.append(self.repack_collision())
|
||
|
data[1] = struct.pack('<L', self.sum_seq(data, 2))
|
||
|
return ''.join(data)
|
||
|
|
||
|
|
||
|
class Face(object):
|
||
|
'''Keeps the indices of the vertices forming this face.'''
|
||
|
# Params:
|
||
|
# - vertices: a list of indices forming this face.
|
||
|
# - coll: the FaceCollection.
|
||
|
def __init__(self, vertices=None, coll=None):
|
||
|
# List of vertex indices(ints).
|
||
|
if vertices:
|
||
|
self.vertices = vertices
|
||
|
else:
|
||
|
self.vertices = None
|
||
|
# Reference to the FaceCollection.
|
||
|
if coll:
|
||
|
self.collection = coll
|
||
|
else:
|
||
|
self.collection = None
|
||
|
self.classname = 'Face'
|
||
|
|
||
|
|
||
|
def get_json(self):
|
||
|
data = {
|
||
|
'vertices': self.vertices,
|
||
|
}
|
||
|
return data
|
||
|
|
||
|
@staticmethod
|
||
|
def from_json(data, collection=None):
|
||
|
face = Face()
|
||
|
face.collection = collection
|
||
|
face.vertices = data['vertices']
|
||
|
return face
|
||
|
|
||
|
def get_connections(self, vertex):
|
||
|
''' Get all vertices that connect to the given vertex in this face
|
||
|
'''
|
||
|
index = self.vertices.index(vertex)
|
||
|
|
||
|
connections = []
|
||
|
|
||
|
# Get the vertex before the given one
|
||
|
# Underflow -> take last one
|
||
|
if index - 1 < 0:
|
||
|
connections.append(self.vertices[-1])
|
||
|
else:
|
||
|
connections.append(self.vertices[index - 1])
|
||
|
|
||
|
# Get the vertex after the given one
|
||
|
# Overflow -> take first one
|
||
|
if index + 1 == len(self.vertices):
|
||
|
connections.append(self.vertices[0])
|
||
|
else:
|
||
|
connections.append(self.vertices[index + 1])
|
||
|
|
||
|
return connections
|
||
|
|
||
|
def replace(self, verts):
|
||
|
'''Replaces the local vertices list with a new one.'''
|
||
|
self.vertices = verts
|
||
|
|
||
|
def reverse_order(self):
|
||
|
'''Reverses the triangle order. Only works with tris.'''
|
||
|
self.vertices = self.vertices[0], self.vertices[2], self.vertices[1]
|
||
|
|
||
|
def add(self, vert_index):
|
||
|
'''Adds a vertex index to the local vertices.'''
|
||
|
if not self.vertices:
|
||
|
self.vertices = [vert_index]
|
||
|
return
|
||
|
self.vertices.append(vert_index)
|
||
|
|
||
|
def i(self, index):
|
||
|
'''Returns the vertex index at index index of the local vertices.'''
|
||
|
return self.vertices[index]
|
||
|
|
||
|
def SIindices(self):
|
||
|
'''Returns the face in CCW order for XSI importing.'''
|
||
|
if self.sides == 4:
|
||
|
return self.i(0), self.i(1), self.i(3), self.i(2)
|
||
|
else:
|
||
|
return self.i(0), self.i(1), self.i(2)
|
||
|
|
||
|
@property
|
||
|
def sides(self):
|
||
|
'''Returns the number of sides/vertices this face is constructed of.'''
|
||
|
return len(self.vertices)
|
||
|
|
||
|
def pack(self):
|
||
|
'''Packs the vertex indices for the STRP chunk.'''
|
||
|
index_map = self.collection.segment.index_map
|
||
|
if (self.sides == 4) and (index_map is not None):
|
||
|
return struct.pack('<HHHH', index_map[self.vertices[0]] + 0x8000,
|
||
|
index_map[self.vertices[1]] + 0x8000,
|
||
|
index_map[self.vertices[2]],
|
||
|
index_map[self.vertices[3]])
|
||
|
elif (self.sides == 3) and (index_map is not None):
|
||
|
return struct.pack('<HHH', index_map[self.vertices[0]] + 0x8000,
|
||
|
index_map[self.vertices[1]] + 0x8000,
|
||
|
index_map[self.vertices[2]])
|
||
|
elif (self.sides == 4) and (index_map is None):
|
||
|
return struct.pack('<HHHH', self.vertices[0] + 0x8000,
|
||
|
self.vertices[1] + 0x8000,
|
||
|
self.vertices[2],
|
||
|
self.vertices[3])
|
||
|
elif (self.sides == 3) and (index_map is None):
|
||
|
return struct.pack('<HHH', self.vertices[0] + 0x8000,
|
||
|
self.vertices[1] + 0x8000,
|
||
|
self.vertices[2])
|
||
|
else:
|
||
|
return ''
|
||
|
|
||
|
def pack_tris(self):
|
||
|
'''Packs the vertex indices as tris for the CMSH chunk.'''
|
||
|
# index_map = self.collection.segment.index_map
|
||
|
# if (self.sides == 4) and (index_map is not None):
|
||
|
# return (struct.pack('<LLL', index_map[self.vertices[0]],
|
||
|
# index_map[self.vertices[1]],
|
||
|
# index_map[self.vertices[2]]),
|
||
|
# struct.pack('<LLL', index_map[self.vertices[0]],
|
||
|
# index_map[self.vertices[2]],
|
||
|
# index_map[self.vertices[3]]))
|
||
|
# elif (self.sides == 3) and (index_map is not None):
|
||
|
# return (struct.pack('<LLL', index_map[self.vertices[0]],
|
||
|
# index_map[self.vertices[1]],
|
||
|
# index_map[self.vertices[2]]),)
|
||
|
# elif (self.sides == 4) and (index_map is None):
|
||
|
# return (struct.pack('<LLL', self.vertices[0],
|
||
|
# self.vertices[1],
|
||
|
# self.vertices[2]),
|
||
|
# struct.pack('<LLL', self.vertices[0],
|
||
|
# self.vertices[2],
|
||
|
# self.vertices[3]))
|
||
|
# elif (self.sides == 3) and (index_map is None):
|
||
|
# return (struct.pack('<LLL', self.vertices[0],
|
||
|
# self.vertices[1],
|
||
|
# self.vertices[2]),)
|
||
|
# else:
|
||
|
# return ()
|
||
|
|
||
|
# Make sure everything returned has a length for correct size calculation.
|
||
|
if self.sides == 4:
|
||
|
return (struct.pack('<LLL', self.vertices[0],
|
||
|
self.vertices[1],
|
||
|
self.vertices[2]),
|
||
|
struct.pack('<LLL', self.vertices[0],
|
||
|
self.vertices[2],
|
||
|
self.vertices[3]))
|
||
|
elif self.sides == 3:
|
||
|
return (struct.pack('<LLL', self.vertices[0],
|
||
|
self.vertices[1],
|
||
|
self.vertices[2]), )
|
||
|
else:
|
||
|
return ()
|
||
|
|
||
|
def get_edges(self):
|
||
|
'''Returns unique edges.'''
|
||
|
if self.sides == 4:
|
||
|
return ((self.vertices[0], self.vertices[1]),
|
||
|
(self.vertices[1], self.vertices[2]),
|
||
|
(self.vertices[2], self.vertices[3]),
|
||
|
(self.vertices[3], self.vertices[0]))
|
||
|
elif self.sides == 3:
|
||
|
return ((self.vertices[0], self.vertices[1]),
|
||
|
(self.vertices[1], self.vertices[2]),
|
||
|
(self.vertices[2], self.vertices[3]))
|
||
|
else:
|
||
|
return []
|
||
|
|
||
|
def __repr__(self):
|
||
|
return 'Face({0})'.format(str(self.vertices))
|
||
|
|
||
|
def __str__(self):
|
||
|
return 'Face({0})'.format(str(self.vertices))
|
||
|
|
||
|
|
||
|
class FaceCollection(object):
|
||
|
def __init__(self, segm=None):
|
||
|
# List of Face objects.
|
||
|
self.faces = []
|
||
|
# Ref to parent(SegmentGeometry).
|
||
|
if segm:
|
||
|
self.segment = segm
|
||
|
else:
|
||
|
self.segment = None
|
||
|
self.classname = 'FaceCollection'
|
||
|
|
||
|
def get_json(self):
|
||
|
data = {
|
||
|
'faces': [face.get_json() for face in self.faces],
|
||
|
}
|
||
|
return data
|
||
|
|
||
|
@staticmethod
|
||
|
def from_json(data, segment=None):
|
||
|
coll = FaceCollection()
|
||
|
coll.segment = segment
|
||
|
coll.faces = [Face.from_json(face_data, coll) for face_data in data['faces']]
|
||
|
return coll
|
||
|
|
||
|
def get_faces(self):
|
||
|
'''Returns faces as vertex indices.'''
|
||
|
faces = []
|
||
|
for face in self.faces:
|
||
|
faces.extend([vertex for vertex in face.vertices])
|
||
|
return faces
|
||
|
|
||
|
def de_ngonize(self, only_tris=False):
|
||
|
'''Makes all n-gons to tris or quads. only_tris defines
|
||
|
if the algorithm should try to fit quads in, too.'''
|
||
|
if only_tris:
|
||
|
return self._triangulate_ngons()
|
||
|
new_faces = []
|
||
|
for ndx, face in enumerate(self.faces):
|
||
|
# If the face has more than 4 sides, cut it into
|
||
|
# multiple faces. Only 3 and 4 sided faces are
|
||
|
# supported.
|
||
|
if face.sides > 4:
|
||
|
nface = Face(self)
|
||
|
numtris = face.sides - 2
|
||
|
numquads = numtris / 2.0
|
||
|
# 4.0 == int(4.0) but 4.5 != int(4.5).
|
||
|
# So, if it's possible to cut the current face
|
||
|
# into multiple quads, do it. Otherwise triangulate
|
||
|
# it.
|
||
|
if int(numquads) == numquads:
|
||
|
for n in xrange(int(numquads)):
|
||
|
nface = Face(self)
|
||
|
nface.vertices = face.vertices[n * 2:n * 2 + 4]
|
||
|
new_faces.append(nface)
|
||
|
else:
|
||
|
for n in xrange(int(numtris)):
|
||
|
# Reverse every 2nd tri so every tri is CCW.
|
||
|
if n / 2 != n / 2.0:
|
||
|
nface = Face(self)
|
||
|
nface.vertices = face.vertices[n:n + 3]
|
||
|
nface.reverse_order()
|
||
|
new_faces.append(nface)
|
||
|
continue
|
||
|
nface = Face(self)
|
||
|
nface.vertices = face.vertices[n:n + 3]
|
||
|
new_faces.append(nface)
|
||
|
else:
|
||
|
new_faces.append(face)
|
||
|
self.faces = new_faces
|
||
|
|
||
|
def _triangulate_ngons(self):
|
||
|
'''Triangulates n-gons of every face and sets
|
||
|
the faces to the newly calculated ones.'''
|
||
|
new_faces = []
|
||
|
for ndx, face in enumerate(self.faces):
|
||
|
if face.sides > 3:
|
||
|
nface = Face(self)
|
||
|
numtris = face.sides - 2
|
||
|
# Just triangulate every face with more than 4 sides.
|
||
|
for n in xrange(int(numtris)):
|
||
|
# Reverse every 2nd tri so every tri is CCW.
|
||
|
if n / 2 != n / 2.0:
|
||
|
nface = Face(self)
|
||
|
nface.vertices = face.vertices[n:n + 3]
|
||
|
nface.reverse_order()
|
||
|
new_faces.append(nface)
|
||
|
continue
|
||
|
nface = Face(self)
|
||
|
nface.vertices = face.vertices[n:n + 3]
|
||
|
new_faces.append(nface)
|
||
|
else:
|
||
|
new_faces.append(face)
|
||
|
self.faces = new_faces
|
||
|
|
||
|
def add(self, face):
|
||
|
'''Adds a face to the collection.'''
|
||
|
face.collection = self
|
||
|
if not self.faces:
|
||
|
self.faces = [face]
|
||
|
return
|
||
|
self.faces.append(face)
|
||
|
|
||
|
def pack(self):
|
||
|
'''Packs vertex indices for every face into
|
||
|
the STRP chunk.'''
|
||
|
data = ['STRP', 'size', 'numitems']
|
||
|
faces = 0
|
||
|
for face in self.faces:
|
||
|
faces += face.sides
|
||
|
if face.sides == 4 or face.sides == 3:
|
||
|
data.append(face.pack())
|
||
|
data[1] = struct.pack('<L', faces * 2 + 4)
|
||
|
data[2] = struct.pack('<L', faces)
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack_cloth(self):
|
||
|
'''Packs triangles of every face into
|
||
|
the cloth CMSH chunk.'''
|
||
|
data = ['CMSH', 'size', 'num']
|
||
|
num = 0
|
||
|
for face in self.faces:
|
||
|
facetris = face.pack_tris()
|
||
|
num += len(facetris)
|
||
|
data.extend(facetris)
|
||
|
# Number of tris * number of points * size of float + size indicator size.
|
||
|
data[1] = struct.pack('<L', num * 3 * 4 + 4)
|
||
|
data[2] = struct.pack('<L', num)
|
||
|
return ''.join(data)
|
||
|
|
||
|
def stretch_edges(self):
|
||
|
'''Gets edges for SPRS cloth chunk.'''
|
||
|
edges = []
|
||
|
for face in self.faces:
|
||
|
face_edges = face.get_edges()
|
||
|
for edge in face_edges:
|
||
|
if not edge in edges and not (edge[1], edge[0]) in edges:
|
||
|
edges.append(edge)
|
||
|
for index, edge in enumerate(edges):
|
||
|
if self.segment.vertices[edge[0]].is_fixed and self.segment.vertices[edge[1]].is_fixed:
|
||
|
edges.pop(index)
|
||
|
return edges
|
||
|
|
||
|
def cross_edges(self):
|
||
|
'''Gets edges for CPRS cloth chunk.'''
|
||
|
edges = []
|
||
|
for face in self.faces:
|
||
|
if face.sides == 4:
|
||
|
# Append edges which cross the quad.
|
||
|
edges.append((face.i(0), face.i(2)))
|
||
|
edges.append((face.i(1), face.i(3)))
|
||
|
return edges
|
||
|
|
||
|
def bend_edges(self):
|
||
|
'''Get edges for BPRS cloth chunk.'''
|
||
|
return ()
|
||
|
|
||
|
def __str__(self):
|
||
|
return str(self.faces)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return str(self.faces)
|
||
|
|
||
|
def __getitem__(self, key):
|
||
|
return self.faces[key]
|
||
|
|
||
|
def __len__(self):
|
||
|
return len(self.faces)
|
||
|
|
||
|
|
||
|
class Vertex(object):
|
||
|
def __init__(self, pos=(0, 0, 0), normal=None, coll=None):
|
||
|
# Reference to parent(VertexCollection).
|
||
|
if coll:
|
||
|
self.collection = coll
|
||
|
else:
|
||
|
self.collection = None
|
||
|
self.classname = 'Vertex'
|
||
|
self.index = 0
|
||
|
self.x = pos[0]
|
||
|
self.y = pos[1]
|
||
|
self.z = pos[2]
|
||
|
if normal:
|
||
|
self.nx = normal[0]
|
||
|
self.ny = normal[1]
|
||
|
self.nz = normal[2]
|
||
|
else:
|
||
|
self.nx = 1.0
|
||
|
self.ny = 1.0
|
||
|
self.nz = 1.0
|
||
|
self.u, self.v = 0.0, 0.0
|
||
|
self.color = Color()
|
||
|
self.deformers = ['none', 'none', 'none', 'none']
|
||
|
self.deformer_indices = [0, 0, 0, 0]
|
||
|
self.weights = [1.0, 0.0, 0.0, 0.0]
|
||
|
|
||
|
def get_json(self):
|
||
|
data = {
|
||
|
'position': self.pos,
|
||
|
'uv': self.uv,
|
||
|
'normal': self.normal,
|
||
|
'deformers': self.deformers,
|
||
|
'weights': self.weights,
|
||
|
'color': self.color.get_json()
|
||
|
}
|
||
|
return data
|
||
|
|
||
|
@staticmethod
|
||
|
def from_json(data, coll=None):
|
||
|
vert = Vertex()
|
||
|
vert.collection = coll
|
||
|
vert.pos = data['position']
|
||
|
vert.normal = data['normal']
|
||
|
vert.uv = data['uv']
|
||
|
vert.color = Color.from_json(data['color'])
|
||
|
vert.deformers = data['deformers']
|
||
|
vert.weights = data['weights']
|
||
|
return vert
|
||
|
|
||
|
def dump(self, fh):
|
||
|
fh.write('\t\tVERTEX\n')
|
||
|
fh.write('\t\t\tPos: {0:3}, {1:3}, {2:3}\n'.format(*self.pos))
|
||
|
fh.write('\t\t\tUV: {0:3}, {1:3}\n'.format(*self.uv))
|
||
|
|
||
|
def __hash__(self):
|
||
|
return hash((self.x, self.y, self.z, self.u, self.v, self.nx, self.ny, self.nz))
|
||
|
|
||
|
def __eq__(self, other):
|
||
|
if (self.x == other.x) and (self.y == other.y) and (self.z == other.z):
|
||
|
if (self.u == other.u) and (self.v == other.v):
|
||
|
if (self.nx == other.nx) and (self.ny == other.ny) and (self.nz == other.nz):
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
def __repr__(self):
|
||
|
return 'Vertex({0}, {1}, {2})'.format(*self.pos)
|
||
|
|
||
|
@property
|
||
|
def pos(self):
|
||
|
return self.x, self.y, self.z
|
||
|
|
||
|
@pos.setter
|
||
|
def pos(self, value):
|
||
|
self.x, self.y, self.z = value
|
||
|
|
||
|
@property
|
||
|
def uv(self):
|
||
|
return self.u, self.v
|
||
|
|
||
|
@uv.setter
|
||
|
def uv(self, value):
|
||
|
self.u, self.v = value
|
||
|
|
||
|
@property
|
||
|
def normal(self):
|
||
|
return self.nx, self.ny, self.nz
|
||
|
|
||
|
@normal.setter
|
||
|
def normal(self, value):
|
||
|
self.nx, self.ny, self.nz = value
|
||
|
|
||
|
def translate(self, vector3):
|
||
|
self.x += vector3[0]
|
||
|
self.y += vector3[1]
|
||
|
self.z += vector3[2]
|
||
|
|
||
|
def pack_pos(self):
|
||
|
return struct.pack('<fff', *self.pos)
|
||
|
|
||
|
def pack_normal(self):
|
||
|
return struct.pack('<fff', *self.normal)
|
||
|
|
||
|
def pack_uvs(self):
|
||
|
return struct.pack('<ff', *self.uv)
|
||
|
|
||
|
def pack_weights(self):
|
||
|
data = []
|
||
|
for index, weight in itertools.izip(self.deformer_indices, self.weights):
|
||
|
data.append(struct.pack('<Lf', index, weight))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack_color(self):
|
||
|
return self.color.pack_bgra('B')
|
||
|
|
||
|
|
||
|
class VertexCollection(object):
|
||
|
def __init__(self, segm=None):
|
||
|
# Ref to parent SegmentGeometry.
|
||
|
if segm:
|
||
|
self.segment = segm
|
||
|
else:
|
||
|
self.segment = None
|
||
|
self.vertices = []
|
||
|
self.classname = 'VertexCollection'
|
||
|
self.colored = False
|
||
|
self.weighted = False
|
||
|
self.uved = False
|
||
|
|
||
|
def get_json(self):
|
||
|
data = {
|
||
|
'vertices': [v.get_json() for v in self.vertices],
|
||
|
'colored': self.colored,
|
||
|
'weighted': self.weighted,
|
||
|
'uved': self.uved,
|
||
|
}
|
||
|
return data
|
||
|
|
||
|
@staticmethod
|
||
|
def from_json(data, segment=None):
|
||
|
coll = VertexCollection()
|
||
|
coll.segment = segment
|
||
|
for vdata in data['vertices']:
|
||
|
coll.vertices.append(Vertex.from_json(vdata, coll))
|
||
|
coll.colored = data['colored']
|
||
|
coll.weighted = data['weighted']
|
||
|
coll.uved = data['uved']
|
||
|
return coll
|
||
|
|
||
|
def dump(self, fh):
|
||
|
for v in self.vertices:
|
||
|
v.dump(fh)
|
||
|
|
||
|
def set_flags_from_vertcoll(self, vertcoll):
|
||
|
'''Transfers flags from the given VertexCollection vertcoll
|
||
|
to this one.'''
|
||
|
self.colored = vertcoll.colored
|
||
|
self.weighted = vertcoll.weighted
|
||
|
self.uved = vertcoll.uved
|
||
|
|
||
|
def get_positions(self):
|
||
|
'''Yield all positions.'''
|
||
|
for vertex in self.vertices:
|
||
|
yield vertex.pos
|
||
|
|
||
|
def pack_positions(self):
|
||
|
data = ['POSL']
|
||
|
data.append(struct.pack('<L', len(self.vertices) * 4 * 3 + 4))
|
||
|
data.append(struct.pack('<L', len(self.vertices)))
|
||
|
for vertex in self.vertices:
|
||
|
data.append(vertex.pack_pos())
|
||
|
return ''.join(data)
|
||
|
|
||
|
def get_normals(self):
|
||
|
'''Yield all normals.'''
|
||
|
for vertex in self.vertices:
|
||
|
yield vertex.normal
|
||
|
|
||
|
def pack_normals(self):
|
||
|
data = ['NRML']
|
||
|
data.append(struct.pack('<L', len(self.vertices) * 4 * 3 + 4))
|
||
|
data.append(struct.pack('<L', len(self.vertices)))
|
||
|
for vertex in self.vertices:
|
||
|
data.append(vertex.pack_normal())
|
||
|
return ''.join(data)
|
||
|
|
||
|
def get_uvs(self):
|
||
|
'''Yield all UVs.'''
|
||
|
for vertex in self.vertices:
|
||
|
yield vertex.uv
|
||
|
|
||
|
def get_uv_list(self):
|
||
|
return [vertex.uv for vertex in self.vertices]
|
||
|
|
||
|
def pack_uvs(self):
|
||
|
data = ['UV0L']
|
||
|
data.append(struct.pack('<L', len(self.vertices) * 4 * 2 + 4))
|
||
|
data.append(struct.pack('<L', len(self.vertices)))
|
||
|
for vertex in self.vertices:
|
||
|
data.append(vertex.pack_uvs())
|
||
|
return ''.join(data)
|
||
|
|
||
|
def set_uvs(self, uv_list):
|
||
|
for uv, vertex in itertools.izip(uv_list, self.vertices):
|
||
|
vertex.u = uv[0]
|
||
|
vertex.v = uv[1]
|
||
|
|
||
|
def get_colors(self):
|
||
|
'''Yield all color values.'''
|
||
|
for vertex in self.vertices:
|
||
|
yield vertex.color.get()
|
||
|
|
||
|
def set_colors(self, color_list):
|
||
|
for color, vertex in itertools.izip(color_list, self.vertices):
|
||
|
vertex.color = color
|
||
|
|
||
|
def pack_colors(self):
|
||
|
data = ['CLRL']
|
||
|
data.append(struct.pack('<L', len(self.vertices) * 4 + 4))
|
||
|
data.append(struct.pack('<L', len(self.vertices)))
|
||
|
for vertex in self.vertices:
|
||
|
data.append(vertex.pack_color())
|
||
|
return ''.join(data)
|
||
|
|
||
|
def get_weights(self):
|
||
|
weights = []
|
||
|
for vertex in self.vertices:
|
||
|
weights.append(vertex.deformers[0])
|
||
|
weights.append(vertex.weights[0])
|
||
|
weights.append(vertex.deformers[1])
|
||
|
weights.append(vertex.weights[1])
|
||
|
weights.append(vertex.deformers[2])
|
||
|
weights.append(vertex.weights[2])
|
||
|
weights.append(vertex.deformers[3])
|
||
|
weights.append(vertex.weights[3])
|
||
|
return weights
|
||
|
|
||
|
def set_weights(self, weights, deformers, indices):
|
||
|
for weight, deformer_tuple, index_tpl, vertex in itertools.izip(weights, deformers, indices, self.vertices):
|
||
|
vertex.weights = weight
|
||
|
vertex.deformers = deformer_tuple
|
||
|
vertex.deformer_indices = index_tpl
|
||
|
|
||
|
def pack_weights(self):
|
||
|
data = ['WGHT']
|
||
|
data.append(struct.pack('<L', len(self.vertices) * 4 * 8 + 4))
|
||
|
data.append(struct.pack('<L', len(self.vertices)))
|
||
|
for vertex in self.vertices:
|
||
|
data.append(vertex.pack_weights())
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack(self):
|
||
|
data = [self.pack_positions(),
|
||
|
self.pack_normals()]
|
||
|
if self.uved:
|
||
|
data.append(self.pack_uvs())
|
||
|
if self.colored:
|
||
|
data.append(self.pack_colors())
|
||
|
if self.weighted:
|
||
|
data.append(self.pack_weights())
|
||
|
return ''.join(data)
|
||
|
|
||
|
def add(self, vertex):
|
||
|
'''Adds a vertex to the collection and sets
|
||
|
its collection property.'''
|
||
|
vertex.collection = self
|
||
|
self.vertices.append(vertex)
|
||
|
|
||
|
def __str__(self):
|
||
|
return str(self.vertices)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return str(self.vertices)
|
||
|
|
||
|
def __getitem__(self, key):
|
||
|
return self.vertices[key]
|
||
|
|
||
|
def __len__(self):
|
||
|
return len(self.vertices)
|
||
|
|
||
|
|
||
|
class ClothVertex(object):
|
||
|
def __init__(self, pos, coll=None):
|
||
|
if coll:
|
||
|
self.collection = coll
|
||
|
else:
|
||
|
self.collection = None
|
||
|
self.classname = 'ClothVertex'
|
||
|
self.index = 0
|
||
|
self.x = pos[0]
|
||
|
self.y = pos[1]
|
||
|
self.z = pos[2]
|
||
|
self.u, self.v = 0.0, 0.0
|
||
|
self.deformer = ''
|
||
|
self.is_fixed = False
|
||
|
|
||
|
def get_json(self):
|
||
|
data = {
|
||
|
'position': self.pos,
|
||
|
'uv': self.uv,
|
||
|
'deformer': self.deformer,
|
||
|
'is_fixed': self.is_fixed,
|
||
|
}
|
||
|
return data
|
||
|
|
||
|
@staticmethod
|
||
|
def from_json(data, collection=None):
|
||
|
v = ClothVertex()
|
||
|
v.collection = collection
|
||
|
v.pos = data['position']
|
||
|
v.uv = data['uv']
|
||
|
v.deformer = data['deformer']
|
||
|
v.is_fixed = data['is_fixed']
|
||
|
return v
|
||
|
|
||
|
@property
|
||
|
def pos(self):
|
||
|
return self.x, self.y, self.z
|
||
|
|
||
|
@pos.setter
|
||
|
def pos(self, value):
|
||
|
self.x, self.y, self.z = value
|
||
|
|
||
|
@property
|
||
|
def uv(self):
|
||
|
return self.u, self.v
|
||
|
|
||
|
@uv.setter
|
||
|
def uv(self, value):
|
||
|
self.u, self.v = value
|
||
|
|
||
|
def pack_pos(self):
|
||
|
'''Returns x, y, z packed into 3 floats.'''
|
||
|
return struct.pack('<fff', *self.pos)
|
||
|
|
||
|
def pack_uvs(self):
|
||
|
'''Returns u, v packed into 2 floats.'''
|
||
|
return struct.pack('<ff', *self.uv)
|
||
|
|
||
|
|
||
|
class ClothVertexCollection(Packer):
|
||
|
def __init__(self, segm=None, verts=None):
|
||
|
if segm:
|
||
|
self.segment = segm
|
||
|
else:
|
||
|
self.segment = None
|
||
|
if verts:
|
||
|
self.vertices = verts
|
||
|
else:
|
||
|
self.vertices = []
|
||
|
self.classname = 'ClothVertexCollection'
|
||
|
self.uved = True
|
||
|
|
||
|
def get_json(self):
|
||
|
data = {
|
||
|
'vertices': [v.get_json() for v in self.vertices],
|
||
|
'uved': self.uved,
|
||
|
}
|
||
|
return data
|
||
|
|
||
|
@staticmethod
|
||
|
def from_json(data, segment=None):
|
||
|
coll = ClothVertexCollection()
|
||
|
coll.segment = segment
|
||
|
coll.uved = data['uved']
|
||
|
coll.vertices = [ClothVertex.from_json(vdata, coll) for vdata in data['vertices']]
|
||
|
return coll
|
||
|
|
||
|
def fixed(self):
|
||
|
return [point for point in self.vertices if point.is_fixed]
|
||
|
|
||
|
def fixed_indices(self):
|
||
|
return [index for index, point in enumerate(self.vertices) if point.is_fixed]
|
||
|
|
||
|
def get_deformers(self):
|
||
|
return [point.deformer for point in self.vertices if point.deformer]
|
||
|
|
||
|
def add(self, vert):
|
||
|
'''Adds a vertex to the collection and sets its
|
||
|
collection property.'''
|
||
|
vert.collection = self
|
||
|
self.vertices.append(vert)
|
||
|
|
||
|
def pack_pos(self):
|
||
|
'''Packs vertex positions into CPOS chunk.'''
|
||
|
data = ['CPOS']
|
||
|
data.append(struct.pack('<L', len(self.vertices) * 4 * 3 + 4))
|
||
|
data.append(struct.pack('<L', len(self.vertices)))
|
||
|
for vert in self.vertices:
|
||
|
data.append(vert.pack_pos())
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack_uvs(self):
|
||
|
data = ['CUV0']
|
||
|
data.append(struct.pack('<L', len(self.vertices) * 4 * 2 + 4))
|
||
|
data.append(struct.pack('<L', len(self.vertices)))
|
||
|
for vert in self.vertices:
|
||
|
data.append(vert.pack_uvs())
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack_fixed(self):
|
||
|
data = ['FIDX', 'size', 'num']
|
||
|
num = 0
|
||
|
for index, vert in enumerate(self.vertices):
|
||
|
if vert.is_fixed:
|
||
|
data.append(struct.pack('<L', index))
|
||
|
num += 1
|
||
|
data[1] = struct.pack('<L', num * 4 + 4)
|
||
|
data[2] = struct.pack('<L', num)
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack_weights(self):
|
||
|
data = ['FWGT', 'size', 'num']
|
||
|
num = 0
|
||
|
data2 = []
|
||
|
for index, vert in enumerate(self.vertices):
|
||
|
if vert.is_fixed and vert.deformer:
|
||
|
data2.append(self.null_terminate(vert.deformer))
|
||
|
num += 1
|
||
|
data.append(''.join(data2))
|
||
|
data[1] = struct.pack('<L', len(data[3]) + 4)
|
||
|
data[2] = struct.pack('<L', num)
|
||
|
return ''.join(data)
|
||
|
|
||
|
def __str__(self):
|
||
|
return str(self.vertices)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return str(self.vertices)
|
||
|
|
||
|
def __getitem__(self, key):
|
||
|
return self.vertices[key]
|
||
|
|
||
|
def __len__(self):
|
||
|
return len(self.vertices)
|
||
|
|
||
|
|
||
|
class Animation(Packer):
|
||
|
# Params:
|
||
|
# - parent: Parent class(Msh)
|
||
|
# - empty: Empty animation?
|
||
|
# 'empty' = empty animation
|
||
|
# 'maybe_empty' = check .bones and .cycle before packing
|
||
|
def __init__(self, msh=None, empty=None):
|
||
|
# Ref to Msh.
|
||
|
self.msh = msh
|
||
|
self.bones = None
|
||
|
self.cycle = None
|
||
|
self.cycles = []
|
||
|
self.classname = 'Animation'
|
||
|
if empty == 'empty':
|
||
|
# If the animation export is disabled, replace pack function.
|
||
|
self.pack = self.pack_empty
|
||
|
elif empty == 'maybe_empty':
|
||
|
self.pack = self.pack_check
|
||
|
|
||
|
@property
|
||
|
def empty(self):
|
||
|
'''Checks if this animation has any bones or cycle.'''
|
||
|
if self.bones and self.cycle:
|
||
|
return False
|
||
|
return True
|
||
|
|
||
|
def pack_check(self):
|
||
|
if self.bones and self.cycle:
|
||
|
return self.pack()
|
||
|
else:
|
||
|
return self.pack_empty()
|
||
|
|
||
|
def pack(self):
|
||
|
data = []
|
||
|
data.append(self.bones.pack_SKL2())
|
||
|
data.append(self.bones.pack_BLN2())
|
||
|
data.append('ANM2')
|
||
|
data.append('size')
|
||
|
data.append(self.cycle.pack())
|
||
|
data.append(self.bones.pack_KFR3())
|
||
|
data[3] = struct.pack('<L', self.sum_seq(data, 4))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack_empty(self):
|
||
|
return ''
|
||
|
|
||
|
|
||
|
class Cycle(object):
|
||
|
# animation: parent Animation
|
||
|
# name: string
|
||
|
# fps: float
|
||
|
# style: int
|
||
|
# frames: tuple2 (frame_start, frame_end)
|
||
|
def __init__(self, animation=None):
|
||
|
# Ref to Animation.
|
||
|
self.animation = animation
|
||
|
self.name = 'fullanimation'
|
||
|
self.classname = 'AnimCycle'
|
||
|
self.fps = 30
|
||
|
self.style = 0
|
||
|
self.frames = (1, 100)
|
||
|
|
||
|
def numframes(self):
|
||
|
return self.frames[1] - (self.frames[0] - 1)
|
||
|
|
||
|
def animname(self):
|
||
|
return self.name.ljust(64, '\x00')
|
||
|
|
||
|
def pack(self):
|
||
|
data = ['CYCL', struct.pack('<L', 84), struct.pack('<L', 1)]
|
||
|
data.append(self.animname())
|
||
|
data.append(struct.pack('<f', self.fps))
|
||
|
data.append(struct.pack('<L', self.style))
|
||
|
data.append(struct.pack('<LL', *self.frames))
|
||
|
return ''.join(data)
|
||
|
|
||
|
|
||
|
class Bone(object):
|
||
|
# Params:
|
||
|
# - collection: class(BoneCollection)
|
||
|
def __init__(self, collection=None):
|
||
|
# Parent BoneCollection.
|
||
|
self.collection = collection
|
||
|
self.classname = 'Bone'
|
||
|
# Name of the bone(and it's accompanying Model).
|
||
|
self.name = ''
|
||
|
# CRC checksum of the bone.
|
||
|
self.CRC = '' # msh2_crc.crc(self.name)
|
||
|
# The purpose of the following parameters is unclear.
|
||
|
self.bone_type = 0
|
||
|
self.constrain = 1.0
|
||
|
self.bone1len = 0.0
|
||
|
self.bone2len = 0.0
|
||
|
self.blend_factor = 0
|
||
|
self.keyframe_type = 0
|
||
|
# List of position frames(x, y, z).
|
||
|
self.pos_keyframes = None
|
||
|
self.pos_keyframe_indices = None
|
||
|
# List of rotation frames(x, y, z, w).
|
||
|
self.rot_keyframes = None
|
||
|
self.rot_keyframe_indices = None
|
||
|
|
||
|
def dump(self, fh):
|
||
|
w = fh.write
|
||
|
w('BONE - {0}\n'.format(self.name))
|
||
|
w('\tRotation:\n')
|
||
|
for rot in self.rot_keyframes:
|
||
|
w('\t\t{0}\n'.format(rot))
|
||
|
w('\tTransform:\n')
|
||
|
for pos in self.pos_keyframes:
|
||
|
w('\t\t{0}\n'.format(pos))
|
||
|
|
||
|
def recrc(self):
|
||
|
'''Calculates a Zero CRC from the name.'''
|
||
|
self.CRC = msh2_crc.strcrc(self.name)
|
||
|
|
||
|
def set_name_from_crc(self):
|
||
|
'''Tries to reverse-engineer the CRC into a name
|
||
|
via comparing to the CRCs of all models in this .msh.'''
|
||
|
if not self.CRC:
|
||
|
raise MSH2Error('Bone doesnt have a CRC applied for reverse-engineering.')
|
||
|
names = self.collection.get_check_names_list()
|
||
|
name = msh2_crc.compare_crc_adv(names, self.CRC)
|
||
|
if name:
|
||
|
self.name = name
|
||
|
self.collection.remove_check_name(name)
|
||
|
|
||
|
def pack_SKL2(self):
|
||
|
'''Packs attributes for the SKL2 chunk.'''
|
||
|
data = [self.CRC,
|
||
|
struct.pack('<L', self.bone_type),
|
||
|
struct.pack('<f', self.constrain),
|
||
|
struct.pack('<L', self.bone1len),
|
||
|
struct.pack('<L', self.bone2len)]
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack_BLN2(self):
|
||
|
'''Packs attributes for the BLN2 chunk.'''
|
||
|
data = [self.CRC,
|
||
|
struct.pack('<L', self.blend_factor)]
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack_KFR3(self):
|
||
|
'''Packs position and rotation frames for the KFR3 chunk.'''
|
||
|
data = [self.CRC,
|
||
|
struct.pack('<L', self.keyframe_type),
|
||
|
struct.pack('<L', len(self.pos_keyframes)),
|
||
|
struct.pack('<L', len(self.rot_keyframes))]
|
||
|
for ndx, frame in enumerate(self.pos_keyframes):
|
||
|
data.append(struct.pack('<L', ndx + self.collection.animation.cycle.frames[0]))
|
||
|
data.append(struct.pack('<fff', *frame))
|
||
|
for ndx, frame in enumerate(self.rot_keyframes):
|
||
|
data.append(struct.pack('<L', ndx + self.collection.animation.cycle.frames[0]))
|
||
|
data.append(struct.pack('<ffff', *frame))
|
||
|
return ''.join(data)
|
||
|
|
||
|
|
||
|
class BoneCollection(Packer):
|
||
|
# Params:
|
||
|
# - animation: class(Animation)
|
||
|
def __init__(self, animation=None):
|
||
|
if animation:
|
||
|
self.animation = animation
|
||
|
else:
|
||
|
self.animation = None
|
||
|
self.classname = 'BoneCollection'
|
||
|
self.bones = []
|
||
|
self.check_names = None
|
||
|
|
||
|
def add(self, bone):
|
||
|
'''Adds bone to the collection and sets its
|
||
|
collection attribute.'''
|
||
|
bone.collection = self
|
||
|
self.bones.append(bone)
|
||
|
|
||
|
def remove(self, index):
|
||
|
'''Remove bone at index index.'''
|
||
|
del self.bones[index]
|
||
|
|
||
|
def replace(self, bones):
|
||
|
'''Replace internal bones list with bones.'''
|
||
|
self.bones = bones
|
||
|
|
||
|
def get_by_name(self, name):
|
||
|
'''Get bone by name.'''
|
||
|
for bone in self.bones:
|
||
|
if name == bone.name:
|
||
|
return bone
|
||
|
|
||
|
def get_check_names_list(self):
|
||
|
'''Returns list of all bone names.
|
||
|
This list will be reduced if the name of a bone is found
|
||
|
to maximize performance. It's used to calculate the name
|
||
|
of a bone by comparing the CRC in the .msh file and a
|
||
|
newly generated CRC of every bone in this list.'''
|
||
|
if not self.check_names:
|
||
|
self.check_names = self.animation.msh.models.get_names_list()
|
||
|
return self.check_names
|
||
|
|
||
|
def remove_check_name(self, name):
|
||
|
'''If the name of a bone is matched with a CRC, remove
|
||
|
the name from the list to reduce iteration time.'''
|
||
|
self.check_names.remove(name)
|
||
|
|
||
|
def __str__(self):
|
||
|
return str(self.bones)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return str(self.bones)
|
||
|
|
||
|
def __getitem__(self, key):
|
||
|
return self.bones[key]
|
||
|
|
||
|
def __len__(self):
|
||
|
return len(self.bones)
|
||
|
|
||
|
def pack_SKL2(self):
|
||
|
data = ['SKL2']
|
||
|
data.append(struct.pack('<L', len(self.bones) * 20 + 4))
|
||
|
data.append(struct.pack('<L', len(self.bones)))
|
||
|
for bone in self.bones:
|
||
|
data.append(bone.pack_SKL2())
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack_BLN2(self):
|
||
|
data = ['BLN2']
|
||
|
data.append(struct.pack('<L', len(self.bones) * 8 + 4))
|
||
|
data.append(struct.pack('<L', len(self.bones)))
|
||
|
for bone in self.bones:
|
||
|
data.append(bone.pack_BLN2())
|
||
|
return ''.join(data)
|
||
|
|
||
|
def pack_KFR3(self):
|
||
|
data = ['KFR3', 'size']
|
||
|
data.append(struct.pack('<L', len(self.bones)))
|
||
|
for bone in self.bones:
|
||
|
data.append(bone.pack_KFR3())
|
||
|
data[1] = struct.pack('<L', self.sum_seq(data, 3) + 4)
|
||
|
return ''.join(data)
|
||
|
|
||
|
|
||
|
class Color(object):
|
||
|
def __init__(self, color=None):
|
||
|
if color:
|
||
|
self.red = color[0]
|
||
|
self.green = color[1]
|
||
|
self.blue = color[2]
|
||
|
self.alpha = color[3]
|
||
|
else:
|
||
|
self.red = 128
|
||
|
self.green = 128
|
||
|
self.blue = 128
|
||
|
self.alpha = 255
|
||
|
self.classname = 'Color'
|
||
|
|
||
|
def get_json(self):
|
||
|
data = {
|
||
|
'rgba': self.rgba,
|
||
|
}
|
||
|
return data
|
||
|
|
||
|
@staticmethod
|
||
|
def from_json(data):
|
||
|
col = Color()
|
||
|
col.rgba = data['rgba']
|
||
|
return col
|
||
|
|
||
|
@property
|
||
|
def rgba(self):
|
||
|
return self.red, self.green, self.blue, self.alpha
|
||
|
|
||
|
@rgba.setter
|
||
|
def rgba(self, value):
|
||
|
self.red, self.green, self.blue, self.alpha = value
|
||
|
|
||
|
def get_f(self):
|
||
|
if isinstance(self.red, float):
|
||
|
return self.red, self.green, self.blue, self.alpha
|
||
|
else:
|
||
|
return ((self.red / 255.), (self.green / 255.),
|
||
|
(self.blue / 255.), (self.alpha / 255.))
|
||
|
|
||
|
def get_b(self):
|
||
|
if isinstance(self.red, float):
|
||
|
return (int(self.red * 255), int(self.green * 255),
|
||
|
int(self.blue * 255), int(self.alpha * 255))
|
||
|
else:
|
||
|
return self.red, self.green, self.blue, self.alpha
|
||
|
|
||
|
def get(self):
|
||
|
return self.red, self.green, self.blue, self.alpha
|
||
|
|
||
|
def set(self, r, g, b, a=255):
|
||
|
self.red = r
|
||
|
self.green = g
|
||
|
self.blue = b
|
||
|
self.alpha = a
|
||
|
|
||
|
def __str__(self):
|
||
|
return '{0}, {1}, {2}, {3}'.format(self.red, self.green, self.blue, self.alpha)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return self.__str__()
|
||
|
|
||
|
def pack(self, mode='B'):
|
||
|
'''Packs color channels, default type is B(bool).
|
||
|
No converting is done, so if you pass f for mode
|
||
|
but the values are from 0-255 you will get that
|
||
|
value as float.'''
|
||
|
return struct.pack('<{0}'.format(mode * 4),
|
||
|
self.red,
|
||
|
self.green,
|
||
|
self.blue,
|
||
|
self.alpha)
|
||
|
|
||
|
def pack_bgra(self, mode='B'):
|
||
|
'''Same as pack but packs into BGRA ordering.'''
|
||
|
return struct.pack('<{0}'.format(mode * 4),
|
||
|
self.blue,
|
||
|
self.green,
|
||
|
self.red,
|
||
|
self.alpha)
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
class Transform(object):
|
||
|
def __init__(self, tra=None, rot=None, sca=None):
|
||
|
if rot:
|
||
|
self.rotation = rot
|
||
|
else:
|
||
|
self.rotation = 0.0, 0.0, 0.0, 1.0
|
||
|
if tra:
|
||
|
self.translation = tra
|
||
|
else:
|
||
|
self.translation = 0.0, 0.0, 0.0
|
||
|
if sca:
|
||
|
self.scale = sca
|
||
|
else:
|
||
|
self.scale = 1.0, 1.0, 1.0
|
||
|
self.classname = 'Transform'
|
||
|
|
||
|
def get_json(self):
|
||
|
data = {
|
||
|
'position': self.translation,
|
||
|
'rotation': self.euler_angles(),
|
||
|
'scale': self.scale,
|
||
|
}
|
||
|
return data
|
||
|
|
||
|
@classmethod
|
||
|
def from_json(cls, data):
|
||
|
tr = cls()
|
||
|
tr.translation = data['position']
|
||
|
tr.rotation = tr.euler_to_quaternion(data['rotation'])
|
||
|
tr.scale = data['scale']
|
||
|
return tr
|
||
|
|
||
|
def __str__(self):
|
||
|
return 'Pos({0}), Rot({1}), Scl({2})'.format(', '.join([str(i) for i in self.translation]),
|
||
|
', '.join([str(i) for i in self.rotation]),
|
||
|
', '.join([str(i) for i in self.scale]))
|
||
|
|
||
|
def __repr__(self):
|
||
|
return 'Pos({0}), Rot({1}), Scl({2})'.format(', '.join([str(i) for i in self.translation]),
|
||
|
', '.join([str(i) for i in self.rotation]),
|
||
|
', '.join([str(i) for i in self.scale]))
|
||
|
|
||
|
def pack(self):
|
||
|
data = ['TRAN', struct.pack('<L', 40)]
|
||
|
data.append(struct.pack('<fff', *self.scale))
|
||
|
data.append(struct.pack('<ffff', *self.rotation))
|
||
|
data.append(struct.pack('<fff', *self.translation))
|
||
|
return ''.join(data)
|
||
|
|
||
|
def euler_angles(self):
|
||
|
'''Returns the internal Quaternion in Euler Angles(radians).'''
|
||
|
test = self.rotation[0] * self.rotation[1] + self.rotation[2] * self.rotation[3]
|
||
|
if test > 0.499:
|
||
|
heading = 2 * math.atan2(self.rotation[0], self.rotation[3])
|
||
|
attitude = math.pi / 2
|
||
|
bank = 0
|
||
|
return math.degrees(heading), math.degrees(attitude), math.degrees(bank)
|
||
|
elif test < -0.499:
|
||
|
heading = -2 * math.atan2(self.rotation[0], self.rotation[3])
|
||
|
attitude = -1 * math.pi / 2
|
||
|
bank = 0
|
||
|
return math.degrees(heading), math.degrees(attitude), math.degrees(bank)
|
||
|
sqx = self.rotation[0] * self.rotation[0]
|
||
|
sqy = self.rotation[1] * self.rotation[1]
|
||
|
sqz = self.rotation[2] * self.rotation[2]
|
||
|
heading = math.atan2((2 * self.rotation[1] * self.rotation[3] -
|
||
|
2 * self.rotation[0] * self.rotation[2]),
|
||
|
1 - 2 * sqy - 2 * sqz)
|
||
|
attitude = math.asin(2 * test)
|
||
|
bank = math.atan2((2 * self.rotation[0] * self.rotation[3] -
|
||
|
2 * self.rotation[1] * self.rotation[2]),
|
||
|
1 - 2 * sqx - 2 * sqz)
|
||
|
return math.degrees(heading), math.degrees(attitude), math.degrees(bank)
|
||
|
|
||
|
def euler_to_quaternion(self, euler):
|
||
|
c1 = math.cos(math.radians(euler[0] / 2))
|
||
|
s1 = math.sin(math.radians(euler[0] / 2))
|
||
|
c2 = math.cos(math.radians(euler[1] / 2))
|
||
|
s2 = math.sin(math.radians(euler[1] / 2))
|
||
|
c3 = math.cos(math.radians(euler[2] / 2))
|
||
|
s3 = math.sin(math.radians(euler[2] / 2))
|
||
|
c1c2 = c1 * c2
|
||
|
s1s2 = s1 * s2
|
||
|
w = c1c2 * c3 - s1s2 * s3
|
||
|
x = c1c2 * s3 + s1s2 * c3
|
||
|
y = s1 * c2 * c3 + c1 * s2 * s3
|
||
|
z = c1 * s2 * c3 - s1 * c2 * s3
|
||
|
self.rotation = x, y, z, w
|
||
|
return x, y, z, w
|
||
|
|
||
|
def reversed_quaternion(self):
|
||
|
'''Returns the Quaternion as W, X, Y, Z.'''
|
||
|
return self.rotation[3], self.rotation[0], self.rotation[1], self.rotation[2]
|
||
|
|
||
|
|
||
|
class BBox(Transform):
|
||
|
def __init__(self):
|
||
|
self.rotation = 0.0, 0.0, 0.0, 1.0
|
||
|
self.extents = 4.0, 4.0, 4.0
|
||
|
self.center = 0.0, 0.0, 0.0
|
||
|
self.radius = 6.92
|
||
|
self.classname = 'BBox'
|
||
|
|
||
|
def get_json(self):
|
||
|
data = {
|
||
|
'rotation': self.euler_angles(),
|
||
|
'extents': self.extents,
|
||
|
'center': self.center,
|
||
|
'radius': self.radius,
|
||
|
}
|
||
|
return data
|
||
|
|
||
|
@staticmethod
|
||
|
def from_json(data):
|
||
|
b = BBox()
|
||
|
b.rotation = b.euler_to_quaternion(data['rotation'])
|
||
|
b.extents = data['extents']
|
||
|
b.center = data['center']
|
||
|
b.radius = data['radius']
|
||
|
return b
|
||
|
|
||
|
def pack(self):
|
||
|
data = ['BBOX', struct.pack('<L', 44)]
|
||
|
data.append(struct.pack('<ffff', *self.rotation))
|
||
|
data.append(struct.pack('<fff', *self.center))
|
||
|
data.append(struct.pack('<fff', *self.extents))
|
||
|
data.append(struct.pack('<f', self.radius))
|
||
|
return ''.join(data)
|