裁切测试
前面的专题里,我们已经一路讲过:
- 裁剪,
Clipping - 透视除法
- NDC
- 视口变换
- 光栅化
到了这里,很多人会在 WebGL API 里又看到一个名字很像的东西:
gl.enable(gl.SCISSOR_TEST);
gl.scissor(x, y, width, height);
这时候就很容易懵:
- 这个“裁切测试”到底是什么
- 它和前面讲的“裁剪”是不是一回事
- 它和模板测试有什么区别
- 它到底裁掉了什么
- 为什么很多 UI、分屏、局部清屏都要用它
先给最短答案
如果你只想先记一句最核心的话,可以先记这个:
裁切测试 = 用一个屏幕空间里的矩形区域,限制哪些片元允许继续写入,哪些片元必须被丢弃
再压缩一下,可以记成:
裁切测试不是裁几何,而是裁“屏幕上的可写区域”。
这句话非常关键。
因为它直接点出了裁切测试和“裁剪”的本质区别:
裁剪关注的是几何图元有没有超出可见体裁切测试关注的是片元当前是否落在一个矩形写入区域里
它在整条管线里的位置
先把它放回整个渲染流程里:
顶点着色器
-> 图元装配
-> 裁剪
-> 透视除法
-> NDC
-> 视口变换
-> 光栅化
-> 片元着色器
-> 裁切测试
-> 模板测试
-> 深度测试
-> 混合
-> 帧缓冲
如果你只看学习阶段的直觉,可以把它理解成:
- 顶点阶段已经结束
- 三角形也已经被光栅化成片元了
- 现在 GPU 先问一句:
这个片元落在我允许写入的矩形区域里吗
如果不在:
- 这个片元就会被丢弃
- 后面的模板、深度、混合都没必要继续了
所以裁切测试本质上是:
片元级别的矩形区域过滤
裁切测试到底在裁什么
这是整篇里最重要的问题。
答案是:
- 它裁的不是顶点
- 也不是三角形的一部分几何结构
- 它裁的是
落在矩形区域外的片元写入机会
换句话说:
- 一个三角形本来可能覆盖整块屏幕
- 经过光栅化后产生很多片元
- 裁切测试只允许矩形内部那些片元继续活下去
- 矩形外的片元直接丢弃
所以更准确地说:
裁切测试裁的是屏幕空间里的片元,不是几何空间里的图元。
先和“裁剪 Clipping”彻底分清
这两个名字很像,但本质完全不是一个东西。
裁剪,Clipping
- 发生在裁剪空间阶段
- 针对的是点、线、三角形这些图元
- 目的是把超出可见体的几何部分切掉
- 它决定“哪些几何还值得进入后续流程”
裁切测试,Scissor Test
- 发生在片元相关阶段
- 针对的是屏幕上的片元
- 目的是限制最终可写入的矩形区域
- 它决定“这些片元有没有资格写进当前目标区域”
你可以这样记:
裁剪 = 几何空间问题裁切测试 = 屏幕矩形区域问题
这是最重要的区分。
再和模板测试分清
很多人学完模板测试以后,会觉得:
- 模板测试能限制区域
- 裁切测试也能限制区域
- 那它们是不是差不多
它们确实都能控制“哪里能画”,但控制方式差别很大。
裁切测试
- 只能用一个轴对齐矩形
- 规则非常简单
- 不需要额外写模板值
- 开销低,适合快速限制矩形区域
模板测试
- 可以做任意形状区域
- 需要 stencil buffer
- 一般要先写模板,再按模板筛选
- 适合遮罩、描边、镜子、传送门等复杂区域控制
所以:
- 如果你只想限制一个简单矩形区域,用裁切测试更直接
- 如果你想限制圆形、文字、不规则洞口,用模板测试更合适
裁切测试的 API 非常少
WebGL 里和它直接相关的核心 API 就两个:
gl.enable(gl.SCISSOR_TEST);
gl.scissor(x, y, width, height);
关闭就是:
gl.disable(gl.SCISSOR_TEST);
它们分工很清楚。
gl.enable(gl.SCISSOR_TEST)
- 开启裁切测试
gl.scissor(x, y, width, height)
- 指定裁切矩形
- 只有这个矩形里的片元允许继续写入
这个矩形的含义是:
- 左下角起点:
(x, y) - 宽:
width - 高:
height
也就是说:
gl.scissor(100, 50, 300, 200);
表示:
- 只允许绘制到从左下角
(100, 50)开始 - 宽
300 - 高
200
的那个矩形区域里。
注意坐标原点:它是左下角
这点非常容易弄错。
WebGL 的 gl.scissor() 使用的是:
- 视口/窗口坐标
- 原点在左下角
不是浏览器 DOM 事件那种:
- 原点在左上角
所以如果你在浏览器里拿鼠标坐标、UI 坐标去算 gl.scissor(),经常需要做 Y 轴翻转。
例如 canvas 高度是 canvas.height,DOM 给你的 top 坐标是:
const domY = 20;
那对应的 WebGL scissor Y 往往要写成:
const glY = canvas.height - domY - rectHeight;
这里之所以还要减 rectHeight,是因为:
- DOM 常用的是“矩形左上角”
gl.scissor()需要的是“矩形左下角”
最直接的直觉例子
假设整个画布是 800 x 600,你设置:
gl.enable(gl.SCISSOR_TEST);
gl.scissor(200, 150, 400, 300);
这表示只有中间这块矩形区域允许写入:
屏幕宽 800,高 600
左边空 200
右边空 200
下边空 150
上边空 150
中间 400 x 300 可以绘制
此时即使你画一个覆盖全屏的大矩形:
- GPU 也只会保留中间
400 x 300区域里的结果 - 其他区域像被“切掉”一样
这就是裁切测试最直观的行为。
它最常见的用途有哪些
裁切测试虽然简单,但非常实用。
最典型的用途有这些:
- 局部 UI 面板绘制
- 分屏渲染
- 小地图窗口
- 画中画
- 局部重绘
- 局部清屏
- 多视图编辑器
- 把后处理限制在某个矩形区域
你会发现它们有个共同点:
- 这些需求都只需要一个矩形区域
- 不需要复杂不规则形状
这正是裁切测试最合适的使用场景。
为什么它特别适合“局部清屏”
这是裁切测试最值得单独记住的一点。
很多人第一次学它,都是在这里真正意识到它的价值:
gl.clear()也会受 scissor 影响
也就是说,如果你先设置:
gl.enable(gl.SCISSOR_TEST);
gl.scissor(0, 0, 300, 300);
gl.clearColor(1, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
结果不是整屏变红,而是:
- 只有左下角
300 x 300这块区域被清成红色
这个特性在工程里非常有用,因为它意味着你可以:
- 不重刷整张画布
- 只重置某个局部区域
这对编辑器、多面板渲染、局部更新都很重要。
完整示例 1:只清除一块矩形区域
先看一个最短、最能说明问题的例子:
// 先把整屏清成深灰色
gl.disable(gl.SCISSOR_TEST);
gl.clearColor(0.1, 0.1, 0.1, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 开启裁切测试,只让左下角 200x200 区域可写
gl.enable(gl.SCISSOR_TEST);
gl.scissor(0, 0, 200, 200);
// 这次 clear 只会影响左下角这一小块(红色)
gl.clearColor(1.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 关闭,避免影响后续绘制
gl.disable(gl.SCISSOR_TEST);
运行结果可以理解成:
- 全屏先被清成深灰色
- 然后左下角一个
200 x 200矩形被重新清成红色
这个例子虽然简单,但已经把裁切测试的核心能力完整体现出来了。
完整示例 2:分两块区域分别绘制不同内容
再看一个更像实际工程的例子。
我们把画布分成左右两半:
- 左边清成绿色
- 右边清成蓝色
代码如下:
const width = canvas.width;
const height = canvas.height;
// 开启裁切测试
gl.enable(gl.SCISSOR_TEST);
// 左半屏清成绿色
gl.scissor(0, 0, Math.floor(width / 2), height);
gl.clearColor(0.0, 1.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 右半屏清成蓝色
gl.scissor(Math.floor(width / 2), 0, Math.floor(width / 2), height);
gl.clearColor(0.0, 0.0, 1.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 关闭裁切测试
gl.disable(gl.SCISSOR_TEST);
这段代码里并没有复杂 shader,但你已经能做出:
- 分栏背景
- 分屏预览
- 左右两个工作区
如果再配合不同的相机和真正的 draw call,就能做:
- 左边俯视图
- 右边透视图
这就是很多编辑器、多视图工具最基础的矩形区域管理方式。
完整示例 3:只在某个面板区域里画内容
下面给一个更接近“真正绘制”的示例。
假设你有一个全屏矩形 shader,本来会把内容铺满全画布:
即使这个 quad 覆盖的是整屏:
- 真正能写入的也只有
(100, 80, 300, 220)这个矩形
所以裁切测试的一个很常见使用方式就是:
让绘制逻辑保持简单再用 scissor 控制输出区域
// 先清成背景颜色
gl.clearColor(0.06, 0.07, 0.09, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 开启裁切测试
gl.enable(gl.SCISSOR_TEST);
// 设置裁切矩形
gl.scissor(100, 80, 300, 220);
// 绘制 quad
gl.useProgram(program);
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
gl.enableVertexAttribArray(loc.aPosition);
gl.vertexAttribPointer(loc.aPosition, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
// 关闭裁切测试
gl.disable(gl.SCISSOR_TEST);
scissor 和 viewport 到底有什么区别
这是一个非常经典的面试/学习问题。
因为很多人第一次看到这两个 API 时,都会觉得它们都像“控制区域”的:
gl.viewport(...)
gl.scissor(...)
但它们控制的根本不是一件事。
gl.viewport()
- 控制 NDC 到屏幕像素的映射区域
- 本质是“坐标映射规则”
- 它影响顶点最终落到屏幕哪里
gl.scissor()
- 控制哪些屏幕像素区域允许写入
- 本质是“矩形写入许可”
- 它不改变几何位置,只限制最后能不能落盘
你可以这样理解:
viewport像是在说“把世界投到哪块屏幕上”scissor像是在说“只有这块矩形区域准许真正写进去”
两者经常一起配合使用,但职责不同。
举个 viewport 和 scissor 配合的例子
比如你要做一个右上角的小地图窗口。
你可能会这样做:
// 小地图区域
const miniX = 550;
const miniY = 350;
const miniW = 200;
const miniH = 200;
gl.viewport(miniX, miniY, miniW, miniH);
gl.enable(gl.SCISSOR_TEST);
gl.scissor(miniX, miniY, miniW, miniH);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
renderMiniMapScene();
gl.disable(gl.SCISSOR_TEST);
这里:
viewport负责把小地图相机的结果映射到右上角这块区域scissor负责保证清屏和写入都只发生在这块区域里
如果你只设 viewport 不设 scissor,常见问题是:
clear()仍可能清整屏
所以在多面板渲染里,这两个 API 往往是一对出现的。
为什么说它是“矩形版的区域控制”
你可以把区域控制手段大致分成三类:
1. 裁切测试
- 形状:轴对齐矩形
- 优点:简单、直接、便宜
- 适合:面板、小地图、分屏、局部 clear
2. 模板测试
- 形状:任意
- 优点:灵活
- 适合:遮罩、描边、镜子、传送门
3. 在 shader 里手动 discard
- 形状:任意,甚至可程序化
- 优点:最灵活
- 缺点:一般更重,也更复杂
所以如果你的需求只是一个普通矩形,通常优先想到的就是:
scissor test
裁切测试为什么不适合做圆形遮罩
因为它天生只能表达一个轴对齐矩形:
gl.scissor(x, y, width, height);
没有:
- 半径
- 多边形点集
- 曲线
- 任意轮廓
所以你做:
- 圆形头像裁切
- 星形区域裁切
- 不规则洞口
这些场景时,裁切测试就不够用了,通常应该上:
- 模板测试
- 或 shader discard
这也是为什么:
裁切测试简单- 但它的表达能力有限
裁切测试会不会影响片元着色器
从学习角度你可以先这么理解:
- 它不会改变你的几何数据
- 不会改变顶点着色器输出
- 不会改变模型矩阵、视图矩阵、投影矩阵
- 它只是在“最后能不能写进目标区域”这件事上加一个矩形筛选条件
所以你可以把它看成:
不改渲染内容本身,只改内容最终允许落在哪
这也是它非常适合做工程级区域管理的原因。
如果和 clear 一起用,它到底限制了什么
很多人最初接触 scissor 的时候会疑惑:
- 明明
clear又不是画三角形 - 为什么它也受 scissor 影响
因为从 GPU 的角度看:
clear本质上也是在对某个目标缓冲做写入
而 scissor test 的职责正是:
- 限制当前写入操作能发生在哪个矩形区域里
所以:
- 不只是 draw call
- 连
clear这种缓冲写操作
也会受 scissor 控制。
和 depth、stencil 一起使用时要怎么理解
你可以把这些测试理解成不同维度的筛选器:
裁切测试
- 看这个片元是不是落在允许的矩形区域里
模板测试
- 看这个片元所在位置的 stencil 值是否符合规则
深度测试
- 看这个片元是不是比已有内容更靠前
所以一个片元可能要连续过很多关:
先过矩形区域关
再过模板规则关
再过前后遮挡关
最后才可能写入颜色
从直觉上讲:
scissor像门口大框stencil像区域密码锁depth像前后排队规则
最容易踩坑的几个点
1. 忘了关闭 SCISSOR_TEST
- 结果后面的 draw call 也被限制在旧矩形里
- 这是最常见的 bug
2. 坐标系搞反了
- 浏览器 UI 习惯左上角原点
- WebGL
scissor用左下角原点 - Y 很容易算错
3. 只设置了 viewport,没设置 scissor
- 结果 draw 位置对了
- 但 clear 或某些写操作越界污染别的面板
4. 想用 scissor 做不规则遮罩
- 做不到
- 它只能是轴对齐矩形
5. resize 后忘了重算矩形
- 画布尺寸变了,但 scissor 还是旧值
- 面板区域就会错位
总结
裁切测试,Scissor Test,本质上是 GPU 在片元写入阶段增加的一道“矩形区域门禁”,它不会裁几何,只会限制哪些屏幕矩形区域允许继续写入,因此特别适合局部绘制、局部清屏、分屏视图、小地图和多面板渲染。