@zkmake/three-audit 0.2.2 → 0.3.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.
- package/README.md +59 -6
- package/dist/{audit-scene-BYsJcco9.js → audit-scene-CJG1DgRF.js} +157 -23
- package/dist/audit-scene-CJG1DgRF.js.map +1 -0
- package/dist/cli.js +6 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +123 -8
- package/dist/index.js +367 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/audit-scene-BYsJcco9.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BufferGeometry, Mesh, Object3D } from "three";
|
|
1
|
+
import { BatchedMesh, BufferGeometry, Mesh, Object3D } from "three";
|
|
2
2
|
//#region src/scene.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Any three.js object, from whichever copy of three's types the app has. The checks take this, so
|
|
@@ -32,6 +32,18 @@ type BadGeometryRow = {
|
|
|
32
32
|
};
|
|
33
33
|
/** Meshes with geometry that renders NaN, hidden ones included. `[]` is a pass. */
|
|
34
34
|
export declare const findBadGeometry: (root: AnyObject3D, options?: BadGeometryOptions) => BadGeometryRow[];
|
|
35
|
+
type EmptyMeshRow = {
|
|
36
|
+
/** `path/to/mesh [material]`. */
|
|
37
|
+
mesh: string;
|
|
38
|
+
/** Not enumerable: kept out of JSON and `console.table`. */
|
|
39
|
+
readonly object: Mesh;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Meshes with no vertices, hidden ones included: what a merge of nothing leaves behind. They draw
|
|
43
|
+
* nothing, but each one is still a draw call and a leak to dispose. A draw range of zero doesn't
|
|
44
|
+
* count: that's usually a pool waiting to fill.
|
|
45
|
+
*/
|
|
46
|
+
export declare const findEmptyMeshes: (root: AnyObject3D, options?: Pick<BadGeometryOptions, "skip" | "tagKey">) => EmptyMeshRow[];
|
|
35
47
|
//#endregion
|
|
36
48
|
//#region src/triangles.d.ts
|
|
37
49
|
type TriangleOptions = {
|
|
@@ -41,7 +53,7 @@ type MeshRow = {
|
|
|
41
53
|
/** `path/to/mesh`: its named ancestors, then its name (or its geometry's name or type). */
|
|
42
54
|
name: string;
|
|
43
55
|
material: string;
|
|
44
|
-
/** Triangles in one copy
|
|
56
|
+
/** Triangles in one copy. A batch's is an average: `geometryCensus` breaks it down. */
|
|
45
57
|
triangles: number;
|
|
46
58
|
instances: number;
|
|
47
59
|
total: number;
|
|
@@ -49,12 +61,28 @@ type MeshRow = {
|
|
|
49
61
|
readonly mesh: Mesh;
|
|
50
62
|
};
|
|
51
63
|
type GeometryRow = {
|
|
52
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* `geometry.name`, or its type and the start of its uuid. For a geometry in a batch, the batch's
|
|
66
|
+
* `label` for it, or `batch#id`.
|
|
67
|
+
*/
|
|
53
68
|
geometry: string;
|
|
54
69
|
/** Copies drawn: every mesh using it, instances counted. */
|
|
55
70
|
uses: number;
|
|
56
71
|
triangles: number;
|
|
57
72
|
total: number;
|
|
73
|
+
/** Its part of the census's total, 0–1. */
|
|
74
|
+
share: number;
|
|
75
|
+
/** With `budget`: whether `share` is over it. */
|
|
76
|
+
overBudget?: boolean;
|
|
77
|
+
};
|
|
78
|
+
type CensusOptions = TriangleOptions & {
|
|
79
|
+
/**
|
|
80
|
+
* Names a geometry inside a batch, e.g. from the app's own table of what it added. Default
|
|
81
|
+
* `batch#id`, the batch named by its path.
|
|
82
|
+
*/
|
|
83
|
+
label?: ((batch: BatchedMesh, geometryId: number) => string | undefined) | undefined;
|
|
84
|
+
/** Flag rows whose share of the total is over this, 0–1: `0.2` for anything over 20%. */
|
|
85
|
+
budget?: number | undefined;
|
|
58
86
|
};
|
|
59
87
|
/**
|
|
60
88
|
* Triangles one pass draws: visible meshes, instances counted, no frustum culling. A shadow pass
|
|
@@ -65,10 +93,16 @@ export declare const countTriangles: (root: AnyObject3D, options?: TriangleOptio
|
|
|
65
93
|
export declare const listMeshes: (scene: AnyObject3D, options?: TriangleOptions) => MeshRow[];
|
|
66
94
|
/**
|
|
67
95
|
* Geometries by triangles drawn, hidden meshes included, most first. Name geometries
|
|
68
|
-
* (`geometry.name`) to make this readable.
|
|
69
|
-
*
|
|
96
|
+
* (`geometry.name`) to make this readable. A batch counts each geometry in it separately, by its
|
|
97
|
+
* visible instances: the row a budget is really spent on.
|
|
98
|
+
*/
|
|
99
|
+
export declare const geometryCensus: (root: AnyObject3D, options?: CensusOptions) => GeometryRow[];
|
|
100
|
+
/**
|
|
101
|
+
* Draw calls one main pass makes: one per visible mesh, line or point cloud, or one per material
|
|
102
|
+
* group for a mesh with several materials. Before frustum culling; each shadow-casting light adds
|
|
103
|
+
* one per caster. `recordDrawLedger` counts the real thing.
|
|
70
104
|
*/
|
|
71
|
-
export declare const
|
|
105
|
+
export declare const countDraws: (root: AnyObject3D, options?: TriangleOptions) => number;
|
|
72
106
|
//#endregion
|
|
73
107
|
//#region src/z-fighting.d.ts
|
|
74
108
|
type ZFightingOptions = {
|
|
@@ -117,21 +151,46 @@ export declare const findZFighting: (scene: AnyObject3D, options?: ZFightingOpti
|
|
|
117
151
|
type AuditSceneOptions = Pick<ZFightingOptions, "gap" | "self" | "tagged"> & {
|
|
118
152
|
skip?: Skip | undefined;
|
|
119
153
|
tagKey?: string;
|
|
154
|
+
/** Only the object(s) tagged this (`userData[tagKey]`): one studio object out of a scene. */
|
|
155
|
+
tag?: string | undefined;
|
|
120
156
|
/** How many of the costliest meshes to list. */
|
|
121
157
|
top?: number;
|
|
122
158
|
};
|
|
123
159
|
type AuditReport = {
|
|
124
160
|
triangles: number;
|
|
161
|
+
/** Draw calls one main pass makes, before culling and shadows: see `countDraws`. */
|
|
162
|
+
draws: number;
|
|
125
163
|
zFighting: ZFightingRow[];
|
|
126
164
|
badGeometry: BadGeometryRow[];
|
|
165
|
+
emptyMeshes: EmptyMeshRow[];
|
|
127
166
|
/** The costliest meshes, most triangles first. */
|
|
128
167
|
meshes: MeshRow[];
|
|
129
168
|
};
|
|
130
169
|
/**
|
|
131
170
|
* Every check at once, JSON-safe: what a test asserts on or a headless browser prints. A clean
|
|
132
|
-
* scene has `zFighting` and `
|
|
171
|
+
* scene has `zFighting`, `badGeometry` and `emptyMeshes` all `[]`.
|
|
133
172
|
*/
|
|
134
173
|
export declare const auditScene: (root: AnyObject3D, options?: AuditSceneOptions) => AuditReport;
|
|
174
|
+
type AuditSummaryOptions = Pick<AuditSceneOptions, "gap" | "skip" | "tag" | "tagKey"> & {
|
|
175
|
+
/** Run the z-fighting check, the slow one. Default true; false gives `null` counts. */
|
|
176
|
+
zFighting?: boolean;
|
|
177
|
+
};
|
|
178
|
+
type AuditSummary = {
|
|
179
|
+
triangles: number;
|
|
180
|
+
draws: number;
|
|
181
|
+
meshes: number;
|
|
182
|
+
/** Pairs of meshes that z-fight, identical pairs counted each. */
|
|
183
|
+
zFighting: number | null;
|
|
184
|
+
/** Meshes that z-fight with themselves: merged geometry whose parts overlap. */
|
|
185
|
+
selfZFighting: number | null;
|
|
186
|
+
badGeometry: number;
|
|
187
|
+
emptyMeshes: number;
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Every check as one count each: the single call to run after every edit. `0` everywhere (and
|
|
191
|
+
* `triangles`, `draws` where you expect them) is a pass. Use `auditScene` to see what failed.
|
|
192
|
+
*/
|
|
193
|
+
export declare const summarizeScene: (root: AnyObject3D, options?: AuditSummaryOptions) => AuditSummary;
|
|
135
194
|
//#endregion
|
|
136
195
|
//#region src/black-frames.d.ts
|
|
137
196
|
/** The `WebGLRenderer` methods the probe uses, typed loosely to fit any copy of three's types. */
|
|
@@ -170,6 +229,10 @@ export declare const watchBlackFrames: (renderer: ProbeRenderer, options?: Black
|
|
|
170
229
|
type LedgerRenderer = {
|
|
171
230
|
render(scene: AnyObject3D, camera: object): void;
|
|
172
231
|
renderBufferDirect(camera: object, scene: object | null, geometry: object, material: object, object: AnyObject3D, group: unknown): void;
|
|
232
|
+
/** For pass sizes: the target being drawn to, `null` for the screen. */
|
|
233
|
+
getRenderTarget?(): unknown;
|
|
234
|
+
/** For pass sizes: the screen's drawing buffer, device pixels. */
|
|
235
|
+
getContext?(): unknown;
|
|
173
236
|
};
|
|
174
237
|
type DrawLedgerOptions = {
|
|
175
238
|
/**
|
|
@@ -193,6 +256,18 @@ type LedgerRow = {
|
|
|
193
256
|
/** Draws per pass name. */
|
|
194
257
|
passes: Record<string, number>;
|
|
195
258
|
};
|
|
259
|
+
type PassRow = {
|
|
260
|
+
name: string;
|
|
261
|
+
draws: number;
|
|
262
|
+
/** Distinct objects drawn: for `shadow`, the casters. */
|
|
263
|
+
objects: number;
|
|
264
|
+
/** Triangles drawn, instances counted. */
|
|
265
|
+
triangles: number;
|
|
266
|
+
/** Sizes drawn to, in device pixels: `2880×1800`, or `screen 2880×1800`. */
|
|
267
|
+
targets: string[];
|
|
268
|
+
/** Every call was one small mesh through an orthographic camera: a post-processing pass. */
|
|
269
|
+
fullscreen: boolean;
|
|
270
|
+
};
|
|
196
271
|
type DrawLedger = {
|
|
197
272
|
/**
|
|
198
273
|
* Pass names in the order they first drew: `shadow`, then each `render()` call as its scene's
|
|
@@ -204,6 +279,11 @@ type DrawLedger = {
|
|
|
204
279
|
/** By object name, most draws first. */
|
|
205
280
|
objects: LedgerRow[];
|
|
206
281
|
draws: number;
|
|
282
|
+
/** Each pass's draws, triangles, casters and target sizes, in `passes` order. */
|
|
283
|
+
passStats: PassRow[];
|
|
284
|
+
/** `render()` calls that drew one fullscreen pass, and the pixels they shaded between them. */
|
|
285
|
+
fullscreenPasses: number;
|
|
286
|
+
fullscreenPixels: number;
|
|
207
287
|
};
|
|
208
288
|
type DrawLedgerRecording = {
|
|
209
289
|
/** Restore the renderer and return what was drawn since `beginDrawLedger`. */
|
|
@@ -234,6 +314,8 @@ type AuditHelpersOptions = {
|
|
|
234
314
|
renderer?: (LedgerRenderer & ProbeRenderer) | undefined;
|
|
235
315
|
skip?: Skip | undefined;
|
|
236
316
|
tagKey?: string;
|
|
317
|
+
/** Names geometries inside a batch in `census()`: see `geometryCensus`'s `label`. */
|
|
318
|
+
batchLabel?: ((batch: BatchedMesh, geometryId: number) => string | undefined) | undefined;
|
|
237
319
|
/**
|
|
238
320
|
* Also put each helper on `globalThis` under its short name (`tris`, `report`…). Default true;
|
|
239
321
|
* false leaves only `threeAudit`, for pages where short names collide with other tooling.
|
|
@@ -250,5 +332,38 @@ type AuditHelpersOptions = {
|
|
|
250
332
|
*/
|
|
251
333
|
export declare const installAuditHelpers: (root: AnyObject3D, options?: AuditHelpersOptions) => () => void;
|
|
252
334
|
//#endregion
|
|
253
|
-
|
|
335
|
+
//#region src/measure.d.ts
|
|
336
|
+
type MeasureOptions = {
|
|
337
|
+
/** `userData` key a string target is looked up by. Default `studioObject`. */
|
|
338
|
+
tagKey?: string;
|
|
339
|
+
};
|
|
340
|
+
/** What to measure: an object, or a tag (`userData[tagKey]`), or failing that an object name. */
|
|
341
|
+
type Target = string | AnyObject3D;
|
|
342
|
+
type Bounds = {
|
|
343
|
+
min: [number, number, number];
|
|
344
|
+
max: [number, number, number];
|
|
345
|
+
size: [number, number, number];
|
|
346
|
+
centre: [number, number, number];
|
|
347
|
+
};
|
|
348
|
+
type Clearance = {
|
|
349
|
+
/**
|
|
350
|
+
* The smallest gap between the two, in world units. Measured between triangles' bounding boxes:
|
|
351
|
+
* exact for faces square to the axes, a slight underestimate for sloped ones. 0 when they touch
|
|
352
|
+
* or intersect.
|
|
353
|
+
*/
|
|
354
|
+
gap: number;
|
|
355
|
+
/** The axis the gap is measured along, when it's straight along one; `null` when diagonal. */
|
|
356
|
+
axis: "x" | "y" | "z" | null;
|
|
357
|
+
/** The closest meshes, one from each side. */
|
|
358
|
+
between: [string, string];
|
|
359
|
+
};
|
|
360
|
+
/** A target's world envelope, from its visible meshes' vertices; `null` when it draws nothing. */
|
|
361
|
+
export declare const measureBounds: (root: AnyObject3D, target: Target, options?: MeasureOptions) => Bounds | null;
|
|
362
|
+
/**
|
|
363
|
+
* The gap between two targets, and which of their meshes come closest. Throws when a target
|
|
364
|
+
* matches nothing; `null` when either draws nothing. Meshes in both targets are left out of `a`.
|
|
365
|
+
*/
|
|
366
|
+
export declare const measureClearance: (root: AnyObject3D, a: Target, b: Target, options?: MeasureOptions) => Clearance | null;
|
|
367
|
+
//#endregion
|
|
368
|
+
export type { AnyObject3D, AuditHelpersOptions, AuditReport, AuditSceneOptions, AuditSummary, AuditSummaryOptions, BadGeometryOptions, BadGeometryRow, BlackFrameOptions, BlackFrameWatch, Bounds, CensusOptions, Clearance, DrawLedger, DrawLedgerOptions, DrawLedgerRecording, EmptyMeshRow, GeometryRow, LedgerRenderer, LedgerRow, MeasureOptions, MeshRow, PassRow, ProbeRenderer, RecordDrawLedgerOptions, Skip, Target, TriangleOptions, ZFightingOptions, ZFightingRow };
|
|
254
369
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { _ as triangleCount, a as countTriangles, c as findBadGeometry, d as batchedTriangles, f as isBatched, g as pathOf, h as meshLabel, i as countDraws, l as findEmptyMeshes, m as isMesh, n as summarizeScene, o as geometryCensus, p as isInstanced, r as findZFighting, s as listMeshes, t as auditScene, u as asObject3D, v as walk } from "./audit-scene-CJG1DgRF.js";
|
|
2
2
|
//#region src/patch.ts
|
|
3
3
|
/** Swap `key` on `target` for `value`; the returned function puts back exactly what was there. */
|
|
4
4
|
const patch = (target, key, value) => {
|
|
@@ -122,6 +122,15 @@ const tally = (rows, name, pass) => {
|
|
|
122
122
|
rows.set(name, row);
|
|
123
123
|
};
|
|
124
124
|
const sorted = (rows) => [...rows.values()].sort((a, b) => b.total - a.total);
|
|
125
|
+
/** Triangles one draw makes: its material group's range, times its instances. */
|
|
126
|
+
const drawTriangles = (geometry, object, group) => {
|
|
127
|
+
const mesh = object;
|
|
128
|
+
if (mesh.isMesh !== true) return 0;
|
|
129
|
+
if (isBatched(mesh)) return batchedTriangles(mesh).triangles;
|
|
130
|
+
const range = group;
|
|
131
|
+
const elements = geometry.index?.count ?? geometry.attributes.position?.count ?? 0;
|
|
132
|
+
return (range && typeof range.count === "number" ? Math.floor(Math.max(0, Math.min(range.count, elements - (range.start ?? 0))) / 3) : triangleCount(geometry)) * (isInstanced(mesh) ? mesh.count : 1);
|
|
133
|
+
};
|
|
125
134
|
/** Start counting every draw. Call `end()` after the frame to restore the renderer and read it. */
|
|
126
135
|
const beginDrawLedger = (renderer, options = {}) => {
|
|
127
136
|
if (typeof renderer.renderBufferDirect !== "function") throw new TypeError("three-audit: the draw ledger needs a WebGLRenderer");
|
|
@@ -131,23 +140,89 @@ const beginDrawLedger = (renderer, options = {}) => {
|
|
|
131
140
|
const passes = [];
|
|
132
141
|
const objects = /* @__PURE__ */ new Map();
|
|
133
142
|
const groups = /* @__PURE__ */ new Map();
|
|
143
|
+
const stats = /* @__PURE__ */ new Map();
|
|
144
|
+
const calls = [];
|
|
134
145
|
let renders = 0;
|
|
135
146
|
let current = "direct";
|
|
136
147
|
let draws = 0;
|
|
148
|
+
let fullscreenPasses = 0;
|
|
149
|
+
let fullscreenPixels = 0;
|
|
150
|
+
const statOf = (pass) => {
|
|
151
|
+
let stat = stats.get(pass);
|
|
152
|
+
if (!stat) {
|
|
153
|
+
stat = {
|
|
154
|
+
name: pass,
|
|
155
|
+
draws: 0,
|
|
156
|
+
triangles: 0,
|
|
157
|
+
objects: /* @__PURE__ */ new Set(),
|
|
158
|
+
targets: /* @__PURE__ */ new Set(),
|
|
159
|
+
calls: 0,
|
|
160
|
+
fullscreenCalls: 0
|
|
161
|
+
};
|
|
162
|
+
stats.set(pass, stat);
|
|
163
|
+
}
|
|
164
|
+
return stat;
|
|
165
|
+
};
|
|
166
|
+
const sizeNow = () => {
|
|
167
|
+
const target = renderer.getRenderTarget?.();
|
|
168
|
+
if (target && typeof target.width === "number" && typeof target.height === "number") return {
|
|
169
|
+
width: target.width,
|
|
170
|
+
height: target.height,
|
|
171
|
+
screen: false
|
|
172
|
+
};
|
|
173
|
+
const gl = renderer.getContext?.();
|
|
174
|
+
return target === null && typeof gl?.drawingBufferWidth === "number" ? {
|
|
175
|
+
width: gl.drawingBufferWidth,
|
|
176
|
+
height: gl.drawingBufferHeight ?? 0,
|
|
177
|
+
screen: true
|
|
178
|
+
} : null;
|
|
179
|
+
};
|
|
137
180
|
const restoreRender = patch(renderer, "render", function(scene, camera) {
|
|
138
181
|
const outer = current;
|
|
139
182
|
renders += 1;
|
|
140
183
|
current = scene.name || `render ${renders}`;
|
|
184
|
+
const call = {
|
|
185
|
+
pass: current,
|
|
186
|
+
draws: 0,
|
|
187
|
+
orthographic: camera.isOrthographicCamera === true,
|
|
188
|
+
vertices: 0,
|
|
189
|
+
size: null
|
|
190
|
+
};
|
|
191
|
+
calls.push(call);
|
|
141
192
|
try {
|
|
142
193
|
render.call(this, scene, camera);
|
|
143
194
|
} finally {
|
|
195
|
+
calls.pop();
|
|
144
196
|
current = outer;
|
|
197
|
+
const stat = stats.get(call.pass);
|
|
198
|
+
if (stat) {
|
|
199
|
+
stat.calls += 1;
|
|
200
|
+
if (call.draws === 1 && call.orthographic && call.vertices <= 6) {
|
|
201
|
+
stat.fullscreenCalls += 1;
|
|
202
|
+
fullscreenPasses += 1;
|
|
203
|
+
fullscreenPixels += call.size ? call.size.width * call.size.height : 0;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
145
206
|
}
|
|
146
207
|
});
|
|
147
208
|
const restoreDraw = patch(renderer, "renderBufferDirect", function(camera, scene, geometry, material, object, group) {
|
|
148
|
-
const
|
|
209
|
+
const shadow = material.isMeshDepthMaterial === true || material.isMeshDistanceMaterial === true;
|
|
210
|
+
const pass = shadow ? "shadow" : current;
|
|
149
211
|
const name = nameOf(object, material);
|
|
150
212
|
if (!passes.includes(pass)) passes.push(pass);
|
|
213
|
+
const stat = statOf(pass);
|
|
214
|
+
const size = sizeNow();
|
|
215
|
+
const drawn = geometry;
|
|
216
|
+
stat.draws += 1;
|
|
217
|
+
stat.triangles += drawTriangles(drawn, object, group);
|
|
218
|
+
stat.objects.add(object);
|
|
219
|
+
if (size) stat.targets.add(`${size.screen ? "screen " : ""}${size.width}×${size.height}`);
|
|
220
|
+
const call = calls.at(-1);
|
|
221
|
+
if (call && !shadow) {
|
|
222
|
+
call.draws += 1;
|
|
223
|
+
call.vertices = drawn.attributes?.position?.count ?? Infinity;
|
|
224
|
+
call.size = size;
|
|
225
|
+
}
|
|
151
226
|
draws += 1;
|
|
152
227
|
tally(objects, name, pass);
|
|
153
228
|
tally(groups, groupOf(name), pass);
|
|
@@ -160,7 +235,20 @@ const beginDrawLedger = (renderer, options = {}) => {
|
|
|
160
235
|
passes,
|
|
161
236
|
groups: sorted(groups),
|
|
162
237
|
objects: sorted(objects),
|
|
163
|
-
draws
|
|
238
|
+
draws,
|
|
239
|
+
passStats: passes.map((pass) => {
|
|
240
|
+
const stat = stats.get(pass);
|
|
241
|
+
return {
|
|
242
|
+
name: pass,
|
|
243
|
+
draws: stat.draws,
|
|
244
|
+
objects: stat.objects.size,
|
|
245
|
+
triangles: stat.triangles,
|
|
246
|
+
targets: [...stat.targets],
|
|
247
|
+
fullscreen: stat.calls > 0 && stat.fullscreenCalls === stat.calls
|
|
248
|
+
};
|
|
249
|
+
}),
|
|
250
|
+
fullscreenPasses,
|
|
251
|
+
fullscreenPixels
|
|
164
252
|
};
|
|
165
253
|
} };
|
|
166
254
|
};
|
|
@@ -205,28 +293,281 @@ const ledgerTable = (rows, passes) => rows.map((row) => ({
|
|
|
205
293
|
const printDrawLedger = (ledger) => {
|
|
206
294
|
console.group(`Draw ledger: ${ledger.draws} draws in one frame`);
|
|
207
295
|
console.table(ledgerTable(ledger.groups, ledger.passes));
|
|
296
|
+
console.table(ledger.passStats.map((pass) => ({
|
|
297
|
+
...pass,
|
|
298
|
+
targets: pass.targets.join(", ")
|
|
299
|
+
})));
|
|
300
|
+
if (ledger.fullscreenPasses > 0) console.log(`${ledger.fullscreenPasses} fullscreen passes: ${(ledger.fullscreenPixels / 1e6).toFixed(1)} Mpx shaded`);
|
|
208
301
|
console.groupCollapsed("Every object");
|
|
209
302
|
console.table(ledgerTable(ledger.objects, ledger.passes));
|
|
210
303
|
console.groupEnd();
|
|
211
304
|
console.groupEnd();
|
|
212
305
|
};
|
|
213
306
|
//#endregion
|
|
214
|
-
//#region src/
|
|
307
|
+
//#region src/measure.ts
|
|
308
|
+
const multiply = (a, b, out) => {
|
|
309
|
+
for (let row = 0; row < 4; row += 1) for (let column = 0; column < 4; column += 1) {
|
|
310
|
+
let sum = 0;
|
|
311
|
+
for (let k = 0; k < 4; k += 1) sum += a[k * 4 + row] * b[column * 4 + k];
|
|
312
|
+
out[column * 4 + row] = sum;
|
|
313
|
+
}
|
|
314
|
+
return out;
|
|
315
|
+
};
|
|
316
|
+
/** Every triangle a mesh draws, in world space: each instance of an instanced or batched mesh. */
|
|
317
|
+
const eachTriangle = (mesh, visit) => {
|
|
318
|
+
const { index, attributes } = mesh.geometry;
|
|
319
|
+
const position = attributes.position;
|
|
320
|
+
if (!position) return;
|
|
321
|
+
const world = mesh.matrixWorld.elements;
|
|
322
|
+
const corner = [
|
|
323
|
+
0,
|
|
324
|
+
0,
|
|
325
|
+
0,
|
|
326
|
+
0,
|
|
327
|
+
0,
|
|
328
|
+
0,
|
|
329
|
+
0,
|
|
330
|
+
0,
|
|
331
|
+
0
|
|
332
|
+
];
|
|
333
|
+
const emit = (matrix, first, count, indexed) => {
|
|
334
|
+
for (let k = first; k + 2 < first + count; k += 3) {
|
|
335
|
+
for (let c = 0; c < 3; c += 1) {
|
|
336
|
+
const v = indexed ? index.getX(k + c) : k + c;
|
|
337
|
+
const x = position.getX(v);
|
|
338
|
+
const y = position.getY(v);
|
|
339
|
+
const z = position.getZ(v);
|
|
340
|
+
for (let axis = 0; axis < 3; axis += 1) corner[c * 3 + axis] = matrix[axis] * x + matrix[axis + 4] * y + matrix[axis + 8] * z + matrix[axis + 12];
|
|
341
|
+
}
|
|
342
|
+
visit(...corner);
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
if (isBatched(mesh)) {
|
|
346
|
+
const batch = mesh;
|
|
347
|
+
const instance = Array.from({ length: 16 }, () => 0);
|
|
348
|
+
const matrix = Array.from({ length: 16 }, () => 0);
|
|
349
|
+
const target = { fromArray(array, offset = 0) {
|
|
350
|
+
for (let i = 0; i < 16; i += 1) instance[i] = array[offset + i];
|
|
351
|
+
return target;
|
|
352
|
+
} };
|
|
353
|
+
for (let id = 0; id < batch.maxInstanceCount; id += 1) {
|
|
354
|
+
let range;
|
|
355
|
+
try {
|
|
356
|
+
if (!batch.getVisibleAt(id)) continue;
|
|
357
|
+
range = batch.getGeometryRangeAt(batch.getGeometryIdAt(id));
|
|
358
|
+
batch.getMatrixAt(id, target);
|
|
359
|
+
} catch {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
multiply(world, instance, matrix);
|
|
363
|
+
if (index) emit(matrix, range.indexStart, range.indexCount, true);
|
|
364
|
+
else emit(matrix, range.vertexStart, range.vertexCount, false);
|
|
365
|
+
}
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const { start, count } = mesh.geometry.drawRange;
|
|
369
|
+
const elements = index?.count ?? position.count;
|
|
370
|
+
const first = Math.min(start, elements);
|
|
371
|
+
const drawn = Math.min(elements, start + count) - first;
|
|
372
|
+
if (isInstanced(mesh)) {
|
|
373
|
+
const matrix = Array.from({ length: 16 }, () => 0);
|
|
374
|
+
const instances = mesh.instanceMatrix.array;
|
|
375
|
+
for (let i = 0; i < mesh.count; i += 1) {
|
|
376
|
+
multiply(world, Array.prototype.slice.call(instances, i * 16, i * 16 + 16), matrix);
|
|
377
|
+
emit(matrix, first, drawn, index !== null);
|
|
378
|
+
}
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
emit(world, first, drawn, index !== null);
|
|
382
|
+
};
|
|
383
|
+
const emptyBox = () => [
|
|
384
|
+
Infinity,
|
|
385
|
+
Infinity,
|
|
386
|
+
Infinity,
|
|
387
|
+
-Infinity,
|
|
388
|
+
-Infinity,
|
|
389
|
+
-Infinity
|
|
390
|
+
];
|
|
391
|
+
const grow = (box, x, y, z) => {
|
|
392
|
+
box[0] = Math.min(box[0], x);
|
|
393
|
+
box[1] = Math.min(box[1], y);
|
|
394
|
+
box[2] = Math.min(box[2], z);
|
|
395
|
+
box[3] = Math.max(box[3], x);
|
|
396
|
+
box[4] = Math.max(box[4], y);
|
|
397
|
+
box[5] = Math.max(box[5], z);
|
|
398
|
+
};
|
|
399
|
+
/** Per axis, how far apart two boxes are (0 where they overlap). */
|
|
400
|
+
const apart = (a, ai, b, bi, out) => {
|
|
401
|
+
for (let axis = 0; axis < 3; axis += 1) out[axis] = Math.max(0, a[ai + axis] - b[bi + axis + 3], b[bi + axis] - a[ai + axis + 3]);
|
|
402
|
+
return Math.hypot(out[0], out[1], out[2]);
|
|
403
|
+
};
|
|
404
|
+
const round = (value) => {
|
|
405
|
+
const rounded = Math.round(value * 1e3) / 1e3;
|
|
406
|
+
return rounded === 0 ? 0 : rounded;
|
|
407
|
+
};
|
|
408
|
+
/** The visible meshes a target stands for. */
|
|
409
|
+
const meshesOf = (root, target, tagKey, role) => {
|
|
410
|
+
let objects;
|
|
411
|
+
if (typeof target === "string") {
|
|
412
|
+
const tagged = [];
|
|
413
|
+
const named = [];
|
|
414
|
+
walk(root, {}, (object) => {
|
|
415
|
+
const tag = object.userData[tagKey];
|
|
416
|
+
if (tag !== void 0 && tag !== null && String(tag) === target) tagged.push(object);
|
|
417
|
+
else if (object.name === target) named.push(object);
|
|
418
|
+
});
|
|
419
|
+
objects = tagged.length > 0 ? tagged : named;
|
|
420
|
+
if (objects.length === 0) throw new Error(`three-audit: ${role}: nothing is tagged userData.${tagKey} = "${target}" or named "${target}"`);
|
|
421
|
+
} else objects = [asObject3D(target)];
|
|
422
|
+
const meshes = /* @__PURE__ */ new Set();
|
|
423
|
+
for (const object of objects) walk(object, { visibleOnly: true }, (child) => {
|
|
424
|
+
if (isMesh(child)) meshes.add(child);
|
|
425
|
+
});
|
|
426
|
+
return meshes;
|
|
427
|
+
};
|
|
428
|
+
/** A target's world envelope, from its visible meshes' vertices; `null` when it draws nothing. */
|
|
429
|
+
const measureBounds = (root, target, options = {}) => {
|
|
430
|
+
const scene = asObject3D(root);
|
|
431
|
+
scene.updateWorldMatrix(true, true);
|
|
432
|
+
const box = emptyBox();
|
|
433
|
+
for (const mesh of meshesOf(scene, target, options.tagKey ?? "studioObject", "bounds")) eachTriangle(mesh, (ax, ay, az, bx, by, bz, cx, cy, cz) => {
|
|
434
|
+
grow(box, ax, ay, az);
|
|
435
|
+
grow(box, bx, by, bz);
|
|
436
|
+
grow(box, cx, cy, cz);
|
|
437
|
+
});
|
|
438
|
+
if (box[0] > box[3]) return null;
|
|
439
|
+
return {
|
|
440
|
+
min: [
|
|
441
|
+
box[0],
|
|
442
|
+
box[1],
|
|
443
|
+
box[2]
|
|
444
|
+
].map(round),
|
|
445
|
+
max: [
|
|
446
|
+
box[3],
|
|
447
|
+
box[4],
|
|
448
|
+
box[5]
|
|
449
|
+
].map(round),
|
|
450
|
+
size: [
|
|
451
|
+
0,
|
|
452
|
+
1,
|
|
453
|
+
2
|
|
454
|
+
].map((axis) => round(box[axis + 3] - box[axis])),
|
|
455
|
+
centre: [
|
|
456
|
+
0,
|
|
457
|
+
1,
|
|
458
|
+
2
|
|
459
|
+
].map((axis) => round((box[axis] + box[axis + 3]) / 2))
|
|
460
|
+
};
|
|
461
|
+
};
|
|
462
|
+
/** Triangle boxes past this many pairs, a mesh pair is measured by its whole boxes instead. */
|
|
463
|
+
const PAIR_LIMIT = 4e6;
|
|
215
464
|
/**
|
|
216
|
-
* The
|
|
217
|
-
*
|
|
218
|
-
* `beginLedger()` and `blackFrames()`. All of them also sit on `threeAudit`, which is the only
|
|
219
|
-
* global with `globals: false`.
|
|
465
|
+
* The gap between two targets, and which of their meshes come closest. Throws when a target
|
|
466
|
+
* matches nothing; `null` when either draws nothing. Meshes in both targets are left out of `a`.
|
|
220
467
|
*/
|
|
468
|
+
const measureClearance = (root, a, b, options = {}) => {
|
|
469
|
+
const scene = asObject3D(root);
|
|
470
|
+
const tagKey = options.tagKey ?? "studioObject";
|
|
471
|
+
scene.updateWorldMatrix(true, true);
|
|
472
|
+
const inB = meshesOf(scene, b, tagKey, "clearance b");
|
|
473
|
+
const inA = [...meshesOf(scene, a, tagKey, "clearance a")].filter((mesh) => !inB.has(mesh));
|
|
474
|
+
const measure = (mesh) => {
|
|
475
|
+
const box = emptyBox();
|
|
476
|
+
eachTriangle(mesh, (ax, ay, az, bx, by, bz, cx, cy, cz) => {
|
|
477
|
+
grow(box, ax, ay, az);
|
|
478
|
+
grow(box, bx, by, bz);
|
|
479
|
+
grow(box, cx, cy, cz);
|
|
480
|
+
});
|
|
481
|
+
return {
|
|
482
|
+
mesh,
|
|
483
|
+
box,
|
|
484
|
+
triangles: null
|
|
485
|
+
};
|
|
486
|
+
};
|
|
487
|
+
const sideA = inA.map(measure).filter(({ box }) => box[0] <= box[3]);
|
|
488
|
+
const sideB = [...inB].map(measure).filter(({ box }) => box[0] <= box[3]);
|
|
489
|
+
if (sideA.length === 0 || sideB.length === 0) return null;
|
|
490
|
+
const trianglesOf = (side) => {
|
|
491
|
+
if (!side.triangles) {
|
|
492
|
+
const boxes = [];
|
|
493
|
+
eachTriangle(side.mesh, (ax, ay, az, bx, by, bz, cx, cy, cz) => {
|
|
494
|
+
boxes.push(Math.min(ax, bx, cx), Math.min(ay, by, cy), Math.min(az, bz, cz), Math.max(ax, bx, cx), Math.max(ay, by, cy), Math.max(az, bz, cz));
|
|
495
|
+
});
|
|
496
|
+
side.triangles = Float64Array.from(boxes);
|
|
497
|
+
}
|
|
498
|
+
return side.triangles;
|
|
499
|
+
};
|
|
500
|
+
const axes = [
|
|
501
|
+
0,
|
|
502
|
+
0,
|
|
503
|
+
0
|
|
504
|
+
];
|
|
505
|
+
const pairs = sideA.flatMap((left) => sideB.map((right) => ({
|
|
506
|
+
left,
|
|
507
|
+
right,
|
|
508
|
+
distance: apart(left.box, 0, right.box, 0, axes)
|
|
509
|
+
}))).sort((x, y) => x.distance - y.distance);
|
|
510
|
+
let best = Infinity;
|
|
511
|
+
let bestAxes = [
|
|
512
|
+
0,
|
|
513
|
+
0,
|
|
514
|
+
0
|
|
515
|
+
];
|
|
516
|
+
let between = [pairs[0].left.mesh, pairs[0].right.mesh];
|
|
517
|
+
for (const { left, right, distance } of pairs) {
|
|
518
|
+
if (distance >= best) break;
|
|
519
|
+
const leftTriangles = trianglesOf(left);
|
|
520
|
+
const rightTriangles = trianglesOf(right);
|
|
521
|
+
const near = (triangles, other) => {
|
|
522
|
+
const kept = [];
|
|
523
|
+
for (let t = 0; t < triangles.length; t += 6) if (apart(triangles, t, other, 0, axes) < best) kept.push(t);
|
|
524
|
+
return kept;
|
|
525
|
+
};
|
|
526
|
+
const nearLeft = near(leftTriangles, right.box);
|
|
527
|
+
const nearRight = near(rightTriangles, left.box);
|
|
528
|
+
if (nearLeft.length * nearRight.length > PAIR_LIMIT) {
|
|
529
|
+
if (distance < best) {
|
|
530
|
+
best = distance;
|
|
531
|
+
apart(left.box, 0, right.box, 0, axes);
|
|
532
|
+
bestAxes = [...axes];
|
|
533
|
+
between = [left.mesh, right.mesh];
|
|
534
|
+
}
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
for (const i of nearLeft) for (const j of nearRight) {
|
|
538
|
+
const gap = apart(leftTriangles, i, rightTriangles, j, axes);
|
|
539
|
+
if (gap < best) {
|
|
540
|
+
best = gap;
|
|
541
|
+
bestAxes = [...axes];
|
|
542
|
+
between = [left.mesh, right.mesh];
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const separated = bestAxes.flatMap((value, axis) => value > 0 ? [axis] : []);
|
|
547
|
+
return {
|
|
548
|
+
gap: round(best),
|
|
549
|
+
axis: separated.length === 1 ? [
|
|
550
|
+
"x",
|
|
551
|
+
"y",
|
|
552
|
+
"z"
|
|
553
|
+
][separated[0]] : null,
|
|
554
|
+
between: [meshLabel(between[0], scene, tagKey), meshLabel(between[1], scene, tagKey)]
|
|
555
|
+
};
|
|
556
|
+
};
|
|
557
|
+
//#endregion
|
|
558
|
+
//#region src/console-helpers.ts
|
|
221
559
|
const NAMESPACE = "threeAudit";
|
|
222
560
|
const NAMES = [
|
|
223
561
|
"auditRoot",
|
|
562
|
+
"summary",
|
|
224
563
|
"tris",
|
|
225
564
|
"meshes",
|
|
226
565
|
"census",
|
|
227
566
|
"audit",
|
|
228
567
|
"zfight",
|
|
229
568
|
"report",
|
|
569
|
+
"bbox",
|
|
570
|
+
"clearance",
|
|
230
571
|
"ledger",
|
|
231
572
|
"beginLedger",
|
|
232
573
|
"blackFrames"
|
|
@@ -243,13 +584,23 @@ const needsRenderer = (name) => () => {
|
|
|
243
584
|
* useLayoutEffect(() => installAuditHelpers(scene, { renderer: gl }), [scene, gl]);
|
|
244
585
|
*/
|
|
245
586
|
const installAuditHelpers = (root, options = {}) => {
|
|
246
|
-
const { renderer, skip } = options;
|
|
587
|
+
const { renderer, skip, batchLabel } = options;
|
|
247
588
|
const tagKey = options.tagKey ?? "studioObject";
|
|
248
589
|
const helpers = {
|
|
249
590
|
auditRoot: root,
|
|
591
|
+
summary: (tag, summaryOptions = {}) => summarizeScene(root, {
|
|
592
|
+
skip,
|
|
593
|
+
tagKey,
|
|
594
|
+
...summaryOptions,
|
|
595
|
+
...tag === void 0 ? {} : { tag }
|
|
596
|
+
}),
|
|
250
597
|
tris: () => countTriangles(root, { skip }),
|
|
251
598
|
meshes: () => listMeshes(root, { skip }),
|
|
252
|
-
census: () => geometryCensus(root, {
|
|
599
|
+
census: (censusOptions = {}) => geometryCensus(root, {
|
|
600
|
+
skip,
|
|
601
|
+
label: batchLabel,
|
|
602
|
+
...censusOptions
|
|
603
|
+
}),
|
|
253
604
|
audit: () => findBadGeometry(root, {
|
|
254
605
|
skip,
|
|
255
606
|
tagKey
|
|
@@ -260,10 +611,13 @@ const installAuditHelpers = (root, options = {}) => {
|
|
|
260
611
|
...gap === void 0 ? {} : { gap },
|
|
261
612
|
...self === void 0 ? {} : { self }
|
|
262
613
|
}),
|
|
263
|
-
report: () => auditScene(root, {
|
|
614
|
+
report: (tag) => auditScene(root, {
|
|
264
615
|
skip,
|
|
265
|
-
tagKey
|
|
616
|
+
tagKey,
|
|
617
|
+
...tag === void 0 ? {} : { tag }
|
|
266
618
|
}),
|
|
619
|
+
bbox: (target) => measureBounds(root, target, { tagKey }),
|
|
620
|
+
clearance: (a, b) => measureClearance(root, a, b, { tagKey }),
|
|
267
621
|
ledger: renderer ? async (ledgerOptions) => {
|
|
268
622
|
const ledger = await recordDrawLedger(renderer, ledgerOptions);
|
|
269
623
|
printDrawLedger(ledger);
|
|
@@ -301,6 +655,6 @@ const installAuditHelpers = (root, options = {}) => {
|
|
|
301
655
|
};
|
|
302
656
|
};
|
|
303
657
|
//#endregion
|
|
304
|
-
export { auditScene, beginDrawLedger, countTriangles, findBadGeometry, findZFighting, geometryCensus, installAuditHelpers, ledgerTable, listMeshes, printDrawLedger, recordDrawLedger, triangleCount, watchBlackFrames };
|
|
658
|
+
export { auditScene, beginDrawLedger, countDraws, countTriangles, findBadGeometry, findEmptyMeshes, findZFighting, geometryCensus, installAuditHelpers, ledgerTable, listMeshes, measureBounds, measureClearance, printDrawLedger, recordDrawLedger, summarizeScene, triangleCount, watchBlackFrames };
|
|
305
659
|
|
|
306
660
|
//# sourceMappingURL=index.js.map
|