Skip to content

[v11 alpha] MeshPortalMaterial throws on construction under /webgpu: accessors run before their uniforms existΒ #2765

Description

@DennisSmolek

Summary

MeshPortalMaterial throws on construction under @react-three/drei/webgpu,
taking the whole canvas subtree down with it. It is unusable on the WebGPU entry.

TypeError: Cannot read properties of undefined (reading 'value')
    at get map (@react-three/drei/webgpu/index.mjs)
    at PortalMaterialImpl.setDefaultValues (three/build/three.webgpu.js)
    at new MeshBasicNodeMaterial (three/build/three.webgpu.js)
    at new PortalMaterialImpl (@react-three/drei/webgpu/index.mjs)
    at handleContainerEffects (@react-three/fiber/dist/webgpu/index.mjs)

followed by a second, cascading error as React unwinds the failed subtree:

TypeError: Cannot read properties of undefined (reading 'map')
    at resolve (@react-three/fiber/dist/webgpu/index.mjs)
    at detach  (@react-three/fiber/dist/webgpu/index.mjs)
    at removeChild ...

Cause: class fields initialise after super() returns

class PortalMaterialImpl extends MeshBasicNodeMaterial {
  _blur;
  _map;          // <- undefined until after super() completes
  ...
  constructor() {
    super();     // <- reads this.map before the line below has run
    this._blur = uniform(0);
    this._map = uniformTexture(new THREE.Texture());
    ...
  }

  get map() { return this._map.value; }   // boom
  set map(v) { this._map.value = v ?? new THREE.Texture(); }
}

Material.setDefaultValues probes every property of the reference material:

// three/src/materials/nodes/NodeMaterial.js
setDefaultValues( material ) {
  for ( const property in material ) {
    const value = material[ property ];
    if ( this[ property ] === undefined ) {   // <- fires the `map` getter
      this[ property ] = value;

MeshBasicNodeMaterial's constructor calls that, so get map() runs from
inside super() β€” before _map exists. Per the JS class-fields spec, subclass
field initialisers do not run until super() returns, so this is not a
three-version quirk; it is unconditional.

map is the only accessor that collides today, because it is the only one of
these six names that is also a real MeshBasicMaterial property. The rest
(blur, sdf, blend, size, resolution) are latent β€” they break the moment
three adds a matching property name.

The same shape appears in BlendMaterial (_textureA, _textureB, _blend)
and UVRenderMaterial (_tex, _inside), and in a long tail of other
uniform-backed materials in that entry, so it may be worth a sweep rather than a
point fix.

Suggested fix

Make the accessors tolerate being called before the uniforms exist:

-  get map() { return this._map.value; }
-  set map(v) { this._map.value = v ?? new THREE.Texture(); }
+  get map() { return this._map?.value; }
+  set map(v) { if (this._map) this._map.value = v ?? new THREE.Texture(); }

Dropping the write during super() is safe here: the constructor assigns
_map = uniformTexture(new THREE.Texture()) immediately afterwards, which is the
value the class wants anyway, and RenderTexture attaches the real texture later
via attach="map".

Workaround

pnpm patch @react-three/drei@11.0.0-alpha.5, guarding all six accessor pairs on
PortalMaterialImpl. Confirmed working: portals render correctly on the WebGPU
entry afterwards, no console errors.

Environment

@react-three/drei 11.0.0-alpha.5 (entry: @react-three/drei/webgpu)
@react-three/fiber 10.0.0-alpha.3 (entry: @react-three/fiber/webgpu)
three 0.185.1
React 19.2.8
Bundler Next.js 16.3.0, Turbopack

Separate from #2764, which is an import-name mismatch in the same entry β€” both
have to be patched before MeshPortalMaterial can be used at all under WebGPU.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions