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>
147 lines
5.8 KiB
JavaScript
147 lines
5.8 KiB
JavaScript
/**
|
||
* 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;
|
||
`);
|
||
}
|