Mesh: a transparent pass, so a faded mesh actually fades - #1635
Open
obiot wants to merge 1 commit into
Open
Conversation
`setOpacity(0.5)` did not make a mesh see-through. The mesh tier renders
opaque — `MeshBatcher.bind()` disables GL_BLEND — and every mesh vertex
shader premultiplies, so a faded mesh wrote `(rgb x a, a)` straight into
the target: darkened toward black, with the background contributing
nothing. Measured before the change: a white mesh at half opacity over a
blue background read [127,127,127], not [127,127,255].
That is the same defect as the far end of the range, which painted an
opaque black silhouette until it was fixed, and the Canvas backend never
had it — meshes there already fade under the 2D context. Nobody was
relying on the darkening; the uncommitted sledding example sets
`rabbit.alpha = 0.35` for an invulnerability flash and has been getting a
dark rabbit rather than a translucent one.
Draws resolving to fractional alpha are now queued and replayed after the
opaque pass, back-to-front, blending, writing no depth but still
depth-tested. `transparent: true` opts in a soft-alpha TEXTURE the
automatic check cannot see into; `transparent: false` pins the old
behaviour. Blending honours the renderable's existing `blendMode`.
Ground shadows become the queue's first client rather than the feature
itself. `queueGroundShadow` and `flushGroundShadows` stay as a delegate
and an alias, so the 52 tests in ground_shadow.spec.js pass with nothing
but internals renames — the regression gate for the whole refactor.
Three defects found on the way, none of them the feature:
- `beginBlendedDraw` passed `renderer.premultipliedAlpha` to the blend
function. That flag describes source TEXTURES; the mesh shaders
premultiply unconditionally, so a non-premultiplied context selected
SRC_ALPHA and multiplied by alpha twice. Invisible while decals were
the only client, because their source colour is black.
- `Renderer.reset()` zeroed the queue count without releasing entry
references, keeping every queued renderable reachable until its
pooled slot was reused.
- `Sprite3d`'s `alphaCutoff` default of 0.5 would have discarded every
soft texel before blending saw it, defeating `transparent: true`
outright. It now drops to 1/255 when transparency is explicit.
Also removes two fossils that would mislead the next reader here: a
comment in `setBatcher` describing a drain #1630 deleted, and an orphaned
JSDoc block documenting the method that went with it.
Additive: an opaque scene pays two property reads and one compare against
a packed tint both backends already compute, plus three early-returning
flushes per frame. No shader change, and WebGPU pipeline keys are
unchanged.
Closes #1516
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Aa37KGXZcnVrbn1yG4j1N
Contributor
There was a problem hiding this comment.
Pull request overview
This PR implements a dedicated transparent pass for the 3D mesh tier so fractional-alpha meshes (e.g. setOpacity(0.5)) actually blend instead of darkening toward black, aligning GPU behavior with the Canvas backend and fixing the long-standing symptom described in #1516.
Changes:
- Introduces a transparent queue in
Renderer(queueTransparent/flushTransparent) with back-to-front replay, blending enabled, depth writes disabled, depth test preserved. - Routes eligible retained mesh draws into the transparent queue on both WebGL and WebGPU backends; ground shadows become a client of the same queue (legacy aliases preserved).
- Adds/updates tests to cover the transparent pass behavior and adjusts docs/changelog (including
Mesh.transparentandSprite3dalphaCutoff behavior).
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/melonjs/src/video/renderer.js | Adds transparent queue + flush logic; keeps ground-shadow APIs as aliases and adds removal/reset safety. |
| packages/melonjs/src/video/webgl/webgl_renderer.js | Routes transparent retained draws into the queue; adds per-entry blend application helper. |
| packages/melonjs/src/video/webgl/batchers/mesh_batcher.js | Uses per-entry replay blend mode (_replayBlend) to enter/exit blended state during replay. |
| packages/melonjs/src/video/webgpu/webgpu_renderer.js | Routes transparent retained draws into the queue; removes queued entries on geometry deletion. |
| packages/melonjs/src/video/webgpu/batchers/mesh_batcher.js | Uses _replayBlend to select pipeline blend state + disable depth writes during replay. |
| packages/melonjs/src/renderable/mesh.js | Documents and implements settings.transparent / mesh.transparent tri-state behavior. |
| packages/melonjs/src/renderable/sprite3d.js | Lowers default alphaCutoff when transparent: true is explicitly requested; forwards transparent. |
| packages/melonjs/src/renderable/container.js | Flushes the transparent pass before entering screen-space (floating) bracket. |
| packages/melonjs/src/camera/camera2d.ts | Flushes the transparent pass after world draw, before FX. |
| packages/melonjs/src/application/application.ts | Flushes the transparent pass at end-of-frame for all-mesh scenes. |
| packages/melonjs/tests/transparent_queue.spec.js | New test suite asserting blending, ordering, drain contract, pool reuse, and state restoration. |
| packages/melonjs/tests/ground_shadow.spec.js | Updates ground-shadow tests to use the transparent queue counters/pool names. |
| packages/melonjs/tests/webgl_mesh_fog.spec.js | Pins fog test mesh to opaque pass to avoid transparent compositing changing the pixel probe. |
| packages/melonjs/tests/webgpu_mesh_retained.spec.js | Updates stub to include removeQueuedTransparent due to deletion now purging the queue. |
| packages/melonjs/skills/melonjs-3d/SKILL.md | Documents the new transparency behavior and options. |
| packages/melonjs/CHANGELOG.md | Adds release note for 3D soft transparency and related behavior changes. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+2110
to
+2127
| applyBlendFunction(mode) { | ||
| const gl = this.gl; | ||
| // ALWAYS premultiplied, whatever `this.premultipliedAlpha` says — that | ||
| // flag describes source TEXTURES, while every mesh vertex shader | ||
| // premultiplies its own output unconditionally | ||
| // (`vColor = vec4(tinted.rgb * tinted.a, tinted.a)`). Passing the flag | ||
| // here selects SRC_ALPHA and multiplies by alpha a second time: a | ||
| // half-faded white mesh over blue comes out at three-quarter blue | ||
| // instead of full. It went unnoticed while decals were the only client, | ||
| // because their source colour is black and 0 × anything is 0. | ||
| const state = blendStateFor(normalizeBlendMode(mode), true); | ||
| gl.enable(gl.BLEND); | ||
| gl.blendEquation(GL_BLEND_OP[state.operation]); | ||
| gl.blendFunc( | ||
| GL_BLEND_FACTOR[state.srcFactor], | ||
| GL_BLEND_FACTOR[state.dstFactor], | ||
| ); | ||
| } |
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.
Closes #1516.
The symptom, measured
setOpacity(0.5)did not make a mesh see-through. The mesh tier renders opaque —MeshBatcher.bind()disablesGL_BLEND— and every mesh vertex shader premultiplies, so a faded mesh wrote(rgb × a, a)straight into the target.A white mesh at half opacity over a blue background:
[127, 127, 127]— darkened toward black, background contributes nothing[127, 127, 255]— blendedThat is the same defect as the far end of the range, which painted an opaque black silhouette until #1626 fixed it. The Canvas backend never had it: meshes there already fade under the 2D context, so the same scene faded on Canvas and darkened on GPU.
Nobody was relying on the darkening. The sledding example sets
rabbit.alpha = 0.35for an invulnerability flash and has been getting a dark rabbit rather than a translucent one — visibly fixed by this branch.The change
Draws resolving to fractional alpha are queued and replayed after the opaque pass, back-to-front, blending, writing no depth but still depth-tested — so transparent objects composite with each other and stay correctly hidden behind opaque geometry.
Blending honours the renderable's existing
blendMode— no new property.Ground shadows become the queue's first client rather than the feature itself.
queueGroundShadow/flushGroundShadowsremain as a delegate and an alias, soground_shadow.spec.jspasses 52/52 with nothing but internals renames — the regression gate for the whole refactor.Three defects found on the way, none of them the feature
beginBlendedDrawdouble-multiplied alpha. It passedrenderer.premultipliedAlphato the blend function, but that flag describes source textures — the mesh shaders premultiply unconditionally. A non-premultiplied context selectedSRC_ALPHAand multiplied a second time. Invisible while decals were the only client, because their source colour is black and0 × anything = 0.Renderer.reset()leaked. It zeroed the queue count without releasing entry references, keeping every queued renderable reachable until its pooled slot was reused. Harmless with two shared quads; not with arbitrary user meshes.Sprite3d'salphaCutoff: 0.5would have defeatedtransparent: trueoutright — discarding every soft texel before blending saw it, which is exactly the case the flag exists for. It now drops to1/255when transparency is explicit.Also removes two fossils that would mislead the next reader here: a comment in
setBatcherdescribing a drain #1630 deleted, and an orphaned JSDoc block documenting the method that went with it.Additive
An opaque scene pays two property reads and one compare against a packed tint both backends already compute per draw, plus three early-returning flushes per frame. No shader change. WebGPU pipeline keys are unchanged — the blend token and
|dw0axis from #1515 already covered this, sopipeline/cache.jsneeded no edit at all.Tests
tests/transparent_queue.spec.js— 19 assertions: the measured symptom as an absolute pixel, thetrue/falseescape hatches, order-independence of submission, near-on-top, not-painted-over-by-a-later-opaque-mesh, still-occluded-by-nearer-geometry, both #1630 drain traps, double-queueing with distinct matrices, destroy-before-drain, pool reuse and reference release, no re-queue during replay, mesh-mode state restored, and no blend-mode leak into the 2D cache.Mutation-checked, all five bite: no sort (2 fail), reversed sort (1), screen-space guard removed (2), references not released (1), and the double-premultiply above (2).
271 files / 6550 tests, lint and types clean. Verified on screen on WebGL and on WebGPU/Metal, with the unfogged/untransparent examples unchanged.Out of scope, deliberately
Intersecting-transparency correctness (per-object sorting cannot order interpenetrating meshes — documented), the 2D-camera accumulated path, per-instance sorting inside an
InstancedMesh, advanced destination-capture blend modes, and soft particles.🤖 Generated with Claude Code
https://claude.ai/code/session_012Aa37KGXZcnVrbn1yG4j1N