在 WebGL 里,gl.viewport() 是一个很基础、却经常被忽略的函数。它决定了绘制结果最终落在画布的哪个区域,以及这个区域有多大。只要你在做窗口自适应、多视口渲染,或者单纯想把画面准确地映射到屏幕上,都会绕不开它。
gl.viewport() 的基本原理
WebGL 的绘制并不是直接在屏幕坐标里进行的。顶点经过一系列变换后,会落到一个标准化设备坐标系(NDC)中,它的范围是 [-1, 1]。接下来,GPU 会把这个坐标映射到真正的画布像素区域,这一步就是视口变换,而 gl.viewport() 正是在做这件事。
换句话说,投影矩阵负责把世界坐标压缩到 NDC,gl.viewport() 再把 NDC 映射到画布上的具体像素位置。两者配合起来,最终图像才会准确显示在我们看到的区域里。
gl.viewport() 的参数含义
gl.viewport() 的调用形式很简单:
gl.viewport(x, y, width, height);
x、y:视口左下角的起点位置,单位是像素。width、height:视口的宽高,单位同样是像素。
这里有个细节要记住:WebGL 里的 (x, y) 指的是左下角,这一点和很多 2D 画布或 DOM 布局习惯并不一样,调试时很容易在这里绕进去。
常见使用方式
默认情况:让视口覆盖整个画布
大多数场景下,你只需要让视口和画布大小一致。比如画布是 800x600,通常就会这样设置:
const canvas = document.getElementById('myCanvas');
const gl = canvas.getContext('webgl');
gl.viewport(0, 0, canvas.width, canvas.height);
这表示渲染内容会填满整个画布,没有额外偏移。
只渲染画布中的一部分区域
如果你希望把图像限制在画布的某个子区域里,直接调整视口的宽高就行。比如:
gl.viewport(50, 50, 400, 300);
这段代码会把渲染区域限制在从 (50, 50) 开始、宽 400、高 300 的矩形内。常见用途包括分屏界面、局部预览窗口、调试视图等。
多视口渲染
在游戏、可视化大屏或编辑器里,多视口并不少见。思路也很直接:每次绘制前先切换视口,再绘制对应内容。
// 左上角区域
gl.viewport(0, 0, canvas.width / 2, canvas.height / 2);
drawSceneLeftTop();
// 右上角区域
gl.viewport(canvas.width / 2, 0, canvas.width / 2, canvas.height / 2);
drawSceneRightTop();
// 左下角区域
gl.viewport(0, canvas.height / 2, canvas.width / 2, canvas.height / 2);
drawSceneLeftBottom();
// 右下角区域
gl.viewport(canvas.width / 2, canvas.height / 2, canvas.width / 2, canvas.height / 2);
drawSceneRightBottom();
这种写法的关键不在于'调用了几次',而在于每次切换视口后,后续绘制都会被限制在当前区域里。只要场景组织得当,就能很自然地实现多窗口、多角度或分区展示。
窗口变化时动态调整视口
WebGL 项目里最常见的坑之一,就是画布大小变了,视口却没跟着更新。结果通常就是画面拉伸、裁切,或者显示区域和实际窗口不一致。
如果你的页面需要响应窗口尺寸变化,建议在 resize 事件里同时更新画布和视口:
window.addEventListener('resize', function () {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
gl.viewport(0, 0, canvas.width, canvas.height);
});
这样处理之后,画布尺寸一变,渲染区域也会同步调整,视觉上会稳定很多。
gl.viewport() 和其他变换的关系
很多人第一次接触 WebGL 时,会把'投影矩阵'和'视口'混在一起理解。实际上它们分工很明确:
- 投影矩阵负责把三维空间中的坐标变换到 NDC。
gl.viewport()负责把 NDC 映射到画布上的像素区域。
如果投影矩阵设置得不对,画面可能本身就变形;如果视口设置得不对,图像即便内容正确,也可能显示在错误的位置,或者只占据画布的一小块。
另外,gl.clear() 清除的是当前上下文的缓冲区内容,而视口会影响你看到的绘制区域。实际开发里,通常是在设置好视口后再清屏、再绘制,这样逻辑更清楚,也更不容易出错。
一个完整的动态视口示例
下面这个例子把画布尺寸和视口绑定在一起。代码不复杂,但它很接近实际项目里的常见写法。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGL Viewport Example</title>
<style>
body {
margin: 0;
}
canvas {
display: block;
}
</style>
</head>
<body>
<canvas id="myCanvas"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
const gl = canvas.getContext('webgl');
// 画布大小变化时,同步更新视口
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
gl.viewport(0, 0, canvas.width, canvas.height);
}
// 初始设置
resizeCanvas();
// 监听窗口变化
window.addEventListener('resize', resizeCanvas);
function drawScene() {
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 这里放具体的渲染逻辑
}
function render() {
drawScene();
requestAnimationFrame(render);
}
render();
</script>
</body>
</html>
这个例子的重点不在于'画了什么',而在于它展示了一条很实用的链路:画布尺寸变化 → 视口同步更新 → 渲染内容始终和窗口一致。做 WebGL 项目时,这条链路越稳定,后面遇到的布局问题就越少。
小结
gl.viewport() 的作用可以概括为一句话:它把 WebGL 的输出范围精确地落到画布的某一块区域上。对于默认全屏绘制、多视口布局、窗口自适应这几类场景,它都很关键。
如果你在项目里看到'画面位置不对''渲染结果被裁掉''窗口缩放后显示异常'这类问题,除了检查投影矩阵,也要第一时间看看视口是不是忘了更新。很多 WebGL 的显示问题,根源其实都藏在这里。
