WebGL 绘制三角形:完整实验教程
通过一次完整的实验了解从现实图形到计算机呈现的完整流水线。
适用对象:计算机图形学入门、本科实验课、WebGL 初学者
实验内容:从零使用 WebGL 1.0 + GLSL ES 绘制一个彩色三角形
学习目标:不仅把三角形画出来,还要真正理解 Canvas、WebGL 上下文、Buffer、Shader、attribute、varying、光栅化、片元和绘制流水线。
一、实验最终要完成什么
本实验最终在浏览器中绘制一个三角形:
- 左下顶点:红色;
- 右下顶点:绿色;
- 上方顶点:蓝色;
- 三角形内部颜色自动平滑过渡。
你最终会看到的核心流程是:
JavaScript 准备顶点数据
↓
上传数据到 GPU Buffer
↓
顶点着色器 Vertex Shader
↓
图元组装 Primitive Assembly
↓
裁剪 Clipping
↓
光栅化 Rasterization
↓
片元 Fragment
↓
片元着色器 Fragment Shader
↓
帧缓冲 Framebuffer
↓
Canvas 显示 最终真正发起绘制的代码只有一句:
gl.drawArrays(gl.TRIANGLES, 0, 3); 但在这句话之前,我们必须准备好顶点数据、GPU 缓冲区、着色器和顶点属性。
二、实验前必须理解的原理
2.1 WebGL 是什么
WebGL 是运行在浏览器中的图形 API。可以把它理解为 JavaScript 与 GPU 之间的一座桥梁:
JavaScript
↓
WebGL API
↓
浏览器图形系统
↓
GPU
↓
屏幕 WebGL 并不是让 JavaScript 自己一个像素一个像素地画图,而是让 JavaScript:
- 准备几何数据;
- 把数据上传到 GPU;
- 编写 GPU 执行的着色器;
- 发出绘制命令;
- 由 GPU 完成顶点处理、裁剪、光栅化和片元处理。
2.2 为什么使用 WebGL 1.0
本实验使用:
canvas.getContext("webgl") 即 WebGL 1.0。
原因是 WebGL 1.0 与教材中常见的 OpenGL ES 2.0 可编程流水线联系最直接,适合刚开始学习计算机图形学。
它的 GLSL 语法中常见:
attribute
varying
gl_FragColor 如果以后学习 WebGL 2.0,会看到:
in
out 等更现代的写法。
2.3 三角形为什么是图形学最重要的基本图元
现代 GPU 最核心的几何图元之一就是三角形。
因为三角形:
- 三点一定共面;
- 三角形内部定义唯一;
- 容易进行插值;
- 任意复杂多边形和三维模型都可以拆成三角形。
例如一个复杂三维器官模型,本质上通常是由大量三角形网格组成:
复杂三维模型
↓
成千上万个三角形
↓
GPU 并行处理 所以“画一个三角形”实际上是在学习整个实时图形学最基础的工作机制。
三、完整图形绘制流水线
本实验对应的简化流水线如下:
顶点数据
↓
Vertex Buffer
↓
Vertex Shader
↓
Primitive Assembly
↓
Clipping
↓
Rasterization
↓
Fragment
↓
Fragment Shader
↓
Per-fragment Operations
↓
Framebuffer
↓
Screen 必须理解各阶段职责。
3.1 顶点着色器
顶点着色器针对“每一个顶点”执行一次。
本实验有 3 个顶点,因此顶点着色器至少执行 3 次。
它最重要的输出是:
gl_Position 用于告诉后续流水线顶点最终位于哪里。
3.2 图元组装
当调用:
gl.drawArrays(gl.TRIANGLES, 0, 3); WebGL 会把 3 个顶点组合成一个三角形。
3.3 裁剪
如果三角形全部或部分超出可见裁剪区域,GPU 会进行裁剪。
裁剪解决的是:
哪些几何部分处在可观察范围内?
3.4 光栅化
三角形是连续的几何图形,而显示器是离散像素。
所以必须把三角形转换成覆盖屏幕区域的一批片元:
连续三角形
↓
Rasterization
↓
大量 Fragment 这就是光栅化。
一句话记忆:
光栅化 = 几何图元 → 片元。
3.5 片元着色器
片元着色器通常负责计算每一个片元的颜色。
本实验中:
gl_FragColor = vec4(v_color, 1.0); 就是把插值得到的颜色作为最终片元颜色。
四、实验环境准备
推荐软件:
- Visual Studio Code;
- Chrome / Edge / Firefox;
- VS Code 插件:Live Server(推荐)。
不需要安装 OpenGL SDK,也不需要额外框架。
创建项目文件夹:
webgl-triangle/ 在里面创建两个文件:
webgl-triangle/
├── index.html
└── main.js 其中:
index.html:创建网页和 Canvas;main.js:负责全部 WebGL 绘制操作。
五、实验 Step 1:创建 HTML 和 Canvas
在 index.html 中写入:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WebGL Triangle</title>
<style>
body {
margin: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: #eeeeee;
}
canvas {
border: 1px solid #333333;
}
</style>
</head>
<body>
<canvas id="glCanvas" width="800" height="600"></canvas>
<script src="main.js"></script>
</body>
</html> 原理
这一句:
<canvas id="glCanvas" width="800" height="600"></canvas> 创建了一个 800×600 的绘图区域。
注意:
Canvas ≠ WebGL Canvas 只是绘图容器。之后还要让它进入 WebGL 模式。
六、实验 Step 2:获取 WebGL 上下文
在 main.js 中首先写:
const canvas = document.getElementById("glCanvas"); 找到刚刚创建的 Canvas。
然后:
const gl = canvas.getContext("webgl"); 这里得到的 gl 是以后调用 WebGL API 的核心对象。
加入检测:
if (!gl) {
alert("当前浏览器不支持 WebGL");
throw new Error("WebGL not supported");
} 此时代码为:
const canvas = document.getElementById("glCanvas");
const gl = canvas.getContext("webgl");
if (!gl) {
alert("当前浏览器不支持 WebGL");
throw new Error("WebGL not supported");
} 七、实验 Step 3:设置视口和清屏
设置视口:
gl.viewport(0, 0, canvas.width, canvas.height); 表示将最终标准化坐标映射到整个 Canvas。
设置背景颜色:
gl.clearColor(0.1, 0.1, 0.1, 1.0); 四个参数分别代表:
R G B A WebGL 中颜色范围通常是:
0.0 ~ 1.0 然后清空颜色缓冲区:
gl.clear(gl.COLOR_BUFFER_BIT); 原理
帧缓冲可以简化理解为:
Framebuffer
├── Color Buffer
├── Depth Buffer
└── Stencil Buffer 现在只使用颜色缓冲区。
八、实验 Step 4:定义三角形顶点
定义:
const vertices = new Float32Array([
-0.6, -0.5,
0.6, -0.5,
0.0, 0.6
]); 三个点分别是:
A = (-0.6, -0.5)
B = ( 0.6, -0.5)
C = ( 0.0, 0.6) 逻辑图:
C(0.0, 0.6)
/\
/ \
/ \
/ \
/ \
A(-0.6,-0.5) ------------ B(0.6,-0.5) 九、为什么不是用像素坐标
这里不是:
(100, 500)
(700, 500)
(400, 100) 而是使用 -1 到 1 附近的坐标。
在完整三维图形学中,坐标会经历:
模型空间
↓
世界空间
↓
观察空间
↓
裁剪空间
↓
NDC
↓
屏幕空间 本实验为了入门,直接让顶点着色器输出:
gl_Position = vec4(a_position, 0.0, 1.0); 因为 w = 1,所以透视除法后:
x_ndc = x / w = x
y_ndc = y / w = y 因此可以暂时直接把顶点写在标准化设备坐标附近:
x ∈ [-1, 1]
y ∈ [-1, 1] 十、实验 Step 5:创建顶点位置 Buffer
JavaScript 数组默认在 CPU 内存中,而 GPU 需要从自己的缓冲区读取顶点数据。
创建 Buffer:
const positionBuffer = gl.createBuffer(); 绑定:
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); 上传数据:
gl.bufferData(
gl.ARRAY_BUFFER,
vertices,
gl.STATIC_DRAW
); 完整过程:
JavaScript Float32Array
↓
gl.bufferData()
↓
GPU Buffer gl.STATIC_DRAW
表示这批顶点数据不会频繁改变,主要用于绘制。
十一、实验 Step 6:定义顶点颜色
定义:
const colors = new Float32Array([
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0
]); 对应:
A → 红色
B → 绿色
C → 蓝色 创建颜色 Buffer:
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(
gl.ARRAY_BUFFER,
colors,
gl.STATIC_DRAW
); 因此 GPU 现在至少有两块数据:
positionBuffer → 顶点位置
colorBuffer → 顶点颜色 十二、实验 Step 7:编写顶点着色器
写:
const vertexShaderSource = `
attribute vec2 a_position;
attribute vec3 a_color;
varying vec3 v_color;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_color = a_color;
}
`; 逐句解释
1. attribute vec2 a_position
attribute vec2 a_position; 表示每个顶点具有一个二维位置。
例如第一个顶点得到:
(-0.6, -0.5) 2. attribute vec3 a_color
attribute vec3 a_color; 表示每个顶点还有 RGB 三个颜色分量。
3. varying vec3 v_color
varying vec3 v_color; 它负责把颜色从顶点着色器传向后续流水线。
4. gl_Position
gl_Position = vec4(a_position, 0.0, 1.0); 顶点着色器必须给 gl_Position 写入最终顶点位置。
vec4 表示:
(x, y, z, w) 这里等价于:
(x, y, 0, 1) 5. 传递颜色
v_color = a_color; 表示:
顶点颜色
↓
顶点着色器输出
↓
光栅化阶段插值
↓
片元着色器输入 十三、实验 Step 8:编写片元着色器
const fragmentShaderSource = `
precision mediump float;
varying vec3 v_color;
void main() {
gl_FragColor = vec4(v_color, 1.0);
}
`; 原理
precision mediump float
指定片元着色器浮点数默认精度。
varying vec3 v_color
接收光栅化阶段插值得到的颜色。
gl_FragColor
gl_FragColor = vec4(v_color, 1.0); 表示当前片元最终输出的 RGBA 颜色。
十四、实验 Step 9:编译 Shader
GLSL 源码目前只是 JavaScript 字符串,需要编译为 GPU 可以执行的 Shader。
写一个通用函数:
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(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
} 编译顶点着色器:
const vertexShader = createShader(
gl,
gl.VERTEX_SHADER,
vertexShaderSource
); 编译片元着色器:
const fragmentShader = createShader(
gl,
gl.FRAGMENT_SHADER,
fragmentShaderSource
); 十五、实验 Step 10:链接 Shader Program
顶点着色器与片元着色器要组成一个完整 GPU 程序:
Vertex Shader
+
Fragment Shader
↓
Shader Program 写:
function createProgram(gl, vertexShader, fragmentShader) {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
const success = gl.getProgramParameter(
program,
gl.LINK_STATUS
);
if (!success) {
console.error(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
return null;
}
return program;
} 调用:
const program = createProgram(
gl,
vertexShader,
fragmentShader
); 启用:
gl.useProgram(program); 十六、实验 Step 11:把位置 Buffer 与 a_position 连接起来
先获取 Shader 中变量位置:
const positionLocation = gl.getAttribLocation(
program,
"a_position"
); 绑定位置 Buffer:
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); 说明数据格式:
gl.vertexAttribPointer(
positionLocation,
2,
gl.FLOAT,
false,
0,
0
); 启用:
gl.enableVertexAttribArray(positionLocation); vertexAttribPointer 参数解释
gl.vertexAttribPointer(
positionLocation,
2,
gl.FLOAT,
false,
0,
0
); 含义依次是:
| 参数 | 含义 |
|---|---|
positionLocation | 数据送给哪个 attribute |
2 | 每个顶点读取 2 个分量,即 x、y |
gl.FLOAT | 数据类型是 float |
false | 不进行归一化 |
0 | 数据紧密排列 |
0 | 从第 0 字节开始读取 |
Buffer 中的数据:
-0.6 -0.5 0.6 -0.5 0.0 0.6 会自动按照每 2 个 float 分组为:
顶点 0 → (-0.6, -0.5)
顶点 1 → ( 0.6, -0.5)
顶点 2 → ( 0.0, 0.6) 十七、实验 Step 12:把颜色 Buffer 与 a_color 连接起来
获取:
const colorLocation = gl.getAttribLocation(
program,
"a_color"
); 绑定颜色 Buffer:
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); 配置:
gl.vertexAttribPointer(
colorLocation,
3,
gl.FLOAT,
false,
0,
0
); 启用:
gl.enableVertexAttribArray(colorLocation); 因为 RGB 有三个分量,所以这里写:
size = 3 十八、实验 Step 13:执行绘制
最后调用:
gl.drawArrays(
gl.TRIANGLES,
0,
3
); 参数解释:
gl.TRIANGLES → 按三角形图元解释顶点
0 → 从第 0 个顶点开始
3 → 处理 3 个顶点 于是:
顶点 0 + 顶点 1 + 顶点 2
↓
一个三角形 十九、最终完整源码
19.1 index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WebGL Triangle</title>
<style>
body {
margin: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: #eeeeee;
}
canvas {
border: 1px solid #333333;
}
</style>
</head>
<body>
<canvas id="glCanvas" width="800" height="600"></canvas>
<script src="main.js"></script>
</body>
</html> 19.2 main.js
// ======================================================
// 1. 获取 Canvas
// ======================================================
const canvas = document.getElementById("glCanvas");
// ======================================================
// 2. 获取 WebGL 上下文
// ======================================================
const gl = canvas.getContext("webgl");
if (!gl) {
alert("当前浏览器不支持 WebGL");
throw new Error("WebGL not supported");
}
// ======================================================
// 3. 设置视口
// ======================================================
gl.viewport(0, 0, canvas.width, canvas.height);
// ======================================================
// 4. 设置背景并清屏
// ======================================================
gl.clearColor(0.1, 0.1, 0.1, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// ======================================================
// 5. 定义顶点位置
// ======================================================
const vertices = new Float32Array([
-0.6, -0.5,
0.6, -0.5,
0.0, 0.6
]);
// ======================================================
// 6. 创建位置 Buffer
// ======================================================
const positionBuffer = gl.createBuffer();
gl.bindBuffer(
gl.ARRAY_BUFFER,
positionBuffer
);
gl.bufferData(
gl.ARRAY_BUFFER,
vertices,
gl.STATIC_DRAW
);
// ======================================================
// 7. 定义三个顶点的颜色
// ======================================================
const colors = new Float32Array([
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0
]);
// ======================================================
// 8. 创建颜色 Buffer
// ======================================================
const colorBuffer = gl.createBuffer();
gl.bindBuffer(
gl.ARRAY_BUFFER,
colorBuffer
);
gl.bufferData(
gl.ARRAY_BUFFER,
colors,
gl.STATIC_DRAW
);
// ======================================================
// 9. 顶点着色器源码
// ======================================================
const vertexShaderSource = `
attribute vec2 a_position;
attribute vec3 a_color;
varying vec3 v_color;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_color = a_color;
}
`;
// ======================================================
// 10. 片元着色器源码
// ======================================================
const fragmentShaderSource = `
precision mediump float;
varying vec3 v_color;
void main() {
gl_FragColor = vec4(v_color, 1.0);
}
`;
// ======================================================
// 11. 创建并编译 Shader
// ======================================================
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(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
// ======================================================
// 12. 编译顶点着色器
// ======================================================
const vertexShader = createShader(
gl,
gl.VERTEX_SHADER,
vertexShaderSource
);
// ======================================================
// 13. 编译片元着色器
// ======================================================
const fragmentShader = createShader(
gl,
gl.FRAGMENT_SHADER,
fragmentShaderSource
);
// ======================================================
// 14. 创建并链接 Shader Program
// ======================================================
function createProgram(gl, vertexShader, fragmentShader) {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
const success = gl.getProgramParameter(
program,
gl.LINK_STATUS
);
if (!success) {
console.error(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
return null;
}
return program;
}
// ======================================================
// 15. 创建 Program
// ======================================================
const program = createProgram(
gl,
vertexShader,
fragmentShader
);
// ======================================================
// 16. 使用 Program
// ======================================================
gl.useProgram(program);
// ======================================================
// 17. 配置顶点位置 attribute
// ======================================================
const positionLocation = gl.getAttribLocation(
program,
"a_position"
);
gl.bindBuffer(
gl.ARRAY_BUFFER,
positionBuffer
);
gl.vertexAttribPointer(
positionLocation,
2,
gl.FLOAT,
false,
0,
0
);
gl.enableVertexAttribArray(positionLocation);
// ======================================================
// 18. 配置顶点颜色 attribute
// ======================================================
const colorLocation = gl.getAttribLocation(
program,
"a_color"
);
gl.bindBuffer(
gl.ARRAY_BUFFER,
colorBuffer
);
gl.vertexAttribPointer(
colorLocation,
3,
gl.FLOAT,
false,
0,
0
);
gl.enableVertexAttribArray(colorLocation);
// ======================================================
// 19. 绘制三角形
// ======================================================
gl.drawArrays(
gl.TRIANGLES,
0,
3
); 二十、从 drawArrays 到屏幕出现三角形,GPU 内部发生了什么
这是本实验最重要的理解部分。
执行:
gl.drawArrays(gl.TRIANGLES, 0, 3); 以后:
Step 1:读取顶点 0
位置:
(-0.6, -0.5) 颜色:
(1, 0, 0) 顶点着色器输出:
(-0.6, -0.5, 0, 1) Step 2:读取顶点 1
位置:
(0.6, -0.5) 颜色:绿色。
Step 3:读取顶点 2
位置:
(0.0, 0.6) 颜色:蓝色。
Step 4:图元组装
因为使用:
gl.TRIANGLES 所以三个点组装为一个三角形。
Step 5:裁剪
GPU 检查三角形是否位于可见裁剪区域。
Step 6:光栅化
连续三角形转换成一批离散片元:
Triangle
↓
Rasterizer
↓
Fragments Step 7:自动插值颜色
三个顶点颜色为红、绿、蓝。
GPU 自动在三角形内部插值,所以内部呈渐变效果。
Step 8:片元着色器执行
每个片元执行:
gl_FragColor = vec4(v_color, 1.0); Step 9:写入帧缓冲
通过后续处理的片元写入 Color Buffer,最终显示在 Canvas。
二十一、为什么内部会出现颜色渐变
关键是 varying 的插值。
三个顶点颜色分别记为:
C_A
C_B
C_C 三角形内部某点 P 的颜色可以概念性理解为:
C(P) = αC_A + βC_B + γC_C 其中:
α + β + γ = 1 所以靠近红色顶点时红色权重大,靠近绿色顶点时绿色权重大。
这实际上与三角形的重心坐标插值有关。
因此你不需要自己计算每个像素颜色,GPU 光栅化器会自动完成。
二十二、attribute、varying、uniform 要怎么区分
attribute
每个顶点可以不同。
典型数据:
顶点位置
顶点颜色
法向量
纹理坐标 本实验:
attribute vec2 a_position;
attribute vec3 a_color; varying
用于从顶点着色器向片元着色器传递数据,并在光栅化过程中自动插值。
本实验:
varying vec3 v_color; uniform
一次绘制调用过程中,对所有顶点/片元通常保持一致。
例如:
变换矩阵
光源位置
相机参数
统一颜色
时间 二十三、关键 WebGL API 总结
| API | 作用 |
|---|---|
getContext("webgl") | 获取 WebGL 上下文 |
gl.viewport() | 设置视口 |
gl.clearColor() | 设置清屏颜色 |
gl.clear() | 清空缓冲区 |
gl.createBuffer() | 创建 GPU Buffer |
gl.bindBuffer() | 绑定 Buffer |
gl.bufferData() | 上传数据到 GPU |
gl.createShader() | 创建 Shader 对象 |
gl.shaderSource() | 设置 Shader 源码 |
gl.compileShader() | 编译 Shader |
gl.createProgram() | 创建 Program |
gl.attachShader() | 附加 Shader |
gl.linkProgram() | 链接 Program |
gl.useProgram() | 使用 Program |
gl.getAttribLocation() | 获取 attribute 位置 |
gl.vertexAttribPointer() | 描述 Buffer 数据布局 |
gl.enableVertexAttribArray() | 启用顶点属性 |
gl.drawArrays() | 发起绘制 |
二十四、常见错误及调试方法
24.1 页面只有背景,没有三角形
按:
F12 打开开发者工具,检查 Console。
24.2 Shader 编译失败
必须查看:
gl.getShaderInfoLog(shader) 常见原因:
少写分号
变量名写错
GLSL 类型不匹配
片元着色器没有 precision 24.3 Program 链接失败
查看:
gl.getProgramInfoLog(program) 典型错误是:
顶点着色器写:
varying vec3 v_color; 片元着色器写成:
varying vec4 v_color; 类型不一致会导致链接失败。
24.4 忘记启用 attribute
如果少了:
gl.enableVertexAttribArray(positionLocation); 顶点数据不能按预期读取。
24.5 attribute 名称拼错
Shader:
attribute vec2 a_position; JavaScript 必须:
gl.getAttribLocation(program, "a_position"); 名称必须完全一致。
24.6 drawArrays 数量写错
本实验只有 3 个顶点:
gl.drawArrays(gl.TRIANGLES, 0, 3); 24.7 顶点跑出可见范围
例如:
const vertices = new Float32Array([
10, 10,
20, 10,
15, 20
]); 会全部超出当前可见区域并被裁剪。
二十五、实验结果应该观察什么
实验成功后不要只写“成功显示三角形”,应该观察:
- Canvas 背景正确清屏;
- 三角形位于画布中心;
- 左下顶点呈红色;
- 右下顶点呈绿色;
- 上方顶点呈蓝色;
- 三角形内部颜色连续变化;
- 修改顶点坐标后形状发生变化;
- 修改颜色数据后渐变结果发生变化。
二十六、实验报告写法
26.1 实验名称
基于 WebGL 的彩色三角形绘制实验 26.2 实验目的
可写:
通过 HTML5 Canvas 和 WebGL API 绘制彩色三角形,掌握 WebGL 程序的基本结构,理解顶点缓冲区、顶点着色器、片元着色器、Shader Program、顶点属性、光栅化与片元等基本概念,并熟悉现代 GPU 图形绘制流水线的基本工作过程。
26.3 实验原理
可写:
WebGL 使用基于 GPU 的可编程图形流水线完成图形绘制。JavaScript 首先创建顶点位置和颜色数据,并将其上传到 GPU 缓冲区。顶点着色器逐顶点计算裁剪空间位置并向后续阶段输出颜色属性;顶点经图元组装形成三角形,随后进行裁剪和光栅化。光栅化器在三角形内部生成片元,并自动对顶点属性进行插值。片元着色器使用插值后的颜色计算片元最终颜色,通过后续测试的片元写入帧缓冲并显示到 Canvas。
26.4 实验流程
创建 Canvas
↓
获取 WebGL 上下文
↓
设置 viewport
↓
准备顶点位置和颜色
↓
创建 Buffer 并上传数据
↓
编写 Vertex Shader
↓
编写 Fragment Shader
↓
编译 Shader
↓
链接 Program
↓
配置 attribute
↓
清空 Color Buffer
↓
drawArrays
↓
光栅化
↓
片元着色
↓
显示结果 26.5 实验结果分析
可写:
实验成功绘制了由三个顶点组成的三角形。三个顶点分别赋予红、绿、蓝三种颜色。颜色通过 varying 变量由顶点着色器传递给光栅化阶段,GPU 在三角形内部对其进行插值,因此三角形内部呈现连续平滑的颜色过渡。实验验证了顶点着色器、图元组装、光栅化和片元着色器在 WebGL 绘制流水线中的基本作用。
26.6 实验结论
可写:
本实验完成了 WebGL 基础绘制流程,掌握了从 CPU 端准备顶点数据、创建 GPU 缓冲区、编写和编译着色器、链接 Shader Program、配置顶点属性,到调用 drawArrays 发起绘制的完整过程,并进一步理解了几何图元经顶点处理、裁剪、光栅化和片元处理后转化为最终屏幕图像的过程。
二十七、进一步练习 1:改变三角形大小
原始顶点:
const vertices = new Float32Array([
-0.6, -0.5,
0.6, -0.5,
0.0, 0.6
]); 改小:
const vertices = new Float32Array([
-0.3, -0.25,
0.3, -0.25,
0.0, 0.3
]); 观察三角形尺寸变化。
二十八、进一步练习 2:把三角形改成纯黄色
颜色数据改为:
const colors = new Float32Array([
1.0, 1.0, 0.0,
1.0, 1.0, 0.0,
1.0, 1.0, 0.0
]); 三角形将基本呈现统一黄色。
二十九、进一步练习 3:使用 uniform 移动三角形
顶点着色器改为:
attribute vec2 a_position;
attribute vec3 a_color;
uniform vec2 u_offset;
varying vec3 v_color;
void main() {
vec2 newPosition = a_position + u_offset;
gl_Position = vec4(newPosition, 0.0, 1.0);
v_color = a_color;
} JavaScript 中获取:
const offsetLocation = gl.getUniformLocation(
program,
"u_offset"
); 设置:
gl.uniform2f(
offsetLocation,
0.3,
0.0
); 此时三角形向右移动。
这个练习可以帮助理解:
attribute → 每个顶点可以不同
uniform → 一次绘制过程中通常统一 三十、进一步练习 4:绘制两个三角形
如果准备 6 个顶点:
const vertices = new Float32Array([
-0.8, -0.5,
-0.2, -0.5,
-0.5, 0.4,
0.2, -0.5,
0.8, -0.5,
0.5, 0.4
]); 然后调用:
gl.drawArrays(
gl.TRIANGLES,
0,
6
); 因为 gl.TRIANGLES 每 3 个顶点组成一个三角形,所以 6 个顶点会形成两个三角形。
三十一、本实验必须达到的掌握标准
Level 1:会运行
能:
创建 Canvas
获取 WebGL 上下文
运行程序
看到三角形 这是最低标准。
Level 2:会解释
不看资料能够解释:
Buffer 是什么?
Vertex Shader 做什么?
Fragment Shader 做什么?
attribute 是什么?
varying 是什么?
uniform 是什么?
gl_Position 是什么?
gl_FragColor 是什么?
光栅化是什么?
Fragment 与 Pixel 有什么区别?
drawArrays 做什么? Level 3:会修改
能够独立:
修改三角形位置
修改大小
修改颜色
绘制两个三角形
使用 uniform 移动图形 Level 4:真正理解流水线
看到:
gl.drawArrays(gl.TRIANGLES, 0, 3); 脑中能自动展开:
顶点读取
↓
Vertex Shader
↓
Primitive Assembly
↓
Clipping
↓
Rasterization
↓
Interpolation
↓
Fragment Shader
↓
Framebuffer 达到这个程度,才算真正掌握了 WebGL 的第一个程序。
三十二、一页速查表
WebGL 绘制三角形完整流程
HTML Canvas
↓
getContext("webgl")
↓
准备顶点数据
↓
createBuffer
↓
bindBuffer
↓
bufferData
↓
Vertex Shader
↓
Fragment Shader
↓
compileShader
↓
createProgram
↓
linkProgram
↓
useProgram
↓
getAttribLocation
↓
vertexAttribPointer
↓
enableVertexAttribArray
↓
clear
↓
drawArrays 三个最关键的数据/命令
顶点位置
const vertices = new Float32Array([
-0.6, -0.5,
0.6, -0.5,
0.0, 0.6
]); 顶点颜色
const colors = new Float32Array([
1, 0, 0,
0, 1, 0,
0, 0, 1
]); 绘制命令
gl.drawArrays(
gl.TRIANGLES,
0,
3
); 三十三、最后必须掌握的一句话
整个实验最重要的不是背 API,而是理解:
JavaScript 负责准备数据和下达绘制命令;顶点着色器负责处理顶点;图元组装把顶点组成三角形;光栅化器把几何图元转成片元;片元着色器计算片元颜色;最终结果写入帧缓冲并显示到屏幕。
只要真正理解这句话,你就已经掌握了 WebGL 和现代实时图形绘制流水线最核心的入门思想。
0 次表情反馈 · 0 条评论