Script Blender: Smooth Auto-Weighting Menggunakan Gaussian Falloff

 Script Blender:
Smooth Auto-Weighting Menggunakan Gaussian Falloff untuk 7 Bone pada Plane

--

Pernahkah Anda merasa kesulitan saat melakukan weight painting manual di Blender, terutama untuk objek seperti bendera, kain, atau tali yang membutuhkan transisi antar bone yang sangat mulus?

Weight painting manual sering kali menghasilkan lekukan yang kaku (sharp/jagged) saat dianimasikan. Metode default Blender (Automatic Weights) pun terkadang kurang presisi jika bentuk mesh kita sederhana seperti Plane.

Artikel ini membagikan script Python khusus untuk Blender yang berfungsi melakukan automatic weight calculation secara presisi menggunakan distribusi kurva Gaussian (Gaussian Falloff). Script ini dirancang untuk mendistribusikan weight secara otomatis pada 7 bone di sepanjang objek Plane, sekaligus memastikan total weight pada setiap vertex selalu ter-normalisasi sempurna (sama dengan 1.0) tanpa mengganggu keyframe atau animasi yang sudah ada.

Cara Kerja Script:

  1. Deteksi Sumbu Otomatis: Membaca posisi awal (Bone.1) hingga akhir (Bone.7) lalu mendeteksi arah sumbu tanpa peduli orientasi objek (X, Y, atau Z).

  2. Kalkulasi Gaussian Falloff: Menghitung pengaruh (influence) tiap bone ke setiap vertex menggunakan fungsi distribusi Gaussian.

  3. Normalisasi Weight: Memastikan penjumlahan seluruh weight dari Bone.1 hingga Bone.7 pada setiap vertex bernilai tepat 1.0.

  4. Proteksi Animasi: Meng-update vertex group tanpa menyentuh data animasi atau keyframe yang sudah tersimpan.

Kode Script (Python - Blender API)

Python
import bpy
import math

# ============================================================
# SMOOTH GAUSSIAN AUTO-WEIGHT
# Plane.005 + Armature.7
#
# Tujuan:
# - Weight Bone.1 ... Bone.7 dibuat sangat smooth
# - Antar bone overlap
# - Total weight per vertex = 1.0
# - Tidak menyentuh animation/keyframe
# ============================================================

MESH_NAME = "Plane.005"
ARMATURE_NAME = "Armature.7"

BONE_NAMES = [
    "Bone.1",
    "Bone.2",
    "Bone.3",
    "Bone.4",
    "Bone.5",
    "Bone.6",
    "Bone.7",
]

# ------------------------------------------------------------
# WIDTH OF INFLUENCE
#
# 0.75 = lebih lokal / tajam
# 1.00 = balanced
# 1.25 = lebih silky / overlap lebar
# 1.50 = sangat lembut
#
# Mulai 1.25 untuk bendera kita.
# ------------------------------------------------------------

SIGMA = 1.25

# ============================================================
# GET OBJECTS
# ============================================================

obj = bpy.data.objects.get(MESH_NAME)
arm = bpy.data.objects.get(ARMATURE_NAME)

if obj is None:
    raise Exception(f"Mesh '{MESH_NAME}' tidak ditemukan")

if arm is None:
    raise Exception(f"Armature '{ARMATURE_NAME}' tidak ditemukan")

if obj.type != 'MESH':
    raise Exception(f"'{MESH_NAME}' bukan Mesh")

if arm.type != 'ARMATURE':
    raise Exception(f"'{ARMATURE_NAME}' bukan Armature")

# ============================================================
# FORCE OBJECT MODE
# ============================================================

if bpy.context.object:
    try:
        bpy.ops.object.mode_set(mode='OBJECT')
    except:
        pass

# ============================================================
# GET BONE POSITIONS
# Kita baca posisi bone dalam LOCAL SPACE mesh.
# ============================================================

bone_positions = []

for bone_name in BONE_NAMES:
    bone = arm.data.bones.get(bone_name)

    if bone is None:
        raise Exception(f"Bone '{bone_name}' tidak ditemukan")

    # bone head -> world
    world_pos = arm.matrix_world @ bone.head_local

    # world -> mesh local
    mesh_pos = obj.matrix_world.inverted() @ world_pos

    bone_positions.append(mesh_pos)

# ============================================================
# DETECT MAIN AXIS
# Cari sumbu sepanjang Bone.1 -> Bone.7.
# Jadi script tidak bergantung apakah mesh memanjang di X/Y/Z.
# ============================================================

start = bone_positions[0]
end = bone_positions[-1]

axis = end - start

if axis.length == 0:
    raise Exception("Bone.1 dan Bone.7 berada di posisi yang sama")

axis.normalize()

# ============================================================
# PROJECT BONE POSITIONS TO 1D
# ============================================================

bone_axis_pos = []

for pos in bone_positions:
    relative = pos - start
    projected = relative.dot(axis)
    bone_axis_pos.append(projected)

print("\n========================================")
print("BONE POSITION ALONG FLAG")
print("========================================")

for name, p in zip(BONE_NAMES, bone_axis_pos):
    print(f"{name:7s}: {p:+.5f}")

# ============================================================
# AVERAGE BONE SPACING
# Digunakan agar SIGMA relatif terhadap jarak antar bone.
# ============================================================

distances = []

for i in range(len(bone_axis_pos) - 1):
    d = abs(bone_axis_pos[i + 1] - bone_axis_pos[i])
    if d > 0.000001:
        distances.append(d)

if not distances:
    raise Exception("Tidak bisa menentukan jarak antar bone")

average_spacing = sum(distances) / len(distances)
sigma_distance = average_spacing * SIGMA

print("\nAverage bone spacing :", average_spacing)
print("Gaussian sigma       :", sigma_distance)

# ============================================================
# CREATE / GET VERTEX GROUPS
# ============================================================

groups = {}

for bone_name in BONE_NAMES:
    vg = obj.vertex_groups.get(bone_name)
    if vg is None:
        vg = obj.vertex_groups.new(name=bone_name)
    groups[bone_name] = vg

# ============================================================
# CLEAR OLD WEIGHTS
# Hanya Bone.1 ... Bone.7.
# Vertex group lain TIDAK disentuh.
# ============================================================

all_indices = [v.index for v in obj.data.vertices]

for bone_name in BONE_NAMES:
    vg = groups[bone_name]
    try:
        vg.remove(all_indices)
    except:
        pass

# ============================================================
# GAUSSIAN FUNCTION
# ============================================================

def gaussian(distance, sigma):
    if sigma <= 0:
        return 0.0
    return math.exp(-0.5 * (distance / sigma) ** 2)

# ============================================================
# CALCULATE WEIGHTS
# ============================================================

print("\nCalculating smooth weights...")

for vertex in obj.data.vertices:

    # vertex position projected along flag axis
    relative = vertex.co - start
    vertex_axis_pos = relative.dot(axis)

    raw_weights = []

    # Gaussian influence from every bone
    for bone_pos in bone_axis_pos:
        distance = vertex_axis_pos - bone_pos
        w = gaussian(distance, sigma_distance)
        raw_weights.append(w)

    # NORMALIZE (Bone.1 + ... + Bone.7 = 1.0)
    total = sum(raw_weights)

    if total <= 0.0000001:
        continue

    normalized = [w / total for w in raw_weights]

    # WRITE WEIGHTS
    for bone_name, weight in zip(BONE_NAMES, normalized):
        # Jangan tulis weight sangat kecil
        if weight > 0.0001:
            groups[bone_name].add([vertex.index], weight, 'REPLACE')

# ============================================================
# UPDATE
# ============================================================

obj.data.update()

bpy.context.view_layer.objects.active = obj
obj.select_set(True)

print("\n========================================")
print("SMOOTH 7-BONE WEIGHT COMPLETE")
print("========================================")
print(f"Mesh       : {MESH_NAME}")
print(f"Armature   : {ARMATURE_NAME}")
print(f"Sigma      : {SIGMA}")
print("Total weight per vertex normalized = 1")
print("Animation untouched")
print("========================================")

Cara Penggunaan di Blender:

  1. Buka tab Scripting di Blender.

  2. Buat file baru (Text -> New) lalu paste kode di atas.

  3. Sesuaikan variabel MESH_NAME dan ARMATURE_NAME dengan nama objek yang kamu miliki di scene.

  4. Kamu juga bisa mengubah nilai SIGMA jika ingin transisi lebih tajam (misal 0.75) atau lebih mulus/lebar (misal 1.50).

  5. Klik ikon Play / Run Script. Hasil weight baru akan langsung di-apply pada objek mesh kamu.

TRANSLATE this Page

Posting Komentar

0 Komentar