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 variablegl
- Create a canvas element and assign it to the variable
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 variabledpr
- Define a function named
resize
to adjust the size and viewport of the canvas when the window size changes
- Use
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
- Define a function named
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
- Define the vertex coordinate array
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 themouse
object, which is used to update the mouse coordinates and touch point collection - Define the
remove
method of themouse
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.
- Define an object named
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; ix: 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! !