Docs

Shaders

SkSL runtime effects on cached nodes. Include the common prelude or wrap the body with withCommonShader. Not GLSL.

View as Markdown

Shaders are SkSL runtime effects. They are not GLSL. Do not write #version 300 es, void main(), sampler2D, texture(), or outColor.

The entry point is half4 main(float2 coord).

Cached node

Put cache on the node that owns the shader. Children render into a texture. The fragment samples that texture. Use cachePadding if the effect bleeds past the box.

<Rect
  cache
  cachePadding={48}
  width={() => this.frameWidth()}
  height={() => this.frameHeight()}
  shaders={{
    fragment: GLASS_SKSL,
    uniforms: {
      uRefraction: () => this.refraction(),
    },
  }}
>
  <Img
    src={() => this.image()}
    width={() => this.frameWidth()}
    height={() => this.frameHeight()}
  />
</Rect>

The image is a child of the cached rect. It is an asset input, not a second timeline clip. See Inputs.

Prelude

Every useful fragment needs sourceTexture, sampleSource, and computeSourceUV. Two equivalent ways to get them:

Include at the top of the inline fragment:

#include "@vidova/core/shaders/common.sksl"
 
half4 main(float2 coord) {
    float2 uv = computeSourceUV(coord);
    return sampleSource(uv);
}

Or wrap the body:

import { withCommonShader } from '@vidova/core';
 
const PASS_THROUGH = withCommonShader(`
half4 main(float2 coord) {
    float2 uv = computeSourceUV(coord);
    return sampleSource(uv);
}
`);

Custom components are a single .tsx file. Vidova expands that include before the runtime effect compiles. You do not import a separate .sksl file.

Helpers and uniforms

From the prelude:

NameRole
sampleSource(uv)Sample the cached node. uv is 0–1
computeSourceUV(coord)Convert fragment coord to source UV
sourceTextureThe cached children
sourceSize, sourceTexelSizeCache size in pixels
resolutionNode size
time, deltaTime, framerate, framePlayback clock

System uniforms are injected. You still declare them by including the prelude (or withCommonShader). Do not paste a fake GLSL preamble.

Destination sampling (what's already on screen) only happens if the fragment also declares uniform shader destinationTexture. Most image effects sample source only.

Custom uniforms

Keys on shaders.uniforms become SkSL uniform names. Signals are allowed.

TypeScriptSkSL
numberfloat
[number, number]float2
[number, number, number]float3
[number, number, number, number]float4
shaders={{
  fragment: GLASS_SKSL,
  uniforms: {
    uRefraction: () => this.refraction(),
    uFrost: () => this.frost(),
  },
}}
uniform float uRefraction;
uniform float uFrost;

Pass-through

import { withCommonShader } from '@vidova/core';
 
const PASS_THROUGH = withCommonShader(`
half4 main(float2 coord) {
    return sampleSource(computeSourceUV(coord));
}
`);

Glass on an image input

Declare an image asset input. Keep @initial('') on the signal. Wrap <Img src={this.image} /> in a cached Rect and run SkSL on that cache.

import { Node, NodeProps, Img, Rect, signal, initial } from '@vidova/2d';
import { SignalValue, SimpleSignal, withCommonShader } from '@vidova/core';
 
const GLASS_SKSL = withCommonShader(`
uniform float uRefraction;
 
half4 main(float2 coord) {
    float2 uv = computeSourceUV(coord);
    float2 warped = uv + (uv - 0.5) * uRefraction;
    return sampleSource(warped);
}
`);
 
export interface GlassImageProps extends NodeProps {
  image?: SignalValue<string>;
  refraction?: SignalValue<number>;
  frameWidth?: SignalValue<number>;
  frameHeight?: SignalValue<number>;
}
 
export class GlassImage extends Node {
  @initial('')
  @signal()
  public declare readonly image: SimpleSignal<string, this>;
 
  @initial(0.04)
  @signal()
  public declare readonly refraction: SimpleSignal<number, this>;
 
  @initial(1600)
  @signal()
  public declare readonly frameWidth: SimpleSignal<number, this>;
 
  @initial(900)
  @signal()
  public declare readonly frameHeight: SimpleSignal<number, this>;
 
  public constructor(props?: GlassImageProps) {
    super({ ...props });
    this.add(
      <Rect
        cache
        cachePadding={32}
        width={() => this.frameWidth()}
        height={() => this.frameHeight()}
        shaders={{
          fragment: GLASS_SKSL,
          uniforms: { uRefraction: () => this.refraction() },
        }}
      >
        <Img
          src={() => this.image()}
          width={() => this.frameWidth()}
          height={() => this.frameHeight()}
        />
      </Rect>,
    );
  }
}

inputDefs for that class:

[
  { "name": "image", "type": "asset", "default": "", "assetTypes": ["image"] },
  { "name": "refraction", "type": "number", "default": 0.04 }
]

Place only the component clip. Pass the image asset ID in componentInputs.image. Do not add a separate image clip underneath.

Failures

What you wroteWhat happens
#version 300 es / void main() / sampler2D / outColorCompile error: shaders are SkSL
#include "local.sksl"Compile error: only the common prelude expands
Shader on a node without cacheEffect is skipped or samples the wrong buffer
Asset UUID in @initial or <Img src>Img.src received asset ID — use @initial('')
Extra image clip on the timelineThe component is unused; the still is just a media clip

A create or edit that reports FAILED TO RENDER is broken. Fix the SkSL before you place the clip. Thumbnail render compiles shaders.

Layout of stacked chrome around the image: Layout. Wiring inputDefs: Inputs.