From d588b18039f4b3f4c0e9fbbf775056ff1a1512ec Mon Sep 17 00:00:00 2001 From: Danila Date: Tue, 3 May 2022 05:32:30 +0300 Subject: [PATCH] 0.8.8 0.8.6: COLLADA writing fix (inputs count) 0.8.7: Writer and Reader interfaces, fields encapsulating 0.8.8: Added "Universal" format instead of python dicts, some fixes, a lot of refactoring --- .idea/3d-converter.iml | 2 +- .idea/misc.xml | 2 +- models_converter/__init__.py | 3 +- models_converter/formats/__init__.py | 4 +- models_converter/formats/collada/__init__.py | 7 + models_converter/formats/collada/collada.py | 51 ++ models_converter/formats/collada/parser.py | 334 ++++++++ models_converter/formats/collada/writer.py | 376 ++++++++ models_converter/formats/dae.py | 804 ------------------ models_converter/formats/gltf/__init__.py | 1 + models_converter/formats/gltf/parser.py | 236 ++--- models_converter/formats/gltf/writer.py | 96 +++ models_converter/formats/obj.py | 240 ------ models_converter/formats/scw/chunks/came.py | 31 +- models_converter/formats/scw/chunks/chunk.py | 55 +- models_converter/formats/scw/chunks/geom.py | 289 +++---- models_converter/formats/scw/chunks/head.py | 37 +- models_converter/formats/scw/chunks/mate.py | 4 +- models_converter/formats/scw/chunks/node.py | 206 ++--- models_converter/formats/scw/chunks/wend.py | 8 - models_converter/formats/scw/parser.py | 67 +- models_converter/formats/scw/writer.py | 54 +- .../formats/universal/__init__.py | 8 + models_converter/formats/universal/camera.py | 26 + .../formats/universal/geometry.py | 118 +++ models_converter/formats/universal/node.py | 106 +++ models_converter/formats/universal/scene.py | 45 + .../formats/wavefront/__init__.py | 7 + models_converter/formats/wavefront/parser.py | 145 ++++ models_converter/formats/wavefront/writer.py | 56 ++ models_converter/interfaces/__init__.py | 7 + .../interfaces/parser_interface.py | 11 + .../interfaces/writer_interface.py | 13 + models_converter/utilities/__init__.py | 20 + models_converter/utilities/math/__init__.py | 7 + models_converter/utilities/math/quaternion.py | 9 + models_converter/utilities/math/vector3.py | 8 + .../{utils => utilities}/matrix/__init__.py | 0 .../{utils => utilities}/matrix/matrix2x2.py | 0 .../{utils => utilities}/matrix/matrix3x3.py | 0 .../{utils => utilities}/matrix/matrix4x4.py | 57 +- .../{utils => utilities}/reader.py | 4 +- .../{utils => utilities}/writer.py | 8 +- models_converter/utils/__init__.py | 5 - setup.py | 2 +- 45 files changed, 1884 insertions(+), 1685 deletions(-) create mode 100644 models_converter/formats/collada/__init__.py create mode 100644 models_converter/formats/collada/collada.py create mode 100644 models_converter/formats/collada/parser.py create mode 100644 models_converter/formats/collada/writer.py delete mode 100644 models_converter/formats/dae.py create mode 100644 models_converter/formats/gltf/writer.py delete mode 100644 models_converter/formats/obj.py create mode 100644 models_converter/formats/universal/__init__.py create mode 100644 models_converter/formats/universal/camera.py create mode 100644 models_converter/formats/universal/geometry.py create mode 100644 models_converter/formats/universal/node.py create mode 100644 models_converter/formats/universal/scene.py create mode 100644 models_converter/formats/wavefront/__init__.py create mode 100644 models_converter/formats/wavefront/parser.py create mode 100644 models_converter/formats/wavefront/writer.py create mode 100644 models_converter/interfaces/__init__.py create mode 100644 models_converter/interfaces/parser_interface.py create mode 100644 models_converter/interfaces/writer_interface.py create mode 100644 models_converter/utilities/__init__.py create mode 100644 models_converter/utilities/math/__init__.py create mode 100644 models_converter/utilities/math/quaternion.py create mode 100644 models_converter/utilities/math/vector3.py rename models_converter/{utils => utilities}/matrix/__init__.py (100%) rename models_converter/{utils => utilities}/matrix/matrix2x2.py (100%) rename models_converter/{utils => utilities}/matrix/matrix3x3.py (100%) rename models_converter/{utils => utilities}/matrix/matrix4x4.py (87%) rename models_converter/{utils => utilities}/reader.py (97%) rename models_converter/{utils => utilities}/writer.py (95%) delete mode 100644 models_converter/utils/__init__.py diff --git a/.idea/3d-converter.iml b/.idea/3d-converter.iml index 8388dbc..d0876a7 100644 --- a/.idea/3d-converter.iml +++ b/.idea/3d-converter.iml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index d56657a..59aa5e3 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/models_converter/__init__.py b/models_converter/__init__.py index f5dd35a..1e5bd87 100644 --- a/models_converter/__init__.py +++ b/models_converter/__init__.py @@ -1,5 +1,4 @@ __all__ = [ 'formats', - 'chunks', - 'utils' + 'utilities' ] diff --git a/models_converter/formats/__init__.py b/models_converter/formats/__init__.py index 3e518f9..701c33a 100644 --- a/models_converter/formats/__init__.py +++ b/models_converter/formats/__init__.py @@ -1,6 +1,6 @@ __all__ = [ 'scw', - 'dae', - 'obj', + 'collada', + 'wavefront', 'gltf' ] diff --git a/models_converter/formats/collada/__init__.py b/models_converter/formats/collada/__init__.py new file mode 100644 index 0000000..cb85844 --- /dev/null +++ b/models_converter/formats/collada/__init__.py @@ -0,0 +1,7 @@ +from .writer import Writer +from .parser import Parser + +__all__ = [ + 'Writer', + 'Parser' +] diff --git a/models_converter/formats/collada/collada.py b/models_converter/formats/collada/collada.py new file mode 100644 index 0000000..7caf9c0 --- /dev/null +++ b/models_converter/formats/collada/collada.py @@ -0,0 +1,51 @@ +from xml.etree.ElementTree import * + + +VERSION = '1.4.1' +NAMESPACE = 'http://www.collada.org/2005/11/COLLADASchema' + + +class Collada: + def __init__(self): + self.root = Element('COLLADA', version=VERSION, xmlns=NAMESPACE) + + @staticmethod + def write_source(parent, + source_id: str, + array_tag: str, + array_data: tuple, + stride: int, + params: list): + source = SubElement(parent, 'source', id=source_id) + + array = SubElement(source, array_tag) + array.attrib = {'id': f'{source_id}-array', + 'count': f'{len(array_data) * stride}'} + + technique_common = SubElement(source, 'technique_common') + accessor = SubElement(technique_common, 'accessor') + accessor.attrib = {'source': f'#{source_id}-array', + 'count': f'{len(array_data)}', + 'stride': f'{stride}'} + + for param_data in params: + param = SubElement(accessor, 'param') + param.attrib = param_data + + array.text = ' '.join(array_data) + + @staticmethod + def write_input(parent, + semantic: str, + source_id: str, + offset: int = None): + attributes = { + 'semantic': semantic, + 'source': f'#{source_id}' + } + + if offset is not None: + attributes['offset'] = f'{offset}' + + _input = SubElement(parent, 'input') + _input.attrib = attributes diff --git a/models_converter/formats/collada/parser.py b/models_converter/formats/collada/parser.py new file mode 100644 index 0000000..d7889c5 --- /dev/null +++ b/models_converter/formats/collada/parser.py @@ -0,0 +1,334 @@ +from xml.etree.ElementTree import * + +from .collada import NAMESPACE +from ..universal import Node, Scene, Geometry +from ...interfaces import ParserInterface +from ...utilities import remove_suffix +from ...utilities.matrix.matrix4x4 import Matrix4x4 + +NAMESPACES = { + 'collada': NAMESPACE +} + + +class Parser(ParserInterface): + def __init__(self, file_data): + self.library_materials = None + self.library_effects = None + self.library_geometries = None + self.library_controllers = None + self.instance_scene = None + self.library_scenes = None + + self.scene = Scene() + + root = fromstring(file_data) + + self.find_libraries(root) + + self.instance_scene = root.find('./collada:scene', NAMESPACES).find('collada:instance_visual_scene', NAMESPACES) + + def find_libraries(self, root): + self.library_materials = root.find('./collada:library_materials', NAMESPACES) + if self.library_materials is None: + self.library_materials = [] + + self.library_effects = root.find('./collada:library_effects', NAMESPACES) + self.library_geometries = root.find('./collada:library_geometries', NAMESPACES) + self.library_controllers = root.find('./collada:library_controllers', NAMESPACES) + self.library_scenes = root.find('./collada:library_visual_scenes', NAMESPACES) + + def parse(self): + self.parse_materials() + + scene_url = self.instance_scene.attrib['url'][1:] + scene = self.library_scenes.find(f'collada:visual_scene[@id="{scene_url}"]', NAMESPACES) + + self.parse_node(scene.findall('collada:node', NAMESPACES)) + self.parse_nodes() + + def parse_materials(self): + for material in self.library_materials: + material_name = material.attrib['name'] + + instance_effect = material.find('collada:instance_effect', NAMESPACES) + if instance_effect is not None: + effect_url = instance_effect.attrib['url'][1:] + effect = self.library_effects.find(f'collada:effect[@id="{effect_url}"]', NAMESPACES) + + if effect is not None: + # profile = None + # for item in effect: + # if 'profile' in item.tag: + # profile = item + # technique = profile.find('collada:technique', NAMESPACES) + # + # emission_data = None + # ambient_data = None + # diffuse_data = None + # + # emission = technique[0].find('collada:emission', NAMESPACES) + # ambient = technique[0].find('collada:ambient', NAMESPACES) + # diffuse = technique[0].find('collada:diffuse', NAMESPACES) + # + # if 'color' in emission[0].tag: + # emission_data = [float(item) for item in emission[0].text.split()] + # emission_data[3] *= 255 + # elif 'texture' in emission[0].tag: + # # emission_data = emission[0].attrib['texture'] + # emission_data = '.' + # + # if 'color' in ambient[0].tag: + # ambient_data = [float(item) for item in ambient[0].text.split()] + # ambient_data[3] *= 255 + # elif 'texture' in ambient[0].tag: + # # ambient_data = ambient[0].attrib['texture'] + # ambient_data = '.' + # + # if 'color' in diffuse[0].tag: + # diffuse_data = [float(item) for item in diffuse[0].text.split()] + # diffuse_data[3] *= 255 + # elif 'texture' in diffuse[0].tag: + # # diffuse_data = diffuse[0].attrib['texture'] + # diffuse_data = '.' + + material_data = { + 'name': material_name, + 'shader': 'shader/uber.vsh', + 'effect': { + 'ambient': [0, 0, 0, 255], # ambient_data, + 'diffuse': '.', # diffuse_data, + 'specular': '.', + 'colorize': [255, 255, 255, 255], + 'emission': [0, 0, 0, 255], # emission_data, + 'lightmaps': { + 'diffuse': 'sc3d/diffuse_lightmap.png', + 'specular': 'sc3d/specular_lightmap.png' + }, + 'shader_define_flags': 3014 + } + } + + self.scene.add_material(material_data) + + def parse_node(self, xml_nodes: list, parent: str = None): + for xml_node in xml_nodes: + if not ('name' in xml_node.attrib): + xml_node.attrib['name'] = xml_node.attrib['id'] + + node: Node = Node( + name=xml_node.attrib['name'], + parent=parent + ) + + instance_geometry = xml_node.findall('collada:instance_geometry', NAMESPACES) + instance_controller = xml_node.findall('collada:instance_controller', NAMESPACES) + + for xml_instance in [*instance_geometry, *instance_controller]: + if instance_geometry: + instance = Node.Instance(name=xml_instance.attrib['url'][1:], instance_type='GEOM') + elif instance_controller: + instance = Node.Instance(name=xml_instance.attrib['url'][1:], instance_type='CONT') + else: + continue + + bind_material = xml_instance.find('collada:bind_material', NAMESPACES) + + technique_common = bind_material[0] + for instance_material in technique_common: + instance.add_bind(instance_material.attrib['symbol'], instance_material.attrib['target'][1:]) + + node.add_instance(instance) + + xml_matrix = xml_node.findall('collada:matrix', NAMESPACES) + if xml_matrix: + matrix = xml_matrix[0].text.split() + matrix = [[float(value) for value in matrix[x:x + 4]] for x in range(0, len(matrix), 4)] + + matrix = Matrix4x4(matrix=matrix) + + scale = matrix.get_scale() + position = matrix.get_position() + rotation = matrix.get_rotation() + + node.add_frame(Node.Frame(0, position, scale, rotation)) + + self.scene.add_node(node) + self.parse_node(xml_node.findall('collada:node', NAMESPACES), node.get_name()) + + def parse_nodes(self): + nodes = self.scene.get_nodes() + for node_index in range(len(nodes)): + node = nodes[node_index] + for instance in node.get_instances(): + controller = None + collada_geometry = None + + if instance.get_type() == 'CONT': + controller = self.library_controllers.find(f'collada:controller[@id="{instance.get_name()}"]', + NAMESPACES) + geometry_url = controller[0].attrib['source'][1:] + collada_geometry = self.library_geometries.find(f'collada:geometry[@id="{geometry_url}"]', + NAMESPACES) + elif instance.get_type() == 'GEOM': + collada_geometry = self.library_geometries.find(f'collada:geometry[@id="{instance.get_name()}"]', + NAMESPACES) + + if not ('name' in collada_geometry.attrib): + collada_geometry.attrib['name'] = collada_geometry.attrib['id'] + + instance._name = collada_geometry.attrib['name'] + + for suffix in ('-skin', '-cont'): + instance._name = remove_suffix(instance.get_name(), suffix) + for suffix in ('-mesh', '-geom'): + instance._name = remove_suffix(instance.get_name(), suffix) + + if collada_geometry is not None: + geometry = self.parse_geometry(collada_geometry) + if controller is not None: + self.parse_controller(controller, geometry) + + def parse_controller(self, collada_controller, geometry: Geometry): + skin = collada_controller[0] + + bind_shape_matrix = skin.find('collada:bind_shape_matrix', NAMESPACES).text + + geometry.set_controller_bind_matrix(list(map(float, bind_shape_matrix.split()))) + + joints = skin.find('collada:joints', NAMESPACES) + joint_inputs = joints.findall('collada:input', NAMESPACES) + for _input in joint_inputs: + # semantic = _input.attrib['semantic'] + source_url = _input.attrib['source'] + source = skin.find(f'collada:source[@id="{source_url[1:]}"]', NAMESPACES) + + accessor = source.find('collada:technique_common/collada:accessor', NAMESPACES) + accessor_stride = int(accessor.attrib['stride']) + accessor_source_url = accessor.attrib['source'] + accessor_source = source.find(f'collada:*[@id="{accessor_source_url[1:]}"]', NAMESPACES) + params = accessor.findall('collada:param', NAMESPACES) + + for param in params: + param_name = param.attrib['name'] + # param_type = param.attrib['type'] + + source_data = accessor_source.text.split() + if param_name == 'JOINT': + for name in source_data: + geometry.add_joint(Geometry.Joint(name, None)) + + if param_name == 'TRANSFORM': + for x in range(int(accessor_source.attrib['count']) // int(accessor_stride)): + matrix = [] + for y in source_data[x * accessor_stride:(x + 1) * accessor_stride]: + matrix.append(float(y)) + geometry.get_joints()[x].set_matrix(matrix) + + vertex_weights = skin.find('collada:vertex_weights', NAMESPACES) + vertex_weights_inputs = vertex_weights.findall('collada:input', NAMESPACES) + for _input in vertex_weights_inputs: + semantic = _input.attrib['semantic'] + source_url = _input.attrib['source'] + source = skin.find(f'collada:source[@id="{source_url[1:]}"]', NAMESPACES) + + if semantic == 'WEIGHT': + accessor = source.find('collada:technique_common/collada:accessor', NAMESPACES) + accessor_source_url = accessor.attrib['source'] + accessor_source = source.find(f'collada:*[@id="{accessor_source_url[1:]}"]', NAMESPACES) + + weights = None + params = accessor.findall('collada:param', NAMESPACES) + for param in params: + param_name = param.attrib['name'] + # param_type = param.attrib['type'] + + if param_name == 'WEIGHT': + weights = [float(x) for x in accessor_source.text.split()] + break + + if weights is None: + continue + + vcount = vertex_weights.find('collada:vcount', NAMESPACES).text + + v = vertex_weights.find('collada:v', NAMESPACES).text + v = map(int, v.split()) + + for count in map(int, vcount.split()): + for i in range(count): + joint_index = next(v) + strength_index = next(v) + geometry.add_weight(Geometry.Weight(joint_index, weights[strength_index])) + + while count < 4: + geometry.add_weight(Geometry.Weight(0, 0)) + count += 1 + break + + def parse_geometry(self, collada_geometry) -> Geometry: + name = collada_geometry.attrib['name'] + + for suffix in ('-mesh', '-geom'): + name = remove_suffix(name, suffix) + + geometry = Geometry(name=name, group='GEO') + + mesh = collada_geometry[0] + + triangles = mesh.findall('collada:triangles', NAMESPACES) + if triangles: + pass + else: + triangles = mesh.findall('collada:polylist', NAMESPACES) + inputs = triangles[0].findall('collada:input', NAMESPACES) + for _input in inputs: + semantic = _input.attrib['semantic'] + source_link = _input.attrib['source'][1:] + source = mesh.find(f'*[@id="{source_link}"]') + + if semantic == 'VERTEX': + vertices_input = source[0] + semantic = vertices_input.attrib['semantic'] + source_link = vertices_input.attrib['source'][1:] + source = mesh.find(f'*[@id="{source_link}"]') + + float_array = source.find('collada:float_array', NAMESPACES) + accessor = source.find('collada:technique_common/collada:accessor', NAMESPACES) + + points_temp = [float(floating) for floating in float_array.text.split()] + + scale = max(max(points_temp), abs(min(points_temp))) + if scale < 1: + scale = 1 + if semantic == 'TEXCOORD': + points_temp[1::2] = [1 - x for x in points_temp[1::2]] + points_temp = [value / scale for value in points_temp] + + points = [] + for x in range(0, len(points_temp), len(accessor)): + points.append(points_temp[x: x + len(accessor)]) + + geometry.add_vertex(Geometry.Vertex( + name='', + vertex_type=semantic, + vertex_index=len(geometry.get_vertices()), + vertex_scale=scale, + points=points + )) + for triangle in triangles: + triangles_material = triangle.attrib['material'] + + p = triangle.find('collada:p', NAMESPACES) + triangles_temp = [int(integer) for integer in p.text.split()] + + triangles = [ + [ + triangles_temp[polygon_index + point_index:polygon_index + point_index + 3] + for point_index in range(0, len(inputs) * 3, 3) + ] for polygon_index in range(0, len(triangles_temp), len(inputs) * 3) + ] + geometry.add_material(Geometry.Material(name=triangles_material, triangles=triangles)) + self.scene.add_geometry(geometry) + + return geometry diff --git a/models_converter/formats/collada/writer.py b/models_converter/formats/collada/writer.py new file mode 100644 index 0000000..2d123c7 --- /dev/null +++ b/models_converter/formats/collada/writer.py @@ -0,0 +1,376 @@ +from xml.etree.ElementTree import * + +from .collada import Collada +from ..universal import Scene, Node, Geometry +from ...interfaces import WriterInterface +from ...utilities.matrix.matrix4x4 import Matrix4x4 + + +class Writer(WriterInterface): + def __init__(self): + self.writen = None + self.dae = Collada() + + self.library_materials = None + self.library_effects = None + self.library_images = None + self.library_geometries = None + self.library_controllers = None + self.library_animations = None + self.library_cameras = None + self.library_visual_scenes = None + + def create_libraries(self): + self.library_materials = SubElement(self.dae.root, 'library_materials') + self.library_effects = SubElement(self.dae.root, 'library_effects') + # self.library_images = SubElement(dae.collada, 'library_images') + self.library_geometries = SubElement(self.dae.root, 'library_geometries') + self.library_controllers = SubElement(self.dae.root, 'library_controllers') + self.library_animations = SubElement(self.dae.root, 'library_animations') + # self.library_cameras = SubElement(self.dae.collada, 'library_cameras') + self.library_visual_scenes = SubElement(self.dae.root, 'library_visual_scenes') + + def sign(self): + asset = SubElement(self.dae.root, 'asset') + + contributor = SubElement(asset, 'contributor') + SubElement(contributor, 'author').text = 'Vorono4ka' + SubElement(contributor, 'authoring_tool').text = 'models_converter (https://github.com/vorono4ka/3d-converter)' + + return contributor + + def write(self, scene: Scene): + contributor = self.sign() + + # if 'version' in data['header']: + # SubElement(contributor, 'comments').text = 'Version: ' + str(data['header']['version']) + + self.create_libraries() + + # for material in scene.get_materials(): + # self.create_material(material) + + for geometry in scene.get_geometries(): + geometry_name = self.create_geometry(geometry) + + if geometry.has_controller(): + self.create_controller(geometry_name, geometry) + + self.create_scene(scene) + + self.writen = tostring(self.dae.root, xml_declaration=True).decode() + + def create_material(self, material_data): + material_name = material_data['name'] + SubElement(self.library_materials, 'material', id=material_name) + effect_name = f'{material_name}-effect' + material = SubElement(self.library_materials, 'material', id=material_name) + SubElement(material, 'instance_effect', url=f'#{effect_name}') + effect = SubElement(self.library_effects, 'effect', id=effect_name) + profile = SubElement(effect, 'profile_COMMON') + technique = SubElement(profile, 'technique', sid='common') + ambient_data = material_data['effect']['ambient'] + diffuse_data = material_data['effect']['diffuse'] + emission_data = material_data['effect']['emission'] + specular_data = material_data['effect']['specular'] + phong = SubElement(technique, 'phong') + if type(ambient_data) is list: + ambient = SubElement(phong, 'ambient') + ambient_data[3] /= 255 + ambient_data = [str(item) for item in ambient_data] + SubElement(ambient, 'color').text = ' '.join(ambient_data) + # else: + # SubElement(ambient, 'texture', texture=ambient_data, texcoord='CHANNEL0') + if type(diffuse_data) is list: + diffuse = SubElement(phong, 'diffuse') + diffuse_data[3] /= 255 + diffuse_data = [str(item) for item in diffuse_data] + SubElement(diffuse, 'color').text = ' '.join(diffuse_data) + # else: + # SubElement(diffuse, 'texture', texture=diffuse_data, texcoord='CHANNEL0') + if type(emission_data) is list: + emission = SubElement(phong, 'emission') + emission_data[3] /= 255 + emission_data = [str(item) for item in emission_data] + SubElement(emission, 'color').text = ' '.join(emission_data) + # else: + # SubElement(emission, 'texture', texture=emission_data, texcoord='CHANNEL0') + if type(specular_data) is list: + specular = SubElement(phong, 'specular') + specular_data[3] /= 255 + specular_data = [str(item) for item in specular_data] + SubElement(specular, 'color').text = ' '.join(specular_data) + # else: + # SubElement(specular, 'texture', texture=specular_data, texcoord='CHANNEL0') + + def create_geometry(self, geometry: Geometry): + geometry_name = geometry.get_name() + collada_geometry = SubElement(self.library_geometries, 'geometry', id=f'{geometry_name}-geom') + mesh = SubElement(collada_geometry, 'mesh') + + for vertex in geometry.get_vertices(): + params = [] + + vertex_type = vertex.get_type() + vertex_name = vertex.get_name() + coordinate = vertex.get_points() + stride = len(coordinate[0]) + + if vertex_type == 'VERTEX': + vertex_type = 'POSITION' + + source_name = f'{geometry_name}-{vertex_name}' + + if vertex_type in ['POSITION', 'NORMAL']: + params.append({'name': 'X', 'type': 'float'}) + params.append({'name': 'Y', 'type': 'float'}) + params.append({'name': 'Z', 'type': 'float'}) + elif vertex_type in ['TEXCOORD']: + params.append({'name': 'S', 'type': 'float'}) + params.append({'name': 'T', 'type': 'float'}) + + self.dae.write_source( + mesh, + source_name, + 'float_array', + tuple(' '.join(str(sub_item * vertex.get_scale()) for sub_item in item) for item in coordinate), + stride, + params + ) + + if vertex_type == 'POSITION': + vertices = SubElement(mesh, 'vertices', id=f'{source_name}-vertices') + self.dae.write_input(vertices, 'POSITION', source_name) + for material in geometry.get_materials(): + collada_triangles = SubElement(mesh, 'triangles', + count=f'{len(material.get_triangles())}', + material=material.get_name()) + + inputs_count = len(material.get_triangles()[0][0]) + for vertex_index in range(len(geometry.get_vertices())): + if vertex_index == inputs_count: + break + + vertex = geometry.get_vertices()[vertex_index] + + input_type = vertex.get_type() + if input_type == 'POSITION': + input_type = 'VERTEX' + + source_id = f'{geometry_name}-{vertex.get_name()}' + if input_type == 'VERTEX': + source_id = f'{source_id}-vertices' + + self.dae.write_input(collada_triangles, input_type, source_id, vertex_index) + polygons = SubElement(collada_triangles, 'p') + + formatted_polygons_data = [] + for triangle in material.get_triangles(): + for point in triangle: + for coordinate in point: + formatted_polygons_data.append(str(coordinate)) + + polygons.text = ' '.join(formatted_polygons_data) + return geometry_name + + def create_controller(self, geometry_name: str, geometry: Geometry): + controller = SubElement(self.library_controllers, 'controller', id=f'{geometry_name}-cont') + skin = SubElement(controller, 'skin', source=f'#{geometry_name}-geom') + + SubElement(skin, 'bind_shape_matrix').text = ' '.join(map(str, geometry.get_bind_matrix())) + + joints_names_source_id = f'{geometry_name}-joints' + joints_matrices_source_id = f'{geometry_name}-joints-bind-matrices' + weights_source_id = f'{geometry_name}-weights' + + self.dae.write_source( + skin, + joints_names_source_id, + 'Name_array', + tuple(joint.get_name() for joint in geometry.get_joints()), + 1, + [{'name': 'JOINT', 'type': 'name'}] + ) + + self.dae.write_source( + skin, + joints_matrices_source_id, + 'float_array', + tuple(' '.join(map(str, joint.get_matrix())) for joint in geometry.get_joints()), + 16, + [{'name': 'TRANSFORM', 'type': 'float4x4'}] + ) + + vertex_weights = [] + unique_weights = [] + vcount = [0] * (len(geometry.get_weights()) // 4) + + for weight_index in range(len(geometry.get_weights())): + weight = geometry.get_weights()[weight_index] + if weight.get_strength() == 0: + continue + + vcount[weight_index // 4] += 1 + vertex_weights.append(weight.get_joint_index()) + if weight.get_strength() in unique_weights: + vertex_weights.append(unique_weights.index(weight.get_strength())) + else: + unique_weights.append(weight.get_strength()) + vertex_weights.append(len(unique_weights) - 1) + + self.dae.write_source( + skin, + weights_source_id, + 'float_array', + tuple(map(str, unique_weights)), + 1, + [{'name': 'WEIGHT', 'type': 'float'}] + ) + + joints = SubElement(skin, 'joints') + self.dae.write_input(joints, 'JOINT', joints_names_source_id) + self.dae.write_input(joints, 'INV_BIND_MATRIX', joints_matrices_source_id) + + collada_vertex_weights = SubElement(skin, 'vertex_weights', count=f'{len(vcount)}') + self.dae.write_input(collada_vertex_weights, 'JOINT', joints_names_source_id, 0) + self.dae.write_input(collada_vertex_weights, 'WEIGHT', weights_source_id, 1) + + SubElement(collada_vertex_weights, 'vcount').text = ' '.join(map(str, vcount)) + SubElement(collada_vertex_weights, 'v').text = ' '.join(map(str, vertex_weights)) + + def create_animation(self, node_name, frames, matrix_output, time_input): + animation = SubElement(self.library_animations, 'animation', id=node_name) + + self.dae.write_source( + animation, + f'{node_name}-time-input', + 'float_array', + time_input, + 1, + [{'name': 'TIME', 'type': 'float'}] + ) + + self.dae.write_source( + animation, + f'{node_name}-matrix-output', + 'float_array', + matrix_output, + 16, + [{'name': 'TRANSFORM', 'type': 'float4x4'}] + ) + + self.dae.write_source( + animation, + f'{node_name}-interpolation', + 'Name_array', + tuple('LINEAR' for _ in range(len(frames))), + 1, + [{'name': 'INTERPOLATION', 'type': 'name'}] + ) + + sampler = SubElement(animation, 'sampler', id=f'{node_name}-sampler') + + self.dae.write_input( + sampler, + 'INPUT', + f'{node_name}-time-input' + ) + + self.dae.write_input( + sampler, + 'OUTPUT', + f'{node_name}-matrix-output' + ) + + self.dae.write_input( + sampler, + 'INTERPOLATION', + f'{node_name}-interpolation' + ) + + SubElement(animation, 'channel', + source=f'#{node_name}-sampler', + target=f'{node_name}/transform') + + def create_scene(self, scene: Scene): + visual_scene = SubElement(self.library_visual_scenes, 'visual_scene', + id='3dConverterScene', + name='3d-Converter Scene') + not_joint_nodes = [] + node_index = 0 + parent_name = None + while node_index < len(not_joint_nodes) or len(not_joint_nodes) == 0: + if len(not_joint_nodes) > 0: + parent_name = not_joint_nodes[node_index] + + for _node in scene.get_nodes(): + if _node.get_instances() or _node.get_name() == parent_name: + if not (_node.get_name() in not_joint_nodes): + not_joint_nodes.append(_node.get_name()) + if not (_node.get_parent() in not_joint_nodes): + not_joint_nodes.append(_node.get_parent()) + node_index += 1 + for node in scene.get_nodes(): + self.create_node(visual_scene, node, scene, not_joint_nodes) + + collada_scene = SubElement(self.dae.root, 'scene') + SubElement(collada_scene, 'instance_visual_scene', + url='#3dConverterScene', + name='3d-Converter Scene') + + def create_node(self, visual_scene, node: Node, scene: Scene, not_joint_nodes): + parent_name = node.get_parent() + parent = visual_scene + if parent_name != '': + parent = visual_scene.find(f'.//*[@id="{parent_name}"]') + if parent is None: + parent = visual_scene + node_name = node.get_name() + collada_node = SubElement(parent, 'node', id=node.get_name()) + for instance in node.get_instances(): + bind_material = None + + instance_type = instance.get_type() + if instance_type == 'CONT': + instance_controller = SubElement(collada_node, 'instance_controller', + url=f'#{instance.get_name()}-cont') + bind_material = SubElement(instance_controller, 'bind_material') + elif instance_type == 'GEOM': + instance_controller = SubElement(collada_node, 'instance_geometry', url=f'#{instance.get_name()}-geom') + bind_material = SubElement(instance_controller, 'bind_material') + + if instance_type in ['GEOM', 'CONT']: + technique_common = SubElement(bind_material, 'technique_common') + for bind in instance.get_binds(): + SubElement(technique_common, 'instance_material', + symbol=bind.get_symbol(), + target=f'#{bind.get_target()}') + else: + if not (node.get_name() in not_joint_nodes): + collada_node.attrib['type'] = 'JOINT' + + time_input = [] + matrix_output = [] + for frame_index in range(len(node.get_frames())): + frame = node.get_frames()[frame_index] + frame_id = frame.get_id() + matrix = Matrix4x4(size=(4, 4)) + + time_input.append(str(frame_id / scene.get_frame_rate())) + + matrix.put_rotation(frame.get_rotation()) + matrix.put_position(frame.get_position()) + matrix.put_scale(frame.get_scale()) + + matrix = matrix.translation_matrix @ matrix.rotation_matrix @ matrix.scale_matrix + matrix_values = [] + for row in matrix.matrix: + for column in row: + matrix_values.append(str(column)) + + if frame_index == 0: + SubElement(collada_node, 'matrix', sid='transform').text = ' '.join(matrix_values) + matrix_output.append(' '.join(matrix_values)) + + if len(node.get_frames()) > 1: + self.create_animation(node_name, node.get_frames(), matrix_output, time_input) diff --git a/models_converter/formats/dae.py b/models_converter/formats/dae.py deleted file mode 100644 index 5cc442f..0000000 --- a/models_converter/formats/dae.py +++ /dev/null @@ -1,804 +0,0 @@ -from xml.etree.ElementTree import * - -from ..utils.matrix.matrix4x4 import Matrix4x4 - - -def _(*args): - print('[ScwUtils]', end=' ') - for arg in args: - print(arg, end=' ') - print() - - -class Collada: - def __init__(self): - self.version = '1.4.1' - self.xml_namespace = 'http://www.collada.org/2005/11/COLLADASchema' - self.collada = Element('COLLADA', version=self.version, xmlns=self.xml_namespace) - - @staticmethod - def write_source(parent, - source_id: str, - array_tag: str, - array_data: list, - stride: int, - params: list): - source = SubElement(parent, 'source', id=source_id) - - array = SubElement(source, array_tag) - array.attrib = {'id': f'{source_id}-array', - 'count': f'{len(array_data) * stride}'} - - technique_common = SubElement(source, 'technique_common') - accessor = SubElement(technique_common, 'accessor') - accessor.attrib = {'source': f'#{source_id}-array', - 'count': f'{len(array_data)}', - 'stride': f'{stride}'} - - for param_data in params: - param = SubElement(accessor, 'param') - param.attrib = param_data - - array.text = ' '.join(array_data) - - @staticmethod - def write_input(parent, - semantic: str, - source_id: str, - offset: int = None): - attributes = { - 'semantic': semantic, - 'source': f'#{source_id}' - } - - if offset is not None: - attributes['offset'] = f'{offset}' - - _input = SubElement(parent, 'input') - _input.attrib = attributes - - -class Writer: - def __init__(self): - self.writen = '' - - def write(self, data: dict): - dae = Collada() - asset = SubElement(dae.collada, 'asset') - - # - library_materials = SubElement(dae.collada, 'library_materials') - library_effects = SubElement(dae.collada, 'library_effects') - # library_images = SubElement(dae.collada, 'library_images') - library_geometries = SubElement(dae.collada, 'library_geometries') - library_controllers = SubElement(dae.collada, 'library_controllers') - library_animations = SubElement(dae.collada, 'library_animations') - # library_cameras = SubElement(dae.collada, 'library_cameras') - library_visual_scenes = SubElement(dae.collada, 'library_visual_scenes') - # - - # - contributor = SubElement(asset, 'contributor') - SubElement(contributor, 'author').text = 'Vorono4ka' - SubElement(contributor, 'authoring_tool').text = 'models_converter (https://github.com/vorono4ka/3d-converter)' - - if 'version' in data['header']: - SubElement(contributor, 'comments').text = 'SCW Version: ' + str(data['header']['version']) - # - - # - for material_data in data['materials']: - material_name = material_data['name'] - - SubElement(library_materials, 'material', id=material_name) - effect_name = f'{material_name}-effect' - - material = SubElement(library_materials, 'material', id=material_name) - SubElement(material, 'instance_effect', url=f'#{effect_name}') - - effect = SubElement(library_effects, 'effect', id=effect_name) - profile = SubElement(effect, 'profile_COMMON') - technique = SubElement(profile, 'technique', sid='common') - - ambient_data = material_data['effect']['ambient'] - diffuse_data = material_data['effect']['diffuse'] - emission_data = material_data['effect']['emission'] - specular_data = material_data['effect']['specular'] - - phong = SubElement(technique, 'phong') - - if type(ambient_data) is list: - ambient = SubElement(phong, 'ambient') - ambient_data[3] /= 255 - ambient_data = [str(item) for item in ambient_data] - SubElement(ambient, 'color').text = ' '.join(ambient_data) - # else: - # SubElement(ambient, 'texture', texture=ambient_data, texcoord='CHANNEL0') - - if type(diffuse_data) is list: - diffuse = SubElement(phong, 'diffuse') - diffuse_data[3] /= 255 - diffuse_data = [str(item) for item in diffuse_data] - SubElement(diffuse, 'color').text = ' '.join(diffuse_data) - # else: - # SubElement(diffuse, 'texture', texture=diffuse_data, texcoord='CHANNEL0') - - if type(emission_data) is list: - emission = SubElement(phong, 'emission') - emission_data[3] /= 255 - emission_data = [str(item) for item in emission_data] - SubElement(emission, 'color').text = ' '.join(emission_data) - # else: - # SubElement(emission, 'texture', texture=emission_data, texcoord='CHANNEL0') - - if type(specular_data) is list: - specular = SubElement(phong, 'specular') - specular_data[3] /= 255 - specular_data = [str(item) for item in specular_data] - SubElement(specular, 'color').text = ' '.join(specular_data) - # else: - # SubElement(specular, 'texture', texture=specular_data, texcoord='CHANNEL0') - # - - # - for geometry_data in data['geometries']: - geometry_name = geometry_data['name'] - - geometry = SubElement(library_geometries, 'geometry', id=f'{geometry_name}-geom') - mesh = SubElement(geometry, 'mesh') - - # - for vertex_data in geometry_data['vertices']: - params = [] - - vertex_type = vertex_data['type'] - vertex_name = vertex_data['name'] - vertex = vertex_data['vertex'] - stride = len(vertex[0]) - - if vertex_type == 'VERTEX': - vertex_type = 'POSITION' - - source_name = f'{geometry_name}-{vertex_name}' - - if vertex_type in ['POSITION', 'NORMAL']: - params.append({'name': 'X', 'type': 'float'}) - params.append({'name': 'Y', 'type': 'float'}) - params.append({'name': 'Z', 'type': 'float'}) - elif vertex_type in ['TEXCOORD']: - params.append({'name': 'S', 'type': 'float'}) - params.append({'name': 'T', 'type': 'float'}) - - dae.write_source( - mesh, - source_name, - 'float_array', - [' '.join([str(sub_item * vertex_data['scale']) for sub_item in item]) for item in vertex], - stride, - params - ) - - if vertex_type == 'POSITION': - vertices = SubElement(mesh, 'vertices', id=f'{source_name}-vertices') - dae.write_input(vertices, 'POSITION', source_name) - # - - # - for material in geometry_data['materials']: - polygons_data = material['polygons'] - material_name = material['name'] - - triangles = SubElement(mesh, 'triangles', - count=f'{len(polygons_data)}', - material=material_name) - for _input in material['inputs']: - input_offset = _input['offset'] - input_name = _input['name'] - input_type = _input['type'] - - if input_type == 'POSITION': - input_type = 'VERTEX' - source_id = f'{geometry_name}-{input_name}' - if input_type == 'VERTEX': - source_id = f'{source_id}-vertices' - - dae.write_input(triangles, input_type, source_id, input_offset) - polygons = SubElement(triangles, 'p') - - formatted_polygons_data = [] - for polygon in polygons_data: - for point in polygon: - for vertex in point: - formatted_polygons_data.append(str(vertex)) - - polygons.text = ' '.join(formatted_polygons_data) - # - - # - if geometry_data['have_bind_matrix']: - joints_matrices = [] - joints_names = [] - - controller = SubElement(library_controllers, 'controller', id=f'{geometry_name}-cont') - skin = SubElement(controller, 'skin', source=f'#{geometry_name}-geom') - - if 'bind_matrix' in geometry_data: - bind_matrix_data = [str(value) for value in geometry_data['bind_matrix']] - SubElement(skin, 'bind_shape_matrix').text = ' '.join(bind_matrix_data) - - for joint in geometry_data['joints']: - joints_names.append(joint['name']) - joint_matrix = [str(value) for value in joint['matrix']] - joint_matrix = ' '.join(joint_matrix) - joints_matrices.append(joint_matrix) - - joints_names_source_id = f'{geometry_name}-joints' - joints_matrices_source_id = f'{geometry_name}-joints-bind-matrices' - weights_source_id = f'{geometry_name}-weights' - - dae.write_source( - skin, - joints_names_source_id, - 'Name_array', - joints_names, - 1, - [{'name': 'JOINT', 'type': 'name'}] - ) - - dae.write_source( - skin, - joints_matrices_source_id, - 'float_array', - joints_matrices, - 16, - [{'name': 'TRANSFORM', 'type': 'float4x4'}] - ) - - dae.write_source( - skin, - weights_source_id, - 'float_array', - [str(value) for value in geometry_data['weights']['weights']], - 1, - [{'name': 'WEIGHT', 'type': 'float'}] - ) - - joints = SubElement(skin, 'joints') - dae.write_input(joints, 'JOINT', joints_names_source_id) - dae.write_input(joints, 'INV_BIND_MATRIX', joints_matrices_source_id) - - vertex_weights_data = [str(value) for value in geometry_data['weights']['vertex_weights']] - vcount = [str(value) for value in geometry_data['weights']['vcount']] - - vertex_weights = SubElement(skin, 'vertex_weights', count=f'{len(vcount)}') - dae.write_input(vertex_weights, 'JOINT', joints_names_source_id, 0) - dae.write_input(vertex_weights, 'WEIGHT', weights_source_id, 1) - - SubElement(vertex_weights, 'vcount').text = ' '.join(vcount) - SubElement(vertex_weights, 'v').text = ' '.join(vertex_weights_data) - # - # - - # - - visual_scene = SubElement(library_visual_scenes, 'visual_scene', - id='3dConverterScene', - name='3d-Converter Scene') - - not_joint_nodes = [] - node_index = 0 - - parent_name = None - while node_index < len(not_joint_nodes) or len(not_joint_nodes) == 0: - if len(not_joint_nodes) > 0: - parent_name = not_joint_nodes[node_index] - - for _node in data['nodes']: - if _node['instances'] or _node['name'] == parent_name: - if not (_node['name'] in not_joint_nodes): - not_joint_nodes.append(_node['name']) - if not (_node['parent'] in not_joint_nodes): - not_joint_nodes.append(_node['parent']) - node_index += 1 - - for node_data in data['nodes']: - parent_name = node_data['parent'] - parent = visual_scene - if parent_name != '': - parent = visual_scene.find(f'.//*[@id="{parent_name}"]') - if parent is None: - parent = visual_scene - node_name = node_data['name'] - - node = SubElement(parent, 'node', id=node_data['name']) - - for instance in node_data['instances']: - instance_type = instance['instance_type'] - instance_name = instance['instance_name'] - bind_material = None - - if instance_type == 'CONT': - instance_controller = SubElement(node, 'instance_controller', url=f'#{instance_name}-cont') - bind_material = SubElement(instance_controller, 'bind_material') - elif instance_type == 'GEOM': - instance_controller = SubElement(node, 'instance_geometry', url=f'#{instance_name}-geom') - bind_material = SubElement(instance_controller, 'bind_material') - - if instance_type in ['GEOM', 'CONT']: - technique_common = SubElement(bind_material, 'technique_common') - for bind in instance['binds']: - symbol = bind['symbol'] - target = bind['target'] - - SubElement(technique_common, 'instance_material', - symbol=symbol, - target=f'#{target}') - else: - if not (node_data['name'] in not_joint_nodes): - node.attrib['type'] = 'JOINT' - - # - frame_rate = data['header']['frame_rate'] - time_input = [] - matrix_output = [] - # - - frames = node_data['frames'] - for frame in frames: - frame_id = frame['frame_id'] - matrix = Matrix4x4(size=(4, 4)) - - time_input.append(str(frame_id/frame_rate)) - - position_xyz = (frame['position']['x'], frame['position']['y'], frame['position']['z']) - rotation_xyz = (frame['rotation']['x'], frame['rotation']['y'], frame['rotation']['z']) - scale_xyz = (frame['scale']['x'], frame['scale']['y'], frame['scale']['z']) - - matrix.put_rotation(rotation_xyz, frame['rotation']['w']) - matrix.put_position(position_xyz) - matrix.put_scale(scale_xyz) - - matrix = matrix.translation_matrix @ matrix.rotation_matrix @ matrix.scale_matrix - matrix_values = [] - for row in matrix.matrix: - for column in row: - matrix_values.append(str(column)) - - if node_data['frames'].index(frame) == 0: - SubElement(node, 'matrix', sid='transform').text = ' '.join(matrix_values) - matrix_output.append(' '.join(matrix_values)) - - if len(frames) > 1: - animation = SubElement(library_animations, 'animation', id=node_name) - - dae.write_source( - animation, - f'{node_name}-time-input', - 'float_array', - time_input, - 1, - [{'name': 'TIME', 'type': 'float'}] - ) - dae.write_source( - animation, - f'{node_name}-matrix-output', - 'float_array', - matrix_output, - 16, - [{'name': 'TRANSFORM', 'type': 'float4x4'}] - ) - dae.write_source( - animation, - f'{node_name}-interpolation', - 'Name_array', - ['LINEAR'] * len(frames), - 1, - [{'name': 'INTERPOLATION', 'type': 'name'}] - ) - - sampler = SubElement(animation, 'sampler', id=f'{node_name}-sampler') - - dae.write_input( - sampler, - 'INPUT', - f'{node_name}-time-input' - ) - - dae.write_input( - sampler, - 'OUTPUT', - f'{node_name}-matrix-output' - ) - - dae.write_input( - sampler, - 'INTERPOLATION', - f'{node_name}-interpolation' - ) - - SubElement(animation, 'channel', - source=f'#{node_name}-sampler', - target=f'{node_name}/transform') - - scene = SubElement(dae.collada, 'scene') - SubElement(scene, 'instance_visual_scene', - url='#3dConverterScene', - name='3d-Converter Scene') - - # - - self.writen = tostring(dae.collada, xml_declaration=True).decode() - - -class Parser: - def node(self, nodes): - nodes_list = [] - for node in nodes: - instance_geometry = node.findall('collada:instance_geometry', self.namespaces) - instance_controller = node.findall('collada:instance_controller', self.namespaces) - - instances = [*instance_geometry, *instance_controller] - - children = self.node(node.findall('collada:node', self.namespaces)) - - if 'name' not in node.attrib: - node.attrib['name'] = node.attrib['id'] - - node_data = { - 'name': node.attrib['name'], - 'instances': [] - } - - for instance in instances: - instance_data = {} - binds = [] - - bind_material = instance.find('collada:bind_material', self.namespaces) - technique_common = bind_material[0] - - for instance_material in technique_common: - binds.append({ - 'symbol': instance_material.attrib['symbol'], - 'target': instance_material.attrib['target'][1:] - }) - - if instance_geometry: - instance_data['instance_type'] = 'GEOM' - - geometry_url = instance.attrib['url'] - instance_data['instance_name'] = geometry_url[1:] - elif instance_controller: - instance_data['instance_type'] = 'CONT' - - controller_url = instance.attrib['url'] - instance_data['instance_name'] = controller_url[1:] - - instance_data['binds'] = binds - node_data['instances'].append(instance_data) - - matrix = node.findall('collada:matrix', self.namespaces) - if matrix: - matrix_data = matrix[0].text.split() - matrix_data = [[float(value) for value in matrix_data[x:x + 4]] for x in range(0, len(matrix_data), 4)] - - node_data['matrix'] = matrix_data - - node_data['children'] = children - - nodes_list.append(node_data) - - return nodes_list - - def fix_nodes_list(self, nodes, parent: str = ''): - for node in nodes: - node_data = { - 'name': node['name'], - 'parent': parent, - 'instances': node['instances'] - } - - if len(node_data['instances']) == 0: - node_data['frames_settings'] = [0, 0, 0, 0, 0, 0, 0, 0] - node_data['frames'] = [] - - if 'matrix' in node: - matrix = Matrix4x4(matrix=node['matrix']) - - # scale = matrix.get_scale() - position = matrix.get_position() - - node_data['frames'] = [ - { - 'frame_id': 0, - 'rotation': {'x': 0, 'y': 0, 'z': 0, 'w': 0}, - 'position': position, - 'scale': {'x': 1, 'y': 1, 'z': 1} - } - ] - else: - node_data['frames'] = [] - - # node_data['frames'] = node['frames'] - self.parsed['nodes'].append(node_data) - self.fix_nodes_list(node['children'], node['name']) - - def __init__(self, file_data): - self.parsed = {'header': {'version': 2, - 'frame_rate': 30, - 'last_frame': 0, - 'materials_file': 'sc3d/character_materials.scw'}, - 'materials': [], - 'geometries': [], - 'cameras': [], - 'nodes': []} - - self.geometry_info = {} - - root = fromstring(file_data) - - self.namespaces = { - 'collada': 'http://www.collada.org/2005/11/COLLADASchema' - } - - # - self.library_materials = root.find('./collada:library_materials', self.namespaces) - self.library_effects = root.find('./collada:library_effects', self.namespaces) - - self.library_geometries = root.find('./collada:library_geometries', self.namespaces) - self.library_controllers = root.find('./collada:library_controllers', self.namespaces) - - self.instance_scene = root.find('./collada:scene', self.namespaces).find('collada:instance_visual_scene', - self.namespaces) - self.library_scenes = root.find('./collada:library_visual_scenes', self.namespaces) - # - - if self.library_materials is None: - self.library_materials = [] - - def parse(self): - for material in self.library_materials: - material_name = material.attrib['name'] - - instance_effect = material.find('collada:instance_effect', self.namespaces) - if instance_effect is not None: - effect_url = instance_effect.attrib['url'][1:] - effect = self.library_effects.find(f'collada:effect[@id="{effect_url}"]', self.namespaces) - - if effect is not None: - # profile = None - # for item in effect: - # if 'profile' in item.tag: - # profile = item - # technique = profile.find('collada:technique', self.namespaces) - # - # emission_data = None - # ambient_data = None - # diffuse_data = None - # - # emission = technique[0].find('collada:emission', self.namespaces) - # ambient = technique[0].find('collada:ambient', self.namespaces) - # diffuse = technique[0].find('collada:diffuse', self.namespaces) - # - # if 'color' in emission[0].tag: - # emission_data = [float(item) for item in emission[0].text.split()] - # emission_data[3] *= 255 - # elif 'texture' in emission[0].tag: - # # emission_data = emission[0].attrib['texture'] - # emission_data = '.' - # - # if 'color' in ambient[0].tag: - # ambient_data = [float(item) for item in ambient[0].text.split()] - # ambient_data[3] *= 255 - # elif 'texture' in ambient[0].tag: - # # ambient_data = ambient[0].attrib['texture'] - # ambient_data = '.' - # - # if 'color' in diffuse[0].tag: - # diffuse_data = [float(item) for item in diffuse[0].text.split()] - # diffuse_data[3] *= 255 - # elif 'texture' in diffuse[0].tag: - # # diffuse_data = diffuse[0].attrib['texture'] - # diffuse_data = '.' - - material_data = { - 'name': material_name, - 'shader': 'shader/uber.vsh', - 'effect': { - 'ambient': [0, 0, 0, 255], # ambient_data, - 'diffuse': '.', # diffuse_data, - 'specular': '.', - 'colorize': [255, 255, 255, 255], - 'emission': [0, 0, 0, 255], # emission_data, - 'lightmaps': { - 'diffuse': 'sc3d/diffuse_lightmap.png', - 'specular': 'sc3d/specular_lightmap.png' - }, - 'shader_define_flags': 3014 - } - } - - self.parsed['materials'].append(material_data) - - scene_url = self.instance_scene.attrib['url'][1:] - scene = self.library_scenes.find(f'collada:visual_scene[@id="{scene_url}"]', self.namespaces) - - nodes = self.node(scene.findall('collada:node', self.namespaces)) - self.fix_nodes_list(nodes) - self.parse_nodes() - - def parse_nodes(self): - nodes = self.parsed['nodes'] - for node_index in range(len(nodes)): - node = nodes[node_index] - for instance in node['instances']: - controller = None - geometry = None - - if instance['instance_type'] == 'CONT': - controller = self.library_controllers \ - .find(f'collada:controller[@id="{instance["instance_name"]}"]', self.namespaces) - - geometry_url = controller[0].attrib['source'][1:] - geometry = self.library_geometries \ - .find(f'collada:geometry[@id="{geometry_url}"]', self.namespaces) - elif instance['instance_type'] == 'GEOM': - geometry = self.library_geometries \ - .find(f'collada:geometry[@id="{instance["instance_name"]}"]', self.namespaces) - - if not ('name' in geometry.attrib): - geometry.attrib['name'] = geometry.attrib['id'] - - instance['instance_name'] = geometry.attrib['name'] - - for suffix in ['-skin', '-cont']: - instance['instance_name'] = instance['instance_name'].removesuffix(suffix) - for suffix in ['-mesh', '-geom']: - instance['instance_name'] = instance['instance_name'].removesuffix(suffix) - - self.parsed['nodes'][node_index] = node - - if geometry is not None: - self.geometry_info = {'name': '', - 'group': '', # node['parent'], - 'vertices': [], - 'have_bind_matrix': False, - 'materials': []} - if controller is not None: - self.parse_controller(controller) - - self.parse_geometry(geometry) - - def parse_controller(self, controller): - self.geometry_info['have_bind_matrix'] = True - - skin = controller[0] - - bind_shape_matrix = skin.find('collada:bind_shape_matrix', self.namespaces).text - bind_shape_matrix = [float(x) for x in bind_shape_matrix.split()] - - self.geometry_info['bind_matrix'] = bind_shape_matrix - - self.geometry_info['joints'] = [] - joints = skin.find('collada:joints', self.namespaces) - joint_inputs = joints.findall('collada:input', self.namespaces) - for _input in joint_inputs: - # semantic = _input.attrib['semantic'] - source_url = _input.attrib['source'] - source = skin.find(f'collada:source[@id="{source_url[1:]}"]', self.namespaces) - - accessor = source.find('collada:technique_common/collada:accessor', self.namespaces) - accessor_stride = int(accessor.attrib['stride']) - accessor_source_url = accessor.attrib['source'] - accessor_source = source.find(f'collada:*[@id="{accessor_source_url[1:]}"]', self.namespaces) - params = accessor.findall('collada:param', self.namespaces) - - for param in params: - param_name = param.attrib['name'] - # param_type = param.attrib['type'] - - if param_name == 'JOINT': - for name in accessor_source.text.split(): - self.geometry_info['joints'].append({ - 'name': name - }) - - if param_name == 'TRANSFORM': - for x in range(int(accessor_source.attrib['count']) // int(accessor_stride)): - matrix = [] - for y in accessor_source.text.split()[x * accessor_stride:(x + 1) * accessor_stride]: - matrix.append(float(y)) - self.geometry_info['joints'][x]['matrix'] = matrix - - self.geometry_info['weights'] = {} - vertex_weights = skin.find('collada:vertex_weights', self.namespaces) - vertex_weights_inputs = vertex_weights.findall('collada:input', self.namespaces) - for _input in vertex_weights_inputs: - semantic = _input.attrib['semantic'] - source_url = _input.attrib['source'] - source = skin.find(f'collada:source[@id="{source_url[1:]}"]', self.namespaces) - - if semantic == 'WEIGHT': - accessor = source.find('collada:technique_common/collada:accessor', self.namespaces) - accessor_source_url = accessor.attrib['source'] - accessor_source = source.find(f'collada:*[@id="{accessor_source_url[1:]}"]', self.namespaces) - - params = accessor.findall('collada:param', self.namespaces) - for param in params: - param_name = param.attrib['name'] - # param_type = param.attrib['type'] - - if param_name == 'WEIGHT': - weights = [float(x) for x in accessor_source.text.split()] - self.geometry_info['weights']['weights'] = weights - - vcount = vertex_weights.find('collada:vcount', self.namespaces).text - vcount = [int(x) for x in vcount.split()] - self.geometry_info['weights']['vcount'] = vcount - - v = vertex_weights.find('collada:v', self.namespaces).text - v = [int(x) for x in v.split()] - self.geometry_info['weights']['vertex_weights'] = v - - def parse_geometry(self, geometry): - name = geometry.attrib['name'] - - if name[-5:] in ['-mesh', '-geom']: - name = name[:-5] - - self.geometry_info['name'] = name - self.geometry_info['group'] = name - - mesh = geometry[0] - - triangles = mesh.findall('collada:triangles', self.namespaces) - if triangles: - pass - else: - triangles = mesh.findall('collada:polylist', self.namespaces) - inputs = triangles[0].findall('collada:input', self.namespaces) - for _input in inputs: - semantic = _input.attrib['semantic'] - source_link = _input.attrib['source'][1:] - source = mesh.find(f'*[@id="{source_link}"]') - - if semantic == 'VERTEX': - vertices_input = source[0] - semantic = vertices_input.attrib['semantic'] - source_link = vertices_input.attrib['source'][1:] - source = mesh.find(f'*[@id="{source_link}"]') - - float_array = source.find('collada:float_array', self.namespaces) - accessor = source.find('collada:technique_common/collada:accessor', self.namespaces) - - vertex_temp = [float(floating) for floating in float_array.text.split()] - - scale = max(max(vertex_temp), abs(min(vertex_temp))) - if scale < 1: - scale = 1 - if semantic == 'TEXCOORD': - vertex_temp[1::2] = [1 - x for x in vertex_temp[1::2]] - vertex_temp = [value / scale for value in vertex_temp] - - vertex = [] - for x in range(0, len(vertex_temp), len(accessor)): - vertex.append(vertex_temp[x: x + len(accessor)]) - - self.geometry_info['vertices'].append({'type': semantic, - 'index': len(self.geometry_info['vertices']), - 'scale': scale, - 'vertex': vertex}) - for triangle in triangles: - triangles_material = triangle.attrib['material'] - - p = triangle.find('collada:p', self.namespaces) - polygons_temp = [int(integer) for integer in p.text.split()] - - polygons = [ - [ - polygons_temp[polygon_index + point_index:polygon_index + point_index + 3] - for point_index in range(0, len(inputs) * 3, 3) - ] for polygon_index in range(0, len(polygons_temp), len(inputs) * 3) - ] - self.geometry_info['materials'].append({'name': triangles_material, - 'polygons': polygons}) - self.parsed['geometries'].append(self.geometry_info) diff --git a/models_converter/formats/gltf/__init__.py b/models_converter/formats/gltf/__init__.py index bad092d..009586a 100644 --- a/models_converter/formats/gltf/__init__.py +++ b/models_converter/formats/gltf/__init__.py @@ -14,6 +14,7 @@ from .skin import Skin from .texture import Texture +from .writer import Writer from .parser import Parser __all__ = [ diff --git a/models_converter/formats/gltf/parser.py b/models_converter/formats/gltf/parser.py index 807cb65..391a984 100644 --- a/models_converter/formats/gltf/parser.py +++ b/models_converter/formats/gltf/parser.py @@ -1,9 +1,12 @@ import json +from models_converter.formats import universal from models_converter.formats.gltf.chunk import GlTFChunk from models_converter.formats.gltf.gltf import GlTF from models_converter.formats.gltf.node import Node -from models_converter.utils.reader import Reader +from models_converter.formats.universal import Scene, Geometry +from models_converter.utilities.math import Vector3, Quaternion +from models_converter.utilities.reader import Reader class Parser(Reader): @@ -14,15 +17,7 @@ def __init__(self, initial_bytes: bytes): if self.magic != b'glTF': raise TypeError('File Magic isn\'t "glTF"') - self.parsed = { - 'header': { - 'frame_rate': 30 - }, - 'materials': [], - 'geometries': [], - 'cameras': [], - 'nodes': [] - } + self.scene = Scene() self.version = None self.length = None @@ -150,44 +145,33 @@ def parse(self): # - def parse_node(self, node: Node, parent: str = None): - node_name = node.name + def parse_node(self, gltf_node: Node, parent: str = None): + node_name = gltf_node.name - node_data = { - 'name': node_name, - 'parent': parent, - 'instances': [], - 'frames': [] - } + node = universal.Node( + name=node_name, + parent=parent + ) instance = None - if node.mesh: - instance = { - 'instance_type': 'GEOM', - 'instance_name': None, - 'binds': [] - } - geometry_data = { - 'name': '', - 'group': '', - 'vertices': [], - 'have_bind_matrix': False, - 'weights': { - 'vertex_weights': [], - 'weights': [], - 'vcount': [] - }, - 'materials': [] - } + if gltf_node.mesh: + mesh = self.gltf.meshes[gltf_node.mesh] # TODO: merge _geo.glb and _.*.glb files to fix TypeError + mesh_name = mesh.name.split('|') + + group = 'GEO' + name = mesh_name[0] + if len(mesh_name) > 1: + group = mesh_name[0] + name = mesh_name[1] - if node.skin: - instance['instance_type'] = 'CONT' + geometry = Geometry(name=name, group=group) - geometry_data['have_bind_matrix'] = True - geometry_data['bind_matrix'] = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] - geometry_data['joints'] = [] + if gltf_node.skin: + instance = universal.Node.Instance(name=geometry.get_name(), instance_type='CONT') - skin_id = node.skin + geometry.set_controller_bind_matrix([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]) + + skin_id = gltf_node.skin skin = self.gltf.skins[skin_id] bind_matrices = self.accessors[skin.inverse_bind_matrices] bind_matrices = [[m[0::4], m[1::4], m[2::4], m[3::4]] for m in bind_matrices] @@ -207,24 +191,9 @@ def parse_node(self, node: Node, parent: str = None): joint_name = joint_node['name'] matrix = bind_matrices[joint_index] - joint_data = { - 'name': joint_name, - 'matrix': matrix - } - geometry_data['joints'].append(joint_data) - - mesh_id = node.mesh - mesh = self.gltf.meshes[mesh_id] - mesh_name = mesh.name - mesh_name = mesh_name.split('|') - - if len(mesh_name) > 1: - geometry_data['group'] = mesh_name[0] - geometry_data['name'] = mesh_name[1] + geometry.add_joint(Geometry.Joint(joint_name, matrix)) else: - geometry_data['name'] = mesh_name[0] - - instance['instance_name'] = geometry_data['name'] + instance = universal.Node.Instance(name=geometry.get_name(), instance_type='GEOM') offsets = { 'POSITION': 0, @@ -239,95 +208,65 @@ def parse_node(self, node: Node, parent: str = None): material_id = primitive.material polygons_id = primitive.indices - inputs = [] - - polygons = self.accessors[polygons_id] + triangles = self.accessors[polygons_id] material = self.gltf.materials[material_id] material_name = material.extensions['SC_shader']['name'] - instance['binds'].append({ - 'symbol': material_name, - 'target': material_name - }) + instance.add_bind(material_name, material_name) position = [] normal = [] texcoord = [] - vertex_weights = 0 + joint_ids = 0 for attribute_id in attributes: attribute = attributes[attribute_id] - vertex = None + points = None if attribute_id == 'POSITION': position = self.accessors[attribute] - vertex = position + points = position elif attribute_id == 'NORMAL': normal = self.accessors[attribute] - vertex = normal + points = normal elif attribute_id.startswith('TEXCOORD'): texcoord = self.accessors[attribute] texcoord = [[item[0], 1 - item[1]] for item in texcoord] attribute_id = 'TEXCOORD' - vertex = texcoord + points = texcoord elif attribute_id.startswith('JOINTS'): - vertex_weights = self.accessors[attribute] + joint_ids = self.accessors[attribute] elif attribute_id.startswith('WEIGHTS'): weights = self.accessors[attribute] - for x in range(len(vertex_weights)): - geometry_data['weights']['vcount'].append(0) - - temp_list = [ - [vertex_weights[x][0], weights[x][0]], - [vertex_weights[x][1], weights[x][1]], - [vertex_weights[x][2], weights[x][2]], - [vertex_weights[x][3], weights[x][3]] - ] - for pair in temp_list: - if pair[1] != 0: - geometry_data['weights']['vcount'][x] += 1 - geometry_data['weights']['vertex_weights'].append(pair[0]) - if pair[1] not in geometry_data['weights']['weights']: - geometry_data['weights']['weights'].append(pair[1]) - geometry_data['weights']['vertex_weights'].append( - geometry_data['weights']['weights'].index(pair[1]) - ) - for weight_index in range(len(geometry_data['weights']['weights'])): - geometry_data['weights']['weights'][weight_index] /= 255 - - if vertex: - geometry_data['vertices'].append({ - 'type': attribute_id, - 'name': f'{attribute_id.lower()}_{primitive_index}', - 'index': len(geometry_data['vertices']), - 'scale': 1, - 'vertex': vertex - }) - - inputs.append({ - 'type': attribute_id, - 'offset': len(inputs), - 'name': f'{attribute_id.lower()}_{primitive_index}', - }) - - polygons = [ + for x in range(len(joint_ids)): + geometry.add_weight(Geometry.Weight(joint_ids[x][0], weights[x][0] / 255)) + geometry.add_weight(Geometry.Weight(joint_ids[x][1], weights[x][1] / 255)) + geometry.add_weight(Geometry.Weight(joint_ids[x][2], weights[x][2] / 255)) + geometry.add_weight(Geometry.Weight(joint_ids[x][3], weights[x][3] / 255)) + + if points: + geometry.add_vertex(Geometry.Vertex( + name=f'{attribute_id.lower()}_{primitive_index}', + vertex_type=attribute_id, + vertex_index=len(geometry.get_vertices()), + vertex_scale=1, + points=points + )) + + triangles = [ [ [ point[0] + offsets['NORMAL'], point[0] + offsets['POSITION'], point[0] + offsets['TEXCOORD'] - ] for point in polygons[x:x + 3] - ] for x in range(0, len(polygons), 3) + ] for point in triangles[x:x + 3] + ] for x in range(0, len(triangles), 3) ] - geometry_data['materials'].append({ - 'name': material_name, - 'inputs': inputs, - 'polygons': polygons - }) + geometry.add_material(Geometry.Material(material_name, triangles)) for attribute_id in attributes: if attribute_id == 'POSITION': @@ -337,42 +276,37 @@ def parse_node(self, node: Node, parent: str = None): elif attribute_id.startswith('TEXCOORD'): offsets['TEXCOORD'] += len(texcoord) - self.parsed['geometries'].append(geometry_data) + self.scene.add_geometry(geometry) if instance is not None: - node_data['instances'].append(instance) - - self.parsed['nodes'].append(node_data) - - if node.translation or node.rotation or node.scale: - node_data['frames'].append({ - 'frame_id': 0, - 'rotation': {'x': 0, 'y': 0, 'z': 0, 'w': 0}, - 'position': {'x': 0, 'y': 0, 'z': 0}, - 'scale': {'x': 1, 'y': 1, 'z': 1} - }) - - if node.translation: - node_data['frames'][0]['position'] = { - 'x': node.translation[0], - 'y': node.translation[1], - 'z': node.translation[2] - } - if node.rotation: - node_data['frames'][0]['rotation'] = { - 'x': node.rotation[0], - 'y': node.rotation[1], - 'z': node.rotation[2], - 'w': node.rotation[3] - } - if node.scale: - node_data['frames'][0]['scale'] = { - 'x': node.scale[0], - 'y': node.scale[1], - 'z': node.scale[2] - } - - if node.children: - for child_id in node.children: + node.add_instance(instance) + + self.scene.add_node(node) + + if gltf_node.translation or gltf_node.rotation or gltf_node.scale: + node.add_frame(universal.Node.Frame(0, Vector3(), Vector3(1, 1, 1), Quaternion())) + + if gltf_node.translation: + node.get_frames()[0].set_position(Vector3( + gltf_node.translation[0], + gltf_node.translation[1], + gltf_node.translation[2] + )) + if gltf_node.rotation: + node.get_frames()[0].set_rotation(Quaternion( + gltf_node.rotation[0], + gltf_node.rotation[1], + gltf_node.rotation[2], + gltf_node.rotation[3] + )) + if gltf_node.scale: + node.get_frames()[0].set_scale(Vector3( + gltf_node.scale[0], + gltf_node.scale[1], + gltf_node.scale[2] + )) + + if gltf_node.children: + for child_id in gltf_node.children: child = self.gltf.nodes[child_id] self.parse_node(child, node_name) diff --git a/models_converter/formats/gltf/writer.py b/models_converter/formats/gltf/writer.py new file mode 100644 index 0000000..2198ab6 --- /dev/null +++ b/models_converter/formats/gltf/writer.py @@ -0,0 +1,96 @@ +import json + +from models_converter.interfaces import WriterInterface + + +class Writer(WriterInterface): + MAGIC = b'glTF' + + def __init__(self): + self.writen = self.MAGIC + + self.data = bytes() + self.asset = {"version": "2.0"} + self.scene = 0 + self.scenes = [{ + "nodes": [] + }] + self.nodes = [] + self.buffers = [] + self.buffer_views = [] + self.accessors = [] + self.meshes = [] + self.materials = [] + self.textures = [] + self.images = [] + self.samplers = [] + + def add_root_node_index(self, index): + self.scenes[0]["nodes"].append(index) + + def add_node(self, node): + self.nodes.append(node) + + def add_mesh(self, mesh): + self.meshes.append(mesh) + + def add_material(self, material): + self.materials.append(material) + + def add_texture(self, texture): + self.textures.append(texture) + + def add_image(self, image): + self.images.append(image) + + def add_sampler(self, sampler): + self.samplers.append(sampler) + + def add_data(self, data): + offset = len(self.data) + self.data += data + return offset + + def add_buffer_view(self, buffer_view): + index = len(self.buffer_views) + self.buffer_views.append(buffer_view) + return index + + def add_accessor(self, accessor): + index = len(self.accessors) + self.accessors.append(accessor) + return index + + def as_dict(self): + return { + "asset": self.asset, + "scene": self.scene, + "scenes": self.scenes, + "nodes": self.nodes, + "buffers": self.buffers, + "bufferViews": self.buffer_views, + "accessors": self.accessors, + "meshes": self.meshes, + "materials": self.materials, + "textures": self.textures, + "images": self.images, + "samplers": self.samplers, + } + + def write(self, data: dict): + print(data) + + json_data = json.dumps(self.as_dict()) + + self.buffers.append({ + "byteLength": len(self.data) + }) + # pad json data with spaces + json_data += " " * (4 - len(json_data) % 4) + # pad binary data with null bytes + self.data += bytes((4 - len(self.data) % 4)) + + self.writen += (2).to_bytes(4, 'little') + self.writen += (len(json_data) + len(self.data) + 28).to_bytes(4, 'little') + self.writen += len(json_data).to_bytes(4, 'little') + b'JSON' + json_data.encode() + self.writen += len(self.data).to_bytes(4, 'little') + b'BIN\x00' + self.data diff --git a/models_converter/formats/obj.py b/models_converter/formats/obj.py deleted file mode 100644 index 673edd3..0000000 --- a/models_converter/formats/obj.py +++ /dev/null @@ -1,240 +0,0 @@ -def _(*args): - print('[ScwUtils]', end=' ') - for arg in args: - print(arg, end=' ') - print() - - -class Writer: - def __init__(self): - self.writen = '' - - self.temp_vertices_offsets = { - 'POSITION': 0, - 'TEXCOORD': 0, - 'NORMAL': 0 - } - - self.vertices_offsets = { - 'POSITION': 0, - 'TEXCOORD': 0, - 'NORMAL': 0 - } - - def write(self, data: dict): - for geom in data['geometries']: - for key in self.vertices_offsets.keys(): - self.vertices_offsets[key] = self.temp_vertices_offsets[key] - prefix = '' - - name = geom['name'] - vertices = geom['vertices'] - materials = geom['materials'] - for vertex in vertices: - if vertex['type'] == 'POSITION': - prefix = 'v ' - elif vertex['type'] == 'NORMAL': - prefix = 'vn ' - elif vertex['type'] == 'TEXCOORD': - prefix = 'vt ' - - self.temp_vertices_offsets[vertex['type']] += len(vertex['vertex']) - - for item in vertex['vertex']: - temp_string = prefix - for subitem in item: - temp_string += str(subitem * vertex['scale']) + ' ' - self.writen += f'{temp_string}\n' - self.writen += '\n\n' - for material in materials: - self.writen += f'o {name}|{material["name"]}\n\n' - for item in material['polygons']: - temp_string = 'f ' - for subitem in item: - temp_list = [ - str(subitem[0] + self.vertices_offsets['POSITION'] + 1), # POSITION - str(subitem[2] + self.vertices_offsets['TEXCOORD'] + 1), # TEXCOORD - str(subitem[1] + self.vertices_offsets['NORMAL'] + 1) # NORMAL - ] - - temp_string += '/'.join(temp_list) + ' ' - self.writen += f'{temp_string}\n' - self.writen += '\n\n' - - -class Parser: - def __init__(self, file_data: bytes or str): - if type(file_data) is bytes: - file_data = file_data.decode() - - self.parsed = {'header': {'version': 2, - 'frame_rate': 30, - 'first_frame': 0, - 'last_frame': 0, - 'materials_file': 'sc3d/character_materials.scw'}, - 'materials': [], - 'geometries': [], - 'cameras': [], - 'nodes': []} - - self.lines = file_data.split('\n') - - self.position_temp, self.position = [], [] - self.normals_temp, self.normals = [], [] - self.texcoord_temp, self.texcoord = [], [] - - def parse(self): - polygons = [] - geometry_name = None - material = 'character_mat' - position_scale, normals_scale, texcoord_scale = 1, 1, 1 - - vertices_offsets = { - 'POSITION': 0, - 'TEXCOORD': 0, - 'NORMAL': 0 - } - - names = [line[2:].split('|')[0] - for line in list(filter(lambda line: line.startswith('o '), self.lines))] - - for line_index in range(len(self.lines)): - line = self.lines[line_index] - items = line.split()[1:] - if line.startswith('v '): # POSITION - for item in items: - self.position_temp.append(float(item)) - elif line.startswith('vn '): # NORMAL - for item in items: - self.normals_temp.append(float(item)) - elif line.startswith('vt '): # TEXCOORD - if len(items) > 2: - items = items[:-1] - for item in items: - self.texcoord_temp.append(float(item)) - elif line.startswith('f '): - temp_list = [] - if len(items) > 3: - _('It is necessary to triangulate the model') - break - for item in items: - second_temp_list = [] - if len(item.split('/')) == 2: - _('Model have not normals or texture') - break - elif len(item.split('/')) == 1: - _('Model have not normals and texture') - break - for x in item.split('/'): - second_temp_list.append(int(x) - 1) - temp_list.append([second_temp_list[0] - vertices_offsets['POSITION'], - second_temp_list[2] - vertices_offsets['TEXCOORD'], - second_temp_list[1] - vertices_offsets['NORMAL']]) - polygons.append(temp_list) - elif line.startswith('o '): - geometry_name = items[0] - if '|' in items[0]: - geometry_name, material = items[0].split('|') - - if self.position_temp: - self.position = [] - position_scale = self.get_vertex_scale(self.position_temp) - for x in range(0, len(self.position_temp), 3): - self.position.append([vertex / position_scale for vertex in self.position_temp[x: x + 3]]) - - if self.normals_temp: - self.normals = [] - normals_scale = self.get_vertex_scale(self.normals_temp) - for x in range(0, len(self.normals_temp), 3): - self.normals.append([vertex / normals_scale for vertex in self.normals_temp[x: x + 3]]) - - if self.texcoord_temp: - self.texcoord = [] - texcoord_scale = self.get_vertex_scale(self.texcoord_temp) - for x in range(0, len(self.texcoord_temp), 2): - self.texcoord.append([vertex / texcoord_scale for vertex in self.texcoord_temp[x: x + 2]]) - - if not line.startswith('f ') and polygons and geometry_name and \ - self.position and self.normals and self.texcoord: - self.position_temp = [] - self.normals_temp = [] - self.texcoord_temp = [] - - if len(names) >= len(self.parsed['geometries']) + 1 and \ - names[len(self.parsed['geometries']) + 1] != geometry_name: - vertices_offsets['POSITION'] += len(self.position) - vertices_offsets['TEXCOORD'] += len(self.normals) - vertices_offsets['NORMAL'] += len(self.texcoord) - if not (self.parsed['geometries'] and self.parsed['geometries'][-1]['name'] == geometry_name): - self.parsed['geometries'].append({ - 'name': geometry_name, - 'group': '', - 'vertices': [ - { - 'type': 'POSITION', - 'name': 'position_0', - 'index': 0, - 'scale': position_scale, - 'vertex': self.position - }, - { - 'type': 'NORMAL', - 'name': 'normal_0', - 'index': 1, - 'scale': normals_scale, - 'vertex': self.normals - }, - { - 'type': 'TEXCOORD', - 'name': 'texcoord_0', - 'index': 2, - 'scale': texcoord_scale, - 'vertex': self.texcoord - } - ], - 'have_bind_matrix': False, - 'materials': [] - }) - self.parsed['geometries'][-1]['materials'].append( - {'name': material, 'polygons': polygons, 'inputs': [ - {'offset': self.parsed['geometries'][-1]['vertices'].index(vertex), - 'name': vertex['name'], - 'type': vertex['type']} - for vertex in self.parsed['geometries'][-1]['vertices'] - ]} - ) - - material = 'character_mat' - polygons = [] - - for geometry in self.parsed['geometries']: - self.parsed['nodes'].append({ - 'name': geometry['name'], - 'parent': '', - 'instances': [ - { - 'instance_name': geometry['name'], - 'instance_type': 'GEOM', - 'binds': [ - {'symbol': material['name'], 'target': material['name']} - for material in geometry['materials'] - ] - } - ], - 'frames': [ - { - 'frame_id': 0, - 'rotation': {'x': 0, 'y': 0, 'z': 0, 'w': 0}, - 'position': {'x': 0, 'y': 0, 'z': 0}, - 'scale': {'x': 1, 'y': 1, 'z': 1} - } - ], - 'frames_settings': [False] * 8 - }) - - @staticmethod - def get_vertex_scale(vertex_data: list): - vertex_scale = max(max(vertex_data), abs(min(vertex_data))) - if vertex_scale < 1: - vertex_scale = 1 - return vertex_scale diff --git a/models_converter/formats/scw/chunks/came.py b/models_converter/formats/scw/chunks/came.py index eb76959..6883626 100644 --- a/models_converter/formats/scw/chunks/came.py +++ b/models_converter/formats/scw/chunks/came.py @@ -1,29 +1,34 @@ from . import Chunk +from ...universal.camera import Camera class CAME(Chunk): - def __init__(self, header=None): + def __init__(self, header): super().__init__(header) self.chunk_name = 'CAME' + self.camera: Camera or None = None + def parse(self, buffer: bytes): super().parse(buffer) - setattr(self, 'name', self.readString()) - setattr(self, 'v1', self.readFloat()) - setattr(self, 'xFov', self.readFloat()) - setattr(self, 'aspectRatio', self.readFloat()) - setattr(self, 'zNear', self.readFloat()) - setattr(self, 'zFar', self.readFloat()) + name = self.readString() + self.readFloat() + fov = self.readFloat() + aspect_ratio = self.readFloat() + near = self.readFloat() + far = self.readFloat() + + self.camera = Camera(name=name, fov=fov, aspect_ratio=aspect_ratio, near=near, far=far) def encode(self): super().encode() - self.writeString(getattr(self, 'name')) - self.writeFloat(getattr(self, 'v1')) - self.writeFloat(getattr(self, 'xFov')) - self.writeFloat(getattr(self, 'aspectRatio')) - self.writeFloat(getattr(self, 'zNear')) - self.writeFloat(getattr(self, 'zFar')) + self.writeString(self.camera.get_name()) + self.writeFloat(self.camera.get_v1()) + self.writeFloat(self.camera.get_fov()) + self.writeFloat(self.camera.get_aspect_ration()) + self.writeFloat(self.camera.get_near()) + self.writeFloat(self.camera.get_far()) self.length = len(self.buffer) diff --git a/models_converter/formats/scw/chunks/chunk.py b/models_converter/formats/scw/chunks/chunk.py index 4f28584..0908c62 100644 --- a/models_converter/formats/scw/chunks/chunk.py +++ b/models_converter/formats/scw/chunks/chunk.py @@ -1,64 +1,17 @@ -from ....utils.reader import Reader -from ....utils.writer import Writer +from ....utilities.reader import Reader +from ....utilities.writer import Writer class Chunk(Writer, Reader): - def __init__(self, header=None): + def __init__(self, header): super().__init__() - if header is None: - header = {} - self.header = header self.chunk_name = '' self.buffer = b'' self.length = 0 - def from_dict(self, dictionary: dict): - if dictionary: - for key, value in dictionary.items(): - setattr(self, key, value) - - def to_dict(self) -> dict: - dictionary = {} - for key, value in self.__dict__.items(): - if key in ['header', 'buffer', 'length', 'endian', 'i']: - continue - if value is not None: - attribute_name = key - value_type = type(value) - - attribute_value = None - - if value_type is list: - attribute_value = [] - for item in value: - item_type = type(item) - - if issubclass(item_type, Chunk): - item = item.to_dict() - attribute_value.append(item) - elif issubclass(value_type, Chunk): - attribute_value = value.to_dict() - elif attribute_value is None: - attribute_value = value - - dictionary[attribute_name] = attribute_value - return dictionary - - def __getitem__(self, key): - if hasattr(self, key): - return getattr(self, key) - else: - raise IndexError('The object has no attribute named ' + key) - - def __repr__(self) -> str: - return f'<{self.__class__.__name__} ({self.to_dict()})>' - - def set(self, key: str, value): - setattr(self, key, value) - def parse(self, buffer: bytes): Reader.__init__(self, buffer, 'big') @@ -66,5 +19,3 @@ def encode(self): Writer.__init__(self, 'big') self.length = len(self.buffer) - - get = __getitem__ diff --git a/models_converter/formats/scw/chunks/geom.py b/models_converter/formats/scw/chunks/geom.py index ffa2c5d..b1600e0 100644 --- a/models_converter/formats/scw/chunks/geom.py +++ b/models_converter/formats/scw/chunks/geom.py @@ -1,17 +1,23 @@ from . import Chunk +from ...universal import Geometry class GEOM(Chunk): - def __init__(self, header: dict): + def __init__(self, header): super().__init__(header) self.chunk_name = 'GEOM' + self.geometry: Geometry or None = None + def parse(self, buffer: bytes): super().parse(buffer) - setattr(self, 'name', self.readString()) - setattr(self, 'group', self.readString()) - if self.header['version'] < 2: + self.geometry = Geometry( + name=self.readString(), + group=self.readString() + ) + + if self.header.version < 2: matrix = [] for x in range(4): temp_list = [] @@ -19,17 +25,13 @@ def parse(self, buffer: bytes): temp_list.append(self.readFloat()) matrix.append(temp_list) - self.parse_vertices() - self.parse_skin() - self.parse_materials() - - def parse_vertices(self): - vertices = [] - inputs = [] + self._parse_vertices() + self._parse_skin() + self._parse_materials() + def _parse_vertices(self): vertex_count = self.readUByte() for x in range(vertex_count): - vertex = [] vertex_type = self.readString() vertex_index = self.readUByte() self.readUByte() # sub_index @@ -40,229 +42,166 @@ def parse_vertices(self): if vertex_type == 'VERTEX': vertex_type = 'POSITION' + coordinates = [] for x1 in range(vertex_count): - coordinates_massive = [] - for x2 in range(vertex_stride): - coordinate = self.readShort() - coordinates_massive.append(coordinate / 32512) + coordinates_massive = [self.readNShort() for _ in range(vertex_stride)] + if vertex_type == 'TEXCOORD': coordinates_massive[1::2] = [1 - x for x in coordinates_massive[1::2]] - vertex.append(coordinates_massive) - - inputs.append({ - 'type': vertex_type, - 'offset': vertex_index, - 'name': f'{vertex_type.lower()}_0' - }) + coordinates.append(coordinates_massive) - vertices.append({ - 'name': f'{vertex_type.lower()}_0', - 'type': vertex_type, - 'index': vertex_index, - 'scale': vertex_scale, - 'vertex': vertex - }) - setattr(self, 'inputs', inputs) - setattr(self, 'vertices', vertices) + self.geometry.add_vertex(Geometry.Vertex( + name=f'{vertex_type.lower()}_0', + vertex_type=vertex_type, + vertex_index=vertex_index, + vertex_scale=vertex_scale, + points=coordinates) + ) - def parse_skin(self): - bind_matrix = [] + def _parse_skin(self): + has_controller = self.readBool() + if has_controller: + self.geometry.set_controller_bind_matrix([self.readFloat() for _ in range(16)]) - setattr(self, 'have_bind_matrix', self.readBool()) - if getattr(self, 'have_bind_matrix'): - for x in range(16): - bind_matrix.append(self.readFloat()) - - setattr(self, 'bind_matrix', bind_matrix) - - self.parse_joints() - self.parse_weights() - - def parse_joints(self): - joints = [] + self._parse_joints() + self._parse_weights() + def _parse_joints(self): joint_counts = self.readUByte() for x in range(joint_counts): - joint_matrix = [] joint_name = self.readString() - for x1 in range(16): - joint_matrix.append(self.readFloat()) - joints.append({'name': joint_name, 'matrix': joint_matrix}) + joint_matrix = [self.readFloat() for _ in range(16)] - setattr(self, 'joints', joints) - - def parse_weights(self): - vertex_weights = [] - weights = [] - vcount = [] + self.geometry.add_joint(Geometry.Joint(joint_name, joint_matrix)) + def _parse_weights(self): vertex_weights_count = self.readUInt32() for x in range(vertex_weights_count): - vcount.append(0) joint_a = self.readUByte() joint_b = self.readUByte() joint_c = self.readUByte() joint_d = self.readUByte() - weight_a = self.readUShort() - weight_b = self.readUShort() - weight_c = self.readUShort() - weight_d = self.readUShort() - temp_list = [ - [joint_a, weight_a], - [joint_b, weight_b], - [joint_c, weight_c], - [joint_d, weight_d] - ] - for pair in temp_list: - if pair[1] != 0: - vcount[x] += 1 - vertex_weights.append(pair[0]) - if pair[1] / 65535 not in weights: - weights.append(pair[1] / 65535) - vertex_weights.append(weights.index(pair[1] / 65535)) - - setattr(self, 'weights', - { - 'vertex_weights': vertex_weights, - 'weights': weights, - 'vcount': vcount - }) - - def parse_materials(self): - materials = [] + weight_a = self.readNUShort() + weight_b = self.readNUShort() + weight_c = self.readNUShort() + weight_d = self.readNUShort() + + self.geometry.add_weight(Geometry.Weight(joint_a, weight_a)) + self.geometry.add_weight(Geometry.Weight(joint_b, weight_b)) + self.geometry.add_weight(Geometry.Weight(joint_c, weight_c)) + self.geometry.add_weight(Geometry.Weight(joint_d, weight_d)) + def _parse_materials(self): materials_count = self.readUByte() for x in range(materials_count): - polygons = [] material_name = self.readString() self.readString() - polygons_count = self.readUShort() + triangles_count = self.readUShort() inputs_count = self.readUByte() - vertex_id_length = self.readUByte() - for x1 in range(polygons_count): - temp_list = [] - for x2 in range(3): - second_temp_list = [] - for x3 in range(inputs_count): - second_temp_list.append(self.readUInteger(vertex_id_length)) - temp_list.append(second_temp_list) - polygons.append(temp_list) - materials.append({ - 'name': material_name, - 'inputs': getattr(self, 'inputs'), - 'polygons': polygons - }) - - setattr(self, 'materials', materials) + vertex_index_length = self.readUByte() + + triangles = [] + for x1 in range(triangles_count): + triangles.append([ + [ + self.readUInteger(vertex_index_length) # Vertex + for _ in range(inputs_count) + ] for _ in range(3) # 3 points + ]) + + self.geometry.add_material(Geometry.Material(material_name, triangles)) def encode(self): super().encode() - self.writeString(self.get('name')) - self.writeString(self.get('group')) + self.writeString(self.geometry.get_name()) + self.writeString(self.geometry.get_group()) - self.encode_vertices(self.get('vertices')) + self._encode_vertices(self.geometry.get_vertices()) - self.encode_skin() + self._encode_skin() - self.encode_materials() + self._encode_materials() self.length = len(self.buffer) - def encode_vertices(self, vertices: dict): + def _encode_vertices(self, vertices): self.writeUByte(len(vertices)) for vertex in vertices: - self.writeString(vertex['type']) - self.writeUByte(vertex['index']) + self.writeString(vertex.get_type()) + self.writeUByte(vertex.get_index()) self.writeUByte(0) # sub_index - self.writeUByte(len(vertex['vertex'][0])) - self.writeFloat(vertex['scale']) - self.writeUInt32(len(vertex['vertex'])) - for coordinates_massive in vertex['vertex']: - if vertex['type'] == 'TEXCOORD': - coordinates_massive[1::2] = [1 - x for x in coordinates_massive[1::2]] - for coordinate in coordinates_massive: + self.writeUByte(vertex.get_point_size()) + self.writeFloat(vertex.get_scale()) + self.writeUInt32(len(vertex.get_points())) + for point in vertex.get_points(): + if vertex.get_type() == 'TEXCOORD': + point[1::2] = [1 - x for x in point[1::2]] + for coordinate in point: # coordinate /= vertex['scale'] coordinate *= 32512 self.writeShort(round(coordinate)) - def encode_skin(self): - self.writeBool(self.get('have_bind_matrix')) - if self.get('have_bind_matrix'): - for x in self.get('bind_matrix'): + def _encode_skin(self): + self.writeBool(self.geometry.has_controller()) + if self.geometry.has_controller(): + for x in self.geometry.get_bind_matrix(): self.writeFloat(x) - self.encode_joints() + self._encode_joints() + self._encode_weight() - self.encode_weight() + def _encode_joints(self): + if not self.geometry.has_controller(): + self.writeUByte(0) + return - def encode_joints(self): - if self.get('have_bind_matrix'): - self.writeUByte(len(self.get('joints'))) + self.writeUByte(len(self.geometry.get_joints())) - for joint in self.get('joints'): - self.writeString(joint['name']) - for x in joint['matrix']: - self.writeFloat(x) - else: - self.writeUByte(0) + for joint in self.geometry.get_joints(): + self.writeString(joint.get_name()) + for x in joint.get_matrix(): + self.writeFloat(x) - def encode_weight(self): - if self.get('have_bind_matrix'): - self.writeUInt32(len(self.get('weights')['vcount'])) - past_index = 0 - for vcount in self.get('weights')['vcount']: - temp_list = [] - for x in range(vcount): - vertex_weights_index = x * 2 + past_index * 2 - joint_id = self.get('weights')['vertex_weights'][vertex_weights_index] - weight_id = self.get('weights')['vertex_weights'][vertex_weights_index + 1] - - weight = self.get('weights')['weights'][weight_id] - - if weight > 1: - weight = 1 - elif weight < 0: - weight = 0 - - weight = int(weight * 65535) - - temp_list.append([joint_id, weight]) - past_index += vcount - while len(temp_list) < 4: - temp_list.append([0, 0]) - for x in temp_list: - self.writeUByte(x[0]) - for x in temp_list: - self.writeUShort(x[1]) - else: + def _encode_weight(self): + if not self.geometry.has_controller(): self.writeUInt32(0) - - def encode_materials(self): - self.writeUByte(len(self.get('materials'))) - for material in self.get('materials'): - self.writeString(material['name']) + return + + weights_quads = len(self.geometry.get_weights()) // 4 + self.writeUInt32(weights_quads) + for quad_index in range(weights_quads): + quad = self.geometry.get_weights()[quad_index * 4:(quad_index + 1) * 4] + for weight in quad: + self.writeUByte(weight.get_joint_index()) + for weight in quad: + self.writeNUShort(weight.get_strength()) + + def _encode_materials(self): + self.writeUByte(len(self.geometry.get_materials())) + for material in self.geometry.get_materials(): + self.writeString(material.get_name()) self.writeString('') - self.writeUShort(len(material['polygons'])) + self.writeUShort(len(material.get_triangles())) # Calculate settings - inputs_count = len(material['polygons'][0][0]) + inputs_count = len(material.get_triangles()[0][0]) maximal_value = 0 - for points in material['polygons']: - for point in points: + for triangle in material.get_triangles(): + for point in triangle: for vertex in point: if vertex > maximal_value: maximal_value = vertex - short_length = 1 if maximal_value <= 255 else 2 + item_length = 1 if maximal_value <= 255 else 2 # Write Settings self.writeUByte(inputs_count) - self.writeUByte(short_length) + self.writeUByte(item_length) # Write Polygons - for points in material['polygons']: - for point in points: + for triangle in material.get_triangles(): + for point in triangle: for vertex in point: - self.writeUInteger(vertex, short_length) + self.writeUInteger(vertex, item_length) diff --git a/models_converter/formats/scw/chunks/head.py b/models_converter/formats/scw/chunks/head.py index a0a4371..9b29c26 100644 --- a/models_converter/formats/scw/chunks/head.py +++ b/models_converter/formats/scw/chunks/head.py @@ -6,28 +6,35 @@ def __init__(self, header=None): super().__init__(header) self.chunk_name = 'HEAD' + self.version: int = 2 + self.frame_rate: int = 30 + self.first_frame: int = 0 + self.last_frame: int = 0 + self.materials_file: str or None = None + self.v3: int = 0 + def parse(self, buffer: bytes): super().parse(buffer) - setattr(self, 'version', self.readUShort()) - setattr(self, 'frame_rate', self.readUShort()) - setattr(self, 'first_frame', self.readUShort()) - setattr(self, 'last_frame', self.readUShort()) - setattr(self, 'materials_file', self.readString()) - if self.get('version') >= 1: - setattr(self, 'v3', self.readUByte()) + self.version = self.readUShort() + self.frame_rate = self.readUShort() + self.first_frame = self.readUShort() + self.last_frame = self.readUShort() + self.materials_file = self.readString() + if self.version >= 1: + self.v3 = self.readUByte() def encode(self): super().encode() - setattr(self, 'version', 2) + self.version = 2 - self.writeUShort(getattr(self, 'version')) - self.writeUShort(getattr(self, 'frame_rate')) - self.writeUShort(getattr(self, 'first_frame')) - self.writeUShort(getattr(self, 'last_frame')) - self.writeString(getattr(self, 'materials_file')) - if self.get('version') >= 1: - self.writeUByte(0) # getattr(self, 'v3') + self.writeUShort(self.version) + self.writeUShort(self.frame_rate) + self.writeUShort(self.first_frame) + self.writeUShort(self.last_frame) + self.writeString(self.materials_file) + if self.version >= 1: + self.writeUByte(0) self.length = len(self.buffer) diff --git a/models_converter/formats/scw/chunks/mate.py b/models_converter/formats/scw/chunks/mate.py index 6dca312..ce7fd82 100644 --- a/models_converter/formats/scw/chunks/mate.py +++ b/models_converter/formats/scw/chunks/mate.py @@ -2,7 +2,7 @@ class MATE(Chunk): - def __init__(self, header: dict): + def __init__(self, header): super().__init__(header) self.chunk_name = 'MATE' @@ -82,7 +82,7 @@ def parse(self, buffer: bytes): 'specular': self.readString() } - if self.header['version'] == 2: + if self.header.version == 2: setattr(self, 'v7', self.readString()) shader_define_flags = self.readUInt32() diff --git a/models_converter/formats/scw/chunks/node.py b/models_converter/formats/scw/chunks/node.py index b76ae33..ec78fd6 100644 --- a/models_converter/formats/scw/chunks/node.py +++ b/models_converter/formats/scw/chunks/node.py @@ -1,145 +1,125 @@ +from models_converter.utilities.math import Vector3 +from models_converter.utilities.math import Quaternion from . import Chunk +from ...universal.node import Node class NODE(Chunk): - def __init__(self, header: dict): + def __init__(self, header): super().__init__(header) self.chunk_name = 'NODE' + self.nodes = [] + def parse(self, buffer: bytes): super().parse(buffer) - nodes = [] nodes_count = self.readUShort() - for node in range(nodes_count): - node_data = { - 'name': self.readString(), - 'parent': self.readString() - } + for node_index in range(nodes_count): + node = Node( + name=self.readString(), + parent=self.readString() + ) instances_count = self.readUShort() - node_data['instances'] = [{}] * instances_count for x in range(instances_count): - instance_type = self.readChar(4) - instance_name = self.readString() + instance = Node.Instance( + instance_type=self.readChars(4), + name=self.readString() + ) - node_data['instances'][x] = {} - if instance_type in ['GEOM', 'CONT']: + if instance.get_type() in ('GEOM', 'CONT'): materials_count = self.readUShort() - binds = [] for bind in range(materials_count): - binds.append({}) symbol = self.readString() target = self.readString() - binds[bind] = {'symbol': symbol, - 'target': target} - node_data['instances'][x]['binds'] = binds - elif instance_type in ['CAME']: - target = self.readString() - node_data['instances'][x]['target'] = target - node_data['instances'][x]['instance_name'] = instance_name - node_data['instances'][x]['instance_type'] = instance_type + instance.add_bind(symbol, target) + elif instance.get_type() == 'CAME': + instance.set_target(self.readString()) + node.add_instance(instance) frames_count = self.readUShort() - node_data['frames'] = [] if frames_count > 0: - rotation = {'x': 0, 'y': 0, 'z': 0, 'w': 0} - scale_x, scale_y, scale_z = 0, 0, 0 - pos_x, pos_y, pos_z = 0, 0, 0 - - settings = list(bin(self.readUByte())[2:].zfill(8)) - settings = [bool(int(value)) for value in settings] - node_data['frames_settings'] = settings - for frame in range(frames_count): - frame_data = { - 'frame_id': self.readUShort() - } - - if settings[7] or frame == 0: # Rotation - rotation = { - 'x': self.readNShort(), - 'y': self.readNShort(), - 'z': self.readNShort(), - 'w': self.readNShort() - } - - if settings[4] or frame == 0: # Position X - pos_x = self.readFloat() - if settings[5] or frame == 0: # Position Y - pos_y = self.readFloat() - if settings[6] or frame == 0: # Position Z - pos_z = self.readFloat() - - if settings[1] or frame == 0: # Scale X - scale_x = self.readFloat() - if settings[2] or frame == 0: # Scale Y - scale_y = self.readFloat() - if settings[3] or frame == 0: # Scale Z - scale_z = self.readFloat() - - frame_data['rotation'] = rotation - frame_data['position'] = { - 'x': pos_x, - 'y': pos_y, - 'z': pos_z - } - frame_data['scale'] = { - 'x': scale_x, - 'y': scale_y, - 'z': scale_z - } - - node_data['frames'].append(frame_data) - nodes.append(node_data) - setattr(self, 'nodes', nodes) + rotation = Quaternion() + position = Vector3() + scale = Vector3() + + node.frames_settings = self.readUByte() + for frame_index in range(frames_count): + frame = Node.Frame(self.readUShort()) + + if node.frames_settings & 1 or frame_index == 0: # Rotation + rotation.x = self.readNShort() + rotation.y = self.readNShort() + rotation.z = self.readNShort() + rotation.w = self.readNShort() + + if node.frames_settings & 2 or frame_index == 0: # Position X + position.x = self.readFloat() + if node.frames_settings & 4 or frame_index == 0: # Position Y + position.y = self.readFloat() + if node.frames_settings & 8 or frame_index == 0: # Position Z + position.z = self.readFloat() + + if node.frames_settings & 16 or frame_index == 0: # Scale X + scale.x = self.readFloat() + if node.frames_settings & 32 or frame_index == 0: # Scale Y + scale.y = self.readFloat() + if node.frames_settings & 64 or frame_index == 0: # Scale Z + scale.z = self.readFloat() + + frame.set_rotation(rotation.clone()) + frame.set_position(position.clone()) + frame.set_scale(scale.clone()) + + node.add_frame(frame) + self.nodes.append(node) def encode(self): super().encode() - self.writeUShort(len(self.get('nodes'))) - for node in self.get('nodes'): - self.writeString(node['name']) - self.writeString(node['parent']) - - self.writeUShort(len(node['instances'])) - for instance in node['instances']: - self.writeChar(instance['instance_type']) - self.writeString(instance['instance_name']) - self.writeUShort(len(instance['binds'])) - for bind in instance['binds']: - self.writeString(bind['symbol']) - self.writeString(bind['target']) - - if 'frames_settings' in node: - frames_settings = node['frames_settings'] - else: - frames_settings = None - self.encode_frames(node['frames'], frames_settings) + self.writeUShort(len(self.nodes)) + for node in self.nodes: + self.writeString(node.get_name()) + self.writeString(node.get_parent()) + + self.writeUShort(len(node.get_instances())) + for instance in node.get_instances(): + self.writeChar(instance.get_type()) + self.writeString(instance.get_name()) + self.writeUShort(len(instance.get_binds())) + for bind in instance.get_binds(): + self.writeString(bind.get_symbol()) + self.writeString(bind.get_target()) + + self._encode_frames(node.get_frames(), node.frames_settings) self.length = len(self.buffer) - def encode_frames(self, frames, frames_settings): + def _encode_frames(self, frames, frames_settings): self.writeUShort(len(frames)) if len(frames) > 0: - self.writeUByte(int(''.join([('1' if item else '0') for item in frames_settings])[::], 2)) + self.writeUByte(frames_settings) for frame in frames: - self.writeUShort(frame['frame_id']) - if frames_settings[7] or frames.index(frame) == 0: # Rotation - self.writeNShort(frame['rotation']['x']) - self.writeNShort(frame['rotation']['y']) - self.writeNShort(frame['rotation']['z']) - self.writeNShort(frame['rotation']['w']) - - if frames_settings[4] or frames.index(frame) == 0: # Position X - self.writeFloat(frame['position']['x']) - if frames_settings[5] or frames.index(frame) == 0: # Position Y - self.writeFloat(frame['position']['y']) - if frames_settings[6] or frames.index(frame) == 0: # Position Z - self.writeFloat(frame['position']['z']) - - if frames_settings[1] or frames.index(frame) == 0: # Scale X - self.writeFloat(frame['scale']['x']) - if frames_settings[2] or frames.index(frame) == 0: # Scale Y - self.writeFloat(frame['scale']['y']) - if frames_settings[3] or frames.index(frame) == 0: # Scale Z - self.writeFloat(frame['scale']['z']) + self.writeUShort(frame.get_id()) + if frames_settings & 128 or frames.index(frame) == 0: # Rotation + rotation = frame.get_rotation() + + self.writeNShort(rotation.x) + self.writeNShort(rotation.y) + self.writeNShort(rotation.z) + self.writeNShort(rotation.w) + + if frames_settings & 16 or frames.index(frame) == 0: # Position X + self.writeFloat(frame.get_position().x) + if frames_settings & 32 or frames.index(frame) == 0: # Position Y + self.writeFloat(frame.get_position().y) + if frames_settings & 64 or frames.index(frame) == 0: # Position Z + self.writeFloat(frame.get_position().z) + + if frames_settings & 2 or frames.index(frame) == 0: # Scale X + self.writeFloat(frame.get_scale().x) + if frames_settings & 4 or frames.index(frame) == 0: # Scale Y + self.writeFloat(frame.get_scale().y) + if frames_settings & 8 or frames.index(frame) == 0: # Scale Z + self.writeFloat(frame.get_scale().z) diff --git a/models_converter/formats/scw/chunks/wend.py b/models_converter/formats/scw/chunks/wend.py index 0252602..28a840c 100644 --- a/models_converter/formats/scw/chunks/wend.py +++ b/models_converter/formats/scw/chunks/wend.py @@ -5,11 +5,3 @@ class WEND(Chunk): def __init__(self, header=None): super().__init__(header) self.chunk_name = 'WEND' - - def parse(self, buffer: bytes): - super().parse(buffer) - - def encode(self): - super().encode() - - self.length = len(self.buffer) diff --git a/models_converter/formats/scw/parser.py b/models_converter/formats/scw/parser.py index df8649f..1998e7e 100644 --- a/models_converter/formats/scw/parser.py +++ b/models_converter/formats/scw/parser.py @@ -1,39 +1,26 @@ -from ...utils.reader import Reader +from ..universal import Scene +from ...interfaces import ParserInterface +from ...utilities.reader import Reader from .chunks import * -class Parser(Reader): +class Parser(ParserInterface, Reader): def __init__(self, file_data: bytes): - super().__init__(file_data) + Reader.__init__(self, file_data) + self.file_data = file_data - self.parsed = { - 'header': {}, - 'materials': [], - 'geometries': [], - 'cameras': [], - 'nodes': [] - } + self.scene = Scene() self.chunks = [] + self.header = None + file_magic = self.read(4) if file_magic != b'SC3D': raise TypeError('File Magic isn\'t "SC3D"') - def split_chunks(self): - # len(Chunk Length) + len(Chunk Name) + len(Chunk CRC) - while len(self.file_data[self.tell():]) >= 12: - chunk_length = self.readUInt32() - chunk_name = self.readChar(4) - chunk_data = self.read(chunk_length) - chunk_crc = self.readUInt32() - - self.chunks.append({ - 'chunk_name': chunk_name, - 'data': chunk_data, - 'crc': chunk_crc - }) - def parse(self): + self._split_chunks() + for chunk in self.chunks: chunk_name = chunk['chunk_name'] chunk_data = chunk['data'] @@ -42,25 +29,39 @@ def parse(self): head = HEAD() head.parse(chunk_data) - self.parsed['header'] = head.to_dict() + self.header = head elif chunk_name == 'MATE': - mate = MATE(self.parsed['header']) + mate = MATE(self.header) mate.parse(chunk_data) - self.parsed['materials'].append(mate.to_dict()) + self.scene.add_material(mate) elif chunk_name == 'GEOM': - geom = GEOM(self.parsed['header']) + geom = GEOM(self.header) geom.parse(chunk_data) - self.parsed['geometries'].append(geom.to_dict()) + self.scene.add_geometry(geom.geometry) elif chunk_name == 'CAME': - came = CAME(self.parsed['header']) + came = CAME(self.header) came.parse(chunk_data) - self.parsed['cameras'].append(came.to_dict()) + self.scene.add_camera(came.camera) elif chunk_name == 'NODE': - node = NODE(self.parsed['header']) + node = NODE(self.header) node.parse(chunk_data) - self.parsed['nodes'] = node.to_dict()['nodes'] + self.scene.get_nodes().extend(node.nodes) elif chunk_name == 'WEND': wend = WEND() wend.parse(chunk_data) else: raise TypeError(f'Unknown chunk: {chunk_name}') + + def _split_chunks(self): + # len(Chunk Length) + len(Chunk Name) + len(Chunk CRC) + while len(self.file_data[self.tell():]) >= 12: + chunk_length = self.readUInt32() + chunk_name = self.readChars(4) + chunk_data = self.read(chunk_length) + chunk_crc = self.readUInt32() + + self.chunks.append({ + 'chunk_name': chunk_name, + 'data': chunk_data, + 'crc': chunk_crc + }) diff --git a/models_converter/formats/scw/writer.py b/models_converter/formats/scw/writer.py index b55efb2..3bb459c 100644 --- a/models_converter/formats/scw/writer.py +++ b/models_converter/formats/scw/writer.py @@ -1,50 +1,54 @@ import binascii from .chunks import * +from ..universal import Scene +from ...interfaces import WriterInterface -class Writer: - def __init__(self): - self.writen = b'SC3D' +class Writer(WriterInterface): + MAGIC = b'SC3D' - def write(self, data: dict): + def __init__(self): + self.writen = self.MAGIC - header = data['header'] + def write(self, scene: Scene): head = HEAD() - head.from_dict(header) + head.version = 2 + head.frame_rate = 30 + head.first_frame = 0 + head.last_frame = 0 + head.materials_file = 'sc3d/character_materials.scw' if len(scene.get_geometries()) > 0 else None - self.write_chunk(head) + self._write_chunk(head) # TODO: materials - for material in data['materials']: - mate = MATE(header) - mate.from_dict(material) + for material in scene.get_materials(): + mate = MATE(head) - self.write_chunk(mate) + self._write_chunk(mate) - for geometry in data['geometries']: - geom = GEOM(header) - geom.from_dict(geometry) + for geometry in scene.get_geometries(): + geom = GEOM(head) + geom.geometry = geometry - self.write_chunk(geom) + self._write_chunk(geom) - # TODO: cameras - for camera in data['cameras']: - came = CAME(header) - came.from_dict(camera) + for camera in scene.get_cameras(): + came = CAME(head) + came.camera = camera - self.write_chunk(came) + self._write_chunk(came) - node = NODE(header) - node.from_dict({'nodes': data['nodes']}) + node = NODE(head) + node.nodes = scene.get_nodes() - self.write_chunk(node) + self._write_chunk(node) wend = WEND() - self.write_chunk(wend) + self._write_chunk(wend) - def write_chunk(self, chunk: Chunk): + def _write_chunk(self, chunk: Chunk): chunk.encode() self.writen += chunk.length.to_bytes(4, 'big') + chunk.chunk_name.encode('utf-8') + chunk.buffer diff --git a/models_converter/formats/universal/__init__.py b/models_converter/formats/universal/__init__.py new file mode 100644 index 0000000..dd30098 --- /dev/null +++ b/models_converter/formats/universal/__init__.py @@ -0,0 +1,8 @@ +from models_converter.formats.universal.scene import Scene, Node, Camera, Geometry + +__all__ = [ + 'Scene', + 'Node', + 'Camera', + 'Geometry' +] diff --git a/models_converter/formats/universal/camera.py b/models_converter/formats/universal/camera.py new file mode 100644 index 0000000..c4e275c --- /dev/null +++ b/models_converter/formats/universal/camera.py @@ -0,0 +1,26 @@ +class Camera: + def __init__(self, *, name: str, fov: float, aspect_ratio: float, near: float, far: float): + self._name: str = name + self._v1: float = 0 + self._fov: float = fov + self._aspect_ratio: float = aspect_ratio + self._near: float = near + self._far: float = far + + def get_name(self) -> str: + return self._name + + def get_v1(self) -> float: + return self._v1 + + def get_fov(self) -> float: + return self._fov + + def get_aspect_ration(self) -> float: + return self._aspect_ratio + + def get_near(self) -> float: + return self._near + + def get_far(self) -> float: + return self._far diff --git a/models_converter/formats/universal/geometry.py b/models_converter/formats/universal/geometry.py new file mode 100644 index 0000000..e56727f --- /dev/null +++ b/models_converter/formats/universal/geometry.py @@ -0,0 +1,118 @@ +from typing import List + + +class Geometry: + class Vertex: + def __init__(self, *, + name: str, + vertex_type: str, + vertex_index: int, + vertex_scale: float, + points: List[List[float]]): + self._name: str = name + self._type: str = vertex_type + self._index: int = vertex_index + self._scale: float = vertex_scale + self._points: List[List[float]] = points + + def get_name(self) -> str: + return self._name + + def get_type(self) -> str: + return self._type + + def get_index(self) -> int: + return self._index + + def get_point_size(self) -> float: + return len(self._points[0]) + + def get_scale(self) -> float: + return self._scale + + def get_points(self) -> List[List[float]]: + return self._points + + class Material: + def __init__(self, name: str, triangles: List[List[List[int]]]): + self._name: str = name + self._triangles: List[List[List[int]]] = triangles + + def get_name(self) -> str: + return self._name + + def get_triangles(self) -> List[List[List[int]]]: + return self._triangles + + class Joint: + def __init__(self, name: str, matrix: List[float] or None): + self._name: str = name + self._matrix: List[float] or None = matrix + + def get_name(self) -> str: + return self._name + + def get_matrix(self) -> List[float]: + return self._matrix + + def set_matrix(self, matrix: List[float]): + self._matrix = matrix + + class Weight: + def __init__(self, joint_index: int, strength: float): + self._joint_index: int = joint_index + self._strength: float = strength + + def get_joint_index(self) -> int: + return self._joint_index + + def get_strength(self) -> float: + return self._strength + + def __init__(self, *, name: str, group: str = None): + self._name: str = name + self._group: str or None = group + self._vertices: List[Geometry.Vertex] = [] + self._materials: List[Geometry.Material] = [] + self._bind_matrix: List[float] or None = None + self._joints: List[Geometry.Joint] = [] + self._weights: List[Geometry.Weight] = [] + + def get_name(self) -> str: + return self._name + + def get_group(self) -> str or None: + return self._group + + def get_vertices(self) -> List[Vertex]: + return self._vertices + + def add_vertex(self, vertex: Vertex): + self._vertices.append(vertex) + + def get_materials(self) -> List[Material]: + return self._materials + + def add_material(self, material: Material): + self._materials.append(material) + + def has_controller(self) -> bool: + return self._bind_matrix is not None + + def get_bind_matrix(self) -> list: + return self._bind_matrix + + def set_controller_bind_matrix(self, matrix: List[float]): + self._bind_matrix = matrix + + def get_joints(self) -> List[Joint]: + return self._joints + + def add_joint(self, joint: Joint): + self._joints.append(joint) + + def get_weights(self) -> List[Weight]: + return self._weights + + def add_weight(self, weight: Weight): + self._weights.append(weight) diff --git a/models_converter/formats/universal/node.py b/models_converter/formats/universal/node.py new file mode 100644 index 0000000..c5bfc6a --- /dev/null +++ b/models_converter/formats/universal/node.py @@ -0,0 +1,106 @@ +from typing import List + +from models_converter.utilities.math import Quaternion +from models_converter.utilities.math import Vector3 + + +class Node: + class Instance: + class Bind: + def __init__(self, symbol: str = None, target: str = None): + self._symbol = symbol + self._target = target + + def get_symbol(self) -> str or None: + return self._symbol + + def get_target(self) -> str or None: + return self._target + + def __init__(self, *, name: str, instance_type: str): + self._name: str = name + self._type: str = instance_type + self._target: str or None = None + self._binds = [] + + def __repr__(self) -> str: + return f'{self._name} - {self._type}' + + def get_name(self) -> str: + return self._name + + def get_type(self) -> str: + return self._type + + def get_target(self) -> str: + return self._target + + def set_target(self, target: str): + self._target = target + + def get_binds(self) -> list: + return self._binds + + def add_bind(self, symbol: str, target: str): + self._binds.append(Node.Instance.Bind(symbol, target)) + + class Frame: + def __init__(self, frame_id: int, position: Vector3 = None, scale: Vector3 = None, rotation: Quaternion = None): + self._id: int = frame_id + self._position: Vector3 = position + self._scale: Vector3 = scale + self._rotation: Quaternion = rotation + + def get_id(self) -> int: + return self._id + + def get_rotation(self) -> Quaternion: + return self._rotation + + def set_rotation(self, rotation: Quaternion): + self._rotation = rotation + + def get_position(self) -> Vector3: + return self._position + + def set_position(self, position: Vector3): + self._position = position + + def get_scale(self) -> Vector3: + return self._scale + + def set_scale(self, scale: Vector3): + self._scale = scale + + def __init__(self, *, name: str, parent: str): + self.frames_settings = 0 + + self._name: str = name + self._parent: str or None = parent + self._instances = [] + self._frames = [] + + def __repr__(self) -> str: + result = self._name + if self._parent: + result += " <- " + self._parent + + return f'Node({result})' + + def get_name(self) -> str: + return self._name + + def get_parent(self) -> str or None: + return self._parent + + def get_instances(self) -> List[Instance]: + return self._instances + + def add_instance(self, instance: Instance): + self._instances.append(instance) + + def get_frames(self) -> List[Frame]: + return self._frames + + def add_frame(self, frame: Frame): + self._frames.append(frame) diff --git a/models_converter/formats/universal/scene.py b/models_converter/formats/universal/scene.py new file mode 100644 index 0000000..2125a30 --- /dev/null +++ b/models_converter/formats/universal/scene.py @@ -0,0 +1,45 @@ +from typing import List + +from models_converter.formats.universal.camera import Camera +from models_converter.formats.universal.geometry import Geometry +from models_converter.formats.universal.node import Node + + +class Scene: + def __init__(self): + self._frame_rate: int = 30 + + self._materials = [] + self._geometries = [] + self._cameras = [] + self._nodes = [] + + def get_materials(self) -> List: + return self._materials + + def add_material(self, material): + self._materials.append(material) + + def get_geometries(self) -> List[Geometry]: + return self._geometries + + def add_geometry(self, geometry: Geometry): + self._geometries.append(geometry) + + def get_cameras(self) -> List[Camera]: + return self._cameras + + def add_camera(self, camera: Camera): + self._cameras.append(camera) + + def get_nodes(self) -> List[Node]: + return self._nodes + + def add_node(self, node: Node): + self._nodes.append(node) + + def get_frame_rate(self) -> int: + return self._frame_rate + + def set_frame_rate(self, frame_rate: int): + self._frame_rate = frame_rate diff --git a/models_converter/formats/wavefront/__init__.py b/models_converter/formats/wavefront/__init__.py new file mode 100644 index 0000000..9e7decf --- /dev/null +++ b/models_converter/formats/wavefront/__init__.py @@ -0,0 +1,7 @@ +from .parser import Parser +from .writer import Writer + +__all__ = [ + 'Writer', + 'Parser' +] diff --git a/models_converter/formats/wavefront/parser.py b/models_converter/formats/wavefront/parser.py new file mode 100644 index 0000000..88b9d17 --- /dev/null +++ b/models_converter/formats/wavefront/parser.py @@ -0,0 +1,145 @@ +from models_converter.formats.universal import Scene, Node, Geometry +from models_converter.interfaces import ParserInterface +from models_converter.utilities.math import Vector3, Quaternion + + +class Parser(ParserInterface): + def __init__(self, file_data: bytes or str): + if type(file_data) is bytes: + file_data = file_data.decode() + + self.scene = Scene() + + self.lines = file_data.split('\n') + + self.position_temp, self.position = [], [] + self.normals_temp, self.normals = [], [] + self.texcoord_temp, self.texcoord = [], [] + + def parse(self): + triangles = [] + geometry_name = None + material = 'character_mat' + position_scale, normals_scale, texcoord_scale = 1, 1, 1 + + vertices_offsets = { + 'POSITION': 0, + 'TEXCOORD': 0, + 'NORMAL': 0 + } + + names = [line[2:].split('|')[0] + for line in list(filter(lambda line: line.startswith('o '), self.lines))] + + for line_index in range(len(self.lines)): + line = self.lines[line_index] + items = line.split()[1:] + if line.startswith('v '): # POSITION + for item in items: + self.position_temp.append(float(item)) + elif line.startswith('vn '): # NORMAL + for item in items: + self.normals_temp.append(float(item)) + elif line.startswith('vt '): # TEXCOORD + if len(items) > 2: + items = items[:-1] + for item in items: + self.texcoord_temp.append(float(item)) + elif line.startswith('f '): + temp_list = [] + if len(items) > 3: + raise ValueError('It is necessary to triangulate the model') + for item in items: + second_temp_list = [] + if len(item.split('/')) == 2: + raise ValueError('Model have not normals or texture') + elif len(item.split('/')) == 1: + raise ValueError('Model have not normals and texture') + for x in item.split('/'): + second_temp_list.append(int(x) - 1) + temp_list.append([second_temp_list[0] - vertices_offsets['POSITION'], + second_temp_list[2] - vertices_offsets['TEXCOORD'], + second_temp_list[1] - vertices_offsets['NORMAL']]) + triangles.append(temp_list) + elif line.startswith('o '): + geometry_name = items[0] + if '|' in items[0]: + geometry_name, material = items[0].split('|') + + if self.position_temp: + self.position = [] + position_scale = self._get_vertex_scale(self.position_temp) + for x in range(0, len(self.position_temp), 3): + self.position.append([vertex / position_scale for vertex in self.position_temp[x: x + 3]]) + + if self.normals_temp: + self.normals = [] + normals_scale = self._get_vertex_scale(self.normals_temp) + for x in range(0, len(self.normals_temp), 3): + self.normals.append([vertex / normals_scale for vertex in self.normals_temp[x: x + 3]]) + + if self.texcoord_temp: + self.texcoord = [] + texcoord_scale = self._get_vertex_scale(self.texcoord_temp) + for x in range(0, len(self.texcoord_temp), 2): + self.texcoord.append([vertex / texcoord_scale for vertex in self.texcoord_temp[x: x + 2]]) + + if not line.startswith('f ') and triangles and geometry_name and \ + self.position and self.normals and self.texcoord: + self.position_temp = [] + self.normals_temp = [] + self.texcoord_temp = [] + + if len(names) > len(self.scene.get_geometries()) + 1 and \ + names[len(self.scene.get_geometries()) + 1] != geometry_name: + vertices_offsets['POSITION'] += len(self.position) + vertices_offsets['TEXCOORD'] += len(self.normals) + vertices_offsets['NORMAL'] += len(self.texcoord) + if not (self.scene.get_geometries() and self.scene.get_geometries()[-1].get_name() == geometry_name): + geometry = Geometry(name=geometry_name, group='GEO') + geometry.add_vertex(Geometry.Vertex( + name='position_0', + vertex_type='POSITION', + vertex_index=0, + vertex_scale=position_scale, + points=self.position + )) + geometry.add_vertex(Geometry.Vertex( + name='normal_0', + vertex_type='NORMAL', + vertex_index=1, + vertex_scale=normals_scale, + points=self.normals + )) + geometry.add_vertex(Geometry.Vertex( + name='texcoord_0', + vertex_type='TEXCOORD', + vertex_index=2, + vertex_scale=texcoord_scale, + points=self.texcoord + )) + self.scene.add_geometry(geometry) + self.scene.get_geometries()[-1].add_material( + Geometry.Material(material, triangles) + ) + + material = 'character_mat' + triangles = [] + + for geometry in self.scene.get_geometries(): + node = Node(name=geometry.get_name(), parent='') + instance = Node.Instance(name=geometry.get_name(), instance_type='GEOM') + for material in geometry.get_materials(): + instance.add_bind(material.get_name(), material.get_name()) + node.add_instance(instance) + + node.add_frame(Node.Frame(0, Vector3(), Vector3(1, 1, 1), Quaternion())) + + self.scene.add_node(node) + + @staticmethod + def _get_vertex_scale(vertex_data: list): + vertex_scale = max(max(vertex_data), abs(min(vertex_data))) + if vertex_scale < 1: + vertex_scale = 1 + return vertex_scale diff --git a/models_converter/formats/wavefront/writer.py b/models_converter/formats/wavefront/writer.py new file mode 100644 index 0000000..0b3dcc2 --- /dev/null +++ b/models_converter/formats/wavefront/writer.py @@ -0,0 +1,56 @@ +from models_converter.formats.universal import Scene +from models_converter.interfaces import WriterInterface + + +class Writer(WriterInterface): + def __init__(self): + self.writen = '' + + self.temp_vertices_offsets = { + 'POSITION': 0, + 'TEXCOORD': 0, + 'NORMAL': 0 + } + + self.vertices_offsets = { + 'POSITION': 0, + 'TEXCOORD': 0, + 'NORMAL': 0 + } + + def write(self, scene: Scene): + for geometry in scene.get_geometries(): + for key in self.vertices_offsets.keys(): + self.vertices_offsets[key] = self.temp_vertices_offsets[key] + prefix = '' + + for vertex in geometry.get_vertices(): + if vertex.get_type() == 'POSITION': + prefix = 'v ' + elif vertex.get_type() == 'NORMAL': + prefix = 'vn ' + elif vertex.get_type() == 'TEXCOORD': + prefix = 'vt ' + + self.temp_vertices_offsets[vertex.get_type()] += len(vertex.get_points()) + + for triangle in vertex.get_points(): + temp_string = prefix + for point in triangle: + temp_string += str(point * vertex.get_scale()) + ' ' + self.writen += f'{temp_string}\n' + self.writen += '\n\n' + for material in geometry.get_materials(): + self.writen += f'o {geometry.get_name()}|{material.get_name()}\n\n' + for triangle in material.get_triangles(): + temp_string = 'f ' + for point in triangle: + temp_list = [ + str(point[0] + self.vertices_offsets['POSITION'] + 1), # POSITION + str(point[2] + self.vertices_offsets['TEXCOORD'] + 1), # TEXCOORD + str(point[1] + self.vertices_offsets['NORMAL'] + 1) # NORMAL + ] + + temp_string += '/'.join(temp_list) + ' ' + self.writen += f'{temp_string}\n' + self.writen += '\n\n' diff --git a/models_converter/interfaces/__init__.py b/models_converter/interfaces/__init__.py new file mode 100644 index 0000000..88a8b31 --- /dev/null +++ b/models_converter/interfaces/__init__.py @@ -0,0 +1,7 @@ +from models_converter.interfaces.writer_interface import WriterInterface +from models_converter.interfaces.parser_interface import ParserInterface + +__all__ = [ + 'WriterInterface', + 'ParserInterface' +] diff --git a/models_converter/interfaces/parser_interface.py b/models_converter/interfaces/parser_interface.py new file mode 100644 index 0000000..f20d998 --- /dev/null +++ b/models_converter/interfaces/parser_interface.py @@ -0,0 +1,11 @@ +from models_converter.formats.universal import Scene + + +class ParserInterface: + def __init__(self): + self.scene: Scene or None = None + + raise NotImplementedError('This is an abstract class') + + def parse(self): + raise NotImplementedError('This is an abstract class') diff --git a/models_converter/interfaces/writer_interface.py b/models_converter/interfaces/writer_interface.py new file mode 100644 index 0000000..d48f25c --- /dev/null +++ b/models_converter/interfaces/writer_interface.py @@ -0,0 +1,13 @@ +from models_converter.formats.universal import Scene + + +class WriterInterface: + MAGIC: bytes + + def __init__(self): + self.writen: bytes or str = None + + raise NotImplementedError('This is an abstract class') + + def write(self, scene: Scene): + raise NotImplementedError('This is an abstract class') diff --git a/models_converter/utilities/__init__.py b/models_converter/utilities/__init__.py new file mode 100644 index 0000000..cbed483 --- /dev/null +++ b/models_converter/utilities/__init__.py @@ -0,0 +1,20 @@ +def remove_prefix(string: str, prefix: str): + if string.startswith(prefix): + return string[len(prefix):] + return string + + +def remove_suffix(string: str, suffix: str): + if string.endswith(suffix): + return string[:len(string) - len(suffix)] + return string + + +__all__ = [ + 'remove_prefix', + 'remove_suffix', + 'writer', + 'reader', + 'math', + 'matrix' +] diff --git a/models_converter/utilities/math/__init__.py b/models_converter/utilities/math/__init__.py new file mode 100644 index 0000000..7d4f1f5 --- /dev/null +++ b/models_converter/utilities/math/__init__.py @@ -0,0 +1,7 @@ +from models_converter.utilities.math.quaternion import Quaternion +from models_converter.utilities.math.vector3 import Vector3 + +__all__ = [ + 'Quaternion', + 'Vector3' +] diff --git a/models_converter/utilities/math/quaternion.py b/models_converter/utilities/math/quaternion.py new file mode 100644 index 0000000..1038576 --- /dev/null +++ b/models_converter/utilities/math/quaternion.py @@ -0,0 +1,9 @@ +class Quaternion: + def __init__(self, x: float = 0, y: float = 0, z: float = 0, w: float = 1): + self.x = x + self.y = y + self.z = z + self.w = w + + def clone(self): + return Quaternion(self.x, self.y, self.z, self.w) diff --git a/models_converter/utilities/math/vector3.py b/models_converter/utilities/math/vector3.py new file mode 100644 index 0000000..335ce98 --- /dev/null +++ b/models_converter/utilities/math/vector3.py @@ -0,0 +1,8 @@ +class Vector3: + def __init__(self, x: float = 0, y: float = 0, z: float = 0): + self.x = x + self.y = y + self.z = z + + def clone(self): + return Vector3(self.x, self.y, self.z) diff --git a/models_converter/utils/matrix/__init__.py b/models_converter/utilities/matrix/__init__.py similarity index 100% rename from models_converter/utils/matrix/__init__.py rename to models_converter/utilities/matrix/__init__.py diff --git a/models_converter/utils/matrix/matrix2x2.py b/models_converter/utilities/matrix/matrix2x2.py similarity index 100% rename from models_converter/utils/matrix/matrix2x2.py rename to models_converter/utilities/matrix/matrix2x2.py diff --git a/models_converter/utils/matrix/matrix3x3.py b/models_converter/utilities/matrix/matrix3x3.py similarity index 100% rename from models_converter/utils/matrix/matrix3x3.py rename to models_converter/utilities/matrix/matrix3x3.py diff --git a/models_converter/utils/matrix/matrix4x4.py b/models_converter/utilities/matrix/matrix4x4.py similarity index 87% rename from models_converter/utils/matrix/matrix4x4.py rename to models_converter/utilities/matrix/matrix4x4.py index 5cc4a33..ee7223d 100644 --- a/models_converter/utils/matrix/matrix4x4.py +++ b/models_converter/utilities/matrix/matrix4x4.py @@ -1,5 +1,6 @@ from .matrix3x3 import Matrix3x3 from . import Matrix +from ..math import Vector3, Quaternion class Matrix4x4(Matrix): @@ -187,8 +188,8 @@ def cofactor(self): self.find_cofactor() - def put_rotation(self, xyz: tuple, w: float): - x, y, z = xyz + def put_rotation(self, quaterion: Quaternion): + x, y, z, w = quaterion.x, quaterion.y, quaterion.z, quaterion.w rotation_matrix = ( (1-2*y**2-2*z**2, 2*x*y-2*z*w, 2*x*z+2*y*w, 0), # x @@ -199,61 +200,39 @@ def put_rotation(self, xyz: tuple, w: float): self.rotation_matrix = Matrix4x4(matrix=rotation_matrix) - def put_position(self, xyz: tuple): - x, y, z = xyz - + def put_position(self, position: Vector3): translation_matrix = ( - (1, 0, 0, x), # x - (0, 1, 0, y), # y - (0, 0, 1, z), # z + (1, 0, 0, position.x), # x + (0, 1, 0, position.y), # y + (0, 0, 1, position.z), # z (0, 0, 0, 1) ) self.translation_matrix = Matrix4x4(matrix=translation_matrix) - def put_scale(self, xyz: tuple): - x, y, z = xyz - + def put_scale(self, scale: Vector3): scale_matrix = ( - (x, 0, 0, 0), - (0, y, 0, 0), - (0, 0, z, 0), + (scale.x, 0, 0, 0), + (0, scale.y, 0, 0), + (0, 0, scale.z, 0), (0, 0, 0, 1) ) self.scale_matrix = Matrix4x4(matrix=scale_matrix) - def get_rotation(self) -> dict: - rotation = { - 'x': 0, - 'y': 0, - 'z': 0, - 'w': 0 - } + def get_rotation(self) -> Quaternion: + return Quaternion() - return rotation - - def get_position(self) -> dict: - position = (self.matrix[0][3], self.matrix[1][3], self.matrix[2][3]) + def get_position(self) -> Vector3: + position = Vector3(self.matrix[0][3], self.matrix[1][3], self.matrix[2][3]) self.put_position(position) - position = { - 'x': position[0], - 'y': position[1], - 'z': position[2] - } return position - def get_scale(self) -> dict: - xyz = (1, 1, 1) - - self.put_scale(xyz) + def get_scale(self) -> Vector3: + scale = Vector3(1, 1, 1) - scale = { - 'x': xyz[0], - 'y': xyz[1], - 'z': xyz[2] - } + self.put_scale(scale) return scale diff --git a/models_converter/utils/reader.py b/models_converter/utilities/reader.py similarity index 97% rename from models_converter/utils/reader.py rename to models_converter/utilities/reader.py index ddf0668..467891f 100644 --- a/models_converter/utils/reader.py +++ b/models_converter/utilities/reader.py @@ -108,12 +108,12 @@ def readBool(self) -> bool: readUByte = readUInt8 readByte = readInt8 - def readChar(self, length: int = 1) -> str: + def readChars(self, length: int = 1) -> str: return self.read(length).decode('utf-8') def readString(self) -> str: length = self.readUShort() - return self.readChar(length) + return self.readChars(length) def tell(self) -> int: return self.i diff --git a/models_converter/utils/writer.py b/models_converter/utilities/writer.py similarity index 95% rename from models_converter/utils/writer.py rename to models_converter/utilities/writer.py index 8af202f..2b4f3b1 100644 --- a/models_converter/utils/writer.py +++ b/models_converter/utilities/writer.py @@ -1,5 +1,8 @@ +from typing import Literal + + class Writer: - def __init__(self, endian='big'): + def __init__(self, endian: Literal['big', 'little'] = 'big'): self.endian = endian self.buffer = b'' @@ -114,6 +117,9 @@ def writeChar(self, string: str) -> None: self.buffer += char.encode('utf-8') def writeString(self, string: str) -> None: + if string is None: + string = '' + encoded = string.encode('utf-8') self.writeUShort(len(encoded)) self.buffer += encoded diff --git a/models_converter/utils/__init__.py b/models_converter/utils/__init__.py deleted file mode 100644 index 87b30e1..0000000 --- a/models_converter/utils/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -__all__ = [ - 'reader', - 'writer', - 'matrix' -] diff --git a/setup.py b/setup.py index 922a0e2..be7912e 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setuptools.setup( name='3d-converter', - version='0.8.5', + version='0.8.8', author='Vorono4ka', author_email='crowo4ka@gmail.com', description='Python 3D Models Converter',