@driftengine/core 4.8.2 → 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 (121) hide show
  1. package/CHANGELOG.json +56 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.js +6 -0
  5. package/dist/render/backend/materialChanges.d.ts +7 -2
  6. package/dist/render/backend/materialChanges.js +8 -3
  7. package/dist/render/backend/webgl2/renderer.d.ts +23 -0
  8. package/dist/render/backend/webgl2/renderer.js +58 -17
  9. package/dist/render/backend/webgpu/causticsPass.js +2 -1
  10. package/dist/render/backend/webgpu/depthPass.d.ts +13 -3
  11. package/dist/render/backend/webgpu/depthPass.js +4 -3
  12. package/dist/render/backend/webgpu/filmPass.js +2 -1
  13. package/dist/render/backend/webgpu/flatPass.d.ts +5 -0
  14. package/dist/render/backend/webgpu/flatPass.js +2 -1
  15. package/dist/render/backend/webgpu/linePass.js +2 -1
  16. package/dist/render/backend/webgpu/panelPass.js +2 -1
  17. package/dist/render/backend/webgpu/particlePass.js +2 -1
  18. package/dist/render/backend/webgpu/plumePass.js +2 -1
  19. package/dist/render/backend/webgpu/postPass.d.ts +12 -2
  20. package/dist/render/backend/webgpu/postPass.js +19 -5
  21. package/dist/render/backend/webgpu/renderer.d.ts +10 -1
  22. package/dist/render/backend/webgpu/renderer.js +38 -12
  23. package/dist/render/backend/webgpu/sdfTextPass.js +2 -1
  24. package/dist/render/backend/webgpu/textPass.js +2 -1
  25. package/dist/render/backend/webgpu/waterPass.js +2 -1
  26. package/dist/render/backend/webgpu/windStreakPass.js +2 -1
  27. package/dist/render/boltBatch.js +2 -1
  28. package/dist/render/causticsRenderer.js +2 -1
  29. package/dist/render/cutoutCaster.d.ts +3 -1
  30. package/dist/render/cutoutCaster.js +4 -0
  31. package/dist/render/decalPass.js +2 -1
  32. package/dist/render/globalMediumPass.js +3 -2
  33. package/dist/render/lineBatch.js +5 -1
  34. package/dist/render/oitPass.js +2 -1
  35. package/dist/render/particleBatch.js +3 -2
  36. package/dist/render/particlePool.d.ts +6 -2
  37. package/dist/render/plumeRenderer.js +3 -2
  38. package/dist/render/renderQuality.d.ts +9 -2
  39. package/dist/render/sceneCoverage.d.ts +54 -0
  40. package/dist/render/sceneCoverage.js +72 -0
  41. package/dist/render/sdfTextRenderer.js +2 -1
  42. package/dist/render/shaders/depth.js +3 -2
  43. package/dist/render/shaders/flat/index.js +6 -1
  44. package/dist/render/shaders/generated/depth.wgsl.d.ts +32 -12
  45. package/dist/render/shaders/generated/depth.wgsl.js +32 -12
  46. package/dist/render/shaders/generated/flat.wgsl.d.ts +25 -0
  47. package/dist/render/shaders/generated/flat.wgsl.js +47 -22
  48. package/dist/render/shaders/generated/line.wgsl.d.ts +1 -1
  49. package/dist/render/shaders/generated/line.wgsl.js +1 -1
  50. package/dist/render/shaders/generated/particle.wgsl.d.ts +3 -3
  51. package/dist/render/shaders/generated/particle.wgsl.js +3 -3
  52. package/dist/render/shaders/generated/rush.wgsl.d.ts +1 -1
  53. package/dist/render/shaders/generated/rush.wgsl.js +1 -1
  54. package/dist/render/shaders/generated/sdfText.wgsl.d.ts +1 -1
  55. package/dist/render/shaders/generated/sdfText.wgsl.js +1 -1
  56. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
  57. package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
  58. package/dist/render/shaders/line.d.ts +1 -1
  59. package/dist/render/shaders/outputTransform.d.ts +14 -1
  60. package/dist/render/shaders/outputTransform.js +23 -1
  61. package/dist/render/shaders/particle.d.ts +3 -3
  62. package/dist/render/shaders/recon/resolve.wgsl.js +15 -1
  63. package/dist/render/shaders/rush.js +13 -2
  64. package/dist/render/shaders/sdfText.d.ts +1 -1
  65. package/dist/render/shaders/temporalResolve.js +12 -5
  66. package/dist/render/ssrPass.js +2 -1
  67. package/dist/render/surfaceTexture.d.ts +11 -0
  68. package/dist/render/textRenderer.js +2 -1
  69. package/dist/render/vertexDefaults.js +1 -0
  70. package/dist/render/waterRenderer.js +2 -1
  71. package/dist/render/windStreakRenderer.js +2 -1
  72. package/package.json +6 -6
  73. package/src/index.ts +13 -0
  74. package/src/render/backend/materialChanges.ts +8 -3
  75. package/src/render/backend/webgl2/renderer.ts +57 -16
  76. package/src/render/backend/webgpu/causticsPass.ts +2 -1
  77. package/src/render/backend/webgpu/depthPass.ts +4 -3
  78. package/src/render/backend/webgpu/filmPass.ts +2 -1
  79. package/src/render/backend/webgpu/flatPass.ts +2 -1
  80. package/src/render/backend/webgpu/linePass.ts +2 -1
  81. package/src/render/backend/webgpu/panelPass.ts +2 -1
  82. package/src/render/backend/webgpu/particlePass.ts +2 -1
  83. package/src/render/backend/webgpu/plumePass.ts +2 -1
  84. package/src/render/backend/webgpu/postPass.ts +20 -5
  85. package/src/render/backend/webgpu/renderer.ts +40 -11
  86. package/src/render/backend/webgpu/sdfTextPass.ts +2 -1
  87. package/src/render/backend/webgpu/textPass.ts +2 -1
  88. package/src/render/backend/webgpu/waterPass.ts +2 -1
  89. package/src/render/backend/webgpu/windStreakPass.ts +2 -1
  90. package/src/render/boltBatch.ts +2 -1
  91. package/src/render/causticsRenderer.ts +2 -1
  92. package/src/render/cutoutCaster.ts +15 -2
  93. package/src/render/decalPass.ts +2 -1
  94. package/src/render/globalMediumPass.ts +3 -2
  95. package/src/render/lineBatch.ts +3 -1
  96. package/src/render/oitPass.ts +2 -1
  97. package/src/render/particleBatch.ts +3 -2
  98. package/src/render/particlePool.ts +6 -2
  99. package/src/render/plumeRenderer.ts +3 -2
  100. package/src/render/renderQuality.ts +9 -2
  101. package/src/render/sceneCoverage.ts +78 -0
  102. package/src/render/sdfTextRenderer.ts +2 -1
  103. package/src/render/shaders/depth.ts +3 -2
  104. package/src/render/shaders/flat/index.ts +6 -1
  105. package/src/render/shaders/generated/depth.wgsl.ts +32 -12
  106. package/src/render/shaders/generated/flat.wgsl.ts +47 -22
  107. package/src/render/shaders/generated/line.wgsl.ts +1 -1
  108. package/src/render/shaders/generated/particle.wgsl.ts +3 -3
  109. package/src/render/shaders/generated/rush.wgsl.ts +1 -1
  110. package/src/render/shaders/generated/sdfText.wgsl.ts +1 -1
  111. package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
  112. package/src/render/shaders/outputTransform.ts +24 -1
  113. package/src/render/shaders/recon/resolve.wgsl.ts +15 -1
  114. package/src/render/shaders/rush.ts +13 -2
  115. package/src/render/shaders/temporalResolve.ts +12 -5
  116. package/src/render/ssrPass.ts +2 -1
  117. package/src/render/surfaceTexture.ts +11 -0
  118. package/src/render/textRenderer.ts +2 -1
  119. package/src/render/vertexDefaults.ts +1 -0
  120. package/src/render/waterRenderer.ts +2 -1
  121. package/src/render/windStreakRenderer.ts +2 -1
@@ -9,6 +9,7 @@ import { SSRTRACE_BINDINGS as SSR_BINDINGS, SSR_TRACE_FRAG_WGSL, } from '../../s
9
9
  import { SSRRESOLVE_BINDINGS as SSR_RESOLVE_BINDINGS, SSR_RESOLVE_FRAG_WGSL, } from '../../shaders/generated/ssrResolve.wgsl.js';
10
10
  import { GLOBALMEDIUM_BINDINGS as MEDIUM_BINDINGS, MEDIUM_FRAG_WGSL, MEDIUM_UPSAMPLE_FRAG_WGSL, } from '../../shaders/generated/globalMedium.wgsl.js';
11
11
  import { shaderModule } from './shaderModules.js';
12
+ import { SCENE_ALPHA_KEEPS, SCENE_ALPHA_TRANSMITS } from '../../sceneCoverage.js';
12
13
  /**
13
14
  * The composite: everything that happens to the frame after the world is drawn.
14
15
  *
@@ -197,7 +198,7 @@ export function createPostStageLayout(device, uniformBinding, uniformSize, textu
197
198
  */
198
199
  export const OIT_COMPOSITE_BLEND = {
199
200
  color: { srcFactor: 'one-minus-src-alpha', dstFactor: 'src-alpha', operation: 'add' },
200
- alpha: { srcFactor: 'one-minus-src-alpha', dstFactor: 'src-alpha', operation: 'add' },
201
+ alpha: SCENE_ALPHA_TRANSMITS,
201
202
  };
202
203
  /**
203
204
  * What a drawn decal does to the pixel under it: `dst * src`.
@@ -215,16 +216,18 @@ export const OIT_COMPOSITE_BLEND = {
215
216
  * exactly the integral — and the composite therefore never samples the colour it is modifying,
216
217
  * which is the read both backends are entitled to call undefined.
217
218
  *
218
- * Alpha is `(zero, one)`: the frame's own alpha is not the medium's to spend. The scene target
219
- * carries it to the canvas, and a medium that wrote there would make a foggy frame translucent.
219
+ * Alpha is the transmittance again, `share * a`: the scene's alpha is how much of the pixel is
220
+ * still the opaque surface, which is what the composite lets ambient occlusion darken, and a
221
+ * medium thick enough to hide a corner hides its occlusion too (`sceneCoverage.ts`). It reaches
222
+ * no canvas: both are opaque, and the composite writes 1.
220
223
  */
221
224
  export const MEDIUM_TRANSMIT_BLEND = {
222
225
  color: { srcFactor: 'one', dstFactor: 'src-alpha', operation: 'add' },
223
- alpha: { srcFactor: 'zero', dstFactor: 'one', operation: 'add' },
226
+ alpha: SCENE_ALPHA_TRANSMITS,
224
227
  };
225
228
  export const DECAL_MULTIPLY_BLEND = {
226
229
  color: { srcFactor: 'zero', dstFactor: 'src', operation: 'add' },
227
- alpha: { srcFactor: 'zero', dstFactor: 'src', operation: 'add' },
230
+ alpha: SCENE_ALPHA_KEEPS,
228
231
  };
229
232
  /**
230
233
  * `over`, against colour a pass has already multiplied by its own coverage.
@@ -238,6 +241,17 @@ export const PREMULTIPLIED_OVER_BLEND = {
238
241
  color: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
239
242
  alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
240
243
  };
244
+ /**
245
+ * The same `over` for the resolve that lands a reflection on the scene, keeping the scene's alpha.
246
+ *
247
+ * A reflection is a layer of the surface it is seen in, so the surface's share of the pixel is
248
+ * unchanged (`sceneCoverage.ts`). The trace keeps `PREMULTIPLIED_OVER_BLEND`, because its target is
249
+ * the reflection's own and its alpha is the coverage this resolve reads.
250
+ */
251
+ export const REFLECTION_OVER_BLEND = {
252
+ color: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
253
+ alpha: SCENE_ALPHA_KEEPS,
254
+ };
241
255
  export const ADDITIVE_BLEND = {
242
256
  color: { srcFactor: 'one', dstFactor: 'one', operation: 'add' },
243
257
  alpha: { srcFactor: 'one', dstFactor: 'one', operation: 'add' },
@@ -1113,6 +1113,13 @@ export declare class WebGPURenderer implements RendererApi {
1113
1113
  private blankAlbedoBindGroup;
1114
1114
  /** Repeats across the mesh's own UV range. Per draw, because `uUvScale` is a vertex uniform. */
1115
1115
  private readonly uvScale;
1116
+ /** Where the texture starts, after the scale. Per draw, beside `uvScale`. */
1117
+ private readonly uvOffset;
1118
+ /**
1119
+ * How the surfaces drawn next meet the medium: what `setSurfaceFog` set, and `FOG_RECEDE` from
1120
+ * each `bindMeshPass`. A translucent draw's own fog is measured against this and puts it back.
1121
+ */
1122
+ private surfaceFog;
1116
1123
  /** Whether the bound material is seen from both faces. See `SurfaceMaterial.doubleSided`. */
1117
1124
  private materialDoubleSided;
1118
1125
  /** Whether the bound material has a map, for the warning a mesh with no coordinates earns. */
@@ -1198,6 +1205,8 @@ export declare class WebGPURenderer implements RendererApi {
1198
1205
  /** How bright the environment the surfaces drawn next reflect is. See `renderer.ts`. */
1199
1206
  setEnvironmentGain(gain: number): void;
1200
1207
  setSurfaceGrain(amount: number): void;
1208
+ /** Whether the surfaces drawn next meet the medium. See `renderer.ts`. */
1209
+ setSurfaceFog(enabled: boolean): void;
1201
1210
  /**
1202
1211
  * How much of the surfaces drawn next survives a screen-door dither. See `renderer.ts`.
1203
1212
  *
@@ -1816,7 +1825,7 @@ export declare class WebGPURenderer implements RendererApi {
1816
1825
  */
1817
1826
  /** The cutout bind group for `texture` in the open layer, built the first time it casts there. */
1818
1827
  private cutoutGroup;
1819
- /** The cutoff and UV scale into a cutout draw's slot, at the offsets its own variant declares. */
1828
+ /** The cutoff and UV scale and offset into a cutout draw's slot, at its own variant's offsets. */
1820
1829
  private writeCutout;
1821
1830
  private shadowGroup;
1822
1831
  /**
@@ -61,7 +61,7 @@ import { ExposurePass } from './exposurePass.js';
61
61
  import { AO_STORE } from '../../shaders/ambientOcclusion.js';
62
62
  import { clampAutoExposure } from '../../shaders/exposure.js';
63
63
  import { clampLocalExposure } from '../../shaders/localExposure.js';
64
- import { createDepthResolveLayout, createPostStageLayout, depthResolvePipeline, postPipeline, postPipelineTargets, sceneColorFormat, AO_BLUR_FIELDS, AO_BLUR_FRAG_WGSL, AO_BLUR_SIZE, AO_BLUR_TEXTURES, AO_BLUR_UNIFORMS, AO_FRAG_FIELDS, AO_FRAG_SIZE, AO_FRAG_WGSL, AO_TEXTURES, AO_UNIFORMS, ADDITIVE_BLEND, BLOOM_DOWNSAMPLE_FIELDS, BLOOM_DOWNSAMPLE_FRAG_WGSL, BLOOM_PREFILTER_FIELDS, BLOOM_PREFILTER_FRAG_WGSL, BLOOM_STAGE_SIZE, BLOOM_TEXTURES, BLOOM_UNIFORMS, BLOOM_UPSAMPLE_FIELDS, BLOOM_UPSAMPLE_FRAG_WGSL, RESOLVED_DEPTH_FORMAT, RUSH_FRAG_FIELDS, RUSH_FRAG_SIZE, RUSH_FRAG_WGSL, RUSH_TEXTURES, RUSH_UNIFORMS, TAA_FRAG_FIELDS, TAA_FRAG_SIZE, TAA_TEXTURES, TAA_UNIFORMS, TEMPORAL_RESOLVE_FRAG_WGSL, MEDIUM_FRAG_FIELDS, MEDIUM_FRAG_SIZE, MEDIUM_FRAG_WGSL, MEDIUM_TEXTURES, MEDIUM_TRANSMIT_BLEND, MEDIUM_UNIFORMS, MEDIUM_UPSAMPLE_FIELDS, MEDIUM_UPSAMPLE_FRAG_WGSL, MEDIUM_UPSAMPLE_SIZE, MEDIUM_UPSAMPLE_TEXTURES, MEDIUM_UPSAMPLE_UNIFORMS, OIT_COMPOSITE_BLEND, OIT_RESOLVE_FRAG_WGSL, OIT_TEXTURES, DECAL_FRAG_FIELDS, DECAL_FRAG_SIZE, DECAL_MULTIPLY_BLEND, DECAL_PROJECT_FRAG_WGSL, DECAL_TEXTURES, DECAL_UNIFORMS, PREMULTIPLIED_OVER_BLEND, SSR_FRAG_FIELDS, SSR_FRAG_SIZE, SSR_RESOLVE_FRAG_WGSL, SSR_RESOLVE_TEXTURES, SSR_TEXTURES, SSR_TRACE_FRAG_WGSL, SSR_UNIFORMS, createOitResolveLayout, createSsrResolveLayout, } from './postPass.js';
64
+ import { createDepthResolveLayout, createPostStageLayout, depthResolvePipeline, postPipeline, postPipelineTargets, sceneColorFormat, AO_BLUR_FIELDS, AO_BLUR_FRAG_WGSL, AO_BLUR_SIZE, AO_BLUR_TEXTURES, AO_BLUR_UNIFORMS, AO_FRAG_FIELDS, AO_FRAG_SIZE, AO_FRAG_WGSL, AO_TEXTURES, AO_UNIFORMS, ADDITIVE_BLEND, BLOOM_DOWNSAMPLE_FIELDS, BLOOM_DOWNSAMPLE_FRAG_WGSL, BLOOM_PREFILTER_FIELDS, BLOOM_PREFILTER_FRAG_WGSL, BLOOM_STAGE_SIZE, BLOOM_TEXTURES, BLOOM_UNIFORMS, BLOOM_UPSAMPLE_FIELDS, BLOOM_UPSAMPLE_FRAG_WGSL, RESOLVED_DEPTH_FORMAT, RUSH_FRAG_FIELDS, RUSH_FRAG_SIZE, RUSH_FRAG_WGSL, RUSH_TEXTURES, RUSH_UNIFORMS, TAA_FRAG_FIELDS, TAA_FRAG_SIZE, TAA_TEXTURES, TAA_UNIFORMS, TEMPORAL_RESOLVE_FRAG_WGSL, MEDIUM_FRAG_FIELDS, MEDIUM_FRAG_SIZE, MEDIUM_FRAG_WGSL, MEDIUM_TEXTURES, MEDIUM_TRANSMIT_BLEND, MEDIUM_UNIFORMS, MEDIUM_UPSAMPLE_FIELDS, MEDIUM_UPSAMPLE_FRAG_WGSL, MEDIUM_UPSAMPLE_SIZE, MEDIUM_UPSAMPLE_TEXTURES, MEDIUM_UPSAMPLE_UNIFORMS, OIT_COMPOSITE_BLEND, OIT_RESOLVE_FRAG_WGSL, OIT_TEXTURES, DECAL_FRAG_FIELDS, DECAL_FRAG_SIZE, DECAL_MULTIPLY_BLEND, DECAL_PROJECT_FRAG_WGSL, DECAL_TEXTURES, DECAL_UNIFORMS, PREMULTIPLIED_OVER_BLEND, REFLECTION_OVER_BLEND, SSR_FRAG_FIELDS, SSR_FRAG_SIZE, SSR_RESOLVE_FRAG_WGSL, SSR_RESOLVE_TEXTURES, SSR_TEXTURES, SSR_TRACE_FRAG_WGSL, SSR_UNIFORMS, createOitResolveLayout, createSsrResolveLayout, } from './postPass.js';
65
65
  import { GRADE_PLACEHOLDER_SIZE, identityGradeLut, validateGradeLut, } from '../../colourGrade.js';
66
66
  import { FILM_LOOK_WITHOUT_COMPOSITE, clampGrain, clampVignette, grainSeed, } from '../../shaders/filmLook.js';
67
67
  import { createProbeMipBindGroupLayout, createProbePrefilterBindGroupLayout, PREFILTER_UNIFORM_OFFSETS, probeLevels, probeMipPipeline, probePrefilterPipeline, PROBE_FACES, PROBE_FACE_BASIS, PROBE_FAR_M, PROBE_FOV_DEG, PROBE_MIP_UNIFORM_SIZE, PROBE_NEAR_M, } from './probePass.js';
@@ -100,7 +100,7 @@ import { LIGHT_VOLUME_VERT_FIELDS, LIGHT_VOLUME_VERT_SIZE, createLightVolumeBind
100
100
  import { aimReflection, createGpuReflection } from './reflectionPass.js';
101
101
  import { FrameBudget } from '../budget.js';
102
102
  import { MaterialChanges, ownsMaterial } from '../materialChanges.js';
103
- import { FOG_RECEDE, fogModeOf } from '../drawFog.js';
103
+ import { FOG_OFF, FOG_RECEDE, fogModeOf } from '../drawFog.js';
104
104
  import { DYNAMIC_ALIGNMENT as DYNAMIC_UNIFORM_ALIGNMENT, UniformRing } from './uniformRing.js';
105
105
  import { DecalQueue, MAX_DRAWN_DECALS } from '../../decalQueue.js';
106
106
  import { DistanceFieldScene } from '../../gi/fieldScene.js';
@@ -2131,6 +2131,13 @@ export class WebGPURenderer {
2131
2131
  blankAlbedoBindGroup;
2132
2132
  /** Repeats across the mesh's own UV range. Per draw, because `uUvScale` is a vertex uniform. */
2133
2133
  uvScale = new Float32Array([1, 1]);
2134
+ /** Where the texture starts, after the scale. Per draw, beside `uvScale`. */
2135
+ uvOffset = new Float32Array([0, 0]);
2136
+ /**
2137
+ * How the surfaces drawn next meet the medium: what `setSurfaceFog` set, and `FOG_RECEDE` from
2138
+ * each `bindMeshPass`. A translucent draw's own fog is measured against this and puts it back.
2139
+ */
2140
+ surfaceFog = FOG_RECEDE;
2134
2141
  /** Whether the bound material is seen from both faces. See `SurfaceMaterial.doubleSided`. */
2135
2142
  materialDoubleSided = false;
2136
2143
  /** Whether the bound material has a map, for the warning a mesh with no coordinates earns. */
@@ -2260,6 +2267,8 @@ export class WebGPURenderer {
2260
2267
  const albedo = material?.albedo ?? null;
2261
2268
  this.uvScale[0] = material?.uScale ?? 1;
2262
2269
  this.uvScale[1] = material?.vScale ?? 1;
2270
+ this.uvOffset[0] = material?.uOffset ?? 0;
2271
+ this.uvOffset[1] = material?.vOffset ?? 0;
2263
2272
  this.materialDoubleSided = material?.doubleSided === true;
2264
2273
  this.materialHasMaps = materialHasMaps(material);
2265
2274
  const normal = (material?.normal ?? null);
@@ -2408,6 +2417,13 @@ export class WebGPURenderer {
2408
2417
  f[this.materialField('uGrain')] = Math.min(1, Math.max(0, amount));
2409
2418
  });
2410
2419
  }
2420
+ /** Whether the surfaces drawn next meet the medium. See `renderer.ts`. */
2421
+ setSurfaceFog(enabled) {
2422
+ this.surfaceFog = enabled ? FOG_RECEDE : FOG_OFF;
2423
+ this.material((_f, i) => {
2424
+ i[this.materialField('uFogEnabled')] = this.surfaceFog;
2425
+ });
2426
+ }
2411
2427
  /**
2412
2428
  * How much of the surfaces drawn next survives a screen-door dither. See `renderer.ts`.
2413
2429
  *
@@ -4363,10 +4379,12 @@ export class WebGPURenderer {
4363
4379
  groups.set(texture, group);
4364
4380
  return group;
4365
4381
  }
4366
- /** The cutoff and UV scale into a cutout draw's slot, at the offsets its own variant declares. */
4382
+ /** The cutoff and UV scale and offset into a cutout draw's slot, at its own variant's offsets. */
4367
4383
  writeCutout(slot, cutout, fields) {
4368
4384
  this.shadowDraws.writeFloat(slot, fields.uUvScale.offset, cutout.u);
4369
4385
  this.shadowDraws.writeFloat(slot, fields.uUvScale.offset + 4, cutout.v);
4386
+ this.shadowDraws.writeFloat(slot, fields.uUvOffset.offset, cutout.uOffset);
4387
+ this.shadowDraws.writeFloat(slot, fields.uUvOffset.offset + 4, cutout.vOffset);
4370
4388
  this.shadowDraws.writeFloat(slot, fields.uAlphaCutout.offset, cutout.cutoff);
4371
4389
  }
4372
4390
  shadowGroup() {
@@ -6248,7 +6266,8 @@ export class WebGPURenderer {
6248
6266
  * these per draw through the material block, the same shape `uOpacity` already uses.
6249
6267
  */
6250
6268
  i[at('uLightingEnabled')] = 1;
6251
- i[at('uFogEnabled')] = 1;
6269
+ i[at('uFogEnabled')] = FOG_RECEDE;
6270
+ this.surfaceFog = FOG_RECEDE;
6252
6271
  i[at('uAlbedoEnabled')] = 0;
6253
6272
  this.materialHasMaps = false;
6254
6273
  /*
@@ -6666,6 +6685,8 @@ export class WebGPURenderer {
6666
6685
  this.bindGroup = this.blankAlbedoBindGroup;
6667
6686
  this.uvScale[0] = 1;
6668
6687
  this.uvScale[1] = 1;
6688
+ this.uvOffset[0] = 0;
6689
+ this.uvOffset[1] = 0;
6669
6690
  }
6670
6691
  /**
6671
6692
  * The slot the next draw's material lives in, taking a new one when something dirtied it.
@@ -6744,7 +6765,10 @@ export class WebGPURenderer {
6744
6765
  if (this.tracksMotionNow())
6745
6766
  this.recordDrawMotion(mesh, model, previousModel);
6746
6767
  noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
6747
- this.submitMesh(mesh, model, tint, 1, false, { depthLayer });
6768
+ /* An opaque draw meets the medium as the dial says: `submitMesh` reads an absent `fog` as
6769
+ true, which is a translucent draw's default and would put every opaque draw back in the fog
6770
+ `setSurfaceFog(false)` took it out of. */
6771
+ this.submitMesh(mesh, model, tint, 1, false, { depthLayer, fog: this.surfaceFog !== FOG_OFF });
6748
6772
  }
6749
6773
  /**
6750
6774
  * Draw a mesh that lets the world through, at `opacity`.
@@ -6894,6 +6918,7 @@ export class WebGPURenderer {
6894
6918
  /* The material's repeats, not a constant: geometry authored in metres is textured at
6895
6919
  whatever density the material asks for without rebuilding it. */
6896
6920
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
6921
+ this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvOffset.offset, this.uvOffset);
6897
6922
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
6898
6923
  /*
6899
6924
  * Whether this mesh's tangent frame is real. WebGPU has no disabled attribute, so location 10
@@ -6926,7 +6951,7 @@ export class WebGPURenderer {
6926
6951
  /* Added light fades in the medium rather than receding into it. See `drawFog.ts`. */
6927
6952
  const fog = fogModeOf(options.fog ?? true, blend && options.additive === true);
6928
6953
  const toneMapped = options.toneMapped ?? true;
6929
- const unlitOrUnfogged = !lit || fog !== FOG_RECEDE;
6954
+ const unlitOrUnfogged = !lit || fog !== this.surfaceFog;
6930
6955
  if (unlitOrUnfogged) {
6931
6956
  if (!lit)
6932
6957
  this.perFrameInts[this.materialField('uLightingEnabled')] = 0;
@@ -6949,7 +6974,7 @@ export class WebGPURenderer {
6949
6974
  const refracting = this.bindSeeThrough(options);
6950
6975
  /* Taken for itself when any of those differs from the pass, and put back below: the rule both
6951
6976
  backends count material changes by. See `materialChanges.ts`. */
6952
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
6977
+ const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog);
6953
6978
  if (own)
6954
6979
  this.materials.dirty();
6955
6980
  const base = mesh.key ?? 'flat:s0:u0';
@@ -7079,7 +7104,7 @@ export class WebGPURenderer {
7079
7104
  if (unlitOrUnfogged) {
7080
7105
  if (!lit)
7081
7106
  this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
7082
- this.perFrameInts[this.materialField('uFogEnabled')] = FOG_RECEDE;
7107
+ this.perFrameInts[this.materialField('uFogEnabled')] = this.surfaceFog;
7083
7108
  }
7084
7109
  /* Back to off for every other draw in the frame, exactly as `uOpacity` is: this scratch is
7085
7110
  shared, so a strength left set is worn by everything drawn after it. */
@@ -7284,6 +7309,7 @@ export class WebGPURenderer {
7284
7309
  const lands = blend && options.reconstructed !== true && this.drawsLate();
7285
7310
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uViewProj.offset, lands ? this.correctedViewProj : this.viewProj);
7286
7311
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
7312
+ this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvOffset.offset, this.uvOffset);
7287
7313
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
7288
7314
  this.perDraw.writeInt(slot, FLAT_VERT_FIELDS.uHasTangents.offset, mesh.hasTangents ? 1 : 0);
7289
7315
  const dimmed = opacity < 1;
@@ -7293,7 +7319,7 @@ export class WebGPURenderer {
7293
7319
  const lit = options.lit ?? true;
7294
7320
  const fog = fogModeOf(options.fog ?? true, blend && options.additive === true);
7295
7321
  const toneMapped = options.toneMapped ?? true;
7296
- const unlitOrUnfogged = !lit || fog !== FOG_RECEDE;
7322
+ const unlitOrUnfogged = !lit || fog !== this.surfaceFog;
7297
7323
  if (unlitOrUnfogged) {
7298
7324
  if (!lit)
7299
7325
  this.perFrameInts[this.materialField('uLightingEnabled')] = 0;
@@ -7306,7 +7332,7 @@ export class WebGPURenderer {
7306
7332
  const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
7307
7333
  /* A blended batch shows what is behind it — refraction or glass — as a single draw does. */
7308
7334
  const refracting = blend && this.bindSeeThrough(options);
7309
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
7335
+ const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog);
7310
7336
  if (own)
7311
7337
  this.materials.dirty();
7312
7338
  const base = mesh.key ?? 'flat:s0:u0';
@@ -7375,7 +7401,7 @@ export class WebGPURenderer {
7375
7401
  }
7376
7402
  if (unlitOrUnfogged) {
7377
7403
  this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
7378
- this.perFrameInts[this.materialField('uFogEnabled')] = FOG_RECEDE;
7404
+ this.perFrameInts[this.materialField('uFogEnabled')] = this.surfaceFog;
7379
7405
  }
7380
7406
  if (!toneMapped) {
7381
7407
  this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
@@ -9583,7 +9609,7 @@ export class WebGPURenderer {
9583
9609
  label: 'post.ssr.resolve',
9584
9610
  colorAttachments: [{ view: scene, loadOp: 'load', storeOp: 'store' }],
9585
9611
  });
9586
- resolve.setPipeline(postPipeline(this.pipelines, device, this.ssrResolveLayout, 'post.ssrResolve', SSR_RESOLVE_FRAG_WGSL, this.pipelines.format, PREMULTIPLIED_OVER_BLEND));
9612
+ resolve.setPipeline(postPipeline(this.pipelines, device, this.ssrResolveLayout, 'post.ssrResolve', SSR_RESOLVE_FRAG_WGSL, this.pipelines.format, REFLECTION_OVER_BLEND));
9587
9613
  resolve.setBindGroup(0, resolveGroup);
9588
9614
  resolve.draw(3);
9589
9615
  resolve.end();
@@ -3,6 +3,7 @@ import { SDFTEXT_BINDINGS, SDF_TEXT_FRAG_WGSL, SDF_TEXT_VERT_WGSL, } from '../..
3
3
  import { DEPTH_FORMAT } from './flatPass.js';
4
4
  import { SdfTextLayout } from '../../sdfTextLayout.js';
5
5
  import { shaderModule } from './shaderModules.js';
6
+ import { SCENE_ALPHA_COVERS } from '../../sceneCoverage.js';
6
7
  /**
7
8
  * The SDF text pass: a string as flat, textured quads, drawn into the scene.
8
9
  *
@@ -183,7 +184,7 @@ export function sdfTextPipeline(cache, device, layout) {
183
184
  format: cache.format,
184
185
  blend: {
185
186
  color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha', operation: 'add' },
186
- alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
187
+ alpha: SCENE_ALPHA_COVERS,
187
188
  },
188
189
  },
189
190
  ],
@@ -3,6 +3,7 @@ import { TEXT_BINDINGS, TEXT_FRAG_WGSL, TEXT_VERT_WGSL, } from '../../shaders/ge
3
3
  import { MAX_CELLS, TEXT_CUBE, TextLayout } from '../../textLayout.js';
4
4
  import { DEPTH_FORMAT } from './flatPass.js';
5
5
  import { shaderModule } from './shaderModules.js';
6
+ import { SCENE_ALPHA_COVERS } from '../../sceneCoverage.js';
6
7
  /**
7
8
  * The text pass: a string as instanced glyph cubes, drawn over the scene.
8
9
  *
@@ -174,7 +175,7 @@ export function textPipeline(cache, device, layout) {
174
175
  format: cache.format,
175
176
  blend: {
176
177
  color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha', operation: 'add' },
177
- alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
178
+ alpha: SCENE_ALPHA_COVERS,
178
179
  },
179
180
  },
180
181
  ],
@@ -3,6 +3,7 @@ import { WATER_BINDINGS, WATER_FRAG_WGSL, WATER_VERT_WGSL, } from '../../shaders
3
3
  import { PATCH_RESOLUTION, buildUnitSheet, buildWaterGrid } from '../../waterGrid.js';
4
4
  import { DEPTH_FORMAT } from './flatPass.js';
5
5
  import { shaderModule } from './shaderModules.js';
6
+ import { SCENE_ALPHA_COVERS } from '../../sceneCoverage.js';
6
7
  /**
7
8
  * The water pass: two sheets and a patch, displaced by a wave field in the vertex stage.
8
9
  *
@@ -148,7 +149,7 @@ export function waterPipeline(cache, device, layout) {
148
149
  format: cache.format,
149
150
  blend: {
150
151
  color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha', operation: 'add' },
151
- alpha: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha', operation: 'add' },
152
+ alpha: SCENE_ALPHA_COVERS,
152
153
  },
153
154
  },
154
155
  ],
@@ -3,6 +3,7 @@ import { WINDSTREAKS_BINDINGS, WIND_STREAK_FRAG_WGSL, WIND_STREAK_VERT_WGSL, } f
3
3
  import { buildWindStreakLattice } from '../../windStreakDraw.js';
4
4
  import { DEPTH_FORMAT } from './flatPass.js';
5
5
  import { shaderModule } from './shaderModules.js';
6
+ import { SCENE_ALPHA_COVERS } from '../../sceneCoverage.js';
6
7
  /**
7
8
  * The wind-streak pass: a lattice of debris that follows the camera and wraps.
8
9
  *
@@ -141,7 +142,7 @@ export function windStreakPipeline(cache, device, layout) {
141
142
  dstFactor: 'one-minus-src-alpha',
142
143
  operation: 'add',
143
144
  },
144
- alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
145
+ alpha: SCENE_ALPHA_COVERS,
145
146
  },
146
147
  },
147
148
  ],
@@ -1,6 +1,7 @@
1
1
  import { compileProgram, uniformLocations } from './shader.js';
2
2
  import { BOLT_FRAG, BOLT_VERT } from './shaders/bolt.js';
3
3
  import { buildSegmentQuads, expandBoltSegments } from './segmentQuads.js';
4
+ import { blendKeeping } from './sceneCoverage.js';
4
5
  /**
5
6
  * One draw call for a pool of electrical arcs.
6
7
  *
@@ -80,7 +81,7 @@ export class BoltBatch {
80
81
  if (segmentCount === 0)
81
82
  return;
82
83
  gl.enable(gl.BLEND);
83
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
84
+ blendKeeping(gl, gl.SRC_ALPHA, gl.ONE);
84
85
  gl.depthMask(false);
85
86
  gl.disable(gl.CULL_FACE);
86
87
  gl.bindVertexArray(this.vao);
@@ -3,6 +3,7 @@ import { CAUSTICS_FRAG, CAUSTICS_VERT } from './shaders/caustics.js';
3
3
  import { bindAtmosphere } from './atmosphere.js';
4
4
  import { buildSheets } from './surfaceSheet.js';
5
5
  import { seaStateForWind } from './seaState.js';
6
+ import { blendKeeping } from './sceneCoverage.js';
6
7
  /**
7
8
  * How far from the water the effect has died out.
8
9
  *
@@ -97,7 +98,7 @@ export class CausticsRenderer {
97
98
  gl.uniform1f(u['uWaveGain'] ?? null, sea.steepness);
98
99
  bindAtmosphere(gl, u, atmosphere, camera.position[1] ?? 0, underwaterEnabled);
99
100
  gl.enable(gl.BLEND);
100
- gl.blendFunc(gl.ONE, gl.ONE);
101
+ blendKeeping(gl, gl.ONE, gl.ONE);
101
102
  /*
102
103
  * No depth write, but depth *testing* stays on: the sheet sits a couple of
103
104
  * centimetres under the surface it lights, so geometry in front of it — the
@@ -12,12 +12,14 @@
12
12
  * reads it and binds before it asks again.
13
13
  */
14
14
  import type { SurfaceMaterial } from './surfaceTexture.ts';
15
- /** A cutout caster's texture, its cutoff and its UV scale. */
15
+ /** A cutout caster's texture, its cutoff and its UV scale and offset. */
16
16
  export interface CutoutCaster<Texture> {
17
17
  readonly albedo: Texture;
18
18
  readonly cutoff: number;
19
19
  readonly u: number;
20
20
  readonly v: number;
21
+ readonly uOffset: number;
22
+ readonly vOffset: number;
21
23
  }
22
24
  /** The cutout `material` casts, or null for an ordinary caster. */
23
25
  export declare function cutoutOf<Texture>(material: SurfaceMaterial<Texture> | null | undefined): CutoutCaster<Texture> | null;
@@ -3,6 +3,8 @@ const answer = {
3
3
  cutoff: 0,
4
4
  u: 1,
5
5
  v: 1,
6
+ uOffset: 0,
7
+ vOffset: 0,
6
8
  };
7
9
  /** The cutout `material` casts, or null for an ordinary caster. */
8
10
  export function cutoutOf(material) {
@@ -16,5 +18,7 @@ export function cutoutOf(material) {
16
18
  answer.cutoff = cutoff;
17
19
  answer.u = material.uScale ?? 1;
18
20
  answer.v = material.vScale ?? 1;
21
+ answer.uOffset = material.uOffset ?? 0;
22
+ answer.vOffset = material.vOffset ?? 0;
19
23
  return answer;
20
24
  }
@@ -4,6 +4,7 @@ import { DEPTH_01_TO_CLIP } from './lightVolumeDraw.js';
4
4
  import { compileProgram, uniformLocations } from './shader.js';
5
5
  import { DECAL_PROJECT_FRAG } from './shaders/decalProject.js';
6
6
  import { FULLSCREEN_VERT } from './shaders/fullscreen.js';
7
+ import { blendKeeping } from './sceneCoverage.js';
7
8
  /**
8
9
  * Drawn decals on WebGL2: one scissored triangle per mark, multiplied into the finished scene.
9
10
  *
@@ -54,7 +55,7 @@ export class DecalPass {
54
55
  gl.depthMask(false);
55
56
  gl.disable(gl.CULL_FACE);
56
57
  gl.enable(gl.BLEND);
57
- gl.blendFunc(gl.ZERO, gl.SRC_COLOR);
58
+ blendKeeping(gl, gl.ZERO, gl.SRC_COLOR);
58
59
  gl.enable(gl.SCISSOR_TEST);
59
60
  /* Unit 0, chosen rather than inherited: `bindTexture` binds to whichever unit is active, and
60
61
  the renderer leaves that wherever its last pass finished. `OitPass` records what leaving it
@@ -4,6 +4,7 @@ import { DEPTH_01_TO_CLIP } from './lightVolumeDraw.js';
4
4
  import { compileProgram, uniformLocations } from './shader.js';
5
5
  import { FULLSCREEN_VERT } from './shaders/fullscreen.js';
6
6
  import { MEDIUM_FRAG, MEDIUM_UPSAMPLE_FRAG } from './shaders/globalMedium.js';
7
+ import { blendCovering, blendTransmitting } from './sceneCoverage.js';
7
8
  /**
8
9
  * The global participating medium on WebGL2: a half-res march, then a depth-aware composite.
9
10
  *
@@ -200,7 +201,7 @@ export class GlobalMediumPass {
200
201
  gl.depthMask(false);
201
202
  gl.disable(gl.CULL_FACE);
202
203
  gl.enable(gl.BLEND);
203
- gl.blendFuncSeparate(gl.ONE, gl.SRC_ALPHA, gl.ZERO, gl.ONE);
204
+ blendTransmitting(gl, gl.ONE, gl.SRC_ALPHA);
204
205
  gl.useProgram(this.upsample);
205
206
  gl.bindVertexArray(this.vao);
206
207
  gl.activeTexture(gl.TEXTURE0);
@@ -220,7 +221,7 @@ export class GlobalMediumPass {
220
221
  gl.activeTexture(gl.TEXTURE0);
221
222
  gl.bindTexture(gl.TEXTURE_2D, null);
222
223
  /* Back to the blend every other pass in this renderer assumes it inherits. */
223
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
224
+ blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
224
225
  gl.disable(gl.BLEND);
225
226
  gl.depthMask(true);
226
227
  gl.enable(gl.DEPTH_TEST);
@@ -1,6 +1,7 @@
1
1
  import { compileProgram, uniformLocations } from './shader.js';
2
2
  import { LINE_FRAG, LINE_VERT } from './shaders/line.js';
3
3
  import { buildSegmentQuads, expandLineSegments } from './segmentQuads.js';
4
+ import { blendCovering, blendKeeping } from './sceneCoverage.js';
4
5
  /**
5
6
  * One draw call for a polyline with a real width — an audio waveform, a trail, a plotted
6
7
  * curve, anything that is a stroke rather than a surface.
@@ -96,7 +97,10 @@ export class LineBatch {
96
97
  if (segmentCount === 0)
97
98
  return;
98
99
  gl.enable(gl.BLEND);
99
- gl.blendFunc(gl.SRC_ALPHA, additive ? gl.ONE : gl.ONE_MINUS_SRC_ALPHA);
100
+ if (additive)
101
+ blendKeeping(gl, gl.SRC_ALPHA, gl.ONE);
102
+ else
103
+ blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
100
104
  gl.depthMask(false);
101
105
  // The quad billboards toward the view direction the same way a bolt's does — see
102
106
  // `line.ts`'s own `cross(dir, view)` — so its winding can flip with the camera exactly
@@ -1,6 +1,7 @@
1
1
  import { compileProgram, uniformLocations } from './shader.js';
2
2
  import { FULLSCREEN_VERT } from './shaders/fullscreen.js';
3
3
  import { OIT_RESOLVE_FRAG } from './shaders/oitResolve.js';
4
+ import { blendTransmitting } from './sceneCoverage.js';
4
5
  /**
5
6
  * Order-independent transparency on WebGL2: two targets, two passes over the same geometry, and a
6
7
  * resolve.
@@ -163,7 +164,7 @@ export class OitPass {
163
164
  gl.depthMask(false);
164
165
  gl.disable(gl.CULL_FACE);
165
166
  gl.enable(gl.BLEND);
166
- gl.blendFunc(gl.ONE_MINUS_SRC_ALPHA, gl.SRC_ALPHA);
167
+ blendTransmitting(gl, gl.ONE_MINUS_SRC_ALPHA, gl.SRC_ALPHA);
167
168
  gl.useProgram(this.program);
168
169
  gl.activeTexture(gl.TEXTURE0);
169
170
  gl.bindTexture(gl.TEXTURE_2D, this.accum);
@@ -1,6 +1,7 @@
1
1
  import { compileProgram, uniformLocations } from './shader.js';
2
2
  import { particleShaders } from './particleMaterial.js';
3
3
  import { PARTICLE_BLADES, PARTICLE_INDICES, PARTICLE_VERTS } from './shaders/particle.js';
4
+ import { blendCovering, blendKeeping } from './sceneCoverage.js';
4
5
  /** Corners of one blade, as a triangle-strip-friendly quad. */
5
6
  const CORNERS = [
6
7
  [-1, -1],
@@ -138,9 +139,9 @@ export class ParticleBatch {
138
139
  return;
139
140
  gl.enable(gl.BLEND);
140
141
  if (this.blend === 'additive')
141
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
142
+ blendKeeping(gl, gl.SRC_ALPHA, gl.ONE);
142
143
  else
143
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
144
+ blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
144
145
  gl.depthMask(false);
145
146
  gl.disable(gl.CULL_FACE);
146
147
  gl.bindVertexArray(this.vao);
@@ -19,7 +19,11 @@ import type { Vec3 } from '../math/color.ts';
19
19
  export interface ParticleInstances {
20
20
  /** World position, three floats each. */
21
21
  positions: Float32Array;
22
- /** Metres across, and radians of roll about the particle's own axis. */
22
+ /**
23
+ * Metres from the centre to an edge, a half-width: the quad spans twice this. Then radians of
24
+ * roll about the particle's own axis. This said metres across until 4.8.3, and a caller who took
25
+ * it at its word drew every particle twice as wide as meant.
26
+ */
23
27
  sizes: Float32Array;
24
28
  spins: Float32Array;
25
29
  /** Linear colour, three floats each. */
@@ -54,7 +58,7 @@ export interface ParticlePoolOptions {
54
58
  capacity: number;
55
59
  /** Seconds a particle lives. */
56
60
  lifeSec: number;
57
- /** Size at birth and at death, metres. */
61
+ /** Size at birth and at death: metres from the centre to an edge, so the quad is twice as wide. */
58
62
  sizeStart: number;
59
63
  sizeEnd: number;
60
64
  /** Colour at birth and at death. */
@@ -3,6 +3,7 @@ import { compileProgram, uniformLocations } from './shader.js';
3
3
  import { bindAtmosphere } from './atmosphere.js';
4
4
  import { plumeShaders } from './plumeMaterial.js';
5
5
  import { buildPlumeGeometry } from './plumeGeometry.js';
6
+ import { blendCovering, blendKeeping } from './sceneCoverage.js';
6
7
  /* The shared count, so both backends step a plume's vertices the same way. */
7
8
  const VERTS_PER_PLUME = PLUME_VERTS;
8
9
  export class PlumeRenderer {
@@ -123,9 +124,9 @@ export class PlumeRenderer {
123
124
  bindAtmosphere(gl, u, atmosphere, atmosphereCameraY, underwaterEnabled);
124
125
  gl.enable(gl.BLEND);
125
126
  if (this.blend === 'additive')
126
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
127
+ blendKeeping(gl, gl.SRC_ALPHA, gl.ONE);
127
128
  else
128
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
129
+ blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
129
130
  gl.depthMask(false);
130
131
  gl.disable(gl.CULL_FACE);
131
132
  gl.bindVertexArray(this.vao);
@@ -14,8 +14,14 @@
14
14
  * `aces` adds filmic tone mapping before that, rolling highlights off instead of
15
15
  * clipping them flat. Together they are what three.js does by default, which is why a
16
16
  * world ported from it looks wrong until this exists.
17
+ *
18
+ * `shoulder` keeps sRGB's midtones exactly and eases only the highlights: past 0.8 the brightest
19
+ * channel rolls toward 1 and the other two are scaled with it, so an overbright red spark stays
20
+ * red where `srgb` clips each channel and turns its core pink and wide. It is the curve a ported
21
+ * game's own resolve applied — for a port that has to match one — and it has none of ACES's toe or
22
+ * desaturation. See `HIGHLIGHT_SHOULDER_GLSL` for the arithmetic.
17
23
  */
18
- export type OutputTransform = 'none' | 'srgb' | 'aces';
24
+ export type OutputTransform = 'none' | 'srgb' | 'aces' | 'shoulder';
19
25
  /**
20
26
  * The three settings whose accepted values are a closed set, written as one.
21
27
  *
@@ -60,7 +66,8 @@ export interface RenderQuality {
60
66
  * chooses is a look rather than a standard — three.js uses 1/0.6 and says so. This
61
67
  * engine holds no opinion and takes the number from whoever is authoring the world, so
62
68
  * that matching another renderer, or simply preferring a different grade, is a setting
63
- * rather than a patch. Ignored when `outputTransform` is not `aces`.
69
+ * rather than a patch. Scales into `aces` and into `shoulder`, the two curves; ignored by
70
+ * `none` and `srgb`, which have none.
64
71
  */
65
72
  readonly outputExposure: number;
66
73
  /**
@@ -0,0 +1,54 @@
1
+ /**
2
+ * What a blended draw into the scene does to its alpha, which holds how much of each pixel is still
3
+ * the opaque surface — the share ambient occlusion is allowed to darken.
4
+ *
5
+ * **Occlusion is applied in the composite, from the depth the opaque world left**, after every
6
+ * translucent draw has landed, and a particle or a pane writes no depth: so a puff of smoke in front
7
+ * of a wall was darkened by the corner behind it. Reported from a game as walls and two figures
8
+ * standing behind a cloud showing through it as outlines, and nothing at all with occlusion off.
9
+ *
10
+ * So the scene's alpha is a running share. The frame clears it to 1 and an opaque draw writes 1,
11
+ * which is the whole of the surface; then each draw blended in front of it says which of three
12
+ * things it is, and the composite darkens `share` of the pixel and leaves the rest:
13
+ *
14
+ * - **It covers**: glass, smoke, water, a line, text in the world, a panel. The share falls by what
15
+ * the draw lets through, `share * (1 - a)`.
16
+ * - **It transmits**: the medium, the order-independent composite. Its alpha already *is* what gets
17
+ * through, `share * a`.
18
+ * - **It keeps**: light added to the surface or a layer of the surface itself — caustics, a light
19
+ * volume, a bolt, a wet film, a decal, a reflection. The share is unchanged.
20
+ *
21
+ * **What it gives up** is added light in front of an occluded corner: a glow or a spark adds to the
22
+ * pixel and keeps the share, so the composite darkens what it added along with the surface — which
23
+ * is how every blended draw behaved before this, and one multiply cannot tell the two apart. **What
24
+ * would make it wrong** is an opaque pass writing an alpha below 1: its surface would then take less
25
+ * occlusion than it should, which is the safe direction, and is the lit shader's own case at a
26
+ * cutout's edge or under a per-vertex alpha lane.
27
+ *
28
+ * **It survives the temporal passes.** A temporal resolve passes the frame's alpha through, since
29
+ * it is a fact about this frame and takes no history; a reconstruction reads it from the scene it
30
+ * rebuilt into the picture it shows, and the blended draws that land after it cover that picture.
31
+ *
32
+ * **One pipeline does not take part**: the GPU-driven pass blends its own surfaces into a target
33
+ * whose alpha is the mask its blit reads to tell a drawn pixel from an undrawn one, and that blit
34
+ * writes the share back to 1. Its panes still take the occlusion of what they cover. What would
35
+ * close it is a channel for that mask other than alpha.
36
+ *
37
+ * A contributed pass drawing something blended into the scene states the same thing for its own
38
+ * pipeline with `SCENE_ALPHA_COVERS`, `SCENE_ALPHA_TRANSMITS` or `SCENE_ALPHA_KEEPS`.
39
+ */
40
+ /** A blended draw that covers the surface by its alpha: `share * (1 - a)`. */
41
+ export declare const SCENE_ALPHA_COVERS: GPUBlendComponent;
42
+ /** A draw whose alpha is what gets through it, a transmittance or a revealage: `share * a`. */
43
+ export declare const SCENE_ALPHA_TRANSMITS: GPUBlendComponent;
44
+ /** Light added to the surface, or a layer of it: the share unchanged. */
45
+ export declare const SCENE_ALPHA_KEEPS: GPUBlendComponent;
46
+ /**
47
+ * The same three on WebGL2, as the alpha half of `blendFuncSeparate` beside the colour half a
48
+ * caller already had. `src` and `dst` are the colour factors.
49
+ */
50
+ export declare function blendCovering(gl: WebGL2RenderingContext, src: number, dst: number): void;
51
+ /** `SCENE_ALPHA_TRANSMITS` on WebGL2, beside the colour factors `src` and `dst`. */
52
+ export declare function blendTransmitting(gl: WebGL2RenderingContext, src: number, dst: number): void;
53
+ /** `SCENE_ALPHA_KEEPS` on WebGL2, beside the colour factors `src` and `dst`. */
54
+ export declare function blendKeeping(gl: WebGL2RenderingContext, src: number, dst: number): void;