16 Commits

Author SHA1 Message Date
LeovanGit
a6118a7def fix vertex colors export 2023-11-22 21:14:38 -06:00
PrismaticFlower
be09e10db5 update version number 2023-11-22 21:14:38 -06:00
PrismaticFlower
ceb8cd79c3 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-11-22 21:14:38 -06:00
LeovanGit
13a6511f23 Add vertex colors to blender + fix unpack_color() 2023-11-22 21:14:38 -06:00
a62f56d461 Partial Revert: Animation Track patch cleanup 2023-11-22 21:14:38 -06:00
8a7d9b0958 Animation Track patch cleanup 2023-11-22 21:14:38 -06:00
62206e8dbc Fixed import of animations to allow for bulk import 2023-11-22 21:14:38 -06:00
William Herald Snyder
cc4a1b0e04 Create sv faces from half-edge list. Current method doesn't check for duplicates, but Blender seems to filter them anyhow. 2023-01-08 22:15:19 -05:00
William Herald Snyder
ab253f0acc Ignore swap files 2023-01-08 20:52:23 -05:00
William Herald Snyder
582ed1ace5 Create mesh from shadow geometry with verts, edges, but no faces 2023-01-07 06:51:54 -05:00
William Herald Snyder
63f9e43e17 Read SHDW chunks 2023-01-06 21:30:45 -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
10 changed files with 243 additions and 48 deletions

2
.gitignore vendored
View File

@@ -1,6 +1,8 @@
.DS_Store
*.msh
*.swp
# Created by https://www.gitignore.io/api/python,visualstudiocode
# Edit at https://www.gitignore.io/?templates=python,visualstudiocode

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',
@@ -118,6 +118,11 @@ 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,
apply_modifiers=self.apply_modifiers,
@@ -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
)

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
@@ -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

@@ -32,7 +32,111 @@ def validate_segment_geometry(segment : GeometrySegment):
return True
def model_to_mesh_object(model: Model, scene : Scene, materials_map : Dict[str, bpy.types.Material]) -> bpy.types.Object:
def get_shadow_geometry(model: Model):
for segment in model.geometry:
if segment.shadow_geometry is not None:
return segment.shadow_geometry
return None
# SHDW mesh info is of a different form from
# normal segment geometry
def model_to_shadow_mesh(model: Model, shadow_geometry : ShadowGeometry):
blender_mesh = bpy.data.meshes.new(model.name)
# As is the case with normal geometry processing,
# these will contain flattened lists
vertex_positions = [convert_vector_space(position) for position in shadow_geometry.positions]
# Vertices
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])
def faces_from_half_edges(half_edges : List[Tuple[int,int,int,int]]) -> List[List[int]]:
faces = []
visited_edges = [False] * len(half_edges)
for i in range(len(half_edges)):
if visited_edges[i]:
continue
curr_edge = half_edges[i]
curr_index = curr_edge[0]
starting_index = curr_index
face_length = 0
face_temp = [0] * 5
while True:
if face_length + 1> len(face_temp):
face_temp.append(curr_index)
else:
face_temp[face_length] = curr_index
face_length += 1
curr_edge = half_edges[curr_edge[1]]
curr_index = curr_edge[0]
if (curr_index == starting_index):
break
#print(f"Added a face of length: {face_length}")
faces.append(face_temp[0:face_length])
return faces
polygons = faces_from_half_edges(shadow_geometry.edges)
# 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)
# POLYGONS/FACES
blender_mesh.polygons.add(len(polygons))
# Indices of starting loop for each polygon
polygon_loop_start_indices = [0] * len(polygons)
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 = [0] * len(polygons)
for i,polygon in enumerate(polygons):
polygon_loop_start_indices[i] = current_polygon_start_index
current_polygon_length = len(polygon)
current_polygon_start_index += current_polygon_length
polygon_loop_totals[i] = 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.validate(clean_customdata=False)
blender_mesh.update()
#sv_name = model.name if model.name.startswith("sv_") else "sv_" + model.name
blender_mesh_object = bpy.data.objects.new(model.name, blender_mesh)
return blender_mesh_object
def model_to_mesh(model: Model, scene: Scene, materials_map : Dict[str, bpy.types.Material]) -> bpy.types.Object:
blender_mesh = bpy.data.meshes.new(model.name)
@@ -40,6 +144,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 +163,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 +182,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 +222,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,28 +239,31 @@ 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
blender_mesh.polygons.add(len(polygons))
# Indices of starting loop for each polygon
polygon_loop_start_indices = []
polygon_loop_start_indices = [0] * len(polygons)
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 = []
polygon_loop_totals = [0] * len(polygons)
for polygon in polygons:
polygon_loop_start_indices.append(current_polygon_start_index)
for i,polygon in enumerate(polygons):
polygon_loop_start_indices[i] = current_polygon_start_index
current_polygon_length = len(polygon)
current_polygon_start_index += current_polygon_length
polygon_loop_totals.append(current_polygon_length)
polygon_loop_totals[i] = current_polygon_length
blender_mesh.polygons.foreach_set("loop_start", polygon_loop_start_indices)
blender_mesh.polygons.foreach_set("loop_total", polygon_loop_totals)
@@ -189,3 +302,16 @@ def model_to_mesh_object(model: Model, scene : Scene, materials_map : Dict[str,
return blender_mesh_object
def model_to_mesh_object(model: Model, scene : Scene, materials_map : Dict[str, bpy.types.Material]) -> bpy.types.Object:
shadow_geometry = get_shadow_geometry(model)
if shadow_geometry is not None:
return model_to_shadow_mesh(model, shadow_geometry)
else:
return model_to_mesh(model, scene, materials_map)

View File

@@ -39,6 +39,20 @@ class VertexWeight:
weight: float = 1.0
bone: int = 0
@dataclass
class ShadowGeometry:
""" Class representing 'SHDW' chunks. """
# Perhaps I could just use the positions list in the segment
# class, but I don't know if SHDW info can coexist with
# a normal geometry segment...
positions: List[Vector] = field(default_factory=list)
# The second two entries may not be necessary...
edges: List[Tuple[int,int,int,int]] = field(default_factory=list)
@dataclass
class GeometrySegment:
""" Class representing a 'SEGM' section in a .msh file. """
@@ -56,6 +70,7 @@ class GeometrySegment:
triangles: List[List[int]] = field(default_factory=list)
triangle_strips: List[List[int]] = None
shadow_geometry: ShadowGeometry = None
@dataclass
class CollisionPrimitive:
@@ -73,7 +88,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,9 @@ 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:
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:
@@ -236,7 +247,9 @@ 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))
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
@@ -290,6 +303,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 +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

@@ -165,18 +165,22 @@ def _read_matd(matd: Reader) -> Material:
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:
@@ -203,6 +207,8 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model:
global model_counter
global mndx_remap
if index not in mndx_remap:
mndx_remap[index] = model_counter
model_counter += 1
@@ -382,6 +388,28 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment:
#if segm.read_u16 != 0:
# segm.skip_bytes(-2)
elif next_header == "SHDW":
shadow_geometry = ShadowGeometry()
with segm.read_child() as shdw:
#print("Found shadow chunk")
num_positions = shdw.read_u32()
#print(f" Num verts in shadow mesh: {num_positions}")
shadow_geometry.positions = [shdw.read_vec() for _ in range(num_positions)]
num_edges = shdw.read_u32()
#print(f" Num edges in shadow mesh: {num_edges}")
edges = []
for i in range(num_edges):
edges.append(tuple(shdw.read_u16(4)))
#print(" " + str(edges[-1]))
shadow_geometry.edges = edges
geometry_seg.shadow_geometry = shadow_geometry
elif next_header == "WGHT":
with segm.read_child() as wght:

View File

@@ -44,10 +44,9 @@ def extract_models(scene: Scene, materials_map : Dict[str, bpy.types.Material])
new_obj = bpy.data.objects.new(model.name, None)
new_obj.empty_display_size = 1
new_obj.empty_display_type = 'PLAIN_AXES'
new_obj.name = model.name
model_map[model.name] = new_obj
new_obj.name = model.name
if model.parent:
new_obj.parent = model_map[model.parent]
@@ -195,6 +194,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]