@woosh/meep-engine 2.71.0 → 2.73.0

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 (37) hide show
  1. package/build/bundle-worker-terrain.js +1 -1
  2. package/build/meep.cjs +36 -187
  3. package/build/meep.min.js +1 -1
  4. package/build/meep.module.js +36 -187
  5. package/package.json +3 -2
  6. package/src/core/collection/array/array_get_index_in_range.js +1 -1
  7. package/src/core/collection/array/array_swap.js +2 -2
  8. package/src/core/geom/3d/v3_compute_triangle_normal.js +7 -1
  9. package/src/core/geom/vec3/v3_normalize_array.js +27 -0
  10. package/src/engine/ecs/terrain/BufferedGeometryArraysBuilder.js +2 -2
  11. package/src/engine/graphics/ecs/camera/FrustumProjector.js +31 -182
  12. package/src/engine/graphics/geometry/buffered/ComputeNormals.js +11 -26
  13. package/src/engine/graphics/texture/virtual/v2/NOTES.md +27 -0
  14. package/src/engine/graphics/texture/virtual/v2/ShaderUsage.js +51 -26
  15. package/src/engine/graphics/texture/virtual/v2/SparseTexture.js +182 -0
  16. package/src/engine/graphics/texture/virtual/v2/TextureTile.js +31 -0
  17. package/src/engine/graphics/texture/virtual/v2/TileLoader.js +215 -0
  18. package/src/engine/graphics/texture/virtual/v2/UsageDebugView.js +64 -0
  19. package/src/engine/graphics/texture/virtual/v2/UsageMetadata.js +153 -0
  20. package/src/engine/graphics/texture/virtual/v2/UsagePyramidDebugView.js +252 -0
  21. package/src/engine/graphics/texture/virtual/v2/VirtualTextureManager.js +257 -0
  22. package/src/engine/graphics/texture/virtual/v2/compose_finger_print.js +13 -0
  23. package/src/engine/graphics/texture/virtual/v2/finger_print_to_tile_index.js +37 -0
  24. package/src/engine/graphics/texture/virtual/v2/prototype.js +122 -31
  25. package/src/engine/graphics/texture/virtual/v2/tile_index_to_finger_print.js +31 -0
  26. package/src/core/geom/2d/quad-tree/PointQuadTree.js +0 -478
  27. package/src/core/math/random/makeRangedRandom.js +0 -19
  28. package/src/engine/ecs/terrain/TerrainGeometryBuilder.js +0 -152
  29. package/src/engine/ecs/terrain/ecs/PromiseSamplerHeight.js +0 -66
  30. package/src/engine/ecs/terrain/ecs/TerrainClassifier.js +0 -125
  31. package/src/engine/graphics/clouds/MaterialTransformer.js +0 -0
  32. package/src/engine/graphics/clouds/TerrainCloudsPlugin.js +0 -0
  33. package/src/engine/graphics/clouds/cs_build_fragment_shader.js +0 -0
  34. package/src/engine/graphics/clouds/cs_build_vertex_shader.js +0 -0
  35. package/src/engine/graphics/ecs/camera/TiltCameraController.js +0 -69
  36. package/src/engine/graphics/ecs/camera/is_valid_distance_value.js +0 -11
  37. package/src/engine/graphics/texture/sampler/SampleTraverser.js +0 -165
@@ -0,0 +1,27 @@
1
+ # Sparse Virtual Texture
2
+
3
+ This tech is based on popular Megatexture technique developed for Rage by John Carmack, working at id software.
4
+
5
+ The key idea boils down to having a very large texture that is impractical to keep in memory, and only load and keep in memory pieces of it that are relevant to the current view.
6
+
7
+ There are 2 parts to this technique, offline and online.
8
+
9
+ ## Offline
10
+
11
+ The original texture is mip-mapped, down to the size of a single tile, for the sake of this document let it be 128x128 pixels. Each mip level is then cut into tiles and stored on disk. A good intuition for this is to think of each tile as a separate pixel, representing non-color data.
12
+
13
+ ## Online
14
+
15
+ This is achieved by pre-rendering the scene with a special shader that records UV and mip-level information, which we call `Usage`, this is then analysed to build a list of used tiles (mip level, x, y) and by counting occurrence of this tile in the `Usage` texture we get a much more useful data structure.
16
+
17
+ Based on the `Usage` data, we populate our `Physical` texture of pages, and based on what's currently in the `Physical` texture - we populate the `Reference` texture that stores information about the entire mip pyramid, and for each tile contains a pointer to the `Physical` texture, where representative tile can be found.
18
+
19
+
20
+ ## Considerations
21
+
22
+
23
+ ### Filtering
24
+
25
+ Most papers suggest introducing a border into each tile of ~4 pixels.
26
+ 4 pixel border on a 128x128 pixel tile is going to take up 12.1% of space, which is quite a lot.
27
+ We can do filtering in software, that is - sample individual pixels and perform interpolation inside the shader.
@@ -1,6 +1,15 @@
1
1
  function vertex() {
2
2
  return `
3
- varying vec2 vUv;
3
+ precision mediump float;
4
+ precision mediump int;
5
+
6
+ uniform mat4 modelViewMatrix;
7
+ uniform mat4 projectionMatrix;
8
+
9
+ in vec2 uv;
10
+ in vec3 position;
11
+
12
+ out vec2 vUv;
4
13
  void main()
5
14
  {
6
15
  vUv = uv;
@@ -13,32 +22,48 @@ function vertex() {
13
22
 
14
23
  function fragment() {
15
24
  return `
16
- uniform vec4 u_mt_tex; // [ width, height, tileSize, id ]
17
- uniform vec2 u_mt_params; // [ bias, maxLevel, xx, xx ]
18
- varying vec2 vUv;
19
-
20
- float MTCalculateMipLevel( const in vec2 uv, const in float bias ) {
21
- vec2 dx = dFdx( uv * u_mt_tex.x );
22
- vec2 dy = dFdy( uv * u_mt_tex.y );
23
- return log2( max( dot( dx, dx ), dot( dy, dy ) ) ) / 2.0 + bias;
24
- }
25
-
26
- vec4 MTCalculateTileKeyRGBA( const in vec2 uv ) {
27
- float level = clamp( floor( MTCalculateMipLevel( uv, u_mt_params.x ) ), 0.0, u_mt_params.y );
28
- vec2 texIS = uv.xy * u_mt_tex.xy; // [0-1]x[0-1] -> [0-texwidth]x[0-texheight]
29
- vec2 texLS = texIS / exp2( level ); // -> [0-levelwidth]x[0-levelheight]
30
- vec2 texTS = floor( texLS / u_mt_tex.z ); // -> [0->tileswide]x[0-tileshigh]
31
- return vec4( u_mt_params.y - level, texTS.xy, u_mt_tex.w ) / 255.0; // [ level, tileX, tileY, texId ] -> RGBA
32
- }
33
-
25
+ precision highp float;
26
+ precision highp int;
34
27
 
35
- void main(){
36
- vec4 tile = MTCalculateTileKeyRGBA(vUv);
37
- // gl_FragColor = tile;
38
- gl_FragColor = vec4(mix(0.1,1.0,tile.x*255.0/u_mt_params.y), tile.yz*64.0, 1.0);
39
- // gl_FragColor = MTCalculateTileKeyRGBA(vUv);
40
- // gl_FragColor = vec4(1.0);
41
- }
28
+ uniform uvec4 u_mt_tex; // [ width, height, tileSize, id ]
29
+ uniform vec2 u_mt_params; // [ bias, maxLevel ]
30
+ uniform vec2 u_mt_viewport_size;
31
+ in vec2 vUv;
32
+
33
+ out uvec4 out_decoded;
34
+
35
+ float MTCalculateMipLevel( const in vec2 uv, const in float bias ) {
36
+
37
+ float scale_x = float(u_mt_tex.x) ;
38
+ float scale_y = float(u_mt_tex.y) ;
39
+
40
+ vec2 dx = dFdx( uv * scale_x)/ u_mt_viewport_size.x;
41
+ vec2 dy = dFdy( uv * scale_y)/ u_mt_viewport_size.y;
42
+
43
+ float d = max( dot( dx, dx ), dot( dy, dy ) );
44
+
45
+ return log2( d ) * 0.5 + bias;
46
+ }
47
+
48
+ uvec4 MTCalculateTileKeyRGBA( const in vec2 uv ) {
49
+ uint level = clamp( uint( floor( MTCalculateMipLevel( uv, u_mt_params.x ) ) ), 0u, uint(u_mt_params.y) );
50
+
51
+ uint mip_resolution_in_tiles = 1u << level;
52
+
53
+ uint mip_resolution_in_texels = mip_resolution_in_tiles * u_mt_tex.z;
54
+
55
+ vec2 texel_position = fract(uv.xy) * vec2(u_mt_tex.xy);
56
+
57
+ uvec2 tile_position = uvec2( texel_position / float(mip_resolution_in_texels) );
58
+
59
+ // [ level, tileX, tileY, texId ] -> RGBA
60
+ return uvec4( uint(u_mt_params.y) - level, tile_position, u_mt_tex.w );
61
+ }
62
+
63
+
64
+ void main(){
65
+ out_decoded = MTCalculateTileKeyRGBA(vUv);
66
+ }
42
67
  `;
43
68
  }
44
69
 
@@ -0,0 +1,182 @@
1
+ import { WebGLRenderTarget } from "three";
2
+ import { BitSet } from "../../../../../core/binary/BitSet.js";
3
+ import { Cache } from "../../../../../core/cache/Cache.js";
4
+ import { array_copy } from "../../../../../core/collection/array/array_copy.js";
5
+ import { passThrough, strictEquals } from "../../../../../core/function/Functions.js";
6
+ import { compose_finger_print } from "./compose_finger_print.js";
7
+ import { TileLoader } from "./TileLoader.js";
8
+
9
+ /**
10
+ * How much extra data to store in cache (in bytes)
11
+ * @type {number}
12
+ */
13
+ const DEFAULT_CACHE_SIZE = 16 * 1024 * 1024;
14
+
15
+ export class SparseTexture {
16
+ #page_texture = new WebGLRenderTarget(1, 1);
17
+
18
+ #page_texture_size = [1, 1];
19
+
20
+ /**
21
+ *
22
+ * @type {TextureTile[]}
23
+ */
24
+ #resident_tiles = [];
25
+
26
+ #page_slot_occupancy = new BitSet();
27
+
28
+ #tile_resolution = 128;
29
+
30
+ #residency_tile_capacity = 0;
31
+
32
+ #loader = new TileLoader();
33
+
34
+ /**
35
+ *
36
+ * @type {AssetManager|null}
37
+ */
38
+ #asset_manager = null;
39
+
40
+ set asset_manager(v) {
41
+ this.#asset_manager = v
42
+
43
+ this.#loader.asset_manager = v;
44
+ }
45
+
46
+ #update_residency_capacity() {
47
+ const tile_resolution = this.#tile_resolution;
48
+ const size = this.#page_texture_size;
49
+
50
+ const x = size[0] / tile_resolution;
51
+ const y = size[1] / tile_resolution;
52
+
53
+ this.#residency_tile_capacity = x * y;
54
+ }
55
+
56
+
57
+ /**
58
+ *
59
+ * @param {number} v
60
+ */
61
+ set tile_resolution(v) {
62
+ this.#tile_resolution = v;
63
+
64
+ this.#update_residency_capacity();
65
+ }
66
+
67
+ /**
68
+ * Tiles that are not currently resident live here
69
+ * key is the tile's "fingerprint"
70
+ * @type {Cache<number,TextureTile>}
71
+ */
72
+ #tile_cache = new Cache({
73
+ keyHashFunction: passThrough,
74
+ keyEqualityFunction: strictEquals,
75
+ maxWeight: DEFAULT_CACHE_SIZE,
76
+ valueWeigher: () => this.#tile_resolution * this.#tile_resolution * 4
77
+ });
78
+
79
+ #find_eviction_target_index() {
80
+ // find least-recently used
81
+ const tiles = this.#resident_tiles;
82
+ const count = tiles.length;
83
+
84
+ let max = -1;
85
+ let max_index = -1;
86
+
87
+ for (let i = 0; i < count; i++) {
88
+ const tile = tiles[i];
89
+
90
+ if (tile.last_used_time > max) {
91
+ max = tile.last_used_time;
92
+ max_index = i;
93
+ }
94
+ }
95
+
96
+ return max_index;
97
+ }
98
+
99
+ #evict_one() {
100
+ const index = this.#find_eviction_target_index();
101
+
102
+ if (index === -1) {
103
+ return false;
104
+ }
105
+
106
+ return this.#remove_resident_tile(index);
107
+ }
108
+
109
+ /**
110
+ *
111
+ * @param {number} index
112
+ * @returns {boolean}
113
+ */
114
+ #remove_resident_tile(index) {
115
+ const [removed] = this.#resident_tiles.splice(index, 1);
116
+
117
+ const pageSlot = removed.page_slot;
118
+
119
+ // mark slot as empty for next tile
120
+ this.#page_slot_occupancy.clear(pageSlot);
121
+
122
+ // push into cache
123
+ this.#tile_cache.put(removed.finder_print, removed);
124
+
125
+ return true;
126
+ }
127
+
128
+ /**
129
+ *
130
+ * @param {number[]} v
131
+ */
132
+ set page_texture_size(v) {
133
+ array_copy(
134
+ v, 0,
135
+ this.#page_texture_size, 0,
136
+ 2
137
+ );
138
+
139
+ this.#page_texture.dispose();
140
+ this.#page_texture.setSize(this.#page_texture[0], this.#page_texture[1]);
141
+
142
+ this.#update_residency_capacity();
143
+ }
144
+
145
+ /**
146
+ *
147
+ * @param {UsageMetadata} usage
148
+ */
149
+ update_usage(usage) {
150
+ const max_mip = usage.max_mip_level;
151
+
152
+ for (let mip = 0; mip < max_mip; mip++) {
153
+
154
+ const resolution = 1 << mip;
155
+
156
+ for (let y = 0; y < resolution; y++) {
157
+ for (let x = 0; x < resolution; x++) {
158
+
159
+ const count = usage.getCountBy(mip, x, y);
160
+
161
+ if (count === 0) {
162
+ continue;
163
+ }
164
+
165
+ const fingerprint = compose_finger_print(mip, x, y);
166
+
167
+ this.#loader.enqueue(fingerprint);
168
+
169
+ }
170
+ }
171
+
172
+ }
173
+
174
+ this.#loader.update_usage(usage);
175
+ }
176
+
177
+ dispose() {
178
+ this.#page_texture.dispose();
179
+ this.#tile_cache.clear();
180
+ this.#page_slot_occupancy.reset();
181
+ }
182
+ }
@@ -0,0 +1,31 @@
1
+ export class TextureTile {
2
+
3
+ finder_print = 0;
4
+
5
+ last_used_time = 0;
6
+
7
+ /**
8
+ * Currently occupied page slot, only valid when page is resident
9
+ * @type {number}
10
+ */
11
+ page_slot = 0;
12
+
13
+ /**
14
+ *
15
+ * @type {Sampler2D|null}
16
+ */
17
+ data = null;
18
+
19
+ /**
20
+ *
21
+ * @param {TextureTile} other
22
+ * @returns {boolean}
23
+ */
24
+ equals(other) {
25
+ return false;
26
+ }
27
+
28
+ hash() {
29
+ return 0;
30
+ }
31
+ }
@@ -0,0 +1,215 @@
1
+ import { assert } from "../../../../../core/assert.js";
2
+ import { arrayQuickSort } from "../../../../../core/collection/array/arrayQuickSort.js";
3
+ import Signal from "../../../../../core/events/signal/Signal.js";
4
+ import { AssetManager } from "../../../../asset/AssetManager.js";
5
+ import { GameAssetType } from "../../../../asset/GameAssetType.js";
6
+ import { compose_finger_print } from "./compose_finger_print.js";
7
+ import { decompose_finger_print } from "./finger_print_to_tile_index.js";
8
+ import { TextureTile } from "./TextureTile.js";
9
+
10
+ export class TileLoader {
11
+ /**
12
+ * Where the tiles are stored
13
+ * @type {string}
14
+ */
15
+ #root_path = "";
16
+
17
+ /**
18
+ *
19
+ * @type {AssetManager}
20
+ */
21
+ #asset_manager = null;
22
+
23
+ set asset_manager(v){
24
+ this.#asset_manager = v;
25
+ }
26
+
27
+ /**
28
+ * How many tiles can be loaded at the same time
29
+ * @type {number}
30
+ */
31
+ #concurrency = 4;
32
+
33
+ /**
34
+ * Tiles that are currently being loaded
35
+ * @readonly
36
+ * @type {number[]}
37
+ */
38
+ #in_flight = [];
39
+
40
+ /**
41
+ * Contains fingerprints
42
+ * @readonly
43
+ * @type {number[]}
44
+ */
45
+ #queue = [];
46
+
47
+ /**
48
+ * @readonly
49
+ */
50
+ on = {
51
+ /**
52
+ * @type {Signal<TextureTile>}
53
+ */
54
+ loaded: new Signal()
55
+ };
56
+
57
+
58
+ /**
59
+ *
60
+ * @param {number} fingerprint
61
+ * @returns {boolean}
62
+ */
63
+ #queue_remove(fingerprint) {
64
+ assert.isNonNegativeInteger(fingerprint, 'fingerprint');
65
+
66
+ const i = this.#queue.indexOf(fingerprint);
67
+
68
+ if (i === -1) {
69
+ return false;
70
+ }
71
+
72
+ this.#queue.splice(i, 1);
73
+
74
+ return true;
75
+ }
76
+
77
+ #queue_add(fingerprint) {
78
+ assert.isNonNegativeInteger(fingerprint, 'fingerprint');
79
+
80
+ if (this.is_queued(fingerprint)) {
81
+ return false;
82
+ }
83
+
84
+ this.#queue.push(fingerprint);
85
+
86
+ return true;
87
+ }
88
+
89
+ /**
90
+ *
91
+ * @param {number} fingerprint
92
+ */
93
+ enqueue(fingerprint) {
94
+ if (this.#in_flight.indexOf(fingerprint) !== -1) {
95
+ return false;
96
+ }
97
+
98
+ // TODO limit queue size, if the queue grows too large we may end up with completely irrelevant tiles being loaded minutes after they were actually relevant
99
+ if (this.#queue_add(fingerprint) === false) {
100
+ return false;
101
+ }
102
+
103
+ this.#prod();
104
+
105
+ return true;
106
+ }
107
+
108
+ /**
109
+ *
110
+ * @param {number} fingerprint
111
+ * @returns {boolean}
112
+ */
113
+ is_queued(fingerprint) {
114
+ assert.isNonNegativeInteger(fingerprint, 'fingerprint');
115
+
116
+ return this.#queue.indexOf(fingerprint) !== -1;
117
+ }
118
+
119
+ /**
120
+ *
121
+ * @param {UsageMetadata} usage
122
+ */
123
+ #sort_queue_by_usage(usage) {
124
+
125
+ /**
126
+ *
127
+ * @param {number} fingerprint
128
+ */
129
+ const score = (fingerprint) => -usage.getCountByFingerprint(fingerprint);
130
+
131
+ arrayQuickSort(
132
+ this.#queue,
133
+ score, null,
134
+ 0, this.#queue.length - 1
135
+ );
136
+
137
+ }
138
+
139
+ /**
140
+ *
141
+ * @param {UsageMetadata} usage
142
+ */
143
+ update_usage(usage) {
144
+ this.#sort_queue_by_usage(usage);
145
+ }
146
+
147
+ /**
148
+ *
149
+ * @param {number} mip
150
+ * @param {number} x
151
+ * @param {number} y
152
+ * @returns {string}
153
+ */
154
+ #build_file_name(mip, x, y) {
155
+
156
+ assert.isNonNegativeInteger(mip, 'mip');
157
+ assert.isNonNegativeInteger(x, 'x');
158
+ assert.isNonNegativeInteger(y, 'y');
159
+
160
+ return `${mip}-${x}-${y}.png`;
161
+ }
162
+
163
+ /**
164
+ * Initializes load of the next element in the queue if there is capacity
165
+ */
166
+ #prod() {
167
+ while (this.#in_flight.length < this.#concurrency && this.#queue.length > 0) {
168
+ const fingerprint = this.#queue.pop();
169
+ const { mip, x, y } = decompose_finger_print(fingerprint);
170
+
171
+ this.#load(mip, x, y);
172
+ }
173
+ }
174
+
175
+ /**
176
+ *
177
+ * @param {number} mip
178
+ * @param {number} x
179
+ * @param {number} y
180
+ * @returns {Promise<TextureTile>}
181
+ */
182
+ #load(mip, x, y) {
183
+ const file_name = this.#build_file_name(mip, x, y);
184
+ const finger_print = compose_finger_print(mip, x, y);
185
+
186
+ this.#queue_remove(finger_print);
187
+ this.#in_flight.push(finger_print);
188
+
189
+ return this.#asset_manager.promise(`${this.#root_path}/${file_name}`, GameAssetType.Image)
190
+ .then(asset => {
191
+
192
+ const sampler = asset.create();
193
+
194
+ const tile = new TextureTile();
195
+
196
+
197
+ tile.data = sampler;
198
+ tile.finder_print = finger_print;
199
+
200
+ this.on.loaded.send1(tile);
201
+
202
+
203
+ return tile;
204
+ })
205
+ .finally(() => {
206
+
207
+ const index = this.#in_flight.indexOf(finger_print);
208
+ this.#in_flight.splice(index, 1);
209
+
210
+ this.#prod();
211
+
212
+ })
213
+
214
+ }
215
+ }
@@ -0,0 +1,64 @@
1
+ import LabelView from "../../../../../view/common/LabelView.js";
2
+ import EmptyView from "../../../../../view/elements/EmptyView.js";
3
+
4
+ export class UsageDebugView extends EmptyView {
5
+ #tileCount = new LabelView("")
6
+ #tiles = new EmptyView();
7
+ #mip_levels = 0;
8
+
9
+
10
+ constructor(opt) {
11
+ super(opt);
12
+
13
+ this.addChild(this.#tileCount);
14
+ this.addChild(this.#tiles);
15
+
16
+ this.css({
17
+ position: "absolute",
18
+ left: "0",
19
+ top: "0",
20
+ background: "rgba(255,255,255,0.5)",
21
+ fontFamily: "monospace",
22
+ fontSize: "10px"
23
+ });
24
+ }
25
+
26
+ set mip_levels(v) {
27
+ this.#mip_levels = v;
28
+ }
29
+
30
+ /**
31
+ *
32
+ * @param {UsageMetadata} usage
33
+ */
34
+ set usage(usage) {
35
+
36
+
37
+ this.#tiles.removeAllChildren();
38
+
39
+ let tileCount = 0;
40
+
41
+ for (let mip = 0; mip < this.#mip_levels; mip++) {
42
+ const resolution = 1 << mip;
43
+
44
+ for (let y = 0; y < resolution; y++) {
45
+ for (let x = 0; x < resolution; x++) {
46
+
47
+ const count = usage.getCountBy(mip, x, y);
48
+
49
+ if (count <= 0) {
50
+ continue;
51
+ }
52
+
53
+ this.#tiles.addChild(new LabelView(`ID: ${0} Level: ${mip} X: ${x} Y: ${y} USAGE: ${count}`));
54
+
55
+ tileCount++;
56
+ }
57
+ }
58
+ }
59
+
60
+ this.#tileCount.updateText(`Tile Count: ${tileCount}`);
61
+ }
62
+
63
+
64
+ }