Compare commits

...

24 Commits

Author SHA1 Message Date
3e066bbe64 Updated to use Blender 4.3.2 2025-02-25 17:42:09 -06:00
PrismaticFlower
0cc8beb830
Merge pull request #19 from styinx/#17_animation_frame_export
#17: Convert frame time from float to int
2025-02-23 06:34:34 +00:00
Christoph Zorn
336870ecb5 #17: Convert frame time from float to int 2024-05-04 10:09:17 +02:00
PrismaticFlower
daacdf1a5d handle Face Corner color attributes
fixes #15
2023-12-05 09:49:59 +13:00
PrismaticFlower
48c05380c6 update version number 2023-12-03 12:04:33 +13:00
PrismaticFlower
9f4a205f3d
Merge pull request #14 from styinx/#13_bsdf_inputs_specular_key
#13: Add check for blender version on specular key
2023-12-03 12:03:14 +13:00
PrismaticFlower
216ea5d7a4 Merge branch 'master' of https://github.com/PrismaticFlower/SWBF-msh-Blender-IO 2023-12-03 11:58:19 +13:00
Christoph Zorn
d2c7dbd79f #13: Add check for blender version on specular key 2023-12-02 11:58:03 +01:00
PrismaticFlower
6247a289e6 update version number 2023-10-31 08:47:35 +13:00
PrismaticFlower
2b60f0d2a6
Merge pull request #12 from LeovanGit/vertex-color
Fix vertex colors export
2023-10-31 08:45:19 +13:00
LeovanGit
8d5c701b86 fix vertex colors export 2023-10-30 12:46:07 +03:00
PrismaticFlower
c072c9e56d update version number 2023-10-29 11:22:51 +13:00
PrismaticFlower
2421ba70c2
Merge pull request #10 from LeovanGit/vertex-colors
Vertex Colors
2023-10-29 11:16:18 +13:00
PrismaticFlower
ae42cda6ab Skip adding color attributes when unneeded
This is a very small change to skip adding the vertex colours to the Blender mesh if no segment of the geometry being loaded has vertex colours.
2023-10-29 11:13:12 +13:00
LeovanGit
0ac921d855 Add vertex colors to blender + fix unpack_color() 2023-10-27 23:05:05 +03:00
Will
806a7cc060
Merge pull request #8 from maximstewart/master
Add Animation Import(s) to NLA Track List
2023-10-01 08:43:52 -07:00
d1d83d39af Partial Revert: Animation Track patch cleanup 2023-06-02 22:27:58 -05:00
1ec4332576 Animation Track patch cleanup 2023-06-02 21:49:24 -05:00
125ad2792c Fixed import of animations to allow for bulk import 2023-05-28 17:18:50 -05:00
William Herald Snyder
f451be4d18 Check number of bytes remaining before reading texture strings 2022-10-29 12:59:11 -04:00
William Herald Snyder
613cb20678 Many msh files (e.g. those in BFX) have multiple models assigned to the same index (MNDX). Indices should be linked only to the first model that uses them to ensure proper skinning. 2022-10-09 13:55:24 -04:00
William Herald Snyder
432c9ff380 Hidden objects will be unhidden after calling evaluated_get, so they must be tracked beforehand when exporting. 2022-10-08 22:10:40 -04:00
William Herald Snyder
ba762d9548 Abort export if SELECTED or SELECTED_WITH_CHILDREN is chosen but nothing is selected and warn user that hidden objects can't be selected. 2022-10-08 22:07:44 -04:00
William Herald Snyder
b120b74cd4 Models shouldn't be hidden by default, as many models have objects that are obviously not hidden but do not have FLGS chunks + objects with children can be hidden, only older versions of Blender automatically hide children when parent is hidden 2022-10-08 21:31:08 -04:00
11 changed files with 86 additions and 44 deletions

View File

@ -1,8 +1,8 @@
bl_info = {
'name': 'SWBF .msh Import-Export',
'author': 'Will Snyder, SleepKiller',
"version": (1, 0, 0),
'blender': (2, 80, 0),
'author': 'Will Snyder, PrismaticFlower',
"version": (1, 3, 3),
'blender': (4, 3, 2),
'location': 'File > Import-Export',
'description': 'Export as SWBF .msh file',
'warning': '',
@ -13,9 +13,9 @@ bl_info = {
}
# Taken from glTF-Blender-IO, because I do not understand Python that well
# (this is the first thing of substance I've created in it) and just wanted
# (this is the first thing of substance I've created in it) and just wanted
# script reloading to work.
#
#
# https://github.com/KhronosGroup/glTF-Blender-IO
#
# Copyright 2018-2019 The glTF-Blender-IO authors.
@ -118,8 +118,13 @@ class ExportMSH(Operator, ExportHelper):
def execute(self, context):
if 'SELECTED' in self.export_target and len(bpy.context.selected_objects) == 0:
raise Exception("{} was chosen, but you have not selected any objects. "
" Don't forget to unhide all the objects you wish to select!".format(self.export_target))
scene, armature_obj = create_scene(
generate_triangle_strips=self.generate_triangle_strips,
generate_triangle_strips=self.generate_triangle_strips,
apply_modifiers=self.apply_modifiers,
export_target=self.export_target,
skel_only=self.animation_export != 'NONE') # Exclude geometry data (except root stuff) if we're doing anims
@ -136,7 +141,7 @@ class ExportMSH(Operator, ExportHelper):
set_scene_animation(scene, armature_obj)
write_scene_to_file(self.filepath, scene)
elif self.animation_export == 'BATCH':
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:
@ -157,14 +162,14 @@ def menu_func_export(self, context):
class ImportMSH(Operator, ImportHelper):
""" Import an SWBF .msh file. """
""" Import SWBF .msh file(s). """
bl_idname = "swbf_msh.import"
bl_label = "Import SWBF .msh File(s)"
filename_ext = ".msh"
files: CollectionProperty(
name="File Path",
name="File Path(s)",
type=bpy.types.OperatorFileListElement,
)
@ -176,7 +181,7 @@ class ImportMSH(Operator, ImportHelper):
animation_only: BoolProperty(
name="Import Animation(s)",
description="Import on or more animations from the selected files and append each as a new Action to currently selected Armature.",
description="Import one or more animations from the selected files and append each as a new Action to currently selected Armature.",
default=False
)
@ -188,9 +193,9 @@ class ImportMSH(Operator, ImportHelper):
if filepath.endswith(".zaabin") or filepath.endswith(".zaa"):
extract_and_apply_munged_anim(filepath)
else:
with open(filepath, 'rb') as input_file:
with open(filepath, 'rb') as input_file:
scene = read_scene(input_file, self.animation_only)
if not self.animation_only:
extract_scene(filepath, scene)
else:
@ -256,4 +261,4 @@ def unregister():
if __name__ == "__main__":
register()
register()

View File

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

View File

@ -67,7 +67,7 @@ def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation:
for frame in range(num_frames):
frame_time = framerange.x + frame * increment
frame_time = int(framerange.x + frame * increment)
bpy.context.scene.frame_set(frame_time)
for keyable_bone in keyable_bones:

View File

@ -32,7 +32,6 @@ def extract_and_apply_anim(filename : str, scene : Scene):
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]
@ -40,6 +39,10 @@ def extract_and_apply_anim(filename : str, scene : Scene):
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
@ -47,7 +50,7 @@ def extract_and_apply_anim(filename : str, scene : Scene):
arma.animation_data_create()
# Record the starting transforms of each bone. Pose space is relative
# Record the starting transforms of each bone. Pose space is relative
# to bones starting transforms. Starting = in edit mode
bone_bind_poses = {}
@ -56,7 +59,7 @@ def extract_and_apply_anim(filename : str, scene : Scene):
for edit_bone in arma.data.edit_bones:
if edit_bone.parent:
bone_local = edit_bone.parent.matrix.inverted() @ edit_bone.matrix
bone_local = edit_bone.parent.matrix.inverted() @ edit_bone.matrix
else:
bone_local = arma.matrix_local @ edit_bone.matrix
@ -72,8 +75,8 @@ def extract_and_apply_anim(filename : str, scene : Scene):
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)
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)
@ -103,4 +106,5 @@ def extract_and_apply_anim(filename : str, scene : Scene):
fcurve_loc_z.keyframe_points.insert(i,t.z)
arma.animation_data.action = action
track = arma.animation_data.nla_tracks.new()
track.strips.new(action.name, int(action.frame_range[0]), action)

View File

@ -180,8 +180,10 @@ to provide an exact emulation"""
texture_input_nodes.append(texImage)
specular_key = "Specular" if bpy.app.version < (4, 0, 0) else "Specular IOR Level"
bsdf.inputs["Roughness"].default_value = 1.0
bsdf.inputs["Specular"].default_value = 0.0
bsdf.inputs[specular_key].default_value = 0.0
material.use_backface_culling = not bool(mat_props.doublesided)

View File

@ -40,6 +40,7 @@ def model_to_mesh_object(model: Model, scene : Scene, materials_map : Dict[str,
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
@ -58,6 +59,7 @@ def model_to_mesh_object(model: Model, scene : Scene, materials_map : Dict[str,
if model.geometry:
geometry_has_colors = any(segment.colors for segment in model.geometry)
for segment in model.geometry:
@ -76,6 +78,11 @@ def model_to_mesh_object(model: Model, scene : Scene, materials_map : Dict[str,
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
@ -111,7 +118,6 @@ def model_to_mesh_object(model: Model, scene : Scene, materials_map : Dict[str,
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]
@ -122,13 +128,16 @@ def model_to_mesh_object(model: Model, scene : Scene, materials_map : Dict[str,
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
@ -165,7 +174,6 @@ def model_to_mesh_object(model: Model, scene : Scene, materials_map : Dict[str,
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)
@ -188,4 +196,3 @@ def model_to_mesh_object(model: Model, scene : Scene, materials_map : Dict[str,
return blender_mesh_object

View File

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

View File

@ -40,6 +40,10 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool
# 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
@ -47,6 +51,10 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool
# groups that do not reference exported objects in the main
# model building loop below this one.
for uneval_obj in select_objects(export_target):
if get_is_model_hidden(uneval_obj):
blender_objects_to_hide.add(uneval_obj.name)
if uneval_obj.type == "ARMATURE" and not armature_found:
# Keep track of the armature, we don't want to process > 1!
armature_found = uneval_obj.evaluated_get(depsgraph) if apply_modifiers else uneval_obj
@ -80,7 +88,6 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool
model = Model()
model.name = obj.name
model.model_type = ModelType.NULL if skeleton_only else get_model_type(obj, armature_found)
model.hidden = get_is_model_hidden(obj)
transform = obj.matrix_local
@ -133,6 +140,8 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool
if get_is_collision_primitive(obj):
model.collisionprimitive = get_collision_primitive(obj)
model.hidden = model.name in blender_objects_to_hide
models_list.append(model)
# We removed all composite bones after looking through the objects,
@ -170,7 +179,7 @@ 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)]
polygons: List[Set[int]] = [set() for i in range(material_count)]
if mesh.vertex_colors.active is not None:
if mesh.color_attributes.active_color is not None:
for segment in segments:
segment.colors = []
@ -206,7 +215,10 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, valid_vgroup_indices: Set[int]) -
yield mesh.uv_layers.active.data[loop_index].uv.y
if segment.colors is not None:
for v in mesh.vertex_colors.active.data[loop_index].color:
active_color = mesh.color_attributes.active_color
data_index = loop_index if active_color.domain == "CORNER" else vertex_index
for v in mesh.color_attributes.active_color.data[data_index].color:
yield v
if segment.weights is not None:
@ -236,7 +248,10 @@ 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())
if segment.colors is not None:
segment.colors.append(list(mesh.vertex_colors.active.data[loop_index].color))
active_color = mesh.color_attributes.active_color
data_index = loop_index if active_color.domain == "CORNER" else vertex_index
segment.colors.append(list(active_color.data[data_index].color))
if segment.weights is not None:
groups = mesh.vertices[vertex_index].groups
@ -290,6 +305,9 @@ def get_model_type(obj: bpy.types.Object, armature_found: bpy.types.Object) -> M
def get_is_model_hidden(obj: bpy.types.Object) -> bool:
""" Gets if a Blender object should be marked as hidden in the .msh file. """
if obj.hide_get():
return True
name = obj.name.lower()
if name.startswith("c_"):
@ -482,6 +500,7 @@ def expand_armature(armature: bpy.types.Object) -> Dict[str, Model]:
model.model_type = ModelType.BONE if bone.name in proper_BONES else ModelType.NULL
model.name = bone.name
model.hidden = True
model.transform.rotation = convert_rotation_space(local_rotation)
model.transform.translation = convert_vector_space(local_translation)

View File

@ -165,19 +165,23 @@ def _read_matd(matd: Reader) -> Material:
elif next_header == "TX0D":
with matd.read_child() as tx0d:
mat.texture0 = tx0d.read_string()
if tx0d.bytes_remaining() > 0:
mat.texture0 = tx0d.read_string()
elif next_header == "TX1D":
with matd.read_child() as tx1d:
mat.texture1 = tx1d.read_string()
if tx1d.bytes_remaining() > 0:
mat.texture1 = tx1d.read_string()
elif next_header == "TX2D":
with matd.read_child() as tx2d:
mat.texture2 = tx2d.read_string()
if tx2d.bytes_remaining() > 0:
mat.texture2 = tx2d.read_string()
elif next_header == "TX3D":
with matd.read_child() as tx3d:
mat.texture3 = tx3d.read_string()
if tx3d.bytes_remaining() > 0:
mat.texture3 = tx3d.read_string()
else:
matd.skip_bytes(1)
@ -203,7 +207,9 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model:
global model_counter
global mndx_remap
mndx_remap[index] = model_counter
if index not in mndx_remap:
mndx_remap[index] = model_counter
model_counter += 1

View File

@ -195,6 +195,5 @@ def extract_scene(filepath: str, scene: Scene):
for model in scene.models:
if model.name in model_map:
obj = model_map[model.name]
if get_is_model_hidden(obj) and len(obj.children) == 0:
obj.hide_set(True)
obj.hide_set(model.hidden or get_is_model_hidden(obj))

View File

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