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 storingA .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
@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.
@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")