@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.
Files changed (72) hide show
  1. package/README.md +70 -0
  2. package/dist/assets/fontWorker-wz61edjR.js +2 -0
  3. package/dist/assets/fontWorker-wz61edjR.js.map +1 -0
  4. package/dist/assets/index-ywHSGImj.js +25 -0
  5. package/dist/assets/index-ywHSGImj.js.map +1 -0
  6. package/dist/assets/{navara_wasm_font_worker_bg-Bnck8OGf.wasm → navara_wasm_font_worker_bg-Du9nb2Pn.wasm} +0 -0
  7. package/dist/assets/{navara_wasm_worker_bg-DG9ORAmj.wasm → navara_wasm_worker_bg-Xadu67fw.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 +578 -18
  11. package/dist/index.js +5298 -4843
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.umd.cjs +266 -234
  14. package/dist/index.umd.cjs.map +1 -1
  15. package/package.json +34 -33
  16. package/src/atmosphere.ts +43 -1
  17. package/src/core/EffectDesc.ts +2 -4
  18. package/src/core/MeshDesc.ts +1 -2
  19. package/src/core/MeshDescWithSelectiveEffect.ts +1 -2
  20. package/src/declutter/DeclutterManager.test.ts +235 -0
  21. package/src/declutter/DeclutterManager.ts +256 -0
  22. package/src/declutter/index.ts +7 -0
  23. package/src/declutter/kernel.ts +46 -0
  24. package/src/declutter/types.ts +61 -0
  25. package/src/declutter/wasmKernel.ts +13 -0
  26. package/src/device.ts +18 -4
  27. package/src/evaluations/FeatureEvaluator.ts +21 -0
  28. package/src/event/context.ts +12 -0
  29. package/src/event/feature.ts +43 -13
  30. package/src/event/features/modelGpuBytes.ts +1 -3
  31. package/src/event/features/polygon.ts +2 -1
  32. package/src/event/hillshade.test.ts +7 -13
  33. package/src/event/index.ts +2 -3
  34. package/src/event/tileHandler.ts +54 -0
  35. package/src/index.ts +137 -35
  36. package/src/input.ts +48 -15
  37. package/src/material/enhancer/ShaderReplacer.ts +1 -2
  38. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/mutates.test.ts +1 -0
  39. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/mutates.ts +2 -0
  40. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/types.ts +2 -0
  41. package/src/material/enhancer/polygon/polygonBaseEnhancer/mutates.ts +2 -0
  42. package/src/material/enhancer/polygon/polygonBaseEnhancer/shader.ts +5 -1
  43. package/src/material/enhancer/polygon/polygonBaseEnhancer/types.ts +2 -0
  44. package/src/material/enhancer/polyline/polylineBaseEnhancer/mutates.ts +2 -0
  45. package/src/material/enhancer/polyline/polylineBaseEnhancer/types.ts +2 -0
  46. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/mutates.ts +8 -0
  47. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/types.ts +13 -0
  48. package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.test.ts +83 -0
  49. package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.ts +13 -3
  50. package/src/material/overrideMaterialsForMRT.ts +7 -2
  51. package/src/mesh/batchTexture.ts +2 -4
  52. package/src/mesh/batchedSdfText.ts +294 -70
  53. package/src/mesh/instanced.ts +1 -1
  54. package/src/mesh/polygon.ts +11 -4
  55. package/src/mesh/polyline.ts +10 -2
  56. package/src/mesh/sdfText.ts +176 -6
  57. package/src/mesh/sprite/instancedSprite.ts +245 -3
  58. package/src/orchestrators/RenderPassOrchestrator.ts +25 -0
  59. package/src/passes/CustomRenderPass.ts +21 -5
  60. package/src/passes/RenderTargetCopyPass.ts +24 -2
  61. package/src/solar.test.ts +33 -0
  62. package/src/solar.ts +31 -0
  63. package/src/test-utils/engine.ts +53 -0
  64. package/src/tileTexture/TileTextureCompositor.test.ts +2 -0
  65. package/src/tileTexture/TileTextureCompositor.ts +7 -1
  66. package/src/utils/fixedGpuFootprint.test.ts +110 -0
  67. package/src/utils/fixedGpuFootprint.ts +162 -0
  68. package/src/worker/tasks/toCreasedNormals.ts +2 -7
  69. package/dist/assets/fontWorker-CCWL5pbx.js +0 -2
  70. package/dist/assets/fontWorker-CCWL5pbx.js.map +0 -1
  71. package/dist/assets/index-BdtHOOT9.js +0 -12
  72. 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
- setDepthTexture(depthTexture: DepthTexture): void {
244
- this.gbufferRenderTarget.depthTexture = depthTexture;
245
- this.globeDepthCopyPass.setDepthTexture(depthTexture.clone());
246
- this.copyPass.setDepthTexture(depthTexture);
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
- gl_FragDepth = texture2D(color, vUv).r;
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
- setDepthTexture(texture: Texture): void {
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
- 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,