WebGL入门基础(一): 从一个鼠标画点开始了解原生webGL

1,780 阅读8分钟

面向web前端的WebGL教程,网络上的教程均是假设有计算机图形学基础,对web开发者来说不是很友好, 故开辟此坑

最终效果

codepen.io/chendonming…

鼠标点击 画一个点。

webGL如何展示一个点

首先得知道webGL如何展示出一个点?

webGL画任意物体 都需要一个顶点着色器片元着色器,

顶点着色器:描述顶点的特性(位置、颜色等)的程序.

片元着色器: 进行着片元处理过程的程序。

也许你会很懵,一大堆官方理论又要望而却步了,所以我直接展示下最简单的展示一个点的代码,相信你会马上明白。

<canvas id="glcanvas" width="640" height="480">
    你的浏览器似乎不支持或者禁用了HTML5 <code>&lt;canvas&gt;</code> 元素.
</canvas>

首先,我需要一些简单的封装函数:

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;
}

这是一段初始化着色器的函数

初始化webgl:

const canvas = document.querySelector("#glcanvas");
// 初始化WebGL上下文
const gl = canvas.getContext("webgl");

// 确认WebGL支持性
if (!gl) {
    alert("无法初始化WebGL,你的浏览器、操作系统或硬件等可能不支持WebGL。");
    return;
}
// 使用完全不透明的黑色清除所有图像
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// 用上面指定的颜色清除缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);

调用初始化着色器函数。

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,目前为止,你应该能看到黑色canvas中间有个红色的点了。

解析

最关键的部分,其实就是VSHADER_SOURCEFSHADER_SOURCE两个字符串,分别表示了点的坐标和点的颜色。

VSHADER_SOURCEFSHADER_SOURCE是属于glsl代码,

VSHADER_SOURCE中的gl_Position代表的就是点的位置,gl_Positionglsl的内置变量。

你会发现gl_Position的值是个vec4类型,坐标居然有4个值?其实这个是齐次坐标.

对于vec4(x, y, z, w), 真实的世界坐标是 (x/w, y/w, z/w), 所以一般vec4第四个参数我们设置为1

为什么需要齐次坐标呢,因为三维世界中,向量也是三个坐标表示的, 所以为了区分向量和真实位置引入了第四个参数,向量的第四个参数是0.

js和GLSL通信

上面代码确实画出一个点, 但是是写在一个字符串中的,这肯定不方便我们进行操作啊,所以操作glsl中的变量就很有必要了。

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

如上图,我们对顶点着色器代码 加入了一个attribute, 然后attribute a_Position赋值给glsl内置变量gl_Position,这是否意味着我改动a_Position的值,gl_Position也会改变呢?

js获取并修改attribute

需要的API:

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

getAttribLocation方法返回了给定WebGLProgram对象中某属性的下标指向位置

vertexAttrib3f可以为顶点attibute变量赋值

现在只需要在gl.drawArrays(gl.POINTS, 0, 1);之前修改attribute即可

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

目前为止,完整代码如下:

    const canvas = document.querySelector("#glcanvas");
    // 初始化WebGL上下文
    const gl = canvas.getContext("webgl");

    // 确认WebGL支持性
    if (!gl) {
        alert("无法初始化WebGL,你的浏览器、操作系统或硬件等可能不支持WebGL。");
        return;
    }
    // 使用完全不透明的黑色清除所有图像
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    // 用上面指定的颜色清除缓冲区
    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);

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

  //画点
  gl.drawArrays(gl.POINTS, 0, 1);

画多个点

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);

你会发现屏幕上存在了两个红点。

drawArrays方法用于从向量数组中绘制图元,每执行一次就要通知GPU渲染图元。

现在两个点还好,如果是成千上万个点呢?我们需要一次性画多个点,这样才能保持性能。

类型化数组TypedArray

对于多个点,我们需要把点的位置存在变量中,我们选择了TypedArray, 它相比普通Array有几个好处:性能 性能 还tm是性能。 对于typedArray介绍看如下代码:

// 下面代码是语法格式,不能直接运行,
// TypedArray 关键字需要替换为底部列出的构造函数。
new TypedArray(); // ES2017中新增
new TypedArray(length);
new TypedArray(typedArray);
new TypedArray(object);
new TypedArray(buffer [, byteOffset [, length]]);

// TypedArray 指的是以下的其中之一:

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

那怎么选择呢, 看如下列表:

类型单个元素值的范围大小(bytes)描述Web IDL 类型C 语言中的等价类型
Int8Array-128 to 12718 位二进制有符号整数byteint8_t
Uint8Array0 to 25518 位无符号整数(超出范围后从另一边界循环)octetuint8_t
Uint8ClampedArray0 to 25518 位无符号整数(超出范围后为边界值)octetuint8_t
Int16Array-32768 to 32767216 位二进制有符号整数shortint16_t
Uint16Array0 to 65535216 位无符号整数unsigned shortuint16_t
Int32Array-2147483648 to 2147483647432 位二进制有符号整数longint32_t
Uint32Array0 to 4294967295432 位无符号整数unsigned longuint32_t
Float32Array1.2×10^-38 to 3.4×10^38432 位 IEEE 浮点数(7 位有效数字,如 1.1234567unrestricted floatfloat
Float64Array5.0×10^-324 to 1.8×10^308864 位 IEEE 浮点数(16 有效数字,如 1.123...15)unrestricted doubledouble
BigInt64Array-2^63 to 2^63-1864 位二进制有符号整数bigintint64_t (signed long long)
BigUint64Array0 to 2^64 - 1864 位无符号整数bigintuint64_t (unsigned long long)

对于这个教程,因为等会我们会使用浮点数,又因为数据不大,所以选择Float32Array.

尝试绘制两个点

  • 将两个点的坐标储存到变量中

    const verties = new Float32Array([0.0, 0.5, -0.5, -0.5]);
    
  • 把数据挂到缓冲区某个内存位置,写入数据

      const vertexBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
      // verties就是我们自己创建的Float32Array数据
      gl.bufferData(gl.ARRAY_BUFFER, verties, gl.STATIC_DRAW);
    
  • 读取数据并修改attribute

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

    理解vertexAttribPointer函数可以看我的这篇笔记 note.youdao.com/s/c5EG9usH

  • 修改了attribute,下一步调用绘制命令

    // 因为是绘制两个点,第三个参数输入2
    gl.drawArrays(gl.POINTS, 0, 2);
    

完整代码

除去初始化webGL和工具函数initShaders, 因为每次都写 没有变化...

  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);

鼠标监听坐标并写入

如果上面的都理解了话,第三步反而是最简单的了(对于web开发人员来说). 具体功能上面代码都是实现了,只需要:

  • 点击的时候把屏幕坐标转成webGL坐标
  • 把坐标存入Float32Array数据
  • 修改attribute,渲染。

转成webGl坐标

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

其中 320 = 640/2 240 = 480/2

320代表canvas元素的宽, 240代表canvas元素高

存入Float32Array数据

首先Float32Array是固定长度的,无法动态修改,所以需要新建一个Float32Array

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;

最终代码

代码可以在codePen里查看, 如果无法打开的话, 我将代码展示出来:

<!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">
        你的浏览器似乎不支持或者禁用了HTML5 <code>&lt;canvas&gt;</code> 元素.
    </canvas>
</body>
<script src="utils/cuon-utils.js"></script>
<script>
    let arrayBuffer = new Float32Array()
    function main() {
        const canvas = document.querySelector("#glcanvas");
        // 初始化WebGL上下文
        const gl = canvas.getContext("webgl");

        // 确认WebGL支持性
        if (!gl) {
            alert("无法初始化WebGL,你的浏览器、操作系统或硬件等可能不支持WebGL。");
            return;
        }
        // 使用完全不透明的黑色清除所有图像
        gl.clearColor(0.0, 0.0, 0.0, 1.0);
        // 用上面指定的颜色清除缓冲区
        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);

        // 监听点击事件
        document.getElementById('glcanvas').addEventListener('mousedown', e => {
            clear(gl);
            // 左上角原点坐标
            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>

其中cuon-utils.js是封装的一个小工具函数

// 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