Skip to content

WebGPU - #75

Merged
alan-wu merged 65 commits into
ABI-Software:developfrom
alan-wu:webgpu
Oct 7, 2026
Merged

alan-wu merged 65 commits into
ABI-Software:developfrom
alan-wu:webgpu

Conversation

@alan-wu

@alan-wu alan-wu commented Oct 4, 2026

Copy link
Copy Markdown
Member

WebGPU will now be the default renderer for ZincJS.
This requires many of the modules to be refactored and improved.
Renderer can now render on demand which will greatly reduce CPU usage on idle.
Please see the following full list for the changes:

three.js and the renderer

  • All three imports are now three/webgpu. The renderer is THREE.WebGPURenderer, which falls back to WebGL 2.
  • Materials are now NodeMaterials, so material.uniforms and onBeforeCompile no longer apply.
  • Renderer.initialiseVisualisation() is now async and must be awaited.
  • Renderer.isWebGL2() and Scene.isWebGL2() now return true only on the WebGL 2 fallback backend.
  • Renderer.forceContextLoss() and Renderer.forceContextRestore() now do nothing.

Legacy geometry removed

  • The legacy Geometry, Face3 and DirectGeometry are deleted (src/three/Geometry.js).
  • JSONLoader.parse() returns a BufferGeometry. It no longer provides faces, vertices, morphTargets, morphColors, skin, bones or animations.
  • toBufferGeometry(geometry) only clones now, and its options argument is ignored.
  • copyMorphColorsToBufferGeometry is removed.
  • Geometry.calculateUVs() and Lines2.render() are removed.

Pointsets

  • A pointset is now an InstancedPoints (an InstancedMesh of camera-facing quads), not a THREE.Points.
  • Positions live in instancePosition and mesh.pointPositions, not in geometry.attributes.position.
  • Morphing is blended on the CPU instead of going through the mixer.
  • addPoints is capped at 500 points (DEFAULT_CAPACITY). Extra points are silently dropped.
  • Scene.addTemporaryPoints() returns an InstancedPoints.

Lines, materials and textures

  • The custom GLSL LineMaterial is deleted. Lines use Line2NodeMaterial instead.
  • material.resolution no longer exists. The resolution is now stored as _resolution on the mesh.
  • Morph colour no longer uses material.vertexColors, which is forced to false. The blend is driven by material.userData.uniforms.morphColorMix.
  • TextureSlides uses TSL materials, and its uniforms are now in material.userData.uniforms.
  • TextureArray.impl is now a Data3DTexture (it was a DataArrayTexture).

Rendering and scene API

  • All primitives now share the signature render(delta, playAnimation, cameraControls, options). TextureSlides and Glyphset gained the cameraControls argument.
  • SceneExporter.exportGLTF() is async, rejects on errors, and exports with onlyVisible: true.
  • Scene.renderGeometries() returns a boolean that says whether anything changed.
  • THREE.Clock is replaced by THREE.Timer.
  • The overlay (HUD) scene is now drawn after the main scene and after the pre-render callbacks.
  • Post-render callbacks only run on frames that are actually drawn.
  • Region.clear(transverse) resets children only when transverse is true.
  • VideoHandler no longer sets RGBFormat.

New features

WebGPU setup

  • When initialiseVisualisation runs, it asks the adapter for its maximum maxTextureArrayLayers and maxStorageBuffersPerShaderStage. Texture arrays can therefore use as many layers as the hardware supports.
  • New Renderer.isWebGPU() and Scene.isWebGPU().
  • New Scene.getRenderer(), which returns the WebGPU renderer for custom TSL compute.

Render on demand (9af3d80)

  • New Renderer.setRenderOnDemand(flag), isRenderOnDemand() and invalidate(), plus Scene.invalidate() and ZincObject.requestRender().
  • Frames are skipped when nothing has changed.
  • Almost every method that changes a scene, region, primitive or the controls requests a frame. So do loads, playback, video, display flags and the minimap.
  • New src/renderRequests.js lets shared resources that belong to no scene, such as async textures, ask every renderer to redraw.

Glyphset transforms and colours on the GPU (9e52df1)

  • New src/tsl/glyphTransform.js: a TSL compute port of resolve_glyph_axes plus keyframe interpolation. It supports the NONE, MIRROR, AXES_2D and AXES_3D repeat modes.
  • New src/tsl/glyphRenderMaterial.js builds each instance's transform and colour from the compute output.
  • During playback, nothing is read back from the GPU. The bounding box is a conservative union over all keyframes, and raycasting is disabled while animating.
  • When playback stops, one accurate readback syncs the instance attributes and labels.
  • Readbacks triggered by setMorphTime are debounced to 500 ms.
  • New Glyphset.prepareColorForExport() and clearColorExportState() for export.
  • New Glyph.getColour(). Glyph.showLabel() now prefers the glyph's own colour.

Text sprites (new src/textSprite.js)

  • Sprites use SpriteNodeMaterial, and their canvas resolution is chosen from the text's size on screen.
  • Text scales with screen height. Sprites are redrawn when the viewport scale changes by more than 10% and when web fonts load.
  • Label alphaTest changed from 0.5 to 0.05.

Morph colour, texture slides and Lines2

  • Morph colour is implemented in TSL (src/tsl/morphColorMaterial.js). It keeps two long-lived morphColor0/1 attributes and copies data into them only when the keyframe pair changes.
  • Texture slides are TSL (src/tsl/textureSlides.js). Time-dependent textures work by swapping image data.
  • Lines2 buffers grow as needed and are edited in place. Before, they were fixed at 300 entries.

glTF

  • Import: relative morph targets are converted to absolute ones, and colour-morph meshes get the morph-colour material.
  • Export: morph colours are written as COLOR_0 targets through a new ZINC_morph_colour plugin. Animation tracks are prefixed with the mesh uuid, and hidden meshes are skipped.

Loading and regions

  • New SceneLoader.cancelPendingLoads(). Results that arrive after cancellation are disposed, and Scene.clearAll() calls it.
  • The OBJ loader adds every mesh in the loaded group as its own object.
  • A multi-source load reports its error once, and the parts that did download are freed.
  • New Region.getScene(). getAllObjects() takes an optional accumulator argument.

Bug fixes

Controls and picking

  • Rotate transitions never ran because they used the non-existent MODE.ROTATE_CAMERA_TRANSITION.
  • Finished transitions were never cleared because the code used == where it meant =.
  • The hover throttle used the wrong delay.
  • Touch-tap picking read the wrong touch list.
  • THREE.Math replaced with THREE.MathUtils.
  • Hidden regions could still be picked.
  • CameraControls.disable() now resets the control state.

Renderer and regions

  • A typo overwrote the post-render callback map on reset.
  • The renderer is now kept at least 1×1.
  • Region.setMorphTime recursed only one level.
  • Region.clear(true) left child groups attached.

Loaders

  • mergeGlyphData appended the wrong set of labels.
  • IndexedSourcesHandler passed the wrong argument to its error handler, and passed factory functions where it should have passed the handlers they return.
  • MultiSourcesHandler disposed the wrong entry.
  • PrimitivesLoader.parse used an undefined loader.
  • The metadata loop used the wrong array length.
  • loadSTL and loadOBJ crashed on options.lod and had no error or progress handlers.
  • toBeDwonloaded was misspelled, so pending downloads were never reset.

Primitives

  • resolve_glyph_axes referenced an undefined glyph_repeat_mode.
  • Glyph.setColour used an undefined colour.
  • Label.dispose did nothing. Labels are now also disposed together with their glyphs.
  • Glyphsets and LOD levels that finished loading after dispose() were leaked.
  • TextureSlides.dispose didn't dispose the mask texture.
  • TextureVolume was missing an idTextureMap declaration.
  • removeVertexAtIndex(maintainLength) threw away its result.
  • TubeLines failed with BufferGeometry input.
  • Morph colour turned parts of the blend black on surfaces and lines.
  • Pointset morphing started halfway through the duration, and static per-vertex colours were not shown.
  • Video texture and marker label fixes.
  • The getCircularTexture texture was flagged for update before its image had loaded.

WebGPU

  • WebGPU copied 16-bit index arrays to 32-bit before the first render (f0377c9). New keepIndexType and useStandardIndexType helpers handle this.

Performance

  • Fewer allocations per frame: region tree walks use accumulators, and renderOptions, minimap scratch vectors and camera path interpolation are reused. Callback loops use for...in.
  • The minimap skips its camera update when nothing has changed.
  • Non-host scenes are picked after updateMatrixWorld() instead of a full render.
  • Marker hover raycasts are throttled to one per animation frame.
  • Bounding boxes are flagged for recalculation only when morph targets actually move positions (hasMorphPositions, cached).
  • getBoundingBox no longer updates children's world matrices.
  • All pointsets share one circular texture.
  • 16-bit indices are kept as 16-bit, which halves index memory.
  • The LOD secondary material is created only for transparent objects.
  • TextureSlides no longer sets material.needsUpdate on every uniform change.

Build, tooling and docs

  • Tests run on headless WebGPU with a fake canvas context, replacing headless-gl.
  • README rewritten: quick start, render on demand, a controls table and build instructions.

Tests added

Loaders and scenes

  • jsonLoader.test.js: the JSON loader's BufferGeometry output.
  • loaderRegressions.test.js: morph colour, video, pointset and tubelines regressions.
  • sceneClearRegressions.test.js: clearing scenes, cancelling loads, and loads that finish after dispose.

Rendering

  • renderOnDemand.test.js: what triggers a frame, and how pre- and post-render callbacks behave.
  • performanceRegressions.test.js: the performance changes listed above.
  • webgpuShaderBuild.test.js: shaders build and render on real WebGPU.

Glyphsets

  • glyphTransformCompute.{NONE,MIRROR,AXES_2D,AXES_3D}.test.js: the GPU compute checked against a CPU reference (glyphTransformReference.js).
  • glyphsetGpuPipeline.test.js: the full GPU path from loading a glyphset to its instance attributes.
  • glyphsetAnimationPipeline.test.js: playback skips instance writes and raycasting, then resyncs once on pause.
  • glyphsetTimeSliderDebounce.test.js: rapid setMorphTime calls collapse into one readback.
  • glyphsetLabelSync.test.js and glyphShowLabelColour.test.js: label position and colour.

Fixtures

  • test/models/timeGlyphs_{geometry,metadata}.json

@alan-wu
alan-wu merged commit a03e6f5 into ABI-Software:develop Oct 7, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant