More Articles
RSS

Mar 2023

WebGL shaders

Here I explore integrating WebGL fragment shaders into a no-dependancy project. The variation of these community-made shaders is impressive mathematically and visually.

<html>
  <head>
    <meta charset="utf-8">
    <style>
      * { box-sizing: border-box; }
      html,body { margin: 0; background: #000; overflow: hidden; height: 100%; }
      #root { width: 100%; height: 350px; }
      canvas { width: 100%; height: 100%; object-fit: cover; image-rendering: pixelated; }
      .toggles { position: absolute; left: 25px; bottom: 30px; }
      .toggles button { margin: 0 5px; }
    </style>

    <!-- Default shaders -->
    <script id="fragmentShader" type="x-shader/x-fragment">
      precision highp float;
      uniform vec2 resolution;
      uniform sampler2D texture;
      void main() {
        vec2 uv = gl_FragCoord.xy / resolution.xy;
        gl_FragColor = texture2D(texture, uv);
      }
    </script>
    <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec3 position;
      void main() {
        gl_Position = vec4(position, 1.0);
      }
    </script>
    <script id="surfaceVertexShader" type="x-shader/x-vertex">
      attribute vec3 position;
      attribute vec2 surfacePosAttrib;
      varying vec2 surfacePosition;
      void main() {
        surfacePosition = surfacePosAttrib;
        gl_Position = vec4(position, 1.0);
      }
    </script>

    <script>
    const createNode = (options) => {
      const node = document.createElement(options.tag);
      if(options.className) node.setAttribute('class',options.className);
      if(options.innerHTML) node.innerHTML = options.innerHTML;
      if(options.attributes) Object.keys(options.attributes).forEach(key => node.setAttribute(key,options.attributes[key]));
      if(options.style) Object.keys(options.style).forEach(key => node.style[key] = options.style[key]);
      if(options.event_listeners) Object.keys(options.event_listeners).forEach(key => node.addEventListener(key,options.event_listeners[key]) )
      if(options.root) options.root.appendChild(node);
      return node;
    }
    class App {
      constructor(){
        document.addEventListener('DOMContentLoaded',this.handleReady.bind(this));
        window.addEventListener('resize',this.handleResize.bind(this));
        this.resize_timeout = null;
        this.active_shader = 'noise';
        this.quality = 2
        this.gl = null;
        this.buffer = null;
        this.currentProgram = null;
        this.screenProgram = null;
        this.vertexPosition = null;
        this.screenVertexPosition = null;
        this.startTime = Date.now();
        this.time = 0;
        this.screenWidth = 0;
        this.screenHeight = 0;
        this.surface = { centerX: 0, centerY: 0, width: 1, height: 1 },
        this.frontTarget = null;
        this.backTarget = null;
      }
      setSurfaceSize(){
        this.els.canvas.width = window.innerWidth / this.quality;
        this.els.canvas.height = window.innerHeight / this.quality;
        this.screenWidth = this.els.canvas.width;
        this.screenHeight = this.els.canvas.height;
        // Compute Surface Corners
        this.surface.width = this.surface.height * this.screenWidth / this.screenHeight;
        const halfWidth = this.surface.width * 0.5;
        const halfHeight = this.surface.height * 0.5;
        this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.surface.buffer);
        this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array([
          this.surface.centerX - halfWidth, this.surface.centerY - halfHeight,
          this.surface.centerX + halfWidth, this.surface.centerY - halfHeight,
          this.surface.centerX - halfWidth, this.surface.centerY + halfHeight,
          this.surface.centerX + halfWidth, this.surface.centerY - halfHeight,
          this.surface.centerX + halfWidth, this.surface.centerY + halfHeight,
          this.surface.centerX - halfWidth, this.surface.centerY + halfHeight]), this.gl.STATIC_DRAW);
        this.gl.viewport(0, 0, this.els.canvas.width, this.els.canvas.height);
        this.frontTarget = this.createTarget(this.screenWidth, this.screenHeight);
        this.backTarget = this.createTarget(this.screenWidth, this.screenHeight);
      }
      createTarget(width,height){
        var target = {};
        target.framebuffer = this.gl.createFramebuffer();
        target.renderbuffer = this.gl.createRenderbuffer();
        target.texture = this.gl.createTexture();
        // set up framebuffer
        this.gl.bindTexture(this.gl.TEXTURE_2D, target.texture);
        this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, width, height, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, null);
        this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
        this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
        this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
        this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
        this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, target.framebuffer);
        this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.TEXTURE_2D, target.texture, 0);
        // set up renderbuffer
        this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, target.renderbuffer);
        this.gl.renderbufferStorage(this.gl.RENDERBUFFER, this.gl.DEPTH_COMPONENT16, width, height);
        this.gl.framebufferRenderbuffer(this.gl.FRAMEBUFFER, this.gl.DEPTH_ATTACHMENT, this.gl.RENDERBUFFER, target.renderbuffer);
        // clean up
        this.gl.bindTexture(this.gl.TEXTURE_2D, null);
        this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, null);
        this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
        return target;
      }
      createShader(src,type){
        const shader = this.gl.createShader(type);
        this.gl.shaderSource(shader,src);
        this.gl.compileShader(shader);
        return shader;
      }
      cacheUniformLocation(program, label) {
        if (program.uniformsCache === undefined) program.uniformsCache = {};
        program.uniformsCache[label] = this.gl.getUniformLocation(program, label);
      }
      async fetchShader(name){
        const response = await fetch(name);
        return await response.text();
      }
      handleResize(event){
        if(this.resize_timeout) clearTimeout(this.resize_timeout);
        this.resize_timeout = setTimeout(() => {
          this.setSurfaceSize();
        },50);
      }
      async handleReady(){
        if(this.interval) cancelAnimationFrame(this.interval);

        this.default_shaders = {
          fragment: document.getElementById('fragmentShader').textContent,
          vertex: document.getElementById('vertexShader').textContent,
          surface: document.getElementById('surfaceVertexShader').textContent,
        }
        this.custom_shaders = {
          noise: await this.fetchShader('shader-noise.frag'),
          brains: await this.fetchShader('shader-brains.frag'),
          clouds: await this.fetchShader('shader-clouds.frag'),
          waves: await this.fetchShader('shader-waves.frag'),
        }
        this.els = {};
        this.els.root = document.getElementById('root');
        this.els.root.innerHTML = ``;
        this.els.buttons = createNode({ root: this.els.root, tag: `div`, className: `toggles` });
        for(const key of Object.keys(this.custom_shaders) ){
          const button = createNode({ root: this.els.buttons, tag: `button`, innerHTML: key, event_listeners: { click: event => {
            if(this.active_shader !== key){
              this.active_shader = key;
              this.handleReady();
            }
          } } })
        }
        // Initialize webgl
        this.els.canvas = createNode({ root: this.els.root, tag: `canvas` })
        this.gl = this.els.canvas.getContext('webgl', { antialias: false, depth: false, stencil: false, premultipliedAlpha: false, preserveDrawingBuffer: true });
        this.gl.getExtension('OES_standard_derivatives');
        this.buffer = this.gl.createBuffer();
        this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
        this.gl.bufferData(this.gl.ARRAY_BUFFER, 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]), this.gl.STATIC_DRAW);
        this.surface.buffer = this.gl.createBuffer();
        this.setSurfaceSize();
        // Current Program
        this.currentProgram = this.gl.createProgram(); // if (this.currentProgram) this.gl.deleteProgram(this.currentProgram);
        const current_vertex_shader = this.createShader(this.default_shaders.surface, this.gl.VERTEX_SHADER);
        const current_fragment_shader = this.createShader(this.custom_shaders[this.active_shader], this.gl.FRAGMENT_SHADER);
        this.gl.attachShader(this.currentProgram, current_vertex_shader);
        this.gl.attachShader(this.currentProgram, current_fragment_shader);
        this.gl.deleteShader(current_vertex_shader);
        this.gl.deleteShader(current_fragment_shader);
        this.gl.linkProgram(this.currentProgram);
        this.cacheUniformLocation(this.currentProgram,'time');
        this.cacheUniformLocation(this.currentProgram,'mouse');
        this.cacheUniformLocation(this.currentProgram,'resolution');
        this.cacheUniformLocation(this.currentProgram,'backbuffer');
        this.cacheUniformLocation(this.currentProgram,'surfaceSize');
        this.gl.useProgram(this.currentProgram);
        // initialize and enable the vertex attribute arrays
        this.surface.positionAttribute = this.gl.getAttribLocation(this.currentProgram, 'surfacePosAttrib');
        this.gl.enableVertexAttribArray(this.surface.positionAttribute);
        this.vertexPosition = this.gl.getAttribLocation(this.currentProgram, 'position');
        this.gl.enableVertexAttribArray(this.vertexPosition);
        // Screen Program
        this.screenProgram = this.gl.createProgram();
        var screen_vertex_shader = this.createShader(this.default_shaders.vertex, this.gl.VERTEX_SHADER);
        var screen_fragment_shader = this.createShader(this.default_shaders.fragment, this.gl.FRAGMENT_SHADER);
        this.gl.attachShader(this.screenProgram, screen_vertex_shader);
        this.gl.attachShader(this.screenProgram, screen_fragment_shader);
        this.gl.deleteShader(screen_vertex_shader);
        this.gl.deleteShader(screen_fragment_shader);
        this.gl.linkProgram(this.screenProgram);
        this.gl.useProgram(this.screenProgram);
        this.cacheUniformLocation(this.screenProgram,'resolution');
        this.cacheUniformLocation(this.screenProgram,'texture');
        this.screenVertexPosition = this.gl.getAttribLocation(this.screenProgram,'position');
        this.gl.enableVertexAttribArray(this.screenVertexPosition);
        this.interval = window.requestAnimationFrame(this.render.bind(this));
      }
      async render(){
        // Set uniforms for custom shader
        this.time = Date.now() - this.startTime;
        this.gl.useProgram(this.currentProgram);
        this.gl.uniform1f(this.currentProgram.uniformsCache['time'], this.time / 1000);
        this.gl.uniform2f(this.currentProgram.uniformsCache['resolution'], this.screenWidth, this.screenHeight);
        this.gl.uniform1i(this.currentProgram.uniformsCache['backbuffer'], 0);
        this.gl.uniform2f(this.currentProgram.uniformsCache['surfaceSize'], this.surface.width, this.surface.height);
        this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.surface.buffer);
        this.gl.vertexAttribPointer(this.surface.positionAttribute, 2, this.gl.FLOAT, false, 0, 0);
        this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
        this.gl.vertexAttribPointer(this.vertexPosition, 2, this.gl.FLOAT, false, 0, 0);
        this.gl.activeTexture(this.gl.TEXTURE0);
        this.gl.bindTexture(this.gl.TEXTURE_2D, this.backTarget.texture);
        // Render custom shader to front buffer
        this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.frontTarget.framebuffer);
        this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
        this.gl.drawArrays(this.gl.TRIANGLES, 0, 6);
        // Set uniforms for screen shader
        this.gl.useProgram(this.screenProgram);
        this.gl.uniform2f(this.screenProgram.uniformsCache['resolution'], this.screenWidth, this.screenHeight);
        this.gl.uniform1i(this.screenProgram.uniformsCache['texture'], 1);
        this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
        this.gl.vertexAttribPointer(this.screenVertexPosition, 2, this.gl.FLOAT, false, 0, 0);
        this.gl.activeTexture(this.gl.TEXTURE1);
        this.gl.bindTexture(this.gl.TEXTURE_2D, this.frontTarget.texture);
        // Render front buffer to screen
        this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
        this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
        this.gl.drawArrays(this.gl.TRIANGLES, 0, 6);
        // Swap buffers
        var tmp = this.frontTarget;
        this.frontTarget = this.backTarget;
        this.backTarget = tmp;
        // rAF
        this.interval = window.requestAnimationFrame(this.render.bind(this));
      }
    }
    const app = new App();
    </script>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

I recently became aware of frament shaders exploring the three.js scene, and was heavily inspired by GLSL Sandbox (made by the creator of three.js) and Shadertoy. How they work (I think) is by using a mathematical formula to alter the same buffer of pixel data.

Disclaimer, almost all of this code is not mine. I first picked a few of my fav shaders and created them as filename.frag files. I then copied public code used for glslsandbox and cleared it out but all the only the essential pieces of code and tidied up the logic into my usual style of coding. Even so, you can see the amount of methods to integrate it is quite a lot.

The above example reads in the frag files as strings and applies them to a webgl canvas. The glslsandbox code includes boilerplate vertex and fragment shaders needed to apply the custom variants. Using this boilerplate code as a foundation, the custom shaders can be easily swapped out.

Project Assets

Asset File Description
shader-noise.frag Download Noise fragment shader
shader-brains.frag Download Brains fragment shader
shader-clouds.frag Download Clouds fragment shader
shader-waves.frag Download Waves fragment shader