@woosh/meep-engine 3.17.0 → 3.17.1

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 (24) hide show
  1. package/README.md +8 -72
  2. package/package.json +1 -1
  3. package/src/engine/ecs/transform/t64_set_rotation_axis_angle.d.ts +25 -0
  4. package/src/engine/ecs/transform/t64_set_rotation_axis_angle.d.ts.map +1 -0
  5. package/src/engine/ecs/transform/t64_set_rotation_axis_angle.js +45 -0
  6. package/src/engine/sound/simulation/AcousticSimulator.d.ts +11 -8
  7. package/src/engine/sound/simulation/AcousticSimulator.d.ts.map +1 -1
  8. package/src/engine/sound/simulation/AcousticSimulator.js +45 -36
  9. package/src/engine/sound/simulation/README.md +2 -2
  10. package/src/engine/sound/simulation/core/OcclusionSolver.d.ts.map +1 -1
  11. package/src/engine/sound/simulation/core/VolumeField.d.ts +9 -9
  12. package/src/engine/sound/simulation/core/VolumeField.d.ts.map +1 -1
  13. package/src/engine/sound/simulation/core/VolumeField.js +29 -13
  14. package/src/engine/sound/simulation/core/acoustic_occlusion_solve.d.ts +31 -0
  15. package/src/engine/sound/simulation/core/acoustic_occlusion_solve.d.ts.map +1 -0
  16. package/src/engine/sound/simulation/core/acoustic_occlusion_solve.js +134 -0
  17. package/src/shade/playground/volumetrics_froxel/main.d.ts.map +1 -1
  18. package/src/shade/playground/volumetrics_froxel/main.js +6 -1
  19. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts +7 -0
  20. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts.map +1 -1
  21. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.js +7 -0
  22. package/src/engine/sound/simulation/core/OcclusionSolver.js +0 -112
  23. package/src/shade/renderer/particles/runtime/ProgramArena.d.ts +0 -115
  24. package/src/shade/renderer/particles/runtime/ProgramArena.d.ts.map +0 -1
package/README.md CHANGED
@@ -8,11 +8,11 @@
8
8
  npm install @woosh/meep-engine
9
9
  ```
10
10
 
11
- **Start here:** [`samples/engine/README.md`](samples/engine/README.md) — the first thirty minutes,
12
- with the Vite stanza you need, why your scene should be authored in metres, photometric reference
13
- values, and how input reaches the engine. The worked example beside it,
14
- [`samples/engine/first-scene`](samples/engine/first-scene), boots the engine, lights a scene and
15
- reads the keyboard in about eighty lines.
11
+ **Start here:** the package ships a guide to the first thirty minutes at
12
+ `samples/engine/README.md` - the Vite stanza you need, why your scene should be authored in
13
+ metres, photometric reference values, and how input reaches the engine. The worked example beside
14
+ it, `samples/engine/first-scene`, boots the engine, lights a scene and reads the keyboard in about
15
+ eighty lines.
16
16
 
17
17
  **Proprietary and source-available.** You get all ~6,200 fine-grained ES modules, unobfuscated,
18
18
  under a licence that lets you ship. Need a lerp function? Import the module that holds it and pay
@@ -32,10 +32,9 @@ on paper and still not have them working together - and every plugin is another
32
32
  tune, per scene and per device.
33
33
 
34
34
  **Shadows, ambient occlusion, indirect lighting, auto-exposure, anti-aliasing and transparency are
35
- all on by default, and there is nothing to configure to get a correct-looking frame.** No quality
36
- tiers to author, no per-device profiles, no exposure value to pick, no shadow cascade splits to
37
- hand-tune. You put a scene together, it looks right, and it holds its frame rate across a wide
38
- range of hardware without you doing anything about it.
35
+ all on by default, with nothing to configure.** No quality tiers to author, no per-device profiles,
36
+ no exposure value to pick, no shadow cascade splits to hand-tune. You put a scene together and it
37
+ holds its frame rate across a wide range of hardware without you doing anything about it.
39
38
 
40
39
  In development since 2013, shipped in a commercial Steam release, and held to **~15,000 tests** -
41
40
  determinism, adversarial and shader-emulation suites among them.
@@ -74,39 +73,6 @@ occlusion culling, the frame graph - they are in **the renderer contract**, whic
74
73
  package at `src/shade/RENDERER_CONTRACT.md`. It also covers how to put a model on the screen and
75
74
  how to add your own work to the frame.
76
75
 
77
- ## How the features interact
78
-
79
- What a feature list cannot tell you is whether the features work together once they are in the same
80
- scene. This can.
81
-
82
- | | Lit by scene lights | Shadows | Stable when it moves | Indirect light |
83
- |---|---|---|---|---|
84
- | Static meshes | ✅ | casts + receives | ✅ | ✅ |
85
- | **Skinned characters** | ✅ | casts + receives | ✅ **per-vertex** | ✅ |
86
- | Terrain | ✅ | casts + receives | ✅ | ✅ |
87
- | Decals | ✅ | receives | ✅ follows the surface | ✅ |
88
- | Transparent surfaces | ✅ | receives | ❌ | ✅ |
89
- | Volumetric fog | ✅ | shadowed by them | ✅ | n/a |
90
- | Water, particles, trails, overlays | ❌ | ❌ | ❌ | ❌ |
91
-
92
- Those top six are not six systems that were each taught about lighting. They read the same lighting,
93
- the same shadows and the same motion data, because there is only one of each. Three consequences:
94
-
95
- * **Animated characters are not a special case.** A skinned character goes down the same path as a
96
- static one: same culling, same shadows, same lighting. It also produces motion data per vertex
97
- rather than per object, so a swinging arm resolves cleanly under anti-aliasing instead of
98
- smearing - which is the usual failure of bolt-on temporal AA.
99
- * **A decal is lit like the surface it lands on.** Decals modify the surface itself rather than
100
- painting over the finished image, so a scorch mark is lit, shadowed and bounced exactly like the
101
- wall under it, and ambient occlusion and reflections see it too.
102
- * **Ordering is declared, not accidental.** Systems state what they must come after, by name, and
103
- the renderer resolves the order. Decals declare that they follow terrain, so scorch marks land on
104
- textured ground - rather than depending on which system happened to register first.
105
-
106
- Water, particles, trails and screen overlays are drawn after lighting, so they are not lit or
107
- shadowed and contribute no motion data. They read the scene's depth, so they are correctly hidden
108
- behind things in front of them.
109
-
110
76
  ## Scaling across hardware
111
77
 
112
78
  **One build covers a wide hardware range, and you don't author the tiers.** When frames start
@@ -165,36 +131,6 @@ skinned objects sample as they move through it. It bakes in-engine, from a live
165
131
  so the precompute is a call you make rather than a tool you leave the engine for. Opaque and
166
132
  transparent surfaces both use it, so turning it on does not leave them lit differently.
167
133
 
168
- ## WebGPU only
169
-
170
- WebGPU is not a better WebGL - it is a different machine. Compute shaders, storage buffers and
171
- indirect draw are what let visibility be decided on the GPU and a whole scene be submitted in a
172
- couple of draw calls. None of that has an equivalent in WebGL, so supporting both would mean either
173
- a second renderer to maintain or holding this one down to what the older API can express. meep
174
- targets WebGPU, and everything above follows from that.
175
-
176
- The floor is fixed and checked before anything is created: WebGPU plus a small set of capabilities
177
- that mainstream desktop GPUs have had for years. Hardware that does not clear it gets a clear
178
- failure message written to be shown to a person, rather than a degraded picture. The exact features
179
- and limits are in §2 of `src/shade/RENDERER_CONTRACT.md`.
180
-
181
- To see what a given browser and GPU actually report, open
182
- **[webgpu.report](https://webgpu.report/)** on the machine in question - it lists the adapter's
183
- features and limits, which is what the floor is checked against.
184
-
185
- ## Numbers
186
-
187
- There is no published benchmark for the renderer yet. What the pipeline does differently is
188
- checkable in the source without running anything - draw count independent of object count, one
189
- lighting pass independent of light count, material cost independent of overdraw - and the tools to
190
- measure the rest ship with it.
191
-
192
- The renderer records `.sgpt` GPU profile captures - per-pass timings, and at higher capture levels
193
- the frame-graph structure, dispatch and draw counts. Assign a
194
- `GPUProfileSession({ level: GPUProfileLevel.WORKLOAD })` to `renderer.profile_session`, call
195
- `start()`, run frames, and `stop()` returns an `ArrayBuffer` to write out. The recorder and the
196
- format specification (`src/shade/device/timing/profile/SGPT_FORMAT.md`) both ship in this package.
197
-
198
134
  ---
199
135
 
200
136
  ## Everything else
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "description": "Advanced 3D game engine",
7
7
  "type": "module",
8
8
  "author": "Alexander Goldring",
9
- "version": "3.17.0",
9
+ "version": "3.17.1",
10
10
  "exports": {
11
11
  "./build/*": "./build/*",
12
12
  "./src/*": "./src/*",
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Set a {@link Transform64}'s rotation to `angle` radians about the axis `(ax, ay, az)`, right-handed.
3
+ *
4
+ * The {@link Transform64} counterpart of `Quaternion.fromAxisAngle`, and it defers to
5
+ * `Quaternion._fromAxisAngle` for the same reason {@link t64_look_rotation} defers to
6
+ * `_lookRotation` — one copy of the arithmetic, and `Quaternion` is itself f64, so routing through
7
+ * one costs no precision.
8
+ *
9
+ * The axis must be unit length. Unlike {@link t64_look_rotation}, which takes a direction whose
10
+ * length means nothing, here it means something and is not recovered by normalizing the result: for
11
+ * an axis of length `k` the quaternion `(axis·sin(angle/2), cos(angle/2))` normalizes to a rotation
12
+ * about the same axis by `2·atan(k·tan(angle/2))`, which is `angle` only for `k = 1`. So a longer
13
+ * axis does not fail, it quietly turns further — hence the assert, which the consumer strips.
14
+ *
15
+ * Nothing here touches `t64`'s matrix — call {@link Transform64#updateMatrix} once you are done
16
+ * setting components.
17
+ *
18
+ * @param {Transform64} t64
19
+ * @param {number} ax axis, unit length
20
+ * @param {number} ay axis, unit length
21
+ * @param {number} az axis, unit length
22
+ * @param {number} angle radians
23
+ */
24
+ export function t64_set_rotation_axis_angle(t64: Transform64, ax: number, ay: number, az: number, angle: number): void;
25
+ //# sourceMappingURL=t64_set_rotation_axis_angle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"t64_set_rotation_axis_angle.d.ts","sourceRoot":"","sources":["../../../../../src/engine/ecs/transform/t64_set_rotation_axis_angle.js"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,kEALW,MAAM,MACN,MAAM,MACN,MAAM,SACN,MAAM,QAehB"}
@@ -0,0 +1,45 @@
1
+ import { assert } from "../../../core/assert.js";
2
+ import { Quaternion } from "../../../core/geom/Quaternion.js";
3
+
4
+ /**
5
+ * @type {Quaternion}
6
+ */
7
+ const SCRATCH_ROTATION = new Quaternion();
8
+
9
+ /**
10
+ * Set a {@link Transform64}'s rotation to `angle` radians about the axis `(ax, ay, az)`, right-handed.
11
+ *
12
+ * The {@link Transform64} counterpart of `Quaternion.fromAxisAngle`, and it defers to
13
+ * `Quaternion._fromAxisAngle` for the same reason {@link t64_look_rotation} defers to
14
+ * `_lookRotation` — one copy of the arithmetic, and `Quaternion` is itself f64, so routing through
15
+ * one costs no precision.
16
+ *
17
+ * The axis must be unit length. Unlike {@link t64_look_rotation}, which takes a direction whose
18
+ * length means nothing, here it means something and is not recovered by normalizing the result: for
19
+ * an axis of length `k` the quaternion `(axis·sin(angle/2), cos(angle/2))` normalizes to a rotation
20
+ * about the same axis by `2·atan(k·tan(angle/2))`, which is `angle` only for `k = 1`. So a longer
21
+ * axis does not fail, it quietly turns further — hence the assert, which the consumer strips.
22
+ *
23
+ * Nothing here touches `t64`'s matrix — call {@link Transform64#updateMatrix} once you are done
24
+ * setting components.
25
+ *
26
+ * @param {Transform64} t64
27
+ * @param {number} ax axis, unit length
28
+ * @param {number} ay axis, unit length
29
+ * @param {number} az axis, unit length
30
+ * @param {number} angle radians
31
+ */
32
+ export function t64_set_rotation_axis_angle(t64, ax, ay, az, angle) {
33
+ // a constant message on purpose - a template literal would build a string on every call, and
34
+ // this is vocabulary the migration puts in hot paths
35
+ assert.ok(Math.abs(ax * ax + ay * ay + az * az - 1) < 1e-6, 'axis must be unit length');
36
+
37
+ SCRATCH_ROTATION._fromAxisAngle(ax, ay, az, angle);
38
+
39
+ t64.setRotation(
40
+ SCRATCH_ROTATION[0],
41
+ SCRATCH_ROTATION[1],
42
+ SCRATCH_ROTATION[2],
43
+ SCRATCH_ROTATION[3]
44
+ );
45
+ }
@@ -103,22 +103,26 @@ export class AcousticSimulator {
103
103
  setProbeField(field: AcousticProbeField | null): void;
104
104
  /**
105
105
  * Compute the full acoustic solution for a source at `source`, given `listener`, into `out`.
106
- * @param {Vector3} listener
107
- * @param {Vector3} source
108
- * @param {number} sourceRadius apparent radius of the source (per-source; 0 = point source). For
106
+ *
107
+ * Both positions are read BY INDEX rather than through `.x/.y/.z`: they are live views over
108
+ * {@link Transform64} buffers (plain `Float64Array`s, no component accessors). A {@link Vector3}
109
+ * is a `Float64Array` subclass whose `[0..2]` alias `x/y/z`, so indexing reads both shapes.
110
+ * @param {number[]|Float32Array|Float64Array} listener listener world position, 3 elements
111
+ * @param {number[]|Float32Array|Float64Array} source source world position, 3 elements
112
+ * @param {number} source_radius apparent radius of the source (per-source; 0 = point source). For
109
113
  * volumetric (fractional) occlusion.
110
114
  * @param {AcousticSourceState} state per-source temporal accumulation (mutated)
111
115
  * @param {AcousticSolution} out
112
116
  */
113
- solveFor(listener: Vector3, source: Vector3, sourceRadius: number, state: AcousticSourceState, out: AcousticSolution): void;
117
+ solveFor(listener: number[] | Float32Array | Float64Array, source: number[] | Float32Array | Float64Array, source_radius: number, state: AcousticSourceState, out: AcousticSolution): void;
114
118
  /**
115
119
  * Solve + push the result to every live instance. Each instance must expose a `position` (Vector3)
116
120
  * and `setAcoustic(solution)`. Called once per frame by the `AudioEmitterSystem`, between the live-set
117
121
  * refresh and the sopra tick.
118
- * @param {EventInstance[]} liveInstances bounded set (≤ voice budget)
119
- * @param {Vector3} listener
122
+ * @param {EventInstance[]} live_instances bounded set (≤ voice budget)
123
+ * @param {number[]|Float32Array|Float64Array} listener listener world position, 3 elements
120
124
  */
121
- apply(liveInstances: EventInstance[], listener: Vector3): void;
125
+ apply(live_instances: EventInstance[], listener: number[] | Float32Array | Float64Array): void;
122
126
  /**
123
127
  * Drop a live instance's accumulated state (call when the instance ends).
124
128
  * @param {EventInstance} instance
@@ -128,7 +132,6 @@ export class AcousticSimulator {
128
132
  }
129
133
  import { AcousticMedium } from "./core/AcousticMedium.js";
130
134
  import { AcousticOccluderIndex } from "./core/AcousticOccluderIndex.js";
131
- import { Vector3 } from "../../../core/geom/Vector3.js";
132
135
  import { AcousticSourceState } from "./core/AcousticSourceState.js";
133
136
  import { AcousticSolution } from "./core/AcousticSolution.js";
134
137
  //# sourceMappingURL=AcousticSimulator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AcousticSimulator.d.ts","sourceRoot":"","sources":["../../../../../src/engine/sound/simulation/AcousticSimulator.js"],"names":[],"mappings":"AA0CA;;;;;;;;;GASG;AACH;IACI;;;OAGG;IACH,UAFU,MAAM,CAEF;IAEd;;;OAGG;IACH,WAFU,MAAM,CAEA;IAEhB;;;;;OAKG;IACH,SAFU,OAAO,CAED;IAEhB;;;;;;OAMG;IACH,gBAFU,OAAO,CAEM;IAEvB;;;;OAIG;IACH,eAFU,MAAM,CAEE;IAgGlB,6BAGC;IAZD;;;;OAIG;IACH,0BAEC;IAiBD,gCAEC;IAZD;;;;;OAKG;IACH,6BAEC;IAMD;;;;OAIG;IACH,2CAEC;IAED;;;;;;;OAOG;IACH,yEAHW,YAAY,GAAC,MAAM,EAAE,GACnB,MAAM,CAIlB;IAED;;;OAGG;IACH,6BAHW,MAAM,UACN,YAAY,GAAC,MAAM,EAAE,QAI/B;IAED;;OAEG;IACH,uBAFW,MAAM,QAIhB;IAED;;;OAGG;IACH,8DAEC;IAED;;OAEG;IACH,iDAEC;IAED;;;;OAIG;IACH,sBAEC;IAED;;;;;OAKG;IACH,qBAFW,qBAAmB,IAAI,QAMjC;IAED;;;;;;;;OAQG;IACH,mBAPW,OAAO,UACP,OAAO,gBACP,MAAM,SAEN,mBAAmB,OACnB,gBAAgB,QAkB1B;IAED;;;;;;OAMG;IACH,qBAHW,eAAe,YACf,OAAO,QAuFjB;IAgPD;;;OAGG;IACH,sCAOC;;CACJ;+BA/nB8B,0BAA0B;sCACnB,iCAAiC;wBAN/C,+BAA+B;oCASW,+BAA+B;iCADhE,4BAA4B"}
1
+ {"version":3,"file":"AcousticSimulator.d.ts","sourceRoot":"","sources":["../../../../../src/engine/sound/simulation/AcousticSimulator.js"],"names":[],"mappings":"AA6CA;;;;;;;;;GASG;AACH;IACI;;;OAGG;IACH,UAFU,MAAM,CAEF;IAEd;;;OAGG;IACH,WAFU,MAAM,CAEA;IAEhB;;;;;OAKG;IACH,SAFU,OAAO,CAED;IAEhB;;;;;;OAMG;IACH,gBAFU,OAAO,CAEM;IAEvB;;;;OAIG;IACH,eAFU,MAAM,CAEE;IA2FlB,6BAGC;IAZD;;;;OAIG;IACH,0BAEC;IAiBD,gCAEC;IAZD;;;;;OAKG;IACH,6BAEC;IAMD;;;;OAIG;IACH,2CAEC;IAED;;;;;;;OAOG;IACH,yEAHW,YAAY,GAAC,MAAM,EAAE,GACnB,MAAM,CAIlB;IAED;;;OAGG;IACH,6BAHW,MAAM,UACN,YAAY,GAAC,MAAM,EAAE,QAI/B;IAED;;OAEG;IACH,uBAFW,MAAM,QAIhB;IAED;;;OAGG;IACH,8DAEC;IAED;;OAEG;IACH,iDAEC;IAED;;;;OAIG;IACH,sBAEC;IAED;;;;;OAKG;IACH,qBAFW,qBAAmB,IAAI,QAMjC;IAED;;;;;;;;;;;;OAYG;IACH,mBAPW,MAAM,EAAE,GAAC,YAAY,GAAC,YAAY,UAClC,MAAM,EAAE,GAAC,YAAY,GAAC,YAAY,iBAClC,MAAM,SAEN,mBAAmB,OACnB,gBAAgB,QAkB1B;IAED;;;;;;OAMG;IACH,sBAHW,eAAe,YACf,MAAM,EAAE,GAAC,YAAY,GAAC,YAAY,QAyF5C;IAqPD;;;OAGG;IACH,sCAOC;;CACJ;+BAroB8B,0BAA0B;sCACnB,iCAAiC;oCAGL,+BAA+B;iCADhE,4BAA4B"}
@@ -1,15 +1,18 @@
1
+ import { assert } from "../../../core/assert.js";
2
+ import {
3
+ line3_compute_segment_closest_point_t
4
+ } from "../../../core/geom/3d/line/line3_compute_segment_closest_point_t.js";
1
5
  import { BinaryElementPool } from "../../../core/geom/3d/topology/struct/binary/BinaryElementPool.js";
6
+ import { v3_length } from "../../../core/geom/vec3/v3_length.js";
2
7
  import { Vector3 } from "../../../core/geom/Vector3.js";
3
- import { seededRandom } from "../../../core/math/random/seededRandom.js";
4
8
  import { INVALID_NODE } from "../../../core/graph/csr/csr_graph_build_distance_field.js";
5
- import { line3_compute_segment_closest_point_t } from "../../../core/geom/3d/line/line3_compute_segment_closest_point_t.js";
6
- import { v3_length } from "../../../core/geom/vec3/v3_length.js";
9
+ import { seededRandom } from "../../../core/math/random/seededRandom.js";
7
10
  import { AcousticMedium } from "./core/AcousticMedium.js";
8
11
  import { AcousticOccluderIndex } from "./core/AcousticOccluderIndex.js";
9
12
  import { AcousticRegionIndex } from "./core/AcousticRegionIndex.js";
10
13
  import { AcousticSolution } from "./core/AcousticSolution.js";
11
14
  import { ACOUSTIC_SOURCE_STATE_FLOATS, AcousticSourceState } from "./core/AcousticSourceState.js";
12
- import { OcclusionSolver } from "./core/OcclusionSolver.js";
15
+ import { acoustic_occlusion_solve } from "./core/acoustic_occlusion_solve.js";
13
16
  import { VolumeField } from "./core/VolumeField.js";
14
17
  import { ProbeFieldPathingSolution } from "./probe/ProbeFieldPathingSolution.js";
15
18
  import { ProbeVisibilityRecheck } from "./probe/ProbeVisibilityRecheck.js";
@@ -105,11 +108,6 @@ export class AcousticSimulator {
105
108
  */
106
109
  #volumeField = new VolumeField(AcousticMedium.from(DEFAULT_AIR_ABSORPTION));
107
110
 
108
- /**
109
- * @type {OcclusionSolver}
110
- */
111
- #occlusionSolver = new OcclusionSolver();
112
-
113
111
  /**
114
112
  * Live-instance → its temporal-state record id in {@link #statePool}. The state DATA lives in the
115
113
  * pool (flat floats); this map only holds the id relation. Audio events are highly transient (a
@@ -274,26 +272,30 @@ export class AcousticSimulator {
274
272
 
275
273
  /**
276
274
  * Compute the full acoustic solution for a source at `source`, given `listener`, into `out`.
277
- * @param {Vector3} listener
278
- * @param {Vector3} source
279
- * @param {number} sourceRadius apparent radius of the source (per-source; 0 = point source). For
275
+ *
276
+ * Both positions are read BY INDEX rather than through `.x/.y/.z`: they are live views over
277
+ * {@link Transform64} buffers (plain `Float64Array`s, no component accessors). A {@link Vector3}
278
+ * is a `Float64Array` subclass whose `[0..2]` alias `x/y/z`, so indexing reads both shapes.
279
+ * @param {number[]|Float32Array|Float64Array} listener listener world position, 3 elements
280
+ * @param {number[]|Float32Array|Float64Array} source source world position, 3 elements
281
+ * @param {number} source_radius apparent radius of the source (per-source; 0 = point source). For
280
282
  * volumetric (fractional) occlusion.
281
283
  * @param {AcousticSourceState} state per-source temporal accumulation (mutated)
282
284
  * @param {AcousticSolution} out
283
285
  */
284
- solveFor(listener, source, sourceRadius, state, out) {
286
+ solveFor(listener, source, source_radius, state, out) {
285
287
  out.reset();
286
288
 
287
289
  // air absorption integrated over the actual source→listener path (distance-dependent, per band)
288
290
  this.#volumeField.integrateAbsorption(
289
291
  this.#region_index,
290
- source.x, source.y, source.z,
291
- listener.x, listener.y, listener.z,
292
+ source[0], source[1], source[2],
293
+ listener[0], listener[1], listener[2],
292
294
  out.airLoss
293
295
  );
294
296
 
295
- this.#occlusionSolver.solve(
296
- listener, source, sourceRadius,
297
+ acoustic_occlusion_solve(
298
+ listener, source, source_radius,
297
299
  this.#occluder_index, this.rayCount, this.smoothing, this.#random,
298
300
  state, out
299
301
  );
@@ -303,10 +305,12 @@ export class AcousticSimulator {
303
305
  * Solve + push the result to every live instance. Each instance must expose a `position` (Vector3)
304
306
  * and `setAcoustic(solution)`. Called once per frame by the `AudioEmitterSystem`, between the live-set
305
307
  * refresh and the sopra tick.
306
- * @param {EventInstance[]} liveInstances bounded set (≤ voice budget)
307
- * @param {Vector3} listener
308
+ * @param {EventInstance[]} live_instances bounded set (≤ voice budget)
309
+ * @param {number[]|Float32Array|Float64Array} listener listener world position, 3 elements
308
310
  */
309
- apply(liveInstances, listener) {
311
+ apply(live_instances, listener) {
312
+ assert.defined(listener, 'listener');
313
+
310
314
  const live = this.#liveScratch;
311
315
  live.clear();
312
316
 
@@ -328,7 +332,7 @@ export class AcousticSimulator {
328
332
  }
329
333
  // Snap to the nearest probe the listener can SEE — never one occluded by a wall/floor (a different
330
334
  // acoustic region), which would route a spurious cross-occluder detour or none at all.
331
- const listenerProbe = this.#probeField.nearestVisibleIndex(listener.x, listener.y, listener.z, this.#occluder_index);
335
+ const listenerProbe = this.#probeField.nearestVisibleIndex(listener[0], listener[1], listener[2], this.#occluder_index);
332
336
  if (listenerProbe >= 0) {
333
337
  this.#pathingSolution.computeExitField(this.#probeField.visibilityGraph, listenerProbe);
334
338
  exitFieldReady = true;
@@ -338,8 +342,8 @@ export class AcousticSimulator {
338
342
  const pool = this.#statePool;
339
343
  const cursor = this.#stateCursor;
340
344
 
341
- for (let i = 0; i < liveInstances.length; i++) {
342
- const instance = liveInstances[i];
345
+ for (let i = 0; i < live_instances.length; i++) {
346
+ const instance = live_instances[i];
343
347
 
344
348
  live.add(instance);
345
349
 
@@ -414,14 +418,18 @@ export class AcousticSimulator {
414
418
  * the first hop (the corner it leaks through), not the blocked listener.
415
419
  * @param {AcousticSolution} solution (occlusion already solved)
416
420
  * @param {EventInstance} instance the source (position + attenuation + orientation + directivity)
417
- * @param {Vector3} listener the listener world position (the leak's far endpoint)
421
+ * @param {number[]|Float32Array|Float64Array} listener the listener world position (the leak's far endpoint)
418
422
  */
419
423
  #applyPath(solution, instance, listener) {
420
424
  const field = this.#probeField;
421
425
  const pathingSolution = this.#pathingSolution;
422
426
  const source = instance.position;
423
427
 
424
- const sourceProbe = field.nearestVisibleIndex(source.x, source.y, source.z, this.#occluder_index);
428
+ // both endpoints are live Transform64 translation views (bare Float64Arrays) — read by index
429
+ const source_x = source[0], source_y = source[1], source_z = source[2];
430
+ const listener_x = listener[0], listener_y = listener[1], listener_z = listener[2];
431
+
432
+ const sourceProbe = field.nearestVisibleIndex(source_x, source_y, source_z, this.#occluder_index);
425
433
 
426
434
  if (sourceProbe < 0) {
427
435
  return;
@@ -450,14 +458,14 @@ export class AcousticSimulator {
450
458
  // to the probe vertex: (a) avoids the spurious there-and-back bend a probe sitting just past the
451
459
  // endpoint would fold into the route (the transfer gate would wrongly refuse it), and (b) lets the
452
460
  // attachment legs count toward the routed distance, so a standoff / oblique endpoint attenuates.
453
- const tS = line3_compute_segment_closest_point_t(spx, spy, spz, fhx, fhy, fhz, source.x, source.y, source.z);
461
+ const tS = line3_compute_segment_closest_point_t(spx, spy, spz, fhx, fhy, fhz, source_x, source_y, source_z);
454
462
  const sX = spx + (fhx - spx) * tS, sY = spy + (fhy - spy) * tS, sZ = spz + (fhz - spz) * tS;
455
- const sourceLeg = Math.hypot(source.x - sX, source.y - sY, source.z - sZ);
463
+ const sourceLeg = Math.hypot(source_x - sX, source_y - sY, source_z - sZ);
456
464
  const firstEdgeLen = Math.hypot(fhx - spx, fhy - spy, fhz - spz);
457
465
 
458
- const tL = line3_compute_segment_closest_point_t(lhx, lhy, lhz, lpx, lpy, lpz, listener.x, listener.y, listener.z);
466
+ const tL = line3_compute_segment_closest_point_t(lhx, lhy, lhz, lpx, lpy, lpz, listener_x, listener_y, listener_z);
459
467
  const lX = lhx + (lpx - lhx) * tL, lY = lhy + (lpy - lhy) * tL, lZ = lhz + (lpz - lhz) * tL;
460
- const listenerLeg = Math.hypot(listener.x - lX, listener.y - lY, listener.z - lZ);
468
+ const listenerLeg = Math.hypot(listener_x - lX, listener_y - lY, listener_z - lZ);
461
469
  const lastEdgeLen = Math.hypot(lpx - lhx, lpy - lhy, lpz - lhz);
462
470
 
463
471
  // Corridor span between the two virtual attach points. A SINGLE-edge route (firstHop IS the listener's
@@ -493,8 +501,8 @@ export class AcousticSimulator {
493
501
  // endpoint ON the corridor passes straight through (degenerate leg → gain 1), one off to the side or
494
502
  // behind it bends and attenuates. (Interior turns: Phase 0b.)
495
503
  if (field.hasTransfer) {
496
- pathGain *= this.#turnGain(sourceProbe, sX - source.x, sY - source.y, sZ - source.z, fhx - spx, fhy - spy, fhz - spz);
497
- pathGain *= this.#turnGain(listenerProbe, lpx - lhx, lpy - lhy, lpz - lhz, listener.x - lX, listener.y - lY, listener.z - lZ);
504
+ pathGain *= this.#turnGain(sourceProbe, sX - source_x, sY - source_y, sZ - source_z, fhx - spx, fhy - spy, fhz - spz);
505
+ pathGain *= this.#turnGain(listenerProbe, lpx - lhx, lpy - lhy, lpz - lhz, listener_x - lX, listener_y - lY, listener_z - lZ);
498
506
  }
499
507
 
500
508
  // Walk the leaked polyline (source-probe → … → listener-probe) once, now that the leak is audible:
@@ -599,9 +607,10 @@ export class AcousticSimulator {
599
607
  #directivityGainToward(instance, tx, ty, tz) {
600
608
  const source = instance.position;
601
609
 
602
- let dx = tx - source.x;
603
- let dy = ty - source.y;
604
- let dz = tz - source.z;
610
+ // a live Transform64 translation view (a bare Float64Array) — read by index
611
+ let dx = tx - source[0];
612
+ let dy = ty - source[1];
613
+ let dz = tz - source[2];
605
614
 
606
615
  const len = v3_length(dx, dy, dz);
607
616
 
@@ -625,10 +634,10 @@ export class AcousticSimulator {
625
634
  * Fill `solution.directivityGain` — the source's radiation toward the LISTENER (the direct path).
626
635
  * @param {AcousticSolution} solution (occlusion already solved; `directivityGain` written)
627
636
  * @param {EventInstance} instance the source — position + orientation + directivity
628
- * @param {Vector3} listener
637
+ * @param {number[]|Float32Array|Float64Array} listener listener world position, 3 elements
629
638
  */
630
639
  #applyDirectivity(solution, instance, listener) {
631
- solution.directivityGain = this.#directivityGainToward(instance, listener.x, listener.y, listener.z);
640
+ solution.directivityGain = this.#directivityGainToward(instance, listener[0], listener[1], listener[2]);
632
641
  }
633
642
 
634
643
  /**
@@ -49,7 +49,7 @@ simulation/
49
49
  AcousticOccluderIndex.js dynamic ray index — a TLAS BVH over OccluderBody shapes (exact shape.raycast)
50
50
  OccluderBody.js one occluder: shape + material + world transform (+ cached inverse / AABB)
51
51
  RayHit.js reusable closest-hit result (caller-allocated): point + normal + t + material
52
- OcclusionSolver.js volumetric occlusion + transmission for one source (temporally smoothed)
52
+ acoustic_occlusion_solve.js volumetric occlusion + transmission for one source (temporally smoothed)
53
53
  VolumeField.js blends AcousticVolume regions into a medium at a point + integrates air
54
54
  absorption along the source→listener path (distance-dependent, per band)
55
55
  AcousticVolumeRegion.js a shape + medium + transition band (boundary blend)
@@ -197,7 +197,7 @@ real-time-direct / baked-indirect split.
197
197
 
198
198
  ## The simulation, in one paragraph each
199
199
 
200
- **Occlusion + transmission.** For each live source, `OcclusionSolver` shoots `rayCount` rays from the
200
+ **Occlusion + transmission.** For each live source, `acoustic_occlusion_solve` shoots `rayCount` rays from the
201
201
  listener toward points spread over the source's sphere (Fibonacci distribution, randomly re-oriented per
202
202
  tick). The fraction blocked is the occlusion; the blocked rays' material `transmission` (3 bands) is what
203
203
  leaks through. The result is folded into a temporal EMA, so a low ray count integrates over frames and
@@ -1 +1 @@
1
- {"version":3,"file":"OcclusionSolver.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/sound/simulation/core/OcclusionSolver.js"],"names":[],"mappings":"AAeA;;;;;;;;;;;;;GAaG;AACH;IACI;;;;;;;;;;OAUG;IACH,wDARW,MAAM,0CAEN,MAAM,aACN,MAAM,gBACK,MAAM,2DAyE3B;CACJ"}
1
+ {"version":3,"file":"OcclusionSolver.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/sound/simulation/core/OcclusionSolver.js"],"names":[],"mappings":"AAgBA;;;;;;;;;;;;;GAaG;AACH;IACI;;;;;;;;;;OAUG;IACH,wDARW,MAAM,0CAEN,MAAM,aACN,MAAM,gBACK,MAAM,2DA4F3B;CACJ"}
@@ -32,8 +32,8 @@ export class VolumeField {
32
32
  */
33
33
  sample(regionIndex: AcousticRegionIndex, x: number, y: number, z: number, out: AcousticMedium): void;
34
34
  /**
35
- * Integrate per-band air absorption along the segment from the source `(sx, sy, sz)` to the listener
36
- * `(lx, ly, lz)`, through the blended media, into `outLoss` (per-band **total dB** attenuation over the
35
+ * Integrate per-band air absorption along the segment from the source `(source_x, source_y, source_z)` to the listener
36
+ * `(listener_x, listener_y, listener_z)`, through the blended media, into `outLoss` (per-band **total dB** attenuation over the
37
37
  * path). Each `AcousticMedium.airAbsorption` is in dB per metre, so the line integral `∫ α ds` gives the
38
38
  * total dB lost per band — i.e. distance-dependent atmospheric absorption (highs accumulate loss faster,
39
39
  * so they die over distance while lows carry), plus extra loss along whatever painted region the path
@@ -44,15 +44,15 @@ export class VolumeField {
44
44
  * midpoints, blending the medium at each via {@link sample}. Allocation-free (module scratch).
45
45
  *
46
46
  * @param {AcousticRegionIndex} regionIndex
47
- * @param {number} sx source position x
48
- * @param {number} sy source position y
49
- * @param {number} sz source position z
50
- * @param {number} lx listener position x
51
- * @param {number} ly listener position y
52
- * @param {number} lz listener position z
47
+ * @param {number} source_x source position x
48
+ * @param {number} source_y source position y
49
+ * @param {number} source_z source position z
50
+ * @param {number} listener_x listener position x
51
+ * @param {number} listener_y listener position y
52
+ * @param {number} listener_z listener position z
53
53
  * @param {Float32Array} outLoss `[low, mid, high]` total dB attenuation over the path
54
54
  */
55
- integrateAbsorption(regionIndex: AcousticRegionIndex, sx: number, sy: number, sz: number, lx: number, ly: number, lz: number, outLoss: Float32Array): void;
55
+ integrateAbsorption(regionIndex: AcousticRegionIndex, source_x: number, source_y: number, source_z: number, listener_x: number, listener_y: number, listener_z: number, outLoss: Float32Array): void;
56
56
  #private;
57
57
  }
58
58
  import { AcousticMedium } from "./AcousticMedium.js";
@@ -1 +1 @@
1
- {"version":3,"file":"VolumeField.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/sound/simulation/core/VolumeField.js"],"names":[],"mappings":"AAaA;;;;;GAKG;AACH;IAMI;;OAEG;IACH,4BAFW,cAAc,EAIxB;IASD;;OAEG;IACH,uCAEC;IAZD;;OAEG;IACH,oCAEC;IASD;;;;;;;;;;;;OAYG;IACH,4CALW,MAAM,KACN,MAAM,KACN,MAAM,OACN,cAAc,QA0BxB;IAED;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,0DARW,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,WACN,YAAY,QA2CtB;;CACJ;+BApJ8B,qBAAqB"}
1
+ {"version":3,"file":"VolumeField.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/sound/simulation/core/VolumeField.js"],"names":[],"mappings":"AAcA;;;;;GAKG;AACH;IAMI;;OAEG;IACH,4BAFW,cAAc,EAIxB;IASD;;OAEG;IACH,uCAEC;IAZD;;OAEG;IACH,oCAEC;IASD;;;;;;;;;;;;OAYG;IACH,4CALW,MAAM,KACN,MAAM,KACN,MAAM,OACN,cAAc,QA0BxB;IAED;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,gEARW,MAAM,YACN,MAAM,YACN,MAAM,cACN,MAAM,cACN,MAAM,cACN,MAAM,WACN,YAAY,QA0DtB;;CACJ;+BAnK8B,qBAAqB"}
@@ -1,3 +1,4 @@
1
+ import { assert } from "../../../../core/assert.js";
1
2
  import { AcousticMedium } from "./AcousticMedium.js";
2
3
 
3
4
  // Samples along a source→listener segment for the air-absorption line integral (midpoint rule). The
@@ -84,8 +85,8 @@ export class VolumeField {
84
85
  }
85
86
 
86
87
  /**
87
- * Integrate per-band air absorption along the segment from the source `(sx, sy, sz)` to the listener
88
- * `(lx, ly, lz)`, through the blended media, into `outLoss` (per-band **total dB** attenuation over the
88
+ * Integrate per-band air absorption along the segment from the source `(source_x, source_y, source_z)` to the listener
89
+ * `(listener_x, listener_y, listener_z)`, through the blended media, into `outLoss` (per-band **total dB** attenuation over the
89
90
  * path). Each `AcousticMedium.airAbsorption` is in dB per metre, so the line integral `∫ α ds` gives the
90
91
  * total dB lost per band — i.e. distance-dependent atmospheric absorption (highs accumulate loss faster,
91
92
  * so they die over distance while lows carry), plus extra loss along whatever painted region the path
@@ -96,18 +97,33 @@ export class VolumeField {
96
97
  * midpoints, blending the medium at each via {@link sample}. Allocation-free (module scratch).
97
98
  *
98
99
  * @param {AcousticRegionIndex} regionIndex
99
- * @param {number} sx source position x
100
- * @param {number} sy source position y
101
- * @param {number} sz source position z
102
- * @param {number} lx listener position x
103
- * @param {number} ly listener position y
104
- * @param {number} lz listener position z
100
+ * @param {number} source_x source position x
101
+ * @param {number} source_y source position y
102
+ * @param {number} source_z source position z
103
+ * @param {number} listener_x listener position x
104
+ * @param {number} listener_y listener position y
105
+ * @param {number} listener_z listener position z
105
106
  * @param {Float32Array} outLoss `[low, mid, high]` total dB attenuation over the path
106
107
  */
107
- integrateAbsorption(regionIndex, sx, sy, sz, lx, ly, lz, outLoss) {
108
- const dx = lx - sx;
109
- const dy = ly - sy;
110
- const dz = lz - sz;
108
+ integrateAbsorption(
109
+ regionIndex,
110
+ source_x, source_y, source_z,
111
+ listener_x, listener_y, listener_z,
112
+ outLoss
113
+ ) {
114
+
115
+ assert.isNumber(source_x, 'source_x');
116
+ assert.isNumber(source_y, 'source_y');
117
+ assert.isNumber(source_z, 'source_z');
118
+
119
+ assert.isNumber(listener_x, 'listener_x');
120
+ assert.isNumber(listener_y, 'listener_y');
121
+ assert.isNumber(listener_z, 'listener_z');
122
+
123
+ const dx = listener_x - source_x;
124
+ const dy = listener_y - source_y;
125
+ const dz = listener_z - source_z;
126
+
111
127
  const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
112
128
 
113
129
  if (distance === 0) {
@@ -134,7 +150,7 @@ export class VolumeField {
134
150
 
135
151
  for (let i = 0; i < AIR_INTEGRAL_SAMPLES; i++) {
136
152
  const t = (i + 0.5) / AIR_INTEGRAL_SAMPLES;
137
- this.sample(regionIndex, sx + dx * t, sy + dy * t, sz + dz * t, STEP_MEDIUM);
153
+ this.sample(regionIndex, source_x + dx * t, source_y + dy * t, source_z + dz * t, STEP_MEDIUM);
138
154
 
139
155
  const air = STEP_MEDIUM.airAbsorption;
140
156
  low += air[0];
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Computes per-source occlusion + transmission by shooting rays from the listener toward points spread
3
+ * over the source's sphere and counting how many are blocked by the {@link AcousticOccluderIndex}.
4
+ *
5
+ * - Volumetric (not boolean): occlusion is the FRACTION of blocked rays, so partially-hidden sources
6
+ * attenuate smoothly instead of popping on/off.
7
+ * - Transmission: blocked rays accumulate their occluder's per-band transmission; the average is the
8
+ * energy that leaks through.
9
+ * - Temporal: the fibonacci sample set is randomly re-oriented each tick (seeded), and the result is
10
+ * folded into an {@link AcousticSourceState} via EMA — so a low per-tick ray count integrates over
11
+ * frames and the output stays frame-stable.
12
+ *
13
+ * Both positions are read BY INDEX, never as `.x/.y/.z`: production hands us live views over
14
+ * {@link Transform64} buffers (plain `Float64Array`s, which carry no component accessors), while a
15
+ * {@link Vector3} is itself a `Float64Array` subclass whose `[0..2]` alias `x/y/z`. Indexing is the one
16
+ * form that reads both.
17
+ *
18
+ * Pure: geometry in, numbers out. No WebAudio, no ECS, no allocation.
19
+ *
20
+ * @param {number[]|Float32Array|Float64Array} listener listener world position, 3 elements
21
+ * @param {number[]|Float32Array|Float64Array} source source world position, 3 elements
22
+ * @param {number} source_radius apparent radius of the source (0 = point source → boolean occlusion)
23
+ * @param {AcousticOccluderIndex} index
24
+ * @param {number} ray_count rays shot this tick (≥ 1)
25
+ * @param {number} smoothing EMA factor in (0, 1] — 1 = no smoothing
26
+ * @param {function():number} random seeded RNG; decorrelates the sample set per call
27
+ * @param {AcousticSourceState} state per-source temporal accumulation (mutated)
28
+ * @param {AcousticSolution} out occlusion + transmission written here (medium untouched)
29
+ */
30
+ export function acoustic_occlusion_solve(listener: number[] | Float32Array | Float64Array, source: number[] | Float32Array | Float64Array, source_radius: number, index: AcousticOccluderIndex, ray_count: number, smoothing: number, random: () => number, state: AcousticSourceState, out: AcousticSolution): void;
31
+ //# sourceMappingURL=acoustic_occlusion_solve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acoustic_occlusion_solve.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/sound/simulation/core/acoustic_occlusion_solve.js"],"names":[],"mappings":"AAgBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,mDAVW,MAAM,EAAE,GAAC,YAAY,GAAC,YAAY,UAClC,MAAM,EAAE,GAAC,YAAY,GAAC,YAAY,iBAClC,MAAM,2CAEN,MAAM,aACN,MAAM,gBACK,MAAM,2DA4F3B"}
@@ -0,0 +1,134 @@
1
+ import { assert } from "../../../../core/assert.js";
2
+ import { Ray3 } from "../../../../core/geom/3d/ray/Ray3.js";
3
+ import { sphere_fibonacci_point } from "../../../../core/geom/3d/sphere/sphere_fibonacci_point.js";
4
+ import { random_orthonormal_basis } from "../../../../core/geom/random/random_orthonormal_basis.js";
5
+ import { v3_matrix3_rotate } from "../../../../core/geom/vec3/v3_matrix3_rotate.js";
6
+ import { RayHit } from "./RayHit.js";
7
+
8
+ // module-static scratch — reused across calls so a solve allocates nothing. Typed throughout (the
9
+ // vectors carry direction cosines / band ratios, so Float64 native precision is free here).
10
+ const RAY = new Ray3();
11
+ const HIT = new RayHit();
12
+ const FIBONACCI = new Float64Array(3);
13
+ const DIRECTION = new Float64Array(3);
14
+ const ROTATION = new Float64Array(9);
15
+ const TRANSMISSION = new Float64Array(3);
16
+
17
+ /**
18
+ * Computes per-source occlusion + transmission by shooting rays from the listener toward points spread
19
+ * over the source's sphere and counting how many are blocked by the {@link AcousticOccluderIndex}.
20
+ *
21
+ * - Volumetric (not boolean): occlusion is the FRACTION of blocked rays, so partially-hidden sources
22
+ * attenuate smoothly instead of popping on/off.
23
+ * - Transmission: blocked rays accumulate their occluder's per-band transmission; the average is the
24
+ * energy that leaks through.
25
+ * - Temporal: the fibonacci sample set is randomly re-oriented each tick (seeded), and the result is
26
+ * folded into an {@link AcousticSourceState} via EMA — so a low per-tick ray count integrates over
27
+ * frames and the output stays frame-stable.
28
+ *
29
+ * Both positions are read BY INDEX, never as `.x/.y/.z`: production hands us live views over
30
+ * {@link Transform64} buffers (plain `Float64Array`s, which carry no component accessors), while a
31
+ * {@link Vector3} is itself a `Float64Array` subclass whose `[0..2]` alias `x/y/z`. Indexing is the one
32
+ * form that reads both.
33
+ *
34
+ * Pure: geometry in, numbers out. No WebAudio, no ECS, no allocation.
35
+ *
36
+ * @param {number[]|Float32Array|Float64Array} listener listener world position, 3 elements
37
+ * @param {number[]|Float32Array|Float64Array} source source world position, 3 elements
38
+ * @param {number} source_radius apparent radius of the source (0 = point source → boolean occlusion)
39
+ * @param {AcousticOccluderIndex} index
40
+ * @param {number} ray_count rays shot this tick (≥ 1)
41
+ * @param {number} smoothing EMA factor in (0, 1] — 1 = no smoothing
42
+ * @param {function():number} random seeded RNG; decorrelates the sample set per call
43
+ * @param {AcousticSourceState} state per-source temporal accumulation (mutated)
44
+ * @param {AcousticSolution} out occlusion + transmission written here (medium untouched)
45
+ */
46
+ export function acoustic_occlusion_solve(
47
+ listener,
48
+ source,
49
+ source_radius,
50
+ index,
51
+ ray_count,
52
+ smoothing,
53
+ random,
54
+ state,
55
+ out
56
+ ) {
57
+
58
+ // a per-tick random orientation for the fibonacci set (decorrelation over frames)
59
+ random_orthonormal_basis(ROTATION, 0, random);
60
+
61
+ const listener_x = listener[0];
62
+ const listener_y = listener[1];
63
+ const listener_z = listener[2];
64
+
65
+ assert.isNumber(listener_x, 'listener_x');
66
+ assert.isNumber(listener_y, 'listener_y');
67
+ assert.isNumber(listener_z, 'listener_z');
68
+
69
+ const source_x = source[0];
70
+ const source_y = source[1];
71
+ const source_z = source[2];
72
+
73
+ assert.isNumber(source_x, 'source_x');
74
+ assert.isNumber(source_y, 'source_y');
75
+ assert.isNumber(source_z, 'source_z');
76
+
77
+ let blocked = 0;
78
+ let t0 = 0;
79
+ let t1 = 0;
80
+ let t2 = 0;
81
+
82
+ for (let i = 0; i < ray_count; i++) {
83
+ sphere_fibonacci_point(FIBONACCI, 0, i, ray_count);
84
+ v3_matrix3_rotate(DIRECTION, 0, FIBONACCI[0], FIBONACCI[1], FIBONACCI[2], ROTATION);
85
+
86
+ // a sample point spread over the source's sphere
87
+ const px = source_x + DIRECTION[0] * source_radius;
88
+ const py = source_y + DIRECTION[1] * source_radius;
89
+ const pz = source_z + DIRECTION[2] * source_radius;
90
+
91
+ const dx = px - listener_x;
92
+ const dy = py - listener_y;
93
+ const dz = pz - listener_z;
94
+
95
+ const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
96
+
97
+ if (distance <= 1e-6) {
98
+ // listener sitting on the source — treat as unoccluded for this ray
99
+ continue;
100
+ }
101
+
102
+ const inv = 1 / distance;
103
+
104
+ RAY.setOrigin(listener_x, listener_y, listener_z);
105
+ RAY.setDirection(dx * inv, dy * inv, dz * inv);
106
+ RAY.tMax = distance;
107
+
108
+ if (index.closestHit(RAY, HIT)) {
109
+ blocked++;
110
+
111
+ const transmission = HIT.material.transmission;
112
+ t0 += transmission[0];
113
+ t1 += transmission[1];
114
+ t2 += transmission[2];
115
+ }
116
+ }
117
+
118
+ const occlusion = blocked / ray_count;
119
+
120
+ // instantaneous transmission = average blocker transmission; transparent when nothing blocked
121
+ if (blocked > 0) {
122
+ const inv = 1 / blocked;
123
+ TRANSMISSION[0] = t0 * inv;
124
+ TRANSMISSION[1] = t1 * inv;
125
+ TRANSMISSION[2] = t2 * inv;
126
+ } else {
127
+ TRANSMISSION[0] = 1;
128
+ TRANSMISSION[1] = 1;
129
+ TRANSMISSION[2] = 1;
130
+ }
131
+
132
+ state.integrate(occlusion, TRANSMISSION, smoothing);
133
+ state.writeTo(out);
134
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/volumetrics_froxel/main.js"],"names":[],"mappings":"AA8ZA,uCAsfC"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/volumetrics_froxel/main.js"],"names":[],"mappings":"AAmaA,uCAsfC"}
@@ -3,6 +3,7 @@ import Quaternion from "../../../core/geom/Quaternion.js";
3
3
  import Vector3 from "../../../core/geom/Vector3.js";
4
4
  import { DEG_TO_RAD } from "../../../core/math/DEG_TO_RAD.js";
5
5
  import { MIE_PARTICLES_STANDARD_PRECOMPUTED } from "../../../core/math/physics/mie/MIE_PARTICLES_STANDARD_PRECOMPUTED.js";
6
+ import { t64_set_rotation_axis_angle } from "../../../engine/ecs/transform/t64_set_rotation_axis_angle.js";
6
7
  import { GPUProfileLevel } from "../../device/timing/profile/GPUProfileLevel.js";
7
8
  import { GPUProfileSession } from "../../device/timing/profile/GPUProfileSession.js";
8
9
  import { OrbitalCameraController } from "../../renderer/camera/OrbitalCameraController.js";
@@ -209,9 +210,13 @@ function add_participating_media(scene) {
209
210
  v.transform.setScale(...scale);
210
211
 
211
212
  if (yaw_degrees !== 0) {
212
- v.transform.rotation.fromAxisAngle(Vector3.up, yaw_degrees * DEG_TO_RAD);
213
+ t64_set_rotation_axis_angle(v.transform, 0, 1, 0, yaw_degrees * DEG_TO_RAD);
213
214
  }
214
215
 
216
+ // the GPU is handed `transform.matrix`, and a Transform64 does not maintain one - the
217
+ // scale and the yaw above are components and nothing more until this call folds them in
218
+ v.transform.updateMatrix();
219
+
215
220
  v.fade_distance = fade;
216
221
  v.particle_spec = VolumetricsParticleSpec.fromMeep(particle);
217
222
  v.target_extinction = extinction;
@@ -98,6 +98,13 @@ export class ParticipatingMediaVolume {
98
98
  /**
99
99
  * Defines actual scale, orientation and world position of the volume.
100
100
  * The base size of the volume is 1, centered at 0,0,0
101
+ *
102
+ * What reaches the GPU is the *matrix*, and its inverse - `GPUSceneVolumetrics` reads nothing
103
+ * else. A `Transform64` does not maintain one, so a volume posed by writing components owes an
104
+ * {@link Transform64#updateMatrix} before it is drawn; one posed by writing the matrix, as
105
+ * `ParticipatingMediaSystem` does through `fromMatrix`, owes nothing. Skip it and the volume is
106
+ * a unit cube at the right place, which reads as a volume that is simply not there.
107
+ *
101
108
  * @type {Transform64}
102
109
  */
103
110
  transform: Transform64;
@@ -1 +1 @@
1
- {"version":3,"file":"ParticipatingMediaVolume.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/volumetrics/ParticipatingMediaVolume.js"],"names":[],"mappings":"AAKA;IA6EI;;;;OAIG;IACH,+BAHW;QAAC,wBAAwB,CAAC;QAAC,wBAAwB,CAAC;QAAC,MAAM,CAAC;QAAC,CAAC,CAAA;KAAC,GAC9D,uBAAuB,CAalC;IAED,oDAIC;IAlGD;;;OAGG;IACH,YAFU,MAAM,EAAE,CAEK;IAEvB;;;OAGG;IACH,YAFU,MAAM,EAAE,CAEK;IAEvB;;;OAGG;IACH,QAFU,MAAM,CAEL;IAEX,UAAQ;IAER,iCAIC;IAED;;;OAGG;IACH,YAFW,uBAAuB,QAOjC;IAED;;;;;aA0BC;IAED;;;OAGG;IACH,cAHW,uBAAuB,GACrB,OAAO,CAOnB;CAyBJ;AAED,6EAKI;AAIJ;IAII,iBAEC;IAED;;;OAGG;IACH,aAFU,MAAM,EAAE,CAEM;IACxB,0BAA4B;IAC5B,uBAAyB;IAEzB;;;OAGG;IACH,gBAFU,MAAM,EAAE,CAES;IAE3B;;;OAGG;IACH,uBAFU,MAAM,EAAE,CAEgB;IAElC;;;OAGG;IACH,MAFU,MAAM,CAEP;IAET;;;OAGG;IACH,gBAAc;IAEd;;;;;;;;;OASG;IACH,mCAEC;IAED,uCAA+E;IAE/E;;;;;;OAMG;IACH,eAFU,MAAM,CAEI;IAEpB;;;;OAIG;IACH,WAFU,WAAW,CAES;;CAEjC;4BAzL2B,8CAA8C"}
1
+ {"version":3,"file":"ParticipatingMediaVolume.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/volumetrics/ParticipatingMediaVolume.js"],"names":[],"mappings":"AAKA;IA6EI;;;;OAIG;IACH,+BAHW;QAAC,wBAAwB,CAAC;QAAC,wBAAwB,CAAC;QAAC,MAAM,CAAC;QAAC,CAAC,CAAA;KAAC,GAC9D,uBAAuB,CAalC;IAED,oDAIC;IAlGD;;;OAGG;IACH,YAFU,MAAM,EAAE,CAEK;IAEvB;;;OAGG;IACH,YAFU,MAAM,EAAE,CAEK;IAEvB;;;OAGG;IACH,QAFU,MAAM,CAEL;IAEX,UAAQ;IAER,iCAIC;IAED;;;OAGG;IACH,YAFW,uBAAuB,QAOjC;IAED;;;;;aA0BC;IAED;;;OAGG;IACH,cAHW,uBAAuB,GACrB,OAAO,CAOnB;CAyBJ;AAED,6EAKI;AAIJ;IAII,iBAEC;IAED;;;OAGG;IACH,aAFU,MAAM,EAAE,CAEM;IACxB,0BAA4B;IAC5B,uBAAyB;IAEzB;;;OAGG;IACH,gBAFU,MAAM,EAAE,CAES;IAE3B;;;OAGG;IACH,uBAFU,MAAM,EAAE,CAEgB;IAElC;;;OAGG;IACH,MAFU,MAAM,CAEP;IAET;;;OAGG;IACH,gBAAc;IAEd;;;;;;;;;OASG;IACH,mCAEC;IAED,uCAA+E;IAE/E;;;;;;OAMG;IACH,eAFU,MAAM,CAEI;IAEpB;;;;;;;;;;;OAWG;IACH,WAFU,WAAW,CAES;;CAEjC;4BAhM2B,8CAA8C"}
@@ -182,6 +182,13 @@ export class ParticipatingMediaVolume {
182
182
  /**
183
183
  * Defines actual scale, orientation and world position of the volume.
184
184
  * The base size of the volume is 1, centered at 0,0,0
185
+ *
186
+ * What reaches the GPU is the *matrix*, and its inverse - `GPUSceneVolumetrics` reads nothing
187
+ * else. A `Transform64` does not maintain one, so a volume posed by writing components owes an
188
+ * {@link Transform64#updateMatrix} before it is drawn; one posed by writing the matrix, as
189
+ * `ParticipatingMediaSystem` does through `fromMatrix`, owes nothing. Skip it and the volume is
190
+ * a unit cube at the right place, which reads as a volume that is simply not there.
191
+ *
185
192
  * @type {Transform64}
186
193
  */
187
194
  transform = new Transform64();
@@ -1,112 +0,0 @@
1
- import { Ray3 } from "../../../../core/geom/3d/ray/Ray3.js";
2
- import { sphere_fibonacci_point } from "../../../../core/geom/3d/sphere/sphere_fibonacci_point.js";
3
- import { random_orthonormal_basis } from "../../../../core/geom/random/random_orthonormal_basis.js";
4
- import { v3_matrix3_rotate } from "../../../../core/geom/vec3/v3_matrix3_rotate.js";
5
- import { RayHit } from "./RayHit.js";
6
-
7
- // module-static scratch — reused across calls so a solve allocates nothing. Typed throughout (the
8
- // vectors carry direction cosines / band ratios, so Float64 native precision is free here).
9
- const RAY = new Ray3();
10
- const HIT = new RayHit();
11
- const FIBONACCI = new Float64Array(3);
12
- const DIRECTION = new Float64Array(3);
13
- const ROTATION = new Float64Array(9);
14
- const TRANSMISSION = new Float64Array(3);
15
-
16
- /**
17
- * Computes per-source occlusion + transmission by shooting rays from the listener toward points spread
18
- * over the source's sphere and counting how many are blocked by the {@link AcousticOccluderIndex}.
19
- *
20
- * - Volumetric (not boolean): occlusion is the FRACTION of blocked rays, so partially-hidden sources
21
- * attenuate smoothly instead of popping on/off.
22
- * - Transmission: blocked rays accumulate their occluder's per-band transmission; the average is the
23
- * energy that leaks through.
24
- * - Temporal: the fibonacci sample set is randomly re-oriented each tick (seeded), and the result is
25
- * folded into an {@link AcousticSourceState} via EMA — so a low per-tick ray count integrates over
26
- * frames and the output stays frame-stable.
27
- *
28
- * Pure: geometry in, numbers out. No WebAudio, no ECS, no allocation.
29
- */
30
- export class OcclusionSolver {
31
- /**
32
- * @param {Vector3} listener listener world position
33
- * @param {Vector3} source source world position
34
- * @param {number} sourceRadius apparent radius of the source (0 = point source → boolean occlusion)
35
- * @param {AcousticOccluderIndex} index
36
- * @param {number} rayCount rays shot this tick (≥ 1)
37
- * @param {number} smoothing EMA factor in (0, 1] — 1 = no smoothing
38
- * @param {function():number} random seeded RNG; decorrelates the sample set per call
39
- * @param {AcousticSourceState} state per-source temporal accumulation (mutated)
40
- * @param {AcousticSolution} out occlusion + transmission written here (medium untouched)
41
- */
42
- solve(listener, source, sourceRadius, index, rayCount, smoothing, random, state, out) {
43
- // a per-tick random orientation for the fibonacci set (decorrelation over frames)
44
- random_orthonormal_basis(ROTATION, 0, random);
45
-
46
- const lx = listener.x;
47
- const ly = listener.y;
48
- const lz = listener.z;
49
-
50
- const sx = source.x;
51
- const sy = source.y;
52
- const sz = source.z;
53
-
54
- let blocked = 0;
55
- let t0 = 0;
56
- let t1 = 0;
57
- let t2 = 0;
58
-
59
- for (let i = 0; i < rayCount; i++) {
60
- sphere_fibonacci_point(FIBONACCI, 0, i, rayCount);
61
- v3_matrix3_rotate(DIRECTION, 0, FIBONACCI[0], FIBONACCI[1], FIBONACCI[2], ROTATION);
62
-
63
- // a sample point spread over the source's sphere
64
- const px = sx + DIRECTION[0] * sourceRadius;
65
- const py = sy + DIRECTION[1] * sourceRadius;
66
- const pz = sz + DIRECTION[2] * sourceRadius;
67
-
68
- const dx = px - lx;
69
- const dy = py - ly;
70
- const dz = pz - lz;
71
-
72
- const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
73
-
74
- if (distance <= 1e-6) {
75
- // listener sitting on the source — treat as unoccluded for this ray
76
- continue;
77
- }
78
-
79
- const inv = 1 / distance;
80
-
81
- RAY.setOrigin(lx, ly, lz);
82
- RAY.setDirection(dx * inv, dy * inv, dz * inv);
83
- RAY.tMax = distance;
84
-
85
- if (index.closestHit(RAY, HIT)) {
86
- blocked++;
87
-
88
- const transmission = HIT.material.transmission;
89
- t0 += transmission[0];
90
- t1 += transmission[1];
91
- t2 += transmission[2];
92
- }
93
- }
94
-
95
- const occlusion = blocked / rayCount;
96
-
97
- // instantaneous transmission = average blocker transmission; transparent when nothing blocked
98
- if (blocked > 0) {
99
- const inv = 1 / blocked;
100
- TRANSMISSION[0] = t0 * inv;
101
- TRANSMISSION[1] = t1 * inv;
102
- TRANSMISSION[2] = t2 * inv;
103
- } else {
104
- TRANSMISSION[0] = 1;
105
- TRANSMISSION[1] = 1;
106
- TRANSMISSION[2] = 1;
107
- }
108
-
109
- state.integrate(occlusion, TRANSMISSION, smoothing);
110
- state.writeTo(out);
111
- }
112
- }
@@ -1,115 +0,0 @@
1
- /**
2
- * Packs many compiled {@link ../isa/ParticleProgram.js} programs into one shared buffer — a header,
3
- * every program's constant pool, then every program's instruction words — and hands back the
4
- * offsets each emitter records.
5
- *
6
- * Data before code, behind a four-word header whose first word says where the code starts: the VM
7
- * reads both sections through one binding (a storage buffer per stage is not free, and code and
8
- * constants are read together), and an emitter's constant pool sits next to the pools of the
9
- * programs it shares a wave with.
10
- *
11
- * Because VM instructions carry no absolute code addresses (control flow is branchless) and
12
- * constant references are program-local (the VM adds the emitter's `constant_offset`), programs
13
- * can be concatenated at any offset. INIT and UPDATE sections are laid out back-to-back per program.
14
- */
15
- export class ProgramArena {
16
- /**
17
- * Flat instruction words, every program's INIT and UPDATE sections concatenated.
18
- * @type {number[]}
19
- */
20
- instruction_words: number[];
21
- /**
22
- * Flat constant words (f32 bit patterns), every program's pool concatenated.
23
- * @type {number[]}
24
- */
25
- constant_words: number[];
26
- /**
27
- * Running instruction index — INIT/UPDATE offsets are in these units.
28
- * @type {number}
29
- */
30
- instruction_count: number;
31
- /**
32
- * Running constant word index — the pool is addressed by word, so a scalar literal costs one
33
- * rather than the four a `vec4` entry used to.
34
- * @type {number}
35
- */
36
- constant_word_count: number;
37
- /**
38
- * Slots the hungriest program in the arena needs — the maximum of every added program's
39
- * `register_count`.
40
- *
41
- * This is the number that picks an interpreter. It is a property of the SET, not of a program:
42
- * one program past {@link ../ParticleConstants.js PARTICLE_VM_FAST_REGISTER_SLOTS} moves every
43
- * pass in the system onto the wide register file, including the emitters whose programs would
44
- * have fit. Per-program dispatch would be the other design, and a much larger one — two pipelines
45
- * live at once, the coherence pass's buckets split along a second axis, and two indirect
46
- * commands to size. This is a fallback for a case that should not arise, so it is the whole
47
- * system or none of it.
48
- *
49
- * @type {number}
50
- */
51
- register_high_water: number;
52
- /**
53
- * Whether the arena holds a program the fast interpreter's register file cannot hold, and the
54
- * system must therefore run on {@link ../vm/chunk_particle_vm_registers_wide.js}.
55
- *
56
- * Monotonic in practice: removing an emitter does not remove its program from the arena (ids are
57
- * dense and buckets are keyed on them), so once the set has seen a wide program it stays wide
58
- * until the arena itself is rebuilt.
59
- *
60
- * @returns {boolean}
61
- */
62
- get requires_wide_registers(): boolean;
63
- /**
64
- * Content key -> the placement the first identical program got.
65
- * @type {Map<string, {init_offset: number, init_count: number, update_offset: number, update_count: number, constant_offset: number, program_id: number}>}
66
- */
67
- programs: Map<string, {
68
- init_offset: number;
69
- init_count: number;
70
- update_offset: number;
71
- update_count: number;
72
- constant_offset: number;
73
- program_id: number;
74
- }>;
75
- /**
76
- * Number of distinct programs in the arena — and therefore the number of buckets the
77
- * simulation-order pass histograms into. Program ids are dense in `[0, program_count)`.
78
- * @returns {number}
79
- */
80
- get program_count(): number;
81
- /**
82
- * Append a program, or hand back the placement an identical one already has.
83
- *
84
- * Deduplication is by CONTENT, not object identity: two emitters that authored the same graph
85
- * separately compile to distinct {@link ParticleProgram} objects holding the same bytecode, and
86
- * they should still land in one simulation bucket. Emitters are few and long-lived, so hashing
87
- * the words is not a cost worth avoiding.
88
- *
89
- * The `program_id` it returns is what the emitter record carries and what the coherence pass
90
- * buckets on. Keying on the emitter index instead would be simpler but strictly worse: N
91
- * emitters running one shared effect would become N buckets, each padded to a whole workgroup,
92
- * which both wastes lanes and splits a run that was already program-coherent.
93
- *
94
- * @param {import("../isa/ParticleProgram.js").ParticleProgram} program
95
- * @returns {{init_offset: number, init_count: number, update_offset: number, update_count: number, constant_offset: number, program_id: number}}
96
- */
97
- add(program: import("../isa/ParticleProgram.js").ParticleProgram): {
98
- init_offset: number;
99
- init_count: number;
100
- update_offset: number;
101
- update_count: number;
102
- constant_offset: number;
103
- program_id: number;
104
- };
105
- /**
106
- * The packed buffer: `[header][constants][code]`. Word {@link PARTICLE_PROGRAM_CODE_BASE_WORD} of
107
- * the header is the word index the code starts at, which moves every time a program's constants
108
- * are appended — the emitter records never carry it, they carry offsets into each section.
109
- *
110
- * @returns {Uint32Array}
111
- */
112
- build_program_buffer(): Uint32Array;
113
- #private;
114
- }
115
- //# sourceMappingURL=ProgramArena.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ProgramArena.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/renderer/particles/runtime/ProgramArena.js"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;GAaG;AACH;IAEI;;;OAGG;IACH,mBAFU,MAAM,EAAE,CAEK;IAEvB;;;OAGG;IACH,gBAFU,MAAM,EAAE,CAEE;IAEpB;;;OAGG;IACH,mBAFU,MAAM,CAEM;IAEtB;;;;OAIG;IACH,qBAFU,MAAM,CAEQ;IAExB;;;;;;;;;;;;;OAaG;IACH,qBAFU,MAAM,CAEQ;IAExB;;;;;;;;;OASG;IACH,uCAEC;IAED;;;OAGG;IACH,UAFU,IAAI,MAAM,EAAE;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAC,CAAC,CAErI;IAErB;;;;OAIG;IACH,4BAEC;IAED;;;;;;;;;;;;;;;OAeG;IACH,aAHW,OAAO,2BAA2B,EAAE,eAAe;qBACnC,MAAM;oBAAc,MAAM;uBAAiB,MAAM;sBAAgB,MAAM;yBAAmB,MAAM;oBAAc,MAAM;MAkC9I;IAUD;;;;;;OAMG;IACH,wBAFa,WAAW,CAcvB;;CACJ"}