16、webgl 基本概念 + 恰似一阵春风吹过水面异色版本

基本概念

图形光栅化:把图形转成片元

颜色

多 attribute 变量

多 attribute 变量 的概念

问题:如何一次性绘制三个不同颜色的点?

回顾: js 修改顶点颜色,下面这种方式只能给批量绘制的点设置为同一个颜色

javascript 复制代码
// 片元着色器
<script id="fragmentShader" type="x-shader/x-fragment">
    precision mediump float;
    uniform vec4 u_FragColor;
    void main() {
        gl_FragColor = u_FragColor;
    }
</script>
javascript 复制代码
// js
  let u_FragColor = gl.getUniformLocation(program, 'u_FragColor');
  gl.uniform4fv(u_FragColor, new Float32Array([1, 1, 0, 1])); 
</script>

如果我们想过要给多点不同的颜色,就需要建立一个接收颜色数据的 attribute 变量

代码实现

1 在顶点着色器中,建立一个名为 a_Color 的 attribute 变量,并通过 varing 变量将其全局化,之后可以在片元着色器中拿到

javascript 复制代码
// 顶点着色器
<script id="vertexShader" type="x-shader/x-vertex">
    // 一个属性值,将会从缓冲区中获取数据
    attribute vec4 a_Position;
    attribute float a_PointSize;
    attribute vec4 a_Color;
    varying vec4 v_Color;
    // 所有着色器都有一个 main 方法
    void main() {
        // gl_Position 是一个顶点着色器主要设置的变量
        gl_Position = a_Position;
        gl_PointSize = a_PointSize;
        v_Color = a_Color;
    }
</script>

2 在片元着色器中获取顶点着色器中全局化的 varing 变量,然后将其作为片元颜色

javascript 复制代码
// 片元着色器
 <script id="fragmentShader" type="x-shader/x-fragment">
  precision mediump float;
  varying vec4 v_Color;
  void main() {
    gl_FragColor = v_Color;
  }
</script>

3 在js中,将顶点数据批量传递给顶点着色器

javascript 复制代码
const vertices = [0.0, 0.1,0.2,0.3, 0.5, 1, ]
const vertexBuffer = gl.createBuffer();
const a_Position = gl.getAttribLocation(program, 'a_Position');
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
gl.vertexAttribPointer(a_Position, 1, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);

4 用同样原理将颜色数据批量传递给顶点着色器

javascript 复制代码
const colors = new Float32Array([
    1, 0, 0, 1, //一组4个值, 对应 rgba 的值
    0, 1, 0, 1,
    0, 0, 1, 1,
    1, 1, 0, 1
])
const colorBuffer = gl.createBuffer();
const a_Color = gl.getAttribLocation(program, 'a_Color');
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);
gl.vertexAttribPointer(a_Color, 4, gl.FLOAT, false, 0, 0); // 第二参数跟 一组颜色多少个值对应,比如我上面是 4个值为一个颜色,那么这里就写 4
gl.enableVertexAttribArray(a_Color);

在这个案例中,我们用 js 建立了两份 attribute 数据,一份是 顶点位置数据,一份是顶点颜色数据.

然后我们将两份 attribute 数据放进了两个缓冲区对象里,后面绘图的时候,顶点着色器就会从这里面找数据

但实际上,我们可以把数据合一下,把点数据和颜色数据放进一个集合里面,然后让 attribute变量按照某种规律从中找数据.

多 attribute 数据合一

合一的数据,前几个对应点位,后几个值对应颜色,如下:

javascript 复制代码
const source = new Float32Array([
    0.0, 0.1, 0, 1, 0, 0, 1, //一组7个值, 前三个是点位,后四个对应 rgba 的值
    0.2, 0.3, 0, 0, 1, 0, 1,
    0.5, 1, 0,   0, 0, 1, 1,
    0.6, 0.7, 0, 1, 1, 0, 1
])

对应上面的数据,先有下面概念:

数据源: 整个合二为一的数据 source

元素字节数: 32位浮点集合中每个元素的字节数

类目: 一个顶点对应一个类目,也就是上面 source 中的每一行

系列: 一个类目中所包含的每一种数据,比如顶点位置数据,顶点颜色数据

系列尺寸: 一个系列所对应的向量的分量数目

类目尺寸: 一个类目中所有系列尺寸的总和

类目字节数: 一个类目的所有字节数量

系列元素索引位置: 一个系列在一个类目中,以集合元素为单位的索引位置

系列字节索引位置: 一个系列在一个类目中,以 字节为单位的索引位置

顶点总数: 数据源中的顶点总数

javascript 复制代码
// 数据源
const source = new Float32Array([
    0.0, 0.1, 0, 1, 0, 0, 1, //一组7个值, 前三个是点位,后四个对应 rgba 的值
    0.2, 0.3, 0, 0, 1, 0, 1,
    0.5, 1, 0,   0, 0, 1, 1,
    0.6, 0.7, 0, 1, 1, 0, 1
])
// 元素字节数
const elementBytes = source.BYTES_PER_ELEMENT
// 系列尺寸
const verticeSize = 3
const colorSize = 4
// 类目尺寸
const categorySize = verticeSize + colorSize
// 类目字节数
const categoryBytes = categorySize * elementBytes
// 系列索引位置
const verticeByteIndex = 0
const colorByteIndex = verticeSize * elementBytes
// 顶点总数
const sourceSize = source.length / categorySize
用 vertexAttribPointer() 方法玩转数据源

这个方法是在告诉顶点着色器中的 attribute 变量以怎样的方式从顶点着色器中寻找它所需要的数据

比如我想让顶点着色器中,名叫 a_Position 的attribute 的变量从数据源中,寻找它所需要的数据

1 把数据源装进绑定在 webgl 上下文对象上的缓冲区中

javascript 复制代码
// 缓冲对象
const sourceBuffer = gl.createBuffer();
// 绑定缓冲对象
gl.bindBuffer(gl.ARRAY_BUFFER, sourceBuffer);
// 写入数据
gl.bufferData(gl.ARRAY_BUFFER, source, gl.STATIC_DRAW);

2 告诉顶点着色器中,名叫 a_Position 的attribute 的变量 如何从数据源中找到它所需要的数据

javascript 复制代码
// 获取 attribute 变量
const a_Position = gl.getAttribLocation(program, 'a_Position');
// 修改 attribute 变量
gl.vertexAttribPointer(a_Position, verticeSize, gl.FLOAT, false, categoryBytes, verticeByteIndex);
gl.enableVertexAttribArray(a_Position);
// 颜色的取值
const a_Color = gl.getAttribLocation(program, 'a_Color');
gl.vertexAttribPointer(a_Color, colorSize, gl.FLOAT, false, categoryBytes, colorByteIndex);
gl.enableVertexAttribArray(a_Color);
彩色三角形

代码实现

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>绘制彩色三角形</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      overflow: hidden;
    }
  </style>
</head>

<body>
  <canvas id="canvas"></canvas>
  
  <script id="vertexShader" type="x-shader/x-vertex">
    // 一个属性值,将会从缓冲区中获取数据
    attribute vec4 a_Position;
    attribute float a_PointSize;
    attribute vec4 a_Color;
    varying vec4 v_Color;
    // 所有着色器都有一个 main 方法
    void main() {
        // gl_Position 是一个顶点着色器主要设置的变量
        gl_Position = a_Position;
        gl_PointSize = a_PointSize;
        v_Color = a_Color;
    }
  </script>
 <script id="fragmentShader" type="x-shader/x-fragment">
  precision mediump float;
  varying vec4 v_Color;
  void main() {
    gl_FragColor = v_Color;
  }
</script>

  <script type="module">
    import { getPosByMouse, getInnerText, initShaderProgram } from "../utils/index.js"
    const canvas = document.querySelector('#canvas');
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    const gl = canvas.getContext('webgl');
    gl.clearColor(0, 0, 0, 1);
    const vertexStr = getInnerText("vertexShader");
    const fragmentStr = getInnerText("fragmentShader");
    const program = initShaderProgram(gl, vertexStr, fragmentStr )
    
    const a_PointSize = gl.getAttribLocation(program, 'a_PointSize');
   
    gl.useProgram(program);

    const source = new Float32Array([
        0.0, 0.1, 0, 1, 0, 0, 1, //一组7个值, 前三个是点位,后四个对应 rgba 的值
        0.2, 0.3, 0, 0, 1, 0.5, 1,
        0.5, 0.2, 0,   0, 0, 1, 1,
    ])
    // 元素字节数
    const elementBytes = source.BYTES_PER_ELEMENT
    // 系列尺寸
    const verticeSize = 3
    const colorSize = 4
    // 类目尺寸
    const categorySize = verticeSize + colorSize
    // 类目字节数
    const categoryBytes = categorySize * elementBytes
    // 系字节列索引位置
    const verticeByteIndex = 0
    const colorByteIndex = verticeSize * elementBytes
    // 顶点总数
    const sourceSize = source.length / categorySize

    // 缓冲对象
    const sourceBuffer = gl.createBuffer();
    // 绑定缓冲对象
    gl.bindBuffer(gl.ARRAY_BUFFER, sourceBuffer);
    // 写入数据
    gl.bufferData(gl.ARRAY_BUFFER, source, gl.STATIC_DRAW);

    // 获取 attribute 变量
    const a_Position = gl.getAttribLocation(program, 'a_Position');
    // 修改 attribute 变量
    gl.vertexAttribPointer(a_Position, verticeSize, gl.FLOAT, false, categoryBytes, verticeByteIndex);
    gl.enableVertexAttribArray(a_Position);
    const a_Color = gl.getAttribLocation(program, 'a_Color');
    gl.vertexAttribPointer(a_Color, colorSize, gl.FLOAT, false, categoryBytes, colorByteIndex);
    gl.enableVertexAttribArray(a_Color);

    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.vertexAttrib1f(a_PointSize, 30);
    gl.drawArrays(gl.TRIANGLES, 0, sourceSize);
 </script>
</body>
</html>

恰似一阵春风吹过水面

异色版本

poly.js 文件

javascript 复制代码
const defAttr = () => ({
  gl: null,
  types: ["POINTS"],
  source: [], // 数据源
  sourceSize: 0, // 数据源尺寸
  elementBytes: 4, // 元素字节数
  categorySize: 0, // 类目尺寸
  attributes: {}, // attribute属性集合
  uniforms: {},
});

export default class Poly {
  constructor(attr) {
    Object.assign(this, defAttr(), attr);
    this.init();
  }

  init() {
    if (!this.gl) {
      return;
    }
    this.calculateSize();
    this.updateAttribute();
    this.updateUniform();
  }
  calculateSize() {
    const { attributes, elementBytes, source } = this;
    let categorySize = 0;
    Object.values(attributes).forEach((ele) => {
      const { size, index } = ele;
      categorySize += size;
      ele.byteIndex = index * elementBytes;
    });
    this.categorySize = categorySize;
    this.categoryBytes = categorySize * elementBytes;
    this.sourceSize = source.length / categorySize;
  }
  updateAttribute() {
    const { gl, attributes, categoryBytes, source } = this;
    // 缓冲对象
    const sourceBuffer = gl.createBuffer();
    // 绑定缓冲对象
    gl.bindBuffer(gl.ARRAY_BUFFER, sourceBuffer);
    // 写入数据
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(source), gl.STATIC_DRAW);

    for (const [key, { size, byteIndex }] of Object.entries(attributes)) {
      const a_Attribute = gl.getAttribLocation(gl.program, key);
      // 修改 attribute 变量
      gl.vertexAttribPointer(
        a_Attribute,
        size,
        gl.FLOAT,
        false,
        categoryBytes,
        byteIndex,
      );
      gl.enableVertexAttribArray(a_Attribute);
    }
  }

  updateUniform() {
    const { gl, uniforms } = this;
    for (let [key, val] of Object.entries(uniforms)) {
      let { type, value } = val;
      const u = gl.getUniformLocation(gl.program, key);
      if (type.includes("Matrix")) {
        gl[type](u, false, value);
      } else {
        gl[type](u, value);
      }
    }
  }

  draw(types = this.types) {
    const { gl, sourceSize } = this;
    for (let type of types) {
      gl.drawArrays(gl[type], 0, sourceSize);
    }
  }
}
html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>一池春水</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      overflow: hidden;
    }
  </style>
</head>

<body>
  <canvas id="canvas"></canvas>
  
  <script id="vertexShader" type="x-shader/x-vertex">
    // 一个属性值,将会从缓冲区中获取数据
    attribute vec4 a_Position;
    uniform mat4 u_ViewMatrix;
    attribute vec4 a_Color;
    varying vec4 v_Color;
    void main() {
        gl_Position = u_ViewMatrix * a_Position;
        gl_PointSize = 3.0;
        v_Color = a_Color;
    }
  </script>
 <script id="fragmentShader" type="x-shader/x-fragment">
  precision mediump float;
  varying vec4 v_Color;
  void main() {
   gl_FragColor = v_Color;
  }
</script>

<script type="module">
    import { Matrix4, Vector3, Color} from "https://www.unpkg.com/three/build/three.module.js"
    import  Poly from "../utils/ColorfulPoly.js"
    import { getPosByMouse, getInnerText, initShaderProgram2, ScaleLinear } from "../utils/index.js"
    const canvas = document.querySelector('#canvas');
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    const gl = canvas.getContext('webgl');
    const vertexStr = getInnerText("vertexShader");
    const fragmentStr = getInnerText("fragmentShader");
    initShaderProgram2(gl, vertexStr, fragmentStr )
    gl.clearColor(0, 0, 0, 1);

    // 视图矩阵
    const viewMatrix = new Matrix4().lookAt(
      new Vector3(0.2, 0.3, 1),
      new Vector3(),
      new Vector3(0, 1, 0)
    )
   /* x,z 方向的空间坐标极值 */
    const [minPosX, maxPosX, minPosZ, maxPosZ] = [
      -0.7, 0.8, -1, 1
    ]
    /* x,z 方向的弧度极值 */
    const [minAngX, maxAngX, minAngZ, maxAngZ] = [
      0, Math.PI * 4, 0, Math.PI * 2
    ]

    /* 比例尺:将空间坐标和弧度相映射 */
    const scalerX = ScaleLinear(minPosX, minAngX, maxPosX, maxAngX)
    const scalerZ = ScaleLinear(minPosZ, minAngZ, maxPosZ, maxAngZ)
    // y方向的坐标极值
    const [a1, a2] = [0.1, 0.03]
    const a12 = a1 + a2;
    const [minY, maxY] = [-a12, a12]
    // 色相极值
    const [minH, maxH] = [0.1, 0.55]
    // 比例尺:将y坐标和色相相映射
    const scalerC = ScaleLinear(minY, minH, maxY, maxH)
    // 颜色对象,可通过 HSL 获取颜色
    const color = new Color(0x00acec)
    // 波浪对象的行数和列数
    const [rows, cols] = [50, 50]
    // 波浪对象的两个 attribute 变量,分别是位置和颜色
    const a_Position = {size: 3, index: 0}
    const a_Color = {size: 4, index: 3}
    // 类目尺寸
    const categorySize = a_Position.size + a_Color.size


    // 创建波浪对象
    const wave = new Poly({
      gl,
      source: createSource(
        cols, rows,
        minPosX, maxPosX, minPosZ, maxPosZ
      ),
      uniforms: {
        u_ViewMatrix: {
          type: 'uniformMatrix4fv',
          value: viewMatrix.elements
        },
      },
      attributes: {
        a_Position,
        a_Color
      },
    })

    // 建立顶点集合
    function createSource(cols, rows,
        minPosX, maxPosX, minPosZ, maxPosZ) {
      const source = [];
      const spaceZ = (maxPosZ - minPosZ) / rows
      const spaceX = (maxPosX - minPosX) / cols

      for(let z = 0; z < rows; z++) {
        for(let x = 0; x < cols; x++) {
          const px = x * spaceX + minPosX
          const pz = z * spaceZ + minPosZ
          source.push(px, 0, pz, 1, 1, 1, 1)
        }
      }
      return source
    }
    // 更新顶点高度
    function updateSource(offset = 0) {
      let {source, categorySize} = wave;
      for(let i = 0; i < source.length; i += categorySize) {
        const [posX, posZ] = [source[i], source[i + 2]];
        const angZ = scalerZ(posZ),
            Omega = 2, 
            a = Math.sin(angZ) * a1 + a2, 
            phi = scalerX(posX) + offset,
            y= SinFn(a, Omega, phi)(angZ);// i+2去赋值导致点消失,所以需要i+1赋值
            source[i + 1] = y;

            const h = scalerC(y), 
             {r, g, b} = color.setHSL(h, 1, 0.6)
             source[i + 3] = r
             source[i + 4] = g
             source[i + 5] = b
          }
    }
    // 正弦函数
    function SinFn(a, Omega, phi) {
      return function(x) {
        return a * Math.sin(Omega * x + phi);
      };
    }
    // 动画: 偏移phi
    let offset = 0;
    !(function ani() {
      offset += 0.08;
      updateSource(offset);
      wave.updateAttribute();
      gl.clear(gl.COLOR_BUFFER_BIT);
      wave.draw();
      requestAnimationFrame(ani);
    })()
 </script>
</body>
</html>

将春水的点换成线条以及三角形连接

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>一池春水</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      overflow: hidden;
    }
  </style>
</head>

<body>
  <canvas id="canvas"></canvas>
  
  <script id="vertexShader" type="x-shader/x-vertex">
    // 一个属性值,将会从缓冲区中获取数据
    attribute vec4 a_Position;
    uniform mat4 u_ViewMatrix;
    attribute vec4 a_Color;
    varying vec4 v_Color;
    void main() {
        gl_Position = u_ViewMatrix * a_Position;
        gl_PointSize = 3.0;
        v_Color = a_Color;
    }
  </script>
 <script id="fragmentShader" type="x-shader/x-fragment">
  precision mediump float;
  varying vec4 v_Color;
  void main() {
   gl_FragColor = v_Color;
  }
</script>

<script type="module">
    import { Matrix4, Vector3, Color} from "https://www.unpkg.com/three/build/three.module.js"
    import  Poly from "../utils/ColorfulPoly.js"
    import { getPosByMouse, getInnerText, initShaderProgram2, ScaleLinear } from "../utils/index.js"
    const canvas = document.querySelector('#canvas');
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    const gl = canvas.getContext('webgl');
    const vertexStr = getInnerText("vertexShader");
    const fragmentStr = getInnerText("fragmentShader");
    initShaderProgram2(gl, vertexStr, fragmentStr )
    gl.clearColor(0, 0, 0, 1);
       // 开启透明度混合
    gl.enable(gl.BLEND);
    // 设置混合公式(正常透明模式)
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE);

    // 视图矩阵
    const viewMatrix = new Matrix4().lookAt(
      new Vector3(0.2, 0.3, 1),
      new Vector3(),
      new Vector3(0, 1, 0)
    )
   /* x,z 方向的空间坐标极值 */
    const [minPosX, maxPosX, minPosZ, maxPosZ] = [
      -0.7, 0.8, -1, 1
    ]
    /* x,z 方向的弧度极值 */
    const [minAngX, maxAngX, minAngZ, maxAngZ] = [
      0, Math.PI * 4, 0, Math.PI * 2
    ]

    /* 比例尺:将空间坐标和弧度相映射 */
    const scalerX = ScaleLinear(minPosX, minAngX, maxPosX, maxAngX)
    const scalerZ = ScaleLinear(minPosZ, minAngZ, maxPosZ, maxAngZ)
    // y方向的坐标极值
    const [a1, a2] = [0.1, 0.03]
    const a12 = a1 + a2;
    const [minY, maxY] = [-a12, a12]
    // 色相极值
    const [minH, maxH] = [0.1, 0.55]
    // 比例尺:将y坐标和色相相映射
    const scalerC = ScaleLinear(minY, minH, maxY, maxH)
    // 颜色对象,可通过 HSL 获取颜色
    const color = new Color(0x00acec)
    // 波浪对象的行数和列数
    const [rows, cols] = [40, 40]
    // 波浪对象的两个 attribute 变量,分别是位置和颜色
    const a_Position = {size: 3, index: 0}
    const a_Color = {size: 4, index: 3}
    // 类目尺寸
    const categorySize = a_Position.size + a_Color.size
    // 建立基于行列获取顶点索引的方法
    const getInd = GetIndexInGrid(cols, categorySize)
    function  GetIndexInGrid(w, size) {
      return function(x, y) {
        return (y * w + x) * size;
      }
    }

    // 获取顶点阵列和三角形的顶点索引集合
    const {vertices, indexes} = createBaseData(cols, rows, minPosX, maxPosX, minPosZ, maxPosZ) 

    function createBaseData(cols, rows, minPosX, maxPosX, minPosZ, maxPosZ)  {
      const vertices = []
      const indexes = []
        const spaceZ = (maxPosZ - minPosZ) / rows
      const spaceX = (maxPosX - minPosX) / cols

      for(let z = 0; z < rows; z++) {
        for(let x = 0; x < cols; x++) {
          const px = x * spaceX + minPosX
          const pz = z * spaceZ + minPosZ
          vertices.push(px, 0, pz, 1, 1, 1, 1)
          if(z && x) {
            const [x0, z0] = [x - 1, z - 1]
            indexes.push(
              getInd(x0, z0),
              getInd(x, z0),
              getInd(x, z),
              getInd(x0, z0),
              getInd(x, z),
              getInd(x0, z),
            )
          }
        }
      }

      return {
        vertices,
        indexes
      }
    }


    // 创建波浪对象
    const wave = new Poly({
      gl,
      source: createSource(indexes, vertices, categorySize),
      uniforms: {
        u_ViewMatrix: {
          type: 'uniformMatrix4fv',
          value: viewMatrix.elements
        },
      },
      attributes: {
        a_Position,
        a_Color
      },
    })

    // 建立顶点集合
    // function createSource(cols, rows,
    //     minPosX, maxPosX, minPosZ, maxPosZ) {
    //   const source = [];
    //   const spaceZ = (maxPosZ - minPosZ) / rows
    //   const spaceX = (maxPosX - minPosX) / cols

    //   for(let z = 0; z < rows; z++) {
    //     for(let x = 0; x < cols; x++) {
    //       const px = x * spaceX + minPosX
    //       const pz = z * spaceZ + minPosZ
    //       source.push(px, 0, pz, 1, 1, 1, 1)
    //     }
    //   }
    //   return source
    // }
     function createSource(indexes, vertices, categorySize) {
      const arr = []
      indexes.forEach(i => {
        arr.push(...vertices.slice(i, i + categorySize))
      })
      return arr
    }
    // 更新顶点高度
    function updateSource(offset = 0) {
      let {source, categorySize} = wave;
      for(let i = 0; i < source.length; i += categorySize) {
        const [posX, posZ] = [source[i], source[i + 2]];
        const angZ = scalerZ(posZ),
            Omega = 2, 
            a = Math.sin(angZ) * a1 + a2, 
            phi = scalerX(posX) + offset,
            y= SinFn(a, Omega, phi)(angZ);// i+2去赋值导致点消失,所以需要i+1赋值
            source[i + 1] = y;

            const h = scalerC(y), 
             {r, g, b} = color.setHSL(h, 1, 0.5)
             source[i + 3] = r
             source[i + 4] = g
             source[i + 5] = b
          }
    }
    // 正弦函数
    function SinFn(a, Omega, phi) {
      return function(x) {
        return a * Math.sin(Omega * x + phi);
      };
    }
    const render = () =>{
      gl.clear(gl.COLOR_BUFFER_BIT);
      // wave.draw();
      wave.draw(['LINES', 'TRIANGLES']);
    }
    // 动画: 偏移phi
    let offset = 0;
    !(function ani() {
      offset += 0.01;
      updateSource(offset);
      wave.updateAttribute();
      render()
      requestAnimationFrame(ani);
    })()
 </script>
</body>
</html>