12、webgl 基本概念 +满天星星眨眼睛

基本概念

webgl 坐标系

webgl 画布的建立和获取,和 canvas 2d 是一样的。

一旦我们使用 canvas.getContext() 方法获取了 webgl 类型的上下文对象,那这张画布就不再是以前的canvas 2d 画布。

当然,它也不会变成三维的,因为我们的电脑屏幕始终是平的。

那这张画布有什么不一样了呢?

它的坐标系变了。

canvas 2d 画布和 webgl 画布使用的坐标系都是二维直角坐标系,只不过它们的坐标原点、y轴的坐标方向,坐标基底都不一样了。

canvas 2d画布的坐标系

canvas 2d 坐标系的原点在左上角, y轴方向是朝下的。

canvas 2d 画布的坐标基底有两个分量,分别是一个像素的宽和一个像素的高,即 1 个单位的宽便是 1 个像素的宽,1个单位的高便是一个像素的高。

webgl 的坐标系

webgl 坐标系的坐标原点在画布中心,y 轴方向是朝上的。

webgl 坐标基底中的两个分量分别是半个 canvas的宽和 canvas 的高,即 1 个单位的宽便是半个 canvas 的宽,1 个单位的高便是半个 canvas 的高。

绘制点的步骤

1、在 html 中建立 canvas 画布

2、在 js 中获取 canvas画布

3、使用 canvas 获取 webgl 绘图上下文

4、在 script 中建立顶点着色器和片元着色器, glsl es

5、在 js 中获取顶点着色器和片元着色器的文本

6、初始化着色器

7、指定将要用来清空绘制图区的颜色

8、使用之前指定的颜色,清空绘制图区

9、绘制顶点

着色器

着色器的概念

webgl 绘图需要两种着色器:

顶点着色器(Vertex shader):描述顶点的特征,如位置、颜色等

片元着色器(Fragment shader):进行逐片元着色

着色器的初始化

1、建立程序对象,目前这只是一个手绘板的外壳

2、建立顶点着色器对象和 片元着色器对象,这是手绘板里用于接收触控笔信号的零部件,二者可以分工合作,把触控笔的压感(js 信号)解析为计算机语言(GLSL ES),然后让计算机(浏览器的webgl 渲染引擎)识别显示。

3、将顶点着色器对象和片元着色器对象装进程序对象中,这就完成手绘板的拼装

4、链接 webgl 上下文对象和程序对象,就像连接触控笔和手绘板一样(触控笔有传感器,可以向手绘板发送信号)

5、启动程序对象,就像按下了手绘板的启动按钮,使其开始工作

用 js 控制一个点的位置

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

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

一些方法和常量

颜色缓冲区: gl.COLOR_BUFFER_BIT

根据attribut变量修改顶点位置: gl.vertexAtrrib2f(a_Position, x, y)

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

用鼠标绘制星空

用鼠标绘制圆形的顶点

星星的形状使用圆形的

html 复制代码
<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 片元在一个点中的位置,此位置是被归一化的;

discard 丢弃,即不会对这个片元进行渲染。

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

制作闪烁的繁星

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

绘制随机透明度的星星

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

css 复制代码
#cancvas {
  background: url("./sky.jpg");
  background-size: cover;
  background-position: right bottom;
}

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

javascript 复制代码
gl.clearColor(0, 0, 0, 0);

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

javascript 复制代码
const arr = new Float32Array([0.87, 0.91, 1, a]);
gl.uniform4fv(u_FragColor, arr);

开启片元的颜色合成功能

javascript 复制代码
gl.enable(gl.BLEND);

设置片元的合成方式

javascript 复制代码
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
架构代码

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) 基于当前时间更新目标对象的状态

先计算本地时间,即世界时间相对于时间轨起始时间的时间。

若时间轨循环播放,则本地时间基于时间轨长度取余。

遍历关键帧集合:

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

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

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

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

javascript 复制代码
function getValBetweenFms(time, fms, last) {
  for(let i = 0; i < last; i++) {
    const fm1 = fms[i], 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, 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);
  })
}

一闪一闪亮晶晶,满天星星眨眼睛

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;
    }
    #canvas {
      background: url("./sky.png");
      background-size: cover;
      background-position: right bottom;
      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;
    // 所有着色器都有一个 main 方法
    void main() {
        // gl_Position 是一个顶点着色器主要设置的变量
        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 Compose from "./utils/Compose.js";
    import Track from "./utils/Track.js"
    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, 0);
    const vertexStr = getInnerText("vertexShader");
    const fragmentStr = getInnerText("fragmentShader");
    const program = initShaderProgram(gl, vertexStr, fragmentStr )
    let a_Position = gl.getAttribLocation(program, 'a_Position');
    let a_PointSize = gl.getAttribLocation(program, 'a_PointSize');
    let u_FragColor = gl.getUniformLocation(program, 'u_FragColor');
    // 开启透明度混合
    gl.enable(gl.BLEND);
    // 设置混合公式(正常透明模式)
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
    gl.useProgram(program);
    const compose = new Compose()
    let stars = []
    canvas.addEventListener('click', function(event) {
      const {x, y} = getPosByMouse(event, canvas);
      const a = 1, s = Math.random() * 5 + 2;
      const obj = {x, y, s, a};
      stars.push(obj);
      const track = new Track(obj);
      track.start = Date.now();
      track.keyMap = new Map([
        [
          'a',
          [
            [500, a],
            [1000, 0],
            [1500, a]
          ]
        ]
      ])
      track.timeLen = 2000;
      track.loop = true;
      compose.add(track);
    })
    
    !(function ani(){
      compose.update(Date.now());
      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.92, 0.92, 1, a]));
        gl.drawArrays(gl.POINTS, 0, 1);
      })
    }
 </script>
</body>
</html>

./utils/Compose.js

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

./utils/Track.js

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

function getValBetweenFms(time, fms, last) {
  for (let i = 0; i < last; i++) {
    const fm1 = fms[i],
      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;
    }
  }
}

./utils/index.js

javascript 复制代码
export function getPosByMouse(event, canvas) {
  const { offsetX, offsetY } = event;
  const { width, height } = canvas;
  console.log(offsetX, offsetY, width, height, "当前数据");

  return {
    x: (offsetX * 2) / width - 1,
    y: 1 - (offsetY * 2) / height,
  };
}

export function getInnerText(id) {
  return document.getElementById(id).innerText;
}

export function initShaderProgram(gl, vsSource, fsSource) {
  const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
  const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
  const shaderProgram = gl.createProgram();
  gl.attachShader(shaderProgram, vertexShader);
  gl.attachShader(shaderProgram, fragmentShader);
  gl.linkProgram(shaderProgram);
  if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
    alert(
      `Unable to initialize the shader program: ${gl.getProgramInfoLog(
        shaderProgram,
      )}`,
    );
    return null;
  }

  return shaderProgram;
}
export function loadShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    alert(
      `An error occurred compiling the shaders: ${gl.getShaderInfoLog(shader)}`,
    );
    gl.deleteShader(shader);
    return null;
  }

  return shader;
}

背景资源文件

相关推荐
陆枫Larry2 小时前
搞懂 package.json 和 package-lock.json
前端
竹林8182 小时前
Solana前端开发:从连接钱包到发送交易,我如何用@solana/web3.js搞定第一个DApp
前端·javascript
Cache技术分享2 小时前
385. Java IO API - Chmod 示例:模拟 chmod 命令的文件权限更改
前端·后端
沙振宇2 小时前
【Web】使用Vue3+PlayCanvas开发3D游戏(十一)渲染3D高斯泼溅效果
前端·游戏·3d
cool32002 小时前
4D实验八:Dubbo微服务 + 注册中心
前端·kubernetes
军军君012 小时前
数字孪生监控大屏实战模板:商圈大数据监控
前端·javascript·vue.js·typescript·前端框架·echarts·three
方安乐2 小时前
try catch vs 异步捕获
前端·javascript·vue.js
chenbin___2 小时前
鸿蒙RN position: ‘absolute‘ 和 zIndex 的兼容性问题(转自千问)
前端·javascript·react native·harmonyos
晴天丨2 小时前
Vue 3项目架构设计:从2200行单文件到24个组件
前端·vue.js