npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

streaming-gltf

v1.0.19

Published

Streaming progressive glTF LOD renderer (BatchedMesh/InstancedMesh tiers, network-lazy GPU-eager LOD streaming, on-GPU position lerping) plus the local bake/convert + streaming-download pipeline (tools/bake-*.mjs).

Readme

Progressive glTF LOD renderer

A self-contained three.js renderer for large scenes of distinct glTF/GLB models with progressive LOD streaming, plus the local pipeline that converts source models into the progressive format it consumes.

Live demo

https://anentrypoint.github.io/streaming-gltf/ — the stress demo, deployed from examples/local-progressive/ by .github/workflows/deploy-pages.yml. It ships code only: three loads from a CDN (importmap) and the baked models are streamed cross-origin from the assets host (https://anentrypoint.github.io/assets/, derived from its manifest.baked.json). Override the asset source with ?assets=<baseUrl>, or use ?assets=local with the dev server (npm run demo:local).

SDK usage

streaming-gltf is an importable ES module. three and @pixiv/three-vrm are peer dependencies — provide them yourself (e.g. via an importmap pointing at a CDN build, or your bundler); they are not bundled.

import { ModelPool } from 'streaming-gltf';
// or: import { BatchedFarTier } from 'streaming-gltf/batched-far-tier';

const pool = new ModelPool({ scene, renderer, camera });
const entity = pool.spawn(url, { position: [x, 0, z] });

// per frame, after advancing the camera:
pool.update();

// sparse position targets — the GPU interpolates each frame (far tier),
// so moving entities cost ~no per-frame CPU matrix writes:
pool.setTarget(entity, x, y, z, durationMs);

VRM support

VRM avatars load through @pixiv/three-vrm v3 (a peer dependency). When a baked GLB carries the VRMC_vrm extension, the GLTFLoader is registered with VRMLoaderPlugin and the parsed gltf.userData.vrm runtime is driven each frame by pool.update() — humanoid bones, spring bones, expressions, and look-at all animate. The HUD in the example reports the detected humanoid bone count.

What works:

  • Full humanoid / spring-bone / expression / look-at runtime on a VRM avatar.
  • Progressive mesh + texture LOD on the avatar's primitives, exactly as for any other model. Sibling LOD chunks are loaded without the VRM plugin (includeVrm: false), so MToon material setup runs once on the root only.
  • MToon materials are LOD-swapped safely: texture-LOD application matches strictly by texture name against the material's existing slots, so it never stamps a foreign bitmap into an MToon slot.

Multi-driver — every instance animates independently. @pixiv/three-vrm v3 exposes no skeleton-rebind clone (VRM.prototype is [constructor, update] only; there is no vrm.clone() / VRMUtils.clone), and its humanoid/spring-bone/ expression managers bind the bones of the scene they were parsed against. Rather than share one runtime, the pool retains the asset's raw GLB bytes and re-parses an independent VRM per driven entity — each gets its own scene, its own humanoid/ spring-bone/expression managers, and is driven independently by vrm.update(dt). N pooled instances of one VRM all animate (distinct spring physics, distinct expressions); none freeze in bind pose. Per-instance parses are concurrency-bounded (heavy work) and queue when the limit is reached. This mirrors how a multiplayer host parses one VRM per player.

entity.dispose() runs VRMUtils.deepDispose() on that entity's own VRM scene, freeing its spring/collider/expression GPU resources without touching siblings. pool.dispose() tears the pool down (every entity, then every asset).

Geometry decoding (Draco + meshopt)

Baked GLBs are compressed with both meshopt and Draco (KHR_draco_mesh_compression). meshopt decodes via three's MeshoptDecoder. Draco decodes via draco.js, mrdoob's pure-JavaScript port of the Draco decoder, vendored at examples/local-progressive/draco-loader.js as a drop-in for three's own DRACOLoader.

This replaces the WASM Draco decoder. The win is deployment simplicity: no .wasm fetch and no runtime CDN — the previous setup pulled the decoder from https://www.gstatic.com/draco/versioned/decoders/... on first decode, a third-party runtime dependency with cross-origin/CSP exposure. draco.js is one ~24 KB-gzipped ES module that ships with the page. It is decode-only (the bake-time encoder in tools/bake-progressive.mjs still uses the draco3dgltf WASM module, which is a dev-time Node dependency, not shipped to the browser), and implements the EdgeBreaker triangle-mesh path that glTF/Draco content uses. WASM is faster in absolute terms (~1.4-1.6x on large meshes) but the decode is byte-for-byte equivalent.

The LOD web worker (lod-worker.js) loads the same vendored module, rewriting its bare three import to the esm.sh URL the worker already uses, so Draco+meshopt sibling LODs decode off-thread too. Decoder logic is a port of Google Draco (Apache-2.0); the loader API mirrors three.js's DRACOLoader (MIT).

Textures (single GPU-compressed KTX2)

Each texture is baked to one mipmapped KTX2 (Basis Universal) file (KHR_texture_basisu) -- no per-size webp ladder. Color maps use ETC1S (sRGB, smallest), normal/ORM maps use UASTC (linear; ETC1S wrecks normals). At runtime a KTX2Loader (transcoder vendored at examples/local-progressive/basis/, served locally) transcodes it to a GPU-compressed texture for the device (BC7/BCn on desktop, ASTC/ETC2 on mobile) -- witnessed isCompressedTexture with a 10-level mip chain. The GPU mip chain handles distance LOD, so texture streaming is eliminated: progressive streaming is now geometry/vertex-only. Far entities render vertex-colored geometry (baked to match the texture); the single KTX2 arrives with the root GLB for the near/textured view -- vertices first, then texture.

Downscaled to BAKE_TEX_SIZE (default 1024; set e.g. 512 to compress harder) before encoding. KHR_texture_basisu is a standard Khronos extension, so any loader with a KTX2 transcoder (three, Babylon, model-viewer) opens it; it is declared required (KTX2 carries no fallback image), but the untextured geometry+vertex-color far path covers loaders without it. Encoded with ktx2-encoder (Basis); the transcoder is three.js's vendored Binomial Basis wasm (Apache-2.0).

Layout

  • examples/local-progressive/ — the renderer (latest). Entry: stress.htmlstress.jsmodel-pool.js (+ draw-call-batching.js, batched-far-tier.js, material-pool.js, deferred-load-queue.js, lod-unload-manager.js, frustum-cache.js, multi-draw-optimizer.js / multi-draw-utils.js, vertex-compression.js, draw-call-sorter.js, buffer-pool.js, lod-worker.js, octahedral-impostor.js / octahedral-impostor-tier.js). serve.mjs is the dev server; measure-fps.mjs the steady-state FPS harness; impostor-smoke.mjs / impostor-pool-smoke.mjs are the impostor test harnesses.
  • tools/ — the conversion + download pipeline:
    • bake-progressive.mjs — convert one source GLB into a progressive GLB (meshopt decimation + sharp texture resizing + a EP_progressive_lod extension referencing sibling LOD files).
    • bake-all.mjs — batch-bake every model under a source dir.
    • bake-streaming.mjs — download + bake for the streaming workflow.
  • models/ — source models fed to the bake tools.

Usage

Install deps once:

npm install

Convert source models into the progressive format the renderer loads (examples/local-progressive/output_<name>/):

npm run bake:local -- models/<model>.glb examples/local-progressive/output_<name>
# or batch every model under a directory:
npm run bake:all -- models

Run the renderer (serves the stress demo at /):

npm run demo:local
# open http://127.0.0.1:5180/

Measure steady-state FPS (hardware GPU via system Chrome):

CHANNEL=chrome npm run measure -- 500

glTF extension: EP_progressive_lod

The bake pipeline emits a glTF extension, EP_progressive_lod, that declares the progressive LOD ladder (per-mesh and per-texture) the renderer consumes. The base glTF always carries the coarsest LOD, and the extension is declared in extensionsUsed — never extensionsRequired — so a viewer that does not implement it simply renders that coarse base and ignores the rest. Two storage modes are covered by one extension, discriminated by a storage field: sibling-file (higher LODs in sibling files, bake-progressive.mjs) and single-glb-range (all LODs packed as bufferView byte ranges in one GLB, range-fetched on demand, bake-streaming.mjs).

Progressive (".plod-style") load on a regular glTF

single-glb-range is baked coarse-first: the base LOD (coarsest geometry + smallest textures) is packed immediately after the JSON chunk, and the default primitives/images reference it. So the one model.streaming.glb is a valid glTF any loader opens (it renders the coarse base; EP_progressive_lod is extensionsUsed, never required), AND a byte-prefix is the renderable base — a client can HTTP-Range-fetch the prefix to show coarse, then range-fetch finer LODs as they arrive, the progressive behaviour of a custom .plod stream but on a standard file. examples/local-progressive/plod-gltf-stream.js is the range consumer (loadStreamingHeader -> streamMeshLods coarse->fine); the dev server (serve.mjs) supports HTTP Range. Witnessed: the coarse base builds from ~58 KB of a 362 KB file (6 range requests) without downloading the whole model.

Size note: single-glb-range packs LOD attributes uncompressed (so each bufferView is a clean range to fetch), which is larger than the draco/meshopt sibling-file format. When download size matters more than single-file range-streaming, prefer sibling-file (the default), which also loads coarse-first and progressively. single-glb-range could adopt per-bufferView EXT_meshopt_compression to remove that size delta.

Registration status: the EP vendor prefix is not yet registered with the Khronos glTF extension registry; the name is provisional until a registration PR to KhronosGroup/glTF lands. Assets baked before the rename (payload under extras.LOCAL_progressive) are still read by the runtime via a compatibility fallback.

Octahedral impostors (final LOD)

Opt-in (ModelPool option useImpostorFinalLod, or the demo's ?impostor=1). Past the vertex-color BatchedMesh far tier, a model below impostorPx (default 14) screen pixels collapses to a single camera-facing billboard sampling a per-asset octahedral atlas — the model captured from a grid of directions and octahedral-encoded. Every distinct asset's atlas is a layer of one WebGL2 sampler2DArray, so the entire far population draws in one InstancedMesh draw call (octahedral-impostor-tier.js).

The atlas is rendered on the fly in-browser the first time an asset reaches impostor distance — no bake step, no extra download. To avoid frame stalls, the bake is incremental: impostorCellBudget octahedral cells (default 4) are rendered per frame and the atlas accumulates over a few frames; the array-texture is eager-allocated at pool construction so its VRAM allocation is off the swap path. The budget is headroom-gated (like the LOD warm loader) — doubled when FPS sits above target so coverage converges faster, halved under target so baking never deepens a frame deficit. ?impostorBlend=1 enables an alpha-weighted bilinear cross-fade of the nearest captured views to soften cell-to-cell popping as the camera orbits.

Each cell carries a small transparent gutter (impostorPadding, default 1.05: the model is captured at radius × padding) so a LinearFilter tap near a billboard edge lands on alpha-0 texels instead of bleeding the neighbouring view — without it, packed-edge-to-edge cells produce faint cross-view ghosting.

Lit impostor variant (?impostorEz=1)

An alternative impostor tier (octahedral-impostor-ez-tier.js) renders lit impostors: the on-the-fly atlas is a 2-target render (albedo + packed normal/depth), so the billboard receives scene lighting (a real MeshStandardMaterial) and blends the three nearest captured views with per-sprite plane-projected UVs. It bakes a 1024 atlas (handles ~1M-triangle source models) incrementally, capped by impostorMaxAssets. Opt in with ?impostorEz=1 (or useImpostorEz); the default tier above stays the single-draw performant path. The impostor sampling/baking code is localized from @three.ez/octahedron-imposter (octahedral-impostor-ez.js).

Notes

  • The renderer is draw-call-bound at scale; the FAR tier collapses many distinct models into a single THREE.BatchedMesh draw and the FPS controller adjusts LOD distance (not a global ceiling) to hold the target frame rate.
  • Baked output_*/ assets are git-ignored (regenerate with the bake tools).

Credits

  • @three.ez/octahedron-imposter by Andrea Gargaro (MIT) — the lit octahedral impostor (atlas capture + sampling shaders) is localized into examples/local-progressive/octahedral-impostor-ez.js (TS ported to JS, GLSL inlined, full-octahedron encode/decode completed). The related @three.ez/batched-mesh-extensions was evaluated but not vendored: its draw/cull wins are non-binding here (the far tier already draws in one call).
  • draco.js by mrdoob (Apache-2.0) — vendored pure-JS Draco decoder (draco-loader.js).