mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 22:55:29 +00:00
- a_faces_seam_stays_within_half_its_width_of_the_face checks Rotate (Classic)'s branches too: No Shading keeps its paint, a gradient keeps the gradient, and Diffuse shades its plane darker at some view. - classic_revolves_seams_are_round checks each seam's reach as well as its join, and covers a gradient. - The Wireframe edge takes seam_stroke (the same stroke it built by hand). - Docs: which seams went from miter to round and which already were; the white-light difference from the 2026-10-17 comparison is expected, not measured; the 3D area rows record the change; the performance note says its measurement predates the Classic Rotate and Revolve paths.
110 lines
10 KiB
Markdown
110 lines
10 KiB
Markdown
# 3D lighting performance
|
||
|
||
Measured 2026-10-17 for PR #2706 on one macOS machine (an Apple M1 Pro, below); the round seam joins on 2026-11-12 on another (an Apple M4 Pro, in their own section).
|
||
|
||
## Method
|
||
|
||
- Apple M1 Pro, 10 cores, 16 GiB RAM, macOS 26.5.1 (arm64).
|
||
- `cargo build --release -p vectorcraft-cli`, the workspace release profile (thin LTO, one codegen unit), without bundled fonts.
|
||
- Before: published PR head `f63fafdb`. After: the reviewed implementation rebased on main `f7de3d6a`, with legacy white-light Inflate restored and colored solid-fill channel groups batched per surface. The main update is included in this comparison.
|
||
- Both executables were retained separately. No compilation ran during measurement. Three paired repetitions alternate before/after for each fixture. Each `bench` invocation warms its renderer once, then averages five frames for each thread setting. The table reports the median of the three reported averages.
|
||
- `bench` renders a 400 x 400 pt artboard at 1200 x 1200 pixels, fitting it to 95% of the viewport. The scene contains one white-filled ellipse, centered at (200, 200), with radii 110 and 140 pt, no stroke, Inflate volume 50%, depth 0, solid cap, one side, view rotation (-20, 25, 0), no perspective, and 15% neutral ambient.
|
||
- Two lights: orange `#ff8040`, intensity 70%, rotation 135 degrees, height 35 degrees, softness 15%; blue `#4080ff`, intensity 65%, rotation -35 degrees, height 25 degrees, softness 10%. Both are in front.
|
||
- The one-light case uses the legacy fields: `#ff8040`, intensity 80%, rotation 125.26438968275465 degrees, height 30 degrees, softness 0%. The white case changes its color to `#ffffff`.
|
||
- The source-gradient case uses the same two lights with a linear source gradient from (90, 200) to (310, 200), orange at opacity 0.45 and blue at opacity 0.7.
|
||
- `perf` covers the standard 50,000-path synthetic budget scene. It was also run three times per executable, alternating before/after. Its table reports medians.
|
||
|
||
## Reproduce
|
||
|
||
Create the two-light scene using the updated CLI, then use the same saved file with both executables:
|
||
|
||
```sh
|
||
vectorcraft-cli run \
|
||
--cmd file.new --params '{"width":400,"height":400}' \
|
||
--cmd document.setup --params '{"backgroundContents":"white"}' \
|
||
--cmd path.create --params '{"d":"M310.0 200.0C310.0 277.3198649763111 260.7513224813873 340.0 200.0 340.0C139.2486775186127 340.0 90.0 277.3198649763111 90.0 200.0C90.0 122.6801350236889 139.2486775186127 60.0 200.0 60.0C260.7513224813873 60.0 310.0 122.6801350236889 310.0 200.0Z"}' \
|
||
--cmd paint.setFill --params '{"color":"#ffffff"}' \
|
||
--cmd paint.setStroke --params '{"none":true}' \
|
||
--cmd effect.apply --params '{"effect":"threeD.inflate","item":null,"params":{"rotationX":-20,"rotationY":25,"rotationZ":0,"ambient":15,"volume":50,"depth":0,"cap":true,"bothSides":false,"lights":[{"color":"#ff8040","intensity":70,"rotation":135,"height":35,"softness":15},{"color":"#4080ff","intensity":65,"rotation":-35,"height":25,"softness":10}]}}' \
|
||
--cmd document.save --params '{"path":"/tmp/inflate-two-lights.vectorcraft"}'
|
||
|
||
vectorcraft-cli bench /tmp/inflate-two-lights.vectorcraft --size 1200x1200 --iters 5
|
||
vectorcraft-cli perf
|
||
```
|
||
|
||
## Measurements
|
||
|
||
Only the legacy white-light row and the white-light comparison below are affected by the round seam joins of 2026-11-12: they were measured on 2026-10-17, and colored-light seams were already round; see [Round seam joins](#round-seam-joins).
|
||
|
||
Milliseconds per frame, medians of three five-frame averages. `8` is the CLI default thread count on this machine; `0` selects the single-thread renderer.
|
||
|
||
| Inflate scene | Before, 8 | After, 8 | Before, 0 | After, 0 |
|
||
|---|---:|---:|---:|---:|
|
||
| Two colored lights, solid fill | 136.2 | 96.8 | 147.2 | 100.6 |
|
||
| One colored light, solid fill | 94.4 | 96.7 | 107.0 | 100.0 |
|
||
| Legacy white light, restored polygon shading | 12.3 | 4.1 | 27.3 | 9.5 |
|
||
| Two colored lights, transparent source gradient | 145.6 | 146.8 | 156.9 | 157.0 |
|
||
|
||
The two-light solid-fill case takes 28.9% less frame time at the default thread count and 31.7% less in the single-thread renderer. The one-light case is 2.4% higher with eight threads and 6.5% lower single-threaded. The source-gradient case retains its previous per-facet compositor and changes by less than 1% in these medians. Restoring legacy white-light polygons also removes the extra smooth triangles from that path.
|
||
|
||
All six `perf` runs exited successfully, with no busy-machine warning and every individual budget row passing. The reported one-minute load averages were 4.5 to 5.9 on ten cores. Median milliseconds:
|
||
|
||
| Standard 50,000-path workload | Before | After | Budget |
|
||
|---|---:|---:|---:|
|
||
| render: fit page, 2880×1800 | 47.15 | 47.80 | 100 |
|
||
| render: background refresh after a pan | 48.18 | 46.71 | 100 |
|
||
| render: 400% zoom, 2880×1800 | 13.45 | 13.68 | 16 |
|
||
| hit test (per click) | 0.43 | 0.42 | 2 |
|
||
| save .vectorcraft | 52.78 | 52.15 | 300 |
|
||
| open .vectorcraft | 143.84 | 142.55 | 300 |
|
||
| export SVG | 207.37 | 206.25 | 500 |
|
||
| Pathfinder Unite, 1,000 paths | 43.18 | 43.29 | 150 |
|
||
| handle drag, 5,000-anchor path (per move) | 0.94 | 0.84 | 8 |
|
||
| segment drag, 5,000-anchor path (per move) | 0.87 | 0.92 | 8 |
|
||
|
||
The colored fixture uses the same projected mesh before and after. The solid-fill batch reduces compositor groups and clips; it retains three sets of channel facet paths. Smooth vector export still has many gradient elements.
|
||
|
||
## Rendering structure
|
||
|
||
For a smooth colored solid fill, all facets share one compound coverage clip and three isolated channel groups. Each channel has an opaque black backdrop; Screen combines the disjoint RGB channels and Multiply applies the source color. Face order is retained in each channel. Surface opacity and outer coverage apply once. The group/clip counts are independent of triangle count, and export remains vector artwork.
|
||
|
||
Source gradients retain their independently placed per-facet vector compositor. They remain a more expensive case. Patterns retain their neutral shading approximation. White-light Inflate uses the original polygon shading and paint composition; the seam width retains its 0.05 to 0.75 pt bounds. Its seams kept the default Miter Join until 2026-11-12, when every 3D seam took round joins (a mitered seam drew thorns past facets seen almost edge-on), so white-light output is no longer identical to the comparison below.
|
||
|
||
Geometry/effects tests cover legacy polygons and seam bounds, shared smooth normals, finite ramps, holes and constant group/clip counts. Engine tests cover the batched SVG round trip, vector PDF, expansion, undo, compound holes and surface opacity.
|
||
|
||
## Legacy white-light comparison
|
||
|
||
Four original synthetic scenes were exported at 1200 x 1200 from unchanged main `f7de3d6a` and the updated implementation. The PNG files matched (2026-10-17) byte for byte in all four cases: the legacy white-lit ellipse above; the same ellipse with both sides, hollow cap, twist 60 degrees, taper 70%, perspective 80 degrees and view Y rotation 145 degrees; an unshaded ellipse; and the transparent source gradient above under the legacy white light. The command recipe was also checked against the measured colored fixture's image pixels.
|
||
|
||
## Round seam joins
|
||
|
||
Measured once, on 2026-11-12, on another machine (an Apple M4 Pro with 14 cores) that was not idle: its one-minute load average was 6 to 13, so these are one loaded run, not the idle measurements above. Two `vectorcraft-cli` builds, each in release (`cargo build --release -p vectorcraft-cli`): main at `6d6bbff53`, and that commit with the round seam joins, run in turn, three times each. The measurement predates the change's rebase onto a main whose Rotate (Classic) and Revolve (Classic) draw their faces through Classic's surfaces, so it does not cover their seams. `bench` fits the artboard to 95% of 2880 x 1800 pixels, draws one frame to warm each renderer and reports the average of the frames after it: `threads 8` with the renderer's default workers (one per core it may use, up to 8, one core left free), `threads 0` on the calling thread alone. A cold frame is a whole `run` that opens the scene, builds its 3D art and draws it once.
|
||
|
||
White light: extruded type with a cove bevel, an Inflate, an Extrude & Bevel (Classic) star with a classic bevel and a Revolve, no strokes (6,283 nodes drawn). The median of three runs of 20 frames, on 8 threads and on one: main 8.0 ms and 18.3 ms; with the round joins 7.5 ms and 17.7 ms. Cold frames took 0.08 s of processor time on each. The differences are within noise for one loaded run. Not measured: `vectorcraft-cli perf`, the legacy white-light Inflate row above (still the 2026-10-17 figures), frames whose 3D art is rebuilt (while dragging a 3D slider, say) apart from the cold run, and SVG/PDF exports.
|
||
|
||
```
|
||
{"command":"file.new","params":{"width":800,"height":600}}
|
||
{"command":"text.create","params":{"x":40,"y":120,"text":"SAMPLE TYPE","size":64,"font":"Source Sans 3","color":"#3366cc"}}
|
||
{"command":"effect.apply","params":{"effect":"threeD.extrude","item":null,"params":{"depth":40,"rotationX":-35,"rotationY":25,"taper":47.5,"bevel":"cove","bevelHeight":2}}}
|
||
{"command":"path.create","params":{"d":"M100 300C100 245 145 200 200 200C255 200 300 245 300 300C300 355 255 400 200 400C145 400 100 355 100 300Z"}}
|
||
{"command":"paint.setFill","params":{"color":"#cc6633"}}
|
||
{"command":"paint.setStroke","params":{"none":true}}
|
||
{"command":"effect.apply","params":{"effect":"threeD.inflate","item":null,"params":{}}}
|
||
{"command":"path.create","params":{"d":"M400 200L440 280L520 290L460 340L480 420L400 380L320 420L340 340L280 290L360 280Z"}}
|
||
{"command":"paint.setFill","params":{"color":"#33aa66"}}
|
||
{"command":"paint.setStroke","params":{"none":true}}
|
||
{"command":"effect.apply","params":{"effect":"threeD.classicExtrude","item":null,"params":{"depth":50,"bevel":"classic","bevelHeight":4,"rotationX":80,"rotationY":10}}}
|
||
{"command":"path.create","params":{"d":"M600 200L640 200L660 300L640 400L600 400Z"}}
|
||
{"command":"paint.setFill","params":{"color":"#aa33aa"}}
|
||
{"command":"paint.setStroke","params":{"none":true}}
|
||
{"command":"effect.apply","params":{"effect":"threeD.revolve","item":null,"params":{}}}
|
||
```
|
||
|
||
The type is set in Source Sans 3, which a build has only with craft-fonts (`CRAFT_FONTS_DIR`, see `docs/development.md` › Fonts); without it the type falls back to another face. To reproduce, save the commands as `scene.jsonl` and, with each build:
|
||
|
||
```
|
||
vectorcraft-cli run --commands scene.jsonl --export scene.vectorcraft
|
||
vectorcraft-cli bench scene.vectorcraft --iters 20
|
||
/usr/bin/time -p vectorcraft-cli run --in scene.vectorcraft --export scene.png
|
||
```
|