Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
66eb930
test: establish V3 composition and alpha compatibility baselines
git-chad Sep 17, 2026
6915fac
test: surface GPU and resource failures in composition checks
git-chad Sep 17, 2026
5aebd21
test: select software WebGPU adapter explicitly
git-chad Sep 17, 2026
fa9be06
test: cover actual editor hydration and project resaving
git-chad Sep 17, 2026
e3a9f7d
ci: run composition GPU regressions on macOS
git-chad Sep 17, 2026
bdab0b7
test: verify hydration replaces stale state and prunes tracks
git-chad Sep 17, 2026
c69bafd
fix: reveal lower content around newly contained media
git-chad Sep 17, 2026
94c7312
fix: preserve alpha through source and effect composition
git-chad Sep 17, 2026
322980b
Add isolated group rendering to editor and runtime pipelines
git-chad Sep 17, 2026
e7a5f6c
Address Greptile review: guard stale media callbacks, dedupe strict g…
git-chad Oct 1, 2026
53340e0
feat: add editor groups with persistence, history, and runtime export
git-chad Sep 17, 2026
8f540ba
Address Greptile review: prune hidden group subtrees when publishing
git-chad Oct 1, 2026
c358bb5
feat: insert text over transparent backgrounds and revise V3 priorities
git-chad Sep 17, 2026
72ee931
Add displaced rings with grouped photographic cutouts
git-chad Sep 17, 2026
033eb38
Fix group-aware layer dragging and drop feedback
git-chad Sep 17, 2026
75968e3
Add photographic cell cutouts and curated ring defaults
git-chad Sep 17, 2026
bfc0cce
Fix cell outline coverage and preserve V3 visual direction
git-chad Sep 17, 2026
b995773
Detail photographic regions, outlines, and paint mode plan
git-chad Sep 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/alpha-compositing-foundation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@basementstudio/shader-lab": patch
---

Preserve alpha when copying external input textures and applying effect passes. Source layers use straight-alpha source-over blending; effect passes preserve input coverage and retain their existing alpha-as-strength behavior. Opaque compositions and legacy mask semantics are unchanged.
5 changes: 5 additions & 0 deletions .changeset/displaced-rings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@basementstudio/shader-lab": minor
---

Add Displaced Rings with rings/half-discs, progressive transforms, seeded offsets, and Distort/Cutout output. Support transformed coverage in the editor and runtime so grouped photographic cutouts reveal external layers, with opacity interpolating color and alpha together.
5 changes: 5 additions & 0 deletions .changeset/editor-groups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@basementstudio/shader-lab": minor
---

Support isolated groups in exported ShaderLabConfig layer lists using group layers and parentId references. Evaluate group visibility and opacity before building the render tree, preserving child effect scope in composition and headless rendering. Existing flat configs remain compatible.
4 changes: 4 additions & 0 deletions .changeset/group-drag-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

Fix editor layer dragging so dropping into or out of a group updates membership and order together. Add destination indicators, subtree movement, cancellation, and edge scrolling. This editor-only change does not require a package release.
5 changes: 5 additions & 0 deletions .changeset/isolated-group-rendering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@basementstudio/shader-lab": patch
---

Add isolated, nested group rendering to the internal renderer frame contract. Group children render against transparent targets before the combined result is blended into the parent. Existing flat public shader configs retain their behavior; editor grouping controls and persisted groups follow separately.
5 changes: 5 additions & 0 deletions .changeset/photographic-cells.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@basementstudio/shader-lab": minor
---

Add Photographic Cells with light/dark/random selection, irregular rectangular cells, full-resolution photographic interiors, alpha cutouts, and optional outlines. Support the effect in runtime configs and grouped compositions. Apply user-curated editor defaults to new Displaced Rings layers while preserving saved settings and renderer fallbacks.
5 changes: 5 additions & 0 deletions .changeset/transparent-media-bounds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@basementstudio/shader-lab": patch
---

Support transparent borders for contained image and video layers with `params.transparentBounds: true`. Existing runtime configs without the setting retain black borders.
4 changes: 4 additions & 0 deletions .changeset/transparent-text-defaults.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

New editor text uses normal composition over a transparent background. Clarify the background opacity control and preserve the solid fallback for saved text that omits the setting. This editor-only change does not require a package release.
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
branches:
- main
- git-chad/shader-lab-v3-plan
- git-chad/v3-*
push:
branches:
- main
Expand Down Expand Up @@ -50,3 +51,24 @@ jobs:
- name: Ensure changeset exists for PR changes
if: github.event_name == 'pull_request'
run: bunx changeset status --since=origin/${{ github.base_ref }}

composition:
# Linux Chromium/SwiftShader currently drops its GPU instance during these
# texture-backed shader compilations. Keep the full GPU checks on macOS.
runs-on: macos-14
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- run: bun install --frozen-lockfile
- run: bunx playwright install chromium
- run: bun run test:composition
- name: Upload rendered compositions
if: always()
uses: actions/upload-artifact@v4
with:
name: composition-actuals
path: .context/composition-test/
if-no-files-found: ignore
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Shader Lab development instructions

Before deciding how a shader behaves or looks, open the relevant images listed in [V3-VISUAL-REFERENCES.md](V3-VISUAL-REFERENCES.md). This is an explicit user requirement. Read that guide for reference locations, visual goals, and review criteria; a prose description or passing GPU tests does not replace looking at the images.

Keep prototype acceptance distinct from completing the visual direction in V3-ROADMAP.md. Preserve the stacked PR workflow; do not merge V3 into main until the plan is complete.
115 changes: 100 additions & 15 deletions V3-ROADMAP.md

Large diffs are not rendered by default.

45 changes: 45 additions & 0 deletions V3-VISUAL-REFERENCES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# V3 visual references

These sixteen images are the user's visual direction for Shader Lab V3. Before choosing shader behavior, geometry, defaults, or appearance, **open the relevant reference images**. Compare the resulting render with them. Technical correctness alone does not establish visual completion.

## Where to find them

- Original folder: `/Users/tobiasmoccagatta/Desktop/Shader Lab V3 - Referencias/`
- Workspace backup: `.context/v3-references/` (all sixteen originals copied September 17, 2026; gitignored).
- Overview for orientation: `.context/v3-reference-sheet.png` (gitignored). Open individual originals to inspect detail.

The images are visual references, not application assets. They are not bundled into the product or committed here. In another workspace, use the original folder or copy it into that workspace's `.context/v3-references/`. If neither location exists, request the images instead of guessing from their names.

## Reference index

| Image | What to inspect |
| --- | --- |
| `01-lovedance-rojo-azul.png` | Bold geometry over red/blue photography, blurred imagery, layered type. |
| `02-collage-editorial-monocromo.png` | Fragmented monochrome photo, circular geometry, dense type and technical lines. |
| `03-rana-falso-color.png` | Selective false color, dark negative space, delicate tracking annotations. |
| `04-trama-puntos-halos.png` | Fine regular dots, luminous cross-shaped halos, optical contrast. |
| `05-recortes-celdas-color.png` | Connected color regions with stepped cellular boundaries, fine dot texture, sparse technical linework on white. |
| `06-james-blake-fotografia-difusa.png` | Washed green photography, diffuse light and grain, drifting photographic forms. |
| `07-industrial-negro-naranja.png` | Burned orange/yellow highlights, deep black, layered industrial photo and type. |
| `08-estrella-halos-color.png` | Soft multicolor contours and halos around a geometric star. |
| `09-collage-tipografia-tramas.png` | Oversized layered type, photographic fragments, print texture and vivid accents. |
| `10-retrato-azul-anotaciones.png` | Diffuse false-color portrait, washed ground, fine annotations and diagram details. |
| `11-puente-recortes-geometricos.png` | Connected stepped photographic silhouettes, thin perimeter contours, generous white negative space and pink linework. |
| `12-retrato-tonos-invertidos.png` | Partial tonal inversions, solarized photographic detail and washed contrast. |
| `13-arrastre-escaner-rojo.png` | Horizontal scanner drag, torn bands, red/black/white distortion. |
| `14-rostro-rosa-erosionado.png` | Eroded photographic reveal, fine grain, pink staining and white negative space. |
| `15-relieve-grabado-plateado.png` | Fine engraved relief, metallic tonal structure and directional edges. |
| `16-anillos-desfasados-azules.png` | Rotated/offset concentric photographic bands, electric blue/black contrast and layered type. |

## Photographic Cells: accepted prototype, unfinished visual direction

The user considers the first layer useful enough; the Gap/outline inflation has been corrected. This does **not** mean it achieves references 05 and 11.

- The bridge is one photographic subject revealed through connected, stepped regions. Its contours follow the region perimeter; it is not a brick grid with every cell individually outlined.
- The white poster combines broad flowing color regions, small stepped edge fragments, fine dots, and delicate technical marks. Large randomly selected rectangular cells alone do not reproduce that appearance.
- Approved next direction: automatic connected regions and integrated None / Perimeter / Every Cell outlines first, followed by a Paint selection mode with Reveal / Erase, Brush Size, Clear, and undo. Preserve photographic detail and share geometry/outline behavior between automatic and painted selection.
- Region Size controls the broad patches; Cell Size controls their stepped boundaries. Painting should remain useful independently of automatic selection, without implying subject detection.
- Evaluate Edge Scatter later. Try existing halftone and color layers for the fine dot treatment before adding more controls. The detailed sequence and acceptance criteria are in roadmap section 3.3.1; these improvements are planned, not implemented.
- Use these images for side-by-side visual review before marking the cell family complete. The present center-sampled row grid, per-cell outlines, and absence of fine dot treatment remain explicit limits.

For every shader slice, note the reference filenames used, which visible qualities it achieves, and which remain follow-up work. Keep the experimental editorial, analog photographic, and acid-graphics direction visible in actual renders.
9 changes: 7 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"version-canary": "changeset version --snapshot canary",
"version-packages": "changeset version",
"start": "next start",
"test:composition": "bun tests/composition/run.mjs",
"typecheck": "tsgo --noEmit",
"typecheck:runtime": "bun run --cwd packages/shader-lab-react typecheck",
"typecheck:compare": "bun run typecheck:tsc && bun run typecheck",
Expand Down Expand Up @@ -84,6 +85,7 @@
"babel-plugin-react-compiler": "1.0.0",
"cross-env": "^10.1.0",
"drizzle-kit": "^0.31.10",
"playwright": "^1.63.0",
"postcss-preset-env": "^10.6.1",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3"
Expand Down
20 changes: 20 additions & 0 deletions packages/shader-lab-react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ Supported effect layers include ASCII, blob tracking, CRT, directional blur, dit

The blob-tracking layer detects moving (or bright) regions of the content below and frames them with CCTV-style shapes, labels, connecting lines, and trails. One inner effect can render inside the detected shapes via `params.innerEffectType` (any pass-backed effect type) and `params.innerEffectParams` (a JSON string of that effect's parameter overrides; see the `ShaderLabBlobInnerEffect` type). Set `params.outputMode` to `"mask"` to emit white-on-black fills for use with `compositeMode: "mask"`.

## Isolated groups

Exported configs can include layers with `kind: "group"`, `type: "group"`, and `params: {}`. Children reference the group's ID through `parentId`. The flat array uses top-first, depth-first order: group first, then all of its descendants, then the next sibling. Missing or null `parentId` places a layer at the scene root. IDs must be unique and nesting is limited to eight groups.

For example, the order `[title, portraitGroup, halftone, photo, background]` with `halftone.parentId` and `photo.parentId` set to `portraitGroup.id` confines halftone to the photo. Each group starts transparent, processes its contents, and then applies its own opacity and blend mode to the result. Hiding a group hides its whole subtree without changing child visibility. Group opacity/visibility can use ordinary layer timeline tracks.

Groups use `compositeMode: "filter"`, `hue: 0`, and `saturation: 1`; coverage masks and group color adjustments are not exposed in this iteration. Group targets currently cost about 32 MiB per group at 1080p. Existing flat configs continue to work without any new fields.

## Fonts

The ASCII and text layers resolve font families at runtime by reading CSS
Expand Down Expand Up @@ -347,6 +355,13 @@ export function PostProcessingExample({
- Keep the postprocessing `width`/`height` and `postprocessing.resize(...)` in sync with the input render target size, especially for screen-space effects like dithering, pixelation, and ASCII
- `postprocessing.texture` always points to the latest output texture

External input textures and output textures use straight (unpremultiplied) RGB
and alpha. The texture path preserves input coverage through effect passes;
effect output alpha controls effect strength. Source layers use source-over
composition. Convert premultiplied input to straight alpha before passing it in.
Legacy mask layers still use their existing RGB-darkening behavior, and the
canvas composition background remains opaque.

## `useShaderLab`

```ts
Expand Down Expand Up @@ -653,6 +668,11 @@ export const sketch = Fn(() => {

## Included Runtime Support

Image and video layers in Contain mode support `params.transparentBounds: true`
to reveal the content beneath their empty borders. Omit the setting or use
`false` to retain black borders. This option does not affect Cover mode or make
the composition's canvas background transparent.

- Gradient
- Text
- Custom shader (source and effect modes)
Expand Down
4 changes: 3 additions & 1 deletion packages/shader-lab-react/src/ambient/three-tsl.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
declare module "three/tsl" {
export interface LoopConfig {
condition?: string
end: number
end: number | TSLNode
start: number
type: "float" | "int"
}
Expand Down Expand Up @@ -113,6 +113,8 @@ declare module "three/tsl" {
): TSLNode
export function exp(value: unknown): TSLNode
export function fract(value: unknown): TSLNode
export function If(condition: TSLNode, callback: () => unknown): TSLNode
export function int(value?: unknown): TSLNode
export function float(value?: unknown): TSLNode
export function floor(value: unknown): TSLNode
export function mat2(
Expand Down
3 changes: 3 additions & 0 deletions packages/shader-lab-react/src/ambient/three-webgpu.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ declare module "three/webgpu" {
import type {
Box3,
Camera,
Color,
ColorRepresentation,
Material,
Scene,
Expand Down Expand Up @@ -47,6 +48,8 @@ declare module "three/webgpu" {
// biome-ignore lint/suspicious/noExplicitAny: compute node type from Fn().compute() is opaque
computeAsync(computeNodes: any): Promise<void>
clear(): void
getClearAlpha(): number
getClearColor(target: Color): Color
copyTextureToTexture(
srcTexture: Texture,
dstTexture: Texture,
Expand Down
46 changes: 45 additions & 1 deletion packages/shader-lab-react/src/renderer/blend-modes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,16 @@ export type MaskNodeConfig = {
source: string
}

export type CompositionRole = "source" | "effect" | "transform"

export function buildBlendNode(
mode: string,
base: Node,
blend: Node,
opacity: Node,
compositeMode: "filter" | "mask" = "filter",
maskConfig?: MaskNodeConfig,
role: CompositionRole = "source",
): Node {
const baseRgb = base.rgb
const blendRgb = blend.rgb
Expand Down Expand Up @@ -228,9 +231,50 @@ export function buildBlendNode(
}

if (compositeMode === "filter") {
return vec4(mix(baseRgb, composited, normalizedOpacity.mul(blendAlpha)), float(1))
const baseAlpha = float(clamp(base.a, float(0), float(1)))
if (role === "transform") {
// Spatial transformations replace coverage as well as color. Interpolate
// premultiplied pixels so opacity=0 restores input and cutout holes stay clear.
const outputAlpha = mix(baseAlpha, blendAlpha, normalizedOpacity)
const transformedRgb = mix(blendRgb, composited, baseAlpha)
const premultiplied = mix(
baseRgb.mul(baseAlpha),
transformedRgb.mul(blendAlpha),
normalizedOpacity
)
return vec4(
premultiplied.div(
select(outputAlpha.greaterThan(0), outputAlpha, float(1))
),
outputAlpha
)
}
const amount = normalizedOpacity.mul(blendAlpha)
const filteredRgb = mix(baseRgb, composited, amount)

if (role === "effect") {
// An effect's output alpha controls its strength, not a second layer of
// coverage. Preserve the input silhouette through repeated filtering.
return vec4(filteredRgb, baseAlpha)
}

// Straight-alpha source-over, with blending only where source and backdrop
// overlap. See https://www.w3.org/TR/compositing-1/#blending .
const outputAlpha = amount.add(baseAlpha.mul(float(1).sub(amount)))
const sourceRgb = mix(blendRgb, composited, baseAlpha)
const premultipliedRgb = sourceRgb.mul(amount).add(
baseRgb.mul(baseAlpha).mul(float(1).sub(amount)),
)
// Avoid division by zero without dimming tiny but nonzero coverage.
const denominator = select(outputAlpha.greaterThan(0), outputAlpha, float(1))
const outputRgb = premultipliedRgb.div(denominator)
// Keep the exact existing expression for opaque backdrops.
return vec4(select(baseAlpha.equal(1), filteredRgb, outputRgb), outputAlpha)
}

// Existing mask mode is a saved RGB-darkening operation. Coverage masks
// require an explicit compatibility path and are not introduced here.

const source = maskConfig?.source ?? "luminance"
let maskValue: Node
switch (source) {
Expand Down
Loading
Loading