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:
1 parent
9fe38ce58a
commit
bac7a6f5da
10 files changed
+258
-45
No files matched your search
+87
-11
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Scene Manager operators: connection, shot context, casting, load, update, remove, assetize.
|
||||
"""Scene Manager operators: connection, shot context, casting, load, update, LOD, remove, assetize.
|
||||
|
||||
Libreflow is only reached through ``ipc``; every request but the ones filling a
|
||||
dialog runs in the background and applies its result on the main thread.
|
||||
@@ -13,7 +13,7 @@ import time
|
||||
|
||||
import bpy
|
||||
from bpy.app.handlers import persistent
|
||||
from bpy.props import BoolProperty, EnumProperty, PointerProperty, StringProperty
|
||||
from bpy.props import BoolProperty, EnumProperty, IntProperty, PointerProperty, StringProperty
|
||||
|
||||
from .. import config
|
||||
from . import ipc
|
||||
@@ -731,6 +731,39 @@ class WTD_OT_scene_select_instance(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class WTD_OT_scene_set_lod(bpy.types.Operator):
|
||||
bl_idname = "wtd_scene.set_lod"
|
||||
bl_label = "Set LOD"
|
||||
bl_description = "Show this level of detail (one of the asset's LOD collections)"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
lod: IntProperty(name="LOD", min=0)
|
||||
library: StringProperty(options={"SKIP_SAVE"})
|
||||
instance: StringProperty(description="Only this instance (object or collection name)", options={"SKIP_SAVE"})
|
||||
selected: BoolProperty(description="The instances of the selected objects", options={"SKIP_SAVE"})
|
||||
|
||||
def execute(self, context):
|
||||
if self.selected:
|
||||
targets = lib.selected_instances(context.view_layer)
|
||||
else:
|
||||
library = bpy.data.libraries.get(self.library)
|
||||
targets = [(library, i) for i in lib.instances(library) if i.name == self.instance] if library else []
|
||||
targets = [(library, instance) for library, instance in targets if lib.lods(library, instance)]
|
||||
if not targets:
|
||||
self.report({"WARNING"}, "No loaded instance with LODs selected")
|
||||
return {"CANCELLED"}
|
||||
switched, errors = [], []
|
||||
for library, instance in targets:
|
||||
try:
|
||||
lib.set_lod(library, instance, self.lod)
|
||||
switched.append(instance.name)
|
||||
except lib.LoadError as exc:
|
||||
errors.append(str(exc))
|
||||
text = f"LOD {self.lod}: {', '.join(switched)}" if switched else f"LOD {self.lod} not set"
|
||||
self.report({"WARNING"} if errors else {"INFO"}, "; ".join([text, *errors]))
|
||||
return {"FINISHED"} if switched else {"CANCELLED"}
|
||||
|
||||
|
||||
class WTD_OT_scene_toggle_expand(bpy.types.Operator):
|
||||
bl_idname = "wtd_scene.toggle_expand"
|
||||
bl_label = "Show Instances"
|
||||
@@ -948,6 +981,7 @@ classes = (
|
||||
WTD_OT_scene_pick_revision,
|
||||
WTD_OT_scene_remove,
|
||||
WTD_OT_scene_select_instance,
|
||||
WTD_OT_scene_set_lod,
|
||||
WTD_OT_scene_toggle_expand,
|
||||
WTD_OT_scene_assetize,
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user