From 1ac1cc5ce1601078e77a2df59018294fdf6e6eca Mon Sep 17 00:00:00 2001 From: SleepKiller Date: Mon, 11 Nov 2019 23:03:52 +1300 Subject: [PATCH] basic working version --- .gitignore | 123 +++++++++++ .vscode/launch.json | 70 ++++++ .vscode/settings.json | 9 + addons/io_scene_swbf_msh/__init__.py | 126 +++++++++++ addons/io_scene_swbf_msh/msh_model.py | 67 ++++++ addons/io_scene_swbf_msh/msh_model_gather.py | 185 ++++++++++++++++ .../msh_model_triangle_strips.py | 81 +++++++ .../io_scene_swbf_msh/msh_model_utilities.py | 117 ++++++++++ addons/io_scene_swbf_msh/msh_scene.py | 81 +++++++ addons/io_scene_swbf_msh/msh_scene_save.py | 156 ++++++++++++++ addons/io_scene_swbf_msh/msh_utilities.py | 31 +++ addons/io_scene_swbf_msh/msh_writer.py | 71 ++++++ glTF-Blender-IO-license.txt | 202 ++++++++++++++++++ 13 files changed, 1319 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 addons/io_scene_swbf_msh/__init__.py create mode 100644 addons/io_scene_swbf_msh/msh_model.py create mode 100644 addons/io_scene_swbf_msh/msh_model_gather.py create mode 100644 addons/io_scene_swbf_msh/msh_model_triangle_strips.py create mode 100644 addons/io_scene_swbf_msh/msh_model_utilities.py create mode 100644 addons/io_scene_swbf_msh/msh_scene.py create mode 100644 addons/io_scene_swbf_msh/msh_scene_save.py create mode 100644 addons/io_scene_swbf_msh/msh_utilities.py create mode 100644 addons/io_scene_swbf_msh/msh_writer.py create mode 100644 glTF-Blender-IO-license.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29e4cdf --- /dev/null +++ b/.gitignore @@ -0,0 +1,123 @@ + +*.msh + +# Created by https://www.gitignore.io/api/python,visualstudiocode +# Edit at https://www.gitignore.io/?templates=python,visualstudiocode + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history + +# End of https://www.gitignore.io/api/python,visualstudiocode \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b69de2e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,70 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Current File (Integrated Terminal)", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + }, + { + "name": "Python: Remote Attach", + "type": "python", + "request": "attach", + "port": 5678, + "host": "localhost", + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", + "remoteRoot": "${workspaceFolder}" + } + ] + }, + { + "name": "Python: Module", + "type": "python", + "request": "launch", + "module": "enter-your-module-name-here", + "console": "integratedTerminal" + }, + { + "name": "Python: Django", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/manage.py", + "console": "integratedTerminal", + "args": [ + "runserver", + "--noreload", + "--nothreading" + ], + "django": true + }, + { + "name": "Python: Flask", + "type": "python", + "request": "launch", + "module": "flask", + "env": { + "FLASK_APP": "app.py" + }, + "args": [ + "run", + "--no-debugger", + "--no-reload" + ], + "jinja": true + }, + { + "name": "Python: Current File (External Terminal)", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "externalTerminal" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..67b355f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "python.autoComplete.extraPaths": [ + "C:/GitHub/blender_autocomplete/2.80" + ], + "python.linting.pylintArgs": [ + "--init-hook", + "import sys; sys.path.append('C:/GitHub/blender_autocomplete/2.80')" + ], +} \ No newline at end of file diff --git a/addons/io_scene_swbf_msh/__init__.py b/addons/io_scene_swbf_msh/__init__.py new file mode 100644 index 0000000..1425387 --- /dev/null +++ b/addons/io_scene_swbf_msh/__init__.py @@ -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') diff --git a/addons/io_scene_swbf_msh/msh_model.py b/addons/io_scene_swbf_msh/msh_model.py new file mode 100644 index 0000000..0dc75ee --- /dev/null +++ b/addons/io_scene_swbf_msh/msh_model.py @@ -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 diff --git a/addons/io_scene_swbf_msh/msh_model_gather.py b/addons/io_scene_swbf_msh/msh_model_gather.py new file mode 100644 index 0000000..5e4cd5d --- /dev/null +++ b/addons/io_scene_swbf_msh/msh_model_gather.py @@ -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 diff --git a/addons/io_scene_swbf_msh/msh_model_triangle_strips.py b/addons/io_scene_swbf_msh/msh_model_triangle_strips.py new file mode 100644 index 0000000..1c2dfef --- /dev/null +++ b/addons/io_scene_swbf_msh/msh_model_triangle_strips.py @@ -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] diff --git a/addons/io_scene_swbf_msh/msh_model_utilities.py b/addons/io_scene_swbf_msh/msh_model_utilities.py new file mode 100644 index 0000000..1236b5e --- /dev/null +++ b/addons/io_scene_swbf_msh/msh_model_utilities.py @@ -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 diff --git a/addons/io_scene_swbf_msh/msh_scene.py b/addons/io_scene_swbf_msh/msh_scene.py new file mode 100644 index 0000000..f6160d3 --- /dev/null +++ b/addons/io_scene_swbf_msh/msh_scene.py @@ -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 diff --git a/addons/io_scene_swbf_msh/msh_scene_save.py b/addons/io_scene_swbf_msh/msh_scene_save.py new file mode 100644 index 0000000..09f5758 --- /dev/null +++ b/addons/io_scene_swbf_msh/msh_scene_save.py @@ -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) diff --git a/addons/io_scene_swbf_msh/msh_utilities.py b/addons/io_scene_swbf_msh/msh_utilities.py new file mode 100644 index 0000000..eb98571 --- /dev/null +++ b/addons/io_scene_swbf_msh/msh_utilities.py @@ -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 diff --git a/addons/io_scene_swbf_msh/msh_writer.py b/addons/io_scene_swbf_msh/msh_writer.py new file mode 100644 index 0000000..77d4a3d --- /dev/null +++ b/addons/io_scene_swbf_msh/msh_writer.py @@ -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" 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"