@navaramap/three 0.0.0 → 0.0.2

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 (66) hide show
  1. package/README.md +70 -0
  2. package/dist/assets/fontWorker-8oWvyvwL.js +2 -0
  3. package/dist/assets/fontWorker-8oWvyvwL.js.map +1 -0
  4. package/dist/assets/index-D9G7kIEj.js +25 -0
  5. package/dist/assets/index-D9G7kIEj.js.map +1 -0
  6. package/dist/assets/{navara_wasm_font_worker_bg-665OLxjy.wasm → navara_wasm_font_worker_bg-CkZP9l2i.wasm} +0 -0
  7. package/dist/assets/{navara_wasm_worker_bg-BXhKC5bE.wasm → navara_wasm_worker_bg-Q8zrkLCR.wasm} +0 -0
  8. package/dist/draco_decoder.wasm +0 -0
  9. package/dist/draco_decoder2.wasm +0 -0
  10. package/dist/index.d.ts +587 -67
  11. package/dist/index.js +5640 -4944
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.umd.cjs +238 -211
  14. package/dist/index.umd.cjs.map +1 -1
  15. package/package.json +35 -34
  16. package/src/core/EffectDesc.ts +2 -4
  17. package/src/core/MeshDesc.ts +1 -2
  18. package/src/core/MeshDescWithSelectiveEffect.ts +1 -2
  19. package/src/declutter/DeclutterManager.test.ts +235 -0
  20. package/src/declutter/DeclutterManager.ts +256 -0
  21. package/src/declutter/index.ts +7 -0
  22. package/src/declutter/kernel.ts +46 -0
  23. package/src/declutter/types.ts +61 -0
  24. package/src/declutter/wasmKernel.ts +13 -0
  25. package/src/device.ts +18 -4
  26. package/src/evaluations/FeatureEvaluator.ts +21 -0
  27. package/src/event/context.ts +17 -0
  28. package/src/event/features/modelGpuBytes.ts +1 -3
  29. package/src/event/hillshade.test.ts +7 -13
  30. package/src/event/index.ts +2 -3
  31. package/src/event/tileHandler.ts +54 -0
  32. package/src/index.ts +143 -33
  33. package/src/material/enhancer/ShaderReplacer.ts +1 -2
  34. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/mutates.ts +6 -0
  35. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/types.ts +11 -0
  36. package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.test.ts +83 -0
  37. package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.ts +19 -4
  38. package/src/material/enhancer/tileComposite/tileElevationHeatmapEnhancer/shader.ts +9 -2
  39. package/src/mesh/batchTexture.ts +2 -4
  40. package/src/mesh/batchedSdfText.ts +61 -1
  41. package/src/mesh/sdfText.ts +170 -5
  42. package/src/mesh/sprite/instancedSprite.ts +245 -3
  43. package/src/mesh/tile/drapeResolver.ts +76 -0
  44. package/src/mesh/{tile.test.ts → tile/index.test.ts} +16 -6
  45. package/src/mesh/{tile.ts → tile/index.ts} +124 -392
  46. package/src/mesh/tile/rasterDrapeResolver.ts +423 -0
  47. package/src/mesh/tile/vectorDrapeResolver.ts +404 -0
  48. package/src/orchestrators/RenderPassOrchestrator.ts +25 -0
  49. package/src/passes/CustomRenderPass.ts +21 -5
  50. package/src/passes/RenderTargetCopyPass.ts +24 -2
  51. package/src/test-utils/engine.ts +53 -0
  52. package/src/tileTexture/TileTextureCache.test.ts +2 -0
  53. package/src/tileTexture/TileTextureCache.ts +1 -0
  54. package/src/tileTexture/TileTextureCompositor.test.ts +107 -0
  55. package/src/tileTexture/TileTextureCompositor.ts +248 -32
  56. package/src/tileTexture/__snapshots__/CompositeShader.snapshot.test.ts.snap +36 -16
  57. package/src/tileTexture/types.ts +7 -0
  58. package/src/utils/demNoDataColor.test.ts +47 -0
  59. package/src/utils/demNoDataColor.ts +48 -0
  60. package/src/utils/fixedGpuFootprint.test.ts +110 -0
  61. package/src/utils/fixedGpuFootprint.ts +162 -0
  62. package/src/worker/tasks/toCreasedNormals.ts +2 -7
  63. package/dist/assets/fontWorker-CEJEfRpb.js +0 -2
  64. package/dist/assets/fontWorker-CEJEfRpb.js.map +0 -1
  65. package/dist/assets/index-BUB-ijMO.js +0 -12
  66. package/dist/assets/index-BUB-ijMO.js.map +0 -1
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Solve `dot(rgb, scaler) == boundary` for RGB bytes: the color a DEM texel
3
+ * must hold so the shader's `decodeDEMHeight` classifies it as no-data. The
4
+ * raster bake paints a baked heatmap render target with this color before
5
+ * drawing sources, so uncovered regions decode as "no elevation here" —
6
+ * entirely inside the encoded-elevation domain, leaving the alpha channel
7
+ * untouched (it is not part of any encoding and stays reserved for future
8
+ * RGBA ones).
9
+ *
10
+ * Solved by greedy positional decomposition (largest scaler first), which is
11
+ * exact for the positional presets (GSI, Mapbox, Terrarium); a non-exact
12
+ * decomposition is still accepted when it lands inside the shader's no-data
13
+ * tolerance band (`decodeDEMHeight` flags |x − boundary| <= 1). Returns null
14
+ * only when no byte color decodes within that band (an exotic custom decoder
15
+ * whose encoding cannot express no-data at all) — callers then skip the
16
+ * no-data underlay and a baked target's uncovered regions decode as whatever
17
+ * black decodes to, which the shader may colormap as a valid height.
18
+ */
19
+ export function demNoDataColorBytes(
20
+ scaler: { x: number; y: number; z: number },
21
+ boundary: number,
22
+ ): [number, number, number] | null {
23
+ const channels: { scaler: number; index: number }[] = [
24
+ { scaler: scaler.x, index: 0 },
25
+ { scaler: scaler.y, index: 1 },
26
+ { scaler: scaler.z, index: 2 },
27
+ ]
28
+ .filter((c) => c.scaler > 0)
29
+ .sort((a, b) => b.scaler - a.scaler);
30
+
31
+ const rgb: [number, number, number] = [0, 0, 0];
32
+ let rest = boundary;
33
+ for (const { scaler: s, index } of channels) {
34
+ // The epsilon guards float division just below an integer (e.g. 0.999…).
35
+ const v = Math.min(255, Math.max(0, Math.floor(rest / s + 1e-9)));
36
+ rgb[index] = v;
37
+ rest -= v * s;
38
+ }
39
+
40
+ // The shader flags |x - boundary| <= 1 as no-data (dem_util.glsl,
41
+ // `epsilon_cmp`): accept exactly that band, boundary included — a color a
42
+ // full 1 away still classifies as no-data, so rejecting it would skip the
43
+ // underlay and let uncovered baked regions decode black as a valid height.
44
+ // No safety margin is subtracted: painting a band-edge color that float32
45
+ // drift pushes out of the band decodes as a near-boundary height, which is
46
+ // no worse than the black the skipped underlay would leave behind.
47
+ return Math.abs(rest) <= 1 ? rgb : null;
48
+ }
@@ -0,0 +1,110 @@
1
+ import type { EffectComposer, Pass } from "postprocessing";
2
+ import { DepthTexture, HalfFloatType, WebGLRenderTarget } from "three";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ import { estimateFixedGpuBytes } from "./fixedGpuFootprint";
6
+
7
+ const W = 100;
8
+ const H = 10;
9
+ const PX = W * H;
10
+
11
+ function composerWith(
12
+ input: WebGLRenderTarget,
13
+ output: WebGLRenderTarget,
14
+ ): EffectComposer {
15
+ return { inputBuffer: input, outputBuffer: output } as EffectComposer;
16
+ }
17
+
18
+ function rgba8Target(): WebGLRenderTarget {
19
+ return new WebGLRenderTarget(W, H, { depthBuffer: false });
20
+ }
21
+
22
+ describe("estimateFixedGpuBytes", () => {
23
+ it("sums composer buffers by texture type and depth attachment", () => {
24
+ const input = new WebGLRenderTarget(W, H, {
25
+ type: HalfFloatType,
26
+ depthBuffer: true,
27
+ });
28
+ const output = new WebGLRenderTarget(W, H, { depthBuffer: false });
29
+ const bytes = estimateFixedGpuBytes(composerWith(input, output), []);
30
+ // input: RGBA16F (8) + depth (4); output: RGBA8 (4)
31
+ expect(bytes).toBe(PX * (8 + 4) + PX * 4);
32
+ });
33
+
34
+ it("counts a render target reachable from two passes only once", () => {
35
+ const shared = rgba8Target();
36
+ const passes = [
37
+ { renderTarget: shared } as unknown as Pass,
38
+ { anotherRef: shared } as unknown as Pass,
39
+ ];
40
+ const bytes = estimateFixedGpuBytes(
41
+ composerWith(rgba8Target(), rgba8Target()),
42
+ passes,
43
+ );
44
+ expect(bytes).toBe(PX * 4 * 3);
45
+ });
46
+
47
+ it("finds targets nested in pass properties and arrays", () => {
48
+ const nested = { copyPass: { target: rgba8Target() } } as unknown as Pass;
49
+ const inArray = {
50
+ effects: [{ renderTarget: rgba8Target() }],
51
+ } as unknown as Pass;
52
+ const bytes = estimateFixedGpuBytes(
53
+ composerWith(rgba8Target(), rgba8Target()),
54
+ [nested, inArray],
55
+ );
56
+ expect(bytes).toBe(PX * 4 * 4);
57
+ });
58
+
59
+ it("adds MSAA renderbuffer bytes on multisampled targets", () => {
60
+ const msaa = new WebGLRenderTarget(W, H, {
61
+ samples: 4,
62
+ depthBuffer: true,
63
+ });
64
+ const bytes = estimateFixedGpuBytes(composerWith(msaa, rgba8Target()), []);
65
+ // msaa: RGBA8 resolve (4) + depth (4) + 4 samples × (4 + 4); output: 4
66
+ expect(bytes).toBe(PX * (4 + 4 + 4 * 8) + PX * 4);
67
+ });
68
+
69
+ it("counts every MRT attachment and depth textures in the MSAA term", () => {
70
+ const mrt = new WebGLRenderTarget(W, H, {
71
+ count: 2,
72
+ samples: 2,
73
+ depthBuffer: false,
74
+ });
75
+ mrt.textures[1].type = HalfFloatType;
76
+ mrt.depthTexture = new DepthTexture(W, H);
77
+ const bytes = estimateFixedGpuBytes(composerWith(mrt, rgba8Target()), []);
78
+ // mrt: RGBA8 (4) + RGBA16F (8) + depth texture (4)
79
+ // + 2 samples × (4 + 8 + 4); output: 4
80
+ expect(bytes).toBe(PX * (4 + 8 + 4 + 2 * 16) + PX * 4);
81
+ });
82
+
83
+ it("skips render targets that are not GL-allocated yet", () => {
84
+ const allocated = rgba8Target();
85
+ const lazy = rgba8Target();
86
+ const composer = {
87
+ inputBuffer: allocated,
88
+ outputBuffer: lazy,
89
+ getRenderer: () => ({
90
+ properties: {
91
+ get: (object: object) =>
92
+ object === allocated ? { __webglFramebuffer: {} } : {},
93
+ },
94
+ }),
95
+ } as unknown as EffectComposer;
96
+ expect(estimateFixedGpuBytes(composer, [])).toBe(PX * 4);
97
+ });
98
+
99
+ it("does not walk into typed arrays or huge buffers", () => {
100
+ const pass = {
101
+ geometryData: new Float32Array(1_000_000),
102
+ target: rgba8Target(),
103
+ } as unknown as Pass;
104
+ const bytes = estimateFixedGpuBytes(
105
+ composerWith(rgba8Target(), rgba8Target()),
106
+ [pass],
107
+ );
108
+ expect(bytes).toBe(PX * 4 * 3);
109
+ });
110
+ });
@@ -0,0 +1,162 @@
1
+ import type { EffectComposer, Pass } from "postprocessing";
2
+ import {
3
+ FloatType,
4
+ HalfFloatType,
5
+ Material,
6
+ Object3D,
7
+ Texture,
8
+ WebGLRenderTarget,
9
+ } from "three";
10
+
11
+ const DEPTH_BYTES_PER_TEXEL = 4; // DEPTH24_STENCIL8 / DEPTH_COMPONENT32F
12
+ const MIPMAP_FACTOR = 1.33;
13
+
14
+ function bytesPerTexel(type: number): number {
15
+ switch (type) {
16
+ case FloatType:
17
+ return 16; // RGBA32F
18
+ case HalfFloatType:
19
+ return 8; // RGBA16F
20
+ default:
21
+ return 4; // RGBA8 and friends
22
+ }
23
+ }
24
+
25
+ function renderTargetBytes(target: WebGLRenderTarget): number {
26
+ const pixels = target.width * target.height;
27
+ let bytes = 0;
28
+ for (const texture of target.textures) {
29
+ let textureBytes = pixels * bytesPerTexel(texture.type);
30
+ if (texture.generateMipmaps) {
31
+ textureBytes = Math.floor(textureBytes * MIPMAP_FACTOR);
32
+ }
33
+ bytes += textureBytes;
34
+ }
35
+ if (target.depthBuffer || target.depthTexture) {
36
+ bytes += pixels * DEPTH_BYTES_PER_TEXEL;
37
+ }
38
+ if (target.samples > 0) {
39
+ // MSAA renderbuffers exist alongside the resolve textures: one per color
40
+ // attachment, plus a depth renderbuffer whether depth is a plain buffer
41
+ // or a depth texture.
42
+ let msaaBytesPerPixel = 0;
43
+ for (const texture of target.textures) {
44
+ msaaBytesPerPixel += bytesPerTexel(texture.type);
45
+ }
46
+ if (target.depthBuffer || target.depthTexture) {
47
+ msaaBytesPerPixel += DEPTH_BYTES_PER_TEXEL;
48
+ }
49
+ bytes += target.samples * pixels * msaaBytesPerPixel;
50
+ }
51
+ return bytes;
52
+ }
53
+
54
+ function collectRenderTargets(
55
+ value: unknown,
56
+ targets: Set<WebGLRenderTarget>,
57
+ visited: Set<object>,
58
+ depth: number,
59
+ ): void {
60
+ if (value === null || typeof value !== "object" || visited.has(value)) {
61
+ return;
62
+ }
63
+ visited.add(value);
64
+
65
+ if (value instanceof WebGLRenderTarget) {
66
+ targets.add(value);
67
+ return;
68
+ }
69
+
70
+ // Prune subtrees that cannot contain render targets: scene graphs,
71
+ // materials, textures, and raw buffers (Object.keys on a large typed array
72
+ // would enumerate every element).
73
+ if (
74
+ depth <= 0 ||
75
+ ArrayBuffer.isView(value) ||
76
+ value instanceof ArrayBuffer ||
77
+ value instanceof Object3D ||
78
+ value instanceof Material ||
79
+ value instanceof Texture
80
+ ) {
81
+ return;
82
+ }
83
+
84
+ // Shallow property scan, mirroring postprocessing's Pass.dispose() search.
85
+ // Recursion is bounded so nested passes/effects (e.g. CustomRenderPass's
86
+ // copy passes, EffectPass's effects) are covered without walking the scene.
87
+ for (const key of Object.keys(value)) {
88
+ const child = (value as Record<string, unknown>)[key];
89
+ if (Array.isArray(child)) {
90
+ for (const item of child) {
91
+ collectRenderTargets(item, targets, visited, depth - 1);
92
+ }
93
+ } else {
94
+ collectRenderTargets(child, targets, visited, depth - 1);
95
+ }
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Returns whether the render target has actually been allocated on the GPU.
101
+ * Three.js allocates lazily on first `setRenderTarget`, so targets that a
102
+ * pipeline never renders into (e.g. a `CopyPass` used with `renderToScreen`,
103
+ * the composer's output buffer while no pass swaps, mode-dependent merge
104
+ * targets) hold no GPU memory and must not be charged to the ledger.
105
+ * `renderer.properties` is an internal-but-stable API (postprocessing itself
106
+ * relies on it); when it is unavailable, assume allocated.
107
+ */
108
+ function isAllocated(renderer: unknown, target: WebGLRenderTarget): boolean {
109
+ const properties = (
110
+ renderer as {
111
+ properties?: { get(object: object): { __webglFramebuffer?: unknown } };
112
+ } | null
113
+ )?.properties;
114
+ if (!properties) return true;
115
+ return properties.get(target).__webglFramebuffer !== undefined;
116
+ }
117
+
118
+ /**
119
+ * Estimates the resident GPU bytes of the fixed (screen-sized, non-tile)
120
+ * render-target stack: the composer's ping-pong and depth buffers plus every
121
+ * render target reachable from the registered passes and their effects.
122
+ * Targets not yet allocated on the GPU are skipped, so the estimate should be
123
+ * re-taken after rendering (allocations happen lazily during the first frames
124
+ * that exercise a code path).
125
+ *
126
+ * The result is an estimate in the same accuracy class as the ledger's
127
+ * `gpu_bytes_est`: texture formats are mapped to nominal byte sizes and
128
+ * driver padding is ignored.
129
+ */
130
+ export function estimateFixedGpuBytes(
131
+ composer: EffectComposer,
132
+ passes: Iterable<Pass>,
133
+ ): number {
134
+ const targets = new Set<WebGLRenderTarget>();
135
+ const visited = new Set<object>();
136
+
137
+ collectRenderTargets(composer.inputBuffer, targets, visited, 0);
138
+ collectRenderTargets(composer.outputBuffer, targets, visited, 0);
139
+ // Stable-depth target (postprocessing >= 6.39); private, hence the cast.
140
+ collectRenderTargets(
141
+ (composer as unknown as { depthRenderTarget?: WebGLRenderTarget | null })
142
+ .depthRenderTarget,
143
+ targets,
144
+ visited,
145
+ 0,
146
+ );
147
+
148
+ for (const pass of passes) {
149
+ collectRenderTargets(pass, targets, visited, 3);
150
+ }
151
+
152
+ const renderer = (
153
+ composer as unknown as { getRenderer?: () => unknown }
154
+ ).getRenderer?.();
155
+
156
+ let bytes = 0;
157
+ for (const target of targets) {
158
+ if (!isAllocated(renderer, target)) continue;
159
+ bytes += renderTargetBytes(target);
160
+ }
161
+ return bytes;
162
+ }
@@ -1,15 +1,10 @@
1
1
  // Ref: https://github.com/takram-design-engineering/geovanni/blob/ee91f675ba2558ee3099f635dfa30bbe3adfe103/libs/3d-tiles/src/toCreasedNormalsAsync.ts
2
2
 
3
- /* eslint-env worker */
4
-
5
3
  import { transfer } from "@navaramap/worker";
6
4
  import { toCreasedNormals as toCreasedNormalsImpl } from "three-stdlib";
7
5
 
8
- import {
9
- fromBufferGeometryLike,
10
- toBufferGeometryLike,
11
- type BufferGeometryLike,
12
- } from "../../utils";
6
+ import { fromBufferGeometryLike, toBufferGeometryLike } from "../../utils";
7
+ import type { BufferGeometryLike } from "../../utils";
13
8
 
14
9
  export function toCreasedNormals(
15
10
  input: BufferGeometryLike,