"""
タイトル: 球体と三角錐の追尾・逃避アニメーション
説明:
三角錐(A)が球体(B)を追いかけ、球体(B)は三角錐から逃げる。
球体は逃げる間に色が黄色になり、通常時は緑色。
三角錐は球体を追いかけながら移動し、方向ベクトルに応じて傾きます。
作成日: 2024年11月17日
バージョン: 2.0.0
"""
import bpy
import math
import random
params = {
"A_color_close": (1.0, 0.0, 0.0, 1.0),
"A_color_far": (0.0, 0.0, 1.0, 1.0),
"B_color_normal": (0.0, 1.0, 0.0, 1.0),
"B_color_escape": (1.0, 0.0, 0.0, 1.0),
"escape_distance": 2.5,
"safe_distance": 10.0,
"A_speed": 0.4,
"B_normal_speed": 0.25,
"B_boost_speed": 1.0,
"initial_position_A": (0, 0, 0),
"initial_position_B": (5, 5, 5),
"min_distance": 3.0,
"max_distance": 5.0,
"direction_change_interval": 12
}
state_flags = {
"B_is_escaping": False
}
def calculate_distance(obj1, obj2):
"""
2つのオブジェクト間の距離を計算します。
"""
return math.sqrt(
(obj1.location.x - obj2.location.x) ** 2 +
(obj1.location.y - obj2.location.y) ** 2 +
(obj1.location.z - obj2.location.z) ** 2
)
def calculate_rotation_from_direction(direction):
"""
ベクトル方向から回転角度を計算します(XY軸の回転を適用)。
Args:
direction (list): 移動方向ベクトル [x, y, z]。
Returns:
tuple: オイラー角 (rotation_x, rotation_y, rotation_z)。
"""
norm = math.sqrt(sum(d**2 for d in direction))
if norm == 0:
return (0.0, 0.0, 0.0)
dx, dy, dz = direction[0] / norm, direction[1] / norm, direction[2] / norm
rotation_x = math.atan2(dz, math.sqrt(dx**2 + dy**2))
rotation_y = -math.atan2(dx, math.sqrt(dy**2 + dz**2))
return (rotation_x, rotation_y, 3.1)
def move_A(chaser, target, frame):
"""
三角錐(A)の追尾動作と色変化を制御します。
"""
distance = calculate_distance(chaser, target)
if distance <= params["min_distance"]:
color_ratio = 0.0
elif distance >= params["max_distance"]:
color_ratio = 1.0
else:
color_ratio = (distance - params["min_distance"]) / (params["max_distance"] - params["min_distance"])
chaser.active_material.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = (
params["A_color_close"][0] * (1 - color_ratio) + params["A_color_far"][0] * color_ratio,
params["A_color_close"][1] * (1 - color_ratio) + params["A_color_far"][1] * color_ratio,
params["A_color_close"][2] * (1 - color_ratio) + params["A_color_far"][2] * color_ratio,
1.0
)
chaser.active_material.node_tree.nodes["Principled BSDF"].inputs["Base Color"].keyframe_insert(data_path="default_value", frame=frame)
direction = [
target.location.x - chaser.location.x,
target.location.y - chaser.location.y,
target.location.z - chaser.location.z
]
norm = math.sqrt(sum(d ** 2 for d in direction))
if norm > 0:
direction = [d / norm for d in direction]
chaser.location.x += direction[0] * params["A_speed"]
chaser.location.y += direction[1] * params["A_speed"]
chaser.location.z += direction[2] * params["A_speed"]
rotation = calculate_rotation_from_direction(direction)
chaser.rotation_euler = rotation
chaser.keyframe_insert(data_path="location", frame=frame)
chaser.keyframe_insert(data_path="rotation_euler", frame=frame)
def create_material(name, color):
"""
指定されたベースカラーのマテリアルを作成します。
"""
mat = bpy.data.materials.get(name)
if mat is None:
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
return mat
def move_B(obj, chaser, frame, escape_distance, safe_distance, direction, last_change_frame):
"""
球体(B)の移動と色変化を制御します。
"""
distance = calculate_distance(obj, chaser)
if state_flags["B_is_escaping"]:
if distance >= safe_distance:
state_flags["B_is_escaping"] = False
obj.active_material.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = params["B_color_normal"]
direction = [random.uniform(-1, 1), random.uniform(-1, 1), random.uniform(-1, 1)]
last_change_frame = frame
else:
escape_vector = [
obj.location.x - chaser.location.x,
obj.location.y - chaser.location.y,
obj.location.z - chaser.location.z
]
norm = math.sqrt(sum(d ** 2 for d in escape_vector))
if norm > 0:
escape_direction = [d / norm for d in escape_vector]
obj.location.x += escape_direction[0] * params["B_boost_speed"]
obj.location.y += escape_direction[1] * params["B_boost_speed"]
obj.location.z += escape_direction[2] * params["B_boost_speed"]
obj.active_material.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = params["B_color_escape"]
else:
if distance <= escape_distance:
state_flags["B_is_escaping"] = True
obj.active_material.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = params["B_color_escape"]
else:
if frame - last_change_frame >= params["direction_change_interval"]:
direction = [random.uniform(-1, 1), random.uniform(-1, 1), random.uniform(-1, 1)]
last_change_frame = frame
obj.location.x += direction[0] * params["B_normal_speed"]
obj.location.y += direction[1] * params["B_normal_speed"]
obj.location.z += direction[2] * params["B_normal_speed"]
obj.active_material.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = params["B_color_normal"]
obj.keyframe_insert(data_path="location", frame=frame)
obj.active_material.node_tree.nodes["Principled BSDF"].inputs["Base Color"].keyframe_insert(data_path="default_value", frame=frame)
return last_change_frame, direction
def main():
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
for mat in bpy.data.materials:
bpy.data.materials.remove(mat, do_unlink=True)
bpy.ops.mesh.primitive_cone_add(vertices=3, radius1=1, depth=2, location=params["initial_position_A"])
A = bpy.context.object
A.name = "A"
A.data.materials.append(create_material("A_Material", params["A_color_far"]))
bpy.ops.mesh.primitive_uv_sphere_add(radius=1, location=params["initial_position_B"])
B = bpy.context.object
B.name = "B"
B.data.materials.append(create_material("B_Material", params["B_color_normal"]))
frame_start = 1
frame_end = 250
direction = [random.uniform(-1, 1), random.uniform(-1, 1), random.uniform(-1, 1)]
last_change_frame = 0
for frame in range(frame_start, frame_end + 1):
last_change_frame, direction = move_B(B, A, frame, params["escape_distance"], params["safe_distance"], direction, last_change_frame)
move_A(A, B, frame)
bpy.context.scene.frame_set(frame_start)
main()