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

nanothree

v0.0.7

Published

Lightweight **WebGPU-only** 3D renderer that implements a subset of the [Three.js](https://threejs.org/) API, focusing on performance and small bundle size.

Readme

nanothree

Lightweight WebGPU-only 3D renderer that implements a subset of the Three.js API, focusing on performance and small bundle size.

nanothree is not a drop-in replacement for Three.js. It targets the same scene graph patterns and naming conventions, but ships a leaner renderer with fewer abstractions. If you need PBR materials, point lights, morph targets, or post-processing, use Three.js. If you want a fast, minimal WebGPU renderer with a familiar API, nanothree may be a good fit.

⚠️ Disclaimer: This project is entirely vibe coded (as in I didn't look at the code) and it is very likely to go unmaintained. Do not use it for anything important.

Features

  • WebGPU-native renderer (no WebGL fallback) with shadow mapping and frustum culling
  • Three.js-compatible scene graph: Object3D, Group, Scene, PerspectiveCamera, Mesh, Line, Sprite
  • 9 geometry types: Box, Sphere, Capsule, Cylinder, Cone, Circle, Plane, Torus, Tetrahedron
  • Lambert and unlit materials with texture mapping, per-vertex colors, and face culling
  • Custom shaders in WGSL (not TSL) via ShaderMaterial
  • GLTF/GLB loading with Draco mesh compression and KTX2/Basis texture support (decoders bundled, zero config)
  • Skeletal animation: Bone, Skeleton, SkinnedMesh, AnimationMixer, AnimationClip
  • Instancing: InstancedMesh and InstancedSprite for rendering thousands of objects
  • Raycasting: Moller-Trumbore ray-triangle intersection with screen-space picking
  • Shadow mapping: Directional light depth pass with 4-tap PCF filtering
  • Automatic frustum culling via bounding spheres
  • Interactive transform gizmo (translate, rotate, scale with snap-to-grid)
  • Debug helpers: CameraHelper, DirectionalLightHelper

Install

npm install nanothree

Quick Start

import {
  AmbientLight,
  BoxGeometry,
  DirectionalLight,
  Mesh,
  MeshLambertMaterial,
  PerspectiveCamera,
  Scene,
  WebGPURenderer,
} from 'nanothree'

const canvas = document.querySelector('canvas')!
const renderer = new WebGPURenderer({ canvas })
await renderer.init()

const scene = new Scene()
const camera = new PerspectiveCamera(60, canvas.clientWidth / canvas.clientHeight, 0.1, 100)
camera.position.set(0, 2, 5)
camera.lookAt(0, 0, 0)

scene.add(new AmbientLight(0x404040, 0.5))
const light = new DirectionalLight(0xffffff, 1)
light.position.set(5, 10, 5)
scene.add(light)

const cube = new Mesh(new BoxGeometry(1, 1, 1), new MeshLambertMaterial({ color: 0x4488ee }))
scene.add(cube)

function animate() {
  requestAnimationFrame(animate)
  cube.rotation.y += 0.01
  renderer.render(scene, camera)
}
animate()

API Overview

Renderer

const renderer = new WebGPURenderer({ canvas, antialias?: boolean })
await renderer.init()
renderer.render(scene, camera)
renderer.setSize(width, height)
renderer.setPixelRatio(devicePixelRatio)
renderer.shadowMap.enabled = true
renderer.p3Boost = 0 // 0..1 saturation dial — see below
renderer.info // { drawCalls, triangles }

Saturation / P3 boost

renderer.p3Boost is a single dial in [0, 1] that boosts saturation into the Display-P3 gamut without changing the overall tint of the scene:

  • 0 (default): canvas stays in sRGB. Bit-exact equivalent to the previous colorSpace: 'srgb'. Zero runtime cost.
  • > 0: canvas switches to display-p3. Every color (solid materials, vertex colors, sampled textures, custom shaders) is pushed through an OKLab chroma scale that boosts saturation uniformly across hues — i.e. without favouring red/green the way a naive sRGB→P3 reinterpretation does. At 1 chroma is multiplied by 1.5 in OKLab. The conversion lives in WGSL (P3_BOOST_WGSL), so authoring is always done in sRGB — users don't have to think about P3.

Scene Graph

All scene objects extend Object3D:

object.position.set(x, y, z) // Vector3
object.rotation.set(x, y, z) // Euler (radians)
object.scale.set(x, y, z) // Vector3
object.visible = true
object.castShadow = true
object.receiveShadow = true
object.add(child)
object.remove(child)

PerspectiveCamera adds a lookAt method and supports an orthographic override:

const camera = new PerspectiveCamera(fov, aspect, near, far)
camera.lookAt(x, y, z)
camera.orthoOverride = { left, right, bottom, top } // or null for perspective

Geometry

All geometries extend BufferGeometry and generate positions, normals, UVs, and indices:

| Class | Constructor | | --------------------- | ---------------------------------------------------------------------- | | BoxGeometry | (width, height, depth, wSegs?, hSegs?, dSegs?) | | SphereGeometry | (radius, wSegs?, hSegs?, phiStart?, phiLen?, thetaStart?, thetaLen?) | | CapsuleGeometry | (radius, height, capSegs?, radialSegs?) | | CylinderGeometry | (radiusTop, radiusBot, height, radialSegs?, hSegs?, openEnded?) | | ConeGeometry | (radius, height, radialSegs?, hSegs?, openEnded?) | | CircleGeometry | (radius, segments?) | | PlaneGeometry | (width, height, wSegs?, hSegs?) | | TorusGeometry | (radius, tube, radialSegs?, tubularSegs?, arc?) | | TetrahedronGeometry | (radius) |

Custom geometry via BufferGeometry:

const geo = new BufferGeometry()
geo.setAttribute('position', new Float32BufferAttribute([...], 3))
geo.setAttribute('normal', new Float32BufferAttribute([...], 3))
geo.setAttribute('uv', new Float32BufferAttribute([...], 2))
geo.setIndex([0, 1, 2, ...])

Materials

MeshLambertMaterial - Lit material with Lambert shading and shadow support:

new MeshLambertMaterial({
  color: 0xff0000, // hex or Color instance
  map: texture, // NanoTexture (albedo)
  wireframe: false,
  side: FrontSide, // FrontSide | BackSide | DoubleSide
  vertexColors: false,
})

MeshBasicMaterial - Unlit, flat color:

new MeshBasicMaterial({ color: 0xff0000, wireframe: false, side: FrontSide })

LineBasicMaterial - For Line objects:

new LineBasicMaterial({ color: 0xffffff })

SpriteMaterial - For camera-facing billboard Sprite objects:

new SpriteMaterial({ color: 0xffffff, opacity: 1, blending: NormalBlending })
// blending: NormalBlending (alpha) or AdditiveBlending

Custom Shaders (WGSL)

nanothree uses WGSL for custom shaders (Three.js uses TSL/GLSL). The renderer auto-prepends a preamble with scene uniforms, so your shader has access to the view-projection matrix, light data, shadow map, and per-object transform:

const material = new ShaderMaterial({
  code: /* wgsl */ `
    // Available from preamble:
    // scene.viewProj, scene.lightDir, scene.ambient, scene.lightColor, scene.lightViewProj
    // objectData.model, objectData.color

    struct VSOut {
      @builtin(position) pos: vec4f,
      @location(0) normal: vec3f,
    }

    @vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f) -> VSOut {
      var out: VSOut;
      out.pos = scene.viewProj * objectData.model * vec4f(position, 1.0);
      out.normal = normalize((objectData.model * vec4f(normal, 0.0)).xyz);
      return out;
    }

    @fragment fn fs(in: VSOut) -> @location(0) vec4f {
      let ndotl = max(dot(in.normal, scene.lightDir.xyz), 0.0);
      return vec4f(objectData.color.rgb * ndotl, 1.0);
    }
  `,
  color: 0xff0000,
  uniforms: new Float32Array(4), // optional, available at @group(2)
})

The preamble provides these bindings:

| Group | Binding | Type | Content | | ----- | ------- | -------------------- | ------------------------------------------------------------------------------------------------------------------- | | 0 | 0 | uniform Scene | viewProj, lightDir, ambient, lightColor, lightViewProj, shadowParams, cameraRight, cameraUp, p3Boost (.x = boost) | | 0 | 1 | texture_depth_2d | Shadow map | | 0 | 2 | sampler_comparison | Shadow sampler | | 1 | 0 | storage ObjectData | Per-object model matrix and color | | 2 | 0 | (yours) | Custom uniforms Float32Array |

Your fs output is automatically wrapped with applyP3Boost(rgb, scene.p3Boost.x) so the renderer's p3Boost setting (see Saturation / P3 boost) applies to custom shaders too. Auto-wrap only fires when your fragment entry point has the canonical signature @fragment fn fs(...) -> @location(0) vec4f { ... } — multi-target or struct-returning fragments must call applyP3Boost themselves. The helper is also exported as P3_BOOST_WGSL if you need to call it explicitly.

Lights

const ambient = new AmbientLight(0x404040, 0.5)
scene.add(ambient)

const dir = new DirectionalLight(0xffffff, 1.2)
dir.position.set(5, 10, 7)
dir.castShadow = true
dir.shadow.mapSize.set(2048, 2048)
dir.shadow.camera.near = 0.5
dir.shadow.camera.far = 200
dir.shadow.camera.left = -60 // orthographic shadow frustum bounds
scene.add(dir)

Textures

import { loadTexture } from 'nanothree'

const texture = loadTexture('/textures/grass.png', tex => {
  // texture is ready, material will auto-update on next render
})

const material = new MeshLambertMaterial({ color: 0xffffff, map: texture })

Textures are cached by URL. Call clearTextureCache() to dispose all.

GLTF/GLB Loading

Draco and Basis decoders are bundled with nanothree and loaded on demand. No setup required:

import { GLTFLoader, AnimationMixer } from 'nanothree'

const loader = new GLTFLoader()
loader.load('/models/character.glb', result => {
  scene.add(result.scene)

  // Play animation
  if (result.animations.length > 0) {
    const mixer = new AnimationMixer(result.scene)
    const action = mixer.clipAction(result.animations[0])
    action.play()
    // call mixer.update(dt) each frame
  }
})

To self-host the decoder files instead of using the bundled ones:

loader.setDracoDecoderPath('/decoders/draco/')
loader.setBasisTranscoderPath('/decoders/basis/')

Supported GLTF features: meshes, materials (base color + texture), node hierarchy, animations (translation/rotation/scale), skeletal meshes (skins, joints, weights), Draco compression (KHR_draco_mesh_compression), KTX2 textures (KHR_texture_basisu, EXT_texture_webp, EXT_texture_avif).

Not supported: morph targets, cameras, lights, sparse accessors, PBR (metalness/roughness/normal maps).

Animation

const mixer = new AnimationMixer(model)
const action = mixer.clipAction(clip)

action.play()
action.stop()
action.reset()
action.setLoop(true)
action.clampWhenFinished = true
action.fadeIn(0.3) // crossfade in over 0.3s
action.fadeOut(0.3) // crossfade out

// Find clip by name
const clip = AnimationClip.findByName(result.animations, 'Walk')

// Each frame
mixer.update(deltaTime)
mixer.stopAllAction()

Instancing

InstancedMesh - Render many copies of the same geometry with per-instance transforms and colors:

const instances = new InstancedMesh(geometry, material, 1000)
const matrix = new Float32Array(16)
instances.setMatrixAt(i, matrix)
instances.setColorAt(i, new Color(0xff0000))
scene.add(instances)

InstancedSprite - Lightweight GPU-billboarded particles:

const sprites = new InstancedSprite(500, NormalBlending)
sprites.setPositionAt(i, x, y, z)
sprites.setSizeAt(i, 0.5)
sprites.setColorAt(i, new Color(1, 1, 0))
sprites.setAlphaAt(i, 0.8)
scene.add(sprites)

Raycasting

const raycaster = new Raycaster()

// From screen coordinates (NDC: -1 to 1)
raycaster.setFromCamera([ndcX, ndcY], camera)

// Or from world-space ray
raycaster.set(origin, direction)

const hits = raycaster.intersectObject(scene, true) // recursive
// hits[0] = { distance, point: [x, y, z], object }

Frustum Culling

Automatic when you pass the camera's view-projection to updateMatrixWorld:

camera.updateViewProjection()
scene.updateMatrixWorld(camera.viewProjection)
renderer.render(scene, camera)

Objects outside the camera frustum are skipped during rendering. Bounding spheres are computed automatically from geometry.

Shadow Mapping

renderer.shadowMap.enabled = true

const light = new DirectionalLight(0xffffff, 1)
light.castShadow = true
light.shadow.mapSize.set(2048, 2048) // shadow map resolution

const mesh = new Mesh(geometry, material)
mesh.castShadow = true
mesh.receiveShadow = true

Shadows use a depth pass from the light's perspective with 4-tap PCF filtering.

Three.js API Differences

nanothree follows Three.js naming conventions but differs in several ways:

| Area | Three.js | nanothree | | ------------------- | --------------------------------------------------- | ------------------------------------------------------------- | | Backend | WebGL2 + WebGPU | WebGPU only | | Custom shaders | TSL (Three Shading Language) or GLSL | WGSL via ShaderMaterial | | Materials | MeshStandardMaterial (PBR), many others | MeshLambertMaterial (diffuse only), MeshBasicMaterial | | Lights | Ambient, Directional, Point, Spot, Hemisphere, Area | AmbientLight, DirectionalLight only | | Renderer init | Synchronous constructor | new WebGPURenderer({ canvas }) then await renderer.init() | | Textures | TextureLoader class | loadTexture(url) function | | GLTF decoders | Manual setup required | Bundled, zero config (Draco + Basis) | | Post-processing | EffectComposer, render passes | Not supported | | Morph targets | Supported | Not supported | | Point lights | Supported | Not supported | | PBR | metalness/roughness/normal/emissive maps | Not supported (Lambert only) | | Orbit controls | OrbitControls class | Not included (see example for manual implementation) | | React bindings | @react-three/fiber | Not included |

Device Compatibility

Google Pixel 10 ships with an Imagination Technologies PowerVR DXT GPU (adapter info: vendor: "img-tec", architecture: "d-series") whose copyExternalImageToTexture silently produces an all-zero texture. Without a workaround, textured meshes — including skinned GLTF characters — render as fully-transparent pixels because the fragment shaders emit texColor.a into an alpha-blended pipeline. Shadows still appear because the shadow pass doesn't sample textures.

WebGPURenderer detects this adapter during init() and falls back to uploading texture pixels via device.queue.writeTexture after rasterising the source image to a 2D canvas. No application-level changes are required.

If you maintain your own WebGPU code, the same Pixel 10 workaround is published standalone as gputex.

Bundle Size

The library builds into three chunks, loaded on demand:

| Chunk | Size | Loaded when | | ---------------- | ------- | --------------------------- | | Main library | ~198 KB | Always | | Draco decoder | ~329 KB | GLTF with Draco compression | | Basis transcoder | ~768 KB | GLTF with KTX2 textures |

License

MIT