diff --git a/.idea/misc.xml b/.idea/misc.xml index b158bc453..68e75f046 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/AUTHORS.rst b/AUTHORS.rst index 393e3c077..e4361b4f0 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -18,13 +18,14 @@ Contributors ------------ * Aaron1178 +* Allofich * Alphax * Arcimaestro * Arthmoor * Artorp - * Deedes * Eli2 +* enpinion * Entim * Eugenius-v * Fritz_fretz @@ -37,11 +38,13 @@ Contributors * mgm101 * opusGlass * Pacificmorrowind +* paulgreenG * Pentinen * @reddraconi * shon * @SubhadeepG * @TagnumElite +* TackYs * Tamira * Thedaywalker * Tijer diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8dd3977b0..bf8c4e8da 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,36 @@ +Version v0.1.00 +=============== + +- #576 Updates to documentation, changelog and makezip.bat (copies over generated folder from cobra-tools). +- #572 Extra development of NiMesh import and some fixes + - Fix to BSInvMarker rotation export. + - Fix for bs_data_flags setting in export - now also applied to other games where applicable, not just Skyrim. + - Fix to transform on packed collision vertices export. + - Adjust StringProperty arguments to prevent crash in Blender 3.2 or lower. + - Fix to mistake in BhkMalleableConstraint info import. + - Added processing of regions to NiMesh bone import. + - Basic DisplayList import (NiMesh with a specific type of datastream, which encodes the geometry). Bone weights for this type of nif are still unimplemented. +- #543 Use "Color" type for "InvertY" group +- #541 NiMesh import and updates for newer xml + - Nif file glob now partially dependent on xml. + - Support for NiMesh import (except those using DisplayList). + - Support for BSDynamicTriShape import. + - Update to Object properties ui (now only show relevant properties) + - Closes #533 "Unknown block type BSDynamicTriShape". + - Closes #421 "Can't import catherine classic .nif files." +- #535 Pyffi overhaul + - Change kf and nif import to make use of the new statically generated nif reading/writing library. + - Added support for SSE mesh import. + - Closes #521. +- #526 Allow setting armature axis manually +- #524 int cast in add_dummy_markers to comply with blender 3.1+'s python and check for interpolator attribute on controller before accessing it. +- #506 Speedup anim import & various other fixes + - Closes #180 + - Closes #495 + - Closes #500 + - Closes #510 + - Closes #517 + Version v0.0.14 =============== diff --git a/docs/user/features/geometry/geometry.rst b/docs/user/features/geometry/geometry.rst index ff1813f96..cb55b20dd 100644 --- a/docs/user/features/geometry/geometry.rst +++ b/docs/user/features/geometry/geometry.rst @@ -94,3 +94,15 @@ Vertex Color & Alpha * `This image should clarify per-face vertex colouring `_ * On export, the scripts will create extra vertices for different vertex colors per face. + + +.. _geometry-shapekeys: + +Shape Key Animations +-------------------- + +**Example:** + +#. :ref:`Create a mesh-object `. +#. Add relative shape keys to your mesh. +#. Keyframe each shape key's value so that the key influences the shape of the mesh at the desired time. \ No newline at end of file diff --git a/docs/user/features/object/index.rst b/docs/user/features/object/index.rst index 86f721703..6f3d27e20 100644 --- a/docs/user/features/object/index.rst +++ b/docs/user/features/object/index.rst @@ -122,8 +122,8 @@ First, we complete the object panel: #. Set your **BSX Flags**. #. Select a **Consistency Flag** from the drop-down box. See `this comment `_ for discussion of what they might do. -#. The **Object Flag** is ???????. Set it to an appropriate number. -#. The **Nif Long Name** is ???????. Set it to an appropriate string. +#. The **Object Flag** corresponds to the flags field on NiAVObjects. The exact meaning will differ based on the block type. Set it to an appropriate number. If left to 0, will use a default value. +#. The **Nif Long Name** is the actual name used for the corresponding block in the nif. You can either set it to an appropriate string, or leave it empty. In the latter case, the nif name will be determined based on the blender name of the object. #. The **Skeleton Root** determines the root bone used in this mesh's SkinInstance. Can usually be ignored / left empty, falls back to the armature object = root node. .. Extra Data and InvMarkers I have no idea how to fill them in. Help? diff --git a/install/makezip.bat b/install/makezip.bat index f89d3321f..fa6ed1499 100644 --- a/install/makezip.bat +++ b/install/makezip.bat @@ -26,11 +26,26 @@ mkdir "%DEPS%" python -m pip install "PyFFI==%PYFFI_VERSION%" --target="%DEPS%" +xcopy "%GENERATED_FOLDER%" "%DEPS%\generated" /s /q /i + xcopy "%ROOT%"\AUTHORS.rst io_scene_niftools xcopy "%ROOT%"\CHANGELOG.rst io_scene_niftools xcopy "%ROOT%"\LICENSE.rst io_scene_niftools xcopy "%ROOT%"\README.rst io_scene_niftools + +:: remove all __pycache__ folders +for /d /r %%x in (*) do if "%%~nx" == "__pycache__" rd %%x /s /q + popd -powershell -executionpolicy bypass -Command "%DIR%\zip.ps1" -source "%DIR%\temp\io_scene_niftools" -destination "%DIR%\%ZIP_NAME%.zip" -rmdir /s /q %DIR%\temp +set "COMMAND_FILE=%DIR%\zip.ps1" +set "COMMAND_FILE=%COMMAND_FILE: =` %" + +set "SOURCE_DIR=%DIR%\temp\io_scene_niftools" +set "SOURCE_DIR=%SOURCE_DIR: =` %" + +set "DESTINATION_DIR=%DIR%\%ZIP_NAME%.zip" +set "DESTINATION_DIR=%DESTINATION_DIR: =` %" + +powershell -executionpolicy bypass -Command "%COMMAND_FILE%" -source "%SOURCE_DIR%" -destination "%DESTINATION_DIR%" +rmdir /s /q "%DIR%\temp" diff --git a/io_scene_niftools/VERSION.txt b/io_scene_niftools/VERSION.txt index 67788826d..b82608c0b 100644 --- a/io_scene_niftools/VERSION.txt +++ b/io_scene_niftools/VERSION.txt @@ -1 +1 @@ -v0.0.14 \ No newline at end of file +v0.1.0 diff --git a/io_scene_niftools/__init__.py b/io_scene_niftools/__init__.py index 26f8138a1..aec39eb05 100644 --- a/io_scene_niftools/__init__.py +++ b/io_scene_niftools/__init__.py @@ -49,7 +49,7 @@ "description": "Import and export files in the NetImmerse/Gamebryo formats (.nif, .kf, .egm)", "author": "Niftools team", "blender": (2, 82, 0), - "version": (0, 0, 14), # can't read from VERSION, blender wants it hardcoded + "version": (0, 1, 0), # can't read from VERSION, blender wants it hardcoded "api": 39257, "location": "File > Import-Export", "warning": "Generally stable port of the Niftool's Blender NifScripts, many improvements, still work in progress", @@ -72,8 +72,8 @@ def locate_dependencies(): with open(os.path.join(current_dir, "VERSION.txt")) as version: NifLog.info(f"Loading: Blender Niftools Addon: {version.read()}") - import pyffi - NifLog.info(f"Loading: PyFFi: {pyffi.__version__}") + import generated.formats.nif as NifFormat + NifLog.info(f"Loading: NifFormat: {NifFormat.__xml_version__}") # todo [generated] update this and library to have actual versioning locate_dependencies() diff --git a/io_scene_niftools/file_io/kf.py b/io_scene_niftools/file_io/kf.py deleted file mode 100644 index ae71702c4..000000000 --- a/io_scene_niftools/file_io/kf.py +++ /dev/null @@ -1,69 +0,0 @@ -"""This module is used to for KeyFrame file operations""" - -# ***** BEGIN LICENSE BLOCK ***** -# -# Copyright © 2016, NIF File Format Library and Tools contributors. -# 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. -# -# * Neither the name of the NIF File Format Library and Tools -# project nor the names of its contributors may be used to endorse -# or promote products derived from this software without specific -# prior written permission. -# -# 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 OWNER 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 ***** - - -from pyffi.formats.nif import NifFormat -from io_scene_niftools.utils.logging import NifLog, NifError - - -class KFFile: - """Class to load and save a NifFile""" - - @staticmethod - def load_kf(file_path): - """Loads a Kf file from the given path""" - NifLog.info(f"Loading {file_path}") - - kf_file = NifFormat.Data() - - # open keyframe file for binary reading - with open(file_path, "rb") as kf_stream: - # check if nif file is valid - kf_file.inspect_version_only(kf_stream) - if kf_file.version >= 0: - # it is valid, so read the file - NifLog.info(f"KF file version: {kf_file.version:x}") - NifLog.info("Reading keyframe file") - kf_file.read(kf_stream) - elif kf_file.version == -1: - raise NifError("Unsupported KF version.") - else: - raise NifError("Not a KF file.") - - return kf_file diff --git a/io_scene_niftools/file_io/nif.py b/io_scene_niftools/file_io/nif.py index e72846267..156ad8e5d 100644 --- a/io_scene_niftools/file_io/nif.py +++ b/io_scene_niftools/file_io/nif.py @@ -37,8 +37,9 @@ # # ***** END LICENSE BLOCK ***** +import os.path as path -from pyffi.formats.nif import NifFormat +import generated.formats.nif as NifFormat from io_scene_niftools.utils.logging import NifLog, NifError @@ -51,18 +52,18 @@ def load_nif(file_path): """Loads a nif from the given file path""" NifLog.info(f"Importing {file_path}") - data = NifFormat.Data() + file_ext = path.splitext(file_path)[1] # open file for binary reading with open(file_path, "rb") as nif_stream: # check if nif file is valid - data.inspect_version_only(nif_stream) - if data.version >= 0: + modification, (version, user_version, bs_version) = NifFormat.NifFile.inspect_version_only(nif_stream) + if version >= 0: # it is valid, so read the file - NifLog.info(f"NIF file version: {data.version:x}") - NifLog.info("Reading file") - data.read(nif_stream) - elif data.version == -1: + NifLog.info(f"NIF file version: {version:x}") + NifLog.info(f"Reading {file_ext} file") + data = NifFormat.NifFile.from_stream(nif_stream) + elif version == -1: raise NifError("Unsupported NIF version.") else: raise NifError("Not a NIF file.") diff --git a/io_scene_niftools/kf_export.py b/io_scene_niftools/kf_export.py index dd6486969..e19df14cc 100644 --- a/io_scene_niftools/kf_export.py +++ b/io_scene_niftools/kf_export.py @@ -40,10 +40,6 @@ import os import bpy -import pyffi.spells.nif.fix - -from io_scene_niftools.file_io.kf import KFFile -from io_scene_niftools.modules.nif_export import armature from io_scene_niftools.modules.nif_export.animation.transform import TransformAnimation from io_scene_niftools.nif_common import NifCommon from io_scene_niftools.utils import math @@ -69,7 +65,7 @@ def execute(self): directory = os.path.dirname(NifOp.props.filepath) filebase, fileext = os.path.splitext(os.path.basename(NifOp.props.filepath)) - if bpy.context.scene.niftools_scene.game == 'NONE': + if bpy.context.scene.niftools_scene.game == 'UNKNOWN': raise NifError("You have not selected a game. Please select a game in the scene tab.") prefix = "x" if bpy.context.scene.niftools_scene.game in ('MORROWIND',) else "" @@ -95,6 +91,8 @@ def execute(self): # scale correction for the skeleton self.apply_scale(data, round(1 / NifOp.props.scale_correction)) + data.validate() + kffile = os.path.join(directory, prefix + filebase + ext) with open(kffile, "wb") as stream: data.write(stream) diff --git a/io_scene_niftools/kf_import.py b/io_scene_niftools/kf_import.py index 7a9aa93ad..542261242 100644 --- a/io_scene_niftools/kf_import.py +++ b/io_scene_niftools/kf_import.py @@ -39,10 +39,7 @@ import os -import pyffi.spells.nif.fix - -from io_scene_niftools.file_io.kf import KFFile -from io_scene_niftools.modules.nif_export import armature +from io_scene_niftools.file_io.nif import NifFile as KFFile from io_scene_niftools.modules.nif_import.animation.transform import TransformAnimation from io_scene_niftools.nif_common import NifCommon from io_scene_niftools.utils import math @@ -72,7 +69,7 @@ def execute(self): # get nif space bind pose of armature here for all anims self.transform_anim.get_bind_data(b_armature) for kf_file in kf_files: - kfdata = KFFile.load_kf(kf_file) + kfdata = KFFile.load_nif(kf_file) self.apply_scale(kfdata, NifOp.props.scale_correction) diff --git a/io_scene_niftools/modules/nif_export/animation/__init__.py b/io_scene_niftools/modules/nif_export/animation/__init__.py index 7f668b6df..647047a92 100644 --- a/io_scene_niftools/modules/nif_export/animation/__init__.py +++ b/io_scene_niftools/modules/nif_export/animation/__init__.py @@ -39,7 +39,7 @@ from abc import ABC import bpy -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.modules.nif_export.block_registry import block_store from io_scene_niftools.utils.singleton import NifOp, NifData @@ -93,14 +93,14 @@ def get_controllers(nodes): """find all nodes and relevant controllers""" node_kfctrls = {} for node in nodes: - if not isinstance(node, NifFormat.NiAVObject): + if not isinstance(node, NifClasses.NiAVObject): continue # get list of all controllers for this node ctrls = node.get_controllers() for ctrl in ctrls: if bpy.context.scene.niftools_scene.game == 'MORROWIND': # morrowind: only keyframe controllers - if not isinstance(ctrl, NifFormat.NiKeyframeController): + if not isinstance(ctrl, NifClasses.NiKeyframeController): continue if node not in node_kfctrls: node_kfctrls[node] = [] @@ -132,14 +132,14 @@ def create_controller(self, parent_block, target_name, priority=0): # link interpolator from the controller n_kfc.interpolator = n_kfi # if parent is a node, attach controller to that node - if isinstance(parent_block, NifFormat.NiNode): + if isinstance(parent_block, NifClasses.NiNode): parent_block.add_controller(n_kfc) if n_kfi: # set interpolator default data n_kfi.scale, n_kfi.rotation, n_kfi.translation = parent_block.get_transform().get_scale_quat_translation() # else ControllerSequence, so create a link - elif isinstance(parent_block, NifFormat.NiControllerSequence): + elif isinstance(parent_block, NifClasses.NiControllerSequence): controlled_block = parent_block.add_controlled_block() controlled_block.priority = priority # todo - pyffi adds the names to the NiStringPalette, but it creates one per controller link... @@ -159,7 +159,7 @@ def create_controller(self, parent_block, target_name, priority=0): controlled_block.controller_type = "NiTransformController" # get the parent's string palette if not parent_block.string_palette: - parent_block.string_palette = NifFormat.NiStringPalette() + parent_block.string_palette = NifClasses.NiStringPalette(NifData.data) # assign string palette to controller controlled_block.string_palette = parent_block.string_palette # add the strings and store their offsets @@ -167,7 +167,7 @@ def create_controller(self, parent_block, target_name, priority=0): controlled_block.node_name_offset = palette.add_string(controlled_block.node_name) controlled_block.controller_type_offset = palette.add_string(controlled_block.controller_type) # morrowind style - elif isinstance(parent_block, NifFormat.NiSequenceStreamHelper): + elif isinstance(parent_block, NifClasses.NiSequenceStreamHelper): # create node reference by name nodename_extra = block_store.create_block("NiStringExtraData") nodename_extra.bytes_remaining = len(target_name) + 4 @@ -184,14 +184,14 @@ def create_controller(self, parent_block, target_name, priority=0): @staticmethod def get_n_interp_from_b_interp(b_ipol): if b_ipol == "LINEAR": - return NifFormat.KeyType.LINEAR_KEY + return NifClasses.KeyType.LINEAR_KEY elif b_ipol == "BEZIER": - return NifFormat.KeyType.QUADRATIC_KEY + return NifClasses.KeyType.QUADRATIC_KEY elif b_ipol == "CONSTANT": - return NifFormat.KeyType.CONST_KEY + return NifClasses.KeyType.CONST_KEY NifLog.warn(f"Unsupported interpolation mode ({b_ipol}) in blend, using quadratic/bezier.") - return NifFormat.KeyType.QUADRATIC_KEY + return NifClasses.KeyType.QUADRATIC_KEY def add_dummy_markers(self, b_action): # if we exported animations, but no animation groups are defined, @@ -201,4 +201,4 @@ def add_dummy_markers(self, b_action): NifLog.info("Defining default action pose markers.") for frame, text in zip(b_action.frame_range, ("Idle: Start/Idle: Loop Start", "Idle: Loop Stop/Idle: Stop")): marker = b_action.pose_markers.new(text) - marker.frame = frame + marker.frame = int(frame) diff --git a/io_scene_niftools/modules/nif_export/animation/material.py b/io_scene_niftools/modules/nif_export/animation/material.py index 028eb763d..5d19487d2 100644 --- a/io_scene_niftools/modules/nif_export/animation/material.py +++ b/io_scene_niftools/modules/nif_export/animation/material.py @@ -37,11 +37,11 @@ # # ***** END LICENSE BLOCK ***** -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.modules.nif_export.animation import Animation from io_scene_niftools.modules.nif_export.block_registry import block_store -from io_scene_niftools.utils.singleton import NifOp +from io_scene_niftools.utils.singleton import NifOp, NifData from io_scene_niftools.utils.logging import NifLog @@ -71,9 +71,9 @@ def export_material_controllers(self, b_material, n_mat_prop): if not n_mat_prop: raise ValueError("Bug!! must add material property before exporting alpha controller") colors = (("alpha", None), - ("niftools.ambient_color", NifFormat.TargetColor.TC_AMBIENT), - ("diffuse_color", NifFormat.TargetColor.TC_DIFFUSE), - ("specular_color", NifFormat.TargetColor.TC_SPECULAR)) + ("niftools.ambient_color", NifClasses.MaterialColor.TC_AMBIENT), + ("diffuse_color", NifClasses.MaterialColor.TC_DIFFUSE), + ("specular_color", NifClasses.MaterialColor.TC_SPECULAR)) # the actual export for b_dtype, n_dtype in colors: self.export_material_alpha_color_controller(b_material, n_mat_prop, b_dtype, n_dtype) @@ -99,8 +99,8 @@ def export_material_alpha_color_controller(self, b_material, n_mat_prop, b_dtype # create the key data n_key_data = block_store.create_block(keydata, fcurves) n_key_data.data.num_keys = len(fcurves[0].keyframe_points) - n_key_data.data.interpolation = NifFormat.KeyType.LINEAR_KEY - n_key_data.data.keys.update_size() + n_key_data.data.interpolation = NifClasses.KeyType.LINEAR_KEY + n_key_data.data.reset_field("keys") # assumption: all curves have same amount of keys and are sampled at the same time for i, n_key in enumerate(n_key_data.data.keys): @@ -148,13 +148,13 @@ def export_uv_controller(self, b_material, n_geom): return # get the uv curves and translate them into nif data - n_uv_data = NifFormat.NiUVData() + n_uv_data = NifClasses.NiUVData(NifData.data) for fcu, n_uv_group in zip(fcurves, n_uv_data.uv_groups): if fcu: NifLog.debug(f"Exporting {fcu} as NiUVData") n_uv_group.num_keys = len(fcu.keyframe_points) - n_uv_group.interpolation = NifFormat.KeyType.LINEAR_KEY - n_uv_group.keys.update_size() + n_uv_group.interpolation = NifClasses.KeyType.LINEAR_KEY + n_uv_group.reset_field("keys") for b_point, n_key in zip(fcu.keyframe_points, n_uv_group.keys): # add each point of the curve b_frame, b_value = b_point.co @@ -167,7 +167,7 @@ def export_uv_controller(self, b_material, n_geom): # if uv data is present then add the controller so it is exported if fcurves[0].keyframe_points: - n_uv_ctrl = NifFormat.NiUVController() + n_uv_ctrl = NifClasses.NiUVController(NifData.data) self.set_flags_and_timing(n_uv_ctrl, fcurves) n_uv_ctrl.data = n_uv_data # attach block to geometry diff --git a/io_scene_niftools/modules/nif_export/animation/morph.py b/io_scene_niftools/modules/nif_export/animation/morph.py index a1e2680a4..e6c3f3a93 100644 --- a/io_scene_niftools/modules/nif_export/animation/morph.py +++ b/io_scene_niftools/modules/nif_export/animation/morph.py @@ -37,7 +37,7 @@ # # ***** END LICENSE BLOCK ***** -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from pyffi.formats.egm import EgmFormat from io_scene_niftools.modules.nif_export.animation import Animation @@ -55,10 +55,11 @@ def __init__(self): EGMData.data = None def export_morph(self, b_mesh, n_trishape, vertmap): - # shape b_key morphing + NifLog.debug(f"Checking {b_mesh.name} for shape keys") + # shape keys are only present on non-evaluated meshes! b_key = b_mesh.shape_keys if b_key and len(b_key.key_blocks) > 1: - + NifLog.debug(f"{b_mesh.name} has shape keys") # yes, there is a b_key object attached # export as egm, or as morph_data? if b_key.key_blocks[1].name.startswith("EGM"): @@ -100,25 +101,25 @@ def export_morph_animation(self, b_mesh, b_key, n_trishape, vertmap): morph_ctrl.data = morph_data morph_data.num_morphs = len(b_key.key_blocks) morph_data.num_vertices = n_trishape.data.num_vertices - morph_data.morphs.update_size() + morph_data.reset_field("morphs") # create interpolators (for newer nif versions) morph_ctrl.num_interpolators = len(b_key.key_blocks) - morph_ctrl.interpolators.update_size() + morph_ctrl.reset_field("interpolators") # interpolator weights (for Fallout 3) - morph_ctrl.interpolator_weights.update_size() + morph_ctrl.reset_field("interpolator_weights") # TODO [morph] some unknowns, bethesda only # TODO [morph] just guessing here, data seems to be zero always morph_ctrl.num_unknown_ints = len(b_key.key_blocks) - morph_ctrl.unknown_ints.update_size() + morph_ctrl.reset_field("unknown_ints") for key_block_num, key_block in enumerate(b_key.key_blocks): # export morphed vertices n_morph = morph_data.morphs[key_block_num] n_morph.frame_name = key_block.name NifLog.info(f"Exporting n_morph {key_block.name}: vertices") n_morph.arg = morph_data.num_vertices - n_morph.vectors.update_size() + n_morph.reset_field("vectors") for b_v_index, (n_v_indices, b_vert) in enumerate(list(zip(vertmap, key_block.data))): # see if this b_vert is used in the nif if not n_v_indices: @@ -139,6 +140,7 @@ def export_morph_animation(self, b_mesh, b_key, n_trishape, vertmap): # create interpolator for shape b_key (needs to be there even if there is no fcu) interpol = block_store.create_block("NiFloatInterpolator") interpol.value = 0 + # [TODO] condition this - only qualifying fields will have been reset to the correct size morph_ctrl.interpolators[key_block_num] = interpol # fallout 3 stores interpolators inside the interpolator_weights block @@ -160,9 +162,9 @@ def export_morph_animation(self, b_mesh, b_key, n_trishape, vertmap): # note: we set data on n_morph for older nifs and on floatdata for newer nifs # of course only one of these will be actually written to the file for n_data in (n_morph, n_floatdata): - n_data.interpolation = NifFormat.KeyType.LINEAR_KEY + n_data.interpolation = NifClasses.KeyType.LINEAR_KEY n_data.num_keys = len(fcurves[0].keyframe_points) - n_data.keys.update_size() + n_data.reset_field("keys") for i, b_keyframe in enumerate(fcurves[0].keyframe_points): frame, value = b_keyframe.co diff --git a/io_scene_niftools/modules/nif_export/animation/object.py b/io_scene_niftools/modules/nif_export/animation/object.py index 321b26062..4428f302a 100644 --- a/io_scene_niftools/modules/nif_export/animation/object.py +++ b/io_scene_niftools/modules/nif_export/animation/object.py @@ -38,7 +38,7 @@ # ***** END LICENSE BLOCK ***** import bpy -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.modules.nif_export.animation import Animation from io_scene_niftools.modules.nif_export.block_registry import block_store @@ -66,13 +66,13 @@ def export_visibility(self, n_node, b_action): # NiVisData = old style, NiBoolData = new style n_vis_data = block_store.create_block("NiVisData", fcurves) n_vis_data.num_keys = len(fcurves[0].keyframe_points) - n_vis_data.keys.update_size() + n_vis_data.reset_field("keys") # we just leave interpolation at constant n_bool_data = block_store.create_block("NiBoolData", fcurves) - n_bool_data.data.interpolation = NifFormat.KeyType.CONST_KEY + n_bool_data.data.interpolation = NifClasses.KeyType.CONST_KEY n_bool_data.data.num_keys = len(fcurves[0].keyframe_points) - n_bool_data.data.keys.update_size() + n_bool_data.data.reset_field("keys") for b_point, n_vis_key, n_bool_key in zip(fcurves[0].keyframe_points, n_vis_data.keys, n_bool_data.data.keys): # add each point of the curve b_frame, b_value = b_point.co diff --git a/io_scene_niftools/modules/nif_export/animation/texture.py b/io_scene_niftools/modules/nif_export/animation/texture.py index 4223ff7c7..ef82138dd 100644 --- a/io_scene_niftools/modules/nif_export/animation/texture.py +++ b/io_scene_niftools/modules/nif_export/animation/texture.py @@ -84,8 +84,7 @@ def export_flip_controller(self, fliptxt, texture, target, target_tex): # create a NiSourceTexture for each n_flip tex = TextureWriter.export_source_texture(texture, t) n_flip.num_sources += 1 - n_flip.sources.update_size() - n_flip.sources[n_flip.num_sources - 1] = tex + n_flip.sources.append(tex) count += 1 if count < 2: raise io_scene_niftools.utils.logging.NifError(f"Error in Texture Flip buffer '{fliptxt.name}': must define at least two textures") diff --git a/io_scene_niftools/modules/nif_export/animation/transform.py b/io_scene_niftools/modules/nif_export/animation/transform.py index 964660061..625b23009 100644 --- a/io_scene_niftools/modules/nif_export/animation/transform.py +++ b/io_scene_niftools/modules/nif_export/animation/transform.py @@ -40,12 +40,13 @@ import bpy import mathutils -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.modules.nif_export.animation import Animation from io_scene_niftools.modules.nif_export.block_registry import block_store from io_scene_niftools.utils import math, consts from io_scene_niftools.utils.logging import NifError, NifLog +from io_scene_niftools.utils.consts import QUAT, EULER, LOC, SCALE class TransformAnimation(Animation): @@ -106,7 +107,7 @@ def export_kf_root(self, b_armature=None): kf_root.name = b_action.name kf_root.unknown_int_1 = 1 kf_root.weight = 1.0 - kf_root.cycle_type = NifFormat.CycleType.CYCLE_CLAMP + kf_root.cycle_type = NifClasses.CycleType.CYCLE_CLAMP kf_root.frequency = 1.0 if anim_textextra.num_text_keys > 0: @@ -166,7 +167,7 @@ def export_transforms(self, parent_block, b_obj, b_action, bone=None): # matrix_local = matrix_parent_inverse * matrix_basis bind_matrix = b_obj.matrix_parent_inverse exp_fcurves = [fcu for fcu in b_action.fcurves if - fcu.data_path in ("rotation_quaternion", "rotation_euler", "location", "scale")] + fcu.data_path in (QUAT, EULER, LOC, SCALE)] else: # bone isn't keyframed in this action, nothing to do here @@ -218,9 +219,9 @@ def export_transforms(self, parent_block, b_obj, b_action, bone=None): if n_kfi: # set the default transforms of the interpolator as the bone's bind pose - n_kfi.translation.x, n_kfi.translation.y, n_kfi.translation.z = bind_trans - n_kfi.rotation.w, n_kfi.rotation.x, n_kfi.rotation.y, n_kfi.rotation.z = bind_rot.to_quaternion() - n_kfi.scale = bind_scale + n_kfi.transform.translation.x, n_kfi.transform.translation.y, n_kfi.transform.translation.z = bind_trans + n_kfi.transform.rotation.w, n_kfi.transform.rotation.x, n_kfi.transform.rotation.y, n_kfi.transform.rotation.z = bind_rot.to_quaternion() + n_kfi.transform.scale = bind_scale if max(len(c) for c in (quat_curve, euler_curve, trans_curve, scale_curve)) > 0: # number of frames is > 0, so add transform data @@ -240,19 +241,20 @@ def export_transforms(self, parent_block, b_obj, b_action, bone=None): # finally we can export the data calculated above if euler_curve: - n_kfd.rotation_type = NifFormat.KeyType.XYZ_ROTATION_KEY + n_kfd.rotation_type = NifClasses.KeyType.XYZ_ROTATION_KEY n_kfd.num_rotation_keys = 1 # *NOT* len(frames) this crashes the engine! + n_kfd.reset_field("xyz_rotations") for i, coord in enumerate(n_kfd.xyz_rotations): coord.num_keys = len(euler_curve) - coord.interpolation = NifFormat.KeyType.LINEAR_KEY - coord.keys.update_size() + coord.interpolation = NifClasses.KeyType.LINEAR_KEY + coord.reset_field("keys") for key, (frame, euler) in zip(coord.keys, euler_curve): key.time = frame / self.fps key.value = euler[i] elif quat_curve: - n_kfd.rotation_type = NifFormat.KeyType.QUADRATIC_KEY + n_kfd.rotation_type = NifClasses.KeyType.QUADRATIC_KEY n_kfd.num_rotation_keys = len(quat_curve) - n_kfd.quaternion_keys.update_size() + n_kfd.reset_field("quaternion_keys") for key, (frame, quat) in zip(n_kfd.quaternion_keys, quat_curve): key.time = frame / self.fps key.value.w = quat.w @@ -260,16 +262,16 @@ def export_transforms(self, parent_block, b_obj, b_action, bone=None): key.value.y = quat.y key.value.z = quat.z - n_kfd.translations.interpolation = NifFormat.KeyType.LINEAR_KEY + n_kfd.translations.interpolation = NifClasses.KeyType.LINEAR_KEY n_kfd.translations.num_keys = len(trans_curve) - n_kfd.translations.keys.update_size() + n_kfd.translations.reset_field("keys") for key, (frame, trans) in zip(n_kfd.translations.keys, trans_curve): key.time = frame / self.fps key.value.x, key.value.y, key.value.z = trans - n_kfd.scales.interpolation = NifFormat.KeyType.LINEAR_KEY + n_kfd.scales.interpolation = NifClasses.KeyType.LINEAR_KEY n_kfd.scales.num_keys = len(scale_curve) - n_kfd.scales.keys.update_size() + n_kfd.scales.reset_field("keys") for key, (frame, scale) in zip(n_kfd.scales.keys, scale_curve): key.time = frame / self.fps key.value = scale @@ -278,9 +280,9 @@ def create_text_keys(self, kf_root): """Create the text keys before filling in the data so that the extra data hierarchy is correct""" # add a NiTextKeyExtraData block n_text_extra = block_store.create_block("NiTextKeyExtraData", None) - if isinstance(kf_root, NifFormat.NiControllerSequence): + if isinstance(kf_root, NifClasses.NiControllerSequence): kf_root.text_keys = n_text_extra - elif isinstance(kf_root, NifFormat.NiSequenceStreamHelper): + elif isinstance(kf_root, NifClasses.NiSequenceStreamHelper): kf_root.add_extra_data(n_text_extra) return n_text_extra @@ -290,7 +292,7 @@ def export_text_keys(self, b_action, n_text_extra): self.add_dummy_markers(b_action) # create a text key for each frame descriptor n_text_extra.num_text_keys = len(b_action.pose_markers) - n_text_extra.text_keys.update_size() + n_text_extra.reset_field("text_keys") f0, f1 = b_action.frame_range for key, marker in zip(n_text_extra.text_keys, b_action.pose_markers): f = marker.frame @@ -303,9 +305,9 @@ def add_dummy_controllers(self): NifLog.info("Adding controllers and interpolators for skeleton") # note: block_store.block_to_obj changes during iteration, so need list copy for n_block in list(block_store.block_to_obj.keys()): - if isinstance(n_block, NifFormat.NiNode) and n_block.name.decode() == "Bip01": - for n_bone in n_block.tree(block_type=NifFormat.NiNode): - n_kfc, n_kfi = self.transform_anim.create_controller(n_bone, n_bone.name.decode()) + if isinstance(n_block, NifClasses.NiNode) and n_block.name == "Bip01": + for n_bone in n_block.tree(block_type=NifClasses.NiNode): + n_kfc, n_kfi = self.transform_anim.create_controller(n_bone, n_bone.name) # todo [anim] use self.nif_export.animationhelper.set_flags_and_timing n_kfc.flags = 12 n_kfc.frequency = 1.0 diff --git a/io_scene_niftools/modules/nif_export/block_registry.py b/io_scene_niftools/modules/nif_export/block_registry.py index 206420ba6..e2d8665f2 100644 --- a/io_scene_niftools/modules/nif_export/block_registry.py +++ b/io_scene_niftools/modules/nif_export/block_registry.py @@ -37,13 +37,14 @@ # # ***** END LICENSE BLOCK ***** -from pyffi.formats.nif import NifFormat +import generated.formats.nif as NifFormat import io_scene_niftools.utils.logging from io_scene_niftools.utils import math from io_scene_niftools.utils.consts import BIP_01, B_L_SUFFIX, BIP01_L, B_R_SUFFIX, BIP01_R, NPC_SUFFIX, B_L_POSTFIX, \ NPC_L, B_R_POSTFIX, BRACE_L, BRACE_R, NPC_R, OPEN_BRACKET, CLOSE_BRACKET from io_scene_niftools.utils.logging import NifLog +from io_scene_niftools.utils.singleton import NifData def replace_blender_name(name, original, replacement, open_replace, close_replace): @@ -88,7 +89,7 @@ def create_block(self, block_type, b_obj=None): @param b_obj: The Blender object. @return: The newly created block.""" try: - block = getattr(NifFormat, block_type)() + block = NifFormat.niobject_map[block_type](NifData.data) except AttributeError: raise io_scene_niftools.utils.logging.NifError(f"'{block_type}': Unknown block type (this is probably a bug).") return self.register_block(block, b_obj) diff --git a/io_scene_niftools/modules/nif_export/collision/bound.py b/io_scene_niftools/modules/nif_export/collision/bound.py index ea4d6398b..6a0a9a976 100644 --- a/io_scene_niftools/modules/nif_export/collision/bound.py +++ b/io_scene_niftools/modules/nif_export/collision/bound.py @@ -63,8 +63,7 @@ def export_bsbound(self, b_obj, block_parent): # block_parent.add_extra_data(n_bbox) # quick hack (better solution would be to make apply_scale non-recursive) block_parent.num_extra_data_list += 1 - block_parent.extra_data_list.update_size() - block_parent.extra_data_list[-1] = n_bbox + block_parent.extra_data_list.append(n_bbox) # set name, center, and dimensions n_bbox.name = "BBX" center = n_bbox.center diff --git a/io_scene_niftools/modules/nif_export/collision/havok.py b/io_scene_niftools/modules/nif_export/collision/havok.py index 9f36dcdeb..8609759b0 100644 --- a/io_scene_niftools/modules/nif_export/collision/havok.py +++ b/io_scene_niftools/modules/nif_export/collision/havok.py @@ -39,13 +39,13 @@ import bpy import mathutils -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses import io_scene_niftools.utils.logging from io_scene_niftools.modules.nif_export.block_registry import block_store from io_scene_niftools.modules.nif_export.collision import Collision from io_scene_niftools.utils import math, consts -from io_scene_niftools.utils.singleton import NifOp +from io_scene_niftools.utils.singleton import NifOp, NifData from io_scene_niftools.utils.logging import NifLog @@ -83,10 +83,11 @@ def export_collision_helper(self, b_obj, parent_block): # find physics properties/defaults # get havok material name from material name + hav_mat_type = type(NifClasses.HavokMaterial(NifData.data).material) if b_obj.data.materials: - n_havok_mat = b_obj.data.materials[0].name + n_havok_mat = hav_mat_type[b_obj.data.materials[0].name] else: - n_havok_mat = "HAV_MAT_STONE" + n_havok_mat = hav_mat_type.from_value(0) # linear_velocity = b_obj.rigid_body.deactivate_linear_velocity # angular_velocity = b_obj.rigid_body.deactivate_angular_velocity @@ -100,7 +101,7 @@ def export_collision_helper(self, b_obj, parent_block): # bhkCollisionObject -> bhkRigidBody if not parent_block.collision_object: # note: collision settings are taken from lowerclasschair01.nif - if layer == NifFormat.OblivionLayer.OL_BIPED: + if layer == NifClasses.OblivionLayer.OL_BIPED: # special collision object for creatures n_col_obj = self.export_bhk_blend_collision(b_obj) @@ -121,7 +122,7 @@ def export_collision_helper(self, b_obj, parent_block): else: n_col_body = parent_block.collision_object.body # fix total mass - n_col_body.mass += rigid_body.mass + n_col_body.rigid_body_info.mass += rigid_body.mass if coll_ispacked: self.export_collision_packed(b_obj, n_col_body, layer, n_havok_mat) @@ -135,53 +136,38 @@ def export_bhk_rigid_body(self, b_obj, n_col_obj): n_r_body = block_store.create_block("bhkRigidBody", b_obj) n_col_obj.body = n_r_body - n_r_body.layer = int(b_obj.nifcollision.collision_layer) - n_r_body.col_filter = b_obj.nifcollision.col_filter - n_r_body.unknown_short = 0 - n_r_body.unknown_int_1 = 0 - n_r_body.unknown_int_2 = 2084020722 - unk_3 = n_r_body.unknown_3_ints - unk_3[0] = 0 - unk_3[1] = 0 - unk_3[2] = 0 - n_r_body.collision_response = 1 - n_r_body.unknown_byte = 0 - n_r_body.process_contact_callback_delay = 65535 - unk_2 = n_r_body.unknown_2_shorts - unk_2[0] = 35899 - unk_2[1] = 16336 - n_r_body.layer_copy = n_r_body.layer - n_r_body.col_filter_copy = n_r_body.col_filter - # TODO [format] nif.xml update required - # ukn_6 = n_r_body.unknown_6_shorts - # ukn_6[0] = 21280 - # ukn_6[1] = 4581 - # ukn_6[2] = 62977 - # ukn_6[3] = 65535 - # ukn_6[4] = 44 - # ukn_6[5] = 0 + + n_r_body.havok_filter.layer = int(b_obj.nifcollision.collision_layer) + n_r_body.havok_filter.flags = b_obj.nifcollision.col_filter + # n_r_body.havok_filter.group = 0 + + n_r_body.entity_info.collision_response = 1 + n_r_body.rigid_body_info.collision_response = 1 + + n_r_body.rigid_body_info.havok_filter = n_r_body.havok_filter b_r_body = b_obj.rigid_body # mass is 1.0 at the moment (unless property was set on import or by the user) # will be fixed in update_rigid_bodies() - n_r_body.mass = b_r_body.mass - n_r_body.linear_damping = b_r_body.linear_damping - n_r_body.angular_damping = b_r_body.angular_damping + n_r_info = n_r_body.rigid_body_info + # TODO [format] update response type and callback delay (if relevant) + # n_r_info.collision_response = ? + # n_r_info.process_contact_callback_delay = ? + n_r_info.mass = b_r_body.mass + n_r_info.linear_damping = b_r_body.linear_damping + n_r_info.angular_damping = b_r_body.angular_damping # n_r_body.linear_velocity = linear_velocity # n_r_body.angular_velocity = angular_velocity - n_r_body.friction = b_r_body.friction - n_r_body.restitution = b_r_body.restitution - n_r_body.max_linear_velocity = b_obj.nifcollision.max_linear_velocity - n_r_body.max_angular_velocity = b_obj.nifcollision.max_angular_velocity - n_r_body.penetration_depth = b_obj.nifcollision.penetration_depth - n_r_body.motion_system = b_obj.nifcollision.motion_system - n_r_body.deactivator_type = b_obj.nifcollision.deactivator_type - n_r_body.solver_deactivation = b_obj.nifcollision.solver_deactivation - # TODO [collision][properties][ui] expose unknowns to UI & make sure to keep defaults - n_r_body.unknown_byte_1 = 1 - n_r_body.unknown_byte_2 = 1 - n_r_body.quality_type = b_obj.nifcollision.quality_type - n_r_body.unknown_int_9 = 0 + n_r_info.friction = b_r_body.friction + n_r_info.restitution = b_r_body.restitution + n_r_info.max_linear_velocity = b_obj.nifcollision.max_linear_velocity + n_r_info.max_angular_velocity = b_obj.nifcollision.max_angular_velocity + n_r_info.penetration_depth = b_obj.nifcollision.penetration_depth + n_r_info.motion_system = NifClasses.HkMotionType[b_obj.nifcollision.motion_system] + n_r_info.deactivator_type = NifClasses.HkDeactivatorType[b_obj.nifcollision.deactivator_type] + n_r_info.solver_deactivation = NifClasses.HkSolverDeactivation[b_obj.nifcollision.solver_deactivation] + n_r_info.quality_type = NifClasses.HkQualityType[b_obj.nifcollision.quality_type] + # TODO [collision] update the body flags to respond/not respond to wind return n_r_body def export_bhk_collison_object(self, b_obj): @@ -189,7 +175,7 @@ def export_bhk_collison_object(self, b_obj): col_filter = b_obj.nifcollision.col_filter n_col_obj = block_store.create_block("bhkCollisionObject", b_obj) - if layer == NifFormat.OblivionLayer.OL_ANIM_STATIC and col_filter != 128: + if layer == NifClasses.OblivionLayer.OL_ANIM_STATIC and col_filter != 128: # animated collision requires flags = 41 # unless it is a constrainted but not keyframed object n_col_obj.flags = 41 @@ -200,7 +186,6 @@ def export_bhk_collison_object(self, b_obj): def export_bhk_blend_collision(self, b_obj): n_col_obj = block_store.create_block("bhkBlendCollisionObject", b_obj) - n_col_obj.flags = 9 n_col_obj.unknown_float_1 = 1.0 n_col_obj.unknown_float_2 = 1.0 return n_col_obj @@ -219,7 +204,7 @@ def export_bhk_blend_controller(self, b_obj, parent_block): # TODO [collision] Move to collision def update_rigid_bodies(self): if bpy.context.scene.niftools_scene.game in ('OBLIVION', 'FALLOUT_3', 'SKYRIM'): - n_rigid_bodies = [n_rigid_body for n_rigid_body in block_store.block_to_obj if isinstance(n_rigid_body, NifFormat.bhkRigidBody)] + n_rigid_bodies = [n_rigid_body for n_rigid_body in block_store.block_to_obj if isinstance(n_rigid_body, NifClasses.BhkRigidBody)] # update rigid body center of gravity and mass if self.IGNORE_BLENDER_PHYSICS: @@ -252,50 +237,29 @@ def update_rigid_bodies(self): def export_bhk_mopp_bv_tree_shape(self, b_obj, n_col_body): n_col_mopp = block_store.create_block("bhkMoppBvTreeShape", b_obj) n_col_body.shape = n_col_mopp - # n_col_mopp.material = n_havok_mat[0] - unk_8 = n_col_mopp.unknown_8_bytes - unk_8[0] = 160 - unk_8[1] = 13 - unk_8[2] = 75 - unk_8[3] = 1 - unk_8[4] = 192 - unk_8[5] = 207 - unk_8[6] = 144 - unk_8[7] = 11 - n_col_mopp.unknown_float = 1.0 return n_col_mopp def export_bhk_packed_nitristrip_shape(self, b_obj, n_col_mopp): # the mopp origin, scale, and data are written later n_col_shape = block_store.create_block("bhkPackedNiTriStripsShape", b_obj) - n_col_shape.unknown_int_1 = 0 - n_col_shape.unknown_int_2 = 21929432 - n_col_shape.unknown_float_1 = 0.1 - n_col_shape.unknown_int_3 = 0 - n_col_shape.unknown_float_2 = 0 - n_col_shape.unknown_float_3 = 0.1 + # TODO [collision] radius has default of 0.1, but maybe let depend on margin scale = n_col_shape.scale scale.x = 0 scale.y = 0 scale.z = 0 - scale.unknown_float_4 = 0 + scale.w = 0 n_col_shape.scale_copy = scale n_col_mopp.shape = n_col_shape return n_col_shape - def export_bhk_convex_vertices_shape(self, b_obj, fdistlist, fnormlist, radius, vertlist): + def export_bhk_convex_vertices_shape(self, b_obj, fdistlist, fnormlist, radius, vertlist, n_havok_mat): colhull = block_store.create_block("bhkConvexVerticesShape", b_obj) - # colhull.material = n_havok_mat[0] + colhull.material.material = n_havok_mat colhull.radius = radius - unk_6 = colhull.unknown_6_floats - unk_6[2] = -0.0 # enables arrow detection - unk_6[5] = -0.0 # enables arrow detection - # note: unknown 6 floats are usually all 0 - # Vertices colhull.num_vertices = len(vertlist) - colhull.vertices.update_size() + colhull.reset_field("vertices") for vhull, vert in zip(colhull.vertices, vertlist): vhull.x = vert[0] / self.HAVOK_SCALE vhull.y = vert[1] / self.HAVOK_SCALE @@ -304,7 +268,7 @@ def export_bhk_convex_vertices_shape(self, b_obj, fdistlist, fnormlist, radius, # Normals colhull.num_normals = len(fnormlist) - colhull.normals.update_size() + colhull.reset_field("normals") for nhull, norm, dist in zip(colhull.normals, fnormlist, fdistlist): nhull.x = norm[0] nhull.y = norm[1] @@ -340,18 +304,8 @@ def export_collision_object(self, b_obj, layer, n_havok_mat): # note: collision settings are taken from lowerclasschair01.nif n_coltf = block_store.create_block("bhkConvexTransformShape", b_obj) - # n_coltf.material = n_havok_mat[0] - n_coltf.unknown_float_1 = 0.1 - - unk_8 = n_coltf.unknown_8_bytes - unk_8[0] = 96 - unk_8[1] = 120 - unk_8[2] = 53 - unk_8[3] = 19 - unk_8[4] = 24 - unk_8[5] = 9 - unk_8[6] = 253 - unk_8[7] = 4 + n_coltf.material.material = n_havok_mat + n_coltf.radius = radius hktf = math.get_object_bind(b_obj) # the translation part must point to the center of the data @@ -380,19 +334,9 @@ def export_collision_object(self, b_obj, layer, n_havok_mat): if collision_shape == 'BOX': n_colbox = block_store.create_block("bhkBoxShape", b_obj) n_coltf.shape = n_colbox - # n_colbox.material = n_havok_mat[0] + n_colbox.material.material = n_havok_mat n_colbox.radius = radius - unk_8 = n_colbox.unknown_8_bytes - unk_8[0] = 0x6b - unk_8[1] = 0xee - unk_8[2] = 0x43 - unk_8[3] = 0x40 - unk_8[4] = 0x3a - unk_8[5] = 0xef - unk_8[6] = 0x8e - unk_8[7] = 0x3e - # fix dimensions for havok coordinate system box_extends = self.calculate_box_extents(b_obj) dims = n_colbox.dimensions @@ -404,7 +348,7 @@ def export_collision_object(self, b_obj, layer, n_havok_mat): elif collision_shape == 'SPHERE': n_colsphere = block_store.create_block("bhkSphereShape", b_obj) n_coltf.shape = n_colsphere - # n_colsphere.material = n_havok_mat[0] + n_colsphere.material.material = n_havok_mat # TODO [object][collision] find out what this is: fix for havok coordinate system (6 * 7 = 42) # take average radius n_colsphere.radius = radius @@ -428,8 +372,7 @@ def export_collision_object(self, b_obj, layer, n_havok_mat): second_point /= self.HAVOK_SCALE n_col_caps = block_store.create_block("bhkCapsuleShape", b_obj) - # n_col_caps.material = n_havok_mat[0] - # n_col_caps.skyrim_material = n_havok_mat[1] + n_col_caps.material.material = n_havok_mat cap_1 = n_col_caps.first_point cap_1.x = first_point.x @@ -490,7 +433,7 @@ def export_collision_object(self, b_obj, layer, n_havok_mat): if len(fnormlist) > 65535 or len(vertlist) > 65535: raise io_scene_niftools.utils.logging.NifError("Mesh has too many polygons/vertices. Simply/split your mesh and try again.") - return self.export_bhk_convex_vertices_shape(b_obj, fdistlist, fnormlist, radius, vertlist) + return self.export_bhk_convex_vertices_shape(b_obj, fdistlist, fnormlist, radius, vertlist, n_havok_mat) else: raise io_scene_niftools.utils.logging.NifError(f'Cannot export collision type {collision_shape} to collision shape list') @@ -522,22 +465,20 @@ def export_collision_packed(self, b_obj, n_col_body, layer, n_havok_mat): transform = math.get_object_bind(b_obj) rotation = transform.decompose()[1] - vertices = [vert.co * transform for vert in b_mesh.vertices] + vertices = [transform @ vert.co for vert in b_mesh.vertices] triangles = [] normals = [] for face in b_mesh.polygons: if len(face.vertices) < 3: continue # ignore degenerate polygons triangles.append([face.vertices[i] for i in (0, 1, 2)]) - normals.append(rotation * face.normal) + normals.append(rotation @ face.normal) if len(face.vertices) == 4: triangles.append([face.vertices[i] for i in (0, 2, 3)]) - normals.append(rotation * face.normal) + normals.append(rotation @ face.normal) # TODO [collision][havok] Redo this as a material lookup - havok_mat = NifFormat.HavokMaterial() - havok_mat.material = n_havok_mat - n_col_shape.add_shape(triangles, normals, vertices, layer, havok_mat.material) + n_col_shape.add_shape(triangles, normals, vertices, layer, n_havok_mat) def export_collision_single(self, b_obj, n_col_body, layer, n_havok_mat): """Add collision object to n_col_body. @@ -559,10 +500,10 @@ def export_collision_list(self, b_obj, n_col_body, layer, n_havok_mat): if not n_col_body.shape: n_col_shape = block_store.create_block("bhkListShape") n_col_body.shape = n_col_shape - # n_col_shape.material = n_havok_mat[0] + n_col_shape.material.material = n_havok_mat else: n_col_shape = n_col_body.shape - if not isinstance(n_col_shape, NifFormat.bhkListShape): + if not isinstance(n_col_shape, NifClasses.BhkListShape): raise ValueError('Not a list of collisions') n_col_shape.add_shape(self.export_collision_object(b_obj, layer, n_havok_mat)) diff --git a/io_scene_niftools/modules/nif_export/constraint/__init__.py b/io_scene_niftools/modules/nif_export/constraint/__init__.py index b7e25322c..3415f4ab8 100644 --- a/io_scene_niftools/modules/nif_export/constraint/__init__.py +++ b/io_scene_niftools/modules/nif_export/constraint/__init__.py @@ -37,11 +37,11 @@ # # ***** END LICENSE BLOCK ***** -from pyffi.formats.nif import NifFormat - import bpy import mathutils +from generated.formats.nif import classes as NifClasses + import io_scene_niftools.utils.logging from io_scene_niftools.modules.nif_export.block_registry import block_store from io_scene_niftools.utils import math, consts @@ -78,7 +78,7 @@ def export_constraints(self, b_obj, root_block): continue # check that the object is a rigid body for otherbody, otherobj in block_store.block_to_obj.items(): - if isinstance(otherbody, NifFormat.bhkRigidBody) and otherobj is b_obj: + if isinstance(otherbody, NifClasses.BhkRigidBody) and otherobj is b_obj: hkbody = otherbody break else: @@ -121,7 +121,7 @@ def export_constraints(self, b_obj, root_block): if b_obj.niftools_constraint.LHMaxFriction != 0: max_friction = b_obj.niftools_constraint.LHMaxFriction else: - if isinstance(n_bhkconstraint, NifFormat.bhkMalleableConstraint): + if isinstance(n_bhkconstraint, NifClasses.BhkMalleableConstraint): # malleable typically have 0 (perhaps because they have a damping parameter) max_friction = 0 else: @@ -133,12 +133,11 @@ def export_constraints(self, b_obj, root_block): # parent constraint to hkbody hkbody.num_constraints += 1 - hkbody.constraints.update_size() - hkbody.constraints[-1] = n_bhkconstraint + hkbody.append(n_bhkconstraint) # export n_bhkconstraint settings n_bhkconstraint.num_entities = 2 - n_bhkconstraint.entities.update_size() + n_bhkconstraint.reset_field("entities") n_bhkconstraint.entities[0] = hkbody # is there a target? targetobj = b_constr.target @@ -147,7 +146,7 @@ def export_constraints(self, b_obj, root_block): continue # find target's bhkRigidBody for otherbody, otherobj in block_store.block_to_obj.items(): - if isinstance(otherbody, NifFormat.bhkRigidBody) and otherobj == targetobj: + if isinstance(otherbody, NifClasses.BhkRigidBody) and otherobj == targetobj: n_bhkconstraint.entities[1] = otherbody break else: @@ -157,7 +156,7 @@ def export_constraints(self, b_obj, root_block): # priority n_bhkconstraint.priority = 1 # extra malleable constraint settings - if isinstance(n_bhkconstraint, NifFormat.bhkMalleableConstraint): + if isinstance(n_bhkconstraint, NifClasses.BhkMalleableConstraint): # unknowns n_bhkconstraint.unknown_int_2 = 2 n_bhkconstraint.unknown_int_3 = 1 @@ -210,7 +209,7 @@ def export_constraints(self, b_obj, root_block): axis_y = mathutils.Vector([0, 1, 0]) * constr_matrix axis_z = mathutils.Vector([0, 0, 1]) * constr_matrix - if isinstance(n_bhkdescriptor, NifFormat.RagdollDescriptor): + if isinstance(n_bhkdescriptor, NifClasses.BhkRagdollConstraintCInfo): # z axis is the twist vector n_bhkdescriptor.twist_a.x = axis_z[0] n_bhkdescriptor.twist_a.y = axis_z[1] @@ -233,7 +232,7 @@ def export_constraints(self, b_obj, root_block): # same for maximum cone angle n_bhkdescriptor.max_friction = max_friction - elif isinstance(n_bhkdescriptor, NifFormat.LimitedHingeDescriptor): + elif isinstance(n_bhkdescriptor, NifClasses.BhkLimitedHingeConstraintCInfo): # y axis is the zero angle vector on the plane of rotation n_bhkdescriptor.perp_2_axle_in_a_1.x = axis_y[0] n_bhkdescriptor.perp_2_axle_in_a_1.y = axis_y[1] diff --git a/io_scene_niftools/modules/nif_export/geometry/mesh/__init__.py b/io_scene_niftools/modules/nif_export/geometry/mesh/__init__.py index 1924f33be..71bd92021 100644 --- a/io_scene_niftools/modules/nif_export/geometry/mesh/__init__.py +++ b/io_scene_niftools/modules/nif_export/geometry/mesh/__init__.py @@ -42,7 +42,7 @@ import numpy as np import struct -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses import io_scene_niftools.utils.logging from io_scene_niftools.modules.nif_export.geometry import mesh @@ -69,7 +69,7 @@ def export_tri_shapes(self, b_obj, n_parent, n_root, trishape_name=None): n_parent, as NiTriShape and NiTriShapeData blocks, possibly along with some NiTexturingProperty, NiSourceTexture, NiMaterialProperty, and NiAlphaProperty blocks. We export one - trishape block per mesh material. We also export vertex weights. + n_geom block per mesh material. We also export vertex weights. The parameter trishape_name passes on the name for meshes that should be exported as a single mesh. @@ -78,95 +78,98 @@ def export_tri_shapes(self, b_obj, n_parent, n_root, trishape_name=None): assert (b_obj.type == 'MESH') - # get mesh from b_obj - b_mesh = self.get_triangulated_mesh(b_obj) - b_mesh.calc_normals_split() + # get mesh from b_obj, and evaluate the mesh with modifiers applied, too + b_mesh = b_obj.data + eval_mesh = self.get_triangulated_mesh(b_obj) + eval_mesh.calc_normals_split() # getVertsFromGroup fails if the mesh has no vertices # (this happens when checking for fallout 3 body parts) # so quickly catch this (rare!) case - if not b_mesh.vertices: + if not eval_mesh.vertices: # do not export anything NifLog.warn(f"{b_obj} has no vertices, skipped.") return # get the mesh's materials, this updates the mesh material list - if not isinstance(n_parent, NifFormat.RootCollisionNode): - mesh_materials = b_mesh.materials + if not isinstance(n_parent, NifClasses.RootCollisionNode): + mesh_materials = eval_mesh.materials else: # ignore materials on collision trishapes mesh_materials = [] - # if the mesh has no materials, all face material indices should be 0, so it's ok to fake one material in the material list + # if mesh has no materials, all face material indices should be 0, so fake one material in the material list if not mesh_materials: mesh_materials = [None] # vertex color check - mesh_hasvcol = b_mesh.vertex_colors + mesh_hasvcol = eval_mesh.vertex_colors # list of body part (name, index, vertices) in this mesh - polygon_parts = self.get_polygon_parts(b_obj, b_mesh) + polygon_parts = self.get_polygon_parts(b_obj, eval_mesh) game = bpy.context.scene.niftools_scene.game # Non-textured materials, vertex colors are used to color the mesh # Textured materials, they represent lighting details - # let's now export one trishape for every mesh material + # let's now export one n_geom for every mesh material # TODO [material] needs refactoring - move material, texture, etc. to separate function - for materialIndex, b_mat in enumerate(mesh_materials): + for b_mat_index, b_mat in enumerate(mesh_materials): mesh_hasnormals = False if b_mat is not None: mesh_hasnormals = True # for proper lighting - if (game == 'SKYRIM') and b_mat.niftools_shader.slsf_1_model_space_normals: + if (game == 'SKYRIM') and b_mat.niftools_shader.model_space_normals: mesh_hasnormals = False # for proper lighting - # create a trishape block + # create a n_geom block if not NifOp.props.stripify: - trishape = block_store.create_block("NiTriShape", b_obj) + n_geom = block_store.create_block("NiTriShape", b_obj) + n_geom.data = block_store.create_block("NiTriShapeData", b_obj) else: - trishape = block_store.create_block("NiTriStrips", b_obj) + n_geom = block_store.create_block("NiTriStrips", b_obj) + n_geom.data = block_store.create_block("NiTriStripsData", b_obj) # fill in the NiTriShape's non-trivial values - if isinstance(n_parent, NifFormat.RootCollisionNode): - trishape.name = "" + if isinstance(n_parent, NifClasses.RootCollisionNode): + n_geom.name = "" else: if not trishape_name: if n_parent.name: - trishape.name = "Tri " + n_parent.name.decode() + n_geom.name = "Tri " + n_parent.name else: - trishape.name = "Tri " + b_obj.name.decode() + n_geom.name = "Tri " + b_obj.name else: - trishape.name = trishape_name + n_geom.name = trishape_name # multimaterial meshes: add material index (Morrowind's child naming convention) if len(mesh_materials) > 1: - trishape.name = f"{trishape.name.decode()}: {materialIndex}" + n_geom.name = f"{n_geom.name}: {b_mat_index}" else: - trishape.name = block_store.get_full_name(trishape) + n_geom.name = block_store.get_full_name(n_geom) - self.set_mesh_flags(b_obj, trishape) + self.set_mesh_flags(b_obj, n_geom) # extra shader for Sid Meier's Railroads if game == 'SID_MEIER_S_RAILROADS': - trishape.has_shader = True - trishape.shader_name = "RRT_NormalMap_Spec_Env_CubeLight" - trishape.unknown_integer = -1 # default + n_geom.has_shader = True + n_geom.shader_name = "RRT_NormalMap_Spec_Env_CubeLight" + n_geom.unknown_integer = -1 # default # if we have an animation of a blender mesh # an intermediate NiNode has been created which holds this b_obj's transform - # the trishape itself then needs identity transform (default) + # the n_geom itself then needs identity transform (default) if trishape_name is not None: # only export the bind matrix on trishapes that were not animated - math.set_object_matrix(b_obj, trishape) + math.set_object_matrix(b_obj, n_geom) # check if there is a parent if n_parent: - # add texture effect block (must be added as parent of the trishape) + # add texture effect block (must be added as parent of the n_geom) n_parent = self.export_texture_effect(n_parent, b_mat) # refer to this mesh in the parent's children list - n_parent.add_child(trishape) + n_parent.add_child(n_geom) - self.object_property.export_properties(b_obj, b_mat, trishape) + self.object_property.export_properties(b_obj, b_mat, n_geom) # -> now comes the real export @@ -188,9 +191,9 @@ def export_tri_shapes(self, b_obj, n_parent, n_root, trishape_name=None): # The following algorithm extracts all unique quads(vert, uv-vert, normal, vcol), # produce lists of vertices, uv-vertices, normals, vertex colors, and face indices. - mesh_uv_layers = b_mesh.uv_layers + b_uv_layers = eval_mesh.uv_layers vertquad_list = [] # (vertex, uv coordinate, normal, vertex color) list - vertex_map = [None for _ in range(len(b_mesh.vertices))] # blender vertex -> nif vertices + vertex_map = [None for _ in range(len(eval_mesh.vertices))] # blender vertex -> nif vertices vertex_positions = [] normals = [] vertex_colors = [] @@ -200,24 +203,28 @@ def export_tri_shapes(self, b_obj, n_parent, n_root, trishape_name=None): bodypartfacemap = [] polygons_without_bodypart = [] - if b_mesh.polygons: - if mesh_uv_layers: + if eval_mesh.polygons: + if b_uv_layers: # if we have uv coordinates double check that we have uv data - if not b_mesh.uv_layer_stencil: - NifLog.warn(f"No UV map for texture associated with selected mesh '{b_mesh.name}'.") + if not eval_mesh.uv_layer_stencil: + NifLog.warn(f"No UV map for texture associated with selected mesh '{eval_mesh.name}'.") use_tangents = False - if mesh_uv_layers and mesh_hasnormals: + if b_uv_layers and mesh_hasnormals: if game in ('OBLIVION', 'FALLOUT_3', 'SKYRIM') or (game in self.texture_helper.USED_EXTRA_SHADER_TEXTURES): use_tangents = True - b_mesh.calc_tangents(uvmap=mesh_uv_layers[0].name) + eval_mesh.calc_tangents(uvmap=b_uv_layers[0].name) tangents = [] bitangent_signs = [] - for poly in b_mesh.polygons: + if game in ('FALLOUT_3', 'SKYRIM'): + if len(b_uv_layers) > 1: + raise NifError(f"{game} does not support multiple UV layers.") - # does the face belong to this trishape? - if b_mat is not None and poly.material_index != materialIndex: + for poly in eval_mesh.polygons: + + # does the face belong to this n_geom? + if b_mat is not None and poly.material_index != b_mat_index: # we have a material but this face has another material, so skip continue @@ -230,25 +237,25 @@ def export_tri_shapes(self, b_obj, n_parent, n_root, trishape_name=None): f_index = [-1] * f_numverts for i, loop_index in enumerate(poly.loop_indices): - fv_index = b_mesh.loops[loop_index].vertex_index - vertex = b_mesh.vertices[fv_index] + fv_index = eval_mesh.loops[loop_index].vertex_index + vertex = eval_mesh.vertices[fv_index] vertex_index = vertex.index fv = vertex.co # smooth = vertex normal, non-smooth = face normal) if mesh_hasnormals: if poly.use_smooth: - fn = b_mesh.loops[loop_index].normal + fn = eval_mesh.loops[loop_index].normal else: fn = poly.normal else: fn = None - fuv = [uv_layer.data[loop_index].uv for uv_layer in b_mesh.uv_layers] + fuv = [uv_layer.data[loop_index].uv for uv_layer in eval_mesh.uv_layers] - # TODO [geomotry][mesh] Need to map b_verts -> n_verts + # TODO [geometry][mesh] Need to map b_verts -> n_verts if mesh_hasvcol: - f_col = list(b_mesh.vertex_colors[0].data[loop_index].color) + f_col = list(eval_mesh.vertex_colors[0].data[loop_index].color) else: f_col = None @@ -282,11 +289,11 @@ def export_tri_shapes(self, b_obj, n_parent, n_root, trishape_name=None): if mesh_hasnormals: normals.append(vertquad[2]) if use_tangents: - tangents.append(b_mesh.loops[loop_index].tangent) - bitangent_signs.append([b_mesh.loops[loop_index].bitangent_sign]) + tangents.append(eval_mesh.loops[loop_index].tangent) + bitangent_signs.append([eval_mesh.loops[loop_index].bitangent_sign]) if mesh_hasvcol: vertex_colors.append(vertquad[3]) - if mesh_uv_layers: + if b_uv_layers: uv_coords.append(vertquad[1]) # now add the (hopefully, convex) face, in triangles @@ -312,73 +319,32 @@ def export_tri_shapes(self, b_obj, n_parent, n_root, trishape_name=None): # check that there are no missing body part polygons if polygons_without_bodypart: - self.select_unassigned_polygons(b_mesh, b_obj, polygons_without_bodypart) + self.select_unassigned_polygons(eval_mesh, b_obj, polygons_without_bodypart) if len(triangles) > 65535: raise NifError("Too many polygons. Decimate your mesh and try again.") if len(vertex_positions) == 0: continue # m_4444x: skip 'empty' material indices - # add NiTriShape's data - if isinstance(trishape, NifFormat.NiTriShape): - tridata = block_store.create_block("NiTriShapeData", b_obj) - else: - tridata = block_store.create_block("NiTriStripsData", b_obj) - trishape.data = tridata - - # data - tridata.num_vertices = len(vertex_positions) - tridata.has_vertices = True - tridata.vertices.update_size() - for i, v in enumerate(tridata.vertices): - v.x, v.y, v.z = vertex_positions[i] - tridata.update_center_radius() - - if mesh_hasnormals: - tridata.has_normals = True - tridata.normals.update_size() - for i, v in enumerate(tridata.normals): - v.x, v.y, v.z = normals[i] - - if mesh_hasvcol: - tridata.has_vertex_colors = True - tridata.vertex_colors.update_size() - for i, v in enumerate(tridata.vertex_colors): - v.r, v.g, v.b, v.a = vertex_colors[i] - - if mesh_uv_layers: - if game in ('FALLOUT_3', 'SKYRIM'): - if len(mesh_uv_layers) > 1: - raise NifError(f"{game} does not support multiple UV layers.") - tridata.num_uv_sets = len(mesh_uv_layers) - tridata.bs_num_uv_sets = len(mesh_uv_layers) - tridata.has_uv = True - tridata.uv_sets.update_size() - for j, uv_layer in enumerate(mesh_uv_layers): - for i, uv in enumerate(tridata.uv_sets[j]): - if len(uv_coords[i]) == 0: - continue # skip non-uv textures - uv.u = uv_coords[i][j][0] - # NIF flips the texture V-coordinate (OpenGL standard) - uv.v = 1.0 - uv_coords[i][j][1] # opengl standard + self.set_geom_data(n_geom, vertex_positions, normals, vertex_colors, uv_coords, b_uv_layers) # set triangles stitch strips for civ4 - tridata.set_triangles(triangles, stitchstrips=NifOp.props.stitch_strips) + n_geom.data.set_triangles(triangles, stitchstrips=NifOp.props.stitch_strips) - # update tangent space (as binary extra data only for Oblivion) + # update tangent space # for extra shader texture games, only export it if those textures are actually exported # (civ4 seems to be consistent with not using tangent space on non shadered nifs) if use_tangents: if game == 'SKYRIM': - tridata.bs_num_uv_sets = tridata.bs_num_uv_sets + 4096 + n_geom.data.bs_data_flags.has_tangents = True # calculate the bitangents using the normals, tangent list and bitangent sign bitangents = bitangent_signs * np.cross(normals, tangents) # B_tan: +d(B_u), B_bit: +d(B_v) and N_tan: +d(N_v), N_bit: +d(N_u) # moreover, N_v = 1 - B_v, so d(B_v) = - d(N_v), therefore N_tan = -B_bit and N_bit = B_tan - self.add_defined_tangents(trishape, + self.add_defined_tangents(n_geom, tangents=-bitangents, bitangents=tangents, - as_extra_data=(game == 'OBLIVION')) + as_extra_data=(game == 'OBLIVION')) # as binary extra data only for Oblivion # todo [mesh/object] use more sophisticated armature finding, also taking armature modifier into account # now export the vertex weights, if there are any @@ -391,7 +357,7 @@ def export_tri_shapes(self, b_obj, n_parent, n_root, trishape_name=None): if boneinfluences: # yes we have skinning! # create new skinning instance block and link it skininst, skindata = self.create_skin_inst_data(b_obj, b_obj_armature, polygon_parts) - trishape.skin_instance = skininst + n_geom.skin_instance = skininst # Vertex weights, find weights and normalization factors vert_list = {} @@ -402,7 +368,7 @@ def export_tri_shapes(self, b_obj, n_parent, n_root, trishape_name=None): b_list_weight = [] b_vert_group = b_obj.vertex_groups[bone_group] - for b_vert in b_mesh.vertices: + for b_vert in eval_mesh.vertices: if len(b_vert.groups) == 0: # check vert has weight_groups unweighted_vertices.append(b_vert.index) continue @@ -424,13 +390,8 @@ def export_tri_shapes(self, b_obj, n_parent, n_root, trishape_name=None): self.select_unweighted_vertices(b_obj, unweighted_vertices) - # for each bone, first we get the bone block then we get the vertex weights and then we add it to the NiSkinData - # note: allocate memory for faster performance - vert_added = [False for _ in range(len(vertex_positions))] + # for each bone, get the vertex weights and add its n_node to the NiSkinData for b_bone_name in boneinfluences: - # find bone in exported blocks - bone_block = self.get_bone_block(b_obj_armature.data.bones[b_bone_name]) - # find vertex weights vert_weights = {} for v in vert_list[b_bone_name]: @@ -445,67 +406,111 @@ def export_tri_shapes(self, b_obj, n_parent, n_root, trishape_name=None): if vertex_map[v[0]] and vert_norm[v[0]]: for vert_index in vertex_map[v[0]]: vert_weights[vert_index] = v[1] / vert_norm[v[0]] - vert_added[vert_index] = True # add bone as influence, but only if there were actually any vertices influenced by the bone if vert_weights: - trishape.add_bone(bone_block, vert_weights) + # find bone in exported blocks + n_node = self.get_bone_block(b_obj_armature.data.bones[b_bone_name]) + n_geom.add_bone(n_node, vert_weights) + del vert_weights # update bind position skinning data - # trishape.update_bind_position() - # override pyffi trishape.update_bind_position with custom one that is relative to the nif root - self.update_bind_position(trishape, n_root, b_obj_armature) + # n_geom.update_bind_position() + # override pyffi n_geom.update_bind_position with custom one that is relative to the nif root + self.update_bind_position(n_geom, n_root, b_obj_armature) # calculate center and radius for each skin bone data block - trishape.update_skin_center_radius() - - if NifData.data.version >= 0x04020100 and NifOp.props.skin_partition: - NifLog.info("Creating skin partition") - - # warn on bad config settings - if game == 'OBLIVION': - if NifOp.props.pad_bones: - NifLog.warn("Using padbones on Oblivion export. Disable the pad bones option to get higher quality skin partitions.") - if game in ('OBLIVION', 'FALLOUT_3'): - if NifOp.props.max_bones_per_partition < 18: - NifLog.warn("Using less than 18 bones per partition on Oblivion/Fallout 3 export." - "Set it to 18 to get higher quality skin partitions.") - elif NifOp.props.max_bones_per_partition > 18: - NifLog.warn("Using more than 18 bones per partition on Oblivion/Fallout 3 export." - "This may cause issues in-game.") - if game == 'SKYRIM': - if NifOp.props.max_bones_per_partition < 24: - NifLog.warn("Using less than 24 bones per partition on Skyrim export." - "Set it to 24 to get higher quality skin partitions.") - # Skyrim Special Edition has a limit of 80 bones per partition, but export is not yet supported - - part_order = [getattr(NifFormat.BSDismemberBodyPartType, face_map.name, None) for face_map in b_obj.face_maps] - part_order = [body_part for body_part in part_order if body_part is not None] - # override pyffi trishape.update_skin_partition with custom one (that allows ordering) - trishape.update_skin_partition = update_skin_partition.__get__(trishape) - lostweight = trishape.update_skin_partition( - maxbonesperpartition=NifOp.props.max_bones_per_partition, - maxbonespervertex=NifOp.props.max_bones_per_vertex, - stripify=NifOp.props.stripify, - stitchstrips=NifOp.props.stitch_strips, - padbones=NifOp.props.pad_bones, - triangles=triangles, - trianglepartmap=bodypartfacemap, - maximize_bone_sharing=(game in ('FALLOUT_3', 'SKYRIM')), - part_sort_order=part_order) - - if lostweight > NifOp.props.epsilon: - NifLog.warn(f"Lost {lostweight:f} in vertex weights while creating a skin partition for Blender object '{b_obj.name}' (nif block '{trishape.name}')") - - # clean up - del vert_weights - del vert_added + n_geom.update_skin_center_radius() + + self.export_skin_partition(b_obj, bodypartfacemap, triangles, n_geom) # fix data consistency type - tridata.consistency_flags = b_obj.niftools.consistency_flags + n_geom.data.consistency_flags = NifClasses.ConsistencyType[b_obj.niftools.consistency_flags] # export EGM or NiGeomMorpherController animation - self.morph_anim.export_morph(b_mesh, trishape, vertex_map) - return trishape + # shape keys are only present on the raw, unevaluated mesh + self.morph_anim.export_morph(b_mesh, n_geom, vertex_map) + return n_geom + + def set_geom_data(self, n_geom, vertex_positions, normals, vertex_colors, uv_coords, b_uv_layers): + """Sets flat lists of per-vertex data to n_geom""" + # coords + n_geom.data.num_vertices = len(vertex_positions) + n_geom.data.has_vertices = True + n_geom.data.reset_field("vertices") + for n_v, b_v in zip(n_geom.data.vertices, vertex_positions): + n_v.x, n_v.y, n_v.z = b_v + n_geom.data.update_center_radius() + # normals + n_geom.data.has_normals = bool(normals) + n_geom.data.reset_field("normals") + for n_v, b_v in zip(n_geom.data.normals, normals): + n_v.x, n_v.y, n_v.z = b_v + # vertex_colors + n_geom.data.has_vertex_colors = bool(vertex_colors) + n_geom.data.reset_field("vertex_colors") + for n_v, b_v in zip(n_geom.data.vertex_colors, vertex_colors): + n_v.r, n_v.g, n_v.b, n_v.a = b_v + # uv_sets + if bpy.context.scene.niftools_scene.nif_version == 0x14020007 and bpy.context.scene.niftools_scene.user_version_2: + data_flags = n_geom.data.bs_data_flags + else: + data_flags = n_geom.data.data_flags + data_flags.has_uv = bool(b_uv_layers) + if hasattr(data_flags, "num_uv_sets"): + data_flags.num_uv_sets = len(b_uv_layers) + else: + if len(b_uv_layers) > 1: + NifLog.warn(f"More than one UV layers for game that doesn't support it, only using first UV layer") + n_geom.data.reset_field("uv_sets") + for j, n_uv_set in enumerate(n_geom.data.uv_sets): + for i, n_uv in enumerate(n_uv_set): + if len(uv_coords[i]) == 0: + continue # skip non-uv textures + n_uv.u = uv_coords[i][j][0] + # NIF flips the texture V-coordinate (OpenGL standard) + n_uv.v = 1.0 - uv_coords[i][j][1] # opengl standard + + def export_skin_partition(self, b_obj, bodypartfacemap, triangles, n_geom): + """Attaches a skin partition to n_geom if needed""" + game = bpy.context.scene.niftools_scene.game + if NifData.data.version >= 0x04020100 and NifOp.props.skin_partition: + NifLog.info("Creating skin partition") + + # warn on bad config settings + if game == 'OBLIVION': + if NifOp.props.pad_bones: + NifLog.warn( + "Using padbones on Oblivion export. Disable the pad bones option to get higher quality skin partitions.") + + # Skyrim Special Edition has a limit of 80 bones per partition, but export is not yet supported + bones_per_partition_lut = {"OBLIVION": 18, "FALLOUT_3": 18, "SKYRIM": 24} + rec_bones = bones_per_partition_lut.get(game, None) + if rec_bones is not None: + if NifOp.props.max_bones_per_partition < rec_bones: + NifLog.warn(f"Using less than {rec_bones} bones per partition on {game} export." + f"Set it to {rec_bones} to get higher quality skin partitions.") + elif NifOp.props.max_bones_per_partition > rec_bones: + NifLog.warn(f"Using more than {rec_bones} bones per partition on {game} export." + f"This may cause issues in-game.") + + part_order = [NifClasses.BSDismemberBodyPartType[face_map.name] for face_map in + b_obj.face_maps if face_map.name in NifClasses.BSDismemberBodyPartType.__members__] + # override pyffi n_geom.update_skin_partition with custom one (that allows ordering) + n_geom.update_skin_partition = update_skin_partition.__get__(n_geom) + lostweight = n_geom.update_skin_partition( + maxbonesperpartition=NifOp.props.max_bones_per_partition, + maxbonespervertex=NifOp.props.max_bones_per_vertex, + stripify=NifOp.props.stripify, + stitchstrips=NifOp.props.stitch_strips, + padbones=NifOp.props.pad_bones, + triangles=triangles, + trianglepartmap=bodypartfacemap, + maximize_bone_sharing=(game in ('FALLOUT_3', 'SKYRIM')), + part_sort_order=part_order) + + if lostweight > NifOp.props.epsilon: + NifLog.warn( + f"Lost {lostweight:f} in vertex weights while creating a skin partition for Blender object '{b_obj.name}' (nif block '{n_geom.name}')") def update_bind_position(self, n_geom, n_root, b_obj_armature): """Transfer the Blender bind position to the nif bind position. @@ -550,7 +555,7 @@ def update_bind_position(self, n_geom, n_root, b_obj_armature): def get_bone_block(self, b_bone): """For a blender bone, return the corresponding nif node from the blocks that have already been exported""" for n_block, b_obj in block_store.block_to_obj.items(): - if isinstance(n_block, NifFormat.NiNode) and b_bone == b_obj: + if isinstance(n_block, NifClasses.NiNode) and b_bone == b_obj: return n_block raise NifError(f"Bone '{b_bone.name}' not found.") @@ -558,10 +563,10 @@ def get_polygon_parts(self, b_obj, b_mesh): """Returns the body part indices of the mesh polygons. -1 is either not assigned to a face map or not a valid body part""" index_group_map = {-1: -1} - for bodypartgroupname in NifFormat.BSDismemberBodyPartType().get_editor_keys(): + for bodypartgroupname in [member.name for member in NifClasses.BSDismemberBodyPartType]: face_map = b_obj.face_maps.get(bodypartgroupname) if face_map: - index_group_map[face_map.index] = getattr(NifFormat.BSDismemberBodyPartType, bodypartgroupname) + index_group_map[face_map.index] = NifClasses.BSDismemberBodyPartType[bodypartgroupname] if len(index_group_map) <= 1: # there were no valid face maps return [] @@ -587,8 +592,8 @@ def create_skin_inst_data(self, b_obj, b_obj_armature, polygon_parts): n_root_name = block_store.get_full_name(b_obj_armature) # make sure that such a block exists, find it for block in block_store.block_to_obj: - if isinstance(block, NifFormat.NiNode): - if block.name.decode() == n_root_name: + if isinstance(block, NifClasses.NiNode): + if block.name == n_root_name: skininst.skeleton_root = block break else: @@ -728,7 +733,7 @@ def export_texture_effect(self, n_block, b_mat): extra_node.rotation.set_identity() extra_node.scale = 1.0 extra_node.flags = 0x000C # morrowind - # create texture effect block and parent the texture effect and trishape to it + # create texture effect block and parent the texture effect and n_geom to it texeff = self.texture_helper.export_texture_effect(ref_mtex) extra_node.add_child(texeff) extra_node.add_effect(texeff) @@ -736,6 +741,7 @@ def export_texture_effect(self, n_block, b_mat): return n_block def get_triangulated_mesh(self, b_obj): + # TODO [geometry][mesh] triangulation could also be done using loop_triangles, without a modifier # get the armature influencing this mesh, if it exists b_armature_obj = b_obj.find_armature() if b_armature_obj: @@ -761,40 +767,34 @@ def ensure_tri_modifier(self, b_obj): else: b_obj.modifiers.new('Triangulate', 'TRIANGULATE') - def add_defined_tangents(self, trishape, tangents, bitangents, as_extra_data): + def add_defined_tangents(self, n_geom, tangents, bitangents, as_extra_data): # check if size of tangents and bitangents is equal to num_vertices - if not (len(tangents) == len(bitangents) == trishape.data.num_vertices): - raise NifError(f'Number of tangents or bitangents does not agree with number of vertices in {trishape.name}') + if not (len(tangents) == len(bitangents) == n_geom.data.num_vertices): + raise NifError(f'Number of tangents or bitangents does not agree with number of vertices in {n_geom.name}') if as_extra_data: # if tangent space extra data already exists, use it # find possible extra data block - for extra in trishape.get_extra_datas(): - if isinstance(extra, NifFormat.NiBinaryExtraData): - if extra.name == b'Tangent space (binormal & tangent vectors)': + extra_name = 'Tangent space (binormal & tangent vectors)' + for extra in n_geom.get_extra_datas(): + if isinstance(extra, NifClasses.NiBinaryExtraData): + if extra.name == extra_name: break else: - extra = None - if not extra: - # otherwise, create a new block and link it - extra = NifFormat.NiBinaryExtraData() - extra.name = b'Tangent space (binormal & tangent vectors)' - trishape.add_extra_data(extra) - + # create a new block and link it + extra = NifClasses.NiBinaryExtraData(NifData.data) + extra.name = extra_name + n_geom.add_extra_data(extra) # write the data extra.binary_data = np.concatenate((tangents, bitangents), axis=0).astype(' root collision node (can be mesh or empty) - # self.nif_export.collisionhelper.export_collision(b_obj, n_parent) + # self.export_collision(b_obj, n_parent) # return None # done; stop here self.n_root = None # there is only one root object so that will be our final root if len(root_objects) == 1: b_obj = root_objects[0] - self.export_node(b_obj, None) + self.export_node(b_obj, None, n_node_type=b_obj.niftools.nodetype) # there is more than one root object so we create a meta root else: - NifLog.info("Created meta root because blender scene had multiple root objects") + NifLog.info(f"Created meta root because blender scene had {len(root_objects)} root objects") self.n_root = types.create_ninode() self.n_root.name = "Scene Root" for b_obj in root_objects: self.export_node(b_obj, self.n_root) - # TODO [object] How dow we know we are selecting the right node in the case of multi-root? - # making root block a fade node - root_type = b_obj.niftools.rootnode - if bpy.context.scene.niftools_scene.game in ('FALLOUT_3', 'SKYRIM') and root_type == 'BSFadeNode': - NifLog.info("Making root block a BSFadeNode") - fade_root_block = NifFormat.BSFadeNode().deepcopy(self.n_root) - fade_root_block.replace_global_node(self.n_root, fade_root_block) - self.n_root = fade_root_block - # various extra datas object_property = ObjectDataProperty() object_property.export_bsxflags_upb(self.n_root, root_objects) - object_property.export_inventory_marker(self.n_root, root_objects) + object_property.export_inventory_marker(self.n_root, b_obj) object_property.export_weapon_location(self.n_root, b_obj) types.export_furniture_marker(self.n_root, filebase) return self.n_root @@ -147,7 +136,7 @@ def set_node_flags(self, b_obj, n_node): else: n_node.flags = 0x000C # morrowind - def export_node(self, b_obj, n_parent): + def export_node(self, b_obj, n_parent, n_node_type=None): """Export a mesh/armature/empty object b_obj as child of n_parent. Export also all children of b_obj. @@ -190,7 +179,7 @@ def export_node(self, b_obj, n_parent): b_action = False # -> everything else (empty/armature) is a (more or less regular) node - node = types.create_ninode(b_obj) + node = types.create_ninode(b_obj, n_node_type=n_node_type) # set parenting here so that it can be accessed if not self.n_root: self.n_root = node @@ -207,32 +196,31 @@ def export_node(self, b_obj, n_parent): # export object animation self.transform_anim.export_transforms(node, b_obj, b_action) self.object_anim.export_visibility(node, b_action) - # if it is a mesh, export the mesh as trishape children of this ninode + # if it is a mesh, export the mesh as n_geom children of this ninode if b_obj.type == 'MESH': return self.mesh_helper.export_tri_shapes(b_obj, node, self.n_root) # if it is an armature, export the bones as ninode children of this ninode elif b_obj.type == 'ARMATURE': self.armaturehelper.export_bones(b_obj, node) - - # export all children of this b_obj as children of this NiNode - self.export_children(b_obj, node) + # special case: objects parented to armature bones + for b_child in b_obj.children: + # find and attach to the right node + if b_child.parent_bone: + b_obj_bone = b_obj.data.bones[b_child.parent_bone] + # find the correct n_node + # todo [object] this is essentially the same as Mesh.get_bone_block() + n_node = [k for k, v in block_store.block_to_obj.items() if v == b_obj_bone][0] + self.export_node(b_child, n_node) + # just child of the armature itself, so attach to armature root + else: + self.export_node(b_child, node) + else: + # export all children of this empty object as children of this node + for b_child in b_obj.children: + self.export_node(b_child, node) return node - def export_children(self, b_parent, n_parent): - """Export all children of blender object b_parent as children of n_parent.""" - # loop over all obj's children - for b_child in b_parent.children: - temp_parent = n_parent - # special case: objects parented to armature bones - find the nif parent bone - if b_parent.type == 'ARMATURE' and b_child.parent_bone != "": - parent_bone = b_parent.data.bones[b_child.parent_bone] - assert (parent_bone in block_store.block_to_obj.values()) - for temp_parent, obj in block_store.block_to_obj.items(): - if obj == parent_bone: - break - self.export_node(b_child, temp_parent) - def export_collision(self, b_obj, n_parent): """Main function for adding collision object b_obj to a node. Returns True if this object is exported as a collision""" @@ -259,7 +247,7 @@ def export_collision(self, b_obj, n_parent): else: # all nodes failed so add new one node = types.create_ninode(b_obj) - node.name = 'collisiondummy{:d}'.format(n_parent.num_children) + node.name = f'collisiondummy{n_parent.num_children}' if b_obj.niftools.flags != 0: node_flag_hex = hex(b_obj.niftools.flags) else: @@ -273,4 +261,4 @@ def export_collision(self, b_obj, n_parent): else: NifLog.warn(f"Collisions not supported for game '{bpy.context.scene.niftools_scene.game}', skipped collision object '{b_obj.name}'") - return True \ No newline at end of file + return True diff --git a/io_scene_niftools/modules/nif_export/property/material/__init__.py b/io_scene_niftools/modules/nif_export/property/material/__init__.py index c60343f4f..fa0d25da3 100644 --- a/io_scene_niftools/modules/nif_export/property/material/__init__.py +++ b/io_scene_niftools/modules/nif_export/property/material/__init__.py @@ -39,11 +39,11 @@ import bpy -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.modules.nif_export.animation.material import MaterialAnimation from io_scene_niftools.modules.nif_export.block_registry import block_store -from io_scene_niftools.utils.singleton import NifOp +from io_scene_niftools.utils.singleton import NifOp, NifData from io_scene_niftools.utils.logging import NifLog EXPORT_OPTIMIZE_MATERIALS = True @@ -62,7 +62,7 @@ def export_material_property(self, b_mat, flags=0x0001): return name = block_store.get_full_name(b_mat) # create n_block - n_mat_prop = NifFormat.NiMaterialProperty() + n_mat_prop = NifClasses.NiMaterialProperty(NifData.data) # list which determines whether the material name is relevant or not only for particular names this holds, # such as EnvMap2 by default, the material name does not affect rendering @@ -108,7 +108,7 @@ def export_material_property(self, b_mat, flags=0x0001): # search for duplicate # (ignore the name string as sometimes import needs to create different materials even when NiMaterialProperty is the same) for n_block in block_store.block_to_obj: - if not isinstance(n_block, NifFormat.NiMaterialProperty): + if not isinstance(n_block, NifClasses.NiMaterialProperty): continue # when optimization is enabled, ignore material name diff --git a/io_scene_niftools/modules/nif_export/property/object/__init__.py b/io_scene_niftools/modules/nif_export/property/object/__init__.py index 844908b3f..18691a92b 100644 --- a/io_scene_niftools/modules/nif_export/property/object/__init__.py +++ b/io_scene_niftools/modules/nif_export/property/object/__init__.py @@ -40,12 +40,11 @@ import bpy -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.modules.nif_export.property.material import MaterialProp from io_scene_niftools.modules.nif_export.property.shader import BSShaderProperty from io_scene_niftools.modules.nif_export.property.texture.types.nitextureprop import NiTextureProp -from io_scene_niftools.properties.object import PRN_DICT from io_scene_niftools.modules.nif_export.block_registry import block_store from io_scene_niftools.utils.consts import UPB_DEFAULT from io_scene_niftools.utils.singleton import NifOp @@ -71,7 +70,8 @@ def export_properties(self, b_obj, b_mat, n_block): self.export_specular_property(b_mat), self.material_property.export_material_property(b_mat) ): - n_block.add_property(prop) + if prop is not None: + n_block.add_property(prop) # todo [property] refactor this # add textures @@ -85,8 +85,7 @@ def export_properties(self, b_obj, b_mat, n_block): block_store.register_block(bsshader) # TODO [pyffi] Add helper function to allow adding bs_property / general list addition - n_block.bs_properties[0] = bsshader - n_block.bs_properties.update_size() + n_block.shader_property = bsshader else: if bpy.context.scene.niftools_scene.game in self.texture_helper.USED_EXTRA_SHADER_TEXTURES: @@ -220,21 +219,19 @@ def has_collision(): # TODO [object][property] Move to object property @staticmethod - def export_inventory_marker(n_root, root_objects): - if bpy.context.scene.niftools_scene.game in ('SKYRIM',): - for root_object in root_objects: - if root_object.niftools_bs_invmarker: - for extra_item in n_root.extra_data_list: - if isinstance(extra_item, NifFormat.BSInvMarker): - raise NifError("Multiple Items have Inventory marker data only one item may contain this data") - else: - n_extra_list = NifFormat.BSInvMarker() - n_extra_list.name = root_object.niftools_bs_invmarker[0].name.encode() - n_extra_list.rotation_x = (-root_object.niftools_bs_invmarker[0].bs_inv_x % (2 * pi)) * 1000 - n_extra_list.rotation_y = (-root_object.niftools_bs_invmarker[0].bs_inv_y % (2 * pi)) * 1000 - n_extra_list.rotation_z = (-root_object.niftools_bs_invmarker[0].bs_inv_z % (2 * pi)) * 1000 - n_extra_list.zoom = root_object.niftools_bs_invmarker[0].bs_inv_zoom - n_root.add_extra_data(n_extra_list) + def export_inventory_marker(n_root, b_obj): + """Attaches a BSInvMarker to n_root if desired and fill in its values""" + niftools_scene = bpy.context.scene.niftools_scene + bs_inv_store = b_obj.niftools.bs_inv + if niftools_scene.game in ('SKYRIM',) and bs_inv_store: + bs_inv = bs_inv_store[0] + n_bs_inv_marker = NifClasses.BSInvMarker(n_root.context) + n_bs_inv_marker.name = bs_inv.name + n_bs_inv_marker.rotation_x = round((-bs_inv.x % (2 * pi)) * 1000) + n_bs_inv_marker.rotation_y = round((-bs_inv.y % (2 * pi)) * 1000) + n_bs_inv_marker.rotation_z = round((-bs_inv.z % (2 * pi)) * 1000) + n_bs_inv_marker.zoom = bs_inv.zoom + n_root.add_extra_data(n_bs_inv_marker) # TODO [object][property] Move to new object type def export_weapon_location(self, n_root, root_obj): @@ -242,11 +239,10 @@ def export_weapon_location(self, n_root, root_obj): game = bpy.context.scene.niftools_scene.game if game in ('OBLIVION', 'FALLOUT_3', 'SKYRIM'): loc = root_obj.niftools.prn_location - if loc != "NONE" and (PRN_DICT[loc][game] is not None): - # add string extra data + if loc: prn = block_store.create_block("NiStringExtraData") prn.name = 'Prn' - prn.string_data = PRN_DICT[loc][game] + prn.string_data = loc n_root.add_extra_data(prn) # TODO [object][property] Move to object property diff --git a/io_scene_niftools/modules/nif_export/property/shader/__init__.py b/io_scene_niftools/modules/nif_export/property/shader/__init__.py index 39cd78941..ba3e239b5 100644 --- a/io_scene_niftools/modules/nif_export/property/shader/__init__.py +++ b/io_scene_niftools/modules/nif_export/property/shader/__init__.py @@ -36,12 +36,13 @@ # POSSIBILITY OF SUCH DAMAGE. # # ***** END LICENSE BLOCK ***** -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses import io_scene_niftools.utils.logging from io_scene_niftools.modules.nif_export.property.texture.types.bsshadertexture import BSShaderTexture from io_scene_niftools.utils import math from io_scene_niftools.utils.consts import FLOAT_MAX +from io_scene_niftools.utils.singleton import NifData class BSShaderProperty: @@ -70,7 +71,7 @@ def export_bs_shader_property(self, b_mat=None): return bsshader def export_bs_effect_shader_property(self, b_mat): - bsshader = NifFormat.BSEffectShaderProperty() + bsshader = NifClasses.BSEffectShaderProperty(NifData.data) self.texturehelper.export_bs_effect_shader_prop_textures(bsshader) @@ -80,10 +81,10 @@ def export_bs_effect_shader_property(self, b_mat): # bsshader.alpha = (1 - b_mat.alpha) # Emissive - BSShaderProperty.set_color3_property(bsshader.emissive_color, b_mat.niftools.emissive_color) - bsshader.emissive_color.a = b_mat.niftools.emissive_alpha.v + BSShaderProperty.set_color3_property(bsshader.base_color, b_mat.niftools.emissive_color) + bsshader.base_color.a = b_mat.niftools.emissive_alpha.v # TODO [shader] Expose a emission multiplier value - # bsshader.emissive_multiple = b_mat.emit + # bsshader.base_color_scale = b_mat.emit # Shader Flags BSShaderProperty.export_shader_flags(b_mat, bsshader) @@ -91,18 +92,18 @@ def export_bs_effect_shader_property(self, b_mat): return bsshader def export_bs_lighting_shader_property(self, b_mat): - bsshader = NifFormat.BSLightingShaderProperty() - b_s_type = NifFormat.BSLightingShaderPropertyShaderType._enumkeys.index(b_mat.niftools_shader.bslsp_shaderobjtype) - bsshader.skyrim_shader_type = NifFormat.BSLightingShaderPropertyShaderType._enumvalues[b_s_type] + bsshader = NifClasses.BSLightingShaderProperty(NifData.data) + b_s_type = NifClasses.BSLightingShaderType[b_mat.niftools_shader.bslsp_shaderobjtype] + bsshader.skyrim_shader_type = NifClasses.BSLightingShaderType[b_mat.niftools_shader.bslsp_shaderobjtype] self.texturehelper.export_bs_lighting_shader_prop_textures(bsshader) # Diffuse color d = b_mat.diffuse_color - if b_s_type == NifFormat.BSLightingShaderPropertyShaderType["Skin Tint"]: + if b_s_type == NifClasses.BSLightingShaderType.SKIN_TINT: BSShaderProperty.set_color3_property(bsshader.skin_tint_color, d) - elif b_s_type == NifFormat.BSLightingShaderPropertyShaderType["Hair Tint"]: + elif b_s_type == NifClasses.BSLightingShaderType.HAIR_TINT: BSShaderProperty.set_color3_property(bsshader.hair_tint_color, d) # TODO [shader] expose intensity value # b_mat.diffuse_intensity = 1.0 @@ -132,11 +133,10 @@ def export_bs_lighting_shader_property(self, b_mat): return bsshader def export_bs_shader_pp_lighting_property(self, b_mat): - bsshader = NifFormat.BSShaderPPLightingProperty() + bsshader = NifClasses.BSShaderPPLightingProperty(NifData.data) # set shader options # TODO: FIXME: - b_s_type = NifFormat.BSShaderType._enumkeys.index(b_mat.niftools_shader.bsspplp_shaderobjtype) - bsshader.shader_type = NifFormat.BSShaderType._enumvalues[b_s_type] + bsshader.shader_type = NifClasses.BSShaderType[b_mat.niftools_shader.bsspplp_shaderobjtype] self.texturehelper.export_bs_shader_pp_lighting_prop_textures(bsshader) @@ -163,12 +163,13 @@ def export_shader_flags(b_mat, shader): @staticmethod def process_flags(b_mat, flags): b_flag_list = b_mat.niftools_shader.bl_rna.properties.keys() - for sf_flag in flags._names: + for sf_flag in flags.__members__: if sf_flag in b_flag_list: b_flag = b_mat.niftools_shader.get(sf_flag) if b_flag: - sf_flag_index = flags._names.index(sf_flag) - flags._items[sf_flag_index]._value = 1 + setattr(flags, sf_flag, True) + else: + setattr(flags, sf_flag, False) @staticmethod def set_color3_property(n_property, b_color): diff --git a/io_scene_niftools/modules/nif_export/property/texture/types/bsshadertexture.py b/io_scene_niftools/modules/nif_export/property/texture/types/bsshadertexture.py index 4e61c96bc..a1ea862c6 100644 --- a/io_scene_niftools/modules/nif_export/property/texture/types/bsshadertexture.py +++ b/io_scene_niftools/modules/nif_export/property/texture/types/bsshadertexture.py @@ -36,10 +36,11 @@ # POSSIBILITY OF SUCH DAMAGE. # # ***** END LICENSE BLOCK ***** -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.modules.nif_export.property.texture import TextureWriter, TextureSlotManager from io_scene_niftools.utils.consts import TEX_SLOTS +from io_scene_niftools.utils.singleton import NifData class BSShaderTexture(TextureSlotManager): @@ -78,7 +79,9 @@ def export_bs_lighting_shader_prop_textures(self, bsshader): # Add in extra texture slots texset.num_textures = 9 - texset.textures.update_size() + existing_textures = texset.textures[:] + texset.reset_field("textures") + texset.textures[:len(existing_textures)] = existing_textures if self.slots[TEX_SLOTS.DECAL_0]: texset.textures[6] = TextureWriter.export_texture_filename(self.slots[TEX_SLOTS.DECAL_0]) @@ -93,7 +96,7 @@ def export_bs_shader_pp_lighting_prop_textures(self, bsshader): bsshader.texture_set = self._create_textureset() def _create_textureset(self): - texset = NifFormat.BSShaderTextureSet() + texset = NifClasses.BSShaderTextureSet(NifData.data) if self.slots[TEX_SLOTS.BASE]: texset.textures[0] = TextureWriter.export_texture_filename(self.slots[TEX_SLOTS.BASE]) @@ -141,7 +144,7 @@ def export_uv_transform(self, shader): if hasattr(shader, 'texture_clamp_mode'): if self.slots[TEX_SLOTS.BASE] and (self.slots[TEX_SLOTS.BASE].extension == "CLIP"): # if the extension is clip, we know the wrap mode is clamp for both, - shader.texture_clamp_mode = (shader.texture_clamp_mode - shader.texture_clamp_mode % 256) + NifFormat.TexClampMode.CLAMP_S_CLAMP_T + shader.texture_clamp_mode = NifClasses.TexClampMode.CLAMP_S_CLAMP_T else: # otherwise, look at the given clip modes from the nodes if not clamp_x: @@ -152,6 +155,6 @@ def export_uv_transform(self, shader): wrap_t = 1 else: wrap_t = 0 - shader.texture_clamp_mode = (shader.texture_clamp_mode - shader.texture_clamp_mode % 256) + (wrap_s + wrap_t) + shader.texture_clamp_mode = NifClasses.TexClampMode.from_value(wrap_s + wrap_t) return shader diff --git a/io_scene_niftools/modules/nif_export/property/texture/types/nitextureprop.py b/io_scene_niftools/modules/nif_export/property/texture/types/nitextureprop.py index 7390a1c1d..eb69af476 100644 --- a/io_scene_niftools/modules/nif_export/property/texture/types/nitextureprop.py +++ b/io_scene_niftools/modules/nif_export/property/texture/types/nitextureprop.py @@ -37,11 +37,12 @@ # # ***** END LICENSE BLOCK ***** import bpy -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.modules.nif_export.block_registry import block_store from io_scene_niftools.modules.nif_export.property.texture import TextureSlotManager, TextureWriter from io_scene_niftools.utils.logging import NifLog +from io_scene_niftools.utils.singleton import NifData class NiTextureProp(TextureSlotManager): @@ -84,7 +85,7 @@ def export_texturing_property(self, flags=0x0001, applymode=None, b_mat=None): self.determine_texture_types(b_mat) - texprop = NifFormat.NiTexturingProperty() + texprop = NifClasses.NiTexturingProperty(NifData.data) texprop.flags = flags texprop.apply_mode = applymode @@ -95,7 +96,7 @@ def export_texturing_property(self, flags=0x0001, applymode=None, b_mat=None): # search for duplicate for n_block in block_store.block_to_obj: - if isinstance(n_block, NifFormat.NiTexturingProperty) and n_block.get_hash() == texprop.get_hash(): + if isinstance(n_block, NifClasses.NiTexturingProperty) and n_block.get_hash() == texprop.get_hash(): return n_block # no texturing property with given settings found, so use and register @@ -162,20 +163,21 @@ def export_nitextureprop_tex_descs(self, texprop): def export_texture_effect(self, b_texture_node=None): """Export a texture effect block from material texture mtex (MTex, not Texture).""" - texeff = NifFormat.NiTextureEffect() + texeff = NifClasses.NiTextureEffect(NifData.data) texeff.flags = 4 texeff.rotation.set_identity() texeff.scale = 1.0 texeff.model_projection_matrix.set_identity() - texeff.texture_filtering = NifFormat.TexFilterMode.FILTER_TRILERP - texeff.texture_clamping = NifFormat.TexClampMode.WRAP_S_WRAP_T - texeff.texture_type = NifFormat.EffectType.EFFECT_ENVIRONMENT_MAP - texeff.coordinate_generation_type = NifFormat.CoordGenType.CG_SPHERE_MAP + texeff.texture_filtering = NifClasses.TexFilterMode.FILTER_TRILERP + texeff.texture_clamping = NifClasses.TexClampMode.WRAP_S_WRAP_T + texeff.texture_type = NifClasses.EffectType.EFFECT_ENVIRONMENT_MAP + texeff.coordinate_generation_type = NifClasses.CoordGenType.CG_SPHERE_MAP if b_texture_node: texeff.source_texture = TextureWriter.export_source_texture(b_texture_node.texture) if bpy.context.scene.niftools_scene.game == 'MORROWIND': texeff.num_affected_node_list_pointers += 1 - texeff.affected_node_list_pointers.update_size() + # added value doesn't matter since it apparently gets automagically updated in engine + texeff.affected_node_list_pointers.append(0) texeff.unknown_vector.x = 1.0 return block_store.register_block(texeff) @@ -187,7 +189,7 @@ def export_texture_shader_effect(self, tex_prop): # sid meier's railroads: # some textures end up in the shader texture list there are 5 slots available, so set them up tex_prop.num_shader_textures = 5 - tex_prop.shader_textures.update_size() + tex_prop.reset_field("shader_textures") for mapindex, shadertexdesc in enumerate(tex_prop.shader_textures): # set default values shadertexdesc.is_used = False @@ -205,7 +207,7 @@ def export_texture_shader_effect(self, tex_prop): elif bpy.context.scene.niftools_scene.game == 'CIVILIZATION_IV': # some textures end up in the shader texture list there are 4 slots available, so set them up tex_prop.num_shader_textures = 4 - tex_prop.shader_textures.update_size() + tex_prop.reset_field("shader_textures") for mapindex, shadertexdesc in enumerate(tex_prop.shader_textures): # set default values shadertexdesc.is_used = False @@ -220,11 +222,11 @@ def add_shader_integer_extra_datas(self, trishape): @staticmethod def get_n_apply_mode_from_b_blend_type(b_blend_type): if b_blend_type == "LIGHTEN": - return NifFormat.ApplyMode.APPLY_HILIGHT + return NifClasses.ApplyMode.APPLY_HILIGHT elif b_blend_type == "MULTIPLY": - return NifFormat.ApplyMode.APPLY_HILIGHT2 + return NifClasses.ApplyMode.APPLY_HILIGHT2 elif b_blend_type == "MIX": - return NifFormat.ApplyMode.APPLY_MODULATE + return NifClasses.ApplyMode.APPLY_MODULATE NifLog.warn(f"Unsupported blend type ({b_blend_type}) in material, using apply mode APPLY_MODULATE") - return NifFormat.ApplyMode.APPLY_MODULATE + return NifClasses.ApplyMode.APPLY_MODULATE diff --git a/io_scene_niftools/modules/nif_export/property/texture/writer.py b/io_scene_niftools/modules/nif_export/property/texture/writer.py index 81d033930..a4c8839d3 100644 --- a/io_scene_niftools/modules/nif_export/property/texture/writer.py +++ b/io_scene_niftools/modules/nif_export/property/texture/writer.py @@ -40,13 +40,14 @@ import os.path import bpy -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses import io_scene_niftools.utils.logging from io_scene_niftools.modules.nif_export.block_registry import block_store from io_scene_niftools.utils import math from io_scene_niftools.utils.singleton import NifOp from io_scene_niftools.utils.logging import NifLog +from io_scene_niftools.utils.singleton import NifData class TextureWriter: @@ -64,7 +65,7 @@ def export_source_texture(n_texture=None, filename=None): """ # create NiSourceTexture - srctex = NifFormat.NiSourceTexture() + srctex = NifClasses.NiSourceTexture(NifData.data) srctex.use_external = True if filename is not None: # preset filename @@ -86,7 +87,7 @@ def export_source_texture(n_texture=None, filename=None): # search for duplicate for block in block_store.block_to_obj: - if isinstance(block, NifFormat.NiSourceTexture) and block.get_hash() == srctex.get_hash(): + if isinstance(block, NifClasses.NiSourceTexture) and block.get_hash() == srctex.get_hash(): return block # no identical source texture found, so use and register the new one diff --git a/io_scene_niftools/modules/nif_export/scene/__init__.py b/io_scene_niftools/modules/nif_export/scene/__init__.py index d68cc9a73..4f29d621c 100644 --- a/io_scene_niftools/modules/nif_export/scene/__init__.py +++ b/io_scene_niftools/modules/nif_export/scene/__init__.py @@ -40,10 +40,10 @@ import bpy from io_scene_niftools.utils.singleton import NifOp from io_scene_niftools.utils.logging import NifLog -from pyffi.formats.nif import NifFormat +import generated.formats.nif as NifFormat def get_version_data(): - """ Returns NifFormat.Data of the correct version and user versions """ + """ Returns NifFormat.NifFile of the correct version and user versions """ b_scene = bpy.context.scene.niftools_scene game = b_scene.game version = b_scene.nif_version @@ -53,4 +53,4 @@ def get_version_data(): user_version = b_scene.user_version if b_scene.user_version else b_scene.USER_VERSION.get(game, 0) user_version_2 = b_scene.user_version_2 if b_scene.user_version_2 else b_scene.USER_VERSION_2.get(game, 0) - return version, NifFormat.Data(version, user_version, user_version_2) + return version, NifFormat.NifFile.from_version(version, user_version, user_version_2) diff --git a/io_scene_niftools/modules/nif_export/types.py b/io_scene_niftools/modules/nif_export/types.py index 758b18a3a..3616bae7e 100644 --- a/io_scene_niftools/modules/nif_export/types.py +++ b/io_scene_niftools/modules/nif_export/types.py @@ -45,21 +45,24 @@ from io_scene_niftools.utils.singleton import NifOp -def create_ninode(b_obj=None): +def create_ninode(b_obj=None, n_node_type=None): """Essentially a wrapper around create_block() that creates nodes of the right type""" - # when no b_obj is passed, it means we create a root node + # when no b_obj is passed, use the passed n_node_type if not b_obj: - return block_store.create_block("NiNode") - + if n_node_type is None: + n_node_type = "NiNode" # get node type - some are stored as custom property of the b_obj - try: - n_node_type = b_obj["type"] - except KeyError: - n_node_type = "NiNode" - - # ...others by presence of constraints - if has_track(b_obj): - n_node_type = "NiBillboardNode" + else: + # let n_node_type overwrite the detected node type + if n_node_type is None: + try: + n_node_type = b_obj.niftools.nodetype + except AttributeError: + n_node_type = "NiNode" + + # ...others by presence of constraints + if has_track(b_obj): + n_node_type = "NiBillboardNode" # now create the node n_node = block_store.create_block(n_node_type, b_obj) @@ -94,8 +97,8 @@ def export_range_lod_data(n_node, b_obj): # set the data n_node.num_lod_levels = len(b_children) n_range_data.num_lod_levels = len(b_children) - n_node.lod_levels.update_size() - n_range_data.lod_levels.update_size() + n_node.reset_field("lod_levels") + n_range_data.reset_field("lod_levels") for b_child, n_lod_level, n_rd_lod_level in zip(b_children, n_node.lod_levels, n_range_data.lod_levels): n_lod_level.near_extent = b_child["near_extent"] n_lod_level.far_extent = b_child["far_extent"] @@ -117,7 +120,7 @@ def export_furniture_marker(n_root, filebase): furnmark = block_store.create_block("BSFurnitureMarker") furnmark.name = "FRN" furnmark.num_positions = 1 - furnmark.positions.update_size() + furnmark.reset_field("positions") furnmark.positions[0].position_ref_1 = furniturenumber furnmark.positions[0].position_ref_2 = furniturenumber diff --git a/io_scene_niftools/modules/nif_import/animation/__init__.py b/io_scene_niftools/modules/nif_import/animation/__init__.py index 9f0af6d9a..faf78b051 100644 --- a/io_scene_niftools/modules/nif_import/animation/__init__.py +++ b/io_scene_niftools/modules/nif_import/animation/__init__.py @@ -38,9 +38,10 @@ # ***** END LICENSE BLOCK ***** import bpy -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.utils.logging import NifLog +from io_scene_niftools.utils.consts import QUAT, EULER, LOC, SCALE class Animation: @@ -54,6 +55,23 @@ def __init__(self): # and still be able to access existing actions from this run self.actions = {} + @staticmethod + def get_controller_data(ctrl): + """Return data for ctrl, look in interpolator (for newer games) or directly on ctrl""" + if hasattr(ctrl, 'interpolator') and ctrl.interpolator: + data = ctrl.interpolator.data + else: + data = ctrl.data + # these have their data set as a KeyGroup on data + if isinstance(data, (NifClasses.NiBoolData, NifClasses.NiFloatData, NifClasses.NiPosData)): + return data.data + return data + + @staticmethod + def get_keys_values(items): + """Returns list of times and keys for an array 'items' with key elements having 'time' and 'value' attributes""" + return [key.time for key in items], [key.value for key in items] + @staticmethod def show_pose_markers(): """Helper function to ensure that pose markers are shown""" @@ -65,9 +83,9 @@ def show_pose_markers(): @staticmethod def get_b_interp_from_n_interp(n_ipol): - if n_ipol in (NifFormat.KeyType.LINEAR_KEY, NifFormat.KeyType.XYZ_ROTATION_KEY): + if n_ipol in (NifClasses.KeyType.LINEAR_KEY, NifClasses.KeyType.XYZ_ROTATION_KEY): return "LINEAR" - elif n_ipol == NifFormat.KeyType.QUADRATIC_KEY: + elif n_ipol == NifClasses.KeyType.QUADRATIC_KEY: return "BEZIER" elif n_ipol == 0: # guessing, not documented in nif.xml @@ -89,21 +107,21 @@ def create_action(self, b_obj, action_name): b_obj.animation_data.action = b_action return b_action - def create_fcurves(self, action, dtype, drange, flags=None, bonename=None, keyname=None): + def create_fcurves(self, action, dtype, drange, flags, bone_name, key_name): """ Create fcurves in action for desired conditions. """ # armature pose bone animation - if bonename: + if bone_name: fcurves = [ - action.fcurves.new(data_path=f'pose.bones["{bonename}"].{dtype}', index=i, action_group=bonename) + action.fcurves.new(data_path=f'pose.bones["{bone_name}"].{dtype}', index=i, action_group=bone_name) for i in drange] # shapekey pose bone animation - elif keyname: + elif key_name: fcurves = [ - action.fcurves.new(data_path=f'key_blocks["{keyname}"].{dtype}', index=0,) + action.fcurves.new(data_path=f'key_blocks["{key_name}"].{dtype}', index=0,) ] else: # Object animation (non-skeletal) is lumped into the "LocRotScale" action_group - if dtype in ("rotation_euler", "rotation_quaternion", "location", "scale"): + if dtype in (QUAT, EULER, LOC, SCALE): action_group = "LocRotScale" # Non-transformaing animations (eg. visibility or material anims) use no action groups else: @@ -141,28 +159,49 @@ def set_extrapolation(extend_type, fcurves): for fcurve in fcurves: fcurve.extrapolation = 'CONSTANT' - def add_key(self, fcurves, t, key, interp): + def add_keys(self, b_action, key_type, key_range, flags, times, keys, interp, bone_name=None, key_name=None): """ - Add a key (len=n) to a set of fcurves (len=n) at the given frame. Set the key's interpolation to interp. + Create needed fcurves and add a list of keys to an action. """ - frame = round(t * self.fps) - for fcurve, k in zip(fcurves, key): - fcurve.keyframe_points.insert(frame, k).interpolation = interp + samples = [round(t * self.fps) for t in times] + assert len(samples) == len(keys) + # get interpolation enum representation + ipo = bpy.types.Keyframe.bl_rna.properties['interpolation'].enum_items[interp].value + interpolations = [ipo for _ in range(len(samples))] + # import the keys + try: + fcurves = self.create_fcurves(b_action, key_type, key_range, flags, bone_name, key_name) + if len(key_range) == 1: + # flat key - make it zippable + key_per_fcurve = [keys] + else: + key_per_fcurve = zip(*keys) + for fcurve, fcu_keys in zip(fcurves, key_per_fcurve): + # add new points + fcurve.keyframe_points.add(count=len(fcu_keys)) + # populate points with keys for this curve + fcurve.keyframe_points.foreach_set("co", [x for co in zip(samples, fcu_keys) for x in co]) + fcurve.keyframe_points.foreach_set("interpolation", interpolations) + # update + fcurve.update() + except RuntimeError: + # blender throws F-Curve ... already exists in action ... + NifLog.warn(f"Could not add fcurve '{key_type}' to '{b_action.name}', already added before?") # import animation groups def import_text_keys(self, n_block, b_action): """Gets and imports a NiTextKeyExtraData""" - if isinstance(n_block, NifFormat.NiControllerSequence): + if isinstance(n_block, NifClasses.NiControllerSequence): txk = n_block.text_keys else: - txk = n_block.find(block_type=NifFormat.NiTextKeyExtraData) + txk = n_block.find(block_type=NifClasses.NiTextKeyExtraData) self.import_text_key_extra_data(txk, b_action) def import_text_key_extra_data(self, txk, b_action): """Stores the text keys as pose markers in a blender action.""" if txk and b_action: for key in txk.text_keys: - newkey = key.value.decode().replace('\r\n', '/').rstrip('/') + newkey = key.value.replace('\r\n', '/').rstrip('/') frame = round(key.time * self.fps) marker = b_action.pose_markers.new(newkey) marker.frame = frame @@ -172,15 +211,14 @@ def set_frames_per_second(self, roots): # find all key times key_times = [] for root in roots: - for kfd in root.tree(block_type=NifFormat.NiKeyframeData): + for kfd in root.tree(block_type=NifClasses.NiKeyframeData): key_times.extend(key.time for key in kfd.translations.keys) key_times.extend(key.time for key in kfd.scales.keys) key_times.extend(key.time for key in kfd.quaternion_keys) - key_times.extend(key.time for key in kfd.xyz_rotations[0].keys) - key_times.extend(key.time for key in kfd.xyz_rotations[1].keys) - key_times.extend(key.time for key in kfd.xyz_rotations[2].keys) + for dimension in kfd.xyz_rotations: + key_times.extend(key.time for key in dimension.keys) - for kfi in root.tree(block_type=NifFormat.NiBSplineInterpolator): + for kfi in root.tree(block_type=NifClasses.NiBSplineInterpolator): if not kfi.basis_data: # skip bsplines without basis data (eg bowidle.kf in Oblivion) continue @@ -189,7 +227,7 @@ def set_frames_per_second(self, roots): / (kfi.basis_data.num_control_points - 2) for point in range(kfi.basis_data.num_control_points - 2)) - for uv_data in root.tree(block_type=NifFormat.NiUVData): + for uv_data in root.tree(block_type=NifClasses.NiUVData): for uv_group in uv_data.uv_groups: key_times.extend(key.time for key in uv_group.keys) @@ -212,3 +250,4 @@ def set_frames_per_second(self, roots): self.fps = fps bpy.context.scene.render.fps = fps bpy.context.scene.frame_set(0) + diff --git a/io_scene_niftools/modules/nif_import/animation/material.py b/io_scene_niftools/modules/nif_import/animation/material.py index 60ec804a5..5a1498f7c 100644 --- a/io_scene_niftools/modules/nif_import/animation/material.py +++ b/io_scene_niftools/modules/nif_import/animation/material.py @@ -37,13 +37,18 @@ # # ***** END LICENSE BLOCK ***** -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.modules.nif_import.animation import Animation from io_scene_niftools.utils import math from io_scene_niftools.utils.singleton import NifOp from io_scene_niftools.utils.logging import NifLog +# indices for blender ShaderNodeMapping node +LOC_DP = 1 +SCALE_DP = 3 +MAPPING = "ShaderNodeMapping" + class MaterialAnimation(Animation): @@ -51,70 +56,136 @@ def import_material_controllers(self, n_geom, b_material): """Import material animation data for given geometry.""" if not NifOp.props.animation: return - n_material = math.find_property(n_geom, NifFormat.NiMaterialProperty) + n_material = math.find_property(n_geom, NifClasses.NiMaterialProperty) if n_material: self.import_material_alpha_controller(b_material, n_material) - for b_channel, n_target_color in (("niftools.ambient_color", NifFormat.TargetColor.TC_AMBIENT), - ("diffuse_color", NifFormat.TargetColor.TC_DIFFUSE), - ("specular_color", NifFormat.TargetColor.TC_SPECULAR)): + for b_channel, n_target_color in (("niftools.ambient_color", NifClasses.MaterialColor.TC_AMBIENT), + ("diffuse_color", NifClasses.MaterialColor.TC_DIFFUSE), + ("specular_color", NifClasses.MaterialColor.TC_SPECULAR)): self.import_material_color_controller(b_material, n_material, b_channel, n_target_color) - self.import_material_uv_controller(b_material, n_geom) + self.import_uv_controller(b_material, n_geom) + self.import_tex_transform_controller(b_material, n_geom) def import_material_alpha_controller(self, b_material, n_material): # find alpha controller - n_alphactrl = math.find_controller(n_material, NifFormat.NiAlphaController) - if not n_alphactrl: + n_ctrl = math.find_controller(n_material, NifClasses.NiAlphaController) + if not n_ctrl: return NifLog.info("Importing alpha controller") b_mat_action = self.create_action(b_material, "MaterialAction") - fcurves = self.create_fcurves(b_mat_action, "niftools.emissive_alpha", range(3), n_alphactrl.flags) - interp = self.get_b_interp_from_n_interp(n_alphactrl.data.data.interpolation) - for key in n_alphactrl.data.data.keys: - self.add_key(fcurves, key.time, (key.value, key.value, key.value), interp) + n_ctrl_data = self.get_controller_data(n_ctrl) + interp = self.get_b_interp_from_n_interp(n_ctrl_data.interpolation) + times, keys = self.get_keys_values(n_ctrl_data.keys) + # key needs to be RGB due to current representation in blender + keys = [(v, v, v) for v in keys] + self.add_keys(b_mat_action, "niftools.emissive_alpha", range(3), n_ctrl.flags, times, keys, interp) def import_material_color_controller(self, b_material, n_material, b_channel, n_target_color): # find material color controller with matching target color - for ctrl in n_material.get_controllers(): - if isinstance(ctrl, NifFormat.NiMaterialColorController): - if ctrl.get_target_color() == n_target_color: - n_matcolor_ctrl = ctrl + for n_ctrl in n_material.get_controllers(): + if isinstance(n_ctrl, NifClasses.NiMaterialColorController): + if n_ctrl.get_target_color() == n_target_color: break else: return NifLog.info(f"Importing material color controller for target color {n_target_color} into blender channel {b_channel}") - - # import data as curves b_mat_action = self.create_action(b_material, "MaterialAction") + n_ctrl_data = self.get_controller_data(n_ctrl) + interp = self.get_b_interp_from_n_interp(n_ctrl_data.interpolation) + times, keys = self.get_keys_values(n_ctrl_data.keys) + self.add_keys(b_mat_action, b_channel, range(3), n_ctrl.flags, times, keys, interp) - fcurves = self.create_fcurves(b_mat_action, b_channel, range(3), n_matcolor_ctrl.flags) - interp = self.get_b_interp_from_n_interp(n_matcolor_ctrl.data.data.interpolation) - for key in n_matcolor_ctrl.data.data.keys: - self.add_key(fcurves, key.time, key.value.as_list(), interp) - - def import_material_uv_controller(self, b_material, n_geom): - """Import UV controller data.""" + def import_uv_controller(self, b_material, n_geom): + """Import UV controller data as a mapping node with animated values.""" # search for the block - n_ctrl = math.find_controller(n_geom, NifFormat.NiUVController) + n_ctrl = math.find_controller(n_geom, NifClasses.NiUVController) if not n_ctrl: return NifLog.info("Importing UV controller") - b_mat_action = self.create_action(b_material, "MaterialAction") + n_ctrl_data = self.get_controller_data(n_ctrl) + if not any(n_uvgroup.keys for n_uvgroup in n_ctrl_data.uv_groups): + return + + b_mat_action, transform = self.insert_mapping_node(b_material) - dtypes = ("offset", 0), ("offset", 1), ("scale", 0), ("scale", 1) + # loc U, loc V, scale U, scale V + dtypes = (LOC_DP, 0), (LOC_DP, 1), (SCALE_DP, 0), (SCALE_DP, 1) for n_uvgroup, (data_path, array_ind) in zip(n_ctrl.data.uv_groups, dtypes): if n_uvgroup.keys: interp = self.get_b_interp_from_n_interp(n_uvgroup.interpolation) - # in blender, UV offset is stored per n_texture slot - # so we have to repeat the import for each used tex slot - for i, texture_slot in enumerate(b_material.texture_slots): - if texture_slot: - fcurves = self.create_fcurves(b_mat_action, f"texture_slots[{i}]." + data_path, (array_ind,), n_ctrl.flags) - for key in n_uvgroup.keys: - if "offset" in data_path: - self.add_key(fcurves, key.time, (-key.value,), interp) - else: - self.add_key(fcurves, key.time, (key.value,), interp) + times, keys = self.get_keys_values(n_uvgroup.keys) + # UV V coordinate is inverted in blender + if 1 == LOC_DP and array_ind == 1: + keys = [-key for key in keys] + self.add_keys(b_mat_action, f'nodes["{transform.name}"].inputs[{data_path}].default_value', (array_ind,), n_ctrl.flags, times, keys, interp) + + def import_tex_transform_controller(self, b_material, n_geom): + """Import UV controller data as a mapping node with animated values.""" + # search for the block + n_tex_prop = math.find_property(n_geom, NifClasses.NiTexturingProperty) + if not n_tex_prop: + return + for n_ctrl in math.controllers_iter(n_tex_prop, NifClasses.NiTextureTransformController): + NifLog.info("Importing Texture Transform controller") + + n_ctrl_data = self.get_controller_data(n_ctrl) + if not n_ctrl_data.keys: + return + # todo [material] get the mapping from enum to node, and standardize texture slot names everywhere + # the whole node logic needs to be refactored to seamlessly integrate this + # get tex slot + tex_slot = n_ctrl.texture_slot + times, keys = self.get_keys_values(n_ctrl_data.keys) + # get operation + operation = n_ctrl.operation + if operation == NifClasses.TransformMember.TT_TRANSLATE_U: + data_path = LOC_DP + array_ind = 0 + elif operation == NifClasses.TransformMember.TT_TRANSLATE_V: + data_path = LOC_DP + array_ind = 1 + # UV V coordinate is inverted in blender + keys = [-key for key in keys] + elif operation == NifClasses.TransformMember.TT_ROTATE: + # not sure, need example nif + NifLog.warn("Rotation in Texture Transform is not supported") + return + elif operation == NifClasses.TransformMember.TT_SCALE_U: + data_path = SCALE_DP + array_ind = 0 + elif operation == NifClasses.TransformMember.TT_SCALE_V: + data_path = SCALE_DP + array_ind = 1 + + # in example nif, no node tree exists, so this doesn't link the transform node + b_mat_action, transform = self.insert_mapping_node(b_material) + + interp = self.get_b_interp_from_n_interp(n_ctrl_data.interpolation) + self.add_keys(b_mat_action, f'nodes["{transform.name}"].inputs[{data_path}].default_value', (array_ind,), n_ctrl.flags, times, keys, interp) + + def insert_mapping_node(self, b_material): + b_mat_action = self.create_action(b_material.node_tree, f"{b_material.name}-MaterialNodesAction") + tree = b_material.node_tree + # reuse mapping node if one had been added before + for node in tree.nodes: + if node.type == "MAPPING": + return b_mat_action, node + transform = tree.nodes.new(MAPPING) + # get previous links + used_links = [] + for link in tree.links: + # get uv nodes + if link.from_node.type == "UVMAP": + used_links.append(link) + # link the node between previous uv node and texture node + for link in used_links: + from_socket = link.from_socket + to_socket = link.to_socket + tree.links.remove(link) + tree.links.new(from_socket, transform.inputs[0]) + tree.links.new(transform.outputs[0], to_socket) + return b_mat_action, transform diff --git a/io_scene_niftools/modules/nif_import/animation/morph.py b/io_scene_niftools/modules/nif_import/animation/morph.py index b61a138d1..6efc0d146 100644 --- a/io_scene_niftools/modules/nif_import/animation/morph.py +++ b/io_scene_niftools/modules/nif_import/animation/morph.py @@ -39,7 +39,7 @@ import bpy import mathutils -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.modules.nif_import import animation from io_scene_niftools.modules.nif_import.animation import Animation @@ -57,57 +57,55 @@ def __init__(self): def import_morph_controller(self, n_node, b_obj): """Import NiGeomMorpherController as shape keys for blender object.""" - n_morphCtrl = math.find_controller(n_node, NifFormat.NiGeomMorpherController) - if n_morphCtrl: + n_morph_ctrl = math.find_controller(n_node, NifClasses.NiGeomMorpherController) + if n_morph_ctrl: NifLog.debug("NiGeomMorpherController processed") b_mesh = b_obj.data - morphData = n_morphCtrl.data - if morphData.num_morphs: + morph_data = n_morph_ctrl.data + if morph_data.num_morphs: # get name for base key - keyname = morphData.morphs[0].frame_name.decode() - if not keyname: - keyname = 'Base' + morph = morph_data.morphs[0] + key_name = morph.frame_name + if not key_name: + key_name = 'Base' - # insert base key at frame 1, using relative keys - sk_basis = b_obj.shape_key_add(name=keyname) + # insert base key, using relative keys + sk_basis = b_obj.shape_key_add(name=key_name) # get base vectors and import all morphs - baseverts = morphData.morphs[0].vectors + base_verts = morph.vectors - shape_action = self.create_action(b_obj.data.shape_keys, b_obj.name + "-Morphs") + shape_action = self.create_action(b_obj.data.shape_keys, f"{b_obj.name}-Morphs") - for idxMorph in range(1, morphData.num_morphs): + for morph_i in range(1, morph_data.num_morphs): + morph = morph_data.morphs[morph_i] # get name for key - keyname = morphData.morphs[idxMorph].frame_name.decode() - if not keyname: - keyname = f'Key {idxMorph}' - NifLog.info(f"Inserting key '{keyname}'") + key_name = morph.frame_name + if not key_name: + key_name = f'Key {morph_i}' + NifLog.info(f"Inserting key '{key_name}'") # get vectors - morph_verts = morphData.morphs[idxMorph].vectors - self.morph_mesh(b_mesh, baseverts, morph_verts) - shape_key = b_obj.shape_key_add(name=keyname, from_mix=False) + morph_verts = morph.vectors + self.morph_mesh(b_mesh, base_verts, morph_verts) + shape_key = b_obj.shape_key_add(name=key_name, from_mix=False) - # first find the keys - # older versions store keys in the morphData - morph_data = morphData.morphs[idxMorph] + # find the keys + # older versions store keys in the morph_data # newer versions store keys in the controller - if not morph_data.keys: + if not morph.keys: try: - if n_morphCtrl.interpolators: - morph_data = n_morphCtrl.interpolators[idxMorph].data.data - elif n_morphCtrl.interpolator_weights: - morph_data = n_morphCtrl.interpolator_weights[idxMorph].interpolator.data.data + if n_morph_ctrl.interpolators: + morph = n_morph_ctrl.interpolators[morph_i].data.data + elif n_morph_ctrl.interpolator_weights: + morph = n_morph_ctrl.interpolator_weights[morph_i].interpolator.data.data except KeyError: - NifLog.info(f"Unsupported interpolator \"{type(n_morphCtrl.interpolator_weights['idxMorph'].interpolator)}\"") + NifLog.info(f"Unsupported interpolator '{type(n_morph_ctrl.interpolator_weights[morph_i].interpolator)}'") continue # get the interpolation mode - interp = self.get_b_interp_from_n_interp( morph_data.interpolation) - fcu = self.create_fcurves(shape_action, "value", (0,), flags=n_morphCtrl.flags, keyname=shape_key.name) - - # set keyframes - for key in morph_data.keys: - self.add_key(fcu, key.time, (key.value,), interp) + interp = self.get_b_interp_from_n_interp(morph.interpolation) + times, keys = self.get_keys_values(morph.keys) + self.add_keys(shape_action, "value", (0,), n_morph_ctrl.flags, times, keys, interp, key_name=shape_key.name) def import_egm_morphs(self, b_obj): """Import all EGM morphs as shape keys for blender object.""" @@ -119,8 +117,6 @@ def import_egm_morphs(self, b_obj): sk_basis = b_obj.shape_key_add(name="Basis") b_mesh.shape_keys.use_relative = False - # TODO: I'm not entirely sure that changing the morphs to f-strings won't - # TODO: break anything. They _shouldn't_. morphs = ([(morph, f"EGM SYM {i}") for i, morph in enumerate(sym_morphs)] + [(morph, f"EGM ASYM {i}") for i, morph in enumerate(asym_morphs)]) @@ -129,8 +125,7 @@ def import_egm_morphs(self, b_obj): # convert tuples into vector here so we can simply add in morph_mesh() for b_v_index, (bv, mv) in enumerate(zip(base_verts, morph_verts)): b_mesh.vertices[b_v_index].co = bv + mathutils.Vector(mv) - # TODO [animation] unused variable is it required - shape_key = b_obj.shape_key_add(name=key_name, from_mix=False) + b_obj.shape_key_add(name=key_name, from_mix=False) def morph_mesh(self, b_mesh, baseverts, morphverts): """Transform a mesh to be in the shape given by morphverts.""" diff --git a/io_scene_niftools/modules/nif_import/animation/object.py b/io_scene_niftools/modules/nif_import/animation/object.py index bafd94c5d..e94423e75 100644 --- a/io_scene_niftools/modules/nif_import/animation/object.py +++ b/io_scene_niftools/modules/nif_import/animation/object.py @@ -37,7 +37,7 @@ # # ***** END LICENSE BLOCK ***** -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.modules.nif_import.animation import Animation from io_scene_niftools.utils import math @@ -49,13 +49,12 @@ class ObjectAnimation(Animation): def import_visibility(self, n_node, b_obj): """Import vis controller for blender object.""" - n_vis_ctrl = math.find_controller(n_node, NifFormat.NiVisController) + n_vis_ctrl = math.find_controller(n_node, NifClasses.NiVisController) if not n_vis_ctrl: return NifLog.info("Importing vis controller") + b_obj_action = self.create_action(b_obj, f"{b_obj.name}-Anim") - b_obj_action = self.create_action(b_obj, b_obj.name + "-Anim") - - fcurves = self.create_fcurves(b_obj_action, "hide", (0,), n_vis_ctrl.flags) - for key in n_vis_ctrl.data.keys: - self.add_key(fcurves, key.time, (key.value,), "CONSTANT") + n_ctrl_data = self.get_controller_data(n_vis_ctrl) + times, keys = self.get_keys_values(n_ctrl_data.keys) + self.add_keys(b_obj_action, "hide_viewport", (0,), n_vis_ctrl.flags, times, keys, "CONSTANT") diff --git a/io_scene_niftools/modules/nif_import/animation/transform.py b/io_scene_niftools/modules/nif_import/animation/transform.py index ce62370e0..336068a7a 100644 --- a/io_scene_niftools/modules/nif_import/animation/transform.py +++ b/io_scene_niftools/modules/nif_import/animation/transform.py @@ -39,16 +39,57 @@ import bpy import mathutils +import time from functools import singledispatch from bisect import bisect_left -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.modules.nif_import.animation import Animation from io_scene_niftools.modules.nif_import.object import block_registry from io_scene_niftools.utils import math -from io_scene_niftools.utils.blocks import safe_decode from io_scene_niftools.utils.logging import NifLog +from io_scene_niftools.utils.consts import QUAT, EULER, LOC, SCALE + + +def as_b_quat(n_val): + return mathutils.Quaternion([n_val.w, n_val.x, n_val.y, n_val.z]) + + +def as_b_loc(n_val): + return mathutils.Vector([n_val.x, n_val.y, n_val.z]) + + +def as_b_scale(n_val): + return n_val, n_val, n_val + + +def as_b_euler(n_val): + return mathutils.Euler(n_val) + + +def correct_loc(key, n_bind_rot_inv, n_bind_trans): + return math.import_keymat(n_bind_rot_inv, mathutils.Matrix.Translation(key - n_bind_trans)).to_translation() + + +def correct_quat(key, n_bind_rot_inv, n_bind_trans): + return math.import_keymat(n_bind_rot_inv, key.to_matrix().to_4x4()).to_quaternion() + + +def correct_euler(key, n_bind_rot_inv, n_bind_trans): + return math.import_keymat(n_bind_rot_inv, key.to_matrix().to_4x4()).to_euler() + + +def correct_scale(key, n_bind_rot_inv, n_bind_trans): + return key + + +key_lut = { + QUAT: (as_b_quat, correct_quat, 4), + EULER: (as_b_euler, correct_euler, 3), + LOC: (as_b_loc, correct_loc, 3), + SCALE: (as_b_scale, correct_scale, 3), +} def interpolate(x_out, x_in, y_in): @@ -74,9 +115,9 @@ class TransformAnimation(Animation): def __init__(self): super().__init__() self.import_kf_root = singledispatch(self.import_kf_root) - self.import_kf_root.register(NifFormat.NiControllerSequence, self.import_controller_sequence) - self.import_kf_root.register(NifFormat.NiSequenceStreamHelper, self.import_sequence_stream_helper) - self.import_kf_root.register(NifFormat.NiSequenceData, self.import_sequence_data) + self.import_kf_root.register(NifClasses.NiControllerSequence, self.import_controller_sequence) + self.import_kf_root.register(NifClasses.NiSequenceStreamHelper, self.import_sequence_stream_helper) + self.import_kf_root.register(NifClasses.NiSequenceData, self.import_sequence_data) def get_bind_data(self, b_armature): """Get the required bind data of an armature. Used by standalone KF import and export. """ @@ -100,12 +141,12 @@ def get_target(self, b_armature_obj, n_name): def import_kf_root(self, kf_root, b_armature_obj): """Base method to warn user that this root type is not supported""" - NifLog.warn(f"Unknown KF root block found : {safe_decode(kf_root.name)}") + NifLog.warn(f"Unknown KF root block found : {kf_root.name}") NifLog.warn(f"This type isn't currently supported: {type(kf_root)}") def import_generic_kf_root(self, kf_root): NifLog.debug(f'Importing {type(kf_root)}...') - return safe_decode(kf_root.name) + return kf_root.name def import_sequence_data(self, kf_root, b_armature_obj): b_action_name = self.import_generic_kf_root(kf_root) @@ -129,12 +170,12 @@ def import_sequence_stream_helper(self, kf_root, b_armature_obj): textkeys = None while extra and controller: # textkeys in the stack do not specify node names, import as markers - while isinstance(extra, NifFormat.NiTextKeyExtraData): + while isinstance(extra, NifClasses.NiTextKeyExtraData): textkeys = extra extra = extra.next_extra_data # grabe the node name from string data - if isinstance(extra, NifFormat.NiStringExtraData): + if isinstance(extra, NifClasses.NiStringExtraData): b_target = self.get_target(b_armature_obj, extra.string_data) actions.add(self.import_keyframe_controller(controller, b_armature_obj, b_target, b_action_name)) # grab next pair of extra and controller @@ -187,83 +228,10 @@ def import_keyframe_controller(self, n_kfc, b_armature, b_target, b_action_name) return NifLog.debug(f'Importing keyframe controller for {b_target.name}') - translations = [] - scales = [] - rotations = [] - eulers = [] n_kfd = None - - # transform controllers (dartgun.nif) - if isinstance(n_kfc, NifFormat.NiTransformController): - if n_kfc.interpolator: - n_kfd = n_kfc.interpolator.data - # B-spline curve import - elif isinstance(n_kfc, NifFormat.NiBSplineInterpolator): - # used by WLP2 (tiger.kf), but only for non-LocRotScale data - # eg. bone stretching - see controlledblock.get_variable_1() - # do not support this for now, no good representation in Blender - if isinstance(n_kfc, NifFormat.NiBSplineCompFloatInterpolator): - # pyffi lacks support for this, but the following gets float keys - # keys = list(kfc._getCompKeys(kfc.offset, 1, kfc.bias, kfc.multiplier)) - return - times = list(n_kfc.get_times()) - # just do these temp steps to avoid generating empty fcurves down the line - trans_temp = [mathutils.Vector(tup) for tup in n_kfc.get_translations()] - if trans_temp: - translations = zip(times, trans_temp) - rot_temp = [mathutils.Quaternion(tup) for tup in n_kfc.get_rotations()] - if rot_temp: - rotations = zip(times, rot_temp) - scale_temp = list(n_kfc.get_scales()) - if scale_temp: - scales = zip(times, scale_temp) - # Bsplines are Bezier curves - interp_rot = interp_loc = interp_scale = "BEZIER" - elif isinstance(n_kfc, NifFormat.NiMultiTargetTransformController): - # not sure what this is used for - return - else: - # ZT2 & Fallout - n_kfd = n_kfc.data - if isinstance(n_kfd, NifFormat.NiKeyframeData): - interp_rot = self.get_b_interp_from_n_interp(n_kfd.rotation_type) - interp_loc = self.get_b_interp_from_n_interp(n_kfd.translations.interpolation) - interp_scale = self.get_b_interp_from_n_interp(n_kfd.scales.interpolation) - if n_kfd.rotation_type == 4: - b_target.rotation_mode = "XYZ" - # uses xyz rotation - if n_kfd.xyz_rotations[0].keys: - # euler keys need not be sampled at the same time in KFs - # but we need complete key sets to do the space conversion - # so perform linear interpolation to import all keys properly - - # get all the keys' times - times_x = [key.time for key in n_kfd.xyz_rotations[0].keys] - times_y = [key.time for key in n_kfd.xyz_rotations[1].keys] - times_z = [key.time for key in n_kfd.xyz_rotations[2].keys] - # the unique time stamps we have to sample all curves at - times_all = sorted(set(times_x + times_y + times_z)) - # the actual resampling - x_r = interpolate(times_all, times_x, [key.value for key in n_kfd.xyz_rotations[0].keys]) - y_r = interpolate(times_all, times_y, [key.value for key in n_kfd.xyz_rotations[1].keys]) - z_r = interpolate(times_all, times_z, [key.value for key in n_kfd.xyz_rotations[2].keys]) - eulers = zip(times_all, zip(x_r, y_r, z_r)) - else: - b_target.rotation_mode = "QUATERNION" - rotations = [(key.time, key.value) for key in n_kfd.quaternion_keys] - - if n_kfd.scales.keys: - scales = [(key.time, key.value) for key in n_kfd.scales.keys] - - if n_kfd.translations.keys: - translations = [(key.time, key.value) for key in n_kfd.translations.keys] - - # ZT2 - get extrapolation for every kfc - if isinstance(n_kfc, NifFormat.NiKeyframeController): - flags = n_kfc.flags # fallout, Loki - we set extrapolation according to the root NiControllerSequence.cycle_type - else: - flags = None + flags = None + n_bind_rot_inv = n_bind_trans = None # create or get the action if b_armature and isinstance(b_target, bpy.types.PoseBone): @@ -277,42 +245,82 @@ def import_keyframe_controller(self, n_kfc, b_armature, b_target, b_action_name) b_action = self.create_action(b_target, f"{b_action_name}_{b_target.name}") bone_name = None - if eulers: - NifLog.debug('Rotation keys..(euler)') - fcurves = self.create_fcurves(b_action, "rotation_euler", range(3), flags, bone_name) - for t, val in eulers: - key = mathutils.Euler(val) - if bone_name: - key = math.import_keymat(n_bind_rot_inv, key.to_matrix().to_4x4()).to_euler() - self.add_key(fcurves, t, key, interp_rot) - elif rotations: - NifLog.debug('Rotation keys...(quaternions)') - fcurves = self.create_fcurves(b_action, "rotation_quaternion", range(4), flags, bone_name) - for t, val in rotations: - key = mathutils.Quaternion([val.w, val.x, val.y, val.z]) - if bone_name: - key = math.import_keymat(n_bind_rot_inv, key.to_matrix().to_4x4()).to_quaternion() - self.add_key(fcurves, t, key, interp_rot) - if translations: - NifLog.debug('Translation keys...') - fcurves = self.create_fcurves(b_action, "location", range(3), flags, bone_name) - for t, val in translations: - key = mathutils.Vector([val.x, val.y, val.z]) - if bone_name: - key = math.import_keymat(n_bind_rot_inv, mathutils.Matrix.Translation(key - n_bind_trans)).to_translation() - self.add_key(fcurves, t, key, interp_loc) - if scales: - NifLog.debug('Scale keys...') - fcurves = self.create_fcurves(b_action, "scale", range(3), flags, bone_name) - for t, val in scales: - key = (val, val, val) - self.add_key(fcurves, t, key, interp_scale) + # B-spline curve import + if isinstance(n_kfc, NifClasses.NiBSplineInterpolator): + # Bsplines are Bezier curves + interp = "BEZIER" + if isinstance(n_kfc, NifClasses.NiBSplineCompFloatInterpolator): + # used by WLP2 (tiger.kf), but only for non-LocRotScale data + # eg. bone stretching - see controlledblock.get_variable_1() + # do not support this for now, no good representation in Blender + # pyffi lacks support for this, but the following gets float keys + # keys = list(kfc._getCompKeys(kfc.offset, 1, kfc.bias, kfc.multiplier)) + return + times = list(n_kfc.get_times()) + keys = [NifClasses.Vector3.from_value(tuple_key) for tuple_key in n_kfc.get_translations()] + self.import_keys(LOC, b_action, bone_name, times, keys, flags, interp, n_bind_rot_inv, n_bind_trans) + keys = [NifClasses.Quaternion.from_value(tuple_key) for tuple_key in n_kfc.get_rotations()] + self.import_keys(QUAT, b_action, bone_name, times, keys, flags, interp, n_bind_rot_inv, n_bind_trans) + keys = list(n_kfc.get_scales()) + self.import_keys(SCALE, b_action, bone_name, times, keys, flags, interp, n_bind_rot_inv, n_bind_trans) + return b_action + elif isinstance(n_kfc, NifClasses.NiMultiTargetTransformController): + # not sure what this is used for + return + n_kfd = self.get_controller_data(n_kfc) + # ZT2 - get extrapolation for every kfc + if isinstance(n_kfc, NifClasses.NiKeyframeController): + flags = n_kfc.flags + if isinstance(n_kfd, NifClasses.NiKeyframeData): + if n_kfd.rotation_type == 4: + b_target.rotation_mode = "XYZ" + # euler keys need not be sampled at the same time in KFs + # but we need complete key sets to do the space conversion + # so perform linear interpolation to import all keys properly + + # get all the times and keys for each coordinate + times_keys = [self.get_keys_values(euler.keys) for euler in n_kfd.xyz_rotations] + # the unique time stamps we have to sample all curves at + times_all = sorted(set(times_keys[0][0] + times_keys[1][0] + times_keys[2][0])) + # todo - this assumes that all three channels are keyframed, but it seems like this need not be the case + # resample each coordinate for all times + keys_res = [interpolate(times_all, times, keys) for times, keys in times_keys] + # for eulers, the actual interpolation type is apparently stored per channel + interp = self.get_b_interp_from_n_interp(n_kfd.xyz_rotations[0].interpolation) + self.import_keys(EULER, b_action, bone_name, times_all, zip(*keys_res), flags, interp, n_bind_rot_inv, n_bind_trans) + else: + b_target.rotation_mode = "QUATERNION" + times, keys = self.get_keys_values(n_kfd.quaternion_keys) + interp = self.get_b_interp_from_n_interp(n_kfd.rotation_type) + self.import_keys(QUAT, b_action, bone_name, times, keys, flags, interp, n_bind_rot_inv, n_bind_trans) + times, keys = self.get_keys_values(n_kfd.scales.keys) + interp = self.get_b_interp_from_n_interp(n_kfd.scales.interpolation) + self.import_keys(SCALE, b_action, bone_name, times, keys, flags, interp, n_bind_rot_inv, n_bind_trans) + + times, keys = self.get_keys_values(n_kfd.translations.keys) + interp = self.get_b_interp_from_n_interp(n_kfd.translations.interpolation) + self.import_keys(LOC, b_action, bone_name, times, keys, flags, interp, n_bind_rot_inv, n_bind_trans) + return b_action + def import_keys(self, key_type, b_action, bone_name, times, keys, flags, interp, n_bind_rot_inv, n_bind_trans): + """Imports key frames according to the specified key_type""" + if not keys: + return + # look up conventions by key type + key_func, key_corrector, key_dim = key_lut[key_type] + NifLog.debug(f'{key_type} keys...') + # convert nif keys to proper key type for blender + keys = [key_func(val) for val in keys] + # correct for bone space if target is an armature bone + if bone_name: + keys = [key_corrector(key, n_bind_rot_inv, n_bind_trans) for key in keys] + self.add_keys(b_action, key_type, range(key_dim), flags, times, keys, interp, bone_name=bone_name) + def import_transforms(self, n_block, b_obj, bone_name=None): """Loads an animation attached to a nif block.""" # find keyframe controller - n_kfc = math.find_controller(n_block, (NifFormat.NiKeyframeController, NifFormat.NiTransformController)) + n_kfc = math.find_controller(n_block, (NifClasses.NiKeyframeController, NifClasses.NiTransformController)) if n_kfc: # skeletal animation if bone_name: @@ -324,7 +332,7 @@ def import_transforms(self, n_block, b_obj, bone_name=None): def import_controller_manager(self, n_block, b_obj, b_armature): ctrlm = n_block.controller - if ctrlm and isinstance(ctrlm, NifFormat.NiControllerManager): + if ctrlm and isinstance(ctrlm, NifClasses.NiControllerManager): NifLog.debug(f'Importing NiControllerManager') if b_armature: self.get_bind_data(b_armature) diff --git a/io_scene_niftools/modules/nif_import/armature/__init__.py b/io_scene_niftools/modules/nif_import/armature/__init__.py index e49900715..94434aa0c 100644 --- a/io_scene_niftools/modules/nif_import/armature/__init__.py +++ b/io_scene_niftools/modules/nif_import/armature/__init__.py @@ -40,17 +40,17 @@ import os import bpy +from bpy_extras.io_utils import orientation_helper import mathutils +from generated.formats.nif import classes as NifClasses -from pyffi.formats.nif import NifFormat import io_scene_niftools.utils.logging -from io_scene_niftools.modules.nif_import.object.block_registry import block_store +from io_scene_niftools.modules.nif_import.object.block_registry import block_store, get_bone_name_for_blender from io_scene_niftools.modules.nif_export.block_registry import block_store as block_store_export from io_scene_niftools.modules.nif_import.animation.transform import TransformAnimation from io_scene_niftools.modules.nif_import.object import Object from io_scene_niftools.utils import math -from io_scene_niftools.utils.blocks import safe_decode from io_scene_niftools.utils.logging import NifLog from io_scene_niftools.utils.singleton import NifOp, NifData @@ -81,12 +81,12 @@ def store_pose_matrices(self, n_node, n_root): def get_skinned_geometries(self, n_root): """Yield all children in n_root's tree that have skinning""" # search for all NiTriShape or NiTriStrips blocks... - for n_block in n_root.tree(block_type=NifFormat.NiTriBasedGeom): + for n_block in n_root.tree(block_type=(NifClasses.NiTriBasedGeom, NifClasses.BSTriShape, NifClasses.NiMesh)): # yes, we found one, does it have skinning? if n_block.is_skin(): yield n_block - def get_skin_bind(self, n_bone, geom, n_root): + def get_skin_bind(self, inv_bind, geom, n_root): """Get armature space bind matrix for skin partition bone's inverse bind matrix""" # get the bind pose from the skin data # NiSkinData stores the inverse bind (=rest) pose for each bone, in armature space @@ -95,7 +95,7 @@ def get_skin_bind(self, n_bone, geom, n_root): # this gives a straight rest pose for MW too # return n_bone.get_transform().get_inverse(fast=False) * geom.skin_instance.data.get_transform().get_inverse(fast=False) # however, this conflicts with send_geometries_to_bind_position for MW meshes, so stick to this now - return n_bone.get_transform().get_inverse(fast=False) * geom.get_transform(n_root) + return inv_bind.get_inverse(fast=False) * geom.get_transform(n_root) def bones_iter(self, skin_instance): # might want to make sure that bone_list includes no dupes too to avoid breaking the first mesh @@ -109,43 +109,68 @@ def store_bind_matrices(self, n_armature): # improved from pyffi's send_geometries_to_bind_position & send_bones_to_bind_position NifLog.debug(f"Calculating bind for {n_armature.name}") # prioritize geometries that have most nodes in their skin instance - geoms = sorted(self.get_skinned_geometries(n_armature), key=lambda g: g.skin_instance.num_bones, reverse=True) + sort_function = lambda g: len(g.extra_em_data.bone_transforms) if isinstance(g, NifClasses.NiMesh) else g.skin_instance.num_bones + geoms = sorted(self.get_skinned_geometries(n_armature), key=sort_function, reverse=True) NifLog.debug(f"Found {len(geoms)} skinned geometries") for geom in geoms: NifLog.debug(f"Checking skin of {geom.name}") - skininst = geom.skin_instance - for bonenode, bonedata in self.bones_iter(skininst): - # make sure all bone data of shared bones coincides - # see if the bone has been used by a previous skin instance - if bonenode in self.bind_store: - # get the bind pose that has been stored - diff = (bonedata.get_transform() - * self.bind_store[bonenode] - # * geom.skin_instance.data.get_transform()) use this if relative to skin instead of geom - * geom.get_transform(n_armature).get_inverse(fast=False)) - # there is a difference between the two geometries' bind poses - if not diff.is_identity(): - NifLog.debug(f"Fixing {geom.name} bind position") - # update the skin for all bones of the new geom - for bonenode, bonedata in self.bones_iter(skininst): - NifLog.debug(f"Transforming bind of {bonenode.name}") - bonedata.set_transform(diff.get_inverse(fast=False) * bonedata.get_transform()) - # transforming verts helps with nifs where the skins differ, eg MW vampire or WLP2 Gastornis - for vert in geom.data.vertices: - newvert = vert * diff - vert.x = newvert.x - vert.y = newvert.y - vert.z = newvert.z - for norm in geom.data.normals: - newnorm = norm * diff.get_matrix_33() - norm.x = newnorm.x - norm.y = newnorm.y - norm.z = newnorm.z - break - # store bind pose - for bonenode, bonedata in self.bones_iter(skininst): - NifLog.debug(f"Stored {geom.name} bind position") - self.bind_store[bonenode] = self.get_skin_bind(bonedata, geom, n_armature) + if isinstance(geom, NifClasses.NiMesh): + # bones have no names and are not associated with any NiNodes + for i, bone_transform in enumerate(geom.extra_em_data.bone_transforms): + # Use transpose because the matrices are stored transposed to usual format. + self.bind_store[i] = self.get_skin_bind(bone_transform.get_transpose(), geom, n_armature) + else: + skininst = geom.skin_instance + for bonenode, bonedata in self.bones_iter(skininst): + # make sure all bone data of shared bones coincides + # see if the bone has been used by a previous skin instance + if bonenode in self.bind_store: + # get the bind pose that has been stored + diff = (bonedata.get_transform() + * self.bind_store[bonenode] + # * geom.skin_instance.data.get_transform()) use this if relative to skin instead of geom + * geom.get_transform(n_armature).get_inverse(fast=False)) + # there is a difference between the two geometries' bind poses + if not diff.is_identity(): + NifLog.debug(f"Fixing {geom.name} bind position") + # update the skin for all bones of the new geom + for bonenode, bonedata in self.bones_iter(skininst): + NifLog.debug(f"Transforming bind of {bonenode.name}") + bonedata.set_transform(diff.get_inverse(fast=False) * bonedata.get_transform()) + # transforming verts helps with nifs where the skins differ, eg MW vampire or WLP2 Gastornis + if isinstance(geom, NifClasses.BSTriShape): + if isinstance(geom, NifClasses.BSDynamicTriShape): + # BSDynamicTriShape uses Vector4 to store vertices with a 0 W component, which would + # nullify translation when multiplied by a Matrix44. Hence, first conversion to Vector3 + # and assign the position values back later. + vertices = [NifClasses.Vector3.from_value((vertex.x, vertex.y, vertex.z)) for vertex in geom.vertices] + else: + vertices = [vert_data.vertex for vert_data in geom.skin.skin_partition.vertex_data] + normals = [vert_data.normal for vert_data in geom.skin.skin_partition.vertex_data] + else: + vertices = geom.data.vertices + normals = geom.data.normals + for vert in vertices: + newvert = vert * diff + vert.x = newvert.x + vert.y = newvert.y + vert.z = newvert.z + diff33 = diff.get_matrix_33() + for norm in normals: + newnorm = norm * diff33 + norm.x = newnorm.x + norm.y = newnorm.y + norm.z = newnorm.z + if isinstance(geom, NifClasses.BSDynamicTriShape): + for vertex, t_vertex in zip(geom.vertices, vertices): + vertex.x = t_vertex.x + vertex.y = t_vertex.y + vertex.z = t_vertex.z + break + # store bind pose + for bonenode, bonedata in self.bones_iter(skininst): + NifLog.debug(f"Stored {geom.name} bind position") + self.bind_store[bonenode] = self.get_skin_bind(bonedata.get_transform(), geom, n_armature) NifLog.debug("Storing non-skeletal bone poses") self.fix_pose(n_armature, n_armature) @@ -154,7 +179,7 @@ def fix_pose(self, n_node, n_root): """reposition non-skeletal bones to maintain their local orientation to their skeletal parents""" for n_child_node in n_node.children: # only process nodes - if not isinstance(n_child_node, NifFormat.NiNode): + if not isinstance(n_child_node, NifClasses.NiNode): continue if n_child_node not in self.bind_store and n_child_node in self.pose_store: NifLog.debug(f"Calculating bind pose for non-skeletal bone {n_child_node.name}") @@ -181,8 +206,11 @@ def import_armature(self, n_armature): b_armature_data = bpy.data.armatures.new(armature_name) b_armature_data.display_type = 'STICK' - # use heuristics to determine a suitable orientation - forward, up = self.guess_orientation(n_armature) + # use heuristics to determine a suitable orientation, if requested + if not NifOp.props.override_armature_orientation: + forward, up = self.guess_orientation(n_armature) + else: + forward, up = (NifOp.props.axis_forward, NifOp.props.axis_up) # pass them to the matrix utility math.set_bone_orientation(forward, up) # store axis orientation for export @@ -210,11 +238,12 @@ def import_armature(self, n_armature): self.transform_anim.get_bind_data(b_armature_obj) for bone_name, b_bone in b_armature_obj.data.bones.items(): - n_block = self.name_to_block[bone_name] - # the property is only available from object mode! - block_store.store_longname(b_bone, safe_decode(n_block.name)) - if NifOp.props.animation: - self.transform_anim.import_transforms(n_block, b_armature_obj, bone_name) + n_block = self.name_to_block.get(bone_name) + if n_block: + # the property is only available from object mode! + block_store.store_longname(b_bone, n_block.name) + if NifOp.props.animation: + self.transform_anim.import_transforms(n_block, b_armature_obj, bone_name) # import pose for b_name, n_block in self.name_to_block.items(): @@ -227,19 +256,12 @@ def import_armature(self, n_armature): return b_armature_obj - def import_bone_bind(self, n_block, b_armature_data, n_armature, b_parent_bone=None): + def create_bone(self, bone_name, bind_key, b_armature_data, b_parent_bone=None): """Adds a bone to the armature in edit mode.""" - # check that n_block is indeed a bone - if not self.is_bone(n_block): - return None - # bone name - bone_name = block_store.import_name(n_block) # create a new bone b_edit_bone = b_armature_data.edit_bones.new(bone_name) - # store nif block for access from object mode - self.name_to_block[b_edit_bone.name] = n_block # get the nif bone's armature space matrix (under the hood all bone space matrixes are multiplied together) - n_bind = math.nifformat_to_mathutils_matrix(self.bind_store.get(n_block, NifFormat.Matrix44())) + n_bind = math.nifformat_to_mathutils_matrix(self.bind_store.get(bind_key, NifClasses.Matrix44())) # get transformation in blender's coordinate space b_bind = math.nif_bind_to_blender_bind(n_bind) @@ -249,6 +271,24 @@ def import_bone_bind(self, n_block, b_armature_data, n_armature, b_parent_bone=N # link to parent if b_parent_bone: b_edit_bone.parent = b_parent_bone + return b_edit_bone + + def import_bone_bind(self, n_block, b_armature_data, n_armature, b_parent_bone=None): + """Adds a bone to the armature in edit mode.""" + if isinstance(n_block, NifClasses.NiMesh): + # NiMesh has bones, but they are not separate blocks + for i, transform in enumerate(n_block.extra_em_data.bone_transforms): + bone_name = get_bone_name_for_blender(str(i)) + b_edit_bone = self.create_bone(bone_name, i, b_armature_data, b_parent_bone) + return + elif not self.is_bone(n_block): + # check that n_block is indeed a bone + return None + # bone name + bone_name = block_store.import_name(n_block) + b_edit_bone = self.create_bone(bone_name, n_block, b_armature_data, b_parent_bone) + # store nif block for access from object mode + self.name_to_block[b_edit_bone.name] = n_block # import and parent bone children for n_child in n_block.children: self.import_bone_bind(n_child, b_armature_data, n_armature, b_edit_bone) @@ -271,21 +311,28 @@ def argmax(values): """Return the index of the max value in values""" return max(zip(values, range(len(values))))[1] - def get_forward_axis(self, n_bone, axis_indices): - """Helper function to get the forward axis of a bone""" - # check that n_block is indeed a bone - if not self.is_bone(n_bone): - return None - trans = n_bone.translation.as_tuple() - trans_abs = tuple(abs(v) for v in trans) + @classmethod + def max_coord_ind_from_translation(cls, translation): + trans_abs = tuple(abs(v) for v in translation) # get the index of the coordinate with the biggest absolute value - max_coord_ind = self.argmax(trans_abs) + max_coord_ind = cls.argmax(trans_abs) # now check the sign - actual_value = trans[max_coord_ind] + actual_value = translation[max_coord_ind] # handle sign accordingly so negative indices map to the negative identifiers in list if actual_value < 0: max_coord_ind += 3 - axis_indices.append(max_coord_ind) + return max_coord_ind + + def get_forward_axis(self, n_bone, axis_indices): + """Helper function to get the forward axis of a bone""" + # check that n_block is indeed a bone + if isinstance(n_bone, NifClasses.NiMesh): + for transform in n_bone.extra_em_data.bone_transforms: + axis_indices.append(self.max_coord_ind_from_translation(transform.get_transpose().get_translation())) + return + elif not self.is_bone(n_bone): + return None + axis_indices.append(self.max_coord_ind_from_translation(n_bone.translation)) # move down the hierarchy for n_child in n_bone.children: self.get_forward_axis(n_child, axis_indices) @@ -326,12 +373,12 @@ def check_for_skin(self, n_root): def is_bone(self, ni_block): """Tests a NiNode to see if it has been marked as a bone.""" - if isinstance(ni_block, NifFormat.NiNode): + if isinstance(ni_block, NifClasses.NiNode): return self.skinned def is_armature_root(self, n_block): """Tests a block to see if it's an armature.""" - if isinstance(n_block, NifFormat.NiNode): + if isinstance(n_block, NifClasses.NiNode): # we have skinning and are waiting for a suitable start node of the tree if self.skinned and not self.n_armature: # now store it as the nif armature's root diff --git a/io_scene_niftools/modules/nif_import/collision/__init__.py b/io_scene_niftools/modules/nif_import/collision/__init__.py index c4de24834..2f95ff8bc 100644 --- a/io_scene_niftools/modules/nif_import/collision/__init__.py +++ b/io_scene_niftools/modules/nif_import/collision/__init__.py @@ -94,19 +94,10 @@ def set_b_collider(b_obj, radius, n_obj=None, bounds_type='BOX', display_type='B b_me = b_obj.data if n_obj: - # todo [pyffi] nif xml 0.7.1.1 HavokMaterial is a union of 3 enums under the HavokMaterial.material field, probably broken! - # needs union support on pyffi end - for mat_type in ("material", "oblivion_havok_material", "fallout_3_havok_material", "skyrim_havok_material"): - havok_material = getattr(n_obj, mat_type, None) - if havok_material: - if hasattr(havok_material, "material"): - # HavokMaterial.material is an enum under the hood - # pyffi exposes it as an int (struct.get_basic_attribute) and returns the enum's default value - # we treat it as if it was non-basic to get the enum itself - mat_enum = havok_material.get_attribute("material") - mat_name = str(mat_enum) - else: - # fallback, not sure if we should do this - mat_name = str(havok_material) - b_mat = get_material(mat_name) - b_me.materials.append(b_mat) + havok_material = getattr(n_obj, 'material', None) + if havok_material: + if hasattr(havok_material, "material"): + mat_enum = havok_material.material + mat_name = mat_enum.name + b_mat = get_material(mat_name) + b_me.materials.append(b_mat) diff --git a/io_scene_niftools/modules/nif_import/collision/bound.py b/io_scene_niftools/modules/nif_import/collision/bound.py index 87d452e88..59674ffeb 100644 --- a/io_scene_niftools/modules/nif_import/collision/bound.py +++ b/io_scene_niftools/modules/nif_import/collision/bound.py @@ -36,7 +36,7 @@ # POSSIBILITY OF SUCH DAMAGE. # # ***** END LICENSE BLOCK ***** -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.modules.nif_import.collision import Collision from io_scene_niftools.modules.nif_import.object import Object @@ -68,30 +68,33 @@ def import_bounding_volume(self, bounding_volume): def import_bounding_box(self, n_block): """Import a NiNode's bounding box or attached BSBound extra data.""" - if not n_block or not isinstance(n_block, NifFormat.NiNode): + if not n_block or not isinstance(n_block, NifClasses.NiNode): return [] # we have a ninode with bounding box - if n_block.has_bounding_box: - b_name = 'Bounding Box' + if n_block.has_bounding_volume: + b_name = 'Bounding Volume' - # Ninode's bbox behaves like a seperate mesh. - # bounding_box center(n_block.bounding_box.translation) is relative to the bound_box - n_bl_trans = n_block.translation - n_bbox = n_block.bounding_box - n_b_trans = n_bbox.translation - minx = n_b_trans.x - n_bl_trans.x - n_bbox.radius.x - miny = n_b_trans.y - n_bl_trans.y - n_bbox.radius.y - minz = n_b_trans.z - n_bl_trans.z - n_bbox.radius.z - maxx = n_b_trans.x - n_bl_trans.x + n_bbox.radius.x - maxy = n_b_trans.y - n_bl_trans.y + n_bbox.radius.y - maxz = n_b_trans.z - n_bl_trans.z + n_bbox.radius.z - bbox_center = n_b_trans.as_list() + if n_block.bounding_volume.collision_type == NifClasses.BoundVolumeType.BOX_BV: + # Ninode's bbox behaves like a seperate mesh. + # bounding_box center(n_block.bounding_box.translation) is relative to the bound_box + n_bl_trans = n_block.translation + n_bbox = n_block.bounding_volume.box + n_b_trans = n_bbox.translation + minx = n_b_trans.x - n_bl_trans.x - n_bbox.radius.x + miny = n_b_trans.y - n_bl_trans.y - n_bbox.radius.y + minz = n_b_trans.z - n_bl_trans.z - n_bbox.radius.z + maxx = n_b_trans.x - n_bl_trans.x + n_bbox.radius.x + maxy = n_b_trans.y - n_bl_trans.y + n_bbox.radius.y + maxz = n_b_trans.z - n_bl_trans.z + n_bbox.radius.z + bbox_center = n_b_trans.as_list() + else: + raise NotImplementedError("Non-box bounding volume are not yet supported.") # we may still have a BSBound extra data attached to this node else: for n_extra in n_block.get_extra_datas(): # TODO [extra][data] Move to property processor - if isinstance(n_extra, NifFormat.BSBound): + if isinstance(n_extra, NifClasses.BSBound): b_name = 'BSBound' center = n_extra.center dims = n_extra.dimensions @@ -138,9 +141,8 @@ def import_capsulebv(self, capsule): offset = capsule.center # always a normalized vector direction = capsule.origin - # nb properly named in newer nif.xmls - extent = capsule.unknown_float_1 - radius = capsule.unknown_float_2 + extent = capsule.extent + radius = capsule.radius # positions of the box verts minx = miny = -radius diff --git a/io_scene_niftools/modules/nif_import/collision/havok.py b/io_scene_niftools/modules/nif_import/collision/havok.py index fa4e4be74..2fe99b4d9 100644 --- a/io_scene_niftools/modules/nif_import/collision/havok.py +++ b/io_scene_niftools/modules/nif_import/collision/havok.py @@ -37,13 +37,14 @@ # # ***** END LICENSE BLOCK ***** +import bpy import mathutils import operator from functools import reduce, singledispatch -from pyffi.formats.nif import NifFormat -from pyffi.utils.quickhull import qhull3d +from generated.formats.nif import classes as NifClasses +from generated.utils.quickhull import qhull3d from io_scene_niftools.modules.nif_import import collision from io_scene_niftools.modules.nif_import.collision import Collision @@ -61,25 +62,26 @@ def __init__(self): collision.DICT_HAVOK_OBJECTS = {} # TODO [collision][havok][property] Need better way to set this, maybe user property - if NifData.data._user_version_value_._value == 12 and NifData.data._user_version_2_value_._value == 83: + if bpy.context.scene.niftools_scene.user_version == 12 and bpy.context.scene.niftools_scene.user_version_2 == 83: self.HAVOK_SCALE = consts.HAVOK_SCALE * 10 else: self.HAVOK_SCALE = consts.HAVOK_SCALE self.process_bhk = singledispatch(self.process_bhk) - self.process_bhk.register(NifFormat.bhkTransformShape, self.import_bhktransform) - self.process_bhk.register(NifFormat.bhkRigidBodyT, self.import_bhk_ridgidbody_t) - self.process_bhk.register(NifFormat.bhkRigidBody, self.import_bhk_ridgid_body) - self.process_bhk.register(NifFormat.bhkBoxShape, self.import_bhkbox_shape) - self.process_bhk.register(NifFormat.bhkSphereShape, self.import_bhksphere_shape) - self.process_bhk.register(NifFormat.bhkCapsuleShape, self.import_bhkcapsule_shape) - self.process_bhk.register(NifFormat.bhkConvexVerticesShape, self.import_bhkconvex_vertices_shape) - self.process_bhk.register(NifFormat.bhkPackedNiTriStripsShape, self.import_bhkpackednitristrips_shape) - self.process_bhk.register(NifFormat.bhkNiTriStripsShape, self.import_bhk_nitristrips_shape) - self.process_bhk.register(NifFormat.NiTriStripsData, self.import_nitristrips) - self.process_bhk.register(NifFormat.bhkMoppBvTreeShape, self.import_bhk_mopp_bv_tree_shape) - self.process_bhk.register(NifFormat.bhkListShape, self.import_bhk_list_shape) - self.process_bhk.register(NifFormat.bhkSimpleShapePhantom, self.import_bhk_simple_shape_phantom) + self.process_bhk.register(NifClasses.BhkTransformShape, self.import_bhktransform) + self.process_bhk.register(NifClasses.BhkConvexTransformShape, self.import_bhk_convex_transform) + self.process_bhk.register(NifClasses.BhkRigidBodyT, self.import_bhk_rigidbody_t) + self.process_bhk.register(NifClasses.BhkRigidBody, self.import_bhk_rigid_body) + self.process_bhk.register(NifClasses.BhkBoxShape, self.import_bhkbox_shape) + self.process_bhk.register(NifClasses.BhkSphereShape, self.import_bhksphere_shape) + self.process_bhk.register(NifClasses.BhkCapsuleShape, self.import_bhkcapsule_shape) + self.process_bhk.register(NifClasses.BhkConvexVerticesShape, self.import_bhkconvex_vertices_shape) + self.process_bhk.register(NifClasses.BhkPackedNiTriStripsShape, self.import_bhkpackednitristrips_shape) + self.process_bhk.register(NifClasses.BhkNiTriStripsShape, self.import_bhk_nitristrips_shape) + self.process_bhk.register(NifClasses.NiTriStripsData, self.import_nitristrips) + self.process_bhk.register(NifClasses.BhkMoppBvTreeShape, self.import_bhk_mopp_bv_tree_shape) + self.process_bhk.register(NifClasses.BhkListShape, self.import_bhk_list_shape) + self.process_bhk.register(NifClasses.BhkSimpleShapePhantom, self.import_bhk_simple_shape_phantom) def process_bhk(self, bhk_shape): """Base method to warn user that this property is not supported""" @@ -122,8 +124,14 @@ def import_bhk_simple_shape_phantom(self, bhkshape): return collision_objs def import_bhktransform(self, bhkshape): - """Imports a BhkTransform block and applies the transform to the collision object""" + """Imports a BhkTransformShape block and applies the transform to the collision object""" + return self._import_bhk_transform(bhkshape) + def import_bhk_convex_transform(self, bhkshape): + """Imports a BhkConvexTransformShape block and applies the transform to the collision object""" + return self._import_bhk_transform(bhkshape) + + def _import_bhk_transform(self, bhkshape): # import shapes collision_objs = self.import_bhk_shape(bhkshape.shape) # find transformation matrix @@ -138,20 +146,21 @@ def import_bhktransform(self, bhkshape): # return a list of transformed collision shapes return collision_objs - def import_bhk_ridgidbody_t(self, bhk_shape): + def import_bhk_rigidbody_t(self, bhk_shape): """Imports a BhkRigidBody block and applies the transform to the collision objects""" NifLog.debug(f"Importing {bhk_shape.__class__.__name__}") # import shapes collision_objs = self.import_bhk_shape(bhk_shape.shape) + body_info = bhk_shape.rigid_body_info # find transformation matrix in case of the T version # set rotation - b_rot = bhk_shape.rotation + b_rot = body_info.rotation transform = mathutils.Quaternion([b_rot.w, b_rot.x, b_rot.y, b_rot.z]).to_matrix().to_4x4() # set translation - b_trans = bhk_shape.translation + b_trans = body_info.translation transform.translation = mathutils.Vector((b_trans.x, b_trans.y, b_trans.z)) * self.HAVOK_SCALE # apply transform @@ -163,7 +172,7 @@ def import_bhk_ridgidbody_t(self, bhk_shape): # and return a list of transformed collision shapes return collision_objs - def import_bhk_ridgid_body(self, bhk_shape): + def import_bhk_rigid_body(self, bhk_shape): """Imports a BhkRigidBody block and applies the transform to the collision objects""" NifLog.debug(f"Importing {bhk_shape.__class__.__name__}") @@ -175,38 +184,39 @@ def import_bhk_ridgid_body(self, bhk_shape): return collision_objs def _import_bhk_rigid_body(self, bhkshape, collision_objs): + body_info = bhkshape.rigid_body_info # set physics flags and mass for b_col_obj in collision_objs: b_r_body = b_col_obj.rigid_body - if bhkshape.mass > 0.0001: + if bhkshape.rigid_body_info.mass > 0.0001: # for physics emulation # (mass 0 results in issues with simulation) - b_r_body.mass = bhkshape.mass / len(collision_objs) + b_r_body.mass = body_info.mass / len(collision_objs) - b_r_body.mass = bhkshape.mass / len(collision_objs) + b_r_body.mass = body_info.mass / len(collision_objs) b_r_body.use_deactivation = True - b_r_body.friction = bhkshape.friction - b_r_body.restitution = bhkshape.restitution - b_r_body.linear_damping = bhkshape.linear_damping - b_r_body.angular_damping = bhkshape.angular_damping - vel = bhkshape.linear_velocity + b_r_body.friction = body_info.friction + b_r_body.restitution = body_info.restitution + b_r_body.linear_damping = body_info.linear_damping + b_r_body.angular_damping = body_info.angular_damping + vel = body_info.linear_velocity b_r_body.deactivate_linear_velocity = mathutils.Vector([vel.w, vel.x, vel.y, vel.z]).magnitude - ang_vel = bhkshape.angular_velocity + ang_vel = body_info.angular_velocity b_r_body.deactivate_angular_velocity = mathutils.Vector([ang_vel.w, ang_vel.x, ang_vel.y, ang_vel.z]).magnitude # Custom Niftools properties - b_col_obj.nifcollision.penetration_depth = bhkshape.penetration_depth - b_col_obj.nifcollision.deactivator_type = NifFormat.DeactivatorType._enumkeys[bhkshape.deactivator_type] - b_col_obj.nifcollision.solver_deactivation = NifFormat.SolverDeactivation._enumkeys[bhkshape.solver_deactivation] - b_col_obj.nifcollision.max_linear_velocity = bhkshape.max_linear_velocity - b_col_obj.nifcollision.max_angular_velocity = bhkshape.max_angular_velocity + b_col_obj.nifcollision.penetration_depth = body_info.penetration_depth + b_col_obj.nifcollision.deactivator_type = body_info.deactivator_type.name + b_col_obj.nifcollision.solver_deactivation = body_info.solver_deactivation.name + b_col_obj.nifcollision.max_linear_velocity = body_info.max_linear_velocity + b_col_obj.nifcollision.max_angular_velocity = body_info.max_angular_velocity - b_col_obj.nifcollision.collision_layer = str(bhkshape.layer) + b_col_obj.nifcollision.collision_layer = str(body_info.havok_filter.layer.value) # b_col_obj.nifcollision.quality_type = NifFormat.MotionQuality._enumkeys[bhkshape.quality_type] # b_col_obj.nifcollision.motion_system = NifFormat.MotionSystem._enumkeys[bhkshape.motion_system] - # b_col_obj.nifcollision.col_filter = bhkshape.col_filter + b_col_obj.nifcollision.col_filter = bhkshape.havok_filter.flags # import constraints # this is done once all objects are imported for now, store all imported havok shapes with object lists @@ -327,5 +337,5 @@ def import_nitristrips(self, bhk_shape): faces = list(bhk_shape.get_triangles()) b_obj = Object.mesh_from_data("poly", verts, faces) # TODO [collision] self.havok_mat! - self.set_b_collider(b_obj, bounds_type="MESH", radius=bhk_shape.radius) + self.set_b_collider(b_obj, bounds_type="MESH", radius=bhk_shape.bounding_sphere.radius) return [b_obj] diff --git a/io_scene_niftools/modules/nif_import/constraint/__init__.py b/io_scene_niftools/modules/nif_import/constraint/__init__.py index 62c99ce55..937f131b4 100644 --- a/io_scene_niftools/modules/nif_import/constraint/__init__.py +++ b/io_scene_niftools/modules/nif_import/constraint/__init__.py @@ -37,8 +37,9 @@ # # ***** END LICENSE BLOCK ***** +import bpy import mathutils -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.modules.nif_import import collision from io_scene_niftools.utils.singleton import NifData @@ -49,7 +50,7 @@ class Constraint: def __init__(self): # TODO [collision][havok][property] Need better way to set this, maybe user property - if NifData.data._user_version_value_._value == 12 and NifData.data._user_version_2_value_._value == 83: + if bpy.context.scene.niftools_scene.user_version == 12 and bpy.context.scene.niftools_scene.user_version_2 == 83: self.HAVOK_SCALE = collision.HAVOK_SCALE * 10 else: self.HAVOK_SCALE = collision.HAVOK_SCALE @@ -60,7 +61,7 @@ def import_bhk_constraints(self): def import_constraint(self, hkbody): """Imports a bone havok constraint as Blender object constraint.""" - assert (isinstance(hkbody, NifFormat.bhkRigidBody)) + assert (isinstance(hkbody, NifClasses.BhkRigidBody)) # check for constraints if not hkbody.constraints: @@ -78,33 +79,31 @@ def import_constraint(self, hkbody): # now import all constraints for hkconstraint in hkbody.constraints: - # check constraint entities - if not hkconstraint.num_entities == 2: + # check constraint + c_info = hkconstraint.constraint_info + if not c_info.num_entities == 2: NifLog.warn("Constraint with more than 2 entities, skipped") continue - if not hkconstraint.entities[0] is hkbody: + if not c_info.entity_a is hkbody: NifLog.warn("First constraint entity not self, skipped") continue - if not hkconstraint.entities[1] in collision.DICT_HAVOK_OBJECTS: + if not c_info.entity_b in collision.DICT_HAVOK_OBJECTS: NifLog.warn("Second constraint entity not imported, skipped") continue # get constraint descriptor - if isinstance(hkconstraint, NifFormat.bhkRagdollConstraint): - hkdescriptor = hkconstraint.ragdoll + hkdescriptor = hkconstraint.constraint + if isinstance(hkdescriptor, (NifClasses.BhkRagdollConstraintCInfo, + NifClasses.BhkLimitedHingeConstraintCInfo, + NifClasses.BhkHingeConstraintCInfo)): b_hkobj.rigid_body.enabled = True - elif isinstance(hkconstraint, NifFormat.bhkLimitedHingeConstraint): - hkdescriptor = hkconstraint.limited_hinge - b_hkobj.rigid_body.enabled = True - elif isinstance(hkconstraint, NifFormat.bhkHingeConstraint): - hkdescriptor = hkconstraint.hinge - b_hkobj.rigid_body.enabled = True - elif isinstance(hkconstraint, NifFormat.bhkMalleableConstraint): - if hkconstraint.type == 7: - hkdescriptor = hkconstraint.ragdoll + elif isinstance(hkdescriptor, NifClasses.BhkMalleableConstraintCInfo): + # TODO [constraint] add other types used by malleable constraint (for values 0, 1, 6 and 8) + if hkdescriptor.type == 2: + hkdescriptor = hkdescriptor.limited_hinge b_hkobj.rigid_body.enabled = False - elif hkconstraint.type == 2: - hkdescriptor = hkconstraint.limited_hinge + elif hkdescriptor.type == 7: + hkdescriptor = hkdescriptor.ragdoll b_hkobj.rigid_body.enabled = False else: NifLog.warn(f"Unknown malleable type ({hkconstraint.type:s}), skipped") @@ -168,7 +167,7 @@ def import_constraint(self, hkbody): # get z- and x-axes of the constraint # (also see export_nif.py NifImport.export_constraints) - if isinstance(hkdescriptor, NifFormat.RagdollDescriptor): + if isinstance(hkdescriptor, NifClasses.BhkRagdollConstraintCInfo): b_constr.pivot_type = 'CONE_TWIST' # for ragdoll, take z to be the twist axis (central axis of the # cone, that is) @@ -193,7 +192,7 @@ def import_constraint(self, hkbody): b_hkobj.niftools_constraint.LHMaxFriction = hkdescriptor.max_friction - elif isinstance(hkdescriptor, NifFormat.LimitedHingeDescriptor): + elif isinstance(hkdescriptor, NifClasses.BhkLimitedHingeConstraintCInfo): # for hinge, y is the vector on the plane of rotation defining # the zero angle axis_y = mathutils.Vector((hkdescriptor.perp_2_axle_in_a_1.x, @@ -228,7 +227,7 @@ def import_constraint(self, hkbody): b_hkobj.niftools_constraint.tau = hkconstraint.tau b_hkobj.niftools_constraint.damping = hkconstraint.damping - elif isinstance(hkdescriptor, NifFormat.HingeDescriptor): + elif isinstance(hkdescriptor, NifClasses.HingeDescriptor): # for hinge, y is the vector on the plane of rotation defining # the zero angle axis_y = mathutils.Vector((hkdescriptor.perp_2_axle_in_a_1.x, @@ -271,7 +270,7 @@ def import_constraint(self, hkbody): # which is exactly enough to provide the euler angles # multiply with rigid body transform - if isinstance(hkbody, NifFormat.bhkRigidBodyT): + if isinstance(hkbody, NifClasses.BhkRigidBodyT): # set rotation transform = mathutils.Quaternion((hkbody.rotation.w, hkbody.rotation.x, @@ -335,10 +334,10 @@ def import_constraint(self, hkbody): assert ((axis_z - mathutils.Vector((0, 0, 1)) * constr_matrix).length < 0.0001) # the generic rigid body type is very buggy... so for simulation purposes let's transform it into ball and hinge - if isinstance(hkdescriptor, NifFormat.RagdollDescriptor): + if isinstance(hkdescriptor, NifClasses.BhkRagdollConstraintCInfo): # cone_twist b_constr.pivot_type = 'CONE_TWIST' - elif isinstance(hkdescriptor, (NifFormat.LimitedHingeDescriptor, NifFormat.HingeDescriptor)): + elif isinstance(hkdescriptor, (NifClasses.BhkLimitedHingeConstraintCInfo, NifClasses.HingeDescriptor)): # (limited) hinge b_constr.pivot_type = 'HINGE' else: diff --git a/io_scene_niftools/modules/nif_import/geometry/mesh/__init__.py b/io_scene_niftools/modules/nif_import/geometry/mesh/__init__.py index 71cfc6327..d781e316c 100644 --- a/io_scene_niftools/modules/nif_import/geometry/mesh/__init__.py +++ b/io_scene_niftools/modules/nif_import/geometry/mesh/__init__.py @@ -36,9 +36,8 @@ # # ***** END LICENSE BLOCK ***** -import mathutils - -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses +from generated.formats.nif.nimesh.structs.DisplayList import DisplayList import io_scene_niftools.utils.logging from io_scene_niftools.modules.nif_import.animation.morph import MorphAnimation @@ -49,11 +48,13 @@ from io_scene_niftools.modules.nif_import.property.geometry.mesh import MeshPropertyProcessor from io_scene_niftools.utils import math from io_scene_niftools.utils.singleton import NifOp -from io_scene_niftools.utils.logging import NifLog +from io_scene_niftools.utils.logging import NifLog, NifError class Mesh: + supported_mesh_types = (NifClasses.BSTriShape, NifClasses.NiMesh, NifClasses.NiTriBasedGeom) + def __init__(self): self.materialhelper = Material() self.morph_anim = MorphAnimation() @@ -67,34 +68,102 @@ def import_mesh(self, n_block, b_obj): :param b_obj: The mesh to which to append the geometry data. If C{None}, a new mesh is created. :type b_obj: A Blender object that has mesh data. """ - assert (isinstance(n_block, NifFormat.NiTriBasedGeom)) - node_name = n_block.name.decode() + node_name = n_block.name NifLog.info(f"Importing mesh data for geometry '{node_name}'") b_mesh = b_obj.data - # shortcut for mesh geometry data - n_tri_data = n_block.data - if not n_tri_data: - raise io_scene_niftools.utils.logging.NifError(f"No shape data in {node_name}") + assert isinstance(n_block, self.supported_mesh_types) + + vertices = [] + triangles = [] + uvs = None + vertex_colors = None + normals = None + + if isinstance(n_block, NifClasses.BSTriShape): + vertex_attributes = n_block.vertex_desc.vertex_attributes + vertex_data = n_block.get_vertex_data() + if isinstance(n_block, NifClasses.BSDynamicTriShape): + # for BSDynamicTriShapes, the vertex data is stored in 4-component vertices + vertices = [(vertex.x, vertex.y, vertex.z) for vertex in n_block.vertices] + elif vertex_attributes.vertex: + vertices = [vertex.vertex for vertex in vertex_data] + triangles = n_block.get_triangles() + if vertex_attributes.u_vs: + uvs = [[vertex.uv for vertex in vertex_data]] + if vertex_attributes.vertex_colors: + vertex_colors = [vertex.vertex_colors for vertex in vertex_data] + if vertex_attributes.normals: + normals = [vertex.normal for vertex in vertex_data] + elif isinstance(n_block, NifClasses.NiMesh): + # if it has a displaylist then we don't know how to process this NiMesh + displaylist_data = n_block.geomdata_by_name("DISPLAYLIST", False, False) + if len(displaylist_data) > 0: + displaylist = DisplayList(displaylist_data) + vertices_info, triangles = displaylist.create_mesh_data(n_block) + vertices = vertices_info[0] + normals = vertices_info[1] + vertex_colors = [NifClasses.Color4.from_value(color) for color in vertices_info[2]] + uvs = [[NifClasses.TexCoord.from_value(tex_coord) for tex_coord in vertices_info[3]]] + else: + # get the data from the associated nidatastreams based on the description in the component semantics + vertices.extend(n_block.geomdata_by_name("POSITION", sep_datastreams=False)) + vertices.extend(n_block.geomdata_by_name("POSITION_BP", sep_datastreams=False)) + triangles = n_block.get_triangles() + uvs = n_block.geomdata_by_name("TEXCOORD") + if len(uvs) == 0: + uvs = None + else: + uvs = [[NifClasses.TexCoord.from_value(tex_coord) for tex_coord in uv_coords] for uv_coords in uvs] + vertex_colors = n_block.geomdata_by_name("COLOR", sep_datastreams=False) + if len(vertex_colors) == 0: + vertex_colors = None + else: + vertex_colors = [NifClasses.Color4.from_value(color) for color in vertex_colors] + normals = n_block.geomdata_by_name("NORMAL", sep_datastreams=False) + normals.extend(n_block.geomdata_by_name("NORMAL_BP", sep_datastreams=False)) + if len(normals) == 0: + normals = None + else: + # for some reason, normals can be four-component structs instead of 3, discard the 4th. + if len(normals[0]) > 3: + normals = [(n[0], n[1], n[2]) for n in normals] + elif isinstance(n_block, NifClasses.NiTriBasedGeom): + + # shortcut for mesh geometry data + n_tri_data = n_block.data + if not n_tri_data: + raise io_scene_niftools.utils.logging.NifError(f"No shape data in {node_name}") + vertices = n_tri_data.vertices + triangles = n_block.get_triangles() + uvs = n_tri_data.uv_sets + if n_tri_data.has_vertex_colors: + vertex_colors = n_tri_data.vertex_colors + if n_tri_data.has_normals: + normals = n_tri_data.normals # create raw mesh from vertices and triangles - b_mesh.from_pydata(n_tri_data.vertices, [], n_tri_data.get_triangles()) + b_mesh.from_pydata(vertices, [], triangles) b_mesh.update() # must set faces to smooth before setting custom normals, or the normals bug out! - is_smooth = True if (n_tri_data.has_normals or n_block.skin_instance) else False + is_smooth = True if (not(normals is None) or n_block.is_skin()) else False self.set_face_smooth(b_mesh, is_smooth) # store additional data layers - Vertex.map_uv_layer(b_mesh, n_tri_data) - Vertex.map_vertex_colors(b_mesh, n_tri_data) - Vertex.map_normals(b_mesh, n_tri_data) + if uvs is not None: + Vertex.map_uv_layer(b_mesh, uvs) + if vertex_colors is not None: + Vertex.map_vertex_colors(b_mesh, vertex_colors) + if normals is not None: + Vertex.map_normals(b_mesh, normals) self.mesh_prop_processor.process_property_list(n_block, b_obj) # import skinning info, for meshes affected by bones - VertexGroup.import_skin(n_block, b_obj) + if n_block.is_skin(): + VertexGroup.import_skin(n_block, b_obj) # import morph controller if NifOp.props.animation: diff --git a/io_scene_niftools/modules/nif_import/geometry/vertex/__init__.py b/io_scene_niftools/modules/nif_import/geometry/vertex/__init__.py index 9d76e88b4..003a16b09 100644 --- a/io_scene_niftools/modules/nif_import/geometry/vertex/__init__.py +++ b/io_scene_niftools/modules/nif_import/geometry/vertex/__init__.py @@ -44,31 +44,28 @@ class Vertex: @staticmethod - def map_vertex_colors(b_mesh, n_tri_data): - if n_tri_data.has_vertex_colors: - b_mesh.vertex_colors.new(name=f"RGBA") - b_mesh.vertex_colors[-1].data.foreach_set("color", [channel for col in [n_tri_data.vertex_colors[loop.vertex_index] for loop in b_mesh.loops] for channel in (col.r, col.g, col.b, col.a)]) + def map_vertex_colors(b_mesh, vertex_colors): + b_mesh.vertex_colors.new(name=f"RGBA") + b_mesh.vertex_colors[-1].data.foreach_set("color", [channel for col in [vertex_colors[loop.vertex_index] for loop in b_mesh.loops] for channel in (col.r, col.g, col.b, col.a)]) @staticmethod - def map_uv_layer(b_mesh, n_tri_data): + def map_uv_layer(b_mesh, uv_sets): """ UV coordinates, NIF files only support 'sticky' UV coordinates, and duplicates vertices to emulate hard edges and UV seam. So whenever a hard edge or a UV seam is present the mesh, vertices are duplicated. Blender only must duplicate vertices for hard edges; duplicating for UV seams would introduce unnecessary hard edges.""" # "sticky" UV coordinates: these are transformed in Blender UV's - for uv_i, uv_set in enumerate(n_tri_data.uv_sets): + for uv_i, uv_set in enumerate(uv_sets): b_mesh.uv_layers.new(name=f"UV{uv_i}") b_mesh.uv_layers[-1].data.foreach_set("uv", [coord for uv in [uv_set[loop.vertex_index] for loop in b_mesh.loops] for coord in (uv.u, 1.0 - uv.v)]) @staticmethod - def map_normals(b_mesh, n_tri_data): + def map_normals(b_mesh, normals): """Import nif normals as custom normals.""" - if not n_tri_data.has_normals: - return - assert len(b_mesh.vertices) == len(n_tri_data.normals) + assert len(b_mesh.vertices) == len(normals) # set normals if NifOp.props.use_custom_normals: - no_array = np.array([normal.as_tuple() for normal in n_tri_data.normals]) + no_array = np.array(normals) # the normals need to be pre-normalized or blender will do it inconsistely, leading to marked sharp edges no_array = Vertex.normalize(no_array) # use normals_split_custom_set_from_vertices to set the loop custom normals from the per-vertex normals diff --git a/io_scene_niftools/modules/nif_import/geometry/vertex/groups.py b/io_scene_niftools/modules/nif_import/geometry/vertex/groups.py index 3665ee06c..c18872900 100644 --- a/io_scene_niftools/modules/nif_import/geometry/vertex/groups.py +++ b/io_scene_niftools/modules/nif_import/geometry/vertex/groups.py @@ -36,9 +36,13 @@ # POSSIBILITY OF SUCH DAMAGE. # # ***** END LICENSE BLOCK ***** -from pyffi.formats.nif import NifFormat -from io_scene_niftools.modules.nif_import.object.block_registry import block_store +import numpy as np +from itertools import chain + +from generated.formats.nif import classes as NifClasses + +from io_scene_niftools.modules.nif_import.object.block_registry import block_store, get_bone_name_for_blender from io_scene_niftools.utils.logging import NifLog @@ -57,7 +61,7 @@ def get_skin_deformation_from_partition(n_geom): skin_data = skin_inst.data skin_partition = skin_inst.skin_partition skel_root = skin_inst.skeleton_root - vertices = [NifFormat.Vector3() for _ in range(n_geom.data.num_vertices)] + vertices = [NifClasses.Vector3() for _ in range(n_geom.data.num_vertices)] # ignore normals for now, not needed for import sum_weights = [0.0 for _ in range(n_geom.data.num_vertices)] @@ -73,7 +77,7 @@ def get_skin_deformation_from_partition(n_geom): bone_transforms.append(transform) # now the actual unique bit - for block in skin_partition.skin_partition_blocks: + for block in skin_partition.partitions: # create all vgroups for this block's bones block_bone_transforms = [bone_transforms[i] for i in block.bones] @@ -100,7 +104,7 @@ def get_skin_deformation_from_partition(n_geom): def apply_skin_deformation(n_data): """ Process all geometries in NIF tree to apply their skin """ # get all geometries with skin - n_geoms = [g for g in n_data.get_global_iterator() if isinstance(g, NifFormat.NiGeometry) and g.is_skin()] + n_geoms = [g for g in n_data.get_global_iterator() if isinstance(g, NifClasses.NiGeometry) and g.is_skin()] # make sure that each skin is applied only once to avoid distortions when a model is referred to twice for n_geom in set(n_geoms): @@ -122,67 +126,98 @@ def apply_skin_deformation(n_data): @staticmethod def import_skin(ni_block, b_obj): """Import a NiSkinInstance and its contents as vertex groups""" - skininst = ni_block.skin_instance - if skininst: - skindata = skininst.data - bones = skininst.bones - # the usual case - if skindata.has_vertex_weights: - bone_weights = skindata.bone_list - for idx, n_bone in enumerate(bones): - # skip empty bones (see pyffi issue #3114079) - if not n_bone: - continue - - vertex_weights = bone_weights[idx].vertex_weights - group_name = block_store.import_name(n_bone) - if group_name not in b_obj.vertex_groups: - v_group = b_obj.vertex_groups.new(name=group_name) - - for skinWeight in vertex_weights: - vert = skinWeight.index - weight = skinWeight.weight - v_group.add([vert], weight, 'REPLACE') - - # WLP2 - hides the weights in the partition - else: - skin_partition = skininst.skin_partition - for block in skin_partition.skin_partition_blocks: - # create all vgroups for this block's bones - block_bone_names = [block_store.import_name(bones[i]) for i in block.bones] - for group_name in block_bone_names: - b_obj.vertex_groups.new(name=group_name) - - # go over each vert in this block - for vert, vertex_weights, bone_indices in zip(block.vertex_map, block.vertex_weights, block.bone_indices): - - # assign this vert's 4 weights to its 4 vgroups (at max) - for w, b_i in zip(vertex_weights, bone_indices): - if w > 0: - group_name = block_bone_names[b_i] - v_group = b_obj.vertex_groups[group_name] - v_group.add([vert], w, 'REPLACE') - - # import body parts as face maps - # get faces (triangles) as map of unordered vertices to list of indices - tri_map = {} - for polygon in b_obj.data.polygons: - vertices = frozenset(polygon.vertices) - if vertices in tri_map: - tri_map[vertices].append(polygon.index) + if isinstance(ni_block, NifClasses.NiMesh): + if ni_block.has_extra_em_data: + # only for Epic Mickey nifs for now + # get all the weights and the corresponding bone (indices) + bone_indices = np.zeros((len(b_obj.data.vertices), 3), dtype=int) + bone_weights = np.zeros((len(b_obj.data.vertices), 3), dtype=float) + bone_weights_set = ni_block.extra_em_data.weights + for i, set_index in enumerate(ni_block.extra_em_data.vertex_to_weight_map): + weight = bone_weights_set[set_index] + bone_indices[i] = weight.bone_indices + bone_weights[i] = weight.weights + bone_names = [get_bone_name_for_blender(str(i)) for i in range(len(ni_block.extra_em_data.bone_transforms))] else: - tri_map[vertices] = [polygon.index] - if isinstance(skininst, NifFormat.BSDismemberSkinInstance): - skinpart = ni_block.get_skin_partition() - for bodypart, skinpartblock in zip(skininst.partitions, skinpart.skin_partition_blocks): - bodypart_wrap = NifFormat.BSDismemberBodyPartType() - bodypart_wrap.set_value(bodypart.body_part) - group_name = bodypart_wrap.get_detail_display() - - # create face map if it did not exist yet - if group_name not in b_obj.face_maps: - f_group = b_obj.face_maps.new(name=group_name) - - # add the triangles to the face map - for vertices in skinpartblock.get_mapped_triangles(): - f_group.add(tri_map[frozenset(vertices)]) + bone_indices = [] + bone_weights = chain.from_iterable(ni_block.geomdata_by_name('BLENDWEIGHT')) + + # assume there's only on SkinningMeshModifier + skin_modifier = [block for block in ni_block.modifiers if isinstance(block, NifClasses.NiSkinningMeshModifier)][0] + bone_names = [block_store.import_name(bone) for bone in skin_modifier.bones] + + bone_palettes = ni_block.geomdata_by_name('BONE_PALETTE', sep_datastreams=False, sep_regions=True) + bone_index_datas = ni_block.geomdata_by_name('BLENDINDICES', sep_datastreams=False, sep_regions=True) + + for palette, index_datas in zip(bone_palettes, bone_index_datas): + bone_indices.extend([[palette[i] for i in indices] for indices in index_datas]) + + # create all vgroups for this block's bones + for group_name in bone_names: + b_obj.vertex_groups.new(name=group_name) + + # add every vertex to the corresponding groups + for i, (weights, indices) in enumerate(zip(bone_weights, bone_indices)): + for w, b_i in zip(weights, indices): + # weights and indices is not necessarily equally long - luckily zip limits to the shortest + if b_i >= 0 and w > 0: + group_name = bone_names[b_i] + v_group = b_obj.vertex_groups[group_name] + v_group.add([int(i)], w, 'REPLACE') + + else: + skininst = ni_block.skin_instance + if skininst: + skindata = skininst.data + bones = skininst.bones + # the usual case + if skindata.has_vertex_weights: + bone_weights = skindata.bone_list + for idx, n_bone in enumerate(bones): + # skip empty bones (see pyffi issue #3114079) + if not n_bone: + continue + + vertex_weights = bone_weights[idx].vertex_weights + group_name = block_store.import_name(n_bone) + if group_name not in b_obj.vertex_groups: + v_group = b_obj.vertex_groups.new(name=group_name) + + for skinWeight in vertex_weights: + vert = skinWeight.index + weight = skinWeight.weight + v_group.add([vert], weight, 'REPLACE') + + # WLP2 - hides the weights in the partition + else: + skin_partition = skininst.skin_partition + for block in skin_partition.partitions: + # create all vgroups for this block's bones + block_bone_names = [block_store.import_name(bones[i]) for i in block.bones] + for group_name in block_bone_names: + b_obj.vertex_groups.new(name=group_name) + + # go over each vert in this block + for vert, vertex_weights, bone_indices in zip(block.vertex_map, block.vertex_weights, block.bone_indices): + + # assign this vert's 4 weights to its 4 vgroups (at max) + for w, b_i in zip(vertex_weights, bone_indices): + if w > 0: + group_name = block_bone_names[b_i] + v_group = b_obj.vertex_groups[group_name] + # conversion from numpy.uint16 to int necessary because Blender doesn't accept them + v_group.add([int(vert)], w, 'REPLACE') + + if isinstance(skininst, NifClasses.BSDismemberSkinInstance): + for bodypart in skininst.partitions: + group_name = bodypart.body_part.name + + # create face map if it did not exist yet + if group_name not in b_obj.face_maps: + f_group = b_obj.face_maps.new(name=group_name) + else: + f_group = b_obj.face_maps[group_name] + triangles, bodyparts = skininst.get_dismember_partitions() + for i, bodypart in enumerate(bodyparts): + f_group = b_obj.face_maps[bodypart.name] + f_group.add([i]) diff --git a/io_scene_niftools/modules/nif_import/object/__init__.py b/io_scene_niftools/modules/nif_import/object/__init__.py index d929bfa4a..c242ceb7e 100644 --- a/io_scene_niftools/modules/nif_import/object/__init__.py +++ b/io_scene_niftools/modules/nif_import/object/__init__.py @@ -38,7 +38,7 @@ # ***** END LICENSE BLOCK ***** import bpy -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.modules.nif_import.geometry.mesh import Mesh from io_scene_niftools.modules.nif_import.object.block_registry import block_store @@ -110,7 +110,7 @@ def set_object_bind(self, b_obj, b_obj_children, b_armature): raise RuntimeError(f"Unexpected object type {b_obj.__class__:s}") def create_mesh_object(self, n_block): - ni_name = n_block.name.decode() + ni_name = n_block.name # create mesh data b_mesh = bpy.data.meshes.new(ni_name) @@ -125,6 +125,9 @@ def create_mesh_object(self, n_block): return b_obj + def has_geometry(self, n_block): + return isinstance(n_block, self.mesh.supported_mesh_types) + def import_geometry_object(self, b_armature, n_block): # it's a shape node and we're not importing skeleton only b_obj = self.create_mesh_object(n_block) @@ -134,7 +137,7 @@ def import_geometry_object(self, b_armature, n_block): # store flags etc self.import_object_flags(n_block, b_obj) # skinning? add armature modifier - if n_block.skin_instance: + if n_block.is_skin(): self.append_armature_modifier(b_obj, b_armature) return b_obj @@ -144,10 +147,9 @@ def import_object_flags(n_block, b_obj): """ Various settings in b_obj's niftools panel """ b_obj.niftools.flags = n_block.flags - if n_block.data.consistency_flags in NifFormat.ConsistencyType._enumvalues: - cf_index = NifFormat.ConsistencyType._enumvalues.index(n_block.data.consistency_flags) - b_obj.niftools.consistency_flags = NifFormat.ConsistencyType._enumkeys[cf_index] - if n_block.is_skin(): + if hasattr(n_block, "data") and isinstance(n_block.data.consistency_flags, NifClasses.ConsistencyType): + b_obj.niftools.consistency_flags = n_block.data.consistency_flags.name + if n_block.is_skin() and hasattr(n_block, "skin_instance"): skininst = n_block.skin_instance skelroot = skininst.skeleton_root b_obj.niftools.skeleton_root = block_store.import_name(skelroot) diff --git a/io_scene_niftools/modules/nif_import/object/block_registry.py b/io_scene_niftools/modules/nif_import/object/block_registry.py index 0862f89bc..8f8188118 100644 --- a/io_scene_niftools/modules/nif_import/object/block_registry.py +++ b/io_scene_niftools/modules/nif_import/object/block_registry.py @@ -39,8 +39,6 @@ from io_scene_niftools.utils.logging import NifLog -from io_scene_niftools.utils.blocks import safe_decode - from io_scene_niftools.utils.consts import BIP_01, BIP01_L, B_L_SUFFIX, BIP01_R, B_R_SUFFIX, NPC_L, NPC_R, NPC_SUFFIX, \ BRACE_R, B_R_POSTFIX, B_L_POSTFIX, CLOSE_BRACKET, BRACE_L, OPEN_BRACKET @@ -53,8 +51,6 @@ def get_bone_name_for_blender(name): :return: Bone name in Blender convention. :rtype: :class:`str` """ - if isinstance(name, bytes): - name = safe_decode(name) if name.startswith(BIP01_L): name = BIP_01 + name[8:] + B_L_SUFFIX elif name.startswith(BIP01_R): @@ -86,14 +82,14 @@ def import_name(n_block): """Get name of n_block, ready for blender but not necessarily unique. :param n_block: A named nif block. - :type n_block: :class:`~pyffi.formats.nif.NifFormat.NiObjectNET` + :type n_block: :class:`~generated.formats.nif.nimain.niobjects.NiObjectNET` """ if n_block is None: return "" NifLog.debug(f"Importing name for {n_block.__class__.__name__} block from {n_block.name}") - n_name = safe_decode(n_block.name) + n_name = n_block.name # if name is empty, create something non-empty if not n_name: diff --git a/io_scene_niftools/modules/nif_import/object/types.py b/io_scene_niftools/modules/nif_import/object/types.py index 43ebd53e9..c7e533983 100644 --- a/io_scene_niftools/modules/nif_import/object/types.py +++ b/io_scene_niftools/modules/nif_import/object/types.py @@ -37,8 +37,8 @@ # # ***** END LICENSE BLOCK ***** -from pyffi.formats.nif import NifFormat import bpy +from generated.formats.nif import classes as NifClasses from io_scene_niftools.modules.nif_import.object import Object @@ -47,21 +47,19 @@ class NiTypes: @staticmethod def import_root_collision(n_node, b_obj): - """ Import a RootCollisionNode """ - if isinstance(n_node, NifFormat.RootCollisionNode): + """ Import a RootCollisionNode, which is usually attached to a root node and holds a NiTriShape""" + if isinstance(n_node, NifClasses.RootCollisionNode): b_obj["type"] = "RootCollisionNode" b_obj.name = "RootCollisionNode" - b_obj.display_type = 'BOUNDS' - b_obj.show_wire = True - b_obj.display_bounds_type = 'BOX' - # b_obj.game.use_collision_bounds = True - # b_obj.game.collision_bounds_type = 'TRIANGLE_MESH' b_obj.niftools.flags = n_node.flags + for b_child in b_obj.children: + b_child.display_type = 'WIRE' + @staticmethod def import_range_lod_data(n_node, b_obj, b_children): """ Import LOD ranges and mark b_obj as a LOD node """ - if isinstance(n_node, NifFormat.NiLODNode): + if isinstance(n_node, NifClasses.NiLODNode): b_obj["type"] = "NiLODNode" range_data = n_node @@ -77,7 +75,7 @@ def import_range_lod_data(n_node, b_obj, b_children): @staticmethod def import_billboard(n_node, b_obj): """ Import a NiBillboardNode """ - if isinstance(n_node, NifFormat.NiBillboardNode) and not isinstance(b_obj, bpy.types.Bone): + if isinstance(n_node, NifClasses.NiBillboardNode) and not isinstance(b_obj, bpy.types.Bone): # find camera object for obj in bpy.context.scene.objects: if obj.type == 'CAMERA': @@ -97,6 +95,4 @@ def import_billboard(n_node, b_obj): def import_empty(n_block): """Creates and returns a grouping empty.""" b_empty = Object.create_b_obj(n_block, None) - # TODO [flags] Move out to generic processing - b_empty.niftools.flags = n_block.flags return b_empty diff --git a/io_scene_niftools/modules/nif_import/property/geometry/mesh.py b/io_scene_niftools/modules/nif_import/property/geometry/mesh.py index 2f5bf9d02..7ee22c971 100644 --- a/io_scene_niftools/modules/nif_import/property/geometry/mesh.py +++ b/io_scene_niftools/modules/nif_import/property/geometry/mesh.py @@ -69,7 +69,8 @@ def process_property_list(self, n_block, b_obj): b_mesh = b_obj.data # get all valid properties that are attached to n_block - props = list(prop for prop in itertools.chain(n_block.properties, n_block.bs_properties) if prop is not None) + bs_properties = [getattr(n_block, prop_name, None) for prop_name in ("shader_property", "alpha_property")] + props = list(prop for prop in itertools.chain(n_block.properties, bs_properties) if prop is not None) # we need no material if we have no properties if not props: @@ -78,7 +79,7 @@ def process_property_list(self, n_block, b_obj): # just to avoid duped materials, a first pass, make sure a named material is created or retrieved for prop in props: if prop.name: - name = prop.name.decode() + name = prop.name if name and name in bpy.data.materials: b_mat = bpy.data.materials[name] NifLog.debug(f"Retrieved already imported material {b_mat.name} from name {name}") @@ -88,7 +89,7 @@ def process_property_list(self, n_block, b_obj): break else: # bs shaders often have no name, so generate one from mesh name - name = n_block.name.decode() + "_nt_mat" + name = f"{n_block.name}_nt_mat" b_mat = bpy.data.materials.new(name) NifLog.debug(f"Created material {name} to store properties in {b_mat.name}") diff --git a/io_scene_niftools/modules/nif_import/property/geometry/niproperty.py b/io_scene_niftools/modules/nif_import/property/geometry/niproperty.py index dc464ab54..217a8f2f7 100644 --- a/io_scene_niftools/modules/nif_import/property/geometry/niproperty.py +++ b/io_scene_niftools/modules/nif_import/property/geometry/niproperty.py @@ -37,7 +37,7 @@ # # ***** END LICENSE BLOCK ***** -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.modules.nif_import.animation.material import MaterialAnimation from io_scene_niftools.modules.nif_import.property.material import Material, NiMaterial @@ -72,13 +72,13 @@ def __init__(self): # NiPropertyProcessor.__instance = self def register(self, processor): - processor.register(NifFormat.NiMaterialProperty, self.process_nimaterial_property) - processor.register(NifFormat.NiAlphaProperty, self.process_nialpha_property) - processor.register(NifFormat.NiTexturingProperty, self.process_nitexturing_property) - processor.register(NifFormat.NiStencilProperty, self.process_nistencil_property) - processor.register(NifFormat.NiSpecularProperty, self.process_nispecular_property) - processor.register(NifFormat.NiWireframeProperty, self.process_niwireframe_property) - processor.register(NifFormat.NiVertexColorProperty, self.process_nivertexcolor_property) + processor.register(NifClasses.NiMaterialProperty, self.process_nimaterial_property) + processor.register(NifClasses.NiAlphaProperty, self.process_nialpha_property) + processor.register(NifClasses.NiTexturingProperty, self.process_nitexturing_property) + processor.register(NifClasses.NiStencilProperty, self.process_nistencil_property) + processor.register(NifClasses.NiSpecularProperty, self.process_nispecular_property) + processor.register(NifClasses.NiWireframeProperty, self.process_niwireframe_property) + processor.register(NifClasses.NiVertexColorProperty, self.process_nivertexcolor_property) @property def b_mesh(self): diff --git a/io_scene_niftools/modules/nif_import/property/material/__init__.py b/io_scene_niftools/modules/nif_import/property/material/__init__.py index 7716dd1ab..461bcab51 100644 --- a/io_scene_niftools/modules/nif_import/property/material/__init__.py +++ b/io_scene_niftools/modules/nif_import/property/material/__init__.py @@ -118,7 +118,7 @@ def import_material(self, n_block, b_mat, n_mat_prop): # update material material name name = block_store.import_name(n_mat_prop) if name is None: - name = (n_block.name.decode() + "_nt_mat") + name = (f"{n_block.name}_nt_mat") b_mat.name = name self.import_material_ambient(b_mat, n_mat_prop.ambient_color) diff --git a/io_scene_niftools/modules/nif_import/property/nodes_wrapper/__init__.py b/io_scene_niftools/modules/nif_import/property/nodes_wrapper/__init__.py index 1e10ac2ec..d262a4d72 100644 --- a/io_scene_niftools/modules/nif_import/property/nodes_wrapper/__init__.py +++ b/io_scene_niftools/modules/nif_import/property/nodes_wrapper/__init__.py @@ -38,7 +38,7 @@ # ***** END LICENSE BLOCK ***** import bpy -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.modules.nif_import.geometry.vertex import Vertex from io_scene_niftools.modules.nif_import.property.texture.loader import TextureLoader @@ -69,6 +69,10 @@ def __init__(self): self.diffuse_texture = None self.vcol = None + @staticmethod + def uv_node_name(uv_index): + return f"TexCoordIndex_{uv_index}" + def set_uv_map(self, b_texture_node, uv_index=0, reflective=False): """Attaches a vector node describing the desired coordinate transforms to the texture node's UV input.""" if reflective: @@ -76,7 +80,7 @@ def set_uv_map(self, b_texture_node, uv_index=0, reflective=False): self.tree.links.new(uv.outputs[6], b_texture_node.inputs[0]) # use supplied UV maps for everything else, if present else: - uv_name = "TexCoordIndex" + str(uv_index) + uv_name = self.uv_node_name(uv_index) existing_node = self.tree.nodes.get(uv_name) if not existing_node: uv = self.tree.nodes.new('ShaderNodeUVMap') @@ -85,53 +89,34 @@ def set_uv_map(self, b_texture_node, uv_index=0, reflective=False): else: uv = existing_node self.tree.links.new(uv.outputs[0], b_texture_node.inputs[0]) - # todo [texture/anim] if present in nifs, support it and move to anim sys - # if tex_transform or tex_anim: - # transform = tree.nodes.new('ShaderNodeMapping') - # # todo [texture] negate V coordinate - # if tex_transform: - # matrix_4x4 = mathutils.Matrix(tex_transform) - # transform.scale = matrix_4x4.to_scale() - # transform.rotation = matrix_4x4.to_euler() - # transform.translation = matrix_4x4.to_translation() - # transform.name = "TextureTransform" + str(i) - # if tex_anim: - # for j, dtype in enumerate(("offsetu", "offsetv")): - # for key in tex_anim[dtype]: - # transform.translation[j] = key[1] - # # note that since we are dealing with UV coordinates, V has to be negated - # if j == 1: transform.translation[j] *= -1 - # transform.keyframe_insert("translation", index=j, frame=int(key[0] * fps)) - # tree.links.new(uv.outputs[0], transform.inputs[0]) - # tree.links.new(transform.outputs[0], tex.inputs[0]) def global_uv_offset_scale(self, x_scale, y_scale, x_offset, y_offset, clamp_x, clamp_y): # get all uv nodes (by name, since we are importing they have the predefined name # and then we don't have to loop through every node uv_nodes = {} - i = 0 + uv_index = 0 while True: - uv_name = "TexCoordIndex" + str(i) + uv_name = self.uv_node_name(uv_index) uv_node = self.tree.nodes.get(uv_name) if uv_node and isinstance(uv_node, bpy.types.ShaderNodeUVMap): - uv_nodes[uv_name] = uv_node - i += 1 + uv_nodes[uv_index] = uv_node + uv_index += 1 else: break clip_texture = clamp_x and clamp_y - for uv_name, uv_node in uv_nodes.items(): + for uv_index, uv_node in uv_nodes.items(): # for each of those, create a new uv output node and relink split_node = self.tree.nodes.new("ShaderNodeSeparateXYZ") - split_node.name = "Separate UV" + uv_name[-1] + split_node.name = f"Separate UV{uv_index}" split_node.label = split_node.name combine_node = self.tree.nodes.new("ShaderNodeCombineXYZ") - combine_node.name = "Combine UV" + uv_name[-1] + combine_node.name = f"Combine UV{uv_index}" combine_node.label = combine_node.name x_node = self.tree.nodes.new("ShaderNodeMath") - x_node.name = "X offset and scale UV" + uv_name[-1] + x_node.name = f"X offset and scale UV{uv_index}" x_node.label = x_node.name x_node.operation = 'MULTIPLY_ADD' # only clamp on the math node when we're not clamping on both directions @@ -143,7 +128,7 @@ def global_uv_offset_scale(self, x_scale, y_scale, x_offset, y_offset, clamp_x, self.tree.links.new(x_node.outputs[0], combine_node.inputs[0]) y_node = self.tree.nodes.new("ShaderNodeMath") - y_node.name = "Y offset and scale UV" + uv_name[-1] + y_node.name = f"Y offset and scale UV{uv_index}" y_node.label = y_node.name y_node.operation = 'MULTIPLY_ADD' y_node.use_clamp = clamp_y and not clip_texture @@ -261,7 +246,7 @@ def create_and_link(self, slot_name, n_tex_info): def create_texture_slot(self, n_tex_desc): # todo [texture] refactor this to separate code paths? # when processing a NiTextureProperty - if isinstance(n_tex_desc, NifFormat.TexDesc): + if isinstance(n_tex_desc, NifClasses.TexDesc): b_image = self.texture_loader.import_texture_source(n_tex_desc.source) uv_layer_index = n_tex_desc.uv_set # when processing a BS shader property - n_tex_desc is a bare string @@ -315,9 +300,9 @@ def link_normal_node(self, b_texture_node): group_nodes = node_group.nodes # add the in/output nodes input_node = group_nodes.new('NodeGroupInput') - node_group.inputs.new('NodeSocketImage', "Input") + node_group.inputs.new('NodeSocketColor', "Input") output_node = group_nodes.new('NodeGroupOutput') - node_group.outputs.new('NodeSocketImage', "Output") + node_group.outputs.new('NodeSocketColor', "Output") # create the converting nodes separate_node = group_nodes.new("ShaderNodeSeparateRGB") invert_node = group_nodes.new("ShaderNodeInvert") @@ -336,7 +321,7 @@ def link_normal_node(self, b_texture_node): group_node = nodes.new('ShaderNodeGroup') group_node.node_tree = node_group links.new(group_node.inputs[0], b_texture_node.outputs[0]) - if self.b_mat.niftools_shader.slsf_1_model_space_normals: + if self.b_mat.niftools_shader.model_space_normals: self.tree.links.new(self.diffuse_shader.inputs[2], group_node.outputs[0]) else: # create tangent normal map converter and link to it @@ -427,15 +412,15 @@ def link_environment_node(self, b_texture_node): @staticmethod def get_b_blend_type_from_n_apply_mode(n_apply_mode): # TODO [material] Check out n_apply_modes - if n_apply_mode == NifFormat.ApplyMode.APPLY_MODULATE: + if n_apply_mode == NifClasses.ApplyMode.APPLY_MODULATE: return "MIX" - elif n_apply_mode == NifFormat.ApplyMode.APPLY_REPLACE: + elif n_apply_mode == NifClasses.ApplyMode.APPLY_REPLACE: return "COLOR" - elif n_apply_mode == NifFormat.ApplyMode.APPLY_DECAL: + elif n_apply_mode == NifClasses.ApplyMode.APPLY_DECAL: return "OVERLAY" - elif n_apply_mode == NifFormat.ApplyMode.APPLY_HILIGHT: + elif n_apply_mode == NifClasses.ApplyMode.APPLY_HILIGHT: return "LIGHTEN" - elif n_apply_mode == NifFormat.ApplyMode.APPLY_HILIGHT2: # used by Oblivion for parallax + elif n_apply_mode == NifClasses.ApplyMode.APPLY_HILIGHT2: # used by Oblivion for parallax return "MULTIPLY" else: NifLog.warn(f"Unknown apply mode ({n_apply_mode}) in material, using blend type 'MIX'") diff --git a/io_scene_niftools/modules/nif_import/property/object/__init__.py b/io_scene_niftools/modules/nif_import/property/object/__init__.py index 21b486f1c..6640fada9 100644 --- a/io_scene_niftools/modules/nif_import/property/object/__init__.py +++ b/io_scene_niftools/modules/nif_import/property/object/__init__.py @@ -37,9 +37,8 @@ # # ***** END LICENSE BLOCK ***** import bpy -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses -from io_scene_niftools.properties.object import PRN_DICT from math import pi @@ -48,44 +47,26 @@ class ObjectProperty: # TODO [property] Add delegate processing def import_extra_datas(self, root_block, b_obj): """ Only to be called on nif and blender root objects! """ + niftools_scene = bpy.context.scene.niftools_scene # store type of root node - if isinstance(root_block, NifFormat.BSFadeNode): - b_obj.niftools.rootnode = 'BSFadeNode' - else: - b_obj.niftools.rootnode = 'NiNode' + if isinstance(root_block, NifClasses.BSFadeNode): + b_obj.niftools.nodetype = 'BSFadeNode' # store its flags b_obj.niftools.flags = root_block.flags # store extra datas for n_extra in root_block.get_extra_datas(): - if isinstance(n_extra, NifFormat.NiStringExtraData): + if isinstance(n_extra, NifClasses.NiStringExtraData): # weapon location or attachment position - if n_extra.name.decode() == "Prn": - game = bpy.context.scene.niftools_scene.game - if game in PRN_DICT[next(iter(PRN_DICT))]: - # first check specifically in that game - for slot, game_map in PRN_DICT.items(): - if game_map[game].lower() == n_extra.string_data.decode().lower(): - b_obj.niftools.prn_location = slot - break - if b_obj.niftools.prn_location == "NONE": - # we didn't find anything, either because the game doesn't have it, - # or we have the wrong game. Check all key, value pairs - for slot, game_map in PRN_DICT.items(): - for k, v in game_map: - if v.lower() == n_extra.string_data.decode().lower(): - b_obj.niftools.prn_location = slot - break - else: - continue - break - elif n_extra.name.decode() == "UPB": - b_obj.niftools.upb = n_extra.string_data.decode() - elif isinstance(n_extra, NifFormat.BSXFlags): + if n_extra.name == "Prn": + b_obj.niftools.prn_location = n_extra.string_data + elif n_extra.name == "UPB": + b_obj.niftools.upb = n_extra.string_data + elif isinstance(n_extra, NifClasses.BSXFlags): b_obj.niftools.bsxflags = n_extra.integer_data - elif isinstance(n_extra, NifFormat.BSInvMarker): - b_obj.niftools_bs_invmarker.add() - b_obj.niftools_bs_invmarker[0].name = n_extra.name.decode() - b_obj.niftools_bs_invmarker[0].bs_inv_x = (-n_extra.rotation_x / 1000) % (2 * pi) - b_obj.niftools_bs_invmarker[0].bs_inv_y = (-n_extra.rotation_y / 1000) % (2 * pi) - b_obj.niftools_bs_invmarker[0].bs_inv_z = (-n_extra.rotation_z / 1000) % (2 * pi) - b_obj.niftools_bs_invmarker[0].bs_inv_zoom = n_extra.zoom + elif isinstance(n_extra, NifClasses.BSInvMarker): + bs_inv_item = b_obj.niftools.bs_inv.add() + bs_inv_item.name = n_extra.name + bs_inv_item.x = (-n_extra.rotation_x / 1000) % (2 * pi) + bs_inv_item.y = (-n_extra.rotation_y / 1000) % (2 * pi) + bs_inv_item.z = (-n_extra.rotation_z / 1000) % (2 * pi) + bs_inv_item.zoom = n_extra.zoom diff --git a/io_scene_niftools/modules/nif_import/property/shader/__init__.py b/io_scene_niftools/modules/nif_import/property/shader/__init__.py index ce64e39fb..dd7fa294c 100644 --- a/io_scene_niftools/modules/nif_import/property/shader/__init__.py +++ b/io_scene_niftools/modules/nif_import/property/shader/__init__.py @@ -131,14 +131,13 @@ def set_alpha_bsshader(b_mat, shader_property): def create_material_name(self, bs_shader_property): name = block_store.import_name(bs_shader_property) if name is None: - name = (self._n_block.name.decode() + "_nt_mat") + name = (f"{self._n_block.name}_nt_mat") b_mat = bpy.data.materials.new(name) self._b_mesh.materials.append(b_mat) return b_mat @staticmethod def import_flags(b_mat, flags): - for name in flags._names: - sf_index = flags._names.index(name) - if flags._items[sf_index]._value == 1: + for name in type(flags).__members__: + if getattr(flags, name): b_mat.niftools_shader[name] = True diff --git a/io_scene_niftools/modules/nif_import/property/shader/bsshaderlightingproperty.py b/io_scene_niftools/modules/nif_import/property/shader/bsshaderlightingproperty.py index c99c9d3c0..d34c832f6 100644 --- a/io_scene_niftools/modules/nif_import/property/shader/bsshaderlightingproperty.py +++ b/io_scene_niftools/modules/nif_import/property/shader/bsshaderlightingproperty.py @@ -37,7 +37,7 @@ # # ***** END LICENSE BLOCK ***** -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.modules.nif_import.property.shader import BSShader from io_scene_niftools.modules.nif_import.property.texture.types.bsshadertexture import BSShaderTexture @@ -72,15 +72,14 @@ def get(): return BSShaderLightingPropertyProcessor.__instance def register(self, processor): - processor.register(NifFormat.BSShaderPPLightingProperty, self.import_bs_shader_pp_lighting_property) + processor.register(NifClasses.BSShaderPPLightingProperty, self.import_bs_shader_pp_lighting_property) def import_bs_shader_pp_lighting_property(self, bs_shader_prop): # Shader Flags b_shader = self._b_mat.niftools_shader b_shader.bs_shadertype = 'BSShaderPPLightingProperty' - shader_type = NifFormat.BSShaderType._enumvalues.index(bs_shader_prop.shader_type) - b_shader.bsspplp_shaderobjtype = NifFormat.BSShaderType._enumkeys[shader_type] + b_shader.bsspplp_shaderobjtype = bs_shader_prop.shader_type.name flags = bs_shader_prop.shader_flags self.import_flags(self._b_mat, flags) diff --git a/io_scene_niftools/modules/nif_import/property/shader/bsshaderproperty.py b/io_scene_niftools/modules/nif_import/property/shader/bsshaderproperty.py index d91374283..b057c3589 100644 --- a/io_scene_niftools/modules/nif_import/property/shader/bsshaderproperty.py +++ b/io_scene_niftools/modules/nif_import/property/shader/bsshaderproperty.py @@ -38,7 +38,7 @@ # ***** END LICENSE BLOCK ***** -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.modules.nif_import.property.material import Material from io_scene_niftools.modules.nif_import.property.shader import BSShader @@ -80,8 +80,8 @@ def get(): return BSShaderPropertyProcessor.__instance def register(self, processor): - processor.register(NifFormat.BSLightingShaderProperty, self.import_bs_lighting_shader_property) - processor.register(NifFormat.BSEffectShaderProperty, self.import_bs_effect_shader_property) + processor.register(NifClasses.BSLightingShaderProperty, self.import_bs_lighting_shader_property) + processor.register(NifClasses.BSEffectShaderProperty, self.import_bs_effect_shader_property) def import_bs_lighting_shader_property(self, bs_shader_property): @@ -89,8 +89,8 @@ def import_bs_lighting_shader_property(self, bs_shader_property): b_shader = self._b_mat.niftools_shader b_shader.bs_shadertype = 'BSLightingShaderProperty' - shader_type = NifFormat.BSLightingShaderPropertyShaderType._enumvalues.index(bs_shader_property.skyrim_shader_type) - b_shader.bslsp_shaderobjtype = NifFormat.BSLightingShaderPropertyShaderType._enumkeys[shader_type] + shader_type = bs_shader_property.shader_type + b_shader.bslsp_shaderobjtype = shader_type.name self.import_shader_flags(bs_shader_property) @@ -102,9 +102,9 @@ def import_bs_lighting_shader_property(self, bs_shader_property): self._nodes_wrapper.global_uv_offset_scale(x_scale, y_scale, x_offset, y_offset, clamp_x, clamp_y) # Diffuse color - if shader_type == NifFormat.BSLightingShaderPropertyShaderType["Skin Tint"]: + if shader_type == NifClasses.BSLightingShaderType.SKIN_TINT: Material.import_material_diffuse(self._b_mat, bs_shader_property.skin_tint_color) - elif shader_type == NifFormat.BSLightingShaderPropertyShaderType["Hair Tint"]: + elif shader_type == NifClasses.BSLightingShaderType.HAIR_TINT: Material.import_material_diffuse(self._b_mat, bs_shader_property.hair_tint_color) # TODO [material][b_shader][property] Handle nialphaproperty node lookup @@ -133,7 +133,7 @@ def import_bs_effect_shader_property(self, bs_effect_shader_property): shader = self._b_mat.niftools_shader shader.bs_shadertype = 'BSEffectShaderProperty' - shader.bslsp_shaderobjtype = 'Default' + shader.bslsp_shaderobjtype = 'DEFAULT' self.import_shader_flags(bs_effect_shader_property) self.texturehelper.import_bseffectshaderproperty_textures(bs_effect_shader_property, self._nodes_wrapper) @@ -148,12 +148,12 @@ def import_bs_effect_shader_property(self, bs_effect_shader_property): # self.b_mat = self.set_alpha_bsshader(self.b_mat, bs_effect_shader_property) # Emissive - if bs_effect_shader_property.emissive_color: - Material.import_material_emissive(self._b_mat, bs_effect_shader_property.emissive_color) + if bs_effect_shader_property.base_color: + Material.import_material_emissive(self._b_mat, bs_effect_shader_property.base_color) # TODO [property][shader][alpha] Map this to actual alpha when component is available - Material.import_material_alpha(self._b_mat, bs_effect_shader_property.emissive_color.a) + Material.import_material_alpha(self._b_mat, bs_effect_shader_property.base_color.a) # todo [shader] create custom float property, or use as factor in mix shader? - # self.b_mat.emit = bs_effect_shader_property.emissive_multiple + # self.b_mat.emit = bs_effect_shader_property.base_color_scale # TODO [animation][shader] Move out to a dedicated controller processor if bs_effect_shader_property.controller: @@ -188,19 +188,17 @@ def get_uv_transform(self, shader): # get the clamp (x and y direction) if hasattr(shader, 'texture_clamp_mode'): - # use modulo 256, because in BSEffectShaderProperty, pyffi also takes other bytes, making the value appear - # higher than it is - clamp_mode = shader.texture_clamp_mode % 256 - if clamp_mode == NifFormat.TexClampMode.WRAP_S_WRAP_T: + clamp_mode = shader.texture_clamp_mode + if clamp_mode == NifClasses.TexClampMode.WRAP_S_WRAP_T: clamp_x = False clamp_y = False - elif clamp_mode == NifFormat.TexClampMode.WRAP_S_CLAMP_T: + elif clamp_mode == NifClasses.TexClampMode.WRAP_S_CLAMP_T: clamp_x = False clamp_y = True - elif clamp_mode == NifFormat.TexClampMode.CLAMP_S_WRAP_T: + elif clamp_mode == NifClasses.TexClampMode.CLAMP_S_WRAP_T: clamp_x = True clamp_y = False - elif clamp_mode == NifFormat.TexClampMode.CLAMP_S_CLAMP_T: + elif clamp_mode == NifClasses.TexClampMode.CLAMP_S_CLAMP_T: clamp_x = True clamp_y = True else: diff --git a/io_scene_niftools/modules/nif_import/property/texture/loader.py b/io_scene_niftools/modules/nif_import/property/texture/loader.py index f395e8708..6b85189c5 100644 --- a/io_scene_niftools/modules/nif_import/property/texture/loader.py +++ b/io_scene_niftools/modules/nif_import/property/texture/loader.py @@ -36,144 +36,22 @@ # POSSIBILITY OF SUCH DAMAGE. # # ***** END LICENSE BLOCK ***** - from functools import reduce import operator +import traceback import os.path import bpy -from pyffi.formats.dds import DdsFormat -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses -from io_scene_niftools.modules.nif_import.property import texture from io_scene_niftools.utils.singleton import NifOp from io_scene_niftools.utils.logging import NifLog -# TODO once the save_dds PR code is merged into pyffi this section can be deleted. -# This section replaces pyffi methods to impliment functionality immediately. -################# -def get_pixeldata_stream_overide(self): - if isinstance(self, NifFormat.NiPersistentSrcTextureRendererData): - return ''.join( - ''.join([chr(x) for x in tex]) - for tex in self.pixel_data) - elif isinstance(self, NifFormat.NiPixelData): - if self.pixel_data: - # used in older nif versions - return bytearray().join( - bytearray().join([bytearray([x]) for x in tex]) - for tex in self.pixel_data) - else: - # used in newer nif versions - return ''.join(self.pixel_data_matrix) - else: - raise ValueError( - "cannot retrieve pixel data when saving pixel format %i as DDS") -def save_as_dds_override(self, stream): - data = DdsFormat.Data() - header = data.header - pixeldata = data.pixeldata - - # create header, depending on the format - if self.pixel_format in (NifFormat.PixelFormat.PX_FMT_RGB8, - NifFormat.PixelFormat.PX_FMT_RGBA8): - # uncompressed RGB(A) - header.flags.caps = 1 - header.flags.height = 1 - header.flags.width = 1 - header.flags.pixel_format = 1 - header.flags.mipmap_count = 1 - header.flags.linear_size = 1 - header.height = self.mipmaps[0].height - header.width = self.mipmaps[0].width - header.linear_size = len(self.pixel_data) - header.mipmap_count = len(self.mipmaps) - header.pixel_format.flags.rgb = 1 - header.pixel_format.bit_count = self.bits_per_pixel - if not self.channels: - header.pixel_format.r_mask = self.red_mask - header.pixel_format.g_mask = self.green_mask - header.pixel_format.b_mask = self.blue_mask - header.pixel_format.a_mask = self.alpha_mask - else: - bit_pos = 0 - for i, channel in enumerate(self.channels): - mask = (2 ** channel.bits_per_channel - 1) << bit_pos - if channel.type == NifFormat.ChannelType.CHNL_RED: - header.pixel_format.r_mask = mask - elif channel.type == NifFormat.ChannelType.CHNL_GREEN: - header.pixel_format.g_mask = mask - elif channel.type == NifFormat.ChannelType.CHNL_BLUE: - header.pixel_format.b_mask = mask - elif channel.type == NifFormat.ChannelType.CHNL_ALPHA: - header.pixel_format.a_mask = mask - bit_pos += channel.bits_per_channel - header.caps_1.complex = 1 - header.caps_1.texture = 1 - header.caps_1.mipmap = 1 - pixeldata.set_value(self.__get_pixeldata_stream()) - elif self.pixel_format == NifFormat.PixelFormat.PX_FMT_DXT1: - # format used in Megami Tensei: Imagine and Bully SE - header.flags.caps = 1 - header.flags.height = 1 - header.flags.width = 1 - header.flags.pixel_format = 1 - header.flags.mipmap_count = 1 - header.flags.linear_size = 0 - header.height = self.mipmaps[0].height - header.width = self.mipmaps[0].width - header.linear_size = 0 - header.mipmap_count = len(self.mipmaps) - header.pixel_format.flags.four_c_c = 1 - header.pixel_format.four_c_c = DdsFormat.FourCC.DXT1 - header.pixel_format.bit_count = 0 - header.pixel_format.r_mask = 0 - header.pixel_format.g_mask = 0 - header.pixel_format.b_mask = 0 - header.pixel_format.a_mask = 0 - header.caps_1.complex = 1 - header.caps_1.texture = 1 - header.caps_1.mipmap = 1 - pixeldata.set_value(self.__get_pixeldata_stream()) - elif self.pixel_format in (NifFormat.PixelFormat.PX_FMT_DXT5, - NifFormat.PixelFormat.PX_FMT_DXT5_ALT): - # format used in Megami Tensei: Imagine - header.flags.caps = 1 - header.flags.height = 1 - header.flags.width = 1 - header.flags.pixel_format = 1 - header.flags.mipmap_count = 1 - header.flags.linear_size = 0 - header.height = self.mipmaps[0].height - header.width = self.mipmaps[0].width - header.linear_size = 0 - header.mipmap_count = len(self.mipmaps) - header.pixel_format.flags.four_c_c = 1 - header.pixel_format.four_c_c = DdsFormat.FourCC.DXT5 - header.pixel_format.bit_count = 0 - header.pixel_format.r_mask = 0 - header.pixel_format.g_mask = 0 - header.pixel_format.b_mask = 0 - header.pixel_format.a_mask = 0 - header.caps_1.complex = 1 - header.caps_1.texture = 1 - header.caps_1.mipmap = 1 - pixeldata.set_value(self.__get_pixeldata_stream()) - else: - raise ValueError( - "cannot save pixel format %i as DDS" % self.pixel_format) - - data.write(stream) - - -NifFormat.ATextureRenderData.__get_pixeldata_stream = get_pixeldata_stream_overide -NifFormat.ATextureRenderData.save_as_dds = save_as_dds_override -################# - - class TextureLoader: + external_textures = set() + @staticmethod def load_image(tex_path): """Returns an image or a generated image if none was found""" @@ -184,7 +62,7 @@ def load_image(tex_path): except: NifLog.warn(f"Texture '{name}' not found or not supported and no alternate available") b_image = bpy.data.images.new(name=name, width=1, height=1, alpha=True) - b_image.filepath=tex_path + b_image.filepath = tex_path else: b_image = bpy.data.images[name] return b_image @@ -198,41 +76,49 @@ def import_texture_source(self, source): if not source: return None - if isinstance(source, NifFormat.NiSourceTexture) and not source.use_external and NifOp.props.use_embedded_texture: + if isinstance(source, NifClasses.NiSourceTexture) and not source.use_external and NifOp.props.use_embedded_texture: return self.import_embedded_texture_source(source) else: return self.import_external_source(source) def import_embedded_texture_source(self, source): - - fn, tex = self.generate_image_name() - - # save embedded texture as dds file - with open(tex, "wb") as stream: - try: - NifLog.info(f"Saving embedded texture as {tex}") - source.pixel_data.save_as_dds(stream) - except ValueError: - NifLog.warn(f"Pixel format not supported in embedded texture {tex}!") - - return self.load_image(tex) + # first try to use the actual file name of this NiSourceTexture + tex_name = source.file_name + tex_path = os.path.join(os.path.dirname(NifOp.props.filepath), tex_name) + # not set, then use generated sequence name + if not tex_name: + tex_path = self.generate_image_name() + + # only save them once per run, obviously only useful if file_name was set + if tex_path not in self.external_textures: + # save embedded texture as dds file + with open(tex_path, "wb") as stream: + try: + NifLog.info(f"Saving embedded texture as {tex_path}") + source.pixel_data.save_as_dds(stream) + except ValueError: + NifLog.warn(f"Pixel format not supported in embedded texture {tex_path}!") + traceback.print_exc() + self.external_textures.add(tex_path) + + return self.load_image(tex_path) @staticmethod def generate_image_name(): """Find a file name (but avoid overwriting)""" n = 0 - while n < 1000: - fn = "image{:0>3d}.dds".format(n) + while n < 10000: + fn = f"image{n:0>4d}.dds" tex = os.path.join(os.path.dirname(NifOp.props.filepath), fn) if not os.path.exists(tex): break n += 1 - return fn, tex + return tex def import_external_source(self, source): # the texture uses an external image file - if isinstance(source, NifFormat.NiSourceTexture): - fn = source.file_name.decode() + if isinstance(source, NifClasses.NiSourceTexture): + fn = source.file_name elif isinstance(source, str): fn = source else: diff --git a/io_scene_niftools/modules/nif_import/property/texture/types/bsshadertexture.py b/io_scene_niftools/modules/nif_import/property/texture/types/bsshadertexture.py index 44973a219..f57d01611 100644 --- a/io_scene_niftools/modules/nif_import/property/texture/types/bsshadertexture.py +++ b/io_scene_niftools/modules/nif_import/property/texture/types/bsshadertexture.py @@ -78,7 +78,7 @@ def import_bsshaderproperty_textureset(self, bs_shader_property, nodes_wrapper): for slot_name, slot_i in slots.items(): # skip those whose index we don't know from old code if slot_i is not None and len(textures) > slot_i: - tex_str = textures[slot_i].decode() + tex_str = textures[slot_i] # see if it holds a texture if tex_str: NifLog.debug(f"Shader has active {slot_name}") @@ -86,11 +86,11 @@ def import_bsshaderproperty_textureset(self, bs_shader_property, nodes_wrapper): def import_bseffectshaderproperty_textures(self, bs_effect_shader_property, nodes_wrapper): - base = bs_effect_shader_property.source_texture.decode() + base = bs_effect_shader_property.source_texture if base: nodes_wrapper.create_and_link(TEX_SLOTS.BASE, base) - glow = bs_effect_shader_property.greyscale_texture.decode() + glow = bs_effect_shader_property.greyscale_texture if glow: nodes_wrapper.create_and_link(TEX_SLOTS.GLOW, glow) diff --git a/io_scene_niftools/modules/nif_import/scene/__init__.py b/io_scene_niftools/modules/nif_import/scene/__init__.py index 369d492eb..942051bc4 100644 --- a/io_scene_niftools/modules/nif_import/scene/__init__.py +++ b/io_scene_niftools/modules/nif_import/scene/__init__.py @@ -38,42 +38,23 @@ # ***** END LICENSE BLOCK ***** import bpy -from pyffi.formats.nif import NifFormat -from io_scene_niftools.properties.scene import _game_to_enum +from generated.formats.nif.versions import get_game + from io_scene_niftools.utils.logging import NifLog def import_version_info(data): scene = bpy.context.scene.niftools_scene - nif_version = data._version_value_._value - user_version = data._user_version_value_._value - user_version_2 = data._user_version_2_value_._value + nif_version = data.version + user_version = data.user_version + user_version_2 = data.bs_header.bs_version if hasattr(data, "bs_header") else 0 # filter possible games by nif version - possible_games = [] - for game, versions in NifFormat.games.items(): - if game != '?': - if nif_version in versions: - game_enum = _game_to_enum(game) - # go to next game if user version for this game does not match defined - if game_enum in scene.USER_VERSION: - if scene.USER_VERSION[game_enum] != user_version: - continue - # or user version in scene is not 0 when this game has no associated user version - elif user_version != 0: - continue - # same checks for user version 2 - if game_enum in scene.USER_VERSION_2: - if scene.USER_VERSION_2[game_enum] != user_version_2: - continue - elif user_version_2 != 0: - continue - # passed all checks, add to possible games list - possible_games.append(game_enum) + possible_games = get_game(data) if len(possible_games) == 1: - scene.game = possible_games[0] + scene.game = possible_games[0].name elif len(possible_games) > 1: - scene.game = possible_games[0] + scene.game = possible_games[0].name # todo[version] - check if this nif's version is marked as default for any of the possible games and use that NifLog.warn(f"Game set to '{possible_games[0]}', but multiple games qualified") scene.nif_version = nif_version diff --git a/io_scene_niftools/nif_common.py b/io_scene_niftools/nif_common.py index f5b345319..f0e0b9954 100644 --- a/io_scene_niftools/nif_common.py +++ b/io_scene_niftools/nif_common.py @@ -38,7 +38,9 @@ # ***** END LICENSE BLOCK ***** import bpy -import pyffi +import generated.formats.nif as NifFormat +from generated.spells.nif import NifToaster +from generated.spells.nif.fix import SpellScale from io_scene_niftools.utils import debugging from io_scene_niftools.utils.singleton import NifOp @@ -65,11 +67,11 @@ def __init__(self, operator, context): NifLog.info(f"Executing - Niftools : Blender Niftools Addon v{niftools_ver}" f"(running on Blender {bpy.app.version_string}, " - f"PyFFI {pyffi.__version__})") + f"Nif xml version {NifFormat.__xml_version__})") @staticmethod def apply_scale(data, scale): NifLog.info(f"Scale Correction set to {scale}") - toaster = pyffi.spells.nif.NifToaster() + toaster = NifToaster() toaster.scale = scale - pyffi.spells.nif.fix.SpellScale(data=data, toaster=toaster).recurse() \ No newline at end of file + SpellScale(data=data, toaster=toaster).recurse() \ No newline at end of file diff --git a/io_scene_niftools/nif_export.py b/io_scene_niftools/nif_export.py index 87ac2a679..9ce419cd9 100644 --- a/io_scene_niftools/nif_export.py +++ b/io_scene_niftools/nif_export.py @@ -41,8 +41,7 @@ import os.path import bpy -import pyffi.spells.nif.fix -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.modules.nif_export.animation.transform import TransformAnimation from io_scene_niftools.modules.nif_export.constraint import Constraint @@ -89,7 +88,7 @@ def execute(self): try: # catch export errors # protect against null nif versions - if bpy.context.scene.niftools_scene.game == 'NONE': + if bpy.context.scene.niftools_scene.game == 'UNKNOWN': raise NifError("You have not selected a game. Please select a game and" " nif version in the scene tab.") @@ -144,7 +143,7 @@ def execute(self): # if we are in that situation, add a trivial keyframe animation has_keyframecontrollers = False for block in block_store.block_to_obj: - if isinstance(block, NifFormat.NiKeyframeController): + if isinstance(block, NifClasses.NiKeyframeController): has_keyframecontrollers = True break if (not has_keyframecontrollers) and (not NifOp.props.bs_animation_node): @@ -154,10 +153,10 @@ def execute(self): if NifOp.props.bs_animation_node: for block in block_store.block_to_obj: - if isinstance(block, NifFormat.NiNode): + if isinstance(block, NifClasses.NiNode): # if any of the shape children has a controller or if the ninode has a controller convert its type - if block.controller or any(child.controller for child in block.children if isinstance(child, NifFormat.NiGeometry)): - new_block = NifFormat.NiBSAnimationNode().deepcopy(block) + if block.controller or any(child.controller for child in block.children if isinstance(child, NifClasses.NiGeometry)): + new_block = NifClasses.NiBSAnimationNode(NifData.data).deepcopy(block) # have to change flags to 42 to make it work new_block.flags = 42 root_block.replace_global_node(block, new_block) @@ -171,9 +170,9 @@ def execute(self): # bhkConvexVerticesShape of children of bhkListShapes need an extra bhkConvexTransformShape (see issue #3308638, reported by Koniption) # note: block_store.block_to_obj changes during iteration, so need list copy for block in list(block_store.block_to_obj.keys()): - if isinstance(block, NifFormat.bhkListShape): + if isinstance(block, NifClasses.BhkListShape): for i, sub_shape in enumerate(block.sub_shapes): - if isinstance(sub_shape, NifFormat.bhkConvexVerticesShape): + if isinstance(sub_shape, NifClasses.BhkConvexVerticesShape): coltf = block_store.create_block("bhkConvexTransformShape") coltf.material = sub_shape.material coltf.unknown_float_1 = 0.1 @@ -236,7 +235,7 @@ def execute(self): # generate mopps (must be done after applying scale!) if bpy.context.scene.niftools_scene.game in ('OBLIVION', 'FALLOUT_3', 'SKYRIM'): for block in block_store.block_to_obj: - if isinstance(block, NifFormat.bhkMoppBvTreeShape): + if isinstance(block, NifClasses.BhkMoppBvTreeShape): NifLog.info("Generating mopp...") block.update_mopp() # print "=== DEBUG: MOPP TREE ===" @@ -269,6 +268,7 @@ def execute(self): elif bpy.context.scene.niftools_scene.game == 'HOWLING_SWORD': data.modification = "jmihs1" + data.validate() with open(niffile, "wb") as stream: data.write(stream) diff --git a/io_scene_niftools/nif_import.py b/io_scene_niftools/nif_import.py index 1e0fca566..4102f0af4 100644 --- a/io_scene_niftools/nif_import.py +++ b/io_scene_niftools/nif_import.py @@ -39,8 +39,8 @@ import bpy -import pyffi.spells.nif.fix -from pyffi.formats.nif import NifFormat +import generated.spells.nif.fix +from generated.formats.nif import classes as NifClasses import io_scene_niftools.utils.logging from io_scene_niftools.file_io.nif import NifFile @@ -60,7 +60,6 @@ from io_scene_niftools.nif_common import NifCommon from io_scene_niftools.utils import math -from io_scene_niftools.utils.blocks import safe_decode from io_scene_niftools.utils.singleton import NifOp, NifData from io_scene_niftools.utils.logging import NifLog, NifError @@ -100,11 +99,11 @@ def execute(self): # merge skeleton roots and transform geometry into the rest pose if NifOp.props.merge_skeleton_roots: - pyffi.spells.nif.fix.SpellMergeSkeletonRoots(data=NifData.data).recurse() + generated.spells.nif.fix.SpellMergeSkeletonRoots(data=NifData.data).recurse() if NifOp.props.send_geoms_to_bind_pos: - pyffi.spells.nif.fix.SpellSendGeometriesToBindPosition(data=NifData.data).recurse() + generated.spells.nif.fix.SpellSendGeometriesToBindPosition(data=NifData.data).recurse() if NifOp.props.send_detached_geoms_to_node_pos: - pyffi.spells.nif.fix.SpellSendDetachedGeometriesToNodePosition(data=NifData.data).recurse() + generated.spells.nif.fix.SpellSendDetachedGeometriesToNodePosition(data=NifData.data).recurse() if NifOp.props.apply_skin_deformation: VertexGroup.apply_skin_deformation(NifData.data) @@ -115,7 +114,7 @@ def execute(self): # import all root blocks for root in NifData.data.roots: # root hack for corrupt better bodies meshes and remove geometry from better bodies on skeleton import - for b in (b for b in root.tree(block_type=NifFormat.NiGeometry) if b.is_skin()): + for b in (b for b in root.tree(block_type=NifClasses.NiGeometry) if b.is_skin()): # check if root belongs to the children list of the skeleton root if root in [c for c in b.skin_instance.skeleton_root.children]: # fix parenting and update transform accordingly @@ -145,18 +144,18 @@ def load_files(self): def import_root(self, root_block): """Main import function.""" # check that this is not a kf file - if isinstance(root_block, (NifFormat.NiSequence, NifFormat.NiSequenceStreamHelper)): + if isinstance(root_block, (NifClasses.NiSequence, NifClasses.NiSequenceStreamHelper)): raise io_scene_niftools.utils.logging.NifError("Use the KF import operator to load KF files.") # divinity 2: handle CStreamableAssetData - if isinstance(root_block, NifFormat.CStreamableAssetData): + if isinstance(root_block, NifClasses.CStreamableAssetData): root_block = root_block.root # mark armature nodes and bones self.armaturehelper.check_for_skin(root_block) # read the NIF tree - if isinstance(root_block, (NifFormat.NiNode, NifFormat.NiTriBasedGeom)): + if isinstance(root_block, NifClasses.NiNode) or self.objecthelper.has_geometry(root_block): b_obj = self.import_branch(root_block) ObjectProperty().import_extra_datas(root_block, b_obj) @@ -169,10 +168,10 @@ def import_root(self, root_block): self.objecthelper.remove_armature_modifier(b_child) self.objecthelper.append_armature_modifier(b_child, b_obj) - elif isinstance(root_block, NifFormat.NiCamera): + elif isinstance(root_block, NifClasses.NiCamera): NifLog.warn('Skipped NiCamera root') - elif isinstance(root_block, NifFormat.NiPhysXProp): + elif isinstance(root_block, NifClasses.NiPhysXProp): NifLog.warn('Skipped NiPhysXProp root') else: @@ -181,9 +180,9 @@ def import_root(self, root_block): def import_collision(self, n_node): """ Imports a NiNode's collision_object, if present""" if n_node.collision_object: - if isinstance(n_node.collision_object, NifFormat.bhkNiCollisionObject): + if isinstance(n_node.collision_object, NifClasses.BhkNiCollisionObject): return self.bhkhelper.import_bhk_shape(n_node.collision_object.body) - elif isinstance(n_node.collision_object, NifFormat.NiCollisionData): + elif isinstance(n_node.collision_object, NifClasses.NiCollisionData): return self.boundhelper.import_bounding_volume(n_node.collision_object.bounding_volume) return [] @@ -196,11 +195,11 @@ def import_branch(self, n_block, b_armature=None): if not n_block: return None - NifLog.info(f"Importing data for block '{safe_decode(n_block.name)}'") - if isinstance(n_block, NifFormat.NiTriBasedGeom) and NifOp.props.process != "SKELETON_ONLY": + NifLog.info(f"Importing data for block '{n_block.name}'") + if self.objecthelper.has_geometry(n_block) and NifOp.props.process != "SKELETON_ONLY": return self.objecthelper.import_geometry_object(b_armature, n_block) - elif isinstance(n_block, NifFormat.NiNode): + elif isinstance(n_block, NifClasses.NiNode): # import object if self.armaturehelper.is_armature_root(n_block): # all bones in the tree are also imported by import_armature @@ -223,11 +222,10 @@ def import_branch(self, n_block, b_armature=None): else: # this is a fallback for a weird bug, when a node is child of a NiLodNode in a skeletal nif b_obj = self.objecthelper.create_b_obj(n_block, None, name=n_name) - b_obj.niftools.flags = n_block.flags - else: # import as an empty b_obj = NiTypes.import_empty(n_block) + b_obj.niftools.flags = n_block.flags # find children b_children = [] diff --git a/io_scene_niftools/operators/common_op.py b/io_scene_niftools/operators/common_op.py index bf67b7c47..f2b69016e 100644 --- a/io_scene_niftools/operators/common_op.py +++ b/io_scene_niftools/operators/common_op.py @@ -37,7 +37,13 @@ # # ***** END LICENSE BLOCK ***** import bpy +from itertools import chain +from generated.formats.nif.versions import available_versions + + +nif_extensions = list(set(chain.from_iterable([version.ext for version in available_versions if version.supported]))) +nif_glob = "*.jmi" + (f";*.{';*.'.join(nif_extensions)}" if nif_extensions else '') class CommonDevOperator: """Abstract base class for import and export user interface.""" @@ -106,7 +112,7 @@ class CommonNif: # File name filter for file select dialog. filter_glob: bpy.props.StringProperty( - default="*.nif;*.item;*.nifcache;*.jmi", + default=nif_glob, options={'HIDDEN'}) diff --git a/io_scene_niftools/operators/geometry.py b/io_scene_niftools/operators/geometry.py index d4f26c9b9..7ae69f19e 100644 --- a/io_scene_niftools/operators/geometry.py +++ b/io_scene_niftools/operators/geometry.py @@ -45,31 +45,31 @@ class BsInvMarkerAdd(Operator): """Adds BsInvMarker set""" - bl_idname = "object.niftools_bs_invmarker_add" + bl_idname = "object.bs_inv_marker_add" bl_label = "Add Inventory Marker" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): - obj = context.active_object - b_obj_invmarker = obj.niftools_bs_invmarker.add() - b_obj_invmarker.name = "INV" - b_obj_invmarker.bs_inv_x = 0 - b_obj_invmarker.bs_inv_y = 0 - b_obj_invmarker.bs_inv_z = 0 - b_obj_invmarker.bs_inv_zoom = 1 + bs_inv = context.object.niftools.bs_inv + bs_inv_item = bs_inv.add() + bs_inv_item.name = "INV" + bs_inv_item.bs_inv_x = 0 + bs_inv_item.bs_inv_y = 0 + bs_inv_item.bs_inv_z = 0 + bs_inv_item.bs_inv_zoom = 1 return {'FINISHED'} class BsInvMarkerRemove(bpy.types.Operator): """Removes BsInvMarker set""" - bl_idname = "object.niftools_bs_invmarker_remove" + bl_idname = "object.bs_inv_marker_remove" bl_label = "Remove Inventory Marker" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): - item = len(context.active_object.niftools_bs_invmarker) - 1 - obj = context.active_object - obj.niftools_bs_invmarker.remove(item) + bs_inv = context.object.niftools.bs_inv + item = len(bs_inv) - 1 + bs_inv.remove(item) return {'FINISHED'} diff --git a/io_scene_niftools/operators/nif_import_op.py b/io_scene_niftools/operators/nif_import_op.py index 27894b945..88876944a 100644 --- a/io_scene_niftools/operators/nif_import_op.py +++ b/io_scene_niftools/operators/nif_import_op.py @@ -39,13 +39,13 @@ import bpy from bpy.types import Operator, Panel -from bpy_extras.io_utils import ImportHelper +from bpy_extras.io_utils import ImportHelper, orientation_helper from io_scene_niftools.nif_import import NifImport from io_scene_niftools.operators.common_op import CommonDevOperator, CommonScale, CommonNif from io_scene_niftools.utils.decorators import register_classes, unregister_classes - +@orientation_helper(axis_forward='Z', axis_up='-Y') class NifImportOperator(Operator, ImportHelper, CommonScale, CommonDevOperator, CommonNif): """Operator for loading a nif file.""" @@ -117,6 +117,13 @@ class NifImportOperator(Operator, ImportHelper, CommonScale, CommonDevOperator, description="Loads texture embedded in .nif", default=False) + #Automatically detect armature orientation + override_armature_orientation: bpy.props.BoolProperty( + name="Override Armature Orientation", + description="Override detected armature orientation", + default=False) + + def draw(self, context): pass diff --git a/io_scene_niftools/properties/collision.py b/io_scene_niftools/properties/collision.py index dd938c22e..1cffb7c24 100644 --- a/io_scene_niftools/properties/collision.py +++ b/io_scene_niftools/properties/collision.py @@ -46,7 +46,7 @@ ) from bpy.types import PropertyGroup -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.utils.decorators import register_classes, unregister_classes @@ -57,13 +57,17 @@ def game_specific_col_layer_items(self, context): current_game = bpy.context.scene.niftools_scene.game else: current_game = context.scene.niftools_scene.game - if current_game == "OBLIVION": - col_layer_format = NifFormat.OblivionLayer + col_layer_format = None + if current_game in ("OBLIVION", "OBLIVION_KF"): + col_layer_format = NifClasses.OblivionLayer elif current_game == "FALLOUT_3": - col_layer_format = NifFormat.OblivionLayer - elif current_game == "SKYRIM": - col_layer_format = NifFormat.SkyrimLayer - return [(str(value), item, "", value) for value, item in zip(col_layer_format._enumvalues, col_layer_format._enumkeys)] + col_layer_format = NifClasses.Fallout3Layer + elif current_game in ("SKYRIM" , "SKYRIM_SE", "FALLOUT_4"): + col_layer_format = NifClasses.SkyrimLayer + if col_layer_format is None: + return [] + else: + return [(str(member.value), member.name, "", member.value) for member in col_layer_format] class CollisionProperty(PropertyGroup): """Group of Havok related properties, which gets attached to objects through a property pointer.""" @@ -71,7 +75,7 @@ class CollisionProperty(PropertyGroup): motion_system: EnumProperty( name='Motion System', description='Havok Motion System settings for bhkRigidBody(t)', - items=[(item, item, "", i) for i, item in enumerate(NifFormat.MotionSystem._enumkeys)], + items=[(member.name, member.name, "", i) for i, member in enumerate(NifClasses.HkMotionType)], # default = 'MO_SYS_FIXED', ) @@ -91,19 +95,19 @@ class CollisionProperty(PropertyGroup): deactivator_type: EnumProperty( name='Deactivator Type', description='Motion deactivation setting', - items=[(item, item, "", i) for i, item in enumerate(NifFormat.DeactivatorType._enumkeys)], + items=[(member.name, member.name, "", i) for i, member in enumerate(NifClasses.HkDeactivatorType)], ) solver_deactivation: EnumProperty( name='Solver Deactivation', description='Motion deactivation setting', - items=[(item, item, "", i) for i, item in enumerate(NifFormat.SolverDeactivation._enumkeys)], + items=[(member.name, member.name, "", i) for i, member in enumerate(NifClasses.HkSolverDeactivation)], ) quality_type: EnumProperty( name='Quality Type', description='Determines quality of motion', - items=[(item, item, "", i) for i, item in enumerate(NifFormat.MotionQuality._enumkeys)], + items=[(member.name, member.name, "", i) for i, member in enumerate(NifClasses.HkQualityType)], # default = 'MO_QUAL_FIXED', ) diff --git a/io_scene_niftools/properties/object.py b/io_scene_niftools/properties/object.py index a35b09a17..36c89fe64 100644 --- a/io_scene_niftools/properties/object.py +++ b/io_scene_niftools/properties/object.py @@ -47,30 +47,35 @@ ) from bpy.types import PropertyGroup, Object -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.utils.decorators import register_classes, unregister_classes -prn_array = [ - ["OBLIVION", "FALLOUT_3", "SKYRIM"], - ["DAGGER", "SideWeapon", "Weapon", "WeaponDagger"], - ["2HANDED", "BackWeapon", "Weapon", "WeaponBack"], - ["BOW", "BackWeapon", None, "WeaponBow"], - ["MACE", "SideWeapon", "Weapon", "WeaponMace"], - ["SHIELD", "Bip01 L ForearmTwist", None, "SHIELD"], - ["STAFF", "Torch", "Weapon", "WeaponStaff"], - ["SWORD", "SideWeapon", "Weapon", "WeaponSword"], - ["AXE", "SideWeapon", "Weapon", "WeaponAxe"], - ["QUIVER", "Quiver", "Weapon", "QUIVER"], - ["TORCH", "Torch", "Weapon", "SHIELD"], - ["HELMET", "Bip01 Head", "Bip01 Head", "NPC Head [Head]"], - ["RING", "Bip01 R Finger1", "Bip01 R Finger1", "NPC R Finger10 [RF10]"] - ] -# PRN_DICT is a dict like so: dict['SLOT']['GAME']: 'Bone' -PRN_DICT = {} -for row in prn_array[1:]: - PRN_DICT[row[0]] = dict(zip(prn_array[0], row[1:])) +prn_map = {"OBLIVION": [("SideWeapon", ""), + ("BackWeapon", ""), + ("Bip01 L ForearmTwist", "Used for shields"), + ("Torch", ""), + ("Quiver", ""), + ("Bip01 Head", "Used for helmets"), + ("Bip01 R Finger1", "Used for rings")], + "FALLOUT_3": [("Weapon", ""), + ("Bip01 Head", "Used for helmets"), + ("Bip01 R Finger1", "")], + "SKYRIM": [("WeaponDagger", ""), + ("WeaponBack", ""), + ("WeaponBow", ""), + ("WeaponMace", ""), + ("SHIELD", ""), + ("WeaponStaff", ""), + ("WeaponSword", ""), + ("WeaponAxe", ""), + ("QUIVER", ""), + ("SHIELD", ""), + ("NPC Head [Head]", "Used for helmets"), + ("NPC R Finger10 [RF10]", "Used for rings")] + } +prn_map["SKYRIM_SE"] = prn_map["SKYRIM"] class ExtraData(PropertyGroup): @@ -97,22 +102,60 @@ class ExtraDataStore(PropertyGroup): ) +class BsInventoryMarker(PropertyGroup): + name: StringProperty( + name="", + default='INV' + ) + + x: FloatProperty( + name="X Rotation", + description="Rotation of object in inventory around the x axis", + default=0, + subtype="ANGLE" + ) + + y: FloatProperty( + name="Y Rotation", + description="Rotation of object in inventory around the y axis", + default=0, + subtype="ANGLE" + ) + + z: FloatProperty( + name="Z Rotation", + description="Rotation of object in inventory around the z axis", + default=0, + subtype="ANGLE" + ) + + zoom: FloatProperty( + name="Zoom", + description="Inventory object Zoom level", + default=1 + ) + + +prn_versioned_arguments = {} +if bpy.app.version >= (3, 3, 0): + prn_versioned_arguments['search'] = lambda self, context, edit_text: prn_map.get(context.scene.niftools_scene.game, []) + class ObjectProperty(PropertyGroup): - rootnode: EnumProperty( - name='Nif Root Node', - description='Type of property used to display meshes', + nodetype: EnumProperty( + name='Node Type', + description='Type of node this empty represents', items=( - ('NiNode', 'NiNode', "", 0), - ('BSFadeNode', 'BSFadeNode', "", 1)), + ('NiNode', 'NiNode', "", 0), + ('BSFadeNode', 'BSFadeNode', "", 1)), default='NiNode', ) - prn_location: EnumProperty( + + prn_location: StringProperty( name='Weapon Location', description='Attachment point of weapon, for Skyrim, FO3 & Oblivion', - items=[(item, item, "", i) for i, item in enumerate(["NONE"] + list(PRN_DICT.keys()))], - # default = 'NONE' + **prn_versioned_arguments, ) longname: StringProperty( @@ -122,7 +165,7 @@ class ObjectProperty(PropertyGroup): consistency_flags: EnumProperty( name='Consistency Flag', description='Controls animation type', - items=[(item, item, "", i) for i, item in enumerate(NifFormat.ConsistencyType._enumkeys)], + items=[(member.name, member.name, "", i) for i, member in enumerate(NifClasses.ConsistencyType)], # default = 'SHADER_DEFAULT' ) @@ -155,47 +198,14 @@ class ObjectProperty(PropertyGroup): description="The bone that acts as the root of the SkinInstance", ) - -class BsInventoryMarker(PropertyGroup): - - name: StringProperty( - name="", - default='INV' - ) - - bs_inv_x: FloatProperty( - name="Inv X value", - description="Rotation of object in inventory around the x axis", - default=0, - subtype = "ANGLE" - ) - - bs_inv_y: FloatProperty( - name="Inv Y value", - description="Rotation of object in inventory around the y axis", - default=0, - subtype = "ANGLE" - ) - - bs_inv_z: FloatProperty( - name="Inv Z value", - description="Rotation of object in inventory around the z axis", - default=0, - subtype = "ANGLE" - ) - - bs_inv_zoom: FloatProperty( - name="Inv Zoom Value", - description="Inventory object Zoom level", - default=1 - ) + bs_inv: bpy.props.CollectionProperty(type=BsInventoryMarker) CLASSES = [ + BsInventoryMarker, ExtraData, ExtraDataStore, ObjectProperty, - BsInventoryMarker ] @@ -203,12 +213,10 @@ def register(): register_classes(CLASSES, __name__) bpy.types.Object.niftools = bpy.props.PointerProperty(type=ObjectProperty) - bpy.types.Object.niftools_bs_invmarker = bpy.props.CollectionProperty(type=BsInventoryMarker) def unregister(): del bpy.types.Object.niftools - del bpy.types.Object.niftools_bs_invmarker unregister_classes(CLASSES, __name__) diff --git a/io_scene_niftools/properties/scene.py b/io_scene_niftools/properties/scene.py index 3b0c5fbdd..3d536cd5d 100644 --- a/io_scene_niftools/properties/scene.py +++ b/io_scene_niftools/properties/scene.py @@ -39,31 +39,45 @@ import bpy -from bpy.props import PointerProperty, IntProperty +from bpy.props import PointerProperty, IntProperty, EnumProperty, StringProperty, FloatProperty, CollectionProperty from bpy.types import PropertyGroup +from itertools import chain -from pyffi.formats.nif import NifFormat +from generated.formats.nif.versions import available_versions, set_game from io_scene_niftools.utils.decorators import register_classes, unregister_classes -def _game_to_enum(game): - symbols = ":,'\" +-*!?;./=" - table = str.maketrans(symbols, "_" * len(symbols)) - enum = game.upper().translate(table).replace("__", "_") - return enum +class DummyClass: pass +dummy_context = DummyClass() +dummy_context.bs_header = DummyClass() +primary_games = list(chain.from_iterable(version.primary_games for version in available_versions if version.supported)) +all_games = list(chain.from_iterable(version.all_games for version in available_versions if version.supported)) +game_version_map = {} + + +def populate_version_map(iterable, version_map): + for game in iterable: + if game not in version_map: + dummy_context.version = 0 + dummy_context.user_version = 0 + dummy_context.bs_header.bs_version = 0 + set_game(dummy_context, game) + game_version_map[game.name] = (dummy_context.version, dummy_context.user_version, dummy_context.bs_header.bs_version) + + +populate_version_map(primary_games, game_version_map) +populate_version_map(all_games, game_version_map) +game_version_map["UNKNOWN"] = (0, 0, 0) + # noinspection PyUnusedLocal def update_version_from_game(self, context): """Updates the Scene panel's numerical version fields if its game value has been changed""" - self.nif_version = self.VERSION.get(self.game, 0) - self.user_version = self.USER_VERSION.get(self.game, 0) - self.user_version_2 = self.USER_VERSION_2.get(self.game, 0) - + self.nif_version, self.user_version, self.user_version_2 = game_version_map[self.game] class Scene(PropertyGroup): - nif_version: IntProperty( name='Version', description="The Gamebryo Engine version used", @@ -84,35 +98,16 @@ class Scene(PropertyGroup): # For which game to export. game: bpy.props.EnumProperty( - items=[('NONE', 'NONE', 'No game selected')] + [ - (_game_to_enum(game), game, "Export for " + game) - for game in sorted( - [x for x in NifFormat.games.keys() if x != '?']) + items=[('UNKNOWN', 'UNKNOWN', 'No game selected')] + [ + (member.name, member.value, "Export for " + member.value) + for member in sorted( + [member for member in set(all_games)], key=lambda x: x.name) ], name="Game", description="For which game to export", - default='NONE', + default='UNKNOWN', update=update_version_from_game) - # Map game enum to nif version. - VERSION = { - _game_to_enum(game): versions[-1] - for game, versions in NifFormat.games.items() if game != '?' - } - - USER_VERSION = { - 'OBLIVION': 11, - 'FALLOUT_3': 11, - 'SKYRIM': 12, - 'DIVINITY_2': 131072 - } - - USER_VERSION_2 = { - 'OBLIVION': 11, - 'FALLOUT_3': 34, - 'SKYRIM': 83 - } - scale_correction: bpy.props.FloatProperty( name="Scale Correction", description="Changes size of mesh to fit onto Blender's default grid", diff --git a/io_scene_niftools/properties/shader.py b/io_scene_niftools/properties/shader.py index 274df5f5f..976f3025c 100644 --- a/io_scene_niftools/properties/shader.py +++ b/io_scene_niftools/properties/shader.py @@ -44,7 +44,7 @@ ) from bpy.types import PropertyGroup -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.utils.decorators import register_classes, unregister_classes @@ -66,14 +66,14 @@ class ShaderProps(PropertyGroup): bsspplp_shaderobjtype: EnumProperty( name='BS Shader PP Lighting Object Type', description='Type of object linked to shader', - items=[(item, item, "", i) for i, item in enumerate(NifFormat.BSShaderType._enumkeys)], + items=[(member.name, member.name, "", i) for i, member in enumerate(NifClasses.BSShaderType)], default='SHADER_DEFAULT' ) bslsp_shaderobjtype: EnumProperty( name='BS Lighting Shader Object Type', description='Type of object linked to shader', - items=[(item, item, "", i) for i, item in enumerate(NifFormat.BSLightingShaderPropertyShaderType._enumkeys)], + items=[(member.name, member.name, "", i) for i, member in enumerate(NifClasses.BSLightingShaderType)], # default = 'SHADER_DEFAULT' ) @@ -88,15 +88,12 @@ def prettify_prop_name(property_name): annotations_dict = ShaderProps.__dict__.get('__annotations__', None) if annotations_dict: - for property_name in NifFormat.BSShaderFlags._names: - if property_name not in annotations_dict: - annotations_dict[property_name] = BoolProperty(name=prettify_prop_name(property_name[3:])) - for property_name in NifFormat.SkyrimShaderPropertyFlags1._names: - if property_name not in annotations_dict: - annotations_dict[property_name] = BoolProperty(name=prettify_prop_name(property_name[7:])) - for property_name in NifFormat.SkyrimShaderPropertyFlags2._names: - if property_name not in annotations_dict: - annotations_dict[property_name] = BoolProperty(name=prettify_prop_name(property_name[7:])) + for flag_field in (NifClasses.BSShaderFlags, + NifClasses.SkyrimShaderPropertyFlags1, + NifClasses.SkyrimShaderPropertyFlags2): + for property_name in flag_field.__members__: + if property_name not in annotations_dict: + annotations_dict[property_name] = BoolProperty(name=prettify_prop_name(property_name)) CLASSES = [ diff --git a/io_scene_niftools/ui/object.py b/io_scene_niftools/ui/object.py index ef57f7184..77d746599 100644 --- a/io_scene_niftools/ui/object.py +++ b/io_scene_niftools/ui/object.py @@ -42,46 +42,53 @@ from io_scene_niftools.utils.decorators import register_classes, unregister_classes -class ObjectPanel(Panel): - bl_label = "Niftools Object Property" - bl_idname = "NIFTOOLS_PT_ObjectPanel" - +class ObjectButtonsPanel(Panel): bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' bl_context = "object" + @staticmethod + def is_root_object(b_obj): + return b_obj.parent is None + +class ObjectPanel(ObjectButtonsPanel): + bl_label = "Niftools Object Property" + bl_idname = "NIFTOOLS_PT_ObjectPanel" + # noinspection PyUnusedLocal @classmethod def poll(cls, context): return True def draw(self, context): - ob = context.object - nif_obj_props = ob.niftools + b_obj = context.object + nif_obj_props = b_obj.niftools layout = self.layout row = layout.column() - row.prop(nif_obj_props, "rootnode") - row.prop(nif_obj_props, "prn_location") - row.prop(nif_obj_props, "upb") - row.prop(nif_obj_props, "bsxflags") - row.prop(nif_obj_props, "consistency_flags") + if self.is_root_object(b_obj): + if b_obj.type == "EMPTY": + row.prop(nif_obj_props, "nodetype") + if b_obj.type != "ARMATURE": + # prn nistringextradata is only useful as replacement for rigging data + row.prop(nif_obj_props, "prn_location") + row.prop(nif_obj_props, "upb") + row.prop(nif_obj_props, "bsxflags") + if b_obj.type == "MESH": + # consistency flags only exist for NiGeometry + row.prop(nif_obj_props, "consistency_flags") row.prop(nif_obj_props, "flags") row.prop(nif_obj_props, "longname") - parent = ob.parent + parent = b_obj.parent if parent and parent.type == 'ARMATURE': row.prop_search(nif_obj_props, "skeleton_root", parent.data, "bones") -class ObjectExtraData(Panel): +class ObjectExtraData(ObjectButtonsPanel): bl_label = "Niftools Object Extra Data" bl_idname = "NIFTOOLS_PT_ObjectExtraDataPanel" - bl_space_type = 'PROPERTIES' - bl_region_type = 'WINDOW' - bl_context = "object" - # noinspection PyUnusedLocal @classmethod def poll(cls, context): @@ -139,43 +146,38 @@ def draw_item(self, context, layout, data, item, icon, active_data, active_propn split.prop(item, "data", text="", emboss=False, translate=False, icon='BORDERMOVE') -class ObjectBSInvMarkerPanel(Panel): +class ObjectBSInvMarkerPanel(ObjectButtonsPanel): bl_label = "Niftools BS Inv Marker" bl_idname = "NIFTOOLS_PT_ObjectBSInvMarker" - - bl_space_type = 'PROPERTIES' - bl_region_type = 'WINDOW' - bl_context = "object" + bl_parent_id = "NIFTOOLS_PT_ObjectPanel" # noinspection PyUnusedLocal @classmethod def poll(cls, context): - return True + return cls.is_root_object(context.object) def draw(self, context): layout = self.layout - nif_bsinv_props = context.object.niftools_bs_invmarker - row = layout.row() - if not context.object.niftools_bs_invmarker: - row.operator("object.niftools_bs_invmarker_add", icon='ZOOM_IN', text="") - if context.object.niftools_bs_invmarker: - row.operator("object.niftools_bs_invmarker_remove", icon='ZOOM_OUT', text="") - + bs_inv = context.object.niftools.bs_inv + if not bs_inv: + row.operator("object.bs_inv_marker_add", icon='ZOOM_IN', text="") + else: + row.operator("object.bs_inv_marker_remove", icon='ZOOM_OUT', text="") col = row.column(align=True) - for i, x in enumerate(nif_bsinv_props): - col.prop(nif_bsinv_props[i], "bs_inv_x", index=i) - col.prop(nif_bsinv_props[i], "bs_inv_y", index=i) - col.prop(nif_bsinv_props[i], "bs_inv_z", index=i) - col.prop(nif_bsinv_props[i], "bs_inv_zoom", index=i) + for i, x in enumerate(bs_inv): + col.prop(bs_inv[i], "x", index=i) + col.prop(bs_inv[i], "y", index=i) + col.prop(bs_inv[i], "z", index=i) + col.prop(bs_inv[i], "zoom", index=i) classes = [ - ObjectBSInvMarkerPanel, ObjectExtraDataList, ObjectExtraDataType, ObjectExtraData, - ObjectPanel + ObjectPanel, + ObjectBSInvMarkerPanel, ] diff --git a/io_scene_niftools/ui/operators/nif_import.py b/io_scene_niftools/ui/operators/nif_import.py index 72c473809..7f630b6cb 100644 --- a/io_scene_niftools/ui/operators/nif_import.py +++ b/io_scene_niftools/ui/operators/nif_import.py @@ -38,7 +38,6 @@ # ***** END LICENSE BLOCK ***** from bpy.types import Panel - from io_scene_niftools.utils.decorators import register_classes, unregister_classes @@ -161,6 +160,32 @@ def draw(self, context): layout.prop(operator, "send_geoms_to_bind_pos") layout.prop(operator, "apply_skin_deformation") + layout.prop(operator, "override_armature_orientation") + + +class OperatorImportOverrideArmatureOrientationPanel(OperatorSetting, Panel): + bl_label = "Override Armature Orientation" + bl_idname = "NIFTOOLS_PT_import_operator_override_armature_orientation" + + @classmethod + def poll(cls, context): + sfile = context.space_data + operator = sfile.active_operator + + return operator.bl_idname == "IMPORT_SCENE_OT_nif" + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False # No animation. + + sfile = context.space_data + operator = sfile.active_operator + + layout.enabled = operator.override_armature_orientation + + layout.prop(operator, "axis_forward") + layout.prop(operator, "axis_up") class OperatorImportAnimationPanel(OperatorSetting, Panel): bl_options = {'DEFAULT_CLOSED'} @@ -194,6 +219,7 @@ def draw(self, context): OperatorImportGeometryPanel, OperatorImportTexturePanel, OperatorImportArmaturePanel, + OperatorImportOverrideArmatureOrientationPanel, OperatorImportAnimationPanel ] diff --git a/io_scene_niftools/ui/scene.py b/io_scene_niftools/ui/scene.py index 16f1503cb..226e79208 100644 --- a/io_scene_niftools/ui/scene.py +++ b/io_scene_niftools/ui/scene.py @@ -39,18 +39,18 @@ from bpy.types import Panel -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.utils.decorators import register_classes, unregister_classes -class SceneButtonsPanel: +class SceneButtonsPanel(Panel): bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' bl_context = "scene" -class ScenePanel(SceneButtonsPanel, Panel): +class ScenePanel(SceneButtonsPanel): bl_label = "Niftools Scene Panel" bl_idname = "NIFTOOLS_PT_scene" @@ -67,7 +67,7 @@ def draw(self, context): row.prop(nif_scene_props, "game") -class SceneVersionInfoPanel(SceneButtonsPanel, Panel): +class SceneVersionInfoPanel(SceneButtonsPanel): bl_label = "Nif Version Info" bl_idname = "NIFTOOLS_PT_scene_version_info" bl_parent_id = "NIFTOOLS_PT_scene" @@ -77,7 +77,7 @@ def draw(self, context): layout.use_property_split = True nif_scene_props = context.scene.niftools_scene - layout.label(text=NifFormat.HeaderString.version_string(nif_scene_props.nif_version)) + layout.label(text=NifClasses.HeaderString.version_string(nif_scene_props.nif_version)) flow = layout.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=True) @@ -90,6 +90,7 @@ def draw(self, context): col = flow.column() col.prop(nif_scene_props, "user_version_2") + # class SceneAuthorInfoPanel(SceneButtonsPanel, Panel): # bl_label = "Nif Author Info" # bl_idname = "NIFTOOLS_PT_scene_author_info" diff --git a/io_scene_niftools/ui/shader.py b/io_scene_niftools/ui/shader.py index 17c757028..7706b4800 100644 --- a/io_scene_niftools/ui/shader.py +++ b/io_scene_niftools/ui/shader.py @@ -39,7 +39,7 @@ from bpy.types import Panel -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.utils.decorators import register_classes, unregister_classes @@ -69,15 +69,15 @@ def draw(self, context): if nif_obj_props.bs_shadertype == 'BSShaderPPLightingProperty': row.prop(nif_obj_props, "bsspplp_shaderobjtype") - for property_name in sorted(NifFormat.BSShaderFlags._names): + for property_name in sorted(NifClasses.BSShaderFlags.__members__): row.prop(nif_obj_props, property_name) elif nif_obj_props.bs_shadertype in ('BSLightingShaderProperty', 'BSEffectShaderProperty'): row.prop(nif_obj_props, "bslsp_shaderobjtype") - for property_name in sorted(NifFormat.SkyrimShaderPropertyFlags1._names): + for property_name in sorted(NifClasses.SkyrimShaderPropertyFlags1.__members__): row.prop(nif_obj_props, property_name) - for property_name in sorted(NifFormat.SkyrimShaderPropertyFlags2._names): + for property_name in sorted(NifClasses.SkyrimShaderPropertyFlags2.__members__): row.prop(nif_obj_props, property_name) diff --git a/io_scene_niftools/utils/blocks.py b/io_scene_niftools/utils/blocks.py deleted file mode 100644 index 64533df58..000000000 --- a/io_scene_niftools/utils/blocks.py +++ /dev/null @@ -1,7 +0,0 @@ -""" Nif Utilities, stores common code that is used across the code base""" - -def safe_decode(b: bytes) -> str: - try: - return b.decode() - except UnicodeDecodeError: - return b.decode("shift-jis", errors="surrogateescape") \ No newline at end of file diff --git a/io_scene_niftools/utils/consts.py b/io_scene_niftools/utils/consts.py index c4dd53c42..22ec76837 100644 --- a/io_scene_niftools/utils/consts.py +++ b/io_scene_niftools/utils/consts.py @@ -87,3 +87,9 @@ class EmptyObject: TEX_SLOTS.DECAL_2 = "Decal 2" TEX_SLOTS.SPECULAR = "Specular" TEX_SLOTS.NORMAL = "Normal" + +# fcurve data types for blender +QUAT = "rotation_quaternion" +EULER = "rotation_euler" +LOC = "location" +SCALE = "scale" diff --git a/io_scene_niftools/utils/math.py b/io_scene_niftools/utils/math.py index 74c10735a..e66ead983 100644 --- a/io_scene_niftools/utils/math.py +++ b/io_scene_niftools/utils/math.py @@ -39,7 +39,7 @@ import bpy from bpy_extras.io_utils import axis_conversion import mathutils -from pyffi.formats.nif import NifFormat +from generated.formats.nif import classes as NifClasses from io_scene_niftools.utils.logging import NifLog @@ -181,8 +181,9 @@ def find_property(n_block, property_type): if isinstance(prop, property_type): return prop - if hasattr(n_block, "bs_properties"): - for prop in n_block.bs_properties: + for prop_name in ("shader_property", "alpha_property"): + if hasattr(n_block, prop_name): + prop = getattr(n_block, prop_name) if isinstance(prop, property_type): return prop return None @@ -198,6 +199,16 @@ def find_controller(n_block, controller_type): ctrl = ctrl.next_controller +def controllers_iter(n_block, controller_type): + """Find a controller.""" + ctrl = n_block.controller + while ctrl: + if isinstance(ctrl, controller_type): + if ctrl.data or ctrl.interpolator: + yield ctrl + ctrl = ctrl.next_controller + + def find_extra(n_block, extratype): # TODO: 3.0 - Optimise @@ -238,7 +249,7 @@ def mathutils_to_nifformat_matrix(b_matrix): """Convert a blender matrix to a NifFormat.Matrix44""" # transpose to swap columns for rows so we can use pyffi's set_rows() directly # instead of setting every single value manually - n_matrix = NifFormat.Matrix44() + n_matrix = NifClasses.Matrix44() n_matrix.set_rows(*b_matrix.transposed()) return n_matrix