最终效果(UE4)内
原理简述
低分辨率的贴图在放大的时候会出现锯齿的原因是,贴图采样的滤波模式大部分都会被设置为双线性插值,这种差值可以一定程度上模糊贴图达到简单的抗锯齿效果,但是对于蒙版贴图来说会造成锯齿状的边缘,这是因为差值得出的信息并不是十分正确的。
双线性差值是根据当前点距离周围像素的距离进行差值,所以如果贴图储存的是距离数据的话,差值的结果将是正确的,因此就引出了距离场蒙版贴图的概念
首先我们要有一张高分辨率的蒙版图片,我们由此生成一张低分辨率的距离场蒙版贴图。对于其中每一个像素作如下操作:
- 确定当前像素的值是1(白)还是0(黑)
- 在当前像素周围的一定范围内,寻找离自己最近的与自己颜色不同的像素,获得到此像素的距离值。
- 如果当前像素值为1,则距离为正,如果值为0,则距离为负。
- 将距离值映射到0-1,储存在图片中
- 使用双线性方法向下采样,输出低分辨率距离场贴图(或者在引擎中降低分辨率)。
其中“一定范围”是一个参数,叫做Spread Factor。
如此操作后,我们就获得了一张储存着距离信息的距离场蒙版图片:
其中像素值为0.5的像素就是边界所在地,在引擎中直接根据0.5进行蒙版即可。
注意:
因为储存的是距离信息而非颜色信息,所以贴图不可被压缩,也不可以勾选sRGB选项
制作
设计
首先使用C++ OpenCV进行初步简单的实现。
对于每一个像素,获取其周围边长为Spread Factor的正方形范围内所有邻居像素,然后遍历其中每个像素。如果平方距离大于Spread Factor的平方,则表示在半径为Spread Factor的圆之外,抛弃。找到离自己最近的颜色不同的点,映射距离值,储存。
初步实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
| ··· std::vector<cv::Point2i> GetAllNeighbors(const cv::Point2i parent, const cv::Size matSize, const int spreadFactor) { std::vector<cv::Point2i> neighbors; int minX, maxX, minY, maxY; minX = MAX(parent.x - spreadFactor, 0); maxX = MIN(parent.x + spreadFactor, matSize.width); minY = MAX(parent.y - spreadFactor, 0); maxY = MIN(parent.y + spreadFactor, matSize.height);
for (int i = minY; i < maxY; i++) { for (int j = minX; j < maxX; j++) { cv::Point2i neighbor(j, i); if (neighbor == parent) continue; neighbors.push_back(neighbor); } } return neighbors; }
float CalculateSquareDistance(const cv::Point2i& parent, const cv::Point2i& neighbor) { float squareDistance = (neighbor - parent).dot(neighbor - parent); return squareDistance; }
void GenerateDistanceField(const cv::Mat& src, cv::Mat des, const int& spreadFactor) { int squareSpreadFactor = pow(spreadFactor, 2); for (int i = 0; i < src.rows; i++) { for (int j = 0; j < src.cols; j++) { cv::Point self(j, i); std::vector<cv::Point2i> neighbors; neighbors = GetAllNeighbors(self, srcSize, spreadFactor);
float nearestOppositeDistance; float squreNearestOppositeDistance; if (src.at<cv::Vec3b>(self)[0] >= 128) { nearestOppositeDistance = spreadFactor; squreNearestOppositeDistance = squareSpreadFactor; for (auto neighbor : neighbors) { if (src.at<cv::Vec3b>(neighbor)[0] < 128) { float squareDistance = CalculateSquareDistance(self, neighbor); if (squareDistance > squareSpreadFactor) continue; if (squareDistance < squreNearestOppositeDistance) { nearestOppositeDistance = sqrt(squareDistance); squreNearestOppositeDistance = pow(nearestOppositeDistance, 2); } } } } else { nearestOppositeDistance = -spreadFactor; squreNearestOppositeDistance = squareSpreadFactor; for (auto neighbor : neighbors) { if (src.at<cv::Vec3b>(neighbor)[0] >= 128) { float squareDistance = CalculateSquareDistance(self, neighbor); if (squareDistance > squareSpreadFactor) continue; if (squareDistance < squreNearestOppositeDistance) { nearestOppositeDistance = -sqrt(squareDistance); squreNearestOppositeDistance = pow(nearestOppositeDistance, 2); } } } } float finalResult = (nearestOppositeDistance + spreadFactor) / (2 * spreadFactor); des.at<uchar>(self) = finalResult * 255; } if (i % 100 == 0) { std::cout << i << std::endl; } } }
···
int main() { cv::String filePath = "su1024.png"; srcImg = ReadImage(filePath); srcSize = cv::Size(srcImg.cols, srcImg.rows); desImg = cv::Mat(srcSize, CV_8UC1, uchar(0)); clock_t start = clock(); GenerateDistanceField(srcImg, desImg, spreadFactor); clock_t end = clock(); std::cout << end - start << "ms" << std::endl;
cv::resize(desImg, desImg, cv::Size(256, 256), cv::INTER_AREA);
cv::imwrite("su_DF_256.png", desImg);
cv::waitKey(0); return 0; }
|
测试
可以生成图片,在UE4中也有不错的表现:
但是算法效率极低,计算一张1024分辨率的贴图经过测算需要长达66秒的时间。
优化
算法的问题显而易见,对于处在边界的像素点来说,如果可以从中心扩散向外检测,则可以及时跳出,节省很多算力。
所以我采用了之前写的获取圆环内整数点的算法,一个圆环一个圆环地扩散检测,这样在近处检测到结果后可以直接跳出,省去不必要的计算。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| constexpr int spreadFactor = 64; constexpr int searchingRingWidth = 1;
···
void GenerateDistanceField(const cv::Mat& src, cv::Mat des, const int& spreadFactor) { int squareSpreadFactor = pow(spreadFactor, 2); for (int i = 0; i < src.rows; i++) { for (int j = 0; j < src.cols; j++) { cv::Point self(j, i); bool bWhite = src.at<cv::Vec3b>(self)[0] >= 128; float nearestOppositeDistance = bWhite ? spreadFactor : -spreadFactor; float squreNearestOppositeDistance = squareSpreadFactor;
for (int t = 0; t < spreadFactor; t = t + searchingRingWidth) { int tBig = ((t + searchingRingWidth) > spreadFactor) ? spreadFactor : t + searchingRingWidth; std::vector<cv::Point2i> neighbors; neighbors = GetAllPointInRing(self, t, tBig, srcSize); bool bFound = false; for (auto neighbor : neighbors) { if (src.at<cv::Vec3b>(neighbor)[0] < 128 && bWhite) { bFound = true; float squareDistance = CalculateSquareDistance(self, neighbor); if (squareDistance < squreNearestOppositeDistance) { nearestOppositeDistance = sqrt(squareDistance); squreNearestOppositeDistance = squareDistance; } } else if (src.at<cv::Vec3b>(neighbor)[0] >= 128 && !bWhite) { bFound = true; float squareDistance = CalculateSquareDistance(self, neighbor); if (squareDistance < squreNearestOppositeDistance) { nearestOppositeDistance = -sqrt(squareDistance); squreNearestOppositeDistance = squareDistance; } } } if (bFound) break; } float finalResult = (nearestOppositeDistance + spreadFactor) / (2 * spreadFactor); des.at<uchar>(self) = finalResult * 255; } } }
|
不过这也有个问题,寻找圆环内整数点的操作需要涉及到开方,比较耗费性能。我使用变量searchingRingWidth控制每次扩大的圆环宽度,如果这个值很小的话,进行圆环找点操作的次数会变多,但是找到结果后的多余运算会更少(因为圆环更细,每次找到的邻居像素更少),而这个只能通过测试找到最佳值。
下面是在Spread Factor为64的情况下处理1024贴图的测试结果:
圆环宽度(像素) |
运行时间(s) |
1 |
99 |
2 |
64.969 |
4 |
50.824 |
8 |
45.757 |
16 |
54.636 |
12 |
45.5 |
10 |
42.929 |
可以看出圆环宽度为10的时候速度最快。
不过速度与处理的贴图之间也有很大关系,这种回溯法对于很复杂的蒙版效率极高,因为大部分都可以在前几个圆环的寻找中就找到结果跳出,但是对于纯色很多的蒙版来说效率会大大降低,因为大部分像素都需要完整的寻找完所有圆环,无疑极大地增加了求圆环算法的执行次数。经其他测试,复杂蒙版与简单蒙版的运行效率差距能有10倍之多。
迁移至UE4内
可以参考我这篇文章:https://1keven1.github.io/2021/07/27/%E3%80%90UE4_C-%E3%80%91%E5%88%9B%E5%BB%BA%E8%B4%B4%E5%9B%BE%E5%B9%B6%E4%BF%9D%E5%AD%98%E4%B8%BAAsset/
经过我后期测试,迁移至UE4内性能不佳,最终采用了外部多线程计算的方法,性能得到了极大提升
资源
https://github.com/1keven1/2D-Distance-Field-Generator
其他待解决问题
- 对于较为简单的蒙版的处理方法有待研究,初步考虑可以用生成MipMap的方法;