Mesh: skip the draw at alpha 0 instead of painting it black - #1626
Merged
Conversation
`CanvasRenderer.drawMesh` has always returned early when the global alpha falls below 1/255 — the same guard eight other Canvas draw methods use. Neither GPU renderer had it. That was not merely a missing optimisation. The mesh path disables blending (`MeshBatcher.bind`), so the alpha byte never reaches the blend stage: the shader multiplies the colour by zero and the result is written OPAQUE. Hiding a mesh with `alpha = 0` painted a black silhouette of it, and the same property behaved differently depending on which backend the player got. Both GPU renderers now carry the identical guard and threshold. Found while hiding collected pickups in a 3D scene, which left black carrots lying on the snow. It also explains an earlier oddity in the same scene: setting a terrain mesh's alpha to 0 to test what was occluding it turned the ground black rather than transparent. Partial mesh transparency is still unsupported and out of scope here — the mesh path renders opaque, so `alpha = 0.5` draws fully solid. Honouring it needs blending plus back-to-front sorting among translucent meshes. Documented in the 3D skill with a symptom row so it is not mistaken for this bug. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Aa37KGXZcnVrbn1yG4j1N
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
CanvasRenderer.drawMeshhas always returned early when the global alpha falls below1/255(canvas_renderer.js:547) — the same guard eight other Canvas draw methods use. Neither GPU renderer had it.That was not a missing optimisation. The mesh path disables blending (
MeshBatcher.bind,mesh_batcher.js:332), so the alpha byte never reaches the blend stage: the shader multiplies the colour by zero and the result is written opaque. Hiding a mesh withalpha = 0painted a black silhouette of it.So the same property behaved differently depending on which backend the player got — invisible on Canvas, a black shape on WebGL and WebGPU. A sprite is unaffected because the 2D path leaves blending on, which is why this went unnoticed.
The fix
The same guard, same threshold, at the top of
drawMeshon both GPU renderers:Tests
tests/mesh-alpha.spec.js, 4 tests, written to fail first — they spy onsetBatcher, whichdrawMeshcalls as its first act, so a skipped draw never reaches it:1/255threshold the Canvas renderer already uses2/255)The last two are controls: without them the guard could over-reach and the test would still pass. Both caught real mistakes in earlier drafts of this test — a pixel-probe version passed vacuously because
Mesh.verticesis the projected output buffer and is zeroed in a bare renderer harness, and a spy version readspy.mock.calls.lengthaftermockRestore()had already cleared it.Full suite: 6,448 passing, 265 files. eslint 0 errors, biome clean.
Out of scope
Partial mesh transparency is still unsupported. The mesh path renders opaque, so
alpha = 0.5draws fully solid — fade a mesh out and it stays solid until it vanishes. Honouring it needs blending plus back-to-front sorting among translucent meshes, which is a feature rather than a fix. Documented inmelonjs-3dwith a symptom row so it is not mistaken for this bug.How it was found
Hiding collected pickups in a 3D scene left black carrots lying on the snow. It also explains an earlier oddity in the same scene: setting a terrain mesh's alpha to 0, to test what was occluding the player, turned the ground black rather than transparent — noted as strange at the time and moved past.
🤖 Generated with Claude Code
https://claude.ai/code/session_012Aa37KGXZcnVrbn1yG4j1N