<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alicia lu</title>
    <description>The latest articles on DEV Community by Alicia lu (@alicia_lu).</description>
    <link>https://dev.to/alicia_lu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4195263%2F443b0bb4-9523-4dd3-a9f5-3b6ba40156ee.png</url>
      <title>DEV Community: Alicia lu</title>
      <link>https://dev.to/alicia_lu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alicia_lu"/>
    <language>en</language>
    <item>
      <title>What broke when I carried bubble shapes through FLIP particles</title>
      <dc:creator>Alicia lu</dc:creator>
      <pubDate>Sat, 14 Nov 2026 05:58:54 +0000</pubDate>
      <link>https://dev.to/alicia_lu/what-broke-when-i-carried-bubble-shapes-through-flip-particles-ci</link>
      <guid>https://dev.to/alicia_lu/what-broke-when-i-carried-bubble-shapes-through-flip-particles-ci</guid>
      <description>&lt;p&gt;Read the &lt;a href="https://digitalgarden.si/blog/houdini-flip-deforming-bubbles/" rel="noopener noreferrer"&gt;complete experiment, code and evidence on Digital Garden&lt;/a&gt;. This is a short companion note; the original article contains the full method and measurements.&lt;/p&gt;

&lt;p&gt;A bubble-shaped particle selection seemed like a manageable exercise after studying Entagma's viscous-fluid tutorial. I built a separate 48-frame pour in Houdini 22.0.429 and compared five ways to carry its distance attribute through the cache.&lt;/p&gt;

&lt;p&gt;Three checks changed the implementation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On frames with no new centers, my distance-node configuration returned -1. Without a guard, the next threshold test selected the whole fluid.&lt;/li&gt;
&lt;li&gt;Stable IDs preserved identity when point order changed. Neighbor repair needed its own second Wrangle to read the completed inheritance result.&lt;/li&gt;
&lt;li&gt;Surfaced cavity shapes needed clipping against an inset liquid SDF. The containment checks covered 53 frame/radius outputs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frjujc4h8pw1qau6k3xoj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frjujc4h8pw1qau6k3xoj.png" alt="Native FLIP cavity meshes shown as an explanatory cutaway" width="799" height="277"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The limits are useful too. This is cached-material post-processing, not a second air-phase simulation. The larger-radius final mesh retained two local face-direction exceptions; the tests do not establish production-ready glass rendering or continuous collision freedom. The explicit CUDA probe fell back to CPU, so formal neural surfacing used DirectML.&lt;/p&gt;

&lt;p&gt;The original write-up includes all five comparisons, the two-pass VEX, discrete mesh checks, an actual Solver SOP comparison, figures, CSV and a small source package. Codex executed the Houdini tests and assisted with numerical checks and writing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://digitalgarden.si/blog/houdini-flip-deforming-bubbles/" rel="noopener noreferrer"&gt;Continue with the full Houdini FLIP bubble study on the original website&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>software</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Idol to Three.js: notes from a local garden-object experiment</title>
      <dc:creator>Alicia lu</dc:creator>
      <pubDate>Sat, 14 Nov 2026 04:19:15 +0000</pubDate>
      <link>https://dev.to/alicia_lu/image-to-gaussian-splat-on-a-16gb-gpu-idol-and-threejs-tested-2004</link>
      <guid>https://dev.to/alicia_lu/image-to-gaussian-splat-on-a-16gb-gpu-idol-and-threejs-tested-2004</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This is a short companion to my &lt;a href="https://digitalgarden.si/blog/image-to-gaussian-splat-threejs/" rel="noopener noreferrer"&gt;original Idol desktop study on Digital Garden&lt;/a&gt;, published on 13 November 2026. The complete measurements, code, figures and reproduction files are available there.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I wanted to know whether an object generated from one owned garden image could survive a local browser loader. The practical check was not just whether Idol generated a PLY, but whether the file displayed with the expected orientation and color in Three.js and Spark.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few observations
&lt;/h2&gt;

&lt;p&gt;The study ran on an RTX 5070 Ti with Windows as the host and CUDA through WSL2. Fourteen generation jobs completed using two owned Blender renders: a potted plant and a watering can. Each condition was one observation at seed 42, so this is a small desktop experiment rather than a broad model benchmark.&lt;/p&gt;

&lt;p&gt;At the default 50-step, 262,144-Gaussian setting, the first pipeline call with new compile caches took &lt;strong&gt;47.68 seconds&lt;/strong&gt;. The same input in the same process then took &lt;strong&gt;29.52 seconds&lt;/strong&gt;. Initial model loading and disk saving were recorded separately. The resulting uncompressed PLY was &lt;strong&gt;17,826,208 bytes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbkrvfbpb5h9dgnmf54yc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbkrvfbpb5h9dgnmf54yc.png" alt="Actual browser captures of the owned plant generated by Idol, viewed from four headings" width="800" height="492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The browser integration mattered
&lt;/h2&gt;

&lt;p&gt;The isolated viewer used Three.js 0.186.1 and Spark 2.3.1. The exported basis initially placed the plant upside down; an X rotation of pi corrected its placement. The native Three.js path also needed explicit SH0 sRGB decoding into the linear working space. These were viewer adaptations, and the generated PLY files were kept unchanged.&lt;/p&gt;

&lt;p&gt;Nineteen final browser loads completed across WebGPU, the WebGL2 fallback and Spark, including the million-point file. Timing ended at the first submitted draw. It &lt;strong&gt;does not measure the first visible pixel or establish a renderer-speed ranking&lt;/strong&gt;; Spark's worker may still be preparing visible data after submission.&lt;/p&gt;

&lt;p&gt;Windows adapter counters included shared memory and unrelated desktop activity, so completion is not proof of zero memory spill. Physical iPhone Safari, public-network 3D delivery and production-garden placement remain untested.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the full original
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://digitalgarden.si/blog/image-to-gaussian-splat-threejs/" rel="noopener noreferrer"&gt;full Digital Garden article&lt;/a&gt; contains all five tables, four original figures, the encoder-source checks, native API examples, exact memory distinctions and an 18.7 MB reproduction bundle with raw records and two representative PLY exports. The article also explains the setup failures and limits behind these observations.&lt;/p&gt;

&lt;p&gt;Alicia — 3D technical artist, TA and independent developer. Codex assisted the tests, numerical checks and writing. Owned text and figures retain CC BY 4.0; the linked bundle carries its own code and upstream notices.&lt;/p&gt;

</description>
      <category>threejs</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Blender 5.3 Gaussian Splats: Import, Crop and Export Tested</title>
      <dc:creator>Alicia lu</dc:creator>
      <pubDate>Thu, 12 Nov 2026 03:54:37 +0000</pubDate>
      <link>https://dev.to/alicia_lu/blender-53-gaussian-splats-import-crop-and-export-tested-3ink</link>
      <guid>https://dev.to/alicia_lu/blender-53-gaussian-splats-import-crop-and-export-tested-3ink</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published by Alicia on 12 November 2026 at &lt;a href="https://digitalgarden.si/blog/blender-5-3-gaussian-splats-threejs/" rel="noopener noreferrer"&gt;Digital Garden&lt;/a&gt;. Alicia is a 3D technical artist (TA) and independent developer. Codex assisted with the desktop tests, numerical checks and writing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Blender 5.3.0 imported the same 12,000-splat plant through PLY, SPZ, glTF and USD. A Geometry Nodes crop preserved its surviving attributes. None of the native exports tested here returned a complete web-readable splat.&lt;/strong&gt; File creation, point preservation and splat preservation were three different results.&lt;/p&gt;

&lt;p&gt;Codex ran this desktop study under Alicia's direction on 12 November 2026. Alicia is a 3D technical artist (TA) and independent developer. The fixture is the owned procedural plant from our &lt;a href="https://digitalgarden.si/blog/houdini-22-gaussian-splats-threejs/" rel="noopener noreferrer"&gt;Houdini 22 GSplats study&lt;/a&gt;, trained previously from 24 Karma views for 1,200 steps. It is a synthetic plant fit, not a photographic scan, a newly trained Blender model or the live Digital Garden scene.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkhwt75empbixnb9bwiby.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkhwt75empbixnb9bwiby.png" alt="The owned 12,000-splat plant rendered in Blender 5.3 Cycles before cropping, with an orange flower and green leaves" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Blender officially promises
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://developer.blender.org/docs/release_notes/5.3/rendering/" rel="noopener noreferrer"&gt;5.3 rendering notes&lt;/a&gt; explicitly say: “There is currently no export support.” They describe native PLY/SPZ import, a Gaussian-splat point-cloud type, and limitations around color and applying transforms. The &lt;a href="https://developer.blender.org/docs/release_notes/5.3/pipeline_io/" rel="noopener noreferrer"&gt;pipeline and I/O notes&lt;/a&gt; separately document glTF/USD splat import and generic point-cloud improvements. Generic point-cloud export is not a promise to preserve a Gaussian-splat field.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://code.blender.org/2026/10/splatting-the-gaussians-into-blender/" rel="noopener noreferrer"&gt;Blender developer article&lt;/a&gt; explains the different rendering approaches and rudimentary relighting. We checked these claims against the installed release, rather than treating release footage or a successful export notification as a round trip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Machine, source and measurement boundary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Recorded setup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Blender&lt;/td&gt;
&lt;td&gt;5.3.0 Release, build &lt;code&gt;02f0899d78e3&lt;/code&gt;, built 10 November 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workstation&lt;/td&gt;
&lt;td&gt;Windows 11, Ryzen 9 9950X, RTX 5070 Ti, driver 621.14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU memory&lt;/td&gt;
&lt;td&gt;Adapter reports 16,303 MiB total; one pre-test snapshot used 2,607 MiB; no per-import allocation or VRAM peak measurement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native images&lt;/td&gt;
&lt;td&gt;800 × 600, fixed orthographic camera; source/crop share a camera; transform pair uses a separate shared camera&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native color setup&lt;/td&gt;
&lt;td&gt;Process-scoped ACES 2.0 OCIO config, sRGB display, Un-tone-mapped view, no look, exposure 0, gamma 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser lab&lt;/td&gt;
&lt;td&gt;Desktop Chrome 157, headed, 800 × 600 canvas, DPR 1; Three.js 0.186.1; Spark 2.3.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source properties&lt;/td&gt;
&lt;td&gt;12,000 splats; degree-three SH storage, with degree-one training active in the historical fit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The original PLY is &lt;strong&gt;3,121,605 bytes&lt;/strong&gt;, its lossy SPZ v3 derivative &lt;strong&gt;235,748 bytes&lt;/strong&gt;, and the native USD &lt;strong&gt;3,121,407 bytes&lt;/strong&gt;. Their hashes accompany the evidence. A local glTF repack uses the same PLY records, linear scale, normalized opacity, xyzw quaternions and the &lt;a href="https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_gaussian_splatting" rel="noopener noreferrer"&gt;KHR_gaussian_splatting&lt;/a&gt; attribute schema, including &lt;code&gt;kernel: ellipse&lt;/code&gt; and &lt;code&gt;colorSpace: srgb_rec709_display&lt;/code&gt;. That repack tests import; it is not a Blender export.&lt;/p&gt;

&lt;p&gt;The historical Karma color pipeline and Blender's documented sRGB assumption differ. The glTF color-space field is an explicit structural-test declaration; no original Karma-to-sRGB conversion was validated. These pictures establish visible output and structural behavior, not colorimetric equivalence across native and browser renderers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four imports, one source
&lt;/h2&gt;

&lt;p&gt;All four paths created &lt;code&gt;POINTCLOUD&lt;/code&gt; objects with type &lt;code&gt;GAUSSIAN_SPLAT&lt;/code&gt;, 12,000 points, &lt;code&gt;scale&lt;/code&gt;, quaternion &lt;code&gt;rotation&lt;/code&gt;, four-component &lt;code&gt;radiance:base&lt;/code&gt;, and 15 &lt;code&gt;radiance:sh_N&lt;/code&gt; vectors.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Single operator duration&lt;/th&gt;
&lt;th&gt;Orientation used for this source&lt;/th&gt;
&lt;th&gt;Other observation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standard 3DGS PLY&lt;/td&gt;
&lt;td&gt;2.79 ms&lt;/td&gt;
&lt;td&gt;Imported identity; rotate object X +90° for Blender Z-up inspection&lt;/td&gt;
&lt;td&gt;Additional source normals/albedo scalar attributes retained&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPZ v3&lt;/td&gt;
&lt;td&gt;4.13 ms&lt;/td&gt;
&lt;td&gt;Importer already changed this file's coordinates to Z-up&lt;/td&gt;
&lt;td&gt;Anti-aliasing support warning in the native log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USD particle field&lt;/td&gt;
&lt;td&gt;33.44 ms&lt;/td&gt;
&lt;td&gt;Importer supplied X +90° object rotation&lt;/td&gt;
&lt;td&gt;Native splat type detected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;glTF repack&lt;/td&gt;
&lt;td&gt;43.55 ms&lt;/td&gt;
&lt;td&gt;Importer converted glTF Y-up data&lt;/td&gt;
&lt;td&gt;Native splat type detected&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are one operator call each in already running background processes. They exclude process startup, interface painting and viewport readiness. They are diagnostics, not a format speed ranking. SPZ versions other than this v3 derivative were not tested. An axis correction for this writer is not a universal SPZ rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cropping works; inspect evaluated geometry
&lt;/h2&gt;

&lt;p&gt;On a duplicate of the PLY object, a Geometry Nodes group connects &lt;strong&gt;Position → Separate XYZ → Compare (X &amp;gt; 0) → Delete Geometry&lt;/strong&gt;, on the point domain. It retains X ≤ 0 in source coordinates. A &lt;code&gt;source_id&lt;/code&gt; integer attribute lets the test match each survivor back to its exact input record.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Crop check&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Base datablock points&lt;/td&gt;
&lt;td&gt;12,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evaluated modifier output&lt;/td&gt;
&lt;td&gt;5,906&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Surviving source IDs match the intended selection&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maximum absolute difference, surviving position/scale/quaternion/base radiance&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maximum absolute difference, all 15 stored SH vectors&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fresh-process &lt;code&gt;.blend&lt;/code&gt; reopen&lt;/td&gt;
&lt;td&gt;Base 12,000 / evaluated 5,906&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Reading only &lt;code&gt;object.data&lt;/code&gt; would incorrectly report that the modifier had done nothing. The tested code obtains &lt;code&gt;object.evaluated_get(depsgraph).data&lt;/code&gt; before checking counts and attributes. Edit Mode deletion and more complex cleanup were not measured.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe5qshx5nkxtuhiee2hzl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe5qshx5nkxtuhiee2hzl.png" alt="The same fixed Blender camera after the X-positive crop, retaining 5,906 splats and removing much of the right side" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Applying transforms changed the appearance
&lt;/h2&gt;

&lt;p&gt;A separate duplicate had X +90° and Z +90° object rotations plus uniform scale 2. Before and after applying rotation and scale, the same camera viewed the same intended world-space arrangement.&lt;/p&gt;

&lt;p&gt;World-space point centers agreed within &lt;strong&gt;5.95 × 10⁻⁸&lt;/strong&gt; after application. However, the &lt;code&gt;scale&lt;/code&gt; and quaternion &lt;code&gt;rotation&lt;/code&gt; attribute arrays did not change at all. The first three SH vectors contained nonzero values before the operation and also remained unchanged. Point centers moved into the datablock while the stored splat shape and directional radiance did not follow the object transform.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu3dqmlvp73bqhkor6x9f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu3dqmlvp73bqhkor6x9f.png" alt="The transformed plant before Apply Transform, with continuous green leaves and an orange flower" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm829citf42ig4so2h5m2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm829citf42ig4so2h5m2.png" alt="The same transformed plant after applying rotation and scale, showing a visibly fragmented flower and leaves" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The paired renders visibly fragment after application. This reproduces the documented transform limitation for this fixture. Keep object transforms unapplied in this workflow; a general transformed-splat exporter would need to handle covariance and SH rotation correctly. Our reference crop writer does neither because it deliberately preserves the original coordinate system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rendering and a bounded relighting probe
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Image operation&lt;/th&gt;
&lt;th&gt;Settings&lt;/th&gt;
&lt;th&gt;One wall-clock duration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Original, Workbench&lt;/td&gt;
&lt;td&gt;Flat lighting&lt;/td&gt;
&lt;td&gt;0.83 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Original, EEVEE&lt;/td&gt;
&lt;td&gt;128 samples&lt;/td&gt;
&lt;td&gt;3.42 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Original, Cycles&lt;/td&gt;
&lt;td&gt;OptiX GPU, 64 samples, denoising off&lt;/td&gt;
&lt;td&gt;0.87 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cropped, Cycles&lt;/td&gt;
&lt;td&gt;Same 64 samples and source camera&lt;/td&gt;
&lt;td&gt;0.29 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Diffuse probe, EEVEE&lt;/td&gt;
&lt;td&gt;Gray Diffuse BSDF plus point light, 128 samples&lt;/td&gt;
&lt;td&gt;0.47 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Diffuse probe, Cycles&lt;/td&gt;
&lt;td&gt;Same material/light, 64 samples&lt;/td&gt;
&lt;td&gt;0.46 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These sequential single renders include different startup/cache costs and different rendering work. They do not establish an engine ranking or viewport FPS. The diffuse probe was a deliberately gray material replacement. It produced a lit grayscale plant, rather than recovering the learned plant's albedo. A retained copy and both probe images are in the package; baked illumination removal was not tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  Export matrix: success messages are insufficient
&lt;/h2&gt;

&lt;p&gt;Only the cropped object was selected. Generic attribute export was enabled where available. Every written artifact was independently reopened in a fresh empty Blender scene.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Native export&lt;/th&gt;
&lt;th&gt;Bytes written&lt;/th&gt;
&lt;th&gt;Reopen result&lt;/th&gt;
&lt;th&gt;Complete splat preserved?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PLY, evaluated modifier enabled&lt;/td&gt;
&lt;td&gt;156&lt;/td&gt;
&lt;td&gt;Header says &lt;code&gt;element vertex 0&lt;/code&gt;; native reimport fails&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLB, default &lt;code&gt;export_apply=false&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;144,664&lt;/td&gt;
&lt;td&gt;12,000 ordinary mesh vertices; crop and splat attributes absent&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLB, &lt;code&gt;export_apply=true&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No output&lt;/td&gt;
&lt;td&gt;Raises &lt;code&gt;Object does not have geometry data&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLB, modifier baked first, &lt;code&gt;export_apply=false&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;71,540&lt;/td&gt;
&lt;td&gt;5,906 ordinary mesh vertices; splat attributes absent&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USD&lt;/td&gt;
&lt;td&gt;2,666,502&lt;/td&gt;
&lt;td&gt;5,906-point &lt;code&gt;POINTS&lt;/code&gt; cloud; scale, rotation and SH retained; base radiance missing&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alembic&lt;/td&gt;
&lt;td&gt;736,250&lt;/td&gt;
&lt;td&gt;5,906-point &lt;code&gt;POINTS&lt;/code&gt; cloud; scale and renamed SH retained; rotation and base radiance missing&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPZ&lt;/td&gt;
&lt;td&gt;Not available&lt;/td&gt;
&lt;td&gt;No native SPZ export operator found&lt;/td&gt;
&lt;td&gt;Not tested through a native exporter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The USD and Alembic logs explicitly warn about unsupported &lt;code&gt;radiance:base&lt;/code&gt;. Their ordinary point-cloud type and missing data prevent calling this a splat round trip. Simply changing the point-cloud type back cannot recover a missing base radiance/opacity vector. The GLB default also illustrates why counting bytes or relying on a saved filename can conceal a modifier-evaluation problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Actual browser return path
&lt;/h2&gt;

&lt;p&gt;The local lab ran eight functional browser starts, one per backend/file pair. Native Three.js selected WebGPU; Spark used &lt;code&gt;WebGLRenderer&lt;/code&gt;. Each successful case settled for 45 frames before capture. There is no FPS benchmark here. Three's native SPZ path needed an X = π object rotation for this particular file; Spark used its own decoded convention.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Native Three.js GaussianSplat&lt;/th&gt;
&lt;th&gt;Spark 2.3.1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Original PLY&lt;/td&gt;
&lt;td&gt;Visible, 12,000 splats&lt;/td&gt;
&lt;td&gt;Visible, 12,000 splats&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Original SPZ v3&lt;/td&gt;
&lt;td&gt;Visible, 12,000 splats&lt;/td&gt;
&lt;td&gt;Visible, 12,000 splats&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reference source-record crop PLY&lt;/td&gt;
&lt;td&gt;Visible, 5,906 splats&lt;/td&gt;
&lt;td&gt;Visible, 5,906 splats&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blender's native crop PLY&lt;/td&gt;
&lt;td&gt;Rejected: required splat properties missing&lt;/td&gt;
&lt;td&gt;Rejected: &lt;code&gt;scale_0&lt;/code&gt; missing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;reference crop&lt;/strong&gt; is a separate diagnostic writer: it copies the original PLY records selected by the same source-X rule. The count and IDs match the evaluated Blender crop. It is &lt;strong&gt;not&lt;/strong&gt; a native Blender export, a general exporter for edited attributes, or evidence that transformed SH were handled correctly. It proves the viewer accepts a complete cropped splat when the original schema and records survive.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv4h5k4p2xx0k6ccrqqlw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv4h5k4p2xx0k6ccrqqlw.png" alt="The original SPZ plant visibly rendered by Spark 2.3.1 in the local desktop browser" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4pl89vkmdy1gnvfhf0q1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4pl89vkmdy1gnvfhf0q1.png" alt="The original PLY in native Three.js WebGPU, showing a visibly more sparse rendering in this setup" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The native Three.js image is visibly sparser than the Spark image. We retain that difference rather than claiming matched image quality. These two functional captures do not explain every renderer difference; our &lt;a href="https://digitalgarden.si/blog/threejs-gaussian-splat-renderer-comparison/" rel="noopener noreferrer"&gt;renderer comparison&lt;/a&gt; treats renderer behavior separately. glTF, USD and Alembic were not forced through PLY/SPZ-only loader entries. The exported GLBs have no Gaussian-splat extension, and these native/Spark lab paths supply no equivalent USD/Alembic splat import.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical workflow and reproduction
&lt;/h2&gt;

&lt;p&gt;Use native Blender import to inspect, combine and crop a splat, then preserve the &lt;code&gt;.blend&lt;/code&gt; as the editable result. Before selecting a delivery tool, ask whether it preserves the &lt;strong&gt;evaluated count, radiance/opacity, scale, rotation, SH convention and coordinate system&lt;/strong&gt;. Reopen the artifact and test it in the intended viewer. A third-party exporter is a separate pipeline with its own version, license and limitations; no PlayCanvas account upload was performed here.&lt;/p&gt;

&lt;p&gt;For a working web deliverable today, keep the source standard PLY/SPZ or use a separately verified export/conversion path such as our bounded &lt;a href="https://digitalgarden.si/blog/houdini-22-gaussian-splats-threejs/" rel="noopener noreferrer"&gt;Houdini attribute round trip&lt;/a&gt;. Compression and viewer choice remain separate decisions; see the &lt;a href="https://digitalgarden.si/blog/spz-vs-sog-vs-ply-gaussian-splat-formats/" rel="noopener noreferrer"&gt;PLY, SPZ and SOG study&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://digitalgarden.si/marketing/research/blender53-20261112/analysis.json" rel="noopener noreferrer"&gt;machine-readable summary&lt;/a&gt; and &lt;a href="https://digitalgarden.si/marketing/research/blender53-20261112/blender53-splat-reproduction.zip" rel="noopener noreferrer"&gt;reproduction package&lt;/a&gt; include owned inputs, the editable compressed &lt;code&gt;.blend&lt;/code&gt;, scripts, raw attribute arrays, native artifacts, screenshots and browser receipts. Source code is MIT; owned fixture data, text and captures are CC BY 4.0, Alicia / digitalgarden.si. Blender and browser/dependency binaries, user settings, account data and full copied vendor pages are excluded. Dependencies retain their upstream licenses.&lt;/p&gt;

&lt;p&gt;The first browser harness incorrectly attempted a WebGL &lt;code&gt;finish()&lt;/code&gt; call on the native WebGPU canvas context. An initial glTF repack also copied nonunit source quaternions; that invalid diagnostic input was excluded, then normalized and reimported for the reported glTF result. The maximum float32 unit-norm error was 1.79 × 10⁻⁷. Pilot receipts remain separate from the corrected results. Public HTTPS hosting, iPhone Safari, large photographic scans, native SPZ v4, generic edited-attribute export and viewport FPS remain untested. Codex assisted execution, numerical checks and writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does Blender 5.3 support Gaussian splatting without an add-on?
&lt;/h3&gt;

&lt;p&gt;Yes. This Blender 5.3.0 desktop test imported the same owned 12,000-splat fixture through PLY, SPZ v3, glTF and USD as GAUSSIAN_SPLAT point clouds. It did not train a new splat from photos.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I crop Gaussian splats in Blender 5.3?
&lt;/h3&gt;

&lt;p&gt;Yes in this fixture. A Geometry Nodes Delete Geometry selection removed positive-X points, leaving 5,906 evaluated splats. Surviving position, scale, rotation, base radiance and all 15 stored SH vectors matched their source records exactly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can Blender 5.3 export a standard Gaussian-splat PLY?
&lt;/h3&gt;

&lt;p&gt;The official release notes say there is currently no export support. Our native PLY exporter returned FINISHED but wrote a 156-byte file with zero vertices. This is one tested build and fixture, not a claim about every add-on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a successful GLB export preserve Gaussian splats?
&lt;/h3&gt;

&lt;p&gt;No in this test. The default export retained 12,000 ordinary vertices without the crop or splat attributes. Baking the crop first retained 5,906 ordinary vertices, still without splat attributes. The export_apply option raised a geometry error.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can Blender relight a splat like a scanned material?
&lt;/h3&gt;

&lt;p&gt;Only rudimentary relighting is documented. A gray Diffuse BSDF and point light produced a lit grayscale result here. That did not recover albedo or remove baked illumination from the learned radiance.&lt;/p&gt;

</description>
      <category>blender</category>
      <category>threejs</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Three.js vs Unreal Engine for Web 3D: Which Should You Ship in the Browser?</title>
      <dc:creator>Alicia lu</dc:creator>
      <pubDate>Wed, 11 Nov 2026 03:50:46 +0000</pubDate>
      <link>https://dev.to/alicia_lu/threejs-vs-unreal-engine-for-web-3d-which-should-you-ship-in-the-browser-47lc</link>
      <guid>https://dev.to/alicia_lu/threejs-vs-unreal-engine-for-web-3d-which-should-you-ship-in-the-browser-47lc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published by Alicia Lu on 11 November 2026 at &lt;a href="https://digitalgarden.si/blog/threejs-vs-unreal-engine-web/" rel="noopener noreferrer"&gt;Digital Garden&lt;/a&gt;. Alicia is a 3D technical artist (TA) and independent developer. Codex assisted with implementation, measurement and drafting.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Start with Three.js for a web-authored viewer or interactive page. Consider Unreal's experimental Wasm target when keeping an existing Unreal project is worth the larger client and Preview restrictions. Consider Pixel Streaming when the Unreal application should run on a server.&lt;/strong&gt; That is a product decision, rather than a universal performance ranking.&lt;/p&gt;

&lt;p&gt;This desktop study was run on 11 November 2026 by Codex under Alicia's direction. Alicia is a 3D technical artist (TA) and independent developer. It adds 48 measured Chrome page loads (24 fresh-context visits and 24 same-context reloads) to the &lt;a href="https://digitalgarden.si/blog/unreal-engine-wasm-webgpu/" rel="noopener noreferrer"&gt;previous Unreal Wasm build test&lt;/a&gt;, with matched hosting software, Brotli compression and explicit cache policies. Public HTTPS hosting and iPhone Safari remain outside this desktop study. Local bandwidth shaping is not a measurement of a public network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three browser delivery paths
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Where the 3D application runs&lt;/th&gt;
&lt;th&gt;What the browser receives&lt;/th&gt;
&lt;th&gt;Decision to make&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Three.js web client&lt;/td&gt;
&lt;td&gt;On the visitor's device&lt;/td&gt;
&lt;td&gt;JavaScript and scene assets&lt;/td&gt;
&lt;td&gt;Can the experience be authored and maintained as a website?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UE 5.9 Preview Wasm client&lt;/td&gt;
&lt;td&gt;On the visitor's device&lt;/td&gt;
&lt;td&gt;A packaged engine client, WebAssembly and content&lt;/td&gt;
&lt;td&gt;Is preserving the Unreal project worth the client download and experimental target?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unreal Pixel Streaming&lt;/td&gt;
&lt;td&gt;On a desktop or server running Unreal&lt;/td&gt;
&lt;td&gt;WebRTC frames and audio, with input returned to the application&lt;/td&gt;
&lt;td&gt;Is a streamed session acceptable, and can the service operate it?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This three-path frame is our decision aid. Epic documents the experimental &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/unreal-engine-5-9-release-notes" rel="noopener noreferrer"&gt;Wasm target in the 5.9 release notes&lt;/a&gt; and describes &lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/pixel-streaming-in-unreal-engine" rel="noopener noreferrer"&gt;Pixel Streaming&lt;/a&gt; as remotely rendered frames and audio delivered over WebRTC. Pixel Streaming latency, server costs and concurrent sessions were not measured in this study.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://forums.unrealengine.com/t/unreal-engine-5-9-megajam-preview/2852840" rel="noopener noreferrer"&gt;MegaJam Preview announcement&lt;/a&gt; calls the browser target experimental, identifies WebAssembly, WebGPU and the mobile renderer, and recommends Chrome. Its source-build/C++ guidance should be read alongside the known issues. Our earlier Windows Launcher result is evidence for one installed build, not a guarantee for every platform or project. Existing production projects were preserved separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the desktop test actually compares
&lt;/h2&gt;

&lt;p&gt;The machine was Windows with an NVIDIA RTX 5070 Ti; the browser was Chrome 157.0.8103.50, headless, at 1280 × 720 and device pixel ratio 1. The Unreal package was UE 5.9.0 MegaJam Preview CL 60351404, Shipping, using the forward WebGPU tier. The Three.js control used 0.186.1, reported revision 186, and actually selected its WebGPU backend.&lt;/p&gt;

&lt;p&gt;Both fixtures have a floor, two upright boxes, a rotating center cube and a fixed camera. The original fixture sources and UE Shipping package were reused unchanged. Materials, lighting, shaders and engine behavior differ. The screenshots show that difference; this is not matched visual quality or a garden-scale scene.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F88pqyuq3evpq3qei8kvr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F88pqyuq3evpq3qei8kvr.png" alt="UE Shipping four-box fixture rendered through WebGPU on the tested desktop." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqhbb3s8r90khritp9qmh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqhbb3s8r90khritp9qmh.png" alt="Three.js WebGPU control with the same box layout but different materials and lighting." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each stack ran three fresh browser contexts, each followed by a reload in the same context, for each of two network profiles and two cache policies: 48 measured page loads in total, using two Playwright browser launches overall, one per cache-policy dataset. Stack order alternated between pairs. Fresh-context means fresh browser storage, not a reboot, a cold GPU driver or a reset of every browser-process and OS cache.&lt;/p&gt;

&lt;p&gt;Both stacks used Epic's packaged local server, loopback HTTP, the same isolation headers and Brotli siblings. The second profile used its server-side 50 Mbit/s shared-bandwidth and 40 ms response-delay setting. Server-side shaping also covers the UE service worker's fetches; the profile still does not reproduce a VPS, TLS, DNS, routing or a mobile radio.&lt;/p&gt;

&lt;p&gt;For the primary table, versioned assets used the server's immutable-cache option and entry documents retained its no-cache policy. This is appropriate only for fixed, versioned test artifacts; a production site must change asset URLs when their content changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Desktop startup and transfer results
&lt;/h2&gt;

&lt;p&gt;Every cell in the primary startup table below is the median of three runs. Times are milliseconds from navigation start. Response-body bytes were summed from the server ledger after 120 sampled post-ready animation intervals and an observed zero-in-flight response count, before screenshots. They exclude HTTP headers, TLS and transport overhead. These are bytes for that observation window, not a folder size and not a count stopped at READY.&lt;/p&gt;

&lt;p&gt;The common hook records the first successful GPU queue submission after a configured canvas acquires a texture. It provides one instrumented API boundary across stacks; it does not establish GPU completion, display scanout or that the first submission contains the final scene. The separate readiness column ends at UE's page READY event or the Three.js control's first rendered submission. Those readiness signals do different work.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stack&lt;/th&gt;
&lt;th&gt;Network profile&lt;/th&gt;
&lt;th&gt;Visit&lt;/th&gt;
&lt;th&gt;First canvas submission, ms&lt;/th&gt;
&lt;th&gt;Stack readiness, ms&lt;/th&gt;
&lt;th&gt;Completed response bodies, B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Three.js r186&lt;/td&gt;
&lt;td&gt;Unshaped loopback&lt;/td&gt;
&lt;td&gt;Fresh context&lt;/td&gt;
&lt;td&gt;199.07&lt;/td&gt;
&lt;td&gt;199.08&lt;/td&gt;
&lt;td&gt;533,231&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Three.js r186&lt;/td&gt;
&lt;td&gt;Unshaped loopback&lt;/td&gt;
&lt;td&gt;Same-context reload&lt;/td&gt;
&lt;td&gt;95.13&lt;/td&gt;
&lt;td&gt;95.14&lt;/td&gt;
&lt;td&gt;124&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Three.js r186&lt;/td&gt;
&lt;td&gt;Local 50 Mbit/s + 40 ms&lt;/td&gt;
&lt;td&gt;Fresh context&lt;/td&gt;
&lt;td&gt;483.83&lt;/td&gt;
&lt;td&gt;483.84&lt;/td&gt;
&lt;td&gt;533,231&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Three.js r186&lt;/td&gt;
&lt;td&gt;Local 50 Mbit/s + 40 ms&lt;/td&gt;
&lt;td&gt;Same-context reload&lt;/td&gt;
&lt;td&gt;114.26&lt;/td&gt;
&lt;td&gt;114.27&lt;/td&gt;
&lt;td&gt;124&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UE 5.9 Wasm&lt;/td&gt;
&lt;td&gt;Unshaped loopback&lt;/td&gt;
&lt;td&gt;Fresh context&lt;/td&gt;
&lt;td&gt;2043.05&lt;/td&gt;
&lt;td&gt;2049.62&lt;/td&gt;
&lt;td&gt;50,111,928&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UE 5.9 Wasm&lt;/td&gt;
&lt;td&gt;Unshaped loopback&lt;/td&gt;
&lt;td&gt;Same-context reload&lt;/td&gt;
&lt;td&gt;1688.59&lt;/td&gt;
&lt;td&gt;1701.52&lt;/td&gt;
&lt;td&gt;40,838&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UE 5.9 Wasm&lt;/td&gt;
&lt;td&gt;Local 50 Mbit/s + 40 ms&lt;/td&gt;
&lt;td&gt;Fresh context&lt;/td&gt;
&lt;td&gt;15493.15&lt;/td&gt;
&lt;td&gt;15507.72&lt;/td&gt;
&lt;td&gt;57,537,511&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UE 5.9 Wasm&lt;/td&gt;
&lt;td&gt;Local 50 Mbit/s + 40 ms&lt;/td&gt;
&lt;td&gt;Same-context reload&lt;/td&gt;
&lt;td&gt;1607.65&lt;/td&gt;
&lt;td&gt;1612.54&lt;/td&gt;
&lt;td&gt;40,838&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The small Three.js control had the smaller transfer and earlier submission in these conditions. That observation supports a lightweight web-client option for this fixture. It does not give a speedup ratio for equally rendered scenes, predict the full garden, or settle the cost of porting an existing Unreal project.&lt;/p&gt;

&lt;p&gt;The UE cold transfer changed with the shaped profile even though the package was identical. On unshaped loopback it was 50,111,928 B. On the shaped profile it was 57,537,511 B. The shaped cold ledgers record 14 Range responses totaling 7,425,583 B alongside whole-file prefetch responses; the unshaped cold ledgers record no Range responses. Count the actual requests when estimating network cost. A Brotli file's size alone misses this observed overlap.&lt;/p&gt;

&lt;p&gt;The earlier build article reports its own before-READY byte count and an uncompressed Three.js control. Those historical figures remain unchanged. This table uses a different, explicitly defined observation window and compressed control, so it should not silently replace the earlier numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache policy is part of the comparison
&lt;/h2&gt;

&lt;p&gt;The server's default policy is no-cache and it provides no validators for these responses. In the default-policy control, the Three.js files were fetched again on reload. UE also used its own service-worker content cache. Enabling the same immutable option on both servers changed the warm byte totals:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stack&lt;/th&gt;
&lt;th&gt;Server policy&lt;/th&gt;
&lt;th&gt;Reload response bodies, B&lt;/th&gt;
&lt;th&gt;Reload readiness, ms&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Three.js&lt;/td&gt;
&lt;td&gt;Default no-cache, no validators&lt;/td&gt;
&lt;td&gt;533,231&lt;/td&gt;
&lt;td&gt;116.46&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Three.js&lt;/td&gt;
&lt;td&gt;Versioned assets immutable&lt;/td&gt;
&lt;td&gt;124&lt;/td&gt;
&lt;td&gt;95.14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UE Wasm&lt;/td&gt;
&lt;td&gt;Default no-cache, no validators&lt;/td&gt;
&lt;td&gt;109,591&lt;/td&gt;
&lt;td&gt;1506.18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UE Wasm&lt;/td&gt;
&lt;td&gt;Versioned assets immutable&lt;/td&gt;
&lt;td&gt;40,838&lt;/td&gt;
&lt;td&gt;1701.52&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These rows use unshaped loopback and medians of three reloads. The default-policy control did not persist its in-flight response counter; its transfer column is the ledger total recorded at collection. They show why “warm” needs a definition. The difference between the two server policies is a deployment result for these files, not a built-in advantage or defect of a JavaScript library. The UE warm page still initialized the engine; caching did not make startup free.&lt;/p&gt;

&lt;p&gt;Across all 48 measured page loads the fixtures rendered moving pixels at the expected canvas size, and the page-error collector recorded zero JavaScript page exceptions. The runs sampled 5,760 post-ready requestAnimationFrame intervals in total. The raw evidence retains four console resource notices from missing favicon requests. Those notices are not silently relabeled as clean network logs. Animation-frame spacing on this tiny, refresh-limited fixture is not a maximum-throughput benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the memory readings do and do not say
&lt;/h2&gt;

&lt;p&gt;The 11 November runner attempted a Wasm linear-memory sample every 100 ms, but its hook did not obtain a memory handle in any of the 24 UE page loads. Those raw fields are null. They mean not measured, not zero memory usage. The startup and transfer results do not supply a new Wasm-memory measurement.&lt;/p&gt;

&lt;p&gt;The separately published 10 November test recorded a sampled peak of 536,870,912 B, or 512 MiB. That historical result belongs to that test. It is a linear-memory allocation, not total browser RAM, not all transient allocations and not GPU VRAM; it is not presented as a newly observed peak for these 24 page loads.&lt;/p&gt;

&lt;p&gt;The data includes Chrome's sampled JavaScript heap counters for both stacks. They cover another memory category and are not comparable to the Wasm allocation as a total-memory ranking. No total-memory or VRAM ranking is presented. A larger scene needs its own resource and memory study.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which path should a project choose?
&lt;/h2&gt;

&lt;p&gt;For an experience built around a web page, Three.js keeps the scene in the front-end code and allows assets to be chosen and loaded at the feature level. Digital Garden already uses that structure: the website shell is separate from the &lt;a href="https://digitalgarden.si/play/" rel="noopener noreferrer"&gt;playable Three.js garden&lt;/a&gt;. The &lt;a href="https://digitalgarden.si/blog/webgpu-garden-houdini-threejs/" rel="noopener noreferrer"&gt;Houdini-to-Three.js garden article&lt;/a&gt; describes that pipeline. This study's four-box scene is a separate fixture.&lt;/p&gt;

&lt;p&gt;For an existing Unreal level, reusing the editor, code and assets may matter more than the initial download. Test the particular features the project relies on in the experimental target, then measure that package on the intended host and client devices. A successful box scene does not validate an arbitrary Blueprint project, large level, deferred rendering path or mobile browser.&lt;/p&gt;

&lt;p&gt;For a project that should remain on a server, assess Pixel Streaming as a service architecture. Its browser client receives a stream rather than downloading the Wasm engine package, but a machine still has to run the Unreal application for the session. Streaming quality, network latency, input behavior and service capacity require separate tests. This article supplies no invented prices or session counts.&lt;/p&gt;

&lt;p&gt;Three.js's &lt;a href="https://threejs.org/docs/pages/WebGPURenderer.html" rel="noopener noreferrer"&gt;WebGPURenderer documentation&lt;/a&gt; describes a WebGL 2 fallback when WebGPU is unavailable. All measured control runs here selected WebGPU; fallback behavior and Safari were not tested. An engine's renderer being called “mobile” does not prove it works on an iPhone. Unity, Babylon.js and PlayCanvas were not measured either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence and reproduction
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://digitalgarden.si/marketing/research/threejs-vs-unreal-20261111/desktop-summary.json" rel="noopener noreferrer"&gt;desktop summary&lt;/a&gt; and &lt;a href="https://digitalgarden.si/marketing/research/threejs-vs-unreal-20261111/threejs-vs-unreal-desktop-reproduction.zip" rel="noopener noreferrer"&gt;reproduction package&lt;/a&gt; contain both 24-run datasets, the harness, authored fixture sources, request ledgers, screenshots and the measurement definitions. The package excludes Unreal Engine binaries, engine scripts and the generated UE runtime; reproduce that side using a licensed engine installation and the build steps linked from the earlier article. The portable runner requires explicit paths to the existing Shipping package, Epic server and Python.&lt;/p&gt;

&lt;p&gt;The first setup attempt encountered Windows' inactive Python alias before a server or measured trial started. It is retained separately as a setup failure. The completed datasets use the installed engine's Python and report no measured-run failures. This distinction matters when counting the 48 measured page loads.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/mrdoob/three.js/blob/r186/LICENSE" rel="noopener noreferrer"&gt;Three.js r186 license&lt;/a&gt; and &lt;a href="https://www.unrealengine.com/en-US/license" rel="noopener noreferrer"&gt;Epic's Unreal Engine terms&lt;/a&gt; remain the authoritative license references. No royalty or seat interpretation is made here.&lt;/p&gt;

&lt;p&gt;Author: Alicia, 3D technical artist (TA) and independent developer. Codex assisted with desktop execution, instrumentation, data checks and this manuscript. Public-host HTTPS, iPhone Safari, Pixel Streaming and larger-scene validation remain untested.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can Unreal Engine run in a browser without Pixel Streaming?
&lt;/h3&gt;

&lt;p&gt;Experimentally, yes. Unreal Engine 5.9 Preview adds a WebAssembly target with WebGPU rendering. Our specific Windows Launcher Shipping fixture rendered locally in Chrome. Public hosting and iPhone Safari remain untested here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are these Three.js and Unreal timings a visual performance ranking?
&lt;/h3&gt;

&lt;p&gt;No. The layout matches but the materials, shaders, runtimes and readiness signals differ. The shared submission hook records a CPU-side WebGPU queue submission, not visible display time or equal rendering work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a warm Unreal Wasm page still initialize the engine?
&lt;/h3&gt;

&lt;p&gt;Yes in this fixture. Cached resources reduced response-body transfers, but each measured reload still initialized the engine. A warm download is not a zero-cost startup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Unreal Wasm work in iPhone Safari?
&lt;/h3&gt;

&lt;p&gt;This study has no iPhone Safari result. Desktop Chrome, a mobile renderer name, and local network shaping do not establish iPhone compatibility.&lt;/p&gt;

</description>
      <category>threejs</category>
      <category>webgpu</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Three.js Exploded View: Explode a Real Multi-Part GLB (and What It Costs in Draw Calls)</title>
      <dc:creator>Alicia lu</dc:creator>
      <pubDate>Mon, 09 Nov 2026 02:52:29 +0000</pubDate>
      <link>https://dev.to/alicia_lu/threejs-exploded-view-explode-a-real-multi-part-glb-and-what-it-costs-in-draw-calls-283g</link>
      <guid>https://dev.to/alicia_lu/threejs-exploded-view-explode-a-real-multi-part-glb-and-what-it-costs-in-draw-calls-283g</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published by Alicia Lu on 9 November 2026 at &lt;a href="https://digitalgarden.si/blog/threejs-exploded-view/" rel="noopener noreferrer"&gt;Digital Garden&lt;/a&gt;. Alicia is a 3D technical artist, TA and independent developer. Codex assisted with implementation, measurement and drafting.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A Three.js exploded view moves a GLB assembly apart by changing its part transforms. For this test, Codex built and exported a potted flower in Houdini 22.0.429 and ran it on Alicia’s RTX 5070 Ti desktop. The fixture has five assemblies: pot, soil, stalk, leaves and bloom.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The short answer.&lt;/strong&gt; Give each movable part a name and a pivot, then save its rest position. The merged, 10-, 50- and 200-part GLBs all contain 30,848 triangles. They took 1, 10, 50 and 200 model draws on both backends. The raw renderer counts were 2, 11, 51 and 201 because an empty scene recorded one draw. From merged to 200 parts, WebGPU load to first GPU completion went from 89.7 to 116.2 ms. Point picking went in the other direction because the split created tighter bounds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd4xcml0kqxrwszycr9lh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd4xcml0kqxrwszycr9lh.png" alt="One purple potted flower assembled, flat-exploded and hierarchy-exploded in Three.js" width="799" height="239"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Export the parts and pivots from Houdini
&lt;/h2&gt;

&lt;p&gt;The native scene uses a Geometry Stash SOP inside each OBJ mesh. Each part has a pivot at its local bounds center and is parented under a named assembly null. The merged control stores the same colored geometry under one mesh at the origin. The 10- and 50-part layouts group the same 200 source components in fewer meshes. The glTF 2.0 ROP exports the OBJ hierarchy, with one mesh primitive per part. A node extras dictionary stores the assembly name and component count. Three.js read both back from userData. &lt;a href="https://www.sidefx.com/docs/houdini/nodes/out/gltf.html" rel="noopener noreferrer"&gt;SideFX glTF 2.0 ROP&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All four native exports passed the glTF validator with zero errors and warnings. A separate audit matched every oriented triangle against the merged control and compared its world-space corners, normals and vertex colors. The largest component difference was 2.46 × 10⁻⁷. The saved HIP reopened with all four layouts and their geometry intact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flat traverse and a two-stage hierarchy
&lt;/h2&gt;

&lt;p&gt;Flat explode moves each part away from the model center. The hierarchy path first moves the five parents apart. At slider value 0.5, the children still have their rest positions inside those groups. The second half moves each child away from its own group center. The two modes use different offsets to illustrate how they open. The flat view spreads parts along the plant’s overall shape, while the hierarchy view opens the five assemblies in stages.&lt;/p&gt;

&lt;p&gt;Both methods compute positions from stored rest values. The test model sits under a rotated, nonuniformly scaled parent. To apply a displacement in world units, convert two points to the parent’s local space and subtract them. The resulting vector length accounts for parent scale. &lt;a href="https://threejs.org/docs/pages/Object3D.html" rel="noopener noreferrer"&gt;Three.js Object3D&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;localOffset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;point&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;worldOffset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;worldToLocal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;worldToLocal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;worldOffset&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Hierarchy path after the local offsets have been prepared.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;groupT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;partT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;group&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addScaledVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;groupT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addScaledVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;partT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Across 24 parted contexts, 960 out-and-back cycles returned to rest with zero measured drift. The largest world-position error in the displacement checks was 1.33 × 10⁻¹⁵ scene units. The two GIFs share a wider camera so the plant stays in frame at full separation. The timing runs use their own fixed camera.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftwhup4ojnz2eb3hbtk8c.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftwhup4ojnz2eb3hbtk8c.gif" alt="Flat explosion of the 10-part Houdini plant from rest to full separation and back" width="760" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fah4vnpcfrybip79g8jp8.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fah4vnpcfrybip79g8jp8.gif" alt="Two-stage hierarchy explosion of the same plant with parent assemblies opening before their children" width="760" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Draw calls and first completion
&lt;/h2&gt;

&lt;p&gt;Chrome and Three.js 0.186.1 ran at 1280 × 720, device pixel ratio 1. The WebGL2 path uses the WebGPU renderer’s WebGL backend on ANGLE/D3D11. Both selected NVIDIA hardware. Four fresh contexts per layout and backend loaded each GLB from localhost with cache disabled. Contexts shared a Chrome process and did not clear driver or OS caches.&lt;/p&gt;

&lt;p&gt;These are medians in milliseconds. Asset-to-completion starts after renderer initialization and includes loading, scene setup, awaited compile and the first render with its work wait. WebGPU used the queue completion promise and WebGL2 used gl.finish(). This measures submitted work completion before display presentation.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layout&lt;/th&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;th&gt;Raw draws&lt;/th&gt;
&lt;th&gt;GLB MiB&lt;/th&gt;
&lt;th&gt;Fetch ms&lt;/th&gt;
&lt;th&gt;Parse ms&lt;/th&gt;
&lt;th&gt;Compile ms&lt;/th&gt;
&lt;th&gt;First render + wait ms&lt;/th&gt;
&lt;th&gt;Asset to completion ms&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Merged&lt;/td&gt;
&lt;td&gt;WebGPU&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0.965&lt;/td&gt;
&lt;td&gt;5.6&lt;/td&gt;
&lt;td&gt;2.4&lt;/td&gt;
&lt;td&gt;60.9&lt;/td&gt;
&lt;td&gt;20.7&lt;/td&gt;
&lt;td&gt;89.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 parts&lt;/td&gt;
&lt;td&gt;WebGPU&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;0.977&lt;/td&gt;
&lt;td&gt;5.2&lt;/td&gt;
&lt;td&gt;3.4&lt;/td&gt;
&lt;td&gt;61.5&lt;/td&gt;
&lt;td&gt;20.7&lt;/td&gt;
&lt;td&gt;91.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50 parts&lt;/td&gt;
&lt;td&gt;WebGPU&lt;/td&gt;
&lt;td&gt;51&lt;/td&gt;
&lt;td&gt;1.027&lt;/td&gt;
&lt;td&gt;5.2&lt;/td&gt;
&lt;td&gt;4.9&lt;/td&gt;
&lt;td&gt;64.6&lt;/td&gt;
&lt;td&gt;21.0&lt;/td&gt;
&lt;td&gt;96.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200 parts&lt;/td&gt;
&lt;td&gt;WebGPU&lt;/td&gt;
&lt;td&gt;201&lt;/td&gt;
&lt;td&gt;1.215&lt;/td&gt;
&lt;td&gt;5.9&lt;/td&gt;
&lt;td&gt;9.8&lt;/td&gt;
&lt;td&gt;71.2&lt;/td&gt;
&lt;td&gt;30.0&lt;/td&gt;
&lt;td&gt;116.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merged&lt;/td&gt;
&lt;td&gt;WebGL2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0.965&lt;/td&gt;
&lt;td&gt;6.2&lt;/td&gt;
&lt;td&gt;2.4&lt;/td&gt;
&lt;td&gt;24.2&lt;/td&gt;
&lt;td&gt;7.6&lt;/td&gt;
&lt;td&gt;43.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 parts&lt;/td&gt;
&lt;td&gt;WebGL2&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;0.977&lt;/td&gt;
&lt;td&gt;6.0&lt;/td&gt;
&lt;td&gt;3.2&lt;/td&gt;
&lt;td&gt;27.0&lt;/td&gt;
&lt;td&gt;8.5&lt;/td&gt;
&lt;td&gt;47.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50 parts&lt;/td&gt;
&lt;td&gt;WebGL2&lt;/td&gt;
&lt;td&gt;51&lt;/td&gt;
&lt;td&gt;1.027&lt;/td&gt;
&lt;td&gt;6.3&lt;/td&gt;
&lt;td&gt;5.1&lt;/td&gt;
&lt;td&gt;36.8&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;td&gt;58.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200 parts&lt;/td&gt;
&lt;td&gt;WebGL2&lt;/td&gt;
&lt;td&gt;201&lt;/td&gt;
&lt;td&gt;1.215&lt;/td&gt;
&lt;td&gt;6.4&lt;/td&gt;
&lt;td&gt;9.2&lt;/td&gt;
&lt;td&gt;29.7&lt;/td&gt;
&lt;td&gt;13.4&lt;/td&gt;
&lt;td&gt;59.1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 200-part asset cost about 26.6 ms more than the merged WebGPU median. The WebGL2 50-part runs ranged from 46.4 to 78.1 ms, which is wider than the difference between the 50- and 200-part medians. The file size, number of nodes and buffer layout all change with the split. The separate columns are medians from four runs and do not necessarily add up.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnif049nyhh6fgo75h3l1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnif049nyhh6fgo75h3l1.png" alt="Median load to first GPU completion for the merged and parted GLBs" width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The harness reads renderer.info.render.drawCalls. Early pilots caught the empty-scene draw and a second issue: back-to-back render calls could accumulate statistics. The formal harness resets counters before each render and measures the empty scene after first completion. &lt;a href="https://threejs.org/docs/pages/Info.html" rel="noopener noreferrer"&gt;Three.js Info&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;200-part mode&lt;/th&gt;
&lt;th&gt;Raw draws&lt;/th&gt;
&lt;th&gt;WebGPU CPU submit ms&lt;/th&gt;
&lt;th&gt;WebGL2 CPU submit ms&lt;/th&gt;
&lt;th&gt;WebGPU / WebGL2 p95 rAF ms&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Assembled&lt;/td&gt;
&lt;td&gt;201&lt;/td&gt;
&lt;td&gt;0.8&lt;/td&gt;
&lt;td&gt;0.6&lt;/td&gt;
&lt;td&gt;16.8 / 16.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flat&lt;/td&gt;
&lt;td&gt;201&lt;/td&gt;
&lt;td&gt;1.1&lt;/td&gt;
&lt;td&gt;0.9&lt;/td&gt;
&lt;td&gt;16.8 / 16.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hierarchy&lt;/td&gt;
&lt;td&gt;201&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;0.9&lt;/td&gt;
&lt;td&gt;16.8 / 16.8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;CPU submit is elapsed time around the render call and counter reset. It excludes the per-frame transform update and GPU wait. Warm rAF intervals stayed near the refresh cadence. The assembly uses one opaque vertex-color material, no shadows and disabled frustum culling, so moving parts does not change the draw count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking got cheaper as the mesh count rose
&lt;/h2&gt;

&lt;p&gt;The harness times setFromCamera and intersectObjects on the same 49 rays. Each context ran 12 timed sweeps after three warm-up sweeps. Every sweep had 12 hits. The per-ray times are CPU measurements grouped by the renderer in use in that context. Picking uses an explicit mesh list and reuses its result array. &lt;a href="https://threejs.org/docs/pages/Raycaster.html" rel="noopener noreferrer"&gt;Three.js Raycaster&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layout&lt;/th&gt;
&lt;th&gt;WebGPU µs/ray&lt;/th&gt;
&lt;th&gt;WebGL2 µs/ray&lt;/th&gt;
&lt;th&gt;Meshes reaching triangle loop per sweep&lt;/th&gt;
&lt;th&gt;Triangle loop entries per sweep&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Merged&lt;/td&gt;
&lt;td&gt;1,373.5&lt;/td&gt;
&lt;td&gt;676.5&lt;/td&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;td&gt;1,264,768&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 parts&lt;/td&gt;
&lt;td&gt;234.7&lt;/td&gt;
&lt;td&gt;117.3&lt;/td&gt;
&lt;td&gt;59&lt;/td&gt;
&lt;td&gt;208,192&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50 parts&lt;/td&gt;
&lt;td&gt;106.1&lt;/td&gt;
&lt;td&gt;55.1&lt;/td&gt;
&lt;td&gt;105&lt;/td&gt;
&lt;td&gt;78,104&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200 parts&lt;/td&gt;
&lt;td&gt;65.3&lt;/td&gt;
&lt;td&gt;30.6&lt;/td&gt;
&lt;td&gt;130&lt;/td&gt;
&lt;td&gt;37,952&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An untimed CPU control counted entries after the built-in bounding tests in the same Three.js version. From 10 to 200 meshes, more candidates passed the bounds checks but many fewer triangles reached the inner loop. All 49 hit-or-miss outcomes matched. The largest difference in the nearest hit distance was below 0.000002 scene units. The result depends on the asset’s bounds and the ray distribution. &lt;a href="https://github.com/mrdoob/three.js/blob/r186/src/objects/Mesh.js" rel="noopener noreferrer"&gt;Three.js r186 mesh raycasting source&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzc6kf3snofp7icg64s19.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzc6kf3snofp7icg64s19.png" alt="Measured mean-per-sweep ray-picking cost summarized by median for 10, 50 and 200 parts" width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Click to fade or hide
&lt;/h2&gt;

&lt;p&gt;The live viewer picks on pointer release and ignores drags longer than five pixels. Fade assigns unselected meshes a shared translucent material at opacity 0.17. The selected mesh retains the opaque material. Hide makes the other meshes invisible and filters them out of the next pick. The camera stays fixed for the comparison.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layout&lt;/th&gt;
&lt;th&gt;Fade / hide raw draws&lt;/th&gt;
&lt;th&gt;WebGPU fade / hide CPU submit ms&lt;/th&gt;
&lt;th&gt;WebGL2 fade / hide CPU submit ms&lt;/th&gt;
&lt;th&gt;Model triangles when hidden&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10 parts&lt;/td&gt;
&lt;td&gt;11 / 2&lt;/td&gt;
&lt;td&gt;0.2 / 0.2&lt;/td&gt;
&lt;td&gt;0.2 / 0.2&lt;/td&gt;
&lt;td&gt;2,176&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50 parts&lt;/td&gt;
&lt;td&gt;51 / 2&lt;/td&gt;
&lt;td&gt;0.4 / 0.2&lt;/td&gt;
&lt;td&gt;0.3 / 0.1&lt;/td&gt;
&lt;td&gt;2,176&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200 parts&lt;/td&gt;
&lt;td&gt;201 / 2&lt;/td&gt;
&lt;td&gt;0.6 / 0.2&lt;/td&gt;
&lt;td&gt;0.6 / 0.2&lt;/td&gt;
&lt;td&gt;1,152&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fade kept the whole shape visible. Hide left one model draw plus the empty-scene draw. The hidden triangle count depends on the chosen part, whose grouping changes with the layout. The table shows warmed CPU submit medians. Transition completion samples are in the raw data. Some WebGL2 samples hit the timer floor, so they cannot support a more precise latency claim.&lt;/p&gt;

&lt;p&gt;A separate browser interaction test moved the slider, changed the rendered canvas, picked a part, toggled hide and fade, and reset. A 300 ms idle window after each timed sequence logged zero further render calls in all 32 contexts. This counts submitted frames. GPU utilization and power were outside the test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the experiment
&lt;/h2&gt;

&lt;p&gt;The bundle includes four native GLBs, the Houdini scene and generator, browser viewer, raw trials, chart sources, GIFs and the picking control. The formal set contains 32 contexts, 7,680 rAF intervals and 18,816 timed ray evaluations, including 14,112 on the three parted layouts. Pilot failures are separate from the formal data. &lt;a href="https://digitalgarden.si/marketing/research/exploded-view-20261109/exploded-view-reproduction.zip" rel="noopener noreferrer"&gt;Download the reproduction bundle&lt;/a&gt; or &lt;a href="https://digitalgarden.si/marketing/research/exploded-view-20261109/summary.csv" rel="noopener noreferrer"&gt;inspect the summary CSV&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The results cover one desktop, one browser version, one camera for timing and an untextured authored model. Mobile devices, production asset loading and batched alternatives are outside the measured scope. Related studies cover &lt;a href="https://digitalgarden.si/blog/threejs-glb-loading-cache-first-frame/" rel="noopener noreferrer"&gt;GLB loading and first frame&lt;/a&gt; and the &lt;a href="https://digitalgarden.si/blog/threejs-forest-performance-budget/" rel="noopener noreferrer"&gt;forest draw budget&lt;/a&gt; with different fixtures.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;By Alicia, a 3D technical artist, TA and independent developer. Codex assisted with implementation, measurement and drafting.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can one merged GLB mesh explode?
&lt;/h3&gt;

&lt;p&gt;A merged mesh has one object transform. Independent movement requires separate logical parts, such as separately transformed meshes or an explicit per-part representation. This study measured separate native GLB meshes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does every named part always add one draw?
&lt;/h3&gt;

&lt;p&gt;Each mesh added one model draw in this one-primitive, one-material test. Extra materials, passes and batching change the count. The raw counter also recorded one draw in an empty scene.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should click-to-isolate fade or hide other parts?
&lt;/h3&gt;

&lt;p&gt;Fade keeps the surrounding assembly visible and retained all model draws here. Hide left one model draw for the selected mesh. Choose based on whether the rest of the assembly helps the reader.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>performance</category>
    </item>
    <item>
      <title>Brush vs Spirula Studio on Windows with the Same Posed Garden</title>
      <dc:creator>Alicia lu</dc:creator>
      <pubDate>Thu, 05 Nov 2026 03:37:44 +0000</pubDate>
      <link>https://dev.to/alicia_lu/brush-vs-spirula-studio-on-windows-with-the-same-posed-garden-5cka</link>
      <guid>https://dev.to/alicia_lu/brush-vs-spirula-studio-on-windows-with-the-same-posed-garden-5cka</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published by Alicia Lu on 5 November 2026 at &lt;a href="https://digitalgarden.si/blog/brush-vs-spirula-gaussian-splatting-windows/" rel="noopener noreferrer"&gt;Digital Garden&lt;/a&gt;. Alicia is a 3D technical artist, TA and independent developer. Codex assisted with this study.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I trained a synthetic garden in Brush 0.3.0 and Spirula Studio 2026.10.16 on one Windows RTX 5070 Ti. Both exported a Gaussian PLY at 600 and 6,000 steps. At 6,000 steps, the saved RGB images averaged 21.48 dB PSNR for Brush and 38.17 dB for Spirula on the same seven held-out views. This result covers the chosen budgets, settings and scene.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4y1v0o39vk3hwvoyqq9h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4y1v0o39vk3hwvoyqq9h.png" alt="Two matched held-out views from Karma, Brush and Spirula at 600 and 6,000 training steps" width="800" height="353"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The shared input and training limits
&lt;/h2&gt;

&lt;p&gt;The 44 posed RGB images and camera intrinsics come from the &lt;a href="https://digitalgarden.si/blog/houdini-procedural-gaussian-splatting/" rel="noopener noreferrer"&gt;Houdini procedural splatting study&lt;/a&gt;. Each image is 192 × 192. I used views 1–36 for training and held out 37–44. Both trainers used SH0 and a 12,000-splat cap, with their own random initialization and optimization settings. Brush used wgpu on Vulkan and seed 381. Spirula used its Vulkan release, 3dgs preset, and explicit random initialization. The GPU driver was NVIDIA 617.42. This experiment starts with known camera poses.&lt;/p&gt;

&lt;p&gt;Brush received separate Nerfstudio train and test manifests. Spirula needed a single transforms.json. I copied the identical PNGs, added train and eval prefixes for its filename split, and kept the original camera matrices and intrinsics. An initial Spirula probe using only the split manifests reported that transforms.json was missing. The derived dataset fixed the packaging issue. &lt;a href="https://github.com/harry7557558/spirula-studio/blob/65246c5/src/data/parsers/DatasetCommon.cpp" rel="noopener noreferrer"&gt;Spirula dataset implementation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four exports and one independent comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Trainer&lt;/th&gt;
&lt;th&gt;Steps&lt;/th&gt;
&lt;th&gt;Process wall time (s)&lt;/th&gt;
&lt;th&gt;Exported splats&lt;/th&gt;
&lt;th&gt;PLY bytes&lt;/th&gt;
&lt;th&gt;Mean RGB PSNR (dB, seven views)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Brush&lt;/td&gt;
&lt;td&gt;600&lt;/td&gt;
&lt;td&gt;6.121&lt;/td&gt;
&lt;td&gt;11,680&lt;/td&gt;
&lt;td&gt;654,515&lt;/td&gt;
&lt;td&gt;13.99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brush&lt;/td&gt;
&lt;td&gt;6,000&lt;/td&gt;
&lt;td&gt;18.370&lt;/td&gt;
&lt;td&gt;12,000&lt;/td&gt;
&lt;td&gt;672,435&lt;/td&gt;
&lt;td&gt;21.48&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spirula&lt;/td&gt;
&lt;td&gt;600&lt;/td&gt;
&lt;td&gt;1.490&lt;/td&gt;
&lt;td&gt;1,200&lt;/td&gt;
&lt;td&gt;82,014&lt;/td&gt;
&lt;td&gt;28.21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spirula&lt;/td&gt;
&lt;td&gt;6,000&lt;/td&gt;
&lt;td&gt;12.320&lt;/td&gt;
&lt;td&gt;11,992&lt;/td&gt;
&lt;td&gt;815,871&lt;/td&gt;
&lt;td&gt;38.17&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I matched the saved Spirula reference PNGs to the original input by exact decoded pixels. The 600-step output repeated view 37 and omitted view 39. The two renderings of view 37 were identical. Both Brush runs and the 6,000-step Spirula run provided all eight views. I kept views 37, 38, 40, 41, 42, 43 and 44 for every row. I independently measured full-frame PSNR in 8-bit RGB, then averaged the seven image-level dB values. The table uses those recalculated values. The application logs keep their own evaluation values.&lt;/p&gt;

&lt;p&gt;The PLY count comes from each exported header. Spirula reported 12,000 active points at the end of the longer run and wrote 11,992 points. Process timing includes startup, import, training, evaluation and export. One run per configuration and the different initialization, growth and settings limit conclusions about speed or trainer quality. I kept whole-board memory samples separate from any per-process VRAM claim.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing the Brush image-header failure
&lt;/h2&gt;

&lt;p&gt;The first two Brush attempts failed with early eof, using both a directory and ZIP. Brush 0.3.0 reads at least 16,387 bytes during its image-header probe. Two otherwise valid compressed PNGs were shorter. I regenerated all 44 PNGs with compression level zero and verified that every decoded RGB pixel matched the earlier files. The reproduced dataset includes the larger lossless PNGs. &lt;a href="https://github.com/ArthurBrussee/brush/blob/v0.3.0/crates/brush-dataset/src/scene.rs" rel="noopener noreferrer"&gt;Brush 0.3.0 loader&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Both used freely available Windows builds from their official releases. The test covers posed synthetic inputs and exported PLYs. It leaves real photos, SfM and other trainers for separate runs. &lt;a href="https://github.com/ArthurBrussee/brush/releases/tag/v0.3.0" rel="noopener noreferrer"&gt;Brush releases&lt;/a&gt;, &lt;a href="https://github.com/harry7557558/spirula-studio/releases/tag/v2026.10.16" rel="noopener noreferrer"&gt;Spirula Studio release&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Alicia is a 3D technical artist, TA and independent developer. Codex assisted with the dataset adapters, measurement and editing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related studies
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://digitalgarden.si/blog/houdini-procedural-gaussian-splatting/" rel="noopener noreferrer"&gt;Train a splat from Houdini Karma renders&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://digitalgarden.si/blog/spirula-studio-garden-splat-threejs/" rel="noopener noreferrer"&gt;Spirula Studio on the official Garden sample&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Data and reproduction
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://digitalgarden.si/marketing/research/splat-training-windows-20261105/matched-training.json" rel="noopener noreferrer"&gt;matched-training.json&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://digitalgarden.si/marketing/research/splat-training-windows-20261105/splat-training-windows-reproduction.zip" rel="noopener noreferrer"&gt;splat-training-windows-reproduction.zip&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>3d</category>
      <category>software</category>
      <category>tools</category>
    </item>
    <item>
      <title>Gaussian Splat Viewers on Desktop: PLY, SOG and SPZ</title>
      <dc:creator>Alicia lu</dc:creator>
      <pubDate>Thu, 05 Nov 2026 03:36:43 +0000</pubDate>
      <link>https://dev.to/alicia_lu/gaussian-splat-viewers-on-desktop-ply-sog-and-spz-38cl</link>
      <guid>https://dev.to/alicia_lu/gaussian-splat-viewers-on-desktop-ply-sog-and-spz-38cl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published by Alicia Lu on 5 November 2026 at &lt;a href="https://digitalgarden.si/blog/gaussian-splat-viewers-desktop/" rel="noopener noreferrer"&gt;Digital Garden&lt;/a&gt;. Alicia is a 3D technical artist, TA and independent developer. Codex assisted with this study.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I used one 4,096-splat synthetic garden to check three ways to view Gaussian splats locally in Chrome. SuperSplat's generated HTML and antimatter15/splat both rendered the garden. A pinned PlayCanvas Model Viewer debug build rendered original PLY, compressed PLY and bundled SOG. The same viewer did not load SPZ v3 or v4 through its local file input. Its release build hit a worker error before displaying the three supported files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three desktop paths
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Viewer path&lt;/th&gt;
&lt;th&gt;Local input actually tried&lt;/th&gt;
&lt;th&gt;Desktop result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SuperSplat Viewer standalone&lt;/td&gt;
&lt;td&gt;SOG embedded by splat-transform 3.10.3&lt;/td&gt;
&lt;td&gt;Visible garden in a 960 × 720 canvas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;antimatter15/splat at ba182b51b7c2&lt;/td&gt;
&lt;td&gt;Original PLY by absolute same-origin URL&lt;/td&gt;
&lt;td&gt;Visible garden and a WebGL instanced draw with 4,096 instances&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PlayCanvas Model Viewer 5.10.0, debug WebGL2&lt;/td&gt;
&lt;td&gt;PLY, compressed PLY and bundled SOG&lt;/td&gt;
&lt;td&gt;All three rendered and each decoded resource held 4,096 splats&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PlayCanvas Model Viewer 5.10.0, debug WebGL2&lt;/td&gt;
&lt;td&gt;SPZ v3 and SPZ v4&lt;/td&gt;
&lt;td&gt;Both left the load screen visible with no decoded splat resource&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffe6v5esro2hzm7vovmqi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffe6v5esro2hzm7vovmqi.png" alt="SuperSplat Viewer displaying the garden from the generated HTML with an embedded SOG payload" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxavdothxqr0cj8pdxofd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxavdothxqr0cj8pdxofd.png" alt="PlayCanvas Model Viewer debug build rendering the bundled SOG with its scene panel visible" width="800" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The screenshots use the viewers' own camera and UI. I checked file loading and actual drawing. The &lt;a href="https://digitalgarden.si/blog/spz-vs-sog-vs-ply-gaussian-splat-formats/" rel="noopener noreferrer"&gt;format comparison&lt;/a&gt; uses one renderer and matched cameras for its separate image-error and startup measurements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replaying local files
&lt;/h2&gt;

&lt;p&gt;For SuperSplat Viewer, this command generates a standalone HTML page that embeds the SOG data. My generated file was 3,892,390 bytes including the viewer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx splat-transform &lt;span class="nt"&gt;-g&lt;/span&gt; cpu garden.ply garden.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For antimatter15/splat, serve the upstream HTML and JavaScript from a static server and set the url query parameter to an absolute same-origin PLY URL. A relative value resolves against the viewer's default asset host. My initial relative-URL probe attempted to reach that host, which the harness blocked. The final replay issued only local GET requests.&lt;/p&gt;

&lt;p&gt;The PlayCanvas test used the open-source Model Viewer at commit 34df5967df32 with PlayCanvas Engine 2.19.7. I built the release and debug targets from the same source and lockfile. The release build hit a ReferenceError in its blob worker on WebGL2 and WebGPU. The error also appeared after the panel reported 4,096 primitives for PLY. The rendered garden was absent, so those runs did not pass. The upstream debug build ran the three supported formats on WebGL2. Its compressed PLY and SOG scene panels showed zero primitives even when the garden was visibly rendered. I checked the decoded resource count for those rows. This describes the pinned local builds. The hosted viewer and the engine's separate SPZ parser have their own paths. &lt;a href="https://github.com/playcanvas/model-viewer/tree/34df5967df32032d34e8833cd29f8d0f19c7a93a" rel="noopener noreferrer"&gt;Model Viewer source&lt;/a&gt;, &lt;a href="https://developer.playcanvas.com/user-manual/gaussian-splatting/viewing/" rel="noopener noreferrer"&gt;Model Viewer formats&lt;/a&gt;, &lt;a href="https://developer.playcanvas.com/user-manual/gaussian-splatting/formats/spz/" rel="noopener noreferrer"&gt;PlayCanvas Engine SPZ parser&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the desktop check covers
&lt;/h2&gt;

&lt;p&gt;The successful local replays required no login and made only local GET requests in the isolated test. The network evidence covers these specific self-hosted paths, with any nonlocal traffic blocked by the harness. It says nothing about upload and account behavior on third-party hosted services. The rendered screenshots and source file receipts are included. Physical iPhone and Quest 3 operation, immersive modes and hosted service policies remain separate tests. &lt;a href="https://github.com/playcanvas/supersplat-viewer" rel="noopener noreferrer"&gt;SuperSplat Viewer&lt;/a&gt;, &lt;a href="https://github.com/antimatter15/splat/tree/ba182b51b7c2ad5738cdd6741cd63336d27470fb" rel="noopener noreferrer"&gt;antimatter15/splat&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Alicia is a 3D technical artist, TA and independent developer. Codex assisted with the test harness, evidence checks and editing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related studies
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://digitalgarden.si/blog/spz-vs-sog-vs-ply-gaussian-splat-formats/" rel="noopener noreferrer"&gt;Five encodings of one splat garden&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://digitalgarden.si/blog/threejs-gaussian-splat-renderer-comparison/" rel="noopener noreferrer"&gt;Five Three.js splat renderer paths&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Data and reproduction
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://digitalgarden.si/marketing/research/splat-formats-20261105/viewers.json" rel="noopener noreferrer"&gt;viewers.json&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://digitalgarden.si/marketing/research/splat-formats-20261105/playcanvas-viewer-debug-resource.json" rel="noopener noreferrer"&gt;playcanvas-viewer-debug-resource.json&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://digitalgarden.si/marketing/research/splat-formats-20261105/splat-viewers-desktop-reproduction.zip" rel="noopener noreferrer"&gt;splat-viewers-desktop-reproduction.zip&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>tools</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Houdini GLB to USDZ with Blender, Three.js and Three-Live</title>
      <dc:creator>Alicia lu</dc:creator>
      <pubDate>Thu, 05 Nov 2026 03:36:30 +0000</pubDate>
      <link>https://dev.to/alicia_lu/houdini-glb-to-usdz-with-blender-threejs-and-three-live-3pnc</link>
      <guid>https://dev.to/alicia_lu/houdini-glb-to-usdz-with-blender-threejs-and-three-live-3pnc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published by Alicia Lu on 5 November 2026 at &lt;a href="https://digitalgarden.si/blog/threejs-glb-to-usdz-desktop/" rel="noopener noreferrer"&gt;Digital Garden&lt;/a&gt;. Alicia is a 3D technical artist, TA and independent developer. Codex assisted with this study.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I converted the same textured Houdini plant with Three.js r187, Three-Live 1.2.3 and Blender 5.2 LTS. All three exports reopened in Houdini with 1,056 triangles. Blender preserved the original world bounds, two textures and the double-sided leaves. Three.js preserved the bounds and textures. Three-Live produced the smallest package, with a shifted bounding box and no linked textures.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwhel83x8fvb3pn955wny.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwhel83x8fvb3pn955wny.png" alt="Houdini plant in Chrome before USDZ conversion, showing the stem and three leaves" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One input and three export paths
&lt;/h2&gt;

&lt;p&gt;The 947,652-byte input is the owned 512-pixel GLB from the &lt;a href="https://digitalgarden.si/blog/houdini-copernicus-bake-glb-threejs/" rel="noopener noreferrer"&gt;Copernicus bake study&lt;/a&gt;. It has 628 vertices, 1,056 triangles, a normal map, a packed occlusion-roughness-metallic map and a double-sided material.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Three.js r187.&lt;/strong&gt; In Chrome 156 on an RTX 5070 Ti, I loaded the GLB with GLTFLoader and ran USDZExporter with maxTextureSize 1024. I exported three times per Quick Look option.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three-Live 1.2.3.&lt;/strong&gt; The verified Windows x64 CLI read the same GLB and exported USDZ directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blender 5.2.0 LTS.&lt;/strong&gt; A factory-startup background session imported the same GLB and exported a Y-up, meter-scale USDZ with USD Preview Surface. A fresh Blender scene then imported the USDZ, recovering 628 vertices, 1,056 faces and two packed images.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;three-live-cli.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;convert&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;input/houdini-plant.glb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;usdz&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;evidence/plant-three-live.usdz&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;blender&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--background&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--factory-startup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--python-exit-code&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;export_blender.py&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scene&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GLTFLoader&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;loadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/input/houdini-plant.glb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;USDZExporter&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;parseAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;quickLookCompatible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxTextureSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What survived when I reopened the packages
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;USDZ bytes&lt;/th&gt;
&lt;th&gt;Stored ZIP members&lt;/th&gt;
&lt;th&gt;Resolved image files&lt;/th&gt;
&lt;th&gt;Points / triangles&lt;/th&gt;
&lt;th&gt;Double-sided&lt;/th&gt;
&lt;th&gt;World Y range (m)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Three-Live 1.2.3&lt;/td&gt;
&lt;td&gt;66,910&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;628 / 1,056&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;−0.9923 to 1.1887&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Three.js r187, option off&lt;/td&gt;
&lt;td&gt;364,858&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;628 / 1,056&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;0.0000 to 2.0287&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Three.js r187, option on&lt;/td&gt;
&lt;td&gt;364,858&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;628 / 1,056&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;0.0000 to 2.0287&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blender 5.2 LTS, Y up&lt;/td&gt;
&lt;td&gt;957,384&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;628 / 1,056&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;0.0000 to 2.0287&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Houdini 22.0.429's OpenUSD 0.26.5 reopened all four packages. All entries passed CRC, store-method and 64-byte alignment checks. Each stage declared Y up and one meter per unit. Three.js and Blender preserved the GLB world bounds to within 0.000001 m per component. The Three-Live file had one USDA layer and a bound UsdPreviewSurface with no linked texture files.&lt;/p&gt;

&lt;p&gt;The Blender package used a binary USDC layer and two PNG textures. Its normal PNG accounts for most of its 957,384 bytes. The result supports a desktop handoff where the leaf backs and floor position matter. The smaller Three.js result is another candidate when the mesh's single-sided export is acceptable for the intended camera.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Quick Look option changed on this plant
&lt;/h2&gt;

&lt;p&gt;All six Three.js exports were byte-identical, across both settings. The exporter warned about the double-sided input in each run. Every Three.js USD mesh reopened with doubleSided false. The result is specific to this asset and exporter build.&lt;/p&gt;

&lt;p&gt;The included device checklist links the saved USDZ files. A physical iPhone can then check texture appearance, back-facing leaves, initial scale and placement. This release reports the desktop export and reopen. Apple documents the device playback path separately. &lt;a href="https://developer.apple.com/quick-look-gallery/" rel="noopener noreferrer"&gt;Apple Quick Look&lt;/a&gt;, &lt;a href="https://threejs.org/docs/#USDZExporter" rel="noopener noreferrer"&gt;Three.js USDZExporter&lt;/a&gt;, &lt;a href="https://github.com/google/three-live/releases/tag/v1.2.3" rel="noopener noreferrer"&gt;Three-Live 1.2.3&lt;/a&gt;, &lt;a href="https://docs.blender.org/manual/en/latest/files/import_export/usd.html" rel="noopener noreferrer"&gt;Blender USD manual&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Alicia is a 3D technical artist, TA and independent developer. Codex assisted with the test harness, evidence checks and editing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related studies
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://digitalgarden.si/blog/houdini-copernicus-bake-glb-threejs/" rel="noopener noreferrer"&gt;Houdini Copernicus bake for Three.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://digitalgarden.si/blog/houdini-usd-vs-gltf-threejs/" rel="noopener noreferrer"&gt;Houdini USD vs glTF for Three.js&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Data and reproduction
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://digitalgarden.si/marketing/research/three-live-usdz-20261105/openusd.json" rel="noopener noreferrer"&gt;openusd.json&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://digitalgarden.si/marketing/research/three-live-usdz-20261105/blender-reopen.json" rel="noopener noreferrer"&gt;blender-reopen.json&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://digitalgarden.si/marketing/research/three-live-usdz-20261105/three-usdz-desktop-reproduction.zip" rel="noopener noreferrer"&gt;three-usdz-desktop-reproduction.zip&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>SPZ vs SOG vs PLY: Five Exports of One Gaussian Splat Garden</title>
      <dc:creator>Alicia lu</dc:creator>
      <pubDate>Thu, 05 Nov 2026 03:35:22 +0000</pubDate>
      <link>https://dev.to/alicia_lu/spz-vs-sog-vs-ply-five-exports-of-one-gaussian-splat-garden-8pb</link>
      <guid>https://dev.to/alicia_lu/spz-vs-sog-vs-ply-five-exports-of-one-gaussian-splat-garden-8pb</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published by Alicia Lu on 5 November 2026 at &lt;a href="https://digitalgarden.si/blog/spz-vs-sog-vs-ply-gaussian-splat-formats/" rel="noopener noreferrer"&gt;Digital Garden&lt;/a&gt;. Alicia is a 3D technical artist, TA and independent developer. Codex assisted with this study.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On one 4,096-splat garden, SPZ v4 was the smallest file at 62,011 bytes, down from 278,942 bytes for the original PLY. Compressed PLY had the smallest image error in the three tested Spark views. SOG landed between compressed PLY and SPZ on that image comparison. The best pick depends on the viewer's supported formats and whether the delivery server applies additional compression.&lt;/p&gt;

&lt;h2&gt;
  
  
  One source, one renderer
&lt;/h2&gt;

&lt;p&gt;I started with an owned synthetic scene fitted from Karma renders in the &lt;a href="https://digitalgarden.si/blog/houdini-procedural-gaussian-splatting/" rel="noopener noreferrer"&gt;Houdini procedural splatting study&lt;/a&gt;. The source has 4,096 splats and SH degree 0. I encoded the exact same PLY with splat-transform 3.10.3, then loaded every result through Spark 2.4.0 and Three.js r187. The browser was Chrome 156.0.8078.80 with WebGL on an RTX 5070 Ti.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx splat-transform garden.ply garden.compressed.ply
npx splat-transform &lt;span class="nt"&gt;--spz-version&lt;/span&gt; 3 garden.ply garden-v3.spz
npx splat-transform &lt;span class="nt"&gt;--spz-version&lt;/span&gt; 4 garden.ply garden.spz
npx splat-transform &lt;span class="nt"&gt;-g&lt;/span&gt; cpu garden.ply garden.sog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Size, first submission and image difference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Raw bytes&lt;/th&gt;
&lt;th&gt;Brotli q11 bytes&lt;/th&gt;
&lt;th&gt;First submit median (ms)&lt;/th&gt;
&lt;th&gt;Full-frame RGB PSNR vs PLY (dB, three views)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PLY&lt;/td&gt;
&lt;td&gt;278,942&lt;/td&gt;
&lt;td&gt;212,282&lt;/td&gt;
&lt;td&gt;143.6&lt;/td&gt;
&lt;td&gt;reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compressed PLY&lt;/td&gt;
&lt;td&gt;67,344&lt;/td&gt;
&lt;td&gt;66,656&lt;/td&gt;
&lt;td&gt;147.8&lt;/td&gt;
&lt;td&gt;54.37 / 53.42 / 54.18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPZ v3&lt;/td&gt;
&lt;td&gt;68,140&lt;/td&gt;
&lt;td&gt;68,145&lt;/td&gt;
&lt;td&gt;164.1&lt;/td&gt;
&lt;td&gt;49.38 / 48.73 / 49.35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPZ v4&lt;/td&gt;
&lt;td&gt;62,011&lt;/td&gt;
&lt;td&gt;62,015&lt;/td&gt;
&lt;td&gt;150.7&lt;/td&gt;
&lt;td&gt;49.38 / 48.73 / 49.35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundled SOG&lt;/td&gt;
&lt;td&gt;69,537&lt;/td&gt;
&lt;td&gt;63,049&lt;/td&gt;
&lt;td&gt;144.3&lt;/td&gt;
&lt;td&gt;50.19 / 49.62 / 50.10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each median covers three fresh browser contexts on loopback, with an uncached asset request. Timing runs from navigation's performance origin until the first &lt;code&gt;renderer.render()&lt;/code&gt; and blocking &lt;code&gt;gl.finish()&lt;/code&gt;. The early submission includes module setup and shader work. One raw PLY sample took 414.1 ms, while the other two took 132.6 and 143.6 ms. These three-sample results describe the startup runs and are too sparse for a speed ranking. Brotli measurements are offline byte counts, with no network latency or server configuration in the comparison.&lt;/p&gt;

&lt;p&gt;After the scene settled, I rendered three fixed views at 768 × 768. The table compares their full 8-bit RGB images, including the background. It measures encoding differences under this renderer. The two SPZ versions gave identical RGB captures in all three views, with different on-disk sizes. Full-frame PSNR can hide small changes around leaves and other thin edges.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ht2n9ntkdpgka1hmqio.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ht2n9ntkdpgka1hmqio.png" alt="The fixed oblique view of the encoded SOG garden in Spark" width="768" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where each result applies
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Keep a full PLY master for editing and repeat exports. Check the actual splat schema when receiving a PLY from another tool.&lt;/li&gt;
&lt;li&gt;SPZ v4 saved about 78% against the source PLY in this test. This file uses Zstandard. SPZ v3 uses gzip and remains an option when a viewer needs that version.&lt;/li&gt;
&lt;li&gt;SOG bundles quantized WebP attribute images and metadata. It shrank by another 6,488 bytes under offline Brotli q11 here. Streamed SOG and remote range loading remain outside this comparison.&lt;/li&gt;
&lt;li&gt;A separate 124-splat synthetic SH3 control encoded and decoded as SPZ v4 and SOG, retaining three SH bands in splat-transform's readback. That is a structural check on a different asset. The main five-format timing and visual comparisons are SH0.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The test did not measure per-process GPU memory, CDN performance, large captures or mobile devices. Supported formats depend on both the reader and the format version. For example, PlayCanvas documents its SPZ parser separately from the Model Viewer drag-and-drop list. &lt;a href="https://github.com/nianticlabs/spz/blob/main/README.md" rel="noopener noreferrer"&gt;Niantic SPZ&lt;/a&gt;, &lt;a href="https://developer.playcanvas.com/user-manual/gaussian-splatting/formats/sog/" rel="noopener noreferrer"&gt;PlayCanvas SOG&lt;/a&gt;, &lt;a href="https://developer.playcanvas.com/user-manual/gaussian-splatting/viewing/" rel="noopener noreferrer"&gt;PlayCanvas Model Viewer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Alicia is a 3D technical artist, TA and independent developer. Codex assisted with the harness, evidence checks and editing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5a98b9g57r2g6b192rx6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5a98b9g57r2g6b192rx6.png" alt="Two matched camera angles across original PLY, compressed PLY, SPZ v4 and bundled SOG" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Related studies
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://digitalgarden.si/blog/threejs-gaussian-splat-renderer-comparison/" rel="noopener noreferrer"&gt;Five Three.js splat renderer paths&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://digitalgarden.si/tools/gaussian-splat-budget-planner/" rel="noopener noreferrer"&gt;Gaussian splat budget planner&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Data and reproduction
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://digitalgarden.si/marketing/research/splat-formats-20261105/format-report.json" rel="noopener noreferrer"&gt;format-report.json&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://digitalgarden.si/marketing/research/splat-formats-20261105/splat-formats-reproduction.zip" rel="noopener noreferrer"&gt;splat-formats-reproduction.zip&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>design</category>
      <category>software</category>
    </item>
    <item>
      <title>A Mountain River in Unreal PCG: Testing a Houdini Erosion Mask</title>
      <dc:creator>Alicia lu</dc:creator>
      <pubDate>Wed, 04 Nov 2026 03:58:03 +0000</pubDate>
      <link>https://dev.to/alicia_lu/a-mountain-river-in-unreal-pcg-testing-a-houdini-erosion-mask-3kjn</link>
      <guid>https://dev.to/alicia_lu/a-mountain-river-in-unreal-pcg-testing-a-houdini-erosion-mask-3kjn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published by Alicia on 4 November 2026 at &lt;a href="https://digitalgarden.si/blog/houdini-erosion-masks-unreal-pcg/" rel="noopener noreferrer"&gt;Digital Garden&lt;/a&gt;. Codex assisted with implementation, testing and writing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Houdini's native erosion changed a 1,008 m valley and produced a flow layer. I exported binary placement masks, brought them into Unreal 5.8.4 and ran the same native PCG grid with three controls. Of 3,844 unmasked candidates, 134 were inside the authored river corridor. A channel-only mask and a channel-plus-flow mask both reduced that number to zero. Flow removed three additional candidates near the channel for this grid and threshold.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fok17xrcin21n3qoo3qys.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fok17xrcin21n3qoo3qys.png" alt="Measured Houdini eroded heights, height changes and native flow on the 1,008 m tile" width="800" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Construct the slope, then measure erosion
&lt;/h2&gt;

&lt;p&gt;I built a 512 × 512 Houdini HeightField on a 1,008 m square, at 1.96875 m voxel spacing. A deterministic procedural surface contains a downward slope, a sinuous channel corridor and noisy valley sides. HeightField Erode 3.0 froze at frame 9. A forced cook of the erosion node took 0.871 s on this machine. Across the tile, the eroded elevation ranged from 0.84 to 185.17 m. Relative to the authored surface, height changes ranged from −28.35 to +10.77 m.&lt;/p&gt;

&lt;p&gt;The channel is defined by the authored centerline and an 18 m half-width. The combined exclusion also removes pixels within 60 m of that centerline when normalized native flow exceeds 0.45. Positive flow was divided by its 98th percentile, 7.3656 in the exported layer's units, and clipped. The channel excludes 9,378 of 262,144 source pixels. Adding the flow rule excludes 133 more. This makes the extra contribution measurable without treating the flow layer as an independent water simulation. &lt;a href="https://www.sidefx.com/docs/houdini/nodes/sop/heightfield_erode.html" rel="noopener noreferrer"&gt;SideFX HeightField Erode reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Houdini's voxel index order put its world Z in the fast axis. I reordered the native data before writing a PNG with X along columns and Z along rows. The PNG alpha carries the allowed mask. The graph reads that channel with bilinear filtering and a 0.5 threshold. The additional 505 × 505, 16-bit heightmap is a center crop intended for future Landscape import. I decoded it independently against the source heights and measured a maximum quantization error of 3.922 mm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Native PCG on the same grid
&lt;/h2&gt;

&lt;p&gt;The isolated Unreal map used 62 × 62 points at 16 m spacing. The graph was Create Points Grid → Sample Texture → Density Filter. The Get Texture Data node uses a 100,800 cm texture footprint. The Houdini XZ plane maps to Unreal XY. The mask textures were imported with sRGB disabled, no mipmaps and uncompressed data settings. All three graph variants used the same editor scene and grid.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;Candidate output points&lt;/th&gt;
&lt;th&gt;Within authored corridor&lt;/th&gt;
&lt;th&gt;Mismatches against independent bilinear check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;All allowed&lt;/td&gt;
&lt;td&gt;3,844&lt;/td&gt;
&lt;td&gt;134&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Channel exclusion&lt;/td&gt;
&lt;td&gt;3,708&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Channel and flow exclusion&lt;/td&gt;
&lt;td&gt;3,705&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each case was forced three times after reopening the saved Unreal map, and each case returned the same coordinates across its three runs. The independent comparison used the actual baseline XY positions and reconstructed sampling from the saved mask. A nearest-texel check differed at two channel-only points and three combined-mask points. The bilinear check matched all retained XY positions. The extra two channel-only removals outside the analytic corridor come from sampling the raster boundary with a threshold.&lt;/p&gt;

&lt;p&gt;The first baseline reached the editor's generation event in 424.2 ms. All eight later requests completed in 14.9–18.0 ms in the same editor session. They may reuse compiled graph work and cached data. These are request-to-event latencies, with no claim about packaged runtime, mesh rendering or per-node CPU time. Earlier null-renderer launches timed out; the successful run used offscreen D3D11. &lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/procedural-content-generation-framework-node-reference-in-unreal-engine" rel="noopener noreferrer"&gt;Epic PCG node reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fad40vi9ndkkk5dondtom.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fad40vi9ndkkk5dondtom.png" alt="Native PCG coordinates over the authored channel for the three cases; red dots mark in-channel candidates" width="800" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope
&lt;/h2&gt;

&lt;p&gt;This is an Unreal editor test of placement eligibility. Native PCG returned point coordinates on a plane at Z = 0. It did not import a Landscape, project points onto terrain, spawn production foliage, render water or run a packaged game. The exported heightmap and separate wetness mask are available for that follow-up. In this fixture, the channel mask alone was sufficient for excluding all tested in-channel points. The flow layer changed a narrow part of the surrounding placement budget.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://digitalgarden.si/marketing/research/pcg-river-20261104/pcg-river-reproduction.zip" rel="noopener noreferrer"&gt;Reproduction archive and data&lt;/a&gt;. Codex implemented the fixture, ran and audited the native tests, and drafted the article.&lt;/p&gt;

&lt;p&gt;Related study: &lt;a href="https://digitalgarden.si/blog/threejs-forest-performance-budget/" rel="noopener noreferrer"&gt;Digital Garden field note&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Raw measurement files
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://digitalgarden.si/marketing/research/pcg-river-20261104/erosion.json" rel="noopener noreferrer"&gt;erosion.json&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://digitalgarden.si/marketing/research/pcg-river-20261104/placement-summary.json" rel="noopener noreferrer"&gt;placement-summary.json&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gamedev</category>
      <category>software</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Procedural Gaussian Splatting in Houdini: Six Fits Without Photos</title>
      <dc:creator>Alicia lu</dc:creator>
      <pubDate>Wed, 04 Nov 2026 03:58:02 +0000</pubDate>
      <link>https://dev.to/alicia_lu/procedural-gaussian-splatting-in-houdini-six-fits-without-photos-49ic</link>
      <guid>https://dev.to/alicia_lu/procedural-gaussian-splatting-in-houdini-six-fits-without-photos-49ic</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published by Alicia on 4 November 2026 at &lt;a href="https://digitalgarden.si/blog/houdini-procedural-gaussian-splatting/" rel="noopener noreferrer"&gt;Digital Garden&lt;/a&gt;. Codex assisted with implementation, testing and writing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I rendered a small procedural valley in Houdini and trained six Gaussian splats on the resulting synthetic images. With the same 4,096 splats and 600 training steps, going from 12 to 24 training views improved held-out quality for both seeds. Going from 24 to 36 gave little extra full-frame PSNR and slightly lower foreground PSNR in both runs. This is a result for one 192 × 192 scene and one fixed-capacity trainer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbt3noh4q8d52lblcojxf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbt3noh4q8d52lblcojxf.png" alt="Karma targets alongside 12-, 24- and 36-view predictions for two held-out cameras" width="799" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Make a scene and keep the cameras honest
&lt;/h2&gt;

&lt;p&gt;The test scene is a small valley diorama with a pool, rocks and eight trees. A Houdini 22.0.429 hython script built a USD stage from procedural primitives, set materials and lights, and saved a HIP with a relative reference to the source USD. Karma CPU produced 44 RGBA EXRs at 192 × 192, with 12 samples per pixel and motion blur disabled.&lt;/p&gt;

&lt;p&gt;Training used the first 12 cameras, then a nested 24-camera set, then all 36 training cameras. The extra eight cameras use offset azimuths and alternating elevations. They were used only to render ground truth and score the finished fits. Initialization sampled random centers in a broad scene-sized box. The fitter received no geometry points, depth, or pixels from the eight held-out views. The scene's approximate dimensions and camera aim were authored and shared across the trial.&lt;/p&gt;

&lt;p&gt;The renderer output was composited over a fixed background in render-linear RGB, then clipped and passed through a fixed gamma curve. It defines the training target for these numbers. It is separate from the native Karma viewer's display transform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six GPU fits at fixed capacity
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Training views&lt;/th&gt;
&lt;th&gt;Seed&lt;/th&gt;
&lt;th&gt;Full-frame PSNR (dB)&lt;/th&gt;
&lt;th&gt;Foreground PSNR (dB)&lt;/th&gt;
&lt;th&gt;SSIM&lt;/th&gt;
&lt;th&gt;Fit wall time (s)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;381&lt;/td&gt;
&lt;td&gt;32.55&lt;/td&gt;
&lt;td&gt;28.92&lt;/td&gt;
&lt;td&gt;0.92114&lt;/td&gt;
&lt;td&gt;13.039&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;381&lt;/td&gt;
&lt;td&gt;35.80&lt;/td&gt;
&lt;td&gt;33.26&lt;/td&gt;
&lt;td&gt;0.94525&lt;/td&gt;
&lt;td&gt;1.663&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;381&lt;/td&gt;
&lt;td&gt;35.97&lt;/td&gt;
&lt;td&gt;33.21&lt;/td&gt;
&lt;td&gt;0.94670&lt;/td&gt;
&lt;td&gt;1.717&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;719&lt;/td&gt;
&lt;td&gt;32.18&lt;/td&gt;
&lt;td&gt;28.51&lt;/td&gt;
&lt;td&gt;0.91714&lt;/td&gt;
&lt;td&gt;1.337&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;719&lt;/td&gt;
&lt;td&gt;35.71&lt;/td&gt;
&lt;td&gt;33.18&lt;/td&gt;
&lt;td&gt;0.94457&lt;/td&gt;
&lt;td&gt;1.443&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;719&lt;/td&gt;
&lt;td&gt;35.82&lt;/td&gt;
&lt;td&gt;33.11&lt;/td&gt;
&lt;td&gt;0.94539&lt;/td&gt;
&lt;td&gt;1.596&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The workstation ran Torch 2.9.1+cu128 and gsplat 1.5.3 on an RTX 5070 Ti. Each fit optimized positions, scales, rotations, opacity and a view-independent RGB value, with no point splitting or pruning. Loss gave equal weight to full-frame and alpha-masked absolute error. Metrics are means of eight per-image scores. Foreground scores exclude target pixels with alpha at or below 0.5. SSIM uses a Gaussian 11 × 11 window.&lt;/p&gt;

&lt;p&gt;The first fit had a much larger wall time than subsequent fits. Those times include initial work in the shared Python process and do not isolate a camera-count speed effect. Peak Torch allocations were 22.45–22.46 MiB and peak Torch reservations 26 MiB for this tiny scene, including cached target tensors. They do not report total driver or GPU residency. The 12-step pilot is separate from the six fits.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdooq5atid5zkalbo43a7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdooq5atid5zkalbo43a7.png" alt="Full-frame and foreground PSNR against training-view count for two seeds" width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser delivery and collision
&lt;/h2&gt;

&lt;p&gt;I preselected the 36-view, seed-381 output for delivery. The binary PLY is 278,942 bytes and the SPZ v3 is 64,915 bytes, 76.7% smaller. Both decoded as 4,096 splats and reopened in Spark 2.4.0 on NVIDIA WebGL2. Browser screenshots retain visible blur and residual floaters. The numeric quality table describes the trainer's renders at 192 × 192. It does not score the browser's differently sized render or the format conversion.&lt;/p&gt;

&lt;p&gt;The invisible Three.js proxy reproduces the base slab of the authored USD using a 2.64 × 0.26 × 2.26 metre box. Three downward rays inside its footprint hit the top at y = 0 within 4.77 × 10⁻⁹ m. Two outside rays missed. Both PLY and SPZ runs gave the same result because the rays tested the separate mesh. The test covers the slab only, with no physics solver, full collision mesh or relighting claim.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr1p4xmejeyt77mpv33fo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr1p4xmejeyt77mpv33fo.png" alt="Spark WebGL2 screenshot of the SPZ fit; blur and residual floaters remain visible" width="768" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://digitalgarden.si/blog/houdini-22-gaussian-splats-threejs/" rel="noopener noreferrer"&gt;previous Houdini native export study&lt;/a&gt; covers GSplat SOP and format transport. This study instead varies synthetic views and separates visual display from collision. &lt;a href="https://sparkjs.dev/docs/splat-mesh/" rel="noopener noreferrer"&gt;Spark SplatMesh reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://digitalgarden.si/marketing/research/procedural-splats-20261104/procedural-splats-reproduction.zip" rel="noopener noreferrer"&gt;Reproduction archive and raw evidence&lt;/a&gt;. Codex implemented and ran the experiment and drafted the article.&lt;/p&gt;

&lt;p&gt;Related study: &lt;a href="https://digitalgarden.si/blog/threejs-gaussian-splat-renderer-comparison/" rel="noopener noreferrer"&gt;Digital Garden field note&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Raw measurement files
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://digitalgarden.si/marketing/research/procedural-splats-20261104/training.json" rel="noopener noreferrer"&gt;training.json&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Three.js Gaussian Splats: Gaussian-Splat-Lite, Spark and Radix on One Desktop</title>
      <dc:creator>Alicia lu</dc:creator>
      <pubDate>Wed, 04 Nov 2026 03:57:57 +0000</pubDate>
      <link>https://dev.to/alicia_lu/threejs-gaussian-splats-gaussian-splat-lite-spark-and-radix-on-one-desktop-3i35</link>
      <guid>https://dev.to/alicia_lu/threejs-gaussian-splats-gaussian-splat-lite-spark-and-radix-on-one-desktop-3i35</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published by Alicia on 4 November 2026 at &lt;a href="https://digitalgarden.si/blog/threejs-gaussian-splat-renderer-comparison/" rel="noopener noreferrer"&gt;Digital Garden&lt;/a&gt;. Codex assisted with implementation, testing and writing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I compared Gaussian-Splat-Lite 1.6.2, Spark 2.4.0 and Radix 0.0.1 using the same fixed SPZ file at each size, with five hardware-backed paths on an RTX 5070 Ti. Spark on r187 reached the first submitted-work wait sooner than the other paths at both sizes. The warm camera orbit paced near 60 Hz in every group. That cadence cannot rank GPU throughput.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fibq8a9lmu5zld1ncgs5u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fibq8a9lmu5zld1ncgs5u.png" alt="Gaussian-Splat-Lite WebGPU showing the 2M synthetic SH0 fixture and the same opaque cube" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What was actually shared
&lt;/h2&gt;

&lt;p&gt;The two SPZ v3 files contained 500,000 and 2,000,000 view-independent splats from the same seeded, synthetic flower-bed generator. File sizes were 6,443,429 and 25,759,887 bytes. Every path decoded the expected count and rendered the same opaque cube, camera, 1280 × 720 output and orbit. This is an owned stress fixture, separate from a photographic scan and the live garden.&lt;/p&gt;

&lt;p&gt;I used Three.js 0.187.0 for Gaussian-Splat-Lite and the main Spark run. The first Radix pilot on r187 hit Three.js's compute dispatch size error. Radix 0.0.1 then ran with its compatible 0.186.1 setup. An extra Spark 2.4.0 path also ran on 0.186.1. The derived local bundles for the two r186 paths only redirect Three.js imports to the pinned alias and remove source-map references. The compatibility failure is preserved. &lt;a href="https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite" rel="noopener noreferrer"&gt;Gaussian-Splat-Lite repository&lt;/a&gt; · &lt;a href="https://sparkjs.dev/docs/splat-mesh/" rel="noopener noreferrer"&gt;Spark&lt;/a&gt; · &lt;a href="https://github.com/sparkjsdev/radix" rel="noopener noreferrer"&gt;Radix&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup and frame pacing
&lt;/h2&gt;

&lt;p&gt;The clean repeat ran 30 fresh contexts in one Chrome 156.0.8078.80 process, with three contexts per size and path and rotating order. WebGPU reported NVIDIA Blackwell, and WebGL2 reported the RTX 5070 Ti through ANGLE D3D11. Antialiasing was disabled and the pixel ratio was 1. Each run included 45 warm-up frames, then 120 recorded requestAnimationFrame intervals during the orbit. The earlier run had an unclassified 404 console entry in one context and overlapped the start of the Unreal task. It stays separate from the clean repeat.&lt;/p&gt;

&lt;p&gt;Startup measures module execution after the common dynamic Three.js import through the first render call and completion wait for submitted work. It includes backend setup and renderer-specific imports and loading. It stops at WebGPU queue completion or WebGL finish. Any asynchronous sort can still be pending. The later capture and full warmed orbit verify the decoded scene separately.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Renderer path&lt;/th&gt;
&lt;th&gt;Three.js&lt;/th&gt;
&lt;th&gt;0.5M first wait (ms)&lt;/th&gt;
&lt;th&gt;2M first wait (ms)&lt;/th&gt;
&lt;th&gt;0.5M warm p95 (ms)&lt;/th&gt;
&lt;th&gt;2M warm p95 (ms)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gaussian-Splat-Lite WebGPU&lt;/td&gt;
&lt;td&gt;0.187.0&lt;/td&gt;
&lt;td&gt;351.2&lt;/td&gt;
&lt;td&gt;847.4&lt;/td&gt;
&lt;td&gt;16.7&lt;/td&gt;
&lt;td&gt;16.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gaussian-Splat-Lite WebGL2 backend&lt;/td&gt;
&lt;td&gt;0.187.0&lt;/td&gt;
&lt;td&gt;250.0&lt;/td&gt;
&lt;td&gt;648.9&lt;/td&gt;
&lt;td&gt;16.8&lt;/td&gt;
&lt;td&gt;16.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spark WebGL2&lt;/td&gt;
&lt;td&gt;0.187.0&lt;/td&gt;
&lt;td&gt;227.7&lt;/td&gt;
&lt;td&gt;475.3&lt;/td&gt;
&lt;td&gt;16.8&lt;/td&gt;
&lt;td&gt;16.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Radix WebGPU&lt;/td&gt;
&lt;td&gt;0.186.1&lt;/td&gt;
&lt;td&gt;767.1&lt;/td&gt;
&lt;td&gt;1,106.2&lt;/td&gt;
&lt;td&gt;16.7&lt;/td&gt;
&lt;td&gt;16.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spark WebGL2 control&lt;/td&gt;
&lt;td&gt;0.186.1&lt;/td&gt;
&lt;td&gt;254.3&lt;/td&gt;
&lt;td&gt;515.7&lt;/td&gt;
&lt;td&gt;16.8&lt;/td&gt;
&lt;td&gt;16.8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cells report the median of three run-level measurements. All ten groups had a 16.7 ms median and 16.8 ms p99, rounded to one decimal. The only intervals above 25 ms were two in the 500,000-splat Gaussian-Splat-Lite WebGL2 group. At that size its first-wait values also ranged from 246.8 to 572.8 ms. Fresh contexts do not clear the browser process, GPU driver or server transform caches. The startup table is a local observation, not a cold network benchmark.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2dbvfcvdj7l9xoknzqc9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2dbvfcvdj7l9xoknzqc9.png" alt="Startup first-submitted-work wait and warm p95 rAF per renderer and file size" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope and what I would test next
&lt;/h2&gt;

&lt;p&gt;These are renderer paths as configured, with different sort and upload implementations. No measured GPU timestamps, total VRAM, sort latency or pixel-accuracy ranking is inferred from rAF. All runs used fully loaded SPZ, so the table says nothing about paged RAD, SOG or LCC2 streaming. The raw renderer counters and per-frame intervals are in the archive.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftjnbgqrx0y4cfmnra3k5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftjnbgqrx0y4cfmnra3k5.png" alt="Radix WebGPU capture of the same 2M file on the r186 path" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://digitalgarden.si/marketing/research/splat-renderers-20261104/splat-renderers-reproduction.zip" rel="noopener noreferrer"&gt;Reproduction archive with deterministic fixture generator and raw evidence&lt;/a&gt;. The &lt;a href="https://digitalgarden.si/blog/spark-radix-vs-spark-webgpu-benchmark/" rel="noopener noreferrer"&gt;earlier Spark and Radix study&lt;/a&gt; used a separate protocol and remains a separate result. Codex implemented the harness, ran the tests and drafted this article.&lt;/p&gt;

&lt;p&gt;Related study: &lt;a href="https://digitalgarden.si/blog/houdini-procedural-gaussian-splatting/" rel="noopener noreferrer"&gt;Digital Garden field note&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Raw measurement files
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://digitalgarden.si/marketing/research/splat-renderers-20261104/runs.json" rel="noopener noreferrer"&gt;runs.json&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://digitalgarden.si/marketing/research/splat-renderers-20261104/summary.json" rel="noopener noreferrer"&gt;summary.json&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
