playcanvas / playcanvas/engine
Unified gsplat: component-scoped 'renderable' signal for streamed (LOD) splats
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 16.8k
- Forks
- 2k
- Avg merge
- 4h 32m
- Merged PRs (30d)
- 222
Description
Description
Feature request: a component-scoped "safe to start rendering" signal for streamed gsplats — an event that fires when a splat has the minimal set of LODs resident to render a coherent scene (no missing chunks), even though streaming continues to refine it. The analogy is HTMLMediaElement's canplay, not ended.
Current state
GSplatComponentSystemfiresframe:ready(EVENT_FRAMEREADY) every frame, per(camera, layer), with(camera, layer, ready, loadingCount). BothreadyandloadingCountare world-scoped:loadingCountis the total pending across every octree instance and placement on that layer. This is ideal for what it was designed for (video capture, whole-frame synchronization), but it only expresses "everything currently demanded has finished loading".GSplatComponentitself fires no events, and exposes no per-component loading state.
Motivating use case
The "reveal" moment: hold a cover (or poster) over the canvas until the scene can be shown without holes, then reveal while higher LODs keep streaming in.
Today the SuperSplat Viewer (and the PlayCanvas Web Components Splat Streaming example, playcanvas/web-components#409) can only approximate this with a two-step trick:
// 1. pin streaming to the coarsest LOD so 'fully loaded' means 'coarse pass complete'
gsplatComponent.lodRangeMin = lodLevels - 1;
// 2. wait for the world to settle, then reveal and lift the pin
app.systems.gsplat.on('frame:ready', (camera, layer, ready, loadingCount) => {
if (ready && loadingCount === 0) {
// reveal, restore lodRangeMin, let higher LODs refine...
}
});
The pin is load-bearing: without it, "nothing left loading" would mean full-quality streaming finished — far too late for a reveal. A built-in "minimal coverage reached" event would make the pin/unpin dance unnecessary, and would work for consumers who never touch lodRange at all.
It would also let PlayCanvas Web Components surface a one-shot, element-scoped DOM event on <pc-gsplat> (analogous to <img> load), which frame:ready cannot back correctly:
- With two splat components, each element would report the union of both (one still-loading splat delays the other's signal).
- With two cameras, the event fires per camera with different values, so an element-level signal would be ambiguous or duplicated.
- It fires every frame, which is the wrong cadence to forward as a DOM event.
Why this looks tractable engine-side
GSplatOctreeInstance already tracks the relevant state per instance: per-node resident/target LODs, pending/prefetchPending load sets, and an unresolved environment splat. Detecting "every node the scene needs has at least one LOD resident (coarsest counts) and the environment is placed" is bookkeeping over existing state rather than new machinery.
Proposal (shape open to discussion)
An event on GSplatComponent — say 'renderable' — fired once the component first reaches minimal coherent coverage: every node required for rendering has at least some LOD resident, and the environment splat (if any) is placed. Streaming continues afterwards; the event is about the start of useful rendering, not completion. Optionally, a readable per-component pending/progress value for loading UIs as a separate, secondary addition.
Open questions:
- What counts as "coverage"? Scene-wide (every octree node has its coarsest LOD resident — camera-independent, never shows holes on later camera moves) vs. current-view demand (what the active camera needs right now — faster to reach, but a later pan can still hit unloaded nodes). Scene-wide-at-coarsest matches what the Viewer's pin trick achieves today. An accompanying loading policy that prioritizes a scene-wide coarse pass first would make the event fire as early as possible.
- Camera multiplicity. Octree instances live per
(camera, layer)manager, so one component can have several instances. With scene-wide-coarsest semantics this mostly evaporates; with view-demand semantics it needs a rule (all cameras vs. per-camera args). - Non-LOD splats. For uniformity, a single-resource splat (
.ply/.sog) could fire the same event once its resource is placed, so consumers need not special-case octree vs plain assets.
If the shape lands in the engine, PlayCanvas Web Components would forward it as a one-shot DOM event on <pc-gsplat>, matching its existing pc-asset load/error precedent.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with GSplatComponentSystem, GSplatComponent, and GSplatOctreeInstance, including EVENT_FRAMEREADY and the existing pending, prefetchPending, resident/target LOD, and environment state. Resolve the coverage and camera semantics before choosing the event shape. Done means a component-scoped signal fires once when minimal coherent coverage is reached while streaming can continue.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- computer-graphics, game-dev
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100