@driftengine/core 4.11.0 → 4.11.1
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/CHANGELOG.json +36 -0
- package/README.md +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/render/ambientOcclusionPass.d.ts +3 -2
- package/dist/render/ambientOcclusionPass.js +4 -2
- package/dist/render/backend/api.d.ts +21 -1
- package/dist/render/backend/webgl2/renderer.d.ts +45 -4
- package/dist/render/backend/webgl2/renderer.js +112 -19
- package/dist/render/backend/webgpu/renderer.d.ts +52 -1
- package/dist/render/backend/webgpu/renderer.js +237 -89
- package/dist/render/instances.d.ts +13 -1
- package/dist/render/instances.js +10 -0
- package/dist/render/occlusionFade.d.ts +21 -0
- package/dist/render/occlusionFade.js +27 -0
- package/dist/render/sceneTarget.d.ts +2 -0
- package/dist/render/sceneTarget.js +1 -1
- package/dist/render/shaders/ambientOcclusion.js +12 -1
- package/dist/render/shaders/flat/index.js +9 -2
- package/dist/render/shaders/flat/layered.d.ts +15 -7
- package/dist/render/shaders/flat/layered.js +114 -31
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +10 -3
- package/dist/render/shaders/flat/normalMap.d.ts +1 -1
- package/dist/render/shaders/flat/normalMap.js +11 -2
- package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +6 -1
- package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +6 -1
- package/dist/render/shaders/generated/flat.wgsl.d.ts +48 -48
- package/dist/render/shaders/generated/flat.wgsl.js +71 -71
- package/dist/render/surfaceLayers.d.ts +83 -15
- package/dist/render/surfaceLayers.js +69 -13
- package/dist/render/surfaceTexture.d.ts +13 -4
- package/dist/render/transmission.d.ts +21 -0
- package/dist/render/transmission.js +21 -0
- package/dist/ui/splash.d.ts +42 -2
- package/dist/ui/splash.js +78 -13
- package/package.json +6 -6
- package/src/index.ts +2 -0
- package/src/render/ambientOcclusionPass.ts +4 -1
- package/src/render/backend/api.ts +31 -0
- package/src/render/backend/webgl2/renderer.ts +130 -20
- package/src/render/backend/webgpu/renderer.ts +371 -139
- package/src/render/instances.ts +18 -1
- package/src/render/occlusionFade.ts +29 -0
- package/src/render/sceneTarget.ts +3 -0
- package/src/render/shaders/ambientOcclusion.ts +12 -1
- package/src/render/shaders/flat/index.ts +9 -2
- package/src/render/shaders/flat/layered.ts +114 -31
- package/src/render/shaders/flat/main.ts +10 -3
- package/src/render/shaders/flat/normalMap.ts +11 -2
- package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +6 -1
- package/src/render/shaders/generated/flat.wgsl.ts +71 -71
- package/src/render/surfaceLayers.ts +138 -19
- package/src/render/surfaceTexture.ts +13 -4
- package/src/render/transmission.ts +27 -0
- package/src/ui/splash.ts +83 -14
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A material blended from layers by a mask: `SurfaceMaterial.layers`,
|
|
3
|
-
* `shaders/flat/layered.ts` reads it from.
|
|
2
|
+
* A material blended from layers by a mask: `SurfaceMaterial.layers`, the four vectors
|
|
3
|
+
* `shaders/flat/layered.ts` reads it from, and the decisions both backends take about it.
|
|
4
4
|
*/
|
|
5
|
+
import type { SurfaceProjection } from './surfaceProjection.ts';
|
|
5
6
|
/** The most layers a material blends: a base and one for each of the mask's four channels. */
|
|
6
7
|
export declare const MAX_SURFACE_LAYERS = 5;
|
|
7
8
|
/**
|
|
@@ -10,30 +11,97 @@ export declare const MAX_SURFACE_LAYERS = 5;
|
|
|
10
11
|
* **Layer `i` is layer `i` of the material's arrays**: `albedo`, `normal` and `orm` each an array
|
|
11
12
|
* whose layers are the material's layers in order, read at the mesh's coordinates times
|
|
12
13
|
* `repeats[i]`, so rock can repeat forty times across a cliff while the moss on it repeats three.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* read
|
|
16
|
-
*
|
|
17
|
-
* layer shows and nowhere it is covered;
|
|
14
|
+
* Where the material has a `projection`, the layers are read at the world's horizontal position
|
|
15
|
+
* instead, so a repeat is so many a metre (with the projection's own scale at 1). **The mask** is
|
|
16
|
+
* read at the mesh's own coordinates and lays each layer over those before it: red lays layer 1
|
|
17
|
+
* over the base, green layer 2 over that, blue layer 3, alpha layer 4. `emissiveLayer` gives the
|
|
18
|
+
* material's emissive map to one layer, glowing where that layer shows and nowhere it is covered;
|
|
19
|
+
* absent, the map glows as on any material.
|
|
20
|
+
*
|
|
21
|
+
* **Maps beyond the layers ride the material's own arrays, after its layers**, so they take no
|
|
22
|
+
* texture binding of their own — the lit stage has none to spare: a mask the ORM array carries
|
|
23
|
+
* (`mask: 'orm'`) is the ORM array's layer just past the layers, an `addMask` the one after that,
|
|
24
|
+
* and `meshNormal` the normal array's layer just past the layers. Each shares its array's size and
|
|
25
|
+
* format, so a four-channel mask wants an array that stores four.
|
|
18
26
|
*
|
|
19
27
|
* **What it gives up**: every layer's maps are read wherever any shows, three reads a layer, so five
|
|
20
28
|
* layers are fifteen reads where one material is three; layers share an array's size and format; a
|
|
21
|
-
* cutout reads the base layer's alpha;
|
|
29
|
+
* cutout reads the base layer's alpha; a mask read where a model's map goes leaves no room for one,
|
|
30
|
+
* so a lightmapped surface carries its mask in its ORM array or its vertices; a triplanar projection
|
|
31
|
+
* lays layers on the horizontal plane alone; and a mask is sampled linear, so it is uploaded as data
|
|
22
32
|
* (`colorSpace: 'linear'`).
|
|
23
33
|
*/
|
|
24
34
|
export interface SurfaceLayers<Texture> {
|
|
25
|
-
/**
|
|
26
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Where the weights come from, red to alpha laying layers 1 to 4: a map read where a shading
|
|
37
|
+
* model's map goes; `'orm'`, the ORM array's layer just past the layers; or `'vertex'`, the
|
|
38
|
+
* vertex colour's red, green and blue, which then weights the layers instead of tinting them.
|
|
39
|
+
*/
|
|
40
|
+
readonly mask: Texture | 'orm' | 'vertex';
|
|
27
41
|
/** Each layer's repeats across the mesh's coordinates, base first: one to five of them. */
|
|
28
42
|
readonly repeats: readonly number[];
|
|
29
43
|
/** The layer the emissive map glows on, 0 to 4. Absent: everywhere, as on any material. */
|
|
30
44
|
readonly emissiveLayer?: number;
|
|
45
|
+
/**
|
|
46
|
+
* How the layers combine. `'over'`, the default, lays each over the ones before it by its
|
|
47
|
+
* channel. `'sum'` mixes the base toward the sum of each layer times its channel, by the channels'
|
|
48
|
+
* sum held to 1: two layers at half each are both half there, where laying over would let the
|
|
49
|
+
* second cover half of the first.
|
|
50
|
+
*/
|
|
51
|
+
readonly blend?: 'over' | 'sum';
|
|
52
|
+
/**
|
|
53
|
+
* A second mask added to one layer's weight, read where the layers are (the world, under a
|
|
54
|
+
* `projection`) times `repeat`, its red times `intensity`: sand drifting into the gravel's layer at
|
|
55
|
+
* a scale of its own. The ORM array's layer after the layers and any mask it carries.
|
|
56
|
+
*/
|
|
57
|
+
readonly addMask?: {
|
|
58
|
+
readonly layer: number;
|
|
59
|
+
readonly repeat: number;
|
|
60
|
+
readonly intensity: number;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* One layer weighted by how much the surface faces up rather than by its mask channel:
|
|
64
|
+
* `saturate(bias + sharpness × (up / 2 + 1/2))`, `up` the world normal's upward share before any
|
|
65
|
+
* normal map. Snow on the tops of rocks, moss on a ledge.
|
|
66
|
+
*/
|
|
67
|
+
readonly facing?: {
|
|
68
|
+
readonly layer: number;
|
|
69
|
+
readonly bias: number;
|
|
70
|
+
readonly sharpness: number;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* A normal map read at the mesh's own coordinates and tangents, the normal array's layer just past
|
|
74
|
+
* the layers, under the layers' blended normal: the large shapes of a rock or a cliff, which a
|
|
75
|
+
* layer repeating forty times across it cannot carry.
|
|
76
|
+
*/
|
|
77
|
+
readonly meshNormal?: boolean;
|
|
31
78
|
}
|
|
32
|
-
/** Floats `packSurfaceLayers` writes:
|
|
33
|
-
export declare const LAYER_FLOATS =
|
|
79
|
+
/** Floats `packSurfaceLayers` writes: four vectors. */
|
|
80
|
+
export declare const LAYER_FLOATS = 16;
|
|
34
81
|
/**
|
|
35
|
-
* `layers` into `out
|
|
36
|
-
*
|
|
37
|
-
*
|
|
82
|
+
* `layers` into `out`, four vectors. The first five floats are the repeats (1 for a layer not
|
|
83
|
+
* given), then how many layers there are, which one glows (−1 for none) and where the mask comes
|
|
84
|
+
* from (0 a map, 1 the ORM array, 2 the vertex colour). Then the blend (0 over, 1 sum), whether a
|
|
85
|
+
* mesh normal is laid under the layers, the added mask's layer (−1 for none) and repeat; its
|
|
86
|
+
* intensity; and the facing layer (−1 for none), its bias and its sharpness. Zeros, no layers, where
|
|
87
|
+
* there are none.
|
|
88
|
+
*
|
|
89
|
+
* A repeat that is not a positive number is 1, so a layer is never laid at a scale that collapses it
|
|
90
|
+
* to a point; a glow, an added mask or a facing layer naming a layer there is not is none, and the
|
|
91
|
+
* base, which no channel lays, is not one an added mask or the facing can name.
|
|
38
92
|
*/
|
|
39
93
|
export declare function packSurfaceLayers<Texture>(layers: SurfaceLayers<Texture> | null | undefined, out: Float32Array): void;
|
|
94
|
+
/** The texture a layered material's mask is, where it is one rather than another array or the vertices. */
|
|
95
|
+
export declare function layerMaskMap<Texture>(layers: SurfaceLayers<Texture> | null | undefined): Texture | null;
|
|
96
|
+
/**
|
|
97
|
+
* Whether a material's layers can be drawn beside its `modelMap`: always, unless the mask is a map,
|
|
98
|
+
* which is read where a model's map goes and so cannot share the slot with one — a lightmap's page
|
|
99
|
+
* among them. The backends draw such a material as one layer and say so once.
|
|
100
|
+
*/
|
|
101
|
+
export declare function layersFitBeside<Texture>(layers: SurfaceLayers<Texture>, modelMap: Texture | null | undefined): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* The projection a layered material is drawn with: a triplanar one is laid on the horizontal plane,
|
|
104
|
+
* at its own scale, since every layer's maps read three times over would be forty-five reads a pixel.
|
|
105
|
+
* Anything else as it was. Whether to say so is the caller's.
|
|
106
|
+
*/
|
|
107
|
+
export declare function layeredProjection<Texture>(projection: SurfaceProjection | null | undefined, layers: SurfaceLayers<Texture> | null | undefined): SurfaceProjection | null;
|
|
@@ -1,26 +1,82 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A material blended from layers by a mask: `SurfaceMaterial.layers`, and the two vectors
|
|
3
|
-
* `shaders/flat/layered.ts` reads it from.
|
|
4
|
-
*/
|
|
5
1
|
/** The most layers a material blends: a base and one for each of the mask's four channels. */
|
|
6
2
|
export const MAX_SURFACE_LAYERS = 5;
|
|
7
|
-
/** Floats `packSurfaceLayers` writes:
|
|
8
|
-
export const LAYER_FLOATS =
|
|
3
|
+
/** Floats `packSurfaceLayers` writes: four vectors. */
|
|
4
|
+
export const LAYER_FLOATS = 16;
|
|
5
|
+
/** Where the mask is read, as the fourth float of the second vector carries it. */
|
|
6
|
+
const MASK_SOURCE = { map: 0, orm: 1, vertex: 2 };
|
|
9
7
|
/**
|
|
10
|
-
* `layers` into `out
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* `layers` into `out`, four vectors. The first five floats are the repeats (1 for a layer not
|
|
9
|
+
* given), then how many layers there are, which one glows (−1 for none) and where the mask comes
|
|
10
|
+
* from (0 a map, 1 the ORM array, 2 the vertex colour). Then the blend (0 over, 1 sum), whether a
|
|
11
|
+
* mesh normal is laid under the layers, the added mask's layer (−1 for none) and repeat; its
|
|
12
|
+
* intensity; and the facing layer (−1 for none), its bias and its sharpness. Zeros, no layers, where
|
|
13
|
+
* there are none.
|
|
14
|
+
*
|
|
15
|
+
* A repeat that is not a positive number is 1, so a layer is never laid at a scale that collapses it
|
|
16
|
+
* to a point; a glow, an added mask or a facing layer naming a layer there is not is none, and the
|
|
17
|
+
* base, which no channel lays, is not one an added mask or the facing can name.
|
|
13
18
|
*/
|
|
14
19
|
export function packSurfaceLayers(layers, out) {
|
|
15
20
|
out.fill(0);
|
|
16
21
|
if (layers === null || layers === undefined || layers.repeats.length === 0)
|
|
17
22
|
return;
|
|
18
23
|
const count = Math.min(layers.repeats.length, MAX_SURFACE_LAYERS);
|
|
19
|
-
for (let i = 0; i < MAX_SURFACE_LAYERS; i++)
|
|
20
|
-
|
|
21
|
-
out[i] = Number.isFinite(repeat) && repeat > 0 ? repeat : 1;
|
|
22
|
-
}
|
|
24
|
+
for (let i = 0; i < MAX_SURFACE_LAYERS; i++)
|
|
25
|
+
out[i] = positive(i < count ? layers.repeats[i] : 1);
|
|
23
26
|
out[5] = count;
|
|
24
27
|
const glow = layers.emissiveLayer;
|
|
25
28
|
out[6] = glow !== undefined && Number.isInteger(glow) && glow >= 0 && glow < count ? glow : -1;
|
|
29
|
+
out[7] =
|
|
30
|
+
layers.mask === 'orm'
|
|
31
|
+
? MASK_SOURCE.orm
|
|
32
|
+
: layers.mask === 'vertex'
|
|
33
|
+
? MASK_SOURCE.vertex
|
|
34
|
+
: MASK_SOURCE.map;
|
|
35
|
+
out[8] = layers.blend === 'sum' ? 1 : 0;
|
|
36
|
+
out[9] = layers.meshNormal === true ? 1 : 0;
|
|
37
|
+
const added = layers.addMask;
|
|
38
|
+
const addLayer = laidLayer(added?.layer, count);
|
|
39
|
+
out[10] = addLayer;
|
|
40
|
+
out[11] = addLayer < 0 ? 0 : positive(added?.repeat);
|
|
41
|
+
out[12] = addLayer < 0 ? 0 : finite(added?.intensity);
|
|
42
|
+
const facing = layers.facing;
|
|
43
|
+
const faceLayer = laidLayer(facing?.layer, count);
|
|
44
|
+
out[13] = faceLayer;
|
|
45
|
+
out[14] = faceLayer < 0 ? 0 : finite(facing?.bias);
|
|
46
|
+
out[15] = faceLayer < 0 ? 0 : finite(facing?.sharpness);
|
|
47
|
+
}
|
|
48
|
+
/** The texture a layered material's mask is, where it is one rather than another array or the vertices. */
|
|
49
|
+
export function layerMaskMap(layers) {
|
|
50
|
+
const mask = layers?.mask;
|
|
51
|
+
return mask === undefined || mask === 'orm' || mask === 'vertex' ? null : mask;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Whether a material's layers can be drawn beside its `modelMap`: always, unless the mask is a map,
|
|
55
|
+
* which is read where a model's map goes and so cannot share the slot with one — a lightmap's page
|
|
56
|
+
* among them. The backends draw such a material as one layer and say so once.
|
|
57
|
+
*/
|
|
58
|
+
export function layersFitBeside(layers, modelMap) {
|
|
59
|
+
return modelMap === null || modelMap === undefined || layerMaskMap(layers) === null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The projection a layered material is drawn with: a triplanar one is laid on the horizontal plane,
|
|
63
|
+
* at its own scale, since every layer's maps read three times over would be forty-five reads a pixel.
|
|
64
|
+
* Anything else as it was. Whether to say so is the caller's.
|
|
65
|
+
*/
|
|
66
|
+
export function layeredProjection(projection, layers) {
|
|
67
|
+
if (projection === null || projection === undefined)
|
|
68
|
+
return null;
|
|
69
|
+
if (layers === null || layers === undefined || projection.kind !== 'triplanar')
|
|
70
|
+
return projection;
|
|
71
|
+
return { kind: 'planar', scale: projection.scale };
|
|
72
|
+
}
|
|
73
|
+
/** A layer an added mask or the facing may name: one a channel lays, 1 to `count − 1`, or −1. */
|
|
74
|
+
function laidLayer(layer, count) {
|
|
75
|
+
return layer !== undefined && Number.isInteger(layer) && layer >= 1 && layer < count ? layer : -1;
|
|
76
|
+
}
|
|
77
|
+
function positive(value) {
|
|
78
|
+
return value !== undefined && Number.isFinite(value) && value > 0 ? value : 1;
|
|
79
|
+
}
|
|
80
|
+
function finite(value) {
|
|
81
|
+
return value !== undefined && Number.isFinite(value) ? value : 0;
|
|
26
82
|
}
|
|
@@ -218,12 +218,21 @@ export interface SurfaceMaterial<Texture = SurfaceTexture> {
|
|
|
218
218
|
* light through by its own rule (`TranslucentMeshOptions.glass`).
|
|
219
219
|
*/
|
|
220
220
|
diffuseTransmission?: number;
|
|
221
|
+
/**
|
|
222
|
+
* The colour the light from behind takes through the surface, linear, in place of its own colour:
|
|
223
|
+
* a banner whose cloth glows a flat red behind a printed face, a leaf whose veins are not what the
|
|
224
|
+
* light through it shows. Absent, the surface's colour, as `diffuseTransmission` has always used.
|
|
225
|
+
* Each component is held at zero or above. Read only where `diffuseTransmission` lets light through.
|
|
226
|
+
*/
|
|
227
|
+
transmissionColor?: readonly [number, number, number] | null;
|
|
221
228
|
/**
|
|
222
229
|
* Up to five layers blended by a mask, each at its own repeat: `albedo`, `normal` and `orm` arrays
|
|
223
|
-
* whose layers are the material's, and a mask laying each over the ones before it
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
230
|
+
* whose layers are the material's, and a mask laying each over the ones before it or summing them,
|
|
231
|
+
* from a map, the ORM array or the vertex colour; under a `projection` the layers are placed by the
|
|
232
|
+
* world. Absent, the material is one layer, as every material was. **A lit switch**, compiled in the
|
|
233
|
+
* first time a material asks. A mask that is a map is read where `modelMap` goes, so such a
|
|
234
|
+
* material carries one or the other; a lightmapped one keeps its mask in its ORM array or its
|
|
235
|
+
* vertices. See `surfaceLayers.ts` for the rest and what it gives up.
|
|
227
236
|
*/
|
|
228
237
|
layers?: SurfaceLayers<Texture> | null;
|
|
229
238
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A thin surface's light from behind as the lit stage reads it: `SurfaceMaterial.diffuseTransmission`
|
|
3
|
+
* and `transmissionColor` into the four lanes of the glass tint an opaque material leaves unused.
|
|
4
|
+
*
|
|
5
|
+
* **The colour rides lanes glass owns**, because the lit stage's uniform rows are spent
|
|
6
|
+
* (`flat/preamble.ts`): a pane writes its own tint there for its draw, so whatever binds a pane
|
|
7
|
+
* puts this back after it, or the next draw of the material lets light through in the pane's colour.
|
|
8
|
+
* **−1 in the colour lanes means "the albedo"**, which is what a material naming no colour has
|
|
9
|
+
* always let through, so a scene that names none draws to the bit as it did.
|
|
10
|
+
*/
|
|
11
|
+
/** What a material lets through from behind and in which colour, as the glass tint carries it. */
|
|
12
|
+
export interface Transmission {
|
|
13
|
+
readonly diffuseTransmission?: number;
|
|
14
|
+
readonly transmissionColor?: readonly [number, number, number] | null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* `material` into `out`: the colour, or −1 three times where it names none, then the amount held
|
|
18
|
+
* to 0..1. A colour component below zero is held at zero, so a mistyped colour lets no light
|
|
19
|
+
* through rather than reading as "the albedo".
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveTransmission(material: Transmission | null, out: Float32Array): void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A thin surface's light from behind as the lit stage reads it: `SurfaceMaterial.diffuseTransmission`
|
|
3
|
+
* and `transmissionColor` into the four lanes of the glass tint an opaque material leaves unused.
|
|
4
|
+
*
|
|
5
|
+
* **The colour rides lanes glass owns**, because the lit stage's uniform rows are spent
|
|
6
|
+
* (`flat/preamble.ts`): a pane writes its own tint there for its draw, so whatever binds a pane
|
|
7
|
+
* puts this back after it, or the next draw of the material lets light through in the pane's colour.
|
|
8
|
+
* **−1 in the colour lanes means "the albedo"**, which is what a material naming no colour has
|
|
9
|
+
* always let through, so a scene that names none draws to the bit as it did.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* `material` into `out`: the colour, or −1 three times where it names none, then the amount held
|
|
13
|
+
* to 0..1. A colour component below zero is held at zero, so a mistyped colour lets no light
|
|
14
|
+
* through rather than reading as "the albedo".
|
|
15
|
+
*/
|
|
16
|
+
export function resolveTransmission(material, out) {
|
|
17
|
+
const colour = material?.transmissionColor ?? null;
|
|
18
|
+
for (let i = 0; i < 3; i++)
|
|
19
|
+
out[i] = colour === null ? -1 : Math.max(colour[i] ?? 0, 0) || 0;
|
|
20
|
+
out[3] = Math.min(Math.max(material?.diffuseTransmission ?? 0, 0), 1) || 0;
|
|
21
|
+
}
|
package/dist/ui/splash.d.ts
CHANGED
|
@@ -49,8 +49,9 @@ export declare const SPLASH_FADE_MS = 420;
|
|
|
49
49
|
/**
|
|
50
50
|
* When the badge gives the screen to the game. Two conditions and a cap, each for a named failure.
|
|
51
51
|
*
|
|
52
|
-
* - **The game has painted
|
|
53
|
-
*
|
|
52
|
+
* - **The game has painted**, and every promise it asked the badge to wait for has settled
|
|
53
|
+
* (`holdSplash`). Swapping before that shows the empty canvas, or the half-loaded first screen,
|
|
54
|
+
* the badge exists to cover.
|
|
54
55
|
* - **The minimum has passed.** A badge that vanishes the instant a fast machine paints is a
|
|
55
56
|
* flicker, and the machines that boot fastest are the ones a developer tests on.
|
|
56
57
|
* - **The cap overrides both**, and is checked first so that a minimum longer than the cap
|
|
@@ -75,7 +76,46 @@ export interface MountedSplash {
|
|
|
75
76
|
present(): void;
|
|
76
77
|
/** Take it down now, whatever the clock says. Idempotent. */
|
|
77
78
|
release(): void;
|
|
79
|
+
/** Keep it up until `until` settles as well, under `capMs`. False once it has left. */
|
|
80
|
+
holdUntil(until: PromiseLike<unknown>, capMs?: number): boolean;
|
|
78
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* What a badge waits for besides the clock: the game's first frame, and every promise the game asked
|
|
84
|
+
* it to wait for (`holdSplash`), under the cap the game named, the longest of several, or the engine's.
|
|
85
|
+
*
|
|
86
|
+
* **A hold settles whichever way its promise does.** A load that failed must not pin the logo: the
|
|
87
|
+
* game is shown, broken and visible, which is the cap's own argument. Pure apart from the promises,
|
|
88
|
+
* so the rule is testable in Node.
|
|
89
|
+
*/
|
|
90
|
+
export declare class SplashState {
|
|
91
|
+
/** Whether a frame of the game has reached the screen. */
|
|
92
|
+
painted: boolean;
|
|
93
|
+
private pending;
|
|
94
|
+
/** The longest cap a hold has named, or 0 for none. */
|
|
95
|
+
private asked;
|
|
96
|
+
/** The longest the badge may stay: the cap a hold named, the longest of several, or the engine's. */
|
|
97
|
+
get capMs(): number;
|
|
98
|
+
/** Wait for `until` too, under `capMs` where it names one; `settled` once it has. */
|
|
99
|
+
hold(until: PromiseLike<unknown>, capMs: number | undefined, settled: () => void): void;
|
|
100
|
+
/** Painted, with every hold settled: what `splashDecision` calls the content being ready. */
|
|
101
|
+
get ready(): boolean;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Keep the engine's badge up until `until` settles, as well as until the game's first frame: a game
|
|
105
|
+
* whose first screen loads for longer than that frame — a stage, figures and their pictures on a
|
|
106
|
+
* first visit — is seen whole rather than loading. **`capMs` is the game's own ceiling** on the
|
|
107
|
+
* badge in place of the engine's twenty seconds, longer for a slow first visit or shorter, so a load
|
|
108
|
+
* that never settles is still shown in the end; the longest of several holds' wins, and the minimum
|
|
109
|
+
* hold is unchanged.
|
|
110
|
+
*
|
|
111
|
+
* **Frames run while the game loads behind the plate**, since a load may need them, and are held
|
|
112
|
+
* once it has settled, for whatever is left of the minimum, as they are after a first frame with no
|
|
113
|
+
* hold. Returns whether there was a badge to hold: none in a packaged build, whose shell shows its
|
|
114
|
+
* own until the first frame, nor where `splash: false` or `?splash=0` turned it off.
|
|
115
|
+
*/
|
|
116
|
+
export declare function holdSplash(until: PromiseLike<unknown>, options?: {
|
|
117
|
+
capMs?: number;
|
|
118
|
+
}): boolean;
|
|
79
119
|
/**
|
|
80
120
|
* Put the badge up, and hand back the two verbs that take it down.
|
|
81
121
|
*
|
package/dist/ui/splash.js
CHANGED
|
@@ -80,8 +80,9 @@ export const SPLASH_FADE_MS = 420;
|
|
|
80
80
|
/**
|
|
81
81
|
* When the badge gives the screen to the game. Two conditions and a cap, each for a named failure.
|
|
82
82
|
*
|
|
83
|
-
* - **The game has painted
|
|
84
|
-
*
|
|
83
|
+
* - **The game has painted**, and every promise it asked the badge to wait for has settled
|
|
84
|
+
* (`holdSplash`). Swapping before that shows the empty canvas, or the half-loaded first screen,
|
|
85
|
+
* the badge exists to cover.
|
|
85
86
|
* - **The minimum has passed.** A badge that vanishes the instant a fast machine paints is a
|
|
86
87
|
* flicker, and the machines that boot fastest are the ones a developer tests on.
|
|
87
88
|
* - **The cap overrides both**, and is checked first so that a minimum longer than the cap
|
|
@@ -99,6 +100,58 @@ export function splashDecision(elapsedMs, minMs, contentReady, capMs = SPLASH_HA
|
|
|
99
100
|
return 'hold';
|
|
100
101
|
return elapsedMs >= minMs ? 'swap' : 'hold';
|
|
101
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* What a badge waits for besides the clock: the game's first frame, and every promise the game asked
|
|
105
|
+
* it to wait for (`holdSplash`), under the cap the game named, the longest of several, or the engine's.
|
|
106
|
+
*
|
|
107
|
+
* **A hold settles whichever way its promise does.** A load that failed must not pin the logo: the
|
|
108
|
+
* game is shown, broken and visible, which is the cap's own argument. Pure apart from the promises,
|
|
109
|
+
* so the rule is testable in Node.
|
|
110
|
+
*/
|
|
111
|
+
export class SplashState {
|
|
112
|
+
/** Whether a frame of the game has reached the screen. */
|
|
113
|
+
painted = false;
|
|
114
|
+
pending = 0;
|
|
115
|
+
/** The longest cap a hold has named, or 0 for none. */
|
|
116
|
+
asked = 0;
|
|
117
|
+
/** The longest the badge may stay: the cap a hold named, the longest of several, or the engine's. */
|
|
118
|
+
get capMs() {
|
|
119
|
+
return this.asked > 0 ? this.asked : SPLASH_HARD_CAP_MS;
|
|
120
|
+
}
|
|
121
|
+
/** Wait for `until` too, under `capMs` where it names one; `settled` once it has. */
|
|
122
|
+
hold(until, capMs, settled) {
|
|
123
|
+
this.pending += 1;
|
|
124
|
+
if (capMs !== undefined && Number.isFinite(capMs) && capMs > this.asked)
|
|
125
|
+
this.asked = capMs;
|
|
126
|
+
const done = () => {
|
|
127
|
+
this.pending -= 1;
|
|
128
|
+
settled();
|
|
129
|
+
};
|
|
130
|
+
Promise.resolve(until).then(done, done);
|
|
131
|
+
}
|
|
132
|
+
/** Painted, with every hold settled: what `splashDecision` calls the content being ready. */
|
|
133
|
+
get ready() {
|
|
134
|
+
return this.painted && this.pending === 0;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** The badge on the page, which `holdSplash` holds. One screen, so one badge: see `bootGate.ts`. */
|
|
138
|
+
let mounted = null;
|
|
139
|
+
/**
|
|
140
|
+
* Keep the engine's badge up until `until` settles, as well as until the game's first frame: a game
|
|
141
|
+
* whose first screen loads for longer than that frame — a stage, figures and their pictures on a
|
|
142
|
+
* first visit — is seen whole rather than loading. **`capMs` is the game's own ceiling** on the
|
|
143
|
+
* badge in place of the engine's twenty seconds, longer for a slow first visit or shorter, so a load
|
|
144
|
+
* that never settles is still shown in the end; the longest of several holds' wins, and the minimum
|
|
145
|
+
* hold is unchanged.
|
|
146
|
+
*
|
|
147
|
+
* **Frames run while the game loads behind the plate**, since a load may need them, and are held
|
|
148
|
+
* once it has settled, for whatever is left of the minimum, as they are after a first frame with no
|
|
149
|
+
* hold. Returns whether there was a badge to hold: none in a packaged build, whose shell shows its
|
|
150
|
+
* own until the first frame, nor where `splash: false` or `?splash=0` turned it off.
|
|
151
|
+
*/
|
|
152
|
+
export function holdSplash(until, options = {}) {
|
|
153
|
+
return mounted?.holdUntil(until, options.capMs) ?? false;
|
|
154
|
+
}
|
|
102
155
|
/**
|
|
103
156
|
* The badge markup. One constant string, interpolated into nothing.
|
|
104
157
|
*
|
|
@@ -228,17 +281,18 @@ export function mountSplash(options = {}) {
|
|
|
228
281
|
the document and already styled; nothing here needs to be undone. */
|
|
229
282
|
}
|
|
230
283
|
}
|
|
231
|
-
|
|
284
|
+
const state = new SplashState();
|
|
232
285
|
let leaving = false;
|
|
233
286
|
let timer = null;
|
|
234
287
|
/*
|
|
235
|
-
* **Freeze the game the moment it has proved it can draw.**
|
|
288
|
+
* **Freeze the game the moment it has proved it can draw and has loaded.**
|
|
236
289
|
*
|
|
237
|
-
*
|
|
238
|
-
* game would otherwise spend running behind an opaque screen.
|
|
290
|
+
* Ready is the first `endFrame` with every hold settled (`SplashState`), and everything after it
|
|
291
|
+
* until the plate leaves is time the game would otherwise spend running behind an opaque screen.
|
|
292
|
+
* Before it, frames run: a load the game is holding the badge for may need them. `bootGate` is read by `startLoop`
|
|
239
293
|
* and named by neither of them; see its own comment for why the state is shared that way.
|
|
240
294
|
*/
|
|
241
|
-
const openGate = holdFrames(() =>
|
|
295
|
+
const openGate = holdFrames(() => state.ready && !leaving);
|
|
242
296
|
const remove = () => {
|
|
243
297
|
/* Leaving the top layer is what `hidePopover` is for. `remove()` alone takes the element out
|
|
244
298
|
of the document, and an element removed while shown is not a state worth relying on. */
|
|
@@ -256,6 +310,8 @@ export function mountSplash(options = {}) {
|
|
|
256
310
|
if (leaving)
|
|
257
311
|
return;
|
|
258
312
|
leaving = true;
|
|
313
|
+
if (mounted === badge)
|
|
314
|
+
mounted = null;
|
|
259
315
|
/* Before the fade, not after it: the game resumes as the plate becomes transparent, so what
|
|
260
316
|
shows through is a running frame rather than the still one it was frozen on. */
|
|
261
317
|
openGate();
|
|
@@ -278,23 +334,32 @@ export function mountSplash(options = {}) {
|
|
|
278
334
|
if (leaving)
|
|
279
335
|
return;
|
|
280
336
|
const elapsed = performance.now() - started;
|
|
281
|
-
if (splashDecision(elapsed, minMs,
|
|
337
|
+
if (splashDecision(elapsed, minMs, state.ready, state.capMs) === 'swap') {
|
|
282
338
|
release();
|
|
283
339
|
return;
|
|
284
340
|
}
|
|
285
341
|
if (timer !== null)
|
|
286
342
|
clearTimeout(timer);
|
|
287
|
-
const next =
|
|
343
|
+
const next = state.ready ? minMs - elapsed : state.capMs - elapsed;
|
|
288
344
|
timer = setTimeout(settle, Math.max(0, next));
|
|
289
345
|
};
|
|
290
|
-
|
|
291
|
-
return {
|
|
346
|
+
const badge = {
|
|
292
347
|
present() {
|
|
293
|
-
if (painted || leaving)
|
|
348
|
+
if (state.painted || leaving)
|
|
294
349
|
return;
|
|
295
|
-
painted = true;
|
|
350
|
+
state.painted = true;
|
|
296
351
|
settle();
|
|
297
352
|
},
|
|
298
353
|
release,
|
|
354
|
+
holdUntil(until, capMs) {
|
|
355
|
+
if (leaving)
|
|
356
|
+
return false;
|
|
357
|
+
state.hold(until, capMs, settle);
|
|
358
|
+
settle();
|
|
359
|
+
return true;
|
|
360
|
+
},
|
|
299
361
|
};
|
|
362
|
+
mounted = badge;
|
|
363
|
+
settle();
|
|
364
|
+
return badge;
|
|
300
365
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/core",
|
|
3
|
-
"version": "4.11.
|
|
3
|
+
"version": "4.11.1",
|
|
4
4
|
"description": "The runtime of DriftEngine, a WebGPU game engine with a WebGL2 fallback: renderer, loop, input, cameras, scene graph and physics",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
],
|
|
30
30
|
"sideEffects": false,
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@driftengine/drft": "4.11.
|
|
33
|
-
"@driftengine/physics": "4.11.
|
|
32
|
+
"@driftengine/drft": "4.11.1",
|
|
33
|
+
"@driftengine/physics": "4.11.1",
|
|
34
34
|
"gl-matrix": "^3.4.3"
|
|
35
35
|
},
|
|
36
36
|
"author": "Drift Technologies",
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
"prepack": "node ../../scripts/build.mjs @driftengine/core"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@driftengine/nav": "4.11.
|
|
71
|
-
"@driftengine/network": "4.11.
|
|
72
|
-
"@driftengine/texture": "4.11.
|
|
70
|
+
"@driftengine/nav": "4.11.1",
|
|
71
|
+
"@driftengine/network": "4.11.1",
|
|
72
|
+
"@driftengine/texture": "4.11.1"
|
|
73
73
|
}
|
|
74
74
|
}
|
package/src/index.ts
CHANGED
|
@@ -481,6 +481,7 @@ export type {
|
|
|
481
481
|
LineHandle,
|
|
482
482
|
MeshHandle,
|
|
483
483
|
MeshOptions,
|
|
484
|
+
PrepareOptions,
|
|
484
485
|
RenderBackend,
|
|
485
486
|
RendererApi,
|
|
486
487
|
ParticleHandle,
|
|
@@ -993,6 +994,7 @@ export {
|
|
|
993
994
|
SPLASH_HARD_CAP_MS,
|
|
994
995
|
SPLASH_MIN_MS,
|
|
995
996
|
forcedSplash,
|
|
997
|
+
holdSplash,
|
|
996
998
|
mountSplash,
|
|
997
999
|
splashDecision,
|
|
998
1000
|
splashWanted,
|
|
@@ -113,7 +113,8 @@ export class AmbientOcclusionPass {
|
|
|
113
113
|
* by, or null if this pass could not run, which the caller must read as "no occlusion"
|
|
114
114
|
* rather than as black.
|
|
115
115
|
*
|
|
116
|
-
* `invProjection` is column-major, as `gl-matrix` produces it.
|
|
116
|
+
* `invProjection` is column-major, as `gl-matrix` produces it. `fade` is `uFade`, where the
|
|
117
|
+
* occlusion fades out with distance (`occlusionFade.ts`).
|
|
117
118
|
*/
|
|
118
119
|
run(
|
|
119
120
|
depth: WebGLTexture,
|
|
@@ -122,6 +123,7 @@ export class AmbientOcclusionPass {
|
|
|
122
123
|
radius: number,
|
|
123
124
|
projScale: Float32Array,
|
|
124
125
|
invProjection: Float32Array,
|
|
126
|
+
fade: Float32Array,
|
|
125
127
|
): WebGLTexture | null {
|
|
126
128
|
if (!this.ensureSize(width, height)) return null;
|
|
127
129
|
const { gl } = this;
|
|
@@ -142,6 +144,7 @@ export class AmbientOcclusionPass {
|
|
|
142
144
|
gl.uniform1i(this.estimateUniforms['uDepth'] ?? null, 0);
|
|
143
145
|
gl.uniform2fv(this.estimateUniforms['uProjScale'] ?? null, projScale);
|
|
144
146
|
gl.uniform1f(this.estimateUniforms['uRadius'] ?? null, radius);
|
|
147
|
+
gl.uniform2fv(this.estimateUniforms['uFade'] ?? null, fade);
|
|
145
148
|
gl.uniformMatrix4fv(this.estimateUniforms['uInvProjection'] ?? null, false, invProjection);
|
|
146
149
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
147
150
|
|
|
@@ -235,6 +235,15 @@ export interface SurfaceTextureHandle {
|
|
|
235
235
|
* `WebGLBuffer` in a private field, so no WebGPU object can satisfy it however identical its
|
|
236
236
|
* shape. It is not meant to be read; it is meant to be enforced.
|
|
237
237
|
*/
|
|
238
|
+
/** Which of a draw's pipelines `prepareMesh` and `prepareInstanced` compile beside the opaque ones. */
|
|
239
|
+
export interface PrepareOptions {
|
|
240
|
+
/**
|
|
241
|
+
* The blended ones too, for a draw made with `drawTranslucentMesh` or `drawTranslucentInstanced`.
|
|
242
|
+
* Off by default: an opaque surface would pay a compile it never draws with.
|
|
243
|
+
*/
|
|
244
|
+
readonly translucent?: boolean;
|
|
245
|
+
}
|
|
246
|
+
|
|
238
247
|
/** What a mesh needs to be told at creation that its geometry cannot say. */
|
|
239
248
|
export interface MeshOptions {
|
|
240
249
|
/**
|
|
@@ -312,6 +321,8 @@ export type RendererApi = Omit<
|
|
|
312
321
|
| 'disposeSurfaceTexture'
|
|
313
322
|
| 'setSurfaceTexture'
|
|
314
323
|
| 'setMaterial'
|
|
324
|
+
| 'prepareMesh'
|
|
325
|
+
| 'prepareInstanced'
|
|
315
326
|
| 'setSurfaceOverlay'
|
|
316
327
|
| 'createClothBinding'
|
|
317
328
|
| 'createClothParticles'
|
|
@@ -455,6 +466,26 @@ export type RendererApi = Omit<
|
|
|
455
466
|
* setter each — and `setSurfaceTexture` is now a wrapper over it.
|
|
456
467
|
*/
|
|
457
468
|
setMaterial(material: SurfaceMaterial<SurfaceTextureHandle> | null): void;
|
|
469
|
+
/**
|
|
470
|
+
* Compile, off the main thread, every pipeline a draw of `mesh` in `material` takes that making
|
|
471
|
+
* the mesh did not — a two-sided, cut-out or shaded-by-a-model variant, a material's lit switches,
|
|
472
|
+
* a skin's halves — so the first frame that draws it waits on none; `ready()` resolves once they
|
|
473
|
+
* have. For `pipelineCompile: 'skip'` above all, where a draw whose pipeline is new is left out
|
|
474
|
+
* until it lands: a figure brought on screen whole rather than without its face for a few frames.
|
|
475
|
+
* A rigged mesh is prepared as drawn with a palette, and one with targets both with weights and
|
|
476
|
+
* without. Call it between frames: it sets `material` to read it and leaves none set after.
|
|
477
|
+
*/
|
|
478
|
+
prepareMesh(
|
|
479
|
+
mesh: MeshHandle,
|
|
480
|
+
material: SurfaceMaterial<SurfaceTextureHandle> | null,
|
|
481
|
+
options?: PrepareOptions,
|
|
482
|
+
): void;
|
|
483
|
+
/** `prepareMesh` for an instanced batch drawn in `material`, its own pipelines being its own. */
|
|
484
|
+
prepareInstanced(
|
|
485
|
+
batch: InstancedHandle,
|
|
486
|
+
material: SurfaceMaterial<SurfaceTextureHandle> | null,
|
|
487
|
+
options?: PrepareOptions,
|
|
488
|
+
): void;
|
|
458
489
|
|
|
459
490
|
/**
|
|
460
491
|
* A 3D text object: one string, drawn as instanced glyph cubes over the scene.
|