{T}

JS 与着色器之间的数据传输

用 js 控制一个点的位置

attribute 变量的概念

gl_Position = vec4(0,0,0,1);这是一种将数据写死了的硬编码,缺乏可扩展性

要让这个点位可以动态改变,那就得把它变成 attribute 变量,attribute 变量是只有顶点着色器才能使用它的

js 可以通过 attribute 变量向顶点着色器传递与顶点相关的数据

JS 向 attribute 变量传参的步骤

  1. 在顶点着色器中声明 attribute 变量
javascript
<script id="vertexShader" type="x-shader/x-vertex">
     attribute vec4 a_Position;
     void main(){
         gl_Position = a_Position;
         gl_PointSize = 50.0;
     }
 </script>
// 在 js 中获取 attribute 变量
const a_Position=gl.getAttribLocation(gl.program,'a_Position');

// 修改 attribute 变量
gl.vertexAttrib3f(a_Position,0.0,0.5,0.0);
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>js改变点位</title>
    <style>
      body {
        margin: 0;
        overflow: hidden;
      }

      #canvas {
        background-color: antiquewhite;
      }
    </style>
  </head>

  <body>
    <canvas id="canvas"></canvas>
    <!-- 顶点着色器 -->
    <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,1,0,1);
      }
    </script>
    <script type="module">
      import { initShaders } from "../jsm/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")

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

      // 声明颜色 rgba
      gl.clearColor(0, 0, 0, 1)
      // 刷底色
      gl.clear(gl.COLOR_BUFFER_BIT)

      // 获取 attribute 变量
      const a_Position = gl.getAttribLocation(gl.program, "a_Position")

      // 修改 attribute 变量
      // gl.vertexAttrib3f(a_Position, 0, 1, 0);
      // gl.vertexAttrib2f(a_Position, 0.5, 0.5);
      gl.vertexAttrib1f(a_Position, 0.1)

      // 绘制顶点
      gl.drawArrays(gl.POINTS, 0, 1)
    </script>
  </body>
</html>

扩展

vertexAttrib3f() 的同族函数

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

javascript
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 浮点型

webgl 函数的命名规律

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

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

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

用鼠标控制点位

用鼠标控制一个点的位置,首先要知道鼠标点在 webgl 坐标系中的位置,这样才能让一个点出现在我们鼠标点击的位置

对于鼠标点在 webgl 坐标系中的位置,我们是无法直接获取的。所以我们得先获取鼠标在 canvas 这个 DOM 元素中的位置

javascript
canvas.addEventListener('click',function(event){
  const {clientX,clientY}=event;
  const {left,top}=canvas.getBoundingClientRect();
  const [cssX,cssY]=[
    clientX-left,
    clientY-top
  ];
})

我们可以用向量减法来求解

已知:向量 a(clientX,clientY),向量 c(left,top) 求:向量 c

由向量的减法得:向量 a 减向量 c,等于以向量 c 的终点为起点,以向量 a 的终点为终点的向量 c

所以:向量 c=a-c=(clientX-left,clientY-top)

将向量 c 视之为坐标点 c,那点 c 就是鼠标在 canvas 画布中的 css 位

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

canvas 坐标系转 webgl 坐标系

1.解决坐标原点位置的差异

javascript
const [halfWidth,halfHeight]=[width/2,height/2];
const [xBaseCenter,yBaseCenter]=[cssX-halfWidth,cssY-halfHeight];

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

2.解决 y 方向的差异

const yBaseCenterTop=-yBaseCenter;

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

3.解决坐标基底的差异。

const [x,y]=[xBaseCenter/halfWidth,yBaseCenterTop/halfHeight];

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

javascript
canvas.addEventListener('click',function(event){
  const {clientX,clientY}=event;
  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];
  const yBaseCenterTop=-yBaseCenter;
  const [x,y]=[xBaseCenter/halfWidth,yBaseCenterTop/halfHeight];
})

关于获取鼠标点在 webgl 坐标系中的位置的方法,我们就说到这,接下来咱们基于这个位置,修改着色器暴露出来的位置变量即可。

修改 attribute 变量

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>鼠标控制点位</title>
    <style>
      body {
        margin: 0;
        overflow: hidden;
      }

      #canvas {
        background-color: antiquewhite;
      }
    </style>
  </head>

  <body>
    <canvas id="canvas"></canvas>
    <!-- 顶点着色器 -->
    <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,1,0,1);
      }
    </script>
    <script type="module">
      import { initShaders } from "../jsm/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")

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

      // 获取 attribute 变量
      const a_Position = gl.getAttribLocation(gl.program, "a_Position")

      // 修改attribute 变量
      gl.vertexAttrib1f(a_Position, 0.1)

      gl.clearColor(0, 0, 0, 1)
      gl.clear(gl.COLOR_BUFFER_BIT)

      // 绘制顶点
      gl.drawArrays(gl.POINTS, 0, 1)

      // 鼠标点击事件
      canvas.addEventListener("click", ({ clientX, clientY }) => {
        console.log(clientX, clientY)
        // 获取 canvas 的位置和尺寸 left 是距离左边的距离,top 是距离上边的距离
        const { left, top, width, height } = canvas.getBoundingClientRect()
        console.log(left, top, width, height)

        // 这里的 left 、top 就是 canvas 的坐标原点
        const [cssX, cssY] = [clientX - left, clientY - top]

        // canvas 坐标转换为 webgl 坐标系
        // 解决坐标原点位置的差异
        // canvas的中心坐标
        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]

        gl.vertexAttrib2f(a_Position, x, y)
        gl.clear(gl.COLOR_BUFFER_BIT)
        gl.drawArrays(gl.POINTS, 0, 1)
      })
    </script>
  </body>
</html>

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

webgl 的同步绘图原理

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

那咱们将 gl.clear() 方法注释掉试试

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

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

  • gl.drawArrays(gl.POINTS, 0, 1) 方法和 canvas 2d 里的 ctx.draw() 方法是不一样的,ctx.draw() 真的像画画一样,一层一层的覆盖图像。
  • gl.drawArrays() 方法只会同步绘图,走完了 js 主线程后,再次绘图时,就会从头再来。也就说,异步执行的 drawArrays() 方法会把画布上的图像都刷掉

案例 1

我先画两个点,然后在一秒后再绘画一个点

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>03-webgl同步绘图原理</title>
    <style>
      body {
        margin: 0;
        overflow: hidden;
      }

      #canvas {
        background-color: antiquewhite;
      }
    </style>
  </head>

  <body>
    <canvas id="canvas"></canvas>
    <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,1,0,1);
      }
    </script>
    <script type="module">
      import { initShaders } from "../jsm/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")

      initShaders(gl, vsSource, fsSource)

      // 获取 attribute 变量
      const a_Position = gl.getAttribLocation(gl.program, "a_Position")

      // 声明颜色 rgba
      gl.clearColor(0, 0, 0, 1)
      // 刷底色
      gl.clear(gl.COLOR_BUFFER_BIT)

      // 修改 attribute 变量
      gl.vertexAttrib2f(a_Position, -0.3, 0)
      // 绘制顶点
      gl.drawArrays(gl.POINTS, 0, 1)

      // 修改 attribute 变量  绘画两个点
      gl.vertexAttrib2f(a_Position, 0.3, 0)
      // 绘制顶点
      gl.drawArrays(gl.POINTS, 0, 1)

      // 在一秒后再绘画一个点
      setTimeout(() => {
        gl.vertexAttrib2f(a_Position, 0, 0)
        // 绘制顶点
        gl.drawArrays(gl.POINTS, 0, 1)
      }, 1000)
    </script>
  </body>
</html>

以前画好的两个点没了,黑色背景也没了。这就是咱们之前说过的 webgl 同步绘图原理。那这个问题如何解决呢?这就是一个简单的逻辑问题了

案例 2

我们可以用数组把一开始的那两个顶点存起来,在异步绘制第3个顶点的时候,把那两个顶点也一起画上

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>绘图原理</title>
    <style>
      body {
        margin: 0;
        overflow: hidden;
      }

      #canvas {
        background-color: antiquewhite;
      }
    </style>
  </head>

  <body>
    <canvas id="canvas"></canvas>
    <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,1,0,1);
      }
    </script>
    <script type="module">
      import { initShaders } from "../jsm/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")

      initShaders(gl, vsSource, fsSource)

      // 获取 attribute 变量
      const a_Position = gl.getAttribLocation(gl.program, "a_Position")

      gl.clearColor(0, 0, 0, 1)
      gl.clear(gl.COLOR_BUFFER_BIT)

      // 存储顶点数据的数组
      const a_points = [
        { x: -0.3, y: 0 },
        { x: 0.3, y: 0 },
      ]

      render()

      setTimeout(() => {
        a_points.push({ x: 0, y: 0 })
        render()
      }, 1000)

      // 渲染方法
      function render() {
        gl.clear(gl.COLOR_BUFFER_BIT)
        a_points.forEach(({ x, y }) => {
          gl.vertexAttrib2f(a_Position, x, y)
          gl.drawArrays(gl.POINTS, 0, 1)
        })
      }
    </script>
  </body>
</html>

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

案例 3:连续画点

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>连续画点</title>
    <style>
      body {
        margin: 0;
        overflow: hidden;
      }

      #canvas {
        background-color: antiquewhite;
      }
    </style>
  </head>

  <body>
    <canvas id="canvas"></canvas>
    <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,1,0,1);
      }
    </script>
    <script type="module">
      import { initShaders } from "../jsm/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")

      initShaders(gl, vsSource, fsSource)

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

      const a_points = [{ x: 0, y: 0 }]

      gl.clearColor(0, 0, 0, 1)
      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]

        a_points.push({ x, y })
        render()
      })

      // 渲染方法
      function render() {
        gl.clear(gl.COLOR_BUFFER_BIT)
        a_points.forEach(({ x, y }) => {
          gl.vertexAttrib2f(a_Position, x, y)
          gl.drawArrays(gl.POINTS, 0, 1)
        })
      }
    </script>
  </body>
</html>

JS 控制顶点尺寸

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

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

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

2.在 js 里获取 attribute 变量 const a_PointSize=gl.getAttribLocation(gl.program,'a_PointSize');

3.修改 attribute 变量 gl.vertexAttrib1f(a_PointSize,100.0);

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>js改变顶点尺寸</title>
    <style>
      body {
        margin: 0;
        overflow: hidden;
      }

      #canvas {
        background-color: antiquewhite;
      }
    </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">
      void main(){
          gl_FragColor=vec4(1,1,0,1);
      }
    </script>
    <script type="module">
      import { initShaders } from "../jsm/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")

      initShaders(gl, vsSource, fsSource)

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

      // 修改 attribute 变量
      gl.vertexAttrib1f(a_Position, 0.1)
      gl.vertexAttrib1f(a_PointSize, 10)

      gl.clearColor(0, 0, 0, 1)
      gl.clear(gl.COLOR_BUFFER_BIT)

      gl.drawArrays(gl.POINTS, 0, 1)
    </script>
  </body>
</html>

鼠标随机改变顶点大小

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

  <head>
    <meta charset="UTF-8"/>
    <title>随机改变顶点尺寸</title>
    <style>
      body {
        margin: 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;
      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,1,0,1);
      }
    </script>
    <script type="module">
      import {initShaders} from "./jsm/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");

      initShaders(gl, vsSource, fsSource);

      // 设置attribute 变量
      const a_Position = gl.getAttribLocation(gl.program, "a_Position");
      const a_PointSize = gl.getAttribLocation(gl.program, "a_PointSize");

      const a_points = [{x: 0, y: 0, size: 10}];

      // 声明颜色 rgba
      gl.clearColor(0, 0, 0, 1);
      // 刷底色
      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];

        const size = Math.random() * 50 + 10;
        a_points.push({x, y, size});
        render();
      });

      // 渲染方法
      function render() {
        gl.clear(gl.COLOR_BUFFER_BIT);
        a_points.forEach(({x, y, size}) => {
          gl.vertexAttrib2f(a_Position, x, y);
          gl.vertexAttrib1f(a_PointSize, size);
          gl.drawArrays(gl.POINTS, 0, 1);
        });
      }
    </script>
  </body>

</html>

无论是控制点位的尺寸,还是控制点位的位置,实际上都是对 attribute 变量的操控。但是想要再改变顶点的颜色呢?那就不能再用 attribute 限定符了,因为 attribute 限定符限定的就是顶点相关的数据

JS 控制顶点的颜色

首先我们要知道,限定颜色变量的限定符叫 uniform,翻译过来是一致、统一的意思

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

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

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

2.在 js 中获取片元着色器暴露出的 uniform 变量 const u_FragColor=gl.getUniformLocation(gl.program,'u_FragColor');

3.修改uniform 变量 gl.uniform4f(u_FragColor,1.0,1.0,0.0,1.0);

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>修改顶点颜色</title>
    <style>
      body {
        margin: 0;
        overflow: hidden;
      }

      #canvas {
        background-color: antiquewhite;
      }
    </style>
  </head>

  <body>
    <canvas id="canvas"></canvas>
    <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">
      precision mediump float;
      uniform vec4 u_FragColor;
      void main(){
          gl_FragColor=u_FragColor;
      }
    </script>
    <script type="module">
      import { initShaders } from "../jsm/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")

      initShaders(gl, vsSource, fsSource)

      gl.clearColor(0, 0, 0, 1)
      gl.clear(gl.COLOR_BUFFER_BIT)

      // 获取 attribute 变量
      const a_Position = gl.getAttribLocation(gl.program, "a_Position")

      // 获取 uniform 变量
      const u_FragColor = gl.getUniformLocation(gl.program, "u_FragColor")

      // 修改 attribute 变量
      gl.vertexAttrib1f(a_Position, 0.1)

      // 修改 uniform 变量
      gl.uniform4f(u_FragColor, 1, 0, 1, 1)

      // 绘制顶点
      gl.drawArrays(gl.POINTS, 0, 1)
    </script>
  </body>
</html>

鼠标随机改变顶点的颜色

使用 uniform4fv() 修改的顶点颜色

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

      #canvas {
        background-color: antiquewhite;
      }
    </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;
      uniform vec4 u_FragColor;
      void main(){
          gl_FragColor=u_FragColor;
      }
    </script>
    <script type="module">
      import { initShaders } from "../jsm/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")

      initShaders(gl, vsSource, fsSource)

      // 获取变量
      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 a_points = [{ x: 0, y: 0, size: 10, color: { r: 1, g: 0, b: 0, a: 1 } }]

      gl.clearColor(0, 0, 0, 1)
      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]

        const size = Math.random() * 50 + 10
        const n = Math.random()
        const color = { r: n, g: n, b: 1, a: 1 }
        a_points.push({ x, y, size, color })
        render()
      })

      // 渲染方法
      function render() {
        gl.clear(gl.COLOR_BUFFER_BIT)
        a_points.forEach(({ x, y, size, color: { r, g, b, a } }) => {
          gl.vertexAttrib2f(a_Position, x, y)
          gl.vertexAttrib1f(a_PointSize, size)
          // gl.uniform4f(u_FragColor, r, g, b, a);
          // 颜色是随机产生的
          const arr = new Float32Array([r, g, b, a])
          gl.uniform4fv(u_FragColor, arr)
          gl.drawArrays(gl.POINTS, 0, 1)
        })
      }
    </script>
  </body>
</html>

uniform4fv() 方法

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

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

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

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

案例-用鼠标绘制星空

用鼠标绘制圆形的顶点

星星的形状是圆形的,所以,我们需要绘制一个圆形的顶点。

javascript
<script id="fragmentShader" type="x-shader/x-fragment">
     precision mediump float;
     uniform vec4 u_FragColor;
     void main() {
         float dist = distance(gl_PointCoord, vec2(0.5, 0.5));
         if(dist < 0.5) {
             gl_FragColor = u_FragColor;
         } else {
             discard;
         }
     }
 </script>
  • distance(p1,p2) 计算两个点位的距离
  • gl_PointCoord 片元在一个点中的位置,此位置是被归一化的(宽高为 1)
  • discard 丢弃,即不会一个片元进行渲染

着色器语法参考地址:https://www.khronos.org/registry/OpenGL-Refpages/gl4/

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>绘制圆点</title>
    <style>
      body {
        margin: 0;
        overflow: hidden;
      }

      #canvas {
        background-color: antiquewhite;
      }
    </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;
      uniform vec4 u_FragColor;
      void main(){
        //  绘制圆点,计算当前边缘直到点中间的位置
        float dist=distance(gl_PointCoord,vec2(0.5,0.5));
        if(dist<0.5){
          gl_FragColor=u_FragColor;
        }else{
          discard; //  放弃
        }
      }
    </script>
    <script type="module">
      import { initShaders } from "../jsm/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")

      initShaders(gl, vsSource, fsSource)

      // 获取变量
      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 a_points = [{ x: 0, y: 0, size: 10, color: { r: 1, g: 0, b: 0, a: 1 } }]

      gl.clearColor(0, 0, 0, 1)
      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]

        const size = Math.random() * 50 + 10
        const n = Math.random()
        const color = { r: n, g: n, b: 1, a: 1 }
        a_points.push({ x, y, size, color })
        render()
      })

      // 渲染方法
      function render() {
        gl.clear(gl.COLOR_BUFFER_BIT)
        a_points.forEach(({ x, y, size, color: { r, g, b, a } }) => {
          gl.vertexAttrib2f(a_Position, x, y)
          gl.vertexAttrib1f(a_PointSize, size)
          // gl.uniform4f(u_FragColor, r, g, b, a);
          const arr = new Float32Array([r, g, b, a])
          gl.uniform4fv(u_FragColor, arr)
          gl.drawArrays(gl.POINTS, 0, 1)
        })
      }
    </script>
  </body>
</html>

绘制随机透明度的星星

首先我们可以先给 canvas 一个星空背景

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

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

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

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

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

      #canvas {
        background: url("./images/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;
      uniform vec4 u_FragColor;
      void main(){
        float dist=distance(gl_PointCoord,vec2(0.5,0.5));
        if(dist<0.5){
          gl_FragColor=u_FragColor;
        }else{
          discard;
        }
      }
    </script>
    <script type="module">
      import { initShaders } from "../jsm/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)

      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 = []

      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]

        const s = Math.random() * 5 + 2
        const a = Math.random()
        stars.push({ x, y, s, a })
        render()
      })

      // 渲染方法
      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>

制作闪烁的繁星

当星星会眨眼睛,会变得灵动而可爱,接下来我要让星星对你眨眼睛

建立补间动画的意识

在这里推荐大家玩一下 AE,因为它可以让你对动画的运行原理和架构方式有一个具象的认知

比如,我在 AE 里画一颗星星,加几个关键帧,让它眨一下眼睛

在这里会涉及以下概念:

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

架构代码

1.建立合成对象

javascript
export default class Compose{
  constructor(){
    this.parent=null
    this.children=[]
  }
  add(obj){
    obj.parent=this
    this.children.push(obj)
  }
  update(t){
    this.children.forEach(ele=>{
      ele.update(t)
    })
  }
}

属性

  • parent 父对象,合成对象可以相互嵌套
  • children 子对象集合,其集合元素可以是时间轨,也可以是合成对象

方法:

  • add(obj) 添加子对象方法
  • update(t) 基于当前时间更新子对象状态的方法

2.建立时间轨

javascript
export default class Track{
  constructor(target){
    this.target=target
    this.parent=null
    this.start=0
    this.timeLen=5
    this.loop=false
    this.keyMap=new Map()
  }
  update(t){
    const {keyMap,timeLen,target,loop}=this
    let time=t-this.start
    if(loop){
      time=time%timeLen
    }
    for(const [key,fms] of keyMap.entries()){
      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)
      }
    }
  }
}

属性

  • target 时间轨上的目标对象
  • parent 父对象,只能是合成对象
  • start 起始时间,即时间轨的建立时间
  • timeLen 时间轨总时长
  • loop 是否循环
  • keyMap 关键帧集合,结构如下:
javascript
[
  [
    '对象属性1',
    [
      [时间1,属性值], //关键帧
      [时间2,属性值], //关键帧
    ]
  ],
  [
    '对象属性2',
    [
      [时间1,属性值], //关键帧
      [时间2,属性值], //关键帧
    ]
  ],
]

方法

  • update(t) 基于当前时间更新目标对象的状态。先计算本地时间,即世界时间相对于时间轨起始时间的的时间。若时间轨循环播放,则本地时间基于时间轨长度取余。遍历关键帧集合:

  • 若本地时间小于第一个关键帧的时间,目标对象的状态等于第一个关键帧的状态

  • 若本地时间大于最后一个关键帧的时间,目标对象的状态等于最后一个关键帧的状态

  • 否则,计算本地时间在左右两个关键帧之间对应的补间状态

3.获取两个关键帧之间补间状态的方法

javascript
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 最后一个关键帧的索引位置

  • 遍历所有关键帧

  • 判断当前时间在哪两个关键帧之间

  • 基于这两个关键帧的时间和状态,求点斜式

  • 基于点斜式求本地时间对应的状态

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

  1. 建立动画相关的对象
javascript
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.用请求动画帧驱动动画,连续更新数据,渲染视图。

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

渲染方法如下:

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

3.最后我们还可以配点应景的音乐,比如虫儿飞

html
#audio{
  position: absolute;
  right: 20px;
  bottom: 20px;
  opacity: 10%;
  transition: opacity 200ms;
  z-index: 20;
}
#audio:hover{
  opacity: 90%;
}
<audio id="audio" controls loop autoplay>
<source src="./audio/cef.mp3" type="audio/mpeg">
</audio>
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>星星向你眨眼睛</title>
    <style>
      body {
        margin: 0;
        overflow: hidden;
      }

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

      #audio {
        position: absolute;
        right: 20px;
        bottom: 20px;
        opacity: 10%;
        transition: opacity 200ms;
        z-index: 20;
      }

      #audio:hover {
        opacity: 90%;
      }
    </style>
  </head>

  <body>
    <canvas id="canvas"></canvas>
    <audio id="audio" controls loop autoplay>
      <source src="./audio/cef.mp3" type="audio/mpeg" />
    </audio>
    <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(){
        float dist=distance(gl_PointCoord,vec2(0.5,0.5));
        if(dist<0.5){
          gl_FragColor=u_FragColor;
        }else{
          discard;
        }
      }
    </script>
    <script type="module">
      import { initShaders } from "../jsm/Utils.js"
      import Compose from "../jsm/Compose.js"
      import Track from "../jsm/Track.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)

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

      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]

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

      // 自调用函数
      !(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>