Bring a simple feline character to life with this easy-to-run Blender Python script. The script:
- Procedurally generates a cute cat mesh from primitives (body, head, ears, tail, legs, eyes).
- Creates a basic armature (spine, neck, head, ears, tail chain, and 4 legs).
- Parents mesh parts to bones via vertex groups + armature modifiers (simple rigging).
- Generates a default idle animation (tail sway, ear twitch, head bob and breathing).
- Provides placeholders for importing AI motion (BVH / JSON) for further extension.
NOTE: This script is intended as a demo / starter template for Blender scripting and animation prototyping. It is beginner-friendly and easy to extend (add textures, Better rigging IK/FK, retarget BVH, lip-sync, etc.).
How to run
- Download script from
https://drive.google.com/file/d/14ROcc3f2x8Q31jEode2HsHEl-HKRwRpE/view?usp=sharing - or Copy the script file:
# create_cat_character.py
# Blender Python script: membuat karakter kucing sederhana (blok + spheroids), armature, dan animasi idle & walk-cycle sederhana.
# Cara pakai:
# 1. Simpan file ini, mis: create_cat_character.py
# 2. Buka Blender -> Scripting -> Open -> pilih file -> Run Script
# atau jalankan dari terminal: blender --background --python create_cat_character.py
#
# Catatan: script ini dibuat untuk demo/prototipe. Mesh dibuat dari primitive dan tiap bagian diparent ke bone dengan vertex group penuh
# sehingga gerakan mengikuti bone dengan kuat (berfungsi sebagai "rigging sederhana").
import bpy
import math
from mathutils import Vector, Euler
# ---------------------------
# Utility
# ---------------------------
def clear_scene():
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
# clean unused datablocks
for block in list(bpy.data.meshes):
if block.users == 0:
bpy.data.meshes.remove(block)
for block in list(bpy.data.materials):
if block.users == 0:
bpy.data.materials.remove(block)
for block in list(bpy.data.armatures):
if block.users == 0:
bpy.data.armatures.remove(block)
def create_material(name, color=(1,1,1,1)):
mat = bpy.data.materials.get(name)
if mat is None:
mat = bpy.data.materials.new(name)
mat.use_nodes = True
bsdf = mat.node_tree.nodes.get('Principled BSDF')
if bsdf:
bsdf.inputs['Base Color'].default_value = color
return mat
# ---------------------------
# Mesh parts (cat body parts)
# ---------------------------
def create_spheroid(name, radius=1.0, loc=(0,0,0), scale=(1,1,1)):
bpy.ops.mesh.primitive_uv_sphere_add(radius=radius, location=loc, segments=32, ring_count=16)
obj = bpy.context.active_object
obj.name = name
obj.scale = scale
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
return obj
def create_cylinder(name, radius=0.2, depth=1.0, loc=(0,0,0), rot=(0,0,0)):
bpy.ops.mesh.primitive_cylinder_add(radius=radius, depth=depth, location=loc, rotation=rot)
obj = bpy.context.active_object
obj.name = name
return obj
def create_box(name, size=(1,1,1), loc=(0,0,0)):
bpy.ops.mesh.primitive_cube_add(size=1, location=loc)
obj = bpy.context.active_object
obj.scale = (size[0]/2, size[1]/2, size[2]/2)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
obj.name = name
return obj
# ---------------------------
# Build cat mesh and return dict of parts
# ---------------------------
def build_cat_mesh(origin=(0,0,0)):
parts = {}
ox, oy, oz = origin
# Body (ellipsoid)
body = create_spheroid('Cat_Body', radius=1.0, loc=(ox, oy, oz+0.8), scale=(1.2, 0.7, 0.6))
body.data.materials.append(create_material('Mat_Body', (0.8, 0.5, 0.3, 1)))
parts['body'] = body
# Head
head = create_spheroid('Cat_Head', radius=0.5, loc=(ox, oy+0.9, oz+1.6), scale=(1,0.95,0.95))
head.data.materials.append(create_material('Mat_Head',(0.85,0.55,0.35,1)))
parts['head'] = head
# Ears (triangular cones approximated by scaled cone)
bpy.ops.mesh.primitive_cone_add(radius1=0.25, depth=0.4, location=(ox+0.32, oy+1.25, oz+1.95))
ear_l = bpy.context.active_object
ear_l.name = 'Ear.L'
ear_l.rotation_euler = Euler((math.radians(10), 0, math.radians(25)),'XYZ')
ear_l.data.materials.append(create_material('Mat_Head'))
parts['ear.L'] = ear_l
bpy.ops.mesh.primitive_cone_add(radius1=0.25, depth=0.4, location=(ox-0.32, oy+1.25, oz+1.95))
ear_r = bpy.context.active_object
ear_r.name = 'Ear.R'
ear_r.rotation_euler = Euler((math.radians(10), 0, math.radians(-25)),'XYZ')
ear_r.data.materials.append(create_material('Mat_Head'))
parts['ear.R'] = ear_r
# Tail (chain of cylinders)
tail_segments = []
seg_count = 5
for i in range(seg_count):
seg_loc = (ox, oy-0.9 - i*0.25, oz+0.9 - i*0.08)
seg = create_cylinder(f'Tail_{i}', radius=0.12, depth=0.45, loc=seg_loc, rot=(math.radians(90),0,0))
seg.data.materials.append(create_material('Mat_Body'))
tail_segments.append(seg)
parts['tail'] = tail_segments
# Legs (upper + lower + paw) - front left/right and back left/right
legs = {}
leg_positions = {
'front.L': (ox+0.45, oy+0.4, oz+0.2),
'front.R': (ox-0.45, oy+0.4, oz+0.2),
'back.L': (ox+0.35, oy-0.4, oz+0.2),
'back.R': (ox-0.35, oy-0.4, oz+0.2),
}
for name, pos in leg_positions.items():
upper = create_cylinder(name+'_thigh', radius=0.15, depth=0.6, loc=(pos[0], pos[1], pos[2]+0.2), rot=(0,0,0))
lower = create_cylinder(name+'_shin', radius=0.12, depth=0.6, loc=(pos[0], pos[1], pos[2]-0.45), rot=(0,0,0))
paw = create_box(name+'_paw', size=(0.2,0.3,0.1), loc=(pos[0], pos[1]+0.05, pos[2]-0.85))
upper.data.materials.append(create_material('Mat_Body'))
lower.data.materials.append(create_material('Mat_Body'))
paw.data.materials.append(create_material('Mat_Body'))
legs[name] = (upper, lower, paw)
parts['legs'] = legs
# Eyes (simple black spheres)
eye_l = create_spheroid('Eye.L', radius=0.08, loc=(ox+0.18, oy+1.05, oz+1.6), scale=(1,1,1))
eye_r = create_spheroid('Eye.R', radius=0.08, loc=(ox-0.18, oy+1.05, oz+1.6), scale=(1,1,1))
mat_eye = create_material('Mat_Eye',(0,0,0,1))
eye_l.data.materials.append(mat_eye)
eye_r.data.materials.append(mat_eye)
parts['eye.L'] = eye_l
parts['eye.R'] = eye_r
return parts
# ---------------------------
# Create armature for cat
# ---------------------------
def create_cat_armature(name='CatArmature'):
bpy.ops.object.armature_add(enter_editmode=True, location=(0,0,0))
arm_obj = bpy.context.active_object
arm_obj.name = name
arm = arm_obj.data
arm.name = name + 'Data'
eb = arm.edit_bones
# Clear default bone and setup bones
base = eb[0]
base.name = 'spine_01'
base.head = Vector((0,0,0.4))
base.tail = Vector((0,0,0.9))
# spine chain
s2 = eb.new('spine_02')
s2.head = base.tail
s2.tail = Vector((0,0,1.25))
s2.parent = base
neck = eb.new('neck')
neck.head = s2.tail
neck.tail = Vector((0,0,1.6))
neck.parent = s2
head = eb.new('head')
head.head = neck.tail
head.tail = Vector((0,0,1.9))
head.parent = neck
# ears (bones as children of head)
ear_l = eb.new('ear.L')
ear_l.head = Vector((0.32,0.95,1.85))
ear_l.tail = Vector((0.44,1.15,2.05))
ear_l.parent = head
ear_r = eb.new('ear.R')
ear_r.head = Vector((-0.32,0.95,1.85))
ear_r.tail = Vector((-0.44,1.15,2.05))
ear_r.parent = head
# tail chain
prev = s2
for i in range(5):
b = eb.new(f'tail_{i}')
b.head = Vector((0, -0.6 - i*0.25, 0.95 - i*0.06))
b.tail = Vector((0, -0.85 - i*0.25, 0.9 - i*0.06))
b.parent = prev
prev = b
# front legs
upper_fl = eb.new('front_thigh.L')
upper_fl.head = Vector((0.45,0.45,0.8))
upper_fl.tail = Vector((0.45,0.45,0.2))
upper_fl.parent = base
lower_fl = eb.new('front_shin.L')
lower_fl.head = upper_fl.tail
lower_fl.tail = Vector((0.45,0.45,-0.2))
lower_fl.parent = upper_fl
paw_fl = eb.new('front_paw.L')
paw_fl.head = lower_fl.tail
paw_fl.tail = Vector((0.45,0.55,-0.25))
paw_fl.parent = lower_fl
upper_fr = eb.new('front_thigh.R')
upper_fr.head = Vector((-0.45,0.45,0.8))
upper_fr.tail = Vector((-0.45,0.45,0.2))
upper_fr.parent = base
lower_fr = eb.new('front_shin.R')
lower_fr.head = upper_fr.tail
lower_fr.tail = Vector((-0.45,0.45,-0.2))
lower_fr.parent = upper_fr
paw_fr = eb.new('front_paw.R')
paw_fr.head = lower_fr.tail
paw_fr.tail = Vector((-0.45,0.55,-0.25))
paw_fr.parent = lower_fr
# back legs
upper_bl = eb.new('back_thigh.L')
upper_bl.head = Vector((0.35,-0.45,0.8))
upper_bl.tail = Vector((0.35,-0.45,0.2))
upper_bl.parent = base
lower_bl = eb.new('back_shin.L')
lower_bl.head = upper_bl.tail
lower_bl.tail = Vector((0.35,-0.45,-0.2))
lower_bl.parent = upper_bl
paw_bl = eb.new('back_paw.L')
paw_bl.head = lower_bl.tail
paw_bl.tail = Vector((0.35,-0.55,-0.25))
paw_bl.parent = lower_bl
upper_br = eb.new('back_thigh.R')
upper_br.head = Vector((-0.35,-0.45,0.8))
upper_br.tail = Vector((-0.35,-0.45,0.2))
upper_br.parent = base
lower_br = eb.new('back_shin.R')
lower_br.head = upper_br.tail
lower_br.tail = Vector((-0.35,-0.45,-0.2))
lower_br.parent = upper_br
paw_br = eb.new('back_paw.R')
paw_br.head = lower_br.tail
paw_br.tail = Vector((-0.35,-0.55,-0.25))
paw_br.parent = lower_br
bpy.ops.object.mode_set(mode='OBJECT')
return arm_obj
# ---------------------------
# Parent meshes to bones via vertex groups
# ---------------------------
def parent_parts_to_armature(parts, arm_obj):
# Create armature modifier and vertex groups for each mesh
for name, obj in list(parts.items()):
if isinstance(obj, list):
# tail segments list
for i, seg in enumerate(obj):
seg_mod = seg.modifiers.new('ArmMod','ARMATURE')
seg_mod.object = arm_obj
vg = seg.vertex_groups.new(name=f'tail_{i}')
all_indices = [v.index for v in seg.data.vertices]
vg.add(all_indices, 1.0, 'ADD')
elif name == 'legs':
for lname, triplet in obj.items():
upper, lower, paw = triplet
# heuristics: map to bones
if 'front' in lname:
if '.L' in lname:
upper_b = 'front_thigh.L'
lower_b = 'front_shin.L'
paw_b = 'front_paw.L'
else:
upper_b = 'front_thigh.R'
lower_b = 'front_shin.R'
paw_b = 'front_paw.R'
else:
if '.L' in lname:
upper_b = 'back_thigh.L'
lower_b = 'back_shin.L'
paw_b = 'back_paw.L'
else:
upper_b = 'back_thigh.R'
lower_b = 'back_shin.R'
paw_b = 'back_paw.R'
for obj_piece, bone_name in [(upper, upper_b), (lower, lower_b), (paw, paw_b)]:
obj_piece_mod = obj_piece.modifiers.new('ArmMod','ARMATURE')
obj_piece_mod.object = arm_obj
vg = obj_piece.vertex_groups.new(name=bone_name)
all_indices = [v.index for v in obj_piece.data.vertices]
vg.add(all_indices, 1.0, 'ADD')
else:
# single objects: map by conventional names
target = None
if name == 'body':
target = 'spine_02'
elif name == 'head':
target = 'head'
elif name == 'eye.L':
target = 'head'
elif name == 'eye.R':
target = 'head'
elif name == 'ear.L':
target = 'ear.L'
elif name == 'ear.R':
target = 'ear.R'
if target:
obj_mod = obj.modifiers.new('ArmMod','ARMATURE')
obj_mod.object = arm_obj
vg = obj.vertex_groups.new(name=target)
all_indices = [v.index for v in obj.data.vertices]
vg.add(all_indices, 1.0, 'ADD')
# ---------------------------
# Simple procedural animations
# - idle: tail sway, head bob, ear twitch, breathing
# - simple walk cycle (crude) optional
# ---------------------------
def set_pose_and_key(arm_obj, bone_name, rot_euler=None, loc=None, frame=None):
bpy.context.view_layer.objects.active = arm_obj
bpy.ops.object.mode_set(mode='POSE')
pb = arm_obj.pose.bones.get(bone_name)
if not pb:
bpy.ops.object.mode_set(mode='OBJECT')
return
if rot_euler is not None:
pb.rotation_mode = 'XYZ'
pb.rotation_euler = rot_euler
pb.keyframe_insert(data_path='rotation_euler', frame=frame)
if loc is not None:
pb.location = Vector(loc)
pb.keyframe_insert(data_path='location', frame=frame)
bpy.ops.object.mode_set(mode='OBJECT')
def generate_idle_animation(arm_obj, start=1, end=120):
frames = end - start + 1
for f in range(start, end+1):
t = (f - start) / frames
# tail sway: use sin wave along tail bones
sway = math.sin(t * math.pi * 2) * math.radians(12)
for i in range(5):
bn = f'tail_{i}'
angle = sway * (1 - i * 0.12) # less movement near base
set_pose_and_key(arm_obj, bn, rot_euler=Euler((0, math.radians(angle*0.0), angle),'XYZ'), frame=f)
# head bob small
head_nod = math.sin(t * math.pi * 2) * math.radians(5)
set_pose_and_key(arm_obj, 'head', rot_euler=Euler((head_nod,0,0),'XYZ'), frame=f)
# ear twitch occasional (frame-based simple)
if f % 30 == 0:
set_pose_and_key(arm_obj, 'ear.L', rot_euler=Euler((math.radians(20),0,math.radians(20)),'XYZ'), frame=f)
set_pose_and_key(arm_obj, 'ear.R', rot_euler=Euler((math.radians(20),0,math.radians(-20)),'XYZ'), frame=f)
else:
set_pose_and_key(arm_obj, 'ear.L', rot_euler=Euler((math.radians(10),0,math.radians(25)),'XYZ'), frame=f)
set_pose_and_key(arm_obj, 'ear.R', rot_euler=Euler((math.radians(10),0,math.radians(-25)),'XYZ'), frame=f)
# breathing: subtle spine scale via pelvis/spine translation
breath = math.sin(t * math.pi * 2) * 0.02
set_pose_and_key(arm_obj, 'spine_02', loc=(0,0,breath), frame=f)
bpy.context.scene.frame_start = start
bpy.context.scene.frame_end = end
def generate_crude_walk_cycle(arm_obj, start=1, end=60):
# Very simplified two-step walk where legs alternate
frames = end - start + 1
for f in range(start, end+1):
t = (f - start) / frames
phase = math.sin(t * math.pi * 2)
# front legs
set_pose_and_key(arm_obj, 'front_thigh.L', rot_euler=Euler((math.radians(-15*phase),0,0),'XYZ'), frame=f)
set_pose_and_key(arm_obj, 'front_thigh.R', rot_euler=Euler((math.radians(15*phase),0,0),'XYZ'), frame=f)
set_pose_and_key(arm_obj, 'front_shin.L', rot_euler=Euler((math.radians(10*phase),0,0),'XYZ'), frame=f)
set_pose_and_key(arm_obj, 'front_shin.R', rot_euler=Euler((math.radians(-10*phase),0,0),'XYZ'), frame=f)
# back legs opposite phase
set_pose_and_key(arm_obj, 'back_thigh.L', rot_euler=Euler((math.radians(15*phase),0,0),'XYZ'), frame=f)
set_pose_and_key(arm_obj, 'back_thigh.R', rot_euler=Euler((math.radians(-15*phase),0,0),'XYZ'), frame=f)
set_pose_and_key(arm_obj, 'back_shin.L', rot_euler=Euler((math.radians(-8*phase),0,0),'XYZ'), frame=f)
set_pose_and_key(arm_obj, 'back_shin.R', rot_euler=Euler((math.radians(8*phase),0,0),'XYZ'), frame=f)
bpy.context.scene.frame_start = start
bpy.context.scene.frame_end = end
# ---------------------------
# Placeholder untuk impor motion JSON/BVH (retargeting sederhana)
# ---------------------------
def apply_motion_from_json(arm_obj, json_path):
import json, math
from mathutils import Euler
try:
with open(json_path, 'r') as f:
data = json.load(f)
except Exception as e:
print('Gagal membuka motion JSON:', e)
return
for bone_name, frames in data.items():
for item in frames:
frame_num = int(item[0])
rot = item[1]
set_pose_and_key(arm_obj, bone_name, rot_euler=Euler((math.radians(rot[0]), math.radians(rot[1]), math.radians(rot[2])),'XYZ'), frame=frame_num)
# ---------------------------
# Scene helper: camera & light
# ---------------------------
def setup_camera_and_light():
# camera
bpy.ops.object.camera_add(location=(4.5, -6.0, 2.5), rotation=(math.radians(65), 0, math.radians(45)))
cam = bpy.context.active_object
bpy.context.scene.camera = cam
# light
bpy.ops.object.light_add(type='SUN', location=(5, -5, 6))
sun = bpy.context.active_object
sun.data.energy = 3.0
# ---------------------------
# MAIN
# ---------------------------
def main():
clear_scene()
setup_camera_and_light()
parts = build_cat_mesh((0,0,0))
arm = create_cat_armature()
parent_parts_to_armature(parts, arm)
# optional: save blend checkpoint
try:
bpy.ops.wm.save_mainfile(filepath='cat_character_checkpoint.blend')
except Exception as e:
print('Tidak bisa auto-save .blend di mode background:', e)
# generate idle animation 120 frames
generate_idle_animation(arm, start=1, end=120)
# also create a crude walk cycle as separate action (frames 1-60)
# if you want to test walk: uncomment next line
# generate_crude_walk_cycle(arm, start=1, end=60)
print('Selesai: Karakter kucing dibuat. Gunakan apply_motion_from_json(arm, path) bila ingin import motion JSON atau import BVH lalu retarget.')
if __name__ == '__main__':
main()
- From Blender UI — Open Blender → Scripting workspace → Open the downloaded
create_cat_character.py
→ click Run Script. - From Terminal / Command Line — run:
blender --background --python create_cat_character.py
What you can customize / next steps
- Improve the mesh: Replace primitives with sculpted or modeled meshes and keep the rig.
- Better rigging: Add IK/FK controls, control bones, and shape keys for facial expressions.
- AI motion: Export BVH/JSON from AI motion tools (DeepMotion, Mixamo, or pose-estimators) and use the script’s import placeholders to apply motion.
- Textures & materials: UV unwrap and apply PBR textures (can combine with Stable Diffusion / Dream Textures for stylized texture generation).
- Lipsync/voice: Add short vocalizations (meow) using TTS or recorded audio and generate visemes for mouth shapes.
0 Comments:
Post a Comment