ltts blog

WebGL 实战:绘制你的第一个点(三)

引言

万丈高楼平地起,学习 WebGL 也需要从基础开始。在前两篇文章中,我们分别介绍了 WebGL 的基础知识和渲染管线的原理。这一次,让我们动手绘制一个简单的点。这看似简单的目标,其实包含了 WebGL 的核心概念:着色器、缓冲区、坐标系等。通过这个案例,你将了解 WebGL 程序的基本结构,为接下来的复杂渲染打下坚实的基础。

准备工作

在开始之前,你需要以下工具和基础文件:

  • 一个现代浏览器(建议使用 Chrome 或 Firefox)。
  • 文本编辑器(例如 VS Code 或 Sublime Text)。

HTML 基础模板:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebGL 第一个点</title>
    <style>
        canvas { width: 100%; height: 100%; display: block; }
        body { margin: 0; }
    </style>
</head>
<body>
    <canvas id="webgl-canvas"></canvas>
    <script src="point.js"></script>
</body>
</html>

将上面的代码保存为 index.html,我们将在 point.js 文件中编写 WebGL 的主要逻辑。

实现步骤

第一步:初始化 WebGL 上下文

创建一个 point.js 文件,并编写以下代码来初始化 WebGL 环境 目的: 获取与显卡交互的 WebGL 上下文 gl,它是后续绘图操作的基础。

const canvas = document.getElementById("webgl-canvas");
const gl = canvas.getContext("webgl");
// 通过上述代码,我们获取了 WebGL 的上下文 gl,这是与显卡交互的接口
 
if (!gl) {
    console.error("WebGL 初始化失败,请检查浏览器兼容性!");
} else {
    console.log('WebGL 初始化成功');
}

第二步:创建着色器

着色器概念: WebGL 渲染的核心逻辑依赖着色器,主要包括 顶点着色器 和 片元着色器。 着色器由 GLSL 语言编写,运行在 GPU 上,负责处理顶点数据和像素颜色。

1、顶点着色器

定义点的位置:

const vertexShaderSource = `
// attribute 是从 CPU 传递到 GPU 的每个顶点特有的数据
// vec4 是四维向量类型,a_position 表示顶点的位置坐标
 
attribute vec4 a_position; 
void main() {
    // 注释:将顶点位置直接传递给 gl_Position
    // 注释:gl_Position 是内置变量,表示顶点的最终位置(在裁剪空间中)
 
    gl_Position = a_position;
    gl_PointSize = 3.0; 
 
    // 注释:此处的 a_position 是 vec4 类型
    // 注释:因为在 WebGL 的渲染管线中,裁剪空间坐标需要四维 (x, y, z, w)
    // 注释:如果是 2D 绘制,可以简单地传入 (x, y, 0.0, 1.0)
}`;
  1. 片元着色器

定义点的颜色:

const fragmentShaderSource = `
// 注释:片元着色器不需要接收额外输入,仅需设置固定颜色
void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 红色
 
   // 注释:vec4 中的各值依次表示 (红, 绿, 蓝, 透明度),范围为 0.0 到 1.0
}`;

第三步:编译着色器并创建程序

function createShader(gl, type, source) {
    const shader = gl.createShader(type);
    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        console.error(gl.getShaderInfoLog(shader));
        gl.deleteShader(shader);
        return null;
    }
    return shader;
}
 
function createProgram(gl, vertexShader, fragmentShader) {
    const program = gl.createProgram();
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
        console.error(gl.getProgramInfoLog(program));
        gl.deleteProgram(program);
        return null;
    }
    return program;
}
 
// 编译着色器
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
 
// 创建程序
const program = createProgram(gl, vertexShader, fragmentShader);
gl.useProgram(program);

第四步:传递顶点数据

传递顶点坐标到 GPU,并绑定到 a_position: 为顶点着色器传递一个点的位置(例如:[0.0, 0.0, 0.0, 1.0] 表示屏幕中心):

// 定义顶点数据
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const positions = new Float32Array([0.0, 0.0, 0.0, 1.0]); // 点的位置
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
 
// 获取属性位置并启用
const positionAttributeLocation = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(positionAttributeLocation);
gl.vertexAttribPointer(positionAttributeLocation, 4, gl.FLOAT, false, 0, 0);

第五步:绘制点

最后,通过 gl.drawArrays 绘制点:

gl.clearColor(0.0, 0.0, 0.0, 1.0); // 设置背景颜色为黑色
gl.clear(gl.COLOR_BUFFER_BIT); // 清除画布
 
gl.drawArrays(gl.POINTS, 0, 1); // 绘制一个点

point.js 完整代码

/// 获取 WebGL 上下文
const canvas = document.getElementById("canvas");
const gl = canvas.getContext("webgl");
 
// 检查 WebGL 是否可用
if (!gl) {
    console.error("WebGL not supported. Please use a compatible browser.");
}
 
// 顶点着色器源码
const vertexShaderSource = `
attribute vec4 a_position; // 顶点位置
void main() {
    gl_Position = a_position; // 设置裁剪空间中的顶点位置
    gl_PointSize = 3.0; // 设置点的大小
}
`;
 
// 片元着色器源码
const fragmentShaderSource = `
void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 设置片元颜色为红色 (RGBA)
}
`;
 
// 创建着色器
function createShader(gl, type, source) {
    const shader = gl.createShader(type); // 创建顶点或片元着色器
    gl.shaderSource(shader, source); // 绑定着色器源码
    gl.compileShader(shader); // 编译着色器
    const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
    if (!success) {
        console.error(`Could not compile shader: ${gl.getShaderInfoLog(shader)}`);
        gl.deleteShader(shader);
        return null;
    }
    return shader;
}
 
// 创建着色器程序
function createProgram(gl, vertexShader, fragmentShader) {
    const program = gl.createProgram(); // 创建 WebGL 程序
    gl.attachShader(program, vertexShader); // 附加顶点着色器
    gl.attachShader(program, fragmentShader); // 附加片元着色器
    gl.linkProgram(program); // 链接着色器程序
    const success = gl.getProgramParameter(program, gl.LINK_STATUS);
    if (!success) {
        console.error(`Program failed to link: ${gl.getProgramInfoLog(program)}`);
        gl.deleteProgram(program);
        return null;
    }
    return program;
}
 
// 创建并编译顶点和片元着色器
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
 
// 创建 WebGL 程序并附加着色器
const program = createProgram(gl, vertexShader, fragmentShader);
 
// 找到 a_position 变量的位置
const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
 
// 创建缓冲区存储顶点数据
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); // 绑定缓冲区到当前的 WebGL 上下文
 
// 定义顶点位置 (x, y, z, w)
// 此处点的位置为裁剪坐标系的中心 (0, 0),z = 0,w = 1
const positions = [
    0.0, 0.0, 0.0, 1.0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); // 将顶点数据传入缓冲区
 
// 使用 WebGL 程序
gl.useProgram(program);
 
// 启用顶点属性数组
gl.enableVertexAttribArray(positionAttributeLocation);
 
// 告诉 WebGL 如何从缓冲区获取数据给 a_position
gl.vertexAttribPointer(
    positionAttributeLocation, // 属性位置
    4, // 每个顶点由 4 个分量 (x, y, z, w) 组成
    gl.FLOAT, // 数据类型
    false, // 不需要归一化数据
    0, // 每个顶点之间的间隔 (步长)
    0 // 从缓冲区起始位置读取数据
);
 
// 设置视口:指定 WebGL 渲染的区域
gl.viewport(0, 0, canvas.width, canvas.height);
 
// 清空画布并设置背景颜色为黑色
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
 
// 绘制点
gl.drawArrays(
    gl.POINTS, // 绘制模式:点
    0,         // 从第一个顶点开始绘制
    1          // 绘制 1 个点
);

运行 index.html,你将在屏幕中心看到一个红色的点。虽然它看起来很简单,但这是 WebGL 渲染的第一步。从这个点开始,你可以添加更多的元素,例如线条、三角形,甚至整个 3D 世界!

补充说明

坐标系说明:

“WebGL 使用的是左手坐标系,x 轴向右,y 轴向上,z 轴垂直屏幕朝内。在顶点着色器中,点的位置需要通过 gl_Position 映射到裁剪坐标系,其范围是 [-1, 1]。”

像素和点的关系

点的大小受设备像素密度(DPI)和 gl_PointSize 的影响,但某些平台可能限制点的最大尺寸,可以通过 gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE) 查询。

点的大小

默认情况下,绘制的点大小是系统决定的。可以通过 gl_PointSize 在顶点着色器中设置点的大小。

为什么点的位置是 vec4?

1、裁剪坐标系的要求: WebGL 使用齐次坐标(vec4)来表示点的位置。在绘制 3D 图形时,这种表示方式非常灵活:

  • 前三个分量 (x, y, z) 表示点在三维空间的位置。

  • 第四个分量 w 用于透视投影计算。例如,当 w = 1.0 时,点是标准的三维坐标;当 w 不等于 1.0 时,点的位置会被按比例缩放。

2、与变换矩阵配合: 在 3D 图形渲染中,变换矩阵通常是 4x4 的形式。将点表示为 vec4,使得可以直接通过矩阵乘法完成模型、视图、投影等变换操作。

3、兼容性: 即使是在 2D 绘制中,我们依然使用 vec4,这样在扩展到 3D 时无需修改顶点着色器。

结语

通过这篇文章,你已经了解了 WebGL 的基本工作流程:初始化上下文、创建着色器、传递顶点数据、绘制图形。从一个简单的点开始,我们逐步掌握 WebGL 的核心概念。下一步,我们将继续探索如何绘制更复杂的图形,例如 线条和三角形,敬请期待!

目录