Skip to content

Latest commit

 

History

History
425 lines (357 loc) · 11 KB

README.md

File metadata and controls

425 lines (357 loc) · 11 KB

🚦 signal-gl examples

Overview

Hello World [playground]

How to import uniforms and attributes into a glsl-shader.

hello_world.mov
const [opacity, setOpacity] = createSignal(0.5)
const vertices = new Float32Array([
    -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0,
  ])

const fragment = glsl`#version 300 es
  precision mediump float;
  in vec2 v_coord; 
  out vec4 outColor;
  void main() {
    float opacity = ${uniform.float(opacity)};
    outColor = vec4(v_coord[0], v_coord[1], v_coord[0], opacity);
  }`

const vertex = glsl`#version 300 es
  out vec2 v_coord;  
  out vec3 v_color;
  void main() {
    vec2 a_coord = ${attribute.vec2(vertices)};
    v_coord = a_coord;
    gl_Position = vec4(a_coord, 0, 1) ;
  }`

return (
  <Stack onMouseMove={(e) => setOpacity(e.clientY / e.currentTarget.offsetHeight)}>
    <Program fragment={fragment} vertex={vertex} mode="TRIANGLES" count={vertices.length / 2} />
  </Stack>
)

Scoped Variable Names and Modules [playground]

How to compose shader snippets into a single shader.

scope_and_modules.mov
const [cursor, setCursor] = createSignal<[number, number]>([1, 1]);
const [colors, setColors] = createSignal(
  new Float32Array(new Array(6 * 3).fill("").map((v) => Math.random())),
  { equals: false },
);
const vertices = new Float32Array([
  -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0,
]);

setInterval(() => {
  setColors((colors) => {
    colors[0] += 0.001;
    colors[0] = colors[0] % 1;
    colors[10] += 0.002;
    colors[10] = colors[0] % 1;
    return colors;
  });
});

const module = glsl`

// variable names can be scoped by interpolating strings: ${"string"}
// useful in glsl-module to prevent name collisions
float ${"getLength"}(float x, float y){ return length(x - y); }

vec4 getColor(vec3 color, vec2 coord){
  vec2 cursor = ${uniform.vec2(cursor)};
  if(
    ${"getLength"}(cursor.x, coord.x) < 0.25 && 
    ${"getLength"}(cursor.y, coord.y) < 0.25
  ){
    return vec4(1. - color, 1.0);
  }
  return vec4(color, 1.0);
}`;

const fragment = glsl`#version 300 es
precision mediump float;

// compose shaders with interpolation
// the interpolated shader-snippet is inlined completely
// so be aware for name-collisions!
${module}

in vec2 v_coord; 
in vec3 v_color;
out vec4 outColor;

void main() {
  // getColor is imported from module
  outColor = getColor(v_color, v_coord);
}`;

const vertex = glsl`#version 300 es

out vec2 v_coord;  
out vec3 v_color;

void main() {
  vec2 a_coord = ${attribute.vec2(vertices)};
  v_color = ${attribute.vec3(colors)};
  v_coord = a_coord - ${uniform.vec2(cursor)};
  gl_Position = vec4(a_coord, 0, 1) ;
}`;

const onMouseMove = (e) => {
  const x = e.clientX / e.currentTarget.clientWidth - 0.5;
  const y =
    (e.currentTarget.clientHeight - e.clientY) /
      e.currentTarget.clientHeight -
    0.5;
  setCursor([x, y]);
};

return (
  <Stack style={{ width: "100vw", height: "100vh" }} onMouseMove={onMouseMove}>
    <Program fragment={fragment} vertex={vertex} mode="TRIANGLES" count={vertices.length / 2} />
  </Stack>
);

Multiple shaders [playground]

How to render multiple shaders into a single image.

multiple-shaders.mov
const [opacity, setOpacity] = createSignal(0.5)
const [cursor, setCursor] = createSignal<[number, number]>([1, 1])

const Plane = (props: {
  vertices: Buffer | Accessor<Buffer>
  fragment: Accessor<ShaderToken>
}) => {
  const vertex = glsl`#version 300 es
    out vec2 v_coord;  
    out vec3 v_color;
    void main() {
      vec2 a_coord = ${attribute.vec2(props.vertices)};
      v_coord = a_coord;
      gl_Position = vec4(a_coord, 0, 1.0);
    }`

  return <Program vertex={vertex} fragment={props.fragment} mode="TRIANGLES" count={vertices.length / 2} />
}

const getColor = glsl`
  float ${'getLength'}(float x, float y){
    return length(x - y);
  }

  vec4 getColor(vec3 color, vec2 coord){
    vec2 cursor = ${uniform.vec2(cursor)};

    float lengthX = ${'getLength'}(cursor.x, coord.x);
    float lengthY = ${'getLength'}(cursor.y, coord.y);

    if(lengthX < 0.25 && lengthY < 0.25){
      return vec4(1. - color, 1.0);
    }else{
      discard;
    }
  }`

return (
  <Stack
    onMouseMove={(e) => {
      setOpacity(1 - e.clientY / e.currentTarget.offsetHeight)
      setCursor([
        2 * (e.clientX / e.currentTarget.clientWidth) - 1,
        2 *
          ((e.currentTarget.clientHeight - e.clientY) /
            e.currentTarget.clientHeight) -
          1,
      ])
    }}
  >
    <Plane
      fragment={glsl`#version 300 es
        precision mediump float;
        in vec2 v_coord; 
        out vec4 outColor;
        void main() {
          float opacity = ${uniform.float(opacity)};
          outColor = vec4(v_coord[0], v_coord[1], v_coord[0], opacity);
        }`}
      vertices={
        new Float32Array([
          -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0,
        ])
      }
    />
    <Plane
      fragment={glsl`#version 300 es
        precision mediump float;
        ${getColor}

        in vec2 v_coord; 
        out vec4 outColor;

        void main() {
          outColor = getColor(vec3(1.0, 0.0, 0.0), v_coord);
        }`}
      vertices={
        new Float32Array([
          -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, -0.5, 0.5, 0.5, -0.5, 0.5,
        ])
      }z
    />
  </Stack>
)

Caching Shaders [playground]

When cacheEnabled is set to true, <Program/> will check if the given fragment/vertex glsl tag template literal has already been used to produce a webgl-program and, if it exists, will use this program instead of compiling a new one. Currently this functionality is marked as unstable since it does not yet work nicely with composing shaders from snippets

caching-shaders.mp4
const Plane = (props: {d
  fragment: Accessor<ShaderToken>
  rotation?: number
  scale?: [number, number]
  position?: [number, number]
}) => {
  const vertex = glsl`#version 300 es
    out vec2 v_coord;  
    out vec3 v_color;
    void main() {
      vec2 a_coord = ${attribute.vec2(planeVertices)};
      float rotation =  ${uniform.float(() => props.rotation || 0)};
      vec2 scale =  ${uniform.vec2(() => props.scale || [1, 1])};
      vec2 translation = ${uniform.vec2(() => props.position || [0, 0])};

      // Scaling
      mat3 scaleMatrix = mat3(
          scale.x, 0, 0,
          0, scale.y, 0,
          0, 0, 1
      );

      // Convert angle to radians
      float angle = radians(rotation);
      float c = cos(angle);
      float s = sin(angle);

      // Rotation
      mat3 rotateMatrix = mat3(
          c, -s, 0,
          s, c, 0,
          0, 0, 1
      );

      // Combine transformations
      mat3 transformMatrix = rotateMatrix * scaleMatrix;

      // Apply the transformation
      a_coord = (transformMatrix * vec3(a_coord, 1.0)).xy;
      v_coord = a_coord;
      gl_Position = vec4(a_coord + translation, 1.0, 1.0);
    }`

  return (
    <Program
      cacheEnabled
      fragment={props.fragment}
      vertex={vertex}
      mode="TRIANGLES"
      count={vertices.length / 2}
    />
  )
}

type Boid = {
  x: number,   y: number,  z: number
  vx: number, vy: number, vz: number
}

function updateBoids(boids: Boid[], width = 200, height = 200, deltaTime = 1) {
  for (let i = 0; i < boids.length; i++) {
    let { x, y, z, vx, vy, vz } = boids[i]!
    x += vx * deltaTime
    y += vy * deltaTime
    z += vz * deltaTime
    // Wrap around edges
    x = (x + width) % width
    y = (y + height) % height
    boids[i] = { x, y, z, vx, vy, vz }
  }
}

const AMOUNT = 20000

const [boids, setBoids] = createSignal<
  {
    x: number
    y: number
    z: number
    vx: number
    vy: number
    vz: number
  }[]
>(
  new Array(AMOUNT).fill('').map(() => ({
    x: Math.random() * 200 - 50,
    y: Math.random() * 200 - 50,
    z: Math.random() * 200 - 50,
    vx: Math.random() - 0.5,
    vy: Math.random() - 0.5,
    vz: Math.random() - 0.5,
  })),
  { equals: false }
)

const loop = () => {
  requestAnimationFrame(loop)
  updateBoids(boids())
  batch(() => setBoids((boids) => boids))
}
loop()

const fragment = (blue: number) => glsl`#version 300 es
  precision mediump float;
  in vec2 v_coord; 
  out vec4 outColor;
  void main() {
    float blue = ${uniform.float(blue)};
    outColor = vec4(v_coord[0], 0.0, blue, 0.25);
  }`

return (
  <Stack onProgramCreate={() => console.log('created a program')}>
    <Index each={boids()}>
      {(boid, index) => {
        return (
          <Plane
            fragment={fragment(0.5 - index / untrack(() => boids().length))}
            scale={[0.0125, 0.0125]}
            position={[boid().x / 100 - 1, boid().y / 100 - 1]}
          />
        )
      }}
    </Index>
  </Stack>
)

Vanilla

create a webgl-animation without JSX-wrappers

const [opacity, setOpacity] = createSignal(0.5)

const vertices = new Float32Array([
  -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0,
])

const fragment = glsl`#version 300 es
  precision mediump float;
  in vec2 v_coord; 
  out vec4 outColor;
  void main() {
    float opacity = ${uniform.float(opacity)};
    outColor = vec4(v_coord[0], v_coord[1], v_coord[0], opacity);
  }`

const vertex = glsl`#version 300 es
  out vec2 v_coord;  
  out vec3 v_color;
  void main() {
    vec2 a_coord = ${attribute.vec2(vertices)};
    v_coord = a_coord;
    gl_Position = vec4(a_coord, 0, 1) ;
  }`

let canvas: HTMLCanvasElement

onMount(() => {
  const program = createProgram({
    canvas,
    vertex: vertex(),
    fragment: fragment(),
    mode: 'TRIANGLES',
  })

  const stack = createStack({
    canvas,
    programs: [ program ],
  })

  createEffect(gl.render) 
  // or creatEffect(program.render)
})

return (
  <canvas
    ref={canvas!}
    onMouseMove={(e) =>
      setOpacity(1 - e.clientY / e.currentTarget.offsetHeight)
    }
  />
)