From 9b21b2043182a01d8f9b50e25d20fa4ec0fadf86 Mon Sep 17 00:00:00 2001 From: dafitius <70489995+dafitius@users.noreply.github.com> Date: Sun, 27 Nov 2022 17:36:18 +0100 Subject: [PATCH] Merge development to main to prepare for v0.1.0 release (#15) * Added documentation and typing to PRIM module * Refactored the vtxd module Removed the bl_import and bl_export functionality as it was did not add any real functionality to the addon. The module format is kept because it provides an easy way to implement potential vtxd features in the future. * Added comments and typing to the BORG module * Small refactor in the main init file, removed duplicate licence * Small bugfix in bl_export_prim.py * Update README.md * Remove license header from __init__.py * Add lod slider preview to README * Feature/extended UI (#13) * Added some readonly type fields * Expose more prim properties * Small bugfixes, balance changes * Added collection selector for prim exporting * Added check to see if the UVmap is set * Added batch importing of prim * Added setting of default collection when exporting prim * Fix typo * Clean up comments/descriptions Co-authored-by: Notexe <43296291+Notexe@users.noreply.github.com> * Update and add descriptions * Add release.yml GH workflow (#14) * Add release.yml GH workflow file * Remove test branch from GH workflow * Add Import-Export category to bl_info * Add doc andtracker urls to bl_info * Change version in bl_info to 0.1.0 * Correctly assign rig_resource_index when it's negative * Removed the draw destination property Co-authored-by: Notexe <43296291+Notexe@users.noreply.github.com> --- .github/workflows/release.yml | 28 ++ README.md | 38 +- __init__.py | 48 +-- file_borg/__init__.py | 29 -- file_borg/bl_import_borg.py | 85 ++--- file_borg/format.py | 7 +- file_mjba/__init__.py | 38 -- file_prim/__init__.py | 310 +++++++++++++--- file_prim/bl_export_prim.py | 281 +++++++------- file_prim/bl_import_prim.py | 116 +++--- file_prim/bl_utils_prim.py | 4 +- file_prim/format.py | 675 ++++++++++++++++++---------------- file_vtxd/__init__.py | 96 +---- file_vtxd/bl_export.py | 107 ------ file_vtxd/bl_import.py | 80 ---- 15 files changed, 979 insertions(+), 963 deletions(-) create mode 100644 .github/workflows/release.yml delete mode 100644 file_vtxd/bl_export.py delete mode 100644 file_vtxd/bl_import.py 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