YCVisionLog
返回笔记
Computer Vision2026-10-1230 minYygz31413 人浏览已发布

WebGL 绘制三角形:完整实验教程

通过一次完整的实验了解从现实图形到计算机呈现的完整流水线。

Notes

适用对象:计算机图形学入门、本科实验课、WebGL 初学者
实验内容:从零使用 WebGL 1.0 + GLSL ES 绘制一个彩色三角形
学习目标:不仅把三角形画出来,还要真正理解 Canvas、WebGL 上下文、Buffer、Shader、attribute、varying、光栅化、片元和绘制流水线。


一、实验最终要完成什么

本实验最终在浏览器中绘制一个三角形:

  • 左下顶点:红色;
  • 右下顶点:绿色;
  • 上方顶点:蓝色;
  • 三角形内部颜色自动平滑过渡。

你最终会看到的核心流程是:

TEXT
JavaScript 准备顶点数据
        ↓
上传数据到 GPU Buffer
        ↓
顶点着色器 Vertex Shader
        ↓
图元组装 Primitive Assembly
        ↓
裁剪 Clipping
        ↓
光栅化 Rasterization
        ↓
片元 Fragment
        ↓
片元着色器 Fragment Shader
        ↓
帧缓冲 Framebuffer
        ↓
Canvas 显示 

最终真正发起绘制的代码只有一句:

JAVASCRIPT
gl.drawArrays(gl.TRIANGLES, 0, 3); 

但在这句话之前,我们必须准备好顶点数据、GPU 缓冲区、着色器和顶点属性。


二、实验前必须理解的原理

2.1 WebGL 是什么

WebGL 是运行在浏览器中的图形 API。可以把它理解为 JavaScript 与 GPU 之间的一座桥梁:

TEXT
JavaScript
   ↓
WebGL API
   ↓
浏览器图形系统
   ↓
GPU
   ↓
屏幕 

WebGL 并不是让 JavaScript 自己一个像素一个像素地画图,而是让 JavaScript:

  1. 准备几何数据;
  2. 把数据上传到 GPU;
  3. 编写 GPU 执行的着色器;
  4. 发出绘制命令;
  5. 由 GPU 完成顶点处理、裁剪、光栅化和片元处理。

2.2 为什么使用 WebGL 1.0

本实验使用:

JAVASCRIPT
canvas.getContext("webgl") 

即 WebGL 1.0。

原因是 WebGL 1.0 与教材中常见的 OpenGL ES 2.0 可编程流水线联系最直接,适合刚开始学习计算机图形学。

它的 GLSL 语法中常见:

GLSL
attribute
varying
gl_FragColor 

如果以后学习 WebGL 2.0,会看到:

GLSL
in
out 

等更现代的写法。


2.3 三角形为什么是图形学最重要的基本图元

现代 GPU 最核心的几何图元之一就是三角形。

因为三角形:

  • 三点一定共面;
  • 三角形内部定义唯一;
  • 容易进行插值;
  • 任意复杂多边形和三维模型都可以拆成三角形。

例如一个复杂三维器官模型,本质上通常是由大量三角形网格组成:

TEXT
复杂三维模型
     ↓
成千上万个三角形
     ↓
GPU 并行处理 

所以“画一个三角形”实际上是在学习整个实时图形学最基础的工作机制。


三、完整图形绘制流水线

本实验对应的简化流水线如下:

TEXT
顶点数据
   ↓
Vertex Buffer
   ↓
Vertex Shader
   ↓
Primitive Assembly
   ↓
Clipping
   ↓
Rasterization
   ↓
Fragment
   ↓
Fragment Shader
   ↓
Per-fragment Operations
   ↓
Framebuffer
   ↓
Screen 

必须理解各阶段职责。

3.1 顶点着色器

顶点着色器针对“每一个顶点”执行一次。

本实验有 3 个顶点,因此顶点着色器至少执行 3 次。

它最重要的输出是:

GLSL
gl_Position 

用于告诉后续流水线顶点最终位于哪里。


3.2 图元组装

当调用:

JAVASCRIPT
gl.drawArrays(gl.TRIANGLES, 0, 3); 

WebGL 会把 3 个顶点组合成一个三角形。


3.3 裁剪

如果三角形全部或部分超出可见裁剪区域,GPU 会进行裁剪。

裁剪解决的是:

哪些几何部分处在可观察范围内?


3.4 光栅化

三角形是连续的几何图形,而显示器是离散像素。

所以必须把三角形转换成覆盖屏幕区域的一批片元:

TEXT
连续三角形
    ↓
Rasterization
    ↓
大量 Fragment 

这就是光栅化。

一句话记忆:

光栅化 = 几何图元 → 片元。


3.5 片元着色器

片元着色器通常负责计算每一个片元的颜色。

本实验中:

GLSL
gl_FragColor = vec4(v_color, 1.0); 

就是把插值得到的颜色作为最终片元颜色。


四、实验环境准备

推荐软件:

  • Visual Studio Code;
  • Chrome / Edge / Firefox;
  • VS Code 插件:Live Server(推荐)。

不需要安装 OpenGL SDK,也不需要额外框架。

创建项目文件夹:

TEXT
webgl-triangle/ 

在里面创建两个文件:

TEXT
webgl-triangle/
├── index.html
└── main.js 

其中:

  • index.html:创建网页和 Canvas;
  • main.js:负责全部 WebGL 绘制操作。

五、实验 Step 1:创建 HTML 和 Canvas

在 index.html 中写入:

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> 

原理

这一句:

HTML
<canvas id="glCanvas" width="800" height="600"></canvas> 

创建了一个 800×600 的绘图区域。

注意:

TEXT
Canvas ≠ WebGL 

Canvas 只是绘图容器。之后还要让它进入 WebGL 模式。


六、实验 Step 2:获取 WebGL 上下文

在 main.js 中首先写:

JAVASCRIPT
const canvas = document.getElementById("glCanvas"); 

找到刚刚创建的 Canvas。

然后:

JAVASCRIPT
const gl = canvas.getContext("webgl"); 

这里得到的 gl 是以后调用 WebGL API 的核心对象。

加入检测:

JAVASCRIPT
if (!gl) {
    alert("当前浏览器不支持 WebGL");
    throw new Error("WebGL not supported");
} 

此时代码为:

JAVASCRIPT
const canvas = document.getElementById("glCanvas");

const gl = canvas.getContext("webgl");

if (!gl) {
    alert("当前浏览器不支持 WebGL");
    throw new Error("WebGL not supported");
} 

七、实验 Step 3:设置视口和清屏

设置视口:

JAVASCRIPT
gl.viewport(0, 0, canvas.width, canvas.height); 

表示将最终标准化坐标映射到整个 Canvas。

设置背景颜色:

JAVASCRIPT
gl.clearColor(0.1, 0.1, 0.1, 1.0); 

四个参数分别代表:

TEXT
R G B A 

WebGL 中颜色范围通常是:

TEXT
0.0 ~ 1.0 

然后清空颜色缓冲区:

JAVASCRIPT
gl.clear(gl.COLOR_BUFFER_BIT); 

原理

帧缓冲可以简化理解为:

TEXT
Framebuffer
├── Color Buffer
├── Depth Buffer
└── Stencil Buffer 

现在只使用颜色缓冲区。


八、实验 Step 4:定义三角形顶点

定义:

JAVASCRIPT
const vertices = new Float32Array([
    -0.6, -0.5,
     0.6, -0.5,
     0.0,  0.6
]); 

三个点分别是:

TEXT
A = (-0.6, -0.5)
B = ( 0.6, -0.5)
C = ( 0.0,  0.6) 

逻辑图:

TEXT
             C(0.0, 0.6)
                  /\
                 /  \
                /    \
               /      \
              /        \
A(-0.6,-0.5) ------------ B(0.6,-0.5) 

九、为什么不是用像素坐标

这里不是:

TEXT
(100, 500)
(700, 500)
(400, 100) 

而是使用 -1 到 1 附近的坐标。

在完整三维图形学中,坐标会经历:

TEXT
模型空间
   ↓
世界空间
   ↓
观察空间
   ↓
裁剪空间
   ↓
NDC
   ↓
屏幕空间 

本实验为了入门,直接让顶点着色器输出:

GLSL
gl_Position = vec4(a_position, 0.0, 1.0); 

因为 w = 1,所以透视除法后:

TEXT
x_ndc = x / w = x
y_ndc = y / w = y 

因此可以暂时直接把顶点写在标准化设备坐标附近:

TEXT
x ∈ [-1, 1]
y ∈ [-1, 1] 

十、实验 Step 5:创建顶点位置 Buffer

JavaScript 数组默认在 CPU 内存中,而 GPU 需要从自己的缓冲区读取顶点数据。

创建 Buffer:

JAVASCRIPT
const positionBuffer = gl.createBuffer(); 

绑定:

JAVASCRIPT
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); 

上传数据:

JAVASCRIPT
gl.bufferData(
    gl.ARRAY_BUFFER,
    vertices,
    gl.STATIC_DRAW
); 

完整过程:

TEXT
JavaScript Float32Array
       ↓
gl.bufferData()
       ↓
GPU Buffer 

gl.STATIC_DRAW

表示这批顶点数据不会频繁改变,主要用于绘制。


十一、实验 Step 6:定义顶点颜色

定义:

JAVASCRIPT
const colors = new Float32Array([
    1.0, 0.0, 0.0,
    0.0, 1.0, 0.0,
    0.0, 0.0, 1.0
]); 

对应:

TEXT
A → 红色
B → 绿色
C → 蓝色 

创建颜色 Buffer:

JAVASCRIPT
const colorBuffer = gl.createBuffer();

gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);

gl.bufferData(
    gl.ARRAY_BUFFER,
    colors,
    gl.STATIC_DRAW
); 

因此 GPU 现在至少有两块数据:

TEXT
positionBuffer → 顶点位置
colorBuffer    → 顶点颜色 

十二、实验 Step 7:编写顶点着色器

写:

JAVASCRIPT
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

GLSL
attribute vec2 a_position; 

表示每个顶点具有一个二维位置。

例如第一个顶点得到:

TEXT
(-0.6, -0.5) 

2. attribute vec3 a_color

GLSL
attribute vec3 a_color; 

表示每个顶点还有 RGB 三个颜色分量。

3. varying vec3 v_color

GLSL
varying vec3 v_color; 

它负责把颜色从顶点着色器传向后续流水线。

4. gl_Position

GLSL
gl_Position = vec4(a_position, 0.0, 1.0); 

顶点着色器必须给 gl_Position 写入最终顶点位置。

vec4 表示:

TEXT
(x, y, z, w) 

这里等价于:

TEXT
(x, y, 0, 1) 

5. 传递颜色

GLSL
v_color = a_color; 

表示:

TEXT
顶点颜色
   ↓
顶点着色器输出
   ↓
光栅化阶段插值
   ↓
片元着色器输入 

十三、实验 Step 8:编写片元着色器

JAVASCRIPT
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

GLSL
gl_FragColor = vec4(v_color, 1.0); 

表示当前片元最终输出的 RGBA 颜色。


十四、实验 Step 9:编译 Shader

GLSL 源码目前只是 JavaScript 字符串,需要编译为 GPU 可以执行的 Shader。

写一个通用函数:

JAVASCRIPT
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;
} 

编译顶点着色器:

JAVASCRIPT
const vertexShader = createShader(
    gl,
    gl.VERTEX_SHADER,
    vertexShaderSource
); 

编译片元着色器:

JAVASCRIPT
const fragmentShader = createShader(
    gl,
    gl.FRAGMENT_SHADER,
    fragmentShaderSource
); 

十五、实验 Step 10:链接 Shader Program

顶点着色器与片元着色器要组成一个完整 GPU 程序:

TEXT
Vertex Shader
      +
Fragment Shader
      ↓
Shader Program 

写:

JAVASCRIPT
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;
} 

调用:

JAVASCRIPT
const program = createProgram(
    gl,
    vertexShader,
    fragmentShader
); 

启用:

JAVASCRIPT
gl.useProgram(program); 

十六、实验 Step 11:把位置 Buffer 与 a_position 连接起来

先获取 Shader 中变量位置:

JAVASCRIPT
const positionLocation = gl.getAttribLocation(
    program,
    "a_position"
); 

绑定位置 Buffer:

JAVASCRIPT
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); 

说明数据格式:

JAVASCRIPT
gl.vertexAttribPointer(
    positionLocation,
    2,
    gl.FLOAT,
    false,
    0,
    0
); 

启用:

JAVASCRIPT
gl.enableVertexAttribArray(positionLocation); 

vertexAttribPointer 参数解释

JAVASCRIPT
gl.vertexAttribPointer(
    positionLocation,
    2,
    gl.FLOAT,
    false,
    0,
    0
); 

含义依次是:

参数含义
positionLocation数据送给哪个 attribute
2每个顶点读取 2 个分量,即 x、y
gl.FLOAT数据类型是 float
false不进行归一化
0数据紧密排列
0从第 0 字节开始读取

Buffer 中的数据:

TEXT
-0.6 -0.5   0.6 -0.5   0.0 0.6 

会自动按照每 2 个 float 分组为:

TEXT
顶点 0 → (-0.6, -0.5)
顶点 1 → ( 0.6, -0.5)
顶点 2 → ( 0.0,  0.6) 

十七、实验 Step 12:把颜色 Buffer 与 a_color 连接起来

获取:

JAVASCRIPT
const colorLocation = gl.getAttribLocation(
    program,
    "a_color"
); 

绑定颜色 Buffer:

JAVASCRIPT
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); 

配置:

JAVASCRIPT
gl.vertexAttribPointer(
    colorLocation,
    3,
    gl.FLOAT,
    false,
    0,
    0
); 

启用:

JAVASCRIPT
gl.enableVertexAttribArray(colorLocation); 

因为 RGB 有三个分量,所以这里写:

TEXT
size = 3 

十八、实验 Step 13:执行绘制

最后调用:

JAVASCRIPT
gl.drawArrays(
    gl.TRIANGLES,
    0,
    3
); 

参数解释:

TEXT
gl.TRIANGLES → 按三角形图元解释顶点
0            → 从第 0 个顶点开始
3            → 处理 3 个顶点 

于是:

TEXT
顶点 0 + 顶点 1 + 顶点 2
           ↓
        一个三角形 

十九、最终完整源码

19.1 index.html

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

JAVASCRIPT
// ======================================================
// 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 内部发生了什么

这是本实验最重要的理解部分。

执行:

JAVASCRIPT
gl.drawArrays(gl.TRIANGLES, 0, 3); 

以后:

Step 1:读取顶点 0

位置:

TEXT
(-0.6, -0.5) 

颜色:

TEXT
(1, 0, 0) 

顶点着色器输出:

TEXT
(-0.6, -0.5, 0, 1) 

Step 2:读取顶点 1

位置:

TEXT
(0.6, -0.5) 

颜色:绿色。

Step 3:读取顶点 2

位置:

TEXT
(0.0, 0.6) 

颜色:蓝色。

Step 4:图元组装

因为使用:

JAVASCRIPT
gl.TRIANGLES 

所以三个点组装为一个三角形。

Step 5:裁剪

GPU 检查三角形是否位于可见裁剪区域。

Step 6:光栅化

连续三角形转换成一批离散片元:

TEXT
Triangle
   ↓
Rasterizer
   ↓
Fragments 

Step 7:自动插值颜色

三个顶点颜色为红、绿、蓝。

GPU 自动在三角形内部插值,所以内部呈渐变效果。

Step 8:片元着色器执行

每个片元执行:

GLSL
gl_FragColor = vec4(v_color, 1.0); 

Step 9:写入帧缓冲

通过后续处理的片元写入 Color Buffer,最终显示在 Canvas。


二十一、为什么内部会出现颜色渐变

关键是 varying 的插值。

三个顶点颜色分别记为:

TEXT
C_A
C_B
C_C 

三角形内部某点 P 的颜色可以概念性理解为:

TEXT
C(P) = αC_A + βC_B + γC_C 

其中:

TEXT
α + β + γ = 1 

所以靠近红色顶点时红色权重大,靠近绿色顶点时绿色权重大。

这实际上与三角形的重心坐标插值有关。

因此你不需要自己计算每个像素颜色,GPU 光栅化器会自动完成。


二十二、attribute、varying、uniform 要怎么区分

attribute

每个顶点可以不同。

典型数据:

TEXT
顶点位置
顶点颜色
法向量
纹理坐标 

本实验:

GLSL
attribute vec2 a_position;
attribute vec3 a_color; 

varying

用于从顶点着色器向片元着色器传递数据,并在光栅化过程中自动插值。

本实验:

GLSL
varying vec3 v_color; 

uniform

一次绘制调用过程中,对所有顶点/片元通常保持一致。

例如:

TEXT
变换矩阵
光源位置
相机参数
统一颜色
时间 

二十三、关键 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 页面只有背景,没有三角形

按:

TEXT
F12 

打开开发者工具,检查 Console。


24.2 Shader 编译失败

必须查看:

JAVASCRIPT
gl.getShaderInfoLog(shader) 

常见原因:

TEXT
少写分号
变量名写错
GLSL 类型不匹配
片元着色器没有 precision 

24.3 Program 链接失败

查看:

JAVASCRIPT
gl.getProgramInfoLog(program) 

典型错误是:

顶点着色器写:

GLSL
varying vec3 v_color; 

片元着色器写成:

GLSL
varying vec4 v_color; 

类型不一致会导致链接失败。


24.4 忘记启用 attribute

如果少了:

JAVASCRIPT
gl.enableVertexAttribArray(positionLocation); 

顶点数据不能按预期读取。


24.5 attribute 名称拼错

Shader:

GLSL
attribute vec2 a_position; 

JavaScript 必须:

JAVASCRIPT
gl.getAttribLocation(program, "a_position"); 

名称必须完全一致。


24.6 drawArrays 数量写错

本实验只有 3 个顶点:

JAVASCRIPT
gl.drawArrays(gl.TRIANGLES, 0, 3); 

24.7 顶点跑出可见范围

例如:

JAVASCRIPT
const vertices = new Float32Array([
    10, 10,
    20, 10,
    15, 20
]); 

会全部超出当前可见区域并被裁剪。


二十五、实验结果应该观察什么

实验成功后不要只写“成功显示三角形”,应该观察:

  1. Canvas 背景正确清屏;
  2. 三角形位于画布中心;
  3. 左下顶点呈红色;
  4. 右下顶点呈绿色;
  5. 上方顶点呈蓝色;
  6. 三角形内部颜色连续变化;
  7. 修改顶点坐标后形状发生变化;
  8. 修改颜色数据后渐变结果发生变化。

二十六、实验报告写法

26.1 实验名称

TEXT
基于 WebGL 的彩色三角形绘制实验 

26.2 实验目的

可写:

通过 HTML5 Canvas 和 WebGL API 绘制彩色三角形,掌握 WebGL 程序的基本结构,理解顶点缓冲区、顶点着色器、片元着色器、Shader Program、顶点属性、光栅化与片元等基本概念,并熟悉现代 GPU 图形绘制流水线的基本工作过程。

26.3 实验原理

可写:

WebGL 使用基于 GPU 的可编程图形流水线完成图形绘制。JavaScript 首先创建顶点位置和颜色数据,并将其上传到 GPU 缓冲区。顶点着色器逐顶点计算裁剪空间位置并向后续阶段输出颜色属性;顶点经图元组装形成三角形,随后进行裁剪和光栅化。光栅化器在三角形内部生成片元,并自动对顶点属性进行插值。片元着色器使用插值后的颜色计算片元最终颜色,通过后续测试的片元写入帧缓冲并显示到 Canvas。

26.4 实验流程

TEXT
创建 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:改变三角形大小

原始顶点:

JAVASCRIPT
const vertices = new Float32Array([
    -0.6, -0.5,
     0.6, -0.5,
     0.0,  0.6
]); 

改小:

JAVASCRIPT
const vertices = new Float32Array([
    -0.3, -0.25,
     0.3, -0.25,
     0.0,  0.3
]); 

观察三角形尺寸变化。


二十八、进一步练习 2:把三角形改成纯黄色

颜色数据改为:

JAVASCRIPT
const colors = new Float32Array([
    1.0, 1.0, 0.0,
    1.0, 1.0, 0.0,
    1.0, 1.0, 0.0
]); 

三角形将基本呈现统一黄色。


二十九、进一步练习 3:使用 uniform 移动三角形

顶点着色器改为:

GLSL
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 中获取:

JAVASCRIPT
const offsetLocation = gl.getUniformLocation(
    program,
    "u_offset"
); 

设置:

JAVASCRIPT
gl.uniform2f(
    offsetLocation,
    0.3,
    0.0
); 

此时三角形向右移动。

这个练习可以帮助理解:

TEXT
attribute → 每个顶点可以不同
uniform   → 一次绘制过程中通常统一 

三十、进一步练习 4:绘制两个三角形

如果准备 6 个顶点:

JAVASCRIPT
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
]); 

然后调用:

JAVASCRIPT
gl.drawArrays(
    gl.TRIANGLES,
    0,
    6
); 

因为 gl.TRIANGLES 每 3 个顶点组成一个三角形,所以 6 个顶点会形成两个三角形。


三十一、本实验必须达到的掌握标准

Level 1:会运行

能:

TEXT
创建 Canvas
获取 WebGL 上下文
运行程序
看到三角形 

这是最低标准。

Level 2:会解释

不看资料能够解释:

TEXT
Buffer 是什么?
Vertex Shader 做什么?
Fragment Shader 做什么?
attribute 是什么?
varying 是什么?
uniform 是什么?
gl_Position 是什么?
gl_FragColor 是什么?
光栅化是什么?
Fragment 与 Pixel 有什么区别?
drawArrays 做什么? 

Level 3:会修改

能够独立:

TEXT
修改三角形位置
修改大小
修改颜色
绘制两个三角形
使用 uniform 移动图形 

Level 4:真正理解流水线

看到:

JAVASCRIPT
gl.drawArrays(gl.TRIANGLES, 0, 3); 

脑中能自动展开:

TEXT
顶点读取
 ↓
Vertex Shader
 ↓
Primitive Assembly
 ↓
Clipping
 ↓
Rasterization
 ↓
Interpolation
 ↓
Fragment Shader
 ↓
Framebuffer 

达到这个程度,才算真正掌握了 WebGL 的第一个程序。


三十二、一页速查表

WebGL 绘制三角形完整流程

TEXT
HTML Canvas
    ↓
getContext("webgl")
    ↓
准备顶点数据
    ↓
createBuffer
    ↓
bindBuffer
    ↓
bufferData
    ↓
Vertex Shader
    ↓
Fragment Shader
    ↓
compileShader
    ↓
createProgram
    ↓
linkProgram
    ↓
useProgram
    ↓
getAttribLocation
    ↓
vertexAttribPointer
    ↓
enableVertexAttribArray
    ↓
clear
    ↓
drawArrays 

三个最关键的数据/命令

顶点位置

JAVASCRIPT
const vertices = new Float32Array([
    -0.6, -0.5,
     0.6, -0.5,
     0.0,  0.6
]); 

顶点颜色

JAVASCRIPT
const colors = new Float32Array([
    1, 0, 0,
    0, 1, 0,
    0, 0, 1
]); 

绘制命令

JAVASCRIPT
gl.drawArrays(
    gl.TRIANGLES,
    0,
    3
); 

三十三、最后必须掌握的一句话

整个实验最重要的不是背 API,而是理解:

JavaScript 负责准备数据和下达绘制命令;顶点着色器负责处理顶点;图元组装把顶点组成三角形;光栅化器把几何图元转成片元;片元着色器计算片元颜色;最终结果写入帧缓冲并显示到屏幕。

只要真正理解这句话,你就已经掌握了 WebGL 和现代实时图形绘制流水线最核心的入门思想。

读者互动

0 次表情反馈 · 0 条评论

登录后可以发表评论、给评论点赞。去登录
还没有评论。