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

@nickpaustian/stelligra

v0.1.0

Published

A Gaussian splat renderer for three.js: PLY and SOG loaders, one draw call for every splat, a radial-distance sort in a worker (JS or WASM), slices that stream in and out under a GPU budget, and meshes for the simple case

Readme

@nickpaustian/stelligra

A Gaussian splat renderer for three.js. It loads .ply (INRIA) and .sog (WebP bundle) splat files, parses them in a worker into GPU-ready arrays with covariances precomputed, keeps every splat in textures and draws them all in one call, and sorts them back-to-front in a worker by radial distance from the camera, which stays correct while the camera turns. Scenes that stream, an octree of slices under a GPU budget, are the design centre; a single splat file is the simple case on top of it. three.js is a peer dependency.

import * as THREE from "three"
import { SplatLoader, SplatMesh, SplatRenderer } from "@nickpaustian/stelligra"

const splats = new SplatRenderer({ shDegree: 2, sortFrustumCull: true })
scene.add(splats)

const loader = new SplatLoader({ parseMode: "gaussianSplat", useWorker: true })
const mesh = new SplatMesh(await loader.loadFromURL("/scene.ply"))
mesh.rotation.x = Math.PI
splats.add(mesh)

renderer.setAnimationLoop(() => {
  splats.update(camera, renderer)
  renderer.render(scene, camera)
})

Loading

new SplatLoader({ parseMode: "gaussianSplat" | "pointCloud", useWorker }) with loadFromURL, loadFromFile and loadFromBuffer(buffer, "ply" | "sog"). The result is GaussianSplatData: positions, RGBA colours (opacity through its sigmoid), the upper triangle of each 3D covariance (from scale and rotation, so the GPU never sees quaternions), optional spherical harmonics and bounds. parsePLYAsGaussianSplat, parseSOGBundle and friends are exported for use without the loader.

Rendering

SplatRenderer is a THREE.Mesh at the world origin, added to the scene once. update(camera, renderer) each frame uploads anything pending and asks the worker for a sort when the camera has moved (or every eighth frame regardless, to catch what changed). Options: shDegree (a compile-time define), splatScale, maxScreenSpaceSplatSize, antialiased, sortFrustumCull with sortCullBaseAngle (cull to the frustum before sorting, with a margin widened by how fast the camera is turning), minResidentSliceLevel, isCameraDirty (skip sorts when nothing moved), log (capacity events only: reserved, grown, exhausted; silent by default) and sortBackend.

Slices

The renderer is a slot allocator over one capacity of splats. reserveCapacity(n) sets the budget (see probeGPUCapacity below), or leave it and the textures grow as needed. allocateSlice(count, level, onEvict) returns an id for a slot range that never moves; uploadSlice(id, data, worldMatrix?) takes compact positions, colours, covariances and optional SH, bakes them through the matrix and shows the slice on the next update; uploadPreScattered(id, center, colour, cov, boundsMin, boundsMax, renderer) takes texture-ready arrays, as a decode worker produces them, and uploads at once. showSlice and hideSlice flip a dead bit without touching the GPU; evictSlice frees the range and freeSlice forgets it; getLRUAllocations(forLevel) lists eviction candidates, hidden first, then by an age, size and level score; when a budgeted allocation does not fit, the allocator evicts along that list itself and returns −1 only if the budget is truly exhausted. Slices at or below minResidentSliceLevel and protected ones are never offered.

Meshes

A SplatMesh holds GaussianSplatData and a transform and owns nothing on the GPU. add(mesh) bakes its world transform into one slice, refreshMesh(mesh) re-bakes after a move, remove(mesh) or mesh.dispose() frees it. Everything registered sorts together in the one draw, so overlapping meshes composite correctly.

The sort

A worker keeps the positions resident, receives slot ranges as they are uploaded and the dead mask only when it changes, and returns a transferred index buffer. The key is distance from the camera, bucketed into 65536 bins between the nearest and furthest visible splat, in one counting pass. Two backends run that algorithm: js (default) and wasm, a Rust build of the same loop (packages/sort-worker-wasm), chosen with sortBackend or setSortBackend at any time; activeSortBackend says which is running (WASM falls back to JS if it cannot load). benchmark(camera, renderer, iterations) times the sort in the worker and reports the backend. lastSortMs is the last sort's time.

GPU budget

probeGPUCapacity(renderer, { log, shDegree, candidates, maxAgeDays, force }) finds how many splats the device can hold: for each size, ascending, it allocates the renderer's textures (centre, colour, covariance, and SH when shDegree is above zero) and writes to them, so the memory has to be real, and skips sizes whose staging copy would not fit a quarter of navigator.deviceMemory. Progress is written to localStorage before each attempt, so a size that crashes the page is skipped on the next load instead of crashing again; a finished result is kept per device (the unmasked renderer name and the texture limit) for thirty days. bytesPerSplat(shDegree) and heapCeiling(deviceMemoryGB, shDegree) are the sums behind it. Pass the result to reserveCapacity.

SOG decoding in a worker

@nickpaustian/stelligra/sog exports the WASM SOG decoder (packages/sog-wasm, Rust): init(wasmUrl) then decode_sog(bytes, worldMatrix) returns one packed buffer of texture-ready center, colour and covariance arrays plus bounds, for uploadPreScattered. It is meant for a worker that decodes slices off the main thread.

Building the WASM

The sort and the decoder are Rust crates. npm run build:wasm at the repo root rebuilds both (needs cargo, the wasm32-unknown-unknown target, wasm-pack, and optionally wasm-opt); the sort is embedded as base64 in SortWasm.ts and the decoder's glue and binary land in src/wasm/sog. Neither is needed to build or use the package as published.