<?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: 有点质量</title>
    <description>The latest articles on DEV Community by 有点质量 (@ian_liu_27ecc3baa5df2eee0).</description>
    <link>https://dev.to/ian_liu_27ecc3baa5df2eee0</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%2F4170640%2F76ef025f-afbd-4636-9bdf-9078b50fdad0.png</url>
      <title>DEV Community: 有点质量</title>
      <link>https://dev.to/ian_liu_27ecc3baa5df2eee0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ian_liu_27ecc3baa5df2eee0"/>
    <language>en</language>
    <item>
      <title>25 Million Gaussians: Loading Detail While You Explore</title>
      <dc:creator>有点质量</dc:creator>
      <pubDate>Sun, 25 Oct 2026 11:55:04 +0000</pubDate>
      <link>https://dev.to/ian_liu_27ecc3baa5df2eee0/25-million-gaussians-loading-detail-while-you-explore-13n3</link>
      <guid>https://dev.to/ian_liu_27ecc3baa5df2eee0/25-million-gaussians-loading-detail-while-you-explore-13n3</guid>
      <description>&lt;p&gt;The model we were integrating came as a PLY file of about 1.76 GB, containing 25,842,067 Gaussians. We previously wrote about the &lt;a href="https://zhuanlan.zhihu.com/p/2093845969829892764" rel="noopener noreferrer"&gt;stalls and blur&lt;/a&gt; we investigated once it was rendering. This time, I want to take a step back: what does the browser receive first, and how does it fill in more detail as the view changes?&lt;/p&gt;

&lt;p&gt;For the close-up test, we set a selection budget of 2.5 million Gaussians. The source contained more than 25 million, while the current selection contained only a few million. We had not simply deleted the rest. Making that work required preparation before the browser ever opened the model.&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%2Fou865d0lrw6bo19ao2z3.jpg" 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%2Fou865d0lrw6bo19ao2z3.jpg" alt="Overview of a public quarry example, showing stepped terrain and haul roads" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. The images in this article show the Gemmingen quarry example from our &lt;a href="https://slim3d-dev-cdn.wuyaotech.com/demo/huagang-copper-mine-0.3.11-0c2c0cb70cb9/index.html" rel="noopener noreferrer"&gt;public demo&lt;/a&gt;, published by diddlkiller and rendered through the SuperSplat viewer. &lt;a href="https://superspl.at/scene/392aab81" rel="noopener noreferrer"&gt;Original scene&lt;/a&gt;. The 25.84 million source Gaussians and loading measurements discussed below belong to a different project model. These images illustrate the appearance of a scene.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prepare data that can be used in parts
&lt;/h2&gt;

&lt;p&gt;We did not simply split the original PLY into small files and tell the browser to read them in order. The order of records in a file does not tell a viewer what matters from a particular position, or which details can wait.&lt;/p&gt;

&lt;p&gt;We first used &lt;a href="https://sparkjs.dev/docs/lod-getting-started/" rel="noopener noreferrer"&gt;Spark's conversion tools&lt;/a&gt; on the server to generate levels of detail, or LOD, and organize them into paged RAD assets for our native WebGPU viewer. Coarse nodes summarize an area, while nodes below them retain finer detail. The viewer can start with a coarse representation and refine the regions that matter for the current view.&lt;/p&gt;

&lt;p&gt;The converted main model contained 581 pages, totaling about 722 MB. That is the size of the complete main asset, not the amount that must be downloaded to open one camera view. The original PLY still had to be downloaded and converted in full as part of this workflow.&lt;/p&gt;

&lt;p&gt;Another number is easy to misread: the resulting tree contained about 38.04 million nodes, more than the source model's 25.84 million Gaussians. The extra nodes represent different levels of detail; they are not all Gaussians that need to be drawn at the same time. Source count, total tree-node count, and current selection count describe three different things.&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%2Fotmf7rpt47mhomchzb2e.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%2Fotmf7rpt47mhomchzb2e.png" alt="Flow from the original PLY through LOD conversion to view-dependent page selection and rendering updates in the browser" width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. The data flow used in this integration. The upper half happens before the viewer opens; the lower half continues as the view changes. Source count, tree-node count, and selection budget refer to different stages.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Show a coarse view, then prepare more detail
&lt;/h2&gt;

&lt;p&gt;When opening the asset, the viewer first reads the page directory to find the data, then fetches the page containing the root nodes. It can display the coarse data already available without waiting for all 581 pages.&lt;/p&gt;

&lt;p&gt;The directory records each page's location and length. Pages can live in a single file, with HTTP Range requests retrieving specific byte ranges, or in separate RADC files. We used separate chunks in this case. The viewer fetches the required file and checks the response length, so a request intended to read one page cannot silently consume an entire large file.&lt;/p&gt;

&lt;p&gt;Camera position, orientation, canvas size, and the count budget then guide selection. A distant region that occupies few pixels can use a coarser representation. As the camera moves closer, finer nodes become useful, and selection identifies the pages that are still missing.&lt;/p&gt;

&lt;p&gt;We request those pages, decode them, and select again. New data may unlock another level of refinement, so this proceeds in rounds. Once the current selection has the data it needs, we pack it for the GPU, upload it, and update rendering. A completed network request does not, by itself, put more detail on screen.&lt;/p&gt;

&lt;p&gt;While the next set of data is being prepared, the current resources remain available for drawing. The replacement takes over only when it is fully ready. Camera interaction and preparation can therefore continue without clearing the scene and rebuilding it from an empty frame.&lt;/p&gt;

&lt;p&gt;In this test, 2.5 million was the limit for the current selection. That selection can contain nodes from several detail levels; it is not the first 2.5 million records from the original file. A different camera position changes which details are worth keeping. Pages no longer involved in the selection can be released and fetched again later if needed.&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%2F2xomqz9eqbdntsyamb4k.jpg" 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%2F2xomqz9eqbdntsyamb4k.jpg" alt="Detail view of the public quarry example, showing rock faces, ramps, and terrain" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 3. A view of the same public quarry example, showing rock faces, ramps, and haul roads. When viewed up close, these features occupy more screen space, which is why the viewer needs to choose detail according to the view.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A short piece of pseudocode shows the steps between data arriving and the rendered image changing:&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;let&lt;/span&gt; &lt;span class="nx"&gt;selected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ready&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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;let&lt;/span&gt; &lt;span class="nx"&gt;round&lt;/span&gt; &lt;span class="o"&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;round&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;pageCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;round&lt;/span&gt;&lt;span class="o"&gt;++&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;cut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;selected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cut&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;selected&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;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cut&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wantedPages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;resident&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ready&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;break&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;id&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchAndDecodePage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;resident&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;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;LOD selection did not converge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;buildPass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aborted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;return&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;old&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;pass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;old&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pseudocode omits scheduling and error handling. &lt;code&gt;buildPass&lt;/code&gt; includes packing and preparing GPU resources. After each round of fetching, selection runs again until no pages are missing. Exceeding a round limit based on the page count raises an error rather than waiting indefinitely. The old resources keep drawing during selection, requests, and uploads; they are replaced only when the new resources are complete. If preparation fails, the old resources remain, and the unfinished update is kept for a retry.&lt;/p&gt;

&lt;p&gt;The count budget is not a memory cap. Downloaded pages, decoded data, resources being prepared, and resources currently in use all occupy memory. Limiting the drawn count does not remove the need to manage their residency and release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loading ended, but refinement had not
&lt;/h2&gt;

&lt;p&gt;Early in the integration, a state flag misled us. Loading had become &lt;code&gt;false&lt;/code&gt;, so the test assumed refinement was complete, checked the image, and failed.&lt;/p&gt;

&lt;p&gt;The root nodes were ready, but the next camera-driven refinement was still queued. An idle downloader did not mean all the work for the current view had finished.&lt;/p&gt;

&lt;p&gt;We added queued LOD updates, selection in progress, and resource rebuilding to the busy state. Our checks also followed the work further: had the selected data actually reached rendering, and had the image changed accordingly?&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%2Fibs5groveku9lkn0g2rt.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%2Fibs5groveku9lkn0g2rt.png" alt="Refinement remains busy while updates are queued, pages are selected and fetched, or resources are rebuilt; committed counts change only after the new pass takes over" width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 4. A coarse view may already be visible while refinement is still queued. Loading state needs to include that follow-up work. Completing selection also does not mean the new resources have reached rendering.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We therefore distinguish between having an initial image and finishing refinement for the current view. Moving the camera creates another round of demand. In a streaming viewer, a statement that loading is complete needs to say which view it applies to.&lt;/p&gt;

&lt;h2&gt;
  
  
  After a failure, state must not get ahead of the image
&lt;/h2&gt;

&lt;p&gt;Breaking loading into stages also creates several places where it can fail. A page may have arrived and selection may have changed when a later request fails. Or the CPU may have prepared new data, but building the GPU resources fails.&lt;/p&gt;

&lt;p&gt;We encountered a case where the decoder remembered a new selection while rendering still used the old data. On retry, asking only whether the selection had changed could return no change. The selected data would then never be sent back to the rendering side.&lt;/p&gt;

&lt;p&gt;A retry after failure must therefore be able to retrieve the current selection again, and unfinished updates must remain pending. The displayed count must describe data successfully handed over for rendering, not data still being prepared.&lt;/p&gt;

&lt;p&gt;The following pseudocode puts the decoder and rendering states together. &lt;code&gt;refine&lt;/code&gt; represents the selection-and-fetch loop above. &lt;code&gt;forceOutput&lt;/code&gt; tells it to return data even if the selection has not changed:&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;selected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;refine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;forceOutput&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;forceOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;forceOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;dirty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;dirty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;buildPass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aborted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;return&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;old&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;pass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;old&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;committedCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;dirty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aborted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;scheduleRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;view&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;&lt;code&gt;data&lt;/code&gt; retains the selected data, and &lt;code&gt;dirty&lt;/code&gt; records that it has not yet been committed. Even if the next selection is unchanged, that rebuild still needs to finish. &lt;code&gt;committedCount&lt;/code&gt; changes only after the new pass replaces the old one. Forcing output restores the data; scheduling a retry makes another attempt happen. Both are necessary.&lt;/p&gt;

&lt;p&gt;If the user leaves the scene during this work, any late result must also be released when the task is cancelled.&lt;/p&gt;

&lt;p&gt;These measures may not make the first load faster. They determine whether the viewer can still function after repeated scene changes and failed attempts.&lt;/p&gt;

&lt;h2&gt;
  
  
  After a two-minute stall, check whether the bytes arrived
&lt;/h2&gt;

&lt;p&gt;When we switched to the complete model, the first remote test stalled again. The scene was ready, but waiting for refinement timed out after 120 seconds. There were no GPU or page errors, and only a small number of page requests had been issued.&lt;/p&gt;

&lt;p&gt;Further investigation showed that the private tunnel used by the test was very slow. An independent copy measured about 34.5 KB per second. A response header reporting a file's size tells us how many bytes to expect, not how many the browser has received.&lt;/p&gt;

&lt;p&gt;To see whether data is still arriving, we can count the bytes read from the response body. This standalone observation snippet uses browser APIs:&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;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`HTTP &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Missing response body&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;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;receivedBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;while &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="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&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="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;receivedBytes&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteLength&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;receivedBytes&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
  &lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;releaseLock&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;&lt;code&gt;receivedBytes&lt;/code&gt; measures only the response body read so far. A chunk returned by &lt;code&gt;read()&lt;/code&gt; is not a RAD page, and receiving it does not mean it has been decoded or displayed. Selection and upload still need to be checked separately.&lt;/p&gt;

&lt;p&gt;We did not relax the refinement timeout or reduce the point count. Instead, we transferred the complete derived assets to the local machine, verified that the bytes matched, and reran the test with the same parameters. Overview, close-up, visibility, and resource-release checks passed in a real browser, giving us evidence to distinguish the earlier wait from a rendering failure.&lt;/p&gt;

&lt;p&gt;That changed how we thought about loading while viewing. Paging lets the browser use a subset of the data first. LOD lets the current view represent a scene with fewer nodes. Neither delivers missing pages on behalf of the network or does the GPU's rendering work. The complete model could be viewed up close in that run, but interaction performance still needed further work.&lt;/p&gt;

&lt;p&gt;When a large model takes too long to become detailed, we now follow the actual sequence: have the required pages arrived, has selection finished, and has the new rendering data taken over? Those questions give us a more concrete next step than repeatedly changing the point budget.&lt;/p&gt;

</description>
      <category>webgpu</category>
      <category>performance</category>
      <category>graphics</category>
    </item>
    <item>
      <title>Combining Gaussian Splats and BIM in One Scene: What We Ran Into</title>
      <dc:creator>有点质量</dc:creator>
      <pubDate>Sun, 25 Oct 2026 11:51:27 +0000</pubDate>
      <link>https://dev.to/ian_liu_27ecc3baa5df2eee0/combining-gaussian-splats-and-bim-in-one-scene-what-we-ran-into-5613</link>
      <guid>https://dev.to/ian_liu_27ecc3baa5df2eee0/combining-gaussian-splats-and-bim-in-one-scene-what-we-ran-into-5613</guid>
      <description>&lt;p&gt;Our BIM viewer originally dealt with walls, beams, equipment, and pipes. These objects had geometry, but also element IDs, properties, and a hierarchy. Adding Gaussian splatting introduced a very different kind of data: it could show what a site looked like, without automatically becoming a collection of engineering components whose properties we could query.&lt;/p&gt;

&lt;p&gt;Putting the two together was appealing. A reconstruction from photographs could provide the surroundings and a sense of the space, while BIM retained the component information in the design model. Once we started integrating them, though, the work quickly went beyond loading another file format.&lt;/p&gt;

&lt;p&gt;An earlier article covered &lt;a href="https://zhuanlan.zhihu.com/p/2093845969829892764" rel="noopener noreferrer"&gt;stalls and blur in Gaussian rendering&lt;/a&gt;. This one looks back at the initial integration into our native WebGPU viewer: how we aligned coordinates, established occlusion, framed the whole scene, and decided which existing component operations still applied.&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%2F89299tsfqawpyug2a6sp.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%2F89299tsfqawpyug2a6sp.png" alt="Knock Community Hall in the native WebGPU viewer, with roof tiles, brick walls, and trees represented by Gaussians" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. The kind of data we were adding: the public Knock Community Hall sample rendered in our viewer, retaining 1,935,275 Gaussians. The model was published by &lt;a href="https://superspl.at/scene/0ff2e6dc" rel="noopener noreferrer"&gt;scbenoit (SteveB)&lt;/a&gt; under &lt;a href="https://creativecommons.org/licenses/by/4.0/" rel="noopener noreferrer"&gt;CC BY 4.0&lt;/a&gt;. We decoded SOG to PLY before rendering it. This image shows only the Gaussian input; the later images use a separate synthetic scene to test occlusion between Gaussians and BIM.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Align the coordinate conventions first
&lt;/h2&gt;

&lt;p&gt;A BIM file and a Gaussian file can each display correctly on their own and still fail to line up when placed in the same scene.&lt;/p&gt;

&lt;p&gt;Gaussian data often uses local coordinates established during reconstruction. An engineering model has its own origin, orientation, and units. Sharing a camera cannot remove those differences. We first need to establish which direction each axis represents and how much physical distance one unit corresponds to.&lt;/p&gt;

&lt;p&gt;Our integration path first converts the source Gaussian coordinates to the viewer's Z-up convention, then applies the scene member's placement matrix. Translation, rotation, and scale are expressed at this layer, so the original Gaussian file can remain unchanged.&lt;/p&gt;

&lt;p&gt;The integration layer therefore places the data using a supplied transform. The registration between the two models still has to be determined beforehand. Where the same wall corner lies in each dataset, and whether their physical scales match, are questions that do not disappear just because both files have loaded.&lt;/p&gt;

&lt;p&gt;For engineering scenes with large world coordinates, we also subtract the same local origin in both rendering paths, letting GPU calculations operate on smaller relative coordinates. If each path chooses its own origin without carrying that difference through the pipeline, the two datasets still will not occupy the same space.&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%2Fu8h90t87o6z792atcw5q.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%2Fu8h90t87o6z792atcw5q.png" alt="BIM and Gaussians use aligned coordinates and a shared camera, then share opaque depth before their colors are composited" width="800" height="531"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. The main relationships in a mixed frame. Opaque depth produced by BIM participates in Gaussian rendering, and the colors from the two paths are composited.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Sharing a camera also means sharing depth
&lt;/h2&gt;

&lt;p&gt;The next task was to make the front-to-back relationships work.&lt;/p&gt;

&lt;p&gt;Suppose a group of Gaussians sits behind an opaque wall. The wall should hide it. When the camera moves to the other side and the Gaussians are in front, they should become visible again. Simply stacking two rendered images in a fixed order cannot guarantee that relationship.&lt;/p&gt;

&lt;p&gt;We let BIM write the depth of opaque objects first. Gaussian rendering then uses that depth to determine whether a fragment lies behind geometry already drawn. Both paths need the same camera, canvas dimensions, and depth convention for those comparisons to mean the same thing.&lt;/p&gt;

&lt;p&gt;We checked this with a deliberately simple synthetic scene: a gray wall as the BIM occluder, with red Gaussians on one side. Simple shapes made it harder for complex textures to hide an error.&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%2Fxztfv8jwzm74x3m85160.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%2Fxztfv8jwzm74x3m85160.png" alt="In the synthetic test, red Gaussians are visible in front of the gray BIM occluder" width="640" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 3. A screenshot from the running test. The red region consists of Gaussians; the gray region is opaque BIM test geometry. From this side, the Gaussians are visible.&lt;/em&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%2Fk06xk9s2t5kgvzytrwud.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%2Fk06xk9s2t5kgvzytrwud.png" alt="From the other side of the same test scene, the gray BIM occluder hides the red Gaussians behind it" width="640" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 4. Looking from the other side hides the Gaussians. Hiding the gray occluder in the test makes the red Gaussians reappear. Figures 3 and 4 both show the synthetic validation scene.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The Gaussians also have their own rendering requirements. &lt;a href="https://repo-sam.inria.fr/fungraph/3d-gaussian-splatting/" rel="noopener noreferrer"&gt;3D Gaussian Splatting&lt;/a&gt; represents a scene using Gaussians with opacity, whose projected contributions must be blended in order. We put the visible Gaussian members into one sorting process and draw them from back to front, so the result does not depend on the order in which models were loaded.&lt;/p&gt;

&lt;p&gt;The Gaussians participate in depth testing, but do not write to that opaque depth buffer. If a translucent Gaussian with a soft edge claimed depth as though it were a solid wall, it could incorrectly hide later contributions.&lt;/p&gt;

&lt;p&gt;The key steps of a frame can be reduced to this pseudocode:&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;const&lt;/span&gt; &lt;span class="nx"&gt;camera&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bimColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;drawOpaqueBim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;camera&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;ordered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sortBackToFront&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;visibleSplats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;camera&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;splatColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;drawSplats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;depthAttachment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;depthLoadOp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;load&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;depthCompare&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;less-equal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;depthWriteEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;blend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;premultiplied-alpha&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;compositeOver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bimColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;splatColor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This leaves out GPU resource creation and antialiasing configuration. It keeps the three relationships that matter here: a shared camera, existing depth, and alpha blending.&lt;/p&gt;

&lt;p&gt;At this point, opaque BIM could correctly occlude the Gaussians. Transparent components such as glass needed separate handling. They also participate in alpha blending, and sharing opaque depth alone cannot resolve their ordering with Gaussians. That integration did not yet implement a global transparent sort covering both glass and Gaussians.&lt;/p&gt;

&lt;h2&gt;
  
  
  Framing the whole scene must include the Gaussians
&lt;/h2&gt;

&lt;p&gt;Once the images could be combined, some previously routine features became worth checking again.&lt;/p&gt;

&lt;p&gt;One example was the command to frame the entire scene. With BIM alone, computing the camera position from the BIM bounds was sufficient. After adding Gaussian members, the scene bounds needed to include them as well. Otherwise, both datasets could be loaded while the camera continued to frame only one of them.&lt;/p&gt;

&lt;p&gt;We built a test that placed the Gaussians far from the BIM geometry and then checked the full scene bounds. This forced us to include Gaussian bounds in camera framing and near/far clipping calculations, rather than merely checking that they displayed correctly near the origin.&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%2Fspvmfnmzme5qcqd6a460.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%2Fspvmfnmzme5qcqd6a460.png" alt="Top-down diagram of separate BIM and Gaussian bounds, with a union enclosing both for whole-scene framing" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 5. A diagram of the combined bounds, not a rendering screenshot. The green dashed rectangle encloses both types of data. Framing only the BIM on the left could leave the Gaussians on the right outside the view.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The combined bounds also have a specific purpose. The camera needs to know the size of the whole scene, while BIM culling and shadow calculations retain their own bounds. Adding distant Gaussian data does not mean that every BIM calculation should expand to cover it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rendering together does not make the interaction capabilities identical
&lt;/h2&gt;

&lt;p&gt;At the interaction layer, it was easy to make another assumption: if everything on screen is 3D, should clicking it open properties, allow hiding by floor, or support distance measurements?&lt;/p&gt;

&lt;p&gt;In our integration path, the answer depends on the data type.&lt;/p&gt;

&lt;p&gt;A BIM element ID can connect geometry to properties and a hierarchy. Ordinary Gaussian data contains rendering information such as position, shape, color, and opacity. It does not inherently carry a business identity such as "this is a beam" or "this belongs to the third floor." A wall that looks continuous in the image is not necessarily a geometric surface available for calculations.&lt;/p&gt;

&lt;p&gt;We therefore manage Gaussians as independent scene members. They can have an initial placement, be shown or hidden as a whole, and participate in scene framing. Element picking, property queries, and hierarchy operations continue to come from BIM data. Measurement and sectioning of Gaussian surfaces did not automatically arrive with the ability to display them.&lt;/p&gt;

&lt;p&gt;The API needs to express that difference too. The application configures its actions from the capabilities returned by each member, rather than enabling every button whenever it sees 3D content:&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;actionsFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;member&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;caps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;toggleVisible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;caps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;supported&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pickElement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;caps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;picking&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;supported&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;showProperties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;caps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;supported&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;browseHierarchy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;caps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hierarchy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;supported&lt;/span&gt;&lt;span class="p"&gt;,&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;For the Gaussian members described here, whole-member visibility is supported, while the other three component operations are not. A capability flag says that the interface supports an operation; it does not guarantee that a particular BIM file contains complete properties or hierarchy data.&lt;/p&gt;

&lt;p&gt;This gives the two data types a clear division of work: Gaussians help people understand the site's appearance, while BIM anchors operations to identifiable components. Connecting a region in the captured site to a specific piece of equipment will require a data correspondence, beyond further changes to rendering code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scene switching is part of a complete integration
&lt;/h2&gt;

&lt;p&gt;The final part of the work often stays outside the screenshot.&lt;/p&gt;

&lt;p&gt;Loading and refining Gaussians creates asynchronous tasks. A user may enter a scene and immediately leave it, or destroy the entire viewer while new data is being uploaded. Requests already in flight, decoded results, and GPU resources all need a clear owner.&lt;/p&gt;

&lt;p&gt;We tie loading, decoding, and rendering resources to the scene session's lifecycle. Disposing the scene cancels its tasks. Late results must be released even if they have already created GPU resources, and must not re-enter a scene the user has left. If initialization fails partway through, buffers allocated earlier also need to be reclaimed. Member visibility is handled separately: hiding is not disposal.&lt;/p&gt;

&lt;p&gt;During refinement, we keep the current drawable resources until the next set is fully ready, then replace them. The content already on screen can keep rendering while preparation continues. If building the new resources fails, the current image and statistics should still correspond to the batch that was successfully committed.&lt;/p&gt;

&lt;p&gt;The following example expands one replacement. Each scene session builds new resources serially, while the old resources continue rendering:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;replacePass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;candidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;candidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;buildPass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disposed&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aborted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setVisibility&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;memberVisibility&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;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;committedCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;candidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&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;If &lt;code&gt;buildPass&lt;/code&gt; fails internally, it must release the resources it has already allocated. If the build succeeds after the scene has been left, &lt;code&gt;finally&lt;/code&gt; releases the unused candidate. Ownership of that candidate passes to the scene only when it successfully replaces the old resources.&lt;/p&gt;

&lt;p&gt;To check the integration, we moved the camera to the other side of the test wall, hid the wall, and then switched the entire scene. Those ordinary operations checked spatial relationships, occlusion, and resource management together. Once Gaussians enter a BIM viewer, they need to withstand those operations alongside the original models.&lt;/p&gt;

</description>
      <category>webgpu</category>
      <category>graphics</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Why We Rebuilt Our BIM Rendering Core with WebGPU</title>
      <dc:creator>有点质量</dc:creator>
      <pubDate>Thu, 08 Oct 2026 09:37:45 +0000</pubDate>
      <link>https://dev.to/ian_liu_27ecc3baa5df2eee0/why-we-rebuilt-our-bim-rendering-core-with-webgpu-2l62</link>
      <guid>https://dev.to/ian_liu_27ecc3baa5df2eee0/why-we-rebuilt-our-bim-rendering-core-with-webgpu-2l62</guid>
      <description>&lt;p&gt;We started with Three.js and WebGL. Once we had models rendering in the browser, we worked through the usual optimizations: loading in chunks, reducing detail at a distance, batching draws, and skipping occluded geometry.&lt;/p&gt;

&lt;p&gt;As that work progressed, changes began to affect one another. Drawing less required more checks. Keeping the display stable meant dealing with visibility decisions that changed from frame to frame. A fix in one part meant another round of checks elsewhere.&lt;/p&gt;

&lt;p&gt;Eventually, we decided to rebuild our BIM rendering core with WebGPU. Looking back, several attempts that didn't work out as expected help explain why.&lt;/p&gt;

&lt;h2&gt;
  
  
  If it's marked invisible, why are we still drawing it?
&lt;/h2&gt;

&lt;p&gt;One investigation in the old viewer stands out. The diagnostics showed that many elements had been marked invisible, but the amount of drawing hadn't gone down.&lt;/p&gt;

&lt;p&gt;We traced the problem to a mismatch in granularity. The visibility logic could decide that an individual element didn't need to be drawn. The data being used to display it, however, didn't always let us remove that element independently. A more precise decision wasn't necessarily something the renderer could act on.&lt;/p&gt;

&lt;p&gt;We tried adding checks for some of those elements. It saved no drawing work and caused more changes in their visibility state. We removed that change.&lt;/p&gt;

&lt;p&gt;This was a limitation of how our old implementation organized data and applied its decisions. Blaming WebGL alone would have missed the problems in our own implementation. But adding more conditions to the visibility logic wasn't going to fix the layer underneath it.&lt;/p&gt;

&lt;p&gt;Investigations like this led us to break down the work in each frame again. The CPU had to traverse the scene, update state, and prepare drawing commands. The main thread also had to handle mouse input and the application UI. Drawing only one room didn't mean the work needed to decide what to draw was cheap.&lt;/p&gt;

&lt;p&gt;We can group that work into a few broad categories. Some of it overlaps, so the timings can't simply be added together:&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%2F3gy0qf2bjoe4wo6c6jy9.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%2F3gy0qf2bjoe4wo6c6jy9.png" alt="Work that contributes to responsiveness during interaction" width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After that, we started asking one more question when reviewing an optimization: did the work we claimed to have saved actually disappear from the steps that followed?&lt;/p&gt;

&lt;h2&gt;
  
  
  WebGPU gave us more room to divide the work
&lt;/h2&gt;

&lt;p&gt;WebGL already let us use the GPU for drawing. What made WebGPU worth considering was the flexibility of its compute and drawing interfaces.&lt;/p&gt;

&lt;p&gt;Compute shaders can process batches of work that lend themselves to parallel execution. Storage buffers can hold the data those tasks need. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/GPURenderPassEncoder/drawIndexedIndirect" rel="noopener noreferrer"&gt;Indirect draws can read their drawing parameters directly from a buffer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In a typical GPU-driven design, some filtering and draw preparation can happen on the GPU. That creates opportunities to reduce CPU work per object and avoid certain round trips between the CPU and GPU. &lt;a href="https://developer.chrome.com/blog/webgpu-io2023/" rel="noopener noreferrer"&gt;Chrome's introduction to WebGPU&lt;/a&gt; discusses these capabilities as well.&lt;/p&gt;

&lt;p&gt;Moving work has a cost, though. Data has to be prepared and transferred, and tasks may have to wait for one another. Small, scattered tasks may not be worth moving. Large batches of independent, similar decisions are more promising candidates.&lt;/p&gt;

&lt;p&gt;We also considered replacing only the renderer while keeping our existing data preparation. We set that option aside during evaluation. If the data organization and repeated processing stayed the same, a new graphics API would inherit those costs.&lt;/p&gt;

&lt;p&gt;The rebuild therefore included how we prepared data before it reached the renderer. We needed to reconsider what belonged on the GPU, what should stay on the CPU, and what work could be avoided earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared geometry still needs to be drawn
&lt;/h2&gt;

&lt;p&gt;Imagine a scene with a thousand identical chairs. They share the same geometry and material, but sit in different positions.&lt;/p&gt;

&lt;p&gt;Storing a complete copy of the geometry for every chair would duplicate a lot of data. Instancing lets the chairs share geometry while retaining their own positions. &lt;a href="https://threejs.org/docs/pages/InstancedMesh.html" rel="noopener noreferrer"&gt;Three.js's InstancedMesh&lt;/a&gt; supports this kind of use.&lt;/p&gt;

&lt;p&gt;But all the chairs visible on screen still need to be drawn. A smaller file doesn't remove the pixels they cover or the occlusion between them.&lt;/p&gt;

&lt;p&gt;Batching involves a similar tradeoff. Combining many objects into one draw batch can reduce submission work. If that batch spans several rooms, viewing one room may also cause geometry from the others to be submitted.&lt;/p&gt;

&lt;p&gt;BIM adds another requirement: individual elements must remain usable. Two identical valves still need to be selected, hidden, and inspected separately. Large batches can reduce draw calls while making those operations harder to support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Culling has a cost too
&lt;/h2&gt;

&lt;p&gt;Inside a building, many elements are hidden behind walls. Not drawing them sounds like a straightforward optimization.&lt;/p&gt;

&lt;p&gt;First, the engine has to establish that they really are hidden.&lt;/p&gt;

&lt;p&gt;The view frustum, spatial hierarchies, and occlusion information can all help narrow down what needs processing. Each approach has different costs and works best in different situations. The most detailed check isn't necessary for every object.&lt;/p&gt;

&lt;p&gt;Consider a CPU that issues an occlusion query and immediately waits for the GPU to return the result. It may save some drawing, but it also gives up an opportunity for CPU and GPU work to overlap. A long enough wait can make the whole frame slower. &lt;a href="https://developer.nvidia.com/gpugems/gpugems/part-v-performance-and-practicalities/chapter-29-efficient-occlusion-culling" rel="noopener noreferrer"&gt;NVIDIA's discussion of occlusion culling&lt;/a&gt; covers this problem.&lt;/p&gt;

&lt;p&gt;Correctness matters just as much. When the camera turns a corner, newly exposed equipment needs to appear promptly. A coarse decision mustn't hide pipes behind glass. When visibility is uncertain, drawing a little extra is usually preferable to leaving out an element.&lt;/p&gt;

&lt;p&gt;Move the camera above the building and the tradeoff changes again. Much more of the scene is visible, so there is less drawing to eliminate, while the checks still cost something. An approach that works well indoors may offer little benefit from above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fewer draw calls, more GPU time
&lt;/h2&gt;

&lt;p&gt;A draw call is a request to draw geometry. Counting draw calls is useful when looking at submission work, but that count doesn't describe the full rendering cost.&lt;/p&gt;

&lt;p&gt;We ran into this again shortly after moving to WebGPU.&lt;/p&gt;

&lt;p&gt;Once the minimal viewer could display a model, we tried merging more draw batches. The reasoning was straightforward: fewer calls should be faster. The call count did fall. GPU time increased, though, and the model took longer to appear.&lt;/p&gt;

&lt;p&gt;We didn't adopt that experiment for the main rendering path, but kept it as a reference. We hadn't established exactly why it was slower. What we could tell was that further merging hadn't helped that version.&lt;/p&gt;

&lt;p&gt;A single draw call can still contain a great deal of geometry. Overlapping transparent objects, shadows, and post-processing can also keep the GPU busy.&lt;/p&gt;

&lt;p&gt;Resolution provides a simple example. Double both the width and height of an image, and the pixel count quadruples. Rendering time won't necessarily quadruple, but the GPU has more pixels to process.&lt;/p&gt;

&lt;p&gt;WebGPU render bundles let us reuse previously recorded drawing commands, avoiding repeated encoding. Indirect draws let the GPU obtain drawing parameters from a buffer. Both are useful, and neither addresses every cost.&lt;/p&gt;

&lt;p&gt;To validate a change like this, we still need to repeat the same interaction: does dragging feel smoother, does the UI pause, and has the image quality changed?&lt;/p&gt;

&lt;h2&gt;
  
  
  How much space does a 4K texture take?
&lt;/h2&gt;

&lt;p&gt;The number of bytes downloaded often differs substantially from the memory a resource occupies at runtime, including GPU memory.&lt;/p&gt;

&lt;p&gt;Take a hypothetical 4096 × 4096 texture. With uncompressed RGBA8 at 4 bytes per pixel, the pixel data for that resolution level alone occupies:&lt;/p&gt;

&lt;p&gt;4096 × 4096 × 4 = 67,108,864 bytes, or 64 MiB.&lt;/p&gt;

&lt;p&gt;That excludes other resolution levels and additional resources. GPU texture compression changes the footprint again. The download size can't be used directly as an estimate of runtime texture memory.&lt;/p&gt;

&lt;p&gt;These costs are worth keeping separate:&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%2Fm99k5z8t16z9zjr3t45l.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%2Fm99k5z8t16z9zjr3t45l.png" alt="Separate measures for transfer, memory, GPU resources, and per-frame work" width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A model loading successfully once doesn't settle the question. We also need to check repeated model switches. Long sessions, canceled loads, and closed views all put resource management to the test. A graphics API upgrade doesn't handle that work for the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  A black screen in another browser
&lt;/h2&gt;

&lt;p&gt;After integrating the new core into the viewer page, we encountered a black screen. The page worked in our development environment, but failed to render in an embedded browser.&lt;/p&gt;

&lt;p&gt;We eventually found that our resource usage exceeded what that environment allowed. Our development machine exposed more generous limits, so it hadn't revealed the problem. After adjusting resource usage and testing again, rendering worked in that environment.&lt;/p&gt;

&lt;p&gt;“Supports WebGPU” isn't the whole story. The browser, operating system, GPU, and driver all affect the capabilities available. Once something works on the development machine, the target environments still need to be tested.&lt;/p&gt;

&lt;p&gt;Framework maintenance belongs in this calculation too. &lt;a href="https://threejs.org/docs/pages/WebGPURenderer.html" rel="noopener noreferrer"&gt;Three.js already provides WebGPURenderer&lt;/a&gt;, so choosing WebGPU still leaves room to use a framework. Taking direct responsibility for more of the rendering also means taking on its debugging, compatibility, and resource management.&lt;/p&gt;




&lt;p&gt;We kept both viewers available during the transition. We compared the same models from the same camera positions and gradually brought the interactions across. Only after the new viewer became the default did we archive the old one.&lt;/p&gt;

&lt;p&gt;We kept the batching experiment that had made rendering slower, too. If we revisit that direction, we can first look at what we changed and why we didn't carry it forward.&lt;/p&gt;

&lt;p&gt;Rebuilding with WebGPU gave us more control over where computation happens and how drawing is organized. Whether a particular change helps still comes down to opening that model and moving the camera back to the spot where it used to stutter.&lt;/p&gt;

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