Generating Seamless Flag Wave Animation from Base Pose
(7-Bone Setup) Script Blender
Creating natural-looking flag wave animations in Blender often requires manual keyframing across multiple bones, which can be time-consuming to loop perfectly.
This Python script solves that problem by generating a continuous, looping flag wave animation automatically. It works by taking the initial pose of a 7-bone armature at Frame 1 as a baseline profile, sampling that spatial wave profile across the timeline, and applying it smoothly to subsequent keyframes (Frames 25, 50, 75, and 100).
Key Features & How It Works:
Base Pose Sampling: Reads the current Y Location of all 7 bones at Frame 1 to create the initial wave shape.
Traveling Profile Algorithm: Shifts the baseline wave profile along the bone chain across the timeline to simulate wave movement.
Perfect Loop Guarantee: Forces Frame 100 to match Frame 1 exactly, ensuring a 100% seamless looping animation (Frames 1–100).
Automatic Bezier Interpolation: Sets keyframe handles to
BEZIERwithAUTOhandles to provide smooth interpolation without manual graph editing.
Kode Script (Python - Blender API)
import bpy
import math
# ============================================================
# 7-BONE FLAG WAVE FROM FRAME-1 POSE
#
# Armature : Armature.7
# Bones : Bone.1 - Bone.7
#
# Frame 1 = Reads actual base pose
# Frame 25 = Wave shift
# Frame 50 = Wave shift
# Frame 75 = Wave shift
# Frame 100 = EXACT Frame 1 (Perfect Loop)
#
# Affects Y Location only
# ============================================================
ARMATURE_NAME = "Armature.7"
BONE_NAMES = [
"Bone.1",
"Bone.2",
"Bone.3",
"Bone.4",
"Bone.5",
"Bone.6",
"Bone.7",
]
KEY_FRAMES = [1, 25, 50, 75, 100]
START_FRAME = 1
END_FRAME = 100
# ============================================================
# GET ARMATURE
# ============================================================
arm = bpy.data.objects.get(ARMATURE_NAME)
if arm is None:
raise Exception(f"Armature '{ARMATURE_NAME}' not found.")
# ============================================================
# READ FRAME 1 BASE POSE
# ============================================================
bpy.context.scene.frame_set(1)
BASE_Y = []
print("")
print("========================================")
print("FRAME 1 SOURCE POSE")
print("========================================")
for bone_name in BONE_NAMES:
bone = arm.pose.bones.get(bone_name)
if bone is None:
raise Exception(f"Bone '{bone_name}' not found.")
y = bone.location.y
BASE_Y.append(y)
print(f"{bone_name:7s} = {y:+.5f}")
# ============================================================
# PROFILE SAMPLER
#
# Reads 7-bone profile cyclically.
# Can sample positions between two bones.
# ============================================================
def sample_profile(values, position):
count = len(values)
position = position % count
i0 = int(math.floor(position))
i1 = (i0 + 1) % count
frac = position - i0
return values[i0] * (1.0 - frac) + values[i1] * frac
# ============================================================
# DELETE OLD Y ANIMATION
#
# Only targets Bone.1 - Bone.7 Y Location.
# ============================================================
if arm.animation_data and arm.animation_data.action:
action = arm.animation_data.action
for fc in list(action.fcurves):
if fc.array_index != 1:
continue
for bone_name in BONE_NAMES:
target = f'pose.bones["{bone_name}"].location'
if fc.data_path == target:
action.fcurves.remove(fc)
break
# ============================================================
# CREATE WAVE
# ============================================================
BONE_COUNT = len(BONE_NAMES)
for frame in KEY_FRAMES:
bpy.context.scene.frame_set(frame)
# Normalized timeline: frame 1 = 0, frame 100 = 1
t = (frame - START_FRAME) / (END_FRAME - START_FRAME)
# Profile travels one complete cycle
shift = t * BONE_COUNT
for bone_index, bone_name in enumerate(BONE_NAMES):
bone = arm.pose.bones.get(bone_name)
if bone is None:
continue
# Traveling Profile: "-" moves wave from Bone.1 to Bone.7
# Change "-" to "+" for reverse direction
source_position = bone_index - shift
y = sample_profile(BASE_Y, source_position)
# APPLY KEYFRAME
bone.location.y = y
bone.keyframe_insert(data_path="location", index=1, frame=frame)
# ============================================================
# FORCE FRAME 100 = EXACT FRAME 1
# ============================================================
bpy.context.scene.frame_set(100)
for bone_name, original_y in zip(BONE_NAMES, BASE_Y):
bone = arm.pose.bones.get(bone_name)
if bone is None:
continue
bone.location.y = original_y
bone.keyframe_insert(data_path="location", index=1, frame=100)
# ============================================================
# SMOOTH TEMPORAL INTERPOLATION
# ============================================================
if arm.animation_data and arm.animation_data.action:
action = arm.animation_data.action
for fc in action.fcurves:
if fc.array_index != 1:
continue
valid = False
for bone_name in BONE_NAMES:
if fc.data_path == f'pose.bones["{bone_name}"].location':
valid = True
break
if not valid:
continue
for kp in fc.keyframe_points:
kp.interpolation = 'BEZIER'
kp.handle_left_type = 'AUTO'
kp.handle_right_type = 'AUTO'
# ============================================================
# TIMELINE SETUP
# ============================================================
bpy.context.scene.frame_start = 1
bpy.context.scene.frame_end = 100
bpy.context.scene.frame_set(1)
print("")
print("========================================")
print("7-BONE FLAG WAVE CREATED")
print("========================================")
print("Frame 1 = source pose")
print("Frame 25 = traveling wave")
print("Frame 50 = traveling wave")
print("Frame 75 = traveling wave")
print("Frame 100 = exact Frame 1")
print("========================================")
0 Komentar