@waica/engine 0.23.0 → 0.25.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 +43 -0
- package/dist/assets/asset-loader.d.ts +1 -1
- package/dist/assets/asset-loader.js +1 -1
- package/dist/assets/texture-backend.d.ts +1 -1
- package/dist/assets/texture-backend.js +1 -1
- package/dist/camera.js +1 -1
- package/dist/components/animated-sprite.d.ts +6 -0
- package/dist/components/animated-sprite.js +16 -2
- package/dist/components/light.d.ts +68 -0
- package/dist/components/light.js +59 -0
- package/dist/components/particle-emitter.d.ts +6 -0
- package/dist/components/particle-emitter.js +13 -0
- package/dist/components/sprite.d.ts +6 -0
- package/dist/components/sprite.js +16 -2
- package/dist/components/tilemap.js +9 -1
- package/dist/entity.d.ts +1 -1
- package/dist/entity.js +1 -1
- package/dist/frame-quad.d.ts +21 -0
- package/dist/frame-quad.js +50 -0
- package/dist/game.d.ts +28 -3
- package/dist/game.js +79 -52
- package/dist/index.d.ts +14 -3
- package/dist/index.js +9 -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 +167 -0
- package/dist/occluder-grid.d.ts +19 -0
- package/dist/occluder-grid.js +50 -0
- package/dist/particle-batch.d.ts +1 -1
- package/dist/particle-batch.js +7 -3
- package/dist/particle-sort-bounds.d.ts +1 -1
- package/dist/particle-sort-bounds.js +1 -1
- package/dist/pointer.js +1 -1
- 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 +34 -0
- package/dist/render-output.js +60 -0
- package/dist/render-readiness.d.ts +37 -0
- package/dist/render-readiness.js +61 -0
- package/dist/render-sort.d.ts +14 -0
- package/dist/render-sort.js +37 -0
- package/dist/runtime-bridge.d.ts +18 -2
- package/dist/runtime-bridge.js +7 -1
- package/dist/runtime-inspection.d.ts +5 -0
- package/dist/runtime-inspection.js +5 -1
- package/dist/runtime-lighting-snapshot.d.ts +41 -0
- package/dist/runtime-lighting-snapshot.js +46 -0
- package/dist/scene-lighting.d.ts +52 -0
- package/dist/scene-lighting.js +107 -0
- package/dist/scene-render-options.d.ts +51 -0
- package/dist/scene-render-options.js +126 -0
- package/dist/scene.d.ts +29 -0
- package/dist/sprite-batch.d.ts +3 -3
- package/dist/sprite-batch.js +45 -29
- package/dist/sprite-batches.d.ts +2 -2
- package/dist/sprite-batches.js +1 -1
- package/dist/sprite-draw-collector.d.ts +3 -3
- package/dist/sprite-draw-collector.js +3 -3
- package/package.json +1 -1
package/dist/particle-batch.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import * as THREE from 'three';
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
2
|
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,
|
|
@@ -116,6 +117,9 @@ export class ParticleBatch {
|
|
|
116
117
|
colors.needsUpdate = true;
|
|
117
118
|
this.sortIndices();
|
|
118
119
|
this.mesh.geometry.setDrawRange(0, this.active * 6);
|
|
120
|
+
// WebGPURenderer issues a zero-count draw (WebGLRenderer skipped it):
|
|
121
|
+
// an empty batch is not drawn at all.
|
|
122
|
+
this.mesh.visible = this.active > 0;
|
|
119
123
|
}
|
|
120
124
|
ySortEntries() {
|
|
121
125
|
this.sortEntrySlots();
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import * as THREE from 'three';
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
2
|
/** Keeps Three's transparent-object sort key on live vertices, never unused capacity. */
|
|
3
3
|
export declare function refreshParticleSortBounds(geometry: THREE.BufferGeometry, active: number, emptyZ: number): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as THREE from 'three';
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
2
|
/** Keeps Three's transparent-object sort key on live vertices, never unused capacity. */
|
|
3
3
|
export function refreshParticleSortBounds(geometry, active, emptyZ) {
|
|
4
4
|
const positions = geometry.getAttribute('position');
|
package/dist/pointer.js
CHANGED
|
@@ -140,7 +140,7 @@ export class Pointer {
|
|
|
140
140
|
renderX <= centerX + halfW &&
|
|
141
141
|
renderY >= centerY - halfH &&
|
|
142
142
|
renderY <= centerY + halfH) {
|
|
143
|
-
// Matches
|
|
143
|
+
// Matches applyYSort (render-sort.ts)'s key exactly: the entity's node
|
|
144
144
|
// position, which for an isometric scene is the projected Y (the
|
|
145
145
|
// same `anchor.y` just used to place this box), not the logical one.
|
|
146
146
|
hits.push({ entity, layer: box.layer, y: anchor.y });
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type ColorGradeEffect, type VignetteEffect } from './scene-render-options.js';
|
|
2
|
+
import type { SceneRenderJson } from './scene.js';
|
|
3
|
+
/**
|
|
4
|
+
* `game.post` (issue #78): the live scene's Post Effects — a vignette and a
|
|
5
|
+
* color grade, each off (null) unless its scene declares it or a game turns
|
|
6
|
+
* it on. With either on, the frame renders through a render target and one
|
|
7
|
+
* full-screen pass; with both off, it draws straight to the canvas. Camera
|
|
8
|
+
* Effects (ADR 0020) sit above and are never altered. They die with their scene.
|
|
9
|
+
*/
|
|
10
|
+
export declare class GamePost {
|
|
11
|
+
private state;
|
|
12
|
+
/** The vignette now, a copy; null when off. */
|
|
13
|
+
get vignette(): VignetteEffect | null;
|
|
14
|
+
/**
|
|
15
|
+
* Turns the vignette off with null, or changes it field by field over the
|
|
16
|
+
* current one (absent fields take their defaults when it was off), clamped to 0..1.
|
|
17
|
+
*/
|
|
18
|
+
set vignette(value: Partial<VignetteEffect> | null);
|
|
19
|
+
/** The color grade now, a copy; null when off. */
|
|
20
|
+
get colorGrade(): ColorGradeEffect | null;
|
|
21
|
+
/**
|
|
22
|
+
* Turns the color grade off with null, or changes it field by field over the
|
|
23
|
+
* current one (absent fields neutral when it was off), clamped — like
|
|
24
|
+
* `game.lighting.ambient`, so a tween of one field keeps the others.
|
|
25
|
+
*/
|
|
26
|
+
set colorGrade(value: Partial<ColorGradeEffect> | null);
|
|
27
|
+
/** Whether any Post Effect is on. */
|
|
28
|
+
get active(): boolean;
|
|
29
|
+
/** Adopts a scene's `render.post`, replacing whatever the previous scene left. Called by Game. */
|
|
30
|
+
loadScene(render: SceneRenderJson | undefined): void;
|
|
31
|
+
/** Every effect off again. */
|
|
32
|
+
unloadScene(): void;
|
|
33
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { resolveColorGrade, resolvePostEffects, resolveVignette, } from './scene-render-options.js';
|
|
2
|
+
/**
|
|
3
|
+
* `game.post` (issue #78): the live scene's Post Effects — a vignette and a
|
|
4
|
+
* color grade, each off (null) unless its scene declares it or a game turns
|
|
5
|
+
* it on. With either on, the frame renders through a render target and one
|
|
6
|
+
* full-screen pass; with both off, it draws straight to the canvas. Camera
|
|
7
|
+
* Effects (ADR 0020) sit above and are never altered. They die with their scene.
|
|
8
|
+
*/
|
|
9
|
+
export class GamePost {
|
|
10
|
+
state = resolvePostEffects(undefined);
|
|
11
|
+
/** The vignette now, a copy; null when off. */
|
|
12
|
+
get vignette() {
|
|
13
|
+
const { vignette } = this.state;
|
|
14
|
+
return vignette ? { ...vignette } : null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Turns the vignette off with null, or changes it field by field over the
|
|
18
|
+
* current one (absent fields take their defaults when it was off), clamped to 0..1.
|
|
19
|
+
*/
|
|
20
|
+
set vignette(value) {
|
|
21
|
+
this.state = { ...this.state, vignette: value ? resolveVignette({ ...this.state.vignette, ...value }) : null };
|
|
22
|
+
}
|
|
23
|
+
/** The color grade now, a copy; null when off. */
|
|
24
|
+
get colorGrade() {
|
|
25
|
+
const { colorGrade } = this.state;
|
|
26
|
+
return colorGrade ? { ...colorGrade } : null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Turns the color grade off with null, or changes it field by field over the
|
|
30
|
+
* current one (absent fields neutral when it was off), clamped — like
|
|
31
|
+
* `game.lighting.ambient`, so a tween of one field keeps the others.
|
|
32
|
+
*/
|
|
33
|
+
set colorGrade(value) {
|
|
34
|
+
this.state = { ...this.state, colorGrade: value ? resolveColorGrade({ ...this.state.colorGrade, ...value }) : null };
|
|
35
|
+
}
|
|
36
|
+
/** Whether any Post Effect is on. */
|
|
37
|
+
get active() {
|
|
38
|
+
return this.state.vignette !== null || this.state.colorGrade !== null;
|
|
39
|
+
}
|
|
40
|
+
/** Adopts a scene's `render.post`, replacing whatever the previous scene left. Called by Game. */
|
|
41
|
+
loadScene(render) {
|
|
42
|
+
this.state = resolvePostEffects(render?.post);
|
|
43
|
+
}
|
|
44
|
+
/** Every effect off again. */
|
|
45
|
+
unloadScene() {
|
|
46
|
+
this.loadScene(undefined);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
|
+
import type { PostEffectsState } from './scene-render-options.js';
|
|
3
|
+
/**
|
|
4
|
+
* The Post Effects' render target and pass: the frame is drawn into `target`
|
|
5
|
+
* exactly as onto the canvas — 8-bit, sRGB-encoded, blending on sRGB values
|
|
6
|
+
* (ADR 0025's canvas path) — at the light-map resolution with nearest upscale
|
|
7
|
+
* (E2), then `draw` writes it to the canvas through the effects. A half-float
|
|
8
|
+
* linear target drew nothing on CI's headless Linux WebGPU (PR #150), and it
|
|
9
|
+
* changed every translucent edge.
|
|
10
|
+
*/
|
|
11
|
+
export declare class PostPass {
|
|
12
|
+
readonly target: THREE.RenderTarget<THREE.Texture<unknown, THREE.TextureEventMap>, THREE.RenderTargetEventMap>;
|
|
13
|
+
private readonly uniforms;
|
|
14
|
+
private readonly drawnLikeCanvas;
|
|
15
|
+
private readonly quad;
|
|
16
|
+
/** Sizes the target for this frame. */
|
|
17
|
+
resize(width: number, height: number): void;
|
|
18
|
+
/** Draws the target to whatever is bound now (the canvas), with this frame's effects. */
|
|
19
|
+
draw(renderer: THREE.WebGPURenderer, effects: PostEffectsState): void;
|
|
20
|
+
private setVignette;
|
|
21
|
+
private setColorGrade;
|
|
22
|
+
dispose(): void;
|
|
23
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { clamp, dot, float, length, max, mix, texture, uniform, uv, vec3, vec4 } from 'three/tsl';
|
|
2
|
+
import * as THREE from 'three/webgpu';
|
|
3
|
+
import { FrameQuad } from './frame-quad.js';
|
|
4
|
+
import { hexChannels } from './scene-lighting.js';
|
|
5
|
+
import { drawLikeCanvas } from './render-output.js';
|
|
6
|
+
function createUniforms() {
|
|
7
|
+
return {
|
|
8
|
+
vignetteOn: uniform(0),
|
|
9
|
+
vignetteIntensity: uniform(0),
|
|
10
|
+
vignetteRadius: uniform(0),
|
|
11
|
+
gradeOn: uniform(0),
|
|
12
|
+
tint: uniform(new THREE.Vector3(1, 1, 1)),
|
|
13
|
+
contrast: uniform(1),
|
|
14
|
+
saturation: uniform(1),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The single full-screen pass of the Post Effects (issue #78 CA-11): it reads
|
|
19
|
+
* the frame from the target, already in the canvas's sRGB values, grades it —
|
|
20
|
+
* tint, then saturation around Rec. 709 luma, then contrast around mid grey —
|
|
21
|
+
* and darkens it towards the corners. Its output is the canvas's sRGB value.
|
|
22
|
+
*/
|
|
23
|
+
function createPostMaterial(scene, u) {
|
|
24
|
+
const material = new THREE.MeshBasicNodeMaterial();
|
|
25
|
+
// Already the canvas's sRGB values (drawLikeCanvas): graded as they would show.
|
|
26
|
+
const display = texture(scene, uv()).rgb;
|
|
27
|
+
const tinted = display.mul(u.tint);
|
|
28
|
+
const luma = dot(tinted, vec3(0.2126, 0.7152, 0.0722));
|
|
29
|
+
const saturated = mix(vec3(luma, luma, luma), tinted, u.saturation);
|
|
30
|
+
const graded = saturated.sub(0.5).mul(u.contrast).add(0.5);
|
|
31
|
+
const color = mix(display, graded, u.gradeOn);
|
|
32
|
+
// 0 at the centre, 1 at the corners.
|
|
33
|
+
const distance = length(uv().sub(0.5).mul(2)).div(Math.SQRT2);
|
|
34
|
+
const ramp = clamp(distance.sub(u.vignetteRadius).div(max(float(1).sub(u.vignetteRadius), 1e-3)), 0, 1);
|
|
35
|
+
const eased = ramp.mul(ramp).mul(float(3).sub(ramp.mul(2)));
|
|
36
|
+
const vignette = float(1).sub(u.vignetteIntensity.mul(eased).mul(u.vignetteOn));
|
|
37
|
+
material.fragmentNode = vec4(clamp(color.mul(vignette), vec3(0, 0, 0), vec3(1, 1, 1)), 1);
|
|
38
|
+
material.depthTest = false;
|
|
39
|
+
material.depthWrite = false;
|
|
40
|
+
return material;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The Post Effects' render target and pass: the frame is drawn into `target`
|
|
44
|
+
* exactly as onto the canvas — 8-bit, sRGB-encoded, blending on sRGB values
|
|
45
|
+
* (ADR 0025's canvas path) — at the light-map resolution with nearest upscale
|
|
46
|
+
* (E2), then `draw` writes it to the canvas through the effects. A half-float
|
|
47
|
+
* linear target drew nothing on CI's headless Linux WebGPU (PR #150), and it
|
|
48
|
+
* changed every translucent edge.
|
|
49
|
+
*/
|
|
50
|
+
export class PostPass {
|
|
51
|
+
target = new THREE.RenderTarget(1, 1, {
|
|
52
|
+
type: THREE.UnsignedByteType,
|
|
53
|
+
minFilter: THREE.NearestFilter,
|
|
54
|
+
magFilter: THREE.NearestFilter,
|
|
55
|
+
depthBuffer: true,
|
|
56
|
+
// The canvas's own MSAA (antialias: true): geometry edges match the frame without post.
|
|
57
|
+
samples: 4,
|
|
58
|
+
});
|
|
59
|
+
uniforms = createUniforms();
|
|
60
|
+
drawnLikeCanvas = drawLikeCanvas(this.target);
|
|
61
|
+
quad = new FrameQuad(createPostMaterial(this.target.texture, this.uniforms));
|
|
62
|
+
/** Sizes the target for this frame. */
|
|
63
|
+
resize(width, height) {
|
|
64
|
+
if (this.target.width !== width || this.target.height !== height)
|
|
65
|
+
this.target.setSize(width, height);
|
|
66
|
+
}
|
|
67
|
+
/** Draws the target to whatever is bound now (the canvas), with this frame's effects. */
|
|
68
|
+
draw(renderer, effects) {
|
|
69
|
+
this.setVignette(effects.vignette);
|
|
70
|
+
this.setColorGrade(effects.colorGrade);
|
|
71
|
+
this.quad.render(renderer);
|
|
72
|
+
}
|
|
73
|
+
setVignette(vignette) {
|
|
74
|
+
const u = this.uniforms;
|
|
75
|
+
u.vignetteOn.value = vignette ? 1 : 0;
|
|
76
|
+
u.vignetteIntensity.value = vignette ? vignette.intensity : 0;
|
|
77
|
+
u.vignetteRadius.value = vignette ? vignette.radius : 0;
|
|
78
|
+
}
|
|
79
|
+
setColorGrade(grade) {
|
|
80
|
+
const u = this.uniforms;
|
|
81
|
+
const neutral = { tint: '#ffffff', contrast: 1, saturation: 1 };
|
|
82
|
+
const { tint, contrast, saturation } = grade ?? neutral;
|
|
83
|
+
u.gradeOn.value = grade ? 1 : 0;
|
|
84
|
+
const [r, g, b] = hexChannels(tint);
|
|
85
|
+
u.tint.value.set(r, g, b);
|
|
86
|
+
u.contrast.value = contrast;
|
|
87
|
+
u.saturation.value = saturation;
|
|
88
|
+
}
|
|
89
|
+
dispose() {
|
|
90
|
+
this.target.dispose();
|
|
91
|
+
this.quad.dispose();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type * as THREE from 'three/webgpu';
|
|
2
|
+
/**
|
|
3
|
+
* The three.js layer an Emissive drawable lives on, alone (issue #78 CA-10).
|
|
4
|
+
* The Game's camera sees layer 0 and this one, so an unlit scene draws an
|
|
5
|
+
* Emissive drawable in place like any other; a lit scene draws layer 0, then
|
|
6
|
+
* its light-map, then this layer at full brightness (ADR 0026).
|
|
7
|
+
*/
|
|
8
|
+
export declare const EMISSIVE_LAYER = 1;
|
|
9
|
+
/** Puts `object` on the Emissive layer alone, or back on the default layer 0. */
|
|
10
|
+
export declare function setEmissive(object: THREE.Object3D, emissive: boolean): void;
|
|
11
|
+
/**
|
|
12
|
+
* The alphaTest every drawable carries (PR #150 review): a texel whose alpha
|
|
13
|
+
* byte is 0 is discarded instead of drawn. It contributes no color either way
|
|
14
|
+
* (normal blending keeps the frame), but discarded it also writes no depth, so
|
|
15
|
+
* an Emissive drawable drawn after the lit pass shows through the transparent
|
|
16
|
+
* texels of a lit sprite in front of it, and stays hidden behind its opaque ones.
|
|
17
|
+
*/
|
|
18
|
+
export declare const TRANSPARENT_TEXEL_ALPHA: number;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three.js layer an Emissive drawable lives on, alone (issue #78 CA-10).
|
|
3
|
+
* The Game's camera sees layer 0 and this one, so an unlit scene draws an
|
|
4
|
+
* Emissive drawable in place like any other; a lit scene draws layer 0, then
|
|
5
|
+
* its light-map, then this layer at full brightness (ADR 0026).
|
|
6
|
+
*/
|
|
7
|
+
export const EMISSIVE_LAYER = 1;
|
|
8
|
+
/** Puts `object` on the Emissive layer alone, or back on the default layer 0. */
|
|
9
|
+
export function setEmissive(object, emissive) {
|
|
10
|
+
object.layers.set(emissive ? EMISSIVE_LAYER : 0);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The alphaTest every drawable carries (PR #150 review): a texel whose alpha
|
|
14
|
+
* byte is 0 is discarded instead of drawn. It contributes no color either way
|
|
15
|
+
* (normal blending keeps the frame), but discarded it also writes no depth, so
|
|
16
|
+
* an Emissive drawable drawn after the lit pass shows through the transparent
|
|
17
|
+
* texels of a lit sprite in front of it, and stays hidden behind its opaque ones.
|
|
18
|
+
*/
|
|
19
|
+
export const TRANSPARENT_TEXEL_ALPHA = 1 / 512;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
|
+
/**
|
|
3
|
+
* Makes WebGPURenderer draw the way WebGLRenderer did (ADR 0025): straight
|
|
4
|
+
* into the 8-bit canvas, each material encoding its color to sRGB in its own
|
|
5
|
+
* shader, so alpha blending happens on sRGB values. By default three renders
|
|
6
|
+
* into a linear half-float target and converts it in an extra full-screen
|
|
7
|
+
* pass: blending in linear space, which changes every translucent edge, and
|
|
8
|
+
* one more draw per frame. A render target keeps the linear working space.
|
|
9
|
+
*/
|
|
10
|
+
export declare function drawStraightToCanvas(renderer: THREE.WebGPURenderer): void;
|
|
11
|
+
/**
|
|
12
|
+
* Makes `target` receive what the canvas would (issue #78): every material
|
|
13
|
+
* encodes to sRGB into it and blends on sRGB values, so a frame drawn there
|
|
14
|
+
* and copied to the canvas is the frame drawn straight to the canvas. The
|
|
15
|
+
* Post Effects' 8-bit target.
|
|
16
|
+
*/
|
|
17
|
+
export declare function drawLikeCanvas(target: THREE.RenderTarget): THREE.RenderTarget;
|
|
18
|
+
/**
|
|
19
|
+
* The scene background for a canvas drawn straight: three clears with a
|
|
20
|
+
* color's working-space (linear) components, and the canvas stores sRGB, so
|
|
21
|
+
* the clear carries the color's sRGB components — what WebGLRenderer wrote.
|
|
22
|
+
*/
|
|
23
|
+
export declare function canvasBackground(color: THREE.ColorRepresentation): THREE.Color;
|
|
24
|
+
/**
|
|
25
|
+
* Applies a material map's UV transform (offset, repeat) per vertex, as
|
|
26
|
+
* WebGLRenderer's `vMapUv` did, instead of per fragment as three's node
|
|
27
|
+
* materials do by default. At a quad's corners the UV is 0 or 1, so the
|
|
28
|
+
* transformed corner UV is the same float however a shader compiler fuses
|
|
29
|
+
* the multiply and add — per fragment it is not, and a sprite whose sheet
|
|
30
|
+
* offset is not 0 could then sample a texel row apart in two shaders of the
|
|
31
|
+
* same picture (a Sprite Batch and its per-sprite fallback, ADR 0024).
|
|
32
|
+
* Textures with their own UV node, or sampled outside a mesh, stay three's.
|
|
33
|
+
*/
|
|
34
|
+
export declare function mapUvPerVertex(renderer: THREE.WebGPURenderer): void;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { uv, vertexStage, workingToColorSpace } from 'three/tsl';
|
|
2
|
+
import * as THREE from 'three/webgpu';
|
|
3
|
+
/**
|
|
4
|
+
* Makes WebGPURenderer draw the way WebGLRenderer did (ADR 0025): straight
|
|
5
|
+
* into the 8-bit canvas, each material encoding its color to sRGB in its own
|
|
6
|
+
* shader, so alpha blending happens on sRGB values. By default three renders
|
|
7
|
+
* into a linear half-float target and converts it in an extra full-screen
|
|
8
|
+
* pass: blending in linear space, which changes every translucent edge, and
|
|
9
|
+
* one more draw per frame. A render target keeps the linear working space.
|
|
10
|
+
*/
|
|
11
|
+
export function drawStraightToCanvas(renderer) {
|
|
12
|
+
// The working space as output: three then needs no intermediate target.
|
|
13
|
+
renderer.outputColorSpace = THREE.ColorManagement.workingColorSpace;
|
|
14
|
+
const context = renderer.contextNode.value;
|
|
15
|
+
context.getOutput = (output, builder) => {
|
|
16
|
+
const target = builder.renderer.getRenderTarget();
|
|
17
|
+
const likeCanvas = target === null || (typeof target === 'object' && canvasLike.has(target));
|
|
18
|
+
return likeCanvas ? workingToColorSpace(output, THREE.SRGBColorSpace) : output;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Render targets that hold a frame exactly as the canvas would: sRGB-encoded 8-bit values. */
|
|
22
|
+
const canvasLike = new WeakSet();
|
|
23
|
+
/**
|
|
24
|
+
* Makes `target` receive what the canvas would (issue #78): every material
|
|
25
|
+
* encodes to sRGB into it and blends on sRGB values, so a frame drawn there
|
|
26
|
+
* and copied to the canvas is the frame drawn straight to the canvas. The
|
|
27
|
+
* Post Effects' 8-bit target.
|
|
28
|
+
*/
|
|
29
|
+
export function drawLikeCanvas(target) {
|
|
30
|
+
canvasLike.add(target);
|
|
31
|
+
return target;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The scene background for a canvas drawn straight: three clears with a
|
|
35
|
+
* color's working-space (linear) components, and the canvas stores sRGB, so
|
|
36
|
+
* the clear carries the color's sRGB components — what WebGLRenderer wrote.
|
|
37
|
+
*/
|
|
38
|
+
export function canvasBackground(color) {
|
|
39
|
+
const { r, g, b } = new THREE.Color(color).getRGB({ r: 0, g: 0, b: 0 }, THREE.SRGBColorSpace);
|
|
40
|
+
return new THREE.Color().setRGB(r, g, b, THREE.LinearSRGBColorSpace);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Applies a material map's UV transform (offset, repeat) per vertex, as
|
|
44
|
+
* WebGLRenderer's `vMapUv` did, instead of per fragment as three's node
|
|
45
|
+
* materials do by default. At a quad's corners the UV is 0 or 1, so the
|
|
46
|
+
* transformed corner UV is the same float however a shader compiler fuses
|
|
47
|
+
* the multiply and add — per fragment it is not, and a sprite whose sheet
|
|
48
|
+
* offset is not 0 could then sample a texel row apart in two shaders of the
|
|
49
|
+
* same picture (a Sprite Batch and its per-sprite fallback, ADR 0024).
|
|
50
|
+
* Textures with their own UV node, or sampled outside a mesh, stay three's.
|
|
51
|
+
*/
|
|
52
|
+
export function mapUvPerVertex(renderer) {
|
|
53
|
+
const context = renderer.contextNode.value;
|
|
54
|
+
context.getUV = (texture, builder) => {
|
|
55
|
+
if (texture.uvNode !== null || !builder.object.isMesh)
|
|
56
|
+
return null;
|
|
57
|
+
texture.setUpdateMatrix(false);
|
|
58
|
+
return vertexStage(texture.getTransformedUV(uv(texture.value.channel)));
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GPU API a Game draws through (CONTEXT.md): `webgpu` when the browser
|
|
3
|
+
* offers it, otherwise three's own WebGL2 fallback. The browser decides; no
|
|
4
|
+
* option forces either one (ADR 0025).
|
|
5
|
+
*/
|
|
6
|
+
export type RenderBackend = 'webgpu' | 'webgl2';
|
|
7
|
+
/** The part of three's WebGPURenderer readiness depends on. */
|
|
8
|
+
export interface InitializingRenderer {
|
|
9
|
+
init(): Promise<unknown>;
|
|
10
|
+
dispose(): Promise<void>;
|
|
11
|
+
/** After init(), the backend it settled on: the WebGPU one, or the WebGL2 fallback. */
|
|
12
|
+
readonly backend: object;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A renderer's asynchronous initialization, owned by its Game: started at
|
|
16
|
+
* construction, settled once, and the only gate between a frame and a draw.
|
|
17
|
+
* `promise` is what `game.ready()` hands out; a failure rejects it with an
|
|
18
|
+
* error naming both Render Backends and is also reported to `onFailure`.
|
|
19
|
+
*/
|
|
20
|
+
export declare class RenderReadiness {
|
|
21
|
+
private readonly renderer;
|
|
22
|
+
readonly promise: Promise<void>;
|
|
23
|
+
private state;
|
|
24
|
+
constructor(renderer: InitializingRenderer, onFailure: (error: Error) => void);
|
|
25
|
+
/** Runs `then` once the renderer is ready; never after a failed init. */
|
|
26
|
+
whenReady(then: () => void): void;
|
|
27
|
+
/** True once the renderer can draw. */
|
|
28
|
+
get isReady(): boolean;
|
|
29
|
+
/** The Render Backend after a successful init; null before it and after a failure. */
|
|
30
|
+
get backend(): RenderBackend | null;
|
|
31
|
+
/**
|
|
32
|
+
* Frees the renderer: right away once it is ready, after init settles
|
|
33
|
+
* while it is pending (three cannot dispose a renderer mid-init), never
|
|
34
|
+
* after a failed init (nothing was allocated).
|
|
35
|
+
*/
|
|
36
|
+
disposeRenderer(): void;
|
|
37
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** three flags its WebGPU backend with `isWebGPUBackend`; its WebGL2 fallback has no such flag. */
|
|
2
|
+
function backendOf(backend) {
|
|
3
|
+
return 'isWebGPUBackend' in backend && backend.isWebGPUBackend === true ? 'webgpu' : 'webgl2';
|
|
4
|
+
}
|
|
5
|
+
function describeCause(cause) {
|
|
6
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A renderer's asynchronous initialization, owned by its Game: started at
|
|
10
|
+
* construction, settled once, and the only gate between a frame and a draw.
|
|
11
|
+
* `promise` is what `game.ready()` hands out; a failure rejects it with an
|
|
12
|
+
* error naming both Render Backends and is also reported to `onFailure`.
|
|
13
|
+
*/
|
|
14
|
+
export class RenderReadiness {
|
|
15
|
+
renderer;
|
|
16
|
+
promise;
|
|
17
|
+
state = { status: 'pending' };
|
|
18
|
+
constructor(renderer, onFailure) {
|
|
19
|
+
this.renderer = renderer;
|
|
20
|
+
this.promise = renderer.init().then(() => {
|
|
21
|
+
this.state = { status: 'ready', backend: backendOf(renderer.backend) };
|
|
22
|
+
}, (cause) => {
|
|
23
|
+
this.state = { status: 'failed' };
|
|
24
|
+
const error = new Error(`No Render Backend could initialize: neither webgpu nor webgl2 is available (${describeCause(cause)}).`, { cause });
|
|
25
|
+
onFailure(error);
|
|
26
|
+
throw error;
|
|
27
|
+
});
|
|
28
|
+
// The rejection belongs to whoever awaits game.ready(); a Game nobody
|
|
29
|
+
// asked must not surface it as an unhandled rejection on top of that.
|
|
30
|
+
this.promise.catch(() => { });
|
|
31
|
+
}
|
|
32
|
+
/** Runs `then` once the renderer is ready; never after a failed init. */
|
|
33
|
+
whenReady(then) {
|
|
34
|
+
this.promise.then(then, () => { });
|
|
35
|
+
}
|
|
36
|
+
/** True once the renderer can draw. */
|
|
37
|
+
get isReady() {
|
|
38
|
+
return this.state.status === 'ready';
|
|
39
|
+
}
|
|
40
|
+
/** The Render Backend after a successful init; null before it and after a failure. */
|
|
41
|
+
get backend() {
|
|
42
|
+
return this.state.status === 'ready' ? this.state.backend : null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Frees the renderer: right away once it is ready, after init settles
|
|
46
|
+
* while it is pending (three cannot dispose a renderer mid-init), never
|
|
47
|
+
* after a failed init (nothing was allocated).
|
|
48
|
+
*/
|
|
49
|
+
disposeRenderer() {
|
|
50
|
+
const dispose = () => {
|
|
51
|
+
this.renderer.dispose().catch((error) => {
|
|
52
|
+
console.error('[waica] could not dispose the renderer', error);
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
if (this.isReady) {
|
|
56
|
+
dispose();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
this.promise.then(dispose, () => { });
|
|
60
|
+
}
|
|
61
|
+
}
|
package/dist/render-sort.d.ts
CHANGED
|
@@ -35,3 +35,17 @@ export declare function isYSortBatchParticipant(value: unknown): value is YSortB
|
|
|
35
35
|
* apart, so their band is exactly the old fixed 0.01.
|
|
36
36
|
*/
|
|
37
37
|
export declare function ySortZ(entries: readonly YSortEntry[]): number[];
|
|
38
|
+
/** What applyYSort reads of an entity: its components and the Y its node draws at. */
|
|
39
|
+
export interface YSortEntity {
|
|
40
|
+
readonly components: readonly unknown[];
|
|
41
|
+
readonly node: {
|
|
42
|
+
readonly position: {
|
|
43
|
+
readonly y: number;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Under y-sort, re-derives every participant's z from layer band + entity Y:
|
|
49
|
+
* the scene's entities, then the batch participants its drains hold.
|
|
50
|
+
*/
|
|
51
|
+
export declare function applyYSort(entities: readonly YSortEntity[], drains: readonly YSortBatchParticipant[]): void;
|
package/dist/render-sort.js
CHANGED
|
@@ -48,3 +48,40 @@ export function ySortZ(entries) {
|
|
|
48
48
|
}
|
|
49
49
|
return z;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Under y-sort, re-derives every participant's z from layer band + entity Y:
|
|
53
|
+
* the scene's entities, then the batch participants its drains hold.
|
|
54
|
+
*/
|
|
55
|
+
export function applyYSort(entities, drains) {
|
|
56
|
+
const singles = [];
|
|
57
|
+
const batches = [];
|
|
58
|
+
const entries = [];
|
|
59
|
+
for (const entity of entities) {
|
|
60
|
+
for (const component of entity.components) {
|
|
61
|
+
if (isYSortBatchParticipant(component)) {
|
|
62
|
+
const batchEntries = component.ySortEntries();
|
|
63
|
+
batches.push({ participant: component, start: entries.length, count: batchEntries.length });
|
|
64
|
+
entries.push(...batchEntries);
|
|
65
|
+
}
|
|
66
|
+
else if (isYSortParticipant(component)) {
|
|
67
|
+
singles.push({ participant: component, index: entries.length });
|
|
68
|
+
entries.push({ layer: component.layer, y: entity.node.position.y });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const participant of drains) {
|
|
73
|
+
const batchEntries = participant.ySortEntries();
|
|
74
|
+
batches.push({ participant, start: entries.length, count: batchEntries.length });
|
|
75
|
+
entries.push(...batchEntries);
|
|
76
|
+
}
|
|
77
|
+
const z = ySortZ(entries);
|
|
78
|
+
for (const { participant, index } of singles) {
|
|
79
|
+
const sortZ = z[index];
|
|
80
|
+
if (sortZ === undefined)
|
|
81
|
+
throw new Error(`ySortZ returned no z for y-sort participant ${index}`);
|
|
82
|
+
participant.setSortZ(sortZ);
|
|
83
|
+
}
|
|
84
|
+
for (const { participant, start, count } of batches) {
|
|
85
|
+
participant.setSortZs(z.slice(start, start + count));
|
|
86
|
+
}
|
|
87
|
+
}
|
package/dist/runtime-bridge.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AssetStatus } from './assets/asset-loader.js';
|
|
2
|
+
import type { RenderBackend } from './render-readiness.js';
|
|
2
3
|
import type { RuntimeSnapshot, RuntimeSnapshotFilters } from './runtime-inspection.js';
|
|
3
4
|
export declare const RUNTIME_BRIDGE_PROTOCOL_VERSION: 1;
|
|
4
5
|
export declare const RUNTIME_BRIDGE_SYMBOL: unique symbol;
|
|
@@ -20,9 +21,13 @@ export type RuntimeMode = 'paused' | 'real-time';
|
|
|
20
21
|
* from `game.cameraEffects`; the `scene` operation stays a hard cut.
|
|
21
22
|
* `analog-actions` (issue #75, ADR 0023) announces that `hold` takes an
|
|
22
23
|
* optional `value` in (0, 1] and that every control result carries
|
|
23
|
-
* `actionValues` beside `heldActions`.
|
|
24
|
+
* `actionValues` beside `heldActions`. `render-backend` (issue #140, ADR
|
|
25
|
+
* 0025) announces that every metadata carries `backend`: the Render Backend
|
|
26
|
+
* once the renderer is ready, null before — a Run Session waits for it — and
|
|
27
|
+
* that a renderer that fails to initialize reports through the activation's
|
|
28
|
+
* `fail`.
|
|
24
29
|
*/
|
|
25
|
-
export declare const RUNTIME_BRIDGE_CAPABILITIES: readonly ['click', 'scene', 'fixed-step', 'assets', 'camera-effects', 'analog-actions'];
|
|
30
|
+
export declare const RUNTIME_BRIDGE_CAPABILITIES: readonly ['click', 'scene', 'fixed-step', 'assets', 'camera-effects', 'analog-actions', 'render-backend'];
|
|
26
31
|
export interface RuntimeMetadata {
|
|
27
32
|
bridgeVersion: typeof RUNTIME_BRIDGE_PROTOCOL_VERSION;
|
|
28
33
|
engineVersion: string;
|
|
@@ -32,6 +37,8 @@ export interface RuntimeMetadata {
|
|
|
32
37
|
capabilities: readonly string[];
|
|
33
38
|
/** `game.assets.status` at the moment of the read — reading it never advances `frame`. */
|
|
34
39
|
assets: AssetStatus;
|
|
40
|
+
/** The Render Backend once `game.ready()` resolved; null until then (and for good after a failed init). */
|
|
41
|
+
backend: RenderBackend | null;
|
|
35
42
|
}
|
|
36
43
|
export type RuntimeControlRequest = {
|
|
37
44
|
operation: 'press' | 'release';
|
|
@@ -76,11 +83,18 @@ export interface RuntimeBridge {
|
|
|
76
83
|
inspect(filters?: RuntimeSnapshotFilters): RuntimeSnapshot;
|
|
77
84
|
control(request: RuntimeControlRequest): RuntimeControlResult;
|
|
78
85
|
}
|
|
86
|
+
/** Why a Game can never become operational; reported through the activation. */
|
|
87
|
+
export interface RuntimeBridgeFailure {
|
|
88
|
+
code: 'render-backend-failed';
|
|
89
|
+
message: string;
|
|
90
|
+
}
|
|
79
91
|
/** Ephemeral pre-page hook installed by the owner of a browser context. */
|
|
80
92
|
export interface RuntimeBridgeActivation {
|
|
81
93
|
readonly protocolVersion: typeof RUNTIME_BRIDGE_PROTOCOL_VERSION;
|
|
82
94
|
register(bridge: RuntimeBridge): void;
|
|
83
95
|
unregister(bridge: RuntimeBridge): void;
|
|
96
|
+
/** Optional: told when the Game's renderer fails to initialize, so the owner stops waiting. */
|
|
97
|
+
fail?(failure: RuntimeBridgeFailure): void;
|
|
84
98
|
}
|
|
85
99
|
export declare function activeRuntimeBridgeHook(): RuntimeBridgeActivation | null;
|
|
86
100
|
export interface RuntimeBridgeHost {
|
|
@@ -104,6 +118,8 @@ export interface RuntimeBridgeHost {
|
|
|
104
118
|
availableScenes(): string[];
|
|
105
119
|
/** A fresh `game.assets.status`. */
|
|
106
120
|
assets(): AssetStatus;
|
|
121
|
+
/** The Render Backend, null until the renderer is ready. */
|
|
122
|
+
backend(): RenderBackend | null;
|
|
107
123
|
}
|
|
108
124
|
export declare class EngineRuntimeBridge implements RuntimeBridge {
|
|
109
125
|
readonly surface: HTMLCanvasElement;
|
package/dist/runtime-bridge.js
CHANGED
|
@@ -20,7 +20,11 @@ export const RUNTIME_BRIDGE_SYMBOL = Symbol.for('@waica/runtime-bridge/v1');
|
|
|
20
20
|
* from `game.cameraEffects`; the `scene` operation stays a hard cut.
|
|
21
21
|
* `analog-actions` (issue #75, ADR 0023) announces that `hold` takes an
|
|
22
22
|
* optional `value` in (0, 1] and that every control result carries
|
|
23
|
-
* `actionValues` beside `heldActions`.
|
|
23
|
+
* `actionValues` beside `heldActions`. `render-backend` (issue #140, ADR
|
|
24
|
+
* 0025) announces that every metadata carries `backend`: the Render Backend
|
|
25
|
+
* once the renderer is ready, null before — a Run Session waits for it — and
|
|
26
|
+
* that a renderer that fails to initialize reports through the activation's
|
|
27
|
+
* `fail`.
|
|
24
28
|
*/
|
|
25
29
|
export const RUNTIME_BRIDGE_CAPABILITIES = [
|
|
26
30
|
'click',
|
|
@@ -29,6 +33,7 @@ export const RUNTIME_BRIDGE_CAPABILITIES = [
|
|
|
29
33
|
'assets',
|
|
30
34
|
'camera-effects',
|
|
31
35
|
'analog-actions',
|
|
36
|
+
'render-backend',
|
|
32
37
|
];
|
|
33
38
|
export class RuntimeBridgeOperationError extends Error {
|
|
34
39
|
code;
|
|
@@ -79,6 +84,7 @@ export class EngineRuntimeBridge {
|
|
|
79
84
|
simulationTime: this.frame * SIMULATION_STEP,
|
|
80
85
|
capabilities: RUNTIME_BRIDGE_CAPABILITIES,
|
|
81
86
|
assets: this.host.assets(),
|
|
87
|
+
backend: this.host.backend(),
|
|
82
88
|
};
|
|
83
89
|
}
|
|
84
90
|
inspect(filters = {}) {
|
|
@@ -3,6 +3,7 @@ import type { CameraEffectsState } from './camera-effects.js';
|
|
|
3
3
|
import type { Game } from './game.js';
|
|
4
4
|
import type { RuntimeMetadata } from './runtime-bridge.js';
|
|
5
5
|
import type { StatValue } from './stats.js';
|
|
6
|
+
import { type RuntimeSnapshotLighting, type RuntimeSnapshotPost } from './runtime-lighting-snapshot.js';
|
|
6
7
|
export type ProjectionMarkerKind = 'cycle' | 'unsupported' | 'error' | 'truncated';
|
|
7
8
|
export interface ProjectionMarker {
|
|
8
9
|
$waica: ProjectionMarkerKind | 'date' | 'bigint' | 'map' | 'set';
|
|
@@ -124,6 +125,10 @@ export interface RuntimeSnapshot extends RuntimeMetadata {
|
|
|
124
125
|
time: RuntimeSnapshotTime;
|
|
125
126
|
ui: RuntimeSnapshotUi;
|
|
126
127
|
camera: RuntimeSnapshotCamera;
|
|
128
|
+
/** Ambient Light and live Lights (issue #78 CA-12), logical coordinates; never filtered. */
|
|
129
|
+
lighting: RuntimeSnapshotLighting;
|
|
130
|
+
/** Post Effects (issue #78 CA-12), each null when off; never filtered. */
|
|
131
|
+
post: RuntimeSnapshotPost;
|
|
127
132
|
}
|
|
128
133
|
export declare const RUNTIME_PROJECTION_LIMITS: {
|
|
129
134
|
readonly depth: 5;
|