Blender Python Script – Create & Animate a Cute Cat Character


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

  1.  Download script from 
    https://drive.google.com/file/d/14ROcc3f2x8Q31jEode2HsHEl-HKRwRpE/view?usp=sharing
  2. 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()

 

  1. From Blender UI — Open Blender → Scripting workspace → Open the downloaded create_cat_character.py → click Run Script.
  2. 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