@driftengine/core 4.13.6 → 4.14.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 (91) hide show
  1. package/CHANGELOG.json +51 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.js +1 -0
  5. package/dist/render/backend/api.d.ts +19 -3
  6. package/dist/render/backend/webgl2/motionTarget.d.ts +39 -0
  7. package/dist/render/backend/webgl2/motionTarget.js +254 -0
  8. package/dist/render/backend/webgl2/pointSkinFeedback.d.ts +13 -3
  9. package/dist/render/backend/webgl2/pointSkinFeedback.js +45 -12
  10. package/dist/render/backend/webgl2/renderer.d.ts +101 -7
  11. package/dist/render/backend/webgl2/renderer.js +337 -9
  12. package/dist/render/backend/webgpu/pointSkinPass.d.ts +16 -3
  13. package/dist/render/backend/webgpu/pointSkinPass.js +79 -21
  14. package/dist/render/backend/webgpu/postPass.d.ts +6 -1
  15. package/dist/render/backend/webgpu/renderer.d.ts +41 -0
  16. package/dist/render/backend/webgpu/renderer.js +143 -5
  17. package/dist/render/baseDecals.d.ts +48 -0
  18. package/dist/render/baseDecals.js +71 -0
  19. package/dist/render/emissiveFlow.d.ts +40 -0
  20. package/dist/render/emissiveFlow.js +51 -0
  21. package/dist/render/fixtureAtlas.d.ts +8 -3
  22. package/dist/render/fixtureAtlas.js +17 -6
  23. package/dist/render/globalMedium.d.ts +17 -0
  24. package/dist/render/globalMedium.js +14 -0
  25. package/dist/render/globalMediumPass.js +3 -2
  26. package/dist/render/iridescentOverlay.d.ts +78 -0
  27. package/dist/render/iridescentOverlay.js +102 -0
  28. package/dist/render/mesh.d.ts +24 -0
  29. package/dist/render/mesh.js +79 -0
  30. package/dist/render/pointSkin.d.ts +18 -0
  31. package/dist/render/pointSkin.js +50 -5
  32. package/dist/render/recon/mover.d.ts +3 -2
  33. package/dist/render/recon/mover.js +3 -2
  34. package/dist/render/sceneTarget.d.ts +2 -0
  35. package/dist/render/sceneTarget.js +1 -1
  36. package/dist/render/shaders/flat/baseDecals.d.ts +17 -0
  37. package/dist/render/shaders/flat/baseDecals.js +71 -0
  38. package/dist/render/shaders/flat/emissiveFlow.d.ts +15 -0
  39. package/dist/render/shaders/flat/emissiveFlow.js +45 -0
  40. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  41. package/dist/render/shaders/flat/glassTint.js +3 -0
  42. package/dist/render/shaders/flat/index.d.ts +16 -0
  43. package/dist/render/shaders/flat/index.js +9 -0
  44. package/dist/render/shaders/flat/iridescentOverlay.d.ts +15 -0
  45. package/dist/render/shaders/flat/iridescentOverlay.js +65 -0
  46. package/dist/render/shaders/flat/layered.js +4 -4
  47. package/dist/render/shaders/flat/main.d.ts +1 -1
  48. package/dist/render/shaders/flat/main.js +12 -1
  49. package/dist/render/shaders/flat/mapReads.d.ts +1 -1
  50. package/dist/render/shaders/flat/mapReads.js +7 -3
  51. package/dist/render/shaders/flat/worldUv.js +6 -5
  52. package/dist/render/shaders/generated/flat.wgsl.d.ts +480 -16
  53. package/dist/render/shaders/generated/flat.wgsl.js +519 -55
  54. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +15 -5
  55. package/dist/render/shaders/generated/temporalResolve.wgsl.js +15 -5
  56. package/dist/render/shaders/temporalResolve.js +25 -9
  57. package/dist/render/surfaceTexture.d.ts +32 -0
  58. package/dist/render/temporalPass.d.ts +3 -1
  59. package/dist/render/temporalPass.js +9 -1
  60. package/package.json +6 -6
  61. package/src/index.ts +3 -0
  62. package/src/render/backend/api.ts +19 -2
  63. package/src/render/backend/webgl2/motionTarget.ts +299 -0
  64. package/src/render/backend/webgl2/pointSkinFeedback.ts +49 -14
  65. package/src/render/backend/webgl2/renderer.ts +411 -9
  66. package/src/render/backend/webgpu/pointSkinPass.ts +90 -23
  67. package/src/render/backend/webgpu/renderer.ts +163 -3
  68. package/src/render/baseDecals.ts +93 -0
  69. package/src/render/emissiveFlow.ts +66 -0
  70. package/src/render/fixtureAtlas.ts +26 -7
  71. package/src/render/globalMedium.ts +31 -0
  72. package/src/render/globalMediumPass.ts +3 -7
  73. package/src/render/iridescentOverlay.ts +177 -0
  74. package/src/render/mesh.ts +82 -0
  75. package/src/render/pointSkin.ts +64 -5
  76. package/src/render/recon/mover.ts +3 -2
  77. package/src/render/sceneTarget.ts +3 -0
  78. package/src/render/shaders/flat/baseDecals.ts +71 -0
  79. package/src/render/shaders/flat/emissiveFlow.ts +45 -0
  80. package/src/render/shaders/flat/glassTint.ts +7 -1
  81. package/src/render/shaders/flat/index.ts +25 -0
  82. package/src/render/shaders/flat/iridescentOverlay.ts +65 -0
  83. package/src/render/shaders/flat/layered.ts +4 -4
  84. package/src/render/shaders/flat/main.ts +12 -1
  85. package/src/render/shaders/flat/mapReads.ts +7 -3
  86. package/src/render/shaders/flat/worldUv.ts +6 -5
  87. package/src/render/shaders/generated/flat.wgsl.ts +519 -55
  88. package/src/render/shaders/generated/temporalResolve.wgsl.ts +15 -5
  89. package/src/render/shaders/temporalResolve.ts +25 -9
  90. package/src/render/surfaceTexture.ts +32 -0
  91. package/src/render/temporalPass.ts +8 -0
@@ -0,0 +1,40 @@
1
+ /**
2
+ * A material's glow moved and masked: `SurfaceMaterial.emissiveScroll` and `emissiveMaskLayer`,
3
+ * packed into `uEmissiveFlow` for the lit stage's `EMISSIVE_FLOW` switch. Both backends pack it
4
+ * here and only upload it, so they cannot disagree about which layer a mask is.
5
+ *
6
+ * **What it is for**: lava in a rock's cracks, a sign's lettering, a glow repeated with a surface's
7
+ * first layer and drifting across it, held to where the surface glows by a map that spans the mesh
8
+ * once. Folded into each vertex's emission instead, the glow spreads across whole faces of a coarse
9
+ * mesh where it should follow the cracks.
10
+ *
11
+ * **The mask is the alpha of a layer of the emissive map's own array**, read at the mesh's first
12
+ * coordinates as they are, before the material's repeat, offset, layers or projection: alpha is
13
+ * never decoded as sRGB, so it is the number it was written as in either colour space, and no
14
+ * texture binding is spent, since the lit stage has none left (`textureUnitBudget.test.ts`). What
15
+ * that gives up is a mask of its own size: a layer of an array is the array's size.
16
+ *
17
+ * **The scroll moves the emissive map alone**, by `Environment.surfaceTime`, in its own repeats a
18
+ * second, wrapped to one so a long-running clock keeps its precision; the albedo and every other map
19
+ * stay where they are.
20
+ */
21
+ /**
22
+ * Floats in `uEmissiveFlow`: the scroll on u and v, the mask's layer plus one (0, none) and one
23
+ * unused; then the scale and offset that take the material's coordinates back to the mesh's own.
24
+ */
25
+ export declare const EMISSIVE_FLOW_FLOATS = 8;
26
+ /** What a material says about its glow's flow, and the repeat and offset its coordinates carry. */
27
+ export interface EmissiveFlowMaterial {
28
+ readonly emissiveScroll?: readonly [number, number] | null;
29
+ readonly emissiveMaskLayer?: number | null;
30
+ readonly uScale?: number;
31
+ readonly vScale?: number;
32
+ readonly uOffset?: number;
33
+ readonly vOffset?: number;
34
+ }
35
+ /**
36
+ * Pack a material's flow into `out` and say whether it asks for one: a scroll that moves, or a mask
37
+ * that is a whole layer. A scroll that is not a number is still; a layer that is not a whole number
38
+ * at or above zero is no mask.
39
+ */
40
+ export declare function packEmissiveFlow(material: EmissiveFlowMaterial | null, out: Float32Array): boolean;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * A material's glow moved and masked: `SurfaceMaterial.emissiveScroll` and `emissiveMaskLayer`,
3
+ * packed into `uEmissiveFlow` for the lit stage's `EMISSIVE_FLOW` switch. Both backends pack it
4
+ * here and only upload it, so they cannot disagree about which layer a mask is.
5
+ *
6
+ * **What it is for**: lava in a rock's cracks, a sign's lettering, a glow repeated with a surface's
7
+ * first layer and drifting across it, held to where the surface glows by a map that spans the mesh
8
+ * once. Folded into each vertex's emission instead, the glow spreads across whole faces of a coarse
9
+ * mesh where it should follow the cracks.
10
+ *
11
+ * **The mask is the alpha of a layer of the emissive map's own array**, read at the mesh's first
12
+ * coordinates as they are, before the material's repeat, offset, layers or projection: alpha is
13
+ * never decoded as sRGB, so it is the number it was written as in either colour space, and no
14
+ * texture binding is spent, since the lit stage has none left (`textureUnitBudget.test.ts`). What
15
+ * that gives up is a mask of its own size: a layer of an array is the array's size.
16
+ *
17
+ * **The scroll moves the emissive map alone**, by `Environment.surfaceTime`, in its own repeats a
18
+ * second, wrapped to one so a long-running clock keeps its precision; the albedo and every other map
19
+ * stay where they are.
20
+ */
21
+ /**
22
+ * Floats in `uEmissiveFlow`: the scroll on u and v, the mask's layer plus one (0, none) and one
23
+ * unused; then the scale and offset that take the material's coordinates back to the mesh's own.
24
+ */
25
+ export const EMISSIVE_FLOW_FLOATS = 8;
26
+ /**
27
+ * Pack a material's flow into `out` and say whether it asks for one: a scroll that moves, or a mask
28
+ * that is a whole layer. A scroll that is not a number is still; a layer that is not a whole number
29
+ * at or above zero is no mask.
30
+ */
31
+ export function packEmissiveFlow(material, out) {
32
+ const scroll = material?.emissiveScroll ?? null;
33
+ const u = scroll !== null && Number.isFinite(scroll[0]) ? scroll[0] : 0;
34
+ const v = scroll !== null && Number.isFinite(scroll[1]) ? scroll[1] : 0;
35
+ const layer = material?.emissiveMaskLayer ?? null;
36
+ const masked = layer !== null && Number.isInteger(layer) && layer >= 0;
37
+ out[0] = u;
38
+ out[1] = v;
39
+ /* Plus one, so an unwritten zero is no mask rather than a mask of layer 0. */
40
+ out[2] = masked ? layer + 1 : 0;
41
+ out[3] = 0;
42
+ /* The vertex stage laid `uv · scale + offset`; this undoes it. A scale of zero has no inverse,
43
+ and every texel of such a surface is the one coordinate, so the mesh's own is taken as 0. */
44
+ const uScale = material?.uScale ?? 1;
45
+ const vScale = material?.vScale ?? 1;
46
+ out[4] = uScale === 0 ? 0 : 1 / uScale;
47
+ out[5] = vScale === 0 ? 0 : 1 / vScale;
48
+ out[6] = uScale === 0 ? 0 : -(material?.uOffset ?? 0) / uScale;
49
+ out[7] = vScale === 0 ? 0 : -(material?.vOffset ?? 0) / vScale;
50
+ return u !== 0 || v !== 0 || masked;
51
+ }
@@ -3,11 +3,16 @@ export interface FixtureAtlasLayout {
3
3
  readonly height: number;
4
4
  /** Rows of cookie tiles at the top: `COOKIE_TILE`, or 0 with none loaded. */
5
5
  readonly cookieRows: number;
6
- /** The first row of the profiles, below the cookies. */
6
+ /** The first row of the decals' images, below the cookies, and how many rows they take. */
7
+ readonly decalTop: number;
8
+ readonly decalRows: number;
9
+ /** The first row of the profiles, below the cookies and the decals. */
7
10
  readonly iesTop: number;
8
11
  }
9
- /** The atlas for `iesRows` rows of profile and `cookieTiles` cookies. */
10
- export declare function fixtureAtlasLayout(iesRows: number, cookieTiles: number): FixtureAtlasLayout;
12
+ /** The row the decals' band starts at: below the cookies, which are there or are not. */
13
+ export declare function decalBandTop(cookieTiles: number): number;
14
+ /** The atlas for `iesRows` rows of profile, `cookieTiles` cookies and `decalTiles` decal images. */
15
+ export declare function fixtureAtlasLayout(iesRows: number, cookieTiles: number, decalTiles?: number): FixtureAtlasLayout;
11
16
  /**
12
17
  * The profiles' rows as the atlas holds them: `data` one float a texel, `width` across and `rows`
13
18
  * down, spread into the red channel of a four-channel image the atlas's size, the rest ones. The
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * What a light fixture throws, in one texture: its spot cookies in the top band, its photometric
3
- * profiles in the rows below.
3
+ * profiles in the rows below. And between the two, since the lit stage had no binding left for them
4
+ * either, the images of the decals laid into base colours (`baseDecals.ts`), a band of squares a
5
+ * frame may rewrite; each band is found from its own end or its own row, so the others' lookups are
6
+ * as they were.
4
7
  *
5
8
  * **One binding for two tables, because the lit stage had one sampler left.** The cookie atlas and
6
9
  * the photometric atlas were two small float textures read with `textureLod`, and the widest lit
@@ -19,14 +22,22 @@
19
22
  */
20
23
  import { IES_ATLAS_WIDTH } from './iesProfile.js';
21
24
  import { COOKIE_TILE } from './lightBudget.js';
22
- /** The atlas for `iesRows` rows of profile and `cookieTiles` cookies. */
23
- export function fixtureAtlasLayout(iesRows, cookieTiles) {
25
+ import { BASE_DECAL_TILE } from './baseDecals.js';
26
+ /** The row the decals' band starts at: below the cookies, which are there or are not. */
27
+ export function decalBandTop(cookieTiles) {
28
+ return cookieTiles > 0 ? COOKIE_TILE : 0;
29
+ }
30
+ /** The atlas for `iesRows` rows of profile, `cookieTiles` cookies and `decalTiles` decal images. */
31
+ export function fixtureAtlasLayout(iesRows, cookieTiles, decalTiles = 0) {
24
32
  const cookieRows = cookieTiles > 0 ? COOKIE_TILE : 0;
33
+ const decalRows = decalTiles > 0 ? BASE_DECAL_TILE : 0;
25
34
  return {
26
- width: Math.max(IES_ATLAS_WIDTH, COOKIE_TILE * cookieTiles),
27
- height: cookieRows + iesRows,
35
+ width: Math.max(IES_ATLAS_WIDTH, COOKIE_TILE * cookieTiles, BASE_DECAL_TILE * decalTiles),
36
+ height: cookieRows + decalRows + iesRows,
28
37
  cookieRows,
29
- iesTop: cookieRows,
38
+ decalTop: cookieRows,
39
+ decalRows,
40
+ iesTop: cookieRows + decalRows,
30
41
  };
31
42
  }
32
43
  /**
@@ -110,6 +110,16 @@ export interface GlobalMediumOptions {
110
110
  readonly sunScatter?: number;
111
111
  /** The same for the sky's fill, which lights a shadowed medium from every side. Default 1. */
112
112
  readonly skyScatter?: number;
113
+ /**
114
+ * The sky's fill the medium scatters, a colour of its own in the units of
115
+ * `Environment.ambient`, which `skyScatter` then scales. Absent or null, the frame's ambient, as
116
+ * it always was. **For a scene whose surfaces are lit by more than its flat ambient**, a sky's
117
+ * cubemap, probes and a bake, which keeps that ambient for what falls outside them: the ambient
118
+ * is then the wrong colour for the sky's light in the air, and setting it to the right one would
119
+ * relight those surfaces too. What it gives up is the sky's direction, as the ambient does: one
120
+ * colour from every side.
121
+ */
122
+ readonly skyFill?: readonly [number, number, number] | null;
113
123
  /**
114
124
  * The same for the lamps, each through its own shadow and times its own `lightMediumScatter`.
115
125
  * **Default 0**: a medium was the sun's and the sky's until there was this, and a scene that
@@ -135,6 +145,8 @@ export interface ResolvedGlobalMedium {
135
145
  readonly sunScatter: number;
136
146
  readonly skyScatter: number;
137
147
  readonly lampScatter: number;
148
+ /** The sky's fill of the medium's own, or null for the frame's ambient: see `mediumSkyFill`. */
149
+ readonly skyFill: readonly [number, number, number] | null;
138
150
  }
139
151
  /**
140
152
  * What a renderer starts with: off, and everything else at a value that means "ordinary air".
@@ -153,6 +165,11 @@ export declare const DEFAULT_GLOBAL_MEDIUM: GlobalMediumOptions;
153
165
  * than reporting; a `maxDistance` of zero is a march with no length that still costs its steps.
154
166
  */
155
167
  export declare function resolveGlobalMedium(options: Partial<GlobalMediumOptions>): ResolvedGlobalMedium;
168
+ /**
169
+ * The colour the medium scatters as the sky's fill: its own where it has one, else the frame's
170
+ * ambient. One decision both backends upload, so the two cannot disagree about which.
171
+ */
172
+ export declare function mediumSkyFill(medium: Pick<ResolvedGlobalMedium, 'skyFill'>, ambient: ArrayLike<number>): ArrayLike<number>;
156
173
  /** `setGlobalMedium`'s object form: a density, and any of the rest. */
157
174
  export type GlobalMediumSettings = Partial<GlobalMediumOptions> & {
158
175
  readonly density: number;
@@ -88,8 +88,22 @@ export function resolveGlobalMedium(options) {
88
88
  sunScatter: Math.max(0, finite(options.sunScatter, 1)),
89
89
  skyScatter: Math.max(0, finite(options.skyScatter, 1)),
90
90
  lampScatter: Math.max(0, finite(options.lampScatter, 0)),
91
+ skyFill: options.skyFill === undefined || options.skyFill === null
92
+ ? null
93
+ : [
94
+ Math.max(0, finite(options.skyFill[0], 0)),
95
+ Math.max(0, finite(options.skyFill[1], 0)),
96
+ Math.max(0, finite(options.skyFill[2], 0)),
97
+ ],
91
98
  };
92
99
  }
100
+ /**
101
+ * The colour the medium scatters as the sky's fill: its own where it has one, else the frame's
102
+ * ambient. One decision both backends upload, so the two cannot disagree about which.
103
+ */
104
+ export function mediumSkyFill(medium, ambient) {
105
+ return medium.skyFill ?? ambient;
106
+ }
93
107
  /**
94
108
  * `setGlobalMedium`'s two forms, its four numbers or the object, read one way for both backends:
95
109
  * a density that is not a number is none.
@@ -1,5 +1,5 @@
1
1
  import { mat4 } from 'gl-matrix';
2
- import { mediumTargetSize } from './globalMedium.js';
2
+ import { mediumSkyFill, mediumTargetSize } from './globalMedium.js';
3
3
  import { DEPTH_01_TO_CLIP } from './lightVolumeDraw.js';
4
4
  import { compileProgram, uniformLocations } from './shader.js';
5
5
  import { FULLSCREEN_VERT } from './shaders/fullscreen.js';
@@ -168,7 +168,8 @@ export class GlobalMediumPass {
168
168
  gl.uniform3f(u['uCameraPos'] ?? null, eye[0] ?? 0, eye[1] ?? 0, eye[2] ?? 0);
169
169
  gl.uniform3f(u['uSunDir'] ?? null, light.sunDir[0] ?? 0, light.sunDir[1] ?? 1, light.sunDir[2] ?? 0);
170
170
  gl.uniform3f(u['uSunColor'] ?? null, light.sunColor[0] ?? 0, light.sunColor[1] ?? 0, light.sunColor[2] ?? 0);
171
- gl.uniform3f(u['uAmbient'] ?? null, light.ambient[0] ?? 0, light.ambient[1] ?? 0, light.ambient[2] ?? 0);
171
+ const sky = mediumSkyFill(options, light.ambient);
172
+ gl.uniform3f(u['uAmbient'] ?? null, sky[0] ?? 0, sky[1] ?? 0, sky[2] ?? 0);
172
173
  gl.uniform1f(u['uDensity'] ?? null, options.density);
173
174
  gl.uniform2f(u['uHeight'] ?? null, options.heightFalloff, options.baseY);
174
175
  gl.uniform3f(u['uLayer2'] ?? null, options.density2, options.heightFalloff2, options.baseY2);
@@ -0,0 +1,78 @@
1
+ /**
2
+ * A surface's iridescent overlay: a hue that shifts with the view, laid over the base colour before
3
+ * it is lit. `SurfaceMaterial.iridescentOverlay`, packed into `uIridescence` for the lit stage's
4
+ * `IRIDESCENT_OVERLAY` switch, and the same arithmetic on the CPU, which the tests hold the shader to.
5
+ *
6
+ * Per pixel, with `N` the mapped normal, `V` toward the eye and `uv` the mesh's own first
7
+ * coordinates, as many material systems shade a sheen on cloth or a coat:
8
+ *
9
+ * h = saturate((N·H)^contrast), 0 where N·H ≤ 0, H = normalize(V + direction)
10
+ * x = N·V + uv.x + blend · (h − N·V)
11
+ * t = saturate(fract(x · tiling))
12
+ * base' = base + (overlay(base, ramp(t)) − base) · x · mask · weight
13
+ *
14
+ * where `overlay` is the usual blend, `a < ½ ? 2ao : 1 − 2(1 − a)(1 − o)` per channel.
15
+ *
16
+ * **The ramp is uniforms, thirty-two colours, and not a texture**, because the lit stage has no
17
+ * texture binding left (`textureUnitBudget.test.ts`): a caller's ramp of any length is resampled to
18
+ * thirty-two on the way in and read back between neighbours. What that gives up is a ramp with
19
+ * detail finer than a thirty-second of its length, which a smooth spectrum does not have. **The
20
+ * mask is a channel of a layer of one of the material's own arrays**, for the same reason, read at
21
+ * the material's coordinates; alpha is linear in every array, and a colour channel of an sRGB array
22
+ * is decoded before it is read.
23
+ *
24
+ * Thirty-six vectors, so WebGL2 refuses it, in words, on a part whose lit stage has no room, as it
25
+ * refuses a material's tiled detail.
26
+ */
27
+ /** Three numbers, read and never written. */
28
+ type Three = readonly [number, number, number];
29
+ /** Colours the ramp is resampled to. */
30
+ export declare const IRIDESCENT_RAMP = 32;
31
+ /** Floats in `uIridescence`: four vectors of numbers, then the ramp a vector a colour. */
32
+ export declare const IRIDESCENT_FLOATS: number;
33
+ /** Which of a material's arrays holds the mask. */
34
+ export type IridescentMaskMap = 'orm' | 'model' | 'albedo' | 'emissive';
35
+ /** `SurfaceMaterial.iridescentOverlay`. */
36
+ export interface IridescentOverlay {
37
+ /**
38
+ * The overlay colour along `t`, linear, three floats a colour, at least two colours, the first at
39
+ * `t = 0` and the last at `t = 1`. Resampled to thirty-two.
40
+ */
41
+ readonly ramp: ArrayLike<number>;
42
+ /** A fixed direction in the world the sheen turns toward, `S`. Need not be unit length. */
43
+ readonly direction: Three;
44
+ /** How far the view's term is turned toward the sheen, 0 to 1. Default 0.6. */
45
+ readonly blend?: number;
46
+ /** The sheen's exponent. Default 1. */
47
+ readonly contrast?: number;
48
+ /** How many times the ramp repeats across `x`. Default 2. */
49
+ readonly tiling?: number;
50
+ /** How much of the overlay is laid on. Default 0.25. */
51
+ readonly weight?: number;
52
+ /** Where it is laid: a channel of a layer of one of the material's arrays. Absent, everywhere. */
53
+ readonly mask?: {
54
+ readonly map: IridescentMaskMap;
55
+ readonly layer: number;
56
+ /** 0 red, 1 green, 2 blue, 3 alpha. */
57
+ readonly channel: 0 | 1 | 2 | 3;
58
+ } | null;
59
+ }
60
+ /** Why an overlay cannot be drawn, or null where it can. */
61
+ export declare function iridescentRefusal(overlay: IridescentOverlay): string | null;
62
+ /**
63
+ * Pack an overlay into `out`, with the scale and offset that take the material's coordinates back
64
+ * to the mesh's own; all zeros, which the shader reads as none, for null. Refuse what
65
+ * `iridescentRefusal` refuses before calling this.
66
+ */
67
+ export declare function packIridescentOverlay(overlay: IridescentOverlay | null, uv: {
68
+ uScale?: number;
69
+ vScale?: number;
70
+ uOffset?: number;
71
+ vOffset?: number;
72
+ }, out: Float32Array): void;
73
+ /**
74
+ * The base colour with the overlay laid on, as the lit stage computes it from a packed block: `n`
75
+ * and `toEye` unit length, `uv` the mesh's own coordinates, `mask` the texel's channel. Into `out`.
76
+ */
77
+ export declare function iridescentBase(packed: ArrayLike<number>, base: Three, n: Three, toEye: Three, uv: readonly [number, number], mask: number, out: [number, number, number]): [number, number, number];
78
+ export {};
@@ -0,0 +1,102 @@
1
+ /** Colours the ramp is resampled to. */
2
+ export const IRIDESCENT_RAMP = 32;
3
+ /** Floats in `uIridescence`: four vectors of numbers, then the ramp a vector a colour. */
4
+ export const IRIDESCENT_FLOATS = (4 + IRIDESCENT_RAMP) * 4;
5
+ const MASK_SOURCE = {
6
+ orm: 1,
7
+ model: 2,
8
+ albedo: 3,
9
+ emissive: 4,
10
+ };
11
+ /** Why an overlay cannot be drawn, or null where it can. */
12
+ export function iridescentRefusal(overlay) {
13
+ const values = overlay.ramp.length;
14
+ if (values < 6 || values % 3 !== 0) {
15
+ return `a ramp of ${values} numbers; it is three a colour, at least two colours`;
16
+ }
17
+ const [x, y, z] = overlay.direction;
18
+ if (!(Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z)) ||
19
+ x * x + y * y + z * z === 0) {
20
+ return 'a direction that is not one';
21
+ }
22
+ const mask = overlay.mask ?? null;
23
+ if (mask !== null && !(Number.isInteger(mask.layer) && mask.layer >= 0)) {
24
+ return `a mask layer of ${mask.layer}; a layer is a whole number from 0`;
25
+ }
26
+ return null;
27
+ }
28
+ /**
29
+ * Pack an overlay into `out`, with the scale and offset that take the material's coordinates back
30
+ * to the mesh's own; all zeros, which the shader reads as none, for null. Refuse what
31
+ * `iridescentRefusal` refuses before calling this.
32
+ */
33
+ export function packIridescentOverlay(overlay, uv, out) {
34
+ out.fill(0);
35
+ if (overlay === null)
36
+ return;
37
+ const [x, y, z] = overlay.direction;
38
+ const length = Math.hypot(x, y, z);
39
+ out[0] = x / length;
40
+ out[1] = y / length;
41
+ out[2] = z / length;
42
+ out[3] = overlay.blend ?? 0.6;
43
+ out[4] = overlay.contrast ?? 1;
44
+ out[5] = overlay.tiling ?? 2;
45
+ out[6] = overlay.weight ?? 0.25;
46
+ /* On: a zero here, which an unwritten block is, is none. */
47
+ out[7] = 1;
48
+ const mask = overlay.mask ?? null;
49
+ out[8] = mask === null ? 0 : MASK_SOURCE[mask.map];
50
+ out[9] = mask === null ? 0 : mask.layer;
51
+ out[10] = mask === null ? 0 : mask.channel;
52
+ const uScale = uv.uScale ?? 1;
53
+ const vScale = uv.vScale ?? 1;
54
+ out[12] = uScale === 0 ? 0 : 1 / uScale;
55
+ out[13] = vScale === 0 ? 0 : 1 / vScale;
56
+ out[14] = uScale === 0 ? 0 : -(uv.uOffset ?? 0) / uScale;
57
+ out[15] = vScale === 0 ? 0 : -(uv.vOffset ?? 0) / vScale;
58
+ /* The ramp, resampled to thirty-two colours between its own neighbours. */
59
+ const colours = overlay.ramp.length / 3;
60
+ for (let i = 0; i < IRIDESCENT_RAMP; i++) {
61
+ const at = (i / (IRIDESCENT_RAMP - 1)) * (colours - 1);
62
+ const below = Math.min(Math.floor(at), colours - 2);
63
+ const share = at - below;
64
+ for (let c = 0; c < 3; c++) {
65
+ const a = overlay.ramp[below * 3 + c];
66
+ const b = overlay.ramp[(below + 1) * 3 + c];
67
+ out[16 + i * 4 + c] = a + (b - a) * share;
68
+ }
69
+ }
70
+ }
71
+ /**
72
+ * The base colour with the overlay laid on, as the lit stage computes it from a packed block: `n`
73
+ * and `toEye` unit length, `uv` the mesh's own coordinates, `mask` the texel's channel. Into `out`.
74
+ */
75
+ export function iridescentBase(packed, base, n, toEye, uv, mask, out) {
76
+ const p = (i) => packed[i];
77
+ let hx = toEye[0] + p(0);
78
+ let hy = toEye[1] + p(1);
79
+ let hz = toEye[2] + p(2);
80
+ const hl = Math.hypot(hx, hy, hz) || 1;
81
+ hx /= hl;
82
+ hy /= hl;
83
+ hz /= hl;
84
+ const nh = n[0] * hx + n[1] * hy + n[2] * hz;
85
+ const h = nh > 0 ? Math.min(1, Math.max(0, nh ** p(4))) : 0;
86
+ const nv = n[0] * toEye[0] + n[1] * toEye[1] + n[2] * toEye[2];
87
+ const x = nv + uv[0] + p(3) * (h - nv);
88
+ const scaled = x * p(5);
89
+ const t = Math.min(1, Math.max(0, scaled - Math.floor(scaled)));
90
+ const at = t * (IRIDESCENT_RAMP - 1);
91
+ const below = Math.min(Math.floor(at), IRIDESCENT_RAMP - 2);
92
+ const share = at - below;
93
+ const amount = x * mask * p(6);
94
+ for (let c = 0; c < 3; c++) {
95
+ const o0 = p(16 + below * 4 + c);
96
+ const o = o0 + (p(16 + (below + 1) * 4 + c) - o0) * share;
97
+ const a = base[c];
98
+ const blended = a < 0.5 ? 2 * a * o : 1 - 2 * (1 - a) * (1 - o);
99
+ out[c] = a + (blended - a) * amount;
100
+ }
101
+ return out;
102
+ }
@@ -22,6 +22,16 @@ export declare class Mesh {
22
22
  readonly positions: WebGLBuffer;
23
23
  readonly normals: WebGLBuffer;
24
24
  } | null;
25
+ /**
26
+ * A dynamic mesh's positions as they stood before this frame's first rewrite, for the motion
27
+ * WebGL2's temporal resolve follows (`backend/webgl2/motionTarget.ts`). Made the first time one is
28
+ * kept, so a mesh that never moves under the resolve pays nothing.
29
+ */
30
+ private previousPositions;
31
+ /** The frame that rewrite belongs to (`frameOfChange`), or -1 for none yet. */
32
+ changedFrame: number;
33
+ /** A vertex array for the motion pass, without and with last frame's positions, made on demand. */
34
+ private motionArrays;
25
35
  /** The colour buffer, kept so `updateColors` can rewrite it. Null unless `dynamicColors`. */
26
36
  private readonly recolourable;
27
37
  /** How many floats the position buffer holds, so an update of the wrong size is refused. */
@@ -140,6 +150,20 @@ export declare class Mesh {
140
150
  /** Draw `count` instances through a batch's own vertex array. See `createInstanceArray`. */
141
151
  drawInstancesThrough(gl: WebGL2RenderingContext, vao: WebGLVertexArrayObject, count: number): void;
142
152
  dispose(gl: WebGL2RenderingContext): void;
153
+ /**
154
+ * Keep this dynamic mesh's positions as they stand, before a rewrite: one buffer copy on the
155
+ * device. The caller decides whether this rewrite is the frame's first (`keepsPrevious`).
156
+ */
157
+ keepPrevious(gl: WebGL2RenderingContext): void;
158
+ /** Whether last frame's positions have been kept: a dynamic mesh rewritten under the resolve. */
159
+ get keptPrevious(): boolean;
160
+ /**
161
+ * Draw this mesh for the motion pass: its positions at 0, last frame's at 1 where `previous` asks
162
+ * and they were kept, and its rig's influences where `flat`'s skinning reads them, 11, 12, 14 and
163
+ * 15. A vertex array of its own, made once a variant, so the one the lit pass draws with is never
164
+ * touched.
165
+ */
166
+ drawMotion(gl: WebGL2RenderingContext, previous: boolean): void;
143
167
  /**
144
168
  * Rewrite this mesh's positions, and its normals where the caller has them.
145
169
  *
@@ -27,6 +27,8 @@ import { validateMeshData } from '@driftengine/drft';
27
27
  */
28
28
  /** Attribute locations are fixed engine-wide (`layout(location = n)` in GLSL). */
29
29
  const ATTR_POSITION = 0;
30
+ /** What the motion pass reads: positions, and a rig's two sets of joints and weights. */
31
+ const MOTION_LOCATIONS = [ATTR_POSITION, 11, 12, 14, 15];
30
32
  const ATTR_NORMAL = 1;
31
33
  const ATTR_COLOR = 2;
32
34
  const ATTR_EMISSIVE = 3;
@@ -96,6 +98,16 @@ export class Mesh {
96
98
  get streams() {
97
99
  return this.deformable;
98
100
  }
101
+ /**
102
+ * A dynamic mesh's positions as they stood before this frame's first rewrite, for the motion
103
+ * WebGL2's temporal resolve follows (`backend/webgl2/motionTarget.ts`). Made the first time one is
104
+ * kept, so a mesh that never moves under the resolve pays nothing.
105
+ */
106
+ previousPositions = null;
107
+ /** The frame that rewrite belongs to (`frameOfChange`), or -1 for none yet. */
108
+ changedFrame = -1;
109
+ /** A vertex array for the motion pass, without and with last frame's positions, made on demand. */
110
+ motionArrays = [null, null];
99
111
  /** The colour buffer, kept so `updateColors` can rewrite it. Null unless `dynamicColors`. */
100
112
  recolourable = null;
101
113
  /** How many floats the position buffer holds, so an update of the wrong size is refused. */
@@ -426,6 +438,73 @@ export class Mesh {
426
438
  for (const buffer of this.buffers)
427
439
  gl.deleteBuffer(buffer);
428
440
  gl.deleteVertexArray(this.vao);
441
+ for (const vao of this.motionArrays)
442
+ if (vao !== null)
443
+ gl.deleteVertexArray(vao);
444
+ if (this.previousPositions !== null)
445
+ gl.deleteBuffer(this.previousPositions);
446
+ }
447
+ /**
448
+ * Keep this dynamic mesh's positions as they stand, before a rewrite: one buffer copy on the
449
+ * device. The caller decides whether this rewrite is the frame's first (`keepsPrevious`).
450
+ */
451
+ keepPrevious(gl) {
452
+ const deformable = this.deformable;
453
+ if (deformable === null)
454
+ return;
455
+ const bytes = this.positionFloats * 4;
456
+ if (this.previousPositions === null) {
457
+ const buffer = gl.createBuffer();
458
+ if (buffer === null)
459
+ throw new Error('createBuffer failed');
460
+ gl.bindBuffer(gl.COPY_WRITE_BUFFER, buffer);
461
+ gl.bufferData(gl.COPY_WRITE_BUFFER, bytes, gl.DYNAMIC_COPY);
462
+ this.previousPositions = buffer;
463
+ }
464
+ gl.bindBuffer(gl.COPY_READ_BUFFER, deformable.positions);
465
+ gl.bindBuffer(gl.COPY_WRITE_BUFFER, this.previousPositions);
466
+ gl.copyBufferSubData(gl.COPY_READ_BUFFER, gl.COPY_WRITE_BUFFER, 0, 0, bytes);
467
+ gl.bindBuffer(gl.COPY_READ_BUFFER, null);
468
+ gl.bindBuffer(gl.COPY_WRITE_BUFFER, null);
469
+ }
470
+ /** Whether last frame's positions have been kept: a dynamic mesh rewritten under the resolve. */
471
+ get keptPrevious() {
472
+ return this.previousPositions !== null;
473
+ }
474
+ /**
475
+ * Draw this mesh for the motion pass: its positions at 0, last frame's at 1 where `previous` asks
476
+ * and they were kept, and its rig's influences where `flat`'s skinning reads them, 11, 12, 14 and
477
+ * 15. A vertex array of its own, made once a variant, so the one the lit pass draws with is never
478
+ * touched.
479
+ */
480
+ drawMotion(gl, previous) {
481
+ const at = previous && this.previousPositions !== null ? 1 : 0;
482
+ let vao = this.motionArrays[at] ?? null;
483
+ if (vao === null) {
484
+ vao = gl.createVertexArray();
485
+ if (vao === null)
486
+ throw new Error('createVertexArray failed');
487
+ gl.bindVertexArray(vao);
488
+ for (const attribute of this.layout) {
489
+ if (!MOTION_LOCATIONS.includes(attribute.location))
490
+ continue;
491
+ gl.bindBuffer(gl.ARRAY_BUFFER, attribute.buffer);
492
+ gl.enableVertexAttribArray(attribute.location);
493
+ gl.vertexAttribPointer(attribute.location, attribute.size, attribute.type, attribute.normalized, 0, 0);
494
+ }
495
+ if (at === 1 && this.previousPositions !== null) {
496
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.previousPositions);
497
+ gl.enableVertexAttribArray(1);
498
+ gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);
499
+ }
500
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
501
+ gl.bindVertexArray(null);
502
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
503
+ this.motionArrays[at] = vao;
504
+ }
505
+ gl.bindVertexArray(vao);
506
+ gl.drawElements(gl.TRIANGLES, this.indexCount, this.indexType, 0);
507
+ gl.bindVertexArray(null);
429
508
  }
430
509
  /**
431
510
  * Rewrite this mesh's positions, and its normals where the caller has them.
@@ -44,8 +44,26 @@ export interface PointSkinReach {
44
44
  }
45
45
  /** Refuse a skin whose arrays disagree with a mesh of `vertices`, or that names a joint past the rest. */
46
46
  export declare function validatePointSkin(data: PointSkinData, vertices: number): void;
47
+ /**
48
+ * Refuse a rewrite of vertices `first` onward, as many as `data` holds, that does not fit a mesh of
49
+ * `vertices`: arrays that disagree with each other, a range that is not whole vertices inside the
50
+ * mesh, or a joint past the rest. Named by the mesh's own vertex numbers.
51
+ */
52
+ export declare function validatePointSkinRange(data: PointSkinData, first: number, vertices: number): void;
47
53
  /** Measure a skin's reach once, at creation. */
48
54
  export declare function reachOf(data: PointSkinData): PointSkinReach;
55
+ /**
56
+ * A skin's reach after part of it is rewritten with `data`: the farther of the old reach and the
57
+ * new at each joint, the joints the two name between them, and the widest span of weight sums.
58
+ *
59
+ * **It widens and never narrows**, because the vertices the rewrite replaced may have been the
60
+ * ones that set a joint's reach, and finding out would mean keeping every vertex on the CPU and
61
+ * reading them all again. A skin that changes shape back and forth settles at the union of its
62
+ * shapes, which is still a box no posed vertex leaves, and culls a little less than it could. What
63
+ * would make that wrong is a skin rewritten into ever larger shapes and never back; making it
64
+ * again with `createPointSkin` measures it afresh.
65
+ */
66
+ export declare function widenReach(reach: PointSkinReach, data: PointSkinData): PointSkinReach;
49
67
  /**
50
68
  * The bounds of a pose, into the mesh's own: `matrices` holds each joint's transform into the
51
69
  * mesh's model space, sixteen floats, column-major. Allocates nothing.