Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
87a9971
refactor(video): hoist ShaderEffect + effects to a backend-neutral home
obiot Aug 2, 2026
6db9de3
feat(video): dual-language ShaderEffect bodies + WGSL contract core
obiot Aug 2, 2026
a30be78
feat(video): WebGPU render targets, pass-target parametrization + fra…
obiot Aug 2, 2026
0e13ba2
feat(video): WebGPU pooled post-effect path — effects composite on We…
obiot Aug 2, 2026
24be724
feat(video): WebGPU single-effect fast path (customShader)
obiot Aug 2, 2026
1509e83
feat(video): WGSL twin bodies for all built-in effects
obiot Aug 2, 2026
04cf5d3
feat(loader): dual-language {glsl, wgsl} shader assets + effect docs
obiot Aug 2, 2026
0de8f01
fix(examples): platformer viewport vignette gates on shader capabilit…
obiot Aug 2, 2026
1e1b7ae
fix(video): linear-time block-comment stripping in the WGSL parser
obiot Aug 2, 2026
6ace7d9
fix(video): scan WGSL comments without a regex
obiot Aug 2, 2026
a98d2c5
fix(examples): platformer attaches its vignette with no renderer chec…
obiot Aug 2, 2026
8a2a6fa
fix(video): apply adversarial-review findings on the effect pipeline
obiot Aug 2, 2026
13f4d70
feat(video): GPU tile rendering on the WebGPU backend (orthogonal TMX)
obiot Aug 3, 2026
e4abd0b
fix(tests): drop the unused batcher-name param in the tmx spec helper
obiot Aug 3, 2026
58aded0
fix(examples): migrate stale bootstrap guards to createExampleComponent
obiot Aug 3, 2026
ed3c43d
fix(examples): WebGPU parity fixes across the 2D example sweep
obiot Aug 3, 2026
e81fba4
feat(examples): WGSL twin for the Water Overworld refraction shader
obiot Aug 3, 2026
d5d2d5e
feat(video): shape-masked gradient fills on the WebGPU backend
obiot Aug 3, 2026
76f3fa0
feat(video): toFrameTexture on the WebGPU backend
obiot Aug 3, 2026
35ae9ea
feat(video): 2D lights and normal-map lighting on the WebGPU backend
obiot Aug 3, 2026
009548f
refactor(video): TextureAtlas no longer reaches for the global game
obiot Aug 3, 2026
b9a47e5
feat(video): compressed texture support on the WebGPU backend
obiot Aug 3, 2026
3afa50a
docs: WebGPU 2D feature completion in the 20.0.0 changelog entry
obiot Aug 3, 2026
bef4cda
fix(examples): whac-a-mole HUD z-order — addChild assigns the z
obiot Aug 3, 2026
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
42 changes: 41 additions & 1 deletion packages/examples/src/examples/aquarium/ExampleAquarium.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,42 @@ vec4 apply(vec4 color, vec2 uv) {
}
`;

// the WGSL twin — same logic and uniform names. The WebGPU capture is
// top-down (row 0 = the top of the frame, matching the quad's UV), so the
// GLSL body's Y flip for the Y-up framebuffer copy is dropped.
const WATER_FRAGMENT_WGSL = `
struct AquariumUniforms {
uTime : f32,
uStrength : f32,
};
@group(3) @binding(0) var<uniform> fx : AquariumUniforms;
@group(3) @binding(1) var uScene : texture_2d<f32>;
@group(3) @binding(2) var uSceneSampler : sampler;
@group(3) @binding(3) var uNoise : texture_2d<f32>;
@group(3) @binding(4) var uNoiseSampler : sampler;

fn apply(color : vec4f, uv : vec2f) -> vec4f {
let s = uv;

// two noise layers scrolling apart -> a living flow field
let f1 = textureSample(uNoise, uNoiseSampler, s * 1.6 + vec2f(fx.uTime * 0.03, fx.uTime * 0.05)).rg;
let f2 = textureSample(uNoise, uNoiseSampler, s * 2.7 - vec2f(fx.uTime * 0.04, fx.uTime * 0.02)).rg;
let flow = f1 + f2 - vec2f(1.0);

// refract the captured scene at the displaced screen coord
let scene = textureSample(uScene, uSceneSampler, clamp(s + flow * fx.uStrength, vec2f(0.0), vec2f(1.0))).rgb;

// the water texture (the sprite's own), gently scrolled, as a wet sheen
let water = textureSample(uTexture, uSampler, uv * 0.6 + flow * 0.02).rgb;
var outc = scene * (vec3f(0.75) + 0.5 * water);

// caustic sparkle where the flow layers pinch together
let caustic = pow(max(f1.r * f2.g, 0.0), 3.0) * 1.2;
outc = outc + vec3f(0.10, 0.20, 0.24) * caustic;
return vec4f(outc, 1.0);
}
`;

// a fish that swims horizontally and turns around at the tank edges
class Fish extends Sprite {
private speed: number;
Expand Down Expand Up @@ -318,7 +354,11 @@ const createGame = async () => {
loader.preload(
[
{ name: "aquariumAtlas", type: "image", src: `${base}aquarium.webp` },
{ name: "aquariumWater", type: "shader", data: WATER_FRAGMENT },
{
name: "aquariumWater",
type: "shader",
data: { glsl: WATER_FRAGMENT, wgsl: WATER_FRAGMENT_WGSL },
},
],
() => {
state.change(state.PLAY);
Expand Down
132 changes: 65 additions & 67 deletions packages/examples/src/examples/aseprite/ExampleAseprite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,80 +3,78 @@
* Copyright (C) 2011 - 2026 AltByte Pte Ltd — MIT License.
* See `packages/examples/LICENSE.md` for full license + asset credits.
*/
import * as me from "melonjs";
import { useEffect } from "react";
import type * as me from "melonjs";
import { createExampleComponent } from "../utils";
import { createGame } from "./game";
import { paladin } from "./play";

export const ExampleAseprite = () => {
useEffect(() => {
// `game` is undefined until the first Application finishes init()
if (!me.game?.isInitialized) {
void createGame();
}
}, []);
const Game = createExampleComponent(createGame);

export const ExampleAseprite = () => {
return (
// float above the fixed #screen overlay (see index.css), like the
// tiledMapLoader / spine selectors — in normal flow the game surface
// paints over the controls
<div
style={{
position: "absolute",
top: 44,
left: 16,
zIndex: 1000,
display: "flex",
alignItems: "center",
gap: 8,
}}
>
<div>Animation:</div>
<select
name="animation_name"
id="animation_name"
defaultValue="run front"
<>
<Game />
{/* float above the fixed #screen overlay (see index.css), like the
tiledMapLoader / spine selectors — in normal flow the game surface
paints over the controls */}
<div
style={{
padding: "6px 12px",
fontSize: 14,
background: "#1a1a1a",
color: "#e0e0e0",
border: "1px solid #444",
borderRadius: 4,
}}
onChange={(event) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
(paladin.renderable as me.Sprite).setCurrentAnimation(
event.target.value,
);
position: "absolute",
top: 44,
left: 16,
zIndex: 1000,
display: "flex",
alignItems: "center",
gap: 8,
}}
>
<option value="step">step</option>
<option value="idle defesa">idle defesa</option>
<option value="SHit">SHit</option>
<option value="Delay">Delay</option>
<option value="release">release</option>
<option value="Idle fight">Idle fight</option>
<option value="run front">run front</option>
<option value="run back">run back</option>
<option value="morte">morte</option>
<option value="Skill 1 - Trust-dash">Skill 1 - Trust-dash</option>
<option value="Hurt">Hurt</option>
<option value="Delay c">Delay c</option>
<option value="Delay Front atk">Delay Front atk</option>
<option value="CF1">CF1</option>
<option value="RF1">RF1</option>
<option value="Sit">Sit</option>
<option value="standup">standup</option>
<option value="idle back">idle back</option>
<option value="skill back">skill back</option>
<option value="skill front">skill front</option>
<option value="War Cry">War Cry</option>
<option value="slide">slide</option>
<option value="Magnum Break">Magnum Break</option>
<option value="fallen">fallen</option>
<option value="Ress">Ress</option>
</select>
</div>
<div>Animation:</div>
<select
name="animation_name"
id="animation_name"
defaultValue="run front"
style={{
padding: "6px 12px",
fontSize: 14,
background: "#1a1a1a",
color: "#e0e0e0",
border: "1px solid #444",
borderRadius: 4,
}}
onChange={(event) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
(paladin.renderable as me.Sprite).setCurrentAnimation(
event.target.value,
);
}}
>
<option value="step">step</option>
<option value="idle defesa">idle defesa</option>
<option value="SHit">SHit</option>
<option value="Delay">Delay</option>
<option value="release">release</option>
<option value="Idle fight">Idle fight</option>
<option value="run front">run front</option>
<option value="run back">run back</option>
<option value="morte">morte</option>
<option value="Skill 1 - Trust-dash">Skill 1 - Trust-dash</option>
<option value="Hurt">Hurt</option>
<option value="Delay c">Delay c</option>
<option value="Delay Front atk">Delay Front atk</option>
<option value="CF1">CF1</option>
<option value="RF1">RF1</option>
<option value="Sit">Sit</option>
<option value="standup">standup</option>
<option value="idle back">idle back</option>
<option value="skill back">skill back</option>
<option value="skill front">skill front</option>
<option value="War Cry">War Cry</option>
<option value="slide">slide</option>
<option value="Magnum Break">Magnum Break</option>
<option value="fallen">fallen</option>
<option value="Ress">Ress</option>
</select>
</div>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ const createGame = async () => {
const app = new Application(canvasW, canvasH, {
parent: "screen",
renderer: video.AUTO,
preferWebGL1: false,
});
await app.init();
} catch {
Expand Down
4 changes: 2 additions & 2 deletions packages/examples/src/examples/clipping/ExampleClipping.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class OverflowingRect extends Renderable {
renderer.setColor(this.color);
renderer.fillRect(0, 0, this.width, this.height);
renderer.setColor("#ffffff");
renderer.setLineWidth(3);
renderer.lineWidth = 3;
renderer.strokeRect(0, 0, this.width, this.height);
}
}
Expand Down Expand Up @@ -96,7 +96,7 @@ class ClipOutline extends Renderable {

draw(renderer: Parameters<Renderable["draw"]>[0]) {
renderer.setColor(this.color);
renderer.setLineWidth(3);
renderer.lineWidth = 3;
renderer.strokeRect(this.pos.x, this.pos.y, this.width, this.height);
}
}
Expand Down
33 changes: 32 additions & 1 deletion packages/examples/src/examples/heatHaze/ExampleHeatHaze.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,31 @@ vec4 apply(vec4 color, vec2 uv) {
}
`;

// the WGSL twin — same logic and uniform names. The WebGPU capture is
// top-down (row 0 = the top of the frame, matching the quad's UV), so the
// GLSL Y flip is dropped: the floor weight becomes (0.15 + y) and the
// upward scroll flips sign, everything else carries over.
const HAZE_FRAGMENT_WGSL = `
struct HazeUniforms {
uTime : f32,
uStrength : f32,
};
@group(3) @binding(0) var<uniform> fx : HazeUniforms;
@group(3) @binding(1) var uScene : texture_2d<f32>;
@group(3) @binding(2) var uSceneSampler : sampler;
@group(3) @binding(3) var uNoise : texture_2d<f32>;
@group(3) @binding(4) var uNoiseSampler : sampler;

fn apply(color : vec4f, uv : vec2f) -> vec4f {
let s = uv;
let n1 = textureSample(uNoise, uNoiseSampler, vec2f(s.x * 2.0, s.y * 1.6 + fx.uTime * 0.18)).r;
let n2 = textureSample(uNoise, uNoiseSampler, vec2f(s.x * 3.3 + 0.5, s.y * 2.2 + fx.uTime * 0.11)).r;
let wob = (n1 + n2 - 1.0) * fx.uStrength * (0.15 + s.y); // stronger near the floor
let d = clamp(s + vec2f(wob, wob * 0.35), vec2f(0.0), vec2f(1.0));
return vec4f(textureSample(uScene, uSceneSampler, d).rgb, 1.0);
}
`;

// an embossed metal-ish tile: a base colour with a bevel highlight/shadow, so
// the normal-mapped relief reads clearly under the moving light
const tileAlbedo = (size: number, hue: number) => {
Expand Down Expand Up @@ -238,7 +263,13 @@ const createGame = async () => {
state.set(state.PLAY, new PlayScreen());

loader.preload(
[{ name: "heatHaze", type: "shader", data: HAZE_FRAGMENT }],
[
{
name: "heatHaze",
type: "shader",
data: { glsl: HAZE_FRAGMENT, wgsl: HAZE_FRAGMENT_WGSL },
},
],
() => {
state.change(state.PLAY);
},
Expand Down
1 change: 0 additions & 1 deletion packages/examples/src/examples/masking/ExampleMasking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ const createGame = async () => {
parent: "screen",
scaleMethod: "fit",
renderer: video.AUTO,
preferWebGL1: false,
});
await app.init();
} catch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ export const createGame = async () => {
parent: "screen",
scaleMethod: "flex-width",
renderer: video.AUTO,
preferWebGL1: false,
subPixel: false,
highPrecisionShader: false,
physic: new MatterAdapter({ gravity: { x: 0, y: 5 } }),
Expand Down
19 changes: 9 additions & 10 deletions packages/examples/src/examples/platformer-matter/play.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
type ShaderEffect,
Stage,
VignetteEffect,
WebGLRenderer,
} from "melonjs";
import { VirtualJoypad } from "./entities/controls";
import UIContainer from "./entities/HUD";
Expand Down Expand Up @@ -57,17 +56,17 @@ export class PlayScreen extends Stage {
app.world.addChild(this.virtualJoypad);
}

// Vignette post-effect (WebGL only). Cache the effect across resets
// so we don't stack a new VignetteEffect per restart. Same goes for
// Vignette post-effect, cached across resets so we don't stack a
// new VignetteEffect per restart. No backend check needed: on a
// renderer without a programmable pipeline (Canvas) the effect
// self-disables and the scene renders without it. Same goes for
// the color grading — set from the identity each time so contrast
// and saturation don't compound on each level reload.
if (app.renderer instanceof WebGLRenderer) {
if (!this.vignette) {
this.vignette = new VignetteEffect(app.renderer);
}
if (!app.viewport.postEffects.includes(this.vignette)) {
app.viewport.addPostEffect(this.vignette);
}
if (!this.vignette) {
this.vignette = new VignetteEffect(app.renderer);
}
if (!app.viewport.postEffects.includes(this.vignette)) {
app.viewport.addPostEffect(this.vignette);
}
app.viewport.colorMatrix
.copy(IDENTITY_COLOR_MATRIX)
Expand Down
1 change: 0 additions & 1 deletion packages/examples/src/examples/platformer/createGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export const createGame = async () => {
parent: "screen",
scaleMethod: "flex-width",
renderer: video.AUTO,
preferWebGL1: false,
subPixel: false,
highPrecisionShader: false,
});
Expand Down
10 changes: 4 additions & 6 deletions packages/examples/src/examples/platformer/play.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
plugin,
Stage,
VignetteEffect,
WebGLRenderer,
} from "melonjs";
import { VirtualJoypad } from "./entities/controls";
import UIContainer from "./entities/HUD";
Expand Down Expand Up @@ -52,11 +51,10 @@ export class PlayScreen extends Stage {
}

// vignette post-effect + built-in color grading (always applied last).
// VignetteEffect is WebGL-only; on the Canvas renderer just skip it
// and let the color grading still apply.
if (app.renderer instanceof WebGLRenderer) {
app.viewport.addPostEffect(new VignetteEffect(app.renderer));
}
// No backend check needed: on a renderer without a programmable
// pipeline (Canvas) the effect self-disables and the scene renders
// without it, while the color grading still applies.
app.viewport.addPostEffect(new VignetteEffect(app.renderer));
app.viewport.colorMatrix.contrast(1.1).saturate(1.1);

// play some music
Expand Down
1 change: 0 additions & 1 deletion packages/examples/src/examples/plinko-planck/createGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ export const createGame = async () => {
scaleMethod: "fit",
scaleTarget,
renderer: video.AUTO,
preferWebGL1: false,
subPixel: false,
highPrecisionShader: false,
// Anti-aliasing smooths every procedural draw call — the peg
Expand Down
1 change: 0 additions & 1 deletion packages/examples/src/examples/pool-matter/createGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export const createGame = async () => {
parent: "screen",
scaleMethod: "fit",
renderer: video.AUTO,
preferWebGL1: false,
subPixel: false,
highPrecisionShader: false,
// Anti-aliasing smooths the painted edges on the table sprite,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ const createGame = async () => {
const app = new Application(1024, 840, {
parent: "screen",
renderer: video.WEBGL,
preferWebGL1: false,
blendMode: "normal",
});
await app.init();
Expand Down
Loading