Shot Viewer: build shot cameras with Add Camera Rigs

New shot cameras come from bpy.ops.object.build_camera_rig: a crane rig
when "3D Camera" is ticked, a 2D rig otherwise (SHOT_CAMERA_RIG_MODES).
The camera and its rig are named for the sequence and shot
(SHOT_CAMERA_RIG_NAME_FORMAT). Camera rigs are left out of the rig
quick-select. Requires the Add Camera Rigs extension.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nero Frame 13andClaude Opus 5.5 committed 2026-10-21 13:56:52 +02:00
1 parent 00788bb3bf
commit 14408b2dbf
3 files changed
+57 -25

No files matched your search

+8 -5
View File
@@ -29,6 +29,8 @@ Most values the tools rely on (prefixes, shot naming, frame rate…) live in `co
Tested on **Blender 5.2**. Tested on **Blender 5.2**.
The Shot Viewer builds its cameras with the **Add Camera Rigs** extension (*Edit > Preferences > Get Extensions*, search "Add Camera Rigs"). Install and enable it next to Melvile Tools.
**As an extension (artists):** build the package, then in Blender go to *Edit > Preferences > Get Extensions > Install from Disk…* and pick the `.zip`. **As an extension (artists):** build the package, then in Blender go to *Edit > Preferences > Get Extensions > Install from Disk…* and pick the `.zip`.
```bash ```bash
@@ -279,11 +281,12 @@ Click **+** next to the shot list:
|---|---| |---|---|
| **Name** | Pre-filled with the next free shot number (`sh0010`, `sh0020`…). This is the marker name. | | **Name** | Pre-filled with the next free shot number (`sh0010`, `sh0020`…). This is the marker name. |
| **Duration** | Length of the shot in frames. | | **Duration** | Length of the shot in frames. |
| **New Camera** | On: creates the camera `CAM_<sequence>_<shot>` (its name is shown in the dialog), copying the lens and position of the current camera, so you start from the current framing. Off: pick an existing camera of the scene. | | **New Camera** | On: builds a camera rig with the **Add Camera Rigs** add-on. The camera is named `CAM_<sequence>_<shot>` (shown in the dialog) and its rig `RIG_<sequence>_<shot>`. Off: pick an existing camera of the scene. |
| **3D Camera** | On: a 3D **crane** rig. Off: a **2D** camera rig. |
The shot is added at the end of the sequence and the scene's **End** frame grows to include it. The first shot starts at frame 1001, and the scene **Start** moves there too. The playhead and the 3D view then jump to the new shot. The shot is added at the end of the sequence and the scene's **End** frame grows to include it. The first shot starts at frame 1001, and the scene **Start** moves there too. The playhead and the 3D view then jump to the new shot.
New cameras go in the scene's top collection: move them into your camera collection if you have one. The rig is built at the **3D cursor**, in the active collection: place the cursor and pick the collection first if you have a camera collection. The add-on keeps the rig's bone shapes in a hidden **Widgets** collection. If the add-on is not enabled, the dialog says so and no shot is added.
### Deleting a shot ### Deleting a shot
@@ -304,7 +307,7 @@ The bottom of the panel lists the armatures of the scene, grouped into **Charact
- **Shift+click**: adds it to the selection, to pose several rigs at once. - **Shift+click**: adds it to the selection, to pose several rigs at once.
- The rig(s) currently selected are highlighted. - The rig(s) currently selected are highlighted.
A rig counts as a character or a prop when its name, one of its collections, or the library file it comes from starts with `chr_` / `prp_` (see `SHOT_VIEWER_RIG_GROUPS` in [Configuration](#configuration)). Hidden rigs cannot be selected. A rig counts as a character or a prop when its name, one of its collections, or the library file it comes from starts with `chr_` / `prp_` (see `SHOT_VIEWER_RIG_GROUPS` in [Configuration](#configuration)). Hidden rigs cannot be selected. Camera rigs (armatures carrying a camera) are not listed.
--- ---
@@ -329,7 +332,7 @@ To change them **without editing the code**, write a JSON file with the keys to
Unknown keys are ignored and reported in the system console. Unknown keys are ignored and reported in the system console.
Name formats use Python's `str.format`: `"sq{:04d}"` turns `10` into `sq0010`. `SHOT_CAMERA_NAME_FORMAT` takes the named tokens `{prefix}` (the camera prefix of `OBJECT_PREFIXES`, `CAM_`), `{sequence}` and `{shot}`. The default `"{prefix}{sequence}_{shot}"` gives `CAM_sq0010_sh0030`. Name formats use Python's `str.format`: `"sq{:04d}"` turns `10` into `sq0010`. `SHOT_CAMERA_NAME_FORMAT` takes the named tokens `{prefix}` (the camera prefix of `OBJECT_PREFIXES`, `CAM_`), `{sequence}` and `{shot}`. The default `"{prefix}{sequence}_{shot}"` gives `CAM_sq0010_sh0030`. `SHOT_CAMERA_RIG_NAME_FORMAT` names the rig the same way, with `{prefix}` = `RIG_`. `SHOT_CAMERA_RIG_MODES` picks the Add Camera Rigs mode for 3D and 2D cameras (`CRANE` and `2D`; `DOLLY` is also available).
### Keys by tool ### Keys by tool
@@ -343,7 +346,7 @@ Name formats use Python's `str.format`: `"sq{:04d}"` turns `10` into `sq0010`. `
| Sanity Check: layout | `SET_COLLECTION_PATTERN` | | Sanity Check: layout | `SET_COLLECTION_PATTERN` |
| Sanity Check: panel | `CATEGORY_ORDER`, `MAX_ISSUES_DRAWN` | | Sanity Check: panel | `CATEGORY_ORDER`, `MAX_ISSUES_DRAWN` |
| Rename | `RENAME_SEPARATOR`, `RENAME_PADDING`, `RENAME_SIDES`, `RENAME_TYPE_SUFFIXES` (plus `OBJECT_PREFIXES` / `DATABLOCK_PREFIXES` for *By Type* prefixes) | | Rename | `RENAME_SEPARATOR`, `RENAME_PADDING`, `RENAME_SIDES`, `RENAME_TYPE_SUFFIXES` (plus `OBJECT_PREFIXES` / `DATABLOCK_PREFIXES` for *By Type* prefixes) |
| Shot Viewer | `SHOT_VIEWER_RIG_GROUPS`, `SHOT_NAME_FORMAT`, `SHOT_NUMBER_STEP`, `SHOT_FRAME_START`, `SHOT_DEFAULT_DURATION`, `SEQUENCE_NAME_FORMAT`, `SHOT_CAMERA_NAME_FORMAT` | | Shot Viewer | `SHOT_VIEWER_RIG_GROUPS`, `SHOT_NAME_FORMAT`, `SHOT_NUMBER_STEP`, `SHOT_FRAME_START`, `SHOT_DEFAULT_DURATION`, `SEQUENCE_NAME_FORMAT`, `SHOT_CAMERA_NAME_FORMAT`, `SHOT_CAMERA_RIG_NAME_FORMAT`, `SHOT_CAMERA_RIG_MODES` |
--- ---
+4
View File
@@ -251,6 +251,10 @@ SEQUENCE_NAME_FORMAT = "sq{:04d}"
# Cameras added by the shot viewer. Tokens: {prefix} (OBJECT_PREFIXES["CAMERA"]), # Cameras added by the shot viewer. Tokens: {prefix} (OBJECT_PREFIXES["CAMERA"]),
# {sequence} (SEQUENCE_NAME_FORMAT) and {shot} (the shot name). # {sequence} (SEQUENCE_NAME_FORMAT) and {shot} (the shot name).
SHOT_CAMERA_NAME_FORMAT = "{prefix}{sequence}_{shot}" SHOT_CAMERA_NAME_FORMAT = "{prefix}{sequence}_{shot}"
# Their rig, same tokens with {prefix} = OBJECT_PREFIXES["ARMATURE"].
SHOT_CAMERA_RIG_NAME_FORMAT = "{prefix}{sequence}_{shot}"
# Mode of bpy.ops.object.build_camera_rig (Add Camera Rigs add-on) for 3D and 2D cameras.
SHOT_CAMERA_RIG_MODES = {"3D": "CRANE", "2D": "2D"}
SHOT_NUMBER_STEP = 10 SHOT_NUMBER_STEP = 10
SHOT_FRAME_START = 1001 SHOT_FRAME_START = 1001
SHOT_DEFAULT_DURATION = 48 SHOT_DEFAULT_DURATION = 48
+45 -20
View File
@@ -12,7 +12,6 @@ import re
import bpy import bpy
from bpy.app.handlers import persistent from bpy.app.handlers import persistent
from bpy.props import BoolProperty, IntProperty, PointerProperty, StringProperty from bpy.props import BoolProperty, IntProperty, PointerProperty, StringProperty
from mathutils import Euler, Vector
from . import config from . import config
@@ -121,22 +120,30 @@ def camera_name(scene, shot):
) )
def _new_camera(scene, shot, source=None): def camera_rig_available():
"""New camera in the scene collection, copying ``source`` (lens and placement) when given.""" """True when the Add Camera Rigs add-on (bpy.ops.object.build_camera_rig) is enabled."""
name = camera_name(scene, shot) return hasattr(bpy.types, "OBJECT_OT_build_camera_rig")
if source is not None and source.type == "CAMERA":
data = source.data.copy()
data.name = name def _new_camera_rig(context, shot, is_3d):
else: """Camera rig built by the Add Camera Rigs add-on at the 3D cursor, named for the shot.
source = None Returns the camera object."""
data = bpy.data.cameras.new(name) scene = context.scene
camera = bpy.data.objects.new(name, data) if context.mode != "OBJECT":
if source is not None: bpy.ops.object.mode_set(mode="OBJECT")
camera.matrix_world = source.matrix_world.copy() before = {ob.as_pointer() for ob in bpy.data.objects}
else: bpy.ops.object.build_camera_rig(mode=config.SHOT_CAMERA_RIG_MODES["3D" if is_3d else "2D"])
camera.location = Vector((0.0, -10.0, 1.6)) # Found by difference: the add-on may also add widget objects for the bone shapes.
camera.rotation_euler = Euler((1.5708, 0.0, 0.0)) new = [ob for ob in bpy.data.objects if ob.as_pointer() not in before]
scene.collection.objects.link(camera) camera = next((ob for ob in new if ob.type == "CAMERA"), None)
if camera is None:
raise RuntimeError("build_camera_rig created no camera")
camera.name = camera.data.name = camera_name(scene, shot)
rig = camera.parent
if rig is not None and rig in new:
rig.name = rig.data.name = config.SHOT_CAMERA_RIG_NAME_FORMAT.format(
prefix=config.OBJECT_PREFIXES.get("ARMATURE", ""), sequence=sequence_name(scene), shot=shot
)
return camera return camera
@@ -158,7 +165,15 @@ def rig_groups(view_layer):
patterns = [(label, re.compile(pattern)) for label, pattern in config.SHOT_VIEWER_RIG_GROUPS.items()] patterns = [(label, re.compile(pattern)) for label, pattern in config.SHOT_VIEWER_RIG_GROUPS.items()]
groups = {label: [] for label, _pattern in patterns} groups = {label: [] for label, _pattern in patterns}
groups[OTHER_GROUP] = [] groups[OTHER_GROUP] = []
rigs = sorted((ob for ob in view_layer.objects if ob.type == "ARMATURE"), key=lambda ob: ob.name.lower()) # Camera rigs (armatures carrying a camera) are left out: one per shot.
rigs = sorted(
(
ob
for ob in view_layer.objects
if ob.type == "ARMATURE" and not any(child.type == "CAMERA" for child in ob.children)
),
key=lambda ob: ob.name.lower(),
)
for ob in rigs: for ob in rigs:
names = _rig_names(ob) names = _rig_names(ob)
label = next( label = next(
@@ -225,9 +240,13 @@ class WTD_OT_shot_viewer_new_shot(bpy.types.Operator):
duration: IntProperty(name="Duration", description="Length of the shot in frames", min=1, default=48) duration: IntProperty(name="Duration", description="Length of the shot in frames", min=1, default=48)
new_camera: BoolProperty( new_camera: BoolProperty(
name="New Camera", name="New Camera",
description="Add a camera for this shot, copying the current one. Otherwise pick an existing camera", description="Build a camera rig for this shot (Add Camera Rigs add-on). Otherwise pick an existing camera",
default=True, default=True,
) )
camera_3d: BoolProperty(
name="3D Camera",
description="Build a 3D crane rig. Otherwise a 2D camera rig",
)
camera_name: StringProperty(name="Camera") camera_name: StringProperty(name="Camera")
def invoke(self, context, event): def invoke(self, context, event):
@@ -243,8 +262,11 @@ class WTD_OT_shot_viewer_new_shot(bpy.types.Operator):
layout.prop(self, "duration") layout.prop(self, "duration")
layout.prop(self, "new_camera") layout.prop(self, "new_camera")
if self.new_camera: if self.new_camera:
layout.prop(self, "camera_3d")
name = self.name.strip() or next_shot_name(context.scene) name = self.name.strip() or next_shot_name(context.scene)
layout.label(text=camera_name(context.scene, name), icon="CAMERA_DATA") layout.label(text=camera_name(context.scene, name), icon="CAMERA_DATA")
if not camera_rig_available():
layout.label(text="Enable the Add Camera Rigs add-on", icon="ERROR")
else: else:
layout.prop_search(self, "camera_name", context.scene, "objects", icon="CAMERA_DATA") layout.prop_search(self, "camera_name", context.scene, "objects", icon="CAMERA_DATA")
@@ -252,7 +274,10 @@ class WTD_OT_shot_viewer_new_shot(bpy.types.Operator):
scene = context.scene scene = context.scene
name = self.name.strip() or next_shot_name(scene) name = self.name.strip() or next_shot_name(scene)
if self.new_camera: if self.new_camera:
camera = _new_camera(scene, name, scene.camera) if not camera_rig_available():
self.report({"WARNING"}, "Enable the Add Camera Rigs add-on to build the shot camera")
return {"CANCELLED"}
camera = _new_camera_rig(context, name, self.camera_3d)
else: else:
camera = scene.objects.get(self.camera_name) camera = scene.objects.get(self.camera_name)
if camera is None or camera.type != "CAMERA": if camera is None or camera.type != "CAMERA":