@basementstudio/shader-lab

中文文档
3.1.0发布于 2026/10/14TypeScriptES Module构建来源已验证近 30 天下载 2828

Portable React runtime for Shader Lab exports.

原始使用文档
文档目录31 节
  1. Install
  2. Peer Dependencies
  3. Requirements
  4. Isolated groups
  5. Fonts
  6. API Overview
  7. High-level API
  8. Advanced APIs
  9. 1. Render a Composition
  10. 2. Use a Composition as a Texture
  11. Texture Notes
  12. 3. Use Shader Lab as Postprocessing
  13. Postprocessing Notes
  14. useShaderLab
  15. Options
  16. Return Value
  17. postprocessing
  18. Advanced APIs
  19. Hooks
  20. Classes
  21. Custom Shader Layer
  22. How it works
  23. Source mode vs Effect mode
  24. Injected scope
  25. TSL globals
  26. Shader Lab utilities
  27. Examples
  28. Included Runtime Support
  29. Utility Exports
  30. Notes
  31. Links

@basementstudio/shader-lab

basement.studio logo NPM version Website

@basementstudio/shader-lab is a portable React runtime for Shader Lab compositions exported from the editor.

It supports three main use cases:

  • Render a composition directly in React
  • Use a composition as a texture in your own scene
  • Use Shader Lab effect layers as postprocessing over your own scene

Install

npm install @basementstudio/shader-lab three
bun add @basementstudio/shader-lab three

Peer Dependencies

  • react
  • react-dom
  • three

Requirements

  • The runtime requires WebGPU support in the browser
  • ShaderLabComposition and the hooks in this package are client-side APIs
  • Media layers expect accessible asset URLs in layer.asset.src
  • Composition texture output is canvas-backed and can be consumed by WebGL or WebGPU host scenes
  • Postprocessing must run on the same WebGPURenderer as the scene texture you pass in

Supported effect layers include ASCII, blob tracking, CRT, directional blur, dithering, halftone, ink, particle grid, pattern, pixelation, pixel sorting, posterize, slice, edge detect, displacement map, and chromatic aberration.

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 custom properties (--geist-mono, --bsmnt-grotesque, --adhesion, …) from the document root. The value of each variable is used as the CSS font-family for glyph rasterization; when a variable is unset, the layer falls back to a system font.

The package ships the three openly licensed families — Geist Sans, Geist Mono, and BSMNT Grotesque — with a stylesheet that declares the @font-face rules and sets their variables:

import "@basementstudio/shader-lab/fonts.css"

The remaining families selectable in the Shader Lab editor (Adhesion, Blob, Bunker, Caniche, Carpenter, Curia, FFFlauta, Numero, Xer0, Trovador, B-Mecha) are commercially licensed. They are used in the hosted Shader Lab app but are deliberately not distributed with this package. To use them (or any font of your own), load the font in your host app and set the matching CSS variable to its family name, e.g. :root { --adhesion: "Adhesion" }.

API Overview

High-level API

  • ShaderLabComposition
  • useShaderLab

Advanced APIs

  • useShaderLabCanvasSource
  • useShaderLabPostProcessingSource
  • useShaderLabTextureSource
  • ShaderLabCanvasSource
  • ShaderLabPostProcessingSource
  • ShaderLabTextureSource

Use useShaderLab unless you specifically need manual timing or lifecycle control.

1. Render a Composition

"use client"

import {
  ShaderLabComposition,
  type ShaderLabConfig,
} from "@basementstudio/shader-lab"

const config: ShaderLabConfig = {
  layers: [],
  timeline: {
    duration: 6,
    loop: true,
    tracks: [],
  },
}

export default function Example() {
  return (
    <div style={{ width: "100%", maxWidth: 1200 }}>
      <ShaderLabComposition config={config} />
    </div>
  )
}

Handle runtime errors:

<ShaderLabComposition
  config={config}
  onRuntimeError={(message) => {
    if (message) {
      console.error(message)
    } else {
      console.info("Shader Lab runtime error resolved.")
    }
  }}
/>

onRuntimeError is called with a string message when a runtime error occurs (renderer initialization failure, shader compilation error, missing WebGPU support). After an error has been reported, it is called once with null when the error is resolved so error UIs can recover. It is never called with null before an error has been reported.

2. Use a Composition as a Texture

useShaderLab can manage the runtime and return a ready-to-use THREE.CanvasTexture.

"use client"

import {
  useShaderLab,
  type ShaderLabConfig,
} from "@basementstudio/shader-lab"
import { OrbitControls, PerspectiveCamera } from "@react-three/drei"
import { Canvas, useFrame } from "@react-three/fiber"
import { useMemo, useRef } from "react"
import * as THREE from "three"
import { WebGPURenderer } from "three/webgpu"

function Scene({ config }: { config: ShaderLabConfig }) {
  const { texture } = useShaderLab(config, {
    width: 1024,
    height: 1024,
  })

  const material = useMemo(
    () => new THREE.MeshBasicMaterial({ color: 0xffffff }),
    []
  )
  const meshRef = useRef<THREE.Mesh | null>(null)

  useFrame((_, delta) => {
    if (meshRef.current) {
      meshRef.current.rotation.y += delta * 0.3
    }

    if (texture) {
      texture.needsUpdate = true
      material.map = texture
      material.needsUpdate = true
    }
  })

  return (
    <>
      <color attach="background" args={["#0a0a0a"]} />
      <PerspectiveCamera makeDefault fov={50} position={[0, 0, 4]} />
      <mesh ref={meshRef} material={material}>
        <sphereGeometry args={[1, 64, 64]} />
      </mesh>
      <OrbitControls enableDamping />
    </>
  )
}

export function TexturedExample({ config }: { config: ShaderLabConfig }) {
  return (
    <Canvas
      gl={async (props) => {
        const renderer = new WebGPURenderer(
          props as ConstructorParameters<typeof WebGPURenderer>[0]
        )
        await renderer.init()
        return renderer
      }}
    >
      <Scene config={config} />
    </Canvas>
  )
}

Texture Notes

  • texture is backed by the package's internal output canvas
  • canvas is also available from useShaderLab if you want to integrate at the raw canvas level
  • The runtime updates the composition for you internally
  • This texture path is portable across WebGL and WebGPU host scenes because the package output is canvas-backed

3. Use Shader Lab as Postprocessing

For postprocessing, useShaderLab exposes a postprocessing handle.

You render your scene into a texture, then hand that texture to Shader Lab.

"use client"

import {
  useShaderLab,
  type ShaderLabConfig,
} from "@basementstudio/shader-lab"
import { Canvas, useFrame, useThree } from "@react-three/fiber"
import { PerspectiveCamera } from "@react-three/drei"
import { createPortal } from "@react-three/fiber"
import { useEffect, useMemo, useRef } from "react"
import * as THREE from "three"
import { float, texture as tslTexture, uv, vec2 } from "three/tsl"
import { MeshBasicNodeMaterial, WebGPURenderer } from "three/webgpu"

function PostProcessedScene({ config }: { config: ShaderLabConfig }) {
  const { gl, scene, size } = useThree()
  const renderer = gl as unknown as WebGPURenderer
  const cameraRef = useRef<THREE.PerspectiveCamera | null>(null)
  const sceneTargetRef = useRef<THREE.WebGLRenderTarget | null>(null)

  const { postprocessing } = useShaderLab(config, {
    renderer,
    width: size.width,
    height: size.height,
  })

  const presentScene = useMemo(() => new THREE.Scene(), [])
  const presentCamera = useMemo(
    () => new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1),
    []
  )
  const presentTextureNode = useMemo(() => {
    const sampleUv = vec2(uv().x, float(1).sub(uv().y))
    return tslTexture(new THREE.Texture(), sampleUv)
  }, [])
  const presentMaterial = useMemo(() => {
    const material = new MeshBasicNodeMaterial()
    material.colorNode = presentTextureNode.rgb
    return material
  }, [presentTextureNode])

  useEffect(() => {
    const target = new THREE.WebGLRenderTarget(size.width, size.height)
    sceneTargetRef.current = target

    return () => {
      sceneTargetRef.current = null
      target.dispose()
    }
  }, [size.height, size.width])

  useEffect(() => {
    sceneTargetRef.current?.setSize(size.width, size.height)
    postprocessing.resize(size.width, size.height)
  }, [postprocessing, size.height, size.width])

  useFrame((state, delta) => {
    const target = sceneTargetRef.current
    const camera = cameraRef.current
    if (!(target && camera && postprocessing.ready)) return

    renderer.setRenderTarget(target)
    renderer.render(scene, camera)

    const output = postprocessing.render(
      target.texture,
      state.clock.elapsedTime,
      delta
    )

    if (output) {
      presentTextureNode.value = output
    }

    renderer.setRenderTarget(null)
    renderer.render(presentScene, presentCamera)
  }, 1)

  return (
    <>
      <PerspectiveCamera ref={cameraRef} makeDefault position={[0, 2, 5]} />

      <mesh position={[-1.2, 0, 0]}>
        <boxGeometry args={[1.4, 1.4, 1.4]} />
        <meshStandardMaterial color="#e74c3c" />
      </mesh>

      <mesh position={[1.2, 0, 0]}>
        <sphereGeometry args={[0.8, 32, 32]} />
        <meshStandardMaterial color="#3498db" />
      </mesh>

      {createPortal(
        <mesh frustumCulled={false}>
          <planeGeometry args={[2, 2]} />
          <primitive attach="material" object={presentMaterial} />
        </mesh>,
        presentScene
      )}
    </>
  )
}

export function PostProcessingExample({
  config,
}: {
  config: ShaderLabConfig
}) {
  return (
    <Canvas
      gl={async (props) => {
        const renderer = new WebGPURenderer(
          props as ConstructorParameters<typeof WebGPURenderer>[0]
        )
        await renderer.init()
        return renderer
      }}
    >
      <PostProcessedScene config={config} />
    </Canvas>
  )
}

Postprocessing Notes

  • Postprocessing is same-renderer only
  • Pass your scene texture from the same WebGPURenderer you gave to useShaderLab
  • WebGL host renderers are not supported for postprocessing
  • Effect-only Shader Lab configs are the most natural fit here
  • 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

const { canvas, ready, texture, postprocessing } = useShaderLab(config, options)

Options

OptionDescription
widthOutput width for the internal runtime
heightOutput height for the internal runtime
pixelRatioOptional pixel ratio override
canvasOptional existing canvas to render the composition into
rendererOptional shared WebGPURenderer used for postprocessing

Return Value

FieldDescription
canvasInternal output canvas
readytrue when the managed texture path is initialized
textureManaged THREE.CanvasTexture backed by the runtime canvas
postprocessingHandle for same-renderer postprocessing

postprocessing

FieldDescription
readytrue when the postprocessing source is initialized
textureLatest processed output texture
resize(width, height, pixelRatio?)Resize the runtime targets
render(inputTexture, time, delta)Render Shader Lab effects into an output texture

Advanced APIs

These APIs are available when you want more direct control over timing, resize behavior, or object lifecycle.

Use them when:

  • You already have your own render loop and want to drive updates manually
  • You want the raw output canvas instead of a managed CanvasTexture
  • You want to own the full postprocessing pipeline yourself
  • You need to use the runtime outside React (but still in a browser/client runtime)

Hooks

  • useShaderLabCanvasSource
  • useShaderLabPostProcessingSource
  • useShaderLabTextureSource
  • useShaderLabTexture

useShaderLabCanvasSource

Use this when you want the raw canvas and manual timing.

const { canvas, ready, resize, update } = useShaderLabCanvasSource(config, {
  width: 1024,
  height: 1024,
})

useFrame((state, delta) => {
  if (!ready) return
  update(state.clock.elapsedTime, delta)
})

This is useful when:

  • You want to create your own THREE.CanvasTexture
  • You want to feed the canvas into some other pipeline
  • You want to decide exactly when the composition updates

useShaderLabPostProcessingSource

Use this when you want to control the full scene-to-texture-to-screen flow yourself.

const { ready, error, resize, texture, update } =
  useShaderLabPostProcessingSource(effectConfig, {
    renderer,
    width: size.width,
    height: size.height,
  })

useFrame((state, delta) => {
  if (!(ready && sceneTarget)) return

  renderer.setRenderTarget(sceneTarget)
  renderer.render(scene, camera)

  const output = update(
    sceneTarget.texture,
    state.clock.elapsedTime,
    delta
  )

  renderer.setRenderTarget(null)
  // present(output)
})

Make sure the Shader Lab postprocessing source is resized whenever your scene render target size changes. Screen-space effects use that size to determine their sampling grid.

This is useful when:

  • You already have your own render targets
  • You want to decide how the final output is presented
  • You need lower-level access than useShaderLab().postprocessing

useShaderLabTextureSource

This exposes the lower-level internal texture source.

Use it only if you specifically want that internal texture path and understand the renderer constraints. For most cases, useShaderLab or useShaderLabCanvasSource is the better choice.

Classes

  • ShaderLabCanvasSource
  • ShaderLabPostProcessingSource
  • ShaderLabTextureSource

The class APIs are the non-React equivalents of the hooks.

Typical flow:

const source = new ShaderLabCanvasSource(config, {
  width: 1024,
  height: 1024,
})

await source.initialize()
source.update(time, delta)
source.resize(width, height)
source.dispose()

Use the classes when:

  • You are outside React
  • You want full manual lifecycle control
  • You are integrating Shader Lab into a custom runtime or framework

Custom Shader Layer

The custom shader layer lets you write GPU shaders using Three.js TSL (Three Shading Language). Your sketch runs entirely on the GPU as a node graph — there is no GLSL or WGSL to write.

How it works

Your code must export a function wrapped in Fn() that returns a TSL node (vec3 or vec4). The editor compiles your TypeScript, strips imports, and evaluates the result in a sandbox with a pre-injected scope. Everything from three/tsl and the Shader Lab utility library is available as globals — no imports needed.

export const sketch = Fn(() => {
  const color = vec3(uv().x, uv().y, sin(time).mul(0.5).add(0.5))
  return color
})

Click Apply (or re-paste) to recompile. The entry export name defaults to sketch but can be changed in the Entry Export field.

Source mode vs Effect mode

The custom shader layer has an Effect Mode toggle:

  • Source mode (default) — the layer generates pixels from scratch, like a gradient or image. Your sketch outputs a color for each pixel. The output is linearized (sRGB → linear via pow(2.2)) to match the compositing pipeline.

  • Effect mode — the layer acts as a post-processing effect. It receives the composited result of all layers below it via inputTexture and transforms it. No gamma conversion is applied since the input is already in linear space.

Injected scope

These variables are injected into your sketch and available as globals:

VariableTypeDescription
timefloatElapsed time in seconds, continuously updated
inputTextureTextureNodeThe composited layers below (effect mode). In source mode this is a blank texture. Use directly for the default-UV color, or call .sample(customUV) to re-sample at custom coordinates

TSL globals

Everything exported from three/tsl is available. Key ones:

FunctionDescription
Fn(() => { ... })Defines a deferred shader function (required wrapper)
float, vec2, vec3, vec4Constructors for scalar and vector types
uv()Fragment UV coordinates (0–1)
screenSizeViewport resolution as vec2
sin, cos, abs, floor, round, pow, mix, clamp, smoothstep, dot, min, maxStandard math
texture(tex, uv)Sample a texture at given UVs
uniform(value)Create a uniform node
select(cond, a, b)Ternary selection
If(cond, () => { ... }).Else(...)Conditional branching with .toVar() assignments

Nodes are chained with methods: a.add(b), a.mul(b), a.sub(b), a.div(b), a.negate(), a.toVar() (makes mutable), a.assign(b) (mutates a toVar node).

Shader Lab utilities

These are additional helpers injected alongside TSL:

Noise

FunctionSignatureDescription
simplexNoise3d(vec3) → float3D simplex noise
simplexNoise4d(vec4) → float4D simplex noise (use .w for time)
perlinNoise3d(vec3) → float3D Perlin noise
valueNoise3d(vec3) → float3D value noise
voronoiNoise3d(vec3) → vec23D Voronoi (.x = cell dist, .y = edge dist)
fbm(vec3) → floatFractal Brownian motion (2-octave simplex)
curlNoise3d(vec3) → vec33D curl noise
curlNoise4d(vec4) → vec34D curl noise
ridgeNoise(vec3) → floatRidge noise
turbulence(vec3) → floatTurbulence noise

Tonemapping

FunctionSignatureDescription
reinhardTonemap(vec3) → vec3Reinhard tonemap
acesTonemap(vec3) → vec3ACES filmic tonemap
technicolorTonemap(vec3) → vec3Warm technicolor look
cinematicTonemap(vec3) → vec3Cinematic S-curve with tint
bleachBypassTonemap(vec3) → vec3Desaturated high-contrast
crossProcessTonemap(vec3) → vec3Cross-process color shift
totosTonemap(vec3) → vec3Basement's custom filmic tonemap

SDF primitives

FunctionSignatureDescription
sdBox2d(vec2, float) → float2D box signed distance
sdSphere(vec3, float) → float3D sphere signed distance
sdDiamond(vec2, float) → float2D diamond signed distance
sdRhombus(vec2, vec2) → float2D rhombus signed distance

Math & space

FunctionSignatureDescription
screenAspectUV(screenSize, range?) → vec2Aspect-corrected UVs centered at origin
rotate(vec2, float) → vec22D rotation by angle (radians)
smin(float, float, float) → floatSmooth minimum
smax(float, float, float) → floatSmooth maximum
atan2(float, float) → floatTwo-argument arctangent
tanh(float) → floatHyperbolic tangent
sinh(float) → floatHyperbolic sine
cosh(float) → floatHyperbolic cosine
cosinePalette(t, a, b, c, d, e?) → vec3Cosine-based color palette generator

Complex math

All complex operations use vec2 where .x = real, .y = imaginary:

complexMul, complexDiv, complexPow, complexSqrt, complexSin, complexCos, complexTan, complexLog, complexConj, complexToPolar, complexMobius

Patterns

FunctionSignatureDescription
bloom(float) → floatSoft glow curve
bloomEdgePattern(float, float, float) → floatEdge-aware bloom
canvasWeavePattern(vec2, float) → floatWoven canvas texture
grainTexturePattern(vec2) → floatFilm grain texture
repeatingPattern(vec2, float) → vec2Tiling UV helper

Examples

Source mode — animated gradient with tonemap:

export const sketch = Fn(() => {
  const uv0 = screenAspectUV(screenSize)
  const color = vec3(
    uv0.x.add(0.5),
    uv0.y.add(0.5),
    sin(time).mul(0.5).add(0.5)
  ).toVar()
  color.assign(technicolorTonemap(color))
  return color
})

Effect mode — sample input at custom UVs with distortion:

export const sketch = Fn(() => {
  const warp = sin(uv().y.mul(20).add(time.mul(2))).mul(0.02)
  const warped = vec2(uv().x.add(warp), uv().y)
  return inputTexture.sample(warped)
})

Effect mode — simple color manipulation:

export const sketch = Fn(() => {
  const color = inputTexture.toVar()
  const luma = dot(color.rgb, vec3(0.299, 0.587, 0.114))
  return vec4(vec3(luma), color.a)
})

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)
  • Image and video sources
  • Live camera input
  • ASCII
  • Pattern
  • Ink
  • Halftone
  • Dithering
  • CRT
  • Particle grid
  • Pixel sorting

Utility Exports

  • createRuntimeClock
  • advanceRuntimeClock
  • buildRuntimeFrame
  • evaluateTimelineForLayers
  • resolveEvaluatedLayers
  • Runtime config and timeline types

Notes

  • ShaderLabComposition preserves aspect ratio only when config.composition is provided, and otherwise fills the size defined by your layout
  • useShaderLab is the recommended entry point for most app integrations
  • If you only need direct composition rendering in the DOM, use ShaderLabComposition
  • If you need full manual control, use the lower-level source APIs
  • Texture output is portable because it is backed by a managed canvas
  • Postprocessing currently requires a shared THREE.WebGPURenderer
  • If you use React Three Fiber with [email protected], you may see a THREE.Clock deprecation warning from @react-three/fiber; that warning is upstream and not emitted by this package

版本与发布记录

@basementstudio/shader-lab 共发布 30 个版本,首次发布于 2026/03/28;过去 12 个月发布了 30 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
3.1.0latest2026/10/1424 天前
3.0.22026/09/012 个月前
3.0.12026/08/242 个月前
3.0.02026/08/202 个月前
2.1.02026/08/192 个月前
2.0.02026/08/122 个月前

关于 @basementstudio/shader-lab 的常见问题

@basementstudio/shader-lab 是什么?

Portable React runtime for Shader Lab exports.

如何安装 @basementstudio/shader-lab?

在项目目录运行 npm install @basementstudio/shader-lab,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(^19.0.0)、three(^0.185.0)、react-dom(^19.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@basementstudio/shader-lab 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@basementstudio/shader-lab 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@basementstudio/shader-lab 可以商用吗?

@basementstudio/shader-lab 使用 未声明 许可。package.json 没有声明许可证。没有许可证的代码默认保留全部权利,商用前请联系作者或查看仓库中的 LICENSE 文件。以上为概括,不构成法律意见,请以许可原文为准。

@basementstudio/shader-lab 的下载量有多少?

根据 npm 官方下载统计,@basementstudio/shader-lab 在 2026/10/08 – 2026/11/06 这 30 天内被下载 2,828 次,日均约 94 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@basementstudio/shader-lab 还在维护吗?

最新版本 3.1.0 发布于 2026/10/14(24 天前);至今共发布 30 个版本,其中过去 12 个月发布了 30 个;首次发布于 2026/03/28。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。