Skip to content
Snippets Groups Projects
camera_turnaround.py 11.35 KiB
# SPDX-License-Identifier: GPL-2.0-or-later

bl_info = {
    "name": "Turnaround Camera",
    "author": "Antonio Vazquez (antonioya)",
    "version": (0, 3, 0),
    "blender": (2, 80, 0),
    "location": "View3D > Sidebar > View Tab > Turnaround Camera",
    "description": "Add a camera rotation around selected object",
    "doc_url": "{BLENDER_MANUAL_URL}/addons/animation/turnaround_camera.html",
    "category": "Animation",
}


import bpy
from math import pi
from bpy.props import (
    BoolProperty,
    EnumProperty,
    FloatProperty,
    PointerProperty,
)
from bpy.types import (
    Operator,
    Panel,
    PropertyGroup,
)


# ------------------------------------------------------
# Action class
# ------------------------------------------------------
class CAMERATURN_OT_RunAction(Operator):
    bl_idname = "object.rotate_around"
    bl_label = "Turnaround"
    bl_description = "Create camera rotation around selected object"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        # ----------------------
        # Save old data
        # ----------------------
        scene = context.scene
        turn_camera = scene.turn_camera
        selectobject = context.active_object
        camera = scene.camera
        savedcursor = scene.cursor.location.copy()  # cursor position
        savedframe = scene.frame_current
        if turn_camera.use_cursor is False:
            bpy.ops.view3d.snap_cursor_to_selected()

        # -------------------------
        # Create empty and parent
        # -------------------------
        bpy.ops.object.empty_add(type='PLAIN_AXES')
        myempty = context.active_object

        myempty.location = selectobject.location
        savedstate = myempty.matrix_world
        myempty.parent = selectobject
        myempty.name = "MCH_Rotation_target"
        myempty.matrix_world = savedstate

        # -------------------------
        # Parent camera to empty
        # -------------------------
        savedstate = camera.matrix_world
        camera.parent = myempty
        camera.matrix_world = savedstate

        # -------------------------
        # Now add revolutions
        # (make empty active object)
        # -------------------------
        bpy.ops.object.select_all(False)
        myempty.select_set(True)
        context.view_layer.objects.active = myempty
        # save current configuration
        savedinterpolation = context.preferences.edit.keyframe_new_interpolation_type
        # change interpolation mode
        context.preferences.edit.keyframe_new_interpolation_type = 'LINEAR'
        # create first frame
        myempty.rotation_euler = (0, 0, 0)
        myempty.empty_display_size = 0.1
        scene.frame_set(scene.frame_start)
        myempty.keyframe_insert(data_path="rotation_euler", frame=scene.frame_start)

        # Clear the Camera Animations if the option is checked
        if turn_camera.reset_cam_anim:
            try:
                if bpy.data.cameras[camera.name].animation_data:
                    bpy.data.cameras[camera.name].animation_data_clear()
            except Exception as e:
                print("\n[Camera Turnaround]\nWarning: {}\n".format(e))

        # Dolly zoom
        if turn_camera.dolly_zoom != "0":
            bpy.data.cameras[camera.name].lens = turn_camera.camera_from_lens
            bpy.data.cameras[camera.name].keyframe_insert("lens", frame=scene.frame_start)

        # Calculate rotation XYZ
        ix = -1 if turn_camera.inverse_x else 1
        iy = -1 if turn_camera.inverse_y else 1
        iz = -1 if turn_camera.inverse_z else 1

        xrot = (pi * 2) * turn_camera.camera_revol_x * ix
        yrot = (pi * 2) * turn_camera.camera_revol_y * iy
        zrot = (pi * 2) * turn_camera.camera_revol_z * iz

        # create middle frame
        if turn_camera.back_forw is True:
            myempty.rotation_euler = (xrot, yrot, zrot)
            myempty.keyframe_insert(
                data_path="rotation_euler",
                frame=((scene.frame_end - scene.frame_start) / 2)
            )
            # reverse
            xrot *= -1
            yrot *= -1
            zrot = 0

        # Dolly zoom
        if turn_camera.dolly_zoom == "2":
            bpy.data.cameras[camera.name].lens = turn_camera.camera_to_lens
            bpy.data.cameras[camera.name].keyframe_insert(
                "lens",
                frame=((scene.frame_end - scene.frame_start) / 2)
            )

        # create last frame
        myempty.rotation_euler = (xrot, yrot, zrot)
        myempty.keyframe_insert(data_path="rotation_euler", frame=scene.frame_end)
        # Dolly zoom
        if turn_camera.dolly_zoom != "0":
            if turn_camera.dolly_zoom == "1":
                bpy.data.cameras[camera.name].lens = turn_camera.camera_to_lens  # final
            else:
                bpy.data.cameras[camera.name].lens = turn_camera.camera_from_lens  # back to init

            bpy.data.cameras[camera.name].keyframe_insert(
                "lens", frame=scene.frame_end
            )

        # Track constraint
        if turn_camera.track is True:
            bpy.context.view_layer.objects.active = camera
            bpy.ops.object.constraint_add(type='TRACK_TO')
            bpy.context.object.constraints[-1].track_axis = 'TRACK_NEGATIVE_Z'
            bpy.context.object.constraints[-1].up_axis = 'UP_Y'
            bpy.context.object.constraints[-1].target = myempty

        # back previous configuration
        context.preferences.edit.keyframe_new_interpolation_type = savedinterpolation
        scene.cursor.location = savedcursor

        # -------------------------
        # Back to old selection
        # -------------------------
        bpy.ops.object.select_all(False)
        selectobject.select_set(True)
        bpy.context.view_layer.objects.active = selectobject
        scene.frame_set(savedframe)

        return {'FINISHED'}


# ------------------------------------------------------
# Define Properties
# ------------------------------------------------------
class CAMERATURN_Props(PropertyGroup):

    camera_revol_x: FloatProperty(
        name="X", min=0, max=25,
        default=0, precision=2,
        description="Number total of revolutions in X axis"
    )
    camera_revol_y: FloatProperty(
        name="Y", min=0, max=25,
        default=0, precision=2,
        description="Number total of revolutions in Y axis"
    )
    camera_revol_z: FloatProperty(
        name="Z", min=0, max=25,
        default=1, precision=2,
        description="Number total of revolutions in Z axis"
    )
    inverse_x: BoolProperty(
        name="-X",
        description="Inverse rotation",
        default=False
    )
    inverse_y: BoolProperty(
        name="-Y",
        description="Inverse rotation",
        default=False
    )
    inverse_z: BoolProperty(
        name="-Z",
        description="Inverse rotation",
        default=False
    )
    use_cursor: BoolProperty(
        name="Use cursor position",
        description="Use cursor position instead of object origin",
        default=False
    )
    back_forw: BoolProperty(
        name="Back and forward",
        description="Create back and forward animation",
        default=False
    )
    dolly_zoom: EnumProperty(
        items=(
            ('0', "None", ""),
            ('1', "Dolly zoom", ""),
            ('2', "Dolly zoom B/F", "")
        ),
        name="Lens Effects",
        description="Create a camera lens movement"
    )
    camera_from_lens: FloatProperty(
        name="From",
        min=1, max=500, default=35,
        precision=3,
        description="Start lens value"
    )
    camera_to_lens: FloatProperty(
        name="To",
        min=1, max=500,
        default=35, precision=3,
        description="End lens value"
    )
    track: BoolProperty(
        name="Create track constraint",
        description="Add a track constraint to the camera",
        default=False
    )
    reset_cam_anim: BoolProperty(
        name="Clear Camera",
        description="Clear previous camera animations if there are any\n"
        "(For instance, previous Dolly Zoom)",
        default=False
    )


# ------------------------------------------------------
# UI Class
# ------------------------------------------------------
class CAMERATURN_PT_ui(Panel):
    bl_idname = "CAMERA_TURN_PT_main"
    bl_label = "Turnaround Camera"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = "Animate"
    bl_context = "objectmode"
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        scene = context.scene
        turn_camera = scene.turn_camera

        try:
            scene.camera.name
        except AttributeError:
            row = layout.row(align=False)
            row.label(text="No defined camera for scene", icon="INFO")
            return

        if context.active_object is not None:
            if context.active_object.type != 'CAMERA':
                buf = context.active_object.name
                row = layout.row(align=True)
                row.operator("object.rotate_around", icon='OUTLINER_DATA_CAMERA')
                box = row.box()
                box.scale_y = 0.5
                box.label(text=buf, icon='MESH_DATA')
                row = layout.row(align=False)
                row.prop(scene, "camera")

                layout.label(text="Rotation:")
                row = layout.row(align=True)
                row.prop(scene, "frame_start")
                row.prop(scene, "frame_end")

                col = layout.column(align=True)
                split = col.split(factor=0.85, align=True)
                split.prop(turn_camera, "camera_revol_x")
                split.prop(turn_camera, "inverse_x", toggle=True)
                split = col.split(factor=0.85, align=True)
                split.prop(turn_camera, "camera_revol_y")
                split.prop(turn_camera, "inverse_y", toggle=True)
                split = col.split(factor=0.85, align=True)
                split.prop(turn_camera, "camera_revol_z")
                split.prop(turn_camera, "inverse_z", toggle=True)

                col = layout.column(align=True)
                col.label(text="Options:")
                row = col.row(align=True)
                row.prop(turn_camera, "back_forw", toggle=True)
                row.prop(turn_camera, "reset_cam_anim", toggle=True)
                col.prop(turn_camera, "track", toggle=True)
                col.prop(turn_camera, "use_cursor", toggle=True)

                row = layout.row()
                row.prop(turn_camera, "dolly_zoom")
                if turn_camera.dolly_zoom != "0":
                    row = layout.row(align=True)
                    row.prop(turn_camera, "camera_from_lens")
                    row.prop(turn_camera, "camera_to_lens")

            else:
                buf = "No valid object selected"
                layout.label(text=buf, icon='MESH_DATA')


# ------------------------------------------------------
# Registration
# ------------------------------------------------------
classes = (
    CAMERATURN_OT_RunAction,
    CAMERATURN_PT_ui,
    CAMERATURN_Props
)


def register():
    from bpy.utils import register_class
    for cls in classes:
        register_class(cls)

    bpy.types.Scene.turn_camera = PointerProperty(type=CAMERATURN_Props)


def unregister():
    from bpy.utils import unregister_class
    for cls in reversed(classes):
        unregister_class(cls)

    del bpy.types.Scene.turn_camera


if __name__ == "__main__":
    register()