问题与范围
四角透视变换、文档矫正或 corner pin 都会先把源图四个角投到新的位置,再为结果分配一个足以容纳它的输出纹理。常见做法是求浮点包围盒、把宽和高取整,然后继续渲染。
这个做法在对称、偶数尺寸的例子里经常看似正确。但当源图是奇数尺寸,或者某一角只向外移动了一小段小数距离时,它可能只把"尺寸"变成整数,却没有保证原始的最小边界和最大边界仍落在输出内。后果通常不是明显的崩溃,而是某一边被裁掉一行像素、单像素细节消失,或不同输入尺寸下出现难解释的半像素偏移。
本文讨论单帧 GPU 图像几何变换中的输出视口合同;不讨论相机、视频编解码、设备级性能或完整照片编辑产品。
先给结论
- 最小包围视口的目标是覆盖四个变换后的边界,不是让宽高看起来是整数。
- 在左上为原点的像素坐标中,最小边界应向下取整、最大边界应向上取整;这等价于对完整矩形向外积分。
- 若内部使用中心坐标系,应先平移到左上像素坐标取整,再平移回来,不能只改宽高。
- 输出纹理的
resize与 shader 的视口 uniform 必须来自同一份解析结果;否则尺寸和采样范围会各自正确、组合后却不一致。
为什么只取整宽高不够
设四角变换得到的水平范围是:
text
minX = -9.2, maxX = 11.0
范围的长度是 20.2。把宽度取为 21 并不能单独说明新矩形是否仍覆盖 -9.2 ... 11.0:还要知道它的原点如何确定。若代码在中心坐标中根据新宽度重建视口,浮点误差、半像素原点或非对称外扩都可能把某一侧向内推。
因此应先保护边界。把范围转换到源图左上角为原点的像素坐标后,分别执行:
text
pixelMinX = floor(pixelMinX)
pixelMinY = floor(pixelMinY)
pixelMaxX = ceil(pixelMaxX)
pixelMaxY = ceil(pixelMaxY)
这样得到的整数矩形一定覆盖原始浮点矩形。宽和高只是这四个已确定边界的差值,而不是先被取整、再反过来猜边界的输入。
中心坐标系中的处理顺序
很多渲染路径把图像中心当作原点:一个宽为 W 的图像在水平方向上位于 [-W/2, W/2]。这种表示适合变换矩阵,但不适合直接做像素边界取整。
可靠的处理顺序是:
text
变换后的四个角
→ 浮点包围盒
→ 平移到左上像素坐标
→ min 用 floor、max 用 ceil
→ 得到整数像素视口
→ 平移回中心坐标
这里的关键不是使用哪一个 API,而是取整发生在哪个坐标系。对一个 5 × 3 的 identity 变换,正确结果仍应是 5 × 3;不能因为中心原点带来半像素表示,就悄悄扩成 6 × 4 或移动输出范围。
一个视口,只解析一次
输出尺寸与实际采样范围必须说同一种语言。假如 resize 根据向外取整后的范围分配纹理,而 fragment shader 仍使用未取整的浮点视口作为 uniform,那么目标纹理的像素网格与变换后的可见区域就失去了一致性,边缘问题仍会出现。
工程上更稳妥的方式是先解析一份 resolvedViewport:resize 用它决定输出大小,fragment uniform 也用它提供 viewport origin 和 size。之后的采样、透明边缘处理和输出尺寸都围绕同一份范围工作。旋转、裁剪、镜像、非均匀缩放等需要整数输出纹理的路径,也能沿用同样的判断方法。
如何测试这类问题
只看一张正常尺寸截图很难发现视口边界错误。至少应覆盖:
- 奇数尺寸的 identity 变换,防止中心坐标转换引入额外像素;
- 某一角向外移动小数距离,确认最小包围范围向外扩展;
- 单像素输入,确认几何路径不会让唯一的可见 texel 消失;
- 输出尺寸与 shader 所用视口来自同一份解析结果。
这些测试把"没有裁边"从肉眼判断变成可复现的合同。它们同样适用于需要根据几何变换重新分配纹理的其他 GPU 管线。
本机验证与参考实现
Harbeth 当前的 RenderQuadTransform 提供 alignsViewportToPixels 配置,默认关闭。启用后,仅在 minimumEnclosing 模式下,它会先把浮点包围盒平移到源图左上像素坐标,再对整个矩形向外取整,最后转换回中心坐标。resize 与 fragment uniform 都消费同一个解析后的视口。
在 Harbeth 358e1de6、macOS arm64e 上,本轮执行:
text
swift test --filter Transform3DTests
共执行 17 项、0 失败,耗时 0.208 秒。定向覆盖包括 5 × 3 奇数尺寸 identity、小数外扩的 20 × 10 → 21 × 11、单像素 GPU 可见性,以及 3D、四角变换和矫正的相关路径。这是当前本机源码与定向测试证据,不代表全设备性能、完整照片校正流程或公开发版已经完成。
Kakapos 在媒体链路中可以把 Harbeth 用作逐帧处理后端;不过时间线、预览、导出与媒体生命周期仍由 Kakapos 自己负责。视口边界的像素合同属于逐帧图像处理层,不应被媒体编排层吞掉。
小结
最小包围视口的关键不是"宽高取整",而是"整数化之后仍完整覆盖原始边界"。当坐标系以中心为原点时,先回到左上像素坐标,分别向外取整最小和最大边界,再回到中心坐标,能让奇数尺寸、透视外扩和边缘 texel 都有明确的可测试行为。
如果你的图像管线同时包含透视变换、GPU 采样和动态输出尺寸,这个细节值得单独立约。许多看似随机的边缘裁切和单像素消失,最终都来自同一类边界取整错误。
源码与参考实现
本文基于本轮本机源码与定向测试,不代表已经公开发版或完成全设备验证。