Files
feuerwerk/tools/e2e/63-render-layers.mjs
T
adminandClaude Opus 5.5 b2326085ed
game-maker / gate (push) Successful in 3m2s
game-maker / suite (push) Successful in 14m26s
Show each camera only its layers and lay overlay cameras on top (#302)
A camera could only show the whole scene, so a HUD drawn in the world
would run through a camera shader (#299) with everything else. Render
layers say which cameras see an object, and overlay cameras draw their
own picture over the main one.

- The project has a list of up to 16 render layers, edited in Project
  settings next to the collision layers. It is a list of its own, read
  by the same rules (`layerNamesOf`).
- Sprites, texts, tilemaps, particle sources and containers carry a row
  `Render layer` under their switches. Its default `Inherit` takes the
  parent's layer, at the top the first one.
- A camera has `Sees` (all layers by default) and `Overlay`. The main
  camera is the first one switched on without Overlay, so two plain
  cameras still draw through the first one only.
- Every view through a camera keeps only what that camera sees and lays
  one see-through view per overlay camera over itself. That view is a
  `WorldViewport` of its own, handed its view and frame from below.
- An overlay is fitted into the main camera's whole picture without
  distortion. One of the same size and zoom covers it exactly.
- `z` sorts within one camera only. The pictures lie on top of each
  other as a whole, which is what a shader per camera (#299) needs.
- The Game tab's preview now collects the scene once per redraw. Before
  it collected it twice, with the cameras it would have been five times.
- `SetCollisionLayersCommand` became `SetLayerNamesCommand`, which
  takes the list it sets.

Follow-ups: #304 (the mouse through a chosen camera) and #305 (change
the render layer at run time).

Tests:
- Gate: 226 files, 3487 tests (3480 + 7)
- e2e 63 (new): 4 checks. A green overlay sprite at z -10 covers a
  red main sprite at z 1000, in the preview and in the running game.
  A `UI` sprite in front of the main camera and a `Default` one in
  front of the overlay stay away. Without Overlay the second camera
  draws nothing. The settings dialog adds a render layer at the end.
- Counter-checks: inheriting from parents switched off fails 1 unit
  test. Overlays never drawn fails the two overlay checks in e2e 63.
- e2e total 1720 (1716 + 4), computed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:32:52 +02:00

147 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Render layers and overlay cameras (#302). The main camera sees only `Default`, an overlay camera
* 2000 units to the right sees only `UI`. Each draws its own picture, the overlay's laid on top:
* a green sprite of the overlay with z -10 covers a red one of the main camera with z 1000, and
* what a camera does not see stays away – a yellow `UI` sprite in front of the main camera, a blue
* `Default` one in front of the overlay. Switched to no overlay, the second camera draws nothing.
*
* Everything it adds carries an `rl63` id and goes again at the end.
*/
import { MAKE_OBJECT } from './lib/scaffold.mjs';
const OVERLAY = 'rl63-cam';
export async function renderLayers(t) {
const { check, json, evaluate, mutate, sleep, shot, showTab, play, stop, clickText, press } = t;
await showTab('scene');
await mutate(`
const p = __gm.state.project;
const scene = __gm.state.activeScene;
const main = scene.objects.find((o) => o.id === 'o3');
window.__rl63 = { layers: p.renderLayers, overrides: structuredClone(main.overrides) };
p.renderLayers = ['Default', 'UI'];
main.overrides = { ...main.overrides, sees: 0b01 };
${MAKE_OBJECT}
const sprite = p.assets.find((a) => a.type === 'sprite');
const square = (id, name, dx, z, size, color, layer, from) => {
const o = make(id, sprite.id, '', 0, name);
Object.assign(o.position, { x: from.x + dx, y: from.y, z });
o.overrides = { width: size, height: size, color, texture: '', shader: '', renderLayer: layer };
return o;
};
const overlay = structuredClone(main);
Object.assign(overlay, { id: '${OVERLAY}', name: 'Overlay 63', instanceEdits: {} });
overlay.position = { ...main.position, x: main.position.x + 2000 };
overlay.overrides = { ...main.overrides, overlay: true, sees: 0b10 };
scene.objects.push(
overlay,
square('rl63-a', 'A 63', 70, 1000, 60, 0xff0000, -1, main.position),
square('rl63-d', 'D 63', -70, 1000, 60, 0xffff00, 1, main.position),
square('rl63-b', 'B 63', 0, -10, 40, 0x00ff00, 1, overlay.position),
square('rl63-c', 'C 63', 70, 1000, 60, 0x0000ff, 0, overlay.position),
);
`);
await evaluate('__gm.state.select(null)');
await showTab('game');
await sleep(300);
const channels = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
const is = (hex, [r, g, b]) => {
const [pr, pg, pb] = channels(hex);
return Math.abs(pr - r) < 30 && Math.abs(pg - g) < 30 && Math.abs(pb - b) < 30;
};
/** The three points of the picture the squares sit on: the centre and 70 units either side. */
const picture = async () => {
const { frame, scale } = await json('__gm.gameScale()');
const cx = Math.round(frame.x + frame.w / 2);
const cy = Math.round(frame.y + frame.h / 2);
const at = (dx) => json(`__gm.pixelAt(${Math.round(cx + dx * scale)}, ${cy})`);
return {
centre: await at(0),
right: await at(70),
left: await at(-70),
main: await json('__gm.gameDrawOrder()'),
overlays: await json('__gm.gameOverlays()'),
};
};
const GREEN = [0, 255, 0];
const RED = [255, 0, 0];
const YELLOW = [255, 255, 0];
const stacked = (seen) =>
seen.overlays.length === 1 &&
seen.overlays[0].camera === OVERLAY &&
seen.overlays[0].drawOrder.includes('rl63-b') &&
!seen.overlays[0].drawOrder.includes('rl63-c') &&
seen.main.includes('rl63-a') &&
!seen.main.includes('rl63-d') &&
!seen.main.includes('rl63-b') &&
is(seen.centre, GREEN) &&
is(seen.right, RED) &&
!is(seen.left, YELLOW);
// 63a. The preview of the Game tab: each camera draws what it sees, the overlay on top.
const preview = await picture();
await shot('63-overlay-preview');
check(
'each camera draws only its layers, and the overlay lies on top whatever the z',
stacked(preview),
JSON.stringify(preview),
);
// 63b. Without Overlay the second camera is a plain one after the first: it draws nothing.
await mutate(`
const cam = __gm.state.activeScene.objects.find((o) => o.id === '${OVERLAY}');
cam.overrides = { ...cam.overrides, overlay: false };
`);
await sleep(200);
const plain = await picture();
check(
'two plain cameras switched on draw through the first one only',
plain.overlays.length === 0 && !is(plain.centre, GREEN) && is(plain.right, RED),
JSON.stringify(plain),
);
await mutate(`
const cam = __gm.state.activeScene.objects.find((o) => o.id === '${OVERLAY}');
cam.overrides = { ...cam.overrides, overlay: true };
`);
// 63c. The running game stacks them the same way.
await play();
await sleep(400);
const running = await picture();
check(
'the running game lays the overlay camera on top the same way',
stacked(running),
JSON.stringify(running),
);
await stop();
// 63d. The project settings list the render layers, apart from the collision layers.
await t.clickToolbar('menu');
await clickText('Project settings…');
const opened = await json('__gm.renderLayers()');
await clickText('Add render layer');
const added = await json('__gm.renderLayers()');
const collision = await json('__gm.collisionLayers()');
await press('Escape');
await sleep(150);
check(
'the project settings list the render layers and add one at the end',
JSON.stringify(opened.dialog) === JSON.stringify(['Default', 'UI']) &&
JSON.stringify(added.dialog) === JSON.stringify(['Default', 'UI', 'Layer 2']) &&
!collision.dialog.includes('UI'),
JSON.stringify({ opened, added, collision }),
);
// Clean up.
await showTab('scene');
await mutate(`
const p = __gm.state.project;
const scene = __gm.state.activeScene;
scene.objects = scene.objects.filter((o) => !o.id.startsWith('rl63-'));
scene.objects.find((o) => o.id === 'o3').overrides = window.__rl63.overrides;
p.renderLayers = window.__rl63.layers;
`);
}