Dynamic graphics effects implemented by WebGL2 Shader

Article directory

  • Preface
  • renderings
  • Create canvas and WebGL context
  • Set page title and style
  • Define device pixel ratio and window resizing functions
  • Write source code for vertex shader and fragment shader
  • Compile shader functions and create programs
  • Initialize vertex data and buffers
  • Define mouse event handling objects and functions
  • Complete code
  • Summarize

Foreword

This article will introduce how to use WebGL2 to create a dynamic image effect based on a classic shader. We will write the code in JavaScript and GLSL and pass it to the WebGL context by using a vertex shader and a fragment shader. By studying this example, you will understand some basic WebGL concepts such as shader programming, vertex buffers, and uniform variables.
In this article, we first create a canvas element for rendering and obtain the WebGL context. Then, we set some basic styling and initialization parameters. Next, we write the source code for the vertex shader and fragment shader and compile them into WebGL shader objects. We also create a procedural object and attach the vertex shader and fragment shader to the procedural object and link them.
By using a buffer object, we send the vertex data into the vertex shader and associate it with the vertex shader through attribute variables. We then set some uniform variables to pass to the fragment shader during rendering. Finally, we use the requestAnimationFrame function to loop the rendering function to achieve the animation effect.
We also added mouse and touch event listeners to update mouse coordinates and touch information upon user interaction. This way we can change the image effects in the fragment shader based on the position and amount of mouse and touches.

Renderings

Create canvas and WebGL context

    • Create a canvas element and assign it to the variable canvas
    • Get the WebGL context object through canvas.getContext("webgl2") and assign it to the variable gl
const canvas = document.createElement("canvas")
const gl = canvas.getContext("webgl2")

Set page title and style

    • Set page title to “”
    • Clear the HTML content of the page
    • Add canvas element to body
    • Set the body style to “margin:0;touch-action:none;overflow:hidden;”
    • Set the style of the canvas element so that its width is 100%, its height is adaptive, and it prohibits users from selecting content.
document.title = ""
document.body.innerHTML = ""
document.body.appendChild(canvas)
document.body.style = "margin:0;touch-action:none;overflow:hidden;"
canvas.style.width = "100%"
canvas.style.height = "auto"
canvas.style.userSelect = "none"

Define device pixel ratio and window resizing function

    • Use Math.max(1, .5*window.devicePixelRatio) to calculate the device pixel ratio and assign it to the variable dpr
    • Define a function named resize to adjust the size and viewport of the canvas when the window size changes
const dpr = Math.max(1, .5*window.devicePixelRatio)
function resize() {<!-- -->
  const {<!-- -->
    innerWidth: width,
    innerHeight: height
  } = window

  canvas.width = width * dpr
  canvas.height = height * dpr

  gl.viewport(0, 0, width * dpr, height * dpr)
}
window.onresize = resize

Write vertex shader and fragment shader source code

    • Define the vertex shader source code, written in ES 3.0 version
    • Define fragment shader source code, including comments and some uniform variables and functions
const vertexSource = `#version 300 es
    // Omit some code...
  `

const fragmentSource = `#version 300 es
    // Omit some code...
  `

Compile shader functions and create programs

    • Define a function named compile to compile the shader source code
    • Define a function named setup that creates and links the program object and attaches the shader to the program
function compile(shader, source) {<!-- -->
  gl.shaderSource(shader, source)
  gl.compileShader(shader);

  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {<!-- -->
    console.error(gl.getShaderInfoLog(shader))
  }
}

let program

function setup() {<!-- -->
  const vs = gl.createShader(gl.VERTEX_SHADER)
  const fs = gl.createShader(gl.FRAGMENT_SHADER)

  compile(vs, vertexSource)
  compile(fs, fragmentSource)

  program = gl.createProgram()

  gl.attachShader(program, vs)
  gl.attachShader(program, fs)
  gl.linkProgram(program)

  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {<!-- -->
    console.error(gl.getProgramInfoLog(program))
  }
}

Initialize vertex data and buffer

    • Define the vertex coordinate array vertices, representing the four vertex coordinates of a rectangle
    • Create a buffer object and store vertex data in the buffer
let vertices, buffer

function init() {<!-- -->
  vertices = [
    -1., -1., 1.,
    -1., -1., 1.,
    -1., 1., 1.,
    -1., 1., 1.,
  ]

  buffer = gl.createBuffer()

  gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW)

  const position = gl.getAttribLocation(program, "position")

  gl.enableVertexAttribArray(position)
  gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0)

  program.resolution = gl.getUniformLocation(program, "resolution")
  program.time = gl.getUniformLocation(program, "time")
  program.touch = gl.getUniformLocation(program, "touch")
  program.pointerCount = gl.getUniformLocation(program, "pointerCount")
}

Define mouse event handling objects and functions

    • Define an object named mouse that contains the x, y coordinates of the mouse and a collection of touch points
    • Define the update method of the mouse object, which is used to update the mouse coordinates and touch point collection
    • Define the remove method of the mouse object to remove the touch point
    • Listen to the mouse events of the window and call the corresponding method of the mouse object according to the event type to update the mouse information.
    • Clear the canvas’s color buffer
    • Using program objects for rendering operations
    • Update the value of a uniform variable
    • Draw vertex array
    • Call the setup function to create the program object and compile the shader
    • Call the init function to initialize vertex data and buffers
    • Call the resize function to adjust the size and viewport of the canvas
    • Call the loop function to start the rendering loop
    • When the mouse is pressed, call the mouse.update method to update the mouse information.
    • When the mouse is lifted, call the mouse.remove method to remove the touch point
    • When the mouse moves, if the mouse touch point exists, the mouse.update method is called to update the mouse information.
const mouse = {<!-- -->
  x: 0,
  y: 0,
  touches: new Set(),
  update: function(x, y, pointerId) {<!-- -->
    this.x = x * dpr;
    this.y = (innerHeight - y) * dpr;
    this.touches.add(pointerId)
  },
  remove: function(pointerId) {<!-- --> this.touches.delete(pointerId) }
}

function loop(now) {<!-- -->
  gl.clearColor(0, 0, 0, 1)
  gl.clear(gl.COLOR_BUFFER_BIT)
  gl.useProgram(program)
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
  gl.uniform2f(program.resolution, canvas.width, canvas.height)
  gl.uniform1f(program.time, now * 1e-3)
  gl.uniform2f(program.touch, mouse.x, mouse.y)
  gl.uniform1i(program.pointerCount, mouse.touches.size)
  gl.drawArrays(gl.TRIANGLES, 0, vertices.length * .5)
  requestAnimationFrame(loop)
}

setup()
init()
resize()
loop(0)

window.addEventListener("pointerdown", e => mouse.update(e.clientX, e.clientY, e.pointerId))
window.addEventListener("pointerup", e => mouse.remove(e.pointerId))
window.addEventListener("pointermove", e => {<!-- -->
  if (mouse.touches.has(e.pointerId))
    mouse.update(e.clientX, e.clientY, e.pointerId)
})


Complete code

Explain the following code point by point and title, and the code and title should correspond;
const canvas = document.createElement("canvas")
const gl = canvas.getContext("webgl2")

document.title = ""
document.body.innerHTML = ""
document.body.appendChild(canvas)
document.body.style = "margin:0;touch-action:none;overflow:hidden;"
canvas.style.width = "100%"
canvas.style.height = "auto"
canvas.style.userSelect = "none"

const dpr = Math.max(1, .5*window.devicePixelRatio)

function resize() {
  const {
    innerWidth: width,
    innerHeight: height
  } = window

  canvas.width = width * dpr
  canvas.height = height * dpr

  gl.viewport(0, 0, width * dpr, height * dpr)
}
window.onresize = resize

const vertexSource = `#version 300 es
    #ifdef GL_FRAGMENT_PRECISION_HIGH
    precision highp float;
    #else
    precision mediump float;
    #endif

    in vec4 position;

    void main(void) {
        gl_Position = position;
    }
    `

const fragmentSource = `#version 300 es
    /**********
     * made by Matthias Hurrle (@atzedent)
     *
     * Adaptation of "Quasar" by @kishimisu
     * Source: https://www.shadertoy.com/view/msGyzc
     */
    #ifdef GL_FRAGMENT_PRECISION_HIGH
    precision highp float;
    #else
    precision mediump float;
    #endif

    out vec4 fragColor;

    uniform vec2 resolution;
    uniform float time;
    uniform vec2 touch;
    uniform int pointerCount;

    #define mouse (touch/resolution)
    #define P pointerCount
    #define T (10. + time*.5)
    #define S smoothstep

    #define hue(a) (.6 + .6*cos(6.3*(a) + vec3(0,23,21)))

    mat2 rot(float a) {
        float c = cos(a), s = sin(a);

        return mat2(c, -s, s, c);
    }

    float orbit(vec2 p, float s) {
        return floor(atan(p.x, p.y)*s + .5)/s;
    }

    void cam(inout vec3 p) {
        if (P > 0) {
            p.yz *= rot(mouse.y*acos(-1.) + acos(.0));
            p.xz *= rot(-mouse.x*acos(-1.)*2.);
        }
    }

    void main(void) {
        vec2uv = (
            gl_FragCoord.xy-.5*resolution
        )/min(resolution.x, resolution.y);

        vec3 col = vec3(0), p = vec3(0),
        rd = normalize(vec3(uv, 1));

        cam(p);
        cam(rd);

        const float steps = 30.;
        float dd = .0;

        for (float i=.0; i
  x: 0,
  y: 0,
  touches: new Set(),
  update: function(x, y, pointerId) {
    this.x = x * dpr;
    this.y = (innerHeight - y) * dpr;
    this.touches.add(pointerId)
  },
  remove: function(pointerId) { this.touches.delete(pointerId) }
}

function loop(now) {
  gl.clearColor(0, 0, 0, 1)
  gl.clear(gl.COLOR_BUFFER_BIT)
  gl.useProgram(program)
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
  gl.uniform2f(program.resolution, canvas.width, canvas.height)
  gl.uniform1f(program.time, now * 1e-3)
  gl.uniform2f(program.touch, mouse.x, mouse.y)
  gl.uniform1i(program.pointerCount, mouse.touches.size)
  gl.drawArrays(gl.TRIANGLES, 0, vertices.length * .5)
  requestAnimationFrame(loop)
}

setup()
init()
resize()
loop(0)

window.addEventListener("pointerdown", e => mouse.update(e.clientX, e.clientY, e.pointerId))
window.addEventListener("pointerup", e => mouse.remove(e.pointerId))
window.addEventListener("pointermove", e => {
  if (mouse.touches.has(e.pointerId))
    mouse.update(e.clientX, e.clientY, e.pointerId)
})

Summary

CSS animation effects are the most suitable case for beginners to learn. I hope this article can help you! ! Follow Ruocheng and we will take you to explore the ocean of code! !