Skip to content

V3: SVG to 3D (extruded logos in the 3D Model layer) - #192

Merged
git-chad merged 130 commits into
git-chad/shader-lab-v3-planfrom
git-chad/v3-svg-3d
Oct 1, 2026
Merged

git-chad merged 130 commits into
git-chad/shader-lab-v3-planfrom
git-chad/v3-svg-3d

Conversation

@git-chad

@git-chad git-chad commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Step 4 of roadmap 4.4 (Fase 5): SVG to 3D. It's stacked on animation clips.

What you get

  • SVG import. Add layer → 3D Model now also takes an .svg. The logo becomes a solid in its SVG colors, and the result is a normal 3D Model layer: Blender-style transforms, Chrome/Glass/Clay/…, studios, shadows, and exact depth for Blur, Glass, Relief and masks. Dropping an SVG on the canvas still makes an image layer.
  • Extrude controls. Depth is a share of the logo's larger side. Bevel rounds the front and back without growing the outline. Bevel Smoothness sets the bevel's steps.
  • Supported: paths, rect, circle, ellipse, polygon, polyline, line, groups with transforms, fill colors and fill-rule (holes and compound paths stay open), and strokes with width, joins and caps.
  • Excluded: text (convert it to outlines; an SVG with only text says so), images, filters, masks, clip paths and patterns. Gradients fall back to a solid color.
  • Clean overlaps and edges. Each later path sits slightly in front of the one before, so stacked shapes don't flicker. Curved walls shade smoothly while corners stay sharp.

Renders are in .context/svg-3d-materials.png and .context/svg-3d-smooth.png, compared against reference 31. Chrome only catches light on bevels, so the default bevel is 0.02 of the logo size.

Tests

  • model-svg.mjs adds 21 checks: holes, bevel inside the outline, depth as a share of size, y-up, fill colors, stacked paths, strokes, the text-only message, extrusion depth seen side-on, replacement materials, depth output and save/reopen.
  • model-layer-ui.mjs imports an SVG and checks the Extrude controls and the saved asset kind.
  • The full bun run test:composition suite passes on this branch, including the performance work below (SwiftShader).

Manual test: the SVG to 3D section in tests/composition/MODEL-LAYER-MANUAL-QA.md.

Also in this PR

  • tests/composition/wgsl-capture.mjs and wgsl-scan.mjs: a shader scan for future three.js upgrades. It catches variables that are read before any branch has set them, the bug that blocked 0.186.1. How to run it is in tests/composition/README.md.

  • A "Para retomar" section in the roadmap, under 4.4: what's pending, the loose ends, and the steps for moving to three r187.

  • Performance for the V3 layers, with no visual change. At 1080p on the Apple GPU:

    Layer Before After
    Connected Dots 43–50 ms about 5 ms
    Displaced Rings 5.4 ms 2.3 ms
    Focus Blur tilt-shift 3.4 ms 1.9 ms
    Glass hammered 3.5 ms 1.3 ms
    Plotter stipple 3.2 ms 1.0 ms

    Every other V3 layer costs the same or less.

    • Still layers are cached. Under an animated layer, the editor reuses the still layers below instead of redrawing them every frame.
    • Blob Tracking settles. It stops forcing redraws once its output has settled on a still image, and restarts when a layer below changes.
    • Annotations re-read edges only on change. In Edges mode the edge field is read again only when the image changes.
    • Pixels checked. The 95 composition renders are byte-identical or within 1/255, apart from 4 pixels at 3/255. Every style of every V3 layer was also compared at 1080p on the real GPU; the few differences are rounding ties.
    • Bug fix. Outline, Flares and Relief no longer read the wrong texture when their shader compiles before the first frame.
    • Changeset. The runtime package gets one: .changeset/v3-render-performance.md.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
shader-lab Ready Ready Preview Oct 1, 2026 2:04pm UTC

@greptile-apps

greptile-apps Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Medium risk] Adds SVG-to-3D extrusion feature to the model layer.

The PR should not merge until imported SVGs preserve partial opacity; the rebuild cost and restoration test also merit attention.

Summary

The PR adds SVG import to 3D Model layers, extruded fill and stroke geometry, editor controls, and composition tests.

  • SVG opacity is lost when original-color materials are built.
  • Extrusion edits rebuild and recompile the full model on each change.
  • The new save/reopen check does not exercise asset restoration.

Reviews (1) · Last reviewed commit: fcb6686

Comment thread src/renderer/model-svg.ts
Comment thread src/renderer/model-pass.ts
Comment thread tests/composition/model-svg.mjs
… style paper, duplicate test

- Un-premultiply ink color in transparent-paper output so alpha coverage is not applied twice (app and SDK pass)
- Show Angle for hatch and squiggle (new oneOf visibility condition); show Cross Angle only in hatch
- Plotter styles also set paper to color
- Duplication test now edits the real mode param and checks both layers
- Scale transparent-paper toner by input alpha so empty pixels stay clear
- Mix Amount in premultiplied space so partial amounts do not darken toner
- Skip the blur pyramid when Amount is zero
- Update a real parameter in the duplicate-independence check
- Mirror the pass changes into the React runtime
…ate test

Seed small shapes at their top-left tip so Cut Line and Cloud style them.
Raise the flood reach cap to 4096 so distant rings survive.
Let Scalloped draw inside for negative offsets.
Make the duplicate-independence check update a real parameter.
…terials, shadows, exact depth and Blender-style transforms

A new 3D Model source layer renders a .glb (or self-contained .gltf) into
its own HalfFloat MSAA target and composites it as a source over the layers
below. Draco, meshopt and KTX2 decoders are served from public/three. The
model is fitted to a tight vertex sphere with a margin; focal length changes
perspective without changing size.

Lighting: five CC0 Poly Haven studios or a custom .hdr, each normalized to
the same mean irradiance so switching studios keeps the exposure. The
environment goes through one persistent pmremTexture node set as
scene.environmentNode, because r183 bakes the environment texture node into
compiled materials and swapping scene.environment left them sampling the
disposed PMREM. A camera-relative key light casts PCF soft shadows onto an
invisible floor, and a blurred height pass under the model draws the
contact shadow. Tone mapping runs in the composite (Neutral by default).

Materials: the file's PBR materials or Chrome, Brushed Metal, Glass, Clay,
Rubber and Iridescent overrides. Anisotropy was dropped (flat white on
derivative tangents); iridescence is a view-angle spectral tint because
thin-film interference was invisible on metals.

Depth: an override pass writes near=1 depth normalized to the model's
bounding sphere, composited over any incoming scene depth, so Blur, Glass,
Relief, Input -> Depth and depth masks read the model, also inside groups.

Transform UX (asked for Blender basics): Location/Rotation/Scale as XYZ
fields with scrubbable axis letters, Move/Rotate/Scale gizmos, G/R/S modal
transforms with X/Y/Z locks, Alt+G/R/S clear, middle-drag orbit and
Shift+middle-drag shift. Modal keys are captured before other handlers so a
focused toolbar tooltip cannot swallow Escape.

Workarounds for three r183, found by tests:
- NodeMaterialObserver is shared per node builder state and its renderId
  fast path skips the world-matrix cache, so a mesh returning to an exact
  earlier pose kept a stale GPU matrix. A node-valued property on file
  materials makes the observer refresh every render.
- compileAsync allocates the transmission backdrop copy with the canvas
  format, breaking glass in HalfFloat targets; transmissive models compile
  on first draw instead.
- ShadowNode updates once per node frame per camera and frames only advance
  on three's own rAF, so the pass advances the node frame before each draw
  (hidden-tab exports and several draws per tick froze shadows).
- WebGPU only allows 1 or 4 samples; MSAA turns off above 4K so 8192 px
  exports stay within memory.

Exports wait up to 30 s for model and HDR loads.
The picker already uses the cube for 3D Model; the layer row fell back to
the generic image placeholder like effect layers do.
…, load tracking

- Offset the floor, contact shadow and key light target by Location Y
- Render the depth pass with per-material alpha-tested variants so cutouts stay empty
- Carry alpha test, alpha map, map alpha, transparency and depth write into material presets
- Track model and environment loads per request so stale loads cannot clear readiness
- Keep model and environment errors in separate slots and flag a missing custom HDR
three 0.183.2 -> 0.185.1 and @types/three 0.185.4. The published runtime now
asks for three ^0.185.0, so projects on 0.183 have to update three with it
(changeset included). @webgpu/types is now an explicit dev dependency
referenced from src/types/webgpu.d.ts, because @types/three stops pulling
it in with 0.186.

0.186.1, the latest release, was tried first and set aside. It has a TSL
codegen bug (three #34339, still open): a shared math value first read
inside an If/select branch is cached in a variable assigned only in that
branch, so every other read sees 0. It broke the Photographic Cells outline
at gap 0 and Focus Blur, and a scan of every WGSL shader the composition
suite generates found the same uninitialized reads in Glass, Connected Dots,
Photocopy and Plotter, with none on 0.185.1. r186 started counting usage
through ContextNode, so far more nodes get cached than before. Turning that
cache off inside branches and loops fixed Cells but made glass and blur
shaders compile several times slower and still failed a Focus Blur check,
so it is not worth shipping. r187 rewrites this cache; retry then with the
full suite and the WGSL scan.

The 3D layer no longer skips compileAsync for transmissive models: 0.183
still throws the transmission CopyTextureToTexture format error without the
skip, 0.185 does not. The exact-pose uniform refresh stays, since three only
fixes that in r186 (0.185 without it fails the pose-return test). The shadow
node-frame advance and the persistent environment node stay as well.

Visible changes: environment rotation follows three r184 (same direction as
rotating an object), so Studio Rotation turns the other way than on 0.183;
the layer is unreleased, so there is no migration. The r185 premultiplied-
alpha output change only touches transparent output pixels and the canvas
is opaque; all alpha checks pass.

three now reports uncaptured GPU errors through console.error. SwiftShader
cannot import decoded video frames, which produces the same 64 validation
errors on 0.183 (Chrome printed them as warnings, so they were never
counted). The composition suite now ignores exactly that ExternalTexture
chain, prints how many it skipped, and still fails on any other GPU error.

The roadmap records the new order the user asked for on September 29: three
upgrade, animation clips, SVG to 3D, and the path-traced Render mode last.
The shader reference lists every three/tsl export so the MCP can tell agents
which TSL functions exist. three 0.185 adds packNormalToRGB,
unpackRGBToNormal, transformNormalByInverseViewMatrix and the other names
from the r184 and r185 releases, so CI's snapshot check failed until the
generated file was refreshed.
Step 3 of roadmap 4.4 (4.3 in Fase 4). The Model section lists the clips of
a .glb, read straight from the glTF JSON (names and durations from the
animation input accessors), so the list shows without loading the model and
works for projects saved before this change.

Clip picks one clip, All clips plays every clip together, None shows the
rest pose. The automatic default plays every clip when each clip moves
different objects (per-object actions from Blender) and otherwise the first
one, so a character with Idle/Walk/Run on one skeleton does not blend them.

Play, Speed (0-4x), Repeat (Loop, Ping-Pong, Once) and Start map composition
time to clip time: Start + time * Speed, wrapped by the repeat mode, set on
each action and evaluated with mixer.update(0). That keeps preview, PNG and
video export identical frame by frame; paused shows the Start frame. The
clip settings are hidden registry params rendered in the Model section, so
they save, undo and duplicate like any other param. Importing an animated
model seeds the timeline length from the playing clips, like video.

Framing, floor height and shadows are computed over 24 poses spread across
the selected clips, so a moving model stays in frame and on its floor. The
depth pass, shadows and replacement materials follow skinned and morphed
meshes; Brushed Metal and Iridescent noise uses positionGeometry so it stays
on the surface instead of sliding while bones move. Floor sampling now
evaluates only the vertices it keeps.

model-animation.mjs adds 44 GPU checks: clip timing math, default clips,
parsing, poses over time, clip-wide framing, animated depth, speed, pause,
repeat modes, Start, None, All clips, skinning, timeline seeding, save and
reopen, and exported frames. The editor UI test imports an animated .glb
and checks the clip list, the timeline length and the Play toggle.
- Fit animated bounds from keyframe-aware pose samples with a larger per-pose vertex limit
- Give each clip a unique selection value so duplicate or reserved names resolve correctly
- Read the input accessor data when its max is missing so clip duration is not zero
- Seed the timeline duration when replacing a model
- Stop requesting renders once a Once clip has finished
Step 4 of roadmap 4.4 (Fase 5). Add layer -> 3D Model now also accepts
.svg: the asset is stored as a model with its image/svg+xml type, and the
layer is a normal 3D Model layer, so framing, Blender-style transforms,
materials, studios, shadows and exact depth all carry over. Dropping an SVG
on the canvas still makes an image layer. Replace accepts .glb, .gltf and
.svg.

Supported: paths with every command, rect, circle, ellipse, polygon,
polyline, line, groups with transforms, fill colors and fill-rule (nonzero
and evenodd, so holes and compound paths stay open), and strokes with their
width, joins and caps. Text has to be converted to outlines first; an SVG
with only text shows that message. Images, filters, masks, clip paths and
patterns are ignored, gradients fall back to a solid color.

Fills go through ShapePath.toShapes() (r185 replaces SVGLoader.createShapes),
are flattened at 48 points per curve with SVG's y-down axis flipped at the
shape level, and extruded with ExtrudeGeometry. Bevel uses a negative bevel
offset, so it rounds the front and back without growing the silhouette.
Strokes come from SVGLoader.pointsToStroke and are extruded as straight
prisms from their triangles, with side walls on boundary edges. Depth and
bevel are shares of the logo's larger side. Each later path sits slightly
in front of the one before so stacked shapes do not z-fight, and
crease-angle normals keep curved walls smooth and corners sharp (chrome
showed facets on the inner wall of a ring without them). Original uses the
SVG fill colors on a satin standard material. Changing Depth, Bevel or
Bevel Smoothness rebuilds the geometry from the parsed SVG, and the SVG
builder loads on demand.

The default bevel is 0.02 of the logo size: renders on the flat yellow of
reference 31 showed chrome only catching light on beveled edges.

model-svg.mjs adds 21 checks: the ring's hole, bevel inside the outline,
depth as a share of size, y-up, fill colors, stacked paths, stroke-only
shapes, the text-only message, the picker accepting .svg, extrusion depth
seen side-on, replacement materials, depth output and save/reopen. The
editor UI test imports an SVG and checks the Extrude controls and the saved
asset kind.
three 0.186.1 compiles some TSL graphs with a variable assigned inside only
one branch of an If/select and read outside it, so other pixels read 0
(three #34339). None of the composition checks fail on most of those reads,
so upgrades need a second gate.

wgsl-capture.mjs runs every registered composition check (discovered from
window, plus the baseline projects) and saves each unique shader module the
checks create. THREE_DIR swaps in an unpacked three package through a Bun
build plugin, so a candidate version can be captured without touching the
installed one; CHECKS limits the run. wgsl-scan.mjs tracks which variables
each block has assigned, treating an if/else chain as assigning a variable
only when every arm does, and reports reads with no assignment on the path.
Given a baseline capture it reports only new patterns and exits non-zero.
On the September 29 captures it finds 40 new patterns on 0.186.1 in exactly
the six layers that broke, and none comparing 0.185.1 with itself.

The roadmap gets a "Para retomar" block: what is pending acceptance, the
unfinished full run on the stack and unconfirmed CI, the six steps for
moving to r187 (types, runtime peer and changeset, MCP reference, dropping
the r186-fixed pose workaround, suite, shader scan), and that the
path-traced Render mode can start on 0.185.1.
Connected Dots cost 43-50 ms per frame at 1080p on an Apple GPU and was the
main source of heat. Each pixel recomputed 81 dot sites; a pre-pass now
stores the 25 distinct sites once per cell and the shader reads them back,
and only the active mode's link maths runs. It now takes about 5 ms.

Other passes, native GPU at 1080p, before -> after (ms):
Displaced Rings 5.4 -> 2.3, Focus Blur depth of field 3.2 -> 0.3 and
tilt-shift 3.4 -> 1.9, Glass hammered 3.5 -> 1.3, Plotter stipple 3.2 -> 1.0,
Signal Rot 1.95 -> 0.65, Outline stroke 2.0 -> 1.2, Photocopy 1.0 -> 0.4-0.85.
The savings come from running only the selected pattern, mode or shape,
skipping work whose slider is 0, sharing dependency chains with Fn/toVar,
and skipping the blur pyramid when no pixel reaches its first level.

The editor pipeline redrew every layer each frame whenever any layer
animated. It now caches the static layers at the bottom of the stack when a
continuous layer sits above them, and reuses them until something changes.
A photo with Connected Dots and Glass under an animated Signal Rot goes from
8.0 to 0.9 ms per frame, and the cached frame is pixel-identical to a full
render.

Passes now learn whether their input may have changed this frame. Blob
Tracking uses it to stop forcing continuous rendering once its temporal
state settles (it previously redrew the whole pipeline forever), restarts
when a layer below changes, and joins the static cache while settled.
Annotations re-read the Edges field only when their input changes instead of
every third frame, and shade only each mark's own kind.

Outline, Flares and Relief bound two texture nodes to one placeholder, which
merged into a single binding when the material compiled before its first
render; they now use separate placeholders.

Verified with the composition suite (all checks pass; 83 of 95 renders
byte-identical, the rest within 1/255 except 4 pixels at 3/255) and a
native-GPU dump of every style of every V3 layer at 1080p against HEAD.
Lumen Print Sabattier re-rolls grain on single-pixel columns and Dot Grid
Night moves a few dot edges by less than a pixel at rounding ties.
… asset in the reopen test

- Carry fill-opacity and stroke-opacity onto the SVG materials so semi-transparent logo parts stay translucent in Original mode
- Make the save/reopen check register the SVG asset, assert it resolves after reopen, and render the reopened project
@git-chad
git-chad force-pushed the git-chad/v3-model-animation branch from 9932ffb to 2f89143 Compare October 1, 2026 14:03
@git-chad
git-chad force-pushed the git-chad/v3-svg-3d branch from 728a4c5 to 6981dcf Compare October 1, 2026 14:03
@git-chad
git-chad changed the base branch from git-chad/v3-model-animation to git-chad/shader-lab-v3-plan October 1, 2026 15:14
@git-chad
git-chad merged commit 6981dcf into git-chad/shader-lab-v3-plan Oct 1, 2026
4 of 5 checks passed

This branch was successfully deployed

1 active deployment
Preview — 6981dcf2 Deployed Oct 1, 2026 by vercel[bot]
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.

1 participant