@driftengine/core 4.13.3 → 4.13.5

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 (153) hide show
  1. package/CHANGELOG.json +52 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +3 -2
  4. package/dist/render/anisotropicLobe.d.ts +5 -0
  5. package/dist/render/anisotropicLobe.js +13 -0
  6. package/dist/render/atmosphere.d.ts +79 -1
  7. package/dist/render/atmosphere.js +66 -19
  8. package/dist/render/backend/webgl2/renderer.d.ts +34 -2
  9. package/dist/render/backend/webgl2/renderer.js +44 -12
  10. package/dist/render/backend/webgpu/boltPass.d.ts +1 -1
  11. package/dist/render/backend/webgpu/causticsPass.d.ts +1 -1
  12. package/dist/render/backend/webgpu/clothSetPack.js +1 -0
  13. package/dist/render/backend/webgpu/clothSolvePack.d.ts +5 -2
  14. package/dist/render/backend/webgpu/clothSolvePack.js +2 -1
  15. package/dist/render/backend/webgpu/filmPass.d.ts +1 -1
  16. package/dist/render/backend/webgpu/gpuClothSet.js +1 -1
  17. package/dist/render/backend/webgpu/gpuDrivenPass.js +20 -3
  18. package/dist/render/backend/webgpu/linePass.d.ts +1 -1
  19. package/dist/render/backend/webgpu/mediumFields.d.ts +17 -0
  20. package/dist/render/backend/webgpu/mediumFields.js +74 -0
  21. package/dist/render/backend/webgpu/plumePass.d.ts +3 -3
  22. package/dist/render/backend/webgpu/postPass.d.ts +7 -2
  23. package/dist/render/backend/webgpu/renderer.d.ts +13 -3
  24. package/dist/render/backend/webgpu/renderer.js +86 -150
  25. package/dist/render/backend/webgpu/scatterPass.d.ts +1 -1
  26. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +1 -1
  27. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +7 -5
  28. package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
  29. package/dist/render/environmentMixing.d.ts +18 -0
  30. package/dist/render/environmentMixing.js +40 -0
  31. package/dist/render/fog.d.ts +43 -1
  32. package/dist/render/fog.js +87 -22
  33. package/dist/render/globalMedium.d.ts +112 -10
  34. package/dist/render/globalMedium.js +96 -1
  35. package/dist/render/globalMediumPass.d.ts +7 -3
  36. package/dist/render/globalMediumPass.js +22 -1
  37. package/dist/render/hairLobes.d.ts +27 -2
  38. package/dist/render/hairLobes.js +57 -4
  39. package/dist/render/lightBudget.d.ts +12 -0
  40. package/dist/render/lightBudget.js +24 -0
  41. package/dist/render/sceneTarget.d.ts +5 -3
  42. package/dist/render/sceneTarget.js +14 -8
  43. package/dist/render/shaders/arcane.d.ts +1 -1
  44. package/dist/render/shaders/bolt.d.ts +1 -1
  45. package/dist/render/shaders/caustics.d.ts +1 -1
  46. package/dist/render/shaders/film.d.ts +1 -1
  47. package/dist/render/shaders/film.js +4 -1
  48. package/dist/render/shaders/fire.d.ts +1 -1
  49. package/dist/render/shaders/flat/anisotropic.d.ts +8 -1
  50. package/dist/render/shaders/flat/anisotropic.js +25 -3
  51. package/dist/render/shaders/flat/hair.js +77 -27
  52. package/dist/render/shaders/flat/main.d.ts +1 -1
  53. package/dist/render/shaders/flat/main.js +23 -3
  54. package/dist/render/shaders/flat/models.js +12 -5
  55. package/dist/render/shaders/flat/preamble.js +7 -0
  56. package/dist/render/shaders/fog.d.ts +4 -4
  57. package/dist/render/shaders/fog.js +81 -32
  58. package/dist/render/shaders/generated/arcane.wgsl.d.ts +29 -39
  59. package/dist/render/shaders/generated/arcane.wgsl.js +29 -39
  60. package/dist/render/shaders/generated/bolt.wgsl.d.ts +26 -36
  61. package/dist/render/shaders/generated/bolt.wgsl.js +26 -36
  62. package/dist/render/shaders/generated/caustics.wgsl.d.ts +25 -35
  63. package/dist/render/shaders/generated/caustics.wgsl.js +25 -35
  64. package/dist/render/shaders/generated/film.wgsl.d.ts +28 -38
  65. package/dist/render/shaders/generated/film.wgsl.js +28 -38
  66. package/dist/render/shaders/generated/fire.wgsl.d.ts +28 -38
  67. package/dist/render/shaders/generated/fire.wgsl.js +28 -38
  68. package/dist/render/shaders/generated/flat.wgsl.d.ts +1466 -1546
  69. package/dist/render/shaders/generated/flat.wgsl.js +1489 -1569
  70. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +98 -12
  71. package/dist/render/shaders/generated/globalMedium.wgsl.js +98 -12
  72. package/dist/render/shaders/generated/line.wgsl.d.ts +25 -35
  73. package/dist/render/shaders/generated/line.wgsl.js +25 -35
  74. package/dist/render/shaders/generated/particle.wgsl.d.ts +100 -140
  75. package/dist/render/shaders/generated/particle.wgsl.js +100 -140
  76. package/dist/render/shaders/generated/rush.wgsl.d.ts +20 -10
  77. package/dist/render/shaders/generated/rush.wgsl.js +20 -10
  78. package/dist/render/shaders/generated/scatter.wgsl.d.ts +25 -35
  79. package/dist/render/shaders/generated/scatter.wgsl.js +25 -35
  80. package/dist/render/shaders/generated/smoke.wgsl.d.ts +28 -38
  81. package/dist/render/shaders/generated/smoke.wgsl.js +28 -38
  82. package/dist/render/shaders/generated/water.wgsl.d.ts +29 -39
  83. package/dist/render/shaders/generated/water.wgsl.js +29 -39
  84. package/dist/render/shaders/globalMedium.js +114 -10
  85. package/dist/render/shaders/gpudriven/blendRaster.wgsl.js +2 -1
  86. package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +10 -4
  87. package/dist/render/shaders/gpudriven/fog.wgsl.js +77 -19
  88. package/dist/render/shaders/gpudriven/frameBlock.wgsl.d.ts +1 -1
  89. package/dist/render/shaders/gpudriven/frameBlock.wgsl.js +12 -1
  90. package/dist/render/shaders/gpudriven/shade.wgsl.js +3 -1
  91. package/dist/render/shaders/line.d.ts +1 -1
  92. package/dist/render/shaders/line.js +4 -2
  93. package/dist/render/shaders/localExposure.d.ts +42 -1
  94. package/dist/render/shaders/localExposure.js +73 -6
  95. package/dist/render/shaders/particle.d.ts +3 -3
  96. package/dist/render/shaders/particle.js +10 -6
  97. package/dist/render/shaders/scatter.d.ts +1 -1
  98. package/dist/render/shaders/scatter.js +4 -2
  99. package/dist/render/shaders/smoke.d.ts +1 -1
  100. package/dist/render/shaders/water.d.ts +1 -1
  101. package/dist/render/shaders/water.js +2 -1
  102. package/dist/render/surfaceModel.d.ts +23 -4
  103. package/dist/render/surfaceModel.js +12 -2
  104. package/package.json +6 -6
  105. package/src/index.ts +12 -2
  106. package/src/render/anisotropicLobe.ts +20 -0
  107. package/src/render/atmosphere.ts +153 -20
  108. package/src/render/backend/webgl2/renderer.ts +63 -13
  109. package/src/render/backend/webgpu/clothSetPack.ts +1 -0
  110. package/src/render/backend/webgpu/clothSolvePack.ts +6 -2
  111. package/src/render/backend/webgpu/gpuClothSet.ts +1 -1
  112. package/src/render/backend/webgpu/gpuDrivenPass.ts +20 -3
  113. package/src/render/backend/webgpu/mediumFields.ts +87 -0
  114. package/src/render/backend/webgpu/renderer.ts +109 -157
  115. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +7 -5
  116. package/src/render/environmentMixing.ts +48 -0
  117. package/src/render/fog.ts +125 -25
  118. package/src/render/globalMedium.ts +214 -11
  119. package/src/render/globalMediumPass.ts +29 -4
  120. package/src/render/hairLobes.ts +109 -3
  121. package/src/render/lightBudget.ts +32 -0
  122. package/src/render/sceneTarget.ts +15 -11
  123. package/src/render/shaders/film.ts +4 -1
  124. package/src/render/shaders/flat/anisotropic.ts +25 -3
  125. package/src/render/shaders/flat/hair.ts +77 -27
  126. package/src/render/shaders/flat/main.ts +23 -3
  127. package/src/render/shaders/flat/models.ts +12 -5
  128. package/src/render/shaders/flat/preamble.ts +7 -0
  129. package/src/render/shaders/fog.ts +81 -32
  130. package/src/render/shaders/generated/arcane.wgsl.ts +29 -39
  131. package/src/render/shaders/generated/bolt.wgsl.ts +26 -36
  132. package/src/render/shaders/generated/caustics.wgsl.ts +25 -35
  133. package/src/render/shaders/generated/film.wgsl.ts +28 -38
  134. package/src/render/shaders/generated/fire.wgsl.ts +28 -38
  135. package/src/render/shaders/generated/flat.wgsl.ts +1489 -1569
  136. package/src/render/shaders/generated/globalMedium.wgsl.ts +98 -12
  137. package/src/render/shaders/generated/line.wgsl.ts +25 -35
  138. package/src/render/shaders/generated/particle.wgsl.ts +100 -140
  139. package/src/render/shaders/generated/rush.wgsl.ts +20 -10
  140. package/src/render/shaders/generated/scatter.wgsl.ts +25 -35
  141. package/src/render/shaders/generated/smoke.wgsl.ts +28 -38
  142. package/src/render/shaders/generated/water.wgsl.ts +29 -39
  143. package/src/render/shaders/globalMedium.ts +114 -10
  144. package/src/render/shaders/gpudriven/blendRaster.wgsl.ts +2 -1
  145. package/src/render/shaders/gpudriven/fog.wgsl.ts +78 -19
  146. package/src/render/shaders/gpudriven/frameBlock.wgsl.ts +12 -1
  147. package/src/render/shaders/gpudriven/shade.wgsl.ts +3 -1
  148. package/src/render/shaders/line.ts +4 -2
  149. package/src/render/shaders/localExposure.ts +115 -6
  150. package/src/render/shaders/particle.ts +10 -6
  151. package/src/render/shaders/scatter.ts +4 -2
  152. package/src/render/shaders/water.ts +2 -1
  153. package/src/render/surfaceModel.ts +41 -6
@@ -24,7 +24,9 @@ import { Camera } from '../../camera.js';
24
24
  import { PickableSet } from '../../pickable.js';
25
25
  import { GpuTimestamps } from './gpuTimestamps.js';
26
26
  import { isWeakGpuFamily } from '../../gpuCapability.js';
27
- import { resolveAtmosphere } from '../../atmosphere.js';
27
+ import { writeMediumFields, writeMediumSlot } from './mediumFields.js';
28
+ import { noEnvironmentMixing, resolveEnvironmentMixing } from '../../environmentMixing.js';
29
+ import { createResolvedAtmosphere, resolveAtmosphere, } from '../../atmosphere.js';
28
30
  import { AMBIENT_SH_FLOATS, packAmbientSH } from '../../ambientHarmonics.js';
29
31
  import { lightmapTexels, warnRegionsWithoutPage, warnUnpagedLightmap, withLightmapUvs, } from '../../lightmap.js';
30
32
  import { BLOOM_FILTER_RADIUS_UV, BLOOM_LEVELS, bloomLevelSizes, bloomThresholdOf, bloomProfileWarning, defaultBloomResponse, resolveBloomResponse, } from '../../bloomChain.js';
@@ -88,7 +90,7 @@ import { DEFAULT_FILMIC_CURVE, FILMIC_CLAMPED, FILMIC_CONSTANTS, FILMIC_WITHOUT_
88
90
  import { ExposurePass } from './exposurePass.js';
89
91
  import { AO_STORE } from '../../shaders/ambientOcclusion.js';
90
92
  import { clampAutoExposure } from '../../shaders/exposure.js';
91
- import { clampLocalExposure } from '../../shaders/localExposure.js';
93
+ import { LOCAL_EXPOSURE_FLOATS, resolveLocalExposure } from '../../shaders/localExposure.js';
92
94
  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, BLOOM_DOWNSAMPLE_FIELDS, BLOOM_DOWNSAMPLE_FRAG_WGSL, BLOOM_PREFILTER_FIELDS, BLOOM_PREFILTER_FRAG_WGSL, BLOOM_STAGE_SIZE, BLOOM_TEXTURES, BLOOM_UNIFORMS, BLOOM_UPSAMPLE_FIELDS, TINTED_ADD_BLEND, 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, FRAME_REFLECTION_BLEND, REFLECTION_OVER_BLEND, SSR_FRAG_FIELDS, SSR_FRAG_SIZE, SSR_MATERIAL_FIELDS, SSR_MATERIAL_RESOLVE_FRAG_WGSL, SSR_MATERIAL_SIZE, SSR_MATERIAL_TEXTURES, SSR_MATERIAL_UNIFORMS, SSR_RESOLVE_FRAG_WGSL, SSR_RESOLVE_TEXTURES, SSR_TEXTURES, SSR_TRACE_FRAG_WGSL, SSR_UNIFORMS, createOitResolveLayout, createSsrResolveLayout, } from './postPass.js';
93
95
  import { GRADE_PLACEHOLDER_SIZE, identityGradeLut, validateGradeLut, } from '../../colourGrade.js';
94
96
  import { FILM_LOOK_WITHOUT_COMPOSITE, clampGrain, clampVignette, grainSeed, } from '../../shaders/filmLook.js';
@@ -102,9 +104,9 @@ import { buildPlumeGeometry } from '../../plumeGeometry.js';
102
104
  import { facingCode } from '../../particleBatch.js';
103
105
  import { PARTICLE_INDICES } from '../../shaders/particle.js';
104
106
  import { createScatterDeform, resolveScatterDeform, } from '../../scatterDeform.js';
105
- import { resolvePointLights } from '../../lightBudget.js';
107
+ import { createResolvedPointLights, resolvePointLights, } from '../../lightBudget.js';
106
108
  import { resolveRenderQuality } from '../../renderQuality.js';
107
- import { DEFAULT_GLOBAL_MEDIUM, mediumActive, mediumTargetSize, WEAK_GPU_MEDIUM_STEPS, resolveGlobalMedium, } from '../../globalMedium.js';
109
+ import { DEFAULT_GLOBAL_MEDIUM, WEAK_GPU_MEDIUM_STEPS, createResolvedMediumLamps, mediumActive, mediumTargetSize, resolveGlobalMedium, resolveGlobalMediumSettings, resolveMediumLamps, } from '../../globalMedium.js';
108
110
  import { castsDepth, glassOf } from '../../shadowCasters.js';
109
111
  import { materialHasMaps, noteMapsWithoutUvs } from '../../mapsWithoutUvs.js';
110
112
  import { VIEW_MODEL_DEPTH_SHARE, viewModelDepthRange } from '../../viewModel.js';
@@ -1532,7 +1534,10 @@ export class WebGPURenderer {
1532
1534
  /** The inverse projection the composite linearises a depth with, built once per frame. */
1533
1535
  mediumInvProjection = mat4.create();
1534
1536
  /** This frame's medium. Off until a caller says otherwise. See `setGlobalMedium`. */
1535
- mediumOptions = DEFAULT_GLOBAL_MEDIUM;
1537
+ mediumOptions = resolveGlobalMedium(DEFAULT_GLOBAL_MEDIUM);
1538
+ /** The lamps the medium is lit by, and the lit stage's lamps they come from, refilled a frame. */
1539
+ mediumLamps = createResolvedMediumLamps();
1540
+ mediumLights = createResolvedPointLights();
1536
1541
  /**
1537
1542
  * The temporal resolve: its block, its ping-pong pair, and the jitter state.
1538
1543
  *
@@ -2194,16 +2199,7 @@ export class WebGPURenderer {
2194
2199
  f[atF('uFilmRoughness')] = Math.min(1, Math.max(0, options.roughness ?? 0));
2195
2200
  f[atF('uFilmRoughnessCycles')] = Math.max(0.01, options.roughnessCyclesPerMetre ?? 60);
2196
2201
  const medium = resolveAtmosphere(env, this.atmosphereHeight(camera), this.quality.underwaterAtmosphere, this.medium);
2197
- f.set(medium.fogColor, atF('uFogColor'));
2198
- f[atF('uFogDensity')] = medium.fogDensity;
2199
- f[atF('uFogHeightFalloff')] = medium.fogHeightFalloff;
2200
- f[atF('uFogEyeY')] = medium.fogEyeY;
2201
- f.set(medium.underwaterColor, atF('uUnderwaterColor'));
2202
- f[atF('uUnderwaterFogDensity')] = medium.underwaterFogDensity;
2203
- f[atF('uUnderwaterFactor')] = medium.underwaterFactor;
2204
- i[atF('uFogMode')] = medium.fogMode;
2205
- f[atF('uFogNear')] = medium.fogNear;
2206
- f[atF('uFogFar')] = medium.fogFar;
2202
+ writeMediumFields(FILM_FRAG_FIELDS, f, medium);
2207
2203
  this.filmFrags.writeBlock(fragmentSlot, this.filmFragInts);
2208
2204
  const filmPipe = filmPipeline(this.blendedPipelines(), this.surface.device, this.filmLayout, `film|${geometry.key ?? ''}`, this.presentOf(geometry));
2209
2205
  /* The mirror is an attachment right now, so the group that does not hold it. */
@@ -2931,6 +2927,15 @@ export class WebGPURenderer {
2931
2927
  this.passEnvironmentGain = Math.max(0, gain);
2932
2928
  this.writeEnvironmentDials();
2933
2929
  }
2930
+ /** The environment brought to the light where a surface stands. See `renderer.ts`. */
2931
+ setEnvironmentMixing(weight, roughnessLow = 0.1, roughnessHigh = 0.3) {
2932
+ resolveEnvironmentMixing(weight, roughnessLow, roughnessHigh, this.environmentMixing);
2933
+ this.material((f) => {
2934
+ f.set(this.environmentMixing, this.materialField('uEnvironmentMixing'));
2935
+ });
2936
+ }
2937
+ /** `uEnvironmentMixing` as it stands; none until a draw asks. See `setEnvironmentMixing`. */
2938
+ environmentMixing = noEnvironmentMixing(new Float32Array(4));
2934
2939
  /**
2935
2940
  * The reflectivity and environment gain the setters gave the pass, and the ones the material
2936
2941
  * set now states, or null where it states none (`SurfaceMaterial.reflectivity`). A draw wears the
@@ -3716,16 +3721,7 @@ export class WebGPURenderer {
3716
3721
  scatterInto(f, fields['uLightWeight'], lights.weights, 1);
3717
3722
  }
3718
3723
  const medium = resolveAtmosphere(env, camera.position[1] ?? 0, this.quality.underwaterAtmosphere, this.medium);
3719
- f.set(medium.fogColor, atF('uFogColor'));
3720
- f[atF('uFogDensity')] = medium.fogDensity;
3721
- f[atF('uFogHeightFalloff')] = medium.fogHeightFalloff;
3722
- f[atF('uFogEyeY')] = medium.fogEyeY;
3723
- f.set(medium.underwaterColor, atF('uUnderwaterColor'));
3724
- f[atF('uUnderwaterFogDensity')] = medium.underwaterFogDensity;
3725
- f[atF('uUnderwaterFactor')] = medium.underwaterFactor;
3726
- i[atF('uFogMode')] = medium.fogMode;
3727
- f[atF('uFogNear')] = medium.fogNear;
3728
- f[atF('uFogFar')] = medium.fogFar;
3724
+ writeMediumFields(fields, f, medium);
3729
3725
  /* Only the mote fragment declares this — spark and smoke stay unconditionally fogged and
3730
3726
  have no such field. See ParticleBatchOptions.fog. */
3731
3727
  if (has('uFogEnabled'))
@@ -3929,16 +3925,7 @@ export class WebGPURenderer {
3929
3925
  frags.writeInt(fragmentSlot, atF('uClipEnabled'), this.reflectionPassActive ? 1 : 0);
3930
3926
  frags.writeFloats(fragmentSlot, atF('uClipPlane'), this.reflection?.clipPlane ?? NO_CLIP_PLANE);
3931
3927
  const medium = resolveAtmosphere(env, this.atmosphereHeight(camera), this.quality.underwaterAtmosphere, this.medium);
3932
- frags.writeFloats(fragmentSlot, atF('uFogColor'), medium.fogColor);
3933
- frags.writeFloat(fragmentSlot, atF('uFogDensity'), medium.fogDensity);
3934
- frags.writeFloat(fragmentSlot, atF('uFogHeightFalloff'), medium.fogHeightFalloff);
3935
- frags.writeFloat(fragmentSlot, atF('uFogEyeY'), medium.fogEyeY);
3936
- frags.writeFloats(fragmentSlot, atF('uUnderwaterColor'), medium.underwaterColor);
3937
- frags.writeFloat(fragmentSlot, atF('uUnderwaterFogDensity'), medium.underwaterFogDensity);
3938
- frags.writeFloat(fragmentSlot, atF('uUnderwaterFactor'), medium.underwaterFactor);
3939
- frags.writeInt(fragmentSlot, atF('uFogMode'), medium.fogMode);
3940
- frags.writeFloat(fragmentSlot, atF('uFogNear'), medium.fogNear);
3941
- frags.writeFloat(fragmentSlot, atF('uFogFar'), medium.fogFar);
3928
+ writeMediumSlot(frags, fragmentSlot, PLUME_FRAG_FIELDS, medium);
3942
3929
  frags.writeInt(fragmentSlot, atF('uOutputTransform'), this.passGrade());
3943
3930
  frags.writeFloat(fragmentSlot, atF('uOutputExposure'), this.passGradeExposure());
3944
3931
  /*
@@ -4079,16 +4066,7 @@ export class WebGPURenderer {
4079
4066
  scatterInto(f, WATER_FRAG_FIELDS['uLightWeight'], lights.weights, 1);
4080
4067
  scatterInto(f, WATER_FRAG_FIELDS['uLightCone'], lights.conesAndFalloffs, 4);
4081
4068
  const medium = resolveAtmosphere(env, camera.position[1] ?? 0, this.quality.underwaterAtmosphere, this.medium);
4082
- f.set(medium.fogColor, atF('uFogColor'));
4083
- f[atF('uFogDensity')] = medium.fogDensity;
4084
- f[atF('uFogHeightFalloff')] = medium.fogHeightFalloff;
4085
- f[atF('uFogEyeY')] = medium.fogEyeY;
4086
- f.set(medium.underwaterColor, atF('uUnderwaterColor'));
4087
- f[atF('uUnderwaterFogDensity')] = medium.underwaterFogDensity;
4088
- f[atF('uUnderwaterFactor')] = medium.underwaterFactor;
4089
- i[atF('uFogMode')] = medium.fogMode;
4090
- f[atF('uFogNear')] = medium.fogNear;
4091
- f[atF('uFogFar')] = medium.fogFar;
4069
+ writeMediumFields(WATER_FRAG_FIELDS, f, medium);
4092
4070
  /* No reflection pass yet: the shader's own "off" path, not an improvised one. */
4093
4071
  i[atF('uReflectionEnabled')] = mirror === null ? 0 : 1;
4094
4072
  i[atF('uReflectionFilterTaps')] = water.reflectionFilterTaps;
@@ -4587,16 +4565,7 @@ export class WebGPURenderer {
4587
4565
  f.set(camera.position, atF('uCameraPos'));
4588
4566
  /* The medium, through the same resolver every other pass here binds from. */
4589
4567
  const medium = resolveAtmosphere(env, camera.position[1] ?? 0, this.quality.underwaterAtmosphere, this.medium);
4590
- f.set(medium.fogColor, atF('uFogColor'));
4591
- f[atF('uFogDensity')] = medium.fogDensity;
4592
- f[atF('uFogHeightFalloff')] = medium.fogHeightFalloff;
4593
- f[atF('uFogEyeY')] = medium.fogEyeY;
4594
- f.set(medium.underwaterColor, atF('uUnderwaterColor'));
4595
- f[atF('uUnderwaterFogDensity')] = medium.underwaterFogDensity;
4596
- f[atF('uUnderwaterFactor')] = medium.underwaterFactor;
4597
- i[atF('uFogMode')] = medium.fogMode;
4598
- f[atF('uFogNear')] = medium.fogNear;
4599
- f[atF('uFogFar')] = medium.fogFar;
4568
+ writeMediumFields(BOLT_FRAG_FIELDS, f, medium);
4600
4569
  this.boltVerts.writeBlock(vertexSlot, this.boltVertBlock);
4601
4570
  this.boltFrags.writeBlock(fragmentSlot, this.boltFragInts);
4602
4571
  const boltPipe = boltPipeline(this.blendedPipelines(), this.surface.device, this.boltLayout);
@@ -4699,16 +4668,7 @@ export class WebGPURenderer {
4699
4668
  this.lineFrags.writeFloat(fragmentSlot, atF('uOutputExposure'), this.passGradeExposure());
4700
4669
  /* The medium, through the same resolver every other pass here binds from. */
4701
4670
  const medium = resolveAtmosphere(env, camera.position[1] ?? 0, this.quality.underwaterAtmosphere, this.medium);
4702
- this.lineFrags.writeFloats(fragmentSlot, atF('uFogColor'), medium.fogColor);
4703
- this.lineFrags.writeFloat(fragmentSlot, atF('uFogDensity'), medium.fogDensity);
4704
- this.lineFrags.writeFloat(fragmentSlot, atF('uFogHeightFalloff'), medium.fogHeightFalloff);
4705
- this.lineFrags.writeFloat(fragmentSlot, atF('uFogEyeY'), medium.fogEyeY);
4706
- this.lineFrags.writeFloats(fragmentSlot, atF('uUnderwaterColor'), medium.underwaterColor);
4707
- this.lineFrags.writeFloat(fragmentSlot, atF('uUnderwaterFogDensity'), medium.underwaterFogDensity);
4708
- this.lineFrags.writeFloat(fragmentSlot, atF('uUnderwaterFactor'), medium.underwaterFactor);
4709
- this.lineFrags.writeInt(fragmentSlot, atF('uFogMode'), medium.fogMode);
4710
- this.lineFrags.writeFloat(fragmentSlot, atF('uFogNear'), medium.fogNear);
4711
- this.lineFrags.writeFloat(fragmentSlot, atF('uFogFar'), medium.fogFar);
4671
+ writeMediumSlot(this.lineFrags, fragmentSlot, LINE_FRAG_FIELDS, medium);
4712
4672
  const linePipe = linePipeline(this.blendedPipelines(), this.surface.device, this.lineLayout, additive);
4713
4673
  /* Two uniform blocks and three vertex buffers, and no attachment among them. */
4714
4674
  const lineCommand = this.recordBlended(0);
@@ -4855,39 +4815,11 @@ export class WebGPURenderer {
4855
4815
  this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uWindSpatialPhase.offset, d.spatialPhase);
4856
4816
  }
4857
4817
  /** Refilled per frame rather than allocated; see `resolvePointLights`. */
4858
- lights = {
4859
- count: 0,
4860
- falloff: 0,
4861
- positions: new Float32Array(0),
4862
- colors: new Float32Array(0),
4863
- radii: new Float32Array(0),
4864
- sourceRadii: new Float32Array(0),
4865
- weights: new Float32Array(0),
4866
- directions: new Float32Array(0),
4867
- coneCos: new Float32Array(0),
4868
- iesProfiles: new Float32Array(0),
4869
- iesAxes: new Float32Array(0),
4870
- cookies: new Float32Array(0),
4871
- heldSourceRadii: new Float32Array(MAX_POINT_LIGHTS),
4872
- axesAndChannels: new Float32Array(MAX_POINT_LIGHTS * 4),
4873
- conesAndFalloffs: new Float32Array(MAX_POINT_LIGHTS * 4),
4874
- heldWeights: new Float32Array(MAX_POINT_LIGHTS),
4875
- };
4818
+ lights = createResolvedPointLights();
4876
4819
  /** The fixed arm's rectangles as `uAreaLightSize` holds them. See `packAreaSizesAndDoors`. */
4877
4820
  areaSizesAndDoors = new Float32Array(MAX_AREA_LIGHTS * 4);
4878
4821
  /** Refilled per frame rather than allocated; see `resolveAtmosphere`. */
4879
- medium = {
4880
- fogColor: new Float32Array(3),
4881
- underwaterColor: new Float32Array(3),
4882
- fogDensity: 0,
4883
- fogHeightFalloff: 0,
4884
- fogEyeY: 0,
4885
- fogMode: 0,
4886
- fogNear: 0,
4887
- fogFar: 1,
4888
- underwaterFogDensity: 0,
4889
- underwaterFactor: 0,
4890
- };
4822
+ medium = createResolvedAtmosphere();
4891
4823
  /* -- Overlays: panels and text ------------------------------------------------------ */
4892
4824
  panelLayout;
4893
4825
  panelCorners;
@@ -6313,6 +6245,7 @@ export class WebGPURenderer {
6313
6245
  this.mediumLayout = createPostStageLayout(device, MEDIUM_UNIFORMS, MEDIUM_FRAG_SIZE, [
6314
6246
  { binding: MEDIUM_TEXTURES.uDepth, filterable: false },
6315
6247
  { binding: MEDIUM_TEXTURES.uSunShadows, filterable: false, dimension: '2d-array' },
6248
+ { binding: MEDIUM_TEXTURES.uPointShadows, filterable: false, dimension: '2d-array' },
6316
6249
  ]);
6317
6250
  this.mediumUniforms = device.createBuffer({
6318
6251
  label: 'post.mediumUniforms',
@@ -7365,16 +7298,7 @@ export class WebGPURenderer {
7365
7298
  * Found by a voxel scene that is the first in the tree to use `fogMode: 'linear'` on WebGPU.
7366
7299
  */
7367
7300
  const meshMedium = resolveAtmosphere(env, this.atmosphereHeight(camera), this.quality.underwaterAtmosphere, this.medium);
7368
- f.set(meshMedium.fogColor, at('uFogColor'));
7369
- f[at('uFogDensity')] = meshMedium.fogDensity;
7370
- f[at('uFogHeightFalloff')] = meshMedium.fogHeightFalloff;
7371
- f[at('uFogEyeY')] = meshMedium.fogEyeY;
7372
- i[at('uFogMode')] = meshMedium.fogMode;
7373
- f[at('uFogNear')] = meshMedium.fogNear;
7374
- f[at('uFogFar')] = meshMedium.fogFar;
7375
- f.set(meshMedium.underwaterColor, at('uUnderwaterColor'));
7376
- f[at('uUnderwaterFogDensity')] = meshMedium.underwaterFogDensity;
7377
- f[at('uUnderwaterFactor')] = meshMedium.underwaterFactor;
7301
+ writeMediumFields(this.fragment.fields, f, meshMedium);
7378
7302
  /*
7379
7303
  * The mirror's half-space, and only while the mirror is being drawn.
7380
7304
  *
@@ -7419,6 +7343,8 @@ export class WebGPURenderer {
7419
7343
  f[at('uWriteMode') + 2] = 0;
7420
7344
  /* 1, not 0: a multiplier's identity is one. See `setEnvironmentGain`. */
7421
7345
  f[at('uEnvironmentDials') + 2] = 1;
7346
+ /* And no mixing, which is stored less one. See `setEnvironmentMixing`. */
7347
+ f.set(noEnvironmentMixing(this.environmentMixing), at('uEnvironmentMixing'));
7422
7348
  /* No overlay until a draw sets one. See `setSurfaceOverlay`. */
7423
7349
  this.overlayFloats.fill(0);
7424
7350
  f.fill(0, at('uOverlay'), at('uOverlay') + OVERLAY_FLOATS);
@@ -7662,16 +7588,7 @@ export class WebGPURenderer {
7662
7588
  * drift `bindAtmosphere` was written to prevent.
7663
7589
  */
7664
7590
  const medium = resolveAtmosphere(env, this.atmosphereHeight(camera), this.quality.underwaterAtmosphere, this.medium);
7665
- f.set(medium.fogColor, at('uFogColor'));
7666
- f[at('uFogDensity')] = medium.fogDensity;
7667
- f[at('uFogHeightFalloff')] = medium.fogHeightFalloff;
7668
- f[at('uFogEyeY')] = medium.fogEyeY;
7669
- i[at('uFogMode')] = medium.fogMode;
7670
- f[at('uFogNear')] = medium.fogNear;
7671
- f[at('uFogFar')] = medium.fogFar;
7672
- f.set(medium.underwaterColor, at('uUnderwaterColor'));
7673
- f[at('uUnderwaterFogDensity')] = medium.underwaterFogDensity;
7674
- f[at('uUnderwaterFactor')] = medium.underwaterFactor;
7591
+ writeMediumFields(this.fragment.fields, f, medium);
7675
7592
  /*
7676
7593
  * The shadow terms, matching what `renderer.ts` binds at default quality.
7677
7594
  *
@@ -8976,16 +8893,7 @@ export class WebGPURenderer {
8976
8893
  f.set(env.ambient, atF('uAmbient'));
8977
8894
  f.set(camera.position, atF('uCameraPos'));
8978
8895
  const medium = resolveAtmosphere(env, camera.position[1] ?? 0, this.quality.underwaterAtmosphere, this.medium);
8979
- f.set(medium.fogColor, atF('uFogColor'));
8980
- f[atF('uFogDensity')] = medium.fogDensity;
8981
- f[atF('uFogHeightFalloff')] = medium.fogHeightFalloff;
8982
- f[atF('uFogEyeY')] = medium.fogEyeY;
8983
- f.set(medium.underwaterColor, atF('uUnderwaterColor'));
8984
- f[atF('uUnderwaterFogDensity')] = medium.underwaterFogDensity;
8985
- f[atF('uUnderwaterFactor')] = medium.underwaterFactor;
8986
- i[atF('uFogMode')] = medium.fogMode;
8987
- f[atF('uFogNear')] = medium.fogNear;
8988
- f[atF('uFogFar')] = medium.fogFar;
8896
+ writeMediumFields(SCATTER_FRAG_FIELDS, f, medium);
8989
8897
  i[atF('uOutputTransform')] = this.passGrade();
8990
8898
  f[atF('uOutputExposure')] = this.passGradeExposure();
8991
8899
  const fragmentSlot = this.scatterFrags.allocate();
@@ -9799,6 +9707,8 @@ export class WebGPURenderer {
9799
9707
  * resolve, which is what sampling a stand-in does.
9800
9708
  */
9801
9709
  this.rebuildFlatBindGroup();
9710
+ /* The medium reads the same array: see `buildMediumBindGroup`. */
9711
+ this.mediumBindGroup = this.buildMediumBindGroup();
9802
9712
  }
9803
9713
  this.pointShadows.prepareStaticMaps(lights.length);
9804
9714
  this.preparedPointLightCount = lights.length;
@@ -10041,6 +9951,8 @@ export class WebGPURenderer {
10041
9951
  if (old !== null) {
10042
9952
  this.retiredPointTexture = old;
10043
9953
  this.rebuildFlatBindGroup();
9954
+ /* The medium reads the same array: see `buildMediumBindGroup`. */
9955
+ this.mediumBindGroup = this.buildMediumBindGroup();
10044
9956
  }
10045
9957
  }
10046
9958
  }
@@ -10523,7 +10435,7 @@ export class WebGPURenderer {
10523
10435
  * it. The held brightness is kept for either, since local exposure moves regions relative to it.
10524
10436
  * See `setAutoExposure` and `setLocalExposure`.
10525
10437
  */
10526
- const localising = this.localExposure > 0;
10438
+ const localising = this.localExposure;
10527
10439
  const measuring = (this.autoExposure > 0 || localising) && this.exposurePass !== null;
10528
10440
  const adapting = measuring && this.autoExposure > 0;
10529
10441
  const shownScene = this.reconstructing ? this.reconShownView : this.sceneColorView;
@@ -10596,7 +10508,13 @@ export class WebGPURenderer {
10596
10508
  f[at('uVignette')] = this.vignette;
10597
10509
  f.set(this.fringe, at('uFringe'));
10598
10510
  f[at('uAutoExposure')] = adapting ? this.autoExposure : 0;
10599
- f[at('uLocalExposure')] = measuring && localising ? this.localExposure : 0;
10511
+ f[at('uLocalExposure')] = measuring && localising ? 1 : 0;
10512
+ const shapeAt = at('uLocalShape');
10513
+ const pivotAt = at('uLocalPivot');
10514
+ for (let k = 0; k < 4; k++) {
10515
+ f[shapeAt + k] = this.localExposureShape[k];
10516
+ f[pivotAt + k] = this.localExposureShape[4 + k];
10517
+ }
10600
10518
  f[at('uGrain')] = this.grain;
10601
10519
  f[at('uGrain') + 1] = this.grainSeed;
10602
10520
  device.queue.writeBuffer(this.rushUniforms, 0, this.rushStaging);
@@ -11810,7 +11728,17 @@ export class WebGPURenderer {
11810
11728
  f.set(env?.directionalColor ?? [0, 0, 0], at('uSunColor'));
11811
11729
  f.set(env?.ambient ?? [0, 0, 0], at('uAmbient'));
11812
11730
  f[at('uDensity')] = options.density;
11813
- f[at('uAlbedo')] = options.albedo;
11731
+ const height = at('uHeight');
11732
+ f[height] = options.heightFalloff;
11733
+ f[height + 1] = options.baseY;
11734
+ const layer2 = at('uLayer2');
11735
+ f[layer2] = options.density2;
11736
+ f[layer2 + 1] = options.heightFalloff2;
11737
+ f[layer2 + 2] = options.baseY2;
11738
+ f.set(options.albedo, at('uAlbedo'));
11739
+ const scatter = at('uScatter');
11740
+ f[scatter] = options.sunScatter;
11741
+ f[scatter + 1] = options.skyScatter;
11814
11742
  f[at('uAnisotropy')] = options.anisotropy;
11815
11743
  f[at('uMaxDistance')] = options.maxDistance;
11816
11744
  ints[at('uSteps')] = this.quality.globalMediumSteps;
@@ -11828,6 +11756,20 @@ export class WebGPURenderer {
11828
11756
  f.set(env?.lightViewProj ?? this.mediumIdentity, at('uLightViewProj'));
11829
11757
  f.set((env === null ? this.mediumIdentity : this.movingLightFor(env)), at('uMovingLightViewProj'));
11830
11758
  ints[at('uPeeledShadowEnabled')] = this.peelView === null || !this.peelFilled ? 0 : 1;
11759
+ /* The lit stage's lamps and the shadows it resolved for them, this frame's. */
11760
+ const lamps = resolveMediumLamps(options, resolvePointLights(env, this.quality.pointLightFalloff, this.mediumLights), this.pointShadows === null ? null : this.resolvedPointShadows, env?.lightMediumScatter, this.mediumLamps);
11761
+ ints[at('uLampCount')] = lamps.count;
11762
+ ints[at('uLampFalloff')] = lamps.falloff;
11763
+ if (lamps.count > 0) {
11764
+ f.set(lamps.position, at('uLampPosition'));
11765
+ f.set(lamps.color, at('uLampColor'));
11766
+ f.set(lamps.aim, at('uLampAim'));
11767
+ f.set(lamps.cone, at('uLampCone'));
11768
+ f.set(lamps.shadow, at('uLampShadow'));
11769
+ f.set(lamps.shadowAt, at('uLampShadowAt'));
11770
+ f.set(lamps.live, at('uLampLive'));
11771
+ f.set(lamps.liveAt, at('uLampLiveAt'));
11772
+ }
11831
11773
  device.queue.writeBuffer(this.mediumUniforms, 0, this.mediumStaging);
11832
11774
  const g = this.mediumUpsampleFloats;
11833
11775
  const upsampleAt = (name) => this.postField(MEDIUM_UPSAMPLE_FIELDS, name);
@@ -14236,8 +14178,9 @@ export class WebGPURenderer {
14236
14178
  /** Eye adaptation: how far, this frame's time step, and the pass once asked for. */
14237
14179
  autoExposure = 0;
14238
14180
  autoExposureDt = 0;
14239
- /** How far each region is brought toward the frame. See `setLocalExposure`. */
14240
- localExposure = 0;
14181
+ /** Whether regions move, and how, as `resolveLocalExposure` packs it. See `setLocalExposure`. */
14182
+ localExposure = false;
14183
+ localExposureShape = new Float32Array(LOCAL_EXPOSURE_FLOATS);
14241
14184
  exposurePass = null;
14242
14185
  /**
14243
14186
  * How strongly the lens darkens the frame's corners, held until changed: 0 is none, 0.5 is about
@@ -14387,12 +14330,7 @@ export class WebGPURenderer {
14387
14330
  * draws nothing, and the three optional arguments are held until changed.
14388
14331
  */
14389
14332
  setGlobalMedium(density, albedo, anisotropy, maxDistance) {
14390
- this.mediumOptions = resolveGlobalMedium({
14391
- density: Number.isFinite(density) ? density : 0,
14392
- albedo,
14393
- anisotropy,
14394
- maxDistance,
14395
- });
14333
+ this.mediumOptions = resolveGlobalMediumSettings(density, albedo, anisotropy, maxDistance);
14396
14334
  }
14397
14335
  /** How much speed blur the frame should resolve with, 0 to 1. */
14398
14336
  setSpeedRush(strength) {
@@ -14463,13 +14401,13 @@ export class WebGPURenderer {
14463
14401
  * the same setter over the same state, sharing eye adaptation's pass, which is built the first
14464
14402
  * time either is asked for.
14465
14403
  */
14466
- setLocalExposure(strength) {
14404
+ setLocalExposure(setting) {
14467
14405
  if (!this.hasComposite || !this.quality.hdrScene) {
14468
- this.warnLookWithoutComposite(strength);
14406
+ this.warnLookWithoutComposite(typeof setting === 'number' ? setting : 1);
14469
14407
  return;
14470
14408
  }
14471
- this.localExposure = clampLocalExposure(strength);
14472
- if (this.localExposure > 0)
14409
+ this.localExposure = resolveLocalExposure(setting, this.localExposureShape);
14410
+ if (this.localExposure)
14473
14411
  this.ensureExposurePass();
14474
14412
  }
14475
14413
  /** The exposure pass, built once; the composite's group holds placeholders until it exists. */
@@ -14606,16 +14544,7 @@ export class WebGPURenderer {
14606
14544
  f[atF('uWindDir') + 1] = inv === 0 ? 0 : windZ * inv;
14607
14545
  f[atF('uWaveGain')] = sea.steepness;
14608
14546
  const medium = resolveAtmosphere(env, this.atmosphereHeight(camera), this.quality.underwaterAtmosphere, this.medium);
14609
- f.set(medium.fogColor, atF('uFogColor'));
14610
- f[atF('uFogDensity')] = medium.fogDensity;
14611
- f[atF('uFogHeightFalloff')] = medium.fogHeightFalloff;
14612
- f[atF('uFogEyeY')] = medium.fogEyeY;
14613
- f.set(medium.underwaterColor, atF('uUnderwaterColor'));
14614
- f[atF('uUnderwaterFogDensity')] = medium.underwaterFogDensity;
14615
- f[atF('uUnderwaterFactor')] = medium.underwaterFactor;
14616
- i[atF('uFogMode')] = medium.fogMode;
14617
- f[atF('uFogNear')] = medium.fogNear;
14618
- f[atF('uFogFar')] = medium.fogFar;
14547
+ writeMediumFields(CAUSTICS_FRAG_FIELDS, f, medium);
14619
14548
  this.causticsVerts.writeBlock(vertexSlot, this.causticsVertBlock);
14620
14549
  this.causticsFrags.writeBlock(fragmentSlot, this.causticsFragInts);
14621
14550
  const causticsPipe = causticsPipeline(this.blendedPipelines(), this.surface.device, this.causticsLayout);
@@ -15556,6 +15485,13 @@ export class WebGPURenderer {
15556
15485
  { binding: MEDIUM_TEXTURES.uDepth.sampler, resource: this.postDepthSampler },
15557
15486
  { binding: MEDIUM_TEXTURES.uSunShadows.texture, resource: this.sunArrayView },
15558
15487
  { binding: MEDIUM_TEXTURES.uSunShadows.sampler, resource: this.volumeShadowSampler },
15488
+ /* The lamps' maps, the lit stage's own array, rebuilt here whenever that array is
15489
+ replaced; the white stand-in reads as nothing in the way, and no lamp reads it. */
15490
+ {
15491
+ binding: MEDIUM_TEXTURES.uPointShadows.texture,
15492
+ resource: this.pointShadowArray?.view ?? this.shadowBlankView,
15493
+ },
15494
+ { binding: MEDIUM_TEXTURES.uPointShadows.sampler, resource: this.volumeShadowSampler },
15559
15495
  ],
15560
15496
  });
15561
15497
  }
@@ -21,7 +21,7 @@ export declare const SCATTER_DEPTH_SIZE: 240;
21
21
  export declare const SCATTER_VERT_FIELDS: UniformFields;
22
22
  export declare const SCATTER_VERT_SIZE: 240;
23
23
  export declare const SCATTER_FRAG_FIELDS: UniformFields;
24
- export declare const SCATTER_FRAG_SIZE: 144;
24
+ export declare const SCATTER_FRAG_SIZE: 192;
25
25
  export declare function scatterVertexLayouts(): GPUVertexBufferLayout[];
26
26
  /** Geometry and placement on the device, behind the opaque handle the surface hands out. */
27
27
  export interface GpuScatter {
@@ -37,4 +37,4 @@
37
37
  * a reset, sits the round out while the rest of the set runs it.
38
38
  */
39
39
  export declare const CLOTH_WORKGROUP = 64;
40
- export declare const CLOTH_SOLVE_WGSL = "\n/* The set's: every garment's particles, records, skin matrices and colliders, and the step length\n they all share. */\nstruct Constants {\n count: u32,\n distances: u32,\n bendings: u32,\n joints: u32,\n colliders: u32,\n pad0: u32,\n pad1: u32,\n pad2: u32,\n h: f32,\n invH: f32,\n invH2: f32,\n pad3: f32,\n}\n\n/* Where this dispatch's round of records starts in paces, at a dynamic offset. */\nstruct Round {\n at: u32,\n pad0: u32,\n pad1: u32,\n pad2: u32,\n}\n\n/* One garment's numbers for one round: its step's, and its own that never change. */\nstruct Pace {\n fraction: f32,\n blend: f32,\n alpha: f32,\n carry: u32,\n /* Where the pose a step starts from and the one it moves toward sit in pose, in floats. */\n fromAt: u32,\n toAt: u32,\n /* 0 where the garment sits this round out. Not active, which WGSL reserves. */\n live: u32,\n /* 1 a max distance, 2 a backstop, 4 a frontstop: which limits the garment has at all. */\n limits: u32,\n /* The character's turn since the last step, by columns; origin.w is the share carried. */\n turn0: vec4<f32>,\n turn1: vec4<f32>,\n turn2: vec4<f32>,\n origin: vec4<f32>,\n shift: vec4<f32>,\n /* The frame's wind: one wind, sampled once a frame, so a step's rather than the set-up's. */\n wind: vec4<f32>,\n colliderBase: u32,\n colliders: u32,\n width: u32,\n /* The share of its offset from its skinned position a particle keeps a substep: the pull's. */\n pullKeep: f32,\n keep: f32,\n dragKeep: f32,\n maxDistanceScale: f32,\n margin: f32,\n gravity: vec4<f32>,\n}\n\n/* One colour of one kind of constraint across the set, at a dynamic offset; first resets the\n multipliers. Publishing reads one garment's particles as begin to end, and first is the garment. */\nstruct Batch {\n begin: u32,\n end: u32,\n first: u32,\n pad: u32,\n}\n\n@group(0) @binding(0) var<uniform> c: Constants;\n@group(0) @binding(1) var<uniform> round: Round;\n@group(0) @binding(2) var<uniform> batch: Batch;\n@group(0) @binding(3) var<storage, read_write> state: array<f32>;\n@group(0) @binding(4) var<storage, read_write> pose: array<f32>;\n@group(0) @binding(5) var<storage, read> statics: array<f32>;\n@group(0) @binding(6) var<storage, read> constraints: array<u32>;\n@group(0) @binding(7) var<storage, read_write> lambda: array<f32>;\n@group(0) @binding(8) var<storage, read> frame: array<f32>;\n@group(0) @binding(9) var<storage, read> paces: array<Pace>;\n\n/* The record of particle i's garment this round: which garment a particle is, in statics. */\nfn paceAt(i: u32) -> u32 {\n return round.at + u32(statics[29u * c.count + i]);\n}\n\nfn stateAt(section: u32, i: u32) -> u32 {\n return section * 3u * c.count + i * 3u;\n}\n\nfn load(at: u32) -> vec3<f32> {\n return vec3<f32>(state[at], state[at + 1u], state[at + 2u]);\n}\n\nfn store(at: u32, v: vec3<f32>) {\n state[at] = v.x;\n state[at + 1u] = v.y;\n state[at + 2u] = v.z;\n}\n\nfn poseOf(at: u32) -> vec3<f32> {\n return vec3<f32>(pose[at], pose[at + 1u], pose[at + 2u]);\n}\n\n/* Sections of state. */\nconst POSITION = 0u;\nconst PREVIOUS = 1u;\nconst VELOCITY = 2u;\nconst START = 3u;\n\nfn inverseMass(i: u32) -> f32 {\n return statics[i];\n}\n\n/* Where skinning puts particle i, fraction of the way from the last pose to the latest. */\nfn skinnedAt(i: u32, g: u32) -> vec3<f32> {\n let a = poseOf(paces[g].fromAt + i * 3u);\n let b = poseOf(paces[g].toAt + i * 3u);\n return a + (b - a) * paces[g].fraction;\n}\n\nfn skinMatrix(j: u32) -> mat4x4<f32> {\n let at = j * 16u;\n return mat4x4<f32>(\n vec4<f32>(frame[at], frame[at + 1u], frame[at + 2u], frame[at + 3u]),\n vec4<f32>(frame[at + 4u], frame[at + 5u], frame[at + 6u], frame[at + 7u]),\n vec4<f32>(frame[at + 8u], frame[at + 9u], frame[at + 10u], frame[at + 11u]),\n vec4<f32>(frame[at + 12u], frame[at + 13u], frame[at + 14u], frame[at + 15u]));\n}\n\n/* skinParticles: each particle's skinned position and normal by up to eight influences. */\n@compute @workgroup_size(64)\nfn clothPose(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let n = c.count;\n let restAt = 7u * n + i * 3u;\n let normalAt = 10u * n + i * 3u;\n let x = vec4<f32>(statics[restAt], statics[restAt + 1u], statics[restAt + 2u], 1.0);\n let q0 = vec3<f32>(statics[normalAt], statics[normalAt + 1u], statics[normalAt + 2u]);\n var p = vec3<f32>(0.0);\n var q = vec3<f32>(0.0);\n for (var half = 0u; half < 2u; half++) {\n for (var k = 0u; k < 4u; k++) {\n let at = 13u * n + i * 16u + half * 8u;\n let w = statics[at + 4u + k];\n if (w == 0.0) {\n continue;\n }\n let m = skinMatrix(u32(statics[at + k]));\n p += w * (m * x).xyz;\n q += w * (mat3x3<f32>(m[0].xyz, m[1].xyz, m[2].xyz) * q0);\n }\n }\n let to = paces[g].toAt + i * 3u;\n pose[to] = p.x;\n pose[to + 1u] = p.y;\n pose[to + 2u] = p.z;\n let len = sqrt(dot(q, q));\n let inv = select(0.0, 1.0 / len, len > 1e-12);\n let normal = 6u * n + i * 3u;\n pose[normal] = q.x * inv;\n pose[normal + 1u] = q.y * inv;\n pose[normal + 2u] = q.z * inv;\n}\n\n/* SkinnedCloth.rest: every particle at the latest skinned position, at rest. */\n@compute @workgroup_size(64)\nfn clothRest(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let p = poseOf(paces[g].toAt + i * 3u);\n store(stateAt(POSITION, i), p);\n store(stateAt(START, i), p);\n store(stateAt(VELOCITY, i), vec3<f32>(0.0));\n}\n\n/* The step's start: the step start kept, a free particle carried, a kinematic one placed. */\n@compute @workgroup_size(64)\nfn clothBegin(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let at = stateAt(POSITION, i);\n let p = load(at);\n store(stateAt(START, i), p);\n if (inverseMass(i) == 0.0) {\n store(at, skinnedAt(i, g));\n return;\n }\n if (paces[g].carry == 0u) {\n return;\n }\n /* carryParticles: the share of the character's turn about its old origin, and of its move. */\n let turn = mat3x3<f32>(paces[g].turn0.xyz, paces[g].turn1.xyz, paces[g].turn2.xyz);\n let share = paces[g].origin.w;\n let x = p - paces[g].origin.xyz;\n store(at, p + (turn * x - x) * share + paces[g].shift.xyz);\n let vAt = stateAt(VELOCITY, i);\n let v = load(vAt);\n store(vAt, v + (turn * v - v) * share);\n}\n\n/* predictParticles. */\n@compute @workgroup_size(64)\nfn clothPredict(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let at = stateAt(POSITION, i);\n let p = load(at);\n store(stateAt(PREVIOUS, i), p);\n if (inverseMass(i) == 0.0) {\n return;\n }\n let vAt = stateAt(VELOCITY, i);\n let air = paces[g].wind.xyz;\n let v = air + (load(vAt) * paces[g].keep - air) * paces[g].dragKeep + paces[g].gravity.xyz * c.h;\n store(vAt, v);\n store(at, p + v * c.h);\n}\n\n/* solveDistanceBatch: one invocation a constraint of the batch, constraints sorted by batch. */\n@compute @workgroup_size(64)\nfn clothDistance(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n let a = constraints[k * 4u];\n let b = constraints[k * 4u + 1u];\n /* A garment sitting the round out solves nothing, and its next step's first pass resets. */\n if (paces[paceAt(a)].live == 0u) {\n return;\n }\n /* The first pass of a substep starts the multiplier at nothing, as the CPU's fill(0) does \u2014\n written before any early return, so a skipped constraint does not carry the last one. */\n if (batch.first == 1u) {\n lambda[k] = 0.0;\n }\n let wa = inverseMass(a);\n let wb = inverseMass(b);\n let w = wa + wb;\n if (w == 0.0) {\n return;\n }\n let aAt = stateAt(POSITION, a);\n let bAt = stateAt(POSITION, b);\n let d = load(bAt) - load(aAt);\n let len = sqrt(dot(d, d));\n if (len < 1e-9) {\n return;\n }\n let alpha = bitcast<f32>(constraints[k * 4u + 3u]) * c.invH2;\n let old = lambda[k];\n let delta = (-(len - bitcast<f32>(constraints[k * 4u + 2u])) - alpha * old) / (w + alpha);\n lambda[k] = old + delta;\n let s = delta / len;\n /* A pinned particle is shared across a batch, so it is never written, even with itself. */\n if (wa > 0.0) {\n store(aAt, load(aAt) - d * s * wa);\n }\n if (wb > 0.0) {\n store(bAt, load(bAt) + d * s * wb);\n }\n}\n\n/* solveBendingBatch, with the angle from atan2 (see the header). */\n@compute @workgroup_size(64)\nfn clothBending(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n let at = c.distances * 4u + k * 6u;\n let slot = c.distances + k;\n if (paces[paceAt(constraints[at])].live == 0u) {\n return;\n }\n if (batch.first == 1u) {\n lambda[slot] = 0.0;\n }\n let ids = vec4<u32>(constraints[at], constraints[at + 1u], constraints[at + 2u], constraints[at + 3u]);\n let w = vec4<f32>(inverseMass(ids.x), inverseMass(ids.y), inverseMass(ids.z), inverseMass(ids.w));\n if (w.x + w.y + w.z + w.w == 0.0) {\n return;\n }\n let origin = load(stateAt(POSITION, ids.x));\n let e = load(stateAt(POSITION, ids.y)) - origin;\n let p3 = load(stateAt(POSITION, ids.z)) - origin;\n let p4 = load(stateAt(POSITION, ids.w)) - origin;\n var n1 = cross(e, p3);\n var n2 = cross(e, p4);\n let l1 = sqrt(dot(n1, n1));\n let l2 = sqrt(dot(n2, n2));\n if (l1 < 1e-12 || l2 < 1e-12) {\n return;\n }\n n1 = n1 / l1;\n n2 = n2 / l2;\n let d = clamp(dot(n1, n2), -1.0, 1.0);\n let sine = sqrt(dot(cross(n1, n2), cross(n1, n2)));\n if (sine < 1e-6) {\n return;\n }\n /* The q vectors: the negative of the derivative of n1 \u00B7 n2. See solveBendingBatch. */\n let q3 = (cross(e, n2) + cross(n1, e) * d) / l1;\n let q4 = (cross(e, n1) + cross(n2, e) * d) / l2;\n let q2 = -((cross(p3, n2) + cross(n1, p3) * d) / l1 + (cross(p4, n1) + cross(n2, p4) * d) / l2);\n let q1 = -(q2 + q3 + q4);\n var sum = w.x * dot(q1, q1) + w.y * dot(q2, q2) + w.z * dot(q3, q3) + w.w * dot(q4, q4);\n sum = sum / (sine * sine);\n if (sum < 1e-12) {\n return;\n }\n let alpha = bitcast<f32>(constraints[at + 5u]) * c.invH2;\n let old = lambda[slot];\n let angle = atan2(sine, d);\n let delta = (-(angle - bitcast<f32>(constraints[at + 4u])) - alpha * old) / (sum + alpha);\n lambda[slot] = old + delta;\n let scale = delta / sine;\n if (w.x > 0.0) {\n let p = stateAt(POSITION, ids.x);\n store(p, load(p) + w.x * scale * q1);\n }\n if (w.y > 0.0) {\n let p = stateAt(POSITION, ids.y);\n store(p, load(p) + w.y * scale * q2);\n }\n if (w.z > 0.0) {\n let p = stateAt(POSITION, ids.z);\n store(p, load(p) + w.z * scale * q3);\n }\n if (w.w > 0.0) {\n let p = stateAt(POSITION, ids.w);\n store(p, load(p) + w.w * scale * q4);\n }\n}\n\n/* solveTethers, one invocation a tether of the batch: a batch holds a particle once. */\n@compute @workgroup_size(64)\nfn clothTether(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n let at = c.distances * 4u + c.bendings * 6u + k * 3u;\n if (paces[paceAt(constraints[at])].live == 0u) {\n return;\n }\n let pAt = stateAt(POSITION, constraints[at]);\n let anchor = load(stateAt(POSITION, constraints[at + 1u]));\n let d = load(pAt) - anchor;\n let dist = sqrt(dot(d, d));\n let reach = bitcast<f32>(constraints[at + 2u]);\n if (dist <= reach || dist < 1e-9) {\n return;\n }\n store(pAt, anchor + d * (reach / dist));\n}\n\n/* pushOut: the point onto the sphere's surface if it is inside. A point at the centre stays. */\nfn pushOut(p: vec3<f32>, centre: vec3<f32>, radius: f32) -> vec3<f32> {\n let d = p - centre;\n let d2 = dot(d, d);\n if (d2 >= radius * radius || d2 < 1e-18) {\n return p;\n }\n return centre + d * (radius / sqrt(d2));\n}\n\n/* applyPull, applyMaxDistance, applyStops (back, then front) and applyColliders, in that order. */\n@compute @workgroup_size(64)\nfn clothLimit(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count || inverseMass(i) == 0.0) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let limits = paces[g].limits;\n let n = c.count;\n let at = stateAt(POSITION, i);\n var p = load(at);\n let t = skinnedAt(i, g);\n /* Only where there is a pull, as on the CPU: t + (p - t) is not p in floats. */\n if (paces[g].pullKeep < 1.0) {\n p = t + (p - t) * paces[g].pullKeep;\n }\n /* A max distance below zero is none: the CPU's Infinity, which a device may not be handed. */\n let maxDistance = statics[n + i];\n if ((limits & 1u) != 0u && maxDistance >= 0.0) {\n let limit = maxDistance * paces[g].maxDistanceScale;\n let d = p - t;\n let d2 = dot(d, d);\n if (d2 > limit * limit) {\n p = t + d * (limit / sqrt(d2));\n }\n }\n let normal = poseOf(6u * n + i * 3u);\n if ((limits & 2u) != 0u) {\n let radius = statics[2u * n + i * 2u + 1u];\n if (radius > 0.0) {\n p = pushOut(p, t - normal * (statics[2u * n + i * 2u] + radius), radius);\n }\n }\n if ((limits & 4u) != 0u) {\n let radius = statics[4u * n + i * 2u + 1u];\n if (radius > 0.0) {\n p = pushOut(p, t + normal * (statics[4u * n + i * 2u] + radius), radius);\n }\n }\n let pad = statics[6u * n + i] + paces[g].margin;\n let ends = c.joints * 16u;\n let radii = ends + c.colliders * 6u;\n let first = paces[g].colliderBase;\n for (var k = first; k < first + paces[g].colliders; k++) {\n let a = vec3<f32>(frame[ends + k * 6u], frame[ends + k * 6u + 1u], frame[ends + k * 6u + 2u]);\n let e = vec3<f32>(frame[ends + k * 6u + 3u], frame[ends + k * 6u + 4u], frame[ends + k * 6u + 5u]) - a;\n let e2 = dot(e, e);\n var s = 0.0;\n if (e2 > 1e-18) {\n s = clamp(dot(p - a, e) / e2, 0.0, 1.0);\n }\n let r0 = frame[radii + k * 2u];\n let r1 = frame[radii + k * 2u + 1u];\n p = pushOut(p, a + e * s, r0 + (r1 - r0) * s + pad);\n }\n store(at, p);\n}\n\n/* finishParticles. */\n@compute @workgroup_size(64)\nfn clothFinish(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n if (paces[paceAt(i)].live == 0u) {\n return;\n }\n let vAt = stateAt(VELOCITY, i);\n if (inverseMass(i) == 0.0) {\n store(vAt, vec3<f32>(0.0));\n return;\n }\n store(vAt, (load(stateAt(POSITION, i)) - load(stateAt(PREVIOUS, i))) * c.invH);\n}\n\n/* SkinnedCloth.blend: after a reset, the simulation's share grows over the blend's steps. */\n@compute @workgroup_size(64)\nfn clothBlend(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n /* A blend of 0 is none, not a last step of one: see ClothDevice.step. */\n let g = paceAt(i);\n let blend = paces[g].blend;\n if (paces[g].live == 0u || blend <= 0.0) {\n return;\n }\n let t = skinnedAt(i, g);\n let at = stateAt(POSITION, i);\n store(at, t + (load(at) - t) * blend);\n let vAt = stateAt(VELOCITY, i);\n store(vAt, load(vAt) * blend);\n}\n\n@group(1) @binding(0) var output: texture_storage_2d<rgba32float, write>;\n\n/* SkinnedCloth.interpolate, into the particle texture the vertex stage reads: one garment's, whose\n particles are batch.begin to batch.end and whose record is batch.first's. */\n@compute @workgroup_size(64)\nfn clothPublish(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = batch.begin + id.x;\n if (i >= batch.end) {\n return;\n }\n let g = round.at + batch.first;\n let a = load(stateAt(START, i));\n let p = a + (load(stateAt(POSITION, i)) - a) * paces[g].alpha;\n let local = id.x;\n let width = paces[g].width;\n textureStore(output, vec2<u32>(local % width, local / width), vec4<f32>(p, 0.0));\n}\n";
40
+ export declare const CLOTH_SOLVE_WGSL = "\n/* The set's: every garment's particles, records, skin matrices and colliders, and the step length\n they all share. */\nstruct Constants {\n count: u32,\n distances: u32,\n bendings: u32,\n joints: u32,\n colliders: u32,\n pad0: u32,\n pad1: u32,\n pad2: u32,\n h: f32,\n invH: f32,\n invH2: f32,\n pad3: f32,\n}\n\n/* Where this dispatch's round of records starts in paces, at a dynamic offset. */\nstruct Round {\n at: u32,\n pad0: u32,\n pad1: u32,\n pad2: u32,\n}\n\n/* One garment's numbers for one round: its step's, and its own that never change. */\nstruct Pace {\n fraction: f32,\n blend: f32,\n alpha: f32,\n carry: u32,\n /* Where the pose a step starts from and the one it moves toward sit in pose, in floats. */\n fromAt: u32,\n toAt: u32,\n /* 0 where the garment sits this round out. Not active, which WGSL reserves. */\n live: u32,\n /* 1 a max distance, 2 a backstop, 4 a frontstop: which limits the garment has at all. */\n limits: u32,\n /* The character's turn since the last step, by columns; origin.w is the share carried. */\n turn0: vec4<f32>,\n turn1: vec4<f32>,\n turn2: vec4<f32>,\n origin: vec4<f32>,\n shift: vec4<f32>,\n /* The frame's wind: one wind, sampled once a frame, so a step's rather than the set-up's. */\n wind: vec4<f32>,\n colliderBase: u32,\n colliders: u32,\n width: u32,\n /* The garment's pull as a substep's stiffness, pull \u00B7 h\u00B2: a particle weighted w keeps\n 1 / (1 + w \u00B7 pullStep) of its offset from its skinned position. */\n pullStep: f32,\n keep: f32,\n dragKeep: f32,\n maxDistanceScale: f32,\n margin: f32,\n gravity: vec4<f32>,\n}\n\n/* One colour of one kind of constraint across the set, at a dynamic offset; first resets the\n multipliers. Publishing reads one garment's particles as begin to end, and first is the garment. */\nstruct Batch {\n begin: u32,\n end: u32,\n first: u32,\n pad: u32,\n}\n\n@group(0) @binding(0) var<uniform> c: Constants;\n@group(0) @binding(1) var<uniform> round: Round;\n@group(0) @binding(2) var<uniform> batch: Batch;\n@group(0) @binding(3) var<storage, read_write> state: array<f32>;\n@group(0) @binding(4) var<storage, read_write> pose: array<f32>;\n@group(0) @binding(5) var<storage, read> statics: array<f32>;\n@group(0) @binding(6) var<storage, read> constraints: array<u32>;\n@group(0) @binding(7) var<storage, read_write> lambda: array<f32>;\n@group(0) @binding(8) var<storage, read> frame: array<f32>;\n@group(0) @binding(9) var<storage, read> paces: array<Pace>;\n\n/* The record of particle i's garment this round: which garment a particle is, in statics. */\nfn paceAt(i: u32) -> u32 {\n return round.at + u32(statics[30u * c.count + i]);\n}\n\nfn stateAt(section: u32, i: u32) -> u32 {\n return section * 3u * c.count + i * 3u;\n}\n\nfn load(at: u32) -> vec3<f32> {\n return vec3<f32>(state[at], state[at + 1u], state[at + 2u]);\n}\n\nfn store(at: u32, v: vec3<f32>) {\n state[at] = v.x;\n state[at + 1u] = v.y;\n state[at + 2u] = v.z;\n}\n\nfn poseOf(at: u32) -> vec3<f32> {\n return vec3<f32>(pose[at], pose[at + 1u], pose[at + 2u]);\n}\n\n/* Sections of state. */\nconst POSITION = 0u;\nconst PREVIOUS = 1u;\nconst VELOCITY = 2u;\nconst START = 3u;\n\nfn inverseMass(i: u32) -> f32 {\n return statics[i];\n}\n\n/* Where skinning puts particle i, fraction of the way from the last pose to the latest. */\nfn skinnedAt(i: u32, g: u32) -> vec3<f32> {\n let a = poseOf(paces[g].fromAt + i * 3u);\n let b = poseOf(paces[g].toAt + i * 3u);\n return a + (b - a) * paces[g].fraction;\n}\n\nfn skinMatrix(j: u32) -> mat4x4<f32> {\n let at = j * 16u;\n return mat4x4<f32>(\n vec4<f32>(frame[at], frame[at + 1u], frame[at + 2u], frame[at + 3u]),\n vec4<f32>(frame[at + 4u], frame[at + 5u], frame[at + 6u], frame[at + 7u]),\n vec4<f32>(frame[at + 8u], frame[at + 9u], frame[at + 10u], frame[at + 11u]),\n vec4<f32>(frame[at + 12u], frame[at + 13u], frame[at + 14u], frame[at + 15u]));\n}\n\n/* skinParticles: each particle's skinned position and normal by up to eight influences. */\n@compute @workgroup_size(64)\nfn clothPose(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let n = c.count;\n let restAt = 7u * n + i * 3u;\n let normalAt = 10u * n + i * 3u;\n let x = vec4<f32>(statics[restAt], statics[restAt + 1u], statics[restAt + 2u], 1.0);\n let q0 = vec3<f32>(statics[normalAt], statics[normalAt + 1u], statics[normalAt + 2u]);\n var p = vec3<f32>(0.0);\n var q = vec3<f32>(0.0);\n for (var half = 0u; half < 2u; half++) {\n for (var k = 0u; k < 4u; k++) {\n let at = 13u * n + i * 16u + half * 8u;\n let w = statics[at + 4u + k];\n if (w == 0.0) {\n continue;\n }\n let m = skinMatrix(u32(statics[at + k]));\n p += w * (m * x).xyz;\n q += w * (mat3x3<f32>(m[0].xyz, m[1].xyz, m[2].xyz) * q0);\n }\n }\n let to = paces[g].toAt + i * 3u;\n pose[to] = p.x;\n pose[to + 1u] = p.y;\n pose[to + 2u] = p.z;\n let len = sqrt(dot(q, q));\n let inv = select(0.0, 1.0 / len, len > 1e-12);\n let normal = 6u * n + i * 3u;\n pose[normal] = q.x * inv;\n pose[normal + 1u] = q.y * inv;\n pose[normal + 2u] = q.z * inv;\n}\n\n/* SkinnedCloth.rest: every particle at the latest skinned position, at rest. */\n@compute @workgroup_size(64)\nfn clothRest(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let p = poseOf(paces[g].toAt + i * 3u);\n store(stateAt(POSITION, i), p);\n store(stateAt(START, i), p);\n store(stateAt(VELOCITY, i), vec3<f32>(0.0));\n}\n\n/* The step's start: the step start kept, a free particle carried, a kinematic one placed. */\n@compute @workgroup_size(64)\nfn clothBegin(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let at = stateAt(POSITION, i);\n let p = load(at);\n store(stateAt(START, i), p);\n if (inverseMass(i) == 0.0) {\n store(at, skinnedAt(i, g));\n return;\n }\n if (paces[g].carry == 0u) {\n return;\n }\n /* carryParticles: the share of the character's turn about its old origin, and of its move. */\n let turn = mat3x3<f32>(paces[g].turn0.xyz, paces[g].turn1.xyz, paces[g].turn2.xyz);\n let share = paces[g].origin.w;\n let x = p - paces[g].origin.xyz;\n store(at, p + (turn * x - x) * share + paces[g].shift.xyz);\n let vAt = stateAt(VELOCITY, i);\n let v = load(vAt);\n store(vAt, v + (turn * v - v) * share);\n}\n\n/* predictParticles. */\n@compute @workgroup_size(64)\nfn clothPredict(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let at = stateAt(POSITION, i);\n let p = load(at);\n store(stateAt(PREVIOUS, i), p);\n if (inverseMass(i) == 0.0) {\n return;\n }\n let vAt = stateAt(VELOCITY, i);\n let air = paces[g].wind.xyz;\n let v = air + (load(vAt) * paces[g].keep - air) * paces[g].dragKeep + paces[g].gravity.xyz * c.h;\n store(vAt, v);\n store(at, p + v * c.h);\n}\n\n/* solveDistanceBatch: one invocation a constraint of the batch, constraints sorted by batch. */\n@compute @workgroup_size(64)\nfn clothDistance(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n let a = constraints[k * 4u];\n let b = constraints[k * 4u + 1u];\n /* A garment sitting the round out solves nothing, and its next step's first pass resets. */\n if (paces[paceAt(a)].live == 0u) {\n return;\n }\n /* The first pass of a substep starts the multiplier at nothing, as the CPU's fill(0) does \u2014\n written before any early return, so a skipped constraint does not carry the last one. */\n if (batch.first == 1u) {\n lambda[k] = 0.0;\n }\n let wa = inverseMass(a);\n let wb = inverseMass(b);\n let w = wa + wb;\n if (w == 0.0) {\n return;\n }\n let aAt = stateAt(POSITION, a);\n let bAt = stateAt(POSITION, b);\n let d = load(bAt) - load(aAt);\n let len = sqrt(dot(d, d));\n if (len < 1e-9) {\n return;\n }\n let alpha = bitcast<f32>(constraints[k * 4u + 3u]) * c.invH2;\n let old = lambda[k];\n let delta = (-(len - bitcast<f32>(constraints[k * 4u + 2u])) - alpha * old) / (w + alpha);\n lambda[k] = old + delta;\n let s = delta / len;\n /* A pinned particle is shared across a batch, so it is never written, even with itself. */\n if (wa > 0.0) {\n store(aAt, load(aAt) - d * s * wa);\n }\n if (wb > 0.0) {\n store(bAt, load(bAt) + d * s * wb);\n }\n}\n\n/* solveBendingBatch, with the angle from atan2 (see the header). */\n@compute @workgroup_size(64)\nfn clothBending(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n let at = c.distances * 4u + k * 6u;\n let slot = c.distances + k;\n if (paces[paceAt(constraints[at])].live == 0u) {\n return;\n }\n if (batch.first == 1u) {\n lambda[slot] = 0.0;\n }\n let ids = vec4<u32>(constraints[at], constraints[at + 1u], constraints[at + 2u], constraints[at + 3u]);\n let w = vec4<f32>(inverseMass(ids.x), inverseMass(ids.y), inverseMass(ids.z), inverseMass(ids.w));\n if (w.x + w.y + w.z + w.w == 0.0) {\n return;\n }\n let origin = load(stateAt(POSITION, ids.x));\n let e = load(stateAt(POSITION, ids.y)) - origin;\n let p3 = load(stateAt(POSITION, ids.z)) - origin;\n let p4 = load(stateAt(POSITION, ids.w)) - origin;\n var n1 = cross(e, p3);\n var n2 = cross(e, p4);\n let l1 = sqrt(dot(n1, n1));\n let l2 = sqrt(dot(n2, n2));\n if (l1 < 1e-12 || l2 < 1e-12) {\n return;\n }\n n1 = n1 / l1;\n n2 = n2 / l2;\n let d = clamp(dot(n1, n2), -1.0, 1.0);\n let sine = sqrt(dot(cross(n1, n2), cross(n1, n2)));\n if (sine < 1e-6) {\n return;\n }\n /* The q vectors: the negative of the derivative of n1 \u00B7 n2. See solveBendingBatch. */\n let q3 = (cross(e, n2) + cross(n1, e) * d) / l1;\n let q4 = (cross(e, n1) + cross(n2, e) * d) / l2;\n let q2 = -((cross(p3, n2) + cross(n1, p3) * d) / l1 + (cross(p4, n1) + cross(n2, p4) * d) / l2);\n let q1 = -(q2 + q3 + q4);\n var sum = w.x * dot(q1, q1) + w.y * dot(q2, q2) + w.z * dot(q3, q3) + w.w * dot(q4, q4);\n sum = sum / (sine * sine);\n if (sum < 1e-12) {\n return;\n }\n let alpha = bitcast<f32>(constraints[at + 5u]) * c.invH2;\n let old = lambda[slot];\n let angle = atan2(sine, d);\n let delta = (-(angle - bitcast<f32>(constraints[at + 4u])) - alpha * old) / (sum + alpha);\n lambda[slot] = old + delta;\n let scale = delta / sine;\n if (w.x > 0.0) {\n let p = stateAt(POSITION, ids.x);\n store(p, load(p) + w.x * scale * q1);\n }\n if (w.y > 0.0) {\n let p = stateAt(POSITION, ids.y);\n store(p, load(p) + w.y * scale * q2);\n }\n if (w.z > 0.0) {\n let p = stateAt(POSITION, ids.z);\n store(p, load(p) + w.z * scale * q3);\n }\n if (w.w > 0.0) {\n let p = stateAt(POSITION, ids.w);\n store(p, load(p) + w.w * scale * q4);\n }\n}\n\n/* solveTethers, one invocation a tether of the batch: a batch holds a particle once. */\n@compute @workgroup_size(64)\nfn clothTether(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n let at = c.distances * 4u + c.bendings * 6u + k * 3u;\n if (paces[paceAt(constraints[at])].live == 0u) {\n return;\n }\n let pAt = stateAt(POSITION, constraints[at]);\n let anchor = load(stateAt(POSITION, constraints[at + 1u]));\n let d = load(pAt) - anchor;\n let dist = sqrt(dot(d, d));\n let reach = bitcast<f32>(constraints[at + 2u]);\n if (dist <= reach || dist < 1e-9) {\n return;\n }\n store(pAt, anchor + d * (reach / dist));\n}\n\n/* pushOut: the point onto the sphere's surface if it is inside. A point at the centre stays. */\nfn pushOut(p: vec3<f32>, centre: vec3<f32>, radius: f32) -> vec3<f32> {\n let d = p - centre;\n let d2 = dot(d, d);\n if (d2 >= radius * radius || d2 < 1e-18) {\n return p;\n }\n return centre + d * (radius / sqrt(d2));\n}\n\n/* applyPull, applyMaxDistance, applyStops (back, then front) and applyColliders, in that order. */\n@compute @workgroup_size(64)\nfn clothLimit(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count || inverseMass(i) == 0.0) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let limits = paces[g].limits;\n let n = c.count;\n let at = stateAt(POSITION, i);\n var p = load(at);\n let t = skinnedAt(i, g);\n /* Only where there is a pull, as on the CPU: t + (p - t) is not p in floats. */\n let pull = statics[29u * n + i] * paces[g].pullStep;\n if (pull > 0.0) {\n p = t + (p - t) * (1.0 / (1.0 + pull));\n }\n /* A max distance below zero is none: the CPU's Infinity, which a device may not be handed. */\n let maxDistance = statics[n + i];\n if ((limits & 1u) != 0u && maxDistance >= 0.0) {\n let limit = maxDistance * paces[g].maxDistanceScale;\n let d = p - t;\n let d2 = dot(d, d);\n if (d2 > limit * limit) {\n p = t + d * (limit / sqrt(d2));\n }\n }\n let normal = poseOf(6u * n + i * 3u);\n if ((limits & 2u) != 0u) {\n let radius = statics[2u * n + i * 2u + 1u];\n if (radius > 0.0) {\n p = pushOut(p, t - normal * (statics[2u * n + i * 2u] + radius), radius);\n }\n }\n if ((limits & 4u) != 0u) {\n let radius = statics[4u * n + i * 2u + 1u];\n if (radius > 0.0) {\n p = pushOut(p, t + normal * (statics[4u * n + i * 2u] + radius), radius);\n }\n }\n let pad = statics[6u * n + i] + paces[g].margin;\n let ends = c.joints * 16u;\n let radii = ends + c.colliders * 6u;\n let first = paces[g].colliderBase;\n for (var k = first; k < first + paces[g].colliders; k++) {\n let a = vec3<f32>(frame[ends + k * 6u], frame[ends + k * 6u + 1u], frame[ends + k * 6u + 2u]);\n let e = vec3<f32>(frame[ends + k * 6u + 3u], frame[ends + k * 6u + 4u], frame[ends + k * 6u + 5u]) - a;\n let e2 = dot(e, e);\n var s = 0.0;\n if (e2 > 1e-18) {\n s = clamp(dot(p - a, e) / e2, 0.0, 1.0);\n }\n let r0 = frame[radii + k * 2u];\n let r1 = frame[radii + k * 2u + 1u];\n p = pushOut(p, a + e * s, r0 + (r1 - r0) * s + pad);\n }\n store(at, p);\n}\n\n/* finishParticles. */\n@compute @workgroup_size(64)\nfn clothFinish(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n if (paces[paceAt(i)].live == 0u) {\n return;\n }\n let vAt = stateAt(VELOCITY, i);\n if (inverseMass(i) == 0.0) {\n store(vAt, vec3<f32>(0.0));\n return;\n }\n store(vAt, (load(stateAt(POSITION, i)) - load(stateAt(PREVIOUS, i))) * c.invH);\n}\n\n/* SkinnedCloth.blend: after a reset, the simulation's share grows over the blend's steps. */\n@compute @workgroup_size(64)\nfn clothBlend(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n /* A blend of 0 is none, not a last step of one: see ClothDevice.step. */\n let g = paceAt(i);\n let blend = paces[g].blend;\n if (paces[g].live == 0u || blend <= 0.0) {\n return;\n }\n let t = skinnedAt(i, g);\n let at = stateAt(POSITION, i);\n store(at, t + (load(at) - t) * blend);\n let vAt = stateAt(VELOCITY, i);\n store(vAt, load(vAt) * blend);\n}\n\n@group(1) @binding(0) var output: texture_storage_2d<rgba32float, write>;\n\n/* SkinnedCloth.interpolate, into the particle texture the vertex stage reads: one garment's, whose\n particles are batch.begin to batch.end and whose record is batch.first's. */\n@compute @workgroup_size(64)\nfn clothPublish(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = batch.begin + id.x;\n if (i >= batch.end) {\n return;\n }\n let g = round.at + batch.first;\n let a = load(stateAt(START, i));\n let p = a + (load(stateAt(POSITION, i)) - a) * paces[g].alpha;\n let local = id.x;\n let width = paces[g].width;\n textureStore(output, vec2<u32>(local % width, local / width), vec4<f32>(p, 0.0));\n}\n";
@@ -87,8 +87,9 @@ struct Pace {
87
87
  colliderBase: u32,
88
88
  colliders: u32,
89
89
  width: u32,
90
- /* The share of its offset from its skinned position a particle keeps a substep: the pull's. */
91
- pullKeep: f32,
90
+ /* The garment's pull as a substep's stiffness, pull · h²: a particle weighted w keeps
91
+ 1 / (1 + w · pullStep) of its offset from its skinned position. */
92
+ pullStep: f32,
92
93
  keep: f32,
93
94
  dragKeep: f32,
94
95
  maxDistanceScale: f32,
@@ -118,7 +119,7 @@ struct Batch {
118
119
 
119
120
  /* The record of particle i's garment this round: which garment a particle is, in statics. */
120
121
  fn paceAt(i: u32) -> u32 {
121
- return round.at + u32(statics[29u * c.count + i]);
122
+ return round.at + u32(statics[30u * c.count + i]);
122
123
  }
123
124
 
124
125
  fn stateAt(section: u32, i: u32) -> u32 {
@@ -445,8 +446,9 @@ fn clothLimit(@builtin(global_invocation_id) id: vec3<u32>) {
445
446
  var p = load(at);
446
447
  let t = skinnedAt(i, g);
447
448
  /* Only where there is a pull, as on the CPU: t + (p - t) is not p in floats. */
448
- if (paces[g].pullKeep < 1.0) {
449
- p = t + (p - t) * paces[g].pullKeep;
449
+ let pull = statics[29u * n + i] * paces[g].pullStep;
450
+ if (pull > 0.0) {
451
+ p = t + (p - t) * (1.0 / (1.0 + pull));
450
452
  }
451
453
  /* A max distance below zero is none: the CPU's Infinity, which a device may not be handed. */
452
454
  let maxDistance = statics[n + i];
@@ -11,7 +11,7 @@ import type { UniformFields } from './scatterPass.ts';
11
11
  export declare const WATER_VERT_FIELDS: UniformFields;
12
12
  export declare const WATER_VERT_SIZE: 208;
13
13
  export declare const WATER_FRAG_FIELDS: UniformFields;
14
- export declare const WATER_FRAG_SIZE: 1520;
14
+ export declare const WATER_FRAG_SIZE: 1568;
15
15
  /** The two meshes and the numbers the grid decided, behind the surface's opaque handle. */
16
16
  export interface GpuWater {
17
17
  /** The ocean: near sheet and far skirt in one buffer. */
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The environment a surface reflects brought to the light where it stands: `setEnvironmentMixing`'s
3
+ * numbers as the lit stage reads them, `uEnvironmentMixing`. Both backends fill it here and upload
4
+ * it, so a clamp cannot differ between them.
5
+ *
6
+ * **The weight is stored less one**, so a program a binder never wrote, all zeros, reflects exactly
7
+ * as it did before the field existed; the two roughnesses beside it are where the weight is mixed in
8
+ * between, smooth to rough.
9
+ */
10
+ /** `uEnvironmentMixing` mixing nothing: a weight of 1, between the default roughnesses. */
11
+ export declare function noEnvironmentMixing(out: Float32Array): Float32Array;
12
+ /**
13
+ * A caller's weight and roughnesses, into `out`: a weight that is not a number at or above zero is
14
+ * 1, the roughnesses are held to 0 to 1, and the upper never below the lower.
15
+ */
16
+ export declare function resolveEnvironmentMixing(weight: number, roughnessLow: number, roughnessHigh: number, out: Float32Array): Float32Array;
17
+ /** The factor a surface of `roughness` reflects by, as the lit stage computes it. */
18
+ export declare function environmentMixingFactor(mixing: ArrayLike<number>, roughness: number): number;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The environment a surface reflects brought to the light where it stands: `setEnvironmentMixing`'s
3
+ * numbers as the lit stage reads them, `uEnvironmentMixing`. Both backends fill it here and upload
4
+ * it, so a clamp cannot differ between them.
5
+ *
6
+ * **The weight is stored less one**, so a program a binder never wrote, all zeros, reflects exactly
7
+ * as it did before the field existed; the two roughnesses beside it are where the weight is mixed in
8
+ * between, smooth to rough.
9
+ */
10
+ /** `uEnvironmentMixing` mixing nothing: a weight of 1, between the default roughnesses. */
11
+ export function noEnvironmentMixing(out) {
12
+ out[0] = 0;
13
+ out[1] = 0.1;
14
+ out[2] = 0.3;
15
+ out[3] = 0;
16
+ return out;
17
+ }
18
+ /**
19
+ * A caller's weight and roughnesses, into `out`: a weight that is not a number at or above zero is
20
+ * 1, the roughnesses are held to 0 to 1, and the upper never below the lower.
21
+ */
22
+ export function resolveEnvironmentMixing(weight, roughnessLow, roughnessHigh, out) {
23
+ const unit = (value, fallback) => Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : fallback;
24
+ out[0] = (Number.isFinite(weight) && weight >= 0 ? weight : 1) - 1;
25
+ const low = unit(roughnessLow, 0.1);
26
+ out[1] = low;
27
+ out[2] = Math.max(low, unit(roughnessHigh, 0.3));
28
+ out[3] = 0;
29
+ return out;
30
+ }
31
+ /** The factor a surface of `roughness` reflects by, as the lit stage computes it. */
32
+ export function environmentMixingFactor(mixing, roughness) {
33
+ const less = mixing[0];
34
+ if (less === 0)
35
+ return 1;
36
+ const low = mixing[1];
37
+ const span = Math.max(mixing[2] - low, 1e-4);
38
+ const t = Math.min(1, Math.max(0, (roughness - low) / span));
39
+ return 1 + less * t * t * (3 - 2 * t);
40
+ }