Compare commits

...

20 Commits

Author SHA1 Message Date
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
itdominator d1d83d39af Partial Revert: Animation Track patch cleanup 2023-06-02 22:27:58 -05:00
itdominator 1ec4332576 Animation Track patch cleanup 2023-06-02 21:49:24 -05:00
itdominator 125ad2792c Fixed import of animations to allow for bulk import 2023-05-28 17:18:50 -05:00
William Herald Snyder f451be4d18 Check number of bytes remaining before reading texture strings 2022-10-29 12:59:11 -04:00
William Herald Snyder 613cb20678 Many msh files (e.g. those in BFX) have multiple models assigned to the same index (MNDX). Indices should be linked only to the first model that uses them to ensure proper skinning. 2022-10-09 13:55:24 -04:00
William Herald Snyder 432c9ff380 Hidden objects will be unhidden after calling evaluated_get, so they must be tracked beforehand when exporting. 2022-10-08 22:10:40 -04:00
William Herald Snyder ba762d9548 Abort export if SELECTED or SELECTED_WITH_CHILDREN is chosen but nothing is selected and warn user that hidden objects can't be selected. 2022-10-08 22:07:44 -04:00
William Herald Snyder b120b74cd4 Models shouldn't be hidden by default, as many models have objects that are obviously not hidden but do not have FLGS chunks + objects with children can be hidden, only older versions of Blender automatically hide children when parent is hidden 2022-10-08 21:31:08 -04:00
Will Snyder 7b9f5c9cfb Prune segments with empty triangle strips. ZE and most versions of ZETools require triangle strips. 2022-10-06 10:23:13 -07:00
William Herald Snyder 84a910f747 When exporting collision primitives, check the swbf_msh_coll_prim primitive property before the name to catch primitives which have incorrect names. Previously the property would only be checked if the primitive was missing a name indicator, but it should be checked first and always because some primitives from XSI have name-type mismatch (cis_hover_aat) 2022-10-05 09:28:27 -04:00
William Herald Snyder 0a1866295c Only include vertex groups used for skinning purposes. More robust requirements for object to be exported as a skin. Misc cleanup and minor refactoring in msh_model_gather. 2022-10-05 09:27:44 -04:00
William Herald Snyder ec54df21d2 Blend + hardedged transparency emulated via settings + additive emulated by adding the output of the PBSDF and a default Transparent BSDF. Bugfix: animation data is cleared from nodetrees via a stable method. FCurves.clear() is too new... 2022-10-04 09:58:36 -04:00
William Herald Snyder b2bd9c8316 Blender 2.80-2.83 supports additive blending, so FillMaterialProps will catch that if present 2022-10-04 07:54:25 -04:00
12 changed files with 220 additions and 85 deletions

View File

@ -1,7 +1,7 @@
bl_info = {
'name': 'SWBF .msh Import-Export',
'author': 'Will Snyder, SleepKiller',
"version": (1, 0, 0),
'author': 'Will Snyder, PrismaticFlower',
"version": (1, 3, 0),
'blender': (2, 80, 0),
'location': 'File > Import-Export',
'description': 'Export as SWBF .msh file',
@ -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:

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

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

View File

@ -47,6 +47,7 @@ class FillSWBFMaterialProperties(bpy.types.Operator):
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
@ -71,6 +72,7 @@ class FillSWBFMaterialProperties(bpy.types.Operator):
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:
@ -124,6 +126,7 @@ def draw_matfill_menu(self, context):
# 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):
@ -170,7 +173,6 @@ to provide an exact emulation"""
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'
@ -181,17 +183,30 @@ to provide an exact emulation"""
bsdf.inputs["Roughness"].default_value = 1.0
bsdf.inputs["Specular"].default_value = 0.0
if mat_props.hardedged_transparency and not mat_props.glow:
material.blend_method = "CLIP"
material.node_tree.links.new(bsdf.inputs['Alpha'], texImage.outputs['Alpha'])
material.use_backface_culling = not bool(mat_props.doublesided)
surface_output_nodes.append(tuple(('BSDF', bsdf)))
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)
if mat_props.glow:
else:
emission = material.node_tree.nodes.new("ShaderNodeEmission")
material.node_tree.links.new(emission.inputs['Color'], texImage.outputs['Color'])
@ -204,7 +219,7 @@ to provide an exact emulation"""
material.node_tree.links.new(emission.inputs['Strength'], emission_strength_multiplier.outputs[0])
surface_output_nodes.append(tuple(("Emission", emission)))
surface_output_nodes.append(("Emission", emission))
surfaces_output = None
if (len(surface_output_nodes) == 1):
@ -261,7 +276,7 @@ to provide an exact emulation"""
# Clear all anims in all cases
if material.node_tree.animation_data:
material.node_tree.animation_data.action.fcurves.clear()
material.node_tree.animation_data_clear()
if "SCROLL" in mat_props.rendertype:
@ -288,7 +303,7 @@ to provide an exact emulation"""
# Don't know how to set interpolation when adding keyframes
# so we must do it after the fact
if material.node_tree.animation_data:
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'

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]
@ -129,6 +135,10 @@ def model_to_mesh_object(model: Model, scene : Scene, materials_map : Dict[str,
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

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

@ -32,33 +32,62 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool
pure_bones_from_armature = {}
armature_found = None
objects_to_export = select_objects(export_target)
# Non-bone objects that will be exported
blender_objects_to_export = []
for uneval_obj in objects_to_export:
if uneval_obj.type == "ARMATURE":
# This must be seperate from the list above,
# since exported objects will contain Blender objects as well as bones
# Here we just keep track of all names, regardless of origin
exported_object_names: Set[str] = set()
# Me must keep track of hidden objects separately because
# evaluated_get clears hidden status
blender_objects_to_hide: Set[str] = set()
# Armature must be processed before everything else!
# In this loop we also build a set of names of all objects
# that will be exported. This is necessary so we can prune vertex
# groups that do not reference exported objects in the main
# model building loop below this one.
for uneval_obj in select_objects(export_target):
if get_is_model_hidden(uneval_obj):
blender_objects_to_hide.add(uneval_obj.name)
if uneval_obj.type == "ARMATURE" and not armature_found:
# Keep track of the armature, we don't want to process > 1!
armature_found = uneval_obj.evaluated_get(depsgraph) if apply_modifiers else uneval_obj
# Get all bones in a separate list. While we iterate through
# objects we removed bones with geometry from this dict. After iteration
# is done, we add the remaining bones to the models from exported
# scene objects.
pure_bones_from_armature = expand_armature(armature_found)
break
# 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:
pass
for uneval_obj in objects_to_export:
if uneval_obj.type == "ARMATURE" or (uneval_obj.type in SKIPPED_OBJECT_TYPES and uneval_obj.name not in parents):
continue
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)
# Test for a mesh object that is actually a BONE (shares name with bone_parent)
# Test for a mesh object that should be a BONE on export.
# If so, we inject geometry into the BONE while not modifying it's transform/name
if obj.parent_bone and obj.parent_bone in pure_bones_from_armature:
model = pure_bones_from_armature[obj.parent_bone]
# Since we found a composite bone, removed it from the dict of pure bones
pure_bones_from_armature.pop(obj.parent_bone)
# and remove it from the set of BONES without geometry (pure).
if obj.name in pure_bones_from_armature:
model = pure_bones_from_armature.pop(obj.name)
else:
model = Model()
model.name = obj.name
model.model_type = get_model_type(obj, skeleton_only)
model.hidden = get_is_model_hidden(obj)
model.model_type = ModelType.NULL if skeleton_only else get_model_type(obj, armature_found)
transform = obj.matrix_local
@ -82,10 +111,19 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool
model.transform.rotation = convert_rotation_space(local_rotation)
model.transform.translation = convert_vector_space(local_translation)
if obj.type in MESH_OBJECT_TYPES:
if obj.type in MESH_OBJECT_TYPES and not skeleton_only:
# Vertex groups are often used for purposes other than skinning.
# Here we gather all vgroups and select the ones that reference
# objects included in the export.
valid_vgroup_indices : Set[int] = set()
if model.model_type == ModelType.SKIN:
valid_vgroups = [group for group in obj.vertex_groups if group.name in exported_object_names]
valid_vgroup_indices = { group.index for group in valid_vgroups }
model.bone_map = [ group.name for group in valid_vgroups ]
mesh = obj.to_mesh()
model.geometry = create_mesh_geometry(mesh, obj.vertex_groups)
model.geometry = create_mesh_geometry(mesh, valid_vgroup_indices)
obj.to_mesh_clear()
@ -98,20 +136,17 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool
raise RuntimeError(f"Object '{obj.name}' has resulted in a .msh geometry segment that has "
f"more than {MAX_MSH_VERTEX_COUNT} vertices! Split the object's mesh up "
f"and try again!")
if obj.vertex_groups:
model.bone_map = [group.name for group in obj.vertex_groups]
if get_is_collision_primitive(obj):
model.collisionprimitive = get_collision_primitive(obj)
model.hidden = model.name in blender_objects_to_hide
models_list.append(model)
# We removed all composite bones after looking through the objects,
# so the bones left are all pure and we add them all here.
models_list += pure_bones_from_armature.values()
return (models_list, armature_found)
return (models_list + list(pure_bones_from_armature.values()), armature_found)
@ -127,7 +162,7 @@ def create_parents_set() -> Set[str]:
return parents
def create_mesh_geometry(mesh: bpy.types.Mesh, has_weights: bool) -> List[GeometrySegment]:
def create_mesh_geometry(mesh: bpy.types.Mesh, valid_vgroup_indices: Set[int]) -> List[GeometrySegment]:
""" Creates a list of GeometrySegment objects from a Blender mesh.
Does NOT create triangle strips in the GeometrySegment however. """
@ -144,11 +179,11 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, has_weights: bool) -> List[Geomet
vertex_remap: List[Dict[Tuple[int, int], int]] = [dict() for i in range(material_count)]
polygons: List[Set[int]] = [set() for i in range(material_count)]
if mesh.vertex_colors.active is not None:
if mesh.color_attributes.active_color is not None:
for segment in segments:
segment.colors = []
if has_weights:
if valid_vgroup_indices:
for segment in segments:
segment.weights = []
@ -180,13 +215,16 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, has_weights: bool) -> List[Geomet
yield mesh.uv_layers.active.data[loop_index].uv.y
if segment.colors is not None:
for v in mesh.vertex_colors.active.data[loop_index].color:
yield v
data_type = mesh.color_attributes.active_color.data_type
if data_type == "FLOAT_COLOR" or data_type == "BYTE_COLOR":
for v in mesh.color_attributes.active_color.data[vertex_index].color:
yield v
if segment.weights is not None:
for v in mesh.vertices[vertex_index].groups:
yield v.group
yield v.weight
if v.group in valid_vgroup_indices:
yield v.group
yield v.weight
vertex_cache_entry = tuple(get_cache_vertex())
cached_vertex_index = cache.get(vertex_cache_entry, vertex_cache_miss_index)
@ -209,12 +247,13 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, has_weights: bool) -> List[Geomet
segment.texcoords.append(mesh.uv_layers.active.data[loop_index].uv.copy())
if segment.colors is not None:
segment.colors.append(list(mesh.vertex_colors.active.data[loop_index].color))
data_type = mesh.color_attributes.active_color.data_type
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:
groups = mesh.vertices[vertex_index].groups
segment.weights.append([VertexWeight(v.weight, v.group) for v in groups])
segment.weights.append([VertexWeight(v.weight, v.group) for v in groups if v.group in valid_vgroup_indices])
return new_index
@ -233,12 +272,29 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, has_weights: bool) -> List[Geomet
return segments
def get_model_type(obj: bpy.types.Object, skel_only: bool) -> ModelType:
def get_model_type(obj: bpy.types.Object, armature_found: bpy.types.Object) -> ModelType:
""" Get the ModelType for a Blender object. """
if obj.type in MESH_OBJECT_TYPES and not skel_only:
if obj.vertex_groups:
return ModelType.SKIN
if obj.type in MESH_OBJECT_TYPES:
# Objects can have vgroups for non-skinning purposes.
# If we can find one vgroup that shares a name with a bone in the
# armature, we know the vgroup is for weighting purposes and thus
# the object is a skin. Otherwise, interpret it as a static mesh.
# We must also check that an armature included in the export
# and that it is the same one this potential skin is weighting to.
# If we failed to do this, a user could export a selected object
# that is a skin, but the weight data in the export would reference
# nonexistent models!
if (obj.vertex_groups and armature_found and
obj.parent and obj.parent.name == armature_found.name):
for vgroup in obj.vertex_groups:
if vgroup.name in armature_found.data.bones:
return ModelType.SKIN
return ModelType.STATIC
else:
return ModelType.STATIC
@ -247,6 +303,9 @@ def get_model_type(obj: bpy.types.Object, skel_only: bool) -> ModelType:
def get_is_model_hidden(obj: bpy.types.Object) -> bool:
""" Gets if a Blender object should be marked as hidden in the .msh file. """
if obj.hide_get():
return True
name = obj.name.lower()
if name.startswith("c_"):
@ -311,6 +370,14 @@ def get_collision_primitive_shape(obj: bpy.types.Object) -> CollisionPrimitiveSh
""" Gets the CollisionPrimitiveShape of an object or raises an error if
it can't. """
# arc170 fighter has examples of box colliders without proper naming
# and cis_hover_aat has a cylinder which is named p_vehiclesphere.
# To export these properly we must check the collision_prim property
# that was assigned on import BEFORE looking at the name.
prim_type = obj.swbf_msh_coll_prim.prim_type
if prim_type in [item.value for item in CollisionPrimitiveShape]:
return CollisionPrimitiveShape(prim_type)
name = obj.name.lower()
if "sphere" in name or "sphr" in name or "spr" in name:
@ -320,11 +387,6 @@ def get_collision_primitive_shape(obj: bpy.types.Object) -> CollisionPrimitiveSh
if "box" in name or "cube" in name or "cuboid" in name:
return CollisionPrimitiveShape.BOX
# arc170 fighter has examples of box colliders without proper naming
prim_type = obj.swbf_msh_coll_prim.prim_type
if prim_type in [item.value for item in CollisionPrimitiveShape]:
return CollisionPrimitiveShape(prim_type)
raise RuntimeError(f"Object '{obj.name}' has no primitive type specified in it's name!")
@ -436,6 +498,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

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

View File

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

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

@ -9,7 +9,7 @@ from mathutils import Vector
from .msh_model import Model, Animation, ModelType
from .msh_scene import Scene, SceneAABB
from .msh_model_gather import gather_models
from .msh_model_utilities import sort_by_parent, has_multiple_root_models, reparent_model_roots, get_model_world_matrix, inject_dummy_data
from .msh_model_utilities import make_null, validate_geometry_segment, sort_by_parent, has_multiple_root_models, reparent_model_roots, get_model_world_matrix, inject_dummy_data
from .msh_model_triangle_strips import create_models_triangle_strips
from .msh_material import *
from .msh_material_gather import gather_materials
@ -53,6 +53,20 @@ def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_t
for segment in model.geometry:
segment.triangle_strips = segment.triangles
# After generating triangle strips we must prune any segments that don't have
# them, or else ZE and most versions of ZETools will crash.
# We could also make models with no valid segments nulls, since they might as well be,
# but that could have unforseeable consequences further down the modding pipeline
# and is not necessary to avoid the aforementioned crashes...
for model in scene.models:
if model.geometry is not None:
# Doing this in msh_model_gather would be messy and the presence/absence
# of triangle strips is required for a validity check.
model.geometry = [segment for segment in model.geometry if validate_geometry_segment(segment)]
#if not model.geometry:
# make_null(model)
if has_multiple_root_models(scene.models):
scene.models = reparent_model_roots(scene.models)

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]