核心问题:一个静态 WebGL 程序,怎样持续生成新的画面并响应用户?
第2章的WebGL程序通常只完成一次绘制。本课要把它改造成持续变化、可以被用户控制的图形程序。
如果要让一个正方形每秒旋转60次,是否必须每一帧都重新计算并上传4个顶点?
一个最小WebGL程序通常按照“准备数据 → 绘制一次 → 停止”的方式运行。
如果模型有100万个顶点,哪种方案更符合现代GPU编程思想?为什么?
已有一个复杂模型的顶点数据保存在 GPU Buffer 中,现在只希望它持续绕原点旋转。更合理的做法是:
uniform,然后重新绘制
gl.clear,不再执行 Draw
Call
theta;通过
uniform 把状态传给 Shader,再执行绘制。
在单位圆构造正方形:随着角度
θ 改变,顶点位置发生旋转。
核心启示:角度 θ 是可以跨帧改变的程序状态。
顶点属性 attribute / in 描述每个顶点不同的数据;
uniform
描述一次绘制中所有顶点共享的数据。
四个原始顶点
Buffer当前旋转角度
uTheta顶点着色器输出旋转后位置
gl_Position#version 300 es
in vec4 aPosition;
uniform float uTheta;
void main() {
gl_Position.x = -sin(uTheta) * aPosition.y + cos(uTheta) * aPosition.x;
gl_Position.y = sin(uTheta) * aPosition.x + cos(uTheta) * aPosition.y;
gl_Position.z = 0.0;
gl_Position.w = 1.0;
}
let theta = 0.0;
const thetaLoc = gl.getUniformLocation(program, "uTheta");
function render() {
gl.clear(gl.COLOR_BUFFER_BIT);
theta += 0.03;
gl.uniform1f(thetaLoc, theta);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
这段代码如果只调用一次
render(),会得到动画吗?为什么?
观察动画循环的内部运作机制:左侧绘制旋转图形,右侧实时监控 Frame、θ 角度、Δt 帧间隔、Buffer 状态与 Uniform 更新状态。
function render() {
update();
draw();
render();
}
同步递归:当前函数尚未返回,就继续调用自身。
这不是动画循环,而是失控的函数调用。
它不是立即再次执行
render(),而是请求浏览器在下一次适合重绘时回调。
function render(time) {
gl.clear(gl.COLOR_BUFFER_BIT);
theta += 0.03;
gl.uniform1f(thetaLoc, theta);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
| 方式 | 特点 | 课堂结论 |
|---|---|---|
setInterval(render, 16) |
按时间间隔尝试调用 | 与浏览器绘制节奏不一定对齐 |
setTimeout(...) |
可人为降低帧率 | 适合控制节奏,但不等于显示同步 |
requestAnimationFrame |
由浏览器在下一帧前回调 | 动画绘制首选 |
请按一帧动画从触发到渲染完成的正确执行顺序排列以下卡片:
requestAnimationFrame 到底做了什么?
在动画程序中,requestAnimationFrame(render)
的作用最接近下面哪一项?
render(),形成普通递归
render
绘制和显示是解耦的。浏览器显示当前完整图像,图形系统在后台绘制下一帧。
屏幕读取并显示的完整画面,防止未画完直接暴露导致撕裂。
GPU 进行光栅化的私有绘制区域,完成后触发 Swap 交换。
观察 Front/Back Buffer 交替 Swap 交换与帧同步机制。
正方形已经会转。现在用户点击按钮,希望它反向旋转。这个操作不是直接“改变画面”,而是改变程序状态。
let direction = true;
document.getElementById("DirectionButton")
.addEventListener("click", function () {
direction = !direction;
});
function render() {
theta += direction ? 0.03 : -0.03;
gl.uniform1f(thetaLoc, theta);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
requestAnimationFrame(render);
}
切换方向
direction = !direction调整速度
speed = value控制暂停、加速、减速
event.keydocument.getElementById("speedSlider").addEventListener("input", function(event) {
speed = Number(event.target.value);
});
window.addEventListener("keydown", function(event) {
if (event.key === " ") paused = !paused;
});
它确实常用于平滑动画。
不应理解为创建新线程;也不能保证固定60fps。
记录 time 或
performance.now(),观察帧间隔。
请AI解释
requestAnimationFrame,但要求它必须说明“什么由浏览器决定、什么由程序员决定”。
动画不是不断重新启动程序,而是不断执行:______ → ______ → ______。
用户交互不是直接改变画面,而是事件先改变:______。
下一课将把“事件改变状态”继续推进:从鼠标事件中得到的位置,如何转换成WebGL中的图形坐标?又如何判断它对应哪个对象?