@driftengine/core 4.11.1 → 4.12.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/CHANGELOG.json +36 -0
- package/README.md +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/render/backend/webgl2/renderer.d.ts +16 -1
- package/dist/render/backend/webgl2/renderer.js +41 -2
- package/dist/render/backend/webgpu/renderer.d.ts +2 -0
- package/dist/render/backend/webgpu/renderer.js +9 -0
- package/dist/render/shaders/flat/areaLight.d.ts +1 -1
- package/dist/render/shaders/flat/areaLight.js +1 -1
- package/dist/render/shaders/flat/glassTint.d.ts +1 -1
- package/dist/render/shaders/flat/glassTint.js +1 -0
- package/dist/render/shaders/flat/index.d.ts +5 -0
- package/dist/render/shaders/flat/index.js +1 -1
- package/dist/render/shaders/flat/layerLooks.d.ts +16 -0
- package/dist/render/shaders/flat/layerLooks.js +116 -0
- package/dist/render/shaders/flat/layered.d.ts +6 -4
- package/dist/render/shaders/flat/layered.js +62 -13
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +7 -2
- package/dist/render/shaders/flat/models.js +1 -1
- package/dist/render/shaders/flat/preamble.js +6 -0
- package/dist/render/shaders/generated/flat.wgsl.d.ts +144 -16
- package/dist/render/shaders/generated/flat.wgsl.js +183 -55
- package/dist/render/surfaceLayerLooks.d.ts +80 -0
- package/dist/render/surfaceLayerLooks.js +83 -0
- package/dist/render/surfaceLayers.d.ts +47 -6
- package/dist/render/surfaceLayers.js +5 -1
- package/package.json +6 -6
- package/scripts/browser.d.mts +5 -0
- package/scripts/browser.mjs +68 -14
- package/scripts/look.d.mts +81 -0
- package/scripts/look.mjs +380 -0
- package/src/index.ts +2 -0
- package/src/render/backend/webgl2/renderer.ts +40 -2
- package/src/render/backend/webgpu/renderer.ts +8 -0
- package/src/render/shaders/flat/areaLight.ts +1 -1
- package/src/render/shaders/flat/glassTint.ts +3 -1
- package/src/render/shaders/flat/index.ts +6 -1
- package/src/render/shaders/flat/layerLooks.ts +116 -0
- package/src/render/shaders/flat/layered.ts +63 -13
- package/src/render/shaders/flat/main.ts +7 -2
- package/src/render/shaders/flat/models.ts +1 -1
- package/src/render/shaders/flat/preamble.ts +6 -0
- package/src/render/shaders/generated/flat.wgsl.ts +183 -55
- package/src/render/surfaceLayerLooks.ts +155 -0
- package/src/render/surfaceLayers.ts +52 -7
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which layer of a layered material's arrays each of its layers reads, and how each looks: the
|
|
3
|
+
* fifteen vectors `shaders/flat/layerLooks.ts` reads, packed from `SurfaceMaterial.layers`.
|
|
4
|
+
*
|
|
5
|
+
* **Why a material needs these at all.** Layers read from the material's own arrays in order, so
|
|
6
|
+
* a texture worn by fifty materials in fifty tints had to be baked fifty times, each with its tint,
|
|
7
|
+
* its roughness range and its normal strength in the pixels. Picked by index from arrays the
|
|
8
|
+
* materials share, and given their look as numbers, each texture is held once: a set of layered
|
|
9
|
+
* materials built from a few textures costs those textures rather than one copy per material.
|
|
10
|
+
*
|
|
11
|
+
* **What it gives up**: fifteen fragment uniform vectors, so it is a lit switch of its own,
|
|
12
|
+
* `LAYER_LOOKS`, which a device short of vectors refuses while still drawing the layers; a layer's
|
|
13
|
+
* look is a handful of numbers, not a graph; and every layer is still read wherever any shows, as
|
|
14
|
+
* `surfaceLayers.ts` says, now from arrays that may be large.
|
|
15
|
+
*/
|
|
16
|
+
import type { Vec3 } from '../math/color.ts';
|
|
17
|
+
import type { SurfaceLayers } from './surfaceLayers.ts';
|
|
18
|
+
/**
|
|
19
|
+
* One layer's own look, applied to what it reads before the layers are blended. Every field is the
|
|
20
|
+
* identity where it is absent, so a material names only what differs.
|
|
21
|
+
*/
|
|
22
|
+
export interface SurfaceLayerLook {
|
|
23
|
+
/** Multiplies the layer's colour, linear. Above 1 brightens it. Absent: `[1, 1, 1]`. */
|
|
24
|
+
readonly tint?: Vec3;
|
|
25
|
+
/** The roughness the ORM's green channel is spread over, from its 0 to its 1. Absent: `[0, 1]`. */
|
|
26
|
+
readonly roughness?: readonly [number, number];
|
|
27
|
+
/** The metalness the ORM's blue channel is spread over, the same way. Absent: `[0, 1]`. */
|
|
28
|
+
readonly metalness?: readonly [number, number];
|
|
29
|
+
/**
|
|
30
|
+
* How far the layer's normal map bends the surface: its tangent-space x and y times this, before
|
|
31
|
+
* the layers' normals are blended. 0 is flat, 1 the map as it is. Absent: 1.
|
|
32
|
+
*/
|
|
33
|
+
readonly normalStrength?: number;
|
|
34
|
+
/**
|
|
35
|
+
* The layer's specular, in the units a mesh's vertices carry theirs: how strong a highlight is,
|
|
36
|
+
* and under `physicalSpecular` the reflectance head-on, `0.08 × specular`. Blended across the
|
|
37
|
+
* layers as their colours are. Absent: the surface's own, as its vertices give it.
|
|
38
|
+
*/
|
|
39
|
+
readonly specular?: number;
|
|
40
|
+
}
|
|
41
|
+
/** What an occlusion read at the mesh's own coordinates darkens, and how far. */
|
|
42
|
+
export interface SurfaceMeshOcclusion {
|
|
43
|
+
/** 0 to 1: the surface times `mix(1, occlusion, strength)`. Absent: 1. */
|
|
44
|
+
readonly strength?: number;
|
|
45
|
+
/**
|
|
46
|
+
* `'color'`, the default, darkens the blended colour, and so every light on it, the sun's too.
|
|
47
|
+
* `'ambient'` darkens the ambient light alone, sky, ground and probe, as an ambient occlusion
|
|
48
|
+
* does, leaving the sun, the lamps and the reflections as they were.
|
|
49
|
+
*/
|
|
50
|
+
readonly into?: 'color' | 'ambient';
|
|
51
|
+
/** The range the occlusion's red is spread over, from its 0 to its 1. Absent: `[0, 1]`. */
|
|
52
|
+
readonly range?: readonly [number, number];
|
|
53
|
+
}
|
|
54
|
+
/** Floats `packLayerLooks` writes: fifteen vectors. */
|
|
55
|
+
export declare const LOOK_FLOATS = 60;
|
|
56
|
+
/**
|
|
57
|
+
* Whether a material asks for anything the `LAYER_LOOKS` switch carries: layers picked by index,
|
|
58
|
+
* extra maps placed somewhere other than just past the layers, a look, a repeat that is a pair, or
|
|
59
|
+
* a mesh occlusion.
|
|
60
|
+
*/
|
|
61
|
+
export declare function asksLayerLooks<Texture>(layers: SurfaceLayers<Texture> | null | undefined): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* `layers` into `out`, fifteen vectors, the identity wherever a material says nothing — which is
|
|
64
|
+
* also what a layered material that asks for none of this is drawn with once another has switched
|
|
65
|
+
* `LAYER_LOOKS` on, so it must draw exactly as it did before.
|
|
66
|
+
*
|
|
67
|
+
* - **0 to 7**: the array layer each layer reads, base first (layer `i` where not given); where the
|
|
68
|
+
* maps beyond the layers start (the layer count where not given); the mesh occlusion's strength,
|
|
69
|
+
* and what it darkens, 0 the colour and 1 the ambient light.
|
|
70
|
+
* - **8 to 47**: two vectors a layer, base first: its tint and its normal strength, then its
|
|
71
|
+
* roughness range and its metalness range.
|
|
72
|
+
* - **48 to 59**: each layer's specular (−1 for the surface's own), each layer's repeat down (its
|
|
73
|
+
* repeat across where it gave one number), and the range the occlusion is spread over.
|
|
74
|
+
*
|
|
75
|
+
* An array layer that is not a whole number at or above 0 reads layer `i`; a start that is not one
|
|
76
|
+
* is the count; a strength is held to 0..1; a tint channel, a normal strength or a specular that is
|
|
77
|
+
* not a number at or above 0 is the identity; a repeat down that is not a positive number is the
|
|
78
|
+
* repeat across; a range end that is not a number is the identity's end.
|
|
79
|
+
*/
|
|
80
|
+
export declare function packLayerLooks<Texture>(layers: SurfaceLayers<Texture> | null | undefined, out: Float32Array): void;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { MAX_SURFACE_LAYERS, across } from './surfaceLayers.js';
|
|
2
|
+
/** Floats `packLayerLooks` writes: fifteen vectors. */
|
|
3
|
+
export const LOOK_FLOATS = 60;
|
|
4
|
+
/** The specular a layer is packed with when it names none: the surface's own, as its vertices give it. */
|
|
5
|
+
const OWN_SPECULAR = -1;
|
|
6
|
+
/**
|
|
7
|
+
* Whether a material asks for anything the `LAYER_LOOKS` switch carries: layers picked by index,
|
|
8
|
+
* extra maps placed somewhere other than just past the layers, a look, a repeat that is a pair, or
|
|
9
|
+
* a mesh occlusion.
|
|
10
|
+
*/
|
|
11
|
+
export function asksLayerLooks(layers) {
|
|
12
|
+
if (layers === null || layers === undefined)
|
|
13
|
+
return false;
|
|
14
|
+
return (layers.arrayLayers !== undefined ||
|
|
15
|
+
layers.extrasAt !== undefined ||
|
|
16
|
+
layers.looks !== undefined ||
|
|
17
|
+
layers.repeats.some((repeat) => typeof repeat !== 'number') ||
|
|
18
|
+
occlusionOf(layers.meshOcclusion).strength > 0);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* `layers` into `out`, fifteen vectors, the identity wherever a material says nothing — which is
|
|
22
|
+
* also what a layered material that asks for none of this is drawn with once another has switched
|
|
23
|
+
* `LAYER_LOOKS` on, so it must draw exactly as it did before.
|
|
24
|
+
*
|
|
25
|
+
* - **0 to 7**: the array layer each layer reads, base first (layer `i` where not given); where the
|
|
26
|
+
* maps beyond the layers start (the layer count where not given); the mesh occlusion's strength,
|
|
27
|
+
* and what it darkens, 0 the colour and 1 the ambient light.
|
|
28
|
+
* - **8 to 47**: two vectors a layer, base first: its tint and its normal strength, then its
|
|
29
|
+
* roughness range and its metalness range.
|
|
30
|
+
* - **48 to 59**: each layer's specular (−1 for the surface's own), each layer's repeat down (its
|
|
31
|
+
* repeat across where it gave one number), and the range the occlusion is spread over.
|
|
32
|
+
*
|
|
33
|
+
* An array layer that is not a whole number at or above 0 reads layer `i`; a start that is not one
|
|
34
|
+
* is the count; a strength is held to 0..1; a tint channel, a normal strength or a specular that is
|
|
35
|
+
* not a number at or above 0 is the identity; a repeat down that is not a positive number is the
|
|
36
|
+
* repeat across; a range end that is not a number is the identity's end.
|
|
37
|
+
*/
|
|
38
|
+
export function packLayerLooks(layers, out) {
|
|
39
|
+
const count = Math.min(layers?.repeats.length ?? 0, MAX_SURFACE_LAYERS);
|
|
40
|
+
const picks = layers?.arrayLayers;
|
|
41
|
+
for (let i = 0; i < MAX_SURFACE_LAYERS; i++)
|
|
42
|
+
out[i] = arrayLayer(picks?.[i], i);
|
|
43
|
+
out[5] = arrayLayer(layers?.extrasAt, count);
|
|
44
|
+
const occlusion = occlusionOf(layers?.meshOcclusion);
|
|
45
|
+
out[6] = occlusion.strength;
|
|
46
|
+
out[7] = occlusion.into === 'ambient' ? 1 : 0;
|
|
47
|
+
for (let i = 0; i < MAX_SURFACE_LAYERS; i++) {
|
|
48
|
+
const look = layers?.looks?.[i] ?? null;
|
|
49
|
+
const at = 8 + i * 8;
|
|
50
|
+
out[at] = atLeastZero(look?.tint?.[0], 1);
|
|
51
|
+
out[at + 1] = atLeastZero(look?.tint?.[1], 1);
|
|
52
|
+
out[at + 2] = atLeastZero(look?.tint?.[2], 1);
|
|
53
|
+
out[at + 3] = atLeastZero(look?.normalStrength, 1);
|
|
54
|
+
out[at + 4] = finiteOr(look?.roughness?.[0], 0);
|
|
55
|
+
out[at + 5] = finiteOr(look?.roughness?.[1], 1);
|
|
56
|
+
out[at + 6] = finiteOr(look?.metalness?.[0], 0);
|
|
57
|
+
out[at + 7] = finiteOr(look?.metalness?.[1], 1);
|
|
58
|
+
out[48 + i] = atLeastZero(look?.specular, OWN_SPECULAR);
|
|
59
|
+
const repeat = layers?.repeats[i];
|
|
60
|
+
const acrossIt = positiveOr(across(repeat), 1);
|
|
61
|
+
out[53 + i] = typeof repeat === 'object' ? positiveOr(repeat[1], acrossIt) : acrossIt;
|
|
62
|
+
}
|
|
63
|
+
out[58] = finiteOr(occlusion.range?.[0], 0);
|
|
64
|
+
out[59] = finiteOr(occlusion.range?.[1], 1);
|
|
65
|
+
}
|
|
66
|
+
/** The number form of `meshOcclusion` as the object form, the strength held to 0..1. */
|
|
67
|
+
function occlusionOf(occlusion) {
|
|
68
|
+
const given = typeof occlusion === 'number' ? { strength: occlusion } : (occlusion ?? { strength: 0 });
|
|
69
|
+
const strength = given.strength ?? 1;
|
|
70
|
+
return { ...given, strength: Number.isFinite(strength) ? Math.min(Math.max(strength, 0), 1) : 0 };
|
|
71
|
+
}
|
|
72
|
+
function arrayLayer(value, otherwise) {
|
|
73
|
+
return value !== undefined && Number.isInteger(value) && value >= 0 ? value : otherwise;
|
|
74
|
+
}
|
|
75
|
+
function atLeastZero(value, otherwise) {
|
|
76
|
+
return value !== undefined && Number.isFinite(value) && value >= 0 ? value : otherwise;
|
|
77
|
+
}
|
|
78
|
+
function positiveOr(value, otherwise) {
|
|
79
|
+
return value !== undefined && Number.isFinite(value) && value > 0 ? value : otherwise;
|
|
80
|
+
}
|
|
81
|
+
function finiteOr(value, otherwise) {
|
|
82
|
+
return value !== undefined && Number.isFinite(value) ? value : otherwise;
|
|
83
|
+
}
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
* A material blended from layers by a mask: `SurfaceMaterial.layers`, the four vectors
|
|
3
3
|
* `shaders/flat/layered.ts` reads it from, and the decisions both backends take about it.
|
|
4
4
|
*/
|
|
5
|
+
import type { SurfaceLayerLook, SurfaceMeshOcclusion } from './surfaceLayerLooks.ts';
|
|
5
6
|
import type { SurfaceProjection } from './surfaceProjection.ts';
|
|
6
7
|
/** The most layers a material blends: a base and one for each of the mask's four channels. */
|
|
7
8
|
export declare const MAX_SURFACE_LAYERS = 5;
|
|
8
9
|
/**
|
|
9
10
|
* Up to five layers, each its own colour, normal and roughness at its own repeat, blended by a mask.
|
|
10
11
|
*
|
|
11
|
-
* **Layer `i` is layer `i` of the material's arrays
|
|
12
|
-
* whose layers are the material's layers
|
|
12
|
+
* **Layer `i` is layer `i` of the material's arrays**, unless `arrayLayers` picks another: `albedo`,
|
|
13
|
+
* `normal` and `orm` each an array whose layers are the material's layers, read at the mesh's
|
|
14
|
+
* coordinates times
|
|
13
15
|
* `repeats[i]`, so rock can repeat forty times across a cliff while the moss on it repeats three.
|
|
14
16
|
* Where the material has a `projection`, the layers are read at the world's horizontal position
|
|
15
17
|
* instead, so a repeat is so many a metre (with the projection's own scale at 1). **The mask** is
|
|
@@ -21,8 +23,13 @@ export declare const MAX_SURFACE_LAYERS = 5;
|
|
|
21
23
|
* **Maps beyond the layers ride the material's own arrays, after its layers**, so they take no
|
|
22
24
|
* texture binding of their own — the lit stage has none to spare: a mask the ORM array carries
|
|
23
25
|
* (`mask: 'orm'`) is the ORM array's layer just past the layers, an `addMask` the one after that,
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
+
* a `meshOcclusion` the one after any of those, and `meshNormal` the normal array's layer just past
|
|
27
|
+
* the layers. `extrasAt` moves where "just past" is, for materials that share arrays. Each shares
|
|
28
|
+
* its array's size and format, so a four-channel mask wants an array that stores four.
|
|
29
|
+
*
|
|
30
|
+
* **Shared arrays and a look of each layer's own** — `arrayLayers`, `extrasAt`, `looks` and
|
|
31
|
+
* `meshOcclusion` — are a lit switch of their own, `LAYER_LOOKS`, and `surfaceLayerLooks.ts` says
|
|
32
|
+
* what they cost.
|
|
26
33
|
*
|
|
27
34
|
* **What it gives up**: every layer's maps are read wherever any shows, three reads a layer, so five
|
|
28
35
|
* layers are fifteen reads where one material is three; layers share an array's size and format; a
|
|
@@ -38,8 +45,12 @@ export interface SurfaceLayers<Texture> {
|
|
|
38
45
|
* vertex colour's red, green and blue, which then weights the layers instead of tinting them.
|
|
39
46
|
*/
|
|
40
47
|
readonly mask: Texture | 'orm' | 'vertex';
|
|
41
|
-
/**
|
|
42
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Each layer's repeats across the mesh's coordinates, base first: one to five of them. A pair is a
|
|
50
|
+
* repeat across and a repeat down apart, moss stretched along a trunk; a pair is drawn through the
|
|
51
|
+
* `LAYER_LOOKS` switch, which a single number does not need.
|
|
52
|
+
*/
|
|
53
|
+
readonly repeats: readonly (number | readonly [number, number])[];
|
|
43
54
|
/** The layer the emissive map glows on, 0 to 4. Absent: everywhere, as on any material. */
|
|
44
55
|
readonly emissiveLayer?: number;
|
|
45
56
|
/**
|
|
@@ -75,6 +86,34 @@ export interface SurfaceLayers<Texture> {
|
|
|
75
86
|
* layer repeating forty times across it cannot carry.
|
|
76
87
|
*/
|
|
77
88
|
readonly meshNormal?: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Which layer of the material's albedo, normal and ORM arrays each layer reads, base first, so
|
|
91
|
+
* many materials can share one array of each kind and hold every texture once. Absent, or past
|
|
92
|
+
* its end, layer `i` reads layer `i`. The same index reads all three arrays, so a texture's three
|
|
93
|
+
* maps sit at the same layer of each.
|
|
94
|
+
*/
|
|
95
|
+
readonly arrayLayers?: readonly number[];
|
|
96
|
+
/**
|
|
97
|
+
* The layer of each array where the maps beyond the layers start: the ORM array's mask, added
|
|
98
|
+
* mask and mesh occlusion, in that order, and the normal array's mesh normal. Absent: just past
|
|
99
|
+
* the layers, at the layer count — which is no place for them once materials share arrays.
|
|
100
|
+
*/
|
|
101
|
+
readonly extrasAt?: number;
|
|
102
|
+
/**
|
|
103
|
+
* Each layer's own look, base first: a tint, the ranges its ORM's roughness and metalness are
|
|
104
|
+
* spread over, and how far its normal map bends the surface. Absent or null for a layer: the map
|
|
105
|
+
* as it is.
|
|
106
|
+
*/
|
|
107
|
+
readonly looks?: readonly (SurfaceLayerLook | null | undefined)[];
|
|
108
|
+
/**
|
|
109
|
+
* An occlusion read at the mesh's own coordinates — the red of the ORM array's layer after the
|
|
110
|
+
* layers and any mask or added mask the array carries — darkening the surface by a strength from 0
|
|
111
|
+
* to 1: the large shading of a cliff that layers repeating across it cannot carry, as
|
|
112
|
+
* `meshNormal` carries its shape. A number is that strength, darkening the blended colour; the
|
|
113
|
+
* object form also chooses what it darkens and the range its red is spread over. Absent or 0:
|
|
114
|
+
* none.
|
|
115
|
+
*/
|
|
116
|
+
readonly meshOcclusion?: number | SurfaceMeshOcclusion;
|
|
78
117
|
}
|
|
79
118
|
/** Floats `packSurfaceLayers` writes: four vectors. */
|
|
80
119
|
export declare const LAYER_FLOATS = 16;
|
|
@@ -105,3 +144,5 @@ export declare function layersFitBeside<Texture>(layers: SurfaceLayers<Texture>,
|
|
|
105
144
|
* Anything else as it was. Whether to say so is the caller's.
|
|
106
145
|
*/
|
|
107
146
|
export declare function layeredProjection<Texture>(projection: SurfaceProjection | null | undefined, layers: SurfaceLayers<Texture> | null | undefined): SurfaceProjection | null;
|
|
147
|
+
/** A layer's repeat across: the number, or the first of a pair. The second is `LAYER_LOOKS`'. */
|
|
148
|
+
export declare function across(repeat: number | readonly [number, number] | undefined): number | undefined;
|
|
@@ -22,7 +22,7 @@ export function packSurfaceLayers(layers, out) {
|
|
|
22
22
|
return;
|
|
23
23
|
const count = Math.min(layers.repeats.length, MAX_SURFACE_LAYERS);
|
|
24
24
|
for (let i = 0; i < MAX_SURFACE_LAYERS; i++)
|
|
25
|
-
out[i] = positive(i < count ? layers.repeats[i] : 1);
|
|
25
|
+
out[i] = positive(i < count ? across(layers.repeats[i]) : 1);
|
|
26
26
|
out[5] = count;
|
|
27
27
|
const glow = layers.emissiveLayer;
|
|
28
28
|
out[6] = glow !== undefined && Number.isInteger(glow) && glow >= 0 && glow < count ? glow : -1;
|
|
@@ -70,6 +70,10 @@ export function layeredProjection(projection, layers) {
|
|
|
70
70
|
return projection;
|
|
71
71
|
return { kind: 'planar', scale: projection.scale };
|
|
72
72
|
}
|
|
73
|
+
/** A layer's repeat across: the number, or the first of a pair. The second is `LAYER_LOOKS`'. */
|
|
74
|
+
export function across(repeat) {
|
|
75
|
+
return typeof repeat === 'number' || repeat === undefined ? repeat : repeat[0];
|
|
76
|
+
}
|
|
73
77
|
/** A layer an added mask or the facing may name: one a channel lays, 1 to `count − 1`, or −1. */
|
|
74
78
|
function laidLayer(layer, count) {
|
|
75
79
|
return layer !== undefined && Number.isInteger(layer) && layer >= 1 && layer < count ? layer : -1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.12.0",
|
|
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.
|
|
33
|
-
"@driftengine/physics": "4.
|
|
32
|
+
"@driftengine/drft": "4.12.0",
|
|
33
|
+
"@driftengine/physics": "4.12.0",
|
|
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.
|
|
71
|
-
"@driftengine/network": "4.
|
|
72
|
-
"@driftengine/texture": "4.
|
|
70
|
+
"@driftengine/nav": "4.12.0",
|
|
71
|
+
"@driftengine/network": "4.12.0",
|
|
72
|
+
"@driftengine/texture": "4.12.0"
|
|
73
73
|
}
|
|
74
74
|
}
|
package/scripts/browser.d.mts
CHANGED
|
@@ -7,6 +7,11 @@ export interface LaunchedBrowser {
|
|
|
7
7
|
close(): Promise<void>;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/** Where a Chromium-based browser lives on a platform, in the order `chromePath` tries them. */
|
|
11
|
+
export function browserCandidates(
|
|
12
|
+
platform?: string,
|
|
13
|
+
env?: Readonly<Record<string, string | undefined>>,
|
|
14
|
+
): string[];
|
|
10
15
|
export function chromePath(): string;
|
|
11
16
|
export function launch(options?: {
|
|
12
17
|
flags?: readonly string[];
|
package/scripts/browser.mjs
CHANGED
|
@@ -27,22 +27,64 @@
|
|
|
27
27
|
* it, and it is what turns "leaks accumulate forever" into "at most one leak per still-running
|
|
28
28
|
* process."
|
|
29
29
|
*/
|
|
30
|
-
import { spawn } from 'node:child_process';
|
|
30
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
31
31
|
import { once } from 'node:events';
|
|
32
32
|
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
33
33
|
import { tmpdir } from 'node:os';
|
|
34
34
|
import path from 'node:path';
|
|
35
35
|
import { sleep } from './cdp.mjs';
|
|
36
36
|
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Where a Chromium-based browser lives on each platform, in the order worth trying.
|
|
39
|
+
*
|
|
40
|
+
* **Edge is last on every list and the only thing on Windows that is always there.** It is
|
|
41
|
+
* Chromium, it speaks the same DevTools protocol, and it ships with Windows 10 and 11, so a person
|
|
42
|
+
* who never installed Chrome still has a browser this module can drive. Chrome comes first where it
|
|
43
|
+
* exists because it is what the engine's captures are measured on.
|
|
44
|
+
*
|
|
45
|
+
* Windows paths are built from the environment rather than written out, because `Program Files`
|
|
46
|
+
* is wherever that machine put it and a per-user install sits under `LOCALAPPDATA`; a Mac's
|
|
47
|
+
* per-user install sits under `~/Applications`. Nothing here has been run on Windows or macOS by
|
|
48
|
+
* this repository yet; the lists are the browsers' documented install locations.
|
|
49
|
+
*/
|
|
50
|
+
export function browserCandidates(platform = process.platform, env = process.env) {
|
|
51
|
+
if (platform === 'win32') {
|
|
52
|
+
const roots = [env['PROGRAMFILES'], env['PROGRAMFILES(X86)'], env['LOCALAPPDATA']].filter(
|
|
53
|
+
(root) => typeof root === 'string' && root !== '',
|
|
54
|
+
);
|
|
55
|
+
return [
|
|
56
|
+
...roots.map((root) =>
|
|
57
|
+
path.win32.join(root, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
58
|
+
),
|
|
59
|
+
...roots.map((root) =>
|
|
60
|
+
path.win32.join(root, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
|
|
61
|
+
),
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
if (platform === 'darwin') {
|
|
65
|
+
/* `/Applications` for an install by an administrator, `~/Applications` for one without. */
|
|
66
|
+
const roots = [
|
|
67
|
+
'/Applications',
|
|
68
|
+
typeof env['HOME'] === 'string' && env['HOME'] !== ''
|
|
69
|
+
? path.posix.join(env['HOME'], 'Applications')
|
|
70
|
+
: null,
|
|
71
|
+
].filter((root) => root !== null);
|
|
72
|
+
const apps = [
|
|
73
|
+
'Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
74
|
+
'Chromium.app/Contents/MacOS/Chromium',
|
|
75
|
+
'Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
76
|
+
];
|
|
77
|
+
return apps.flatMap((app) => roots.map((root) => path.posix.join(root, app)));
|
|
78
|
+
}
|
|
79
|
+
return [
|
|
80
|
+
'/opt/google/chrome/chrome',
|
|
81
|
+
'/usr/bin/google-chrome',
|
|
82
|
+
'/usr/bin/google-chrome-stable',
|
|
83
|
+
'/usr/bin/chromium',
|
|
84
|
+
'/usr/bin/chromium-browser',
|
|
85
|
+
'/usr/bin/microsoft-edge',
|
|
86
|
+
];
|
|
87
|
+
}
|
|
46
88
|
|
|
47
89
|
/**
|
|
48
90
|
* What it takes to reach the real card from a headless process.
|
|
@@ -80,7 +122,7 @@ const PROFILE_PREFIX = 'driftengine-shots-';
|
|
|
80
122
|
const OWNER_FILE = 'driftengine-owner.json';
|
|
81
123
|
|
|
82
124
|
/**
|
|
83
|
-
*
|
|
125
|
+
* Stop a browser and every process it started, escalating if it does not go.
|
|
84
126
|
*
|
|
85
127
|
* **Chrome is a tree, not a process.** It forks a zygote, a GPU process and one renderer per
|
|
86
128
|
* page, and signalling the parent alone leaves the rest running with the port closed and the
|
|
@@ -89,9 +131,20 @@ const OWNER_FILE = 'driftengine-owner.json';
|
|
|
89
131
|
*
|
|
90
132
|
* SIGTERM first because a browser asked politely flushes and exits; SIGKILL after a grace period
|
|
91
133
|
* because one that is wedged never will, and a wedged browser is exactly the case this exists for.
|
|
134
|
+
*
|
|
135
|
+
* **Windows has no process groups to signal**, so there `taskkill /T` walks the tree from the
|
|
136
|
+
* browser's own PID and `/F` ends it at once. Synchronous, because this also runs from an `exit`
|
|
137
|
+
* handler, where nothing asynchronous gets to finish.
|
|
92
138
|
*/
|
|
93
139
|
function killTree(pid, graceMs = 2000) {
|
|
94
140
|
if (!Number.isInteger(pid) || pid <= 0) return;
|
|
141
|
+
if (process.platform === 'win32') {
|
|
142
|
+
spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], {
|
|
143
|
+
stdio: 'ignore',
|
|
144
|
+
windowsHide: true,
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
95
148
|
try {
|
|
96
149
|
process.kill(-pid, 'SIGTERM');
|
|
97
150
|
} catch {
|
|
@@ -194,10 +247,11 @@ function defaultIsAlive(pid) {
|
|
|
194
247
|
export function chromePath() {
|
|
195
248
|
const asked = process.env['CHROME_PATH'];
|
|
196
249
|
if (asked !== undefined && asked !== '') return asked;
|
|
197
|
-
const
|
|
250
|
+
const candidates = browserCandidates();
|
|
251
|
+
const found = candidates.find((candidate) => existsSync(candidate));
|
|
198
252
|
if (found === undefined) {
|
|
199
253
|
throw new Error(
|
|
200
|
-
`no Chrome or
|
|
254
|
+
`no Chrome, Chromium or Edge found. Tried:\n ${candidates.join('\n ')}\nSet CHROME_PATH to one.`,
|
|
201
255
|
);
|
|
202
256
|
}
|
|
203
257
|
return found;
|
|
@@ -227,7 +281,7 @@ export async function launch({ flags = GPU_FLAGS, headless = true, timeoutMs = 2
|
|
|
227
281
|
process and every renderer with one signal to the negative PID. `unref()` is deliberately
|
|
228
282
|
not called: the parent must stay attached, or its own exit and signal handlers below never
|
|
229
283
|
fire, and those are half of what closes the other three holes. */
|
|
230
|
-
const child = spawn(binary, args, { stdio: 'ignore', detached: true });
|
|
284
|
+
const child = spawn(binary, args, { stdio: 'ignore', detached: true, windowsHide: true });
|
|
231
285
|
writeFileSync(
|
|
232
286
|
path.join(profile, OWNER_FILE),
|
|
233
287
|
JSON.stringify({ ownerPid: process.pid, chromePid: child.pid }),
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { RgbaImage } from './png.d.mts';
|
|
2
|
+
|
|
3
|
+
/** The backends `look` photographs by default, in the order a consumer runs them. */
|
|
4
|
+
export const LOOK_BACKENDS: readonly ['webgpu', 'webgl2'];
|
|
5
|
+
|
|
6
|
+
/** At or above this share of one colour, a canvas is called empty. */
|
|
7
|
+
export const EMPTY_SHARE: number;
|
|
8
|
+
|
|
9
|
+
/** What a frame is made of. */
|
|
10
|
+
export interface FrameStats {
|
|
11
|
+
readonly width: number;
|
|
12
|
+
readonly height: number;
|
|
13
|
+
/** The most common colour, as `#rrggbb`. */
|
|
14
|
+
readonly dominant: string;
|
|
15
|
+
/** The share of the frame that colour covers, 0 to 1. */
|
|
16
|
+
readonly dominantShare: number;
|
|
17
|
+
/** Rec. 709 luminance averaged over the frame, 0 to 255. */
|
|
18
|
+
readonly meanLuminance: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** One backend's photograph and everything the page said while it was taken. */
|
|
22
|
+
export interface LookCapture {
|
|
23
|
+
readonly backend: string;
|
|
24
|
+
readonly url: string;
|
|
25
|
+
/** What a player sees, interface included. */
|
|
26
|
+
readonly file: string;
|
|
27
|
+
/**
|
|
28
|
+
* The backend that actually drew, read from the context the page's canvas asked for: `'webgpu'`,
|
|
29
|
+
* `'webgl2'`, or `null` when no canvas in the page asked for either.
|
|
30
|
+
*/
|
|
31
|
+
readonly drawnWith: 'webgpu' | 'webgl2' | null;
|
|
32
|
+
/** Measured over the canvas alone; `null` when the page never booted. */
|
|
33
|
+
readonly stats: FrameStats | null;
|
|
34
|
+
/** The share of the canvas that changed between two frames; `null` when the page never booted. */
|
|
35
|
+
readonly moved: number | null;
|
|
36
|
+
readonly errors: readonly string[];
|
|
37
|
+
readonly warnings: readonly string[];
|
|
38
|
+
/** Every console line, `level: text`. */
|
|
39
|
+
readonly logs: readonly string[];
|
|
40
|
+
readonly problems: readonly string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface LookReport {
|
|
44
|
+
readonly url: string;
|
|
45
|
+
/** The renderer string of the GPU that drew, read from a WebGL2 context. */
|
|
46
|
+
readonly gpu: string;
|
|
47
|
+
readonly captures: readonly LookCapture[];
|
|
48
|
+
readonly problems: readonly string[];
|
|
49
|
+
/** True when no capture found a problem. */
|
|
50
|
+
readonly ok: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface LookOptions {
|
|
54
|
+
/** The address of the running game. */
|
|
55
|
+
readonly url: string;
|
|
56
|
+
readonly backends?: readonly string[];
|
|
57
|
+
/** Where the PNGs and `look.json` go. Defaults to `.driftengine/look`. */
|
|
58
|
+
readonly out?: string;
|
|
59
|
+
readonly width?: number;
|
|
60
|
+
readonly height?: number;
|
|
61
|
+
/** How long to wait for a canvas in the page to ask for a WebGPU or WebGL2 context. */
|
|
62
|
+
readonly bootMs?: number;
|
|
63
|
+
/** A pause after that for the first frames. */
|
|
64
|
+
readonly settleMs?: number;
|
|
65
|
+
/** How long to wait after that for the canvas to show more than one colour. */
|
|
66
|
+
readonly drawMs?: number;
|
|
67
|
+
/** The gap between the two frames compared to tell whether the game moves. */
|
|
68
|
+
readonly moveMs?: number;
|
|
69
|
+
/** Browser flags; defaults to `gpuFlagsFor()`. */
|
|
70
|
+
readonly flags?: readonly string[] | null;
|
|
71
|
+
/** `false` opens a real window, for a machine whose headless browser cannot reach its GPU. */
|
|
72
|
+
readonly headless?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function gpuFlagsFor(platform?: string): string[] | null;
|
|
76
|
+
export function lookUrl(url: string, backend: string): string;
|
|
77
|
+
export function frameStats(image: RgbaImage): FrameStats;
|
|
78
|
+
export function changedShare(a: RgbaImage, b: RgbaImage, tolerance?: number): number;
|
|
79
|
+
export function sortConsole(lines: readonly string[]): { errors: string[]; warnings: string[] };
|
|
80
|
+
export function look(options: LookOptions): Promise<LookReport>;
|
|
81
|
+
export function formatLook(report: LookReport): string;
|