倡導(dǎo)文明和諧,往往需要給圖片打上萬(wàn)惡的馬賽克,對(duì)于iOS開發(fā)者來(lái)說(shuō),給圖片打碼需要使用OpenGL ES,編寫GLSL文件給圖片打碼。
馬賽克的原理其實(shí)就是將圖片的某個(gè)區(qū)域用同一個(gè)色塊填充,從而達(dá)到降低圖片分辨率的效果,色塊的顏色要根據(jù)該區(qū)域某個(gè)點(diǎn)來(lái)確定。本文將介紹正方形、正六邊形、正三角形3種馬賽克的實(shí)現(xiàn)算法。
正方形馬賽克

正方形馬賽克是將圖片分成n*n個(gè)小的正方形色塊,每個(gè)色塊取一個(gè)當(dāng)前色塊某個(gè)點(diǎn)的顏色值填充。
比如一張 w*w 圖片,如果我們要使用s*s的正方形馬賽克濾鏡,那么這個(gè)圖片就要分割成 n*n 個(gè)正方形色塊(n=floor(w/s), floor()是向下取整公式),每個(gè)正方形的色塊顏色取這個(gè)這個(gè)正方形起始點(diǎn)的顏色值。比如如果當(dāng)前紋理坐標(biāo)為(x, y),我們可以通過(guò)公式
得到這個(gè)紋理坐標(biāo)所處色塊的起始點(diǎn)坐標(biāo),并取該起始點(diǎn)坐標(biāo)的顏色值填充。

算法的具體實(shí)現(xiàn)在片元著色代碼中。
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) {
// 紋理坐標(biāo)是0~1, 先將紋理坐標(biāo)擴(kuò)大假定紋理大小
vec2 TextureXY = vec2(TextureCoordsVarying.x * TextureSize.x, TextureCoordsVarying.y * TextureSize.y);
// 計(jì)算得到假定紋理大小下當(dāng)前紋理坐標(biāo)所處色塊的起始點(diǎn)位置
vec2 MosaicXY = vec2(floor(TextureXY.x/MosaicSize.x)*MosaicSize.x, floor(TextureXY.y/MosaicSize.y)*MosaicSize.y);
// 在將起始點(diǎn)位置換算成標(biāo)準(zhǔn)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);
}
另外附上頂點(diǎn)著色器代碼
attribute vec4 Position;
attribute vec2 TextureCoords;
varying vec2 TextureCoordsVarying;
void main() {
TextureCoordsVarying = TextureCoords;
gl_Position = Position;
}
頂點(diǎn)著色器不需要做任何與馬賽克濾鏡有關(guān)的操作,較為簡(jiǎn)單,后續(xù)的六邊形馬賽克和三角形馬賽克均采用相同頂點(diǎn)著色器代碼。
六邊形馬賽克

六邊形馬賽克濾鏡是用交錯(cuò)的六邊形將圖片分割成一個(gè)個(gè)色塊,與正方形不同,我們無(wú)法直接知道當(dāng)前紋理坐標(biāo)位于哪個(gè)六邊形,但我們可以將兩個(gè)相鄰的六邊形組成一個(gè)矩形,如下圖所示。

我們可以將六邊形問(wèn)題轉(zhuǎn)換為矩形問(wèn)題,和正方形一樣,我們可以算出當(dāng)前坐標(biāo)位于哪個(gè)矩形內(nèi),然后再判斷當(dāng)前坐標(biāo)是離v1和v2距離,決定采用哪個(gè)v1還是v2的色值,達(dá)到我們的目標(biāo)效果。
要實(shí)現(xiàn)這個(gè)算法我們首先需要計(jì)算矩形的長(zhǎng)寬,我們用高中的幾何知識(shí)來(lái)計(jì)算,目前的已知條件是六邊形的邊長(zhǎng)ab,根據(jù)正六邊形的性質(zhì),我們知道頂點(diǎn)a到原點(diǎn)v1的距離等于邊長(zhǎng),即 v1a = ab。而a、d、v2組成的三角形,很明顯是一個(gè)角度分別為30°、60°、90°的直角三角形,我們都知道直角三角形30°角所對(duì)的那條邊等于斜邊的一半,得出 ad = 0.5*v2a = 0.5*ab,另外根據(jù)勾股定理得出 v2d = ad*√3 = ab*0.5*√3 ≈ 0.866025*ab, 而矩形的長(zhǎng) v1d = v1a + ad = ab + 0.5*ab = 1.5ab。所以我們可以得出公式
length為六邊形的邊長(zhǎng),width為矩形的長(zhǎng),height為矩形的寬
得到矩形的長(zhǎng)寬后,我們就可以計(jì)算當(dāng)前紋理坐標(biāo)(x, y)所處矩形的兩個(gè)原點(diǎn)v1、v2,因?yàn)榫匦蔚脑c(diǎn)的位置有兩種情況,我們需要分開討論。
矩形一

首先需要知道當(dāng)前坐標(biāo)位于第幾行第幾列的矩形,
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
矩形二

同樣的,可以計(jì)算得到
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
接下來(lái)的步驟顯然是得知當(dāng)前坐標(biāo)(x, y)所在矩形是矩形一還是矩形二,我們可以通過(guò)奇偶行列獲得。

化繁為簡(jiǎn),我們可以從起始位置開始觀察,當(dāng)前坐標(biāo)位于偶數(shù)行、偶數(shù)列和奇數(shù)行、奇數(shù)列時(shí)是矩形二,奇數(shù)行、偶數(shù)列和偶數(shù)行、奇數(shù)列時(shí)是矩形一,至此,我們可以拿到v1和v2的坐標(biāo)。
得到v1,v2后,我們就可以判斷當(dāng)前坐標(biāo)(x, y)離誰(shuí)更近而決定采用哪個(gè)原點(diǎn)的顏色值。
附上著色器代碼:
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;
// 當(dāng)前位于第幾行和第幾列矩形
int wx = int(x/dx/length);
int wy = int(y/dy/length);
vec2 v1, v2, vn;
if (wx/2 * 2 == wx) {// 偶數(shù)行
if (wy/2 * 2 == wy) { // 偶數(shù)列
v1 = vec2(length*dx*float(wx), length*dy*float(wy));
v2 = vec2(length*dx*float(wx+1), length*dy*float(wy+1));
} else { // 奇數(shù)列
v1 = vec2(length*dx*float(wx), length*dy*float(wy+1));
v2 = vec2(length*dx*float(wx+1), length*dy*float(wy));
}
} else { // 奇數(shù)行
if (wy/2 * 2 == wy) { //偶數(shù)列
v1 = vec2(length*dx*float(wx), length*dy*float(wy+1));
v2 = vec2(length*dx*float(wx+1), length*dy*float(wy));
} else { // 奇數(shù)列
v1 = vec2(length*dx*float(wx), length*dy*float(wy));
v2 = vec2(length*dx*float(wx+1), length*dy*float(wy+1));
}
}
// 當(dāng)前坐標(biāo)到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;
}
三角形馬賽克

三角形馬賽克是在六邊形馬賽克基礎(chǔ)上變化得來(lái)的,從下圖可以看到,一個(gè)正六邊形可以分割成6個(gè)正三角形,而我們已經(jīng)知道當(dāng)前坐標(biāo)(x, y)所在的六邊形,只需要通過(guò)計(jì)算當(dāng)前坐標(biāo)(x, y)和原點(diǎn)的連線與起始邊的夾角判斷該點(diǎn)位于哪個(gè)3角形區(qū)域內(nèi),再取該三角形區(qū)域中心點(diǎn)的顏色值填充,就可以得到最終的結(jié)果。

夾角的計(jì)算可以使用用公式
著色器具體代碼如下:
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));
// 每個(gè)中心點(diǎn)與原點(diǎn)的xy偏移值
float xoffset = length*0.5;
float yoffset = xoffset*dy;
// 對(duì)象圖中6個(gè)三角形區(qū)域的中心點(diǎn)
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);
// 判斷當(dāng)前坐標(biāo)位于哪個(gè)區(qū)域
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;
}