basic working version
This commit is contained in:
		
							
								
								
									
										126
									
								
								addons/io_scene_swbf_msh/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								addons/io_scene_swbf_msh/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,126 @@ | ||||
| bl_info = { | ||||
|     'name': 'SWBF .msh export', | ||||
|     'author': '', | ||||
|     "version": (0, 1, 0), | ||||
|     'blender': (2, 80, 0), | ||||
|     'location': 'File > Import-Export', | ||||
|     'description': 'Export as SWBF .msh file', | ||||
|     'warning': '', | ||||
|     'wiki_url': "", | ||||
|     'tracker_url': "", | ||||
|     'support': 'COMMUNITY', | ||||
|     'category': 'Import-Export' | ||||
| } | ||||
|  | ||||
| # Taken from glTF-Blender-IO, because I do not understand Python that well | ||||
| # (this is the first thing of substance I've created in it) and just wanted  | ||||
| # script reloading to work. | ||||
| #  | ||||
| # https://github.com/KhronosGroup/glTF-Blender-IO | ||||
| # | ||||
| # Copyright 2018-2019 The glTF-Blender-IO authors. | ||||
| # | ||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| # you may not use this file except in compliance with the License. | ||||
| # You may obtain a copy of the License at | ||||
| # | ||||
| #     http://www.apache.org/licenses/LICENSE-2.0 | ||||
| # | ||||
| # Unless required by applicable law or agreed to in writing, software | ||||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| # See the License for the specific language governing permissions and | ||||
| # limitations under the License. | ||||
| def reload_package(module_dict_main): | ||||
|     import importlib | ||||
|     from pathlib import Path | ||||
|  | ||||
|     def reload_package_recursive(current_dir, module_dict): | ||||
|         for path in current_dir.iterdir(): | ||||
|             if "__init__" in str(path) or path.stem not in module_dict: | ||||
|                 continue | ||||
|  | ||||
|             if path.is_file() and path.suffix == ".py": | ||||
|                 importlib.reload(module_dict[path.stem]) | ||||
|             elif path.is_dir(): | ||||
|                 reload_package_recursive(path, module_dict[path.stem].__dict__) | ||||
|  | ||||
|     reload_package_recursive(Path(__file__).parent, module_dict_main) | ||||
|  | ||||
|  | ||||
| if "bpy" in locals(): | ||||
|     reload_package(locals()) | ||||
| # End of stuff taken from glTF | ||||
|  | ||||
| import bpy | ||||
| from bpy_extras.io_utils import ExportHelper | ||||
| from bpy.props import StringProperty, BoolProperty, EnumProperty | ||||
| from bpy.types import Operator | ||||
| from .msh_scene import create_scene | ||||
| from .msh_scene_save import save_scene | ||||
|  | ||||
| def write_some_data(context, filepath, use_some_setting): | ||||
|     with open(filepath, 'wb') as output_file: | ||||
|         save_scene(output_file, create_scene()) | ||||
|  | ||||
|     return {'FINISHED'} | ||||
|  | ||||
|  | ||||
| # ExportHelper is a helper class, defines filename and | ||||
| # invoke() function which calls the file selector. | ||||
| class ExportSomeData(Operator, ExportHelper): | ||||
|     """This appears in the tooltip of the operator and in the generated docs""" | ||||
|     bl_idname = "export_test.some_data"  # important since its how bpy.ops.import_test.some_data is constructed | ||||
|     bl_label = "Export Some Data" | ||||
|  | ||||
|     # ExportHelper mixin class uses this | ||||
|     filename_ext = ".msh" | ||||
|  | ||||
|     filter_glob: StringProperty( | ||||
|         default="*.msh", | ||||
|         options={'HIDDEN'}, | ||||
|         maxlen=255,  # Max internal buffer length, longer would be clamped. | ||||
|     ) | ||||
|  | ||||
|     # List of operator properties, the attributes will be assigned | ||||
|     # to the class instance from the operator settings before calling. | ||||
|     use_setting: BoolProperty( | ||||
|         name="Example Boolean", | ||||
|         description="Example Tooltip", | ||||
|         default=True, | ||||
|     ) | ||||
|  | ||||
|     type: EnumProperty( | ||||
|         name="Example Enum", | ||||
|         description="Choose between two items", | ||||
|         items=( | ||||
|             ('OPT_A', "First Option", "Description one"), | ||||
|             ('OPT_B', "Second Option", "Description two"), | ||||
|         ), | ||||
|         default='OPT_A', | ||||
|     ) | ||||
|  | ||||
|     def execute(self, context): | ||||
|         return write_some_data(context, self.filepath, self.use_setting) | ||||
|  | ||||
|  | ||||
| # Only needed if you want to add into a dynamic menu | ||||
| def menu_func_export(self, context): | ||||
|     self.layout.operator(ExportSomeData.bl_idname, text="SWBF msh (.msh)") | ||||
|  | ||||
|  | ||||
| def register(): | ||||
|     bpy.utils.register_class(ExportSomeData) | ||||
|     bpy.types.TOPBAR_MT_file_export.append(menu_func_export) | ||||
|  | ||||
|  | ||||
| def unregister(): | ||||
|     bpy.utils.unregister_class(ExportSomeData) | ||||
|     bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     register() | ||||
|  | ||||
|     # test call | ||||
|     # bpy.ops.export_test.some_data('INVOKE_DEFAULT') | ||||
							
								
								
									
										67
									
								
								addons/io_scene_swbf_msh/msh_model.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								addons/io_scene_swbf_msh/msh_model.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| """ Contains Model and dependent types for constructing a scene hierarchy easilly | ||||
|     saved to a .msh file. """ | ||||
|  | ||||
| from dataclasses import dataclass, field | ||||
| from typing import List | ||||
| from enum import Enum | ||||
| from mathutils import Vector, Quaternion | ||||
|  | ||||
| class ModelType(Enum): | ||||
|     NULL = 0 | ||||
|     SKIN = 1 | ||||
|     CLOTH = 2 | ||||
|     BONE = 3 | ||||
|     STATIC = 4 | ||||
|  | ||||
| class CollisionPrimitiveShape(Enum): | ||||
|     SPHERE = 0 | ||||
|     # ELLIPSOID = 1 | ||||
|     CYLINDER = 2 | ||||
|     # MESH = 3 | ||||
|     CUBE = 4 | ||||
|  | ||||
| @dataclass | ||||
| class ModelTransform: | ||||
|     """ Class representing a `TRAN` section in a .msh file. """ | ||||
|  | ||||
|     translation: Vector = Vector((0.0, 0.0, 0.0)) | ||||
|     rotation: Quaternion = Quaternion((1.0, 0.0, 0.0, 0.0)) | ||||
|  | ||||
| @dataclass | ||||
| class GeometrySegment: | ||||
|     """ Class representing a 'SEGM' section in a .msh file. """ | ||||
|  | ||||
|     material_name: str = "" | ||||
|  | ||||
|     positions: List[Vector] = field(default_factory=list) | ||||
|     normals: List[Vector] = field(default_factory=list) | ||||
|     colors: List[List[float]] = None | ||||
|     texcoords: List[Vector] = field(default_factory=list) | ||||
|     # TODO: Skin support. | ||||
|  | ||||
|     polygons: List[List[int]] = field(default_factory=list) | ||||
|     triangles: List[List[int]] = field(default_factory=list) | ||||
|     triangle_strips: List[List[int]] = None | ||||
|  | ||||
| @dataclass | ||||
| class CollisionPrimitive: | ||||
|     """ Class representing a 'SWCI' section in a .msh file. """ | ||||
|  | ||||
|     collision_primitive_shape: CollisionPrimitiveShape | ||||
|     radius: float | ||||
|     height: float | ||||
|     length: float | ||||
|  | ||||
| @dataclass | ||||
| class Model: | ||||
|     """ Class representing a 'MODL' section in a .msh file. """ | ||||
|  | ||||
|     name: str = "Model" | ||||
|     parent: str = "" | ||||
|     model_type: ModelType = ModelType.NULL | ||||
|     hidden: bool = True | ||||
|  | ||||
|     transform: ModelTransform = ModelTransform() | ||||
|  | ||||
|     geometry: List[GeometrySegment] = None | ||||
|     collisionprimitive: CollisionPrimitive = None | ||||
							
								
								
									
										185
									
								
								addons/io_scene_swbf_msh/msh_model_gather.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								addons/io_scene_swbf_msh/msh_model_gather.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,185 @@ | ||||
| """ Gathers the Blender objects from the current scene and returns them as a list of | ||||
|     Model objects. """ | ||||
|  | ||||
| import bpy | ||||
| from typing import List, Set, Dict | ||||
| from itertools import zip_longest | ||||
| from .msh_model import * | ||||
| from .msh_model_utilities import * | ||||
| from .msh_utilities import * | ||||
|  | ||||
| SKIPPED_OBJECT_TYPES = {"LATTICE", "CAMERA", "LIGHT", "SPEAKER", "LIGHT_PROBE"} | ||||
| MESH_OBJECT_TYPES = {"MESH", "CURVE", "SURFACE", "META", "FONT", "GPENCIL"} | ||||
|  | ||||
| def gather_models() -> List[Model]: | ||||
|     """ Gathers the Blender objects from the current scene and returns them as a list of | ||||
|         Model objects. """ | ||||
|  | ||||
|     parents = create_parents_set() | ||||
|  | ||||
|     models_list: List[Model] = [] | ||||
|  | ||||
|     for obj in bpy.context.scene.objects: | ||||
|         if obj.type in SKIPPED_OBJECT_TYPES and obj.name not in parents: | ||||
|             continue | ||||
|  | ||||
|         model = Model() | ||||
|         model.name = obj.name | ||||
|         model.model_type = get_model_type(obj) | ||||
|         model.hidden = get_is_model_hidden(obj) | ||||
|         model.transform.rotation = obj.rotation_quaternion.copy() | ||||
|         model.transform.rotation.rotate(obj.delta_rotation_quaternion) | ||||
|         model.transform.translation = add_vec(obj.location, obj.delta_location) | ||||
|  | ||||
|         if obj.parent is not None: | ||||
|             model.parent = obj.parent.name | ||||
|  | ||||
|         if obj.type in MESH_OBJECT_TYPES: | ||||
|             mesh = obj.to_mesh() | ||||
|             model.geometry = create_mesh_geometry(mesh) | ||||
|             obj.to_mesh_clear() | ||||
|  | ||||
|             mesh_scale = get_object_worldspace_scale(obj) | ||||
|             scale_segments(mesh_scale, model.geometry) | ||||
|  | ||||
|         models_list.append(model) | ||||
|  | ||||
|     return models_list | ||||
|  | ||||
| def create_parents_set() -> Set[str]: | ||||
|     """ Creates a set with the names of the Blender objects from the current scene | ||||
|         that have at least one child. """ | ||||
|          | ||||
|     parents = set() | ||||
|  | ||||
|     for obj in bpy.context.scene.objects: | ||||
|         if obj.parent is not None: | ||||
|             parents.add(obj.parent.name) | ||||
|  | ||||
|     return parents | ||||
|  | ||||
| def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]: | ||||
|     """ Creates a list of GeometrySegment objects from a Blender mesh. | ||||
|         Does NOT create triangle strips in the GeometrySegment however. """ | ||||
|  | ||||
|     mesh.validate_material_indices() | ||||
|     mesh.calc_loop_triangles() | ||||
|  | ||||
|     if len(mesh.materials) != 0: | ||||
|         return create_mesh_geometry_with_materials(mesh) | ||||
|  | ||||
|     return [create_mesh_geometry_without_materials(mesh)] | ||||
|  | ||||
| def create_mesh_geometry_with_materials(mesh: bpy.types.Mesh) -> List[GeometrySegment]: | ||||
|     """ Creates a list of GeometrySegment objects from a Blender mesh | ||||
|         that references materials. """ | ||||
|  | ||||
|     segments: List[GeometrySegment] = [GeometrySegment() for i in range(len(mesh.materials))] | ||||
|     vertex_remap: List[Dict[int, int]] = [dict() for i in range(len(mesh.materials))] | ||||
|     polygons: List[Set[int]] = [set() for i in range(len(mesh.materials))] | ||||
|  | ||||
|     if mesh.vertex_colors.active is not None: | ||||
|         for segment in segments: | ||||
|             segment.colors = [] | ||||
|  | ||||
|     for segment, material in zip(segments, mesh.materials): | ||||
|         segment.material_name = material.name | ||||
|  | ||||
|     def add_vertex(material_index: int, index : int) -> int: | ||||
|         nonlocal segments, vertex_remap | ||||
|  | ||||
|         segment = segments[material_index] | ||||
|         remap = vertex_remap[material_index] | ||||
|  | ||||
|         if index in remap: | ||||
|             return remap[index] | ||||
|  | ||||
|         new_index: int = len(segment.positions) | ||||
|         remap[index] = new_index | ||||
|  | ||||
|         segment.positions.append(mesh.vertices[index].co.copy()) | ||||
|         segment.normals.append(mesh.vertices[index].normal.copy()) | ||||
|  | ||||
|         if mesh.uv_layers.active is None: | ||||
|             segment.texcoords.append(Vector((0.0, 0.0))) | ||||
|         else: | ||||
|             segment.texcoords.append(mesh.uv_layers.active.data[index].uv.copy()) | ||||
|  | ||||
|         if segment.colors is not None: | ||||
|             segment.colors.append([v for v in mesh.vertex_colors.active.data[index].color]) | ||||
|  | ||||
|         return new_index | ||||
|  | ||||
|     for tri in mesh.loop_triangles: | ||||
|         polygons[tri.material_index].add(tri.polygon_index) | ||||
|         segments[tri.material_index].triangles.append([add_vertex(tri.material_index, i) for i in  tri.vertices]) | ||||
|  | ||||
|     for segment, remap, polys in zip(segments, vertex_remap, polygons): | ||||
|         for poly_index in polys: | ||||
|             poly = mesh.polygons[poly_index] | ||||
|  | ||||
|             segment.polygons.append([remap[i] for i in poly.vertices]) | ||||
|  | ||||
|     return segments | ||||
|  | ||||
| def create_mesh_geometry_without_materials(mesh: bpy.types.Mesh) -> GeometrySegment: | ||||
|     """ Creates a list of GeometrySegment objects from a Blender mesh | ||||
|         that doesn't references materials. """ | ||||
|  | ||||
|     segment = GeometrySegment() | ||||
|  | ||||
|     for tri in mesh.loop_triangles: | ||||
|         segment.triangles.append([i for i in  tri.vertices]) | ||||
|  | ||||
|     for vertex in mesh.vertices: | ||||
|         segment.positions.append(vertex.co.copy()) | ||||
|         segment.normals.append(vertex.normal.copy()) | ||||
|  | ||||
|     if mesh.uv_layers.active is None: | ||||
|         segment.texcoords = [Vector((0.0, 0.0))] * len(mesh.vertices) | ||||
|     else: | ||||
|         segment.texcoords = [texcoords.uv.copy() for texcoords in mesh.uv_layers.active.data] | ||||
|  | ||||
|     if mesh.vertex_colors.active is not None: | ||||
|         segment.colors = [[v for v in color.color] for color in mesh.vertex_colors.active.data] | ||||
|  | ||||
|     for polygon in mesh.polygons: | ||||
|         segment.polygons.append([v for v in polygon.vertices]) | ||||
|  | ||||
|     return segment | ||||
|  | ||||
| def get_object_worldspace_scale(obj: bpy.types.Object) -> Vector: | ||||
|     """ Get the worldspace scale transform for a Blender object. """ | ||||
|  | ||||
|     scale = mul_vec(obj.scale, obj.delta_scale) | ||||
|  | ||||
|     while obj.parent is not None: | ||||
|         obj = obj.parent | ||||
|         scale = mul_vec(scale, mul_vec(obj.scale, obj.delta_scale)) | ||||
|  | ||||
|     return scale | ||||
|  | ||||
| def get_model_type(obj: bpy.types.Object) -> ModelType: | ||||
|     """ Get the ModelType for a Blender object. """ | ||||
|     # TODO: Skinning support, etc | ||||
|  | ||||
|     if obj.type in MESH_OBJECT_TYPES: | ||||
|         return ModelType.STATIC | ||||
|  | ||||
|     return ModelType.NULL | ||||
|  | ||||
| def get_is_model_hidden(obj: bpy.types.Object) -> bool: | ||||
|     """ Gets if a Blender object should be marked as hidden in the .msh file. """ | ||||
|  | ||||
|     name = obj.name.lower() | ||||
|  | ||||
|     if name.startswith("sv_"): | ||||
|         return True | ||||
|     if name.startswith("p_"): | ||||
|         return True | ||||
|     if name.startswith("collision"): | ||||
|         return True | ||||
|     if obj.type not in MESH_OBJECT_TYPES: | ||||
|         return True | ||||
|  | ||||
|     return False | ||||
							
								
								
									
										81
									
								
								addons/io_scene_swbf_msh/msh_model_triangle_strips.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								addons/io_scene_swbf_msh/msh_model_triangle_strips.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| """ Contains triangle strip generation functions for GeometrySegment. """ | ||||
|  | ||||
| from typing import List, Tuple | ||||
| from copy import deepcopy | ||||
| from .msh_model import * | ||||
|  | ||||
| def create_models_triangle_strips(models: List[Model]) -> List[Model]: | ||||
|     """ Create the triangle strips for a list of models geometry. """ | ||||
|  | ||||
|     for model in models: | ||||
|         if model.geometry is not None: | ||||
|             for segment in model.geometry: | ||||
|                 segment.triangle_strips = create_triangle_strips(segment.triangles) | ||||
|  | ||||
|     return models | ||||
|  | ||||
| def create_triangle_strips(segment_triangles: List[List[int]]) -> List[List[int]]: | ||||
|     """ Create the triangle strips for a list of triangles. """ | ||||
|  | ||||
|     triangles = deepcopy(segment_triangles) | ||||
|     strips: List[List[int]] = [] | ||||
|  | ||||
|     # The general idea here is we loop based off if 'triangles' is empty or not. | ||||
|     # | ||||
|     # For each iteration of the loop we create a new strip starting from the first | ||||
|     # triangle still in 'triangles'. | ||||
|     # | ||||
|     # Then we loop, attempting to find a triangle to add the strip each time. If we | ||||
|     # find one then we continue the loop, else we break out of it and append the | ||||
|     # created strip. | ||||
|  | ||||
|     def create_strip() -> List[int]: | ||||
|         strip: List[int] = [triangles[0][0], | ||||
|                             triangles[0][1], | ||||
|                             triangles[0][2]] | ||||
|         strip_head: Tuple[int, int] = (strip[1], strip[2]) | ||||
|  | ||||
|         triangles.remove(triangles[0]) | ||||
|  | ||||
|         while True: | ||||
|             def find_next_vertex(): | ||||
|                 nonlocal triangles | ||||
|  | ||||
|                 even: bool = len(strip) % 2 == 0 | ||||
|  | ||||
|                 for tri, edge, last_vertex in iterate_triangle_edges_last_vertex(triangles, even): | ||||
|                     if edge == strip_head: | ||||
|                         triangles.remove(tri) | ||||
|                         return last_vertex | ||||
|  | ||||
|                 return None | ||||
|  | ||||
|             next_vertex: int = find_next_vertex() | ||||
|  | ||||
|             if next_vertex is None: | ||||
|                 break | ||||
|  | ||||
|             strip.append(next_vertex) | ||||
|             strip_head = (strip_head[1], next_vertex) | ||||
|  | ||||
|         return strip | ||||
|  | ||||
|     while triangles: | ||||
|         strips.append(create_strip()) | ||||
|  | ||||
|     return strips | ||||
|  | ||||
| def iterate_triangle_edges_last_vertex(triangles: List[List[int]], even: bool): | ||||
|     """ Generator for iterating through the of each triangle in a list edges. | ||||
|         Yields (triangle, edge, last_vertex). """ | ||||
|  | ||||
|     if even: | ||||
|         for tri in triangles: | ||||
|             yield tri, (tri[0], tri[1]), tri[2] | ||||
|             yield tri, (tri[0], tri[2]), tri[1] | ||||
|             yield tri, (tri[1], tri[2]), tri[0] | ||||
|     else: | ||||
|         for tri in triangles: | ||||
|             yield tri, (tri[1], tri[0]), tri[2] | ||||
|             yield tri, (tri[2], tri[0]), tri[1] | ||||
|             yield tri, (tri[2], tri[1]), tri[0] | ||||
							
								
								
									
										117
									
								
								addons/io_scene_swbf_msh/msh_model_utilities.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								addons/io_scene_swbf_msh/msh_model_utilities.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | ||||
| """ Utilities for operating on msh_model objects. """ | ||||
|  | ||||
| from typing import List | ||||
| from .msh_model import * | ||||
| from .msh_utilities import * | ||||
| from mathutils import Vector, Matrix | ||||
|  | ||||
| def scale_segments(scale: Vector, segments: List[GeometrySegment]): | ||||
|     """ Scales are positions in the GeometrySegment list. """ | ||||
|  | ||||
|     for segment in segments: | ||||
|         for pos in segment.positions: | ||||
|             pos = mul_vec(pos, scale) | ||||
|  | ||||
| def get_model_world_matrix(model: Model, models: List[Model]) -> Matrix: | ||||
|     """ Gets a Blender Matrix for transforming the model into world space. """ | ||||
|  | ||||
|     transform_stack: List[ModelTransform] = [model.transform] | ||||
|     transform_stack.extend((parent.transform for parent in get_model_ancestors(model, models))) | ||||
|  | ||||
|     world_matrix: Matrix = Matrix() | ||||
|  | ||||
|     for transform in reversed(transform_stack): | ||||
|         translation_matrix = Matrix.Translation(transform.translation) | ||||
|         rotation_matrix = transform.rotation.to_matrix().to_4x4() | ||||
|  | ||||
|         world_matrix = world_matrix @ (translation_matrix @ rotation_matrix) | ||||
|  | ||||
|     return world_matrix | ||||
|  | ||||
| def sort_by_parent(models: List[Model]) -> List[Model]: | ||||
|     """ Sorts a Model list so that models are ordered by their parent. | ||||
|         Required for some tools to be able to load .msh files. """ | ||||
|  | ||||
|     sorted_models: List[Model] = [] | ||||
|  | ||||
|     for root in get_root_models(models): | ||||
|         def add_children(model: Model): | ||||
|             nonlocal sorted_models | ||||
|  | ||||
|             for child in get_model_children(model, models): | ||||
|                 sorted_models.append(child) | ||||
|                 add_children(child) | ||||
|  | ||||
|         sorted_models.append(root) | ||||
|         add_children(root) | ||||
|  | ||||
|     return sorted_models | ||||
|  | ||||
| def reparent_model_roots(models: List[Model]) -> List[Model]: | ||||
|     """ Reparents all root models in a list to a new empty node. """ | ||||
|  | ||||
|     new_root: Model = Model() | ||||
|     new_root.name = get_unique_scene_root_name(models) | ||||
|  | ||||
|     for model in models: | ||||
|         if not model.parent: | ||||
|             model.parent = new_root.name | ||||
|  | ||||
|     models.insert(0, new_root) | ||||
|  | ||||
|     return models | ||||
|  | ||||
| def has_multiple_root_models(models: List[Model]) -> bool: | ||||
|     """ Checks if a list of Model objects has multiple roots. """ | ||||
|  | ||||
|     return sum(1 for root in get_root_models(models)) > 1 | ||||
|  | ||||
| def get_root_models(models: List[Model]) -> Model: | ||||
|     """ Generator. Returns all Model objects in a list with no parent. """ | ||||
|  | ||||
|     for model in models: | ||||
|         if model.parent == "": | ||||
|             yield model | ||||
|  | ||||
| def get_model_children(parent: Model, models: List[Model]) -> Model: | ||||
|     """ Generator. Returns all Model objects in a list whose parent is | ||||
|         the supplied model. """ | ||||
|  | ||||
|     for model in models: | ||||
|         if parent.name == model.parent: | ||||
|             yield model | ||||
|  | ||||
| def get_model_ancestors(child: Model, models: List[Model]) -> Model: | ||||
|     """ Generator. Yields the parent for a model, then yields the parent's parent, | ||||
|         repeating until at the root model. """ | ||||
|  | ||||
|     for model in models: | ||||
|         if child.parent == model.name: | ||||
|             child = model | ||||
|             yield model | ||||
|  | ||||
| def get_unique_scene_root_name(models: List[Model]) -> Model: | ||||
|     """ Returns a unique model name of the form of either "SceneRoot" or | ||||
|         "SceneRoot{i}". """ | ||||
|  | ||||
|     name: str = "SceneRoot" | ||||
|  | ||||
|     if is_model_name_unused(name, models): | ||||
|         return name | ||||
|  | ||||
|     for i in range(len(models) + 1): | ||||
|         name = f"SceneRoot{i}" | ||||
|  | ||||
|         if is_model_name_unused(name, models): | ||||
|             return name | ||||
|  | ||||
|     return name | ||||
|  | ||||
| def is_model_name_unused(name: str, models: List[Model]) -> bool: | ||||
|     """ Checks if there is no Model using a name in a list of models.  """ | ||||
|  | ||||
|     for model in models: | ||||
|         if model.name == name: | ||||
|             return False | ||||
|  | ||||
|     return True | ||||
							
								
								
									
										81
									
								
								addons/io_scene_swbf_msh/msh_scene.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								addons/io_scene_swbf_msh/msh_scene.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| """ Contains Scene object for representing a .msh file and the function to create one | ||||
|     from a Blender scene.  """ | ||||
|  | ||||
| from dataclasses import dataclass, field | ||||
| from typing import List | ||||
| from copy import copy | ||||
| import bpy | ||||
| from mathutils import Vector | ||||
| from .msh_model import Model | ||||
| from .msh_model_gather import gather_models | ||||
| from .msh_model_utilities import sort_by_parent, has_multiple_root_models, reparent_model_roots, get_model_world_matrix | ||||
| from .msh_model_triangle_strips import create_models_triangle_strips | ||||
| from .msh_utilities import * | ||||
|  | ||||
| @dataclass | ||||
| class SceneAABB: | ||||
|     """ Class representing an axis-aligned bounding box. """ | ||||
|  | ||||
|     AABB_INIT_MAX = -3.402823466e+38 | ||||
|     AABB_INIT_MIN = 3.402823466e+38 | ||||
|  | ||||
|     max_: Vector = Vector((AABB_INIT_MAX, AABB_INIT_MAX, AABB_INIT_MAX)) | ||||
|     min_: Vector = Vector((AABB_INIT_MIN, AABB_INIT_MIN, AABB_INIT_MIN)) | ||||
|  | ||||
|     def integrate_aabb(self, other): | ||||
|         """ Merge another AABB with this AABB. """ | ||||
|  | ||||
|         self.max_ = max_vec(self.max_, other.max_) | ||||
|         self.min_ = min_vec(self.min_, other.min_) | ||||
|  | ||||
|     def integrate_position(self, position): | ||||
|         """ Integrate a position with the AABB, potentially expanding it. """ | ||||
|  | ||||
|         self.max_ = max_vec(self.max_, position) | ||||
|         self.min_ = min_vec(self.min_, position) | ||||
|  | ||||
| @dataclass | ||||
| class Scene: | ||||
|     """ Class containing the scene data for a .msh """ | ||||
|     name: str = "Scene" | ||||
|     models: List[Model] = field(default_factory=list) | ||||
|  | ||||
| def create_scene() -> Scene: | ||||
|     """ Create a msh Scene from the active Blender scene. """ | ||||
|  | ||||
|     scene = Scene() | ||||
|  | ||||
|     scene.name = bpy.context.scene.name | ||||
|  | ||||
|     scene.models = gather_models() | ||||
|     scene.models = sort_by_parent(scene.models) | ||||
|     scene.models = create_models_triangle_strips(scene.models) | ||||
|  | ||||
|     if has_multiple_root_models(scene.models): | ||||
|         scene.models = reparent_model_roots(scene.models) | ||||
|  | ||||
|     return scene | ||||
|  | ||||
| def create_scene_aabb(scene: Scene) -> SceneAABB: | ||||
|     """ Create a SceneAABB for a Scene. """ | ||||
|  | ||||
|     global_aabb = SceneAABB() | ||||
|  | ||||
|     for model in scene.models: | ||||
|         if model.geometry is None: | ||||
|             continue | ||||
|  | ||||
|         model_world_matrix = get_model_world_matrix(model, scene.models) | ||||
|         model_aabb = SceneAABB() | ||||
|  | ||||
|         for segment in model.geometry: | ||||
|             segment_aabb = SceneAABB() | ||||
|  | ||||
|             for pos in segment.positions: | ||||
|                 segment_aabb.integrate_position(model_world_matrix @ pos) | ||||
|  | ||||
|             model_aabb.integrate_aabb(segment_aabb) | ||||
|  | ||||
|         global_aabb.integrate_aabb(model_aabb) | ||||
|  | ||||
|     return global_aabb | ||||
							
								
								
									
										156
									
								
								addons/io_scene_swbf_msh/msh_scene_save.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								addons/io_scene_swbf_msh/msh_scene_save.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,156 @@ | ||||
| """ Contains functions for saving a Scene to a .msh file.  """ | ||||
|  | ||||
| from itertools import islice | ||||
| from .msh_scene import Scene, create_scene_aabb | ||||
| from .msh_model import * | ||||
| from .msh_writer import Writer | ||||
| from .msh_utilities import * | ||||
|  | ||||
| def save_scene(output_file, scene: Scene): | ||||
|     """ Saves scene to the supplied file. """ | ||||
|  | ||||
|     with Writer(file=output_file, chunk_id="HEDR") as hedr: | ||||
|         with hedr.create_child("MSH2") as msh2: | ||||
|  | ||||
|             with msh2.create_child("SINF") as sinf: | ||||
|                 _write_sinf(sinf, scene) | ||||
|  | ||||
|             with msh2.create_child("MATL") as matl: | ||||
|                 _write_matl(matl, scene) | ||||
|  | ||||
|             for index, model in enumerate(scene.models): | ||||
|                 with msh2.create_child("MODL") as modl: | ||||
|                     _write_modl(modl, model, index) | ||||
|  | ||||
|         with hedr.create_child("CL1L"): | ||||
|             pass | ||||
|  | ||||
| def _write_sinf(sinf: Writer, scene: Scene): | ||||
|     with sinf.create_child("NAME") as name: | ||||
|         name.write_string(scene.name) | ||||
|  | ||||
|     with sinf.create_child("FRAM") as fram: | ||||
|         fram.write_i32(0, 1) | ||||
|         fram.write_f32(29.97003) | ||||
|  | ||||
|     with sinf.create_child("BBOX") as bbox: | ||||
|         aabb = create_scene_aabb(scene) | ||||
|  | ||||
|         bbox_position = div_vec(add_vec(aabb.min_, aabb.max_), Vector((2.0, 2.0, 2.0))) | ||||
|         bbox_size = div_vec(sub_vec(aabb.max_, aabb.min_), Vector((2.0, 2.0, 2.0))) | ||||
|         bbox_length = bbox_size.length | ||||
|  | ||||
|         bbox.write_f32(0.0, 0.0, 0.0, 1.0) | ||||
|         bbox.write_f32(bbox_position.x, bbox_position.y, bbox_position.z) | ||||
|         bbox.write_f32(bbox_size.x, bbox_size.y, bbox_size.z, bbox_length) | ||||
|  | ||||
| def _write_matl(matl: Writer, scene: Scene): | ||||
|     # TODO: Material support. | ||||
|  | ||||
|     matl.write_u32(1) # Material count. | ||||
|  | ||||
|     with matl.create_child("MATD") as matd: | ||||
|         with matd.create_child("NAME") as name: | ||||
|             name.write_string(f"{scene.name}Material") # TODO: Proper name with material support. | ||||
|  | ||||
|         with matd.create_child("DATA") as data: | ||||
|             data.write_f32(1.0, 1.0, 1.0, 1.0) # Diffuse Color (Seams to get ignored by modelmunge) | ||||
|             data.write_f32(1.0, 1.0, 1.0, 1.0) # Specular Color | ||||
|             data.write_f32(0.0, 0.0, 0.0, 1.0) # Ambient Color (Seams to get ignored by modelmunge and Zero(?)) | ||||
|             data.write_f32(50.0) # Specular Exponent/Decay (Gets ignored by RedEngine in SWBFII for all known materials) | ||||
|  | ||||
|         with matd.create_child("ATRB") as atrb: | ||||
|             atrb.write_u8(0) # Material Flags | ||||
|             atrb.write_u8(0) # Rendertype | ||||
|             atrb.write_u8(0, 0) # Rendertype Params (Scroll rate, animation divisors, etc) | ||||
|  | ||||
|         with matd.create_child("TX0D") as tx0d: | ||||
|             tx0d.write_string("null_detailmap.tga") | ||||
|  | ||||
| def _write_modl(modl: Writer, model: Model, index: int): | ||||
|     with modl.create_child("MTYP") as mtyp: | ||||
|         mtyp.write_u32(model.model_type.value) | ||||
|  | ||||
|     with modl.create_child("MNDX") as mndx: | ||||
|         mndx.write_u32(index) | ||||
|  | ||||
|     with modl.create_child("NAME") as name: | ||||
|         name.write_string(model.name) | ||||
|  | ||||
|     if model.parent: | ||||
|         with modl.create_child("PRNT") as prnt: | ||||
|             prnt.write_string(model.parent) | ||||
|  | ||||
|     if model.hidden: | ||||
|         with modl.create_child("FLGS") as flgs: | ||||
|             flgs.write_u32(1) | ||||
|  | ||||
|     with modl.create_child("TRAN") as tran: | ||||
|         _write_tran(tran, model.transform) | ||||
|  | ||||
|     if model.geometry is not None: | ||||
|         with modl.create_child("GEOM") as geom: | ||||
|             for segment in model.geometry: | ||||
|                 with geom.create_child("SEGM") as segm: | ||||
|                     _write_segm(segm, segment) | ||||
|  | ||||
|     # TODO: Collision Primitive | ||||
|  | ||||
| def _write_tran(tran: Writer, transform: ModelTransform): | ||||
|     tran.write_f32(1.0, 1.0, 1.0) # Scale, ignored by modelmunge | ||||
|     tran.write_f32(transform.rotation.x, transform.rotation.y, transform.rotation.z, transform.rotation.w) | ||||
|     tran.write_f32(transform.translation.x, transform.translation.y, transform.translation.z) | ||||
|  | ||||
| def _write_segm(segm: Writer, segment: GeometrySegment): | ||||
|  | ||||
|     with segm.create_child("MATI") as mati: | ||||
|         mati.write_u32(0) | ||||
|  | ||||
|     with segm.create_child("POSL") as posl: | ||||
|         posl.write_u32(len(segment.positions)) | ||||
|  | ||||
|         for position in segment.positions: | ||||
|             posl.write_f32(position.x, position.y, position.z) | ||||
|  | ||||
|     with segm.create_child("NRML") as nrml: | ||||
|         nrml.write_u32(len(segment.normals)) | ||||
|  | ||||
|         for normal in segment.normals: | ||||
|             nrml.write_f32(normal.x, normal.y, normal.z) | ||||
|  | ||||
|     if segment.colors is not None: | ||||
|         with segm.create_child("CLRL") as clrl: | ||||
|             clrl.write_u32(len(segment.colors)) | ||||
|  | ||||
|             for color in segment.colors: | ||||
|                 clrl.write_u32(pack_color(color)) | ||||
|  | ||||
|     with segm.create_child("UV0L") as uv0l: | ||||
|         uv0l.write_u32(len(segment.texcoords)) | ||||
|  | ||||
|         for texcoord in segment.texcoords: | ||||
|             uv0l.write_f32(texcoord.x, texcoord.y) | ||||
|  | ||||
|     with segm.create_child("NDXL") as ndxl: | ||||
|         ndxl.write_u32(len(segment.polygons)) | ||||
|  | ||||
|         for polygon in segment.polygons: | ||||
|             ndxl.write_u16(len(polygon)) | ||||
|  | ||||
|             for index in polygon: | ||||
|                 ndxl.write_u16(index) | ||||
|  | ||||
|     with segm.create_child("NDXT") as ndxt: | ||||
|         ndxt.write_u32(len(segment.triangles)) | ||||
|  | ||||
|         for triangle in segment.triangles: | ||||
|             ndxt.write_u16(triangle[0], triangle[1], triangle[2]) | ||||
|  | ||||
|     with segm.create_child("STRP") as strp: | ||||
|         strp.write_u32(sum(len(strip) for strip in segment.triangle_strips)) | ||||
|  | ||||
|         for strip in segment.triangle_strips: | ||||
|             strp.write_u16(strip[0] | 0x8000, strip[1] | 0x8000) | ||||
|  | ||||
|             for index in islice(strip, 2, len(strip)): | ||||
|                 strp.write_u16(index) | ||||
							
								
								
									
										31
									
								
								addons/io_scene_swbf_msh/msh_utilities.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								addons/io_scene_swbf_msh/msh_utilities.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| """ Misc utilities. """ | ||||
|  | ||||
| from mathutils import Vector | ||||
|  | ||||
| def add_vec(l: Vector, r: Vector) -> Vector: | ||||
|     return Vector(v0 + v1 for v0, v1 in zip(l, r)) | ||||
|  | ||||
| def sub_vec(l: Vector, r: Vector) -> Vector: | ||||
|     return Vector(v0 - v1 for v0, v1 in zip(l, r)) | ||||
|  | ||||
| def mul_vec(l: Vector, r: Vector) -> Vector: | ||||
|     return Vector(v0 * v1 for v0, v1 in zip(l, r)) | ||||
|  | ||||
| def div_vec(l: Vector, r: Vector) -> Vector: | ||||
|     return Vector(v0 / v1 for v0, v1 in zip(l, r)) | ||||
|  | ||||
| def max_vec(l: Vector, r: Vector) -> Vector: | ||||
|     return Vector(max(v0, v1) for v0, v1 in zip(l, r)) | ||||
|  | ||||
| def min_vec(l: Vector, r: Vector) -> Vector: | ||||
|     return Vector(min(v0, v1) for v0, v1 in zip(l, r)) | ||||
|  | ||||
| def pack_color(color) -> int: | ||||
|     packed = 0 | ||||
|  | ||||
|     packed |= (int(color[0] * 255.0 + 0.5) << 8) | ||||
|     packed |= (int(color[1] * 255.0 + 0.5) << 16) | ||||
|     packed |= (int(color[2] * 255.0 + 0.5)) | ||||
|     packed |= (int(color[3] * 255.0 + 0.5) << 24) | ||||
|  | ||||
|     return packed | ||||
							
								
								
									
										71
									
								
								addons/io_scene_swbf_msh/msh_writer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								addons/io_scene_swbf_msh/msh_writer.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
|  | ||||
| import io | ||||
| import struct | ||||
|  | ||||
| class Writer: | ||||
|     def __init__(self, file, chunk_id: str, parent=None): | ||||
|         self.file = file | ||||
|         self.size: int = 0 | ||||
|         self.size_pos = None | ||||
|         self.parent = parent | ||||
|  | ||||
|         self.file.write(bytes(chunk_id[0:4], "ascii")) | ||||
|  | ||||
|     def __enter__(self): | ||||
|         self.size_pos = self.file.tell() | ||||
|         self.file.write(struct.pack(f"<I", 0)) | ||||
|  | ||||
|         return self | ||||
|  | ||||
|     def __exit__(self, exc_type, exc_value, traceback): | ||||
|         if self.size > self.MAX_SIZE: | ||||
|             raise OverflowError(f".msh file overflowed max size. size = {self.size} MAX_SIZE = {self.MAX_SIZE}") | ||||
|  | ||||
|         if (self.size % 4) > 0: | ||||
|             padding = 4 - (self.size % 4) | ||||
|             self.write_bytes(bytes([0 for i in range(padding)])) | ||||
|  | ||||
|         head_pos = self.file.tell() | ||||
|         self.file.seek(self.size_pos) | ||||
|         self.file.write(struct.pack(f"<I", self.size)) | ||||
|         self.file.seek(head_pos) | ||||
|  | ||||
|         if self.parent is not None: | ||||
|             self.parent.size += self.size | ||||
|  | ||||
|     def write_bytes(self, packed_bytes): | ||||
|         self.size += len(packed_bytes) | ||||
|         self.file.write(packed_bytes) | ||||
|  | ||||
|     def write_string(self, string: str): | ||||
|         self.write_bytes(bytes(string, "utf-8")) | ||||
|         self.write_bytes(b'\0') | ||||
|  | ||||
|     def write_i8(self, *ints): | ||||
|         self.write_bytes(struct.pack(f"<{len(ints)}b", *ints)) | ||||
|  | ||||
|     def write_u8(self, *ints): | ||||
|         self.write_bytes(struct.pack(f"<{len(ints)}B", *ints)) | ||||
|  | ||||
|     def write_i16(self, *ints): | ||||
|         self.write_bytes(struct.pack(f"<{len(ints)}h", *ints)) | ||||
|  | ||||
|     def write_u16(self, *ints): | ||||
|         self.write_bytes(struct.pack(f"<{len(ints)}H", *ints)) | ||||
|  | ||||
|     def write_i32(self, *ints): | ||||
|         self.write_bytes(struct.pack(f"<{len(ints)}i", *ints)) | ||||
|  | ||||
|     def write_u32(self, *ints): | ||||
|         self.write_bytes(struct.pack(f"<{len(ints)}I", *ints)) | ||||
|  | ||||
|     def write_f32(self, *floats): | ||||
|         self.write_bytes(struct.pack(f"<{len(floats)}f", *floats)) | ||||
|  | ||||
|     def create_child(self, child_id: str): | ||||
|         child = Writer(self.file, chunk_id=child_id, parent=self) | ||||
|         self.size += 8 | ||||
|  | ||||
|         return child | ||||
|  | ||||
|     MAX_SIZE: int = 2147483647 - 8 | ||||
		Reference in New Issue
	
	Block a user
	 SleepKiller
					SleepKiller