Skip to content

Surface instances, phase 1: copies of a piece on another's surface (RFC 0022) - #241

Merged
crisprintsstuff merged 4 commits into
mainfrom
surface-instances
Nov 14, 2026
Merged

crisprintsstuff merged 4 commits into
mainfrom
surface-instances

Conversation

@crisprintsstuff

Copy link
Copy Markdown
Owner

First phase of Surface instances (RFC 0022): copy one piece onto the faces, points or one face group of another as live copies that follow the surface.

What's in it

  • Placement core (oxy-mesh::instances): anchors (host points and weights) per polygon (quads kept), point or face group; per-copy transforms from the host's current surface: up along the interpolated normal, forward from a world direction (default), the longest edge, the first edge or random; size (share of the face or scene units), lift, turn, random size/turn with a seed; realize for Make real (one face group per copy).
  • Renderer: instanced pipelines and entry points (per-instance mat4 at locations 5–8), used only for instanced draws, so plain pieces keep their original shaders. Demo screenshot is pixel-identical to main and shading_timing is unchanged within noise.
  • Document and saving: Instancer { source, settings } on the host piece; new skippable INST section (layout in the format.rs header). Sources are saved by their place in the piece list. Older versions skip it and open the host and the hidden source as ordinary pieces.
  • Undo: new Step::Instancers and Step::Visible. Adding, changing (a slider drag is one step), removing, and deleting a host (its source is shown again) are each one undo step.
  • Editor and UI: a new Instances section in the right panel: pick a piece and Copy onto this piece, then On, Facing, Size, Lift, Turn, Vary size, Vary turn, Seed; Edit source shows the source alone and makes it active, so every tool works on it, and Done editing source puts the scene back; Make real runs as a background job and adds a new piece, keeping the instancer and its source; Remove. Sources show "copied" in the Pieces list. Each source belongs to one instancer, and copies of copies are refused.
  • Docs: CHANGELOG, README, ROADMAP, the in-app guide (new "Surface instances" topic), tooltips, and the RFC status.

Performance

Transforms are cached per instancer and worked out again only when the host, the source or the settings change. They are uploaded to the GPU only when they change. From the instance_timing example (release build, 4 cores):

  • 10k copies: under 1 ms.
  • 100k copies: 5–8 ms.

So sculpting a host with 100k copies adds that much per frame. With no instancers the per-frame cost is iterating the piece list.

Verified

  • cargo fmt --check, clippy -D warnings, cargo test --workspace, check_corpus_ip.py.
  • New tests:
    • oxy-mesh placement (5)
    • INST round trip, and an older reader skipping it (2)
    • history swap (1)
    • editor: add/undo/redo, one undo step per slider drag, group placement, following a sculpted host and an edited source, Edit source enter/exit, Make real, Remove, deleting a host
  • Headless screenshot (lavapipe) of a ball with 920 spikes: copies sit on the surface along the normals, and the Instances section looks right.

Not verified / limitations

  • Not run on a real GPU, Windows or macOS.
  • Exports (glTF, OBJ, STL, PLY) don't include the copies yet: Make real first (phase 4 adds glTF instancing).
  • No scatter, mask or edge placement, tiling, exclusions or fitted copies yet (phases 2–3).
  • Copies aren't pickable in the viewport, and the scene bounds (framing, floor) ignore them.
  • Make real refuses above 40M triangles instead of asking.
  • On triangle meshes, OxyBrush's geometric quad detection pairs some triangles into quads, so a copy can sit on a pair instead of each triangle (the same quads the status bar counts).
  • Saving while in Edit source saves the temporary visibility (other pieces hidden); nothing is lost.

Generated by Claude Code

oxy_mesh::instances: where copies of a source piece go on a host
(one per polygon, quads kept where the host has them; per point; or on
one face group), anchored to host points and weights so they follow the
host when it is sculpted; each copy's transform (up along the
interpolated normal, forward along a world direction, the longest edge,
the first edge or random; absolute or face-relative size; offset; spin;
seeded size and spin variation); and realize() for Make real.

instance_timing (this container, 4 cores): 100k copies re-placed after a
host change in 5-8 ms (rayon), 10k in under 1 ms; anchors, needed only
when the host's triangles change, 14-120 ms at 100k.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GEttLK8THCkZ7KHELbSp7g
Signed-off-by: Claude <noreply@anthropic.com>
ViewportRenderer::set_instances uploads a set of model matrices; a
DrawItem with `instances: Some(key)` draws its mesh once per matrix, in
the matcap, wireframe and shadow passes. Instanced items use their own
pipelines and *_instanced vertex entry points (the matrix in vertex slot
1, locations 5-8), so ordinary pieces keep exactly the vertex shaders
they had: a first version that read an identity matrix for every piece
cost 2-5% per frame on lavapipe (shading_timing); with separate
pipelines, interleaved runs of main and this branch are the same within
noise (plain 103.6-107.7 vs 103.8-105.2 ms, shadows 179.8-185.7 vs
181.8-185.0 ms). The demo screenshot is pixel-identical to before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GEttLK8THCkZ7KHELbSp7g
Signed-off-by: Claude <noreply@anthropic.com>
…22, phase 1)

Wire the placement core and the instanced renderer into the app, so a
piece can be copied onto the faces, points or one face group of another
as live copies that follow the surface.

- oxy-doc: Instancer on the host piece, and a skippable INST section.
  Sources are saved by their place in the piece list, because piece ids
  aren't saved. Older versions skip INST and open the host and the hidden
  source as ordinary pieces.
- oxy-history: Instancers and Visible steps, so adding, changing,
  removing and deleting a host are each one undo step.
- oxy-app: the Instances section, Edit source (the source alone, active,
  with every tool), Make real as a background job that keeps the
  instancer and its source, and cached transforms. Transforms are worked
  out again and uploaded only when the host, the source or the settings
  change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GEttLK8THCkZ7KHELbSp7g
Signed-off-by: Claude <noreply@anthropic.com>
Resolve the CHANGELOG and ROADMAP conflicts with View > Inactive pieces
(keep both entries; Surface instances replaces its 'planned' note).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GEttLK8THCkZ7KHELbSp7g
Signed-off-by: Claude <noreply@anthropic.com>
@crisprintsstuff
crisprintsstuff merged commit 3eb65b3 into main Nov 14, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants