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
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
*/
import {
Application,
BloomEffect,
BlurEffect,
ChromaticAberrationEffect,
ColorMatrixEffect,
DesaturateEffect,
DissolveEffect,
DropShadowEffect,
Expand All @@ -20,7 +22,9 @@ import {
PixelateEffect,
ScanlineEffect,
SepiaEffect,
ShineEffect,
Sprite,
SquishEffect,
Stage,
state,
Text,
Expand All @@ -32,8 +36,13 @@ import {
import { createExampleComponent } from "../utils";
import monsterImg from "./assets/monster.png";

// five rows of four
const WIDTH = 1218;
const HEIGHT = 702;
const SPRITE_SCALE = 0.25;

const createGame = async () => {
const app = new Application(1218, 562, {
const app = new Application(WIDTH, HEIGHT, {
parent: "screen",
scale: "auto",
backgroundColor: "#202020",
Expand All @@ -53,10 +62,7 @@ const createGame = async () => {
class EffectsScene extends Stage {
override onResetEvent() {
const r = app.renderer;
const img = loader.getImage("monster");
if (!img) return;
const tw = img.width;
const th = img.height;
if (!loader.getImage("monster")) return;

const names = [
"Original",
Expand All @@ -75,18 +81,26 @@ const createGame = async () => {
"Invert",
"Sepia",
"Hologram",
"Shine",
"Squish",
"Color Matrix",
"Bloom",
];

const cols = 4;
const rows = Math.ceil(names.length / cols);
const cellW = 1218 / cols;
const cellH = 562 / rows;
const cellW = WIDTH / cols;
const cellH = HEIGHT / rows;
let flashEffect: FlashEffect | null = null;
let glowEffect: GlowEffect | null = null;
let dissolveEffect: DissolveEffect | null = null;
let pulseEffect: TintPulseEffect | null = null;
let waveEffect: WaveEffect | null = null;
let hologramEffect: HologramEffect | null = null;
let shineEffect: ShineEffect | null = null;
let squishEffect: SquishEffect | null = null;
let squishSprite: Sprite | null = null;
let matrixEffect: ColorMatrixEffect | null = null;

for (let i = 0; i < names.length; i++) {
const col = i % cols;
Expand All @@ -95,7 +109,7 @@ const createGame = async () => {
const cy = cellH * row + cellH / 2;

const sprite = new Sprite(cx, cy - 10, { image: "monster" });
sprite.scale(0.25);
sprite.scale(SPRITE_SCALE);
app.world.addChild(sprite);

// apply effect based on index
Expand All @@ -109,14 +123,12 @@ const createGame = async () => {
color: [1, 1, 0],
width: 2,
});
(sprite.shader as OutlineEffect).setTextureSize(tw, th);
break;
case 3:
glowEffect = new GlowEffect(r, {
color: [0.3, 0.6, 1],
width: 10,
intensity: 2.5,
textureSize: [tw, th],
});
sprite.shader = glowEffect;
break;
Expand All @@ -126,19 +138,16 @@ const createGame = async () => {
case 5:
sprite.shader = new PixelateEffect(r, {
size: 6,
textureSize: [tw, th],
});
break;
case 6:
sprite.shader = new BlurEffect(r, {
strength: 5,
textureSize: [tw, th],
});
break;
case 7:
sprite.shader = new ChromaticAberrationEffect(r, {
offset: 5,
textureSize: [tw, th],
});
break;
case 8:
Expand All @@ -150,7 +159,6 @@ const createGame = async () => {
offsetX: 6,
offsetY: 6,
opacity: 0.7,
textureSize: [tw, th],
});
break;
case 10:
Expand Down Expand Up @@ -186,6 +194,30 @@ const createGame = async () => {
hologramEffect = new HologramEffect(r);
sprite.shader = hologramEffect;
break;
case 16:
shineEffect = new ShineEffect(r, {
color: [1.0, 0.95, 0.7],
width: 0.18,
intensity: 0.7,
});
sprite.shader = shineEffect;
break;
case 17:
squishEffect = new SquishEffect(r);
squishSprite = sprite;
sprite.shader = squishEffect;
break;
case 18:
matrixEffect = new ColorMatrixEffect(r);
sprite.shader = matrixEffect;
break;
case 19:
sprite.shader = new BloomEffect(r, {
threshold: 0.3,
intensity: 2.2,
radius: 6,
});
break;
}

app.world.addChild(
Expand Down Expand Up @@ -232,6 +264,26 @@ const createGame = async () => {
if (hologramEffect) {
hologramEffect.setTime(t);
}
if (shineEffect) {
shineEffect.setTime(t);
}
if (squishEffect && squishSprite) {
// squash and stretch about the feet, keeping the volume: a
// taller sprite pinches in, a shorter one bulges out
const scaleY = 1 + 0.2 * Math.sin(t * 4);
const feet = squishSprite.height * (1 - squishSprite.anchorPoint.y);
squishSprite.currentTransform
.identity()
.scale(SPRITE_SCALE)
.translate(0, feet)
.scale(1, scaleY)
.translate(0, -feet);
squishEffect.setBulge(1 - scaleY);
}
if (matrixEffect) {
// the hue turns around the color wheel
matrixEffect.reset().hueRotate(t);
}
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/examples/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,7 @@ const examples: {
path: "shader-effects",
sourceDir: "shaderEffects",
description:
"Showcase of all 16 built-in ShaderEffect presets for per-sprite visual effects (WebGL).",
"Showcase of the built-in ShaderEffect presets for per-sprite visual effects.",
},
{
component: <ExampleSprite />,
Expand Down
1 change: 1 addition & 0 deletions packages/melonjs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
- Physics: the builtin `raycast` measures a body from the frame its renderable draws in, as collisions have since 20.7. On a body whose anchor is not its top left corner, such as a `Sprite`'s default centre, a ray hit half the body's size away from where it collides, or missed it
- Camera: a zoomed camera follows its target, stays within its bounds and centres on `focusOn` in world units, and `localToWorld` and `worldToLocal` take its zoom and its place on the screen (`screenX`, `screenY`) into account. Zoomed out, it kept the target off centre and scrolled past its bounds, zoomed in it stopped short of them, and a pointer on a zoomed camera, or on one drawn in a corner of the screen, hit the world somewhere else than it pointed
- Effects: a glow, outline, blur, drop shadow, chromatic aberration, wave, squish, pixelate or bloom on a sprite cut from an atlas or a tileset keeps to the sprite's own frame. Along the frame's edges it picked up the neighbouring frames, and drew a glow or a line from pixels that are not part of the sprite
- Effects: a glow, outline, blur, drop shadow, chromatic aberration, pixelate or bloom measures its width, offset or size in pixels of the texture it draws, as documented, with no `textureSize` to pass: a sprite's texture, the screen for a camera's effect, a mesh's texture. Each assumed a 256x256 texture (the screen for an outline or a bloom), so on any other texture it came out thinner or thicker than asked; a game that tuned its widths around that should check them, and an effect given a `textureSize` keeps it
- Tiled: a tile layer drawn with smooth filtering, scaled or off the pixel grid, shows no seams between its tiles on WebGL and WebGPU. Along each tile's edge, filtering blended in the next tile of the tileset, or the spacing around it
- Tiled: a pre-rendered tile layer holds every tile it draws, on hexagonal, staggered and oblique maps and with tiles larger than their cell or offset from it, and shows the tiles `setTile` and `clearTile` change. It was baked at an orthogonal layer's size, which cut off the last column of a hexagonal map, what an oblique map draws left of or above its origin and the parts of large tiles reaching past the edge; `setTile` left it as it was, and `clearTile` erased a rectangle placed as on an orthogonal map, cutting into the tiles around it
- Tiled: an orthogonal tile layer with an opacity below 1, or inside a faded container, is drawn faded on WebGL and WebGPU. Its colors were drawn at full strength over what lies behind it, as bright as an opaque layer or brighter, and the container's fade was ignored
Expand Down
20 changes: 15 additions & 5 deletions packages/melonjs/skills/melonjs-effects-and-shaders/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,19 @@ pixel keep to the sprite's own frame.** `GlowEffect`, `OutlineEffect`,
`SwayEffect`, `PixelateEffect` and `BloomEffect` read the neighbouring frames
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. Their `textureSize` is still the size of the whole atlas image: it is
what turns a width in texels into a distance in the texture.
edge (a standalone image, or the outside of an atlas) nothing changes.

**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
camera's effect, a mesh's texture. Do not pass `textureSize` to get this: a
`textureSize` (or `setTextureSize()`) fixes the size instead, for a width
meant in some other texture's pixels.

```js
// 2 pixels of the sprite's art, whatever the size of its atlas
sprite.addPostEffect(new OutlineEffect(app.renderer, { width: 2 }));
```

`SwayEffect` leans a sprite or a layer from side to side, as if in a breeze: the
top sways most and everything from its `anchor` row down stays put, so a tree's
Expand Down Expand Up @@ -217,9 +228,8 @@ app.viewport.addPostEffect(
);
```

Its tap offsets are in texels, so it takes its size from the renderer at
construction; call `setTextureSize(w, h)` from an `event.ONRESIZE` handler, or
when applying it to something smaller than the frame.
Its `radius` is in texels of what it filters, the screen on a camera, and it
follows a resize by itself.

**`ToneMappingEffect` goes after it, and goes last.** Bloom spreads the bright
parts; the tone map is what stops the sum of the two clipping flat at white,
Expand Down
29 changes: 12 additions & 17 deletions packages/melonjs/src/video/effects/bloom.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,8 @@ ${tapList("vec2f", (at) => {
* Not to be confused with {@link GlowEffect}, which draws an outline OUTSIDE a
* sprite's silhouette and does nothing at all on an opaque frame.
*
* `textureSize` must match what is being filtered, since the tap offsets are
* in texels; it defaults to the renderer's own size, which is what a camera
* effect wants. Call {@link BloomEffect#setTextureSize} from an
* {@link event.ONRESIZE} handler, or after applying it to something smaller.
* Its reach is in texels of what it filters: the screen for a camera's
* effect, the sprite's texture on a sprite, so it follows a resize by itself.
* @category Effects
* @see {@link Renderable#addPostEffect} for usage
* @example
Expand All @@ -160,26 +158,23 @@ export default class BloomEffect extends ShaderEffect {
* @param {number} [options.threshold=0.75] - luminance a pixel must pass before it blooms, `0` (everything blooms) to `1` (only white does)
* @param {number} [options.intensity=1.0] - how much of the gathered light is added back
* @param {number} [options.radius=2.0] - halo reach, in texels
* @param {number[]} [options.textureSize] - dimensions of what is being filtered, `[width, height]`; defaults to the renderer's size
* @param {number[]} [options.textureSize] - the size, `[width, height]`, of the texture its reach is measured in; by default the texture it draws (a sprite's own, the screen for a camera's effect)
*/
constructor(renderer, options = {}) {
super(renderer, { glsl: glslFragment, wgsl: wgslFragment });

this.threshold = options.threshold ?? 0.75;
this.intensity = options.intensity ?? 1.0;
this.radius = options.radius ?? 2.0;
// A camera effect filters the frame, so the frame's size is the useful
// default. `BlurEffect` predates this and defaults to 256x256, which
// is why its own docs have to tell you to set it.
const texSize = options.textureSize ?? [
renderer?.width ?? 256,
renderer?.height ?? 256,
];

this.setUniform("uThreshold", this.threshold);
this.setUniform("uIntensity", this.intensity);
this.setUniform("uRadius", this.radius);
this.setUniform("uTextureSize", new Float32Array(texSize));
// its reach is in texels: of the texture it draws, unless given one
this._initTextureSize(options.textureSize, [
renderer?.width ?? 256,
renderer?.height ?? 256,
]);
}

/**
Expand Down Expand Up @@ -210,10 +205,10 @@ export default class BloomEffect extends ShaderEffect {
}

/**
* set the dimensions of what is being filtered. The tap offsets are in
* texels, so a stale size makes the halo the wrong width after a resize.
* @param {number} width - width in pixels
* @param {number} height - height in pixels
* Measure the effect's reach in the texels of a texture of this size,
* rather than of the texture it draws (the default)
* @param {number} width - texture width in pixels
* @param {number} height - texture height in pixels
*/
setTextureSize(width, height) {
this.setUniform("uTextureSize", new Float32Array([width, height]));
Expand Down
9 changes: 5 additions & 4 deletions packages/melonjs/src/video/effects/blur.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default class BlurEffect extends ShaderEffect {
* @param {Renderer} renderer - the current renderer instance
* @param {object} [options] - effect options
* @param {number} [options.strength=1.0] - blur strength in texels
* @param {number[]} [options.textureSize=[256, 256]] - texture dimensions [width, height]
* @param {number[]} [options.textureSize] - the size, `[width, height]`, of the texture its strength is measured in; by default the texture it draws (a sprite's own, the screen for a camera's effect)
*/
constructor(renderer, options = {}) {
super(renderer, {
Expand Down Expand Up @@ -77,10 +77,10 @@ export default class BlurEffect extends ShaderEffect {
});

this.strength = options.strength ?? 1.0;
const texSize = options.textureSize ?? [256, 256];

this.setUniform("uBlurStrength", this.strength);
this.setUniform("uTextureSize", new Float32Array(texSize));
// its strength is in texels: of the texture it draws, unless given one
this._initTextureSize(options.textureSize, [256, 256]);
}

/**
Expand All @@ -93,7 +93,8 @@ export default class BlurEffect extends ShaderEffect {
}

/**
* set the texture size
* Measure the effect's strength in the texels of a texture of this size,
* rather than of the texture it draws (the default)
* @param {number} width - texture width in pixels
* @param {number} height - texture height in pixels
*/
Expand Down
9 changes: 5 additions & 4 deletions packages/melonjs/src/video/effects/chromaticAberration.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default class ChromaticAberrationEffect extends ShaderEffect {
* @param {Renderer} renderer - the current renderer instance
* @param {object} [options] - effect options
* @param {number} [options.offset=3.0] - channel offset in texels
* @param {number[]} [options.textureSize=[256, 256]] - texture dimensions [width, height]
* @param {number[]} [options.textureSize] - the size, `[width, height]`, of the texture its offset is measured in; by default the texture it draws (a sprite's own, the screen for a camera's effect)
*/
constructor(renderer, options = {}) {
super(renderer, {
Expand All @@ -66,10 +66,10 @@ export default class ChromaticAberrationEffect extends ShaderEffect {
});

this.offset = options.offset ?? 3.0;
const texSize = options.textureSize ?? [256, 256];

this.setUniform("uOffset", this.offset);
this.setUniform("uTextureSize", new Float32Array(texSize));
// its offset is in texels: of the texture it draws, unless given one
this._initTextureSize(options.textureSize, [256, 256]);
}

/**
Expand All @@ -82,7 +82,8 @@ export default class ChromaticAberrationEffect extends ShaderEffect {
}

/**
* set the texture size
* Measure the effect's offset in the texels of a texture of this size,
* rather than of the texture it draws (the default)
* @param {number} width - texture width in pixels
* @param {number} height - texture height in pixels
*/
Expand Down
9 changes: 5 additions & 4 deletions packages/melonjs/src/video/effects/dropShadow.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export default class DropShadowEffect extends ShaderEffect {
* @param {number} [options.offsetY=2.0] - shadow y offset in texels
* @param {number[]} [options.color=[0.0, 0.0, 0.0]] - shadow color as [r, g, b] or [r, g, b, a] (0.0–1.0); the alpha scales `opacity`
* @param {number} [options.opacity=0.5] - shadow opacity (0.0–1.0)
* @param {number[]} [options.textureSize=[256, 256]] - texture dimensions [width, height]
* @param {number[]} [options.textureSize] - the size, `[width, height]`, of the texture its offset is measured in; by default the texture it draws (a sprite's own, the screen for a camera's effect)
*/
constructor(renderer, options = {}) {
super(renderer, {
Expand Down Expand Up @@ -105,14 +105,14 @@ export default class DropShadowEffect extends ShaderEffect {
wgsl: wgslFragment,
});

const texSize = options.textureSize ?? [256, 256];
this.setUniform(
"uShadowOffset",
new Float32Array([options.offsetX ?? 2.0, options.offsetY ?? 2.0]),
);
this.setUniform("uShadowColor", rgba(options.color ?? [0.0, 0.0, 0.0]));
this.setUniform("uShadowOpacity", options.opacity ?? 0.5);
this.setUniform("uTextureSize", new Float32Array(texSize));
// its offset is in texels: of the texture it draws, unless given one
this._initTextureSize(options.textureSize, [256, 256]);
// uv.y grows downward until a renderer path says otherwise
this.setUniform("uUVYDir", 1.0);
}
Expand All @@ -135,7 +135,8 @@ export default class DropShadowEffect extends ShaderEffect {
}

/**
* set the texture size
* Measure the effect's offset in the texels of a texture of this size,
* rather than of the texture it draws (the default)
* @param {number} width - texture width in pixels
* @param {number} height - texture height in pixels
*/
Expand Down
Loading
Loading