Refactor: Node Wrangler: PreviewNode operator
[blender-addons.git] / add_camera_rigs / build_rigs.py
blob08016ae2092e715671f04f739ea4430fe35b90a3
1 # SPDX-FileCopyrightText: 2019-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 import bpy
6 from bpy.types import Operator
7 from bpy_extras import object_utils
8 from mathutils import Vector
9 from math import pi
11 from .create_widgets import (create_root_widget,
12 create_camera_widget, create_camera_offset_widget,
13 create_aim_widget, create_circle_widget,
14 create_corner_widget)
17 def create_prop_driver(rig, cam, prop_from, prop_to):
18 """Create driver to a property on the rig"""
19 driver = cam.data.driver_add(prop_to)
20 driver.driver.type = 'SCRIPTED'
21 var = driver.driver.variables.new()
22 var.name = 'var'
23 var.type = 'SINGLE_PROP'
25 # Target the custom bone property
26 var.targets[0].id = rig
27 var.targets[0].data_path = 'pose.bones["Camera"]["%s"]' % prop_from
28 driver.driver.expression = 'var'
31 def create_dolly_bones(rig):
32 """Create bones for the dolly camera rig"""
33 bones = rig.data.edit_bones
35 # Add new bones
36 root = bones.new("Root")
37 root.tail = (0.0, 1.0, 0.0)
38 root.show_wire = True
39 rig.data.collections.new(name="Controls")
40 rig.data.collections['Controls'].assign(root)
42 ctrl_aim_child = bones.new("MCH-Aim_shape_rotation")
43 ctrl_aim_child.head = (0.0, 10.0, 1.7)
44 ctrl_aim_child.tail = (0.0, 11.0, 1.7)
45 # Create bone collection and assign bone
46 rig.data.collections.new(name="MCH")
47 rig.data.collections['MCH'].assign(ctrl_aim_child)
48 rig.data.collections['MCH'].is_visible = False
50 ctrl_aim = bones.new("Aim")
51 ctrl_aim.head = (0.0, 10.0, 1.7)
52 ctrl_aim.tail = (0.0, 11.0, 1.7)
53 ctrl_aim.show_wire = True
54 rig.data.collections['Controls'].assign(ctrl_aim)
56 ctrl = bones.new("Camera")
57 ctrl.head = (0.0, 0.0, 1.7)
58 ctrl.tail = (0.0, 1.0, 1.7)
59 ctrl.show_wire = True
60 rig.data.collections['Controls'].assign(ctrl)
62 ctrl_offset = bones.new("Camera_Offset")
63 ctrl_offset.head = (0.0, 0.0, 1.7)
64 ctrl_offset.tail = (0.0, 1.0, 1.7)
65 ctrl_offset.show_wire = True
66 rig.data.collections['Controls'].assign(ctrl_offset)
69 # Setup hierarchy
70 ctrl.parent = root
71 ctrl_offset.parent = ctrl
72 ctrl_aim.parent = root
73 ctrl_aim_child.parent = ctrl_aim
75 # Jump into object mode
76 bpy.ops.object.mode_set(mode='OBJECT')
77 pose_bones = rig.pose.bones
78 # Lock the relevant scale channels of the Camera_offset bone
79 pose_bones["Camera_Offset"].lock_scale = (True,) * 3
82 def create_crane_bones(rig):
83 """Create bones for the crane camera rig"""
84 bones = rig.data.edit_bones
86 # Add new bones
87 root = bones.new("Root")
88 root.tail = (0.0, 1.0, 0.0)
89 root.show_wire = True
90 rig.data.collections.new(name="Controls")
91 rig.data.collections['Controls'].assign(root)
93 ctrl_aim_child = bones.new("MCH-Aim_shape_rotation")
94 ctrl_aim_child.head = (0.0, 10.0, 1.7)
95 ctrl_aim_child.tail = (0.0, 11.0, 1.7)
96 rig.data.collections.new(name="MCH")
97 rig.data.collections['MCH'].assign(ctrl_aim_child)
98 rig.data.collections['MCH'].is_visible = False
100 ctrl_aim = bones.new("Aim")
101 ctrl_aim.head = (0.0, 10.0, 1.7)
102 ctrl_aim.tail = (0.0, 11.0, 1.7)
103 ctrl_aim.show_wire = True
104 rig.data.collections['Controls'].assign(ctrl_aim)
106 ctrl = bones.new("Camera")
107 ctrl.head = (0.0, 1.0, 1.7)
108 ctrl.tail = (0.0, 2.0, 1.7)
109 rig.data.collections['Controls'].assign(ctrl)
111 ctrl_offset = bones.new("Camera_Offset")
112 ctrl_offset.head = (0.0, 1.0, 1.7)
113 ctrl_offset.tail = (0.0, 2.0, 1.7)
114 rig.data.collections['Controls'].assign(ctrl_offset)
116 arm = bones.new("Crane_Arm")
117 arm.head = (0.0, 0.0, 1.7)
118 arm.tail = (0.0, 1.0, 1.7)
119 rig.data.collections['Controls'].assign(arm)
121 height = bones.new("Crane_Height")
122 height.head = (0.0, 0.0, 0.0)
123 height.tail = (0.0, 0.0, 1.7)
124 rig.data.collections['Controls'].assign(height)
126 # Setup hierarchy
127 ctrl.parent = arm
128 ctrl_offset.parent = ctrl
129 ctrl.use_inherit_rotation = False
130 ctrl.inherit_scale = "NONE"
131 ctrl.show_wire = True
133 arm.parent = height
134 arm.inherit_scale = "NONE"
136 height.parent = root
137 ctrl_aim.parent = root
138 ctrl_aim_child.parent = ctrl_aim
140 # Jump into object mode
141 bpy.ops.object.mode_set(mode='OBJECT')
142 pose_bones = rig.pose.bones
144 # Lock the relevant loc, rot and scale
145 pose_bones["Crane_Arm"].lock_rotation = (False, True, False)
146 pose_bones["Crane_Arm"].lock_scale = (True, False, True)
147 pose_bones["Crane_Height"].lock_location = (True,) * 3
148 pose_bones["Crane_Height"].lock_rotation = (True,) * 3
149 pose_bones["Crane_Height"].lock_scale = (True, False, True)
150 pose_bones["Camera_Offset"].lock_scale = (True,) * 3
153 def setup_3d_rig(rig, cam):
154 """Finish setting up Dolly and Crane rigs"""
155 # Jump into object mode and change bones to euler
156 bpy.ops.object.mode_set(mode='OBJECT')
157 pose_bones = rig.pose.bones
158 for bone in pose_bones:
159 bone.rotation_mode = 'XYZ'
161 # Lens property
162 pb = pose_bones['Camera']
163 pb["lens"] = 50.0
164 ui_data = pb.id_properties_ui("lens")
165 ui_data.update(min=1.0, max=1000000.0, soft_max = 5000.0, default=50.0)
167 # Build the widgets
168 root_widget = create_root_widget("Camera_Root")
169 camera_widget = create_camera_widget("Camera")
170 camera_offset_widget = create_camera_offset_widget("Camera_Offset")
171 aim_widget = create_aim_widget("Aim")
173 # Add the custom bone shapes
174 pose_bones["Root"].custom_shape = root_widget
175 pose_bones["Aim"].custom_shape = aim_widget
176 pose_bones["Camera"].custom_shape = camera_widget
177 pose_bones["Camera_Offset"].custom_shape = camera_offset_widget
179 # Set the "Override Transform" field to the mechanism position
180 pose_bones["Aim"].custom_shape_transform = pose_bones["MCH-Aim_shape_rotation"]
182 # Add constraints to bones
183 con = pose_bones['MCH-Aim_shape_rotation'].constraints.new('COPY_ROTATION')
184 con.target = rig
185 con.subtarget = "Camera"
187 con = pose_bones['Camera'].constraints.new('TRACK_TO')
188 con.track_axis = 'TRACK_Y'
189 con.up_axis = 'UP_Z'
190 con.target = rig
191 con.subtarget = "Aim"
192 con.use_target_z = True
194 cam.data.display_size = 1.0
195 cam.rotation_euler[0] = pi / 2.0 # Rotate the camera 90 degrees in x
197 create_prop_driver(rig, cam, "lens", "lens")
200 def create_2d_bones(context, rig, cam):
201 """Create bones for the 2D camera rig"""
202 scene = context.scene
203 bones = rig.data.edit_bones
205 # Add new bones
206 bones = rig.data.edit_bones
207 root = bones.new("Root")
208 root.tail = Vector((0.0, 0.0, 1.0))
209 root.show_wire = True
210 rig.data.collections.new(name="Controls")
211 rig.data.collections['Controls'].assign(root)
213 ctrl = bones.new('Camera')
214 ctrl.tail = Vector((0.0, 0.0, 1.0))
215 ctrl.show_wire = True
216 rig.data.collections['Controls'].assign(ctrl)
218 left_corner = bones.new("Left_Corner")
219 left_corner.head = (-3, 10, -2)
220 left_corner.tail = left_corner.head + Vector((0.0, 0.0, 1.0))
221 left_corner.show_wire = True
222 rig.data.collections['Controls'].assign(left_corner)
224 right_corner = bones.new("Right_Corner")
225 right_corner.head = (3, 10, -2)
226 right_corner.tail = right_corner.head + Vector((0.0, 0.0, 1.0))
227 right_corner.show_wire = True
228 rig.data.collections['Controls'].assign(right_corner)
230 corner_distance_x = (left_corner.head - right_corner.head).length
231 corner_distance_y = -left_corner.head.z
232 corner_distance_z = left_corner.head.y
233 rig.data.collections['Controls'].assign(root)
235 center = bones.new("MCH-Center")
236 center.head = ((right_corner.head + left_corner.head) / 2.0)
237 center.tail = center.head + Vector((0.0, 0.0, 1.0))
238 center.show_wire = True
239 rig.data.collections.new(name="MCH")
240 rig.data.collections['MCH'].assign(center)
241 rig.data.collections['MCH'].is_visible = False
242 center.show_wire = True
244 # Setup hierarchy
245 ctrl.parent = root
246 left_corner.parent = root
247 right_corner.parent = root
248 center.parent = root
250 # Jump into object mode and change bones to euler
251 bpy.ops.object.mode_set(mode='OBJECT')
252 pose_bones = rig.pose.bones
253 for bone in pose_bones:
254 bone.rotation_mode = 'XYZ'
256 # Bone drivers
257 center_drivers = pose_bones["MCH-Center"].driver_add("location")
259 # Center X driver
260 driver = center_drivers[0].driver
261 driver.type = 'AVERAGE'
263 for corner in ('left', 'right'):
264 var = driver.variables.new()
265 var.name = corner
266 var.type = 'TRANSFORMS'
267 var.targets[0].id = rig
268 var.targets[0].bone_target = corner.capitalize() + '_Corner'
269 var.targets[0].transform_type = 'LOC_X'
270 var.targets[0].transform_space = 'TRANSFORM_SPACE'
272 # Center Y driver
273 driver = center_drivers[1].driver
274 driver.type = 'SCRIPTED'
276 driver.expression = '({distance_x} - (left_x-right_x))*(res_y/res_x)/2 + (left_y + right_y)/2'.format(
277 distance_x=corner_distance_x)
279 for direction in ('x', 'y'):
280 for corner in ('left', 'right'):
281 var = driver.variables.new()
282 var.name = '%s_%s' % (corner, direction)
283 var.type = 'TRANSFORMS'
284 var.targets[0].id = rig
285 var.targets[0].bone_target = corner.capitalize() + '_Corner'
286 var.targets[0].transform_type = 'LOC_' + direction.upper()
287 var.targets[0].transform_space = 'TRANSFORM_SPACE'
289 var = driver.variables.new()
290 var.name = 'res_' + direction
291 var.type = 'SINGLE_PROP'
292 var.targets[0].id_type = 'SCENE'
293 var.targets[0].id = scene
294 var.targets[0].data_path = 'render.resolution_' + direction
296 # Center Z driver
297 driver = center_drivers[2].driver
298 driver.type = 'AVERAGE'
300 for corner in ('left', 'right'):
301 var = driver.variables.new()
302 var.name = corner
303 var.type = 'TRANSFORMS'
304 var.targets[0].id = rig
305 var.targets[0].bone_target = corner.capitalize() + '_Corner'
306 var.targets[0].transform_type = 'LOC_Z'
307 var.targets[0].transform_space = 'TRANSFORM_SPACE'
309 # Bone constraints
310 con = pose_bones["Camera"].constraints.new('DAMPED_TRACK')
311 con.target = rig
312 con.subtarget = "MCH-Center"
313 con.track_axis = 'TRACK_NEGATIVE_Z'
315 # Build the widgets
316 left_widget = create_corner_widget("Left_Corner", reverse=True)
317 right_widget = create_corner_widget("Right_Corner")
318 parent_widget = create_circle_widget("Root", radius=0.5)
319 camera_widget = create_circle_widget("Camera_2D", radius=0.3)
321 # Add the custom bone shapes
322 pose_bones["Left_Corner"].custom_shape = left_widget
323 pose_bones["Right_Corner"].custom_shape = right_widget
324 pose_bones["Root"].custom_shape = parent_widget
325 pose_bones["Camera"].custom_shape = camera_widget
327 # Lock the relevant loc, rot and scale
328 pose_bones["Left_Corner"].lock_rotation = (True,) * 3
329 pose_bones["Right_Corner"].lock_rotation = (True,) * 3
330 pose_bones["Camera"].lock_rotation = (True,) * 3
331 pose_bones["Camera"].lock_scale = (True,) * 3
333 # Camera settings
335 cam.data.sensor_fit = "HORIZONTAL" # Avoids distortion in portrait format
337 # Property to switch between rotation and switch mode
338 pose_bones["Camera"]['rotation_shift'] = 0.0
339 ui_data = pose_bones["Camera"].id_properties_ui('rotation_shift')
340 ui_data.update(min=0.0, max=1.0, description="rotation_shift")
342 # Rotation / shift switch driver
343 driver = con.driver_add('influence').driver
344 driver.expression = '1 - rotation_shift'
346 var = driver.variables.new()
347 var.name = 'rotation_shift'
348 var.type = 'SINGLE_PROP'
349 var.targets[0].id = rig
350 var.targets[0].data_path = 'pose.bones["Camera"]["rotation_shift"]'
352 # Focal length driver
353 driver = cam.data.driver_add('lens').driver
354 driver.expression = 'abs({distance_z} - (left_z + right_z)/2 + cam_z) * 36 / frame_width'.format(
355 distance_z=corner_distance_z)
357 var = driver.variables.new()
358 var.name = 'frame_width'
359 var.type = 'LOC_DIFF'
360 var.targets[0].id = rig
361 var.targets[0].bone_target = "Left_Corner"
362 var.targets[0].transform_space = 'WORLD_SPACE'
363 var.targets[1].id = rig
364 var.targets[1].bone_target = "Right_Corner"
365 var.targets[1].transform_space = 'WORLD_SPACE'
367 for corner in ('left', 'right'):
368 var = driver.variables.new()
369 var.name = corner + '_z'
370 var.type = 'TRANSFORMS'
371 var.targets[0].id = rig
372 var.targets[0].bone_target = corner.capitalize() + '_Corner'
373 var.targets[0].transform_type = 'LOC_Z'
374 var.targets[0].transform_space = 'TRANSFORM_SPACE'
376 var = driver.variables.new()
377 var.name = 'cam_z'
378 var.type = 'TRANSFORMS'
379 var.targets[0].id = rig
380 var.targets[0].bone_target = "Camera"
381 var.targets[0].transform_type = 'LOC_Z'
382 var.targets[0].transform_space = 'TRANSFORM_SPACE'
384 # Orthographic scale driver
385 driver = cam.data.driver_add('ortho_scale').driver
386 driver.expression = 'abs({distance_x} - (left_x - right_x))'.format(distance_x=corner_distance_x)
388 for corner in ('left', 'right'):
389 var = driver.variables.new()
390 var.name = corner + '_x'
391 var.type = 'TRANSFORMS'
392 var.targets[0].id = rig
393 var.targets[0].bone_target = corner.capitalize() + '_Corner'
394 var.targets[0].transform_type = 'LOC_X'
395 var.targets[0].transform_space = 'TRANSFORM_SPACE'
397 # Shift driver X
398 driver = cam.data.driver_add('shift_x').driver
400 driver.expression = 'rotation_shift * (((left_x + right_x)/2 - cam_x) * lens / abs({distance_z} - (left_z + right_z)/2 + cam_z) / 36)'.format(
401 distance_z=corner_distance_z)
403 var = driver.variables.new()
404 var.name = 'rotation_shift'
405 var.type = 'SINGLE_PROP'
406 var.targets[0].id = rig
407 var.targets[0].data_path = 'pose.bones["Camera"]["rotation_shift"]'
409 for direction in ('x', 'z'):
410 for corner in ('left', 'right'):
411 var = driver.variables.new()
412 var.name = '%s_%s' % (corner, direction)
413 var.type = 'TRANSFORMS'
414 var.targets[0].id = rig
415 var.targets[0].bone_target = corner.capitalize() + '_Corner'
416 var.targets[0].transform_type = 'LOC_' + direction.upper()
417 var.targets[0].transform_space = 'TRANSFORM_SPACE'
419 var = driver.variables.new()
420 var.name = 'cam_' + direction
421 var.type = 'TRANSFORMS'
422 var.targets[0].id = rig
423 var.targets[0].bone_target = "Camera"
424 var.targets[0].transform_type = 'LOC_' + direction.upper()
425 var.targets[0].transform_space = 'TRANSFORM_SPACE'
427 var = driver.variables.new()
428 var.name = 'lens'
429 var.type = 'SINGLE_PROP'
430 var.targets[0].id_type = 'CAMERA'
431 var.targets[0].id = cam.data
432 var.targets[0].data_path = 'lens'
434 # Shift driver Y
435 driver = cam.data.driver_add('shift_y').driver
437 driver.expression = 'rotation_shift * -(({distance_y} - (left_y + right_y)/2 + cam_y) * lens / abs({distance_z} - (left_z + right_z)/2 + cam_z) / 36 - (res_y/res_x)/2)'.format(
438 distance_y=corner_distance_y, distance_z=corner_distance_z)
440 var = driver.variables.new()
441 var.name = 'rotation_shift'
442 var.type = 'SINGLE_PROP'
443 var.targets[0].id = rig
444 var.targets[0].data_path = 'pose.bones["Camera"]["rotation_shift"]'
446 for direction in ('y', 'z'):
447 for corner in ('left', 'right'):
448 var = driver.variables.new()
449 var.name = '%s_%s' % (corner, direction)
450 var.type = 'TRANSFORMS'
451 var.targets[0].id = rig
452 var.targets[0].bone_target = corner.capitalize() + '_Corner'
453 var.targets[0].transform_type = 'LOC_' + direction.upper()
454 var.targets[0].transform_space = 'TRANSFORM_SPACE'
456 var = driver.variables.new()
457 var.name = 'cam_' + direction
458 var.type = 'TRANSFORMS'
459 var.targets[0].id = rig
460 var.targets[0].bone_target = "Camera"
461 var.targets[0].transform_type = 'LOC_' + direction.upper()
462 var.targets[0].transform_space = 'TRANSFORM_SPACE'
464 for direction in ('x', 'y'):
465 var = driver.variables.new()
466 var.name = 'res_' + direction
467 var.type = 'SINGLE_PROP'
468 var.targets[0].id_type = 'SCENE'
469 var.targets[0].id = scene
470 var.targets[0].data_path = 'render.resolution_' + direction
472 var = driver.variables.new()
473 var.name = 'lens'
474 var.type = 'SINGLE_PROP'
475 var.targets[0].id_type = 'CAMERA'
476 var.targets[0].id = cam.data
477 var.targets[0].data_path = 'lens'
480 def build_camera_rig(context, mode):
481 """Create stuff common to all camera rigs."""
482 # Add the camera object
483 cam_name = "%s_Camera" % mode.capitalize()
484 cam_data = bpy.data.cameras.new(cam_name)
485 cam = object_utils.object_data_add(context, cam_data, name=cam_name)
486 context.scene.camera = cam
488 # Add the rig object
489 rig_name = mode.capitalize() + "_Rig"
490 rig_data = bpy.data.armatures.new(rig_name)
491 rig = object_utils.object_data_add(context, rig_data, name=rig_name)
492 rig["rig_id"] = "%s" % rig_name
493 rig.location = context.scene.cursor.location
495 bpy.ops.object.mode_set(mode='EDIT')
497 # Add new bones and setup specific rigs
498 if mode == "DOLLY":
499 create_dolly_bones(rig)
500 setup_3d_rig(rig, cam)
501 elif mode == "CRANE":
502 create_crane_bones(rig)
503 setup_3d_rig(rig, cam)
504 elif mode == "2D":
505 create_2d_bones(context, rig, cam)
507 # Parent the camera to the rig
508 cam.location = (0.0, -1.0, 0.0) # Move the camera to the correct position
509 cam.parent = rig
510 cam.parent_type = "BONE"
511 if mode == "2D":
512 cam.parent_bone = "Camera"
513 else:
514 cam.parent_bone = "Camera_Offset"
516 # Change display to BBone: it just looks nicer
517 rig.data.display_type = 'BBONE'
518 # Change display to wire for object
519 rig.display_type = 'WIRE'
521 # Lock camera transforms
522 cam.lock_location = (True,) * 3
523 cam.lock_rotation = (True,) * 3
524 cam.lock_scale = (True,) * 3
526 # Add custom properties to the armature’s Camera bone,
527 # so that all properties may be animated in a single action
529 pose_bones = rig.pose.bones
531 # DOF Focus Distance property
532 pb = pose_bones['Camera']
533 pb["focus_distance"] = 10.0
534 ui_data = pb.id_properties_ui('focus_distance')
535 ui_data.update(min=0.0, default=10.0)
537 # DOF F-Stop property
538 pb = pose_bones['Camera']
539 pb["aperture_fstop"] = 2.8
540 ui_data = pb.id_properties_ui('aperture_fstop')
541 ui_data.update(min=0.0, soft_min=0.1, soft_max=128.0, default=2.8)
543 # Add drivers to link the camera properties to the custom props
544 # on the armature
545 create_prop_driver(rig, cam, "focus_distance", "dof.focus_distance")
546 create_prop_driver(rig, cam, "aperture_fstop", "dof.aperture_fstop")
548 # Make the rig the active object
549 view_layer = context.view_layer
550 for obj in view_layer.objects:
551 obj.select_set(False)
552 rig.select_set(True)
553 view_layer.objects.active = rig
556 class OBJECT_OT_build_camera_rig(Operator):
557 bl_idname = "object.build_camera_rig"
558 bl_label = "Build Camera Rig"
559 bl_description = "Build a Camera Rig"
560 bl_options = {'REGISTER', 'UNDO'}
562 mode: bpy.props.EnumProperty(items=(('DOLLY', 'Dolly', 'Dolly rig'),
563 ('CRANE', 'Crane', 'Crane rig',),
564 ('2D', '2D', '2D rig')),
565 name="mode",
566 description="Type of camera to create",
567 default="DOLLY")
569 def execute(self, context):
570 # Build the rig
571 build_camera_rig(context, self.mode)
572 return {'FINISHED'}
575 def add_dolly_crane_buttons(self, context):
576 """Dolly and crane entries in the Add Object > Camera Menu"""
577 if context.mode == 'OBJECT':
578 self.layout.operator(
579 OBJECT_OT_build_camera_rig.bl_idname,
580 text="Dolly Camera Rig",
581 icon='VIEW_CAMERA'
582 ).mode = "DOLLY"
584 self.layout.operator(
585 OBJECT_OT_build_camera_rig.bl_idname,
586 text="Crane Camera Rig",
587 icon='VIEW_CAMERA'
588 ).mode = "CRANE"
590 self.layout.operator(
591 OBJECT_OT_build_camera_rig.bl_idname,
592 text="2D Camera Rig",
593 icon='PIVOT_BOUNDBOX'
594 ).mode = "2D"
597 classes = (
598 OBJECT_OT_build_camera_rig,
602 def register():
603 from bpy.utils import register_class
604 for cls in classes:
605 register_class(cls)
607 bpy.types.VIEW3D_MT_camera_add.append(add_dolly_crane_buttons)
610 def unregister():
611 from bpy.utils import unregister_class
612 for cls in classes:
613 unregister_class(cls)
615 bpy.types.VIEW3D_MT_camera_add.remove(add_dolly_crane_buttons)
618 if __name__ == "__main__":
619 register()