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.

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.

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.

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.

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.
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.pyprints 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.