Skip to content

Effects: place a pattern on the renderable drawn - #1789

Merged
obiot merged 2 commits into
masterfrom
fix/effect-uvs-1407
Oct 29, 2026
Merged

obiot merged 2 commits into
masterfrom
fix/effect-uvs-1407

Conversation

@obiot

@obiot obiot commented Oct 29, 2026

Copy link
Copy Markdown
Member

Description

Fixes #1407. The built-in effects that place a pattern placed it by uv, the coordinate they sample their texture at, which runs 0..1 across the renderable only for a sprite with a texture of its own drawn with that one effect:

  • On a sprite cut from an atlas: SwayEffect and an anchored WaveEffect moved the sprite's roots (the anchor was a row of the atlas), and VignetteEffect, RadialGradientEffect and ScanlineEffect's curvature landed on the atlas rather than the sprite.
  • With two or more effects: the chain runs over a canvas-sized capture, so every pattern, noise_uv included, sat on the canvas and slid with the camera, and ran upside down on WebGL.

How

A new internal module, effects/local_space.js, gives effect bodies where they are on the renderable:

  • ME_local: 0..1 across the renderable, following its art (mirrored with a flip, turned with a rotation).
  • ME_art: the coordinate its texture has there, for what is measured in the texture.
  • ME_localToUV, ME_artToUV, ME_localToArt: turn a displacement in one into the others, to move where a body samples.

The renderer works the space out at the end of Renderable#preDraw, from the transform that draws the renderable (Renderer#_setEffectSpace):

  • A renderable drawn with its one effect hands the space to that effect for its draws. It is computed from each vertex's position, so a renderable drawn in several quads (a nine-slice) is one space. The effect is put back afterwards.
  • A renderable with a chain hands it to the blits of the chain (_beginChainSpace). There noise_uv is the renderable's too, and is no longer the canvas's.
  • Every other path keeps the space uv always was (mode 0, the default): a camera's effects, drawLight, layers, and meshes (the mesh splice gives such a body uv, so meshes host exactly what they hosted before).

Nothing here is public API: no new option, method or documented shader input.

Backward compatibility

  • What is measured in the texture stays measured in it, so values tuned on an atlas sprite look the same: WaveEffect's amplitude and frequency, ShineEffect's sweep, bands and width, DissolveEffect's grain, the lines of HologramEffect and ScanlineEffect. With other effects after them, they now stay on the sprite.
  • SwayEffect, new in 20.11, measures everything across the sprite (strength, bend, anchor), as agreed.
  • Sprites with a texture of their own and camera effects draw as before. This is pinned in pixels against the previous bodies, as custom effects.
  • Cost: sprites sharing one of these eight effects now draw one by one, each told where it is (documented in the effects skill's batching list).

Docs

JSDoc of Sway, Wave, Shine and RadialGradient (its atlas caveat is gone), the noise_uv docs, the melonjs-effects-and-shaders skill, and the CHANGELOG (20.12.0, Fixed).

Type of change

  • Bug fix
  • New feature
  • Documentation update
  • Performance improvement
  • Refactoring (no functional changes)

Checklist

  • I have read the Contributing Guide
  • My code follows the existing code style (pnpm lint passes): eslint and biome on the changed files, tsc clean for melonjs
  • I have tested my changes locally (pnpm test passes)
  • I have added tests that cover my changes (if applicable)
  • The build succeeds (pnpm build)

Tests: new tests/effect-local-space.spec.js (55 tests). It compares pixels on WebGL, and every picture must be at least 15% lit, so two blank draws can never match.

  • One effect on an atlas sprite, plain and flipped, draws as on a texture of its own: Sway, Vignette, RadialGradient, Scanline curvature.
  • Wave's anchor: the sprite's last row stays still (it moved 37.5% of its pixels before).
  • Shine draws exactly as the previous body did, on its own texture and on an atlas.
  • Two effects (plus a pass-through) draw as one, on own textures, on atlas sprites and on rotated sprites, for all eight effects and for a custom noise_uv body.
  • Unchanged: a sprite with its own texture draws exactly as the effect always drew. A camera's curved Scanline and slanted Shine match the previous bodies, and a camera's vertical sweep still runs top to bottom over WebGL's bottom-up capture.
  • The space itself: a renderable's corners map to 0..1 under any transform; a renderable that can't be placed (infinite size, a perspective camera, not a sprite) keeps uv; a single effect is put back after its renderable draws; a camera's chain gets no space.
  • WebGPU: the block's mirror writes and the WGSL scaffold.
  • Mesh splice: a body reading the space gets uv.

Nineteen mutants of the fix (each part reverted in turn) each fail at least one test.

Also checked:

  • Snapshots: the GLSL golden and WGSL scaffold snapshots are updated (the intended new builtins).
  • Specs updated to match: webgl_review_fixes.spec.js now checks that Shine reads the direction through the space (its own uUVYDir uniform is gone), and webgpu_post_effect_flow.spec.js's bare renderer gets the two new fields.
  • Full suite: 363 files, 9083 tests pass, three runs.
  • WebGPU on a real device: tests/webgpu_effects_validate.spec.js, run headed with WebGPU enabled, compiles every built-in effect's WGSL with no errors.
  • Real browser, both renderers: a temporary page (not committed) puts each case next to its reference. Every case matches its reference on WebGL and WebGPU, with the atlas Shine kept as before on purpose.

Related issues

Closes #1407

🤖 Generated with Claude Code

https://claude.ai/code/session_01SeWCkETjW35ZbXDRswV1hx

The built-in effects that place a pattern placed it by `uv`, the
coordinate they sample their texture at. That runs 0..1 across the
renderable only for a sprite with a texture of its own drawn with that
one effect:

- on a sprite cut from an atlas, SwayEffect and an anchored WaveEffect
  rocked the sprite's roots (the anchor was a row of the atlas), and
  VignetteEffect, RadialGradientEffect and ScanlineEffect's curvature
  landed on the atlas, not the sprite;
- with two or more effects, the chain runs over a canvas-sized capture,
  so every pattern, noise_uv included, sat on the canvas and slid with
  the camera, upside down on WebGL.

Effects now read where they are on the renderable (effects/local_space.js,
internal): ME_local runs 0..1 across the renderable following its art
(flip and rotation included), ME_art is the coordinate its texture has
there, and helpers turn a displacement in either into one of `uv`. The
renderer works the space out at the end of Renderable#preDraw, from the
transform that draws the renderable: a renderable drawn with its one
effect hands it to the effect for its draws (from each vertex's
position, so a renderable drawn in several quads is one space), and one
with a chain hands it to the blits of the chain, where noise_uv is the
renderable's too. Every other path keeps the space `uv` always was:
a camera's effects, drawLight, meshes (spliced with a fallback), layers.

What is measured in the texture stays measured in it, so values tuned
on an atlas sprite look the same: WaveEffect's amplitude and frequency,
ShineEffect's sweep, bands and width, DissolveEffect's grain, the lines
of HologramEffect and ScanlineEffect. SwayEffect, new in 20.11, measures
everything across the sprite.

Camera effects draw exactly as before (pinned against the previous
Scanline and Shine bodies). Sprites sharing one of these effects now
draw one by one, each told where it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SeWCkETjW35ZbXDRswV1hx
Copilot AI balanced review requested due to automatic review settings October 29, 2026 00:49

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 insufficient remaining quota.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SeWCkETjW35ZbXDRswV1hx
Copilot AI balanced review requested due to automatic review settings October 29, 2026 00:51

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 insufficient remaining quota.

@obiot
obiot merged commit 05ed64b into master Oct 29, 2026
6 checks passed
@obiot
obiot deleted the fix/effect-uvs-1407 branch October 29, 2026 01:39
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.

Per-sprite multi-pass effects use canvas-relative UVs instead of sprite-local UVs

2 participants