@woosh/meep-engine 3.11.1 → 3.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
- package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +294 -290
- package/src/engine/graphics3/ParticipatingMedia.d.ts +111 -0
- package/src/engine/graphics3/ParticipatingMedia.d.ts.map +1 -0
- package/src/engine/graphics3/ParticipatingMedia.js +203 -0
- package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.d.ts +34 -0
- package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.d.ts.map +1 -0
- package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.js +73 -0
- package/src/engine/graphics3/ParticipatingMediaSystem3.d.ts +87 -0
- package/src/engine/graphics3/ParticipatingMediaSystem3.d.ts.map +1 -0
- package/src/engine/graphics3/ParticipatingMediaSystem3.js +184 -0
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/shade/playground/volumetrics_froxel/README.md +104 -1
- package/src/shade/playground/volumetrics_froxel/index.html +20 -1
- package/src/shade/playground/volumetrics_froxel/main.d.ts.map +1 -1
- package/src/shade/playground/volumetrics_froxel/main.js +298 -0
- package/src/shade/playground/volumetrics_froxel/measure.d.ts +119 -0
- package/src/shade/playground/volumetrics_froxel/measure.d.ts.map +1 -0
- package/src/shade/playground/volumetrics_froxel/measure.js +559 -0
- package/src/shade/renderer/Renderer.d.ts.map +1 -1
- package/src/shade/renderer/Renderer.js +22 -18
- package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
- package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.d.ts +14 -13
- package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.d.ts.map +1 -1
- package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.js +133 -129
- package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +480 -315
- package/src/shade/renderer/volumetrics/NOTES.md +649 -8
- package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts +5 -0
- package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.js +21 -2
- package/src/shade/renderer/volumetrics/SceneVolumetrics.d.ts +26 -0
- package/src/shade/renderer/volumetrics/SceneVolumetrics.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/SceneVolumetrics.js +64 -21
- package/src/shade/renderer/volumetrics/VOLUMETRICS_METADATA_STRUCT.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/VOLUMETRICS_METADATA_STRUCT.js +84 -28
- package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.d.ts +98 -0
- package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.js +111 -0
- package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.d.ts +84 -0
- package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.js +252 -0
- package/src/shade/renderer/volumetrics/chunk_integrate_optical_depth.js +75 -75
- package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.d.ts +43 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.js +145 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.d.ts +20 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.js +48 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.d.ts +38 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.js +107 -0
- package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts +4 -1
- package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +577 -471
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js +64 -13
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.d.ts +8 -0
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.js +174 -0
- package/src/shade/wgsl/emulator/CPUBitmapData.d.ts +25 -0
- package/src/shade/wgsl/emulator/CPUBitmapData.d.ts.map +1 -1
- package/src/shade/wgsl/emulator/CPUBitmapData.js +33 -0
- package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +2221 -2215
- package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
- package/src/shade/wgsl/emulator/WGSLLanguageCore.js +1297 -1117
- package/src/shade/wgsl/emulator/cpu_texture_gather.d.ts +69 -0
- package/src/shade/wgsl/emulator/cpu_texture_gather.d.ts.map +1 -0
- package/src/shade/wgsl/emulator/cpu_texture_gather.js +148 -0
- package/src/shade/wgsl/emulator/cpu_texture_sample.d.ts +16 -0
- package/src/shade/wgsl/emulator/cpu_texture_sample.d.ts.map +1 -1
- package/src/shade/wgsl/emulator/cpu_texture_sample.js +176 -172
|
@@ -0,0 +1,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
|
-
* @
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
case "
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
}
|