Cleanup: strip trailing space, remove BOM
[blender-addons.git] / add_camera_rigs / build_rigs.py
blob6b47fef8882dded142674748dcf4af4e8fc12088
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 import bpy
20 from bpy.types import Operator
21 from bpy_extras import object_utils
22 from mathutils import Vector
23 from rna_prop_ui import rna_idprop_ui_prop_get
24 from math import pi
26 from .create_widgets import (create_root_widget,
27 create_camera_widget, create_camera_offset_widget,
28 create_aim_widget, create_circle_widget,
29 create_corner_widget)
32 def create_prop_driver(rig, cam, prop_from, prop_to):
33 """Create driver to a property on the rig"""
34 driver = cam.data.driver_add(prop_to)
35 driver.driver.type = 'SCRIPTED'
36 var = driver.driver.variables.new()
37 var.name = 'var'
38 var.type = 'SINGLE_PROP'
40 # Target the custom bone property
41 var.targets[0].id = rig
42 var.targets[0].data_path = 'pose.bones["Camera"]["%s"]' % prop_from
43 driver.driver.expression = 'var'
46 def create_dolly_bones(rig):
47 """Create bones for the dolly camera rig"""
48 bones = rig.data.edit_bones
50 # Add new bones
51 root = bones.new("Root")
52 root.tail = (0.0, 1.0, 0.0)
53 root.show_wire = True
55 ctrl_aim_child = bones.new("Aim_shape_rotation-MCH")
56 ctrl_aim_child.head = (0.0, 10.0, 1.7)
57 ctrl_aim_child.tail = (0.0, 11.0, 1.7)
58 ctrl_aim_child.layers = tuple(i == 1 for i in range(32))
60 ctrl_aim = bones.new("Aim")
61 ctrl_aim.head = (0.0, 10.0, 1.7)
62 ctrl_aim.tail = (0.0, 11.0, 1.7)
63 ctrl_aim.show_wire = True
65 ctrl = bones.new("Camera")
66 ctrl.head = (0.0, 0.0, 1.7)
67 ctrl.tail = (0.0, 1.0, 1.7)
68 ctrl.show_wire = True
70 ctrl_offset = bones.new("Camera_offset")
71 ctrl_offset.head = (0.0, 0.0, 1.7)
72 ctrl_offset.tail = (0.0, 1.0, 1.7)
73 ctrl_offset.show_wire = True
75 # Setup hierarchy
76 ctrl.parent = root
77 ctrl_offset.parent = ctrl
78 ctrl_aim.parent = root
79 ctrl_aim_child.parent = ctrl_aim
81 # Jump into object mode
82 bpy.ops.object.mode_set(mode='OBJECT')
83 pose_bones = rig.pose.bones
84 # Lock the relevant scale channels of the Camera_offset bone
85 pose_bones["Camera_offset"].lock_scale = (True,) * 3
88 def create_crane_bones(rig):
89 """Create bones for the crane camera rig"""
90 bones = rig.data.edit_bones
92 # Add new bones
93 root = bones.new("Root")
94 root.tail = (0.0, 1.0, 0.0)
95 root.show_wire = True
97 ctrl_aim_child = bones.new("Aim_shape_rotation-MCH")
98 ctrl_aim_child.head = (0.0, 10.0, 1.7)
99 ctrl_aim_child.tail = (0.0, 11.0, 1.7)
100 ctrl_aim_child.layers = tuple(i == 1 for i in range(32))
102 ctrl_aim = bones.new("Aim")
103 ctrl_aim.head = (0.0, 10.0, 1.7)
104 ctrl_aim.tail = (0.0, 11.0, 1.7)
105 ctrl_aim.show_wire = True
107 ctrl = bones.new("Camera")
108 ctrl.head = (0.0, 1.0, 1.7)
109 ctrl.tail = (0.0, 2.0, 1.7)
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)
115 arm = bones.new("Crane_arm")
116 arm.head = (0.0, 0.0, 1.7)
117 arm.tail = (0.0, 1.0, 1.7)
119 height = bones.new("Crane_height")
120 height.head = (0.0, 0.0, 0.0)
121 height.tail = (0.0, 0.0, 1.7)
123 # Setup hierarchy
124 ctrl.parent = arm
125 ctrl_offset.parent = ctrl
126 ctrl.use_inherit_rotation = False
127 ctrl.use_inherit_scale = False
128 ctrl.show_wire = True
130 arm.parent = height
131 arm.use_inherit_scale = False
133 height.parent = root
134 ctrl_aim.parent = root
135 ctrl_aim_child.parent = ctrl_aim
137 # Jump into object mode
138 bpy.ops.object.mode_set(mode='OBJECT')
139 pose_bones = rig.pose.bones
141 # Lock the relevant loc, rot and scale
142 pose_bones["Crane_arm"].lock_rotation = (False, True, False)
143 pose_bones["Crane_arm"].lock_scale = (True, False, True)
144 pose_bones["Crane_height"].lock_location = (True,) * 3
145 pose_bones["Crane_height"].lock_rotation = (True,) * 3
146 pose_bones["Crane_height"].lock_scale = (True, False, True)
147 pose_bones["Camera_offset"].lock_scale = (True,) * 3
150 def setup_3d_rig(rig, cam):
151 """Finish setting up Dolly and Crane rigs"""
152 # Jump into object mode and change bones to euler
153 bpy.ops.object.mode_set(mode='OBJECT')
154 pose_bones = rig.pose.bones
155 for bone in pose_bones:
156 bone.rotation_mode = 'XYZ'
158 # Lens property
159 pb = pose_bones['Camera']
160 pb["lens"] = 50.0
161 prop = rna_idprop_ui_prop_get(pb, "lens", create=True)
162 prop["default"] = 50.0
163 prop["min"] = 1.0
164 prop["max"] = 1000000.0
165 prop["soft_max"] = 5000.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["Aim_shape_rotation-MCH"]
182 # Add constraints to bones
183 con = pose_bones['Aim_shape_rotation-MCH'].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
211 ctrl = bones.new('Camera')
212 ctrl.tail = Vector((0.0, 0.0, 1.0))
213 ctrl.show_wire = True
215 left_corner = bones.new("Left_corner")
216 left_corner.head = (-3, 10, -2)
217 left_corner.tail = left_corner.head + Vector((0.0, 0.0, 1.0))
218 left_corner.show_wire = True
220 right_corner = bones.new("Right_corner")
221 right_corner.head = (3, 10, -2)
222 right_corner.tail = right_corner.head + Vector((0.0, 0.0, 1.0))
223 right_corner.show_wire = True
225 corner_distance_x = (left_corner.head - right_corner.head).length
226 corner_distance_y = -left_corner.head.z
227 corner_distance_z = left_corner.head.y
229 center = bones.new("Center-MCH")
230 center.head = ((right_corner.head + left_corner.head) / 2.0)
231 center.tail = center.head + Vector((0.0, 0.0, 1.0))
232 center.layers = tuple(i == 1 for i in range(32))
233 center.show_wire = True
235 # Setup hierarchy
236 ctrl.parent = root
237 left_corner.parent = root
238 right_corner.parent = root
239 center.parent = root
241 # Jump into object mode and change bones to euler
242 bpy.ops.object.mode_set(mode='OBJECT')
243 pose_bones = rig.pose.bones
244 for bone in pose_bones:
245 bone.rotation_mode = 'XYZ'
247 # Bone drivers
248 center_drivers = pose_bones["Center-MCH"].driver_add("location")
250 # Center X driver
251 driver = center_drivers[0].driver
252 driver.type = 'AVERAGE'
254 for corner in ('left', 'right'):
255 var = driver.variables.new()
256 var.name = corner
257 var.type = 'TRANSFORMS'
258 var.targets[0].id = rig
259 var.targets[0].bone_target = corner.capitalize() + '_corner'
260 var.targets[0].transform_type = 'LOC_X'
261 var.targets[0].transform_space = 'TRANSFORM_SPACE'
263 # Center Y driver
264 driver = center_drivers[1].driver
265 driver.type = 'SCRIPTED'
267 driver.expression = '({distance_x} - (left_x-right_x))*(res_y/res_x)/2 + (left_y + right_y)/2'.format(
268 distance_x=corner_distance_x)
270 for direction in ('x', 'y'):
271 for corner in ('left', 'right'):
272 var = driver.variables.new()
273 var.name = '%s_%s' % (corner, direction)
274 var.type = 'TRANSFORMS'
275 var.targets[0].id = rig
276 var.targets[0].bone_target = corner.capitalize() + '_corner'
277 var.targets[0].transform_type = 'LOC_' + direction.upper()
278 var.targets[0].transform_space = 'TRANSFORM_SPACE'
280 var = driver.variables.new()
281 var.name = 'res_' + direction
282 var.type = 'SINGLE_PROP'
283 var.targets[0].id_type = 'SCENE'
284 var.targets[0].id = scene
285 var.targets[0].data_path = 'render.resolution_' + direction
287 # Center Z driver
288 driver = center_drivers[2].driver
289 driver.type = 'AVERAGE'
291 for corner in ('left', 'right'):
292 var = driver.variables.new()
293 var.name = corner
294 var.type = 'TRANSFORMS'
295 var.targets[0].id = rig
296 var.targets[0].bone_target = corner.capitalize() + '_corner'
297 var.targets[0].transform_type = 'LOC_Z'
298 var.targets[0].transform_space = 'TRANSFORM_SPACE'
300 # Bone constraints
301 con = pose_bones["Camera"].constraints.new('DAMPED_TRACK')
302 con.target = rig
303 con.subtarget = "Center-MCH"
304 con.track_axis = 'TRACK_NEGATIVE_Z'
306 # Build the widgets
307 left_widget = create_corner_widget("Left_corner", reverse=True)
308 right_widget = create_corner_widget("Right_corner")
309 parent_widget = create_circle_widget("Root", radius=0.5)
310 camera_widget = create_circle_widget("Camera_2D", radius=0.3)
312 # Add the custom bone shapes
313 pose_bones["Left_corner"].custom_shape = left_widget
314 pose_bones["Right_corner"].custom_shape = right_widget
315 pose_bones["Root"].custom_shape = parent_widget
316 pose_bones["Camera"].custom_shape = camera_widget
318 # Lock the relevant loc, rot and scale
319 pose_bones["Left_corner"].lock_rotation = (True,) * 3
320 pose_bones["Right_corner"].lock_rotation = (True,) * 3
321 pose_bones["Camera"].lock_rotation = (True,) * 3
322 pose_bones["Camera"].lock_scale = (True,) * 3
324 # Camera settings
326 cam.data.sensor_fit = "HORIZONTAL" # Avoids distortion in portrait format
328 # Property to switch between rotation and switch mode
329 pose_bones["Camera"]['rotation_shift'] = 0.0
330 prop = rna_idprop_ui_prop_get(pose_bones["Camera"], 'rotation_shift', create=True)
331 prop["min"] = 0.0
332 prop["max"] = 1.0
333 prop["soft_min"] = 0.0
334 prop["soft_max"] = 1.0
335 prop["description"] = 'rotation_shift'
337 # Rotation / shift switch driver
338 driver = con.driver_add('influence').driver
339 driver.expression = '1 - rotation_shift'
341 var = driver.variables.new()
342 var.name = 'rotation_shift'
343 var.type = 'SINGLE_PROP'
344 var.targets[0].id = rig
345 var.targets[0].data_path = 'pose.bones["Camera"]["rotation_shift"]'
347 # Focal length driver
348 driver = cam.data.driver_add('lens').driver
349 driver.expression = 'abs({distance_z} - (left_z + right_z)/2 + cam_z) * 36 / frame_width'.format(
350 distance_z=corner_distance_z)
352 var = driver.variables.new()
353 var.name = 'frame_width'
354 var.type = 'LOC_DIFF'
355 var.targets[0].id = rig
356 var.targets[0].bone_target = "Left_corner"
357 var.targets[0].transform_space = 'WORLD_SPACE'
358 var.targets[1].id = rig
359 var.targets[1].bone_target = "Right_corner"
360 var.targets[1].transform_space = 'WORLD_SPACE'
362 for corner in ('left', 'right'):
363 var = driver.variables.new()
364 var.name = corner + '_z'
365 var.type = 'TRANSFORMS'
366 var.targets[0].id = rig
367 var.targets[0].bone_target = corner.capitalize() + '_corner'
368 var.targets[0].transform_type = 'LOC_Z'
369 var.targets[0].transform_space = 'TRANSFORM_SPACE'
371 var = driver.variables.new()
372 var.name = 'cam_z'
373 var.type = 'TRANSFORMS'
374 var.targets[0].id = rig
375 var.targets[0].bone_target = "Camera"
376 var.targets[0].transform_type = 'LOC_Z'
377 var.targets[0].transform_space = 'TRANSFORM_SPACE'
379 # Orthographic scale driver
380 driver = cam.data.driver_add('ortho_scale').driver
381 driver.expression = 'abs({distance_x} - (left_x - right_x))'.format(distance_x=corner_distance_x)
383 for corner in ('left', 'right'):
384 var = driver.variables.new()
385 var.name = corner + '_x'
386 var.type = 'TRANSFORMS'
387 var.targets[0].id = rig
388 var.targets[0].bone_target = corner.capitalize() + '_corner'
389 var.targets[0].transform_type = 'LOC_X'
390 var.targets[0].transform_space = 'TRANSFORM_SPACE'
392 # Shift driver X
393 driver = cam.data.driver_add('shift_x').driver
395 driver.expression = 'rotation_shift * (((left_x + right_x)/2 - cam_x) * lens / abs({distance_z} - (left_z + right_z)/2 + cam_z) / 36)'.format(
396 distance_z=corner_distance_z)
398 var = driver.variables.new()
399 var.name = 'rotation_shift'
400 var.type = 'SINGLE_PROP'
401 var.targets[0].id = rig
402 var.targets[0].data_path = 'pose.bones["Camera"]["rotation_shift"]'
404 for direction in ('x', 'z'):
405 for corner in ('left', 'right'):
406 var = driver.variables.new()
407 var.name = '%s_%s' % (corner, direction)
408 var.type = 'TRANSFORMS'
409 var.targets[0].id = rig
410 var.targets[0].bone_target = corner.capitalize() + '_corner'
411 var.targets[0].transform_type = 'LOC_' + direction.upper()
412 var.targets[0].transform_space = 'TRANSFORM_SPACE'
414 var = driver.variables.new()
415 var.name = 'cam_' + direction
416 var.type = 'TRANSFORMS'
417 var.targets[0].id = rig
418 var.targets[0].bone_target = "Camera"
419 var.targets[0].transform_type = 'LOC_' + direction.upper()
420 var.targets[0].transform_space = 'TRANSFORM_SPACE'
422 var = driver.variables.new()
423 var.name = 'lens'
424 var.type = 'SINGLE_PROP'
425 var.targets[0].id_type = 'CAMERA'
426 var.targets[0].id = cam.data
427 var.targets[0].data_path = 'lens'
429 # Shift driver Y
430 driver = cam.data.driver_add('shift_y').driver
432 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(
433 distance_y=corner_distance_y, distance_z=corner_distance_z)
435 var = driver.variables.new()
436 var.name = 'rotation_shift'
437 var.type = 'SINGLE_PROP'
438 var.targets[0].id = rig
439 var.targets[0].data_path = 'pose.bones["Camera"]["rotation_shift"]'
441 for direction in ('y', 'z'):
442 for corner in ('left', 'right'):
443 var = driver.variables.new()
444 var.name = '%s_%s' % (corner, direction)
445 var.type = 'TRANSFORMS'
446 var.targets[0].id = rig
447 var.targets[0].bone_target = corner.capitalize() + '_corner'
448 var.targets[0].transform_type = 'LOC_' + direction.upper()
449 var.targets[0].transform_space = 'TRANSFORM_SPACE'
451 var = driver.variables.new()
452 var.name = 'cam_' + direction
453 var.type = 'TRANSFORMS'
454 var.targets[0].id = rig
455 var.targets[0].bone_target = "Camera"
456 var.targets[0].transform_type = 'LOC_' + direction.upper()
457 var.targets[0].transform_space = 'TRANSFORM_SPACE'
459 for direction in ('x', 'y'):
460 var = driver.variables.new()
461 var.name = 'res_' + direction
462 var.type = 'SINGLE_PROP'
463 var.targets[0].id_type = 'SCENE'
464 var.targets[0].id = scene
465 var.targets[0].data_path = 'render.resolution_' + direction
467 var = driver.variables.new()
468 var.name = 'lens'
469 var.type = 'SINGLE_PROP'
470 var.targets[0].id_type = 'CAMERA'
471 var.targets[0].id = cam.data
472 var.targets[0].data_path = 'lens'
475 def build_camera_rig(context, mode):
476 """Create stuff common to all camera rigs."""
477 # Add the camera object
478 cam_name = "%s_Camera" % mode.capitalize()
479 cam_data = bpy.data.cameras.new(cam_name)
480 cam = object_utils.object_data_add(context, cam_data, name=cam_name)
481 context.scene.camera = cam
483 # Add the rig object
484 rig_name = mode.capitalize() + "_Rig"
485 rig_data = bpy.data.armatures.new(rig_name)
486 rig = object_utils.object_data_add(context, rig_data, name=rig_name)
487 rig["rig_id"] = "%s" % rig_name
488 rig.location = context.scene.cursor.location
490 bpy.ops.object.mode_set(mode='EDIT')
492 # Add new bones and setup specific rigs
493 if mode == "DOLLY":
494 create_dolly_bones(rig)
495 setup_3d_rig(rig, cam)
496 elif mode == "CRANE":
497 create_crane_bones(rig)
498 setup_3d_rig(rig, cam)
499 elif mode == "2D":
500 create_2d_bones(context, rig, cam)
502 # Parent the camera to the rig
503 cam.location = (0.0, -1.0, 0.0) # Move the camera to the correct position
504 cam.parent = rig
505 cam.parent_type = "BONE"
506 if mode == "2D":
507 cam.parent_bone = "Camera"
508 else:
509 cam.parent_bone = "Camera_offset"
511 # Change display to BBone: it just looks nicer
512 rig.data.display_type = 'BBONE'
513 # Change display to wire for object
514 rig.display_type = 'WIRE'
516 # Lock camera transforms
517 cam.lock_location = (True,) * 3
518 cam.lock_rotation = (True,) * 3
519 cam.lock_scale = (True,) * 3
521 # Add custom properties to the armature’s Camera bone,
522 # so that all properties may be animated in a single action
524 pose_bones = rig.pose.bones
526 # DOF Focus Distance property
527 pb = pose_bones['Camera']
528 pb["focus_distance"] = 10.0
529 prop = rna_idprop_ui_prop_get(pb, "focus_distance", create=True)
530 prop["default"] = 10.0
531 prop["min"] = 0.0
533 # DOF F-Stop property
534 pb = pose_bones['Camera']
535 pb["aperture_fstop"] = 2.8
536 prop = rna_idprop_ui_prop_get(pb, "aperture_fstop", create=True)
537 prop["default"] = 2.8
538 prop["min"] = 0.0
539 prop["soft_min"] = 0.1
540 prop["soft_max"] = 128.0
542 # Add drivers to link the camera properties to the custom props
543 # on the armature
544 create_prop_driver(rig, cam, "focus_distance", "dof.focus_distance")
545 create_prop_driver(rig, cam, "aperture_fstop", "dof.aperture_fstop")
547 # Make the rig the active object
548 view_layer = context.view_layer
549 for obj in view_layer.objects:
550 obj.select_set(False)
551 rig.select_set(True)
552 view_layer.objects.active = rig
555 class OBJECT_OT_build_camera_rig(Operator):
556 bl_idname = "object.build_camera_rig"
557 bl_label = "Build Camera Rig"
558 bl_description = "Build a Camera Rig"
559 bl_options = {'REGISTER', 'UNDO'}
561 mode: bpy.props.EnumProperty(items=(('DOLLY', 'Dolly', 'Dolly rig'),
562 ('CRANE', 'Crane', 'Crane rig',),
563 ('2D', '2D', '2D rig')),
564 name="mode",
565 description="Type of camera to create",
566 default="DOLLY")
568 def execute(self, context):
569 # Build the rig
570 build_camera_rig(context, self.mode)
571 return {'FINISHED'}
574 def add_dolly_crane_buttons(self, context):
575 """Dolly and crane entries in the Add Object > Camera Menu"""
576 if context.mode == 'OBJECT':
577 self.layout.operator(
578 OBJECT_OT_build_camera_rig.bl_idname,
579 text="Dolly Camera Rig",
580 icon='VIEW_CAMERA'
581 ).mode = "DOLLY"
583 self.layout.operator(
584 OBJECT_OT_build_camera_rig.bl_idname,
585 text="Crane Camera Rig",
586 icon='VIEW_CAMERA'
587 ).mode = "CRANE"
589 self.layout.operator(
590 OBJECT_OT_build_camera_rig.bl_idname,
591 text="2D Camera Rig",
592 icon='PIVOT_BOUNDBOX'
593 ).mode = "2D"
596 classes = (
597 OBJECT_OT_build_camera_rig,
601 def register():
602 from bpy.utils import register_class
603 for cls in classes:
604 register_class(cls)
606 bpy.types.VIEW3D_MT_camera_add.append(add_dolly_crane_buttons)
609 def unregister():
610 from bpy.utils import unregister_class
611 for cls in classes:
612 unregister_class(cls)
614 bpy.types.VIEW3D_MT_camera_add.remove(add_dolly_crane_buttons)
617 if __name__ == "__main__":
618 register()