@woosh/meep-engine 2.138.16 → 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.
Files changed (54) hide show
  1. package/editor/Editor.d.ts.map +1 -1
  2. package/editor/SelectionVisualizer.d.ts.map +1 -1
  3. package/editor/actions/concrete/ComponentAddAction.d.ts.map +1 -1
  4. package/editor/actions/concrete/EntityCreateAction.d.ts.map +1 -1
  5. package/editor/actions/concrete/SelectionAddAction.d.ts.map +1 -1
  6. package/editor/enableEditor.d.ts.map +1 -1
  7. package/package.json +1 -1
  8. package/src/engine/ecs/grid/HeightMap2AOMap.d.ts +25 -0
  9. package/src/engine/ecs/grid/HeightMap2AOMap.d.ts.map +1 -0
  10. package/src/engine/ecs/grid/HeightMap2AOMap.js +96 -0
  11. package/src/engine/ecs/terrain/ecs/BuildLightTexture.d.ts +15 -7
  12. package/src/engine/ecs/terrain/ecs/BuildLightTexture.d.ts.map +1 -1
  13. package/src/engine/ecs/terrain/ecs/BuildLightTexture.js +44 -99
  14. package/src/engine/ecs/terrain/ecs/Terrain.d.ts.map +1 -1
  15. package/src/engine/ecs/terrain/ecs/Terrain.js +36 -0
  16. package/src/engine/ecs/terrain/ecs/splat/SplatMapping.d.ts +1 -1
  17. package/src/engine/ecs/terrain/ecs/splat/SplatMapping.d.ts.map +1 -1
  18. package/src/engine/ecs/terrain/ecs/splat/SplatMapping.js +3 -1
  19. package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts +12 -1
  20. package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts.map +1 -1
  21. package/src/engine/ecs/terrain/tiles/TerrainTileManager.js +21 -0
  22. package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderDepthV0.d.ts.map +1 -1
  23. package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderDepthV0.js +2 -14
  24. package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderLitV0.d.ts.map +1 -1
  25. package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderLitV0.js +1 -7
  26. package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderNormalsV0.d.ts.map +1 -1
  27. package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderNormalsV0.js +1 -6
  28. package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderViewportDepthV0.d.ts.map +1 -1
  29. package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderViewportDepthV0.js +1 -6
  30. package/src/engine/graphics/material/TerrainDepthMaterial.d.ts +36 -0
  31. package/src/engine/graphics/material/TerrainDepthMaterial.d.ts.map +1 -0
  32. package/src/engine/graphics/material/TerrainDepthMaterial.js +65 -0
  33. package/src/engine/graphics/shaders/AmbientOcclusionShader.d.ts +69 -12
  34. package/src/engine/graphics/shaders/AmbientOcclusionShader.d.ts.map +1 -1
  35. package/src/engine/graphics/shaders/AmbientOcclusionShader.js +386 -128
  36. package/src/engine/graphics/util/build_max_height_pyramid.d.ts +32 -0
  37. package/src/engine/graphics/util/build_max_height_pyramid.d.ts.map +1 -0
  38. package/src/engine/graphics/util/build_max_height_pyramid.js +143 -0
  39. package/editor/ecs/component/FieldDescriptor.d.ts +0 -27
  40. package/editor/ecs/component/FieldDescriptor.d.ts.map +0 -1
  41. package/editor/ecs/component/FieldValueAdapter.d.ts +0 -7
  42. package/editor/ecs/component/FieldValueAdapter.d.ts.map +0 -1
  43. package/editor/ecs/component/createFieldEditor.d.ts +0 -9
  44. package/editor/ecs/component/createFieldEditor.d.ts.map +0 -1
  45. package/editor/ecs/component/createObjectEditor.d.ts +0 -14
  46. package/editor/ecs/component/createObjectEditor.d.ts.map +0 -1
  47. package/editor/ecs/component/findNearestRegisteredType.d.ts +0 -8
  48. package/editor/ecs/component/findNearestRegisteredType.d.ts.map +0 -1
  49. package/src/engine/ecs/grid/HeightMap2NormalMap.d.ts +0 -10
  50. package/src/engine/ecs/grid/HeightMap2NormalMap.d.ts.map +0 -1
  51. package/src/engine/ecs/grid/HeightMap2NormalMap.js +0 -72
  52. package/src/engine/ecs/grid/NormalMap2AOMap.d.ts +0 -15
  53. package/src/engine/ecs/grid/NormalMap2AOMap.d.ts.map +0 -1
  54. package/src/engine/ecs/grid/NormalMap2AOMap.js +0 -82
@@ -1,128 +1,386 @@
1
- import { Vector2 } from "three";
2
-
3
- const AmbientOcclusionShader = function () {
4
- return {
5
-
6
- uniforms: {
7
-
8
- "normalMap": { type: "t", value: null },
9
- "heightMap": { type: "t", value: null },
10
- "world_size": { type: "v2", value: new Vector2(512, 512) },
11
- "rayLength": { type: 'f', value: 17 }
12
-
13
- },
14
-
15
- defines: {
16
- 'NUM_SAMPLES': 64,
17
- 'NUM_RINGS': 7,
18
- },
19
-
20
- vertexShader: [
21
-
22
- "varying vec2 vUv;",
23
-
24
- "void main() {",
25
-
26
- "vUv = uv;",
27
- "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
28
-
29
- "}"
30
-
31
- ].join("\n"),
32
-
33
- fragmentShader: [
34
- `
35
- uniform sampler2D normalMap;
36
- uniform sampler2D heightMap;
37
- uniform float rayLength;
38
- uniform vec2 world_size;
39
-
40
- varying vec2 vUv;
41
-
42
- vec3 get(float x, float y){
43
- vec2 _uv = vUv.xy + vec2(x,y) / world_size;
44
- float h = texture2D(heightMap, _uv).x;
45
- return vec3( _uv.x * world_size.x, h, _uv.y * world_size.y );
46
- }
47
-
48
- float hash1( float n )
49
- {
50
- return fract( n*17.0*fract( n*0.3183099 ) );
51
- }
52
-
53
- float hash1( vec2 p )
54
- {
55
- p = 50.0*fract( p*0.3183099 );
56
- return fract( p.x*p.y*(p.x+p.y) );
57
- }
58
-
59
- // Non-sin based hash function, fast and has good randomness
60
- float hash12(vec2 p){
61
- vec3 p3 = fract(vec3(p.xyx) * .1031);
62
- p3 += dot(p3, p3.yzx + 33.33);
63
- return fract((p3.x + p3.y) * p3.z);
64
- }
65
-
66
- const float bias = 0.001;
67
-
68
- float pow2(float x){
69
- return x*x;
70
- }
71
-
72
- float getOcclusion(vec3 origin, vec3 normal, vec3 hit_position){
73
- vec3 viewDelta = hit_position - origin;
74
-
75
- float viewDistance = length( viewDelta );
76
-
77
- float vn = dot( normal, viewDelta );
78
- float a2 = (vn) / viewDistance - bias;
79
- float a1 = (1.0 + pow2( viewDistance ) );
80
-
81
- return max(0.0, a2) / a1;
82
- }
83
-
84
- const float PI2 = 6.28318530717958;
85
- const float kernelRadius = 100.0;
86
-
87
- const float ANGLE_STEP = PI2 * float( NUM_RINGS ) / float( NUM_SAMPLES );
88
- const float INV_NUM_SAMPLES = 1.0 / float( NUM_SAMPLES );
89
-
90
- float getAmbientOcclusion(vec3 world_position, vec3 world_normal){
91
- // jsfiddle that shows sample pattern: https://jsfiddle.net/a16ff1p7/
92
- float angle = hash12( vUv ) * PI2;
93
- vec2 radius = vec2( rayLength * INV_NUM_SAMPLES );
94
-
95
- float occlusionSum = 0.0;
96
- float weightSum = 0.0;
97
-
98
- for( int i = 0; i < NUM_SAMPLES; i ++ ) {
99
- vec2 sampleUv = vec2( cos( angle ), sin( angle ) ) * radius * float(i+1);
100
-
101
- angle += ANGLE_STEP;
102
-
103
- vec3 sample_pos = get(sampleUv.x, sampleUv.y);
104
-
105
- occlusionSum += getOcclusion(world_position, world_normal, sample_pos);
106
- weightSum += 1.0;
107
- }
108
-
109
- return occlusionSum/weightSum;
110
- }
111
-
112
- void main() {
113
- vec3 pos = get(0.0, 0.0);
114
-
115
- vec3 normal = texture2D( normalMap, vUv ).xzy;
116
-
117
- float occlusion = getAmbientOcclusion(pos, normal);
118
-
119
- float incident = 1.0 - occlusion;
120
-
121
- gl_FragColor = vec4(pow(incident,10.0), 0.0, 0.0, 1.0);
122
- }
123
- `
124
- ].join('\n')
125
-
126
- }
127
- };
128
- export default AmbientOcclusionShader;
1
+ import { GLSL3, Vector2 } from "three";
2
+
3
+ /**
4
+ * Default ray count for the AO baker. Higher = less noise, longer bake.
5
+ * Variance falls as 1/sqrt(NUM_RAYS), so doubling rays halves stdev.
6
+ *
7
+ * @type {number}
8
+ */
9
+ const DEFAULT_NUM_RAYS = 64;
10
+
11
+ /**
12
+ * Lightmap baker for heightfield terrain.
13
+ *
14
+ * Per output texel, traces `NUM_RAYS` cosine-weighted rays into the
15
+ * hemisphere around the surface normal and counts how many are occluded by
16
+ * the heightfield. The output channel is visibility ∈ [0, 1]:
17
+ * 1 = fully lit (no occlusion)
18
+ * 0 = fully occluded
19
+ *
20
+ * Architecture
21
+ * ------------
22
+ *
23
+ * 1. Pre-pass (orchestrator side): a max-reduction mipmap pyramid is built
24
+ * from the heightmap — see `build_max_height_pyramid`. Each mip stores
25
+ * per-cell upper bounds on height. The whole pyramid is one DataTexture
26
+ * with `tex.mipmaps[]`, sampled by `textureLod`.
27
+ *
28
+ * 2. Sampling: directions are drawn from a cosine-weighted hemisphere using
29
+ * Shirley's concentric disk mapping. Cosine-weighting matches the
30
+ * Lambertian diffuse assumption — each sample's contribution to the AO
31
+ * integral has unit weight, and the estimator collapses to a simple
32
+ * hits/total ratio.
33
+ *
34
+ * 3. Random seeding: a PCG hash (pcg3d on the fragment coordinate) seeds an
35
+ * independent stream per output texel. Subsequent rays draw from `pcg`
36
+ * on the running state. White noise — no Halton/blue-noise sequences —
37
+ * keeps the implementation simple and the noise spectrum unbiased; a
38
+ * bilateral denoise pass downstream cleans up the speckle.
39
+ *
40
+ * 4. Tracing: Hi-Z traversal in world space. The ray's XZ projection walks
41
+ * texture-space cells; per cell, the max-height at the current mip is
42
+ * compared against the ray's min height across the cell. Above max:
43
+ * advance to the next cell and try to coarsen the mip. Below max:
44
+ * refine the mip; at mip 0, record a hit. Bounded by MAX_HIZ_ITER per
45
+ * ray as a safety net for pathological convergence.
46
+ *
47
+ * World space
48
+ * -----------
49
+ *
50
+ * Convention: X and Z are horizontal (UV-aligned), Y is up. The heightmap's
51
+ * UV maps linearly to world (x, z) ∈ [0, worldSize.x] × [0, worldSize.y],
52
+ * and the heightmap value at any UV is the world Y coordinate of the
53
+ * surface. Surface normals are recomputed in-shader by central differences
54
+ * on the heightmap, removing any dependency on an external normal map.
55
+ *
56
+ * Inputs (uniforms — plumbing, not user-facing)
57
+ * ---------------------------------------------
58
+ *
59
+ * heightMap R32F texture with a max-reduction mipmap chain
60
+ * worldSize XZ extent of the terrain in world units
61
+ * heightMapSize Resolution of heightMap mip 0 in texels
62
+ * maxMipLevel Index of the coarsest mip in heightMap
63
+ *
64
+ * The single user-facing parameter is `NUM_RAYS`, configured by the factory
65
+ * argument.
66
+ *
67
+ * @param {Object} [opts]
68
+ * @param {number} [opts.numRays=64] number of rays per output texel
69
+ */
70
+ function AmbientOcclusionShader({ numRays = DEFAULT_NUM_RAYS } = {}) {
71
+ return {
72
+ uniforms: {
73
+ heightMap: { value: null },
74
+ worldSize: { value: new Vector2(1, 1) },
75
+ heightMapSize: { value: new Vector2(1, 1) },
76
+ maxMipLevel: { value: 0 }
77
+ },
78
+
79
+ defines: {
80
+ NUM_RAYS: numRays | 0
81
+ },
82
+
83
+ glslVersion: GLSL3,
84
+
85
+ vertexShader: `
86
+ out vec2 vUv;
87
+
88
+ void main() {
89
+ vUv = uv;
90
+ gl_Position = vec4((uv - 0.5) * 2.0, 0.0, 1.0);
91
+ }
92
+ `,
93
+
94
+ fragmentShader: `
95
+ // three.js auto-prefixes float/int precision; declare highp for
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;
103
+
104
+ uniform sampler2DArray heightMap;
105
+ uniform vec2 worldSize;
106
+ uniform vec2 heightMapSize;
107
+ uniform int maxMipLevel;
108
+
109
+ in vec2 vUv;
110
+ out vec4 fragColor;
111
+
112
+ const float PI = 3.14159265358979;
113
+
114
+ // Safety cap on Hi-Z iterations per ray. log2 of typical heightmap
115
+ // dimensions × a handful of refine/coarsen rounds per cell is
116
+ // usually under 80; 256 gives generous headroom.
117
+ const int MAX_HIZ_ITER = 256;
118
+
119
+ // ============================================================
120
+ // PCG random number generator
121
+ // ============================================================
122
+ // pcg is the standard 32-bit scrambler from O'Neill's family.
123
+ // pcg3d is the 3-vector variant used purely for seeding so the
124
+ // spatial pattern of per-texel seeds is uncorrelated.
125
+
126
+ uint rng_state;
127
+
128
+ uint pcg(uint v) {
129
+ uint state = v * 747796405u + 2891336453u;
130
+ uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
131
+ return (word >> 22u) ^ word;
132
+ }
133
+
134
+ uvec3 pcg3d(uvec3 v) {
135
+ v = v * 1664525u + 1013904223u;
136
+ v.x += v.y * v.z;
137
+ v.y += v.z * v.x;
138
+ v.z += v.x * v.y;
139
+ v ^= (v >> uvec3(16u));
140
+ v.x += v.y * v.z;
141
+ v.y += v.z * v.x;
142
+ v.z += v.x * v.y;
143
+ return v;
144
+ }
145
+
146
+ // Map a uint to [0, 1) by stuffing 23 bits into a float's mantissa
147
+ // and subtracting the implicit leading 1. Faster than divide and
148
+ // produces a uniform distribution.
149
+ float hash_to_float01(uint h) {
150
+ return uintBitsToFloat(0x3f800000u | (h >> 9u)) - 1.0;
151
+ }
152
+
153
+ uint random_uint() {
154
+ rng_state = pcg(rng_state);
155
+ return rng_state;
156
+ }
157
+
158
+ float random() {
159
+ return hash_to_float01(random_uint());
160
+ }
161
+
162
+ void random_initialize(uvec3 invocation_id, uvec3 seed) {
163
+ uvec3 mixed = pcg3d(invocation_id + seed * 37u);
164
+ rng_state = mixed.x ^ mixed.y ^ mixed.z;
165
+ }
166
+
167
+ // ============================================================
168
+ // Cosine-weighted hemisphere sampling
169
+ // ============================================================
170
+ // Shirley's concentric disk → cosine hemisphere via z = sqrt(1 - r²).
171
+ // The orthonormal basis from the normal uses Frisvad's branchless
172
+ // construction with z-flip for numerical stability near n.z = -1.
173
+
174
+ vec2 sample_uniform_disk_concentric(vec2 uv) {
175
+ vec2 offset = 2.0 * uv - vec2(1.0);
176
+ if (offset.x == 0.0 && offset.y == 0.0) {
177
+ return vec2(0.0);
178
+ }
179
+ float r;
180
+ float theta;
181
+ const float PI_4 = PI / 4.0;
182
+ const float PI_2 = PI / 2.0;
183
+ if (abs(offset.x) > abs(offset.y)) {
184
+ r = offset.x;
185
+ theta = PI_4 * (offset.y / offset.x);
186
+ } else {
187
+ r = offset.y;
188
+ theta = PI_2 - PI_4 * (offset.x / offset.y);
189
+ }
190
+ return r * vec2(cos(theta), sin(theta));
191
+ }
192
+
193
+ vec3 sample_cosine_weighted_hemisphere(vec2 uv) {
194
+ vec2 d = sample_uniform_disk_concentric(uv);
195
+ float z = sqrt(max(0.0, 1.0 - d.x * d.x - d.y * d.y));
196
+ return vec3(d.x, d.y, z);
197
+ }
198
+
199
+ mat3 build_orthonormal_matrix_n(vec3 n) {
200
+ vec3 T;
201
+ vec3 B;
202
+ if (n.z < 0.0) {
203
+ float a = 1.0 / (1.0 - n.z);
204
+ float b = n.x * n.y * a;
205
+ T = vec3(1.0 - n.x * n.x * a, -b, n.x);
206
+ B = vec3(b, n.y * n.y * a - 1.0, -n.y);
207
+ } else {
208
+ float a = 1.0 / (1.0 + n.z);
209
+ float b = -n.x * n.y * a;
210
+ T = vec3(1.0 - n.x * n.x * a, b, -n.x);
211
+ B = vec3(b, 1.0 - n.y * n.y * a, -n.y);
212
+ }
213
+ return mat3(T, B, n);
214
+ }
215
+
216
+ vec3 cosine_weighted_world_direction(vec2 uv, vec3 n) {
217
+ vec3 local = sample_cosine_weighted_hemisphere(uv);
218
+ return normalize(build_orthonormal_matrix_n(n) * local);
219
+ }
220
+
221
+ // ============================================================
222
+ // Heightmap utilities
223
+ // ============================================================
224
+
225
+ // Sample mip-0 (the original heightmap) at uv. Layer 0 of the
226
+ // 2D array holds the un-reduced heightmap data.
227
+ float sample_height(vec2 uv) {
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;
237
+ }
238
+
239
+ // Central-difference normal in world space. Faster than building
240
+ // a normal map up front and stays self-consistent with the
241
+ // heightmap used for tracing (no convention mismatch possible).
242
+ vec3 compute_world_normal(vec2 uv) {
243
+ vec2 texel_step = 1.0 / heightMapSize;
244
+ vec2 texel_world = worldSize / heightMapSize;
245
+
246
+ float h_l = sample_height(uv - vec2(texel_step.x, 0.0));
247
+ float h_r = sample_height(uv + vec2(texel_step.x, 0.0));
248
+ float h_d = sample_height(uv - vec2(0.0, texel_step.y));
249
+ float h_u = sample_height(uv + vec2(0.0, texel_step.y));
250
+
251
+ float dHdx = (h_r - h_l) / (2.0 * texel_world.x);
252
+ float dHdz = (h_u - h_d) / (2.0 * texel_world.y);
253
+
254
+ return normalize(vec3(-dHdx, 1.0, -dHdz));
255
+ }
256
+
257
+ // ============================================================
258
+ // Hi-Z ray traversal
259
+ // ============================================================
260
+ // Trace one ray against the max-height pyramid. The ray's XZ
261
+ // projection steps through texture-space cells; height is tracked
262
+ // separately along the ray parameter t (world units).
263
+ //
264
+ // Returns true iff the ray is occluded by the heightfield before
265
+ // exiting the terrain extent.
266
+
267
+ bool trace_ray_hiz(vec3 origin_w, vec3 dir_w) {
268
+ vec2 inv_world_size = 1.0 / worldSize;
269
+ vec2 origin_tex = origin_w.xz * inv_world_size;
270
+ vec2 dir_tex = dir_w.xz * inv_world_size;
271
+
272
+ // Reciprocal of dir_tex used to compute "world distance to
273
+ // next cell boundary". A zero component means the ray is
274
+ // axis-aligned in tex space — it never crosses that axis's
275
+ // boundaries — so route to a large finite value with sign
276
+ // matching step(0, dir) (= +1 at the zero crossing).
277
+ vec2 inv_dir_tex = vec2(
278
+ dir_tex.x == 0.0 ? 1e20 : 1.0 / dir_tex.x,
279
+ dir_tex.y == 0.0 ? 1e20 : 1.0 / dir_tex.y
280
+ );
281
+
282
+ // Step past the cell boundary by a tiny world-space epsilon
283
+ // so the next iteration sees the new cell, not the one we
284
+ // just exited.
285
+ const float T_EPS = 1e-4;
286
+
287
+ int mip = 0;
288
+ float t = 0.0;
289
+
290
+ for (int iter = 0; iter < MAX_HIZ_ITER; iter++) {
291
+ vec2 tex_pos = origin_tex + t * dir_tex;
292
+
293
+ // Exited the terrain extent — ray escaped, no occlusion
294
+ if (any(lessThan(tex_pos, vec2(0.0))) ||
295
+ any(greaterThan(tex_pos, vec2(1.0)))) {
296
+ return false;
297
+ }
298
+
299
+ // Cell extents at the current mip
300
+ float mip_scale = exp2(float(mip));
301
+ vec2 cell_size = vec2(mip_scale) / heightMapSize;
302
+ vec2 cell_origin = floor(tex_pos / cell_size) * cell_size;
303
+ vec2 cell_center = cell_origin + 0.5 * cell_size;
304
+
305
+ // World distance along the ray to the next cell boundary
306
+ vec2 next_boundary = cell_origin + step(0.0, dir_tex) * cell_size;
307
+ vec2 t_to_boundary = max((next_boundary - tex_pos) * inv_dir_tex, vec2(0.0));
308
+ float t_exit = min(t_to_boundary.x, t_to_boundary.y) + T_EPS;
309
+
310
+ // Max occluder height in this cell at this mip
311
+ float max_h = sample_max_height(cell_center, mip);
312
+
313
+ // Ray height is linear in t, so the minimum across the
314
+ // cell is at one of the endpoints
315
+ float h_now = origin_w.y + t * dir_w.y;
316
+ float h_exit = origin_w.y + (t + t_exit) * dir_w.y;
317
+ float h_min = min(h_now, h_exit);
318
+
319
+ if (h_min <= max_h) {
320
+ // Cell could contain an occluder
321
+ if (mip == 0) {
322
+ // Finest mip: confirmed hit
323
+ return true;
324
+ }
325
+ // Refine into smaller cells without advancing t
326
+ mip = mip - 1;
327
+ } else {
328
+ // Cell is empty above the ray — skip past it and try
329
+ // to coarsen so the next iteration takes a bigger step
330
+ t = t + t_exit;
331
+ mip = min(maxMipLevel, mip + 1);
332
+ }
333
+ }
334
+
335
+ // Iteration cap reached — choose the unbiased side and return
336
+ // "not occluded". Visibility errors on the bright side are
337
+ // less visible than spurious dark pixels.
338
+ return false;
339
+ }
340
+
341
+ // ============================================================
342
+ // Main
343
+ // ============================================================
344
+
345
+ void main() {
346
+ // Seed an independent PCG stream per output texel
347
+ uvec2 frag = uvec2(gl_FragCoord.xy);
348
+ random_initialize(uvec3(frag, 0u), uvec3(0u));
349
+
350
+ // World-space origin: lift the texel out of the heightmap
351
+ vec2 world_xz = vUv * worldSize;
352
+ float h0 = sample_height(vUv);
353
+ vec3 origin_w = vec3(world_xz.x, h0, world_xz.y);
354
+
355
+ vec3 normal_w = compute_world_normal(vUv);
356
+
357
+ // Push origin a fraction of a texel along the normal so the
358
+ // first Hi-Z cell at mip 0 doesn't trivially self-occlude
359
+ vec2 texel_world = worldSize / heightMapSize;
360
+ float origin_bias = 0.5 * min(texel_world.x, texel_world.y);
361
+ origin_w += normal_w * origin_bias;
362
+
363
+ int hits = 0;
364
+
365
+ for (int i = 0; i < NUM_RAYS; i++) {
366
+ vec2 r2 = vec2(random(), random());
367
+ vec3 dir = cosine_weighted_world_direction(r2, normal_w);
368
+
369
+ if (trace_ray_hiz(origin_w, dir)) {
370
+ hits++;
371
+ }
372
+ }
373
+
374
+ // Cosine-weighted sampling makes each ray's contribution
375
+ // unit-weighted in the AO integral, so the estimator is just
376
+ // the fraction of escaped rays.
377
+ float occlusion = float(hits) / float(NUM_RAYS);
378
+ float visibility = 1.0 - occlusion;
379
+
380
+ fragColor = vec4(visibility, 0.0, 0.0, 1.0);
381
+ }
382
+ `
383
+ };
384
+ }
385
+
386
+ export default AmbientOcclusionShader;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Build a max-reduction Hi-Z height pyramid from a single-channel float
3
+ * heightmap sampler.
4
+ *
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
+ *
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
+ *
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.
23
+ *
24
+ * Non-power-of-two heightmaps are handled by halving with floor and stopping
25
+ * once both dimensions reach 1.
26
+ *
27
+ * @param {Sampler2D} sampler single-channel Float32 heightmap (itemSize=1)
28
+ * @returns {DataTexture2DArray} R32F array texture; depth = mip count
29
+ */
30
+ export function build_max_height_pyramid(sampler: Sampler2D): DataTexture2DArray;
31
+ import { DataTexture2DArray } from "three";
32
+ //# sourceMappingURL=build_max_height_pyramid.d.ts.map
@@ -0,0 +1 @@
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"}