@woosh/meep-engine 2.165.2 → 2.165.4

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 (35) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/sphere/harmonics/sh3_convolve_with_zh.d.ts +21 -0
  3. package/src/core/geom/3d/sphere/harmonics/sh3_convolve_with_zh.d.ts.map +1 -0
  4. package/src/core/geom/3d/sphere/harmonics/sh3_convolve_with_zh.js +53 -0
  5. package/src/core/geom/3d/sphere/harmonics/sh3_ggx_zh.d.ts +18 -0
  6. package/src/core/geom/3d/sphere/harmonics/sh3_ggx_zh.d.ts.map +1 -0
  7. package/src/core/geom/3d/sphere/harmonics/sh3_ggx_zh.js +30 -0
  8. package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts +2 -0
  9. package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts.map +1 -1
  10. package/src/engine/graphics/ecs/trail2d/Trail2D.js +5 -2
  11. package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts.map +1 -1
  12. package/src/engine/graphics/ecs/trail2d/Trail2DSystem.js +3 -2
  13. package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts +2 -0
  14. package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts.map +1 -1
  15. package/src/engine/graphics/ecs/trail3d/Trail3D.js +5 -2
  16. package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts.map +1 -1
  17. package/src/engine/graphics/ecs/trail3d/Trail3DSystem.js +3 -2
  18. package/src/engine/graphics/sh3/lpg/LightProbeGridBaker.d.ts +43 -4
  19. package/src/engine/graphics/sh3/lpg/LightProbeGridBaker.d.ts.map +1 -1
  20. package/src/engine/graphics/sh3/lpg/LightProbeGridBaker.js +72 -5
  21. package/src/engine/graphics/sh3/lpg/bake_probe_sh3_resampled.d.ts +37 -0
  22. package/src/engine/graphics/sh3/lpg/bake_probe_sh3_resampled.d.ts.map +1 -0
  23. package/src/engine/graphics/sh3/lpg/bake_probe_sh3_resampled.js +298 -0
  24. package/src/engine/graphics/sh3/lpg/light_probe_grid_dilate.d.ts +18 -0
  25. package/src/engine/graphics/sh3/lpg/light_probe_grid_dilate.d.ts.map +1 -0
  26. package/src/engine/graphics/sh3/lpg/light_probe_grid_dilate.js +142 -0
  27. package/src/engine/graphics/sh3/path_tracer/PathTracer.d.ts +6 -0
  28. package/src/engine/graphics/sh3/path_tracer/PathTracer.d.ts.map +1 -1
  29. package/src/engine/graphics/sh3/path_tracer/PathTracer.js +1 -1
  30. package/src/engine/graphics/trail/tube/simulator/TubeXFixedPhysicsSimulator.d.ts +5 -3
  31. package/src/engine/graphics/trail/tube/simulator/TubeXFixedPhysicsSimulator.d.ts.map +1 -1
  32. package/src/engine/graphics/trail/tube/simulator/TubeXFixedPhysicsSimulator.js +6 -4
  33. package/src/engine/graphics/trail/x/simulator/RibbonXFixedPhysicsSimulator.d.ts +2 -1
  34. package/src/engine/graphics/trail/x/simulator/RibbonXFixedPhysicsSimulator.d.ts.map +1 -1
  35. package/src/engine/graphics/trail/x/simulator/RibbonXFixedPhysicsSimulator.js +3 -2
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "description": "Pure JavaScript game engine. Fully featured and production ready.",
7
7
  "type": "module",
8
8
  "author": "Alexander Goldring",
9
- "version": "2.165.2",
9
+ "version": "2.165.4",
10
10
  "main": "build/meep.module.js",
11
11
  "module": "build/meep.module.js",
12
12
  "exports": {
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Convolve 3-band spherical harmonic coefficients with a zonal harmonic kernel.
3
+ *
4
+ * Convolution with a circularly-symmetric (zonal) kernel is a per-band scale: band 0 is scaled by
5
+ * `zh0`, band 1 by `zh1` and band 2 by `zh2`. The kernel coefficients are expected to already include
6
+ * the `sqrt(4*PI / (2l + 1))` convolution normalization (as e.g. {@link sh3_ggx_zh} does).
7
+ *
8
+ * Layout is coefficient-major with `dimensions` interleaved channels per coefficient, matching
9
+ * {@link sh3_vector_accumulate}. Safe to use in-place (`out === input` with equal offsets).
10
+ *
11
+ * @param {number[]|Float32Array|Float64Array} out
12
+ * @param {number} out_offset
13
+ * @param {number[]|Float32Array|Float64Array} input
14
+ * @param {number} input_offset
15
+ * @param {number} zh0 band 0 scale
16
+ * @param {number} zh1 band 1 scale
17
+ * @param {number} zh2 band 2 scale
18
+ * @param {number} dimensions number of interleaved channels per coefficient, e.g. 3 for RGB
19
+ */
20
+ export function sh3_convolve_with_zh(out: number[] | Float32Array | Float64Array, out_offset: number, input: number[] | Float32Array | Float64Array, input_offset: number, zh0: number, zh1: number, zh2: number, dimensions: number): void;
21
+ //# sourceMappingURL=sh3_convolve_with_zh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sh3_convolve_with_zh.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/geom/3d/sphere/harmonics/sh3_convolve_with_zh.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AACH,0CATW,MAAM,EAAE,GAAC,YAAY,GAAC,YAAY,cAClC,MAAM,SACN,MAAM,EAAE,GAAC,YAAY,GAAC,YAAY,gBAClC,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,cACN,MAAM,QAiChB"}
@@ -0,0 +1,53 @@
1
+ import { assert } from "../../../../assert.js";
2
+
3
+ /**
4
+ * Convolve 3-band spherical harmonic coefficients with a zonal harmonic kernel.
5
+ *
6
+ * Convolution with a circularly-symmetric (zonal) kernel is a per-band scale: band 0 is scaled by
7
+ * `zh0`, band 1 by `zh1` and band 2 by `zh2`. The kernel coefficients are expected to already include
8
+ * the `sqrt(4*PI / (2l + 1))` convolution normalization (as e.g. {@link sh3_ggx_zh} does).
9
+ *
10
+ * Layout is coefficient-major with `dimensions` interleaved channels per coefficient, matching
11
+ * {@link sh3_vector_accumulate}. Safe to use in-place (`out === input` with equal offsets).
12
+ *
13
+ * @param {number[]|Float32Array|Float64Array} out
14
+ * @param {number} out_offset
15
+ * @param {number[]|Float32Array|Float64Array} input
16
+ * @param {number} input_offset
17
+ * @param {number} zh0 band 0 scale
18
+ * @param {number} zh1 band 1 scale
19
+ * @param {number} zh2 band 2 scale
20
+ * @param {number} dimensions number of interleaved channels per coefficient, e.g. 3 for RGB
21
+ */
22
+ export function sh3_convolve_with_zh(
23
+ out, out_offset,
24
+ input, input_offset,
25
+ zh0, zh1, zh2,
26
+ dimensions
27
+ ) {
28
+ assert.isArrayLike(out, 'out');
29
+ assert.isNonNegativeInteger(out_offset, 'out_offset');
30
+ assert.isArrayLike(input, 'input');
31
+ assert.isNonNegativeInteger(input_offset, 'input_offset');
32
+ assert.isNumber(zh0, 'zh0');
33
+ assert.isNumber(zh1, 'zh1');
34
+ assert.isNumber(zh2, 'zh2');
35
+ assert.isNonNegativeInteger(dimensions, 'dimensions');
36
+
37
+ const band_1_end = 4 * dimensions;
38
+ const band_2_end = 9 * dimensions;
39
+
40
+ let i = 0;
41
+
42
+ for (; i < dimensions; i++) {
43
+ out[out_offset + i] = input[input_offset + i] * zh0;
44
+ }
45
+
46
+ for (; i < band_1_end; i++) {
47
+ out[out_offset + i] = input[input_offset + i] * zh1;
48
+ }
49
+
50
+ for (; i < band_2_end; i++) {
51
+ out[out_offset + i] = input[input_offset + i] * zh2;
52
+ }
53
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Zonal harmonic coefficients approximating a GGX lobe of the given roughness.
3
+ *
4
+ * Convolving a radiance SH with these coefficients (see {@link sh3_convolve_with_zh}) low-passes it with
5
+ * a GGX-shaped cone: band 0 is preserved while bands 1 and 2 are attenuated progressively harder as
6
+ * `alpha` grows. Attenuating the directional bands tapers the SH reconstruction towards its mean,
7
+ * which suppresses ringing (negative lobes / light bleeding) at the cost of directional sharpness.
8
+ *
9
+ * The rational fit is from MJP's SH library.
10
+ *
11
+ * @see https://github.com/TheRealMJP/SHforHLSL/blob/e426058959123063e13d61a62df6217259b72cec/SH.hlsli#L394
12
+ *
13
+ * @param {number[]|Float32Array|Float64Array} out zonal coefficients [l0, l1, l2] are written here
14
+ * @param {number} out_offset
15
+ * @param {number} alpha GGX roughness (perceptual roughness squared) in [0, 1]; the fit is only valid in this range
16
+ */
17
+ export function sh3_ggx_zh(out: number[] | Float32Array | Float64Array, out_offset: number, alpha: number): void;
18
+ //# sourceMappingURL=sh3_ggx_zh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sh3_ggx_zh.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/geom/3d/sphere/harmonics/sh3_ggx_zh.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AACH,gCAJW,MAAM,EAAE,GAAC,YAAY,GAAC,YAAY,cAClC,MAAM,SACN,MAAM,QAahB"}
@@ -0,0 +1,30 @@
1
+ import { assert } from "../../../../assert.js";
2
+
3
+ /**
4
+ * Zonal harmonic coefficients approximating a GGX lobe of the given roughness.
5
+ *
6
+ * Convolving a radiance SH with these coefficients (see {@link sh3_convolve_with_zh}) low-passes it with
7
+ * a GGX-shaped cone: band 0 is preserved while bands 1 and 2 are attenuated progressively harder as
8
+ * `alpha` grows. Attenuating the directional bands tapers the SH reconstruction towards its mean,
9
+ * which suppresses ringing (negative lobes / light bleeding) at the cost of directional sharpness.
10
+ *
11
+ * The rational fit is from MJP's SH library.
12
+ *
13
+ * @see https://github.com/TheRealMJP/SHforHLSL/blob/e426058959123063e13d61a62df6217259b72cec/SH.hlsli#L394
14
+ *
15
+ * @param {number[]|Float32Array|Float64Array} out zonal coefficients [l0, l1, l2] are written here
16
+ * @param {number} out_offset
17
+ * @param {number} alpha GGX roughness (perceptual roughness squared) in [0, 1]; the fit is only valid in this range
18
+ */
19
+ export function sh3_ggx_zh(out, out_offset, alpha) {
20
+ assert.isArrayLike(out, 'out');
21
+ assert.isNonNegativeInteger(out_offset, 'out_offset');
22
+ assert.isNumber(alpha, 'alpha');
23
+ assert.notNaN(alpha, 'alpha');
24
+ assert.greaterThanOrEqual(alpha, 0, 'alpha');
25
+ assert.lessThanOrEqual(alpha, 1, 'alpha');
26
+
27
+ out[out_offset] = 1;
28
+ out[out_offset + 1] = 1.66711256633276 / (1.65715038133932 + alpha);
29
+ out[out_offset + 2] = 1.56127990596116 / (0.96989757593282 + alpha) - 0.599972342361123;
30
+ }
@@ -43,6 +43,8 @@ declare class Trail2D {
43
43
  */
44
44
  distanceSinceLastUpdate: number;
45
45
  /**
46
+ * RGB tints the ribbon; alpha is the trail's base opacity, which the
47
+ * age-driven fade scales down to zero.
46
48
  * @readonly
47
49
  * @type {Color}
48
50
  */
@@ -1 +1 @@
1
- {"version":3,"file":"Trail2D.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/graphics/ecs/trail2d/Trail2D.js"],"names":[],"mappings":";AAwBA;IAgMI,oCAMC;IApMD;;;OAGG;IACH,QAFU,MAAM,CAES;IAEzB;;;OAGG;IACH,OAFU,MAAM,CAEM;IAEtB;;;OAGG;IACH,WAFU,cAAc,GAAC,MAAM,CAEC;IAEhC;;;;OAIG;IACH,eAFU,MAAM,CAEuB;IAEvC;;;OAGG;IACH,MAFU,MAAM,CAEP;IAET;;;OAGG;IACH,eAFU,MAAM,CAEE;IAElB;;;OAGG;IACH,qBAFU,MAAM,CAEQ;IAExB;;;OAGG;IACH,yBAFU,MAAM,CAEY;IAE5B;;;OAGG;IACH,gBAFU,KAAK,CAEe;IAE9B;;;;OAIG;IACH,iBAFU,OAAO,CAEM;IAEvB;;;OAGG;IACH,QAFU,OAAO,GAAC,IAAI,CAER;IAEd;;;OAGG;IACH,UAFU,mBAAmB,CAEQ;IAErC;;;OAGG;IACH,cAFU,SAAS,CAEG;IAEtB;;;OAGG;IACH,cAAsB;IAEtB,gBAQC;IAMD,4BAEC;IAND,yBAEC;IAMD;;;;OAIG;IACH,cAHW,MAAM,GAAC,YAAY,GACjB,IAAI,CAIhB;IAED;;;;OAIG;IACH,gBAHW,MAAM,GAAC,YAAY,GACjB,IAAI,CAIhB;IAED;;;;OAIG;IACH,gBAHW,MAAM,GAAC,YAAY,SACnB,OAAO,QAQjB;IAED;;;;OAIG;IACH,cAHW,MAAM,GAAC,YAAY,GACjB,OAAO,CAInB;IAED;;;OAGG;IACH,qBAFW,MAAM,QAehB;IADG,kHAAgB;IAGpB;;OAEG;IACH,cAsBC;IAUD;;;;;;;;;;;;;aAkBC;IAGD;;;;;;;;;;;;;;;;;MAUC;IAED;;;OAGG;IACH,cAFW,OAAO,WAWjB;IAED;;;;;;;OAOG;IACH,cANW,MAAM,KACN,MAAM,KACN,MAAM,YACN,MAAM,GACJ,OAAO,CAkEnB;CAEJ;;;;;;+BAnV8B,+BAA+B;sBALxC,iCAAiC;oBACnC,kCAAkC;wBAM9B,0BAA0B;oCACd,sCAAsC;0BAThD,yCAAyC;6BAWtC,mBAAmB;kCADd,wBAAwB"}
1
+ {"version":3,"file":"Trail2D.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/graphics/ecs/trail2d/Trail2D.js"],"names":[],"mappings":";AAwBA;IAkMI,oCAMC;IAtMD;;;OAGG;IACH,QAFU,MAAM,CAES;IAEzB;;;OAGG;IACH,OAFU,MAAM,CAEM;IAEtB;;;OAGG;IACH,WAFU,cAAc,GAAC,MAAM,CAEC;IAEhC;;;;OAIG;IACH,eAFU,MAAM,CAEuB;IAEvC;;;OAGG;IACH,MAFU,MAAM,CAEP;IAET;;;OAGG;IACH,eAFU,MAAM,CAEE;IAElB;;;OAGG;IACH,qBAFU,MAAM,CAEQ;IAExB;;;OAGG;IACH,yBAFU,MAAM,CAEY;IAE5B;;;;;OAKG;IACH,gBAFU,KAAK,CAEe;IAE9B;;;;OAIG;IACH,iBAFU,OAAO,CAEM;IAEvB;;;OAGG;IACH,QAFU,OAAO,GAAC,IAAI,CAER;IAEd;;;OAGG;IACH,UAFU,mBAAmB,CAEQ;IAErC;;;OAGG;IACH,cAFU,SAAS,CAEG;IAEtB;;;OAGG;IACH,cAAsB;IAEtB,gBAQC;IAMD,4BAEC;IAND,yBAEC;IAMD;;;;OAIG;IACH,cAHW,MAAM,GAAC,YAAY,GACjB,IAAI,CAIhB;IAED;;;;OAIG;IACH,gBAHW,MAAM,GAAC,YAAY,GACjB,IAAI,CAIhB;IAED;;;;OAIG;IACH,gBAHW,MAAM,GAAC,YAAY,SACnB,OAAO,QAQjB;IAED;;;;OAIG;IACH,cAHW,MAAM,GAAC,YAAY,GACjB,OAAO,CAInB;IAED;;;OAGG;IACH,qBAFW,MAAM,QAehB;IADG,kHAAgB;IAGpB;;OAEG;IACH,cAsBC;IAUD;;;;;;;;;;;;;aAkBC;IAGD;;;;;;;;;;;;;;;;;MAUC;IAED;;;OAGG;IACH,cAFW,OAAO,WAWjB;IAED;;;;;;;OAOG;IACH,cANW,MAAM,KACN,MAAM,KACN,MAAM,YACN,MAAM,GACJ,OAAO,CAmEnB;CAEJ;;;;;;+BAtV8B,+BAA+B;sBALxC,iCAAiC;oBACnC,kCAAkC;wBAM9B,0BAA0B;oCACd,sCAAsC;0BAThD,yCAAyC;6BAWtC,mBAAmB;kCADd,wBAAwB"}
@@ -74,6 +74,8 @@ class Trail2D {
74
74
  distanceSinceLastUpdate = 0;
75
75
 
76
76
  /**
77
+ * RGB tints the ribbon; alpha is the trail's base opacity, which the
78
+ * age-driven fade scales down to zero.
77
79
  * @readonly
78
80
  * @type {Color}
79
81
  */
@@ -336,9 +338,10 @@ class Trail2D {
336
338
  this.setFlag(Trail2DFlags.BoundsNeedUpdate);
337
339
  }
338
340
 
339
- // set head to fresh value
341
+ // set head to fresh value; alpha follows the same fade law the simulator
342
+ // applies every tick, so the point doesn't pop when it stops being the head
340
343
  ribbon.setPointAttribute_Scalar(head_index, RIBBON_ATTRIBUTE_ADDRESS_AGE, head_age);
341
- ribbon.setPointAlpha(head_index, clamp01(head_age / maxAge));
344
+ ribbon.setPointAlpha(head_index, (1 - clamp01(head_age / maxAge)) * this.color.w);
342
345
  ribbon.setPointThickness(head_index, this.width);
343
346
 
344
347
  return head_position_changed;
@@ -1 +1 @@
1
- {"version":3,"file":"Trail2DSystem.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/graphics/ecs/trail2d/Trail2DSystem.js"],"names":[],"mappings":";AA0BA;IAkCI;;;OAGG;IACH,4BA4BC;IAjED,oDAAoC;IAEpC,+DAEE;IAEF;;;OAGG;IACH,WAFU,4BAA4B,CAES;IAE/C;;;;OAIG;IACH,wBAAiC;IAGjC;;;;OAIG;IACH,oBAAgB;IAEhB;;;OAGG;IACH,YAAgB;IAcZ;;;;OAIG;IACH,iBAAsB;IAEtB;;;OAGG;IACH,UAFU,cAAc,CAEO;IAE/B;;;OAGG;IACH,aAFU,cAAY,IAAI,CAEH;IAK3B,2CAmBC;IAED,4CAIC;IAED;;;;;OAKG;IACH,sBAqDC;IAED;;;;;OAKG;IACH,YAHW,OAAO,aADP,SAAS,YAET,MAAM,QAahB;IAED;;;;;OAKG;IACH,kBAJW,OAAO,aACP,SAAS,UACT,MAAM,QAShB;IAED;;;;OAIG;IACH,yBAHW,OAAO,aACP,SAAS,QAoCnB;IAED,6BAWC;CACJ;uBArQsB,wBAAwB;0BACrB,qCAAqC;oBAY3C,cAAc;4CAdU,uDAAuD;6CAYtD,yDAAyD;+BARvE,yBAAyB"}
1
+ {"version":3,"file":"Trail2DSystem.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/graphics/ecs/trail2d/Trail2DSystem.js"],"names":[],"mappings":";AA0BA;IAkCI;;;OAGG;IACH,4BA4BC;IAjED,oDAAoC;IAEpC,+DAEE;IAEF;;;OAGG;IACH,WAFU,4BAA4B,CAES;IAE/C;;;;OAIG;IACH,wBAAiC;IAGjC;;;;OAIG;IACH,oBAAgB;IAEhB;;;OAGG;IACH,YAAgB;IAcZ;;;;OAIG;IACH,iBAAsB;IAEtB;;;OAGG;IACH,UAFU,cAAc,CAEO;IAE/B;;;OAGG;IACH,aAFU,cAAY,IAAI,CAEH;IAK3B,2CAmBC;IAED,4CAIC;IAED;;;;;OAKG;IACH,sBAsDC;IAED;;;;;OAKG;IACH,YAHW,OAAO,aADP,SAAS,YAET,MAAM,QAahB;IAED;;;;;OAKG;IACH,kBAJW,OAAO,aACP,SAAS,UACT,MAAM,QAShB;IAED;;;;OAIG;IACH,yBAHW,OAAO,aACP,SAAS,QAoCnB;IAED,6BAWC;CACJ;uBAtQsB,wBAAwB;0BACrB,qCAAqC;oBAY3C,cAAc;4CAdU,uDAAuD;6CAYtD,yDAAyD;+BARvE,yBAAyB"}
@@ -167,7 +167,8 @@ class Trail2DSystem extends System {
167
167
  ribbon.setPointPosition(i, position_x, position_y, position_z);
168
168
  ribbon.setPointThickness(i, trail.width);
169
169
  ribbon.setPointAttribute_Scalar(i, RIBBON_ATTRIBUTE_ADDRESS_UV_OFFSET, f);
170
- ribbon.setPointAlpha(i, color.w);
170
+ // seeded age = maxAge means "already expired", so alpha follows the fade law: 0
171
+ ribbon.setPointAlpha(i, 0);
171
172
  ribbon.setPointAttribute_Scalar(i, RIBBON_ATTRIBUTE_ADDRESS_AGE, trail.maxAge);
172
173
 
173
174
  }
@@ -238,7 +239,7 @@ class Trail2DSystem extends System {
238
239
 
239
240
  const ageOffset = max2(0, trail.maxAge - trail.time);
240
241
 
241
- this.simulator.update(ribbon, trail.maxAge, timeDelta);
242
+ this.simulator.update(ribbon, trail.maxAge, timeDelta, trail.color.w);
242
243
 
243
244
  if (trail.getFlag(Trail2DFlags.Spawning)) {
244
245
  trail.updateHead(newPosition.x, newPosition.y, newPosition.z, ageOffset);
@@ -54,6 +54,8 @@ declare class Trail3D {
54
54
  */
55
55
  distanceSinceLastUpdate: number;
56
56
  /**
57
+ * RGB tints the tube; alpha is the trail's base opacity, which the
58
+ * age-driven fade scales down to zero.
57
59
  * @readonly
58
60
  * @type {Color}
59
61
  */
@@ -1 +1 @@
1
- {"version":3,"file":"Trail3D.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/graphics/ecs/trail3d/Trail3D.js"],"names":[],"mappings":";AAgCA;;;;;GAKG;AACH;IA4MI,oCAMC;IAhND;;;OAGG;IACH,QAFU,MAAM,CAES;IAEzB;;;OAGG;IACH,OAFU,MAAM,CAEM;IAEtB;;;;OAIG;IACH,gBAFU,MAAM,CAEyB;IAEzC;;;OAGG;IACH,WAFU,cAAc,GAAC,MAAM,CAEC;IAEhC;;;;OAIG;IACH,eAFU,MAAM,CAEuB;IAEvC;;;OAGG;IACH,MAFU,MAAM,CAEP;IAET;;OAEG;IACH,eAFU,MAAM,CAEE;IAElB;;;OAGG;IACH,qBAFU,MAAM,CAEQ;IAExB;;;OAGG;IACH,yBAFU,MAAM,CAEY;IAE5B;;;OAGG;IACH,gBAFU,KAAK,CAEe;IAE9B;;;;OAIG;IACH,iBAFU,OAAO,CAEM;IAEvB;;;OAGG;IACH,MAFU,KAAK,GAAC,IAAI,CAER;IAEZ;;;OAGG;IACH,MAFU,OAAK,IAAI,CAEP;IAEZ;;;OAGG;IACH,UAFU,iBAAiB,CAEQ;IAEnC;;;OAGG;IACH,cAFU,SAAS,CAEG;IAEtB;;;OAGG;IACH,cAAsB;IAEtB,gBAQC;IAMD,4BAEC;IAND,yBAEC;IAUD,6BAEC;IAND,0BAEC;IAMD;;;OAGG;IACH,cAHW,MAAM,GAAC,YAAY,GACjB,IAAI,CAIhB;IAED;;;OAGG;IACH,gBAHW,MAAM,GAAC,YAAY,GACjB,IAAI,CAIhB;IAED;;;OAGG;IACH,gBAHW,MAAM,GAAC,YAAY,SACnB,OAAO,QAQjB;IAED;;;OAGG;IACH,cAHW,MAAM,GAAC,YAAY,GACjB,OAAO,CAInB;IAED;;OAEG;IACH,qBAFW,MAAM,QAehB;IAED;;OAEG;IACH,cAmBC;IAUD;;;;;;;;;;;;;;;aAsBC;IAED;;;;;;;;;;;;;;;;;;;MAYC;IAED;;OAEG;IACH,cAFW,OAAO,WAajB;IAkDD;;;;;;OAMG;IACH,cANW,MAAM,KACN,MAAM,KACN,MAAM,YACN,MAAM,GACJ,OAAO,CAgEnB;;CAEJ;;;;;+BA9Z8B,+BAA+B;sBAPxC,iCAAiC;oBACnC,kCAAkC;sBAShC,2BAA2B;kCACf,uCAAuC;0BAZ/C,yCAAyC;6BAatC,mBAAmB"}
1
+ {"version":3,"file":"Trail3D.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/graphics/ecs/trail3d/Trail3D.js"],"names":[],"mappings":";AAgCA;;;;;GAKG;AACH;IA8MI,oCAMC;IAlND;;;OAGG;IACH,QAFU,MAAM,CAES;IAEzB;;;OAGG;IACH,OAFU,MAAM,CAEM;IAEtB;;;;OAIG;IACH,gBAFU,MAAM,CAEyB;IAEzC;;;OAGG;IACH,WAFU,cAAc,GAAC,MAAM,CAEC;IAEhC;;;;OAIG;IACH,eAFU,MAAM,CAEuB;IAEvC;;;OAGG;IACH,MAFU,MAAM,CAEP;IAET;;OAEG;IACH,eAFU,MAAM,CAEE;IAElB;;;OAGG;IACH,qBAFU,MAAM,CAEQ;IAExB;;;OAGG;IACH,yBAFU,MAAM,CAEY;IAE5B;;;;;OAKG;IACH,gBAFU,KAAK,CAEe;IAE9B;;;;OAIG;IACH,iBAFU,OAAO,CAEM;IAEvB;;;OAGG;IACH,MAFU,KAAK,GAAC,IAAI,CAER;IAEZ;;;OAGG;IACH,MAFU,OAAK,IAAI,CAEP;IAEZ;;;OAGG;IACH,UAFU,iBAAiB,CAEQ;IAEnC;;;OAGG;IACH,cAFU,SAAS,CAEG;IAEtB;;;OAGG;IACH,cAAsB;IAEtB,gBAQC;IAMD,4BAEC;IAND,yBAEC;IAUD,6BAEC;IAND,0BAEC;IAMD;;;OAGG;IACH,cAHW,MAAM,GAAC,YAAY,GACjB,IAAI,CAIhB;IAED;;;OAGG;IACH,gBAHW,MAAM,GAAC,YAAY,GACjB,IAAI,CAIhB;IAED;;;OAGG;IACH,gBAHW,MAAM,GAAC,YAAY,SACnB,OAAO,QAQjB;IAED;;;OAGG;IACH,cAHW,MAAM,GAAC,YAAY,GACjB,OAAO,CAInB;IAED;;OAEG;IACH,qBAFW,MAAM,QAehB;IAED;;OAEG;IACH,cAmBC;IAUD;;;;;;;;;;;;;;;aAsBC;IAED;;;;;;;;;;;;;;;;;;;MAYC;IAED;;OAEG;IACH,cAFW,OAAO,WAajB;IAkDD;;;;;;OAMG;IACH,cANW,MAAM,KACN,MAAM,KACN,MAAM,YACN,MAAM,GACJ,OAAO,CAiEnB;;CAEJ;;;;;+BAja8B,+BAA+B;sBAPxC,iCAAiC;oBACnC,kCAAkC;sBAShC,2BAA2B;kCACf,uCAAuC;0BAZ/C,yCAAyC;6BAatC,mBAAmB"}
@@ -94,6 +94,8 @@ class Trail3D {
94
94
  distanceSinceLastUpdate = 0;
95
95
 
96
96
  /**
97
+ * RGB tints the tube; alpha is the trail's base opacity, which the
98
+ * age-driven fade scales down to zero.
97
99
  * @readonly
98
100
  * @type {Color}
99
101
  */
@@ -413,9 +415,10 @@ class Trail3D {
413
415
  this.setFlag(Trail3DFlags.BoundsNeedUpdate);
414
416
  }
415
417
 
416
- // set head to fresh value
418
+ // set head to fresh value; alpha follows the same fade law the simulator
419
+ // applies every tick, so the knot doesn't pop when it stops being the head
417
420
  tube.setKnotAttribute_Scalar(head_index, TUBE_ATTRIBUTE_ADDRESS_AGE, head_age);
418
- tube.setKnotAlpha(head_index, clamp01(head_age / maxAge));
421
+ tube.setKnotAlpha(head_index, (1 - clamp01(head_age / maxAge)) * this.color.w);
419
422
  tube.setKnotThickness(head_index, this.width);
420
423
 
421
424
  return head_position_changed;
@@ -1 +1 @@
1
- {"version":3,"file":"Trail3DSystem.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/graphics/ecs/trail3d/Trail3DSystem.js"],"names":[],"mappings":";AAyBA;IAiCI;;;OAGG;IACH,4BAkBC;IAtDD,oDAAoC;IAEpC,+DAEE;IAEF;;OAEG;IACH,WAFU,0BAA0B,CAES;IAiB7C;;;OAGG;IACH,YAAgB;IAeZ;;OAEG;IACH,UAFU,cAAc,CAEO;IAE/B;;OAEG;IACH,aAFU,cAAY,IAAI,CAEH;IAG3B,2CAmBC;IAED,4CAIC;IA0DD;;;;OAIG;IACH,YAJW,OAAO,aACP,SAAS,YACT,MAAM,QAYhB;IAED;;;;OAIG;IACH,kBAJW,OAAO,aACP,SAAS,UACT,MAAM,QAQhB;IAED;;;OAGG;IACH,yBAHW,OAAO,aACP,SAAS,QAkCnB;IAED,6BAWC;;CACJ;uBA7OsB,wBAAwB;0BACrB,qCAAqC;oBAW3C,cAAc;4CAbU,uDAAuD;2CAUxD,0DAA0D;+BANtE,yBAAyB"}
1
+ {"version":3,"file":"Trail3DSystem.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/graphics/ecs/trail3d/Trail3DSystem.js"],"names":[],"mappings":";AAyBA;IAiCI;;;OAGG;IACH,4BAkBC;IAtDD,oDAAoC;IAEpC,+DAEE;IAEF;;OAEG;IACH,WAFU,0BAA0B,CAES;IAiB7C;;;OAGG;IACH,YAAgB;IAeZ;;OAEG;IACH,UAFU,cAAc,CAEO;IAE/B;;OAEG;IACH,aAFU,cAAY,IAAI,CAEH;IAG3B,2CAmBC;IAED,4CAIC;IA2DD;;;;OAIG;IACH,YAJW,OAAO,aACP,SAAS,YACT,MAAM,QAYhB;IAED;;;;OAIG;IACH,kBAJW,OAAO,aACP,SAAS,UACT,MAAM,QAQhB;IAED;;;OAGG;IACH,yBAHW,OAAO,aACP,SAAS,QAkCnB;IAED,6BAWC;;CACJ;uBA9OsB,wBAAwB;0BACrB,qCAAqC;oBAW3C,cAAc;4CAbU,uDAAuD;2CAUxD,0DAA0D;+BANtE,yBAAyB"}
@@ -151,7 +151,8 @@ class Trail3DSystem extends System {
151
151
  tube.setKnotPosition(i, position_x, position_y, position_z);
152
152
  tube.setKnotThickness(i, trail.width);
153
153
  tube.setKnotAttribute_Scalar(i, TUBE_ATTRIBUTE_ADDRESS_UV_V, f);
154
- tube.setKnotAlpha(i, color.w);
154
+ // seeded age = maxAge means "already expired", so alpha follows the fade law: 0
155
+ tube.setKnotAlpha(i, 0);
155
156
  tube.setKnotAttribute_Scalar(i, TUBE_ATTRIBUTE_ADDRESS_AGE, trail.maxAge);
156
157
  }
157
158
 
@@ -215,7 +216,7 @@ class Trail3DSystem extends System {
215
216
 
216
217
  const ageOffset = max2(0, trail.maxAge - trail.time);
217
218
 
218
- this.simulator.update(tube, trail.maxAge, timeDelta);
219
+ this.simulator.update(tube, trail.maxAge, timeDelta, trail.color.w);
219
220
 
220
221
  if (trail.getFlag(Trail3DFlags.Spawning)) {
221
222
  trail.updateHead(newPosition.x, newPosition.y, newPosition.z, ageOffset);
@@ -1,9 +1,45 @@
1
1
  export class LightProbeGridBaker {
2
+ /**
3
+ * Directions sampled one hit deep per probe. Direct light and sky converge with this count,
4
+ * every candidate also feeds the back-face visibility statistic.
5
+ *
6
+ * The defaults match the directional noise of the previous 4096-full-path bake at roughly half
7
+ * the cost (candidates are single-segment); raise both counts together for higher quality.
8
+ * @type {number}
9
+ */
10
+ candidate_count: number;
11
+ /**
12
+ * Full multi-bounce path traces per probe, re-sampled from the candidates by first-bounce
13
+ * importance. Only the indirect part of the estimate rides on these, so far fewer are needed
14
+ * than candidates.
15
+ * @type {number}
16
+ */
17
+ indirect_sample_count: number;
18
+ /**
19
+ * Total path depth including the first hit
20
+ * @type {number}
21
+ */
22
+ max_bounce_count: number;
23
+ /**
24
+ * GGX roughness of the prefilter cone applied to the baked radiance SH, in [0, 1].
25
+ * Attenuates band 1 before the 2-band truncation, suppressing directional overshoot that would
26
+ * otherwise reconstruct as negative irradiance (light/shadow bleeding) on the far side of a
27
+ * strong light. Higher values trade directionality for stability.
28
+ * @type {number}
29
+ */
30
+ filter_ggx_alpha: number;
31
+ /**
32
+ * A probe whose fraction of candidate rays hitting geometry back-faces exceeds this threshold is
33
+ * considered to be inside geometry. Such probes are filled from their valid neighbors after the
34
+ * bake instead of contributing interior darkness to trilinear filtering.
35
+ * @type {number}
36
+ */
37
+ interior_backface_threshold: number;
2
38
  /**
3
39
  * Renderer used to bake probes. Exposed so callers (and tests) can configure the underlying scene.
4
- * @return {ProbeRenderer}
40
+ * @return {PathTracerProbeRenderer}
5
41
  */
6
- get renderer(): ProbeRenderer;
42
+ get renderer(): PathTracerProbeRenderer;
7
43
  /**
8
44
  * Bake light probes
9
45
  * @returns {TaskGroup}
@@ -13,12 +49,15 @@ export class LightProbeGridBaker {
13
49
  bake(grid: LightProbeGrid, ecd: EntityComponentDataset): TaskGroup;
14
50
  /**
15
51
  *
16
- * @param {ProbeRenderer} renderer
52
+ * @param {PathTracerProbeRenderer} renderer
17
53
  * @param {LightProbeGrid} grid
18
54
  * @param {number} probe_index
55
+ * @returns {boolean} true if the probe is valid, false if it appears to be inside geometry and
56
+ * should be filled from its neighbors
19
57
  */
20
- bake_probe(renderer: ProbeRenderer, grid: LightProbeGrid, probe_index: number): void;
58
+ bake_probe(renderer: PathTracerProbeRenderer, grid: LightProbeGrid, probe_index: number): boolean;
21
59
  #private;
22
60
  }
61
+ import { PathTracerProbeRenderer } from "../lpv/PathTracerProbeRenderer.js";
23
62
  import TaskGroup from "../../../../core/process/task/TaskGroup.js";
24
63
  //# sourceMappingURL=LightProbeGridBaker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"LightProbeGridBaker.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/graphics/sh3/lpg/LightProbeGridBaker.js"],"names":[],"mappings":"AAWA;IAII;;;OAGG;IACH,8BAEC;IAED;;;;;OAKG;IACH,yDAJa,SAAS,CA2CrB;IAED;;;;;OAKG;IACH,uEAFW,MAAM,QAYhB;;CACJ;sBArFqB,4CAA4C"}
1
+ {"version":3,"file":"LightProbeGridBaker.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/graphics/sh3/lpg/LightProbeGridBaker.js"],"names":[],"mappings":"AAgBA;IAII;;;;;;;OAOG;IACH,iBAFU,MAAM,CAEO;IAEvB;;;;;OAKG;IACH,uBAFU,MAAM,CAEY;IAE5B;;;OAGG;IACH,kBAFU,MAAM,CAEK;IAErB;;;;;;OAMG;IACH,kBAFU,MAAM,CAEO;IAEvB;;;;;OAKG;IACH,6BAFU,MAAM,CAEmB;IAEnC;;;OAGG;IACH,wCAEC;IAED;;;;;OAKG;IACH,yDAJa,SAAS,CAoDrB;IAED;;;;;;;OAOG;IACH,qBANW,uBAAuB,qCAEvB,MAAM,GACJ,OAAO,CAuBnB;;CACJ;wCAlJuC,mCAAmC;sBAJrD,4CAA4C"}
@@ -1,21 +1,67 @@
1
1
  import { assert } from "../../../../core/assert.js";
2
2
  import { array_copy } from "../../../../core/collection/array/array_copy.js";
3
+ import { sh3_convolve_with_zh } from "../../../../core/geom/3d/sphere/harmonics/sh3_convolve_with_zh.js";
4
+ import { sh3_ggx_zh } from "../../../../core/geom/3d/sphere/harmonics/sh3_ggx_zh.js";
3
5
  import TaskGroup from "../../../../core/process/task/TaskGroup.js";
4
6
  import { actionTask } from "../../../../core/process/task/util/actionTask.js";
5
7
  import { countTask } from "../../../../core/process/task/util/countTask.js";
6
8
  import { promiseTask } from "../../../../core/process/task/util/promiseTask.js";
7
9
  import { PathTracerProbeRenderer } from "../lpv/PathTracerProbeRenderer.js";
10
+ import { bake_probe_sh3_resampled } from "./bake_probe_sh3_resampled.js";
11
+ import { light_probe_grid_dilate } from "./light_probe_grid_dilate.js";
8
12
 
9
13
  const __position = new Float32Array(3);
10
14
  const __sh3_rgb = new Float64Array(27);
15
+ const __zh = new Float64Array(3);
11
16
 
12
17
  export class LightProbeGridBaker {
13
18
 
14
19
  #ren = new PathTracerProbeRenderer();
15
20
 
21
+ /**
22
+ * Directions sampled one hit deep per probe. Direct light and sky converge with this count,
23
+ * every candidate also feeds the back-face visibility statistic.
24
+ *
25
+ * The defaults match the directional noise of the previous 4096-full-path bake at roughly half
26
+ * the cost (candidates are single-segment); raise both counts together for higher quality.
27
+ * @type {number}
28
+ */
29
+ candidate_count = 2048;
30
+
31
+ /**
32
+ * Full multi-bounce path traces per probe, re-sampled from the candidates by first-bounce
33
+ * importance. Only the indirect part of the estimate rides on these, so far fewer are needed
34
+ * than candidates.
35
+ * @type {number}
36
+ */
37
+ indirect_sample_count = 512;
38
+
39
+ /**
40
+ * Total path depth including the first hit
41
+ * @type {number}
42
+ */
43
+ max_bounce_count = 5;
44
+
45
+ /**
46
+ * GGX roughness of the prefilter cone applied to the baked radiance SH, in [0, 1].
47
+ * Attenuates band 1 before the 2-band truncation, suppressing directional overshoot that would
48
+ * otherwise reconstruct as negative irradiance (light/shadow bleeding) on the far side of a
49
+ * strong light. Higher values trade directionality for stability.
50
+ * @type {number}
51
+ */
52
+ filter_ggx_alpha = 0.5;
53
+
54
+ /**
55
+ * A probe whose fraction of candidate rays hitting geometry back-faces exceeds this threshold is
56
+ * considered to be inside geometry. Such probes are filled from their valid neighbors after the
57
+ * bake instead of contributing interior darkness to trilinear filtering.
58
+ * @type {number}
59
+ */
60
+ interior_backface_threshold = 0.25;
61
+
16
62
  /**
17
63
  * Renderer used to bake probes. Exposed so callers (and tests) can configure the underlying scene.
18
- * @return {ProbeRenderer}
64
+ * @return {PathTracerProbeRenderer}
19
65
  */
20
66
  get renderer() {
21
67
  return this.#ren;
@@ -40,8 +86,10 @@ export class LightProbeGridBaker {
40
86
 
41
87
  const probe_count = grid.count;
42
88
 
89
+ const validity = new Uint8Array(probe_count);
90
+
43
91
  const tBakeIrradiance = countTask(0, probe_count, (probe_index) => {
44
- this.bake_probe(renderer, grid, probe_index);
92
+ validity[probe_index] = this.bake_probe(renderer, grid, probe_index) ? 1 : 0;
45
93
 
46
94
  });
47
95
  tBakeIrradiance.estimatedDuration = probe_count;
@@ -57,6 +105,13 @@ export class LightProbeGridBaker {
57
105
  const tFinish = actionTask(() => {
58
106
  renderer.bake_end();
59
107
 
108
+ // probes inside geometry take on the average of their valid neighbors, so trilinear
109
+ // sampling near walls does not blend in interior darkness
110
+ light_probe_grid_dilate(
111
+ grid.harmonics, validity,
112
+ grid.resolution_x, grid.resolution_y, grid.resolution_z
113
+ );
114
+
60
115
  grid.incrementVersion();
61
116
  });
62
117
 
@@ -70,19 +125,31 @@ export class LightProbeGridBaker {
70
125
 
71
126
  /**
72
127
  *
73
- * @param {ProbeRenderer} renderer
128
+ * @param {PathTracerProbeRenderer} renderer
74
129
  * @param {LightProbeGrid} grid
75
130
  * @param {number} probe_index
131
+ * @returns {boolean} true if the probe is valid, false if it appears to be inside geometry and
132
+ * should be filled from its neighbors
76
133
  */
77
134
  bake_probe(renderer, grid, probe_index) {
78
135
  assert.isNonNegativeInteger(probe_index, 'probe_index');
79
136
 
80
137
  grid.probe_position(__position, 0, probe_index);
81
138
 
82
- // render a full SH3 probe with the shared facilities
83
- renderer.bake(__position, 0, __sh3_rgb, 0);
139
+ const backface_fraction = bake_probe_sh3_resampled(
140
+ __sh3_rgb, 0,
141
+ renderer.scene, renderer.tracer, renderer.random,
142
+ __position, 0,
143
+ this.candidate_count, this.indirect_sample_count, this.max_bounce_count
144
+ );
145
+
146
+ // prefilter the radiance with a GGX cone before truncation, taming band-1 overshoot
147
+ sh3_ggx_zh(__zh, 0, this.filter_ggx_alpha);
148
+ sh3_convolve_with_zh(__sh3_rgb, 0, __sh3_rgb, 0, __zh[0], __zh[1], __zh[2], 3);
84
149
 
85
150
  // keep only bands 0 and 1: the coefficient-major layout puts them in the first 12 values
86
151
  array_copy(__sh3_rgb, 0, grid.harmonics, probe_index * 12, 12);
152
+
153
+ return backface_fraction <= this.interior_backface_threshold;
87
154
  }
88
155
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Bake a single SH3 radiance probe with importance re-sampling and back-face visibility rejection.
3
+ *
4
+ * Two-phase estimator, split by path length:
5
+ *
6
+ * 1. Candidate phase: `candidate_count` uniform sphere directions are traced one hit deep. Misses
7
+ * take the background radiance; front-face hits take `emissive + albedo * direct_light`, both of
8
+ * which are cheap and low-noise, and every such sample is projected onto the SH. Directions whose
9
+ * first hit is a back face see the inside of geometry — they are rejected and the estimate is
10
+ * renormalized over the remaining directions, so interior darkness does not bleed into the probe.
11
+ *
12
+ * 2. Indirect phase: `indirect_sample_count` continuations are picked from the hit candidates with
13
+ * probability proportional to their first-bounce luminance (resampled importance sampling — the
14
+ * selection a weighted reservoir would produce, drawn from the materialized candidate table), and
15
+ * only these few directions pay for a full multi-bounce path trace. The 1/p_hat re-sampling weight
16
+ * keeps the combined estimator unbiased.
17
+ *
18
+ * The two phases sum to the same integral as tracing every direction to full depth, but direct light
19
+ * converges with the full candidate count while the expensive multi-bounce work is spent where the
20
+ * first bounce found energy.
21
+ *
22
+ * @param {number[]|Float32Array|Float64Array} out 27 values: SH3 radiance, coefficient-major RGB triplets
23
+ * @param {number} out_offset
24
+ * @param {PathTracedScene} scene
25
+ * @param {PathTracer} tracer
26
+ * @param {function():number} random
27
+ * @param {number[]|Float32Array} position probe position
28
+ * @param {number} position_offset
29
+ * @param {number} candidate_count directions sampled one hit deep, at least 1
30
+ * @param {number} indirect_sample_count full path-traced continuations re-sampled from the candidates
31
+ * @param {number} max_bounce_count total path depth including the first hit, at least 1
32
+ * @returns {number} fraction of candidate directions whose first hit was a back face, in [0, 1]
33
+ */
34
+ export function bake_probe_sh3_resampled(out: number[] | Float32Array | Float64Array, out_offset: number, scene: PathTracedScene, tracer: PathTracer, random: () => number, position: number[] | Float32Array, position_offset: number, candidate_count: number, indirect_sample_count: number, max_bounce_count: number): number;
35
+ import { PathTracedScene } from "../path_tracer/PathTracedScene.js";
36
+ import { PathTracer } from "../path_tracer/PathTracer.js";
37
+ //# sourceMappingURL=bake_probe_sh3_resampled.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bake_probe_sh3_resampled.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/graphics/sh3/lpg/bake_probe_sh3_resampled.js"],"names":[],"mappings":"AAyEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,8CAZW,MAAM,EAAE,GAAC,YAAY,GAAC,YAAY,cAClC,MAAM,SACN,eAAe,UACf,UAAU,gBACC,MAAM,YACjB,MAAM,EAAE,GAAC,YAAY,mBACrB,MAAM,mBACN,MAAM,yBACN,MAAM,oBACN,MAAM,GACJ,MAAM,CAiMlB;gCAlS+B,mCAAmC;2BACjB,8BAA8B"}
@@ -0,0 +1,298 @@
1
+ import { assert } from "../../../../core/assert.js";
2
+ import { rgb_to_luminance } from "../../../../core/color/rgb_to_luminance.js";
3
+ import { Ray3 } from "../../../../core/geom/3d/ray/Ray3.js";
4
+ import { sh3_basis_at } from "../../../../core/geom/3d/sphere/harmonics/sh3_basis_at.js";
5
+ import { sh3_vector_accumulate } from "../../../../core/geom/3d/sphere/harmonics/sh3_vector_accumulate.js";
6
+ import { random_point_on_sphere } from "../../../../core/geom/random/random_point_on_sphere.js";
7
+ import { v3_dot } from "../../../../core/geom/vec3/v3_dot.js";
8
+ import { PathTracedScene } from "../path_tracer/PathTracedScene.js";
9
+ import { PathTracer, SELF_OCCLUSION_OFFSET } from "../path_tracer/PathTracer.js";
10
+ import { getBiasedNormalSample } from "../path_tracer/sampling/getBiasedNormalSample.js";
11
+
12
+ /**
13
+ * Per-candidate record: direction(3), offset surface position(3), shading normal(3), albedo(3)
14
+ * @type {number}
15
+ */
16
+ const CANDIDATE_STRIDE = 12;
17
+
18
+ /**
19
+ * Fraction of the mean candidate luminance added to every re-sampling weight. Keeps surfaces that
20
+ * receive no direct light selectable — they may still return indirect radiance — so the estimator
21
+ * stays unbiased.
22
+ * @type {number}
23
+ */
24
+ const RESAMPLING_WEIGHT_FLOOR = 0.1;
25
+
26
+ const __ray = new Ray3();
27
+ const __hit = [];
28
+ const __surface = new Float32Array(9);
29
+ const __direct = new Float32Array(3);
30
+ const __radiance = new Float32Array(3);
31
+ const __basis = new Float64Array(9);
32
+ const __sh_direct = new Float64Array(27);
33
+ const __sh_indirect = new Float64Array(27);
34
+
35
+ let __candidates = new Float32Array(0);
36
+ let __p_hat = new Float64Array(0);
37
+ let __cumulative_p_hat = new Float64Array(0);
38
+
39
+ /**
40
+ *
41
+ * @param {number} candidate_count
42
+ */
43
+ function ensure_pool_capacity(candidate_count) {
44
+ if (__p_hat.length < candidate_count) {
45
+ __candidates = new Float32Array(candidate_count * CANDIDATE_STRIDE);
46
+ __p_hat = new Float64Array(candidate_count);
47
+ __cumulative_p_hat = new Float64Array(candidate_count);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Index of the candidate owning the cumulative-weight interval that contains `value`
53
+ * @param {number} value in [0, cumulative[count - 1])
54
+ * @param {number} count
55
+ * @returns {number}
56
+ */
57
+ function pick_candidate(value, count) {
58
+ let low = 0;
59
+ let high = count - 1;
60
+
61
+ while (low < high) {
62
+ const mid = (low + high) >> 1;
63
+
64
+ if (__cumulative_p_hat[mid] > value) {
65
+ high = mid;
66
+ } else {
67
+ low = mid + 1;
68
+ }
69
+ }
70
+
71
+ return low;
72
+ }
73
+
74
+ /**
75
+ * Bake a single SH3 radiance probe with importance re-sampling and back-face visibility rejection.
76
+ *
77
+ * Two-phase estimator, split by path length:
78
+ *
79
+ * 1. Candidate phase: `candidate_count` uniform sphere directions are traced one hit deep. Misses
80
+ * take the background radiance; front-face hits take `emissive + albedo * direct_light`, both of
81
+ * which are cheap and low-noise, and every such sample is projected onto the SH. Directions whose
82
+ * first hit is a back face see the inside of geometry — they are rejected and the estimate is
83
+ * renormalized over the remaining directions, so interior darkness does not bleed into the probe.
84
+ *
85
+ * 2. Indirect phase: `indirect_sample_count` continuations are picked from the hit candidates with
86
+ * probability proportional to their first-bounce luminance (resampled importance sampling — the
87
+ * selection a weighted reservoir would produce, drawn from the materialized candidate table), and
88
+ * only these few directions pay for a full multi-bounce path trace. The 1/p_hat re-sampling weight
89
+ * keeps the combined estimator unbiased.
90
+ *
91
+ * The two phases sum to the same integral as tracing every direction to full depth, but direct light
92
+ * converges with the full candidate count while the expensive multi-bounce work is spent where the
93
+ * first bounce found energy.
94
+ *
95
+ * @param {number[]|Float32Array|Float64Array} out 27 values: SH3 radiance, coefficient-major RGB triplets
96
+ * @param {number} out_offset
97
+ * @param {PathTracedScene} scene
98
+ * @param {PathTracer} tracer
99
+ * @param {function():number} random
100
+ * @param {number[]|Float32Array} position probe position
101
+ * @param {number} position_offset
102
+ * @param {number} candidate_count directions sampled one hit deep, at least 1
103
+ * @param {number} indirect_sample_count full path-traced continuations re-sampled from the candidates
104
+ * @param {number} max_bounce_count total path depth including the first hit, at least 1
105
+ * @returns {number} fraction of candidate directions whose first hit was a back face, in [0, 1]
106
+ */
107
+ export function bake_probe_sh3_resampled(
108
+ out, out_offset,
109
+ scene, tracer, random,
110
+ position, position_offset,
111
+ candidate_count, indirect_sample_count, max_bounce_count
112
+ ) {
113
+ assert.isArrayLike(out, 'out');
114
+ assert.isNonNegativeInteger(out_offset, 'out_offset');
115
+ assert.isInstanceOf(scene, PathTracedScene, 'scene');
116
+ assert.isInstanceOf(tracer, PathTracer, 'tracer');
117
+ assert.isFunction(random, 'random');
118
+ assert.isArrayLike(position, 'position');
119
+ assert.isNonNegativeInteger(position_offset, 'position_offset');
120
+ assert.isNonNegativeInteger(candidate_count, 'candidate_count');
121
+ assert.greaterThan(candidate_count, 0, 'candidate_count');
122
+ assert.isNonNegativeInteger(indirect_sample_count, 'indirect_sample_count');
123
+ assert.isNonNegativeInteger(max_bounce_count, 'max_bounce_count');
124
+ assert.greaterThan(max_bounce_count, 0, 'max_bounce_count');
125
+
126
+ ensure_pool_capacity(candidate_count);
127
+
128
+ __sh_direct.fill(0);
129
+ __sh_indirect.fill(0);
130
+
131
+ const position_x = position[position_offset];
132
+ const position_y = position[position_offset + 1];
133
+ const position_z = position[position_offset + 2];
134
+
135
+ let hit_count = 0;
136
+ let valid_count = 0;
137
+ let backface_count = 0;
138
+ let luminance_sum = 0;
139
+
140
+ for (let i = 0; i < candidate_count; i++) {
141
+
142
+ __ray[0] = position_x;
143
+ __ray[1] = position_y;
144
+ __ray[2] = position_z;
145
+
146
+ random_point_on_sphere(random, __ray, 3);
147
+
148
+ __ray.tMax = Infinity;
149
+
150
+ const direction_x = __ray[3];
151
+ const direction_y = __ray[4];
152
+ const direction_z = __ray[5];
153
+
154
+ const hit_distance = scene.trace(__hit, __ray);
155
+
156
+ if (hit_distance < 0) {
157
+ // sky
158
+ scene.sample_background(__radiance, 0, __ray, 3);
159
+ } else if (v3_dot(
160
+ __hit[3], __hit[4], __hit[5],
161
+ direction_x, direction_y, direction_z
162
+ ) > 0) {
163
+ // the first hit is a back face: this direction sees the inside of geometry
164
+ backface_count++;
165
+ continue;
166
+ } else {
167
+ scene.sample_material(__surface, __hit, __ray);
168
+
169
+ // stage the surface state in the ray: origin at the hit, direction holding the shading
170
+ // normal, then step off the surface along the normal — mirrors PathTracer exactly, so
171
+ // shadow rays and continuations start from the same offset point the tracer would use
172
+ __ray[0] = __hit[0];
173
+ __ray[1] = __hit[1];
174
+ __ray[2] = __hit[2];
175
+
176
+ __ray[3] = __surface[3];
177
+ __ray[4] = __surface[4];
178
+ __ray[5] = __surface[5];
179
+
180
+ const origin_magnitude = Math.max(Math.abs(__ray[0]), Math.abs(__ray[1]), Math.abs(__ray[2]));
181
+ __ray.shiftForward(SELF_OCCLUSION_OFFSET * Math.max(1, origin_magnitude));
182
+
183
+ scene.sample_lights(__direct, 0, __ray);
184
+
185
+ // first-bounce radiance: emission plus albedo-modulated direct light
186
+ __radiance[0] = __surface[6] + __surface[0] * __direct[0];
187
+ __radiance[1] = __surface[7] + __surface[1] * __direct[1];
188
+ __radiance[2] = __surface[8] + __surface[2] * __direct[2];
189
+
190
+ // record the candidate for indirect re-sampling
191
+ const candidate_address = hit_count * CANDIDATE_STRIDE;
192
+
193
+ __candidates[candidate_address] = direction_x;
194
+ __candidates[candidate_address + 1] = direction_y;
195
+ __candidates[candidate_address + 2] = direction_z;
196
+
197
+ __candidates[candidate_address + 3] = __ray[0];
198
+ __candidates[candidate_address + 4] = __ray[1];
199
+ __candidates[candidate_address + 5] = __ray[2];
200
+
201
+ __candidates[candidate_address + 6] = __surface[3];
202
+ __candidates[candidate_address + 7] = __surface[4];
203
+ __candidates[candidate_address + 8] = __surface[5];
204
+
205
+ __candidates[candidate_address + 9] = __surface[0];
206
+ __candidates[candidate_address + 10] = __surface[1];
207
+ __candidates[candidate_address + 11] = __surface[2];
208
+
209
+ const luminance = rgb_to_luminance(__radiance[0], __radiance[1], __radiance[2]);
210
+
211
+ __p_hat[hit_count] = luminance;
212
+ luminance_sum += luminance;
213
+
214
+ hit_count++;
215
+ }
216
+
217
+ valid_count++;
218
+
219
+ sh3_basis_at(direction_x, direction_y, direction_z, __basis);
220
+ sh3_vector_accumulate(__sh_direct, 0, __basis, 0, __radiance, 0, 3);
221
+ }
222
+
223
+ const backface_fraction = backface_count / candidate_count;
224
+
225
+ if (valid_count === 0) {
226
+ // probe is fully embedded in geometry, there is no usable radiance
227
+ for (let i = 0; i < 27; i++) {
228
+ out[out_offset + i] = 0;
229
+ }
230
+
231
+ return backface_fraction;
232
+ }
233
+
234
+ let total_p_hat = 0;
235
+
236
+ if (max_bounce_count > 1 && indirect_sample_count > 0 && hit_count > 0) {
237
+
238
+ // floor the weights so indirectly-lit surfaces stay selectable; when nothing received any
239
+ // direct light the selection degenerates to uniform (selection is scale-invariant in p_hat)
240
+ const mean_luminance = luminance_sum / hit_count;
241
+ const weight_floor = mean_luminance > 0 ? mean_luminance * RESAMPLING_WEIGHT_FLOOR : 1;
242
+
243
+ for (let i = 0; i < hit_count; i++) {
244
+ const p_hat = __p_hat[i] + weight_floor;
245
+
246
+ __p_hat[i] = p_hat;
247
+ total_p_hat += p_hat;
248
+
249
+ __cumulative_p_hat[i] = total_p_hat;
250
+ }
251
+
252
+ for (let k = 0; k < indirect_sample_count; k++) {
253
+ const pick = pick_candidate(random() * total_p_hat, hit_count);
254
+
255
+ const candidate_address = pick * CANDIDATE_STRIDE;
256
+
257
+ __ray[0] = __candidates[candidate_address + 3];
258
+ __ray[1] = __candidates[candidate_address + 4];
259
+ __ray[2] = __candidates[candidate_address + 5];
260
+
261
+ // cosine-weighted continuation about the shading normal, matching the tracer's own
262
+ // bounce sampling; the albedo/pi BRDF and the cosine/pi pdf cancel to a bare albedo scale
263
+ getBiasedNormalSample(__ray, 3, __candidates, candidate_address + 6, 1, random);
264
+
265
+ __ray.tMax = Infinity;
266
+
267
+ tracer.path_trace(__radiance, __ray, 1, max_bounce_count - 1, random, scene);
268
+
269
+ const resampling_weight = 1 / __p_hat[pick];
270
+
271
+ __radiance[0] *= __candidates[candidate_address + 9] * resampling_weight;
272
+ __radiance[1] *= __candidates[candidate_address + 10] * resampling_weight;
273
+ __radiance[2] *= __candidates[candidate_address + 11] * resampling_weight;
274
+
275
+ sh3_basis_at(
276
+ __candidates[candidate_address],
277
+ __candidates[candidate_address + 1],
278
+ __candidates[candidate_address + 2],
279
+ __basis
280
+ );
281
+ sh3_vector_accumulate(__sh_indirect, 0, __basis, 0, __radiance, 0, 3);
282
+ }
283
+ }
284
+
285
+ // back-face directions are excluded from the domain: normalizing by the valid count spreads the
286
+ // radiance of the visible directions over the full sphere, so partially embedded probes represent
287
+ // the open side instead of averaging in interior darkness
288
+ const direct_scale = (4 * Math.PI) / valid_count;
289
+ const indirect_scale = indirect_sample_count > 0
290
+ ? direct_scale * (total_p_hat / indirect_sample_count)
291
+ : 0;
292
+
293
+ for (let i = 0; i < 27; i++) {
294
+ out[out_offset + i] = __sh_direct[i] * direct_scale + __sh_indirect[i] * indirect_scale;
295
+ }
296
+
297
+ return backface_fraction;
298
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Fill invalid probes with the average of their valid 6-connected neighbors, expanding in waves until
3
+ * every reachable probe is filled.
4
+ *
5
+ * Probes marked invalid (typically because they sit inside geometry) would otherwise contribute their
6
+ * meaningless data to hardware trilinear filtering and bleed darkness across walls. Each pass reads
7
+ * only probes that were valid before the pass started, so the result is independent of traversal
8
+ * order. Probes with no valid probe anywhere in their connected region are left untouched.
9
+ *
10
+ * @param {Float32Array} harmonics probe data, {@link PROBE_VALUE_COUNT} values per probe; filled probes are written in place
11
+ * @param {Uint8Array} validity one flag per probe, non-zero = valid; filled probes are marked valid in place
12
+ * @param {number} resolution_x
13
+ * @param {number} resolution_y
14
+ * @param {number} resolution_z
15
+ * @returns {number} number of probes that remained invalid
16
+ */
17
+ export function light_probe_grid_dilate(harmonics: Float32Array, validity: Uint8Array, resolution_x: number, resolution_y: number, resolution_z: number): number;
18
+ //# sourceMappingURL=light_probe_grid_dilate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"light_probe_grid_dilate.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/graphics/sh3/lpg/light_probe_grid_dilate.js"],"names":[],"mappings":"AAYA;;;;;;;;;;;;;;;GAeG;AACH,mDAPW,YAAY,YACZ,UAAU,gBACV,MAAM,gBACN,MAAM,gBACN,MAAM,GACJ,MAAM,CAsGlB"}
@@ -0,0 +1,142 @@
1
+ import { assert } from "../../../../core/assert.js";
2
+
3
+ /**
4
+ * Values per probe in {@link LightProbeGrid#harmonics}
5
+ * @type {number}
6
+ */
7
+ const PROBE_VALUE_COUNT = 12;
8
+
9
+ const __accumulator = new Float64Array(PROBE_VALUE_COUNT);
10
+
11
+ let __source_validity = new Uint8Array(0);
12
+
13
+ /**
14
+ * Fill invalid probes with the average of their valid 6-connected neighbors, expanding in waves until
15
+ * every reachable probe is filled.
16
+ *
17
+ * Probes marked invalid (typically because they sit inside geometry) would otherwise contribute their
18
+ * meaningless data to hardware trilinear filtering and bleed darkness across walls. Each pass reads
19
+ * only probes that were valid before the pass started, so the result is independent of traversal
20
+ * order. Probes with no valid probe anywhere in their connected region are left untouched.
21
+ *
22
+ * @param {Float32Array} harmonics probe data, {@link PROBE_VALUE_COUNT} values per probe; filled probes are written in place
23
+ * @param {Uint8Array} validity one flag per probe, non-zero = valid; filled probes are marked valid in place
24
+ * @param {number} resolution_x
25
+ * @param {number} resolution_y
26
+ * @param {number} resolution_z
27
+ * @returns {number} number of probes that remained invalid
28
+ */
29
+ export function light_probe_grid_dilate(
30
+ harmonics,
31
+ validity,
32
+ resolution_x, resolution_y, resolution_z
33
+ ) {
34
+ assert.isNonNegativeInteger(resolution_x, 'resolution_x');
35
+ assert.isNonNegativeInteger(resolution_y, 'resolution_y');
36
+ assert.isNonNegativeInteger(resolution_z, 'resolution_z');
37
+
38
+ const probe_count = resolution_x * resolution_y * resolution_z;
39
+
40
+ assert.greaterThanOrEqual(validity.length, probe_count, 'validity.length');
41
+ assert.greaterThanOrEqual(harmonics.length, probe_count * PROBE_VALUE_COUNT, 'harmonics.length');
42
+
43
+ if (__source_validity.length < probe_count) {
44
+ __source_validity = new Uint8Array(probe_count);
45
+ }
46
+
47
+ let invalid_count = 0;
48
+
49
+ for (let i = 0; i < probe_count; i++) {
50
+ if (validity[i] === 0) {
51
+ invalid_count++;
52
+ }
53
+ }
54
+
55
+ const slice_size = resolution_x * resolution_y;
56
+
57
+ let filled_this_pass = 1;
58
+
59
+ while (invalid_count > 0 && filled_this_pass > 0) {
60
+ filled_this_pass = 0;
61
+
62
+ // snapshot so each pass only reads probes that were valid before it started
63
+ __source_validity.set(validity.subarray(0, probe_count));
64
+
65
+ for (let z = 0; z < resolution_z; z++) {
66
+ for (let y = 0; y < resolution_y; y++) {
67
+ for (let x = 0; x < resolution_x; x++) {
68
+
69
+ const index = x + resolution_x * y + slice_size * z;
70
+
71
+ if (__source_validity[index] !== 0) {
72
+ continue;
73
+ }
74
+
75
+ __accumulator.fill(0);
76
+ let neighbor_count = 0;
77
+
78
+ if (x > 0 && __source_validity[index - 1] !== 0) {
79
+ accumulate_probe(harmonics, index - 1);
80
+ neighbor_count++;
81
+ }
82
+
83
+ if (x < resolution_x - 1 && __source_validity[index + 1] !== 0) {
84
+ accumulate_probe(harmonics, index + 1);
85
+ neighbor_count++;
86
+ }
87
+
88
+ if (y > 0 && __source_validity[index - resolution_x] !== 0) {
89
+ accumulate_probe(harmonics, index - resolution_x);
90
+ neighbor_count++;
91
+ }
92
+
93
+ if (y < resolution_y - 1 && __source_validity[index + resolution_x] !== 0) {
94
+ accumulate_probe(harmonics, index + resolution_x);
95
+ neighbor_count++;
96
+ }
97
+
98
+ if (z > 0 && __source_validity[index - slice_size] !== 0) {
99
+ accumulate_probe(harmonics, index - slice_size);
100
+ neighbor_count++;
101
+ }
102
+
103
+ if (z < resolution_z - 1 && __source_validity[index + slice_size] !== 0) {
104
+ accumulate_probe(harmonics, index + slice_size);
105
+ neighbor_count++;
106
+ }
107
+
108
+ if (neighbor_count === 0) {
109
+ continue;
110
+ }
111
+
112
+ const probe_address = index * PROBE_VALUE_COUNT;
113
+ const inverse_count = 1 / neighbor_count;
114
+
115
+ for (let i = 0; i < PROBE_VALUE_COUNT; i++) {
116
+ harmonics[probe_address + i] = __accumulator[i] * inverse_count;
117
+ }
118
+
119
+ validity[index] = 1;
120
+ filled_this_pass++;
121
+ }
122
+ }
123
+ }
124
+
125
+ invalid_count -= filled_this_pass;
126
+ }
127
+
128
+ return invalid_count;
129
+ }
130
+
131
+ /**
132
+ *
133
+ * @param {Float32Array} harmonics
134
+ * @param {number} probe_index
135
+ */
136
+ function accumulate_probe(harmonics, probe_index) {
137
+ const address = probe_index * PROBE_VALUE_COUNT;
138
+
139
+ for (let i = 0; i < PROBE_VALUE_COUNT; i++) {
140
+ __accumulator[i] += harmonics[address + i];
141
+ }
142
+ }
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Base self-occlusion offset; scaled by coordinate magnitude so it stays above the Float32 ULP
3
+ * at large world scales
4
+ * @type {number}
5
+ */
6
+ export const SELF_OCCLUSION_OFFSET: number;
1
7
  export class PathTracer {
2
8
  /**
3
9
  *
@@ -1 +1 @@
1
- {"version":3,"file":"PathTracer.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/graphics/sh3/path_tracer/PathTracer.js"],"names":[],"mappings":"AA+CA;IAGI;;;;;;;;OAQG;IACH,gBAPW,MAAM,EAAE,GAAC,YAAY,OACrB,MAAM,EAAE,GAAC,YAAY,GAAC,IAAI,cAC1B,MAAM,cACN,MAAM,kDAuFhB;CACJ;qBA7IoB,sCAAsC"}
1
+ {"version":3,"file":"PathTracer.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/graphics/sh3/path_tracer/PathTracer.js"],"names":[],"mappings":"AAsCA;;;;GAIG;AACH,oCAFU,MAAM,CAE6B;AAI7C;IAGI;;;;;;;;OAQG;IACH,gBAPW,MAAM,EAAE,GAAC,YAAY,OACrB,MAAM,EAAE,GAAC,YAAY,GAAC,IAAI,cAC1B,MAAM,cACN,MAAM,kDAuFhB;CACJ;qBA7IoB,sCAAsC"}
@@ -41,7 +41,7 @@ const RUSSIAN_ROULETTE_SURVIVAL_RECIPROCAL = 1 / (1 - RUSSIAN_ROULETTE_PROBABILI
41
41
  * at large world scales
42
42
  * @type {number}
43
43
  */
44
- const SELF_OCCLUSION_OFFSET = 0.00001;
44
+ export const SELF_OCCLUSION_OFFSET = 0.00001;
45
45
 
46
46
  const color_mask = new Float32Array(3);
47
47
 
@@ -1,14 +1,16 @@
1
1
  /**
2
2
  * Fixed-function simulation for {@link TubeX} tubes — the 3D analogue of
3
- * {@link RibbonXFixedPhysicsSimulator}. Ages every knot, fades its alpha to zero
4
- * as it approaches `max_age`, and flows the along-tube UV coordinate with age.
3
+ * {@link RibbonXFixedPhysicsSimulator}. Ages every knot, fades its alpha from
4
+ * `alpha` to zero as it approaches `max_age`, and flows the along-tube UV
5
+ * coordinate with age.
5
6
  */
6
7
  export class TubeXFixedPhysicsSimulator {
7
8
  /**
8
9
  * @param {TubeX} tube
9
10
  * @param {number} max_age
10
11
  * @param {number} timeDelta
12
+ * @param {number} [alpha] base alpha of an age-0 knot; the age fade scales it
11
13
  */
12
- update(tube: TubeX, max_age: number, timeDelta: number): void;
14
+ update(tube: TubeX, max_age: number, timeDelta: number, alpha?: number): void;
13
15
  }
14
16
  //# sourceMappingURL=TubeXFixedPhysicsSimulator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TubeXFixedPhysicsSimulator.d.ts","sourceRoot":"","sources":["../../../../../../../src/engine/graphics/trail/tube/simulator/TubeXFixedPhysicsSimulator.js"],"names":[],"mappings":"AAGA;;;;GAIG;AACH;IAEI;;;;OAIG;IACH,6BAHW,MAAM,aACN,MAAM,QAiBhB;CACJ"}
1
+ {"version":3,"file":"TubeXFixedPhysicsSimulator.d.ts","sourceRoot":"","sources":["../../../../../../../src/engine/graphics/trail/tube/simulator/TubeXFixedPhysicsSimulator.js"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH;IAEI;;;;;OAKG;IACH,6BAJW,MAAM,aACN,MAAM,UACN,MAAM,QAiBhB;CACJ"}
@@ -3,8 +3,9 @@ import { TUBE_ATTRIBUTE_ADDRESS_AGE, TUBE_ATTRIBUTE_ADDRESS_UV_V } from "../tube
3
3
 
4
4
  /**
5
5
  * Fixed-function simulation for {@link TubeX} tubes — the 3D analogue of
6
- * {@link RibbonXFixedPhysicsSimulator}. Ages every knot, fades its alpha to zero
7
- * as it approaches `max_age`, and flows the along-tube UV coordinate with age.
6
+ * {@link RibbonXFixedPhysicsSimulator}. Ages every knot, fades its alpha from
7
+ * `alpha` to zero as it approaches `max_age`, and flows the along-tube UV
8
+ * coordinate with age.
8
9
  */
9
10
  export class TubeXFixedPhysicsSimulator {
10
11
 
@@ -12,8 +13,9 @@ export class TubeXFixedPhysicsSimulator {
12
13
  * @param {TubeX} tube
13
14
  * @param {number} max_age
14
15
  * @param {number} timeDelta
16
+ * @param {number} [alpha] base alpha of an age-0 knot; the age fade scales it
15
17
  */
16
- update(tube, max_age, timeDelta) {
18
+ update(tube, max_age, timeDelta, alpha = 1) {
17
19
 
18
20
  const n = tube.getCount();
19
21
 
@@ -23,7 +25,7 @@ export class TubeXFixedPhysicsSimulator {
23
25
  const relative_age = clamp01(age / max_age);
24
26
 
25
27
  // fade out with age
26
- tube.setKnotAlpha(i, 1.0 - relative_age);
28
+ tube.setKnotAlpha(i, (1.0 - relative_age) * alpha);
27
29
 
28
30
  // flow the along-tube UV with age
29
31
  tube.setKnotAttribute_Scalar(i, TUBE_ATTRIBUTE_ADDRESS_UV_V, relative_age);
@@ -7,7 +7,8 @@ export class RibbonXFixedPhysicsSimulator {
7
7
  * @param {RibbonX} ribbon
8
8
  * @param {number} max_age
9
9
  * @param {number} timeDelta
10
+ * @param {number} [alpha] base alpha of an age-0 point; the age fade scales it
10
11
  */
11
- update(ribbon: RibbonX, max_age: number, timeDelta: number): void;
12
+ update(ribbon: RibbonX, max_age: number, timeDelta: number, alpha?: number): void;
12
13
  }
13
14
  //# sourceMappingURL=RibbonXFixedPhysicsSimulator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RibbonXFixedPhysicsSimulator.d.ts","sourceRoot":"","sources":["../../../../../../../src/engine/graphics/trail/x/simulator/RibbonXFixedPhysicsSimulator.js"],"names":[],"mappings":"AAIA;;GAEG;AACH;IAEI;;;;;OAKG;IACH,iCAHW,MAAM,aACN,MAAM,QAkBhB;CACJ"}
1
+ {"version":3,"file":"RibbonXFixedPhysicsSimulator.d.ts","sourceRoot":"","sources":["../../../../../../../src/engine/graphics/trail/x/simulator/RibbonXFixedPhysicsSimulator.js"],"names":[],"mappings":"AAIA;;GAEG;AACH;IAEI;;;;;;OAMG;IACH,iCAJW,MAAM,aACN,MAAM,UACN,MAAM,QAkBhB;CACJ"}
@@ -12,8 +12,9 @@ export class RibbonXFixedPhysicsSimulator {
12
12
  * @param {RibbonX} ribbon
13
13
  * @param {number} max_age
14
14
  * @param {number} timeDelta
15
+ * @param {number} [alpha] base alpha of an age-0 point; the age fade scales it
15
16
  */
16
- update(ribbon, max_age, timeDelta) {
17
+ update(ribbon, max_age, timeDelta, alpha = 1) {
17
18
 
18
19
  const n = ribbon.getCount();
19
20
  for (let i = 0; i < n; i++) {
@@ -23,7 +24,7 @@ export class RibbonXFixedPhysicsSimulator {
23
24
  const relative_age = clamp01(age / max_age);
24
25
 
25
26
  // apply alpha over-time
26
- ribbon.setPointAlpha(i, 1.0 - relative_age);
27
+ ribbon.setPointAlpha(i, (1.0 - relative_age) * alpha);
27
28
 
28
29
  ribbon.setPointAttribute_Scalar(i, RIBBON_ATTRIBUTE_ADDRESS_UV_OFFSET, relative_age);
29
30
  }