fix(viewer): guard TextureNode updates against null textures#527
Conversation
three's override-material passes (shadow, prepasses) copy per-object texture slots onto shared materials whose cached per-mesh node graphs can disagree about a slot's presence; when a graph with a TextureNode pulls a null slot, the exception kills the whole render pass and the scene goes black. Patch TextureNode.prototype.update to skip null values (with a throttled warning) so the slot just renders textureless for a frame and recovers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Giving every standard material a displacement texture put displacement TextureNodes into the shared shadow/prepass override graphs; in scenes mixing preset materials with GLB item materials (no displacementMap), an item's shadow render can hit a cached graph that expects the texture and pull null — build-order dependent, which is why it broke many-but-not-all projects at open right after the release. The TextureNode fallback guard covers the original paint-hover crash (black texel = zero displacement), so the broad neutral-texture approach is retired. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 01acd19. Configure here.
| this.value = getFallbackTexture() | ||
| } | ||
| originalUpdate.call(this) | ||
| } |
There was a problem hiding this comment.
Update patch drops frame argument
Medium Severity
The TextureNode.prototype.update wrapper doesn't forward the frame argument to the original update function. While three's node runtime invokes update(frame), the original implementation always receives undefined for frame, breaking frame-dependent texture updates like video textures.
Reviewed by Cursor Bugbot for commit 01acd19. Configure here.
| } | ||
| this.value = getFallbackTexture() | ||
| } | ||
| originalUpdate.call(this) |
There was a problem hiding this comment.
Fallback assignment can stick on node
Medium Severity
When value is null, the guard writes the shared fallback onto this.value and leaves it there. Recovery assumes a reference will overwrite it later; if that pull is skipped or only runs on material changes, the node keeps sampling the 1×1 black texture after a real map is available, or keeps a fallback where the slot should stay empty.
Reviewed by Cursor Bugbot for commit 01acd19. Configure here.


Hotfix for the prod black-scene crash on project open (
TypeError: Cannot read properties of null (reading 'matrix')inTextureNode.update).three's override-material passes (shadow pass, depth/normal prepasses) copy each object's texture slots onto shared per-light/per-pass materials while caching per-mesh node graphs against those shared materials. When a cached graph contains a TextureNode for a slot the current base material doesn't provide, the per-frame reference pulls
nulland the exception aborts the whole render pass — one bad slot blacks out the scene.This guards
TextureNode.prototype.update(installed once at viewer module load): a null value skips the update with a throttled console warning instead of throwing. The affected slot renders textureless for a frame and recovers as soon as the reference pulls a real value. Root-cause investigation of the specific null slot is ongoing; this stops the bleeding for all variants of the class.🤖 Generated with Claude Code
Note
Medium Risk
Prototype-level monkey patch of three.js WebGPU node internals affects every texture update, but behavior is narrowly defensive; removing displacement-specific fallbacks shifts reliance entirely to this guard.
Overview
Fixes the production black-scene crash (
Cannot read properties of null (reading 'matrix')inTextureNode.update) when override-material passes (shadow, depth/normal prepass) leave a cached node graph pointing at a texture slot the current material no longer provides.Adds
installTextureNodeNullGuard(), invoked at viewer module load before any node materials build. It patchesTextureNode.prototype.updateso a nullvalueis swapped for a shared 1×1 blackDataTexture(with throttled warnings) before the original update runs—skipping alone is insufficient because null still breaks backend texture binding.Removes the earlier
materials.tsworkaround: neutral displacement textures on everyMeshStandardNodeMaterial,ensureDisplacementFallback, andclearedSlotValue. Texture clearing and cold-load paths again set slots tonulland triggerneedsUpdateto rebuild graphs, relying on the guard for safety across all texture slots, not only displacement.Reviewed by Cursor Bugbot for commit 01acd19. Bugbot is set up for automated code reviews on this repo. Configure here.