***本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 **
前言
之前在知乎看到一篇关于 LowPoly js 实现的贴,觉得效果很棒,就打算在 Android 上实现看看。本来以为一两天就可以搞定,没想到不知不觉耗了一周多,踏坑不少。鉴于内容可能会比较多,所以暂时打算分篇幅介绍实现过程。
LowPoly (低多边形) 这种风格在前两年十分风靡,比较有名的《纪念碑谷》就是采用这种美术风格的游戏。
效果
这个是由纯 java 实现的版本,在查找资料时找到的,应该是国内某人写的。用 Nexus5 测试,处理尺寸为 900 * 900 的 bitmap 图片耗时基本在 60~90 s 左右,可见效率确实比较不能忍受。从 Log 的计时器看,在处理两千不到的取样点的情况下,三角化和绘图耗时都在 1s 以内,所以估计主要耗时应该在 Sobel 边缘处理和点列采样的过程中,没有验证,有心的同学可以试试看。需要说明的是,我的实现使用了这位同学的 Delaunay 算法和 canvas 部分的代码(表示感谢!),所以在取样点差不多时,我的效果应该和这个差不多。
这个是国外某位使用 jni 实现的版本,是在着手写该篇时才找到的,看到其介绍由 jni 实现,想必肯定会比 java 快,担心如果比 rs 实现快很多,也没必要写出来了。根据测试效果,明显比 java 实现快很多,处理同样的图片,用了7秒多,比 java 快了十倍左右。从效果图上来看,其边缘更加清晰,尖锐三角形也很多,风格与 java 实现的稍有不同,不过这些应该是因为使用不同的三角化算法导致,对耗时影响应该不大。
最后是使用 RenderScript 实现的,为了方便描述实现过程 Demo 分步骤展示了主要的过程。处理同样的图片,通过计时器可以看到,总耗时大概在 2s以内,效率应该说还是比较能够让人接受的。正如前文提到的,使用了前文同学的 Delaunay 算法和 canvas 部分代码,所以风格应该差不多。不同的是,用 rs 处理了灰度、边缘化和采样的过程,大大节省了时间。
实现原理
根据知乎那个答案,这种效果主要有那么几个步骤:
- 首先将原图转为灰度图(不是必须);
- 使用灰度图(或原图)查找边缘,获得边缘线图;
- 从边缘线上采样,采样率决定后期三角的数量与图片精度;
- 使用上步骤得到的采样点列,生成三角化序列;
- 遍历三角形,使用原图得到的色值填充三角形。
其中,在边缘检测上一般采用 Sobel 算法,即通过 sobel 算子计算某一点的梯度,可以简单理解为某一点与周围点灰度差异度;然后,在三角化处理上,一般采用 Delaunay 算法。
在实现方面,首先 jni 并不是一定会比 java 快的,使用 jni 一般是为了某些特定场景,其中图形处理所需要的大量数学运算就是其一,在 LowPoly 的代码中:
LowPoly.java
...
static {
System.loadLibrary("lowpoly");
}
public static native int[] getTriangles(int[] pixels,int width,int height,int pointNum);
/**
*
* @param input
* @param blur
* @return
*/
public static Bitmap generate(Bitmap input,int blur){
int width = input.getWidth();
int height = input.getHeight();
Bitmap newImage = Bitmap.createBitmap(input.getWidth(),input.getHeight(),Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(newImage);
Paint paint = new Paint();
paint.setAntiAlias(false);
paint.setStyle(Paint.Style.FILL);
int x1, x2, x3, y1, y2, y3;
int pixels[] = new int[width*height];
for (int i=0;i<height;i++){
for(int j=0;j<width;j++){
pixels[i*width+j] = input.getPixel(j,i);
}
}
int[] triangles = getTriangles(pixels,width,height,blur);
for (int i=0;i<triangles.length;i=i+6){
x1 = triangles[i];
y1 = triangles[i+1];
x2 = triangles[i+2];
y2 = triangles[i+3];
x3 = triangles[i+4];
y3 = triangles[i+5];
int color = input.getPixel((x1 + x2 + x3) / 3, (y1 + y2 + y3) / 3);
Path path = new Path();
path.moveTo(x1,y1);
path.lineTo(x2,y2);
path.lineTo(x3,y3);
path.close();
paint.setColor(color);
canvas.drawPath(path,paint);
}
return newImage;
}
...
可见,除了取色、上色两个步骤,其他步骤都通过调用 native
的 getTriangles
方法处理,其中应该就是负责图形处理的大量运算。
对于 RenderScript,Google 这样描述:
RenderScript
RenderScript is a framework for running computationally intensive tasks at high performance on Android.
RenderScript is primarily oriented for use with data-parallel computation, although serial workloads can
benefit as well. The RenderScript runtime parallelizes work across processors available on a device,
such as multi-core CPUs and GPUs. This allows you to focus on expressing algorithms rather than
scheduling work. RenderScript is especially useful for applications performing image processing,
computational photography, or computer vision.
介绍了 RenderScript 主要基于多核 CPU 或者 GPU,主要是为了处理图形运算。早些时候,在手机处理器和图形处理器的性能普遍不高的情况下, rs 似乎并不太引人注意,从网络上少得可怜的资料就可以看出。不过,时至今日,各大手机厂商在硬件的堆砌上似乎一点也没有节制,以及 VR、 AR 等视觉技术的兴起,相信 rs 技术会有更多应用场景。
这篇就到这里,下一篇将会介绍一下 RenderScript 的简单基础和常用函数。