#sec-1
Computer Graphics · Chapter 2 · 03

一个完整的 WebGL 程序怎样运行起来?

Canvas · Context · Shader · Buffer · Attribute · Draw Call

HTML / Canvas
→
JavaScript
→
WebGL / GPU
→
Pixels
#sec-2-1
Bridge-in · 0–4 min

目标只有一个:让浏览器画出一个正方形

WebGL正方形示例

请先别看代码

要让这个图形出现,你认为至少需要哪些“角色”和“工具”?

快速列举90 s

HTML、Canvas、JavaScript、顶点数据、Shader……还能想到什么?

#sec-2-2
Program anatomy

完整程序的六个角色

HTML / Canvas

提供页面与绘图表面

JavaScript

组织程序、资源与调用

Vertex Data

顶点位置/颜色等

Shader

GPU执行的程序

Buffer / Attribute

上传并解释数据

Draw Call

触发绘制

#sec-3-1
Pre-assessment · 4–9 min

代码拼图:把六块代码排成合理顺序

A
获取 Canvas / WebGL2 context
初始化渲染上下文环境
B
定义 vertices 顶点数据
构建 CPU 端几何位置数组
C
编译并链接 Shader
生成 GPU 可执行着色器程序
D
创建 Buffer / 上传数据
将顶点数据送入显存缓冲区
E
Buffer ↔ Shader input 关联
配置顶点属性指针与启用属性
F
调用 drawArrays()
发出 Draw Call 触发 GPU 绘制
小组活动3 min

先给出六步顺序,并思考每一步“依赖什么、产生什么”。

参考逻辑顺序 A (Context) → B (顶点) → C (Shader) → D (Buffer) → E (关联) → F (Draw)

核心依赖规律:B 与 C 彼此独立可交换;但 E 强依赖 C 与 D,F 必须在所有资源配置就绪后最后调用。

#sec-4-1
HTML · 9–14 min

HTML负责“环境”,不是负责几何计算

<!DOCTYPE html>
<html>
<head>
  <script id="vertex-shader" type="x-shader/x-vertex">...</script>
  <script id="fragment-shader" type="x-shader/x-fragment">...</script>
  <script src="square.js"></script>
</head>
<body>
  <canvas id="square-canvas" width="500" height="500"></canvas>
</body>
</html>

三个角色

  • <canvas>:绘图目标
  • Shader源码:GPU程序文本
  • square.js:应用逻辑
#sec-4-2
Canvas & context

JavaScript获取GPU入口

var canvas = document.getElementById("square-canvas");
var gl = canvas.getContext("webgl2");
if(!gl){ alert("WebGL 2.0 unavailable"); }

Context 是什么?

它是当前 Canvas 与 WebGL API 的调用入口;之后所有 gl.* 调用都通过它发出。

#sec-4-3
HTML source · Extension

完整HTML结构:不要逐行背,先看“角色分区”

<html>
<head>
  <meta charset="utf-8">
  <script id="vertex-shader" type="x-shader/x-vertex">...vertex shader...</script>
  <script id="fragment-shader" type="x-shader/x-fragment">...fragment shader...</script>
  <script src="../js/common/webgl-utils.js"></script>
  <script src="../js/common/initShaders.js"></script>
  <script src="../js/ch01/square.js"></script>
</head>
<body>
  <canvas id="square-canvas" width="500" height="500"></canvas>
</body>
</html>

原课件中的Shader可内嵌在HTML,也可根据项目结构拆为外部文件。

#sec-4-4
Utility files · Extension

辅助文件分别解决什么问题?

webgl-utils.js

对浏览器/WebGL环境相关的常用操作进行封装。

initShaders.js

读取、编译、链接Shader并创建Program。

矩阵/向量库

提供几何计算工具;教材不同版本可能使用MV.js或其它库。

square.js

真正的应用程序逻辑。

#sec-5-1
Shader program · 14–18 min

两段Shader都是完整GPU程序

Vertex Shader

#version 300 es
in vec4 aPosition;
void main(){
  gl_Position = aPosition;
}

Fragment Shader

#version 300 es
precision mediump float;
out vec4 fColor;
void main(){
  fColor = vec4(1.0, 1.0, 0.0, 1.0);
}
#sec-5-2
Compile & link

源码不能直接执行:先编译,再链接成 Program

Shader Source
→
Compile
→
Shader Objects
→
Link
→
Program
var program = initShaders(gl, "vertex-shader", "fragment-shader");
gl.useProgram(program);
#sec-6-1
Data · 18–23 min

正方形其实只是四个位置数据

var points = new Float32Array([
  -0.5, -0.5,
   0.5, -0.5,
   0.5,  0.5,
  -0.5,  0.5
]);

注意

这里只有8个浮点数。它们还没有告诉GPU“这是一块正方形”。

#sec-6-2
Buffer

把CPU数据上传到GPU

points
→
createBuffer
→
bindBuffer
→
bufferData
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
#sec-6-3
Attribute description

Buffer只是一串字节:GPU还需要知道“怎么读”

var loc = gl.getAttribLocation(program, "aPosition");
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(loc);

一句话解释

“从当前 Buffer 中,每次读取 2 个 float,送给 Shader 的 aPosition。”

#sec-6-4
JavaScript source · Part 1

完整程序前半:环境 + 数据 + 视口

"use strict";
var gl;
window.onload = function init(){
  var canvas = document.getElementById("square-canvas");
  gl = canvas.getContext("webgl2");
  var points = new Float32Array([
    -0.5, -0.5,   0.5, -0.5,
     0.5,  0.5,  -0.5,  0.5
  ]);
  gl.viewport(0, 0, canvas.width, canvas.height);
  gl.clearColor(1.0, 0.0, 0.0, 1.0);

阅读方式:先找“资源角色”,再看API细节。

#sec-6-5
JavaScript source · Part 2

完整程序后半:Program + Buffer + Attribute + Draw

  var program = initShaders(gl, "vertex-shader", "fragment-shader");
  gl.useProgram(program);
  var buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
  var aPosition = gl.getAttribLocation(program, "aPosition");
  gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(aPosition);
  render();
};
function render(){
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
}
#sec-6-6
Read code by responsibility

不要按行号背代码:按职责块阅读

职责块 代表调用 你应该问的问题
环境 getContext / viewport 画在哪里?
GPU程序 initShaders / useProgram GPU执行什么?
数据资源 create/bind/bufferData 数据在哪里?
接口描述 vertexAttribPointer 这些字节怎么解释?
绘制 drawArrays 用什么图元、多少顶点?
#sec-7-1
Viewport & clear · 23–26 min

绘制前还需要确定“画在哪”和“背景是什么”

Viewport

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

Canvas中用于显示图像的像素矩形。

Clear Color

gl.clearColor(1.0, 0.0, 0.0, 1.0);

清空颜色缓存时使用的背景颜色。

#sec-7-2
Draw call

最后一行才真正触发绘制

function render() {
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
}

drawArrays 三个问题

① 怎样解释顶点?
② 从哪个顶点开始?
③ 使用多少顶点?

#sec-7-3
Event-driven model · Extension

Web图形程序为什么不是“main执行完就退出”?

Initialization

window.onload = init:页面加载后完成资源初始化。

Interaction / Animation

鼠标、键盘、定时器、requestAnimationFrame 等事件触发状态变化和重绘。

交互式图形程序本质上是事件驱动的长期运行程序。

#sec-7-4
Aspect ratio · Extension

Canvas、Viewport 与宽高比

Clip / View region

图形空间中的可见范围。

Viewport

Canvas中的像素矩形。

两者宽高比不一致时,图像会产生拉伸或压缩。

#sec-8-1
Prediction → Run · 26–34 min

现场实验:每次只改一行

A · viewport

width/2
height/2

B · clearColor

红 → 蓝

C · MODE

POINTS / LINE_STRIP / TRIANGLE_FAN

D · Shader

修改 fColor

预测—运行—解释8 min

每次修改前先投票预测结果;运行后必须解释“是哪一层发生了变化”。

#sec-8-2
Experiment A · Viewport

实验A:为什么图只出现在 Canvas 的左下角?

gl.viewport(0, 0, canvas.width / 2, canvas.height / 2);
层级归属

视口变换层:规范化坐标到屏幕物理像素的映射范围

顶点数据没有变,为什么图像大小和位置变了?

💡 原理解析

因为裁剪坐标最终要映射到指定的 Viewport 像素矩形。视口宽高减半,渲染出的正方形也随之缩小到左下角四分之一区域。

#sec-8-3
Experiment B · Clear Color

实验B:背景从红变蓝,为什么正方形颜色没变?

// 原代码: gl.clearColor(1.0, 0.0, 0.0, 1.0); (红底)
gl.clearColor(0.0, 0.0, 1.0, 1.0); // 蓝色背景
gl.clear(gl.COLOR_BUFFER_BIT);
层级归属

帧缓冲清空层:仅刷新颜色缓冲区底色,与后续图元绘制解耦

“背景”是谁画出来的?修改它会影响几何图形吗?

💡 原理解析

不会。clearColor 仅设置清空颜色缓存(Color Buffer)时填充的底色;正方形由随后的着色器和绘制调用覆盖写入,处于管线不同阶段。

#sec-8-4
Experiment C · Primitive Mode

实验C:同一组4个顶点,换MODE会发生什么?

// 替换原 gl.TRIANGLE_FAN
gl.drawArrays(gl.LINE_LOOP, 0, 4);

显存中的顶点数据改动了吗?换 MODE 真正改变的是什么?

💡 原理解析

显存顶点数据完全没有变。改变的是图元装配层对顶点序列的拓扑连接与填充解释方式。

POINTS

4 个独立点
仅在顶点位置绘制孤立像素点

LINE_LOOP

正方形闭合线框
首尾相连封闭折线轮廓

TRIANGLE_FAN

实心正方形
扇形三角面片拼合填充

#sec-8-5
Experiment D · Fragment Shader

实验D:怎样改变图形本身的颜色?

#version 300 es
precision mediump float;
out vec4 fColor;
void main(){
  // 原黄色 vec4(1.0, 1.0, 0.0, 1.0) -> 改为纯绿
  fColor = vec4(0.0, 1.0, 0.0, 1.0);
}
层级归属

片元着色层:GPU 可编程管线中决定最终像素输出的阶段

只在 JS 脚本中改一个变量,能改变正方形颜色吗?

💡 原理解析

不能(若颜色未关联 attribute/uniform)。在现代可编程管线中,图元内部片元的颜色完全由片元着色器计算输出。

#sec-9-1
Bug Hunt · 34–37 min

为什么“什么都没画出来”?

可能原因

  • Shader 编译失败
  • Buffer 没有上传/绑定
  • Attribute 没有 enable
  • draw count / mode 错误

调试证据

  • Console error
  • Shader compile log
  • 检查数据长度
  • 逐步缩小问题范围
现场故障2 min

教师故意删掉一行或改错变量名,学生先判断最可能的层级。

#sec-9-2
AI collaborative debugging

让AI提出假设,不让AI直接接管程序

“下面是错误信息和相关代码。请不要重写整个程序,只列出最可能的3个错误位置,并给出每一项的验证方法。”

AI协作调试2 min

AI提出假设 → 浏览器Console/代码验证 → 只修改有证据支持的问题。

#sec-10-1
Generalization · 37–39 min

Square → Sierpinski:什么不变?什么改变?

不变

  • Canvas / Context
  • Shader编译链接
  • Buffer机制
  • Attribute绑定
  • Draw Call
↔

改变

  • 顶点生成算法
  • 顶点数量
  • Primitive mode
  • 可能增加颜色/深度数据

你学的不是一个“正方形程序”,而是图形程序的一般结构。

#sec-10-2
Extension · Recursion

用递归三角形生成 Sierpinski

function divideTriangle(a, b, c, count) {
  if (count == 0) triangle(a, b, c);
  else {
    // midpoints
    divideTriangle(..., count - 1);
    divideTriangle(..., count - 1);
    divideTriangle(..., count - 1);
  }
}

变化

数据生成算法从“随机点”换成“递归细分”。

GPU编程结构基本不变。

#sec-10-3
Extension · 3D

二维 → 三维:新增的真正问题是什么?

vec2 → vec3

数据增加 z 坐标。

Color / Face

三维结构需要更多视觉线索。

Depth Test

解决前后遮挡:gl.enable(gl.DEPTH_TEST)

#sec-11-1
Chapter 2 map

三节课合起来:现代图形程序的完整心智模型

CPUGenerate Data顶点/属性
→
RESOURCEBuffer上传数据
→
PROGRAMShaderGPU程序
→
COMMANDDraw Call图元模式/数量
Canvas
←
Framebuffer / Pixels
#sec-11-2
Exit ticket · 39–40 min

用箭头连接七个词

Canvas Data Buffer Attribute
Vertex Shader Fragment Shader Draw Call
提交1 min

画出你自己的“最小WebGL程序运行图”,不要求和教师图完全一样,但必须解释每条箭头。