@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.
- package/README.md +70 -0
- package/dist/assets/fontWorker-8oWvyvwL.js +2 -0
- package/dist/assets/fontWorker-8oWvyvwL.js.map +1 -0
- package/dist/assets/index-D9G7kIEj.js +25 -0
- package/dist/assets/index-D9G7kIEj.js.map +1 -0
- package/dist/assets/{navara_wasm_font_worker_bg-665OLxjy.wasm → navara_wasm_font_worker_bg-CkZP9l2i.wasm} +0 -0
- package/dist/assets/{navara_wasm_worker_bg-BXhKC5bE.wasm → navara_wasm_worker_bg-Q8zrkLCR.wasm} +0 -0
- package/dist/draco_decoder.wasm +0 -0
- package/dist/draco_decoder2.wasm +0 -0
- package/dist/index.d.ts +587 -67
- package/dist/index.js +5640 -4944
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +238 -211
- package/dist/index.umd.cjs.map +1 -1
- package/package.json +35 -34
- package/src/core/EffectDesc.ts +2 -4
- package/src/core/MeshDesc.ts +1 -2
- package/src/core/MeshDescWithSelectiveEffect.ts +1 -2
- package/src/declutter/DeclutterManager.test.ts +235 -0
- package/src/declutter/DeclutterManager.ts +256 -0
- package/src/declutter/index.ts +7 -0
- package/src/declutter/kernel.ts +46 -0
- package/src/declutter/types.ts +61 -0
- package/src/declutter/wasmKernel.ts +13 -0
- package/src/device.ts +18 -4
- package/src/evaluations/FeatureEvaluator.ts +21 -0
- package/src/event/context.ts +17 -0
- package/src/event/features/modelGpuBytes.ts +1 -3
- package/src/event/hillshade.test.ts +7 -13
- package/src/event/index.ts +2 -3
- package/src/event/tileHandler.ts +54 -0
- package/src/index.ts +143 -33
- package/src/material/enhancer/ShaderReplacer.ts +1 -2
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/mutates.ts +6 -0
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/types.ts +11 -0
- package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.test.ts +83 -0
- package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.ts +19 -4
- package/src/material/enhancer/tileComposite/tileElevationHeatmapEnhancer/shader.ts +9 -2
- package/src/mesh/batchTexture.ts +2 -4
- package/src/mesh/batchedSdfText.ts +61 -1
- package/src/mesh/sdfText.ts +170 -5
- package/src/mesh/sprite/instancedSprite.ts +245 -3
- package/src/mesh/tile/drapeResolver.ts +76 -0
- package/src/mesh/{tile.test.ts → tile/index.test.ts} +16 -6
- package/src/mesh/{tile.ts → tile/index.ts} +124 -392
- package/src/mesh/tile/rasterDrapeResolver.ts +423 -0
- package/src/mesh/tile/vectorDrapeResolver.ts +404 -0
- package/src/orchestrators/RenderPassOrchestrator.ts +25 -0
- package/src/passes/CustomRenderPass.ts +21 -5
- package/src/passes/RenderTargetCopyPass.ts +24 -2
- package/src/test-utils/engine.ts +53 -0
- package/src/tileTexture/TileTextureCache.test.ts +2 -0
- package/src/tileTexture/TileTextureCache.ts +1 -0
- package/src/tileTexture/TileTextureCompositor.test.ts +107 -0
- package/src/tileTexture/TileTextureCompositor.ts +248 -32
- package/src/tileTexture/__snapshots__/CompositeShader.snapshot.test.ts.snap +36 -16
- package/src/tileTexture/types.ts +7 -0
- package/src/utils/demNoDataColor.test.ts +47 -0
- package/src/utils/demNoDataColor.ts +48 -0
- package/src/utils/fixedGpuFootprint.test.ts +110 -0
- package/src/utils/fixedGpuFootprint.ts +162 -0
- package/src/worker/tasks/toCreasedNormals.ts +2 -7
- package/dist/assets/fontWorker-CEJEfRpb.js +0 -2
- package/dist/assets/fontWorker-CEJEfRpb.js.map +0 -1
- package/dist/assets/index-BUB-ijMO.js +0 -12
- 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
|
-
|
|
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,
|