basic working version
This commit is contained in:
parent
42201d31a9
commit
1ac1cc5ce1
|
@ -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
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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')"
|
||||||
|
],
|
||||||
|
}
|
|
@ -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')
|
|
@ -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
|
|
@ -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
|
|
@ -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]
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,202 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
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.
|
Loading…
Reference in New Issue