Home

Add Shaders

30 December 2020

One method I often use during development is to design the interface first, and implement it afterwards. This works for me most of the time :)

When I first got the idea to add shaders to css-doodle, I started with what it might look like:

background: @shaders(
  /* code */
);

Later I realized there are two kinds of shaders, vertex shader and fragment shader. They're usually separated in different <script> tags or JavaScript variables.

In order to put them into single @shader() together, I borrowed the idea of the CSS syntax, that is, to group them with the syntax of CSS selectors.

background: @shaders(
  fragment {
    /* code */
  }
  vertex {
    /* code */
  }
);

This also extends well while adding textures.

background: @shaders(
  fragment { }
  vertex { }
  texture { }
);

Multiple textures can be identified by their prefixes.

background: @shaders(
  fragment { }
  vertex { }
  /* multiple textures */
  texture { }
  texture_1 { }
);

If there's no fragment group I'll treat the whole as fragment shader, which makes the code much simpler.

background: @shaders(
  void main() {
    /* code */
  }
);

Code inside Custom Properties

The length of the shader code increases easily so it's a good idea to place it to somewhere else. Using CSS Custom properties is an option.

--shaders: @shaders(
  void main() {
    /* code */
  }
);

background: var(--shaders);

Since css-doodle supports writing rules in ordinary CSS files, the code can be breakdown into smaller parts and join them together with var().

<style>
  css-doodle {
    --texture: (
      /* code */
    );
    --fragment: (
      /* code */
    );
    --rule: (
      background: @shaders(
        texture { var(--texture) }
        fragment { var(--fragment) }
      );
    );
  }
</style>
<css-doodle use="var(--rule)"></css-doodle>

Pitfalls

However, the values of custom property will be serialized by the CSS parser. All the numbers like 3.0 will be transformed to integer 3, which makes the shader code fails to compile.

css-doodle {
  --fragment: (
    void main() {
      float number = 3.0; /* won't work */
    }
  );
}

A workaround is to write 3. instead of 3.0.

css-doodle {
  --fragment: (
    void main() {
      float number = 3.; /* ok */
    }
  );
}

Implementation

The implementation is quite straightforward. Parse code inside @shader() to read fragment/vertex shader and textures. Transform textures into images before passing them to WebGL. Handle shader compiling and linking. Output the canvas as image data in the end.

Examples

So there's a basic example, fill the color of each pixel based on its coordinates.

@grid: 1 / 180px;
background: @shaders(
  void main() {
    vec2 p = gl_FragCoord.xy / u_resolution.xy;
    gl_FragColor = vec4(p.xy, .8, 1.);
  }
);
@grid: 1 / 180px; background: @shaders( void main() { vec2 p = gl_FragCoord.xy / u_resolution.xy; gl_FragColor = vec4(p.xy, .8, 1.); } );

Make texture with css-doodle and load it with fragment shader.

@grid: 1 / 180px;
background: @shaders(
  texture_0 {
    @grid: 1 / 100%;
    background: linear-gradient(45deg, @stripe.@m20.@p(
      #d62828, #fcbf49, #eae2b7, #000
    ));
  }
  fragment {
    void main() {
      vec2 uv = gl_FragCoord.xy / u_resolution.xy;
      gl_FragColor = texture2D(texture_0, uv);
    }
  }
);
@grid: 1 / 180px; background: @shaders( texture_0 { @grid: 1 / 100%; background: linear-gradient( 45deg, @stripe.@m20.@p(#d62828, #fcbf49, #eae2b7, #000) ); } fragment { void main() { vec2 uv = gl_FragCoord.xy / u_resolution.xy; gl_FragColor = texture2D(texture_0, uv); } } );

Now, I can finally make something that can't do with CSS and SVG.

@grid: 1 / 180px;
background: @shaders(
  texture_0 {
    /* ... */
  }
  fragment {
    void main() {
      vec2 p = gl_FragCoord.xy/u_resolution.xy;
      vec2 c = vec2(.5, .5);
      vec2 uv = p.xy - c;
      float ratio = u_resolution.x/u_resolution.y;
      float len = length(uv * vec2(ratio, 1.));
      float t = atan(uv.y, uv.x) +
        2. * 3.141593 * smoothstep(.5, 0., len);
      float r = length(uv) + .1;
      vec2 coords = vec2(r*cos(t),r*sin(t)) + c;
      gl_FragColor = texture2D(texture_0, coords);
    }
  }
);
@grid: 1 / 180px; background: @shaders( texture_0 { @grid: 1 / 100%; background: linear-gradient( 45deg, @stripe.@m20.@p(#d62828, #fcbf49, #eae2b7, #000) ); } fragment { void main() { vec2 p = gl_FragCoord.xy / u_resolution.xy; vec2 c = vec2(.5, .5); vec2 uv = p.xy - c; float R = 2. * 3.141593; float len = length(uv * vec2(u_resolution.x / u_resolution.y, 1.)); float angle = atan(uv.y, uv.x) + R * smoothstep(.5, 0., len); float r = length(uv) + .1; vec2 coords = vec2(r * cos(angle), r * sin(angle)) + c; gl_FragColor = texture2D(texture_0, coords); } } );

Although there's no animation yet, I'm already excited about it. See also the above demo on CodePen.

Next step

I'll try to add animation by introducing the uniform u_time, but then the whole thing can't be put into background anymore. I really wish the element() function can be supported more widely soon.

Time to learn more GLSL!