An experimental particle-based voxel renderer designed to explore optimization strategies for rendering large (1–8 million) particle buffers in Three.js. The test model is a 6cm test sphere generated by a volumetric GLSL shader and stored as a sequence of PNGs or a single "sprite sheet." This voxel shader was used to create larger lamp shades with various Stratasys PolyJet 3D printers. These printers achieve variable-transparency, full-color prints by dithering voxels of CMYKW (cyan, magenta, yellow, black, white) and transparent photopolymer resins, using a sacrificial support material.
View it live here: https://caseprince.github.io/voxel-visualizer
Left: Screenshot of Voxel Visualizer. Right: Voxel data 3D printed on Stratasys J750.
- Reading individual pixels from a Canvas context via
var pixel = ctx.getImageData(x, y, 1, 1).datais quite slow, even when using a sprite sheet. SettingwillReadFrequentlyappears to have little effect. - Reads are faster if
ImageDatais first converted into aUint32Array(e.g.,new Uint32Array(myImageData.data.buffer)) — see Faster canvas pixel manipulation with typed arrays. Array.push()does not seem slower thanFloat32Array.set()for writing data.- Bypassing the Canvas entirely by loading a PNG as a raw file using
XMLHttpRequest, then piping the ArrayBuffer into aUint8Arrayand decoding via UPNG seems to be the fastest way to read pixel data; it also supports indexed-color PNG-8s. 8-bit PNGs are more than sufficient for PolyJet voxel printing, since PolyJet printers support a maximum of 7–8 colors/resin types per print. - Not all PNGs are created equal. UPNG and ImageMagick both encode PNGs with 4-bit
depthproperties (as decoded by UPNG), which corresponds to the bit depth of color indices and makes sense given a small color palette. Photoshop, on the other hand, encodesPNG-8with 8-bit indices, which makes access viaUint8Arraypainless and avoids bitwise logic. (JavaScript has no built-inUint4Arraytyped array.) Oddly, despite this, Photoshop's PNGs are slightly smaller. This makes me think I don't fully understand the PNG bit depth behavior, or there may be a bug in UPNG. The two formats appear incompatible for reading color indices. This demo uses Photoshop-encoded PNGs for simplicity. - UPNG supports encoding animated PNGs (APNGs), which theoretically should be the most efficient format since frames only encode pixels from regions that are changing. However, initial experimentation encoding APNGs yielded files that are ~15% larger than equivalent 2D sprite sheets. This may warrant further investigation.
- Install Node.js version 22.11.0 (or a compatible LTS release).
- Install dependencies
npm install- Start dev server
npm run dev- Visit http://127.0.0.1:8080
- 1 Million Points in Three.js: https://observablehq.com/@joewdavies/1-million-points-in-three-js
- Three.js TypeScript Boilerplate: https://github.com/Sean-Bradley/Three.js-TypeScript-Boilerplate.git
- UPNG.js: https://github.com/photopea/UPNG.js
- dat.GUI: https://github.com/dataarts/dat.gui
- FileSaver.js: https://github.com/eligrey/FileSaver.js
- Simulate color blending
- Output sprites directly as animated PNG
- Add ability to parse indexed-color PNGs with 4bit indices
- 3D bounding box calculation