Model loading · interface

DrftLoaderOptions

Explained in Importing models.

interface DrftLoaderOptions
import type { DrftLoaderOptions } from '@driftengine/assets';

Properties

NameTypeDescription
fetchImplreadonlyoptionalFetchLikeWhere the bytes come from. The global fetch unless a consumer says otherwise.
More

Optional and defaulting to the browser's, so no existing caller changes — the shape AGENTS.md asks for: take the capability as a parameter, ship a browser implementation as the default.

bcWorkerreadonlyoptional() => BcWorkerWhat starts the worker a BC texture the device cannot take as blocks is decoded in: spawnBcWorker, from @driftengine/assets/bcWorkers. Absent, they decode on the main thread and the loader says so once — the factory is behind its own specifier so a consumer that never names it has no worker in its build. See bcWorkers.ts.
More

On a device that samples ETC2 and not BC, which is a phone, a second worker from the same factory then re-encodes each such texture as ETC2 or EAC and swaps the chain in behind the handle its decoded image went up under, at half a byte a texel, or a byte with alpha, where the image holds four. Nothing arrives later for it; see etc2Load.ts for what it costs. Without a worker the textures stay RGBA.

uploadsPerFramereadonlyoptionalnumberThe most parts one update may take, however cheap they turn out to be.
More

A ceiling, not the budget. The budget is uploadMsPerFrame, because what a part costs cannot be known by counting parts — see mayBeginMore. This stays as the guard against the opposite shape of asset, a model of hundreds of tiny parts where the clock would happily take a hundred of them in one frame and spend the whole reveal in two.

uploadMsPerFramereadonlyoptionalnumberHow long one update may spend starting new stream work, in milliseconds.
More

DEFAULT_UPLOAD_MS_PER_FRAME unless stated. Raise it for a tool that would rather have the model in front of it sooner than hold a rate; lower it for a scene where the frame matters more than the wait. Zero is the slowest honest setting rather than a stall: one piece a frame still begins, because a load has to finish.

revealSecreadonlyoptionalnumberSeconds for a part to fade from nothing to itself. Zero makes it appear.
anisotropyreadonlyoptionalnumberAnisotropic filtering asked of every image, where the extension exists.
More

A model's maps are the case for it: tread on a tyre, a panel gap, a plate, all bands of fine detail seen at a grazing angle, which is exactly where a trilinear sample gives up.

textureWrapreadonlyoptional'repeat' | 'clamp'How every image this asset carries behaves past its own edge. repeat unless stated.
More

Reported from outside, and it is a real trap on a bought model. A surface texture repeats by default, which is right for a tiling map and wrong at the border of a UV island: a sample whose footprint crosses the edge wraps to the far side of the image, and a mip level high enough averages across islands that have nothing to do with each other. On a model whose untextured surfaces sit at an exporter's default 0.8 grey, the result is single bright pixels tracing every seam of every textured part, which is invisible against a light background and obvious against black paint.

clamp is the repair where an asset's own UV padding is thin, and it is stated rather than assumed because a model that genuinely tiles a map needs repeat and would band without it.

transformreadonlyoptional(mesh: MeshData, material: DrftMaterial | undefined) => MeshDataA last chance to change a mesh's vertex data before it is uploaded.
More

For a scene that dresses an import rather than taking it as it comes: a bought asset that stores every surface as the same default grey while naming them body, glass and chrome can have its paint decided here, per material, without the loader knowing what a car is.

surfacereadonlyoptional(material: DrftMaterial | undefined) => DrawSurfaceOverride | undefinedPer-material opacity, reflectivity and metalness overrides, for the same reason as transform.
outlinereadonlyoptionalbooleanDraw a coarse whole model while the parts arrive, where the file carries one.
More

On when the file carries one, and false refuses it. This was off by default for two releases, on the argument that a LODM chunk is a decimated model and how good it looks is a property of the asset rather than of this code. That argument was about the old hull, which was built by clustering vertices and joined clusters lying on different surfaces of the model, so a car grew spikes through its own bodywork and somebody waiting for it saw white cliffs. A bad preview reads as a broken import where an empty stage reads as a load, so refusing to guess was right.

The hull is the surface of an occupancy grid now (buildCoarseLevel), which cannot do that: it emits the boundary of a solid, so the worst case is a coarse version of the shape rather than a shape that was never there. And nothing carries a LODM by accident. A chunk exists only because somebody baked one, so ignoring it by default meant the file said one thing and the reader did another.

outline: false still refuses, for a caller swapping a model already on screen for a better one, where a coarse version of the new one is a step backwards.

keepRevealreadonlyoptionalbooleanKeep the arrival state after the load, so replay can put any point of it back on screen.
More

Off by default, because it costs a second copy of the model on the GPU. The load normally ends by disposing the outline and the per-part meshes, since the merged groups draw the same geometry in seven draws rather than 188. Holding them is what makes the reveal inspectable rather than a thing that happens once and cannot be looked at again: a stage that is only ever seen while the bytes are arriving can only be studied by reloading, at whatever speed the network happens to give.

The cost is stated rather than hidden: on a 187-part car the per-part meshes are about the same vertex count as the merged ones, so this roughly doubles the geometry the asset holds. For a tool, a documentation page or a demo that is worth it. For a game shipping a level it is not, which is why it is not the default.

texturePreviewreadonlyoptionalnumberPut a small version of each image on its surfaces before the full one, at this many pixels on the longest side. Absent means every image arrives once, at full size.
More

This is the blur-to-sharp arrival of docs/FORMAT.md §4.6, done from the bytes already in the file. The section asks for mip levels stored smallest first as separate chunks, which is the better answer for a metered connection because it can stop early and keep the small one. It needs the baker to resize an image, and the baker runs in Node where there is no decoder for a JPEG, so that half is a dependency decision rather than an implementation.

What is available without one: createImageBitmap resizes while it decodes. A 256 pixel version of a 2048 square costs a fraction of the full decode, so a surface stops being flat about a second earlier on a real model, and the full image then replaces it in the same GPU object. It costs a second decode per image, off the main thread, and not one byte of the file — which is why it is an option a caller weighs rather than a default.

onImagereadonlyoptional(name: string, image: ImageBitmap | null) => voidTake each image as it decodes, by the name its TEXS chunk gives it, and upload none: for a consumer that builds its own textures — a world packing every picture into a layer of an array — where a surface texture of each as well would spend the memory twice. On the same frame clock as an upload, one a frame; null for an image that did not decode. Materials in the file that name an image then draw untextured, which is the consumer's to answer.
onMeshreadonlyoptional(mesh: MeshData, ordinal: number) => booleanOffered each mesh no region holds as it arrives, by its ordinal in the file; return true to take it, and it is neither uploaded as a part nor merged. For a consumer that draws some of a file's meshes its own way — a crowd of kinds it instances and moves every frame — which a part cannot be, since the merge bakes a part into a static group by the image it wears. Counted as arrived, so a loading bar still finishes. What it gives up: the loader's upload budget and fade, which a taken mesh leaves to the consumer.