@waica/engine 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -1
- package/dist/anchored-pieces.d.ts +3 -2
- package/dist/anchored-pieces.js +12 -8
- package/dist/archetype.d.ts +2 -2
- package/dist/assets/asset-loader.d.ts +28 -5
- package/dist/assets/asset-loader.js +63 -7
- package/dist/assets/model-backend.d.ts +21 -0
- package/dist/assets/model-backend.js +21 -0
- package/dist/assets/model-cache.d.ts +47 -0
- package/dist/assets/model-cache.js +117 -0
- package/dist/audio/audio-subsystem.d.ts +6 -1
- package/dist/audio/audio-subsystem.js +8 -15
- package/dist/audio/placement.d.ts +41 -0
- package/dist/audio/placement.js +21 -0
- package/dist/audio/spatial.d.ts +20 -0
- package/dist/audio/spatial.js +26 -0
- package/dist/audio/types.d.ts +7 -1
- package/dist/camera-projection.d.ts +38 -0
- package/dist/camera-projection.js +44 -0
- package/dist/camera-rig.d.ts +48 -0
- package/dist/camera-rig.js +98 -0
- package/dist/camera.d.ts +20 -3
- package/dist/camera.js +3 -1
- package/dist/component.d.ts +38 -2
- package/dist/component.js +3 -0
- package/dist/components/animated-sprite.d.ts +8 -1
- package/dist/components/animated-sprite.js +16 -1
- package/dist/components/collider.d.ts +89 -0
- package/dist/components/collider.js +57 -0
- package/dist/components/dynamic-body.d.ts +2 -1
- package/dist/components/dynamic-body.js +1 -0
- package/dist/components/hitbox.d.ts +2 -1
- package/dist/components/hitbox.js +1 -0
- package/dist/components/light.d.ts +69 -0
- package/dist/components/light.js +60 -0
- package/dist/components/model.d.ts +63 -0
- package/dist/components/model.js +117 -0
- package/dist/components/particle-emitter.d.ts +8 -1
- package/dist/components/particle-emitter.js +14 -0
- package/dist/components/point-light.d.ts +75 -0
- package/dist/components/point-light.js +102 -0
- package/dist/components/rigid-body.d.ts +114 -0
- package/dist/components/rigid-body.js +83 -0
- package/dist/components/solid.d.ts +2 -1
- package/dist/components/solid.js +1 -0
- package/dist/components/sprite.d.ts +8 -1
- package/dist/components/sprite.js +16 -1
- package/dist/components/sun.d.ts +46 -0
- package/dist/components/sun.js +74 -0
- package/dist/components/tilemap.d.ts +2 -1
- package/dist/components/tilemap.js +7 -0
- package/dist/entity-transform.d.ts +18 -0
- package/dist/entity-transform.js +17 -0
- package/dist/frame-quad.d.ts +21 -0
- package/dist/frame-quad.js +50 -0
- package/dist/game.d.ts +59 -15
- package/dist/game.js +100 -75
- package/dist/index.d.ts +39 -4
- package/dist/index.js +18 -1
- package/dist/light-field.d.ts +78 -0
- package/dist/light-field.js +145 -0
- package/dist/light-map-material.d.ts +40 -0
- package/dist/light-map-material.js +203 -0
- package/dist/light-map.d.ts +46 -0
- package/dist/light-map.js +132 -0
- package/dist/lit-frame.d.ts +48 -0
- package/dist/lit-frame.js +174 -0
- package/dist/occluder-grid.d.ts +19 -0
- package/dist/occluder-grid.js +50 -0
- package/dist/particle-batch.js +3 -2
- package/dist/physics-3d/body-contacts.d.ts +10 -0
- package/dist/physics-3d/body-contacts.js +59 -0
- package/dist/physics-3d/body-sync.d.ts +52 -0
- package/dist/physics-3d/body-sync.js +153 -0
- package/dist/physics-3d/character-controller.d.ts +44 -0
- package/dist/physics-3d/character-controller.js +126 -0
- package/dist/physics-3d/physics-host.d.ts +86 -0
- package/dist/physics-3d/physics-host.js +174 -0
- package/dist/physics-3d/physics-world.d.ts +65 -0
- package/dist/physics-3d/physics-world.js +158 -0
- package/dist/physics-3d/rapier-module.d.ts +17 -0
- package/dist/physics-3d/rapier-module.js +19 -0
- package/dist/physics-3d/sensor-dispatch.d.ts +10 -0
- package/dist/physics-3d/sensor-dispatch.js +77 -0
- package/dist/physics-3d/world-queries.d.ts +39 -0
- package/dist/physics-3d/world-queries.js +63 -0
- package/dist/pointer-raycast.d.ts +17 -0
- package/dist/pointer-raycast.js +48 -0
- package/dist/pointer.d.ts +27 -6
- package/dist/pointer.js +43 -17
- package/dist/post-effects.d.ts +33 -0
- package/dist/post-effects.js +48 -0
- package/dist/post-pass.d.ts +23 -0
- package/dist/post-pass.js +93 -0
- package/dist/render-layers.d.ts +18 -0
- package/dist/render-layers.js +19 -0
- package/dist/render-output.d.ts +7 -0
- package/dist/render-output.js +17 -1
- package/dist/runtime-bridge.d.ts +1 -1
- package/dist/runtime-inspection.d.ts +14 -0
- package/dist/runtime-inspection.js +12 -2
- package/dist/runtime-lighting-snapshot.d.ts +67 -0
- package/dist/runtime-lighting-snapshot.js +77 -0
- package/dist/runtime-physics-snapshot.d.ts +41 -0
- package/dist/runtime-physics-snapshot.js +45 -0
- package/dist/runtime-view-snapshot.d.ts +25 -0
- package/dist/runtime-view-snapshot.js +30 -0
- package/dist/scene-camera-3d.d.ts +66 -0
- package/dist/scene-camera-3d.js +117 -0
- package/dist/scene-lighting.d.ts +52 -0
- package/dist/scene-lighting.js +107 -0
- package/dist/scene-lights-3d.d.ts +17 -0
- package/dist/scene-lights-3d.js +27 -0
- package/dist/scene-render-options.d.ts +51 -0
- package/dist/scene-render-options.js +126 -0
- package/dist/scene-simulation.d.ts +22 -0
- package/dist/scene-simulation.js +29 -0
- package/dist/scene-space.d.ts +50 -0
- package/dist/scene-space.js +103 -0
- package/dist/scene.d.ts +41 -2
- package/dist/scene.js +11 -4
- package/dist/simulation-phases.d.ts +9 -0
- package/dist/simulation-phases.js +19 -0
- package/dist/spatial-query-3d.d.ts +48 -0
- package/dist/spatial-query-3d.js +96 -0
- package/dist/spatial-query.d.ts +18 -4
- package/dist/spatial-query.js +57 -1
- package/dist/sprite-batch.js +5 -1
- package/package.json +2 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
|
+
import { FrameQuad } from './frame-quad.js';
|
|
3
|
+
import { lightFootprint } from './light-field.js';
|
|
4
|
+
import { createLightMapUniforms, createLightMaterial, createMultiplyMaterial, setLightDraw, } from './light-map-material.js';
|
|
5
|
+
import { projectIsometric } from './projection.js';
|
|
6
|
+
function emptyOccluders() {
|
|
7
|
+
const texture = new THREE.DataTexture(new Uint8Array([0]), 1, 1, THREE.RedFormat, THREE.UnsignedByteType);
|
|
8
|
+
texture.needsUpdate = true;
|
|
9
|
+
return texture;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The scene's light-map (issue #78, ADR 0026): an 8-bit target (inference
|
|
13
|
+
* 16) the size of the internal resolution — or of the drawing buffer — with
|
|
14
|
+
* nearest upscale, cleared to the Ambient Light, onto which each live Light
|
|
15
|
+
* adds its term through one mesh over its footprint. Then multiplied over
|
|
16
|
+
* the frame by one quad.
|
|
17
|
+
*/
|
|
18
|
+
export class LightMap {
|
|
19
|
+
target = new THREE.RenderTarget(1, 1, {
|
|
20
|
+
type: THREE.UnsignedByteType,
|
|
21
|
+
minFilter: THREE.NearestFilter,
|
|
22
|
+
magFilter: THREE.NearestFilter,
|
|
23
|
+
depthBuffer: false,
|
|
24
|
+
});
|
|
25
|
+
scene = new THREE.Scene();
|
|
26
|
+
geometry = new THREE.PlaneGeometry(1, 1);
|
|
27
|
+
empty = emptyOccluders();
|
|
28
|
+
uniforms = createLightMapUniforms(this.empty);
|
|
29
|
+
material = createLightMaterial(this.uniforms);
|
|
30
|
+
meshes = new Map();
|
|
31
|
+
multiply = new FrameQuad(createMultiplyMaterial(this.target.texture));
|
|
32
|
+
occluderTexture = null;
|
|
33
|
+
builtRevision = '';
|
|
34
|
+
clear = new THREE.Color();
|
|
35
|
+
savedClear = new THREE.Color();
|
|
36
|
+
/** Draws this frame's light-map into its target and leaves the renderer bound to `previous`. */
|
|
37
|
+
render(renderer, camera, frame) {
|
|
38
|
+
if (this.target.width !== frame.width || this.target.height !== frame.height) {
|
|
39
|
+
this.target.setSize(frame.width, frame.height);
|
|
40
|
+
}
|
|
41
|
+
this.syncOccluders(frame);
|
|
42
|
+
this.syncLights(frame);
|
|
43
|
+
const previous = renderer.getRenderTarget();
|
|
44
|
+
const alpha = renderer.getClearAlpha();
|
|
45
|
+
renderer.getClearColor(this.savedClear);
|
|
46
|
+
this.clear.setRGB(frame.ambient[0], frame.ambient[1], frame.ambient[2], THREE.LinearSRGBColorSpace);
|
|
47
|
+
renderer.setClearColor(this.clear, 1);
|
|
48
|
+
renderer.setRenderTarget(this.target);
|
|
49
|
+
renderer.render(this.scene, camera);
|
|
50
|
+
renderer.setRenderTarget(previous);
|
|
51
|
+
renderer.setClearColor(this.savedClear, alpha);
|
|
52
|
+
}
|
|
53
|
+
/** Multiplies the light-map over what is bound now: the canvas, or a Post Effect's canvas-like target. */
|
|
54
|
+
multiplyOver(renderer) {
|
|
55
|
+
this.multiply.render(renderer);
|
|
56
|
+
}
|
|
57
|
+
dispose() {
|
|
58
|
+
this.target.dispose();
|
|
59
|
+
this.geometry.dispose();
|
|
60
|
+
this.material.dispose();
|
|
61
|
+
this.multiply.dispose();
|
|
62
|
+
this.empty.dispose();
|
|
63
|
+
this.occluderTexture?.dispose();
|
|
64
|
+
this.meshes.clear();
|
|
65
|
+
}
|
|
66
|
+
syncOccluders(frame) {
|
|
67
|
+
if (frame.occluderRevision === this.builtRevision)
|
|
68
|
+
return;
|
|
69
|
+
this.builtRevision = frame.occluderRevision;
|
|
70
|
+
this.occluderTexture?.dispose();
|
|
71
|
+
this.occluderTexture = null;
|
|
72
|
+
const grid = frame.occluders();
|
|
73
|
+
if (!grid) {
|
|
74
|
+
this.uniforms.occluders.value = this.empty;
|
|
75
|
+
this.uniforms.grid.value.set(0, 0, 1, 0);
|
|
76
|
+
this.uniforms.gridSize.value.set(1, 1);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const bytes = grid.solid.map((value) => (value === 1 ? 255 : 0));
|
|
80
|
+
const texture = new THREE.DataTexture(bytes, grid.columns, grid.rows, THREE.RedFormat, THREE.UnsignedByteType);
|
|
81
|
+
texture.needsUpdate = true;
|
|
82
|
+
this.occluderTexture = texture;
|
|
83
|
+
this.uniforms.occluders.value = texture;
|
|
84
|
+
this.uniforms.grid.value.set(grid.originX, grid.originY, grid.cellSize, 1);
|
|
85
|
+
this.uniforms.gridSize.value.set(grid.columns, grid.rows);
|
|
86
|
+
}
|
|
87
|
+
/** One mesh per live Light, placed over its footprint; a destroyed Light's mesh leaves now. */
|
|
88
|
+
syncLights(frame) {
|
|
89
|
+
this.uniforms.isometric.value = frame.projection === 'isometric' ? 1 : 0;
|
|
90
|
+
const live = new Set(frame.lights);
|
|
91
|
+
for (const [light, entry] of this.meshes) {
|
|
92
|
+
if (live.has(light))
|
|
93
|
+
continue;
|
|
94
|
+
entry.mesh.removeFromParent();
|
|
95
|
+
this.meshes.delete(light);
|
|
96
|
+
}
|
|
97
|
+
for (const light of frame.lights)
|
|
98
|
+
this.place(light, frame.projection);
|
|
99
|
+
}
|
|
100
|
+
place(light, projection) {
|
|
101
|
+
const entry = this.meshes.get(light) ?? this.createMesh(light);
|
|
102
|
+
const field = light.field();
|
|
103
|
+
entry.draw.position.set(field.x, field.y);
|
|
104
|
+
entry.draw.color.set(field.color[0], field.color[1], field.color[2]).multiplyScalar(field.intensity);
|
|
105
|
+
entry.draw.radius = field.radius;
|
|
106
|
+
entry.draw.bands = field.bands;
|
|
107
|
+
entry.draw.softness = field.softness;
|
|
108
|
+
entry.draw.castShadows = field.castShadows ? 1 : 0;
|
|
109
|
+
const center = projection === 'isometric' ? projectIsometric(field.x, field.y) : { x: field.x, y: field.y };
|
|
110
|
+
const footprint = lightFootprint(field.radius, projection);
|
|
111
|
+
entry.mesh.position.set(center.x, center.y, 0);
|
|
112
|
+
entry.mesh.scale.set(Math.max(footprint.width, 1e-6), Math.max(footprint.height, 1e-6), 1);
|
|
113
|
+
entry.mesh.visible = field.radius > 0 && field.intensity > 0;
|
|
114
|
+
}
|
|
115
|
+
createMesh(light) {
|
|
116
|
+
const mesh = new THREE.Mesh(this.geometry, this.material);
|
|
117
|
+
mesh.frustumCulled = false;
|
|
118
|
+
const draw = {
|
|
119
|
+
position: new THREE.Vector2(),
|
|
120
|
+
color: new THREE.Vector3(),
|
|
121
|
+
radius: 0,
|
|
122
|
+
bands: 0,
|
|
123
|
+
softness: 0,
|
|
124
|
+
castShadows: 1,
|
|
125
|
+
};
|
|
126
|
+
setLightDraw(mesh, draw);
|
|
127
|
+
this.scene.add(mesh);
|
|
128
|
+
const entry = { mesh, draw };
|
|
129
|
+
this.meshes.set(light, entry);
|
|
130
|
+
return entry;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
|
+
import type { Game, GameResolution } from './game.js';
|
|
3
|
+
import type { SpriteBatches } from './sprite-batches.js';
|
|
4
|
+
/** What a frame is drawn with besides the Game's public state: its renderer and Sprite Batches. */
|
|
5
|
+
export interface FrameSurface {
|
|
6
|
+
readonly game: Game;
|
|
7
|
+
readonly renderer: THREE.WebGPURenderer;
|
|
8
|
+
readonly spriteBatches: SpriteBatches;
|
|
9
|
+
/** The fixed internal resolution, null to fill the canvas. */
|
|
10
|
+
readonly resolution: GameResolution | null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* What the occluder grid is built from: the solid tiles' revision and every
|
|
14
|
+
* Tilemap's origin, so moving a Tilemap rebuilds it too (review) — still
|
|
15
|
+
* never per frame while nothing changes.
|
|
16
|
+
*/
|
|
17
|
+
export declare function occluderSignature(game: Game): string;
|
|
18
|
+
/**
|
|
19
|
+
* Draws one frame (issue #78, ADR 0026). Off path (B4): an unlit scene with
|
|
20
|
+
* no Post Effect is one render straight into the canvas, exactly as before,
|
|
21
|
+
* and nothing here is ever built. Lit: the scene without its Emissive
|
|
22
|
+
* drawables, then the light-map multiplied over it, then the Emissive
|
|
23
|
+
* drawables. With a Post Effect, all of that goes into a target drawn like the
|
|
24
|
+
* canvas and
|
|
25
|
+
* one pass writes it to the canvas.
|
|
26
|
+
*/
|
|
27
|
+
export declare class FrameComposer {
|
|
28
|
+
private readonly surface;
|
|
29
|
+
private lightMap;
|
|
30
|
+
private postPass;
|
|
31
|
+
private readonly size;
|
|
32
|
+
constructor(surface: FrameSurface);
|
|
33
|
+
draw(): void;
|
|
34
|
+
dispose(): void;
|
|
35
|
+
/** The light-map's (and the Post Effects') size: the internal resolution, else the drawing buffer (B2). */
|
|
36
|
+
private frameSize;
|
|
37
|
+
/** Letterbox bars: clear the whole canvas, then render inside the scissor. */
|
|
38
|
+
private clearLetterbox;
|
|
39
|
+
/** The scene through its Sprite Batches (ADR 0024), for the camera's current layers. */
|
|
40
|
+
private drawScene;
|
|
41
|
+
/** The scene for one layer mask, restoring the camera's own mask after. */
|
|
42
|
+
private drawLayers;
|
|
43
|
+
/** Scene, light-map, Emissive drawables, into whatever target is bound now. */
|
|
44
|
+
private drawLit;
|
|
45
|
+
private composeLight;
|
|
46
|
+
/** The frame into the Post Effects' canvas-like target, then one pass to the canvas (CA-11). */
|
|
47
|
+
private drawThroughPost;
|
|
48
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
|
+
import { Tilemap } from './components/tilemap.js';
|
|
3
|
+
import { LightMap } from './light-map.js';
|
|
4
|
+
import { buildOccluderGrid } from './occluder-grid.js';
|
|
5
|
+
import { PostPass } from './post-pass.js';
|
|
6
|
+
import { EMISSIVE_LAYER } from './render-layers.js';
|
|
7
|
+
import { ambientIsFull, ambientMultiplier, occluderRevision } from './scene-lighting.js';
|
|
8
|
+
/** Layer 0 alone: every drawable that is not Emissive. */
|
|
9
|
+
const LIT_MASK = 1;
|
|
10
|
+
const EMISSIVE_MASK = 1 << EMISSIVE_LAYER;
|
|
11
|
+
/**
|
|
12
|
+
* What the occluder grid is built from: the solid tiles' revision and every
|
|
13
|
+
* Tilemap's origin, so moving a Tilemap rebuilds it too (review) — still
|
|
14
|
+
* never per frame while nothing changes.
|
|
15
|
+
*/
|
|
16
|
+
export function occluderSignature(game) {
|
|
17
|
+
const origins = occluderSources(game.entities).map(({ originX, originY }) => `${originX},${originY}`);
|
|
18
|
+
return `${occluderRevision(game.lighting)}|${origins.join(';')}`;
|
|
19
|
+
}
|
|
20
|
+
/** Every Tilemap of the scene as an occluder source (inference 8). */
|
|
21
|
+
function occluderSources(entities) {
|
|
22
|
+
const sources = [];
|
|
23
|
+
for (const entity of entities) {
|
|
24
|
+
for (const component of entity.components) {
|
|
25
|
+
if (!(component instanceof Tilemap))
|
|
26
|
+
continue;
|
|
27
|
+
sources.push({
|
|
28
|
+
originX: entity.position.x,
|
|
29
|
+
originY: entity.position.y,
|
|
30
|
+
cellSize: component.cellSize,
|
|
31
|
+
mapWidth: component.mapWidth,
|
|
32
|
+
mapHeight: component.mapHeight,
|
|
33
|
+
cells: component.cells,
|
|
34
|
+
solidTiles: component.solidTiles,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return sources;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Draws one frame (issue #78, ADR 0026). Off path (B4): an unlit scene with
|
|
42
|
+
* no Post Effect is one render straight into the canvas, exactly as before,
|
|
43
|
+
* and nothing here is ever built. Lit: the scene without its Emissive
|
|
44
|
+
* drawables, then the light-map multiplied over it, then the Emissive
|
|
45
|
+
* drawables. With a Post Effect, all of that goes into a target drawn like the
|
|
46
|
+
* canvas and
|
|
47
|
+
* one pass writes it to the canvas.
|
|
48
|
+
*/
|
|
49
|
+
export class FrameComposer {
|
|
50
|
+
surface;
|
|
51
|
+
lightMap = null;
|
|
52
|
+
postPass = null;
|
|
53
|
+
size = new THREE.Vector2();
|
|
54
|
+
constructor(surface) {
|
|
55
|
+
this.surface = surface;
|
|
56
|
+
}
|
|
57
|
+
draw() {
|
|
58
|
+
// Full Ambient Light: the light-map would be 1 everywhere, so nothing to multiply.
|
|
59
|
+
// A 3D scene never builds one: its lights are three's own (ADR 0027).
|
|
60
|
+
const { game } = this.surface;
|
|
61
|
+
const lit = game.space === '2d' && game.lighting.active && !ambientIsFull(game.lighting);
|
|
62
|
+
const post = game.post.active;
|
|
63
|
+
if (!lit && !post) {
|
|
64
|
+
this.clearLetterbox();
|
|
65
|
+
this.drawScene();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const { width, height } = this.frameSize();
|
|
69
|
+
if (!post) {
|
|
70
|
+
this.clearLetterbox();
|
|
71
|
+
this.drawLit(width, height);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
this.drawThroughPost(width, height, lit);
|
|
75
|
+
}
|
|
76
|
+
dispose() {
|
|
77
|
+
this.lightMap?.dispose();
|
|
78
|
+
this.lightMap = null;
|
|
79
|
+
this.postPass?.dispose();
|
|
80
|
+
this.postPass = null;
|
|
81
|
+
}
|
|
82
|
+
/** The light-map's (and the Post Effects') size: the internal resolution, else the drawing buffer (B2). */
|
|
83
|
+
frameSize() {
|
|
84
|
+
const { renderer, resolution } = this.surface;
|
|
85
|
+
if (resolution)
|
|
86
|
+
return { width: Math.max(1, Math.round(resolution.width)), height: Math.max(1, Math.round(resolution.height)) };
|
|
87
|
+
renderer.getDrawingBufferSize(this.size);
|
|
88
|
+
return { width: Math.max(1, this.size.x), height: Math.max(1, this.size.y) };
|
|
89
|
+
}
|
|
90
|
+
/** Letterbox bars: clear the whole canvas, then render inside the scissor. */
|
|
91
|
+
clearLetterbox() {
|
|
92
|
+
const { renderer, resolution } = this.surface;
|
|
93
|
+
if (!resolution)
|
|
94
|
+
return;
|
|
95
|
+
renderer.setScissorTest(false);
|
|
96
|
+
renderer.setClearColor(0x000000, 1);
|
|
97
|
+
renderer.clear(true, false, false);
|
|
98
|
+
renderer.setScissorTest(true);
|
|
99
|
+
}
|
|
100
|
+
/** The scene through its Sprite Batches (ADR 0024), for the camera's current layers. */
|
|
101
|
+
drawScene() {
|
|
102
|
+
const { spriteBatches, renderer } = this.surface;
|
|
103
|
+
const { scene, camera, space } = this.surface.game;
|
|
104
|
+
// A 3D scene has no sprites to batch: it draws straight, the depth buffer ordering it.
|
|
105
|
+
if (space === '3d') {
|
|
106
|
+
renderer.render(scene, camera);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
spriteBatches.drawFrame(scene, camera, () => renderer.render(scene, camera));
|
|
110
|
+
}
|
|
111
|
+
/** The scene for one layer mask, restoring the camera's own mask after. */
|
|
112
|
+
drawLayers(mask) {
|
|
113
|
+
const layers = this.surface.game.camera.layers;
|
|
114
|
+
const own = layers.mask;
|
|
115
|
+
layers.mask = mask;
|
|
116
|
+
try {
|
|
117
|
+
this.drawScene();
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
layers.mask = own;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Scene, light-map, Emissive drawables, into whatever target is bound now. */
|
|
124
|
+
drawLit(width, height) {
|
|
125
|
+
this.drawLayers(LIT_MASK);
|
|
126
|
+
this.composeLight(width, height);
|
|
127
|
+
}
|
|
128
|
+
composeLight(width, height) {
|
|
129
|
+
const { renderer } = this.surface;
|
|
130
|
+
const { scene, camera, lighting } = this.surface.game;
|
|
131
|
+
this.lightMap ??= new LightMap();
|
|
132
|
+
this.lightMap.render(renderer, camera, {
|
|
133
|
+
width,
|
|
134
|
+
height,
|
|
135
|
+
ambient: ambientMultiplier(lighting),
|
|
136
|
+
lights: lighting.lights,
|
|
137
|
+
projection: this.surface.game.projection,
|
|
138
|
+
occluderRevision: occluderSignature(this.surface.game),
|
|
139
|
+
occluders: () => buildOccluderGrid(occluderSources(this.surface.game.entities)),
|
|
140
|
+
});
|
|
141
|
+
const autoClear = renderer.autoClear;
|
|
142
|
+
const background = scene.background;
|
|
143
|
+
renderer.autoClear = false;
|
|
144
|
+
scene.background = null;
|
|
145
|
+
try {
|
|
146
|
+
this.lightMap.multiplyOver(renderer);
|
|
147
|
+
this.drawLayers(EMISSIVE_MASK);
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
renderer.autoClear = autoClear;
|
|
151
|
+
scene.background = background;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** The frame into the Post Effects' canvas-like target, then one pass to the canvas (CA-11). */
|
|
155
|
+
drawThroughPost(width, height, lit) {
|
|
156
|
+
const { renderer } = this.surface;
|
|
157
|
+
const { post } = this.surface.game;
|
|
158
|
+
this.postPass ??= new PostPass();
|
|
159
|
+
this.postPass.resize(width, height);
|
|
160
|
+
// The target stores what the canvas would, so the scene's own (canvas) background clears it.
|
|
161
|
+
renderer.setRenderTarget(this.postPass.target);
|
|
162
|
+
try {
|
|
163
|
+
if (lit)
|
|
164
|
+
this.drawLit(width, height);
|
|
165
|
+
else
|
|
166
|
+
this.drawScene();
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
renderer.setRenderTarget(null);
|
|
170
|
+
}
|
|
171
|
+
this.clearLetterbox();
|
|
172
|
+
this.postPass.draw(renderer, { vignette: post.vignette, colorGrade: post.colorGrade });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { OccluderGrid } from './light-field.js';
|
|
2
|
+
/** What a Tilemap contributes to occlusion: its logical grid and which of its tiles are solid. */
|
|
3
|
+
export interface OccluderSource {
|
|
4
|
+
originX: number;
|
|
5
|
+
originY: number;
|
|
6
|
+
cellSize: number;
|
|
7
|
+
mapWidth: number;
|
|
8
|
+
mapHeight: number;
|
|
9
|
+
cells: readonly number[];
|
|
10
|
+
solidTiles: readonly number[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Every Tilemap's solid tiles rasterized onto one logical grid (inference 8):
|
|
14
|
+
* the finest cell size among them, spanning all their maps. With a single
|
|
15
|
+
* Tilemap — or several sharing a cell size and an aligned origin — each grid
|
|
16
|
+
* cell is exactly one tile. Null when no solid tile is placed anywhere: the
|
|
17
|
+
* lights then cast no shadow at all.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildOccluderGrid(sources: readonly OccluderSource[]): OccluderGrid | null;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Floating-point slack so a tile edge lying on a grid line covers no neighbouring cell. */
|
|
2
|
+
const EDGE = 1e-6;
|
|
3
|
+
function solidBoxes(source) {
|
|
4
|
+
const solid = new Set(source.solidTiles);
|
|
5
|
+
const width = Math.max(0, Math.floor(source.mapWidth));
|
|
6
|
+
const height = Math.max(0, Math.floor(source.mapHeight));
|
|
7
|
+
const size = source.cellSize;
|
|
8
|
+
const boxes = [];
|
|
9
|
+
if (!(size > 0) || solid.size === 0)
|
|
10
|
+
return boxes;
|
|
11
|
+
for (let index = 0; index < width * height; index += 1) {
|
|
12
|
+
if (!solid.has(source.cells[index] ?? -1))
|
|
13
|
+
continue;
|
|
14
|
+
const minX = source.originX + (index % width) * size;
|
|
15
|
+
const minY = source.originY + Math.floor(index / width) * size;
|
|
16
|
+
boxes.push({ minX, minY, maxX: minX + size, maxY: minY + size });
|
|
17
|
+
}
|
|
18
|
+
return boxes;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Every Tilemap's solid tiles rasterized onto one logical grid (inference 8):
|
|
22
|
+
* the finest cell size among them, spanning all their maps. With a single
|
|
23
|
+
* Tilemap — or several sharing a cell size and an aligned origin — each grid
|
|
24
|
+
* cell is exactly one tile. Null when no solid tile is placed anywhere: the
|
|
25
|
+
* lights then cast no shadow at all.
|
|
26
|
+
*/
|
|
27
|
+
export function buildOccluderGrid(sources) {
|
|
28
|
+
const contributing = sources.map((source) => ({ source, boxes: solidBoxes(source) })).filter(({ boxes }) => boxes.length > 0);
|
|
29
|
+
if (contributing.length === 0)
|
|
30
|
+
return null;
|
|
31
|
+
const cellSize = Math.min(...contributing.map(({ source }) => source.cellSize));
|
|
32
|
+
const originX = Math.min(...contributing.map(({ source }) => source.originX));
|
|
33
|
+
const originY = Math.min(...contributing.map(({ source }) => source.originY));
|
|
34
|
+
const right = Math.max(...contributing.map(({ source }) => source.originX + Math.floor(source.mapWidth) * source.cellSize));
|
|
35
|
+
const top = Math.max(...contributing.map(({ source }) => source.originY + Math.floor(source.mapHeight) * source.cellSize));
|
|
36
|
+
const columns = Math.max(1, Math.ceil((right - originX) / cellSize - EDGE));
|
|
37
|
+
const rows = Math.max(1, Math.ceil((top - originY) / cellSize - EDGE));
|
|
38
|
+
const solid = new Uint8Array(columns * rows);
|
|
39
|
+
for (const { boxes } of contributing) {
|
|
40
|
+
for (const box of boxes) {
|
|
41
|
+
const fromColumn = Math.max(0, Math.floor((box.minX - originX) / cellSize + EDGE));
|
|
42
|
+
const toColumn = Math.min(columns, Math.ceil((box.maxX - originX) / cellSize - EDGE));
|
|
43
|
+
const fromRow = Math.max(0, Math.floor((box.minY - originY) / cellSize + EDGE));
|
|
44
|
+
const toRow = Math.min(rows, Math.ceil((box.maxY - originY) / cellSize - EDGE));
|
|
45
|
+
for (let row = fromRow; row < toRow; row += 1)
|
|
46
|
+
solid.fill(1, row * columns + fromColumn, row * columns + toColumn);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { originX, originY, cellSize, columns, rows, solid };
|
|
50
|
+
}
|
package/dist/particle-batch.js
CHANGED
|
@@ -3,6 +3,7 @@ import { reportRejection } from './report-rejection.js';
|
|
|
3
3
|
import { ParticlePool, particleValueAt, } from './particle-pool.js';
|
|
4
4
|
import { projectIsometric } from './projection.js';
|
|
5
5
|
import { refreshParticleSortBounds } from './particle-sort-bounds.js';
|
|
6
|
+
import { TRANSPARENT_TEXEL_ALPHA } from './render-layers.js';
|
|
6
7
|
function createGeometry(capacity) {
|
|
7
8
|
const geometry = new THREE.BufferGeometry();
|
|
8
9
|
const uvs = new Float32Array(capacity * 8);
|
|
@@ -38,8 +39,8 @@ export class ParticleBatch {
|
|
|
38
39
|
this._layer = Number.isFinite(layer) ? layer : 0;
|
|
39
40
|
this.material = new THREE.MeshBasicMaterial({
|
|
40
41
|
color: 0xffffff,
|
|
41
|
-
|
|
42
|
-
vertexColors: true,
|
|
42
|
+
// alphaTest: see TRANSPARENT_TEXEL_ALPHA (an Emissive behind shows through transparent texels).
|
|
43
|
+
transparent: true, alphaTest: TRANSPARENT_TEXEL_ALPHA, vertexColors: true,
|
|
43
44
|
depthTest: true,
|
|
44
45
|
depthWrite: true,
|
|
45
46
|
blending: blend === 'additive' ? THREE.AdditiveBlending : THREE.NormalBlending,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Game } from '../game.js';
|
|
2
|
+
import type { PhysicsWorld } from './physics-world.js';
|
|
3
|
+
/**
|
|
4
|
+
* After the physics step (CA-15): every component on an entity with a solid
|
|
5
|
+
* Collider that implements `onBodyContact` hears about each other entity it
|
|
6
|
+
* touches, once per step, with the normal pointing from it to the other. The
|
|
7
|
+
* common scene, where no component listens, allocates nothing here: the
|
|
8
|
+
* narrow phase is read only for an entity that has a receiver.
|
|
9
|
+
*/
|
|
10
|
+
export declare function dispatchBodyContacts(game: Game, world: PhysicsWorld): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Adds zero so a negated zero reads as 0, not -0. */
|
|
2
|
+
const clean = (value) => value + 0;
|
|
3
|
+
/** The contact one Rapier manifold makes for `record` against `other`, or null when it holds no solver contact point. */
|
|
4
|
+
function manifoldContact(sides, manifold, flipped) {
|
|
5
|
+
const [record, other] = sides;
|
|
6
|
+
const point = manifold.numSolverContacts() > 0 ? manifold.solverContactPoint(0) : null;
|
|
7
|
+
if (!point)
|
|
8
|
+
return null;
|
|
9
|
+
// The manifold's normal points from its first shape to its second; `flipped` says that first shape is the other one, so it is negated to point from ours.
|
|
10
|
+
const n = manifold.normal();
|
|
11
|
+
const sign = flipped ? -1 : 1;
|
|
12
|
+
return {
|
|
13
|
+
entity: record.entity,
|
|
14
|
+
other: other.entity,
|
|
15
|
+
normal: { x: clean(sign * n.x), y: clean(sign * n.y), z: clean(sign * n.z) },
|
|
16
|
+
point: { x: point.x, y: point.y, z: point.z },
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** The solid contacts of one body this step, one per other entity, read from Rapier's narrow phase. */
|
|
20
|
+
function contactsOf(world, record) {
|
|
21
|
+
const contacts = new Map();
|
|
22
|
+
world.raw.contactPairsWith(record.shape, (shape) => {
|
|
23
|
+
const other = world.recordOfCollider(shape.handle);
|
|
24
|
+
if (!other || other === record || contacts.has(other))
|
|
25
|
+
return;
|
|
26
|
+
world.raw.contactPair(record.shape, shape, (manifold, flipped) => {
|
|
27
|
+
const contact = contacts.has(other) ? null : manifoldContact([record, other], manifold, flipped);
|
|
28
|
+
if (contact)
|
|
29
|
+
contacts.set(other, contact);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
return [...contacts.values()];
|
|
33
|
+
}
|
|
34
|
+
const listens = (component) => typeof component.onBodyContact === 'function';
|
|
35
|
+
function deliver(contacts, receivers) {
|
|
36
|
+
for (const contact of contacts) {
|
|
37
|
+
if (!contact.entity.alive || !contact.other.alive)
|
|
38
|
+
continue;
|
|
39
|
+
for (const component of receivers)
|
|
40
|
+
component.onBodyContact?.(contact);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* After the physics step (CA-15): every component on an entity with a solid
|
|
45
|
+
* Collider that implements `onBodyContact` hears about each other entity it
|
|
46
|
+
* touches, once per step, with the normal pointing from it to the other. The
|
|
47
|
+
* common scene, where no component listens, allocates nothing here: the
|
|
48
|
+
* narrow phase is read only for an entity that has a receiver.
|
|
49
|
+
*/
|
|
50
|
+
export function dispatchBodyContacts(game, world) {
|
|
51
|
+
for (const entity of game.entities) {
|
|
52
|
+
if (game.physics.world !== world)
|
|
53
|
+
return;
|
|
54
|
+
const record = world.recordOf(entity);
|
|
55
|
+
if (!record || record.sensor || !entity.alive || !entity.components.some(listens))
|
|
56
|
+
continue;
|
|
57
|
+
deliver(contactsOf(world, record), entity.components.filter(listens));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Collider } from '../components/collider.js';
|
|
2
|
+
import { RigidBody } from '../components/rigid-body.js';
|
|
3
|
+
import type { Entity } from '../entity.js';
|
|
4
|
+
import type { RapierBody, RapierCollider, RapierModule, RapierWorld } from './rapier-module.js';
|
|
5
|
+
/** Rapier's three body types, as the Runtime Snapshot names them. */
|
|
6
|
+
export type BodyKind = 'fixed' | 'dynamic' | 'kinematic';
|
|
7
|
+
/** What a body is built from: the entity and the Collider and RigidBody components it has. */
|
|
8
|
+
export interface BodyParts {
|
|
9
|
+
readonly entity: Entity;
|
|
10
|
+
readonly collider: Collider;
|
|
11
|
+
readonly rigid: RigidBody | null;
|
|
12
|
+
}
|
|
13
|
+
/** One entity's body in the world: its components and the Rapier handles built from them. */
|
|
14
|
+
export interface BodyRecord {
|
|
15
|
+
readonly entity: Entity;
|
|
16
|
+
readonly collider: Collider;
|
|
17
|
+
readonly rigid: RigidBody | null;
|
|
18
|
+
readonly kind: BodyKind;
|
|
19
|
+
/** Whether the Rapier collider was built as a sensor; frozen here because `Collider.sensor` is a plain field a script may flip later. */
|
|
20
|
+
readonly sensor: boolean;
|
|
21
|
+
readonly body: RapierBody;
|
|
22
|
+
readonly shape: RapierCollider;
|
|
23
|
+
/** Whether a kinematic body stands on something after the last step; written by the character controller. */
|
|
24
|
+
grounded: boolean;
|
|
25
|
+
/** A kinematic body's vertical velocity (units per second), integrated by the character controller. */
|
|
26
|
+
vy: number;
|
|
27
|
+
/** A kinematic body's movement over the last step, per second. */
|
|
28
|
+
velocity: {
|
|
29
|
+
x: number;
|
|
30
|
+
y: number;
|
|
31
|
+
z: number;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** Why this collider cannot become a Rapier shape, or null when it can. */
|
|
35
|
+
export declare function colliderProblem(collider: Collider): string | null;
|
|
36
|
+
/** Why this RigidBody cannot become a Rapier body, or null when it can (the mirror of `colliderProblem`). */
|
|
37
|
+
export declare function rigidBodyProblem(rigid: RigidBody): string | null;
|
|
38
|
+
/**
|
|
39
|
+
* Builds the entity's body and collider from its components and puts them in
|
|
40
|
+
* the world. Static bodies are created once and never follow the entity; a
|
|
41
|
+
* dynamic body is Rapier's from here on. The caller has checked the parts
|
|
42
|
+
* with `colliderProblem` and `rigidBodyProblem`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createBody(R: RapierModule, world: RapierWorld, parts: BodyParts): BodyRecord;
|
|
45
|
+
/** A body's linear velocity: Rapier's for a dynamic one, the measured movement of a kinematic one, none for a fixed one. */
|
|
46
|
+
export declare function bodyVelocity(record: BodyRecord): {
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
z: number;
|
|
50
|
+
};
|
|
51
|
+
/** After a step: a moving body's pose becomes its entity's (a sleeping dynamic body has not moved). */
|
|
52
|
+
export declare function syncBodyPose(record: BodyRecord): void;
|