@driftengine/core 4.8.5 → 4.8.6

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 (136) hide show
  1. package/CHANGELOG.json +4321 -4250
  2. package/dist/index.d.ts +10 -2
  3. package/dist/index.js +11 -1
  4. package/dist/render/ambientHarmonics.d.ts +46 -0
  5. package/dist/render/ambientHarmonics.js +80 -0
  6. package/dist/render/areaLights.d.ts +19 -0
  7. package/dist/render/areaLights.js +20 -0
  8. package/dist/render/backend/api.d.ts +13 -2
  9. package/dist/render/backend/webgl2/instanced.d.ts +2 -0
  10. package/dist/render/backend/webgl2/instanced.js +11 -2
  11. package/dist/render/backend/webgl2/renderer.d.ts +74 -1
  12. package/dist/render/backend/webgl2/renderer.js +229 -37
  13. package/dist/render/backend/webgl2/skinScatter.d.ts +14 -0
  14. package/dist/render/backend/webgl2/skinScatter.js +88 -26
  15. package/dist/render/backend/webgpu/buffers.js +2 -1
  16. package/dist/render/backend/webgpu/clusterBinner.js +3 -2
  17. package/dist/render/backend/webgpu/flatPass.d.ts +7 -5
  18. package/dist/render/backend/webgpu/flatPass.js +15 -13
  19. package/dist/render/backend/webgpu/particlePass.d.ts +37 -4
  20. package/dist/render/backend/webgpu/particlePass.js +80 -12
  21. package/dist/render/backend/webgpu/postPass.d.ts +6 -1
  22. package/dist/render/backend/webgpu/postPass.js +7 -2
  23. package/dist/render/backend/webgpu/renderer.d.ts +58 -7
  24. package/dist/render/backend/webgpu/renderer.js +329 -78
  25. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
  26. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.js +41 -1
  27. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +40 -28
  28. package/dist/render/backend/webgpu/skinPaletteRing.js +69 -32
  29. package/dist/render/backend/webgpu/skinScatterPass.d.ts +10 -2
  30. package/dist/render/backend/webgpu/skinScatterPass.js +59 -9
  31. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +2 -1
  32. package/dist/render/backend/webgpu/surfaceTexturePass.js +18 -4
  33. package/dist/render/bloomChain.d.ts +44 -0
  34. package/dist/render/bloomChain.js +28 -0
  35. package/dist/render/bloomPass.d.ts +3 -1
  36. package/dist/render/bloomPass.js +16 -2
  37. package/dist/render/clusteredLights.d.ts +55 -1
  38. package/dist/render/clusteredLights.js +182 -8
  39. package/dist/render/fixtureAtlas.js +1 -1
  40. package/dist/render/instanceCull.js +7 -0
  41. package/dist/render/instances.d.ts +32 -10
  42. package/dist/render/instances.js +17 -8
  43. package/dist/render/lightBudget.d.ts +26 -1
  44. package/dist/render/lightBudget.js +26 -3
  45. package/dist/render/lightmap.d.ts +102 -0
  46. package/dist/render/lightmap.js +118 -0
  47. package/dist/render/mesh.js +2 -1
  48. package/dist/render/particleBatch.d.ts +59 -3
  49. package/dist/render/particleBatch.js +53 -2
  50. package/dist/render/particleMaterial.d.ts +6 -1
  51. package/dist/render/particleMaterial.js +10 -2
  52. package/dist/render/particlePool.d.ts +11 -0
  53. package/dist/render/particlePool.js +2 -0
  54. package/dist/render/particleSort.d.ts +34 -0
  55. package/dist/render/particleSort.js +96 -0
  56. package/dist/render/sceneTarget.d.ts +5 -0
  57. package/dist/render/sceneTarget.js +8 -1
  58. package/dist/render/shaders/bloom.d.ts +8 -4
  59. package/dist/render/shaders/bloom.js +20 -3
  60. package/dist/render/shaders/flat/areaLight.d.ts +10 -0
  61. package/dist/render/shaders/flat/areaLight.js +96 -0
  62. package/dist/render/shaders/flat/index.js +30 -4
  63. package/dist/render/shaders/flat/main.d.ts +1 -1
  64. package/dist/render/shaders/flat/main.js +107 -105
  65. package/dist/render/shaders/flat/models.d.ts +16 -4
  66. package/dist/render/shaders/flat/models.js +64 -4
  67. package/dist/render/shaders/flat/normalMap.d.ts +10 -0
  68. package/dist/render/shaders/flat/normalMap.js +25 -0
  69. package/dist/render/shaders/flat/preamble.js +74 -37
  70. package/dist/render/shaders/flat/skin.js +6 -5
  71. package/dist/render/shaders/generated/bloom.wgsl.d.ts +13 -3
  72. package/dist/render/shaders/generated/bloom.wgsl.js +13 -3
  73. package/dist/render/shaders/generated/flat.wgsl.d.ts +1631 -2044
  74. package/dist/render/shaders/generated/flat.wgsl.js +1669 -2082
  75. package/dist/render/shaders/generated/particle.wgsl.d.ts +115 -1
  76. package/dist/render/shaders/generated/particle.wgsl.js +115 -1
  77. package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +12 -2
  78. package/dist/render/shaders/generated/skinBlur.wgsl.js +12 -2
  79. package/dist/render/shaders/particle.d.ts +17 -1
  80. package/dist/render/shaders/particle.js +133 -1
  81. package/dist/render/shaders/skinBlur.js +25 -3
  82. package/dist/render/skinBlur.d.ts +8 -5
  83. package/dist/render/skinBlur.js +8 -5
  84. package/dist/render/surfaceModel.d.ts +21 -1
  85. package/dist/render/surfaceModel.js +22 -0
  86. package/dist/render/surfaceTexture.d.ts +14 -1
  87. package/dist/render/surfaceTexture.js +15 -2
  88. package/package.json +6 -6
  89. package/src/index.ts +20 -1
  90. package/src/render/ambientHarmonics.ts +92 -0
  91. package/src/render/areaLights.ts +39 -0
  92. package/src/render/backend/api.ts +14 -1
  93. package/src/render/backend/webgl2/instanced.ts +12 -2
  94. package/src/render/backend/webgl2/renderer.ts +292 -35
  95. package/src/render/backend/webgl2/skinScatter.ts +120 -43
  96. package/src/render/backend/webgpu/buffers.ts +2 -1
  97. package/src/render/backend/webgpu/clusterBinner.ts +3 -1
  98. package/src/render/backend/webgpu/flatPass.ts +18 -13
  99. package/src/render/backend/webgpu/particlePass.ts +118 -12
  100. package/src/render/backend/webgpu/postPass.ts +7 -2
  101. package/src/render/backend/webgpu/renderer.ts +396 -95
  102. package/src/render/backend/webgpu/shaders/clusterBin.wgsl.ts +41 -1
  103. package/src/render/backend/webgpu/skinPaletteRing.ts +65 -33
  104. package/src/render/backend/webgpu/skinScatterPass.ts +59 -9
  105. package/src/render/backend/webgpu/surfaceTexturePass.ts +26 -5
  106. package/src/render/bloomChain.ts +69 -0
  107. package/src/render/bloomPass.ts +26 -2
  108. package/src/render/clusteredLights.ts +221 -7
  109. package/src/render/fixtureAtlas.ts +1 -1
  110. package/src/render/instanceCull.ts +5 -0
  111. package/src/render/instances.ts +40 -11
  112. package/src/render/lightBudget.ts +48 -2
  113. package/src/render/lightmap.ts +202 -0
  114. package/src/render/mesh.ts +2 -1
  115. package/src/render/particleBatch.ts +95 -4
  116. package/src/render/particleMaterial.ts +11 -1
  117. package/src/render/particlePool.ts +13 -0
  118. package/src/render/particleSort.ts +133 -0
  119. package/src/render/sceneTarget.ts +18 -1
  120. package/src/render/shaders/bloom.ts +20 -3
  121. package/src/render/shaders/flat/areaLight.ts +96 -0
  122. package/src/render/shaders/flat/index.ts +30 -4
  123. package/src/render/shaders/flat/main.ts +107 -105
  124. package/src/render/shaders/flat/models.ts +73 -8
  125. package/src/render/shaders/flat/normalMap.ts +25 -0
  126. package/src/render/shaders/flat/preamble.ts +74 -37
  127. package/src/render/shaders/flat/skin.ts +6 -5
  128. package/src/render/shaders/generated/bloom.wgsl.ts +13 -3
  129. package/src/render/shaders/generated/flat.wgsl.ts +1669 -2082
  130. package/src/render/shaders/generated/particle.wgsl.ts +116 -1
  131. package/src/render/shaders/generated/skinBlur.wgsl.ts +12 -2
  132. package/src/render/shaders/particle.ts +134 -1
  133. package/src/render/shaders/skinBlur.ts +25 -3
  134. package/src/render/skinBlur.ts +8 -5
  135. package/src/render/surfaceModel.ts +36 -1
  136. package/src/render/surfaceTexture.ts +41 -3
@@ -63,7 +63,9 @@ import {
63
63
  clusteredMode,
64
64
  createClusterLightSet,
65
65
  createClusterTable,
66
+ clusteredLightTotal,
66
67
  fillClusterLightSet,
68
+ lightChannelsOf,
67
69
  } from '../../clusteredLights.ts';
68
70
  import type {
69
71
  PassContext,
@@ -88,7 +90,14 @@ import { FILM_FRAG, FILM_VERT } from '../../shaders/film.ts';
88
90
  import { SceneTarget } from '../../sceneTarget.ts';
89
91
  import type { ColourGradeLut } from '../../colourGrade.ts';
90
92
  import { FILM_LOOK_WITHOUT_COMPOSITE } from '../../shaders/filmLook.ts';
91
- import { bloomProfileWarning, bloomThresholdOf } from '../../bloomChain.ts';
93
+ import { AMBIENT_SH_FLOATS, packAmbientSH } from '../../ambientHarmonics.ts';
94
+ import {
95
+ bloomProfileWarning,
96
+ bloomThresholdOf,
97
+ defaultBloomResponse,
98
+ resolveBloomResponse,
99
+ type BloomResponse,
100
+ } from '../../bloomChain.ts';
92
101
  import {
93
102
  createEmptyTexture2D,
94
103
  createEmptyTexture2DArray,
@@ -129,7 +138,14 @@ import {
129
138
  } from '../../lightBudget.ts';
130
139
  import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel } from '../../surfaceModel.ts';
131
140
  import type { SurfaceModelKind } from '../../surfaceModel.ts';
132
- import { modelBound, skinHalfBound } from '../../shaders/flat/models.ts';
141
+ import { lightmapRegionsBound, modelBound, skinHalfBound } from '../../shaders/flat/models.ts';
142
+ import {
143
+ lightmapTexels,
144
+ warnRegionsWithoutPage,
145
+ warnUnpagedLightmap,
146
+ withLightmapUvs,
147
+ } from '../../lightmap.ts';
148
+ import type { LightmapPage } from '../../lightmap.ts';
133
149
  import type { SkinHalf } from '../../shaders/flat/models.ts';
134
150
  import { GlSkinScatter } from './skinScatter.ts';
135
151
  import {
@@ -224,6 +240,7 @@ import {
224
240
  bindAreaLights,
225
241
  bindAreaShadows,
226
242
  openCones,
243
+ PARTICLE_DEPTH_UNIT,
227
244
  } from '../../lightBudget.ts';
228
245
  import { resolveRenderQuality } from '../../renderQuality.ts';
229
246
  import type { RenderQuality, RenderQualityOptions } from '../../renderQuality.ts';
@@ -430,6 +447,12 @@ export interface Environment extends Atmosphere {
430
447
  lightIesAxes: Float32Array;
431
448
  /** One per active light: a tile of the cookie atlas, or −1 for none. */
432
449
  lightCookies: Float32Array;
450
+ /**
451
+ * One per active light: the lighting channels it lights, a mask from 1 to 255. Absent, short, or
452
+ * a light at 1 is the default channel every surface is on too. See `PointLightSet.lightChannels`
453
+ * and `SurfaceMaterial.lightChannels`.
454
+ */
455
+ lightChannels?: Float32Array;
433
456
  /**
434
457
  * The rectangular emitters this frame, or null for none.
435
458
  *
@@ -508,6 +531,8 @@ export function createEnvironment(overrides: Partial<Environment> = {}): Environ
508
531
  lightIesAxes: new Float32Array(MAX_POINT_LIGHTS * 3),
509
532
  /* −1, because tile 0 is a real cookie and no light here has asked for one. */
510
533
  lightCookies: new Float32Array(MAX_POINT_LIGHTS).fill(-1),
534
+ /* Channel 1, the default every surface is on, so every light lights everything. */
535
+ lightChannels: new Float32Array(MAX_POINT_LIGHTS).fill(1),
511
536
  /* None, so the shader's loop breaks before it reads anything. */
512
537
  areaLights: null,
513
538
  activeLightWorldIndices: new Int32Array(MAX_POINT_LIGHTS).fill(-1),
@@ -618,6 +643,9 @@ export interface SkyColors {
618
643
  export { MAX_DEPTH_LAYER } from '../../depthConvention.ts';
619
644
 
620
645
  /** The identity tint, handed back after any draw that asked for one. See `drawMesh`. */
646
+ /** The far corner of no highlight box: what an environment that names none reads. */
647
+ const NO_HIGHLIGHT: readonly number[] = [0, 0, 0];
648
+
621
649
  const WHITE_TINT = new Float32Array([1, 1, 1]);
622
650
  /** The lit stage's clip plane when there is no mirror: zero, which clips nothing. */
623
651
  const NO_CLIP_PLANE = new Float32Array(4);
@@ -1364,6 +1392,23 @@ export class WebGL2Renderer implements RendererApi {
1364
1392
  private motionBlurScale = 1;
1365
1393
  /** How much of the bloom ceiling this frame takes. See `setBloom`. */
1366
1394
  private bloomScale = 1;
1395
+ /** The draw's own ambient, as `packAmbientSH` writes it; zeros are the frame's. */
1396
+ private readonly ambientSH = new Float32Array(AMBIENT_SH_FLOATS);
1397
+ /** Scratch for a sprite's depth conversion, so a particle draw allocates nothing. */
1398
+ private readonly spriteInverseProjection = mat4.create();
1399
+ /** Said once: a sprite asked for a soft edge where no depth copy could be had. */
1400
+ private warnedSpriteDepth = false;
1401
+ /** Said once: more rectangles than the fixed arm shades, and no froxel table for the rest. */
1402
+ private warnedAreaOverflow = false;
1403
+ /**
1404
+ * `uEnvironmentDials` as it stands: which chain the probe holds, the reflectivity, the gain. One
1405
+ * vector on the device, so written whole, and kept here so a setter moving one keeps the others.
1406
+ */
1407
+ private readonly environmentDials = new Float32Array(3);
1408
+ /** `uEmission` as it stands: the emissive gain and the night factor. */
1409
+ private readonly emission = new Float32Array([1, 0]);
1410
+ /** The ramp and each level's tint, as `resolveBloomResponse` writes them; the subtraction and white. */
1411
+ private readonly bloomResponse = defaultBloomResponse();
1367
1412
  /** In scene units, once `setBloom` has moved it; null reads the profile's. */
1368
1413
  private bloomThresholdSet: number | null = null;
1369
1414
  /**
@@ -2460,9 +2505,7 @@ export class WebGL2Renderer implements RendererApi {
2460
2505
  gl.activeTexture(gl.TEXTURE0 + FIXTURE_ATLAS_TEXTURE_UNIT);
2461
2506
  gl.bindTexture(gl.TEXTURE_2D, this.fixtureTexture);
2462
2507
  gl.uniform1i(u['uFixtureAtlas'] ?? null, FIXTURE_ATLAS_TEXTURE_UNIT);
2463
- gl.uniform1f(u['uIesAtlasRows'] ?? null, this.iesRows);
2464
- gl.uniform1f(u['uIesPlaneCount'] ?? null, this.iesPlanes);
2465
- gl.uniform1f(u['uCookieTiles'] ?? null, this.cookieTiles);
2508
+ gl.uniform3f(u['uFixtureShape'] ?? null, this.iesRows, this.iesPlanes, this.cookieTiles);
2466
2509
  gl.activeTexture(gl.TEXTURE0);
2467
2510
  }
2468
2511
 
@@ -2516,7 +2559,8 @@ export class WebGL2Renderer implements RendererApi {
2516
2559
  gl.uniformMatrix4fv(u['uView'] ?? null, false, camera.view);
2517
2560
 
2518
2561
  this.clusterTable ??= createClusterTable();
2519
- const lights = fillClusterLightSet(env, this.clusterLights);
2562
+ /* The rectangles past the fixed arm's go into the table. See `ClusterLightSet.areas`. */
2563
+ const lights = fillClusterLightSet(env, this.clusterLights, this.shadedAreaLights);
2520
2564
  /* A slot is read only under `POINT_SHADOWS`; see `lightHasFixture`. */
2521
2565
  const shadowSlots = this.quality.pointShadows ? this.lightBudget.maxLights : 0;
2522
2566
  buildLightClusters(
@@ -2536,7 +2580,10 @@ export class WebGL2Renderer implements RendererApi {
2536
2580
  */
2537
2581
  shadowSlots,
2538
2582
  );
2539
- gl.uniform1i(u['uClustered'] ?? null, clusteredMode(lights, lights.count, shadowSlots));
2583
+ gl.uniform1i(
2584
+ u['uClustered'] ?? null,
2585
+ clusteredMode(lights, clusteredLightTotal(lights), shadowSlots),
2586
+ );
2540
2587
  gl.texSubImage2D(
2541
2588
  gl.TEXTURE_2D,
2542
2589
  0,
@@ -3252,8 +3299,9 @@ export class WebGL2Renderer implements RendererApi {
3252
3299
  * one. Measured on hardware: WebGL2 drew neither the bend nor the bulge while WebGPU drew
3253
3300
  * both, which is the two-backend comparison doing exactly what it is for.
3254
3301
  */
3255
- this.warmVariants(data);
3256
- return new Mesh(this.gl, data, options.dynamic === true);
3302
+ const drawn = withLightmapUvs(data);
3303
+ this.warmVariants(drawn);
3304
+ return new Mesh(this.gl, drawn, options.dynamic === true);
3257
3305
  }
3258
3306
 
3259
3307
  /**
@@ -3266,8 +3314,9 @@ export class WebGL2Renderer implements RendererApi {
3266
3314
  * the signal that it was abandoned.
3267
3315
  */
3268
3316
  createMeshIncremental(data: MeshData, options: MeshOptions = {}): IncrementalMesh {
3269
- this.warmVariants(data);
3270
- const { mesh, upload } = createMeshIncremental(this.gl, data, options.dynamic === true);
3317
+ const drawn = withLightmapUvs(data);
3318
+ this.warmVariants(drawn);
3319
+ const { mesh, upload } = createMeshIncremental(this.gl, drawn, options.dynamic === true);
3271
3320
  return {
3272
3321
  mesh,
3273
3322
  upload: {
@@ -3445,6 +3494,11 @@ export class WebGL2Renderer implements RendererApi {
3445
3494
  return new SurfaceTexture(this.gl, sources, options, this.compressedFormats);
3446
3495
  }
3447
3496
 
3497
+ /** A baked lightmap page, as a `lightmapModel` material's `modelMap`. See `RendererApi`. */
3498
+ createLightmap(page: LightmapPage): SurfaceTexture {
3499
+ return new SurfaceTexture(this.gl, lightmapTexels(page), { wrap: 'clamp', mipmap: false });
3500
+ }
3501
+
3448
3502
  /**
3449
3503
  * Replace a surface texture's pixels, keeping the GPU object and its sampler state.
3450
3504
  *
@@ -3522,7 +3576,7 @@ export class WebGL2Renderer implements RendererApi {
3522
3576
  const label = `flat.${variant}.${kind}.${half}`;
3523
3577
  const program = compileProgram(
3524
3578
  gl,
3525
- litVertexOf(variant),
3579
+ modelVertexOf(variant, kind),
3526
3580
  skinHalfBound(modelBound(this.flatFragSource, kind), half),
3527
3581
  label,
3528
3582
  );
@@ -3693,6 +3747,7 @@ export class WebGL2Renderer implements RendererApi {
3693
3747
  this.currentMaterial = material;
3694
3748
  /* What the blur spreads this skin by: the last material to name a profile says. */
3695
3749
  const model = material?.model;
3750
+ warnUnpagedLightmap(model, material?.modelMap);
3696
3751
  if (model?.kind === 'skin' && this.skinScatter !== null) {
3697
3752
  this.skinScatter.setProfile(
3698
3753
  model.profile,
@@ -3738,7 +3793,8 @@ export class WebGL2Renderer implements RendererApi {
3738
3793
  v[0] = material?.cutout ?? 0;
3739
3794
  v[1] = CUTOUT_RESOLVE_CODE[this.cutoutResolveNow(material, translucent)];
3740
3795
  v[2] = this.cutoutFrame;
3741
- v[3] = 0;
3796
+ /* The material's lighting channels, which the lamps' are compared with. See `uCutout`. */
3797
+ v[3] = lightChannelsOf(material?.lightChannels);
3742
3798
  return v;
3743
3799
  }
3744
3800
 
@@ -3866,6 +3922,8 @@ export class WebGL2Renderer implements RendererApi {
3866
3922
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
3867
3923
  this.materialFlags[0] = 0;
3868
3924
  gl.uniform4iv(u['uMaterialFlags'] ?? null, this.materialFlags);
3925
+ /* No image to cut, and the channels all the same: a material needs no albedo to name them. */
3926
+ gl.uniform4f(u['uCutout'] ?? null, 0, 0, 0, lightChannelsOf(material?.lightChannels));
3869
3927
  return;
3870
3928
  }
3871
3929
  albedo.bind(gl, SURFACE_TEXTURE_UNIT);
@@ -3917,6 +3975,17 @@ export class WebGL2Renderer implements RendererApi {
3917
3975
  * is 1 and `bindMeshPass` restores it, so it scales geometry that declared grain and
3918
3976
  * leaves geometry that did not exactly as it is.
3919
3977
  */
3978
+ /**
3979
+ * The ambient the following draws take, as nine spherical-harmonic coefficients of incoming
3980
+ * radiance — red, green and blue of each, twenty-seven numbers — or `null` for the frame's own:
3981
+ * the sky's gradient, or the probes'. See `ambientHarmonics.ts` for the basis and the axes.
3982
+ *
3983
+ * **For a thing lit by where it stands**: a consumer sampling a stage's baked volume of indirect
3984
+ * light at a character every frame sets the result around that character's draws, and the
3985
+ * character takes the light of its spot rather than the frame's. It replaces the diffuse ambient
3986
+ * only — a glossy surface still reflects the probes — and it is pass state like the material,
3987
+ * which `bindMeshPass` clears, so a mirror or a probe bake drawn after does not inherit it.
3988
+ */
3920
3989
  /**
3921
3990
  * How much of the environment the following draws mirror, 0 to 1.
3922
3991
  *
@@ -3927,10 +3996,22 @@ export class WebGL2Renderer implements RendererApi {
3927
3996
  * Defaults to zero and bindMeshPass restores it, so a scene that never calls this looks
3928
3997
  * exactly as it did.
3929
3998
  */
3999
+ setAmbientSH(coefficients: ArrayLike<number> | null): void {
4000
+ if (this.contextLost) return;
4001
+ packAmbientSH(coefficients, this.ambientSH);
4002
+ this.materials.dirty();
4003
+ for (const target of this.flatTargets) {
4004
+ this.gl.useProgram(target.program);
4005
+ this.gl.uniform4fv(target.uniforms['uAmbientSH[0]'] ?? null, this.ambientSH);
4006
+ }
4007
+ this.useFlatProgram();
4008
+ }
4009
+
3930
4010
  setSurfaceReflectivity(amount: number): void {
3931
4011
  if (this.contextLost) return;
3932
4012
  this.materials.dirty();
3933
- this.writeFlatFloat('uReflectivity', Math.min(1, Math.max(0, amount)));
4013
+ this.environmentDials[1] = Math.min(1, Math.max(0, amount));
4014
+ this.writeFlatVector('uEnvironmentDials', this.environmentDials);
3934
4015
  }
3935
4016
 
3936
4017
  /**
@@ -3948,7 +4029,8 @@ export class WebGL2Renderer implements RendererApi {
3948
4029
  setEnvironmentGain(gain: number): void {
3949
4030
  if (this.contextLost) return;
3950
4031
  this.materials.dirty();
3951
- this.writeFlatFloat('uEnvironmentGain', Math.max(0, gain));
4032
+ this.environmentDials[2] = Math.max(0, gain);
4033
+ this.writeFlatVector('uEnvironmentDials', this.environmentDials);
3952
4034
  }
3953
4035
 
3954
4036
  /**
@@ -4019,6 +4101,18 @@ export class WebGL2Renderer implements RendererApi {
4019
4101
  this.useFlatProgram();
4020
4102
  }
4021
4103
 
4104
+ /** A vector of material state, two to four floats, into every flat program. */
4105
+ private writeFlatVector(name: string, value: Float32Array): void {
4106
+ for (const target of this.flatTargets) {
4107
+ this.gl.useProgram(target.program);
4108
+ const at = target.uniforms[name] ?? null;
4109
+ if (value.length === 2) this.gl.uniform2fv(at, value);
4110
+ else if (value.length === 3) this.gl.uniform3fv(at, value);
4111
+ else this.gl.uniform4fv(at, value);
4112
+ }
4113
+ this.useFlatProgram();
4114
+ }
4115
+
4022
4116
  /** One int of material state, into every flat program, for `writeFlatFloat`'s reason. */
4023
4117
  private writeFlatInt(name: string, value: number): void {
4024
4118
  for (const target of this.flatTargets) {
@@ -4083,8 +4177,13 @@ export class WebGL2Renderer implements RendererApi {
4083
4177
  setSurfaceRelief(amount: number, cyclesPerMetre = 60): void {
4084
4178
  if (this.contextLost) return;
4085
4179
  this.materials.dirty();
4086
- this.writeFlatFloat('uRelief', Math.min(1, Math.max(0, amount)));
4087
- this.writeFlatFloat('uReliefCycles', Math.max(0.01, cyclesPerMetre));
4180
+ const strength = Math.min(1, Math.max(0, amount));
4181
+ const cycles = Math.max(0.01, cyclesPerMetre);
4182
+ for (const target of this.flatTargets) {
4183
+ this.gl.useProgram(target.program);
4184
+ this.gl.uniform2f(target.uniforms['uRelief'] ?? null, strength, cycles);
4185
+ }
4186
+ this.useFlatProgram();
4088
4187
  }
4089
4188
 
4090
4189
  /**
@@ -4142,7 +4241,8 @@ export class WebGL2Renderer implements RendererApi {
4142
4241
  setEmissiveGain(gain: number): void {
4143
4242
  if (this.contextLost) return;
4144
4243
  this.materials.dirty();
4145
- this.writeFlatFloat('uEmissiveGain', gain);
4244
+ this.emission[0] = gain;
4245
+ this.writeFlatVector('uEmission', this.emission);
4146
4246
  }
4147
4247
 
4148
4248
  /* -- Instanced meshes --------------------------------------------------------------- */
@@ -4254,6 +4354,7 @@ export class WebGL2Renderer implements RendererApi {
4254
4354
  options: TranslucentMeshOptions,
4255
4355
  ): void {
4256
4356
  if (this.contextLost) return;
4357
+ warnRegionsWithoutPage(this.currentMaterial?.model, data.lightmapRegions);
4257
4358
  const mesh = batch.mesh;
4258
4359
  /* Nor a batch of a mesh with nothing in it: see `Mesh.indexCount`. */
4259
4360
  if (!mesh.complete || mesh.indexCount === 0) return;
@@ -4767,8 +4868,9 @@ export class WebGL2Renderer implements RendererApi {
4767
4868
  const { gl } = this;
4768
4869
  const u = batch.uniforms;
4769
4870
  gl.useProgram(batch.program);
4770
- batch.upload(gl, data);
4871
+ batch.upload(gl, data, camera.position);
4771
4872
  batch.bindMaterial(gl);
4873
+ if (batch.material === 'sprite') this.bindSpriteFades(batch, camera);
4772
4874
  gl.uniformMatrix4fv(u['uViewProj'] ?? null, false, this.viewProjFor(camera));
4773
4875
  gl.uniform3fv(u['uCameraPos'] ?? null, camera.position);
4774
4876
  gl.uniform1f(u['uTime'] ?? null, timeSeconds);
@@ -4800,6 +4902,48 @@ export class WebGL2Renderer implements RendererApi {
4800
4902
  if (this.materialDoubleSided) gl.enable(gl.CULL_FACE);
4801
4903
  }
4802
4904
 
4905
+ /**
4906
+ * A sprite's two fades and the depth copy the soft one reads. The copy is the frame's opaque
4907
+ * depth, so it is taken only in the frame's own picture: in a mirror or a probe's face it would
4908
+ * be another view's, and the edge stays hard there. Without a scene target there is no copy at
4909
+ * all, and the renderer says so once rather than drawing a hard edge that was asked to be soft.
4910
+ */
4911
+ private bindSpriteFades(batch: ParticleBatch, camera: Camera): void {
4912
+ const { gl } = this;
4913
+ const u = batch.uniforms;
4914
+ const scene = this.sceneTarget;
4915
+ let depth: WebGLTexture | null = null;
4916
+ if (batch.softDepth > 0 && !this.reflectionPassActive && !this.probePassActive) {
4917
+ depth = scene?.snapshotDepth(true) ?? null;
4918
+ if (depth === null && !this.warnedSpriteDepth) {
4919
+ this.warnedSpriteDepth = true;
4920
+ console.warn(
4921
+ "WebGL2: a 'sprite' asked for a soft edge, which reads a copy of the frame's depth, and " +
4922
+ 'this profile keeps none — screenEffects is off, or the driver refused the copy. The ' +
4923
+ 'edge is hard.',
4924
+ );
4925
+ }
4926
+ }
4927
+ gl.activeTexture(gl.TEXTURE0 + PARTICLE_DEPTH_UNIT);
4928
+ gl.bindTexture(gl.TEXTURE_2D, depth ?? this.emptyTexture2D);
4929
+ gl.uniform1i(u['uDepth'] ?? null, PARTICLE_DEPTH_UNIT);
4930
+ gl.uniform4f(u['uFade'] ?? null, batch.softDepth, batch.cameraFade, depth === null ? 0 : 1, 0);
4931
+ mat4.invert(this.spriteInverseProjection, camera.projection);
4932
+ const inverse = this.spriteInverseProjection;
4933
+ gl.uniform4f(
4934
+ u['uDepthToViewZ'] ?? null,
4935
+ inverse[10] ?? 0,
4936
+ inverse[14] ?? 0,
4937
+ inverse[11] ?? 0,
4938
+ inverse[15] ?? 1,
4939
+ );
4940
+ gl.uniform2f(
4941
+ u['uViewport'] ?? null,
4942
+ scene?.frameWidth ?? gl.drawingBufferWidth,
4943
+ scene?.frameHeight ?? gl.drawingBufferHeight,
4944
+ );
4945
+ }
4946
+
4803
4947
  /** Build a batch for electrical arcs. `capacity` is in segments, not arcs. */
4804
4948
  createBolts(segmentCapacity: number, label = 'bolts'): BoltBatch {
4805
4949
  return new BoltBatch(this.gl, segmentCapacity, label);
@@ -5918,7 +6062,7 @@ export class WebGL2Renderer implements RendererApi {
5918
6062
  const label = `flat.${variant}.${kind}.${half}`;
5919
6063
  const program = compileProgram(
5920
6064
  gl,
5921
- litVertexOf(variant),
6065
+ modelVertexOf(variant, kind),
5922
6066
  skinHalfBound(modelBound(this.flatFragSource, kind), half),
5923
6067
  label,
5924
6068
  );
@@ -6800,10 +6944,17 @@ export class WebGL2Renderer implements RendererApi {
6800
6944
  * and wrong for a day whose exposure spans 2.5 to 14: a courtyard blooming at noon, or candles
6801
6945
  * never blooming at night. A caller whose exposure moves passes what it wants on screen over the
6802
6946
  * exposure. Omitted leaves it where it was, so a caller that never passes one keeps the profile's.
6947
+ *
6948
+ * **`response` is how a colour comes in past the threshold and what colour each octave of the
6949
+ * halo takes** — a ramp in place of the subtraction, and a tint a level — held until moved like
6950
+ * the threshold; `null` goes back to the subtraction and white. See `BloomResponse`: it is what
6951
+ * matches a stage built for another engine's bloom, whose tints are how a strength past 1 is
6952
+ * asked for.
6803
6953
  */
6804
- setBloom(scale: number, threshold?: number): void {
6954
+ setBloom(scale: number, threshold?: number, response?: BloomResponse | null): void {
6805
6955
  this.bloomScale = Math.min(Math.max(scale, 0), 1);
6806
6956
  if (threshold !== undefined) this.bloomThresholdSet = bloomThresholdOf(threshold);
6957
+ if (response !== undefined) resolveBloomResponse(response, this.bloomResponse);
6807
6958
  }
6808
6959
 
6809
6960
  /**
@@ -7364,6 +7515,7 @@ export class WebGL2Renderer implements RendererApi {
7364
7515
  ? {
7365
7516
  strength: bloomStrength,
7366
7517
  threshold: this.bloomThresholdSet ?? this.quality.bloomThreshold,
7518
+ response: this.bloomResponse,
7367
7519
  }
7368
7520
  : undefined;
7369
7521
 
@@ -7699,6 +7851,19 @@ export class WebGL2Renderer implements RendererApi {
7699
7851
  bindMeshPass(camera: Camera, env: Environment): void {
7700
7852
  /* The pass's own material is reopened, so the next draw opens one. See `materialChanges.ts`. */
7701
7853
  this.materials.dirty();
7854
+ /* Rectangles past the fixed arm's are the froxel table's, and without it nobody shades them. */
7855
+ if (
7856
+ !this.warnedAreaOverflow &&
7857
+ !this.quality.clusteredLights &&
7858
+ (env.areaLights?.count ?? 0) > this.shadedAreaLights
7859
+ ) {
7860
+ this.warnedAreaOverflow = true;
7861
+ console.warn(
7862
+ `WebGL2: ${env.areaLights?.count ?? 0} rectangular lights and ${this.shadedAreaLights} ` +
7863
+ 'shaded without clusteredLights: the rest are not drawn. Turn clusteredLights on, and the ' +
7864
+ 'froxel table shades every one past the fixed ones, unshadowed.',
7865
+ );
7866
+ }
7702
7867
  /*
7703
7868
  * Captured here rather than taken as an argument to `endFrame`, because this is the
7704
7869
  * camera the world was drawn with and a caller should not have to hand it over twice.
@@ -7810,6 +7975,13 @@ export class WebGL2Renderer implements RendererApi {
7810
7975
  this.materialHasMaps = false;
7811
7976
  this.currentMaterial = null;
7812
7977
  this.currentSurfaceTexture = null;
7978
+ /* Nor an ambient of its own: the frame's, until a draw asks. See `setAmbientSH`. */
7979
+ this.ambientSH.fill(0);
7980
+ /* No reflection and an environment at its own brightness; the emission the world asks for. */
7981
+ this.environmentDials[1] = 0;
7982
+ this.environmentDials[2] = 1;
7983
+ this.emission[0] = env.emissiveGain;
7984
+ this.emission[1] = env.nightFactor;
7813
7985
  for (const target of this.flatTargets) {
7814
7986
  this.gl.useProgram(target.program);
7815
7987
  this.writeMeshPassState(target.uniforms, camera, env);
@@ -8036,13 +8208,18 @@ export class WebGL2Renderer implements RendererApi {
8036
8208
  this.oitWeight = 0;
8037
8209
  this.ditherFade = 0;
8038
8210
  gl.uniform2f(u['uWriteMode'] ?? null, 0, 0);
8039
- /* No environment reflection unless a caller asks, so existing scenes are unchanged. */
8040
- gl.uniform1f(u['uReflectivity'] ?? null, 0);
8041
- /* 1, not 0: this one is a multiplier and its identity is one. See `setEnvironmentGain`. */
8042
- gl.uniform1f(u['uEnvironmentGain'] ?? null, 1);
8211
+ /* The draw's own ambient as it stands, which `bindMeshPass` has just cleared: a program built
8212
+ in the middle of a pass takes what the caller set. See `setAmbientSH`. */
8213
+ gl.uniform4fv(u['uAmbientSH[0]'] ?? null, this.ambientSH);
8214
+ /*
8215
+ * Which chain the probe holds, no environment reflection, and an environment at its own
8216
+ * brightness — the reflectivity's identity is 0 and the gain's is 1 — as `bindMeshPass` set
8217
+ * them, or as a draw since has: a program built in the middle of a pass takes what stands.
8218
+ * See `setSurfaceReflectivity` and `setEnvironmentGain`.
8219
+ */
8220
+ gl.uniform3fv(u['uEnvironmentDials'] ?? null, this.environmentDials);
8043
8221
  /* And no relief unless a caller asks, for the same reason. */
8044
- gl.uniform1f(u['uRelief'] ?? null, 0);
8045
- gl.uniform1f(u['uReliefCycles'] ?? null, 60);
8222
+ gl.uniform2f(u['uRelief'] ?? null, 0, 60);
8046
8223
  /* And no relief read off a surface texture either, for the same reason. */
8047
8224
  gl.uniform1f(u['uTextureRelief'] ?? null, 0);
8048
8225
  /* The mirror's plane in a reflection pass, and zero — which clips nothing — in every other. */
@@ -8060,8 +8237,8 @@ export class WebGL2Renderer implements RendererApi {
8060
8237
  this.reflectionPassActive ? this.reflectionAtmosphereY : (camera.position[1] ?? 0),
8061
8238
  this.quality.underwaterAtmosphere,
8062
8239
  );
8063
- gl.uniform1f(u['uEmissiveGain'] ?? null, env.emissiveGain);
8064
- gl.uniform1f(u['uNightFactor'] ?? null, env.nightFactor);
8240
+ /* The emissive gain and the night factor, as the pass set them. See `uEmission`. */
8241
+ gl.uniform2fv(u['uEmission'] ?? null, this.emission);
8065
8242
  /* Absent means zero, so a world written before this term existed adds nothing. */
8066
8243
  gl.uniform1f(u['uNightEmissive'] ?? null, env.nightEmissive ?? 0);
8067
8244
  /*
@@ -8083,8 +8260,16 @@ export class WebGL2Renderer implements RendererApi {
8083
8260
  gl.uniform1f(u['uShadowMaxSlope'] ?? null, this.quality.directionalShadowMaxSlope);
8084
8261
  gl.uniform1i(u['uShadowFilterTaps'] ?? null, this.quality.shadowFilterTaps);
8085
8262
  gl.uniform3fv(u['uHighlightMin'] ?? null, env.highlightMin);
8086
- gl.uniform3fv(u['uHighlightMax'] ?? null, env.highlightMax);
8087
- gl.uniform1f(u['uHighlightGain'] ?? null, env.highlightGain);
8263
+ /* The far corner, and the gain in its w. See `uHighlightMax`. */
8264
+ /* An environment written by hand may leave the box out; then there is none, as before. */
8265
+ const max = env.highlightMax ?? NO_HIGHLIGHT;
8266
+ gl.uniform4f(
8267
+ u['uHighlightMax'] ?? null,
8268
+ max[0] ?? 0,
8269
+ max[1] ?? 0,
8270
+ max[2] ?? 0,
8271
+ env.highlightGain ?? 0,
8272
+ );
8088
8273
 
8089
8274
  bindPointLights(gl, u, env, this.quality.pointLightFalloff);
8090
8275
  this.bindClusters(u, camera, env);
@@ -8184,7 +8369,8 @@ export class WebGL2Renderer implements RendererApi {
8184
8369
  * between them; there is one array, so `RenderQuality.environmentPrefilter` decides what the
8185
8370
  * convolution writes and a profile that changes it has to bake again.
8186
8371
  */
8187
- gl.uniform1f(u['uEnvironmentPrefiltered'] ?? null, this.quality.environmentPrefilter ? 1 : 0);
8372
+ this.environmentDials[0] = this.quality.environmentPrefilter ? 1 : 0;
8373
+ gl.uniform3fv(u['uEnvironmentDials'] ?? null, this.environmentDials);
8188
8374
  gl.uniform1f(u['uEnvironmentMaxLod'] ?? null, array?.ggxMaxLevel ?? 0);
8189
8375
  /*
8190
8376
  * The map's edge, which used to be `exp2(maxLod)` and is not any more: the chain stops one
@@ -8383,12 +8569,17 @@ export class WebGL2Renderer implements RendererApi {
8383
8569
  if (this.quality.cullDraws && this.occluded(mesh.bounds, model)) return;
8384
8570
  this.drawBudget.ask();
8385
8571
  this.takeMaterial();
8386
- /* Skin under the screen-space blur draws its two halves: the frame's, then at once its diffuse. */
8572
+ /*
8573
+ * Skin under the screen-space blur draws its halves: the frame's, then at once its diffuse's
8574
+ * light and its colour, each into the blur's own target.
8575
+ */
8387
8576
  const splits = this.skinSplits();
8388
8577
  this.drawMeshHalf(mesh, model, depthLayer, tint, splits ? 'scene' : 'whole');
8389
8578
  if (splits && this.skinScatter !== null) {
8390
8579
  this.skinScatter.begin();
8391
8580
  this.drawMeshHalf(mesh, model, depthLayer, tint, 'diffuse');
8581
+ this.skinScatter.beginAlbedo();
8582
+ this.drawMeshHalf(mesh, model, depthLayer, tint, 'albedo');
8392
8583
  this.sceneTarget?.bind();
8393
8584
  }
8394
8585
  }
@@ -8574,8 +8765,9 @@ export class WebGL2Renderer implements RendererApi {
8574
8765
  * depth 1.0 against a buffer cleared to 1.0, and under `LESS` it would not draw at
8575
8766
  * all. Film, caustics and text lie flush on surfaces for the same reason.
8576
8767
  */
8577
- /* A skin's diffuse half is the same surface again: it finds the depth its frame half wrote. */
8578
- const again = half === 'diffuse';
8768
+ /* A skin's diffuse and colour halves are the same surface again: each finds the depth its
8769
+ frame half wrote. */
8770
+ const again = half === 'diffuse' || half === 'albedo';
8579
8771
  gl.depthFunc(again ? gl.EQUAL : this.reversedDepth ? gl.GREATER : gl.LESS);
8580
8772
  if (again) gl.depthMask(false);
8581
8773
  /* A dithered cutout in a multisampled frame with no temporal resolve: its share becomes the
@@ -9030,6 +9222,62 @@ export class WebGL2Renderer implements RendererApi {
9030
9222
  return true;
9031
9223
  }
9032
9224
 
9225
+ /**
9226
+ * One layer of the declared probe grid from an image, the way `setEnvironmentImage` fills a
9227
+ * single probe: the same equirectangular image of linear RGB floats, projected onto the capture
9228
+ * cube and prefiltered into that layer, the others left as they are. Returns false — and says
9229
+ * why — where there is no probe, no grid, or no such layer.
9230
+ *
9231
+ * **What it is for**: a scene baked somewhere else. Another engine's reflection captures are
9232
+ * images taken where each one stands; a consumer fits them to a lattice — for each point, the
9233
+ * capture whose reach holds it — and hands each point its image here, where `bakeProbe` would
9234
+ * have drawn this engine's own world into it.
9235
+ *
9236
+ * `options.irradiance` decides, at grid scope as a bake's does, whether the grid also lights the
9237
+ * scene's diffuse ambient. A grid that crossfades writes the set being swept, as a bake does.
9238
+ */
9239
+ setProbeLayerImage(
9240
+ layer: number,
9241
+ image: {
9242
+ readonly width: number;
9243
+ readonly height: number;
9244
+ readonly data: Float32Array;
9245
+ },
9246
+ options?: ProbeBakeOptions,
9247
+ ): boolean {
9248
+ if (this.contextLost) return false;
9249
+ const probe = this.probeCapture;
9250
+ const array = this.probeArray;
9251
+ const grid = this.probes;
9252
+ const sweeps = this.probeSweeps;
9253
+ if (probe === null || array === null || grid === null || sweeps === null) {
9254
+ console.warn(
9255
+ 'Renderer: setProbeLayerImage needs a reflection probe (`reflectionProbeSize`) and a grid ' +
9256
+ 'declared with setProbeGrid; the image was ignored.',
9257
+ );
9258
+ return false;
9259
+ }
9260
+ if (!Number.isInteger(layer) || layer < 0 || layer >= grid.layers) {
9261
+ console.warn(
9262
+ `Renderer: setProbeLayerImage was given layer ${layer}, and the grid has ${grid.layers}.`,
9263
+ );
9264
+ return false;
9265
+ }
9266
+ if (!probe.upload(this.gl, equirectToCubeFaces(image, probe.size))) return false;
9267
+ this.probeAmbient = options?.irradiance ?? true;
9268
+ this.environmentPrefilter?.run(
9269
+ this.gl,
9270
+ probe,
9271
+ array,
9272
+ sweeps.writeLayer(layer),
9273
+ ENVIRONMENT_TEXTURE_UNIT,
9274
+ this.quality.environmentPrefilterSamples,
9275
+ this.quality.environmentPrefilter,
9276
+ );
9277
+ sweeps.baked(layer);
9278
+ return true;
9279
+ }
9280
+
9033
9281
  /**
9034
9282
  * Draw a volume of light: a beam from a lamp, a shaft through a window, the cone under a
9035
9283
  * street light.
@@ -9420,6 +9668,15 @@ const NO_MODEL_PARAMS = new Float32Array(MODEL_PARAM_FLOATS);
9420
9668
  * The lit vertex stage a program of this variant compiles: the standard programs' and the cloth's,
9421
9669
  * which `modelProgram` pairs a model's fragment stage with.
9422
9670
  */
9671
+ /**
9672
+ * The vertex stage a model's program draws with: the variant's own, but for a lightmapped instanced
9673
+ * batch, whose tint lane is each instance's region of the page. See `lightmap.ts`.
9674
+ */
9675
+ function modelVertexOf(variant: string, kind: SurfaceModelKind): string {
9676
+ const vertex = litVertexOf(variant);
9677
+ return kind === 'lightmap' && variant === 'instanced' ? lightmapRegionsBound(vertex) : vertex;
9678
+ }
9679
+
9423
9680
  function litVertexOf(variant: string): string {
9424
9681
  if (variant.startsWith('cloth')) {
9425
9682
  const eight = variant[5] === '8';