@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.
- package/editor/Editor.d.ts.map +1 -1
- package/editor/SelectionVisualizer.d.ts.map +1 -1
- package/editor/actions/concrete/ComponentAddAction.d.ts.map +1 -1
- package/editor/actions/concrete/EntityCreateAction.d.ts.map +1 -1
- package/editor/actions/concrete/SelectionAddAction.d.ts.map +1 -1
- package/editor/enableEditor.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/engine/ecs/grid/HeightMap2AOMap.d.ts +25 -0
- package/src/engine/ecs/grid/HeightMap2AOMap.d.ts.map +1 -0
- package/src/engine/ecs/grid/HeightMap2AOMap.js +95 -0
- package/src/engine/ecs/terrain/ecs/BuildLightTexture.d.ts +15 -7
- package/src/engine/ecs/terrain/ecs/BuildLightTexture.d.ts.map +1 -1
- package/src/engine/ecs/terrain/ecs/BuildLightTexture.js +44 -99
- package/src/engine/ecs/terrain/ecs/Terrain.d.ts.map +1 -1
- package/src/engine/ecs/terrain/ecs/Terrain.js +36 -0
- package/src/engine/ecs/terrain/ecs/splat/SplatMapping.d.ts +1 -1
- package/src/engine/ecs/terrain/ecs/splat/SplatMapping.d.ts.map +1 -1
- package/src/engine/ecs/terrain/ecs/splat/SplatMapping.js +3 -1
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts +12 -1
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts.map +1 -1
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.js +21 -0
- package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderDepthV0.d.ts.map +1 -1
- package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderDepthV0.js +23 -12
- package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderLitV0.d.ts.map +1 -1
- package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderLitV0.js +1 -7
- package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderNormalsV0.d.ts.map +1 -1
- package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderNormalsV0.js +1 -6
- package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderViewportDepthV0.d.ts.map +1 -1
- package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderViewportDepthV0.js +1 -6
- package/src/engine/graphics/material/TerrainDepthMaterial.d.ts +36 -0
- package/src/engine/graphics/material/TerrainDepthMaterial.d.ts.map +1 -0
- package/src/engine/graphics/material/TerrainDepthMaterial.js +65 -0
- package/src/engine/graphics/shaders/AmbientOcclusionShader.d.ts +69 -12
- package/src/engine/graphics/shaders/AmbientOcclusionShader.d.ts.map +1 -1
- package/src/engine/graphics/shaders/AmbientOcclusionShader.js +371 -128
- package/src/engine/graphics/shaders/TerrainShader.js +2 -2
- package/src/engine/graphics/util/build_max_height_pyramid.d.ts +25 -0
- package/src/engine/graphics/util/build_max_height_pyramid.d.ts.map +1 -0
- package/src/engine/graphics/util/build_max_height_pyramid.js +100 -0
- package/src/engine/intelligence/mcts/MonteCarlo.d.ts +35 -4
- package/src/engine/intelligence/mcts/MonteCarlo.d.ts.map +1 -1
- package/src/engine/intelligence/mcts/MonteCarlo.js +101 -31
- package/src/engine/intelligence/mcts/StateNode.d.ts +47 -24
- package/src/engine/intelligence/mcts/StateNode.d.ts.map +1 -1
- package/src/engine/intelligence/mcts/StateNode.js +364 -316
- package/editor/ecs/component/FieldDescriptor.d.ts +0 -27
- package/editor/ecs/component/FieldDescriptor.d.ts.map +0 -1
- package/editor/ecs/component/FieldValueAdapter.d.ts +0 -7
- package/editor/ecs/component/FieldValueAdapter.d.ts.map +0 -1
- package/editor/ecs/component/createFieldEditor.d.ts +0 -9
- package/editor/ecs/component/createFieldEditor.d.ts.map +0 -1
- package/editor/ecs/component/createObjectEditor.d.ts +0 -14
- package/editor/ecs/component/createObjectEditor.d.ts.map +0 -1
- package/editor/ecs/component/findNearestRegisteredType.d.ts +0 -8
- package/editor/ecs/component/findNearestRegisteredType.d.ts.map +0 -1
- package/src/engine/ecs/grid/HeightMap2NormalMap.d.ts +0 -10
- package/src/engine/ecs/grid/HeightMap2NormalMap.d.ts.map +0 -1
- package/src/engine/ecs/grid/HeightMap2NormalMap.js +0 -72
- package/src/engine/ecs/grid/NormalMap2AOMap.d.ts +0 -15
- package/src/engine/ecs/grid/NormalMap2AOMap.d.ts.map +0 -1
- package/src/engine/ecs/grid/NormalMap2AOMap.js +0 -82
|
@@ -1,128 +1,371 @@
|
|
|
1
|
-
import { Vector2 } from "three";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
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
|
+
}
|