Begin to understand native webGL from a mouse drawing point

Time:2022-11-25

WebGL tutorials for the web front-end, the tutorials on the Internet are all based on the assumption of computer graphics, which is not very friendly to web developers, so this pit is opened

final effect

https://codepen.io/chendonmin…

Mouse click draws a point.

How webGL displays a point

First of all, you need to know how webGL displays a point?

webGL draws any object needs avertex shaderandFragment shader,

vertex shader: A program that describes the properties (position, color, etc.) of a vertex.

Fragment shader: A program that performs fragment processing.

Maybe you will be very confused, and a lot of official theories are going to be daunting again, so I will directly show the simplest code to show a point, I believe you will understand immediately.

<canvas id="glcanvas" width="640" height="480">
    Your browser does not seem to support or disable the HTML5 <code><canvas></code> element.
</canvas>

First, I need some simple wrapper functions:

function initShaders(gl, vshader, fshader) {
  var program = createProgram(gl, vshader, fshader);
  if (!program) {
    console.log('Failed to create program');
    return false;
  }

  gl.useProgram(program);
  gl.program = program;

  return true;
}

/**
 * Create the linked program object
 * @param gl GL context
 * @param vshader a vertex shader program (string)
 * @param fshader a fragment shader program (string)
 * @return created program object, or null if the creation has failed
 */
function createProgram(gl, vshader, fshader) {
  // Create shader object
  var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
  var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
  if (!vertexShader || !fragmentShader) {
    return null;
  }

  // Create a program object
  var program = gl.createProgram();
  if (!program) {
    return null;
  }

  // Attach the shader objects
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);

  // Link the program object
  gl.linkProgram(program);

  // Check the result of linking
  var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
  if (!linked) {
    var error = gl.getProgramInfoLog(program);
    console.log('Failed to link program: ' + error);
    gl.deleteProgram(program);
    gl.deleteShader(fragmentShader);
    gl.deleteShader(vertexShader);
    return null;
  }
  return program;
}

/**
 * Create a shader object
 * @param gl GL context
 * @param type the type of the shader object to be created
 * @param source shader program (string)
 * @return created shader object, or null if the creation has failed.
 */
function loadShader(gl, type, source) {
  // Create shader object
  var shader = gl.createShader(type);
  if (shader == null) {
    console.log('unable to create shader');
    return null;
  }

  // Set the shader program
  gl.shaderSource(shader, source);

  // Compile the shader
  gl.compileShader(shader);

  // Check the result of compilation
  var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
  if (!compiled) {
    var error = gl.getShaderInfoLog(shader);
    console.log('Failed to compile shader: ' + error);
    gl.deleteShader(shader);
    return null;
  }

  return shader;
}

This is a function that initializes the shader

Initialize webgl:

const canvas = document.querySelector("#glcanvas");
// Initialize the WebGL context
const gl = canvas.getContext("webgl");

// Confirm WebGL support
if (!gl) {
    alert("Unable to initialize WebGL, your browser, operating system or hardware may not support WebGL.");
    return;
}
// clear all images with fully opaque black
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// clear the buffer with the color specified above
gl.clear(gl.COLOR_BUFFER_BIT);

Call the initialize shader function.

const VSHADER_SOURCE = `
    void main() {
        gl_Position = vec4(0.0 ,0.0 ,0.0 , 1.0);
        gl_PointSize = 10.0;
    }
`;
const FSHADER_SOURCE = `
    void main() {
        gl_FragColor = vec4(1.0 ,0.0 ,0.0 ,1.0);
    }
`;
 initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);
 gl.drawArrays(gl.POINTS, 0, 1);

OK, so far, you should be able to see a red dot in the middle of the black canvas.

analyze

The most crucial part is actuallyVSHADER_SOURCEandFSHADER_SOURCETwo strings, respectively represent the coordinates of the point and the color of the point.

VSHADER_SOURCEandFSHADER_SOURCEbelongs toglslcode,

VSHADER_SOURCEneutralgl_Positionrepresents the position of the point,gl_PositionYesglslbuilt-in variables.

you will findgl_PositionThe value is avec4Type, coordinates actually have 4 values? In fact, these are homogeneous coordinates.

For vec4(x, y, z, w), the real world coordinates are (x/w, y/w, z/w), so we generally set the fourth parameter of vec4 as1

Why do you need homogeneous coordinates, because in the three-dimensional world, vectors are also represented by three coordinates, so in order to distinguish between vectors and real positions, a fourth parameter is introduced, and the fourth parameter of vectors is 0.

js and GLSL communication

The above code does draw a point, but it is written in a string, which is definitely inconvenient for us to operate, so the operationglslThe variables in are necessary.

const VSHADER_SOURCE = `
    attribute vec4 a_Position;
    void main() {
        gl_Position = a_Position;
        gl_PointSize = 10.0;
    }
`;

As shown above, wevertex shaderThe code adds aattribute, and then assign the attribute a_Position to the glsl built-in variable gl_Position. Does this mean that if I change the value of a_Position, gl_Position will also change?

js gets and modifies attribute

Required APIs:

gl.getAttribLocation(gl.program, attribute);
gl.vertexAttrib3f(index, x, y, z);

The getAttribLocation method returns the subscript pointing location of an attribute in a given WebGLProgram object

vertexAttrib3f can assign values ​​to vertex attibute variables

Now just need to be ingl.drawArrays(gl.POINTS, 0, 1);Just modify the attribute before

var a_Position = gl.getAttribLocation(gl.program, "a_Position");
gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0);

So far, the complete code is as follows:

const canvas = document.querySelector("#glcanvas");
    // Initialize the WebGL context
    const gl = canvas.getContext("webgl");

    // Confirm WebGL support
    if (!gl) {
        alert("Unable to initialize WebGL, your browser, operating system or hardware may not support WebGL.");
        return;
    }
    // clear all images with fully opaque black
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    // clear the buffer with the color specified above
    gl.clear(gl.COLOR_BUFFER_BIT);

    const VSHADER_SOURCE = `
        attribute vec4 a_Position;
        void main() {
            gl_Position = a_Position;
            gl_PointSize = 10.0;
        }
    `;
    const FSHADER_SOURCE = `
        void main() {
             gl_FragColor = vec4(1.0 ,0.0 ,0.0 ,1.0);
         }
    `;

  //Initialize the shader
  initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);

  var a_Position = gl.getAttribLocation(gl.program, "a_Position");
  gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0);

  // draw dots
  gl.drawArrays(gl.POINTS, 0, 1);

draw multiple points

gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0);
gl.drawArrays(gl.POINTS, 0, 1);

gl.vertexAttrib3f(a_Position, 0.5, 0.5, 0.0);
gl.drawArrays(gl.POINTS, 0, 1);

You will notice that there are two red dots on the screen.

drawArraysThe method is used to draw primitives from the vector array, and the GPU is notified to render the primitives every time it is executed.

Now two points are fine, but what if there are tens of thousands of points? We need to draw multiple points at once to maintain performance.

Typed Array TypedArray

For multiple points, we need to store the position of the point in a variable, we chose TypedArray,
It has several advantages over ordinary Array: performance performance or tm performance.
fortypedArraySee the following code for introduction:

// The following code is in grammatical format and cannot be run directly.
// The TypedArray keyword needs to be replaced with the constructor function listed at the bottom.
new TypedArray(); // new in ES2017
new TypedArray(length);
new TypedArray(typedArray);
new TypedArray(object);
new TypedArray(buffer [, byteOffset [, length]]);

// TypedArray refers to one of the following:

Int8Array();
Uint8Array();
Uint8ClampedArray();
Int16Array();
Uint16Array();
Int32Array();
Uint32Array();
Float32Array();
Float64Array();

So how to choose, see the following list:

type range of individual element values size (bytes) describe Web IDL type Equivalent types in C
Int8Array -128 to 127 1 8-bit binary signed integer byte int8_t
Uint8Array 0 to 255 1 8-bit unsigned integer (cycle from another bound after out of bounds) octet uint8_t
Uint8ClampedArray 0 to 255 1 8-bit unsigned integer (boundary value after out-of-range) octet uint8_t
Int16Array -32768 to 32767 2 16-bit binary signed integer short int16_t
Uint16Array 0 to 65535 2 16-bit unsigned integer unsigned short uint16_t
Int32Array -2147483648 to 2147483647 4 32-bit binary signed integer long int32_t
Uint32Array 0 to 4294967295 4 32-bit unsigned integer unsigned long uint32_t
Float32Array 1.2×10^-38 to 3.4×10^38 4 32-bit IEEE floating-point number (7 significant digits, such as1.1234567 unrestricted float float
Float64Array 5.0×10^-324 to 1.8×10^308 8 64-bit IEEE floating-point numbers (16 significant digits, such as1.123...15) unrestricted double double
BigInt64Array -2^63 to 2^63-1 8 64-bit binary signed integer bigint int64_t (signed long long)
BigUint64Array 0 to 2^64 - 1 8 64-bit unsigned integer bigint uint64_t (unsigned long long)

For this tutorial, because we will use floating point numbers later, and because the data is not large, chooseFloat32Array.

try to plot two points

  • Store the coordinates of two points in variables

    const verties = new Float32Array([0.0, 0.5, -0.5, -0.5]);
  • Hang the data to a memory location in the buffer and write the data

    const vertexBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
      // verties is the Float32Array data we created ourselves
      gl.bufferData(gl.ARRAY_BUFFER, verties, gl.STATIC_DRAW);
  • Read data and modify attribute

     const a_Position = gl.getAttribLocation(gl.program, "a_Position");
      gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
      gl.enableVertexAttribArray(a_Position);

    To understand the vertexAttribPointer function, you can read this note of mine
    https://note.youdao.com/s/c5E…

  • Modify the attribute, the next step is to call the drawing command

    // Because it is drawing two points, enter 2 for the third parameter
    gl.drawArrays(gl.POINTS, 0, 2);

full code

Remove the initialization webGL and utility functionsinitShaders, because no change is written every time…

  initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);

  const verties = new Float32Array([0.0, 0.5, -0.5, -0.5]);

  const vertexBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, verties, gl.STATIC_DRAW);

  const a_Position = gl.getAttribLocation(gl.program, "a_Position");

  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(a_Position);
  gl.drawArrays(gl.POINTS, 0, 2);

The mouse monitors the coordinates and writes

If the above is understood, the third step is the easiest (for web developers).
The above codes of the specific functions are all realized, only need:

  • Convert screen coordinates to webGL coordinates when clicked
  • Store coordinates into Float32Array data
  • Modify attribute, rendering.

Convert to webGl coordinates

const x = (e.offsetX - 320) / 320;
const y = -(e.offsetY - 240) / 240;

where 320 = 640/2
240 = 480/2

320 represents the width of the canvas element, and 240 represents the height of the canvas element

Store data in Float32Array

First of all, Float32Array is of fixed length and cannot be modified dynamically, so a new one needs to be createdFloat32Array

const newArr = new Float32Array(length + 2)
for (let i = 0; i < arrayBuffer.length; i++) {
    newArr[i] = arrayBuffer[i]
}
newArr[arrayBuffer.length] = x;
newArr[arrayBuffer.length + 1] = y;

final code

The code can be viewed in codePen, if it cannot be opened, I will show the code:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>test</title>
</head>

<body onload="main()">
    <canvas id="glcanvas" width="640" height="480">
        Your browser does not seem to support or disable the HTML5 <code><canvas></code> element.
    </canvas>
</body>
<script src="utils/cuon-utils.js"></script>
<script>
    let arrayBuffer = new Float32Array()
    function main() {
        const canvas = document.querySelector("#glcanvas");
        // Initialize the WebGL context
        const gl = canvas.getContext("webgl");

        // Confirm WebGL support
        if (!gl) {
            alert("Unable to initialize WebGL, your browser, operating system or hardware may not support WebGL.");
            return;
        }
        // clear all images with fully opaque black
        gl.clearColor(0.0, 0.0, 0.0, 1.0);
        // clear the buffer with the color specified above
        gl.clear(gl.COLOR_BUFFER_BIT);

        const VSHADER_SOURCE = `
            attribute vec4 a_Position;
            void main() {
                gl_Position = a_Position;
                gl_PointSize = 10.0;
            }
        `;
        const FSHADER_SOURCE = `
            void main() {
                 gl_FragColor = vec4(1.0 ,0.0 ,0.0 ,1.0);
             }
        `;
        initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);

        // listen to click event
        document.getElementById('glcanvas').addEventListener('mousedown', e => {
            clear(gl);
            // origin coordinates of upper left corner
            const x = (e.offsetX - 320) / 320;
            const y = -(e.offsetY - 240) / 240;
            let length = arrayBuffer.length;
            const newArr = new Float32Array(length + 2)
            for (let i = 0; i < arrayBuffer.length; i++) {
                newArr[i] = arrayBuffer[i]
            }
            newArr[arrayBuffer.length] = x;
            newArr[arrayBuffer.length + 1] = y;
            const len = initVertexBuffer(gl, newArr);
            gl.drawArrays(gl.POINTS, 0, len);
            arrayBuffer = newArr;
        })
    }

    function initVertexBuffer(gl, verties) {
        const vertexBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, verties, gl.STATIC_DRAW);

        const a_Position = gl.getAttribLocation(gl.program, "a_Position");
        const FSIZE = verties.BYTES_PER_ELEMENT
        gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 2 * FSIZE, 0);
        gl.enableVertexAttribArray(a_Position);
        return verties.length / 2;
    }

    function clear(gl) {
        gl.clearColor(0.0, 0.0, 0.0, 1.0);
        gl.clear(gl.COLOR_BUFFER_BIT);
    }
</script>

</html>

incuon-utils.jsIt is a small utility function encapsulated

// cuon-utils.js (c) 2012 kanda and matsuda
/**
 * Create a program object and make current
 * @param gl GL context
 * @param vshader a vertex shader program (string)
 * @param fshader a fragment shader program (string)
 * @return true, if the program object was created and successfully made current 
 */
 function initShaders(gl, vshader, fshader) {
    var program = createProgram(gl, vshader, fshader);
    if (!program) {
      console.log('Failed to create program');
      return false;
    }
  
    gl.useProgram(program);
    gl.program = program;
  
    return true;
  }
  
  /**
   * Create the linked program object
   * @param gl GL context
   * @param vshader a vertex shader program (string)
   * @param fshader a fragment shader program (string)
   * @return created program object, or null if the creation has failed
   */
  function createProgram(gl, vshader, fshader) {
    // Create shader object
    var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
    var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
    if (!vertexShader || !fragmentShader) {
      return null;
    }
  
    // Create a program object
    var program = gl.createProgram();
    if (!program) {
      return null;
    }
  
    // Attach the shader objects
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
  
    // Link the program object
    gl.linkProgram(program);
  
    // Check the result of linking
    var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
    if (!linked) {
      var error = gl.getProgramInfoLog(program);
      console.log('Failed to link program: ' + error);
      gl.deleteProgram(program);
      gl.deleteShader(fragmentShader);
      gl.deleteShader(vertexShader);
      return null;
    }
    return program;
  }
  
  /**
   * Create a shader object
   * @param gl GL context
   * @param type the type of the shader object to be created
   * @param source shader program (string)
   * @return created shader object, or null if the creation has failed.
   */
  function loadShader(gl, type, source) {
    // Create shader object
    var shader = gl.createShader(type);
    if (shader == null) {
      console.log('unable to create shader');
      return null;
    }
  
    // Set the shader program
    gl.shaderSource(shader, source);
  
    // Compile the shader
    gl.compileShader(shader);
  
    // Check the result of compilation
    var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
    if (!compiled) {
      var error = gl.getShaderInfoLog(shader);
      console.log('Failed to compile shader: ' + error);
      gl.deleteShader(shader);
      return null;
    }
  
    return shader;
  }
  
  /** 
   * Initialize and get the rendering for WebGL
   * @param canvas <cavnas> element
   * @param opt_debug flag to initialize the context for debugging
   * @return the rendering context for WebGL
   */
  function getWebGLContext(canvas, opt_debug) {
    // Get the rendering context for WebGL
    var gl = WebGLUtils.setupWebGL(canvas);
    if (!gl) return null;
  
    // if opt_debug is explicitly false, create the context for debugging
    if (arguments.length < 2 || opt_debug) {
      gl = WebGLDebugUtils.makeDebugContext(gl);
    }
  
    return gl;
  }

happy