@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,96 @@
|
|
|
1
|
+
import { RenderExtension } from "../../renderer/extension/RenderExtension.js";
|
|
2
|
+
import { FramePhase } from "../../renderer/extension/FramePhase.js";
|
|
3
|
+
import { SceneColor } from "../../renderer/extension/SceneColor.js";
|
|
4
|
+
import { ImageShader } from "../../renderer/shader/ImageShader.js";
|
|
5
|
+
import { CodeChunk } from "../../renderer/shader/compiler/CodeChunk.js";
|
|
6
|
+
import { chunk_decode_g_buffer_normal } from "../../renderer/shader/chunk/geometry/normal/chunk_decode_g_buffer_normal.js";
|
|
7
|
+
import { graph_image_pass } from "../../renderer/shader/graph/graph_image_pass.js";
|
|
8
|
+
import { graph_import_texture } from "../../renderer/shader/graph/graph_import_texture.js";
|
|
9
|
+
import { ShaderResourceSetDescriptor } from "../../renderer/shader/resource/ShaderResourceSetDescriptor.js";
|
|
10
|
+
import { ShaderDescriptor } from "../../renderer/shader/ShaderDescriptor.js";
|
|
11
|
+
import { GPUTextureContext } from "../../renderer/texture/GPUTextureContext.js";
|
|
12
|
+
import { download_texture_data } from "../../renderer/texture/download_texture_data.js";
|
|
13
|
+
|
|
14
|
+
const resources = new ShaderResourceSetDescriptor();
|
|
15
|
+
resources.createGroup().addTexture('tSource', 'unfilterable-float').addTexture('tNormal', 'uint');
|
|
16
|
+
const shader = new ImageShader({
|
|
17
|
+
descriptor: ShaderDescriptor.from({ label: 'SSR variance / capture', resources,
|
|
18
|
+
body: CodeChunk.from(/* language=WGSL */ `
|
|
19
|
+
struct Capture { @location(0) radiance: vec4f, @location(1) mask: vec4f }
|
|
20
|
+
@fragment fn main(@builtin(position) coord: vec4f) -> Capture {
|
|
21
|
+
let p = vec2i(coord.xy);
|
|
22
|
+
var output: Capture;
|
|
23
|
+
output.radiance = textureLoad(tSource, p, 0);
|
|
24
|
+
output.mask = vec4f(select(0.0, 1.0, decode_g_buffer_normal(textureLoad(tNormal, p, 0).xy).y > 0.99));
|
|
25
|
+
return output;
|
|
26
|
+
}`, [chunk_decode_g_buffer_normal]) }),
|
|
27
|
+
targets: [{ format: 'rgba32float' }, { format: 'rgba32float' }],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/** TAA-off currently leaves raster jitter active. Freeze only this lab's view before rasterization. */
|
|
31
|
+
export class FixedProjection extends RenderExtension {
|
|
32
|
+
name = 'SSR variance / fixed projection';
|
|
33
|
+
phase = FramePhase.FrameStart;
|
|
34
|
+
record(frame) {
|
|
35
|
+
frame.view.setJitter(0, 0);
|
|
36
|
+
frame.view.camera.setViewportOffset(0, 0);
|
|
37
|
+
frame.view.camera.update();
|
|
38
|
+
frame.view.update_uniforms();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Exact textureLoad copies of production SSR stages, only on requested frames. */
|
|
43
|
+
export class CaptureSSR extends RenderExtension {
|
|
44
|
+
name = 'SSR variance / capture';
|
|
45
|
+
phase = FramePhase.BeforePresent;
|
|
46
|
+
enabled = false;
|
|
47
|
+
textures = new Map();
|
|
48
|
+
constructor(device) { super(); this.device = device; }
|
|
49
|
+
record(frame) {
|
|
50
|
+
if (!this.enabled) { return; }
|
|
51
|
+
this.jitter = Array.from(frame.view.jitter);
|
|
52
|
+
const passes = frame.graph.__pass_nodes;
|
|
53
|
+
const resolve = passes.find(p => p.name === 'SSR resolve / IBL');
|
|
54
|
+
const spatial1 = passes.find(p => p.name === 'SSR / spatial denoise with metadata');
|
|
55
|
+
const temporal = passes.find(p => p.name === 'SSR Reproject');
|
|
56
|
+
const spatial = passes.filter(p => p.name === 'SSR / spatial denoise');
|
|
57
|
+
const taa = passes.find(p => p.name === 'TAA');
|
|
58
|
+
if (!resolve || !temporal) { throw new Error('SSR stages missing from the frame graph'); }
|
|
59
|
+
const resolved = resolve.data['@output'][0];
|
|
60
|
+
const reprojected = temporal.data['@output'][0];
|
|
61
|
+
const stages = {
|
|
62
|
+
resolve: resolved,
|
|
63
|
+
spatial1: spatial1?.data['@output'][0] ?? resolved,
|
|
64
|
+
temporal: reprojected,
|
|
65
|
+
spatial2: spatial[0]?.data['@output'][0] ?? reprojected,
|
|
66
|
+
spatial4: spatial[1]?.data['@output'][0] ?? reprojected,
|
|
67
|
+
'pre-taa': taa?.data.ColourTexture ?? frame.get(SceneColor).color,
|
|
68
|
+
scene: frame.get(SceneColor).color,
|
|
69
|
+
};
|
|
70
|
+
const [width, height] = frame.resolution;
|
|
71
|
+
const texture = name => {
|
|
72
|
+
let t = this.textures.get(name);
|
|
73
|
+
if (!t || t.width !== width || t.height !== height) {
|
|
74
|
+
t?.destroy();
|
|
75
|
+
t = GPUTextureContext.fromTexture(this.device.createTexture({
|
|
76
|
+
label: `SSR variance / ${name}`, size: [width, height, 1], format: 'rgba32float',
|
|
77
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
|
|
78
|
+
}), this.device);
|
|
79
|
+
this.textures.set(name, t);
|
|
80
|
+
}
|
|
81
|
+
return graph_import_texture(frame.graph, t, `SSR variance / ${name}`);
|
|
82
|
+
};
|
|
83
|
+
let mask = texture('mask');
|
|
84
|
+
for (const [name, handle] of Object.entries(stages)) {
|
|
85
|
+
[, mask] = graph_image_pass({ graph: frame.graph, shader,
|
|
86
|
+
inputs: { tSource: handle, tNormal: resolve.data.tNormal }, output: [texture(name), mask] });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async read() {
|
|
90
|
+
const result = {};
|
|
91
|
+
for (const [name, texture] of this.textures) {
|
|
92
|
+
result[name] = await download_texture_data({ device: this.device, source: texture.gpu_texture });
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
3
|
+
<title>SSR variance lab</title><style>
|
|
4
|
+
body{margin:24px;background:#14161a;color:#e7e9ee;font:14px system-ui}h1{font-size:24px;margin-bottom:8px}p{color:#acb5c5;max-width:900px}button,select,a{font:inherit}button,select{padding:8px;background:#29313e;color:inherit;border:1px solid #4b586d;border-radius:4px}button:disabled{opacity:.45}a{color:#9ac9ff}#view{display:block;width:800px;max-width:100%;height:auto;background:#08090a;margin:18px 0}#status{white-space:pre-wrap}table{border-collapse:collapse;font-variant-numeric:tabular-nums}td,th{padding:8px 14px;border-bottom:1px solid #394150;text-align:right}td:first-child,th:first-child{text-align:left}.frames{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;max-width:1200px}.frames img{width:100%}figure{margin:0}figcaption{padding:5px}#heatmap{width:800px;max-width:100%}.error{color:#ff9a9a}
|
|
5
|
+
</style></head><body>
|
|
6
|
+
<h1>SSR variance lab</h1>
|
|
7
|
+
<p>One ground plane · roughness 0.30 · metallic 1 · textured emissive card · fixed camera and exposure. Starts at frame zero. Rendering advances only when requested.</p>
|
|
8
|
+
<label>Spatial <select id="spatial"><option value="on">On</option><option value="off">Off</option></select></label>
|
|
9
|
+
<label>Temporal <select id="temporal"><option value="on">On</option><option value="off">Off</option></select></label>
|
|
10
|
+
<label>Raster jitter <select id="jitter"><option value="off">Off</option><option value="on">On</option></select></label>
|
|
11
|
+
<label>Main TAA <select id="taa"><option value="off">Off</option><option value="on">On</option></select></label>
|
|
12
|
+
<button id="step" disabled>Step one frame</button> <button id="run" disabled>Capture frames 100–115</button>
|
|
13
|
+
<button id="restart">Restart at frame zero</button>
|
|
14
|
+
<button id="converge" disabled>Measure convergence</button>
|
|
15
|
+
<canvas id="view" width="800" height="500"></canvas><div id="status">Initializing…</div>
|
|
16
|
+
<div id="results" hidden>
|
|
17
|
+
<p>Per-pixel temporal luminance variance, measured in linear HDR across 16 samples. Table: fixed reflection rectangle intersected with upward-facing ground pixels. Population variance uses ÷16; the JSON also includes sample variance (÷15), whole-ground and full-image measurements.</p>
|
|
18
|
+
<table><thead><tr><th>Stage</th><th>Mean variance</th><th>RMS σ</th><th>95th percentile σ</th><th>Mean peak-to-peak</th></tr></thead><tbody id="metrics"></tbody></table>
|
|
19
|
+
<p><a id="download">Download measurements JSON</a></p>
|
|
20
|
+
<p>Final scene temporal standard deviation. Fixed scale: black = 0, yellow = 0.10 linear luminance. Red outline = measurement rectangle.</p><canvas id="heatmap" width="800" height="500"></canvas>
|
|
21
|
+
<p>Recorded canvas output. Select a frame to download its full-resolution PNG.</p><div id="frames" class="frames"></div>
|
|
22
|
+
</div><div id="convergence-results" hidden><p>SSR error against a separate float32 average of 1,024 later pre-temporal samples (frames 513–1536). The reference is never fed into the rendered image. This measures convergence to the prefilter's mean, not physical ground truth.</p><table><thead><tr><th>Frame</th><th>Input RMSE</th><th>Temporal RMSE</th><th>After spatial RMSE</th><th>Temporal bias</th></tr></thead><tbody id="convergence-metrics"></tbody></table><p><a id="convergence-download">Download convergence JSON</a></p></div><script type="module" src="./main.js"></script></body></html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/ssr_variance/main.js"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import Vector3 from "../../../core/geom/Vector3.js";
|
|
2
|
+
import { PerspectiveCamera } from "../../renderer/camera/PerspectiveCamera.js";
|
|
3
|
+
import { Renderer } from "../../renderer/Renderer.js";
|
|
4
|
+
import { ShadeIndirectLightingMode } from "../../renderer/ShadeIndirectLightingMode.js";
|
|
5
|
+
import { CaptureSSR, FixedProjection } from "./capture.js";
|
|
6
|
+
import { create_scene } from "./scene.js";
|
|
7
|
+
import { measure_variance } from "./statistics.js";
|
|
8
|
+
import { ReflectionReference, reflection_error } from "./reference.js";
|
|
9
|
+
|
|
10
|
+
const element = id => document.getElementById(id);
|
|
11
|
+
const params = new URLSearchParams(location.search);
|
|
12
|
+
const report = text => { element('status').textContent = text; };
|
|
13
|
+
function png(canvas) {
|
|
14
|
+
const url = canvas.toDataURL('image/png');
|
|
15
|
+
return { url, bytes: Uint8Array.from(atob(url.split(',')[1]), c => c.charCodeAt(0)) };
|
|
16
|
+
}
|
|
17
|
+
function json_download(id, name, value, files) {
|
|
18
|
+
const json = JSON.stringify(value, null, 2);
|
|
19
|
+
files.set(name, new TextEncoder().encode(json));
|
|
20
|
+
element(id).href = `data:application/json;charset=utf-8,${encodeURIComponent(json)}`;
|
|
21
|
+
element(id).download = name;
|
|
22
|
+
}
|
|
23
|
+
function restart(mode) {
|
|
24
|
+
const query = new URLSearchParams(Object.fromEntries(['spatial', 'temporal', 'jitter', 'taa'].map(name => [name, element(name).value])));
|
|
25
|
+
if (mode) { query.set(mode, ''); }
|
|
26
|
+
location.search = query.toString();
|
|
27
|
+
}
|
|
28
|
+
element('restart').onclick = () => restart();
|
|
29
|
+
for (const name of ['spatial', 'temporal', 'jitter', 'taa']) {
|
|
30
|
+
element(name).value = name === 'jitter' || name === 'taa' ? (params.get(name) === 'on' ? 'on' : 'off') : (params.get(name) === 'off' ? 'off' : 'on');
|
|
31
|
+
element(name).onchange = () => restart();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function start() {
|
|
35
|
+
const canvas = element('view');
|
|
36
|
+
const renderer = new Renderer();
|
|
37
|
+
await renderer.initialize({ context: canvas.getContext('webgpu'), pixelRatio: 1 });
|
|
38
|
+
renderer.feature_ssr_enabled = true;
|
|
39
|
+
renderer.indirect_lighting_mode = ShadeIndirectLightingMode.IBL;
|
|
40
|
+
for (const name of ['taa', 'ssao', 'bloom', 'sharpening', 'motion_blur', 'automatic_exposure']) {
|
|
41
|
+
renderer[`feature_${name}_enabled`] = false;
|
|
42
|
+
}
|
|
43
|
+
renderer.exposure_compensation = 1;
|
|
44
|
+
renderer.feature_taa_enabled = element('taa').value === 'on';
|
|
45
|
+
renderer.internal_resolution_scale = 1;
|
|
46
|
+
renderer.ssr.spatial_denoising_enabled = element('spatial').value === 'on';
|
|
47
|
+
renderer.ssr.reprojection_mode = element('temporal').value === 'off' ? 2 : 0;
|
|
48
|
+
const world = await create_scene();
|
|
49
|
+
const camera = new PerspectiveCamera();
|
|
50
|
+
camera.near = 0.1; camera.far = 50; camera.fov = 50 * Math.PI / 180;
|
|
51
|
+
camera.aspect = canvas.width / canvas.height;
|
|
52
|
+
camera.transform.setTranslation(0, 3.2, 7);
|
|
53
|
+
camera.transform.lookAt(new Vector3(0, 0.2, -2));
|
|
54
|
+
camera.transform.updateMatrix();
|
|
55
|
+
renderer.resize(canvas.width, canvas.height);
|
|
56
|
+
const device = renderer.graphics.device;
|
|
57
|
+
let failure, busy = false, frame_count = 0;
|
|
58
|
+
const fail = error => {
|
|
59
|
+
failure = error; report(error.message ?? String(error));
|
|
60
|
+
element('status').classList.add('error'); console.error(error);
|
|
61
|
+
element('step').disabled = element('run').disabled = element('converge').disabled = true;
|
|
62
|
+
};
|
|
63
|
+
device.addEventListener('uncapturederror', event => fail(event.error));
|
|
64
|
+
device.lost.then(info => fail(new Error(`GPU device lost: ${info.message}`)));
|
|
65
|
+
const capture = new CaptureSSR(device);
|
|
66
|
+
const reference = new ReflectionReference(device);
|
|
67
|
+
renderer.add_extension(reference);
|
|
68
|
+
if (element('jitter').value === 'off') { renderer.add_extension(new FixedProjection()); }
|
|
69
|
+
renderer.add_extension(capture);
|
|
70
|
+
const frames = [], files = new Map();
|
|
71
|
+
// A run owns hundreds of MB of HDR captures. Release them when navigating
|
|
72
|
+
// between controls, including when the browser keeps a page in its back cache.
|
|
73
|
+
addEventListener('pagehide', () => {
|
|
74
|
+
frames.length = 0; files.clear();
|
|
75
|
+
renderer.destroy(); device.destroy();
|
|
76
|
+
});
|
|
77
|
+
addEventListener('pageshow', event => { if (event.persisted) { location.reload(); } });
|
|
78
|
+
async function render_frame(record = false) {
|
|
79
|
+
if (failure) { throw failure; }
|
|
80
|
+
capture.enabled = record;
|
|
81
|
+
const rendered = renderer.render(camera, world.scene, 1 / 60);
|
|
82
|
+
if (rendered === false) { throw new Error('Renderer did not submit the requested frame'); }
|
|
83
|
+
frame_count++;
|
|
84
|
+
if (renderer.frame_count !== frame_count) { throw new Error('Renderer frame count diverged from the experiment'); }
|
|
85
|
+
// Snapshot before yielding: the browser may discard the presented canvas after compositing.
|
|
86
|
+
let display;
|
|
87
|
+
if (record) {
|
|
88
|
+
display = document.createElement('canvas'); display.width = canvas.width; display.height = canvas.height;
|
|
89
|
+
display.getContext('2d').drawImage(canvas, 0, 0);
|
|
90
|
+
}
|
|
91
|
+
await device.queue.onSubmittedWorkDone();
|
|
92
|
+
if (failure) { throw failure; }
|
|
93
|
+
if (record) {
|
|
94
|
+
const stages = await capture.read();
|
|
95
|
+
const output = png(display);
|
|
96
|
+
const frame = { frame: frame_count, ssr_frame_index: renderer.ssr.frame_index, jitter: capture.jitter, stages, png: output.bytes, png_url: output.url };
|
|
97
|
+
frames.push(frame);
|
|
98
|
+
files.set(`frame-${frame_count}.png`, output.bytes);
|
|
99
|
+
for (const [name, data] of Object.entries(stages)) {
|
|
100
|
+
// Keep one CPU copy. Hundreds of HDR blobs can exhaust the browser's
|
|
101
|
+
// disk-backed Blob quota before a batch exporter gets to read them.
|
|
102
|
+
files.set(`frame-${frame_count}-${name}.f32`, data);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
capture.enabled = false;
|
|
106
|
+
report(`Frame ${frame_count} · ${frames.length} captures · idle between steps`);
|
|
107
|
+
return frame_count;
|
|
108
|
+
}
|
|
109
|
+
async function exclusive(action) {
|
|
110
|
+
if (busy) { throw new Error('A render operation is already running'); }
|
|
111
|
+
busy = true;
|
|
112
|
+
for (const id of ['step', 'run', 'spatial', 'temporal', 'jitter', 'taa', 'restart', 'converge']) { element(id).disabled = true; }
|
|
113
|
+
try { return await action(); }
|
|
114
|
+
catch (error) { fail(error); throw error; }
|
|
115
|
+
finally {
|
|
116
|
+
busy = false;
|
|
117
|
+
for (const id of ['step', 'run', 'spatial', 'temporal', 'jitter', 'taa', 'restart', 'converge']) { element(id).disabled = false; }
|
|
118
|
+
if (failure) { element('step').disabled = element('run').disabled = element('converge').disabled = true; }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const step = (n = 1, record = false) => exclusive(async () => {
|
|
122
|
+
if (!Number.isSafeInteger(n) || n < 1) { throw new Error('Step count must be a positive integer'); }
|
|
123
|
+
for (let i = 0; i < n; i++) { await render_frame(record); }
|
|
124
|
+
return frame_count;
|
|
125
|
+
});
|
|
126
|
+
async function run() {
|
|
127
|
+
if (frame_count !== 0) { throw new Error('Restart at frame zero before a measurement run'); }
|
|
128
|
+
return exclusive(async () => {
|
|
129
|
+
// No initial render, animation loop, or readback-only render is hidden in these counts.
|
|
130
|
+
for (let i = 1; i <= 115; i++) { await render_frame(i >= 100); }
|
|
131
|
+
const summary = {
|
|
132
|
+
width: canvas.width, height: canvas.height, frames: frames.map(f => f.frame),
|
|
133
|
+
ssr_frame_indices: frames.map(f => f.ssr_frame_index), dt: 1 / 60,
|
|
134
|
+
raster_jitter: frames.map(f => f.jitter),
|
|
135
|
+
automatic_exposure: false, exposure_compensation: 1,
|
|
136
|
+
roughness: 0.3, metallic: 1, spatial: renderer.ssr.spatial_denoising_enabled,
|
|
137
|
+
temporal: renderer.ssr.reprojection_mode !== 2, taa: renderer.feature_taa_enabled,
|
|
138
|
+
camera: { position: [0, 3.2, 7], target: [0, 0.2, -2], fov_degrees: 50 },
|
|
139
|
+
raw_format: 'Little-endian float32 RGBA, row-major, top-left origin. SSR radiance alpha is internal variance; scene alpha is opacity; mask is ground=1 elsewhere=0.',
|
|
140
|
+
variance_definition: 'Per-pixel Rec.709 linear luminance population variance across frames 100–115 (divide by 16). Spatial mean of those variances in each region.',
|
|
141
|
+
stages: {},
|
|
142
|
+
};
|
|
143
|
+
let scene_measurement;
|
|
144
|
+
for (const name of Object.keys(frames[0].stages).filter(n => n !== 'mask')) {
|
|
145
|
+
const measurement = measure_variance(frames.map(f => f.stages[name]), frames[0].stages.mask, canvas.width, canvas.height);
|
|
146
|
+
summary.roi = measurement.roi;
|
|
147
|
+
summary.stages[name] = measurement.summary;
|
|
148
|
+
files.set(`${name}-mean-luminance.f32`, measurement.mean);
|
|
149
|
+
files.set(`${name}-variance.f32`, measurement.variance);
|
|
150
|
+
if (name === 'scene') { scene_measurement = measurement; }
|
|
151
|
+
const r = measurement.summary.reflection;
|
|
152
|
+
const row = document.createElement('tr');
|
|
153
|
+
for (const value of [name, r.mean_population_variance.toExponential(5), r.rms_temporal_sigma.toFixed(6), r.p95_temporal_sigma.toFixed(6), r.mean_peak_to_peak.toFixed(6)]) {
|
|
154
|
+
const cell = document.createElement('td'); cell.textContent = value; row.append(cell);
|
|
155
|
+
}
|
|
156
|
+
element('metrics').append(row);
|
|
157
|
+
}
|
|
158
|
+
const heatmap = element('heatmap'), ctx = heatmap.getContext('2d');
|
|
159
|
+
const image = ctx.createImageData(canvas.width, canvas.height);
|
|
160
|
+
for (let p = 0; p < scene_measurement.variance.length; p++) {
|
|
161
|
+
const v = Math.min(1, Math.sqrt(scene_measurement.variance[p]) / 0.1);
|
|
162
|
+
image.data[p * 4] = 255 * Math.min(1, v * 2);
|
|
163
|
+
image.data[p * 4 + 1] = 255 * v;
|
|
164
|
+
image.data[p * 4 + 2] = 40 * v; image.data[p * 4 + 3] = 255;
|
|
165
|
+
}
|
|
166
|
+
ctx.putImageData(image, 0, 0);
|
|
167
|
+
const roi = summary.roi;
|
|
168
|
+
ctx.strokeStyle = '#ff4444'; ctx.strokeRect(roi.x, roi.y, roi.width, roi.height);
|
|
169
|
+
files.set('scene-temporal-sigma.png', png(heatmap).bytes);
|
|
170
|
+
json_download('download', 'measurements.json', summary, files);
|
|
171
|
+
for (const frame of frames) {
|
|
172
|
+
const figure = document.createElement('figure'), link = document.createElement('a'), img = document.createElement('img'), caption = document.createElement('figcaption');
|
|
173
|
+
img.src = link.href = frame.png_url; img.alt = `Frame ${frame.frame}`;
|
|
174
|
+
link.download = `frame-${frame.frame}.png`; link.append(img); caption.textContent = `Frame ${frame.frame}`;
|
|
175
|
+
figure.append(link, caption); element('frames').append(figure);
|
|
176
|
+
}
|
|
177
|
+
element('results').hidden = false;
|
|
178
|
+
lab.summary = summary;
|
|
179
|
+
report(`Complete · exactly 115 submitted frames · 16 captures (100–115) · renderer stopped`);
|
|
180
|
+
return summary;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
async function convergence() {
|
|
184
|
+
if (frame_count !== 0) { throw new Error('Restart at frame zero before measuring convergence'); }
|
|
185
|
+
if (element('jitter').value !== 'off') {
|
|
186
|
+
const message = 'Convergence requires raster jitter off so checkpoints and reference use the same pixel grid.';
|
|
187
|
+
report(message);
|
|
188
|
+
throw new Error(message);
|
|
189
|
+
}
|
|
190
|
+
return exclusive(async () => {
|
|
191
|
+
const checkpoints = [4, 8, 16, 32, 64, 100, 128, 256, 512];
|
|
192
|
+
try {
|
|
193
|
+
for (let i = 1; i <= 1536; i++) {
|
|
194
|
+
reference.enabled = i > 512;
|
|
195
|
+
await render_frame(checkpoints.includes(i));
|
|
196
|
+
report(i <= 512 ? `Convergence · frame ${i}/512` : `Building separate reference · ${i - 512}/1024 samples`);
|
|
197
|
+
}
|
|
198
|
+
} finally { reference.enabled = false; }
|
|
199
|
+
const target = await reference.read();
|
|
200
|
+
const mask = frames.at(-1).stages.mask;
|
|
201
|
+
const summary = { width: canvas.width, height: canvas.height, checkpoints,
|
|
202
|
+
ssr_frame_indices: frames.map(f => f.ssr_frame_index), dt: 1 / 60,
|
|
203
|
+
reference_frames: [513, 1536], reference_sample_count: reference.count,
|
|
204
|
+
automatic_exposure: false, exposure_compensation: 1,
|
|
205
|
+
roughness: 0.3, metallic: 1, spatial: renderer.ssr.spatial_denoising_enabled,
|
|
206
|
+
temporal: renderer.ssr.reprojection_mode !== 2,
|
|
207
|
+
taa: renderer.feature_taa_enabled, raster_jitter: element('jitter').value,
|
|
208
|
+
camera: { position: [0, 3.2, 7], target: [0, 0.2, -2], fov_degrees: 50 },
|
|
209
|
+
definition: 'Per-pixel linear-luminance error in the reflection ROI against the unrectified float32 mean of pre-temporal samples from later frames. The reference is never fed into SSR.',
|
|
210
|
+
samples: frames.map(frame => ({ frame: frame.frame,
|
|
211
|
+
input: reflection_error(frame.stages.spatial1, target, mask, canvas.width, canvas.height),
|
|
212
|
+
temporal: reflection_error(frame.stages.temporal, target, mask, canvas.width, canvas.height),
|
|
213
|
+
spatial: reflection_error(frame.stages.spatial4, target, mask, canvas.width, canvas.height) })),
|
|
214
|
+
};
|
|
215
|
+
files.set('reference.f32', target);
|
|
216
|
+
json_download('convergence-download', 'convergence.json', summary, files);
|
|
217
|
+
for (const sample of summary.samples) {
|
|
218
|
+
const row = document.createElement('tr');
|
|
219
|
+
for (const value of [sample.frame, sample.input.rmse.toFixed(6), sample.temporal.rmse.toFixed(6), sample.spatial.rmse.toFixed(6), sample.temporal.bias.toFixed(6)]) {
|
|
220
|
+
const cell = document.createElement('td'); cell.textContent = String(value); row.append(cell);
|
|
221
|
+
}
|
|
222
|
+
element('convergence-metrics').append(row);
|
|
223
|
+
}
|
|
224
|
+
element('convergence-results').hidden = false;
|
|
225
|
+
lab.summary = summary;
|
|
226
|
+
report('Convergence complete · 9 checkpoints · 1,024 later reference samples · frame 1536 · renderer stopped');
|
|
227
|
+
return summary;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
const lab = globalThis.ssr_variance_lab = {
|
|
231
|
+
renderer, camera, world, step, run, convergence, frames, files, get frame_count() { return frame_count; },
|
|
232
|
+
};
|
|
233
|
+
element('step').disabled = element('run').disabled = false;
|
|
234
|
+
element('converge').disabled = false;
|
|
235
|
+
element('converge').onclick = () => frame_count === 0 ? convergence().catch(() => {}) : restart('convergence');
|
|
236
|
+
element('step').onclick = () => step().catch(() => {});
|
|
237
|
+
element('run').onclick = () => frame_count === 0 ? run().catch(() => {}) : restart('run');
|
|
238
|
+
report('Ready · frame 0 · no frames submitted');
|
|
239
|
+
if (params.has('convergence')) { await convergence(); }
|
|
240
|
+
else if (params.has('run')) { await run(); }
|
|
241
|
+
return lab;
|
|
242
|
+
}
|
|
243
|
+
globalThis.ssr_variance_ready = start().catch(error => {
|
|
244
|
+
report(error.message ?? String(error)); element('status').classList.add('error'); console.error(error);
|
|
245
|
+
throw error;
|
|
246
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function reflection_error(sample: any, reference: any, mask: any, width: any, height: any): {
|
|
2
|
+
pixels: number;
|
|
3
|
+
mse: number;
|
|
4
|
+
rmse: number;
|
|
5
|
+
bias: number;
|
|
6
|
+
};
|
|
7
|
+
/** Unclipped float32 average of later pre-temporal samples, never fed into rendering. */
|
|
8
|
+
export class ReflectionReference extends RenderExtension {
|
|
9
|
+
constructor(device: any);
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
count: number;
|
|
12
|
+
textures: any[];
|
|
13
|
+
device: any;
|
|
14
|
+
record(frame: any): void;
|
|
15
|
+
read(): Promise<TypedArray>;
|
|
16
|
+
}
|
|
17
|
+
import { RenderExtension } from "../../renderer/extension/RenderExtension.js";
|
|
18
|
+
//# sourceMappingURL=reference.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reference.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/ssr_variance/reference.js"],"names":[],"mappings":"AA8DA;;;;;EAeC;AAjDD,yFAAyF;AACzF;IAKI,yBAAsD;IAHtD,iBAAgB;IAChB,cAAU;IACV,gBAAc;IACiB,YAAoB;IACnD,yBAmBC;IACD,4BAIC;CACJ;gCA5D+B,6CAA6C"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { RenderExtension } from "../../renderer/extension/RenderExtension.js";
|
|
2
|
+
import { ImageShader } from "../../renderer/shader/ImageShader.js";
|
|
3
|
+
import { CodeChunk } from "../../renderer/shader/compiler/CodeChunk.js";
|
|
4
|
+
import { ShaderDescriptor } from "../../renderer/shader/ShaderDescriptor.js";
|
|
5
|
+
import { ShaderResourceSetDescriptor } from "../../renderer/shader/resource/ShaderResourceSetDescriptor.js";
|
|
6
|
+
import { WebGPUStruct } from "../../renderer/shader/type/WebGPUStruct.js";
|
|
7
|
+
import { f32 } from "../../renderer/shader/type/WEBGPU_WGSL_PRIMITIVE_TYPES.js";
|
|
8
|
+
import { graph_image_pass } from "../../renderer/shader/graph/graph_image_pass.js";
|
|
9
|
+
import { graph_import_texture } from "../../renderer/shader/graph/graph_import_texture.js";
|
|
10
|
+
import { GPUTextureContext } from "../../renderer/texture/GPUTextureContext.js";
|
|
11
|
+
import { download_texture_data } from "../../renderer/texture/download_texture_data.js";
|
|
12
|
+
|
|
13
|
+
const settings = WebGPUStruct.from({ count: f32 });
|
|
14
|
+
const resources = new ShaderResourceSetDescriptor();
|
|
15
|
+
resources.setImmediate('settings', settings);
|
|
16
|
+
resources.createGroup().addTexture('tInput', 'unfilterable-float').addTexture('tPrevious', 'unfilterable-float');
|
|
17
|
+
const shader = new ImageShader({ descriptor: ShaderDescriptor.from({
|
|
18
|
+
label: 'SSR lab / later-sample reference', resources,
|
|
19
|
+
body: CodeChunk.from(/* language=WGSL */ `
|
|
20
|
+
@fragment fn main(@builtin(position) coord: vec4f) -> @location(0) vec4f {
|
|
21
|
+
let p = vec2i(coord.xy);
|
|
22
|
+
let sample = textureLoad(tInput, p, 0);
|
|
23
|
+
if (settings.count <= 1.0) { return sample; }
|
|
24
|
+
let previous = textureLoad(tPrevious, p, 0);
|
|
25
|
+
return previous + (sample - previous) / settings.count;
|
|
26
|
+
}`, [settings.declaration_chunk]),
|
|
27
|
+
}), targets: [{ format: 'rgba32float' }] });
|
|
28
|
+
|
|
29
|
+
/** Unclipped float32 average of later pre-temporal samples, never fed into rendering. */
|
|
30
|
+
export class ReflectionReference extends RenderExtension {
|
|
31
|
+
name = 'SSR lab / later-sample reference';
|
|
32
|
+
enabled = false;
|
|
33
|
+
count = 0;
|
|
34
|
+
textures = [];
|
|
35
|
+
constructor(device) { super(); this.device = device; }
|
|
36
|
+
record(frame) {
|
|
37
|
+
if (!this.enabled) { return; }
|
|
38
|
+
const temporal = frame.graph.__pass_nodes.find(p => p.name === 'SSR Reproject');
|
|
39
|
+
if (!temporal) { throw new Error('SSR is required for a reflection reference'); }
|
|
40
|
+
if (!this.textures.length) {
|
|
41
|
+
for (let i = 0; i < 2; i++) {
|
|
42
|
+
this.textures.push(GPUTextureContext.fromTexture(this.device.createTexture({
|
|
43
|
+
size: [frame.resolution[0], frame.resolution[1], 1], format: 'rgba32float',
|
|
44
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
|
|
45
|
+
}), this.device));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const index = this.count % 2;
|
|
49
|
+
const previous = graph_import_texture(frame.graph, this.textures[1 - index], 'SSR reference previous');
|
|
50
|
+
const output = graph_import_texture(frame.graph, this.textures[index], 'SSR reference output');
|
|
51
|
+
graph_image_pass({ graph: frame.graph, shader,
|
|
52
|
+
inputs: { tInput: temporal.data.tInput, tPrevious: previous, settings: { count: this.count + 1 } },
|
|
53
|
+
output: [output] });
|
|
54
|
+
this.count++;
|
|
55
|
+
}
|
|
56
|
+
async read() {
|
|
57
|
+
if (!this.count) { throw new Error('Reference has no samples'); }
|
|
58
|
+
return download_texture_data({ device: this.device,
|
|
59
|
+
source: this.textures[(this.count - 1) % 2].gpu_texture });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function reflection_error(sample, reference, mask, width, height) {
|
|
64
|
+
let count = 0, squared_error = 0, bias = 0;
|
|
65
|
+
for (let y = Math.floor(height * 0.5); y < Math.floor(height * 0.92); y++) {
|
|
66
|
+
for (let x = Math.floor(width * 0.25); x < Math.floor(width * 0.75); x++) {
|
|
67
|
+
const p = (y * width + x) * 4;
|
|
68
|
+
if (mask[p] <= 0.5) { continue; }
|
|
69
|
+
const error = (sample[p] - reference[p]) * 0.2126
|
|
70
|
+
+ (sample[p + 1] - reference[p + 1]) * 0.7152
|
|
71
|
+
+ (sample[p + 2] - reference[p + 2]) * 0.0722;
|
|
72
|
+
if (!Number.isFinite(error)) { throw new Error('Non-finite convergence sample'); }
|
|
73
|
+
squared_error += error * error; bias += error; count++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (!count) { throw new Error('No reflection pixels in convergence sample'); }
|
|
77
|
+
return { pixels: count, mse: squared_error / count, rmse: Math.sqrt(squared_error / count), bias: bias / count };
|
|
78
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Two quads. The cross is a texture, with no extra geometry or surface-normal edges. */
|
|
2
|
+
export function create_scene(): Promise<{
|
|
3
|
+
scene: Scene;
|
|
4
|
+
ground: Mesh;
|
|
5
|
+
card: Mesh;
|
|
6
|
+
ground_material: StandardShadeMaterial;
|
|
7
|
+
}>;
|
|
8
|
+
import { Scene } from "../../renderer/scene/Scene.js";
|
|
9
|
+
import { Mesh } from "../../renderer/scene/Mesh.js";
|
|
10
|
+
import { StandardShadeMaterial } from "../../renderer/material/StandardShadeMaterial.js";
|
|
11
|
+
//# sourceMappingURL=scene.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scene.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/ssr_variance/scene.js"],"names":[],"mappings":"AAeA,yFAAyF;AACzF;;;;;GA4CC;sBAnDqB,+BAA+B;qBADhC,8BAA8B;sCADb,kDAAkD"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Color } from "../../../core/color/Color.js";
|
|
2
|
+
import Quaternion from "../../../core/geom/Quaternion.js";
|
|
3
|
+
import Vector3 from "../../../core/geom/Vector3.js";
|
|
4
|
+
import { MeshoptClusterizer } from "../../renderer/geometry/meshlet/build/meshoptimizer/meshopt_clusterizer.js";
|
|
5
|
+
import { make_plane_geometry } from "../../renderer/geometry/primitives/make_plane_geometry.js";
|
|
6
|
+
import { MeshletGeometry } from "../../renderer/geometry/MeshletGeometry.js";
|
|
7
|
+
import { meshlet_geometry_build_from_geometry } from "../../renderer/geometry/meshlet_geometry_build_from_geometry.js";
|
|
8
|
+
import { StandardShadeMaterial } from "../../renderer/material/StandardShadeMaterial.js";
|
|
9
|
+
import { Mesh } from "../../renderer/scene/Mesh.js";
|
|
10
|
+
import { Scene } from "../../renderer/scene/Scene.js";
|
|
11
|
+
import { ColorSpace } from "../../renderer/texture/ColorSpace.js";
|
|
12
|
+
import { ShadeTexture } from "../../renderer/texture/ShadeTexture.js";
|
|
13
|
+
import { ShadeImage } from "../../renderer/texture/source/ShadeImage.js";
|
|
14
|
+
import { make_flat_environment } from "../skinned_blas_refit/flat_environment.js";
|
|
15
|
+
|
|
16
|
+
/** Two quads. The cross is a texture, with no extra geometry or surface-normal edges. */
|
|
17
|
+
export async function create_scene() {
|
|
18
|
+
await MeshoptClusterizer.ready;
|
|
19
|
+
const scene = new Scene();
|
|
20
|
+
const quad = (name, width, height, material) => {
|
|
21
|
+
const geometry = new MeshletGeometry();
|
|
22
|
+
meshlet_geometry_build_from_geometry(make_plane_geometry(width, height), geometry);
|
|
23
|
+
const mesh = Mesh.from(geometry, material);
|
|
24
|
+
mesh.name = name;
|
|
25
|
+
scene.add(mesh);
|
|
26
|
+
return mesh;
|
|
27
|
+
};
|
|
28
|
+
const ground_material = new StandardShadeMaterial();
|
|
29
|
+
ground_material.diffuse_color.copy(new Color(0.85, 0.85, 0.85));
|
|
30
|
+
ground_material.roughness_factor = 0.3;
|
|
31
|
+
ground_material.metallic_factor = 1;
|
|
32
|
+
const ground = quad('Ground / roughness 0.30', 18, 18, ground_material);
|
|
33
|
+
const rotation = Quaternion.fromAxisAngle(Vector3.right, -Math.PI / 2);
|
|
34
|
+
ground.transform_local.setRotation(rotation.x, rotation.y, rotation.z, rotation.w);
|
|
35
|
+
ground.transform_local.setTranslation(0, 0, -2);
|
|
36
|
+
ground.updateMatrices();
|
|
37
|
+
|
|
38
|
+
const canvas = document.createElement('canvas');
|
|
39
|
+
canvas.width = canvas.height = 512;
|
|
40
|
+
const ctx = canvas.getContext('2d');
|
|
41
|
+
ctx.fillStyle = '#181818'; ctx.fillRect(0, 0, 512, 512);
|
|
42
|
+
ctx.fillStyle = '#f4f4f4';
|
|
43
|
+
ctx.fillRect(216, 64, 80, 384); ctx.fillRect(64, 216, 384, 80);
|
|
44
|
+
// Small inset bars expose detail loss at a second scale.
|
|
45
|
+
ctx.fillStyle = '#ff8822'; ctx.fillRect(56, 56, 96, 24);
|
|
46
|
+
ctx.fillStyle = '#33bbff'; ctx.fillRect(360, 408, 96, 48);
|
|
47
|
+
const image = ShadeImage.fromImageBitmap(await createImageBitmap(canvas, { premultiplyAlpha: 'none' }));
|
|
48
|
+
image.color_space = ColorSpace.SRGB;
|
|
49
|
+
const pattern = ShadeTexture.from(image);
|
|
50
|
+
pattern.label = 'SSR variance / cross';
|
|
51
|
+
const card_material = new StandardShadeMaterial();
|
|
52
|
+
card_material.diffuse_color.copy(new Color(0, 0, 0));
|
|
53
|
+
card_material.texture_emissive = pattern;
|
|
54
|
+
card_material.emissive_factor.copy(new Color(1, 1, 1));
|
|
55
|
+
card_material.roughness_factor = 1;
|
|
56
|
+
const card = quad('Pattern card', 3, 3, card_material);
|
|
57
|
+
card.transform_local.setTranslation(0, 2, -2);
|
|
58
|
+
card.updateMatrices();
|
|
59
|
+
scene.lights.environment = make_flat_environment([0.025, 0.025, 0.025]);
|
|
60
|
+
return { scene, ground, card, ground_material };
|
|
61
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Per-pixel temporal moments of linear Rec.709 luminance. No spatial pooling before variance. */
|
|
2
|
+
export function measure_variance(samples: any, mask: any, width: any, height: any): {
|
|
3
|
+
mean: Float32Array;
|
|
4
|
+
variance: Float32Array;
|
|
5
|
+
roi: {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
};
|
|
11
|
+
summary: {
|
|
12
|
+
all: {
|
|
13
|
+
pixels: number;
|
|
14
|
+
mean_luminance: number;
|
|
15
|
+
mean_population_variance: number;
|
|
16
|
+
mean_sample_variance: number;
|
|
17
|
+
rms_temporal_sigma: number;
|
|
18
|
+
p95_temporal_sigma: number;
|
|
19
|
+
mean_peak_to_peak: number;
|
|
20
|
+
max_peak_to_peak: number;
|
|
21
|
+
};
|
|
22
|
+
ground: {
|
|
23
|
+
pixels: number;
|
|
24
|
+
mean_luminance: number;
|
|
25
|
+
mean_population_variance: number;
|
|
26
|
+
mean_sample_variance: number;
|
|
27
|
+
rms_temporal_sigma: number;
|
|
28
|
+
p95_temporal_sigma: number;
|
|
29
|
+
mean_peak_to_peak: number;
|
|
30
|
+
max_peak_to_peak: number;
|
|
31
|
+
};
|
|
32
|
+
reflection: {
|
|
33
|
+
pixels: number;
|
|
34
|
+
mean_luminance: number;
|
|
35
|
+
mean_population_variance: number;
|
|
36
|
+
mean_sample_variance: number;
|
|
37
|
+
rms_temporal_sigma: number;
|
|
38
|
+
p95_temporal_sigma: number;
|
|
39
|
+
mean_peak_to_peak: number;
|
|
40
|
+
max_peak_to_peak: number;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=statistics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"statistics.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/ssr_variance/statistics.js"],"names":[],"mappings":"AAAA,kGAAkG;AAClG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiDC"}
|