关于图片采样
关于图片采样
前言
在游戏开发中,我们经常会对图片进行采样。比如物体的纹理信息,阴影贴图的信息等等。我们都需要对图片进行采样,以丰富游戏的表现。
我们时常会遇到这样的一个问题,我们需要将一个图片渲染到屏幕上,而图片所覆盖的屏幕像素远大于图片自身的像素。这时候我们就需要对图片进行放大处理。又或是反过来,我们需要对图片进行缩小处理。这些都会涉及到如何对图像本身进行采样。
图像采样的方法和规则
我们假设现在我们有一张256x256的图片。我们需要分别将其渲染成64x64和1024x1024的图片。对于64x64的处理,你或许会觉得很简单,因为256刚好是64的4倍,我们直接按照他们的比例进行像素的选取就好了。但是对于1024x1024的处理,你是否有想法呢?如果我需要的是100x100的图片,那么你又该如何处理呢?
无论是放大(上采样)还是缩小(下采样),我们都可以去做一个映射操作。比如我想知道64x64中坐标为(31,31)(这里我依照程序里面下标从0开始计算)的像素点是什么颜色。我先计算出此坐标在原图中的比例,显然它这里xy比例是(0.5,0.5)。然后我们在用这个计算出256x256图中对应的像素点坐标为(127,127)。那么(31,31)对应的颜色就可以是(127,127)位置的颜色。同理1024x1024的图片也可以这么操作。
这时候,你或许会问,我这边定的坐标过于刚好。如果是在1024x1024的图片中找(1,1),对应的颜色呢?同样用上面的方法,我们可以计算出(1,1),在256x256图中对应的坐标为(0.5,0.5)。但是我们知道像素点的坐标都是正整数。这(0.5,0.5)根本没有对应的像素点。所以,我们需要对这样的坐标进行一些处理。最简单的方法是最近邻插值(Nearest Neighbor Interpolation)。所谓的最近邻插值就是我们判断计算得到的小数部分,如果小于0.5,我们就向下取值,大于则向上取值。而0.5这样的分界位置视具体的需求而定,我后面实现的方法是直接算向下取值。当然你也可以按照你需求更改分界值。核心代码如下所示:
1 | private Color NearestNeighborInterpolation(Texture2D texture2D,Vector2 pos) |
项目中更多的情况是使用双线性插值法(Bilinear Interpolation)。双线性插值法则是考虑到周围的四个像素点,计算出一个值。我们还是以在256*256图中对应的坐标为(0.5,0.5)为例子,我们对其x和y值分别向上和向下取值。则我们可以得到这四个坐标点:(0,0),(0,1),(1,0),(1,1)。并且分别得到这四个点对应的颜色值。接下来我们先对y值相同坐标点对应的颜值进行插值计算,插入的值为我们想要(0.5,0.5)坐标的x值的小数部分。则四个颜色就变成了两个颜色,最后我们再将这两个以我们想要(0.5,0.5)坐标的y值的小数部分进行插值计算得到我们最终的颜色值。核心代码如下所示:
1 | private Color BilinearInterpolation(Texture2D texture2D, Vector2 pos) |
上面所述的是双线性插值法的一种实现方法,实际上还有其他的做法,但是原理都是一致的。在图片进行缩小的时候,这两种方法的区别并不会很大,但是在图片进行放大的时候,最近邻插值会出现锯齿状的边缘,而双线性插值法的图片看起来会糊一些(有些人觉得会更细腻一些,但是我就觉得是糊了)。所以,在具体的项目中,我们需要根据需求选择合适的采样方法。实际上还存在一个三线性插值法,但是这个方法要结合mipmap来实现,这里就不再展开了。大多数情况下,项目都是使用双线性插值,这个效果已经足够了。Unity本身也直接提供了相关的API,我们可以直接调用。上面的代码并不是unity内部的实现,而是我自己写的,所以在表现上和数值传递上所有区别。具体请看官方的文档Texture2D.GetPixelBilinear。
在我们进行图片缩小的时候,最近邻插值过于暴力而双线性插值法覆盖的区域有所限制,当我们缩小的程度到达一定数值时,这两个方法都会照成非常严重的锯齿。我们仍然以256x256图为例子,现在我们要将其缩小到32x32。此时32x32其中的一个像素代表着256x256中一个8x8范围的像素。当我们使用最近邻插值,则我们只会在这8x8范围内选择一个像素来表示,而因为此时计算出来的小数点是不存在小数的(256被32整除),所以双线性插值法就退化为最近邻插值了。一个8x8范围的像素集合,能被一个像素代表吗?显然不能。所以,当我们做缩小的时候,这样两个方法就存在了一定的问题。一般而言,我们要选出一个范围内区域的代表,我们就是将这个范围内的像素进行累加后平均。我们以这个平均值来作为这个范围内的像素的颜色。当然这是最简单的做法,实际上还有其他的方法,这就看你们的需求了。当然这个区域也不一定是正方形,所以我们还需要考虑到区域的大小。核心代码如下:
1 | private Color AvgAreaColor(Texture2D texture2D,Vector2 pos,Vector2Int area) |
在实际项目中,大多数情况下我们都很难得到这么正好的比例。我们时常会得到一些小数。其实我们可以忽略这个小数值。比如我们要得到一个30x30的图片。\(256/30 \approx 8.5333\),我们直接取8,得到\(256/8=32\)。那我们先得到这32x32的图片,然后再使用线性插值或是最近邻插值来得到最终30x30的图片。
闲言碎语
我突然想写一篇关于图片采样的文章,然后我写着写着发现我也没啥可写的。网上也有很多类似的文章,我这篇就像是水文。sorry。