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
20 changes: 18 additions & 2 deletions packages/examples/src/examples/sprite/ExampleSprite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
* seconds (click it for the next one), up to two effects chained. Coins cut
* from a TexturePacker atlas orbit it, spinning and passing in front of it
* and behind it; the astronaut drifts by, walking in mid-air; flies buzz
* around; tinted, fading and additive little monsters float in the dark.
* around; tinted, fading and additive little monsters float in the dark,
* one of them fading through two effects.
* The nebula behind them is Earth Defender's sky, drawn once at the size of
* the screen with the same noise.
*/
Expand Down Expand Up @@ -79,7 +80,8 @@ class Showcase extends Stage {
0,
);

// ---- little monsters, tinted, fading, two of them added on --------
// ---- little monsters, tinted, fading, two of them added on, one
// of them fading through two effects ---------------------------------
const minions = [
{ x: 170, y: 150, tint: "#ff7a6e", blend: "normal" },
{ x: 1050, y: 560, tint: "#7ab8ff", blend: "normal" },
Expand All @@ -91,6 +93,20 @@ class Showcase extends Stage {
minion.scale(0.13);
minion.tint.parseCSS(m.tint);
minion.blendMode = m.blend;
if (i === 0 && app.renderer.shaderLanguage !== null) {
// two effects: drawn apart and composited, at the opacity it
// fades to like any other sprite
minion.addPostEffect(
new OutlineEffect(app.renderer, { color: [1, 0.9, 0.85], width: 2 }),
);
minion.addPostEffect(
new GlowEffect(app.renderer, {
color: [1.0, 0.5, 0.4],
width: 8,
intensity: 1.5,
}),
);
}
world.addChild(minion, 1);
move((time, step) => {
minion.setOpacity(0.45 + 0.4 * Math.sin(time * 0.8 + i * 1.7));
Expand Down
1 change: 1 addition & 0 deletions packages/melonjs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- 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
- Effects: a renderable drawn through two or more effects keeps its opacity, its own and its containers', on WebGL and WebGPU, as does a `UIPanel`, `ProgressBar` or `Trail` with one ([#1791](https://github.com/melonjs/melonJS/issues/1791)). Their effects were composited at full opacity, so `setOpacity()` or a fade changed nothing while the effects were on

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

Expand Down
3 changes: 2 additions & 1 deletion packages/melonjs/src/video/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1442,9 +1442,10 @@ export default class Renderer {
* @param {number} height - destination height
* @param {ShaderEffect} shader - the shader effect to apply
* @param {boolean} [keepBlend=false] - if true, keep current blend mode (for sprite compositing)
* @param {number} [alpha=1] - the opacity to composite the blit at (0.0 to 1.0)
*/
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
blitEffect(source, x, y, width, height, shader, keepBlend) {}
blitEffect(source, x, y, width, height, shader, keepBlend, alpha) {}

/**
* Sets the viewport for the renderer.
Expand Down
12 changes: 10 additions & 2 deletions packages/melonjs/src/video/webgl/batchers/lit_quad_batcher.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Color } from "../../../math/color.ts";
import { off, on, TEXTURE2D_DESTROYED } from "../../../system/event.ts";
import { FRAME_UNBOUNDED, isFramed } from "../../gpu/frame.js";
import { scaleQuad, transformQuadCorners } from "../../gpu/quadcorners.ts";
Expand All @@ -13,6 +14,12 @@ import quadMultiLitVertex from "./../shaders/quad-multi-lit.vert";
import { WebGLTextureStore } from "../texture/store.js";
import QuadBatcher from "./quad_batcher.js";

/**
* the blit's tint: white, at the opacity the blit composites at
* @ignore
*/
const blitTint = new Color(255, 255, 255);

/**
* additional import for TypeScript
* @import {TextureAtlas} from "./../../texture/atlas.js";
Expand Down Expand Up @@ -697,8 +704,9 @@ export default class LitQuadBatcher extends QuadBatcher {
* @param {number} width - destination width
* @param {number} height - destination height
* @param {GLShader|ShaderEffect} shader - the shader effect to apply
* @param {number} [alpha=1] - the opacity to composite at (0.0 to 1.0)
*/
blitTexture(source, x, y, width, height, shader) {
blitTexture(source, x, y, width, height, shader, alpha = 1) {
const gl = this.gl;

this.useShader(shader);
Expand Down Expand Up @@ -741,7 +749,7 @@ export default class LitQuadBatcher extends QuadBatcher {
// blits are always rendered at z = 0 (screen-space, ortho), carry no
// normal map (-1) and no material (shininess 0), and draw the whole
// target: no frame
const tint = 0xffffffff;
const tint = blitTint.toUint32(alpha);
const open = FRAME_UNBOUNDED;
const vertexData = this.vertexData;
vertexData.pushSprite(vec0.x, vec0.y, 0, 0, 1, tint, 0, open, open, -1, 0);
Expand Down
12 changes: 10 additions & 2 deletions packages/melonjs/src/video/webgl/batchers/quad_batcher.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Color } from "../../../math/color.ts";
import { FRAME_UNBOUNDED, frameClampMode, isFramed } from "../../gpu/frame.js";
import { scaleQuad, transformQuadCorners } from "../../gpu/quadcorners.ts";
import IndexBuffer from "../buffer/index.js";
Expand All @@ -6,6 +7,12 @@ import quadMultiVertex from "./../shaders/quad-multi.vert";
import { resolveTopology } from "../utils/topology.js";
import { MaterialBatcher } from "./material_batcher.js";

/**
* the blit's tint: white, at the opacity the blit composites at
* @ignore
*/
const blitTint = new Color(255, 255, 255);

/**
* additional import for TypeScript
* @import {TextureAtlas} from "./../../texture/atlas.js";
Expand Down Expand Up @@ -246,8 +253,9 @@ export default class QuadBatcher extends MaterialBatcher {
* @param {number} width - destination width
* @param {number} height - destination height
* @param {GLShader|ShaderEffect} shader - the shader effect to apply
* @param {number} [alpha=1] - the opacity to composite at (0.0 to 1.0)
*/
blitTexture(source, x, y, width, height, shader) {
blitTexture(source, x, y, width, height, shader, alpha = 1) {
const gl = this.gl;

this.useShader(shader);
Expand Down Expand Up @@ -300,7 +308,7 @@ export default class QuadBatcher extends MaterialBatcher {

// blits stay at z = 0 (screen-space, ortho); pre-PR behavior
// preserved unchanged. A blit draws the whole target: no frame
const tint = 0xffffffff;
const tint = blitTint.toUint32(alpha);
const open = FRAME_UNBOUNDED;
const vertexData = this.vertexData;
vertexData.pushSprite(vec0.x, vec0.y, 0, 0, 1, tint, 0, open, open);
Expand Down
35 changes: 32 additions & 3 deletions packages/melonjs/src/video/webgl/webgl_renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,16 @@ export default class WebGLRenderer extends Renderer {
* @internal
*/
this._effectProjectionStack = [];

/**
* The opacity each (possibly nested) post-effect pass was begun
* under, by depth: the pass draws its content opaque, and the last
* blit of a renderable's chain composites it at this.
* @type {number[]}
* @ignore
* @internal
*/
this._effectAlphaStack = [];
/**
* @ignore
* @internal
Expand Down Expand Up @@ -2107,6 +2117,9 @@ export default class WebGLRenderer extends Renderer {
new Matrix3d();
}
savedProjection.copy(this.projectionMatrix);
// the opacity the renderable is drawn at: its content goes into the
// target opaque, and the chain's last blit composites it at this
this._effectAlphaStack[this._effectPassDepth] = this.globalAlpha();
this._effectPassDepth++;

const rt = this._renderTargetPool.begin(
Expand Down Expand Up @@ -2215,8 +2228,13 @@ export default class WebGLRenderer extends Renderer {

// the chain's passes are told where the renderable is
this._beginChainSpace(renderable);
// a renderable's chain lands at the opacity it is drawn at, by its
// last blit only; a camera's composites whole
const alpha = isCamera
? 1
: this._effectAlphaStack[this._effectPassDepth - 1];
if (effects.length === 1) {
this.blitEffect(rt1.texture, 0, 0, w, h, effects[0], keepBlend);
this.blitEffect(rt1.texture, 0, 0, w, h, effects[0], keepBlend, alpha);
} else {
// multi-pass: ping-pong between two render targets
let src = rt1;
Expand Down Expand Up @@ -2247,6 +2265,7 @@ export default class WebGLRenderer extends Renderer {
h,
effects[effects.length - 1],
keepBlend,
alpha,
);
}

Expand Down Expand Up @@ -2450,8 +2469,18 @@ export default class WebGLRenderer extends Renderer {
* @param {number} height - destination height
* @param {ShaderEffect} shader - the shader effect to apply
* @param {boolean} [keepBlend=false] - if true, keep current blend mode (for sprite compositing)
* @param {number} [alpha=1] - the opacity to composite the blit at (0.0 to 1.0)
*/
blitEffect(source, x, y, width, height, shader, keepBlend = false) {
blitEffect(
source,
x,
y,
width,
height,
shader,
keepBlend = false,
alpha = 1,
) {
// A blit is compositing machinery, not a scene draw: it must never
// open an advanced-blend bracket of its own. That covers both the
// post-effect chain's ping-pong blits (which run under the
Expand All @@ -2475,7 +2504,7 @@ export default class WebGLRenderer extends Renderer {
if (!keepBlend) {
this.setBlendEnabled(false);
}
batcher.blitTexture(source, x, y, width, height, shader);
batcher.blitTexture(source, x, y, width, height, shader, alpha);
if (!keepBlend) {
this.setBlendEnabled(true);
}
Expand Down
12 changes: 10 additions & 2 deletions packages/melonjs/src/video/webgpu/batchers/quad_batcher.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Color } from "../../../math/color.ts";
import IndexBuffer from "../../buffer/index.js";
import { FRAME_UNBOUNDED, isFramed } from "../../gpu/frame.js";
import { scaleQuad, transformQuadCorners } from "../../gpu/quadcorners.ts";
Expand All @@ -6,6 +7,12 @@ import { prepareEffectBinding } from "../effect_binding.js";
import { MAX_QUAD_TEXTURES } from "../pipeline/cache.js";
import WebGPUBatcher from "./webgpu_batcher.js";

/**
* the blit's tint: white, at the opacity the blit composites at
* @ignore
*/
const blitTint = new Color(255, 255, 255);

/**
* The WebGPU quad batcher — textured-quad accumulation with the same
* frozen 36-byte vertex layout as the WebGL `QuadBatcher` and the same
Expand Down Expand Up @@ -555,10 +562,11 @@ export default class WebGPUQuadBatcher extends WebGPUBatcher {
* @param {number} h - destination height
* @param {ShaderEffect} [effect] - the effect to composite with
* @param {boolean} [keepBlend=false] - keep the current blend mode (else replace)
* @param {number} [alpha=1] - the opacity to composite at (0.0 to 1.0)
* @ignore
* @internal
*/
blitTexture(source, x, y, w, h, effect, keepBlend = false) {
blitTexture(source, x, y, w, h, effect, keepBlend = false, alpha = 1) {
// drain pending quads under their own material first
this.flush();
const renderer = this.renderer;
Expand Down Expand Up @@ -603,7 +611,7 @@ export default class WebGPUQuadBatcher extends WebGPUBatcher {
// whole target drawn: no frame
const vertexData = this.vertexData;
const open = FRAME_UNBOUNDED;
const tint = 0xffffffff;
const tint = blitTint.toUint32(alpha);
vertexData.pushSprite(x, y, 0, 0, 0, tint, 0, open, open);
vertexData.pushSprite(x + w, y, 0, 1, 0, tint, 0, open, open);
vertexData.pushSprite(x, y + h, 0, 0, 1, tint, 0, open, open);
Expand Down
45 changes: 41 additions & 4 deletions packages/melonjs/src/video/webgpu/webgpu_renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,16 @@ export default class WebGPURenderer extends Renderer {
/** @ignore
* @internal */
this.effectProjectionStack = [];

/**
* The opacity each (possibly nested) post-effect pass was begun
* under, by depth: the pass draws its content opaque, and the last
* blit of a renderable's chain composites it at this.
* @type {number[]}
* @ignore
* @internal
*/
this.effectAlphaStack = [];
/** @ignore
* @internal */
this.effectPassDepth = 0;
Expand Down Expand Up @@ -1829,6 +1839,9 @@ export default class WebGPURenderer extends Renderer {
new Matrix3d();
}
savedProjection.copy(this.projectionMatrix);
// the opacity the renderable is drawn at: its content goes into the
// target opaque, and the chain's last blit composites it at this
this.effectAlphaStack[this.effectPassDepth] = this.globalAlpha();
this.effectPassDepth++;

const rt = this._postEffectPool().begin(
Expand Down Expand Up @@ -1925,8 +1938,13 @@ export default class WebGPURenderer extends Renderer {

// the chain's passes are told where the renderable is
this._beginChainSpace(renderable);
// a renderable's chain lands at the opacity it is drawn at, by its
// last blit only; a camera's composites whole
const alpha = isCamera
? 1
: this.effectAlphaStack[this.effectPassDepth - 1];
if (effects.length === 1) {
this.blitEffect(rt1, 0, 0, w, h, effects[0], keepBlend);
this.blitEffect(rt1, 0, 0, w, h, effects[0], keepBlend, alpha);
} else {
// multi-pass: ping-pong between the two pool targets
let src = rt1;
Expand All @@ -1939,7 +1957,16 @@ export default class WebGPURenderer extends Renderer {
dst = tmp;
}
this.setRenderTarget(parentRT);
this.blitEffect(src, 0, 0, w, h, effects[effects.length - 1], keepBlend);
this.blitEffect(
src,
0,
0,
w,
h,
effects[effects.length - 1],
keepBlend,
alpha,
);
}

if (isCamera && renderable.isDefault === false) {
Expand Down Expand Up @@ -1969,9 +1996,19 @@ export default class WebGPURenderer extends Renderer {
* @param {number} height - destination height
* @param {ShaderEffect} effect - the effect to composite with
* @param {boolean} [keepBlend=false] - keep the current blend mode
* @param {number} [alpha=1] - the opacity to composite the blit at (0.0 to 1.0)
* @override
*/
blitEffect(source, x, y, width, height, effect, keepBlend = false) {
blitEffect(
source,
x,
y,
width,
height,
effect,
keepBlend = false,
alpha = 1,
) {
// A blit is compositing machinery, not a scene draw: it must never
// open an advanced-blend bracket of its own. Covers both the
// post-effect chain's ping-pong blits (which run under the
Expand All @@ -1984,7 +2021,7 @@ export default class WebGPURenderer extends Renderer {
blitSavedProjection.copy(this.projectionMatrix);
this.projectionMatrix.ortho(0, width, height, 0, -1, 1);
this.pushFrameGlobals();
batcher.blitTexture(source, x, y, width, height, effect, keepBlend);
batcher.blitTexture(source, x, y, width, height, effect, keepBlend, alpha);
this.projectionMatrix.copy(blitSavedProjection);
this.pushFrameGlobals();
this._advancedBlendBusy--;
Expand Down
Loading
Loading