咳咳,大家好!今天咱们聊点儿刺激的,直接上手,聊聊JavaScript里的“显卡超频”——WebGL。
一、 啥是WebGL?—— 浏览器里的硬件加速器
WebGL,全称Web Graphics Library,直译过来就是“网页图形库”。它可不是什么新奇玩意儿,它本质上是OpenGL ES 2.0/3.0的JavaScript binding(绑定)。这意味着啥?意味着你可以直接在浏览器里,用JavaScript来调用GPU的强大计算能力,搞出各种炫酷的3D图形效果,甚至做一些复杂的计算任务。
想想看,以前只能在桌面应用里看到的3D游戏、数据可视化、科学模拟等等,现在都能在浏览器里跑起来,是不是有点小激动?
二、 WebGL的工作原理:流水线的故事
要把3D世界搬到浏览器里,WebGL可不是简单地画几个三角形就完事儿的。它背后有一套复杂的渲染流程,我们通常称之为“渲染管线”(Rendering Pipeline)。这个管线就像一个工厂的流水线,把原始的3D数据一步步加工成最终的图像。
我们来简单地拆解一下这个流水线:
-
顶点数据(Vertex Data): 这是所有故事的起点。它包含了3D模型的所有顶点信息,比如每个顶点的坐标、颜色、法向量等等。
-
顶点着色器(Vertex Shader): 这是第一个“工人”,负责对顶点数据进行处理。它可以进行坐标变换(比如把模型坐标转换到世界坐标、再转换到相机坐标)、光照计算等等。它的输入是顶点数据,输出是变换后的顶点数据。
-
图元装配(Primitive Assembly): 这个阶段把顶点组装成一个个的图元,比如三角形、线段、点等等。
-
光栅化(Rasterization): 这个阶段把图元转换成像素。简单来说,就是确定哪些像素需要被绘制,以及每个像素的颜色和深度值。
-
片元着色器(Fragment Shader): 这是第二个“工人”,负责对每个像素进行着色。它可以进行更复杂的光照计算、纹理贴图等等。它的输入是光栅化阶段产生的像素信息,输出是最终的像素颜色。
-
测试与混合(Tests and Blending): 这个阶段进行深度测试、模板测试、颜色混合等等,最终把像素颜色写入帧缓冲区(Frame Buffer)。
-
帧缓冲区(Frame Buffer): 存储最终图像的缓冲区。
代码示例:一个简单的三角形
光说不练假把式,咱们来写一个简单的WebGL程序,画一个彩色的三角形。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WebGL Triangle</title>
<style>
body { margin: 0; }
canvas { width: 100%; height: 100%; display: block; }
</style>
</head>
<body>
<canvas id="glCanvas"></canvas>
<script>
const canvas = document.getElementById("glCanvas");
const gl = canvas.getContext("webgl");
if (!gl) {
alert("WebGL not supported!");
}
// 顶点着色器代码
const vsSource = `
attribute vec4 aVertexPosition;
attribute vec4 aVertexColor;
varying lowp vec4 vColor;
void main() {
gl_Position = aVertexPosition;
vColor = aVertexColor;
}
`;
// 片元着色器代码
const fsSource = `
varying lowp vec4 vColor;
void main() {
gl_FragColor = vColor;
}
`;
// 初始化着色器程序
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;
}
// 加载着色器
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;
}
// 初始化缓冲区
function initBuffers(gl) {
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const positions = [
0.0, 0.5,
-0.5, -0.5,
0.5, -0.5,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
const colors = [
1.0, 0.0, 0.0, 1.0, // 红色
0.0, 1.0, 0.0, 1.0, // 绿色
0.0, 0.0, 1.0, 1.0, // 蓝色
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
return {
position: positionBuffer,
color: colorBuffer,
};
}
// 绘制场景
function drawScene(gl, programInfo, buffers) {
gl.clearColor(0.0, 0.0, 0.0, 1.0); // Clear to black, fully opaque
gl.clearDepth(1.0); // Clear everything
gl.enable(gl.DEPTH_TEST); // Enable depth testing
gl.depthFunc(gl.LEQUAL); // Near things obscure far things
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 设置顶点属性指针
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);
gl.vertexAttribPointer(
programInfo.attribLocations.vertexPosition,
2, // 每个顶点属性由2个值组成
gl.FLOAT, // 数据类型是浮点数
false, // 不进行归一化
0, // 步长为0
0 // 偏移量为0
);
gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition);
// 设置颜色属性指针
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.color);
gl.vertexAttribPointer(
programInfo.attribLocations.vertexColor,
4, // 每个顶点属性由4个值组成
gl.FLOAT, // 数据类型是浮点数
false, // 不进行归一化
0, // 步长为0
0 // 偏移量为0
);
gl.enableVertexAttribArray(programInfo.attribLocations.vertexColor);
// 设置着色器程序
gl.useProgram(programInfo.program);
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, 3);
}
// 程序入口
function main() {
const shaderProgram = initShaderProgram(gl, vsSource, fsSource);
const programInfo = {
program: shaderProgram,
attribLocations: {
vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
vertexColor: gl.getAttribLocation(shaderProgram, 'aVertexColor'),
},
};
const buffers = initBuffers(gl);
drawScene(gl, programInfo, buffers);
}
main();
</script>
</body>
</html>
这个代码有点长,但别害怕,咱们一步步来解释:
- HTML: 创建了一个
<canvas>
元素,这是WebGL绘制的地方。 - JavaScript:
- 获取WebGL上下文:
canvas.getContext("webgl")
获取WebGL的上下文对象。 - 着色器代码:
vsSource
和fsSource
分别是顶点着色器和片元着色器的代码。它们是用GLSL(OpenGL Shading Language)写的,这是一种专门用于GPU编程的语言。 - 创建和编译着色器:
loadShader
函数负责加载、编译着色器代码。 - 创建着色器程序:
initShaderProgram
函数负责把顶点着色器和片元着色器链接成一个着色器程序。 - 创建缓冲区:
initBuffers
函数负责创建顶点缓冲区和颜色缓冲区,并把数据上传到GPU。 - 绘制场景:
drawScene
函数负责设置WebGL的状态,并调用gl.drawArrays
函数来绘制三角形。 - 程序入口:
main
函数是程序的入口,它负责初始化WebGL,并调用drawScene
函数来绘制场景。
- 获取WebGL上下文:
把这段代码保存成一个HTML文件,用浏览器打开,你就能看到一个彩色的三角形了。
三、 GLSL:GPU的语言
刚才我们看到了,顶点着色器和片元着色器是用GLSL写的。GLSL是一种类C的语言,专门用于GPU编程。它有很多内置的函数和数据类型,可以方便地进行图形计算。
GLSL有一些特殊的变量,需要特别注意:
变量名 | 类型 | 说明 |
---|---|---|
attribute |
变量修饰符 | 用于顶点着色器,声明从JavaScript传递过来的顶点属性。只能在顶点着色器中使用。 |
uniform |
变量修饰符 | 用于声明全局变量,可以从JavaScript传递过来。可以在顶点着色器和片元着色器中使用。 |
varying |
变量修饰符 | 用于声明从顶点着色器传递到片元着色器的变量。在顶点着色器中赋值,在片元着色器中接收。 |
gl_Position |
vec4 |
顶点着色器的内置变量,用于指定顶点的最终位置。必须在顶点着色器中赋值。 |
gl_FragColor |
vec4 |
片元着色器的内置变量,用于指定像素的最终颜色。必须在片元着色器中赋值。 |
四、 WebGL的进阶:矩阵变换、纹理贴图、光照模型
画一个三角形只是WebGL的入门。要做出更炫酷的效果,还需要掌握一些更高级的技术。
-
矩阵变换: 通过矩阵变换,可以对3D模型进行平移、旋转、缩放等操作。常用的矩阵变换包括模型矩阵(Model Matrix)、视图矩阵(View Matrix)、投影矩阵(Projection Matrix)。
-
纹理贴图: 把图像贴到3D模型上,可以增加模型的细节和真实感。
-
光照模型: 模拟光照效果,可以使3D场景更加逼真。常用的光照模型包括环境光、漫反射光、镜面反射光。
代码示例:矩阵变换
我们来修改一下之前的代码,用矩阵变换来旋转三角形。
// 顶点着色器代码 (修改后)
const vsSource = `
attribute vec4 aVertexPosition;
attribute vec4 aVertexColor;
uniform mat4 uModelViewMatrix; // 模型视图矩阵
uniform mat4 uProjectionMatrix; // 投影矩阵
varying lowp vec4 vColor;
void main() {
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
vColor = aVertexColor;
}
`;
// 片元着色器代码 (不变)
const fsSource = `
varying lowp vec4 vColor;
void main() {
gl_FragColor = vColor;
}
`;
// 初始化着色器程序 (修改后)
function initShaderProgram(gl, vsSource, fsSource) {
// ... (省略) ...
return shaderProgram;
}
// 绘制场景 (修改后)
function drawScene(gl, programInfo, buffers, deltaTime) {
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clearDepth(1.0);
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 创建透视投影矩阵
const fieldOfView = 45 * Math.PI / 180; // in radians
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const zNear = 0.1;
const zFar = 100.0;
const projectionMatrix = mat4.create(); // 使用gl-matrix库
mat4.perspective(projectionMatrix,
fieldOfView,
aspect,
zNear,
zFar);
// 创建模型视图矩阵
const modelViewMatrix = mat4.create();
// 将模型视图矩阵移动到绘制正方形的起始位置
mat4.translate(modelViewMatrix, // destination matrix
modelViewMatrix, // matrix to translate
[-0.0, 0.0, -6.0]); // amount to translate
// 旋转模型视图矩阵
mat4.rotate(modelViewMatrix, // destination matrix
modelViewMatrix, // matrix to rotate
deltaTime, // amount to rotate in radians
[0, 0, 1]); // axis to rotate around (Z)
// 设置顶点属性指针 (不变)
// ... (省略) ...
// 设置着色器程序
gl.useProgram(programInfo.program);
// 设置 uniform 变量
gl.uniformMatrix4fv(
programInfo.uniformLocations.projectionMatrix,
false,
projectionMatrix);
gl.uniformMatrix4fv(
programInfo.uniformLocations.modelViewMatrix,
false,
modelViewMatrix);
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, 3);
}
// 程序入口 (修改后)
function main() {
const shaderProgram = initShaderProgram(gl, vsSource, fsSource);
const programInfo = {
program: shaderProgram,
attribLocations: {
vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
vertexColor: gl.getAttribLocation(shaderProgram, 'aVertexColor'),
},
uniformLocations: {
projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'),
modelViewMatrix: gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
},
};
const buffers = initBuffers(gl);
let then = 0;
// 绘制场景
function render(now) {
now *= 0.001; // convert to seconds
const deltaTime = now - then;
then = now;
drawScene(gl, programInfo, buffers, deltaTime);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
这个代码主要做了以下修改:
- 顶点着色器: 增加了
uModelViewMatrix
和uProjectionMatrix
两个uniform
变量,分别表示模型视图矩阵和投影矩阵。在main
函数中,将顶点位置乘以这两个矩阵,得到最终的顶点位置。 - 绘制场景: 增加了
projectionMatrix
和modelViewMatrix
两个矩阵,分别表示投影矩阵和模型视图矩阵。使用mat4.perspective
函数创建透视投影矩阵,使用mat4.translate
和mat4.rotate
函数创建模型视图矩阵。然后,通过gl.uniformMatrix4fv
函数把这两个矩阵传递给顶点着色器。 - 动画循环: 使用
requestAnimationFrame
创建动画循环,使三角形不断旋转。
注意: 这个代码使用了 gl-matrix
库来进行矩阵运算。你需要在HTML文件中引入这个库。你可以从https://glmatrix.net/下载它。
五、 WebGL的用途:不仅仅是游戏
虽然WebGL在游戏开发中应用广泛,但它的用途远不止于此。
- 数据可视化: 可以用WebGL来创建各种炫酷的数据可视化图表,比如3D散点图、3D柱状图等等。
- 科学模拟: 可以用WebGL来模拟各种科学现象,比如流体模拟、粒子模拟等等。
- 虚拟现实/增强现实: WebGL是WebVR/WebAR的基础技术,可以用它来创建各种虚拟现实/增强现实应用。
- 图像处理: 可以用WebGL来进行图像处理,比如滤镜、特效等等。
六、 WebGL的挑战:性能优化、兼容性
WebGL虽然强大,但也面临一些挑战。
- 性能优化: WebGL程序通常需要处理大量的图形数据,因此性能优化非常重要。常用的优化方法包括减少绘制调用、使用顶点缓冲区对象(VBO)、使用纹理贴图集(Texture Atlas)等等。
- 兼容性: WebGL的兼容性受到浏览器和显卡驱动的影响。为了保证程序的兼容性,需要进行充分的测试,并针对不同的平台进行优化。
七、 总结:WebGL,开启无限可能
WebGL是JavaScript里的一把利器,它让开发者能够直接利用GPU的强大计算能力,创造出各种令人惊艳的图形效果和应用。虽然学习WebGL需要掌握一些新的概念和技术,但只要你肯努力,就能开启无限可能。
今天的分享就到这里。希望大家能从中学到一些东西,并开始尝试用WebGL创造属于自己的精彩世界。下次有机会再见!