Chapitre 0 · Blender Scripting

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()

Création de fonctionnalités

Connexion pour valider →