2. Js与着色器间的数据传输——修改顶点位置和大小

194 阅读16分钟
  • 使用js向着色器传递数据
  • 获取鼠标在canvas 中的webgl 坐标系位置

一. 用js控制一个点的位置

1-1 attribute变量的概念

<!-- 顶点着色器 -->
    <script id="vertexShader" type="x-shader/x-vertex">
      void main() {
          gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
          gl_PointSize = 100.0;
      }
    </script>

其中,点的定位:

gl_Position = vec4(0,0,0,1);

这种方式将数据写死了,缺乏可扩展性。要让这个点位可以动态改变,那就得把它变成attribute变量。

  • attribute 变量是只有顶点着色器才能使用的。
  • js 可以通过attribute 变量顶点着色器传递与顶点相关的数据。

1-2 js向attribute 变量传参的步骤

1-2-1. 在顶点着色器中声明attribute 变量。

<script id="vertexShader" type="x-shader/x-vertex">
    attribute vec4 a_Position;
    void main(){
        gl_Position = a_Position;
        gl_PointSize = 50.0;
    }
</script>

1-2-2. 在js中获取attribute 变量

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

1-2-3. 修改attribute 变量

gl.vertexAttrib3f(a_Position,0.0,0.5,0.0);

image.png

整体代码

<!DOCTYPE html>
<html>
  <body>
    <div id="app">
      <canvas id="canvas"></canvas>
    </div>
    <!-- 顶点着色器 -->
    <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      void main(){
          //点位
          gl_Position=a_Position;
          //尺寸
          gl_PointSize=50.0;
      }
    </script>
    <!-- 片元着色器 -->
    <script id="fragmentShader" type="x-shader/x-fragment">
      void main() {
          //颜色
          gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
      }
    </script>
    <script>
      // canvas 画布
      const canvas = document.getElementById('canvas')
      canvas.width = window.innerWidth
      canvas.height = window.innerHeight
      // webgl画笔
      const gl = canvas.getContext('webgl')
      // 顶点着色器
      const vsSource = document.getElementById('vertexShader').innerText
      // const vsSource = `attribute vec4 a_Position;
      // void main(){
      //     //点位
      //     gl_Position=a_Position;
      //     //尺寸
      //     gl_PointSize=50.0;
      // }`
      // 片元着色器
      const fsSource = document.getElementById('fragmentShader').innerText
      // 初始化着色器
      initShaders(gl, vsSource, fsSource)

      // 在js中获取attribute 变量
      const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
      // 修改attribute 变量
      gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0)

      // 指定将要用来清理绘图区的颜色
      gl.clearColor(0.0, 0.0, 0.0, 1.0)
      // 清理绘图区
      gl.clear(gl.COLOR_BUFFER_BIT)
      // 绘制顶点
      gl.drawArrays(gl.POINTS, 0, 1)

      function initShaders(gl, vsSource, fsSource) {
        //创建程序对象
        const program = gl.createProgram()
        //建立着色对象
        const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource)
        const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource)
        //把顶点着色对象装进程序对象中
        gl.attachShader(program, vertexShader)
        //把片元着色对象装进程序对象中
        gl.attachShader(program, fragmentShader)
        //连接webgl上下文对象和程序对象
        gl.linkProgram(program)
        //启动程序对象
        gl.useProgram(program)
        //将程序对象挂到上下文对象上
        gl.program = program
        return true
      }

      function loadShader(gl, type, source) {
        //根据着色类型,建立着色器对象
        const shader = gl.createShader(type)
        //将着色器源文件传入着色器对象中
        gl.shaderSource(shader, source)
        //编译着色器对象
        gl.compileShader(shader)
        //返回着色器对象
        return shader
      }
    </script>
  </body>
</html>

1-3 js向attribute 变量传参的原理

1-3-1 着色器中的attribute 变量——存储限定符

attribute vec4 a_Position;
void main(){
    gl_Position = a_Position;
    gl_PointSize = 50.0;
}

关键词attribute被称为存储限定符,它表示接下来的变量是一个attribute变量。attribute变量必须声明成全局变量,数据从着色器外部传给该变量。变量的声明必须按照以下的格式:<存储限定符> <类型> <变量名>。

image.png

  • attribute 是存储限定符,是专门用于向外部导出与点位相关的对象的,这类似于es6模板语法中export 。
  • vec4 是变量类型,vec4是4维矢量对象。
  • a_Position 是变量名,之后在js中会根据这个变量名导入变量。这个变量名是一个指针,指向实际数据的存储位置。也是说,如果在着色器外部改变了a_Position所指向的实际数据,那么在着色器中a_Position 所对应的数据也会修改。

1-3-2 在js中获取attribute 变量

在js 里不能直接写a_Position 来获取着色器中的变量。因为着色器和js 是两个不同的语种,着色器无法通过window.a_Position 原理向全局暴露变量。

要在js 里获取着色器暴露的变量,就需要找人来翻译,这个人就是程序对象gl.program

const a_Position=gl.getAttribLocation(gl.program,'a_Position');
  • gl 是webgl 的上下文对象。

  • gl.getAttribLocation() 是获取着色器中attribute 变量的方法。

  • getAttribLocation() 方法的参数中:

    • gl.program 是初始化着色器时,在上下文对象上挂载的程序对象。
    • 'a_Position' 是着色器暴露出的变量名。

这个过程简单来说就是:gl 上下文对象对program 程序对象说,你去顶点着色器里找一个名叫'a_Position' 的attribute变量。

image.png

1-3-3 在js中修改attribute 变量

attribute 变量即使在js中获取了,他也是只会说GLSL ES语言的人,并不认识js 语言,所以不能用js 的语法来修改attribute 变量的值:

a_Position.a=1.0

用特定的方法改变a_Position的值:

gl.vertexAttrib3f(a_Position,0.0,0.5,0.0);
  • gl.vertexAttrib3f() 是改变变量值的方法。

  • gl.vertexAttrib3f() 方法的参数中:

    • a_Position 就是之前获取的着色器变量。
    • 后面的3个参数是顶点的x、y、z位置

a_Position被修改后,就可以使用上下文对象绘制最新的点位了。

gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);

1.gif

1-4 webgl 函数扩展知识

1-4-1 webgl 函数的命名规律

image.png

GLSL ES里函数的命名结构是:<基础函数名><参数个数><参数类型>

以vertexAttrib3f(location,v0,v1,v2,v3) 为例:

  • vertexAttrib:基础函数名
  • 3:参数个数,这里的参数个数是要传给变量的参数个数,而不是当前函数的参数个数
  • f:参数类型,f 代表float 浮点类型,除此之外还有i 代表整型,v代表数字……

1-4-2 vertexAttrib3f()的同族函数

gl.vertexAttrib3f(location,v0,v1,v2) 方法是一系列修改着色器中的attribute 变量的方法之一,它还有许多同族方法,如:

gl.vertexAttrib1f(location,v0) 
gl.vertexAttrib2f(location,v0,v1)
gl.vertexAttrib3f(location,v0,v1,v2)
gl.vertexAttrib4f(location,v0,v1,v2,v3)

它们都可以改变attribute 变量的前n 个值。

比如 vertexAttrib1f() 方法自定一个矢量对象的v0值,v1、v2 则默认为0.0,v3默认为1.0,其数值类型为float 浮点型。

二、用鼠标控制顶点位置——对attribute 变量的操控

我们要用鼠标控制一个点的位置,首先要获取鼠标点在webgl 坐标系中的位置。

2-1 获取鼠标点在webgl 坐标系中的位置

鼠标点在webgl 坐标系中的位置,是无法直接获取的。需要先获取鼠标在canvas 这个DOM元素中的位置。

2-1-1 获取鼠标在canvas 画布中的css 位置

  • canvas 2d 坐标系的原点在左上角
  • canvas 2d 坐标系的y 轴方向是朝下的
  • canvas 2d 坐标系的坐标基底有两个分量,分别是一个像素的宽和一个像素的高,即1个单位的宽便是1个像素的宽,1个单位的高便是一个像素的高
canvas.addEventListener('click', function (event) {
    const { clientX, clientY } = event
    const { left, top } = canvas.getBoundingClientRect()
    const [cssX, cssY] = [clientX - left, clientY - top]
    console.log([cssX, cssY])
})

对于cssX,cssY 的获取,这在canvas 2d 也会用到。

1.gif

因为html 坐标系中的坐标原点和轴向与canvas 2d是一致的,所以在我们没有用css 改变画布大小,也没有对其坐标系做变换的情况下,鼠标点在canvas 画布中的css 位就是鼠标点在canvas 2d坐标系中的位置。

2-1-2-canvas 坐标系转webgl 坐标系

这里的变换思路就是解决差异,接着上面的代码来写。

  • webgl坐标系的坐标原点在画布中心
  • webgl坐标系的y 轴方向是朝上的
  • webgl坐标基底中的两个分量分别是半个canvas的宽和canvas的高,即1个单位的宽便是半个个canvas的宽,1个单位的高便是半个canvas的高。
1. 解决坐标原点位置的差异
const [halfWidth, halfHeight] = [canvas.width / 2, canvas.height / 2]
const [xBaseCenter, yBaseCenter] = [cssX - halfWidth, cssY - halfHeight]
console.log('解决坐标原点差异', [xBaseCenter, yBaseCenter])

上面的[halfWidth,halfHeight]是canvas 画布中心的位置。

[xBaseCenter,yBaseCenter] 是用鼠标位减去canvas 画布的中心位,得到的就是鼠标基于画布中心的位置。

1.gif

2.解决y 方向的差 异。
const yBaseCenterTop=-yBaseCenter;

因为webgl 里的y 轴和canvas 2d 里的y轴相反,所以对yBaseCenter 值取反即可。

3.解决坐标基底的差异。
const [x,y]=[xBaseCenter/halfWidth,yBaseCenterTop/halfHeight]

由于canvas 2d 的坐标基底中的两个分量分别是一个像素的宽高,而webgl的坐标基底的两个分量是画布的宽高,所以咱们得求个比值。

整体代码:

canvas.addEventListener('click', function (event) {
    const { clientX, clientY } = event
    const { left, top, width, height } = canvas.getBoundingClientRect()
    const [cssX, cssY] = [clientX - left, clientY - top]
    // console.log('鼠标对应的canvas坐标', [cssX, cssY])
    const [halfWidth, halfHeight] = [canvas.width / 2, canvas.height / 2]
    const [xBaseCenter, yBaseCenter] = [cssX - halfWidth, cssY - halfHeight]
    // console.log('解决坐标原点差异', [xBaseCenter, yBaseCenter])
    const yBaseCenterTop = -yBaseCenter
    const [x, y] = [xBaseCenter / halfWidth, yBaseCenterTop / halfHeight]
    // console.log('鼠标对应的webgl坐标', [x, y])
 })

1.gif

获取鼠标点在webgl 坐标系中的位置,接下来基于这个位置,修改着色器暴露出来的位置变量。

2-2 按照鼠标点击位置-修改attribute 变量

步骤:

  1. 获取attribute 变量
  2. 在获取鼠标在webgl 画布中的位置的时候,修改attribute 变量
  3. 清理画布
  4. 绘图
<!DOCTYPE html>
<html>
  <body>
    <div id="app">
      <canvas id="canvas"></canvas>
    </div>
    <!-- 顶点着色器 -->
    <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      void main(){
          //点位
          gl_Position=a_Position;
          //尺寸
          gl_PointSize=50.0;
      }
    </script>
    <!-- 片元着色器 -->
    <script id="fragmentShader" type="x-shader/x-fragment">
      void main() {
          //颜色
          gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
      }
    </script>
    <script>
      // canvas 画布
      const canvas = document.getElementById('canvas')
      canvas.width = window.innerWidth
      canvas.height = window.innerHeight
      // webgl画笔
      const gl = canvas.getContext('webgl')
      // 顶点着色器
      const vsSource = document.getElementById('vertexShader').innerText
      // const vsSource = `attribute vec4 a_Position;
      // void main(){
      //     //点位
      //     gl_Position=a_Position;
      //     //尺寸
      //     gl_PointSize=50.0;
      // }`
      // 片元着色器
      const fsSource = document.getElementById('fragmentShader').innerText
      // 初始化着色器
      initShaders(gl, vsSource, fsSource)

      // 在js中获取attribute 变量
      const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
      // 修改attribute 变量
      gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0)
      // gl.vertexAttrib3f(a_Position, 0.0, 0.5, 0.0)

      // 指定将要用来清理绘图区的颜色
      gl.clearColor(0.0, 0.0, 0.0, 1.0)
      // 清理绘图区
      gl.clear(gl.COLOR_BUFFER_BIT)
      // 绘制顶点
      gl.drawArrays(gl.POINTS, 0, 1)

      function initShaders(gl, vsSource, fsSource) {
        //创建程序对象
        const program = gl.createProgram()
        //建立着色对象
        const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource)
        const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource)
        //把顶点着色对象装进程序对象中
        gl.attachShader(program, vertexShader)
        //把片元着色对象装进程序对象中
        gl.attachShader(program, fragmentShader)
        //连接webgl上下文对象和程序对象
        gl.linkProgram(program)
        //启动程序对象
        gl.useProgram(program)
        //将程序对象挂到上下文对象上
        gl.program = program
        return true
      }

      function loadShader(gl, type, source) {
        //根据着色类型,建立着色器对象
        const shader = gl.createShader(type)
        //将着色器源文件传入着色器对象中
        gl.shaderSource(shader, source)
        //编译着色器对象
        gl.compileShader(shader)
        //返回着色器对象
        return shader
      }
      canvas.addEventListener('click', function (event) {
        const { clientX, clientY } = event
        const { left, top, width, height } = canvas.getBoundingClientRect()
        const [cssX, cssY] = [clientX - left, clientY - top]
        // console.log('鼠标对应的canvas坐标', [cssX, cssY])
        const [halfWidth, halfHeight] = [canvas.width / 2, canvas.height / 2]
        const [xBaseCenter, yBaseCenter] = [cssX - halfWidth, cssY - halfHeight]
        // console.log('解决坐标原点差异', [xBaseCenter, yBaseCenter])
        const yBaseCenterTop = -yBaseCenter
        const [x, y] = [xBaseCenter / halfWidth, yBaseCenterTop / halfHeight]
        console.log('鼠标对应的webgl坐标', [x, y])
        gl.vertexAttrib2f(a_Position, x, y)
        gl.clear(gl.COLOR_BUFFER_BIT)
        gl.drawArrays(gl.POINTS, 0, 1)
      })
    </script>
  </body>
</html>

1.gif

在上面的例子中,每点击一次canvas 画布,都会画出一个点,而上一次画的点就会消失,我们无法连续画出多个点。

2-3 webgl 的同步绘图原理——Js异步进程中之前绘制的图像会清空

可能会认为无法画出多点是gl.clear(gl.COLOR_BUFFER_BIT) 清理画布导致,因为我们在用canvas 2d 做动画时,其中就有一个ctx.clearRect() 清理画布的方法。

2-3-1 WebGLRenderingContext.drawArrays()

WebGL API 中的 WebGLRenderingContext.drawArrays()  方法用于从向量数组中绘制图元。

gl.drawArrays(mode, first, count);
  • mode 指定绘制图元的方式,可能值如下。

    • gl.POINTS: 绘制一系列点。
    • gl.LINE_STRIP: 绘制一个线条。即,绘制一系列线段,上一点连接下一点。
    • gl.LINE_LOOP: 绘制一个线圈。即,绘制一系列线段,上一点连接下一点,并且最后一点与第一个点相连。
    • gl.LINES: 绘制一系列单独线段。每两个点作为端点,线段之间不连接。
    • gl.TRIANGLE_STRIP:绘制一个三角带
    • gl.TRIANGLE_FAN:绘制一个三角扇
    • gl.TRIANGLES: 绘制一系列三角形。每三个点作为顶点。
  • first 指定从哪个点开始绘制。

  • count 指定绘制需要使用到多少个点。

2-3-2 试一试——将gl.clear() 方法注释掉

gl.vertexAttrib2f(a_Position,x,y);
//gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);

当鼠标点击画布时,画布中原本的黑色已经没有了,而且我们每次也只能画一个点。

1.gif

gl.drawArrays(gl.POINTS, 0, 1) 方法和canvas 2d 里的ctx.draw() 方法是不一样的,ctx.draw() 真的像画画一样,一层一层的覆盖图像。

gl.drawArrays() 方法只会同步绘图,走完了js 主线程后,再次绘图时,就会从头再来。也就说,异步执行的drawArrays() 方法会把画布上的图像都刷掉。

举个例子:

  1. 先画两个点
const a_Position=gl.getAttribLocation(gl.program,'a_Position');
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.vertexAttrib2f(a_Position,0.1,0);
gl.drawArrays(gl.POINTS, 0, 1);
gl.vertexAttrib2f(a_Position,-0.1,0);
gl.drawArrays(gl.POINTS, 0, 1);

image.png

image.png

  1. 一秒后,再画一个点。
const a_Position=gl.getAttribLocation(gl.program,'a_Position');
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.vertexAttrib2f(a_Position,0.1,0);
gl.drawArrays(gl.POINTS, 0, 1);
gl.vertexAttrib2f(a_Position,-0.1,0);
gl.drawArrays(gl.POINTS, 0, 1);
setTimeout(()=>{
  gl.vertexAttrib2f(a_Position,0,0);
  gl.drawArrays(gl.POINTS, 0, 1);
},1000)

image.png

1.gif

以前画好的两个点没了,黑色背景也没了。这就是webgl 同步绘图原理。

  1. 用数组把一开始的那两个顶点存起来,在异步绘制第3个顶点的时候,把那两个顶点也一起画上。
const a_Position=gl.getAttribLocation(gl.program,'a_Position');
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
const g_points=[  {x:0.1,y:0},  {x:-0.1,y:0},];
render();
setTimeout(()=>{
  g_points.push({x:0,y:0});
  render();
},1000)
function render(){
  gl.clear(gl.COLOR_BUFFER_BIT);
  g_points.forEach(({x,y})=>{
    gl.vertexAttrib2f(a_Position,x,y);
    gl.drawArrays(gl.POINTS, 0, 1);
  })
}    

image.png

1.gif

这样就可以以叠加覆盖的方式画出第三个点了。

4.理解上面的原理后,就可以用鼠标绘制多个点了。

1.gif

<!DOCTYPE html>
<html>
  <body>
    <div id="app">
      <canvas id="canvas"></canvas>
    </div>
    <!-- 顶点着色器 -->
    <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      void main(){
          //顶点位置
          gl_Position=a_Position;
          //顶点尺寸大小
          gl_PointSize=50.0;
      }
    </script>
    <!-- 片元着色器 -->
    <script id="fragmentShader" type="x-shader/x-fragment">
      void main() {
          //颜色
          gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
      }
    </script>
    <script>
      // canvas 画布
      const canvas = document.getElementById('canvas')
      canvas.width = window.innerWidth
      canvas.height = window.innerHeight
      // webgl画笔
      const gl = canvas.getContext('webgl')
      // 顶点着色器
      const vsSource = document.getElementById('vertexShader').innerText
      // const vsSource = `attribute vec4 a_Position;
      // void main(){
      //     //点位
      //     gl_Position=a_Position;
      //     //尺寸
      //     gl_PointSize=50.0;
      // }`
      // 片元着色器
      const fsSource = document.getElementById('fragmentShader').innerText
      // 初始化着色器
      initShaders(gl, vsSource, fsSource)
      // 在js中获取attribute 变量
      const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
      // 修改attribute 变量
      // gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0)
      // gl.vertexAttrib3f(a_Position, 0.0, 0.5, 0.0)
      // 指定将要用来清理绘图区的颜色
      gl.clearColor(0.0, 0.0, 0.0, 1.0)
      // 清理绘图区
      gl.clear(gl.COLOR_BUFFER_BIT)
      // 绘制顶点
      //gl.drawArrays(gl.POINTS, 0, 1)

      function initShaders(gl, vsSource, fsSource) {
        //创建程序对象
        const program = gl.createProgram()
        //建立着色对象
        const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource)
        const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource)
        //把顶点着色对象装进程序对象中
        gl.attachShader(program, vertexShader)
        //把片元着色对象装进程序对象中
        gl.attachShader(program, fragmentShader)
        //连接webgl上下文对象和程序对象
        gl.linkProgram(program)
        //启动程序对象
        gl.useProgram(program)
        //将程序对象挂到上下文对象上
        gl.program = program
        return true
      }

      function loadShader(gl, type, source) {
        //根据着色类型,建立着色器对象
        const shader = gl.createShader(type)
        //将着色器源文件传入着色器对象中
        gl.shaderSource(shader, source)
        //编译着色器对象
        gl.compileShader(shader)
        //返回着色器对象
        return shader
      }
      const g_points = []
      canvas.addEventListener('click', function (event) {
        const { clientX, clientY } = event
        const { left, top, width, height } = canvas.getBoundingClientRect()
        const [cssX, cssY] = [clientX - left, clientY - top]
        // console.log('鼠标对应的canvas坐标', [cssX, cssY])
        const [halfWidth, halfHeight] = [canvas.width / 2, canvas.height / 2]
        const [xBaseCenter, yBaseCenter] = [cssX - halfWidth, cssY - halfHeight]
        // console.log('解决坐标原点差异', [xBaseCenter, yBaseCenter])
        const yBaseCenterTop = -yBaseCenter
        const [x, y] = [xBaseCenter / halfWidth, yBaseCenterTop / halfHeight]
        console.log('鼠标对应的webgl坐标', [x, y])
        // webgl同步绘制多个点
        g_points.push({ x, y })
        gl.clear(gl.COLOR_BUFFER_BIT)
        g_points.forEach(({ x, y }) => {
          gl.vertexAttrib2f(a_Position, x, y)
          gl.drawArrays(gl.POINTS, 0, 1)
        })
      })
    </script>
  </body>
</html>

2-3-2 webgl 同步绘图原理介绍

webgl 的同步绘图的现象,其实是由webgl 底层内置的颜色缓冲区导致的。

  • 这个颜色缓冲区,在电脑里会占用一块内存。在使用webgl 绘图的时候,是先在颜色缓冲区中画出来,这样的图像,外人看不见,只有webgl系统自己知道
  • 在我们想要将图像显示出来的时候,那就照着颜色缓冲区中的图像去画,这个步骤是webgl 内部自动完成的,我们只要执行绘图命令即可。
  • 颜色缓冲区中存储的图像,只在当前线程有效。比如我们先在js 主线程中绘图,主线程结束后,会再去执行信息队列里的异步线程。在执行异步线程时,颜色缓冲区就会被webgl 系统重置,我们曾经在主线程里的图像也就没了,也就画不出那时的图像了

2-4 用js控制顶点尺寸大小——对attribute 变量的操控

用js 控制顶点尺寸的方法和控制顶点位置的方法是一样的。

1.首先还是在着色器里暴露出一个可以控制顶点尺寸的attribute 变量。

<script id="vertexShader" type="x-shader/x-vertex">
    attribute vec4 a_Position;
    attribute float a_PointSize;
    void main(){
        gl_Position = a_Position;
        gl_PointSize = a_PointSize;
    }
</script>

上面的a_PointSize 是一个浮点类型的变量。

2.在js 里获取attribute 变量

const a_PointSize=gl.getAttribLocation(gl.program,'a_PointSize');

3.修改attribute 变量

gl.vertexAttrib1f(a_PointSize,100.0);

2-4-1 改变顶点大小

整体代码:

const a_Position=gl.getAttribLocation(gl.program,'a_Position');
const a_PointSize=gl.getAttribLocation(gl.program,'a_PointSize');
gl.vertexAttrib3f(a_Position,0.0,0.0,0.0);
// 顶点大小改成100
gl.vertexAttrib1f(a_PointSize,100.0);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);

image.png

2-4-2 用鼠标随机改变顶点大小

1.gif

<!DOCTYPE html>
<html>
  <body>
    <div id="app">
      <canvas id="canvas"></canvas>
    </div>
    <!-- 顶点着色器 -->
    <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      attribute float a_PointSize;
      void main(){
          // 顶点位置
          gl_Position=a_Position;
          // 顶点尺寸大小
          gl_PointSize=a_PointSize;
      }
    </script>
    <!-- 片元着色器 -->
    <script id="fragmentShader" type="x-shader/x-fragment">
      void main() {
          //颜色
          gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
      }
    </script>
    <script>
      // canvas 画布
      const canvas = document.getElementById('canvas')
      canvas.width = window.innerWidth
      canvas.height = window.innerHeight
      // webgl画笔
      const gl = canvas.getContext('webgl')
      // 顶点着色器
      const vsSource = document.getElementById('vertexShader').innerText
      // const vsSource = `attribute vec4 a_Position;
      // void main(){
      //     //点位
      //     gl_Position=a_Position;
      //     //尺寸
      //     gl_PointSize=50.0;
      // }`
      // 片元着色器
      const fsSource = document.getElementById('fragmentShader').innerText
      // 初始化着色器
      initShaders(gl, vsSource, fsSource)
      // 在js中获取attribute 变量
      const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
      const a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize')
      // 修改attribute 变量
      // gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0)
      // gl.vertexAttrib3f(a_Position, 0.0, 0.5, 0.0)
      // 指定将要用来清理绘图区的颜色
      gl.clearColor(0.0, 0.0, 0.0, 1.0)
      // 清理绘图区
      gl.clear(gl.COLOR_BUFFER_BIT)
      // 绘制顶点
      //gl.drawArrays(gl.POINTS, 0, 1)

      function initShaders(gl, vsSource, fsSource) {
        //创建程序对象
        const program = gl.createProgram()
        //建立着色对象
        const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource)
        const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource)
        //把顶点着色对象装进程序对象中
        gl.attachShader(program, vertexShader)
        //把片元着色对象装进程序对象中
        gl.attachShader(program, fragmentShader)
        //连接webgl上下文对象和程序对象
        gl.linkProgram(program)
        //启动程序对象
        gl.useProgram(program)
        //将程序对象挂到上下文对象上
        gl.program = program
        return true
      }

      function loadShader(gl, type, source) {
        //根据着色类型,建立着色器对象
        const shader = gl.createShader(type)
        //将着色器源文件传入着色器对象中
        gl.shaderSource(shader, source)
        //编译着色器对象
        gl.compileShader(shader)
        //返回着色器对象
        return shader
      }
      const g_points = []
      canvas.addEventListener('click', function (event) {
        const { clientX, clientY } = event
        const { left, top, width, height } = canvas.getBoundingClientRect()
        const [cssX, cssY] = [clientX - left, clientY - top]
        // console.log('鼠标对应的canvas坐标', [cssX, cssY])
        const [halfWidth, halfHeight] = [canvas.width / 2, canvas.height / 2]
        const [xBaseCenter, yBaseCenter] = [cssX - halfWidth, cssY - halfHeight]
        // console.log('解决坐标原点差异', [xBaseCenter, yBaseCenter])
        const yBaseCenterTop = -yBaseCenter
        const [x, y] = [xBaseCenter / halfWidth, yBaseCenterTop / halfHeight]
        console.log('鼠标对应的webgl坐标', [x, y])
        // webgl同步绘制多个不同尺寸大小的点
        g_points.push({ x, y, z: Math.random() * 50 })
        gl.clear(gl.COLOR_BUFFER_BIT)
        g_points.forEach(({ x, y, z }) => {
          gl.vertexAttrib2f(a_Position, x, y)
          gl.vertexAttrib1f(a_PointSize, z)
          gl.drawArrays(gl.POINTS, 0, 1)
        })
      })
    </script>
  </body>
</html>

在上面的案例中,无论是控制顶点的尺寸大小,还是控制顶点的位置,实际上都是对attribute 变量的操控。

不过如果想要再改变顶点的颜色,那就不能再用attribute 限定符了

三 用js 控制顶点的颜色

限定颜色变量的限定符叫uniform。uniform 翻译过来是一致、统一的意思。

3-1 用js 控制顶点颜色的步骤

1.在片元着色器里把控制顶点颜色的变量暴露出来。

<script id="fragmentShader" type="x-shader/x-fragment">
    precision mediump float;
    uniform vec4 u_FragColor;
    void main() {
        gl_FragColor = u_FragColor;
    }
</script>

上面的uniform 也是限定符,vec4 是4维的变量类型,u_FragColor 就是变量名。

注意,第一行的precision mediump float 是对浮点数精度的定义,mediump 是中等精度的意思,这个必须要有,不然画不出东西来。

2.在js 中获取片元着色器暴露出的uniform 变量

const u_FragColor=gl.getUniformLocation(gl.program,'u_FragColor');

上面的getUniformLocation() 方法就是用于获取片元着色器暴露出的uniform 变量的,其第一个参数是程序对象,第二个参数是变量名。这里的参数结构和获取attribute 变量的getAttributeLocation() 方法是一样的。

3.修改uniform 变量

gl.uniform4f(u_FragColor,1.0,1.0,0.0,1.0);

用js 控制顶点的颜色的基本步骤,其整体思路和控制顶点位置是一样的。

3-1-1 用js 控制单个顶点的颜色

const a_Position=gl.getAttribLocation(gl.program,'a_Position');
const a_PointSize=gl.getAttribLocation(gl.program,'a_PointSize');
const u_FragColor=gl.getUniformLocation(gl.program,'u_FragColor');
gl.vertexAttrib3f(a_Position,0.0,0.0,0.0);
gl.vertexAttrib1f(a_PointSize,100.0);
// 改变顶点的颜色
gl.uniform4f(u_FragColor,1.0,1.0,0.0,1.0);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);

image.png

3-1-2 同样可以用鼠标随机改变顶点的颜色。

1.gif

<!DOCTYPE html>
<html>
  <body>
    <div id="app">
      <canvas id="canvas"></canvas>
    </div>
    <!-- 顶点着色器 -->
    <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      attribute float a_PointSize;
      void main(){
          // 顶点位置
          gl_Position=a_Position;
          // 顶点尺寸大小
          gl_PointSize=a_PointSize;
      }
    </script>
    <!-- 片元着色器 -->
    <script id="fragmentShader" type="x-shader/x-fragment">
      precision mediump float;
      uniform vec4 u_FragColor;
      void main() {
          gl_FragColor = u_FragColor;
      }
    </script>
    <script>
      // canvas 画布
      const canvas = document.getElementById('canvas')
      canvas.width = window.innerWidth
      canvas.height = window.innerHeight
      // webgl画笔
      const gl = canvas.getContext('webgl')
      // 顶点着色器
      const vsSource = document.getElementById('vertexShader').innerText
      // const vsSource = `attribute vec4 a_Position;
      // void main(){
      //     //点位
      //     gl_Position=a_Position;
      //     //尺寸
      //     gl_PointSize=50.0;
      // }`
      // 片元着色器
      const fsSource = document.getElementById('fragmentShader').innerText
      // 初始化着色器
      initShaders(gl, vsSource, fsSource)
      // 在js中获取attribute 变量
      const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
      const a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize')
      // 获取颜色变量
      const u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor')
      // 修改attribute 变量
      // gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0)
      // gl.vertexAttrib3f(a_Position, 0.0, 0.5, 0.0)
      // 指定将要用来清理绘图区的颜色
      gl.clearColor(0.0, 0.0, 0.0, 1.0)
      // 清理绘图区
      gl.clear(gl.COLOR_BUFFER_BIT)
      // 绘制顶点
      //gl.drawArrays(gl.POINTS, 0, 1)

      function initShaders(gl, vsSource, fsSource) {
        //创建程序对象
        const program = gl.createProgram()
        //建立着色对象
        const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource)
        const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource)
        //把顶点着色对象装进程序对象中
        gl.attachShader(program, vertexShader)
        //把片元着色对象装进程序对象中
        gl.attachShader(program, fragmentShader)
        //连接webgl上下文对象和程序对象
        gl.linkProgram(program)
        //启动程序对象
        gl.useProgram(program)
        //将程序对象挂到上下文对象上
        gl.program = program
        return true
      }

      function loadShader(gl, type, source) {
        //根据着色类型,建立着色器对象
        const shader = gl.createShader(type)
        //将着色器源文件传入着色器对象中
        gl.shaderSource(shader, source)
        //编译着色器对象
        gl.compileShader(shader)
        //返回着色器对象
        return shader
      }
      const g_points = []
      canvas.addEventListener('click', function (event) {
        const { clientX, clientY } = event
        const { left, top, width, height } = canvas.getBoundingClientRect()
        const [cssX, cssY] = [clientX - left, clientY - top]
        // console.log('鼠标对应的canvas坐标', [cssX, cssY])
        const [halfWidth, halfHeight] = [canvas.width / 2, canvas.height / 2]
        const [xBaseCenter, yBaseCenter] = [cssX - halfWidth, cssY - halfHeight]
        // console.log('解决坐标原点差异', [xBaseCenter, yBaseCenter])
        const yBaseCenterTop = -yBaseCenter
        const [x, y] = [xBaseCenter / halfWidth, yBaseCenterTop / halfHeight]
        console.log('鼠标对应的webgl坐标', [x, y])
        // webgl同步绘制多个不同尺寸大小、不同颜色的点
        const color = new Float32Array([
          Math.random(),
          Math.random(),
          Math.random(),
          1.0
        ])
        g_points.push({ x, y, z: Math.random() * 50, color })
        gl.clear(gl.COLOR_BUFFER_BIT)
        g_points.forEach(({ x, y, z, color }) => {
          gl.vertexAttrib2f(a_Position, x, y)
          gl.vertexAttrib1f(a_PointSize, z)
          gl.uniform4fv(u_FragColor, color)
          gl.drawArrays(gl.POINTS, 0, 1)
        })
      })
    </script>
  </body>
</html>

在上面的代码中,使用uniform4fv() 修改的顶点颜色

3-2 uniform4fv() 方法

在改变uniform 变量的时候,既可以用uniform4f() 方法一个个的写参数,也可以用uniform4fv() 方法传递类型数组。

  • uniform4f 中,4 是有4个数据,f 是float 浮点类型,在上面的例子里就是r、g、b、a 这四个颜色数据。
  • uniform4fv 中,4f 的意思和上面一样,v 是vector 矢量的意思,这在数学里就是向量的意思。由之前的4f 可知,这个向量由4个浮点类型的分量构成。

在修改uniform变量的时候,这两种写法是一样的:

gl.uniform4f(u_FragColor,1.0,1.0,0.0,1.0);
//等同于
const color=new Float32Array([1.0,1.0,0.0,1.0]);
gl.uniform4fv(u_FragColor,color);

image.png

  • uniform4f() 和uniform4fv() 也有着自己的同族方法,其中的4 可以变成1/2/3。
  • uniform4fv() 方法的第二个参数必须是Float32Array 数组,不能使用普通的Array 对象
  • Float32Array 是一种32 位的浮点型数组,它在浏览器中的运行效率要比普通的Array 高很多。

四 案例-用鼠标绘制星空

4-1 用鼠标绘制圆形的顶点

星星的形状是圆形的,绘制一个圆形的顶点。

gl.POINTS绘制模式 顶点默认渲染效果是方形区域,通过下面片元着色器代码设置可以把默认渲染效果更改为圆形区域。

<script id="fragmentShader" type="x-shader/x-fragment">
    precision mediump float; // 所有float类型数据的精度是mediump
    uniform vec4 u_FragColor;
    void main() {
        // 计算方形区域每个片元距离方形几何中心的距离 
        // gl.POINTS模式点渲染的方形区域,方形中心是0.5,0.5,左上角是坐标原点,右下角是1.0,1.0,
        float dist = distance(gl_PointCoord, vec2(0.5, 0.5));
        if(dist < 0.5) {
            // 方形区域片元距离几何中心半径小于0.5,设置像素颜色
            gl_FragColor = u_FragColor;
        } else {
            discard; // 方形区域距离几何中心半径不小于0.5的片元剪裁舍弃掉
        }
    }
</script>
  • distance(p1,p2) 计算两个点位的距离
  • gl_PointCoord 内置变量
  • discard 丢弃片元

image.png

4-2 绘制随机透明度的星星

先给canvas 一个星空背景

sky.jpg

#canvas {
    background: url(".sky.jpg");
    background-size: cover;
    background-position: right bottom;
}

刷底色的时候给一个透明的底色,这样才能看见canvas的css背景

gl.clearColor(0, 0, 0, 0);

接下来图形的透明度作为变量:

const arr = new Float32Array([0.87, 0.91, 1, a]);
gl.uniform4fv(u_FragColor, arr);

开启片元的颜色合成功能

gl.enable(gl.BLEND)

设置片元的合成方式

gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)

4-3 制作闪烁的繁星

当星星会眨眼睛,会变得灵动而可爱。

4-3-1 建立补间动画的意识

画一颗星星,加几个关键帧,让它眨一下眼睛。

在这里会涉及以下概念:

  • 合成:多个时间轨的集合
  • 时间轨:通过关键帧,对其中目标对象的状态进行插值计算
  • 补间动画:通过两个关键帧,对一个对象在这两个关键帧之间的状态进行插值计算,从而实现这个对象在两个关键帧间的平滑过渡

4-3-2 架构代码

1.建立合成对象

export default class Compose{
  constructor() {
    this.parent = null // parent 父对象,合成对象可以相互嵌套
    this.children = [] // children 子对象集合,其集合元素可以是时间轨,也可以是合成对象
  }
  // add(obj) 添加子对象方法
  add(obj) {
    obj.parent = this
    this.children.push(obj)
  }
  // update(t) 基于当前时间更新子对象状态的方法
  update(t) {
    this.children.forEach((ele) => {
      ele.update(t)
    })
  }
}

2.建立时间轨

export default class Track{
    constructor(target){
        this.target = target // target 时间轨上的目标对象
        this.parent = null // parent 父对象,只能是合成对象
        this.start = 0 // start 起始时间,即时间轨的建立时间
        this.timeLen = 5 // 时间轨总时长
        this.loop = false // 是否循环
        this.keyMap = new Map() // keyMap 关键帧集合
    }
    // update(t) 基于当前时间更新目标对象的状态。
    update(t) {
        const { keyMap, timeLen, target, loop, start } = this
        //先计算本地时间,即世界时间相对于时间轨起始时间的的时间。
        let time = t - start
        // 若时间轨循环播放,则本地时间基于时间轨长度取余。
        if (loop) {
          time = time % timeLen
        }
        // 遍历关键帧集合:
        // -   若本地时间小于第一个关键帧的时间,目标对象的状态等于第一个关键帧的状态
        // -   若本地时间大于最后一个关键帧的时间,目标对象的状态等于最后一个关键帧的状态
        // -   否则,计算本地时间在左右两个关键帧之间对应的补间状态
        for (const [key, fms] of keyMap) {
          const last = fms.length - 1
          if (time < fms[0][0]) {
            target[key] = fms[0][1]
          } else if (time > fms[last][0]) {
            target[key] = fms[last][1]
          } else {
            target[key] = getValBetweenFms(time, fms, last)
          }
        }
    }
}

属性

  • keyMap 关键帧集合,结构如下:

image.png

  1. 获取两个关键帧之间补间状态的方法
function getValBetweenFms(time,fms,last){
    for(let i=0;i<last;i++){
        const fm1=fms[i]
        const fm2=fms[i+1]
        if(time>=fm1[0]&&time<=fm2[0]){
            const delta={
                x:fm2[0]-fm1[0],
                y:fm2[1]-fm1[1],
            }
            const k=delta.y/delta.x
            const b=fm1[1]-fm1[0]*k
            return k*time+b
        }
    }
}
  • getValBetweenFms(time,fms,last)

    • time 本地时间
    • fms 某个属性的关键帧集合
    • last 最后一个关键帧的索引位置

    其实现思路如下:

    • 遍历所有关键帧
    • 判断当前时间在哪两个关键帧之间
    • 基于这两个关键帧的时间和状态,求点斜式
    • 基于点斜式求本地时间对应的状态

4-3-3 使用合成对象和轨道对象制作补间动画

  1. 建立动画相关的对象
const compose=new Compose()
const stars=[]
canvas.addEventListener('click',function(event){
    const {x,y}=getPosByMouse(event,canvas)
    const a=1
    const s=Math.random()*5+2
    const obj={x,y,s,a}
    stars.push(obj)

    const track=new Track(obj)
    track.start=new Date()
    track.keyMap=new Map([
        ['a',[
            [500,a],
            [1000,0],
            [1500,a],
        ]]
    ])
    track.timeLen=2000
    track.loop=true
    compose.add(track)
})
  • compose 合成对象的实例化
  • stars 存储顶点数据的集合
  • track 时间轨道对象的实例化

2.用请求动画帧驱动动画,连续更新数据,渲染视图。

!(function ani(){
    compose.update(new Date())
    render()
    requestAnimationFrame(ani)
})()

渲染方法如下:

function render(){
    gl.clear(gl.COLOR_BUFFER_BIT);
    stars.forEach(({x,y,s,a})=>{
        gl.vertexAttrib2f(a_Position,x,y);
        gl.vertexAttrib1f(a_PointSize,s);
        gl.uniform4fv(u_FragColor,new Float32Array([0.87,0.92,1,a]));
        gl.drawArrays(gl.POINTS, 0, 1);
    })
}

4-4 案例效果

1.gif

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>星星眨眼</title>
    <style>
      body {
        margin: 0;
        overflow: hidden;
      }

      #canvas {
        background: url('./sky.jpg');
        background-size: cover;
        background-position: right bottom;
      }
    </style>
  </head>

  <body>
    <canvas id="canvas"></canvas>

    <!-- 顶点着色器 -->
    <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      attribute float a_PointSize;
      void main(){
          //顶点位置
          gl_Position=a_Position;
          //顶点大小
          gl_PointSize=a_PointSize;
      }
    </script>
    <!-- 片元着色器 -->
    <script id="fragmentShader" type="x-shader/x-fragment">
      precision mediump float; // 所有float类型数据的精度是mediump
      uniform vec4 u_FragColor;
      void main() {
          // 计算方形区域每个片元距离方形几何中心的距离
          // gl.POINTS模式点渲染的方形区域,方形中心是0.5,0.5,左上角是坐标原点,右下角是1.0,1.0,
          float dist = distance(gl_PointCoord, vec2(0.5, 0.5));
          if(dist < 0.5) {
              // 方形区域片元距离几何中心半径小于0.5,设置像素颜色
              gl_FragColor = u_FragColor;
          } else {
              discard; // 方形区域距离几何中心半径不小于0.5的片元剪裁舍弃掉
          }
      }
    </script>
    <script type="module">
      import { initShaders, Compose, Track } from './Utils.js'

      const canvas = document.querySelector('#canvas')
      canvas.width = window.innerWidth
      canvas.height = window.innerHeight

      // 获取着色器文本
      const vsSource = document.querySelector('#vertexShader').innerText
      const fsSource = document.querySelector('#fragmentShader').innerText

      //三维画笔
      const gl = canvas.getContext('webgl')
      gl.enable(gl.BLEND) // 开启片元的颜色合成功能
      gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) // 设置片元的合成方式

      //初始化着色器
      initShaders(gl, vsSource, fsSource)

      //设置attribute 变量
      // a_Position=vec4(1,0,0,1)
      const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
      const a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize')
      const u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor')

      const stars = []

      //建立合成对象
      const compose = new Compose()

      //给一个透明的底色,这样才能看见canvas的css背景
      gl.clearColor(0, 0, 0, 0)
      //刷底色
      gl.clear(gl.COLOR_BUFFER_BIT)

      //绘制顶点
      render()

      // 鼠标点击事件
      canvas.addEventListener('click', ({ clientX, clientY }) => {
        console.log(clientX, clientY)
        const { left, top, width, height } = canvas.getBoundingClientRect()
        const [cssX, cssY] = [clientX - left, clientY - top]

        //解决坐标原点位置的差异
        const [halfWidth, halfHeight] = [width / 2, height / 2]
        const [xBaseCenter, yBaseCenter] = [cssX - halfWidth, cssY - halfHeight]
        // 解决y 方向的差异
        const yBaseCenterTop = -yBaseCenter
        //解决坐标基底的差异
        const [x, y] = [xBaseCenter / halfWidth, yBaseCenterTop / halfHeight]
        // 星星大小2-7
        const s = Math.random() * 5 + 2
        const a = 1
        const obj = { x, y, s, a }
        stars.push(obj)

        //建立轨道对象
        const track = new Track(obj)
        track.start = new Date()
        track.timeLen = 2000
        track.loop = true
        track.keyMap = new Map([
          [
            'a',
            [
              [500, a],
              [1000, 0],
              [1500, a]
            ]
          ]
        ])
        compose.add(track)

        // render();
      })

      !(function ani() {
        compose.update(new Date())
        render()
        requestAnimationFrame(ani)
      })()

      // 渲染方法
      function render() {
        gl.clear(gl.COLOR_BUFFER_BIT)
        stars.forEach(({ x, y, s, a }) => {
          gl.vertexAttrib2f(a_Position, x, y)
          gl.vertexAttrib1f(a_PointSize, s)
          const arr = new Float32Array([0.87, 0.91, 1, a])
          gl.uniform4fv(u_FragColor, arr)
          gl.drawArrays(gl.POINTS, 0, 1)
        })
      }
    </script>
  </body>
</html>
// ./Utils.js
function initShaders(gl, vsSource, fsSource) {
  //创建程序对象
  const program = gl.createProgram()
  //建立着色对象
  const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource)
  const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource)
  //把顶点着色对象装进程序对象中
  gl.attachShader(program, vertexShader)
  //把片元着色对象装进程序对象中
  gl.attachShader(program, fragmentShader)
  //连接webgl上下文对象和程序对象
  gl.linkProgram(program)
  //启动程序对象
  gl.useProgram(program)
  //将程序对象挂到上下文对象上
  gl.program = program
  return true
}
function loadShader(gl, type, source) {
  //根据着色类型,建立着色器对象
  const shader = gl.createShader(type)
  //将着色器源文件传入着色器对象中
  gl.shaderSource(shader, source)
  //编译着色器对象
  gl.compileShader(shader)
  //返回着色器对象
  return shader
}
//建立合成对象
class Compose {
  constructor() {
    this.parent = null // parent 父对象,合成对象可以相互嵌套
    this.children = [] // children 子对象集合,其集合元素可以是时间轨,也可以是合成对象
  }
  // add(obj) 添加子对象方法
  add(obj) {
    obj.parent = this
    this.children.push(obj)
  }
  // update(t) 基于当前时间更新子对象状态的方法
  update(t) {
    this.children.forEach((ele) => {
      ele.update(t)
    })
  }
}
// 建立时间轨
class Track {
  constructor(target) {
    this.target = target // target 时间轨上的目标对象
    this.parent = null // parent 父对象,只能是合成对象
    this.start = 0 // start 起始时间,即时间轨的建立时间
    this.timeLen = 5 // 时间轨总时长
    this.loop = false // 是否循环
    this.keyMap = new Map() // keyMap 关键帧集合
  }
  // update(t) 基于当前时间更新目标对象的状态。
  update(t) {
    const { keyMap, timeLen, target, loop, start } = this
    //先计算本地时间,即世界时间相对于时间轨起始时间的的时间。
    let time = t - start
    // 若时间轨循环播放,则本地时间基于时间轨长度取余。
    if (loop) {
      time = time % timeLen
    }

    // 遍历关键帧集合:
    // -   若本地时间小于第一个关键帧的时间,目标对象的状态等于第一个关键帧的状态
    // -   若本地时间大于最后一个关键帧的时间,目标对象的状态等于最后一个关键帧的状态
    // -   否则,计算本地时间在左右两个关键帧之间对应的补间状态
    for (const [key, fms] of keyMap) {
      const last = fms.length - 1
      if (time < fms[0][0]) {
        target[key] = fms[0][1]
      } else if (time > fms[last][0]) {
        target[key] = fms[last][1]
      } else {
        target[key] = getValBetweenFms(time, fms, last)
      }
    }
  }
}

function getValBetweenFms(time, fms, last) {
  for (let i = 0; i < last; i++) {
    const fm1 = fms[i]
    const fm2 = fms[i + 1]
    if (time >= fm1[0] && time <= fm2[0]) {
      const delta = {
        x: fm2[0] - fm1[0],
        y: fm2[1] - fm1[1]
      }
      const k = delta.y / delta.x
      const b = fm1[1] - fm1[0] * k
      return k * time + b
    }
  }
}

export { initShaders, Track, Compose }

使用js 与着色器间的数据传输,从而去动态控制顶点的位置、大小和颜色,这是webgl 绘图的基础。