Model loading · class
DrftLoader
A .drft loaded progressively: the model builds up on screen instead of appearing.
Explained in Importing models.
class DrftLoaderimport { 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)| Parameter | Type | Description |
|---|---|---|
renderer | RendererApi | |
options? | DrftLoaderOptions |
Accessors
| Name | Type | Description |
|---|---|---|
progressget | DrftLoadProgress | |
partsget | readonly DrftPart[] | The parts to draw this frame. Grows while loading, then becomes the merged groups.MoreWhile 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. |
nodesget | readonly DrftNode[] | The asset's hierarchy, or empty for a file carrying no NODE.MoreHanded 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. |
lightsget | readonly 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. |
collidersget | readonly 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. |
navigationget | NavPolyMesh | null | The asset's navigation mesh, unfitted, or null for a file carrying no NAVM. |
entitiesget | EntsScene | null | The 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. |
regionsget | ReadonlyMap<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. |
lightVolumeget | DrftLightVolume | null | A 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. |
fieldsget | readonly 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. |
skinsget | readonly DrftSkin[] | Skins the file carried, in the order their chunks appeared. Empty for a file with none. |
clipsget | readonly AnimationClip[] | Clips the file carried, in the order their chunks appeared. Empty for a file with none. |
morphsget | readonly DrftMorph[] | Morph deltas the file carried, each naming the mesh ordinal it deforms. |
hasOutlineget | boolean | |
revealStepsget | number | How many states the finished reveal can be put back into, or 0 when none were kept.MoreA 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 |
texturesget | TextureSet<SurfaceTextureHandle> | null | The asset's images by the name its source gave them, or null before any have arrived. |
modelBoundsget | readonly 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; } | null | How 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): voidBring 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.
| Parameter | Type | Description |
|---|---|---|
id | number | |
level | number | |
resident | boolean |
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): voidPut one state of the reveal back on screen. Needs keepReveal, and does nothing without it.
| Parameter | Type | Description |
|---|---|---|
step | number |
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.
| Parameter | Type | Description |
|---|---|---|
url | string | |
fitTo | DrftFit |
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.
| Parameter | Type | Description |
|---|---|---|
response | Response | |
fitTo | DrftFit |
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): booleanSpend this frame's upload budget and advance the fades. Call once per frame.
| Parameter | Type | Description |
|---|---|---|
dtSec | number |
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