Skip to content

fix(viewer): guard TextureNode updates against null textures#527

Merged
wass08 merged 3 commits into
mainfrom
fix/texture-node-null-guard
Jul 21, 2026
Merged

fix(viewer): guard TextureNode updates against null textures#527
wass08 merged 3 commits into
mainfrom
fix/texture-node-null-guard

Conversation

@wass08

@wass08 wass08 commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Hotfix for the prod black-scene crash on project open (TypeError: Cannot read properties of null (reading 'matrix') in TextureNode.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 null and 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') in TextureNode.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 patches TextureNode.prototype.update so a null value is swapped for a shared 1×1 black DataTexture (with throttled warnings) before the original update runs—skipping alone is insufficient because null still breaks backend texture binding.

Removes the earlier materials.ts workaround: neutral displacement textures on every MeshStandardNodeMaterial, ensureDisplacementFallback, and clearedSlotValue. Texture clearing and cold-load paths again set slots to null and trigger needsUpdate to 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.

wass08 and others added 3 commits July 21, 2026 11:19
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>
@wass08
wass08 merged commit adc1ec8 into main Jul 21, 2026
2 checks passed

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ 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)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 01acd19. Configure here.

}
this.value = getFallbackTexture()
}
originalUpdate.call(this)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 01acd19. Configure here.

@wass08
wass08 deleted the fix/texture-node-null-guard branch July 21, 2026 16:12
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