I wanted a small cat living on my dock. Not a wallpaper, not a widget. Something that sat there while I worked and reacted to what my machine was doing.
One problem. I cannot draw. Not a little, not badly. At all.
A year ago that ends the idea. Now it does not, and the reason is worth being precise about. I did not use an image generator. There is no drawing in this project at any point. The cat is a program, and programs are the thing these models are actually good at.
How the cat is made
Blender has a Python API and a headless mode. You can run it with no interface at all:
Blender -b -P blender/build_cat.py
That command opens no window. It runs a script that builds a cat out of boxes, rigs it, points a fixed camera at it, and writes PNGs straight into the app's asset folder. So "design the character" becomes "write the script", and writing the script is a coding task I can hand to Claude and then review like any other diff.
The current state is 1,936 lines of Python across five build scripts, producing 72 sprites. Every pose is a separate script rather than a pose applied to one model, because a sitting cat needs different geometry, not a rotated standing one.
Changing her coat colour is editing three constants and running one command.
Blender writes into the Swift package directly. There is no sync step between renderer and app, because a sync step is a drift bug waiting to happen.
That is the part that genuinely feels new. Not that a model can make art, but that an entire visual asset pipeline can be expressed as code, which means it can be reviewed, diffed, reverted and regenerated. I have never opened Blender's interface on this project.
Queued reminders make her heavier. Machine load changes her posture.
The messy half
I want to be honest about the middle, because the demo above skips it.
There are 706 exploration renders sitting in this repo's ignored directories. Face studies, colourway sweeps, greyscale tests, palette sheets. That is not a figure of speech, it is a file count. Most of that is not clever iteration, it is me failing to say what I wanted.
The clearest example is the standing pose. Four rounds of me looking at a render, saying it was not right, and adjusting. In order, I got:
- A dog.
- A cow.
- A lamb.
- A rabbit. At this point I stopped.
Each round I nudged numbers that were genuinely off. Each round produced a different species. The model executed everything I asked for, competently and quickly, and the result got further from a cat every time.
The sleeping pose went the same way, and the commit log is the record: curled up, then the loaf position because curled was unreadable, then a real curve on the tail, then the value contrast it was missing, then stretched right out because curled and loafed both read as a rock, then a neck notch and a lower head, then three features on the outline instead of one flat line.
Seven rebuilds. None of them fixed it.
What actually moved it
Two things, and neither was a better prompt.
The first was a reference with numbers attached. I had generated a short clip of roughly the character I wanted with Google Veo, while testing whether an AI video model could produce usable sprites. It could not. But it had produced a cat that read as a cat, and I had been staring past it.
So instead of adjusting by eye a fifth time, I measured its proportions and ported the ratios. That inverted the model. I had been building a small head on a substantial body, the way a real cat is proportioned. The head needed to be bigger than the body is deep. It is the oversized-head trick every cartoon animal uses, and no amount of "make it more cat-like" was ever going to get there, because that instruction contains no number.
The deflating part came later. An oversized head on a small body is not a trick I stumbled into. Konrad Lorenz described it in 1943 and called it Kindchenschema: a large head relative to the body, big eyes set low on the skull, short thick limbs, rounded shapes. It is why infant mammals look the way they do, and animation has exploited it deliberately for a century. Mickey Mouse was progressively softened, head and eyes growing against the body, until he read as a mascot instead of vermin.
So the number I spent four passes failing to find had been written down for eighty years. I did not need a better model. I needed twenty minutes of reading before I started.
The second was a written contract instead of a preference. Every sprite is 640 by 512, with the ground line exactly 24 pixels above the bottom edge, so the app can anchor them all bottom-centre and trust she neither jumps nor resizes when she changes state. That is a paragraph in a file called SPRITE_CONTRACT.md. Once it existed, an entire class of alignment bugs stopped happening, because there was something to check against rather than something to eyeball.
Adjectives produced 706 renders. Measurements produced the cat.
The division of labour
The thing I would tell anyone starting a side project like this: the model is extremely good at the part you think is hard, and no help at all with the part you think is easy.
Writing 1,936 lines of Blender geometry code, getting the bone parenting right, handling the orthographic camera, wiring three body weights through a build script: fast, competent, better than I would have managed alone.
Deciding whether the thing on screen is a cat: entirely mine, and I was bad at it. Not because I could not see it was wrong. I could see it was wrong every single time. I could not say why in terms a script could act on, so I said things like "more cat-like", and got a lamb.
Here is the failure that made this concrete. In the very first render her tail was missing. Not misplaced, not clipped. Absent. It was in the model and not in the image.
The camera is orthographic, which I chose so she never changes scale between poses. An orthographic camera has no perspective to peek around things with. Her tail sat directly behind her body, so its screen footprint was entirely inside the body's, and it was simply gone. Offsetting it sideways until it cleared her outline brought it back.
Weeks later I moved the tail again, checked the profile view, and it looked right. I did not check the front view, where the tail now rose above her head line at dead centre and poked up through the gap between her ears. Head on, she had three ears. From the side, which is the angle I had been reviewing, it was invisible.
That bug is not an art problem or a taste problem. It is a fact about projection, and once I understood it I could state it as a rule the script could honour. Every problem I could convert into a rule got solved. Every problem I left as a feeling produced another render.
I was reviewing the wrong picture
One more, and it was entirely self-inflicted.
Blender renders her at 640 by 512. The app draws her at 160 by 128. That is a four to one reduction, and for weeks I reviewed the renders.
A seated profile pose passed review that way. It looked good. In the app it was an orange blob, because its tail did not quite clear its back line, and at quarter size that gap was the only thing separating a cat from a lump. At full size the tail was clearly separate and the pose was fine. The detail I was approving did not survive to the screen anyone would see it on.
At 160 by 128 the interior of the shape carries almost nothing. What you perceive is the outline: the tail standing clear of the back, the space between the front and back legs, the step down from head to shoulder. A standing cat has holes in it. That is most of why the idle pose works, and why seven rebuilds of a solid sleeping lump never did.
The fix was a build step, not a better eye. The pipeline now emits a contact sheet at display size, and that is what gets reviewed. Same class of change as the sprite contract: replace a judgement call with an artifact you can check.
Six of the eight states. Each one is its own build script.
What she does now
Two signals, deliberately independent. The count of incomplete reminders in my Reminders app maps to her body size across three weights. Sustained CPU or memory pressure changes her posture instead: she stops walking, hunches, and bristles. Because they are separate, she can be both at once, which is exactly the state of having too much queued on a machine that is already struggling.
She walks the dock, rests in a corner, and falls asleep when I have been away from the keyboard for a few minutes. The whole app is Swift with no third-party dependencies and asks for one permission.
The part worth stealing
It has genuinely never been easier to build the thing you idly want. What has not changed is that you still have to know what you want, precisely enough to say it in numbers or rules.
The cost of execution went to nearly zero. The cost of specification did not move at all. My 706 renders are almost entirely the second cost, paid in the currency of the first, which is exactly the mistake the cheapness invites you to make.
She is on my dock now. She gets heavier when my reminders pile up, hunches when the CPU is pinned, and falls asleep when I walk away. She is also, finally, recognisably a cat.