倡导文明和谐,往往需要给图片打上万恶的马赛克,对于iOS开发者来说,给图片打码需要使用OpenGL ES,编写GLSL文件给图片打码。
马赛克的原理其实就是将图片的某个区域用同一个色块填充,从而达到降低图片分辨率的效果,色块的颜色要根据该区域某个点来确定。本文将介绍正方形、正六边形、正三角形3种马赛克的实现算法。
正方形马赛克
正方形马赛克是将图片分成n*n个小的正方形色块,每个色块取一个当前色块某个点的颜色值填充。
比如一张 w*w 图片,如果我们要使用s*s的正方形马赛克滤镜,那么这个图片就要分割成 n*n 个正方形色块(n=floor(w/s), floor()是向下取整公式),每个正方形的色块颜色取这个这个正方形起始点的颜色值。比如如果当前纹理坐标为(x, y),我们可以通过公式
得到这个纹理坐标所处色块的起始点坐标,并取该起始点坐标的颜色值填充。
算法的具体实现在片元着色代码中。
precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;
// 假定纹理的大小
const vec2 TextureSize = vec2(400.0, 400.0);
// 马赛克的大小
const vec2 MosaicSize = vec2(8.0, 8.0);
void main (void) {
// 纹理坐标是0~1, 先将纹理坐标扩大假定纹理大小
vec2 TextureXY = vec2(TextureCoordsVarying.x * TextureSize.x, TextureCoordsVarying.y * TextureSize.y);
// 计算得到假定纹理大小下当前纹理坐标所处色块的起始点位置
vec2 MosaicXY = vec2(floor(TextureXY.x/MosaicSize.x)*MosaicSize.x, floor(TextureXY.y/MosaicSize.y)*MosaicSize.y);
// 在将起始点位置换算成标准0~1的范围
vec2 MosaicCoord = vec2(MosaicXY.x/TextureSize.x, MosaicXY.y/TextureSize.y);
vec4 mask = texture2D(Texture, MosaicCoord);
gl_FragColor = vec4(mask.rgb, 1.0);
}
另外附上顶点着色器代码
attribute vec4 Position;
attribute vec2 TextureCoords;
varying vec2 TextureCoordsVarying;
void main() {
TextureCoordsVarying = TextureCoords;
gl_Position = Position;
}
顶点着色器不需要做任何与马赛克滤镜有关的操作,较为简单,后续的六边形马赛克和三角形马赛克均采用相同顶点着色器代码。
六边形马赛克
六边形马赛克滤镜是用交错的六边形将图片分割成一个个色块,与正方形不同,我们无法直接知道当前纹理坐标位于哪个六边形,但我们可以将两个相邻的六边形组成一个矩形,如下图所示。
我们可以将六边形问题转换为矩形问题,和正方形一样,我们可以算出当前坐标位于哪个矩形内,然后再判断当前坐标是离v1和v2距离,决定采用哪个v1还是v2的色值,达到我们的目标效果。
要实现这个算法我们首先需要计算矩形的长宽,我们用高中的几何知识来计算,目前的已知条件是六边形的边长ab,根据正六边形的性质,我们知道顶点a到原点v1的距离等于边长,即 v1a = ab。而a、d、v2组成的三角形,很明显是一个角度分别为30°、60°、90°的直角三角形,我们都知道直角三角形30°角所对的那条边等于斜边的一半,得出 ad = 0.5*v2a = 0.5*ab,另外根据勾股定理得出 v2d = ad*√3 = ab*0.5*√3 ≈ 0.866025*ab, 而矩形的长 v1d = v1a + ad = ab + 0.5*ab = 1.5ab。所以我们可以得出公式
length为六边形的边长,width为矩形的长,height为矩形的宽
得到矩形的长宽后,我们就可以计算当前纹理坐标(x, y)所处矩形的两个原点v1、v2,因为矩形的原点的位置有两种情况,我们需要分开讨论。
矩形一
首先需要知道当前坐标位于第几行第几列的矩形,
column = floor(x/v1d) = floor(x/(ab*1.5)) = floor(x/(1.5*length))
row = floor(y/v2d) = floor(y/(0.866025*length))
所以,
v1.x = column * width = column * length * 1.5
v1.y = row * height + height = (row+1) * length * 0.866025
v2.x = column * width + width = (column+1) * length * 1.5
v2.y = row * height = row * length * 0.866025
矩形二
同样的,可以计算得到
v1.x = column * width = column * length * 1.5
v1.y = row * height = row * length * 0.866025
v2.x = column * width + width = (column+1) * length * 1.5
v2.y = row * height + height = (row+1) * length * 0.866025
接下来的步骤显然是得知当前坐标(x, y)所在矩形是矩形一还是矩形二,我们可以通过奇偶行列获得。
化繁为简,我们可以从起始位置开始观察,当前坐标位于偶数行、偶数列和奇数行、奇数列时是矩形二,奇数行、偶数列和偶数行、奇数列时是矩形一,至此,我们可以拿到v1和v2的坐标。
得到v1,v2后,我们就可以判断当前坐标(x, y)离谁更近而决定采用哪个原点的颜色值。
附上着色器代码:
precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;
const float mosaicSize = 0.015;
void main (void) {
float length = mosaicSize;
float dx = 1.5;
float dy = 0.866025;
float x = TextureCoordsVarying.x;
float y = TextureCoordsVarying.y;
// 当前位于第几行和第几列矩形
int wx = int(x/dx/length);
int wy = int(y/dy/length);
vec2 v1, v2, vn;
if (wx/2 * 2 == wx) {// 偶数行
if (wy/2 * 2 == wy) { // 偶数列
v1 = vec2(length*dx*float(wx), length*dy*float(wy));
v2 = vec2(length*dx*float(wx+1), length*dy*float(wy+1));
} else { // 奇数列
v1 = vec2(length*dx*float(wx), length*dy*float(wy+1));
v2 = vec2(length*dx*float(wx+1), length*dy*float(wy));
}
} else { // 奇数行
if (wy/2 * 2 == wy) { //偶数列
v1 = vec2(length*dx*float(wx), length*dy*float(wy+1));
v2 = vec2(length*dx*float(wx+1), length*dy*float(wy));
} else { // 奇数列
v1 = vec2(length*dx*float(wx), length*dy*float(wy));
v2 = vec2(length*dx*float(wx+1), length*dy*float(wy+1));
}
}
// 当前坐标到v1、v2的距离
float d1 = sqrt(pow(v1.x-x, 2.0) + pow(v1.y-y, 2.0));
float d2 = sqrt(pow(v2.x-x, 2.0) + pow(v2.y-y, 2.0));
if (d1 < d2) {
vn = v1;
} else {
vn = v2;
}
vec4 mask = texture2D(Texture, vn);
gl_FragColor = mask;
}
三角形马赛克
三角形马赛克是在六边形马赛克基础上变化得来的,从下图可以看到,一个正六边形可以分割成6个正三角形,而我们已经知道当前坐标(x, y)所在的六边形,只需要通过计算当前坐标(x, y)和原点的连线与起始边的夹角判断该点位于哪个3角形区域内,再取该三角形区域中心点的颜色值填充,就可以得到最终的结果。
夹角的计算可以使用用公式
着色器具体代码如下:
precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;
const float mosaicSize = 0.03;
void main (void) {
float length = mosaicSize;
float dx = 1.5;
float dy = 0.866025;
float x = TextureCoordsVarying.x;
float y = TextureCoordsVarying.y;
int wx = int(x/dx/length);
int wy = int(y/dy/length);
vec2 v1, v2, vn;
if (wx/2 * 2 == wx) {
if (wy/2 * 2 == wy) {
v1 = vec2(length*dx*float(wx), length*dy*float(wy));
v2 = vec2(length*dx*float(wx+1), length*dy*float(wy+1));
} else {
v1 = vec2(length*dx*float(wx), length*dy*float(wy+1));
v2 = vec2(length*dx*float(wx+1), length*dy*float(wy));
}
} else {
if (wy/2 * 2 == wy) {
v1 = vec2(length*dx*float(wx), length*dy*float(wy+1));
v2 = vec2(length*dx*float(wx+1), length*dy*float(wy));
} else {
v1 = vec2(length*dx*float(wx), length*dy*float(wy));
v2 = vec2(length*dx*float(wx+1), length*dy*float(wy+1));
}
}
float d1 = sqrt(pow(v1.x-x, 2.0) + pow(v1.y-y, 2.0));
float d2 = sqrt(pow(v2.x-x, 2.0) + pow(v2.y-y, 2.0));
if (d1 < d2) {
vn = v1;
} else {
vn = v2;
}
// 将π分为3等分
float PI3 = 3.14159/3.0;
// 获得弧度值
float a = atan((y-vn.y), (x-vn.x));
// 每个中心点与原点的xy偏移值
float xoffset = length*0.5;
float yoffset = xoffset*dy;
// 对象图中6个三角形区域的中心点
vec2 area1 = vec2(vn.x+xoffset, vn.y+yoffset);
vec2 area2 = vec2(vn.x, vn.y+yoffset);
vec2 area3 = vec2(vn.x-xoffset, vn.y+yoffset);
vec2 area4 = vec2(vn.x-xoffset, vn.y-yoffset);
vec2 area5 = vec2(vn.x, vn.y-yoffset);
vec2 area6 = vec2(vn.x+xoffset, vn.y-yoffset);
// 判断当前坐标位于哪个区域
if (a >= 0.0 && a < PI3) {
vn = area1;
} else if (a >= PI3 && a < PI3*2.0) {
vn = area2;
} else if (a >= PI3*2.0 && a <= PI3*3.0) {
vn = area3;
} else if (a <= -PI3*2.0 && a >= -PI3*3.0) {
vn = area4;
} else if (a <= -PI3 && a > -PI3*2.0) {
vn = area5;
} else if (a <= 0.0 && a < -PI3) {
vn = area6;
}
vec4 mask = texture2D(Texture, vn);
gl_FragColor = mask;
}