Blender to Roblox Camera Cutscene Addon

Camera Cutscene Addon


This addon allows you to export Blender camera keyframes to JSON data, which then can be used to make Roblox cutscenes after decoding to Lua.

Installing Addon to Blender

Step 1

Create a Python script file with the Addon source code.

Source Code:
bl_info = {
    "name": "Export Camera Animation to JSON (with CFrame Matrix)",
    "author": "ApparentlyJames",
    "version": (1, 2),
    "blender": (2, 80, 0),
    "location": "File > Export > Camera Animation (.json)",
    "description": "Export camera animation with baked modifiers using CFrame-compatible matrix format for Roblox.",
    "category": "Import-Export",
}

import bpy
import json
from bpy_extras.io_utils import ExportHelper
import mathutils
import bpy_extras

class ExportCameraAnimation(bpy.types.Operator, ExportHelper):
    bl_idname = "export_anim.camera_json"
    bl_label = "Export Camera Animation"
    filename_ext = ".json"

    filter_glob: bpy.props.StringProperty(default="*.json", options={'HIDDEN'})

    def execute(self, context):
        scene = context.scene
        cam = scene.camera

        if cam is None:
            self.report({'ERROR'}, "No active camera in the scene.")
            return {'CANCELLED'}

        depsgraph = context.evaluated_depsgraph_get()
        eval_cam = cam.evaluated_get(depsgraph)

        fps = scene.render.fps
        start = scene.frame_start
        end = scene.frame_end

        transform_to_y_up = bpy_extras.io_utils.axis_conversion(
            from_forward='-Y', from_up='Z', to_forward='Z', to_up='Y'
        ).to_4x4()

        cframe_frames = {}
        fov_frames = {}

        for frame in range(start, end + 1):
            scene.frame_set(frame)
            depsgraph.update()

            matrix = eval_cam.matrix_world.copy()
            y_up_matrix = transform_to_y_up @ matrix

            cframe_components = [
                y_up_matrix[0][3], y_up_matrix[1][3], y_up_matrix[2][3],
                y_up_matrix[0][0], y_up_matrix[0][1], y_up_matrix[0][2],
                y_up_matrix[1][0], y_up_matrix[1][1], y_up_matrix[1][2],
                y_up_matrix[2][0], y_up_matrix[2][1], y_up_matrix[2][2],
            ]

            cframe_frames[frame] = cframe_components
            fov_frames[frame] = cam.data.angle

        output = {
            "camera_animation": {
                "fps": fps,
                "frame_range": [start, end],
                "frames": cframe_frames,
                "fov_frames": fov_frames
            }
        }

        with open(self.filepath, 'w') as f:
            json.dump(output, f, indent=4)

        self.report({'INFO'}, f"Camera animation exported to {self.filepath}")
        return {'FINISHED'}

def menu_func_export(self, context):
    self.layout.operator(ExportCameraAnimation.bl_idname, text="Camera Animation (.json)")

def register():
    bpy.utils.register_class(ExportCameraAnimation)
    bpy.types.TOPBAR_MT_file_export.append(menu_func_export)

def unregister():
    bpy.utils.unregister_class(ExportCameraAnimation)
    bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)

if __name__ == "__main__":
    register()

Installing Roblox Plugins

Cutscene Essentials - Creator Store
JSON Importer - Creator Store

Usage Example
Basic Animating

Step 0 (optional):

Enable Auto-Keying by pressing the dot button on the timeline.

image

Step 1:

Set Viewpoint to be the camera by going to View > Viewpoint > Camera, or alternatively pressing the keybind assigned to it (Numpad 0).

Step 2:

Go to Fly/Walk Navigation to position the Camera to your needs by going to View > Navigation > Move Navigation, or alternatively pressing the keybind assigned to it (Shift + F).

Step 3:

Once you're done with the first keyframe, expand the "Summary" in the timeline and make sure the keyframe is inserted. If not, then press I to insert it manually.

Step 4 (optional):

Uncheck "Only Keyframes from Selected Channels", so the timeline keyframes are always visible.

This concludes the essential basic animating, for advanced camera manipulation you should research from YouTube or other articles.

How to Use the Camera in Blender (Tutorial) - YouTube
Mastering Camera Control in Blender: A Comprehensive Guide - BlinksAndButtons

Adding scene to Blender

Step 1:

Select the instances you'll want to insert to Blender, then Right-click and press "Export Selection".

Step 2:

Create a folder, and export selection into it.

Step 3:

On Blender, go to File > Import > Wavefront (.obj), and go to the folder you just created, then select the .obj file.

Step 4:

!! DO NOT MOVE OR SCALE THE IMPORTED SCENE !!

Use the Walk Navigation to move to the scene and continue your animating process.

Tip: Hold SHIFT to move faster.

Exporting

Before baking and exporting, I recommend saving the .blend file to a folder, incase you'll want to edit something.

Step 1:

Press F3, and type in "bake". The Bake Actions, so if you have any graph modifiers, they should get baked onto the animation timeline.

Step 2:

Go to File > Export > Camera Animation (.json). This will save your camera keyframes as a JSON file.

Previewing Animation

Make sure you have installed both Roblox Plugins listed above

Step 1:

Use the JSON Importer plugin to import the exported animation data to workspace.

image

Step 2:

Select the module script you just imported, and click "Play Camera Keyframes"

NOTE: I’ve noticed that when using (Tween) in Edit mode, sometimes it may look jittery. However, this doesn’t seem to be an issue in runtime.

Code Example to playing animations in-game
-- Function to turn Array keyframe to a CFrame
local function arrayToCFrame(arr)
	return CFrame.new(
		arr[1], arr[2], arr[3],
		arr[4], arr[5], arr[6],
		arr[7], arr[8], arr[9],
		arr[10], arr[11], arr[12]
	)
end

-- Animation Variables
local AnimationModule = require(pathToModule)
local animation = AnimationModule.camera_animation

local startPoint = animation.frame_range[1]
local endPoint = animation.frame_range[2]

-- Keyframe Loop
for i = startPoint, endPoint do
	local keyframeID = tostring(i)
	local keyframe = animation.frames[keyframeID]

	if not keyframe then
		continue
	end

	TweenService:Create(workspace.Camera, TweenInfo.new(1 / animation.fps, Enum.EasingStyle.Linear), {
		CFrame = arrayToCFrame(keyframe),
		FieldOfView = math.deg(animation.fov_frames[keyframeID])
	}):Play()

	task.wait(1 / animation.fps)
end

--[[
Animation Data  Format:
{
	camera_animation = {
		fps = <number>,
		frame_range = {
			<start_keyframe_number>,
			<end_keyframe_number>
		},
		frames = {
			[<keyframe_id_string>] = <cframe_array>,
			...
		},
		fov_frames = {
			[<keyframe_id_string>] = <fov_radians>,
			...
		},
	}
}
]]

If you like the plugin, consider donating (through the gamepasses) :]

Made with :heart: by ApparentlyJames


24 Likes

Hello. I tried to use your plugin but encountered this error (see screenshot). Is there a workaround or fix? My animation is very long. I know shortening it might help, but I’d prefer to keep it as is if possible. If you need any additional information, please tag me.

From the error I can see it’s Roblox’s string length limit, so yes, the animation json data is too long for Roblox. So from what I can see you can either shorten the animation itself, or split the json data into chunks to multiple module scripts, which you may need to do manually

1 Like

Okay thank you. Btw cool plugin glad I found it.

2 Likes

Hello! Just started using this plugin and it’s nearly perfect for my use case. However, I want to be able to play camera animations relative to certain parts ingame (e.g. the player’s character pivot) and I’ve tried to modify the code to accomplish this. However, the camera doesn’t animate where it should be.

This is how it looks with my code to convert the CFrames to the dummy’s object space.
https://i.gyazo.com/104fe590b96217b92edfbca68b9833b3.mp4

Here’s how it looks just using the Camera Essentials player.
https://i.gyazo.com/006f4f89379e2b445c93772f3357aefc.mp4

And here’s how it looks in Blender.
https://i.gyazo.com/56cb5e73dcbf364b6a005db1e2aa74bd.mp4

Here’s the modified code which uses the snippet you gave.

local TweenService = game:GetService("TweenService")
-- Function to turn Array keyframe to a CFrame
local function arrayToCFrame(arr)
	return CFrame.new(
		arr[1], arr[2], arr[3],
		arr[4], arr[5], arr[6],
		arr[7], arr[8], arr[9],
		arr[10], arr[11], arr[12]
		)
end

-- Animation Variables
local AnimationModule = require(workspace["imported.lua"])
local animation = AnimationModule.camera_animation
local BlockingDummy = workspace.Alive.BlockingDummy

local startPoint = animation.frame_range[1]
local endPoint = animation.frame_range[2]

-- Keyframe Loop
for i = startPoint, endPoint do
	local keyframeID = tostring(i)
	local keyframe = animation.frames[keyframeID]

	if not keyframe then
		continue
	end
	local keyframeCFrame = arrayToCFrame(keyframe)
-- Need to get cframe position relative to the dummy
	workspace.CurrentCamera.CFrame = BlockingDummy:GetPivot():ToObjectSpace(keyframeCFrame) 
	workspace.CurrentCamera.FieldOfView = math.deg(animation.fov_frames[keyframeID])

	task.wait(1 / animation.fps)
end

Hi! I don’t think you need to use :ToObjectSpace to offset the key frame CFrames to be relative to your target dummy.

Try simply multiplying the Dummy’s Pivot with the original keyframe CFrame like this:

workspace.CurrentCamera.CFrame = BlockingDummy:GetPivot() * keyframeCFrame
2 Likes

Hello, thanks for replying. I replaced the line you gave but the camera is now slightly higher up than in the original animation. See the gif below.

https://i.gyazo.com/51fca83a3da70f9da2fbe5bc94714c68.gif

Update: I replaced the line with this and it fixed it mostly!

workspace.CurrentCamera.CFrame = CFrame.new(BlockingDummy.HumanoidRootPart.CFrame.X, 0, BlockingDummy.HumanoidRootPart.CFrame.Z) * keyframeCFrame
1 Like

Also, I fixed the jittering by replacing the wait after the tween with a RunService.Heartbeat:Wait() which is more accurate. Here’s the entire code.

local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
-- Function to turn Array keyframe to a CFrame
local function arrayToCFrame(arr)
	return CFrame.new(
		arr[1], arr[2], arr[3],
		arr[4], arr[5], arr[6],
		arr[7], arr[8], arr[9],
		arr[10], arr[11], arr[12]
		)
end

-- Animation Variables
local AnimationModule = require(workspace.MuayThaiIntro)
local animation = AnimationModule.camera_animation
local BlockingDummy = workspace.Alive.BlockingDummy

local startPoint = animation.frame_range[1]
local endPoint = animation.frame_range[2]

-- Keyframe Loop
for i = startPoint, endPoint do
	local keyframeID = tostring(i)
	local keyframe = animation.frames[keyframeID]

	if not keyframe then
		continue
	end
	local keyframeCFrame = arrayToCFrame(keyframe)
	
	TweenService:Create(workspace.Camera, TweenInfo.new(1 / animation.fps, Enum.EasingStyle.Linear), {
		CFrame = CFrame.new(BlockingDummy.HumanoidRootPart.CFrame.X, 0, BlockingDummy.HumanoidRootPart.CFrame.Z) * keyframeCFrame,
		FieldOfView = math.deg(animation.fov_frames[keyframeID])
	}):Play()
	
	RunService.Heartbeat:Wait()
end
1 Like

Oh thanks! I’ll update the plugin to use RunService.Heartbeat:Wait() then

1 Like

Hello! Back here again. Would it be possible to update the plugin so you’re able to play camera animations regardless of the character’s current rotation? I have been trying to make my own fixes but to no avail. My use case is as follows:

Currently I’m making a match intro system for a fighting game. Each player has an intro animation which includes a camera animation. However, their rotations are such that the players are facing one another. Since the camera animation only works properly if the player has a rotation of 0,0,0, the animation looks off.

Thanks in advance!

For example, here’s how my camera animation from earlier looks if you orient the character 180 degrees to face backwards.

https://i.gyazo.com/fc4889e180ba5420fba5b2c13d33eb74.mp4

Hello again, currently I can’t provide a potential solution as I’m busy with other matters. If I find the time, I’ll get back to see if I can try solving this potentially, if you haven’t already!

Also, I know a lot of people are wary and don’t like to use AI for coding, but have you tried using an AI model like ChatGPT or Claude to potentially get a solution? If not, then perhaps you should try it at least, if you’re fine with using AI, as I know some developers prefer not to.