@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
@@ -20,7 +20,16 @@ import { maskOf } from '../../frame/resources.ts';
20
20
  /** The two copies a registered pass may declare it reads. See `drawPass`. */
21
21
  const COLOR_SNAPSHOT = maskOf('colorSnapshot');
22
22
  const DEPTH_SNAPSHOT = maskOf('depthSnapshot');
23
- import type { Mover } from '../../recon/mover.ts';
23
+ import {
24
+ MOVER_MOTION,
25
+ MOVER_TWICE,
26
+ advanceMover,
27
+ claimRendererId,
28
+ isMover,
29
+ type Mover,
30
+ } from '../../recon/mover.ts';
31
+ import { frameOfChange, keepsPrevious } from '../../recon/changeFrames.ts';
32
+ import { GlMotionPass } from './motionTarget.ts';
24
33
  import { resolveGlass } from '../../glass.ts';
25
34
  import type { GlassOptions, ResolvedGlass } from '../../glass.ts';
26
35
  import type { ClipControlExtension } from '../../depthConvention.ts';
@@ -132,6 +141,20 @@ import {
132
141
  type SurfaceDetail,
133
142
  } from '../../surfaceDetail.ts';
134
143
  import { LOOK_FLOATS, asksLayerLooks, packLayerLooks } from '../../surfaceLayerLooks.ts';
144
+ import { EMISSIVE_FLOW_FLOATS, packEmissiveFlow } from '../../emissiveFlow.ts';
145
+ import { sourceSize } from '../../textureSource.ts';
146
+ import {
147
+ BASE_DECAL_FLOATS,
148
+ BASE_DECAL_TILE,
149
+ packBaseDecals,
150
+ type BaseDecal,
151
+ } from '../../baseDecals.ts';
152
+ import {
153
+ IRIDESCENT_FLOATS,
154
+ iridescentRefusal,
155
+ packIridescentOverlay,
156
+ type IridescentOverlay,
157
+ } from '../../iridescentOverlay.ts';
135
158
  import type { SurfaceProjection } from '../../surfaceProjection.ts';
136
159
  import { FILM_FRAG, FILM_VERT } from '../../shaders/film.ts';
137
160
  import { SceneTarget } from '../../sceneTarget.ts';
@@ -230,7 +253,7 @@ import { ProbeSweeps } from '../../probeSweeps.ts';
230
253
  import { octahedralEdgeFor } from '../../prefilterEnvMap.ts';
231
254
  import { EnvironmentPrefilterPass } from '../../prefilterPass.ts';
232
255
  import { packIesAtlas } from '../../iesProfile.ts';
233
- import { fixtureAtlasLayout, fixtureAtlasTexels } from '../../fixtureAtlas.ts';
256
+ import { decalBandTop, fixtureAtlasLayout, fixtureAtlasTexels } from '../../fixtureAtlas.ts';
234
257
  import { resolveSurfaceScene } from '../../surfaceScene.ts';
235
258
  import type { AreaLightBuffer, AreaLightSource } from '../../areaLights.ts';
236
259
  import { MAX_AREA_LIGHTS } from '../../areaLights.ts';
@@ -396,6 +419,7 @@ import { noLitSwitches, type LitSwitch } from '../../shaders/flat/glassTint.ts';
396
419
  import {
397
420
  poseBounds,
398
421
  validatePointSkin,
422
+ validatePointSkinRange,
399
423
  validatePose,
400
424
  type PointSkinData,
401
425
  } from '../../pointSkin.ts';
@@ -1745,6 +1769,17 @@ export class WebGL2Renderer implements RendererApi {
1745
1769
  private iesAtlas = packIesAtlas([]);
1746
1770
  /** The cookie images, kept so a new set of profiles can rebuild the atlas around them. */
1747
1771
  private cookieImages: readonly TexImageSource[] = [];
1772
+ /** The images the frame's base decals lay, in the atlas's band of them. See `setBaseDecalImages`. */
1773
+ private decalImages: readonly TexImageSource[] = [];
1774
+ /** The decals as last laid, packed again whenever the atlas moves their band. */
1775
+ private baseDecalList: readonly BaseDecal[] = [];
1776
+ /** `uBaseDecals`, held until the next `setBaseDecals`, across passes. */
1777
+ private readonly baseDecalFloats = new Float32Array(BASE_DECAL_FLOATS);
1778
+ /** Whether the lit stage has room for base decals, asked once. */
1779
+ private decalRoom: boolean | null = null;
1780
+ private warnedDecalRoom = false;
1781
+ /** The refusals of a decal or its image already said, each once. */
1782
+ private readonly warnedBaseDecals = new Set<string>();
1748
1783
  /** Rows of profile in the atlas. One until a consumer loads a profile, which is the row of ones. */
1749
1784
  private iesRows = 1;
1750
1785
  /** Cookie tiles in the atlas. 0 is none, and the whole off path. */
@@ -1827,6 +1862,18 @@ export class WebGL2Renderer implements RendererApi {
1827
1862
  private warnedLooksRoom = false;
1828
1863
  /** `uDetail`, the material's tiled detail packed by `packSurfaceDetail`. */
1829
1864
  private readonly detailFloats = new Float32Array(DETAIL_FLOATS);
1865
+ /** `uEmissiveFlow`, a material's glow moved and masked, packed by `packEmissiveFlow`. */
1866
+ private readonly flowFloats = new Float32Array(EMISSIVE_FLOW_FLOATS);
1867
+ /** Whether the lit stage has room for that, asked once. See `emissiveFlowFits`. */
1868
+ private flowRoom: boolean | null = null;
1869
+ private warnedFlowRoom = false;
1870
+ /** `uIridescence`, a material's iridescent overlay packed by `packIridescentOverlay`. */
1871
+ private readonly iridescentFloats = new Float32Array(IRIDESCENT_FLOATS);
1872
+ /** Whether the lit stage has room for it, asked once. See `iridescentOf`. */
1873
+ private iridescentRoom: boolean | null = null;
1874
+ private warnedIridescentRoom = false;
1875
+ /** The refusals of an overlay already said, each once. */
1876
+ private readonly warnedIridescent = new Set<string>();
1830
1877
  /** Whether the lit stage has room for detail on this part, asked once. See `detailOfMaterial`. */
1831
1878
  private detailRoom: boolean | null = null;
1832
1879
  private warnedDetailRoom = false;
@@ -1990,6 +2037,19 @@ export class WebGL2Renderer implements RendererApi {
1990
2037
  return this.sceneViewProj as unknown as ReadonlyMat4;
1991
2038
  }
1992
2039
  private shadowPassActive = false;
2040
+ /**
2041
+ * The motion WebGL2's temporal resolve follows: the frame's movers, recorded as they are drawn and
2042
+ * drawn into a target of their own at `endFrame`. See `motionTarget.ts`.
2043
+ */
2044
+ private readonly motionPass = new GlMotionPass();
2045
+ /** Which renderer a `Mover` was last drawn by, and the frame serial it reads. See `mover.ts`. */
2046
+ private readonly moverRenderer = claimRendererId();
2047
+ private moverFrame = 0;
2048
+ /** Between `beginFrame` and `endFrame`, which decides which frame a rewrite belongs to. */
2049
+ private inFrame = false;
2050
+ private readonly motionJittered = new Float32Array(16);
2051
+ private warnedMoverTwice = false;
2052
+ private warnedMotionUnkept = false;
1993
2053
  /** Non-zero while the drawing buffer is pinned; see `lockDrawingBuffer`. */
1994
2054
  private lockedWidth = 0;
1995
2055
  private lockedHeight = 0;
@@ -2711,6 +2771,8 @@ export class WebGL2Renderer implements RendererApi {
2711
2771
  setSpotCookies(images: readonly TexImageSource[]): void {
2712
2772
  this.cookieImages = images.slice();
2713
2773
  this.buildFixtureAtlas();
2774
+ /* The decals' band starts below the cookies, so their rows move with them. */
2775
+ if (this.baseDecalList.length > 0) this.setBaseDecals(this.baseDecalList);
2714
2776
  }
2715
2777
 
2716
2778
  setIesProfiles(profiles: readonly PhotometricProfile[]): void {
@@ -2718,6 +2780,102 @@ export class WebGL2Renderer implements RendererApi {
2718
2780
  this.buildFixtureAtlas();
2719
2781
  }
2720
2782
 
2783
+ /**
2784
+ * The images the frame's base decals lay, each a `BASE_DECAL_TILE` square, into the fixture
2785
+ * atlas's band of them. **Once a frame is what this is for**: the same count again uploads the
2786
+ * squares and nothing else, while a different count builds the atlas again. An image of another
2787
+ * size is refused, said once, and its square left clear. See `baseDecals.ts`.
2788
+ */
2789
+ setBaseDecalImages(images: readonly TexImageSource[]): void {
2790
+ if (this.contextLost) return;
2791
+ if (images.length === this.decalImages.length && this.fixtureTexture !== null) {
2792
+ this.decalImages = images;
2793
+ const { gl } = this;
2794
+ gl.activeTexture(gl.TEXTURE0 + FIXTURE_ATLAS_TEXTURE_UNIT);
2795
+ gl.bindTexture(gl.TEXTURE_2D, this.fixtureTexture);
2796
+ this.uploadDecalImages();
2797
+ gl.bindTexture(gl.TEXTURE_2D, null);
2798
+ gl.activeTexture(gl.TEXTURE0);
2799
+ return;
2800
+ }
2801
+ this.decalImages = images.slice();
2802
+ this.buildFixtureAtlas();
2803
+ this.setBaseDecals(this.baseDecalList);
2804
+ }
2805
+
2806
+ /** The decal images into their squares of the atlas bound on the fixture unit. */
2807
+ private uploadDecalImages(): void {
2808
+ const { gl } = this;
2809
+ for (let tile = 0; tile < this.decalImages.length; tile++) {
2810
+ const image = this.decalImages[tile];
2811
+ if (image === undefined || !this.decalImageFits(image)) continue;
2812
+ gl.texSubImage2D(
2813
+ gl.TEXTURE_2D,
2814
+ 0,
2815
+ tile * BASE_DECAL_TILE,
2816
+ decalBandTop(this.cookieImages.length),
2817
+ gl.RGBA,
2818
+ gl.HALF_FLOAT,
2819
+ image,
2820
+ );
2821
+ }
2822
+ }
2823
+
2824
+ /** Whether an image is a decal's square, said once where it is not. */
2825
+ private decalImageFits(image: TexImageSource): boolean {
2826
+ const { width, height } = sourceSize(image);
2827
+ if (width === BASE_DECAL_TILE && height === BASE_DECAL_TILE) return true;
2828
+ const refusal =
2829
+ `a base decal image of ${width} by ${height}; it is a square of ${BASE_DECAL_TILE} ` +
2830
+ 'texels, and is left clear';
2831
+ if (!this.warnedBaseDecals.has(refusal)) {
2832
+ this.warnedBaseDecals.add(refusal);
2833
+ console.warn(`Renderer: ${refusal}.`);
2834
+ }
2835
+ return false;
2836
+ }
2837
+
2838
+ /**
2839
+ * The decals laid into the base colours of the draws after it, until the next call, across
2840
+ * passes: each a box, an image of `setBaseDecalImages` and how much of it. **Refused, said once,
2841
+ * where the lit stage has no room** for their thirty-four vectors, and the draws are then marked
2842
+ * by none. See `baseDecals.ts`.
2843
+ */
2844
+ setBaseDecals(decals: readonly BaseDecal[]): void {
2845
+ if (this.contextLost) return;
2846
+ this.baseDecalList = decals;
2847
+ if (decals.length > 0 && !this.baseDecalsFit()) {
2848
+ this.baseDecalFloats.fill(0);
2849
+ } else {
2850
+ const refusal = packBaseDecals(
2851
+ decals,
2852
+ this.decalImages.length,
2853
+ decalBandTop(this.cookieImages.length),
2854
+ this.baseDecalFloats,
2855
+ );
2856
+ if (refusal !== null && !this.warnedBaseDecals.has(refusal)) {
2857
+ this.warnedBaseDecals.add(refusal);
2858
+ console.warn(`Renderer: ${refusal}.`);
2859
+ }
2860
+ if ((this.baseDecalFloats[0] as number) > 0) this.askLit('BASE_DECALS');
2861
+ }
2862
+ this.materials.dirty();
2863
+ this.writeFlatVector('uBaseDecals', this.baseDecalFloats);
2864
+ }
2865
+
2866
+ private baseDecalsFit(): boolean {
2867
+ this.decalRoom ??= this.switchFits('BASE_DECALS');
2868
+ if (!this.decalRoom && !this.warnedDecalRoom) {
2869
+ this.warnedDecalRoom = true;
2870
+ console.warn(
2871
+ 'Renderer: base decals are refused on this device. They take thirty-four fragment uniform ' +
2872
+ 'vectors the lit shader does not have, so nothing is marked. A lower `maxLights` makes ' +
2873
+ 'the room, or WebGPU has it.',
2874
+ );
2875
+ }
2876
+ return this.decalRoom;
2877
+ }
2878
+
2721
2879
  /**
2722
2880
  * The fixture atlas from the profiles and cookies in hand: the profiles' rows at the bottom, the
2723
2881
  * cookies' tiles on top, one `RGBA16F` texture. See `fixtureAtlas.ts` for the layout.
@@ -2729,7 +2887,11 @@ export class WebGL2Renderer implements RendererApi {
2729
2887
  */
2730
2888
  private buildFixtureAtlas(): void {
2731
2889
  const { gl } = this;
2732
- const layout = fixtureAtlasLayout(this.iesAtlas.height, this.cookieImages.length);
2890
+ const layout = fixtureAtlasLayout(
2891
+ this.iesAtlas.height,
2892
+ this.cookieImages.length,
2893
+ this.decalImages.length,
2894
+ );
2733
2895
  if (this.fixtureTexture === null) this.fixtureTexture = gl.createTexture();
2734
2896
  gl.activeTexture(gl.TEXTURE0 + FIXTURE_ATLAS_TEXTURE_UNIT);
2735
2897
  gl.bindTexture(gl.TEXTURE_2D, this.fixtureTexture);
@@ -2749,6 +2911,7 @@ export class WebGL2Renderer implements RendererApi {
2749
2911
  if (image === undefined) continue;
2750
2912
  gl.texSubImage2D(gl.TEXTURE_2D, 0, tile * COOKIE_TILE, 0, gl.RGBA, gl.HALF_FLOAT, image);
2751
2913
  }
2914
+ this.uploadDecalImages();
2752
2915
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
2753
2916
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
2754
2917
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
@@ -3547,6 +3710,7 @@ export class WebGL2Renderer implements RendererApi {
3547
3710
 
3548
3711
  const { gl } = this;
3549
3712
  if (!this.contextLost) this.skinScatter?.dispose();
3713
+ if (!this.contextLost) this.motionPass.dispose(gl);
3550
3714
  this.detachContextLoss();
3551
3715
  this.contextLostListeners.length = 0;
3552
3716
  this.contextRestoredListeners.length = 0;
@@ -3738,6 +3902,7 @@ export class WebGL2Renderer implements RendererApi {
3738
3902
  * buffer is the same buffer. Rebinding here would cost a state change for nothing.
3739
3903
  */
3740
3904
  updateMesh(mesh: Mesh, positions: Float32Array, normals?: Float32Array): void {
3905
+ if (!this.contextLost) this.keepPositionsBefore(mesh);
3741
3906
  mesh.update(this.gl, positions, normals);
3742
3907
  }
3743
3908
 
@@ -3769,9 +3934,20 @@ export class WebGL2Renderer implements RendererApi {
3769
3934
  validatePose(matrices, skin.reach);
3770
3935
  if (this.contextLost || this.pointSkinPose === null) return;
3771
3936
  poseBounds(matrices, skin.reach, skin.mesh.bounds);
3937
+ this.keepPositionsBefore(skin.mesh);
3772
3938
  skin.pose(this.gl, this.pointSkinPose, matrices);
3773
3939
  }
3774
3940
 
3941
+ /**
3942
+ * Rewrite part of a skin in place. A command in the stream like a pose, so the pose after it reads
3943
+ * the rewrite. See `GlPointSkin.rewrite`.
3944
+ */
3945
+ updatePointSkin(skin: GlPointSkin, first: number, data: PointSkinData): void {
3946
+ validatePointSkinRange(data, first, skin.mesh.vertexCount);
3947
+ if (this.contextLost) return;
3948
+ skin.rewrite(this.gl, first, data);
3949
+ }
3950
+
3775
3951
  disposePointSkin(skin: GlPointSkin): void {
3776
3952
  skin.dispose(this.gl);
3777
3953
  }
@@ -4473,6 +4649,20 @@ export class WebGL2Renderer implements RendererApi {
4473
4649
  emissiveScale?.[1] ?? 1,
4474
4650
  emissiveScale?.[2] ?? 1,
4475
4651
  );
4652
+ /* And its glow's scroll and mask, where the lit stage has room for them: emissiveFlow.ts. */
4653
+ if (packEmissiveFlow(material, this.flowFloats) && this.emissiveFlowFits()) {
4654
+ this.askLit('EMISSIVE_FLOW');
4655
+ } else {
4656
+ this.flowFloats.fill(0, 0, 4);
4657
+ }
4658
+ gl.uniform4fv(u['uEmissiveFlow'] ?? null, this.flowFloats);
4659
+ /* And its iridescent overlay, where it can be drawn: iridescentOverlay.ts. */
4660
+ const iridescent = this.iridescentOf(material);
4661
+ packIridescentOverlay(iridescent, material ?? {}, this.iridescentFloats);
4662
+ if (iridescent !== null) this.askLit('IRIDESCENT_OVERLAY');
4663
+ gl.uniform4fv(u['uIridescence'] ?? null, this.iridescentFloats);
4664
+ /* And whether the frame's decals mark it. See `SurfaceMaterial.receivesDecals`. */
4665
+ gl.uniform4f(u['uDecalReceiver'] ?? null, material?.receivesDecals === false ? 1 : 0, 0, 0, 0);
4476
4666
 
4477
4667
  /*
4478
4668
  * **The scale belongs to the material, not to the albedo.** It was uploaded on the albedo
@@ -4716,6 +4906,53 @@ export class WebGL2Renderer implements RendererApi {
4716
4906
  return this.overlayRoom;
4717
4907
  }
4718
4908
 
4909
+ /**
4910
+ * A material's iridescent overlay, or null where it is refused: malformed, or the lit stage has no
4911
+ * room for its thirty-six vectors. Each said once.
4912
+ */
4913
+ private iridescentOf(material: SurfaceMaterial | null): IridescentOverlay | null {
4914
+ const overlay = material?.iridescentOverlay ?? null;
4915
+ if (overlay === null) return null;
4916
+ const refusal = iridescentRefusal(overlay);
4917
+ if (refusal !== null) {
4918
+ if (!this.warnedIridescent.has(refusal)) {
4919
+ this.warnedIridescent.add(refusal);
4920
+ console.warn(`Renderer: an iridescent overlay is refused: ${refusal}. Drawn without it.`);
4921
+ }
4922
+ return null;
4923
+ }
4924
+ this.iridescentRoom ??= this.switchFits('IRIDESCENT_OVERLAY');
4925
+ if (this.iridescentRoom) return overlay;
4926
+ if (!this.warnedIridescentRoom) {
4927
+ this.warnedIridescentRoom = true;
4928
+ console.warn(
4929
+ "Renderer: a material's iridescent overlay is refused on this device. It takes thirty-six " +
4930
+ 'fragment uniform vectors the lit shader does not have, so the material is drawn without ' +
4931
+ 'it. A lower `maxLights` makes the room, or WebGPU has it.',
4932
+ );
4933
+ }
4934
+ return null;
4935
+ }
4936
+
4937
+ /**
4938
+ * Whether a material's glow can be moved and masked: the lit stage has room for its two vectors.
4939
+ * **Refused, said once, where it has not**, and the glow is then drawn still and unmasked, as it
4940
+ * was before it existed.
4941
+ */
4942
+ private emissiveFlowFits(): boolean {
4943
+ this.flowRoom ??= this.switchFits('EMISSIVE_FLOW');
4944
+ if (!this.flowRoom && !this.warnedFlowRoom) {
4945
+ this.warnedFlowRoom = true;
4946
+ console.warn(
4947
+ "Renderer: a material's emissive scroll and mask are refused on this device. They take " +
4948
+ 'two fragment uniform vectors and the lit shader already uses all ' +
4949
+ `${this.fragmentVectorLimit()} this GPU offers; the glow is drawn still and unmasked. ` +
4950
+ 'A lower `maxLights` makes the room, or WebGPU has it.',
4951
+ );
4952
+ }
4953
+ return this.flowRoom;
4954
+ }
4955
+
4719
4956
  /**
4720
4957
  * Whether a material's projection can be drawn: none asked, or the lit stage has the room for its
4721
4958
  * one vector. **Refused, said once, where it has not**, as the overlay is, and the material's
@@ -7215,6 +7452,9 @@ export class WebGL2Renderer implements RendererApi {
7215
7452
  layered: asIf.LAYERED ?? this.litOn.LAYERED,
7216
7453
  layerLooks: asIf.LAYER_LOOKS ?? this.litOn.LAYER_LOOKS,
7217
7454
  surfaceDetail: asIf.SURFACE_DETAIL ?? this.litOn.SURFACE_DETAIL,
7455
+ emissiveFlow: asIf.EMISSIVE_FLOW ?? this.litOn.EMISSIVE_FLOW,
7456
+ iridescentOverlay: asIf.IRIDESCENT_OVERLAY ?? this.litOn.IRIDESCENT_OVERLAY,
7457
+ baseDecals: asIf.BASE_DECALS ?? this.litOn.BASE_DECALS,
7218
7458
  maxLights: budget.maxLights,
7219
7459
  maxAreaLights: budget.maxAreaLights,
7220
7460
  });
@@ -8637,6 +8877,10 @@ export class WebGL2Renderer implements RendererApi {
8637
8877
  if (this.contextLost) return;
8638
8878
  /* One frame of the jitter's sequence however many times the frame binds its camera. */
8639
8879
  this.temporalHistory.nextFrame();
8880
+ /* A frame states its movers afresh. See `motionTarget.ts`. */
8881
+ this.moverFrame += 1;
8882
+ this.inFrame = true;
8883
+ this.motionPass.reset();
8640
8884
  this.skinScattered = false;
8641
8885
  this.skinScatter?.reset();
8642
8886
  if (this.litAsked) {
@@ -8727,6 +8971,8 @@ export class WebGL2Renderer implements RendererApi {
8727
8971
  if (this.viewModelActive) this.endViewModel();
8728
8972
  /* Skin not yet spread, because nothing blended came after it, is spread now. */
8729
8973
  this.scatterSkin();
8974
+ /* A draw after the frame, an overlay over the presented picture, is no mover of it. */
8975
+ this.inFrame = false;
8730
8976
 
8731
8977
  /*
8732
8978
  * Camera motion blur, built from the camera the frame was *rendered* with.
@@ -8748,6 +8994,7 @@ export class WebGL2Renderer implements RendererApi {
8748
8994
  blend: number;
8749
8995
  periodStart: boolean;
8750
8996
  depthToViewZ: Float32Array;
8997
+ motion: WebGLTexture | null;
8751
8998
  }
8752
8999
  | undefined;
8753
9000
  /*
@@ -8757,6 +9004,11 @@ export class WebGL2Renderer implements RendererApi {
8757
9004
  * reproject through a `previousViewProj` that had never been written.
8758
9005
  */
8759
9006
  const wantsReprojection = blur > 0 || this.temporalJittering;
9007
+ /* The movers' own motion, drawn while last frame's view is still the one kept: motionTarget.ts. */
9008
+ const moved =
9009
+ this.temporalJittering && view !== null && this.hasPreviousView
9010
+ ? this.drawMotionPass(view)
9011
+ : null;
8760
9012
  if (wantsReprojection && view !== null) {
8761
9013
  if (this.hasPreviousView) {
8762
9014
  mat4.invert(this.reprojection, view);
@@ -8791,6 +9043,7 @@ export class WebGL2Renderer implements RendererApi {
8791
9043
  blend: this.temporalHistoryUsable ? TEMPORAL_HISTORY_BLEND : 0,
8792
9044
  periodStart: this.temporalHistory.periodStart,
8793
9045
  depthToViewZ: this.taaDepthToViewZ,
9046
+ motion: moved,
8794
9047
  };
8795
9048
  }
8796
9049
 
@@ -9392,6 +9645,10 @@ export class WebGL2Renderer implements RendererApi {
9392
9645
  this.layerFloats.fill(0);
9393
9646
  this.lookFloats.fill(0);
9394
9647
  this.detailFloats.fill(0);
9648
+ /* Nor a glow that moves or is masked. */
9649
+ this.flowFloats.fill(0);
9650
+ /* Nor an iridescent overlay. */
9651
+ this.iridescentFloats.fill(0);
9395
9652
  this.overlayMaps = null;
9396
9653
  this.overlayTexture = null;
9397
9654
  /* Nor an ambient of its own: the frame's, until a draw asks. See `setAmbientSH`. */
@@ -9570,6 +9827,11 @@ export class WebGL2Renderer implements RendererApi {
9570
9827
  gl.uniform4fv(u['uLayers'] ?? null, this.layerFloats);
9571
9828
  gl.uniform4fv(u['uLayerLooks'] ?? null, this.lookFloats);
9572
9829
  gl.uniform4fv(u['uDetail'] ?? null, this.detailFloats);
9830
+ gl.uniform4fv(u['uEmissiveFlow'] ?? null, this.flowFloats);
9831
+ gl.uniform4fv(u['uIridescence'] ?? null, this.iridescentFloats);
9832
+ /* The frame's decals as last laid, held across passes, and every surface receiving them. */
9833
+ gl.uniform4fv(u['uBaseDecals'] ?? null, this.baseDecalFloats);
9834
+ gl.uniform4f(u['uDecalReceiver'] ?? null, 0, 0, 0, 0);
9573
9835
  /* Neither refracting nor glass, which is every draw that does not say otherwise. See
9574
9836
  `bindSeeThrough` for the four numbers in the one vector. */
9575
9837
  gl.uniform4f(u['uSeeThrough'] ?? null, 0, 0, 0, 0);
@@ -9991,12 +10253,10 @@ export class WebGL2Renderer implements RendererApi {
9991
10253
  */
9992
10254
  tint: Vec3 | null = null,
9993
10255
  /**
9994
- * Where the mesh was last frame — a `Mover` or a matrix — which this backend takes and ignores.
9995
- *
9996
- * Accepted rather than absent so that one scene draws through both renderers unchanged — the
9997
- * parity rule this repository is built on. Reconstruction is WebGPU's, having no compute stage
9998
- * here, so there is no motion target for this to be written into, and a mover passed here is
9999
- * neither read nor advanced.
10256
+ * Where the mesh was last frame, a `Mover` or a matrix, which the temporal resolve follows under
10257
+ * `temporalAa`: a rigid, skinned or rewritten mesh is drawn again into the motion target at the
10258
+ * end of the frame, so its pixels reproject by its own motion and not the camera's alone. See
10259
+ * `motionTarget.ts`; a mover is advanced only where the resolve reads it.
10000
10260
  */
10001
10261
  previousModel: ReadonlyMat4 | Mover | null = null,
10002
10262
  ): void {
@@ -10035,6 +10295,7 @@ export class WebGL2Renderer implements RendererApi {
10035
10295
  if (this.quality.cullDraws && this.occluded(mesh.bounds, model)) return;
10036
10296
  this.drawBudget.ask();
10037
10297
  this.takeMaterial();
10298
+ if (this.followsMotion()) this.recordDrawMotion(mesh, model, previousModel);
10038
10299
  /*
10039
10300
  * Skin under the screen-space blur draws its halves: the frame's, then at once its diffuse's
10040
10301
  * light and its colour, each into the blur's own target.
@@ -10056,6 +10317,147 @@ export class WebGL2Renderer implements RendererApi {
10056
10317
  }
10057
10318
  }
10058
10319
 
10320
+ /**
10321
+ * The frame's movers drawn into the motion target against its depth, rasterised through the
10322
+ * jittered matrix every geometry upload took (`viewProjFor`) and moved between the unjittered
10323
+ * ones; null where nothing moved or the frame is multisampled, which keeps the camera's motion
10324
+ * and says so once.
10325
+ */
10326
+ private drawMotionPass(view: ReadonlyMat4): WebGLTexture | null {
10327
+ const target = this.sceneTarget;
10328
+ if (target === null || this.motionPass.recorded === 0) return null;
10329
+ if (target.sampleCount > 1) {
10330
+ if (!this.warnedMotionUnkept) {
10331
+ this.warnedMotionUnkept = true;
10332
+ console.warn(
10333
+ "Renderer: WebGL2's temporal resolve follows the camera's motion alone in a multisampled " +
10334
+ "frame; a mover's own motion is drawn against single-sampled depth.",
10335
+ );
10336
+ }
10337
+ return null;
10338
+ }
10339
+ const depth = target.depthAttachment();
10340
+ if (depth === null) return null;
10341
+ jitterProjection(
10342
+ this.motionJittered,
10343
+ this.sceneMatrix(view),
10344
+ this.temporalJitterX,
10345
+ this.temporalJitterY,
10346
+ Math.max(1, this.gl.drawingBufferWidth),
10347
+ Math.max(1, this.gl.drawingBufferHeight),
10348
+ );
10349
+ return this.motionPass.draw(
10350
+ this.gl,
10351
+ depth,
10352
+ target.frameWidth,
10353
+ target.frameHeight,
10354
+ this.reversedDepth,
10355
+ this.motionJittered,
10356
+ view as Float32Array,
10357
+ this.previousViewProj as Float32Array,
10358
+ );
10359
+ }
10360
+
10361
+ /**
10362
+ * Whether a draw now is one the temporal resolve follows: the profile resolves temporally, this is
10363
+ * the frame's own picture between `beginFrame` and `endFrame`, and not a mirror's, a probe's, a
10364
+ * capture's, a shadow's or the view model's. See `motionTarget.ts`.
10365
+ */
10366
+ private followsMotion(): boolean {
10367
+ return (
10368
+ this.inFrame &&
10369
+ this.quality.temporalAa &&
10370
+ this.sceneTarget !== null &&
10371
+ !this.reflectionPassActive &&
10372
+ !this.probePassActive &&
10373
+ !this.capturePassActive &&
10374
+ !this.shadowPassActive &&
10375
+ !this.viewModelActive
10376
+ );
10377
+ }
10378
+
10379
+ /**
10380
+ * A draw's motion for the resolve, decided as WebGPU's `recordDrawMotion` decides it: a mesh
10381
+ * rewritten this frame by its last positions, with or without a mover; a skinned mover by its last
10382
+ * pose; a rigid one by its last model. A cloth draw keeps the camera's motion, said once.
10383
+ */
10384
+ private recordDrawMotion(
10385
+ mesh: Mesh,
10386
+ model: ReadonlyMat4,
10387
+ previous: ReadonlyMat4 | Mover | null,
10388
+ ): void {
10389
+ const rewritten = mesh.keptPrevious && mesh.changedFrame === this.moverFrame;
10390
+ if (this.cloth !== null && this.skinPaletteSet && mesh.isSkinned) {
10391
+ if (previous !== null && !this.warnedMotionUnkept) {
10392
+ this.warnedMotionUnkept = true;
10393
+ console.warn(
10394
+ "Renderer: a cloth draw's own motion is not followed by WebGL2's temporal resolve; it " +
10395
+ "takes the camera's. WebGPU's reconstruction follows it.",
10396
+ );
10397
+ }
10398
+ return;
10399
+ }
10400
+ if (previous === null) {
10401
+ if (rewritten) this.motionPass.recordDynamic(mesh, model, model);
10402
+ return;
10403
+ }
10404
+ if (!isMover(previous)) {
10405
+ if (rewritten) this.motionPass.recordDynamic(mesh, model, previous);
10406
+ else this.motionPass.recordRigid(mesh, model, previous);
10407
+ return;
10408
+ }
10409
+ const skinned = !rewritten && this.skinPaletteSet && mesh.isSkinned;
10410
+ const verdict = advanceMover(
10411
+ previous,
10412
+ this.moverRenderer,
10413
+ this.moverFrame,
10414
+ model,
10415
+ skinned ? this.paletteData : null,
10416
+ );
10417
+ if (verdict === MOVER_TWICE) {
10418
+ if (!this.warnedMoverTwice) {
10419
+ this.warnedMoverTwice = true;
10420
+ console.warn(
10421
+ 'Renderer: a Mover was drawn twice in one frame. A mover is one object; the second draw ' +
10422
+ 'records no motion. Give each moving object a mover of its own.',
10423
+ );
10424
+ }
10425
+ return;
10426
+ }
10427
+ if (rewritten) {
10428
+ this.motionPass.recordDynamic(
10429
+ mesh,
10430
+ model,
10431
+ verdict === MOVER_MOTION ? previous.previousModel : model,
10432
+ );
10433
+ return;
10434
+ }
10435
+ if (verdict !== MOVER_MOTION) return;
10436
+ if (skinned) {
10437
+ this.motionPass.recordSkinned(
10438
+ mesh,
10439
+ model,
10440
+ previous.previousModel,
10441
+ /* Each exactly its rig's length, as `advanceMover` keeps them, so neither is sliced. */
10442
+ previous.palette,
10443
+ previous.previousPalette,
10444
+ );
10445
+ } else {
10446
+ this.motionPass.recordRigid(mesh, model, previous.previousModel);
10447
+ }
10448
+ }
10449
+
10450
+ /**
10451
+ * Keep a dynamic mesh's positions before its frame's first rewrite, where the temporal resolve
10452
+ * follows motion: a rewrite before `beginFrame` belongs to the next frame (`frameOfChange`).
10453
+ */
10454
+ private keepPositionsBefore(mesh: Mesh): void {
10455
+ if (!(this.quality.temporalAa && this.sceneTarget !== null)) return;
10456
+ const frame = frameOfChange(this.moverFrame, this.inFrame);
10457
+ if (keepsPrevious(mesh.changedFrame, frame)) mesh.keepPrevious(this.gl);
10458
+ mesh.changedFrame = frame;
10459
+ }
10460
+
10059
10461
  /**
10060
10462
  * Whether a skin draw now draws its two halves: the profile asked, the frame has not spread its
10061
10463
  * skin yet, this is the frame's own picture — not a mirror's or a probe's, nor an overlay after