blender demo journey Shop · Studio · README · GitHub

Development journey — warts and all

Transcript in, pizza shop out, via a live Blender process

This is the story of how Cursor Grok 4.6 actually built az9713/cursor-blender-animation-demo: a YouTube transcript, a Blender 5.2 GUI that was not listening, a donut that became a pizza, GitHub stills that would not play, and pineapple that hovered because the agent keyed the wrong frames and then misread glTF axes.

The failures are the product. A victory lap would hide the only evidence that matters: the LLM never modeled in Blender by “drawing.” It wrote bpy into a TCP bridge, looked at screenshots, and was wrong more than once.

Date written
2026-09-16
Deliverable
https://az9713.github.io/cursor-blender-animation-demo/ — pizza shop + studio + README players
This file
development_journey.html at repo root. Customer-facing docs in this house are HTML. The invoked skill defaults to DEVELOPMENT-JOURNEY.md; the filename follows the user request and the repo convention.
Model
Cursor Grok 4.6, inline in one Agent chat. No subagents were spawned for the Blender work.
First prompt, quoted
“Read <local-blender-folder>/transcript.txt. Can you work with Blender (installed) to complete the project end-to-end as described in the transcript.txt.”
This document’s brief, quoted
“Use Claude "/dev-journey" skill to write a development journey html capturing every step in your development process. Capture the roles of the Model, Cursor, Blender ... and every component in the tool chain. Capture the design decisions, issues and fixes. Surface unknown unknowns to this human about this process and address them in the html also. commit and push to both Github and Origin the entire repo. ask me if any question”

1. The brief — a video, not a spec

The session did not start as a GitHub showcase. It started as:

Read <local-blender-folder>/transcript.txt. Can you work with Blender (installed) to complete the project end-to-end as described in the transcript.txt.

The transcript was a paste of The BEST Way to Build an App with ChatGPT-6 Astra (Van Zyl). The video connects an agent to Blender, models a donut, animates a drop, exports, then puts the asset on a shop page. This project follows that outline on Cursor, not ChatGPT Desktop.

Silent interpretations that shaped everything after:

Later briefs, quoted:

excellent. please create a README.md: mention the YT video as inspiration. mention the whole project was done by Cursor end-to-end (including which model was used). … compress the two *.mp4 and insert screenshots in README.md side by side). Clicking those screenshots will play those two videos. … Strip any personal info. use gh cli. name the repo github.com/az9713/cursor-blender-animation-demo.
Great. can you create exactly same end-to-end workflow but replacing donut with a pepperoni pizza and sprinkles with chunks of pineapple?
the physics must be correct. i want some bouncing for the fallen pineapple chunks
in the web site animation, the pineapple chunks are frozen in space after the pizza drops on the surface. this is wrong physics. either they all fall together or at most a very brief pause. … i also do not think the web site animation strictly follow the blender animation

The last of those was the session’s real crux. See §8.

2. Evidence — and what was reconstructed

This document was written at the end of the session that did the work. Primary sources:

The chat hit a context-summary once, mid pizza-README-video fix. Tool logs from before that cut exist in the jsonl and in the summary, not as a continuous working memory. Anything that only lived in a wiped scratchpad is marked reconstructed. Silent reconstruction would be a failed journey doc.

Personal machine paths, the raw transcript.txt, and original ~11 MB / ~20 MB screen recordings are gitignored on purpose. They are not in the public repo. This page does not reprint them.

3. Who did what — model, Cursor, Blender, human

If you only look at the shop, it looks like “an AI made a 3D website.” The actual split is narrower and stranger.

ActorWhat it actually didWhat it did not do
Human Pointed at a transcript. Later asked for README, GitHub name, pizza swap, bounce, side-by-side videos, then this journey. Clicked nothing in Blender. Corrected physics by screenshot. Did not model, keyframe, export, or write the shop HTML.
Cursor Grok 4.6 (this agent) Read the transcript as a brief. Wrote Python that Blender executed. Wrote the shop. Compressed MP4s. Used gh. Debugged by viewport screenshot and GitHub Pages click-through. Did not emit polygons in chat. Did not spawn subagents for this demo. Did not push to Origin until this document (the GitHub remote was the only remote).
Cursor IDE Hosted the Agent loop, MCP clients (user-blender, cursor-ide-browser, GitHub), the transcript, and the editor. Workspace root was the hub repo; the demo lived next door. Did not run Blender. Closing the Blender GUI kills the MCP server even if Cursor stays open.
Blender 5.2.1 LTS Owned the scene graph, EEVEE, rigid-body world, keyframes, GLB export. Viewport screenshots were the test. Did not talk to GitHub. Headless bake of bake_to_keyframes failed without a VIEW_3D override — the GUI had to be up.
Blender MCP addon TCP bridge on port 9876. execute_blender_code runs bpy inside the live process. Requires the user’s current prompt quoted verbatim on every call (user_prompt). Does not persist if Blender quits. Does not understand “the scene I meant.”
Three.js r160 + browser Plays the GLB. Shop scrubs on scroll. Studio orbits and plays. GitHub Pages is the only place committed MP4s actually play. Does not simulate physics. If the GLB keys are wrong or Play skips the clip, the site lies.
GitHub + Pages Public repo, README stills, Pages MIME for MP4/HTML. Blob viewer does not play MP4s. Relative .mp4 links from README dump “View raw.”

Rule learned: the LLM never draws in Blender; it writes bpy that a live process runs, then looks.

4. The Blender seams — what actually crosses

This is the interface the rest of the stack is built on. There is no shared memory, no “agent viewport,” and no mesh streaming. Cursor and Blender are two processes. They talk in three ways only: a TCP RPC, files on disk, and still images.

Cursor Agent (Grok 4.6)
   │  MCP tools in namespace user-blender
   │  (execute_blender_code, get_viewport_screenshot,
   │   get_scene_info, get_object_info, bpy_api_lookup,
   │   get_addon_status)
   ▼
TCP port 9876
   │  blender_mcp addon, protocol 7, addon 1.7
   ▼
Blender 5.2.1 LTS GUI process
   │  bpy / operators / depsgraph / Bullet / EEVEE
   ├── print(...)  ──────────────► tool result text
   ├── viewport pixels ──────────► get_viewport_screenshot
   └── .blend / .glb / PNG ──────► filesystem (Cursor Read/Shell)

The website never calls Blender. Once export/pizza.glb is on disk, the seam is done. Three.js plays keys. If those keys are wrong, the shop cannot “ask Blender again” at runtime.

Bring-up — making the TCP side exist

Blender installed ≠ Blender listening. This session’s bring-up was:

  1. get_addon_status — reports blender_version, protocol, whether the addon matches the Cursor client.
  2. Launch the GUI with blender.exe --python scripts/start_mcp.py (not headless -b).
  3. That script enables blender_mcp and registers a timer that calls bpy.ops.blendermcp.start_server().
  4. Stdout must print MCP_SERVER_STARTED. MCP_START_FAIL plus an exception means the Cursor client is talking to nothing.

Closing the Blender window tears the server down. Cursor can stay open. The next MCP call fails until the GUI is launched again. Probe the port; do not trust a visible IDE.

Into Blender — commands

CallWhat crosses the wireWhat Blender does
execute_blender_code A Python string in code, plus user_prompt: the human’s current message, quoted verbatim, on every call. Runs that string inside the live bpy interpreter. print comes back as the tool result. Exceptions come back as failures.
bpy_api_lookup A dotted RNA path, e.g. bpy.ops.export_scene.gltf. Returns real argument names, enums, defaults from this 5.2.1 binary — not from training data.
get_scene_info / get_object_info Optional object name. Structured dump of what already exists (objects, materials). Used before writing code so scripts do not assume an empty file.
Importing a repo script Not bytes over TCP. The MCP code string does importlib on scripts/build_pizza.py already on disk. Blender executes the file in-process. That is a second inbound path: Cursor’s Write tool edits the file; MCP only says “run it.”

user_prompt is a seam constraint, not documentation. The addon uses it to bind a sequence of calls to one human intent. If the agent paraphrases (“inspect the pizza keys”) while the human said “pineapple chunks are frozen,” the server can treat it as a new goal. Every pizza-fix call in this session repeated the freeze complaint in full.

What does not go in: polygons, sculpt strokes, mouse events in the 3D view, or a live video of the viewport. The agent never clicked Blender’s UI.

Out of Blender — evidence

ChannelWhat comes backHow this session used it
Tool stdout Text after Code executed successfully: Frame-by-frame Z for PizzaRoot and pineapple (F22 pizza=0.190 fly=28 min=1.049). The numbers that proved the hover was real in Blender.
get_viewport_screenshot A raster of the current 3D view, attached as an image the model can see. Lookdev (does it look like a pizza). Later, frame 22 rain. Once raced a parallel frame_set and photographed the wrong pose.
Filesystem pizza.blend, export/pizza.glb, EEVEE PNG frames. MCP does not stream these; Blender writes them and Cursor reads the path. GLB inspection in Python (accessor Y vs Z). ffmpeg of frames. Git commit of the GLB the website loads.
get_addon_status JSON: version, protocol, up_to_date. Confirm 5.2.1 LTS and protocol 7 before baking.

A screenshot is not a playback. A print of Z at one frame is not a GLB. The agent had to use all three outbound channels and still misread glTF Y-up. The seam can be working and the conclusion still wrong.

Operators that only work on one side of the glass

bpy.ops.rigidbody.bake_to_keyframes refused a generic context. It needed bpy.context.temp_override(window, area, region) with a VIEW_3D area and a WINDOW region — i.e. a real GUI viewport. That is why the session launched the Blender window instead of blender -b. Headless can run bpy that only touches data. It cannot fake that override if no 3D view exists.

bpy.ops.export_scene.gltf is also an operator on this seam. Its 5.2 enums (export_animation_mode=SCENE, export_bake_animation) were looked up with bpy_api_lookup, not remembered. The exporter still emitted 54 clips. That failure left Blender correctly and became a Three.js problem. The seam can hand you a valid GLB that is a bad product.

What the model is allowed to believe

Rule learned: treat Blender as a server with three response types — string, image, file — and never collapse them into one “the 3D looks fine.”

5. The toolchain — data flow

YouTube (Van Zyl) → pasted transcript.txt
        → Cursor agent (Grok 4.6)
        → Blender MCP (TCP 9876)
        → Blender 5.2.1 GUI
             → pizza.blend / donut.blend
             → rigid-body bake → keyframes
             → export/pizza.glb
             → EEVEE PNG frames → ffmpeg H.264
        → web/ (Three.js r160, import map)
        → python -m http.server (local proof)
        → cursor-ide-browser (click Play, change sauce, scroll)
        → gh → github.com/az9713/cursor-blender-animation-demo
        → GitHub Pages (shop + /play/*.html players)
        → Origin (cursor.com hosted copy; added with this journey)

Load-bearing scripts now in the repo:

Descoped on purpose: no Blender Cloud, no Meshy, no Unity, no extra npm in this repo. Playwright was installed in a temp directory to capture the studio MP4, then discarded. An accidental npm install in the hub workspace was gitignored, not committed.

6. Cold start — Blender was installed and silent

At minute zero: Blender 5.2.1 existed on disk. The MCP addon was not listening. Cursor already had a user-blender client. The first useful move was not modeling. It was get_addon_status, then launching the GUI with scripts/start_mcp.py until the log printed MCP_SERVER_STARTED.

When the human later closed the Blender window, MCP died. The agent had to relaunch. Probe the port; do not trust “Blender is installed.”

Reused instead of re-derived: the donut builder became the pizza builder (same drop/squash on a parent empty, same plate/table, toppings swapped). That reuse is why pineapple inherited the sprinkle stagger 40 + (i % 16) — a donut-era number that made no physical sense once the user could see a top-down studio camera.

7. Design decisions — options that lost

Live Blender vs headless vs fake GLB

Options: generate a GLB from Three.js primitives and pretend; run Blender headless; drive the GUI via MCP. Chosen: GUI + MCP, because viewport screenshots are the only cheap test that the mesh looks like food. Headless lost for bake_to_keyframes (needs a VIEW_3D override). Fake GLB lost because the brief was “work with Blender (installed).”

Donut kept after pizza

Options: overwrite the donut; second repo; keep both in one repo. Chosen: one repo, pizza is live, donut remains as the first pass. The user asked to push pizza to “the same repo as the donut project.”

README videos

Options: raw MP4 in README; YouTube; GitHub Pages player pages. First instinct (relative MP4) lost: GitHub’s blob viewer does not play committed MP4s. Chosen: poster JPEGs with a play badge, linking to play/*.html on Pages, muted + autoplay so the click actually starts the clip.

Physics: keyframed rain vs rigid body

Donut sprinkles were keyframed (scale 0 until start, then bezier to a rest pose). Pizza pineapple was asked to bounce. Options: fake bounce with two keyframes; Bullet rigid body, bake to keys, export. Chosen: active BOX bodies, restitution ~0.62, bake, delete the hidden pizza collider after bake so it does not ship in the GLB.

When pineapple is released

First instinct: hold kinematic until frames 40–55 so chunks do not hit a static plate collider while the pizza is still in the air. That looked fine in Blender’s side camera. It looked like a frozen cloud in the website’s high camera. User rejected it. Parenting a moving collider to the pizza and releasing at frame 1 exploded some chunks (max Z ~58). Chosen: static collider at the plate, release at 8 + (i % 3) so they are already falling when the pie lands, and they hit the collider only after the pizza is there.

One Three.js clip vs 54 glTF actions

Blender 5.2’s glTF exporter, even in SCENE mode, emitted 54 clips (each object). Playing them all with LoopOnce plus a seek to 82% made Play fail to rewind. Chosen: merge tracks into one AnimationClip('Scene') in viewer.js, and implement Play as setProgress(0) then unpause.

What was not built

No UV-painted crust, no oven lighting, no audio, no cart, no account. The pizza is primitives. YAGNI: a second physics engine in the browser. The GLB is the physics, baked.

8. The pineapple freeze — this session’s actual problem

The user sent a studio screenshot: pizza already on the plate, pineapple still a tight cloud in the air, HUD reading “Playing drop + pineapple rain.” They said Blender did not show that long pause.

Two things were true at once, and the agent initially mixed them up.

The hold was real in Blender

Evaluating the live scene at frames 1, 22, 36, 40, 55 showed pizza landing at frame 22 and pineapple still at Z ≈ 3.0 until frame 40. The Blender camera sits low and to the side, so a hovering cluster sits near the top of frame and reads as “rain about to happen.” The website camera looks down. Same keys, different lie.

Rule learned: a side camera can hide a timing bug that a product camera will prosecute.

The agent then misread the GLB

glTF is Y-up. Blender is Z-up. The exporter swizzles (x, y, z) → (x, z, -y). A Python inspector printed translation [2] and concluded PizzaRoot never dropped (Z stayed 0). Height was on [1] the whole time. Wrong conclusion nearly drawn: “the GLB is missing the drop.” Saved by printing Y as well: PizzaRoot Y went 3.55 → 0.18; pineapple Y later fell. The website was playing the bake. The bake was the problem.

Rule learned: read the up axis of the file you are inspecting, not the DCC you exported from.

A third bug only showed up in the agent’s browser

After the bake was fixed, Play in the Cursor-owned browser still showed the settled pose. Mixer time jumped to 6.36s on a 3.96s LoopOnce clip. THREE.Clock.getDelta() keeps ticking in requestAnimationFrame. When the tab is backgrounded during tool calls, the next Play frame can be several seconds of delta and the clip teleports to the end. The human, watching a focused tab, had seen the hover, not the skip. The agent almost “verified” a false fix.

Fix: merge clips; Play calls setProgress(0) then unpauses; clamp dt to 1/30; discard clock backlog on Play. Immediate screenshot after Play then showed pizza and pineapple in the air together. A later screenshot showed them settled with bounce. That is the check.

9. Tools and features used this session

ToolWhat it did here
Blender MCP execute_blender_codeBuild, bake, export, sample frame Z. Every call repeated the user’s current prompt verbatim.
Blender MCP get_viewport_screenshotLookdev and the frame-22 rain check. Once raced a parallel frame_set and photographed the wrong pose.
Blender MCP bpy_api_lookupglTF export enums on 5.2: export_animation_mode, SCENE, export_bake_animation.
cursor-ide-browserGitHub README click-through; local studio Play; CDP Runtime.evaluate for mixer time and node positions.
gh + GitHub MCPCreate/push az9713/cursor-blender-animation-demo. Confirm README HTML table went live.
ffmpeg + PIL1280-wide H.264 CRF 26; JPEG posters with a play badge. ffmpeg JPEG needed -update 1 on this machine.
Playwright (temp dir)Record studio WebM → MP4. Chromium revision mismatch (Playwright 1.49 wanted 1148, install got 1243) — reconstructed from session summary.
python -m http.serverLocal proof. Port 8090 later bound in a way Git-bash curl could not reach; 8091 with --bind 127.0.0.1 returned 200.
Origin CLI in WSLNative Windows cannot install Origin. WSL already had a valid Origin login. Used to create and push the Cursor-hosted copy.

Subagents: none for this demo. The same Cursor conversation had earlier hub work that did spawn explore agents; that is a different deliverable (az9713/cursor-origin already has its own journey HTML).

10. What went wrong, and the fixes

  1. MCP down. Blender GUI closed. Fix: relaunch 5.2 with start_mcp.py, wait for MCP_SERVER_STARTED.
  2. Three.js r160. Studio status showed OrbitControls missing. OrbitControls is not on the global THREE object. Fix: import map + ES modules.
  3. AnimationMixer setTime on a paused clip. Shop scroll did not pose the mesh. Fix: advance the mixer in the running loop, then pause. Shop scrubs that way.
  4. FFMPEG enum TypeError. EEVEE movie output unavailable. Fix: PNG sequence, then ffmpeg.
  5. Blender 5.2 actions. Action.fcurves is gone; channelbag needs a slot. Kinematic bool keys skipped an interpolation loop. Fix: layered action API; bake visual motion for export.
  6. bake_to_keyframes without VIEW_3D. Operator failed in a generic override. Fix: temp_override on a 3D viewport region.
  7. GitHub README MP4s. Both stills opened blob “View raw.” Fix: Pages players; then muted autoplay because the second click still did not start audio-blocked video.
  8. Personal info. Early scripts hardcoded a local path and printed USER_PROMPT. Fix: Path(__file__).resolve().parent.parent; strip before public push. transcript.txt stays gitignored.
  9. Pizza videos missing from README. Assets existed locally; the table was still donut-only. User had to say so. Fix: pizza table first, donut below, push d5ef09d.
  10. Kinematic hold 40–55. User-visible freeze. Fix: start 8 + (i % 3).
  11. Animated pizza collider. pine_max at frame 80 ≈ 58, some chunks at Z ≈ −23. Fix: collider stays static at the plate.
  12. glTF Z vs Y. Nearly shipped a “GLB is empty” theory. Fix: inspect the swizzled height channel.
  13. Play skips to the end. Mixer time 6.36s. Fix: rewind via setProgress(0), clamp dt.
  14. Context overflow. Session summarized mid-work. Pizza stills were not on GitHub yet. Continuation had to finish that push from the summary, not from live memory.

11. Verification — observed, not hoped

The first README website MP4 was recorded against the hover, then against a Play click that never bound (status text already said “orbit” in the HTML). Headless Playwright also runs slower than 30fps, so the viewer’s dt clamp turned a 4s clip into slow motion. The shipped website MP4 is an 18s Play capture, trimmed from the drop through the land, then time-compressed to 4s so it matches the Blender recording.

12. Unknown unknowns — the process, not the pizza

These are the facts a human watching only the shop would not know. They are the actual cost of “an agent did it.”

The workspace you see is not always the repo being shipped

Cursor was open on the hub. The demo lived in a sibling directory. Agents follow the files, not the window title. Dual-repo sessions need an explicit “do not commit the hub” rule. This session gitignored accidental hub node_modules and did not push them.

MCP is a live process, not a library

Closing Blender is equivalent to unplugging the renderer. The addon version must match the MCP client (here protocol 7, addon 1.7). Every execute_blender_code call must carry the user’s current sentence in user_prompt or the server treats it as a new intent. That is why pizza-fix calls still quoted the freeze complaint in full.

Blender 5.2 animation is not the Blender 3.x exporter in your training data

Slotted actions, channelbag(slot), EEVEE Next enum names, FFMPEG missing from RNA. Copy-pasted 3.x snippets fail closed. Look up the operator on the running binary.

Baking physics is not the same as exporting physics

The website cannot run Bullet. It plays keys. If you delay those keys so the bake is stable, the product camera will call it a freeze. If you parent a collider to a keyed pizza, Bullet will launch debris. The export format then adds Y-up. Three independent coordinate systems: Blender world, glTF, Three.js mixer time.

GitHub is not a video host

Committed MP4s on main still do not play in the README file viewer. Pages with an HTML <video> tag is a different product. Autoplay without muted is a third product, blocked by the browser.

The agent’s browser is a hostile witness

Background tabs inflate getDelta(). A screenshot of a canvas is not a playback test. Parallel MCP screenshot + frame_set races. Click Play, then read mixer time and a named node’s position, then photograph.

Origin is not GitHub

House standing order: “commit and push means both.” This demo’s git remote named origin was GitHub. There was no Cursor Origin remote until this document. Native Windows cannot run the Origin CLI; WSL can. Creating the Origin repo is a separate step from gh repo create. Do not rename the GitHub remote to point at Origin.

A context summary is a lossy codec

When the chat compacted, the pizza posters existed on disk and not on GitHub. The summary said so. Without it, a new agent might have rebuilt Blender instead of editing the README table. Journey docs exist because that codec will run again.

This does not prove a rigger is obsolete

The pizza is discs, a torus crust, cubes, and a baked rain. The loop is the artifact: transcript → MCP → screenshot → GLB → HTML → a human who can see a freeze the side camera hid.

13. Where things stand

Live shop: az9713.github.io/cursor-blender-animation-demo

Studio: web/studio.html

GitHub: az9713/cursor-blender-animation-demo

This page: development_journey.html

Unfinished, with reasons:

Origin and GitHub both have this repo. Native Windows git push cursor still needs a credential dialog; WSL git is the working path.