import bpy
import math
# ============================================================
# 4-BONE TRAVELING Y WAVE
#
# Mix konsep:
# A = setiap bone naik/turun
# B = wave merambat sepanjang deretan bone
#
# ONLY LOCATION Y
#
# Frame 1 = current Y pose
# Frame 201 = exact frame 1
#
# Playback / render:
# 1 - 200
# ============================================================
ARMATURE_NAME = "Armature.003"
START_FRAME = 1
PERIOD = 200
LOOP_FRAME = START_FRAME + PERIOD
# ============================================================
# BONE ORDER SECARA SPASIAL
#
# Sumber/ujung kanan -> menuju ujung lainnya.
#
# Kalau nanti arah rambat terbalik,
# cukup reverse list ini atau ubah WAVE_DIRECTION.
# ============================================================
BONES = [
"Bone.005",
"Bone.004",
"Bone.002",
"Bone001",
]
# ============================================================
# CONTROLS
# ============================================================
# Besar displacement wave dari bentuk awal.
AMPLITUDE = 1.45
# Berapa banyak gelombang sepanjang 4 bone.
#
# 1.0 = satu cycle penuh dari bone pertama ke terakhir.
# 0.75 = wave lebih panjang / elegan.
# 1.25 = wave lebih rapat.
SPATIAL_CYCLES = 0.85
# +1 / -1 membalik arah rambat.
WAVE_DIRECTION = 1.0
# ============================================================
# SPARSE SAMPLE FRAMES
#
# Bukan tiap frame.
# Masih gampang diedit manual.
# ============================================================
KEY_FRAMES = [
1,
13,
26,
38,
51,
63,
76,
88,
101,
113,
126,
138,
151,
163,
176,
188,
201,
]
# ============================================================
# GET ARMATURE
# ============================================================
arm = bpy.data.objects.get(ARMATURE_NAME)
if arm is None:
raise Exception(
f"Armature '{ARMATURE_NAME}' tidak ditemukan."
)
if arm.type != 'ARMATURE':
raise Exception(
f"'{ARMATURE_NAME}' bukan Armature."
)
scene = bpy.context.scene
# ============================================================
# FRAME 1 = BASE SHAPE
# ============================================================
scene.frame_set(START_FRAME)
BASE_Y = {}
for name in BONES:
pb = arm.pose.bones.get(name)
if pb is None:
raise Exception(
f"Bone '{name}' tidak ditemukan."
)
BASE_Y[name] = pb.location.y
# ============================================================
# BACKUP ACTION
# ============================================================
if arm.animation_data and arm.animation_data.action:
old = arm.animation_data.action
backup = old.copy()
backup.name = old.name + "_BEFORE_TRAVEL_WAVE"
backup.use_fake_user = True
# ============================================================
# CLEAR OLD ANIMATION
# ============================================================
if arm.animation_data:
arm.animation_data_clear()
# ============================================================
# SPATIAL POSITION
#
# 4 bones:
#
# 005 = 0.00
# 004 = 0.33
# 002 = 0.67
# 001 = 1.00
# ============================================================
N = len(BONES)
X = {}
for i, name in enumerate(BONES):
X[name] = (
i / (N - 1)
)
# ============================================================
# WAVE FUNCTION
#
# Traveling wave:
#
# sin(kx - wt)
# ============================================================
def wave(name, time_phase):
x = X[name]
spatial_phase = (
math.tau
* SPATIAL_CYCLES
* x
)
return math.sin(
spatial_phase
-
WAVE_DIRECTION * time_phase
)
# ============================================================
# INITIAL WAVE VALUE
#
# Kita kurangi nilai ini dari seluruh animasi.
#
# Hasilnya:
# displacement pada frame 1 = 0.
# ============================================================
INITIAL_WAVE = {}
for name in BONES:
INITIAL_WAVE[name] = wave(
name,
0.0
)
# ============================================================
# GENERATE
# ============================================================
for frame in KEY_FRAMES:
progress = (
frame - START_FRAME
) / PERIOD
time_phase = (
math.tau * progress
)
for name in BONES:
pb = arm.pose.bones[name]
# ----------------------------------------------------
# displacement dari traveling wave
# ----------------------------------------------------
current_wave = wave(
name,
time_phase
)
wave_delta = (
current_wave
-
INITIAL_WAVE[name]
)
# ----------------------------------------------------
# final Y
# ----------------------------------------------------
y = (
BASE_Y[name]
+
wave_delta
* AMPLITUDE
)
# Exact start/end
if frame == START_FRAME or frame == LOOP_FRAME:
y = BASE_Y[name]
pb.location.y = y
pb.keyframe_insert(
data_path="location",
index=1,
frame=frame
)
# ============================================================
# SMOOTH INTERPOLATION
#
# AUTO handles lebih flowy.
#
# Tidak pakai AUTO_CLAMPED karena di sini sedikit curvature
# antar sample justru membantu rasa gelombang.
# ============================================================
if arm.animation_data and arm.animation_data.action:
action = arm.animation_data.action
action.name = "TRAVELING_Y_WAVE"
for fc in action.fcurves:
for kp in fc.keyframe_points:
kp.interpolation = 'BEZIER'
kp.handle_left_type = 'AUTO'
kp.handle_right_type = 'AUTO'
# ============================================================
# TIMELINE
# ============================================================
scene.frame_start = 1
scene.frame_end = 200
scene.frame_set(1)
# ============================================================
# REPORT
# ============================================================
print("")
print("=" * 70)
print("TRAVELING Y WAVE CREATED")
print("=" * 70)
print("")
print("BASE Y:")
for name in BONES:
print(
f"{name:10s}",
round(BASE_Y[name], 4),
"| x =",
round(X[name], 3)
)
print("")
print("Amplitude :", AMPLITUDE)
print("Spatial cycles :", SPATIAL_CYCLES)
print("Period :", PERIOD)
print("Loop reference :", LOOP_FRAME)
print("")
print("Frame 1 = original Y")
print("Frame 201 = original Y exact")
print("")
print("Only Y Location animated.")
print("=" * 70)
-----
evently
---
import bpy
import math
# ============================================================
# TRAVELING Y WAVE - EVENT BASED SPARSE KEYS
#
# ONLY LOCATION Y
#
# Frame 1 = current Y
# Frame 201 = current Y lagi
#
# Setiap bone hanya dapat key pada:
# START / MAX / CENTER / MIN / CENTER / LOOP
# ============================================================
ARMATURE_NAME = "Armature.005"
START_FRAME = 1
PERIOD = 200
LOOP_FRAME = START_FRAME + PERIOD
AMPLITUDE = 2.10
BONES = [
"Bone.005",
"Bone.004",
"Bone.002",
"Bone001",
]
# ============================================================
# SPATIAL PHASE
#
# Ini yang bikin wave "jalan" dari bone ke bone.
# ============================================================
SPATIAL_PHASE = {
"Bone.005": math.radians(0),
"Bone.004": math.radians(60),
"Bone.002": math.radians(120),
"Bone001": math.radians(180),
}
# ============================================================
# GET ARMATURE
# ============================================================
arm = bpy.data.objects.get(ARMATURE_NAME)
if arm is None:
raise Exception(
f"Armature '{ARMATURE_NAME}' tidak ditemukan."
)
scene = bpy.context.scene
scene.frame_set(START_FRAME)
# ============================================================
# READ CURRENT Y
# ============================================================
START_Y = {}
for name in BONES:
pb = arm.pose.bones.get(name)
if pb is None:
raise Exception(
f"Bone '{name}' tidak ditemukan."
)
START_Y[name] = pb.location.y
# ============================================================
# BACKUP
# ============================================================
if arm.animation_data and arm.animation_data.action:
old = arm.animation_data.action
backup = old.copy()
backup.name = old.name + "_BEFORE_SPARSE_EVENT"
backup.use_fake_user = True
# ============================================================
# CLEAR OLD ANIMATION
# ============================================================
if arm.animation_data:
arm.animation_data_clear()
# ============================================================
# TIME -> FRAME
#
# satu cycle 0..2π = 200 frame
# ============================================================
def angle_to_frame(angle):
angle %= math.tau
return (
START_FRAME
+
(angle / math.tau)
* PERIOD
)
# ============================================================
# CALCULATE PHASE FROM START POSITION
#
# Kita ingin:
#
# Y(0) = START_Y
#
# dengan:
#
# Y = A sin(theta + phase)
#
# lalu kita tambahkan spatial phase.
# ============================================================
PHASE = {}
for name in BONES:
normalized = max(
-1.0,
min(
1.0,
START_Y[name] / AMPLITUDE
)
)
base_phase = math.asin(normalized)
PHASE[name] = (
base_phase
+ SPATIAL_PHASE[name]
) % math.tau
# ============================================================
# EVENT ANGLES
#
# posisi sinus:
#
# +MAX = π/2
# CENTER = π
# -MAX = 3π/2
# CENTER = 2π
#
# lalu siklus lanjut.
# ============================================================
TARGETS = [
("MAX", math.pi / 2, AMPLITUDE),
("CENTER", math.pi, 0.0),
("MIN", 3 * math.pi / 2, -AMPLITUDE),
("CENTER", 2 * math.pi, 0.0),
("MAX", 5 * math.pi / 2, AMPLITUDE),
]
# ============================================================
# BUILD SPARSE KEYS
# ============================================================
ALL_KEYS = {}
for name in BONES:
phase = PHASE[name]
pb = arm.pose.bones[name]
keys = []
# --------------------------------------------------------
# START
# --------------------------------------------------------
keys.append(
(
float(START_FRAME),
START_Y[name],
"START"
)
)
# --------------------------------------------------------
# FIND NEXT EVENTS
# --------------------------------------------------------
for label, target_angle, target_y in TARGETS:
theta = (
target_angle
- phase
) % math.tau
# kalau persis di start, skip
if theta < 1e-5:
continue
frame = angle_to_frame(
theta
)
# hanya di dalam cycle
if (
frame > START_FRAME
and frame < LOOP_FRAME
):
keys.append(
(
frame,
target_y,
label
)
)
# --------------------------------------------------------
# LOOP END
# --------------------------------------------------------
keys.append(
(
float(LOOP_FRAME),
START_Y[name],
"LOOP"
)
)
# --------------------------------------------------------
# SORT
# --------------------------------------------------------
keys.sort(
key=lambda x: x[0]
)
# ========================================================
# INSERT KEYS
# ========================================================
for frame, y, label in keys:
pb.location.y = y
pb.keyframe_insert(
data_path="location",
index=1,
frame=frame
)
ALL_KEYS[name] = keys
# ============================================================
# BEZIER SMOOTH
# ============================================================
if arm.animation_data and arm.animation_data.action:
action = arm.animation_data.action
action.name = "TRAVELING_Y_WAVE_SPARSE"
for fc in action.fcurves:
for kp in fc.keyframe_points:
kp.interpolation = 'BEZIER'
kp.handle_left_type = 'AUTO_CLAMPED'
kp.handle_right_type = 'AUTO_CLAMPED'
# ============================================================
# TIMELINE
# ============================================================
scene.frame_start = START_FRAME
scene.frame_end = LOOP_FRAME - 1
scene.frame_set(START_FRAME)
# ============================================================
# PRINT RESULT
# ============================================================
print("")
print("=" * 75)
print("TRAVELING Y WAVE - SPARSE EVENT KEYS")
print("=" * 75)
for name in BONES:
print("")
print(name)
for frame, y, label in ALL_KEYS[name]:
print(
f" {label:7s}"
f" frame {frame:7.2f}"
f" Y = {y: .4f}"
)
print("")
print("Frame 201 = Frame 1")
print("=" * 75)

0 Komentar