diff --git a/addons/io_scene_swbf_msh/__init__.py b/addons/io_scene_swbf_msh/__init__.py index eea25b8..530b654 100644 --- a/addons/io_scene_swbf_msh/__init__.py +++ b/addons/io_scene_swbf_msh/__init__.py @@ -100,15 +100,15 @@ class ExportMSH(Operator, ExportHelper): default=True ) - export_animated: BoolProperty( - name="Export Animated Object", - description="Always check if the object will be animated.", + export_with_animation: BoolProperty( + name="Export With Animation", + description="Includes animation data extracted from the action currently set on armature.", default=False ) - export_skeleton_only: BoolProperty( - name="Export Skeleton", - description="Check if you intend to export skeleton data only.", + export_as_skeleton: BoolProperty( + name="Export Objects As Skeleton", + description="Check if you intend to export skeleton data for consumption by ZenAsset.", default=False ) @@ -122,9 +122,9 @@ class ExportMSH(Operator, ExportHelper): generate_triangle_strips=self.generate_triangle_strips, apply_modifiers=self.apply_modifiers, export_target=self.export_target, - skel_only=self.export_skeleton_only + skel_only=self.export_as_skeleton, + export_anim=self.export_with_animation ), - is_animated=self.export_animated ) return {'FINISHED'} diff --git a/addons/io_scene_swbf_msh/msh_anim_gather.py b/addons/io_scene_swbf_msh/msh_anim_gather.py index 1da0003..0129587 100644 --- a/addons/io_scene_swbf_msh/msh_anim_gather.py +++ b/addons/io_scene_swbf_msh/msh_anim_gather.py @@ -12,7 +12,7 @@ from .msh_utilities import * from .msh_model_gather import * -def extract_anim(armature: bpy.types.Armature) -> Animation: +def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation: action = armature.animation_data.action anim = Animation(); @@ -27,19 +27,23 @@ def extract_anim(armature: bpy.types.Armature) -> Animation: anim.end_index = num_frames - 1 - anim.bone_transforms["DummyRoot"] = [] + anim.bone_frames[root_name] = ([], []) for bone in armature.data.bones: - anim.bone_transforms[bone.name] = [] + anim.bone_frames[bone.name] = ([], []) for frame in range(num_frames): - - #if frame % 10 == 0: - # print("Sample frame {}:".format(frame)) - + frame_time = framerange.x + frame * increment bpy.context.scene.frame_set(frame_time) - anim.bone_transforms["DummyRoot"].append(ModelTransform()) + + rframe_dummy = RotationFrame(frame, convert_rotation_space(Quaternion())) + tframe_dummy = TranslationFrame(frame, Vector((0.0,0.0,0.0))) + + anim.bone_frames[root_name][0].append(tframe_dummy) + anim.bone_frames[root_name][1].append(rframe_dummy) + + for bone in armature.pose.bones: transform = bone.matrix @@ -49,13 +53,11 @@ def extract_anim(armature: bpy.types.Armature) -> Animation: loc, rot, _ = transform.decompose() - xform = ModelTransform() - xform.rotation = convert_rotation_space(rot) - xform.translation = convert_vector_space(loc) + rframe = RotationFrame(frame, convert_rotation_space(rot)) + tframe = TranslationFrame(frame, convert_vector_space(loc)) - #if frame % 10 == 0: - # print("\t{:10}: loc {:15} rot {:15}".format(bone.name, vec_to_str(xform.translation), quat_to_str(xform.rotation))) + anim.bone_frames[bone.name][0].append(tframe) + anim.bone_frames[bone.name][1].append(rframe) - anim.bone_transforms[bone.name].append(xform) return anim diff --git a/addons/io_scene_swbf_msh/msh_model.py b/addons/io_scene_swbf_msh/msh_model.py index 8976991..a1df175 100644 --- a/addons/io_scene_swbf_msh/msh_model.py +++ b/addons/io_scene_swbf_msh/msh_model.py @@ -2,7 +2,7 @@ saved to a .msh file. """ from dataclasses import dataclass, field -from typing import List, Tuple +from typing import List, Tuple, Dict from enum import Enum from mathutils import Vector, Quaternion @@ -52,7 +52,6 @@ class GeometrySegment: triangles: List[List[int]] = field(default_factory=list) triangle_strips: List[List[int]] = None - weights: List[List[Tuple[int, float]]] = None @dataclass class CollisionPrimitive: @@ -80,14 +79,27 @@ class Model: collisionprimitive: CollisionPrimitive = None +@dataclass +class RotationFrame: + + index : int = 0 + rotation : Quaternion = field(default_factory=Quaternion) + + +@dataclass +class TranslationFrame: + + index : int = 0 + translation : Vector = field(default_factory=Vector) + + @dataclass class Animation: """ Class representing 'CYCL' + 'KFR3' sections in a .msh file """ name: str = "fullanimation" - bone_transforms: Dict[str, List[ModelTransform]] = field(default_factory=dict) + bone_frames: Dict[str, Tuple[List[TranslationFrame], List[RotationFrame]]] = field(default_factory=dict) framerate: float = 29.97 start_index : int = 0 end_index : int = 0 - \ No newline at end of file diff --git a/addons/io_scene_swbf_msh/msh_model_gather.py b/addons/io_scene_swbf_msh/msh_model_gather.py index 776222d..2c5fe99 100644 --- a/addons/io_scene_swbf_msh/msh_model_gather.py +++ b/addons/io_scene_swbf_msh/msh_model_gather.py @@ -14,7 +14,7 @@ SKIPPED_OBJECT_TYPES = {"LATTICE", "CAMERA", "LIGHT", "SPEAKER", "LIGHT_PROBE"} MESH_OBJECT_TYPES = {"MESH", "CURVE", "SURFACE", "META", "FONT", "GPENCIL"} MAX_MSH_VERTEX_COUNT = 32767 -def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool) -> List[Model]: +def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool) -> Tuple[List[Model], bpy.types.Object]: """ Gathers the Blender objects from the current scene and returns them as a list of Model objects. """ @@ -23,6 +23,8 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool models_list: List[Model] = [] + armature_found = None + for uneval_obj in select_objects(export_target): if uneval_obj.type in SKIPPED_OBJECT_TYPES and uneval_obj.name not in parents: continue @@ -36,6 +38,7 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool if obj.type == "ARMATURE": models_list += expand_armature(obj) + armature_found = obj continue model = Model() @@ -44,7 +47,6 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool model.hidden = get_is_model_hidden(obj) transform = obj.matrix_local - transform_reset = Matrix.Identity(4) if obj.parent_bone: model.parent = obj.parent_bone @@ -52,18 +54,8 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool # matrix_local, when called on an armature child also parented to a bone, appears to be broken. # At the very least, the results contradict the docs... armature_relative_transform = obj.parent.matrix_world.inverted() @ obj.matrix_world - bone_relative_transform = obj.parent.data.bones[obj.parent_bone].matrix_local.inverted() @ armature_relative_transform + transform = obj.parent.data.bones[obj.parent_bone].matrix_local.inverted() @ armature_relative_transform - transform = bone_relative_transform - - ''' - # Since the transforms of direct bone children are discarded by ZEngine (but not ZEditor), we apply the transform - # before geometry extraction, then apply the inversion after. - if obj.type in MESH_OBJECT_TYPES: - obj.data.transform(bone_relative_transform) - transform_reset = bone_relative_transform.inverted() - transform = Matrix.Identity(4) - ''' else: if obj.parent is not None: if obj.parent.type == "ARMATURE": @@ -95,8 +87,6 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool if obj.vertex_groups: model.bone_map = [group.name for group in obj.vertex_groups] - obj.data.transform(transform_reset) - if get_is_collision_primitive(obj): model.collisionprimitive = get_collision_primitive(obj) @@ -104,7 +94,9 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool models_list.append(model) - return models_list + + return (models_list, armature_found) + def create_parents_set() -> Set[str]: diff --git a/addons/io_scene_swbf_msh/msh_reader.py b/addons/io_scene_swbf_msh/msh_reader.py index f81a077..c9941bf 100644 --- a/addons/io_scene_swbf_msh/msh_reader.py +++ b/addons/io_scene_swbf_msh/msh_reader.py @@ -3,12 +3,13 @@ import io import struct class Reader: - def __init__(self, file, parent=None, indent=0): + def __init__(self, file, parent=None, indent=0, debug=False): self.file = file self.size: int = 0 self.size_pos = None self.parent = parent self.indent = " " * indent #for print debugging + self.debug = debug def __enter__(self): @@ -19,7 +20,8 @@ class Reader: padding_length = 4 - (self.size % 4) if self.size % 4 > 0 else 0 self.end_pos = self.size_pos + padding_length + self.size + 8 - #print(self.indent + "Begin " + self.header + ", Size: " + str(self.size) + ", Pos: " + str(self.size_pos)) + if self.debug: + print(self.indent + "Begin " + self.header + ", Size: " + str(self.size) + ", Pos: " + str(self.size_pos)) return self @@ -28,7 +30,9 @@ class Reader: if self.size > self.MAX_SIZE: raise OverflowError(f".msh file overflowed max size. size = {self.size} MAX_SIZE = {self.MAX_SIZE}") - #print(self.indent + "End " + self.header) + if self.debug: + print(self.indent + "End " + self.header) + self.file.seek(self.end_pos) @@ -84,7 +88,7 @@ class Reader: def read_child(self): - child = Reader(self.file, parent=self, indent=int(len(self.indent) / 2) + 1) + child = Reader(self.file, parent=self, indent=int(len(self.indent) / 2) + 1, debug=self.debug) return child @@ -95,7 +99,13 @@ class Reader: def peak_next_header(self): buf = self.read_bytes(4); self.file.seek(-4,1) - return buf.decode("utf-8") + try: + result = buf.decode("utf-8") + except: + result = "" + + return result + def could_have_child(self): diff --git a/addons/io_scene_swbf_msh/msh_scene.py b/addons/io_scene_swbf_msh/msh_scene.py index c6337f9..ab29a67 100644 --- a/addons/io_scene_swbf_msh/msh_scene.py +++ b/addons/io_scene_swbf_msh/msh_scene.py @@ -6,7 +6,7 @@ from typing import List, Dict from copy import copy import bpy from mathutils import Vector -from .msh_model import Model +from .msh_model import Model, Animation 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 from .msh_model_triangle_strips import create_models_triangle_strips @@ -44,9 +44,11 @@ class Scene: materials: Dict[str, Material] = field(default_factory=dict) models: List[Model] = field(default_factory=list) + animation: Animation = None + skeleton: List[int] = field(default_factory=list) -def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_target: str, skel_only: bool) -> Scene: +def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_target: str, skel_only: bool, export_anim: bool) -> Scene: """ Create a msh Scene from the active Blender scene. """ scene = Scene() @@ -55,7 +57,7 @@ def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_t scene.materials = gather_materials() - scene.models = gather_models(apply_modifiers=apply_modifiers, export_target=export_target, skeleton_only=skel_only) + scene.models, armature_obj = gather_models(apply_modifiers=apply_modifiers, export_target=export_target, skeleton_only=skel_only) scene.models = sort_by_parent(scene.models) if generate_triangle_strips: @@ -71,12 +73,17 @@ def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_t scene.materials = remove_unused_materials(scene.materials, scene.models) - #creates a dummy basepose if no Action is selected - if "Armature" in bpy.context.scene.objects.keys(): - scene.anims = [extract_anim(bpy.context.scene.objects["Armature"])] root = scene.models[0] + + if export_anim: + if armature_obj is not None: + scene.animation = extract_anim(armature_obj, root.name) + else: + raise Exception("Export Error: Could not find an armature object from which to export an animation!") + if skel_only and root.model_type == ModelType.NULL: + # For ZenAsset inject_dummy_data(root) return scene diff --git a/addons/io_scene_swbf_msh/msh_scene_read.py b/addons/io_scene_swbf_msh/msh_scene_read.py index 2ebf4d1..6bdcfc7 100644 --- a/addons/io_scene_swbf_msh/msh_scene_read.py +++ b/addons/io_scene_swbf_msh/msh_scene_read.py @@ -12,6 +12,8 @@ from .crc import * model_counter = 0 +mndx_remap = {} + def read_scene(input_file) -> Scene: @@ -19,10 +21,13 @@ def read_scene(input_file) -> Scene: scene.models = [] scene.materials = {} + global mndx_remap + mndx_remap = {} + global model_counter model_counter = 0 - with Reader(file=input_file) as hedr: + with Reader(file=input_file, debug=True) as hedr: while hedr.could_have_child(): @@ -54,8 +59,7 @@ def read_scene(input_file) -> Scene: scene.models.append(_read_modl(modl, materials_list)) else: - with hedr.read_child() as unknown: - pass + msh2.skip_bytes(1) elif "SKL2" in next_header: with hedr.read_child() as skl2: @@ -67,15 +71,27 @@ def read_scene(input_file) -> Scene: _read_anm2(anm2, scene.models) else: - with hedr.read_child() as null: - pass + hedr.skip_bytes(1) + if scene.skeleton: print("Skeleton models: ") for model in scene.models: - if crc(model.name) in scene.skeleton: - print("\t" + model.name) + for i in range(len(scene.skeleton)): + if crc(model.name) == scene.skeleton[i]: + print("\t" + model.name) + if model.model_type == ModelType.SKIN: + scene.skeleton.pop(i) + break + + for model in scene.models: + if model.geometry: + for seg in model.geometry: + if seg.weights: + for weight_set in seg.weights: + for vweight in weight_set: + vweight.bone = mndx_remap[vweight.bone] return scene @@ -135,7 +151,7 @@ def _read_matd(matd: Reader) -> Material: mat.texture3 = tx3d.read_string() else: - matd.skip_bytes(4) + matd.skip_bytes(1) return mat @@ -154,9 +170,14 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: elif "MNDX" in next_header: with modl.read_child() as mndx: + index = mndx.read_u32() + global model_counter - if mndx.read_u32() - 1 != model_counter: - print("MODEL INDEX DIDNT MATCH COUNTER!") + print(mndx.indent + "MNDX doesn't match counter, expected: {} found: {}".format(model_counter, index)) + + global mndx_remap + mndx_remap[index] = model_counter + model_counter += 1 elif "NAME" in next_header: @@ -191,18 +212,20 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: elif "ENVL" in next_header_geom: with geom.read_child() as envl: num_indicies = envl.read_u32() - envelope += [envl.read_u32() - 1 for _ in range(num_indicies)] + envelope += [envl.read_u32() for _ in range(num_indicies)] else: - with geom.read_child() as null: - pass + geom.skip_bytes(1) + #with geom.read_child() as null: + #pass for seg in model.geometry: - if seg.weights: + if seg.weights and envelope: for weight_set in seg.weights: for i in range(len(weight_set)): - weight = weight_set[i] - weight_set[i] = (envelope[weight[0]], weight[1]) + vertex_weight = weight_set[i] + index = vertex_weight.bone + weight_set[i] = VertexWeight(vertex_weight.weight, envelope[vertex_weight.bone]) elif "SWCI" in next_header: prim = CollisionPrimitive() @@ -214,10 +237,9 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: model.collisionprimitive = prim else: - with modl.read_child() as null: - pass + modl.skip_bytes(1) - print("Reading model " + model.name + " of type: " + str(model.model_type)[10:]) + print(modl.indent + "Read model " + model.name + " of type: " + str(model.model_type)[10:]) return model @@ -293,11 +315,45 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: geometry_seg.triangles.append(ndxt.read_u16(3)) elif "STRP" in next_header: - with segm.read_child() as strp: - pass + strips : List[List[int]] = [] - if segm.read_u16 != 0: #trailing 0 bug https://schlechtwetterfront.github.io/ze_filetypes/msh.html#STRP - segm.skip_bytes(-2) + with segm.read_child() as strp: + num_indicies = strp.read_u32() + + num_indicies_read = 0 + + curr_strip = [] + previous_flag = False + + if num_indicies > 0: + index, index1 = strp.read_u16(2) + curr_strip = [index & 0x7fff, index1 & 0x7fff] + num_indicies_read += 2 + + for i in range(num_indicies - 2): + index = strp.read_u16(1) + + if index & 0x8000 > 0: + index = index & 0x7fff + + if previous_flag: + previous_flag = False + curr_strip.append(index) + strips.append(curr_strip[:-2]) + curr_strip = curr_strip[-2:] + continue + else: + previous_flag = True + + else: + previous_flag = False + + curr_strip.append(index) + + geometry_seg.triangle_strips = strips + + #if segm.read_u16 != 0: #trailing 0 bug https://schlechtwetterfront.github.io/ze_filetypes/msh.html#STRP + # segm.skip_bytes(-2) elif "WGHT" in next_header: with segm.read_child() as wght: @@ -312,13 +368,12 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: value = wght.read_f32() if value > 0.000001: - weight_set.append((index,value)) + weight_set.append(VertexWeight(value,index)) geometry_seg.weights.append(weight_set) else: - with segm.read_child() as null: - pass + segm.skip_bytes(1) return geometry_seg diff --git a/addons/io_scene_swbf_msh/msh_scene_save.py b/addons/io_scene_swbf_msh/msh_scene_save.py index 67c1f10..9e65fbf 100644 --- a/addons/io_scene_swbf_msh/msh_scene_save.py +++ b/addons/io_scene_swbf_msh/msh_scene_save.py @@ -8,13 +8,10 @@ from .msh_material import * from .msh_writer import Writer from .msh_utilities import * -<<<<<<< HEAD from .crc import * -def save_scene(output_file, scene: Scene, is_animated: bool): -======= + def save_scene(output_file, scene: Scene): ->>>>>>> mshread """ Saves scene to the supplied file. """ with Writer(file=output_file, chunk_id="HEDR") as hedr: @@ -36,16 +33,15 @@ def save_scene(output_file, scene: Scene): with msh2.create_child("MODL") as modl: _write_modl(modl, model, index, material_index, model_index) - if is_animated: + if scene.animation is not None: with hedr.create_child("SKL2") as skl2: - _write_skl2(skl2, scene) + _write_skl2(skl2, scene.animation) with hedr.create_child("BLN2") as bln2: - _write_bln2(bln2, scene) + _write_bln2(bln2, scene.animation) - with hedr.create_child("ANM2") as anm2: #simple for now - for anim in scene.anims: - _write_anm2(anm2, anim) + with hedr.create_child("ANM2") as anm2: #simple for now + _write_anm2(anm2, scene.animation) with hedr.create_child("CL1L"): pass @@ -251,23 +247,23 @@ def _write_envl(envl: Writer, model: Model, model_index: Dict[str, int]): ''' SKELETON CHUNKS ''' -def _write_bln2(bln2: Writer, scene: Scene): - bones = scene.anims[0].bone_transforms.keys() +def _write_bln2(bln2: Writer, anim: Animation): + bones = anim.bone_frames.keys() bln2.write_u32(len(bones)) for boneName in bones: bln2.write_u32(crc(boneName), 0) -def _write_skl2(skl2: Writer, scene: Scene): - bones = scene.anims[0].bone_transforms.keys() - skl2.write_u32(len(bones)) +def _write_skl2(skl2: Writer, anim: Animation): + bones = anim.bone_frames.keys() + skl2.write_u32(len(bones)) for boneName in bones: - skl2.write_u32(crc(boneName), 0) #default values - skl2.write_f32(1.0, 0.0, 0.0) #from docs + skl2.write_u32(crc(boneName), 0) #default values from docs + skl2.write_f32(1.0, 0.0, 0.0) ''' -ANIMATION CHUNK +ANIMATION CHUNKS ''' def _write_anm2(anm2: Writer, anim: Animation): @@ -286,19 +282,21 @@ def _write_anm2(anm2: Writer, anim: Animation): with anm2.create_child("KFR3") as kfr3: - kfr3.write_u32(len(anim.bone_transforms.keys())) + kfr3.write_u32(len(anim.bone_frames)) - for boneName in anim.bone_transforms.keys(): + for boneName in anim.bone_frames: kfr3.write_u32(crc(boneName)) kfr3.write_u32(0) #what is keyframe type? - num_frames = 1 + anim.end_index - anim.start_index - kfr3.write_u32(num_frames, num_frames) #basic testing + translation_frames, rotation_frames = anim.bone_frames[boneName] - for i, xform in enumerate(anim.bone_transforms[boneName]): - kfr3.write_u32(i) - kfr3.write_f32(xform.translation.x, xform.translation.y, xform.translation.z) + kfr3.write_u32(len(translation_frames), len(rotation_frames)) - for i, xform in enumerate(anim.bone_transforms[boneName]): - kfr3.write_u32(i) - kfr3.write_f32(xform.rotation.x, xform.rotation.y, xform.rotation.z, xform.rotation.w) + for frame in translation_frames: + kfr3.write_u32(frame.index) + kfr3.write_f32(frame.translation.x, frame.translation.y, frame.translation.z) + + for frame in rotation_frames: + kfr3.write_u32(frame.index) + kfr3.write_f32(frame.rotation.x, frame.rotation.y, frame.rotation.z, frame.rotation.w) + \ No newline at end of file diff --git a/addons/io_scene_swbf_msh/msh_to_blend.py b/addons/io_scene_swbf_msh/msh_to_blend.py index dc1ed09..0492eec 100644 --- a/addons/io_scene_swbf_msh/msh_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_to_blend.py @@ -79,7 +79,7 @@ def extract_refined_skeleton(scene: Scene): if seg.weights: for weight_set in seg.weights: for weight in weight_set: - model_weighted_to = scene.models[weight[0]] + model_weighted_to = scene.models[weight.bone] if crc(model_weighted_to.name) not in scene.skeleton: scene.skeleton.append(crc(model_weighted_to.name)) @@ -164,7 +164,14 @@ def extract_models(scene: Scene, materials_map): else: full_texcoords += [(0.0,0.0) for _ in range(len(seg.positions))] - faces += [tuple([ind + offset for ind in tri]) for tri in seg.triangles] + if seg.triangles: + faces += [tuple([ind + offset for ind in tri]) for tri in seg.triangles] + else: + for strip in seg.triangle_strips: + for i in range(len(strip) - 2): + face = tuple([offset + strip[j] for j in range(i,i+3)]) + print("strip face: " + str(face)) + faces.append(face) offset += len(seg.positions) @@ -199,13 +206,13 @@ def extract_models(scene: Scene, materials_map): for offset in weights_offsets: for i, weight_set in enumerate(weights_offsets[offset]): for weight in weight_set: - index = weight[0] + index = weight.bone if index not in vertex_groups_indicies: model_name = scene.models[index].name vertex_groups_indicies[index] = new_obj.vertex_groups.new(name=model_name) - vertex_groups_indicies[index].add([offset + i], weight[1], 'ADD') + vertex_groups_indicies[index].add([offset + i], weight.weight, 'ADD') ''' Assign Materials - will do per segment later... @@ -277,8 +284,8 @@ def extract_scene(filepath: str, scene: Scene): skel = extract_refined_skeleton(scene) armature = refined_skeleton_to_armature(skel, model_map) + reparent_obj = None for model in scene.models: - reparent_obj = None if model.model_type == ModelType.SKIN: if model.parent: @@ -314,8 +321,9 @@ def extract_scene(filepath: str, scene: Scene): for model in scene.models: if model.name in bpy.data.objects: - if model.hidden and len(bpy.data.objects[model.name].children) == 0: - bpy.data.objects[model.name].hide_set(True) + obj = bpy.data.objects[model.name] + if get_is_model_hidden(obj) and len(obj.children) == 0: + obj.hide_set(True)