1-CPU和GPU
学习Webgl,必须要知道计算机里的2种处理器:
- CPU(中央处理器):电脑总指挥官 + 通用运算核心
- GPU(图形处理器):并行计算加速器
CPU和GPU 都属于处理器芯片。
CPU 相当于公司的资深全能主管,一个公司只有几个,能力极强,可处理大型事务,串行工作,但由于人数较少,处理的事务不宜太多。
GPU相当于上千个流水线工人,会并行重复做同一套简单工序;任务单一、重复时效率极高;一旦工序复杂、每个人步骤不一样,就会严重卡顿。
Web前端的js 就是通过CPU运行的,shader 是通过GPU 运行的。
shader 面向的是像素,一张画布会有很多像素,通常需要快速渲染,若像CPU 那样一个个计算会很慢,所以适合GPU的海量、轻量、并行运算。
2-显卡
显卡是一整块电路板,包含:
- GPU 核心
- 供电电路、散热风扇、PCB 板、视频输出接口
- 若显卡是独显,还会有独立显存(VRAM)。
根据显存的来源,显卡分三类:
- 独立显卡(独显):自带专属物理显存 VRAM。
- CPU 核显(现在轻薄本、新机主流):无独立 VRAM,挪用系统内存。
- 老式主板集显(十几年前老电脑):同样挪用内存,和核显原理近似。
现在NVIDIA 的显卡都是独显,比如RTX 4060、RTX 4090、GTX 1660、T 系列。
NVIDIA 的显卡具备以下特点:
- 自带独立显存 VRAM;
- 单独芯片、独立供电、单独 PCB 板;
- 性能强,主打游戏、渲染、AI 运算。
3-数据的存储
CPU和GPU 所处理的数据是需要硬件存储的。
GPU 的数据存储
GPU 的数据是存在显存里的。
上面说了,只有独显才有独立的显存,若显卡是其它类型,那么显存会挪用系统内存。
CPU 的数据存储
硬盘(永久存储)→ 内存(临时存放)→ CPU 缓存 → CPU 核心计算
- 打开软件时,系统把程序从硬盘加载到内存;
- CPU 不会直接读取整根内存,先把要用的数据拷贝到CPU 一级 / 二级 / 三级缓存(离运算核心最近、速度最快);
- CPU 核心从缓存取数据运算,算完写回缓存,再同步回内存;
- 关闭软件 / 断电,内存数据清空,硬盘保留文件。
在webgl 里,我们经常会说把CPU 里的attribute 数据上传到GPU。这里的CPU 就是CPU 缓冲,GPU 指的时GPU显存里的buffer。
当然,我们也可以说把内存里的attribute 数据上传到GPU,这都没问题。
RAM和VRAM 间的数据交互
VRAM是 GPU 存放大规模数据的主力;RAM是GPU 的 "外部仓库"。
独立显卡不能直接访问主机内存,但两者可以相互间接访问数据:
- 数据上传:CPU 把图片、模型、文件从内存拷贝到显存,GPU 才能计算;
- 数据回传:GPU 算完画面 / 结果,从显存拷贝回内存,再交给显示器 / CPU 保存;
- 统一内存(CUDA Unified Memory)
软件层面打通内存 + 显存,自动迁移数据,但底层依然要拷贝,不会消除传输开销。
4-以硬件为锚点思考WebGL
根据硬件规则,我们可以确定2个锚点:
-
执行
- CPU 执行的程序,如html+css+js
- GPU:shader program 的执行
-
存储
- 内存存储,如js数据
- 显存存储,如webgl 的Buffer对象
接下来我们用WebGL 画一个三角形,看一下其中的执行和存储操作。
5-WebGL示例
我们可以依托在three.js 项目,建立一个examples/webgl_hardware.html 文件。
ini
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - pure WebGL triangle </title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="main.css">
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script type="importmap">
{
"imports": {
"three": "../build/three.module.js",
"three/addons/": "./jsm/"
}
}
</script>
<script id="vertexShader" type="x-shader/x-vertex">
#version 300 es
precision highp float;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
in vec3 position;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
#version 300 es
layout(location = 0) out highp vec4 pc_fragColor;
precision highp float;
uniform vec3 diffuse;
void main() {
pc_fragColor = vec4( diffuse, 1.0 );
}
</script>
<script type="module">
import * as THREE from 'three';
// 设置绘制缓冲区尺寸与 CSS 显示尺寸
const canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.width = window.innerWidth + 'px';
canvas.style.height = window.innerHeight + 'px';
// WebGL 上下文
const gl = canvas.getContext('webgl2');
// 从 <script> 标签读取 GLSL 源码;trim() 去掉首尾空白,确保 #version 在第一行
const vsSource = document.getElementById('vertexShader').innerText.trim();
const fsSource = document.getElementById('fragmentShader').innerText.trim();
// 声明投影矩阵和模型视图矩阵
const camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.1, 100 );
camera.position.z = 4;
camera.updateMatrixWorld();
camera.updateProjectionMatrix();
const projectionMatrix = camera.projectionMatrix.clone();
const modelMatrix = new THREE.Matrix4(); // 单位矩阵,三角形位于世界原点
const modelViewMatrix = camera.matrixWorldInverse.clone().multiply( modelMatrix );
// 创建Program 对象,关联 vertexShader 和 fragmentShader,并链接和使用Program对象
const program = gl.createProgram();
const vertexShader = compileShader( gl, gl.VERTEX_SHADER, vsSource );
const fragmentShader = compileShader( gl, gl.FRAGMENT_SHADER, fsSource );
gl.attachShader( program, vertexShader );
gl.attachShader( program, fragmentShader );
gl.linkProgram( program );
gl.useProgram( program );
function compileShader( gl, type, source ) {
const shader = gl.createShader( type );
gl.shaderSource( shader, source );
gl.compileShader( shader );
return shader;
}
// 视口映射到整个 canvas(坐标系左下角为原点)
gl.viewport( 0, 0, canvas.width, canvas.height );
// 创建 VBO(Vertex Buffer Object),在已绑定的 VAO 上配置 position 属性
const vertices = new Float32Array( [
-1.0, -1.0, 0.0,
1.0, -1.0, 0.0,
0.0, 1.0, 0.0
] );
const vertexBuffer = gl.createBuffer();
gl.bindBuffer( gl.ARRAY_BUFFER, vertexBuffer );
gl.bufferData( gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW );
// 创建 VAO(Vertex Array Object):缓存「position 怎么读」的配置,绘制时 bind 即可恢复
const vao = gl.createVertexArray();
gl.bindVertexArray( vao );
// 查询 attribute 在 program 中的 location(与 shader 中 in vec3 position 对应)
const a_Position = gl.getAttribLocation( program, 'position' );
// vertexAttribPointer(index, size, type, normalized, stride, offset)
// size=3:每顶点读 x/y/z;stride=3*4:每顶点占 12 字节;offset=0:从 buffer 开头读
// 以下 enable / pointer 调用会写入当前 VAO,而非全局默认状态
gl.vertexAttribPointer( a_Position, 3, gl.FLOAT, false, 3 * 4, 0 );
gl.enableVertexAttribArray( a_Position );
// 解绑 VBO(可选);避免后续 bufferData 误操作当前 buffer
gl.bindBuffer( gl.ARRAY_BUFFER, null );
// 解绑 VAO(可选);避免后续 bindVertexArray 误操作当前 vao
gl.bindVertexArray( null );
// 向着色器传入模型视图矩阵和投影矩阵
const u_modelViewMatrix = gl.getUniformLocation( program, 'modelViewMatrix' );
gl.uniformMatrix4fv( u_modelViewMatrix, false, modelViewMatrix.elements );
const u_projectionMatrix = gl.getUniformLocation( program, 'projectionMatrix' );
gl.uniformMatrix4fv( u_projectionMatrix, false, projectionMatrix.elements );
// 向着色器传入三角形颜色
const u_diffuse = gl.getUniformLocation( program, 'diffuse' );
gl.uniform3fv( u_diffuse, new Float32Array( [ 0.0, 0.0, 1.0 ] ) ); // 蓝色
// 绘制背景色
gl.clearColor( 1.0, 1.0, 0.0, 1.0 ); // 黄色
gl.clear( gl.COLOR_BUFFER_BIT ); // 清屏
// 绘制三角形:先 bind VAO,一次性恢复顶点属性配置,再 draw
gl.bindVertexArray( vao );
gl.drawArrays( gl.TRIANGLES, 0, 3 ); // 从第 0 个顶点起,绘制 3 个顶点(1 个三角形)
gl.bindVertexArray( null );
// 画完后就释放 GPU 资源(对应 Three.js WebGLBindingStates 中的 deleteVertexArray)
gl.deleteVertexArray( vao );
gl.deleteBuffer( vertexBuffer );
gl.deleteProgram( program );
gl.deleteShader( vertexShader );
gl.deleteShader( fragmentShader );
</script>
</body>
</html>
CPU 执行
这整个html+css+js 就是通过CPU 执行的。
RAM 存储
在绝大多数的场景下,js 声明的变量是存在RAM中的。
ini
const vertices = new Float32Array( [
-1.0, -1.0, 0.0,
1.0, -1.0, 0.0,
0.0, 1.0, 0.0
] );
- vertices 是栈变量,只保存一个指向堆对象的引用地址。
- new Float32Array( ......) 是堆对象,会作为实际数据存放在堆中。
上例中的栈和堆都存在于RAM中。
VRAM 存储
js 可以通过webgl 协议,在VRAM 中创建对象。
ini
const program = gl.createProgram();
gl.createProgram() 可以在VRAM 创建一个真正的shader program。
program 是指向shader program 的引用句柄,它本身是一个普通的 JS 对象,存在RAM的栈内存中。
ini
const program = gl.createProgram();
console.log(typeof program ); // object
在js中创建的webgl 对象基本都是这种引用句柄的形式:js引用句柄 → VRAM 中的真实对象。
ini
const shader = gl.createShader( type );
const vertexBuffer = gl.createBuffer();
const vao = gl.createVertexArray();
const a_Position = gl.getAttribLocation( program, 'position' );
const u_diffuse = gl.getUniformLocation( program, 'diffuse' );
GPU 执行
在GPU 中执行的命令,是在js中通过webgl 协议调用的。
ini
const gl = canvas.getContext('webgl2');
gl 就是根据webgl2 协议创建的上下文对象。
通过gl 可以让GPU 执行webgl 相关的各种指令,比如上面在VRAM中创建各种对象,bindBuffer,drawArrays等。
总结
这一章我们讲解了计算机中的硬件:CPU、GPU,VAM、VRAM。
并围绕执行和存储2个锚点,以webgl 绘制三角形为示例,对webgl 和硬件的关系进行了解析。