basic working version

This commit is contained in:
SleepKiller
2019-11-11 23:03:52 +13:00
parent 42201d31a9
commit 1ac1cc5ce1
13 changed files with 1319 additions and 0 deletions

View 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')

View 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

View 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

View 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]

View 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

View 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

View 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)

View 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

View 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