Skip to content

Mesh: skip the draw at alpha 0 instead of painting it black - #1626

Merged
obiot merged 1 commit into
masterfrom
fix/mesh-alpha-zero
Aug 31, 2026
Merged

Mesh: skip the draw at alpha 0 instead of painting it black#1626
obiot merged 1 commit into
masterfrom
fix/mesh-alpha-zero

Conversation

@obiot

@obiot obiot commented Aug 31, 2026

Copy link
Copy Markdown
Member

The bug

CanvasRenderer.drawMesh has always returned early when the global alpha falls below 1/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 with alpha = 0 painted 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 drawMesh on both GPU renderers:

if (this.getGlobalAlpha() < 1 / 255) {
    return;
}

Tests

tests/mesh-alpha.spec.js, 4 tests, written to fail first — they spy on setBatcher, which drawMesh calls as its first act, so a skipped draw never reaches it:

  • does not draw at alpha 0
  • skips below the 1/255 threshold the Canvas renderer already uses
  • still draws at one full step of alpha (2/255)
  • still draws at full alpha

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.vertices is the projected output buffer and is zeroed in a bare renderer harness, and a spy version read spy.mock.calls.length after mockRestore() 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.5 draws 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 in melonjs-3d with 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

`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
Copilot AI lite review requested due to automatic review settings August 31, 2026 05:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@obiot
obiot merged commit d37285e into master Aug 31, 2026
6 checks passed
@obiot
obiot deleted the fix/mesh-alpha-zero branch August 31, 2026 06:09
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.

2 participants