@navaramap/three 0.0.1 → 0.0.3
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-wz61edjR.js +2 -0
- package/dist/assets/fontWorker-wz61edjR.js.map +1 -0
- package/dist/assets/index-ywHSGImj.js +25 -0
- package/dist/assets/index-ywHSGImj.js.map +1 -0
- package/dist/assets/{navara_wasm_font_worker_bg-Bnck8OGf.wasm → navara_wasm_font_worker_bg-Du9nb2Pn.wasm} +0 -0
- package/dist/assets/{navara_wasm_worker_bg-DG9ORAmj.wasm → navara_wasm_worker_bg-Xadu67fw.wasm} +0 -0
- package/dist/draco_decoder.wasm +0 -0
- package/dist/draco_decoder2.wasm +0 -0
- package/dist/index.d.ts +578 -18
- package/dist/index.js +5298 -4843
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +266 -234
- package/dist/index.umd.cjs.map +1 -1
- package/package.json +34 -33
- package/src/atmosphere.ts +43 -1
- 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 +12 -0
- package/src/event/feature.ts +43 -13
- package/src/event/features/modelGpuBytes.ts +1 -3
- package/src/event/features/polygon.ts +2 -1
- 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 +137 -35
- package/src/input.ts +48 -15
- package/src/material/enhancer/ShaderReplacer.ts +1 -2
- package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/mutates.test.ts +1 -0
- package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/mutates.ts +2 -0
- package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/types.ts +2 -0
- package/src/material/enhancer/polygon/polygonBaseEnhancer/mutates.ts +2 -0
- package/src/material/enhancer/polygon/polygonBaseEnhancer/shader.ts +5 -1
- package/src/material/enhancer/polygon/polygonBaseEnhancer/types.ts +2 -0
- package/src/material/enhancer/polyline/polylineBaseEnhancer/mutates.ts +2 -0
- package/src/material/enhancer/polyline/polylineBaseEnhancer/types.ts +2 -0
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/mutates.ts +8 -0
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/types.ts +13 -0
- package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.test.ts +83 -0
- package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.ts +13 -3
- package/src/material/overrideMaterialsForMRT.ts +7 -2
- package/src/mesh/batchTexture.ts +2 -4
- package/src/mesh/batchedSdfText.ts +294 -70
- package/src/mesh/instanced.ts +1 -1
- package/src/mesh/polygon.ts +11 -4
- package/src/mesh/polyline.ts +10 -2
- package/src/mesh/sdfText.ts +176 -6
- package/src/mesh/sprite/instancedSprite.ts +245 -3
- package/src/orchestrators/RenderPassOrchestrator.ts +25 -0
- package/src/passes/CustomRenderPass.ts +21 -5
- package/src/passes/RenderTargetCopyPass.ts +24 -2
- package/src/solar.test.ts +33 -0
- package/src/solar.ts +31 -0
- package/src/test-utils/engine.ts +53 -0
- package/src/tileTexture/TileTextureCompositor.test.ts +2 -0
- package/src/tileTexture/TileTextureCompositor.ts +7 -1
- 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-CCWL5pbx.js +0 -2
- package/dist/assets/fontWorker-CCWL5pbx.js.map +0 -1
- package/dist/assets/index-BdtHOOT9.js +0 -12
- package/dist/assets/index-BdtHOOT9.js.map +0 -1
|
@@ -2,11 +2,13 @@ import { Globe } from "@navaramap/core";
|
|
|
2
2
|
import { DepthCopyPass } from "postprocessing";
|
|
3
3
|
import {
|
|
4
4
|
Color,
|
|
5
|
+
DepthStencilFormat,
|
|
5
6
|
DepthTexture,
|
|
6
7
|
HalfFloatType,
|
|
7
8
|
NearestFilter,
|
|
8
9
|
RGBADepthPacking,
|
|
9
10
|
Scene,
|
|
11
|
+
UnsignedInt248Type,
|
|
10
12
|
WebGLRenderTarget,
|
|
11
13
|
type PerspectiveCamera,
|
|
12
14
|
type WebGLRenderer,
|
|
@@ -90,7 +92,20 @@ export class CustomRenderPass extends RenderPass {
|
|
|
90
92
|
emissiveTexture.type = HalfFloatType;
|
|
91
93
|
this.gbufferRenderTarget.textures.push(emissiveTexture);
|
|
92
94
|
|
|
95
|
+
// The gbuffer owns its depth texture instead of sharing the composer's one
|
|
96
|
+
// (postprocessing >= 6.39 no longer exposes a writable shared depth
|
|
97
|
+
// texture). Depth is propagated to the composer's input buffer explicitly
|
|
98
|
+
// by the copy pass below. Stencil is required for draped-mesh rendering.
|
|
99
|
+
const gbufferDepthTexture = new DepthTexture(
|
|
100
|
+
this.gbufferRenderTarget.width,
|
|
101
|
+
this.gbufferRenderTarget.height,
|
|
102
|
+
UnsignedInt248Type,
|
|
103
|
+
);
|
|
104
|
+
gbufferDepthTexture.format = DepthStencilFormat;
|
|
105
|
+
this.gbufferRenderTarget.depthTexture = gbufferDepthTexture;
|
|
106
|
+
|
|
93
107
|
this.copyPass = new RenderTargetCopyPass(this.gbufferRenderTarget);
|
|
108
|
+
this.copyPass.setDepthTexture(gbufferDepthTexture);
|
|
94
109
|
|
|
95
110
|
this.globeDepthCopyPass = new DepthCopyPass({
|
|
96
111
|
depthPacking: RGBADepthPacking,
|
|
@@ -240,11 +255,12 @@ export class CustomRenderPass extends RenderPass {
|
|
|
240
255
|
this.allDepthCopyPass.render(renderer, null, null);
|
|
241
256
|
}
|
|
242
257
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
258
|
+
// The composer-provided depth texture is intentionally ignored: the gbuffer
|
|
259
|
+
// owns its depth texture and depth is copied into the input buffer by the
|
|
260
|
+
// copy pass. `needsDepthTexture` stays true so the composer still attaches a
|
|
261
|
+
// depth texture to the input buffer, which `allDepthCopyPass` reads after
|
|
262
|
+
// the opaque scene has been rendered.
|
|
263
|
+
setDepthTexture(): void {}
|
|
248
264
|
|
|
249
265
|
setSize(width: number, height: number) {
|
|
250
266
|
this.gbufferRenderTarget.setSize(width, height);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import PostProcessingCommon from "@shaders/glsl/postprocessingCommon.vert.glsl";
|
|
2
2
|
import { Pass } from "postprocessing";
|
|
3
3
|
import {
|
|
4
|
+
AlwaysDepth,
|
|
4
5
|
NoBlending,
|
|
5
6
|
ShaderMaterial,
|
|
6
7
|
Texture,
|
|
@@ -10,13 +11,18 @@ import {
|
|
|
10
11
|
|
|
11
12
|
const fragmentShader = `
|
|
12
13
|
uniform sampler2D color;
|
|
14
|
+
|
|
15
|
+
#ifdef COPY_DEPTH
|
|
13
16
|
uniform sampler2D depth;
|
|
17
|
+
#endif
|
|
14
18
|
|
|
15
19
|
varying vec2 vUv;
|
|
16
20
|
|
|
17
21
|
void main() {
|
|
18
22
|
gl_FragColor = texture2D(color, vUv);
|
|
19
|
-
|
|
23
|
+
#ifdef COPY_DEPTH
|
|
24
|
+
gl_FragDepth = texture2D(depth, vUv).r;
|
|
25
|
+
#endif
|
|
20
26
|
}
|
|
21
27
|
`;
|
|
22
28
|
|
|
@@ -71,8 +77,24 @@ export class RenderTargetCopyPass extends Pass {
|
|
|
71
77
|
this._material.uniforms.color.value = texture;
|
|
72
78
|
}
|
|
73
79
|
|
|
74
|
-
|
|
80
|
+
/**
|
|
81
|
+
* When a depth texture is set, the pass copies its depth into the target's
|
|
82
|
+
* depth buffer via gl_FragDepth. Depth test must be enabled (with AlwaysDepth)
|
|
83
|
+
* because GL discards depth writes while the depth test is disabled.
|
|
84
|
+
*/
|
|
85
|
+
setDepthTexture(texture: Texture | null): void {
|
|
75
86
|
this._material.uniforms.depth.value = texture;
|
|
87
|
+
const copyDepth = texture !== null;
|
|
88
|
+
if ("COPY_DEPTH" in this._material.defines === copyDepth) return;
|
|
89
|
+
if (copyDepth) {
|
|
90
|
+
this._material.defines.COPY_DEPTH = "";
|
|
91
|
+
} else {
|
|
92
|
+
delete this._material.defines.COPY_DEPTH;
|
|
93
|
+
}
|
|
94
|
+
this._material.depthWrite = copyDepth;
|
|
95
|
+
this._material.depthTest = copyDepth;
|
|
96
|
+
this._material.depthFunc = AlwaysDepth;
|
|
97
|
+
this._material.needsUpdate = true;
|
|
76
98
|
}
|
|
77
99
|
|
|
78
100
|
render(
|
package/src/solar.test.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { AstroTime, Body, HourAngle, Observer } from "astronomy-engine";
|
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
+
dateForSolarTime,
|
|
6
|
+
getSolarTime,
|
|
5
7
|
getSunElevation,
|
|
6
8
|
shiftDateToElevation,
|
|
7
9
|
shiftDateToHourAngle,
|
|
@@ -66,6 +68,37 @@ describe("shiftDateToElevation", () => {
|
|
|
66
68
|
});
|
|
67
69
|
});
|
|
68
70
|
|
|
71
|
+
describe("getSolarTime / dateForSolarTime", () => {
|
|
72
|
+
it("reads ~12 (solar noon) at Tokyo solar noon", () => {
|
|
73
|
+
// Tokyo solar noon ≈ 02:43 UTC on 2024-06-21
|
|
74
|
+
const date = new Date("2024-06-21T02:43:00Z");
|
|
75
|
+
expect(getSolarTime(date, 139.69)).toBeCloseTo(12, 1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("reads ~0 (solar midnight) at Tokyo solar midnight", () => {
|
|
79
|
+
// Tokyo solar midnight ≈ 14:43 UTC on 2024-06-21
|
|
80
|
+
const date = new Date("2024-06-21T14:43:00Z");
|
|
81
|
+
const t = getSolarTime(date, 139.69);
|
|
82
|
+
expect(Math.min(t, 24 - t)).toBeCloseTo(0, 1);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("round-trips: dateForSolarTime then getSolarTime returns the input", () => {
|
|
86
|
+
for (const hours of [0, 6.3, 12, 18.75]) {
|
|
87
|
+
const date = dateForSolarTime(BASE_DATE, 139.69, hours);
|
|
88
|
+
// Compare on the 24h circle so 0 and 24 count as equal.
|
|
89
|
+
const diff = Math.abs(getSolarTime(date, 139.69) - hours);
|
|
90
|
+
expect(Math.min(diff, 24 - diff)).toBeCloseTo(0, 2);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("puts the sun above the horizon at midday and below at midnight", () => {
|
|
95
|
+
const noon = dateForSolarTime(BASE_DATE, 139.69, 12);
|
|
96
|
+
expect(getSunElevation(noon, 35.68, 139.69)).toBeGreaterThan(0);
|
|
97
|
+
const midnight = dateForSolarTime(BASE_DATE, 139.69, 0);
|
|
98
|
+
expect(getSunElevation(midnight, 35.68, 139.69)).toBeLessThan(0);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
69
102
|
describe("getSunElevation", () => {
|
|
70
103
|
it("returns positive elevation at solar noon", () => {
|
|
71
104
|
// Tokyo solar noon ≈ 02:43 UTC on 2024-06-21
|
package/src/solar.ts
CHANGED
|
@@ -20,6 +20,37 @@ export function getSunElevation(date: Date, lat: number, lng: number): number {
|
|
|
20
20
|
return Horizon(time, observer, eq.ra, eq.dec, "normal").altitude;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Returns the local apparent solar time at `lng` for `date`, in hours in the
|
|
25
|
+
* range [0, 24) where 12 is solar noon. Derived from the sun's hour angle, so
|
|
26
|
+
* it accounts for the equation of time.
|
|
27
|
+
*/
|
|
28
|
+
export function getSolarTime(date: Date, lng: number): number {
|
|
29
|
+
const ha = HourAngle(Body.Sun, new AstroTime(date), new Observer(0, lng, 0));
|
|
30
|
+
return (ha + 12) % 24;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Returns a new Date on the same UTC-anchored solar day such that the local
|
|
35
|
+
* apparent solar time at `lng` equals `hours` (0–24). Inverse of
|
|
36
|
+
* {@link getSolarTime}.
|
|
37
|
+
*/
|
|
38
|
+
export function dateForSolarTime(date: Date, lng: number, hours: number): Date {
|
|
39
|
+
const targetHourAngle = (((hours - 12) % 24) + 24) % 24;
|
|
40
|
+
const dayStart = new AstroTime(
|
|
41
|
+
new Date(
|
|
42
|
+
Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()),
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
return SearchHourAngle(
|
|
46
|
+
Body.Sun,
|
|
47
|
+
new Observer(0, lng, 0),
|
|
48
|
+
targetHourAngle,
|
|
49
|
+
dayStart,
|
|
50
|
+
1,
|
|
51
|
+
).time.date;
|
|
52
|
+
}
|
|
53
|
+
|
|
23
54
|
/**
|
|
24
55
|
* Returns a new Date such that the sun's hour angle at `targetLng` matches the
|
|
25
56
|
* hour angle currently at `fromLng`.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
|
|
4
|
+
import { Core, initSync } from "@navaramap/engine";
|
|
5
|
+
|
|
6
|
+
import type { TileHandler } from "../event/context";
|
|
7
|
+
import { createTileHandler } from "../event/tileHandler";
|
|
8
|
+
|
|
9
|
+
let engineInitialized = false;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Initialize the engine WASM once per vitest worker. The web-target module
|
|
13
|
+
* normally streams the binary over `fetch` (`initCore`), which Node cannot do
|
|
14
|
+
* for workspace files — so the bytes are read from the `@navaramap/engine`
|
|
15
|
+
* package and compiled synchronously. Repeat calls are no-ops; every test
|
|
16
|
+
* file that touches Rust-backed functions can call this without coordination.
|
|
17
|
+
*/
|
|
18
|
+
export function initTestEngine(): void {
|
|
19
|
+
if (engineInitialized) return;
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const wasmPath = require.resolve("@navaramap/engine/navara_wasm_bg.wasm");
|
|
22
|
+
initSync({ module: readFileSync(wasmPath) });
|
|
23
|
+
engineInitialized = true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let sharedCore: Core | undefined;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Lazily created `Core` shared across a vitest worker's test files.
|
|
30
|
+
* `Core::new` only builds the Bevy app (no DOM access), so it is safe in the
|
|
31
|
+
* Node test environment. Shared deliberately: tests exercise Rust-backed
|
|
32
|
+
* queries against one engine instead of paying a per-file world setup.
|
|
33
|
+
*/
|
|
34
|
+
export function getTestCore(): Core {
|
|
35
|
+
initTestEngine();
|
|
36
|
+
sharedCore ??= new Core("navara-test");
|
|
37
|
+
return sharedCore;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The production {@link TileHandler} assembled over the shared test engine —
|
|
42
|
+
* tests reach the same Rust functions (e.g. `mercatorY`) the app wires through
|
|
43
|
+
* the event context, instead of stubbing them. The `Core` is created lazily on
|
|
44
|
+
* first Core-backed call, so pure-function consumers never construct it.
|
|
45
|
+
*/
|
|
46
|
+
export function createTestTileHandler(): TileHandler {
|
|
47
|
+
initTestEngine();
|
|
48
|
+
return createTileHandler({
|
|
49
|
+
getCore: () => getTestCore(),
|
|
50
|
+
getVectorRevision: () => 0,
|
|
51
|
+
getRasterRevision: () => 0,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -2,6 +2,7 @@ import { Mesh, BoxGeometry, MeshBasicMaterial } from "three";
|
|
|
2
2
|
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
|
|
4
4
|
import { TexturizedSceneByTileCoordinates } from "../scene";
|
|
5
|
+
import { createTestTileHandler } from "../test-utils/engine";
|
|
5
6
|
|
|
6
7
|
import { TileTextureCompositor } from "./TileTextureCompositor";
|
|
7
8
|
import type { AtlasFactory, CompositeAtlas } from "./types";
|
|
@@ -47,6 +48,7 @@ function setup() {
|
|
|
47
48
|
typeof TileTextureCompositor
|
|
48
49
|
>[0]["renderer"],
|
|
49
50
|
texturizedSceneByTileCoordinates: texturizedScenes,
|
|
51
|
+
mercatorY: createTestTileHandler().mercatorY,
|
|
50
52
|
atlasFactory: fakeFactory,
|
|
51
53
|
});
|
|
52
54
|
return { compositor, renderer, texturizedScenes };
|
|
@@ -77,6 +77,10 @@ export const defaultAtlasFactory =
|
|
|
77
77
|
export type TileTextureCompositorOptions = {
|
|
78
78
|
renderer: WebGLRenderer;
|
|
79
79
|
texturizedSceneByTileCoordinates: TexturizedSceneByTileCoordinates;
|
|
80
|
+
/** Rust's clamped WebMercator northing (`TileHandler.mercatorY`), handed to
|
|
81
|
+
* the core uniform mutates so the paste-side reprojection constants come
|
|
82
|
+
* from the same implementation as the Rust bake affine. */
|
|
83
|
+
mercatorY: (lat: number) => number;
|
|
80
84
|
/** Atlas RT side length (defaults to 512). */
|
|
81
85
|
size?: number;
|
|
82
86
|
/** Test seam: replace MRT RT creation. */
|
|
@@ -142,6 +146,7 @@ export class TileTextureCompositor {
|
|
|
142
146
|
* per-tile GPU byte cost it reports to the memory ledger. */
|
|
143
147
|
readonly size: number;
|
|
144
148
|
private readonly texturizedScenes: TexturizedSceneByTileCoordinates;
|
|
149
|
+
private readonly mercatorY: (lat: number) => number;
|
|
145
150
|
// Single camera shared by every drape bake (vector and raster), re-framed
|
|
146
151
|
// per source by `frameBakeCamera`. Ancestor fallback is resolved in Rust,
|
|
147
152
|
// so no per-tile camera transform is needed.
|
|
@@ -189,6 +194,7 @@ export class TileTextureCompositor {
|
|
|
189
194
|
constructor(opts: TileTextureCompositorOptions) {
|
|
190
195
|
this.renderer = opts.renderer;
|
|
191
196
|
this.texturizedScenes = opts.texturizedSceneByTileCoordinates;
|
|
197
|
+
this.mercatorY = opts.mercatorY;
|
|
192
198
|
this.size = opts.size ?? 512;
|
|
193
199
|
this.cache = new TileTextureCache({
|
|
194
200
|
size: this.size,
|
|
@@ -560,7 +566,7 @@ export class TileTextureCompositor {
|
|
|
560
566
|
// uniform ownership: each active enhancer attaches its own uniform refs and
|
|
561
567
|
// contributes the matching GLSL, so a new expression touches only its module.
|
|
562
568
|
const chain = createCompositeLayerEnhancers(features);
|
|
563
|
-
const core = createCoreUniformMutates();
|
|
569
|
+
const core = createCoreUniformMutates(this.mercatorY);
|
|
564
570
|
|
|
565
571
|
const uniforms: CompositeUniformTarget = {};
|
|
566
572
|
core.attachUniforms(uniforms, numTextures, placeholderTexture);
|
|
@@ -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,
|