diff --git a/lods.py b/lods.py index 0da977a3..2cf42140 100644 --- a/lods.py +++ b/lods.py @@ -1,95 +1,164 @@ """LOD Management system.""" import bpy +from bpy.types import ( + Context, + Mesh, + PropertyGroup, + Object, +) +from bpy.props import ( + EnumProperty, + PointerProperty, + StringProperty, + BoolProperty, +) from typing import Callable, Optional -from .sollumz_properties import SollumType, LODLevel, FRAGMENT_TYPES, DRAWABLE_TYPES, SOLLUMZ_UI_NAMES, BOUND_TYPES, BOUND_POLYGON_TYPES, items_from_enums -from .tools.blenderhelper import get_all_collections, lod_level_enum_flag_prop_factory +from .sollumz_properties import ( + SollumType, + LODLevel, + LODLevelEnumItems, + FRAGMENT_TYPES, + DRAWABLE_TYPES, + SOLLUMZ_UI_NAMES, + BOUND_TYPES, + BOUND_POLYGON_TYPES, +) +from .tools.blenderhelper import lod_level_enum_flag_prop_factory from .sollumz_helper import find_sollumz_parent from .icons import icon_manager -class ObjectLODProps(bpy.types.PropertyGroup): - def update_mesh(self, context: bpy.types.Context): - obj: bpy.types.Object = self.id_data +class LODLevelProps(PropertyGroup): + def on_lod_level_enter(self): + """Called when the LOD level switches to this level.""" + obj: Object = self.id_data - active_obj_lod = obj.sollumz_lods.active_lod + if self.has_mesh: + # Update the object current mesh to this LOD mesh + obj.data = self.mesh_ref + self.mesh_ref = None # keep a single ref to the mesh, in Object.data - if active_obj_lod == self and self.mesh is not None: - obj.data = self.mesh - if obj.name in context.view_layer.objects: - obj.hide_set(False) - elif self.mesh is None: - if obj.name in context.view_layer.objects: - obj.hide_set(True) + if obj.name in bpy.context.view_layer.objects: + obj.hide_set(not self.has_mesh) - level: bpy.props.EnumProperty( - items=items_from_enums(LODLevel)) - mesh: bpy.props.PointerProperty( - type=bpy.types.Mesh, update=update_mesh) + def on_lod_level_exit(self): + """Called when the LOD level switches aways from this level.""" + obj: Object = self.id_data + # Store a reference to the mesh + self.mesh_ref = obj.data if self.has_mesh else None -class LODLevels(bpy.types.PropertyGroup): - def get_lod(self, lod_level: str) -> ObjectLODProps | None: - for lod in self.lods: - if lod.level == lod_level: - return lod + def _get_mesh_name(self) -> str: + m = self.mesh + return m.name if m is not None else "" - def set_lod_mesh(self, lod_level: str, mesh: bpy.types.Mesh) -> ObjectLODProps | None: - for lod in self.lods: - if lod.level == lod_level: - lod.mesh = mesh - return lod + def _set_mesh_name(self, value: str) -> str: + self.mesh = bpy.data.meshes.get(value, None) - def add_lod(self, lod_level: str, mesh: Optional[bpy.types.Mesh] = None) -> ObjectLODProps | None: - # Can't have multiple lods with the same type - if self.get_lod(lod_level): + # Wrapper property so we can modify the LOD meshes from the UI without keeping a reference to the mesh of the + # active LOD. + mesh_name: StringProperty(get=_get_mesh_name, set=_set_mesh_name) + + # Only set on non-active LOD levels, used to keep a reference to the mesh data-block so Blender doesn't remove it + # during garbage collection. For the active LOD, the mesh is the current object.data mesh. + # Blender expects a single reference to the mesh (in object.data) for many operations, otherwise asks + # the user to duplicate the mesh and then object.data becomes out-of-sync with Sollumz LODs. + mesh_ref: PointerProperty(type=Mesh) # DO NOT MODIFY DIRECTLY OUTSIDE THIS CLASS, use .mesh or .mesh_name + + # Whether this LOD actually has a mesh. Needed because when a level that doesn't have a mesh becomes the active LOD, + # object.data keeps the previous mesh and instead the object is hidden, so when switching away we wouldn't know if + # object.data is actually our mesh or not. + has_mesh: BoolProperty(default=False) # DO NOT MODIFY DIRECTLY OUTSIDE THIS CLASS, use .mesh or .mesh_name + + @property + def mesh(self) -> Optional[Mesh]: + """Gets the mesh of this LOD level, or ``None`` if there is no mesh.""" + if not self.has_mesh: return None - self.lods.add() - i = len(self.lods) - 1 - obj_lod = self.lods[i] - obj_lod.level = lod_level + obj: Object = self.id_data + lods: LODLevels = obj.sz_lods + if lods.active_lod_level == self.level: + return obj.data + else: + return self.mesh_ref + + @mesh.setter + def mesh(self, value: Optional[Mesh]): + """Sets the mesh of this LOD level. Set to ``None`` to remove the mesh.""" + obj: Object = self.id_data + lods: LODLevels = obj.sz_lods + self.has_mesh = value is not None + if lods.active_lod_level == self.level: + self.mesh_ref = None + if self.has_mesh: + obj.data = value + + if obj.name in bpy.context.view_layer.objects: + obj.hide_set(not self.has_mesh) + else: + self.mesh_ref = value - if mesh is not None: - obj_lod.mesh = mesh + @property + def level(self) -> LODLevel: + obj: Object = self.id_data + lods: LODLevels = obj.sz_lods + if lods.very_high == self: + return LODLevel.VERYHIGH + if lods.high == self: + return LODLevel.HIGH + if lods.medium == self: + return LODLevel.MEDIUM + if lods.low == self: + return LODLevel.LOW + if lods.very_low == self: + return LODLevel.VERYLOW + + assert False, "LODLevelProps for unknown LOD level" + + +class LODLevels(PropertyGroup): + def on_lod_level_update(self, context: Context): + prev_lod = self.get_lod(self.active_lod_level_prev) + curr_lod = self.get_lod(self.active_lod_level) + prev_lod.on_lod_level_exit() + curr_lod.on_lod_level_enter() + self.active_lod_level_prev = self.active_lod_level + + active_lod_level: EnumProperty(items=LODLevelEnumItems, update=on_lod_level_update) + active_lod_level_prev: EnumProperty(items=LODLevelEnumItems) + very_high: PointerProperty(type=LODLevelProps) + high: PointerProperty(type=LODLevelProps) + medium: PointerProperty(type=LODLevelProps) + low: PointerProperty(type=LODLevelProps) + very_low: PointerProperty(type=LODLevelProps) - return obj_lod + @property + def active_lod(self) -> LODLevelProps: + return self.get_lod(self.active_lod_level) + + def get_lod(self, lod_level: LODLevel) -> LODLevelProps: + match lod_level: + case LODLevel.VERYHIGH: + return self.very_high + case LODLevel.HIGH: + return self.high + case LODLevel.MEDIUM: + return self.medium + case LODLevel.LOW: + return self.low + case LODLevel.VERYLOW: + return self.very_low + case _: + assert False, f"Unknown LOD level '{lod_level}'" def set_highest_lod_active(self): - lod_levels = [LODLevel.VERYHIGH, LODLevel.HIGH, - LODLevel.MEDIUM, LODLevel.LOW, LODLevel.VERYLOW] - - for lod_level in lod_levels: + for lod_level in LODLevel: lod = self.get_lod(lod_level) if lod.mesh is not None: - self.set_active_lod(lod_level) + self.active_lod_level = lod_level return - def set_active_lod(self, lod_level: str): - for i, lod in enumerate(self.lods): - if lod.level == lod_level: - self.active_lod_index = i - return - - def update_active_lod(self, context): - self.active_lod.update_mesh(context) - - def add_empty_lods(self): - """Add all LOD lods with no meshes assigned.""" - self.add_lod(LODLevel.VERYHIGH) - self.add_lod(LODLevel.HIGH) - self.add_lod(LODLevel.MEDIUM) - self.add_lod(LODLevel.LOW) - self.add_lod(LODLevel.VERYLOW) - - @property - def active_lod(self) -> ObjectLODProps | None: - if self.active_lod_index < len(self.lods): - return self.lods[self.active_lod_index] - - lods: bpy.props.CollectionProperty(type=ObjectLODProps) - active_lod_index: bpy.props.IntProperty( - min=0, update=update_active_lod) - class SetLodLevelHelper: """Helper class for setting the LOD level of Sollumz objects.""" @@ -159,8 +228,8 @@ def execute(self, context): obj.hide_set(do_hide) for child in obj.children_recursive: - active_lod = child.sollumz_lods.active_lod - if child.sollum_type != SollumType.DRAWABLE_MODEL or not active_lod or active_lod.mesh is None: + active_lod = child.sz_lods.active_lod + if child.sollum_type != SollumType.DRAWABLE_MODEL or active_lod.mesh is None: continue child.hide_set(do_hide) @@ -228,28 +297,29 @@ class SOLLUMZ_OT_copy_lod(bpy.types.Operator): def poll(self, context): aobj = context.active_object - return aobj is not None and aobj.sollumz_lods.active_lod is not None + return aobj is not None and aobj.sz_lods.active_lod.mesh is not None def draw(self, context): self.layout.props_enum(self, "copy_lod_levels") def execute(self, context): aobj = context.active_object + lods = aobj.sz_lods - active_lod = aobj.sollumz_lods.active_lod + active_lod = lods.active_lod + active_lod_mesh = active_lod.mesh for lod_level in self.copy_lod_levels: - lod = aobj.sollumz_lods.get_lod(lod_level) + lod = lods.get_lod(lod_level) if lod is None: return {"CANCELLED"} if lod.mesh is not None: - self.report( - {"INFO"}, f"{SOLLUMZ_UI_NAMES[lod_level]} already has a mesh!") + self.report({"INFO"}, f"{SOLLUMZ_UI_NAMES[lod_level]} already has a mesh!") return {"CANCELLED"} - lod.mesh = active_lod.mesh.copy() + lod.mesh = active_lod_mesh.copy() return {"FINISHED"} @@ -257,20 +327,6 @@ def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) -class SOLLUMZ_UL_OBJ_LODS_LIST(bpy.types.UIList): - bl_idname = "SOLLUMZ_UL_OBJ_LODS_LIST" - - def draw_item( - self, context, layout: bpy.types.UILayout, data, item, icon, active_data, active_propname, index - ): - col = layout.column() - col.scale_x = 0.35 - col.label(text=SOLLUMZ_UI_NAMES[item.level]) - col = layout.column() - col.scale_x = 0.65 - col.prop(item, "mesh", text="") - - class SOLLUMZ_PT_LOD_LEVEL_PANEL(bpy.types.Panel): bl_label = "Sollumz LODs" bl_idname = "SOLLUMZ_PT_LOD_LEVEL_PANEL" @@ -289,15 +345,24 @@ def draw_header(self, context): def draw(self, context): layout = self.layout active_obj = context.view_layer.objects.active + lods = active_obj.sz_lods layout.enabled = active_obj.mode == "OBJECT" - row = layout.row() - row.template_list( - SOLLUMZ_UL_OBJ_LODS_LIST.bl_idname, "", active_obj.sollumz_lods, "lods", active_obj.sollumz_lods, "active_lod_index" - ) - - row.operator("sollumz.copy_lod", icon="COPYDOWN", text="") + col = row.column(align=True) + for lod_level in ( + LODLevel.VERYHIGH, + LODLevel.HIGH, + LODLevel.MEDIUM, + LODLevel.LOW, + LODLevel.VERYLOW, + ): + lod = lods.get_lod(lod_level) + lod_split = col.split(align=True, factor=0.3) + lod_split.prop_enum(lods, "active_lod_level", lod_level) + lod_split.prop_search(lod, "mesh_name", bpy.data, "meshes", text="") + + row.operator(SOLLUMZ_OT_copy_lod.bl_idname, icon="COPYDOWN", text="") def set_collision_visibility(is_visible: bool): @@ -327,7 +392,7 @@ def set_all_lods(obj: bpy.types.Object, lod_level: LODLevel): for child in obj.children_recursive: if child.type == "MESH" and child.sollum_type == SollumType.DRAWABLE_MODEL: - child.sollumz_lods.set_active_lod(lod_level) + child.sz_lods.active_lod_level = lod_level continue @@ -336,15 +401,15 @@ def operates_on_lod_level(func: Callable): Will automatically set the LOD level to ``lod_level`` at the beginning of execution and will set it back to the original LOD level at the end.""" def wrapper(model_obj: bpy.types.Object, lod_level: LODLevel, *args, **kwargs): - current_lod_level = model_obj.sollumz_lods.active_lod.level + current_lod_level = model_obj.sz_lods.active_lod_level was_hidden = model_obj.hide_get() - model_obj.sollumz_lods.set_active_lod(lod_level) + model_obj.sz_lods.active_lod_level = lod_level res = func(model_obj, lod_level, *args, **kwargs) # Set the lod level back to what it was - model_obj.sollumz_lods.set_active_lod(current_lod_level) + model_obj.sz_lods.active_lod_level = current_lod_level model_obj.hide_set(was_hidden) return res @@ -353,19 +418,15 @@ def wrapper(model_obj: bpy.types.Object, lod_level: LODLevel, *args, **kwargs): def register(): - bpy.types.Object.sollumz_lods = bpy.props.PointerProperty( - type=LODLevels) + bpy.types.Object.sz_lods = bpy.props.PointerProperty(type=LODLevels) bpy.types.Object.sollumz_obj_is_hidden = bpy.props.BoolProperty() - bpy.types.Scene.sollumz_show_collisions = bpy.props.BoolProperty( - default=True) - bpy.types.Scene.sollumz_show_shattermaps = bpy.props.BoolProperty( - default=True) - bpy.types.Scene.sollumz_copy_lod_level = bpy.props.EnumProperty( - items=items_from_enums(LODLevel)) + bpy.types.Scene.sollumz_show_collisions = bpy.props.BoolProperty(default=True) + bpy.types.Scene.sollumz_show_shattermaps = bpy.props.BoolProperty(default=True) + bpy.types.Scene.sollumz_copy_lod_level = bpy.props.EnumProperty(items=LODLevelEnumItems) def unregister(): - del bpy.types.Object.sollumz_lods + del bpy.types.Object.sz_lods del bpy.types.Object.sollumz_obj_is_hidden del bpy.types.Scene.sollumz_show_collisions del bpy.types.Scene.sollumz_show_shattermaps diff --git a/sollumz_helper.py b/sollumz_helper.py index 30dd6a4a..e0946fd2 100644 --- a/sollumz_helper.py +++ b/sollumz_helper.py @@ -141,11 +141,14 @@ def get_sollumz_materials(obj: bpy.types.Object): if child.sollum_type != SollumType.DRAWABLE_MODEL: continue - for lod in child.sollumz_lods.lods: - if lod.mesh is None: + lods = child.sz_lods + for lod_level in LODLevel: + lod = lods.get_lod(lod_level) + lod_mesh = lod.mesh + if lod_mesh is None: continue - mats = lod.mesh.materials + mats = lod_mesh.materials for mat in mats: if mat.sollum_type != MaterialType.SHADER: diff --git a/sollumz_operators.py b/sollumz_operators.py index 72ee55f1..e6ae9f4a 100644 --- a/sollumz_operators.py +++ b/sollumz_operators.py @@ -663,7 +663,7 @@ def execute(self, context): bpy.data.objects.remove(obj) model_obj = create_blender_object(SollumType.DRAWABLE_MODEL) - model_obj.sollumz_lods.add_empty_lods() + model_lods = model_obj.sz_lods old_mesh = model_obj.data for lod_level, geometries in models_by_lod.items(): @@ -673,8 +673,8 @@ def execute(self, context): else: joined_obj = geometries[0] - model_obj.sollumz_lods.set_lod_mesh(lod_level, joined_obj.data) - model_obj.sollumz_lods.set_active_lod(lod_level) + model_lods.get_lod(lod_level).mesh = joined_obj.data + model_lods.active_lod_level = lod_level context.view_layer.objects.active = joined_obj model_obj.select_set(True) @@ -688,8 +688,8 @@ def execute(self, context): # Set highest lod level for lod_level in [LODLevel.HIGH, LODLevel.MEDIUM, LODLevel.LOW, LODLevel.VERYLOW]: - if model_obj.sollumz_lods.get_lod(lod_level) != None: - model_obj.sollumz_lods.set_active_lod(lod_level) + if model_lods.get_lod(lod_level).mesh is not None: + model_lods.active_lod_level = lod_level break return {"FINISHED"} diff --git a/sollumz_properties.py b/sollumz_properties.py index 20f0ee65..2ddef9d6 100644 --- a/sollumz_properties.py +++ b/sollumz_properties.py @@ -127,6 +127,15 @@ class LODLevel(str, Enum): VERYHIGH = "sollumz_veryhigh" +LODLevelEnumItems = ( + (LODLevel.HIGH, "High", "", 0), + (LODLevel.MEDIUM, "Medium", "", 1), + (LODLevel.LOW, "Low", "", 2), + (LODLevel.VERYLOW, "Very Low", "", 3), + (LODLevel.VERYHIGH, "Very High", "", 4), +) + + class EntityLodLevel(str, Enum): LODTYPES_DEPTH_HD = "sollumz_lodtypes_depth_hd" LODTYPES_DEPTH_LOD = "sollumz_lodtypes_depth_lod" @@ -357,7 +366,7 @@ class VehiclePaintLayer(str, Enum): LODLevel.VERYHIGH: "Very High", LODLevel.HIGH: "High", - LODLevel.MEDIUM: "Med", + LODLevel.MEDIUM: "Medium", LODLevel.LOW: "Low", LODLevel.VERYLOW: "Very Low", diff --git a/tests/versioning/data/v240_lods.blend b/tests/versioning/data/v240_lods.blend new file mode 100644 index 00000000..f1a21a58 Binary files /dev/null and b/tests/versioning/data/v240_lods.blend differ diff --git a/tests/versioning/test_versioning_240.py b/tests/versioning/test_versioning_240.py new file mode 100644 index 00000000..8841ccb9 --- /dev/null +++ b/tests/versioning/test_versioning_240.py @@ -0,0 +1,45 @@ +import bpy +from pathlib import Path +from ..shared import SOLLUMZ_TEST_VERSIONING_DATA_DIR +from ...versioning import do_versions +from ...sollumz_properties import LODLevel + + +def data_path(file_name: str) -> Path: + path = SOLLUMZ_TEST_VERSIONING_DATA_DIR.joinpath(file_name) + assert path.exists() + return path + + +def load_blend_data(data: bpy.types.BlendData, file_name: str): + with data.libraries.load(str(data_path(file_name))) as (data_from, data_to): + for attr in dir(data_to): + setattr(data_to, attr, getattr(data_from, attr)) + + +def test_versioning_lods(): + with bpy.data.temp_data() as temp_data: + load_blend_data(temp_data, "v240_lods.blend") + + do_versions(temp_data) + + obj = temp_data.objects["Sphere.model"] + lods = obj.sz_lods + + assert lods.active_lod_level == LODLevel.VERYHIGH + assert lods.active_lod_level_prev == LODLevel.VERYHIGH + for lod_level, mesh_name in ( + (LODLevel.VERYHIGH, "Sphere.very_high"), + (LODLevel.HIGH, "Sphere.high"), + (LODLevel.MEDIUM, "Sphere.med"), + (LODLevel.LOW, "Sphere.low"), + (LODLevel.VERYLOW, "Sphere.very_low"), + ): + lod = lods.get_lod(lod_level) + assert lod.has_mesh + if lod_level == LODLevel.VERYHIGH: + assert lod.mesh_ref is None # active LOD, no ref + else: + assert lod.mesh_ref == temp_data.meshes[mesh_name] + assert lod.mesh == temp_data.meshes[mesh_name] + assert lod.mesh_name == mesh_name diff --git a/tools/drawablehelper.py b/tools/drawablehelper.py index d66b478d..88ec1218 100644 --- a/tools/drawablehelper.py +++ b/tools/drawablehelper.py @@ -240,9 +240,8 @@ def convert_objs_to_single_drawable(objs: list[bpy.types.Object]): def convert_obj_to_model(obj: bpy.types.Object): obj.sollum_type = SollumType.DRAWABLE_MODEL - obj.sollumz_lods.add_empty_lods() - obj.sollumz_lods.set_lod_mesh(LODLevel.HIGH, obj.data) - obj.sollumz_lods.set_active_lod(LODLevel.HIGH) + obj.sz_lods.get_lod(LODLevel.HIGH).mesh = obj.data + obj.sz_lods.active_lod_level = LODLevel.HIGH def center_drawable_to_models(drawable_obj: bpy.types.Object): diff --git a/versioning/__init__.py b/versioning/__init__.py index 63f54d9b..24853fe7 100644 --- a/versioning/__init__.py +++ b/versioning/__init__.py @@ -12,7 +12,7 @@ SOLLUMZ_INTERNAL_VERSION_MISSING = -1 """Represents a .blend file not yet saved or saved before versioning system.""" -SOLLUMZ_INTERNAL_VERSION = 1 +SOLLUMZ_INTERNAL_VERSION = 2 """Current internal version for Sollumz data stored in .blend files. Independent of release versions. @@ -22,9 +22,10 @@ Version History: == v2.3.1 == - 0: changes between 2.3.1 and 2.4.0, until the introduction of versioning system. - - 1: renamed LightFlags, light shadow_blur normalized to 0.0-1.0 range - - : + - 1: renamed LightFlags, light shadow_blur normalized to 0.0-1.0 range. == v2.4.0 == + - 2: LOD system fixes. + - : """ @@ -65,8 +66,9 @@ def do_versions(data: bpy.types.BlendData): log(f"Upgrading Sollumz data from version {data_version} to version {SOLLUMZ_INTERNAL_VERSION}") - from . import versioning_230 + from . import versioning_230, versioning_240 versioning_230.do_versions(data_version, data) + versioning_240.do_versions(data_version, data) def register(): diff --git a/versioning/versioning_240.py b/versioning/versioning_240.py new file mode 100644 index 00000000..7ce692e3 --- /dev/null +++ b/versioning/versioning_240.py @@ -0,0 +1,67 @@ +"""Handle changes between 2.4.0 and 2.5.0.""" + +from bpy.types import ( + BlendData, + Object, +) + + +def update_lods(obj: Object): + lods_props = obj.get("sollumz_lods", None) + if lods_props is None: + return + + lods_arr = lods_props.get("lods", None) + if lods_arr is None or len(lods_arr) == 0: + return + + # Get LODs from the old property group + active_lod_index = lods_props.get("active_lod_index", -1) + active_lod_level_int = None + + new_lods = {} + for i, lod_props in enumerate(lods_arr): + lod_level_int = lod_props.get("level", -1) + if not 0 <= lod_level_int <= 4: + continue + + mesh = lod_props.get("mesh", None) + if mesh is None: + continue + + new_lods[lod_level_int] = mesh + if i == active_lod_index: + active_lod_level_int = lod_level_int + + # Delete the old LOD properties + del obj["sollumz_lods"] + + # Create the new LOD properties + obj["sz_lods"] = {} + new_lods_props = obj["sz_lods"] + if active_lod_level_int is not None: + new_lods_props["active_lod_level"] = active_lod_level_int + new_lods_props["active_lod_level_prev"] = active_lod_level_int + + lod_prop_names = [ + # field names in class LODLevels(PropertyGroup) + "high", # 0 + "medium", # 1 + "low", # 2 + "very_low", # 3 + "very_high", # 4 + ] + for new_lod_level, new_lod_mesh in new_lods.items(): + lod_prop_name = lod_prop_names[new_lod_level] + new_lods_props[lod_prop_name] = {} + new_lod_props = new_lods_props[lod_prop_name] + new_lod_props["has_mesh"] = new_lod_mesh is not None + if new_lod_level != active_lod_level_int: + # only non-active LODs keep a reference to the mesh + new_lod_props["mesh_ref"] = new_lod_mesh + + +def do_versions(data_version: int, data: BlendData): + if data_version < 2: + for obj in data.objects: + update_lods(obj) diff --git a/ydr/operators.py b/ydr/operators.py index f0a4792c..5f71f006 100644 --- a/ydr/operators.py +++ b/ydr/operators.py @@ -1035,23 +1035,20 @@ def execute(self, context: Context): if not lods: return {"CANCELLED"} - obj_lods: LODLevels = aobj.sollumz_lods - - if not self.has_sollumz_lods(aobj): - obj_lods.add_empty_lods() + obj_lods: LODLevels = aobj.sz_lods decimate_step = context.scene.sollumz_auto_lod_decimate_step last_mesh = ref_mesh previous_mode = aobj.mode - previous_lod_level = obj_lods.active_lod.level + previous_lod_level = obj_lods.active_lod_level for lod_level in lods: mesh = last_mesh.copy() mesh.name = self.get_lod_mesh_name(aobj.name, lod_level) - obj_lods.set_lod_mesh(lod_level, mesh) - obj_lods.set_active_lod(lod_level) + obj_lods.get_lod(lod_level).mesh = mesh + obj_lods.active_lod_level = lod_level bpy.ops.object.mode_set(mode="EDIT") bpy.ops.mesh.decimate(ratio=1.0 - decimate_step) @@ -1059,15 +1056,10 @@ def execute(self, context: Context): last_mesh = mesh - obj_lods.set_active_lod(previous_lod_level) + obj_lods.active_lod_level = previous_lod_level return {"FINISHED"} - def has_sollumz_lods(self, obj: bpy.types.Object): - """Ensure obj has sollumz_lods.lods populated""" - obj_lod_levels = [lod.level for lod in obj.sollumz_lods.lods] - return all(lod_level in obj_lod_levels for lod_level in LODLevel) - def get_lod_mesh_name(self, obj_name: str, lod_level: LODLevel): return f"{obj_name}.{SOLLUMZ_UI_NAMES[lod_level].lower()}" @@ -1093,14 +1085,14 @@ def execute(self, context: Context): parent = self.create_parent(context, f"{aobj.name}.LODs") lod_levels = context.scene.sollumz_extract_lods_levels + lods = aobj.sz_lods for lod_level in lod_levels: - lod = aobj.sollumz_lods.get_lod(lod_level) - - if lod is None or lod.mesh is None: + lod = lods.get_lod(lod_level) + lod_mesh = lod.mesh + if lod_mesh is None: continue - mesh = lod.mesh - lod_obj = create_blender_object(SollumType.NONE, mesh.name, mesh) + lod_obj = create_blender_object(SollumType.NONE, lod_mesh.name, lod_mesh) self.parent_object(lod_obj, parent) return {"FINISHED"} diff --git a/ydr/properties.py b/ydr/properties.py index 8c3e49ee..af25a69b 100644 --- a/ydr/properties.py +++ b/ydr/properties.py @@ -523,13 +523,14 @@ def get_model_properties(model_obj: bpy.types.Object, lod_level: LODLevel) -> Dr if drawable_obj is not None and model_obj.vertex_groups: return drawable_obj.skinned_model_properties.get_lod(lod_level) - lod = model_obj.sollumz_lods.get_lod(lod_level) + lod = model_obj.sz_lods.get_lod(lod_level) + lod_mesh = lod.mesh - if lod is None or lod.mesh is None: + if lod_mesh is None: raise ValueError( f"Failed to get Drawable Model properties: {model_obj.name} has no {SOLLUMZ_UI_NAMES[lod_level]} LOD!") - return lod.mesh.drawable_model_properties + return lod_mesh.drawable_model_properties def register(): diff --git a/ydr/ui.py b/ydr/ui.py index 6ec2582b..76c9b46c 100644 --- a/ydr/ui.py +++ b/ydr/ui.py @@ -83,11 +83,9 @@ def poll(cls, context): if obj is None: return False - active_lod = obj.sollumz_lods.active_lod - sollumz_parent = find_sollumz_parent( - obj, parent_type=SollumType.DRAWABLE) + sollumz_parent = find_sollumz_parent(obj, parent_type=SollumType.DRAWABLE) - return active_lod is not None and obj.sollum_type == SollumType.DRAWABLE_MODEL and obj.type == "MESH" and sollumz_parent is not None + return obj.sollum_type == SollumType.DRAWABLE_MODEL and obj.type == "MESH" and sollumz_parent is not None def draw(self, context): layout = self.layout @@ -95,10 +93,9 @@ def draw(self, context): layout.use_property_split = True obj = context.active_object - active_lod_level = obj.sollumz_lods.active_lod.level + active_lod_level = obj.sz_lods.active_lod_level mesh = obj.data - sollumz_parent = find_sollumz_parent( - obj, parent_type=SollumType.DRAWABLE) + sollumz_parent = find_sollumz_parent(obj, parent_type=SollumType.DRAWABLE) model_props = mesh.drawable_model_properties @@ -110,14 +107,11 @@ def draw(self, context): # All skinned objects (objects with vertex groups) go in the same drawable model if is_skinned_model: - model_props = sollumz_parent.skinned_model_properties.get_lod( - active_lod_level) + model_props = sollumz_parent.skinned_model_properties.get_lod(active_lod_level) - col.label( - text=f"Active LOD: {SOLLUMZ_UI_NAMES[active_lod_level]} (Skinned)") + col.label(text=f"Active LOD: {SOLLUMZ_UI_NAMES[active_lod_level]} (Skinned)") else: - col.label( - text=f"Active LOD: {SOLLUMZ_UI_NAMES[active_lod_level]}") + col.label(text=f"Active LOD: {SOLLUMZ_UI_NAMES[active_lod_level]}") col.separator() diff --git a/ydr/ydrexport.py b/ydr/ydrexport.py index c5d39909..5171d370 100644 --- a/ydr/ydrexport.py +++ b/ydr/ydrexport.py @@ -126,17 +126,20 @@ def create_model_xmls(drawable_xml: Drawable, drawable_obj: bpy.types.Object, ma for model_obj in model_objs: transforms_to_apply = get_export_transforms_to_apply(model_obj) - for lod in model_obj.sollumz_lods.lods: - if lod.mesh is None or lod.level == LODLevel.VERYHIGH: + lods = model_obj.sz_lods + for lod_level in LODLevel: + if lod_level == LODLevel.VERYHIGH: continue - model_xml = create_model_xml( - model_obj, lod.level, materials, bones, transforms_to_apply) + lod = lods.get_lod(lod_level) + if lod.mesh is None: + continue + model_xml = create_model_xml(model_obj, lod_level, materials, bones, transforms_to_apply) if not model_xml.geometries: continue - append_model_xml(drawable_xml, model_xml, lod.level) + append_model_xml(drawable_xml, model_xml, lod_level) # Drawables only ever have 1 skinned drawable model per LOD level. Since, the skinned portion of the # drawable can be split by vertex group, we have to join each separate part into a single object. diff --git a/ydr/ydrimport.py b/ydr/ydrimport.py index d20bb9d5..15ef28d6 100644 --- a/ydr/ydrimport.py +++ b/ydr/ydrimport.py @@ -110,11 +110,9 @@ def create_rigged_model_obj(model_data: ModelData, materials: list[bpy.types.Mat def create_lod_meshes(model_data: ModelData, model_obj: bpy.types.Object, materials: list[bpy.types.Material], bones: Optional[list[bpy.types.Bone]] = None): - lod_levels: LODLevels = model_obj.sollumz_lods + lods: LODLevels = model_obj.sz_lods original_mesh = model_obj.data - lod_levels.add_empty_lods() - for lod_level, mesh_data in model_data.mesh_data_lods.items(): mesh_name = f"{model_obj.name}_{SOLLUMZ_UI_NAMES[lod_level].lower().replace(' ', '_')}" @@ -133,18 +131,17 @@ def create_lod_meshes(model_data: ModelData, model_obj: bpy.types.Object, materi f"Error occured during creation of mesh '{mesh_name}'! Is the mesh data valid?\n{traceback.format_exc()}") continue - lod_levels.set_lod_mesh(lod_level, lod_mesh) - lod_levels.set_active_lod(lod_level) + lods.get_lod(lod_level).mesh = lod_mesh + lods.active_lod_level = lod_level - set_drawable_model_properties( - lod_mesh.drawable_model_properties, model_data.xml_lods[lod_level]) + set_drawable_model_properties(lod_mesh.drawable_model_properties, model_data.xml_lods[lod_level]) is_skinned = "BlendWeights" in mesh_data.vert_arr.dtype.names if is_skinned and bones is not None: mesh_builder.create_vertex_groups(model_obj, bones) - lod_levels.set_highest_lod_active() + lods.set_highest_lod_active() # Original mesh no longer used since the obj is managed by LODs, so delete it if model_obj.data != original_mesh: @@ -169,14 +166,13 @@ def set_lod_model_properties(model_objs: list[bpy.types.Object], drawable_xml: D for lod_level, models in get_model_xmls_by_lod(drawable_xml).items(): for i, model_xml in enumerate(models): obj = model_objs[i] - obj_lods: LODLevels = obj.sollumz_lods + obj_lods: LODLevels = obj.sz_lods lod = obj_lods.get_lod(lod_level) - - if lod.mesh is None: + lod_mesh = lod.mesh + if lod_mesh is None: continue - set_drawable_model_properties( - lod.mesh.drawable_model_properties, model_xml[lod.level]) + set_drawable_model_properties(lod_mesh.drawable_model_properties, model_xml[lod.level]) def set_drawable_model_properties(model_props: DrawableModelProperties, model_xml: DrawableModel): diff --git a/yft/yftexport.py b/yft/yftexport.py index ff4f4e0d..7582b025 100644 --- a/yft/yftexport.py +++ b/yft/yftexport.py @@ -223,19 +223,21 @@ def remove_non_hi_lods(drawable_obj: bpy.types.Object): if model_obj.sollum_type != SollumType.DRAWABLE_MODEL: continue - very_high_lod = model_obj.sollumz_lods.get_lod(LODLevel.VERYHIGH) + lods = model_obj.sz_lods + very_high_lod = lods.get_lod(LODLevel.VERYHIGH) - if very_high_lod is None or very_high_lod.mesh is None: + if very_high_lod.mesh is None: bpy.data.objects.remove(model_obj) continue - lod_props = model_obj.sollumz_lods + lods.get_lod(LODLevel.HIGH).mesh = very_high_lod.mesh + lods.active_lod_level = LODLevel.HIGH - lod_props.set_lod_mesh(LODLevel.HIGH, very_high_lod.mesh) - lod_props.set_active_lod(LODLevel.HIGH) - - for lod in lod_props.lods: - if lod.level != LODLevel.HIGH and lod.mesh is not None: + for lod_level in LODLevel: + if lod_level == LODLevel.HIGH: + continue + lod = lods.get_lod(lod_level) + if lod.mesh is not None: lod.mesh = None @@ -260,9 +262,9 @@ def has_hi_lods(frag_obj: bpy.types.Object): if child.sollum_type != SollumType.DRAWABLE_MODEL and not child.sollumz_is_physics_child_mesh: continue - for lod in child.sollumz_lods.lods: - if lod.level == LODLevel.VERYHIGH and lod.mesh is not None: - return True + very_high_lod = child.sz_lods.get_lod(LODLevel.VERYHIGH) + if very_high_lod.mesh is not None: + return True return False @@ -699,14 +701,17 @@ def create_phys_child_drawable(child_xml: PhysicsChild, materials: list[bpy.type scale = get_scale_to_apply_to_bound(obj) transforms_to_apply = Matrix.Diagonal(scale).to_4x4() - for lod in obj.sollumz_lods.lods: - if lod.mesh is None or lod.level == LODLevel.VERYHIGH: + lods = obj.sz_lods + for lod_level in LODLevel: + if lod_level == LODLevel.VERYHIGH: + continue + lod_mesh = lods.get_lod(lod_level).mesh + if lod_mesh is None: continue - model_xml = create_model_xml( - obj, lod.level, materials, transforms_to_apply=transforms_to_apply) + model_xml = create_model_xml(obj, lod_level, materials, transforms_to_apply=transforms_to_apply) model_xml.bone_index = 0 - append_model_xml(drawable_xml, model_xml, lod.level) + append_model_xml(drawable_xml, model_xml, lod_level) set_drawable_xml_extents(drawable_xml)