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:
- “End-to-end” meant a live local Blender process, not a render farm or a fake screenshot.
- Sponsor segments in the transcript were ignored.
- The Cursor workspace open in the IDE was the hub repo
cursor-origin. The Blender files lived in a sibling folder. The agent worked in the sibling folder anyway. That split is easy to miss and easy to commit into the wrong repo. - Customer-facing GitHub README is Markdown because GitHub renders
README.md. House rule “customer-facing docs are HTML” still applies to this journey page and the shop.
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 Cursor agent transcript for this chat (jsonl). User prompts above are quoted from that file.
- Git history on
main:0f04bbfdonut publish →8101d38Pages players →b29110cmuted autoplay →68f5abbpizza ship →d5ef09dpizza stills →0b57ef7pineapple timing. - Current artifacts:
pizza.blend,scripts/build_pizza.py,export/pizza.glb,web/js/viewer.js.
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.
| Actor | What it actually did | What 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:
get_addon_status— reportsblender_version, protocol, whether the addon matches the Cursor client.- Launch the GUI with
blender.exe --python scripts/start_mcp.py(not headless-b). - That script enables
blender_mcpand registers a timer that callsbpy.ops.blendermcp.start_server(). - Stdout must print
MCP_SERVER_STARTED.MCP_START_FAILplus 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
| Call | What crosses the wire | What 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
| Channel | What comes back | How 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
- If MCP returns text, that text is from this Blender, this file, this frame — unless a race moved the playhead under the screenshot.
- If MCP is down, guessing the scene from the last screenshot is fiction. Relaunch.
- Files on disk can lag the live scene until
wm.save_as_mainfile/export_scene.gltfruns. The studio plays the GLB, not the unsaved .blend.
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:
scripts/start_mcp.py— enableblender_mcp, start the server.scripts/build_donut.py/scripts/build_pizza.py— procedural scene + animation + export.scripts/render_video.py— EEVEE stills or FFMPEG; on this install the FFMPEG enum throwsTypeError, so PNG sequence + ffmpeg.web/js/viewer.js— one mixer, merged glTF clips, Play rewinds viasetProgress(0), dt clamped to 1/30s.
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
| Tool | What it did here |
|---|---|
Blender MCP execute_blender_code | Build, bake, export, sample frame Z. Every call repeated the user’s current prompt verbatim. |
Blender MCP get_viewport_screenshot | Lookdev and the frame-22 rain check. Once raced a parallel frame_set and photographed the wrong pose. |
Blender MCP bpy_api_lookup | glTF export enums on 5.2: export_animation_mode, SCENE, export_bake_animation. |
| cursor-ide-browser | GitHub README click-through; local studio Play; CDP Runtime.evaluate for mixer time and node positions. |
gh + GitHub MCP | Create/push az9713/cursor-blender-animation-demo. Confirm README HTML table went live. |
| ffmpeg + PIL | 1280-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.server | Local 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 WSL | Native 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
- MCP down. Blender GUI closed. Fix: relaunch 5.2 with
start_mcp.py, wait forMCP_SERVER_STARTED. - Three.js r160. Studio status showed OrbitControls missing.
OrbitControlsis not on the globalTHREEobject. Fix: import map + ES modules. - AnimationMixer
setTimeon a paused clip. Shop scroll did not pose the mesh. Fix: advance the mixer in the running loop, then pause. Shop scrubs that way. - FFMPEG enum
TypeError. EEVEE movie output unavailable. Fix: PNG sequence, then ffmpeg. - Blender 5.2 actions.
Action.fcurvesis gone;channelbagneeds a slot. Kinematic bool keys skipped an interpolation loop. Fix: layered action API; bake visual motion for export. bake_to_keyframeswithout VIEW_3D. Operator failed in a generic override. Fix:temp_overrideon a 3D viewport region.- 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.
- Personal info. Early scripts hardcoded a local path and printed
USER_PROMPT. Fix:Path(__file__).resolve().parent.parent; strip before public push.transcript.txtstays gitignored. - 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. - Kinematic hold 40–55. User-visible freeze. Fix: start
8 + (i % 3). - Animated pizza collider.
pine_maxat frame 80 ≈ 58, some chunks at Z ≈ −23. Fix: collider stays static at the plate. - glTF Z vs Y. Nearly shipped a “GLB is empty” theory. Fix: inspect the swizzled height channel.
- Play skips to the end. Mixer time 6.36s. Fix: rewind via
setProgress(0), clamp dt. - 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
- Blender frame 22 after the timing fix: pizza on the plate, pineapple mid-air and spread, not a spawn cloud. Viewport screenshot.
- GLB pineapple translation Y falling by t ≈ 0.67–0.92s while PizzaRoot Y is already near the plate.
- GitHub README after
d5ef09d: pizza posters side by side; click opened Pages player with a 4s Blender clip. - Local studio after Play: pizza and pineapple both in the air, then later settled with chunks on plate and table.
- Shop: pesto recolor, scroll HUD to settled pineapple (earlier in the session, donut-era and first pizza bake).
- README clips recaptured after the bake: Blender EEVEE frame 22 shows pizza on the plate and pineapple falling; studio poster shows the same beat with HUD “Playing drop + pineapple rain.”
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:
- Donut sprinkles remain keyframed, not rigid. Out of scope after the pizza swap.
Origin and GitHub both have this repo. Native Windows git push cursor still needs a credential dialog; WSL git is the working path.