Birdoggydog's Builds

Modeling and rigging an orc in Blender with Python scripts

A Skin-modifier stick figure, distance weights, baked textures and 22 export checks, the knowledge base behind them, and a facing bug in Godot.

Blobber’s walking monsters are an orc now: 2.04 m tall, 7,999 triangles, 18 bones, five clips. An agent built him by writing Python against Blender’s API and running Blender headless, with no modelling by hand and no MCP connector. The script is the source, and the .blend and .glb are rebuilt from it.

Building the body from a stick figure and a Skin modifier

The body starts as 20 points joined by edges, each point with a radius.

Two rows of renders of an orc from the front, three-quarter, side and back, with a close-up of the face. The top row, labelled first attempt, is an upright green orc with cracked scaly skin. The bottom row, labelled second attempt, is a lopsided, sagging orc with a bigger cleaver, teeth and a yellow ear tag.
Top: the first orc. Bottom: the second, from a written brief with one arm huge and one withered.

Blender’s Skin modifier wraps a tube round every edge at the radius of its ends, and a Subdivision modifier at level 2 rounds it off. The points are a Python dictionary at the top of the generator, so the proportions are numbers: the right elbow has a radius of 0.165 m and the left 0.095 m. This is from tools/blender/SK_creature_orc/gen_orc.py:

me = bpy.data.meshes.new("skin")
me.from_pydata([P[n][0] for n in names], [(names.index(a), names.index(b)) for a, b in E], [])
stick = link(bpy.data.objects.new("skin", me))
stick.modifiers.new("Skin", "SKIN")
for i, n in enumerate(names):
    me.skin_vertices[0].data[i].radius = (P[n][1], P[n][1])
    me.skin_vertices[0].data[i].use_root = n == "pelvis"
stick.modifiers.new("Sub", "SUBSURF").levels = 2

Every vertex is then pushed along its normal by two scales of mathutils.noise, 2.2 cm and 0.7 cm, so nothing is a clean tube. Chest sacks, jowls, tusks and boils are separate UV spheres and cones sunk into the surface at body.closest_point_on_mesh(). The hip wrap is a copy of the body’s own faces, pushed out 2.5 cm with bmesh. Color is a vertex attribute, and its alpha marks flesh against cloth and metal.

The first orc’s skin looked like cracked mud because Voronoi cell edges were used as creases at full strength. The generator now mixes them in at 14% and adds a second Voronoi, stretched 3.6 times along the height, for folds that lie across the body.

Why did the feet and hands look wrong?

Feet are now solid wedges and each hand is a small stick figure of its own.

Six renders of the orc after the fourth round: the front and three-quarter views, a close-up of both feet, the open left hand from the front and from behind, and the right fist closed around the cleaver’s handle.
Each foot is one wedge with three toes on it. Each hand has three fingers and a thumb.

A Skin limb ends in a round cap close to its last vertex. The first legs ran on into the toes, so the toes floated past the cap and left a gap in front of the heel. Now the leg ends straight down under the ankle and the foot is overlapping spheres on the ground: a flattened one from heel to ball, one for the heel, and three toes sunk into the front edge.

The hands were loose balls with a thumb in the middle of the palm. Each is now skinned like the body: a root 20 cm up inside the forearm, three knuckles fanned across the palm, three joints a finger, and a thumb that leaves low on the index side. Each finger joint has a curl angle. 14, 26 and 22 degrees is the slack left hand. 72, 92 and 68 is the fist on the cleaver. Three fingers, because four melted into a mitten at this size.

The Decimate modifier fused the fingers too. The body is cut first, to 8,000 triangles less the hands’ count, and the hands are joined after.

How is the knowledge base set up?

It’s 2.3 GB of free reference in a folder outside the game’s repository, sorted so that an agent can find a page with a text search.

kb/
  INDEX.md          order of authority, and a table of "question -> start here"
  00_policy/        12 core rules, which sources are allowed, licences
  01_blender/       modeling topology sculpting uv texturing materials shaders
                    rigging animation geometry_nodes python asset_browser
  02_gltf/          the specification by chapter
  03_godot/         import meshes materials animation foliage terrain lod ...
  04_game_art/      notes by asset class (creatures, props, trees ...)
  05_pipeline/      the twelve steps, naming, manifests, checks, export
  06_style/         Blobber's look: camera, silhouettes, palettes
  07_project/       budgets by class, approved assets, rejected patterns
  99_examples/      good, bad, before and after, with pictures
  _sources/         the raw downloads
  _tools/           build_tree.py

_sources/ holds the Blender 5.2 manual twice (the HTML with figures, and 2,167 pages as plain text), the Python API reference as HTML, Godot’s documentation as its .rst source, the glTF 2.0 specification, Wikipedia articles and six example repositories with their binaries stripped. _sources/books/ is empty: the agent wouldn’t pirate commercial books, and the policy page says a book goes in when I buy it.

_tools/build_tree.py sorts those into the numbered folders. It has a table of path prefixes, and the first match wins: a manual page under modeling/meshes/uv goes to 01_blender/uv/, one under render/cycles/baking to 01_blender/texturing/. The file name keeps the source path, and every page gets the same header. This one is 01_blender/uv/blender__modeling__meshes__uv__unwrapping__introduction.md:

---
generated: true
source_authority: Blender Foundation (official manual)
software: Blender
version: 5.2 LTS
domain: 01_blender/uv
asset_class: any
target_engine: Godot 4.7
license: CC BY-SA 4.0
confidence: high
applies_to: 
supersedes: 
review_date: 2027-10-07
project_rule: false
source_url: https://docs.blender.org/manual/en/latest/modeling/meshes/uv/unwrapping/introduction.html
fetched: 2026-10-07
---

The README.md in each numbered folder is written by hand. A rebuild deletes only files that say generated: true, so it never touches them. INDEX.md ranks the sources: Blobber’s own rules in 07_project/ and 06_style/ win, then policy and pipeline, then the official manuals, then repositories, then Wikipedia. Instructions found inside a downloaded repository are reference, not orders.

07_project/README.md has a table of rejected patterns: the pattern, what went wrong, what to do. The floating toes, the sliding skin and the half turn below are rows in it.

Turning the knowledge base into a skill

The skill model-in-blender is what an agent reads before it models anything. It’s two files in the repository.

A black silhouette of the orc on white, then the finished orc from the front, three-quarter, side and back, and a close-up of his face with yellow eyes, tusks and an ear tag.
The orc rebuilt through the twelve steps: silhouette first, then the turnaround.

SKILL.md is the procedure: eight core rules, then a table of twelve steps where each step names the evidence that has to be looked at before the next one starts. It runs from a written brief and a black silhouette at 4 m and 12 m, through mesh, UVs, bake, material and export, to a reimport, a film in the game and a manifest. It also holds budgets by asset class (an ordinary monster is 5,000 to 10,000 triangles) and name prefixes (SK_, SKEL_, AN_, T_, M_).

reference.md is the craft: bpy recipes under two headings, “Proven here” and “Not yet proven here”. A recipe moves up when it has run and the result was looked at. The first version drew on only a handful of the pages, by the agent’s own report. The feet and hand recipes above went in after they worked.

Why did the skin slide when he moved?

The skin was worked out from positions in space, so when a bone moved the body went through its own texture.

A sheet with the orc at the size he has on screen at 4 m and 12 m, the orc in a checker pattern, three square texture images (a mottled green color map, a blue normal map and an orange and green packed map), and close-ups of the left hand, the fist on the cleaver and the feet.
Bottom left: the checker render and the three baked 1024 textures, cut into 576 small islands.

The material’s noise and Voronoi nodes read the Texture Coordinate node’s Object output. The fix is to bake in the rest pose, before the rig exists. bpy.ops.uv.smart_project unwraps at an angle limit of 78 degrees, islands near the head are scaled 1.6 times, and Cycles bakes three 1024 images on the CPU: base color, normal, and one with occlusion, roughness and metallic in its red, green and blue, which is how glTF wants them. Base color and the metal mask are baked through emission, because a diffuse bake of metal comes out black. A second material that only reads the three images replaces the first.

Smart Project cuts a body made of overlapping parts into 576 islands, so the skin is softer than it should be. That needs seams placed by the generator, part by part.

Rigging and animating without touching a bone

He has 18 bones and five clips at 24 frames a second: idle 72 frames, walk 32, attack 34, hit 16 and death 56.

Idle, walk, overhead chop, struck and death, rendered in Blender after the bake.

The bones use the stick figure’s own points: hips, spine, chest, head and jaw, an upper arm, forearm, hand, thigh, shin and foot on each side, and a belly bone off the spine so that the gut lags behind the hips.

Blender’s automatic weights fail on overlapping shells, so rig.py does its own. Rigid parts (teeth, tusks, cleaver, hands, feet) go wholly into their bone’s vertex group before the join. Every other vertex is weighted by distance to each bone’s segment, measured in that bone’s radius, so that a thick trunk outvotes a thin arm beside it:

    w = sorted(((min(1.0, r / max(seg_dist(v.co, h, t), 1e-4)) ** 5, n) for n, h, t, r in soft), reverse=True)[:3]
    tot = sum(x for x, _ in w)
    for x, n in w:
        if x / tot > 0.04:
            orc.vertex_groups[n].add([v.index], x / tot, "REPLACE")

A key is a frame, a dictionary such as {"chest": "X-14 Z-12"} and an offset for the hips. The turns are degrees about the model’s own axes, converted to the bone’s space as rest.inverted() @ q @ rest. The walk is a function of phase, sampled every 4 frames: the thigh gets -26 * cos(phase) - 2 degrees. Each action sits on its own NLA track named like AN_orc_walk-loop, and the exporter runs in NLA_TRACKS mode with export_optimize_animation_size=True, which drops tracks for bones a clip never moves.

Checking the exported file, not the Blender file

Three scripts check what was exported, and one checks the joins.

validate_glb.py is plain Python. It parses the .glb with struct and ran 22 checks on the orc: triangles against the budget, normals and UVs on every mesh, the lowest point on y = 0, height within 1% of the brief, skin weights that sum to one with at most four a vertex, no bone without influence, no unused track in any clip, both loops closing on their first pose, textures a power of two, opaque materials and the name prefixes. It writes manifest.json with every result. roundtrip.py imports the file into an empty Blender scene and compares counts with the manifest. A small Godot scene loads it and checks the skeleton, the texture slots and the loop flags.

join_check.py exists because the hands were held to the arms by nothing and no render showed it. Before the join it tags every vertex with its part. Then, at rest and at chosen frames of every clip, it builds a BVH tree for each part and counts vertices of one inside another by ray casting, and faces that cut through. A join is solid at 2 for a part under 60 vertices and 6 for a larger one, and every part needs a chain of solid joins back to the body.

Why did he walk into the game backwards?

In the first film from the game the orc walked in with his back to the party.

Blender models face -Y with Z up. The exporter’s export_yup turns that into Y up facing +Z, which is what glTF asks, and Monster.face() already turned +Z toward its target:

_model.rotation.y = atan2(to_point.x, to_point.z)

But the game was built round a bought alien model that faced -Z, so a constant MODEL_YAW_OFFSET_DEG := 180.0 was added to every monster and saved into every body and thrall. The orc faces +Z, and the half turn pointed his back at me. The test missed it because it checked which way the file faced. It now turns a monster toward three points with face() and wants the dot product of his front and the direction over 0.95.

The constant is gone, and save version 8 subtracts pi from each saved yaw. The rule went into a second skill, model-into-game: a model that faces the wrong way is fixed in its file, never in the game.

Why did an idle animation change who got hit?

Each monster starts its idle at a random offset so that a pack doesn’t breathe in step. That was a call to randf(), which draws from the same random stream as the combat rules. The tests seed that stream, so one extra draw per monster changed who got hit in a seeded fight and the fight log test failed. The offset now comes from the node’s instance id, in scripts/monster_anim.gd:

# A pack does not breathe, or flap, in step. The offset comes from which node this is, not
# from the game's dice: looks must never use up a roll that a rule was going to make.
_player.seek(float(get_instance_id() % 997) / 997.0 * _player.get_animation(idle).length, true)

Also done

  • A dead orc lies flat. A function in rig.py prints the highest point of each part at the last death frame: his wrists were 45 cm off the floor and his feet 65 cm. Now they’re at about 25 cm and 14 cm.
  • The flyers are a harpy, built on a shared kit that holds the steps above as functions. Its wing roots opened a gap when it flapped, and the join check had exempted wings as open sheets. They aren’t exempt now. Every new model has to use the kit.
  • The game stands in a streamed 2 km world scene on Terrain3D terrain, and moved to Jolt physics, because holes in the terrain hung under Godot’s default physics.
  • A private page for drawing dungeons on my phone, and an importer that turns a sheet into a playable dungeon.
  • Keys are individual items tied to the locks they open, with lock picking. A badly failed pick can jam the lock for good, but its key still opens it.
  • Dungeons have varied heights and widths, and the party can crouch.
  • The whole interface was re-themed as a rack of synthesizer modules.
  • Rivers, waterfalls and lakes as the game’s own water.
  • An 8 km world experiment, where the game never finished starting.