@zkmake/three-audit 0.6.0 → 0.6.1
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 +78 -49
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,7 +52,10 @@ or a `setupFiles` module:
|
|
|
52
52
|
```ts
|
|
53
53
|
import "@zkmake/three-audit/vitest";
|
|
54
54
|
|
|
55
|
+
import { expect, test } from "vitest";
|
|
56
|
+
|
|
55
57
|
import budgets from "../src/budgets.ts";
|
|
58
|
+
import { buildKart } from "../src/kart.ts";
|
|
56
59
|
|
|
57
60
|
test("the kart", () => {
|
|
58
61
|
const kart = buildKart();
|
|
@@ -81,10 +84,10 @@ A failure says what to fix, as the CLI does:
|
|
|
81
84
|
```
|
|
82
85
|
|
|
83
86
|
`toFitBudget` takes `{ skip, top }` (meshes listed, default 5). A frame budget's `calls` counts as
|
|
84
|
-
`draws
|
|
85
|
-
(`expect.toFitBudget(…)`), and their types come with
|
|
86
|
-
them again, for a setup that resets `expect`; `matchers`
|
|
87
|
-
entry re-exports `triangles`. Vitest 5 or later.
|
|
87
|
+
`draws`; a budget that sets none of triangles, draws (calls) or meshes throws. The matchers work
|
|
88
|
+
negated (`.not`) and as asymmetric matchers (`expect.toFitBudget(…)`), and their types come with
|
|
89
|
+
the import. `installMatchers()` registers them again, for a setup that resets `expect`; `matchers`
|
|
90
|
+
is the object for `expect.extend`. The entry re-exports `triangles`. Vitest 5 or later.
|
|
88
91
|
|
|
89
92
|
## Budgets
|
|
90
93
|
|
|
@@ -115,12 +118,13 @@ export default defineBudgets({
|
|
|
115
118
|
| `parts.*.draws` | Draw calls one main pass makes for it (`countDraws`) |
|
|
116
119
|
| `parts.*.meshes` | Visible meshes in it |
|
|
117
120
|
|
|
118
|
-
`@zkmake/three-audit/budgets` holds `defineBudgets` and `parseBudgets`
|
|
119
|
-
the app's own code can import the file (its HUD reads it) and ship a few hundred
|
|
120
|
-
checks; the main entry exports both too. `
|
|
121
|
+
`@zkmake/three-audit/budgets` holds `defineBudgets` and `parseBudgets` (and their types) alone,
|
|
122
|
+
with no imports, so the app's own code can import the file (its HUD reads it) and ship a few hundred
|
|
123
|
+
bytes, not the checks; the main entry exports both too. `frame` is required, `parts` optional.
|
|
124
|
+
`defineBudgets` returns them frozen, with each part in the type: `budgets.parts.truck` is an error
|
|
121
125
|
when no truck is listed. `parseBudgets(value)` checks budgets from anywhere else (JSON) and throws
|
|
122
|
-
on a bad key. `checkBudget(object, budget)
|
|
123
|
-
`{ metric, actual, limit, over }`.
|
|
126
|
+
on a bad key. `checkBudget(object, budget, { skip }?)`, in the main entry, measures an object
|
|
127
|
+
against one, a row per limit: `{ metric, actual, limit, over }`.
|
|
124
128
|
|
|
125
129
|
- In a test: `expect(kart).toFitBudget(budgets.parts.kart)`.
|
|
126
130
|
- In the HUD: `mountPerfHud(monitor, { budgets })` (or `<PerfHud budgets={budgets} />`) applies the
|
|
@@ -147,9 +151,11 @@ windmill.glb 4,812 triangles, 14 draws
|
|
|
147
151
|
…
|
|
148
152
|
```
|
|
149
153
|
|
|
150
|
-
It exits 1 when any model fails a check (2 when one can't be read), so it can gate CI.
|
|
151
|
-
`--gap <m>`, `--self`, `--skip <regex>` (leave out
|
|
152
|
-
`--json
|
|
154
|
+
It exits 1 when any model fails a check (2 when one can't be read), so it can gate CI. Empty meshes
|
|
155
|
+
are listed (`!`) but don't fail. Options: `--gap <m>`, `--self`, `--skip <regex>` (leave out
|
|
156
|
+
objects by name, e.g. decals), `--top <n>` (default 5, 0 for none), `--json` (an array of
|
|
157
|
+
`{ file, report, budget }`, `report` being `auditScene`'s), `--no-fail`,
|
|
158
|
+
`-h` / `--help`, `-v` / `--version`.
|
|
153
159
|
|
|
154
160
|
`--budgets <file>` also holds each model to the app's [budgets](#budgets): a JSON file, or a module
|
|
155
161
|
whose default export is them (`.js`, or `.ts` where Node strips types). A model answers to the part
|
|
@@ -174,12 +180,18 @@ In a test, load a model the same way:
|
|
|
174
180
|
```ts
|
|
175
181
|
import { findZFighting } from "@zkmake/three-audit";
|
|
176
182
|
import { loadModel } from "@zkmake/three-audit/node";
|
|
183
|
+
import { expect, test } from "vitest";
|
|
177
184
|
|
|
178
185
|
test("windmill.glb has no z-fighting", async () => {
|
|
179
186
|
expect(findZFighting(await loadModel("assets/windmill.glb"))).toEqual([]);
|
|
180
187
|
});
|
|
181
188
|
```
|
|
182
189
|
|
|
190
|
+
`parseModel(bytes, directory?)` does the same from bytes already read (GLB or glTF JSON, its
|
|
191
|
+
buffers resolved from `directory`). `createNodeDracoLoader()` is the in-process Draco decoder it
|
|
192
|
+
uses, for your own `GLTFLoader` in Node: it has the two methods `GLTFLoader` calls on a
|
|
193
|
+
`DRACOLoader`, so pass it with a cast (`loader.setDRACOLoader(draco as never)`).
|
|
194
|
+
|
|
183
195
|
glTF is in metres, which is what `--gap`'s default assumes. Instanced meshes
|
|
184
196
|
(`EXT_mesh_gpu_instancing`) aren't checked for z-fighting; skinned and morphing meshes are
|
|
185
197
|
checked in their rest pose.
|
|
@@ -200,23 +212,24 @@ const { scene, gl } = useThree();
|
|
|
200
212
|
useLayoutEffect(() => installAuditHelpers(scene, { renderer: gl }), [scene, gl]);
|
|
201
213
|
```
|
|
202
214
|
|
|
203
|
-
| Global
|
|
204
|
-
|
|
|
205
|
-
| `summary(tag?)`
|
|
206
|
-
| `tris()`
|
|
207
|
-
| `meshes()`
|
|
208
|
-
| `census({ budget? })` | Geometries by triangles drawn, batches broken down, hidden meshes included
|
|
209
|
-
| `audit()`
|
|
210
|
-
| `zfight(gap?, self?)`
|
|
211
|
-
| `report(tag?)`
|
|
212
|
-
| `bbox(target)`
|
|
213
|
-
| `clearance(a, b)`
|
|
214
|
-
| `ledger(options?)`
|
|
215
|
-
| `beginLedger()`
|
|
216
|
-
| `blackFrames(10)`
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
215
|
+
| Global | Returns |
|
|
216
|
+
| ----------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
217
|
+
| `summary(tag?, options?)` | One count per check: the call to run after every edit. `{ zFighting: false }` skips the slow one |
|
|
218
|
+
| `tris()` | Triangles one pass draws: visible meshes, instances and batches counted, no culling |
|
|
219
|
+
| `meshes()` | Every visible mesh: triangles, instances, total, most first |
|
|
220
|
+
| `census({ budget?, label? })` | Geometries by triangles drawn, batches broken down, hidden meshes included |
|
|
221
|
+
| `audit()` | Meshes with NaN positions or normals, or zero-length normals |
|
|
222
|
+
| `zfight(gap?, self?)` | Pairs of meshes that z-fight |
|
|
223
|
+
| `report(tag?)` | Every check in one JSON-safe object |
|
|
224
|
+
| `bbox(target)` | An object's world envelope: min, max, size, centre |
|
|
225
|
+
| `clearance(a, b)` | The gap between two objects, and which of their meshes come closest |
|
|
226
|
+
| `ledger(options?)` | One frame's draw calls by object and pass, and each pass's size (WebGL) |
|
|
227
|
+
| `beginLedger()` | `{ end() }`: draw calls between the two, for any span you choose (WebGL) |
|
|
228
|
+
| `blackFrames(10)` | `{ frames, blackFrames }`: frames in the next 10 s that came out black (WebGL) |
|
|
229
|
+
| `auditRoot` | The root the helpers were installed on |
|
|
230
|
+
|
|
231
|
+
A `tag` is a `userData.studioObject` value (or your `tagKey`); a tag nothing carries throws. A
|
|
232
|
+
`target` (`bbox`, `clearance`) is a tag too, falling back to an object's name, or the object itself.
|
|
220
233
|
|
|
221
234
|
```js
|
|
222
235
|
threeAudit.summary("bridge");
|
|
@@ -227,6 +240,11 @@ threeAudit.clearance("train", "tower");
|
|
|
227
240
|
|
|
228
241
|
Each one is also on `threeAudit` (`threeAudit.report()`). Where short names like `report` collide
|
|
229
242
|
with other tooling, `installAuditHelpers(scene, { globals: false })` installs only `threeAudit`.
|
|
243
|
+
The other options: `renderer` (the WebGL renderer `ledger`, `beginLedger` and `blackFrames` need;
|
|
244
|
+
without it they throw), `skip`, `tagKey`, [`bakes`](#baked-groups), and `batchLabel`, which names
|
|
245
|
+
the geometries inside a batch in `census()` (`geometryCensus`'s `label`).
|
|
246
|
+
[three-devtools](../three-devtools) installs them behind `?debug` (its `audit` option), and
|
|
247
|
+
[three-studio](../three-studio)'s pages install them on their own scene.
|
|
230
248
|
|
|
231
249
|
From a headless browser: `agent-browser eval 'JSON.stringify(threeAudit.report())'`.
|
|
232
250
|
|
|
@@ -270,7 +288,9 @@ between each other instead of over each other's ends.
|
|
|
270
288
|
### `findBadGeometry(root, options?)`
|
|
271
289
|
|
|
272
290
|
Vertices with a non-finite position or normal, and zero-length normals on triangles with area.
|
|
273
|
-
Either lights a pixel NaN, and bloom smears one NaN pixel into a black block.
|
|
291
|
+
Either lights a pixel NaN, and bloom smears one NaN pixel into a black block. Hidden meshes are
|
|
292
|
+
checked too. A row is `{ mesh, under, nonFinite, zeroNormals }`, `under` being the tagged part it
|
|
293
|
+
sits in (or its parent's name). Options: `skip`, `tagKey`, `bakes`.
|
|
274
294
|
|
|
275
295
|
### `summarizeScene(root, options?)`
|
|
276
296
|
|
|
@@ -280,10 +300,12 @@ emptyMeshes }`. `tag` narrows it to one tagged object. `zFighting: false` skips
|
|
|
280
300
|
|
|
281
301
|
### `countTriangles`, `listMeshes`, `geometryCensus`, `countDraws`, `triangles`
|
|
282
302
|
|
|
283
|
-
Where a triangle budget goes. `
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
303
|
+
Where a triangle budget goes. `countTriangles` counts what one pass draws, with no culling; a shadow
|
|
304
|
+
pass adds roughly as much again per shadow-casting light. `triangles(target)` is the one a test
|
|
305
|
+
reaches for: a geometry's own triangles (`triangleCount`, within its draw range), or what an object
|
|
306
|
+
draws (`countTriangles`). Name meshes and geometries (`mesh.name`, `geometry.name`) so the rows say
|
|
307
|
+
something. A mesh row's `name` is its named ancestors, then its own name, or its geometry's name or
|
|
308
|
+
type when it has none (`CharacterHands/left/BoxGeometry`).
|
|
287
309
|
|
|
288
310
|
`geometryCensus` lists a `BatchedMesh` one geometry at a time: visible instances × triangles each,
|
|
289
311
|
which is where a batch's budget actually goes. Rows are `batch#id` unless `label(batch, geometryId)`
|
|
@@ -296,29 +318,32 @@ treadmill_belt 570 513 292,410 0.306 true
|
|
|
296
318
|
treadmill_coupler 570 297 169,290 0.177 false
|
|
297
319
|
```
|
|
298
320
|
|
|
299
|
-
`countDraws` counts the draw calls one main pass makes: one per visible mesh, line
|
|
300
|
-
one per material group when a mesh has several. It doesn't cull and doesn't count shadows;
|
|
301
|
-
ledger counts the real thing.
|
|
321
|
+
`countDraws` counts the draw calls one main pass makes: one per visible mesh, line, point cloud or
|
|
322
|
+
sprite, one per material group when a mesh has several. It doesn't cull and doesn't count shadows;
|
|
323
|
+
the draw ledger counts the real thing.
|
|
302
324
|
|
|
303
|
-
### `findEmptyMeshes(root)`
|
|
325
|
+
### `findEmptyMeshes(root, options?)`
|
|
304
326
|
|
|
305
327
|
Meshes with no vertices, such as what merging nothing leaves. Each one still costs a draw call and
|
|
306
328
|
a geometry to dispose. A zero draw range doesn't count, since that's usually a pool waiting to fill.
|
|
329
|
+
Hidden meshes are included; it takes `findBadGeometry`'s options.
|
|
307
330
|
|
|
308
|
-
### `measureBounds(root, target)` / `measureClearance(root, a, b)`
|
|
331
|
+
### `measureBounds(root, target, options?)` / `measureClearance(root, a, b, options?)`
|
|
309
332
|
|
|
310
333
|
An object's world envelope, and the smallest gap between two objects, from their vertices as drawn:
|
|
311
334
|
instances and batches included, rotations exact. The gap is measured between triangles' bounding
|
|
312
335
|
boxes, which is exact for faces square to the axes and slightly under for sloped ones. So a tower
|
|
313
336
|
merged into one mesh still measures to its crossbar, not to the box around the whole tower. `axis`
|
|
314
|
-
says which way the gap runs when it's straight along one axis. Draw ranges
|
|
337
|
+
says which way the gap runs when it's straight along one axis (`null` when diagonal). Draw ranges
|
|
338
|
+
are respected. A target matching nothing throws; one that draws nothing gives `null`. Options:
|
|
339
|
+
`tagKey`, `bakes`.
|
|
315
340
|
|
|
316
341
|
### `recordDrawLedger(renderer)` / `beginDrawLedger(renderer)`
|
|
317
342
|
|
|
318
343
|
Wraps `WebGLRenderer.renderBufferDirect` and counts every draw by object name, by group (the name up
|
|
319
|
-
to its first `:` or
|
|
320
|
-
`render 1`, `render 2`… An unnamed object goes by its named ancestors, type and
|
|
321
|
-
(`CharacterHands/Mesh [skin]`).
|
|
344
|
+
to its first `:` or `#`, or your `groupOf(name)`), and by pass: `shadow`, then each `render()` call
|
|
345
|
+
by scene name, or `render 1`, `render 2`… An unnamed object goes by its named ancestors, type and
|
|
346
|
+
material (`CharacterHands/Mesh [skin]`).
|
|
322
347
|
|
|
323
348
|
Each row (object or group) carries its draws per pass (`passes`) and its triangles, in all
|
|
324
349
|
(`triangles`) and per pass (`passTriangles`): what a frame really draws of each part, so the city
|
|
@@ -327,7 +352,8 @@ pass, after culling them one by one.
|
|
|
327
352
|
|
|
328
353
|
`recordDrawLedger` records exactly one animation frame, and fails after `timeout` (2 s) when no
|
|
329
354
|
frame comes. `{ render }` records one call of your own render instead, frames or not.
|
|
330
|
-
`beginDrawLedger` returns `{ end() }` for any span. `printDrawLedger` prints the tables
|
|
355
|
+
`beginDrawLedger` returns `{ end() }` for any span. `printDrawLedger` prints the tables;
|
|
356
|
+
`ledgerTable(rows, passes)` is one of them as rows for `console.table`.
|
|
331
357
|
|
|
332
358
|
`passStats` sizes each pass: draws, distinct objects (for `shadow`, the casters), triangles, and
|
|
333
359
|
the targets drawn to in device pixels (`screen 2880×1800` for the canvas). A pass is `fullscreen`
|
|
@@ -338,14 +364,17 @@ pass. `fullscreenPasses` and `fullscreenPixels` total them: 8 fullscreen passes
|
|
|
338
364
|
### `watchBlackFrames(renderer)`
|
|
339
365
|
|
|
340
366
|
After each render to the screen, reads a 3 × 3 grid of pixels and counts frames where three or more
|
|
341
|
-
are pure black
|
|
342
|
-
stalls the GPU, so watch, read `
|
|
367
|
+
are pure black (options `grid` and `threshold` set the two numbers). Use a visible browser:
|
|
368
|
+
headless ones haven't reproduced black frames. Each read stalls the GPU, so watch, read `frames`
|
|
369
|
+
and `blackFrames` (which frames, 1-based), `stop()`; `reset()` starts the count over.
|
|
343
370
|
|
|
344
371
|
### `auditScene(root, options?)`
|
|
345
372
|
|
|
346
|
-
`{ triangles, zFighting, badGeometry, meshes }` in one call
|
|
347
|
-
|
|
348
|
-
|
|
373
|
+
`{ triangles, draws, zFighting, badGeometry, emptyMeshes, meshes }` in one call, `meshes` the
|
|
374
|
+
costliest `top` (default 10). `tag` narrows it to one tagged object. Rows carry the live mesh as a
|
|
375
|
+
non-enumerable property (`row.mesh` on a mesh row, `row.meshes` and `row.pairs` on a z-fighting
|
|
376
|
+
row, `row.object` on a bad or empty one), so they print and serialise without dragging the scene
|
|
377
|
+
along.
|
|
349
378
|
|
|
350
379
|
## Baked groups
|
|
351
380
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zkmake/three-audit",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Scene checks for three.js: triangle counts, cost census, NaN normals, z-fighting, black frames, a one-frame draw-call ledger, and per-app budgets with Vitest matchers. Zero dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"audit",
|