WebGL 入门 | 青训营笔记

83 阅读3分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 16 天

0x1 初识 WebGL

  1. WebGL

    是一种运用 GPU 能力的渲染技术

  2. Modern Graphics System

    • 光栅(Raster):是指构成图像的像素阵列
    • 像素(Pixel):通常保存图像上的某个具体位置的颜色等信息
    • 帧缓存(Frame Buffer):是一块内存地址,用于存放像素信息
    • CPU(Central Processing Unit):中央处理单元,负责逻辑计算
    • GPU(Graphics Processing Unit):图形处理单元,负责图形计算

    MGS 工作流程:

    graph LR
    A(轮廓提取 / Meshing)-->B(光栅化)
    B-->帧缓存
    帧缓存-->渲染
    
  3. The Pipeline

    graph LR
    Data-->Processor
    Processor-->A(Frame buffer)
    A-->Pixels
    

0x2 WebGL 实战

  1. WebGL Startup

    1. 创建 WebGL 上下文
    2. 创建 WebGL Program
    3. 将数据存入缓冲区
    4. 将缓冲区数据读取到 GPU
    5. GPU 执行 WebGL 程序并输出结果

    举例:绘制三角形

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="UTF-8" />
            <title>1</title>
            <style>
                html,body{
                    padding: 0;
                    margin: 0;
                }
                canvas{
                    width: 100vw;
                    height: 100vw;
                    max-width: 800px;
                    max-height: 800px;
                }
            </style>
        </head>
        <body>
            <canvas width="800" height="800"></canvas>
            <script>
                // 创建上下文
                const canvas = document.querySelector('canvas');
                const gl = canvas.getContext('webgl');
                
                // 定义着色器,添加运行在 GPU 中的 GLSL 代码
                // 顶点着色器
                const vertex = `attribute vec2 position;
                void main(){
                    gl_PointSize = 1.0;
                    gl_Position = vec4(position, 1.0, 1.0);
                }`;
                // 片段着色器
                const fragment = `precision highp float;
                void main(){
                    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
                }`;
    
                // 创建项目
                const vertexShader = gl.createShader(gl.VERTEX_SHADER);
                gl.shaderSource(vertexShader, vertex);
                gl.compileShader(vertexShader);
    
                const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
                gl.shaderSource(fragmentShader, fragment);
                gl.compileShader(fragmentShader);
    
                const program = gl.createProgram();
                gl.attachShader(program, vertexShader);
                gl.attachShader(program, fragmentShader);
                gl.linkProgram(program);
    
                gl.useProgram(program);
    
                // 定义顶点,原点在画布正中央,右向 x 正半轴,上向 y 正半轴
                const points = new Float32Array([
                    -1, -1,
                    0, 1,
                    1, -1
                ]);
    
                // 创建缓冲区
                const bufferId = gl.createBuffer();
                gl.bindBuffer(gl.ARRAY_BUFFER, bufferId);
                gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
    
                // 数据读取
                const vPosition = gl.getAttribLocation(program, 'position');
                gl.vertexAttribPointer(vPosition, 2, gl.FLOAT, false, 0, 0);
                gl.enableVertexAttribArray(vPosition);
    
                // 清理缓存区并绘图
                gl.clear(gl.COLOR_BUFFER_BIT);
                gl.drawArrays(gl.TRIANGLES, 0, points.length/2);
            </script>
        </body>
    </html>
    
  2. 其他绘制方法

    1. 2D

      const canvas = document.querySelector('canvas');
      const ctx = canvas.getContext('2d');
      
      ctx.beginPath();
      ctx.moveTo(250, 0);
      ctx.lineTo(500, 500);
      ctx.lineTo(0, 500);
      ctx.fillStyle = 'red';
      ctx.fill();
      
    2. mesh.js

      const { Renderer, Figure2D, Mesh2D } = meshjs;
      
      const canvas = document.querySelector('canvas');
      const renderer = new Renderer(canvas);
      
      const figure = new Figure2D();
      figure.beginPath();
      figure.moveTo(250, 0);
      figure.lineTo(500, 500);
      figure.lineTo(0, 500);
      
      const mesh = new Mesh2D(figure, canvas);
      mesh.setFill({
          color: [1, 0, 0, 1]
      });
      
      renderer.drawMeshes([mesh]);
      
  3. 绘制多边形:将多边形进行三角剖分

    const vertices = [
        [-0.7, 0.5],
        [-0.3, 0.4],
        [-0.2, 0.5],
        [-0.1, 0.9],
        [-0.3, 0.6],
        [-0.1, 0.1]
    ];
    const points = vertices.flat();
    cosnt triangles = earcut(points);
    
    const cells = new Uint16Array(triangles); 
    
    const cellsBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cellsBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, cells, gl.STATIC_DRAW);
    
    gl.drawElements(gl.TRIANGLES, cells.length, gl.UNSIGNED_SHORT, 0);
    
  4. 3D Meshing:通过三角剖分绘制 3D 图形

  5. 图形移动(Transforms)

    1. 平移

      { x=x0+x1 y=y0+y1\begin{cases} \ x = x_0 + x_1 \\ \ y = y_0 + y_1 \end{cases}
    2. 旋转

      { x=x0cosθy0sinθ y=x0sinθy0cosθ(xy)=(cosθsinθsinθcosθ)×(x0y0)\begin{cases} \ x = x_0\cos\theta - y_0\sin\theta \\ \ y = x_0\sin\theta - y_0\cos\theta \end{cases}\qquad \begin{pmatrix} x \\ y \end{pmatrix}= \begin{pmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{pmatrix}× \begin{pmatrix} x_0 \\ y_0 \end{pmatrix}
    3. 缩放

      { x=sxx0 y=syy0(xy)=(sx00sy)×(x0y0)\begin{cases} \ x = s_xx_0 \\ \ y = s_yy_0 \end{cases}\qquad \begin{pmatrix} x \\ y \end{pmatrix}= \begin{pmatrix} s_x & 0 \\ 0 & s_y \end{pmatrix}× \begin{pmatrix} x_0 \\ y_0 \end{pmatrix}
    4. 平移的线性变换

      1. 旋转 + 缩放是线性变换,存在矩阵相乘的关系

        P=M1×M2×M3××Mn×P0=M×P0(M=M1×M2×M3××Mn)P = M_1×M_2×M_3×\cdots ×M_n×P_0 \\ = M×P_0\quad (M=M_1×M_2×M_3×\cdots ×M_n)
      2. 假设平移为 P1P_1 ,此时

        P=M×P0+P1P=M × P_0 + P_1
      3. 此时平移的线性变换表达式为

        (P1)=(MP101)×(P01)\begin{pmatrix} P \\ 1 \end{pmatrix}= \begin{pmatrix} M & P_1 \\ 0 & 1 \end{pmatrix}× \begin{pmatrix} P_0 \\ 1 \end{pmatrix}
    5. 实现变换

      attribute vec2 position;
      uniform mat3 modelMatrix;
      
      void main(){
          gl_PointSize = 1.0;
          vec3 pos = modelMatrix * vec3(position, 1.0);
          gl_Position = vec4(pos, 1.0);
      }
      
      let transform = gl.getUniformLocation(program, 'modelMatrix');
      gl.uniformMatrix3fv(transform, false,[
          0.5, 0, 0,
          0, 0.5, 0,
          0, 0, 1
      ]);
      
  6. 3D Matrix

    3D 标准模型的四个齐次矩阵(mat4):

    1. 投影矩阵:Projection Matrix
    2. 模型矩阵:Model Matrix
    3. 视图矩阵:View Matrix
    4. 法向量矩阵:Normal Matrix

0x3 Shader

  1. 纯色

    #version 300 es
    precision highp float;
    
    out vec4 fragColor;
    
    void main(){
    	fragColor = vec4(1.0, 0.0, 0.0, 1.0);
    }
    
    • 通过修改vec4(R, G, B, A)中的参数(0~1)调整颜色
  2. 渐变

    #version 300 es
    precision highp float;
    
    uniform vec2 dd_resolution;
    out vec4 fragColor;
    
    void main(){
    	vec2 st = gl_FragCoord.xy / dd_resolution;
    	fragColor = vec4(st, 0.0, 1.0);
    }
    
    • dd_resolution:实时获取页面分辨率
    • 经计算后st的横纵坐标在 0~1 之间渐变,代入vec4()中实现渐变效果
  3. 画圆

    1. 基础圆

      #version 300 es
      precision highp float;
      
      uniform vec2 dd_resolution;
      out vec4 fragColor;
      
      void main(){
      	vec2 st = gl_FragCoord.xy / dd_resolution;
      	vec2 center = vec2(0.5);
      	float r = 0.2;
      	fragColor.rgb = step(length(st - center), r) * vec3(1.0);
      	fragColor.a = 1.0;
      }
      
    2. 平滑圆

      float d = length(st - center);
      fragColor.rgb = smoothstep(d - 0.01, d, r) * vec3(1.0);
      
    3. 圆环

      vec3 circle1 = smoothstep(d - 0.01, d, r) * vec3(1.0);
      vec3 circle2 = smoothstep(d, d + 0.01, r - 0.02) * vec3(1.0);
      fragColor.rgb = circle1 - circle2;
      
      • 将画圆环的操作封装成函数

        float stroke(float d, float d0, float w, float smth){
        	float th = 0.5 * w;
        	smth = smth * w;
        	float start = d0 - th;
        	float end = d0 + th;
        	return smoothstep(start, start + smth, d) - smoothstep(end - smth, end, d);
        }
        void main(){
        	vec2 st = gl_FragCoord.xy / dd_resolution;
        	vec2 center = vec2(0.5);
        	float r = 0.2;
        	float d = length(st - center);
        	float d1 = stroke(d, r, 0.02, 0.3);
        	fragColor.rgb = d1 * vec3(1.0);
        	fragColor.a = 1.0;
        }
        
  4. 斜线、曲线

    #version 300 es
    precision highp float;
    
    uniform vec2 dd_resolution;
    out vec4 fragColor;
    
    float stroke(float d, float d0, float w, float smth){
    	float th = 0.5 * w;
    	smth = smth * w;
    	float start = d0 - th;
    	float end = d0 + th;
    	return smoothstep(start, start + smth, d) - smoothstep(end - smth, end, d);
    }
    
    void main(){
    	vec2 st = gl_FragCoord.xy / dd_resolution;
        
    	// 斜线
    	float d1 = stroke(st.y, st.x, 0.02, 0.1);
    	// 曲线
    	float d2 = stroke(st.y, 4.0 * (st.x - 0.5) * (st.x - 0.5), 0.02, 0.1);
        float d3 = stroke(st.y * 2.0, 1.0 - sin(30.0 * st.x), 0.02, 0.1);
    	fragColor.rgb = d1 * vec3(1.0, 1.0, 0.0) + d2 * vec3(0.0, 1.0, 1.0) + d3 * vec3(0.5, 1.0, 0.5);
    	fragColor.a = 1.0;
    }
    	
        fragColor.rgb = d1 * vec3(1.0, 1.0, 0.0) + d2 * vec3(0.0, 1.0, 1.0);
    	fragColor.a = 1.0;
    }