[webgpu migration] Transforms broke when swapping WebGLRenderer for WebGPURenderer

I’m working to convert my stuff from WebGLRenderer to WebGPURenderer (exciting!). However by simply swapping WebGLRenderer with WebGPURenderer (and small tweaks like removing some properties WebGPURenderer no longer has like .premultipliedAlpha), my scene broke, and objects are no longer transformed in the same positions on screen.

Here is my test case, which is not very simplified yet, it is built with Lume HTML elements which use Three.js for rendering under the hood (I will try to make a simplified reproduction later):

https://raw.githack.com/lume/lume/b5242eb39d1f3613c1071f6db40d954ca559faf6/examples/autolayout.html

You can see the demo looks like this:

When I swap WebGLRenderer to WebGPURenderer, without doing anything else (the meshes all use non-node MeshPhysicalMaterial), the translations of the meshes break like this:

Basically my patch in Lume to test WebGPURenderer is simple, like this:

diff --git a/src/renderers/WebglRendererThree.ts b/src/renderers/WebglRendererThree.ts
index a6b17da81..d019381c3 100644
--- a/src/renderers/WebglRendererThree.ts
+++ b/src/renderers/WebglRendererThree.ts
@@ -1,18 +1,18 @@
 import {reactive, signal, Effects} from 'classy-solid'
-import {WebGLRenderer} from 'three/src/renderers/WebGLRenderer.js'
+import WebGPURenderer from 'three/src/renderers/webgpu/WebGPURenderer.js'
 import {BasicShadowMap, PCFSoftShadowMap, PCFShadowMap} from 'three/src/constants.js'
-import {PMREMGenerator} from 'three/src/extras/PMREMGenerator.js'
+import PMREMGenerator from 'three/src/renderers/common/extras/PMREMGenerator.js'
 import {TextureLoader} from 'three/src/loaders/TextureLoader.js'
 import {Motor} from '../core/Motor.js'
-import {triangleBlurTexture} from '../utils/three/texture-blur.js'
+// import {triangleBlurTexture} from '../utils/three/texture-blur.js'
 import './handle-DOM-absence.js'
-import {VRButton} from 'three/examples/jsm/webxr/VRButton.js'
+// import {VRButton} from 'three/examples/jsm/webxr/VRButton.js'
 // TODO import {ARButton}  from 'three/examples/jsm/webxr/ARButton.js'
 import type {Scene} from '../core/Scene.js'
 import type {Texture} from 'three/src/Three.js'
 
 interface SceneState {
-	renderer: WebGLRenderer
+	renderer: WebGPURenderer
 	pmremgen?: PMREMGenerator
 	hasBg?: boolean
 	bgIsEquirectangular?: boolean
@@ -22,7 +22,7 @@ interface SceneState {
 	effects: Effects
 }
 
-let instance: WebglRendererThree | null = null
+let instance: WebgpuRendererThree | null = null
 let isCreatingSingleton = false
 
 /** @typedef {'pcf' | 'pcfsoft' | 'basic'} ShadowMapTypeString */
@@ -35,13 +35,13 @@ export type ShadowMapTypeString = 'pcf' | 'pcfsoft' | 'basic'
  */
 export
 @reactive
-class WebglRendererThree {
+class WebgpuRendererThree {
 	static singleton() {
 		if (instance) return instance
 		else {
 			try {
 				isCreatingSingleton = true
-				return (instance = new WebglRendererThree())
+				return (instance = new WebgpuRendererThree())
 			} catch (e) {
 				throw e
 			} finally {
@@ -69,10 +69,10 @@ class WebglRendererThree {
 		if (sceneState) return
 
 		// TODO: options controlled by HTML attributes on scene elements.
-		const renderer = new WebGLRenderer({
+		const renderer = new WebGPURenderer({
 			// TODO: how do we change alpha:true to alpha:false after the fact?
 			alpha: true,
-			premultipliedAlpha: true,
+			// premultipliedAlpha: true, // not applicable for WebGPURenderer
 
 			antialias: true,
 		})
@@ -114,6 +114,7 @@ class WebglRendererThree {
 		scene._glLayer?.removeChild(sceneState.renderer.domElement)
 
 		sceneState.renderer.dispose()
+		// @ts-expect-error no type yet
 		sceneState.pmremgen?.dispose()
 		sceneState.effects.stopEffects()
 
@@ -208,6 +209,7 @@ class WebglRendererThree {
 			// Load the PMREM machinery only if needed.
 			if (!state.pmremgen) {
 				state.pmremgen = new PMREMGenerator(state.renderer)
+				// @ts-expect-error no type yet
 				state.pmremgen.compileCubemapShader()
 			}
 		}
@@ -227,6 +229,7 @@ class WebglRendererThree {
 		this.#bgVersion += 1
 
 		if (!state.hasBg && !state.hasEnv) {
+			// @ts-expect-error no type yet
 			state.pmremgen?.dispose()
 			state.pmremgen = undefined
 		}
@@ -255,20 +258,22 @@ class WebglRendererThree {
 			if (version !== this.#bgVersion) return
 
 			if (blurAmount > 0) {
-				// state.bgTexture = blurTexture(state.renderer, tex, 5) // Faster, but quality is not as good, has a pixelated effect. Perhaps we should provide a Scene attribute to easily pick which blur to use.
-				state.bgTexture = triangleBlurTexture(state.renderer, tex, blurAmount, 2)
-				tex.dispose()
-				tex = state.bgTexture
+				// TODO port to WebGPURenderer
+				// // state.bgTexture = blurTexture(state.renderer, tex, 5) // Faster, but quality is not as good, has a pixelated effect. Perhaps we should provide a Scene attribute to easily pick which blur to use.
+				// state.bgTexture = triangleBlurTexture(state.renderer, tex, blurAmount, 2)
+				// tex.dispose()
+				// tex = state.bgTexture
 			}
 
 			if (state.bgIsEquirectangular) {
+				// @ts-expect-error no type yet
 				state.bgTexture = state.pmremgen!.fromEquirectangular(tex).texture
 				tex.dispose() // might not be needed, but just in case.
 			} else {
 				state.bgTexture = tex
 			}
 
-			cb(state.bgTexture)
+			cb(state.bgTexture!)
 		})
 	}
 
@@ -290,7 +295,7 @@ class WebglRendererThree {
 		// Load the PMREM machinery only if needed.
 		if (!state.pmremgen) {
 			state.pmremgen = new PMREMGenerator(state.renderer)
-			state.pmremgen.compileCubemapShader()
+			// state.pmremgen.compileCubemapShader() // not needed for gpu renderer?
 		}
 
 		state.hasEnv = true
@@ -308,7 +313,7 @@ class WebglRendererThree {
 		this.#envVersion += 1
 
 		if (!state.hasBg && !state.hasEnv) {
-			state.pmremgen?.dispose()
+			// state.pmremgen?.dispose() // not needed for gpu renderer?
 			state.pmremgen = undefined
 		}
 
@@ -334,10 +339,11 @@ class WebglRendererThree {
 			// corresponds to previous state:
 			if (version !== this.#envVersion) return
 
+			// @ts-expect-error no type yet
 			state.envTexture = state.pmremgen!.fromEquirectangular(tex).texture
 			tex.dispose() // might not be needed, but just in case.
 
-			cb(state.envTexture)
+			cb(state.envTexture!)
 		})
 	}
 
@@ -346,21 +352,14 @@ class WebglRendererThree {
 		if (!state) throw new ReferenceError('Unable to request frame. Scene state should be initialized first.')
 
 		const {renderer} = state
-
-		if (renderer.setAnimationLoop)
-			// >= r94
-			renderer.setAnimationLoop(fn)
-		else if (renderer.animate)
-			// < r94
-			renderer.animate(fn as () => void)
+		renderer.setAnimationLoop(fn)
 	}
 
 	// TODO: at the moment this has only been tested toggling it on
-	// once. Should we be able to turn it off too (f.e. the vr attribute is removed)?
-	// TODO Update to WebXR (WebXRManager in Three)
+	// once. Should we be able to turn it off too (f.e. the xr attribute is removed)?
 	enableVR(scene: Scene, enable: boolean) {
 		const state = this.sceneStates.get(scene)
-		if (!state) throw new ReferenceError('Unable to enable VR. Scene state should be initialized first.')
+		if (!state) throw new ReferenceError('Unable to enable XR. Scene state should be initialized first.')
 
 		const {renderer} = state
 		renderer.xr.enabled = enable
@@ -370,10 +369,12 @@ class WebglRendererThree {
 	// TODO Update to WebXR
 	createDefaultVRButton(scene: Scene): HTMLElement {
 		const state = this.sceneStates.get(scene)
-		if (!state) throw new ReferenceError('Unable to create VR button. Scene state should be initialized first.')
+		if (!state) throw new ReferenceError('Unable to create XR button. Scene state should be initialized first.')
 
-		const {renderer} = state
-		return VRButton.createButton(renderer)
+		// TODO port to WebGPURenderer
+		// const {renderer} = state
+		// return VRButton.createButton(renderer)
+		return document.createElement('div')
 	}
 }

This WebglRendererThree.ts file is a draft design of an abstraction that will allow me to swap renderers for Lume. For example WebgpuRendererPlaycanvas, WebglRendererBabylon, etc. The elements define a standard set of 3D features, while the underlying renderer is an implementation detail.

But as you can see, I basically swapped out GL for GPU, commented out a couple things that don’t apply to transformations (scene background blur and VRButton) and somehow this broke the mesh transforms.

Any ideas?

All of the meshes in Lume have .matrixAutoUpdate set to false, and Lume calls the update methods manually. Could this have to do with it? Maybe this is exposing an edge case WebGPU mode is not handling yet?