@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
@@ -8,7 +8,7 @@
8
8
  * for every resource a later write in the frame overwrites. A skin posed twice in a frame draws its
9
9
  * last pose everywhere, as a mesh rewritten twice with `updateMesh` does.
10
10
  */
11
- import { reachOf, type PointSkinData, type PointSkinReach } from '../../pointSkin.ts';
11
+ import { reachOf, widenReach, type PointSkinData, type PointSkinReach } from '../../pointSkin.ts';
12
12
  import type { GpuMesh } from './buffers.ts';
13
13
  import { POINT_SKIN_WGSL, POINT_SKIN_WORKGROUP } from './shaders/pointSkin.wgsl.ts';
14
14
 
@@ -30,12 +30,20 @@ export function createPointSkinPipeline(device: GPUDevice): GPUComputePipeline {
30
30
 
31
31
  export class GpuPointSkin {
32
32
  readonly mesh: GpuMesh;
33
- readonly reach: PointSkinReach;
33
+ /** Widened by each rewrite, never narrowed: see `widenReach`. */
34
+ reach: PointSkinReach;
34
35
  private readonly count: number;
35
36
  private readonly pipeline: GPUComputePipeline;
36
- private readonly joints: GPUBuffer;
37
+ private joints: GPUBuffer;
38
+ /** Params, points with their weights, joints named, normals: in that order. */
37
39
  private readonly buffers: readonly GPUBuffer[];
38
- private readonly group: GPUBindGroup;
40
+ private group: GPUBindGroup;
41
+ /** A rewrite's vertices laid out as the buffers hold them, grown to the largest rewrite. */
42
+ private scratch = {
43
+ points: new Float32Array(0),
44
+ named: new Uint32Array(0),
45
+ normals: new Float32Array(0),
46
+ };
39
47
 
40
48
  constructor(device: GPUDevice, pipeline: GPUComputePipeline, mesh: GpuMesh, data: PointSkinData) {
41
49
  this.mesh = mesh;
@@ -44,15 +52,11 @@ export class GpuPointSkin {
44
52
  const count = data.weights.length / 4;
45
53
  this.count = count;
46
54
 
47
- /* Each influence a vec4: its point, and its weight in w. */
48
- const points = new Float32Array(count * 16);
49
- for (let i = 0; i < count * 4; i++) {
50
- points.set(data.points.subarray(i * 3, i * 3 + 3), i * 4);
51
- points[i * 4 + 3] = data.weights[i] as number;
52
- }
53
- const named = Uint32Array.from(data.joints);
54
- const normals = new Float32Array(count * 4);
55
- for (let v = 0; v < count; v++) normals.set(data.normals.subarray(v * 3, v * 3 + 3), v * 4);
55
+ const { points, named, normals } = laidOut(data, count, {
56
+ points: new Float32Array(count * 16),
57
+ named: new Uint32Array(count * 4),
58
+ normals: new Float32Array(count * 4),
59
+ });
56
60
  const params = new Uint32Array([count, 0, 0, 0]);
57
61
 
58
62
  const made = (
@@ -68,22 +72,22 @@ export class GpuPointSkin {
68
72
  device.queue.writeBuffer(buffer, 0, contents);
69
73
  return buffer;
70
74
  };
71
- this.joints = device.createBuffer({
72
- label: 'pointSkin.joints',
73
- size: Math.max(1, this.reach.joints) * 64,
74
- usage: STORAGE | COPY_DST,
75
- });
76
- const buffers = [
75
+ this.joints = jointBuffer(device, this.reach.joints);
76
+ this.buffers = [
77
77
  made('pointSkin.params', params, UNIFORM),
78
78
  made('pointSkin.points', points, STORAGE),
79
79
  made('pointSkin.named', named, STORAGE),
80
80
  made('pointSkin.normals', normals, STORAGE),
81
81
  ];
82
- this.buffers = buffers;
83
- const [positionsOut, normalsOut] = mesh.vertexBuffers as [GPUBuffer, GPUBuffer];
84
- this.group = device.createBindGroup({
82
+ this.group = this.bind(device);
83
+ }
84
+
85
+ private bind(device: GPUDevice): GPUBindGroup {
86
+ const buffers = this.buffers;
87
+ const [positionsOut, normalsOut] = this.mesh.vertexBuffers as [GPUBuffer, GPUBuffer];
88
+ return device.createBindGroup({
85
89
  label: 'pointSkin.pose',
86
- layout: pipeline.getBindGroupLayout(0),
90
+ layout: this.pipeline.getBindGroupLayout(0),
87
91
  entries: [
88
92
  { binding: 0, resource: { buffer: buffers[0] as GPUBuffer } },
89
93
  { binding: 1, resource: { buffer: this.joints } },
@@ -96,6 +100,37 @@ export class GpuPointSkin {
96
100
  });
97
101
  }
98
102
 
103
+ /**
104
+ * Rewrite vertices `first` onward, as many as `data` holds, in place: three queue writes at their
105
+ * offsets. **A queue write reaches every dispatch submitted after it**, so the renderer submits a
106
+ * pose already recorded before calling this, and the rewrite reaches the next pose alone, as it
107
+ * does on WebGL2. A joint none of the skin named before grows the joints buffer, which is made
108
+ * again with its bind group; the old one goes once the work already submitted is done with it.
109
+ */
110
+ rewrite(device: GPUDevice, first: number, data: PointSkinData): void {
111
+ const count = data.weights.length / 4;
112
+ const scratch = this.scratch;
113
+ if (scratch.named.length < count * 4) {
114
+ this.scratch = {
115
+ points: new Float32Array(count * 16),
116
+ named: new Uint32Array(count * 4),
117
+ normals: new Float32Array(count * 4),
118
+ };
119
+ }
120
+ const { points, named, normals } = laidOut(data, count, this.scratch);
121
+ const buffers = this.buffers;
122
+ device.queue.writeBuffer(buffers[1] as GPUBuffer, first * 64, points, 0, count * 16);
123
+ device.queue.writeBuffer(buffers[2] as GPUBuffer, first * 16, named, 0, count * 4);
124
+ device.queue.writeBuffer(buffers[3] as GPUBuffer, first * 16, normals, 0, count * 4);
125
+ const joints = this.reach.joints;
126
+ this.reach = widenReach(this.reach, data);
127
+ if (Math.max(1, this.reach.joints) > Math.max(1, joints)) {
128
+ this.joints.destroy();
129
+ this.joints = jointBuffer(device, this.reach.joints);
130
+ this.group = this.bind(device);
131
+ }
132
+ }
133
+
99
134
  /** Write the pose's joints and record the dispatch that poses every vertex into `encoder`. */
100
135
  pose(device: GPUDevice, encoder: GPUCommandEncoder, matrices: Float32Array): void {
101
136
  if (this.reach.joints > 0) {
@@ -113,3 +148,35 @@ export class GpuPointSkin {
113
148
  for (const buffer of this.buffers) buffer.destroy();
114
149
  }
115
150
  }
151
+
152
+ function jointBuffer(device: GPUDevice, joints: number): GPUBuffer {
153
+ return device.createBuffer({
154
+ label: 'pointSkin.joints',
155
+ size: Math.max(1, joints) * 64,
156
+ usage: STORAGE | COPY_DST,
157
+ });
158
+ }
159
+
160
+ /**
161
+ * `count` vertices of `data` as the buffers hold them, into `out`'s leading entries: each influence
162
+ * a vec4 of its point and its weight in w, the joints as integers, each normal a vec4.
163
+ */
164
+ function laidOut(
165
+ data: PointSkinData,
166
+ count: number,
167
+ out: { points: Float32Array; named: Uint32Array; normals: Float32Array },
168
+ ): { points: Float32Array; named: Uint32Array; normals: Float32Array } {
169
+ for (let i = 0; i < count * 4; i++) {
170
+ out.points[i * 4] = data.points[i * 3] as number;
171
+ out.points[i * 4 + 1] = data.points[i * 3 + 1] as number;
172
+ out.points[i * 4 + 2] = data.points[i * 3 + 2] as number;
173
+ out.points[i * 4 + 3] = data.weights[i] as number;
174
+ out.named[i] = data.joints[i] as number;
175
+ }
176
+ for (let v = 0; v < count; v++) {
177
+ out.normals[v * 4] = data.normals[v * 3] as number;
178
+ out.normals[v * 4 + 1] = data.normals[v * 3 + 1] as number;
179
+ out.normals[v * 4 + 2] = data.normals[v * 3 + 2] as number;
180
+ }
181
+ return out;
182
+ }
@@ -125,7 +125,7 @@ import { ProbeSweeps } from '../../probeSweeps.ts';
125
125
  import { ggxMaxLevelFor, irradianceLevelFor, octahedralEdgeFor } from '../../prefilterEnvMap.ts';
126
126
  import { environmentTexels } from './environmentTexels.ts';
127
127
  import { packIesAtlas } from '../../iesProfile.ts';
128
- import { fixtureAtlasLayout, fixtureAtlasTexels } from '../../fixtureAtlas.ts';
128
+ import { decalBandTop, fixtureAtlasLayout, fixtureAtlasTexels } from '../../fixtureAtlas.ts';
129
129
  import { resolveSurfaceScene } from '../../surfaceScene.ts';
130
130
  import { COOKIE_TILE } from '../../lightBudget.ts';
131
131
  import { FULL_LIGHT_BUDGET } from '../../uniformVectorBudget.ts';
@@ -192,6 +192,7 @@ import { GpuPointSkin, createPointSkinPipeline } from './pointSkinPass.ts';
192
192
  import {
193
193
  poseBounds,
194
194
  validatePointSkin,
195
+ validatePointSkinRange,
195
196
  validatePose,
196
197
  type PointSkinData,
197
198
  } from '../../pointSkin.ts';
@@ -232,6 +233,20 @@ import {
232
233
  type SurfaceDetail,
233
234
  } from '../../surfaceDetail.ts';
234
235
  import { LOOK_FLOATS, asksLayerLooks, packLayerLooks } from '../../surfaceLayerLooks.ts';
236
+ import { EMISSIVE_FLOW_FLOATS, packEmissiveFlow } from '../../emissiveFlow.ts';
237
+ import {
238
+ BASE_DECAL_FLOATS,
239
+ BASE_DECAL_TILE,
240
+ packBaseDecals,
241
+ type BaseDecal,
242
+ } from '../../baseDecals.ts';
243
+ import { sourceSize } from '../../textureSource.ts';
244
+ import {
245
+ IRIDESCENT_FLOATS,
246
+ iridescentRefusal,
247
+ packIridescentOverlay,
248
+ type IridescentOverlay,
249
+ } from '../../iridescentOverlay.ts';
235
250
  import {
236
251
  frameReflectionsRefused,
237
252
  resolveFrameReflections,
@@ -439,6 +454,7 @@ import {
439
454
  } from '../../lightBudget.ts';
440
455
  import { resolveRenderQuality, type RenderQuality } from '../../renderQuality.ts';
441
456
  import {
457
+ mediumSkyFill,
442
458
  DEFAULT_GLOBAL_MEDIUM,
443
459
  WEAK_GPU_MEDIUM_STEPS,
444
460
  createResolvedMediumLamps,
@@ -651,6 +667,8 @@ import { fitProbeGrid } from '../../gi/probeGridFit.ts';
651
667
  * `fitProbeGrid` widens it uniformly when the budget bites, so a large scene gets a coarser grid
652
668
  * rather than a truncated one.
653
669
  */
670
+ /** The ambient a medium is lit by in a frame bound with no environment: none. */
671
+ const MEDIUM_NO_AMBIENT: readonly number[] = [0, 0, 0];
654
672
  const INDIRECT_PROBE_SPACING = 2;
655
673
 
656
674
  /** What the trace is given when a frame never said what was lighting it. */
@@ -3057,6 +3075,12 @@ export class WebGPURenderer implements RendererApi {
3057
3075
  private warnedLayersModelMap = false;
3058
3076
  /** `uDetail`, the material's tiled detail packed by `packSurfaceDetail`. */
3059
3077
  private readonly detailFloats = new Float32Array(DETAIL_FLOATS);
3078
+ /** `uEmissiveFlow`, a material's glow moved and masked, packed by `packEmissiveFlow`. */
3079
+ private readonly flowFloats = new Float32Array(EMISSIVE_FLOW_FLOATS);
3080
+ /** `uIridescence`, a material's iridescent overlay packed by `packIridescentOverlay`. */
3081
+ private readonly iridescentFloats = new Float32Array(IRIDESCENT_FLOATS);
3082
+ /** The refusals of an overlay already said, each once. */
3083
+ private readonly warnedIridescent = new Set<string>();
3060
3084
  /** The detail refusals already said, each once. See `detailOfMaterial`. */
3061
3085
  private readonly warnedDetail = new Set<DetailRefusal>();
3062
3086
  private warnedLayersTriplanar = false;
@@ -3607,6 +3631,16 @@ export class WebGPURenderer implements RendererApi {
3607
3631
  f[emissive] = asked?.[0] ?? 1;
3608
3632
  f[emissive + 1] = asked?.[1] ?? 1;
3609
3633
  f[emissive + 2] = asked?.[2] ?? 1;
3634
+ /* And its glow's scroll and mask, the first time a material asks: emissiveFlow.ts. */
3635
+ if (packEmissiveFlow(material, this.flowFloats)) this.enableLit('EMISSIVE_FLOW');
3636
+ f.set(this.flowFloats, this.materialField('uEmissiveFlow'));
3637
+ /* And its iridescent overlay, the first time a material lays one: iridescentOverlay.ts. */
3638
+ const iridescent = this.iridescentOf(material);
3639
+ packIridescentOverlay(iridescent, material ?? {}, this.iridescentFloats);
3640
+ f.set(this.iridescentFloats, this.materialField('uIridescence'));
3641
+ /* And whether the frame's decals mark it. See `SurfaceMaterial.receivesDecals`. */
3642
+ f[this.materialField('uDecalReceiver')] = material?.receivesDecals === false ? 1 : 0;
3643
+ if (iridescent !== null) this.enableLit('IRIDESCENT_OVERLAY');
3610
3644
  /* The model's numbers and whether its map is bound. See `packModel`. */
3611
3645
  packModel(
3612
3646
  material?.model ?? null,
@@ -6426,6 +6460,21 @@ export class WebGPURenderer implements RendererApi {
6426
6460
  }
6427
6461
 
6428
6462
  /** One of the lit stage's switches on, in every cache a lit pipeline can come from. */
6463
+ /** A material's iridescent overlay, or null where it is malformed, said once. */
6464
+ private iridescentOf(
6465
+ material: SurfaceMaterial<GpuSurfaceTexture> | null,
6466
+ ): IridescentOverlay | null {
6467
+ const overlay = material?.iridescentOverlay ?? null;
6468
+ if (overlay === null) return null;
6469
+ const refusal = iridescentRefusal(overlay);
6470
+ if (refusal === null) return overlay;
6471
+ if (!this.warnedIridescent.has(refusal)) {
6472
+ this.warnedIridescent.add(refusal);
6473
+ console.warn(`Renderer: an iridescent overlay is refused: ${refusal}. Drawn without it.`);
6474
+ }
6475
+ return null;
6476
+ }
6477
+
6429
6478
  private enableLit(feature: LitSwitch): void {
6430
6479
  if (this.pipelines.litSwitches[feature]) return;
6431
6480
  void this.pipelines.enable(feature);
@@ -7759,6 +7808,9 @@ export class WebGPURenderer implements RendererApi {
7759
7808
  { binding: TAA_TEXTURES.uFlicker },
7760
7809
  { binding: TAA_TEXTURES.uFlickerMotion },
7761
7810
  { binding: TAA_TEXTURES.uDepth, filterable: false },
7811
+ /* WebGL2's movers' motion (`motionTarget.ts`); here the depth stands in and is never read,
7812
+ `uMotionOn` being 0, since this backend follows movers through its reconstruction. */
7813
+ { binding: TAA_TEXTURES.uMotion, filterable: false },
7762
7814
  ]);
7763
7815
  this.taaUniforms = device.createBuffer({
7764
7816
  label: 'post.taaUniforms',
@@ -8932,6 +8984,19 @@ export class WebGPURenderer implements RendererApi {
8932
8984
  * a destroyed buffer in a submit loses the whole command buffer. Nothing else reads them, so once
8933
8985
  * the rewrites are submitted, which only moves them earlier, the buffers can go at once.
8934
8986
  */
8987
+ /**
8988
+ * Rewrite part of a skin in place. **A pose already recorded is submitted first**, since a queue
8989
+ * write reaches every dispatch submitted after it and would otherwise reach a pose the caller made
8990
+ * before the rewrite; then the pose after it alone reads the rewrite, as on WebGL2. A rewrite with
8991
+ * nothing pending submits nothing.
8992
+ */
8993
+ updatePointSkin(skin: GpuPointSkin, first: number, data: PointSkinData): void {
8994
+ validatePointSkinRange(data, first, skin.mesh.vertexCount);
8995
+ if (this.surface.lost) return;
8996
+ if (this.pendingRewrites !== null) this.submit([]);
8997
+ skin.rewrite(this.surface.device, first, data);
8998
+ }
8999
+
8935
9000
  disposePointSkin(skin: GpuPointSkin): void {
8936
9001
  if (this.pendingRewrites !== null && !this.surface.lost) this.submit([]);
8937
9002
  skin.dispose();
@@ -9094,6 +9159,13 @@ export class WebGPURenderer implements RendererApi {
9094
9159
  f.fill(0, at('uLayerLooks'), at('uLayerLooks') + LOOK_FLOATS);
9095
9160
  /* Nor detail. */
9096
9161
  f.fill(0, at('uDetail'), at('uDetail') + DETAIL_FLOATS);
9162
+ /* Nor a glow that moves or is masked. */
9163
+ f.fill(0, at('uEmissiveFlow'), at('uEmissiveFlow') + EMISSIVE_FLOW_FLOATS);
9164
+ /* Nor an iridescent overlay. */
9165
+ f.fill(0, at('uIridescence'), at('uIridescence') + IRIDESCENT_FLOATS);
9166
+ /* The frame's decals as last laid, held across passes, and every surface receiving them. */
9167
+ f.set(this.baseDecalFloats, at('uBaseDecals'));
9168
+ f[at('uDecalReceiver')] = 0;
9097
9169
  f[at('uCutout')] = 0;
9098
9170
  f[at('uCutout') + 1] = 0;
9099
9171
  this.cutoutResolveStaged = 'hard';
@@ -13840,6 +13912,8 @@ export class WebGPURenderer implements RendererApi {
13840
13912
  f[depthRow + 3] = 1;
13841
13913
  }
13842
13914
  f[at('uAntiFlicker')] = 1;
13915
+ /* The camera's motion alone: this backend follows a mover through its reconstruction. */
13916
+ f[at('uMotionOn')] = 0;
13843
13917
  device.queue.writeBuffer(this.taaUniforms, 0, this.taaStaging);
13844
13918
 
13845
13919
  const pass = encoder.beginRenderPass({
@@ -14330,7 +14404,7 @@ export class WebGPURenderer implements RendererApi {
14330
14404
  f.set(this.frameEye, at('uCameraPos'));
14331
14405
  f.set(env?.directionalDir ?? [0, 1, 0], at('uSunDir'));
14332
14406
  f.set(env?.directionalColor ?? [0, 0, 0], at('uSunColor'));
14333
- f.set(env?.ambient ?? [0, 0, 0], at('uAmbient'));
14407
+ f.set(mediumSkyFill(options, env?.ambient ?? MEDIUM_NO_AMBIENT), at('uAmbient'));
14334
14408
  f[at('uDensity')] = options.density;
14335
14409
  const height = at('uHeight');
14336
14410
  f[height] = options.heightFalloff;
@@ -17647,6 +17721,14 @@ export class WebGPURenderer implements RendererApi {
17647
17721
  /** The profiles as packed and the cookie images, kept so either can rebuild the atlas. */
17648
17722
  private iesAtlas = packIesAtlas([]);
17649
17723
  private cookieImages: readonly TexImageSource[] = [];
17724
+ /** The images the frame's base decals lay, in the atlas's band of them. See `setBaseDecalImages`. */
17725
+ private decalImages: readonly TexImageSource[] = [];
17726
+ /** The decals as last laid, packed again whenever the cookies move their band. */
17727
+ private baseDecalList: readonly BaseDecal[] = [];
17728
+ /** `uBaseDecals`, held until the next `setBaseDecals`, across passes. */
17729
+ private readonly baseDecalFloats = new Float32Array(BASE_DECAL_FLOATS);
17730
+ /** The refusals of a decal or its image already said, each once. */
17731
+ private readonly warnedBaseDecals = new Set<string>();
17650
17732
  /** The scene's DriftLight field, and its two volumes once it is whole. See `createLightField`. */
17651
17733
  private lightField: DriftLightVolumes | null = null;
17652
17734
  private driftIndex: GPUTexture | null = null;
@@ -18198,6 +18280,78 @@ export class WebGPURenderer implements RendererApi {
18198
18280
  this.cookieImages = images.slice();
18199
18281
  this.forgetFixtureAtlas();
18200
18282
  this.rebuildFlatBindGroup();
18283
+ /* The decals' band starts below the cookies, so their rows move with them. */
18284
+ if (this.baseDecalList.length > 0) this.setBaseDecals(this.baseDecalList);
18285
+ }
18286
+
18287
+ /**
18288
+ * The images the frame's base decals lay, each a `BASE_DECAL_TILE` square. The same count again
18289
+ * copies the squares into the atlas and nothing else; a different count builds it again. See
18290
+ * `renderer.ts` and `baseDecals.ts`. **Once a frame, before the draws they mark**: a copy reaches
18291
+ * every draw of the frame it is made in, as any queue write does.
18292
+ */
18293
+ setBaseDecalImages(images: readonly TexImageSource[]): void {
18294
+ if (this.surface.lost) return;
18295
+ if (images.length === this.decalImages.length && this.fixtureTexture !== null) {
18296
+ this.decalImages = images;
18297
+ this.copyDecalImages(this.fixtureTexture);
18298
+ return;
18299
+ }
18300
+ this.decalImages = images.slice();
18301
+ this.forgetFixtureAtlas();
18302
+ this.rebuildFlatBindGroup();
18303
+ this.setBaseDecals(this.baseDecalList);
18304
+ }
18305
+
18306
+ /** The decal images into their squares of the atlas. */
18307
+ private copyDecalImages(texture: GPUTexture): void {
18308
+ const top = decalBandTop(this.cookieImages.length);
18309
+ for (let tile = 0; tile < this.decalImages.length; tile++) {
18310
+ const source = this.decalImages[tile];
18311
+ if (source === undefined || !this.decalImageFits(source)) continue;
18312
+ this.surface.device.queue.copyExternalImageToTexture(
18313
+ { source: source as GPUCopyExternalImageSource },
18314
+ { texture, origin: { x: tile * BASE_DECAL_TILE, y: top } },
18315
+ { width: BASE_DECAL_TILE, height: BASE_DECAL_TILE },
18316
+ );
18317
+ }
18318
+ }
18319
+
18320
+ /** Whether an image is a decal's square, said once where it is not. */
18321
+ private decalImageFits(image: TexImageSource): boolean {
18322
+ const { width, height } = sourceSize(image);
18323
+ if (width === BASE_DECAL_TILE && height === BASE_DECAL_TILE) return true;
18324
+ const refusal =
18325
+ `a base decal image of ${width} by ${height}; it is a square of ${BASE_DECAL_TILE} ` +
18326
+ 'texels, and is left clear';
18327
+ if (!this.warnedBaseDecals.has(refusal)) {
18328
+ this.warnedBaseDecals.add(refusal);
18329
+ console.warn(`WebGPU: ${refusal}.`);
18330
+ }
18331
+ return false;
18332
+ }
18333
+
18334
+ /**
18335
+ * The decals laid into the base colours of the draws after it, until the next call, across
18336
+ * passes. In the material block, as the overlay is, and written again where each pass starts it.
18337
+ * See `renderer.ts` and `baseDecals.ts`.
18338
+ */
18339
+ setBaseDecals(decals: readonly BaseDecal[]): void {
18340
+ if (this.surface.lost) return;
18341
+ this.baseDecalList = decals;
18342
+ const refusal = packBaseDecals(
18343
+ decals,
18344
+ this.decalImages.length,
18345
+ decalBandTop(this.cookieImages.length),
18346
+ this.baseDecalFloats,
18347
+ );
18348
+ if (refusal !== null && !this.warnedBaseDecals.has(refusal)) {
18349
+ this.warnedBaseDecals.add(refusal);
18350
+ console.warn(`WebGPU: ${refusal}.`);
18351
+ }
18352
+ if ((this.baseDecalFloats[0] as number) > 0) this.enableLit('BASE_DECALS');
18353
+ this.perFrameFloats.set(this.baseDecalFloats, this.materialField('uBaseDecals'));
18354
+ this.materials.dirty();
18201
18355
  }
18202
18356
 
18203
18357
  /** Let the atlas go, to be built again from what is in hand the next time it is bound. */
@@ -18218,7 +18372,11 @@ export class WebGPURenderer implements RendererApi {
18218
18372
  private fixtureAtlasView(): GPUTextureView {
18219
18373
  if (this.fixtureView !== null) return this.fixtureView;
18220
18374
  const { device } = this.surface;
18221
- const layout = fixtureAtlasLayout(this.iesAtlas.height, this.cookieImages.length);
18375
+ const layout = fixtureAtlasLayout(
18376
+ this.iesAtlas.height,
18377
+ this.cookieImages.length,
18378
+ this.decalImages.length,
18379
+ );
18222
18380
  const texture = device.createTexture({
18223
18381
  label: 'fixture.atlas',
18224
18382
  size: [layout.width, layout.height],
@@ -18241,6 +18399,7 @@ export class WebGPURenderer implements RendererApi {
18241
18399
  { width: COOKIE_TILE, height: COOKIE_TILE },
18242
18400
  );
18243
18401
  }
18402
+ this.copyDecalImages(texture);
18244
18403
  this.fixtureTexture = texture;
18245
18404
  this.fixtureView = texture.createView();
18246
18405
  this.iesRows = this.iesAtlas.height;
@@ -19039,6 +19198,7 @@ export class WebGPURenderer implements RendererApi {
19039
19198
  ...entry(TAA_TEXTURES.uFlicker, record),
19040
19199
  ...entry(TAA_TEXTURES.uFlickerMotion, motion),
19041
19200
  ...entry(TAA_TEXTURES.uDepth, this.resolvedDepthView, true),
19201
+ ...entry(TAA_TEXTURES.uMotion, this.resolvedDepthView, true),
19042
19202
  ],
19043
19203
  });
19044
19204
  }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Decals laid into the base colour of what they land on, before it is lit: `setBaseDecals` and
3
+ * `setBaseDecalImages`, packed into `uBaseDecals` for the lit stage's `BASE_DECALS` switch.
4
+ *
5
+ * **What they are for, and why `DecalProjector` is not it.** A projected decal multiplies the
6
+ * finished pixel, so where the surface under it glows, a lettered floor or a lit panel, the mark
7
+ * dims the glow too. A shadow a body throws on a lit floor, a footprint, a stain is a change in what
8
+ * the surface *is*: it darkens the colour light falls on and leaves what the surface emits, how
9
+ * rough it is and its normal as they are. So each of these is a box, as a `DecalBox` is, an image
10
+ * read across its face, and the base colour moved toward the image's colour by its alpha.
11
+ *
12
+ * **The images ride in the fixture atlas**, a band of `BASE_DECAL_TILE` squares between the cookies
13
+ * and the photometric profiles (`fixtureAtlas.ts`), because the lit stage has no texture binding
14
+ * left (`textureUnitBudget.test.ts`). An image is that square, read as a picture: its colour is
15
+ * decoded from sRGB and its alpha is as it is. Rewriting them once a frame is the use they are for,
16
+ * and costs an upload of the squares; changing how many there are builds the atlas again.
17
+ *
18
+ * **What it gives up**: eight a frame, the uniforms they take being the limit (thirty-four vectors,
19
+ * so WebGL2 refuses them, in words, on a part whose lit stage has no room); the GPU-driven pipeline
20
+ * does not draw them; and a surface opts out by its material (`receivesDecals: false`), not by a
21
+ * mask of what each decal may mark.
22
+ */
23
+ import { worldToDecalMatrix, type DecalBox } from './decalProjector.ts';
24
+
25
+ /** Decals a frame. */
26
+ export const MAX_BASE_DECALS = 8;
27
+ /** Texels across an image's square in the atlas. */
28
+ export const BASE_DECAL_TILE = 128;
29
+ /** Floats in `uBaseDecals`: a header, then four vectors a decal. */
30
+ export const BASE_DECAL_FLOATS = (1 + MAX_BASE_DECALS * 4) * 4;
31
+
32
+ /** One decal: a box, which image it lays, and how much. */
33
+ export interface BaseDecal extends DecalBox {
34
+ /** Which of `setBaseDecalImages`' images it lays, from 0. */
35
+ readonly image: number;
36
+ /** How much of the image's alpha lands, 0 to 1. Default 1. */
37
+ readonly opacity?: number;
38
+ /**
39
+ * How far from facing back along `forward` a surface may be turned and still be marked, as a
40
+ * cosine of the geometric normal against it. Default 0.2, so a floor is marked and the side of a
41
+ * step it stands on is not smeared. -1 marks every surface in the box.
42
+ */
43
+ readonly facingCos?: number;
44
+ }
45
+
46
+ const MATRIX = new Float32Array(16);
47
+
48
+ /**
49
+ * Pack `decals` against `images` laid in the atlas band starting at row `bandTop` into `out`, and
50
+ * return why one was left out, or null where none was: past the eighth, an image there is none of,
51
+ * or a box with no size or no orientation. The rest are packed in order.
52
+ */
53
+ export function packBaseDecals(
54
+ decals: readonly BaseDecal[],
55
+ images: number,
56
+ bandTop: number,
57
+ out: Float32Array,
58
+ ): string | null {
59
+ out.fill(0);
60
+ let refusal: string | null =
61
+ decals.length > MAX_BASE_DECALS
62
+ ? `${decals.length} base decals; a frame lays the first ${MAX_BASE_DECALS}`
63
+ : null;
64
+ let kept = 0;
65
+ for (let i = 0; i < decals.length && kept < MAX_BASE_DECALS; i++) {
66
+ const decal = decals[i] as BaseDecal;
67
+ if (!(Number.isInteger(decal.image) && decal.image >= 0 && decal.image < images)) {
68
+ refusal ??= `a base decal lays image ${decal.image} and there are ${images}`;
69
+ continue;
70
+ }
71
+ try {
72
+ worldToDecalMatrix(MATRIX, decal);
73
+ } catch (cause) {
74
+ refusal ??= cause instanceof Error ? cause.message : String(cause);
75
+ continue;
76
+ }
77
+ const at = 4 + kept * 16;
78
+ for (let row = 0; row < 3; row++) {
79
+ out[at + row * 4] = MATRIX[row] as number;
80
+ out[at + row * 4 + 1] = MATRIX[4 + row] as number;
81
+ out[at + row * 4 + 2] = MATRIX[8 + row] as number;
82
+ out[at + row * 4 + 3] = MATRIX[12 + row] as number;
83
+ }
84
+ out[at + 12] = decal.image;
85
+ out[at + 13] = Math.min(1, Math.max(0, decal.opacity ?? 1));
86
+ out[at + 14] = decal.facingCos ?? 0.2;
87
+ kept += 1;
88
+ }
89
+ out[0] = kept;
90
+ out[1] = bandTop;
91
+ out[2] = BASE_DECAL_TILE;
92
+ return refusal;
93
+ }
@@ -0,0 +1,66 @@
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
+ /**
23
+ * Floats in `uEmissiveFlow`: the scroll on u and v, the mask's layer plus one (0, none) and one
24
+ * unused; then the scale and offset that take the material's coordinates back to the mesh's own.
25
+ */
26
+ export const EMISSIVE_FLOW_FLOATS = 8;
27
+
28
+ /** What a material says about its glow's flow, and the repeat and offset its coordinates carry. */
29
+ export interface EmissiveFlowMaterial {
30
+ readonly emissiveScroll?: readonly [number, number] | null;
31
+ readonly emissiveMaskLayer?: number | null;
32
+ readonly uScale?: number;
33
+ readonly vScale?: number;
34
+ readonly uOffset?: number;
35
+ readonly vOffset?: number;
36
+ }
37
+
38
+ /**
39
+ * Pack a material's flow into `out` and say whether it asks for one: a scroll that moves, or a mask
40
+ * that is a whole layer. A scroll that is not a number is still; a layer that is not a whole number
41
+ * at or above zero is no mask.
42
+ */
43
+ export function packEmissiveFlow(
44
+ material: EmissiveFlowMaterial | null,
45
+ out: Float32Array,
46
+ ): boolean {
47
+ const scroll = material?.emissiveScroll ?? null;
48
+ const u = scroll !== null && Number.isFinite(scroll[0]) ? scroll[0] : 0;
49
+ const v = scroll !== null && Number.isFinite(scroll[1]) ? scroll[1] : 0;
50
+ const layer = material?.emissiveMaskLayer ?? null;
51
+ const masked = layer !== null && Number.isInteger(layer) && layer >= 0;
52
+ out[0] = u;
53
+ out[1] = v;
54
+ /* Plus one, so an unwritten zero is no mask rather than a mask of layer 0. */
55
+ out[2] = masked ? layer + 1 : 0;
56
+ out[3] = 0;
57
+ /* The vertex stage laid `uv · scale + offset`; this undoes it. A scale of zero has no inverse,
58
+ and every texel of such a surface is the one coordinate, so the mesh's own is taken as 0. */
59
+ const uScale = material?.uScale ?? 1;
60
+ const vScale = material?.vScale ?? 1;
61
+ out[4] = uScale === 0 ? 0 : 1 / uScale;
62
+ out[5] = vScale === 0 ? 0 : 1 / vScale;
63
+ out[6] = uScale === 0 ? 0 : -(material?.uOffset ?? 0) / uScale;
64
+ out[7] = vScale === 0 ? 0 : -(material?.vOffset ?? 0) / vScale;
65
+ return u !== 0 || v !== 0 || masked;
66
+ }
@@ -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,24 +22,40 @@
19
22
  */
20
23
  import { IES_ATLAS_WIDTH } from './iesProfile.ts';
21
24
  import { COOKIE_TILE } from './lightBudget.ts';
25
+ import { BASE_DECAL_TILE } from './baseDecals.ts';
22
26
 
23
27
  export interface FixtureAtlasLayout {
24
28
  readonly width: number;
25
29
  readonly height: number;
26
30
  /** Rows of cookie tiles at the top: `COOKIE_TILE`, or 0 with none loaded. */
27
31
  readonly cookieRows: number;
28
- /** The first row of the profiles, below the cookies. */
32
+ /** The first row of the decals' images, below the cookies, and how many rows they take. */
33
+ readonly decalTop: number;
34
+ readonly decalRows: number;
35
+ /** The first row of the profiles, below the cookies and the decals. */
29
36
  readonly iesTop: number;
30
37
  }
31
38
 
32
- /** The atlas for `iesRows` rows of profile and `cookieTiles` cookies. */
33
- export function fixtureAtlasLayout(iesRows: number, cookieTiles: number): FixtureAtlasLayout {
39
+ /** The row the decals' band starts at: below the cookies, which are there or are not. */
40
+ export function decalBandTop(cookieTiles: number): number {
41
+ return cookieTiles > 0 ? COOKIE_TILE : 0;
42
+ }
43
+
44
+ /** The atlas for `iesRows` rows of profile, `cookieTiles` cookies and `decalTiles` decal images. */
45
+ export function fixtureAtlasLayout(
46
+ iesRows: number,
47
+ cookieTiles: number,
48
+ decalTiles = 0,
49
+ ): FixtureAtlasLayout {
34
50
  const cookieRows = cookieTiles > 0 ? COOKIE_TILE : 0;
51
+ const decalRows = decalTiles > 0 ? BASE_DECAL_TILE : 0;
35
52
  return {
36
- width: Math.max(IES_ATLAS_WIDTH, COOKIE_TILE * cookieTiles),
37
- height: cookieRows + iesRows,
53
+ width: Math.max(IES_ATLAS_WIDTH, COOKIE_TILE * cookieTiles, BASE_DECAL_TILE * decalTiles),
54
+ height: cookieRows + decalRows + iesRows,
38
55
  cookieRows,
39
- iesTop: cookieRows,
56
+ decalTop: cookieRows,
57
+ decalRows,
58
+ iesTop: cookieRows + decalRows,
40
59
  };
41
60
  }
42
61