@driftengine/core 4.13.6 → 4.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.json +62 -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
@@ -1,9 +1,23 @@
1
1
  import { MAX_JOINTS } from './skinPalette.js';
2
2
  /** Refuse a skin whose arrays disagree with a mesh of `vertices`, or that names a joint past the rest. */
3
3
  export function validatePointSkin(data, vertices) {
4
- const fail = (what) => {
5
- throw new Error(`point skin: ${what}`);
6
- };
4
+ checkArrays(data, vertices, 'the mesh has');
5
+ checkJoints(data, 0);
6
+ }
7
+ /**
8
+ * Refuse a rewrite of vertices `first` onward, as many as `data` holds, that does not fit a mesh of
9
+ * `vertices`: arrays that disagree with each other, a range that is not whole vertices inside the
10
+ * mesh, or a joint past the rest. Named by the mesh's own vertex numbers.
11
+ */
12
+ export function validatePointSkinRange(data, first, vertices) {
13
+ const count = data.weights.length / 4;
14
+ checkArrays(data, count, 'the weights hold');
15
+ if (!(Number.isInteger(first) && first >= 0 && first + count <= vertices)) {
16
+ throw new Error(`point skin: a rewrite of ${count} vertices from vertex ${first} for a mesh of ${vertices}`);
17
+ }
18
+ checkJoints(data, first);
19
+ }
20
+ function checkArrays(data, vertices, against) {
7
21
  for (const [name, values, width] of [
8
22
  ['joints', data.joints, 4],
9
23
  ['weights', data.weights, 4],
@@ -11,13 +25,17 @@ export function validatePointSkin(data, vertices) {
11
25
  ['normals', data.normals, 3],
12
26
  ]) {
13
27
  if (values.length !== vertices * width) {
14
- fail(`${name} holds ${values.length / width} vertices and the mesh has ${vertices}`);
28
+ throw new Error(`point skin: ${name} holds ${values.length / width} vertices and ${against} ${vertices}`);
15
29
  }
16
30
  }
31
+ }
32
+ /** Every joint a whole number below `MAX_JOINTS`; a vertex named as `first` plus its place. */
33
+ function checkJoints(data, first) {
17
34
  for (let at = 0; at < data.joints.length; at++) {
18
35
  const joint = data.joints[at];
19
36
  if (!(Number.isInteger(joint) && joint >= 0 && joint < MAX_JOINTS)) {
20
- fail(`vertex ${Math.floor(at / 4)} names joint ${joint}; a joint is 0 to ${MAX_JOINTS - 1}`);
37
+ throw new Error(`point skin: vertex ${first + Math.floor(at / 4)} names joint ${joint}; a joint is 0 to ` +
38
+ `${MAX_JOINTS - 1}`);
21
39
  }
22
40
  }
23
41
  }
@@ -54,6 +72,33 @@ export function reachOf(data) {
54
72
  sums: vertices === 0 ? [1, 1] : [least, most],
55
73
  };
56
74
  }
75
+ /**
76
+ * A skin's reach after part of it is rewritten with `data`: the farther of the old reach and the
77
+ * new at each joint, the joints the two name between them, and the widest span of weight sums.
78
+ *
79
+ * **It widens and never narrows**, because the vertices the rewrite replaced may have been the
80
+ * ones that set a joint's reach, and finding out would mean keeping every vertex on the CPU and
81
+ * reading them all again. A skin that changes shape back and forth settles at the union of its
82
+ * shapes, which is still a box no posed vertex leaves, and culls a little less than it could. What
83
+ * would make that wrong is a skin rewritten into ever larger shapes and never back; making it
84
+ * again with `createPointSkin` measures it afresh.
85
+ */
86
+ export function widenReach(reach, data) {
87
+ const added = reachOf(data);
88
+ const joints = Math.max(reach.joints, added.joints);
89
+ const widened = new Float32Array(joints).fill(-1);
90
+ for (let j = 0; j < joints; j++) {
91
+ widened[j] = Math.max(j < reach.joints ? reach.reach[j] : -1, j < added.joints ? added.reach[j] : -1);
92
+ }
93
+ const vertices = data.weights.length / 4;
94
+ return {
95
+ joints,
96
+ reach: widened,
97
+ sums: vertices === 0
98
+ ? reach.sums
99
+ : [Math.min(reach.sums[0], added.sums[0]), Math.max(reach.sums[1], added.sums[1])],
100
+ };
101
+ }
57
102
  /**
58
103
  * The bounds of a pose, into the mesh's own: `matrices` holds each joint's transform into the
59
104
  * mesh's model space, sixteen floats, column-major. Allocates nothing.
@@ -8,8 +8,9 @@
8
8
  * draw, last frame's palette.
9
9
  *
10
10
  * **Plain data, advanced by the renderer.** No renderer is needed to make one, so a consumer holds
11
- * it beside the object it belongs to. Only a reconstructing WebGPU renderer reads or writes it; it
12
- * costs nothing anywhere else. The fields past `kind` are the renderer's and a caller leaves them
11
+ * it beside the object it belongs to. A reconstructing WebGPU renderer reads and writes it, and so
12
+ * does a WebGL2 one resolving temporally (`backend/webgl2/motionTarget.ts`); it costs nothing
13
+ * anywhere else. The fields past `kind` are the renderer's and a caller leaves them
13
14
  * alone.
14
15
  *
15
16
  * **What would make this wrong** is a consumer whose objects are rebuilt every frame with no state
@@ -8,8 +8,9 @@
8
8
  * draw, last frame's palette.
9
9
  *
10
10
  * **Plain data, advanced by the renderer.** No renderer is needed to make one, so a consumer holds
11
- * it beside the object it belongs to. Only a reconstructing WebGPU renderer reads or writes it; it
12
- * costs nothing anywhere else. The fields past `kind` are the renderer's and a caller leaves them
11
+ * it beside the object it belongs to. A reconstructing WebGPU renderer reads and writes it, and so
12
+ * does a WebGL2 one resolving temporally (`backend/webgl2/motionTarget.ts`); it costs nothing
13
+ * anywhere else. The fields past `kind` are the renderer's and a caller leaves them
13
14
  * alone.
14
15
  *
15
16
  * **What would make this wrong** is a consumer whose objects are rebuilt every frame with no state
@@ -341,6 +341,8 @@ export declare class SceneTarget {
341
341
  readonly blend: number;
342
342
  readonly periodStart: boolean;
343
343
  readonly depthToViewZ: Float32Array;
344
+ /** Where the frame's movers stood last frame, or absent for the camera's motion alone. */
345
+ readonly motion?: WebGLTexture | null;
344
346
  }): void;
345
347
  dispose(): void;
346
348
  }
@@ -789,7 +789,7 @@ export class SceneTarget {
789
789
  let scene = this.texture;
790
790
  if (temporal !== undefined && this.texture !== null && this.depth !== null) {
791
791
  this.temporal ??= new TemporalPass(gl, this.floatColor);
792
- scene = this.temporal.temporalResolve(this.texture, this.depth, temporal.reprojection, this.width, this.height, temporal.blend, temporal.periodStart, temporal.depthToViewZ);
792
+ scene = this.temporal.temporalResolve(this.texture, this.depth, temporal.reprojection, this.width, this.height, temporal.blend, temporal.periodStart, temporal.depthToViewZ, temporal.motion ?? null);
793
793
  }
794
794
  let bloomTexture = null;
795
795
  if (bloom !== undefined && scene !== null) {
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Decals laid into a surface's base colour before it is lit: `setBaseDecals`, packed into
3
+ * `uBaseDecals` by `baseDecals.ts`, where what they are for and what they give up is said, and the
4
+ * material's own refusal in `uDecalReceiver` (`SurfaceMaterial.receivesDecals`).
5
+ *
6
+ * **Each decal is a test and a fetch**: the world point taken into the box by three rows, outside
7
+ * it or turned too far from the projection and nothing, else the image read across the box's face
8
+ * from its square in the fixture atlas, its colour decoded from sRGB and laid over the base colour
9
+ * by its alpha and the decal's opacity. The geometric normal decides the facing, so a normal map's
10
+ * bump does not cut a mark. The loop is over a count in a uniform and the fetch is `textureLod`, as
11
+ * every read of the atlas is, so neither needs uniform control flow: AGENTS.md 2026-08-07.
12
+ *
13
+ * **A lit switch, `BASE_DECALS`**, off until a frame first lays one, declared after
14
+ * `IRIDESCENT_OVERLAY` so each switch before it keeps its id. Compiled out, its thirty-four vectors
15
+ * are not declared and the call is a constant.
16
+ */
17
+ export declare function baseDecalsGlsl(on: boolean): string;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Decals laid into a surface's base colour before it is lit: `setBaseDecals`, packed into
3
+ * `uBaseDecals` by `baseDecals.ts`, where what they are for and what they give up is said, and the
4
+ * material's own refusal in `uDecalReceiver` (`SurfaceMaterial.receivesDecals`).
5
+ *
6
+ * **Each decal is a test and a fetch**: the world point taken into the box by three rows, outside
7
+ * it or turned too far from the projection and nothing, else the image read across the box's face
8
+ * from its square in the fixture atlas, its colour decoded from sRGB and laid over the base colour
9
+ * by its alpha and the decal's opacity. The geometric normal decides the facing, so a normal map's
10
+ * bump does not cut a mark. The loop is over a count in a uniform and the fetch is `textureLod`, as
11
+ * every read of the atlas is, so neither needs uniform control flow: AGENTS.md 2026-08-07.
12
+ *
13
+ * **A lit switch, `BASE_DECALS`**, off until a frame first lays one, declared after
14
+ * `IRIDESCENT_OVERLAY` so each switch before it keeps its id. Compiled out, its thirty-four vectors
15
+ * are not declared and the call is a constant.
16
+ */
17
+ export function baseDecalsGlsl(on) {
18
+ if (!on) {
19
+ return /* glsl */ `
20
+ const bool BASE_DECALS = false; // wgsl:override
21
+ vec3 baseDecals(vec3 base, vec3 world, vec3 geometric) { return base; }
22
+ `;
23
+ }
24
+ return /* glsl */ `
25
+ /* Whether this program lays decals into base colours. Off but where a frame has laid one. */
26
+ const bool BASE_DECALS = true; // wgsl:override
27
+ /*
28
+ * A header, how many decals, the atlas row their band starts at and its squares' side (0); then
29
+ * four vectors a decal, eight decals: the three rows taking a world point into its box, x across,
30
+ * y up the image, z along the projection, and its image, opacity and facing cosine.
31
+ */
32
+ uniform vec4 uBaseDecals[33]; // wgsl:material
33
+ /* x 1 where the material refuses decals, so an unwritten zero receives them. */
34
+ uniform vec4 uDecalReceiver; // wgsl:material
35
+
36
+ vec3 decalLinear(vec3 c) {
37
+ return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));
38
+ }
39
+
40
+ vec3 baseDecals(vec3 base, vec3 world, vec3 geometric) {
41
+ if (uDecalReceiver.x > 0.5) return base;
42
+ int count = int(uBaseDecals[0].x + 0.5);
43
+ vec2 atlas = vec2(textureSize(uFixtureAtlas, 0));
44
+ float span = uBaseDecals[0].z;
45
+ /* Half a texel of a square, so a bilinear read at its edge cannot reach the next one. */
46
+ float inset = 0.5 / span;
47
+ for (int i = 0; i < 8; i++) {
48
+ if (i >= count) break;
49
+ vec4 across = uBaseDecals[1 + i * 4];
50
+ vec4 up = uBaseDecals[2 + i * 4];
51
+ vec4 along = uBaseDecals[3 + i * 4];
52
+ vec4 laid = uBaseDecals[4 + i * 4];
53
+ vec3 local = vec3(
54
+ dot(across.xyz, world) + across.w,
55
+ dot(up.xyz, world) + up.w,
56
+ dot(along.xyz, world) + along.w
57
+ );
58
+ if (any(greaterThan(abs(local), vec3(1.0)))) continue;
59
+ if (dot(geometric, -normalize(along.xyz)) < laid.z) continue;
60
+ vec2 face = vec2(local.x, -local.y) * 0.5 + 0.5;
61
+ vec2 at = vec2(
62
+ (laid.x + clamp(face.x, inset, 1.0 - inset)) * span / atlas.x,
63
+ (uBaseDecals[0].y + clamp(face.y, inset, 1.0 - inset) * span) / atlas.y
64
+ );
65
+ vec4 texel = textureLod(uFixtureAtlas, at, 0.0);
66
+ base = mix(base, decalLinear(texel.rgb), clamp(texel.a * laid.y, 0.0, 1.0));
67
+ }
68
+ return base;
69
+ }
70
+ `;
71
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * A material's glow moved and masked: `SurfaceMaterial.emissiveScroll` and `emissiveMaskLayer`,
3
+ * packed into `uEmissiveFlow` by `emissiveFlow.ts`, where what each gives up is said.
4
+ *
5
+ * **The scroll** is added to wherever the emissive map is read, the mesh's coordinates, a layer's
6
+ * place or a projection's planes, as its own repeats a second times the surface clock, wrapped to
7
+ * one repeat. **The mask** is the alpha of a layer of the emissive map's own array at the mesh's
8
+ * first coordinates as they are: the vertex stage has already laid the material's repeat and offset
9
+ * on `vUv`, so the second vector holds their inverse, which gives the mesh's own back.
10
+ *
11
+ * **A lit switch, `EMISSIVE_FLOW`**, off until a material first asks, declared after
12
+ * `SURFACE_DETAIL` so each switch before it keeps its id. Compiled out, its two vectors are not
13
+ * declared and the shift and the mask are constants.
14
+ */
15
+ export declare function emissiveFlowGlsl(on: boolean): string;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * A material's glow moved and masked: `SurfaceMaterial.emissiveScroll` and `emissiveMaskLayer`,
3
+ * packed into `uEmissiveFlow` by `emissiveFlow.ts`, where what each gives up is said.
4
+ *
5
+ * **The scroll** is added to wherever the emissive map is read, the mesh's coordinates, a layer's
6
+ * place or a projection's planes, as its own repeats a second times the surface clock, wrapped to
7
+ * one repeat. **The mask** is the alpha of a layer of the emissive map's own array at the mesh's
8
+ * first coordinates as they are: the vertex stage has already laid the material's repeat and offset
9
+ * on `vUv`, so the second vector holds their inverse, which gives the mesh's own back.
10
+ *
11
+ * **A lit switch, `EMISSIVE_FLOW`**, off until a material first asks, declared after
12
+ * `SURFACE_DETAIL` so each switch before it keeps its id. Compiled out, its two vectors are not
13
+ * declared and the shift and the mask are constants.
14
+ */
15
+ export function emissiveFlowGlsl(on) {
16
+ if (!on) {
17
+ return /* glsl */ `
18
+ const bool EMISSIVE_FLOW = false; // wgsl:override
19
+ vec2 emissiveShift() { return vec2(0.0); }
20
+ float emissiveMask() { return 1.0; }
21
+ `;
22
+ }
23
+ return /* glsl */ `
24
+ /* Whether this program moves and masks a material's glow. Off but where one has asked. */
25
+ const bool EMISSIVE_FLOW = true; // wgsl:override
26
+ /*
27
+ * Two vectors, as emissiveFlow.ts packs them: the scroll in repeats a second on u and v (x y) and
28
+ * the mask's layer of the emissive array plus one, 0 for none (z); then what takes the material's
29
+ * coordinates back to the mesh's own, a scale (x y) and an offset (z w).
30
+ */
31
+ uniform vec4 uEmissiveFlow[2]; // wgsl:material
32
+
33
+ /* The emissive map's coordinates moved by the clock, wrapped to one repeat for precision. */
34
+ vec2 emissiveShift() {
35
+ return fract(uEmissiveFlow[0].xy * uSurfaceScene.x);
36
+ }
37
+
38
+ /* The mask's alpha at the mesh's own coordinates, under the branch on a bound emissive map. */
39
+ float emissiveMask() {
40
+ if (uEmissiveFlow[0].z < 0.5) return 1.0;
41
+ vec2 own = vUv.xy * uEmissiveFlow[1].xy + uEmissiveFlow[1].zw;
42
+ return texture(uEmissiveMap, vec3(own, uEmissiveFlow[0].z - 1.0)).a;
43
+ }
44
+ `;
45
+ }
@@ -8,7 +8,7 @@ export declare function glassTintGlsl(glassShadows: boolean): string;
8
8
  * The lit stage's switches, by the name of the constant each is: the decision both backends make
9
9
  * about which optional code a lit program carries. See `litSwitchesGlsl`.
10
10
  */
11
- export type LitSwitch = 'GLASS_SHADOWS' | 'CLUSTERED_LIGHTS' | 'LIGHT_FIXTURES' | 'SURFACE_EFFECTS' | 'DRIFT_LIGHT' | 'PHYSICAL_SPECULAR' | 'SURFACE_OVERLAY' | 'REFLECTION_MAPS' | 'WORLD_UVS' | 'MOVING_SUN' | 'LAYERED' | 'LAYER_LOOKS' | 'SURFACE_DETAIL';
11
+ export type LitSwitch = 'GLASS_SHADOWS' | 'CLUSTERED_LIGHTS' | 'LIGHT_FIXTURES' | 'SURFACE_EFFECTS' | 'DRIFT_LIGHT' | 'PHYSICAL_SPECULAR' | 'SURFACE_OVERLAY' | 'REFLECTION_MAPS' | 'WORLD_UVS' | 'MOVING_SUN' | 'LAYERED' | 'LAYER_LOOKS' | 'SURFACE_DETAIL' | 'EMISSIVE_FLOW' | 'IRIDESCENT_OVERLAY' | 'BASE_DECALS';
12
12
  /** Every switch off: what a renderer starts from before its profile and its content say otherwise. */
13
13
  export declare function noLitSwitches(): Record<LitSwitch, boolean>;
14
14
  /** Which of the lit stage's optional features a build reads. See `litSwitchesGlsl`. */
@@ -39,6 +39,9 @@ export function noLitSwitches() {
39
39
  LAYERED: false,
40
40
  LAYER_LOOKS: false,
41
41
  SURFACE_DETAIL: false,
42
+ EMISSIVE_FLOW: false,
43
+ IRIDESCENT_OVERLAY: false,
44
+ BASE_DECALS: false,
42
45
  };
43
46
  }
44
47
  /**
@@ -150,6 +150,22 @@ export interface FlatShaderOptions {
150
150
  * (\`SurfaceMaterial.detail\`). True when absent. See \`detailGlsl\`.
151
151
  */
152
152
  readonly surfaceDetail?: boolean;
153
+ /**
154
+ * Whether it moves and masks a material's glow, `uEmissiveFlow` and all
155
+ * (`SurfaceMaterial.emissiveScroll` and `emissiveMaskLayer`). True when absent. See
156
+ * `emissiveFlowGlsl`.
157
+ */
158
+ readonly emissiveFlow?: boolean;
159
+ /**
160
+ * Whether it lays a material's iridescent overlay on the base colour, `uIridescence` and all
161
+ * (`SurfaceMaterial.iridescentOverlay`). True when absent. See `iridescentOverlayGlsl`.
162
+ */
163
+ readonly iridescentOverlay?: boolean;
164
+ /**
165
+ * Whether it lays the frame's decals into base colours, `uBaseDecals` and all
166
+ * (`setBaseDecals`). True when absent. See `baseDecalsGlsl`.
167
+ */
168
+ readonly baseDecals?: boolean;
153
169
  /**
154
170
  * How many point lights this build declares room for. `MAX_POINT_LIGHTS` when absent.
155
171
  *
@@ -25,6 +25,9 @@ import { reflectionSurfaceGlsl } from './reflectionSurface.js';
25
25
  import { worldUvGlsl } from './worldUv.js';
26
26
  import { movingSunGlsl } from './movingSun.js';
27
27
  import { detailGlsl } from './detail.js';
28
+ import { emissiveFlowGlsl } from './emissiveFlow.js';
29
+ import { iridescentOverlayGlsl } from './iridescentOverlay.js';
30
+ import { baseDecalsGlsl } from './baseDecals.js';
28
31
  import { layeredGlsl } from './layered.js';
29
32
  import { MAP_READS_GLSL } from './mapReads.js';
30
33
  import { CHANNEL_BEND, LIT_CHANNEL_ATTRIBUTE } from '../vertexChannel.js';
@@ -517,6 +520,12 @@ export function flatFrag(options) {
517
520
  layeredGlsl(options.layered ?? true, options.layerLooks ?? true),
518
521
  /* After the layers, so its switch takes the next id; main calls it. See detail.ts. */
519
522
  detailGlsl(options.surfaceDetail ?? true),
523
+ /* After the detail, so its switch takes the next id; the map reads call it. emissiveFlow.ts. */
524
+ emissiveFlowGlsl(options.emissiveFlow ?? true),
525
+ /* After the glow's flow, so its switch takes the next id; main calls it. iridescentOverlay.ts. */
526
+ iridescentOverlayGlsl(options.iridescentOverlay ?? true),
527
+ /* After the overlay, so its switch takes the next id; main calls it. baseDecals.ts. */
528
+ baseDecalsGlsl(options.baseDecals ?? true),
520
529
  /* Where each map is read, after both of the ways it can be read otherwise. See mapReads.ts. */
521
530
  MAP_READS_GLSL,
522
531
  /* After the frame and the projection it calls, unconditional for the same reason as the
@@ -0,0 +1,15 @@
1
+ /**
2
+ * A surface's iridescent overlay on its base colour, before it is lit:
3
+ * `SurfaceMaterial.iridescentOverlay`, packed into `uIridescence` by `iridescentOverlay.ts`, whose
4
+ * `iridescentBase` is this arithmetic on the CPU and says what each term is and what it gives up.
5
+ *
6
+ * **Called once the normal is mapped and turned to the viewer**, so the view term reads the surface
7
+ * a pixel shows, and before any light is added, so the lights below read the colour it makes. The
8
+ * mask is read in uniform control flow, under the lighting branch and this switch's, so its implicit
9
+ * level is legal: AGENTS.md 2026-08-07.
10
+ *
11
+ * **A lit switch, `IRIDESCENT_OVERLAY`**, off until a material first asks, declared after
12
+ * `EMISSIVE_FLOW` so each switch before it keeps its id. Compiled out, its thirty-six vectors are
13
+ * not declared and the call is a constant.
14
+ */
15
+ export declare function iridescentOverlayGlsl(on: boolean): string;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * A surface's iridescent overlay on its base colour, before it is lit:
3
+ * `SurfaceMaterial.iridescentOverlay`, packed into `uIridescence` by `iridescentOverlay.ts`, whose
4
+ * `iridescentBase` is this arithmetic on the CPU and says what each term is and what it gives up.
5
+ *
6
+ * **Called once the normal is mapped and turned to the viewer**, so the view term reads the surface
7
+ * a pixel shows, and before any light is added, so the lights below read the colour it makes. The
8
+ * mask is read in uniform control flow, under the lighting branch and this switch's, so its implicit
9
+ * level is legal: AGENTS.md 2026-08-07.
10
+ *
11
+ * **A lit switch, `IRIDESCENT_OVERLAY`**, off until a material first asks, declared after
12
+ * `EMISSIVE_FLOW` so each switch before it keeps its id. Compiled out, its thirty-six vectors are
13
+ * not declared and the call is a constant.
14
+ */
15
+ export function iridescentOverlayGlsl(on) {
16
+ if (!on) {
17
+ return /* glsl */ `
18
+ const bool IRIDESCENT_OVERLAY = false; // wgsl:override
19
+ bool iridescent() { return false; }
20
+ vec3 iridescentBase(vec3 base, vec3 n, vec2 uv) { return base; }
21
+ `;
22
+ }
23
+ return /* glsl */ `
24
+ /* Whether this program lays an iridescent overlay. Off but where a material has asked. */
25
+ const bool IRIDESCENT_OVERLAY = true; // wgsl:override
26
+ /*
27
+ * Thirty-six vectors, as iridescentOverlay.ts packs them: the direction toward the sheen and the
28
+ * blend (0); the contrast, the tiling, the weight and 1 for on (1); the mask's array, 0 none, 1 ORM,
29
+ * 2 model, 3 albedo, 4 emissive, its layer and its channel (2); what takes the material's
30
+ * coordinates back to the mesh's own (3); then the ramp, thirty-two colours.
31
+ */
32
+ uniform vec4 uIridescence[36]; // wgsl:material
33
+
34
+ bool iridescent() { return uIridescence[1].w > 0.5; }
35
+
36
+ /* The mask's channel at the material's coordinates; 1 where it names no array. */
37
+ float iridescentMask(vec2 uv) {
38
+ float source = uIridescence[2].x;
39
+ if (source < 0.5) return 1.0;
40
+ vec3 at = vec3(uv, uIridescence[2].y);
41
+ vec4 texel = source < 1.5 ? texture(uOrmMap, at)
42
+ : source < 2.5 ? texture(uModelMap, at)
43
+ : source < 3.5 ? texture(uAlbedo, at)
44
+ : texture(uEmissiveMap, at);
45
+ return texel[int(uIridescence[2].z + 0.5)];
46
+ }
47
+
48
+ vec3 iridescentBase(vec3 base, vec3 n, vec2 uv) {
49
+ vec3 toEye = normalize(uCameraPos - vWorldPos);
50
+ vec3 halfway = toEye + uIridescence[0].xyz;
51
+ float span = length(halfway);
52
+ float nh = span > 0.0 ? dot(n, halfway / span) : 0.0;
53
+ float sheen = nh > 0.0 ? clamp(pow(nh, uIridescence[1].x), 0.0, 1.0) : 0.0;
54
+ float nv = dot(n, toEye);
55
+ vec2 own = vUv.xy * uIridescence[3].xy + uIridescence[3].zw;
56
+ float x = nv + own.x + uIridescence[0].w * (sheen - nv);
57
+ float t = clamp(fract(x * uIridescence[1].y), 0.0, 1.0);
58
+ float at = t * 31.0;
59
+ int below = min(int(floor(at)), 30);
60
+ vec3 o = mix(uIridescence[4 + below].rgb, uIridescence[5 + below].rgb, at - float(below));
61
+ vec3 blended = mix(2.0 * base * o, 1.0 - 2.0 * (1.0 - base) * (1.0 - o), step(0.5, base));
62
+ return base + (blended - base) * (x * iridescentMask(uv) * uIridescence[1].z);
63
+ }
64
+ `;
65
+ }
@@ -40,7 +40,7 @@ vec3 layeredTexel() { return vec3(0.0, 0.0, 1.0); }
40
40
  bool layeredMeshNormal() { return false; }
41
41
  vec3 layeredMeshTexel() { return vec3(0.0, 0.0, 1.0); }
42
42
  bool layeredGlows() { return false; }
43
- vec3 layeredEmissive() { return vec3(0.0); }
43
+ vec3 layeredEmissive(vec2 shift) { return vec3(0.0); }
44
44
  float layeredAmbient() { return 1.0; }
45
45
  float layeredSpecular(float own) { return own; }
46
46
  `;
@@ -253,10 +253,10 @@ bool layeredGlows() {
253
253
  return uLayers[1].z > -0.5;
254
254
  }
255
255
 
256
- /* The emissive map at its layer's place, glowing by that layer's share. */
257
- vec3 layeredEmissive() {
256
+ /* The emissive map at its layer's place, moved by its scroll, glowing by that layer's share. */
257
+ vec3 layeredEmissive(vec2 shift) {
258
258
  int glow = int(uLayers[1].z + 0.5);
259
- return texture(uEmissiveMap, layerAt(glow)).rgb * layerShare(glow);
259
+ return texture(uEmissiveMap, layerAt(glow) + vec3(shift, 0.0)).rgb * layerShare(glow);
260
260
  }
261
261
  `;
262
262
  }