SPHR is a hackable web runtime for spatial tours, digital twins, and browser-native 3D scenes.
This branch is the Next.js + Three.js migration. It removes the old separate backend requirement and ships as a client-rendered viewer that can load a default demo, a remote JSON config, or embedded bootstrap data from a host page.
sphr-3dgs.mp4
The reworked SPHR is centered on 3D Gaussian Splatting. The runtime can load splat assets or 360 images, etc directly in the browser, place them in a Three.js scene, animate camera movement through the capture, and layer guided tour text, UI controls, skyboxes, annotations, models, and effects over the scene.
SPHR uses @sparkjsdev/spark for splat rendering. The current splat layer supports:
.splat,.ply,.spz,.ksplat,.sog, and.radsources- local or remote URLs loaded through
fetch - per-splat position, rotation, scale, opacity, and LOD settings
- reveal animation for initial scene entry
- guided-tour study modes such as point shrinking, projected splat emphasis, and night mode
- a separate scene graph for GLB/mesh overlays, raycast targets, and transition meshes
Example splat config:
{
"space": {
"title": "Research Garden Capture",
"type": "splat",
"space_data": {
"noPanos": true,
"initialPosition": { "x": 4, "y": 1.5, "z": 4 },
"initialRotation": { "azimuth": 0, "polar": 90 },
"splats": [
{
"id": "garden",
"url": "https://your-asset-host.example/captures/garden.splat",
"fileType": "splat",
"position": [0, 2.6, 0],
"rotation": [-2.56518, -2.66973, 0.2615546],
"scale": 1,
"lod": false,
"reveal": true
}
]
}
}
}Large splat files are intentionally not meant to live in git. Host captures in object storage, a CDN, a research artifact mirror, or a local static directory that is excluded from version control.
SPHR currently supports these runtime layers:
- 3D Gaussian splats through
SparkSplatLayer - 360 panorama nodes through
PanoramaLayer - IIIF image planes through
IiifImageLayer - custom GLB/model/light hierarchies through
SceneGraphLayer - 3D annotations through
AnnotationLayer - navigation markers and floor click navigation
- equirectangular, cube, and color skyboxes
- first-person and orbit camera modes
- guided tours with text, audio, annotations, model visibility, and camera moves
The runtime is not a visual authoring tool. It is the viewer/runtime that renders a structured SPHR scene.
Install dependencies:
npm installRun the Next.js dev server:
npm run devOpen http://localhost:3000.
Build and serve a production build:
npm run build
npm run startRun TypeScript validation:
npm run typechecknpm run start is for production builds. Use npm run dev while developing.
To open your own capture:
- Export or convert the capture to
.splat,.ply,.spz,.ksplat,.sog, or.rad. - Put the file behind a URL the browser can fetch. For remote URLs, make sure CORS allows the app origin.
- Create a bootstrap JSON with a
space.typeofsplatand aspace.space_data.splatsentry. - Load it with
/?config=/path/to/bootstrap.json.
Useful prompts for coding agents:
Find an openly available 3D Gaussian Splatting research capture with a downloadable .splat, .ply, .spz, .ksplat, .sog, or .rad asset. Confirm the license and whether the URL is browser-fetchable, then generate a SPHR bootstrap JSON that opens it.
Use the Garden scene from recent 3DGS research as the target scene. Locate or prepare a compatible splat asset, keep large binaries out of git, and wire SPHR to it through ?config with correct position, rotation, scale, and optional skybox settings.
Open my local 3DGS capture in SPHR. Inspect the asset format, serve it from an ignored local static path or external asset host, create a bootstrap JSON, and tune the camera start position until the scene opens correctly.
The app starts in components/SphrApp.tsx and calls loadBootstrapData() from lib/bootstrap.ts.
Bootstrap data can come from four places, in this order:
?config=/path/to/bootstrap.jsonwindow.__SPHR_BOOTSTRAP__- embedded script tags with IDs
space_data,tour_data,ordered_spaces_data, andui_data - the built-in garden demo
The recommended integration path is a single bootstrap JSON file:
{
"space": {
"id": "my-space",
"title": "My Space",
"type": "splat",
"mesh": "/models/dollhouse.glb",
"space_data": {
"initialPosition": { "x": 0, "y": 1.6, "z": 4 },
"initialRotation": { "azimuth": 0, "polar": 0 },
"splats": [
{
"id": "capture",
"url": "/captures/scene.splat",
"scale": 1
}
],
"sceneGraph": []
}
},
"tour": {
"id": "my-tour",
"title": "My Tour",
"tour_data": {
"defaultShowText": true,
"spaces": [
{
"id": "my-space",
"title": "My Space",
"tourpoints": [
{
"viewMode": "FPV",
"targetType": "FREE",
"text": "Start here.",
"position": { "x": 0, "y": 1.6, "z": 4 },
"rotation": { "azimuth": 0, "polar": 0 },
"zoom": 0
}
]
}
]
}
},
"ui": {
"titlePart1": "SPHR",
"titlePart2": "Demo",
"enterButtonText": "Enter",
"exploreButtonText": "Explore"
}
}The authoritative schema lives in lib/types.ts.
SPHR also supports classic 360-image tours. A 360 space is defined as a set of nodes. Each node has a camera position and either an equirectangular image, six cube faces, or a texture template that resolves to cube faces.
Minimal node example:
{
"space": {
"title": "360 Tour",
"type": "spaces",
"space_data": {
"initialNode": "scan-000",
"nodes": [
{
"uuid": "scan-000",
"image": "/panos/scan-000.jpg",
"position": { "x": 0, "y": 1.6, "z": 0 },
"floorPosition": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0 }
},
{
"uuid": "scan-001",
"faces": [
"/panos/scan-001/face0.jpg",
"/panos/scan-001/face1.jpg",
"/panos/scan-001/face2.jpg",
"/panos/scan-001/face3.jpg",
"/panos/scan-001/face4.jpg",
"/panos/scan-001/face5.jpg"
],
"position": { "x": 2.5, "y": 1.6, "z": 0 },
"floorPosition": { "x": 2.5, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0 }
}
]
}
}
}The basic 360 transition crossfades between the outgoing panorama and the incoming panorama while the camera animates to the next node.
SPHR also includes a stronger mesh-based transition for moving between 360 images. During navigation, SphrRuntime captures the outgoing panorama into a WebGLCubeRenderTarget with a CubeCamera. That cube texture is temporarily applied to configured scene graph meshes, usually a low-poly dollhouse or scan mesh, while the main camera flies to the next node. The transition mesh fades out as the new panorama becomes active. This makes the movement feel spatial instead of just cutting between two photos.
Transition config example:
{
"space_data": {
"navigationTransition": {
"enabled": true,
"meshIds": ["dollhouse"],
"opacity": 0.2,
"meshFadeMs": 400,
"navigationMs": 1100,
"cubeRenderTargetSize": 2048
},
"sceneGraph": [
{
"id": "dollhouse",
"type": "model",
"file": "/models/dollhouse.glb",
"visible": true,
"persistent": true,
"raycast": true,
"fpvOpacity": 0,
"orbitOpacity": 1,
"transitionMesh": true,
"transitionOpacity": 0.2,
"transitionFadeMs": 400,
"transitionTexture": "cube-render-target"
}
]
}
}For this transition to run, the space needs:
- panorama nodes with images or cube faces
- a scene graph mesh marked as a transition mesh
navigationTransition.enablednot set tofalse- first-person navigation between two different nodes
Set noPanos: true for splat-only spaces where panorama rendering should be disabled.
Tours are arrays of spaces and tourpoints. A tourpoint can move the camera, switch view mode, show text, reveal models, show annotations, play sounds, or trigger runtime modes through the extra field.
Common tourpoint fields:
viewMode:FPVorORBITtargetType:NODE,FREE, orMODELnodeUUID: node to navigate to in a 360 tourposition: free camera position for splat or free-flight pointsrotation: camera azimuth and polar angleszoom: camera FOV adjustmenttextandsecondaryText: guided tour copymodels: scene graph IDs to show at this stepannotationsoroverlays: annotation IDs to show at this stepsounds: audio IDs to play at this stepextra: runtime mode such asnightMode,shrinkToPoints, orprojectToSplats
The scene graph lets a tour add persistent or step-specific 3D content around the main capture.
Supported node uses include:
- GLB or other model files
- groups
- ambient, point, directional, and custom lights
- raycast meshes for cursor and floor navigation
- FPV, orbit, and debug-specific opacity
- transition meshes for the 360 navigation effect
- nested children with inherited transforms
Transforms use arrays for scene graph nodes:
{
"id": "artifact",
"type": "model",
"file": "/models/artifact.glb",
"position": [0, 1.2, -2],
"rotation": [0, 0.4, 0],
"scale": 1,
"visible": false,
"raycast": true
}MIT, but if this is useful to you, please cite this repo because it helps the work continue.
@misc{hollis2024sphr,
author = {Luke Hollis},
title = {SPHR - Spatial Human Reality in a web browser},
year = {2024},
publisher = {GitHub},
journal = {GitHub repository},
howpublished = {\url{https://github.com/lukehollis/sphr}}
}