Skip to content
Snippets Groups Projects
camera_turnaround.py 12.61 KiB
# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

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",
    "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/"
                "Scripts/Animation/TurnaroundCamera",
    "category": "Camera"}


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"

    def execute(self, context):
        # ----------------------
        # Save old data
        # ----------------------
        scene = context.scene
        turn_camera = scene.turn_camera
        selectobject = context.active_object
        camera = context.scene.camera
        savedcursor = bpy.context.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
        context.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 = bpy.data.objects[myempty.name]

        # back previous configuration
        context.preferences.edit.keyframe_new_interpolation_type = savedinterpolation
        bpy.context.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
        bpy.context.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 = "View"

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

        try:
            bpy.context.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()