feat(shader): screen_texture / screen_uv / noise_uv builtins + {vertex, fragment} shader assets (+ Water Overworld example)#1548
Merged
Conversation
…se_uv Inside a ShaderEffect fragment body (design contributed by the melonJS editor team, mirroring Godot's hint_screen_texture / SCREEN_UV): - `uniform sampler2D name : screen_texture;` — annotation parsed and stripped; the sampler is auto-wired to the renderer's shared toFrameTexture capture (live GPU-resident entry) and the engine refreshes the capture right before the effect draws: in drawImage's customShader block (scene behind the sprite) and in endPostEffect (camera FBO = the scene for camera chains, parent target for renderable chains). True BackBufferCopy semantics on every path. Optional `(repeat)` wrap honored under WebGL 2 (captures are NPOT). - `screen_uv` — free varying, clip→0..1 screen position, computed in a deterministically-templated vertex variant (user source never regex- rewritten). - `noise_uv` — free varying, frame-local 0..1 across the drawn object (undoes atlas packing, scaled to object pixels); fed per draw through the new @ignore ShaderEffect._setNoiseUVRect from the quad batchers' single-texture path and both blitTextures. Back-compat: annotation was never valid GLSL; varyings activate only when referenced and never when the body declares them itself; a body using no builtins compiles byte-identical to before. New renderer helper getSharedFrameTexture() gives the shared capture slot a stable identity from effect-construction time. 8 new tests (shadereffect-builtins.spec.js): parse/wiring/introspection, true screen sampling + orientation via two-tone backdrop + swizzle readback, frame-local noise_uv on an atlas sub-frame (both axes), no-capture-without-annotation (spy), clone, self-declared back-compat. Full suite 4810 passed | 15 skipped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A
…d 1:1 Side-scroller overworld (GandalfHardcore free 32x32 pack) with the water-refraction shader from the contributed build, verbatim: two cellular NoiseTexture2d flow maps + screenTex : screen_texture + screen_uv mirror + noise_uv flow — the canonical showcase of the new shader builtins. TMX level, atlas strip-sliced animated props (campfire, portal, cooking area), drifting clouds, playable character (A/D walk, W jump, Shift run — their controller plugin replaced by ~25 lines of plain melonJS input/body code with the same tuning). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A
- water sprite uses the two regions verbatim (their settings semantics) instead of getAnimationSettings, whose bottom-align trim mis-placed the frame and bled the adjacent cloud5 atlas pixels (the white artifact on the left edge); the 2dWater art carries its own transparent sky band, matching the original placement - clouds drift right and respawn off the left edge (ported from the demo's update user-code: speed 1+random, wrap at x 1040) - handle video.init() returning false (CodeRabbit finding), alongside the existing thrown-error path Verified side-by-side against the zip's build served statically — scene, water surface line, reflection and drift now match; CodeRabbit re-review clean (0 findings). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A
- register @melonjs/debug-plugin (press "S" to toggle), like the other examples and the original demo build - arrow keys as movement aliases: ←/→ walk, ↑ jump (alongside A/D/W) - controls hint updated Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A
The camera chain (capture from the still-bound camera FBO = the scene, orientation preserved, exactly one toFrameTexture call) and the multi-effect renderable chain (capture = the parent target behind the object, not the object's own rendering) were the only untested branches of the builtins feature — the example and prior tests only exercised the sprite/customShader path. 10 tests total now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A
…GLShaders
A shader asset can now be a complete program pair — src: {vertex,
fragment} URLs (fetched in parallel) or inline via data: — compiled at
load time into a shared raw GLShader, returned by the same
loader.getShader(). This is the honest type for full programs (Mesh
custom shaders, renderer.customShader, custom batchers) and sidesteps
ShaderEffect's apply() wrapping entirely: no source rewriting, no
builtins injection — the user owns both stages. Same shared/clone/
unload lifecycle as fragment-body assets; WebGL-only (a pair loads as
null under Canvas, with a warning — no canvas analog for a raw
program; unload guards the null entry).
6 adversarial tests (shader-loader.spec.js), mutation-verified: drop
shared flag / swap vertex-fragment stages / remove the already-loaded
guard each kill exactly their matching test. Full suite 4818 | 15
skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A
The builtins stand on their own names — comparisons to other engines' equivalents removed from JSDoc, module comments, spec headers, the example, and the 19.9.0 changelog entries (generic "back-buffer copy" terminology kept where technically descriptive). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A
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.
Summary
Screen/noise shader ergonomics inside
ShaderEffectfragment bodies — design contributed by the melonJS editor team (ported from their customized v19 build), implemented on top of the #1543/#1544toFrameTextureinfrastructure. Users write three magic names and never touch JS plumbing or UV math:How it works
: screen_textureannotation: parsed + stripped at construction; the sampler is auto-wired to the renderer's shared frame capture through the existingsetTexturelive-entry path. The engine refreshes the capture right before the effect draws —drawImage's customShader block (scene behind the sprite) andendPostEffect(camera FBO for camera chains, parent target for renderable chains) — true back-buffer-copy semantics on every path, one screen copy per draw, only for effects that use it. Optional(repeat)wrap under WebGL 2.screen_uv/noise_uvvaryings: computed in a deterministically-templated vertex variant (user source is never regex-rewritten).noise_uvundoes atlas packing (fed per draw via an internal_setNoiseUVRecthook in the quad batchers), so seamless noise tiles correctly regardless of where the sprite's frame sits in its atlas.getSharedFrameTexture()gives the shared capture slot stable identity from effect-construction time.Backward compatibility
No JS API changes. The annotation was never valid GLSL (no existing shader can contain it); the varyings activate only when referenced and never when the body declares them itself; a body using none of the builtins compiles byte-identical to before. Manual
setTexture(name, renderer.toFrameTexture())keeps working.Divergence from the contributed build (intentional)
Their sprite-path implementation aliased the screen sampler to
uSampler's unit (the sprite's own texture, not the screen — their true-capture helper was orphaned after rebasing ontotoFrameTexture). This port wires every path to the real screen capture — matching the intended “everything drawn so far” semantics, which is the point of the feature.Also in this PR —
{vertex, fragment}shader-asset pairsA
"shader"asset can now be a complete program pair —src: {vertex, fragment}URLs (fetched in parallel) or inline viadata:— compiled at load time into a shared rawGLShader, returned by the sameloader.getShader(). The honest type for full programs (Meshcustom shaders,renderer.customShader, custom batchers): noapply()wrapping, no builtins injection — the user owns both stages. Same shared/clone()/unload lifecycle; WebGL-only (nullunder Canvas, unload-safe). 6 adversarial tests, mutation-verified (droppedsharedflag / swapped stages / removed already-loaded guard each kill exactly their matching test).This covers the useful half of the contributed build's "full shader" surface; the remaining half (
void mainbodies insideShaderEffect, with vertex-source rewriting) stays deferred until someone needs it.Example — Water Overworld
Their demo scene ported 1:1 (GandalfHardcore free 32×32 pack, itch.io): TMX overworld, animated props, drifting clouds, playable character (A/D/W/Shift), and the water shader verbatim — a pond that mirrors and refracts the entire scene behind it. Verified side-by-side against their build served statically.
Verification
shadereffect-builtins.spec.js): parse/wiring introspection, true screen sampling + orientation (two-tone backdrop + swizzle readback), frame-localnoise_uvon an atlas sub-frame (both axes), no-capture-without-annotation (spy), clone, self-declared-name back-compatvideo.initfalse return in the example) — fixed, re-review clean (0 findings)🤖 Generated with Claude Code
https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A