@woosh/meep-engine 3.14.0 → 3.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -64
- package/package.json +1 -1
- package/src/avif/native/DECISIONS.md +544 -536
- package/src/avif/native/heif/parse_avif_file.d.ts.map +1 -1
- package/src/avif/native/heif/parse_avif_file.js +384 -383
- package/src/avif/native/heif/parse_image_grid.d.ts.map +1 -1
- package/src/avif/native/heif/parse_image_grid.js +49 -48
- package/src/basis/BasisBitReader.d.ts +77 -0
- package/src/basis/BasisBitReader.d.ts.map +1 -0
- package/src/basis/BasisBitReader.js +231 -0
- package/src/basis/BasisHuffmanTable.d.ts +53 -0
- package/src/basis/BasisHuffmanTable.d.ts.map +1 -0
- package/src/basis/BasisHuffmanTable.js +263 -0
- package/src/basis/basis_read_huffman_table.d.ts +20 -0
- package/src/basis/basis_read_huffman_table.d.ts.map +1 -0
- package/src/basis/basis_read_huffman_table.js +187 -0
- package/src/basis/etc1s/Etc1sSelectorHistory.d.ts +50 -0
- package/src/basis/etc1s/Etc1sSelectorHistory.d.ts.map +1 -0
- package/src/basis/etc1s/Etc1sSelectorHistory.js +89 -0
- package/src/basis/etc1s/etc1s_block_colors.d.ts +14 -0
- package/src/basis/etc1s/etc1s_block_colors.d.ts.map +1 -0
- package/src/basis/etc1s/etc1s_block_colors.js +75 -0
- package/src/basis/etc1s/etc1s_decode_slice.d.ts +57 -0
- package/src/basis/etc1s/etc1s_decode_slice.d.ts.map +1 -0
- package/src/basis/etc1s/etc1s_decode_slice.js +258 -0
- package/src/basis/etc1s/etc1s_read_endpoint_codebook.d.ts +48 -0
- package/src/basis/etc1s/etc1s_read_endpoint_codebook.d.ts.map +1 -0
- package/src/basis/etc1s/etc1s_read_endpoint_codebook.js +111 -0
- package/src/basis/etc1s/etc1s_read_selector_codebook.d.ts +42 -0
- package/src/basis/etc1s/etc1s_read_selector_codebook.d.ts.map +1 -0
- package/src/basis/etc1s/etc1s_read_selector_codebook.js +127 -0
- package/src/basis/etc1s/etc1s_read_slice_tables.d.ts +54 -0
- package/src/basis/etc1s/etc1s_read_slice_tables.d.ts.map +1 -0
- package/src/basis/etc1s/etc1s_read_slice_tables.js +47 -0
- package/src/basis/transcode/build_range_plan.js +54 -0
- package/src/basis/transcode/etc1s_to_bc1.d.ts +24 -0
- package/src/basis/transcode/etc1s_to_bc1.d.ts.map +1 -0
- package/src/basis/transcode/etc1s_to_bc1.js +205 -0
- package/src/basis/transcode/etc1s_to_bc7.d.ts +30 -0
- package/src/basis/transcode/etc1s_to_bc7.d.ts.map +1 -0
- package/src/basis/transcode/etc1s_to_bc7.js +496 -0
- package/src/basis/transcode/fit_endpoints.d.ts +59 -0
- package/src/basis/transcode/fit_endpoints.d.ts.map +1 -0
- package/src/basis/transcode/fit_endpoints.js +226 -0
- package/src/{avif/native/isobmff → core/binary}/ByteCursor.d.ts +19 -6
- package/src/core/binary/ByteCursor.d.ts.map +1 -0
- package/src/{avif/native/isobmff → core/binary}/ByteCursor.js +41 -14
- package/src/engine/EngineHarness.js +1 -1
- package/src/engine/asset/codec/Codec.d.ts.map +1 -0
- package/src/engine/asset/codec/CodecWithFallback.d.ts.map +1 -0
- package/src/engine/asset/{loaders/image/codec → codec}/CodecWithFallback.js +2 -2
- package/src/engine/asset/loaders/image/IMAGE_CODEC_REVIEW_2026_08_28.md +4 -4
- package/src/engine/asset/loaders/image/ImageRGBADataLoader.d.ts +1 -1
- package/src/engine/asset/loaders/image/ImageRGBADataLoader.d.ts.map +1 -1
- package/src/engine/asset/loaders/image/ImageRGBADataLoader.js +1 -1
- package/src/engine/asset/loaders/image/codec/NativeImageDecoder.d.ts +1 -1
- package/src/engine/asset/loaders/image/codec/NativeImageDecoder.d.ts.map +1 -1
- package/src/engine/asset/loaders/image/codec/NativeImageDecoder.js +1 -1
- package/src/engine/asset/loaders/image/codec/ThreadedImageDecoder.d.ts +1 -1
- package/src/engine/asset/loaders/image/codec/ThreadedImageDecoder.d.ts.map +1 -1
- package/src/engine/asset/loaders/image/codec/ThreadedImageDecoder.js +1 -1
- package/src/engine/graphics/texture/atlas/TextureAtlas.d.ts.map +1 -1
- package/src/engine/graphics/texture/atlas/TextureAtlas.js +13 -0
- package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
- package/src/engine/physics/broadphase/generate_pairs.js +7 -1
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/ktx2/KTX2_IDENTIFIER.d.ts +14 -0
- package/src/ktx2/KTX2_IDENTIFIER.d.ts.map +1 -0
- package/src/ktx2/KTX2_IDENTIFIER.js +15 -0
- package/src/ktx2/KhrDfModel.d.ts +35 -0
- package/src/ktx2/KhrDfModel.d.ts.map +1 -0
- package/src/ktx2/KhrDfModel.js +49 -0
- package/src/ktx2/SupercompressionScheme.d.ts +16 -0
- package/src/ktx2/SupercompressionScheme.d.ts.map +1 -0
- package/src/ktx2/SupercompressionScheme.js +23 -0
- package/src/ktx2/ktx2_read.d.ts +71 -0
- package/src/ktx2/ktx2_read.d.ts.map +1 -0
- package/src/ktx2/ktx2_read.js +581 -0
- package/src/ktx2/ktx2_read_basis_lz_global_data.d.ts +27 -0
- package/src/ktx2/ktx2_read_basis_lz_global_data.d.ts.map +1 -0
- package/src/ktx2/ktx2_read_basis_lz_global_data.js +189 -0
- package/src/ktx2/vk_format_to_texture_format.d.ts +55 -0
- package/src/ktx2/vk_format_to_texture_format.d.ts.map +1 -0
- package/src/ktx2/vk_format_to_texture_format.js +290 -0
- package/src/shade/descriptor/GPUTextureUsage.d.ts +23 -0
- package/src/shade/descriptor/GPUTextureUsage.d.ts.map +1 -0
- package/src/shade/descriptor/GPUTextureUsage.js +24 -0
- package/src/shade/descriptor/WebGPUExtensionType.d.ts +3 -0
- package/src/shade/descriptor/WebGPUExtensionType.js +26 -0
- package/src/shade/descriptor/texture/TextureDescriptor.d.ts +2 -6
- package/src/shade/descriptor/texture/TextureDescriptor.d.ts.map +1 -1
- package/src/shade/descriptor/texture/TextureDescriptor.js +24 -40
- package/src/shade/descriptor/texture/format/TextureFormat.d.ts +125 -0
- package/src/shade/descriptor/texture/format/TextureFormat.d.ts.map +1 -0
- package/src/shade/descriptor/texture/format/TextureFormat.js +146 -0
- package/src/shade/descriptor/texture/format/gpu_texture_format_info.d.ts +60 -0
- package/src/shade/descriptor/texture/format/gpu_texture_format_info.d.ts.map +1 -1
- package/src/shade/descriptor/texture/format/gpu_texture_format_info.js +102 -0
- package/src/shade/device/graph/TextureResourceDescriptor.d.ts.map +1 -1
- package/src/shade/device/graph/TextureResourceDescriptor.js +160 -155
- package/src/shade/device/mock/SoftwareGPUQueue.d.ts +11 -4
- package/src/shade/device/mock/SoftwareGPUQueue.d.ts.map +1 -1
- package/src/shade/device/mock/SoftwareGPUQueue.js +31 -37
- package/src/shade/device/mock/SoftwareGPUTexture.d.ts.map +1 -1
- package/src/shade/device/mock/SoftwareGPUTexture.js +8 -4
- package/src/shade/device/timing/profile/frame_graph_extract_topology.d.ts.map +1 -1
- package/src/shade/device/timing/profile/frame_graph_extract_topology.js +164 -174
- package/src/shade/playground/ground_seam/README.md +235 -0
- package/src/shade/playground/ground_seam/capture_scene_color.d.ts +45 -0
- package/src/shade/playground/ground_seam/capture_scene_color.d.ts.map +1 -0
- package/src/shade/playground/ground_seam/capture_scene_color.js +242 -0
- package/src/shade/playground/ground_seam/depth_step_across_seam.d.ts +11 -0
- package/src/shade/playground/ground_seam/depth_step_across_seam.d.ts.map +1 -0
- package/src/shade/playground/ground_seam/depth_step_across_seam.js +185 -0
- package/src/shade/playground/ground_seam/index.html +195 -0
- package/src/shade/playground/ground_seam/main.d.ts +2 -0
- package/src/shade/playground/ground_seam/main.d.ts.map +1 -0
- package/src/shade/playground/ground_seam/main.js +879 -0
- package/src/shade/playground/ground_seam/make_grid_texture.d.ts +29 -0
- package/src/shade/playground/ground_seam/make_grid_texture.d.ts.map +1 -0
- package/src/shade/playground/ground_seam/make_grid_texture.js +105 -0
- package/src/shade/playground/ground_seam/make_ground.d.ts +70 -0
- package/src/shade/playground/ground_seam/make_ground.d.ts.map +1 -0
- package/src/shade/playground/ground_seam/make_ground.js +161 -0
- package/src/shade/playground/ground_seam/measure_flicker.d.ts +71 -0
- package/src/shade/playground/ground_seam/measure_flicker.d.ts.map +1 -0
- package/src/shade/playground/ground_seam/measure_flicker.js +531 -0
- package/src/shade/renderer/Renderer.d.ts.map +1 -1
- package/src/shade/renderer/Renderer.js +2519 -2511
- package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
- package/src/shade/renderer/hiz/HierarchicalZBuffer.js +275 -274
- package/src/shade/renderer/light/GPULightCollection.d.ts.map +1 -1
- package/src/shade/renderer/light/GPULightCollection.js +323 -314
- package/src/shade/renderer/lightmap/GPULightMap.js +336 -336
- package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts.map +1 -1
- package/src/shade/renderer/material/resident/GPUResidentMaterialContext.js +12 -2
- package/src/shade/renderer/material/standard/fragment_gbuffer.d.ts.map +1 -1
- package/src/shade/renderer/material/standard/fragment_gbuffer.js +3 -1
- package/src/shade/renderer/material/standard/fragment_gbuffer_vt.js +169 -169
- package/src/shade/renderer/path_tracer/shader/chunk_sample_material_data.d.ts.map +1 -1
- package/src/shade/renderer/path_tracer/shader/chunk_sample_material_data.js +3 -1
- package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
- package/src/shade/renderer/rasterize/native/oit/chunk_forward_shade_standard_fragment_brick4.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/native/oit/chunk_forward_shade_standard_fragment_brick4.js +3 -1
- package/src/shade/renderer/rasterize/native/oit/chunk_forward_shade_standard_fragment_ibl.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/native/oit/chunk_forward_shade_standard_fragment_ibl.js +3 -1
- package/src/shade/renderer/shader/chunk/geometry/normal/chunk_decode_tangent_normal.d.ts +28 -0
- package/src/shade/renderer/shader/chunk/geometry/normal/chunk_decode_tangent_normal.d.ts.map +1 -0
- package/src/shade/renderer/shader/chunk/geometry/normal/chunk_decode_tangent_normal.js +39 -0
- package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_barycentric_full.d.ts +45 -0
- package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_barycentric_full.d.ts.map +1 -1
- package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_barycentric_full.js +94 -61
- package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_barycentric_solve.d.ts +24 -0
- package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_barycentric_solve.d.ts.map +1 -0
- package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_barycentric_solve.js +43 -0
- package/src/shade/renderer/texture/COMPRESSED_TEXTURE_PLAN_2026_09_01.md +1462 -0
- package/src/shade/renderer/texture/TextureManager.d.ts.map +1 -1
- package/src/shade/renderer/texture/TextureManager.js +20 -8
- package/src/shade/renderer/texture/TextureRole.d.ts +19 -0
- package/src/shade/renderer/texture/TextureRole.d.ts.map +1 -0
- package/src/shade/renderer/texture/TextureRole.js +26 -0
- package/src/shade/renderer/texture/TextureVariantSet.d.ts +71 -0
- package/src/shade/renderer/texture/TextureVariantSet.d.ts.map +1 -0
- package/src/shade/renderer/texture/TextureVariantSet.js +105 -0
- package/src/shade/renderer/texture/download_texture_data.d.ts.map +1 -1
- package/src/shade/renderer/texture/download_texture_data.js +22 -0
- package/src/shade/renderer/texture/format/gpu_texture_compression_support.d.ts +15 -0
- package/src/shade/renderer/texture/format/gpu_texture_compression_support.d.ts.map +1 -0
- package/src/shade/renderer/texture/format/gpu_texture_compression_support.js +50 -0
- package/src/shade/renderer/texture/format/gpu_texture_format_can_generate_mipmaps.d.ts +27 -0
- package/src/shade/renderer/texture/format/gpu_texture_format_can_generate_mipmaps.d.ts.map +1 -0
- package/src/shade/renderer/texture/format/gpu_texture_format_can_generate_mipmaps.js +65 -0
- package/src/shade/renderer/texture/format/infer_format_from_parameters.d.ts +11 -0
- package/src/shade/renderer/texture/format/infer_format_from_parameters.d.ts.map +1 -0
- package/src/shade/renderer/texture/format/infer_format_from_parameters.js +88 -0
- package/src/shade/renderer/texture/format/texture_format_from_shade_image.d.ts +24 -0
- package/src/shade/renderer/texture/format/texture_format_from_shade_image.d.ts.map +1 -0
- package/src/shade/renderer/texture/format/texture_format_from_shade_image.js +71 -0
- package/src/shade/renderer/texture/format/texture_format_preference.d.ts +22 -0
- package/src/shade/renderer/texture/format/texture_format_preference.d.ts.map +1 -0
- package/src/shade/renderer/texture/format/texture_format_preference.js +89 -0
- package/src/shade/renderer/texture/format/texture_variant_select.d.ts +24 -0
- package/src/shade/renderer/texture/format/texture_variant_select.d.ts.map +1 -0
- package/src/shade/renderer/texture/format/texture_variant_select.js +38 -0
- package/src/shade/renderer/texture/shade_texture_might_have_alpha.d.ts +9 -1
- package/src/shade/renderer/texture/shade_texture_might_have_alpha.d.ts.map +1 -1
- package/src/shade/renderer/texture/shade_texture_might_have_alpha.js +73 -8
- package/src/shade/renderer/texture/source/AlphaMode.d.ts +27 -0
- package/src/shade/renderer/texture/source/AlphaMode.d.ts.map +1 -0
- package/src/shade/renderer/texture/source/AlphaMode.js +52 -0
- package/src/shade/renderer/texture/source/ShadeImage.d.ts +33 -0
- package/src/shade/renderer/texture/source/ShadeImage.d.ts.map +1 -1
- package/src/shade/renderer/texture/source/ShadeImage.js +74 -0
- package/src/shade/renderer/texture/source/ShadeTexelData.d.ts +78 -0
- package/src/shade/renderer/texture/source/ShadeTexelData.d.ts.map +1 -0
- package/src/shade/renderer/texture/source/ShadeTexelData.js +158 -0
- package/src/shade/renderer/texture/texture_descriptor_from_texture_shade.d.ts +0 -10
- package/src/shade/renderer/texture/texture_descriptor_from_texture_shade.d.ts.map +1 -1
- package/src/shade/renderer/texture/texture_descriptor_from_texture_shade.js +73 -175
- package/src/shade/renderer/texture/texture_write_level.d.ts +32 -0
- package/src/shade/renderer/texture/texture_write_level.d.ts.map +1 -0
- package/src/shade/renderer/texture/texture_write_level.js +77 -0
- package/src/shade/renderer/texture/texture_write_to_gpu.d.ts +8 -1
- package/src/shade/renderer/texture/texture_write_to_gpu.d.ts.map +1 -1
- package/src/shade/renderer/texture/texture_write_to_gpu.js +62 -37
- package/src/shade/renderer/texture/virtual/source/VTSourceImage.d.ts +8 -0
- package/src/shade/renderer/texture/virtual/source/VTSourceImage.d.ts.map +1 -1
- package/src/shade/renderer/texture/virtual/source/VTSourceImage.js +8 -0
- package/src/shade/util/texture_mip_extent.d.ts +49 -0
- package/src/shade/util/texture_mip_extent.d.ts.map +1 -0
- package/src/shade/util/texture_mip_extent.js +87 -0
- package/src/avif/native/isobmff/ByteCursor.d.ts.map +0 -1
- package/src/engine/asset/loaders/image/codec/Codec.d.ts.map +0 -1
- package/src/engine/asset/loaders/image/codec/CodecWithFallback.d.ts.map +0 -1
- package/src/shade/util/compute_max_mip_count.d.ts +0 -8
- package/src/shade/util/compute_max_mip_count.d.ts.map +0 -1
- package/src/shade/util/compute_max_mip_count.js +0 -27
- /package/src/engine/asset/{loaders/image/codec → codec}/Codec.d.ts +0 -0
- /package/src/engine/asset/{loaders/image/codec → codec}/Codec.js +0 -0
- /package/src/engine/asset/{loaders/image/codec → codec}/CodecWithFallback.d.ts +0 -0
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
import Vector3 from "../../../core/geom/Vector3.js";
|
|
2
|
+
import { DEG_TO_RAD } from "../../../core/math/DEG_TO_RAD.js";
|
|
3
|
+
import { PerspectiveCamera } from "../../renderer/camera/PerspectiveCamera.js";
|
|
4
|
+
import { create_frame_loop } from "../../renderer/create_frame_loop.js";
|
|
5
|
+
import { make_sunlight } from "../../renderer/light/make_sunlight.js";
|
|
6
|
+
import { Renderer } from "../../renderer/Renderer.js";
|
|
7
|
+
import { Scene } from "../../renderer/scene/Scene.js";
|
|
8
|
+
import { load_default_environment } from "../load_default_environment.js";
|
|
9
|
+
import { make_grid_texture } from "./make_grid_texture.js";
|
|
10
|
+
import { GROUND_TRIANGULATION, make_ground } from "./make_ground.js";
|
|
11
|
+
import { FramePhase } from "../../renderer/extension/FramePhase.js";
|
|
12
|
+
import { CaptureSceneColor } from "./capture_scene_color.js";
|
|
13
|
+
import { depth_step_across_seam } from "./depth_step_across_seam.js";
|
|
14
|
+
import { attach_measurement, classify_ground, screen_line_distance } from "./measure_flicker.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A ground quad that shimmers on one side of its own triangle seam — as a page that can measure it.
|
|
18
|
+
*
|
|
19
|
+
* The report this page was built from was written from *outside* the engine, by screenshotting a
|
|
20
|
+
* demo through CDP, and three of its conclusions had to be retracted: two were stated before they
|
|
21
|
+
* were measured, and one came from a sign error in a hand-rolled camera basis. So the point of this
|
|
22
|
+
* page is not to look at the artifact. It is to put every claim about the artifact on a number that
|
|
23
|
+
* comes out of the renderer's own state:
|
|
24
|
+
*
|
|
25
|
+
* - the picture is read back from a texture this page owns, not scraped off the canvas;
|
|
26
|
+
* - the frames are driven from JS, so the burst length is a number chosen here rather than one the
|
|
27
|
+
* compositor handed over;
|
|
28
|
+
* - pixels are assigned to triangles by unprojecting through `camera.view_projection_matrix` and
|
|
29
|
+
* intersecting the ground plane, and the unprojection round-trips itself and reports the error.
|
|
30
|
+
*
|
|
31
|
+
* `npm run dev` serves it. HMR is off across the playground by design — an edit anywhere in the
|
|
32
|
+
* module graph would otherwise reset the temporal accumulation this page exists to measure.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The camera the artifact was reported at, and which the report says it is sensitive to.
|
|
37
|
+
*
|
|
38
|
+
* Distance 45 at 0.9 rad above the horizontal: `45·sin(0.9) = 35.25` up and `45·cos(0.9) = 27.97`
|
|
39
|
+
* back. Written out as a position rather than as pitch/yaw so it does not depend on which of the
|
|
40
|
+
* two conventions ("elevation" or "polar angle") a helper happens to mean by *pitch*.
|
|
41
|
+
*/
|
|
42
|
+
const CAMERA = {
|
|
43
|
+
position: new Vector3(0, 35.25, 27.97),
|
|
44
|
+
target: new Vector3(0, 0, 0),
|
|
45
|
+
fov_degrees: 45,
|
|
46
|
+
near: 0.1,
|
|
47
|
+
far: 100,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Viewport the report says the seam is clearest at, at a pixel ratio of 1.
|
|
52
|
+
*
|
|
53
|
+
* Resolution-dependence is one of its claims, so this is a control rather than the window size.
|
|
54
|
+
*/
|
|
55
|
+
const VIEWPORT = { width: 1258, height: 566 };
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {number} value
|
|
59
|
+
* @param {number} [digits]
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
function fixed(value, digits = 4) {
|
|
63
|
+
return Number.isFinite(value) ? value.toFixed(digits) : String(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function start() {
|
|
67
|
+
|
|
68
|
+
const element = id => document.getElementById(id);
|
|
69
|
+
|
|
70
|
+
const report_element = element('report');
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {string} text
|
|
74
|
+
*/
|
|
75
|
+
function report(text) {
|
|
76
|
+
report_element.textContent = text;
|
|
77
|
+
console.log(text);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const canvas = element('view');
|
|
81
|
+
const diff_canvas = element('diff');
|
|
82
|
+
|
|
83
|
+
const context = canvas.getContext('webgpu');
|
|
84
|
+
|
|
85
|
+
if (context === null) {
|
|
86
|
+
report('no WebGPU context — this browser has none, or the page is not a secure context');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const renderer = new Renderer();
|
|
91
|
+
|
|
92
|
+
report('acquiring a device…');
|
|
93
|
+
|
|
94
|
+
await renderer.initialize({ context, pixelRatio: 1 });
|
|
95
|
+
|
|
96
|
+
/*
|
|
97
|
+
Auto-exposure is the single most important thing to pin. It keeps drifting for seconds after
|
|
98
|
+
any change, by more than the signal being measured is worth, and a burst taken while it drifts
|
|
99
|
+
reports the drift rather than the artifact.
|
|
100
|
+
*/
|
|
101
|
+
renderer.feature_automatic_exposure_enabled = false;
|
|
102
|
+
|
|
103
|
+
const camera = new PerspectiveCamera();
|
|
104
|
+
|
|
105
|
+
camera.near = CAMERA.near;
|
|
106
|
+
camera.far = CAMERA.far;
|
|
107
|
+
camera.fov = CAMERA.fov_degrees * DEG_TO_RAD;
|
|
108
|
+
camera.transform.position.copy(CAMERA.position);
|
|
109
|
+
camera.transform.lookAt(CAMERA.target);
|
|
110
|
+
camera.transform.updateMatrix();
|
|
111
|
+
|
|
112
|
+
const scene = new Scene();
|
|
113
|
+
|
|
114
|
+
const sun = make_sunlight({
|
|
115
|
+
intensity: 0.9,
|
|
116
|
+
direction: new Vector3(0.17, -1, 0.17),
|
|
117
|
+
temperature: 5500,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
scene.lights.add(sun);
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Two textures differing only in whether the sampler is granted anisotropy, built up front so
|
|
124
|
+
* switching between them costs no upload and cannot perturb the temporal history by more than
|
|
125
|
+
* the swap itself.
|
|
126
|
+
*
|
|
127
|
+
* @type {Record<string, ShadeTexture>}
|
|
128
|
+
*/
|
|
129
|
+
const albedos = {
|
|
130
|
+
anisotropic: await make_grid_texture(256, true),
|
|
131
|
+
isotropic: await make_grid_texture(256, false),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/** @type {Mesh|undefined} */
|
|
135
|
+
let ground;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Swap the ground for one built with different topology. Everything else — the camera, the
|
|
139
|
+
* light, the texture, the material constants — is untouched, so a difference between two builds
|
|
140
|
+
* is a difference in how the quad is cut up and nothing else.
|
|
141
|
+
*
|
|
142
|
+
* @param {number} segments
|
|
143
|
+
* @param {string} triangulation key of {@link GROUND_TRIANGULATION}
|
|
144
|
+
* @param {number} [yaw_degrees] rotation about +Y; 180 swaps the two triangles without moving
|
|
145
|
+
* the seam they share
|
|
146
|
+
* @param {boolean} [anisotropy] whether the sampler is granted 16× anisotropy
|
|
147
|
+
* @param {number} [extent] edge length of the quad, which is what sets how badly conditioned the
|
|
148
|
+
* reconstruction is
|
|
149
|
+
*/
|
|
150
|
+
function rebuild_ground(segments, triangulation, yaw_degrees = 0, anisotropy = true, extent = 425) {
|
|
151
|
+
if (ground !== undefined) {
|
|
152
|
+
scene.remove(ground);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
ground = make_ground({
|
|
156
|
+
albedo: albedos[anisotropy ? 'anisotropic' : 'isotropic'],
|
|
157
|
+
extent,
|
|
158
|
+
segments,
|
|
159
|
+
indices: GROUND_TRIANGULATION[triangulation],
|
|
160
|
+
yaw_degrees,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
scene.add(ground);
|
|
164
|
+
|
|
165
|
+
/*
|
|
166
|
+
Proof the index buffer actually reached the GPU differently, published so a null result on
|
|
167
|
+
an index-order A/B cannot be mistaken for 'the reconstruction does not care'. The meshlet
|
|
168
|
+
builder is free to reorder, and a builder that normalised the order away would make every
|
|
169
|
+
such test silently vacuous.
|
|
170
|
+
*/
|
|
171
|
+
globalThis.__meshlet_signature = Array.from(
|
|
172
|
+
new Uint8Array(ground.geometry.meshlets.data_buffer)
|
|
173
|
+
).join(',');
|
|
174
|
+
|
|
175
|
+
renderer.indicate_view_change();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
rebuild_ground(1, 'default');
|
|
179
|
+
|
|
180
|
+
const measurement = attach_measurement({ renderer, scene, camera });
|
|
181
|
+
|
|
182
|
+
// handy from the console
|
|
183
|
+
Object.assign(globalThis, { renderer, scene, camera, measurement, classify_ground });
|
|
184
|
+
|
|
185
|
+
// ---- controls ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
let paused = false;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Edge length of the quad currently in the scene. Held here because the pixel classifier needs
|
|
191
|
+
* the half-extent to decide what is ground, and a stale value there would silently label the
|
|
192
|
+
* far half of a big quad as sky.
|
|
193
|
+
*
|
|
194
|
+
* @type {number}
|
|
195
|
+
*/
|
|
196
|
+
let ground_extent = 425;
|
|
197
|
+
|
|
198
|
+
function apply_settings() {
|
|
199
|
+
renderer.feature_taa_enabled = element('taa').checked;
|
|
200
|
+
renderer.feature_ssao_enabled = element('ssao').checked;
|
|
201
|
+
renderer.feature_shadows_enabled = element('shadows').checked;
|
|
202
|
+
renderer.feature_sharpening_enabled = element('sharpening').checked;
|
|
203
|
+
renderer.feature_automatic_exposure_enabled = element('exposure').checked;
|
|
204
|
+
|
|
205
|
+
/*
|
|
206
|
+
Not a look control. The readback is 8-bit `bgra8unorm` — the display is not HDR — so one
|
|
207
|
+
code value is 1/255, and a ground sitting at 0.06 luma leaves the whole measurement two
|
|
208
|
+
code values wide. Lifting the fixed exposure puts the image in the middle of the range the
|
|
209
|
+
readback can actually resolve, without touching the light, the material or the sampler.
|
|
210
|
+
*/
|
|
211
|
+
renderer.exposure_compensation = Number(element('exposure_compensation').value);
|
|
212
|
+
|
|
213
|
+
VIEWPORT.width = Math.max(2, Number(element('width').value) | 0);
|
|
214
|
+
VIEWPORT.height = Math.max(2, Number(element('height').value) | 0);
|
|
215
|
+
|
|
216
|
+
ground_extent = Number(element('extent').value);
|
|
217
|
+
|
|
218
|
+
rebuild_ground(
|
|
219
|
+
Number(element('segments').value),
|
|
220
|
+
element('triangulation').value,
|
|
221
|
+
Number(element('yaw').value),
|
|
222
|
+
element('anisotropy').checked,
|
|
223
|
+
ground_extent
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
element('apply').addEventListener('click', () => {
|
|
228
|
+
apply_settings();
|
|
229
|
+
report('applied — the temporal history was dropped, so settle before measuring');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Set controls by name and apply them, for driving an A/B from a script rather than by hand.
|
|
234
|
+
*
|
|
235
|
+
* The controls are the single source of truth for what the page is configured as — a script
|
|
236
|
+
* that reached past them and assigned renderer fields directly would leave the panel describing
|
|
237
|
+
* a state the renderer is not in, and the report is built from the panel.
|
|
238
|
+
*
|
|
239
|
+
* @param {Record<string, string|number|boolean>} settings keyed by input id
|
|
240
|
+
* @returns {Promise<void>}
|
|
241
|
+
*/
|
|
242
|
+
globalThis.configure = async settings => {
|
|
243
|
+
for (const [id, value] of Object.entries(settings)) {
|
|
244
|
+
const input = element(id);
|
|
245
|
+
|
|
246
|
+
if (input === null) {
|
|
247
|
+
throw new Error(`no control named '${id}'`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (input.type === 'checkbox') {
|
|
251
|
+
if (input.checked !== Boolean(value)) {
|
|
252
|
+
input.checked = Boolean(value);
|
|
253
|
+
input.dispatchEvent(new Event('change'));
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
input.value = String(value);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
apply_settings();
|
|
261
|
+
|
|
262
|
+
// the environment handler is async and fires off `change`; give it a turn to land
|
|
263
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
let environment_loaded = false;
|
|
267
|
+
|
|
268
|
+
element('environment').addEventListener('change', async event => {
|
|
269
|
+
if (event.target.checked) {
|
|
270
|
+
if (!environment_loaded) {
|
|
271
|
+
environment_loaded = await load_default_environment(scene);
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
scene.lights.environment = undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
renderer.indicate_view_change();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// ---- measurement ------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Settle, take a burst, and describe the spread across it — overall, and split by which of the
|
|
284
|
+
* ground quad's triangles covers each pixel.
|
|
285
|
+
*
|
|
286
|
+
* @returns {Promise<object>} also written to `globalThis.__last_measurement`
|
|
287
|
+
*/
|
|
288
|
+
async function measure() {
|
|
289
|
+
paused = true;
|
|
290
|
+
|
|
291
|
+
const settle_frames = Number(element('settle').value) | 0;
|
|
292
|
+
const frames = Number(element('frames').value) | 0;
|
|
293
|
+
const gain = Number(element('gain').value);
|
|
294
|
+
|
|
295
|
+
const { spread, mean, width, height } = await measurement.burst({ settle_frames, frames });
|
|
296
|
+
|
|
297
|
+
/*
|
|
298
|
+
The camera's matrices are whatever the last frame left them as, which is what the pixels
|
|
299
|
+
were drawn with — so the classification is taken from the same state, not recomputed from
|
|
300
|
+
the authored numbers.
|
|
301
|
+
*/
|
|
302
|
+
const {
|
|
303
|
+
side,
|
|
304
|
+
side_flipped,
|
|
305
|
+
round_trip_error_px,
|
|
306
|
+
covered
|
|
307
|
+
} = classify_ground(camera, width, height, ground_extent * 0.5);
|
|
308
|
+
|
|
309
|
+
const triangulation = element('triangulation').value;
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Split the frame by a labelling and describe the spread on each side.
|
|
313
|
+
*
|
|
314
|
+
* Both the *absolute* spread and the spread *relative to the local mean* are reported. The
|
|
315
|
+
* two halves of this view are not equally lit — one is more oblique than the other, so it
|
|
316
|
+
* is dimmer and more anisotropically filtered whatever the index buffer says — and an
|
|
317
|
+
* absolute ratio would charge that difference to the artifact. The relative figure is the
|
|
318
|
+
* one that survives it.
|
|
319
|
+
*
|
|
320
|
+
* @param {Int8Array} labels
|
|
321
|
+
* @returns {object}
|
|
322
|
+
*/
|
|
323
|
+
function split(labels) {
|
|
324
|
+
const buckets = {
|
|
325
|
+
negative: { count: 0, total: 0, relative_total: 0, max: 0, luma_total: 0 },
|
|
326
|
+
positive: { count: 0, total: 0, relative_total: 0, max: 0, luma_total: 0 },
|
|
327
|
+
sky: { count: 0, total: 0, relative_total: 0, max: 0, luma_total: 0 },
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
for (let p = 0; p < labels.length; p++) {
|
|
331
|
+
const bucket = labels[p] === 0
|
|
332
|
+
? buckets.sky
|
|
333
|
+
: (labels[p] < 0 ? buckets.negative : buckets.positive);
|
|
334
|
+
|
|
335
|
+
bucket.count++;
|
|
336
|
+
bucket.total += spread[p];
|
|
337
|
+
bucket.luma_total += mean[p];
|
|
338
|
+
|
|
339
|
+
// guard against a pixel that is exactly black, where the ratio is meaningless
|
|
340
|
+
if (mean[p] > 1e-4) {
|
|
341
|
+
bucket.relative_total += spread[p] / mean[p];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (spread[p] > bucket.max) {
|
|
345
|
+
bucket.max = spread[p];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
for (const bucket of Object.values(buckets)) {
|
|
350
|
+
bucket.mean = bucket.count > 0 ? bucket.total / bucket.count : 0;
|
|
351
|
+
bucket.relative = bucket.count > 0 ? bucket.relative_total / bucket.count : 0;
|
|
352
|
+
bucket.mean_luma = bucket.count > 0 ? bucket.luma_total / bucket.count : 0;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const { negative, positive } = buckets;
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
negative,
|
|
359
|
+
positive,
|
|
360
|
+
sky: buckets.sky,
|
|
361
|
+
ratio: negative.mean > 0 && positive.mean > 0
|
|
362
|
+
? Math.max(negative.mean, positive.mean) / Math.min(negative.mean, positive.mean)
|
|
363
|
+
: null,
|
|
364
|
+
relative_ratio: negative.relative > 0 && positive.relative > 0
|
|
365
|
+
? Math.max(negative.relative, positive.relative)
|
|
366
|
+
/ Math.min(negative.relative, positive.relative)
|
|
367
|
+
: null,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/*
|
|
372
|
+
Both diagonals, always. The claim under test is that the instability is bound to the index
|
|
373
|
+
buffer, so the interesting comparison is between the split along the diagonal that *is* in
|
|
374
|
+
the mesh and the split along the one that is not — the second is a control cut through the
|
|
375
|
+
same picture, and if it shows the same asymmetry then the asymmetry is about the view, not
|
|
376
|
+
about the triangulation.
|
|
377
|
+
*/
|
|
378
|
+
const along_default = split(side);
|
|
379
|
+
const along_flipped = split(side_flipped);
|
|
380
|
+
|
|
381
|
+
const stats = triangulation === 'flipped' ? along_flipped : along_default;
|
|
382
|
+
|
|
383
|
+
// ---- the difference map
|
|
384
|
+
|
|
385
|
+
diff_canvas.width = width;
|
|
386
|
+
diff_canvas.height = height;
|
|
387
|
+
|
|
388
|
+
const ctx = diff_canvas.getContext('2d');
|
|
389
|
+
const image = ctx.createImageData(width, height);
|
|
390
|
+
|
|
391
|
+
for (let p = 0; p < spread.length; p++) {
|
|
392
|
+
const v = Math.min(255, Math.round(spread[p] * gain * 255));
|
|
393
|
+
|
|
394
|
+
const i = p * 4;
|
|
395
|
+
|
|
396
|
+
image.data[i] = v;
|
|
397
|
+
image.data[i + 1] = v;
|
|
398
|
+
image.data[i + 2] = v;
|
|
399
|
+
image.data[i + 3] = 255;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
ctx.putImageData(image, 0, 0);
|
|
403
|
+
|
|
404
|
+
const result = {
|
|
405
|
+
viewport: [width, height],
|
|
406
|
+
internal_resolution_scale: renderer.internal_resolution_scale,
|
|
407
|
+
segments: Number(element('segments').value),
|
|
408
|
+
triangulation,
|
|
409
|
+
taa: renderer.feature_taa_enabled,
|
|
410
|
+
settle_frames,
|
|
411
|
+
frames,
|
|
412
|
+
round_trip_error_px,
|
|
413
|
+
ground_pixels: covered,
|
|
414
|
+
stats,
|
|
415
|
+
along_default,
|
|
416
|
+
along_flipped,
|
|
417
|
+
ratio: stats.ratio,
|
|
418
|
+
relative_ratio: stats.relative_ratio,
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
globalThis.__last_measurement = result;
|
|
422
|
+
|
|
423
|
+
/*
|
|
424
|
+
The raw fields, kept off the summary object so a script can do its own analysis — a profile
|
|
425
|
+
across the seam, a different labelling, a histogram — without the page having to anticipate
|
|
426
|
+
it. Summaries are where a real structure goes to hide: two halves that differ by 2× in the
|
|
427
|
+
mean can be a step at the seam or a smooth gradient, and only the field tells you which.
|
|
428
|
+
*/
|
|
429
|
+
globalThis.__last_fields = { spread, mean, side, side_flipped, width, height };
|
|
430
|
+
|
|
431
|
+
/*
|
|
432
|
+
The profile is what separates "a seam" from "one half of the frame is more oblique than the
|
|
433
|
+
other". A half-plane mean reports the same 2× either way; a step at ±1 px does not.
|
|
434
|
+
*/
|
|
435
|
+
result.profile = {
|
|
436
|
+
'x = -z': profile_across([1, -1] /* world direction of the default seam */),
|
|
437
|
+
'x = +z': profile_across([1, 1]),
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Mean spread and mean relative spread in 1-pixel bands of signed screen distance from a
|
|
442
|
+
* world line on the ground plane.
|
|
443
|
+
*
|
|
444
|
+
* @param {number[]} direction the line's XZ direction
|
|
445
|
+
* @returns {object}
|
|
446
|
+
*/
|
|
447
|
+
function profile_across(direction) {
|
|
448
|
+
const distance = screen_line_distance(camera, width, height, direction);
|
|
449
|
+
|
|
450
|
+
const RANGE = 40;
|
|
451
|
+
|
|
452
|
+
const bins = new Float64Array(2 * RANGE);
|
|
453
|
+
const bins_relative = new Float64Array(2 * RANGE);
|
|
454
|
+
const counts = new Float64Array(2 * RANGE);
|
|
455
|
+
|
|
456
|
+
for (let py = 0; py < height; py++) {
|
|
457
|
+
for (let px = 0; px < width; px++) {
|
|
458
|
+
const d = distance(px, py);
|
|
459
|
+
|
|
460
|
+
const bin = Math.floor(d) + RANGE;
|
|
461
|
+
|
|
462
|
+
if (bin < 0 || bin >= bins.length) {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const p = py * width + px;
|
|
467
|
+
|
|
468
|
+
bins[bin] += spread[p];
|
|
469
|
+
|
|
470
|
+
if (mean[p] > 1e-4) {
|
|
471
|
+
bins_relative[bin] += spread[p] / mean[p];
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
counts[bin]++;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const relative = [];
|
|
479
|
+
|
|
480
|
+
for (let i = 0; i < bins.length; i++) {
|
|
481
|
+
relative.push(counts[i] > 0 ? bins_relative[i] / counts[i] : null);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return { offset: -RANGE, relative, counts: Array.from(counts) };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
report([
|
|
488
|
+
`viewport ${width} × ${height}`,
|
|
489
|
+
`ground pixels ${covered}`,
|
|
490
|
+
`unproject round-trip ${fixed(round_trip_error_px, 5)} px (must be ≪ 1)`,
|
|
491
|
+
`segments ${result.segments} triangulation ${triangulation}`,
|
|
492
|
+
`TAA ${result.taa}`,
|
|
493
|
+
``,
|
|
494
|
+
`spread, side − ${fixed(stats.negative.mean, 6)} rel ${fixed(stats.negative.relative, 5)} luma ${fixed(stats.negative.mean_luma, 4)}`,
|
|
495
|
+
`spread, side + ${fixed(stats.positive.mean, 6)} rel ${fixed(stats.positive.relative, 5)} luma ${fixed(stats.positive.mean_luma, 4)}`,
|
|
496
|
+
`spread, sky ${fixed(stats.sky.mean, 6)} (n=${stats.sky.count})`,
|
|
497
|
+
``,
|
|
498
|
+
`asymmetry, x = −z ${along_default.relative_ratio === null ? 'n/a' : fixed(along_default.relative_ratio, 3)} (absolute ${fixed(along_default.ratio, 3)})`,
|
|
499
|
+
`asymmetry, x = +z ${along_flipped.relative_ratio === null ? 'n/a' : fixed(along_flipped.relative_ratio, 3)} (absolute ${fixed(along_flipped.ratio, 3)})`,
|
|
500
|
+
].join('\n'));
|
|
501
|
+
|
|
502
|
+
paused = false;
|
|
503
|
+
|
|
504
|
+
return result;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Read the depth buffer and ask whether the two triangles' planes agree along the edge they
|
|
509
|
+
* share, and whether the depth a pixel is written with is stable frame to frame.
|
|
510
|
+
*
|
|
511
|
+
* Both halves matter and they answer different questions. A *step* at the seam would mean the
|
|
512
|
+
* rasterizer interpolated the shared edge two different ways, which is what the brief's
|
|
513
|
+
* candidate chain needs in order to start. *Instability* across frames is what a depth-driven
|
|
514
|
+
* history rejection would actually be reacting to — and with the camera still, the only thing
|
|
515
|
+
* that moves a pixel's depth is the sub-pixel jitter, so the two triangles should wobble by the
|
|
516
|
+
* same amount unless something about them differs.
|
|
517
|
+
*
|
|
518
|
+
* @param {number} [frames] how many frames of depth to compare
|
|
519
|
+
* @returns {Promise<object>}
|
|
520
|
+
*/
|
|
521
|
+
async function probe_depth(frames = 16) {
|
|
522
|
+
paused = true;
|
|
523
|
+
|
|
524
|
+
await measurement.settle(Number(element('settle').value) | 0);
|
|
525
|
+
|
|
526
|
+
const first = await measurement.read_depth();
|
|
527
|
+
|
|
528
|
+
const { width, height } = first;
|
|
529
|
+
|
|
530
|
+
const { side, side_flipped } = classify_ground(camera, width, height, ground_extent * 0.5);
|
|
531
|
+
|
|
532
|
+
const triangulation = element('triangulation').value;
|
|
533
|
+
const labels = triangulation === 'flipped' ? side_flipped : side;
|
|
534
|
+
|
|
535
|
+
const distance = screen_line_distance(
|
|
536
|
+
camera, width, height,
|
|
537
|
+
triangulation === 'flipped' ? [1, 1] : [1, -1]
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
const step = depth_step_across_seam(first.depth, labels, distance, width, height);
|
|
541
|
+
|
|
542
|
+
// per-pixel spread of depth across a burst, split the same way the colour spread is
|
|
543
|
+
const min = Float32Array.from(first.depth);
|
|
544
|
+
const max = Float32Array.from(first.depth);
|
|
545
|
+
|
|
546
|
+
for (let i = 1; i < frames; i++) {
|
|
547
|
+
measurement.frame();
|
|
548
|
+
|
|
549
|
+
const { depth } = await measurement.read_depth();
|
|
550
|
+
|
|
551
|
+
for (let p = 0; p < depth.length; p++) {
|
|
552
|
+
if (depth[p] < min[p]) {
|
|
553
|
+
min[p] = depth[p];
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (depth[p] > max[p]) {
|
|
557
|
+
max[p] = depth[p];
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const wobble = { negative: 0, positive: 0 };
|
|
563
|
+
const counts = { negative: 0, positive: 0 };
|
|
564
|
+
|
|
565
|
+
for (let p = 0; p < min.length; p++) {
|
|
566
|
+
if (labels[p] === 0) {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const bucket = labels[p] < 0 ? 'negative' : 'positive';
|
|
571
|
+
|
|
572
|
+
// relative to the depth itself: an absolute wobble means nothing without the magnitude
|
|
573
|
+
// it sits on, and depth here runs over two orders of magnitude across the frame
|
|
574
|
+
if (max[p] > 0) {
|
|
575
|
+
wobble[bucket] += (max[p] - min[p]) / max[p];
|
|
576
|
+
counts[bucket]++;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const result = {
|
|
581
|
+
resolution: [width, height],
|
|
582
|
+
triangulation,
|
|
583
|
+
segments: Number(element('segments').value),
|
|
584
|
+
depth_range: [Math.min(...first.depth.subarray(0, 4096)), Math.max(...first.depth.subarray(0, 4096))],
|
|
585
|
+
step,
|
|
586
|
+
depth_wobble: {
|
|
587
|
+
negative: counts.negative > 0 ? wobble.negative / counts.negative : 0,
|
|
588
|
+
positive: counts.positive > 0 ? wobble.positive / counts.positive : 0,
|
|
589
|
+
},
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
globalThis.__last_depth = result;
|
|
593
|
+
|
|
594
|
+
paused = false;
|
|
595
|
+
|
|
596
|
+
return result;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
globalThis.probe_depth = probe_depth;
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Two captures of the same record at two phases: the temporal filter's input and its output.
|
|
603
|
+
*
|
|
604
|
+
* Registered separately rather than re-phased in place — the registry resolves ordering at
|
|
605
|
+
* registration, so a phase assigned afterwards would be ignored.
|
|
606
|
+
*
|
|
607
|
+
* @type {Record<string, CaptureSceneColor>}
|
|
608
|
+
*/
|
|
609
|
+
const captures = {
|
|
610
|
+
pre_taa: new CaptureSceneColor(renderer.device),
|
|
611
|
+
post_taa: new CaptureSceneColor(renderer.device),
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
captures.pre_taa.name = 'ground_seam/capture pre-TAA';
|
|
615
|
+
captures.pre_taa.phase = FramePhase.AfterTransparency;
|
|
616
|
+
|
|
617
|
+
captures.post_taa.name = 'ground_seam/capture post-TAA';
|
|
618
|
+
captures.post_taa.phase = FramePhase.BeforePresent;
|
|
619
|
+
|
|
620
|
+
renderer.add_extension(captures.pre_taa);
|
|
621
|
+
renderer.add_extension(captures.post_taa);
|
|
622
|
+
|
|
623
|
+
const capture_scene_color = captures.pre_taa;
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* The same burst measurement, taken on the temporal filter's **input** instead of its output.
|
|
627
|
+
*
|
|
628
|
+
* This is the experiment that splits the seam in two. If the pre-TAA frame already differs
|
|
629
|
+
* across the edge — one triangle's shading swinging more from jitter phase to jitter phase than
|
|
630
|
+
* the other's — then the fault is in what the material pass reconstructs per triangle, and TAA
|
|
631
|
+
* is only making a real difference visible. If the pre-TAA variation is flat across the edge
|
|
632
|
+
* and the post-TAA one is not, the shading agrees and the temporal filter is treating the two
|
|
633
|
+
* triangles differently.
|
|
634
|
+
*
|
|
635
|
+
* The capture is HDR and linear, upstream of the tonemap, so there is no 8-bit floor under it
|
|
636
|
+
* the way there is under the colour readback.
|
|
637
|
+
*
|
|
638
|
+
* @param {number} [frames]
|
|
639
|
+
* @returns {Promise<object>}
|
|
640
|
+
*/
|
|
641
|
+
async function probe_pre_taa(frames = 16, stage = 'pre_taa') {
|
|
642
|
+
paused = true;
|
|
643
|
+
|
|
644
|
+
const capture_scene_color = captures[stage];
|
|
645
|
+
|
|
646
|
+
capture_scene_color.enabled = true;
|
|
647
|
+
|
|
648
|
+
try {
|
|
649
|
+
await measurement.settle(Number(element('settle').value) | 0);
|
|
650
|
+
|
|
651
|
+
const first = await capture_scene_color.read_luminance();
|
|
652
|
+
|
|
653
|
+
const { width, height } = first;
|
|
654
|
+
|
|
655
|
+
const min = Float32Array.from(first.luma);
|
|
656
|
+
const max = Float32Array.from(first.luma);
|
|
657
|
+
const total = Float64Array.from(first.luma);
|
|
658
|
+
|
|
659
|
+
/*
|
|
660
|
+
Total variation along the burst — the sum of the absolute frame-to-frame steps. A range
|
|
661
|
+
cannot tell a value that sweeps once from one that alternates every frame, and those
|
|
662
|
+
two defeat a neighbourhood clamp very differently: the clamp rebuilds its box from the
|
|
663
|
+
current frame, so a value that jumps to the far end each frame keeps the history outside
|
|
664
|
+
the box while a slow sweep never does. Divided by the range it is 1 for a single sweep
|
|
665
|
+
and (frames − 1) for a full alternation.
|
|
666
|
+
*/
|
|
667
|
+
const variation = new Float64Array(first.luma.length);
|
|
668
|
+
|
|
669
|
+
let previous = Float32Array.from(first.luma);
|
|
670
|
+
|
|
671
|
+
for (let i = 1; i < frames; i++) {
|
|
672
|
+
measurement.frame();
|
|
673
|
+
|
|
674
|
+
const { luma } = await capture_scene_color.read_luminance();
|
|
675
|
+
|
|
676
|
+
for (let p = 0; p < luma.length; p++) {
|
|
677
|
+
if (luma[p] < min[p]) {
|
|
678
|
+
min[p] = luma[p];
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (luma[p] > max[p]) {
|
|
682
|
+
max[p] = luma[p];
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
total[p] += luma[p];
|
|
686
|
+
variation[p] += Math.abs(luma[p] - previous[p]);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
previous = luma;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const { side, side_flipped } = classify_ground(camera, width, height, ground_extent * 0.5);
|
|
693
|
+
|
|
694
|
+
const triangulation = element('triangulation').value;
|
|
695
|
+
const labels = triangulation === 'flipped' ? side_flipped : side;
|
|
696
|
+
|
|
697
|
+
const distance = screen_line_distance(
|
|
698
|
+
camera, width, height,
|
|
699
|
+
triangulation === 'flipped' ? [1, 1] : [1, -1]
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
/*
|
|
703
|
+
The 3×3 standard deviation of the *last* frame captured, which is the quantity TAA's
|
|
704
|
+
neighbourhood box is built from: `taa_history_color_bounding_box_YCoCg` takes the mean
|
|
705
|
+
and σ over exactly this footprint and clips the history to `mean ± γ·σ`. How hard the
|
|
706
|
+
history gets clipped — and therefore how much of the jittering current frame is let
|
|
707
|
+
through — is the ratio of the frame-to-frame swing to that σ, so it is measured beside
|
|
708
|
+
it rather than left to be inferred.
|
|
709
|
+
*/
|
|
710
|
+
const { luma: last } = await capture_scene_color.read_luminance();
|
|
711
|
+
|
|
712
|
+
const sigma = new Float32Array(width * height);
|
|
713
|
+
|
|
714
|
+
for (let py = 1; py < height - 1; py++) {
|
|
715
|
+
for (let px = 1; px < width - 1; px++) {
|
|
716
|
+
let m1 = 0;
|
|
717
|
+
let m2 = 0;
|
|
718
|
+
|
|
719
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
720
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
721
|
+
const v = last[(py + dy) * width + px + dx];
|
|
722
|
+
|
|
723
|
+
m1 += v;
|
|
724
|
+
m2 += v * v;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
m1 /= 9;
|
|
729
|
+
m2 /= 9;
|
|
730
|
+
|
|
731
|
+
sigma[py * width + px] = Math.sqrt(Math.max(0, m2 - m1 * m1));
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const buckets = {
|
|
736
|
+
negative: { relative: 0, count: 0, luma: 0, sigma: 0, jaggedness: 0 },
|
|
737
|
+
positive: { relative: 0, count: 0, luma: 0, sigma: 0, jaggedness: 0 },
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
const RANGE = 20;
|
|
741
|
+
|
|
742
|
+
const band_total = new Float64Array(2 * RANGE);
|
|
743
|
+
const band_sigma = new Float64Array(2 * RANGE);
|
|
744
|
+
const band_jagged = new Float64Array(2 * RANGE);
|
|
745
|
+
const band_count = new Float64Array(2 * RANGE);
|
|
746
|
+
|
|
747
|
+
for (let py = 1; py < height - 1; py++) {
|
|
748
|
+
for (let px = 1; px < width - 1; px++) {
|
|
749
|
+
const p = py * width + px;
|
|
750
|
+
|
|
751
|
+
if (labels[p] === 0) {
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const m = total[p] / frames;
|
|
756
|
+
|
|
757
|
+
if (!(m > 1e-6)) {
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const swing = max[p] - min[p];
|
|
762
|
+
const relative = swing / m;
|
|
763
|
+
const relative_sigma = sigma[p] / m;
|
|
764
|
+
|
|
765
|
+
/*
|
|
766
|
+
How many times the value crosses its own range over the burst. 1 is a single
|
|
767
|
+
sweep, `frames − 1` is a full alternation every frame. This is the shape a
|
|
768
|
+
range cannot see and the one a neighbourhood clamp cares about most.
|
|
769
|
+
*/
|
|
770
|
+
const jaggedness = swing > 1e-9 ? variation[p] / swing : 0;
|
|
771
|
+
|
|
772
|
+
const bucket = labels[p] < 0 ? buckets.negative : buckets.positive;
|
|
773
|
+
|
|
774
|
+
bucket.relative += relative;
|
|
775
|
+
bucket.luma += m;
|
|
776
|
+
bucket.sigma += relative_sigma;
|
|
777
|
+
bucket.jaggedness += jaggedness;
|
|
778
|
+
bucket.count++;
|
|
779
|
+
|
|
780
|
+
const bin = Math.floor(distance(px, py)) + RANGE;
|
|
781
|
+
|
|
782
|
+
if (bin >= 0 && bin < band_total.length) {
|
|
783
|
+
band_total[bin] += relative;
|
|
784
|
+
band_sigma[bin] += relative_sigma;
|
|
785
|
+
band_jagged[bin] += jaggedness;
|
|
786
|
+
band_count[bin]++;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
for (const bucket of Object.values(buckets)) {
|
|
792
|
+
bucket.mean_relative = bucket.count > 0 ? bucket.relative / bucket.count : 0;
|
|
793
|
+
bucket.mean_luma = bucket.count > 0 ? bucket.luma / bucket.count : 0;
|
|
794
|
+
bucket.mean_sigma = bucket.count > 0 ? bucket.sigma / bucket.count : 0;
|
|
795
|
+
bucket.mean_jaggedness = bucket.count > 0 ? bucket.jaggedness / bucket.count : 0;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const profile = [];
|
|
799
|
+
const profile_sigma = [];
|
|
800
|
+
const profile_jagged = [];
|
|
801
|
+
|
|
802
|
+
for (let i = 0; i < band_total.length; i++) {
|
|
803
|
+
profile.push(band_count[i] > 0 ? band_total[i] / band_count[i] : null);
|
|
804
|
+
profile_sigma.push(band_count[i] > 0 ? band_sigma[i] / band_count[i] : null);
|
|
805
|
+
profile_jagged.push(band_count[i] > 0 ? band_jagged[i] / band_count[i] : null);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const result = {
|
|
809
|
+
resolution: [width, height],
|
|
810
|
+
stage,
|
|
811
|
+
triangulation,
|
|
812
|
+
segments: Number(element('segments').value),
|
|
813
|
+
frames,
|
|
814
|
+
negative: buckets.negative,
|
|
815
|
+
positive: buckets.positive,
|
|
816
|
+
ratio: buckets.positive.mean_relative > 0
|
|
817
|
+
? buckets.negative.mean_relative / buckets.positive.mean_relative
|
|
818
|
+
: null,
|
|
819
|
+
sigma_ratio: buckets.positive.mean_sigma > 0
|
|
820
|
+
? buckets.negative.mean_sigma / buckets.positive.mean_sigma
|
|
821
|
+
: null,
|
|
822
|
+
jaggedness_ratio: buckets.positive.mean_jaggedness > 0
|
|
823
|
+
? buckets.negative.mean_jaggedness / buckets.positive.mean_jaggedness
|
|
824
|
+
: null,
|
|
825
|
+
profile: {
|
|
826
|
+
offset: -RANGE,
|
|
827
|
+
relative: profile,
|
|
828
|
+
sigma: profile_sigma,
|
|
829
|
+
jaggedness: profile_jagged,
|
|
830
|
+
},
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
globalThis.__last_pre_taa = result;
|
|
834
|
+
|
|
835
|
+
return result;
|
|
836
|
+
} finally {
|
|
837
|
+
capture_scene_color.enabled = false;
|
|
838
|
+
paused = false;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
globalThis.probe_pre_taa = probe_pre_taa;
|
|
843
|
+
|
|
844
|
+
element('measure').addEventListener('click', () => {
|
|
845
|
+
element('measure').disabled = true;
|
|
846
|
+
|
|
847
|
+
measure()
|
|
848
|
+
.catch(error => {
|
|
849
|
+
report(String(error?.stack ?? error));
|
|
850
|
+
})
|
|
851
|
+
.finally(() => {
|
|
852
|
+
element('measure').disabled = false;
|
|
853
|
+
});
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
globalThis.measure = measure;
|
|
857
|
+
|
|
858
|
+
// ---- frame loop -------------------------------------------------------------------------
|
|
859
|
+
|
|
860
|
+
apply_settings();
|
|
861
|
+
|
|
862
|
+
create_frame_loop(time_delta_s => {
|
|
863
|
+
if (paused) {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
renderer.pixel_ratio = 1;
|
|
868
|
+
renderer.resize(VIEWPORT.width, VIEWPORT.height);
|
|
869
|
+
|
|
870
|
+
canvas.style.width = `${VIEWPORT.width}px`;
|
|
871
|
+
canvas.style.height = `${VIEWPORT.height}px`;
|
|
872
|
+
|
|
873
|
+
camera.aspect = VIEWPORT.width / VIEWPORT.height;
|
|
874
|
+
|
|
875
|
+
renderer.render(camera, scene, time_delta_s);
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
report('ready — press Measure');
|
|
879
|
+
}
|