Blender-ZeroEngine-MSH2-Plugin/src/blender_addon/io_mesh_msh2/msh2/msh2_data.py

2766 lines
95 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 os
import itertools
import struct
import math
# import logging
from .Logger import Logger
logging = Logger("Msh2").get_logger()
try:
import bson
json = bson
except Exception as e:
import json
from . import msh2_crc
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 == b'':
return b''
i = 0
while len(string) >= i:
i += 4
return string.ljust(i, b'\x00')
def null_terminate(self, string):
'''Null-terminates the given string.'''
return string + b'\x00'
def pack_long_chunk(self, header, number):
'''Chunk with only one long int.'''
data = [header.encode("ascii")]
data.append(struct.pack('<L', 4))
data.append(struct.pack('<L', number))
return b''.join(data)
def pack_string_chunk(self, header, string):
'''String Chunk(NAME, TX0D...) packer.'''
data = [header.encode("ascii")]
comp_str = self.pad_string(string)
data.append(struct.pack('<L', len(comp_str)))
data.append(comp_str)
return b''.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=(',', ': ')))
fh.write(json.dumps(data))
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=(',', ': ')))
fh.write(json.dumps(self.info.get_json()))
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()))
# 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 = b''
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 = b''
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 = [b'CL1L']
data.append(struct.pack('<L', 0))
return b''.join(data)
def pack(self):
'''Packs data to .msh file structure and returns the data as a string.'''
data = [b'HEDR', 'size']
# First pack the MSH2 header with all its children chunks.
msh2_data = [b'MSH2', 'size']
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(b''.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 b''.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 = [b'HEDR', 'size']
# First pack the MSH2 header with all its children chunks.
msh2_data = [b'MSH2', 'size']
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(b''.join(msh2_data))
# HEDR only have 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 b''.join(data)
class SceneInfo(Packer):
def __init__(self, msh=None):
# Reference to Msh.
self.msh = msh
self.name = b''
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 = [b'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 b''.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 = [b'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 b''.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
self.collection = coll
self.name = b''
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 = [b'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 b''.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 = [b'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 b''.join(data)
def pack_ATRB(self):
'''Packs the material render attributes.'''
data = [b'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 b''.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 b''.join(data)
class MaterialCollection(Packer):
def __init__(self, msh=None, materials=[]):
# Reference to Msh.
self.msh = msh
self.classname = 'MaterialCollection'
self.materials = 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 = [b'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 b''.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.
self.collection = collection
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))
# 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=(',', ': ')))
fh.write(json.dumps(segment.get_json()))
@classmethod
def load_segmented_json(cls, folder, name):
logging.debug('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 = [b'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 b''.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 = [b'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 b''.join(data)
def pack_ENVL(self):
data = [b'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 b''.join(data)
def pack_SWCI(self):
'''Packs the collision primitive chunk.'''
data = [b'SWCI']
data.append(struct.pack('<L', 16))
data.append(struct.pack('<Lfff', *self.primitive))
return b''.join(data)
def repack(self):
'''Repacks the MODL chunk. This should be used to retrieve the model in packed form.'''
data = [b'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 b''.join(data)
def repack_GEOM(self):
data = [b'GEOM']
data.append('size')
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 b''.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 b''.join(data)
def repack(self):
data = [model.repack() for model in self.models]
return b''.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 range(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 = []
logging.debug(type(self.segments))
logging.debug(self.segments)
for segment in self.segments:
data.append(segment.pack())
return b''.join(data)
def repack(self):
data = [bsegment.repack() for segment in self.segments]
return b''.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.
self.collection = collection
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.decode().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'].decode().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.debug('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 = [b'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 b''.join(data)
def repack(self):
return self.pack()
class ShadowGeometry(Packer):
def __init__(self, collection=None):
self.collection = collection
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 = [b'SHDW', 'size', self.data]
data[1] = struct.pack('<L', len(self.data))
return b''.join(data)
def pack(self):
data = [b'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 b''.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 b''.join(data)
class ClothGeometry(Packer):
def __init__(self, collection=None):
self.collection = collection
self.classname = 'ClothGeometry'
self.texture = b'no_texture'
self.vertices = None
self.faces = None
self.stretch = b''
self.stretch_constraints = set()
self.cross = b''
self.cross_constraints = set()
self.bend = b''
self.bend_constraints = set()
self.collision = b''
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 = [b'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 b''.join(data)
def repack_stretch(self):
data = [b'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 b''.join(data)
def pack_cross(self):
data = [b'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 b''.join(data)
def repack_cross(self):
data = [b'CPRS', 'size']
data.append(self.cross)
data[1] = struct.pack('<L', len(self.cross))
return b''.join(data)
def pack_bend(self):
data = [b'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 b''.join(data)
def repack_bend(self):
data = [b'BPRS', 'size']
data.append(self.bend)
data[1] = struct.pack('<L', len(self.cross))
return b''.join(data)
def pack_collision(self):
data = [b'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 b''.join(data)
def repack_collision(self):
data = [b'COLL', 'size']
data.append(self.collision)
data[1] = struct.pack('<L', len(self.collision))
return b''.join(data)
def pack(self):
data = [b'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 b''.join(data)
def repack(self):
data = [b'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 b''.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 = None
if self.collection:
if self.collection.segment:
index_map = self.collection.segment.index_map
if (self.sides == 4) and (index_map is not None):
logging.debug(f"Vertex indices for the STRP index_map chunk are: {self.sides}...")
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):
logging.debug(f"Vertex indices for the STRP index_map chunk are: {self.sides}...")
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):
logging.debug(f"Vertex indices for the STRP chunk are: {self.sides}...")
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):
logging.debug(f"Vertex indices for the STRP chunk are: {self.sides}...")
return struct.pack('<HHH', self.vertices[0] + 0x8000,
self.vertices[1] + 0x8000,
self.vertices[2])
else:
logging.debug("Vertex indices for the STRP chunk are empty...")
return b''
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 range(int(numquads)):
nface = Face(self)
nface.vertices = face.vertices[n * 2:n * 2 + 4]
new_faces.append(nface)
else:
for n in range(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 range(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 = [b'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 b''.join(data)
def pack_cloth(self):
'''Packs triangles of every face into the cloth CMSH chunk.'''
data = [b'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 b''.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).
self.collection = coll
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 b''.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 = [b'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 b''.join(data)
def get_normals(self):
'''Yield all normals.'''
for vertex in self.vertices:
yield vertex.normal
def pack_normals(self):
data = [b'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 b''.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 = [b'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 b''.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 = [b'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 b''.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 = [b'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 b''.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 b''.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 = [b'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 b''.join(data)
def pack_uvs(self):
data = [b'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 b''.join(data)
def pack_fixed(self):
data = [b'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 b''.join(data)
def pack_weights(self):
data = [b'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(b''.join(data2))
data[1] = struct.pack('<L', len(data[3]) + 4)
data[2] = struct.pack('<L', num)
return b''.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 b''.join(data)
def pack_empty(self):
return b''
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 = b'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 = [b'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 b''.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 b''.join(data)
def pack_BLN2(self):
'''Packs attributes for the BLN2 chunk.'''
data = [self.CRC,
struct.pack('<L', self.blend_factor)]
return b''.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 b''.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 = [b'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 b''.join(data)
def pack_BLN2(self):
data = [b'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 b''.join(data)
def pack_KFR3(self):
data = [b'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 b''.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 = [b'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 b''.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 = [b'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 b''.join(data)