Scripts sur Blender (Bêta)
Utilises Blender sous forme de script écrit en langage Python de manière complète incluant les concepts mathématiques les plus poussés.
Quelques bases
# Python
import os # I/O
import bpy
import math
import bmesh
import colorsys
import mathutils
from mathutils import Vector, Matrix, Color
if bpy.context.mode != 'OBJECT': # si : pas en mode OBJECT, passe en
bpy.ops.object.mode_set(mode='OBJECT') # mode OBJECT (EDIT — POSE — WEIGHT_PAINT — … )
bpy.ops.object.select_all(action='SELECT') # sélectionne tout, DESELECT — INVERT — TOGGLE
bpy.ops.object.delete(use_global=False) # supprime la sélection
IMG_SIZE = 4096 # 1024 is ok
SAVE_DIR = bpy.path.abspath("//textures/")
os.makedirs(SAVE_DIR, exist_ok=True)
bpy.ops.wm.save_mainfile()
Configure ta scène
# Python
bpy.context.scene.frame_start = 1
bpy.context.scene.frame_end = 360
bpy.context.scene.render.fps = 120
bpy.context.scene.render.fps_base = 1.0
bpy.context.scene.frame_current = 1
bpy.context.scene.frame_set(1)
scene = bpy.context.scene
scene.render.engine = 'CYCLES'
# GPU si disponible
prefs = bpy.context.preferences.addons["cycles"].preferences
try:
prefs.compute_device_type = 'METAL'
scene.cycles.device = 'GPU'
except:
scene.cycles.device = 'CPU'
scene.cycles.samples = 1
Créer des meshes
# Python
bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=2, radius=1.0,
calc_uvs=True, enter_editmode=False, align='WORLD',
location=(0.0, 0.0, 0.0),
rotation=(0.0, 0.0, 0.0),
scale=(0.0, 0.0, 0.0))
myIcoSphere = bpy.context.active_object # sélectionne le dernier objet créé.
myIcoSphere.name = "Ballon" # nomme-le
myIcoSphere.scale = (1.0, 2.3, 0.89) # met à l'échelle puis applique les modifications.
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
bpy.ops.object.add(radius=1.0, type='CAMERA', enter_editmode=False, align='WORLD',
location=(0.0, 0.0, -3.0), rotation=(0.0, 0.0, 0.0), scale=(0.0, 0.0, 0.0))
bpy.ops.object.add(radius=1.0, type='LIGHT', enter_editmode=False, align='WORLD',
location=(0.0, 0.0, 0.0), rotation=(0.0, 0.0, 0.0), scale=(0.0, 0.0, 0.0))
bpy.ops.object.add(radius=1.0, type='ARMATURE', enter_editmode=False, align='WORLD',
location=(0.0, 0.0, 0.0), rotation=(0.0, 0.0, 0.0), scale=(0.0, 0.0, 0.0))
# bpy.ops.object.add(radius=1.0, type='MESH', enter_editmode=False, align='WORLD',
location=(0.0, 0.0, 0.0), rotation=(0.0, 0.0, 0.0), scale=(0.0, 0.0, 0.0))
bpy.ops.mesh.primitive_cylinder_add(vertices=32, radius=1.0, depth=2.0,
end_fill_type='NGON', calc_uvs=True,
enter_editmode=False, align='WORLD',
location=(0.0, 0.0, 0.0),
rotation=(0.0, 0.0, 0.0),
scale=(0.0, 0.0, 0.0))
bpy.ops.mesh.primitive_cube_add(size=2.0, calc_uvs=True, enter_editmode=False, align='WORLD',
location=(0.0, 0.0, 0.0), rotation=(0.0, 0.0, 0.0),
scale=(0.0, 0.0, 0.0))
bpy.ops.mesh.primitive_uv_sphere_add(segments=32, ring_count=16, radius=1.0,
calc_uvs=True, enter_editmode=False, align='WORLD',
location=(0.0, 0.0, 0.0),
rotation=(0.0, 0.0, 0.0),
scale=(0.0, 0.0, 0.0))
bpy.ops.mesh.primitive_plane_add(size=2.0, calc_uvs=True, enter_editmode=False, align='WORLD', location=(0.0, 0.0, 0.0), rotation=(0.0, 0.0, 0.0), scale=(0.0, 0.0, 0.0))
def new_mesh_obj(name, collection=None):
mesh = bpy.data.meshes.new(name + "_mesh")
obj = bpy.data.objects.new(name, mesh)
col = collection or bpy.context.scene.collection
col.objects.link(obj)
return obj, mesh
L'armature
# Python
bpy.ops.object.armature_add(radius=1.0, enter_editmode=False, align='WORLD',
location=(0.0, 0.0, 0.0),
rotation=(0.0, 0.0, 0.0),
scale=(0.0, 0.0, 0.0))
armature = bpy.context.active_object
armature.rotation_euler[0] = math.radians(-90)
bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
armature.scale = (scale_factor, scale_factor, scale_factor)
bpy.ops.armature.extrude_move(TRANSFORM_OT_translate={"value": (0.0, 0.0, 3.0), "orient_type": 'GLOBAL',})
bpy.ops.armature.select_all(action='DESELECT')
bone = bpy.context.object.data.bone[0]
bone[0].select = True # False
bone.select_head = True
bone.select_tail = True
bone.remove(bone[0])
armature.name = "armature"
bpy.ops.object.parent_set(type='ARMATURE_AUTO')
Modifier des meshes
bpy.context.view_layer.objects.active = myPlane myPlane.select_set(True)Parentage des objets
Sélection des faces et autres
obj = bpy.context.active_object bpy.ops.object.editmode_toggle() bm = bmesh.from_edit_mesh(obj.data) bm.faces.ensure_lookup_table() for f in bm.faces: f.select = False face_indices = [10, 14] for i in face_indices: bm.faces[i].select = True bmesh.update_edit_mesh(obj.data)Image
IMG_SIZE = 4096
bpy.ops.image.new(name='Untitled', width=1024, height=1024, color=(0.0, 0.0, 0.0, 1.0), alpha=True, generated_type='BLANK', float=False, use_stereo_3d=False, tiled=False)
img_basecolor = bpy.data.images.new("myPlane_BaseColor", width=IMG_SIZE,
height=IMG_SIZE, alpha=False)
img_basecolor.colorspace_settings.name = 'Display P3'
img_basecolor.filepath_raw = os.path.join(SAVE_DIR, "myPlane_BaseColor.png")
img_basecolor.file_format = 'PNG'
img_normal = bpy.data.images.new("myPlane_Normal", width=IMG_SIZE,
height=IMG_SIZE, alpha=False)
img_normal.colorspace_settings.name = 'Non-Color'
img_normal.filepath_raw = os.path.join(SAVE_DIR, "myPlane_Normal.png")
img_normal.file_format = 'PNG'
# valeur par defaut = normal "plat" (0.5, 0.5, 1.0) plutot que noir
pixels_normal = [0.5, 0.5, 1.0, 1.0] * (IMG_SIZE * IMG_SIZE)
img_normal.pixels = pixels_normal
img_roughness = bpy.data.images.new("myPlane_Roughness", width=IMG_SIZE,
height=IMG_SIZE, alpha=False)
img_roughness.colorspace_settings.name = 'Non-Color'
img_roughness.filepath_raw = os.path.join(SAVE_DIR, "myPlane_Roughness.png")
img_roughness.file_format = 'PNG'
img_ao = bpy.data.images.new("myPlane_AO", width=IMG_SIZE, height=IMG_SIZE, alpha=False)
img_ao.colorspace_settings.name = 'Non-Color'
img_ao.filepath_raw = os.path.join(SAVE_DIR, "myPlane_AO.png")
img_ao.file_format = 'PNG'
UV
IMG_SIZE = 4096
SAVE_DIR = bpy.path.abspath("//textures/")
os.makedirs(SAVE_DIR, exist_ok=True)
bpy.ops.mesh.primitive_plane_add(size=8.9, rotation=(0.0, 0.0, 0.0),
scale=(2.3, 2.3, 2.3))
myPlane = bpy.context.active_object
myPlane.name = "myPlane"
def smart_uv_unwrap(obj, angle_limit=66.0, island_margin=0.0):
bpy.context.view_layer.objects.active = obj
for o in bpy.context.selected_objects:
o.select_set(False)
obj.select_set(True)
if not obj.data.uv_layers:
obj.data.uv_layers.new(name="UVMap")
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.uv.smart_project(angle_limit=1.15192, margin_method='SCALED', rotate_method='AXIS_ALIGNED_Y', island_margin=0.0, area_weight=0.0, correct_aspect=True, scale_to_bounds=False)
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.image.new(name="UV_Grid", width=4096, height=4096,
color=(0.0, 0.0, 0.0, 1.0), alpha=True,
generated_type='UV_GRID',
float=True, # 32-bit float
tiled=False)
img_uv_grid = bpy.data.images["UV_Grid"]
smart_uv_unwrap(myPlane)
Nœud de texture
def create_colorramp_node(nodes, n_stops=20):
ramp = nodes.new("ShaderNodeValToRGB")
ramp.name = "MyColorRamp"
cr = ramp.color_ramp
cr.interpolation = 'LINEAR'
while len(cr.elements) > 1:
cr.elements.remove(cr.elements[-1])
spacing = 1.0 / (n_stops - 1)
for i in range(1, n_stops):
cr.elements.new(i * spacing)
return ramp
mat = bpy.data.materials.new("myPlaneMaterial")
mat.use_nodes = True
nodes = mat.node_tree.nodes
links = mat.node_tree.links
bsdf = nodes.get("Principled BSDF")
bsdf.location = (600, 100)
bsdf.inputs["Metallic"].default_value = 0.6 # 0.0 by default
bsdf.inputs["Roughness"].default_value = 0.0 # 0.5 by default
output = nodes.get("Material Output")
output.location = (900, 0)
tex_coord = nodes.new("ShaderNodeTexCoord");
tex_coord.location = (-900, 0)
voronoise = nodes.new("ShaderNodeTexVoronoi");
voronoise.location = (-700, 0)
mapping_col = nodes.new("ShaderNodeMapping");
mapping_col.location = (-700, 2000)
noise_bump = nodes.new("ShaderNodeTexNoise");
noise_bump.location = (-700, 310)
noise_bump.inputs["Scale"].default_value = 23.7
noise_bump.noise_dimensions = '4D'
ramp = nodes.new("ShaderNodeValToRGB");
ramp.location = (-200, 250)
bump = nodes.new("ShaderNodeBump");
bump.location = (-200, -250)
bump.inputs["Strength"].default_value = 1.0
bump.inputs["Filter Width"].default_value = 3.0
bump.inputs["Distance"].default_value = -1000
mixt = nodes.new("ShaderNodeMix");
mixt.location = (200, 500)
mixt.data_type = 'RGBA' # FLOAT — VECTOR
mixt.inputs["Factor"].default_value = 0.5
matwheel = create_colorramp_node(nodes)
matwheel.location = (-200, 450)
matwheel2 = create_colorramp_material(nodes)
matwheel2.location = (-200, -25)
tex_bake_target = nodes.new('ShaderNodeTexImage')
tex_bake_target.image = img_basecolor
tex_bake_target.location = (600, 360)
nodes.active = tex_bake_target # IMPORTANT : c'est le node actif que bake() utilise
normal_bake_target = nodes.new('ShaderNodeTexImage')
normal_bake_target.image = img_normal
normal_bake_target.location = (900, 360)
nodes.active = normal_bake_target
Liens des textures nodes
links.new(tex_coord.outputs["Object"], mapping_col.inputs["Vector"])
links.new(mapping_col.outputs["Vector"], noise_col.inputs["Vector"])
links.new(noise_col.outputs["Fac"], ramp.inputs["Fac"])
links.new(ramp.outputs["Color"], bsdf.inputs["Base Color"])
links.new(tex_coord.outputs["Generated"], mapping_bump.inputs["Vector"])
links.new(mapping_bump.outputs["Vector"], noise_bump.inputs["Vector"])
links.new(noise_bump.outputs["Fac"], bump.inputs["Height"])
links.new(bump.outputs["Normal"], bsdf.inputs["Normal"])
links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])
links.new(tex_coord.outputs["Object"], voronoise.inputs["Vector"])
links.new(noise_bump.outputs["Fac"], matwheel.inputs["Fac"])
links.new(mixt.inputs["A"], matwheel.outputs["Color"])
links.new(mixt.inputs["B"], matwheel2.outputs["Color"])
links.new(mixt.outputs["Result"], bsdf.inputs["Base Color"])
links.new(voronoise.outputs["Color"], bump.inputs["Height"])
links.new(voronoise.outputs["Color"], matwheel2.inputs["Fac"])
links.new(bump.outputs["Normal"], bsdf.inputs["Normal"])
links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])
Animations
armature.animation_data_create()
action_idle = bpy.data.actions.new("Idle")
slot_idle = action_idle.slots.new(id_type='OBJECT', name="BalloonArmature")
layer_idle = action_idle.layers.new("Layer")
strip_idle = layer_idle.strips.new(type='KEYFRAME')
cb_idle = strip_idle.channelbag(slot_idle, ensure=True)
fc_idle_z = cb_idle.fcurves.new('pose.bones["Bone_Spine"].location', index=2)
fc_idle_z.keyframe_points.add(3)
fc_idle_z.keyframe_points[0].co = (1, 0.0)
fc_idle_z.keyframe_points[1].co = (25, 0.05)
fc_idle_z.keyframe_points[2].co = (50, 0.0)
for kp in fc_idle_z.keyframe_points:
kp.interpolation = 'SINE'
armature.animation_data.action = action_idle
armature.animation_data.action_slot = slot_idle
track_idle = armature.animation_data.nla_tracks.new()
track_idle.name = "Idle"
track_idle.strips.new("Idle", 1, action_idle)
armature.animation_data.action = None
# ---------- Walk : demarche diagonale (TR+BL vs BR+TL) ----------
action_walk = bpy.data.actions.new("Walk")
slot_walk = action_walk.slots.new(id_type='OBJECT', name="BalloonArmature")
layer_walk = action_walk.layers.new("Layer")
strip_walk = layer_walk.strips.new(type='KEYFRAME')
cb_walk = strip_walk.channelbag(slot_walk, ensure=True)
diag_a = ["Bone_WheelTR", "Bone_WheelBL"]
diag_b = ["Bone_WheelBR", "Bone_WheelTL"]
for bone_name in diag_a:
fc = cb_walk.fcurves.new('pose.bones["%s"].rotation_euler' % bone_name, index=0)
fc.keyframe_points.add(3)
fc.keyframe_points[0].co = (1, math.radians(-20))
fc.keyframe_points[1].co = (20, math.radians(20))
fc.keyframe_points[2].co = (40, math.radians(-20))
for kp in fc.keyframe_points:
kp.interpolation = 'SINE'
for bone_name in diag_b:
fc = cb_walk.fcurves.new('pose.bones["%s"].rotation_euler' % bone_name, index=0)
fc.keyframe_points.add(3)
fc.keyframe_points[0].co = (1, math.radians(20))
fc.keyframe_points[1].co = (20, math.radians(-20))
fc.keyframe_points[2].co = (40, math.radians(20))
for kp in fc.keyframe_points:
kp.interpolation = 'SINE'
armature.animation_data.action = action_walk
armature.animation_data.action_slot = slot_walk
track_walk = armature.animation_data.nla_tracks.new()
track_walk.name = "Walk"
track_walk.strips.new("Walk", 100, action_walk)
armature.animation_data.action = None
# ---------- Bounce : les 4 pattes ecartees en meme temps + squash colonne ----------
action_bounce = bpy.data.actions.new("Bounce")
slot_bounce = action_bounce.slots.new(id_type='OBJECT', name="BalloonArmature")
layer_bounce = action_bounce.layers.new("Layer")
strip_bounce = layer_bounce.strips.new(type='KEYFRAME')
cb_bounce = strip_bounce.channelbag(slot_bounce, ensure=True)
all_legs = ["Bone_WheelTR", "Bone_WheelBR", "Bone_WheelBL", "Bone_WheelTL"]
for bone_name in all_legs:
fc = cb_bounce.fcurves.new('pose.bones["%s"].rotation_euler' % bone_name, index=2)
fc.keyframe_points.add(3)
fc.keyframe_points[0].co = (1, 0.0)
fc.keyframe_points[1].co = (15, math.radians(15))
fc.keyframe_points[2].co = (30, 0.0)
for kp in fc.keyframe_points:
kp.interpolation = 'BEZIER'
fc_squash = cb_bounce.fcurves.new('pose.bones["Bone_Spine"].scale', index=2)
fc_squash.keyframe_points.add(3)
fc_squash.keyframe_points[0].co = (1, 1.0)
fc_squash.keyframe_points[1].co = (15, 0.85)
fc_squash.keyframe_points[2].co = (30, 1.0)
for kp in fc_squash.keyframe_points:
kp.interpolation = 'BEZIER'
armature.animation_data.action = action_bounce
armature.animation_data.action_slot = slot_bounce
track_bounce = armature.animation_data.nla_tracks.new()
track_bounce.name = "Bounce"
track_bounce.strips.new("Bounce", 180, action_bounce)
armature.animation_data.action = None
Bake
# Python
# BAKE
bpy.context.view_layer.objects.active = myPlane
myPlane.select_set(True)
# BAKE BASE COLOR
nodes.active = tex_bake_target
scene.cycles.bake_type = 'DIFFUSE'
scene.render.bake.use_pass_direct = False
scene.render.bake.use_pass_indirect = False
scene.render.bake.use_pass_color = True
scene.render.bake.margin = 16
bpy.ops.object.bake(type='DIFFUSE')
img_basecolor.save()
# BAKE NORMAL
nodes.active = normal_bake_target
scene.cycles.bake_type = 'NORMAL'
scene.render.bake.normal_space = 'TANGENT'
scene.render.bake.margin = 16
bpy.ops.object.bake(type='NORMAL')
img_normal.save()
def new_bake_image(name, colorspace):
img = bpy.data.images.new(name, width=IMG_SIZE, height=IMG_SIZE, alpha=False)
img.colorspace_settings.name = colorspace
img.filepath_raw = os.path.join(SAVE_DIR, f"{name}.png")
img.file_format = 'PNG'
return img
img_basecolor = new_bake_image("myPlane_BaseColor", 'sRGB')
img_normal = new_bake_image("myPlane_Normal", 'Non-Color')
img_roughness = new_bake_image("myPlane_Roughness", 'Non-Color')
img_metallic = new_bake_image("myPlane_Metallic", 'Non-Color')
Collection
def new_collection(name): if name in bpy.data.collections: return bpy.data.collections[name] col = bpy.data.collections.new(name) bpy.context.scene.collection.children.link(col) return col def link_to(obj, collection): for c in obj.users_collection: c.objects.unlink(obj) collection.objects.link(obj)Placer la caméra sur les mêmes axes que les conventions Metal
import bpy
import math
import mathutils
from mathutils import Vector, Matrix
def create_camera(name: str,
location: Vector,
target: Vector,
up: Vector = None,
fov_deg: float = 60.0,
near: float = 0.1,
far: float = 100.0) -> bpy.types.Object:
"""
Crée une caméra Blender orientée via lookAt mathématique.
Args:
name : nom de l'objet
location : position monde (espace Blender, Z-up)
target : point visé
up : vecteur "haut" hint (défaut Z+ en Blender)
fov_deg : champ de vision horizontal en degrés
near/far : plans de clipping
Returns:
L'objet caméra Blender.
Maths :
forward = normalize(target − location) # vecteur de vue
right = normalize(forward × up_hint) # axe X de la cam
true_up = right × forward # axe Y réel
La matrice de rotation R est [right | true_up | −forward]
(Blender : la caméra pointe vers −Y local, donc on stocke −forward)
"""
if up is None:
up = Vector((0, 0, 1))
forward = (target - location).normalized()
# Dégénérescence : si forward // up, on pivote le hint
if abs(forward.dot(up)) > 0.999:
up = Vector((0, 1, 0)) if abs(forward.z) > 0.9 else Vector((0, 0, 1))
right = forward.cross(up).normalized()
true_up = right.cross(forward).normalized()
# Matrice de rotation 3×3 → quaternion
# Colonne 0 = right, Colonne 1 = true_up, Colonne 2 = -forward (convention Blender)
rot_mat = Matrix((
(right.x, right.y, right.z),
(true_up.x, true_up.y, true_up.z),
(-forward.x,-forward.y,-forward.z),
)).transposed() # transposée car Blender attend les colonnes en lignes
cam_data = bpy.data.cameras.new(name)
cam_data.angle = math.radians(fov_deg) # FOV horizontal
cam_data.clip_start = near
cam_data.clip_end = far
cam_data.sensor_fit = 'HORIZONTAL'
cam_ob = bpy.data.objects.new(name, cam_data)
bpy.context.scene.collection.objects.link(cam_ob)
cam_ob.location = location
cam_ob.rotation_mode = 'QUATERNION'
cam_ob.rotation_quaternion = rot_mat.to_quaternion()
return cam_ob
if __name__ == "__main__":
TARGET = Vector((0, 0, 0))
RADIUS = 5.0
AZIMUTH = 225.0 # deg, angle orbital horizontal
ELEV = 20.0 # deg, angle au-dessus de l'horizon
EYE_POS = Vector((0, 0, -3))
cam = create_camera(name = "Camera",
location = EYE_POS,
target = TARGET,
fov_deg = 60,
near = 0.1,
far = 200.0,)
bpy.context.scene.camera = cam
Une animation bien utile !
togen.py
import bpy
import math
import bmesh
scale_factor = 25 #nbBones, scale armature, nbSquare, -Y +Y frame,
BONE_LENGTH = 0.1
SCALE_FACTOR = 0.8743 # 25 0.8743 so 50 * 2
bpy.context.scene.frame_start = 1
bpy.context.scene.frame_end = 500
bpy.context.scene.render.fps = 120
bpy.context.scene.render.fps_base = 1.0
bpy.context.scene.frame_current = 1
if bpy.context.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
#bpy.ops.object.select_all(action='SELECT')
#bpy.ops.object.delete(use_global=False)
radius = 3
magic_cubique_bezier = 0.5522847498
circumference = 2 * math.pi * radius
bpy.ops.object.armature_add(enter_editmode=False, location=(0, -scale_factor, 0))
armature = bpy.context.active_object
armature.name = "petite_chenille_deviendra_grande"
armature.rotation_euler[0] = math.radians(-90)
bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
armature.scale = (scale_factor, scale_factor, scale_factor)
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.armature.select_all(action='SELECT')
bpy.ops.armature.subdivide(number_cuts=scale_factor - 1)
edit_bones = armature.data.edit_bones
bones_by_position = sorted(edit_bones, key=lambda b: b.head.y, reverse=True)
num_bones = len(bones_by_position)
for i, bone in enumerate(bones_by_position):
bone.name = f"Bone_{i:03d}"
bpy.ops.object.mode_set(mode='OBJECT')
bones = armature.data.bones
num_bones = len(bones)
first_bone = bones[0]
bone_length_local = first_bone.length
bone_length = bone_length_local * scale_factor
bone_head_world = armature.matrix_world @ first_bone.head_local
bpy.ops.mesh.primitive_plane_add(size=bone_length, location=(0, 0, 0))
plane = bpy.context.active_object
plane.name = "do_not_export"
plane.rotation_euler[1] = math.radians(90)
plane.location = bone_head_world.copy()
plane.location.y -= bone_length / 2 - scale_factor
plane.scale.x = 0.25
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
bpy.ops.object.mode_set(mode='EDIT')
bm = bmesh.from_edit_mesh(plane.data)
bm.faces.ensure_lookup_table()
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
for f in bm.faces:
f.select = False
for e in bm.edges:
e.select = False
for v in bm.verts:
v.select = False
horizontal_edges_per_iteration = []
for edge in bm.edges:
v1, v2 = edge.verts
if v1.co.y > 0 and v2.co.y > 0:
edge_vert_indices = [v.index for v in edge.verts]
horizontal_edges_per_iteration.append(edge_vert_indices)
break
for edge in bm.edges:
v1, v2 = edge.verts
if v1.co.y < 0 and v2.co.y < 0:
edge.select = True
break
bmesh.update_edit_mesh(plane.data)
for i in range(num_bones - 1):
selected_edges = [e for e in bm.edges if e.select]
if not selected_edges:
break
edge_vert_indices = []
for e in selected_edges:
edge_vert_indices.extend([v.index for v in e.verts])
horizontal_edges_per_iteration.append(list(set(edge_vert_indices)))
extruded = bmesh.ops.extrude_edge_only(bm, edges=selected_edges)
new_verts = [v for v in extruded['geom'] if isinstance(v, bmesh.types.BMVert)]
if new_verts:
bmesh.ops.translate(bm, vec=(0, -bone_length, 0), verts=new_verts)
for e in bm.edges:
e.select = False
new_edges = [g for g in extruded['geom'] if isinstance(g, bmesh.types.BMEdge)]
for e in new_edges:
v1, v2 = e.verts
if abs(v1.co.x - v2.co.x) > 0.1:
e.select = True
bmesh.update_edit_mesh(plane.data)
selected_edges = [e for e in bm.edges if e.select]
if selected_edges:
edge_vert_indices = []
for e in selected_edges:
edge_vert_indices.extend([v.index for v in e.verts])
horizontal_edges_per_iteration.append(list(set(edge_vert_indices)))
bpy.ops.object.mode_set(mode='OBJECT')
max_group_index = len(horizontal_edges_per_iteration) - 1
for i, vert_indices in enumerate(horizontal_edges_per_iteration):
vg_name = f"Group_{i:03d}"
vg = plane.vertex_groups.new(name=vg_name)
vg.add(vert_indices, 1.0, 'REPLACE')
bpy.ops.object.select_all(action='DESELECT')
plane.select_set(True)
armature.select_set(True)
bpy.context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='POSE')
pose_bones = armature.pose.bones
last_bone = pose_bones[scale_factor - 1]
first_bone = pose_bones[0]
copy_loc = first_bone.constraints.new('COPY_LOCATION')
copy_loc.target = plane
copy_loc.subtarget = f"Group_{max_group_index:03d}"
copy_rot = first_bone.constraints.new('COPY_ROTATION')
copy_rot.target = plane
copy_rot.subtarget = f"Group_{max_group_index:03d}"
stretch = first_bone.constraints.new('STRETCH_TO')
stretch.target = plane
stretch.subtarget = f"Group_{max_group_index - 1:03d}"
stretch.rest_length = 0
stretch.bulge = 0
for i in range(1, scale_factor):
bone = pose_bones[i]
rotation_group_index = i + 1
if rotation_group_index <= max_group_index:
copy_rot = bone.constraints.new('COPY_ROTATION')
copy_rot.target = plane
copy_rot.subtarget = f"Group_{max_group_index - i:03d}"
for i in range(1, num_bones):
bone = pose_bones[i]
stretch_group_index = i + 1
if stretch_group_index <= max_group_index:
stretch = bone.constraints.new('STRETCH_TO')
stretch.target = plane
stretch.subtarget = f"Group_{max_group_index - i - 1:03d}"
stretch.rest_length = 0
stretch.bulge = 0
bpy.ops.object.mode_set(mode='OBJECT')
bpy.context.view_layer.update()
bpy.context.evaluated_depsgraph_get()
bpy.context.view_layer.update()
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
bpy.context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = armature.data.edit_bones
bpy.ops.armature.select_all(action='DESELECT')
edit_bones[0].select = True
edit_bones[0].select_head = True
edit_bones[0].select_tail = True
bpy.ops.armature.duplicate()
new_bone = bpy.context.selected_editable_bones[0]
new_bone.name = "Bone_Control"
new_bone.head.x = 3
new_bone.head.y -= 6
new_bone.head.z += 3
new_bone.tail.x = 3
new_bone.tail.y -= 5.6
new_bone.tail.z += 3
center = (new_bone.head + new_bone.tail) / 2
new_bone.head = center + (new_bone.head - center) * 2.5
new_bone.tail = center + (new_bone.tail - center) * 2.5
bpy.ops.armature.select_all(action='DESELECT')
edit_bones[0].select = True
edit_bones[0].select_head = True
edit_bones[0].select_tail = True
edit_bones["Bone_Control"].select = True
edit_bones["Bone_Control"].select_head = True
edit_bones["Bone_Control"].select_tail = True
armature.data.edit_bones.active = edit_bones["Bone_Control"]
bpy.ops.armature.parent_set(type='OFFSET')
bpy.ops.object.mode_set(mode='POSE')
bone_control_pose = armature.pose.bones["Bone_Control"]
while len(bone_control_pose.constraints) > 0:
bone_control_pose.constraints.remove(bone_control_pose.constraints[0])
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
plane.select_set(True)
armature.select_set(True)
bpy.context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='POSE')
bpy.ops.pose.select_all(action='DESELECT')
armature.data.bones.active = armature.data.bones["Bone_Control"]
#bone_control_pose.bone.select = True
bpy.ops.object.parent_set(type='BONE')
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
bpy.context.view_layer.objects.active = armature
armature.location.y = scale_factor
armature.keyframe_insert(data_path="location", frame=1, index=1)
armature.location.y = -scale_factor
armature.keyframe_insert(data_path="location", frame=450, index=1)
curve_data = bpy.data.curves.new('TrackPath_Data', type='CURVE')
curve_data.dimensions = '3D'
curve_data.resolution_u = 1024
curve_data.use_radius = False
curve_obj = bpy.data.objects.new('CurvePath', curve_data)
bpy.context.collection.objects.link(curve_obj)
spline = curve_data.splines.new('BEZIER')
spline.resolution_u = 64
points_coords = [
(-0.3000, -0.1273, 0),
( 0.6000, -0.1273, 0),
( 0.6486, 0.1176, 0),
( 0.0669, 0.4618, 0),
(-0.1091, 0.4369, 0),
(-0.3793, 0.0996, 0)
]
spline.bezier_points.add(len(points_coords) - 1)
for i, coord in enumerate(points_coords):
bp = spline.bezier_points[i]
bp.co = coord
bp.handle_left_type = 'FREE'
bp.handle_right_type = 'FREE'
def set_handle(bp, point, center, radius, scale_k, is_exit):
angle = math.atan2(point[1] - center[1], point[0] - center[0])
tangent_x = -math.sin(angle)
tangent_y = math.cos(angle)
handle_len = scale_k * radius
if is_exit:
bp.handle_left = (
point[0] - handle_len * tangent_x,
point[1] - handle_len * tangent_y,
0 )
else:
bp.handle_right = (
point[0] + handle_len * tangent_x,
point[1] + handle_len * tangent_y,
0 )
bp0 = spline.bezier_points[0]
set_handle(bp0, points_coords[0], (-0.3, 0), 0.1273, 0.949, True)
bp0.handle_right = (-0.25, -0.1273, 0)
bp1 = spline.bezier_points[1]
bp1.handle_left = (0.55, -0.1273, 0)
set_handle(bp1, points_coords[1], (0.6, 0), 0.1273, 1.095, False)
bp2 = spline.bezier_points[2]
set_handle(bp2, points_coords[2], (0.6, 0), 0.1273, 1.095, True)
dx = points_coords[3][0] - points_coords[2][0]
dy = points_coords[3][1] - points_coords[2][1]
line_len = math.sqrt(dx*dx + dy*dy)
bp2.handle_right = (
points_coords[2][0] + 0.1 * dx / line_len,
points_coords[2][1] + 0.1 * dy / line_len,
0 )
bp3 = spline.bezier_points[3]
bp3.handle_left = (
points_coords[3][0] - 0.1 * dx / line_len,
points_coords[3][1] - 0.1 * dy / line_len,
0 )
set_handle(bp3, points_coords[3], (0, 0.3), 0.1751, 0.364, False)
bp4 = spline.bezier_points[4]
set_handle(bp4, points_coords[4], (0, 0.3), 0.1751, 0.364, True)
dx = points_coords[5][0] - points_coords[4][0]
dy = points_coords[5][1] - points_coords[4][1]
line_len = math.sqrt(dx*dx + dy*dy)
bp4.handle_right = (
points_coords[4][0] + 0.1 * dx / line_len,
points_coords[4][1] + 0.1 * dy / line_len,
0 )
bp5 = spline.bezier_points[5]
bp5.handle_left = (
points_coords[5][0] - 0.1 * dx / line_len,
points_coords[5][1] - 0.1 * dy / line_len,
0 )
set_handle(bp5, points_coords[5], (-0.3, 0), 0.1273, 0.949, False)
spline.use_cyclic_u = True
bpy.ops.object.select_all(action='DESELECT')
curve_obj.select_set(True)
bpy.context.view_layer.objects.active = curve_obj
bpy.ops.transform.resize(value=(SCALE_FACTOR * 10, SCALE_FACTOR * 10, SCALE_FACTOR * 10))
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
#plane.select_set(True)
obj = bpy.data.objects.get("do_not_export")
bpy.context.view_layer.objects.active = obj
curve_modifier = plane.modifiers.new(name="Curve", type='CURVE')
curve_modifier.object = curve_obj
curve_modifier.deform_axis = 'POS_Y'
bpy.context.view_layer.update()