@woosh/meep-engine 3.17.0 → 3.17.2
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.
- package/README.md +8 -72
- package/package.json +1 -1
- package/src/engine/ecs/transform/t64_set_rotation_axis_angle.d.ts +25 -0
- package/src/engine/ecs/transform/t64_set_rotation_axis_angle.d.ts.map +1 -0
- package/src/engine/ecs/transform/t64_set_rotation_axis_angle.js +45 -0
- package/src/engine/sound/simulation/AcousticSimulator.d.ts +11 -8
- package/src/engine/sound/simulation/AcousticSimulator.d.ts.map +1 -1
- package/src/engine/sound/simulation/AcousticSimulator.js +45 -36
- package/src/engine/sound/simulation/README.md +2 -2
- package/src/engine/sound/simulation/core/VolumeField.d.ts +9 -9
- package/src/engine/sound/simulation/core/VolumeField.d.ts.map +1 -1
- package/src/engine/sound/simulation/core/VolumeField.js +29 -13
- package/src/engine/sound/simulation/core/acoustic_occlusion_solve.d.ts +31 -0
- package/src/engine/sound/simulation/core/acoustic_occlusion_solve.d.ts.map +1 -0
- package/src/engine/sound/simulation/core/acoustic_occlusion_solve.js +134 -0
- package/src/shade/playground/volumetrics_froxel/main.d.ts.map +1 -1
- package/src/shade/playground/volumetrics_froxel/main.js +6 -1
- package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts +7 -0
- package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.js +7 -0
- package/src/engine/sound/simulation/core/OcclusionSolver.d.ts +0 -29
- package/src/engine/sound/simulation/core/OcclusionSolver.d.ts.map +0 -1
- package/src/engine/sound/simulation/core/OcclusionSolver.js +0 -112
- package/src/shade/renderer/particles/runtime/ProgramArena.d.ts +0 -115
- 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:**
|
|
12
|
-
|
|
13
|
-
values, and how input reaches the engine. The worked example beside
|
|
14
|
-
|
|
15
|
-
|
|
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,
|
|
36
|
-
|
|
37
|
-
|
|
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
|
@@ -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
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* @
|
|
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:
|
|
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[]}
|
|
119
|
-
* @param {
|
|
122
|
+
* @param {EventInstance[]} live_instances bounded set (≤ voice budget)
|
|
123
|
+
* @param {number[]|Float32Array|Float64Array} listener listener world position, 3 elements
|
|
120
124
|
*/
|
|
121
|
-
apply(
|
|
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":"
|
|
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 {
|
|
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 {
|
|
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
|
-
*
|
|
278
|
-
*
|
|
279
|
-
* @
|
|
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,
|
|
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
|
|
291
|
-
listener
|
|
292
|
+
source[0], source[1], source[2],
|
|
293
|
+
listener[0], listener[1], listener[2],
|
|
292
294
|
out.airLoss
|
|
293
295
|
);
|
|
294
296
|
|
|
295
|
-
|
|
296
|
-
listener, source,
|
|
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[]}
|
|
307
|
-
* @param {
|
|
308
|
+
* @param {EventInstance[]} live_instances bounded set (≤ voice budget)
|
|
309
|
+
* @param {number[]|Float32Array|Float64Array} listener listener world position, 3 elements
|
|
308
310
|
*/
|
|
309
|
-
apply(
|
|
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
|
|
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 <
|
|
342
|
-
const instance =
|
|
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 {
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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,
|
|
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(
|
|
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 -
|
|
497
|
-
pathGain *= this.#turnGain(listenerProbe, lpx - lhx, lpy - lhy, lpz - lhz,
|
|
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
|
-
|
|
603
|
-
let
|
|
604
|
-
let
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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, `
|
|
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
|
|
@@ -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 `(
|
|
36
|
-
* `(
|
|
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}
|
|
48
|
-
* @param {number}
|
|
49
|
-
* @param {number}
|
|
50
|
-
* @param {number}
|
|
51
|
-
* @param {number}
|
|
52
|
-
* @param {number}
|
|
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,
|
|
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":"
|
|
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 `(
|
|
88
|
-
* `(
|
|
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}
|
|
100
|
-
* @param {number}
|
|
101
|
-
* @param {number}
|
|
102
|
-
* @param {number}
|
|
103
|
-
* @param {number}
|
|
104
|
-
* @param {number}
|
|
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(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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,
|
|
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":"
|
|
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
|
|
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
|
|
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,29 +0,0 @@
|
|
|
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
|
-
* Pure: geometry in, numbers out. No WebAudio, no ECS, no allocation.
|
|
14
|
-
*/
|
|
15
|
-
export class OcclusionSolver {
|
|
16
|
-
/**
|
|
17
|
-
* @param {Vector3} listener listener world position
|
|
18
|
-
* @param {Vector3} source source world position
|
|
19
|
-
* @param {number} sourceRadius apparent radius of the source (0 = point source → boolean occlusion)
|
|
20
|
-
* @param {AcousticOccluderIndex} index
|
|
21
|
-
* @param {number} rayCount rays shot this tick (≥ 1)
|
|
22
|
-
* @param {number} smoothing EMA factor in (0, 1] — 1 = no smoothing
|
|
23
|
-
* @param {function():number} random seeded RNG; decorrelates the sample set per call
|
|
24
|
-
* @param {AcousticSourceState} state per-source temporal accumulation (mutated)
|
|
25
|
-
* @param {AcousticSolution} out occlusion + transmission written here (medium untouched)
|
|
26
|
-
*/
|
|
27
|
-
solve(listener: Vector3, source: Vector3, sourceRadius: number, index: AcousticOccluderIndex, rayCount: number, smoothing: number, random: () => number, state: AcousticSourceState, out: AcousticSolution): void;
|
|
28
|
-
}
|
|
29
|
-
//# sourceMappingURL=OcclusionSolver.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
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,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"}
|