@zkmake/three-audit 0.2.1 → 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 CHANGED
@@ -27,10 +27,20 @@ test("windmill has no z-fighting and no NaN geometry", () => {
27
27
  });
28
28
  ```
29
29
 
30
- A failure lists each pair of meshes with faces in one plane, facing the same way, overlapping:
30
+ A failure lists each pair of meshes with faces in one plane, facing the same way, overlapping: the
31
+ meshes by their named ancestors, the plane they share, and where they overlap.
31
32
 
32
- ```
33
- [{ a: "windmill/plinth [stone] @ (0, 0.1, 0)", b: "windmill/wall [plaster] @ (0, 0.5, 0)", triangles: 4 }]
33
+ ```ts
34
+ [
35
+ {
36
+ a: "windmill/plinth [stone] @ (0, 0.1, 0)",
37
+ b: "windmill/wall [plaster] @ (0, 0.5, 0)",
38
+ triangles: 4,
39
+ planes: ["y = 0.000, facing -y"],
40
+ overlap: { area: 1, centre: [0, 0, 0] },
41
+ count: 1,
42
+ },
43
+ ];
34
44
  ```
35
45
 
36
46
  ## From the command line
@@ -42,10 +52,11 @@ npx @zkmake/three-audit assets/*.glb # or bunx
42
52
  ```
43
53
 
44
54
  ```
45
- windmill.glb 4,812 triangles
55
+ windmill.glb 4,812 triangles, 14 draws
46
56
  ✗ 1 z-fighting pair
47
57
  windmill/plinth [stone] @ (0, 0.1, 0)
48
- ↔ windmill/wall [plaster] @ (0, 0.5, 0) (4 triangles)
58
+ ↔ windmill/wall [plaster] @ (0, 0.5, 0)
59
+ 4 triangles in y = 0.000, facing -y; overlap 1 m² at (0, 0, 0)
49
60
  ✓ no NaN geometry
50
61
  most triangles:
51
62
  1,920 sails
@@ -93,16 +104,38 @@ useLayoutEffect(() => installAuditHelpers(scene, { renderer: gl }), [scene, gl])
93
104
 
94
105
  | Global | Returns |
95
106
  | --------------------- | ----------------------------------------------------------------------------------- |
107
+ | `summary(tag?)` | One count per check: the call to run after every edit |
96
108
  | `tris()` | Triangles one pass draws: visible meshes, instances and batches counted, no culling |
97
109
  | `meshes()` | Every visible mesh: triangles, instances, total, most first |
98
- | `census()` | Geometries by triangles drawn, hidden meshes included |
110
+ | `census({ budget? })` | Geometries by triangles drawn, batches broken down, hidden meshes included |
99
111
  | `audit()` | Meshes with NaN positions or normals, or zero-length normals |
100
112
  | `zfight(gap?, self?)` | Pairs of meshes that z-fight |
101
- | `report()` | All of the above in one JSON-safe object |
102
- | `ledger()` | One frame's draw calls by object and pass, printed as tables (WebGL) |
113
+ | `report(tag?)` | Every check in one JSON-safe object |
114
+ | `bbox(target)` | An object's world envelope: min, max, size, centre |
115
+ | `clearance(a, b)` | The gap between two objects, and which of their meshes come closest |
116
+ | `ledger(options?)` | One frame's draw calls by object and pass, and each pass's size (WebGL) |
117
+ | `beginLedger()` | `{ end() }`: draw calls between the two, for any span you choose (WebGL) |
103
118
  | `blackFrames(10)` | Frames in the next 10 s that came out black (WebGL) |
104
119
 
105
- From a headless browser: `agent-browser eval 'JSON.stringify(report())'`.
120
+ A `tag` or `target` is a `userData.studioObject` value (or your `tagKey`), falling back to an
121
+ object's name; `bbox` and `clearance` also take the object itself.
122
+
123
+ ```js
124
+ threeAudit.summary("bridge");
125
+ // { triangles: 4812, draws: 14, meshes: 9, zFighting: 0, selfZFighting: 0, badGeometry: 0, emptyMeshes: 0 }
126
+ threeAudit.clearance("train", "tower");
127
+ // { gap: 0.15, axis: "y", between: ["train/car [paint]", "tower/crossbar [steel]"] }
128
+ ```
129
+
130
+ Each one is also on `threeAudit` (`threeAudit.report()`). Where short names like `report` collide
131
+ with other tooling, `installAuditHelpers(scene, { globals: false })` installs only `threeAudit`.
132
+
133
+ From a headless browser: `agent-browser eval 'JSON.stringify(threeAudit.report())'`.
134
+
135
+ A tab an agent drives is often in the background, where the browser pauses animation frames.
136
+ `ledger()` and `blackFrames()` fail after 2 s with an error that says so, instead of waiting for
137
+ ever. To record a background tab's draws, have the ledger record one call of your app's own
138
+ render: `ledger({ render: () => app.renderOnce() })`. Black frames need a visible tab.
106
139
 
107
140
  ## The checks
108
141
 
@@ -119,7 +152,19 @@ World space, so parent transforms count.
119
152
  - `skip(object)`: leave an object and its subtree out, e.g. a merged copy whose sources are still in
120
153
  the scene: `skip: (o) => o.userData.bakedResult === true`.
121
154
 
122
- Instanced and batched meshes are skipped: check their source parts as plain meshes.
155
+ Each row names both meshes by their named ancestors (`bridge/deck/slab [concrete]`), the planes they
156
+ share (`z = -3.000, facing -z`; opposite-facing faces never pair), and the overlap's area in world
157
+ units² and its centre. Identical pairs, such as pooled copies of one prop parked at one spot, fold
158
+ into one row with a `count`.
159
+
160
+ Left out because they can't fight:
161
+
162
+ - hidden objects
163
+ - instanced and batched meshes, and `InstancedBufferGeometry`, which a shader positions. Check
164
+ their source parts as plain meshes.
165
+ - pairs where neither material writes depth, or one ignores depth entirely. A decal that doesn't
166
+ write depth, over a wall that does, still fights and is still listed.
167
+ - pairs where either material has a polygon offset, the usual decal fix
123
168
 
124
169
  Typical fixes: stand the smaller part 5 mm off, shorten it inside the larger one, or fit panels
125
170
  between each other instead of over each other's ends.
@@ -129,17 +174,62 @@ between each other instead of over each other's ends.
129
174
  Vertices with a non-finite position or normal, and zero-length normals on triangles with area.
130
175
  Either lights a pixel NaN, and bloom smears one NaN pixel into a black block.
131
176
 
132
- ### `countTriangles`, `listMeshes`, `geometryCensus`
177
+ ### `summarizeScene(root, options?)`
178
+
179
+ Every check as one count: `{ triangles, draws, meshes, zFighting, selfZFighting, badGeometry,
180
+ emptyMeshes }`. `tag` narrows it to one tagged object. `zFighting: false` skips the slowest check
181
+ (its counts come back `null`) for a quick look at a large scene.
182
+
183
+ ### `countTriangles`, `listMeshes`, `geometryCensus`, `countDraws`
133
184
 
134
185
  Where a triangle budget goes. Name meshes and geometries (`mesh.name`, `geometry.name`) so the rows
135
- say something. Draw ranges are respected; a shadow pass adds roughly as much again per light.
186
+ say something. A mesh row's `name` is its named ancestors, then its own name, or its geometry's name
187
+ or type when it has none (`CharacterHands/left/BoxGeometry`).
188
+
189
+ `geometryCensus` lists a `BatchedMesh` one geometry at a time: visible instances × triangles each,
190
+ which is where a batch's budget actually goes. Rows are `batch#id` unless `label(batch, geometryId)`
191
+ names them from the app's own records. Each row has its `share` of the total; with `budget: 0.2`,
192
+ rows over 20% get `overBudget: true`.
193
+
194
+ ```
195
+ geometry uses triangles total share overBudget
196
+ treadmill_belt 570 513 292,410 0.306 true
197
+ treadmill_coupler 570 297 169,290 0.177 false
198
+ ```
199
+
200
+ `countDraws` counts the draw calls one main pass makes: one per visible mesh, line or point cloud,
201
+ one per material group when a mesh has several. It doesn't cull and doesn't count shadows; the draw
202
+ ledger counts the real thing.
203
+
204
+ ### `findEmptyMeshes(root)`
205
+
206
+ Meshes with no vertices, such as what merging nothing leaves. Each one still costs a draw call and
207
+ a geometry to dispose. A zero draw range doesn't count, since that's usually a pool waiting to fill.
208
+
209
+ ### `measureBounds(root, target)` / `measureClearance(root, a, b)`
210
+
211
+ An object's world envelope, and the smallest gap between two objects, from their vertices as drawn:
212
+ instances and batches included, rotations exact. The gap is measured between triangles' bounding
213
+ boxes, which is exact for faces square to the axes and slightly under for sloped ones. So a tower
214
+ merged into one mesh still measures to its crossbar, not to the box around the whole tower. `axis`
215
+ says which way the gap runs when it's straight along one axis. Draw ranges are respected; a shadow pass adds roughly as much again per light.
136
216
 
137
217
  ### `recordDrawLedger(renderer)` / `beginDrawLedger(renderer)`
138
218
 
139
219
  Wraps `WebGLRenderer.renderBufferDirect` and counts every draw by object name, by group (the name up
140
220
  to its first `:` or `#`), and by pass: `shadow`, then each `render()` call by scene name, or
141
- `render 1`, `render 2`… `recordDrawLedger` records exactly one animation frame; `beginDrawLedger`
142
- returns `{ end() }` for any span. `printDrawLedger` prints the tables.
221
+ `render 1`, `render 2`… An unnamed object goes by its named ancestors, type and material
222
+ (`CharacterHands/Mesh [skin]`).
223
+
224
+ `recordDrawLedger` records exactly one animation frame, and fails after `timeout` (2 s) when no
225
+ frame comes. `{ render }` records one call of your own render instead, frames or not.
226
+ `beginDrawLedger` returns `{ end() }` for any span. `printDrawLedger` prints the tables.
227
+
228
+ `passStats` sizes each pass: draws, distinct objects (for `shadow`, the casters), triangles, and
229
+ the targets drawn to in device pixels (`screen 2880×1800` for the canvas). A pass is `fullscreen`
230
+ when each call drew one small mesh through an orthographic camera, the shape of a post-processing
231
+ pass. `fullscreenPasses` and `fullscreenPixels` total them: 8 fullscreen passes at 2880×1800 shade
232
+ 41 Mpx a frame before the scene itself draws a pixel.
143
233
 
144
234
  ### `watchBlackFrames(renderer)`
145
235
 
@@ -153,6 +243,12 @@ stalls the GPU, so watch, read `blackFrames`, `stop()`.
153
243
  non-enumerable property (`row.mesh`, `row.meshes`), so they print and serialise without dragging the
154
244
  scene along.
155
245
 
246
+ ## Types
247
+
248
+ The checks take any object with three's `isObject3D` flag, and the renderer as only the methods
249
+ they call. An app on a newer `@types/three` than the one this package was built against passes its
250
+ scene and renderer without a cast.
251
+
156
252
  ## License
157
253
 
158
254
  MIT