@woosh/meep-engine 2.138.17 → 2.138.18

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 CHANGED
@@ -5,7 +5,7 @@
5
5
  "description": "Pure JavaScript game engine. Fully featured and production ready.",
6
6
  "type": "module",
7
7
  "author": "Alexander Goldring",
8
- "version": "2.138.17",
8
+ "version": "2.138.18",
9
9
  "main": "build/meep.module.js",
10
10
  "module": "build/meep.module.js",
11
11
  "exports": {
@@ -1 +1 @@
1
- {"version":3,"file":"HeightMap2AOMap.d.ts","sourceRoot":"","sources":["../../../../../src/engine/ecs/grid/HeightMap2AOMap.js"],"names":[],"mappings":";AA6BA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qEAPW,SAAS,cACT,OAAO,cACP,OAAO,aAEP,MAAM,GACJ,SAAS,CA6CrB;0BAxFyB,6CAA6C;wBAJiB,OAAO"}
1
+ {"version":3,"file":"HeightMap2AOMap.d.ts","sourceRoot":"","sources":["../../../../../src/engine/ecs/grid/HeightMap2AOMap.js"],"names":[],"mappings":";AA6BA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qEAPW,SAAS,cACT,OAAO,cACP,OAAO,aAEP,MAAM,GACJ,SAAS,CA8CrB;0BAzFyB,6CAA6C;wBAJiB,OAAO"}
@@ -57,9 +57,10 @@ function heightMap2AOMap(
57
57
  const width = resultSize.x;
58
58
  const height = resultSize.y;
59
59
 
60
- // Pre-pass: max-height pyramid for Hi-Z ray traversal
60
+ // Pre-pass: max-height pyramid for Hi-Z ray traversal.
61
+ // The pyramid is a DataTexture2DArray; each Hi-Z mip is one layer.
61
62
  const pyramid_texture = build_max_height_pyramid(heightMap);
62
- const max_mip_level = pyramid_texture.mipmaps.length - 1;
63
+ const max_mip_level = pyramid_texture.userData.mipCount - 1;
63
64
 
64
65
  // AO bake
65
66
  const shader_ao = AmbientOcclusionShader({ numRays: rayCount });
@@ -1 +1 @@
1
- {"version":3,"file":"AmbientOcclusionShader.d.ts","sourceRoot":"","sources":["../../../../../src/engine/graphics/shaders/AmbientOcclusionShader.js"],"names":[],"mappings":";AAUA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH;IAFyB,OAAO,GAArB,MAAM;;;;;;;;;;;;;;;;;;;;;;EA6ShB;wBAhX8B,OAAO"}
1
+ {"version":3,"file":"AmbientOcclusionShader.d.ts","sourceRoot":"","sources":["../../../../../src/engine/graphics/shaders/AmbientOcclusionShader.js"],"names":[],"mappings":";AAUA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH;IAFyB,OAAO,GAArB,MAAM;;;;;;;;;;;;;;;;;;;;;;EA4ThB;wBA/X8B,OAAO"}
@@ -93,10 +93,15 @@ function AmbientOcclusionShader({ numRays = DEFAULT_NUM_RAYS } = {}) {
93
93
 
94
94
  fragmentShader: `
95
95
  // three.js auto-prefixes float/int precision; declare highp for
96
- // the heightmap sampler so R32F reads keep full precision
97
- precision highp sampler2D;
96
+ // the heightmap sampler so R32F reads keep full precision.
97
+ //
98
+ // The pyramid is stored as a DataTexture2DArray with one mip per
99
+ // layer (all at full mip-0 resolution; smaller mips are nearest-
100
+ // upscaled). This sidesteps a three.js r136 upload bug for
101
+ // DataTexture custom mipmap chains — see build_max_height_pyramid.
102
+ precision highp sampler2DArray;
98
103
 
99
- uniform sampler2D heightMap;
104
+ uniform sampler2DArray heightMap;
100
105
  uniform vec2 worldSize;
101
106
  uniform vec2 heightMapSize;
102
107
  uniform int maxMipLevel;
@@ -217,8 +222,18 @@ function AmbientOcclusionShader({ numRays = DEFAULT_NUM_RAYS } = {}) {
217
222
  // Heightmap utilities
218
223
  // ============================================================
219
224
 
225
+ // Sample mip-0 (the original heightmap) at uv. Layer 0 of the
226
+ // 2D array holds the un-reduced heightmap data.
220
227
  float sample_height(vec2 uv) {
221
- return textureLod(heightMap, uv, 0.0).r;
228
+ return texture(heightMap, vec3(uv, 0.0)).r;
229
+ }
230
+
231
+ // Sample max-height at a specific Hi-Z level. Mip k lives in
232
+ // layer k, nearest-upscaled to mip-0 resolution. NearestFilter
233
+ // on the array snaps the layer to integer 'mip' and returns
234
+ // the underlying mip-k texel containing uv.
235
+ float sample_max_height(vec2 uv, int mip) {
236
+ return texture(heightMap, vec3(uv, float(mip))).r;
222
237
  }
223
238
 
224
239
  // Central-difference normal in world space. Faster than building
@@ -293,7 +308,7 @@ function AmbientOcclusionShader({ numRays = DEFAULT_NUM_RAYS } = {}) {
293
308
  float t_exit = min(t_to_boundary.x, t_to_boundary.y) + T_EPS;
294
309
 
295
310
  // Max occluder height in this cell at this mip
296
- float max_h = textureLod(heightMap, cell_center, float(mip)).r;
311
+ float max_h = sample_max_height(cell_center, mip);
297
312
 
298
313
  // Ray height is linear in t, so the minimum across the
299
314
  // cell is at one of the endpoints
@@ -1,25 +1,32 @@
1
1
  /**
2
- * Build a max-reduction mipmap pyramid (Hi-Z–style height buffer) from a
3
- * single-channel float heightmap sampler.
2
+ * Build a max-reduction Hi-Z height pyramid from a single-channel float
3
+ * heightmap sampler.
4
4
  *
5
- * Each mip level k holds, per texel, the maximum height across the 2×2 block
6
- * of level k-1 it covers. A ray trace can then skip whole cells safely: if
7
- * the ray's height is strictly above the cell's max-height at any mip, the
8
- * cell can be advanced past without checking any finer level.
5
+ * Each pyramid level k holds, per texel, the maximum height across the 2×2
6
+ * block of level k-1 it covers. A ray trace can then skip whole cells
7
+ * safely: if the ray's height is strictly above the cell's max-height at
8
+ * any level, the cell can be advanced past without checking any finer level.
9
9
  *
10
- * The returned texture is configured for explicit-LOD sampling
11
- * (`textureLod(tex, uv, mip)`): NearestMipmapNearestFilter + NearestFilter,
12
- * generateMipmaps disabled, custom mipmap chain assigned to `tex.mipmaps`.
10
+ * Storage layout **DataTexture2DArray, one mip per layer**, all layers at
11
+ * full mip-0 resolution. Smaller mips are nearest-upscaled into their layer.
12
+ * The shader samples mip k via `texture(heightMap, vec3(uv, float(k)))`.
13
13
  *
14
- * The level-0 storage is shared (no copy) with `sampler.data`. Higher levels
15
- * are freshly allocated Float32Array data.
14
+ * This sidesteps the three.js r136 bug in `WebGLTextures.uploadTexture`
15
+ * where DataTexture custom mipmap chains are uploaded with `texSubImage2D`
16
+ * called with level hardcoded to 0 — every "mip" smashes the previous one
17
+ * into level 0 and the actual mip levels 1..N are left uninitialized. The
18
+ * 2DArray upload path uses one `texSubImage3D` call to level 0 covering
19
+ * all layers, so it works correctly.
20
+ *
21
+ * Memory cost is (mip_count × W × H × 4) bytes versus (≈ 4/3 × W × H × 4)
22
+ * for a true mip pyramid — about a 3× overhead, traded for correctness.
16
23
  *
17
24
  * Non-power-of-two heightmaps are handled by halving with floor and stopping
18
25
  * once both dimensions reach 1.
19
26
  *
20
27
  * @param {Sampler2D} sampler single-channel Float32 heightmap (itemSize=1)
21
- * @returns {DataTexture} R32F texture with custom mipmap chain
28
+ * @returns {DataTexture2DArray} R32F array texture; depth = mip count
22
29
  */
23
- export function build_max_height_pyramid(sampler: Sampler2D): DataTexture;
24
- import { DataTexture } from "three";
30
+ export function build_max_height_pyramid(sampler: Sampler2D): DataTexture2DArray;
31
+ import { DataTexture2DArray } from "three";
25
32
  //# sourceMappingURL=build_max_height_pyramid.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"build_max_height_pyramid.d.ts","sourceRoot":"","sources":["../../../../../src/engine/graphics/util/build_max_height_pyramid.js"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,8DAFa,WAAW,CAsEvB;4BA5FM,OAAO"}
1
+ {"version":3,"file":"build_max_height_pyramid.d.ts","sourceRoot":"","sources":["../../../../../src/engine/graphics/util/build_max_height_pyramid.js"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,8DAFa,kBAAkB,CA2G9B;mCAxIM,OAAO"}
@@ -1,100 +1,143 @@
1
1
  import {
2
2
  ClampToEdgeWrapping,
3
- DataTexture,
3
+ DataTexture2DArray,
4
4
  FloatType,
5
5
  NearestFilter,
6
- NearestMipmapNearestFilter,
7
6
  RedFormat
8
7
  } from "three";
9
8
 
10
9
  /**
11
- * Build a max-reduction mipmap pyramid (Hi-Z–style height buffer) from a
12
- * single-channel float heightmap sampler.
10
+ * Build a max-reduction Hi-Z height pyramid from a single-channel float
11
+ * heightmap sampler.
13
12
  *
14
- * Each mip level k holds, per texel, the maximum height across the 2×2 block
15
- * of level k-1 it covers. A ray trace can then skip whole cells safely: if
16
- * the ray's height is strictly above the cell's max-height at any mip, the
17
- * cell can be advanced past without checking any finer level.
13
+ * Each pyramid level k holds, per texel, the maximum height across the 2×2
14
+ * block of level k-1 it covers. A ray trace can then skip whole cells
15
+ * safely: if the ray's height is strictly above the cell's max-height at
16
+ * any level, the cell can be advanced past without checking any finer level.
18
17
  *
19
- * The returned texture is configured for explicit-LOD sampling
20
- * (`textureLod(tex, uv, mip)`): NearestMipmapNearestFilter + NearestFilter,
21
- * generateMipmaps disabled, custom mipmap chain assigned to `tex.mipmaps`.
18
+ * Storage layout **DataTexture2DArray, one mip per layer**, all layers at
19
+ * full mip-0 resolution. Smaller mips are nearest-upscaled into their layer.
20
+ * The shader samples mip k via `texture(heightMap, vec3(uv, float(k)))`.
22
21
  *
23
- * The level-0 storage is shared (no copy) with `sampler.data`. Higher levels
24
- * are freshly allocated Float32Array data.
22
+ * This sidesteps the three.js r136 bug in `WebGLTextures.uploadTexture`
23
+ * where DataTexture custom mipmap chains are uploaded with `texSubImage2D`
24
+ * called with level hardcoded to 0 — every "mip" smashes the previous one
25
+ * into level 0 and the actual mip levels 1..N are left uninitialized. The
26
+ * 2DArray upload path uses one `texSubImage3D` call to level 0 covering
27
+ * all layers, so it works correctly.
28
+ *
29
+ * Memory cost is (mip_count × W × H × 4) bytes versus (≈ 4/3 × W × H × 4)
30
+ * for a true mip pyramid — about a 3× overhead, traded for correctness.
25
31
  *
26
32
  * Non-power-of-two heightmaps are handled by halving with floor and stopping
27
33
  * once both dimensions reach 1.
28
34
  *
29
35
  * @param {Sampler2D} sampler single-channel Float32 heightmap (itemSize=1)
30
- * @returns {DataTexture} R32F texture with custom mipmap chain
36
+ * @returns {DataTexture2DArray} R32F array texture; depth = mip count
31
37
  */
32
38
  export function build_max_height_pyramid(sampler) {
33
39
  const w0 = sampler.width;
34
40
  const h0 = sampler.height;
35
41
 
42
+ // Step 1: build the conventional mip pyramid in JS
36
43
  /** @type {Array<{data: Float32Array, width: number, height: number}>} */
37
- const mipmaps = [];
38
-
39
- // Level 0: reference original data without copying — the heightmap is the
40
- // ground truth and this saves the allocation
41
- let prevW = w0;
42
- let prevH = h0;
43
- let prevData = sampler.data;
44
-
45
- mipmaps.push({ data: prevData, width: prevW, height: prevH });
46
-
47
- while (prevW > 1 || prevH > 1) {
48
- const curW = Math.max(1, prevW >> 1);
49
- const curH = Math.max(1, prevH >> 1);
50
- const curData = new Float32Array(curW * curH);
51
-
52
- // Source rows/cols beyond the last valid index clamp to the edge so
53
- // a dimension that hits 1 early (and stays at 1 while the other
54
- // shrinks) folds rows together rather than reading garbage
55
- const lastSrcX = prevW - 1;
56
- const lastSrcY = prevH - 1;
57
-
58
- for (let y = 0; y < curH; y++) {
59
- const sy0 = y << 1;
60
- const sy1 = Math.min(sy0 + 1, lastSrcY);
61
- const row0 = sy0 * prevW;
62
- const row1 = sy1 * prevW;
63
-
64
- for (let x = 0; x < curW; x++) {
65
- const sx0 = x << 1;
66
- const sx1 = Math.min(sx0 + 1, lastSrcX);
67
-
68
- const a = prevData[row0 + sx0];
69
- const b = prevData[row0 + sx1];
70
- const c = prevData[row1 + sx0];
71
- const d = prevData[row1 + sx1];
72
-
73
- let m = a > b ? a : b;
74
- if (c > m) m = c;
75
- if (d > m) m = d;
76
-
77
- curData[y * curW + x] = m;
44
+ const mips = [];
45
+ mips.push({ data: sampler.data, width: w0, height: h0 });
46
+
47
+ {
48
+ let prevW = w0;
49
+ let prevH = h0;
50
+ let prevData = sampler.data;
51
+
52
+ while (prevW > 1 || prevH > 1) {
53
+ const curW = Math.max(1, prevW >> 1);
54
+ const curH = Math.max(1, prevH >> 1);
55
+ const curData = new Float32Array(curW * curH);
56
+
57
+ const lastSrcX = prevW - 1;
58
+ const lastSrcY = prevH - 1;
59
+
60
+ for (let y = 0; y < curH; y++) {
61
+ const sy0 = y << 1;
62
+ const sy1 = Math.min(sy0 + 1, lastSrcY);
63
+ const row0 = sy0 * prevW;
64
+ const row1 = sy1 * prevW;
65
+
66
+ for (let x = 0; x < curW; x++) {
67
+ const sx0 = x << 1;
68
+ const sx1 = Math.min(sx0 + 1, lastSrcX);
69
+
70
+ const a = prevData[row0 + sx0];
71
+ const b = prevData[row0 + sx1];
72
+ const c = prevData[row1 + sx0];
73
+ const d = prevData[row1 + sx1];
74
+
75
+ let m = a > b ? a : b;
76
+ if (c > m) m = c;
77
+ if (d > m) m = d;
78
+
79
+ curData[y * curW + x] = m;
80
+ }
78
81
  }
82
+
83
+ mips.push({ data: curData, width: curW, height: curH });
84
+
85
+ prevW = curW;
86
+ prevH = curH;
87
+ prevData = curData;
88
+ }
89
+ }
90
+
91
+ // Step 2: pack each mip nearest-upscaled to w0 × h0 as a layer of the
92
+ // array texture. Layer k holds mip k blown up; sampling with NearestFilter
93
+ // at any uv returns mip k's value at that uv exactly.
94
+ const depth = mips.length;
95
+ const layerSize = w0 * h0;
96
+ const layered = new Float32Array(depth * layerSize);
97
+
98
+ for (let k = 0; k < depth; k++) {
99
+ const mip = mips[k];
100
+ const mw = mip.width;
101
+ const mh = mip.height;
102
+ const mdata = mip.data;
103
+
104
+ const layerOffset = k * layerSize;
105
+
106
+ if (mw === w0 && mh === h0) {
107
+ // Level 0 — direct copy
108
+ layered.set(mdata, layerOffset);
109
+ continue;
79
110
  }
80
111
 
81
- mipmaps.push({ data: curData, width: curW, height: curH });
112
+ // Nearest-upscale: dst (x, y) → src (floor(x * mw / w0), floor(y * mh / h0))
113
+ // Works for both POT and NPO2 reductions.
114
+ for (let y = 0; y < h0; y++) {
115
+ const sy = Math.min(mh - 1, ((y * mh) / h0) | 0);
116
+ const srcRow = sy * mw;
117
+ const dstRow = layerOffset + y * w0;
82
118
 
83
- prevW = curW;
84
- prevH = curH;
85
- prevData = curData;
119
+ for (let x = 0; x < w0; x++) {
120
+ const sx = Math.min(mw - 1, ((x * mw) / w0) | 0);
121
+ layered[dstRow + x] = mdata[srcRow + sx];
122
+ }
123
+ }
86
124
  }
87
125
 
88
- const tex = new DataTexture(mipmaps[0].data, w0, h0, RedFormat, FloatType);
89
- tex.mipmaps = mipmaps;
90
- tex.generateMipmaps = false;
91
- tex.minFilter = NearestMipmapNearestFilter;
126
+ const tex = new DataTexture2DArray(layered, w0, h0, depth);
127
+ tex.format = RedFormat;
128
+ tex.type = FloatType;
129
+ tex.internalFormat = 'R32F';
130
+ tex.minFilter = NearestFilter;
92
131
  tex.magFilter = NearestFilter;
93
132
  tex.wrapS = ClampToEdgeWrapping;
94
133
  tex.wrapT = ClampToEdgeWrapping;
95
- tex.internalFormat = 'R32F';
134
+ tex.generateMipmaps = false;
96
135
  tex.flipY = false;
97
136
  tex.needsUpdate = true;
98
137
 
138
+ // Expose the depth so callers can plumb `maxMipLevel` to the shader
139
+ // without re-deriving it
140
+ tex.userData.mipCount = depth;
141
+
99
142
  return tex;
100
143
  }