@driftengine/core 4.11.0 → 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.
Files changed (77) hide show
  1. package/CHANGELOG.json +72 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +4 -2
  4. package/dist/index.js +1 -1
  5. package/dist/render/ambientOcclusionPass.d.ts +3 -2
  6. package/dist/render/ambientOcclusionPass.js +4 -2
  7. package/dist/render/backend/api.d.ts +21 -1
  8. package/dist/render/backend/webgl2/renderer.d.ts +61 -5
  9. package/dist/render/backend/webgl2/renderer.js +153 -21
  10. package/dist/render/backend/webgpu/renderer.d.ts +54 -1
  11. package/dist/render/backend/webgpu/renderer.js +246 -89
  12. package/dist/render/instances.d.ts +13 -1
  13. package/dist/render/instances.js +10 -0
  14. package/dist/render/occlusionFade.d.ts +21 -0
  15. package/dist/render/occlusionFade.js +27 -0
  16. package/dist/render/sceneTarget.d.ts +2 -0
  17. package/dist/render/sceneTarget.js +1 -1
  18. package/dist/render/shaders/ambientOcclusion.js +12 -1
  19. package/dist/render/shaders/flat/areaLight.d.ts +1 -1
  20. package/dist/render/shaders/flat/areaLight.js +1 -1
  21. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  22. package/dist/render/shaders/flat/glassTint.js +1 -0
  23. package/dist/render/shaders/flat/index.d.ts +5 -0
  24. package/dist/render/shaders/flat/index.js +10 -3
  25. package/dist/render/shaders/flat/layerLooks.d.ts +16 -0
  26. package/dist/render/shaders/flat/layerLooks.js +116 -0
  27. package/dist/render/shaders/flat/layered.d.ts +19 -9
  28. package/dist/render/shaders/flat/layered.js +169 -37
  29. package/dist/render/shaders/flat/main.d.ts +1 -1
  30. package/dist/render/shaders/flat/main.js +17 -5
  31. package/dist/render/shaders/flat/models.js +1 -1
  32. package/dist/render/shaders/flat/normalMap.d.ts +1 -1
  33. package/dist/render/shaders/flat/normalMap.js +11 -2
  34. package/dist/render/shaders/flat/preamble.js +6 -0
  35. package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +6 -1
  36. package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +6 -1
  37. package/dist/render/shaders/generated/flat.wgsl.d.ts +176 -48
  38. package/dist/render/shaders/generated/flat.wgsl.js +215 -87
  39. package/dist/render/surfaceLayerLooks.d.ts +80 -0
  40. package/dist/render/surfaceLayerLooks.js +83 -0
  41. package/dist/render/surfaceLayers.d.ts +128 -19
  42. package/dist/render/surfaceLayers.js +73 -13
  43. package/dist/render/surfaceTexture.d.ts +13 -4
  44. package/dist/render/transmission.d.ts +21 -0
  45. package/dist/render/transmission.js +21 -0
  46. package/dist/ui/splash.d.ts +42 -2
  47. package/dist/ui/splash.js +78 -13
  48. package/package.json +6 -6
  49. package/scripts/browser.d.mts +5 -0
  50. package/scripts/browser.mjs +68 -14
  51. package/scripts/look.d.mts +81 -0
  52. package/scripts/look.mjs +380 -0
  53. package/src/index.ts +4 -0
  54. package/src/render/ambientOcclusionPass.ts +4 -1
  55. package/src/render/backend/api.ts +31 -0
  56. package/src/render/backend/webgl2/renderer.ts +170 -22
  57. package/src/render/backend/webgpu/renderer.ts +379 -139
  58. package/src/render/instances.ts +18 -1
  59. package/src/render/occlusionFade.ts +29 -0
  60. package/src/render/sceneTarget.ts +3 -0
  61. package/src/render/shaders/ambientOcclusion.ts +12 -1
  62. package/src/render/shaders/flat/areaLight.ts +1 -1
  63. package/src/render/shaders/flat/glassTint.ts +3 -1
  64. package/src/render/shaders/flat/index.ts +15 -3
  65. package/src/render/shaders/flat/layerLooks.ts +116 -0
  66. package/src/render/shaders/flat/layered.ts +170 -37
  67. package/src/render/shaders/flat/main.ts +17 -5
  68. package/src/render/shaders/flat/models.ts +1 -1
  69. package/src/render/shaders/flat/normalMap.ts +11 -2
  70. package/src/render/shaders/flat/preamble.ts +6 -0
  71. package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +6 -1
  72. package/src/render/shaders/generated/flat.wgsl.ts +215 -87
  73. package/src/render/surfaceLayerLooks.ts +155 -0
  74. package/src/render/surfaceLayers.ts +187 -23
  75. package/src/render/surfaceTexture.ts +13 -4
  76. package/src/render/transmission.ts +27 -0
  77. package/src/ui/splash.ts +83 -14
@@ -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
+ }
@@ -1,39 +1,148 @@
1
1
  /**
2
- * A material blended from layers by a mask: `SurfaceMaterial.layers`, and the two vectors
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 { SurfaceLayerLook, SurfaceMeshOcclusion } from './surfaceLayerLooks.ts';
6
+ import type { SurfaceProjection } from './surfaceProjection.ts';
5
7
  /** The most layers a material blends: a base and one for each of the mask's four channels. */
6
8
  export declare const MAX_SURFACE_LAYERS = 5;
7
9
  /**
8
10
  * Up to five layers, each its own colour, normal and roughness at its own repeat, blended by a mask.
9
11
  *
10
- * **Layer `i` is layer `i` of the material's arrays**: `albedo`, `normal` and `orm` each an array
11
- * whose layers are the material's layers in order, read at the mesh's coordinates times
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
12
15
  * `repeats[i]`, so rock can repeat forty times across a cliff while the moss on it repeats three.
13
- * **The mask** is read at the mesh's own coordinates and lays each layer over those before it: red
14
- * lays layer 1 over the base, green layer 2 over that, blue layer 3, alpha layer 4. Its layer 0 is
15
- * read, from where a shading model's map goes, which is why a layered material carries no
16
- * `modelMap`. `emissiveLayer` gives the material's emissive map to one layer, glowing where that
17
- * layer shows and nowhere it is covered; absent, the map glows as on any material.
16
+ * Where the material has a `projection`, the layers are read at the world's horizontal position
17
+ * instead, so a repeat is so many a metre (with the projection's own scale at 1). **The mask** is
18
+ * read at the mesh's own coordinates and lays each layer over those before it: red lays layer 1
19
+ * over the base, green layer 2 over that, blue layer 3, alpha layer 4. `emissiveLayer` gives the
20
+ * material's emissive map to one layer, glowing where that layer shows and nowhere it is covered;
21
+ * absent, the map glows as on any material.
22
+ *
23
+ * **Maps beyond the layers ride the material's own arrays, after its layers**, so they take no
24
+ * texture binding of their own — the lit stage has none to spare: a mask the ORM array carries
25
+ * (`mask: 'orm'`) is the ORM array's layer just past the layers, an `addMask` the one after that,
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.
18
33
  *
19
34
  * **What it gives up**: every layer's maps are read wherever any shows, three reads a layer, so five
20
35
  * 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; and the mask is sampled linear, so it is uploaded as data
36
+ * cutout reads the base layer's alpha; a mask read where a model's map goes leaves no room for one,
37
+ * so a lightmapped surface carries its mask in its ORM array or its vertices; a triplanar projection
38
+ * lays layers on the horizontal plane alone; and a mask is sampled linear, so it is uploaded as data
22
39
  * (`colorSpace: 'linear'`).
23
40
  */
24
41
  export interface SurfaceLayers<Texture> {
25
- /** The weights, red to alpha laying layers 1 to 4, read at the mesh's coordinates. */
26
- readonly mask: Texture;
27
- /** Each layer's repeats across the mesh's coordinates, base first: one to five of them. */
28
- readonly repeats: readonly number[];
42
+ /**
43
+ * Where the weights come from, red to alpha laying layers 1 to 4: a map read where a shading
44
+ * model's map goes; `'orm'`, the ORM array's layer just past the layers; or `'vertex'`, the
45
+ * vertex colour's red, green and blue, which then weights the layers instead of tinting them.
46
+ */
47
+ readonly mask: Texture | 'orm' | 'vertex';
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])[];
29
54
  /** The layer the emissive map glows on, 0 to 4. Absent: everywhere, as on any material. */
30
55
  readonly emissiveLayer?: number;
56
+ /**
57
+ * How the layers combine. `'over'`, the default, lays each over the ones before it by its
58
+ * channel. `'sum'` mixes the base toward the sum of each layer times its channel, by the channels'
59
+ * sum held to 1: two layers at half each are both half there, where laying over would let the
60
+ * second cover half of the first.
61
+ */
62
+ readonly blend?: 'over' | 'sum';
63
+ /**
64
+ * A second mask added to one layer's weight, read where the layers are (the world, under a
65
+ * `projection`) times `repeat`, its red times `intensity`: sand drifting into the gravel's layer at
66
+ * a scale of its own. The ORM array's layer after the layers and any mask it carries.
67
+ */
68
+ readonly addMask?: {
69
+ readonly layer: number;
70
+ readonly repeat: number;
71
+ readonly intensity: number;
72
+ };
73
+ /**
74
+ * One layer weighted by how much the surface faces up rather than by its mask channel:
75
+ * `saturate(bias + sharpness × (up / 2 + 1/2))`, `up` the world normal's upward share before any
76
+ * normal map. Snow on the tops of rocks, moss on a ledge.
77
+ */
78
+ readonly facing?: {
79
+ readonly layer: number;
80
+ readonly bias: number;
81
+ readonly sharpness: number;
82
+ };
83
+ /**
84
+ * A normal map read at the mesh's own coordinates and tangents, the normal array's layer just past
85
+ * the layers, under the layers' blended normal: the large shapes of a rock or a cliff, which a
86
+ * layer repeating forty times across it cannot carry.
87
+ */
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;
31
117
  }
32
- /** Floats `packSurfaceLayers` writes: two vectors. */
33
- export declare const LAYER_FLOATS = 8;
118
+ /** Floats `packSurfaceLayers` writes: four vectors. */
119
+ export declare const LAYER_FLOATS = 16;
34
120
  /**
35
- * `layers` into `out`: the five repeats (1 for a layer not given), then how many layers there are
36
- * and which one glows (−1 for none); zeros, no layers, where there are none. A repeat that is not a
37
- * positive number is 1, so a layer is never laid at a scale that collapses it to a point.
121
+ * `layers` into `out`, four vectors. The first five floats are the repeats (1 for a layer not
122
+ * given), then how many layers there are, which one glows (−1 for none) and where the mask comes
123
+ * from (0 a map, 1 the ORM array, 2 the vertex colour). Then the blend (0 over, 1 sum), whether a
124
+ * mesh normal is laid under the layers, the added mask's layer (−1 for none) and repeat; its
125
+ * intensity; and the facing layer (−1 for none), its bias and its sharpness. Zeros, no layers, where
126
+ * there are none.
127
+ *
128
+ * A repeat that is not a positive number is 1, so a layer is never laid at a scale that collapses it
129
+ * to a point; a glow, an added mask or a facing layer naming a layer there is not is none, and the
130
+ * base, which no channel lays, is not one an added mask or the facing can name.
38
131
  */
39
132
  export declare function packSurfaceLayers<Texture>(layers: SurfaceLayers<Texture> | null | undefined, out: Float32Array): void;
133
+ /** The texture a layered material's mask is, where it is one rather than another array or the vertices. */
134
+ export declare function layerMaskMap<Texture>(layers: SurfaceLayers<Texture> | null | undefined): Texture | null;
135
+ /**
136
+ * Whether a material's layers can be drawn beside its `modelMap`: always, unless the mask is a map,
137
+ * which is read where a model's map goes and so cannot share the slot with one — a lightmap's page
138
+ * among them. The backends draw such a material as one layer and say so once.
139
+ */
140
+ export declare function layersFitBeside<Texture>(layers: SurfaceLayers<Texture>, modelMap: Texture | null | undefined): boolean;
141
+ /**
142
+ * The projection a layered material is drawn with: a triplanar one is laid on the horizontal plane,
143
+ * at its own scale, since every layer's maps read three times over would be forty-five reads a pixel.
144
+ * Anything else as it was. Whether to say so is the caller's.
145
+ */
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;
@@ -1,26 +1,86 @@
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: two vectors. */
8
- export const LAYER_FLOATS = 8;
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`: the five repeats (1 for a layer not given), then how many layers there are
11
- * and which one glows (−1 for none); zeros, no layers, where there are none. A repeat that is not a
12
- * positive number is 1, so a layer is never laid at a scale that collapses it to a point.
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
- const repeat = i < count ? layers.repeats[i] : 1;
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 ? across(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'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
+ }
77
+ /** A layer an added mask or the facing may name: one a channel lays, 1 to `count − 1`, or −1. */
78
+ function laidLayer(layer, count) {
79
+ return layer !== undefined && Number.isInteger(layer) && layer >= 1 && layer < count ? layer : -1;
80
+ }
81
+ function positive(value) {
82
+ return value !== undefined && Number.isFinite(value) && value > 0 ? value : 1;
83
+ }
84
+ function finite(value) {
85
+ return value !== undefined && Number.isFinite(value) ? value : 0;
26
86
  }
@@ -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. Absent, the
224
- * material is one layer, as every material was. **A lit switch**, compiled in the first time a
225
- * material asks. The mask is read where `modelMap` goes, so a material carries one or the other.
226
- * See `surfaceLayers.ts` for what it gives up.
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
+ }
@@ -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.** Swapping before that shows the empty canvas the badge exists to
53
- * cover.
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
  *