Model loading · class

DrftLoader

A .drft loaded progressively: the model builds up on screen instead of appearing.

Explained in Importing models.

class DrftLoader
import { DrftLoader } from '@driftengine/assets';

In depth

What this exists to prevent is the two obvious ways of doing it, both of which are worse. Reading the whole file and uploading it at the end switches from nothing to everything after several seconds, which demonstrates nothing about a format built to arrive in priority order. Uploading each part the moment its bytes land puts an unbounded number of GPU allocations in one animation frame and hitches the page. This does neither: bytes stream, arrivals queue, and a bounded number of parts are uploaded per frame with a fade.

It starts on an outline where the file carries one. A LODM chunk is a coarse whole model laid out ahead of everything else, so it is uploaded first and drawn until the real geometry is complete — which turns "nothing, then parts appearing" into "an object, sharpening". The parts fade in over it and the swap to the finished model is atomic, because a frame showing both is showing the same car twice. A caller that would rather hold the last picture says outline: false. See DrftLoaderOptions.outline.

It ends in one mesh per material group. The reveal wants one upload per part and the steady state wants as few draws as possible, so the load does the first and finishes by doing the second: after the last byte the parts are merged by the image and blend they wear, uploaded once, and the per-part meshes are disposed. On a 187-part car that is 187 draws while loading and seven afterwards.

Nothing here touches WebGL. Every GPU object comes from the Renderer it is handed, which is what keeps the engine's rule about where raw GL may live intact.

Constructor

new

constructor(renderer: RendererApi, options?: DrftLoaderOptions)
ParameterTypeDescription
rendererRendererApi
options?DrftLoaderOptions

Accessors

NameTypeDescription
progressgetDrftLoadProgress
partsgetreadonly DrftPart[]The parts to draw this frame. Grows while loading, then becomes the merged groups.
More

While a coarse level is on screen it is the first entry, so a caller drawing them in order draws the outline behind the parts that are replacing it. It leaves the list in the same frame the merged model enters it.

nodesgetreadonly DrftNode[]The asset's hierarchy, or empty for a file carrying no NODE.
More

Handed on rather than consumed here, because what to do with a hierarchy is the caller's: this class draws parts, and placing them under a graph is a scene decision.

lightsgetreadonly DrftLight[]The lights the file was authored with, in glTF's units, or empty. They arrive ahead of the geometry. What a candela is in this scene's light units is the caller's decision.
collidersgetreadonly Float32Array[]The convex hulls the asset collides as, in the file's own space and unfitted, or empty. Handed on for the caller to build shapes from, as hullShape takes them.
entitiesgetEntsScene | nullThe asset's serialised entities, or null for a file carrying no ENTS — handed on as deserializeWorld takes them, because what an entity is belongs to the caller's schemas.
regionsgetReadonlyMap<number, LoadedRegion>A streamed world's regions by id, each filling as its meshes and props upload — pending is 0 once one is whole. Their meshes are never among parts: a region's levels are alternatives, drawn one at a time through HlodSet, and its batches cull instance by instance.
lightVolumegetDrftLightVolume | nullA world's summed lights, unfitted, or null until LVOL lands and for a file carrying none. Handed straight to createWorldLightField, whose volume it is shaped as.
fieldsgetreadonly DrftFieldPlacement[]The distance fields the file carries, each where it stands: at the loader's fit, and once a copy for a mesh drawn many times. Empty until the fields and the fit have both arrived, and for a file carrying none. A scene declares these to addDistanceField every frame, with the colour of the surface each covers. See fieldPlacement.ts.
skinsgetreadonly DrftSkin[]Skins the file carried, in the order their chunks appeared. Empty for a file with none.
clipsgetreadonly AnimationClip[]Clips the file carried, in the order their chunks appeared. Empty for a file with none.
morphsgetreadonly DrftMorph[]Morph deltas the file carried, each naming the mesh ordinal it deforms.
hasOutlinegetboolean
revealStepsgetnumberHow many states the finished reveal can be put back into, or 0 when none were kept.
More

A count of states rather than a fraction, because the states are what a viewer is actually choosing between and they are not evenly spaced in anything: an outline is one thing, a part is one thing, and the merge is one thing. A caller drives it with an integer so every state is reachable — a fraction would make the outline a band 1/188th of a slider wide, which is a state nobody can stop on.

State 0 is an empty stage. State 1 is the outline where the file carried one, and the first part where it did not. The last state is the finished model, drawn exactly as ready draws it. Everything between adds one more part, in the order the file delivered them.

texturesgetTextureSet<SurfaceTextureHandle> | nullThe asset's images by the name its source gave them, or null before any have arrived.
modelBoundsgetreadonly number[]The model's own bounds, from HEAD, which arrives before any geometry.
placementget{ readonly scale: number; readonly x: number; readonly y: number; readonly z: number; } | nullHow the model was fitted: a uniform scale and an offset that centres it on the origin with its base at zero. Null until HEAD lands.

Methods

pageRegion

pageRegion(id: number, level: number, resident: boolean): void

Bring a region's paged level up, or free it. A level that arrived as assemblies (paged) is held as the copies the file carried; asked for, each mesh is expanded and uploaded on the loader's clocked queue in later updates, and resident turns true when the last is up. Freed, its meshes go and the copies stay. A level that is not paged ignores this.

ParameterTypeDescription
idnumber
levelnumber
residentboolean
More

The caller decides from what it will draw: page in the levels HlodSet is choosing or about to, draw a coarser one until the fine one is resident, and page out what the eye has left.

replay

replay(step: number): void

Put one state of the reveal back on screen. Needs keepReveal, and does nothing without it.

ParameterTypeDescription
stepnumber
More

This is a presentation control, not a second loader. Nothing is re-read, re-uploaded or re-merged: the meshes the load made are all still there and this decides which of them parts names. So it is free to call every frame, and a caller may scrub it as fast as it likes without touching the GPU.

load

load(url: string, fitTo: DrftFit): Promise<void>

Start loading, and resolve when the last byte has been read.

ParameterTypeDescription
urlstring
fitToDrftFit
More

Deliberately not the thing that produces the parts: the parts appear through update, so a caller draws its frames as usual and never awaits anything. A failed load leaves the phase at failed with a message rather than throwing, because a scene is usually still valid without its model and a blank screen would report the wrong problem.

fitTo is the size the model's larger of footprint and height is scaled to fit, which is the one thing a loader cannot infer: a bought asset arrives in whatever units its author used. baseY is where its lowest point should sit, for a scene whose floor is not at zero — a plinth, a turntable, a deck. It defaults to zero, and getting it wrong is very visible: the first version of this had no such parameter and put a car's wheels through the turntable it was standing on.

{ fit: 'none' } declines all of it — scale 1, no centring, no base — for a caller whose importer has already placed the model. See DrftFit for why that is a stated option rather than a very large footprint.

consume

consume(response: Response, fitTo: DrftFit): Promise<void>

The same load, from a container this class did not fetch.

ParameterTypeDescription
responseResponse
fitToDrftFit
More

Split out because a .drft is not the only way a model arrives. A source format read at runtime is parsed and written to a container in a worker, and what comes back is bytes that have never been near a URL. Everything after the first byte is identical, so it would be a second copy of the stream, the upload budget, the fade and the merge, kept in step by hand. A Response is the seam because it is what streamDrft already takes, and one can be built over an ArrayBuffer with no copy and no server.

update

update(dtSec: number): boolean

Spend this frame's upload budget and advance the fades. Call once per frame.

ParameterTypeDescription
dtSecnumber
More

Returns whether anything changed, for a caller that wants to know it should redraw a progress readout rather than rebuild one every frame.

dispose

dispose(): void