@driftengine/core 4.8.1 → 4.8.3

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 (159) hide show
  1. package/CHANGELOG.json +142 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +3 -1
  4. package/dist/index.js +8 -0
  5. package/dist/input/actionMap.d.ts +6 -1
  6. package/dist/input/actionMap.js +20 -7
  7. package/dist/input/input.d.ts +28 -0
  8. package/dist/input/input.js +71 -9
  9. package/dist/input/inputHarness.d.ts +9 -0
  10. package/dist/input/inputHarness.js +34 -2
  11. package/dist/render/backend/materialChanges.d.ts +7 -2
  12. package/dist/render/backend/materialChanges.js +8 -3
  13. package/dist/render/backend/webgl2/renderer.d.ts +75 -3
  14. package/dist/render/backend/webgl2/renderer.js +209 -22
  15. package/dist/render/backend/webgpu/buffers.d.ts +2 -0
  16. package/dist/render/backend/webgpu/buffers.js +2 -0
  17. package/dist/render/backend/webgpu/causticsPass.js +2 -1
  18. package/dist/render/backend/webgpu/depthPass.d.ts +13 -3
  19. package/dist/render/backend/webgpu/depthPass.js +4 -3
  20. package/dist/render/backend/webgpu/filmPass.js +2 -1
  21. package/dist/render/backend/webgpu/flatPass.d.ts +5 -0
  22. package/dist/render/backend/webgpu/flatPass.js +18 -8
  23. package/dist/render/backend/webgpu/linePass.js +2 -1
  24. package/dist/render/backend/webgpu/panelPass.js +2 -1
  25. package/dist/render/backend/webgpu/particlePass.js +2 -1
  26. package/dist/render/backend/webgpu/pipelineCache.d.ts +40 -8
  27. package/dist/render/backend/webgpu/pipelineCache.js +76 -9
  28. package/dist/render/backend/webgpu/plumePass.js +2 -1
  29. package/dist/render/backend/webgpu/postPass.d.ts +12 -2
  30. package/dist/render/backend/webgpu/postPass.js +19 -5
  31. package/dist/render/backend/webgpu/renderer.d.ts +66 -2
  32. package/dist/render/backend/webgpu/renderer.js +189 -38
  33. package/dist/render/backend/webgpu/sdfTextPass.js +2 -1
  34. package/dist/render/backend/webgpu/textPass.js +2 -1
  35. package/dist/render/backend/webgpu/waterPass.js +2 -1
  36. package/dist/render/backend/webgpu/windStreakPass.js +2 -1
  37. package/dist/render/boltBatch.js +2 -1
  38. package/dist/render/causticsRenderer.js +2 -1
  39. package/dist/render/cutoutCaster.d.ts +3 -1
  40. package/dist/render/cutoutCaster.js +4 -0
  41. package/dist/render/decalPass.js +2 -1
  42. package/dist/render/globalMediumPass.js +3 -2
  43. package/dist/render/lineBatch.js +5 -1
  44. package/dist/render/mapsWithoutUvs.d.ts +24 -0
  45. package/dist/render/mapsWithoutUvs.js +24 -0
  46. package/dist/render/mesh.d.ts +2 -0
  47. package/dist/render/mesh.js +3 -0
  48. package/dist/render/oitPass.js +2 -1
  49. package/dist/render/particleBatch.js +3 -2
  50. package/dist/render/particlePool.d.ts +21 -2
  51. package/dist/render/particlePool.js +72 -37
  52. package/dist/render/pass.d.ts +12 -0
  53. package/dist/render/plumeRenderer.js +3 -2
  54. package/dist/render/renderQuality.d.ts +16 -8
  55. package/dist/render/sceneCoverage.d.ts +54 -0
  56. package/dist/render/sceneCoverage.js +72 -0
  57. package/dist/render/sdfTextRenderer.js +2 -1
  58. package/dist/render/shaders/depth.js +3 -2
  59. package/dist/render/shaders/flat/glassTint.d.ts +28 -0
  60. package/dist/render/shaders/flat/glassTint.js +31 -0
  61. package/dist/render/shaders/flat/index.d.ts +12 -0
  62. package/dist/render/shaders/flat/index.js +13 -2
  63. package/dist/render/shaders/flat/main.d.ts +1 -1
  64. package/dist/render/shaders/flat/main.js +60 -25
  65. package/dist/render/shaders/flat/surfaceEffects.d.ts +1 -1
  66. package/dist/render/shaders/flat/surfaceEffects.js +6 -2
  67. package/dist/render/shaders/generated/depth.wgsl.d.ts +32 -12
  68. package/dist/render/shaders/generated/depth.wgsl.js +32 -12
  69. package/dist/render/shaders/generated/flat.wgsl.d.ts +89 -0
  70. package/dist/render/shaders/generated/flat.wgsl.js +127 -38
  71. package/dist/render/shaders/generated/line.wgsl.d.ts +1 -1
  72. package/dist/render/shaders/generated/line.wgsl.js +1 -1
  73. package/dist/render/shaders/generated/particle.wgsl.d.ts +3 -3
  74. package/dist/render/shaders/generated/particle.wgsl.js +3 -3
  75. package/dist/render/shaders/generated/rush.wgsl.d.ts +1 -1
  76. package/dist/render/shaders/generated/rush.wgsl.js +1 -1
  77. package/dist/render/shaders/generated/sdfText.wgsl.d.ts +1 -1
  78. package/dist/render/shaders/generated/sdfText.wgsl.js +1 -1
  79. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
  80. package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
  81. package/dist/render/shaders/line.d.ts +1 -1
  82. package/dist/render/shaders/outputTransform.d.ts +14 -1
  83. package/dist/render/shaders/outputTransform.js +23 -1
  84. package/dist/render/shaders/particle.d.ts +3 -3
  85. package/dist/render/shaders/recon/resolve.wgsl.js +15 -1
  86. package/dist/render/shaders/rush.js +13 -2
  87. package/dist/render/shaders/sdfText.d.ts +1 -1
  88. package/dist/render/shaders/temporalResolve.js +12 -5
  89. package/dist/render/ssrPass.js +2 -1
  90. package/dist/render/surfaceTexture.d.ts +13 -0
  91. package/dist/render/surfaceTexture.js +4 -0
  92. package/dist/render/textRenderer.js +2 -1
  93. package/dist/render/vertexDefaults.js +1 -0
  94. package/dist/render/viewModel.d.ts +29 -0
  95. package/dist/render/viewModel.js +35 -0
  96. package/dist/render/waterRenderer.js +2 -1
  97. package/dist/render/windStreakRenderer.js +2 -1
  98. package/package.json +6 -6
  99. package/src/index.ts +22 -1
  100. package/src/input/actionMap.ts +23 -8
  101. package/src/input/input.ts +79 -9
  102. package/src/input/inputHarness.ts +27 -2
  103. package/src/render/backend/materialChanges.ts +8 -3
  104. package/src/render/backend/webgl2/renderer.ts +223 -29
  105. package/src/render/backend/webgpu/buffers.ts +5 -0
  106. package/src/render/backend/webgpu/causticsPass.ts +2 -1
  107. package/src/render/backend/webgpu/depthPass.ts +4 -3
  108. package/src/render/backend/webgpu/filmPass.ts +2 -1
  109. package/src/render/backend/webgpu/flatPass.ts +43 -26
  110. package/src/render/backend/webgpu/linePass.ts +2 -1
  111. package/src/render/backend/webgpu/panelPass.ts +2 -1
  112. package/src/render/backend/webgpu/particlePass.ts +2 -1
  113. package/src/render/backend/webgpu/pipelineCache.ts +82 -8
  114. package/src/render/backend/webgpu/plumePass.ts +2 -1
  115. package/src/render/backend/webgpu/postPass.ts +20 -5
  116. package/src/render/backend/webgpu/renderer.ts +191 -37
  117. package/src/render/backend/webgpu/sdfTextPass.ts +2 -1
  118. package/src/render/backend/webgpu/textPass.ts +2 -1
  119. package/src/render/backend/webgpu/waterPass.ts +2 -1
  120. package/src/render/backend/webgpu/windStreakPass.ts +2 -1
  121. package/src/render/boltBatch.ts +2 -1
  122. package/src/render/causticsRenderer.ts +2 -1
  123. package/src/render/cutoutCaster.ts +15 -2
  124. package/src/render/decalPass.ts +2 -1
  125. package/src/render/globalMediumPass.ts +3 -2
  126. package/src/render/lineBatch.ts +3 -1
  127. package/src/render/mapsWithoutUvs.ts +43 -0
  128. package/src/render/mesh.ts +3 -0
  129. package/src/render/oitPass.ts +2 -1
  130. package/src/render/particleBatch.ts +3 -2
  131. package/src/render/particlePool.ts +107 -56
  132. package/src/render/pass.ts +12 -0
  133. package/src/render/plumeRenderer.ts +3 -2
  134. package/src/render/renderQuality.ts +16 -8
  135. package/src/render/sceneCoverage.ts +78 -0
  136. package/src/render/sdfTextRenderer.ts +2 -1
  137. package/src/render/shaders/depth.ts +3 -2
  138. package/src/render/shaders/flat/glassTint.ts +48 -0
  139. package/src/render/shaders/flat/index.ts +25 -2
  140. package/src/render/shaders/flat/main.ts +60 -25
  141. package/src/render/shaders/flat/surfaceEffects.ts +6 -2
  142. package/src/render/shaders/generated/depth.wgsl.ts +32 -12
  143. package/src/render/shaders/generated/flat.wgsl.ts +127 -38
  144. package/src/render/shaders/generated/line.wgsl.ts +1 -1
  145. package/src/render/shaders/generated/particle.wgsl.ts +3 -3
  146. package/src/render/shaders/generated/rush.wgsl.ts +1 -1
  147. package/src/render/shaders/generated/sdfText.wgsl.ts +1 -1
  148. package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
  149. package/src/render/shaders/outputTransform.ts +24 -1
  150. package/src/render/shaders/recon/resolve.wgsl.ts +15 -1
  151. package/src/render/shaders/rush.ts +13 -2
  152. package/src/render/shaders/temporalResolve.ts +12 -5
  153. package/src/render/ssrPass.ts +2 -1
  154. package/src/render/surfaceTexture.ts +16 -0
  155. package/src/render/textRenderer.ts +2 -1
  156. package/src/render/vertexDefaults.ts +1 -0
  157. package/src/render/viewModel.ts +41 -0
  158. package/src/render/waterRenderer.ts +2 -1
  159. package/src/render/windStreakRenderer.ts +2 -1
@@ -1,6 +1,7 @@
1
1
  import { FrameBudget } from '../budget.ts';
2
2
  import { MaterialChanges, ownsMaterial } from '../materialChanges.ts';
3
- import { FOG_RECEDE, fogModeOf } from '../drawFog.ts';
3
+ import { FOG_OFF, FOG_RECEDE, fogModeOf } from '../drawFog.ts';
4
+ import { blendCovering, blendKeeping } from '../../sceneCoverage.ts';
4
5
  import { mat4 } from 'gl-matrix';
5
6
  import {
6
7
  GL_DEPTH_REMAP,
@@ -257,6 +258,9 @@ import type { InstancedOptions, MeshInstances } from '../../instances.ts';
257
258
  import { cutoutOf } from '../../cutoutCaster.ts';
258
259
  import type { CutoutCaster } from '../../cutoutCaster.ts';
259
260
  import type { SurfaceTextureHandle } from '../api.ts';
261
+ import { materialHasMaps, noteMapsWithoutUvs } from '../../mapsWithoutUvs.ts';
262
+ import { VIEW_MODEL_DEPTH_SHARE, viewModelDepthRange } from '../../viewModel.ts';
263
+ import { noLitSwitches, type LitSwitch } from '../../shaders/flat/glassTint.ts';
260
264
 
261
265
  /**
262
266
  * How many frames `firstFrameSettled` will ask before it stops asking.
@@ -1087,8 +1091,8 @@ export class WebGL2Renderer implements RendererApi {
1087
1091
 
1088
1092
  private presentedFrameCount = 0;
1089
1093
 
1090
- private readonly flatProgram: WebGLProgram;
1091
- private readonly flatUniforms: Record<string, WebGLUniformLocation>;
1094
+ private flatProgram: WebGLProgram;
1095
+ private flatUniforms: Record<string, WebGLUniformLocation>;
1092
1096
  /**
1093
1097
  * The lit fragment source every flat program is built from, and the budget it was built at.
1094
1098
  *
@@ -1099,7 +1103,7 @@ export class WebGL2Renderer implements RendererApi {
1099
1103
  * for a lazily compiled variant to end up at a different light budget from the one the renderer
1100
1104
  * uploads for, which is a picture nobody could explain.
1101
1105
  */
1102
- private readonly flatFragSource: string;
1106
+ private flatFragSource: string;
1103
1107
  /**
1104
1108
  * How many lights this renderer's shader has room for, after the device had its say.
1105
1109
  *
@@ -1246,6 +1250,11 @@ export class WebGL2Renderer implements RendererApi {
1246
1250
  private readonly surfaceScene = new Float32Array(4);
1247
1251
  /** The two halves of `uWriteMode`, kept here because a `vec2` is written whole. */
1248
1252
  private oitWeight = 0;
1253
+ /**
1254
+ * How the surfaces drawn next meet the medium: what `setSurfaceFog` set, and `FOG_RECEDE` from
1255
+ * each `bindMeshPass`. A translucent draw's own fog is measured against this and puts it back.
1256
+ */
1257
+ private surfaceFog = FOG_RECEDE;
1249
1258
  private ditherFade = 0;
1250
1259
  /** Said once, not per frame, where the context cannot give the float target the sum needs. */
1251
1260
  private oitRefused = false;
@@ -1683,6 +1692,33 @@ export class WebGL2Renderer implements RendererApi {
1683
1692
  * each such draw and handed back, where the other backend bakes it into a pipeline.
1684
1693
  */
1685
1694
  private materialDoubleSided = false;
1695
+ /** Whether the bound material has a map, for the warning a mesh with no coordinates earns. */
1696
+ private materialHasMaps = false;
1697
+ /** The material last set, so a lit program rebuilt for glass can be given it. */
1698
+ private currentMaterial: SurfaceMaterial | null = null;
1699
+ /**
1700
+ * The lit stage's switches: what the lit programs were compiled with (`litOn`), and what the
1701
+ * profile and the content have asked for since (`litWanted`).
1702
+ *
1703
+ * **Each is off until it is used** — glass until a pane is offered, a fixture until one is loaded,
1704
+ * the effects until a material carries a table, DriftLight until a volume is set — and then on for
1705
+ * good, rebuilt at the next `beginFrame`. Clustering is the profile's. The code costs the lit pass
1706
+ * registers on every surface whether it finds anything or not, and a world without the feature
1707
+ * draws the same picture without it. `PipelineCache.litSwitches` makes the same decision on the
1708
+ * other backend. The frame a feature is first used in draws without it; the rebuild is a
1709
+ * synchronous compile, and the next frame waits for it.
1710
+ */
1711
+ private readonly litOn = noLitSwitches();
1712
+ private readonly litWanted = noLitSwitches();
1713
+ private litAsked = false;
1714
+
1715
+ /** Ask for a switch, which the next `beginFrame` compiles in. Never glass with it turned off. */
1716
+ private askLit(feature: LitSwitch): void {
1717
+ if (this.litWanted[feature]) return;
1718
+ if (feature === 'GLASS_SHADOWS' && this.quality.glassShadows === 'off') return;
1719
+ this.litWanted[feature] = true;
1720
+ this.litAsked = true;
1721
+ }
1686
1722
 
1687
1723
  /**
1688
1724
  * The one sink handed to every caster enumeration.
@@ -1715,6 +1751,7 @@ export class WebGL2Renderer implements RendererApi {
1715
1751
  /* Glass: kept aside for its own two passes rather than dropped. See `glassCasters.ts`. */
1716
1752
  if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
1717
1753
  this.glassCasters.recordMesh(mesh, model, material, this.glassCasterScratch);
1754
+ this.askLit('GLASS_SHADOWS');
1718
1755
  }
1719
1756
  return;
1720
1757
  }
@@ -1756,6 +1793,7 @@ export class WebGL2Renderer implements RendererApi {
1756
1793
  if (!this.drawingGlassDepth && !castsDepth(material)) {
1757
1794
  if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
1758
1795
  this.glassCasters.recordInstanced(batch, data, material, this.glassCasterScratch);
1796
+ this.askLit('GLASS_SHADOWS');
1759
1797
  }
1760
1798
  return;
1761
1799
  }
@@ -1789,6 +1827,7 @@ export class WebGL2Renderer implements RendererApi {
1789
1827
  if (!this.drawingGlassDepth && !castsDepth(material)) {
1790
1828
  if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
1791
1829
  this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
1830
+ this.askLit('GLASS_SHADOWS');
1792
1831
  }
1793
1832
  return;
1794
1833
  }
@@ -1849,6 +1888,7 @@ export class WebGL2Renderer implements RendererApi {
1849
1888
  gl.uniform1i(u['uPeelShadowLayer'] ?? null, this.activeDepthPeel ? 1 : 0);
1850
1889
  if (this.activeDepthPeel) gl.uniform1i(u['uPreviousShadowMap'] ?? null, 0);
1851
1890
  gl.uniform2f(u['uUvScale'] ?? null, cutout.u, cutout.v);
1891
+ gl.uniform2f(u['uUvOffset'] ?? null, cutout.uOffset, cutout.vOffset);
1852
1892
  gl.uniform1f(u['uAlphaCutout'] ?? null, cutout.cutoff);
1853
1893
  /* Narrowed for the reason the sink's own `mesh` gives: this renderer only hands out its own. */
1854
1894
  (cutout.albedo as SurfaceTexture).bind(gl, SURFACE_TEXTURE_UNIT);
@@ -2297,6 +2337,8 @@ export class WebGL2Renderer implements RendererApi {
2297
2337
  this.iesRows = this.iesAtlas.height;
2298
2338
  this.iesPlanes = this.iesAtlas.planes;
2299
2339
  this.cookieTiles = this.cookieImages.length;
2340
+ /* The first profile or cookie is when the lit programs start reading them. See `litOn`. */
2341
+ if (this.iesRows > 1 || this.cookieTiles > 0) this.askLit('LIGHT_FIXTURES');
2300
2342
  gl.bindTexture(gl.TEXTURE_2D, null);
2301
2343
  gl.activeTexture(gl.TEXTURE0);
2302
2344
  }
@@ -2714,6 +2756,9 @@ export class WebGL2Renderer implements RendererApi {
2714
2756
  * Built for the profile this renderer resolved, not for every profile at once — so a
2715
2757
  * world with shadows off compiles none of the shadow path. See `shaders/flat.ts`.
2716
2758
  */
2759
+ /* The one switch the profile decides; the rest wait for their content. See `litOn`. */
2760
+ this.litOn.CLUSTERED_LIGHTS = this.quality.clusteredLights;
2761
+ this.litWanted.CLUSTERED_LIGHTS = this.quality.clusteredLights;
2717
2762
  const fitted = this.fitFlatProgram(gl);
2718
2763
  this.flatProgram = fitted.program;
2719
2764
  this.flatFragSource = fitted.source;
@@ -3306,6 +3351,8 @@ export class WebGL2Renderer implements RendererApi {
3306
3351
  this.materials.dirty();
3307
3352
  this.currentSurfaceTexture = material?.albedo ?? null;
3308
3353
  this.materialDoubleSided = material?.doubleSided === true;
3354
+ this.materialHasMaps = materialHasMaps(material);
3355
+ this.currentMaterial = material;
3309
3356
  /*
3310
3357
  * Written to every flat program, because material state persists across draws and a skinned
3311
3358
  * draw is entitled to the material the caller set before it. A second program holding none of
@@ -3413,6 +3460,7 @@ export class WebGL2Renderer implements RendererApi {
3413
3460
  * how big a dome was — WebGPU writes it before its own early return and WebGL2 did not.
3414
3461
  */
3415
3462
  gl.uniform2f(u['uUvScale'] ?? null, material?.uScale ?? 1, material?.vScale ?? 1);
3463
+ gl.uniform2f(u['uUvOffset'] ?? null, material?.uOffset ?? 0, material?.vOffset ?? 0);
3416
3464
  gl.uniform1i(u['uDoubleSided'] ?? null, material?.doubleSided === true ? 1 : 0);
3417
3465
 
3418
3466
  /* The albedo's effects table, or the stand-in the shader reads as none. Before the early
@@ -3422,6 +3470,8 @@ export class WebGL2Renderer implements RendererApi {
3422
3470
  gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
3423
3471
  } else {
3424
3472
  albedo.bindEffects(gl, SURFACE_EFFECTS_TEXTURE_UNIT, this.emptyTexture2D);
3473
+ /* The first table bound is when the lit programs start reading one. See `litOn`. */
3474
+ if (albedo.hasEffects) this.askLit('SURFACE_EFFECTS');
3425
3475
  }
3426
3476
  gl.uniform1i(u['uSurfaceEffects'] ?? null, SURFACE_EFFECTS_TEXTURE_UNIT);
3427
3477
  if (albedo === null) {
@@ -3543,6 +3593,28 @@ export class WebGL2Renderer implements RendererApi {
3543
3593
  this.writeFlatFloat('uGrain', Math.min(1, Math.max(0, amount)));
3544
3594
  }
3545
3595
 
3596
+ /**
3597
+ * Whether the surfaces drawn next meet the medium: distance fog and the global medium's haze.
3598
+ * True is the default, and what `bindMeshPass` restores.
3599
+ *
3600
+ * **The opaque counterpart of `TranslucentMeshOptions.fog`**, which a translucent draw has had
3601
+ * all along; the shader's per-draw switch was there, and nothing reached it for an ordinary mesh.
3602
+ * What it is for is a medium that darkens one thing and not another — a floor receding into the
3603
+ * distance under figures that must keep their full value however far from the camera they stand.
3604
+ * Material state like `setSurfaceGrain`: instanced and skinned draws read it too, and a
3605
+ * translucent draw's own `fog` is measured against it and puts it back.
3606
+ *
3607
+ * **What it gives up** is the medium's honesty: a surface out of the fog is drawn as though the
3608
+ * air between it and the eye were empty, so it should be a surface the scene means to stand
3609
+ * apart, not one whose distance is the point.
3610
+ */
3611
+ setSurfaceFog(enabled: boolean): void {
3612
+ if (this.contextLost) return;
3613
+ this.materials.dirty();
3614
+ this.surfaceFog = enabled ? FOG_RECEDE : FOG_OFF;
3615
+ this.writeFlatInt('uFogEnabled', this.surfaceFog);
3616
+ }
3617
+
3546
3618
  /**
3547
3619
  * One float of material state, into every flat program.
3548
3620
  *
@@ -3560,6 +3632,15 @@ export class WebGL2Renderer implements RendererApi {
3560
3632
  this.useFlatProgram();
3561
3633
  }
3562
3634
 
3635
+ /** One int of material state, into every flat program, for `writeFlatFloat`'s reason. */
3636
+ private writeFlatInt(name: string, value: number): void {
3637
+ for (const target of this.flatTargets) {
3638
+ this.gl.useProgram(target.program);
3639
+ this.gl.uniform1i(target.uniforms[name] ?? null, value);
3640
+ }
3641
+ this.useFlatProgram();
3642
+ }
3643
+
3563
3644
  /**
3564
3645
  * How much of the surfaces drawn next survives a screen-door dither, for crossfading two levels
3565
3646
  * of detail of one thing.
@@ -3810,7 +3891,7 @@ export class WebGL2Renderer implements RendererApi {
3810
3891
  /* A blended batch shows what is behind it — refraction or glass — as a single draw does, and
3811
3892
  counts a material of its own for it by the rule both backends share (`materialChanges.ts`). */
3812
3893
  const refracting = blend && this.bindSeeThrough(u, options);
3813
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
3894
+ const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog);
3814
3895
  if (own) this.materials.dirty();
3815
3896
  this.takeMaterial();
3816
3897
  const depthWrite = options.depthWrite ?? true;
@@ -3834,14 +3915,13 @@ export class WebGL2Renderer implements RendererApi {
3834
3915
  * part of what the count asks.
3835
3916
  */
3836
3917
  if (!lit) gl.uniform1i(u['uLightingEnabled'] ?? null, 0);
3837
- if (fog !== FOG_RECEDE) gl.uniform1i(u['uFogEnabled'] ?? null, fog);
3918
+ if (fog !== this.surfaceFog) gl.uniform1i(u['uFogEnabled'] ?? null, fog);
3838
3919
  if (!toneMapped) gl.uniform1i(u['uOutputTransform'] ?? null, Math.min(this.gradeCode(), 1));
3839
3920
 
3840
3921
  if (blend) {
3841
3922
  gl.enable(gl.BLEND);
3842
- if (options.additive === true) gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE, gl.ZERO, gl.ONE);
3843
- else
3844
- gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
3923
+ if (options.additive === true) blendKeeping(gl, gl.SRC_ALPHA, gl.ONE);
3924
+ else blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
3845
3925
  if (!depthWrite) gl.depthMask(false);
3846
3926
  }
3847
3927
  /* The sign follows the compare, which `depthOffsetForLayer` owns — see `drawMesh`, where the
@@ -3874,7 +3954,7 @@ export class WebGL2Renderer implements RendererApi {
3874
3954
  outlives its frame, so a snapshot left on unit 14 is held against the next frame's copy. */
3875
3955
  if (refracting) this.unbindSeeThrough(u);
3876
3956
  if (!lit) gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
3877
- if (fog !== FOG_RECEDE) gl.uniform1i(u['uFogEnabled'] ?? null, FOG_RECEDE);
3957
+ if (fog !== this.surfaceFog) gl.uniform1i(u['uFogEnabled'] ?? null, this.surfaceFog);
3878
3958
  if (!toneMapped) gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
3879
3959
  if (own) this.materials.dirty();
3880
3960
  this.useFlatProgram();
@@ -5214,6 +5294,7 @@ export class WebGL2Renderer implements RendererApi {
5214
5294
  gl.uniform4fv(u['uGlassLight'] ?? null, this.glassLight);
5215
5295
  if (cutout !== null) {
5216
5296
  gl.uniform2f(u['uUvScale'] ?? null, cutout.u, cutout.v);
5297
+ gl.uniform2f(u['uUvOffset'] ?? null, cutout.uOffset, cutout.vOffset);
5217
5298
  gl.uniform1f(u['uAlphaCutout'] ?? null, cutout.cutoff);
5218
5299
  (cutout.albedo as SurfaceTexture).bind(gl, SURFACE_TEXTURE_UNIT);
5219
5300
  gl.uniform1i(u['uCutoutMap'] ?? null, SURFACE_TEXTURE_UNIT);
@@ -5317,21 +5398,91 @@ export class WebGL2Renderer implements RendererApi {
5317
5398
  * rather than propagating. Only a shader that will not link at *any* budget throws, and it
5318
5399
  * throws a sentence with the device's numbers in it rather than a bare driver string.
5319
5400
  */
5320
- private fitFlatProgram(gl: WebGL2RenderingContext): {
5321
- program: WebGLProgram;
5322
- source: string;
5323
- budget: LightBudget;
5324
- } {
5325
- const variant = {
5401
+ /** The lit fragment source this renderer's profile compiles, at a light budget. */
5402
+ private flatFragAt(budget: LightBudget): string {
5403
+ return flatFrag({
5326
5404
  pointShadows: this.quality.pointShadows,
5327
5405
  directionalShadows: this.quality.directionalShadows,
5328
5406
  environmentProbe: this.probeCapture !== null,
5329
5407
  nightEmissive: this.quality.nightEmissive,
5330
- /* Off compiles none of the glass lookup: see `FlatShaderOptions.glassShadows`. */
5331
- glassShadows: this.quality.glassShadows !== 'off',
5408
+ /* Each off until it is used: see `litOn`. */
5409
+ glassShadows: this.litOn.GLASS_SHADOWS,
5410
+ clusteredLights: this.litOn.CLUSTERED_LIGHTS,
5411
+ lightFixtures: this.litOn.LIGHT_FIXTURES,
5412
+ surfaceEffects: this.litOn.SURFACE_EFFECTS,
5413
+ driftLight: this.litOn.DRIFT_LIGHT,
5414
+ maxLights: budget.maxLights,
5415
+ maxAreaLights: budget.maxAreaLights,
5416
+ });
5417
+ }
5418
+
5419
+ /**
5420
+ * Every lit program that exists, compiled again with the switches asked for since, at the budget
5421
+ * the renderer already uploads for, and given the pass and the material the old one held.
5422
+ *
5423
+ * At `beginFrame`, not where a feature was first used: a pane is offered inside a shadow pass
5424
+ * with the depth program bound, and a program swapped under it would draw the next caster with
5425
+ * the lit one.
5426
+ */
5427
+ private rebuildLit(): void {
5428
+ const { gl } = this;
5429
+ Object.assign(this.litOn, this.litWanted);
5430
+ this.flatFragSource = this.flatFragAt(this.lightBudget);
5431
+ const recompile = (
5432
+ old: WebGLProgram | null,
5433
+ skinned: boolean,
5434
+ morphed: boolean,
5435
+ instanced: boolean,
5436
+ label: string,
5437
+ ): { program: WebGLProgram; uniforms: Record<string, WebGLUniformLocation> } | null => {
5438
+ if (old === null) return null;
5439
+ const program = compileProgram(
5440
+ gl,
5441
+ flatVert({ skinned, morphed, instanced }),
5442
+ this.flatFragSource,
5443
+ label,
5444
+ );
5445
+ gl.deleteProgram(old);
5446
+ return { program, uniforms: uniformLocations(gl, program, label) };
5332
5447
  };
5333
- const build = (budget: LightBudget): string =>
5334
- flatFrag({ ...variant, maxLights: budget.maxLights, maxAreaLights: budget.maxAreaLights });
5448
+ const plain = recompile(this.flatProgram, false, false, false, 'flat');
5449
+ if (plain !== null) {
5450
+ this.flatProgram = plain.program;
5451
+ this.flatUniforms = plain.uniforms;
5452
+ }
5453
+ const skinned = recompile(this.flatSkinnedProgram, true, false, false, 'flat.skinned');
5454
+ this.flatSkinnedProgram = skinned?.program ?? null;
5455
+ this.flatSkinnedUniforms = skinned?.uniforms ?? null;
5456
+ const morphed = recompile(this.flatMorphedProgram, false, true, false, 'flat.morphed');
5457
+ this.flatMorphedProgram = morphed?.program ?? null;
5458
+ this.flatMorphedUniforms = morphed?.uniforms ?? null;
5459
+ const both = recompile(this.flatBothProgram, true, true, false, 'flat.morphedskinned');
5460
+ this.flatBothProgram = both?.program ?? null;
5461
+ this.flatBothUniforms = both?.uniforms ?? null;
5462
+ const instanced = recompile(this.flatInstancedProgram, false, false, true, 'flat.instanced');
5463
+ this.flatInstancedProgram = instanced?.program ?? null;
5464
+ this.flatInstancedUniforms = instanced?.uniforms ?? null;
5465
+
5466
+ this.flatTargets.length = 0;
5467
+ for (const target of [plain, skinned, morphed, both, instanced]) {
5468
+ if (target !== null) this.flatTargets.push(target);
5469
+ }
5470
+ const camera = this.lastPassCamera;
5471
+ const env = this.lastPassEnv;
5472
+ for (const target of this.flatTargets) {
5473
+ gl.useProgram(target.program);
5474
+ if (camera !== null && env !== null) this.writeMeshPassState(target.uniforms, camera, env);
5475
+ this.writeMaterialState(target.uniforms, this.currentMaterial);
5476
+ }
5477
+ this.useFlatProgram();
5478
+ }
5479
+
5480
+ private fitFlatProgram(gl: WebGL2RenderingContext): {
5481
+ program: WebGLProgram;
5482
+ source: string;
5483
+ budget: LightBudget;
5484
+ } {
5485
+ const build = (budget: LightBudget): string => this.flatFragAt(budget);
5335
5486
  const vert = flatVert({ skinned: false, morphed: false, instanced: false });
5336
5487
  const limit = gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS) as number;
5337
5488
  const ceiling: LightBudget = {
@@ -5960,7 +6111,7 @@ export class WebGL2Renderer implements RendererApi {
5960
6111
  */
5961
6112
  gl.disable(gl.CULL_FACE);
5962
6113
  gl.enable(gl.BLEND);
5963
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
6114
+ blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
5964
6115
 
5965
6116
  gl.uniform4f(this.panelUniforms['uRect'] ?? null, rect.left, rect.top, rect.width, rect.height);
5966
6117
  gl.uniform2f(
@@ -5989,6 +6140,35 @@ export class WebGL2Renderer implements RendererApi {
5989
6140
  this.restoreViewport();
5990
6141
  }
5991
6142
 
6143
+ /**
6144
+ * Draw what follows as a **view model**, in the nearest sliver of depth, until `endViewModel`.
6145
+ *
6146
+ * A first-person view model — arms and a held weapon — drawn with the frame's camera, or with one
6147
+ * of its own through `bindMeshPass`, lands in front of anything past the near plane without the
6148
+ * world's depth being cleared, so every pass that reads depth afterwards still sees the world.
6149
+ * `share` is how much of the range it takes, 1% by default. See `viewModel.ts` for what it gives
6150
+ * up, which includes temporal reconstruction: draw a view model with multisampling.
6151
+ *
6152
+ * Not inside an inset: an inset has its own depth, cleared for it, and needs no squeeze.
6153
+ */
6154
+ beginViewModel(share = VIEW_MODEL_DEPTH_SHARE): void {
6155
+ if (this.contextLost) return;
6156
+ viewModelDepthRange(this.reversedDepth, share, this.viewModelRange);
6157
+ this.gl.depthRange(this.viewModelRange[0], this.viewModelRange[1]);
6158
+ this.viewModelActive = true;
6159
+ }
6160
+
6161
+ /** Give the whole depth range back. Safe to call without a matching `beginViewModel`. */
6162
+ endViewModel(): void {
6163
+ this.viewModelActive = false;
6164
+ if (this.contextLost) return;
6165
+ this.gl.depthRange(0, 1);
6166
+ }
6167
+
6168
+ /** Whether a view model is being drawn, so `endFrame` can close one the caller left open. */
6169
+ private viewModelActive = false;
6170
+ private readonly viewModelRange: [number, number] = [0, 1];
6171
+
5992
6172
  /**
5993
6173
  * How much speed blur the frame about to be drawn should resolve with, 0 to 1.
5994
6174
  *
@@ -6508,6 +6688,10 @@ export class WebGL2Renderer implements RendererApi {
6508
6688
 
6509
6689
  beginFrame(clearColor: Vec3): void {
6510
6690
  if (this.contextLost) return;
6691
+ if (this.litAsked) {
6692
+ this.litAsked = false;
6693
+ this.rebuildLit();
6694
+ }
6511
6695
  this.budget.reset();
6512
6696
  this.materials.dirty();
6513
6697
  /*
@@ -6584,6 +6768,8 @@ export class WebGL2Renderer implements RendererApi {
6584
6768
  */
6585
6769
  endFrame(): void {
6586
6770
  if (this.contextLost) return;
6771
+ /* A view model left open would squeeze every pass after the world, the composite included. */
6772
+ if (this.viewModelActive) this.endViewModel();
6587
6773
 
6588
6774
  /*
6589
6775
  * Camera motion blur, built from the camera the frame was *rendered* with.
@@ -7240,6 +7426,8 @@ export class WebGL2Renderer implements RendererApi {
7240
7426
  gl.bindTexture(gl.TEXTURE_3D, null);
7241
7427
  this.driftIndex = index;
7242
7428
  this.driftAtlas = atlas;
7429
+ /* The first volume is when the lit programs start reading one. See `litOn`. */
7430
+ this.askLit('DRIFT_LIGHT');
7243
7431
  }
7244
7432
 
7245
7433
  /** The field's uniforms and volumes for this pass, or the stand-ins that switch it off. */
@@ -7360,7 +7548,8 @@ export class WebGL2Renderer implements RendererApi {
7360
7548
  * shape `uOpacity` and `uTint` already use.
7361
7549
  */
7362
7550
  gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
7363
- gl.uniform1i(u['uFogEnabled'] ?? null, 1);
7551
+ gl.uniform1i(u['uFogEnabled'] ?? null, FOG_RECEDE);
7552
+ this.surfaceFog = FOG_RECEDE;
7364
7553
  /* Grain on by default, so every scene written before it was a choice looks unchanged. */
7365
7554
  gl.uniform1f(u['uGrain'] ?? null, 1);
7366
7555
  /* No dither and no weighting: every draw whole, until a crossfade asks. See `setDitherFade`. */
@@ -7560,9 +7749,12 @@ export class WebGL2Renderer implements RendererApi {
7560
7749
  gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
7561
7750
  gl.uniform1i(u['uSurfaceEffects'] ?? null, SURFACE_EFFECTS_TEXTURE_UNIT);
7562
7751
  gl.uniform2f(u['uUvScale'] ?? null, 1, 1);
7752
+ gl.uniform2f(u['uUvOffset'] ?? null, 0, 0);
7563
7753
  gl.uniform1f(u['uAlbedoCutout'] ?? null, 0);
7564
7754
  gl.uniform1i(u['uDoubleSided'] ?? null, 0);
7565
7755
  this.materialDoubleSided = false;
7756
+ this.materialHasMaps = false;
7757
+ this.currentMaterial = null;
7566
7758
  this.currentSurfaceTexture = null;
7567
7759
 
7568
7760
  /*
@@ -7689,6 +7881,7 @@ export class WebGL2Renderer implements RendererApi {
7689
7881
  /* Geometry that has not all arrived is not drawn, nor a mesh with none. See `Mesh.complete`
7690
7882
  and `Mesh.indexCount`. */
7691
7883
  if (!mesh.complete || mesh.indexCount === 0) return;
7884
+ noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
7692
7885
 
7693
7886
  /*
7694
7887
  * Skipped where the flag asks and the bounds say so.
@@ -7877,6 +8070,7 @@ export class WebGL2Renderer implements RendererApi {
7877
8070
  /* Geometry that has not all arrived is not drawn, nor a mesh with none. See `Mesh.complete`
7878
8071
  and `Mesh.indexCount`. */
7879
8072
  if (!mesh.complete || mesh.indexCount === 0) return;
8073
+ noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
7880
8074
 
7881
8075
  if (opacity <= 0) return;
7882
8076
 
@@ -7908,7 +8102,7 @@ export class WebGL2Renderer implements RendererApi {
7908
8102
  const fog = fogModeOf(options.fog ?? true, options.additive === true);
7909
8103
  const toneMapped = options.toneMapped ?? true;
7910
8104
  if (!lit) gl.uniform1i(u['uLightingEnabled'] ?? null, 0);
7911
- if (fog !== FOG_RECEDE) gl.uniform1i(u['uFogEnabled'] ?? null, fog);
8105
+ if (fog !== this.surfaceFog) gl.uniform1i(u['uFogEnabled'] ?? null, fog);
7912
8106
  /* `1` is sRGB alone: the conversion without the curve, which is what dropping the tone map
7913
8107
  means rather than dropping the whole transform. `Math.min` rather than a literal, so a
7914
8108
  renderer asked for `none` stays at none and one asked for `srgb` is already there. */
@@ -7940,15 +8134,15 @@ export class WebGL2Renderer implements RendererApi {
7940
8134
  */
7941
8135
  /* Taken for itself when any option differs from the pass, and put back below: the rule both
7942
8136
  backends count material changes by. See `materialChanges.ts`. */
7943
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
8137
+ const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog);
7944
8138
  if (own) this.materials.dirty();
7945
8139
  this.takeMaterial();
7946
8140
  const ownsState = !this.oitReplaying;
7947
8141
  if (ownsState) {
7948
8142
  gl.enable(gl.BLEND);
7949
8143
  /* Added, where asked: colour times alpha onto what is there, and the target's alpha kept. */
7950
- if (options.additive === true) gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE, gl.ZERO, gl.ONE);
7951
- else gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
8144
+ if (options.additive === true) blendKeeping(gl, gl.SRC_ALPHA, gl.ONE);
8145
+ else blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
7952
8146
  }
7953
8147
  /*
7954
8148
  * The two depth options, both scoped to this draw and handed straight back — the same
@@ -7980,7 +8174,7 @@ export class WebGL2Renderer implements RendererApi {
7980
8174
  /* Put back for whatever is drawn next, exactly as `uOpacity` and `uTint` are — a pass
7981
8175
  cannot inherit a material from the draw before it. */
7982
8176
  if (!lit) gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
7983
- if (fog !== FOG_RECEDE) gl.uniform1i(u['uFogEnabled'] ?? null, FOG_RECEDE);
8177
+ if (fog !== this.surfaceFog) gl.uniform1i(u['uFogEnabled'] ?? null, this.surfaceFog);
7984
8178
  if (!toneMapped) gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
7985
8179
  if (own) this.materials.dirty();
7986
8180
  }
@@ -8408,7 +8602,7 @@ export class WebGL2Renderer implements RendererApi {
8408
8602
  }
8409
8603
 
8410
8604
  gl.enable(gl.BLEND);
8411
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
8605
+ blendKeeping(gl, gl.SRC_ALPHA, gl.ONE);
8412
8606
  gl.depthMask(false);
8413
8607
  gl.cullFace(inside ? gl.FRONT : gl.BACK);
8414
8608
  mesh.draw(gl);
@@ -8503,7 +8697,7 @@ export class WebGL2Renderer implements RendererApi {
8503
8697
  );
8504
8698
 
8505
8699
  gl.enable(gl.BLEND);
8506
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
8700
+ blendKeeping(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
8507
8701
  gl.depthMask(false);
8508
8702
  mesh.draw(gl);
8509
8703
  gl.depthMask(true);
@@ -200,6 +200,8 @@ export interface GpuMesh {
200
200
  * the key is built before the present map is consulted.
201
201
  */
202
202
  readonly isSkinned: boolean;
203
+ /** Whether it has texture coordinates, without which a material's maps read one texel. */
204
+ readonly hasUvs: boolean;
203
205
  /**
204
206
  * Whether this mesh carries the per-vertex channel, which an instanced draw cannot.
205
207
  *
@@ -425,6 +427,7 @@ export function createGpuMeshIncremental(
425
427
  progress,
426
428
  hasTangents: data.tangents !== undefined,
427
429
  isSkinned: data.joints !== undefined,
430
+ hasUvs: data.uvs !== undefined,
428
431
  skinOffsets: skinOffsetsOf(supplied),
429
432
  hasChannel: data.channel !== undefined,
430
433
  morph:
@@ -595,6 +598,7 @@ function gpuMeshOf(parts: {
595
598
  readonly progress: { readonly uploaded: boolean };
596
599
  readonly hasTangents: boolean;
597
600
  readonly isSkinned: boolean;
601
+ readonly hasUvs: boolean;
598
602
  readonly skinOffsets: GpuMesh['skinOffsets'];
599
603
  readonly hasChannel: boolean;
600
604
  readonly morph: MorphTexture | null;
@@ -614,6 +618,7 @@ function gpuMeshOf(parts: {
614
618
  },
615
619
  hasTangents: parts.hasTangents,
616
620
  isSkinned: parts.isSkinned,
621
+ hasUvs: parts.hasUvs,
617
622
  hasChannel: parts.hasChannel,
618
623
  morph: parts.morph,
619
624
  update: parts.update,
@@ -8,6 +8,7 @@ import type { UniformFields } from './scatterPass.ts';
8
8
  import { DEPTH_FORMAT } from './flatPass.ts';
9
9
  import type { PipelineCache } from './pipelineCache.ts';
10
10
  import { shaderModule } from './shaderModules.ts';
11
+ import { SCENE_ALPHA_KEEPS } from '../../sceneCoverage.ts';
11
12
 
12
13
  /**
13
14
  * The caustics pass: a surface lit by nearby water.
@@ -162,7 +163,7 @@ export function causticsPipeline(
162
163
  format: cache.format,
163
164
  blend: {
164
165
  color: { srcFactor: 'one', dstFactor: 'one', operation: 'add' },
165
- alpha: { srcFactor: 'one', dstFactor: 'one', operation: 'add' },
166
+ alpha: SCENE_ALPHA_KEEPS,
166
167
  },
167
168
  },
168
169
  ],
@@ -24,9 +24,10 @@ export const DEPTH_VERT_FIELDS = DEPTH_BINDINGS.DEPTH_VERT.fields;
24
24
  export const DEPTH_VERT_SIZE = DEPTH_BINDINGS.DEPTH_VERT.uniformSize;
25
25
  export const DEPTH_FRAG_SIZE = DEPTH_BINDINGS.DEPTH_FRAG.uniformSize;
26
26
  /*
27
- * The cutout variants' vertex blocks. The rigid one is the plain block with `uUvScale` and
28
- * `uAlphaCutout` after every other field, so a slot written for it is written with the plain
29
- * offsets plus these two; the instanced one has no model and no wind, so its two sit earlier.
27
+ * The cutout variants' vertex blocks. The rigid one is the plain block with `uUvScale`,
28
+ * `uUvOffset` and `uAlphaCutout` after every other field, so a slot written for it is written with
29
+ * the plain offsets plus these three; the instanced one has no model and no wind, so its three sit
30
+ * earlier.
30
31
  */
31
32
  export const DEPTH_CUTOUT_VERT_FIELDS = DEPTH_BINDINGS.DEPTH_CUTOUT_VERT.fields;
32
33
  export const DEPTH_CUTOUT_VERT_SIZE = DEPTH_BINDINGS.DEPTH_CUTOUT_VERT.uniformSize;
@@ -9,6 +9,7 @@ import type { UniformFields } from './scatterPass.ts';
9
9
  import { DEPTH_FORMAT } from './flatPass.ts';
10
10
  import type { PipelineCache } from './pipelineCache.ts';
11
11
  import { shaderModule } from './shaderModules.ts';
12
+ import { SCENE_ALPHA_KEEPS } from '../../sceneCoverage.ts';
12
13
 
13
14
  /**
14
15
  * The film pass: a thin wet layer — an oil slick, a puddle — lying over the world.
@@ -111,7 +112,7 @@ export function filmPipeline(
111
112
  format: cache.format,
112
113
  blend: {
113
114
  color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha', operation: 'add' },
114
- alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
115
+ alpha: SCENE_ALPHA_KEEPS,
115
116
  },
116
117
  },
117
118
  ],