Statify extensions

Scene widget

Use the Scene client widget to place an interactive Three.js scene in a Statify page. A scene can load glTF/GLB models and HDR environments, expose named views, discover cameras and animations stored in a GLB, and switch between several arrangements of the same resources.

Start with the game-3d template

For a new 3D site, initialize Statify's game-3d template:

statify init my-game-site game-3d
cd my-game-site
npm start

The game-3d template already includes the extra Scene JavaScript and CSS libraries. Authors using that template only need to define the widget in page JSON and add their scene assets.

When adding Scene to another template, copy the compiled Scene files into the site and load them once from metadata.json. Load the Scene script after Statify's client runtime:

{
  "head": [
    {"t": "link", "rel": "stylesheet", "href": "/app/extra/statify-scene.css"},
    {"t": "script", "src": "/app/website.min.js"},
    {"t": "script", "src": "/app/extra/statify-scene.min.js"}
  ]
}

Scene structure

The object passed to attrs is the scene definition itself.

{
  "res": {},
  "env": {},
  "cam": {},
  "run": "showroom",
  "scenes": {},
  "debug": false
}
Field Purpose
res Named GLB, glTF, and HDR resources.
env Background, environment map, exposure, fog, ground, and light.
cam Named interactive cameras and their saved spots.
run ID of the scene selected when the widget starts.
scenes Named combinations of visible resources and available cameras.
debug Shows the temporary authoring control used to capture camera spots.

All declared resources are loaded before the scene starts. Keep files suitable for the web even when the source game uses higher-resolution assets.

Resources

Declare every asset once under res, then refer to it by its ID elsewhere:

{
  "res": {
    "level": {"file": "/res/scene/level.glb"},
    "sky": {"file": "/res/scene/studio.hdr"}
  }
}

Supported model formats are .glb and .gltf; environment maps use .hdr. A model remains hidden until the active named scene includes its resource ID in show.

Use autoplay to start one or more animation clips as soon as a model is shown. A value may be an animation ID or label, or an array of them:

{
  "res": {
    "character": {
      "file": "/res/scene/character.glb",
      "autoplay": ["Idle", "Blink"]
    }
  }
}

Animations discovered in visible models are also exposed in the widget's animation row. Each animation button toggles that clip on and off.

Environment, ground, and shadows

env controls the shared Three.js environment:

{
  "env": {
    "envMap": "sky",
    "backgroundColor": "#070711",
    "exposure": 1,
    "fog": false,
    "fogColor": "#070711",
    "fogDist": 0.01,
    "ground": {
      "position": [0, 0, 0],
      "size": [50, 50],
      "color": "#171725",
      "reflect": 0.1
    },
    "light": {
      "pos": [20, 30, 10],
      "target": [0, 0, 0],
      "color": "#ffffff",
      "intensity": 1.5,
      "shadow": {
        "cameraSize": 80,
        "near": 0.1,
        "far": 150,
        "bias": -0.0001,
        "normalBias": 0.02
      }
    }
  }
}

envMap names an HDR resource and applies it as both the lighting environment and the background. When no environment map is selected, backgroundColor remains visible.

The ground is optional. When used, ground.position creates it, size sets its width and depth, and reflect is a value from 0 to 1. Omit ground when the model already contains its terrain, floor, or complete surroundings.

light creates a directional light. Adding a shadow object enables shadow rendering, makes model meshes cast shadows, and lets the ground receive them. cameraSize controls the total area covered by the light's orthographic shadow camera; increase it for large levels, or reduce it for more detailed shadows in a smaller area.

Cameras and spots

Declare cameras under cam. The supported authored camera types are orbit, walk, and follow.

{
  "cam": {
    "overview": {
      "label": "Overview",
      "type": "orbit",
      "default": "entrance",
      "position": [8, 5, 12],
      "target": [0, 2, 0],
      "minDistance": 2,
      "maxDistance": 40,
      "spots": [
        {
          "id": "entrance",
          "label": "Entrance",
          "position": [8, 5, 12],
          "target": [0, 2, 0]
        },
        {
          "id": "hangar",
          "label": "Hangar",
          "position": [-10, 4, 3],
          "rotation": [0, -90, 0]
        }
      ]
    },
    "visitor": {
      "label": "Walk",
      "type": "walk",
      "position": [0, 1.7, 8],
      "speed": 4,
      "lookSpeed": 0.002
    },
    "follow-ship": {
      "label": "Follow Ship",
      "type": "follow",
      "target": "level/Ship",
      "offset": [0, 4, 10]
    }
  }
}

Camera poses accept position or pos. Orientation can use Euler degrees in rotation, or [x, y, z, w] values in quaternion or quat. Orbit cameras can also declare target, dampingFactor, rotateSpeed, panSpeed, zoomSpeed, moveSpeed, and lookSpeed.

A follow target may be an object name or resourceId/ObjectName. Qualify the name when two resources contain objects with the same name.

Any cameras contained in a visible GLB are discovered automatically and added to the available cameras. Their IDs are namespaced as resourceId/cameraName. Animations contained in the GLB are discovered at the same time.

The default desktop controls are:

  • left drag rotates around the selected geometry;
  • middle drag pans;
  • right drag looks freely;
  • the mouse wheel zooms;
  • W, A, S, and D move, Q and E move vertically, and Shift moves faster while free-looking.

Touch input supports one-finger rotation and two-finger zoom and pan.

Named scenes

A named scene selects which resources are visible and which authored cameras belong in its interface:

{
  "run": "showroom",
  "scenes": {
    "showroom": {
      "label": "Showroom",
      "show": ["level"],
      "cams": ["overview", "visitor"],
      "camera": "overview"
    },
    "flight": {
      "label": "Flight",
      "show": ["level"],
      "cams": ["follow-ship"],
      "camera": "follow-ship",
      "html": "<strong>Flight demonstration</strong>"
    }
  }
}

show contains resource IDs. cams contains authored camera IDs, while camera is the initial camera for that named scene. Cameras discovered inside the visible GLB resources are appended automatically. Optional html is trusted authored HTML displayed over the scene; never populate it from untrusted input.

The widget renders selectors only when they are useful: scenes select the active arrangement, cameras select the current viewpoint, spots select saved positions for the active camera, and animations toggle imported clips.

Creating a Scene page

A dedicated Scene page uses Statify's Client page widget. Place the scene definition directly in client.attrs:

{
  "widget": "Client",
  "title": "Explore the level",
  "client": {
    "widget": "Scene",
    "attrs": {
      "res": {
        "level": {"file": "/res/scene/level.glb"},
        "sky": {"file": "/res/scene/studio.hdr"}
      },
      "env": {
        "envMap": "sky",
        "exposure": 1,
        "light": {
          "pos": [20, 30, 10],
          "target": [0, 0, 0],
          "intensity": 1.5
        }
      },
      "cam": {
        "overview": {
          "label": "Overview",
          "type": "orbit",
          "position": [8, 5, 12],
          "target": [0, 2, 0]
        }
      },
      "run": "showroom",
      "scenes": {
        "showroom": {
          "label": "Showroom",
          "show": ["level"],
          "cams": ["overview"],
          "camera": "overview"
        }
      }
    }
  }
}

Embedding Scene in a FeatureList

Client widgets can also be placed directly in a FeatureList items array. In this form, use widget: "Scene" and put the same scene definition in attrs. Do not add the outer Client page wrapper:

{
  "widget": "FeatureList",
  "title": "Interactive tour",
  "list": [
    {
      "title": "Explore the ship",
      "containerClass": "scene-feature",
      "items": [
        {
          "widget": "Scene",
          "attrs": {
            "res": {
              "ship": {"file": "/res/scene/ship.glb"},
              "sky": {"file": "/res/scene/studio.hdr"}
            },
            "env": {"envMap": "sky", "exposure": 1},
            "cam": {
              "exterior": {
                "label": "Exterior",
                "type": "orbit",
                "position": [12, 5, 18],
                "target": [0, 2, 0]
              }
            },
            "run": "ship",
            "scenes": {
              "ship": {
                "label": "Ship",
                "show": ["ship"],
                "cams": ["exterior"],
                "camera": "exterior"
              }
            }
          }
        }
      ]
    }
  ]
}

The same client-widget item can be used inside the nested items of a feature or card. Keep the scene definition unchanged; use the surrounding FeatureList classes to control its placement.

Give an embedded scene a definite usable height or aspect ratio in the site's CSS. Do not repeatedly derive a parent height from the canvas's changing rendered height, because that can create a ResizeObserver feedback loop.

Each Scene instance creates its own WebGL renderer and loads its own resources. Prefer one Scene with several named scenes over a grid of heavy independent viewers, and optimize GLB and HDR assets before publishing.

Authoring and validation

Set debug: true temporarily to display the + control. It asks for a name and captures the current camera pose as an in-memory spot. Copy the resulting pose into the appropriate camera's spots array; debug changes are not a replacement for editing the source JSON.

Before publishing:

  1. Verify that every ID used by envMap, show, cams, camera, run, and follow targets exists.
  2. Test every scene, camera, spot, and animation control.
  3. Test mouse, keyboard, touch, and resize behavior.
  4. Check the page on a mobile device and on a machine with a modest GPU.
  5. Confirm that models and HDR files are appropriately compressed for web delivery.