In this post, we show how to create a UI panel and Python script to rotate a camera (or other object) around a target. This builds on our previous post where we created a camera rotation script.
The Code
Here is the full code. Note that throughout the script, we call the rotating object a camera. However, this could be any object.
#import the modules
import bpy
import numpy as np
import math
from mathutils import *
# Property Group for the settings
class RotationPropertyGroup(bpy.types.PropertyGroup):
speed: bpy.props.FloatProperty(name="speed",default=1, description="number of rotations in timeframe")
numKeyframes: bpy.props.IntProperty(name="keyframes",default=30, description="number of keyframes to create")
target: bpy.props.StringProperty(name="target", description="target object to rotate around")
class RotationOperator(bpy.types.Operator):
"""Adds keyframes to an object to rotate around another object."""
bl_idname = "object.rotation_operator"
bl_label = "Camera Rotation"
def rotateCamera(self,frameNumber):
"""Rotate the camera around an object."""
newTheta = self.theta*frameNumber
rotationMatrix = np.array([[math.cos(newTheta),-math.sin(newTheta),0],
[math.sin(newTheta), math.cos(newTheta),0],
[0,0,1]])
return np.dot(self.cameraOrigin,rotationMatrix) + self.focusObject.location
def execute(self, context):
# Create a variable to hold the currently selected object
camera = bpy.context.object
self.cameraOrigin = Vector(camera.location)
# Create a variable to hold the focus object
self.focusObject = bpy.data.objects[bpy.context.object.rotation_prop.target]
keyframes = bpy.context.object.rotation_prop.numKeyframes
speed = bpy.context.object.rotation_prop.speed
# Angle (in radians) that the camera will rotate each frame
self.theta = 2*math.pi*speed/(bpy.context.scene.frame_end - bpy.context.scene.frame_start)
framesPerKey = (bpy.context.scene.frame_end - bpy.context.scene.frame_start)/keyframes
for currentKeyframe in range(0,keyframes):
currentFrame = currentKeyframe*framesPerKey + 1
newPos = self.rotateCamera(currentFrame)
camera.location = newPos
camera.keyframe_insert(data_path='location',frame=currentFrame+bpy.context.scene.frame_start-1)
# Reset to the first frame
bpy.context.scene.frame_current = bpy.context.scene.frame_start
return {'FINISHED'}
class RotationClearOperator(bpy.types.Operator):
"""Clear keyframes."""
bl_idname = "object.rotation_clear_operator"
bl_label = "Clear object rotation keyframes"
def execute(self, context):
return bpy.ops.anim.keyframe_clear_v3d()
class RotationPanel(bpy.types.Panel):
bl_idname = 'VIEW3D_PT_rotation_panel'
bl_label = 'Camera Rotation'
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
def draw(self, context):
self.layout.prop(bpy.context.object.rotation_prop,"speed")
self.layout.prop(bpy.context.object.rotation_prop,"numKeyframes")
self.layout.prop_search(bpy.context.object.rotation_prop, "target", bpy.context.scene, "objects")
self.layout.separator()
self.layout.operator(RotationOperator.bl_idname, text=RotationOperator.bl_label)
self.layout.operator(RotationClearOperator.bl_idname,text="Clear keyframes")
def register():
bpy.utils.register_class(RotationPropertyGroup)
bpy.utils.register_class(RotationOperator)
bpy.utils.register_class(RotationClearOperator)
bpy.utils.register_class(RotationPanel)
# Create pointer reference for the property group
bpy.types.Object.rotation_prop = bpy.props.PointerProperty(type=RotationPropertyGroup)
if __name__ == "__main__":
register()
Rotation Property Group
To start, we define a rotation property group. A property group is a convenient way of keeping related properties together. In this case, we have three properties:
- speed: determines the speed of the rotation
- numKeyframes: the number of keyframes to create. More keyframes results in a smoother rotation
- target: target object to rotate around
# Property Group for the settings
class RotationPropertyGroup(bpy.types.PropertyGroup):
speed: bpy.props.FloatProperty(name="speed",default=1, description="number of rotations in timeframe")
numKeyframes: bpy.props.IntProperty(name="keyframes",default=30, description="number of keyframes to create")
target: bpy.props.StringProperty(name="target", description="target object to rotate around")
Camera Rotation Operator
Next, we create a rotation operator. This operator will be called when the user clicks a button to run the rotation. The operator will cycle through the target frames and add keyframes at set intervals.
Camera Rotation Method
We create a rotateCamera method. This function takes the current frame number as a parameter. It then calculates the current location of the camera using a rotation matrix (and Numpy). For more detail on the math, see here.
def rotateCamera(self,frameNumber):
"""Rotate the camera around an object."""
newTheta = self.theta*frameNumber
rotationMatrix = np.array([[math.cos(newTheta),-math.sin(newTheta),0],
[math.sin(newTheta), math.cos(newTheta),0],
[0,0,1]])
return np.dot(self.cameraOrigin,rotationMatrix) + self.focusObject.location
Execute the Camera Rotation Operator
The next method executes the operator.
The Variables
We create variables to hold the currently selected object and the start location of the currently selected object.
camera = bpy.context.object
self.cameraOrigin = Vector(camera.location)
Next, we create variables to hold the user specified information from the UI panel (more on the UI panel later). Specifically, the target object, the number of keyframes to create, and the speed of the rotation.
self.focusObject = bpy.data.objects[bpy.context.object.rotation_prop.target]
keyframes = bpy.context.object.rotation_prop.numKeyframes
speed = bpy.context.object.rotation_prop.speed
We then calculate the rotation angle per frame (in radians) and the number of frames per keyframe:
self.theta = 2*math.pi*speed/(bpy.context.scene.frame_end - bpy.context.scene.frame_start)
framesPerKey = (bpy.context.scene.frame_end - bpy.context.scene.frame_start)/keyframes
The Camera Rotation Loop
Now we create the keyframes. We loop through the number of keyframes. For each iteration of the loop, we call the rotateCamera method, which returns the next position. We then insert a keyframe. We adjust the insertion keyframe based on the scene.frame_start (in case the user doesn’t want the rotation to start on the first frame).
for currentKeyframe in range(0,keyframes):
currentFrame = currentKeyframe*framesPerKey + 1
newPos = self.rotateCamera(currentFrame)
camera.location = newPos
camera.keyframe_insert(data_path='location', frame=currentFrame+bpy.context.scene.frame_start-1)
Finally, we change the current frame back to the start frame.
bpy.context.scene.frame_current = bpy.context.scene.frame_start
Clearing the Camera Rotation
Since the rotation operator works by adding keyframes, we want an easy way to clean up the keyframes. Therefore, we also create an operator that clears the keyframes on the current object. This requires a simple call to the existing operator
bpy.ops.anim.keyframe_clear_v3d()
The UI Panel
We have our operators, now we need to create the interface.
class RotationPanel(bpy.types.Panel):
bl_idname = 'VIEW3D_PT_rotation_panel'
bl_label = 'Camera Rotation'
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
def draw(self, context):
self.layout.prop(bpy.context.object.rotation_prop,"speed")
self.layout.prop(bpy.context.object.rotation_prop,"numKeyframes")
self.layout.prop_search(bpy.context.object.rotation_prop, "target", bpy.context.scene, "objects")
self.layout.separator()
self.layout.operator(RotationOperator.bl_idname, text=RotationOperator.bl_label)
self.layout.operator(RotationClearOperator.bl_idname,text="Clear keyframes")
Location, Location, Location
The local variables of the class set the name of the panel, but they also determine where the panel will appear. Specifically, bl_space_type is set to VIEW_3D, which tells Blender to place this in the 3D viewport.
bl_idname = 'VIEW3D_PT_rotation_panel'
bl_label = 'Camera Rotation'
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
Draw the UI Elements
Next, we create the draw method. This method has two parts, the first 3 lines show the properties that we want to allow the user to edit. Blender takes care of determining what type of UI element to show based on the type of property. For example, speed and keyframes are just numbers, while the prop_search creates a dropdown of current objects.
self.layout.prop(bpy.context.object.rotation_prop,"speed")
self.layout.prop(bpy.context.object.rotation_prop,"numKeyframes")
self.layout.prop_search(bpy.context.object.rotation_prop, "target", bpy.context.scene, "objects")
We add a separator for spacing:
self.layout.separator()
Then we add two buttons. One creates the key frames, and the other deletes them.
self.layout.operator(RotationOperator.bl_idname, text=RotationOperator.bl_label)
self.layout.operator(RotationClearOperator.bl_idname,text="Clear keyframes")
Register the Classes
We register the four subclasses that we created:
- RotationPropertyGroup
- RotationOperator
- RotationClearOperator
- RotationPanel
bpy.utils.register_class(RotationPropertyGroup)
bpy.utils.register_class(RotationOperator)
bpy.utils.register_class(RotationClearOperator)
bpy.utils.register_class(RotationPanel)
Next, we create a pointer property to our RotationPropertyGroup and assign it to the objects in the scene. This allows each object in the scene to have its own speed and number of keyframes. We could assign this everywhere, because these are only used to make the keyframes for the object. However, I think assigning this to the object makes sense so we can remember the setting that we used for this particular object if we have multiple rotating objects in the scene.
# Create pointer reference for the property group
bpy.types.Object.rotation_prop = bpy.props.PointerProperty(type=RotationPropertyGroup)
Conclusion
I am excited about this UI panel. I like to add camera rotation to my animations. This UI panel allows me to easily add the rotation and to customize the look of the rotation. The last thing that I do is set the track to constraint to the object I am rotating. However, depending on what you are rotating, you may not want that (e.g., a planet rotating around a sun). Please let me know in the comments if there are any other features that would be useful in this panel.