68 Commits

Author SHA1 Message Date
3e066bbe64 Updated to use Blender 4.3.2 2025-02-25 17:42:09 -06:00
PrismaticFlower
0cc8beb830 Merge pull request #19 from styinx/#17_animation_frame_export
#17: Convert frame time from float to int
2025-02-23 06:34:34 +00:00
Christoph Zorn
336870ecb5 #17: Convert frame time from float to int 2024-05-04 10:09:17 +02:00
PrismaticFlower
daacdf1a5d handle Face Corner color attributes
fixes #15
2023-12-05 09:49:59 +13:00
PrismaticFlower
48c05380c6 update version number 2023-12-03 12:04:33 +13:00
PrismaticFlower
9f4a205f3d Merge pull request #14 from styinx/#13_bsdf_inputs_specular_key
#13: Add check for blender version on specular key
2023-12-03 12:03:14 +13:00
PrismaticFlower
216ea5d7a4 Merge branch 'master' of https://github.com/PrismaticFlower/SWBF-msh-Blender-IO 2023-12-03 11:58:19 +13:00
Christoph Zorn
d2c7dbd79f #13: Add check for blender version on specular key 2023-12-02 11:58:03 +01:00
PrismaticFlower
6247a289e6 update version number 2023-10-31 08:47:35 +13:00
PrismaticFlower
2b60f0d2a6 Merge pull request #12 from LeovanGit/vertex-color
Fix vertex colors export
2023-10-31 08:45:19 +13:00
LeovanGit
8d5c701b86 fix vertex colors export 2023-10-30 12:46:07 +03:00
PrismaticFlower
c072c9e56d update version number 2023-10-29 11:22:51 +13:00
PrismaticFlower
2421ba70c2 Merge pull request #10 from LeovanGit/vertex-colors
Vertex Colors
2023-10-29 11:16:18 +13:00
PrismaticFlower
ae42cda6ab Skip adding color attributes when unneeded
This is a very small change to skip adding the vertex colours to the Blender mesh if no segment of the geometry being loaded has vertex colours.
2023-10-29 11:13:12 +13:00
LeovanGit
0ac921d855 Add vertex colors to blender + fix unpack_color() 2023-10-27 23:05:05 +03:00
Will
806a7cc060 Merge pull request #8 from maximstewart/master
Add Animation Import(s) to NLA Track List
2023-10-01 08:43:52 -07:00
d1d83d39af Partial Revert: Animation Track patch cleanup 2023-06-02 22:27:58 -05:00
1ec4332576 Animation Track patch cleanup 2023-06-02 21:49:24 -05:00
125ad2792c Fixed import of animations to allow for bulk import 2023-05-28 17:18:50 -05:00
William Herald Snyder
f451be4d18 Check number of bytes remaining before reading texture strings 2022-10-29 12:59:11 -04:00
William Herald Snyder
613cb20678 Many msh files (e.g. those in BFX) have multiple models assigned to the same index (MNDX). Indices should be linked only to the first model that uses them to ensure proper skinning. 2022-10-09 13:55:24 -04:00
William Herald Snyder
432c9ff380 Hidden objects will be unhidden after calling evaluated_get, so they must be tracked beforehand when exporting. 2022-10-08 22:10:40 -04:00
William Herald Snyder
ba762d9548 Abort export if SELECTED or SELECTED_WITH_CHILDREN is chosen but nothing is selected and warn user that hidden objects can't be selected. 2022-10-08 22:07:44 -04:00
William Herald Snyder
b120b74cd4 Models shouldn't be hidden by default, as many models have objects that are obviously not hidden but do not have FLGS chunks + objects with children can be hidden, only older versions of Blender automatically hide children when parent is hidden 2022-10-08 21:31:08 -04:00
Will Snyder
7b9f5c9cfb Prune segments with empty triangle strips. ZE and most versions of ZETools require triangle strips. 2022-10-06 10:23:13 -07:00
William Herald Snyder
84a910f747 When exporting collision primitives, check the swbf_msh_coll_prim primitive property before the name to catch primitives which have incorrect names. Previously the property would only be checked if the primitive was missing a name indicator, but it should be checked first and always because some primitives from XSI have name-type mismatch (cis_hover_aat) 2022-10-05 09:28:27 -04:00
William Herald Snyder
0a1866295c Only include vertex groups used for skinning purposes. More robust requirements for object to be exported as a skin. Misc cleanup and minor refactoring in msh_model_gather. 2022-10-05 09:27:44 -04:00
William Herald Snyder
ec54df21d2 Blend + hardedged transparency emulated via settings + additive emulated by adding the output of the PBSDF and a default Transparent BSDF. Bugfix: animation data is cleared from nodetrees via a stable method. FCurves.clear() is too new... 2022-10-04 09:58:36 -04:00
William Herald Snyder
b2bd9c8316 Blender 2.80-2.83 supports additive blending, so FillMaterialProps will catch that if present 2022-10-04 07:54:25 -04:00
William Herald Snyder
56f6ce6940 Fill doublesidedness and transparency (TODO: figure out additive mapping/emulation) from material settings 2022-10-03 23:06:33 -04:00
William Herald Snyder
f44c7bfdf3 DFS property fill starts from BSDF Principled Base Color input and runs until a texture image is hit. The extension of the texture image's source path is always replaced with .tga. Operator skips previously handled materials. 2022-10-03 22:56:45 -04:00
William Herald Snyder
8bf2196991 Don't change rendertype on matfill and fill texture_0 as well as diffuse map 2022-10-03 21:16:59 -04:00
William Herald Snyder
1252a6d192 Grayscale bumpmaps support via option files 2022-10-03 14:12:21 -04:00
William Herald Snyder
9fcdb3dfb7 Quick and dirty option file parser 2022-10-03 14:11:44 -04:00
William Herald Snyder
dd17fe902e Normal mapping support, though bump mapping is not supported yet. To support it, .tga.option files will have to be read and the various bump mapping munge parameters will need proper interpretation. Emulating the bump effect via shader nodes is straightforward. 2022-10-03 12:30:34 -04:00
William Herald Snyder
226682de8b Data field 0 overrunning bounds of UI_MATERIAL_ANIMATION_LENGTHS bugfix 2022-10-03 10:28:53 -04:00
William Herald Snyder
69e959e7a3 Proper scroll speed units + scene fps accounted for 2022-10-03 08:38:13 -04:00
William Herald Snyder
188b270ad1 Linear interpolation for scrolling keyframes. Keyframe are created on the active action of the materials node tree. TODO: determine correct scroll speed speed units 2022-10-02 18:19:29 -04:00
William Herald Snyder
9a344d0652 Simple UV scrolling nodes in place. Simple approach will probably remain since the only interaction will be with image textures nodes 2022-10-02 16:30:11 -04:00
William Herald Snyder
ea82d24356 Glow flag supported via mixing emission and BSDF. TODO: poll GT on automatically setting renderer to Eevee, find out if both emission and BSDF are necessary or just the latter when glow is enabled, find out if rendertype 1 is relevant, outline general implementation of props to nodes mapping procedure 2022-10-02 15:55:47 -04:00
William Herald Snyder
6e05bba9e5 Fix import order bug in materials_to_blend that invalidated script reloading 2022-10-02 13:31:16 -04:00
William Herald Snyder
9981b64d60 Missing armature bug fix 2022-10-01 15:17:02 -04:00
William Herald Snyder
48aabaf8d4 Anim import code moved to new file 2022-10-01 15:10:00 -04:00
William Herald Snyder
329303e256 Silly _REVERSE_RENDERTYPES_MAPPING replaced with dict comprehension 2022-10-01 12:17:26 -04:00
William Herald Snyder
4970f974fd Generate Nodes operator docs subsection 2022-10-01 08:02:59 -04:00
William Herald Snyder
d359975bd6 Generation op will abort if diffuse texture isn't found + options for failing silently or with error message. TODO: found out how to create a pop-up panel with error message that fades away if not interacted with for a couple seconds... 2022-10-01 08:02:59 -04:00
William Herald Snyder
45782f9a2f Docs update for bones with geometry 2022-10-01 07:56:26 -04:00
William Herald Snyder
aa8a61175d Skeleton import code moved to separate file + other renaming 2022-10-01 07:56:19 -04:00
William Herald Snyder
5692a60907 When exporting, if an object has a parent bone with the same name, add its geometry to the parent bone's model and discard the object itself. 2022-10-01 07:53:28 -04:00
William Herald Snyder
7cfa101d42 Catch models that are marked as bones but still have geometry 2022-10-01 07:53:28 -04:00
William Herald Snyder
43b1f9650e When gathering bone transforms during armature expansion, don't assume armature's local transform is zeroed 2022-10-01 07:53:28 -04:00
William Herald Snyder
aa8f05dd42 Create mesh object for any model with geometry regardless of type + triangle strip winding order bugfix 2022-10-01 07:52:19 -04:00
William Herald Snyder
9bd8479e31 Reorg + use generation operator when initially creating Blender materials + make all texture fields in SWBF materials properties paths 2022-09-28 08:49:14 -04:00
William Herald Snyder
8974131550 Better description for material generation op 2022-09-28 08:49:14 -04:00
William Herald Snyder
abd727d39a Better material generation for basic rendertypes 2022-09-28 08:48:50 -04:00
William Herald Snyder
f69ed3f143 Armature offset bug fix 2022-09-27 08:23:49 -04:00
William Herald Snyder
f88b62c986 When exporting cylinder primitives, do not test for exact match in x,y dimensions since cylinders created in xsi often have so few polys that their widths and lengths don't match (e.g. p_collision of tat3_bldg_reservoir). Taking the larger dimension gives the correct result for cylinders that originated in both XSI and Blender. 2022-09-27 07:56:25 -04:00
William Herald Snyder
13a92e46c6 Always calculate and use split normals when exporting mesh in order to account for sharp edges 2022-09-23 13:11:00 -04:00
William Herald Snyder
d6973e9793 Catch absent geometry in objects not of type EMPTY 2022-09-22 19:31:30 -04:00
William Herald Snyder
5d86a88411 Normals importing as part of custom split set. Mesh import code moved to seperate file 2022-09-21 09:02:15 -04:00
Will
fe1e16a117 Image sizing fix for material fill operator docs 2022-05-05 11:45:51 -04:00
William Herald Snyder
4e2f6bf423 Docs update for msh material property fill operator and menu entry 2022-02-09 15:17:52 -05:00
William Herald Snyder
bdcd4c4aa9 Integrated Starlord's feedback (quick-n-dirty object context submenu + operator for copying diffuse texture names to SWBF material properties for all selected objects' materials) 2022-02-09 14:50:52 -05:00
William Herald Snyder
c3f5f0bed3 Integrated Teancum's feedback (Normal rendertype changed to Standard + armature root bone properly parented to skin if one is present + animation export options simplified with batch export capability and relevant docs changes + minor changes to import/export options and docs to make behavior obvious) 2022-01-28 17:28:12 -05:00
William Herald Snyder
091e295649 Triangle winding order for strips accounted for 2022-01-27 22:09:23 -05:00
William Herald Snyder
40343a2f69 The last index of the last triangle strip in a segment is properly included + cleaner reading of STRP 2022-01-27 18:34:53 -05:00
William Herald Snyder
a962b4475e Change Blender version to 2.80 to silence warning 2022-01-21 13:26:07 -05:00
William Herald Snyder
637c3c2afa Catch segments with no valid geometry, read NDXL in try-except block 2022-01-20 23:40:19 -05:00
23 changed files with 1540 additions and 805 deletions

View File

@@ -1,8 +1,8 @@
bl_info = {
'name': 'SWBF .msh Import-Export',
'author': 'Will Snyder, SleepKiller',
"version": (1, 0, 0),
'blender': (3, 0, 0),
'author': 'Will Snyder, PrismaticFlower',
"version": (1, 3, 3),
'blender': (4, 3, 2),
'location': 'File > Import-Export',
'description': 'Export as SWBF .msh file',
'warning': '',
@@ -13,9 +13,9 @@ bl_info = {
}
# Taken from glTF-Blender-IO, because I do not understand Python that well
# (this is the first thing of substance I've created in it) and just wanted
# (this is the first thing of substance I've created in it) and just wanted
# script reloading to work.
#
#
# https://github.com/KhronosGroup/glTF-Blender-IO
#
# Copyright 2018-2019 The glTF-Blender-IO authors.
@@ -55,14 +55,16 @@ if "bpy" in locals():
import bpy
from bpy_extras.io_utils import ExportHelper, ImportHelper
from bpy.props import BoolProperty, EnumProperty, CollectionProperty
from bpy.types import Operator
from .msh_scene_utilities import create_scene
from bpy.types import Operator, Menu
from .msh_scene_utilities import create_scene, set_scene_animation
from .msh_scene_save import save_scene
from .msh_scene_read import read_scene
from .msh_material_properties import *
from .msh_skeleton_properties import *
from .msh_collision_prim_properties import *
from .msh_to_blend import *
from .msh_material_operators import *
from .msh_scene_to_blend import *
from .msh_anim_to_blend import *
from .zaa_to_blend import *
@@ -103,32 +105,52 @@ class ExportMSH(Operator, ExportHelper):
default=True
)
export_with_animation: BoolProperty(
name="Export With Animation",
description="Includes animation data extracted from the action currently set on armature.",
default=False
)
export_as_skeleton: BoolProperty(
name="Export Objects As Skeleton",
description="Check if you intend to export skeleton data for consumption by ZenAsset.",
default=False
)
animation_export: EnumProperty(name="Export Animation(s)",
description="If/how animation data should be exported.",
items=(
('NONE', "None", "Do not include animation data in the export."),
('ACTIVE', "Active", "Export animation extracted from the scene's Armature's active Action."),
('BATCH', "Batch", "Export a separate animation file for each Action in the scene.")
),
default='NONE')
def execute(self, context):
with open(self.filepath, 'wb') as output_file:
save_scene(
output_file=output_file,
scene=create_scene(
generate_triangle_strips=self.generate_triangle_strips,
apply_modifiers=self.apply_modifiers,
export_target=self.export_target,
skel_only=self.export_as_skeleton,
export_anim=self.export_with_animation
),
)
if 'SELECTED' in self.export_target and len(bpy.context.selected_objects) == 0:
raise Exception("{} was chosen, but you have not selected any objects. "
" Don't forget to unhide all the objects you wish to select!".format(self.export_target))
scene, armature_obj = create_scene(
generate_triangle_strips=self.generate_triangle_strips,
apply_modifiers=self.apply_modifiers,
export_target=self.export_target,
skel_only=self.animation_export != 'NONE') # Exclude geometry data (except root stuff) if we're doing anims
if self.animation_export != 'NONE' and not armature_obj:
raise Exception("Could not find an armature object from which to export animations!")
def write_scene_to_file(filepath : str, scene_to_write : Scene):
with open(filepath, 'wb') as output_file:
save_scene(output_file=output_file, scene=scene_to_write)
if self.animation_export == 'ACTIVE':
set_scene_animation(scene, armature_obj)
write_scene_to_file(self.filepath, scene)
elif self.animation_export == 'BATCH':
export_dir = self.filepath if os.path.isdir(self.filepath) else os.path.dirname(self.filepath)
for action in bpy.data.actions:
anim_save_path = os.path.join(export_dir, action.name + ".msh")
armature_obj.animation_data.action = action
set_scene_animation(scene, armature_obj)
write_scene_to_file(anim_save_path, scene)
else:
write_scene_to_file(self.filepath, scene)
return {'FINISHED'}
@@ -139,17 +161,15 @@ def menu_func_export(self, context):
class ImportMSH(Operator, ImportHelper):
""" Import an SWBF .msh file. """
""" Import SWBF .msh file(s). """
bl_idname = "swbf_msh.import"
bl_label = "Import SWBF .msh File"
bl_label = "Import SWBF .msh File(s)"
filename_ext = ".msh"
files: CollectionProperty(
name="File Path",
name="File Path(s)",
type=bpy.types.OperatorFileListElement,
)
@@ -160,8 +180,8 @@ class ImportMSH(Operator, ImportHelper):
)
animation_only: BoolProperty(
name="Import Animation Only",
description="Import animation and append as a new action to currently selected armature.",
name="Import Animation(s)",
description="Import one or more animations from the selected files and append each as a new Action to currently selected Armature.",
default=False
)
@@ -173,13 +193,13 @@ class ImportMSH(Operator, ImportHelper):
if filepath.endswith(".zaabin") or filepath.endswith(".zaa"):
extract_and_apply_munged_anim(filepath)
else:
with open(filepath, 'rb') as input_file:
with open(filepath, 'rb') as input_file:
scene = read_scene(input_file, self.animation_only)
if not self.animation_only:
extract_scene(filepath, scene)
else:
extract_and_apply_anim(filepath, scene)
if not self.animation_only:
extract_scene(filepath, scene)
else:
extract_and_apply_anim(filepath, scene)
return {'FINISHED'}
@@ -208,6 +228,12 @@ def register():
bpy.types.Material.swbf_msh_mat = bpy.props.PointerProperty(type=MaterialProperties)
bpy.types.Armature.swbf_msh_skel = bpy.props.CollectionProperty(type=SkeletonProperties)
bpy.utils.register_class(FillSWBFMaterialProperties)
bpy.utils.register_class(VIEW3D_MT_SWBF)
bpy.types.VIEW3D_MT_object_context_menu.append(draw_matfill_menu)
bpy.utils.register_class(GenerateMaterialNodesFromSWBFProperties)
def unregister():
@@ -225,6 +251,14 @@ def unregister():
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
bpy.utils.unregister_class(FillSWBFMaterialProperties)
bpy.utils.unregister_class(VIEW3D_MT_SWBF)
bpy.types.VIEW3D_MT_object_context_menu.remove(draw_matfill_menu)
bpy.utils.unregister_class(GenerateMaterialNodesFromSWBFProperties)
if __name__ == "__main__":
register()
register()

View File

@@ -138,6 +138,9 @@ class Reader:
def how_much_left(self, pos):
return self.end_pos - pos
def bytes_remaining(self):
return self.end_pos - self.file.tell()
def skip_until(self, header):
while (self.could_have_child() and header not in self.peak_next_header()):
self.skip_bytes(1)

View File

@@ -39,7 +39,7 @@ def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation:
# If it doesn't have a preserved skeleton, then we add the scene root.
# If it does have a preserved skeleton, any objects not animatable by blender (i.e. objects above the skeleton, scene root)
# will be included in the preserved skeleton
if not has_preserved_skeleton(armature):
if len(armature.data.swbf_msh_skel):
keyable_bones.add(root_name)
# Subset of above bones to key with dummy frames (all bones not in armature)
@@ -67,7 +67,7 @@ def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation:
for frame in range(num_frames):
frame_time = framerange.x + frame * increment
frame_time = int(framerange.x + frame * increment)
bpy.context.scene.frame_set(frame_time)
for keyable_bone in keyable_bones:

View File

@@ -0,0 +1,110 @@
""" Gathers the Blender objects from the current scene and returns them as a list of
Model objects. """
import bpy
import bmesh
import math
from enum import Enum
from typing import List, Set, Dict, Tuple
from .msh_scene import Scene
from .msh_material_to_blend import *
from .msh_model import *
from .msh_skeleton_utilities import *
from .msh_skeleton_to_blend import *
from .msh_model_gather import get_is_model_hidden
from .msh_mesh_to_blend import model_to_mesh_object
from .crc import *
import os
# Extracts and applies anims in the scene to the currently selected armature
def extract_and_apply_anim(filename : str, scene : Scene):
arma = bpy.context.view_layer.objects.active
if not arma or arma.type != 'ARMATURE':
raise Exception("Select an armature to attach the imported animation to!")
if scene.animation is None:
raise Exception("No animation found in msh file!")
else:
head, tail = os.path.split(filename)
anim_name = tail.split(".")[0]
if anim_name in bpy.data.actions:
bpy.data.actions.remove(bpy.data.actions[anim_name], do_unlink=True)
for nt in arma.animation_data.nla_tracks:
if anim_name == nt.strips[0].name:
arma.animation_data.nla_tracks.remove(nt)
action = bpy.data.actions.new(anim_name)
action.use_fake_user = True
if not arma.animation_data:
arma.animation_data_create()
# Record the starting transforms of each bone. Pose space is relative
# to bones starting transforms. Starting = in edit mode
bone_bind_poses = {}
bpy.context.view_layer.objects.active = arma
bpy.ops.object.mode_set(mode='EDIT')
for edit_bone in arma.data.edit_bones:
if edit_bone.parent:
bone_local = edit_bone.parent.matrix.inverted() @ edit_bone.matrix
else:
bone_local = arma.matrix_local @ edit_bone.matrix
bone_bind_poses[edit_bone.name] = bone_local.inverted()
bpy.ops.object.mode_set(mode='OBJECT')
for bone in arma.pose.bones:
if to_crc(bone.name) in scene.animation.bone_frames:
bind_mat = bone_bind_poses[bone.name]
translation_frames, rotation_frames = scene.animation.bone_frames[to_crc(bone.name)]
loc_data_path = "pose.bones[\"{}\"].location".format(bone.name)
rot_data_path = "pose.bones[\"{}\"].rotation_quaternion".format(bone.name)
fcurve_rot_w = action.fcurves.new(rot_data_path, index=0, action_group=bone.name)
fcurve_rot_x = action.fcurves.new(rot_data_path, index=1, action_group=bone.name)
fcurve_rot_y = action.fcurves.new(rot_data_path, index=2, action_group=bone.name)
fcurve_rot_z = action.fcurves.new(rot_data_path, index=3, action_group=bone.name)
for frame in rotation_frames:
i = frame.index
q = (bind_mat @ convert_rotation_space(frame.rotation).to_matrix().to_4x4()).to_quaternion()
fcurve_rot_w.keyframe_points.insert(i,q.w)
fcurve_rot_x.keyframe_points.insert(i,q.x)
fcurve_rot_y.keyframe_points.insert(i,q.y)
fcurve_rot_z.keyframe_points.insert(i,q.z)
fcurve_loc_x = action.fcurves.new(loc_data_path, index=0, action_group=bone.name)
fcurve_loc_y = action.fcurves.new(loc_data_path, index=1, action_group=bone.name)
fcurve_loc_z = action.fcurves.new(loc_data_path, index=2, action_group=bone.name)
for frame in translation_frames:
i = frame.index
t = (bind_mat @ Matrix.Translation(convert_vector_space(frame.translation))).translation
fcurve_loc_x.keyframe_points.insert(i,t.x)
fcurve_loc_y.keyframe_points.insert(i,t.y)
fcurve_loc_z.keyframe_points.insert(i,t.z)
arma.animation_data.action = action
track = arma.animation_data.nla_tracks.new()
track.strips.new(action.name, int(action.frame_range[0]), action)

View File

@@ -7,6 +7,8 @@ from .msh_material import *
from .msh_material_utilities import _RENDERTYPES_MAPPING
import os
def gather_materials() -> Dict[str, Material]:
""" Gathers the Blender materials and returns them as
a dictionary of strings and Material objects. """
@@ -35,16 +37,16 @@ def read_material(blender_material: bpy.types.Material) -> Material:
result.data = _read_material_props_data(props)
if "UNSUPPORTED" not in props.rendertype:
result.texture0 = props.diffuse_map
result.texture0 = os.path.basename(props.diffuse_map)
result.texture1 = _read_normal_map_or_distortion_map_texture(props)
result.texture2 = _read_detail_texture(props)
result.texture3 = _read_envmap_texture(props)
else:
result.texture0 = props.texture_0
result.texture1 = props.texture_1
result.texture2 = props.texture_2
result.texture3 = props.texture_3
result.texture0 = os.path.basename(props.texture_0)
result.texture1 = os.path.basename(props.texture_1)
result.texture2 = os.path.basename(props.texture_2)
result.texture3 = os.path.basename(props.texture_3)
return result
@@ -96,11 +98,13 @@ def _read_material_props_data(props) -> Tuple[int, int]:
return (props.detail_map_tiling_u, props.detail_map_tiling_v)
def _read_normal_map_or_distortion_map_texture(props) -> str:
if "REFRACTION" in props.rendertype:
return props.distortion_map
return os.path.basename(props.distortion_map)
if "NORMALMAPPED" in props.rendertype:
return props.normal_map
return os.path.basename(props.normal_map)
return ""
@@ -108,10 +112,10 @@ def _read_detail_texture(props) -> str:
if "REFRACTION" in props.rendertype:
return ""
return props.detail_map
return os.path.basename(props.detail_map)
def _read_envmap_texture(props) -> str:
if "ENVMAPPED" not in props.rendertype:
return ""
return props.environment_map
return os.path.basename(props.environment_map)

View File

@@ -0,0 +1,324 @@
""" Operators for basic emulation and mapping of SWBF material system in Blender.
Only relevant if the builtin Eevee renderer is being used! """
import bpy
from .msh_material_properties import *
from math import sqrt
from bpy.props import BoolProperty, EnumProperty, StringProperty
from bpy.types import Operator, Menu
from .option_file_parser import MungeOptions
import os
# FillSWBFMaterialProperties
# Iterates through all material slots of all selected
# objects and fills basic SWBF material properties
# from any Principled BSDF nodes it finds.
class FillSWBFMaterialProperties(bpy.types.Operator):
bl_idname = "swbf_msh.fill_mat_props"
bl_label = "Fill SWBF Material Properties"
bl_description = ("Fill in SWBF properties of all materials used by selected objects.\n"
"Only considers materials that use nodes.\n"
"Please see 'Materials > Materials Operators' in the docs for more details.")
def execute(self, context):
slots = sum([list(ob.material_slots) for ob in bpy.context.selected_objects if ob.type == 'MESH'],[])
mats = [slot.material for slot in slots if (slot.material and slot.material.node_tree)]
mats_visited = set()
for mat in mats:
if mat.name in mats_visited or not mat.swbf_msh_mat:
continue
else:
mats_visited.add(mat.name)
mat.swbf_msh_mat.doublesided = not mat.use_backface_culling
mat.swbf_msh_mat.hardedged_transparency = (mat.blend_method == "CLIP")
mat.swbf_msh_mat.blended_transparency = (mat.blend_method == "BLEND")
mat.swbf_msh_mat.additive_transparency = (mat.blend_method == "ADDITIVE")
# Below is all for filling the diffuse map/texture_0 fields
try:
for BSDF_node in [n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED']:
base_col = BSDF_node.inputs['Base Color']
stack = []
texture_node = None
current_socket = base_col
if base_col.is_linked:
stack.append(base_col.links[0].from_node)
while stack:
curr_node = stack.pop()
if curr_node.type == 'TEX_IMAGE':
texture_node = curr_node
break
else:
# Crude but good for now
next_nodes = []
for node_input in curr_node.inputs:
for link in node_input.links:
next_nodes.append(link.from_node)
# reversing it so we go from up to down
stack += reversed(next_nodes)
if texture_node is not None:
tex_path = texture_node.image.filepath
tex_name = os.path.basename(tex_path)
i = tex_name.find('.')
# Get rid of trailing number in case one is present
if i > 0:
tex_name = tex_name[0:i] + ".tga"
refined_tex_path = os.path.join(os.path.dirname(tex_path), tex_name)
mat.swbf_msh_mat.diffuse_map = refined_tex_path
mat.swbf_msh_mat.texture_0 = refined_tex_path
break
except:
# Many chances for null ref exceptions. None if user reads doc section...
pass
return {'FINISHED'}
class VIEW3D_MT_SWBF(bpy.types.Menu):
bl_label = "SWBF"
def draw(self, _context):
layout = self.layout
layout.operator("swbf_msh.fill_mat_props", text="Fill SWBF Material Properties")
def draw_matfill_menu(self, context):
layout = self.layout
layout.separator()
layout.menu("VIEW3D_MT_SWBF")
# GenerateMaterialNodesFromSWBFProperties
# Creates shader nodes to emulate SWBF material properties.
# Will probably only support for a narrow subset of properties...
# So much fun to write this, will probably do all render types by end of October
class GenerateMaterialNodesFromSWBFProperties(bpy.types.Operator):
bl_idname = "swbf_msh.generate_material_nodes"
bl_label = "Generate Nodes"
bl_description= """Generate Cycles shader nodes from SWBF material properties.
The nodes generated are meant to give one a general idea
of how the material would look ingame. They cannot
to provide an exact emulation"""
material_name: StringProperty(
name = "Material Name",
description = "Name of material whose SWBF properties the generated nodes will emulate."
)
fail_silently: BoolProperty(
name = "Fail Silently"
)
def execute(self, context):
material = bpy.data.materials.get(self.material_name, None)
if not material or not material.swbf_msh_mat:
return {'CANCELLED'}
mat_props = material.swbf_msh_mat
texture_input_nodes = []
surface_output_nodes = []
# Op will give up if no diffuse map is present.
# Eventually more nuance will be added for different
# rtypes
diffuse_texture_path = mat_props.diffuse_map
if diffuse_texture_path and os.path.exists(diffuse_texture_path):
material.use_nodes = True
material.node_tree.nodes.clear()
bsdf = material.node_tree.nodes.new("ShaderNodeBsdfPrincipled")
texImage = material.node_tree.nodes.new('ShaderNodeTexImage')
texImage.image = bpy.data.images.load(diffuse_texture_path)
texImage.image.alpha_mode = 'CHANNEL_PACKED'
material.node_tree.links.new(bsdf.inputs['Base Color'], texImage.outputs['Color'])
texture_input_nodes.append(texImage)
specular_key = "Specular" if bpy.app.version < (4, 0, 0) else "Specular IOR Level"
bsdf.inputs["Roughness"].default_value = 1.0
bsdf.inputs[specular_key].default_value = 0.0
material.use_backface_culling = not bool(mat_props.doublesided)
surface_output_nodes.append(('BSDF', bsdf))
if not mat_props.glow:
if mat_props.hardedged_transparency:
material.blend_method = "CLIP"
material.node_tree.links.new(bsdf.inputs['Alpha'], texImage.outputs['Alpha'])
elif mat_props.blended_transparency:
material.blend_method = "BLEND"
material.node_tree.links.new(bsdf.inputs['Alpha'], texImage.outputs['Alpha'])
elif mat_props.additive_transparency:
# most complex
transparent_bsdf = material.node_tree.nodes.new("ShaderNodeBsdfTransparent")
add_shader = material.node_tree.nodes.new("ShaderNodeAddShader")
material.node_tree.links.new(add_shader.inputs[0], bsdf.outputs["BSDF"])
material.node_tree.links.new(add_shader.inputs[1], transparent_bsdf.outputs["BSDF"])
surface_output_nodes[0] = ('Shader', add_shader)
# Glow (adds another shader output)
else:
emission = material.node_tree.nodes.new("ShaderNodeEmission")
material.node_tree.links.new(emission.inputs['Color'], texImage.outputs['Color'])
emission_strength_multiplier = material.node_tree.nodes.new("ShaderNodeMath")
emission_strength_multiplier.operation = 'MULTIPLY'
emission_strength_multiplier.inputs[1].default_value = 32.0
material.node_tree.links.new(emission_strength_multiplier.inputs[0], texImage.outputs['Alpha'])
material.node_tree.links.new(emission.inputs['Strength'], emission_strength_multiplier.outputs[0])
surface_output_nodes.append(("Emission", emission))
surfaces_output = None
if (len(surface_output_nodes) == 1):
surfaces_output = surface_output_nodes[0][1]
else:
mix = material.node_tree.nodes.new("ShaderNodeMixShader")
material.node_tree.links.new(mix.inputs[1], surface_output_nodes[0][1].outputs[0])
material.node_tree.links.new(mix.inputs[2], surface_output_nodes[1][1].outputs[0])
surfaces_output = mix
# Normal/bump mapping (needs more rendertype support!)
if "NORMALMAP" in mat_props.rendertype and mat_props.normal_map and os.path.exists(mat_props.normal_map):
normalMapTexImage = material.node_tree.nodes.new('ShaderNodeTexImage')
normalMapTexImage.image = bpy.data.images.load(mat_props.normal_map)
normalMapTexImage.image.alpha_mode = 'CHANNEL_PACKED'
normalMapTexImage.image.colorspace_settings.name = 'Non-Color'
texture_input_nodes.append(normalMapTexImage)
options = MungeOptions(mat_props.normal_map + ".option")
if options.get_bool("bumpmap"):
# First we must convert the RGB data to brightness
rgb_to_bw_node = material.node_tree.nodes.new("ShaderNodeRGBToBW")
material.node_tree.links.new(rgb_to_bw_node.inputs["Color"], normalMapTexImage.outputs["Color"])
# Now create a bump map node (perhaps we could also use this with normals and just plug color into normal input?)
bumpMapNode = material.node_tree.nodes.new('ShaderNodeBump')
bumpMapNode.inputs["Distance"].default_value = options.get_float("bumpscale", default=1.0)
material.node_tree.links.new(bumpMapNode.inputs["Height"], rgb_to_bw_node.outputs["Val"])
normalsOutputNode = bumpMapNode
else:
normalMapNode = material.node_tree.nodes.new('ShaderNodeNormalMap')
material.node_tree.links.new(normalMapNode.inputs["Color"], normalMapTexImage.outputs["Color"])
normalsOutputNode = normalMapNode
material.node_tree.links.new(bsdf.inputs['Normal'], normalsOutputNode.outputs["Normal"])
output = material.node_tree.nodes.new("ShaderNodeOutputMaterial")
material.node_tree.links.new(output.inputs['Surface'], surfaces_output.outputs[0])
# Scrolling
# This approach works 90% of the time, but notably produces very incorrect results
# on mus1_bldg_world_1,2,3
# Clear all anims in all cases
if material.node_tree.animation_data:
material.node_tree.animation_data_clear()
if "SCROLL" in mat_props.rendertype:
uv_input = material.node_tree.nodes.new("ShaderNodeUVMap")
vector_add = material.node_tree.nodes.new("ShaderNodeVectorMath")
# Add keyframes
scroll_per_sec_divisor = 255.0
frame_step = 60.0
fps = bpy.context.scene.render.fps
for i in range(2):
vector_add.inputs[1].default_value[0] = i * mat_props.scroll_speed_u * frame_step / scroll_per_sec_divisor
vector_add.inputs[1].keyframe_insert("default_value", index=0, frame=i * frame_step * fps)
vector_add.inputs[1].default_value[1] = i * mat_props.scroll_speed_v * frame_step / scroll_per_sec_divisor
vector_add.inputs[1].keyframe_insert("default_value", index=1, frame=i * frame_step * fps)
material.node_tree.links.new(vector_add.inputs[0], uv_input.outputs[0])
for texture_node in texture_input_nodes:
material.node_tree.links.new(texture_node.inputs["Vector"], vector_add.outputs[0])
# Don't know how to set interpolation when adding keyframes
# so we must do it after the fact
if material.node_tree.animation_data and material.node_tree.animation_data.action:
for fcurve in material.node_tree.animation_data.action.fcurves:
for kf in fcurve.keyframe_points.values():
kf.interpolation = 'LINEAR'
'''
else:
# Todo: figure out some way to raise an error but continue operator execution...
if self.fail_silently:
return {'CANCELLED'}
else:
raise RuntimeError(f"Diffuse texture at path: '{diffuse_texture_path}' was not found.")
'''
return {'FINISHED'}

View File

@@ -9,8 +9,12 @@ from .msh_material_ui_strings import *
from .msh_material_utilities import _REVERSE_RENDERTYPES_MAPPING
from .msh_material_operators import GenerateMaterialNodesFromSWBFProperties
UI_MATERIAL_RENDERTYPES = (
('NORMAL_BF2', "00 Normal (SWBF2)", UI_RENDERTYPE_NORMAL_BF2_DESC),
('NORMAL_BF2', "00 Standard (SWBF2)", UI_RENDERTYPE_NORMAL_BF2_DESC),
('SCROLLING_BF2', "03 Scrolling (SWBF2)", UI_RENDERTYPE_SCROLLING_BF2_DESC),
('ENVMAPPED_BF2', "06 Envmapped (SWBF2)", UI_RENDERTYPE_ENVMAPPED_BF2_DESC),
('ANIMATED_BF2', "07 Animated (SWBF2)", UI_RENDERTYPE_ANIMATED_BF2_DESC),
@@ -167,26 +171,31 @@ class MaterialProperties(PropertyGroup):
description="The basic diffuse map for the material. The alpha channel "
"is either the Transparency Map, Glow Map or Gloss Map, "
"depending on the selected rendertype and flags.",
default="white.tga")
default="white.tga",
subtype='FILE_PATH')
detail_map: StringProperty(name="Detail Map",
description="Detail maps allow you to add in 'detail' to the diffuse "
"map at runtime. Or they can be used as fake ambient occlusion "
"maps or even wacky emissive maps. See docs for more details.")
"maps or even wacky emissive maps. See docs for more details.",
subtype='FILE_PATH')
normal_map: StringProperty(name="Normal Map",
description="Normal maps can provide added detail from lighting. "
"If Specular is enabled the alpha channel will be "
"the Gloss Map.")
"the Gloss Map.",
subtype='FILE_PATH')
environment_map: StringProperty(name="Environment Map",
description="Environment map for the material. Provides static "
"reflections around the surface. Must be a cubemap.")
"reflections around the surface. Must be a cubemap.",
subtype='FILE_PATH')
distortion_map: StringProperty(name="Distortion Map",
description="Distortion maps control how Refractive materials "
"distort the scene behind them. Should be a normal map "
"with '-forceformat v8u8' in it's '.tga.option' file.")
"with '-forceformat v8u8' in it's '.tga.option' file.",
subtype='FILE_PATH')
# Below props are for yet unsupported render types
data_value_0: IntProperty(name="", description="First data value")
@@ -194,10 +203,10 @@ class MaterialProperties(PropertyGroup):
rendertype_value: IntProperty(name="Rendertype Value", description="Raw number value of rendertype.", min=0, max=31)
texture_0: StringProperty(name="1", description="First texture slot")
texture_1: StringProperty(name="2", description="Second texture slot")
texture_2: StringProperty(name="3", description="Third texture slot")
texture_3: StringProperty(name="4", description="Fourth texture slot")
texture_0: StringProperty(name="1", description="First texture slot", subtype='FILE_PATH', default="white.tga")
texture_1: StringProperty(name="2", description="Second texture slot", subtype='FILE_PATH')
texture_2: StringProperty(name="3", description="Third texture slot", subtype='FILE_PATH')
texture_3: StringProperty(name="4", description="Fourth texture slot", subtype='FILE_PATH')
class MaterialPropertiesPanel(bpy.types.Panel):
@@ -285,3 +294,8 @@ class MaterialPropertiesPanel(bpy.types.Panel):
layout.prop(material_props, "texture_2")
layout.prop(material_props, "texture_3")
op_props = layout.operator("swbf_msh.generate_material_nodes", text="Generate Nodes")
op_props.material_name = context.material.name
op_props.fail_silently = False

View File

@@ -2,9 +2,9 @@
import bpy
from typing import Dict
from .msh_material import *
from .msh_material_gather import *
from .msh_material_properties import *
from .msh_material import *
from .msh_material_utilities import _REVERSE_RENDERTYPES_MAPPING
@@ -14,6 +14,8 @@ import os
def find_texture_path(folder_path : str, name : str) -> str:
if not folder_path or not name:
@@ -30,11 +32,23 @@ def find_texture_path(folder_path : str, name : str) -> str:
if os.path.exists(possible_path):
return possible_path
return ""
return name
def fill_material_props(material : Material, material_properties):
def swbf_material_to_blend(material_name : str, material : Material, folder_path : str) -> bpy.types.Material:
new_mat = bpy.data.materials.new(name=material_name)
fill_material_props(material, new_mat.swbf_msh_mat, folder_path)
bpy.ops.swbf_msh.generate_material_nodes('EXEC_DEFAULT', material_name=new_mat.name, fail_silently=True)
return new_mat
def fill_material_props(material, material_properties, folder_path):
""" Fills MaterialProperties from Material instance """
if material_properties is None or material is None:
@@ -47,7 +61,7 @@ def fill_material_props(material : Material, material_properties):
_fill_material_props_rendertype(material, material_properties)
_fill_material_props_flags(material, material_properties)
_fill_material_props_data(material, material_properties)
_fill_material_props_texture_maps(material, material_properties)
_fill_material_props_texture_maps(material, material_properties, folder_path)
@@ -92,7 +106,7 @@ def _fill_material_props_data(material, material_properties):
anim_length_index = int(sqrt(material.data[0]))
if anim_length_index < 0:
anim_length_index = 0
elif anim_length_index > len(UI_MATERIAL_ANIMATION_LENGTHS):
elif anim_length_index >= len(UI_MATERIAL_ANIMATION_LENGTHS):
anim_length_index = len(UI_MATERIAL_ANIMATION_LENGTHS) - 1
material_properties.animation_length = UI_MATERIAL_ANIMATION_LENGTHS[anim_length_index][0]
@@ -102,15 +116,22 @@ def _fill_material_props_data(material, material_properties):
material_properties.detail_map_tiling_v = material.data[1]
def _fill_material_props_texture_maps(material, material_properties):
def _fill_material_props_texture_maps(material, material_properties, folder_path):
t0path = find_texture_path(folder_path, material.texture0)
t1path = find_texture_path(folder_path, material.texture1)
t2path = find_texture_path(folder_path, material.texture2)
t3path = find_texture_path(folder_path, material.texture3)
material_properties.texture_0 = t0path
material_properties.texture_1 = t1path
material_properties.texture_2 = t2path
material_properties.texture_3 = t3path
material_properties.diffuse_map = t0path
material_properties.distortion_map = t1path
material_properties.normal_map = t1path
material_properties.detail_map = t2path
material_properties.environment_map = t3path
material_properties.texture_0 = material.texture0
material_properties.texture_1 = material.texture1
material_properties.texture_2 = material.texture2
material_properties.texture_3 = material.texture3
material_properties.diffuse_map = material.texture0
material_properties.distortion_map = material.texture1
material_properties.normal_map = material.texture1
material_properties.detail_map = material.texture2
material_properties.environment_map = material.texture3

View File

@@ -18,19 +18,7 @@ _RENDERTYPES_MAPPING = {
"NORMALMAPPED_TILED_ENVMAPPED_BF2": Rendertype.NORMALMAPPED_TILED_ENVMAP}
_REVERSE_RENDERTYPES_MAPPING = {
Rendertype.NORMAL : "NORMAL_BF2",
Rendertype.SCROLLING : "SCROLLING_BF2",
Rendertype.ENVMAPPED : "ENVMAPPED_BF2",
Rendertype.ANIMATED : "ANIMATED_BF2",
Rendertype.REFRACTION : "REFRACTION_BF2",
Rendertype.BLINK : "BLINK_BF2",
Rendertype.NORMALMAPPED_TILED : "NORMALMAPPED_TILED_BF2",
Rendertype.NORMALMAPPED_ENVMAPPED : "NORMALMAPPED_ENVMAPPED_BF2",
Rendertype.NORMALMAPPED : "NORMALMAPPED_BF2",
Rendertype.NORMALMAPPED_TILED_ENVMAP : "NORMALMAPPED_TILED_ENVMAPPED_BF2"}
_REVERSE_RENDERTYPES_MAPPING = {val: key for (key, val) in _RENDERTYPES_MAPPING.items()}
def remove_unused_materials(materials: Dict[str, Material],

View File

@@ -0,0 +1,198 @@
""" Converts msh meshes to Blender counterparts """
import bpy
import bmesh
import math
from enum import Enum
from typing import List, Set, Dict, Tuple
from .msh_scene import Scene
from .msh_material_to_blend import *
from .msh_model import *
from .msh_skeleton_utilities import *
from .msh_model_gather import get_is_model_hidden
from .crc import *
import os
def validate_segment_geometry(segment : GeometrySegment):
if not segment.positions:
return False
if not segment.triangles and not segment.triangle_strips and not segment.polygons:
return False
if not segment.material_name:
return False
if not segment.normals:
return False
return True
def model_to_mesh_object(model: Model, scene : Scene, materials_map : Dict[str, bpy.types.Material]) -> bpy.types.Object:
blender_mesh = bpy.data.meshes.new(model.name)
# Per vertex data which will eventually be remapped to loops
vertex_positions = []
vertex_uvs = []
vertex_normals = []
vertex_colors = []
# Keeps track of which vertices each group of weights affects
# i.e. maps offset of vertices -> weights that affect them
vertex_weights_offsets = {}
# Since polygons in a msh segment index into the segment's verts,
# we must keep an offset to index them into the verts of the whole mesh
polygon_index_offset = 0
# List of tuples of face indices
polygons = []
# Each polygon has an index into the mesh's material list
current_material_index = 0
polygon_material_indices = []
if model.geometry:
geometry_has_colors = any(segment.colors for segment in model.geometry)
for segment in model.geometry:
if not validate_segment_geometry(segment):
continue
blender_mesh.materials.append(materials_map[segment.material_name])
vertex_positions += [tuple(convert_vector_space(p)) for p in segment.positions]
if segment.texcoords:
vertex_uvs += [tuple(texcoord) for texcoord in segment.texcoords]
else:
vertex_uvs += [(0.0,0.0) for _ in range(len(segment.positions))]
if segment.normals:
vertex_normals += [tuple(convert_vector_space(n)) for n in segment.normals]
if segment.colors:
vertex_colors.extend(segment.colors)
elif geometry_has_colors:
[vertex_colors.extend([0.0, 0.0, 0.0, 1.0]) for _ in range(len(segment.positions))]
if segment.weights:
vertex_weights_offsets[polygon_index_offset] = segment.weights
segment_polygons = []
if segment.triangles:
segment_polygons = [tuple([ind + polygon_index_offset for ind in tri]) for tri in segment.triangles]
elif segment.triangle_strips:
winding = [0,1,2]
rwinding = [1,0,2]
for strip in segment.triangle_strips:
for i in range(len(strip) - 2):
strip_tri = tuple([polygon_index_offset + strip[i+j] for j in (winding if i % 2 == 0 else rwinding)])
segment_polygons.append(strip_tri)
elif segment.polygons:
segment_polygons = [tuple([ind + polygon_index_offset for ind in polygon]) for polygon in segment.polygons]
polygon_index_offset += len(segment.positions)
polygons += segment_polygons
polygon_material_indices += [current_material_index for _ in segment_polygons]
current_material_index += 1
'''
Start building the blender mesh
'''
# VERTICES
# This is all we have to do for vertices, other attributes are done per-loop
blender_mesh.vertices.add(len(vertex_positions))
blender_mesh.vertices.foreach_set("co", [component for vertex_position in vertex_positions for component in vertex_position])
# LOOPS
flat_indices = [index for polygon in polygons for index in polygon]
blender_mesh.loops.add(len(flat_indices))
# Position indices
blender_mesh.loops.foreach_set("vertex_index", flat_indices)
# Normals
blender_mesh.loops.foreach_set("normal", [component for i in flat_indices for component in vertex_normals[i]])
# UVs
blender_mesh.uv_layers.new(do_init=False)
blender_mesh.uv_layers[0].data.foreach_set("uv", [component for i in flat_indices for component in vertex_uvs[i]])
# Colors
if geometry_has_colors:
blender_mesh.color_attributes.new("COLOR0", "FLOAT_COLOR", "POINT")
blender_mesh.color_attributes[0].data.foreach_set("color", vertex_colors)
# POLYGONS/FACES
blender_mesh.polygons.add(len(polygons))
# Indices of starting loop for each polygon
polygon_loop_start_indices = []
current_polygon_start_index = 0
# Number of loops in this polygon. Polygon i will use
# loops from polygon_loop_start_indices[i] to
# polygon_loop_start_indices[i] + polygon_loop_totals[i]
polygon_loop_totals = []
for polygon in polygons:
polygon_loop_start_indices.append(current_polygon_start_index)
current_polygon_length = len(polygon)
current_polygon_start_index += current_polygon_length
polygon_loop_totals.append(current_polygon_length)
blender_mesh.polygons.foreach_set("loop_start", polygon_loop_start_indices)
blender_mesh.polygons.foreach_set("loop_total", polygon_loop_totals)
blender_mesh.polygons.foreach_set("material_index", polygon_material_indices)
blender_mesh.polygons.foreach_set("use_smooth", [True for _ in polygons])
blender_mesh.validate(clean_customdata=False)
blender_mesh.update()
# Reset custom normals after calling update/validate
reset_normals = [0.0] * (len(blender_mesh.loops) * 3)
blender_mesh.loops.foreach_get("normal", reset_normals)
blender_mesh.normals_split_custom_set(tuple(zip(*(iter(reset_normals),) * 3)))
blender_mesh_object = bpy.data.objects.new(model.name, blender_mesh)
# VERTEX GROUPS
vertex_groups_indicies = {}
for offset in vertex_weights_offsets:
for i, weight_set in enumerate(vertex_weights_offsets[offset]):
for weight in weight_set:
index = weight.bone
if index not in vertex_groups_indicies:
model_name = scene.models[index].name
vertex_groups_indicies[index] = blender_mesh_object.vertex_groups.new(name=model_name)
vertex_groups_indicies[index].add([offset + i], weight.weight, 'ADD')
return blender_mesh_object

View File

@@ -73,7 +73,7 @@ class Model:
name: str = "Model"
parent: str = ""
model_type: ModelType = ModelType.NULL
hidden: bool = True
hidden: bool = False
transform: ModelTransform = field(default_factory=ModelTransform)

View File

@@ -24,55 +24,106 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool
models_list: List[Model] = []
# Composite bones are bones which have geometry.
# If a child object has the same name, it will take said child's geometry.
# Pure bones are just bones and after all objects are explored the only
# entries remaining in this dict will be bones without geometry.
pure_bones_from_armature = {}
armature_found = None
for uneval_obj in select_objects(export_target):
if uneval_obj.type in SKIPPED_OBJECT_TYPES and uneval_obj.name not in parents:
continue
# Non-bone objects that will be exported
blender_objects_to_export = []
if apply_modifiers:
obj = uneval_obj.evaluated_get(depsgraph)
# This must be seperate from the list above,
# since exported objects will contain Blender objects as well as bones
# Here we just keep track of all names, regardless of origin
exported_object_names: Set[str] = set()
# Me must keep track of hidden objects separately because
# evaluated_get clears hidden status
blender_objects_to_hide: Set[str] = set()
# Armature must be processed before everything else!
# In this loop we also build a set of names of all objects
# that will be exported. This is necessary so we can prune vertex
# groups that do not reference exported objects in the main
# model building loop below this one.
for uneval_obj in select_objects(export_target):
if get_is_model_hidden(uneval_obj):
blender_objects_to_hide.add(uneval_obj.name)
if uneval_obj.type == "ARMATURE" and not armature_found:
# Keep track of the armature, we don't want to process > 1!
armature_found = uneval_obj.evaluated_get(depsgraph) if apply_modifiers else uneval_obj
# Get all bones in a separate list. While we iterate through
# objects we removed bones with geometry from this dict. After iteration
# is done, we add the remaining bones to the models from exported
# scene objects.
pure_bones_from_armature = expand_armature(armature_found)
# All bones to set
exported_object_names.update(pure_bones_from_armature.keys())
elif not (uneval_obj.type in SKIPPED_OBJECT_TYPES and uneval_obj.name not in parents):
exported_object_names.add(uneval_obj.name)
blender_objects_to_export.append(uneval_obj)
else:
obj = uneval_obj
pass
for uneval_obj in blender_objects_to_export:
obj = uneval_obj.evaluated_get(depsgraph) if apply_modifiers else uneval_obj
check_for_bad_lod_suffix(obj)
if obj.type == "ARMATURE":
models_list += expand_armature(obj)
armature_found = obj
continue
model = Model()
model.name = obj.name
model.model_type = get_model_type(obj, skeleton_only)
model.hidden = get_is_model_hidden(obj)
transform = obj.matrix_local
if obj.parent_bone:
model.parent = obj.parent_bone
# matrix_local, when called on an armature child also parented to a bone, appears to be broken.
# At the very least, the results contradict the docs...
armature_relative_transform = obj.parent.matrix_world.inverted() @ obj.matrix_world
transform = obj.parent.data.bones[obj.parent_bone].matrix_local.inverted() @ armature_relative_transform
# Test for a mesh object that should be a BONE on export.
# If so, we inject geometry into the BONE while not modifying it's transform/name
# and remove it from the set of BONES without geometry (pure).
if obj.name in pure_bones_from_armature:
model = pure_bones_from_armature.pop(obj.name)
else:
if obj.parent is not None:
if obj.parent.type == "ARMATURE":
model.parent = obj.parent.parent.name if obj.parent.parent else ""
transform = obj.parent.matrix_local @ transform
else:
model.parent = obj.parent.name
model = Model()
model.name = obj.name
model.model_type = ModelType.NULL if skeleton_only else get_model_type(obj, armature_found)
local_translation, local_rotation, _ = transform.decompose()
model.transform.rotation = convert_rotation_space(local_rotation)
model.transform.translation = convert_vector_space(local_translation)
transform = obj.matrix_local
if obj.type in MESH_OBJECT_TYPES:
if obj.parent_bone:
model.parent = obj.parent_bone
# matrix_local, when called on an armature child also parented to a bone, appears to be broken.
# At the very least, the results contradict the docs...
armature_relative_transform = obj.parent.matrix_world.inverted() @ obj.matrix_world
transform = obj.parent.data.bones[obj.parent_bone].matrix_local.inverted() @ armature_relative_transform
else:
if obj.parent is not None:
if obj.parent.type == "ARMATURE":
model.parent = obj.parent.parent.name if obj.parent.parent else ""
transform = obj.parent.matrix_local @ transform
else:
model.parent = obj.parent.name
local_translation, local_rotation, _ = transform.decompose()
model.transform.rotation = convert_rotation_space(local_rotation)
model.transform.translation = convert_vector_space(local_translation)
if obj.type in MESH_OBJECT_TYPES and not skeleton_only:
# Vertex groups are often used for purposes other than skinning.
# Here we gather all vgroups and select the ones that reference
# objects included in the export.
valid_vgroup_indices : Set[int] = set()
if model.model_type == ModelType.SKIN:
valid_vgroups = [group for group in obj.vertex_groups if group.name in exported_object_names]
valid_vgroup_indices = { group.index for group in valid_vgroups }
model.bone_map = [ group.name for group in valid_vgroups ]
mesh = obj.to_mesh()
model.geometry = create_mesh_geometry(mesh, obj.vertex_groups)
model.geometry = create_mesh_geometry(mesh, valid_vgroup_indices)
obj.to_mesh_clear()
@@ -85,18 +136,17 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool
raise RuntimeError(f"Object '{obj.name}' has resulted in a .msh geometry segment that has "
f"more than {MAX_MSH_VERTEX_COUNT} vertices! Split the object's mesh up "
f"and try again!")
if obj.vertex_groups:
model.bone_map = [group.name for group in obj.vertex_groups]
if get_is_collision_primitive(obj):
model.collisionprimitive = get_collision_primitive(obj)
model.hidden = model.name in blender_objects_to_hide
models_list.append(model)
return (models_list, armature_found)
# We removed all composite bones after looking through the objects,
# so the bones left are all pure and we add them all here.
return (models_list + list(pure_bones_from_armature.values()), armature_found)
@@ -112,12 +162,12 @@ def create_parents_set() -> Set[str]:
return parents
def create_mesh_geometry(mesh: bpy.types.Mesh, has_weights: bool) -> List[GeometrySegment]:
def create_mesh_geometry(mesh: bpy.types.Mesh, valid_vgroup_indices: Set[int]) -> List[GeometrySegment]:
""" Creates a list of GeometrySegment objects from a Blender mesh.
Does NOT create triangle strips in the GeometrySegment however. """
if mesh.has_custom_normals:
mesh.calc_normals_split()
# We have to do this for all meshes to account for sharp edges
mesh.calc_normals_split()
mesh.validate_material_indices()
mesh.calc_loop_triangles()
@@ -129,18 +179,18 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, has_weights: bool) -> List[Geomet
vertex_remap: List[Dict[Tuple[int, int], int]] = [dict() for i in range(material_count)]
polygons: List[Set[int]] = [set() for i in range(material_count)]
if mesh.vertex_colors.active is not None:
if mesh.color_attributes.active_color is not None:
for segment in segments:
segment.colors = []
if has_weights:
if valid_vgroup_indices:
for segment in segments:
segment.weights = []
for segment, material in zip(segments, mesh.materials):
segment.material_name = material.name
def add_vertex(material_index: int, vertex_index: int, loop_index: int, use_smooth_normal: bool, face_normal: Vector) -> int:
def add_vertex(material_index: int, vertex_index: int, loop_index: int) -> int:
nonlocal segments, vertex_remap
vertex_cache_miss_index = -1
@@ -148,15 +198,8 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, has_weights: bool) -> List[Geomet
cache = vertex_cache[material_index]
remap = vertex_remap[material_index]
vertex_normal: Vector
if use_smooth_normal or mesh.use_auto_smooth:
if mesh.has_custom_normals:
vertex_normal = Vector( mesh.loops[loop_index].normal )
else:
vertex_normal = Vector( mesh.vertices[vertex_index].normal )
else:
vertex_normal = Vector(face_normal)
# always use loop normals since we always calculate a custom split set
vertex_normal = Vector( mesh.loops[loop_index].normal )
def get_cache_vertex():
yield mesh.vertices[vertex_index].co.x
@@ -172,13 +215,17 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, has_weights: bool) -> List[Geomet
yield mesh.uv_layers.active.data[loop_index].uv.y
if segment.colors is not None:
for v in mesh.vertex_colors.active.data[loop_index].color:
active_color = mesh.color_attributes.active_color
data_index = loop_index if active_color.domain == "CORNER" else vertex_index
for v in mesh.color_attributes.active_color.data[data_index].color:
yield v
if segment.weights is not None:
for v in mesh.vertices[vertex_index].groups:
yield v.group
yield v.weight
if v.group in valid_vgroup_indices:
yield v.group
yield v.weight
vertex_cache_entry = tuple(get_cache_vertex())
cached_vertex_index = cache.get(vertex_cache_entry, vertex_cache_miss_index)
@@ -201,21 +248,23 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, has_weights: bool) -> List[Geomet
segment.texcoords.append(mesh.uv_layers.active.data[loop_index].uv.copy())
if segment.colors is not None:
segment.colors.append(list(mesh.vertex_colors.active.data[loop_index].color))
active_color = mesh.color_attributes.active_color
data_index = loop_index if active_color.domain == "CORNER" else vertex_index
segment.colors.append(list(active_color.data[data_index].color))
if segment.weights is not None:
groups = mesh.vertices[vertex_index].groups
segment.weights.append([VertexWeight(v.weight, v.group) for v in groups])
segment.weights.append([VertexWeight(v.weight, v.group) for v in groups if v.group in valid_vgroup_indices])
return new_index
for tri in mesh.loop_triangles:
polygons[tri.material_index].add(tri.polygon_index)
segments[tri.material_index].triangles.append([
add_vertex(tri.material_index, tri.vertices[0], tri.loops[0], tri.use_smooth, tri.normal),
add_vertex(tri.material_index, tri.vertices[1], tri.loops[1], tri.use_smooth, tri.normal),
add_vertex(tri.material_index, tri.vertices[2], tri.loops[2], tri.use_smooth, tri.normal)])
add_vertex(tri.material_index, tri.vertices[0], tri.loops[0]),
add_vertex(tri.material_index, tri.vertices[1], tri.loops[1]),
add_vertex(tri.material_index, tri.vertices[2], tri.loops[2])])
for segment, remap, polys in zip(segments, vertex_remap, polygons):
for poly_index in polys:
@@ -225,12 +274,29 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, has_weights: bool) -> List[Geomet
return segments
def get_model_type(obj: bpy.types.Object, skel_only: bool) -> ModelType:
def get_model_type(obj: bpy.types.Object, armature_found: bpy.types.Object) -> ModelType:
""" Get the ModelType for a Blender object. """
if obj.type in MESH_OBJECT_TYPES and not skel_only:
if obj.vertex_groups:
return ModelType.SKIN
if obj.type in MESH_OBJECT_TYPES:
# Objects can have vgroups for non-skinning purposes.
# If we can find one vgroup that shares a name with a bone in the
# armature, we know the vgroup is for weighting purposes and thus
# the object is a skin. Otherwise, interpret it as a static mesh.
# We must also check that an armature included in the export
# and that it is the same one this potential skin is weighting to.
# If we failed to do this, a user could export a selected object
# that is a skin, but the weight data in the export would reference
# nonexistent models!
if (obj.vertex_groups and armature_found and
obj.parent and obj.parent.name == armature_found.name):
for vgroup in obj.vertex_groups:
if vgroup.name in armature_found.data.bones:
return ModelType.SKIN
return ModelType.STATIC
else:
return ModelType.STATIC
@@ -239,6 +305,9 @@ def get_model_type(obj: bpy.types.Object, skel_only: bool) -> ModelType:
def get_is_model_hidden(obj: bpy.types.Object) -> bool:
""" Gets if a Blender object should be marked as hidden in the .msh file. """
if obj.hide_get():
return True
name = obj.name.lower()
if name.startswith("c_"):
@@ -287,10 +356,7 @@ def get_collision_primitive(obj: bpy.types.Object) -> CollisionPrimitive:
primitive.radius = max(obj.dimensions[0], obj.dimensions[1], obj.dimensions[2]) * 0.5
elif primitive.shape == CollisionPrimitiveShape.CYLINDER:
if not math.isclose(obj.dimensions[0], obj.dimensions[1], rel_tol=0.001):
raise RuntimeError(f"Object '{obj.name}' is being used as a cylinder collision "
f"primitive but it's X and Y dimensions are not uniform!")
primitive.radius = obj.dimensions[0] * 0.5
primitive.radius = max(obj.dimensions[0], obj.dimensions[1]) * 0.5
primitive.height = obj.dimensions[2]
elif primitive.shape == CollisionPrimitiveShape.BOX:
primitive.radius = obj.dimensions[0] * 0.5
@@ -306,6 +372,14 @@ def get_collision_primitive_shape(obj: bpy.types.Object) -> CollisionPrimitiveSh
""" Gets the CollisionPrimitiveShape of an object or raises an error if
it can't. """
# arc170 fighter has examples of box colliders without proper naming
# and cis_hover_aat has a cylinder which is named p_vehiclesphere.
# To export these properly we must check the collision_prim property
# that was assigned on import BEFORE looking at the name.
prim_type = obj.swbf_msh_coll_prim.prim_type
if prim_type in [item.value for item in CollisionPrimitiveShape]:
return CollisionPrimitiveShape(prim_type)
name = obj.name.lower()
if "sphere" in name or "sphr" in name or "spr" in name:
@@ -315,11 +389,6 @@ def get_collision_primitive_shape(obj: bpy.types.Object) -> CollisionPrimitiveSh
if "box" in name or "cube" in name or "cuboid" in name:
return CollisionPrimitiveShape.BOX
# arc170 fighter has examples of box colliders without proper naming
prim_type = obj.swbf_msh_coll_prim.prim_type
if prim_type in [item.value for item in CollisionPrimitiveShape]:
return CollisionPrimitiveShape(prim_type)
raise RuntimeError(f"Object '{obj.name}' has no primitive type specified in it's name!")
@@ -381,11 +450,17 @@ def select_objects(export_target: str) -> List[bpy.types.Object]:
def expand_armature(armature: bpy.types.Object) -> List[Model]:
def expand_armature(armature: bpy.types.Object) -> Dict[str, Model]:
proper_BONES = get_real_BONES(armature)
bones: List[Model] = []
bones: Dict[str, Model] = {}
for bone in armature.data.bones:
model = Model()
@@ -399,21 +474,36 @@ def expand_armature(armature: bpy.types.Object) -> List[Model]:
# set model parent to SKIN object if there is one
# set model parent to armature parent if there is one
else:
for child_obj in armature.children:
bone_world_matrix = get_bone_world_matrix(armature, bone.name)
parent_obj = None
for child_obj in armature.original.children:
if child_obj.vertex_groups and not get_is_model_hidden(child_obj) and not child_obj.parent_bone:
model.parent = child_obj.name
#model.parent = child_obj.name
parent_obj = child_obj
break
if not model.parent and armature.parent:
if parent_obj:
transform = parent_obj.matrix_world.inverted() @ bone_world_matrix
model.parent = parent_obj.name
elif not parent_obj and armature.parent:
transform = armature.parent.matrix_world.inverted() @ bone_world_matrix
model.parent = armature.parent.name
else:
transform = bone_world_matrix
model.parent = ""
local_translation, local_rotation, _ = transform.decompose()
model.model_type = ModelType.BONE if bone.name in proper_BONES else ModelType.NULL
model.name = bone.name
model.hidden = True
model.transform.rotation = convert_rotation_space(local_rotation)
model.transform.translation = convert_vector_space(local_translation)
bones.append(model)
bones[bone.name] = model
return bones

View File

@@ -8,6 +8,25 @@ import math
from mathutils import Vector, Matrix
# Convert model with geometry to null.
# Currently not used, but could be necessary in the future.
def make_null(model : Model):
model.model_type = ModelType.NULL
bone_map = None
geometry = None
# I think this is all we need to check for to avoid
# common ZE/ZETools crashes...
def validate_geometry_segment(segment : GeometrySegment) -> bool:
if not segment.positions or not segment.triangle_strips:
return False
else:
return True
def inject_dummy_data(model : Model):
""" Adds a triangle and material to the model (scene root). Needed to export zenasst-compatible skeletons. """
model.hidden = True

View File

@@ -165,19 +165,23 @@ def _read_matd(matd: Reader) -> Material:
elif next_header == "TX0D":
with matd.read_child() as tx0d:
mat.texture0 = tx0d.read_string()
if tx0d.bytes_remaining() > 0:
mat.texture0 = tx0d.read_string()
elif next_header == "TX1D":
with matd.read_child() as tx1d:
mat.texture1 = tx1d.read_string()
if tx1d.bytes_remaining() > 0:
mat.texture1 = tx1d.read_string()
elif next_header == "TX2D":
with matd.read_child() as tx2d:
mat.texture2 = tx2d.read_string()
if tx2d.bytes_remaining() > 0:
mat.texture2 = tx2d.read_string()
elif next_header == "TX3D":
with matd.read_child() as tx3d:
mat.texture3 = tx3d.read_string()
if tx3d.bytes_remaining() > 0:
mat.texture3 = tx3d.read_string()
else:
matd.skip_bytes(1)
@@ -203,7 +207,9 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model:
global model_counter
global mndx_remap
mndx_remap[index] = model_counter
if index not in mndx_remap:
mndx_remap[index] = model_counter
model_counter += 1
@@ -323,18 +329,23 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment:
for _ in range(num_texcoords):
geometry_seg.texcoords.append(Vector(uv0l.read_f32(2)))
# TODO: Can't remember exact issue here...
elif next_header == "NDXL":
with segm.read_child() as ndxl:
pass
'''
num_polygons = ndxl.read_u32()
for _ in range(num_polygons):
polygon = ndxl.read_u16(ndxl.read_u16())
geometry_seg.polygons.append(polygon)
'''
# TODO: Can't remember exact issue here, but this chunk sometimes fails
elif next_header == "NDXL":
with segm.read_child() as ndxl:
try:
num_polygons = ndxl.read_u32()
for _ in range(num_polygons):
num_inds = ndxl.read_u16()
polygon = ndxl.read_u16(num_inds)
geometry_seg.polygons.append(polygon)
except:
print("Failed to read polygon list!")
geometry_seg.polygons = []
elif next_header == "NDXT":
with segm.read_child() as ndxt:
@@ -343,46 +354,37 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment:
for _ in range(num_tris):
geometry_seg.triangles.append(ndxt.read_u16(3))
# There could be major issues with this, so far it hasn't failed but its inelegance irks me
# Try catch for safety's sake
elif next_header == "STRP":
strips : List[List[int]] = []
with segm.read_child() as strp:
num_indicies = strp.read_u32()
num_indicies_read = 0
try:
num_indicies = strp.read_u32()
curr_strip = []
previous_flag = False
indices = strp.read_u16(num_indicies)
if num_indicies > 0:
index, index1 = strp.read_u16(2)
curr_strip = [index & 0x7fff, index1 & 0x7fff]
num_indicies_read += 2
strip_indices = []
for i in range(num_indicies - 2):
index = strp.read_u16(1)
for i in range(num_indicies - 1):
if indices[i] & 0x8000 > 0 and indices[i+1] & 0x8000 > 0:
strip_indices.append(i)
if index & 0x8000 > 0:
index = index & 0x7fff
strip_indices.append(num_indicies)
if previous_flag:
previous_flag = False
curr_strip.append(index)
strips.append(curr_strip[:-2])
curr_strip = curr_strip[-2:]
continue
else:
previous_flag = True
for i in range(len(strip_indices) - 1):
start = strip_indices[i]
end = strip_indices[i+1]
else:
previous_flag = False
curr_strip.append(index)
strips.append(list([indices[start] & 0x7fff, indices[start+1] & 0x7fff]) + list(indices[start+2 : end]))
except:
print("Failed to read triangle strips")
geometry_seg.triangle_strips = []
geometry_seg.triangle_strips = strips
# TODO: Dont know how to handle trailing 0 bug yet: https://schlechtwetterfront.github.io/ze_filetypes/msh.html#STRP
# TODO: Dont know if/how to handle trailing 0 bug yet: https://schlechtwetterfront.github.io/ze_filetypes/msh.html#STRP
#if segm.read_u16 != 0:
# segm.skip_bytes(-2)

View File

@@ -0,0 +1,199 @@
""" Gathers the Blender objects from the current scene and returns them as a list of
Model objects. """
import bpy
import bmesh
import math
from enum import Enum
from typing import List, Set, Dict, Tuple
from .msh_scene import Scene
from .msh_material_to_blend import *
from .msh_model import *
from .msh_skeleton_utilities import *
from .msh_skeleton_to_blend import *
from .msh_model_gather import get_is_model_hidden
from .msh_mesh_to_blend import model_to_mesh_object
from .crc import *
import os
# Create the msh hierachy. Armatures are not created here.
def extract_models(scene: Scene, materials_map : Dict[str, bpy.types.Material]) -> Dict[str, bpy.types.Object]:
# This will be filled with model names -> Blender objects and returned
model_map : Dict[str, bpy.types.Object] = {}
sorted_models : List[Model] = sort_by_parent(scene.models)
for model in sorted_models:
new_obj = None
if model.geometry:
new_obj = model_to_mesh_object(model, scene, materials_map)
else:
new_obj = bpy.data.objects.new(model.name, None)
new_obj.empty_display_size = 1
new_obj.empty_display_type = 'PLAIN_AXES'
model_map[model.name] = new_obj
new_obj.name = model.name
if model.parent:
new_obj.parent = model_map[model.parent]
new_obj.location = convert_vector_space(model.transform.translation)
new_obj.rotation_mode = "QUATERNION"
new_obj.rotation_quaternion = convert_rotation_space(model.transform.rotation)
if model.collisionprimitive is not None:
new_obj.swbf_msh_coll_prim.prim_type = model.collisionprimitive.shape.value
bpy.context.collection.objects.link(new_obj)
return model_map
# TODO: Add to custom material info struct, maybe some material conversion/import?
def extract_materials(folder_path: str, scene: Scene) -> Dict[str, bpy.types.Material]:
extracted_materials : Dict[str, bpy.types.Material] = {}
for material_name, material in scene.materials.items():
extracted_materials[material_name] = swbf_material_to_blend(material_name, material, folder_path)
return extracted_materials
def extract_scene(filepath: str, scene: Scene):
folder = os.path.join(os.path.dirname(filepath),"")
# material_map mapes Material names to Blender materials
material_map = extract_materials(folder, scene)
# model_map maps Model names to Blender objects.
model_map = extract_models(scene, material_map)
# skel contains all models needed in an armature
skel = extract_required_skeleton(scene)
# Create the armature if skel is non-empty
armature = None if not skel else required_skeleton_to_armature(skel, model_map, scene)
if armature is not None:
preserved_skel = armature.data.swbf_msh_skel
for model in scene.models:
if to_crc(model.name) in scene.skeleton or model.model_type == ModelType.BONE:
entry = preserved_skel.add()
entry.name = model.name
'''
If an armature was created, we need to do a few extra
things to ensure the import makes sense in Blender. It can
get a bit messy, as XSI + SWBF have very loose requirements
when it comes to skin-skeleton parentage.
If not, we're good.
'''
if armature is not None:
has_skin = False
# Handle armature related parenting
for curr_model in scene.models:
curr_obj = model_map[curr_model.name]
# Parent all skins to armature
if curr_model.model_type == ModelType.SKIN:
has_skin = True
worldmat = curr_obj.matrix_world
curr_obj.parent = armature
curr_obj.parent_type = 'ARMATURE'
curr_obj.matrix_world = worldmat
# Parent the object to a bone if necessary
else:
parent_bone_name = ""
if curr_model.name in armature.data.bones and curr_model.geometry:
parent_bone_name = curr_model.name
elif curr_model.parent in armature.data.bones and curr_model.name not in armature.data.bones:
parent_bone_name = curr_model.parent
if parent_bone_name:
# Not sure what the different mats do, but saving the worldmat and
# applying it after clearing the other mats yields correct results...
worldmat = curr_obj.matrix_world
curr_obj.parent = armature
curr_obj.parent_type = 'BONE'
curr_obj.parent_bone = parent_bone_name
# ''
curr_obj.matrix_basis = Matrix()
curr_obj.matrix_parent_inverse = Matrix()
curr_obj.matrix_world = worldmat
'''
Sometimes skins are parented to other skins. We need to find the skin highest in the hierarchy and
parent all skins to its parent (armature_reparent_obj).
If not skin exists, we just reparent the armature to the parent of the highest node in the skeleton
'''
armature_reparent_obj = None
if has_skin:
for model in sort_by_parent(scene.models):
if model.model_type == ModelType.SKIN:
armature_reparent_obj = None if not model.parent else model_map[model.parent]
else:
skeleton_parent_name = skel[0].parent
for model in scene.models:
if model.name == skeleton_parent_name:
armature_reparent_obj = None if not skeleton_parent_name else model_map[skeleton_parent_name]
# Now we reparent the armature to the node (armature_reparent_obj) we just found
if armature_reparent_obj is not None and armature.name != armature_reparent_obj.name:
world_tx = armature.matrix_world
armature.parent = armature_reparent_obj
armature.matrix_basis = Matrix()
armature.matrix_parent_inverse = Matrix()
armature.matrix_world = Matrix.Identity(4)
# If an bone exists in the armature, delete its
# object counterpart (as created in extract_models)
for bone in skel:
model_to_remove = model_map[bone.name]
if model_to_remove and model_to_remove.parent_bone == "":
bpy.data.objects.remove(model_to_remove, do_unlink=True)
model_map.pop(bone.name)
armature.matrix_world = Matrix.Identity(4)
# Lastly, hide all that is hidden in the msh scene
for model in scene.models:
if model.name in model_map:
obj = model_map[model.name]
obj.hide_set(model.hidden or get_is_model_hidden(obj))

View File

@@ -9,7 +9,7 @@ from mathutils import Vector
from .msh_model import Model, Animation, ModelType
from .msh_scene import Scene, SceneAABB
from .msh_model_gather import gather_models
from .msh_model_utilities import sort_by_parent, has_multiple_root_models, reparent_model_roots, get_model_world_matrix, inject_dummy_data
from .msh_model_utilities import make_null, validate_geometry_segment, sort_by_parent, has_multiple_root_models, reparent_model_roots, get_model_world_matrix, inject_dummy_data
from .msh_model_triangle_strips import create_models_triangle_strips
from .msh_material import *
from .msh_material_gather import gather_materials
@@ -19,7 +19,21 @@ from .msh_anim_gather import extract_anim
def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_target: str, skel_only: bool, export_anim: bool) -> Scene:
def set_scene_animation(scene : Scene, armature_obj : bpy.types.Object):
if not scene or not armature_obj:
return
root = scene.models[0]
scene.animation = extract_anim(armature_obj, root.name)
def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_target: str, skel_only: bool) -> Tuple[Scene, bpy.types.Object]:
""" Create a msh Scene from the active Blender scene. """
scene = Scene()
@@ -39,6 +53,20 @@ def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_t
for segment in model.geometry:
segment.triangle_strips = segment.triangles
# After generating triangle strips we must prune any segments that don't have
# them, or else ZE and most versions of ZETools will crash.
# We could also make models with no valid segments nulls, since they might as well be,
# but that could have unforseeable consequences further down the modding pipeline
# and is not necessary to avoid the aforementioned crashes...
for model in scene.models:
if model.geometry is not None:
# Doing this in msh_model_gather would be messy and the presence/absence
# of triangle strips is required for a validity check.
model.geometry = [segment for segment in model.geometry if validate_geometry_segment(segment)]
#if not model.geometry:
# make_null(model)
if has_multiple_root_models(scene.models):
scene.models = reparent_model_roots(scene.models)
@@ -47,17 +75,11 @@ def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_t
root = scene.models[0]
if export_anim:
if armature_obj is not None:
scene.animation = extract_anim(armature_obj, root.name)
else:
raise Exception("Export Error: Could not find an armature object from which to export an animation!")
if skel_only and root.model_type == ModelType.NULL:
if skel_only and (root.model_type == ModelType.NULL or root.model_type == ModelType.BONE):
# For ZenAsset
inject_dummy_data(root)
return scene
return scene, armature_obj
def create_scene_aabb(scene: Scene) -> SceneAABB:

View File

@@ -0,0 +1,178 @@
""" SWBF skeleton-armature mapping functions. By skeleton, we simply
mean models that will end up in an armature. Literal SWBF skeletons (zafbins)
are not relevant as of now. """
import bpy
import math
from typing import List, Set, Dict, Tuple
from .msh_scene import Scene
from .msh_model import *
from .msh_model_utilities import *
from .crc import *
'''
Creates armature from the required nodes.
Assumes the required_skeleton is already sorted by parent.
Uses model_map to get the world matrix of each bone (hacky, see NOTE)
'''
def required_skeleton_to_armature(required_skeleton : List[Model], model_map : Dict[str, bpy.types.Object], msh_scene : Scene) -> bpy.types.Object:
armature = bpy.data.armatures.new("skeleton")
armature_obj = bpy.data.objects.new("skeleton", armature)
armature_obj.matrix_world = Matrix.Identity(4)
bpy.context.view_layer.active_layer_collection.collection.objects.link(armature_obj)
bones_set = set([model.name for model in required_skeleton])
armature_obj.select_set(True)
bpy.context.view_layer.objects.active = armature_obj
bpy.ops.object.mode_set(mode='EDIT')
for bone in required_skeleton:
edit_bone = armature.edit_bones.new(bone.name)
if bone.parent and bone.parent in bones_set:
edit_bone.parent = armature.edit_bones[bone.parent]
'''
NOTE: I recall there being some rare issue with the get_world_matrix utility func.
Never bothered to figure it out and referencing the bone object's world mat always works.
Bone objects will be deleted later.
'''
bone_obj = model_map[bone.name]
# TODO: This will lead to mistranslated bones when armature is reparented!
edit_bone.matrix = bone_obj.matrix_world
edit_bone.tail = bone_obj.matrix_world @ Vector((0.0,1.0,0.0))
bone_children = [b for b in get_model_children(bone, required_skeleton)]
'''
Perhaps we'll add an option for importing bones tip-to-tail, but that would
require preserving their original transforms as changing the tail position
changes the bones' transform...
'''
tail_pos = Vector()
if bone_children:
for bone_child in bone_children:
tail_pos += bone_obj.matrix_world.translation
tail_pos = tail_pos / len(bone_children)
edit_bone.length = .5 #(tail_pos - edit_bone.head).magnitude
else:
bone_length = .5# edit_bone.parent.length if edit_bone.parent is not None else .5
edit_bone.tail = bone_obj.matrix_world @ Vector((0.0,bone_length,0.0))
bpy.ops.object.mode_set(mode='OBJECT')
armature_obj.select_set(True)
bpy.context.view_layer.update()
return armature_obj
'''
Ok, so this method is crucial. What this does is:
1) Find all nodes that are weighted to by skinned segments.
2) A node must be included in the armature if it:
- is in SKL2 and is not the scene root
- has model_type == BONE
- is weighted to
- has a parent and child that must be in the armature
This may need a lot of adjustments, don't think I can prove it's validity but it has worked very well
and handles all stock + ZETools + Pandemic XSI exporter models I've tested
'''
def extract_required_skeleton(scene: Scene) -> List[Model]:
# Will map Model names to Models in scene, for convenience
model_dict : Dict[str, Model] = {}
'''
Will contain hashes of all models that definitely need to be in the skeleton/armature.
We initialize it with the contents of SKL2 i.e. the nodes that are animated.
For now this includes the scene root, but that'll be excluded later.
'''
skeleton_hashes = set(scene.skeleton)
'''
We also need to add all nodes that are weighted to. These are not necessarily in
SKL2, as SKL2 seems to only reference nodes that are keyframed.
However, sometimes SKL2 is not included when it should be, but it can be mostly recovered
by checking which models are BONEs.
'''
for model in scene.models:
model_dict[model.name] = model
if model.model_type == ModelType.BONE:
skeleton_hashes.add(to_crc(model.name))
elif model.geometry:
for seg in model.geometry:
if seg.weights:
for weight_set in seg.weights:
for weight in weight_set:
model_weighted_to = scene.models[weight.bone]
if to_crc(model_weighted_to.name) not in skeleton_hashes:
skeleton_hashes.add(to_crc(model_weighted_to.name))
# The result of this function (to be sorted by parent)
required_skeleton_models = []
# Set of nodes to be included in required skeleton/were visited
visited_nodes = set()
'''
Here we add all skeleton nodes (except root) and any necessary ancestors to the armature.
- e.g. in bone_x/eff_x/eff_y, the effectors do not have to be in armature, as they are not ancestors of a bone
- but in bone_x/eff_x/eff_y/bone_y, they do.
'''
for bone in sort_by_parent(scene.models):
# make sure we exclude the scene root and any nodes irrelevant to the armature
if not bone.parent or to_crc(bone.name) not in skeleton_hashes:
continue
potential_bones = [bone]
visited_nodes.add(bone.name)
# Stacked transform will be needed if we decide to include an option for excluding effectors/roots or
# connecting bones tip-to-tail
#stacked_transform = model_transform_to_matrix(bone.transform)
curr_ancestor = model_dict[bone.parent]
while True:
# If we hit a non-skin scene root, that means we just add the bone we started with, no ancestors.
if not curr_ancestor.parent and curr_ancestor.model_type != ModelType.SKIN:
required_skeleton_models.append(bone)
visited_nodes.add(bone.name)
break
# If we encounter another bone, a skin, or a previously visited object, we need to add the bone and its
# ancestors.
elif to_crc(curr_ancestor.name) in scene.skeleton or curr_ancestor.model_type == ModelType.SKIN or curr_ancestor.name in visited_nodes:
for potential_bone in potential_bones:
required_skeleton_models.append(potential_bone)
visited_nodes.add(potential_bone.name)
break
# Add ancestor to potential bones, update next ancestor
else:
if curr_ancestor.name not in visited_nodes:
potential_bones.insert(0, curr_ancestor)
curr_ancestor = model_dict[curr_ancestor.parent]
#stacked_transform = model_transform_to_matrix(curr_ancestor.transform) @ stacked_transform
return required_skeleton_models

View File

@@ -1,4 +1,4 @@
""" Helpers for SWBF skeleton-armature mapping """
""" Armature -> SWBF skeleton mapping functions """
import bpy
import math
@@ -12,9 +12,11 @@ from .msh_model_utilities import *
from .crc import *
def has_preserved_skeleton(armature : bpy.types.Armature):
return len(armature.data.swbf_msh_skel) > 0
def get_bone_world_matrix(armature: bpy.types.Object, bone_name: str) -> Matrix:
if bone_name in armature.data.bones:
return armature.matrix_world @ armature.data.bones[bone_name].matrix_local
else:
return None
'''Returns all bones that should be marked as BONE'''
@@ -33,179 +35,13 @@ def get_real_BONES(armature: bpy.types.Armature) -> Set[str]:
if len(skel_props) > 0:
for bone in skel_props:
#print(f"{bone.name} is a real BONE")
real_bones.add(bone.name)
elif action:
if action:
for group in armature.animation_data.action.groups:
#print(f"{group.name} is a real BONE")
real_bones.add(group.name)
else:
if len(skel_props) == 0 and action is None:
for bone in armature.data.bones:
#print(f"{bone.name} is a real BONE")
real_bones.add(bone.name)
return real_bones
'''
Creates armature from the required nodes.
Assumes the required_skeleton is already sorted by parent.
Uses model_map to get the world matrix of each bone (hacky, see NOTE)
'''
def required_skeleton_to_armature(required_skeleton : List[Model], model_map : Dict[str, bpy.types.Object], msh_scene : Scene) -> bpy.types.Object:
armature = bpy.data.armatures.new("skeleton")
armature_obj = bpy.data.objects.new("skeleton", armature)
bpy.context.view_layer.active_layer_collection.collection.objects.link(armature_obj)
bones_set = set([model.name for model in required_skeleton])
armature_obj.select_set(True)
bpy.context.view_layer.objects.active = armature_obj
bpy.ops.object.mode_set(mode='EDIT')
for bone in required_skeleton:
edit_bone = armature.edit_bones.new(bone.name)
if bone.parent and bone.parent in bones_set:
edit_bone.parent = armature.edit_bones[bone.parent]
'''
NOTE: I recall there being some rare issue with the get_world_matrix utility func.
Never bothered to figure it out and referencing the bone object's world mat always works.
Bone objects will be deleted later.
'''
bone_obj = model_map[bone.name]
edit_bone.matrix = bone_obj.matrix_world
edit_bone.tail = bone_obj.matrix_world @ Vector((0.0,1.0,0.0))
bone_children = [b for b in get_model_children(bone, required_skeleton)]
'''
Perhaps we'll add an option for importing bones tip-to-tail, but that would
require preserving their original transforms as changing the tail position
changes the bones' transform...
'''
tail_pos = Vector()
if bone_children:
for bone_child in bone_children:
tail_pos += bone_obj.matrix_world.translation
tail_pos = tail_pos / len(bone_children)
edit_bone.length = .5 #(tail_pos - edit_bone.head).magnitude
else:
bone_length = .5# edit_bone.parent.length if edit_bone.parent is not None else .5
edit_bone.tail = bone_obj.matrix_world @ Vector((0.0,bone_length,0.0))
bpy.ops.object.mode_set(mode='OBJECT')
armature_obj.select_set(True)
bpy.context.view_layer.update()
return armature_obj
'''
Ok, so this method is crucial. What this does is:
1) Find all nodes that are weighted to by skinned segments.
2) A node must be included in the armature if it:
- is in SKL2 and is not the scene root
- has model_type == BONE
- is weighted to
- has a parent and child that must be in the armature
This may need a lot of adjustments, don't think I can prove it's validity but it has worked very well
and handles all stock + ZETools + Pandemic XSI exporter models I've tested
'''
def extract_required_skeleton(scene: Scene) -> List[Model]:
# Will map Model names to Models in scene, for convenience
model_dict : Dict[str, Model] = {}
'''
Will contain hashes of all models that definitely need to be in the skeleton/armature.
We initialize it with the contents of SKL2 i.e. the nodes that are animated.
For now this includes the scene root, but that'll be excluded later.
'''
skeleton_hashes = set(scene.skeleton)
'''
We also need to add all nodes that are weighted to. These are not necessarily in
SKL2, as SKL2 seems to only reference nodes that are keyframed.
However, sometimes SKL2 is not included when it should be, but it can be mostly recovered
by checking which models are BONEs.
'''
for model in scene.models:
model_dict[model.name] = model
if model.model_type == ModelType.BONE:
skeleton_hashes.add(to_crc(model.name))
elif model.geometry:
for seg in model.geometry:
if seg.weights:
for weight_set in seg.weights:
for weight in weight_set:
model_weighted_to = scene.models[weight.bone]
if to_crc(model_weighted_to.name) not in skeleton_hashes:
skeleton_hashes.add(to_crc(model_weighted_to.name))
# The result of this function (to be sorted by parent)
required_skeleton_models = []
# Set of nodes to be included in required skeleton/were visited
visited_nodes = set()
'''
Here we add all skeleton nodes (except root) and any necessary ancestors to the armature.
- e.g. in bone_x/eff_x/eff_y, the effectors do not have to be in armature, as they are not ancestors of a bone
- but in bone_x/eff_x/eff_y/bone_y, they do.
'''
for bone in sort_by_parent(scene.models):
# make sure we exclude the scene root and any nodes irrelevant to the armature
if not bone.parent or to_crc(bone.name) not in skeleton_hashes:
continue
potential_bones = [bone]
visited_nodes.add(bone.name)
# Stacked transform will be needed if we decide to include an option for excluding effectors/roots or
# connecting bones tip-to-tail
#stacked_transform = model_transform_to_matrix(bone.transform)
curr_ancestor = model_dict[bone.parent]
while True:
# If we hit a non-skin scene root, that means we just add the bone we started with, no ancestors.
if not curr_ancestor.parent and curr_ancestor.model_type != ModelType.SKIN:
required_skeleton_models.append(bone)
visited_nodes.add(bone.name)
break
# If we encounter another bone, a skin, or a previously visited object, we need to add the bone and its
# ancestors.
elif to_crc(curr_ancestor.name) in scene.skeleton or curr_ancestor.model_type == ModelType.SKIN or curr_ancestor.name in visited_nodes:
for potential_bone in potential_bones:
required_skeleton_models.append(potential_bone)
visited_nodes.add(potential_bone.name)
break
# Add ancestor to potential bones, update next ancestor
else:
if curr_ancestor.name not in visited_nodes:
potential_bones.insert(0, curr_ancestor)
curr_ancestor = model_dict[curr_ancestor.parent]
#stacked_transform = model_transform_to_matrix(curr_ancestor.transform) @ stacked_transform
return required_skeleton_models

View File

@@ -1,374 +0,0 @@
""" Gathers the Blender objects from the current scene and returns them as a list of
Model objects. """
import bpy
import bmesh
import math
from enum import Enum
from typing import List, Set, Dict, Tuple
from .msh_scene import Scene
from .msh_material_to_blend import *
from .msh_model import *
from .msh_skeleton_utilities import *
from .msh_model_gather import get_is_model_hidden
from .crc import *
import os
# Extracts and applies anims in the scene to the currently selected armature
def extract_and_apply_anim(filename : str, scene : Scene):
arma = bpy.context.view_layer.objects.active
if not arma or arma.type != 'ARMATURE':
raise Exception("Select an armature to attach the imported animation to!")
if scene.animation is None:
raise Exception("No animation found in msh file!")
else:
head, tail = os.path.split(filename)
anim_name = tail.split(".")[0]
action = bpy.data.actions.new(anim_name)
action.use_fake_user = True
if not arma.animation_data:
arma.animation_data_create()
# Record the starting transforms of each bone. Pose space is relative
# to bones starting transforms. Starting = in edit mode
bone_bind_poses = {}
bpy.context.view_layer.objects.active = arma
bpy.ops.object.mode_set(mode='EDIT')
for edit_bone in arma.data.edit_bones:
if edit_bone.parent:
bone_local = edit_bone.parent.matrix.inverted() @ edit_bone.matrix
else:
bone_local = arma.matrix_local @ edit_bone.matrix
bone_bind_poses[edit_bone.name] = bone_local.inverted()
bpy.ops.object.mode_set(mode='OBJECT')
for bone in arma.pose.bones:
if to_crc(bone.name) in scene.animation.bone_frames:
bind_mat = bone_bind_poses[bone.name]
translation_frames, rotation_frames = scene.animation.bone_frames[to_crc(bone.name)]
loc_data_path = "pose.bones[\"{}\"].location".format(bone.name)
rot_data_path = "pose.bones[\"{}\"].rotation_quaternion".format(bone.name)
fcurve_rot_w = action.fcurves.new(rot_data_path, index=0, action_group=bone.name)
fcurve_rot_x = action.fcurves.new(rot_data_path, index=1, action_group=bone.name)
fcurve_rot_y = action.fcurves.new(rot_data_path, index=2, action_group=bone.name)
fcurve_rot_z = action.fcurves.new(rot_data_path, index=3, action_group=bone.name)
for frame in rotation_frames:
i = frame.index
q = (bind_mat @ convert_rotation_space(frame.rotation).to_matrix().to_4x4()).to_quaternion()
fcurve_rot_w.keyframe_points.insert(i,q.w)
fcurve_rot_x.keyframe_points.insert(i,q.x)
fcurve_rot_y.keyframe_points.insert(i,q.y)
fcurve_rot_z.keyframe_points.insert(i,q.z)
fcurve_loc_x = action.fcurves.new(loc_data_path, index=0, action_group=bone.name)
fcurve_loc_y = action.fcurves.new(loc_data_path, index=1, action_group=bone.name)
fcurve_loc_z = action.fcurves.new(loc_data_path, index=2, action_group=bone.name)
for frame in translation_frames:
i = frame.index
t = (bind_mat @ Matrix.Translation(convert_vector_space(frame.translation))).translation
fcurve_loc_x.keyframe_points.insert(i,t.x)
fcurve_loc_y.keyframe_points.insert(i,t.y)
fcurve_loc_z.keyframe_points.insert(i,t.z)
arma.animation_data.action = action
# Create the msh hierachy. Armatures are not created here. Much of this could use some optimization...
def extract_models(scene: Scene, materials_map : Dict[str, bpy.types.Material]) -> Dict[str, bpy.types.Object]:
# This will be filled with model names -> Blender objects and returned
model_map : Dict[str, bpy.types.Object] = {}
sorted_models : List[Model] = sort_by_parent(scene.models)
for model in sorted_models:
new_obj = None
if model.model_type == ModelType.STATIC or model.model_type == ModelType.SKIN or model.model_type == ModelType.SHADOWVOLUME:
new_mesh = bpy.data.meshes.new(model.name)
verts = []
faces = []
offset = 0
full_texcoords = []
weights_offsets = {}
face_range_to_material_index = []
if model.geometry:
#if model.collisionprimitive is None:
# print(f"On model: {model.name}")
for i,seg in enumerate(model.geometry):
verts += [tuple(convert_vector_space(v)) for v in seg.positions]
#if model.collisionprimitive is None:
# print("Importing segment with material: {} with and {} verts".format(seg.material_name, len(seg.positions)))
if seg.weights:
weights_offsets[offset] = seg.weights
if seg.texcoords is not None:
full_texcoords += seg.texcoords
else:
full_texcoords += [(0.0,0.0) for _ in range(len(seg.positions))]
face_range_lower = len(faces)
if seg.triangles:
faces += [tuple([ind + offset for ind in tri]) for tri in seg.triangles]
else:
for strip in seg.triangle_strips:
for i in range(len(strip) - 2):
face = tuple([offset + strip[j] for j in range(i,i+3)])
faces.append(face)
face_range_upper = len(faces)
face_range_to_material_index.append((face_range_lower, face_range_upper, i))
offset += len(seg.positions)
new_mesh.from_pydata(verts, [], faces)
new_mesh.update()
new_mesh.validate()
# If tex coords are present, add material and UV data
if full_texcoords:
edit_mesh = bmesh.new()
edit_mesh.from_mesh(new_mesh)
uvlayer = edit_mesh.loops.layers.uv.verify()
for edit_mesh_face in edit_mesh.faces:
face_index = edit_mesh_face.index
mesh_face = faces[face_index]
for frL, frU, ind in face_range_to_material_index:
if face_index >= frL and face_index < frU:
edit_mesh_face.material_index = ind
for i,loop in enumerate(edit_mesh_face.loops):
texcoord = full_texcoords[mesh_face[i]]
loop[uvlayer].uv = tuple([texcoord.x, texcoord.y])
edit_mesh.to_mesh(new_mesh)
edit_mesh.free()
new_obj = bpy.data.objects.new(new_mesh.name, new_mesh)
vertex_groups_indicies = {}
for offset in weights_offsets:
for i, weight_set in enumerate(weights_offsets[offset]):
for weight in weight_set:
index = weight.bone
if index not in vertex_groups_indicies:
model_name = scene.models[index].name
vertex_groups_indicies[index] = new_obj.vertex_groups.new(name=model_name)
vertex_groups_indicies[index].add([offset + i], weight.weight, 'ADD')
'''
Assign Material slots
'''
if model.geometry:
for seg in model.geometry:
if seg.material_name:
material = materials_map[seg.material_name]
new_obj.data.materials.append(material)
else:
new_obj = bpy.data.objects.new(model.name, None)
new_obj.empty_display_size = 1
new_obj.empty_display_type = 'PLAIN_AXES'
model_map[model.name] = new_obj
if model.parent:
new_obj.parent = model_map[model.parent]
new_obj.location = convert_vector_space(model.transform.translation)
new_obj.rotation_mode = "QUATERNION"
new_obj.rotation_quaternion = convert_rotation_space(model.transform.rotation)
if model.collisionprimitive is not None:
new_obj.swbf_msh_coll_prim.prim_type = model.collisionprimitive.shape.value
bpy.context.collection.objects.link(new_obj)
return model_map
# TODO: Add to custom material info struct, maybe some material conversion/import?
def extract_materials(folder_path: str, scene: Scene) -> Dict[str, bpy.types.Material]:
extracted_materials : Dict[str, bpy.types.Material] = {}
for material_name, material in scene.materials.items():
new_mat = bpy.data.materials.new(name=material_name)
new_mat.use_nodes = True
bsdf = new_mat.node_tree.nodes["Principled BSDF"]
diffuse_texture_path = find_texture_path(folder_path, material.texture0)
if diffuse_texture_path:
texImage = new_mat.node_tree.nodes.new('ShaderNodeTexImage')
texImage.image = bpy.data.images.load(diffuse_texture_path)
new_mat.node_tree.links.new(bsdf.inputs['Base Color'], texImage.outputs['Color'])
fill_material_props(material, new_mat.swbf_msh_mat)
extracted_materials[material_name] = new_mat
return extracted_materials
def extract_scene(filepath: str, scene: Scene):
folder = os.path.join(os.path.dirname(filepath),"")
# material_map mapes Material names to Blender materials
material_map = extract_materials(folder, scene)
# model_map maps Model names to Blender objects.
model_map = extract_models(scene, material_map)
# skel contains all models needed in an armature
skel = extract_required_skeleton(scene)
# Create the armature if skel is non-empty
armature = None if not skel else required_skeleton_to_armature(skel, model_map, scene)
if armature is not None:
preserved_skel = armature.data.swbf_msh_skel
for model in scene.models:
if to_crc(model.name) in scene.skeleton or model.model_type == ModelType.BONE:
entry = preserved_skel.add()
entry.name = model.name
'''
If an armature was created, we need to do a few extra
things to ensure the import makes sense in Blender. It can
get a bit messy, as XSI + SWBF have very loose requirements
when it comes to skin-skeleton parentage.
If not, we're good.
'''
if armature is not None:
has_skin = False
# Handle armature related parenting
for curr_model in scene.models:
curr_obj = model_map[curr_model.name]
# Parent all skins to armature
if curr_model.model_type == ModelType.SKIN:
has_skin = True
curr_obj.parent = armature
curr_obj.parent_type = 'ARMATURE'
# Parent the object to a bone if necessary
else:
if curr_model.parent in armature.data.bones and curr_model.name not in armature.data.bones:
# Not sure what the different mats do, but saving the worldmat and
# applying it after clearing the other mats yields correct results...
worldmat = curr_obj.matrix_world
curr_obj.parent = armature
curr_obj.parent_type = 'BONE'
curr_obj.parent_bone = curr_model.parent
# ''
curr_obj.matrix_basis = Matrix()
curr_obj.matrix_parent_inverse = Matrix()
curr_obj.matrix_world = worldmat
'''
Sometimes skins are parented to other skins. We need to find the skin highest in the hierarchy and
parent all skins to its parent (armature_reparent_obj).
If not skin exists, we just reparent the armature to the parent of the highest node in the skeleton
'''
armature_reparent_obj = None
if has_skin:
for model in sort_by_parent(scene.models):
if model.model_type == ModelType.SKIN:
armature_reparent_obj = None if not model.parent else model_map[model.parent]
else:
skeleton_parent_name = skel[0].parent
for model in scene.models:
if model.name == skeleton_parent_name:
armature_reparent_obj = None if not skeleton_parent_name else model_map[skeleton_parent_name]
# Now we reparent the armature to the node (armature_reparent_obj) we just found
if armature_reparent_obj is not None and armature.name != armature_reparent_obj.name:
armature.parent = armature_reparent_obj
# If an bone exists in the armature, delete its
# object counterpart (as created in extract_models)
for bone in skel:
model_to_remove = model_map[bone.name]
if model_to_remove:
bpy.data.objects.remove(model_to_remove, do_unlink=True)
model_map.pop(bone.name)
# Lastly, hide all that is hidden in the msh scene
for model in scene.models:
if model.name in model_map:
obj = model_map[model.name]
if get_is_model_hidden(obj) and len(obj.children) == 0:
obj.hide_set(True)

View File

@@ -39,12 +39,9 @@ def pack_color(color) -> int:
return packed
def unpack_color(color: int) -> List[float]:
mask = int(0x000000ff)
r = (color & (mask << 16)) / 255.0
g = (color & (mask << 8)) / 255.0
b = (color & mask) / 255.0
a = (color & (mask << 24)) / 255.0
r = (color >> 16 & 0xFF) / 255.0
g = (color >> 8 & 0xFF) / 255.0
b = (color >> 0 & 0xFF) / 255.0
a = (color >> 24 & 0xFF) / 255.0
return [r,g,b,a]

View File

@@ -0,0 +1,46 @@
""" Parses .tga.option and .msh.option files. Only used with the former as of now. """
import os
class MungeOptions:
def __init__(self, path_to_option_file):
self.options = {}
if os.path.exists(path_to_option_file):
with open(path_to_option_file, 'r') as option_file:
option_text = option_file.read()
option_parts = option_text.split()
current_parameter = ""
for part in option_parts:
if part.startswith("-"):
current_parameter = part[1:]
self.options[current_parameter] = ""
elif current_parameter:
current_value = self.options[current_parameter]
# Keep adding to value in case there are vector options
self.options[current_parameter] += part if not current_value else (" " + part)
def is_option_present(self, param):
return param in self.options
def get_bool(self, param, default=False):
return True if param in self.options else default
def get_float(self, param, default=0.0):
if param in self.options:
try:
result = float(self.options[param])
except:
result = default
finally:
return result
else:
return default
def get_string(self, param, default=""):
return self.options.get(param, default)

BIN
docs/images/mat_fill_op.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

View File

@@ -24,14 +24,15 @@
+ [Materials.Flags](#materialsflags)
+ [Materials.Data](#materialsdata)
+ [Materials.Texture Maps](#materialstexture-maps)
+ [Materials Operators](#materials-operators)
- [Skeletons and Skinning](#skeletons-and-skinning)
+ [XSI vs Blender](#xsi-vs-blender)
+ [Example Skin Hierarchy](#example-skin-hierarchy)
+ [Example Bone-Parent Hierarchy](#example-bone-parent-hierarchy)
+ [Skeleton Notes](#skeleton-notes)
+ [Skinning Notes](#skinning-notes)
- [Animation](#animation)
+ [Actions and Animations](#actions-and-animations)
+ [Exporter Animation Options](#exporter-animation-options)
+ [Animation Notes](#animation-notes)
- [Appendices](#appendices)
+ [Appendix Detail Map Blending](#appendix-detail-map-blending)
@@ -70,15 +71,21 @@ Controls what to export from Blender.
#### Apply Modifiers
Whether to apply [Modifiers](https://docs.blender.org/manual/en/latest/modeling/modifiers/index.html) during export or not.
#### Export As Skeleton
#### Export Animation(s)
| | |
| ---------------------- | ---------------------------------------------------------------------- |
| None | Export the current active scene without animation data. |
| Active | Export the current active scene with animation data extracted from the active Action on the scene's Armature. To save space, the exporter will exclude geometry data from the resulting .msh file but will ensure the root object has some geometry and a material for munge compatibility. |
| Batch | Export the current active scene with animation data but produce a separate .msh file for and named after each Action in the scene. Exported files will be placed in the selected directory. If a file is selected, they will be placed in that file's directory. This option essentially repeats the export behavior of "Active" for each Action in the current Scene. Be sure to remove an Action from the scene if you do not want it exported! |
Excludes geometry data from the exported .msh file, BUT ensures that the scene root has dummy geometry to satisfy the animation munger.
#### Export With Animation
Convert the active Action on the scene's armature to an SWBF animation and include it in the exported file.
(Please see [Exporter Animation Options](#exporter-animation-options) for more details on the previous two parameters)
### Export Failures
@@ -131,6 +138,12 @@ This error indicates that an object in your scene ends with what looks like an L
To solve this error consult the [LOD Models](#lod-models) section and rename the problematic objects to use the correct LOD suffix.
#### "RuntimeError: Could not find an Armature object from which to export animations!"
This error is thrown when you intend to export one or more animations but no Armature is found among the objects to be exported.
### Export Behaviour to Know About
#### Materials for .msh files must be managed through the added UI panel named "SWBF .msh Properties" is added under the Material context.
@@ -186,15 +199,18 @@ Can't imagine this coming up much (Maybe if you're model is just for collisions
#### Meshes without any materials will be assigned the first material in the .msh file.
This shouldn't be relevant as any mesh that you haven't assigned a material to is likely to just be collision geometry or shadow geometry.
#### Dummy frames for the scene root will be included when exporting an animation.
If the scene root is not keyed in the Action(s) to be exported, dummy frames for the scene root with no translation or rotation will be added to the exported animation.
## Importer
This plugin can import .msh files as well as .zaa_ and .zaabin files. .msh files can be imported as models or animations.
This plugin can import one or more .msh files as well as .zaabin files. .msh files can be imported as models or animations.
### Import Properties
#### Import Animation Only
#### Import Animation(s)
If you wish to import an .msh or zaa_/zaabin file as an animation, check this box. This will only work so long as you have preselected an armature. The imported animation will then be added to the armature as an Action. If an Action with the same name already exists, the importer will replace it.
If you wish to import animation data from one or more .msh files or a single .zaabin file, check this box. This will only work so long as you have preselected an Armature! The imported animations will then be added to the Armature as Actions. If an Action with the same name already exists, the importer will replace it.
### Import Failures
@@ -561,6 +577,23 @@ Environment map for the material. Used to provide static reflections for the mod
Distortion maps control how Refractive materials distort the scene behind them. Should be a Normal Map with '-forceformat v8u8' in it's '.tga.option' file. See Appendix .tga.option Files.
### Materials Operators
#### Fill SWBF Properties
Fills in SWBF properties of each material used by all selected objects. This operator will only work with materials that have ```Use Nodes``` enabled and will just fill in the Diffuse Map property with the name of the image used by the material's Principled BSDF node.
It is used by selecting the relevant objects and choosing `SWBF` > `Fill SWBF Material Properties` in the `Object Context` menu:
<img src="https://raw.githubusercontent.com/SleepKiller/SWBF-msh-Blender-IO/master/docs/images/mat_fill_op.png" width="400" height="400"/>
#### Generate SWBF Nodes
Generates shader nodes that attempt to emulate the SWBF properties of a selected material. Call this operator by clicking the `Generate Nodes` button found at the bottom of the selected material's SWBF properties panel. Only transparency settings and diffuse texture mapping are currently supported. When importing a .msh file, this operator is automatically called on each material extracted from the file.
You must click the `Generate Nodes` button every time you edit the material properties and wish to see the results. The generated nodes will not automatically update when the SWBF properties are changed.
It is not necessary to call this operator for materials to correctly export.
@@ -614,6 +647,8 @@ The same goes for objects that are children of an armature, but are parented dir
* bone_doorright
* right_door_mesh
### Skeleton Notes
1. Animated msh bones can have have geometry. To accomplish this, create a bone in the scene's armature and a mesh object with the same name as the bone. Parent the mesh object to the bone and make sure their origins are equal. When exported, the bone and the mesh object will be merged into one. The inverse process occurs when importing bones with geometry.
### Skinning Notes
@@ -640,17 +675,6 @@ When exporting an Action, all frames between and including the first and last *k
If you have armature bones that are weighted to by a skinned object, but you do not wish for them to be exported as part of the animated skeleton, don't keyframe them. The exported animation will only include bones that are explicitly keyframed at least once in the Action.
### Exporter Animation Options
#### ```Export As Skeleton```
Excludes geometry data from the exported .msh file, since ```zenasset``` ignores it. Skins and static meshes will be exported as nulls. However, since ```zenasset``` does mandate the root object have some material and geometry data, this option will add in dummy geometry and a material to the .msh file's scene root. This isn't necessary for exporting animations, but is highly recommended to avoid writing unnecessary data and ensuring the root object is acceptable to ```zenasset```.
#### ```Export With Animation```
If checked, the action currently attached to the scene's armature will be included in the exported msh file as an animation. Dummy frames are also included for the scene root to satisfy ```zenasset```. You do not have to explicitly animate the scene root!
So, if you wish to export an animation to be munged, it is best to select both ```Export As Skeleton``` and ```Export With Animation.```
### Animation notes: