Compare commits

..

No commits in common. "master" and "vertex-weights" have entirely different histories.

33 changed files with 226 additions and 3763 deletions

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
.DS_Store
*.msh *.msh
# Created by https://www.gitignore.io/api/python,visualstudiocode # Created by https://www.gitignore.io/api/python,visualstudiocode

View File

@ -1,14 +1,33 @@
# SWBF-msh-Blender-IO # SWBF-msh-Blender-Export
.msh (SWBF toolchain version) Import-Exporter for Blender WIP .msh (SWBF toolchain version) exporter for Blender 2.8
Currently capable of exporting the active scene without skinning information.
### Installing ### Installing
You install it like any other Blender addon, if you already know how to do that then great! Else head over [here](https://docs.blender.org/manual/en/3.0/editors/preferences/addons.html#installing-add-ons) to learn how to do it in Blender 3.0. You install it like any other Blender addon, if you already know how to do that then great! Grab the [latest release](https://github.com/SleepKiller/SWBF-msh-Blender-Export/releases/latest) (or if you're the adventerous type [clone from source](https://github.com/SleepKiller/SWBF-msh-Blender-Export/archive/master.zip)) and you'll be good to go.
However if you don't know how to install then read on!
These instructions are going to be for Windows. If you're on a different platform I'm sure some quick web searching can help provide you with answers.
First download and extract the addon [latest release](https://github.com/SleepKiller/SWBF-msh-Blender-Export/releases/latest).
Then open up Explorer and paste `%USERPROFILE%\AppData\Roaming\Blender Foundation\Blender\2.80\` into it's address bar. Then go into the `scripts` folder in that directory and copy the `addons` folder from the extracted addon into the scripts folder.
Next open up Blender, go into Edit > Preferences > Addons. Select "Community" tab and then search for ".msh". "Import-Export: SWBF .msh export" should come up, check the box next to it. The preferences window should look like this once you're done.
![Installed addon.](docs/images/blender_addon_installed.png)
If you've done that then the addon is installed and you should now find "SWBF msh" listed under Blender's export options.
### Reference Manual ### Reference Manual
Included in the repository is a [Reference Manual](https://github.com/SleepKiller/SWBF-msh-Blender-Export/blob/master/docs/reference_manual.md#reference-manual) of sorts. There is no need to read through it before using the addon but anytime you have a question about how something works or why an export failed it should hopefully have the answers. Included in the repository is a [Reference Manual](https://github.com/SleepKiller/SWBF-msh-Blender-Export/blob/master/docs/reference_manual.md#reference-manual) of sorts. There is no need to read through it before using the addon but anytime you have a question about how something works or why an export failed it should hopefully have the answers.
### Work to be done ### Work to be done
- [ ] Investigate and add support for exporting bones and vertex weights.
- [ ] Investigate and add support for exporting animations.
- [ ] Investigate and add support for editing and exporting SWBF2 cloth. - [ ] Investigate and add support for editing and exporting SWBF2 cloth.
- [ ] Implement .msh importing. Currently you can use the 1.2 release of [swbf-unmunge](releases/tag/v1.2.0) to save out munged models to glTF 2.0 files if you need to open a model in Blender.
### What from [glTF-Blender-IO](https://github.com/KhronosGroup/glTF-Blender-IO) was used? ### What from [glTF-Blender-IO](https://github.com/KhronosGroup/glTF-Blender-IO) was used?
The `reload_package` function from \_\_init\_\_.py. Before writing this I had barely touched Python and when I saw that glTF-Blender-IO had a function to assist script reloading "I thought that's useful, I think I kinda need that and I don't know how to write something like that myself yet.". And it was very useful, so thank you to all the glTF-Blender-IO developers and contributors. The `reload_package` function from \_\_init\_\_.py. Before writing this I had barely touched Python and when I saw that glTF-Blender-IO had a function to assist script reloading "I thought that's useful, I think I kinda need that and I don't know how to write something like that myself yet.". And it was very useful, so thank you to all the glTF-Blender-IO developers and contributors.

View File

@ -1,7 +1,7 @@
bl_info = { bl_info = {
'name': 'SWBF .msh Import-Export', 'name': 'SWBF .msh export',
'author': 'Will Snyder, PrismaticFlower', 'author': 'SleepKiller',
"version": (1, 3, 0), "version": (0, 2, 1),
'blender': (2, 80, 0), 'blender': (2, 80, 0),
'location': 'File > Import-Export', 'location': 'File > Import-Export',
'description': 'Export as SWBF .msh file', 'description': 'Export as SWBF .msh file',
@ -53,20 +53,12 @@ if "bpy" in locals():
# End of stuff taken from glTF # End of stuff taken from glTF
import bpy import bpy
from bpy_extras.io_utils import ExportHelper, ImportHelper from bpy_extras.io_utils import ExportHelper
from bpy.props import BoolProperty, EnumProperty, CollectionProperty from bpy.props import BoolProperty, EnumProperty
from bpy.types import Operator, Menu from bpy.types import Operator
from .msh_scene_utilities import create_scene, set_scene_animation from .msh_scene import create_scene
from .msh_scene_save import save_scene from .msh_scene_save import save_scene
from .msh_scene_read import read_scene
from .msh_material_properties import * from .msh_material_properties import *
from .msh_skeleton_properties import *
from .msh_collision_prim_properties import *
from .msh_material_operators import *
from .msh_scene_to_blend import *
from .msh_anim_to_blend import *
from .zaa_to_blend import *
class ExportMSH(Operator, ExportHelper): class ExportMSH(Operator, ExportHelper):
""" Export the current scene as a SWBF .msh file. """ """ Export the current scene as a SWBF .msh file. """
@ -105,160 +97,35 @@ class ExportMSH(Operator, ExportHelper):
default=True default=True
) )
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): def execute(self, context):
with open(self.filepath, 'wb') as output_file:
if 'SELECTED' in self.export_target and len(bpy.context.selected_objects) == 0: save_scene(
raise Exception("{} was chosen, but you have not selected any objects. " output_file=output_file,
" Don't forget to unhide all the objects you wish to select!".format(self.export_target)) scene=create_scene(
scene, armature_obj = create_scene(
generate_triangle_strips=self.generate_triangle_strips, generate_triangle_strips=self.generate_triangle_strips,
apply_modifiers=self.apply_modifiers, apply_modifiers=self.apply_modifiers,
export_target=self.export_target, 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'} return {'FINISHED'}
# Only needed if you want to add into a dynamic menu # Only needed if you want to add into a dynamic menu
def menu_func_export(self, context): def menu_func_export(self, context):
self.layout.operator(ExportMSH.bl_idname, text="SWBF msh (.msh)") self.layout.operator(ExportMSH.bl_idname, text="SWBF msh (.msh)")
class ImportMSH(Operator, ImportHelper):
""" Import SWBF .msh file(s). """
bl_idname = "swbf_msh.import"
bl_label = "Import SWBF .msh File(s)"
filename_ext = ".msh"
files: CollectionProperty(
name="File Path(s)",
type=bpy.types.OperatorFileListElement,
)
filter_glob: StringProperty(
default="*.msh;*.zaa;*.zaabin",
options={'HIDDEN'},
maxlen=255, # Max internal buffer length, longer would be clamped.
)
animation_only: BoolProperty(
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
)
def execute(self, context):
dirname = os.path.dirname(self.filepath)
for file in self.files:
filepath = os.path.join(dirname, file.name)
if filepath.endswith(".zaabin") or filepath.endswith(".zaa"):
extract_and_apply_munged_anim(filepath)
else:
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)
return {'FINISHED'}
def menu_func_import(self, context):
self.layout.operator(ImportMSH.bl_idname, text="SWBF msh (.msh)")
def register(): def register():
bpy.utils.register_class(CollisionPrimitiveProperties)
bpy.utils.register_class(MaterialProperties) bpy.utils.register_class(MaterialProperties)
bpy.utils.register_class(MaterialPropertiesPanel) bpy.utils.register_class(MaterialPropertiesPanel)
bpy.utils.register_class(SkeletonProperties)
bpy.utils.register_class(SkeletonPropertiesPanel)
bpy.utils.register_class(ExportMSH) bpy.utils.register_class(ExportMSH)
bpy.utils.register_class(ImportMSH)
bpy.types.TOPBAR_MT_file_export.append(menu_func_export) bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
bpy.types.TOPBAR_MT_file_import.append(menu_func_import) bpy.types.Material.swbf_msh = bpy.props.PointerProperty(type=MaterialProperties)
bpy.types.Object.swbf_msh_coll_prim = bpy.props.PointerProperty(type=CollisionPrimitiveProperties)
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(): def unregister():
bpy.utils.unregister_class(CollisionPrimitiveProperties)
bpy.utils.unregister_class(MaterialProperties) bpy.utils.unregister_class(MaterialProperties)
bpy.utils.unregister_class(MaterialPropertiesPanel) bpy.utils.unregister_class(MaterialPropertiesPanel)
bpy.utils.unregister_class(SkeletonProperties)
bpy.utils.unregister_class(SkeletonPropertiesPanel)
bpy.utils.unregister_class(ExportMSH) bpy.utils.unregister_class(ExportMSH)
bpy.utils.unregister_class(ImportMSH)
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) 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__": if __name__ == "__main__":
register() register()

View File

@ -1,153 +0,0 @@
"""
Reader class for both zaabin, zaa, and msh files.
"""
import io
import struct
import os
from mathutils import Vector, Quaternion
class Reader:
def __init__(self, file, parent=None, indent=0, debug=False):
self.file = file
self.size: int = 0
self.size_pos = None
self.parent = parent
self.indent = " " * indent #for print debugging, should be stored as str so msh_scene_read can access it
self.debug = debug
def __enter__(self):
self.size_pos = self.file.tell()
if self.parent is not None:
self.header = self.read_bytes(4).decode("utf-8")
else:
self.header = "File"
if self.parent is not None:
self.size = self.read_u32()
else:
self.size = os.path.getsize(self.file.name) - 8
# No padding to multiples of 4. Files exported from XSI via zetools do not align by 4!
self.end_pos = self.size_pos + self.size + 8
if self.debug:
print("{}Begin {} of Size {} at pos {}:".format(self.indent, self.header, self.size, self.size_pos))
return self
def __exit__(self, exc_type, exc_value, traceback):
if self.size > self.MAX_SIZE:
raise OverflowError(f"File overflowed max size. size = {self.size} MAX_SIZE = {self.MAX_SIZE}")
if self.debug:
print("{}End {} at pos: {}".format(self.indent, self.header, self.end_pos))
self.file.seek(self.end_pos)
def read_bytes(self,num_bytes):
return self.file.read(num_bytes)
def read_string(self):
last_byte = self.read_bytes(1)
result = b''
while last_byte[0] != 0x0:
result += last_byte
last_byte = self.read_bytes(1)
return result.decode("utf-8")
def read_i8(self, num=1):
buf = self.read_bytes(num)
result = struct.unpack(f"<{num}b", buf)
return result[0] if num == 1 else result
def read_u8(self, num=1):
buf = self.read_bytes(num)
result = struct.unpack(f"<{num}B", buf)
return result[0] if num == 1 else result
def read_i16(self, num=1):
buf = self.read_bytes(num * 2)
result = struct.unpack(f"<{num}h", buf)
return result[0] if num == 1 else result
def read_u16(self, num=1):
buf = self.read_bytes(num * 2)
result = struct.unpack(f"<{num}H", buf)
return result[0] if num == 1 else result
def read_i32(self, num=1):
buf = self.read_bytes(num * 4)
result = struct.unpack(f"<{num}i", buf)
return result[0] if num == 1 else result
def read_u32(self, num=1):
buf = self.read_bytes(num * 4)
result = struct.unpack(f"<{num}I", buf)
return result[0] if num == 1 else result
def read_f32(self, num=1):
buf = self.read_bytes(num * 4)
result = struct.unpack(f"<{num}f", buf)
return result[0] if num == 1 else result
def read_quat(self):
rot = self.read_f32(4)
return Quaternion((rot[3], rot[0], rot[1], rot[2]))
def read_vec(self):
return Vector(self.read_f32(3))
def read_child(self):
child = Reader(self.file, parent=self, indent=int(len(self.indent) / 2) + 1, debug=self.debug)
return child
def skip_bytes(self,num):
self.file.seek(num,1)
def peak_next_header(self):
buf = self.read_bytes(4);
self.file.seek(-4,1)
try:
result = buf.decode("utf-8")
return result
except:
return ""
def get_current_pos(self):
return self.file.tell()
def reset_pos(self):
self.file.seek(self.size_pos - self.file.tell() + 8, 1)
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)
def could_have_child(self):
return self.end_pos - self.file.tell() >= 8
MAX_SIZE: int = 2147483647 - 8

View File

@ -1,89 +0,0 @@
'''
Based on code by Benedikt Schatz from https://github.com/Schlechtwetterfront/xsizetools/blob/master/Application/Modules/msh2_crc.py
'''
# CRC lookup table.
table32_lookup = (
0x00000000, 0x04C11DB7, 0x09823B6E, 0x0D4326D9,
0x130476DC, 0x17C56B6B, 0x1A864DB2, 0x1E475005,
0x2608EDB8, 0x22C9F00F, 0x2F8AD6D6, 0x2B4BCB61,
0x350C9B64, 0x31CD86D3, 0x3C8EA00A, 0x384FBDBD,
0x4C11DB70, 0x48D0C6C7, 0x4593E01E, 0x4152FDA9,
0x5F15ADAC, 0x5BD4B01B, 0x569796C2, 0x52568B75,
0x6A1936C8, 0x6ED82B7F, 0x639B0DA6, 0x675A1011,
0x791D4014, 0x7DDC5DA3, 0x709F7B7A, 0x745E66CD,
0x9823B6E0, 0x9CE2AB57, 0x91A18D8E, 0x95609039,
0x8B27C03C, 0x8FE6DD8B, 0x82A5FB52, 0x8664E6E5,
0xBE2B5B58, 0xBAEA46EF, 0xB7A96036, 0xB3687D81,
0xAD2F2D84, 0xA9EE3033, 0xA4AD16EA, 0xA06C0B5D,
0xD4326D90, 0xD0F37027, 0xDDB056FE, 0xD9714B49,
0xC7361B4C, 0xC3F706FB, 0xCEB42022, 0xCA753D95,
0xF23A8028, 0xF6FB9D9F, 0xFBB8BB46, 0xFF79A6F1,
0xE13EF6F4, 0xE5FFEB43, 0xE8BCCD9A, 0xEC7DD02D,
0x34867077, 0x30476DC0, 0x3D044B19, 0x39C556AE,
0x278206AB, 0x23431B1C, 0x2E003DC5, 0x2AC12072,
0x128E9DCF, 0x164F8078, 0x1B0CA6A1, 0x1FCDBB16,
0x018AEB13, 0x054BF6A4, 0x0808D07D, 0x0CC9CDCA,
0x7897AB07, 0x7C56B6B0, 0x71159069, 0x75D48DDE,
0x6B93DDDB, 0x6F52C06C, 0x6211E6B5, 0x66D0FB02,
0x5E9F46BF, 0x5A5E5B08, 0x571D7DD1, 0x53DC6066,
0x4D9B3063, 0x495A2DD4, 0x44190B0D, 0x40D816BA,
0xACA5C697, 0xA864DB20, 0xA527FDF9, 0xA1E6E04E,
0xBFA1B04B, 0xBB60ADFC, 0xB6238B25, 0xB2E29692,
0x8AAD2B2F, 0x8E6C3698, 0x832F1041, 0x87EE0DF6,
0x99A95DF3, 0x9D684044, 0x902B669D, 0x94EA7B2A,
0xE0B41DE7, 0xE4750050, 0xE9362689, 0xEDF73B3E,
0xF3B06B3B, 0xF771768C, 0xFA325055, 0xFEF34DE2,
0xC6BCF05F, 0xC27DEDE8, 0xCF3ECB31, 0xCBFFD686,
0xD5B88683, 0xD1799B34, 0xDC3ABDED, 0xD8FBA05A,
0x690CE0EE, 0x6DCDFD59, 0x608EDB80, 0x644FC637,
0x7A089632, 0x7EC98B85, 0x738AAD5C, 0x774BB0EB,
0x4F040D56, 0x4BC510E1, 0x46863638, 0x42472B8F,
0x5C007B8A, 0x58C1663D, 0x558240E4, 0x51435D53,
0x251D3B9E, 0x21DC2629, 0x2C9F00F0, 0x285E1D47,
0x36194D42, 0x32D850F5, 0x3F9B762C, 0x3B5A6B9B,
0x0315D626, 0x07D4CB91, 0x0A97ED48, 0x0E56F0FF,
0x1011A0FA, 0x14D0BD4D, 0x19939B94, 0x1D528623,
0xF12F560E, 0xF5EE4BB9, 0xF8AD6D60, 0xFC6C70D7,
0xE22B20D2, 0xE6EA3D65, 0xEBA91BBC, 0xEF68060B,
0xD727BBB6, 0xD3E6A601, 0xDEA580D8, 0xDA649D6F,
0xC423CD6A, 0xC0E2D0DD, 0xCDA1F604, 0xC960EBB3,
0xBD3E8D7E, 0xB9FF90C9, 0xB4BCB610, 0xB07DABA7,
0xAE3AFBA2, 0xAAFBE615, 0xA7B8C0CC, 0xA379DD7B,
0x9B3660C6, 0x9FF77D71, 0x92B45BA8, 0x9675461F,
0x8832161A, 0x8CF30BAD, 0x81B02D74, 0x857130C3,
0x5D8A9099, 0x594B8D2E, 0x5408ABF7, 0x50C9B640,
0x4E8EE645, 0x4A4FFBF2, 0x470CDD2B, 0x43CDC09C,
0x7B827D21, 0x7F436096, 0x7200464F, 0x76C15BF8,
0x68860BFD, 0x6C47164A, 0x61043093, 0x65C52D24,
0x119B4BE9, 0x155A565E, 0x18197087, 0x1CD86D30,
0x029F3D35, 0x065E2082, 0x0B1D065B, 0x0FDC1BEC,
0x3793A651, 0x3352BBE6, 0x3E119D3F, 0x3AD08088,
0x2497D08D, 0x2056CD3A, 0x2D15EBE3, 0x29D4F654,
0xC5A92679, 0xC1683BCE, 0xCC2B1D17, 0xC8EA00A0,
0xD6AD50A5, 0xD26C4D12, 0xDF2F6BCB, 0xDBEE767C,
0xE3A1CBC1, 0xE760D676, 0xEA23F0AF, 0xEEE2ED18,
0xF0A5BD1D, 0xF464A0AA, 0xF9278673, 0xFDE69BC4,
0x89B8FD09, 0x8D79E0BE, 0x803AC667, 0x84FBDBD0,
0x9ABC8BD5, 0x9E7D9662, 0x933EB0BB, 0x97FFAD0C,
0xAFB010B1, 0xAB710D06, 0xA6322BDF, 0xA2F33668,
0xBCB4666D, 0xB8757BDA, 0xB5365D03, 0xB1F740B4
)
def to_lower(charcode):
if charcode <= 64 or charcode > 90:
return charcode
else:
return charcode + 32
# Not sure what Schlechtwetterfront means by "Simulate unsigned behavior.",
# kept it anyways just without the extra functions
def to_crc(string):
crc_ = ~0 & 0xFFFFFFFF
if string:
for char in string:
ind = (crc_ >> 24) ^ to_lower(ord(char))
crc_ = ((crc_ << 8) & 0xFFFFFFFF) ^ table32_lookup[ind]
return ~crc_ & 0xFFFFFFFF

View File

@ -1,100 +0,0 @@
""" Converts currently active Action to an msh Animation """
import bpy
import math
from enum import Enum
from typing import List, Set, Dict, Tuple
from itertools import zip_longest
from .msh_model import *
from .msh_model_utilities import *
from .msh_utilities import *
from .msh_model_gather import *
from .msh_skeleton_utilities import *
from .crc import to_crc
'''
Convert the active Action into an Animation. When exported SWBF anims, there is the issue
that all bones in the anim must be in the skeleton/basepose anim. We guarantee this by
only keying bones if they are in the armature's preserved skeleton (swbf_msh_skel) and
adding dummy frames if the bones are not in the armature.
If a preserved skeleton is not present, we include only the keyed bones and add dummy frames for
the root (root_name)
'''
def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation:
if not armature.animation_data or not armature.animation_data.action:
raise RuntimeError("Cannot export animation data without an active Action on armature!")
action = armature.animation_data.action
# Set of bones to include in SKL2/animation stuff
keyable_bones = get_real_BONES(armature)
# 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 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)
dummy_bones = set([keyable_bone for keyable_bone in keyable_bones if keyable_bone not in armature.data.bones])
anim = Animation();
root_crc = to_crc(root_name)
if not action:
framerange = Vector((0.0,1.0))
else:
framerange = action.frame_range
num_frames = math.floor(framerange.y - framerange.x) + 1
increment = (framerange.y - framerange.x) / (num_frames - 1)
anim.end_index = num_frames - 1
for keyable_bone in keyable_bones:
anim.bone_frames[to_crc(keyable_bone)] = ([], [])
for frame in range(num_frames):
frame_time = framerange.x + frame * increment
bpy.context.scene.frame_set(frame_time)
for keyable_bone in keyable_bones:
bone_crc = to_crc(keyable_bone)
if keyable_bone in dummy_bones:
rframe = RotationFrame(frame, convert_rotation_space(Quaternion()))
tframe = TranslationFrame(frame, Vector((0.0,0.0,0.0)))
else:
bone = armature.pose.bones[keyable_bone]
transform = bone.matrix
if bone.parent:
transform = bone.parent.matrix.inverted() @ transform
loc, rot, _ = transform.decompose()
rframe = RotationFrame(frame, convert_rotation_space(rot))
tframe = TranslationFrame(frame, convert_vector_space(loc))
anim.bone_frames[bone_crc][0].append(tframe)
anim.bone_frames[bone_crc][1].append(rframe)
return anim

View File

@ -1,110 +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_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, action.frame_range[0], action)

View File

@ -1,16 +0,0 @@
""" IntProperty needed to keep track of Collision Primitives types that are imported without indicitive names.
Not sure I needed a PropertyGroup/what a leaner method would be. The prims shouldn't be renamed on import because
they are often referenced in ODFs.
Don't see a reason these should be exposed via a panel or need to be changed..."""
import bpy
from bpy.props import IntProperty
from bpy.types import PropertyGroup
class CollisionPrimitiveProperties(PropertyGroup):
prim_type: IntProperty(name="Primitive Type", default=-1)

View File

@ -19,31 +19,6 @@ class Rendertype(Enum):
NORMALMAPPED = 27 NORMALMAPPED = 27
NORMALMAPPED_TILED_ENVMAP = 29 NORMALMAPPED_TILED_ENVMAP = 29
# Placeholders to avoid crashes/import-export inconsistencies
OTHER_1 = 1
OTHER_2 = 2
OTHER_4 = 4
OTHER_5 = 5
OTHER_8 = 8
OTHER_9 = 9
OTHER_10 = 10
OTHER_11 = 11
OTHER_12 = 12
OTHER_13 = 13
OTHER_14 = 14
OTHER_15 = 15
OTHER_16 = 16
OTHER_17 = 17
OTHER_18 = 18
OTHER_19 = 19
OTHER_20 = 20
OTHER_21 = 21
OTHER_23 = 23
OTHER_28 = 28
OTHER_30 = 30
OTHER_31 = 31
class MaterialFlags(Flag): class MaterialFlags(Flag):
NONE = 0 NONE = 0
UNLIT = 1 UNLIT = 1
@ -57,9 +32,8 @@ class MaterialFlags(Flag):
@dataclass @dataclass
class Material: class Material:
""" Data class representing a .msh material.""" """ Data class representing a .msh material.
Intended to be stored in a dictionary so name is missing. """
name: str = ""
specular_color: Color = Color((1.0, 1.0, 1.0)) specular_color: Color = Color((1.0, 1.0, 1.0))
rendertype: Rendertype = Rendertype.NORMAL rendertype: Rendertype = Rendertype.NORMAL

View File

@ -5,10 +5,6 @@ import bpy
from typing import Dict from typing import Dict
from .msh_material import * from .msh_material import *
from .msh_material_utilities import _RENDERTYPES_MAPPING
import os
def gather_materials() -> Dict[str, Material]: def gather_materials() -> Dict[str, Material]:
""" Gathers the Blender materials and returns them as """ Gathers the Blender materials and returns them as
a dictionary of strings and Material objects. """ a dictionary of strings and Material objects. """
@ -26,35 +22,35 @@ def read_material(blender_material: bpy.types.Material) -> Material:
result = Material() result = Material()
if blender_material.swbf_msh_mat is None: if blender_material.swbf_msh is None:
return result return result
props = blender_material.swbf_msh_mat props = blender_material.swbf_msh
result.specular_color = props.specular_color.copy() result.specular_color = props.specular_color.copy()
result.rendertype = _read_material_props_rendertype(props) result.rendertype = _read_material_props_rendertype(props)
result.flags = _read_material_props_flags(props) result.flags = _read_material_props_flags(props)
result.data = _read_material_props_data(props) result.data = _read_material_props_data(props)
result.texture0 = props.diffuse_map
if "UNSUPPORTED" not in props.rendertype:
result.texture0 = os.path.basename(props.diffuse_map)
result.texture1 = _read_normal_map_or_distortion_map_texture(props) result.texture1 = _read_normal_map_or_distortion_map_texture(props)
result.texture2 = _read_detail_texture(props) result.texture2 = _read_detail_texture(props)
result.texture3 = _read_envmap_texture(props) result.texture3 = _read_envmap_texture(props)
else:
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 return result
_RENDERTYPES_MAPPING = {
"NORMAL_BF2": Rendertype.NORMAL,
"SCROLLING_BF2": Rendertype.SCROLLING,
"ENVMAPPED_BF2": Rendertype.ENVMAPPED,
"ANIMATED_BF2": Rendertype.ANIMATED,
"REFRACTION_BF2": Rendertype.REFRACTION,
"BLINK_BF2": Rendertype.BLINK,
"NORMALMAPPED_TILED_BF2": Rendertype.NORMALMAPPED_TILED,
"NORMALMAPPED_ENVMAPPED_BF2": Rendertype.NORMALMAPPED_ENVMAPPED,
"NORMALMAPPED_BF2": Rendertype.NORMALMAPPED,
"NORMALMAPPED_TILED_ENVMAPPED_BF2": Rendertype.NORMALMAPPED_TILED_ENVMAP}
def _read_material_props_rendertype(props) -> Rendertype: def _read_material_props_rendertype(props) -> Rendertype:
if "UNSUPPORTED" in props.rendertype:
return Rendertype(props.rendertype_value)
else:
return _RENDERTYPES_MAPPING[props.rendertype] return _RENDERTYPES_MAPPING[props.rendertype]
def _read_material_props_flags(props) -> MaterialFlags: def _read_material_props_flags(props) -> MaterialFlags:
@ -83,8 +79,6 @@ def _read_material_props_flags(props) -> MaterialFlags:
return flags return flags
def _read_material_props_data(props) -> Tuple[int, int]: def _read_material_props_data(props) -> Tuple[int, int]:
if "UNSUPPORTED" in props.rendertype:
return (props.data_value_0, props.data_value_1)
if "SCROLLING" in props.rendertype: if "SCROLLING" in props.rendertype:
return (props.scroll_speed_u, props.scroll_speed_v) return (props.scroll_speed_u, props.scroll_speed_v)
if "BLINK" in props.rendertype: if "BLINK" in props.rendertype:
@ -98,13 +92,11 @@ def _read_material_props_data(props) -> Tuple[int, int]:
return (props.detail_map_tiling_u, props.detail_map_tiling_v) return (props.detail_map_tiling_u, props.detail_map_tiling_v)
def _read_normal_map_or_distortion_map_texture(props) -> str: def _read_normal_map_or_distortion_map_texture(props) -> str:
if "REFRACTION" in props.rendertype: if "REFRACTION" in props.rendertype:
return os.path.basename(props.distortion_map) return props.distortion_map
if "NORMALMAPPED" in props.rendertype: if "NORMALMAPPED" in props.rendertype:
return os.path.basename(props.normal_map) return props.normal_map
return "" return ""
@ -112,10 +104,10 @@ def _read_detail_texture(props) -> str:
if "REFRACTION" in props.rendertype: if "REFRACTION" in props.rendertype:
return "" return ""
return os.path.basename(props.detail_map) return props.detail_map
def _read_envmap_texture(props) -> str: def _read_envmap_texture(props) -> str:
if "ENVMAPPED" not in props.rendertype: if "ENVMAPPED" not in props.rendertype:
return "" return ""
return os.path.basename(props.environment_map) return props.environment_map

View File

@ -1,322 +0,0 @@
""" 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)
bsdf.inputs["Roughness"].default_value = 1.0
bsdf.inputs["Specular"].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

@ -3,18 +3,10 @@
import bpy import bpy
from bpy.props import StringProperty, BoolProperty, EnumProperty, FloatVectorProperty, IntProperty from bpy.props import StringProperty, BoolProperty, EnumProperty, FloatVectorProperty, IntProperty
from bpy.types import PropertyGroup from bpy.types import PropertyGroup
from .msh_material import *
from .msh_material_ui_strings import * from .msh_material_ui_strings import *
from .msh_material_utilities import _REVERSE_RENDERTYPES_MAPPING
from .msh_material_operators import GenerateMaterialNodesFromSWBFProperties
UI_MATERIAL_RENDERTYPES = ( UI_MATERIAL_RENDERTYPES = (
('NORMAL_BF2', "00 Standard (SWBF2)", UI_RENDERTYPE_NORMAL_BF2_DESC), ('NORMAL_BF2', "00 Normal (SWBF2)", UI_RENDERTYPE_NORMAL_BF2_DESC),
('SCROLLING_BF2', "03 Scrolling (SWBF2)", UI_RENDERTYPE_SCROLLING_BF2_DESC), ('SCROLLING_BF2', "03 Scrolling (SWBF2)", UI_RENDERTYPE_SCROLLING_BF2_DESC),
('ENVMAPPED_BF2', "06 Envmapped (SWBF2)", UI_RENDERTYPE_ENVMAPPED_BF2_DESC), ('ENVMAPPED_BF2', "06 Envmapped (SWBF2)", UI_RENDERTYPE_ENVMAPPED_BF2_DESC),
('ANIMATED_BF2', "07 Animated (SWBF2)", UI_RENDERTYPE_ANIMATED_BF2_DESC), ('ANIMATED_BF2', "07 Animated (SWBF2)", UI_RENDERTYPE_ANIMATED_BF2_DESC),
@ -23,9 +15,7 @@ UI_MATERIAL_RENDERTYPES = (
('NORMALMAPPED_TILED_BF2', "24 Normalmapped Tiled (SWBF2)", UI_RENDERTYPE_NORMALMAPPED_TILED_BF2_DESC), ('NORMALMAPPED_TILED_BF2', "24 Normalmapped Tiled (SWBF2)", UI_RENDERTYPE_NORMALMAPPED_TILED_BF2_DESC),
('NORMALMAPPED_ENVMAPPED_BF2', "26 Normalmapped Envmapped (SWBF2)", UI_RENDERTYPE_NORMALMAPPED_ENVMAPPED_BF2_DESC), ('NORMALMAPPED_ENVMAPPED_BF2', "26 Normalmapped Envmapped (SWBF2)", UI_RENDERTYPE_NORMALMAPPED_ENVMAPPED_BF2_DESC),
('NORMALMAPPED_BF2', "27 Normalmapped (SWBF2)", UI_RENDERTYPE_NORMALMAPPED_BF2_DESC), ('NORMALMAPPED_BF2', "27 Normalmapped (SWBF2)", UI_RENDERTYPE_NORMALMAPPED_BF2_DESC),
('NORMALMAPPED_TILED_ENVMAPPED_BF2', "29 Normalmapped Tiled Envmapped (SWBF2)", UI_RENDERTYPE_NORMALMAPPED_TILED_ENVMAPPED_BF2_DESC), ('NORMALMAPPED_TILED_ENVMAPPED_BF2', "29 Normalmapped Tiled Envmapped (SWBF2)", UI_RENDERTYPE_NORMALMAPPED_TILED_ENVMAPPED_BF2_DESC))
('UNSUPPORTED', "Other (SWBF1/2)", UI_RENDERTYPE_UNSUPPORTED_BF2_DESC))
def _make_anim_length_entry(length): def _make_anim_length_entry(length):
from math import sqrt from math import sqrt
@ -171,43 +161,26 @@ class MaterialProperties(PropertyGroup):
description="The basic diffuse map for the material. The alpha channel " description="The basic diffuse map for the material. The alpha channel "
"is either the Transparency Map, Glow Map or Gloss Map, " "is either the Transparency Map, Glow Map or Gloss Map, "
"depending on the selected rendertype and flags.", "depending on the selected rendertype and flags.",
default="white.tga", default="white.tga")
subtype='FILE_PATH')
detail_map: StringProperty(name="Detail Map", detail_map: StringProperty(name="Detail Map",
description="Detail maps allow you to add in 'detail' to the diffuse " description="Detail maps allow you to add in 'detail' to the diffuse "
"map at runtime. Or they can be used as fake ambient occlusion " "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", normal_map: StringProperty(name="Normal Map",
description="Normal maps can provide added detail from lighting. " description="Normal maps can provide added detail from lighting. "
"If Specular is enabled the alpha channel will be " "If Specular is enabled the alpha channel will be "
"the Gloss Map.", "the Gloss Map.")
subtype='FILE_PATH')
environment_map: StringProperty(name="Environment Map", environment_map: StringProperty(name="Environment Map",
description="Environment map for the material. Provides static " 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", distortion_map: StringProperty(name="Distortion Map",
description="Distortion maps control how Refractive materials " description="Distortion maps control how Refractive materials "
"distort the scene behind them. Should be a normal map " "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")
data_value_1: IntProperty(name="", description="Second data value")
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", 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): class MaterialPropertiesPanel(bpy.types.Panel):
""" Creates a Panel in the Object properties window """ """ Creates a Panel in the Object properties window """
@ -217,21 +190,15 @@ class MaterialPropertiesPanel(bpy.types.Panel):
bl_region_type = 'WINDOW' bl_region_type = 'WINDOW'
bl_context = "material" bl_context = "material"
def draw(self, context): def draw(self, context):
if context.material is None: if context.material is None:
return return
layout = self.layout layout = self.layout
material_props = context.material.swbf_msh_mat material_props = context.material.swbf_msh
layout.prop(material_props, "rendertype") layout.prop(material_props, "rendertype")
if "UNSUPPORTED" in material_props.rendertype:
layout.prop(material_props, "rendertype_value")
layout.prop(material_props, "specular_color") layout.prop(material_props, "specular_color")
if "REFRACTION" not in material_props.rendertype: if "REFRACTION" not in material_props.rendertype:
@ -266,15 +233,11 @@ class MaterialPropertiesPanel(bpy.types.Panel):
elif "NORMALMAPPED_TILED" in material_props.rendertype: elif "NORMALMAPPED_TILED" in material_props.rendertype:
row.prop(material_props, "normal_map_tiling_u") row.prop(material_props, "normal_map_tiling_u")
row.prop(material_props, "normal_map_tiling_v") row.prop(material_props, "normal_map_tiling_v")
elif "UNSUPPORTED" in material_props.rendertype:
row.prop(material_props, "data_value_0")
row.prop(material_props, "data_value_1")
else: else:
row.prop(material_props, "detail_map_tiling_u") row.prop(material_props, "detail_map_tiling_u")
row.prop(material_props, "detail_map_tiling_v") row.prop(material_props, "detail_map_tiling_v")
layout.label(text="Texture Maps: ") layout.label(text="Texture Maps: ")
if "UNSUPPORTED" not in material_props.rendertype:
layout.prop(material_props, "diffuse_map") layout.prop(material_props, "diffuse_map")
if "REFRACTION" not in material_props.rendertype: if "REFRACTION" not in material_props.rendertype:
@ -288,14 +251,3 @@ class MaterialPropertiesPanel(bpy.types.Panel):
if "REFRACTION" in material_props.rendertype: if "REFRACTION" in material_props.rendertype:
layout.prop(material_props, "distortion_map") layout.prop(material_props, "distortion_map")
else:
layout.prop(material_props, "texture_0")
layout.prop(material_props, "texture_1")
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

@ -1,137 +0,0 @@
""" For finding textures and assigning MaterialProperties from entries in a Material """
import bpy
from typing import Dict
from .msh_material_properties import *
from .msh_material import *
from .msh_material_utilities import _REVERSE_RENDERTYPES_MAPPING
from math import sqrt
import os
def find_texture_path(folder_path : str, name : str) -> str:
if not folder_path or not name:
return ""
possible_paths = [
os.path.join(folder_path, name),
os.path.join(folder_path, "PC", name),
os.path.join(folder_path, "pc", name),
os.path.join(folder_path, ".." , name),
]
for possible_path in possible_paths:
if os.path.exists(possible_path):
return possible_path
return name
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:
return
material_properties.rendertype_value = material.rendertype.value
material_properties.specular_color = (material.specular_color[0], material.specular_color[1], material.specular_color[2])
_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, folder_path)
def _fill_material_props_rendertype(material, material_properties):
if material.rendertype in _REVERSE_RENDERTYPES_MAPPING:
material_properties.rendertype = _REVERSE_RENDERTYPES_MAPPING[material.rendertype]
else:
material_properties.rendertype = "UNSUPPORTED"
def _fill_material_props_flags(material, material_properties):
if material.rendertype == Rendertype.REFRACTION:
material_properties.blended_transparency = True
return
flags = material.flags
material_properties.blended_transparency = bool(flags & MaterialFlags.BLENDED_TRANSPARENCY)
material_properties.additive_transparency = bool(flags & MaterialFlags.ADDITIVE_TRANSPARENCY)
material_properties.hardedged_transparency = bool(flags & MaterialFlags.HARDEDGED_TRANSPARENCY)
material_properties.unlit = bool(flags & MaterialFlags.UNLIT)
material_properties.glow = bool(flags & MaterialFlags.GLOW)
material_properties.perpixel = bool(flags & MaterialFlags.PERPIXEL)
material_properties.specular = bool(flags & MaterialFlags.SPECULAR)
material_properties.doublesided = bool(flags & MaterialFlags.DOUBLESIDED)
def _fill_material_props_data(material, material_properties):
material_properties.data_value_0 = material.data[0]
material_properties.data_value_1 = material.data[1]
material_properties.scroll_speed_u = material.data[0]
material_properties.scroll_speed_v = material.data[1]
material_properties.blink_min_brightness = material.data[0]
material_properties.blink_speed = material.data[1]
material_properties.normal_map_tiling_u = material.data[0]
material_properties.normal_map_tiling_v = material.data[1]
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):
anim_length_index = len(UI_MATERIAL_ANIMATION_LENGTHS) - 1
material_properties.animation_length = UI_MATERIAL_ANIMATION_LENGTHS[anim_length_index][0]
material_properties.animation_speed = material.data[1]
material_properties.detail_map_tiling_u = material.data[0]
material_properties.detail_map_tiling_v = material.data[1]
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

View File

@ -1,10 +1,5 @@
""" UI strings that are too long to have in msh_materials_properties.py """ """ UI strings that are too long to have in msh_materials_properties.py """
UI_RENDERTYPE_UNSUPPORTED_BF2_DESC = \
"Unsupported rendertype. The raw values of the material "\
"are fully accessible, but their purpose is unknown. "
UI_RENDERTYPE_DETAIL_MAP_DESC = \ UI_RENDERTYPE_DETAIL_MAP_DESC = \
"Can optionally have a Detail Map." "Can optionally have a Detail Map."

View File

@ -4,23 +4,6 @@ from typing import Dict, List
from .msh_material import * from .msh_material import *
from .msh_model import * from .msh_model import *
_RENDERTYPES_MAPPING = {
"NORMAL_BF2": Rendertype.NORMAL,
"SCROLLING_BF2": Rendertype.SCROLLING,
"ENVMAPPED_BF2": Rendertype.ENVMAPPED,
"ANIMATED_BF2": Rendertype.ANIMATED,
"REFRACTION_BF2": Rendertype.REFRACTION,
"BLINK_BF2": Rendertype.BLINK,
"NORMALMAPPED_TILED_BF2": Rendertype.NORMALMAPPED_TILED,
"NORMALMAPPED_ENVMAPPED_BF2": Rendertype.NORMALMAPPED_ENVMAPPED,
"NORMALMAPPED_BF2": Rendertype.NORMALMAPPED,
"NORMALMAPPED_TILED_ENVMAPPED_BF2": Rendertype.NORMALMAPPED_TILED_ENVMAP}
_REVERSE_RENDERTYPES_MAPPING = {val: key for (key, val) in _RENDERTYPES_MAPPING.items()}
def remove_unused_materials(materials: Dict[str, Material], def remove_unused_materials(materials: Dict[str, Material],
models: List[Model]) -> Dict[str, Material]: models: List[Model]) -> Dict[str, Material]:
""" Given a dictionary of materials and a list of models """ Given a dictionary of materials and a list of models

View File

@ -1,201 +0,0 @@
""" 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.create_normals_split()
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.use_auto_smooth = True
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

@ -2,7 +2,7 @@
saved to a .msh file. """ saved to a .msh file. """
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import List, Tuple, Dict from typing import List
from enum import Enum from enum import Enum
from mathutils import Vector, Quaternion from mathutils import Vector, Quaternion
@ -13,16 +13,11 @@ class ModelType(Enum):
BONE = 3 BONE = 3
STATIC = 4 STATIC = 4
# Maybe there are only for BF1 models (http://www.secretsociety.com/forum/downloads/BF1/BF1%20Mesh%20File%20Format.txt)?
# According to that link #3 is envelope, not bone, maybe that's for TCW or smthg
# CHILDSKIN = 5 # I didnt bother with these, never encountered one and they might need adjustments to vertex data
SHADOWVOLUME = 6 # Pretty common
class CollisionPrimitiveShape(Enum): class CollisionPrimitiveShape(Enum):
SPHERE = 0 SPHERE = 0
ELLIPSOID = 1 # ELLIPSOID = 1
CYLINDER = 2 CYLINDER = 2
MESH = 3 # MESH = 3
BOX = 4 BOX = 4
@dataclass @dataclass
@ -49,14 +44,12 @@ class GeometrySegment:
normals: List[Vector] = field(default_factory=list) normals: List[Vector] = field(default_factory=list)
colors: List[List[float]] = None colors: List[List[float]] = None
texcoords: List[Vector] = field(default_factory=list) texcoords: List[Vector] = field(default_factory=list)
weights: List[List[VertexWeight]] = None weights: List[List[VertexWeight]] = None
polygons: List[List[int]] = field(default_factory=list) polygons: List[List[int]] = field(default_factory=list)
triangles: List[List[int]] = field(default_factory=list) triangles: List[List[int]] = field(default_factory=list)
triangle_strips: List[List[int]] = None triangle_strips: List[List[int]] = None
@dataclass @dataclass
class CollisionPrimitive: class CollisionPrimitive:
""" Class representing a 'SWCI' section in a .msh file. """ """ Class representing a 'SWCI' section in a .msh file. """
@ -73,7 +66,7 @@ class Model:
name: str = "Model" name: str = "Model"
parent: str = "" parent: str = ""
model_type: ModelType = ModelType.NULL model_type: ModelType = ModelType.NULL
hidden: bool = False hidden: bool = True
transform: ModelTransform = field(default_factory=ModelTransform) transform: ModelTransform = field(default_factory=ModelTransform)
@ -81,29 +74,3 @@ class Model:
geometry: List[GeometrySegment] = None geometry: List[GeometrySegment] = None
collisionprimitive: CollisionPrimitive = None collisionprimitive: CollisionPrimitive = None
@dataclass
class RotationFrame:
index : int = 0
rotation : Quaternion = field(default_factory=Quaternion)
@dataclass
class TranslationFrame:
index : int = 0
translation : Vector = field(default_factory=Vector)
@dataclass
class Animation:
""" Class representing 'CYCL' + 'KFR3' sections in a .msh file """
name: str = "fullanimation"
bone_frames: Dict[int, Tuple[List[TranslationFrame], List[RotationFrame]]] = field(default_factory=dict)
framerate: float = 29.97
start_index : int = 0
end_index : int = 0

View File

@ -9,13 +9,12 @@ from itertools import zip_longest
from .msh_model import * from .msh_model import *
from .msh_model_utilities import * from .msh_model_utilities import *
from .msh_utilities import * from .msh_utilities import *
from .msh_skeleton_utilities import *
SKIPPED_OBJECT_TYPES = {"LATTICE", "CAMERA", "LIGHT", "SPEAKER", "LIGHT_PROBE"} SKIPPED_OBJECT_TYPES = {"LATTICE", "CAMERA", "LIGHT", "SPEAKER", "LIGHT_PROBE"}
MESH_OBJECT_TYPES = {"MESH", "CURVE", "SURFACE", "META", "FONT", "GPENCIL"} MESH_OBJECT_TYPES = {"MESH", "CURVE", "SURFACE", "META", "FONT", "GPENCIL"}
MAX_MSH_VERTEX_COUNT = 32767 MAX_MSH_VERTEX_COUNT = 32767
def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool) -> Tuple[List[Model], bpy.types.Object]: def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]:
""" Gathers the Blender objects from the current scene and returns them as a list of """ Gathers the Blender objects from the current scene and returns them as a list of
Model objects. """ Model objects. """
@ -24,107 +23,35 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool
models_list: List[Model] = [] 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
# Non-bone objects that will be exported
blender_objects_to_export = []
# 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): for uneval_obj in select_objects(export_target):
if uneval_obj.type in SKIPPED_OBJECT_TYPES and uneval_obj.name not in parents:
continue
if get_is_model_hidden(uneval_obj): if apply_modifiers:
blender_objects_to_hide.add(uneval_obj.name) obj = uneval_obj.evaluated_get(depsgraph)
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: else:
pass obj = uneval_obj
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) check_for_bad_lod_suffix(obj)
# Test for a mesh object that should be a BONE on export. if obj.type == "ARMATURE":
# If so, we inject geometry into the BONE while not modifying it's transform/name models_list += expand_armature(obj)
# and remove it from the set of BONES without geometry (pure).
if obj.name in pure_bones_from_armature: local_translation, local_rotation, _ = obj.matrix_local.decompose()
model = pure_bones_from_armature.pop(obj.name)
else:
model = Model() model = Model()
model.name = obj.name model.name = obj.name
model.model_type = ModelType.NULL if skeleton_only else get_model_type(obj, armature_found) model.model_type = get_model_type(obj)
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
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.rotation = convert_rotation_space(local_rotation)
model.transform.translation = convert_vector_space(local_translation) model.transform.translation = convert_vector_space(local_translation)
if obj.type in MESH_OBJECT_TYPES and not skeleton_only: if obj.parent is not None:
model.parent = obj.parent.name
# 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 ]
if obj.type in MESH_OBJECT_TYPES:
mesh = obj.to_mesh() mesh = obj.to_mesh()
model.geometry = create_mesh_geometry(mesh, valid_vgroup_indices) model.geometry = create_mesh_geometry(mesh, obj.vertex_groups)
obj.to_mesh_clear() obj.to_mesh_clear()
_, _, world_scale = obj.matrix_world.decompose() _, _, world_scale = obj.matrix_world.decompose()
@ -140,15 +67,12 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool
if get_is_collision_primitive(obj): if get_is_collision_primitive(obj):
model.collisionprimitive = get_collision_primitive(obj) model.collisionprimitive = get_collision_primitive(obj)
model.hidden = model.name in blender_objects_to_hide if obj.vertex_groups:
model.bone_map = [group.name for group in obj.vertex_groups]
models_list.append(model) models_list.append(model)
# We removed all composite bones after looking through the objects, return models_list
# so the bones left are all pure and we add them all here.
return (models_list + list(pure_bones_from_armature.values()), armature_found)
def create_parents_set() -> Set[str]: def create_parents_set() -> Set[str]:
""" Creates a set with the names of the Blender objects from the current scene """ Creates a set with the names of the Blender objects from the current scene
@ -162,11 +86,11 @@ def create_parents_set() -> Set[str]:
return parents return parents
def create_mesh_geometry(mesh: bpy.types.Mesh, valid_vgroup_indices: Set[int]) -> List[GeometrySegment]: def create_mesh_geometry(mesh: bpy.types.Mesh, has_weights: bool) -> List[GeometrySegment]:
""" Creates a list of GeometrySegment objects from a Blender mesh. """ Creates a list of GeometrySegment objects from a Blender mesh.
Does NOT create triangle strips in the GeometrySegment however. """ Does NOT create triangle strips in the GeometrySegment however. """
# We have to do this for all meshes to account for sharp edges if mesh.has_custom_normals:
mesh.calc_normals_split() mesh.calc_normals_split()
mesh.validate_material_indices() mesh.validate_material_indices()
@ -179,18 +103,18 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, valid_vgroup_indices: Set[int]) -
vertex_remap: List[Dict[Tuple[int, int], int]] = [dict() for i in range(material_count)] 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)] polygons: List[Set[int]] = [set() for i in range(material_count)]
if mesh.color_attributes.active_color is not None: if mesh.vertex_colors.active is not None:
for segment in segments: for segment in segments:
segment.colors = [] segment.colors = []
if valid_vgroup_indices: if has_weights:
for segment in segments: for segment in segments:
segment.weights = [] segment.weights = []
for segment, material in zip(segments, mesh.materials): for segment, material in zip(segments, mesh.materials):
segment.material_name = material.name segment.material_name = material.name
def add_vertex(material_index: int, vertex_index: int, loop_index: int) -> int: def add_vertex(material_index: int, vertex_index: int, loop_index: int, use_smooth_normal: bool, face_normal: Vector) -> int:
nonlocal segments, vertex_remap nonlocal segments, vertex_remap
vertex_cache_miss_index = -1 vertex_cache_miss_index = -1
@ -198,8 +122,15 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, valid_vgroup_indices: Set[int]) -
cache = vertex_cache[material_index] cache = vertex_cache[material_index]
remap = vertex_remap[material_index] remap = vertex_remap[material_index]
# always use loop normals since we always calculate a custom split set vertex_normal: Vector
vertex_normal = Vector( mesh.loops[loop_index].normal )
if use_smooth_normal or mesh.use_auto_smooth:
if mesh.has_custom_normals:
vertex_normal = mesh.loops[loop_index].normal
else:
vertex_normal = mesh.vertices[vertex_index].normal
else:
vertex_normal = face_normal
def get_cache_vertex(): def get_cache_vertex():
yield mesh.vertices[vertex_index].co.x yield mesh.vertices[vertex_index].co.x
@ -215,14 +146,11 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, valid_vgroup_indices: Set[int]) -
yield mesh.uv_layers.active.data[loop_index].uv.y yield mesh.uv_layers.active.data[loop_index].uv.y
if segment.colors is not None: if segment.colors is not None:
data_type = mesh.color_attributes.active_color.data_type for v in mesh.vertex_colors.active.data[loop_index].color:
if data_type == "FLOAT_COLOR" or data_type == "BYTE_COLOR":
for v in mesh.color_attributes.active_color.data[vertex_index].color:
yield v yield v
if segment.weights is not None: if segment.weights is not None:
for v in mesh.vertices[vertex_index].groups: for v in mesh.vertices[vertex_index].groups:
if v.group in valid_vgroup_indices:
yield v.group yield v.group
yield v.weight yield v.weight
@ -247,22 +175,21 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, valid_vgroup_indices: Set[int]) -
segment.texcoords.append(mesh.uv_layers.active.data[loop_index].uv.copy()) segment.texcoords.append(mesh.uv_layers.active.data[loop_index].uv.copy())
if segment.colors is not None: if segment.colors is not None:
data_type = mesh.color_attributes.active_color.data_type segment.colors.append(list(mesh.vertex_colors.active.data[loop_index].color))
if data_type == "FLOAT_COLOR" or data_type == "BYTE_COLOR":
segment.colors.append(list(mesh.color_attributes.active_color.data[vertex_index].color))
if segment.weights is not None: if segment.weights is not None:
groups = mesh.vertices[vertex_index].groups groups = mesh.vertices[vertex_index].groups
segment.weights.append([VertexWeight(v.weight, v.group) for v in groups if v.group in valid_vgroup_indices])
segment.weights.append([VertexWeight(v.weight, v.group) for v in groups])
return new_index return new_index
for tri in mesh.loop_triangles: for tri in mesh.loop_triangles:
polygons[tri.material_index].add(tri.polygon_index) polygons[tri.material_index].add(tri.polygon_index)
segments[tri.material_index].triangles.append([ segments[tri.material_index].triangles.append([
add_vertex(tri.material_index, tri.vertices[0], tri.loops[0]), 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]), 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])]) add_vertex(tri.material_index, tri.vertices[2], tri.loops[2], tri.use_smooth, tri.normal)])
for segment, remap, polys in zip(segments, vertex_remap, polygons): for segment, remap, polys in zip(segments, vertex_remap, polygons):
for poly_index in polys: for poly_index in polys:
@ -272,29 +199,12 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, valid_vgroup_indices: Set[int]) -
return segments return segments
def get_model_type(obj: bpy.types.Object, armature_found: bpy.types.Object) -> ModelType: def get_model_type(obj: bpy.types.Object) -> ModelType:
""" Get the ModelType for a Blender object. """ """ Get the ModelType for a Blender object. """
if obj.type in MESH_OBJECT_TYPES: if obj.type in MESH_OBJECT_TYPES:
# Objects can have vgroups for non-skinning purposes. if obj.vertex_groups:
# 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.SKIN
return ModelType.STATIC
else: else:
return ModelType.STATIC return ModelType.STATIC
@ -303,13 +213,8 @@ def get_model_type(obj: bpy.types.Object, armature_found: bpy.types.Object) -> M
def get_is_model_hidden(obj: bpy.types.Object) -> bool: def get_is_model_hidden(obj: bpy.types.Object) -> bool:
""" Gets if a Blender object should be marked as hidden in the .msh file. """ """ Gets if a Blender object should be marked as hidden in the .msh file. """
if obj.hide_get():
return True
name = obj.name.lower() name = obj.name.lower()
if name.startswith("c_"):
return True
if name.startswith("sv_"): if name.startswith("sv_"):
return True return True
if name.startswith("p_"): if name.startswith("p_"):
@ -354,7 +259,10 @@ def get_collision_primitive(obj: bpy.types.Object) -> CollisionPrimitive:
primitive.radius = max(obj.dimensions[0], obj.dimensions[1], obj.dimensions[2]) * 0.5 primitive.radius = max(obj.dimensions[0], obj.dimensions[1], obj.dimensions[2]) * 0.5
elif primitive.shape == CollisionPrimitiveShape.CYLINDER: elif primitive.shape == CollisionPrimitiveShape.CYLINDER:
primitive.radius = max(obj.dimensions[0], obj.dimensions[1]) * 0.5 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.height = obj.dimensions[2] primitive.height = obj.dimensions[2]
elif primitive.shape == CollisionPrimitiveShape.BOX: elif primitive.shape == CollisionPrimitiveShape.BOX:
primitive.radius = obj.dimensions[0] * 0.5 primitive.radius = obj.dimensions[0] * 0.5
@ -363,21 +271,10 @@ def get_collision_primitive(obj: bpy.types.Object) -> CollisionPrimitive:
return primitive return primitive
def get_collision_primitive_shape(obj: bpy.types.Object) -> CollisionPrimitiveShape: def get_collision_primitive_shape(obj: bpy.types.Object) -> CollisionPrimitiveShape:
""" Gets the CollisionPrimitiveShape of an object or raises an error if """ Gets the CollisionPrimitiveShape of an object or raises an error if
it can't. """ 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() name = obj.name.lower()
if "sphere" in name or "sphr" in name or "spr" in name: if "sphere" in name or "sphr" in name or "spr" in name:
@ -389,7 +286,6 @@ def get_collision_primitive_shape(obj: bpy.types.Object) -> CollisionPrimitiveSh
raise RuntimeError(f"Object '{obj.name}' has no primitive type specified in it's name!") raise RuntimeError(f"Object '{obj.name}' has no primitive type specified in it's name!")
def check_for_bad_lod_suffix(obj: bpy.types.Object): def check_for_bad_lod_suffix(obj: bpy.types.Object):
""" Checks if the object has an LOD suffix that is known to be ignored by """ """ Checks if the object has an LOD suffix that is known to be ignored by """
@ -446,21 +342,10 @@ def select_objects(export_target: str) -> List[bpy.types.Object]:
return objects + parents return objects + parents
def expand_armature(obj: bpy.types.Object) -> List[Model]:
bones: List[Model] = []
for bone in obj.data.bones:
def expand_armature(armature: bpy.types.Object) -> Dict[str, Model]:
proper_BONES = get_real_BONES(armature)
bones: Dict[str, Model] = {}
for bone in armature.data.bones:
model = Model() model = Model()
transform = bone.matrix_local transform = bone.matrix_local
@ -468,40 +353,25 @@ def expand_armature(armature: bpy.types.Object) -> Dict[str, Model]:
if bone.parent: if bone.parent:
transform = bone.parent.matrix_local.inverted() @ transform transform = bone.parent.matrix_local.inverted() @ transform
model.parent = bone.parent.name model.parent = bone.parent.name
# If the bone has no parent_bone:
# set model parent to SKIN object if there is one
# set model parent to armature parent if there is one
else: else:
model.parent = obj.name
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
parent_obj = child_obj
break
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() local_translation, local_rotation, _ = transform.decompose()
model.model_type = ModelType.BONE if bone.name in proper_BONES else ModelType.NULL model.model_type = ModelType.BONE
model.name = bone.name model.name = bone.name
model.hidden = True
model.transform.rotation = convert_rotation_space(local_rotation) model.transform.rotation = convert_rotation_space(local_rotation)
model.transform.translation = convert_vector_space(local_translation) model.transform.translation = convert_vector_space(local_translation)
bones[bone.name] = model bones.append(model)
return bones return bones
def convert_vector_space(vec: Vector) -> Vector:
return Vector((-vec.x, vec.z, vec.y))
def convert_scale_space(vec: Vector) -> Vector:
return Vector(vec.xzy)
def convert_rotation_space(quat: Quaternion) -> Quaternion:
return Quaternion((-quat.w, quat.x, -quat.z, -quat.y))

View File

@ -3,60 +3,8 @@
from typing import List from typing import List
from .msh_model import * from .msh_model import *
from .msh_utilities import * from .msh_utilities import *
import mathutils
import math
from mathutils import Vector, Matrix 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
dummy_seg = GeometrySegment()
dummy_seg.material_name = ""
dummy_seg.positions = [Vector((0.0,0.1,0.0)), Vector((0.1,0.0,0.0)), Vector((0.0,0.0,0.1))]
dummy_seg.normals = [Vector((0.0,1.0,0.0)), Vector((1.0,0.0,0.0)), Vector((0.0,0.0,1.0))]
dummy_seg.texcoords = [Vector((0.1,0.1)), Vector((0.2,0.2)), Vector((0.3,0.3))]
tri = [[0,1,2]]
dummy_seg.triangles = tri
dummy_seg.polygons = tri
dummy_seg.triangle_strips = tri
model.geometry = [dummy_seg]
model.model_type = ModelType.STATIC
def convert_vector_space(vec: Vector) -> Vector:
return Vector((-vec.x, vec.z, vec.y))
def convert_scale_space(vec: Vector) -> Vector:
return Vector(vec.xzy)
def convert_rotation_space(quat: Quaternion) -> Quaternion:
return Quaternion((-quat.w, quat.x, -quat.z, -quat.y))
def model_transform_to_matrix(transform: ModelTransform):
return Matrix.Translation(convert_vector_space(transform.translation)) @ convert_rotation_space(transform.rotation).to_matrix().to_4x4()
def scale_segments(scale: Vector, segments: List[GeometrySegment]): def scale_segments(scale: Vector, segments: List[GeometrySegment]):
""" Scales are positions in the GeometrySegment list. """ """ Scales are positions in the GeometrySegment list. """
@ -166,4 +114,3 @@ def is_model_name_unused(name: str, models: List[Model]) -> bool:
return False return False
return True return True

View File

@ -4,15 +4,17 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import List, Dict from typing import List, Dict
from copy import copy from copy import copy
import bpy import bpy
from mathutils import Vector from mathutils import Vector
from .msh_model import Model
from .msh_model import Model, Animation, ModelType from .msh_model_gather import gather_models
from .msh_model_utilities import sort_by_parent, has_multiple_root_models, reparent_model_roots, get_model_world_matrix
from .msh_model_triangle_strips import create_models_triangle_strips
from .msh_material import * from .msh_material import *
from .msh_material_gather import gather_materials
from .msh_material_utilities import remove_unused_materials
from .msh_utilities import * from .msh_utilities import *
@dataclass @dataclass
class SceneAABB: class SceneAABB:
""" Class representing an axis-aligned bounding box. """ """ Class representing an axis-aligned bounding box. """
@ -42,6 +44,53 @@ class Scene:
materials: Dict[str, Material] = field(default_factory=dict) materials: Dict[str, Material] = field(default_factory=dict)
models: List[Model] = field(default_factory=list) models: List[Model] = field(default_factory=list)
animation: Animation = None def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_target: str) -> Scene:
""" Create a msh Scene from the active Blender scene. """
skeleton: List[int] = field(default_factory=list) scene = Scene()
scene.name = bpy.context.scene.name
scene.materials = gather_materials()
scene.models = gather_models(apply_modifiers=apply_modifiers, export_target=export_target)
scene.models = sort_by_parent(scene.models)
if generate_triangle_strips:
scene.models = create_models_triangle_strips(scene.models)
else:
for model in scene.models:
if model.geometry:
for segment in model.geometry:
segment.triangle_strips = segment.triangles
if has_multiple_root_models(scene.models):
scene.models = reparent_model_roots(scene.models)
scene.materials = remove_unused_materials(scene.materials, scene.models)
return scene
def create_scene_aabb(scene: Scene) -> SceneAABB:
""" Create a SceneAABB for a Scene. """
global_aabb = SceneAABB()
for model in scene.models:
if model.geometry is None or model.hidden:
continue
model_world_matrix = get_model_world_matrix(model, scene.models)
model_aabb = SceneAABB()
for segment in model.geometry:
segment_aabb = SceneAABB()
for pos in segment.positions:
segment_aabb.integrate_position(model_world_matrix @ pos)
model_aabb.integrate_aabb(segment_aabb)
global_aabb.integrate_aabb(model_aabb)
return global_aabb

View File

@ -1,472 +0,0 @@
""" Contains functions for extracting a scene from a .msh file"""
from itertools import islice
from typing import Dict
from .msh_scene import Scene
from .msh_model import *
from .msh_material import *
from .msh_utilities import *
from .crc import *
from .chunked_file_reader import Reader
# Current model position
model_counter = 0
# Used to remap MNDX to the MODL's actual position
mndx_remap : Dict[int, int] = {}
# How much to print
debug_level = 0
'''
Debug levels just indicate how much info should be printed.
0 = nothing
1 = just blurbs about valuable info in the chunks
2 = #1 + full chunk structure
'''
def read_scene(input_file, anim_only=False, debug=0) -> Scene:
global debug_level
debug_level = debug
scene = Scene()
scene.models = []
scene.materials = {}
global mndx_remap
mndx_remap = {}
global model_counter
model_counter = 0
with Reader(file=input_file, debug=debug_level>0) as head:
head.skip_until("HEDR")
with head.read_child() as hedr:
while hedr.could_have_child():
next_header = hedr.peak_next_header()
if next_header == "MSH2":
with hedr.read_child() as msh2:
if not anim_only:
materials_list = []
while (msh2.could_have_child()):
next_header = msh2.peak_next_header()
if next_header == "SINF":
with msh2.read_child() as sinf:
pass
elif next_header == "MATL":
with msh2.read_child() as matl:
materials_list += _read_matl_and_get_materials_list(matl)
for i,mat in enumerate(materials_list):
scene.materials[mat.name] = mat
elif next_header == "MODL":
with msh2.read_child() as modl:
scene.models.append(_read_modl(modl, materials_list))
else:
msh2.skip_bytes(1)
elif next_header == "SKL2":
with hedr.read_child() as skl2:
num_bones = skl2.read_u32()
scene.skeleton = [skl2.read_u32(5)[0] for i in range(num_bones)]
elif next_header == "ANM2":
with hedr.read_child() as anm2:
scene.animation = _read_anm2(anm2)
else:
hedr.skip_bytes(1)
# Print models in skeleton
if scene.skeleton and debug_level > 0:
print("Skeleton models: ")
for model in scene.models:
for i in range(len(scene.skeleton)):
if to_crc(model.name) == scene.skeleton[i]:
print("\t" + model.name)
if model.model_type == ModelType.SKIN:
scene.skeleton.pop(i)
break
'''
Iterate through every vertex weight in the scene and
change its index to directly reference its bone's index.
It will reference the MNDX of its bone's MODL by default.
'''
for model in scene.models:
if model.geometry:
for seg in model.geometry:
if seg.weights:
for weight_set in seg.weights:
for vweight in weight_set:
if vweight.bone in mndx_remap:
vweight.bone = mndx_remap[vweight.bone]
else:
vweight.bone = 0
return scene
def _read_matl_and_get_materials_list(matl: Reader) -> List[Material]:
materials_list: List[Material] = []
num_mats = matl.read_u32()
for _ in range(num_mats):
with matl.read_child() as matd:
materials_list.append(_read_matd(matd))
return materials_list
def _read_matd(matd: Reader) -> Material:
mat = Material()
while matd.could_have_child():
next_header = matd.peak_next_header()
if next_header == "NAME":
with matd.read_child() as name:
mat.name = name.read_string()
elif next_header == "DATA":
with matd.read_child() as data:
data.read_f32(4) # Diffuse Color (Seams to get ignored by modelmunge)
mat.specular_color = data.read_f32(4)
data.read_f32(4) # Ambient Color (Seams to get ignored by modelmunge and Zero(?))
data.read_f32() # Specular Exponent/Decay (Gets ignored by RedEngine in SWBFII for all known materials)
elif next_header == "ATRB":
with matd.read_child() as atrb:
mat.flags = MaterialFlags(atrb.read_u8())
mat.rendertype = Rendertype(atrb.read_u8())
mat.data = atrb.read_u8(2)
elif next_header == "TX0D":
with matd.read_child() as tx0d:
if tx0d.bytes_remaining() > 0:
mat.texture0 = tx0d.read_string()
elif next_header == "TX1D":
with matd.read_child() as tx1d:
if tx1d.bytes_remaining() > 0:
mat.texture1 = tx1d.read_string()
elif next_header == "TX2D":
with matd.read_child() as tx2d:
if tx2d.bytes_remaining() > 0:
mat.texture2 = tx2d.read_string()
elif next_header == "TX3D":
with matd.read_child() as tx3d:
if tx3d.bytes_remaining() > 0:
mat.texture3 = tx3d.read_string()
else:
matd.skip_bytes(1)
return mat
def _read_modl(modl: Reader, materials_list: List[Material]) -> Model:
model = Model()
while modl.could_have_child():
next_header = modl.peak_next_header()
if next_header == "MTYP":
with modl.read_child() as mtyp:
model.model_type = ModelType(mtyp.read_u32())
elif next_header == "MNDX":
with modl.read_child() as mndx:
index = mndx.read_u32()
global model_counter
global mndx_remap
if index not in mndx_remap:
mndx_remap[index] = model_counter
model_counter += 1
elif next_header == "NAME":
with modl.read_child() as name:
model.name = name.read_string()
elif next_header == "PRNT":
with modl.read_child() as prnt:
model.parent = prnt.read_string()
elif next_header == "FLGS":
with modl.read_child() as flgs:
model.hidden = flgs.read_u32()
elif next_header == "TRAN":
with modl.read_child() as tran:
model.transform = _read_tran(tran)
elif next_header == "GEOM":
model.geometry = []
envelope = []
with modl.read_child() as geom:
while geom.could_have_child():
next_header_geom = geom.peak_next_header()
if next_header_geom == "SEGM":
with geom.read_child() as segm:
model.geometry.append(_read_segm(segm, materials_list))
elif next_header_geom == "ENVL":
with geom.read_child() as envl:
num_indicies = envl.read_u32()
envelope += [envl.read_u32() for _ in range(num_indicies)]
elif next_header_geom == "CLTH":
with geom.read_child() as clth:
pass
else:
geom.skip_bytes(1)
for seg in model.geometry:
if seg.weights and envelope:
for weight_set in seg.weights:
for vertex_weight in weight_set:
vertex_weight.bone = envelope[vertex_weight.bone]
elif next_header == "SWCI":
prim = CollisionPrimitive()
with modl.read_child() as swci:
prim.shape = CollisionPrimitiveShape(swci.read_u32())
prim.radius = swci.read_f32()
prim.height = swci.read_f32()
prim.length = swci.read_f32()
model.collisionprimitive = prim
else:
modl.skip_bytes(1)
return model
def _read_tran(tran: Reader) -> ModelTransform:
xform = ModelTransform()
tran.skip_bytes(12) #ignore scale
xform.rotation = tran.read_quat()
xform.translation = tran.read_vec()
return xform
def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment:
geometry_seg = GeometrySegment()
while segm.could_have_child():
next_header = segm.peak_next_header()
if next_header == "MATI":
with segm.read_child() as mati:
geometry_seg.material_name = materials_list[mati.read_u32()].name
elif next_header == "POSL":
with segm.read_child() as posl:
num_positions = posl.read_u32()
for _ in range(num_positions):
geometry_seg.positions.append(Vector(posl.read_f32(3)))
elif next_header == "NRML":
with segm.read_child() as nrml:
num_normals = nrml.read_u32()
for _ in range(num_positions):
geometry_seg.normals.append(Vector(nrml.read_f32(3)))
elif next_header == "CLRL":
geometry_seg.colors = []
with segm.read_child() as clrl:
num_colors = clrl.read_u32()
for _ in range(num_colors):
geometry_seg.colors += unpack_color(clrl.read_u32())
elif next_header == "UV0L":
with segm.read_child() as uv0l:
num_texcoords = uv0l.read_u32()
for _ in range(num_texcoords):
geometry_seg.texcoords.append(Vector(uv0l.read_f32(2)))
# 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:
num_tris = ndxt.read_u32()
for _ in range(num_tris):
geometry_seg.triangles.append(ndxt.read_u16(3))
# Try catch for safety's sake
elif next_header == "STRP":
strips : List[List[int]] = []
with segm.read_child() as strp:
try:
num_indicies = strp.read_u32()
indices = strp.read_u16(num_indicies)
strip_indices = []
for i in range(num_indicies - 1):
if indices[i] & 0x8000 > 0 and indices[i+1] & 0x8000 > 0:
strip_indices.append(i)
strip_indices.append(num_indicies)
for i in range(len(strip_indices) - 1):
start = strip_indices[i]
end = strip_indices[i+1]
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 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)
elif next_header == "WGHT":
with segm.read_child() as wght:
geometry_seg.weights = []
num_weights = wght.read_u32()
for _ in range(num_weights):
weight_set = []
for _ in range(4):
index = wght.read_u32()
value = wght.read_f32()
if value > 0.000001:
weight_set.append(VertexWeight(value,index))
geometry_seg.weights.append(weight_set)
else:
segm.skip_bytes(1)
return geometry_seg
def _read_anm2(anm2: Reader) -> Animation:
anim = Animation()
while anm2.could_have_child():
next_header = anm2.peak_next_header()
if next_header == "CYCL":
with anm2.read_child() as cycl:
# Dont even know what CYCL's data does. Tried playing
# with the values but didn't change anything in zenasset or ingame...
# Besides num_anims, which is never > 1 for any SWBF1/2 mshs I've seen
'''
num_anims = cycl.read_u32()
for _ in range(num_anims):
cycl.skip_bytes(64)
print("CYCL play style {}".format(cycl.read_u32(4)[1]))
'''
pass
elif next_header == "KFR3":
with anm2.read_child() as kfr3:
num_bones = kfr3.read_u32()
bone_crcs = []
for _ in range(num_bones):
bone_crc = kfr3.read_u32()
bone_crcs.append(bone_crc)
frames = ([],[])
frametype = kfr3.read_u32()
num_loc_frames = kfr3.read_u32()
num_rot_frames = kfr3.read_u32()
for i in range(num_loc_frames):
frames[0].append(TranslationFrame(kfr3.read_u32(), kfr3.read_vec()))
for i in range(num_rot_frames):
frames[1].append(RotationFrame(kfr3.read_u32(), kfr3.read_quat()))
anim.bone_frames[bone_crc] = frames
else:
anm2.skip_bytes(1)
return anim

View File

@ -2,16 +2,12 @@
from itertools import islice from itertools import islice
from typing import Dict from typing import Dict
from .msh_scene import Scene from .msh_scene import Scene, create_scene_aabb
from .msh_scene_utilities import create_scene_aabb
from .msh_model import * from .msh_model import *
from .msh_material import * from .msh_material import *
from .msh_writer import Writer from .msh_writer import Writer
from .msh_utilities import * from .msh_utilities import *
from .crc import *
def save_scene(output_file, scene: Scene): def save_scene(output_file, scene: Scene):
""" Saves scene to the supplied file. """ """ Saves scene to the supplied file. """
@ -21,7 +17,7 @@ def save_scene(output_file, scene: Scene):
with msh2.create_child("SINF") as sinf: with msh2.create_child("SINF") as sinf:
_write_sinf(sinf, scene) _write_sinf(sinf, scene)
model_index: Dict[str, int] = {model.name:(i+1) for i, model in enumerate(scene.models)} model_index: Dict[str, int] = {model.name:i for i, model in enumerate(scene.models)}
material_index: Dict[str, int] = {} material_index: Dict[str, int] = {}
with msh2.create_child("MATL") as matl: with msh2.create_child("MATL") as matl:
@ -31,23 +27,6 @@ def save_scene(output_file, scene: Scene):
with msh2.create_child("MODL") as modl: with msh2.create_child("MODL") as modl:
_write_modl(modl, model, index, material_index, model_index) _write_modl(modl, model, index, material_index, model_index)
# Contrary to earlier belief, anim/skel info does not need to be exported for animated models
# BUT, unless a model is a BONE, it wont animate!
# This is not necessary when exporting animations. When exporting animations, the following
# chunks are necessary and the animated models can be marked as NULLs
if scene.animation is not None:
# Seems as though SKL2 is wholly unneccessary from SWBF's perspective (for models and anims),
# but it is there in all stock models/anims
with hedr.create_child("SKL2") as skl2:
_write_skl2(skl2, scene.animation)
# Def not necessary, including anyways
with hedr.create_child("BLN2") as bln2:
_write_bln2(bln2, scene.animation)
with hedr.create_child("ANM2") as anm2:
_write_anm2(anm2, scene.animation)
with hedr.create_child("CL1L"): with hedr.create_child("CL1L"):
pass pass
@ -98,7 +77,7 @@ def _write_matd(matd: Writer, material_name: str, material: Material):
data.write_f32(1.0, 1.0, 1.0, 1.0) # Diffuse Color (Seams to get ignored by modelmunge) data.write_f32(1.0, 1.0, 1.0, 1.0) # Diffuse Color (Seams to get ignored by modelmunge)
data.write_f32(material.specular_color[0], material.specular_color[1], data.write_f32(material.specular_color[0], material.specular_color[1],
material.specular_color[2], 1.0) material.specular_color[2], 1.0)
data.write_f32(1.0, 1.0, 1.0, 1.0) # Ambient Color (Seams to get ignored by modelmunge and Zero(?)) data.write_f32(0.0, 0.0, 0.0, 1.0) # Ambient Color (Seams to get ignored by modelmunge and Zero(?))
data.write_f32(50.0) # Specular Exponent/Decay (Gets ignored by RedEngine in SWBFII for all known materials) data.write_f32(50.0) # Specular Exponent/Decay (Gets ignored by RedEngine in SWBFII for all known materials)
with matd.create_child("ATRB") as atrb: with matd.create_child("ATRB") as atrb:
atrb.write_u8(material.flags.value) atrb.write_u8(material.flags.value)
@ -124,7 +103,7 @@ def _write_modl(modl: Writer, model: Model, index: int, material_index: Dict[str
mtyp.write_u32(model.model_type.value) mtyp.write_u32(model.model_type.value)
with modl.create_child("MNDX") as mndx: with modl.create_child("MNDX") as mndx:
mndx.write_u32(index + 1) mndx.write_u32(index)
with modl.create_child("NAME") as name: with modl.create_child("NAME") as name:
name.write_string(model.name) name.write_string(model.name)
@ -142,12 +121,6 @@ def _write_modl(modl: Writer, model: Model, index: int, material_index: Dict[str
if model.geometry is not None: if model.geometry is not None:
with modl.create_child("GEOM") as geom: with modl.create_child("GEOM") as geom:
with geom.create_child("BBOX") as bbox:
bbox.write_f32(0.0, 0.0, 0.0, 1.0)
bbox.write_f32(0, 0, 0)
bbox.write_f32(1.0,1.0,1.0,2.0)
for segment in model.geometry: for segment in model.geometry:
with geom.create_child("SEGM") as segm: with geom.create_child("SEGM") as segm:
_write_segm(segm, segment, material_index) _write_segm(segm, segment, material_index)
@ -186,7 +159,7 @@ def _write_segm(segm: Writer, segment: GeometrySegment, material_index: Dict[str
with segm.create_child("NRML") as nrml: with segm.create_child("NRML") as nrml:
nrml.write_u32(len(segment.normals)) nrml.write_u32(len(segment.normals))
for i,normal in enumerate(segment.normals): for normal in segment.normals:
nrml.write_f32(normal.x, normal.y, normal.z) nrml.write_f32(normal.x, normal.y, normal.z)
if segment.colors is not None: if segment.colors is not None:
@ -196,7 +169,6 @@ def _write_segm(segm: Writer, segment: GeometrySegment, material_index: Dict[str
for color in segment.colors: for color in segment.colors:
clrl.write_u32(pack_color(color)) clrl.write_u32(pack_color(color))
if segment.texcoords is not None:
with segm.create_child("UV0L") as uv0l: with segm.create_child("UV0L") as uv0l:
uv0l.write_u32(len(segment.texcoords)) uv0l.write_u32(len(segment.texcoords))
@ -227,9 +199,6 @@ def _write_segm(segm: Writer, segment: GeometrySegment, material_index: Dict[str
for index in islice(strip, 2, len(strip)): for index in islice(strip, 2, len(strip)):
strp.write_u16(index) strp.write_u16(index)
'''
SKINNING CHUNKS
'''
def _write_wght(wght: Writer, weights: List[List[VertexWeight]]): def _write_wght(wght: Writer, weights: List[List[VertexWeight]]):
wght.write_u32(len(weights)) wght.write_u32(len(weights))
@ -246,62 +215,6 @@ def _write_wght(wght: Writer, weights: List[List[VertexWeight]]):
def _write_envl(envl: Writer, model: Model, model_index: Dict[str, int]): def _write_envl(envl: Writer, model: Model, model_index: Dict[str, int]):
envl.write_u32(len(model.bone_map)) envl.write_u32(len(model.bone_map))
for bone_name in model.bone_map: for bone_name in model.bone_map:
envl.write_u32(model_index[bone_name]) envl.write_u32(model_index[bone_name])
'''
SKELETON CHUNKS
'''
def _write_bln2(bln2: Writer, anim: Animation):
bones = anim.bone_frames.keys()
bln2.write_u32(len(bones))
for bone_crc in bones:
bln2.write_u32(bone_crc, 0)
def _write_skl2(skl2: Writer, anim: Animation):
bones = anim.bone_frames.keys()
skl2.write_u32(len(bones))
for bone_crc in bones:
skl2.write_u32(bone_crc, 0) #default values from docs
skl2.write_f32(1.0, 0.0, 0.0)
'''
ANIMATION CHUNKS
'''
def _write_anm2(anm2: Writer, anim: Animation):
with anm2.create_child("CYCL") as cycl:
cycl.write_u32(1)
cycl.write_string(anim.name)
for _ in range(63 - len(anim.name)):
cycl.write_u8(0)
cycl.write_f32(anim.framerate)
cycl.write_u32(0) #what does play style refer to?
cycl.write_u32(anim.start_index, anim.end_index) #first frame indices
with anm2.create_child("KFR3") as kfr3:
kfr3.write_u32(len(anim.bone_frames))
for bone_crc in anim.bone_frames:
kfr3.write_u32(bone_crc)
kfr3.write_u32(0) #what is keyframe type?
translation_frames, rotation_frames = anim.bone_frames[bone_crc]
kfr3.write_u32(len(translation_frames), len(rotation_frames))
for frame in translation_frames:
kfr3.write_u32(frame.index)
kfr3.write_f32(frame.translation.x, frame.translation.y, frame.translation.z)
for frame in rotation_frames:
kfr3.write_u32(frame.index)
kfr3.write_f32(frame.rotation.x, frame.rotation.y, frame.rotation.z, frame.rotation.w)

View File

@ -1,199 +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_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

@ -1,107 +0,0 @@
""" Contains Scene object for representing a .msh file and the function to create one
from a Blender scene. """
from dataclasses import dataclass, field
from typing import List, Dict
from copy import copy
import bpy
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 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
from .msh_material_utilities import remove_unused_materials
from .msh_utilities import *
from .msh_anim_gather import extract_anim
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()
scene.name = bpy.context.scene.name
scene.materials = gather_materials()
scene.models, armature_obj = gather_models(apply_modifiers=apply_modifiers, export_target=export_target, skeleton_only=skel_only)
scene.models = sort_by_parent(scene.models)
if generate_triangle_strips:
scene.models = create_models_triangle_strips(scene.models)
else:
for model in scene.models:
if model.geometry:
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)
scene.materials = remove_unused_materials(scene.materials, scene.models)
root = scene.models[0]
if skel_only and (root.model_type == ModelType.NULL or root.model_type == ModelType.BONE):
# For ZenAsset
inject_dummy_data(root)
return scene, armature_obj
def create_scene_aabb(scene: Scene) -> SceneAABB:
""" Create a SceneAABB for a Scene. """
global_aabb = SceneAABB()
for model in scene.models:
if model.geometry is None or model.hidden:
continue
model_world_matrix = get_model_world_matrix(model, scene.models)
model_aabb = SceneAABB()
for segment in model.geometry:
segment_aabb = SceneAABB()
for pos in segment.positions:
segment_aabb.integrate_position(model_world_matrix @ pos)
model_aabb.integrate_aabb(segment_aabb)
global_aabb.integrate_aabb(model_aabb)
return global_aabb

View File

@ -1,48 +0,0 @@
""" Keeps track of exact skeleton when imported. Possibly needed for exporting skeleton-compatible animations. Will
probably be needed (with a matrix property) if we:
- add tip-to-tail adjustment and/or omit roots/effectors for imported skeletons to keep track of the original bone transforms
- add some sort of basepose-adjustment animation import option for already imported skeletons
I guess this might not need a panel, but I included it because the docs might need to reference it and
people may want to exclude certain bones without deleting keyframes.
"""
import bpy
from bpy.props import StringProperty
from bpy.types import PropertyGroup
class SkeletonProperties(PropertyGroup):
name: StringProperty(name="Name", default="Bone Name")
class SkeletonPropertiesPanel(bpy.types.Panel):
""" Creates a Panel in the Object properties window """
bl_label = "SWBF Skeleton Properties"
bl_idname = "SKELETON_PT_swbf_msh"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "data"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return context.object.type == 'ARMATURE' and context.object.data.swbf_msh_skel and len(context.object.data.swbf_msh_skel) > 0
def draw(self, context):
if context.object is None:
return
layout = self.layout
skel_props = context.object.data.swbf_msh_skel
layout.label(text = "Bones In MSH Skeleton: ")
for prop in skel_props:
layout.prop(prop, "name")

View File

@ -1,178 +0,0 @@
""" 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,47 +0,0 @@
""" Armature -> SWBF skeleton mapping functions """
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 *
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'''
def get_real_BONES(armature: bpy.types.Armature) -> Set[str]:
# First priority, add the names of the skeleton preserved on import
skel_props = armature.data.swbf_msh_skel
# Second, add all keyed bones
action = armature.animation_data.action if armature.animation_data else None
# Third, just add all bones in armature
# Set of bones to include
real_bones : Set[str] = set()
if len(skel_props) > 0:
for bone in skel_props:
real_bones.add(bone.name)
if action:
for group in armature.animation_data.action.groups:
real_bones.add(group.name)
if len(skel_props) == 0 and action is None:
for bone in armature.data.bones:
real_bones.add(bone.name)
return real_bones

View File

@ -1,14 +1,6 @@
""" Misc utilities. """ """ Misc utilities. """
from mathutils import Vector from mathutils import Vector
from typing import List
def vec_to_str(vec):
return "({:.4},{:.4},{:.4})".format(vec.x,vec.y,vec.z)
def quat_to_str(quat):
return "({:.4},{:.4},{:.4},{:.4})".format(quat.w, quat.x, quat.y, quat.z)
def add_vec(l: Vector, r: Vector) -> Vector: def add_vec(l: Vector, r: Vector) -> Vector:
return Vector(v0 + v1 for v0, v1 in zip(l, r)) return Vector(v0 + v1 for v0, v1 in zip(l, r))
@ -37,11 +29,3 @@ def pack_color(color) -> int:
packed |= (int(color[3] * 255.0 + 0.5) << 24) packed |= (int(color[3] * 255.0 + 0.5) << 24)
return packed return packed
def unpack_color(color: int) -> List[float]:
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

@ -1,46 +0,0 @@
""" 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)

View File

@ -1,433 +0,0 @@
"""
Script for reading zaabin/zaa files and applying the unmunged animation
to the currently selected armature.
As regards decompress_curves, I should really make a separate AnimationSet
dataclass instead of returning a convoluted nested dict.
"""
import os
import bpy
import re
from .chunked_file_reader import Reader
from .crc import *
from .msh_model import *
from .msh_model_utilities import *
from .msh_utilities import *
from typing import List, Set, Dict, Tuple
debug = False
#anims #bones #components #keyframes: index,value
def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]]]:
global debug
decompressed_anims: Dict[int, Dict[int, List[ Dict[int,float]]]] = {}
with Reader(input_file, debug=debug) as head:
# Dont read SMNA as child, since it has a length field always set to 0...
head.skip_until("SMNA")
head.skip_bytes(20)
num_anims = head.read_u16()
if debug:
print("\nFile contains {} animations\n".format(num_anims))
head.skip_bytes(2)
anim_crcs = []
anim_metadata = {}
head.skip_until("MINA")
# Read metadata (crc, num frames, num bones) for each anim
with head.read_child() as mina:
for i in range(num_anims):
transBitFlags = mina.read_u32()
mina.skip_bytes(4)
anim_crc = mina.read_u32()
anim_crcs.append(anim_crc)
anim_metadata[anim_crc] = {
"num_frames" : mina.read_u16(),
"num_bones" : mina.read_u16(),
"transBitFlags" : transBitFlags,
}
head.skip_until("TNJA")
# Read TADA offsets and quantization parameters for each rot + loc component, for each bone, for each anim
with head.read_child() as tnja:
for i, anim_crc in enumerate(anim_crcs):
bone_params = {}
bone_list = []
for _ in range(anim_metadata[anim_crc]["num_bones"]):
bone_crc = tnja.read_u32()
bone_list.append(bone_crc)
bone_params[bone_crc] = {
"rot_offsets" : [tnja.read_u32() for _ in range(4)], # Offsets into TADA for rotation
"loc_offsets" : [tnja.read_u32() for _ in range(3)], # and translation curves
"qparams" : [tnja.read_f32() for _ in range(4)], # Translation quantization parameters, 3 biases, 1 multiplier
}
anim_metadata[anim_crc]["bone_params"] = bone_params
anim_metadata[anim_crc]["bone_list"] = bone_list
head.skip_until("TADA")
# Decompress/dequantize frame data into discrete per-component curves
with head.read_child() as tada:
for anim_crc in anim_crcs:
decompressed_anims[anim_crc] = {}
num_frames = anim_metadata[anim_crc]["num_frames"]
num_bones = anim_metadata[anim_crc]["num_bones"]
transBitFlags = anim_metadata[anim_crc]["transBitFlags"]
if debug:
print("\n\tAnim hash: {} Num frames: {} Num joints: {}".format(hex(anim_crc), num_frames, num_bones))
for bone_num, bone_crc in enumerate(anim_metadata[anim_crc]["bone_list"]):
bone_curves = []
params_bone = anim_metadata[anim_crc]["bone_params"][bone_crc]
offsets_list = params_bone["rot_offsets"] + params_bone["loc_offsets"]
qparams = params_bone["qparams"]
if debug:
print("\n\t\tBone #{} hash: {}".format(bone_num,hex(bone_crc)))
print("\n\t\tQParams: {}, {}, {}, {}".format(*qparams))
for o, start_offset in enumerate(offsets_list):
# Init curve dict
curve : Dict[int,float] = {}
# Init accumulator
accumulator = 0.0
# 2047 = max val of signed 12 bit int, the (overwhelmingly) common compression amount.
# This is used for all rotation components in the file, with no offset
if o < 4:
mult = 1 / 2047
bias = 0.0
# Translations have specific quantization parameters; biases for each component and
# a single multiplier for all three
else:
mult = qparams[-1]
bias = qparams[o - 4]
if debug:
print("\n\t\t\tBias = {}, multiplier = {}".format(bias, mult))
if debug:
print("\n\t\t\tOffset {}: {} ({}, {} remaining)".format(o,start_offset, tada.get_current_pos(), tada.how_much_left(tada.get_current_pos())))
# Skip to start of compressed data for component, as specified in TNJA
tada.skip_bytes(start_offset)
j = 0
while (j < num_frames):
accumulator = bias + mult * tada.read_i16()
curve[j if j < num_frames else num_frames] = accumulator
if debug:
print("\t\t\t\t{}: {}".format(j, accumulator))
j+=1
while (j < num_frames):
control = tada.read_i8()
# Reset the accumulator to next dequantized i16
if control == -0x7f:
if debug:
print("\t\t\t\tControl: READING NEXT FRAME")
break
# RLE: hold current accumulator for the next u8 frames
elif control == -0x80:
num_skips = tada.read_u8()
if debug:
print("\t\t\t\tControl: HOLDING FOR {} FRAMES".format(num_skips))
j += num_skips
# If not a special value, increment accumulator by the dequantized i8
# The bias is NOT applied here, only for accumulator resets
else:
accumulator += mult * float(control)
curve[j if j < num_frames else num_frames] = accumulator
if debug:
print("\t\t\t\t{}: {}".format(j, accumulator))
j+=1
curve[num_frames - 1] = accumulator
tada.reset_pos()
bone_curves.append(curve)
decompressed_anims[anim_crc][bone_crc] = bone_curves
return decompressed_anims
'''
Gets the animation names from the supplied
.anims file. Handy since .zaabin files often
share a dir with a .anims file.
'''
def read_anims_file(anims_file_path):
if not os.path.exists(anims_file_path):
return []
with open(anims_file_path, 'r') as file:
anims_text = file.read()
splits = anims_text.split('"')
if len(splits) > 1:
return splits[1:-1:2]
return []
'''
Unmunge the .zaa(bin) file and apply the resulting animation
to the currently selected armature object.
Contains some bloated code for calculating the world transforms of each bone,
for now this will work ONLY if the model was directly imported from a .msh file.
'''
def extract_and_apply_munged_anim(input_file_path):
global debug
with open(input_file_path,"rb") as input_file:
animation_set = decompress_curves(input_file)
anim_names = []
if input_file_path.endswith(".zaabin"):
anim_names = read_anims_file(input_file_path.replace(".zaabin", ".anims"))
arma = bpy.context.view_layer.objects.active
if arma.type != 'ARMATURE':
raise Exception("Select an armature to attach the imported animation to!")
if arma.animation_data is not None:
arma.animation_data_clear()
arma.animation_data_create()
"""
When directly imported from .msh files,
all skeleton models are saved as emptys, since
some are excluded from the actual armature (effectors, roots, eg...).
bond_bind_poses contains matrices for converting the transform of
bones found in .msh/.zaabin files to ones that'll fit the extracted armature.
This will be replaced with the eventual importer release.
"""
animated_bones = set()
for anim_crc in animation_set:
for bone_crc in animation_set[anim_crc]:
animated_bones.add(bone_crc)
bpy.context.view_layer.objects.active = arma
bpy.ops.object.mode_set(mode='EDIT')
bone_bind_poses = {}
for edit_bone in arma.data.edit_bones:
if to_crc(edit_bone.name) not in animated_bones:
continue
curr_ancestor = edit_bone.parent
while curr_ancestor is not None and to_crc(curr_ancestor.name) not in animated_bones:
curr_ancestor = curr_ancestor.parent
if curr_ancestor:
bind_mat = curr_ancestor.matrix.inverted() @ edit_bone.matrix
else:
bind_mat = arma.matrix_local @ edit_bone.matrix
bone_bind_poses[edit_bone.name] = bind_mat.inverted()
bpy.ops.object.mode_set(mode='OBJECT')
if debug:
print("Extracting {} animations from {}:".format(len(animation_set), input_file_path))
for anim_crc in animation_set:
found_anim = [anim_name for anim_name in anim_names if to_crc(anim_name) == anim_crc]
if found_anim:
anim_str = found_anim[0]
else:
anim_str = str(hex(anim_crc))
if debug:
print("\tExtracting anim {}:".format(anim_str))
#if anim_str in bpy.data.actions:
# bpy.data.actions[anim_str].use_fake_user = False
# bpy.data.actions.remove(bpy.data.actions[anim_str])
action = bpy.data.actions.new(anim_str)
action.use_fake_user = True
animation = animation_set[anim_crc]
bone_crcs_list = [bone_crc_ for bone_crc_ in animation]
for bone_crc in sorted(bone_crcs_list):
bone_name = next((bone.name for bone in arma.pose.bones if to_crc(bone.name) == bone_crc), None)
if bone_name is None:
continue
bone = arma.pose.bones[bone_name]
bone_crc = to_crc(bone.name)
if bone_crc not in animation:
continue;
bind_mat = bone_bind_poses[bone.name]
loc_data_path = "pose.bones[\"{}\"].location".format(bone.name)
rot_data_path = "pose.bones[\"{}\"].rotation_quaternion".format(bone.name)
bone_curves = animation[bone_crc]
num_frames = max(bone_curves[0])
has_translation = bone_curves[4] is not None
if debug:
print("\t\tBone {} has {} frames: ".format(bone_name, num_frames))
last_values = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
def get_quat(index):
nonlocal bone_curves, last_values
q = Quaternion()
valmap = [1,2,3,0]
has_key = False
for i in range(4):
curve = bone_curves[i]
if index in curve:
has_key = True
last_values[i] = curve[index]
q[valmap[i]] = last_values[i]
return q if has_key else None
def get_vec(index):
nonlocal bone_curves, last_values
v = Vector()
has_key = False
for i in range(4,7):
curve = bone_curves[i]
if index in curve:
has_key = True
last_values[i] = curve[index]
v[i - 4] = last_values[i]
return v if has_key else None
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)
if has_translation:
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 range(num_frames):
q = get_quat(frame)
if q is not None:
if debug:
print("\t\t\tRot key: ({}, {})".format(frame, quat_to_str(q)))
# Very bloated, but works for now
q = (bind_mat @ convert_rotation_space(q).to_matrix().to_4x4()).to_quaternion()
fcurve_rot_w.keyframe_points.insert(frame,q.w)
fcurve_rot_x.keyframe_points.insert(frame,q.x)
fcurve_rot_y.keyframe_points.insert(frame,q.y)
fcurve_rot_z.keyframe_points.insert(frame,q.z)
if has_translation:
t = get_vec(frame)
if t is not None:
if debug:
print("\t\t\tPos key: ({}, {})".format(frame, vec_to_str(t)))
t = (bind_mat @ Matrix.Translation(convert_vector_space(t))).translation
fcurve_loc_x.keyframe_points.insert(frame,t.x)
fcurve_loc_y.keyframe_points.insert(frame,t.y)
fcurve_loc_z.keyframe_points.insert(frame,t.z)
arma.animation_data.action = action

Binary file not shown.

Before

Width:  |  Height:  |  Size: 375 KiB

View File

@ -5,10 +5,6 @@
+ [Export Properties](#export-properties) + [Export Properties](#export-properties)
+ [Export Failures](#export-failures) + [Export Failures](#export-failures)
+ [Export Behaviour to Know About](#export-behaviour-to-know-about) + [Export Behaviour to Know About](#export-behaviour-to-know-about)
- [Importer](#importer)
+ [Import Properties](#import-properties)
+ [Import Failures](#import-failures)
+ [Import Behaviour to Know About](#import-behaviour-to-know-about)
- [Shadow Volumes](#shadow-volumes) - [Shadow Volumes](#shadow-volumes)
- [Terrain Cutters](#terrain-cutters) - [Terrain Cutters](#terrain-cutters)
- [Collision](#collision) - [Collision](#collision)
@ -24,16 +20,6 @@
+ [Materials.Flags](#materialsflags) + [Materials.Flags](#materialsflags)
+ [Materials.Data](#materialsdata) + [Materials.Data](#materialsdata)
+ [Materials.Texture Maps](#materialstexture-maps) + [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)
+ [Animation Notes](#animation-notes)
- [Appendices](#appendices) - [Appendices](#appendices)
+ [Appendix Detail Map Blending](#appendix-detail-map-blending) + [Appendix Detail Map Blending](#appendix-detail-map-blending)
+ [Appendix Normal Map Example](#appendix-normal-map-example) + [Appendix Normal Map Example](#appendix-normal-map-example)
@ -44,8 +30,9 @@
+ [Appendix LOD Models Visualizations](#appendix-lod-models-visualizations) + [Appendix LOD Models Visualizations](#appendix-lod-models-visualizations)
## Exporter ## Exporter
The currently exporter has pretty straight forward behaviour. It'll grab the current active scene and export it as a .msh file that can be consumed by Zero Editor, modelmunge, and zenasset. The currently exporter has pretty straight forward behaviour. It'll grab the current active scene and export it as a .msh file that can be consumed by Zero Editor and modelmunge.
> NOTE: A key limitation to know of is that there is currently no support for skinned meshes. (Meshes with vertex weights.) Support is planned in the future.
### Export Properties ### Export Properties
@ -71,23 +58,6 @@ Controls what to export from Blender.
#### Apply Modifiers #### Apply Modifiers
Whether to apply [Modifiers](https://docs.blender.org/manual/en/latest/modeling/modifiers/index.html) during export or not. Whether to apply [Modifiers](https://docs.blender.org/manual/en/latest/modeling/modifiers/index.html) during export or not.
#### 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! |
### Export Failures ### Export Failures
There should be few things that can cause an export to fail. Should you encounter one you can consult the list below for how to remedy the situation. If you're error isn't on the list then feel free to [Open an issue](https://github.com/SleepKiller/SWBF-msh-Blender-Export/issues/new), remember to attach a .blend file that reproduces the issue. There should be few things that can cause an export to fail. Should you encounter one you can consult the list below for how to remedy the situation. If you're error isn't on the list then feel free to [Open an issue](https://github.com/SleepKiller/SWBF-msh-Blender-Export/issues/new), remember to attach a .blend file that reproduces the issue.
@ -138,12 +108,6 @@ 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. 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 ### 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. #### Materials for .msh files must be managed through the added UI panel named "SWBF .msh Properties" is added under the Material context.
@ -199,48 +163,6 @@ 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. #### 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. 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 one or more .msh files as well as .zaabin files. .msh files can be imported as models or animations.
### Import Properties
#### Import Animation(s)
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
#### "RuntimeError: Select an armature to attach the imported animation to!"
Be sure to have an armature selected before you import an animation.
#### "RuntimeError: No animation found in msh file!"
You tried to import an animation from a file with no animation data.
#### "struct.error: unpack requires a buffer of x bytes"
Serious bug with many possible causes, please notify a dev.
### Import Behaviour to Know About
#### Deleted skeleton meshes
If the .msh model to be imported has nodes with meshes that are weighted to or animated, the mesh data on that node will be lost upon import. This is because nodes that are weighted to or animated must be converted to bones in an armature, and bones in an armature cannot be meshes. Eventually we will add functionality to preserve the mesh as a specially named child object of the relevant armature bone.
#### Normals and vertex colors
Normals and vertex colors are currently not imported. Normals will be calculated by Blender.
## Shadow Volumes ## Shadow Volumes
SWBF's rendering engine uses Shadow Volumes for it's shadows. What this means is that the mesh for the shadow is seperate and different from the main mesh. And in order for your model to have shadows you must make the shadow mesh. SWBF's rendering engine uses Shadow Volumes for it's shadows. What this means is that the mesh for the shadow is seperate and different from the main mesh. And in order for your model to have shadows you must make the shadow mesh.
@ -473,9 +395,6 @@ Can optionally have a Detail Map.
This rendertype also enables per-pixel lighting. This rendertype also enables per-pixel lighting.
#### Other
These tools currently do not support render types not previously listed. If you select "Other" from the rendertype dropdown menu, you'll be able to set the number value of the exact rendertype you want. Since the meanings of the specific textures and data values are not supported yet for these render types, they will be listed as texture0-3 and data-value0-3 respectively.
### Materials.Transparency Flags ### Materials.Transparency Flags
> TODO: Improve this section. > TODO: Improve this section.
@ -576,117 +495,6 @@ Environment map for the material. Used to provide static reflections for the mod
#### Materials.Texture Maps.Distortion Map #### Materials.Texture Maps.Distortion Map
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. 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.
## Skeletons and Skinning
This guide assumes you have working knowledge of Blender Armatures and deformation with vertex groups, as well as how to create, edit, pose, and keyframe bones in an Armature.
### XSI vs Blender
XSI has a very free-form take on skeletons and skinning. Models can be skinned to other models in a rather arbitrary manner, whereas in Blender, a model can only be skinned if it is parented to an armature and can only skin to bones in that armature.
Moreover, Zero Editor requires that skinned models be parents of their skeletons, which directly contradicts the structure Blender mandates! The exporter works around this by reparenting skinned objects to their armature's parent, and reparenting the root bones of the armature to the skin object. Note in the examples below that the armature's bones are not technically part of the scene's object hierarchy in Blender, as they belong to the armature object itself.
### Example Skin Hierarchy
Upon export, skinned objects are reparented to the armature's parent, and the armature skeleton reparented to the main skinned object:
#### Blender
* dummyroot
* Armature
* bone_root
* bone_one
* bone_two
* skinned_obj
* sv_skinned_obj
* skinned_obj_lowrez
#### Exported
* dummyroot
* skinned_obj
* bone_root
* bone_one
* bone_two
* sv_skinned_obj
* skinned_obj_lowrez
### Example Bone Parent Hierarchy
The same goes for objects that are children of an armature, but are parented directly to bones in that armature, as could be the case in a simple door:
#### Blender
* dummyroot
* Armature
* bone_doorleft
* bone_doorright
* left_door_mesh (bone parent: bone_doorleft)
* right_door_mesh (bone parent: bone_doorright)
#### Exported
* dummyroot
* bone_doorleft
* left_door_mesh
* 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
Skinning/vertex-weighting in Blender is a very complex topic, these docs will focus solely on your considerations for exporting a SWBF compatible skin.
1. .msh files can weight a vertex to a maximum of 4 bones. When painting weights, ensure each vertex is meaningfully influenced by a maximum of 4 bones. It won't break the exporter if you exceed 4, but the 4 largest weights per-vertex will be kept and renormalized, and the others will be discarded. It is also worth mentioning however that SWBF2 (PC) only appears to supports 1 weight per vertex, despite the toolchain accepting and processing .msh files with more weights.
2. An object will be exported as a skin if it is parented to an armature and has vertex groups. The skeleton will be reparented to the skin object which is not named as a collision or LOD object.
3. As is the case with exporting in XSI, make sure you apply transforms on your skinned objects before exporting!
## Animation
This guide assumes you know how Armatures work, and how to switch between, create, and edit Actions in Blender.
### Actions and Animations
This exporter can convert Actions used by Armatures to animations compatible with SWBF's toolchain. If an armature is found among the objects to be exported, the exporter can include the armature's currently set Action as an animation in the .msh file. As of now, animation via Armature is the only way to export Blender Actions.
When exporting an Action, all frames between and including the first and last *keyframes* of the Action will be included. For example, if the first and last keyframes are 0 and 5, the exporter will record bone positions at frames 0, 1, 2, 3, 4, and 5, regardless of how many frames are actually keyed. Don't worry about using as few keyframes as possible to save a smaller animation as the exporter will record bone positions and rotations for each frame.
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.
### Animation notes:
1. If exporting an animation, your exported .msh file's name should be that of the animation/action itself.
2. Bone constraints are not exported.
3. Don't include multiple armatures in one export!
4. Blender's animation speed defaults to 24 fps. If you want to see exactly how your animation will play ingame, set it to 29.97 in the `Output Properties` section of the `Properties` editor.
## Appendices ## Appendices
### Appendix Detail Map Blending ### Appendix Detail Map Blending
@ -785,3 +593,5 @@ All spheres were ico spheres with each LOD model have one less subdivision than
The map's near scene range values set to `NearSceneRange(90.0, 400.0, 120.0, 600.0);`. The map's near scene range values set to `NearSceneRange(90.0, 400.0, 120.0, 600.0);`.
![LOD Models Visualized from a hill.](images/lod_example_distances_0_hill_view.jpg) ![LOD Models Visualized from a hill.](images/lod_example_distances_0_hill_view.jpg)

201
license
View File

@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.