Skip to content

Commit 0f395d6

Browse files
committed
feat: add xbox mesh importing
1 parent f1c9cf2 commit 0f395d6

File tree

5 files changed

+345
-7
lines changed

5 files changed

+345
-7
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ Blender 4.1.1 is supported.
1515
- CFP file compression
1616
- Mesh cleanup option
1717
- File search directory option
18+
- Import console version meshes (Xbox only)
1819

1920
You can find out how to use the add-on in the [wiki](https://github.com/mixsims/ts1-blender-io/wiki).

addons/io_scene_ts1/__init__.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"name": "The Sims 1 3D Formats",
55
"description": "Imports and exports The Sims 1 meshes and animations.",
66
"author": "mix",
7-
"version": (1, 0, 3),
7+
"version": (1, 1, 0),
88
"blender": (4, 1, 0),
99
"location": "File > Import-Export",
1010
"warning": "",
@@ -34,13 +34,13 @@ class TS1IOImport(bpy.types.Operator, bpy_extras.io_utils.ImportHelper):
3434

3535
bl_idname: str = "import.bcf"
3636
bl_label: str = "Import The Sims 1 meshes and animations"
37-
bl_description: str = "Import a cmx or bcf file from The Sims 1"
37+
bl_description: str = "Import a cmx/bcf or xbm file from The Sims 1"
3838
bl_options: typing.ClassVar[set[str]] = {'UNDO'}
3939

4040
filename_ext = ".cmx.bcf"
4141

4242
filter_glob: bpy.props.StringProperty( # type: ignore[valid-type]
43-
default="*.cmx;*.cmx.bcf",
43+
default="*.xbm;*.cmx;*.cmx.bcf",
4444
options={'HIDDEN'},
4545
)
4646
files: bpy.props.CollectionProperty( # type: ignore[valid-type]
@@ -214,7 +214,7 @@ def draw(self, _: bpy.context) -> None:
214214

215215
def menu_import(self: bpy.types.TOPBAR_MT_file_import, _: bpy.context) -> None:
216216
"""Add an entry to the import menu."""
217-
self.layout.operator(TS1IOImport.bl_idname, text="The Sims 1 (.cmx/.bcf)")
217+
self.layout.operator(TS1IOImport.bl_idname, text="The Sims 1 (.cmx/.bcf/.xbm)")
218218

219219

220220
def menu_export(self: bpy.types.TOPBAR_MT_file_export, _: bpy.context) -> None:
@@ -234,9 +234,17 @@ class TS1IOAddonPreferences(bpy.types.AddonPreferences):
234234
default="",
235235
)
236236

237+
file_search_directory_xbox: bpy.props.StringProperty( # type: ignore[valid-type]
238+
name="Xbox Textures",
239+
description="Directory that will be searched to find textures for xbox meshes",
240+
subtype='DIR_PATH',
241+
default="",
242+
)
243+
237244
def draw(self, _: bpy.context) -> None:
238245
"""Draw the addon preferences ui."""
239246
self.layout.prop(self, "file_search_directory")
247+
self.layout.prop(self, "file_search_directory_xbox")
240248

241249

242250
classes = (

addons/io_scene_ts1/import_ts1.py

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
import mathutils
99
import pathlib
1010
import re
11+
import contextlib
1112

1213

1314
from . import bcf
1415
from . import bmf
1516
from . import cfp
1617
from . import cmx
18+
from . import xbm
1719
from . import skn
1820
from . import texture_loader
1921
from . import utils
@@ -531,7 +533,113 @@ def import_skill( # noqa: C901 PLR0912 PLR0915
531533
armature_object.animation_data.action = original_action
532534

533535

534-
def import_files( # noqa: C901 PLR0912 PLR0913
536+
def import_xbox_model( # noqa: C901 PLR0912 PLR0915
537+
context: bpy.types.Context,
538+
logger: logging.Logger,
539+
file_path: pathlib.Path,
540+
xbox_texture_file_list: list[pathlib.Path],
541+
) -> None:
542+
"""Import an xbox model file."""
543+
try:
544+
model_desc = xbm.read_file(file_path)
545+
except utils.FileReadError as _:
546+
logger.info(f"Could not load xbox mesh {file_path}") # noqa: G004
547+
return
548+
549+
file_collection = bpy.data.collections.get(model_desc.name)
550+
if file_collection is None:
551+
file_collection = bpy.data.collections.new(model_desc.name)
552+
553+
if file_collection.name not in context.collection.children:
554+
context.collection.children.link(file_collection)
555+
556+
for object_index, object_desc in enumerate(model_desc.objects):
557+
object_collection_name = f"{model_desc.name} {object_index}"
558+
559+
object_collection = bpy.data.collections.get(object_collection_name)
560+
if object_collection is None:
561+
object_collection = bpy.data.collections.new(object_collection_name)
562+
563+
if object_collection.name not in file_collection.children:
564+
file_collection.children.link(object_collection)
565+
566+
for mesh_index, mesh_desc in enumerate(object_desc.meshes):
567+
mesh_name = f"{model_desc.name} {object_index} {mesh_index}"
568+
569+
mesh = bpy.data.meshes.new(mesh_name)
570+
obj = bpy.data.objects.new(mesh_name, mesh)
571+
572+
object_collection.objects.link(obj)
573+
574+
b_mesh = bmesh.new()
575+
576+
for vertex in mesh_desc.positions:
577+
position = mathutils.Vector(vertex.position)
578+
b_mesh.verts.new(position)
579+
580+
b_mesh.verts.ensure_lookup_table()
581+
b_mesh.verts.index_update()
582+
583+
if len(mesh_desc.faces):
584+
for i in range(len(mesh_desc.faces) - 2):
585+
with contextlib.suppress(ValueError):
586+
b_mesh.faces.new(
587+
(
588+
b_mesh.verts[mesh_desc.faces[i + 2]],
589+
b_mesh.verts[mesh_desc.faces[i + 1]],
590+
b_mesh.verts[mesh_desc.faces[i + 0]],
591+
),
592+
)
593+
594+
deform_layer = b_mesh.verts.layers.deform.verify()
595+
596+
for index, strip in enumerate(mesh_desc.strips):
597+
vertex_group = obj.vertex_groups.new(name=str(index))
598+
599+
for i in range(strip[0], strip[1] - 2):
600+
vert_a = b_mesh.verts[i + 0]
601+
vert_b = b_mesh.verts[i + 1]
602+
vert_c = b_mesh.verts[i + 2]
603+
604+
vert_a[deform_layer][vertex_group.index] = 1.0
605+
vert_b[deform_layer][vertex_group.index] = 1.0
606+
vert_c[deform_layer][vertex_group.index] = 1.0
607+
608+
if vert_a.co != vert_b.co and vert_a.co != vert_c.co and vert_b.co != vert_c.co: # noqa: PLR1714
609+
b_mesh.faces.new((vert_a, vert_b, vert_c))
610+
611+
if len(mesh_desc.uvs):
612+
uv_layer = b_mesh.loops.layers.uv.verify()
613+
for face in b_mesh.faces:
614+
for loop in face.loops:
615+
loop[uv_layer].uv = mesh_desc.uvs[loop.vert.index]
616+
617+
b_mesh.to_mesh(mesh)
618+
b_mesh.free()
619+
620+
if len(mesh_desc.normals) > 0:
621+
mesh.normals_split_custom_set_from_vertices(mesh_desc.normals)
622+
623+
for polygon in mesh.polygons:
624+
if polygon.normal.dot(mathutils.Vector(mesh_desc.normals[polygon.vertices[0]])) < 0.0:
625+
polygon.flip()
626+
627+
mesh.normals_split_custom_set_from_vertices(mesh_desc.normals)
628+
629+
mesh.update()
630+
631+
texture_id_string = f'{mesh_desc.texture_id:x}'
632+
for file_path in xbox_texture_file_list:
633+
if file_path.stem.endswith(texture_id_string):
634+
texture_loader.create_material(obj, file_path.stem, file_path)
635+
636+
if not obj.data.materials:
637+
for file_path in xbox_texture_file_list:
638+
if file_path.stem.lower().startswith(f"{model_desc.name.lower()} "):
639+
texture_loader.create_material(obj, file_path.stem, file_path)
640+
641+
642+
def import_files( # noqa: C901 PLR0912 PLR0913 PLR0915
535643
context: bpy.types.Context,
536644
logger: logging.Logger,
537645
file_paths: list[pathlib.Path],
@@ -640,3 +748,15 @@ def import_files( # noqa: C901 PLR0912 PLR0913
640748
for bcf_file_path, bcf_file in bcf_files:
641749
for skill in bcf_file.skills:
642750
import_skill(context, logger, bcf_file_path.parent, animation_file_list, skill)
751+
752+
file_search_directory_xbox = pathlib.Path(
753+
context.preferences.addons["io_scene_ts1"].preferences.file_search_directory_xbox,
754+
)
755+
if file_search_directory_xbox == "":
756+
file_search_directory_xbox = file_paths[0].parent
757+
file_list_xbox = list(file_search_directory_xbox.glob("*.png"))
758+
759+
for file_path in file_paths:
760+
match file_path.suffix:
761+
case ".xbm":
762+
import_xbox_model(context, logger, file_path, file_list_xbox)

addons/io_scene_ts1/texture_loader.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,9 +299,9 @@ def create_material(obj: bpy.types.Object, texture_name: str, texture_file_path:
299299
principled_bsdf.inputs[2].default_value = 1.0
300300
principled_bsdf.inputs[12].default_value = 0.0
301301

302-
if texture_file_path.suffix.lower() == ".tga":
302+
if texture_file_path.suffix.lower() == ".tga" or texture_file_path.suffix.lower() == ".png":
303303
material.node_tree.links.new(image_node.outputs[1], principled_bsdf.inputs[4])
304-
material.blend_method = 'BLEND'
304+
material.blend_method = 'HASHED'
305305

306306
if material.name not in obj.data.materials:
307307
obj.data.materials.append(material)

0 commit comments

Comments
 (0)