Skip to content

Commit

Permalink
fix(ymap): error when loading cargen mesh .obj
Browse files Browse the repository at this point in the history
Blender 4.0 removed `bpy.ops.import_scene.obj`. Refactored the .obj
reader used in the archetype extension gizmos into its own module
to use it in ymap code. Also added tests for this .obj reader.

Fixes Sollumz#825.
  • Loading branch information
alexguirre committed Nov 26, 2023
1 parent bff527b commit a110936
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 44 deletions.
57 changes: 57 additions & 0 deletions shared/obj_reader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""
Simple Wavefront .obj reader for loading Sollumz builtin models. Only supports
vertices (`v`) and triangular faces (`f` with three vertex indices).
"""

import bpy
import io
import numpy as np
from numpy.typing import NDArray
from typing import NamedTuple
from pathlib import Path


class ObjMesh(NamedTuple):
vertices: NDArray[np.float32]
indices: NDArray[np.uint16]

def as_vertices_only(self) -> NDArray[np.float32]:
return self.vertices[self.indices.flatten()]

def as_bpy_mesh(self, name: str) -> bpy.types.Mesh:
mesh = bpy.data.meshes.new(name)
mesh.from_pydata(self.vertices, [], self.indices)
return mesh


def obj_read(obj_io: io.TextIOBase) -> ObjMesh:
vertices = []
indices = []
for line in obj_io.readlines():
line = line.strip()
c = line[0] if len(line) > 0 else None
match c:
case "v":
x, y, z = line.strip("v ").split(" ")
vertices.extend((float(x), float(y), float(z)))
case "f":
v0, v1, v2 = line.strip("f ").split(" ")
indices.extend((int(v0) - 1, int(v1) - 1, int(v2) - 1))
case _:
# ignore unknown/unsupported elements
pass

return ObjMesh(
vertices=np.reshape(np.array(vertices, dtype=np.float32), (-1, 3)),
indices=np.reshape(np.array(indices, dtype=np.uint16), (-1, 3))
)


def obj_read_from_file(file_path: Path) -> ObjMesh:
with file_path.open("r") as f:
return obj_read(f)


def obj_read_from_str(obj_str: str) -> ObjMesh:
with io.StringIO(obj_str) as s:
return obj_read(s)
109 changes: 109 additions & 0 deletions tests/test_obj_reader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import bpy
import pytest
import numpy as np
from numpy.testing import assert_array_equal
from pathlib import Path
from ..shared.obj_reader import obj_read_from_str, obj_read_from_file, ObjMesh


def test_obj_read():
obj = obj_read_from_str("""
v 1.2 3.4 5.6
v 2.0 4.0 6.0
v 10.0 5.0 10.0
v 1.0 2.0 3.0
f 1 2 3
f 4 3 1
""")

assert obj is not None

assert_array_equal(obj.vertices, np.array([
[1.2, 3.4, 5.6],
[2.0, 4.0, 6.0],
[10.0, 5.0, 10.0],
[1.0, 2.0, 3.0],
], dtype=np.float32))

assert_array_equal(obj.indices, np.array([
[0, 1, 2],
[3, 2, 0],
], dtype=np.uint16))


def test_obj_read_ignores_comments_and_unknown_elements():
obj = obj_read_from_str("""
# comment
v 1.0 2.0 3.0
# other comment
u unknown
f 1 2 3
""")

assert obj is not None

assert_array_equal(obj.vertices, np.array([
[1.0, 2.0, 3.0],
], dtype=np.float32))

assert_array_equal(obj.indices, np.array([
[0, 1, 2],
], dtype=np.uint16))


def test_obj_as_vertices_only():
obj_mesh = ObjMesh(
vertices=np.array([
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
[7.0, 8.0, 9.0],
], dtype=np.float32),
indices=np.array([
[0, 1, 2],
[2, 1, 0],
], dtype=np.uint16)
)

obj_mesh_vertices = obj_mesh.as_vertices_only()
assert_array_equal(obj_mesh_vertices, np.array([
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
[7.0, 8.0, 9.0],
[7.0, 8.0, 9.0],
[4.0, 5.0, 6.0],
[1.0, 2.0, 3.0],
]))


@pytest.mark.parametrize("obj_relative_file_path", (
"../tools/car_model.obj",
"../ytyp/gizmos/models/AudioEmitter.obj",
"../ytyp/gizmos/models/AudioCollisionSettings.obj",
"../ytyp/gizmos/models/Buoyancy.obj",
"../ytyp/gizmos/models/Door.obj",
"../ytyp/gizmos/models/Expression.obj",
"../ytyp/gizmos/models/Ladder.obj",
"../ytyp/gizmos/models/LadderTop.obj",
"../ytyp/gizmos/models/LadderBottom.obj",
"../ytyp/gizmos/models/LightShaft.obj",
"../ytyp/gizmos/models/ParticleEffect.obj",
"../ytyp/gizmos/models/ProcObject.obj",
"../ytyp/gizmos/models/SpawnPoint.obj",
"../ytyp/gizmos/models/SpawnPointOverride.obj",
"../ytyp/gizmos/models/WindDisturbance.obj",
))
def test_obj_read_sollumz_builtin_asset(obj_relative_file_path: str):
obj_path = Path(__file__).parent.joinpath(obj_relative_file_path)
obj = obj_read_from_file(obj_path)

assert obj is not None
assert len(obj.vertices.flatten()) > 0
assert len(obj.indices.flatten()) > 0

obj_mesh = obj.as_bpy_mesh(obj_path.stem)
assert obj_mesh is not None

invalid_geom = obj_mesh.validate(verbose=True)
assert not invalid_geom

bpy.data.meshes.remove(obj_mesh)
File renamed without changes.
16 changes: 15 additions & 1 deletion tools/ymaphelper.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import bpy

from pathlib import Path
from ..sollumz_properties import SOLLUMZ_UI_NAMES, SollumType
from ..tools.blenderhelper import find_bsdf_and_material_output
from ..shared.obj_reader import obj_read_from_file

# TODO: This is not a real flag calculation, definitely need to do better

Expand Down Expand Up @@ -70,3 +71,16 @@ def add_occluder_material(sollum_type=None):
bsdf.inputs["Metallic"].default_value = 0

return material


CARGEN_MESH_NAME = ".sollumz.cargen_mesh"


def get_cargen_mesh() -> bpy.types.Mesh:
mesh = bpy.data.meshes.get(CARGEN_MESH_NAME, None)
if mesh is None:
file_path = Path(__file__).parent.joinpath("car_model.obj")
cargen_obj_mesh = obj_read_from_file(file_path)
mesh = cargen_obj_mesh.as_bpy_mesh(CARGEN_MESH_NAME)

return mesh
16 changes: 6 additions & 10 deletions ymap/operators.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import bpy
import os

from mathutils import Vector
from ..sollumz_helper import SOLLUMZ_OT_base, set_object_collection
from ..tools.ymaphelper import add_occluder_material, create_ymap, create_ymap_group
from ..sollumz_properties import SOLLUMZ_UI_NAMES, SollumType
from ..tools.ymaphelper import add_occluder_material, create_ymap, create_ymap_group, get_cargen_mesh
from ..sollumz_properties import SollumType


class SOLLUMZ_OT_create_ymap(SOLLUMZ_OT_base, bpy.types.Operator):
Expand Down Expand Up @@ -162,10 +159,8 @@ class SOLLUMZ_OT_create_car_generator(SOLLUMZ_OT_base, bpy.types.Operator):

def run(self, context):
group_obj = context.active_object
file_loc = os.path.join(os.path.dirname(__file__), "car_model.obj")
bpy.ops.import_scene.obj(filepath=file_loc)
cargen_obj = bpy.context.selected_objects[0]
cargen_obj.name = "Car Generator"
cargen_ref_mesh = get_cargen_mesh()
cargen_obj = bpy.data.objects.new("Car Generator", object_data=cargen_ref_mesh)
cargen_obj.sollum_type = SollumType.YMAP_CAR_GENERATOR
cargen_obj.ymap_cargen_properties.orient_x = 0.0
cargen_obj.ymap_cargen_properties.orient_y = 0.0
Expand All @@ -178,7 +173,8 @@ def run(self, context):
cargen_obj.ymap_cargen_properties.body_color_remap_4 = -1
cargen_obj.ymap_cargen_properties.pop_group = ""
cargen_obj.ymap_cargen_properties.livery = -1
bpy.context.collection.objects.link(cargen_obj)
bpy.context.view_layer.objects.active = cargen_obj
cargen_obj.parent = group_obj

return True
return True
20 changes: 3 additions & 17 deletions ymap/ymapimport.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import binascii
import struct
import math
import os
import bpy

from mathutils import Vector, Euler
from ..sollumz_helper import duplicate_object_with_children, set_object_collection
from ..tools.ymaphelper import add_occluder_material
from ..tools.ymaphelper import add_occluder_material, get_cargen_mesh
from ..sollumz_properties import SollumType
from ..sollumz_preferences import get_import_settings
from ..cwxml.ymap import CMapData, OccludeModel, YMAP
Expand Down Expand Up @@ -210,11 +208,10 @@ def cargen_to_obj(obj: bpy.types.Object, ymap: CMapData):
bpy.context.collection.objects.link(group_obj)
bpy.context.view_layer.objects.active = group_obj

cargen_ref_mesh = import_cargen_mesh()
cargen_ref_mesh = get_cargen_mesh()

for cargen in ymap.car_generators:
cargen_obj = bpy.data.objects.new(
"Car Generator", object_data=cargen_ref_mesh)
cargen_obj = bpy.data.objects.new("Car Generator", object_data=cargen_ref_mesh)
cargen_obj.ymap_cargen_properties.orient_x = cargen.orient_x
cargen_obj.ymap_cargen_properties.orient_y = cargen.orient_y
cargen_obj.ymap_cargen_properties.perpendicular_length = cargen.perpendicular_length
Expand All @@ -235,17 +232,6 @@ def cargen_to_obj(obj: bpy.types.Object, ymap: CMapData):
cargen_obj.parent = group_obj


def import_cargen_mesh() -> bpy.types.Mesh:
file_loc = os.path.join(os.path.dirname(__file__), "car_model.obj")
bpy.ops.import_scene.obj(filepath=file_loc)
cargen_ref_obj = bpy.context.selected_objects[0]
mesh = cargen_ref_obj.data

bpy.data.objects.remove(cargen_ref_obj)

return mesh


def ymap_to_obj(ymap: CMapData):
ymap_obj = bpy.data.objects.new(ymap.name, None)
ymap_obj.sollum_type = SollumType.YMAP
Expand Down
21 changes: 5 additions & 16 deletions ytyp/gizmos/extensions.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import bpy
import math
import os
import functools
from pathlib import Path
from mathutils import Matrix, Vector, Quaternion
from typing import Literal
from collections.abc import Iterator
from ..utils import get_selected_archetype, get_selected_extension, get_selected_ytyp
from ...tools.blenderhelper import tag_redraw
from ...sollumz_properties import ArchetypeType
from ...shared.obj_reader import obj_read_from_file
from ..properties.ytyp import ArchetypeProperties
from ..properties.extensions import ExtensionType, ExtensionProperties

Expand Down Expand Up @@ -46,21 +47,9 @@ def get_extension_shapes() -> dict[ExtensionType, object]:
shapes = {}

def _load_extension_model(extension_type: ExtensionType, file_name: str):
file_loc = os.path.join(os.path.dirname(__file__), "models", file_name)
with open(file_loc, "r") as f:
verts = []
model_verts = []
for line in f.readlines():
if line.startswith("v"):
x, y, z = line.strip("v ").split(" ")
verts.append((float(x), float(y), float(z)))
elif line.startswith("f"):
v0, v1, v2 = line.strip("f ").split(" ")
model_verts.append(verts[int(v0) - 1])
model_verts.append(verts[int(v1) - 1])
model_verts.append(verts[int(v2) - 1])

shapes[extension_type] = bpy.types.Gizmo.new_custom_shape("TRIS", model_verts)
file_path = Path(__file__).parent.joinpath("models", file_name)
obj_mesh = obj_read_from_file(file_path)
shapes[extension_type] = bpy.types.Gizmo.new_custom_shape("TRIS", obj_mesh.as_vertices_only())

for ext_type, model_file_name in (
(ExtensionType.AUDIO_COLLISION, "AudioCollisionSettings.obj"),
Expand Down

0 comments on commit a110936

Please sign in to comment.