diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..643f1af --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,28 @@ +name: Release + +on: + push: + branches: + - main + tags: + - v* + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Make release + if: startsWith(github.ref, 'refs/tags/') + run: | + mkdir io_scene_glacier + rsync -av --progress . ./io_scene_glacier --exclude '.*' + zip -r io_scene_glacier-${{github.ref_name}}.zip io_scene_glacier + - name: Release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + files: | + io_scene_glacier-${{github.ref_name}}.zip \ No newline at end of file diff --git a/README.md b/README.md index f7c0b7e..01e135d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,17 @@ -# io_scene_glacier +# Glacier 2 Blender addon +*View and create assets from the Glacier 2 game engine.* + +![lod_slider](https://user-images.githubusercontent.com/43296291/203970131-4080b2cb-c09e-49e4-b8a9-5aa9a9a61d50.gif) + +## Supported Titles and Features +The following games are supported by this addon: + + * Hitman 2016 + * Hitman 2 + * Hitman 3 + +The addon supports the following formats: -## Supported formats | Extension | Description | Can import | Can export | |---------------|-----------------------------------|:-----------------------------:|:-----------------------------:| | .prim | Standard RenderPrimitive | Yes | Yes | @@ -9,4 +20,25 @@ | .borg | AnimationBoneData | Yes | No | | .mjba | MorphemeJointBoneAnimationData | No | No | | .mrtr | MorphemeRuntimeRig | No | No | -| .vtxd | VertexData | Yes | No | \ No newline at end of file +| .vtxd | VertexData | Yes | No | + +*Support for more formats or titles may be added in the future* + +## Requirements + - Blender **3.0.0** or above + +## Installation + - Download the addon: **[Glacier 2 addon](https://github.com/glacier-modding/io_scene_glacier/archive/master.zip)** + - Install the addon in blender like so: + - go to *Edit > Preferences > Add-ons.* + - use the *Install…* button and use the File Browser to select the `.zip` + +## Credits + + * [PawREP](https://github.com/pawREP) + * For making the original `.prim` editing tool known as PrimIO that was used as a reference. + + + * [Khronos Group](https://github.com/KhronosGroup) + * For making glTF-Blender-IO that was used as a reference addon. + diff --git a/__init__.py b/__init__.py index 129ded3..3557559 100644 --- a/__init__.py +++ b/__init__.py @@ -1,44 +1,15 @@ -# ##### BEGIN LICENSE BLOCK ##### -# -# io_scene_glacier -# Copyright (c) 2020+, REDACTED -# All rights reserved. - -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: - -# * Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. - -# * Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. - -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -# ##### END LICENSE BLOCK ##### - bl_info = { "name": "Glacier 2 Engine Tools", "description": "Tools for the Glacier 2 Engine", - "version": (1, 0, 0), + "version": (0, 1, 0), "blender": (3, 0, 0), - "wiki_url": "", - "tracker_url": "", + "doc_url": "https://wiki.glaciermodding.org/blender", + "tracker_url": "https://github.com/glacier-modding/io_scene_glacier/issues", + "category": "Import-Export", } import bpy from . import file_prim -from . import file_vtxd from . import file_mjba from . import file_borg @@ -69,12 +40,6 @@ def show_lod_update(self, context): obj.hide_set(not should_show) - if all(self.show_lod): - print("All selected") - - if not any(self.show_lod): - print("None selected") - return None show_lod: BoolVectorProperty( @@ -84,8 +49,6 @@ def show_lod_update(self, context): size=8, subtype='LAYER', update=show_lod_update, - # get=None, - # set=None ) @@ -120,8 +83,7 @@ def draw(self, context): modules = [ file_prim, - file_vtxd, - file_mjba, + # file_mjba, # WIP module. enable at own risk file_borg ] diff --git a/file_borg/__init__.py b/file_borg/__init__.py index cb16207..aa7789e 100644 --- a/file_borg/__init__.py +++ b/file_borg/__init__.py @@ -1,31 +1,3 @@ -# ##### BEGIN LICENSE BLOCK ##### -# -# io_scene_glacier -# Copyright (c) 2020+, REDACTED -# All rights reserved. - -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: - -# * Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. - -# * Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. - -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -# ##### END LICENSE BLOCK ##### import bpy # ------------------------------------------------------------------------ @@ -33,7 +5,6 @@ # ------------------------------------------------------------------------ classes = [] - modules = [] diff --git a/file_borg/bl_import_borg.py b/file_borg/bl_import_borg.py index 77dd528..43769c2 100644 --- a/file_borg/bl_import_borg.py +++ b/file_borg/bl_import_borg.py @@ -4,24 +4,17 @@ import math from mathutils import Vector, Quaternion, Matrix -import numpy as np - -from bpy.props import (BoolProperty, - FloatProperty, - StringProperty, - EnumProperty, - ) - -from bpy_extras.io_utils import (ImportHelper, - ExportHelper, - unpack_list, - unpack_face_list, - axis_conversion, - ) from . import format from .. import io_binary +""" +Most functions are taken from the glTF blender addon and have been adapted to fit this add-on. +This code was written in accordance with the Apache 2.0 license used by the glTF add-on. +glTF-Blender-IO license: https://github.com/KhronosGroup/glTF-Blender-IO/blob/master/LICENSE.txt +""" + + class Bone: def __init__(self): @@ -39,7 +32,6 @@ def __init__(self): self.rotation_before = Quaternion((1, 0, 0, 0)) def trs(self): - # (final TRS) = (rotation after) (base TRS) (rotation before) t, r, s = self.base_trs m = scale_rot_swap_matrix(self.rotation_before) return ( @@ -48,7 +40,8 @@ def trs(self): m @ s, ) -def nearby_signed_perm_matrix(rot): #found in gltf_blender addon + +def nearby_signed_perm_matrix(rot: Quaternion): """Returns a signed permutation matrix close to rot.to_matrix(). (A signed permutation matrix is like a permutation matrix, except the non-zero entries can be ±1.) @@ -78,11 +71,9 @@ def nearby_signed_perm_matrix(rot): #found in gltf_blender addon return m -def scale_rot_swap_matrix(rot): #found in gltf_blender addon - """Returns a matrix m st. Scale[s] Rot[rot] = Rot[rot] Scale[m s]. - If rot.to_matrix() is a signed permutation matrix, works for any s. - Otherwise works only if s is a uniform scaling. - """ + +def scale_rot_swap_matrix(rot: Quaternion): + m = nearby_signed_perm_matrix(rot) # snap to signed perm matrix m.transpose() # invert permutation for i in range(3): @@ -90,8 +81,9 @@ def scale_rot_swap_matrix(rot): #found in gltf_blender addon m[i][j] = abs(m[i][j]) # discard sign return m -def pick_bone_length(bones, bone_id): - """Heuristic for bone length.""" + +def pick_bone_length(bones: [], bone_id: int): + """Return the bone length by finding the smallest child length""" bone = bones[bone_id] child_locs = [ @@ -104,10 +96,10 @@ def pick_bone_length(bones, bone_id): return bones[bone.parent].bone_length -def pick_bone_rotation(bones, bone_id, parent_rot): + +def pick_bone_rotation(bones: [], bone_id: int, parent_rot: Quaternion): bone = bones[bone_id] - # Try to put our tip at the centroid of our children child_locs = [ bones[child].editbone_trans for child in bone.children @@ -121,13 +113,9 @@ def pick_bone_rotation(bones, bone_id, parent_rot): return parent_rot -def local_rotation(bones, bone_id, rot): - """Appends a local rotation to bone's world transform: - (new world transform) = (old world transform) @ (rot) - without changing the world transform of bone's children. - For correctness, rot must be a signed permutation of the axes - """ +def local_rotation(bones: [], bone_id: int, rot: Quaternion): + bones[bone_id].rotation_before @= rot # Append the inverse rotation after children's TRS to cancel it out. @@ -136,7 +124,8 @@ def local_rotation(bones, bone_id, rot): bones[child].rotation_after = \ rot_inv @ bones[child].rotation_after -def rotate_edit_bone(bones, bone_id, rot): + +def rotate_edit_bone(bones: [], bone_id: int, rot: Quaternion): """Rotate one edit bone without affecting anything else.""" bones[bone_id].editbone_rot @= rot # Cancel out the rotation so children aren't affected. @@ -145,26 +134,24 @@ def rotate_edit_bone(bones, bone_id, rot): child = bones[child_id] child.editbone_trans = rot_inv @ child.editbone_trans child.editbone_rot = rot_inv @ child.editbone_rot - # Need to rotate the bone's final TRS by the same amount so skinning - # isn't affected. local_rotation(bones, bone_id, rot) -def prettify_bones(bones): +def prettify_bones(bones: []): """ Prettify bone lengths/directions. """ - def visit(bone_id, parent_rot=None): # Depth-first walk + def visit(bone_id: int, parent_rot: Quaternion = None): # Depth-first walk bone = bones[bone_id] rot = None bone.bone_length = pick_bone_length(bones, bone_id) - #clamp the bonelength if necessary + # clamp the bonelength if necessary if bone.bone_length < 0.0001: bone.bone_length = 0.001 - + rot = pick_bone_rotation(bones, bone_id, parent_rot) if rot is not None: rotate_edit_bone(bones, bone_id, rot) @@ -173,13 +160,14 @@ def visit(bone_id, parent_rot=None): # Depth-first walk visit(0) -def calc_bone_matrices(bones): + +def calc_bone_matrices(bones: []): """ Calculate the transformations from bone space to arma space in the bind pose and in the edit bone pose. """ - def visit(bone_id): # Depth-first walk + def visit(bone_id: int): # Depth-first walk bone = bones[bone_id] parent_bind_mat = Matrix.Identity(4) @@ -202,6 +190,7 @@ def visit(bone_id): # Depth-first walk visit(0) + def compute_bones(borg): bones = {} init_bones(borg, bones) @@ -209,19 +198,21 @@ def compute_bones(borg): calc_bone_matrices(bones) return bones + def get_bone_trs(svq): t = Vector([svq.position[0], -svq.position[2], svq.position[1]]) r = Quaternion([svq.rotation[3], -svq.rotation[0], svq.rotation[2], -svq.rotation[1]]) s = Vector([1, 1, 1]) return t, r, s + def init_bones(borg, bones): for i, bone in enumerate(borg.bone_definitions): bl_bone = Bone() bones[i] = bl_bone bl_bone.name = bone.name bl_bone.base_trs = get_bone_trs(borg.bind_poses[i]) - if i == 0: # if root + if i == 0: # if root we rotate the bone. This will result in a rotation of the entire rig rot = mathutils.Euler((0.0, 0.0, 0.0), 'XYZ') rot.rotate_axis('X', math.radians(-90.0)) bl_bone.base_trs[1].rotate(rot) @@ -241,7 +232,6 @@ def load_borg(operator, context, filepath): fp = os.fsencode(filepath) file = open(fp, "rb") br = io_binary.BinaryReader(file) - rig = [] borg_name = bpy.path.display_name_from_filepath(filepath) @@ -251,7 +241,7 @@ def load_borg(operator, context, filepath): bones = compute_bones(borg) - #for constr in borg.bone_constraints.bone_constraints: + # for constr in borg.bone_constraints.bone_constraints: # print(borg.bone_definitions[constr.bone_index].name) # print(print(', '.join("%s: %s" % item for item in vars(constr).items()))) # print() @@ -261,7 +251,8 @@ def load_borg(operator, context, filepath): armature = blender_arma.data bone_ids = [] - def visit(id): # Depth-first walk + + def visit(id): bone_ids.append(id) for child in bones[id].children: @@ -269,8 +260,7 @@ def visit(id): # Depth-first walk visit(0) - # Switch into edit mode to create all edit bones - + # Switch to edit mode to create all edit bones if bpy.context.mode != 'OBJECT': bpy.ops.object.mode_set(mode='OBJECT') bpy.context.view_layer.objects.active = blender_arma @@ -300,14 +290,13 @@ def visit(id): # Depth-first walk parent_editbone = armature.edit_bones[parent_bone.bl_bone_name] editbone.parent = parent_editbone - # Switch back to object mode and do pose bones + # Switch back to object mode and apply pose bones bpy.ops.object.mode_set(mode="OBJECT") for id in bone_ids: bone = bones[id] pose_bone = blender_arma.pose.bones[bone.bl_bone_name] - # BoneTRS = EditBone * PoseBone t, r, s = bone.trs() et, er = bone.editbone_trans, bone.editbone_rot pose_bone.location = er.conjugated() @ (t - et) diff --git a/file_borg/format.py b/file_borg/format.py index 76ad0a9..cc95e98 100644 --- a/file_borg/format.py +++ b/file_borg/format.py @@ -267,9 +267,10 @@ def __init__(self): self.pose_bones = [] self.pose_bone_indices = [] self.pose_entry_index = [] - self.pose_bone_counts = [] + self.pose_bone_count_array = [] self.names_list = [] self.face_bone_indices = [] + self.bone_constraints = [] def read(self, br): br.seek(br.readUInt64()) @@ -323,7 +324,7 @@ def read(self, br): self.pose_entry_index = br.readUIntVec(pose_bone_header.pose_count) br.seek(pose_bone_header.pose_bone_count_array_offset) - self.pose_bone_count = br.readUIntVec(pose_bone_header.pose_count) + self.pose_bone_count_array = br.readUIntVec(pose_bone_header.pose_count) # read names names_entry_index_array = [] @@ -357,7 +358,7 @@ def write(self, br): br.align(16) pose_bone_header.pose_bone_count_array_offset = br.tell() - br.writeUIntVec(self.pose_bone_count) + br.writeUIntVec(self.pose_bone_count_array) br.align(16) pose_bone_header.names_list_offset = br.tell() diff --git a/file_mjba/__init__.py b/file_mjba/__init__.py index 184981a..07e4f71 100644 --- a/file_mjba/__init__.py +++ b/file_mjba/__init__.py @@ -1,41 +1,3 @@ -# ##### BEGIN LICENSE BLOCK ##### -# -# io_scene_glacier -# Copyright (c) 2020+, REDACTED -# All rights reserved. - -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: - -# * Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. - -# * Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. - -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -# ##### END LICENSE BLOCK ##### - -bl_info = { - "name": "Glacier 2 Engine Tools", - "description": "Tools for the Glacier 2 Engine", - "version": (1, 0, 0), - "blender": (3, 0, 0), - "wiki_url": "", - "tracker_url": "", -} - import bpy from .GlacierEngine import GlacierEngine from .. import BlenderUI diff --git a/file_prim/__init__.py b/file_prim/__init__.py index d4f87b6..a93bbbd 100644 --- a/file_prim/__init__.py +++ b/file_prim/__init__.py @@ -2,6 +2,7 @@ import os from . import bl_utils_prim +from .. import BlenderUI from bpy_extras.io_utils import ( ImportHelper, @@ -11,8 +12,12 @@ from bpy.props import (StringProperty, BoolProperty, BoolVectorProperty, + CollectionProperty, PointerProperty, - IntProperty + IntProperty, + EnumProperty, + FloatVectorProperty, + IntVectorProperty ) from bpy.types import (Panel, @@ -32,10 +37,9 @@ class ImportPRIM(bpy.types.Operator, ImportHelper): options={'HIDDEN'}, ) - filepath: StringProperty( - name="PRIM", - description="Set path for the PRIM file", - subtype="FILE_PATH" + files: CollectionProperty( + name="File Path", + type=bpy.types.OperatorFileListElement, ) use_rig: BoolProperty( @@ -44,7 +48,8 @@ class ImportPRIM(bpy.types.Operator, ImportHelper): ) rig_filepath: StringProperty( - name="BoneRig Path" + name="BoneRig Path", + description="Path to the BoneRig (BORG) file", ) use_aloc: BoolProperty( @@ -53,12 +58,13 @@ class ImportPRIM(bpy.types.Operator, ImportHelper): ) aloc_filepath: StringProperty( - name="Collision Path" + name="Collision Path", + description="Path to the Collision (ALOC) file", ) def draw(self, context): - if not ".prim" in self.filepath.lower(): + if ".prim" not in self.filepath.lower(): return layout = self.layout @@ -67,11 +73,13 @@ def draw(self, context): is_weighted_prim = bl_utils_prim.is_weighted(self.filepath) if not is_weighted_prim: layout.label(text="The selected prim does not support a rig", icon="ERROR") + elif len(self.files) - 1: + layout.label(text="Rigs are not supported when batch importing") else: row = layout.row(align=True) row.prop(self, "use_rig") row = layout.row(align=True) - row.enabled = self.use_rig is not False + row.enabled = self.use_rig row.prop(self, "rig_filepath") if self.use_rig: @@ -81,44 +89,47 @@ def draw(self, context): except IOError: layout.label(text="Given filepath not valid", icon="ERROR") finally: - if f: f.close() + if f: + f.close() layout.row(align=True) - # row.prop(self, "use_aloc") - # row = layout.row(align=True) - # row.enabled = self.use_aloc is not False - # row.prop(self, "aloc_filepath") - def execute(self, context): from . import bl_import_prim - collection = bpy.data.collections.new(bpy.path.display_name_from_filepath(self.filepath)) + prim_paths = ["%s\\%s" % (os.path.dirname(self.filepath), meshPaths.name) for meshPaths in self.files] + for prim_path in prim_paths: + + collection = bpy.data.collections.new(bpy.path.display_name_from_filepath(prim_path)) + + self.rig_filepath = self.rig_filepath.replace(os.sep, '/') - self.rig_filepath = self.rig_filepath.replace(os.sep, '/') + arma_obj = None + if self.use_rig: + from ..file_borg import bl_import_borg + armature = bl_import_borg.load_borg(self, context, self.rig_filepath) + arma_obj = bpy.data.objects.new(armature.name, armature) + collection.objects.link(arma_obj) + + meshes = bl_import_prim.load_prim(self, context, collection, prim_path, self.use_rig, self.rig_filepath) - arma_obj = None - if self.use_rig: - from ..file_borg import bl_import_borg - armature = bl_import_borg.load_borg(self, context, self.rig_filepath) - arma_obj = bpy.data.objects.new(armature.name, armature) - collection.objects.link(arma_obj) + if not meshes: + BlenderUI.MessageBox("Failed to import \"%s\"" % prim_path, "Importing error", 'ERROR') + return {'CANCELLED'} - meshes = bl_import_prim.load_prim(self, context, self.filepath, self.use_rig, self.rig_filepath) + for mesh in meshes: + obj = bpy.data.objects.new(mesh.name, mesh) + if self.use_rig and arma_obj: + obj.modifiers.new(name='Glacier Bonerig', type='ARMATURE') + obj.modifiers['Glacier Bonerig'].object = arma_obj - if not meshes: - return {'CANCELLED'} + obj.data.polygons.foreach_set('use_smooth', [True] * len(obj.data.polygons)) - for mesh in meshes: - obj = bpy.data.objects.new(mesh.name, mesh) - if self.use_rig and arma_obj: - obj.modifiers.new(name='Glacier Bonerig', type='ARMATURE') - obj.modifiers['Glacier Bonerig'].object = arma_obj - collection.objects.link(obj) + collection.objects.link(obj) - context.scene.collection.children.link(collection) - layer = bpy.context.view_layer - layer.update() + context.scene.collection.children.link(collection) + layer = bpy.context.view_layer + layer.update() return {'FINISHED'} @@ -131,25 +142,107 @@ class ExportPRIM(bpy.types.Operator, ExportHelper): filename_ext = '.prim' filter_glob: StringProperty(default='*.prim', options={'HIDDEN'}) - def draw(self, context): + def get_collections(self, context): + items = [(col.name, col.name, "") for col in bpy.data.collections] - if not ".prim" in self.filepath.lower(): + for i, coll_name in enumerate(items): + if coll_name[0] == bpy.context.collection.name: + items[0], items[i] = items[i], items[0] + + return items + + export_collection: EnumProperty( + name='', + description='The collection to turn into a prim', + items=get_collections, + default=None, + ) + + def draw(self, context): + if ".prim" not in self.filepath.lower(): return layout = self.layout layout.label(text="export options:") - + row = layout.row(align=True) + row.prop(self, "export_collection") def execute(self, context): from . import bl_export_prim keywords = self.as_keywords(ignore=( 'check_existing', - 'filter_glob' + 'filter_glob', + 'export_collection' )) - return bl_export_prim.save_prim(self, context, **keywords) + return bl_export_prim.save_prim(bpy.data.collections[self.export_collection], **keywords) + + +class PrimCollectionProperties(PropertyGroup): + + bone_rig_resource_index: IntProperty( + name="Bone Rig Resource Index", + description="", + default=-1, + min=-1, + max=1000, + step=1, + ) + + has_bones: BoolProperty( + name="Has Bones", + description="The prim has bones", + ) + + has_frames: BoolProperty( + name="Has Frames", + ) + + is_linked: BoolProperty( + name='Linked', + description='The prim is linked', + ) + + is_weighted: BoolProperty( + name='Weighted', + description='The prim is weighted', + ) + + +class GLACIER_PT_PrimCollectionPropertiesPanel(bpy.types.Panel): + bl_idname = 'GLACIER_PT_PrimCollectionPropertiesPanel' + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = 'collection' + bl_category = 'Glacier' + bl_label = 'Global Prim Properties' + + @classmethod + def poll(self, context): + return context.collection is not None + + def draw(self, context): + coll = context.collection + layout = self.layout + + layout.row(align=True).prop(coll.prim_collection_properties, "bone_rig_resource_index") + + layout.label(text="Flags:") + + row = layout.row(align=True) + row.prop(coll.prim_collection_properties, "has_bones") + row.prop(coll.prim_collection_properties, "has_frames") + row.enabled = False + + row = layout.row(align=True) + row.prop(coll.prim_collection_properties, "is_linked") + row.prop(coll.prim_collection_properties, "is_weighted") + row.enabled = False + + +class PrimProperties(PropertyGroup): + """"Stored exposed variables relevant to the RenderPrimitive files""" -class Prim_Properties(PropertyGroup): lod: BoolVectorProperty( name='lod_mask', description='Set which LOD levels should be shown', @@ -159,15 +252,93 @@ class Prim_Properties(PropertyGroup): ) material_id: IntProperty( - name='', + name='Material ID', description='Set the Material ID', default=0, min=0, max=255 ) + prim_type: EnumProperty( + name='Type', + description='The type of the prim', + items=[ + ("PrimType.Unknown", "Unknown", ""), + ("PrimType.ObjectHeader", "Object Header", "The header of an Object"), + ("PrimType.Mesh", "Mesh", ""), + ("PrimType.Decal", "Decal", ""), + ("PrimType.Sprites", "Sprite", ""), + ("PrimType.Shape", "Shape", ""), + ], + default="PrimType.Mesh", + ) + + prim_subtype: EnumProperty( + name='Sub-Type', + description='The type of the prim', + items=[ + ("PrimObjectSubtype.Standard", "Standard", ""), + ("PrimObjectSubtype.Linked", "Linked", ""), + ("PrimObjectSubtype.Weighted", "Weighted", ""), + ], + default="PrimObjectSubtype.Standard", + ) + + axis_lock: BoolVectorProperty( + name='', + description='Locks an axis', + size=3, + subtype='LAYER' + ) + + no_physics: BoolProperty( + name="No physics", + ) + + # properties found in PrimSubMesh + + variant_id: IntProperty( + name='Variant ID', + description='Set the Variant ID', + default=0, + min=0, + max=255 + ) + + z_bias: IntProperty( + name='Z Bias', + description='Set the Z Bias', + default=0, + min=0, + max=255 + ) + + z_offset: IntProperty( + name='Z Offset', + description='Set the Z Offset', + default=0, + min=0, + max=255 + ) + + use_mesh_color: BoolProperty( + name="Use Mesh Color" + ) + + mesh_color: FloatVectorProperty( + name="Mesh Color", + description="Applies a global color to the mesh. Will replace all vertex colors!", + subtype="COLOR", + size=4, + min=0.0, + max=1.0, + default=(1.0, 1.0, 1.0, 1.0) + ) + class GLACIER_PT_PrimPropertiesPanel(bpy.types.Panel): + """"Adds a panel to the object window to show the Prim_Properties""" + bl_idname = 'GLACIER_PT_PrimPropertiesPanel' bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' @@ -187,19 +358,66 @@ def draw(self, context): mesh = obj.data layout = self.layout + layout.label(text="Lod mask:") row = layout.row(align=True) for i, name in enumerate(["high", " ", " ", " ", " ", " ", " ", "low"]): row.prop(mesh.prim_properties, "lod", index=i, text=name, toggle=True) - layout.label(text="Material ID:") + layout.label(text="Lock Axis:") + row = layout.row(align=True) + for i, name in enumerate(["X", "Y", "Z"]): + row.prop(mesh.prim_properties, "axis_lock", index=i, text=name, toggle=True) + + layout.use_property_split = True + layout.row(align=True).label(text="") + row = layout.row(align=True) row.prop(mesh.prim_properties, "material_id") + row = layout.row(align=True) + row.prop(mesh.prim_properties, "no_physics") + + row = layout.row(align=True) + row.prop(mesh.prim_properties, "prim_type") + row.enabled = False + + row = layout.row() + row.prop(mesh.prim_properties, "prim_subtype") + row.enabled = False + + # properties for PrimSubMesh + row = layout.row() + row.prop(mesh.prim_properties, "variant_id") + + row = layout.row() + row.prop(mesh.prim_properties, "z_bias") + + row = layout.row() + row.prop(mesh.prim_properties, "z_offset") + + row = layout.row() + row.prop(mesh.prim_properties, "use_mesh_color") + + row = layout.row() + row.prop(mesh.prim_properties, "mesh_color") + row.enabled = mesh.prim_properties.use_mesh_color + + # TODO: add mesh buttons here + # This will act as a temporary way to edit cloth. at least until a in-blender editor is made. + # Button to export cloth data to json + # Button to import cloth data from json + + # TODO: add trigger collision stuff + # A mesh picker to select the collision mesh + # A button to generate a new collision mesh + classes = [ - Prim_Properties, + PrimProperties, + PrimCollectionProperties, GLACIER_PT_PrimPropertiesPanel, + GLACIER_PT_PrimCollectionPropertiesPanel, ImportPRIM, ExportPRIM ] @@ -210,7 +428,8 @@ def register(): bpy.utils.register_class(c) bpy.types.TOPBAR_MT_file_import.append(menu_func_import) bpy.types.TOPBAR_MT_file_export.append(menu_func_export) - bpy.types.Mesh.prim_properties = PointerProperty(type=Prim_Properties) + bpy.types.Mesh.prim_properties = PointerProperty(type=PrimProperties) + bpy.types.Collection.prim_collection_properties = PointerProperty(type=PrimCollectionProperties) def unregister(): @@ -219,6 +438,7 @@ def unregister(): bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) del bpy.types.Mesh.prim_properties + del bpy.types.Collection.prim_collection_properties def menu_func_import(self, context): diff --git a/file_prim/bl_export_prim.py b/file_prim/bl_export_prim.py index 1f3e3c0..253fa89 100644 --- a/file_prim/bl_export_prim.py +++ b/file_prim/bl_export_prim.py @@ -1,117 +1,25 @@ import os import bpy import bmesh -import mathutils as mu import numpy as np -from bpy.props import (BoolProperty, - FloatProperty, - StringProperty, - EnumProperty, - ) -from bpy_extras.io_utils import (ImportHelper, - ExportHelper, - unpack_list, - unpack_face_list, - axis_conversion, - ) from . import format from .. import io_binary - - -def __get_positions(mesh, matrix): - # read the vertex locations - locs = np.empty(len(mesh.vertices) * 3, dtype=np.float32) - source = mesh.vertices - for vert in source: - vert.co = matrix @ vert.co - - source.foreach_get('co', locs) - locs = locs.reshape(len(mesh.vertices), 3) - - return locs - - -def __get_normals(mesh): - """Get normal for each loop.""" - normals = np.empty(len(mesh.loops) * 3, dtype=np.float32) - mesh.calc_normals_split() - mesh.loops.foreach_get('normal', normals) - - normals = normals.reshape(len(mesh.loops), 3) - - for ns in normals: - for axis in range(3): - if int(round(ns[axis])) != 0: - ns[axis] = round(ns[axis]) - else: - ns[axis] = ns[axis] + (1 / 255) - - return normals - - -def __get_tangents(mesh): - """Get an array of the tangent for each loop.""" - tangents = np.empty(len(mesh.loops) * 3, dtype=np.float32) - mesh.loops.foreach_get('tangent', tangents) - tangents = tangents.reshape(len(mesh.loops), 3) - - for ts in tangents: - for axis in range(3): - if int(round(ts[axis])) != 0: - ts[axis] = round(ts[axis]) - else: - ts[axis] = ts[axis] + (1 / 255) - - return tangents - - -def __get_bitangents(mesh): - """Get an array of the tangent for each loop.""" - bitangents = np.empty(len(mesh.loops) * 3, dtype=np.float32) - mesh.loops.foreach_get('bitangent', bitangents) - bitangents = bitangents.reshape(len(mesh.loops), 3) - - for bs in bitangents: - for axis in range(3): - if int(round(bs[axis])) != 0: - bs[axis] = round(bs[axis]) - else: - bs[axis] = bs[axis] + (1 / 255) - - return bitangents - - -def __get_uvs(mesh, uv_i): - layer = mesh.uv_layers[uv_i] - uvs = np.empty(len(mesh.loops) * 2, dtype=np.float32) - layer.data.foreach_get('uv', uvs) - uvs = uvs.reshape(len(mesh.loops), 2) - - # u,v -> u,1-v - uvs[:, 1] *= -1 - uvs[:, 1] += 1 - - return uvs - - -def __get_colors(mesh, color_i): - colors = np.empty(len(mesh.loops) * 4, dtype=np.float32) - layer = mesh.vertex_colors[color_i] - mesh.color_attributes[layer.name].data.foreach_get('color', colors) - colors = colors.reshape(len(mesh.loops), 4) - # colors are already linear, no need to switch color space - return colors - - -def save_prim(operator, context, filepath): - # Export the selected mesh - scene = context.scene +from .. import BlenderUI + +def save_prim(collection, filepath: str): + """ + Export the selected collection to a prim + Writes to the given path. + Returns "FINISHED" when successful + """ prim = format.RenderPrimitve() + prim.header.bone_rig_resource_index = collection.prim_collection_properties.bone_rig_resource_index + prim.header.object_table = [] - mesh_obs = [o for o in bpy.context.collection.all_objects if o.type == 'MESH'] + mesh_obs = [o for o in collection.all_objects if o.type == 'MESH'] for ob in mesh_obs: prim_obj = format.PrimMesh() triangulate_object(ob) @@ -119,20 +27,48 @@ def save_prim(operator, context, filepath): material_id = ob.data.prim_properties.material_id prim_obj.prim_object.material_id = material_id + if ob.data.prim_properties.axis_lock[0]: + prim_obj.prim_object.properties.setXaxisLocked() + + if ob.data.prim_properties.axis_lock[1]: + prim_obj.prim_object.properties.setYaxisLocked() + + if ob.data.prim_properties.axis_lock[2]: + prim_obj.prim_object.properties.setZaxisLocked() + + if ob.data.prim_properties.no_physics: + prim_obj.prim_object.properties.setNoPhysics() + lod = bitArrToInt(ob.data.prim_properties.lod) prim_obj.prim_object.lodmask = lod - print("now lodmask", prim_obj.prim_object.lodmask, "is", lod) prim_obj.sub_mesh = save_prim_sub_mesh(ob) - if len(prim_obj.sub_mesh.vertexBuffer.vertices) > 10000: - prim_obj.prim_object.properties.setHighResulution() + + if prim_obj.sub_mesh is None: + return {'CANCELLED'} + # Set subMesh properties + if len(prim_obj.sub_mesh.vertexBuffer.vertices) > 100000: + prim_obj.prim_object.properties.setHighResolution() + + if ob.data.prim_properties.use_mesh_color: + prim_obj.sub_mesh.prim_object.properties.setColor1() + + prim_obj.sub_mesh.prim_object.variant_id = ob.data.prim_properties.variant_id + prim_obj.sub_mesh.prim_object.zbias = ob.data.prim_properties.z_bias + prim_obj.sub_mesh.prim_object.zoffset = ob.data.prim_properties.z_offset + if ob.data.prim_properties.use_mesh_color: + print("Set color to: ", [round(ob.data.prim_properties.mesh_color[0] * 255), round(ob.data.prim_properties.mesh_color[1] * 255), round(ob.data.prim_properties.mesh_color[2] * 255), round(ob.data.prim_properties.mesh_color[3] * 255)]) + prim_obj.sub_mesh.prim_object.color1[0] = round(ob.data.prim_properties.mesh_color[0] * 255) + prim_obj.sub_mesh.prim_object.color1[1] = round(ob.data.prim_properties.mesh_color[1] * 255) + prim_obj.sub_mesh.prim_object.color1[2] = round(ob.data.prim_properties.mesh_color[2] * 255) + prim_obj.sub_mesh.prim_object.color1[3] = round(ob.data.prim_properties.mesh_color[3] * 255) prim.header.object_table.append(prim_obj) - exportFile = os.fsencode(filepath) - if os.path.exists(exportFile): - os.remove(exportFile) - bre = io_binary.BinaryReader(open(exportFile, 'wb')) + export_file = os.fsencode(filepath) + if os.path.exists(export_file): + os.remove(export_file) + bre = io_binary.BinaryReader(open(export_file, 'wb')) prim.write(bre) bre.close() @@ -140,12 +76,20 @@ def save_prim(operator, context, filepath): def save_prim_sub_mesh(blender_obj): + """ + Export a blender mesh to a PrimSubMesh + Returns a PrimSubMesh + """ mesh = blender_obj.to_mesh() prim_mesh = format.PrimSubMesh() - mesh.calc_tangents(uvmap="UVMap") + if blender_obj.data.uv_layers: + mesh.calc_tangents(uvmap="UVMap") + else: + BlenderUI.MessageBox("\"%s\" is missing a UV map" % mesh.name, "Exporting error", 'ERROR') + return None - locs = __get_positions(mesh, blender_obj.matrix_world.copy()) + locs = get_positions(mesh, blender_obj.matrix_world.copy()) dot_fields = [('vertex_index', np.uint32)] dot_fields += [('nx', np.float32), ('ny', np.float32), ('nz', np.float32)] @@ -167,32 +111,32 @@ def save_prim_sub_mesh(blender_obj): dots['vertex_index'] = vidxs del vidxs - normals = __get_normals(mesh) + normals = get_normals(mesh) dots['nx'] = normals[:, 0] dots['ny'] = normals[:, 1] dots['nz'] = normals[:, 2] del normals - tangents = __get_tangents(mesh) + tangents = get_tangents(mesh) dots['tx'] = tangents[:, 0] dots['ty'] = tangents[:, 1] dots['tz'] = tangents[:, 2] del tangents - bitangents = __get_bitangents(mesh) + bitangents = get_bitangents(mesh) dots['bx'] = bitangents[:, 0] dots['by'] = bitangents[:, 1] dots['bz'] = bitangents[:, 2] del bitangents for uv_i in range(len(mesh.uv_layers)): - uvs = __get_uvs(mesh, uv_i) + uvs = get_uvs(mesh, uv_i) dots['uv%dx' % uv_i] = uvs[:, 0] dots['uv%dy' % uv_i] = uvs[:, 1] del uvs if len(mesh.vertex_colors) > 0: - colors = __get_colors(mesh, 0) + colors = get_colors(mesh, 0) dots['colorR'] = colors[:, 0] dots['colorG'] = colors[:, 1] dots['colorB'] = colors[:, 2] @@ -253,9 +197,8 @@ def save_prim_sub_mesh(blender_obj): colors[:, 1] = prim_dots['colorG'] colors[:, 2] = prim_dots['colorB'] colors[:, 3] = prim_dots['colorA'] - - i = 0 - for vertex in prim_mesh.vertexBuffer.vertices: + + for i, vertex in enumerate(prim_mesh.vertexBuffer.vertices): vertex = format.Vertex() vertex.position = positions[i] vertex.normal = normals[i] @@ -265,28 +208,110 @@ def save_prim_sub_mesh(blender_obj): vertex.uv[tex_coord_i] = uvs[tex_coord_i, i] vertex.color = (colors[i] * 255).astype("uint8").tolist() prim_mesh.vertexBuffer.vertices[i] = vertex - i = i + 1 return prim_mesh + +def get_positions(mesh, matrix): + # read the vertex locations + locs = np.empty(len(mesh.vertices) * 3, dtype=np.float32) + source = mesh.vertices + for vert in source: + vert.co = matrix @ vert.co + + source.foreach_get('co', locs) + locs = locs.reshape(len(mesh.vertices), 3) + + return locs + + +def get_normals(mesh): + """Get normal for each loop.""" + normals = np.empty(len(mesh.loops) * 3, dtype=np.float32) + mesh.calc_normals_split() + mesh.loops.foreach_get('normal', normals) + + normals = normals.reshape(len(mesh.loops), 3) + + for ns in normals: + for axis in range(3): + if int(round(ns[axis])) != 0: + ns[axis] = round(ns[axis]) + else: + ns[axis] = ns[axis] + (1 / 255) + + return normals + + +def get_tangents(mesh): + """Get an array of the tangent for each loop.""" + tangents = np.empty(len(mesh.loops) * 3, dtype=np.float32) + mesh.loops.foreach_get('tangent', tangents) + tangents = tangents.reshape(len(mesh.loops), 3) + + for ts in tangents: + for axis in range(3): + if int(round(ts[axis])) != 0: + ts[axis] = round(ts[axis]) + else: + ts[axis] = ts[axis] + (1 / 255) + + return tangents + + +def get_bitangents(mesh): + """Get an array of the tangent for each loop.""" + bitangents = np.empty(len(mesh.loops) * 3, dtype=np.float32) + mesh.loops.foreach_get('bitangent', bitangents) + bitangents = bitangents.reshape(len(mesh.loops), 3) + + for bs in bitangents: + for axis in range(3): + if int(round(bs[axis])) != 0: + bs[axis] = round(bs[axis]) + else: + bs[axis] = bs[axis] + (1 / 255) + + return bitangents + + +def get_uvs(mesh, uv_i): + layer = mesh.uv_layers[uv_i] + uvs = np.empty(len(mesh.loops) * 2, dtype=np.float32) + layer.data.foreach_get('uv', uvs) + uvs = uvs.reshape(len(mesh.loops), 2) + + # u,v -> u,1-v + uvs[:, 1] *= -1 + uvs[:, 1] += 1 + + return uvs + + +def get_colors(mesh, color_i): + colors = np.empty(len(mesh.loops) * 4, dtype=np.float32) + layer = mesh.vertex_colors[color_i] + mesh.color_attributes[layer.name].data.foreach_get('color', colors) + colors = colors.reshape(len(mesh.loops), 4) + return colors + + def bitArrToInt(arr): - lodStr = "" + lod_str = "" for bit in arr: if bit: - lodStr = "1" + lodStr + lod_str = "1" + lod_str else: - lodStr = "0" + lodStr - return int(lodStr, 2) + lod_str = "0" + lod_str + return int(lod_str, 2) + def triangulate_object(obj): me = obj.data - # Get a BMesh representation bm = bmesh.new() bm.from_mesh(me) bmesh.ops.triangulate(bm, faces=bm.faces[:]) - # V2.79 : bmesh.ops.triangulate(bm, faces=bm.faces[:], quad_method=0, ngon_method=0) - # Finish up, write the bmesh back to the mesh bm.to_mesh(me) bm.free() diff --git a/file_prim/bl_import_prim.py b/file_prim/bl_import_prim.py index 1ebbbc9..9905391 100644 --- a/file_prim/bl_import_prim.py +++ b/file_prim/bl_import_prim.py @@ -1,27 +1,18 @@ import os import bpy -import mathutils import numpy as np -from bpy.props import (BoolProperty, - FloatProperty, - StringProperty, - EnumProperty, - ) - -from bpy_extras.io_utils import (ImportHelper, - ExportHelper, - unpack_list, - unpack_face_list, - axis_conversion, - ) - from . import format as prim_format from ..file_borg import format as borg_format from .. import io_binary -def load_prim(operator, context, filepath, use_rig, rig_filepath): +def load_prim(operator, context, collection, filepath, use_rig, rig_filepath): + """Imports a mesh from the given path""" + + prim_name = bpy.path.display_name_from_filepath(filepath) + print("Started reading: " + str(prim_name) + "\n") + fp = os.fsencode(filepath) file = open(fp, "rb") br = io_binary.BinaryReader(file) @@ -29,17 +20,25 @@ def load_prim(operator, context, filepath, use_rig, rig_filepath): prim.read(br) br.close() + if prim.header.bone_rig_resource_index == 0xFFFFFFFF: + collection.prim_collection_properties.bone_rig_resource_index = -1 + else: + collection.prim_collection_properties.bone_rig_resource_index = prim.header.bone_rig_resource_index + collection.prim_collection_properties.has_bones = prim.header.property_flags.hasBones() + collection.prim_collection_properties.has_frames = prim.header.property_flags.hasFrames() + collection.prim_collection_properties.is_weighted = prim.header.property_flags.isWeightedObject() + collection.prim_collection_properties.is_linked = prim.header.property_flags.isLinkedObject() + borg = None if use_rig: + borg_name = bpy.path.display_name_from_filepath(filepath) + print("Started reading: " + str(borg_name) + "\n") fp = os.fsencode(rig_filepath) file = open(fp, "rb") br = io_binary.BinaryReader(file) borg = borg_format.BoneRig() borg.read(br) - prim_name = bpy.path.display_name_from_filepath(filepath) - print("Start reading: " + str(prim_name) + "\n") - meshes = [] for meshIndex in range(prim.num_objects()): meshes.append(load_prim_mesh(prim, borg, prim_name, meshIndex)) @@ -50,40 +49,44 @@ def load_prim(operator, context, filepath, use_rig, rig_filepath): return meshes -def load_prim_coli(prim, prim_name, meshIndex): - for boxColi in prim.header.object_table[meshIndex].sub_mesh.collision.box_entries: +def load_prim_coli(prim, prim_name: str, mesh_index: int): + """Testing class for the prim BoxColi """ + for boxColi in prim.header.object_table[mesh_index].sub_mesh.collision.box_entries: x, y, z = boxColi.min x1, y1, z1 = boxColi.max - bbMin = prim.header.object_table[meshIndex].prim_object.min - bbMax = prim.header.object_table[meshIndex].prim_object.max + bb_max = prim.header.object_table[mesh_index].prim_object.max - x = (x / 255) * bbMax[0] - y = (y / 255) * bbMax[1] - z = (z / 255) * bbMax[2] + x = (x / 255) * bb_max[0] + y = (y / 255) * bb_max[1] + z = (z / 255) * bb_max[2] - x1 = (x1 / 255) * bbMax[0] - y1 = (y1 / 255) * bbMax[1] - z1 = (z1 / 255) * bbMax[2] + x1 = (x1 / 255) * bb_max[0] + y1 = (y1 / 255) * bb_max[1] + z1 = (z1 / 255) * bb_max[2] - boxX = (x1 + x) / 2 - boxY = (y1 + y) / 2 - boxZ = (z1 + z) / 2 + box_x = (x1 + x) / 2 + box_y = (y1 + y) / 2 + box_z = (z1 + z) / 2 - scaleX = (x1 - x) / 2 - scaleY = (y1 - y) / 2 - scaleZ = (z1 - z) / 2 + scale_x = (x1 - x) / 2 + scale_y = (y1 - y) / 2 + scale_z = (z1 - z) / 2 - mesh = bpy.ops.mesh.primitive_cube_add(scale=(scaleX, scaleY, scaleZ), calc_uvs=True, align='WORLD', - location=(boxX, boxY, boxZ)) + bpy.ops.mesh.primitive_cube_add(scale=(scale_x, scale_y, scale_z), calc_uvs=True, align='WORLD', + location=(box_x, box_y, box_z)) ob = bpy.context.object me = ob.data - ob.name = (str(prim_name) + "_" + str(meshIndex) + "_Coli") + ob.name = (str(prim_name) + "_" + str(mesh_index) + "_Coli") me.name = 'CUBEMESH' -def load_prim_mesh(prim, borg, prim_name, meshIndex): - mesh = bpy.data.meshes.new(name=(str(prim_name) + "_" + str(meshIndex))) +def load_prim_mesh(prim, borg, prim_name: str, mesh_index: int): + """ + Turn the prim data structure into a Blender mesh. + Returns the generated Mesh + """ + mesh = bpy.data.meshes.new(name=(str(prim_name) + "_" + str(mesh_index))) use_rig = False if borg is not None: @@ -99,21 +102,19 @@ def load_prim_mesh(prim, borg, prim_name, meshIndex): if prim.header.property_flags.isWeightedObject() and use_rig: num_joint_sets = 2 - sub_mesh = prim.header.object_table[meshIndex].sub_mesh + sub_mesh = prim.header.object_table[mesh_index].sub_mesh - vert_joints = [[[0] * 4 for j in range(len(sub_mesh.vertexBuffer.vertices))] for i in range(num_joint_sets)] - vert_weights = [[[0] * 4 for j in range(len(sub_mesh.vertexBuffer.vertices))] for i in range(num_joint_sets)] + vert_joints = [[[0] * 4 for _ in range(len(sub_mesh.vertexBuffer.vertices))] for _ in range(num_joint_sets)] + vert_weights = [[[0] * 4 for _ in range(len(sub_mesh.vertexBuffer.vertices))] for _ in range(num_joint_sets)] loop_vidxs.extend(sub_mesh.indices) - i = 0 - for vert in sub_mesh.vertexBuffer.vertices: + for i, vert in enumerate(sub_mesh.vertexBuffer.vertices): vert_locs.extend([vert.position[0], vert.position[1], vert.position[2]]) for j in range(num_joint_sets): vert_joints[j][i] = (vert.joint[j]) vert_weights[j][i] = (vert.weight[j]) - i = i + 1 for index in sub_mesh.indices: vert = sub_mesh.vertexBuffer.vertices[index] @@ -167,13 +168,32 @@ def load_prim_mesh(prim, borg, prim_name, meshIndex): mesh.validate() mesh.update() - material_id = prim.header.object_table[meshIndex].prim_object.material_id - mesh.prim_properties.material_id = material_id + # write the additional properties to the blender structure + prim_mesh_obj = prim.header.object_table[mesh_index].prim_object + prim_sub_mesh_obj = prim.header.object_table[mesh_index].sub_mesh.prim_object - lod = prim.header.object_table[meshIndex].prim_object.lodmask + lod = prim_mesh_obj.lodmask mask = [] for bit in range(8): mask.append(0 != (lod & (1 << bit))) mesh.prim_properties.lod = mask + mesh.prim_properties.material_id = prim_mesh_obj.material_id + mesh.prim_properties.prim_type = str(prim_mesh_obj.prims.prim_header.type) + mesh.prim_properties.prim_sub_type = str(prim_mesh_obj.sub_type) + + mesh.prim_properties.axis_lock = [prim_mesh_obj.properties.isXaxisLocked(), + prim_mesh_obj.properties.isYaxisLocked(), + prim_mesh_obj.properties.isZaxisLocked()] + mesh.prim_properties.no_physics = prim_mesh_obj.properties.hasNoPhysicsProp() + + mesh.prim_properties.variant_id = prim_sub_mesh_obj.variant_id + mesh.prim_properties.z_bias = prim_sub_mesh_obj.zbias + mesh.prim_properties.z_offset = prim_sub_mesh_obj.zoffset + mesh.prim_properties.use_mesh_color = prim_sub_mesh_obj.properties.useColor1() + mesh.prim_properties.mesh_color = [prim_sub_mesh_obj.color1[0]/255, + prim_sub_mesh_obj.color1[1]/255, + prim_sub_mesh_obj.color1[2]/255, + prim_sub_mesh_obj.color1[3]/255] + return mesh diff --git a/file_prim/bl_utils_prim.py b/file_prim/bl_utils_prim.py index d8721cf..faddbc3 100644 --- a/file_prim/bl_utils_prim.py +++ b/file_prim/bl_utils_prim.py @@ -7,6 +7,4 @@ def is_weighted(filepath): fp = os.fsencode(filepath) file = open(fp, "rb") br = io_binary.BinaryReader(file) - - prim = format.RenderPrimitve() - return prim.readHeader(br).bone_rig_resource_index != 0xFFFFFFFF + return format.readHeader(br).bone_rig_resource_index != 0xFFFFFFFF diff --git a/file_prim/format.py b/file_prim/format.py index 0540945..c76b206 100644 --- a/file_prim/format.py +++ b/file_prim/format.py @@ -1,9 +1,26 @@ -from enum import IntEnum +import enum import sys -import abc - -class PrimObjectSubtype(IntEnum): +""" +The RenderPrimitive format: + +RenderPrimitive ↴ + PrimObjectHeader + Objects ↴ + PrimMesh ↴ + PrimObject + PrimSubMesh ↴ + PrimObject + ... +""" + + +class PrimObjectSubtype(enum.IntEnum): + """ + Enum defining a subtype. All objects inside a prim have a subtype + The StandarduvX types are not used within the H2016, H2 and H3 games, + a probable cause for this is the introduction of a num_uvchannels variable. + """ Standard = 0 Linked = 1 Weighted = 2 @@ -12,8 +29,11 @@ class PrimObjectSubtype(IntEnum): Standarduv4 = 5 -class PrimType(IntEnum): - unknown = 0 +class PrimType(enum.IntEnum): + """ + A type property attached to all headers found within the prim format + """ + Unknown = 0 ObjectHeader = 1 Mesh = 2 Decal = 3 @@ -23,6 +43,8 @@ class PrimType(IntEnum): class PrimMeshClothId: + """Bitfield defining properties of the cloth data. Most bits are unknown.""" + def __init__(self, value): self.bitfield = value @@ -34,7 +56,9 @@ def write(self, br): class PrimObjectHeaderPropertyFlags: - def __init__(self, val): + """Global properties defined in the main header of a RenderPrimitive.""" + + def __init__(self, val: int): self.bitfield = val def hasBones(self): @@ -69,7 +93,8 @@ def toString(self): class PrimObjectPropertyFlags: - def __init__(self, value): + """Mesh specific properties, used in Mesh and SubMesh. """ + def __init__(self, value: int): self.bitfield = value def isXaxisLocked(self): @@ -93,10 +118,23 @@ def useColor1(self): def hasNoPhysicsProp(self): return self.bitfield & 0b1000000 == 64 - def setHighResulution(self): - if not self.isHighResolution(): - self.bitfield = self.bitfield + 8 + def setXaxisLocked(self): + self.bitfield |= 0b1 + + def setYaxisLocked(self): + self.bitfield |= 0b10 + def setZaxisLocked(self): + self.bitfield |= 0b100 + + def setHighResolution(self): + self.bitfield |= 0b1000 + + def setColor1(self): + self.bitfield |= 0b100000 + + def setNoPhysics(self): + self.bitfield |= 0b1000000 def write(self, br): br.writeUByte(self.bitfield) @@ -112,6 +150,7 @@ def toString(self): class BoxColiEntry: + """Helper class for BoxColi, defines an entry to store BoxColi""" def __init__(self): self.min = [0] * 3 self.max = [0] * 3 @@ -126,6 +165,7 @@ def write(self, br): class BoxColi: + """Used to store and array of BoxColi. Used for bullet collision""" def __init__(self): self.tri_per_chunk = 0x20 self.box_entries = [] @@ -134,9 +174,9 @@ def read(self, br): num_chunks = br.readUShort() self.tri_per_chunk = br.readUShort() self.box_entries = [-1] * num_chunks - for entry in range(num_chunks): - self.box_entries[entry] = BoxColiEntry() - self.box_entries[entry].read(br) + self.box_entries = [BoxColiEntry() for _ in range(5)] + for box_entry in self.box_entries: + box_entry.read(br) def write(self, br): br.writeUShort(len(self.box_entries)) @@ -147,10 +187,11 @@ def write(self, br): class Vertex: + """A vertex with all field found inside a RenderPrimitive file""" def __init__(self): self.position = [0] * 4 - self.weight = [[0] * 4 for i in range(2)] - self.joint = [[0] * 4 for i in range(2)] + self.weight = [[0] * 4 for _ in range(2)] + self.joint = [[0] * 4 for _ in range(2)] self.normal = [1] * 4 self.tangent = [1] * 4 self.bitangent = [1] * 4 @@ -158,118 +199,268 @@ def __init__(self): self.color = [0xFF] * 4 -class VertexBuffer: +class PrimMesh: + """A subMesh wrapper class, used to store information about a mesh, as well as the mesh itself (called sub_mesh)""" def __init__(self): - self.vertices = [] + self.prim_object = PrimObject(2) + self.pos_scale = [1.0] * 4 # TODO: Remove this, should be calculated when exporting + self.pos_bias = [0.0] * 4 # TODO: No need to keep these around + self.tex_scale_bias = [1.0, 1.0, 0.0, 0.0] # TODO: This can also go + self.cloth_id = PrimMeshClothId(0) + self.sub_mesh = PrimSubMesh() + + def read(self, br, flags): + self.prim_object.read(br) - def read(self, br, num_vertices, num_uvchannels, mesh, sub_mesh_color1, sub_mesh_flags, flags): - self.vertices = [0] * num_vertices + # this will point to a table of submeshes, this is not really usefull since this table will always contain a + # single pointer if the table were to contain multiple pointer we'd have no way of knowing since the table + # size is never defined. to improve readability sub_mesh_table is not an array + sub_mesh_table_offset = br.readUInt() + + self.pos_scale = br.readFloatVec(4) + self.pos_bias = br.readFloatVec(4) + self.tex_scale_bias = br.readFloatVec(4) + + self.cloth_id = PrimMeshClothId(br.readUInt()) + + old_offset = br.tell() + br.seek(sub_mesh_table_offset) + sub_mesh_offset = br.readUInt() + br.seek(sub_mesh_offset) + self.sub_mesh.read(br, self, flags) + br.seek(old_offset) # reset offset to end of header, this is required for WeightedPrimMesh + + def write(self, br, flags): + self.update() + sub_mesh_offset = self.sub_mesh.write(br, self, flags) + + header_offset = br.tell() + + self.prim_object.write(br) + + br.writeUInt(sub_mesh_offset) + + br.writeFloatVec(self.pos_scale) + br.writeFloatVec(self.pos_bias) + br.writeFloatVec(self.tex_scale_bias) + + self.cloth_id.write(br) + + br.align(16) + + return header_offset + + def update(self): + bb = self.sub_mesh.calc_bb() + bb_min = bb[0] + bb_max = bb[1] + + # set bounding box + self.prim_object.min = bb_min + self.prim_object.max = bb_max + + # set position scale + self.pos_scale[0] = (bb_max[0] - bb_min[0]) * 0.5 + self.pos_scale[1] = (bb_max[1] - bb_min[1]) * 0.5 + self.pos_scale[2] = (bb_max[2] - bb_min[2]) * 0.5 + self.pos_scale[3] = 0.5 + + # set position bias + self.pos_bias[0] = (bb_max[0] + bb_min[0]) * 0.5 + self.pos_bias[1] = (bb_max[1] + bb_min[1]) * 0.5 + self.pos_bias[2] = (bb_max[2] + bb_min[2]) * 0.5 + self.pos_bias[3] = 1 + + bb_uv = self.sub_mesh.calc_UVbb() + bb_uv_min = bb_uv[0] + bb_uv_max = bb_uv[1] + + # set UV scale + self.tex_scale_bias[0] = (bb_uv_max[0] - bb_uv_min[0]) * 0.5 + self.tex_scale_bias[1] = (bb_uv_max[1] - bb_uv_min[1]) * 0.5 - for vert in range(num_vertices): - self.vertices[vert] = Vertex() + # set UV bias + self.tex_scale_bias[2] = (bb_uv_max[0] + bb_uv_min[0]) * 0.5 + self.tex_scale_bias[3] = (bb_uv_max[1] + bb_uv_min[1]) * 0.5 + + +class PrimMeshWeighted(PrimMesh): + """A different variant of PrimMesh. In addition to PrimMesh it also stores bone data""" + def __init__(self): + super().__init__() + self.prim_mesh = PrimMesh() + self.num_copy_bones = 0 + self.copy_bones = 0 + self.bone_indices = BoneIndices() + self.bone_info = BoneInfo() + + def read(self, br, flags): + super().read(br, flags) + self.num_copy_bones = br.readUInt() + copy_bones_offset = br.readUInt() + + bone_indices_offset = br.readUInt() + bone_info_offset = br.readUInt() + + br.seek(copy_bones_offset) + self.copy_bones = 0 # empty, because unknown + + br.seek(bone_indices_offset) + self.bone_indices.read(br) + + br.seek(bone_info_offset) + self.bone_info.read(br) + + def write(self, br, flags): + sub_mesh_offset = self.sub_mesh.write(br, self.prim_mesh, flags) + + bone_info_offset = br.tell() + self.bone_info.write(br) + + bone_indices_offset = br.tell() + self.bone_indices.write(br) + + header_offset = br.tell() + + self.update() + self.prim_object.write(br) + + br.writeUInt(sub_mesh_offset) + + br.writeFloatVec(self.pos_scale) + br.writeFloatVec(self.pos_bias) + br.writeFloatVec(self.tex_scale_bias) + + self.cloth_id.write(br) + + br.writeUInt(self.num_copy_bones) + br.writeUInt(0) # copy_bones offset PLACEHOLDER + + br.writeUInt(bone_indices_offset) + br.writeUInt(bone_info_offset) + + br.align(16) + return header_offset + + +class VertexBuffer: + """A helper class used to store and manage the vertices found inside a PrimSubMesh""" + def __init__(self): + self.vertices = [] - for vert in range(num_vertices): - if (mesh.prim_object.properties.isHighResolution()): - self.vertices[vert].position[0] = (br.readFloat() * mesh.pos_scale[0]) + mesh.pos_bias[0] - self.vertices[vert].position[1] = (br.readFloat() * mesh.pos_scale[1]) + mesh.pos_bias[1] - self.vertices[vert].position[2] = (br.readFloat() * mesh.pos_scale[2]) + mesh.pos_bias[2] - self.vertices[vert].position[3] = float(1.0) + def read(self, br, num_vertices: int, + num_uvchannels: int, + mesh: PrimMesh, + sub_mesh_color1: [], + sub_mesh_flags: PrimObjectPropertyFlags, + flags: PrimObjectHeaderPropertyFlags): + + self.vertices = [Vertex() for _ in range(num_vertices)] + + for vertex in self.vertices: + if mesh.prim_object.properties.isHighResolution(): + vertex.position[0] = (br.readFloat() * mesh.pos_scale[0]) + mesh.pos_bias[0] + vertex.position[1] = (br.readFloat() * mesh.pos_scale[1]) + mesh.pos_bias[1] + vertex.position[2] = (br.readFloat() * mesh.pos_scale[2]) + mesh.pos_bias[2] + vertex.position[3] = 1 else: - self.vertices[vert].position = br.readShortQuantizedVec(4, mesh.pos_scale, mesh.pos_bias) - - if (flags.isWeightedObject()): - for vert in range(num_vertices): - self.vertices[vert].weight[0][0] = br.readUByte() / 255 - self.vertices[vert].weight[0][1] = br.readUByte() / 255 - self.vertices[vert].weight[0][2] = br.readUByte() / 255 - self.vertices[vert].weight[0][3] = br.readUByte() / 255 - - self.vertices[vert].joint[0][0] = br.readUByte() - self.vertices[vert].joint[0][1] = br.readUByte() - self.vertices[vert].joint[0][2] = br.readUByte() - self.vertices[vert].joint[0][3] = br.readUByte() - - self.vertices[vert].weight[1][0] = br.readUByte() / 255 - self.vertices[vert].weight[1][1] = br.readUByte() / 255 - self.vertices[vert].weight[1][2] = 0 - self.vertices[vert].weight[1][3] = 0 - - self.vertices[vert].joint[1][0] = br.readUByte() - self.vertices[vert].joint[1][1] = br.readUByte() - self.vertices[vert].joint[1][2] = 0 - self.vertices[vert].joint[1][3] = 0 - - for vert in range(num_vertices): - self.vertices[vert].normal = br.readUByteQuantizedVec(4) - self.vertices[vert].tangent = br.readUByteQuantizedVec(4) - self.vertices[vert].bitangent = br.readUByteQuantizedVec(4) - self.vertices[vert].uv = [0] * num_uvchannels + vertex.position = br.readShortQuantizedVec(4, mesh.pos_scale, mesh.pos_bias) + + if flags.isWeightedObject(): + for vertex in self.vertices: + vertex.weight[0][0] = br.readUByte() / 255 + vertex.weight[0][1] = br.readUByte() / 255 + vertex.weight[0][2] = br.readUByte() / 255 + vertex.weight[0][3] = br.readUByte() / 255 + + vertex.joint[0][0] = br.readUByte() + vertex.joint[0][1] = br.readUByte() + vertex.joint[0][2] = br.readUByte() + vertex.joint[0][3] = br.readUByte() + + vertex.weight[1][0] = br.readUByte() / 255 + vertex.weight[1][1] = br.readUByte() / 255 + vertex.weight[1][2] = 0 + vertex.weight[1][3] = 0 + + vertex.joint[1][0] = br.readUByte() + vertex.joint[1][1] = br.readUByte() + vertex.joint[1][2] = 0 + vertex.joint[1][3] = 0 + + for vertex in self.vertices: + vertex.normal = br.readUByteQuantizedVec(4) + vertex.tangent = br.readUByteQuantizedVec(4) + vertex.bitangent = br.readUByteQuantizedVec(4) + vertex.uv = [0] * num_uvchannels for uv in range(num_uvchannels): - self.vertices[vert].uv[uv] = br.readShortQuantizedVec(2, mesh.tex_scale_bias[0:2], + vertex.uv[uv] = br.readShortQuantizedVec(2, mesh.tex_scale_bias[0:2], mesh.tex_scale_bias[2:4]) - if (not mesh.prim_object.properties.useColor1() or flags.isWeightedObject()): - if (not sub_mesh_flags.useColor1()): - for vert in range(num_vertices): - self.vertices[vert].color = br.readUByteVec(4) + if not mesh.prim_object.properties.useColor1() or flags.isWeightedObject(): + if not sub_mesh_flags.useColor1(): + for vertex in self.vertices: + vertex.color = br.readUByteVec(4) else: - for vert in range(num_vertices): - self.vertices[vert].color[0] = sub_mesh_color1[0] - self.vertices[vert].color[1] = sub_mesh_color1[1] - self.vertices[vert].color[2] = sub_mesh_color1[2] - self.vertices[vert].color[3] = sub_mesh_color1[3] + for vertex in self.vertices: + vertex.color[0] = sub_mesh_color1[0] + vertex.color[1] = sub_mesh_color1[1] + vertex.color[2] = sub_mesh_color1[2] + vertex.color[3] = sub_mesh_color1[3] - def write(self, br, mesh, sub_mesh_flags, flags): + def write(self, br, mesh, + sub_mesh_flags: PrimObjectPropertyFlags, + flags: PrimObjectHeaderPropertyFlags): - num_vertices = len(self.vertices) - if num_vertices > 0: + if len(self.vertices) > 0: num_uvchannels = len(self.vertices[0].uv) else: num_uvchannels = 0 # positions - for vert in range(num_vertices): - if (mesh.prim_object.properties.isHighResolution()): - br.writeFloat((self.vertices[vert].position[0] - mesh.pos_bias[0]) / mesh.pos_scale[0]) - br.writeFloat((self.vertices[vert].position[1] - mesh.pos_bias[1]) / mesh.pos_scale[1]) - br.writeFloat((self.vertices[vert].position[2] - mesh.pos_bias[2]) / mesh.pos_scale[2]) + for vertex in self.vertices: + if mesh.prim_object.properties.isHighResolution(): + br.writeFloat((vertex.position[0] - mesh.pos_bias[0]) / mesh.pos_scale[0]) + br.writeFloat((vertex.position[1] - mesh.pos_bias[1]) / mesh.pos_scale[1]) + br.writeFloat((vertex.position[2] - mesh.pos_bias[2]) / mesh.pos_scale[2]) else: - br.writeShortQuantizedVec(self.vertices[vert].position, mesh.pos_scale, mesh.pos_bias) + br.writeShortQuantizedVec(vertex.position, mesh.pos_scale, mesh.pos_bias) # joints and weights - if (flags.isWeightedObject()): - for vert in range(num_vertices): - br.writeUByte(br.IOI_round(self.vertices[vert].weight[0][0])) - br.writeUByte(br.IOI_round(self.vertices[vert].weight[0][1])) - br.writeUByte(br.IOI_round(self.vertices[vert].weight[0][2])) - br.writeUByte(br.IOI_round(self.vertices[vert].weight[0][3])) - - br.writeUByte(self.vertices[vert].joint[0][0]) - br.writeUByte(self.vertices[vert].joint[0][1]) - br.writeUByte(self.vertices[vert].joint[0][2]) - br.writeUByte(self.vertices[vert].joint[0][3]) - - br.writeUByte(br.IOI_round(self.vertices[vert].weight[1][0])) - br.writeUByte(br.IOI_round(self.vertices[vert].weight[1][1])) - br.writeUByte(self.vertices[vert].joint[1][0]) - br.writeUByte(self.vertices[vert].joint[1][1]) + if flags.isWeightedObject(): + for vertex in self.vertices: + br.writeUByte(br.IOI_round(vertex.weight[0][0])) + br.writeUByte(br.IOI_round(vertex.weight[0][1])) + br.writeUByte(br.IOI_round(vertex.weight[0][2])) + br.writeUByte(br.IOI_round(vertex.weight[0][3])) + + br.writeUByte(vertex.joint[0][0]) + br.writeUByte(vertex.joint[0][1]) + br.writeUByte(vertex.joint[0][2]) + br.writeUByte(vertex.joint[0][3]) + + br.writeUByte(br.IOI_round(vertex.weight[1][0])) + br.writeUByte(br.IOI_round(vertex.weight[1][1])) + br.writeUByte(vertex.joint[1][0]) + br.writeUByte(vertex.joint[1][1]) # ntb + uv - for vert in range(num_vertices): - br.writeUByteQuantizedVec(self.vertices[vert].normal) - br.writeUByteQuantizedVec(self.vertices[vert].tangent) - br.writeUByteQuantizedVec(self.vertices[vert].bitangent) + for vertex in self.vertices: + br.writeUByteQuantizedVec(vertex.normal) + br.writeUByteQuantizedVec(vertex.tangent) + br.writeUByteQuantizedVec(vertex.bitangent) for uv in range(num_uvchannels): - br.writeShortQuantizedVec(self.vertices[vert].uv[uv], mesh.tex_scale_bias[0:2], + br.writeShortQuantizedVec(vertex.uv[uv], mesh.tex_scale_bias[0:2], mesh.tex_scale_bias[2:4]) # color - if (not mesh.prim_object.properties.useColor1() or flags.isWeightedObject()): - if (not sub_mesh_flags.useColor1() or flags.isWeightedObject()): - for vert in range(num_vertices): - br.writeUByteVec(self.vertices[vert].color) + if not mesh.prim_object.properties.useColor1() or flags.isWeightedObject(): + if not sub_mesh_flags.useColor1() or flags.isWeightedObject(): + for vertex in self.vertices: + br.writeUByteVec(vertex.color) class PrimSubMesh: + """Stores the mesh data. as well as the BoxColi and ClothData""" def __init__(self): self.prim_object = PrimObject(0) self.num_vertices = 0 @@ -282,7 +473,7 @@ def __init__(self): self.collision = BoxColi() self.cloth = -1 - def read(self, br, mesh, flags): + def read(self, br, mesh: PrimMesh, flags: PrimObjectHeaderPropertyFlags): self.prim_object.read(br) num_vertices = br.readUInt() @@ -309,14 +500,15 @@ def read(self, br, mesh, flags): br.seek(collision_offset) self.collision.read(br) - # optional detour for cloth data - if (cloth_offset != 0 and self.cloth != -1): + # optional detour for cloth data, + # !locked because the format is not known enough! + if cloth_offset != 0 and self.cloth != -1: br.seek(cloth_offset) self.cloth.read(br, mesh, self) else: self.cloth = -1 - def write(self, br, mesh, flags): + def write(self, br, mesh, flags: PrimObjectHeaderPropertyFlags): index_offset = br.tell() for index in self.indices: br.writeUShort(index) @@ -331,7 +523,7 @@ def write(self, br, mesh, flags): br.align(16) - if (self.cloth != -1): + if self.cloth != -1: cloth_offset = br.tell() self.cloth.write(br, mesh) br.align(16) @@ -341,7 +533,6 @@ def write(self, br, mesh, flags): header_offset = br.tell() # IOI uses a cleared primMesh object. so let's clear it here as well self.prim_object.lodmask = 0x0 - self.prim_object.color1 = [0, 0, 0, 0] self.prim_object.wire_color = 0x0 # TODO: optimze this away @@ -370,50 +561,41 @@ def write(self, br, mesh, flags): obj_table_offset = br.tell() br.writeUInt(header_offset) - br.writeUInt(0) # padd - br.writeUInt64(0) # padd + br.writeUInt(0) # padding + br.writeUInt64(0) # padding return obj_table_offset def calc_bb(self): - bb = [0.0] * 6 - max = [sys.float_info.min] * 3 - min = [sys.float_info.max] * 3 + bb_max = [sys.float_info.min] * 3 + bb_min = [sys.float_info.max] * 3 for vert in self.vertexBuffer.vertices: for axis in range(3): - if max[axis] < vert.position[axis]: - max[axis] = vert.position[axis] - - if min[axis] > vert.position[axis]: - min[axis] = vert.position[axis] + if bb_max[axis] < vert.position[axis]: + bb_max[axis] = vert.position[axis] - bb = [min, max] - return bb + if bb_min[axis] > vert.position[axis]: + bb_min[axis] = vert.position[axis] + return [bb_min, bb_max] def calc_UVbb(self): - max = [sys.float_info.min] * 3 - min = [sys.float_info.max] * 3 + bb_max = [sys.float_info.min] * 3 + bb_min = [sys.float_info.max] * 3 layer = 0 for vert in self.vertexBuffer.vertices: for axis in range(2): - if max[axis] < vert.uv[layer][axis]: - max[axis] = vert.uv[layer][axis] + if bb_max[axis] < vert.uv[layer][axis]: + bb_max[axis] = vert.uv[layer][axis] - if min[axis] > vert.uv[layer][axis]: - min[axis] = vert.uv[layer][axis] - uvBB = [min, max] - return uvBB - - def num_uvchannels(self): - if self.num_vertices() > 0: - return len(self.vertexBuffer.vertices[0].uv) - else: - return 0 + if bb_min[axis] > vert.uv[layer][axis]: + bb_min[axis] = vert.uv[layer][axis] + return [bb_min, bb_max] class PrimObject: - def __init__(self, type): - self.prims = Prims(type) + """A header class used to store information about PrimMesh and PrimSubMesh""" + def __init__(self, type_preset: int): + self.prims = Prims(type_preset) self.sub_type = PrimObjectSubtype(0) self.properties = PrimObjectPropertyFlags(0) self.lodmask = 0xFF @@ -422,8 +604,7 @@ def __init__(self, type): self.zoffset = 0 self.material_id = 0 self.wire_color = 0xFFFFFFFF - self.color1 = [ - 0xFF] * 4 # global color used when useColor1 is set. will only work when defined inside PrimSubMesh + self.color1 = [0xFF] * 4 # global color used when useColor1 is set. only works inside PrimSubMesh self.min = [0] * 3 self.max = [0] * 3 @@ -476,7 +657,7 @@ def write(self, br): class BoneInfo: def __init__(self): - self.total_size = 0 # TODO: get shortest value here + self.total_size = 0 self.bone_remap = [0xFF] * 255 self.pad = 0 self.accel_entries = [] @@ -489,10 +670,9 @@ def read(self, br): self.bone_remap[i] = br.readUByte() self.pad = br.readUByte() - self.accel_entries = [0] * num_accel_entries - for i in range(num_accel_entries): - self.accel_entries[i] = BoneAccel() - self.accel_entries[i].read(br) + self.accel_entries = [BoneAccel() for _ in range(num_accel_entries)] + for accel_entry in self.accel_entries: + accel_entry.read(br) def write(self, br): @@ -528,11 +708,12 @@ def write(self, br): # needs additional research class ClothData: + """Class to store data about cloth""" def __init__(self): self.size = 0 self.cloth_data = [0] * self.size - def read(self, br, mesh, sub_mesh): + def read(self, br, mesh: PrimMesh, sub_mesh: PrimSubMesh): if mesh.cloth_id.isSmoll(): self.size = br.readUInt() @@ -549,154 +730,12 @@ def write(self, br, mesh): br.writeUByte(b) -class PrimMesh: - def __init__(self): - self.prim_object = PrimObject(2) - self.pos_scale = [1] * 4 - self.pos_bias = [0] * 4 - self.tex_scale_bias = [1, 1, 0, 0] - self.cloth_id = PrimMeshClothId(0) - self.sub_mesh = PrimSubMesh() - - def read(self, br, flags): - self.prim_object.read(br) - - # this will point to a table of submeshes, this is not really usefull since this table will always contain a single pointer - # if the table were to contain multiple pointer we'd have no way of knowing since the table size is never defined. - # to improve readability sub_mesh_table is not an array - sub_mesh_table_offset = br.readUInt() - - self.pos_scale = br.readFloatVec(4) - self.pos_bias = br.readFloatVec(4) - self.tex_scale_bias = br.readFloatVec(4) - - self.cloth_id = PrimMeshClothId(br.readUInt()) - - old_offset = br.tell() - br.seek(sub_mesh_table_offset) - sub_mesh_offset = br.readUInt() - br.seek(sub_mesh_offset) - self.sub_mesh.read(br, self, flags) - br.seek(old_offset) # reset offset to end of header, this is required for WeightedPrimMesh - - def write(self, br, flags): - self.update() - sub_mesh_offset = self.sub_mesh.write(br, self, flags) - - header_offset = br.tell() - - self.prim_object.write(br) - - br.writeUInt(sub_mesh_offset) - - br.writeFloatVec(self.pos_scale) - br.writeFloatVec(self.pos_bias) - br.writeFloatVec(self.tex_scale_bias) - - self.cloth_id.write(br) - - br.align(16) - - return header_offset - - def update(self): - bb = self.sub_mesh.calc_bb() - min = bb[0] - max = bb[1] - - # set bounding box - self.prim_object.min = min - self.prim_object.max = max - - # set position scale - self.pos_scale[0] = (max[0] - min[0]) * 0.5 - self.pos_scale[1] = (max[1] - min[1]) * 0.5 - self.pos_scale[2] = (max[2] - min[2]) * 0.5 - self.pos_scale[3] = 0.5 - - # set position bias - self.pos_bias[0] = (max[0] + min[0]) * 0.5 - self.pos_bias[1] = (max[1] + min[1]) * 0.5 - self.pos_bias[2] = (max[2] + min[2]) * 0.5 - self.pos_bias[3] = 1 - - UVbb = self.sub_mesh.calc_UVbb() - minUV = UVbb[0] - maxUV = UVbb[1] - - # set UV scale - self.tex_scale_bias[0] = (maxUV[0] - minUV[0]) * 0.5 - self.tex_scale_bias[1] = (maxUV[1] - minUV[1]) * 0.5 - - # set UV bias - self.tex_scale_bias[2] = (maxUV[0] + minUV[0]) * 0.5 - self.tex_scale_bias[3] = (maxUV[1] + minUV[1]) * 0.5 - - -class PrimMeshWeighted(PrimMesh): - def __init__(self): - super().__init__() - self.prim_mesh = PrimMesh() - self.num_copy_bones = 0 - self.copy_bones = 0 - self.bone_indices = BoneIndices() - self.bone_info = BoneInfo() - - def read(self, br, flags): - super().read(br, flags) - self.num_copy_bones = br.readUInt() - copy_bones_offset = br.readUInt() - - bone_indices_offset = br.readUInt() - bone_info_offset = br.readUInt() - - br.seek(copy_bones_offset) - self.copy_bones = 0 # empty, because unknown - - br.seek(bone_indices_offset) - self.bone_indices.read(br) - - br.seek(bone_info_offset) - self.bone_info.read(br) - - def write(self, br, flags): - sub_mesh_offset = self.sub_mesh.write(br, self.prim_mesh, flags) - - bone_info_offset = br.tell() - self.bone_info.write(br) - - bone_indices_offset = br.tell() - self.bone_indices.write(br) - - header_offset = br.tell() - - self.update() - self.prim_object.write(br) - - br.writeUInt(sub_mesh_offset) - - br.writeFloatVec(self.pos_scale) - br.writeFloatVec(self.pos_bias) - br.writeFloatVec(self.tex_scale_bias) - - self.cloth_id.write(br) - - br.writeUInt(self.num_copy_bones) - br.writeUInt(0) # copy_bones offset PLACEHOLDER - - br.writeUInt(bone_indices_offset) - br.writeUInt(bone_info_offset) - - br.align(16) - return header_offset - - -# TODO: make pos scale and bias local only class PrimHeader: - def __init__(self, type): + """Small header class used by other header classes""" + def __init__(self, type_preset): self.draw_destination = 0 self.pack_type = 0 - self.type = PrimType(type) + self.type = PrimType(type_preset) def read(self, br): self.draw_destination = br.readUByte() @@ -710,8 +749,12 @@ def write(self, br): class Prims: - def __init__(self, type): - self.prim_header = PrimHeader(type) + """ + Wrapper class for PrimHeader. + I'm not quite sure why it exists, but here it is :) + """ + def __init__(self, type_preset: int): + self.prim_header = PrimHeader(type_preset) def read(self, br): self.prim_header.read(br) @@ -721,6 +764,7 @@ def write(self, br): class PrimObjectHeader: + """Global RenderPrimitive header. used by all objects defined""" def __init__(self): self.prims = Prims(1) self.property_flags = PrimObjectHeaderPropertyFlags(0) @@ -747,7 +791,7 @@ def read(self, br): self.object_table = [-1] * num_objects for obj in range(num_objects): br.seek(object_table_offsets[obj]) - if (self.property_flags.isWeightedObject()): + if self.property_flags.isWeightedObject(): self.object_table[obj] = PrimMeshWeighted() self.object_table[obj].read(br, self.property_flags) else: @@ -774,7 +818,12 @@ def write(self, br): header_offset = br.tell() self.prims.write(br) self.property_flags.write(br) - br.writeUInt(self.bone_rig_resource_index) + + if self.bone_rig_resource_index < 0: + br.writeUInt(0xFFFFFFFF) + else: + br.writeUInt(self.bone_rig_resource_index) + br.writeUInt(len(obj_offsets)) br.writeUInt(obj_table_offset) @@ -785,15 +834,35 @@ def write(self, br): br.writeUInt(0) return header_offset - def append_bb(self, min, max): + def append_bb(self, bb_min: [], bb_max: []): for axis in range(3): - if min[axis] < self.min[axis]: - self.min[axis] = min[axis] - if max[axis] > self.max[axis]: - self.max[axis] = max[axis] + if bb_min[axis] < self.min[axis]: + self.min[axis] = bb_min[axis] + if bb_max[axis] > self.max[axis]: + self.max[axis] = bb_max[axis] + + +def readHeader(br): + """"Global function to read only the header of a RenderPrimitive, used to fast file identification""" + offset = br.readUInt() + br.seek(offset) + header_values = PrimObjectHeader() + header_values.prims.read(br) + header_values.property_flags = PrimObjectHeaderPropertyFlags(br.readUInt()) + header_values.bone_rig_resource_index = br.readUInt() + br.readUInt() + br.readUInt() + header_values.min = br.readFloatVec(3) + header_values.max = br.readFloatVec(3) + return header_values class RenderPrimitve: + """ + RenderPrimitive class, represents the .prim file format. + It contains a multitude of meshes and properties. + The RenderPrimitive format has built-in support for: armatures, bounding boxes, collision and cloth physics. + """ def __init__(self): self.header = PrimObjectHeader() @@ -809,18 +878,6 @@ def write(self, br): br.seek(0) br.writeUInt64(header_offset) - def readHeader(self, br): - offset = br.readUInt() - br.seek(offset) - header_values = PrimObjectHeader() - header_values.prims.read(br) - header_values.property_flags = PrimObjectHeaderPropertyFlags(br.readUInt()) - header_values.bone_rig_resource_index = br.readUInt() - br.readUInt() - br.readUInt() - header_values.min = br.readFloatVec(3) - header_values.max = br.readFloatVec(3) - return header_values def num_objects(self): num = 0 for obj in self.header.object_table: diff --git a/file_vtxd/__init__.py b/file_vtxd/__init__.py index c8094e5..7327154 100644 --- a/file_vtxd/__init__.py +++ b/file_vtxd/__init__.py @@ -1,95 +1,33 @@ import bpy -from bpy_extras.io_utils import ( - ImportHelper, - ExportHelper -) +# ------------------------------------------------------------------------ +# Registration +# ------------------------------------------------------------------------ -from bpy.props import ( - StringProperty -) +classes = [] - -class ImportVTXD(bpy.types.Operator, ImportHelper): - """Load a VTXD file""" - bl_idname = "import_mesh.vtxd" - bl_label = "Import VTXD Mesh" - filename_ext = ".vtxd" - filter_glob = StringProperty( - default="*.vtxd", - options={'HIDDEN'}, - ) - - filepath: StringProperty( - name="VTXD", - description="Set path for the VTXD file", - subtype="FILE_PATH" - ) - - def execute(self, context): - from . import bl_import - keywords = self.as_keywords(ignore=( - 'filter_glob', - )) - - meshes = bl_import.load_vtxd(self, context, **keywords) - - if not meshes: - return {'CANCELLED'} - - scene = bpy.context.scene - for mesh in meshes: - obj = bpy.data.objects.new(mesh.name, mesh) - scene.collection.objects.link(obj) - layer = bpy.context.view_layer - layer.update() - return {'FINISHED'} - - -class ExportVTXD(bpy.types.Operator, ExportHelper): - """Export to a VTXD file""" - bl_idname = 'export_mesh.vtxd' - bl_label = 'Export VTXD Mesh' - check_extension = True - filename_ext = '.vtxd' - filter_glob: StringProperty(default='*.vtxd', options={'HIDDEN'}) - - def execute(self, context): - from . import bl_export - keywords = self.as_keywords(ignore=( - 'check_existing', - 'filter_glob', - )) - return bl_export.save_vtxd(self, context, **keywords) - - -classes = [ - ImportVTXD, - ExportVTXD -] +modules = [] def register(): - for c in classes: - bpy.utils.register_class(c) - bpy.types.TOPBAR_MT_file_import.append(menu_func_import) - bpy.types.TOPBAR_MT_file_export.append(menu_func_export) + from bpy.utils import register_class + for module in modules: + module.register() -def unregister(): - for c in reversed(classes): - bpy.utils.unregister_class(c) - bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) - bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) + for cls in classes: + register_class(cls) -def menu_func_import(self, context): - self.layout.operator(ImportVTXD.bl_idname, text="Glacier VertexData (.vtxd)") +def unregister(): + from bpy.utils import unregister_class + for module in reversed(modules): + module.unregister() -def menu_func_export(self, context): - self.layout.operator(ExportVTXD.bl_idname, text="Glacier VertexData (.vtxd)") + for cls in classes: + unregister_class(cls) -if __name__ == '__main__': +if __name__ == "__main__": register() diff --git a/file_vtxd/bl_export.py b/file_vtxd/bl_export.py deleted file mode 100644 index baa93bf..0000000 --- a/file_vtxd/bl_export.py +++ /dev/null @@ -1,107 +0,0 @@ -import os -import bpy -import bmesh -import mathutils as mu -import numpy as np - -from bpy.props import (BoolProperty, - FloatProperty, - StringProperty, - EnumProperty, - ) -from bpy_extras.io_utils import (ImportHelper, - ExportHelper, - unpack_list, - unpack_face_list, - axis_conversion, - ) - -from . import format -from .. import io_binary - - -def __get_colors(mesh, color_i): - colors = np.empty(len(mesh.loops) * 4, dtype=np.float32) - layer = mesh.vertex_colors[color_i] - mesh.color_attributes[layer.name].data.foreach_get('color', colors) - colors = colors.reshape(len(mesh.loops), 4) - # colors are already linear, no need to switch color space - return colors - - -def save_vtxd(operator, context, filepath): - # Export the selected mesh - scene = context.scene - vtxd = format.VertexData() - - mesh_obs = [o for o in bpy.context.scene.objects if o.type == 'MESH'] - for ob in mesh_obs: - triangulate_object(ob) - sub_mesh = save_vtxd_sub_mesh(ob) - - vtxd.sub_meshes.append(sub_mesh) - - exportFile = os.fsencode(filepath) - if os.path.exists(exportFile): - os.remove(exportFile) - bre = io_binary.BinaryReader(open(exportFile, 'wb')) - vtxd.write(bre) - bre.close() - - return {'FINISHED'} - - -def save_vtxd_sub_mesh(blender_obj): - mesh = blender_obj.to_mesh() - sub_mesh = format.VertexDataSubMesh() - - dot_fields += [ - ('colorR', np.float32), - ('colorG', np.float32), - ('colorB', np.float32), - ('colorA', np.float32), - ] - - dots = np.empty(len(mesh.loops), dtype=np.dtype(dot_fields)) - - if len(mesh.vertex_colors) > 0: - colors = __get_colors(mesh, 0) - dots['colorR'] = colors[:, 0] - dots['colorG'] = colors[:, 1] - dots['colorB'] = colors[:, 2] - dots['colorA'] = colors[:, 3] - del colors - - # Calculate triangles and sort them into primitives. - mesh.calc_loop_triangles() - loop_indices = np.empty(len(mesh.loop_triangles) * 3, dtype=np.uint32) - mesh.loop_triangles.foreach_get('loops', loop_indices) - - prim_dots = dots[loop_indices] - - colors = np.empty((len(prim_dots), 4), dtype=np.float32) - colors[:, 0] = prim_dots['colorR'] - colors[:, 1] = prim_dots['colorG'] - colors[:, 2] = prim_dots['colorB'] - colors[:, 3] = prim_dots['colorA'] - - for i in range(len(colors)): - color = (colors[i] * 255).astype("uint8").tolist() - sub_mesh.vertexColors.append(color) - - return sub_mesh - - -# TODO: currently breaks original mesh. please fix -def triangulate_object(obj): - me = obj.data - # Get a BMesh representation - bm = bmesh.new() - bm.from_mesh(me) - - bmesh.ops.triangulate(bm, faces=bm.faces[:]) - # V2.79 : bmesh.ops.triangulate(bm, faces=bm.faces[:], quad_method=0, ngon_method=0) - - # Finish up, write the bmesh back to the mesh - bm.to_mesh(me) - bm.free() diff --git a/file_vtxd/bl_import.py b/file_vtxd/bl_import.py deleted file mode 100644 index 6b78321..0000000 --- a/file_vtxd/bl_import.py +++ /dev/null @@ -1,80 +0,0 @@ -import os -import bpy -import math -import mathutils -import numpy as np - -from bpy.props import (BoolProperty, - FloatProperty, - StringProperty, - EnumProperty, - ) -from bpy_extras.io_utils import (ImportHelper, - ExportHelper, - unpack_list, - unpack_face_list, - axis_conversion, - ) - -from . import format -from .. import io_binary - - -def load_vtxd(operator, context, filepath): - fp = os.fsencode(filepath) - file = open(fp, "rb") - br = io_binary.BinaryReader(file) - meshes = [] - - vtxd_name = bpy.path.display_name_from_filepath(filepath) - print("Start reading: " + str(vtxd_name) + "\n") - - vtxd = format.VertexData() - vtxd.read(br) - - for meshIndex in range(vtxd.num_submeshes()): - meshes.append(load_vtxd_submesh(vtxd, vtxd_name, meshIndex)) - - return meshes - - -def load_vtxd_submesh(vtxd, vtxd_name, meshIndex): - mesh = bpy.data.meshes.new(name=(str(vtxd_name) + "_" + str(meshIndex))) - - vert_locs = [] - loop_cols = [] - loop_vidxs = [] - - sub_mesh = vtxd.sub_meshes[meshIndex] - - # create a plane - height = int(math.sqrt(int(sub_mesh.num_vertices()))) - width = int(math.sqrt(int(sub_mesh.num_vertices()))) - - verts = [] - fac = [] - for indY in range(height): - for indX in range(width): - verts.append([indX * 0.01, indY * 0.01, 0]) - - for indY in range(height - 1): - for indX in range(width - 1): - target_ind = (indY * width) + indX - fac.append([target_ind, target_ind + 1, target_ind + width]) - fac.append([target_ind + 1, target_ind + width, (target_ind + width) + 1]) - - mesh.from_pydata(verts, [], fac) - - # add colors to the plane - for face in fac: - for idx in face: - col = sub_mesh.vertexColors[idx] - loop_cols.extend([col[0] / 255, col[1] / 255, col[2] / 255, col[3] / 255]) - - layer = mesh.vertex_colors.new(name='Col') - mesh.color_attributes[layer.name].data.foreach_set('color', loop_cols) - - mesh.validate() - mesh.update() - - return mesh