Files
blender-addons/add_camera_rigs/build_rigs.py
Campbell Barton e8da6131fd License headers: use SPDX-FileCopyrightText for all addons
Move copyright text to SPDX-FileCopyrightText or set to the
Blender Foundation so "make check_licenses" now runs without warnings.
2023-06-15 16:54:05 +10:00

593 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SPDX-FileCopyrightText: 2019-2022 Blender Foundation
#
# SPDX-License-Identifier: GPL-2.0-or-later
import bpy
from bpy.types import Operator
from bpy_extras import object_utils
from mathutils import Vector
from math import pi
from .create_widgets import (create_root_widget,
create_camera_widget, create_camera_offset_widget,
create_aim_widget, create_circle_widget,
create_corner_widget)
def create_prop_driver(rig, cam, prop_from, prop_to):
"""Create driver to a property on the rig"""
driver = cam.data.driver_add(prop_to)
driver.driver.type = 'SCRIPTED'
var = driver.driver.variables.new()
var.name = 'var'
var.type = 'SINGLE_PROP'
# Target the custom bone property
var.targets[0].id = rig
var.targets[0].data_path = 'pose.bones["Camera"]["%s"]' % prop_from
driver.driver.expression = 'var'
def create_dolly_bones(rig):
"""Create bones for the dolly camera rig"""
bones = rig.data.edit_bones
# Add new bones
root = bones.new("Root")
root.tail = (0.0, 1.0, 0.0)
root.show_wire = True
ctrl_aim_child = bones.new("Aim_shape_rotation-MCH")
ctrl_aim_child.head = (0.0, 10.0, 1.7)
ctrl_aim_child.tail = (0.0, 11.0, 1.7)
ctrl_aim_child.layers = tuple(i == 1 for i in range(32))
ctrl_aim = bones.new("Aim")
ctrl_aim.head = (0.0, 10.0, 1.7)
ctrl_aim.tail = (0.0, 11.0, 1.7)
ctrl_aim.show_wire = True
ctrl = bones.new("Camera")
ctrl.head = (0.0, 0.0, 1.7)
ctrl.tail = (0.0, 1.0, 1.7)
ctrl.show_wire = True
ctrl_offset = bones.new("Camera_offset")
ctrl_offset.head = (0.0, 0.0, 1.7)
ctrl_offset.tail = (0.0, 1.0, 1.7)
ctrl_offset.show_wire = True
# Setup hierarchy
ctrl.parent = root
ctrl_offset.parent = ctrl
ctrl_aim.parent = root
ctrl_aim_child.parent = ctrl_aim
# Jump into object mode
bpy.ops.object.mode_set(mode='OBJECT')
pose_bones = rig.pose.bones
# Lock the relevant scale channels of the Camera_offset bone
pose_bones["Camera_offset"].lock_scale = (True,) * 3
def create_crane_bones(rig):
"""Create bones for the crane camera rig"""
bones = rig.data.edit_bones
# Add new bones
root = bones.new("Root")
root.tail = (0.0, 1.0, 0.0)
root.show_wire = True
ctrl_aim_child = bones.new("Aim_shape_rotation-MCH")
ctrl_aim_child.head = (0.0, 10.0, 1.7)
ctrl_aim_child.tail = (0.0, 11.0, 1.7)
ctrl_aim_child.layers = tuple(i == 1 for i in range(32))
ctrl_aim = bones.new("Aim")
ctrl_aim.head = (0.0, 10.0, 1.7)
ctrl_aim.tail = (0.0, 11.0, 1.7)
ctrl_aim.show_wire = True
ctrl = bones.new("Camera")
ctrl.head = (0.0, 1.0, 1.7)
ctrl.tail = (0.0, 2.0, 1.7)
ctrl_offset = bones.new("Camera_offset")
ctrl_offset.head = (0.0, 1.0, 1.7)
ctrl_offset.tail = (0.0, 2.0, 1.7)
arm = bones.new("Crane_arm")
arm.head = (0.0, 0.0, 1.7)
arm.tail = (0.0, 1.0, 1.7)
height = bones.new("Crane_height")
height.head = (0.0, 0.0, 0.0)
height.tail = (0.0, 0.0, 1.7)
# Setup hierarchy
ctrl.parent = arm
ctrl_offset.parent = ctrl
ctrl.use_inherit_rotation = False
ctrl.use_inherit_scale = False
ctrl.show_wire = True
arm.parent = height
arm.use_inherit_scale = False
height.parent = root
ctrl_aim.parent = root
ctrl_aim_child.parent = ctrl_aim
# Jump into object mode
bpy.ops.object.mode_set(mode='OBJECT')
pose_bones = rig.pose.bones
# Lock the relevant loc, rot and scale
pose_bones["Crane_arm"].lock_rotation = (False, True, False)
pose_bones["Crane_arm"].lock_scale = (True, False, True)
pose_bones["Crane_height"].lock_location = (True,) * 3
pose_bones["Crane_height"].lock_rotation = (True,) * 3
pose_bones["Crane_height"].lock_scale = (True, False, True)
pose_bones["Camera_offset"].lock_scale = (True,) * 3
def setup_3d_rig(rig, cam):
"""Finish setting up Dolly and Crane rigs"""
# Jump into object mode and change bones to euler
bpy.ops.object.mode_set(mode='OBJECT')
pose_bones = rig.pose.bones
for bone in pose_bones:
bone.rotation_mode = 'XYZ'
# Lens property
pb = pose_bones['Camera']
pb["lens"] = 50.0
ui_data = pb.id_properties_ui("lens")
ui_data.update(min=1.0, max=1000000.0, soft_max = 5000.0, default=50.0)
# Build the widgets
root_widget = create_root_widget("Camera_Root")
camera_widget = create_camera_widget("Camera")
camera_offset_widget = create_camera_offset_widget("Camera_offset")
aim_widget = create_aim_widget("Aim")
# Add the custom bone shapes
pose_bones["Root"].custom_shape = root_widget
pose_bones["Aim"].custom_shape = aim_widget
pose_bones["Camera"].custom_shape = camera_widget
pose_bones["Camera_offset"].custom_shape = camera_offset_widget
# Set the "Override Transform" field to the mechanism position
pose_bones["Aim"].custom_shape_transform = pose_bones["Aim_shape_rotation-MCH"]
# Add constraints to bones
con = pose_bones['Aim_shape_rotation-MCH'].constraints.new('COPY_ROTATION')
con.target = rig
con.subtarget = "Camera"
con = pose_bones['Camera'].constraints.new('TRACK_TO')
con.track_axis = 'TRACK_Y'
con.up_axis = 'UP_Z'
con.target = rig
con.subtarget = "Aim"
con.use_target_z = True
cam.data.display_size = 1.0
cam.rotation_euler[0] = pi / 2.0 # Rotate the camera 90 degrees in x
create_prop_driver(rig, cam, "lens", "lens")
def create_2d_bones(context, rig, cam):
"""Create bones for the 2D camera rig"""
scene = context.scene
bones = rig.data.edit_bones
# Add new bones
bones = rig.data.edit_bones
root = bones.new("Root")
root.tail = Vector((0.0, 0.0, 1.0))
root.show_wire = True
ctrl = bones.new('Camera')
ctrl.tail = Vector((0.0, 0.0, 1.0))
ctrl.show_wire = True
left_corner = bones.new("Left_corner")
left_corner.head = (-3, 10, -2)
left_corner.tail = left_corner.head + Vector((0.0, 0.0, 1.0))
left_corner.show_wire = True
right_corner = bones.new("Right_corner")
right_corner.head = (3, 10, -2)
right_corner.tail = right_corner.head + Vector((0.0, 0.0, 1.0))
right_corner.show_wire = True
corner_distance_x = (left_corner.head - right_corner.head).length
corner_distance_y = -left_corner.head.z
corner_distance_z = left_corner.head.y
center = bones.new("Center-MCH")
center.head = ((right_corner.head + left_corner.head) / 2.0)
center.tail = center.head + Vector((0.0, 0.0, 1.0))
center.layers = tuple(i == 1 for i in range(32))
center.show_wire = True
# Setup hierarchy
ctrl.parent = root
left_corner.parent = root
right_corner.parent = root
center.parent = root
# Jump into object mode and change bones to euler
bpy.ops.object.mode_set(mode='OBJECT')
pose_bones = rig.pose.bones
for bone in pose_bones:
bone.rotation_mode = 'XYZ'
# Bone drivers
center_drivers = pose_bones["Center-MCH"].driver_add("location")
# Center X driver
driver = center_drivers[0].driver
driver.type = 'AVERAGE'
for corner in ('left', 'right'):
var = driver.variables.new()
var.name = corner
var.type = 'TRANSFORMS'
var.targets[0].id = rig
var.targets[0].bone_target = corner.capitalize() + '_corner'
var.targets[0].transform_type = 'LOC_X'
var.targets[0].transform_space = 'TRANSFORM_SPACE'
# Center Y driver
driver = center_drivers[1].driver
driver.type = 'SCRIPTED'
driver.expression = '({distance_x} - (left_x-right_x))*(res_y/res_x)/2 + (left_y + right_y)/2'.format(
distance_x=corner_distance_x)
for direction in ('x', 'y'):
for corner in ('left', 'right'):
var = driver.variables.new()
var.name = '%s_%s' % (corner, direction)
var.type = 'TRANSFORMS'
var.targets[0].id = rig
var.targets[0].bone_target = corner.capitalize() + '_corner'
var.targets[0].transform_type = 'LOC_' + direction.upper()
var.targets[0].transform_space = 'TRANSFORM_SPACE'
var = driver.variables.new()
var.name = 'res_' + direction
var.type = 'SINGLE_PROP'
var.targets[0].id_type = 'SCENE'
var.targets[0].id = scene
var.targets[0].data_path = 'render.resolution_' + direction
# Center Z driver
driver = center_drivers[2].driver
driver.type = 'AVERAGE'
for corner in ('left', 'right'):
var = driver.variables.new()
var.name = corner
var.type = 'TRANSFORMS'
var.targets[0].id = rig
var.targets[0].bone_target = corner.capitalize() + '_corner'
var.targets[0].transform_type = 'LOC_Z'
var.targets[0].transform_space = 'TRANSFORM_SPACE'
# Bone constraints
con = pose_bones["Camera"].constraints.new('DAMPED_TRACK')
con.target = rig
con.subtarget = "Center-MCH"
con.track_axis = 'TRACK_NEGATIVE_Z'
# Build the widgets
left_widget = create_corner_widget("Left_corner", reverse=True)
right_widget = create_corner_widget("Right_corner")
parent_widget = create_circle_widget("Root", radius=0.5)
camera_widget = create_circle_widget("Camera_2D", radius=0.3)
# Add the custom bone shapes
pose_bones["Left_corner"].custom_shape = left_widget
pose_bones["Right_corner"].custom_shape = right_widget
pose_bones["Root"].custom_shape = parent_widget
pose_bones["Camera"].custom_shape = camera_widget
# Lock the relevant loc, rot and scale
pose_bones["Left_corner"].lock_rotation = (True,) * 3
pose_bones["Right_corner"].lock_rotation = (True,) * 3
pose_bones["Camera"].lock_rotation = (True,) * 3
pose_bones["Camera"].lock_scale = (True,) * 3
# Camera settings
cam.data.sensor_fit = "HORIZONTAL" # Avoids distortion in portrait format
# Property to switch between rotation and switch mode
pose_bones["Camera"]['rotation_shift'] = 0.0
ui_data = pose_bones["Camera"].id_properties_ui('rotation_shift')
ui_data.update(min=0.0, max=1.0, description="rotation_shift")
# Rotation / shift switch driver
driver = con.driver_add('influence').driver
driver.expression = '1 - rotation_shift'
var = driver.variables.new()
var.name = 'rotation_shift'
var.type = 'SINGLE_PROP'
var.targets[0].id = rig
var.targets[0].data_path = 'pose.bones["Camera"]["rotation_shift"]'
# Focal length driver
driver = cam.data.driver_add('lens').driver
driver.expression = 'abs({distance_z} - (left_z + right_z)/2 + cam_z) * 36 / frame_width'.format(
distance_z=corner_distance_z)
var = driver.variables.new()
var.name = 'frame_width'
var.type = 'LOC_DIFF'
var.targets[0].id = rig
var.targets[0].bone_target = "Left_corner"
var.targets[0].transform_space = 'WORLD_SPACE'
var.targets[1].id = rig
var.targets[1].bone_target = "Right_corner"
var.targets[1].transform_space = 'WORLD_SPACE'
for corner in ('left', 'right'):
var = driver.variables.new()
var.name = corner + '_z'
var.type = 'TRANSFORMS'
var.targets[0].id = rig
var.targets[0].bone_target = corner.capitalize() + '_corner'
var.targets[0].transform_type = 'LOC_Z'
var.targets[0].transform_space = 'TRANSFORM_SPACE'
var = driver.variables.new()
var.name = 'cam_z'
var.type = 'TRANSFORMS'
var.targets[0].id = rig
var.targets[0].bone_target = "Camera"
var.targets[0].transform_type = 'LOC_Z'
var.targets[0].transform_space = 'TRANSFORM_SPACE'
# Orthographic scale driver
driver = cam.data.driver_add('ortho_scale').driver
driver.expression = 'abs({distance_x} - (left_x - right_x))'.format(distance_x=corner_distance_x)
for corner in ('left', 'right'):
var = driver.variables.new()
var.name = corner + '_x'
var.type = 'TRANSFORMS'
var.targets[0].id = rig
var.targets[0].bone_target = corner.capitalize() + '_corner'
var.targets[0].transform_type = 'LOC_X'
var.targets[0].transform_space = 'TRANSFORM_SPACE'
# Shift driver X
driver = cam.data.driver_add('shift_x').driver
driver.expression = 'rotation_shift * (((left_x + right_x)/2 - cam_x) * lens / abs({distance_z} - (left_z + right_z)/2 + cam_z) / 36)'.format(
distance_z=corner_distance_z)
var = driver.variables.new()
var.name = 'rotation_shift'
var.type = 'SINGLE_PROP'
var.targets[0].id = rig
var.targets[0].data_path = 'pose.bones["Camera"]["rotation_shift"]'
for direction in ('x', 'z'):
for corner in ('left', 'right'):
var = driver.variables.new()
var.name = '%s_%s' % (corner, direction)
var.type = 'TRANSFORMS'
var.targets[0].id = rig
var.targets[0].bone_target = corner.capitalize() + '_corner'
var.targets[0].transform_type = 'LOC_' + direction.upper()
var.targets[0].transform_space = 'TRANSFORM_SPACE'
var = driver.variables.new()
var.name = 'cam_' + direction
var.type = 'TRANSFORMS'
var.targets[0].id = rig
var.targets[0].bone_target = "Camera"
var.targets[0].transform_type = 'LOC_' + direction.upper()
var.targets[0].transform_space = 'TRANSFORM_SPACE'
var = driver.variables.new()
var.name = 'lens'
var.type = 'SINGLE_PROP'
var.targets[0].id_type = 'CAMERA'
var.targets[0].id = cam.data
var.targets[0].data_path = 'lens'
# Shift driver Y
driver = cam.data.driver_add('shift_y').driver
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(
distance_y=corner_distance_y, distance_z=corner_distance_z)
var = driver.variables.new()
var.name = 'rotation_shift'
var.type = 'SINGLE_PROP'
var.targets[0].id = rig
var.targets[0].data_path = 'pose.bones["Camera"]["rotation_shift"]'
for direction in ('y', 'z'):
for corner in ('left', 'right'):
var = driver.variables.new()
var.name = '%s_%s' % (corner, direction)
var.type = 'TRANSFORMS'
var.targets[0].id = rig
var.targets[0].bone_target = corner.capitalize() + '_corner'
var.targets[0].transform_type = 'LOC_' + direction.upper()
var.targets[0].transform_space = 'TRANSFORM_SPACE'
var = driver.variables.new()
var.name = 'cam_' + direction
var.type = 'TRANSFORMS'
var.targets[0].id = rig
var.targets[0].bone_target = "Camera"
var.targets[0].transform_type = 'LOC_' + direction.upper()
var.targets[0].transform_space = 'TRANSFORM_SPACE'
for direction in ('x', 'y'):
var = driver.variables.new()
var.name = 'res_' + direction
var.type = 'SINGLE_PROP'
var.targets[0].id_type = 'SCENE'
var.targets[0].id = scene
var.targets[0].data_path = 'render.resolution_' + direction
var = driver.variables.new()
var.name = 'lens'
var.type = 'SINGLE_PROP'
var.targets[0].id_type = 'CAMERA'
var.targets[0].id = cam.data
var.targets[0].data_path = 'lens'
def build_camera_rig(context, mode):
"""Create stuff common to all camera rigs."""
# Add the camera object
cam_name = "%s_Camera" % mode.capitalize()
cam_data = bpy.data.cameras.new(cam_name)
cam = object_utils.object_data_add(context, cam_data, name=cam_name)
context.scene.camera = cam
# Add the rig object
rig_name = mode.capitalize() + "_Rig"
rig_data = bpy.data.armatures.new(rig_name)
rig = object_utils.object_data_add(context, rig_data, name=rig_name)
rig["rig_id"] = "%s" % rig_name
rig.location = context.scene.cursor.location
bpy.ops.object.mode_set(mode='EDIT')
# Add new bones and setup specific rigs
if mode == "DOLLY":
create_dolly_bones(rig)
setup_3d_rig(rig, cam)
elif mode == "CRANE":
create_crane_bones(rig)
setup_3d_rig(rig, cam)
elif mode == "2D":
create_2d_bones(context, rig, cam)
# Parent the camera to the rig
cam.location = (0.0, -1.0, 0.0) # Move the camera to the correct position
cam.parent = rig
cam.parent_type = "BONE"
if mode == "2D":
cam.parent_bone = "Camera"
else:
cam.parent_bone = "Camera_offset"
# Change display to BBone: it just looks nicer
rig.data.display_type = 'BBONE'
# Change display to wire for object
rig.display_type = 'WIRE'
# Lock camera transforms
cam.lock_location = (True,) * 3
cam.lock_rotation = (True,) * 3
cam.lock_scale = (True,) * 3
# Add custom properties to the armatures Camera bone,
# so that all properties may be animated in a single action
pose_bones = rig.pose.bones
# DOF Focus Distance property
pb = pose_bones['Camera']
pb["focus_distance"] = 10.0
ui_data = pb.id_properties_ui('focus_distance')
ui_data.update(min=0.0, default=10.0)
# DOF F-Stop property
pb = pose_bones['Camera']
pb["aperture_fstop"] = 2.8
ui_data = pb.id_properties_ui('aperture_fstop')
ui_data.update(min=0.0, soft_min=0.1, soft_max=128.0, default=2.8)
# Add drivers to link the camera properties to the custom props
# on the armature
create_prop_driver(rig, cam, "focus_distance", "dof.focus_distance")
create_prop_driver(rig, cam, "aperture_fstop", "dof.aperture_fstop")
# Make the rig the active object
view_layer = context.view_layer
for obj in view_layer.objects:
obj.select_set(False)
rig.select_set(True)
view_layer.objects.active = rig
class OBJECT_OT_build_camera_rig(Operator):
bl_idname = "object.build_camera_rig"
bl_label = "Build Camera Rig"
bl_description = "Build a Camera Rig"
bl_options = {'REGISTER', 'UNDO'}
mode: bpy.props.EnumProperty(items=(('DOLLY', 'Dolly', 'Dolly rig'),
('CRANE', 'Crane', 'Crane rig',),
('2D', '2D', '2D rig')),
name="mode",
description="Type of camera to create",
default="DOLLY")
def execute(self, context):
# Build the rig
build_camera_rig(context, self.mode)
return {'FINISHED'}
def add_dolly_crane_buttons(self, context):
"""Dolly and crane entries in the Add Object > Camera Menu"""
if context.mode == 'OBJECT':
self.layout.operator(
OBJECT_OT_build_camera_rig.bl_idname,
text="Dolly Camera Rig",
icon='VIEW_CAMERA'
).mode = "DOLLY"
self.layout.operator(
OBJECT_OT_build_camera_rig.bl_idname,
text="Crane Camera Rig",
icon='VIEW_CAMERA'
).mode = "CRANE"
self.layout.operator(
OBJECT_OT_build_camera_rig.bl_idname,
text="2D Camera Rig",
icon='PIVOT_BOUNDBOX'
).mode = "2D"
classes = (
OBJECT_OT_build_camera_rig,
)
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
bpy.types.VIEW3D_MT_camera_add.append(add_dolly_crane_buttons)
def unregister():
from bpy.utils import unregister_class
for cls in classes:
unregister_class(cls)
bpy.types.VIEW3D_MT_camera_add.remove(add_dolly_crane_buttons)
if __name__ == "__main__":
register()