| name | blender-impl-animation |
| description | Use when implementing AEC-specific animations -- construction sequences, camera walkthroughs, solar studies, or phasing visualizations in Blender. Prevents the common mistake of keyframing visibility instead of using proper collection visibility for construction phases. Covers NLA workflow orchestration, batch keyframe operations, and time-based visualizations. Keywords: construction animation, camera walkthrough, solar study, phasing, NLA, keyframe, construction sequence, visibility animation, AEC visualization, animate building, show build sequence, camera fly-through.
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires Blender 3.x/4.x/5.x with Python. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
blender-impl-animation
Quick Reference
Critical Warnings
ALWAYS set the property value BEFORE calling keyframe_insert() — keyframes record the current value at call time.
ALWAYS call fcurve.update() after manually adding or modifying keyframe points via keyframe_points.add() or direct .co assignment.
ALWAYS create animation_data before accessing .action or .nla_tracks — call obj.animation_data_create() if obj.animation_data is None.
NEVER use bone.layers or pose.bone_groups in Blender 4.0+ — removed. Use BoneCollection API.
NEVER insert keyframes on objects not linked to any collection — they will exist in bpy.data but have no scene presence.
ALWAYS call scene.frame_set(frame) when you need depsgraph evaluation at that frame (e.g., for visibility keyframes, constraint evaluation). Direct scene.frame_current = frame does NOT trigger depsgraph update.
ALWAYS use bpy.data.objects.get("Name") with None-check instead of direct key access bpy.data.objects["Name"] — prevents KeyError crashes.
Decision Tree: AEC Animation Workflow
What type of AEC animation do you need?
├─ Construction sequence (4D BIM)?
│ ├─ Objects appear/disappear over time?
│ │ └─ WORKFLOW A: Visibility Phasing
│ └─ Objects move into position (crane sim)?
│ └─ WORKFLOW B: Transform Sequencing
├─ Camera walkthrough / flythrough?
│ ├─ Fixed path (predefined route)?
│ │ └─ WORKFLOW C: Path-Based Camera
│ └─ Point-to-point with smooth transitions?
│ └─ WORKFLOW D: Keyframed Camera
├─ Solar / daylight study?
│ └─ WORKFLOW E: Sun Path Animation
├─ Phasing visualization (design options)?
│ └─ WORKFLOW F: Collection Phasing
└─ Multiple animation clips combined?
└─ WORKFLOW G: NLA Orchestration
Version Matrix: Implementation Features
| Feature | Blender 3.x | Blender 4.0+ | Blender 5.x |
|---|
| Visibility/NLA/Path/Constraints | Available | Available | Available |
BoneCollection | Not available | NEW | Available |
bgl for overlays | Available | Deprecated | REMOVED |
Workflow A: Visibility Phasing (Construction Sequence)
Use when objects must appear/disappear at specific frames to simulate construction phases.
Step 1: Organize objects into phase collections
import bpy
phase_names = ["Phase_Foundation", "Phase_Structure", "Phase_Envelope", "Phase_Interior"]
phases = {}
for name in phase_names:
col = bpy.data.collections.new(name)
bpy.context.scene.collection.children.link(col)
phases[name] = col
Step 2: Assign objects to phases
def assign_to_phase(obj, phase_collection):
for col in obj.users_collection:
col.objects.unlink(obj)
phase_collection.objects.link(obj)
Step 3: Keyframe visibility per phase
def animate_phase_visibility(obj, appear_frame, disappear_frame=None):
"""Keyframe object visibility for construction phasing."""
obj.hide_viewport = True
obj.hide_render = True
obj.keyframe_insert(data_path="hide_viewport", frame=appear_frame - 1)
obj.keyframe_insert(data_path="hide_render", frame=appear_frame - 1)
obj.hide_viewport = False
obj.hide_render = False
obj.keyframe_insert(data_path="hide_viewport", frame=appear_frame)
obj.keyframe_insert(data_path="hide_render", frame=appear_frame)
if disappear_frame is not None:
obj.hide_viewport = False
obj.hide_render = False
obj.keyframe_insert(data_path="hide_viewport", frame=disappear_frame - 1)
obj.keyframe_insert(data_path="hide_render", frame=disappear_frame - 1)
obj.hide_viewport = True
obj.hide_render = True
obj.keyframe_insert(data_path="hide_viewport", frame=disappear_frame)
obj.keyframe_insert(data_path="hide_render", frame=disappear_frame)
if obj.animation_data and obj.animation_data.action:
for fc in obj.animation_data.action.fcurves:
if fc.data_path in ("hide_viewport", "hide_render"):
for kp in fc.keyframe_points:
kp.interpolation = 'CONSTANT'
Step 4: Apply to phase schedule
schedule = {
"Phase_Foundation": (1, None),
"Phase_Structure": (60, None),
"Phase_Envelope": (120, None),
"Phase_Interior": (180, None),
}
for phase_name, (start, end) in schedule.items():
col = bpy.data.collections.get(phase_name)
if col is None:
continue
for obj in col.objects:
animate_phase_visibility(obj, start, end)
bpy.context.scene.frame_start = 1
bpy.context.scene.frame_end = 250
Workflow B: Transform Sequencing (Crane/Assembly)
Use when objects must animate from off-screen or staging positions to final positions.
import bpy
def animate_assembly(obj, staging_location, final_location,
start_frame, end_frame, interpolation='BEZIER'):
"""Animate object from staging to final position."""
obj.location = staging_location
obj.keyframe_insert(data_path="location", frame=start_frame)
obj.location = final_location
obj.keyframe_insert(data_path="location", frame=end_frame)
if obj.animation_data and obj.animation_data.action:
for fc in obj.animation_data.action.fcurves:
if fc.data_path == "location":
for kp in fc.keyframe_points:
kp.interpolation = interpolation
beam = bpy.data.objects.get("SteelBeam_01")
if beam:
animate_assembly(
beam,
staging_location=(beam.location.x, beam.location.y, beam.location.z + 20),
final_location=(beam.location.x, beam.location.y, beam.location.z),
start_frame=60,
end_frame=90
)
Workflow C: Path-Based Camera Walkthrough
Use for architectural walkthroughs following a predefined curve path.
Step 1: Create camera and path
import bpy
from mathutils import Vector
cam_data = bpy.data.cameras.new("WalkthroughCam")
cam_data.lens = 24
cam_obj = bpy.data.objects.new("WalkthroughCam", cam_data)
bpy.context.scene.collection.objects.link(cam_obj)
curve_data = bpy.data.curves.new("CameraPath", type='CURVE')
curve_data.dimensions = '3D'
spline = curve_data.splines.new('NURBS')
points = [(0, 0, 1.7), (5, 0, 1.7), (5, 10, 1.7), (10, 10, 1.7)]
spline.points.add(count=len(points) - 1)
for i, (x, y, z) in enumerate(points):
spline.points[i].co = (x, y, z, 1.0)
spline.use_endpoint_u = True
spline.order_u = 3
path_obj = bpy.data.objects.new("CameraPath", curve_data)
bpy.context.scene.collection.objects.link(path_obj)
Step 2: Add Follow Path constraint
follow = cam_obj.constraints.new('FOLLOW_PATH')
follow.target = path_obj
follow.use_curve_follow = True
follow.forward_axis = 'FORWARD_Y'
follow.up_axis = 'UP_Z'
follow.offset = 0.0
follow.keyframe_insert(data_path="offset", frame=1)
follow.offset = -100.0
follow.keyframe_insert(data_path="offset", frame=250)
cam_obj_action = cam_obj.animation_data.action
for fc in cam_obj_action.fcurves:
if "offset" in fc.data_path:
for kp in fc.keyframe_points:
kp.interpolation = 'LINEAR'
Step 3: Optional: Track To constraint for look-at target
target = bpy.data.objects.new("CamTarget", None)
bpy.context.scene.collection.objects.link(target)
target.location = (5, 5, 3)
track = cam_obj.constraints.new('TRACK_TO')
track.target = target
track.track_axis = 'TRACK_NEGATIVE_Z'
track.up_axis = 'UP_Y'
track.influence = 0.5
Workflow D: Keyframed Camera (Point-to-Point)
Use for controlled camera transitions between specific viewpoints.
import bpy
from math import radians
def keyframe_camera_viewpoint(cam_obj, location, rotation_euler,
frame, interpolation='BEZIER'):
"""Set camera to a viewpoint and keyframe it."""
cam_obj.location = location
cam_obj.rotation_euler = rotation_euler
cam_obj.keyframe_insert(data_path="location", frame=frame)
cam_obj.keyframe_insert(data_path="rotation_euler", frame=frame)
if cam_obj.animation_data and cam_obj.animation_data.action:
for fc in cam_obj.animation_data.action.fcurves:
for kp in fc.keyframe_points:
if kp.co[0] == frame:
kp.interpolation = interpolation
viewpoints = [
((20, -20, 10), (radians(60), 0, radians(135)), 1),
((5, -15, 5), (radians(70), 0, radians(160)), 60),
((-5, 0, 2), (radians(85), 0, radians(200)), 120),
((0, 10, 15), (radians(30), 0, radians(250)), 180),
]
cam = bpy.data.objects.get("Camera")
if cam:
for loc, rot, frame in viewpoints:
keyframe_camera_viewpoint(cam, loc, rot, frame)
Workflow E: Sun Path Animation (Solar Study)
Use for daylight/shadow analysis across time-of-day or seasons.
import bpy
from math import radians, sin, cos
def animate_sun_arc(sun_obj, latitude, start_hour, end_hour,
frame_start, frame_end):
"""Animate sun lamp along simplified arc. See references/examples.md for full version."""
total_frames = frame_end - frame_start
total_hours = end_hour - start_hour
for i in range(total_frames + 1):
frame = frame_start + i
t = i / total_frames
hour = start_hour + t * total_hours
hour_angle = (hour - 12) * 15
altitude = max(0, min(90, 90 - abs(latitude) + 23.5 * sin(radians(hour_angle))))
azimuth = hour_angle
distance = 100
x = distance * cos(radians(altitude)) * sin(radians(azimuth))
y = distance * cos(radians(altitude)) * cos(radians(azimuth))
z = distance * sin(radians(altitude))
sun_obj.location = (x, y, z)
sun_obj.keyframe_insert(data_path="location", frame=frame)
if sun_obj.animation_data and sun_obj.animation_data.action:
for fc in sun_obj.animation_data.action.fcurves:
for kp in fc.keyframe_points:
kp.interpolation = 'LINEAR'
sun_data = bpy.data.lights.new("SunStudy", type='SUN')
sun_data.energy = 5.0
sun_obj = bpy.data.objects.new("SunStudy", sun_data)
bpy.context.scene.collection.objects.link(sun_obj)
track = sun_obj.constraints.new('TRACK_TO')
target = bpy.data.objects.new("SunTarget", None)
bpy.context.scene.collection.objects.link(target)
target.location = (0, 0, 0)
track.target = target
track.track_axis = 'TRACK_NEGATIVE_Z'
track.up_axis = 'UP_Y'
animate_sun_arc(sun_obj, latitude=52.0, start_hour=6, end_hour=18,
frame_start=1, frame_end=120)
Workflow F: Collection Phasing (Design Options)
Use when toggling entire collections to show design alternatives or construction phases.
import bpy
def animate_collection_visibility(collection, visible_start, visible_end=None):
"""Animate collection visibility using exclude flag via view layer.
NOTE: Collection.hide_viewport can be keyframed directly.
"""
collection.hide_viewport = True
collection.keyframe_insert(data_path="hide_viewport", frame=1)
collection.hide_viewport = False
collection.keyframe_insert(data_path="hide_viewport", frame=visible_start)
if visible_end is not None:
collection.hide_viewport = False
collection.keyframe_insert(data_path="hide_viewport", frame=visible_end - 1)
collection.hide_viewport = True
collection.keyframe_insert(data_path="hide_viewport", frame=visible_end)
if collection.animation_data and collection.animation_data.action:
for fc in collection.animation_data.action.fcurves:
for kp in fc.keyframe_points:
kp.interpolation = 'CONSTANT'
phases = {
"Demolition": (1, 30),
"Foundation": (30, 90),
"Structure": (90, 150),
"Envelope": (150, 210),
"Finishes": (210, None),
}
for name, (start, end) in phases.items():
col = bpy.data.collections.get(name)
if col is not None:
animate_collection_visibility(col, start, end)
Workflow G: NLA Orchestration
Use when combining multiple animation actions into a sequenced timeline. See references/examples.md for full NLA example.
import bpy
def create_visibility_action(action_name):
"""Create a self-contained visibility action."""
action = bpy.data.actions.new(name=action_name)
fc = action.fcurves.new(data_path="hide_viewport")
fc.keyframe_points.add(count=2)
fc.keyframe_points[0].co = (0.0, 1.0)
fc.keyframe_points[1].co = (1.0, 0.0)
for kp in fc.keyframe_points:
kp.interpolation = 'CONSTANT'
fc.update()
return action
def sequence_on_nla(obj, actions_schedule):
"""Place actions on NLA tracks. actions_schedule: list of (action, start_frame, track_name)."""
anim_data = obj.animation_data if obj.animation_data else obj.animation_data_create()
for action, start_frame, track_name in actions_schedule:
track = anim_data.nla_tracks.new()
track.name = track_name
strip = track.strips.new(name=action.name, start=start_frame, action=action)
strip.blend_type = 'REPLACE'
strip.use_auto_blend = False
collection = bpy.data.collections.get("Structure")
if collection:
for i, obj in enumerate(collection.objects):
action = create_visibility_action(f"Appear_{obj.name}")
sequence_on_nla(obj, [(action, i * 10, f"Phase_{obj.name}")])
Batch Keyframe Operations
import bpy
def batch_keyframe_property(objects, data_path, frame, index=-1):
"""Insert keyframes for the current value on multiple objects."""
for obj in objects:
try:
obj.keyframe_insert(data_path=data_path, frame=frame, index=index)
except TypeError:
pass
def batch_set_interpolation(objects, interpolation='LINEAR'):
"""Set interpolation type on all keyframes of given objects."""
for obj in objects:
if obj.animation_data and obj.animation_data.action:
for fc in obj.animation_data.action.fcurves:
for kp in fc.keyframe_points:
kp.interpolation = interpolation
def batch_clear_animation(objects):
"""Remove all animation data from objects."""
for obj in objects:
if obj.animation_data:
obj.animation_data_clear()
Render Settings for Animation Output
import bpy
scene = bpy.context.scene
scene.frame_start = 1
scene.frame_end = 250
scene.render.filepath = "//render/construction_"
scene.render.image_settings.file_format = 'PNG'
scene.render.resolution_x = 1920
scene.render.resolution_y = 1080
scene.render.resolution_percentage = 100
scene.render.fps = 24
Reference Links