作者:田勇?OpenHarmony知識(shí)體系工作組
現(xiàn)在市面上有很多APP,都或多或少對(duì)圖片有模糊上的設(shè)計(jì),所以,圖片模糊效果到底怎么實(shí)現(xiàn)的呢? 首先,我們來(lái)了解下模糊效果的對(duì)比 ? ? ? 從視覺上,兩張圖片,有一張是模糊的,那么,在實(shí)現(xiàn)圖片模糊效果之前,我們首先需要了解圖片模糊的本質(zhì)是什么? 在此介紹模糊本質(zhì)之前,我們來(lái)了解下當(dāng)前主流的兩個(gè)移動(dòng)端平臺(tái)(Android與iOS)的實(shí)現(xiàn)。 對(duì)Android開發(fā)者而言,比較熟悉且完善的圖片變換三方庫(kù)以glide-transformations(https://github.com/wasabeef/glide-transformations)為樣例,來(lái)看看它是基于什么實(shí)現(xiàn)的。 ? Android中有兩種實(shí)現(xiàn): 1、FastBlur,根據(jù)stackBlur模糊算法來(lái)操作圖片的像素點(diǎn)實(shí)現(xiàn)效果,但效率低,已過時(shí)。 2、RenderScript,這個(gè)是Google官方提供的,用來(lái)在Android上編寫一套高性能代碼的語(yǔ)言,可以運(yùn)行在CPU及其GPU上,效率較高。 而對(duì)iOS開發(fā)者而言,GPUImage(https://github.com/BradLarson/GPUImage/)比較主流。我們可以在其中看到高斯模糊過濾器(GPUImageGaussianBlurFilter),它里面是根據(jù)OpenGL來(lái)實(shí)現(xiàn),通過GLSL語(yǔ)言定義的著色器,操作GPU單元,達(dá)到模糊效果。 所以,我們可以看出,操作GPU來(lái)達(dá)到我們所需要的效果效率更高。因此我們?cè)贠penHarmony上也能通過操作GPU,來(lái)實(shí)現(xiàn)我們想要的高性能模糊效果。 回歸正題,先來(lái)了解下模糊的本質(zhì)是什么? ?
本質(zhì)
模糊,可以理解為圖片中的每個(gè)像素點(diǎn)都取其周邊像素的平均值。 ?
上圖M點(diǎn)的像素點(diǎn)就是我們的焦點(diǎn)像素。周圍ABCDEFGH都是M點(diǎn)(焦點(diǎn))周圍的像素點(diǎn),那么根據(jù)模糊的概念: ? M(rgb)? =(A+B+C+D+E+F+G+H)/ 8 ? 我們根據(jù)像素點(diǎn)的r、g、b值,得到M點(diǎn)的像素點(diǎn)值,就這樣,一個(gè)一個(gè)像素點(diǎn)的操作,中間點(diǎn)相當(dāng)于失去視覺上的焦點(diǎn),整個(gè)圖片就產(chǎn)生模糊的效果。但這樣一邊倒的方式,在模糊的效果上,達(dá)不到需求的,所以,我們就需要根據(jù)這個(gè)模糊的本質(zhì)概念,去想想,加一些東西或者更改取平均值的規(guī)則,完成我們想要的效果。故,高斯模糊,一個(gè)家喻戶曉的名字,就出現(xiàn)在我們面前。 ?
高斯模糊
高斯模糊,運(yùn)用了正態(tài)分布函數(shù),進(jìn)行各個(gè)加權(quán)平均,正態(tài)分布函數(shù)如下: ?
其中參數(shù):μ為期望值,σ為標(biāo)準(zhǔn)差,當(dāng)μ=0,σ=0的時(shí)候,為標(biāo)準(zhǔn)的正態(tài)分布,其形狀參考如下圖: ?
可以看出: 其一,離中心點(diǎn)越近,分配的權(quán)重就越高。這樣我們?cè)谟?jì)算圖片的焦點(diǎn)像素值時(shí),將該點(diǎn)當(dāng)作中心點(diǎn),當(dāng)作1的權(quán)重,其他周圍的點(diǎn),按照該正態(tài)分布的位置,去分配它的權(quán)重,這樣我們就可以根據(jù)該正態(tài)分布函數(shù)及其各個(gè)點(diǎn)的像素ARGB值,算出經(jīng)過正態(tài)分布之后的像素ARGB值。 其二,離中心點(diǎn)越近,若是設(shè)置的模糊半徑很小,代表其模糊的焦點(diǎn)周圍的像素點(diǎn)離焦點(diǎn)的像素相差就不大,這樣模糊的效果就清晰。而模糊半徑越大,其周圍分布的像素色差就很大,這樣的模糊效果就越模糊。 通過圖片的寬高拿到每個(gè)像素點(diǎn)的數(shù)據(jù),再根據(jù)這個(gè)正態(tài)分布公式,得到我們想要的像素點(diǎn)的ARGB值,之后將處理過的像素點(diǎn)重新寫入到圖片中,就能實(shí)現(xiàn)我們想要的圖片模糊效果。 ?
流程
根據(jù)上面的闡述,就可以梳理出在OpenHarmony中的具體的實(shí)現(xiàn)流程: ●獲取整張圖片的像素點(diǎn)數(shù)據(jù) ●循環(huán)圖片的寬高,獲取每個(gè)像素點(diǎn)的焦點(diǎn) ●在上述循環(huán)里,根據(jù)焦點(diǎn)按照正態(tài)分布公式進(jìn)行加權(quán)平均,算出各個(gè)焦點(diǎn)周圍新的像素值 ●將各個(gè)像素點(diǎn)寫入圖片 關(guān)鍵依賴OpenHarmony系統(tǒng)基礎(chǔ)能力如下: 第一、獲取圖片的像素點(diǎn),系統(tǒng)有提供一次性獲取整張圖片的像素點(diǎn)數(shù)據(jù),其接口如下。
?
readPixelsToBuffer(dst: ArrayBuffer): Promise可以看出,系統(tǒng)將獲取到像素點(diǎn)數(shù)據(jù)ARGB值,存儲(chǔ)到ArrayBuffer中去。 ? 第二、循環(huán)獲取每個(gè)像素點(diǎn),將其x、y點(diǎn)的像素點(diǎn)當(dāng)作焦點(diǎn)。; readPixelsToBuffer(dst:?ArrayBuffer,?callback:?AsyncCallback ):?void;
for (y = 0; y < imageHeight; y++) { for (x = 0; x < imageWidth; x++) { //...... 獲取當(dāng)前的像素焦點(diǎn)x、y } }? 第三、循環(huán)獲取焦點(diǎn)周圍的像素點(diǎn)(以焦點(diǎn)為原點(diǎn),以設(shè)置的模糊半徑為半徑)。
for ( let m = centPointY-radius; m < centPointY+radius; m++) { for ( let n = centPointX-radius; n < centPointX+radius; n++) { //...... this.calculatedByNormality(...); //正態(tài)分布公式化處理像素點(diǎn) //...... } }? 第四、將各個(gè)圖片的像素?cái)?shù)據(jù)寫入圖片中。系統(tǒng)有提供一次性寫入像素點(diǎn),其接口如下。
writeBufferToPixels(src: ArrayBuffer): Promise通過上面的流程,我們可以在OpenHarmony系統(tǒng)下,獲取到經(jīng)過正態(tài)分布公式處理的像素點(diǎn),至此圖片模糊效果已經(jīng)實(shí)現(xiàn)。 ? 但是,經(jīng)過測(cè)試發(fā)現(xiàn),這個(gè)方式實(shí)現(xiàn)模糊化的過程,很耗時(shí),達(dá)不到我們的性能要求。若是一張很大的圖片,就單單寬高循環(huán)來(lái)看,比如1920*1080寬高的圖片就要循環(huán)2,073,600次,非常耗時(shí)且對(duì)設(shè)備的CPU也有非常大的消耗,因此我們還需要對(duì)其進(jìn)行性能優(yōu)化。 ?; writeBufferToPixels(src:?ArrayBuffer,?callback:?AsyncCallback ):?void;
?
?
模糊性能優(yōu)化思路
如上面所訴,考慮到OpenHarmony的環(huán)境的特點(diǎn)及其系統(tǒng)提供的能力,可以考慮如下幾個(gè)方面進(jìn)行優(yōu)化: ? 第一、參照社區(qū)已有成熟的圖片模糊算法處理,如(Android的FastBlur)。 第二、C層性能要比JS層更好,將像素點(diǎn)的數(shù)據(jù)處理,通過NAPI機(jī)制,將其放入C層處理。如:將其循環(huán)獲取焦點(diǎn)及其通過正態(tài)分布公式處理的都放到C層中處理。 第三、基于系統(tǒng)底層提供的OpenGL,操作頂點(diǎn)著色器及片元著色器操作GPU,得到我們要的模糊效果。 ? 首先,我們來(lái)根據(jù)Android中的FastBlur模糊化處理,參照其實(shí)現(xiàn)原理進(jìn)行在基于OpenHarmony系統(tǒng)下實(shí)現(xiàn)的代碼如下:
let imageInfo = await bitmap.getImageInfo(); let size = { width: imageInfo.size.width, height: imageInfo.size.height } if (!size) { func(new Error("fastBlur The image size does not exist."), null) return; } let w = size.width; let h = size.height; var pixEntry: Array從上面代碼,可以看出,按照FastBlur的邏輯,還是逃不開上層去處理單個(gè)像素點(diǎn),逃不開圖片寬高的循環(huán)。經(jīng)過測(cè)試也發(fā)現(xiàn),在一張400*300的圖片上,完成圖片的模糊需要十幾秒,所以第一個(gè)優(yōu)化方案,在js環(huán)境上是行不通的。 其次,將其像素點(diǎn)處理,通過NAPI的機(jī)制,將像素點(diǎn)數(shù)據(jù)ArrayBuffer傳入到C層,由于在C層也需要循環(huán)去處理每個(gè)像素點(diǎn),傳入大數(shù)據(jù)的ArrayBuffer時(shí)對(duì)系統(tǒng)的native的消耗嚴(yán)重。最后經(jīng)過測(cè)試也發(fā)現(xiàn),模糊的過程也很緩慢,達(dá)不到性能要求。 所以對(duì)比分析之后,最終的優(yōu)化方案是采取系統(tǒng)底層提供的OpenGL,通過GPU去操作系統(tǒng)的圖形處理器,解放出CPU的能力。 ?= new Array() var pix: Array = new Array() let bufferData = new ArrayBuffer(bitmap.getPixelBytesNumber()); await bitmap.readPixelsToBuffer(bufferData); let dataArray = new Uint8Array(bufferData); for (let index = 0; index < dataArray.length; index+=4) { const r = dataArray[index]; const g = dataArray[index+1]; const b = dataArray[index+2]; const f = dataArray[index+3]; let entry = new PixelEntry(); entry.a = 0; entry.b = b; entry.g = g; entry.r = r; entry.f = f; entry.pixel = ColorUtils.rgb(entry.r, entry.g, entry.b); pixEntry.push(entry); pix.push(ColorUtils.rgb(entry.r, entry.g, entry.b)); } let wm = w - 1; let hm = h - 1; let wh = w * h; let div = radius + radius + 1; let r = CalculatePixelUtils.createIntArray(wh); let g = CalculatePixelUtils.createIntArray(wh); let b = CalculatePixelUtils.createIntArray(wh); let rsum, gsum, bsum, x, y, i, p, yp, yi, yw: number; let vmin = CalculatePixelUtils.createIntArray(Math.max(w, h)); let divsum = (div + 1) >> 1; divsum *= divsum; let dv = CalculatePixelUtils.createIntArray(256 * divsum); for (i = 0; i < 256 * divsum; i++) { dv[i] = (i / divsum); } yw = yi = 0; let stack = CalculatePixelUtils.createInt2DArray(div, 3); let stackpointer, stackstart, rbs, routsum, goutsum, boutsum, rinsum, ginsum, binsum: number; let sir: Array ; let r1 = radius + 1; for (y = 0; y < h; y++) { rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; for (i = -radius; i <= radius; i++) { p = pix[yi + Math.min(wm, Math.max(i, 0))]; sir = stack[i + radius]; sir[0] = (p & 0xff0000) >> 16; sir[1] = (p & 0x00ff00) >> 8; sir[2] = (p & 0x0000ff); rbs = r1 - Math.abs(i); rsum += sir[0] * rbs; gsum += sir[1] * rbs; bsum += sir[2] * rbs; if (i > 0) { rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]; } else { routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]; } } stackpointer = radius; for (x = 0; x < w; x++) { r[yi] = dv[rsum]; g[yi] = dv[gsum]; b[yi] = dv[bsum]; rsum -= routsum; gsum -= goutsum; bsum -= boutsum; stackstart = stackpointer - radius + div; sir = stack[stackstart % div]; routsum -= sir[0]; goutsum -= sir[1]; boutsum -= sir[2]; if (y == 0) { vmin[x] = Math.min(x + radius + 1, wm); } p = pix[yw + vmin[x]]; sir[0] = (p & 0xff0000) >> 16; sir[1] = (p & 0x00ff00) >> 8; sir[2] = (p & 0x0000ff); rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]; rsum += rinsum; gsum += ginsum; bsum += binsum; stackpointer = (stackpointer + 1) % div; sir = stack[(stackpointer) % div]; routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]; rinsum -= sir[0]; ginsum -= sir[1]; binsum -= sir[2]; yi++; } yw += w; } for (x = 0; x < w; x++) { rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; yp = -radius * w; for (i = -radius; i <= radius; i++) { yi = Math.max(0, yp) + x; sir = stack[i + radius]; sir[0] = r[yi]; sir[1] = g[yi]; sir[2] = b[yi]; rbs = r1 - Math.abs(i); rsum += r[yi] * rbs; gsum += g[yi] * rbs; bsum += b[yi] * rbs; if (i > 0) { rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]; } else { routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]; } if (i < hm) { yp += w; } } yi = x; stackpointer = radius; for (y = 0; y < h; y++) { // Preserve alpha channel: ( 0xff000000 & pix[yi] ) pix[yi] = (0xff000000 & pix[Math.round(yi)]) | (dv[Math.round(rsum)] << 16) | (dv[ Math.round(gsum)] << 8) | dv[Math.round(bsum)]; rsum -= routsum; gsum -= goutsum; bsum -= boutsum; stackstart = stackpointer - radius + div; sir = stack[stackstart % div]; routsum -= sir[0]; goutsum -= sir[1]; boutsum -= sir[2]; if (x == 0) { vmin[y] = Math.min(y + r1, hm) * w; } p = x + vmin[y]; sir[0] = r[p]; sir[1] = g[p]; sir[2] = b[p]; rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]; rsum += rinsum; gsum += ginsum; bsum += binsum; stackpointer = (stackpointer + 1) % div; sir = stack[stackpointer]; routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]; rinsum -= sir[0]; ginsum -= sir[1]; binsum -= sir[2]; yi += w; } } let bufferNewData = new ArrayBuffer(bitmap.getPixelBytesNumber()); let dataNewArray = new Uint8Array(bufferNewData); let index = 0; for (let i = 0; i < dataNewArray.length; i += 4) { dataNewArray[i] = ColorUtils.red(pix[index]); dataNewArray[i+1] = ColorUtils.green(pix[index]); dataNewArray[i+2] = ColorUtils.blue(pix[index]); dataNewArray[i+3] = pixEntry[index].f; index++; } await bitmap.writeBufferToPixels(bufferNewData); if (func) { func("success", bitmap); }
基于OpenGL操作GPU來(lái)提升模糊性能
在進(jìn)行基于OpenGL進(jìn)行性能提升前,我們需要了解OpenGL中的頂點(diǎn)著色器(vertex shader)及其片元著色器(fragment shader)。著色器(shader)是運(yùn)行在GPU上的最小單元,功能是將輸入轉(zhuǎn)換輸出且各個(gè)shader之間是不能通信的,需要使用的開發(fā)語(yǔ)言GLSL。這里就不介紹GLSL的語(yǔ)言規(guī)則了。 ? 頂點(diǎn)著色器(vertex shader) 確定要畫圖片的各個(gè)頂點(diǎn)(如:三角形的角的頂點(diǎn)),注意:每個(gè)頂點(diǎn)運(yùn)行一次。一旦最終位置已知,OpenGL將獲取可見的頂點(diǎn)集,并將它們組裝成點(diǎn)、線和三角形。且以逆時(shí)針繪制的。 ? 片元著色器(fragment shader) 生成點(diǎn)、線或三角形的每個(gè)片元的最終顏色,并對(duì)每個(gè)fragment運(yùn)行一次。fragment是單一顏色的小矩形區(qū)域,類似于計(jì)算機(jī)屏幕上的像素,簡(jiǎn)單的說(shuō),就是將頂點(diǎn)著色器形成的點(diǎn)、線或者三角形區(qū)域,添加顏色。 片元著色器的主要目的是告訴GPU每個(gè)片元的最終顏色應(yīng)該是什么。對(duì)于圖元(primitive)的每個(gè)fragment,片元著色器將被調(diào)用一次,因此如果一個(gè)三角形映射到10000個(gè)片元,那么片元著色器將被調(diào)用10000次。 ? OpenGL簡(jiǎn)單的繪制流程: 讀取頂點(diǎn)信息 ----------> 運(yùn)行頂點(diǎn)著色器 ----------> 圖元裝配----------> 運(yùn)行片元著色器----------> 往幀緩沖區(qū)寫入----------> 屏幕上最終效果 簡(jiǎn)單的說(shuō),就是根據(jù)頂點(diǎn)著色器形成的點(diǎn)、線、三角形形成的區(qū)域,由片元著色器對(duì)其著色,之后就將這些數(shù)據(jù)寫入幀緩沖區(qū)(Frame Buffer)的內(nèi)存塊中,再由屏幕顯示這個(gè)緩沖區(qū)。 ? 那模糊的效果怎么來(lái)實(shí)現(xiàn)呢? 首先我們來(lái)定義我們的頂點(diǎn)著色器及其片元著色器。如下代碼: ? 頂點(diǎn)著色器:
const char vShaderStr[] = "#version 300 es " "layout(location = 0) in vec4 a_position; " "layout(location = 1) in vec2 a_texCoord; " "out vec2 v_texCoord; " "void main() " "{ " " gl_Position = a_position; " " v_texCoord = a_texCoord; " ??????"}?????????????????????????????????????????? ";? 片元著色器:
const char fShaderStr0[] = "#version 300 es " "precision mediump float; " "in vec2 v_texCoord; " "layout(location = 0) out vec4 outColor; " "uniform sampler2D s_TextureMap; " "void main() " "{ " " outColor = texture(s_TextureMap, v_texCoord); " ????"}";其中version代表OpenGL的版本,layout在GLSL中是用于著色器的輸入或者輸出,uniform為一致變量。在著色器執(zhí)行期間一致變量的值是不變的,只能在全局范圍進(jìn)行聲明,gl_Position是OpenGL內(nèi)置的變量(輸出屬性-變換后的頂點(diǎn)的位置,用于后面的固定的裁剪等操作。所有的頂點(diǎn)著色器都必須寫這個(gè)值),texture函數(shù)是openGL采用2D紋理繪制。然后,我們還需要定義好初始的頂點(diǎn)坐標(biāo)數(shù)據(jù)等;
//頂點(diǎn)坐標(biāo) const GLfloat vVertices[] = { -1.0f, -1.0f, 0.0f, // bottom left 1.0f, -1.0f, 0.0f, // bottom right -1.0f, 1.0f, 0.0f, // top left 1.0f, 1.0f, 0.0f, // top right }; //正常紋理坐標(biāo) const GLfloat vTexCoors[] = { 0.0f, 1.0f, // bottom left 1.0f, 1.0f, // bottom right 0.0f, 0.0f, // top left 1.0f, 0.0f, // top right }; //fbo 紋理坐標(biāo)與正常紋理方向不同(上下鏡像) const GLfloat vFboTexCoors[] = { 0.0f, 0.0f, // bottom left 1.0f, 0.0f, // bottom right 0.0f, 1.0f, // top left 1.0f, 1.0f, // top right };下面就進(jìn)行OpenGL的初始化操作, 獲取display,用來(lái)創(chuàng)建EGLSurface的
m_eglDisplay?=?eglGetDisplay(EGL_DEFAULT_DISPLAY);?初始化 EGL 方法
eglInitialize(m_eglDisplay,?&eglMajVers,?&eglMinVers)獲取 EGLConfig 對(duì)象,確定渲染表面的配置信息
eglChooseConfig(m_eglDisplay,?confAttr,?&m_eglConf,?1,?&numConfigs)創(chuàng)建渲染表面 EGLSurface,使用 eglCreatePbufferSurface 創(chuàng)建屏幕外渲染區(qū)域
m_eglSurface?=?eglCreatePbufferSurface(m_eglDisplay,?m_eglConf,?surfaceAttr)創(chuàng)建渲染上下文 EGLContext
m_eglCtx?=?eglCreateContext(m_eglDisplay,?m_eglConf,?EGL_NO_CONTEXT,?ctxAttr);綁定上下文
eglMakeCurrent(m_eglDisplay,?m_eglSurface,?m_eglSurface,?m_eglCtx)通過默認(rèn)的頂點(diǎn)著色器與片元著色器,加載到GPU中
GLuint GLUtils::LoadShader(GLenum shaderType, const char *pSource) { GLuint shader = 0; shader = glCreateShader(shaderType); if(shader) { glShaderSource(shader, 1, &pSource, NULL); glCompileShader(shader); GLint compiled = 0; glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled); if (!compiled){ GLint infoLen = 0; glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen); if (infoLen) { char* buf = (char*) malloc((size_t)infoLen); if (buf) { glGetShaderInfoLog(shader, infoLen, NULL, buf); LOGI("gl--> GLUtils::LoadShader Could not link shader:%{public}s", buf); free(buf); } glDeleteShader(shader); shader = 0; } } } return shader; }創(chuàng)建一個(gè)空的著色器程序?qū)ο?
program?=?glCreateProgram();將著色器對(duì)象附加到program對(duì)象
glAttachShader(program, vertexShaderHandle); glAttachShader(program,?fragShaderHandle);連接一個(gè)program對(duì)象
glLinkProgram(program);創(chuàng)建并初始化緩沖區(qū)對(duì)象的數(shù)據(jù)存儲(chǔ)
glGenBuffers(3, m_VboIds); glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]); glBufferData(GL_ARRAY_BUFFER, sizeof(vVertices), vVertices, GL_STATIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[1]); glBufferData(GL_ARRAY_BUFFER, sizeof(vFboTexCoors), vTexCoors, GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_VboIds[2]); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); glGenVertexArrays(1, m_VaoIds); glBindVertexArray(m_VaoIds[0]);到這,整個(gè)OpenGL的初始化操作,差不多完成了,接下來(lái),我們就要去基于OpenGL去實(shí)現(xiàn)我們想要的模糊效果。 考慮到模糊的效果,那么我們需要給開發(fā)者提供模糊半徑blurRadius、模糊偏移量blurOffset、模糊的權(quán)重sumWeight。所以我們需要在我們模糊的片元著色器上,定義開發(fā)者輸入,其模糊的片元著色器代碼如下:
const char blurShaderStr[] = "#version 300 es " "precision highp float; " "uniform lowp sampler2D s_TextureMap; " "in vec2 v_texCoord; " "layout(location = 0) out vec4 outColor; " "uniform highp int blurRadius; " "uniform highp vec2 blurOffset; " " " "uniform highp float sumWeight; " "float PI = 3.1415926; " "float getWeight(int i) " "{ " "float sigma = float(blurRadius) / 3.0; " "return (1.0 / sqrt(2.0 * PI * sigma * sigma)) * exp(-float(i * i) / (2.0 * sigma * sigma)) / sumWeight; " "} " "vec2 clampCoordinate(vec2 coordinate) " "{ " " return vec2(clamp(coordinate.x, 0.0, 1.0), clamp(coordinate.y, 0.0, 1.0)); " "} " " " "void main() " "{ " "vec4 sourceColor = texture(s_TextureMap, v_texCoord); " "if (blurRadius <= 1) " "{ " "outColor = sourceColor; " "return; " "} " "float weight = getWeight(0); " "vec3 finalColor = sourceColor.rgb * weight; " "for (int i = 1; i < blurRadius; i++) " "{ " "weight = getWeight(i); " "finalColor += texture(s_TextureMap, clampCoordinate(v_texCoord - blurOffset * float(i))).rgb * weight; " "finalColor += texture(s_TextureMap, clampCoordinate(v_texCoord + blurOffset * float(i))).rgb * weight; " "} " "outColor = vec4(finalColor, sourceColor.a); " ????????????????????????"} ";里面的邏輯暫時(shí)就不介紹了,有興趣的朋友可以去研究研究。 通過上述的LoadShader函數(shù)將其片元著色器加載到GPU的運(yùn)行單元中去。
m_ProgramObj = GLUtils::CreateProgram(vShaderStr, blurShaderStr, m_VertexShader, m_FragmentShader); if (!m_ProgramObj) { GLUtils::CheckGLError("Create Program"); LOGI("gl--> EGLRender::SetIntParams Could not create program."); return; } m_SamplerLoc = glGetUniformLocation(m_ProgramObj, "s_TextureMap"); m_TexSizeLoc?=?glGetUniformLocation(m_ProgramObj,?"u_texSize");然后我們就需要將圖片的整個(gè)像素?cái)?shù)據(jù)傳入; 定義好ts層的方法:
setImageData(buf: ArrayBuffer, width: number, height: number) { if (!buf) { throw new Error("this pixelMap data is empty"); } if (width <= 0 || height <= 0) { throw new Error("this pixelMap of width and height is invalidation"); } this.width = width; this.height = height; this.ifNeedInit(); this.onReadySize(); this.setSurfaceFilterType(); this.render.native_EglRenderSetImageData(buf, width, height); };將ArrayBuffer數(shù)據(jù)傳入NAPI層。通過napi_get_arraybuffer_info NAPI獲取ArrayBuffer數(shù)據(jù)。
napi_value EGLRender::RenderSetData(napi_env env, napi_callback_info info) { .... void* buffer; size_t bufferLength; napi_status buffStatus= napi_get_arraybuffer_info(env,args[0],&buffer,&bufferLength); if (buffStatus != napi_ok) { return nullptr; } .... EGLRender::GetInstance()->SetImageData(uint8_buf, width, height); return nullptr; ?}將其數(shù)據(jù)綁定到OpenGL中的紋理中去
void EGLRender::SetImageData(uint8_t *pData, int width, int height){ if (pData && m_IsGLContextReady) { ... m_RenderImage.width = width; m_RenderImage.height = height; m_RenderImage.format = IMAGE_FORMAT_RGBA; NativeImageUtil::AllocNativeImage(&m_RenderImage); memcpy(m_RenderImage.ppPlane[0], pData, width*height*4); glBindTexture(GL_TEXTURE_2D, m_ImageTextureId); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_RenderImage.width, m_RenderImage.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, m_RenderImage.ppPlane[0]); glBindTexture(GL_TEXTURE_2D, GL_NONE); .... } }然后就是讓開發(fā)者自己定義模糊半徑及其模糊偏移量,通過OpenGL提供的
glUniform1i(location,(int)value); 設(shè)置int 片元著色器blurRadius變量 glUniform2f(location,value[0],value[1]);??設(shè)置float數(shù)組??片元著色器blurOffset變量將半徑及其偏移量設(shè)置到模糊的片元著色器上。 之后,通過GPU將其渲染
napi_value EGLRender::Rendering(napi_env env, napi_callback_info info){ // 渲染 glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0); glBindVertexArray(GL_NONE); glBindTexture(GL_TEXTURE_2D, GL_NONE); return nullptr; }最后,就剩下獲取圖片像素的ArrayBuffer數(shù)據(jù)了,通過glReadPixels讀取到指定區(qū)域內(nèi)的像素點(diǎn)了
glReadPixels(x,y,surfaceWidth,surfaceHeight,GL_RGBA,GL_UNSIGNED_BYTE,pixels);但是,在這里,因?yàn)镺penGL里面的坐標(biāo)系,在2D的思維空間上,與我們通常認(rèn)知的是倒立的,所以需要對(duì)像素點(diǎn)進(jìn)行處理,得到我們想要的像素點(diǎn)集
int totalLength= width * height * 4; int oneLineLength = width * 4; uint8_t* tmp = (uint8_t*)malloc(totalLength); memcpy(tmp, *buf, totalLength); memset(*buf,0,sizeof(uint8_t)*totalLength); for(int i = 0 ; i< height;i ++){ memcpy(*buf+oneLineLength*i, tmp+totalLength-oneLineLength*(i+1), oneLineLength); } ?free(tmp);最后在上層,通過系統(tǒng)提供的createPixelMap得到我們想要的圖片,也就是模糊的圖片。
getPixelMap(x: number, y: number, width: number, height: number): Promise綜上,本篇文章介紹了由單純的在JS中用正態(tài)分布公式操作像素點(diǎn)實(shí)現(xiàn)模糊效果,引出性能問題,最后到基于OpenGL實(shí)現(xiàn)模糊效果的優(yōu)化,最后性能上也從模糊一張大圖片要十幾秒提升到100ms內(nèi)。 ?{ ..... let that = this; return new Promise((resolve, rejects) => { that.onDraw(); let buf = this.render.native_EglBitmapFromGLSurface(x, y, width, height); if (!buf) { rejects(new Error("get pixelMap fail")) } else { let initOptions = { size: { width: width, height: height }, editable: true, } image.createPixelMap(buf, initOptions).then(p => { resolve(p); }).catch((e) => { rejects(e) }) } }) }
編輯:黃飛
?
評(píng)論