如何缩放 UI 面板而不把边框拉变形
你画了一张 24×24 的对话框,边框是干净利落的两像素。到了游戏里它被拉到 200 像素宽, 边框一边粗一边细,谁也说不清到底哪里出了错。这篇讲的就是哪里出了错。
1. 拉伸到底对边框做了什么
缩放一张图,就是为输出的每一个像素决定它该显示输入的哪个像素。 把 24 像素宽的面板拉到 200 像素,映射关系是每个输出像素对应 24 ÷ 200 = 0.12 个输入像素—— 也就是每个输入像素要覆盖 8 个或 9 个输出像素, 到底是 8 还是 9,取决于它落在四舍五入的哪一侧。
对面板中间那块来说,这没什么。对边框来说,这就是全部问题。 两像素的边框在一边变成 16 像素、在另一边变成 18 像素, 因为那两个原始像素的舍入方向不同。没有变糊—— 用邻近采样的话每个像素依然边缘锐利—— 但整个框肉眼可见地歪,而且一眼看上去像是画得潦草, 而不像是缩放造成的。
开平滑滤镜救不了:那只是把「不均匀的边框」换成「发虚的边框」。 真正的解法是别去缩放边框。
2. 九宫格:把四个角钉住
用两条竖线和两条横线把面板切开,得到九块区域。 面板改变尺寸时,这九块的待遇各不相同:
四个角按原始大小原样复制,永远不缩放。上下两条边只在水平方向变长。左右两条边只在垂直方向变长。中间那块两个方向都变。
于是边框的粗细保住了——因为决定粗细的那部分都在角和边里, 而角和边只会沿着边框自身延伸的方向变长。
整套配置就是四个数字:图片的每条边到它那条切割线的距离。 每个引擎给它们起的名字都不一样,而每个引擎要的都是同样这四个值。
3. 切割线该放在哪
放在「不能变形的那部分」刚刚往外一点。角上的装饰花纹占了三个像素,线就放在第四个像素处。 普通的两像素边框加圆角,圆弧过去一个像素就够了。
但也别放得更远。线以内的部分就是会被重复的部分,中间区域太小, 填满一张大面板就得重复很多次——纯色填充无所谓,有花纹就很明显。
留意中间那块画的是什么。如果中间是有周期的纹理——每四像素一个点、斜向的排线—— 那中间区域的宽和高最好都是周期的整数倍。 否则花纹会在每个平铺边界处跳一下, 接缝在面板上连成一张网格,比那点纹理本身更扎眼。
有一个特例值得记住:两端是圆头的血条, 其实就是一个上下边距为 0 的九宫格—— 只有左右两端固定,中间朝一个方向伸长。用的是同样这四个数字。
4. 三个引擎都默认错了的那个设置
九宫格解决的是「哪些区域会被缩放」。 它本身并没有规定边和中间怎么去填满新增的空间, 而默认答案是拉伸——Godot 是,Unity 是,CSS 也是。
拉伸中间那块,问题和拉伸整张面板一模一样,只是范围小了一点。 中间是纯色就没人看得出来;只要有一点纹理, 那纹理就会以非整数比例被重采样,出来宽窄不匀。
平铺则是按原始尺寸重复中间那块。 每个像素永远是被画出来时的大小。要显式地设:
Godot —— NinePatchRect, Axis Stretch Mode 的水平和垂直都设成 Tile。默认是 Stretch。
Unity —— Image 组件的 Image Type 选 Tiled 而不是 Sliced。 Sliced 是「九宫格 + 拉伸」,Tiled 是「九宫格 + 平铺」。
CSS —— border-image-repeat: repeat(或者 round,它会稍微缩放一点让平铺次数刚好是整数)。 初始值是 stretch。
另外三者共通的一条:贴图过滤必须设成邻近采样, 否则上面这些做对了,引擎照样会在像素之间做插值。
5. 三种交付方式
那四个数字。常规做法。引擎在绘制时现切, 一张小贴图伺候所有尺寸,以后要改面板也只需要改一张图。 Godot 的边距叫 patch_margin_left 之类,有名字; Unity 的 Sprite Editor 收一个 Border,顺序是左、下、右、上——别的地方都不用这个顺序, 中间两个一调换,做出来的面板看着几乎是对的, 直到上下边距不一样才露馅。
一个 .9.png。Android 和 LibGDX 直接从图里读切割位置: 外面一圈 1 像素的边,黑色标出可以拉伸的列和行。 好处是数字跟着文件走;麻烦是这圈边的其余部分必须完全透明—— 多出一个像素,文件就会被判为格式错误。
一张预先渲染好的 PNG。有时候这块面板从头到尾只有一个尺寸,那最省事的就是按那个尺寸渲染一次、 直接发一张平面图,运行时完全不用切。 走这条路的话,请用整数倍缩放——像素画缩放工具就是干这个的——别让图像软件替你重采样。
常见故障
边框一边比另一边粗
中间那块在被拉伸而不是平铺。把 Godot 的 Axis Stretch Mode 设成 Tile、 Unity 的 Image Type 设成 Tiled,或者 CSS 的 border-image-repeat 设成 repeat。
中间的花纹有明显接缝
中间区域不是花纹周期的整数倍。挪一下切割线让它是整数倍, 或者在 CSS 里用 round 代替 repeat。
角上的装饰被切掉了一半
有一条切割线放进了装饰里面,而不是外面。往外挪, 直到整个花纹都落在角的区域内。
平铺开了但整张图还是糊的
那是贴图过滤的问题,不是九宫格的问题。Godot 设 Nearest, Unity 设 Point,CSS 加 image-rendering: pixelated。
Unity 里上下边距是反的
Unity 的 border 顺序是「左、下、右、上」, 而你是按「左、上、右、下」填的。
.9.png 被判为格式错误
那圈 1 像素的标记边上有既不是纯黑、也不是全透明的像素—— 通常是手工画的时候带上了抗锯齿。这圈边应该生成,不要手涂。