Files
effectcraft/crates/engine/src/cmd/blender_scene.py
T

205 lines
9.5 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.
# EffectCraft camera tracker → Blender, original code under MIT OR Apache-2.0.
# Run in Blender's Scripting workspace (Open, Run Script), or:
# blender --background --python "Tracked Scene.py"
# Creates a NEW scene; existing scenes and objects are preserved. Save the .blend yourself.
# Public API types: bpy.types.Camera, bpy.types.ImageUser
import json
import math
import os
import bpy
from mathutils import Matrix
DATA = json.loads(__EFFECTCRAFT_DATA__)
def _curves(owner):
"""Blender 4.0–4.3 legacy actions and 4.4+/5.x layered actions."""
anim = owner.animation_data
if anim is None or anim.action is None:
return
action = anim.action
if getattr(action, "is_action_layered", False):
for layer in action.layers:
for strip in layer.strips:
if strip.type == 'KEYFRAME':
bag = strip.channelbag(anim.action_slot)
if bag is not None:
yield from bag.fcurves
else:
yield from action.fcurves
def _interpolation(owner):
for curve in _curves(owner):
# Footage indices and discrete switches must never interpolate between frames.
stepped = ("frame_offset" in curve.data_path or "use_dof" in curve.data_path
or "aperture_blades" in curve.data_path)
for key in curve.keyframe_points:
key.interpolation = 'CONSTANT' if stepped else 'LINEAR'
def _media_path():
original = DATA["media"]["path"]
candidates = [original]
# When sharing, place the video alongside the script or the .blend and run again.
script_path = globals().get("__file__", "")
if script_path:
candidates.append(os.path.join(os.path.dirname(os.path.abspath(script_path)),
os.path.basename(original)))
if bpy.data.filepath:
candidates.append(os.path.join(os.path.dirname(bpy.data.filepath),
os.path.basename(original)))
return next((p for p in candidates if os.path.isfile(p)), None)
def import_scene():
if bpy.app.version < (4, 0, 0):
raise RuntimeError("EffectCraft scene import requires Blender 4.0 or newer")
scene = bpy.data.scenes.new(DATA["name"])
scene.frame_start = DATA["startFrame"]
scene.frame_end = DATA["endFrame"]
fps = DATA["fpsNumerator"] / DATA["fpsDenominator"]
scene.render.fps = max(1, round(fps))
scene.render.fps_base = scene.render.fps / fps
scene.render.resolution_x, scene.render.resolution_y = map(int, DATA["resolution"])
scene.render.resolution_percentage = 100
# The tracker assumes square pixels, regardless of the source's interpretation metadata.
scene.render.pixel_aspect_x = scene.render.pixel_aspect_y = 1.0
scene.render.film_transparent = True
scene.render.motion_blur_shutter = max(0.0, min(2.0, DATA["shutter"]))
scene.unit_settings.system = 'METRIC'
scene.unit_settings.scale_length = 1.0
scene["effectcraft_unit_scale"] = DATA["unitScale"]
scene["effectcraft_scale_is_arbitrary"] = True
scene["effectcraft_shutter_phase_degrees"] = DATA["shutterPhase"]
collection = bpy.data.collections.new("EffectCraft Tracking")
scene.collection.children.link(collection)
camera_data = bpy.data.cameras.new("3D Tracker Camera")
camera = bpy.data.objects.new("3D Tracker Camera", camera_data)
collection.objects.link(camera)
scene.camera = camera
camera.rotation_mode = 'QUATERNION'
camera_data.type = 'PERSP'
camera_data.sensor_fit = 'HORIZONTAL'
camera_data.sensor_width = DATA["sensorWidth"]
camera_data.sensor_height = (DATA["sensorWidth"] * DATA["resolution"][1]
/ DATA["resolution"][0])
camera_data.show_background_images = True
camera_data.show_limits = True
extent = max([DATA["groundSize"], 1.0]
+ [math.dist(p["location"], f["location"])
for p in DATA["points"][::max(1, len(DATA["points"]) // 100)]
for f in DATA["frames"][::max(1, len(DATA["frames"]) // 10)]])
camera_data.clip_start = max(0.00001, min(0.1, extent / 10000.0))
camera_data.clip_end = max(1000.0, extent * 10.0)
previous = None
for sample in DATA["frames"]:
frame = sample["frame"]
camera.location = sample["location"]
# Exported right/up/back vectors are COLUMNS of Blender's local → world basis.
quaternion = Matrix(sample["rotation"]).transposed().to_quaternion()
if previous is not None and previous.dot(quaternion) < 0:
quaternion.negate()
camera.rotation_quaternion = quaternion
previous = quaternion.copy()
camera.keyframe_insert(data_path="location", frame=frame)
camera.keyframe_insert(data_path="rotation_quaternion", frame=frame)
camera_data.lens = sample["lens"]
camera_data.keyframe_insert(data_path="lens", frame=frame)
camera_data.dof.use_dof = sample["dof"]
camera_data.dof.focus_distance = sample["focus"]
camera_data.dof.aperture_fstop = sample["fstop"]
camera_data.dof.aperture_blades = sample["blades"]
camera_data.dof.aperture_rotation = (sample["irisRotation"] + math.pi) % math.tau - math.pi
camera_data.dof.aperture_ratio = sample["irisRatio"]
for prop in ("use_dof", "focus_distance", "aperture_fstop", "aperture_blades",
"aperture_rotation", "aperture_ratio"):
camera_data.keyframe_insert(data_path="dof." + prop, frame=frame)
path = _media_path()
if path is not None:
try:
image = bpy.data.images.load(path, check_existing=False)
image.source = DATA["media"]["kind"]
background = camera_data.background_images.new()
background.source = 'IMAGE'
background.image = image
background.alpha = 1.0
background.display_depth = 'BACK'
background.frame_method = 'STRETCH'
user = background.image_user
user.use_auto_refresh = True
user.use_cyclic = False
user.frame_start = DATA["startFrame"]
user.frame_duration = max(1, min(1048574, image.frame_duration))
if image.source == 'MOVIE':
for sample in DATA["frames"]:
# Blender uses 1-based image frames; sourceFrame is 0-based.
user.frame_offset = sample["sourceFrame"] - (sample["frame"] - user.frame_start)
camera_data.keyframe_insert(
data_path="background_images[0].image_user.frame_offset",
frame=sample["frame"])
except (RuntimeError, ValueError, OSError) as error:
DATA["warnings"].append("Blender could not load the background video: " + str(error))
else:
DATA["warnings"].append("Video not found: " + DATA["media"]["path"]
+ ". Place it beside this script or relink the camera background.")
if DATA["points"]:
mesh = bpy.data.meshes.new("Solved Track Points")
mesh.from_pydata([p["location"] for p in DATA["points"]], [], [])
mesh.update()
cloud = bpy.data.objects.new("Solved Track Points", mesh)
collection.objects.link(cloud)
cloud.show_in_front = True
cloud.hide_render = True
for name, kind, key in (("track_id", 'INT', "id"), ("reprojection_error", 'FLOAT', "error"),
("first_frame", 'INT', "first"), ("last_frame", 'INT', "last")):
attr = mesh.attributes.new(name=name, type=kind, domain='POINT')
for dest, point in zip(attr.data, DATA["points"]):
# Track IDs are u32 in EffectCraft; keep the full IDs in the JSON text too.
dest.value = point[key] if kind == 'FLOAT' else min(2147483647, point[key])
if DATA["ground"]:
size = DATA["groundSize"]
mesh = bpy.data.meshes.new("Solved Ground Plane")
mesh.from_pydata([(-size, -size, 0), (size, -size, 0),
(size, size, 0), (-size, size, 0)], [], [(0, 1, 2, 3)])
mesh.update()
ground = bpy.data.objects.new("Solved Ground Plane", mesh)
collection.objects.link(ground)
ground.display_type = 'WIRE'
ground.hide_render = True
for marker in DATA["markers"]:
scene.timeline_markers.new(marker["name"], frame=marker["frame"])
_interpolation(camera)
_interpolation(camera_data)
metadata = bpy.data.texts.new("EffectCraft Tracking Metadata.json")
metadata.write(json.dumps(DATA, ensure_ascii=False, indent=2))
scene["effectcraft_metadata_text"] = metadata.name
camera_data["effectcraft_average_error_pixels"] = DATA["solve"]["averageError"]
camera_data["effectcraft_solve_method"] = DATA["solve"]["methodUsed"]
camera_data["effectcraft_focus_source"] = "See warnings in " + metadata.name
distortion = DATA["solve"].get("distortion")
if distortion:
for key in ("k1", "k2", "radius"):
camera_data["effectcraft_distortion_" + key] = distortion[key]
if bpy.context.window is not None:
bpy.context.window.scene = scene
for area in bpy.context.window.screen.areas:
if area.type == 'VIEW_3D':
area.spaces.active.region_3d.view_perspective = 'CAMERA'
scene.frame_set(scene.frame_start)
for warning in DATA["warnings"]:
print("EffectCraft: " + warning)
print("EffectCraft: imported", len(DATA["frames"]), "baked camera frames into", scene.name)
return scene
if __name__ == "__main__":
EFFECTCRAFT_SCENE = import_scene()