SplatViewer (SplatViewer v0.1.0)

Copy Markdown View Source

Displays 3D Gaussian splats in a Phoenix application.

<.splat_viewer src={~p"/scans/room.sog"} camera={@scan.camera} />

A Gaussian splat is a photographic 3D capture — a room, an object, a site — reconstructed from ordinary video as a few million translucent ellipsoids. Nothing on Hex renders one, and the browser engines that do are JavaScript, so this is a Phoenix component and a LiveView hook around one of them.

Setting it up

Two pieces: the component, and the hook.

# in your html helpers
import SplatViewer.Components

// in assets/js/app.js
import SplatViewer from "splat_viewer"

let liveSocket = new LiveSocket("/live", Socket, {
  hooks: { SplatViewer, ...otherHooks }
})

and in assets/package.json:

"dependencies": {
  "splat_viewer": "file:../deps/splat_viewer",
  "playcanvas": "^2.21.0"
}

The rendering engine is not bundled. PlayCanvas is around two megabytes, and a Hex package has no business shipping that or pinning your copy of it. It is imported at runtime from a URL you control — see SplatViewer.engine_path/0 and the README.

Feed it a .sog

.ply is what a splat trainer emits and it is enormous: a captured room is commonly a hundred megabytes or more. .sog is the compressed delivery format, roughly 45× smaller, and it is what splat_tools produces:

{:ok, asset} = SplatTools.prepare("room.ply", "priv/static/scans")

asset.sog     #=> "priv/static/scans/room.sog"
asset.camera  #=> a viewpoint worth storing

A .ply will load. On a real capture it will cost your visitor the full hundred megabytes to find that out.

Store the camera

No splat format carries a viewpoint. A viewer opening without one points wherever its default points, which for most captures is at nothing at all — so prepare/3 derives a camera from the scene's own geometry and the right thing to do is persist it alongside the file and pass it back here.

Without one this falls back to framing the bounding box, which is a guess a single stray splat can ruin.

Summary

Functions

The URL the hook imports the rendering engine from.

Absolute path to the hook's JavaScript, inside this package.

Functions

engine_path()

@spec engine_path() :: String.t()

The URL the hook imports the rendering engine from.

Defaults to "/assets/playcanvas.mjs". Point it wherever your pipeline puts PlayCanvas:

config :splat_viewer, engine_path: "/assets/vendor/playcanvas.mjs"

A per-element data-engine-url overrides this, and so does a window.SPLAT_VIEWER_ENGINE_URL global — the config is only the default.

hook_path()

@spec hook_path() :: Path.t()

Absolute path to the hook's JavaScript, inside this package.

For an asset pipeline that cannot resolve deps/ as a node module — or for copying the file somewhere your bundler already looks:

File.cp!(SplatViewer.hook_path(), "assets/vendor/splat_viewer.js")