@woosh/meep-engine 3.18.0 → 3.19.0
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/build/bundle-worker-image-decoder.js +1 -1
- package/editor/view/particles/effect/ParticleCurveEditorView.d.ts.map +1 -1
- package/editor/view/particles/effect/ParticleCurveEditorView.js +303 -120
- package/editor/view/particles/effect/ParticleGradientEditorView.d.ts.map +1 -1
- package/editor/view/particles/effect/ParticleGradientEditorView.js +108 -63
- package/editor/view/particles/effect/ParticleGraphEditorView.js +1 -1
- package/editor/view/particles/effect/ParticleNodeParametersView.d.ts.map +1 -1
- package/editor/view/particles/effect/ParticleNodeParametersView.js +3 -1
- package/editor/view/particles/effect/particle-editor.css +60 -30
- package/package.json +1 -2
- package/samples/engine/README.md +1 -1
- package/src/core/binary/compression/decompress_bytes.d.ts +13 -0
- package/src/core/binary/compression/decompress_bytes.d.ts.map +1 -0
- package/src/core/binary/compression/decompress_bytes.js +28 -0
- package/src/core/model/node-graph/visual/layout/layout_assign_coordinates.js +67 -22
- package/src/engine/asset/loaders/image/ImageDecoderWorker.js +12 -27
- package/src/engine/asset/loaders/image/prototypePNG.js +8 -7
- package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
- package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +4 -0
- package/src/engine/graphics/ecs/particles/ParticleEffect.d.ts +266 -0
- package/src/engine/graphics/ecs/particles/ParticleEffect.d.ts.map +1 -0
- package/src/engine/graphics/ecs/particles/ParticleEffect.js +455 -0
- package/src/engine/graphics/ecs/particles/ParticleEffectSerializationAdapter.d.ts +58 -0
- package/src/engine/graphics/ecs/particles/ParticleEffectSerializationAdapter.d.ts.map +1 -0
- package/src/engine/graphics/ecs/particles/ParticleEffectSerializationAdapter.js +219 -0
- package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts +178 -25
- package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts.map +1 -1
- package/src/engine/graphics3/GPUParticleEmitterSystem.js +809 -310
- package/src/format/image/png/PNGReader.d.ts +7 -6
- package/src/format/image/png/PNGReader.d.ts.map +1 -1
- package/src/format/image/png/PNGReader.js +13 -12
- package/src/format/image/png/chunk/png_chunk_decode_iTXt.d.ts +3 -2
- package/src/format/image/png/chunk/png_chunk_decode_iTXt.d.ts.map +1 -1
- package/src/format/image/png/chunk/png_chunk_decode_iTXt.js +5 -4
- package/src/format/image/png/chunk/png_chunk_decode_zTXt.d.ts +3 -2
- package/src/format/image/png/chunk/png_chunk_decode_zTXt.d.ts.map +1 -1
- package/src/format/image/png/chunk/png_chunk_decode_zTXt.js +5 -4
- package/src/format/image/png/png_inflate.d.ts +3 -3
- package/src/format/image/png/png_inflate.d.ts.map +1 -1
- package/src/format/image/png/png_inflate.js +29 -39
- package/src/format/texture/ktx2/ktx2_read.d.ts +4 -4
- package/src/format/texture/ktx2/ktx2_read.d.ts.map +1 -1
- package/src/format/texture/ktx2/ktx2_read.js +18 -21
- package/src/shade/playground/particle_ecs/README.md +203 -0
- package/src/shade/playground/particle_ecs/bonfire_editor.d.ts +39 -0
- package/src/shade/playground/particle_ecs/bonfire_editor.d.ts.map +1 -0
- package/src/shade/playground/particle_ecs/bonfire_editor.js +315 -0
- package/src/shade/playground/particle_ecs/bonfire_effects.d.ts +145 -0
- package/src/shade/playground/particle_ecs/bonfire_effects.d.ts.map +1 -0
- package/src/shade/playground/particle_ecs/bonfire_effects.js +202 -0
- package/src/shade/playground/particle_ecs/bonfire_sprites.d.ts +23 -0
- package/src/shade/playground/particle_ecs/bonfire_sprites.d.ts.map +1 -0
- package/src/shade/playground/particle_ecs/bonfire_sprites.js +315 -0
- package/src/shade/playground/particle_ecs/bonfire_world.d.ts +86 -0
- package/src/shade/playground/particle_ecs/bonfire_world.d.ts.map +1 -0
- package/src/shade/playground/particle_ecs/bonfire_world.js +303 -0
- package/src/shade/playground/particle_ecs/effects/embers.json +1634 -0
- package/src/shade/playground/particle_ecs/effects/flame.json +1882 -0
- package/src/shade/playground/particle_ecs/effects/smoke.json +1860 -0
- package/src/shade/playground/particle_ecs/effects/soot.json +1606 -0
- package/src/shade/playground/particle_ecs/index.html +330 -0
- package/src/shade/playground/particle_ecs/main.d.ts +2 -0
- package/src/shade/playground/particle_ecs/main.d.ts.map +1 -0
- package/src/shade/playground/particle_ecs/main.js +566 -0
- package/src/shade/playground/particle_ecs/moonlit_environment.d.ts +16 -0
- package/src/shade/playground/particle_ecs/moonlit_environment.d.ts.map +1 -0
- package/src/shade/playground/particle_ecs/moonlit_environment.js +144 -0
- package/src/shade/playground/particle_editor/README.md +5 -4
- package/src/shade/playground/profile_hotkey.d.ts +58 -0
- package/src/shade/playground/profile_hotkey.d.ts.map +1 -0
- package/src/shade/playground/profile_hotkey.js +325 -0
- package/src/shade/playground/ssr_variance/README.md +105 -0
- package/src/shade/playground/ssr_variance/capture.d.ts +16 -0
- package/src/shade/playground/ssr_variance/capture.d.ts.map +1 -0
- package/src/shade/playground/ssr_variance/capture.js +96 -0
- package/src/shade/playground/ssr_variance/index.html +22 -0
- package/src/shade/playground/ssr_variance/main.d.ts +2 -0
- package/src/shade/playground/ssr_variance/main.d.ts.map +1 -0
- package/src/shade/playground/ssr_variance/main.js +246 -0
- package/src/shade/playground/ssr_variance/reference.d.ts +18 -0
- package/src/shade/playground/ssr_variance/reference.d.ts.map +1 -0
- package/src/shade/playground/ssr_variance/reference.js +78 -0
- package/src/shade/playground/ssr_variance/scene.d.ts +11 -0
- package/src/shade/playground/ssr_variance/scene.d.ts.map +1 -0
- package/src/shade/playground/ssr_variance/scene.js +61 -0
- package/src/shade/playground/ssr_variance/statistics.d.ts +44 -0
- package/src/shade/playground/ssr_variance/statistics.d.ts.map +1 -0
- package/src/shade/playground/ssr_variance/statistics.js +51 -0
- package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts.map +1 -1
- package/src/shade/renderer/loader/gltf/tiny-gltf.js +9 -3
- package/src/shade/renderer/loader/usd/usd_decode_image.d.ts +4 -4
- package/src/shade/renderer/loader/usd/usd_decode_image.d.ts.map +1 -1
- package/src/shade/renderer/loader/usd/usd_decode_image.js +4 -4
- package/src/shade/renderer/particles/DESIGN.md +748 -703
- package/src/shade/renderer/particles/runtime/ParticleEmitter.d.ts +11 -4
- package/src/shade/renderer/particles/runtime/ParticleEmitter.d.ts.map +1 -1
- package/src/shade/renderer/particles/runtime/ParticleEmitter.js +431 -424
- package/src/shade/renderer/postprocess/ssr/SSR.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/ssr/SSR.js +2 -1
- package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_reprojection.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_reprojection.js +0 -6
- package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_sample_history.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_sample_history.js +19 -5
- package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_temporal_accumulate.d.ts +4 -0
- package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_temporal_accumulate.d.ts.map +1 -0
- package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_temporal_accumulate.js +133 -0
- package/src/shade/renderer/postprocess/ssr/ssr_reproject_shader.d.ts +0 -8
- package/src/shade/renderer/postprocess/ssr/ssr_reproject_shader.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/ssr/ssr_reproject_shader.js +16 -113
- package/src/shade/renderer/texture/source/texel_data_from_ktx2.d.ts +2 -2
- package/src/shade/renderer/texture/source/texel_data_from_ktx2.d.ts.map +1 -1
- package/src/shade/renderer/texture/source/texel_data_from_ktx2.js +3 -3
- package/src/shade/renderer/particles/shaders/chunk_particle_emitter_warmup.d.ts +0 -20
- package/src/shade/renderer/particles/shaders/chunk_particle_emitter_warmup.d.ts.map +0 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_emitter_world_sphere.d.ts +0 -14
- package/src/shade/renderer/particles/shaders/chunk_particle_emitter_world_sphere.d.ts.map +0 -1
- package/src/shade/renderer/particles/shaders/shader_particle_reclaim.d.ts +0 -50
- package/src/shade/renderer/particles/shaders/shader_particle_reclaim.d.ts.map +0 -1
- package/src/shade/renderer/postprocess/ssr/reproject/shader_ffx_denoiser_reflections_reproject.d.ts +0 -17
- package/src/shade/renderer/postprocess/ssr/reproject/shader_ffx_denoiser_reflections_reproject.d.ts.map +0 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import Vector3 from "../../../core/geom/Vector3.js";
|
|
2
|
+
import { Sampler2D } from "../../../engine/graphics/texture/sampler/Sampler2D.js";
|
|
3
|
+
import { sampler2d_to_f16 } from "../../../engine/graphics/texture/sampler/sampler2d_to_f16.js";
|
|
4
|
+
import { octahedral_uv_to_direction } from "../../renderer/light/environment/octahedral_uv_to_direction.js";
|
|
5
|
+
import { ColorSpace } from "../../renderer/texture/ColorSpace.js";
|
|
6
|
+
import { ShadeImage } from "../../renderer/texture/source/ShadeImage.js";
|
|
7
|
+
import { ShadeTexture } from "../../renderer/texture/ShadeTexture.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A moonlit night sky, generated: what the bonfire is lit by when it is not lighting itself.
|
|
11
|
+
*
|
|
12
|
+
* Built rather than loaded for the same reason the sprites are — `test_assets/` is gitignored, so a
|
|
13
|
+
* fresh checkout has no environment map, and a page that needs one to look right would look wrong
|
|
14
|
+
* for everybody who has not fetched the asset repository. This is small, it is in linear radiance,
|
|
15
|
+
* and it is the same octahedral projection `scene.lights.environment` samples.
|
|
16
|
+
*
|
|
17
|
+
* **The sky is the reason the fire reads as a fire.** Shade assumes global illumination, so a scene
|
|
18
|
+
* with no environment renders unlit; give it a bright day and the flame washes out to a pale smear,
|
|
19
|
+
* because the fire's own light is then a small fraction of what is already there. A night sky about
|
|
20
|
+
* three orders of magnitude below daylight is what leaves the fire room to be the brightest thing
|
|
21
|
+
* in the frame, and what makes the smoke read as blue where the firelight has stopped reaching it.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolution per octahedral face grid.
|
|
26
|
+
*
|
|
27
|
+
* Small on purpose: this is an ambient source, convolved into irradiance and a handful of roughness
|
|
28
|
+
* mips before anything samples it. What it has to be is smooth and correctly oriented.
|
|
29
|
+
*
|
|
30
|
+
* @type {number}
|
|
31
|
+
*/
|
|
32
|
+
const RESOLUTION = 128;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Where the moon is. Unit, +Y up — the direction light arrives *from*, so the shadows fall the
|
|
36
|
+
* other way.
|
|
37
|
+
*
|
|
38
|
+
* @type {Vector3}
|
|
39
|
+
*/
|
|
40
|
+
export const MOON_DIRECTION = new Vector3(-0.42, 0.62, -0.66).normalize();
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Angular radius of the moon's disc, in radians. Far larger than the real one (about 0.0045): the
|
|
44
|
+
* map is 128 texels across a hemisphere, so a true disc would be a fraction of a texel and would
|
|
45
|
+
* disappear into the first mip. This is a moon to look at, not an ephemeris.
|
|
46
|
+
*
|
|
47
|
+
* @type {number}
|
|
48
|
+
*/
|
|
49
|
+
const MOON_RADIUS = 0.075;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @type {Vector3}
|
|
53
|
+
*/
|
|
54
|
+
const SCRATCH_DIRECTION = new Vector3();
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Radiance arriving from a direction, in linear space.
|
|
58
|
+
*
|
|
59
|
+
* @param {number[]} out rgb
|
|
60
|
+
* @param {number} x
|
|
61
|
+
* @param {number} y +Y is up
|
|
62
|
+
* @param {number} z
|
|
63
|
+
*/
|
|
64
|
+
function moonlit_radiance(out, x, y, z) {
|
|
65
|
+
// How close this direction is to the moon, as the cosine of the angle between them.
|
|
66
|
+
const alignment = x * MOON_DIRECTION.x + y * MOON_DIRECTION.y + z * MOON_DIRECTION.z;
|
|
67
|
+
const angle = Math.acos(Math.min(1, Math.max(-1, alignment)));
|
|
68
|
+
|
|
69
|
+
if (y >= 0) {
|
|
70
|
+
// Night sky: deepest at the zenith, lifting toward the horizon where the atmosphere is
|
|
71
|
+
// thicker and there is more of it to scatter what little light there is.
|
|
72
|
+
const horizon = Math.pow(1 - y, 4);
|
|
73
|
+
|
|
74
|
+
out[0] = 0.0035 + horizon * 0.011;
|
|
75
|
+
out[1] = 0.0052 + horizon * 0.014;
|
|
76
|
+
out[2] = 0.0115 + horizon * 0.020;
|
|
77
|
+
} else {
|
|
78
|
+
// Below the horizon: ground bounce, dimmer and less blue than the sky. This is what lights
|
|
79
|
+
// the underside of the smoke, so it is not zero however dark the night is.
|
|
80
|
+
const t = Math.pow(1 + y, 2);
|
|
81
|
+
|
|
82
|
+
out[0] = 0.0016 + t * 0.0042;
|
|
83
|
+
out[1] = 0.0017 + t * 0.0044;
|
|
84
|
+
out[2] = 0.0022 + t * 0.0055;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A broad halo, then the disc itself. The halo is what makes the sky around the moon read as
|
|
88
|
+
// sky with a moon in it rather than as a sticker on a gradient.
|
|
89
|
+
const halo = Math.exp(-angle * 5.5) * 0.05;
|
|
90
|
+
|
|
91
|
+
out[0] += halo * 0.75;
|
|
92
|
+
out[1] += halo * 0.82;
|
|
93
|
+
out[2] += halo;
|
|
94
|
+
|
|
95
|
+
if (angle < MOON_RADIUS) {
|
|
96
|
+
// Soft-edged, so the disc survives the convolution into the roughness mips with a shape
|
|
97
|
+
// rather than as ringing.
|
|
98
|
+
const disc = 1 - Math.pow(angle / MOON_RADIUS, 3);
|
|
99
|
+
|
|
100
|
+
out[0] += disc * 5.2;
|
|
101
|
+
out[1] += disc * 5.6;
|
|
102
|
+
out[2] += disc * 6.4;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The environment map. Assign it to `scene.lights.environment`.
|
|
108
|
+
*
|
|
109
|
+
* @returns {ShadeTexture}
|
|
110
|
+
*/
|
|
111
|
+
export function moonlit_environment() {
|
|
112
|
+
const sampler = Sampler2D.float32(4, RESOLUTION, RESOLUTION);
|
|
113
|
+
|
|
114
|
+
const data = sampler.data;
|
|
115
|
+
const rgb = [0, 0, 0];
|
|
116
|
+
|
|
117
|
+
for (let v = 0; v < RESOLUTION; v++) {
|
|
118
|
+
for (let u = 0; u < RESOLUTION; u++) {
|
|
119
|
+
// texel centres, so the mapping is symmetric across the octahedron's folds
|
|
120
|
+
octahedral_uv_to_direction(
|
|
121
|
+
SCRATCH_DIRECTION,
|
|
122
|
+
(u + 0.5) / RESOLUTION,
|
|
123
|
+
(v + 0.5) / RESOLUTION
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
moonlit_radiance(rgb, SCRATCH_DIRECTION.x, SCRATCH_DIRECTION.y, SCRATCH_DIRECTION.z);
|
|
127
|
+
|
|
128
|
+
const offset = (v * RESOLUTION + u) * 4;
|
|
129
|
+
|
|
130
|
+
data[offset] = rgb[0];
|
|
131
|
+
data[offset + 1] = rgb[1];
|
|
132
|
+
data[offset + 2] = rgb[2];
|
|
133
|
+
data[offset + 3] = 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const image = ShadeImage.fromSampler2D(sampler2d_to_f16(sampler));
|
|
138
|
+
|
|
139
|
+
// radiance, not colour: the values above are light arriving, and treating them as sRGB would
|
|
140
|
+
// put a transfer curve through numbers that never had one
|
|
141
|
+
image.color_space = ColorSpace.LinearSRGB;
|
|
142
|
+
|
|
143
|
+
return ShadeTexture.from(image);
|
|
144
|
+
}
|
|
@@ -78,10 +78,11 @@ edits it at both altitudes.
|
|
|
78
78
|
- Two of them are **ramps**, edited as a picture. An **RGBA Gradient** (and an RGB one) is a strip of
|
|
79
79
|
the colour it produces with its stops underneath: drag a stop, click the lane to add one — in the
|
|
80
80
|
colour the gradient already is there, so adding never changes anything — and middle-click to remove.
|
|
81
|
-
A **Curve**
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
A **Curve** plots normalized time against unrestricted values, with a Y axis that fits the whole
|
|
82
|
+
curve, including peaks between keys. Its fields edit the selected key's time, value and tangents.
|
|
83
|
+
The scale stays fixed during a drag and refits on release. Neither ramp is sampled from a table
|
|
84
|
+
at runtime: both are written out into the program as arithmetic, which is what the **Code** tab
|
|
85
|
+
shows. `Curve Table`, which *is* the table, is the other node — it takes a row handle rather than a curve.
|
|
85
86
|
- The strip along the bottom lists every problem with the effect, from **both** phases, loudest
|
|
86
87
|
first. Click one to jump to the phase it is in and select the node; the node is outlined in the
|
|
87
88
|
graph too.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One key that records a GPU profile capture and downloads it.
|
|
3
|
+
*
|
|
4
|
+
* `T` starts a recording, `T` again stops it and saves an `.sgpt` next to the browser's other
|
|
5
|
+
* downloads. That is the whole interaction, and it is what every playground page wants: a capture is
|
|
6
|
+
* something you take *while looking at* the thing you are suspicious of, so reaching for a panel is
|
|
7
|
+
* already too slow.
|
|
8
|
+
*
|
|
9
|
+
* **The engine never constructs a `GPUProfileSession`.** `Renderer.profile_session` is a nullable
|
|
10
|
+
* field and the null checks around it are the entire integration, which is what keeps the profiler
|
|
11
|
+
* out of a bundle that does not ask for it. Importing this module is the opt-in.
|
|
12
|
+
*
|
|
13
|
+
* `volumetrics_froxel/main.js` has its own copy of this, written first; it is the page this was
|
|
14
|
+
* lifted from and it can adopt this when somebody has the assets to verify it against.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {object} ProfileHotkeyHandle
|
|
18
|
+
* @property {function(): boolean} is_recording
|
|
19
|
+
* @property {function(): Promise<void>} toggle start a recording, or stop and save the one running
|
|
20
|
+
* @property {function(): void} dispose remove the key listener
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Wire the key up.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} params
|
|
26
|
+
* @param {import("../renderer/Renderer.js").Renderer} params.renderer initialized — this reads its
|
|
27
|
+
* device for the capture's metadata
|
|
28
|
+
* @param {string} params.name goes in the filename and in the capture's note
|
|
29
|
+
* @param {HTMLElement|null} [params.status] written with what the recording is doing; the element
|
|
30
|
+
* gets the class `recording` while one is running, and nothing otherwise
|
|
31
|
+
* @param {function(): string[]} [params.settings] what the page was set to when the capture was
|
|
32
|
+
* taken. A pass timing says nothing until you know that
|
|
33
|
+
* @param {number} [params.level] see {@link GPUProfileLevel}; `WORKLOAD` is the most detailed
|
|
34
|
+
* @param {string} [params.key] the letter to bind, lowercase
|
|
35
|
+
* @param {EventTarget} [params.target] where the listener goes
|
|
36
|
+
* @returns {ProfileHotkeyHandle}
|
|
37
|
+
*/
|
|
38
|
+
export function install_profile_hotkey({ renderer, name, status, settings, level, key, target, }: {
|
|
39
|
+
renderer: import("../renderer/Renderer.js").Renderer;
|
|
40
|
+
name: string;
|
|
41
|
+
status?: HTMLElement | null;
|
|
42
|
+
settings?: () => string[];
|
|
43
|
+
level?: number;
|
|
44
|
+
key?: string;
|
|
45
|
+
target?: EventTarget;
|
|
46
|
+
}): ProfileHotkeyHandle;
|
|
47
|
+
export type ProfileHotkeyHandle = {
|
|
48
|
+
is_recording: () => boolean;
|
|
49
|
+
/**
|
|
50
|
+
* start a recording, or stop and save the one running
|
|
51
|
+
*/
|
|
52
|
+
toggle: () => Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* remove the key listener
|
|
55
|
+
*/
|
|
56
|
+
dispose: () => void;
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=profile_hotkey.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profile_hotkey.d.ts","sourceRoot":"","sources":["../../../../src/shade/playground/profile_hotkey.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;GAcG;AAEH;;;;;GAKG;AAEH;;;;;;;;;;;;;;;GAeG;AACH;IAZ8D,QAAQ,EAA3D,OAAO,yBAAyB,EAAE,QAAQ;IAE3B,IAAI,EAAnB,MAAM;IACoB,MAAM,GAAhC,WAAW,GAAC,IAAI;IAEc,QAAQ,SAA1B,MAAM,EAAE;IAEJ,KAAK,GAArB,MAAM;IACU,GAAG,GAAnB,MAAM;IACe,MAAM,GAA3B,WAAW;IACT,mBAAmB,CAwO/B;;wBA3PyB,OAAO;;;;kBACP,QAAQ,IAAI,CAAC;;;;mBACb,IAAI"}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import package_json from "../../../package.json";
|
|
2
|
+
import { GPUProfileLevel } from "../device/timing/profile/GPUProfileLevel.js";
|
|
3
|
+
import { GPUProfileSession } from "../device/timing/profile/GPUProfileSession.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One key that records a GPU profile capture and downloads it.
|
|
7
|
+
*
|
|
8
|
+
* `T` starts a recording, `T` again stops it and saves an `.sgpt` next to the browser's other
|
|
9
|
+
* downloads. That is the whole interaction, and it is what every playground page wants: a capture is
|
|
10
|
+
* something you take *while looking at* the thing you are suspicious of, so reaching for a panel is
|
|
11
|
+
* already too slow.
|
|
12
|
+
*
|
|
13
|
+
* **The engine never constructs a `GPUProfileSession`.** `Renderer.profile_session` is a nullable
|
|
14
|
+
* field and the null checks around it are the entire integration, which is what keeps the profiler
|
|
15
|
+
* out of a bundle that does not ask for it. Importing this module is the opt-in.
|
|
16
|
+
*
|
|
17
|
+
* `volumetrics_froxel/main.js` has its own copy of this, written first; it is the page this was
|
|
18
|
+
* lifted from and it can adopt this when somebody has the assets to verify it against.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {object} ProfileHotkeyHandle
|
|
23
|
+
* @property {function(): boolean} is_recording
|
|
24
|
+
* @property {function(): Promise<void>} toggle start a recording, or stop and save the one running
|
|
25
|
+
* @property {function(): void} dispose remove the key listener
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Wire the key up.
|
|
30
|
+
*
|
|
31
|
+
* @param {object} params
|
|
32
|
+
* @param {import("../renderer/Renderer.js").Renderer} params.renderer initialized — this reads its
|
|
33
|
+
* device for the capture's metadata
|
|
34
|
+
* @param {string} params.name goes in the filename and in the capture's note
|
|
35
|
+
* @param {HTMLElement|null} [params.status] written with what the recording is doing; the element
|
|
36
|
+
* gets the class `recording` while one is running, and nothing otherwise
|
|
37
|
+
* @param {function(): string[]} [params.settings] what the page was set to when the capture was
|
|
38
|
+
* taken. A pass timing says nothing until you know that
|
|
39
|
+
* @param {number} [params.level] see {@link GPUProfileLevel}; `WORKLOAD` is the most detailed
|
|
40
|
+
* @param {string} [params.key] the letter to bind, lowercase
|
|
41
|
+
* @param {EventTarget} [params.target] where the listener goes
|
|
42
|
+
* @returns {ProfileHotkeyHandle}
|
|
43
|
+
*/
|
|
44
|
+
export function install_profile_hotkey({
|
|
45
|
+
renderer,
|
|
46
|
+
name,
|
|
47
|
+
status = null,
|
|
48
|
+
settings = () => [],
|
|
49
|
+
level = GPUProfileLevel.WORKLOAD,
|
|
50
|
+
key = "t",
|
|
51
|
+
target = window,
|
|
52
|
+
}) {
|
|
53
|
+
/**
|
|
54
|
+
* The recording in progress, or null.
|
|
55
|
+
* @type {GPUProfileSession|null}
|
|
56
|
+
*/
|
|
57
|
+
let session = null;
|
|
58
|
+
|
|
59
|
+
/** Guards against the key being leaned on while a stop is still draining. */
|
|
60
|
+
let busy = false;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {string} text
|
|
64
|
+
* @param {boolean} [recording]
|
|
65
|
+
*/
|
|
66
|
+
function set_status(text, recording = false) {
|
|
67
|
+
if (status === null) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
status.textContent = text;
|
|
72
|
+
status.className = recording ? "recording" : "";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function start() {
|
|
76
|
+
const device = renderer.device;
|
|
77
|
+
|
|
78
|
+
const recording = new GPUProfileSession({
|
|
79
|
+
level,
|
|
80
|
+
note: `${name} — ${[...settings(), `${renderer.pixel_ratio.toFixed(2)}x`].join(" · ")}`,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/*
|
|
84
|
+
Populated before start(): the meta record is written into the stream there and not read
|
|
85
|
+
again. Without it the capture is unreadable in the sense that matters — a pass taking 0.4 ms
|
|
86
|
+
says nothing until you know which GPU and at what resolution.
|
|
87
|
+
*/
|
|
88
|
+
const meta = recording.meta;
|
|
89
|
+
|
|
90
|
+
// `GPUDevice.adapterInfo` is recent enough that a browser running this page may not have it,
|
|
91
|
+
// and several browsers withhold the fields even when they do. Empty is what the format
|
|
92
|
+
// expects in that case.
|
|
93
|
+
const info = device.adapterInfo;
|
|
94
|
+
|
|
95
|
+
if (info !== undefined && info !== null) {
|
|
96
|
+
meta.adapter_vendor = info.vendor ?? "";
|
|
97
|
+
meta.adapter_architecture = info.architecture ?? "";
|
|
98
|
+
meta.adapter_device = info.device ?? "";
|
|
99
|
+
meta.adapter_description = info.description ?? "";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
meta.features = [...device.features];
|
|
103
|
+
meta.engine_version = package_json.version;
|
|
104
|
+
|
|
105
|
+
recording.onBytesWritten.add(bytes => {
|
|
106
|
+
/*
|
|
107
|
+
Only while this is still the live recording. Frames keep landing all through the drain
|
|
108
|
+
in `stop`, which is the whole point of draining — and without this check each one would
|
|
109
|
+
overwrite "stopping…" with "● recording · T to stop", so a capture that is on its way to
|
|
110
|
+
disk reads as one that is still running.
|
|
111
|
+
*/
|
|
112
|
+
if (session !== recording) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
set_status(
|
|
117
|
+
`● recording — ${recording.frames_recorded} frames, ${format_bytes(bytes)}`
|
|
118
|
+
+ ` · ${key.toUpperCase()} to stop`,
|
|
119
|
+
true
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
renderer.profile_session = recording;
|
|
124
|
+
recording.start();
|
|
125
|
+
|
|
126
|
+
session = recording;
|
|
127
|
+
|
|
128
|
+
// The timers go quiet without this rather than failing, so the capture still carries the
|
|
129
|
+
// frame graph and the workload — it just has no durations in it. Worth knowing before you
|
|
130
|
+
// record for a minute and then look.
|
|
131
|
+
const timed = device.features.has("timestamp-query");
|
|
132
|
+
|
|
133
|
+
set_status(
|
|
134
|
+
timed
|
|
135
|
+
? `● recording — 0 frames · ${key.toUpperCase()} to stop`
|
|
136
|
+
: "● recording without timestamp-query — structure only, no durations"
|
|
137
|
+
+ ` · ${key.toUpperCase()} to stop`,
|
|
138
|
+
true
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Wait for frames already submitted to finish landing in the session.
|
|
144
|
+
*
|
|
145
|
+
* A frame is committed when its timestamp readback resolves — a `mapAsync` a frame or two behind
|
|
146
|
+
* submit, see `ShadeGPUCommandContext.profiling_absorbed` — and `record_frame` silently ignores
|
|
147
|
+
* anything arriving after `stop()`. Stopping the instant the key goes down therefore discards
|
|
148
|
+
* the last few frames, which are the ones you were most likely looking at.
|
|
149
|
+
*
|
|
150
|
+
* Drains the queue, then waits for the frame count to stop moving. Capped, because a recording
|
|
151
|
+
* taken while the page is paused never moves at all.
|
|
152
|
+
*
|
|
153
|
+
* @param {GPUProfileSession} recording
|
|
154
|
+
* @returns {Promise<void>}
|
|
155
|
+
*/
|
|
156
|
+
async function drain(recording) {
|
|
157
|
+
const DEADLINE_MS = 1000;
|
|
158
|
+
const QUIET_MS = 60;
|
|
159
|
+
|
|
160
|
+
await renderer.device.queue.onSubmittedWorkDone();
|
|
161
|
+
|
|
162
|
+
const deadline = performance.now() + DEADLINE_MS;
|
|
163
|
+
|
|
164
|
+
let previous = recording.frames_recorded;
|
|
165
|
+
|
|
166
|
+
while (performance.now() < deadline) {
|
|
167
|
+
await new Promise(resolve => setTimeout(resolve, QUIET_MS));
|
|
168
|
+
|
|
169
|
+
if (recording.frames_recorded === previous) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
previous = recording.frames_recorded;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function stop() {
|
|
178
|
+
const recording = session;
|
|
179
|
+
|
|
180
|
+
session = null;
|
|
181
|
+
|
|
182
|
+
// Detached first, so nothing encoded during the drain below joins the recording. Frames
|
|
183
|
+
// already encoded are unaffected: Renderer captures the session into a local before it
|
|
184
|
+
// subscribes to the readback.
|
|
185
|
+
renderer.profile_session = null;
|
|
186
|
+
|
|
187
|
+
set_status("stopping — draining in-flight frames…");
|
|
188
|
+
|
|
189
|
+
await drain(recording);
|
|
190
|
+
|
|
191
|
+
const bytes = recording.stop();
|
|
192
|
+
const filename = `${name}-${file_timestamp()}.sgpt`;
|
|
193
|
+
|
|
194
|
+
download_bytes(bytes, filename);
|
|
195
|
+
|
|
196
|
+
const topologies = `${recording.topology_count} topolog${recording.topology_count === 1 ? "y" : "ies"}`;
|
|
197
|
+
|
|
198
|
+
// Also to the console, because the status line is the only other place the name appears and
|
|
199
|
+
// a browser that declines the save says nothing about which capture it declined.
|
|
200
|
+
console.log(
|
|
201
|
+
`profile: ${recording.frames_recorded} frames, ${format_bytes(bytes.byteLength)}, `
|
|
202
|
+
+ `${topologies} -> ${filename}`
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
set_status(
|
|
206
|
+
`${recording.frames_recorded} frames, ${format_bytes(bytes.byteLength)}, ${topologies}`
|
|
207
|
+
+ ` · ${filename}`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* @returns {Promise<void>}
|
|
213
|
+
*/
|
|
214
|
+
function toggle() {
|
|
215
|
+
if (busy) {
|
|
216
|
+
return Promise.resolve();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
busy = true;
|
|
220
|
+
|
|
221
|
+
const done = session === null ? Promise.resolve(start()) : stop();
|
|
222
|
+
|
|
223
|
+
return done.catch(error => {
|
|
224
|
+
set_status(`profiling failed: ${error?.message ?? error}`);
|
|
225
|
+
console.error(error);
|
|
226
|
+
|
|
227
|
+
session = null;
|
|
228
|
+
renderer.profile_session = null;
|
|
229
|
+
}).finally(() => {
|
|
230
|
+
busy = false;
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @param {KeyboardEvent} event
|
|
236
|
+
*/
|
|
237
|
+
function on_keydown(event) {
|
|
238
|
+
if (event.ctrlKey || event.metaKey || event.altKey || event.repeat) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/*
|
|
243
|
+
`key` first, because it is the letter actually printed on the key the user pressed — `code`
|
|
244
|
+
is physical position, so on AZERTY it names the key where T sits on QWERTY. `code` is the
|
|
245
|
+
fallback for layouts whose `key` is not Latin at all (Cyrillic gives "е" here).
|
|
246
|
+
*/
|
|
247
|
+
if (event.key.toLowerCase() !== key && event.code !== `Key${key.toUpperCase()}`) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// a panel's fields and buttons are focusable; do not steal a key from one
|
|
252
|
+
const focused = event.target;
|
|
253
|
+
|
|
254
|
+
if (
|
|
255
|
+
focused instanceof HTMLInputElement
|
|
256
|
+
|| focused instanceof HTMLTextAreaElement
|
|
257
|
+
|| focused instanceof HTMLSelectElement
|
|
258
|
+
) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
event.preventDefault();
|
|
263
|
+
|
|
264
|
+
toggle();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
target.addEventListener("keydown", on_keydown);
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
is_recording: () => session !== null,
|
|
271
|
+
toggle,
|
|
272
|
+
dispose: () => target.removeEventListener("keydown", on_keydown),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* @param {Uint8Array} bytes
|
|
278
|
+
* @param {string} filename
|
|
279
|
+
*/
|
|
280
|
+
function download_bytes(bytes, filename) {
|
|
281
|
+
const url = URL.createObjectURL(new Blob([bytes], { type: "application/octet-stream" }));
|
|
282
|
+
|
|
283
|
+
const anchor = document.createElement("a");
|
|
284
|
+
|
|
285
|
+
anchor.href = url;
|
|
286
|
+
anchor.download = filename;
|
|
287
|
+
|
|
288
|
+
document.body.appendChild(anchor);
|
|
289
|
+
anchor.click();
|
|
290
|
+
anchor.remove();
|
|
291
|
+
|
|
292
|
+
// Revoking in the same task cancels the download in some browsers — the click only schedules it.
|
|
293
|
+
setTimeout(() => URL.revokeObjectURL(url), 60000);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* `YYYYMMDD-HHMMSS` in local time, matching the naming the other `.sgpt` captures already use — same
|
|
298
|
+
* sort order, and it reads as the clock you took it by rather than as UTC.
|
|
299
|
+
*
|
|
300
|
+
* @returns {string}
|
|
301
|
+
*/
|
|
302
|
+
function file_timestamp() {
|
|
303
|
+
const now = new Date();
|
|
304
|
+
|
|
305
|
+
const pad = value => String(value).padStart(2, "0");
|
|
306
|
+
|
|
307
|
+
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`
|
|
308
|
+
+ `-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* @param {number} bytes
|
|
313
|
+
* @returns {string}
|
|
314
|
+
*/
|
|
315
|
+
function format_bytes(bytes) {
|
|
316
|
+
if (bytes < 1024) {
|
|
317
|
+
return `${bytes} B`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (bytes < 1024 * 1024) {
|
|
321
|
+
return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
|
|
325
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# SSR variance playground
|
|
2
|
+
|
|
3
|
+
Run `npm run dev` from the workspace, then open
|
|
4
|
+
<http://localhost:5173/src/shade/playground/ssr_variance/>.
|
|
5
|
+
|
|
6
|
+
The scene contains exactly two quads: a metallic ground plane at y=0 with roughness
|
|
7
|
+
0.30, and a vertical card facing the camera. The card carries an emissive cross
|
|
8
|
+
texture with two coloured details. The pattern does not add geometry or normal
|
|
9
|
+
edges. A constant dim environment supplies the SSR fallback.
|
|
10
|
+
|
|
11
|
+
The camera, scene, exposure (compensation 1), and 800×500 resolution are fixed.
|
|
12
|
+
Automatic exposure, SSAO, bloom, sharpening, and motion blur are disabled.
|
|
13
|
+
Main TAA defaults off and can be enabled independently.
|
|
14
|
+
Raster jitter defaults off separately: the production renderer still applies its
|
|
15
|
+
jitter sequence with TAA off, so a playground-only `FrameStart` extension freezes
|
|
16
|
+
the view's projection and jitter uniforms before rasterization. The jitter control
|
|
17
|
+
can restore production behaviour. SSR's stochastic frame index still advances.
|
|
18
|
+
|
|
19
|
+
## Exact capture
|
|
20
|
+
|
|
21
|
+
The page starts at frame **0** without submitting a frame or scheduling an animation
|
|
22
|
+
loop. **Capture frames 100–115** submits exactly 115 frames, each with dt=1/60, and
|
|
23
|
+
records frame 100 and every subsequent frame through 115. GPU completion and readback
|
|
24
|
+
are awaited between captures. Readback does not render extra frames. The renderer's
|
|
25
|
+
own count is checked against the experiment count; its zero-based SSR indices are
|
|
26
|
+
99–114 for these captures.
|
|
27
|
+
|
|
28
|
+
From the console:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
const lab = await ssr_variance_ready;
|
|
32
|
+
const measurements = await lab.run(); // requires frame zero; finishes at frame 115
|
|
33
|
+
lab.frame_count; // 115, stays there indefinitely
|
|
34
|
+
lab.frames; // 16 PNG byte arrays and linear float32 stage arrays
|
|
35
|
+
lab.files; // Map<filename, Uint8Array | Float32Array>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
For manual control on a fresh page:
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
await ssr_variance_ready;
|
|
42
|
+
await ssr_variance_lab.step(99);
|
|
43
|
+
await ssr_variance_lab.step(1, true); // submit frame 100 and capture it
|
|
44
|
+
await ssr_variance_lab.step(15, true); // capture every frame from 101 through 115
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`step(n, true)` records each of its n frames. The standard `run()` additionally
|
|
48
|
+
calculates statistics and fills the page's results. Operations are serialized;
|
|
49
|
+
overlapping calls are rejected. Restarting or changing a control reloads the page,
|
|
50
|
+
giving every comparison a fresh renderer and the same frame-index sequence.
|
|
51
|
+
Appending `?run` runs the standard capture automatically. `spatial=off`,
|
|
52
|
+
`temporal=off`, `jitter=on`, and `taa=on` select controls in the query string.
|
|
53
|
+
Use `?run&jitter=on&taa=on` to measure the full production jitter/TAA path.
|
|
54
|
+
|
|
55
|
+
## Convergence
|
|
56
|
+
|
|
57
|
+
**Measure convergence**, `?convergence`, or `await lab.convergence()` on a fresh page
|
|
58
|
+
records frames 4, 8, 16, 32, 64, 100, 128, 256, and 512. It then averages the
|
|
59
|
+
pre-temporal input from frames 513–1536 into a separate float32 GPU texture. This
|
|
60
|
+
reference is never fed into the renderer. The table compares each checkpoint's
|
|
61
|
+
temporal and post-spatial output with that 1,024-sample mean, using linear luminance
|
|
62
|
+
RMSE and signed bias over the same reflection rectangle.
|
|
63
|
+
|
|
64
|
+
This catches a darkened or frozen output that scores well on temporal variance.
|
|
65
|
+
The reference estimates the prefilter's expected output; it is not ray-traced
|
|
66
|
+
ground truth and does not validate SSR visibility, ray reuse, or the prefilter's
|
|
67
|
+
own bias. Finite reference noise also sets an error floor. This pixel-aligned
|
|
68
|
+
comparison requires raster jitter off; the playground rejects it otherwise before
|
|
69
|
+
submitting any frames. Production history is bounded at 128 frames to
|
|
70
|
+
retain responsiveness, so convergence approaches a noise floor rather than an
|
|
71
|
+
unbounded average. `convergence.json`, `reference.f32`, checkpoint PNGs, and stage
|
|
72
|
+
arrays are available in `lab.files` after the run stops at frame 1536.
|
|
73
|
+
|
|
74
|
+
## Outputs and measurement
|
|
75
|
+
|
|
76
|
+
The playground copies resolve, spatial step 1, temporal reprojection, spatial step 2,
|
|
77
|
+
spatial step 4, pre-TAA scene colour, and final linear scene colour after TAA using
|
|
78
|
+
integer `textureLoad` calls before presentation.
|
|
79
|
+
Copies run only on recorded frames. Disabling spatial filtering makes those stage
|
|
80
|
+
captures refer to their bypass inputs. Resolve still performs its own ray reuse
|
|
81
|
+
and roughness-dependent source mip sampling.
|
|
82
|
+
|
|
83
|
+
Each displayed image is the actual canvas output copied immediately after render,
|
|
84
|
+
before the browser can discard it. Select a thumbnail to download its PNG. The JSON
|
|
85
|
+
download includes frame indices, controls, camera settings, and statistics. Full raw
|
|
86
|
+
captures and per-pixel mean/variance maps are available as typed arrays in `lab.files`.
|
|
87
|
+
An external driver can write each array directly, for example as a `fetch` request
|
|
88
|
+
body. Captures stay in CPU memory without duplicating HDR arrays into Blob storage.
|
|
89
|
+
RGBA captures are little-endian float32, row-major, top-left origin. SSR radiance
|
|
90
|
+
alpha carries the shader's internal variance estimate; scene alpha is opacity.
|
|
91
|
+
The mask uses 1 for upward-facing ground pixels and 0 elsewhere.
|
|
92
|
+
|
|
93
|
+
Temporal variance is computed **per pixel** over all 16 linear Rec.709 luminance
|
|
94
|
+
samples (0.2126 R + 0.7152 G + 0.0722 B), then averaged spatially. Population variance
|
|
95
|
+
divides by 16; sample variance divides by 15. RMS temporal sigma is the square root
|
|
96
|
+
of mean population variance. This is distinct from image contrast or the shader's
|
|
97
|
+
alpha-channel estimate. Non-finite readback values fail the measurement.
|
|
98
|
+
|
|
99
|
+
The JSON reports full-image, whole-ground, and reflection-region statistics. The
|
|
100
|
+
reflection region is the fixed rectangle x=200…599, y=250…459 intersected with the
|
|
101
|
+
frame-100 ground mask. It covers the main visible reflection, not its full rough
|
|
102
|
+
tail. The heatmap uses a fixed 0…0.10 linear-luminance sigma scale. With raster jitter
|
|
103
|
+
enabled, silhouette coverage can vary; the fixed-projection default avoids that
|
|
104
|
+
confound. A lower variance alone does not prove better image quality: blurring can
|
|
105
|
+
reduce variance while destroying reflected detail.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** TAA-off currently leaves raster jitter active. Freeze only this lab's view before rasterization. */
|
|
2
|
+
export class FixedProjection extends RenderExtension {
|
|
3
|
+
record(frame: any): void;
|
|
4
|
+
}
|
|
5
|
+
/** Exact textureLoad copies of production SSR stages, only on requested frames. */
|
|
6
|
+
export class CaptureSSR extends RenderExtension {
|
|
7
|
+
constructor(device: any);
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
textures: Map<any, any>;
|
|
10
|
+
device: any;
|
|
11
|
+
record(frame: any): void;
|
|
12
|
+
jitter: any[];
|
|
13
|
+
read(): Promise<{}>;
|
|
14
|
+
}
|
|
15
|
+
import { RenderExtension } from "../../renderer/extension/RenderExtension.js";
|
|
16
|
+
//# sourceMappingURL=capture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/ssr_variance/capture.js"],"names":[],"mappings":"AA6BA,uGAAuG;AACvG;IAGI,yBAKC;CACJ;AAED,mFAAmF;AACnF;IAKI,yBAAsD;IAFtD,iBAAgB;IAChB,wBAAqB;IACU,YAAoB;IACnD,yBAuCC;IArCG,cAA2C;IAsC/C,oBAMC;CACJ;gCA/F+B,6CAA6C"}
|