@woosh/meep-engine 2.138.15 → 2.138.17

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 (61) 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 +95 -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 +23 -12
  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 +371 -128
  36. package/src/engine/graphics/shaders/TerrainShader.js +2 -2
  37. package/src/engine/graphics/util/build_max_height_pyramid.d.ts +25 -0
  38. package/src/engine/graphics/util/build_max_height_pyramid.d.ts.map +1 -0
  39. package/src/engine/graphics/util/build_max_height_pyramid.js +100 -0
  40. package/src/engine/intelligence/mcts/MonteCarlo.d.ts +35 -4
  41. package/src/engine/intelligence/mcts/MonteCarlo.d.ts.map +1 -1
  42. package/src/engine/intelligence/mcts/MonteCarlo.js +101 -31
  43. package/src/engine/intelligence/mcts/StateNode.d.ts +47 -24
  44. package/src/engine/intelligence/mcts/StateNode.d.ts.map +1 -1
  45. package/src/engine/intelligence/mcts/StateNode.js +364 -316
  46. package/editor/ecs/component/FieldDescriptor.d.ts +0 -27
  47. package/editor/ecs/component/FieldDescriptor.d.ts.map +0 -1
  48. package/editor/ecs/component/FieldValueAdapter.d.ts +0 -7
  49. package/editor/ecs/component/FieldValueAdapter.d.ts.map +0 -1
  50. package/editor/ecs/component/createFieldEditor.d.ts +0 -9
  51. package/editor/ecs/component/createFieldEditor.d.ts.map +0 -1
  52. package/editor/ecs/component/createObjectEditor.d.ts +0 -14
  53. package/editor/ecs/component/createObjectEditor.d.ts.map +0 -1
  54. package/editor/ecs/component/findNearestRegisteredType.d.ts +0 -8
  55. package/editor/ecs/component/findNearestRegisteredType.d.ts.map +0 -1
  56. package/src/engine/ecs/grid/HeightMap2NormalMap.d.ts +0 -10
  57. package/src/engine/ecs/grid/HeightMap2NormalMap.d.ts.map +0 -1
  58. package/src/engine/ecs/grid/HeightMap2NormalMap.js +0 -72
  59. package/src/engine/ecs/grid/NormalMap2AOMap.d.ts +0 -15
  60. package/src/engine/ecs/grid/NormalMap2AOMap.d.ts.map +0 -1
  61. package/src/engine/ecs/grid/NormalMap2AOMap.js +0 -82
@@ -1,128 +1,371 @@
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
+ precision highp sampler2D;
98
+
99
+ uniform sampler2D heightMap;
100
+ uniform vec2 worldSize;
101
+ uniform vec2 heightMapSize;
102
+ uniform int maxMipLevel;
103
+
104
+ in vec2 vUv;
105
+ out vec4 fragColor;
106
+
107
+ const float PI = 3.14159265358979;
108
+
109
+ // Safety cap on Hi-Z iterations per ray. log2 of typical heightmap
110
+ // dimensions × a handful of refine/coarsen rounds per cell is
111
+ // usually under 80; 256 gives generous headroom.
112
+ const int MAX_HIZ_ITER = 256;
113
+
114
+ // ============================================================
115
+ // PCG random number generator
116
+ // ============================================================
117
+ // pcg is the standard 32-bit scrambler from O'Neill's family.
118
+ // pcg3d is the 3-vector variant used purely for seeding so the
119
+ // spatial pattern of per-texel seeds is uncorrelated.
120
+
121
+ uint rng_state;
122
+
123
+ uint pcg(uint v) {
124
+ uint state = v * 747796405u + 2891336453u;
125
+ uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
126
+ return (word >> 22u) ^ word;
127
+ }
128
+
129
+ uvec3 pcg3d(uvec3 v) {
130
+ v = v * 1664525u + 1013904223u;
131
+ v.x += v.y * v.z;
132
+ v.y += v.z * v.x;
133
+ v.z += v.x * v.y;
134
+ v ^= (v >> uvec3(16u));
135
+ v.x += v.y * v.z;
136
+ v.y += v.z * v.x;
137
+ v.z += v.x * v.y;
138
+ return v;
139
+ }
140
+
141
+ // Map a uint to [0, 1) by stuffing 23 bits into a float's mantissa
142
+ // and subtracting the implicit leading 1. Faster than divide and
143
+ // produces a uniform distribution.
144
+ float hash_to_float01(uint h) {
145
+ return uintBitsToFloat(0x3f800000u | (h >> 9u)) - 1.0;
146
+ }
147
+
148
+ uint random_uint() {
149
+ rng_state = pcg(rng_state);
150
+ return rng_state;
151
+ }
152
+
153
+ float random() {
154
+ return hash_to_float01(random_uint());
155
+ }
156
+
157
+ void random_initialize(uvec3 invocation_id, uvec3 seed) {
158
+ uvec3 mixed = pcg3d(invocation_id + seed * 37u);
159
+ rng_state = mixed.x ^ mixed.y ^ mixed.z;
160
+ }
161
+
162
+ // ============================================================
163
+ // Cosine-weighted hemisphere sampling
164
+ // ============================================================
165
+ // Shirley's concentric disk → cosine hemisphere via z = sqrt(1 - r²).
166
+ // The orthonormal basis from the normal uses Frisvad's branchless
167
+ // construction with z-flip for numerical stability near n.z = -1.
168
+
169
+ vec2 sample_uniform_disk_concentric(vec2 uv) {
170
+ vec2 offset = 2.0 * uv - vec2(1.0);
171
+ if (offset.x == 0.0 && offset.y == 0.0) {
172
+ return vec2(0.0);
173
+ }
174
+ float r;
175
+ float theta;
176
+ const float PI_4 = PI / 4.0;
177
+ const float PI_2 = PI / 2.0;
178
+ if (abs(offset.x) > abs(offset.y)) {
179
+ r = offset.x;
180
+ theta = PI_4 * (offset.y / offset.x);
181
+ } else {
182
+ r = offset.y;
183
+ theta = PI_2 - PI_4 * (offset.x / offset.y);
184
+ }
185
+ return r * vec2(cos(theta), sin(theta));
186
+ }
187
+
188
+ vec3 sample_cosine_weighted_hemisphere(vec2 uv) {
189
+ vec2 d = sample_uniform_disk_concentric(uv);
190
+ float z = sqrt(max(0.0, 1.0 - d.x * d.x - d.y * d.y));
191
+ return vec3(d.x, d.y, z);
192
+ }
193
+
194
+ mat3 build_orthonormal_matrix_n(vec3 n) {
195
+ vec3 T;
196
+ vec3 B;
197
+ if (n.z < 0.0) {
198
+ float a = 1.0 / (1.0 - n.z);
199
+ float b = n.x * n.y * a;
200
+ T = vec3(1.0 - n.x * n.x * a, -b, n.x);
201
+ B = vec3(b, n.y * n.y * a - 1.0, -n.y);
202
+ } else {
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, 1.0 - n.y * n.y * a, -n.y);
207
+ }
208
+ return mat3(T, B, n);
209
+ }
210
+
211
+ vec3 cosine_weighted_world_direction(vec2 uv, vec3 n) {
212
+ vec3 local = sample_cosine_weighted_hemisphere(uv);
213
+ return normalize(build_orthonormal_matrix_n(n) * local);
214
+ }
215
+
216
+ // ============================================================
217
+ // Heightmap utilities
218
+ // ============================================================
219
+
220
+ float sample_height(vec2 uv) {
221
+ return textureLod(heightMap, uv, 0.0).r;
222
+ }
223
+
224
+ // Central-difference normal in world space. Faster than building
225
+ // a normal map up front and stays self-consistent with the
226
+ // heightmap used for tracing (no convention mismatch possible).
227
+ vec3 compute_world_normal(vec2 uv) {
228
+ vec2 texel_step = 1.0 / heightMapSize;
229
+ vec2 texel_world = worldSize / heightMapSize;
230
+
231
+ float h_l = sample_height(uv - vec2(texel_step.x, 0.0));
232
+ float h_r = sample_height(uv + vec2(texel_step.x, 0.0));
233
+ float h_d = sample_height(uv - vec2(0.0, texel_step.y));
234
+ float h_u = sample_height(uv + vec2(0.0, texel_step.y));
235
+
236
+ float dHdx = (h_r - h_l) / (2.0 * texel_world.x);
237
+ float dHdz = (h_u - h_d) / (2.0 * texel_world.y);
238
+
239
+ return normalize(vec3(-dHdx, 1.0, -dHdz));
240
+ }
241
+
242
+ // ============================================================
243
+ // Hi-Z ray traversal
244
+ // ============================================================
245
+ // Trace one ray against the max-height pyramid. The ray's XZ
246
+ // projection steps through texture-space cells; height is tracked
247
+ // separately along the ray parameter t (world units).
248
+ //
249
+ // Returns true iff the ray is occluded by the heightfield before
250
+ // exiting the terrain extent.
251
+
252
+ bool trace_ray_hiz(vec3 origin_w, vec3 dir_w) {
253
+ vec2 inv_world_size = 1.0 / worldSize;
254
+ vec2 origin_tex = origin_w.xz * inv_world_size;
255
+ vec2 dir_tex = dir_w.xz * inv_world_size;
256
+
257
+ // Reciprocal of dir_tex used to compute "world distance to
258
+ // next cell boundary". A zero component means the ray is
259
+ // axis-aligned in tex space — it never crosses that axis's
260
+ // boundaries — so route to a large finite value with sign
261
+ // matching step(0, dir) (= +1 at the zero crossing).
262
+ vec2 inv_dir_tex = vec2(
263
+ dir_tex.x == 0.0 ? 1e20 : 1.0 / dir_tex.x,
264
+ dir_tex.y == 0.0 ? 1e20 : 1.0 / dir_tex.y
265
+ );
266
+
267
+ // Step past the cell boundary by a tiny world-space epsilon
268
+ // so the next iteration sees the new cell, not the one we
269
+ // just exited.
270
+ const float T_EPS = 1e-4;
271
+
272
+ int mip = 0;
273
+ float t = 0.0;
274
+
275
+ for (int iter = 0; iter < MAX_HIZ_ITER; iter++) {
276
+ vec2 tex_pos = origin_tex + t * dir_tex;
277
+
278
+ // Exited the terrain extent — ray escaped, no occlusion
279
+ if (any(lessThan(tex_pos, vec2(0.0))) ||
280
+ any(greaterThan(tex_pos, vec2(1.0)))) {
281
+ return false;
282
+ }
283
+
284
+ // Cell extents at the current mip
285
+ float mip_scale = exp2(float(mip));
286
+ vec2 cell_size = vec2(mip_scale) / heightMapSize;
287
+ vec2 cell_origin = floor(tex_pos / cell_size) * cell_size;
288
+ vec2 cell_center = cell_origin + 0.5 * cell_size;
289
+
290
+ // World distance along the ray to the next cell boundary
291
+ vec2 next_boundary = cell_origin + step(0.0, dir_tex) * cell_size;
292
+ vec2 t_to_boundary = max((next_boundary - tex_pos) * inv_dir_tex, vec2(0.0));
293
+ float t_exit = min(t_to_boundary.x, t_to_boundary.y) + T_EPS;
294
+
295
+ // Max occluder height in this cell at this mip
296
+ float max_h = textureLod(heightMap, cell_center, float(mip)).r;
297
+
298
+ // Ray height is linear in t, so the minimum across the
299
+ // cell is at one of the endpoints
300
+ float h_now = origin_w.y + t * dir_w.y;
301
+ float h_exit = origin_w.y + (t + t_exit) * dir_w.y;
302
+ float h_min = min(h_now, h_exit);
303
+
304
+ if (h_min <= max_h) {
305
+ // Cell could contain an occluder
306
+ if (mip == 0) {
307
+ // Finest mip: confirmed hit
308
+ return true;
309
+ }
310
+ // Refine into smaller cells without advancing t
311
+ mip = mip - 1;
312
+ } else {
313
+ // Cell is empty above the ray — skip past it and try
314
+ // to coarsen so the next iteration takes a bigger step
315
+ t = t + t_exit;
316
+ mip = min(maxMipLevel, mip + 1);
317
+ }
318
+ }
319
+
320
+ // Iteration cap reached — choose the unbiased side and return
321
+ // "not occluded". Visibility errors on the bright side are
322
+ // less visible than spurious dark pixels.
323
+ return false;
324
+ }
325
+
326
+ // ============================================================
327
+ // Main
328
+ // ============================================================
329
+
330
+ void main() {
331
+ // Seed an independent PCG stream per output texel
332
+ uvec2 frag = uvec2(gl_FragCoord.xy);
333
+ random_initialize(uvec3(frag, 0u), uvec3(0u));
334
+
335
+ // World-space origin: lift the texel out of the heightmap
336
+ vec2 world_xz = vUv * worldSize;
337
+ float h0 = sample_height(vUv);
338
+ vec3 origin_w = vec3(world_xz.x, h0, world_xz.y);
339
+
340
+ vec3 normal_w = compute_world_normal(vUv);
341
+
342
+ // Push origin a fraction of a texel along the normal so the
343
+ // first Hi-Z cell at mip 0 doesn't trivially self-occlude
344
+ vec2 texel_world = worldSize / heightMapSize;
345
+ float origin_bias = 0.5 * min(texel_world.x, texel_world.y);
346
+ origin_w += normal_w * origin_bias;
347
+
348
+ int hits = 0;
349
+
350
+ for (int i = 0; i < NUM_RAYS; i++) {
351
+ vec2 r2 = vec2(random(), random());
352
+ vec3 dir = cosine_weighted_world_direction(r2, normal_w);
353
+
354
+ if (trace_ray_hiz(origin_w, dir)) {
355
+ hits++;
356
+ }
357
+ }
358
+
359
+ // Cosine-weighted sampling makes each ray's contribution
360
+ // unit-weighted in the AO integral, so the estimator is just
361
+ // the fraction of escaped rays.
362
+ float occlusion = float(hits) / float(NUM_RAYS);
363
+ float visibility = 1.0 - occlusion;
364
+
365
+ fragColor = vec4(visibility, 0.0, 0.0, 1.0);
366
+ }
367
+ `
368
+ };
369
+ }
370
+
371
+ export default AmbientOcclusionShader;
@@ -280,10 +280,10 @@ function fragment() {
280
280
  float weight = texture(splatWeightMap, vec3(uv, i)).x;
281
281
 
282
282
  weightSum += weight;
283
- colorSum += diffuseData*weight;
283
+ colorSum += diffuseData * weight;
284
284
  }
285
285
 
286
- return weightSum > 0.0 ? colorSum / weightSum : vec4(0.0);
286
+ return weightSum > 0.0 ? (colorSum / weightSum) : vec4(0.0);
287
287
  }
288
288
 
289
289
  ${ShaderChunks.clouds_pars_fragment}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Build a max-reduction mipmap pyramid (Hi-Z–style height buffer) from a
3
+ * single-channel float heightmap sampler.
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.
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`.
13
+ *
14
+ * The level-0 storage is shared (no copy) with `sampler.data`. Higher levels
15
+ * are freshly allocated Float32Array data.
16
+ *
17
+ * Non-power-of-two heightmaps are handled by halving with floor and stopping
18
+ * once both dimensions reach 1.
19
+ *
20
+ * @param {Sampler2D} sampler single-channel Float32 heightmap (itemSize=1)
21
+ * @returns {DataTexture} R32F texture with custom mipmap chain
22
+ */
23
+ export function build_max_height_pyramid(sampler: Sampler2D): DataTexture;
24
+ import { DataTexture } from "three";
25
+ //# 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":"AASA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,8DAFa,WAAW,CAsEvB;4BA5FM,OAAO"}
@@ -0,0 +1,100 @@
1
+ import {
2
+ ClampToEdgeWrapping,
3
+ DataTexture,
4
+ FloatType,
5
+ NearestFilter,
6
+ NearestMipmapNearestFilter,
7
+ RedFormat
8
+ } from "three";
9
+
10
+ /**
11
+ * Build a max-reduction mipmap pyramid (Hi-Z–style height buffer) from a
12
+ * single-channel float heightmap sampler.
13
+ *
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.
18
+ *
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`.
22
+ *
23
+ * The level-0 storage is shared (no copy) with `sampler.data`. Higher levels
24
+ * are freshly allocated Float32Array data.
25
+ *
26
+ * Non-power-of-two heightmaps are handled by halving with floor and stopping
27
+ * once both dimensions reach 1.
28
+ *
29
+ * @param {Sampler2D} sampler single-channel Float32 heightmap (itemSize=1)
30
+ * @returns {DataTexture} R32F texture with custom mipmap chain
31
+ */
32
+ export function build_max_height_pyramid(sampler) {
33
+ const w0 = sampler.width;
34
+ const h0 = sampler.height;
35
+
36
+ /** @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;
78
+ }
79
+ }
80
+
81
+ mipmaps.push({ data: curData, width: curW, height: curH });
82
+
83
+ prevW = curW;
84
+ prevH = curH;
85
+ prevData = curData;
86
+ }
87
+
88
+ const tex = new DataTexture(mipmaps[0].data, w0, h0, RedFormat, FloatType);
89
+ tex.mipmaps = mipmaps;
90
+ tex.generateMipmaps = false;
91
+ tex.minFilter = NearestMipmapNearestFilter;
92
+ tex.magFilter = NearestFilter;
93
+ tex.wrapS = ClampToEdgeWrapping;
94
+ tex.wrapT = ClampToEdgeWrapping;
95
+ tex.internalFormat = 'R32F';
96
+ tex.flipY = false;
97
+ tex.needsUpdate = true;
98
+
99
+ return tex;
100
+ }