@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
@@ -781,8 +781,8 @@ export declare class WebGL2Renderer implements RendererApi {
781
781
  */
782
782
  get presentedFrames(): number;
783
783
  private presentedFrameCount;
784
- private readonly flatProgram;
785
- private readonly flatUniforms;
784
+ private flatProgram;
785
+ private flatUniforms;
786
786
  /**
787
787
  * The lit fragment source every flat program is built from, and the budget it was built at.
788
788
  *
@@ -793,7 +793,7 @@ export declare class WebGL2Renderer implements RendererApi {
793
793
  * for a lazily compiled variant to end up at a different light budget from the one the renderer
794
794
  * uploads for, which is a picture nobody could explain.
795
795
  */
796
- private readonly flatFragSource;
796
+ private flatFragSource;
797
797
  /**
798
798
  * How many lights this renderer's shader has room for, after the device had its say.
799
799
  *
@@ -936,6 +936,11 @@ export declare class WebGL2Renderer implements RendererApi {
936
936
  private readonly surfaceScene;
937
937
  /** The two halves of `uWriteMode`, kept here because a `vec2` is written whole. */
938
938
  private oitWeight;
939
+ /**
940
+ * How the surfaces drawn next meet the medium: what `setSurfaceFog` set, and `FOG_RECEDE` from
941
+ * each `bindMeshPass`. A translucent draw's own fog is measured against this and puts it back.
942
+ */
943
+ private surfaceFog;
939
944
  private ditherFade;
940
945
  /** Said once, not per frame, where the context cannot give the float target the sum needs. */
941
946
  private oitRefused;
@@ -1294,6 +1299,27 @@ export declare class WebGL2Renderer implements RendererApi {
1294
1299
  * each such draw and handed back, where the other backend bakes it into a pipeline.
1295
1300
  */
1296
1301
  private materialDoubleSided;
1302
+ /** Whether the bound material has a map, for the warning a mesh with no coordinates earns. */
1303
+ private materialHasMaps;
1304
+ /** The material last set, so a lit program rebuilt for glass can be given it. */
1305
+ private currentMaterial;
1306
+ /**
1307
+ * The lit stage's switches: what the lit programs were compiled with (`litOn`), and what the
1308
+ * profile and the content have asked for since (`litWanted`).
1309
+ *
1310
+ * **Each is off until it is used** — glass until a pane is offered, a fixture until one is loaded,
1311
+ * the effects until a material carries a table, DriftLight until a volume is set — and then on for
1312
+ * good, rebuilt at the next `beginFrame`. Clustering is the profile's. The code costs the lit pass
1313
+ * registers on every surface whether it finds anything or not, and a world without the feature
1314
+ * draws the same picture without it. `PipelineCache.litSwitches` makes the same decision on the
1315
+ * other backend. The frame a feature is first used in draws without it; the rebuild is a
1316
+ * synchronous compile, and the next frame waits for it.
1317
+ */
1318
+ private readonly litOn;
1319
+ private readonly litWanted;
1320
+ private litAsked;
1321
+ /** Ask for a switch, which the next `beginFrame` compiles in. Never glass with it turned off. */
1322
+ private askLit;
1297
1323
  /**
1298
1324
  * The one sink handed to every caster enumeration.
1299
1325
  *
@@ -1845,6 +1871,22 @@ export declare class WebGL2Renderer implements RendererApi {
1845
1871
  */
1846
1872
  setEnvironmentAmbient(_amount: number): void;
1847
1873
  setSurfaceGrain(amount: number): void;
1874
+ /**
1875
+ * Whether the surfaces drawn next meet the medium: distance fog and the global medium's haze.
1876
+ * True is the default, and what `bindMeshPass` restores.
1877
+ *
1878
+ * **The opaque counterpart of `TranslucentMeshOptions.fog`**, which a translucent draw has had
1879
+ * all along; the shader's per-draw switch was there, and nothing reached it for an ordinary mesh.
1880
+ * What it is for is a medium that darkens one thing and not another — a floor receding into the
1881
+ * distance under figures that must keep their full value however far from the camera they stand.
1882
+ * Material state like `setSurfaceGrain`: instanced and skinned draws read it too, and a
1883
+ * translucent draw's own `fog` is measured against it and puts it back.
1884
+ *
1885
+ * **What it gives up** is the medium's honesty: a surface out of the fog is drawn as though the
1886
+ * air between it and the eye were empty, so it should be a surface the scene means to stand
1887
+ * apart, not one whose distance is the point.
1888
+ */
1889
+ setSurfaceFog(enabled: boolean): void;
1848
1890
  /**
1849
1891
  * One float of material state, into every flat program.
1850
1892
  *
@@ -1855,6 +1897,8 @@ export declare class WebGL2Renderer implements RendererApi {
1855
1897
  * reads, drew what was asked.
1856
1898
  */
1857
1899
  private writeFlatFloat;
1900
+ /** One int of material state, into every flat program, for `writeFlatFloat`'s reason. */
1901
+ private writeFlatInt;
1858
1902
  /**
1859
1903
  * How much of the surfaces drawn next survives a screen-door dither, for crossfading two levels
1860
1904
  * of detail of one thing.
@@ -2523,6 +2567,17 @@ export declare class WebGL2Renderer implements RendererApi {
2523
2567
  * rather than propagating. Only a shader that will not link at *any* budget throws, and it
2524
2568
  * throws a sentence with the device's numbers in it rather than a bare driver string.
2525
2569
  */
2570
+ /** The lit fragment source this renderer's profile compiles, at a light budget. */
2571
+ private flatFragAt;
2572
+ /**
2573
+ * Every lit program that exists, compiled again with the switches asked for since, at the budget
2574
+ * the renderer already uploads for, and given the pass and the material the old one held.
2575
+ *
2576
+ * At `beginFrame`, not where a feature was first used: a pane is offered inside a shadow pass
2577
+ * with the depth program bound, and a program swapped under it would draw the next caster with
2578
+ * the lit one.
2579
+ */
2580
+ private rebuildLit;
2526
2581
  private fitFlatProgram;
2527
2582
  /**
2528
2583
  * How many point lights this renderer's shader shades at once.
@@ -2716,6 +2771,23 @@ export declare class WebGL2Renderer implements RendererApi {
2716
2771
  fillPanel(rect: InsetRect, color: Vec3, alpha: number): void;
2717
2772
  /** Give the whole canvas back. Safe to call without a matching `beginInset`. */
2718
2773
  endInset(): void;
2774
+ /**
2775
+ * Draw what follows as a **view model**, in the nearest sliver of depth, until `endViewModel`.
2776
+ *
2777
+ * A first-person view model — arms and a held weapon — drawn with the frame's camera, or with one
2778
+ * of its own through `bindMeshPass`, lands in front of anything past the near plane without the
2779
+ * world's depth being cleared, so every pass that reads depth afterwards still sees the world.
2780
+ * `share` is how much of the range it takes, 1% by default. See `viewModel.ts` for what it gives
2781
+ * up, which includes temporal reconstruction: draw a view model with multisampling.
2782
+ *
2783
+ * Not inside an inset: an inset has its own depth, cleared for it, and needs no squeeze.
2784
+ */
2785
+ beginViewModel(share?: number): void;
2786
+ /** Give the whole depth range back. Safe to call without a matching `beginViewModel`. */
2787
+ endViewModel(): void;
2788
+ /** Whether a view model is being drawn, so `endFrame` can close one the caller left open. */
2789
+ private viewModelActive;
2790
+ private readonly viewModelRange;
2719
2791
  /**
2720
2792
  * How much speed blur the frame about to be drawn should resolve with, 0 to 1.
2721
2793
  *
@@ -1,6 +1,7 @@
1
1
  import { FrameBudget } from '../budget.js';
2
2
  import { MaterialChanges, ownsMaterial } from '../materialChanges.js';
3
- import { FOG_RECEDE, fogModeOf } from '../drawFog.js';
3
+ import { FOG_OFF, FOG_RECEDE, fogModeOf } from '../drawFog.js';
4
+ import { blendCovering, blendKeeping } from '../../sceneCoverage.js';
4
5
  import { mat4 } from 'gl-matrix';
5
6
  import { GL_DEPTH_REMAP, MAX_DEPTH_LAYER, depthClearFor, depthOffsetForLayer, GL_SHADOW_REMAP, REVERSED_DEPTH, SHADOW_DEPTH_CLEAR, glDepthFuncEqual, glShadowDepthFunc, } from '../../depthConvention.js';
6
7
  import { resolveGlass } from '../../glass.js';
@@ -93,6 +94,9 @@ import { deviceSnappedCellSize } from '../../textLayout.js';
93
94
  import { WindStreakRenderer } from '../../windStreakRenderer.js';
94
95
  import { InstancedBatch } from './instanced.js';
95
96
  import { cutoutOf } from '../../cutoutCaster.js';
97
+ import { materialHasMaps, noteMapsWithoutUvs } from '../../mapsWithoutUvs.js';
98
+ import { VIEW_MODEL_DEPTH_SHARE, viewModelDepthRange } from '../../viewModel.js';
99
+ import { noLitSwitches } from '../../shaders/flat/glassTint.js';
96
100
  /**
97
101
  * How many frames `firstFrameSettled` will ask before it stops asking.
98
102
  *
@@ -557,6 +561,11 @@ export class WebGL2Renderer {
557
561
  surfaceScene = new Float32Array(4);
558
562
  /** The two halves of `uWriteMode`, kept here because a `vec2` is written whole. */
559
563
  oitWeight = 0;
564
+ /**
565
+ * How the surfaces drawn next meet the medium: what `setSurfaceFog` set, and `FOG_RECEDE` from
566
+ * each `bindMeshPass`. A translucent draw's own fog is measured against this and puts it back.
567
+ */
568
+ surfaceFog = FOG_RECEDE;
560
569
  ditherFade = 0;
561
570
  /** Said once, not per frame, where the context cannot give the float target the sum needs. */
562
571
  oitRefused = false;
@@ -961,6 +970,34 @@ export class WebGL2Renderer {
961
970
  * each such draw and handed back, where the other backend bakes it into a pipeline.
962
971
  */
963
972
  materialDoubleSided = false;
973
+ /** Whether the bound material has a map, for the warning a mesh with no coordinates earns. */
974
+ materialHasMaps = false;
975
+ /** The material last set, so a lit program rebuilt for glass can be given it. */
976
+ currentMaterial = null;
977
+ /**
978
+ * The lit stage's switches: what the lit programs were compiled with (`litOn`), and what the
979
+ * profile and the content have asked for since (`litWanted`).
980
+ *
981
+ * **Each is off until it is used** — glass until a pane is offered, a fixture until one is loaded,
982
+ * the effects until a material carries a table, DriftLight until a volume is set — and then on for
983
+ * good, rebuilt at the next `beginFrame`. Clustering is the profile's. The code costs the lit pass
984
+ * registers on every surface whether it finds anything or not, and a world without the feature
985
+ * draws the same picture without it. `PipelineCache.litSwitches` makes the same decision on the
986
+ * other backend. The frame a feature is first used in draws without it; the rebuild is a
987
+ * synchronous compile, and the next frame waits for it.
988
+ */
989
+ litOn = noLitSwitches();
990
+ litWanted = noLitSwitches();
991
+ litAsked = false;
992
+ /** Ask for a switch, which the next `beginFrame` compiles in. Never glass with it turned off. */
993
+ askLit(feature) {
994
+ if (this.litWanted[feature])
995
+ return;
996
+ if (feature === 'GLASS_SHADOWS' && this.quality.glassShadows === 'off')
997
+ return;
998
+ this.litWanted[feature] = true;
999
+ this.litAsked = true;
1000
+ }
964
1001
  /**
965
1002
  * The one sink handed to every caster enumeration.
966
1003
  *
@@ -992,6 +1029,7 @@ export class WebGL2Renderer {
992
1029
  /* Glass: kept aside for its own two passes rather than dropped. See `glassCasters.ts`. */
993
1030
  if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
994
1031
  this.glassCasters.recordMesh(mesh, model, material, this.glassCasterScratch);
1032
+ this.askLit('GLASS_SHADOWS');
995
1033
  }
996
1034
  return;
997
1035
  }
@@ -1037,6 +1075,7 @@ export class WebGL2Renderer {
1037
1075
  if (!this.drawingGlassDepth && !castsDepth(material)) {
1038
1076
  if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
1039
1077
  this.glassCasters.recordInstanced(batch, data, material, this.glassCasterScratch);
1078
+ this.askLit('GLASS_SHADOWS');
1040
1079
  }
1041
1080
  return;
1042
1081
  }
@@ -1075,6 +1114,7 @@ export class WebGL2Renderer {
1075
1114
  if (!this.drawingGlassDepth && !castsDepth(material)) {
1076
1115
  if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
1077
1116
  this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
1117
+ this.askLit('GLASS_SHADOWS');
1078
1118
  }
1079
1119
  return;
1080
1120
  }
@@ -1135,6 +1175,7 @@ export class WebGL2Renderer {
1135
1175
  if (this.activeDepthPeel)
1136
1176
  gl.uniform1i(u['uPreviousShadowMap'] ?? null, 0);
1137
1177
  gl.uniform2f(u['uUvScale'] ?? null, cutout.u, cutout.v);
1178
+ gl.uniform2f(u['uUvOffset'] ?? null, cutout.uOffset, cutout.vOffset);
1138
1179
  gl.uniform1f(u['uAlphaCutout'] ?? null, cutout.cutoff);
1139
1180
  /* Narrowed for the reason the sink's own `mesh` gives: this renderer only hands out its own. */
1140
1181
  cutout.albedo.bind(gl, SURFACE_TEXTURE_UNIT);
@@ -1538,6 +1579,9 @@ export class WebGL2Renderer {
1538
1579
  this.iesRows = this.iesAtlas.height;
1539
1580
  this.iesPlanes = this.iesAtlas.planes;
1540
1581
  this.cookieTiles = this.cookieImages.length;
1582
+ /* The first profile or cookie is when the lit programs start reading them. See `litOn`. */
1583
+ if (this.iesRows > 1 || this.cookieTiles > 0)
1584
+ this.askLit('LIGHT_FIXTURES');
1541
1585
  gl.bindTexture(gl.TEXTURE_2D, null);
1542
1586
  gl.activeTexture(gl.TEXTURE0);
1543
1587
  }
@@ -1896,6 +1940,9 @@ export class WebGL2Renderer {
1896
1940
  * Built for the profile this renderer resolved, not for every profile at once — so a
1897
1941
  * world with shadows off compiles none of the shadow path. See `shaders/flat.ts`.
1898
1942
  */
1943
+ /* The one switch the profile decides; the rest wait for their content. See `litOn`. */
1944
+ this.litOn.CLUSTERED_LIGHTS = this.quality.clusteredLights;
1945
+ this.litWanted.CLUSTERED_LIGHTS = this.quality.clusteredLights;
1899
1946
  const fitted = this.fitFlatProgram(gl);
1900
1947
  this.flatProgram = fitted.program;
1901
1948
  this.flatFragSource = fitted.source;
@@ -2410,6 +2457,8 @@ export class WebGL2Renderer {
2410
2457
  this.materials.dirty();
2411
2458
  this.currentSurfaceTexture = material?.albedo ?? null;
2412
2459
  this.materialDoubleSided = material?.doubleSided === true;
2460
+ this.materialHasMaps = materialHasMaps(material);
2461
+ this.currentMaterial = material;
2413
2462
  /*
2414
2463
  * Written to every flat program, because material state persists across draws and a skinned
2415
2464
  * draw is entitled to the material the caller set before it. A second program holding none of
@@ -2499,6 +2548,7 @@ export class WebGL2Renderer {
2499
2548
  * how big a dome was — WebGPU writes it before its own early return and WebGL2 did not.
2500
2549
  */
2501
2550
  gl.uniform2f(u['uUvScale'] ?? null, material?.uScale ?? 1, material?.vScale ?? 1);
2551
+ gl.uniform2f(u['uUvOffset'] ?? null, material?.uOffset ?? 0, material?.vOffset ?? 0);
2502
2552
  gl.uniform1i(u['uDoubleSided'] ?? null, material?.doubleSided === true ? 1 : 0);
2503
2553
  /* The albedo's effects table, or the stand-in the shader reads as none. Before the early
2504
2554
  return, so a material with no albedo still leaves a complete texture on the unit. */
@@ -2508,6 +2558,9 @@ export class WebGL2Renderer {
2508
2558
  }
2509
2559
  else {
2510
2560
  albedo.bindEffects(gl, SURFACE_EFFECTS_TEXTURE_UNIT, this.emptyTexture2D);
2561
+ /* The first table bound is when the lit programs start reading one. See `litOn`. */
2562
+ if (albedo.hasEffects)
2563
+ this.askLit('SURFACE_EFFECTS');
2511
2564
  }
2512
2565
  gl.uniform1i(u['uSurfaceEffects'] ?? null, SURFACE_EFFECTS_TEXTURE_UNIT);
2513
2566
  if (albedo === null) {
@@ -2624,6 +2677,28 @@ export class WebGL2Renderer {
2624
2677
  this.materials.dirty();
2625
2678
  this.writeFlatFloat('uGrain', Math.min(1, Math.max(0, amount)));
2626
2679
  }
2680
+ /**
2681
+ * Whether the surfaces drawn next meet the medium: distance fog and the global medium's haze.
2682
+ * True is the default, and what `bindMeshPass` restores.
2683
+ *
2684
+ * **The opaque counterpart of `TranslucentMeshOptions.fog`**, which a translucent draw has had
2685
+ * all along; the shader's per-draw switch was there, and nothing reached it for an ordinary mesh.
2686
+ * What it is for is a medium that darkens one thing and not another — a floor receding into the
2687
+ * distance under figures that must keep their full value however far from the camera they stand.
2688
+ * Material state like `setSurfaceGrain`: instanced and skinned draws read it too, and a
2689
+ * translucent draw's own `fog` is measured against it and puts it back.
2690
+ *
2691
+ * **What it gives up** is the medium's honesty: a surface out of the fog is drawn as though the
2692
+ * air between it and the eye were empty, so it should be a surface the scene means to stand
2693
+ * apart, not one whose distance is the point.
2694
+ */
2695
+ setSurfaceFog(enabled) {
2696
+ if (this.contextLost)
2697
+ return;
2698
+ this.materials.dirty();
2699
+ this.surfaceFog = enabled ? FOG_RECEDE : FOG_OFF;
2700
+ this.writeFlatInt('uFogEnabled', this.surfaceFog);
2701
+ }
2627
2702
  /**
2628
2703
  * One float of material state, into every flat program.
2629
2704
  *
@@ -2640,6 +2715,14 @@ export class WebGL2Renderer {
2640
2715
  }
2641
2716
  this.useFlatProgram();
2642
2717
  }
2718
+ /** One int of material state, into every flat program, for `writeFlatFloat`'s reason. */
2719
+ writeFlatInt(name, value) {
2720
+ for (const target of this.flatTargets) {
2721
+ this.gl.useProgram(target.program);
2722
+ this.gl.uniform1i(target.uniforms[name] ?? null, value);
2723
+ }
2724
+ this.useFlatProgram();
2725
+ }
2643
2726
  /**
2644
2727
  * How much of the surfaces drawn next survives a screen-door dither, for crossfading two levels
2645
2728
  * of detail of one thing.
@@ -2868,7 +2951,7 @@ export class WebGL2Renderer {
2868
2951
  /* A blended batch shows what is behind it — refraction or glass — as a single draw does, and
2869
2952
  counts a material of its own for it by the rule both backends share (`materialChanges.ts`). */
2870
2953
  const refracting = blend && this.bindSeeThrough(u, options);
2871
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
2954
+ const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog);
2872
2955
  if (own)
2873
2956
  this.materials.dirty();
2874
2957
  this.takeMaterial();
@@ -2894,16 +2977,16 @@ export class WebGL2Renderer {
2894
2977
  */
2895
2978
  if (!lit)
2896
2979
  gl.uniform1i(u['uLightingEnabled'] ?? null, 0);
2897
- if (fog !== FOG_RECEDE)
2980
+ if (fog !== this.surfaceFog)
2898
2981
  gl.uniform1i(u['uFogEnabled'] ?? null, fog);
2899
2982
  if (!toneMapped)
2900
2983
  gl.uniform1i(u['uOutputTransform'] ?? null, Math.min(this.gradeCode(), 1));
2901
2984
  if (blend) {
2902
2985
  gl.enable(gl.BLEND);
2903
2986
  if (options.additive === true)
2904
- gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE, gl.ZERO, gl.ONE);
2987
+ blendKeeping(gl, gl.SRC_ALPHA, gl.ONE);
2905
2988
  else
2906
- gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
2989
+ blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
2907
2990
  if (!depthWrite)
2908
2991
  gl.depthMask(false);
2909
2992
  }
@@ -2943,8 +3026,8 @@ export class WebGL2Renderer {
2943
3026
  this.unbindSeeThrough(u);
2944
3027
  if (!lit)
2945
3028
  gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
2946
- if (fog !== FOG_RECEDE)
2947
- gl.uniform1i(u['uFogEnabled'] ?? null, FOG_RECEDE);
3029
+ if (fog !== this.surfaceFog)
3030
+ gl.uniform1i(u['uFogEnabled'] ?? null, this.surfaceFog);
2948
3031
  if (!toneMapped)
2949
3032
  gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
2950
3033
  if (own)
@@ -4023,6 +4106,7 @@ export class WebGL2Renderer {
4023
4106
  gl.uniform4fv(u['uGlassLight'] ?? null, this.glassLight);
4024
4107
  if (cutout !== null) {
4025
4108
  gl.uniform2f(u['uUvScale'] ?? null, cutout.u, cutout.v);
4109
+ gl.uniform2f(u['uUvOffset'] ?? null, cutout.uOffset, cutout.vOffset);
4026
4110
  gl.uniform1f(u['uAlphaCutout'] ?? null, cutout.cutoff);
4027
4111
  cutout.albedo.bind(gl, SURFACE_TEXTURE_UNIT);
4028
4112
  gl.uniform1i(u['uCutoutMap'] ?? null, SURFACE_TEXTURE_UNIT);
@@ -4132,16 +4216,76 @@ export class WebGL2Renderer {
4132
4216
  * rather than propagating. Only a shader that will not link at *any* budget throws, and it
4133
4217
  * throws a sentence with the device's numbers in it rather than a bare driver string.
4134
4218
  */
4135
- fitFlatProgram(gl) {
4136
- const variant = {
4219
+ /** The lit fragment source this renderer's profile compiles, at a light budget. */
4220
+ flatFragAt(budget) {
4221
+ return flatFrag({
4137
4222
  pointShadows: this.quality.pointShadows,
4138
4223
  directionalShadows: this.quality.directionalShadows,
4139
4224
  environmentProbe: this.probeCapture !== null,
4140
4225
  nightEmissive: this.quality.nightEmissive,
4141
- /* Off compiles none of the glass lookup: see `FlatShaderOptions.glassShadows`. */
4142
- glassShadows: this.quality.glassShadows !== 'off',
4226
+ /* Each off until it is used: see `litOn`. */
4227
+ glassShadows: this.litOn.GLASS_SHADOWS,
4228
+ clusteredLights: this.litOn.CLUSTERED_LIGHTS,
4229
+ lightFixtures: this.litOn.LIGHT_FIXTURES,
4230
+ surfaceEffects: this.litOn.SURFACE_EFFECTS,
4231
+ driftLight: this.litOn.DRIFT_LIGHT,
4232
+ maxLights: budget.maxLights,
4233
+ maxAreaLights: budget.maxAreaLights,
4234
+ });
4235
+ }
4236
+ /**
4237
+ * Every lit program that exists, compiled again with the switches asked for since, at the budget
4238
+ * the renderer already uploads for, and given the pass and the material the old one held.
4239
+ *
4240
+ * At `beginFrame`, not where a feature was first used: a pane is offered inside a shadow pass
4241
+ * with the depth program bound, and a program swapped under it would draw the next caster with
4242
+ * the lit one.
4243
+ */
4244
+ rebuildLit() {
4245
+ const { gl } = this;
4246
+ Object.assign(this.litOn, this.litWanted);
4247
+ this.flatFragSource = this.flatFragAt(this.lightBudget);
4248
+ const recompile = (old, skinned, morphed, instanced, label) => {
4249
+ if (old === null)
4250
+ return null;
4251
+ const program = compileProgram(gl, flatVert({ skinned, morphed, instanced }), this.flatFragSource, label);
4252
+ gl.deleteProgram(old);
4253
+ return { program, uniforms: uniformLocations(gl, program, label) };
4143
4254
  };
4144
- const build = (budget) => flatFrag({ ...variant, maxLights: budget.maxLights, maxAreaLights: budget.maxAreaLights });
4255
+ const plain = recompile(this.flatProgram, false, false, false, 'flat');
4256
+ if (plain !== null) {
4257
+ this.flatProgram = plain.program;
4258
+ this.flatUniforms = plain.uniforms;
4259
+ }
4260
+ const skinned = recompile(this.flatSkinnedProgram, true, false, false, 'flat.skinned');
4261
+ this.flatSkinnedProgram = skinned?.program ?? null;
4262
+ this.flatSkinnedUniforms = skinned?.uniforms ?? null;
4263
+ const morphed = recompile(this.flatMorphedProgram, false, true, false, 'flat.morphed');
4264
+ this.flatMorphedProgram = morphed?.program ?? null;
4265
+ this.flatMorphedUniforms = morphed?.uniforms ?? null;
4266
+ const both = recompile(this.flatBothProgram, true, true, false, 'flat.morphedskinned');
4267
+ this.flatBothProgram = both?.program ?? null;
4268
+ this.flatBothUniforms = both?.uniforms ?? null;
4269
+ const instanced = recompile(this.flatInstancedProgram, false, false, true, 'flat.instanced');
4270
+ this.flatInstancedProgram = instanced?.program ?? null;
4271
+ this.flatInstancedUniforms = instanced?.uniforms ?? null;
4272
+ this.flatTargets.length = 0;
4273
+ for (const target of [plain, skinned, morphed, both, instanced]) {
4274
+ if (target !== null)
4275
+ this.flatTargets.push(target);
4276
+ }
4277
+ const camera = this.lastPassCamera;
4278
+ const env = this.lastPassEnv;
4279
+ for (const target of this.flatTargets) {
4280
+ gl.useProgram(target.program);
4281
+ if (camera !== null && env !== null)
4282
+ this.writeMeshPassState(target.uniforms, camera, env);
4283
+ this.writeMaterialState(target.uniforms, this.currentMaterial);
4284
+ }
4285
+ this.useFlatProgram();
4286
+ }
4287
+ fitFlatProgram(gl) {
4288
+ const build = (budget) => this.flatFragAt(budget);
4145
4289
  const vert = flatVert({ skinned: false, morphed: false, instanced: false });
4146
4290
  const limit = gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS);
4147
4291
  const ceiling = {
@@ -4652,7 +4796,7 @@ export class WebGL2Renderer {
4652
4796
  */
4653
4797
  gl.disable(gl.CULL_FACE);
4654
4798
  gl.enable(gl.BLEND);
4655
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
4799
+ blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
4656
4800
  gl.uniform4f(this.panelUniforms['uRect'] ?? null, rect.left, rect.top, rect.width, rect.height);
4657
4801
  gl.uniform2f(this.panelUniforms['uViewport'] ?? null, Math.max(this.canvas.clientWidth, 1), Math.max(this.canvas.clientHeight, 1));
4658
4802
  /* Identity: this backend's clip space is the one the shader was written against. */
@@ -4673,6 +4817,34 @@ export class WebGL2Renderer {
4673
4817
  gl.disable(gl.SCISSOR_TEST);
4674
4818
  this.restoreViewport();
4675
4819
  }
4820
+ /**
4821
+ * Draw what follows as a **view model**, in the nearest sliver of depth, until `endViewModel`.
4822
+ *
4823
+ * A first-person view model — arms and a held weapon — drawn with the frame's camera, or with one
4824
+ * of its own through `bindMeshPass`, lands in front of anything past the near plane without the
4825
+ * world's depth being cleared, so every pass that reads depth afterwards still sees the world.
4826
+ * `share` is how much of the range it takes, 1% by default. See `viewModel.ts` for what it gives
4827
+ * up, which includes temporal reconstruction: draw a view model with multisampling.
4828
+ *
4829
+ * Not inside an inset: an inset has its own depth, cleared for it, and needs no squeeze.
4830
+ */
4831
+ beginViewModel(share = VIEW_MODEL_DEPTH_SHARE) {
4832
+ if (this.contextLost)
4833
+ return;
4834
+ viewModelDepthRange(this.reversedDepth, share, this.viewModelRange);
4835
+ this.gl.depthRange(this.viewModelRange[0], this.viewModelRange[1]);
4836
+ this.viewModelActive = true;
4837
+ }
4838
+ /** Give the whole depth range back. Safe to call without a matching `beginViewModel`. */
4839
+ endViewModel() {
4840
+ this.viewModelActive = false;
4841
+ if (this.contextLost)
4842
+ return;
4843
+ this.gl.depthRange(0, 1);
4844
+ }
4845
+ /** Whether a view model is being drawn, so `endFrame` can close one the caller left open. */
4846
+ viewModelActive = false;
4847
+ viewModelRange = [0, 1];
4676
4848
  /**
4677
4849
  * How much speed blur the frame about to be drawn should resolve with, 0 to 1.
4678
4850
  *
@@ -5162,6 +5334,10 @@ export class WebGL2Renderer {
5162
5334
  beginFrame(clearColor) {
5163
5335
  if (this.contextLost)
5164
5336
  return;
5337
+ if (this.litAsked) {
5338
+ this.litAsked = false;
5339
+ this.rebuildLit();
5340
+ }
5165
5341
  this.budget.reset();
5166
5342
  this.materials.dirty();
5167
5343
  /*
@@ -5235,6 +5411,9 @@ export class WebGL2Renderer {
5235
5411
  endFrame() {
5236
5412
  if (this.contextLost)
5237
5413
  return;
5414
+ /* A view model left open would squeeze every pass after the world, the composite included. */
5415
+ if (this.viewModelActive)
5416
+ this.endViewModel();
5238
5417
  /*
5239
5418
  * Camera motion blur, built from the camera the frame was *rendered* with.
5240
5419
  *
@@ -5797,6 +5976,8 @@ export class WebGL2Renderer {
5797
5976
  gl.bindTexture(gl.TEXTURE_3D, null);
5798
5977
  this.driftIndex = index;
5799
5978
  this.driftAtlas = atlas;
5979
+ /* The first volume is when the lit programs start reading one. See `litOn`. */
5980
+ this.askLit('DRIFT_LIGHT');
5800
5981
  }
5801
5982
  /** The field's uniforms and volumes for this pass, or the stand-ins that switch it off. */
5802
5983
  bindDriftLight(u, eye) {
@@ -5906,7 +6087,8 @@ export class WebGL2Renderer {
5906
6087
  * shape `uOpacity` and `uTint` already use.
5907
6088
  */
5908
6089
  gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
5909
- gl.uniform1i(u['uFogEnabled'] ?? null, 1);
6090
+ gl.uniform1i(u['uFogEnabled'] ?? null, FOG_RECEDE);
6091
+ this.surfaceFog = FOG_RECEDE;
5910
6092
  /* Grain on by default, so every scene written before it was a choice looks unchanged. */
5911
6093
  gl.uniform1f(u['uGrain'] ?? null, 1);
5912
6094
  /* No dither and no weighting: every draw whole, until a crossfade asks. See `setDitherFade`. */
@@ -6091,9 +6273,12 @@ export class WebGL2Renderer {
6091
6273
  gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
6092
6274
  gl.uniform1i(u['uSurfaceEffects'] ?? null, SURFACE_EFFECTS_TEXTURE_UNIT);
6093
6275
  gl.uniform2f(u['uUvScale'] ?? null, 1, 1);
6276
+ gl.uniform2f(u['uUvOffset'] ?? null, 0, 0);
6094
6277
  gl.uniform1f(u['uAlbedoCutout'] ?? null, 0);
6095
6278
  gl.uniform1i(u['uDoubleSided'] ?? null, 0);
6096
6279
  this.materialDoubleSided = false;
6280
+ this.materialHasMaps = false;
6281
+ this.currentMaterial = null;
6097
6282
  this.currentSurfaceTexture = null;
6098
6283
  /*
6099
6284
  * **And the normal map, which this did not reset and had to.**
@@ -6213,6 +6398,7 @@ export class WebGL2Renderer {
6213
6398
  and `Mesh.indexCount`. */
6214
6399
  if (!mesh.complete || mesh.indexCount === 0)
6215
6400
  return;
6401
+ noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
6216
6402
  /*
6217
6403
  * Skipped where the flag asks and the bounds say so.
6218
6404
  *
@@ -6405,6 +6591,7 @@ export class WebGL2Renderer {
6405
6591
  and `Mesh.indexCount`. */
6406
6592
  if (!mesh.complete || mesh.indexCount === 0)
6407
6593
  return;
6594
+ noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
6408
6595
  if (opacity <= 0)
6409
6596
  return;
6410
6597
  /*
@@ -6435,7 +6622,7 @@ export class WebGL2Renderer {
6435
6622
  const toneMapped = options.toneMapped ?? true;
6436
6623
  if (!lit)
6437
6624
  gl.uniform1i(u['uLightingEnabled'] ?? null, 0);
6438
- if (fog !== FOG_RECEDE)
6625
+ if (fog !== this.surfaceFog)
6439
6626
  gl.uniform1i(u['uFogEnabled'] ?? null, fog);
6440
6627
  /* `1` is sRGB alone: the conversion without the curve, which is what dropping the tone map
6441
6628
  means rather than dropping the whole transform. `Math.min` rather than a literal, so a
@@ -6470,7 +6657,7 @@ export class WebGL2Renderer {
6470
6657
  */
6471
6658
  /* Taken for itself when any option differs from the pass, and put back below: the rule both
6472
6659
  backends count material changes by. See `materialChanges.ts`. */
6473
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
6660
+ const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog);
6474
6661
  if (own)
6475
6662
  this.materials.dirty();
6476
6663
  this.takeMaterial();
@@ -6479,9 +6666,9 @@ export class WebGL2Renderer {
6479
6666
  gl.enable(gl.BLEND);
6480
6667
  /* Added, where asked: colour times alpha onto what is there, and the target's alpha kept. */
6481
6668
  if (options.additive === true)
6482
- gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE, gl.ZERO, gl.ONE);
6669
+ blendKeeping(gl, gl.SRC_ALPHA, gl.ONE);
6483
6670
  else
6484
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
6671
+ blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
6485
6672
  }
6486
6673
  /*
6487
6674
  * The two depth options, both scoped to this draw and handed straight back — the same
@@ -6521,8 +6708,8 @@ export class WebGL2Renderer {
6521
6708
  cannot inherit a material from the draw before it. */
6522
6709
  if (!lit)
6523
6710
  gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
6524
- if (fog !== FOG_RECEDE)
6525
- gl.uniform1i(u['uFogEnabled'] ?? null, FOG_RECEDE);
6711
+ if (fog !== this.surfaceFog)
6712
+ gl.uniform1i(u['uFogEnabled'] ?? null, this.surfaceFog);
6526
6713
  if (!toneMapped)
6527
6714
  gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
6528
6715
  if (own)
@@ -6890,7 +7077,7 @@ export class WebGL2Renderer {
6890
7077
  gl.uniform1i(u['uSceneDepth'] ?? null, 3);
6891
7078
  }
6892
7079
  gl.enable(gl.BLEND);
6893
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
7080
+ blendKeeping(gl, gl.SRC_ALPHA, gl.ONE);
6894
7081
  gl.depthMask(false);
6895
7082
  gl.cullFace(inside ? gl.FRONT : gl.BACK);
6896
7083
  mesh.draw(gl);
@@ -6958,7 +7145,7 @@ export class WebGL2Renderer {
6958
7145
  }
6959
7146
  bindAtmosphere(gl, u, env, this.reflectionPassActive ? this.reflectionAtmosphereY : (camera.position[1] ?? 0), this.quality.underwaterAtmosphere);
6960
7147
  gl.enable(gl.BLEND);
6961
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
7148
+ blendKeeping(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
6962
7149
  gl.depthMask(false);
6963
7150
  mesh.draw(gl);
6964
7151
  gl.depthMask(true);
@@ -114,6 +114,8 @@ export interface GpuMesh {
114
114
  * the key is built before the present map is consulted.
115
115
  */
116
116
  readonly isSkinned: boolean;
117
+ /** Whether it has texture coordinates, without which a material's maps read one texel. */
118
+ readonly hasUvs: boolean;
117
119
  /**
118
120
  * Whether this mesh carries the per-vertex channel, which an instanced draw cannot.
119
121
  *
@@ -257,6 +257,7 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
257
257
  progress,
258
258
  hasTangents: data.tangents !== undefined,
259
259
  isSkinned: data.joints !== undefined,
260
+ hasUvs: data.uvs !== undefined,
260
261
  skinOffsets: skinOffsetsOf(supplied),
261
262
  hasChannel: data.channel !== undefined,
262
263
  morph: data.morphTargets === undefined || data.morphTargetCount === undefined
@@ -392,6 +393,7 @@ function gpuMeshOf(parts) {
392
393
  },
393
394
  hasTangents: parts.hasTangents,
394
395
  isSkinned: parts.isSkinned,
396
+ hasUvs: parts.hasUvs,
395
397
  hasChannel: parts.hasChannel,
396
398
  morph: parts.morph,
397
399
  update: parts.update,
@@ -2,6 +2,7 @@ import { DEPTH_COMPARE } from '../../depthConvention.js';
2
2
  import { CAUSTICS_BINDINGS, CAUSTICS_FRAG_WGSL, CAUSTICS_VERT_WGSL, } from '../../shaders/generated/caustics.wgsl.js';
3
3
  import { DEPTH_FORMAT } from './flatPass.js';
4
4
  import { shaderModule } from './shaderModules.js';
5
+ import { SCENE_ALPHA_KEEPS } from '../../sceneCoverage.js';
5
6
  /**
6
7
  * The caustics pass: a surface lit by nearby water.
7
8
  *
@@ -126,7 +127,7 @@ export function causticsPipeline(cache, device, layout) {
126
127
  format: cache.format,
127
128
  blend: {
128
129
  color: { srcFactor: 'one', dstFactor: 'one', operation: 'add' },
129
- alpha: { srcFactor: 'one', dstFactor: 'one', operation: 'add' },
130
+ alpha: SCENE_ALPHA_KEEPS,
130
131
  },
131
132
  },
132
133
  ],