@woosh/meep-engine 3.11.1 → 3.11.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/package.json +1 -1
- package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
- package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +294 -290
- package/src/engine/graphics3/ParticipatingMedia.d.ts +111 -0
- package/src/engine/graphics3/ParticipatingMedia.d.ts.map +1 -0
- package/src/engine/graphics3/ParticipatingMedia.js +203 -0
- package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.d.ts +34 -0
- package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.d.ts.map +1 -0
- package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.js +73 -0
- package/src/engine/graphics3/ParticipatingMediaSystem3.d.ts +87 -0
- package/src/engine/graphics3/ParticipatingMediaSystem3.d.ts.map +1 -0
- package/src/engine/graphics3/ParticipatingMediaSystem3.js +184 -0
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/shade/playground/volumetrics_froxel/README.md +104 -1
- package/src/shade/playground/volumetrics_froxel/index.html +20 -1
- package/src/shade/playground/volumetrics_froxel/main.d.ts.map +1 -1
- package/src/shade/playground/volumetrics_froxel/main.js +298 -0
- package/src/shade/playground/volumetrics_froxel/measure.d.ts +119 -0
- package/src/shade/playground/volumetrics_froxel/measure.d.ts.map +1 -0
- package/src/shade/playground/volumetrics_froxel/measure.js +559 -0
- package/src/shade/renderer/Renderer.d.ts.map +1 -1
- package/src/shade/renderer/Renderer.js +22 -18
- package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
- package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.d.ts +14 -13
- package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.d.ts.map +1 -1
- package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.js +133 -129
- package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +480 -315
- package/src/shade/renderer/volumetrics/NOTES.md +649 -8
- package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts +5 -0
- package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.js +21 -2
- package/src/shade/renderer/volumetrics/SceneVolumetrics.d.ts +26 -0
- package/src/shade/renderer/volumetrics/SceneVolumetrics.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/SceneVolumetrics.js +64 -21
- package/src/shade/renderer/volumetrics/VOLUMETRICS_METADATA_STRUCT.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/VOLUMETRICS_METADATA_STRUCT.js +84 -28
- package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.d.ts +98 -0
- package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.js +111 -0
- package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.d.ts +84 -0
- package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.js +252 -0
- package/src/shade/renderer/volumetrics/chunk_integrate_optical_depth.js +75 -75
- package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.d.ts +43 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.js +145 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.d.ts +20 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.js +48 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.d.ts +38 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.js +107 -0
- package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts +4 -1
- package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +577 -471
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js +64 -13
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.d.ts +8 -0
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.js +174 -0
- package/src/shade/wgsl/emulator/CPUBitmapData.d.ts +25 -0
- package/src/shade/wgsl/emulator/CPUBitmapData.d.ts.map +1 -1
- package/src/shade/wgsl/emulator/CPUBitmapData.js +33 -0
- package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +2221 -2215
- package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
- package/src/shade/wgsl/emulator/WGSLLanguageCore.js +1297 -1117
- package/src/shade/wgsl/emulator/cpu_texture_gather.d.ts +69 -0
- package/src/shade/wgsl/emulator/cpu_texture_gather.d.ts.map +1 -0
- package/src/shade/wgsl/emulator/cpu_texture_gather.js +148 -0
- package/src/shade/wgsl/emulator/cpu_texture_sample.d.ts +16 -0
- package/src/shade/wgsl/emulator/cpu_texture_sample.d.ts.map +1 -1
- package/src/shade/wgsl/emulator/cpu_texture_sample.js +176 -172
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { assert } from "../../core/assert.js";
|
|
2
|
+
import { isArrayEqualStrict } from "../../core/collection/array/isArrayEqualStrict.js";
|
|
3
|
+
import { ParticipatingMediaVolume } from "../../shade/renderer/volumetrics/ParticipatingMediaVolume.js";
|
|
4
|
+
import { System } from "../ecs/System.js";
|
|
5
|
+
import { Transform } from "../ecs/transform/Transform.js";
|
|
6
|
+
import { ParticipatingMedia } from "./ParticipatingMedia.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Mirrors fog, smoke and cloud entities into Shade's volumetrics.
|
|
10
|
+
*
|
|
11
|
+
* The same shape as {@link LightSystem3}: the ECS says what exists and where, and this keeps the
|
|
12
|
+
* renderer's own objects in step. A {@link ParticipatingMediaVolume} is a unit cube posed by a
|
|
13
|
+
* transform, so an entity's `Transform` is the volume — position, size and orientation with no
|
|
14
|
+
* translation step in between.
|
|
15
|
+
*
|
|
16
|
+
* **The transform is taken as world-space, with no hierarchy applied**, which is what the volume
|
|
17
|
+
* model can express: a volume is an oriented box in the scene and Shade has no parent chain to walk
|
|
18
|
+
* for it. Parenting a fog volume to a moving entity therefore leaves the fog where the child's own
|
|
19
|
+
* transform puts it. The lights are placed the same way, for the same reason.
|
|
20
|
+
*
|
|
21
|
+
* **Changes are polled once a frame rather than subscribed to.** A `Transform` announces itself and
|
|
22
|
+
* nothing else here does — a density tuned by an editor slider, a particle spec swapped wholesale
|
|
23
|
+
* for a different Mie solution — so subscribing would cover one field in three and the other two
|
|
24
|
+
* would still have to be polled. Two paths that have to agree, where one does the whole job. It
|
|
25
|
+
* costs about two dozen float comparisons per volume per frame and a scene holds volumes in the
|
|
26
|
+
* tens, so the comparison is cheaper than the signal plumbing it replaces. The price is latency: a
|
|
27
|
+
* change is visible within a frame rather than instantly.
|
|
28
|
+
*
|
|
29
|
+
* **Nothing is uploaded unless something actually changed.** The GPU table is repacked whole
|
|
30
|
+
* whenever {@link SceneVolumetrics.version} moves, so an unconditional copy every frame would
|
|
31
|
+
* re-upload every volume in the scene forever. The comparison is what decides, and on almost every
|
|
32
|
+
* frame it decides nothing has to happen.
|
|
33
|
+
*
|
|
34
|
+
* @see ParticipatingMedia the component
|
|
35
|
+
* @see SceneVolumetrics what the volumes are handed to
|
|
36
|
+
*/
|
|
37
|
+
export class ParticipatingMediaSystem3 extends System {
|
|
38
|
+
|
|
39
|
+
dependencies = [ParticipatingMedia, Transform];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @type {GraphicsEngine3}
|
|
43
|
+
*/
|
|
44
|
+
#graphics;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @type {Scene}
|
|
48
|
+
*/
|
|
49
|
+
#scene;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @type {Map<number, {component: ParticipatingMedia, transform: Transform, volume: ParticipatingMediaVolume}>}
|
|
53
|
+
*/
|
|
54
|
+
#entries = new Map();
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {GraphicsEngine3} graphics
|
|
58
|
+
* @param {Scene} scene
|
|
59
|
+
*/
|
|
60
|
+
constructor(graphics, scene) {
|
|
61
|
+
super();
|
|
62
|
+
|
|
63
|
+
assert.defined(graphics, 'graphics');
|
|
64
|
+
assert.defined(scene, 'scene');
|
|
65
|
+
assert.equal(scene.isScene, true, 'scene.isScene !== true');
|
|
66
|
+
|
|
67
|
+
this.#graphics = graphics;
|
|
68
|
+
this.#scene = scene;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async startup(entityManager) {
|
|
72
|
+
this.entityManager = entityManager;
|
|
73
|
+
|
|
74
|
+
this.#graphics.set_scene(this.#scene);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async shutdown(entityManager) {
|
|
78
|
+
for (const entity of Array.from(this.#entries.keys())) {
|
|
79
|
+
this.#drop(entity);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {ParticipatingMedia} component
|
|
85
|
+
* @param {Transform} transform
|
|
86
|
+
* @param {number} entity
|
|
87
|
+
*/
|
|
88
|
+
link(component, transform, entity) {
|
|
89
|
+
const volume = new ParticipatingMediaVolume();
|
|
90
|
+
|
|
91
|
+
participating_media_volume_write(volume, component, transform);
|
|
92
|
+
|
|
93
|
+
this.#scene.volumetrics.add(volume);
|
|
94
|
+
|
|
95
|
+
this.#entries.set(entity, { component, transform, volume });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {ParticipatingMedia} component
|
|
100
|
+
* @param {Transform} transform
|
|
101
|
+
* @param {number} entity
|
|
102
|
+
*/
|
|
103
|
+
unlink(component, transform, entity) {
|
|
104
|
+
this.#drop(entity);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {number} time_delta_seconds
|
|
109
|
+
*/
|
|
110
|
+
update(time_delta_seconds) {
|
|
111
|
+
const volumetrics = this.#scene.volumetrics;
|
|
112
|
+
|
|
113
|
+
for (const entry of this.#entries.values()) {
|
|
114
|
+
const volume = entry.volume;
|
|
115
|
+
|
|
116
|
+
if (participating_media_volume_is_current(volume, entry.component, entry.transform)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
participating_media_volume_write(volume, entry.component, entry.transform);
|
|
121
|
+
|
|
122
|
+
volumetrics.invalidate();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {number} entity
|
|
128
|
+
*/
|
|
129
|
+
#drop(entity) {
|
|
130
|
+
const entry = this.#entries.get(entity);
|
|
131
|
+
|
|
132
|
+
if (entry === undefined) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.#scene.volumetrics.remove(entry.volume);
|
|
137
|
+
|
|
138
|
+
this.#entries.delete(entity);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Copy what the component and its transform say onto the volume Shade will render.
|
|
144
|
+
*
|
|
145
|
+
* The particle spec is **copied, not referenced**: two entities are routinely built from the same
|
|
146
|
+
* blueprint and would otherwise share one spec, so tuning one volume's particle would silently
|
|
147
|
+
* retune the other's.
|
|
148
|
+
*
|
|
149
|
+
* @param {ParticipatingMediaVolume} volume Shade's
|
|
150
|
+
* @param {ParticipatingMedia} component meep's
|
|
151
|
+
* @param {Transform} transform where the box is
|
|
152
|
+
*/
|
|
153
|
+
export function participating_media_volume_write(volume, component, transform) {
|
|
154
|
+
volume.density = component.density;
|
|
155
|
+
volume.fade_distance = component.fade_distance;
|
|
156
|
+
|
|
157
|
+
volume.particle_spec.copy(component.particle_spec);
|
|
158
|
+
|
|
159
|
+
volume.transform.copy(transform);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Whether the volume already says what the component and transform say.
|
|
164
|
+
*
|
|
165
|
+
* True on all but a handful of frames, which is the point of asking: a `false` here costs the whole
|
|
166
|
+
* scene's volumetrics table a trip to the GPU.
|
|
167
|
+
*
|
|
168
|
+
* **The transform is compared by its matrix rather than by `Transform.equals`**, because the matrix
|
|
169
|
+
* is the entire transform the GPU is given — the volume's position, rotation and scale are never
|
|
170
|
+
* read on their own. That makes this exactly the question worth asking, "would the upload differ",
|
|
171
|
+
* and it catches a transform posed by writing its matrix, which comparing position, rotation and
|
|
172
|
+
* scale would not.
|
|
173
|
+
*
|
|
174
|
+
* @param {ParticipatingMediaVolume} volume
|
|
175
|
+
* @param {ParticipatingMedia} component
|
|
176
|
+
* @param {Transform} transform
|
|
177
|
+
* @returns {boolean}
|
|
178
|
+
*/
|
|
179
|
+
export function participating_media_volume_is_current(volume, component, transform) {
|
|
180
|
+
return volume.density === component.density
|
|
181
|
+
&& volume.fade_distance === component.fade_distance
|
|
182
|
+
&& isArrayEqualStrict(volume.transform.matrix, transform.matrix)
|
|
183
|
+
&& volume.particle_spec.equals(component.particle_spec);
|
|
184
|
+
}
|
|
@@ -64,11 +64,11 @@ export class FluidObstacleSystem extends System<any> {
|
|
|
64
64
|
/**
|
|
65
65
|
* @param {FluidComponent} fluid
|
|
66
66
|
*/
|
|
67
|
-
static "__#
|
|
67
|
+
static "__#356@#refresh_masks"(fluid: FluidComponent): void;
|
|
68
68
|
/**
|
|
69
69
|
* @param {FluidComponent} fluid
|
|
70
70
|
*/
|
|
71
|
-
static "__#
|
|
71
|
+
static "__#356@#clear_field"(fluid: FluidComponent): void;
|
|
72
72
|
/**
|
|
73
73
|
* Mark every cell of `fluid` whose centre lies within `inflation` of the
|
|
74
74
|
* posed shape as solid. Iteration is clipped to the shape's world AABB
|
|
@@ -80,7 +80,7 @@ export class FluidObstacleSystem extends System<any> {
|
|
|
80
80
|
* @param {number} inflation world-units SDF threshold
|
|
81
81
|
* @param {Float64Array} point length-3 scratch
|
|
82
82
|
*/
|
|
83
|
-
static "__#
|
|
83
|
+
static "__#356@#voxelize"(fluid: FluidComponent, posed: PosedShape3D, aabb: Float64Array, inflation: number, point: Float64Array): void;
|
|
84
84
|
/**
|
|
85
85
|
* Write the obstacle's translation velocity onto every face of every cell
|
|
86
86
|
* it voxelized — the moving-wall boundary condition. Runs AFTER the mask
|
|
@@ -100,7 +100,7 @@ export class FluidObstacleSystem extends System<any> {
|
|
|
100
100
|
* @param {number} wvy
|
|
101
101
|
* @param {number} wvz
|
|
102
102
|
*/
|
|
103
|
-
static "__#
|
|
103
|
+
static "__#356@#stamp_wall_velocity"(fluid: FluidComponent, posed: PosedShape3D, aabb: Float64Array, inflation: number, point: Float64Array, wvx: number, wvy: number, wvz: number): void;
|
|
104
104
|
constructor();
|
|
105
105
|
dependencies: (typeof FluidObstacle)[];
|
|
106
106
|
components_used: (ResourceAccessSpecification<typeof Transform> | ResourceAccessSpecification<typeof RigidBody> | ResourceAccessSpecification<typeof Collider> | ResourceAccessSpecification<typeof FluidComponent> | ResourceAccessSpecification<typeof FluidObstacle>)[];
|
|
@@ -30,6 +30,36 @@ Those fixes are in the permanent path now and there is nothing left to A/B. What
|
|
|
30
30
|
the *next* change here — the performance findings especially, since every one of them trades image
|
|
31
31
|
quality for time and none can be judged from a screenshot.
|
|
32
32
|
|
|
33
|
+
Q-01 was judged here and now ships unconditionally: every clustered point and spot light is occluded
|
|
34
|
+
by geometry, sampled with one nearest tap out of the shadow atlas. The two checkboxes that switched
|
|
35
|
+
that off, or swapped the tap for the deferred pass's filter kernels, are gone along with the metadata
|
|
36
|
+
word behind them — `volumetrics/NOTES.md` keeps the measurements that closed the question. The
|
|
37
|
+
torches are the local lights here: 130 point lights, every one of them a shadow caster, and **zero
|
|
38
|
+
spot lights**, so the spot half has no on-screen coverage on this page and is covered by
|
|
39
|
+
`shader_volumetrics_build_lighting_local_shadow.spec.js` instead. Adding a `SpotLight` from the
|
|
40
|
+
console is the way to look at it.
|
|
41
|
+
|
|
42
|
+
Q-04 went the same way and its switch is gone too. The sun's media self-shadowing is one fetch into
|
|
43
|
+
a ladder of three cascaded volumes built once a frame with their third axis on the sun, and that is
|
|
44
|
+
now the only path for it: the `__VOL_SUN_SHADOW` global and the checkbox over it are removed along
|
|
45
|
+
with the `sun_shadow_mode` word they drove, so nothing on this page reproduces the march/cascade
|
|
46
|
+
comparison any more. Restoring it means re-adding that word and the branch in
|
|
47
|
+
`shader_volumetrics_build_lighting` that reads it.
|
|
48
|
+
|
|
49
|
+
Worth knowing what it was, because it is the size of what the page can no longer show. Against a
|
|
50
|
+
march at `STEP_COUNT = 512` the cascades read within 1.2 mean luminance at every viewpoint here and
|
|
51
|
+
across 40 to 110 degrees of field of view, while sixteen steps read **+58** and **+70** at the two
|
|
52
|
+
poses beside the small fog boxes — the step there is eight units, the box is three thick, and the
|
|
53
|
+
march goes straight over it. They are also cheaper: 0.37 ms against 0.53 at 1080p, 0.81 against 1.52
|
|
54
|
+
at 2160p, per pass. `volumetrics/NOTES.md` has the whole measurement, including the one that says a
|
|
55
|
+
*single* world-uniform box could not have been sized correctly at any resolution — every shape
|
|
56
|
+
measured has a field of view at which it is 28 to 50 luminance from the truth, and which one that is
|
|
57
|
+
moves with the resolution.
|
|
58
|
+
|
|
59
|
+
The march itself is still in the tree and still runs: every point and spot light uses it, and so
|
|
60
|
+
would a second directional light. Only the first one is amortized, because there is one ladder of
|
|
61
|
+
boxes and it is aligned to one direction.
|
|
62
|
+
|
|
33
63
|
## Measuring, rather than eyeballing
|
|
34
64
|
|
|
35
65
|
This pass is stochastic and temporally filtered, so most changes to it move the image by less than
|
|
@@ -50,6 +80,14 @@ confident wrong answer during the work above:
|
|
|
50
80
|
*change → settle ~45 frames → average*, and take the floor by making an identical no-op change so
|
|
51
81
|
both paths cost the same.
|
|
52
82
|
|
|
83
|
+
- **Frame time here is presentation-throttled, not GPU-bound.** At 1920×1080 every setting reads
|
|
84
|
+
~16.5 ms, including settings that do obviously different amounts of work — `ms_octaves` at 1 and at
|
|
85
|
+
8 come back within noise of each other, which is the tell. The swap chain is pacing the
|
|
86
|
+
measurement. Raise `renderer.pixel_ratio` until the GPU is the bottleneck (3 gives 3840×2160 and
|
|
87
|
+
~10 ms of real work here), interleave the settings round-robin so drift hits all of them equally,
|
|
88
|
+
and take medians. Remember the froxel grid scales with resolution, so a per-froxel cost measured at
|
|
89
|
+
4× the pixels is 4× the one you will pay.
|
|
90
|
+
|
|
53
91
|
Prefer the **signed mean** when the change has a direction; it discriminates far better than the
|
|
54
92
|
mean-absolute, which is dominated by noise. C-01b read −0.148 at 60° FOV against a floor of ±0.05,
|
|
55
93
|
and −1.071 at 110°: same fix, and that 7× is the `1/cos θ` scaling it predicts.
|
|
@@ -57,6 +95,68 @@ and −1.071 at 110°: same fix, and that 7× is the `1/cos θ` scaling it predi
|
|
|
57
95
|
Also: mutating `light.intensity` does not mark the light buffer dirty, so a sweep of it renders
|
|
58
96
|
identically. Remove and re-add the light instead.
|
|
59
97
|
|
|
98
|
+
### measure.js
|
|
99
|
+
|
|
100
|
+
`measure.js` is that protocol as code, loaded from the console rather than by the page:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
const m = await (await import('./measure.js')).attach();
|
|
104
|
+
const r = await m.ab(v => globalThis.__VOL_TAA_ENABLED = v, false, true);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`attach` pauses the page's loop — which matters for a reason beyond frame counting: that loop
|
|
108
|
+
assigns `renderer.pixel_ratio = window.devicePixelRatio` on every tick, ahead of its own pause check,
|
|
109
|
+
so a raised pixel ratio is undone before the next frame unless the loop is stopped. `set_pixel_ratio`
|
|
110
|
+
replaces the property for the same reason.
|
|
111
|
+
|
|
112
|
+
Everything renders into a texture the module owns, through `Renderer.render_to_target`, which shares
|
|
113
|
+
the view context and therefore all the temporal history with the canvas path. That is not a
|
|
114
|
+
refinement: a canvas texture is configured by the presentation path, cannot carry `COPY_SRC`, and is
|
|
115
|
+
rotated away the moment the frame is presented, so a `drawImage` off it a task later returns an empty
|
|
116
|
+
bitmap — and a comparison of two blanks reports perfect agreement. Driving the frames from here also
|
|
117
|
+
takes the swap chain out of the timing entirely, which is the presentation-throttling trap above.
|
|
118
|
+
|
|
119
|
+
`ab` runs the two settings interleaved and takes its own floor from a repeat of each. `fly`,
|
|
120
|
+
`converge_at` and `series_stats` are the camera-path half: a structure fitted to the camera can be
|
|
121
|
+
perfectly correct standing still and swim under motion, and neither swimming nor the ghosting the
|
|
122
|
+
0.95-blend TAA turns it into is visible in a still. `time` and `profile` answer different questions and
|
|
123
|
+
`profile` is the one to believe — at 1080p, whole-frame timing of Q-04 came back with the wrong sign
|
|
124
|
+
three runs in a row while the per-pass capture read it cleanly.
|
|
125
|
+
|
|
126
|
+
`profile` records a `WORKLOAD` session and decodes it in the page, so a cost number does not need a
|
|
127
|
+
downloaded `.sgpt` and a second tool. It returns the median duration per pass label over the frames
|
|
128
|
+
it recorded. Interleave the settings and take medians across rounds even so — the run-to-run spread
|
|
129
|
+
on this machine moves a pass by more than most changes to it do:
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
const p = await m.profile({ frames: 24 });
|
|
133
|
+
p['Volumetrics / build sun shadow volume 0'];
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Recording a GPU profile
|
|
137
|
+
|
|
138
|
+
**T** starts a recording, **T** again stops it and downloads a `.sgpt` capture. The engine side is
|
|
139
|
+
already there — `Renderer.profile_session` is a nullable field and the null checks around it are the
|
|
140
|
+
whole integration — so this page is only the two imports, the key handler, and the metadata a capture
|
|
141
|
+
needs in order to mean anything later: adapter, engine version, device features, and a note carrying
|
|
142
|
+
the viewpoint, FOV, pixel ratio and volumetric TAA setting that were in force when you pressed the
|
|
143
|
+
key.
|
|
144
|
+
|
|
145
|
+
Level is `WORKLOAD`: pass spans, the frame graph, and dispatch/draw counts. Measured on this page it
|
|
146
|
+
runs about 11 KB a frame — roughly 660 KB/s at 60 Hz, and around 393 spans per frame — with the frame
|
|
147
|
+
graph topology stored once and shared by every frame. `VERBOSE` above it adds bind group contents at
|
|
148
|
+
about seven times the rate and wants a frame limit set; `PROFILE_LEVEL` in `main.js` is the one line
|
|
149
|
+
to change.
|
|
150
|
+
|
|
151
|
+
Two things to know. Stopping does not end the recording immediately: a frame is committed when its
|
|
152
|
+
timestamp readback lands, a `mapAsync` a frame or two behind submit, so the handler drains the queue
|
|
153
|
+
and waits for the frame count to settle before it writes the file — otherwise the last frames, the
|
|
154
|
+
ones you were probably looking at, are the ones missing. And the browser asks before letting a page
|
|
155
|
+
save more than one file, so a second capture may never appear; the filename goes to the console
|
|
156
|
+
either way.
|
|
157
|
+
|
|
158
|
+
Frames only accumulate while the page is rendering, so a recording taken while paused stays at zero.
|
|
159
|
+
|
|
60
160
|
## Scene
|
|
61
161
|
|
|
62
162
|
`flying_world_-_battle_of_the_trash_god/v2`, six participating-media volumes, a low sun, and the
|
|
@@ -80,7 +180,10 @@ accepts version 2 only, deliberately and with no backward compatibility.
|
|
|
80
180
|
## Notes
|
|
81
181
|
|
|
82
182
|
Two viewpoints from the sibling repo (`v_volumetrics_light_extinction_0` and `_1`) sit inside
|
|
83
|
-
geometry here and were replaced with placements verified against this build.
|
|
183
|
+
geometry here and were replaced with placements verified against this build. Two more —
|
|
184
|
+
*Beside the fog box* and *Beside the fog box, deeper* — were added for Q-04: they are the only
|
|
185
|
+
placements in the scene where the sun's path through the medium is short enough for the 16-step
|
|
186
|
+
march to miss it, which makes them the ones any change to that path has to be judged at. *Log camera* writes the
|
|
84
187
|
current placement to the console in the form the `VIEWS` table takes, which is how to add more.
|
|
85
188
|
|
|
86
189
|
Frame time settles around 28 ms at 1920×1080 on a 4070-class part. That is the whole frame, not the
|
|
@@ -117,12 +117,14 @@
|
|
|
117
117
|
#status.fail { color: var(--fail); }
|
|
118
118
|
#status.warn { color: var(--warn); }
|
|
119
119
|
|
|
120
|
-
#stats {
|
|
120
|
+
#stats, #profile_status {
|
|
121
121
|
margin-top: 4px;
|
|
122
122
|
font: 12px ui-monospace, SFMono-Regular, Consolas, monospace;
|
|
123
123
|
color: var(--text);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
#profile_status.recording { color: var(--fail); }
|
|
127
|
+
|
|
126
128
|
p.note { margin: 6px 0 0; font-size: 11.5px; color: var(--muted); }
|
|
127
129
|
</style>
|
|
128
130
|
</head>
|
|
@@ -220,6 +222,23 @@
|
|
|
220
222
|
|
|
221
223
|
<hr>
|
|
222
224
|
|
|
225
|
+
<h2>Profiling</h2>
|
|
226
|
+
|
|
227
|
+
<div id="profile_status">idle</div>
|
|
228
|
+
<p class="note">
|
|
229
|
+
<strong>T</strong> starts a GPU profile recording; <strong>T</strong> again stops it and
|
|
230
|
+
downloads the <code>.sgpt</code> capture. Level is <code>WORKLOAD</code> — pass spans, the
|
|
231
|
+
frame graph, and dispatch/draw counts, which measures around 11 KB a frame on this page.
|
|
232
|
+
Frames only accumulate while the page is rendering, so a recording taken while paused stays
|
|
233
|
+
at zero.
|
|
234
|
+
</p>
|
|
235
|
+
<p class="note">
|
|
236
|
+
The browser asks before letting a page save more than one file. If a second capture never
|
|
237
|
+
appears, that prompt is why — allow it, and the filename is on the console either way.
|
|
238
|
+
</p>
|
|
239
|
+
|
|
240
|
+
<hr>
|
|
241
|
+
|
|
223
242
|
<div class="row">
|
|
224
243
|
<button id="pause">Pause</button>
|
|
225
244
|
<button id="log_camera">Log camera</button>
|
|
@@ -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":"AA8ZA,uCAsfC"}
|