Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/melonjs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
- Audio: a clip declared with `preload: false` is fetched when it is first played (`audio.play`, `audio.playTrack` or `autoplay`) and streamed, as with `stream: true`, so it starts as soon as enough of it has arrived ([#1784](https://github.com/melonjs/melonJS/issues/1784)). It was never fetched at all, so it never played; a clip that is never played is still never downloaded
- State: the first frame of a new stage shows what it built, and the first frame after a level loaded while the game runs shows that level. That frame was drawn before the world had been stepped, and a renderable is drawn only once a step has found it on screen, so every state change showed one frame of background alone unless a transition covered it
- State: a `state.change()` asked for while a transition covers the screen is made by that transition, to the last state asked for and with its arguments, as without a transition ([#1787](https://github.com/melonjs/melonJS/issues/1787)). It put up a second cover, and the first stayed on the camera for good, the screen black over a game still running; a change to a state with no transition of its own is still made at once, and takes the cover down
- Effects: the built-in effects that place a pattern place it on the sprite itself, cut from an atlas or with other effects after it, and `SwayEffect` measures its strength across the sprite ([#1407](https://github.com/melonjs/melonJS/issues/1407)). On an atlas sprite, `SwayEffect` and an anchored `WaveEffect` moved its roots and `VignetteEffect`, `RadialGradientEffect` and `ScanlineEffect`'s curve landed on the atlas; with two or more effects, every pattern, `noise_uv` and `ShineEffect`'s sweep included, sat on the canvas and slid with the camera

## [20.11.0] (melonJS 2) - _2026-10-27_

Expand Down
42 changes: 31 additions & 11 deletions packages/melonjs/skills/melonjs-effects-and-shaders/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,19 @@ as transparent, as if the sprite were drawn on its own, so they never show
along its edges and the atlas needs no padding for them. Past the texture's own
edge (a standalone image, or the outside of an atlas) nothing changes.

**The effects that place a pattern place it on the renderable they draw.**
`SwayEffect`'s planted row, `WaveEffect`'s `anchor`, the centre of
`VignetteEffect`, `RadialGradientEffect` and `ScanlineEffect`'s curvature all
land on the sprite itself: cut from an atlas, flipped, rotated, or with other
effects after it, the pattern sits where it sits on a sprite with a texture of
its own and that one effect. What is measured in the texture stays measured in
it: `ShineEffect`'s sweep, bands and width, `WaveEffect`'s amplitude and
frequency, `DissolveEffect`'s grain, the lines of `HologramEffect` and
`ScanlineEffect` (on an atlas sprite, fractions of the atlas, so a single shine
crosses an atlas sprite only part of the time). With other effects after them
they all stay on the sprite. `SwayEffect` measures everything across the
sprite.

**Their widths, offsets and sizes are in pixels of the texture they draw, with
nothing to pass.** Each of those effects takes the size of what it draws: the
sprite's whole texture (the atlas image, not the frame), the screen for a
Expand Down Expand Up @@ -166,17 +179,18 @@ tree.addPostEffect(sway);
sway.setTime(timer.getTime() / 1000);
```

Rows are measured across the texture the effect samples, so on a sprite cut from
an atlas the anchor is a row of the atlas: it suits a standalone image or a
layer best.
Its strength (a fraction of the sprite's width), its `anchor` and how it bends
are all measured across the sprite, so a sprite cut from an atlas sways exactly
like the same art with a texture of its own. `WaveEffect`'s `anchor` is a row of
the sprite too, but its amplitude and frequency are measured across the texture,
so on an atlas sprite they are fractions of the atlas.

`RadialGradientEffect` is a procedural soft spot: solid at the centre of its
host quad, fading linearly to transparent at the edge, and elliptical for free
on a non-square quad. It is the one to reach for instead of baking a disc into
a canvas, for a pickup highlight, a trigger marker or a damage flash. Its
falloff assumes the quad samples a FULL texture rect, so pair it with a
standalone image or `Renderer.getWhitePixel()` rather than a sprite that draws
a sub-region of an atlas, where the centre would land off-quad.
`RadialGradientEffect` is a procedural soft spot: solid at the centre of the
renderable it draws, fading linearly to transparent at the edge, and elliptical
for free on a non-square one. It is the one to reach for instead of baking a
disc into a canvas, for a pickup highlight, a trigger marker or a damage flash,
on a standalone image, `Renderer.getWhitePixel()` or a sprite cut from an atlas
alike.

`SquishEffect` bulges a sprite's middle out (`setBulge(0.3)`) or pinches it in
(`setBulge(-0.3)`), its top and bottom rows staying put. Bulging out widens the
Expand Down Expand Up @@ -524,6 +538,10 @@ has changes nothing). What splits the batch:
- a different effect instance, or a sprite without the effect in between;
- a `noise_uv` effect on sprites of different frames or sizes (its rect is
per sprite);
- one of the effects that place a pattern on the sprite (`SwayEffect`,
`WaveEffect`, `ShineEffect`, `VignetteEffect`, `RadialGradientEffect`,
`ScanlineEffect`, `DissolveEffect`, `HologramEffect`): each sprite is told
where it is, so each draws on its own;
- a `screen_texture` effect, which draws each sprite on its own, as each one
samples a capture of everything drawn before it.

Expand Down Expand Up @@ -603,7 +621,9 @@ For refraction, distortion and anything that samples what is already drawn:
- **`screen_texture`** — a sampler kept filled with everything drawn so far
(GLSL: annotate the sampler `uniform sampler2D tex : screen_texture;`)
- **`screen_uv`** — this fragment's 0..1 position in that capture
- **`noise_uv`** — 0..1 across the sprite regardless of its atlas frame
- **`noise_uv`** — 0..1 across the sprite regardless of its atlas frame, and
across the renderable when it has several effects (it does not slide with
the camera)

In WGSL the capture is sampled through `screen_sampler` (clamped) or
`screen_sampler_repeat` (wrapping) instead of GLSL's `: screen_texture`
Expand Down
7 changes: 7 additions & 0 deletions packages/melonjs/src/renderable/renderable.js
Original file line number Diff line number Diff line change
Expand Up @@ -1390,6 +1390,13 @@ export default class Renderable extends Rect {
renderer.translate(-ax, -ay);
}

// its own transform applied: its effects learn where it is, for the
// ones that place a pattern on it (a sway's planted row, a shine's
// sweep). Optional, for a renderer that is not a `Renderer`
if (!this._postEffectManaged && this.postEffects.length > 0) {
renderer._setEffectSpace?.(this);
}

// Apply the current tint and opacity.
//
// Multiplied by the alpha already on the renderer, so opacity
Expand Down
6 changes: 4 additions & 2 deletions packages/melonjs/src/video/effects/dissolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ fn apply(color : vec4f, uv : vec2f) -> vec4f {
if (fx.uDissolveProgress <= 0.0) {
return color;
}
let n = dissolveFBM(uv * 8.0);
// grain of its texture, fixed on the sprite
let n = dissolveFBM(ME_art * 8.0);
if (n < fx.uDissolveProgress) {
discard;
}
Expand Down Expand Up @@ -123,7 +124,8 @@ export default class DissolveEffect extends ShaderEffect {
if (uDissolveProgress <= 0.0) {
return color;
}
float n = fbm(uv * 8.0);
// grain of its texture, fixed on the sprite
float n = fbm(ME_art * 8.0);
if (n < uDissolveProgress) {
discard;
}
Expand Down
35 changes: 27 additions & 8 deletions packages/melonjs/src/video/effects/glsl_realization.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import quadVertex from "../webgl/shaders/quad.vert";
import {
LOCAL_FRAGMENT_GLSL,
LOCAL_SPACE_IDENTIFIER,
localVertexDeclarationsGLSL,
localVertexStatementsGLSL,
NOISE_UV_GLSL,
} from "./local_space.js";

/*
* The GLSL realization of a ShaderEffect body — the pure source-assembly
Expand All @@ -20,11 +27,15 @@ import quadVertex from "../webgl/shaders/quad.vert";
* (0..1 across the screen).
* - `noise_uv` — varying with a frame-local coordinate across the drawn
* object (undoes atlas packing; scaled to object pixels so patterns keep
* their density when the destination is scaled).
* their density when the destination is scaled). On a pass of a
* renderable's effect chain, 0..1 across the renderable (local_space.js).
*
* The builtins only activate when referenced, and never when the body
* carries its OWN declaration of the identifier — a shader that used these
* names before this feature keeps compiling unchanged.
*
* The engine's own bodies also read `ME_local` and `ME_art`, where they are
* on the renderable they draw (see local_space.js).
*/
const SCREEN_TEXTURE_ANNOTATION =
/\buniform\s+sampler2D\s+([A-Za-z_]\w*)\s*:\s*screen_texture(?:\((repeat|repeat-x|repeat-y|no-repeat)\))?\s*;/g;
Expand Down Expand Up @@ -69,7 +80,8 @@ function parseShaderBuiltins(fragmentBody) {
const noiseUV =
NOISE_UV_IDENTIFIER.test(body) && !hasOwnDeclaration(body, "noise_uv");
const spriteFrame = SPRITE_FRAME_SWITCH.test(body);
return { body, screenTextures, screenUV, noiseUV, spriteFrame };
const local = LOCAL_SPACE_IDENTIFIER.test(body);
return { body, screenTextures, screenUV, noiseUV, spriteFrame, local };
}

/**
Expand All @@ -84,9 +96,17 @@ function parseShaderBuiltins(fragmentBody) {
* @internal
*/
function buildEffectVertex(builtins) {
if (!builtins.screenUV && !builtins.noiseUV && !builtins.spriteFrame) {
if (
!builtins.screenUV &&
!builtins.noiseUV &&
!builtins.spriteFrame &&
!builtins.local
) {
return quadVertex;
}
// `noise_uv` and the local space both read where the vertex is on the
// renderable (local_space.js)
const space = builtins.noiseUV || builtins.local;
return [
"attribute vec3 aVertex;",
"attribute vec2 aRegion;",
Expand All @@ -111,17 +131,15 @@ function buildEffectVertex(builtins) {
"varying vec2 noise_uv;",
]
: []),
...(space ? localVertexDeclarationsGLSL(builtins) : []),
"void main(void) {",
" vec4 ME_clip = uProjectionMatrix * vec4(aVertex, 1.0);",
" gl_Position = ME_clip;",
...(builtins.screenUV
? [" screen_uv = ME_clip.xy / ME_clip.w * 0.5 + 0.5;"]
: []),
...(builtins.noiseUV
? [
" noise_uv = aRegion * (ME_size_img / ME_size_obj) - ME_offset / ME_size_obj;",
]
: []),
...(space ? localVertexStatementsGLSL(builtins) : []),
...(builtins.noiseUV ? [NOISE_UV_GLSL] : []),
...(builtins.spriteFrame
? [
// a side on the texture's own edge (within a hundredth of
Expand Down Expand Up @@ -169,6 +187,7 @@ export function buildGLSLProgram(fragmentBody) {
"varying vec2 vRegion;",
...(builtins.screenUV ? ["varying vec2 screen_uv;"] : []),
...(builtins.noiseUV ? ["varying vec2 noise_uv;"] : []),
...(builtins.local ? [LOCAL_FRAGMENT_GLSL] : []),
builtins.body,
"void main(void) {",
" vec4 texColor = texture2D(uSampler, vRegion) * vColor;",
Expand Down
6 changes: 4 additions & 2 deletions packages/melonjs/src/video/effects/hologram.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ struct HologramUniforms {

fn apply(color : vec4f, uv : vec2f) -> vec4f {
// scan line
let scan = sin(uv.y * 200.0 + fx.uTime * 5.0) * 0.5 + 0.5;
// lines of its texture, fixed on the sprite
let scan = sin(ME_art.y * 200.0 + fx.uTime * 5.0) * 0.5 + 0.5;
// flicker
let flicker = 0.95 + 0.05 * sin(fx.uTime * 30.0);
// color shift
Expand Down Expand Up @@ -62,7 +63,8 @@ export default class HologramEffect extends ShaderEffect {
uniform float uTime;
vec4 apply(vec4 color, vec2 uv) {
// scan line
float scan = sin(uv.y * 200.0 + uTime * 5.0) * 0.5 + 0.5;
// lines of its texture, fixed on the sprite
float scan = sin(ME_art.y * 200.0 + uTime * 5.0) * 0.5 + 0.5;
// flicker
float flicker = 0.95 + 0.05 * sin(uTime * 30.0);
// color shift
Expand Down
Loading
Loading