Babylon.js Spinning Product Viewer — 3D Orbit Canvas Snippet

Babylon.js Spinning Product Viewer · Misc · Plain HTML, CSS & JS · Live preview

What's included

Features

Spherical orbit camera
ArcRotateCamera parameterizes view as alpha/beta/radius around a fixed target.
One-line drag controls
attachControl wires pointer and wheel input to orbit and zoom automatically.
Frame-locked auto-rotation
registerBeforeRender ties idle spin to actual render frames, never drifting.
Clamped zoom range
lowerRadiusLimit/upperRadiusLimit stop the camera clipping through or flying too far.
Panning disabled
panningSensibility = 0 keeps the camera always orbiting the product, never off-target.
Approximated three-point lighting
A hemispheric key light plus two tinted point lights fake a studio setup cheaply.
Auto/manual handoff
Starting a drag automatically pauses idle rotation so the two never fight.
Responsive canvas
engine.resize() on window resize keeps the render target matched to CSS size.

About this UI Snippet

Babylon.js Spinning Product Viewer — ArcRotateCamera and the Render Loop

Screenshot of the Babylon.js Spinning Product Viewer snippet rendered live

Product viewers are one of the clearest cases where a 2D CSS trick won't do — you actually need a camera that can orbit a 3D object in true depth. Babylon.js ships exactly the camera type this needs, and getting a polished result comes down to understanding two things: how ArcRotateCamera parameterizes orbit, and how registerBeforeRender drives continuous motion without fighting the render loop.

ArcRotateCamera is spherical, not free-look

new BABYLON.ArcRotateCamera('camera', alpha, beta, radius, target, scene) places the camera using spherical coordinates around a target point rather than a position and look direction:

- `alpha` — the horizontal (azimuthal) angle around the target, in radians. - `beta` — the vertical (polar) angle from the top, in radians. Math.PI / 2 points straight at the equator; smaller values look down from above. - `radius` — the distance from the target.

This is the correct camera model for a product viewer because the user should only ever be able to look *at* the product from different angles, never fly past it or lose it off-screen — something a free-look/universal camera would allow by default. camera.attachControl(canvas, true) wires up pointer drag to alpha/beta and wheel/pinch to radius with zero extra code.

Clamping and disabling the parts you don't want

Three lines turn the generic orbit camera into a constrained product-viewer camera:

camera.lowerRadiusLimit = 3.5; camera.upperRadiusLimit = 10; camera.panningSensibility = 0;

The radius limits stop the user zooming through the product or so far out it becomes a speck. panningSensibility = 0 disables the camera's built-in panning (which would let the target point itself be dragged off-center, breaking the "always orbiting the product" guarantee) — it's a single property rather than a custom control scheme.

The render loop, and why auto-rotation lives in registerBeforeRender

engine.runRenderLoop(function () { scene.render(); }) is Babylon's main loop, calling scene.render() on every animation frame the browser gives it (effectively requestAnimationFrame, managed internally). scene.registerBeforeRender(callback) registers a function that runs immediately before each of those renders — the idiomatic place for continuous per-frame state changes like:

if (autoRotate) { product.rotation.y += 0.006; }

Incrementing rotation here, rather than in a setInterval, guarantees the spin rate is tied to actual rendered frames — it can never drift out of sync with what's on screen, and it automatically pauses correctly if the tab is backgrounded and requestAnimationFrame stops firing.

Coordinating auto-rotate with manual drag

Manual orbiting (via attachControl) and the automatic idle spin both ultimately affect what the camera sees, but they operate on different objects — attachControl moves the *camera's* alpha/beta, while auto-rotate spins the *product mesh's* own rotation.y. Without any coordination, a user dragging to orbit would fight against the product still spinning underneath them. The pointerdown listener on the canvas simply flips autoRotate to false the instant a drag starts, so control cleanly hands from automatic to manual.

Three-point-style lighting

A HemisphericLight (soft ambient/key fill from above), plus two PointLights with tinted diffuse colors positioned to one side (cool blue "fill") and behind (warm pink "rim"), approximate a photography three-point setup entirely with cheap, real-time lights — no baked lightmaps or HDR environment needed for a simple showcase mesh.

Reusing it

Swap the torus for an imported .glb/.gltf model via BABYLON.SceneLoader.ImportMeshAsync and everything else — camera, lighting, auto-rotate — works unchanged. Pair it with a Babylon.js Orbit Camera Showcase for animated preset-angle camera moves instead of continuous idle spin.

Build with AI

Build, Understand, Optimize, and Extend It With AI

This snippet is a compact reference for Babylon.js's core scene-setup pattern, so it's worth using an AI assistant to go deeper on the parts that generalize. Paste the code into Claude and ask it to explain exactly what alpha, beta, and radius represent geometrically in ArcRotateCamera, and why a spherical parameterization is preferable to a free-look camera specifically for a "look at one object" use case. Then ask what would happen to the auto-rotation if registerBeforeRender were replaced with a setInterval(fn, 16) instead, and why that would be a strictly worse choice (frame-rate independence, background-tab behavior, jank under load). To extend it: ask it to add a set of material-swap buttons that change mat.diffuseColor to simulate product color variants, add a subtle idle "breathing" scale animation combined with the rotation, load a real .glb model via SceneLoader instead of the primitive torus, or add an environment reflection using a CubeTexture for a more premium studio look.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a 3D product viewer using Babylon.js (from a CDN, global namespace BABYLON) in plain HTML, CSS, and JavaScript with a single <canvas> element.

Requirements:
- Initialize a BABYLON.Engine on the canvas and a BABYLON.Scene with a dark clear color.
- Use an ArcRotateCamera targeting the origin, with attachControl(canvas, true) enabling drag-to-orbit and wheel-to-zoom out of the box. Set lowerRadiusLimit and upperRadiusLimit to sane values so zoom is clamped, and set panningSensibility to 0 so the camera can only orbit, never pan off the product.
- Create a primitive mesh (a torus or box standing in for a product) with a StandardMaterial that has distinct diffuse and specular colors so it reads as a lit, reflective object rather than flat-shaded.
- Add a small ground disc beneath it with its own darker material, for context.
- Set up approximate three-point lighting: one HemisphericLight for soft ambient/key light, and two PointLights with different tinted diffuse colors positioned to create a fill light and a rim light.
- Auto-rotate the product mesh continuously by incrementing its rotation.y inside scene.registerBeforeRender (not setInterval), at a slow constant rate.
- Add a toggle button that turns auto-rotation on/off, and make starting a manual drag on the canvas automatically pause auto-rotation.
- Call engine.runRenderLoop(() => scene.render()) to start rendering, and resize the engine on window resize.
- Style the page as a dark panel with the canvas taking up most of the width at a fixed height, rounded corners, and a subtle border.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

Requires
<div class="bsp-stage">
  <div class="bsp-head">
    <span class="bsp-tag">Babylon.js · ArcRotateCamera</span>
    <h2>Product Viewer</h2>
    <p>Auto-rotates on its own — drag to orbit manually, scroll to zoom.</p>
  </div>
  <canvas id="bspCanvas"></canvas>
  <div class="bsp-controls">
    <button class="bsp-btn is-on" id="bspAutoBtn">Auto-rotate: On</button>
  </div>
</div>

Step by step

How to Use

  1. 1
    Add the Babylon.js CDNInclude the babylon.js UMD build for the global BABYLON namespace.
  2. 2
    Paste HTML, CSS, and JSA canvas renders a blue torus auto-rotating on a dark ground disc.
  3. 3
    Watch it auto-rotateregisterBeforeRender increments rotation.y by a small amount every frame.
  4. 4
    Drag to orbit manuallyArcRotateCamera.attachControl wires pointer drag to alpha/beta with no extra code.
  5. 5
    Scroll or pinch to zoomThe wheel adjusts camera radius, clamped between lowerRadiusLimit and upperRadiusLimit.
  6. 6
    Swap in a real modelReplace CreateTorus with BABYLON.SceneLoader.ImportMeshAsync for a .glb product model.

Real-world uses

Common Use Cases

E-commerce product pages
A 3D alternative to static product photography with real orbit interaction.
Configurator previews
The base for a viewer where color/material buttons swap the mesh material live.
Portfolio and case study pieces
Showcase a 3D asset with minimal setup for a design or 3D portfolio.
Learning Babylon.js cameras
A focused reference for ArcRotateCamera configuration without scene complexity.
Marketing landing sections
A hero-adjacent interactive 3D element that auto-plays until touched.
Rapid 3D prototyping
A minimal starting scene to drop a GLTF model into for quick review.

Got questions?

Frequently Asked Questions

ArcRotateCamera is parameterized around a fixed target point (alpha, beta, radius) rather than free position and rotation, so it is structurally impossible for the user to fly away from or lose the product -- exactly the constraint a product viewer needs. UniversalCamera would require you to hand-write that constraint yourself.

It lives inside scene.registerBeforeRender(), which Babylon calls once per rendered frame right before rendering it. Using setInterval instead would run on wall-clock time independent of the actual render loop, which can drift out of sync with frame rate, keep running even if rendering pauses (e.g. a hidden tab), and generally produce less smooth motion than an update tied directly to requestAnimationFrame-driven rendering.

They rotate different things: attachControl changes the camera's alpha/beta around a stationary product, while auto-rotate changes the product mesh's own rotation.y. To avoid both happening at once and looking chaotic, a pointerdown listener on the canvas sets autoRotate to false the instant a drag starts, handing control cleanly to the user.

By default ArcRotateCamera also supports panning (dragging the target point itself off-center) and unlimited zoom. Both break the "always looking at the product" guarantee a product viewer needs -- panningSensibility = 0 disables panning entirely, and lowerRadiusLimit/upperRadiusLimit stop the camera from zooming through the mesh or out to where it becomes indistinguishable from the background.

Replace the CreateTorus call with BABYLON.SceneLoader.ImportMeshAsync("", "https://your-cdn/", "model.glb", scene), which returns a promise resolving with the imported meshes -- everything else (camera, lights, auto-rotate logic, resize handling) works unchanged since they operate on the scene, not the specific mesh.

Create the canvas via a ref, and initialize the Engine/Scene/camera/meshes once inside a useEffect (React) or onMounted (Vue) with an empty dependency array, storing the engine instance so you can call engine.dispose() on unmount to free the WebGL context. Keep the render loop and resize listener registration inside that same effect.