<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Digital Garden by Alicia]]></title><description><![CDATA[Digital Garden by Alicia]]></description><link>https://digitalgarden-alicia.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 25 Oct 2026 23:44:23 GMT</lastBuildDate><atom:link href="https://digitalgarden-alicia.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Claude Code for 3D Artists: A Real Houdini Pipeline Setup]]></title><description><![CDATA[Claude Code for 3D artists: a Houdini setup with a CLAUDE.md template, a hython fix loop, MCP risks, and a human check list before you trust output.]]></description><link>https://digitalgarden-alicia.hashnode.dev/claude-code-for-3d-artists-a-real-houdini-pipeline-setup</link><guid isPermaLink="true">https://digitalgarden-alicia.hashnode.dev/claude-code-for-3d-artists-a-real-houdini-pipeline-setup</guid><dc:creator><![CDATA[Alicia lu]]></dc:creator><pubDate>Sun, 25 Oct 2026 07:28:52 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>AI-assisted disclosure: Codex assisted with research, code and writing. Measurements, failure cases and limitations are retained from the original article.</p>
</blockquote>
<blockquote>
<p>Originally published by Alicia on 2026-10-14: <a href="https://digitalgarden.si/blog/claude-code-for-3d/">Digital Garden by Alicia</a>.</p>
</blockquote>
<p>When I'm building <a href="https://digitalgarden.si/">digitalgarden.si</a>, an <a href="https://digitalgarden.si/virtual-garden/">interactive 3D garden</a>, the slow part is rarely sculpting one more flower. Claude Code is what I use on the Houdini side, with hython in the terminal: exports come out with the wrong up-axis, USD layer names don't match, and a batch of assets all need the same attribute changed.</p>
<p>That's where Claude Code for 3D work earns its place. It reads and writes the scripts in your shot folder, runs <code>hython</code> in the terminal, reads the error, fixes the script, and runs it again.</p>
<p>People in the English-speaking Claude Code crowd already make this point: if you treat it like a normal coding chatbot, you're wasting it. The difference comes from the system you build around it: persistent conventions, commands it can run, and output you can check.<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup><sup><a id="footnote-ref-12" href="#footnote-12" data-footnote-ref aria-describedby="footnote-label">2</a></sup></p>
<p>It won't click nodes in your viewport for you, and it won't deliver finished shots on its own. You still decide whether the shot looks good. The agent writes the repetitive glue code, gets it running, and fixes it until it passes a minimum check list.<sup><a id="footnote-ref-1-2" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup><sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">3</a></sup></p>
<h2>A script partner, not a viewport clicker</h2>
<p>Compared with asking a question in a browser chat and pasting the answer into a Wrangle, there are three main differences.</p>
<ol>
<li><strong>It can see your repo.</strong> Your existing exporters, naming rules, and past mistakes become context. It doesn't have to guess from zero every time.<sup><a id="footnote-ref-1-3" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup></li>
<li><strong>It can run code and read the errors back.</strong> The script goes through the terminal, the traceback comes back, and it gets fixed. You don't just get a block of code that looks right.<sup><a id="footnote-ref-1-4" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup></li>
<li><code>CLAUDE.md</code> <strong>pins your conventions to every session.</strong> Write the axis, units, and Houdini version down once, and you get fewer Maya-flavored <code>hou</code> calls.<sup><a id="footnote-ref-1-5" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup><sup><a id="footnote-ref-3" href="#footnote-3" data-footnote-ref aria-describedby="footnote-label">4</a></sup></li>
</ol>
<p>Houdini users on Reddit are fairly practical about this. With docs for the right version, Claude often writes usable VEX and Python. It still hallucinates, so you need to be able to check node and parameter names. One user tried a Houdini MCP over a weekend and found that most of the time it was faster to ask, then do the work themselves. Their studio's security policy wouldn't allow it anyway.<sup><a id="footnote-ref-4" href="#footnote-4" data-footnote-ref aria-describedby="footnote-label">5</a></sup></p>
<p>For most artists, I'd suggest this order: get the no-MCP, terminal-only setup working first. Then decide whether you want live session control at all.</p>
<p>The agent can't move your mouse. What you can reliably hand off is text: VEX drafts, <code>hou</code> scripts, <code>hython</code> / <code>hbatch</code> / <code>husk</code> commands, and written node recipes you wire up yourself.<sup><a id="footnote-ref-2-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">3</a></sup></p>
<p>In one line: you're the director and the reviewer. Claude Code is a TD assistant that edits scripts, runs commands, and reads logs. It is not an auto-clicker.</p>
<h2>What goes in the <code>CLAUDE.md</code> at your repo root</h2>
<p>Anthropic's docs describe <code>CLAUDE.md</code> as persistent project instructions the agent reads at the start of every session. Specific and short works better than vague lines like "write high-quality code." It's context, not a sandbox. If a path really must be off-limits, you still need permissions, hooks, or similar mechanisms.<sup><a id="footnote-ref-3-2" href="#footnote-3" data-footnote-ref aria-describedby="footnote-label">4</a></sup></p>
<p>The Retrodeck guide makes a related point. Put your Blender or Houdini version, coordinate system (Y-up or Z-up), units, asset naming, and entry points in the repo root. That's what stops basic failures like Maya-flavored code or assuming centimeters by default.<sup><a id="footnote-ref-1-6" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup></p>
<p>Here's a <strong>TEMPLATE</strong>. Change the versions and paths for your own machine. It is not a log from a real run.</p>
<pre><code class="language-markdown"># CLAUDE.md — Houdini shot / asset pipeline (TEMPLATE)

## Stack (pin versions; don't let the model guess)
- Houdini: 20.5.x (or your actual version)
- Python: the hython that ships with that version
- USD / Solaris: does this project require a layer naming prefix?
- Downstream: Unreal / real-time preview (if any)

## Axis and units (prevents wrong-axis exports)
- Scene up-axis: Y-up (or Z-up — pick one and write it down)
- Units: meters (or centimeters — pick one and write it down)
- Export to Unreal: state whether an extra rotation / scale convention applies
- Never: assume Maya-style transforms by default; if unsure, ask first

## Naming
- Shots: `shots/SH####/`
- Caches: `cache/&lt;asset&gt;/&lt;version&gt;/`
- Suggested USD asset prim: `/World/Assets/&lt;Name&gt;`
- Never ship meaningless names like `geo1` / `null2`

## What the agent may / may not do
- May: edit scripts under `pipeline/` and `shots/*/scripts/`; run `hython` smoke tests on a single asset
- May not: delete caches, touch secrets, submit to the farm without confirmation, edit a `.hip` that has no backup

## How to run
- Smoke test: `hython path/to/script.py`
- One asset first, then the batch
- After every change: print axis check / bbox / whether all material slot names are present

## Minimum human checks
- Do axis and scale look right at a glance?
- Are any materials or attributes missing?
- Was an error swallowed and reported as a fake success?
</code></pre>
<p>Make each line something you can verify. "Print the world transform before export" beats "be careful with coordinates." The docs also suggest moving multi-step procedures into skills or path-specific rules, so <code>CLAUDE.md</code> doesn't grow into an encyclopedia.<sup><a id="footnote-ref-3-3" href="#footnote-3" data-footnote-ref aria-describedby="footnote-label">4</a></sup></p>
<h2>The Claude Code Houdini loop: write, run hython, read the error, fix</h2>
<p>Without MCP, the terminal already covers most of the pipeline glue:</p>
<ul>
<li>Write or edit scripts for export, renaming, USD layer checks, and pre-publish validation.</li>
<li>Run <code>hython script.py</code>, or <code>hython scene.hip</code>, to get an environment with <code>hou</code> loaded.</li>
<li>Use <code>hbatch</code> for HScript. When you need them, <code>hrender</code>, or <code>husk</code> on exported USD. Check the flags against the docs for your installed version.<sup><a id="footnote-ref-2-3" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">3</a></sup></li>
</ul>
<p>The loop looks like this. Treat it as an <strong>EXAMPLE workflow pattern</strong>, not a measured timing or a hardware spec from any machine.</p>
<ol>
<li>You describe the goal: read the asset list from the shot folder, export Alembic or USD, and print the world transforms.</li>
<li>The agent writes the script in <code>pipeline/</code>, reusing your existing naming functions where it can.</li>
<li>You or the agent run <code>hython export_one.py</code>, on one asset first.</li>
<li>Read stderr and the exit code: missing node, wrong parm name, path that doesn't exist, license message.</li>
<li>Fix it and rerun <strong>the same asset</strong>. Once that passes, widen it to a batch over the folder.</li>
</ol>
<p>The Terminal Skills page has a useful reminder. Node type names and internal parameter names change between versions. Read them from the current session or the docs for your version. Don't let the model recite them from memory. Python is good for managing scenes and files; big per-point loops belong in VEX.<sup><a id="footnote-ref-2-4" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">3</a></sup></p>
<h3>EXAMPLE: fixing an export script with a flipped axis</h3>
<p>This is a reusable workflow story pattern. It's made up but common. It is <strong>not</strong> a claim that this ran on a specific machine yesterday.</p>
<ul>
<li><strong>Symptom:</strong> In the downstream engine, garden props are lying on their side, or the scale is off by an order of magnitude.</li>
<li><strong>Common causes:</strong> The script hard-codes rotations from another DCC's habits. Units drift between meters and centimeters. <code>CLAUDE.md</code> doesn't state the up-axis, so the model falls back on Maya assumptions.<sup><a id="footnote-ref-1-7" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup></li>
<li><strong>Fix:</strong><ol>
<li>Open the repo-root conventions and confirm the up-axis and units.</li>
<li>Apply the project's conversion once, before export, instead of hand-fixing each asset.</li>
<li>At the end of the script, <code>print</code> the rotation, scale, and bbox.</li>
<li>Compare a single hero asset in the Houdini viewport and downstream.</li>
</ol>
</li>
<li><strong>Check:</strong> The same asset stands upright at the same scale. Only after one asset passes do you let the batch run.</li>
</ul>
<p>One artist on X said they aren't very familiar with Houdini but used Claude Code to help run a sim and export Alembic caches for an ML Deformer workflow on the Unreal side.<sup><a id="footnote-ref-6" href="#footnote-6" data-footnote-ref aria-describedby="footnote-label">6</a></sup> Same direction: scripts and exports. The agent isn't doing the art direction. Opening that export in Unreal is a different check. The <a href="https://digitalgarden.si/blog/houdini-unreal-usd/">Houdini to Unreal USD test</a> measures a composed stage and a baked HDA, including units, materials and instance counts. The simulation cook is separate again. A <a href="https://digitalgarden.si/blog/houdini-ml-volume-upres-pyro/">Houdini ML volume upres</a> log starts from a small Pyro cache, compares pretrained 2× and 3× density, and only then runs a short training pass.</p>
<h2>Optional: Houdini MCP and skills, and the risks</h2>
<p>In Claude Code, MCP is a channel for external tools. Once a server is connected, the agent can call whatever that server exposes. Configuration has scopes such as local, project, and user, and you can check status with <code>/mcp</code>.<sup><a id="footnote-ref-7" href="#footnote-7" data-footnote-ref aria-describedby="footnote-label">7</a></sup></p>
<p>There are already several open-source projects that connect Houdini to MCP, covering things like creating nodes, changing parameters, running Python or HScript, and even grabbing viewport screenshots. Creators on X have shown Claude Code + MCP + Houdini experiments such as camera-path work.<sup><a id="footnote-ref-5" href="#footnote-5" data-footnote-ref aria-describedby="footnote-label">8</a></sup><sup><a id="footnote-ref-8" href="#footnote-8" data-footnote-ref aria-describedby="footnote-label">9</a></sup></p>
<p><strong>I am not vouching for the security or production readiness of any third-party MCP server.</strong> Their architectures differ (local sockets, RPyC, a built-in web server, and so on), and the permission surface is usually large. If it can execute code, it can change your scene. The Reddit thread also has first-hand reports of "slow" and "my company won't allow it."<sup><a id="footnote-ref-4-2" href="#footnote-4" data-footnote-ref aria-describedby="footnote-label">5</a></sup></p>
<p>Before you connect one, use this table to check the impulse:</p>
<table>
<thead>
<tr>
<th>Can go to the agent (still confirm verbally)</th>
<th>You watch it in the UI / by hand</th>
</tr>
</thead>
<tbody><tr>
<td>Read-only queries of nodes, warnings, error messages</td>
<td>Long sims, large cooks, overnight caches</td>
</tr>
<tr>
<td>Editing the script repo and <code>hython</code> smoke tests</td>
<td>Overwriting a production <code>.hip</code> with no backup</td>
</tr>
<tr>
<td>VEX drafts that you paste into the Wrangle yourself</td>
<td>Render farm submissions, anything that creates a bill</td>
</tr>
<tr>
<td>A skill that explains how to write <code>hou</code> / command-line calls</td>
<td>Any secrets, credentials, cloud storage, or farm accounts</td>
</tr>
</tbody></table>
<p>The skill route packages Houdini command-line and VEX conventions as instructions the agent can read. That's more controllable than remote-controlling the whole desktop, because it mainly changes how the agent writes text. It doesn't hand over full-scene RPC by default. Which skill to install, and whether to trust it, is still on you: check where the files come from. This article doesn't rank vendors.<sup><a id="footnote-ref-2-5" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">3</a></sup></p>
<h2>High-return tasks: start with these</h2>
<p>Pick work that is tedious, repetitive, clearly specified, and easy to catch when it goes wrong. The Retrodeck guide calls this pipeline plumbing, as opposed to "modeling for you."<sup><a id="footnote-ref-1-8" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup></p>
<p><strong>High return:</strong></p>
<ul>
<li>VEX / Attribute Wrangle drafts (you paste them into the node and cook them yourself)</li>
<li>Batch renaming, path rewriting, cache folder cleanup</li>
<li>FBX / Alembic / USD exporters, plus checks that material slots and key attributes are all there</li>
<li>USD layer and prim naming validation</li>
<li>Pre-publish checks: scale, naming, empty materials, a policy for obvious broken geometry (a check script, not automatic retopology)</li>
<li>Small <code>hython</code> smoke-test scripts, instead of sending the whole shot to the farm on day one</li>
</ul>
<p><strong>Low return or high risk:</strong></p>
<ul>
<li>Asking the agent to "tweak it in the viewport until it feels right"</li>
<li>Large overwrite-to-disk jobs with no review</li>
<li>Handing the only copy of a production shot file to an unattended rewrite</li>
</ul>
<p>Rule of thumb: VEX handles per-element work. Python and <code>hou</code> build networks and manage files. You look at the result.<sup><a id="footnote-ref-2-6" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">3</a></sup></p>
<h2>Security red lines: paths and secrets the agent stays away from</h2>
<ul>
<li><strong>Secrets and accounts:</strong> API keys, farm credentials, cloud storage tokens. Keep them out of the chat and out of any <code>CLAUDE.md</code> you commit. When they're needed, use local environment variables or your existing secrets manager, and don't let the agent print them.</li>
<li><strong>Write scope:</strong> By default, only <code>pipeline/</code>, <code>scripts/</code>, and clearly marked temp output. Production caches and publish folders need a human to confirm.</li>
<li><strong>Licenses:</strong> <code>hython</code>, <code>hbatch</code>, and <code>import hou</code> take a license. For long-running services, release it the way the docs allow.<sup><a id="footnote-ref-2-7" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">3</a></sup></li>
<li><strong>Untrusted</strong> <code>.hip</code> <strong>/</strong> <code>.hda</code> <strong>files:</strong> They can carry scripts that run on load. Don't let the agent open files you don't know.<sup><a id="footnote-ref-2-8" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">3</a></sup></li>
<li><strong>Convention isn't enforcement:</strong> "Don't delete" in <code>CLAUDE.md</code> is only a reminder. Actual blocking takes tool permissions, hooks, or OS permissions.<sup><a id="footnote-ref-3-4" href="#footnote-3" data-footnote-ref aria-describedby="footnote-label">4</a></sup></li>
<li><strong>Small before big:</strong> Every batch job starts on one file. Talk about unattended runs only once you can undo or roll back from version control.</li>
</ul>
<h2>How this splits with Meshy and ComfyUI</h2>
<p>Short version: generation belongs to generation tools; glue belongs to the scripting agent.</p>
<ul>
<li><strong>ComfyUI 3D:</strong> Text-to-mesh or image-to-mesh, multi-view shape exploration. That's the front half of the pipeline. A <a href="https://digitalgarden.si/blog/comfyui-3d-workflow/">ComfyUI 3D workflow</a> test took one TRELLIS.2 draft through remesh, decimation and a GLB check in Blender and Houdini. A generated mesh is not the same as something ready for a game or a <a href="https://digitalgarden.si/play/">real-time garden</a>.</li>
<li><strong>Meshy and similar generators:</strong> Shape iteration on the web or generation side. I won't compare vendors here; the point is just to keep it separate from the scripting agent that comes after. A later <a href="https://digitalgarden.si/blog/meshy-3d-agent-houdini-cleanup/">Meshy free-to-Houdini cleanup</a> downloaded one Meshy 6 Lite GLB, rejected a cleanup that opened holes, and reduced the accepted mesh to 3,000 triangles before instancing it.</li>
<li><strong>Claude Code + Houdini:</strong> Batch processing after cleanup, exports, USD packaging, naming validation, publish checks. The back half of the pipeline.</li>
<li>On YouTube, the more common demos are Claude Code + Blender MCP building scenes or filling them with props.<sup><a id="footnote-ref-9" href="#footnote-9" data-footnote-ref aria-describedby="footnote-label">10</a></sup><sup><a id="footnote-ref-10" href="#footnote-10" data-footnote-ref aria-describedby="footnote-label">11</a></sup> That's live DCC control plus generation. It complements a script loop in your shot folder. Don't judge them by the same metric. Public tests of <a href="https://digitalgarden.si/blog/gemini-4-argon-threejs/">Gemini 4 Argon for Three.js</a> include a Blender MCP path toward an AI-generated 3D scene, and they still need a human check before anything ships.</li>
<li>A <a href="https://digitalgarden.si/blog/codex-machine-skill-threejs/">machine-skill with Codex</a> test built two Three.js pump simulations from that skill. Codex was the host, not Claude Code, and the solvers still needed corrections before the numbers and cutaways agreed. A later <a href="https://digitalgarden.si/blog/claude-code-vs-codex-threejs/">Claude Code and Codex wind task</a> gave both tools the same frozen Three.js plant, then checked the unchanged builds on WebGPU and WebGL2.</li>
</ul>
<p>The garden itself (for example, this <a href="https://digitalgarden.si/virtual-garden/">interactive 3D garden</a>) makes a good practice ground. Take one plant or prop all the way through script, <code>hython</code>, and opening the result to look at it. The <a href="https://digitalgarden.si/blog/openusd-threejs-usd-web-viewer/">OpenUSD in Three.js</a> note is that kind of check: a headless hython Solaris export, then the browser load. The <a href="https://digitalgarden.si/blog/openusd-pipeline-beginners/">OpenUSD beginner stage</a> is the smaller file exercise underneath it: references, a payload and a season variant. Then think about batching a full set of seasonal assets. For work in progress, follow <a href="https://x.com/EmberAtelier">@EmberAtelier on X</a>.</p>
<h2>FAQ</h2>
<h3>How is Claude Code for 3D different from asking a chatbot and copying the code?</h3>
<p>A browser chat gives you a snippet. Claude Code edits, runs, and reads errors against your actual folder, and <code>CLAUDE.md</code> remembers your team's axis and naming. The more real context it has, the more useful it gets.<sup><a id="footnote-ref-1-9" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup><sup><a id="footnote-ref-3-5" href="#footnote-3" data-footnote-ref aria-describedby="footnote-label">4</a></sup></p>
<h3>Without MCP, which Houdini tasks can Claude Code handle from the terminal?</h3>
<p>Anything you can write as Python or <code>hou</code> and run with <code>hython</code>, <code>hbatch</code>, or <code>husk</code>: exports, checks, batch renames, smoke-test render scripts, reading cook errors and fixing them. Clicking in the viewport, tuning parameters by feel, and final artistic judgment are still yours. That's the honest scope of a Claude Code Houdini setup.<sup><a id="footnote-ref-2-9" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">3</a></sup></p>
<h3>How do I stop the agent from writing wrong-axis or Maya-flavored code?</h3>
<p>Pin the up-axis and units in <code>CLAUDE.md</code>. Print transforms at the end of export scripts. Compare a single asset downstream first. Explicitly forbid "assume Maya." Read node and parameter names from the session for your version.<sup><a id="footnote-ref-1-10" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup><sup><a id="footnote-ref-2-10" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">3</a></sup></p>
<h3>What's the minimum a human should check?</h3>
<p>Axis and scale. Whether materials and key attributes survived. Whether a file reports a successful exit code but is empty. And open the result and look at it. Assembling a scene without looking at it is a common way agents fail.<sup><a id="footnote-ref-11" href="#footnote-11" data-footnote-ref aria-describedby="footnote-label">12</a></sup></p>
<hr />
<h2>References</h2>
<hr />
<p><a href="https://digitalgarden.si/blog/claude-code-for-3d/">Read the original article, supporting data and updates</a>.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>Retrodeck, <em>Claude Code for 3D Artists: A Real Pipeline Setup</em> — <a href="https://retrodeck.ai/guides/every-3d-artist-should-be-using-claude-code-my-setup">https://retrodeck.ai/guides/every-3d-artist-should-be-using-claude-code-my-setup</a> <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩︎</a> <a href="#footnote-ref-1-2" data-footnote-backref aria-label="Back to reference 1">↩︎<sup>2</sup></a> <a href="#footnote-ref-1-3" data-footnote-backref aria-label="Back to reference 1">↩︎<sup>3</sup></a> <a href="#footnote-ref-1-4" data-footnote-backref aria-label="Back to reference 1">↩︎<sup>4</sup></a> <a href="#footnote-ref-1-5" data-footnote-backref aria-label="Back to reference 1">↩︎<sup>5</sup></a> <a href="#footnote-ref-1-6" data-footnote-backref aria-label="Back to reference 1">↩︎<sup>6</sup></a> <a href="#footnote-ref-1-7" data-footnote-backref aria-label="Back to reference 1">↩︎<sup>7</sup></a> <a href="#footnote-ref-1-8" data-footnote-backref aria-label="Back to reference 1">↩︎<sup>8</sup></a> <a href="#footnote-ref-1-9" data-footnote-backref aria-label="Back to reference 1">↩︎<sup>9</sup></a> <a href="#footnote-ref-1-10" data-footnote-backref aria-label="Back to reference 1">↩︎<sup>10</sup></a></p>
</li>
<li id="footnote-12">
<p>X @Origin_AI_01 (<a href="http://CLAUDE.md">CLAUDE.md</a> as a system workflow) — <a href="https://x.com/Origin_AI_01/status/2101541975392657563">https://x.com/Origin_AI_01/status/2101541975392657563</a> <a href="#footnote-ref-12" data-footnote-backref aria-label="Back to reference 12">↩︎</a></p>
</li>
<li id="footnote-2">
<p>Terminal Skills, <em>SideFX Houdini Skill for Claude Code &amp; AI Agents</em> — <a href="https://terminalskills.io/skills/houdini">https://terminalskills.io/skills/houdini</a> <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩︎</a> <a href="#footnote-ref-2-2" data-footnote-backref aria-label="Back to reference 2">↩︎<sup>2</sup></a> <a href="#footnote-ref-2-3" data-footnote-backref aria-label="Back to reference 2">↩︎<sup>3</sup></a> <a href="#footnote-ref-2-4" data-footnote-backref aria-label="Back to reference 2">↩︎<sup>4</sup></a> <a href="#footnote-ref-2-5" data-footnote-backref aria-label="Back to reference 2">↩︎<sup>5</sup></a> <a href="#footnote-ref-2-6" data-footnote-backref aria-label="Back to reference 2">↩︎<sup>6</sup></a> <a href="#footnote-ref-2-7" data-footnote-backref aria-label="Back to reference 2">↩︎<sup>7</sup></a> <a href="#footnote-ref-2-8" data-footnote-backref aria-label="Back to reference 2">↩︎<sup>8</sup></a> <a href="#footnote-ref-2-9" data-footnote-backref aria-label="Back to reference 2">↩︎<sup>9</sup></a> <a href="#footnote-ref-2-10" data-footnote-backref aria-label="Back to reference 2">↩︎<sup>10</sup></a></p>
</li>
<li id="footnote-3">
<p>Anthropic, <em>How Claude remembers your project (</em><a href="http://CLAUDE.md"><em>CLAUDE.md</em></a><em>)</em> — <a href="https://code.claude.com/docs/en/claude-md">https://code.claude.com/docs/en/claude-md</a> <a href="#footnote-ref-3" data-footnote-backref aria-label="Back to reference 3">↩︎</a> <a href="#footnote-ref-3-2" data-footnote-backref aria-label="Back to reference 3">↩︎<sup>2</sup></a> <a href="#footnote-ref-3-3" data-footnote-backref aria-label="Back to reference 3">↩︎<sup>3</sup></a> <a href="#footnote-ref-3-4" data-footnote-backref aria-label="Back to reference 3">↩︎<sup>4</sup></a> <a href="#footnote-ref-3-5" data-footnote-backref aria-label="Back to reference 3">↩︎<sup>5</sup></a></p>
</li>
<li id="footnote-4">
<p>Reddit r/Houdini, <em>Do you use LLMs for Houdini problems?</em> — <a href="https://www.reddit.com/r/Houdini/comments/1l7kkss/do_you_use_llms_for_houdini_problems/">https://www.reddit.com/r/Houdini/comments/1l7kkss/do_you_use_llms_for_houdini_problems/</a> <a href="#footnote-ref-4" data-footnote-backref aria-label="Back to reference 4">↩︎</a> <a href="#footnote-ref-4-2" data-footnote-backref aria-label="Back to reference 4">↩︎<sup>2</sup></a></p>
</li>
<li id="footnote-6">
<p>X @melnukoff — <a href="https://x.com/melnukoff/status/2102733870219366851">https://x.com/melnukoff/status/2102733870219366851</a> <a href="#footnote-ref-6" data-footnote-backref aria-label="Back to reference 6">↩︎</a></p>
</li>
<li id="footnote-7">
<p>Anthropic, <em>Connect to MCP servers</em> — <a href="https://code.claude.com/docs/en/mcp-quickstart">https://code.claude.com/docs/en/mcp-quickstart</a> <a href="#footnote-ref-7" data-footnote-backref aria-label="Back to reference 7">↩︎</a></p>
</li>
<li id="footnote-5">
<p>X @JADM823 — <a href="https://x.com/JADM823/status/2108594805039079637">https://x.com/JADM823/status/2108594805039079637</a> <a href="#footnote-ref-5" data-footnote-backref aria-label="Back to reference 5">↩︎</a></p>
</li>
<li id="footnote-8">
<p>Community Houdini MCP projects (listed only to show they exist; not a security endorsement): <a href="https://github.com/atayilgun/Houdini-claudecode-mcp">https://github.com/atayilgun/Houdini-claudecode-mcp</a> , <a href="https://github.com/OtakuSquirrel/houdini-mcp">https://github.com/OtakuSquirrel/houdini-mcp</a> , <a href="https://github.com/healkeiser/fxhoudinimcp">https://github.com/healkeiser/fxhoudinimcp</a> <a href="#footnote-ref-8" data-footnote-backref aria-label="Back to reference 8">↩︎</a></p>
</li>
<li id="footnote-9">
<p>YouTube, <em>Claude Code + Blender MCP</em> — <a href="https://www.youtube.com/watch?v=Bdelqytx9oI">https://www.youtube.com/watch?v=Bdelqytx9oI</a> <a href="#footnote-ref-9" data-footnote-backref aria-label="Back to reference 9">↩︎</a></p>
</li>
<li id="footnote-10">
<p>YouTube, <em>Claude + 3D Model MCP</em> — <a href="https://www.youtube.com/watch?v=g_XF5l1ighc">https://www.youtube.com/watch?v=g_XF5l1ighc</a> <a href="#footnote-ref-10" data-footnote-backref aria-label="Back to reference 10">↩︎</a></p>
</li>
<li id="footnote-11">
<p>X @iamrexei — <a href="https://x.com/iamrexei/status/2100496481899712802">https://x.com/iamrexei/status/2100496481899712802</a> <a href="#footnote-ref-11" data-footnote-backref aria-label="Back to reference 11">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded></item><item><title><![CDATA[Google Gemini 4 Argon for Three.js: What Community Tests Actually Show]]></title><description><![CDATA[Google's Gemini 4 Argon AI model for Three.js: public community tests, the Blender MCP asset route, recurring failures, and what still needs human wor]]></description><link>https://digitalgarden-alicia.hashnode.dev/google-gemini-4-argon-for-three-js-what-community-tests-actually-show</link><guid isPermaLink="true">https://digitalgarden-alicia.hashnode.dev/google-gemini-4-argon-for-three-js-what-community-tests-actually-show</guid><dc:creator><![CDATA[Alicia lu]]></dc:creator><pubDate>Sun, 25 Oct 2026 07:22:40 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>AI-assisted disclosure: Codex assisted with research, code and writing. Measurements, failure cases and limitations are retained from the original article.</p>
</blockquote>
<blockquote>
<p>Originally published by Alicia on 2026-10-14: <a href="https://digitalgarden.si/blog/gemini-4-argon-threejs/">Digital Garden by Alicia</a>.</p>
</blockquote>
<p>Gemini 4 Argon is an AI model in Google's Gemini family, <a href="https://blog.google/innovation-and-ai/models-and-research/gemini-models/introducing-gemini-4-argon/">released on October 13, 2026</a>. This article collects public community tests of the model generating Three.js scenes, including workflows that drive Blender MCP. It is written for <a href="https://digitalgarden.si/">digitalgarden.si</a>, an <a href="https://digitalgarden.si/virtual-garden/">interactive 3D garden</a> built with Three.js.</p>
<p>First, the ground rules. I didn't test Argon myself. This post collects tests other people published. Every case below names the author and links to the original post, and I only describe what each person actually showed. If a post didn't mention frame rate or failures, I don't guess.</p>
<p>Second, some context. digitalgarden.si is an interactive 3D garden built with Three.js. I didn't build it with Argon. But because I work in Three.js every day, my question is narrow: how far is an AI-generated Three.js scene from something you could ship?</p>
<h2>What the official post says</h2>
<p>I'm only using Google's own announcement here,<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup> and only the parts that touch 3D.</p>
<ul>
<li>Google says Argon improves on earlier Gemini models in spatial reasoning, mathematical precision, and "visual coding."</li>
<li>Googlers use Argon inside Antigravity, with slash commands like <code>/goal</code> and multi-agent loops with custom skills, to prototype 3D visualizations, games, and interactive animations. The output is vanilla code that renders directly in the browser.</li>
<li>One demo, Desk Racer, turned pictures of a real desk into a playable miniature racing game. Blender MCP modeled and collision-checked the desk objects, room, and track. Nano Banana made the textures. Three.js handled the physics and custom shaders, with car engine audio from Gemini Omni Flash.</li>
<li>Another demo built a pagoda temple complex in Blender, using multi-agent visual critique loops.</li>
<li>In a third, Argon wrote Python scripts to procedurally sculpt, rig, animate, and PBR-bake a 3D game, then wired it for the web in Three.js with GLSL shaders and shatter physics.</li>
<li>A Gothic scene started from a single Nano Banana image, with Argon driving Blender MCP in an autonomous loop.</li>
</ul>
<p>Note the word Google uses: prototype. That's their framing, not mine. And these are Google's own showcase pieces, not independent tests.</p>
<h2>The community cases</h2>
<h3>Case 1: Margarita slosh, four models side by side (@ann_nnng)</h3>
<p><strong>Task.</strong> Ann Nguyen (@ann_nnng) tested the same effect, liquid sloshing in a margarita glass, with Argon, Muse Wonder, Claude Opus 5.5, and GPT 6 Astra.<sup><a id="footnote-ref-3" href="#footnote-3" data-footnote-ref aria-describedby="footnote-label">2</a></sup></p>
<p><strong>Result.</strong> She reports that all four used Three.js with WebGL. Her verdict was one line: Gemini has "improved so much."</p>
<p><strong>Failure points.</strong> None stated. It's one person's visual comparison. The post gives no frame rate, no code, and no detail on how many runs each model got. It's a useful vibe check, not a benchmark.</p>
<h3>Case 2: A fluffy pumpkin with no Blender (@thebuggeddev)</h3>
<p><strong>Task.</strong> The Bugged Dev (@thebuggeddev) had Argon make a fluffy pumpkin plushie without using Blender at all.<sup><a id="footnote-ref-4" href="#footnote-4" data-footnote-ref aria-describedby="footnote-label">3</a></sup></p>
<p><strong>Result.</strong> By his description, it's a procedural GLB model with volumetric fur shells, GPU-instanced fur strands, and real-time air-jet physics, built with Three.js and GLSL. You can blow the fur around in real time. In a reply, he explained that Argon generated the meshes mathematically and packed them directly into binary glTF 2.0 with Python.<sup><a id="footnote-ref-5" href="#footnote-5" data-footnote-ref aria-describedby="footnote-label">4</a></sup></p>
<p><strong>Failure points.</strong> He didn't mention any.</p>
<h3>Case 3: A turbofan engine and a Rubik's cube robot (@ai_for_success)</h3>
<p><strong>Task.</strong> Ashutosh Shrivastava (@ai_for_success) posted two builds. In one, he asked Argon for a high-bypass turbofan jet engine using Blender and Three.js.<sup><a id="footnote-ref-6" href="#footnote-6" data-footnote-ref aria-describedby="footnote-label">5</a></sup> In the other, Argon in Antigravity built a whole project: robot arms modeled in Blender, then a 3D simulation in Three.js. He says Gemini then solved the cube through the Interactions API.<sup><a id="footnote-ref-7" href="#footnote-7" data-footnote-ref aria-describedby="footnote-label">6</a></sup></p>
<p><strong>Result.</strong> Two videos, both following the same route: model in Blender, show it in Three.js.</p>
<p><strong>Failure points.</strong> Neither post mentions any.</p>
<h3>Case 4: A flying dragon (@ashen_one)</h3>
<p><strong>Task.</strong> @ashen_one made a flying dragon test and said everything was custom-made, with no premade animations and no video generation.<sup><a id="footnote-ref-8" href="#footnote-8" data-footnote-ref aria-describedby="footnote-label">7</a></sup></p>
<p><strong>Result.</strong> The mesh came from Tripo 3D. He's specific that he drove Tripo through browser control, not MCP. Then Blender MCP and Argon took it from there. His own take: "not bad for one shot?"</p>
<p><strong>Failure points.</strong> None stated, but note what this shows: Argon didn't make the model. The mesh and the motion came from different tools.</p>
<h3>Case 5: A storm lighthouse in one HTML file (@YFarmX)</h3>
<p><strong>Task.</strong> @YFarmX had Argon build a storm-lashed lighthouse in a single HTML file.<sup><a id="footnote-ref-9" href="#footnote-9" data-footnote-ref aria-describedby="footnote-label">8</a></sup></p>
<p><strong>Result.</strong> Real-time Three.js with no image files. The island, waves, and lightning are all generated in code. By the author's own count, the whole chat used about 87k tokens.</p>
<p><strong>Failure points.</strong> None mentioned in the post.</p>
<h3>Case 6: A sneaker from primitives (Creative AI News)</h3>
<p><strong>Task.</strong> The Creative AI News staff gave Argon and Gemini 3.1 Pro the same prompt, once each: a single web page with a 3D sneaker on a round pedestal, soft studio light, slow rotation, and drag to look around. No 3D files allowed, so the shoe had to be built from simple shapes in Three.js.<sup><a id="footnote-ref-10" href="#footnote-10" data-footnote-ref aria-describedby="footnote-label">9</a></sup></p>
<p><strong>Result.</strong> Argon produced a rounded sole, a tongue, laces, and a heel loop on an orange-trimmed pedestal. It also added a product title nobody asked for. Both pages opened without errors.</p>
<p><strong>Failure points.</strong> The toe is boxy and the spin is very slow. The writers said themselves that neither shoe "would pass for a real product render," and called it a first look, not a benchmark.</p>
<h2>Recurring problems</h2>
<p>Six cases is a small set, but a few patterns show up.</p>
<ol>
<li><strong>Failures are mostly unreported.</strong> Of the six, only the one written up by a publication, Creative AI News, lists concrete flaws.<sup><a id="footnote-ref-10-2" href="#footnote-10" data-footnote-ref aria-describedby="footnote-label">9</a></sup> The X posts show the result and stop. That doesn't mean nothing went wrong. It means you can't tell from the post.</li>
<li><strong>Close isn't finished.</strong> The sneaker test is the clearest look at quality: recognizable, but boxy in places and not product-render grade.<sup><a id="footnote-ref-10-3" href="#footnote-10" data-footnote-ref aria-describedby="footnote-label">9</a></sup></li>
<li><strong>Argon didn't always make the assets.</strong> The dragon mesh came from Tripo.<sup><a id="footnote-ref-8-2" href="#footnote-8" data-footnote-ref aria-describedby="footnote-label">7</a></sup> Before you judge a demo, work out which step the model actually did.</li>
<li><strong>The samples are tiny.</strong> Every test here is a single post or a single run per model. Nobody published repeat runs.</li>
<li><strong>Setups are hard to reproduce.</strong> XiaoHu wrote a step-by-step guide to Google's Desk Racer demo and found that the headless Blender MCP configuration used in that demo wasn't published. The guide uses the community mcp-for-blender add-on instead.<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">10</a></sup> If you try to repeat a demo, expect your setup to differ.</li>
</ol>
<h2>The Blender MCP to Three.js asset route</h2>
<p>Two routes come up again and again.</p>
<ul>
<li><strong>Route A: through Blender.</strong> Argon runs Python through Blender MCP, models in Blender, exports GLB, and Three.js loads it. That's how Google describes Desk Racer,<sup><a id="footnote-ref-1-2" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup> and the XiaoHu guide spells out the same split: Blender MCP models through Python scripts and exports GLB props, and Three.js loads, renders, and handles driving physics.<sup><a id="footnote-ref-2-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">10</a></sup> Cases 3 and 4 also go through Blender.<sup><a id="footnote-ref-6-2" href="#footnote-6" data-footnote-ref aria-describedby="footnote-label">5</a></sup><sup><a id="footnote-ref-7-2" href="#footnote-7" data-footnote-ref aria-describedby="footnote-label">6</a></sup><sup><a id="footnote-ref-8-3" href="#footnote-8" data-footnote-ref aria-describedby="footnote-label">7</a></sup></li>
<li><strong>Route B: skip Blender.</strong> Argon writes Python that generates glTF directly,<sup><a id="footnote-ref-5-2" href="#footnote-5" data-footnote-ref aria-describedby="footnote-label">4</a></sup> or builds everything in code inside one HTML file.<sup><a id="footnote-ref-9-2" href="#footnote-9" data-footnote-ref aria-describedby="footnote-label">8</a></sup><sup><a id="footnote-ref-10-4" href="#footnote-10" data-footnote-ref aria-describedby="footnote-label">9</a></sup></li>
</ul>
<p>Here's how I'd choose. If the asset needs to stay editable, or another artist will pick it up later, use Route A. You get a .blend file you can open and fix. If it's a one-off effect or an atmosphere piece, Route B is fine. There's no file to hand off, but there's also less to set up.</p>
<p><strong>Checklist before you start:</strong></p>
<ul>
<li><input disabled type="checkbox" /> Which Blender MCP implementation, and which port? Google's demo setup wasn't published; the XiaoHu guide's add-on defaults to local port 9876.<sup><a id="footnote-ref-2-3" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">10</a></sup></li>
<li><input disabled type="checkbox" /> Do the exported GLB's units, orientation, and up-axis match your Three.js scene?</li>
<li><input disabled type="checkbox" /> Are colliders built separately, or are you colliding against the render mesh?</li>
<li><input disabled type="checkbox" /> Where do textures come from: Nano Banana, procedural, or hand-painted?</li>
</ul>
<h2>Performance and accessibility: still human work</h2>
<p>This part is my own checklist from Three.js projects. It doesn't come from the tests above. None of the cases reported mobile performance or accessibility, so treat these as open items for any AI-generated scene.</p>
<p><strong>Performance:</strong></p>
<ul>
<li><input disabled type="checkbox" /> Test the frame rate on mid- and low-end phones, not just the author's desktop recording.</li>
<li><input disabled type="checkbox" /> Count draw calls. Merge meshes where you can and use instancing for repeated objects.</li>
<li><input disabled type="checkbox" /> Compress textures (KTX2) and geometry (Draco or Meshopt).</li>
<li><input disabled type="checkbox" /> Dispose geometry, materials, and textures when the page is left or the scene unloads.</li>
<li><input disabled type="checkbox" /> Make post-processing like bloom something you can turn off.</li>
</ul>
<p><strong>Accessibility:</strong></p>
<ul>
<li><input disabled type="checkbox" /> Respect <code>prefers-reduced-motion</code>: reduce or stop automatic animation.</li>
<li><input disabled type="checkbox" /> Support keyboard controls, not just mouse dragging.</li>
<li><input disabled type="checkbox" /> Give the canvas a text description, and don't put key information only inside the 3D view.</li>
<li><input disabled type="checkbox" /> Limit how often things flash. The lightning in a storm scene is exactly where this matters.</li>
</ul>
<h2>How it splits with a Claude Code workflow</h2>
<p>I wrote earlier about <a href="https://digitalgarden.si/blog/claude-code-for-3d/">Claude Code for 3D artists and a real Houdini pipeline setup</a>. Looking at these community cases, here's how I'd split the work for now:</p>
<ul>
<li><strong>Argon:</strong> go from a photo, sketch, or one-line idea to a playable prototype fast, to check whether an idea works. Use it to run Blender MCP for a first pass on assets.</li>
<li><strong>Claude Code:</strong> refactoring inside an existing codebase, pipeline scripts, performance work, and anything that needs long-term maintenance.</li>
<li><strong>A person:</strong> lighting, materials, how interaction feels, the performance budget, and accessibility.</li>
</ul>
<p>Image-to-mesh is a separate front end from either of those tools. A <a href="https://digitalgarden.si/blog/comfyui-3d-workflow/">tested ComfyUI prop workflow</a> still had to remesh, decimate and reopen the GLB in Blender and Houdini before the generated draft counted as a prop. A downloaded <a href="https://digitalgarden.si/blog/meshy-3d-agent-houdini-cleanup/">Meshy 6 Lite model</a> needed the same kind of Houdini cleanup before Three.js instanced it.</p>
<p>This split is a suggestion based on public material. It is not a head-to-head test of the two tools. A separate <a href="https://digitalgarden.si/blog/codex-machine-skill-threejs/">machine-skill with Codex</a> test built two Three.js pump simulations; Codex was the host, not Argon, and those pages still needed human corrections. The <a href="https://digitalgarden.si/blog/claude-code-vs-codex-threejs/">Claude Code versus Codex wind comparison</a> is a paired run of those two coding tools on one Three.js plant, with WebGPU and WebGL2 checked independently.</p>
<h2>Verdict: prototype yes, production not yet</h2>
<p>From the public tests, Argon's strength in Three.js is getting from nothing to something you can see and play with, fast. Google itself frames it as a prototyping tool.<sup><a id="footnote-ref-1-3" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup> The one test that listed flaws found a scene that looked close but not finished,<sup><a id="footnote-ref-10-5" href="#footnote-10" data-footnote-ref aria-describedby="footnote-label">9</a></sup> and none of the posts covered performance or accessibility. Those are still on you.</p>
<p>So I'd use it to sketch, not to ship. Take the prototype, then do the boring parts yourself.</p>
<p>If you want a feel for what a browser scene built with Three.js can be, walk around the <a href="https://digitalgarden.si/virtual-garden/">virtual garden</a> or try <a href="https://digitalgarden.si/play/">Play</a>. Neither was built with Argon.</p>
<h2>References</h2>
<hr />
<p><a href="https://digitalgarden.si/blog/gemini-4-argon-threejs/">Read the original article, supporting data and updates</a>.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>Google, "Gemini 4 Argon: Built to solve your hardest problems" — <a href="https://blog.google/innovation-and-ai/models-and-research/gemini-models/introducing-gemini-4-argon/">https://blog.google/innovation-and-ai/models-and-research/gemini-models/introducing-gemini-4-argon/</a> <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩︎</a> <a href="#footnote-ref-1-2" data-footnote-backref aria-label="Back to reference 1">↩︎<sup>2</sup></a> <a href="#footnote-ref-1-3" data-footnote-backref aria-label="Back to reference 1">↩︎<sup>3</sup></a></p>
</li>
<li id="footnote-3">
<p>@ann_nnng, margarita slosh four-model comparison — <a href="https://x.com/ann_nnng/status/2110234900616126769">https://x.com/ann_nnng/status/2110234900616126769</a> <a href="#footnote-ref-3" data-footnote-backref aria-label="Back to reference 3">↩︎</a></p>
</li>
<li id="footnote-4">
<p>@thebuggeddev, fluffy pumpkin — <a href="https://x.com/thebuggeddev/status/2110043093466743194">https://x.com/thebuggeddev/status/2110043093466743194</a> <a href="#footnote-ref-4" data-footnote-backref aria-label="Back to reference 4">↩︎</a></p>
</li>
<li id="footnote-5">
<p>@thebuggeddev, how the GLB was generated — <a href="https://x.com/thebuggeddev/status/2110205444836950118">https://x.com/thebuggeddev/status/2110205444836950118</a> <a href="#footnote-ref-5" data-footnote-backref aria-label="Back to reference 5">↩︎</a> <a href="#footnote-ref-5-2" data-footnote-backref aria-label="Back to reference 5">↩︎<sup>2</sup></a></p>
</li>
<li id="footnote-6">
<p>@ai_for_success, turbofan engine — <a href="https://x.com/ai_for_success/status/2110040065800097793">https://x.com/ai_for_success/status/2110040065800097793</a> <a href="#footnote-ref-6" data-footnote-backref aria-label="Back to reference 6">↩︎</a> <a href="#footnote-ref-6-2" data-footnote-backref aria-label="Back to reference 6">↩︎<sup>2</sup></a></p>
</li>
<li id="footnote-7">
<p>@ai_for_success, Rubik's cube robot arms — <a href="https://x.com/ai_for_success/status/2110040948638126557">https://x.com/ai_for_success/status/2110040948638126557</a> <a href="#footnote-ref-7" data-footnote-backref aria-label="Back to reference 7">↩︎</a> <a href="#footnote-ref-7-2" data-footnote-backref aria-label="Back to reference 7">↩︎<sup>2</sup></a></p>
</li>
<li id="footnote-8">
<p>@ashen_one, flying dragon — <a href="https://x.com/ashen_one/status/2110192210964521388">https://x.com/ashen_one/status/2110192210964521388</a> <a href="#footnote-ref-8" data-footnote-backref aria-label="Back to reference 8">↩︎</a> <a href="#footnote-ref-8-2" data-footnote-backref aria-label="Back to reference 8">↩︎<sup>2</sup></a> <a href="#footnote-ref-8-3" data-footnote-backref aria-label="Back to reference 8">↩︎<sup>3</sup></a></p>
</li>
<li id="footnote-9">
<p>@YFarmX, storm lighthouse — <a href="https://x.com/YFarmX/status/2110093177088885008">https://x.com/YFarmX/status/2110093177088885008</a> <a href="#footnote-ref-9" data-footnote-backref aria-label="Back to reference 9">↩︎</a> <a href="#footnote-ref-9-2" data-footnote-backref aria-label="Back to reference 9">↩︎<sup>2</sup></a></p>
</li>
<li id="footnote-10">
<p>Creative AI News, sneaker scene test — <a href="https://www.creativeainews.com/articles/gemini-4-argon-launch-price-3d-test-2026/">https://www.creativeainews.com/articles/gemini-4-argon-launch-price-3d-test-2026/</a> <a href="#footnote-ref-10" data-footnote-backref aria-label="Back to reference 10">↩︎</a> <a href="#footnote-ref-10-2" data-footnote-backref aria-label="Back to reference 10">↩︎<sup>2</sup></a> <a href="#footnote-ref-10-3" data-footnote-backref aria-label="Back to reference 10">↩︎<sup>3</sup></a> <a href="#footnote-ref-10-4" data-footnote-backref aria-label="Back to reference 10">↩︎<sup>4</sup></a> <a href="#footnote-ref-10-5" data-footnote-backref aria-label="Back to reference 10">↩︎<sup>5</sup></a></p>
</li>
<li id="footnote-2">
<p>XiaoHu, Desk Racer reproduction guide — <a href="https://best.xiaohu.ai/en/article/desk-racer-photo-to-game/">https://best.xiaohu.ai/en/article/desk-racer-photo-to-game/</a> <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩︎</a> <a href="#footnote-ref-2-2" data-footnote-backref aria-label="Back to reference 2">↩︎<sup>2</sup></a> <a href="#footnote-ref-2-3" data-footnote-backref aria-label="Back to reference 2">↩︎<sup>3</sup></a></p>
</li>
</ol>
</section>
]]></content:encoded></item><item><title><![CDATA[OpenUSD in Three.js: A Houdini Solaris Test of USD Web Viewer]]></title><description><![CDATA[A tested Houdini Solaris to Three.js workflow with usd-web-viewer 0.1.1: materials, instances, variants, units, warnings and measured local load times]]></description><link>https://digitalgarden-alicia.hashnode.dev/openusd-in-three-js-a-houdini-solaris-test-of-usd-web-viewer</link><guid isPermaLink="true">https://digitalgarden-alicia.hashnode.dev/openusd-in-three-js-a-houdini-solaris-test-of-usd-web-viewer</guid><dc:creator><![CDATA[Alicia lu]]></dc:creator><pubDate>Sun, 25 Oct 2026 07:20:35 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>AI-assisted disclosure: Codex assisted with research, code and writing. Measurements, failure cases and limitations are retained from the original article.</p>
</blockquote>
<blockquote>
<p>Originally published by Alicia on 2026-10-16: <a href="https://digitalgarden.si/blog/openusd-threejs-usd-web-viewer/">Digital Garden by Alicia</a>.</p>
</blockquote>
<p>Can a Houdini Solaris stage reach a Three.js scene without a glTF conversion? <strong>Yes, for the features in this small test.</strong> On October 16, 2026, I exported a purpose-built Solaris stage and loaded its USDA, USDC and USDZ versions with Hugging Face's <code>@huggingface/usd-web-viewer</code> 0.1.1. Textured geometry, 100 point instances, two instanceable references and the selected look variant survived. A MaterialX-only material became grey, and a curve disappeared with a warning.</p>
<p>This browser test is one of the delivery notes in the <a href="https://digitalgarden.si/blog/threejs-3d-garden-rendering/">rendering guide overview</a>.</p>
<p>I make <a href="https://digitalgarden.si/">digitalgarden.si</a>, an <a href="https://digitalgarden.si/virtual-garden/">interactive 3D garden</a> built with Three.js and Houdini-authored plants. This test ran in an independent local Three.js scene using the garden's Three.js version. It did not replace the live garden's GLB assets or exercise its WebGPU/TSL rendering pipeline. The results below describe this fixture and this machine.</p>
<p><img src="https://digitalgarden.si/marketing/research/openusd/solaris-browser.png" alt="The actual browser test: textured panel, two spheres, selected blue cube and a grid of 100 pink instances" /></p>
<p>The screenshot shows the local test harness, not the production garden. The grey sphere is an observed material fallback, not an unfinished screenshot.</p>
<h2>What I tested</h2>
<table>
<thead>
<tr>
<th>Component</th>
<th>Tested version or setting</th>
</tr>
</thead>
<tbody><tr>
<td>Authoring</td>
<td>Houdini 22.0.429, headless hython; bundled OpenUSD 26.05</td>
</tr>
<tr>
<td>Loader</td>
<td>@huggingface/usd-web-viewer 0.1.1, pinned from npm</td>
</tr>
<tr>
<td>Rendering</td>
<td>Three.js 0.186.1, WebGL2, antialiasing enabled</td>
</tr>
<tr>
<td>Browser and OS</td>
<td>Codex embedded Chromium 155, Windows 11</td>
</tr>
<tr>
<td>GPU</td>
<td>NVIDIA GeForce RTX 5070 Ti, ANGLE / Direct3D 11</td>
</tr>
<tr>
<td>Render size</td>
<td>1265 × 446 pixels, renderer pixel ratio 1</td>
</tr>
<tr>
<td>Texture mode</td>
<td>full, with a separate preview-mode check</td>
</tr>
<tr>
<td>Network</td>
<td>localhost, no throttling; five sequential measurements per format</td>
</tr>
</tbody></table>
<p>The package's published peer range is <code>&gt;=0.162.0 &lt;0.187.0</code> for Three.js, so the garden's 0.186.1 fits. Version 0.1.1 is MIT licensed. These are properties of the tested package, not a promise about a future release.<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup></p>
<p>The fixture is deliberately simple: a UV-mapped quad with a checker texture and flat normal map; two implicit spheres; a colored cube; a cube with two look variants; a 10 × 10 grid of small cube instances; two instanceable references to an external layer; guide, proxy and render-purpose cubes; and a BasisCurves prim. I also authored a USD camera and light to check whether they appeared in the returned group.</p>
<h2>The Solaris export that actually ran</h2>
<p>I used <strong>Python Script LOP → Configure Layer → USD ROP</strong>. The node types in this installation were <code>pythonscript</code>, <code>configurelayer</code> and <code>usd_rop</code>. The Python LOP authored the controlled fixture with OpenUSD APIs. Configure Layer explicitly set Z-up and 0.01 meters per unit. The USD ROP used <code>savestyle = flattenalllayers</code> and <code>defaultprim = World</code>.</p>
<p>This is a reproducible headless Solaris test, rather than a manual Material Library or Karma look-development session. The two materials were authored with UsdShade: one <code>UsdPreviewSurface</code> network and one MaterialX-only surface output. I did not test Karma image matching or a production MaterialX library.</p>
<p>There was a useful authoring catch. My first export retained Z-up but came out with <code>metersPerUnit = 1</code>. Adding the explicit Configure Layer node corrected the exported metadata to <code>0.01</code>. I reopened all three files through OpenUSD and checked the metadata, variant selection, 100 point placements and two native instances before measuring the browser. Merely setting a value in an intermediate LOP was not enough evidence.</p>
<p>The USD ROP wrote USDA and USDC files. I then packaged the USDC with OpenUSD's <code>UsdUtils.CreateNewUsdzPackage</code>, including its external reference and textures. The ROP's layer-flattening option retained the authored references and variant selection; this was not a fully flattened stage. SideFX documents USD ROP as the Solaris stage export node.<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup></p>
<table>
<thead>
<tr>
<th>File</th>
<th align="right">Actual size</th>
<th>Contents needed beside it</th>
</tr>
</thead>
<tbody><tr>
<td>solaris-test.usda</td>
<td align="right">7,918 bytes</td>
<td>183-byte reference layer and 385 bytes of PNG textures</td>
</tr>
<tr>
<td>solaris-test.usdc</td>
<td align="right">5,724 bytes</td>
<td>Same reference layer and PNG textures</td>
</tr>
<tr>
<td>solaris-test.usdz</td>
<td align="right">7,014 bytes</td>
<td>Self-contained package in this test</td>
</tr>
</tbody></table>
<p>You can <a href="https://digitalgarden.si/marketing/research/openusd/solaris-test.usdz">download the tested USDZ fixture</a> and <a href="https://digitalgarden.si/marketing/research/openusd/results-2026-10-16.json">inspect the measurements and object diagnostics</a>. The fixture is test geometry made for this article; it contains no production garden plants.</p>
<h2>Load USD into an existing Three.js scene</h2>
<p>The loader composes USD using Rust compiled to WebAssembly, runs loading in a worker, and returns a Three.js group. The package also offers a ready-made viewer, but I used <code>loadUsd</code> so the test scene owned its renderer, camera and lights.<sup><a id="footnote-ref-1-2" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup></p>
<pre><code class="language-bash">npm install @huggingface/usd-web-viewer@0.1.1 three@0.186.1
</code></pre>
<p>For Vite development, exclude the package from dependency pre-bundling so its worker and WASM asset URLs remain usable.<sup><a id="footnote-ref-1-3" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup></p>
<pre><code class="language-javascript">export default {
  optimizeDeps: { exclude: ['@huggingface/usd-web-viewer'] }
};
</code></pre>
<p>This is the load pattern used in the test, with cleanup included:</p>
<pre><code class="language-javascript">import * as THREE from 'three';
import { loadUsd } from '@huggingface/usd-web-viewer';

const placement = new THREE.Group();
scene.add(placement); // scene, camera and lights already exist

const result = await loadUsd('/fixtures/solaris-test.usdz', {
  textures: 'full',
  maxTriangles: 100000,
  maxInstances: 1000
});

placement.add(result.root);
const textures = await result.complete;
console.log(result.info.warnings, result.info.materialKinds, textures);

// When this stage is no longer needed:
// scene.remove(placement);
// result.dispose();
</code></pre>
<p>The example adds explicit limits suitable for this small fixture; the recorded measurements used the loader's defaults. <code>complete</code> waits for textures after geometry becomes available. Keep a separate placement group: the returned root already carries the source-unit scale and up-axis rotation. In my test, its scale was <code>[0.01, 0.01, 0.01]</code> and its X rotation was <code>-π/2</code>. Overwriting those transforms would undo the conversion.</p>
<h2>What survived, and what did not</h2>
<p>All three formats produced the same feature results. The returned diagnostics counted 108 mesh instances, eight geometries, nine materials and 3,182 triangles, with zero missing layers. The 100-copy grid was one actual <code>THREE.InstancedMesh</code>; the diagnostic mesh count should not be read as a draw-call count.</p>
<table>
<thead>
<tr>
<th>Feature</th>
<th>Observed result</th>
</tr>
</thead>
<tbody><tr>
<td>Mesh with UVs</td>
<td>The two-triangle checker panel rendered with its UVs</td>
</tr>
<tr>
<td>Implicit geometry</td>
<td>Both Sphere prims were tessellated and rendered</td>
</tr>
<tr>
<td>UsdPreviewSurface</td>
<td>Mapped to MeshStandardMaterial; checker texture visible</td>
</tr>
<tr>
<td>MaterialX-only material</td>
<td>Grey fallback; material-fallback warning</td>
</tr>
<tr>
<td>PNG textures</td>
<td>full loaded two textures with zero failures; preview loaded one and omitted normalMap</td>
</tr>
<tr>
<td>displayColor</td>
<td>Green cube and other authored colors appeared in material data</td>
</tr>
<tr>
<td>PointInstancer</td>
<td>100 placements in one InstancedMesh</td>
</tr>
<tr>
<td>Instanceable references</td>
<td>Both referenced objects appeared; external layer resolved</td>
</tr>
<tr>
<td>Variant selection</td>
<td>The authored cool variant produced RGB [0.25, 0.4, 0.95]</td>
</tr>
<tr>
<td>Units and axis</td>
<td>Z-up centimeters became Y-up meters; fixture height measured 1 meter</td>
</tr>
<tr>
<td>Purpose</td>
<td>guide and proxy cubes absent; render cube present</td>
</tr>
<tr>
<td>Curves</td>
<td>BasisCurves absent, with a prim-unsupported warning</td>
</tr>
<tr>
<td>USD camera and light</td>
<td>Zero cameras and lights in the returned group; the harness supplied its own</td>
</tr>
</tbody></table>
<p>The flat normal map tests whether the normal-map path is loaded, not the quality of detailed normal shading. The reference test checks that both objects survive; it does not establish that every native-instance pattern is GPU-instanced. The variant test checks an authored selection at load time, not a live JavaScript variant switch. Those references and variants were already composed before export. The <a href="https://digitalgarden.si/blog/openusd-pipeline-beginners/">OpenUSD beginner stage</a> is the smaller Solaris exercise for that composition: five files, a payload and a season variant, with no browser loader involved.</p>
<p>The loader reported:</p>
<pre><code class="language-json">{
  "materialKinds": { "preview": 1, "displayColor": 7, "fallback": 1 },
  "upAxis": "Z",
  "metersPerUnit": 0.01
}
</code></pre>
<p>There were exactly two warning codes: <code>prim-unsupported</code> for <code>/World/UnsupportedCurve</code> and <code>material-fallback</code> for <code>/World/Looks/MaterialXOnly</code>. Their full diagnostic messages are preserved in the linked results JSON. They agree with the package's documented warning categories.<sup><a id="footnote-ref-3" href="#footnote-3" data-footnote-ref aria-describedby="footnote-label">3</a></sup></p>
<p>This is why I would check warnings before judging a USD import by its first frame. The stage loaded successfully, but it did not preserve every authored feature.</p>
<h2>Measured load times: small local fixture, not a mobile benchmark</h2>
<p>After the first USDC load, I ran five sequential loads of each format. The table shows the median of each measurement. Fetches were local and caches were warm; these numbers are not internet delivery times, mobile results or a ranking of the formats on large scenes.</p>
<table>
<thead>
<tr>
<th>Measurement</th>
<th align="right">USDA</th>
<th align="right">USDC</th>
<th align="right">USDZ</th>
</tr>
</thead>
<tbody><tr>
<td>Loader totalMs</td>
<td align="right">11.1 ms</td>
<td align="right">12.1 ms</td>
<td align="right">10.0 ms</td>
</tr>
<tr>
<td>Fetch / parse / compose</td>
<td align="right">2.1 / 0.9 / 3.9 ms</td>
<td align="right">2.1 / 0.5 / 4.5 ms</td>
<td align="right">2.5 / 0.5 / 3.9 ms</td>
</tr>
<tr>
<td>Await loadUsd, from call</td>
<td align="right">21.3 ms</td>
<td align="right">21.0 ms</td>
<td align="right">20.4 ms</td>
</tr>
<tr>
<td>Await complete, from call</td>
<td align="right">23.9 ms</td>
<td align="right">36.6 ms</td>
<td align="right">21.5 ms</td>
</tr>
<tr>
<td>Reported WASM memory</td>
<td align="right">2,031,616 bytes</td>
<td align="right">1,966,080 bytes</td>
<td align="right">1,966,080 bytes</td>
</tr>
<tr>
<td>Triangles</td>
<td align="right">3,182</td>
<td align="right">3,182</td>
<td align="right">3,182</td>
</tr>
</tbody></table>
<p>The first USDC load in the session reported 39.1 ms for <code>info.stats.totalMs</code>, 82.3 ms until <code>loadUsd</code> resolved and 87.2 ms until textures completed. I did not clear the browser's entire cache to establish a controlled cold-start experiment. The five raw samples per format are included in the results file.</p>
<p><code>totalMs</code> is the loader's own statistic. My surrounding <code>performance.now()</code> measurements include more of the call lifecycle, so I report both. The fetch, parse and compose medians are independent medians and should not be added to reconstruct total time. Likewise, <code>wasmMemoryBytes</code> is not total browser or GPU memory.<sup><a id="footnote-ref-3-2" href="#footnote-3" data-footnote-ref aria-describedby="footnote-label">3</a></sup></p>
<p>For a real garden asset, the next useful test is much larger: plant texture fidelity, many repeated meshes, actual mobile devices and the existing WebGPU/TSL renderer. A seven-kilobyte USDZ cannot answer those questions. Two composed NVIDIA stages later used this same viewer. That <a href="https://digitalgarden.si/blog/simready-usd-to-glb-threejs-webgpu/">direct OpenUSD load of SimReady assets</a> also recorded where Blender, Houdini and usd2gltf stopped preserving materials.</p>
<h2>USD or glTF for the garden?</h2>
<p>My practical result is that USD is now worth testing as a direct preview path from Solaris. The external reference, selected variant and instancing cases worked in this fixture. The material and geometry omissions also make the boundary clear.</p>
<p>The live <a href="https://digitalgarden.si/play/">garden</a> continues to use its existing GLB pipeline. Khronos describes glTF as a format for efficient runtime 3D asset delivery; that remains a useful fit for a public browser experience.<sup><a id="footnote-ref-4" href="#footnote-4" data-footnote-ref aria-describedby="footnote-label">4</a></sup> I did not run a matched Houdini-to-glTF benchmark, so this article makes no USD-versus-GLB speed or size claim. Exporting the already-loaded browser group to GLB would carry forward the import's losses and would not be a fair source-format comparison.</p>
<p>Three.js also provides its own <code>USDLoader</code> addon for USD, USDA, USDC and USDZ.<sup><a id="footnote-ref-5" href="#footnote-5" data-footnote-ref aria-describedby="footnote-label">5</a></sup> I did not benchmark it here. Results for one loader should not be assigned to another. A later <a href="https://digitalgarden.si/blog/houdini-usd-vs-gltf-threejs/">Houdini USD and glTF test</a> did use that loader on one authored plant pair, exported as GLB, USDC and USDZ. USDLoader kept a single plant and dropped the wind attributes, so the result is a feature failure rather than a speed ranking.</p>
<h2>FAQ</h2>
<p><strong>Can Three.js load USD files from Houdini?</strong></p>
<p>In this test, yes: Solaris-generated USDA, USDC and a packaged USDZ loaded through usd-web-viewer 0.1.1. The supplied fixture and results let you check the same narrow case.</p>
<p><strong>Does a successful load mean MaterialX works?</strong></p>
<p>No. My MaterialX-only sphere rendered grey and produced a warning. The UsdPreviewSurface checker panel worked. Test the actual material network you intend to ship. Unreal accepted an embedded MaterialX surface in a separate <a href="https://digitalgarden.si/blog/houdini-unreal-usd/">Houdini to Unreal test</a> after the shader input used the MaterialX name and the mtlx context was selected.</p>
<p><strong>Why do normals disappear in preview mode?</strong></p>
<p>The package's preview mode omits normal maps.<sup><a id="footnote-ref-3-3" href="#footnote-3" data-footnote-ref aria-describedby="footnote-label">3</a></sup> My full-mode load attached a normalMap; the preview-mode load did not. Request full mode when that map matters.</p>
<p><strong>Does this show that USD will make the phone garden faster?</strong></p>
<p>No. These are desktop localhost measurements of a small diagnostic scene. No iPhone, Android, Safari or production-garden frame-rate claim follows from them.</p>
<p><strong>What would I test next?</strong></p>
<p>One real Houdini plant, its actual textures, a controlled glTF comparison and both garden rendering backends. The <a href="https://digitalgarden.si/blog/claude-code-for-3d/">Claude Code and Houdini workflow</a> is how I keep these experiments repeatable: run the script, inspect the output, then decide what belongs in production.</p>
<hr />
<p><a href="https://digitalgarden.si/blog/openusd-threejs-usd-web-viewer/">Read the original article, supporting data and updates</a>.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>Hugging Face, <a href="https://github.com/huggingface/huggingface.js/tree/main/packages/usd-web-viewer">usd-web-viewer package and README</a>. This experiment installed and inspected the published npm 0.1.1 artifact; the repository main branch can change. <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩︎</a> <a href="#footnote-ref-1-2" data-footnote-backref aria-label="Back to reference 1">↩︎<sup>2</sup></a> <a href="#footnote-ref-1-3" data-footnote-backref aria-label="Back to reference 1">↩︎<sup>3</sup></a></p>
</li>
<li id="footnote-2">
<p>SideFX, <a href="https://www.sidefx.com/docs/houdini/nodes/lop/usd_rop.html">USD ROP</a>. Node names and parameters in this article were also checked in the installed Houdini 22.0.429. <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩︎</a></p>
</li>
<li id="footnote-3">
<p>Hugging Face, <a href="https://github.com/huggingface/huggingface.js/blob/main/packages/usd-web-viewer/src/types.ts">load options, diagnostics and timing types</a>. The installed 0.1.1 declaration files were checked alongside the measurements. <a href="#footnote-ref-3" data-footnote-backref aria-label="Back to reference 3">↩︎</a> <a href="#footnote-ref-3-2" data-footnote-backref aria-label="Back to reference 3">↩︎<sup>2</sup></a> <a href="#footnote-ref-3-3" data-footnote-backref aria-label="Back to reference 3">↩︎<sup>3</sup></a></p>
</li>
<li id="footnote-4">
<p>Khronos Group, <a href="https://www.khronos.org/gltf/">glTF: Runtime 3D Asset Delivery</a>. <a href="#footnote-ref-4" data-footnote-backref aria-label="Back to reference 4">↩︎</a></p>
</li>
<li id="footnote-5">
<p>Three.js, <a href="https://threejs.org/docs/pages/USDLoader.html">USDLoader documentation</a>. <a href="#footnote-ref-5" data-footnote-backref aria-label="Back to reference 5">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded></item><item><title><![CDATA[I Tested machine-skill with Codex: Two Three.js Pump Simulations]]></title><description><![CDATA[A hands-on Codex test of machine-skill: a 4:1 gear pump and rain-barrel irrigation rig, with real screenshots, solver checks, fixes and downloadable d]]></description><link>https://digitalgarden-alicia.hashnode.dev/i-tested-machine-skill-with-codex-two-three-js-pump-simulations</link><guid isPermaLink="true">https://digitalgarden-alicia.hashnode.dev/i-tested-machine-skill-with-codex-two-three-js-pump-simulations</guid><dc:creator><![CDATA[Alicia lu]]></dc:creator><pubDate>Sun, 25 Oct 2026 07:17:46 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>AI-assisted disclosure: Codex assisted with research, code and writing. Measurements, failure cases and limitations are retained from the original article.</p>
</blockquote>
<blockquote>
<p>Originally published by Alicia on 2026-10-18: <a href="https://digitalgarden.si/blog/codex-machine-skill-threejs/">Digital Garden by Alicia</a>.</p>
</blockquote>
<p><strong>Codex can use machine-skill to build an interactive Three.js machine. My two pump tests also needed corrections before their numbers, cutaways and controls told a consistent story.</strong> On October 18, 2026, I used the skill in a Codex desktop conversation on my Windows workstation, built a hand-cranked gear pump and a rain-barrel irrigation rig, and checked the resulting pages in Chromium.</p>
<p>The useful result is an editable explanation: turn the handle, expose a pump chamber, change the water level and inspect the equations. The result does not establish that either pump would perform this way in hardware. The dimensions and operating constants below describe deliberately constructed teaching fixtures.</p>
<p>I make <a href="https://digitalgarden.si/">digitalgarden.si</a>, an <a href="https://digitalgarden.si/virtual-garden/">interactive 3D garden</a>. This experiment ran separately from the live garden. Its renderer, plants and sound were not replaced.</p>
<p><img src="https://digitalgarden.si/marketing/research/machine-skill/gear-pump.png" alt="The actual Codex gear-pump page, with a brass reduction wheel and a computed 0.153 L/min readout" /></p>
<h2>What I actually ran</h2>
<p>The original brief suggested Claude Code. I changed the test host to Codex and read the same machine-skill instructions and reference example. I did not run Claude, compare models, or measure a one-prompt success rate.</p>
<table>
<thead>
<tr>
<th>Component</th>
<th>Tested setup</th>
</tr>
</thead>
<tbody><tr>
<td>Agent workflow</td>
<td>This Codex desktop conversation; explicit reading of the cloned SKILL.md</td>
</tr>
<tr>
<td>Model identifier</td>
<td>Not independently recorded by this running session</td>
</tr>
<tr>
<td>Upstream</td>
<td>AirsupHQ/machine-skill, commit e0bd066ac5beaf48512935de2fc0f0e55636853a</td>
</tr>
<tr>
<td>Three.js</td>
<td>0.170.0, as pinned by the skill; WebGL2</td>
</tr>
<tr>
<td>Browser</td>
<td>Playwright Chromium 156.0.8078.4, headless, Windows</td>
</tr>
<tr>
<td>Graphics</td>
<td>ANGLE / NVIDIA GeForce RTX 5070 Ti / Direct3D 11</td>
</tr>
<tr>
<td>Measurement viewport</td>
<td>1440 × 900, device scale factor 1</td>
</tr>
<tr>
<td>Review</td>
<td>Four rounds per machine: whole, cut, exploded, X-ray, two joint views and film stills</td>
</tr>
<tr>
<td>Timing and cost</td>
<td>Prompt-to-first-page time, token use and cost were not instrumented</td>
</tr>
</tbody></table>
<p>This was an iterative build in one conversation. The prompts supplied the machines and questions; Codex then authored their geometry and solvers, ran checks and repaired the output. A multi-stage build should not be described as an untouched one-line result.</p>
<h2>Using this Claude-oriented skill with Codex</h2>
<p>The upstream README explicitly includes Codex among hosts that can read SKILL.md.<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup> I cloned that repository into an isolated experiment folder and had Codex read the file directly. I did not test automatic discovery, a slash command or a global installation.</p>
<p>For a project-scoped setup, OpenAI documents .agents/skills as a Codex skill location.<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup> A reproducible placement is:</p>
<pre><code class="language-powershell">git clone https://github.com/AirsupHQ/machine-skill.git .agents/skills/machine
git -C .agents/skills/machine checkout e0bd066ac5beaf48512935de2fc0f0e55636853a
</code></pre>
<p>Then explicitly ask Codex to read that SKILL.md and build the machine. Claude's /reload-skills instruction is not part of this tested Codex workflow.</p>
<p>The page contains the kit and machine code inline, but imports Three.js and fonts over the network. A single HTML file is convenient to move; it is not a fully offline package. These are purpose-written mathematical models and kinematic animations, without a general contact solver.<sup><a id="footnote-ref-1-2" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup></p>
<h2>Test 1: a hand-cranked gear pump</h2>
<p>The task I used was:</p>
<pre><code class="language-text">Use machine-skill with Codex: a hand-cranked gear pump with a 4:1 spur gear reduction, slow it down so I can see the teeth mesh, and say what breaks if I crank it dry.
</code></pre>
<p>The input pinion has 20 teeth and the reduction wheel 80. At module 0.005 m, their pitch radii are 0.05 m and 0.20 m, giving a 0.25 m centre distance. The two pump gears have 16 teeth each. The display slows rotation by 1:8 while leaving the reported rpm and flow on the solver's time scale.</p>
<p>At 60 hand-crank rpm, the model produces 15 pump rpm and <strong>0.153 L/min</strong>. That value follows the declared displacement of 12 mL/revolution and assumed volumetric efficiency of 0.85:</p>
<pre><code class="language-text">pump rpm = crank rpm / 4
flow (L/min) = 12 × pump rpm × 0.85 / 1000
</code></pre>
<p>I checked inputs of 0, 30, 60 and 120 rpm, the actual animation's inverse rotation ratio and phase relationship, and the modelled centre distance. Those checks passed. The gear helper itself describes its tooth profile as involute-like: seeing teeth pass one another is not proof of a manufactured involute pair or solved tooth contact.</p>
<p>The first dry-running model had a more revealing error. Its friction and cooling assumptions put the equilibrium temperature below the chosen 85 °C failure threshold, even at maximum crank speed. The failure could never occur. The corrected example declares a smaller illustrative seal thermal mass of 0.5 J/K and cooling of 0.02 W/K, alongside 1.6 N m friction. Removing water now gives zero water flow and a computed temperature that crosses the threshold during the simulated run.</p>
<p>Those constants were selected to demonstrate the thermal equation. The 85 °C threshold is an assumption, not a manufacturer's seal rating, and the test does not predict a real pump's time to damage.</p>
<h2>Test 2: rain-barrel drip irrigation</h2>
<p>The second task was:</p>
<pre><code class="language-text">Use machine-skill with Codex: a garden drip irrigation rig fed by a small centrifugal pump from a rain barrel; show flow at each emitter and what happens when the barrel runs low.
</code></pre>
<p><img src="https://digitalgarden.si/marketing/research/machine-skill/irrigation-cut.png" alt="The actual irrigation test in cutaway mode, showing the water barrel, pump and four labelled outlets" /></p>
<p>This fixture uses a centrifugal pump curve intersected with a system curve. The system includes lift from the barrel, a lumped pipe resistance and four equal emitters. At full speed and a 0.50 m water level, the result is approximately <strong>0.276 L/min total, or 4.14 L/h at each emitter</strong>.</p>
<pre><code class="language-text">H_pump = 3 s² - 0.08 Q²
H_system = 0.75 - h + 0.1 Q² + 10.197 (60 Q / 32)²
</code></pre>
<p>Here H and h are metres, Q is L/min and s is speed divided by rated speed. Each emitter is assumed to deliver 8 L/h at 1 bar, with square-root dependence on pressure. The constants describe this fixture; they were not fitted to a measured pump curve.</p>
<p>The automated checks compare the two heads at the operating point and verify that four branch flows sum to the total. At water levels of 4 cm or below, the suction input is considered uncovered and calculated water flow becomes zero. Just above that threshold, flow returns. The <strong>Expose the intake</strong> button sets the water-level input to 2 cm; it does not bypass the solver and force a decorative failure animation. Reset restores the water and temperature state.</p>
<p>Equal pressure at all four emitters is a simplification. This model does not resolve pressure variations along individual hoses, air entrainment, priming or cavitation. Shaft motion is displayed at 1:80.</p>
<h2>What needed fixing</h2>
<table>
<thead>
<tr>
<th>Area</th>
<th>Gear pump</th>
<th>Irrigation rig</th>
</tr>
</thead>
<tbody><tr>
<td>Physics</td>
<td>Initial thermal assumptions made the failure unreachable; corrected and disclosed</td>
<td>Pump/system intersection and water cutoff passed; branch pressure is assumed equal</td>
</tr>
<tr>
<td>Geometry and scale</td>
<td>Added an actual cavity around the pump gears; checked 0.25 m gear centres</td>
<td>Replaced a solid casing with a shell around the impeller; metres are explicit</td>
</tr>
<tr>
<td>Materials and cutaway</td>
<td>Shared X-ray materials initially hid the base</td>
<td>Same shared-material problem; isolated shell materials while retaining clipping and cap callbacks</td>
</tr>
<tr>
<td>Performance and layout</td>
<td>Reduced detail without deleting parts; repaired narrow-screen framing</td>
<td>Reduced detail without deleting parts; moved controls off the model on narrow screens</td>
</tr>
</tbody></table>
<p>The follow-up work had three goals: audit the equations and failure thresholds; isolate X-ray materials and inspect solid sections; then halve the geometry budget while keeping every registered part and repair the mobile framing. The review also caught an easy-to-miss implementation detail: cloning a material alone did not preserve the kit's custom shader callback and shared clipping planes. Both needed to remain attached.</p>
<p>The original studio palette is still fairly clean. It reads as a mechanical explanation rather than a weathered prop from the garden. Weathering, detailed valves and seal geometry would be further art and engineering work.</p>
<h2>Measured performance</h2>
<p>The upstream screenshot script initially reported about 2 FPS on this Windows setup. A local change to its browser launcher, adding --use-angle=d3d11, restored hardware rendering and roughly 60 FPS. This is an environment correction, not a claimed benefit of reducing triangles.</p>
<table>
<thead>
<tr>
<th>Page</th>
<th align="right">Standard triangles</th>
<th align="right">Lightweight triangles</th>
<th align="right">Kit-reported parts</th>
</tr>
</thead>
<tbody><tr>
<td>Gear pump</td>
<td align="right">60,100</td>
<td align="right">18,628</td>
<td align="right">25 in both versions</td>
</tr>
<tr>
<td>Irrigation</td>
<td align="right">37,560</td>
<td align="right">7,908</td>
<td align="right">33 in both versions</td>
</tr>
</tbody></table>
<p>The lightweight variant reduces curve segments, tooth-flank samples and bevel detail. It retains the tooth counts and the registered part list. Three one-second animation-frame samples per standard page stayed around 60 FPS at the stated viewport, with no page exceptions in those checks. These short, warm desktop samples do not establish sustained phone performance, startup speed or a rendering limit.</p>
<p>The original narrow layout enlarged the model behind its text. I separated the canvas from the control panel and checked 390 px and 320 px desktop-emulated viewports. That is responsive testing, not a physical-phone acceptance result.</p>
<h2>Download, recording and embedding</h2>
<p>The <a href="https://digitalgarden.si/marketing/research/machine-skill/codex-pump-demos.zip">demo ZIP</a> contains both HTML pages, the generator, reproduction notes and the upstream licence. Extract it and open either page in a WebGL2 browser with network access. When served over localhost, add ?low=1 to select the lightweight version. The <a href="https://digitalgarden.si/marketing/research/machine-skill/results-2026-10-18.json">test results JSON</a> preserves the geometry, solver, mode and responsive checks.</p>
<p>I also tested the upstream recording command locally with a Windows launcher adaptation. Both delivered clips are 20 seconds, 960 × 600, H.264 at 15 fps, without music. They use seconds 8–28 of the generated film, beginning with the assembled machine; the full 28-second takes were retained locally. Full decode and the recorder's pink-frame scan passed. These are local delivery files, separate from the desktop FPS measurements; their sizes and hashes are in the result data.</p>
<p>A plain same-origin local iframe loaded the irrigation page in a 390 px viewport. The site's current Content Security Policy, however, explicitly blocks frames. A local copy of that policy blocked the same embed as expected; the production policy also restricts the external script and font sources used by these pages. <strong>I have not embedded this renderer in the production garden or changed that policy.</strong> The downloadable example and screenshots are the published results.</p>
<p>Merging a generated machine into the garden would require deliberate scene, material and solver integration. For comparison, my <a href="https://digitalgarden.si/blog/openusd-threejs-usd-web-viewer/">Houdini-to-Three.js OpenUSD test</a> deals with importing authored assets. The earlier <a href="https://digitalgarden.si/blog/claude-code-for-3d/">Claude Code for 3D workflow</a> describes another agent setup; it is not the host used here. A later <a href="https://digitalgarden.si/blog/claude-code-vs-codex-threejs/">paired Claude Code and Codex task</a> used one frozen plant-wind starter and checked both untouched outputs on WebGPU and the WebGL2 fallback.</p>
<h2>Would I use it again?</h2>
<p>Yes, for an inspectable mechanism study or an article that needs moving parts and equations readers can examine. The kit gave the two tests a useful common structure. The more valuable part of the exercise was catching mistakes that a good-looking screenshot concealed: an unreachable failure, a solid block where a pump chamber should be, and transparency shared with unrelated parts.</p>
<p>For a production garden prop, I would still author and review the asset for that scene. For engineering analysis, I would start from measured dimensions and validated component data. This test establishes a working Codex-assisted explanation workflow, with its assumptions left visible.</p>
<p>The next <a href="https://digitalgarden.si/blog/img2interior-codex-threejs-garden/">img2interior test with Codex</a> applies that review loop to a Blender-authored room: three output contracts, a 95 MB runtime payload, actual cloth/hem checks and a viewer lifecycle repair.</p>
<hr />
<p><a href="https://digitalgarden.si/blog/codex-machine-skill-threejs/">Read the original article, supporting data and updates</a>.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>AirsupHQ, machine-skill README and source at the tested commit, including the Codex host note, template, geometry helpers and solver recipes. <a href="https://github.com/AirsupHQ/machine-skill/tree/e0bd066ac5beaf48512935de2fc0f0e55636853a">https://github.com/AirsupHQ/machine-skill/tree/e0bd066ac5beaf48512935de2fc0f0e55636853a</a> <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩︎</a> <a href="#footnote-ref-1-2" data-footnote-backref aria-label="Back to reference 1">↩︎<sup>2</sup></a></p>
</li>
<li id="footnote-2">
<p>OpenAI, Agent Skills documentation, skill invocation and repository skill locations. <a href="https://developers.openai.com/codex/skills/">https://developers.openai.com/codex/skills/</a> <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded></item><item><title><![CDATA[Houdini ML Volume Upres for Pyro: Pretrained Models and a Real Training Run]]></title><description><![CDATA[A Houdini 22 Pyro cook log: pretrained 2x and 3x upres, CPU and DirectML inference, a small CUDA training run, cache sizes and field-resolution pitfal]]></description><link>https://digitalgarden-alicia.hashnode.dev/houdini-ml-volume-upres-for-pyro-pretrained-models-and-a-real-training-run</link><guid isPermaLink="true">https://digitalgarden-alicia.hashnode.dev/houdini-ml-volume-upres-for-pyro-pretrained-models-and-a-real-training-run</guid><dc:creator><![CDATA[Alicia lu]]></dc:creator><pubDate>Sun, 25 Oct 2026 07:15:29 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>AI-assisted disclosure: Codex assisted with research, code and writing. Measurements, failure cases and limitations are retained from the original article.</p>
</blockquote>
<blockquote>
<p>Originally published by Alicia on 2026-10-19: <a href="https://digitalgarden.si/blog/houdini-ml-volume-upres-pyro/">Digital Garden by Alicia</a>.</p>
</blockquote>
<p><strong>Start with the pretrained ML Volume Upres nodes, then train a small model only after the data and inference path work.</strong> On this workstation, the native billowy-smoke recipe produced valid 2× and 3× density volumes; the native training recipe also exported an ONNX model that could be run again through the same SOP. The interesting failures were mostly about field selection, environment setup and what a short training run can actually prove.</p>
<p>This is a cook log from Houdini 22.0.429 with a commercial license, OpenUSD 26.5, Windows 11, an AMD Ryzen 9 9950X, 64 GB RAM and an NVIDIA RTX 5070 Ti with 16 GB VRAM. The observed driver was 617.42. Training logs selected CUDA with PyTorch 2.9.0+cu130 in a separate recipe-managed Python environment. GPU inference below explicitly used DirectML where stated; selecting Automatic alone is not proof of which provider was chosen.</p>
<p><img src="https://digitalgarden.si/marketing/research/briefs-20261019/pyro-low-2x-3x.png" alt="Actual Houdini density volumes rendered side by side with one camera and one shared Blender volume shader: low resolution, pretrained 2x and pretrained 3x" /></p>
<p>Left to right: low resolution, pretrained 2×, pretrained 3×. These are the actual density VDBs at frame 12, rendered in Blender 5.2 with the same camera, lights and volume-shader density. This compares the volume output, not Karma/PyroLook renderer parity.</p>
<h2>What the node changes</h2>
<p>ML Volume Upres applies a trained ONNX model to a native volume or VDB.<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup> It increases spatial resolution and sharpens density detail. The upstream simulation still determines the smoke's motion, shape and interaction with sources or colliders. It is not a replacement for the Pyro solver or a guarantee that a coarse simulation has the dynamics of a high-resolution one.</p>
<p>The distinction matters for the <a href="https://digitalgarden.si/virtual-garden/">Digital Garden</a> pipeline. If I eventually use a volume element in a shot, I still need to art-direct its source, motion, density and lighting. A sharper cached field is one stage of that work. This is also a different job from image-to-mesh. The <a href="https://digitalgarden.si/blog/comfyui-3d-workflow/">ComfyUI 3D workflow</a> cleans a generated prop and checks the GLB in Blender and Houdini; it does not upres a smoke cache.</p>
<p>SideFX's documentation also requires DOP-level permissions even though the inference node lives in SOPs. The documented supported license categories include FX, Apprentice, Indie and Education; Houdini Core is excluded.<sup><a id="footnote-ref-1-2" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup> The local run used the licensed native application, not a Python package that merely resembles a volume operation.</p>
<h2>Reproduce the pretrained path</h2>
<p>The native <code>sidefx::recipe::pyro::billowysmoke</code> recipe created the source, rasterization, Pyro solver, file-cache node, <code>ml_volumeupres_2x</code>, <code>ml_volumeupres_3x</code> and PyroLook nodes. Both ML nodes were present; the 2× branch feeds the default look path. The pretrained model paths resolve through Houdini's <code>shfs:</code> resources rather than an arbitrary downloaded model.</p>
<p>I set the solver's voxel size to 0.15 and cached frames 1–12 before inference. The first frame took about 1.54 s, followed by roughly 0.06–0.30 s per frame in this small fixture. Total low-resolution cache cook/write time was about 2.56 s. These figures include native geometry access and writing the compressed cache, not just solver time.</p>
<p>For each inference result, I connected a File SOP to the same cached frame. This prevents a live simulation recook from being mistaken for ML inference time.</p>
<table>
<thead>
<tr>
<th>Frame-12 output</th>
<th>Density resolution</th>
<th>Voxel size</th>
<th>Compressed full-field cache</th>
</tr>
</thead>
<tbody><tr>
<td>Low resolution</td>
<td>33 × 17 × 32</td>
<td>0.15</td>
<td>97,957 bytes</td>
</tr>
<tr>
<td>Pretrained 2×, DirectML</td>
<td>68 × 36 × 64</td>
<td>0.075</td>
<td>412,887 bytes</td>
</tr>
<tr>
<td>Pretrained 3×, DirectML</td>
<td>102 × 54 × 96</td>
<td>0.05</td>
<td>1,164,333 bytes</td>
</tr>
</tbody></table>
<p>The dimensions are not exact multiples of the low-resolution active bounds. Sparse-grid resampling, padding and pruning can change those bounds. The voxel transform gives a more useful scale check than demanding <code>old_dimension × factor</code> on every active bounding-box dimension.</p>
<h2>CPU and GPU inference on the same input</h2>
<table>
<thead>
<tr>
<th>Model</th>
<th>Automatic</th>
<th>CPU</th>
<th>DirectML</th>
</tr>
</thead>
<tbody><tr>
<td>Pretrained 2×</td>
<td>2.582 s</td>
<td>1.104 s</td>
<td>0.638 s</td>
</tr>
<tr>
<td>Pretrained 3×</td>
<td>2.155 s</td>
<td>12.943 s</td>
<td>1.990 s</td>
</tr>
</tbody></table>
<p>These are single sequential cook-and-write observations, not medians or isolated-kernel benchmarks. The first Automatic run includes model/provider warm-up. Another editor was open, and no allocator-level peak VRAM measurement was recorded. The table supports the narrow observation that DirectML successfully processed these inputs and the larger 3× CPU cook took longer here. It does not establish a universal acceleration ratio.</p>
<p>The node documentation lists Automatic, CPU, CUDA, DirectML on Windows and CoreML on macOS.<sup><a id="footnote-ref-1-3" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup> This test explicitly exercised CPU and DirectML. CUDA was used by training; CUDA inference was not separately benchmarked, and the Automatic inference provider was not independently identified.</p>
<p>At this render size, both pretrained results show sharper folds than the coarse input, with additional small ridges in the 3× result. That is a visual observation about this frame. A short sequence, an alternate source and a render at the actual delivery size are needed before deciding whether the extra cache and cook cost is worthwhile for a shot.</p>
<h2>The field-resolution trap</h2>
<p>The recipe's ML selection is <code>@name=density</code>. In the default result, temperature stayed at 0.15 voxel size while density became 0.075 for 2×. Velocity started at 0.30 and also stayed unchanged. Merely seeing a detailed density field does not prove that every companion field has been resampled.</p>
<p>I enabled <strong>Naive Upsample Other Fields</strong> and selected the fields using the actual primitive-group expression:</p>
<pre><code class="language-text">@name=temperature @name=vel
</code></pre>
<p>Temperature then became approximately 0.075 and velocity approximately 0.15. The velocity field remained coarser than density because the operation scales its existing grid; it does not force every field onto the density transform. Flame, which was empty in this smoke fixture and not selected, retained its source transform.</p>
<p>A string such as <code>temperature vel</code> did not select those named primitives in this test. Check the result's field names and voxel transforms, rather than treating a checked parameter as proof that the intended fields changed. If a downstream operation requires identical transforms, explicitly resample to a reference grid and verify it.</p>
<h2>A real, deliberately small training run</h2>
<p>The native <strong>ML Train Volume Upres</strong> recipe supplies a training branch and an inference branch.<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup> I used its own Pyro source, cached high-resolution frames at voxel size 0.075, and fed frames 8–12 to the training subnet.</p>
<table>
<thead>
<tr>
<th>Setting</th>
<th>Local value</th>
</tr>
</thead>
<tbody><tr>
<td>Scale factor</td>
<td>2</td>
</tr>
<tr>
<td>Training frames</td>
<td>8–12, interval 1</td>
</tr>
<tr>
<td>Tiles per frame</td>
<td>2</td>
</tr>
<tr>
<td>Tile size / input padding</td>
<td>48 / 6</td>
</tr>
<tr>
<td>Scatter seed</td>
<td>1</td>
</tr>
<tr>
<td>Channel multiplier</td>
<td>1</td>
</tr>
<tr>
<td>Batch size</td>
<td>1</td>
</tr>
<tr>
<td>Training schedule</td>
<td>Two partial cooks of two epochs; four epochs total</td>
</tr>
<tr>
<td>Training device</td>
<td>CUDA, as recorded by the recipe log</td>
</tr>
</tbody></table>
<p>The first end-to-end cook, including creation and population of the separate Python environment, took about 226.7 s. A second partial cook resumed the checkpoint and completed in 22.2 s. That second number is <strong>not</strong> the total setup-and-training time, and it includes TOP orchestration rather than isolating the GPU training kernel.</p>
<p>The recipe produced an ONNX graph of 20,304 bytes plus 210,816 bytes of external tensor data, alongside its metadata and checkpoint files. Preserve the external <code>.onnx.data</code> file with the graph. The presence of an <code>.onnx</code> filename by itself is not a complete model-delivery check.</p>
<p>The subsequent inference test loaded that exported model into ML Volume Upres and cooked the same cached low-resolution frame. The final recorded CPU and DirectML cook/write observations were about 0.591 s and 0.517 s, with no node errors or warnings. Both produced a valid density volume at approximately 0.075 voxel size.</p>
<h2>What the training run does not establish</h2>
<p>Four epochs on ten source tiles are a functional smoke test. They demonstrate dataset export, native CUDA training, checkpoint continuation, ONNX export and independent inference. They do not show that the custom model beats the shipped model or generalizes to fire, mist, wind or collision-heavy smoke. The training log remains resumable; I did not label the model converged.</p>
<p>No model-specific blur or seam failure was established in this small run, so there is no invented before/after model-fix story. Before using a trained model for a shot, hold out frames or another simulation, compare against both the pretrained result and a genuine high-resolution solve, inspect temporal stability, and judge it under the final lighting.</p>
<p>For the comparison image I kept one common Blender shader unchanged. I did not perform a dedicated PyroLook/Karma retuning pass. A denser-looking render may need shader-density or exposure changes, but that decision should follow an actual shot comparison; it is not a mandatory numeric compensation derived from the upres factor.</p>
<h2>Deliverables and the next useful test</h2>
<p>The <a href="https://digitalgarden.si/marketing/research/briefs-20261019/pyro-upres-test.zip">experiment archive</a> contains the scripts, compact metrics and the small exported model with its dependencies. Full local simulation caches, Python environment and editable scene files remain in the experiment workspace rather than in the public web download.</p>
<p>Start with pretrained inference, get a reliable cache boundary, inspect every field, and only then train. If the source motion is wrong, fix the simulation. If only the visible density detail is too soft, the upres comparison becomes a meaningful next experiment. The <a href="https://digitalgarden.si/blog/houdini-unreal-usd/">Houdini/Unreal test</a> and <a href="https://digitalgarden.si/blog/openusd-pipeline-beginners/">OpenUSD beginner stage</a> cover downstream scene organization; neither is a claim that this smoke cache was shipped in the live browser garden.</p>
<h2>References</h2>
<hr />
<p><a href="https://digitalgarden.si/blog/houdini-ml-volume-upres-pyro/">Read the original article, supporting data and updates</a>.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>SideFX, <a href="https://www.sidefx.com/docs/houdini/nodes/sop/ml_volumeupres.html">ML Volume Upres</a>, checked 19 October 2026. <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩︎</a> <a href="#footnote-ref-1-2" data-footnote-backref aria-label="Back to reference 1">↩︎<sup>2</sup></a> <a href="#footnote-ref-1-3" data-footnote-backref aria-label="Back to reference 1">↩︎<sup>3</sup></a></p>
</li>
<li id="footnote-2">
<p>SideFX, <a href="https://www.sidefx.com/docs/houdini/ml/train_solutions/ml_trainvolumeupres.html">ML Train Volume Upres recipe</a>, checked 19 October 2026. <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded></item><item><title><![CDATA[OpenUSD for Artists: A First Stage Tested in Houdini Solaris]]></title><description><![CDATA[Build a five-file OpenUSD scene with references, payloads and season variants. Tested in Houdini Solaris, with downloadable files and composition chec]]></description><link>https://digitalgarden-alicia.hashnode.dev/openusd-for-artists-a-first-stage-tested-in-houdini-solaris</link><guid isPermaLink="true">https://digitalgarden-alicia.hashnode.dev/openusd-for-artists-a-first-stage-tested-in-houdini-solaris</guid><dc:creator><![CDATA[Alicia lu]]></dc:creator><pubDate>Sun, 25 Oct 2026 07:13:43 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>AI-assisted disclosure: Codex assisted with research, code and writing. Measurements, failure cases and limitations are retained from the original article.</p>
</blockquote>
<blockquote>
<p>Originally published by Alicia on 2026-10-19: <a href="https://digitalgarden.si/blog/openusd-pipeline-beginners/">Digital Garden by Alicia</a>.</p>
</blockquote>
<p>OpenUSD becomes easier to understand when one asset appears twice in a scene and changing its source updates both copies. This experiment does exactly that: a cylinder and sphere stand in for a plant, two references place it in a garden, and a stronger shot layer changes its season.</p>
<p><strong>The practical starting point is five small files, not a production scene.</strong> Keep geometry in a payload, give the asset an entry layer with a default prim, reference that entry layer into a layout, and put shot-specific choices in a separate override. The downloadable example was opened and checked with Houdini 22.0.429 and its bundled OpenUSD 26.5 on Windows. The fixture is deliberately schematic; it is not one of the production plants in <a href="https://digitalgarden.si/virtual-garden/">Digital Garden</a>.</p>
<p><img src="https://digitalgarden.si/marketing/research/briefs-20261019/usd-garden.png" alt="The same two referenced plants, one with the spring look and one with the autumn look, rendered through Houdini's bundled USD Storm renderer" /></p>
<p>The image is a render of the actual USD stage through Houdini's bundled Storm renderer. Headless Solaris loaded the files through Sublayer LOPs. Composition evidence was read with OpenUSD's property-stack API; this is not a screenshot of the interactive Scene Graph Details panel.</p>
<h2>The five-file project</h2>
<p>The <a href="https://digitalgarden.si/marketing/research/briefs-20261019/openusd-beginners.zip">example archive</a> contains the five authored layers, the native Component Builder output, the test script and measured results. Start with this structure:</p>
<pre><code class="language-text">garden_usd/
  garden.usda
  autumn_shot.usda
  autumn_overrides.usda
  assets/lily/
    lily.usda
    payload.usda
</code></pre>
<p>A <strong>layer</strong> contains opinions: a radius, a color, a reference, a variant selection. A <strong>prim</strong> is an item at a path such as <code>/garden/lily_01</code>. A <strong>stage</strong> is the composed result of opening a root layer and resolving those opinions. <strong>Composition</strong> is the process that makes those files act like one scene.</p>
<p>An entry layer needs a useful root. The plant file sets <code>defaultPrim = "lily"</code>, so a reference can bring in the intended asset without spelling out its root path. This follows OpenUSD's referencing workflow.<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup> In the layout, the asset root is remapped from <code>/lily</code> to <code>/garden/lily_01</code>; a second reference maps it to <code>/garden/lily_02</code>.</p>
<p>The geometry layer has a 0.3 m cylinder and a small sphere. It uses Y-up coordinates and meters. The entry file carries a payload and the season variants:</p>
<pre><code class="language-usda">#usda 1.0
(
    defaultPrim = "lily"
    metersPerUnit = 1
    upAxis = "Y"
)
def Xform "lily" (
    kind = "component"
    prepend payload = @./payload.usda@
    variants = { string season = "spring" }
    prepend variantSets = "season"
)
{
    # The complete spring/autumn definitions are in the archive.
}
</code></pre>
<p>The geometry is gray in <code>payload.usda</code>. A season variant authors a stronger display-color opinion on the petals. The spring selection yields approximately <code>(0.95, 0.95, 0.9)</code>; autumn yields <code>(0.8, 0.55, 0.2)</code>.</p>
<h2>Reference, payload, sublayer and variant: choose by the edit</h2>
<table>
<thead>
<tr>
<th>Mechanism</th>
<th>Use it for</th>
<th>What this fixture does</th>
</tr>
</thead>
<tbody><tr>
<td>Reference</td>
<td>Reuse an asset at a new scene path</td>
<td>Place the plant twice</td>
</tr>
<tr>
<td>Payload</td>
<td>Delay loading heavier content</td>
<td>Keep plant geometry behind the asset entry</td>
</tr>
<tr>
<td>Sublayer</td>
<td>Stack opinions in the same namespace</td>
<td>Put a shot override above the layout</td>
</tr>
<tr>
<td>Variant</td>
<td>Offer named alternatives on one prim</td>
<td>Select spring or autumn</td>
</tr>
</tbody></table>
<p>A reference is not automatically an instance. The two layout prims reuse the same source, but this particular beginner example does not set <code>instanceable = true</code>. That distinction matters when discussing memory or per-instance editing. For measured native and point instancing, see the separate <a href="https://digitalgarden.si/blog/houdini-unreal-usd/">Houdini to Unreal test</a>.</p>
<p>The shot layer lists the override first and the garden second:</p>
<pre><code class="language-usda">subLayers = [
    @./autumn_overrides.usda@,
    @./garden.usda@
]
</code></pre>
<p>The override selects autumn for <code>lily_01</code>. Opening <code>garden.usda</code> gives one spring plant and one autumn plant. Opening <code>autumn_shot.usda</code> gives two autumn plants. Both results were asserted in the bundled OpenUSD API and in the stage returned by a Solaris Sublayer LOP.</p>
<h2>Find the opinion that actually wins</h2>
<p>When a color appears unchanged, do not start by re-exporting everything. Inspect the attribute's property stack:</p>
<pre><code class="language-python">attr = stage.GetPrimAtPath(
    "/garden/lily_01/geo/petals"
).GetAttribute("primvars:displayColor")
for spec in attr.GetPropertyStack():
    print(spec.layer.identifier, spec.path)
</code></pre>
<p>For the autumn shot, the first color opinion came from <code>/lily{season=autumn}geo/petals</code> in <code>lily.usda</code>. The weaker gray opinion came from <code>payload.usda</code>. The shot layer chooses the variant; the selected variant supplies the color. Those are different contributions.</p>
<p>There is a useful trap here: a local attribute value can override a value authored inside a variant. OpenUSD's own variant tutorial explicitly demonstrates clearing the local value before expecting variant colors to win.<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup> I also hit this while preparing the companion Unreal fixture: a radius authored outside the growth variants kept defeating the bud selection. Clearing that local radius made the visible size change work. That was a fixture authoring error, not an Unreal variant limitation.</p>
<h2>Unloading a payload does not mean an empty tree</h2>
<p>One claim in the original draft did not survive the test. With <code>Usd.Stage.LoadNone</code>, both plant roots remained, but so did <code>/geo</code> and <code>/geo/petals</code> as over prims introduced by the selected variants. The cylinder stem, defined only in the payload, was absent.</p>
<p>Loading just <code>/garden/lily_01</code> brought back its stem while the other stem stayed absent. The useful test is therefore <strong>whether the payload-defined geometry is populated</strong>, not whether every descendant path disappears. Inspect <code>IsDefined()</code>, the prim type and payload state before treating a remaining tree item as loaded geometry.</p>
<p>This difference is easy to miss in a Scene Graph Tree. A prim path can exist because a stronger layer contains opinions for it even when the referenced geometry behind that path is unloaded.</p>
<h2>The native Solaris Component Builder version</h2>
<p>I built a second version with native nodes: two Component Geometry nodes, Component Geometry Variants and Component Output. Each geometry branch contains the same simple stem and sphere, with a different SOP color. The variant names are <code>spring</code> and <code>autumn</code> under the <code>geo</code> set; these are geometry variants carrying color, rather than a separate MaterialX look-development test.</p>
<p>Component Output wrote <code>lily.usd</code>, <code>payload.usdc</code>, <code>geo.usdc</code> and <code>mtl.usdc</code>. Reopening the entry file returned both variant names, with no final node errors or warnings. SideFX describes this node family as a framework for reusable component models with separated geometry, materials, payloads and variants.<sup><a id="footnote-ref-3" href="#footnote-3" data-footnote-ref aria-describedby="footnote-label">3</a></sup></p>
<p>Use forward slashes in Houdini parameter paths. My first Python pass supplied Windows backslashes to an expression-evaluated parameter and produced escape warnings. Switching to <code>Path.as_posix()</code> removed them. The final downloadable Component Builder output is the corrected run.</p>
<h2>What the checks establish</h2>
<table>
<thead>
<tr>
<th>Check</th>
<th>Observed result</th>
</tr>
</thead>
<tbody><tr>
<td>Five authored layers</td>
<td>Opened with bundled OpenUSD 26.5</td>
</tr>
<tr>
<td>Base layout</td>
<td>Spring first plant, autumn second plant</td>
</tr>
<tr>
<td>Shot override</td>
<td>Both plants autumn</td>
</tr>
<tr>
<td>Property stack</td>
<td>Selected asset variant wins over payload gray</td>
</tr>
<tr>
<td>Payload control</td>
<td>Loading one plant restores only its payload-defined stem</td>
</tr>
<tr>
<td>Session variant switch</td>
<td>Spring → autumn → spring returned the expected colors</td>
</tr>
<tr>
<td>Native Component Builder</td>
<td>Two variants, four exported USD layers, clean final cook</td>
</tr>
<tr>
<td>Bundled <code>usdchecker</code></td>
<td>Root stage and enumerated season selections passed</td>
</tr>
</tbody></table>
<p>Houdini's installation also includes <code>usdview.cmd</code> and <code>usdchecker.exe</code>. This test invoked <code>usdchecker</code> and used Storm for the render. It did not validate the interactive <code>usdview</code> GUI, compress a five-day learning exercise into a claimed learning outcome, or benchmark a large production stage.</p>
<p>For a first exercise, rebuild this folder, change the second reference's placement, add a winter selection, then create a shot that changes only one plant. If that works, you have used the core ideas rather than merely memorized their names. Keep this separate from format support in a receiving application: the <a href="https://digitalgarden.si/blog/openusd-threejs-usd-web-viewer/">Three.js USD viewer test</a> demonstrates why a valid stage can still lose a material or primitive type in a particular viewer. The <a href="https://digitalgarden.si/blog/houdini-usd-vs-gltf-threejs/">two-plant USD and glTF export</a> asks that question of Solaris output loaded by Three.js USDLoader. Two composed NVIDIA packages later posed it at a different scale: the <a href="https://digitalgarden.si/blog/simready-usd-to-glb-threejs-webgpu/">SimReady USD</a> notes record which of Blender, Houdini, usd2gltf and direct loading kept materials.</p>
<h2>References</h2>
<hr />
<p><a href="https://digitalgarden.si/blog/openusd-pipeline-beginners/">Read the original article, supporting data and updates</a>.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>OpenUSD, <a href="https://openusd.org/release/tut_referencing_layers.html">Referencing Layers</a>. Current online tutorial checked 19 October 2026; local results above use the separately recorded 26.5 build. <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩︎</a></p>
</li>
<li id="footnote-2">
<p>OpenUSD, <a href="https://openusd.org/release/tut_authoring_variants.html">Authoring Variants</a>. <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩︎</a></p>
</li>
<li id="footnote-3">
<p>SideFX, <a href="https://www.sidefx.com/docs/houdini/nodes/lop/componentoutput.html">Component Output</a>. <a href="#footnote-ref-3" data-footnote-backref aria-label="Back to reference 3">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded></item><item><title><![CDATA[ComfyUI 3D Workflow: From a Million-Triangle Draft to a DCC-Tested Prop]]></title><description><![CDATA[A real TRELLIS.2 workflow in ComfyUI: generate, remesh, decimate, unwrap and bake a prop, then check its GLB, textures and scale in Blender and Houdin]]></description><link>https://digitalgarden-alicia.hashnode.dev/comfyui-3d-workflow-from-a-million-triangle-draft-to-a-dcc-tested-prop</link><guid isPermaLink="true">https://digitalgarden-alicia.hashnode.dev/comfyui-3d-workflow-from-a-million-triangle-draft-to-a-dcc-tested-prop</guid><dc:creator><![CDATA[Alicia lu]]></dc:creator><pubDate>Sun, 25 Oct 2026 07:11:44 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>AI-assisted disclosure: Codex assisted with research, code and writing. Measurements, failure cases and limitations are retained from the original article.</p>
</blockquote>
<blockquote>
<p>Originally published by Alicia on 2026-10-19: <a href="https://digitalgarden.si/blog/comfyui-3d-workflow/">Digital Garden by Alicia</a>.</p>
</blockquote>
<p><strong>A useful ComfyUI 3D workflow needs an acceptance stage after generation.</strong> In this test, TRELLIS.2 produced a 1,175,882-triangle mesh from the official example image. Remeshing, decimation, UV unwrapping and texture baking reduced the deliverable to 19,738 triangles in a 4.55 MB textured GLB. Native Blender and Houdini imports then exposed a second problem: an embedded texture that worked in Blender was not directly usable by this Houdini SOP's generated material.</p>
<p>The test ran on ComfyUI 0.37.0, frontend 1.52.7, PyTorch 2.10.0.dev20251209+cu130 and an RTX 5070 Ti with 16 GB VRAM on Windows. The DCC checks used Blender 5.2.0 LTS and Houdini 22.0.429. This was a local TRELLIS.2 run, not a partner-node credit purchase or a comparison with Meshy, Rodin or Hunyuan. The separate <a href="https://digitalgarden.si/blog/meshy-3d-agent-houdini-cleanup/">Meshy Houdini cleanup</a> follows that other download path: a free Meshy 6 Lite GLB, a rejected cleanup, and a 3,000-triangle instance in the browser. Mesh generation is not the only local model experiment here. <a href="https://digitalgarden.si/blog/houdini-ml-volume-upres-pyro/">ML volume upres for Pyro</a> applies an ONNX model to cached Houdini smoke instead of turning an image into triangles.</p>
<p><img src="https://digitalgarden.si/marketing/research/briefs-20261019/comfy-raw-clean.png" alt="Actual generated axe before cleanup on the left and the reduced textured result on the right, imported and rendered in Blender" /></p>
<p>The raw shape on the left is shown with neutral clay; the reduced result on the right has the generated and baked PBR material. The camera, lighting and relative display scale are shared. Because the materials differ, this image demonstrates the delivered stages rather than serving as a controlled shading-quality comparison.</p>
<h2>Start with the official graph and one input</h2>
<p>I used Comfy's published <strong>Pixal3D &amp; TRELLIS.2: Image to Model</strong> template and its <code>viking_wolf_rune_axe.png</code> input.<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup> That keeps the starting asset and graph identifiable. The axe is a workflow fixture, not a production garden asset or an original character design for sale.</p>
<p>The graph was adapted to select TRELLIS.2 and keep the 512-resolution shape stage. The 1024/1536 upsample branch was omitted for this 16 GB test. This matters: the result should not be described as the unmodified highest-resolution template output.</p>
<p>The active generation path was:</p>
<ol>
<li>Load the input and remove its background with BiRefNet.</li>
<li>Center/crop the subject and obtain DINOv3 image conditioning.</li>
<li>Sample the sparse structure at 12 steps.</li>
<li>Decode the structure and sample shape at 20 steps.</li>
<li>Decode the raw mesh.</li>
<li>Sample the texture stage at 12 steps and decode the material voxels.</li>
</ol>
<p>All three sampler seeds were fixed to 42. The model was the official <code>trellis_2_int8_convrot.safetensors</code> distribution, with the shape VAE, texture VAE and DINOv3 encoder identified by the template. The exact submitted API prompt and model hashes are included with the test evidence.</p>
<h2>Save the raw mesh before improving it</h2>
<p>The raw GLB was 20,790,420 bytes. Blender counted 1,175,882 triangles and no UV layer. Houdini read the same primitive count. A default material slot existed, but the raw shape export did not contain the finished baked texture set.</p>
<p>Do not equate the vertex count across importers. Houdini counted 556,025 points in the raw input, while Blender's import with Merge Vertices enabled yielded 554,859 vertices. The triangle count agreed. Importers may weld or split vertices around normals, UV seams and other attributes; report both the importer and its settings when comparing these figures.</p>
<p>The raw asset is useful as a shape/detail source. It is much heavier than the reduced static-prop target, and it still needs UV and texture delivery. That is why a visually successful preview is only the beginning of the acceptance process.</p>
<h2>The cleanup chain that actually ran</h2>
<table>
<thead>
<tr>
<th>Stage</th>
<th>Local settings or result</th>
</tr>
</thead>
<tbody><tr>
<td>Remesh Mesh</td>
<td>UDF, resolution 256, two smoothing iterations</td>
</tr>
<tr>
<td>Decimate Mesh</td>
<td>Requested 20,000 faces; output 19,738 triangles</td>
</tr>
<tr>
<td>Smooth Normals</td>
<td>180-degree setting from the template</td>
</tr>
<tr>
<td>Unwrap Mesh</td>
<td>1024 target atlas; automatic charts</td>
</tr>
<tr>
<td>Normal bake</td>
<td>From the raw shape onto the reduced mesh</td>
</tr>
<tr>
<td>Texture bake</td>
<td>Generated base color, metallic and roughness material data</td>
</tr>
<tr>
<td>Ambient occlusion</td>
<td>Separate bake applied to the delivered material</td>
</tr>
<tr>
<td>Save GLB</td>
<td>4,548,732 bytes with UVs and embedded textures</td>
</tr>
</tbody></table>
<p>The exact prompt records all effective inputs, including remaining template values. The requested triangle target is a budget, not a promise of an exact face count. Unwrapping reported 489 charts and an internal atlas of 1039 × 1026 in this run. That is automatic output; it does not establish good artist-authored seam placement.</p>
<p>The first API submission failed at Remesh Mesh because I encoded its dynamic <code>sign_mode</code> input as a nested object. The running ComfyUI API expected the template's flat selector and prefixed child widget fields. Restoring that representation fixed execution. This was an API-prompt construction error, not a failed TRELLIS.2 generation.</p>
<p>The successful cleanup/texture submission reported 34.38 s of execution and reused already cached structure and shape nodes. A preceding failed submission had already generated and saved the raw shape. <strong>Do not quote 34.38 s as an end-to-end cold generation benchmark.</strong> Model downloading, startup and cached work are different costs; no isolated peak VRAM measurement was captured.</p>
<h2>Blender: verify the actual exported GLB</h2>
<p>Blender imported the reduced GLB with a UVMap and its embedded material. With Merge Vertices enabled it counted 9,792 vertices and 19,738 triangles. The studio render above is from the imported file, not ComfyUI's preview framebuffer.</p>
<p>That distinction catches problems a node preview can hide: missing image payloads, unsupported material channels, a wrong pivot or an exporter that saved a different result from the displayed mesh. At minimum, reopen the exact exported path, inspect the material's image nodes and rotate the prop under a new light.</p>
<p>The visual result is promising as a static prop draft. The wolf-head ornaments, handle ends and small carved details still contain soft or irregular forms. The automatic UV layout has many charts. I would not call it a finished hero prop, a deformation-ready mesh or clean subdivision topology based on this test.</p>
<h2>Houdini: the embedded-texture failure and its fix</h2>
<p>Houdini's glTF SOP loaded both meshes without geometry errors. The cleaned GLB exposed point UVs, normals and tangents, but the SOP warned that embedded images could not be used directly as texture filenames for its generated material.</p>
<p>The fix in this experiment was explicit and reproducible: import that same GLB in Blender and export <strong>glTF Separate</strong>, producing a <code>.gltf</code>, its binary buffer and external image files. Loading the separate <code>.gltf</code> through Houdini's native glTF SOP then returned no import warnings or errors.</p>
<p>The <code>.gltf</code> descriptor alone was only 2,202 bytes. That is <strong>not the asset size</strong>: its binary buffer and image files are dependencies. Keep the entire folder together. The original textured GLB remains the convenient single-file web deliverable; the external glTF folder solves this specific Houdini material-import boundary.</p>
<p>After unpacking, I used a uniform transform to make the longest dimension 0.60 m and put the lower Y bound on the ground. Clean and Normal SOPs followed. The final geometry bounds were approximately <code>0.5994 × 0.6000 × 0.1021</code> m, with 19,721 primitives after cleanup. The change of 17 triangles is a local cleaning result, not a topology-quality score.</p>
<p>This is one reason to keep the generated raw mesh and a separate cleaned branch. A size or cleanup edit should not destroy the higher-detail source needed for later bakes.</p>
<h2>What I would accept before placing it in a scene</h2>
<ol>
<li>The exported file reopens independently in the target application.</li>
<li>Triangle count, attributes and every texture dependency are recorded.</li>
<li>The object has a deliberate physical size, orientation and pivot.</li>
<li>Base color is inspected for baked lighting, and roughness/metallic behave under new lights.</li>
<li>Small details survive the target camera distance and the chosen polygon budget.</li>
<li>Collision, LODs and performance are checked in the actual destination if those are required.</li>
</ol>
<p>This experiment completed generation, mesh post-processing, texturing, native imports, a render and a 60 cm geometry cleanup. It did not insert the axe into the production <a href="https://digitalgarden.si/virtual-garden/">interactive garden</a>, build collision or LODs, run animation/deformation acceptance, or compare paid partner models. Those claims require different tests.</p>
<p>For scene packaging after cleanup, the <a href="https://digitalgarden.si/blog/openusd-pipeline-beginners/">OpenUSD starter example</a> demonstrates references and payloads, while the <a href="https://digitalgarden.si/blog/houdini-unreal-usd/">Houdini to Unreal test</a> checks units, materials and native instancing. Generation supplies one part; a stable pipeline makes that part reusable.</p>
<h2>Downloads and licensing scope</h2>
<p>The <a href="https://digitalgarden.si/marketing/research/briefs-20261019/comfyui-3d-test.zip">workflow evidence archive</a> contains the exact API prompt, compact measured results and model hashes. The original workflow and example input are linked to their official source rather than presented as my own artwork. The local raw/clean GLBs, external-texture folder and editable DCC scenes are retained for inspection.</p>
<p>Comfy's documentation identifies TRELLIS.2 as the local model for this template.<sup><a id="footnote-ref-1-2" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup> The upstream project publishes its license in its repository.<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup> Paid partner services and other model families have separate access conditions and terms; this run does not establish their cost, availability or commercial permissions.</p>
<h2>References</h2>
<hr />
<p><a href="https://digitalgarden.si/blog/comfyui-3d-workflow/">Read the original article, supporting data and updates</a>.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>ComfyUI, <a href="https://docs.comfy.org/tutorials/3d/trellis2">TRELLIS.2 Image to 3D Model workflow</a>, including the official graph, input image and model links; checked 19 October 2026. <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩︎</a> <a href="#footnote-ref-1-2" data-footnote-backref aria-label="Back to reference 1">↩︎<sup>2</sup></a></p>
</li>
<li id="footnote-2">
<p>Microsoft, <a href="https://github.com/microsoft/TRELLIS.2">TRELLIS.2 repository</a>, primary source for the model and license. <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded></item><item><title><![CDATA[Houdini to Three.js: Testing glTF Compression and Instancing]]></title><description><![CDATA[Measured Houdini GLB, Draco, Meshopt and KTX2 results: file size, texture memory, attribute survival and 1,000 plant draw calls in Three.js.]]></description><link>https://digitalgarden-alicia.hashnode.dev/houdini-to-three-js-testing-gltf-compression-and-instancing</link><guid isPermaLink="true">https://digitalgarden-alicia.hashnode.dev/houdini-to-three-js-testing-gltf-compression-and-instancing</guid><dc:creator><![CDATA[Alicia lu]]></dc:creator><pubDate>Sun, 25 Oct 2026 07:08:32 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>AI-assisted disclosure: Codex assisted with research, code and writing. Measurements, failure cases and limitations are retained from the original article.</p>
</blockquote>
<blockquote>
<p>Originally published by Alicia on 2026-10-19: <a href="https://digitalgarden.si/blog/houdini-gltf-threejs-optimization/">Digital Garden by Alicia</a>.</p>
</blockquote>
<p>For this Houdini to Three.js test, texture compression changed file size far more than geometry compression. The larger catch was semantic: the tested gltfpack outputs lost the custom attributes needed by wind. I measured export, browser loading and repeated placement separately so a small GLB could not silently pass as a usable plant.</p>
<p>The renderer, format and splat notes that build on this export test are collected in <a href="https://digitalgarden.si/blog/threejs-3d-garden-rendering/">the full rendering guide for the interactive 3D garden</a>.</p>
<p>The fixture is an original procedural grape hyacinth built for this experiment, with 6,056 triangles, one material and an intentionally noisy 2048 × 2048 PNG test texture. It is not a production garden asset or a representative survey of plant textures. Codex executed the scripts and browser checks on 19 October 2026.</p>
<p><img src="https://digitalgarden.si/marketing/research/webgpu-series-20261019/plant-raw.png" alt="The original procedural plant loaded in the browser with its test texture" /></p>
<h2>Start with the native export</h2>
<p>Houdini 22.0.429 used the <code>gltf::2.0</code> ROP. A single quad exported as two triangles without a separate Divide SOP. Ten packed copies exported as ten meshes and ten nodes, totaling 60,560 triangles. Packing inside Houdini did not create <code>EXT_mesh_gpu_instancing</code> in this output. If the website needs an InstancedMesh, create and verify that runtime representation explicitly.</p>
<p>Custom point attributes used the underscore prefix required by the exporter. The raw GLB contained <code>_STIFF</code> and <code>_PHASE</code> alongside position, normal, color and UV attributes.<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup> An initial validator failure exposed zero length normals at degenerate leaf endpoints. I fixed the generator’s normal fallback, exported again and validated the final candidates. That repair is part of the tested result; the first export was not accepted.</p>
<h2>File size and attribute survival</h2>
<p>The native Draco export used Houdini. The other candidates used the official Windows gltfpack 1.3.0 binary or glTF Transform CLI 4.5.1. Sizes are actual file bytes, shown as decimal MB. Every final GLB passed the glTF validator with zero errors; nonblocking unused data and extension warnings are retained in the evidence.</p>
<table>
<thead>
<tr>
<th>Candidate</th>
<th>File MB</th>
<th>Triangles</th>
<th>Wind attributes survive?</th>
<th>Median local fetch to submitted frame</th>
</tr>
</thead>
<tbody><tr>
<td>Native GLB + PNG</td>
<td>4.438</td>
<td>6,056</td>
<td>Yes</td>
<td>65.7 ms</td>
</tr>
<tr>
<td>Native Draco + PNG</td>
<td>4.367</td>
<td>6,056</td>
<td>Yes</td>
<td>49.8 ms</td>
</tr>
<tr>
<td>gltfpack Meshopt + PNG</td>
<td>4.191</td>
<td>6,056</td>
<td>No</td>
<td>53.0 ms</td>
</tr>
<tr>
<td>gltfpack Meshopt + KTX2, 2048</td>
<td>0.793</td>
<td>6,056</td>
<td>No</td>
<td>33.4 ms</td>
</tr>
<tr>
<td>gltfpack Meshopt + KTX2, 1024</td>
<td>0.238</td>
<td>6,056</td>
<td>No</td>
<td>16.7 ms</td>
</tr>
<tr>
<td>glTF Transform Meshopt + PNG</td>
<td>4.208</td>
<td>6,056</td>
<td>Yes</td>
<td>55.6 ms</td>
</tr>
<tr>
<td>Native vertex color, no texture</td>
<td>0.290</td>
<td>6,056</td>
<td>Yes</td>
<td>16.7 ms</td>
</tr>
</tbody></table>
<p>The browser used Three.js 0.186.1, its Draco and Basis decoders, and MeshoptDecoder on WebGPU. Each row used five distinct local HTTP URLs. Timing covered fetch, parse, decode, compile and the first submitted frame. Decoder code and GPU pipelines were warm. These are localhost observations with frame scheduling granularity, not production network, device cold start or GPU completion timings. A separate WebGL2 check also loaded the 2048 KTX2 candidate and reported the same 12 BC7 mip levels and 5,592,432 uploaded bytes.</p>
<p>Draco reduced this texture dominated file by only about 1.6%. KTX2 plus a smaller texture had a much larger effect, but those gltfpack candidates failed the wind attribute requirement. The <code>-kv</code> experiment did not rescue the unknown attributes. The glTF Transform Meshopt candidate retained them, using generic attribute quantization at 12 bits. Attribute presence still does not guarantee sufficient numerical precision for every shader; inspect the decoded values and appearance before release. A later <a href="https://digitalgarden.si/blog/threejs-glb-loading-cache-first-frame/">Three.js GLB loading</a> test separated cache, decoding and the first GPU frame on a texture-free plant. Meshopt reduced that file from 319,648 to 130,296 bytes with identical decoded attributes, and the fresh-URL first-frame median fell from 35.7 ms to 17.6 ms. Those twenty rows use a different protocol from this warm, texture-dominated table.</p>
<h2>Commands and their limits</h2>
<p>The tested gltfpack commands were:</p>
<pre><code class="language-sh">gltfpack -i plant-raw.glb -o plant-pack.glb -cc -ce ext
gltfpack -i plant-raw.glb -o plant-ktx2.glb -cc -ce ext -tc -tj 4
gltfpack -i plant-raw.glb -o plant-1024.glb -cc -ce ext -tc -tj 4 -tl 1024
</code></pre>
<p>The tested texture size option was <code>-tl 1024</code>. These commands use <code>EXT_meshopt_compression</code> explicitly and the binary’s built in texture encoder.<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup> I did not install or benchmark a separate toktx conversion. The alternate Meshopt command was:</p>
<pre><code class="language-sh">gltf-transform meshopt plant-raw.glb plant-transform.glb --level high
</code></pre>
<p>The vertex color candidate removed the image entirely and encoded phase/stiffness into red/green channels. It is a data transport probe, not an accepted visual replacement: a material that displays those channels as ordinary vertex color will tint the plant. A deliberate vertex color palette can suit stylized plants, but it cannot preserve details that existed only in the texture. A later <a href="https://digitalgarden.si/blog/houdini-vs-geometry-nodes-plant-export/">Houdini versus Geometry Nodes plant export</a> kept three vertex-color groups on one shared material. The first Blender Geometry Nodes file wrote a valid GLB that had lost its green stem and foliage colors; the corrected export kept all three groups.</p>
<h2>Uploaded texture data is a separate budget</h2>
<p>The 2048 PNG expands to an estimated 22,369,620 bytes for an RGBA8 texture and full mip chain. The actual BC7 KTX2 mip arrays totaled 5,592,432 bytes at 2048 and 1,398,128 bytes at 1024. Those totals describe uploaded mip data. They do not measure total process VRAM, decoder scratch space, staging allocations or driver overhead.</p>
<p>The visual check matters as much as this arithmetic. Downsizing the artificial high frequency texture changes its fine detail; the smaller candidate should not be described as visually identical. Real foliage needs its own close and distant views, especially around alpha edges.</p>
<h2>One thousand plants: count the draws</h2>
<p>A second test repeated the plant 1,000 times at 1280 × 720, DPR 1, with a 30 second orbit after warmup. All paths used the same simple material settings and stayed close to the display’s 60 Hz ceiling on the RTX 5070 Ti.</p>
<table>
<thead>
<tr>
<th>Runtime representation</th>
<th>Total render calls</th>
<th>Total triangles</th>
</tr>
</thead>
<tbody><tr>
<td>InstancedMesh, LOD0</td>
<td>2</td>
<td>6,056,001</td>
</tr>
<tr>
<td>BatchedMesh, LOD0</td>
<td>1,001</td>
<td>6,056,001</td>
</tr>
<tr>
<td>Merged geometry, LOD0</td>
<td>2</td>
<td>6,056,001</td>
</tr>
<tr>
<td>InstancedMesh, all plants at LOD1</td>
<td>2</td>
<td>2,116,001</td>
</tr>
</tbody></table>
<p>The totals include a final output pass, which adds one call and one triangle. In the pinned WebGPU backend, the tested BatchedMesh path loops over draw commands. Its class name did not imply a single GPU draw. Merging reduced calls but duplicates vertex data; instancing shares geometry. The LOD row replaces the whole patch with its authored lower detail model, not a per instance LOD algorithm. The same question, across classic WebGL and the node renderer, is in the <a href="https://digitalgarden.si/blog/threejs-webgpu-vs-webgl-instanced-plants/">nine-path WebGPU and WebGL comparison</a>.</p>
<p>The practical acceptance rule is to check file size, decoded attributes, material appearance and runtime counters together. A later <a href="https://digitalgarden.si/blog/simready-usd-to-glb-threejs-webgpu/">Houdini USD to GLB</a> test made the same distinction on NVIDIA SimReady geometry: optimizing a broken export cleared the reported index errors only by changing the mesh, so the untouched geometry snapshot was the file that passed first. The <a href="https://digitalgarden.si/marketing/research/webgpu-series-20261019/webgpu-garden-tests.zip">small reproducible experiment</a> includes the measurements and owned generator. Continue with <a href="https://digitalgarden.si/blog/houdini-procedural-plants-realtime-web/">procedural plants and wind</a> or the <a href="https://digitalgarden.si/blog/webgpu-garden-houdini-threejs/">complete garden pipeline</a>. Production models were not replaced by these test outputs.</p>
<hr />
<p><a href="https://digitalgarden.si/blog/houdini-gltf-threejs-optimization/">Read the original article, supporting data and updates</a>.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>SideFX, <a href="https://www.sidefx.com/docs/houdini/nodes/out/gltf.html">glTF ROP</a>, including custom attribute and export behavior. <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩︎</a></p>
</li>
<li id="footnote-2">
<p>meshoptimizer, <a href="https://meshoptimizer.org/gltf/">gltfpack documentation</a>. Also see the official <a href="https://gltf-transform.dev/cli">glTF Transform CLI</a>. Sources checked 19 October 2026. <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded></item><item><title><![CDATA[How I Built a WebGPU 3D Garden with Houdini and Three.js]]></title><description><![CDATA[Inside Alicia’s interactive 3D garden: Houdini assets, Three.js TSL, measured WebGPU and WebGL2 results, and the limits of this desktop test.]]></description><link>https://digitalgarden-alicia.hashnode.dev/how-i-built-a-webgpu-3d-garden-with-houdini-and-three-js</link><guid isPermaLink="true">https://digitalgarden-alicia.hashnode.dev/how-i-built-a-webgpu-3d-garden-with-houdini-and-three-js</guid><dc:creator><![CDATA[Alicia lu]]></dc:creator><pubDate>Sun, 25 Oct 2026 07:06:26 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>AI-assisted disclosure: Codex assisted with research, code and writing. Measurements, failure cases and limitations are retained from the original article.</p>
</blockquote>
<blockquote>
<p>Originally published by Alicia on 2026-10-19: <a href="https://digitalgarden.si/blog/webgpu-garden-houdini-threejs/">Digital Garden by Alicia</a>.</p>
</blockquote>
<p>The <a href="https://digitalgarden.si/play/">interactive garden</a> starts in Houdini and runs in Three.js. Houdini makes the plant geometry and its deformation attributes; the browser places, lights and animates those plants. On 19 October 2026, I tested the current garden code on both WebGPU and WebGL2, then built a separate small plant to check the export pipeline. The useful result is a working authoring boundary, not a claim that switching APIs makes every scene faster.</p>
<p>This note is part of the <a href="https://digitalgarden.si/blog/threejs-3d-garden-rendering/">Three.js 3D garden rendering guide</a>, which groups the renderer, plant, format and splat tests in reading order.</p>
<p><img src="https://digitalgarden.si/marketing/research/webgpu-series-20261019/garden-webgpu.png" alt="The actual spring garden running through Three.js WebGPURenderer, with the active backend visible in the test harness" /></p>
<h2>What the browser actually renders</h2>
<p>The garden has four seasonal compositions, day and night transitions, shader driven wind, plant interaction and a photography mode. It uses Three.js 0.186.1 and TSL materials. The renderer is WebGPURenderer, which can use its WebGPU backend or its WebGL2 fallback. The active backend is checked after renderer initialization; the constructor name alone cannot establish which one ran.<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup></p>
<p>Houdini exports reusable plant assets rather than a giant frozen scene. The browser owns placement, camera movement, screen space detail decisions and interactions. That keeps artistic geometry work in the DCC while allowing the website to respond without a Houdini installation. A stem is authored once and placed many times. Its wind attributes describe how it may deform; they do not contain a frame by frame animation cache.</p>
<p>The production wind system keeps roots fixed and bends a shared stem while preserving coherent flower heads. It also deforms normals. The small downloadable experiment uses a much simpler sine displacement to make attribute loss easy to see. Its shader is a teaching fixture, not the production wind implementation. A later <a href="https://digitalgarden.si/blog/mutracer-houdini-plant-path-tracing/">MuTracer WebGPU path tracer</a> test rendered a frozen Houdini plant as a still: vertex colors, three instances and alpha masks survived on WebGPU, and the WebGL attempt lost its context. That image does not convert the animated wind in this garden.</p>
<h2>A measured comparison of the current garden</h2>
<p>I imported the existing Experience module from source commit <code>9c25ecc6ab2e</code> into a local measurement harness. Each backend ran the same 30 second partial orbit after a 1.5 second warmup. The canvas was 1920 × 1080 at device pixel ratio 1; the browser viewport was 1280 × 900 so the desktop scene policy stayed active. The spring scene contained 162,420 plant placements.</p>
<p>The machine used an NVIDIA RTX 5070 Ti. The in app browser reported Chromium 155; WebGPU reported NVIDIA Blackwell, and the WebGL2 fallback reported ANGLE over Direct3D 11. Both tabs were visible. Other applications were not closed, and this was one sample per backend.</p>
<table>
<thead>
<tr>
<th>Active backend</th>
<th>Mean FPS</th>
<th>95th percentile frame time</th>
<th>Mean render calls</th>
<th>Mean triangles, including passes</th>
</tr>
</thead>
<tbody><tr>
<td>WebGPU</td>
<td>54.66</td>
<td>33.4 ms</td>
<td>67.22</td>
<td>9.49 million</td>
</tr>
<tr>
<td>WebGL2 fallback</td>
<td>58.40</td>
<td>16.8 ms</td>
<td>67.24</td>
<td>9.48 million</td>
</tr>
</tbody></table>
<p>WebGL2 was faster in this particular run. The display was close to a 60 Hz ceiling, the detail scheduler was active, and the triangle totals vary slightly as detail transitions settle. These measurements do not isolate API overhead or establish a universal winner. They do establish that both backends render the current garden and that this scene does not justify an unconditional “WebGPU is faster” headline.</p>
<p>Both runs reused 36 assets from the browser’s garden cache, with zero model downloads. First frame observations were 1.59 seconds for WebGPU and 10.81 seconds for WebGL2. Those are single initialization observations with different backend pipeline work, not controlled cold load or CDN benchmarks. They should not be used to predict a visitor’s loading time. A later <a href="https://digitalgarden.si/blog/threejs-glb-loading-cache-first-frame/">Three.js GLB cache benchmark</a> published twenty controlled rows for one texture-free plant, separating a fresh URL from an immediate cache hit. It does not replace these single garden initialization observations. A later desktop profile counted the garden’s full transfer, including seasons loaded after the first frame, and set the spring scene beside a <a href="https://digitalgarden.si/blog/houdini-vs-code-procedural-threejs-slop-newyork/">procedural city built at runtime</a>. Both ran at 1920×1080 on WebGPU; they are different scenes, not a controlled pipeline comparison.</p>
<h2>Where the scene budget goes</h2>
<p>Instancing shares a plant geometry across many placements. It does not eliminate triangle processing, alpha testing or pixel shading. In the separate 1,000 plant fixture, 6,056 triangles per plant still produced about 6.06 million scene triangles. Reducing the plant to 2,116 triangles brought that to about 2.12 million. Both configurations remained near 60 FPS on this desktop, so the geometry reduction is clearer evidence than a rounded FPS comparison. A later <a href="https://digitalgarden.si/blog/threejs-webgpu-vs-webgl-instanced-plants/">WebGPU and WebGL plant matrix</a> repeated one plant as separate meshes, InstancedMesh and BatchedMesh on three renderer paths, and compared submission cost with the actual draw counters.</p>
<p>The garden’s current desktop policy keeps two decoded seasons resident and uses a bounded 160 MiB model cache on disk. Background preparation pauses around activity and photography. The ordinary scene has a scene pass and bloom; depth of field belongs to photography mode. These are deliberate budgets and lifecycle choices, not benefits supplied automatically by WebGPU.</p>
<p>Reversed depth is enabled only for the actual WebGPU backend in this project. Earlier fallback testing produced a blank post processing result with the reversed path. That is a project compatibility decision, not a general statement that WebGL2 cannot support any reversed depth technique.</p>
<h2>Test the boundary before scaling the garden</h2>
<p>The companion export test caught a practical failure: native Houdini GLB and the tested glTF Transform Meshopt path preserved <code>_STIFF</code> and <code>_PHASE</code>, while the tested gltfpack paths dropped them. A smaller file that loses the data required by wind is not an accepted replacement. The production garden assets were preserved during this experiment.</p>
<p>The most useful sequence is to validate one plant, reopen its editable Houdini scene, inspect the exported attributes, load the compressed candidate with the real browser decoders, and only then test repeated placements. A renderer screenshot alone cannot prove that deformation data survived compression.</p>
<p>Read the measured details in <a href="https://digitalgarden.si/blog/houdini-gltf-threejs-optimization/">Houdini glTF optimization</a>, <a href="https://digitalgarden.si/blog/houdini-procedural-plants-realtime-web/">procedural plants and wind attributes</a>, and <a href="https://digitalgarden.si/blog/gaussian-splats-threejs/">Gaussian splats in Three.js</a>. The Gaussian article uses a separate synthetic fixture; splats are not part of the production garden. A later <a href="https://digitalgarden.si/blog/spark-vs-gaussiansplats3d-threejs/">Spark versus GaussianSplats3D integration</a> ran that fixture on native Three.js, Spark and GaussianSplats3D, and recorded which encodings actually drew.</p>
<h2>Reproduce the experiment and understand its limits</h2>
<p>A later <a href="https://digitalgarden.si/blog/viggle-jst2-spz-threejs-garden/">free PINOC / JST-2 garden test</a> places an actual million-point SPZ environment beside an owned Houdini plant. It reports native WebGPU, native WebGL2 and Spark results, with a separate renderer-integration failure. That backdrop remains an independent research fixture.</p>
<p>Codex executed the native scripts and browser checks. The <a href="https://digitalgarden.si/marketing/research/webgpu-series-20261019/results-2026-10-19.json">compact measurement receipts</a> preserve versions, counts and methods alongside the rendered evidence.</p>
<p>The <a href="https://digitalgarden.si/marketing/research/webgpu-series-20261019/webgpu-garden-tests.zip">experiment download</a> contains the original small plant generator, compact results, owned fixtures and independent asset/splat viewers. It excludes the private garden engine and production models. The full garden measurements are supplied as dated results with their method, not as a promise that the private application is included in the download.</p>
<p>No physical phone was tested. Responsive layout checks are not phone GPU tests. I also did not record historical TSL migration hours, measure total driver VRAM, or test every browser version. Those remain unmeasured rather than being filled with estimates. The concrete outcome is a working current garden on both backends, and a set of smaller tests that expose where its asset pipeline can fail.</p>
<hr />
<p><a href="https://digitalgarden.si/blog/webgpu-garden-houdini-threejs/">Read the original article, supporting data and updates</a>.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>Three.js, <a href="https://threejs.org/docs/pages/WebGPURenderer.html">WebGPURenderer documentation</a>, checked 19 October 2026. <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded></item><item><title><![CDATA[Meshy Free to Houdini: Testing the Download and Cleanup Path]]></title><description><![CDATA[A real Meshy 6 Lite GLB taken through Houdini: free-download limits, topology checks, a rejected cleanup, 3,000-triangle reduction and browser instanc]]></description><link>https://digitalgarden-alicia.hashnode.dev/meshy-free-to-houdini-testing-the-download-and-cleanup-path</link><guid isPermaLink="true">https://digitalgarden-alicia.hashnode.dev/meshy-free-to-houdini-testing-the-download-and-cleanup-path</guid><dc:creator><![CDATA[Alicia lu]]></dc:creator><pubDate>Sun, 25 Oct 2026 07:02:18 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>AI-assisted disclosure: Codex assisted with research, code and writing. Measurements, failure cases and limitations are retained from the original article.</p>
</blockquote>
<blockquote>
<p>Originally published by Alicia on 2026-10-20: <a href="https://digitalgarden.si/blog/meshy-3d-agent-houdini-cleanup/">Digital Garden by Alicia</a>.</p>
</blockquote>
<p>The free route worked, with an important boundary: Meshy 3D Agent could not select Meshy 6 Lite, and the Agent's higher-tier generated models were not downloadable on this free account. I used the regular Text to 3D tool with 6 Lite, downloaded a real GLB, and tested the Houdini-to-browser pipeline from that file. This article is a free-workflow test, not a paid Agent export demonstration.</p>
<p>The other plant asset and renderer tests are collected in <a href="https://digitalgarden.si/blog/threejs-3d-garden-rendering/">the garden's Three.js rendering overview</a>.</p>
<p>Codex operated Meshy's web UI, Houdini 22.0.429 and the Three.js acceptance viewer on 20 October 2026. The account was already logged in by its owner. No paid subscription was purchased.</p>
<p><img src="https://digitalgarden.si/marketing/research/comparisons-20261020/meshy-comparison.png" alt="Actual generated Meshy source compared with the reduced result under the same inspection material" /></p>
<h2>Verify the download before spending credits</h2>
<p>Meshy's current free-plan help described 100 monthly credits and 10 monthly downloads limited to Meshy 6 Lite. The Agent model selector offered higher-tier models, but no Lite option; its chat answer also directed the free-download workflow to the regular generator. I checked the actual selector and download controls rather than treating the chat answer as proof.<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup></p>
<p>The regular Text to 3D panel allowed Meshy 6 Lite, quoted 10 credits and selected CC BY 4.0. The prompt requested one stylized grape hyacinth: a slender green stem, five narrow leaves and hanging purple-blue bells, with no pot, soil or ground plane. One generation completed and the GLB download counter changed from 10 to 9. The account also received a separate 30-credit daily check-in reward, so its final balance of 120 is not the generation cost.</p>
<p>The page displayed a notice that Text to 3D would be retired on 31 October. That is a dated UI observation, not a promise that this exact free path will still exist afterward. Recheck access and download entitlement before a new batch; do not spend a session generating models that the account cannot export.</p>
<h2>What the downloaded file actually contained</h2>
<p>The GLB was 784,708 bytes, with 21,781 vertices and 43,546 triangles. Its only mesh attribute was POSITION. It contained no UVs, normals, material images or PBR materials. The requested flower colours therefore did not arrive as a textured deliverable. It was a geometry result, not a ready botanical asset with verified species anatomy.</p>
<p>Houdini's imported and unpacked geometry had 13 connected components, zero boundary edges and zero edges shared by more than two faces. Those checks do not prove there are no self-intersections, that all parts form one connected object, or that it is ready for printing. The website's printable badge is not substituted for those measurements.</p>
<p>The original file was preserved. Derived copies were centered in X/Z, placed at ground height and normalized to 0.6 units tall. The later browser inspection applies a common neutral material, with computed display normals where needed; that material is not a generated Meshy texture.</p>
<h2>A failed cleanup made the tolerance problem visible</h2>
<p>The first Clean/PolyDoctor chain used defaults after the 0.6-unit normalization. It removed much of the thin geometry: only 23,817 triangles remained, with 11,625 boundary edges and 69 components. I rejected it. A smaller file with more holes is not an accepted cleanup.</p>
<p>The accepted Clean degeneracy tolerance was reduced to 1e-12. Potential overlap/intersection conditions were marked rather than silently repaired. The accepted cleanup preserved all 43,546 triangles and the original closed-edge/component counts. SideFX documents that Clean's degeneracy tolerance is an edge-length measure whose square is compared with area; scale matters.<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup></p>
<p>PolyReduce then used its actual Output Polygon Count target with finalcount=3000. Merely writing a count while leaving its target on percentage would not activate that target. The remesh branch used target size 0.006 and five iterations. Its loss of some leaf width is visible and consistent with the changed bounds, so it is not called visually identical.</p>
<table>
<thead>
<tr>
<th>Geometry stage</th>
<th>Points</th>
<th>Triangles</th>
<th>Boundary edges</th>
<th>Nonmanifold edges</th>
<th>Components</th>
</tr>
</thead>
<tbody><tr>
<td>raw</td>
<td>21,781</td>
<td>43,546</td>
<td>0</td>
<td>0</td>
<td>13</td>
</tr>
<tr>
<td>clean</td>
<td>21,781</td>
<td>43,546</td>
<td>0</td>
<td>0</td>
<td>13</td>
</tr>
<tr>
<td>reduce</td>
<td>1,508</td>
<td>3,000</td>
<td>0</td>
<td>0</td>
<td>13</td>
</tr>
<tr>
<td>remesh</td>
<td>2,785</td>
<td>5,554</td>
<td>0</td>
<td>0</td>
<td>13</td>
</tr>
</tbody></table>
<p>All derived branches received a six-plane UV unwrap and computed normals before native GLB export. That creates inspectable UVs; it is not a hand-authored low-distortion UV layout. There was no source PBR image to rebake, so I did not invent a texture-baking success. The HIP was saved, reopened and cooked again.</p>
<h2>Validate the browser deliverable</h2>
<p>The source and final GLBs passed glTF validation with zero errors and warnings; informational and buffer-view hints remain in the receipts. Three.js 0.186.1 loaded each candidate through five unique localhost URLs. Timings include parsing, compilation and a submitted frame with warm code/GPU pipelines, not public-network or device-cold time.</p>
<table>
<thead>
<tr>
<th>Candidate</th>
<th>Bytes</th>
<th>Median local load to submitted frame</th>
</tr>
</thead>
<tbody><tr>
<td>Original download</td>
<td>784,708</td>
<td>33.3 ms</td>
</tr>
<tr>
<td>clean, with generated UVs/normals</td>
<td>2,025,332</td>
<td>33.3 ms</td>
</tr>
<tr>
<td>reduce, with generated UVs/normals</td>
<td>214,740</td>
<td>16.6 ms</td>
</tr>
<tr>
<td>remesh, with generated UVs/normals</td>
<td>391,480</td>
<td>16.6 ms</td>
</tr>
</tbody></table>
<p>Adding UVs and normals makes the full-resolution derived file larger than the position-only download; reduction and attribute creation are different operations. The 3,000-triangle candidate was repeated 1,000 times with explicit InstancedMesh: the receipt records 2 total render calls and 3,000,001 triangles, including the node renderer's output pass. Instancing shares data but does not remove the repeated triangle workload. Both WebGPU and forced WebGL2 fallback were checked.</p>
<p>An independently authored Houdini hyacinth is included as a reference for geometry budget and visible structure, not as botanical ground truth or a blinded aesthetic comparison. The free AI result still needs art direction, material work, appropriate deformation attributes and a scene-specific acceptance check before replacing a production garden asset.</p>
<h2>Reproduce it and retain attribution</h2>
<p>The <a href="https://digitalgarden.si/marketing/research/comparisons-20261020/assets-tests.zip">asset-test package</a> contains the original free download, derived meshes, Houdini scripts, viewer and results. The Meshy source and its derivatives retain CC BY 4.0 attribution: model created with Meshy by Alicia. The package includes the license link and records the modifications. It does not grant rights to the private garden engine or its production models.<sup><a id="footnote-ref-3" href="#footnote-3" data-footnote-ref aria-describedby="footnote-label">3</a></sup></p>
<p>For a procedural alternative with explicit shape controls and wind data, read <a href="https://digitalgarden.si/blog/houdini-procedural-plants-realtime-web/">the Houdini plant test</a>. For the runtime budget, continue with <a href="https://digitalgarden.si/blog/threejs-webgpu-vs-webgl-instanced-plants/">the nine-path instancing matrix</a>.</p>
<hr />
<p><a href="https://digitalgarden.si/blog/meshy-3d-agent-houdini-cleanup/">Read the original article, supporting data and updates</a>.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>Meshy, <a href="https://help.meshy.ai/en/articles/15696428-what-is-included-on-the-free-plan">what is included on the free plan</a> and <a href="https://docs.meshy.ai/en/webapp/3d-agent">3D Agent documentation</a>, checked 20 October 2026. Entitlement was also verified by an actual download. <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩︎</a></p>
</li>
<li id="footnote-2">
<p>SideFX, <a href="https://www.sidefx.com/docs/houdini/nodes/sop/clean.html">Clean SOP</a> and <a href="https://www.sidefx.com/docs/houdini/nodes/sop/polyreduce.html">PolyReduce SOP</a>, checked 20 October 2026. <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩︎</a></p>
</li>
<li id="footnote-3">
<p>Meshy, <a href="https://help.meshy.ai/en/articles/10137554-what-is-the-ownership-of-the-generated-models">ownership of generated models</a>, checked 20 October 2026. The model is distributed with <a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a> attribution and modification notes. <a href="#footnote-ref-3" data-footnote-backref aria-label="Back to reference 3">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded></item><item><title><![CDATA[Three.js WebGPU vs WebGL: Testing 1,000 Instanced Plants]]></title><description><![CDATA[Nine measured Three.js renderer and submission paths: 1,000 plants, repeated frame samples, CPU render time, GPU probes and actual draw calls.]]></description><link>https://digitalgarden-alicia.hashnode.dev/three-js-webgpu-vs-webgl-testing-1-000-instanced-plants</link><guid isPermaLink="true">https://digitalgarden-alicia.hashnode.dev/three-js-webgpu-vs-webgl-testing-1-000-instanced-plants</guid><dc:creator><![CDATA[Alicia lu]]></dc:creator><pubDate>Sun, 25 Oct 2026 07:00:58 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>AI-assisted disclosure: Codex assisted with research, code and writing. Measurements, failure cases and limitations are retained from the original article.</p>
</blockquote>
<blockquote>
<p>Originally published by Alicia on 2026-10-20: <a href="https://digitalgarden.si/blog/threejs-webgpu-vs-webgl-instanced-plants/">Digital Garden by Alicia</a>.</p>
</blockquote>
<p>With this plant fixture, instancing reduced draw submission on every tested backend. Switching to WebGPU did not produce a meaningful FPS lead: all nine paths were close to the display's 60 Hz ceiling. The useful distinction was the submitted representation and the work inside renderer.render(), rather than the API name.</p>
<p>For the related asset, format and splat experiments, see the <a href="https://digitalgarden.si/blog/threejs-3d-garden-rendering/">Three.js 3D garden rendering guide</a>.</p>
<p>This test supports the Houdini asset workflow behind <a href="https://digitalgarden.si/play/">Alicia's interactive 3D garden</a>. Codex operated the independent scripts and browser on 20 October 2026. It did not change the production garden or benchmark a physical phone.</p>
<p><img src="https://digitalgarden.si/marketing/research/comparisons-20261020/plants-submission.png" alt="Measured submission cost and actual draw counters for all nine paths" /></p>
<h2>What was kept equal</h2>
<p>Three.js was pinned to 0.186.1. One owned procedural grape hyacinth supplied 6,056 triangles, repeated 1,000 times: 6,056,000 scene triangles. The same placement matrices, geometry, unlit vertex colours, double-sided material, camera path and bounded root-anchored sine displacement were used throughout. The classic renderer used an equivalent GLSL injection; the node renderer used TSL. This is a small teaching wind, not the garden's production deformation and normal system.</p>
<p>The three renderer paths were classic WebGLRenderer, WebGPURenderer on actual WebGPU, and WebGPURenderer with forceWebGL enabled. Each used separate Mesh objects, InstancedMesh, then BatchedMesh. The active backend was inspected after initialization; a WebGPURenderer constructor alone does not prove WebGPU ran.<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup></p>
<p>The canvas was 1280 × 720 at renderer DPR 1, without antialiasing. The browser reported Chromium 155 on Windows and an RTX 5070 Ti. Each accepted cell ran three complete 10-second warm-ups, discarded, followed by five complete 10-second measurements along the same partial orbit. The archived results report the median of each run's summary. The slowest 1% of frame intervals define the 1% low FPS.</p>
<p>Frustum culling was disabled for every object; BatchedMesh per-object culling and sorting were disabled too. That keeps the submitted set equal. It also means this is not a comparison of each class's default visibility policy. Other applications remained open; these are workstation observations rather than isolated maximum-throughput measurements. GPU experiments ran sequentially. Draw representation and counters are clearer evidence than small cross-backend timing differences under background load.</p>
<p>The first full matrix used random Y rotations and 20-second trials. Review caught a parity problem: classic GLSL displaced before instancing, while TSL positionNode ran after the instance/batch transform. The same expression could therefore bend in different world directions. I preserved that matrix as a rejected comparison, removed rotations from all nine paths, and reran the complete matrix with 10-second trials. The chart and archived tables use only this matched-transform rerun. This shorter run does not certify long-duration frame pacing.</p>
<h2>Full dataset and 10k/100k runs</h2>
<p>The current tables and machine-readable measurements live in the <a href="https://digitalgarden.si/benchmarks/threejs-webgpu-vs-webgl-instancing/">Three.js plant instancing benchmark v1</a>, with 1k, 10k and 100k tests, static and moving cameras, and explicit failed trials. The v1 page reruns 1k using at least five seconds and 300 warm-up frames followed by thirty seconds or 1,800 intervals, with five repeats and alternating backend order. Each sample uses a new visible test document. It does not reuse this article's shorter measurements.</p>
<p>This article and its chart retain the findings of the 20 October nine-path test. Its historical numbers and original protocol remain in the <a href="https://digitalgarden.si/marketing/research/comparisons-20261020/plants-tests.zip">original measurement archive</a>. The two protocols must not be pooled into one performance table.</p>
<p>The node renderer's totals include its final output pass, adding one call and one triangle. Classic WebGL's counters do not contain that extra pass. Neither counter should be relabelled as a direct browser API call count.</p>
<p>The BatchedMesh result is especially useful. In this pinned WebGPU path, the backend loops over the batch's draw commands. Its counter remains at 1,001 calls even though the JavaScript submission can cost much less than traversing 1,000 individual Mesh objects. Classic WebGL used the available multi-draw path and reported one call. A class called BatchedMesh does not guarantee a single draw on every backend.<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup></p>
<p>InstancedMesh is a clear fit for this repeated identical geometry. BatchedMesh has a different purpose when many geometries share compatible material state; the present one-geometry case does not establish how it behaves in every mixed scene. Merging was not part of this nine-cell matrix.</p>
<h2>What the timers do and do not measure</h2>
<p>CPU render time surrounds renderer.render(). It includes JavaScript and backend submission work, not completed GPU execution. A separate 30-frame GPU probe used EXT_disjoint_timer_query_webgl2 on classic WebGL and resolveTimestampsAsync on the node backends. Invalid or unavailable samples stay absent in the receipts. Repeated discrete WebGPU probe values are retained as diagnostics, not used to rank small timing differences. The probe is outside the FPS trials and is not total VRAM or end-to-end latency.</p>
<p>Cell setup to first submitted frame was also recorded once per cell, including compilation, after the asset had already loaded. Those values are not device-cold startup or public CDN loading times. A later <a href="https://digitalgarden.si/blog/threejs-glb-loading-cache-first-frame/">meshopt GLB loading</a> test times the file itself across twenty WebGPU trials, separating a fresh URL, a cache hit, decoding and the first GPU frame, and it keeps the first-run compilation outlier. JavaScript heap snapshots are process observations without controlled garbage collection, so I do not rank memory efficiency from them.</p>
<p>The display cap hides how much headroom a fast path has. It would be misleading to call 60.00 versus 59.99 FPS a renderer victory, or to extrapolate this desktop result to a phone. This experiment also does not establish a universal instance-count threshold, transparent-foliage overdraw cost or physical-device power consumption. A different workload sits outside this one-plant matrix. The <a href="https://digitalgarden.si/blog/houdini-vs-code-procedural-threejs-slop-newyork/">WebGPU performance of two complete scenes</a> profiles slop-newyork and the spring garden at 1080p, where a lower draw-call count did not by itself produce a shorter frame. A still <a href="https://digitalgarden.si/blog/mutracer-houdini-plant-path-tracing/">WebGPU plant path tracing</a> test with MuTracer rendered three tinted instances of one Houdini plant. The WebGL attempt lost its context and produced no finished image. Those samples are not rows in this raster matrix.</p>
<p>A separate <a href="https://digitalgarden.si/blog/threejs-scatter-objects-uneven-terrain/">terrain placement comparison</a> measures the CPU work before rendering: downward rays, BVH, surface sampling and baked Houdini points. Its 70 placement rows are separate from this renderer matrix, and minimum spacing remains a distribution requirement.</p>
<h2>Reproduce the actual result</h2>
<p>The <a href="https://digitalgarden.si/marketing/research/comparisons-20261020/plants-tests.zip">historical plant matrix archive</a> preserves the pinned 20 October package, owned fixture, harness and all 72 raw trial records. For new measurements, use the <a href="https://digitalgarden.si/benchmarks/data/threejs-instancing-v1-harness.zip">v1 harness with updated local tooling</a>. Keep the page visible, run the suite sequentially, and inspect the actual backend and canvas dimensions in each result.</p>
<p>For this garden pipeline, the actionable result is to share repeated geometry, retain wind attributes, and inspect real counters. Continue with <a href="https://digitalgarden.si/blog/houdini-gltf-threejs-optimization/">export and compression tests</a>, <a href="https://digitalgarden.si/blog/webgpu-garden-houdini-threejs/">the actual garden integration</a>, or <a href="https://digitalgarden.si/blog/claude-code-vs-codex-threejs/">Claude Code and Codex on a smaller wind task</a>.</p>
<hr />
<p><a href="https://digitalgarden.si/blog/threejs-webgpu-vs-webgl-instanced-plants/">Read the original article, supporting data and updates</a>.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>Three.js, <a href="https://threejs.org/docs/pages/WebGPURenderer.html">WebGPURenderer</a>, checked 20 October 2026. Pinned source is included through the package lock. <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩︎</a></p>
</li>
<li id="footnote-2">
<p>Three.js, <a href="https://threejs.org/docs/pages/BatchedMesh.html">BatchedMesh</a>. The backend-specific draw behavior above is an observed result of 0.186.1, not a promise made by the class name. <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded></item><item><title><![CDATA[Houdini USD vs glTF: A Two-Plant Three.js Test]]></title><description><![CDATA[The same Houdini plant pair exported as GLB, USDC and USDZ: measured browser loads, textures, variants, instance representation and export failures.]]></description><link>https://digitalgarden-alicia.hashnode.dev/houdini-usd-vs-gltf-a-two-plant-three-js-test</link><guid isPermaLink="true">https://digitalgarden-alicia.hashnode.dev/houdini-usd-vs-gltf-a-two-plant-three-js-test</guid><dc:creator><![CDATA[Alicia lu]]></dc:creator><pubDate>Sun, 25 Oct 2026 06:59:08 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>AI-assisted disclosure: Codex assisted with research, code and writing. Measurements, failure cases and limitations are retained from the original article.</p>
</blockquote>
<blockquote>
<p>Originally published by Alicia on 2026-10-20: <a href="https://digitalgarden.si/blog/houdini-usd-vs-gltf-threejs/">Digital Garden by Alicia</a>.</p>
</blockquote>
<p>The native GLB loaded both plants. The same authored pair, delivered as USDC or USDZ through Three.js USDLoader 0.186.1, loaded only one plant and did not retain the vertex-colour or custom wind attributes. USD retained the authoring concepts in Houdini, but this browser representation failed the pair's feature requirements. That is the main result of this test, not a format-speed victory.</p>
<p>For where this delivery test sits among the other garden experiments, see the <a href="https://digitalgarden.si/blog/threejs-3d-garden-rendering/">rendering guide overview</a>.</p>
<p>Codex ran the native Houdini scripts and browser checks on 20 October 2026. The fixture is an owned procedural grape hyacinth used to study the asset boundary behind <a href="https://digitalgarden.si/">digitalgarden.si</a>, Alicia's interactive 3D garden. It is not a production model.</p>
<p><img src="https://digitalgarden.si/marketing/research/comparisons-20261020/usd-browser.jpg" alt="The paired USDZ plant fixture loaded through the pinned Three.js USDLoader" /></p>
<h2>Build a native two-plant fixture</h2>
<p>Houdini 22.0.429 generated one 6,056-triangle plant, with normals, UVs, vertex colour and wind attributes. The visible pair places copies at X=0 and X=0.3, for 12,112 triangles. The USD stage contains two seed variants, 11 and 12, and an instanceable internal reference for the second plant. Both variants use 3,838 points and 6,056 triangles per plant, with different procedural leaf shapes.</p>
<p>The USD was authored with pxr under Houdini's hython and composed/read back through a native Solaris Sublayer LOP. It is not claimed to be an automatic Component Builder material conversion. The root layer was exported as USDC; UsdUtils packaged the layer and shared PNG as USDZ. The HIP was saved and reopened, with both variants and the native instance relation verified. OpenUSD reported 0.26.5.</p>
<p>The material probe used UsdPreviewSurface, an sRGB UsdUVTexture and a deliberately noisy 2048 PNG. glTF used Houdini's native principled-material export with the same image. Different exporter mappings can still produce shading differences, so I do not describe the materials as visually identical. MaterialX conversion was not tested.</p>
<h2>Two rejected exports mattered</h2>
<p>Calling Usd.Stage.Export() flattened the composition and removed the variant set. Exporting the authored root layer preserved it. Flattening can be appropriate for delivery, but it cannot be presented as retaining editable variant structure.</p>
<p>The first packed-copy GLB also contained an unused image entry without a resource URI or buffer view. A downstream tool rejected it. I retained that failure, unpacked the pair before the native glTF ROP, and re-exported. The accepted GLB passed glTF validation with zero errors and warnings; seven buffer-view-target hints remain. The accepted result therefore bakes the two copies into one mesh. That is this exporter path's result, not a limitation imposed by the glTF format.</p>
<h2>Packaged bytes and local loading</h2>
<p>Three.js 0.186.1 used GLTFLoader or USDLoader in the same classic WebGL viewer, 1280 × 720 at DPR 1. Each format used five distinct loopback URLs. The timer includes fetch, parsing, texture readiness, compilation and a submitted frame, without a GPU completion fence. Code, OS caches and GPU pipelines were warm. USDC's external PNG can be cached, so its timing is a multi-file diagnostic, not directly equivalent to the self-contained GLB/USDZ loads.</p>
<table>
<thead>
<tr>
<th>Accepted input</th>
<th>Main-file MB</th>
<th>Additional image MB</th>
<th>Median local load to submitted frame</th>
</tr>
</thead>
<tbody><tr>
<td>GLB</td>
<td>4.726</td>
<td>embedded</td>
<td>63.7 ms</td>
</tr>
<tr>
<td>USDZ</td>
<td>4.608</td>
<td>embedded</td>
<td>71.3 ms</td>
</tr>
<tr>
<td>USDC</td>
<td>0.269</td>
<td>4.338</td>
<td>44.8 ms</td>
</tr>
</tbody></table>
<p>The standalone USDC is not a 0.27 MB textured delivery: its external image must be counted. Likewise, a smaller USDZ here does not prove USD is universally smaller than glTF. The two paths encode variants, geometry sharing and custom data differently. The large artificial PNG dominates both packages. More importantly, the loaded USD scene has half the intended triangles, so these timing rows are compatibility diagnostics and cannot be ranked as equal-workload performance.</p>
<h2>Inspect the browser objects</h2>
<table>
<thead>
<tr>
<th>Loaded input</th>
<th>Mesh objects</th>
<th>Unique geometries</th>
<th>Materials</th>
<th>Texture objects</th>
<th>Triangles</th>
<th>Submitted calls</th>
</tr>
</thead>
<tbody><tr>
<td>GLB</td>
<td>1</td>
<td>1</td>
<td>1</td>
<td>1</td>
<td>12,112</td>
<td>1</td>
</tr>
<tr>
<td>USDZ</td>
<td>1</td>
<td>1</td>
<td>1</td>
<td>1</td>
<td>6,056</td>
<td>1</td>
</tr>
<tr>
<td>USDC</td>
<td>1</td>
<td>1</td>
<td>1</td>
<td>1</td>
<td>6,056</td>
<td>1</td>
</tr>
</tbody></table>
<p>The GLB returned 12,112 triangles, UVs, vertex colour, _stiff and _phase. USDC/USDZ returned 6,056 triangles with position, normal and UV only. A texture object was present, but without the authored vertex colour the plant also appeared neutral rather than matching the GLB's colour modulation. A later <a href="https://digitalgarden.si/blog/houdini-vs-geometry-nodes-plant-export/">Houdini plant GLB export</a> from Houdini and Blender Geometry Nodes kept the wind attributes in both accepted files. A Geometry Nodes GLB that lost its foliage colors was rejected even though validation reported no errors. The second instance's absence is a failed rendering requirement, not an optimization. A native USD instance flag does not by itself create Three.js InstancedMesh, and the glTF format's support for an instancing extension does not make this native ROP emit it.</p>
<p>USDComposer's explicit seed selections produced different loaded position hashes in this test. That check uses the pinned parser/composer API and recomposes the stage; it is not a claim that USDLoader supplies a complete interactive variant-switching UI. The separate USDA transform-animation diagnostic exposed 1 clip(s) and numerically changing transforms, but returned 0 mesh objects. It therefore did not pass visible plant-animation acceptance. No skinned or botanical animation export was tested.</p>
<h2>Choose the boundary you can validate</h2>
<p>The <a href="https://digitalgarden.si/marketing/research/comparisons-20261020/assets-tests.zip">format fixture download</a> includes the owned source generator, portable scripts, accepted files, loader checks and compact results. It also documents the rejected export paths. Bundle costs are measured separately in the receipt; a loader's dependency graph is distinct from asset bytes and from an already-cached application bundle.</p>
<p>For a Houdini authoring pipeline, keep composition and variants where they are needed, then test the specific browser representation that will ship. For repeated identical plants, an explicit runtime instancing step can matter more than the container extension. This test does not cover Quick Look on a physical iPhone, MaterialX, every USD schema, total GPU memory or production network speed. Two NVIDIA packages later followed a different route, <a href="https://digitalgarden.si/blog/simready-usd-to-glb-threejs-webgpu/">USD to GLB in Blender and Houdini</a>, where PreviewSurface textures, a geometry-cache workaround, direct loading and a usd2gltf failure were separate outcomes. Continue with <a href="https://digitalgarden.si/blog/openusd-pipeline-beginners/">OpenUSD pipeline basics</a>, <a href="https://digitalgarden.si/blog/openusd-threejs-usd-web-viewer/">the earlier USD web-viewer test</a> and <a href="https://digitalgarden.si/blog/houdini-gltf-threejs-optimization/">measured glTF optimization</a>.</p>
<hr />
<p><a href="https://digitalgarden.si/blog/houdini-usd-vs-gltf-threejs/">Read the original article, supporting data and updates</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Procedural Plants for the Web: Houdini Geometry and Wind Data]]></title><description><![CDATA[Build and test a small Houdini plant for Three.js: seeded geometry, two LODs, custom wind attributes, instancing and alpha tested leaf cards.]]></description><link>https://digitalgarden-alicia.hashnode.dev/procedural-plants-for-the-web-houdini-geometry-and-wind-data</link><guid isPermaLink="true">https://digitalgarden-alicia.hashnode.dev/procedural-plants-for-the-web-houdini-geometry-and-wind-data</guid><dc:creator><![CDATA[Alicia lu]]></dc:creator><pubDate>Sun, 25 Oct 2026 06:54:32 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>AI-assisted disclosure: Codex assisted with research, code and writing. Measurements, failure cases and limitations are retained from the original article.</p>
</blockquote>
<blockquote>
<p>Originally published by Alicia on 2026-10-19: <a href="https://digitalgarden.si/blog/houdini-procedural-plants-realtime-web/">Digital Garden by Alicia</a>.</p>
</blockquote>
<p>A procedural plant for the web needs more than an attractive still. It needs repeatable geometry, a stable root, a useful lower detail version and data that survives export. I built an original grape hyacinth fixture in Houdini 22.0.429 and tested it in Three.js 0.186.1 on 19 October 2026. The result is a small working HDA and a browser experiment, with explicit limits around wind, transparency and performance.</p>
<p>This plant fixture is one step in <a href="https://digitalgarden.si/blog/threejs-3d-garden-rendering/">the full rendering guide for the interactive 3D garden</a>.</p>
<p><img src="https://digitalgarden.si/marketing/research/webgpu-series-20261019/plant-wind.png" alt="One thousand instances of the original test plant with shader driven wind in WebGPU" /></p>
<h2>A small generator with inspectable parameters</h2>
<p>The HDA contains a Python SOP that builds a stem, five leaves and eight tiers of five bell shaped flowers. It exposes a seed, LOD, height in meters and stem side count. The tested default is seed 11, height 0.6 meters and eight stem sides. The geometry includes UVs, vertex colors and normals; separate point attributes describe stiffness and wind phase.</p>
<table>
<thead>
<tr>
<th>Output</th>
<th>Points</th>
<th>Triangles</th>
<th>Forced cook, seed 11</th>
</tr>
</thead>
<tbody><tr>
<td>LOD0</td>
<td>3,838</td>
<td>6,056</td>
<td>89.30 ms</td>
</tr>
<tr>
<td>LOD1</td>
<td>1,553</td>
<td>2,116</td>
<td>38.06 ms</td>
</tr>
</tbody></table>
<p>One hundred forced seed cooks took 9.38 seconds. These are wall clock measurements of this Python SOP in hython on this workstation. They are not a Houdini Performance Monitor breakdown or a comparison with a compiled SOP implementation. The saved HIP was loaded again for the Gaussian fixture generation, which read the expected plant geometry successfully.</p>
<p>The lower detail plant is authored by reducing generator segments. It is useful for testing a smaller shape, but it is not the production garden’s nested LOD generation and transition system. A distant silhouette can be acceptable even when a close view is visibly coarser. Those acceptance distances belong to each scene and camera.</p>
<h2>Bake attributes, then animate in the shader</h2>
<p><code>_STIFF</code> ranges from zero at the base to one toward flexible tips. <code>_PHASE</code> is a stable per plant seed value; seed 11 produces 0.507. In Three.js, the GLTFLoader exposes those attribute names in lowercase. The browser fixture reads <code>_stiff</code> and <code>_phase</code> through TSL.</p>
<p>Its displacement is deliberately simple:</p>
<pre><code class="language-js">const stiff = attribute('_stiff', 'float');
const phase = attribute('_phase', 'float');
const bend = sin(clock.mul(1.8).add(phase.mul(6.283)))
  .mul(stiff.pow(2)).mul(strength).mul(0.10);
material.positionNode = positionLocal.add(vec3(bend, 0, 0));
</code></pre>
<p>Because zero stiffness produces zero displacement, the fixture’s root remains fixed algebraically. This is baked deformation data, not a baked animation. The function offsets points horizontally; it does not preserve stem arc length, solve collisions or correct every shading normal. Calling it a botanical simulation would overstate what was built.</p>
<p>The actual <a href="https://digitalgarden.si/play/">interactive garden</a> uses a more involved shared bend and normal deformation, with coherent flower crowns and per stalk anchors for relevant species. The downloadable shader illustrates the export contract without redistributing that private engine. A copied sine shader should not be mistaken for the whole production animation system.</p>
<h2>Attribute survival is an animation test</h2>
<p>The native Houdini GLB included <code>_STIFF</code> and <code>_PHASE</code>. So did its Draco export and the tested glTF Transform Meshopt candidate. The gltfpack candidates dropped them, including a run with <code>-kv</code>. That discovery changes which output can be accepted for this shader: a texture optimized file may load successfully while lacking the intended motion data.</p>
<p>In the harness, a missing stiffness attribute falls back to height and a missing phase falls back to a constant. That keeps a comparison visible, but it is not proof of a correct export. A production acceptance check should fail or explicitly flag that fallback. The <a href="https://digitalgarden.si/blog/houdini-gltf-threejs-optimization/">compression article</a> separates successful decoding from usable deformation attributes.</p>
<p>An earlier native export also failed normal validation at degenerate leaf endpoints. Adding a defined fallback normal corrected the final GLBs. Validation found a problem the distant plant preview did not make obvious. A later <a href="https://digitalgarden.si/blog/houdini-vs-geometry-nodes-plant-export/">Houdini versus Geometry Nodes plant export</a> assembled one controlled plant in both applications. The accepted GLBs corresponded triangle for triangle and kept the wind attributes, after a Geometry Nodes material setup that lost its foliage colors was rejected.</p>
<h2>Wind, interaction signals and leaf cards</h2>
<p>I tested a 1,000 plant patch at 1280 × 720, DPR 1, on an RTX 5070 Ti in the visible Chromium 155 browser. Each run used 1.5 seconds of warmup and 30 seconds of the same scripted orbit. Wind off and wind on both measured about 59.99 FPS with two total render calls, including the output pass. This ceiling limited observation does not establish the shader’s cost on a phone or at a higher resolution. A later <a href="https://digitalgarden.si/blog/threejs-webgpu-vs-webgl-instanced-plants/">WebGPU versus WebGL plant test</a> kept this plant and separated InstancedMesh, BatchedMesh and one mesh per copy, because a capped frame rate hides the submission difference.</p>
<p>The interaction variant adds a scripted oscillating touch uniform. It also stayed near 59.99 FPS with two calls. It tests a deformation signal without a pointer event, spatial search, collision or input latency measurement. Real garden brushing involves more work and cannot inherit this fixture’s result automatically.</p>
<p>For the alpha test variant, I removed the five solid leaves and added five segmented cards per plant, using an original canvas generated leaf mask and <code>alphaTest = 0.5</code>. This is 5,000 leaf instances in an additional draw: three total calls, 5,936,001 total triangles and 59.99 FPS in the sampled patch. It changes the geometry and shader workload together. It is a practical construction comparison, not a controlled measurement of alpha testing in isolation.</p>
<p><img src="https://digitalgarden.si/marketing/research/webgpu-series-20261019/plant-alpha-cards.png" alt="The same fixture with alpha tested leaf cards replacing the solid leaves" /></p>
<p>The cards use a hard alpha cutoff rather than blended transparency. Camera angle, double sided rendering, silhouette loss and distant aliasing still need visual inspection. A desktop FPS number does not settle those questions. A later still, <a href="https://digitalgarden.si/blog/mutracer-houdini-plant-path-tracing/">MuTracer with a Houdini plant</a>, path-traced double-sided alpha masks and vertex colors on frozen geometry. It does not measure this orbiting patch, and its WebGL run lost the context before a finished image.</p>
<p>For placing those assets, the later <a href="https://digitalgarden.si/blog/threejs-scatter-objects-uneven-terrain/">uneven-terrain scatter test</a> separates surface attachment from minimum spacing. It uses an independent owned terrain, native Houdini layouts and runtime sampling, including a boundary-ray validator that initially rejected valid points.</p>
<h2>Keep the shadow contract explicit</h2>
<p>Moving vertices in a color material raises a second question: which positions does the shadow pass use? I captured frozen wind poses with default, explicit rest position and explicit deformed <code>castShadowPositionNode</code> settings. The default and explicit deformed screenshots were identical in the inspected region; forcing rest positions changed 12,014 pixels there. The default worked for this pinned material and renderer. This was not a reproduced default shadow bug.</p>
<p><img src="https://digitalgarden.si/marketing/research/webgpu-series-20261019/plant-shadow.png" alt="A frozen bent test plant with its default directional shadow, which matched the explicit deformed shadow setting" /></p>
<p>The production garden’s stylized lighting is a separate implementation. This small directional shadow map test does not certify production shadows or promise correct results for every custom TSL material. Inspect the actual bent silhouette and its ground shadow whenever a deformation changes.</p>
<h2>What is included</h2>
<p>Codex executed the generator, native export and browser checks; the dated results describe that workstation run.</p>
<p>The <a href="https://digitalgarden.si/marketing/research/webgpu-series-20261019/webgpu-garden-tests.zip">experiment package</a> includes the original generator and HDA, compact native and browser results, and a standalone viewer. The large noisy test texture can be regenerated from its seeded script. Editable full HIP files and local capture evidence remain with the project; third party tool binaries and the private garden engine are excluded.</p>
<p>The test proves a seeded Houdini plant can reach the browser with its wind data intact through selected export paths. It does not prove scan quality, biome realism, mobile performance or robust physical bending. It also does not settle <a href="https://digitalgarden.si/blog/houdini-vs-code-procedural-threejs-slop-newyork/">code versus Houdini</a> for a finished scene. A later 1080p profile compared this garden with slop-newyork and kept the two pipelines mixed rather than declaring a winner. For the larger context, read <a href="https://digitalgarden.si/blog/webgpu-garden-houdini-threejs/">the garden pipeline</a> and the separate <a href="https://digitalgarden.si/blog/gaussian-splats-threejs/">Gaussian splat compatibility test</a>. That splat fixture is compared again in <a href="https://digitalgarden.si/blog/spark-vs-gaussiansplats3d-threejs/">Spark, GaussianSplats3D and native Three.js</a>.</p>
<h2>References</h2>
<p>SideFX documents the <a href="https://www.sidefx.com/docs/houdini/nodes/out/gltf.html">glTF exporter and custom attribute rules</a>. The browser uses the official <a href="https://threejs.org/docs/pages/WebGPURenderer.html">Three.js WebGPURenderer</a>. Versions and documentation were checked on 19 October 2026; the download records the exact experiment dependencies.</p>
<hr />
<p><a href="https://digitalgarden.si/blog/houdini-procedural-plants-realtime-web/">Read the original article, supporting data and updates</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Houdini to Unreal with USD: A Tested Stage, HDA and Material Pipeline]]></title><description><![CDATA[A Houdini 22 to Unreal 5.8 test of USD Stage, MaterialX, 100 instances and HDA baking. Includes measured units, variant fixes and reproducible fixture]]></description><link>https://digitalgarden-alicia.hashnode.dev/houdini-to-unreal-with-usd-a-tested-stage-hda-and-material-pipeline</link><guid isPermaLink="true">https://digitalgarden-alicia.hashnode.dev/houdini-to-unreal-with-usd-a-tested-stage-hda-and-material-pipeline</guid><dc:creator><![CDATA[Alicia lu]]></dc:creator><pubDate>Sun, 25 Oct 2026 06:52:33 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>AI-assisted disclosure: Codex assisted with research, code and writing. Measurements, failure cases and limitations are retained from the original article.</p>
</blockquote>
<blockquote>
<p>Originally published by Alicia on 2026-10-19: <a href="https://digitalgarden.si/blog/houdini-unreal-usd/">Digital Garden by Alicia</a>.</p>
</blockquote>
<p><strong>Use a USD Stage when you want to retain a composed scene; use Houdini Engine when the deliverable is a procedural tool that should cook and bake inside Unreal.</strong> I tested both with independent fixtures: a small referenced flower and a 100-copy scatter. The units, materials and instance counts below are read from native Unreal components, not inferred from a successful file import.</p>
<p>The test used Houdini 22.0.429, its OpenUSD 26.5 build, Unreal Engine 5.8.3 and Unreal's OpenUSD 26.3 build on Windows. The installed Houdini Engine plugin identifies itself as <code>3.0 - H22.0.429</code> with version integer <code>22000429</code>. That local descriptor is the relevant compatibility evidence here; it differs from the plugin-version table in the pre-test draft.</p>
<p>The garden at <a href="https://digitalgarden.si/virtual-garden/">digitalgarden.si</a> renders in Three.js. These Unreal experiments are separate test assets. They do not migrate the live garden or demonstrate the full Project Titan EVO tool collection.</p>
<h2>One fixture per failure</h2>
<p>The <a href="https://digitalgarden.si/marketing/research/briefs-20261019/houdini-unreal-usd.zip">test archive</a> includes the USD layers, a small packed-scatter HDA and source scripts. The USD stage contains:</p>
<ul>
<li>A one-meter cube for units and axis conversion.</li>
<li>A Preview Surface material and an embedded MaterialX standard surface.</li>
<li>A separate cube with an <code>unrealMaterial</code> attribute pointing to a native project material.</li>
<li>A referenced schematic flower with bud and bloom geometry variants.</li>
<li>A PointInstancer with 100 placements and one prototype.</li>
</ul>
<p>Houdini opened this stage through a native Sublayer LOP. A separate SOP network packs a simple proxy and copies it onto a 10 × 10 grid. That network becomes the HDA used for the Engine path. The HDA proxy is intentionally simpler than the USD flower: this is a workflow and instance-count comparison, not a matched performance benchmark.</p>
<h2>What crosses each boundary</h2>
<table>
<thead>
<tr>
<th>Path</th>
<th>What remains editable</th>
<th>Observed output</th>
</tr>
</thead>
<tbody><tr>
<td>Open a USD Stage</td>
<td>USD composition, source prims and selected variants</td>
<td>Generated transient meshes, material instances and a 100-instance component</td>
</tr>
<tr>
<td>Native USD asset import</td>
<td>Converted project assets</td>
<td>Preview material instance and static meshes in the test Content folder</td>
</tr>
<tr>
<td>Interchange USD import</td>
<td>Converted project assets</td>
<td>Static meshes, Preview materials and native MaterialX materials</td>
</tr>
<tr>
<td>Houdini Engine HDA</td>
<td>Procedural network and exposed controls while cooking</td>
<td>100 native instances; bake completed successfully</td>
</tr>
</tbody></table>
<p>Epic still labels the USD Stage feature Beta in the current 5.8 documentation.<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup> Keep that shipping caveat separate from the success of a small editor test. Opening a stage and importing assets are different operations, even when both start from the same file.</p>
<h2>Units: the reference cube measured 100 centimeters</h2>
<p>The source explicitly declares Y-up and <code>metersPerUnit = 1</code>. In Unreal, the one-meter cube had half-extents <code>(50, 50, 50)</code> cm and its center was at Z = 50 cm. Its source X position of −1.5 m became −150 cm. The source vertical Y direction became Unreal's vertical Z direction.</p>
<p>That is a stronger acceptance check than reading the source metadata alone. Metadata describes how values should be interpreted; it does not repair geometry authored at the wrong scale. Put a real reference object in your fixture and measure the receiving component.</p>
<pre><code class="language-python">component = usd_actor.get_generated_component("/World/MeterCube")
origin, half_extent, radius = unreal.SystemLibrary.get_component_bounds(component)
# Expected half_extent for a 1 m cube: 50 cm on each axis.
</code></pre>
<h2>Materials: render context and schema both matter</h2>
<table>
<thead>
<tr>
<th>Case</th>
<th>Native observation</th>
</tr>
</thead>
<tbody><tr>
<td>Preview Surface, default context</td>
<td><code>MI_Preview</code> assigned to the cube and flower</td>
</tr>
<tr>
<td>MaterialX-only material, default context</td>
<td>DisplayColor fallback material</td>
</tr>
<tr>
<td>Same embedded MaterialX, <code>mtlx</code> context</td>
<td>Generated MaterialX material instance assigned</td>
</tr>
<tr>
<td><code>unrealMaterial</code> path</td>
<td>The expected <code>/Game/BlogTests/M_Garden</code> material assigned</td>
</tr>
<tr>
<td>Interchange, corrected MaterialX schema</td>
<td>Native MaterialX materials and static meshes created; final commandlet reported zero errors</td>
</tr>
</tbody></table>
<p>My first MaterialX definition used a Preview Surface input name: <code>roughness</code>. Interchange correctly rejected it because a MaterialX standard surface expects <code>specular_roughness</code>. After correcting that authored shader input, the same import finished with zero errors. This was a fixture bug. It is not evidence that Unreal cannot import MaterialX.</p>
<p>The context test is also narrower than a full material-match test. It checks native material creation and assignment for an embedded, constant-color standard surface. It does not establish Karma-to-Unreal pixel parity, external <code>.mtlx</code> reference behavior, every texture channel, displacement or a production shader library. Keep a Preview Surface fallback when that is the receiving application's more portable path.</p>
<p>For a native project override, the exact asset path must exist in the receiving project. The test created a green native material in its own Content folder before loading the USD cube. A string that happens to look like a material path is not sufficient evidence of a successful binding.</p>
<h2>Count the instance component, not the scene root</h2>
<p>The PointInstancer's generated root was a plain SceneComponent. Its descendant <code>Prototype_0</code> was an <strong>InstancedStaticMeshComponent with 100 instances</strong>. Looking only at the root, or only at the actors returned by a commandlet's editor actor list, initially hid that result.</p>
<pre><code class="language-python">root = usd_actor.get_generated_component("/World/Scatter")
for component in root.get_children_components(True):
    if hasattr(component, "get_instance_count"):
        print(component.get_class().get_name(), component.get_instance_count())
</code></pre>
<p>The installed version produced ISM, not HISM, for this fixture. Do not turn the documentation's list of supported representations into a promise about the specific class your scene will create. Count it after import and inspect the prototype's actual material assignment.</p>
<p>The HDA path also produced one InstancedStaticMeshComponent containing 100 packed copies. The native material override was authored before packing. This test does not establish arbitrary per-point color propagation across every packing/export combination; a color attribute and a material assignment are different contracts.</p>
<h2>A variant failure that was really a composition failure</h2>
<p>The first bud selection did nothing. The source sphere already had a local radius opinion outside the growth variants. That stronger opinion defeated the variant-authored radii.</p>
<p>Clearing the local radius before authoring the variants fixed it. In Unreal, the generated flower's X/Y half-extent changed from 12 cm for bloom to 5 cm for bud. Reloading the source root layer restored the on-disk bloom selection and the 12 cm extent. An actor-level translation of 33 cm survived that reload.</p>
<p>This test authored the temporary variant opinion through the native OpenUSD Python API in the Unreal process. It did not prove that a particular USD Stage panel edit is saved into a persistent override layer. If the requirement is to keep an Unreal-authored variant selection across reloads, explicitly choose an edit target, save that layer and reopen it. The <a href="https://digitalgarden.si/blog/openusd-pipeline-beginners/">beginner stage exercise</a> explains the opinion stack behind this failure.</p>
<h2>Cook and bake the HDA</h2>
<p>The Engine plugin imported the test HDA, started a matching Houdini session, instantiated it and completed synchronous processing. The cooked actor contained 100 instances. <code>bake_all_outputs()</code> returned true and produced baked output actors.</p>
<p>SideFX's public API documents session management, asset wrappers, synchronous processing and baking.<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup> The installed API still supports <code>instantiate_asset</code>, but marks it deprecated in favor of <code>instantiate</code>. Record that distinction if you adapt this fixture; do not copy a tutorial's method names without checking your installed build.</p>
<p>The bake demonstrates that this small tool can produce native output. It is not a packaged-game test or evidence that a shipped game should cook HDAs at runtime. Reopen and inspect the baked assets without depending on a live Houdini session before accepting that delivery contract.</p>
<h2>A checklist that catches more than a successful import</h2>
<ol>
<li>Record the Houdini build, receiving UE patch, plugin descriptor and both USD versions.</li>
<li>Measure a known object in receiving units and confirm the vertical direction.</li>
<li>Test Preview Surface, MaterialX and native overrides separately.</li>
<li>Correct shader-schema errors before blaming a render context.</li>
<li>Inspect descendant instance components and assert the actual count.</li>
<li>Change a variant and measure a geometric consequence.</li>
<li>Reload the file and record which source and actor edits survive.</li>
<li>Import native assets separately from opening a live stage.</li>
<li>Cook and bake an HDA, then inspect the baked deliverable.</li>
</ol>
<p>All substantive import, component and bake results were obtained in isolated test projects. The production demo project and the live garden's models were not changed. This is an editor pipeline test on one workstation, not a production certification. For the browser destination, the <a href="https://digitalgarden.si/blog/openusd-threejs-usd-web-viewer/">OpenUSD in Three.js experiment</a> records a different set of material and primitive limitations.</p>
<h2>References</h2>
<hr />
<p><a href="https://digitalgarden.si/blog/houdini-unreal-usd/">Read the original article, supporting data and updates</a>.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>Epic Games, <a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/universal-scene-description-in-unreal-engine">Universal Scene Description in Unreal Engine</a>, 5.8 documentation checked 19 October 2026. <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩︎</a></p>
</li>
<li id="footnote-2">
<p>SideFX, <a href="https://www.sidefx.com/docs/houdini/unreal/publicapi.html">Houdini Engine for Unreal Public API</a>. Runtime signatures were additionally read from the installed plugin. <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded></item><item><title><![CDATA[Gaussian Splats in Three.js: Native Renderer and Spark Tested]]></title><description><![CDATA[Test native Three.js Gaussian splats and Spark 2.3.1 with owned fixtures: WebGPU compatibility, SPZ v4 loading, GSOPs cropping and transparency limits]]></description><link>https://digitalgarden-alicia.hashnode.dev/gaussian-splats-in-three-js-native-renderer-and-spark-tested</link><guid isPermaLink="true">https://digitalgarden-alicia.hashnode.dev/gaussian-splats-in-three-js-native-renderer-and-spark-tested</guid><dc:creator><![CDATA[Alicia lu]]></dc:creator><pubDate>Sun, 25 Oct 2026 06:50:52 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>AI-assisted disclosure: Codex assisted with research, code and writing. Measurements, failure cases and limitations are retained from the original article.</p>
</blockquote>
<blockquote>
<p>Originally published by Alicia on 2026-10-19: <a href="https://digitalgarden.si/blog/gaussian-splats-threejs/">Digital Garden by Alicia</a>.</p>
</blockquote>
<p>Native Three.js Gaussian splats worked on both WebGPU and WebGL2 in this test, including direct SPZ v4 loading with SPZLoader. Spark 2.3.1 worked with WebGLRenderer, but the tested integration failed with WebGPURenderer. The same real SPZ v4 file failed Spark’s reader with an “Invalid gzip header” error.</p>
<p>For how this splat study relates to the mesh plant notes, see <a href="https://digitalgarden.si/blog/threejs-3d-garden-rendering/">the garden's Three.js rendering overview</a>.</p>
<p><strong>Correction, 20 October 2026:</strong> the original article incorrectly stated that the pinned Three.js 0.186.1 package had no SPZLoader. It does include that loader. I have now directly loaded and rendered the original SPZ v4 fixture on native WebGPU and WebGL2. The download viewer and receipts below include this correction; the original 19 October PLY measurements retain their original scope and date.</p>
<p>These findings come from an <strong>owned synthetic fixture</strong>, tested on 19 October 2026. It contains 12,014 Gaussian points derived from the procedural plant in the previous article, including 500 intentionally placed outliers. It is not a trained capture or a photogrammetry quality benchmark. Its higher spherical harmonic coefficients are all zero, so the results do not validate view dependent appearance.</p>
<p><img src="https://digitalgarden.si/marketing/research/webgpu-series-20261019/splat-native.png" alt="Native Three.js Gaussian splats showing the synthetic flower, outliers and opaque mesh references" /></p>
<h2>Pin the renderer and the data</h2>
<p>The native path used Three.js 0.186.1 with its GaussianSplat mesh and PLY loader under WebGPURenderer. The fallback used the same renderer with the WebGL backend forced. The Spark path used Spark 2.3.1 with WebGLRenderer, matching its documented setup.<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup> All tests ran in a visible Chromium 155 in app browser on an NVIDIA RTX 5070 Ti, at 1280 × 720 and DPR 1.</p>
<p>The PLY generator emitted position, color, opacity, scale, rotation and the expected spherical harmonic fields. The underlying plant produced 11,514 jittered points; the separate 500 red outliers made crop failures easy to detect. Encoding a repeated synthetic shape is much easier than compressing a complex real capture, so this dataset must not be used to advertise a general compression ratio.</p>
<h2>What loaded, and what failed</h2>
<table>
<thead>
<tr>
<th>Path</th>
<th>Input</th>
<th>Result</th>
</tr>
</thead>
<tbody><tr>
<td>Native WebGPURenderer, WebGPU</td>
<td>PLY, 12,014 points</td>
<td>Rendered</td>
</tr>
<tr>
<td>Native WebGPURenderer, WebGL2</td>
<td>Same PLY</td>
<td>Rendered</td>
</tr>
<tr>
<td>Spark + WebGLRenderer</td>
<td>Same PLY</td>
<td>Rendered</td>
</tr>
<tr>
<td>Spark + WebGPURenderer</td>
<td>Same PLY</td>
<td>TypeError at renderer context extension access</td>
</tr>
<tr>
<td>Spark + WebGLRenderer</td>
<td>Actual SPZ v4</td>
<td>Invalid gzip header</td>
</tr>
<tr>
<td>Native WebGPU</td>
<td>SPZ v4 decoded to PLY</td>
<td>Rendered, 12,014 points</td>
</tr>
<tr>
<td>Native WebGPU, checked 20 October</td>
<td>Direct SPZ v4 via SPZLoader</td>
<td>Rendered, 12,014 points</td>
</tr>
<tr>
<td>Native WebGL2, checked 20 October</td>
<td>Same direct SPZ v4</td>
<td>Rendered, 12,014 points</td>
</tr>
<tr>
<td>Native WebGPU</td>
<td>32 byte per point SPLAT</td>
<td>Rendered, 12,014 points</td>
</tr>
<tr>
<td>Native WebGPU</td>
<td>GSOPs cropped PLY</td>
<td>Rendered, 11,514 points</td>
</tr>
</tbody></table>
<p>The Spark WebGPU failure was <code>this.renderer.getContext(...).getExtension is not a function</code>. It is evidence about this integration and version, not proof that every future Spark path or adapter must fail. The working WebGLRenderer route remains useful.</p>
<p>Three.js 0.186.1 includes SPZLoader alongside the PLY, SPLAT and KSPLAT loaders. For v4, parsing can be asynchronous; <code>loadAsync</code> handles that path.<sup><a id="footnote-ref-4" href="#footnote-4" data-footnote-ref aria-describedby="footnote-label">2</a></sup> The earlier SPZ-to-PLY conversion worked, but it is not required to use this file with the native renderer.</p>
<pre><code class="language-js">import { SPZLoader } from 'three/addons/loaders/SPZLoader.js';
import { GaussianSplat } from 'three/addons/objects/GaussianSplat.js';

const geometry = await new SPZLoader().loadAsync('fixture-v4.spz');
const splat = new GaussianSplat(geometry);
// Align this converted fixture with its original Y-up PLY scene.
splat.rotation.x = Math.PI;
scene.add(splat);
</code></pre>
<p>The Rx180 alignment above is specific to this file’s conversion and our reference scene, not a universal SPZ requirement. After that transform, corresponding decoded centers differed from the original PLY by at most 0.0001221 scene units per component. Both visible browser backends rendered 12,014 points at a 1280 × 720 canvas and renderer pixel ratio 1. Five local load checks per backend passed; this follow-up did not rerun the FPS benchmark or validate nonzero SH appearance.</p>
<p><img src="https://digitalgarden.si/marketing/research/webgpu-series-20261019/native-spz-v4.png" alt="Direct SPZ v4 loaded by the pinned native Three.js SPZLoader on WebGPU, aligned with the original fixture and opaque mesh references" /></p>
<h2>SPZ v4 is not the old gzip container</h2>
<p>I uploaded the owned PLY into Niantic’s official browser utility, downloaded SPZ, and inspected the result. It had an <code>NGSP</code> header, version 4, 12,014 points and 55,323 bytes. Reopening it in the utility showed six streams. The utility then decoded it back to PLY for the native renderer test. No external scan was uploaded or republished.</p>
<p>The official format documentation describes v4’s plain header and Zstandard streams, while earlier versions used gzip.<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">3</a></sup> That explains why a reader expecting the older container can reject a valid v4 file before rendering starts. It does not mean the SPZ download was corrupt. The exact utility build commit was not exposed in its UI; the record keeps the test date, downloaded file hash and inspected header instead of inventing one.</p>
<p>The unusually small SPZ is a property of this simple synthetic dataset with zero higher SH coefficients. Real captures need separate file size, decoder time and appearance measurements.</p>
<h2>Crop in Houdini, then reopen</h2>
<p>I used the public GSOPs 3.0.0 HDAs, pinned to <a href="https://github.com/cgnomads/GSOPs/tree/46f1756c65d40315fa977af78dfc7af860117894">commit 46f1756c65d4</a>, in an isolated Houdini 22.0.429 subprocess. Loading only the apparent import/crop/export HDAs initially left missing dependencies. Loading the public dependency set resolved that problem without changing the user’s global Houdini package configuration.</p>
<p>The crop box removed the 500 known outliers. The exported PLY was then imported again: <strong>12,014 input points → 11,514 cropped points → 11,514 reopened points</strong>, with no node errors. Some no matching group/attribute export warnings remained and are recorded. This proves the known count crop and round trip, not arbitrary scan cleanup or attribute equivalence for nonzero SH data.<sup><a id="footnote-ref-3" href="#footnote-3" data-footnote-ref aria-describedby="footnote-label">4</a></sup></p>
<p><img src="https://digitalgarden.si/marketing/research/webgpu-series-20261019/splat-cropped.png" alt="The cropped synthetic plant in the native renderer after the known outliers were removed" /></p>
<p>GSOPs is a third party tool with its own license. The download links its official source and pinned version; it does not redistribute the plugin. An external sample marked noncommercial was considered and rejected for this publication. All published splat imagery instead comes from the owned fixture.</p>
<h2>Performance and compositing boundaries</h2>
<p>Each primary renderer ran a 10 second static view after one second of warmup. All three working paths measured about 59.99 FPS, with 95th percentile frame intervals of 16.7–16.8 ms. Native totals were four render calls and Spark totals were three. At only 12,014 points and a 60 Hz display ceiling, this is a compatibility check, not a large scene throughput ranking.</p>
<p>Five warm local load trials had medians of 116.7 ms for native WebGPU PLY, 76.9 ms for native WebGL2 PLY and 16.7 ms for Spark WebGL PLY. Each includes fetch, parsing, initialization and the first submitted frame. Their pipelines and decoder paths differ; these values do not isolate decoder speed or predict CDN loading.</p>
<p>I also placed opaque red and green mesh references before and behind the splats, then introduced a translucent pink plane. The working paths rendered the opaque controls, but the translucent views differed in brightness and opacity. They did not achieve pixel parity. A scene with layered petals, glass or overlapping translucent materials needs a dedicated sorting and art direction acceptance pass. “It loads” is a much narrower claim than “it composites correctly for every asset.”</p>
<h2>Use the test as a compatibility receipt</h2>
<p>Codex executed the native scripts, official utility workflow and browser checks. The supplied receipts distinguish a successful render, a known setup error and a load only check.</p>
<p>The <a href="https://digitalgarden.si/marketing/research/webgpu-series-20261019/webgpu-garden-tests.zip">experiment download</a> includes owned Gaussian fixtures, generation code, compact receipts and a standalone viewer, updated 20 October. It includes the actual SPZ v4 file, the corrected native loader path and the recorded Spark container failure. The <a href="https://digitalgarden.si/marketing/research/webgpu-series-20261019/results-2026-10-19.json">machine-readable results</a> preserve the original measurements and add a separately dated correction. No training run, real capture reconstruction, large splat scene or physical phone was tested. A later <a href="https://digitalgarden.si/blog/spark-vs-gaussiansplats3d-threejs/">Spark and GaussianSplats3D comparison</a> loads this same fixture through native Three.js, Spark and GaussianSplats3D, including KSPLAT and direct SPZ v4, and keeps the combinations that failed to draw.</p>
<p>For mesh plants, start with <a href="https://digitalgarden.si/blog/houdini-procedural-plants-realtime-web/">Houdini procedural geometry and wind</a> and <a href="https://digitalgarden.si/blog/houdini-gltf-threejs-optimization/">glTF compression</a>. The <a href="https://digitalgarden.si/blog/webgpu-garden-houdini-threejs/">main garden article</a> explains where those meshes fit. The current production garden remains a mesh based scene; this splat study does not silently replace it.</p>
<p>For a trained sample rather than this synthetic flower, <a href="https://digitalgarden.si/blog/spirula-studio-garden-splat-threejs/">Spirula Studio to Three.js</a> uses the official Mip-NeRF 360 Garden images with supplied camera poses, Vulkan training, a PLY checkpoint and Spark. It documents 100,000 splats and is not a phone capture. For an actual generated environment rather than this synthetic flower, the <a href="https://digitalgarden.si/blog/viggle-jst2-spz-threejs-garden/">free PINOC / JST-2 SPZ test</a> uses a million-point garden export. It records native and Spark 2.4.0 loading, opaque plant occlusion and the WebGPURenderer combination that failed. Different input files and package versions are kept separate.</p>
<hr />
<p><a href="https://digitalgarden.si/blog/gaussian-splats-threejs/">Read the original article, supporting data and updates</a>.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p><a href="https://sparkjs.dev/docs/">Spark documentation</a>, including its WebGLRenderer quick start, checked 19 October 2026. <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩︎</a></p>
</li>
<li id="footnote-4">
<p>Three.js, <a href="https://threejs.org/docs/pages/SPZLoader.html">SPZLoader documentation</a>, checked against the installed 0.186.1 source and the original fixture on 20 October 2026. <a href="#footnote-ref-4" data-footnote-backref aria-label="Back to reference 4">↩︎</a></p>
</li>
<li id="footnote-2">
<p>Niantic, <a href="https://github.com/nianticlabs/spz">SPZ format and official utility</a>. <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩︎</a></p>
</li>
<li id="footnote-3">
<p><a href="https://github.com/cgnomads/GSOPs">GSOPs official repository</a>, version 3.0.0 public HDAs at the pinned commit above. <a href="#footnote-ref-3" data-footnote-backref aria-label="Back to reference 3">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded></item><item><title><![CDATA[Claude Code vs Codex: One Reproducible Three.js Wind Task]]></title><description><![CDATA[A paired Claude Code and Codex workflow test: the same Three.js plant fixture, wind and color-space tasks, unchanged outputs and independent browser c]]></description><link>https://digitalgarden-alicia.hashnode.dev/claude-code-vs-codex-one-reproducible-three-js-wind-task</link><guid isPermaLink="true">https://digitalgarden-alicia.hashnode.dev/claude-code-vs-codex-one-reproducible-three-js-wind-task</guid><dc:creator><![CDATA[Alicia lu]]></dc:creator><pubDate>Sun, 25 Oct 2026 06:48:29 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>AI-assisted disclosure: Codex assisted with research, code and writing. Measurements, failure cases and limitations are retained from the original article.</p>
</blockquote>
<blockquote>
<p>Originally published by Alicia on 2026-10-20: <a href="https://digitalgarden.si/blog/claude-code-vs-codex-threejs/">Digital Garden by Alicia</a>.</p>
</blockquote>
<p>Claude Code and Codex both completed this small Three.js task and produced builds that passed. Independent browser checks then loaded both unchanged outputs on WebGPU and WebGL2 fallback. This is one paired workflow case study, not evidence that either tool is generally better at 3D work.</p>
<p>For the renderer and asset tests this wind task builds on, see <a href="https://digitalgarden.si/blog/threejs-3d-garden-rendering/">the garden's Three.js rendering overview</a>.</p>
<p>The <a href="https://digitalgarden.si/play/">production garden</a> already has wind. I used a separate frozen starter so that this test would not pretend to add a feature that was already present, expose the private application, or depend on unrelated project history. Codex operated the test and subsequent acceptance checks on 20 October 2026.</p>
<p><img src="https://digitalgarden.si/marketing/research/comparisons-20261020/agent-claude.jpg" alt="Claude Code's unchanged output, running in the independent browser acceptance wrapper" /></p>
<h2>The same starter and request</h2>
<p>Both tools received the same source files, fixture hashes, pinned Three.js 0.186.1 and Vite 8.3.1 dependencies, AGENTS.md and task prompt. CLAUDE.md contained an AGENTS.md reference. Each started a new noninteractive session with an isolated copy of the frozen repository.</p>
<p>Task A was a bounded sinusoidal X bend driven by the exported _stiff and _phase vertex attributes. Roots with stiffness zero had to stay fixed; maximum displacement at wind=1 had to stay within 0.10 units. The existing Wind slider had to update a GPU uniform, with one InstancedMesh per source primitive and no per-frame matrix rewrites. Task B was a deliberately incorrect albedo texture colour space: fix the colour texture without treating geometry data as sRGB.</p>
<p>The fixture contains 16 instances of an owned 6,056-triangle procedural plant. Its matrices are translations, not arbitrary scaling or rotated parents. The root/bound argument in this article therefore applies to that controlled transform setup. It does not validate a general production wind solver.</p>
<h2>Which tools actually ran</h2>
<table>
<thead>
<tr>
<th>Tool</th>
<th>CLI</th>
<th>Model recorded/requested</th>
<th>Execution environment</th>
</tr>
</thead>
<tbody><tr>
<td>Claude Code</td>
<td>2.1.303</td>
<td>claude-opus-5-5, reported in the run</td>
<td>Existing authenticated first-party Claude account on a Linux VPS, reached over Tailscale SSH</td>
</tr>
<tr>
<td>Codex</td>
<td>0.144.1</td>
<td>gpt-5.6-sol, successful explicit CLI selection</td>
<td>Existing ChatGPT login on the local Windows workstation</td>
</tr>
</tbody></table>
<p>The first Codex invocation used the desktop's gpt-6.1-sol default and was rejected before editing because that model was unsupported by this CLI account route. I retained the failure and used a model listed by the CLI's local catalogue. This was a setup correction, not an extra coding attempt on a failed solution. The successful task used the original unchanged starter.<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup></p>
<p>The environments and permission profiles differ. Claude used the VPS's existing configuration and encountered command permission denials while inspecting the GLB. It worked around them with shell utilities, and its report disclosed an inspection script written in a temporary directory outside the intended fixture. That is a scope failure worth retaining: a written instruction is not filesystem confinement. Codex used its Windows workspace-write mode. I did not normalize these into an artificial security or speed score.<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup></p>
<h2>What the patches changed</h2>
<p>Claude added a sine expression with clamped wind and stiffness, a slider input listener, and the sRGB albedo setting. Codex used the same basic bounded multiplication but relied on the verified fixture's stiffness range and the slider's 0–1 range. Both used phase as radians. The prompt left frequency open, so their selected frequencies differ slightly.</p>
<p>For the actual input domain, sin is bounded by one and both wind/stiffness factors lie between zero and one. Multiplying by 0.10 establishes the maximum X offset; multiplying by zero establishes the fixed root. This is source and data reasoning, not GPU vertex readback. Only Claude's additional clamps defend against a later out-of-range input; Codex's code needs its stated input contract to remain true.</p>
<p>Both outputs changed the albedo to THREE.SRGBColorSpace. The fixture assets, camera, placements, dependency versions and protected instruction files remained unchanged. The <a href="https://digitalgarden.si/marketing/research/comparisons-20261020/agents-tests.zip">paired-task download</a> includes the exact prompt, starter hashes and unmodified source outputs. The public package excludes authentication data, private configuration and raw model transcripts.</p>
<h2>Independent browser acceptance</h2>
<p>Neither tool claimed to have verified the browser. Both ran npm run build and disclosed that visual testing remained outstanding. I then appended an independent QA wrapper to copies of their unchanged source. The wrapper displays the active backend, uniform value, instance count and texture colour space, and records three 10-second static-camera observations. Its additions are separated from the saved agent patches.</p>
<p>The browser checks used a 960 × 640 canvas at DPR 1. Both outputs loaded on both backends, retained one InstancedMesh with 16 instances, and reported the albedo as sRGB. Moving the slider to 0 and 1 updated the displayed shared uniform; the rendered plants stopped and swayed as expected. Screenshots and DOM receipts are retained. This verifies the fixture's interactive behavior, not production normals, physically preserved stem length or every device.</p>
<p>The observations stayed near the display ceiling, so no FPS winner is inferred. Wall time and token telemetry are retained in the compact evidence, but the accounts, model families, OS, caching and permission delays differ. Subscription telemetry is not an invoice and is not used to estimate comparative cash cost.</p>
<h2>What I would carry into a real garden change</h2>
<p>The useful common behavior was editing a small explicit task, preserving the asset contract, building, and stating validation limits. The additional operator step was essential: a passing build did not verify WebGPU compilation, the fallback, visible motion or the UI.</p>
<p>For a real change, freeze the task and inputs, record the actual model and execution route, review the diff, and independently test each required backend. This paired run does not test Houdini automation, a large migration, long-term maintenance or general model intelligence. Continue with the <a href="https://digitalgarden.si/blog/threejs-webgpu-vs-webgl-instanced-plants/">full plant submission matrix</a> or the <a href="https://digitalgarden.si/blog/claude-code-for-3d/">Houdini-oriented Claude Code setup</a>.</p>
<hr />
<p><a href="https://digitalgarden.si/blog/claude-code-vs-codex-threejs/">Read the original article, supporting data and updates</a>.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>OpenAI, <a href="https://learn.chatgpt.com/docs/developer-commands#codex-exec">Codex noninteractive execution</a>, checked 20 October 2026. Availability was verified by the actual account response, not inferred from a desktop model label. <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩︎</a></p>
</li>
<li id="footnote-2">
<p>Anthropic, <a href="https://code.claude.com/docs/en/cli-reference">Claude Code CLI reference</a>, checked 20 October 2026. Permission and print-mode settings describe configuration; they do not substitute for inspecting the recorded actions. <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded></item><item><title><![CDATA[Spark vs GaussianSplats3D vs Native Three.js: An Integration Test]]></title><description><![CDATA[Pinned Spark, GaussianSplats3D and native Three.js tests: the same synthetic splats, actual PLY/KSPLAT/SPZ loads, mesh occlusion and backend compatibi]]></description><link>https://digitalgarden-alicia.hashnode.dev/spark-vs-gaussiansplats3d-vs-native-three-js-an-integration-test</link><guid isPermaLink="true">https://digitalgarden-alicia.hashnode.dev/spark-vs-gaussiansplats3d-vs-native-three-js-an-integration-test</guid><dc:creator><![CDATA[Alicia lu]]></dc:creator><pubDate>Sun, 25 Oct 2026 06:47:16 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>AI-assisted disclosure: Codex assisted with research, code and writing. Measurements, failure cases and limitations are retained from the original article.</p>
</blockquote>
<blockquote>
<p>Originally published by Alicia on 2026-10-20: <a href="https://digitalgarden.si/blog/spark-vs-gaussiansplats3d-threejs/">Digital Garden by Alicia</a>.</p>
</blockquote>
<p>A Gaussian splat viewer must be tested with the renderer and file encoding it will actually use. In this integration test, I compared Spark 2.3.1, GaussianSplats3D 0.4.7 and native Three.js 0.186.1 on the same owned synthetic fixture. The measurements separate successful rendering paths from setup and format failures; they do not rank capture quality.</p>
<p>This integration test is grouped with the other renderer experiments in <a href="https://digitalgarden.si/blog/threejs-3d-garden-rendering/">the garden's Three.js rendering overview</a>.</p>
<p>Codex ran the conversions and browser checks on 20 October 2026. Splats remain a research fixture alongside <a href="https://digitalgarden.si/play/">Alicia's interactive garden</a>, not a production garden feature.</p>
<p><img src="https://digitalgarden.si/marketing/research/comparisons-20261020/splats-native.jpg" alt="The owned synthetic Gaussian fixture with opaque mesh references" /></p>
<h2>A known input rather than an unknown capture</h2>
<p>The fixture contains 12,014 synthetic splats and known outliers. Higher spherical-harmonic coefficients are zero; the KSPLAT conversion explicitly used degree zero. The PLY is 2,981,073 bytes. GaussianSplats3D's public parser and compression-level-1 SplatBufferGenerator produced a 294,024-byte KSPLAT and retained 12,014 splats. The independently converted SPZ v4 fixture is 55,323 bytes.</p>
<p>These are different encodings and compression choices. Their byte counts are not an objective image-quality ranking, and this tiny generated flower does not represent a room scan, reflective object, noisy phone capture or full SH scene. No scanning, training or reconstruction quality is claimed.</p>
<p>All paths used one deduplicated Three.js 0.186.1 installation, the same camera, opaque Houdini plant, orange depth reference, canvas and DPR. Native paths used WebGPURenderer on WebGPU or forced WebGL2. Spark and GaussianSplats3D used classic WebGLRenderer. The accepted GaussianSplats3D DropInViewer path used CPU floating-point sorting and shared-memory workers; the local server supplied COOP/COEP headers.<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup></p>
<h2>Record which combinations actually load</h2>
<p>Each listed file used up to five unique loopback URLs; a failure stops that format's attempts and is retained. Successful load timings include readiness, compilation and a submitted frame. They are warm local observations rather than public CDN throughput or completed GPU timing.</p>
<table>
<thead>
<tr>
<th>Rendering path</th>
<th>Format</th>
<th>Result</th>
<th>Median local load</th>
</tr>
</thead>
<tbody><tr>
<td>native-webgpu</td>
<td>fixture.ply</td>
<td>loaded, 12,014 splats</td>
<td>116.3 ms</td>
</tr>
<tr>
<td>native-webgpu</td>
<td>fixture-v4.spz</td>
<td>loaded, 12,014 splats</td>
<td>66.6 ms</td>
</tr>
<tr>
<td>native-fallback</td>
<td>fixture.ply</td>
<td>loaded, 12,014 splats</td>
<td>83.3 ms</td>
</tr>
<tr>
<td>native-fallback</td>
<td>fixture-v4.spz</td>
<td>loaded, 12,014 splats</td>
<td>33.4 ms</td>
</tr>
<tr>
<td>spark-webgl</td>
<td>fixture.ply</td>
<td>loaded, 12,014 splats</td>
<td>33.2 ms</td>
</tr>
<tr>
<td>spark-webgl</td>
<td>fixture-v4.spz</td>
<td>failed in pinned combination</td>
<td>not reported</td>
</tr>
<tr>
<td>gs3d-webgl</td>
<td>fixture.ply</td>
<td>loaded, 12,014 splats</td>
<td>152.6 ms</td>
</tr>
<tr>
<td>gs3d-webgl</td>
<td>fixture.ksplat</td>
<td>loaded, 12,014 splats</td>
<td>141.8 ms</td>
</tr>
</tbody></table>
<p>Spark's WebGPURenderer compatibility was checked separately and its failure is retained. That is an observed result of these versions and this renderer integration, not a statement that future Spark versions cannot support WebGPU. Spark's documented quick-start path and renderer design should be read for the tested version.<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup></p>
<p>The first GaussianSplats3D GPU/integer-sort configuration decoded the expected count but never submitted a visible splat draw. The screenshot contained only the mesh references, and the triangle counter confirmed that absence. I rejected its empty-scene FPS, added a rendering-contribution guard, initialized the DropInViewer with the host renderer before loading, and checked a CPU floating-point-sort branch. Only that branch's visible rerun is included below. This is a compatibility finding for the tested integration, not a claim that the library's GPU sorting is always broken.</p>
<p>Native Three.js 0.186.1 does include SPZLoader. An older assertion that native SPZ loading was absent was wrong and was corrected before this comparison. The SPZ path uses that loader explicitly. This converted Y-up fixture needed an X-axis half-turn to align with its PLY reference; that correction is specific to this file, not a universal SPZ rule.</p>
<p>File-format support alone does not ensure that every encoding works: SPZ versions, compression headers and library versions matter. A failed v4 file load is not turned into a successful PLY result under the same label.</p>
<p>Update, 21 October: a different, million-point SPZ v4 export from free PINOC loaded successfully with Spark 2.4.0 and native Three.js in <a href="https://digitalgarden.si/blog/viggle-jst2-spz-threejs-garden/">the JST-2 garden integration test</a>. Both the input and Spark version changed, so that result does not establish which change explains the earlier failure. Spark with WebGPURenderer still failed in the newer test. A later <a href="https://digitalgarden.si/blog/spirula-studio-garden-splat-threejs/">Spirula Studio and Spark</a> test also used Spark 2.4.0, this time on a Vulkan-trained official Garden PLY and the SPZ converted from it. That sample is not this synthetic fixture.</p>
<h2>Orbit observations after compatibility</h2>
<p>The browser reported Chromium 155 on Windows and an RTX 5070 Ti. Each successful path ran one discarded 20-second warm-up and three measured 20-second half-orbits at 1280 × 720, DPR 1, without antialiasing. This bounded integration protocol replaces the broader unexecuted draft benchmark plan; no three-warm-up/five-run performance claim is made here. Trials ran sequentially after the plant matrix, with visibility guards.</p>
<table>
<thead>
<tr>
<th>Successful path</th>
<th>Median mean FPS</th>
<th>Median 1% low FPS</th>
<th>Median CPU render time</th>
</tr>
</thead>
<tbody><tr>
<td>native-webgpu</td>
<td>59.99</td>
<td>59.74</td>
<td>0.215 ms</td>
</tr>
<tr>
<td>native-fallback</td>
<td>59.99</td>
<td>59.74</td>
<td>0.155 ms</td>
</tr>
<tr>
<td>spark-webgl</td>
<td>59.99</td>
<td>59.72</td>
<td>0.135 ms</td>
</tr>
<tr>
<td>gs3d-webgl</td>
<td>59.99</td>
<td>59.77</td>
<td>0.080 ms</td>
</tr>
</tbody></table>
<p>The scene is small and display capped. These figures cannot establish a universal fastest renderer. Gaussian sorting may involve asynchronous worker work outside the measured renderer.render() call; CPU submission time is not total application CPU cost. JavaScript heap is not total VRAM, and there is no controlled memory ranking.</p>
<h2>Test depth and transparency separately</h2>
<p>The viewer supplies three fixed inspection states: an opaque plant/bar in front, the splats in front, and a translucent petal. Screenshots retain the same references and view. Opaque depth occlusion and mixed transparent compositing are different acceptance questions; a plausible opaque screenshot does not prove globally correct sorting with arbitrary transparent objects. The flower appeared darker under Spark and GaussianSplats3D's tested defaults than under the native path. I did not normalize that into a pixel-quality score or claim colour-management parity.</p>
<p>The fast half-orbit helps expose transient sorting changes, but the saved screenshots are representative frames, not proof that every intermediate ordering is perfect. This test does not certify a general transparency solution or high-motion capture scene. The small fixture and its simple degree-zero colours limit what can be concluded.</p>
<h2>Carry a verified integration into the project</h2>
<p>The <a href="https://digitalgarden.si/marketing/research/comparisons-20261020/splats-tests.zip">splat comparison download</a> contains the pinned package, known-count fixtures, KSPLAT converter, independent viewer and detailed success/failure receipts. Bundle-export gzip sizes are recorded separately from asset sizes and actual network transfer. Dependencies are installed from their official packages and retain their licenses; no external capture sample is redistributed.</p>
<p>For this garden's current node-renderer stack, native loading is worth testing first because it shares that renderer. Classic-WebGL libraries remain separate viable integration paths where their versions and formats pass. The correct choice follows the required format, renderer, sorting behavior and scene budget. Continue with <a href="https://digitalgarden.si/blog/gaussian-splats-threejs/">the earlier Gaussian asset workflow</a> or <a href="https://digitalgarden.si/blog/threejs-webgpu-vs-webgl-instanced-plants/">the plant submission measurements</a>.</p>
<hr />
<p><a href="https://digitalgarden.si/blog/spark-vs-gaussiansplats3d-threejs/">Read the original article, supporting data and updates</a>.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>GaussianSplats3D, <a href="https://github.com/mkkellogg/GaussianSplats3D">official repository and DropInViewer documentation</a>, checked 20 October 2026. The tested package was 0.4.7. <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩︎</a></p>
</li>
<li id="footnote-2">
<p>Spark, <a href="https://sparkjs.dev/docs/">official documentation</a>, checked 20 October 2026. The tested package was 2.3.1. Native SPZ behavior was checked against the installed Three.js 0.186.1 source and actual v4 input. <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded></item><item><title><![CDATA[Viggle JST-2 SPZ in Three.js: A Free PINOC Garden Test]]></title><description><![CDATA[A free PINOC garden export tested in Three.js 0.186.1 and Spark 2.4.0: SPZ v4 loading, opaque mesh occlusion, local timings and a WebGPU incompatibili]]></description><link>https://digitalgarden-alicia.hashnode.dev/viggle-jst-2-spz-in-three-js-a-free-pinoc-garden-test</link><guid isPermaLink="true">https://digitalgarden-alicia.hashnode.dev/viggle-jst-2-spz-in-three-js-a-free-pinoc-garden-test</guid><dc:creator><![CDATA[Alicia lu]]></dc:creator><pubDate>Sun, 25 Oct 2026 06:45:44 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>AI-assisted disclosure: Codex assisted with research, code and writing. Measurements, failure cases and limitations are retained from the original article.</p>
</blockquote>
<blockquote>
<p>Originally published by Alicia on 2026-10-21: <a href="https://digitalgarden.si/blog/viggle-jst2-spz-threejs-garden/">Digital Garden by Alicia</a>.</p>
</blockquote>
<p>A garden exported from Viggle's free PINOC interface loaded as SPZ v4 in native Three.js 0.186.1 on WebGPU and its WebGL2 fallback, and in Spark 2.4.0 with classic WebGLRenderer. Spark did not initialize with WebGPURenderer in this test. The exported scene contains 1,048,576 Gaussian splats and occupies 17,575,822 bytes. These are measurements from one generated scene, not a claim about every JST-2 output.</p>
<p>Codex operated the browser and ran the local tests for Alicia on 21 October 2026. This is a research fixture beside <a href="https://digitalgarden.si/play/">the interactive garden</a>, not a new feature in the production garden. No paid API, subscription or purchased credits were used.</p>
<p>The earlier synthetic splat tests and the mesh plant notes are collected in <a href="https://digitalgarden.si/blog/threejs-3d-garden-rendering/">the Three.js 3D garden rendering guide</a>.</p>
<p><img src="https://digitalgarden.si/marketing/research/pinoc-20261021/native-front.png" alt="The PINOC garden and an opaque Houdini plant in the native Three.js viewer" /></p>
<h2>What the free PINOC session actually produced</h2>
<p>Viggle presents JST-2 as a world model, and PINOC is its browser interface.<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup> I used this prompt:</p>
<blockquote>
<p>Make an environment: a compact peaceful botanical garden at eye level, with a curving narrow pale gravel path through dense blue grape hyacinths, pink tulips, green leaves and soft grass. Low flower beds in the foreground, a small weathered stone border, distant leafy trees. Soft overcast daylight, natural botanical colors, realistic spatial depth, no people, no text or logos. Keep an open patch beside the path for adding a separate 3D plant mesh.</p>
</blockquote>
<p>The free balance went from 60 to 43 credits: five for the initial generation and three each for four automatic filling passes. The agent kept trying to repair gaps, so I stopped that sequence after the fourth pass. This is the observed cost of this session, including those passes; it is not a quote for all future accounts or a one-pass generation cost. The changing interface quality scores did not establish that every hole had been removed.</p>
<p>The downloaded file was a <code>.spz</code>, not a mesh with editable faces. Its header identified SPZ version 4, 1,048,576 points, spherical-harmonic degree zero and 12 fractional bits. Its SHA-256 is <code>c0dfc6a38528e1324cb891b670a67d3e9dd7b1e7249e9375759e95a965a0d421</code>.</p>
<p>I did not measure the vendor's model inference time. The interactive session includes prompting, queueing, automatic repairs, viewing and export; it cannot validate a one-second model-generation claim. No PLY or character <code>.vsplat</code> result is included in this experiment. A later <a href="https://digitalgarden.si/blog/spirula-studio-garden-splat-threejs/">Spirula Studio PLY to SPZ</a> test started from the official Mip-NeRF 360 Garden sample: Spirula Studio trained it on Vulkan, wrote a PLY, and Spark converted that PLY. It does not replace this generated PINOC file.</p>
<h2>Three working renderer paths and one recorded failure</h2>
<p>The independent viewer combines that export with one owned Houdini grape-hyacinth mesh, containing 6,056 triangles. The mesh uses an opaque magenta inspection material, so it is easy to distinguish from generated flowers. Both libraries resolve to the same Three.js installation.</p>
<table>
<thead>
<tr>
<th>Path</th>
<th>Renderer</th>
<th>Actual result</th>
</tr>
</thead>
<tbody><tr>
<td>Native WebGPU</td>
<td>WebGPURenderer, actual WebGPU backend</td>
<td>Loaded and rendered 1,048,576 splats</td>
</tr>
<tr>
<td>Native fallback</td>
<td>WebGPURenderer with forceWebGL</td>
<td>Loaded and rendered through WebGL2</td>
</tr>
<tr>
<td>Spark WebGL</td>
<td>Spark 2.4.0 with WebGLRenderer</td>
<td>Loaded and rendered after sorting was ready</td>
</tr>
<tr>
<td>Spark WebGPU</td>
<td>Spark 2.4.0 with WebGPURenderer</td>
<td>Initialization failed</td>
</tr>
</tbody></table>
<p>The native branch uses <code>SPZLoader</code> and <code>GaussianSplat</code> from the installed Three.js add-ons.<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup> For this PINOC export, zero rotation aligned the scene with the camera; the half-turn used for my earlier synthetic fixture was unnecessary. Coordinate corrections must follow the actual file, not the extension alone.</p>
<pre><code class="language-js">import { WebGPURenderer } from 'three/webgpu';
import { SPZLoader } from 'three/addons/loaders/SPZLoader.js';
import { GaussianSplat } from 'three/addons/objects/GaussianSplat.js';

const renderer = new WebGPURenderer({ forceWebGL: false });
await renderer.init();
const geometry = await new SPZLoader().loadAsync('/scene.spz');
scene.add(new GaussianSplat(geometry));
await renderer.compileAsync(scene, camera);
renderer.render(scene, camera);
</code></pre>
<p>For Spark, <code>await splat.initialized</code> alone was insufficient to prove that the first submitted frame contained splats. An initial frame reported only the mesh's 6,056 triangles. I corrected readiness to await <code>spark.update({ scene, camera })</code> after attaching the splat and updating scene matrices, then confirmed 2,103,208 submitted triangles. That guard prevents an empty-scene frame from looking like a fast successful load.<sup><a id="footnote-ref-3" href="#footnote-3" data-footnote-ref aria-describedby="footnote-label">3</a></sup></p>
<pre><code class="language-js">const spark = new SparkRenderer({ renderer }); // classic WebGLRenderer
scene.add(spark);
const splat = new SplatMesh({ url: '/scene.spz', lod: false });
await splat.initialized;
scene.add(splat);
scene.updateMatrixWorld(true);
await spark.update({ scene, camera });
renderer.render(scene, camera);
</code></pre>
<p>With WebGPURenderer, Spark's constructor instead threw <code>TypeError: this.renderer.getContext(...).getExtension is not a function</code>. This is a version-specific integration failure, not a prediction about future Spark support. The earlier <a href="https://digitalgarden.si/blog/spark-vs-gaussiansplats3d-threejs/">Spark, GaussianSplats3D and native comparison</a> used Spark 2.3.1 and a different synthetic SPZ; its format failure does not override this successful 2.4.0 PINOC-file test.</p>
<h2>Local timings, with the display ceiling kept visible</h2>
<p>The desktop browser reported Chromium 155 on Windows and an NVIDIA RTX 5070 Ti. The canvas was 1280 × 720 at renderer DPR 1, with antialiasing disabled. Each working path used five unique loopback URLs for load measurements, one discarded five-second warm-up and three visible 30-second camera-path trials. The renderer stayed initialized between loads. These are warm local asset loads, not cold-device startup or public CDN download times.</p>
<table>
<thead>
<tr>
<th>Path</th>
<th>Median local load</th>
<th>Median mean FPS</th>
<th>Median 1% low FPS</th>
<th>Median CPU render call</th>
</tr>
</thead>
<tbody><tr>
<td>Native WebGPU</td>
<td>400.10 ms</td>
<td>59.99</td>
<td>59.50</td>
<td>0.215 ms</td>
</tr>
<tr>
<td>Native WebGL2 fallback</td>
<td>266.53 ms</td>
<td>59.99</td>
<td>59.60</td>
<td>0.155 ms</td>
</tr>
<tr>
<td>Spark WebGL</td>
<td>299.96 ms</td>
<td>59.99</td>
<td>59.50</td>
<td>0.130 ms</td>
</tr>
</tbody></table>
<p>Load time includes decoding/readiness, compilation where available, sorting readiness for Spark, and submitted frames. It does not wait for a GPU-completion timestamp. CPU render-call duration excludes asynchronous worker work and is not total application CPU cost. The 1% low is the reciprocal of the mean of the slowest one percent of frame intervals; table values are medians across the three trials.</p>
<p>All three paths reached this setup's roughly 60 Hz presentation ceiling. That establishes smooth operation in this bounded test, not equal maximum throughput or a universal fastest renderer. The workstation was not a dedicated thermal or power-controlled laboratory. No physical iPhone, Android phone, mobile memory limit or mobile FPS was measured.</p>
<h2>Check mesh occlusion before calling it a mixed scene</h2>
<p>Three fixed states put the opaque plant in front, behind and through the splat scene. In the inspected frames, the foreground plant remained visible, the rear plant was hidden, and the enlarged intersecting plant showed partial occlusion. This is a useful opaque-depth check; it does not prove globally correct ordering for translucent petals or every view in a large scene.</p>
<p><img src="https://digitalgarden.si/marketing/research/pinoc-20261021/native-straddle.png" alt="An enlarged opaque plant partly occluded by the generated scene in native Three.js" /></p>
<p>Spark and native Three.js produced visibly different brightness under their tested defaults. I did not normalize their colour pipelines or score one as higher quality. These representative views do not certify complete geometry from every camera position. It is a captured-looking spatial backdrop, not a replacement for individually authored, deformable flower meshes.</p>
<h2>Reproduce the integration without redistributing the generated world</h2>
<p>The <a href="https://digitalgarden.si/marketing/research/pinoc-20261021/pinoc-integration-tests.zip">test package</a> contains the pinned viewer, owned mesh fixture, prompt, header metadata and all accepted load/frame receipts, including the WebGPU failure. Supply your own exported file as <code>public/pinoc-garden.spz</code>; the vendor-generated world is not included. Screenshots document the tested result. Dependencies and the owned fixture have their own notices in the archive.</p>
<p>For this garden, I would test a generated splat as a bounded background around a small authored interaction area. Editable plants, wind, per-flower interaction and reusable geometry remain mesh work. Native loading fits the current node-renderer stack; Spark is a separate classic-WebGL integration when the required format and renderer pass together. Continue with <a href="https://digitalgarden.si/blog/gaussian-splats-threejs/">the Gaussian asset workflow</a> or <a href="https://digitalgarden.si/blog/webgpu-garden-houdini-threejs/">the Houdini-to-Three.js garden pipeline</a>.</p>
<hr />
<p><a href="https://digitalgarden.si/blog/viggle-jst2-spz-threejs-garden/">Read the original article, supporting data and updates</a>.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>Viggle, <a href="https://viggle.ai/jst2">JST-2 and PINOC</a>, checked 21 October 2026. Product claims are distinguished from this browser-session measurement. <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩︎</a></p>
</li>
<li id="footnote-2">
<p>Three.js, <a href="https://threejs.org/docs/pages/SPZLoader.html">SPZLoader documentation</a>; installed and tested Three.js 0.186.1. <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩︎</a></p>
</li>
<li id="footnote-3">
<p>Spark, <a href="https://sparkjs.dev/docs/spark-renderer/">SparkRenderer documentation</a>, checked 21 October 2026; tested package 2.4.0 and awaited scene/camera sorting update. <a href="#footnote-ref-3" data-footnote-backref aria-label="Back to reference 3">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded></item><item><title><![CDATA[Rendering an Interactive 3D Garden in Three.js: WebGPU, Plants, Formats and Splats]]></title><description><![CDATA[A guide to the tested Three.js notes behind Alicia's interactive 3D garden: renderer choice, instanced plants, glTF and USD delivery, splats and AI me]]></description><link>https://digitalgarden-alicia.hashnode.dev/rendering-an-interactive-3d-garden-in-three-js-webgpu-plants-formats-and-splats</link><guid isPermaLink="true">https://digitalgarden-alicia.hashnode.dev/rendering-an-interactive-3d-garden-in-three-js-webgpu-plants-formats-and-splats</guid><dc:creator><![CDATA[Alicia lu]]></dc:creator><pubDate>Sun, 25 Oct 2026 06:44:58 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>AI-assisted disclosure: Codex assisted with research, code and writing. Measurements, failure cases and limitations are retained from the original article.</p>
</blockquote>
<blockquote>
<p>Originally published by Alicia on 2026-10-21: <a href="https://digitalgarden.si/blog/threejs-3d-garden-rendering/">Digital Garden by Alicia</a>.</p>
</blockquote>
<p>This guide collects the notes behind the <a href="https://digitalgarden.si/play/">interactive 3D garden</a> at digitalgarden.si. The hands-on notes record experiments with pinned versions, owned fixtures and explicit limits. The Gemini note is a separate community case summary. Read them in the order below if you are building a plant scene in Three.js, or jump to the question you have now.</p>
<p>The notes and the open benchmark dataset come from one desktop setup; they are case studies, not general performance claims. Where a result depends on a fixture, a renderer path or a file encoding, the linked page says so.</p>
<h2>Renderer and performance</h2>
<p>Start with <a href="https://digitalgarden.si/blog/webgpu-garden-houdini-threejs/">how the interactive 3D garden is built</a>. It follows the production garden from Houdini assets to Three.js, with the current code tested on both WebGPU and its WebGL2 fallback, and explains where the scene budget goes.</p>
<p>When the question is <a href="https://digitalgarden.si/blog/threejs-webgpu-vs-webgl-instanced-plants/">which renderer and submission path to choose for plants</a>, read the controlled 1,000 plant fixture next. It separates the submitted representation, such as separate meshes, InstancedMesh or BatchedMesh, from the API name, and compares CPU render time with the actual draw counters. The first note is a project record; the second is a controlled measurement.</p>
<p>For larger scenes, the <a href="https://digitalgarden.si/benchmarks/threejs-webgpu-vs-webgl-instancing/">open plant instancing data at 1k, 10k and 100k</a> is a separate v1 dataset: 240 attempts across 48 cells, three renderer paths, static and moving cameras, and per-run CSV and JSON downloads. It uses a longer protocol than the 1,000 plant note, so the two sets of numbers are not pooled. No physical phone was measured.</p>
<h2>Plant assets: procedural, optimized and instanced</h2>
<p>For <a href="https://digitalgarden.si/blog/threejs-scatter-objects-uneven-terrain/">scattering objects on uneven terrain without floating or clumping</a>, a separate 70-trial fixture compares plain and BVH rays, weighted surface sampling, hard spacing and native Houdini layouts. Placement accuracy, distribution and render pacing are kept separate.</p>
<p>A plant for the web needs repeatable geometry and data that survives export. The note on <a href="https://digitalgarden.si/blog/houdini-procedural-plants-realtime-web/">authoring wind-ready plant geometry</a> builds a small seeded Houdini plant with two levels of detail, custom wind attributes and alpha tested leaf cards.</p>
<p><a href="https://digitalgarden.si/blog/houdini-gltf-threejs-optimization/">Compressing and instancing plant GLBs</a> measures Draco, Meshopt and KTX2 on that kind of asset, and checks whether the attributes needed by wind survive compression before repeating the plant a thousand times.</p>
<p>Generated meshes need the same scrutiny. <a href="https://digitalgarden.si/blog/meshy-3d-agent-houdini-cleanup/">Making a generated mesh safe to instance</a> takes a real Meshy 6 Lite download through topology checks and a Houdini reduction before it is placed in the browser.</p>
<h2>Delivery formats: glTF and OpenUSD</h2>
<p>For <a href="https://digitalgarden.si/blog/houdini-usd-vs-gltf-threejs/">choosing glTF or USD for browser delivery</a>, the same two-plant pair was exported from Houdini as GLB, USDC and USDZ, then loaded in Three.js. The note records which representation kept both plants, their colors and their wind data.</p>
<p><a href="https://digitalgarden.si/blog/openusd-threejs-usd-web-viewer/">Loading a Solaris stage in the browser</a> tests a Houdini Solaris scene in a USD web viewer, with materials, instances, variants, units and warnings. If USD composition is new to you, the <a href="https://digitalgarden.si/blog/openusd-pipeline-beginners/">first OpenUSD stage for artists</a> explains references, payloads and variants with downloadable files.</p>
<h2>Follow-up delivery checks</h2>
<p>Three follow-up tests cover different parts of the pipeline. <a href="https://digitalgarden.si/blog/mutracer-houdini-plant-path-tracing/">MuTracer with a Houdini plant</a> checks colors, alpha, instances, denoising downloads and fixed-sample convergence. <a href="https://digitalgarden.si/blog/threejs-glb-loading-cache-first-frame/">GLB cache and first-frame measurements</a> publish twenty controlled loading rows. <a href="https://digitalgarden.si/blog/houdini-vs-geometry-nodes-plant-export/">Houdini and Geometry Nodes plant export</a> compares editable native files, triangle correspondence and retained wind attributes, including two rejected export cases.</p>
<h2>Gaussian splats</h2>
<p>Splats are not part of the production garden. To see <a href="https://digitalgarden.si/blog/gaussian-splats-threejs/">where splats fit next to mesh plants</a>, read the native Three.js test with an owned synthetic fixture, opaque mesh references and the SPZ v4 correction.</p>
<p><a href="https://digitalgarden.si/blog/spark-vs-gaussiansplats3d-threejs/">Choosing a splat library for Three.js</a> then loads that same fixture through three integrations and records which renderer and encoding combinations actually drew.</p>
<p>For <a href="https://digitalgarden.si/blog/viggle-jst2-spz-threejs-garden/">a generated million-point garden in Three.js</a>, a free PINOC / JST-2 export in SPZ v4 is placed beside an opaque Houdini plant. It loaded in native Three.js on WebGPU and its WebGL2 fallback, and in Spark 2.4.0 with the classic renderer; Spark with WebGPURenderer failed to initialize. That input file and those package versions differ from the synthetic fixture above.</p>
<h2>Coding agents for rendering code</h2>
<p><a href="https://digitalgarden.si/blog/claude-code-vs-codex-threejs/">Handing a shader task to a coding agent</a> gives two coding tools the same Three.js plant wind and color-space task, keeps their outputs unchanged, and checks them independently on WebGPU and WebGL2. For further agent experiments, see the <a href="https://digitalgarden.si/blog/codex-machine-skill-threejs/">machine-skill pump simulations with Codex</a> and the <a href="https://digitalgarden.si/blog/gemini-4-argon-threejs/">community tests of Gemini 4 Argon</a>.</p>
<h2>How to use these notes</h2>
<p>The <a href="https://digitalgarden.si/blog/houdini-vs-code-procedural-threejs-slop-newyork/">slop-newyork and garden profile</a> compares full download budgets and three 1080p observations of two different WebGPU scenes. The <a href="https://digitalgarden.si/blog/simready-usd-to-glb-threejs-webgpu/">SimReady USD-to-GLB test</a> follows two NVIDIA assets through Blender, Houdini, usd2gltf and direct USD loading, including the failed paths and material-preservation limits.</p>
<p>The <a href="https://digitalgarden.si/blog/img2interior-codex-threejs-garden/">img2interior room test with Codex</a> follows a single synthetic reference through Blender authoring, PBR/runtime delivery, a static portable GLB and actual garden import. It records Windows fixes and the difference between a 2.67 MB model and its 95.10 MB required runtime payload.</p>
<p>The <a href="https://digitalgarden.si/blog/spirula-studio-garden-splat-threejs/">Spirula Studio to Spark sample test</a> adds a measured training-to-browser handoff: the official Mip-NeRF 360 Garden sample, supplied camera poses, Vulkan training, PLY export and SPZ conversion. It documents 100,000 splats and a mixed WebGL render while keeping phone capture, fresh SfM and mobile performance outside the tested scope.</p>
<p>Each note links its downloadable fixtures and machine-readable results where they exist. Rerun the experiment on your own hardware before relying on a number, and treat a screenshot as evidence of one renderer path, not of every device. Then <a href="https://digitalgarden.si/play/">walk through the garden</a> to see the production scene these tests support.</p>
<hr />
<p><a href="https://digitalgarden.si/blog/threejs-3d-garden-rendering/">Read the original article, supporting data and updates</a>.</p>
]]></content:encoded></item><item><title><![CDATA[I Tested img2interior with Codex: A Room and a 95 MB Runtime]]></title><description><![CDATA[A Windows Codex test of img2interior: editable Blender output, an 8.80 MB portable GLB, a 95.10 MB runtime, real cloth checks and a garden import.]]></description><link>https://digitalgarden-alicia.hashnode.dev/i-tested-img2interior-with-codex-a-room-and-a-95-mb-runtime</link><guid isPermaLink="true">https://digitalgarden-alicia.hashnode.dev/i-tested-img2interior-with-codex-a-room-and-a-95-mb-runtime</guid><dc:creator><![CDATA[Alicia lu]]></dc:creator><pubDate>Sun, 25 Oct 2026 06:44:07 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>AI-assisted disclosure: Codex assisted with research, code and writing. Measurements, failure cases and limitations are retained from the original article.</p>
</blockquote>
<blockquote>
<p>Originally published by Alicia on 2026-10-22: <a href="https://digitalgarden.si/blog/img2interior-codex-threejs-garden/">Digital Garden by Alicia</a>.</p>
</blockquote>
<p><strong>img2interior worked in this iterative Codex test, with local Windows fixes and a viewer lifecycle repair.</strong> It produced an editable Blender scene, a runtime GLB with a Three.js viewer, and a separate portable GLB. The room is a simplified interpretation of one synthetic reference, with no measured reconstruction claim.</p>
<p>The largest practical finding was the payload. The runtime GLB was <strong>2.67 MB</strong>, but its required lightmaps and reflection probe brought the declared total to <strong>95.10 MB</strong>. The standalone portable file was <strong>8.80 MB</strong>. The full desktop workflow passed after review and repair; no physical phone was tested.</p>
<p><img src="https://digitalgarden.si/marketing/research/img2interior-20261022/native.png" alt="Independent reopening and rendering of the final editable Blender room" /></p>
<p>This is an actual render from a fresh process opening the final master. The geometric artwork, uniform stone, simplified upholstery and schematic trees show where the reconstruction stops.</p>
<p><a href="https://digitalgarden.si/research/img2interior/">Inspect the static portable model</a>, <a href="https://digitalgarden.si/marketing/research/img2interior-20261022/room-study.zip">download the editable study</a>, or read the <a href="https://digitalgarden.si/marketing/research/img2interior-20261022/results.json">machine-readable desktop results</a>. The online preview is a separate static consumer; it does not reproduce the full PBR viewer's cloth simulation.</p>
<h2>One synthetic reference, a new scene builder</h2>
<p>The user selected the project's public <a href="https://img2threejs.io/living-room/assets/interior-modern-source.png">synthetic living-room reference</a>: 2752×1536 pixels, with full-height glazing, an ivory sofa, a pale round table, a dark chair and a walnut cabinet. It was not a greenhouse photograph. The reference remains linked at its source and is not redistributed in the download.</p>
<p>img2interior is an agent workflow for authoring a Blender scene, rather than a pretrained image-to-mesh model. In the current Codex desktop conversation, I wrote a scene-specific Python builder using the package's reusable geometry helpers. I did not call its bundled Oak/Linen room recipe or run Claude Code.</p>
<p>The room has actual shell and furniture geometry, glazing, curtains, UV-mapped surfaces and authored lighting: <strong>131 meshes, 46,544 triangles and 12 semantic components</strong>. I estimated dimensions in metres, using a roughly 3.1-metre sofa as an anchor. Hidden construction and the camera remain authored assumptions. Native authoring uses Z-up; the glTF exports use Y-up.</p>
<p>This was not a fresh-session, unchanged-prompt benchmark. I preserved failed renders, changed the builder and checked the results. The <a href="https://digitalgarden.si/blog/codex-machine-skill-threejs/">earlier Codex machine-skill test</a> follows the same distinction between using a skill and accepting its first output.</p>
<h2>Setup and the Windows interventions</h2>
<p>The test used img2interior <strong>0.3.1</strong>, img2 CLI <strong>0.4.0</strong>, Python <strong>3.11.14</strong>, bpy <strong>4.3.0</strong> and NumPy <strong>1.26.4</strong>. The workstation ran Windows 11, a Ryzen 9 9950X and an RTX 5070 Ti. The observed browser was Chromium 155 through ANGLE D3D11. Codex ran in the current desktop conversation; the CLI version readback was 0.144.1.</p>
<p>I registered the package in an isolated harness home and host fixture. Doctor returned zero failures and zero warnings; sync returned zero drift. That verifies the isolated fixture. It does not establish that a newly restarted user Codex host discovered the installed skill.</p>
<p>Three Windows issues needed attention:</p>
<ul>
<li>An inherited OCIO setting pointed Blender at an incompatible studio configuration. Clearing OCIO only in the authoring child process fixed preflight while preserving the workstation setting.</li>
<li>The exporter wrote a backslash in the source-image URL. The viewer rejected it. A local <code>Path.as_posix()</code> change fixed that field, with the original retained for rollback.</li>
<li>Python 3.12 reported different path-stat and file-stat ctime values, triggering the file-change audit. Running the unchanged checks with Python 3.11 passed. The delivery contract also needed an equivalent relative root because its drive path was classified as a URL.</li>
</ul>
<p>The downloadable study contains the builder, inputs, generated linen maps, packed master, portable model, applicable notices and the viewer lifecycle patch. Install the pinned official toolkit separately to rebuild. The archive excludes the reference image, environments, production garden plants and the large runtime sidecars.</p>
<h2>Render, reject, repair, then bake</h2>
<p>The first native render took <strong>5.44 seconds</strong>. I rejected it: the camera was too close, and the table and chair were clipped. Two repair rounds adjusted the framing, hidden front-room extent and furniture proportions. I inspected source, lateral, reverse and textile views before final baking.</p>
<p>Five full/ambient bake groups took <strong>193.17 seconds</strong>, using a base resolution of 1024 and 2048 for textiles and outdoor geometry, 32 samples and OpenImageDenoise. A 1024×512 reflection probe took another <strong>6.61 seconds</strong>. These are measured stages, not a complete setup or generation stopwatch.</p>
<p>The UV/color/coordinate round trip reported zero error. A separate matched 256×192 browser calibration fixture had a mean absolute RGB difference of <strong>0.003818</strong> on a 0–1 scale against the native expected render. That checks lightmap transport, not resemblance to the source sofa.</p>
<h2>Three deliverables with different contracts</h2>
<table>
<thead>
<tr>
<th>Product</th>
<th>File bytes</th>
<th>What it preserves</th>
</tr>
</thead>
<tbody><tr>
<td>Packed Blender master</td>
<td>1,037,640</td>
<td>Editable geometry, cameras, lights and packed PBR source maps</td>
</tr>
<tr>
<td>Runtime GLB</td>
<td>2,669,308</td>
<td>Geometry and bindings for the full viewer, with required sidecars</td>
</tr>
<tr>
<td>Five EXRs plus reflection probe</td>
<td>92,429,949</td>
<td>Baked lighting and reflections required by that runtime</td>
</tr>
<tr>
<td>Runtime model plus those sidecars</td>
<td>95,099,257</td>
<td>Declared file total, before reference image and viewer code</td>
</tr>
<tr>
<td>Static portable GLB</td>
<td>8,804,144</td>
<td>Embedded baked appearance for independent glTF consumers</td>
</tr>
</tbody></table>
<p>These are decimal MB and file-byte totals, not a measured cold network transfer. The online static preview loads the unchanged portable GLB only after an explicit button press. It does not load the 95 MB runtime set.</p>
<p>Fresh Blender processes reopened the master and independently imported the portable model, then rendered each. Both final GLBs passed the Khronos validator with <strong>zero errors and zero warnings</strong>. The full viewer's actual downloads matched the final master and portable file hashes.</p>
<p><img src="https://digitalgarden.si/marketing/research/img2interior-20261022/portable.png" alt="Independent Blender import and render of the portable GLB, with softer static baked surface detail" /></p>
<p>The portable model embeds five static images and uses unlit materials for its baked appearance. Close surface detail is softer than the native/runtime PBR version. It does not carry the current browser fabric finish, live curtain state or dynamic relighting. The <a href="https://digitalgarden.si/blog/houdini-gltf-threejs-optimization/">GLB export notes</a> cover a separate optimization workflow; I did not run that pass here or invent before-and-after numbers.</p>
<h2>Loading it in the actual Digital Garden renderer</h2>
<p>I loaded the portable GLB through the existing Digital Garden Experience and real plant assets, using a local test entry, an inspection camera and derived placement. The garden used Three.js <strong>0.186.1</strong>; the original PBR room viewer used <strong>0.180.0</strong>. The room imported at scale 1, upright, without an extra rotation.</p>
<table>
<thead>
<tr>
<th>Path</th>
<th>Observed conditions</th>
<th>Result</th>
</tr>
</thead>
<tbody><tr>
<td>Full WebGL2 room viewer</td>
<td>1340×748 drawing buffer, wind enabled, preset views</td>
<td>601 rendered frames in 10.0066 s, about 60.06 FPS</td>
</tr>
<tr>
<td>Garden WebGPU</td>
<td>1265×540, DPR 1, low quality, spring/day, 162,420 plants</td>
<td>600 intervals over 10 s, about 59.99 FPS</td>
</tr>
<tr>
<td>Garden WebGL2 fallback</td>
<td>Same scene and dimensions</td>
<td>About 59.99 FPS; median 16.7 ms, p95 16.8 ms</td>
</tr>
</tbody></table>
<p>Both garden paths hit the display's roughly 60 Hz ceiling. This does not show which backend is faster. The sampled garden view reported 175 draw calls and about 12.15 million rendered triangles, including rendering passes. For a controlled submission comparison, use the separate <a href="https://digitalgarden.si/benchmarks/threejs-webgpu-vs-webgl-instancing/">plant instancing benchmark</a>.</p>
<p><img src="https://digitalgarden.si/marketing/research/img2interior-20261022/garden.png" alt="Actual local garden integration: existing plants visibly intersect the new room" /></p>
<p>The import exposed a placement problem: existing plants intersected the room. I preserved the scatter. A production installation needs a reserved footprint. The live garden has not been changed to include this experimental room. Its portable baked light also cannot follow the garden's sun.</p>
<h2>Cloth, hems and the lifecycle bug</h2>
<p>The desktop follow-up tested low, middle and high fabric points and the lower hem on both panels: eight bidirectional drag/release cases, plus two pinned upper-hem checks. The ray-hit data actually identified <code>part=hem</code>; cloth hits were not relabelled as hem results. The far lower hem was initially obscured by the sofa, so I navigated into a clear viewing position instead of weakening occlusion checks.</p>
<p>The <a href="https://digitalgarden.si/marketing/research/img2interior-20261022/hem.png">saved hem-grab capture</a> shows the full cloth viewer during the test. The online portable preview has static curtains.</p>
<p>A separate vertical pull kept rail coverage at zero while moving the middle probe <strong>76.37 mm</strong>, a higher probe <strong>5.09 mm</strong>, and the pinned top <strong>0 mm</strong>. The maximum observed movable held-point error was <strong>3.86 mm</strong>, below the stated 5 mm test threshold. Camera transforms/projection and UV0/UV1 fingerprints stayed unchanged. Rendered world vertices matched the solver within Float32 precision.</p>
<p>Native pointer-capture loss, release outside the canvas, trusted browser blur, Source-mode changes, real offscreen pause, wind-off sleep, page exit and return all passed. Real navigation produced <code>pagehide</code> and hidden events and removed the old canvas. Isolated hidden-pause used a restored temporary getter/event because inactive tabs in the in-app browser stayed visible. Extra-touch/cancellation handlers used DOM PointerEvents. Neither simulation is physical-phone evidence.</p>
<p>The tests found a real lifecycle bug: <strong>a late context-loss event on a detached old canvas could put its replacement viewer into an error state</strong>. I removed the old keyboard/context listeners during disposal and guarded the context handler by session ownership. Dispatching the same old-canvas events after repair left the new viewer ready and its camera unchanged.</p>
<p>Actual WebGL loss, a blocked manifest request and a blocked atlas request each recovered through Retry load. The eight review stages and all three blocking gates passed after repair. The gates check ordering and bytes; the browser observations establish the scoped behavior.</p>
<h2>What I would use it for</h2>
<p>The editable project is useful as a starting point for an approximate room, especially when I want a builder and Blender scene to continue editing. Review and repair remain part of the work. The geometric artwork, upholstery, wood, stone, outdoor trees and lighting are visibly simplified.</p>
<p>Before using this room as a production garden asset, I would improve the reconstruction, reserve its footprint, reduce the runtime payload and complete real-device checks. The 390×844 layout emulation had no horizontal overflow, but <strong>no physical phone was connected</strong>. There is also no complete setup timer or per-run token/cost baseline.</p>
<h3>Does img2interior work with Codex?</h3>
<p>It did in this Windows test, using a new builder, render iterations, local compatibility changes and a viewer repair. This does not establish untouched first-pass reliability across other images or fresh host installations.</p>
<h3>Which GLB should I put in another Three.js scene?</h3>
<p>The portable file is the self-contained static-appearance export. The runtime GLB requires its declared lightmaps, probe and viewer integration. The packed Blender master is the editing source.</p>
<h3>Is it open source?</h3>
<p>The tested package is source-available under the <a href="https://github.com/img2threejs/img2interior/blob/main/LICENSE">img2 Interior Free Use, Restricted Commercialization License 1.0</a>, not an OSI open-source license. Read its output/runtime and third-party terms for your use. This test does not relicense the reference image or toolkit.</p>
<p>Related: <a href="https://digitalgarden.si/blog/threejs-3d-garden-rendering/">Three.js garden rendering guide</a>, <a href="https://digitalgarden.si/blog/codex-machine-skill-threejs/">Codex machine-skill test</a> and the <a href="https://github.com/img2threejs/img2interior">official img2interior project</a>.</p>
<hr />
<p><a href="https://digitalgarden.si/blog/img2interior-codex-threejs-garden/">Read the original article, supporting data and updates</a>.</p>
]]></content:encoded></item></channel></rss>