Scene Manager: LOD switching; Shot Viewer renamed Shot Manager

LODs are child collections of the asset collection (COL_{asset}_LOD{n}).
Overrides hide the other LOD collections (viewport and render), keeping
the rig, pose and animation; collection instances instance the LOD
collection. Each instance has its own LOD, set per instance or for the
selection in the Loaded panel, kept through save, reload and updates.
Loading shows SCENE_MANAGER_DEFAULT_LOD (0).

The Shot Viewer panel is now "Shot Manager" and starts closed. Code
names and SHOT_VIEWER_* config keys are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
NDemaerschalkandClaude Opus 5.5 committed 2026-10-27 16:42:24 +01:00
1 parent 9fe38ce58a
commit bac7a6f5da
10 files changed
+258 -45

No files matched your search

+87 -11
View File
@@ -158,17 +158,90 @@ def add_instance(scene, view_layer, library, parent=None):
raise LoadError(f"{library.name}: collection {library.get('wtd_collection')} missing from the file")
parent = parent or _type_collection(scene, library.get("wtd_asset_type", ""))
if link_mode(library.get("wtd_asset_type", "")) == "OVERRIDE":
override = collection.override_hierarchy_create(scene, view_layer)
if scene.collection.children.get(override.name) == override and parent != scene.collection:
scene.collection.children.unlink(override)
if parent.children.get(override.name) != override:
parent.children.link(override)
return override
empty = bpy.data.objects.new(library.get("wtd_asset_name", collection.name), None)
empty.instance_type = "COLLECTION"
empty.instance_collection = collection
parent.objects.link(empty)
return empty
instance = collection.override_hierarchy_create(scene, view_layer)
if scene.collection.children.get(instance.name) == instance and parent != scene.collection:
scene.collection.children.unlink(instance)
if parent.children.get(instance.name) != instance:
parent.children.link(instance)
else:
instance = bpy.data.objects.new(library.get("wtd_asset_name", collection.name), None)
instance.instance_type = "COLLECTION"
instance.instance_collection = collection
parent.objects.link(instance)
apply_default_lod(library, instance)
return instance
# --------------------------------------------------------------------------- #
# Levels of detail
# --------------------------------------------------------------------------- #
def _lod_pattern(asset_name):
before, _, after = config.SCENE_MANAGER_LOD_COLLECTION.partition("{lod}")
return re.compile(re.escape(before.format(asset=asset_name)) + r"(\d+)" + re.escape(after.format(asset=asset_name)) + "$")
def lods(library, instance):
"""{LOD number: collection}: the LOD child collections of the asset collection, linked
for a collection instance, their overrides for an override."""
root = linked_collection(library) if isinstance(instance, bpy.types.Object) else instance
if root is None:
return {}
pattern = _lod_pattern(library.get("wtd_asset_name", library.name))
found = {}
for child in root.children:
# Overrides of a second instance are renamed (COL_bob_LOD0.001): match the linked name.
name = child.override_library.reference.name if child.override_library else child.name
match = pattern.match(name)
if match:
found[int(match.group(1))] = child
return dict(sorted(found.items()))
def current_lod(library, instance):
"""LOD shown by the instance, None when it shows none or several."""
found = lods(library, instance)
if isinstance(instance, bpy.types.Object):
return next((lod for lod, c in found.items() if c == instance.instance_collection), None)
shown = [lod for lod, c in found.items() if not c.hide_viewport]
return shown[0] if len(shown) == 1 else None
def set_lod(library, instance, lod):
"""Show one LOD: an override hides the other LOD collections (viewport and render, so
they are not evaluated), a collection instance instances the LOD collection."""
found = lods(library, instance)
if lod not in found:
available = ", ".join(str(n) for n in found) or "none"
raise LoadError(f"{instance.name}: no LOD {lod} (LODs in the file: {available})")
if isinstance(instance, bpy.types.Object):
root = linked_collection(library)
others = [c.name for c in root.children if c not in found.values()]
if root.objects or others:
raise LoadError(
f"{instance.name}: {library.name} has content outside its LOD collections "
f"({', '.join([ob.name for ob in root.objects] + others)}); load it as an override to switch LODs"
)
instance.instance_collection = found[lod]
return
for number, collection in found.items():
collection.hide_viewport = collection.hide_render = number != lod
def apply_default_lod(library, instance):
"""After a load or an update: show config SCENE_MANAGER_DEFAULT_LOD (else the lowest LOD)
when the instance shows no single LOD. An instance of a LOD the file no longer has goes
back to the whole asset collection first."""
found = lods(library, instance)
root = linked_collection(library)
if isinstance(instance, bpy.types.Object) and root and instance.instance_collection not in (root, *found.values()):
instance.instance_collection = root
default = config.SCENE_MANAGER_DEFAULT_LOD
if not found or default is None or current_lod(library, instance) is not None:
return
try:
set_lod(library, instance, default if default in found else min(found))
except LoadError:
pass # a collection instance with content outside its LODs keeps the whole collection
# --------------------------------------------------------------------------- #
@@ -190,6 +263,9 @@ def update(library_name, revision):
for key, value in tags.items():
library[key] = value
library["wtd_revision"] = revision["name"]
# The new revision may add, rename or drop LOD collections.
for instance in instances(library):
apply_default_lod(library, instance)
return library