@driftengine/core 4.13.5 → 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 +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
@@ -8,6 +8,9 @@ import { maskOf } from '../../frame/resources.js';
8
8
  /** The two copies a registered pass may declare it reads. See `drawPass`. */
9
9
  const COLOR_SNAPSHOT = maskOf('colorSnapshot');
10
10
  const DEPTH_SNAPSHOT = maskOf('depthSnapshot');
11
+ import { MOVER_MOTION, MOVER_TWICE, advanceMover, claimRendererId, isMover, } from '../../recon/mover.js';
12
+ import { frameOfChange, keepsPrevious } from '../../recon/changeFrames.js';
13
+ import { GlMotionPass } from './motionTarget.js';
11
14
  import { resolveGlass } from '../../glass.js';
12
15
  import { PickableSet } from '../../pickable.js';
13
16
  import { TEMPORAL_HISTORY_BLEND, TemporalHistory, jitterOffset, jitterProjection, } from '../../temporalAa.js';
@@ -44,6 +47,10 @@ import { orthographicDepthSpan } from '../../lightMatrix.js';
44
47
  import { LAYER_FLOATS, layerMaskMap, layeredProjection, layersFitBeside, packSurfaceLayers, } from '../../surfaceLayers.js';
45
48
  import { DETAIL_FLOATS, detailOf, detailRefusal, detailRefusalWarning, packSurfaceDetail, } from '../../surfaceDetail.js';
46
49
  import { LOOK_FLOATS, asksLayerLooks, packLayerLooks } from '../../surfaceLayerLooks.js';
50
+ import { EMISSIVE_FLOW_FLOATS, packEmissiveFlow } from '../../emissiveFlow.js';
51
+ import { sourceSize } from '../../textureSource.js';
52
+ import { BASE_DECAL_FLOATS, BASE_DECAL_TILE, packBaseDecals, } from '../../baseDecals.js';
53
+ import { IRIDESCENT_FLOATS, iridescentRefusal, packIridescentOverlay, } from '../../iridescentOverlay.js';
47
54
  import { FILM_FRAG, FILM_VERT } from '../../shaders/film.js';
48
55
  import { SceneTarget } from '../../sceneTarget.js';
49
56
  import { FILM_LOOK_WITHOUT_COMPOSITE } from '../../shaders/filmLook.js';
@@ -79,7 +86,7 @@ import { ProbeSweeps } from '../../probeSweeps.js';
79
86
  import { octahedralEdgeFor } from '../../prefilterEnvMap.js';
80
87
  import { EnvironmentPrefilterPass } from '../../prefilterPass.js';
81
88
  import { packIesAtlas } from '../../iesProfile.js';
82
- import { fixtureAtlasLayout, fixtureAtlasTexels } from '../../fixtureAtlas.js';
89
+ import { decalBandTop, fixtureAtlasLayout, fixtureAtlasTexels } from '../../fixtureAtlas.js';
83
90
  import { resolveSurfaceScene } from '../../surfaceScene.js';
84
91
  import { MAX_AREA_LIGHTS } from '../../areaLights.js';
85
92
  import { areaShadowLayerCount, castingRange, createResolvedAreaShadows, firstAreaShadowLayer, AreaShadowSet, } from '../../areaShadowSet.js';
@@ -136,7 +143,7 @@ import { CUTOUT_RESOLVE_CODE, resolveCutout } from '../../cutoutDither.js';
136
143
  import { materialHasMaps, noteMapsWithoutUvs } from '../../mapsWithoutUvs.js';
137
144
  import { VIEW_MODEL_DEPTH_SHARE, viewModelDepthRange } from '../../viewModel.js';
138
145
  import { noLitSwitches } from '../../shaders/flat/glassTint.js';
139
- import { poseBounds, validatePointSkin, validatePose, } from '../../pointSkin.js';
146
+ import { poseBounds, validatePointSkin, validatePointSkinRange, validatePose, } from '../../pointSkin.js';
140
147
  import { GlPointSkin, createPointSkinPose } from './pointSkinFeedback.js';
141
148
  /**
142
149
  * How many frames `firstFrameSettled` will ask before it stops asking.
@@ -900,6 +907,17 @@ export class WebGL2Renderer {
900
907
  iesAtlas = packIesAtlas([]);
901
908
  /** The cookie images, kept so a new set of profiles can rebuild the atlas around them. */
902
909
  cookieImages = [];
910
+ /** The images the frame's base decals lay, in the atlas's band of them. See `setBaseDecalImages`. */
911
+ decalImages = [];
912
+ /** The decals as last laid, packed again whenever the atlas moves their band. */
913
+ baseDecalList = [];
914
+ /** `uBaseDecals`, held until the next `setBaseDecals`, across passes. */
915
+ baseDecalFloats = new Float32Array(BASE_DECAL_FLOATS);
916
+ /** Whether the lit stage has room for base decals, asked once. */
917
+ decalRoom = null;
918
+ warnedDecalRoom = false;
919
+ /** The refusals of a decal or its image already said, each once. */
920
+ warnedBaseDecals = new Set();
903
921
  /** Rows of profile in the atlas. One until a consumer loads a profile, which is the row of ones. */
904
922
  iesRows = 1;
905
923
  /** Cookie tiles in the atlas. 0 is none, and the whole off path. */
@@ -982,6 +1000,18 @@ export class WebGL2Renderer {
982
1000
  warnedLooksRoom = false;
983
1001
  /** `uDetail`, the material's tiled detail packed by `packSurfaceDetail`. */
984
1002
  detailFloats = new Float32Array(DETAIL_FLOATS);
1003
+ /** `uEmissiveFlow`, a material's glow moved and masked, packed by `packEmissiveFlow`. */
1004
+ flowFloats = new Float32Array(EMISSIVE_FLOW_FLOATS);
1005
+ /** Whether the lit stage has room for that, asked once. See `emissiveFlowFits`. */
1006
+ flowRoom = null;
1007
+ warnedFlowRoom = false;
1008
+ /** `uIridescence`, a material's iridescent overlay packed by `packIridescentOverlay`. */
1009
+ iridescentFloats = new Float32Array(IRIDESCENT_FLOATS);
1010
+ /** Whether the lit stage has room for it, asked once. See `iridescentOf`. */
1011
+ iridescentRoom = null;
1012
+ warnedIridescentRoom = false;
1013
+ /** The refusals of an overlay already said, each once. */
1014
+ warnedIridescent = new Set();
985
1015
  /** Whether the lit stage has room for detail on this part, asked once. See `detailOfMaterial`. */
986
1016
  detailRoom = null;
987
1017
  warnedDetailRoom = false;
@@ -1125,6 +1155,19 @@ export class WebGL2Renderer {
1125
1155
  return this.sceneViewProj;
1126
1156
  }
1127
1157
  shadowPassActive = false;
1158
+ /**
1159
+ * The motion WebGL2's temporal resolve follows: the frame's movers, recorded as they are drawn and
1160
+ * drawn into a target of their own at `endFrame`. See `motionTarget.ts`.
1161
+ */
1162
+ motionPass = new GlMotionPass();
1163
+ /** Which renderer a `Mover` was last drawn by, and the frame serial it reads. See `mover.ts`. */
1164
+ moverRenderer = claimRendererId();
1165
+ moverFrame = 0;
1166
+ /** Between `beginFrame` and `endFrame`, which decides which frame a rewrite belongs to. */
1167
+ inFrame = false;
1168
+ motionJittered = new Float32Array(16);
1169
+ warnedMoverTwice = false;
1170
+ warnedMotionUnkept = false;
1128
1171
  /** Non-zero while the drawing buffer is pinned; see `lockDrawingBuffer`. */
1129
1172
  lockedWidth = 0;
1130
1173
  lockedHeight = 0;
@@ -1810,11 +1853,95 @@ export class WebGL2Renderer {
1810
1853
  setSpotCookies(images) {
1811
1854
  this.cookieImages = images.slice();
1812
1855
  this.buildFixtureAtlas();
1856
+ /* The decals' band starts below the cookies, so their rows move with them. */
1857
+ if (this.baseDecalList.length > 0)
1858
+ this.setBaseDecals(this.baseDecalList);
1813
1859
  }
1814
1860
  setIesProfiles(profiles) {
1815
1861
  this.iesAtlas = packIesAtlas(profiles);
1816
1862
  this.buildFixtureAtlas();
1817
1863
  }
1864
+ /**
1865
+ * The images the frame's base decals lay, each a `BASE_DECAL_TILE` square, into the fixture
1866
+ * atlas's band of them. **Once a frame is what this is for**: the same count again uploads the
1867
+ * squares and nothing else, while a different count builds the atlas again. An image of another
1868
+ * size is refused, said once, and its square left clear. See `baseDecals.ts`.
1869
+ */
1870
+ setBaseDecalImages(images) {
1871
+ if (this.contextLost)
1872
+ return;
1873
+ if (images.length === this.decalImages.length && this.fixtureTexture !== null) {
1874
+ this.decalImages = images;
1875
+ const { gl } = this;
1876
+ gl.activeTexture(gl.TEXTURE0 + FIXTURE_ATLAS_TEXTURE_UNIT);
1877
+ gl.bindTexture(gl.TEXTURE_2D, this.fixtureTexture);
1878
+ this.uploadDecalImages();
1879
+ gl.bindTexture(gl.TEXTURE_2D, null);
1880
+ gl.activeTexture(gl.TEXTURE0);
1881
+ return;
1882
+ }
1883
+ this.decalImages = images.slice();
1884
+ this.buildFixtureAtlas();
1885
+ this.setBaseDecals(this.baseDecalList);
1886
+ }
1887
+ /** The decal images into their squares of the atlas bound on the fixture unit. */
1888
+ uploadDecalImages() {
1889
+ const { gl } = this;
1890
+ for (let tile = 0; tile < this.decalImages.length; tile++) {
1891
+ const image = this.decalImages[tile];
1892
+ if (image === undefined || !this.decalImageFits(image))
1893
+ continue;
1894
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, tile * BASE_DECAL_TILE, decalBandTop(this.cookieImages.length), gl.RGBA, gl.HALF_FLOAT, image);
1895
+ }
1896
+ }
1897
+ /** Whether an image is a decal's square, said once where it is not. */
1898
+ decalImageFits(image) {
1899
+ const { width, height } = sourceSize(image);
1900
+ if (width === BASE_DECAL_TILE && height === BASE_DECAL_TILE)
1901
+ return true;
1902
+ const refusal = `a base decal image of ${width} by ${height}; it is a square of ${BASE_DECAL_TILE} ` +
1903
+ 'texels, and is left clear';
1904
+ if (!this.warnedBaseDecals.has(refusal)) {
1905
+ this.warnedBaseDecals.add(refusal);
1906
+ console.warn(`Renderer: ${refusal}.`);
1907
+ }
1908
+ return false;
1909
+ }
1910
+ /**
1911
+ * The decals laid into the base colours of the draws after it, until the next call, across
1912
+ * passes: each a box, an image of `setBaseDecalImages` and how much of it. **Refused, said once,
1913
+ * where the lit stage has no room** for their thirty-four vectors, and the draws are then marked
1914
+ * by none. See `baseDecals.ts`.
1915
+ */
1916
+ setBaseDecals(decals) {
1917
+ if (this.contextLost)
1918
+ return;
1919
+ this.baseDecalList = decals;
1920
+ if (decals.length > 0 && !this.baseDecalsFit()) {
1921
+ this.baseDecalFloats.fill(0);
1922
+ }
1923
+ else {
1924
+ const refusal = packBaseDecals(decals, this.decalImages.length, decalBandTop(this.cookieImages.length), this.baseDecalFloats);
1925
+ if (refusal !== null && !this.warnedBaseDecals.has(refusal)) {
1926
+ this.warnedBaseDecals.add(refusal);
1927
+ console.warn(`Renderer: ${refusal}.`);
1928
+ }
1929
+ if (this.baseDecalFloats[0] > 0)
1930
+ this.askLit('BASE_DECALS');
1931
+ }
1932
+ this.materials.dirty();
1933
+ this.writeFlatVector('uBaseDecals', this.baseDecalFloats);
1934
+ }
1935
+ baseDecalsFit() {
1936
+ this.decalRoom ??= this.switchFits('BASE_DECALS');
1937
+ if (!this.decalRoom && !this.warnedDecalRoom) {
1938
+ this.warnedDecalRoom = true;
1939
+ console.warn('Renderer: base decals are refused on this device. They take thirty-four fragment uniform ' +
1940
+ 'vectors the lit shader does not have, so nothing is marked. A lower `maxLights` makes ' +
1941
+ 'the room, or WebGPU has it.');
1942
+ }
1943
+ return this.decalRoom;
1944
+ }
1818
1945
  /**
1819
1946
  * The fixture atlas from the profiles and cookies in hand: the profiles' rows at the bottom, the
1820
1947
  * cookies' tiles on top, one `RGBA16F` texture. See `fixtureAtlas.ts` for the layout.
@@ -1826,7 +1953,7 @@ export class WebGL2Renderer {
1826
1953
  */
1827
1954
  buildFixtureAtlas() {
1828
1955
  const { gl } = this;
1829
- const layout = fixtureAtlasLayout(this.iesAtlas.height, this.cookieImages.length);
1956
+ const layout = fixtureAtlasLayout(this.iesAtlas.height, this.cookieImages.length, this.decalImages.length);
1830
1957
  if (this.fixtureTexture === null)
1831
1958
  this.fixtureTexture = gl.createTexture();
1832
1959
  gl.activeTexture(gl.TEXTURE0 + FIXTURE_ATLAS_TEXTURE_UNIT);
@@ -1838,6 +1965,7 @@ export class WebGL2Renderer {
1838
1965
  continue;
1839
1966
  gl.texSubImage2D(gl.TEXTURE_2D, 0, tile * COOKIE_TILE, 0, gl.RGBA, gl.HALF_FLOAT, image);
1840
1967
  }
1968
+ this.uploadDecalImages();
1841
1969
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1842
1970
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1843
1971
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
@@ -2481,6 +2609,8 @@ export class WebGL2Renderer {
2481
2609
  const { gl } = this;
2482
2610
  if (!this.contextLost)
2483
2611
  this.skinScatter?.dispose();
2612
+ if (!this.contextLost)
2613
+ this.motionPass.dispose(gl);
2484
2614
  this.detachContextLoss();
2485
2615
  this.contextLostListeners.length = 0;
2486
2616
  this.contextRestoredListeners.length = 0;
@@ -2652,6 +2782,8 @@ export class WebGL2Renderer {
2652
2782
  * buffer is the same buffer. Rebinding here would cost a state change for nothing.
2653
2783
  */
2654
2784
  updateMesh(mesh, positions, normals) {
2785
+ if (!this.contextLost)
2786
+ this.keepPositionsBefore(mesh);
2655
2787
  mesh.update(this.gl, positions, normals);
2656
2788
  }
2657
2789
  /** The program and feedback object every point skin poses with, made at the first one. */
@@ -2679,8 +2811,19 @@ export class WebGL2Renderer {
2679
2811
  if (this.contextLost || this.pointSkinPose === null)
2680
2812
  return;
2681
2813
  poseBounds(matrices, skin.reach, skin.mesh.bounds);
2814
+ this.keepPositionsBefore(skin.mesh);
2682
2815
  skin.pose(this.gl, this.pointSkinPose, matrices);
2683
2816
  }
2817
+ /**
2818
+ * Rewrite part of a skin in place. A command in the stream like a pose, so the pose after it reads
2819
+ * the rewrite. See `GlPointSkin.rewrite`.
2820
+ */
2821
+ updatePointSkin(skin, first, data) {
2822
+ validatePointSkinRange(data, first, skin.mesh.vertexCount);
2823
+ if (this.contextLost)
2824
+ return;
2825
+ skin.rewrite(this.gl, first, data);
2826
+ }
2684
2827
  disposePointSkin(skin) {
2685
2828
  skin.dispose(this.gl);
2686
2829
  }
@@ -3300,6 +3443,22 @@ export class WebGL2Renderer {
3300
3443
  this.materialFlags[2] = emissiveMap === null ? 0 : 1;
3301
3444
  const emissiveScale = material?.emissiveScale ?? null;
3302
3445
  gl.uniform3f(u['uEmissiveScale'] ?? null, emissiveScale?.[0] ?? 1, emissiveScale?.[1] ?? 1, emissiveScale?.[2] ?? 1);
3446
+ /* And its glow's scroll and mask, where the lit stage has room for them: emissiveFlow.ts. */
3447
+ if (packEmissiveFlow(material, this.flowFloats) && this.emissiveFlowFits()) {
3448
+ this.askLit('EMISSIVE_FLOW');
3449
+ }
3450
+ else {
3451
+ this.flowFloats.fill(0, 0, 4);
3452
+ }
3453
+ gl.uniform4fv(u['uEmissiveFlow'] ?? null, this.flowFloats);
3454
+ /* And its iridescent overlay, where it can be drawn: iridescentOverlay.ts. */
3455
+ const iridescent = this.iridescentOf(material);
3456
+ packIridescentOverlay(iridescent, material ?? {}, this.iridescentFloats);
3457
+ if (iridescent !== null)
3458
+ this.askLit('IRIDESCENT_OVERLAY');
3459
+ gl.uniform4fv(u['uIridescence'] ?? null, this.iridescentFloats);
3460
+ /* And whether the frame's decals mark it. See `SurfaceMaterial.receivesDecals`. */
3461
+ gl.uniform4f(u['uDecalReceiver'] ?? null, material?.receivesDecals === false ? 1 : 0, 0, 0, 0);
3303
3462
  /*
3304
3463
  * **The scale belongs to the material, not to the albedo.** It was uploaded on the albedo
3305
3464
  * path alone, which was right while albedo was the only map: a material with a normal and no
@@ -3530,6 +3689,49 @@ export class WebGL2Renderer {
3530
3689
  this.overlayRoom ??= this.switchFits('SURFACE_OVERLAY');
3531
3690
  return this.overlayRoom;
3532
3691
  }
3692
+ /**
3693
+ * A material's iridescent overlay, or null where it is refused: malformed, or the lit stage has no
3694
+ * room for its thirty-six vectors. Each said once.
3695
+ */
3696
+ iridescentOf(material) {
3697
+ const overlay = material?.iridescentOverlay ?? null;
3698
+ if (overlay === null)
3699
+ return null;
3700
+ const refusal = iridescentRefusal(overlay);
3701
+ if (refusal !== null) {
3702
+ if (!this.warnedIridescent.has(refusal)) {
3703
+ this.warnedIridescent.add(refusal);
3704
+ console.warn(`Renderer: an iridescent overlay is refused: ${refusal}. Drawn without it.`);
3705
+ }
3706
+ return null;
3707
+ }
3708
+ this.iridescentRoom ??= this.switchFits('IRIDESCENT_OVERLAY');
3709
+ if (this.iridescentRoom)
3710
+ return overlay;
3711
+ if (!this.warnedIridescentRoom) {
3712
+ this.warnedIridescentRoom = true;
3713
+ console.warn("Renderer: a material's iridescent overlay is refused on this device. It takes thirty-six " +
3714
+ 'fragment uniform vectors the lit shader does not have, so the material is drawn without ' +
3715
+ 'it. A lower `maxLights` makes the room, or WebGPU has it.');
3716
+ }
3717
+ return null;
3718
+ }
3719
+ /**
3720
+ * Whether a material's glow can be moved and masked: the lit stage has room for its two vectors.
3721
+ * **Refused, said once, where it has not**, and the glow is then drawn still and unmasked, as it
3722
+ * was before it existed.
3723
+ */
3724
+ emissiveFlowFits() {
3725
+ this.flowRoom ??= this.switchFits('EMISSIVE_FLOW');
3726
+ if (!this.flowRoom && !this.warnedFlowRoom) {
3727
+ this.warnedFlowRoom = true;
3728
+ console.warn("Renderer: a material's emissive scroll and mask are refused on this device. They take " +
3729
+ 'two fragment uniform vectors and the lit shader already uses all ' +
3730
+ `${this.fragmentVectorLimit()} this GPU offers; the glow is drawn still and unmasked. ` +
3731
+ 'A lower `maxLights` makes the room, or WebGPU has it.');
3732
+ }
3733
+ return this.flowRoom;
3734
+ }
3533
3735
  /**
3534
3736
  * Whether a material's projection can be drawn: none asked, or the lit stage has the room for its
3535
3737
  * one vector. **Refused, said once, where it has not**, as the overlay is, and the material's
@@ -5666,6 +5868,9 @@ export class WebGL2Renderer {
5666
5868
  layered: asIf.LAYERED ?? this.litOn.LAYERED,
5667
5869
  layerLooks: asIf.LAYER_LOOKS ?? this.litOn.LAYER_LOOKS,
5668
5870
  surfaceDetail: asIf.SURFACE_DETAIL ?? this.litOn.SURFACE_DETAIL,
5871
+ emissiveFlow: asIf.EMISSIVE_FLOW ?? this.litOn.EMISSIVE_FLOW,
5872
+ iridescentOverlay: asIf.IRIDESCENT_OVERLAY ?? this.litOn.IRIDESCENT_OVERLAY,
5873
+ baseDecals: asIf.BASE_DECALS ?? this.litOn.BASE_DECALS,
5669
5874
  maxLights: budget.maxLights,
5670
5875
  maxAreaLights: budget.maxAreaLights,
5671
5876
  });
@@ -6890,6 +7095,10 @@ export class WebGL2Renderer {
6890
7095
  return;
6891
7096
  /* One frame of the jitter's sequence however many times the frame binds its camera. */
6892
7097
  this.temporalHistory.nextFrame();
7098
+ /* A frame states its movers afresh. See `motionTarget.ts`. */
7099
+ this.moverFrame += 1;
7100
+ this.inFrame = true;
7101
+ this.motionPass.reset();
6893
7102
  this.skinScattered = false;
6894
7103
  this.skinScatter?.reset();
6895
7104
  if (this.litAsked) {
@@ -6978,6 +7187,8 @@ export class WebGL2Renderer {
6978
7187
  this.endViewModel();
6979
7188
  /* Skin not yet spread, because nothing blended came after it, is spread now. */
6980
7189
  this.scatterSkin();
7190
+ /* A draw after the frame, an overlay over the presented picture, is no mover of it. */
7191
+ this.inFrame = false;
6981
7192
  /*
6982
7193
  * Camera motion blur, built from the camera the frame was *rendered* with.
6983
7194
  *
@@ -7000,6 +7211,10 @@ export class WebGL2Renderer {
7000
7211
  * reproject through a `previousViewProj` that had never been written.
7001
7212
  */
7002
7213
  const wantsReprojection = blur > 0 || this.temporalJittering;
7214
+ /* The movers' own motion, drawn while last frame's view is still the one kept: motionTarget.ts. */
7215
+ const moved = this.temporalJittering && view !== null && this.hasPreviousView
7216
+ ? this.drawMotionPass(view)
7217
+ : null;
7003
7218
  if (wantsReprojection && view !== null) {
7004
7219
  if (this.hasPreviousView) {
7005
7220
  mat4.invert(this.reprojection, view);
@@ -7034,6 +7249,7 @@ export class WebGL2Renderer {
7034
7249
  blend: this.temporalHistoryUsable ? TEMPORAL_HISTORY_BLEND : 0,
7035
7250
  periodStart: this.temporalHistory.periodStart,
7036
7251
  depthToViewZ: this.taaDepthToViewZ,
7252
+ motion: moved,
7037
7253
  };
7038
7254
  }
7039
7255
  /*
@@ -7530,6 +7746,10 @@ export class WebGL2Renderer {
7530
7746
  this.layerFloats.fill(0);
7531
7747
  this.lookFloats.fill(0);
7532
7748
  this.detailFloats.fill(0);
7749
+ /* Nor a glow that moves or is masked. */
7750
+ this.flowFloats.fill(0);
7751
+ /* Nor an iridescent overlay. */
7752
+ this.iridescentFloats.fill(0);
7533
7753
  this.overlayMaps = null;
7534
7754
  this.overlayTexture = null;
7535
7755
  /* Nor an ambient of its own: the frame's, until a draw asks. See `setAmbientSH`. */
@@ -7677,6 +7897,11 @@ export class WebGL2Renderer {
7677
7897
  gl.uniform4fv(u['uLayers'] ?? null, this.layerFloats);
7678
7898
  gl.uniform4fv(u['uLayerLooks'] ?? null, this.lookFloats);
7679
7899
  gl.uniform4fv(u['uDetail'] ?? null, this.detailFloats);
7900
+ gl.uniform4fv(u['uEmissiveFlow'] ?? null, this.flowFloats);
7901
+ gl.uniform4fv(u['uIridescence'] ?? null, this.iridescentFloats);
7902
+ /* The frame's decals as last laid, held across passes, and every surface receiving them. */
7903
+ gl.uniform4fv(u['uBaseDecals'] ?? null, this.baseDecalFloats);
7904
+ gl.uniform4f(u['uDecalReceiver'] ?? null, 0, 0, 0, 0);
7680
7905
  /* Neither refracting nor glass, which is every draw that does not say otherwise. See
7681
7906
  `bindSeeThrough` for the four numbers in the one vector. */
7682
7907
  gl.uniform4f(u['uSeeThrough'] ?? null, 0, 0, 0, 0);
@@ -8056,12 +8281,10 @@ export class WebGL2Renderer {
8056
8281
  */
8057
8282
  tint = null,
8058
8283
  /**
8059
- * Where the mesh was last frame — a `Mover` or a matrix — which this backend takes and ignores.
8060
- *
8061
- * Accepted rather than absent so that one scene draws through both renderers unchanged — the
8062
- * parity rule this repository is built on. Reconstruction is WebGPU's, having no compute stage
8063
- * here, so there is no motion target for this to be written into, and a mover passed here is
8064
- * neither read nor advanced.
8284
+ * Where the mesh was last frame, a `Mover` or a matrix, which the temporal resolve follows under
8285
+ * `temporalAa`: a rigid, skinned or rewritten mesh is drawn again into the motion target at the
8286
+ * end of the frame, so its pixels reproject by its own motion and not the camera's alone. See
8287
+ * `motionTarget.ts`; a mover is advanced only where the resolve reads it.
8065
8288
  */
8066
8289
  previousModel = null) {
8067
8290
  if (this.contextLost)
@@ -8100,6 +8323,8 @@ export class WebGL2Renderer {
8100
8323
  return;
8101
8324
  this.drawBudget.ask();
8102
8325
  this.takeMaterial();
8326
+ if (this.followsMotion())
8327
+ this.recordDrawMotion(mesh, model, previousModel);
8103
8328
  /*
8104
8329
  * Skin under the screen-space blur draws its halves: the frame's, then at once its diffuse's
8105
8330
  * light and its colour, each into the blur's own target.
@@ -8123,6 +8348,109 @@ export class WebGL2Renderer {
8123
8348
  this.sceneTarget?.bind();
8124
8349
  }
8125
8350
  }
8351
+ /**
8352
+ * The frame's movers drawn into the motion target against its depth, rasterised through the
8353
+ * jittered matrix every geometry upload took (`viewProjFor`) and moved between the unjittered
8354
+ * ones; null where nothing moved or the frame is multisampled, which keeps the camera's motion
8355
+ * and says so once.
8356
+ */
8357
+ drawMotionPass(view) {
8358
+ const target = this.sceneTarget;
8359
+ if (target === null || this.motionPass.recorded === 0)
8360
+ return null;
8361
+ if (target.sampleCount > 1) {
8362
+ if (!this.warnedMotionUnkept) {
8363
+ this.warnedMotionUnkept = true;
8364
+ console.warn("Renderer: WebGL2's temporal resolve follows the camera's motion alone in a multisampled " +
8365
+ "frame; a mover's own motion is drawn against single-sampled depth.");
8366
+ }
8367
+ return null;
8368
+ }
8369
+ const depth = target.depthAttachment();
8370
+ if (depth === null)
8371
+ return null;
8372
+ jitterProjection(this.motionJittered, this.sceneMatrix(view), this.temporalJitterX, this.temporalJitterY, Math.max(1, this.gl.drawingBufferWidth), Math.max(1, this.gl.drawingBufferHeight));
8373
+ return this.motionPass.draw(this.gl, depth, target.frameWidth, target.frameHeight, this.reversedDepth, this.motionJittered, view, this.previousViewProj);
8374
+ }
8375
+ /**
8376
+ * Whether a draw now is one the temporal resolve follows: the profile resolves temporally, this is
8377
+ * the frame's own picture between `beginFrame` and `endFrame`, and not a mirror's, a probe's, a
8378
+ * capture's, a shadow's or the view model's. See `motionTarget.ts`.
8379
+ */
8380
+ followsMotion() {
8381
+ return (this.inFrame &&
8382
+ this.quality.temporalAa &&
8383
+ this.sceneTarget !== null &&
8384
+ !this.reflectionPassActive &&
8385
+ !this.probePassActive &&
8386
+ !this.capturePassActive &&
8387
+ !this.shadowPassActive &&
8388
+ !this.viewModelActive);
8389
+ }
8390
+ /**
8391
+ * A draw's motion for the resolve, decided as WebGPU's `recordDrawMotion` decides it: a mesh
8392
+ * rewritten this frame by its last positions, with or without a mover; a skinned mover by its last
8393
+ * pose; a rigid one by its last model. A cloth draw keeps the camera's motion, said once.
8394
+ */
8395
+ recordDrawMotion(mesh, model, previous) {
8396
+ const rewritten = mesh.keptPrevious && mesh.changedFrame === this.moverFrame;
8397
+ if (this.cloth !== null && this.skinPaletteSet && mesh.isSkinned) {
8398
+ if (previous !== null && !this.warnedMotionUnkept) {
8399
+ this.warnedMotionUnkept = true;
8400
+ console.warn("Renderer: a cloth draw's own motion is not followed by WebGL2's temporal resolve; it " +
8401
+ "takes the camera's. WebGPU's reconstruction follows it.");
8402
+ }
8403
+ return;
8404
+ }
8405
+ if (previous === null) {
8406
+ if (rewritten)
8407
+ this.motionPass.recordDynamic(mesh, model, model);
8408
+ return;
8409
+ }
8410
+ if (!isMover(previous)) {
8411
+ if (rewritten)
8412
+ this.motionPass.recordDynamic(mesh, model, previous);
8413
+ else
8414
+ this.motionPass.recordRigid(mesh, model, previous);
8415
+ return;
8416
+ }
8417
+ const skinned = !rewritten && this.skinPaletteSet && mesh.isSkinned;
8418
+ const verdict = advanceMover(previous, this.moverRenderer, this.moverFrame, model, skinned ? this.paletteData : null);
8419
+ if (verdict === MOVER_TWICE) {
8420
+ if (!this.warnedMoverTwice) {
8421
+ this.warnedMoverTwice = true;
8422
+ console.warn('Renderer: a Mover was drawn twice in one frame. A mover is one object; the second draw ' +
8423
+ 'records no motion. Give each moving object a mover of its own.');
8424
+ }
8425
+ return;
8426
+ }
8427
+ if (rewritten) {
8428
+ this.motionPass.recordDynamic(mesh, model, verdict === MOVER_MOTION ? previous.previousModel : model);
8429
+ return;
8430
+ }
8431
+ if (verdict !== MOVER_MOTION)
8432
+ return;
8433
+ if (skinned) {
8434
+ this.motionPass.recordSkinned(mesh, model, previous.previousModel,
8435
+ /* Each exactly its rig's length, as `advanceMover` keeps them, so neither is sliced. */
8436
+ previous.palette, previous.previousPalette);
8437
+ }
8438
+ else {
8439
+ this.motionPass.recordRigid(mesh, model, previous.previousModel);
8440
+ }
8441
+ }
8442
+ /**
8443
+ * Keep a dynamic mesh's positions before its frame's first rewrite, where the temporal resolve
8444
+ * follows motion: a rewrite before `beginFrame` belongs to the next frame (`frameOfChange`).
8445
+ */
8446
+ keepPositionsBefore(mesh) {
8447
+ if (!(this.quality.temporalAa && this.sceneTarget !== null))
8448
+ return;
8449
+ const frame = frameOfChange(this.moverFrame, this.inFrame);
8450
+ if (keepsPrevious(mesh.changedFrame, frame))
8451
+ mesh.keepPrevious(this.gl);
8452
+ mesh.changedFrame = frame;
8453
+ }
8126
8454
  /**
8127
8455
  * Whether a skin draw now draws its two halves: the profile asked, the frame has not spread its
8128
8456
  * skin yet, this is the frame's own picture — not a mirror's or a probe's, nor an overlay after
@@ -14,13 +14,26 @@ import type { GpuMesh } from './buffers.ts';
14
14
  export declare function createPointSkinPipeline(device: GPUDevice): GPUComputePipeline;
15
15
  export declare class GpuPointSkin {
16
16
  readonly mesh: GpuMesh;
17
- readonly reach: PointSkinReach;
17
+ /** Widened by each rewrite, never narrowed: see `widenReach`. */
18
+ reach: PointSkinReach;
18
19
  private readonly count;
19
20
  private readonly pipeline;
20
- private readonly joints;
21
+ private joints;
22
+ /** Params, points with their weights, joints named, normals: in that order. */
21
23
  private readonly buffers;
22
- private readonly group;
24
+ private group;
25
+ /** A rewrite's vertices laid out as the buffers hold them, grown to the largest rewrite. */
26
+ private scratch;
23
27
  constructor(device: GPUDevice, pipeline: GPUComputePipeline, mesh: GpuMesh, data: PointSkinData);
28
+ private bind;
29
+ /**
30
+ * Rewrite vertices `first` onward, as many as `data` holds, in place: three queue writes at their
31
+ * offsets. **A queue write reaches every dispatch submitted after it**, so the renderer submits a
32
+ * pose already recorded before calling this, and the rewrite reaches the next pose alone, as it
33
+ * does on WebGL2. A joint none of the skin named before grows the joints buffer, which is made
34
+ * again with its bind group; the old one goes once the work already submitted is done with it.
35
+ */
36
+ rewrite(device: GPUDevice, first: number, data: PointSkinData): void;
24
37
  /** Write the pose's joints and record the dispatch that poses every vertex into `encoder`. */
25
38
  pose(device: GPUDevice, encoder: GPUCommandEncoder, matrices: Float32Array): void;
26
39
  dispose(): void;