@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
@@ -254,6 +254,7 @@ import {
254
254
  DECAL_TEXTURES,
255
255
  DECAL_UNIFORMS,
256
256
  PREMULTIPLIED_OVER_BLEND,
257
+ REFLECTION_OVER_BLEND,
257
258
  SSR_FRAG_FIELDS,
258
259
  SSR_FRAG_SIZE,
259
260
  SSR_RESOLVE_FRAG_WGSL,
@@ -337,6 +338,8 @@ import {
337
338
  import type { GlobalMediumOptions } from '../../globalMedium.ts';
338
339
  import type { MeshData } from '../../mesh.ts';
339
340
  import { castsDepth, glassOf } from '../../shadowCasters.ts';
341
+ import { materialHasMaps, noteMapsWithoutUvs } from '../../mapsWithoutUvs.ts';
342
+ import { VIEW_MODEL_DEPTH_SHARE, viewModelDepthRange } from '../../viewModel.ts';
340
343
  import { GlassCasterList, type GlassReplaySink } from '../../glassCasters.ts';
341
344
  import { GpuGlassTint } from './glassTintPass.ts';
342
345
  import {
@@ -379,7 +382,7 @@ import {
379
382
  createDepthBindGroupLayout,
380
383
  depthPipeline,
381
384
  } from './depthPass.ts';
382
- import { PipelineCache } from './pipelineCache.ts';
385
+ import { PipelineCache, type LitSwitch } from './pipelineCache.ts';
383
386
  import {
384
387
  SKY_FIELDS,
385
388
  SKY_UNIFORM_SIZE,
@@ -490,7 +493,7 @@ import {
490
493
  import { aimReflection, createGpuReflection, type GpuReflection } from './reflectionPass.ts';
491
494
  import { FrameBudget } from '../budget.ts';
492
495
  import { MaterialChanges, ownsMaterial } from '../materialChanges.ts';
493
- import { FOG_RECEDE, fogModeOf } from '../drawFog.ts';
496
+ import { FOG_OFF, FOG_RECEDE, fogModeOf } from '../drawFog.ts';
494
497
  import { DYNAMIC_ALIGNMENT as DYNAMIC_UNIFORM_ALIGNMENT, UniformRing } from './uniformRing.ts';
495
498
  import { DecalQueue, MAX_DRAWN_DECALS } from '../../decalQueue.ts';
496
499
  import { DistanceFieldScene } from '../../gi/fieldScene.ts';
@@ -2803,8 +2806,17 @@ export class WebGPURenderer implements RendererApi {
2803
2806
  private blankAlbedoBindGroup: GPUBindGroup;
2804
2807
  /** Repeats across the mesh's own UV range. Per draw, because `uUvScale` is a vertex uniform. */
2805
2808
  private readonly uvScale = new Float32Array([1, 1]);
2809
+ /** Where the texture starts, after the scale. Per draw, beside `uvScale`. */
2810
+ private readonly uvOffset = new Float32Array([0, 0]);
2811
+ /**
2812
+ * How the surfaces drawn next meet the medium: what `setSurfaceFog` set, and `FOG_RECEDE` from
2813
+ * each `bindMeshPass`. A translucent draw's own fog is measured against this and puts it back.
2814
+ */
2815
+ private surfaceFog = FOG_RECEDE;
2806
2816
  /** Whether the bound material is seen from both faces. See `SurfaceMaterial.doubleSided`. */
2807
2817
  private materialDoubleSided = false;
2818
+ /** Whether the bound material has a map, for the warning a mesh with no coordinates earns. */
2819
+ private materialHasMaps = false;
2808
2820
 
2809
2821
  createSurfaceTexture(
2810
2822
  source: TexImageSource,
@@ -2936,7 +2948,10 @@ export class WebGPURenderer implements RendererApi {
2936
2948
  const albedo = material?.albedo ?? null;
2937
2949
  this.uvScale[0] = material?.uScale ?? 1;
2938
2950
  this.uvScale[1] = material?.vScale ?? 1;
2951
+ this.uvOffset[0] = material?.uOffset ?? 0;
2952
+ this.uvOffset[1] = material?.vOffset ?? 0;
2939
2953
  this.materialDoubleSided = material?.doubleSided === true;
2954
+ this.materialHasMaps = materialHasMaps(material);
2940
2955
  const normal = (material?.normal ?? null) as GpuSurfaceTexture | null;
2941
2956
  const orm = (material?.orm ?? null) as GpuSurfaceTexture | null;
2942
2957
  const emissiveMap = (material?.emissive ?? null) as GpuSurfaceTexture | null;
@@ -3099,6 +3114,14 @@ export class WebGPURenderer implements RendererApi {
3099
3114
  });
3100
3115
  }
3101
3116
 
3117
+ /** Whether the surfaces drawn next meet the medium. See `renderer.ts`. */
3118
+ setSurfaceFog(enabled: boolean): void {
3119
+ this.surfaceFog = enabled ? FOG_RECEDE : FOG_OFF;
3120
+ this.material((_f, i) => {
3121
+ i[this.materialField('uFogEnabled')] = this.surfaceFog;
3122
+ });
3123
+ }
3124
+
3102
3125
  /**
3103
3126
  * How much of the surfaces drawn next survives a screen-door dither. See `renderer.ts`.
3104
3127
  *
@@ -5387,17 +5410,20 @@ export class WebGPURenderer implements RendererApi {
5387
5410
  return group;
5388
5411
  }
5389
5412
 
5390
- /** The cutoff and UV scale into a cutout draw's slot, at the offsets its own variant declares. */
5413
+ /** The cutoff and UV scale and offset into a cutout draw's slot, at its own variant's offsets. */
5391
5414
  private writeCutout(
5392
5415
  slot: number,
5393
5416
  cutout: CutoutCaster<unknown>,
5394
5417
  fields: {
5395
5418
  readonly uUvScale: { readonly offset: number };
5419
+ readonly uUvOffset: { readonly offset: number };
5396
5420
  readonly uAlphaCutout: { readonly offset: number };
5397
5421
  },
5398
5422
  ): void {
5399
5423
  this.shadowDraws.writeFloat(slot, fields.uUvScale.offset, cutout.u);
5400
5424
  this.shadowDraws.writeFloat(slot, fields.uUvScale.offset + 4, cutout.v);
5425
+ this.shadowDraws.writeFloat(slot, fields.uUvOffset.offset, cutout.uOffset);
5426
+ this.shadowDraws.writeFloat(slot, fields.uUvOffset.offset + 4, cutout.vOffset);
5401
5427
  this.shadowDraws.writeFloat(slot, fields.uAlphaCutout.offset, cutout.cutoff);
5402
5428
  }
5403
5429
 
@@ -5450,6 +5476,27 @@ export class WebGPURenderer implements RendererApi {
5450
5476
  */
5451
5477
  /** The glass the open shadow pass was offered, kept for its own passes: `glassCasters.ts`. */
5452
5478
  private readonly glassCasters = new GlassCasterList();
5479
+ /** Whether a glass caster has ever been offered, which is when lit pipelines start reading it. */
5480
+ private glassSeen = false;
5481
+
5482
+ /**
5483
+ * The first glass caster switches every lit pipeline to reading glass. See
5484
+ * `PipelineCache.litSwitches`: until a pane exists the lookups would find nothing, and they cost
5485
+ * the lit pass on every surface whether they find anything or not.
5486
+ */
5487
+ private noteGlassCaster(): void {
5488
+ if (this.glassSeen) return;
5489
+ this.glassSeen = true;
5490
+ this.enableLit('GLASS_SHADOWS');
5491
+ }
5492
+
5493
+ /** One of the lit stage's switches on, in every cache a lit pipeline can come from. */
5494
+ private enableLit(feature: LitSwitch): void {
5495
+ if (this.pipelines.litSwitches[feature]) return;
5496
+ void this.pipelines.enable(feature);
5497
+ if (this.overlayPipelines !== this.pipelines) void this.overlayPipelines.enable(feature);
5498
+ if (this.latePipelines !== this.pipelines) void this.latePipelines.enable(feature);
5499
+ }
5453
5500
  /** One resolution every glass caster is read through, so recording one allocates nothing. */
5454
5501
  private readonly glassCasterScratch: ResolvedGlass = {
5455
5502
  transmission: 0,
@@ -5462,6 +5509,7 @@ export class WebGPURenderer implements RendererApi {
5462
5509
  /* Glass: kept aside for its own two passes rather than dropped. See `glassCasters.ts`. */
5463
5510
  if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
5464
5511
  this.glassCasters.recordMesh(mesh, model, material, this.glassCasterScratch);
5512
+ this.noteGlassCaster();
5465
5513
  }
5466
5514
  return;
5467
5515
  }
@@ -5517,6 +5565,7 @@ export class WebGPURenderer implements RendererApi {
5517
5565
  if (!this.drawingGlassDepth && !castsDepth(material)) {
5518
5566
  if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
5519
5567
  this.glassCasters.recordInstanced(batch, data, material, this.glassCasterScratch);
5568
+ this.noteGlassCaster();
5520
5569
  }
5521
5570
  return;
5522
5571
  }
@@ -5572,6 +5621,7 @@ export class WebGPURenderer implements RendererApi {
5572
5621
  if (!this.drawingGlassDepth && !castsDepth(material)) {
5573
5622
  if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
5574
5623
  this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
5624
+ this.noteGlassCaster();
5575
5625
  }
5576
5626
  return;
5577
5627
  }
@@ -5968,6 +6018,7 @@ export class WebGPURenderer implements RendererApi {
5968
6018
  quality.screenEffects ? sceneColorFormat(quality.hdrScene) : surface.format,
5969
6019
  this.samples,
5970
6020
  glassShadows,
6021
+ quality.clusteredLights,
5971
6022
  );
5972
6023
  /*
5973
6024
  * The canvas' own state, for the pass `ensurePass` reopens after `endFrame`.
@@ -5980,11 +6031,11 @@ export class WebGPURenderer implements RendererApi {
5980
6031
  this.overlayPipelines =
5981
6032
  this.pipelines.format === surface.format && this.samples === 1
5982
6033
  ? this.pipelines
5983
- : new PipelineCache(device, surface.format, 1, glassShadows);
6034
+ : new PipelineCache(device, surface.format, 1, glassShadows, quality.clusteredLights);
5984
6035
  this.latePipelines =
5985
6036
  this.pipelines.format === RECON_HISTORY_FORMAT && this.samples === 1
5986
6037
  ? this.pipelines
5987
- : new PipelineCache(device, RECON_HISTORY_FORMAT, 1, glassShadows);
6038
+ : new PipelineCache(device, RECON_HISTORY_FORMAT, 1, glassShadows, quality.clusteredLights);
5988
6039
  this.issueBound = this.issueCommand.bind(this);
5989
6040
  this.pipelineTargets =
5990
6041
  this.overlayPipelines === this.pipelines
@@ -6288,7 +6339,10 @@ export class WebGPURenderer implements RendererApi {
6288
6339
  /* The albedo's effects table, or a one-texel stand-in the shader's size test reads as none.
6289
6340
  The albedo is the group's key, so its table needs no key of its own. */
6290
6341
  if (name === 'uSurfaceEffects') {
6291
- return { view: this.albedo?.effectsView ?? blankView, sampler: blankSampler };
6342
+ const effects = this.albedo?.effectsView ?? null;
6343
+ /* The first table bound is when the lit pipelines start reading one. See `litSwitches`. */
6344
+ if (effects !== null) this.enableLit('SURFACE_EFFECTS');
6345
+ return { view: effects ?? blankView, sampler: blankSampler };
6292
6346
  }
6293
6347
  /*
6294
6348
  * The room, once a bake has filled it. Unbaked reads as the blank cube: nothing to mirror.
@@ -7750,8 +7804,10 @@ export class WebGPURenderer implements RendererApi {
7750
7804
  * these per draw through the material block, the same shape `uOpacity` already uses.
7751
7805
  */
7752
7806
  i[at('uLightingEnabled')] = 1;
7753
- i[at('uFogEnabled')] = 1;
7807
+ i[at('uFogEnabled')] = FOG_RECEDE;
7808
+ this.surfaceFog = FOG_RECEDE;
7754
7809
  i[at('uAlbedoEnabled')] = 0;
7810
+ this.materialHasMaps = false;
7755
7811
 
7756
7812
  /*
7757
7813
  * **The camera medium, which this pass did not upload until 2026-08-29.**
@@ -8201,6 +8257,8 @@ export class WebGPURenderer implements RendererApi {
8201
8257
  this.bindGroup = this.blankAlbedoBindGroup;
8202
8258
  this.uvScale[0] = 1;
8203
8259
  this.uvScale[1] = 1;
8260
+ this.uvOffset[0] = 0;
8261
+ this.uvOffset[1] = 0;
8204
8262
  }
8205
8263
 
8206
8264
  /**
@@ -8284,7 +8342,11 @@ export class WebGPURenderer implements RendererApi {
8284
8342
  * where it happened to pass, write motion for a surface no pixel of the frame belongs to.
8285
8343
  */
8286
8344
  if (this.tracksMotionNow()) this.recordDrawMotion(mesh, model, previousModel);
8287
- this.submitMesh(mesh, model, tint, 1, false, { depthLayer });
8345
+ noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
8346
+ /* An opaque draw meets the medium as the dial says: `submitMesh` reads an absent `fog` as
8347
+ true, which is a translucent draw's default and would put every opaque draw back in the fog
8348
+ `setSurfaceFog(false)` took it out of. */
8349
+ this.submitMesh(mesh, model, tint, 1, false, { depthLayer, fog: this.surfaceFog !== FOG_OFF });
8288
8350
  }
8289
8351
 
8290
8352
  /**
@@ -8389,6 +8451,7 @@ export class WebGPURenderer implements RendererApi {
8389
8451
  return;
8390
8452
  }
8391
8453
 
8454
+ noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
8392
8455
  /* The tint travels in the options here, where `drawMesh` takes it positionally — see
8393
8456
  `TranslucentMeshOptions.tint` for why, and for the fade it was added for. */
8394
8457
  this.submitMesh(
@@ -8465,6 +8528,7 @@ export class WebGPURenderer implements RendererApi {
8465
8528
  /* The material's repeats, not a constant: geometry authored in metres is textured at
8466
8529
  whatever density the material asks for without rebuilding it. */
8467
8530
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
8531
+ this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvOffset.offset, this.uvOffset);
8468
8532
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
8469
8533
  /*
8470
8534
  * Whether this mesh's tangent frame is real. WebGPU has no disabled attribute, so location 10
@@ -8498,7 +8562,7 @@ export class WebGPURenderer implements RendererApi {
8498
8562
  /* Added light fades in the medium rather than receding into it. See `drawFog.ts`. */
8499
8563
  const fog = fogModeOf(options.fog ?? true, blend && options.additive === true);
8500
8564
  const toneMapped = options.toneMapped ?? true;
8501
- const unlitOrUnfogged = !lit || fog !== FOG_RECEDE;
8565
+ const unlitOrUnfogged = !lit || fog !== this.surfaceFog;
8502
8566
  if (unlitOrUnfogged) {
8503
8567
  if (!lit) this.perFrameInts[this.materialField('uLightingEnabled')] = 0;
8504
8568
  this.perFrameInts[this.materialField('uFogEnabled')] = fog;
@@ -8520,7 +8584,7 @@ export class WebGPURenderer implements RendererApi {
8520
8584
  const refracting = this.bindSeeThrough(options);
8521
8585
  /* Taken for itself when any of those differs from the pass, and put back below: the rule both
8522
8586
  backends count material changes by. See `materialChanges.ts`. */
8523
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
8587
+ const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog);
8524
8588
  if (own) this.materials.dirty();
8525
8589
  const base = (mesh as GpuMesh & { key?: string }).key ?? 'flat:s0:u0';
8526
8590
  /*
@@ -8664,7 +8728,7 @@ export class WebGPURenderer implements RendererApi {
8664
8728
  }
8665
8729
  if (unlitOrUnfogged) {
8666
8730
  if (!lit) this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
8667
- this.perFrameInts[this.materialField('uFogEnabled')] = FOG_RECEDE;
8731
+ this.perFrameInts[this.materialField('uFogEnabled')] = this.surfaceFog;
8668
8732
  }
8669
8733
  /* Back to off for every other draw in the frame, exactly as `uOpacity` is: this scratch is
8670
8734
  shared, so a strength left set is worn by everything drawn after it. */
@@ -8911,6 +8975,7 @@ export class WebGPURenderer implements RendererApi {
8911
8975
  lands ? this.correctedViewProj : this.viewProj,
8912
8976
  );
8913
8977
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
8978
+ this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvOffset.offset, this.uvOffset);
8914
8979
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
8915
8980
  this.perDraw.writeInt(slot, FLAT_VERT_FIELDS.uHasTangents.offset, mesh.hasTangents ? 1 : 0);
8916
8981
 
@@ -8921,7 +8986,7 @@ export class WebGPURenderer implements RendererApi {
8921
8986
  const lit = options.lit ?? true;
8922
8987
  const fog = fogModeOf(options.fog ?? true, blend && options.additive === true);
8923
8988
  const toneMapped = options.toneMapped ?? true;
8924
- const unlitOrUnfogged = !lit || fog !== FOG_RECEDE;
8989
+ const unlitOrUnfogged = !lit || fog !== this.surfaceFog;
8925
8990
  if (unlitOrUnfogged) {
8926
8991
  if (!lit) this.perFrameInts[this.materialField('uLightingEnabled')] = 0;
8927
8992
  this.perFrameInts[this.materialField('uFogEnabled')] = fog;
@@ -8934,7 +8999,7 @@ export class WebGPURenderer implements RendererApi {
8934
8999
  const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
8935
9000
  /* A blended batch shows what is behind it — refraction or glass — as a single draw does. */
8936
9001
  const refracting = blend && this.bindSeeThrough(options);
8937
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
9002
+ const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog);
8938
9003
  if (own) this.materials.dirty();
8939
9004
  const base = (mesh as GpuMesh & { key?: string }).key ?? 'flat:s0:u0';
8940
9005
  /* `|inst` in the key, for the reason every other suffix is there: an instanced pipeline binds
@@ -9016,7 +9081,7 @@ export class WebGPURenderer implements RendererApi {
9016
9081
  }
9017
9082
  if (unlitOrUnfogged) {
9018
9083
  this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
9019
- this.perFrameInts[this.materialField('uFogEnabled')] = FOG_RECEDE;
9084
+ this.perFrameInts[this.materialField('uFogEnabled')] = this.surfaceFog;
9020
9085
  }
9021
9086
  if (!toneMapped) {
9022
9087
  this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
@@ -11578,7 +11643,7 @@ export class WebGPURenderer implements RendererApi {
11578
11643
  'post.ssrResolve',
11579
11644
  SSR_RESOLVE_FRAG_WGSL,
11580
11645
  this.pipelines.format,
11581
- PREMULTIPLIED_OVER_BLEND,
11646
+ REFLECTION_OVER_BLEND,
11582
11647
  ),
11583
11648
  );
11584
11649
  resolve.setBindGroup(0, resolveGroup);
@@ -12598,6 +12663,7 @@ export class WebGPURenderer implements RendererApi {
12598
12663
  command.instances = 1;
12599
12664
  command.indirect = null;
12600
12665
  recordNode(this.arena, VERB_DRAW, reads, writes, at, 0);
12666
+ if (this.framePresented) this.queueOverlayFlush();
12601
12667
  return command;
12602
12668
  }
12603
12669
 
@@ -12613,11 +12679,7 @@ export class WebGPURenderer implements RendererApi {
12613
12679
  if (verb === VERB_PASS) {
12614
12680
  const definition = passAt(this.passes, nodeState(this.arena, at));
12615
12681
  if (definition === undefined) return;
12616
- this.passContext.pass = pass;
12617
- /* The frame's grade, so a contributed pass can obey the 2026-08-17 rule. See `PassContext`. */
12618
- this.passContext.outputTransform = this.gradeCode();
12619
- this.passContext.outputExposure = this.gradeExposure();
12620
- definition.draw(this.passContext as PassContext);
12682
+ definition.draw(this.fillPassContext(pass));
12621
12683
  return;
12622
12684
  }
12623
12685
  if (verb !== VERB_DRAW) return;
@@ -12704,6 +12766,16 @@ export class WebGPURenderer implements RendererApi {
12704
12766
  }
12705
12767
 
12706
12768
  private openPass(): GPURenderPassEncoder | null {
12769
+ const was = this.pass;
12770
+ const pass = this.openAnyPass();
12771
+ /* A pass opened while a view model is drawn starts with the whole depth range: squeeze it. */
12772
+ if (this.viewModelActive && pass !== null && pass !== was && !this.reflectionPassActive) {
12773
+ this.squeezeViewport(pass);
12774
+ }
12775
+ return pass;
12776
+ }
12777
+
12778
+ private openAnyPass(): GPURenderPassEncoder | null {
12707
12779
  /* While the replay is running this is the only pass there is. See `oitEncoder`. */
12708
12780
  if (this.oitEncoder !== null) return this.oitEncoder;
12709
12781
  /*
@@ -12797,19 +12869,31 @@ export class WebGPURenderer implements RendererApi {
12797
12869
  });
12798
12870
  this.overlayActive = true;
12799
12871
 
12800
- /*
12801
- * **Submitted on a microtask, which runs before the browser presents.** There is no second
12802
- * `endFrame` to hang this on — a consumer thinks the frame is over — and presentation happens
12803
- * when the task ends, after the microtask checkpoint. Anything that needs the pixels sooner
12804
- * flushes synchronously; `copyRegionTo` is the one that does.
12805
- */
12806
- if (!this.overlayFlushQueued) {
12807
- this.overlayFlushQueued = true;
12808
- queueMicrotask(() => this.flushOverlay());
12809
- }
12872
+ this.queueOverlayFlush();
12810
12873
  return this.pass;
12811
12874
  }
12812
12875
 
12876
+ /**
12877
+ * Submit what is drawn after the present on a microtask, which runs before the browser presents.
12878
+ *
12879
+ * There is no second `endFrame` to hang this on — a consumer thinks the frame is over — and
12880
+ * presentation happens when the task ends, after the microtask checkpoint. Anything that needs
12881
+ * the pixels sooner flushes synchronously; `copyRegionTo` is the one that does.
12882
+ *
12883
+ * **Queued wherever such work begins: where the overlay pass opens, and where a verb records.**
12884
+ * With the graph on a verb records rather than opening a pass, and until 4.8.2 only an opening
12885
+ * queued this, so `fillPanel` and a contributed pass drawn after `endFrame` waited for the next
12886
+ * `beginFrame` — whose replay drew onto a canvas texture already presented and destroyed.
12887
+ */
12888
+ private queueOverlayFlush(): void {
12889
+ if (this.overlayFlushQueued) return;
12890
+ this.overlayFlushQueued = true;
12891
+ queueMicrotask(this.flushOverlayBound);
12892
+ }
12893
+
12894
+ /** `flushOverlay` bound once, so queueing it each frame allocates nothing. */
12895
+ private readonly flushOverlayBound = (): void => this.flushOverlay();
12896
+
12813
12897
  /**
12814
12898
  * The cache whose pipelines match the pass this draw will land in.
12815
12899
  *
@@ -12873,6 +12957,8 @@ export class WebGPURenderer implements RendererApi {
12873
12957
  }
12874
12958
 
12875
12959
  endFrame(): void {
12960
+ /* A view model left open would squeeze every pass after the world, the composite included. */
12961
+ if (this.viewModelActive) this.endViewModel();
12876
12962
  this.inFrame = false;
12877
12963
  if (this.surface.lost || this.encoder === null) return;
12878
12964
  /*
@@ -13341,14 +13427,40 @@ export class WebGPURenderer implements RendererApi {
13341
13427
  outputTransform: number;
13342
13428
  outputExposure: number;
13343
13429
  jitter: Float32Array;
13430
+ format: GPUTextureFormat;
13431
+ depthFormat: GPUTextureFormat;
13432
+ samples: number;
13344
13433
  } = {
13345
13434
  backend: 'webgpu',
13346
13435
  pass: null,
13347
13436
  outputTransform: 0,
13348
13437
  outputExposure: 1,
13349
13438
  jitter: this.passJitter,
13439
+ format: 'rgba8unorm',
13440
+ depthFormat: DEPTH_FORMAT,
13441
+ samples: 1,
13350
13442
  };
13351
13443
 
13444
+ /**
13445
+ * Fill the context a contributed pass draws with, for the pass it is about to draw into.
13446
+ *
13447
+ * **The attachments as well as the encoder**: a pass drawn after `endFrame` lands on the canvas
13448
+ * at one sample where the frame's own is the scene target at the scene's samples, and a pipeline
13449
+ * built for the one is refused by the other. The cache a mesh drawn here would use is the answer,
13450
+ * so the two cannot disagree.
13451
+ */
13452
+ private fillPassContext(pass: GPURenderPassEncoder): PassContext {
13453
+ const target = this.targetPipelines();
13454
+ const context = this.passContext;
13455
+ context.pass = pass;
13456
+ /* The frame's grade, so a contributed pass can obey the 2026-08-17 rule. See `PassContext`. */
13457
+ context.outputTransform = this.gradeCode();
13458
+ context.outputExposure = this.gradeExposure();
13459
+ context.format = target.format;
13460
+ context.samples = target.sampleCount;
13461
+ return context as PassContext;
13462
+ }
13463
+
13352
13464
  /**
13353
13465
  * Let something outside this file draw into the frame. See `Renderer.registerPass`.
13354
13466
  *
@@ -13502,11 +13614,7 @@ export class WebGPURenderer implements RendererApi {
13502
13614
  if (!this.quality.frameGraph) {
13503
13615
  const pass = this.openPass();
13504
13616
  if (pass === null) return;
13505
- this.passContext.pass = pass;
13506
- /* The frame's grade, so a contributed pass can obey the 2026-08-17 rule. See `PassContext`. */
13507
- this.passContext.outputTransform = this.gradeCode();
13508
- this.passContext.outputExposure = this.gradeExposure();
13509
- definition.draw(this.passContext as PassContext);
13617
+ definition.draw(this.fillPassContext(pass));
13510
13618
  return;
13511
13619
  }
13512
13620
  recordNode(
@@ -13517,6 +13625,7 @@ export class WebGPURenderer implements RendererApi {
13517
13625
  handle,
13518
13626
  0,
13519
13627
  );
13628
+ if (this.framePresented) this.queueOverlayFlush();
13520
13629
  }
13521
13630
 
13522
13631
  /** Let go of a pass. A handle kept past this draws nothing; see `PassHandle`. */
@@ -13728,7 +13837,10 @@ export class WebGPURenderer implements RendererApi {
13728
13837
  * **The clear is a drawn quad rather than a clear**, because WebGPU clears whole attachments
13729
13838
  * and cannot confine one to a scissor. See `insetPass.ts`.
13730
13839
  *
13731
- * Returns the drawing-buffer height of the inset, as the other backend does.
13840
+ * Returns the **aspect** of the inset, width over height in the pixels its viewport got, as the
13841
+ * other backend does — for `camera.updateMatrices`. With no pass to draw into, a lost device,
13842
+ * it is the aspect of the box asked for: there are no pixels to measure, and a zero would be a
13843
+ * projection of infinities in the caller's camera.
13732
13844
  */
13733
13845
  beginInset(rect: InsetRect, clearColor: Vec3 | null): number {
13734
13846
  /*
@@ -13738,7 +13850,8 @@ export class WebGPURenderer implements RendererApi {
13738
13850
  */
13739
13851
  if (this.quality.frameGraph) this.flushGraph();
13740
13852
  const pass = this.openPass();
13741
- if (this.surface.lost || pass === null) return 0;
13853
+ if (this.surface.lost || pass === null)
13854
+ return Math.max(rect.width, 1) / Math.max(rect.height, 1);
13742
13855
  const canvas = this.surface.context.canvas as HTMLCanvasElement;
13743
13856
  const cssWidth = Math.max(canvas.clientWidth, 1);
13744
13857
  const cssHeight = Math.max(canvas.clientHeight, 1);
@@ -13843,6 +13956,43 @@ export class WebGPURenderer implements RendererApi {
13843
13956
  return w / h;
13844
13957
  }
13845
13958
 
13959
+ /**
13960
+ * Draw what follows as a **view model**, in the nearest sliver of depth, until `endViewModel`.
13961
+ *
13962
+ * The twin of the WebGL2 method, which carries the reasons. Here the squeeze is the viewport's
13963
+ * depth bounds, which are pass state applied when a draw is issued, so both ends flush the graph
13964
+ * as an inset's do, and a pass opened in between is given the bounds again in `openPass`.
13965
+ */
13966
+ beginViewModel(share = VIEW_MODEL_DEPTH_SHARE): void {
13967
+ if (this.quality.frameGraph) this.flushGraph();
13968
+ viewModelDepthRange(this.reversedDepth, share, this.viewModelRange);
13969
+ this.viewModelActive = true;
13970
+ const pass = this.openPass();
13971
+ if (this.surface.lost || pass === null) return;
13972
+ this.squeezeViewport(pass);
13973
+ }
13974
+
13975
+ /** Give the whole depth range back. Safe to call without a matching `beginViewModel`. */
13976
+ endViewModel(): void {
13977
+ if (!this.viewModelActive) return;
13978
+ if (this.quality.frameGraph) this.flushGraph();
13979
+ this.viewModelActive = false;
13980
+ const pass = this.pass;
13981
+ if (this.surface.lost || pass === null) return;
13982
+ const [width, height] = this.passSize();
13983
+ pass.setViewport(0, 0, width, height, 0, 1);
13984
+ }
13985
+
13986
+ /** The view model's depth bounds on the whole of the pass. */
13987
+ private squeezeViewport(pass: GPURenderPassEncoder): void {
13988
+ const [width, height] = this.passSize();
13989
+ pass.setViewport(0, 0, width, height, this.viewModelRange[0], this.viewModelRange[1]);
13990
+ }
13991
+
13992
+ /** Whether a view model is being drawn: `openPass` re-applies it, `endFrame` closes it. */
13993
+ private viewModelActive = false;
13994
+ private readonly viewModelRange: [number, number] = [0, 1];
13995
+
13846
13996
  /** Give the whole canvas back. Safe to call without a matching `beginInset`. */
13847
13997
  endInset(): void {
13848
13998
  /*
@@ -15030,6 +15180,8 @@ export class WebGPURenderer implements RendererApi {
15030
15180
  this.iesRows = this.iesAtlas.height;
15031
15181
  this.iesPlanes = this.iesAtlas.planes;
15032
15182
  this.cookieTiles = this.cookieImages.length;
15183
+ /* The first profile or cookie is when the lit pipelines start reading them. See `litSwitches`. */
15184
+ if (this.iesRows > 1 || this.cookieTiles > 0) this.enableLit('LIGHT_FIXTURES');
15033
15185
  return this.fixtureView;
15034
15186
  }
15035
15187
 
@@ -15125,6 +15277,8 @@ export class WebGPURenderer implements RendererApi {
15125
15277
  );
15126
15278
  this.driftIndex = index;
15127
15279
  this.driftAtlas = atlas;
15280
+ /* The first volume is when the lit pipelines start reading one. See `litSwitches`. */
15281
+ this.enableLit('DRIFT_LIGHT');
15128
15282
  this.driftIndexView = index.createView({ dimension: '3d' });
15129
15283
  this.driftAtlasView = atlas.createView({ dimension: '3d' });
15130
15284
  this.bakeLightField = {
@@ -11,6 +11,7 @@ import type { SdfFont } from '../../sdfFont.ts';
11
11
  import { SdfTextLayout, type SdfTextStyle } from '../../sdfTextLayout.ts';
12
12
  import type { GpuSurfaceTexture } from './surfaceTexturePass.ts';
13
13
  import { shaderModule } from './shaderModules.ts';
14
+ import { SCENE_ALPHA_COVERS } from '../../sceneCoverage.ts';
14
15
 
15
16
  /**
16
17
  * The SDF text pass: a string as flat, textured quads, drawn into the scene.
@@ -213,7 +214,7 @@ export function sdfTextPipeline(
213
214
  format: cache.format,
214
215
  blend: {
215
216
  color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha', operation: 'add' },
216
- alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
217
+ alpha: SCENE_ALPHA_COVERS,
217
218
  },
218
219
  },
219
220
  ],
@@ -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_COVERS } from '../../sceneCoverage.ts';
12
13
 
13
14
  /**
14
15
  * The text pass: a string as instanced glyph cubes, drawn over the scene.
@@ -208,7 +209,7 @@ export function textPipeline(
208
209
  format: cache.format,
209
210
  blend: {
210
211
  color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha', operation: 'add' },
211
- alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
212
+ alpha: SCENE_ALPHA_COVERS,
212
213
  },
213
214
  },
214
215
  ],
@@ -9,6 +9,7 @@ import { DEPTH_FORMAT } from './flatPass.ts';
9
9
  import type { PipelineCache } from './pipelineCache.ts';
10
10
  import type { UniformFields } from './scatterPass.ts';
11
11
  import { shaderModule } from './shaderModules.ts';
12
+ import { SCENE_ALPHA_COVERS } from '../../sceneCoverage.ts';
12
13
 
13
14
  /**
14
15
  * The water pass: two sheets and a patch, displaced by a wave field in the vertex stage.
@@ -199,7 +200,7 @@ export function waterPipeline(
199
200
  format: cache.format,
200
201
  blend: {
201
202
  color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha', operation: 'add' },
202
- alpha: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha', operation: 'add' },
203
+ alpha: SCENE_ALPHA_COVERS,
203
204
  },
204
205
  },
205
206
  ],
@@ -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_COVERS } from '../../sceneCoverage.ts';
12
13
 
13
14
  /**
14
15
  * The wind-streak pass: a lattice of debris that follows the camera and wraps.
@@ -184,7 +185,7 @@ export function windStreakPipeline(
184
185
  dstFactor: 'one-minus-src-alpha',
185
186
  operation: 'add',
186
187
  },
187
- alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
188
+ alpha: SCENE_ALPHA_COVERS,
188
189
  },
189
190
  },
190
191
  ],
@@ -2,6 +2,7 @@ import { compileProgram, uniformLocations } from './shader.ts';
2
2
  import { BOLT_FRAG, BOLT_VERT } from './shaders/bolt.ts';
3
3
  import { buildSegmentQuads, expandBoltSegments } from './segmentQuads.ts';
4
4
  import type { BoltSegments } from './boltPool.ts';
5
+ import { blendKeeping } from './sceneCoverage.ts';
5
6
 
6
7
  /**
7
8
  * One draw call for a pool of electrical arcs.
@@ -86,7 +87,7 @@ export class BoltBatch {
86
87
  drawTo(gl: WebGL2RenderingContext, segmentCount: number): void {
87
88
  if (segmentCount === 0) return;
88
89
  gl.enable(gl.BLEND);
89
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
90
+ blendKeeping(gl, gl.SRC_ALPHA, gl.ONE);
90
91
  gl.depthMask(false);
91
92
  gl.disable(gl.CULL_FACE);
92
93
 
@@ -7,6 +7,7 @@ import type { Atmosphere } from './atmosphere.ts';
7
7
  import { buildSheets } from './surfaceSheet.ts';
8
8
  import type { SheetSpan } from './surfaceSheet.ts';
9
9
  import { seaStateForWind } from './seaState.ts';
10
+ import { blendKeeping } from './sceneCoverage.ts';
10
11
 
11
12
  /**
12
13
  * A surface lit by nearby water: the floor of a pool, the bottom of a flooded
@@ -149,7 +150,7 @@ export class CausticsRenderer {
149
150
  bindAtmosphere(gl, u, atmosphere, camera.position[1] ?? 0, underwaterEnabled);
150
151
 
151
152
  gl.enable(gl.BLEND);
152
- gl.blendFunc(gl.ONE, gl.ONE);
153
+ blendKeeping(gl, gl.ONE, gl.ONE);
153
154
  /*
154
155
  * No depth write, but depth *testing* stays on: the sheet sits a couple of
155
156
  * centimetres under the surface it lights, so geometry in front of it — the