@woosh/meep-engine 3.11.1 → 3.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/package.json +1 -1
  2. package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
  3. package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +294 -290
  4. package/src/engine/graphics3/ParticipatingMedia.d.ts +111 -0
  5. package/src/engine/graphics3/ParticipatingMedia.d.ts.map +1 -0
  6. package/src/engine/graphics3/ParticipatingMedia.js +203 -0
  7. package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.d.ts +34 -0
  8. package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.d.ts.map +1 -0
  9. package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.js +73 -0
  10. package/src/engine/graphics3/ParticipatingMediaSystem3.d.ts +87 -0
  11. package/src/engine/graphics3/ParticipatingMediaSystem3.d.ts.map +1 -0
  12. package/src/engine/graphics3/ParticipatingMediaSystem3.js +184 -0
  13. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  14. package/src/shade/playground/volumetrics_froxel/README.md +104 -1
  15. package/src/shade/playground/volumetrics_froxel/index.html +20 -1
  16. package/src/shade/playground/volumetrics_froxel/main.d.ts.map +1 -1
  17. package/src/shade/playground/volumetrics_froxel/main.js +298 -0
  18. package/src/shade/playground/volumetrics_froxel/measure.d.ts +119 -0
  19. package/src/shade/playground/volumetrics_froxel/measure.d.ts.map +1 -0
  20. package/src/shade/playground/volumetrics_froxel/measure.js +559 -0
  21. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  22. package/src/shade/renderer/Renderer.js +22 -18
  23. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  24. package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.d.ts +14 -13
  25. package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.d.ts.map +1 -1
  26. package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.js +133 -129
  27. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
  28. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +480 -315
  29. package/src/shade/renderer/volumetrics/NOTES.md +649 -8
  30. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts +5 -0
  31. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts.map +1 -1
  32. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.js +21 -2
  33. package/src/shade/renderer/volumetrics/SceneVolumetrics.d.ts +26 -0
  34. package/src/shade/renderer/volumetrics/SceneVolumetrics.d.ts.map +1 -1
  35. package/src/shade/renderer/volumetrics/SceneVolumetrics.js +64 -21
  36. package/src/shade/renderer/volumetrics/VOLUMETRICS_METADATA_STRUCT.d.ts.map +1 -1
  37. package/src/shade/renderer/volumetrics/VOLUMETRICS_METADATA_STRUCT.js +84 -28
  38. package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.d.ts +98 -0
  39. package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.d.ts.map +1 -0
  40. package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.js +111 -0
  41. package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.d.ts +84 -0
  42. package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.d.ts.map +1 -0
  43. package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.js +252 -0
  44. package/src/shade/renderer/volumetrics/chunk_integrate_optical_depth.js +75 -75
  45. package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.d.ts +43 -0
  46. package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.d.ts.map +1 -0
  47. package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.js +145 -0
  48. package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.d.ts +20 -0
  49. package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.d.ts.map +1 -0
  50. package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.js +48 -0
  51. package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.d.ts +38 -0
  52. package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.d.ts.map +1 -0
  53. package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.js +107 -0
  54. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts +4 -1
  55. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts.map +1 -1
  56. package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +577 -471
  57. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts.map +1 -1
  58. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js +64 -13
  59. package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.d.ts +8 -0
  60. package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.d.ts.map +1 -0
  61. package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.js +174 -0
  62. package/src/shade/wgsl/emulator/CPUBitmapData.d.ts +25 -0
  63. package/src/shade/wgsl/emulator/CPUBitmapData.d.ts.map +1 -1
  64. package/src/shade/wgsl/emulator/CPUBitmapData.js +33 -0
  65. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +2221 -2215
  66. package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
  67. package/src/shade/wgsl/emulator/WGSLLanguageCore.js +1297 -1117
  68. package/src/shade/wgsl/emulator/cpu_texture_gather.d.ts +69 -0
  69. package/src/shade/wgsl/emulator/cpu_texture_gather.d.ts.map +1 -0
  70. package/src/shade/wgsl/emulator/cpu_texture_gather.js +148 -0
  71. package/src/shade/wgsl/emulator/cpu_texture_sample.d.ts +16 -0
  72. package/src/shade/wgsl/emulator/cpu_texture_sample.d.ts.map +1 -1
  73. package/src/shade/wgsl/emulator/cpu_texture_sample.js +176 -172
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Read the four texels of a bilinear footprint out of a {@link CPUBitmapData} without
3
+ * blending them — the CPU side of WGSL's `textureGather` / `textureGatherCompare`.
4
+ *
5
+ * **The footprint is the linear filter's footprint.** `cpu_texture_sample` derives it as
6
+ * `px = texel_coord - 0.5`, then `floor(px)` and `floor(px) + 1` on each axis, each index
7
+ * passed through {@link wrap_int} for the address mode. A gather covers exactly those four
8
+ * texels, so this function reuses that arithmetic rather than restating it.
9
+ *
10
+ * **`magFilter` is deliberately ignored.** A gather always returns four texels — the
11
+ * sampler's filter mode selects nothing, because no filtering happens. This is the one
12
+ * place `cpu_texture_sample` cannot simply be called: under `magFilter: "nearest"` it
13
+ * collapses to a single texel, which is right for a sample and wrong for a gather. The
14
+ * address modes still apply, and they are the only part of the sampler this reads.
15
+ *
16
+ * **Mipmaps are not modelled — LOD 0 only.** Same stance, and for the same reason, as
17
+ * {@link cpu_texture_sample}; see its header. `textureGather` has no LOD parameter in WGSL
18
+ * anyway, so here the restriction costs nothing.
19
+ *
20
+ * Convention for `texel_coord`, as in {@link cpu_texture_sample}: **texel-space**
21
+ * coordinates (UV * size), not normalised UV. The shader-facing intrinsics in
22
+ * {@link WGSLLanguageCore} do the conversion before calling.
23
+ *
24
+ * Component order is WGSL's, counter-clockwise from the lower-left texel — the same order
25
+ * D3D and Vulkan use:
26
+ *
27
+ * ```
28
+ * output[0] = (u_min, v_max) lower-left
29
+ * output[1] = (u_max, v_max) lower-right
30
+ * output[2] = (u_max, v_min) upper-right
31
+ * output[3] = (u_min, v_min) upper-left
32
+ * ```
33
+ *
34
+ * so `gathered.wzxy` is the row-major (x0y0, x1y0, x0y1, x1y1) order a bilinear blend
35
+ * wants — which is exactly the swizzle `shadowmap_sample_filtered` applies before handing
36
+ * the result to `interpolate_bilinear_f32`.
37
+ *
38
+ * @param {Float32Array | number[]} output — receives the 4 gathered texel values
39
+ * @param {import("../../descriptor/texture/SamplerDescriptor.js").SamplerDescriptor} sampler
40
+ * @param {import("./CPUBitmapData.js").CPUBitmapData} texture
41
+ * @param {Float32Array | number[]} texel_coord — `[x, y]` in texel space
42
+ * @param {number} [component] — channel to read, 0..3. Forced to 0 for a depth texture,
43
+ * which has a single channel and whose WGSL overloads take no component argument.
44
+ * @param {number} [offset_x] — const `vec2<i32>` offset, applied to the integer texel
45
+ * indices *before* the address mode, per WGSL
46
+ * @param {number} [offset_y]
47
+ * @param {number} [layer] — array slice for the `_2d_array` forms; clamped to the
48
+ * texture's depth
49
+ */
50
+ export function cpu_texture_gather(output: Float32Array | number[], sampler: import("../../descriptor/texture/SamplerDescriptor.js").SamplerDescriptor, texture: import("./CPUBitmapData.js").CPUBitmapData, texel_coord: Float32Array | number[], component?: number, offset_x?: number, offset_y?: number, layer?: number): void;
51
+ /**
52
+ * Evaluate a `GPUCompareFunction` the way a comparison sampler does.
53
+ *
54
+ * **Argument order matters and is easy to invert.** WebGPU evaluates
55
+ * `compare(reference, sampled)` — the shader's `depth_ref` is the LEFT operand, the value
56
+ * read out of the depth texture is the right one. So under `"greater"` (what the engine's
57
+ * {@link SHADOWMAP_COMPARE_SAMPLER_DESCRIPTOR} uses, because shadow maps are reverse-Z) a
58
+ * receiver passes — result 1.0, "lit" — when its own depth is *greater* than the stored
59
+ * occluder depth, i.e. when it is nearer to the light. Swapping the operands inverts every
60
+ * shadow in the engine while still producing plausible-looking output, which is why
61
+ * `cpu_texture_gather.spec.js` pins the direction explicitly.
62
+ *
63
+ * @param {GPUCompareFunction} compare
64
+ * @param {number} reference — the shader's `depth_ref`
65
+ * @param {number} sampled — the texel read from the depth texture
66
+ * @returns {number} 1.0 when the comparison passes, 0.0 when it fails
67
+ */
68
+ export function evaluate_gpu_compare_function(compare: GPUCompareFunction, reference: number, sampled: number): number;
69
+ //# sourceMappingURL=cpu_texture_gather.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cpu_texture_gather.d.ts","sourceRoot":"","sources":["../../../../../src/shade/wgsl/emulator/cpu_texture_gather.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,2CAZW,YAAY,GAAG,MAAM,EAAE,WACvB,OAAO,+CAA+C,EAAE,iBAAiB,WACzE,OAAO,oBAAoB,EAAE,aAAa,eAC1C,YAAY,GAAG,MAAM,EAAE,cACvB,MAAM,aAEN,MAAM,aAEN,MAAM,UACN,MAAM,QAqChB;AAwBD;;;;;;;;;;;;;;;;GAgBG;AACH,sFAJW,MAAM,WACN,MAAM,GACJ,MAAM,CAuBlB"}
@@ -0,0 +1,148 @@
1
+ import { wrap_int } from "./cpu_texture_sample.js";
2
+
3
+ /**
4
+ * Read the four texels of a bilinear footprint out of a {@link CPUBitmapData} without
5
+ * blending them — the CPU side of WGSL's `textureGather` / `textureGatherCompare`.
6
+ *
7
+ * **The footprint is the linear filter's footprint.** `cpu_texture_sample` derives it as
8
+ * `px = texel_coord - 0.5`, then `floor(px)` and `floor(px) + 1` on each axis, each index
9
+ * passed through {@link wrap_int} for the address mode. A gather covers exactly those four
10
+ * texels, so this function reuses that arithmetic rather than restating it.
11
+ *
12
+ * **`magFilter` is deliberately ignored.** A gather always returns four texels — the
13
+ * sampler's filter mode selects nothing, because no filtering happens. This is the one
14
+ * place `cpu_texture_sample` cannot simply be called: under `magFilter: "nearest"` it
15
+ * collapses to a single texel, which is right for a sample and wrong for a gather. The
16
+ * address modes still apply, and they are the only part of the sampler this reads.
17
+ *
18
+ * **Mipmaps are not modelled — LOD 0 only.** Same stance, and for the same reason, as
19
+ * {@link cpu_texture_sample}; see its header. `textureGather` has no LOD parameter in WGSL
20
+ * anyway, so here the restriction costs nothing.
21
+ *
22
+ * Convention for `texel_coord`, as in {@link cpu_texture_sample}: **texel-space**
23
+ * coordinates (UV * size), not normalised UV. The shader-facing intrinsics in
24
+ * {@link WGSLLanguageCore} do the conversion before calling.
25
+ *
26
+ * Component order is WGSL's, counter-clockwise from the lower-left texel — the same order
27
+ * D3D and Vulkan use:
28
+ *
29
+ * ```
30
+ * output[0] = (u_min, v_max) lower-left
31
+ * output[1] = (u_max, v_max) lower-right
32
+ * output[2] = (u_max, v_min) upper-right
33
+ * output[3] = (u_min, v_min) upper-left
34
+ * ```
35
+ *
36
+ * so `gathered.wzxy` is the row-major (x0y0, x1y0, x0y1, x1y1) order a bilinear blend
37
+ * wants — which is exactly the swizzle `shadowmap_sample_filtered` applies before handing
38
+ * the result to `interpolate_bilinear_f32`.
39
+ *
40
+ * @param {Float32Array | number[]} output — receives the 4 gathered texel values
41
+ * @param {import("../../descriptor/texture/SamplerDescriptor.js").SamplerDescriptor} sampler
42
+ * @param {import("./CPUBitmapData.js").CPUBitmapData} texture
43
+ * @param {Float32Array | number[]} texel_coord — `[x, y]` in texel space
44
+ * @param {number} [component] — channel to read, 0..3. Forced to 0 for a depth texture,
45
+ * which has a single channel and whose WGSL overloads take no component argument.
46
+ * @param {number} [offset_x] — const `vec2<i32>` offset, applied to the integer texel
47
+ * indices *before* the address mode, per WGSL
48
+ * @param {number} [offset_y]
49
+ * @param {number} [layer] — array slice for the `_2d_array` forms; clamped to the
50
+ * texture's depth
51
+ */
52
+ export function cpu_texture_gather(
53
+ output,
54
+ sampler,
55
+ texture,
56
+ texel_coord,
57
+ component = 0,
58
+ offset_x = 0,
59
+ offset_y = 0,
60
+ layer = 0
61
+ ) {
62
+ const w = texture.size[0];
63
+ const h = texture.size[1];
64
+ const d = texture.size[2];
65
+
66
+ // A depth texture is single-channel; its gather overloads carry no component argument.
67
+ const channel = texture.is_depth === true ? 0 : (component | 0);
68
+
69
+ // Same derivation as the linear branch of `cpu_texture_sample`: shift by half a texel so
70
+ // the footprint is anchored on texel CENTRES, then floor.
71
+ const x_floor = Math.floor(texel_coord[0] - 0.5) + (offset_x | 0);
72
+ const y_floor = Math.floor(texel_coord[1] - 0.5) + (offset_y | 0);
73
+
74
+ const x0 = wrap_int(x_floor, w, sampler.addressModeU);
75
+ const x1 = wrap_int(x_floor + 1, w, sampler.addressModeU);
76
+ const y0 = wrap_int(y_floor, h, sampler.addressModeV);
77
+ const y1 = wrap_int(y_floor + 1, h, sampler.addressModeV);
78
+
79
+ // The array index is not an address-mode axis — WGSL clamps it to the layer count.
80
+ const z = d <= 0 ? 0 : Math.min(Math.max(layer | 0, 0), d - 1);
81
+
82
+ output[0] = read_channel(texture, x0, y1, z, channel);
83
+ output[1] = read_channel(texture, x1, y1, z, channel);
84
+ output[2] = read_channel(texture, x1, y0, z, channel);
85
+ output[3] = read_channel(texture, x0, y0, z, channel);
86
+ }
87
+
88
+ /**
89
+ * One channel of one texel. Channels past the texture's `item_size` follow the same
90
+ * convention `cpu_texture_sample` uses for its output vector: zero, with alpha defaulting
91
+ * to 1 — matching WGSL's rule for formats with fewer than four channels.
92
+ *
93
+ * Coordinates arrive already wrapped by {@link wrap_int}, so no bounds check is needed.
94
+ *
95
+ * @param {import("./CPUBitmapData.js").CPUBitmapData} texture
96
+ * @param {number} x
97
+ * @param {number} y
98
+ * @param {number} z
99
+ * @param {number} component
100
+ * @returns {number}
101
+ */
102
+ function read_channel(texture, x, y, z, component) {
103
+ if (component >= texture.item_size) {
104
+ return component === 3 ? 1 : 0;
105
+ }
106
+
107
+ return texture.data[texture.pixel_offset(x, y, z) + component];
108
+ }
109
+
110
+ /**
111
+ * Evaluate a `GPUCompareFunction` the way a comparison sampler does.
112
+ *
113
+ * **Argument order matters and is easy to invert.** WebGPU evaluates
114
+ * `compare(reference, sampled)` — the shader's `depth_ref` is the LEFT operand, the value
115
+ * read out of the depth texture is the right one. So under `"greater"` (what the engine's
116
+ * {@link SHADOWMAP_COMPARE_SAMPLER_DESCRIPTOR} uses, because shadow maps are reverse-Z) a
117
+ * receiver passes — result 1.0, "lit" — when its own depth is *greater* than the stored
118
+ * occluder depth, i.e. when it is nearer to the light. Swapping the operands inverts every
119
+ * shadow in the engine while still producing plausible-looking output, which is why
120
+ * `cpu_texture_gather.spec.js` pins the direction explicitly.
121
+ *
122
+ * @param {GPUCompareFunction} compare
123
+ * @param {number} reference — the shader's `depth_ref`
124
+ * @param {number} sampled — the texel read from the depth texture
125
+ * @returns {number} 1.0 when the comparison passes, 0.0 when it fails
126
+ */
127
+ export function evaluate_gpu_compare_function(compare, reference, sampled) {
128
+ switch (compare) {
129
+ case "never":
130
+ return 0;
131
+ case "less":
132
+ return reference < sampled ? 1 : 0;
133
+ case "equal":
134
+ return reference === sampled ? 1 : 0;
135
+ case "less-equal":
136
+ return reference <= sampled ? 1 : 0;
137
+ case "greater":
138
+ return reference > sampled ? 1 : 0;
139
+ case "not-equal":
140
+ return reference !== sampled ? 1 : 0;
141
+ case "greater-equal":
142
+ return reference >= sampled ? 1 : 0;
143
+ case "always":
144
+ return 1;
145
+ default:
146
+ throw new Error(`evaluate_gpu_compare_function: unsupported compare function '${compare}' (expected one of "never", "less", "equal", "less-equal", "greater", "not-equal", "greater-equal", "always")`);
147
+ }
148
+ }
@@ -36,4 +36,20 @@
36
36
  * `texture.dimensionality` (1, 2 or 3 entries)
37
37
  */
38
38
  export function cpu_texture_sample(output: Float32Array | number[], sampler: import("../../descriptor/texture/SamplerDescriptor.js").SamplerDescriptor, texture: import("./CPUBitmapData.js").CPUBitmapData, texel_coord: Float32Array | number[]): void;
39
+ /**
40
+ * Wrap an integer texel index back into `[0, size)` using the address
41
+ * mode. Shared by both nearest and linear filtering — the linear filter
42
+ * passes both the floor and floor+1 indices through this so wrapping at
43
+ * the edge is consistent.
44
+ *
45
+ * Exported because {@link cpu_texture_gather} needs the *same* address
46
+ * arithmetic: a gather's 2x2 footprint is the linear filter's footprint,
47
+ * and a second copy of this switch would be free to drift from it.
48
+ *
49
+ * @param {number} i
50
+ * @param {number} size
51
+ * @param {GPUAddressMode} mode
52
+ * @returns {number}
53
+ */
54
+ export function wrap_int(i: number, size: number, mode: GPUAddressMode): number;
39
55
  //# sourceMappingURL=cpu_texture_sample.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cpu_texture_sample.d.ts","sourceRoot":"","sources":["../../../../../src/shade/wgsl/emulator/cpu_texture_sample.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,2CARW,YAAY,GAAG,MAAM,EAAE,WAGvB,OAAO,+CAA+C,EAAE,iBAAiB,WACzE,OAAO,oBAAoB,EAAE,aAAa,eAC1C,YAAY,GAAG,MAAM,EAAE,QAsGjC"}
1
+ {"version":3,"file":"cpu_texture_sample.d.ts","sourceRoot":"","sources":["../../../../../src/shade/wgsl/emulator/cpu_texture_sample.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,2CARW,YAAY,GAAG,MAAM,EAAE,WAGvB,OAAO,+CAA+C,EAAE,iBAAiB,WACzE,OAAO,oBAAoB,EAAE,aAAa,eAC1C,YAAY,GAAG,MAAM,EAAE,QAsGjC;AAED;;;;;;;;;;;;;;GAcG;AACH,4BALW,MAAM,QACN,MAAM,yBAEJ,MAAM,CAsBlB"}
@@ -1,172 +1,176 @@
1
- import { assert } from "../../../core/assert.js";
2
-
3
- /**
4
- * Sample a {@link CPUBitmapData} as if it were a GPU texture bound with
5
- * the given sampler. Routes through nearest or linear filtering on each of
6
- * the texture's actual axes (1D / 2D / 3D inferred from
7
- * `texture.dimensionality`).
8
- *
9
- * **Mipmaps are intentionally not modelled.** The emulator runs at LOD 0
10
- * only — there is no mip pyramid in {@link CPUBitmapData}, and
11
- * `mipmapFilter` / `lodMinClamp` / `lodMaxClamp` on the sampler are
12
- * ignored. We pick `magFilter` for the actual filter mode, which matches
13
- * the intuition that emulator tests are typically up-sampling. Add a mip
14
- * pyramid + minification path later if a test ever needs minified
15
- * sampling.
16
- *
17
- * Convention for `texel_coord`: this is **TEXEL-SPACE coordinates** (i.e.
18
- * UV * size, NOT normalised UV in [0, 1]). The shader-facing intrinsics
19
- * `textureSample` / `textureSampleLevel` take normalised UV; the emulator
20
- * converts to texel-space before calling here so the address-mode
21
- * arithmetic doesn't have to special-case sub-1.0 jitter. WebGPU's
22
- * convention places texel `i`'s centre at texel-space `i + 0.5`, so the
23
- * left edge of the texture is at 0 and the right edge at `size`.
24
- *
25
- * Address-mode handling lives in {@link wrap_int}: every integer texel
26
- * index produced by either filter passes through it before reading the
27
- * backing buffer. Out-of-range fractional inputs fall out naturally from
28
- * floor + integer wrap, so we don't pre-clamp the fractional coord (a
29
- * previous version did and lost full-edge texel sampling for
30
- * `clamp-to-edge`).
31
- *
32
- * @param {Float32Array | number[]} output — receives 4 channel values
33
- * (channels beyond `texture.item_size` are zero, with alpha forced to 1
34
- * for textures with fewer than 4 channels — matches WGSL semantics)
35
- * @param {import("../../descriptor/texture/SamplerDescriptor.js").SamplerDescriptor} sampler
36
- * @param {import("./CPUBitmapData.js").CPUBitmapData} texture
37
- * @param {Float32Array | number[]} texel_coord — length matches
38
- * `texture.dimensionality` (1, 2 or 3 entries)
39
- */
40
- export function cpu_texture_sample(output, sampler, texture, texel_coord) {
41
- assert.defined(output, 'output');
42
- assert.defined(sampler, 'sampler');
43
- assert.defined(texture, 'texture');
44
- assert.defined(texel_coord, 'texel_coord');
45
-
46
- // Default channels 0..3 to 0; alpha to 1 if the texture has < 4 channels.
47
- const item_size = texture.item_size;
48
- for (let i = 0; i < 4; i++) output[i] = 0;
49
- if (item_size < 4) output[3] = 1;
50
-
51
- const dim = texture.dimensionality;
52
- const w = texture.size[0];
53
- const h = texture.size[1];
54
- const d = texture.size[2];
55
-
56
- const tx = texel_coord[0];
57
- const ty = dim >= 2 ? texel_coord[1] : 0;
58
- const tz = dim >= 3 ? texel_coord[2] : 0;
59
-
60
- // Mipmaps not modelled — see header. Use magFilter as the single
61
- // filter setting (sampler is bound at LOD 0 conceptually).
62
- const filter = sampler.magFilter;
63
-
64
- if (filter === "nearest") {
65
- // WGSL nearest: texel index = floor(texel_coord), then wrap.
66
- const ix = wrap_int(Math.floor(tx), w, sampler.addressModeU);
67
- const iy = dim >= 2 ? wrap_int(Math.floor(ty), h, sampler.addressModeV) : 0;
68
- const iz = dim >= 3 ? wrap_int(Math.floor(tz), d, sampler.addressModeW) : 0;
69
- const base = texture.pixel_offset(ix, iy, iz);
70
- const n = Math.min(item_size, 4);
71
- for (let i = 0; i < n; i++) output[i] = texture.data[base + i];
72
- return;
73
- }
74
-
75
- if (filter !== "linear") {
76
- throw new Error(`cpu_texture_sample: unsupported magFilter '${filter}' (expected "nearest" or "linear")`);
77
- }
78
-
79
- // Linear filter. Sample 2 (1D) / 4 (2D) / 8 (3D) neighbouring texels
80
- // around `texel_coord - 0.5` (the offset puts the bilinear origin at
81
- // texel CENTRES) with weights `(1 - frac, frac)` per axis. Each
82
- // integer index passes through `wrap_int` so the right/bottom/back
83
- // neighbour falling off the edge is resolved by the address mode.
84
- const px = tx - 0.5;
85
- const fx_floor = Math.floor(px);
86
- const ix0 = wrap_int(fx_floor, w, sampler.addressModeU);
87
- const ix1 = wrap_int(fx_floor + 1, w, sampler.addressModeU);
88
- const fx = px - fx_floor;
89
-
90
- let iy0 = 0, iy1 = 0, fy = 0;
91
- if (dim >= 2) {
92
- const py = ty - 0.5;
93
- const fy_floor = Math.floor(py);
94
- iy0 = wrap_int(fy_floor, h, sampler.addressModeV);
95
- iy1 = wrap_int(fy_floor + 1, h, sampler.addressModeV);
96
- fy = py - fy_floor;
97
- }
98
-
99
- let iz0 = 0, iz1 = 0, fz = 0;
100
- if (dim >= 3) {
101
- const pz = tz - 0.5;
102
- const fz_floor = Math.floor(pz);
103
- iz0 = wrap_int(fz_floor, d, sampler.addressModeW);
104
- iz1 = wrap_int(fz_floor + 1, d, sampler.addressModeW);
105
- fz = pz - fz_floor;
106
- }
107
-
108
- const n = Math.min(item_size, 4);
109
-
110
- // Unrolled 2-corner loop per active axis (max 8 corners total). Skip
111
- // zero-weight corners early — common when the fractional offset is
112
- // exactly an integer (texel-centre sample).
113
- const xs = [ix0, ix1];
114
- const wxs = [1 - fx, fx];
115
- const ys = [iy0, iy1];
116
- const wys = [1 - fy, fy];
117
- const zs = [iz0, iz1];
118
- const wzs = [1 - fz, fz];
119
-
120
- const y_count = dim >= 2 ? 2 : 1;
121
- const z_count = dim >= 3 ? 2 : 1;
122
-
123
- for (let kz = 0; kz < z_count; kz++) {
124
- const wz = z_count === 1 ? 1 : wzs[kz];
125
- const z = zs[kz];
126
- for (let ky = 0; ky < y_count; ky++) {
127
- const wy = y_count === 1 ? 1 : wys[ky];
128
- const y = ys[ky];
129
- for (let kx = 0; kx < 2; kx++) {
130
- const wx = wxs[kx];
131
- const x = xs[kx];
132
- const weight = wx * wy * wz;
133
- if (weight === 0) continue;
134
- const base = texture.pixel_offset(x, y, z);
135
- for (let i = 0; i < n; i++) output[i] += texture.data[base + i] * weight;
136
- }
137
- }
138
- }
139
- }
140
-
141
- /**
142
- * Wrap an integer texel index back into `[0, size)` using the address
143
- * mode. Shared by both nearest and linear filtering — the linear filter
144
- * passes both the floor and floor+1 indices through this so wrapping at
145
- * the edge is consistent.
146
- *
147
- * @param {number} i
148
- * @param {number} size
149
- * @param {GPUAddressMode} mode
150
- * @returns {number}
151
- */
152
- function wrap_int(i, size, mode) {
153
- if (size <= 0) return 0;
154
- switch (mode) {
155
- case "clamp-to-edge":
156
- if (i < 0) return 0;
157
- if (i >= size) return size - 1;
158
- return i;
159
- case "repeat":
160
- return ((i % size) + size) % size;
161
- case "mirror-repeat": {
162
- // fold the second half of the period back onto the first:
163
- // w in [size, 2*size) maps to period - 1 - w in [0, size)
164
- const period = size * 2;
165
- let w = ((i % period) + period) % period;
166
- if (w >= size) w = period - 1 - w;
167
- return w;
168
- }
169
- default:
170
- throw new Error(`cpu_texture_sample: unsupported address mode '${mode}' (expected "clamp-to-edge", "repeat", or "mirror-repeat")`);
171
- }
172
- }
1
+ import { assert } from "../../../core/assert.js";
2
+
3
+ /**
4
+ * Sample a {@link CPUBitmapData} as if it were a GPU texture bound with
5
+ * the given sampler. Routes through nearest or linear filtering on each of
6
+ * the texture's actual axes (1D / 2D / 3D inferred from
7
+ * `texture.dimensionality`).
8
+ *
9
+ * **Mipmaps are intentionally not modelled.** The emulator runs at LOD 0
10
+ * only — there is no mip pyramid in {@link CPUBitmapData}, and
11
+ * `mipmapFilter` / `lodMinClamp` / `lodMaxClamp` on the sampler are
12
+ * ignored. We pick `magFilter` for the actual filter mode, which matches
13
+ * the intuition that emulator tests are typically up-sampling. Add a mip
14
+ * pyramid + minification path later if a test ever needs minified
15
+ * sampling.
16
+ *
17
+ * Convention for `texel_coord`: this is **TEXEL-SPACE coordinates** (i.e.
18
+ * UV * size, NOT normalised UV in [0, 1]). The shader-facing intrinsics
19
+ * `textureSample` / `textureSampleLevel` take normalised UV; the emulator
20
+ * converts to texel-space before calling here so the address-mode
21
+ * arithmetic doesn't have to special-case sub-1.0 jitter. WebGPU's
22
+ * convention places texel `i`'s centre at texel-space `i + 0.5`, so the
23
+ * left edge of the texture is at 0 and the right edge at `size`.
24
+ *
25
+ * Address-mode handling lives in {@link wrap_int}: every integer texel
26
+ * index produced by either filter passes through it before reading the
27
+ * backing buffer. Out-of-range fractional inputs fall out naturally from
28
+ * floor + integer wrap, so we don't pre-clamp the fractional coord (a
29
+ * previous version did and lost full-edge texel sampling for
30
+ * `clamp-to-edge`).
31
+ *
32
+ * @param {Float32Array | number[]} output — receives 4 channel values
33
+ * (channels beyond `texture.item_size` are zero, with alpha forced to 1
34
+ * for textures with fewer than 4 channels — matches WGSL semantics)
35
+ * @param {import("../../descriptor/texture/SamplerDescriptor.js").SamplerDescriptor} sampler
36
+ * @param {import("./CPUBitmapData.js").CPUBitmapData} texture
37
+ * @param {Float32Array | number[]} texel_coord — length matches
38
+ * `texture.dimensionality` (1, 2 or 3 entries)
39
+ */
40
+ export function cpu_texture_sample(output, sampler, texture, texel_coord) {
41
+ assert.defined(output, 'output');
42
+ assert.defined(sampler, 'sampler');
43
+ assert.defined(texture, 'texture');
44
+ assert.defined(texel_coord, 'texel_coord');
45
+
46
+ // Default channels 0..3 to 0; alpha to 1 if the texture has < 4 channels.
47
+ const item_size = texture.item_size;
48
+ for (let i = 0; i < 4; i++) output[i] = 0;
49
+ if (item_size < 4) output[3] = 1;
50
+
51
+ const dim = texture.dimensionality;
52
+ const w = texture.size[0];
53
+ const h = texture.size[1];
54
+ const d = texture.size[2];
55
+
56
+ const tx = texel_coord[0];
57
+ const ty = dim >= 2 ? texel_coord[1] : 0;
58
+ const tz = dim >= 3 ? texel_coord[2] : 0;
59
+
60
+ // Mipmaps not modelled — see header. Use magFilter as the single
61
+ // filter setting (sampler is bound at LOD 0 conceptually).
62
+ const filter = sampler.magFilter;
63
+
64
+ if (filter === "nearest") {
65
+ // WGSL nearest: texel index = floor(texel_coord), then wrap.
66
+ const ix = wrap_int(Math.floor(tx), w, sampler.addressModeU);
67
+ const iy = dim >= 2 ? wrap_int(Math.floor(ty), h, sampler.addressModeV) : 0;
68
+ const iz = dim >= 3 ? wrap_int(Math.floor(tz), d, sampler.addressModeW) : 0;
69
+ const base = texture.pixel_offset(ix, iy, iz);
70
+ const n = Math.min(item_size, 4);
71
+ for (let i = 0; i < n; i++) output[i] = texture.data[base + i];
72
+ return;
73
+ }
74
+
75
+ if (filter !== "linear") {
76
+ throw new Error(`cpu_texture_sample: unsupported magFilter '${filter}' (expected "nearest" or "linear")`);
77
+ }
78
+
79
+ // Linear filter. Sample 2 (1D) / 4 (2D) / 8 (3D) neighbouring texels
80
+ // around `texel_coord - 0.5` (the offset puts the bilinear origin at
81
+ // texel CENTRES) with weights `(1 - frac, frac)` per axis. Each
82
+ // integer index passes through `wrap_int` so the right/bottom/back
83
+ // neighbour falling off the edge is resolved by the address mode.
84
+ const px = tx - 0.5;
85
+ const fx_floor = Math.floor(px);
86
+ const ix0 = wrap_int(fx_floor, w, sampler.addressModeU);
87
+ const ix1 = wrap_int(fx_floor + 1, w, sampler.addressModeU);
88
+ const fx = px - fx_floor;
89
+
90
+ let iy0 = 0, iy1 = 0, fy = 0;
91
+ if (dim >= 2) {
92
+ const py = ty - 0.5;
93
+ const fy_floor = Math.floor(py);
94
+ iy0 = wrap_int(fy_floor, h, sampler.addressModeV);
95
+ iy1 = wrap_int(fy_floor + 1, h, sampler.addressModeV);
96
+ fy = py - fy_floor;
97
+ }
98
+
99
+ let iz0 = 0, iz1 = 0, fz = 0;
100
+ if (dim >= 3) {
101
+ const pz = tz - 0.5;
102
+ const fz_floor = Math.floor(pz);
103
+ iz0 = wrap_int(fz_floor, d, sampler.addressModeW);
104
+ iz1 = wrap_int(fz_floor + 1, d, sampler.addressModeW);
105
+ fz = pz - fz_floor;
106
+ }
107
+
108
+ const n = Math.min(item_size, 4);
109
+
110
+ // Unrolled 2-corner loop per active axis (max 8 corners total). Skip
111
+ // zero-weight corners early — common when the fractional offset is
112
+ // exactly an integer (texel-centre sample).
113
+ const xs = [ix0, ix1];
114
+ const wxs = [1 - fx, fx];
115
+ const ys = [iy0, iy1];
116
+ const wys = [1 - fy, fy];
117
+ const zs = [iz0, iz1];
118
+ const wzs = [1 - fz, fz];
119
+
120
+ const y_count = dim >= 2 ? 2 : 1;
121
+ const z_count = dim >= 3 ? 2 : 1;
122
+
123
+ for (let kz = 0; kz < z_count; kz++) {
124
+ const wz = z_count === 1 ? 1 : wzs[kz];
125
+ const z = zs[kz];
126
+ for (let ky = 0; ky < y_count; ky++) {
127
+ const wy = y_count === 1 ? 1 : wys[ky];
128
+ const y = ys[ky];
129
+ for (let kx = 0; kx < 2; kx++) {
130
+ const wx = wxs[kx];
131
+ const x = xs[kx];
132
+ const weight = wx * wy * wz;
133
+ if (weight === 0) continue;
134
+ const base = texture.pixel_offset(x, y, z);
135
+ for (let i = 0; i < n; i++) output[i] += texture.data[base + i] * weight;
136
+ }
137
+ }
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Wrap an integer texel index back into `[0, size)` using the address
143
+ * mode. Shared by both nearest and linear filtering — the linear filter
144
+ * passes both the floor and floor+1 indices through this so wrapping at
145
+ * the edge is consistent.
146
+ *
147
+ * Exported because {@link cpu_texture_gather} needs the *same* address
148
+ * arithmetic: a gather's 2x2 footprint is the linear filter's footprint,
149
+ * and a second copy of this switch would be free to drift from it.
150
+ *
151
+ * @param {number} i
152
+ * @param {number} size
153
+ * @param {GPUAddressMode} mode
154
+ * @returns {number}
155
+ */
156
+ export function wrap_int(i, size, mode) {
157
+ if (size <= 0) return 0;
158
+ switch (mode) {
159
+ case "clamp-to-edge":
160
+ if (i < 0) return 0;
161
+ if (i >= size) return size - 1;
162
+ return i;
163
+ case "repeat":
164
+ return ((i % size) + size) % size;
165
+ case "mirror-repeat": {
166
+ // fold the second half of the period back onto the first:
167
+ // w in [size, 2*size) maps to period - 1 - w in [0, size)
168
+ const period = size * 2;
169
+ let w = ((i % period) + period) % period;
170
+ if (w >= size) w = period - 1 - w;
171
+ return w;
172
+ }
173
+ default:
174
+ throw new Error(`cpu_texture_sample: unsupported address mode '${mode}' (expected "clamp-to-edge", "repeat", or "mirror-repeat")`);
175
+ }
176
+ }