第二章:WebGL入门

148 阅读12分钟

衔接上一篇:第一章:WebGL概述

收获

当你学习完下面内容后你会有如下收获:

  1. 初始化WebGL
  2. 理解着色器GLSL ES语言以及如何编写着色器
  3. 理解WebGL坐标系屏幕坐标转WebGL坐标
  4. 通过JavaScript动态修改着色器
  5. 理解webgl同步绘图原理

1.清空绘图区

功能:使用背景色清空<canvas>标签的绘画区。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      body {
        margin: 0;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script>
      // 获取canvas元素
      const canvas = document.getElementById('canvas');
      // 设置canvas宽高
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      // 获取WebGL绘图上下文
      const gl = canvas.getContext('webgl');
      // 设置清空颜色缓冲时的颜色值
      gl.clearColor(0, 0, 0, 1);
      // 清除颜色缓冲区
      gl.clear(gl.COLOR_BUFFER_BIT);
    </script>
  </body>
</html>
  • clearColor(0, 0, 0, 1):由于WebGL是继承自OpenGL的,所以它遵循传统的OpenGL颜色分量的值范围,即从0.0到1.0。RGB的值越高,颜色就越亮﹔类似地,第4分量α的值越高,颜色就越不透明。
  • clear(gl.COLOR_BUFFER_BIT):传递参数COLOR_BUFFER_BIT就是在告诉WebGL清空颜色缓冲区。

运行之后的效果图如下:

image.png

2.绘制一个点

需求:我们需要在三维坐标的原点位置,绘制一个黄色大小为100个像素的点。

下面这段代码在canvas中就能完成上面的需求,本以为在WebGL中也能够如实轻松。直到看完发现我还是太年轻了。

// 核心代码
ctx.fillStyle = 'yellow';
ctx.fillRect(0, 0, 100, 100)

在绘制之前我们需要了解这些知识:

如何得到图形:什么是着色器?

如何实现图形:GLSL ES语言是什么?怎么编写着色器?

2.1 什么是着色器

在WebGL中,着色器(Shader)是一种用于控制图形渲染的程序。要使用WebGL进行绘图就必须使用着色器。在代码中着色器程序是以字符串的形式“嵌入”在JavaScript文件中的,在程序真正开始运行前它就已经设置好了。

WebGL使用两种类型的着色器:

  1. 顶点着色器:顶点着色器是用来描述顶点特性(如位置、颜色等)的程序。顶点(vertex)是指二维或三维空间中的一个点,比如二维或三维图形的端点或交点。
  2. 片元着色器:片元着色器是用于处理每个片元(像素)的小型程序。它接收由顶点着色器传递的数据,并根据需要计算片元的最终颜色。片元着色器通常用于执行光照计算、纹理采样和其他可视化效果的计算。最终,片元着色器将计算得到的颜色输出到屏幕上,完成图形的渲染过程。

下面这张图是从执行JavaScript程序到在浏览器中显示结果的过程

Pasted image 20230713150143.png

到这里我们已经知道了得到图形需要用到着色器。那么着色器如何用代码实现呢?上面之所以说到着色器程序是以字符串的形式“嵌入”在JavaScript文件中的原因是因为着色器并不是使用JS语言编写的。而是使用的GLSL ES语言编写的。

2.2 什么是GLSL ES语言

GLSL ES(OpenGL ES Shading Language)是一种针对嵌入式系统的着色器语言,它是OpenGL ES标准的一部分。它被设计用于在OpenGL ES渲染管线中编写顶点着色器和片段着色器的代码。它与C语言非常相似,但它专注于定义顶点和片段着色器所需的计算和操作。GLSL ES支持向量和矩阵运算,可以执行数学运算、纹理采样、光照计算和其他图形渲染操作。

2.3 如何使用GLSL ES语言编写着色器

顶点着色器

 const vsSource = 'void main() { gl_Position = vec4(0, 0, 0, 1); gl_PointSize = 100.0; }';
变量名称类型描述默认值注意
gl_Positionvec4顶点位置-该变量必须赋值
gl_PointSizefloat点的尺寸(像素数)1.0大小一定要浮点类型100.0而非100

注意

  • gl_Positiongl_PointSize都是顶点着色器的内置变量,变量名是固定的。赋予gl_Position的矢量称为齐次坐标
  • GLSL ES代码结尾需要加;

Pasted image 20230713163641.png

片元着色器

const fsSource = 'void main() { gl_FragColor = vec4(1, 1, 0, 1); }';

注意gl_FragColor是片元着色器的内置变量,其变量名也是固定的。其值也是vec4类型,包含四个浮点类型分量,分别代表RGBA值。

2.4 实现需求

到了这里想必你已经了解了着色器以及如何编写,下面我们就可以编写代码来实现需求啦!首先我们先缕一缕思路,思考实现步骤如下:

  1. 获取canvas元素
  2. 获取WebGL上下文
  3. 编写顶点着色器和片元着色器代码
  4. 初始化着色器
  5. 清除canvas
  6. 绘制图元

明确步骤后实现代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <!-- 此处代码在下面有编写 -->
    <script src="./utils.js"></script>
    <style>
      body {
        margin: 0;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script>
      // 获取canvas元素
      const canvas = document.getElementById('canvas');
      // 设置canvas宽高
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      // 获取WebGL绘图上下文
      const gl = canvas.getContext('webgl');
      // 顶点着色器代码
      const vsSource = 'void main() { gl_Position = vec4(0, 0, 0, 1); gl_PointSize = 100.0; }';
      // 片元着色器代码
      const fsSource = 'void main() { gl_FragColor = vec4(1, 1, 0, 1); }';
      // 初始化着色器(下面有实现代码)
      initShader(gl, vsSource, fsSource);
      // 清除canvas
      gl.clearColor(0, 0, 0, 1);
      gl.clear(gl.COLOR_BUFFER_BIT);
      // 绘制图元
      gl.drawArrays(gl.POINTS, 0, 1);
    </script>
  </body>
</html>

drawArrays(mode, first, count)方法用于从向量数组中绘制图元。

  • mode:图元类型
  • first:指定从哪个点开始绘制。
  • count:指定绘制需要使用到多少个点。

image.png

初始化着色器

在初始化着色器之前,顶点着色器与片元着色器都是空白的,我们需要将字符串形式的着色器代码从JavaScript传给WebGL系统,并建立着色器。我们可以定义一个通用的函数来初始化着色器,代码如下:

:下面这些代码难以理解没有关系,后面会有讲到这些内容。

// utils.js
/**
 * 加载和编译着色器
 * @param {object} gl WebGL上下文对象
 * @param {string} type 着色器类型
 * @param {string} source 着色器代码
 * @returns {object} 着色器对象
 */
const loadShader = (gl, type, source) => {
  // 创建着色器对象
  const shader = gl.createShader(type);
  // 设置着色器的GLSL程序代码
  gl.shaderSource(shader, source);
  // 编译着色器
  gl.compileShader(shader);
  return shader;
};
  
/**
 * 初始化着色器
 * @param {object} gl WebGL上下文对象
 * @param {string} vsSource 顶点着色器代码
 * @param {string} fsSource 片元着色器代码
 * @returns {boolean} 是否初始化成功
 */
const initShader = (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;
};

3.WebGL坐标系统

由于WebGL处理的是三维图形,所以它使用三维坐标系统(笛卡尔坐标系),具有X轴、Y轴和Z轴。当你面向计算机屏幕时,X轴是水平的(正方向为右),Y轴是垂直的(正方向为下),而Z轴垂直与屏幕(正方向为外)。观察者的眼睛位于原点(0.0,0.0,0.0)处,视线则是沿着Z轴的负方向,从你指向屏幕。这套坐标系又称为右手坐标系

Pasted image 20230716160750.png

3.1 Canvas与WebGl坐标系的区别

  • Canvas的坐标原点在左上角,X轴向右为正,Y轴向下为正
  • WebGL的坐标原点为绘图区域的中心点。X轴向右为正,Y轴向上为正,Z轴向外为正。

Pasted image 20230718175342.png

  • WebGL中使用的坐标范围是从-1到1,即X、Y和Z坐标的范围都在-1到1之间。
  • 如果Canvas的宽度为500像素,高度为300像素,则X坐标的范围是从0到500,Y坐标的范围是从0到300。 Pasted image 20230716161346.png

4.动态修改点的位置

至此我们已经了解了如何绘制一个点,但是上面绘制点的代码不灵活。总是将点绘制在固定的位置,因为点的位置是直接编写(“硬编码”)在顶点着色器中的,虽然示例程序易于理解,但缺乏可扩展性。所以我们需要将点变为动态的可修改的,WebGL程序可以将顶点的位置坐标从JavaScript传到着色器程序中,然后在对应位置上将点绘制出来。虽然最终的结果与上面效果一样,但它用的方法是可扩展的,后面的例子都将使用这种方式。

4.1 attribute变量

我们的目标是,将位置信息从JavaScript程序中传给顶点着色器。有两种方式可以实现:attribute变量uniform变量。使用哪一个变量取决于需传输的数据本身,attribute变量传输的是那些与顶点相关的数据,而uniform变量传输的是那些对于所有顶点都相同(或与顶点无关)的数据。本例将使用attribute变量来传输顶点坐标,显然不同的顶点通常具有不同的坐标。

为了使用attribute变量实现,程序需要包含如下几个步骤:

  1. 在顶点着色器中声明attribute变量。
    • 关键词attribute称为存储限定符
    • vec4为变量类型
    • a_Position为变量名
const vsSource = 'attribute vec4 a_Position; void main () { }';
  1. attribute变量赋值给gl_Position变量。
const vsSource = ' attribute vec4 a_Position; void main () { gl_Position = a_Position; }';
  1. 获取a_Position变量的存储位置
    • gl.program:该参数为程序对象,它包括了顶点着色器和片元着色器,这里不需要你花精力去知道它是什么怎么来的。只要将它作为参数即可。注意:你必须在调用initShader()之后在访问gl.program,因为是initShader()创建了这个程序对象。
const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
  1. attribute变量传递数据。
gl.vertexAttrib3f(a_Position, 0.0, 0.5, 0.0);

完整代码如下:

<!DOCTYPE html>

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./utils.js"></script>
    <style>
      body {
        margin: 0;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script>
      const canvas = document.getElementById('canvas');
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      const gl = canvas.getContext('webgl');
      // 在顶点着色器中声明attribute变量,并赋值给gl_Position
      const vsSource = 'attribute vec4 a_Position; void main () { gl_Position = a_Position; gl_PointSize = 100.0; }';
      const fsSource = 'void main () { gl_FragColor = vec4(1, 1, 0, 1); }';
      initShader(gl, vsSource, fsSource);
      // 获取a_Position变量的存储位置
      const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
      // 向attribute变量传递数据。
      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);
    </script>
  </body>
</html>

image.png

4.2 vertexAttrib3f的同族函数

gl.vertexAttrib3f()是一系列同族函数中的一个,该系列函数的任务就是从JavaScript向顶点着色器中的attribute变量传值。同族函数如下:

Pasted image 20230717165820.png

4.3 鼠标点击修改点的位置

需求:将点的坐标更改为鼠标点击的位置。

前面我们已经明白了如何通过JS动态的修改点的位置,这一次我们将通过获取鼠标点击canvas在webgl中的坐标修改点的位置。

  1. 获取鼠标点击的屏幕坐标
...
canvas.addEventListener('click', ({ clientX, clientY }) => {
	console.log(clientX, clientY)
});

Pasted image 20230718203808.png

问题:这里我们获取了点在屏幕上的坐标,但是我们需要的是点在canvas坐标系中的坐标。

  1. 获取在canvas中的坐标
...
canvas.addEventListener('click', ({ clientX, clientY }) => {
	// 获取canvas距离窗口左顶点距离
	const { left, top } = canvas.getBoundingClientRect();
	// 得到点在canvas中的坐标
	const [inCanvasX, inCanvasY] = [clientX - left, clientY - top];
});

Pasted image 20230718210116.png

问题:鼠标点在canvas中的坐标有了(inCanvasX,inCanvasY),但是canvas与webgl的坐标系不一样。那么点在webgl坐标系中的坐标是多少?在这之前我们还需要得到webgl的原点坐标。

  1. 解决坐标原点位置差异
 ...
 canvas.addEventListener('click', ({ clientX, clientY }) => {
	const { left, top, width, height } = canvas.getBoundingClientRect();
	const [inCanvasX, inCanvasY] = [clientX - left, clientY - top];
	// 获取webgl的原点坐标
	const [webglOriginX, webglOriginY] = [width / 2, height / 2];
	// 计算出鼠标点在webgl坐标系中的坐标
	let [inWebglX, inWebglY] = [inCanvasX - webglOriginX, inCanvasY - webglOriginY];
	// canvas与webgl的Y轴是相反的
	inWebglY = -inWebglY;
});

问题:webgl中使用的坐标范围是从-1到1,即X、Y和Z坐标的范围都在-1到1之间。但是现在的坐标值还是canvas中的像素值。

  1. 改用webgl的基准值
...
canvas.addEventListener('click', ({ clientX, clientY }) => {
	const { left, top, width, height } = canvas.getBoundingClientRect();
	const [inCanvasX, inCanvasY] = [clientX - left, clientY - top];
	const [webglOriginX, webglOriginY] = [width / 2, height / 2];
	let [inWebglX, inWebglY] = [inCanvasX - webglOriginX, inCanvasY - webglOriginY];
	inWebglY = -inWebglY;
	// 计算出所占份数即可
	const [inWebglBaseX, inWebglBaseY] = [inWebglX / webglOriginX, inWebglY / webglOriginY];
});
  1. 然后修改坐标值即可。
...
canvas.addEventListener('click', ({ clientX, clientY }) => {
	const { left, top, width, height } = canvas.getBoundingClientRect();
	const [inCanvasX, inCanvasY] = [clientX - left, clientY - top];
	const [webglOriginX, webglOriginY] = [width / 2, height / 2];
	let [inWebglX, inWebglY] = [inCanvasX - webglOriginX, inCanvasY - webglOriginY];
	inWebglY = -inWebglY;
	const [inWebglBaseX, inWebglBaseY] = [inWebglX / webglOriginX, inWebglY / webglOriginY];
	gl.vertexAttrib2f(a_Position, inWebglBaseX, inWebglBaseY);
	gl.clearColor(0.0, 0.0, 0.0, 1.0);
	gl.clear(gl.COLOR_BUFFER_BIT);
	gl.drawArrays(gl.POINTS, 0, 1);
});

4.4 鼠标点击绘制多个点

在完成鼠标点击绘制多个点之前我们需要先了解一下webgl同步绘图原理

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

同步绘图

首先我们同步的绘制两个点

...
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.vertexAttrib3f(a_Position, -0.3, 0.0, 0.0);
gl.drawArrays(gl.POINTS, 0, 1);
gl.vertexAttrib3f(a_Position, 0.3, 0.0, 0.0);
gl.drawArrays(gl.POINTS, 0, 1);

image.png

异步绘制

然后异步的绘制第三个点

...
gl.clear(gl.COLOR_BUFFER_BIT);
gl.vertexAttrib3f(a_Position, -0.3, 0.0, 0.0);
gl.drawArrays(gl.POINTS, 0, 1);
gl.vertexAttrib3f(a_Position, 0.3, 0.0, 0.0);
gl.drawArrays(gl.POINTS, 0, 1);
setTimeout(() => {
  gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0);
  gl.drawArrays(gl.POINTS, 0, 1);
}, 1000);

image.png

运行发现一秒前画好的两个点没有了,黑色背景也没有了。这就是webgl的同步绘图原理导致的。

那么这个问题将如何解决呢?

思路:其实非常简单,我们只需要准备一个数组用于存放每个点的位置。然后每次需要添加点的时候就向数组中push其位置,需要展示的时候就将数组遍历,同步的一个一个进行渲染。

  1. 准备一个数组专门用来存储每个点的位置
// gl.vertexAttrib3f(a_Position, -0.3, 0.0, 0.0);
// gl.vertexAttrib3f(a_Position, 0.3, 0.0, 0.0);
const pointPositions = [
  {
    x: -0.3,
    y: 0.0,
  },
  {
    x: 0.3,
    y: 0.0,
  },
];
  1. 写一个函数用于遍历数组同步渲染
const render = (pointPositions) => {
  gl.clear(gl.COLOR_BUFFER_BIT);
  pointPositions.forEach(({ x, y }) => {
    gl.vertexAttrib2f(a_Position, x, y);
    gl.drawArrays(gl.POINTS, 0, 1);
  });
};
  1. 同步添加的两个点
render(pointPositions);
  1. 异步添加的一个点
setTimeout(() => {
  pointPositions.push({
    x: 0.0,
    y: 0.0,
  });
  render(pointPositions);
}, 1000);

image.png

明白了webgl的同步渲染原理之后,我们只需要在原来代码的基础之上稍加修改就能实现鼠标点击绘制多个点了。代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./utils.js"></script>
    <style>
      body {
        margin: 0;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script>
      const canvas = document.getElementById('canvas');
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      const gl = canvas.getContext('webgl');
      const vsSource = 'attribute vec4 a_Position; void main () { gl_Position = a_Position; gl_PointSize = 100.0; }';
      const fsSource = 'void main () { gl_FragColor = vec4(1, 1, 0, 1); }';
      initShader(gl, vsSource, fsSource);
      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 pointPositions = [];
      // 同步渲染所有的点
      const render = (pointPositions) => {
        gl.clear(gl.COLOR_BUFFER_BIT);
        pointPositions.forEach(({ x, y }) => {
          gl.vertexAttrib2f(a_Position, x, y);
          gl.drawArrays(gl.POINTS, 0, 1);
        });
      };
      canvas.addEventListener('click', ({ clientX, clientY }) => {
        const { left, top, width, height } = canvas.getBoundingClientRect();
        const [inCanvasX, inCanvasY] = [clientX - left, clientY - top];
        const [webglOriginX, webglOriginY] = [width / 2, height / 2];
        let [inWebglX, inWebglY] = [inCanvasX - webglOriginX, inCanvasY - webglOriginY];
        inWebglY = -inWebglY;
        const [inWebglBaseX, inWebglBaseY] = [inWebglX / webglOriginX, inWebglY / webglOriginY];
        // 新增点
        pointPositions.push({
          x: inWebglBaseX,
          y: inWebglBaseY,
        });
        render(pointPositions);
      });
    </script>
  </body>
</html>

image.png

5.动态修改点的颜色

现在,你应该对着色器是如何工作的,以及如何将数据从JavaScript程序传入着色器,有了很好的理解。我们将在此基础上构建一个更复杂的程序——改变绘制点的颜色。前面我们已经学习了通过attribute变量将顶位置传递给了顶点着色器,这一次我们将学习使用变量uniform变量将颜色传递给片元着色器。

5.1 uniform变量

我们可以通过uniform变量将颜色信息从JavaScript程序中传给片元着色器中,其步骤attribute变量类似。具体如下:

  1. 在片元着色器中创建uniform变量
/**
 * 这里的precision mediump float;是必须的。表示浮点数的精度说明
 * precision是精度的意思
 * mediump是精度等级(从高到低:highp|mediump|lowp)
 */
const fsSource = "precision mediump float; uniform vec4 u_FragColor;";
  1. u_FragColor变量赋值给gl_FragColor
const fsSource = 'precision mediump float; uniform vec4 u_FragColor; void main () { gl_FragColor = u_FragColor; }';
  1. 获取u_FragColor变量的存储位置
const u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor');
  1. u_FragColor变量赋值
gl.uniform4f(u_FragColor, 1, 0, 0, 1);

完整代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./utils.js"></script>
    <style>
      body {
        margin: 0;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script>
      const canvas = document.getElementById('canvas');
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      const gl = canvas.getContext('webgl');
      const vsSource = 'void main() { gl_Position = vec4(0, 0, 0, 1); gl_PointSize = 100.0; }';
      const fsSource =
        'precision mediump float; uniform vec4 u_FragColor; void main () { gl_FragColor = u_FragColor; }';
      initShader(gl, vsSource, fsSource);
      const u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor');
      gl.uniform4f(u_FragColor, 1, 0, 0, 1);
      gl.clearColor(0, 0, 0, 1);
      gl.clear(gl.COLOR_BUFFER_BIT);
      gl.drawArrays(gl.POINTS, 0, 1);
    </script>
  </body>
</html>

image.png

5.2 uniform4f的同族函数

gl.uniform4f()也有一系列同族函数。gl.uniformlf()函数用来传输1个值(v0),gl.uniform2f()传输2个值(v0和v1),gl.uniform3f ()传输3个值 (v0,vl和 v2)。

image.png

5.3 鼠标点击绘制随机大小、颜色点

至此我们已经了解了如何通过JavaScript动态修改着色器的属性,以及如何获取鼠标点击后在webgl坐标系下的坐标。相信完成这一需求应该不在话下,下面是实现代码就不一一讲解。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./utils.js"></script>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script>
      const canvas = document.getElementById('canvas');
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      const gl = canvas.getContext('webgl');
      const vsSource =
        'attribute vec4 a_Position; attribute float a_PointSize; void main () { gl_Position = a_Position; gl_PointSize = a_PointSize; }';
      const fsSource =
        'precision mediump float; uniform vec4 u_FragColor; void main () { gl_FragColor = u_FragColor; }';
      initShader(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 point = [];
      gl.clearColor(0, 0, 0, 1);
      gl.clear(gl.COLOR_BUFFER_BIT);

      function render(point) {
        gl.clear(gl.COLOR_BUFFER_BIT);
        point.forEach(({ position, size, color }) => {
          gl.vertexAttrib2f(a_Position, position.x, position.y);
          gl.vertexAttrib1f(a_PointSize, size);
          gl.uniform4f(u_FragColor, color.r, color.g, color.b, color.a);
          gl.drawArrays(gl.POINTS, 0, 1);
        });
      }

      function randomColor() {
        return {
          r: Math.random(),
          g: Math.random(),
          b: Math.random(),
          a: Math.random(),
        };
      }

      canvas.addEventListener('click', ({ clientX, clientY }) => {
        const { left, top, width, height } = canvas.getBoundingClientRect();
        const [inCanvasX, inCanvasY] = [clientX - left, clientY - top];
        const [webglOriginX, webglOriginY] = [width / 2, height / 2];
        let [inWebglX, inWebglY] = [
          (inCanvasX - webglOriginX) / webglOriginX,
          (inCanvasY - webglOriginY) / webglOriginY,
        ];
        inWebglY = -inWebglY;
        point.push({
          position: {
            x: inWebglX,
            y: inWebglY,
          },
          size: Math.random() * 50 + 10,
          color: randomColor(),
        });
        render(point);
      });
    </script>
  </body>
</html>

image.png

下一章:第三章:绘制和变换图形

参考

WebGL编程指南