webdevpuneet.com

GLB Side-by-Side Model Comparison — Free Dual-Viewport Three.js glTF Snippet

GLB Side-by-Side Model Comparison · Media · Plain HTML, CSS & JS · Live preview

Copy & paste

Source Code

<div class="gmc-wrap">
  <div class="gmc-toolbar">
    <label class="gmc-sync"><input type="checkbox" id="gmcSync" checked /> Sync orbit</label>
    <div class="gmc-hint">Drag either pane to orbit · Ctrl/Cmd + scroll to zoom</div>
  </div>
  <div class="gmc-panes">
    <div class="gmc-pane">
      <canvas id="gmcCanvasA"></canvas>
      <div class="gmc-label">Duck <span id="gmcTrisA">&middot; &hellip; tris</span></div>
      <div class="gmc-zoom">
        <input type="range" id="gmcZoomA" class="gmc-zoom-slider" min="0" max="100" step="1" />
      </div>
    </div>
    <div class="gmc-divider"></div>
    <div class="gmc-pane">
      <canvas id="gmcCanvasB"></canvas>
      <div class="gmc-label">Antique Camera <span id="gmcTrisB">&middot; &hellip; tris</span></div>
      <div class="gmc-zoom">
        <input type="range" id="gmcZoomB" class="gmc-zoom-slider" min="0" max="100" step="1" />
      </div>
    </div>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Two real glTF binary models
Loaded via THREE.GLTFLoader into fully independent scenes, side by side.
Synced-orbit toggle
Mirrors one pane's spherical viewing angle onto the other, independent of zoom.
Independent zoom per pane
Each pane keeps its own zoom level even while orbit angle stays synced.
Live triangle counts
A real per-model tri count computed from the loaded geometry after load.
Auto-fit model scale
Both models are scaled to the same target height via bounding-box measurement.
Independent honest fallbacks
Each pane logs its own load failure and shows a distinct placeholder shape.
Slider + Ctrl/Cmd-scroll zoom
Zoom is an explicit, opt-in gesture on both panes, never a hijacked scroll wheel.
Fully responsive layout
Panes stack vertically on narrow viewports instead of squeezing side by side.

About this UI Snippet

GLB Side-by-Side Model Comparison — Two Independent Viewports, One Optional Sync

Screenshot of the GLB Side-by-Side Model Comparison snippet rendered live

Comparing two 3D models properly means more than putting two screenshots next to each other — it means being able to inspect both from the same angle, live, while still being able to break away and look at either one independently. This snippet runs two entirely separate Three.js scenes, cameras, and OrbitControls instances side by side, and adds a "Sync orbit" toggle that mirrors one pane's camera angle onto the other whenever it's checked.

Two full scenes, not one shared one

Every piece of Three.js state — scene, camera, renderer, and OrbitControls — is built twice, once per key ('A' and 'B'), and stored in small lookup objects rather than duplicated as separate top-level variables. Each pane's GLTFLoader.load() call, resize handler, and render call are entirely independent, so a slow or failing load in one pane never affects the other.

Mirroring spherical coordinates, not raw camera positions

When "Sync orbit" is checked, dragging pane A fires OrbitControls' 'change' event, which calls mirrorOrbit('A', 'B'). Rather than copying A's camera position directly onto B (which would ignore that the two models sit at different distances from their own cameras), the snippet converts A's camera offset from its target into a THREE.Spherical — an azimuthal angle, polar angle, and radius — then rebuilds B's camera offset using A's *angles* but B's own existing *radius*. The result: both panes always show the same relative viewing angle, even while each keeps its own independent zoom level.

Auto-fit scale for both models independently

Each call to loadModel() measures its own model's bounding box with THREE.Box3 and scales it to the same fixed target height, so two models of very different native sizes and origins in their source .glb files still compare fairly, side by side, at a consistent visual scale.

A live triangle count per model

After each model loads, the snippet walks every mesh in its scene graph and sums up triangle counts from either the geometry's index buffer or its raw vertex count, displaying the total next to that pane's label — a genuinely useful, real number for anyone comparing model complexity, not a decorative placeholder.

Independent, honest fallbacks

If either .glb fails to load, that pane's error callback logs which pane failed and swaps in its own distinct placeholder shape and color, so a single broken URL never blanks out the whole comparison.

Zoom is opt-in, not a hijacked scroll wheel

Both panes turn off OrbitControls' built-in wheel-zoom and reimplement it as an explicit slider plus Ctrl/Cmd + scroll, exactly like every other snippet in this family, so a plain scroll over either pane always scrolls the page.

Customizing it

Swap either MODEL_URL for any other Khronos sample-asset .glb, add a third pane by extending the 'A'/'B' keys to 'C', or pair it with GLB product color configurator for a compare-then-customize flow.

Step by step

How to Use

  1. 1
    Add all three CDN scriptsthree.min.js, GLTFLoader.js, and OrbitControls.js.
  2. 2
    Paste HTML, CSS, and JSTwo models load side by side, each with its own triangle count.
  3. 3
    Drag either paneWith "Sync orbit" checked, the other pane mirrors the same viewing angle.
  4. 4
    Uncheck "Sync orbit"Each pane orbits fully independently.
  5. 5
    Use either slider or Ctrl/Cmd + scrollZoom each pane independently at any time — plain scroll always scrolls the page.
  6. 6
    Swap the model URLsPoint either loadModel() call at a different .glb to compare other models.

Real-world uses

Common Use Cases

Product variant comparisons
Show two SKUs, materials or revisions in separate scenes, each with its own OrbitControls camera so either model can be inspected on its own.
3D asset export QA
Compare a new export against its reference, switching on synced orbit so both models are viewed from exactly the same angle.
Before and after model pages
Demonstrate an optimisation by showing two versions side by side, with live triangle counts computed from the loaded geometry for each.
Museum and collectible comparisons
Place two artefacts next to each other, with independent zoom per pane even while the orbit angle is mirrored.
Variant and finish companion
Pair with the GLB colour configurator to let buyers recolour a model and then compare the result against the original.
Related: Rental Car Comparison Cards
See the Rental Car Comparison Cards for a related cards pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

It converts the dragged pane's camera offset from its target into spherical coordinates (an azimuthal angle, a polar angle, and a radius), then rebuilds the other pane's camera offset using the dragged pane's two angles but that other pane's own existing radius. Angle and distance are kept as separate values on purpose, so both panes always face the same direction while each still remembers its own independent zoom.

Two fully separate scenes, cameras, renderers, and OrbitControls instances, each keyed by 'A' or 'B' in small lookup objects. Nothing is shared between them except the mirroring logic that runs only when Sync orbit is checked — a slow load, a failed model, or independent dragging in one pane never touches the other.

After each model loads, the snippet walks every mesh in its scene graph. For each mesh's geometry, it uses the index buffer's vertex count divided by three if the geometry is indexed, or the raw position attribute's vertex count divided by three otherwise, then sums that across every mesh in the model.

Each pane has its own independent GLTFLoader error callback. If pane B's model fails, its callback logs specifically which pane failed and swaps in its own placeholder shape and label text, while pane A continues to load and render completely normally, so one broken URL never blanks out the whole comparison.

Set up both renderers, scenes, GLTFLoader calls, and OrbitControls instances inside a mount effect, keeping the per-key lookup objects and the syncBox checkbox state in refs so the change-event mirroring logic can reach current values. Call both controls' .dispose() and both renderers' .dispose() in the cleanup function to release both WebGL contexts on unmount.