原文:https://mp.weixin.qq.com/s/vwXVj5vmAxDRG_jTk_8hPA,點選連結檢視更多技術內容。
現在市面上有很多APP,都或多或少對圖片有模糊上的設計,所以,圖片模糊效果到底怎麼實現的呢?
首先,我們來了解下模糊效果的對比
從視覺上,兩張圖片,有一張是模糊的,那麼,在實現圖片模糊效果之前,我們首先需要了解圖片模糊的本質是什麼?
在此介紹模糊本質之前,我們來了解下當前主流的兩個移動端平臺(Android與iOS)的實現。對Android開發者而言,比較熟悉且完善的圖片變換三方庫以glide-transformations(https://github.com/wasabeef/glide-transformations)為樣例,來看看它是基於什麼實現的。
Android中有兩種實現:
1、 FastBlur,根據stackBlur模糊演算法來操作圖片的畫素點實現效果,但效率低,已過時。
2、 RenderScript,這個是Google官方提供的,用來在Android上編寫一套高效能程式碼的語言,可以執行在CPU及其GPU上,效率較高。
而對iOS開發者而言,GPUImage(https://github.com/BradLarson/GPUImage/)比較主流。我們可以在其中看到高斯模糊過濾器(GPUImageGaussianBlurFilter),它裡面是根據OpenGL來實現,透過GLSL語言定義的著色器,操作GPU單元,達到模糊效果。
所以,我們可以看出,操作GPU來達到我們所需要的效果效率更高。因此我們在OpenHarmony上也能透過操作GPU,來實現我們想要的高效能模糊效果。
迴歸正題,先來了解下模糊的本質是什麼?
本質
模糊,可以理解為圖片中的每個畫素點都取其周邊畫素的平均值。
上圖M點的畫素點就是我們的焦點畫素。周圍ABCDEFGH都是M點(焦點)周圍的畫素點,那麼根據模糊的概念:M(rgb) =(A+B+C+D+E+F+G+H)/ 8
我們根據畫素點的r、g、b值,得到M點的畫素點值,就這樣,一個一個畫素點的操作,中間點相當於失去視覺上的焦點,整個圖片就產生模糊的效果。但這樣一邊倒的方式,在模糊的效果上,達不到需求的,所以,我們就需要根據這個模糊的本質概念,去想想,加一些東西或者更改取平均值的規則,完成我們想要的效果。故,高斯模糊,一個家喻戶曉的名字,就出現在我們面前。
高斯模糊
高斯模糊,運用了正態分佈函式,進行各個加權平均,正態分佈函式如下:
其中引數:μ為期望值,σ為標準差,當μ=0,σ=0的時候,為標準的正態分佈,其形狀參考如下圖:
可以看出:
其一,離中心點越近,分配的權重就越高。這樣我們在計算圖片的焦點畫素值時,將該點當作中心點,當作1的權重,其他周圍的點,按照該正態分佈的位置,去分配它的權重,這樣我們就可以根據該正態分佈函式及其各個點的畫素ARGB值,算出經過正態分佈之後的畫素ARGB值。
其二,離中心點越近,若是設定的模糊半徑很小,代表其模糊的焦點周圍的畫素點離焦點的畫素相差就不大,這樣模糊的效果就清晰。而模糊半徑越大,其周圍分佈的畫素色差就很大,這樣的模糊效果就越模糊。
透過圖片的寬高拿到每個畫素點的資料,再根據這個正態分佈公式,得到我們想要的畫素點的ARGB值,之後將處理過的畫素點重新寫入到圖片中,就能實現我們想要的圖片模糊效果。
流程
根據上面的闡述,就可以梳理出在OpenHarmony中的具體的實現流程:
● 獲取整張圖片的畫素點資料
● 迴圈圖片的寬高,獲取每個畫素點的焦點
● 在上述迴圈裡,根據焦點按照正態分佈公式進行加權平均,算出各個焦點周圍新的畫素值
● 將各個畫素點寫入圖片
關鍵依賴OpenHarmony系統基礎能力如下:
第一、獲取圖片的畫素點,系統有提供一次性獲取整張圖片的畫素點資料,其介面如下。
readPixelsToBuffer(dst: ArrayBuffer): Promise<void>;
readPixelsToBuffer(dst: ArrayBuffer, callback: AsyncCallback<void>): void;
可以看出,系統將獲取到畫素點資料ARGB值,儲存到ArrayBuffer中去。
第二、迴圈獲取每個畫素點,將其x、y點的畫素點當作焦點。
for (y = 0; y < imageHeight; y++) { for (x = 0; x < imageWidth; x++) { //...... 獲取當前的畫素焦點x、y }}
第三、迴圈獲取焦點周圍的畫素點(以焦點為原點,以設定的模糊半徑為半徑)。
for ( let m = centPointY-radius; m < centPointY+radius; m++) { for ( let n = centPointX-radius; n < centPointX+radius; n++) { //...... this.calculatedByNormality(...); //正態分佈公式化處理畫素點 //...... }}
第四、將各個圖片的畫素資料寫入圖片中。系統有提供一次性寫入畫素點,其介面如下。
writeBufferToPixels(src: ArrayBuffer): Promise;writeBufferToPixels(src: ArrayBuffer, callback: AsyncCallback): void;
透過上面的流程,我們可以在OpenHarmony系統下,獲取到經過正態分佈公式處理的畫素點,至此圖片模糊效果已經實現。
但是,經過測試發現,這個方式實現模糊化的過程,很耗時,達不到我們的效能要求。若是一張很大的圖片,就單單寬高迴圈來看,比如1920*1080寬高的圖片就要迴圈2,073,600次,非常耗時且對裝置的CPU也有非常大的消耗,因此我們還需要對其進行效能最佳化。
模糊效能最佳化思路
如上面所訴,考慮到OpenHarmony的環境的特點及其系統提供的能力,可以考慮如下幾個方面進行最佳化:
第一、參照社群已有成熟的圖片模糊演算法處理,如(Android的FastBlur)。
第二、C層效能要比JS層更好,將畫素點的資料處理,透過NAPI機制,將其放入C層處理。如:將其迴圈獲取焦點及其透過正態分佈公式處理的都放到C層中處理。
第三、基於系統底層提供的OpenGL,操作頂點著色器及片元著色器操作GPU,得到我們要的模糊效果。
首先,我們來根據Android中的FastBlur模糊化處理,參照其實現原理進行在基於OpenHarmony系統下實現的程式碼如下:
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<PixelEntry> = new Array()
var pix: Array<number> = 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<number>;
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);
}
從上面程式碼,可以看出,按照FastBlur的邏輯,還是逃不開上層去處理單個畫素點,逃不開圖片寬高的迴圈。經過測試也發現,在一張400*300的圖片上,完成圖片的模糊需要十幾秒,所以第一個最佳化方案,在js環境上是行不通的。
其次,將其畫素點處理,透過NAPI的機制,將畫素點資料ArrayBuffer傳入到C層,由於在C層也需要迴圈去處理每個畫素點,傳入大資料的ArrayBuffer時對系統的native的消耗嚴重。最後經過測試也發現,模糊的過程也很緩慢,達不到效能要求。
所以對比分析之後,最終的最佳化方案是採取系統底層提供的OpenGL,透過GPU去作業系統的圖形處理器,解放出CPU的能力。
基於OpenGL操作GPU來提升模糊效能
在進行基於OpenGL進行效能提升前,我們需要了解OpenGL中的頂點著色器(vertex shader)及其片元著色器(fragment shader)。著色器(shader)是執行在GPU上的最小單元,功能是將輸入轉換輸出且各個shader之間是不能通訊的,需要使用的開發語言GLSL。這裡就不介紹GLSL的語言規則了。
頂點著色器(vertex shader)
確定要畫圖片的各個頂點(如:三角形的角的頂點),注意:每個頂點執行一次。一旦最終位置已知,OpenGL將獲取可見的頂點集,並將它們組裝成點、線和三角形。且以逆時針繪製的。
片元著色器(fragment shader)
生成點、線或三角形的每個片元的最終顏色,並對每個fragment執行一次。fragment是單一顏色的小矩形區域,類似於計算機螢幕上的畫素,簡單的說,就是將頂點著色器形成的點、線或者三角形區域,新增顏色。
片元著色器的主要目的是告訴GPU每個片元的最終顏色應該是什麼。對於圖元(primitive)的每個fragment,片元著色器將被呼叫一次,因此如果一個三角形對映到10000個片元,那麼片元著色器將被呼叫10000次。
OpenGL簡單的繪製流程:
讀取頂點資訊 ----------> 執行頂點著色器 ----------> 圖元裝配----------> 執行片元著色器----------> 往幀緩衝區寫入----------> 螢幕上最終效果
簡單的說,就是根據頂點著色器形成的點、線、三角形形成的區域,由片元著色器對其著色,之後就將這些資料寫入幀緩衝區(Frame Buffer)的記憶體塊中,再由螢幕顯示這個緩衝區。
那模糊的效果怎麼來實現呢?
首先我們來定義我們的頂點著色器及其片元著色器。如下程式碼:頂點著色器:
const char vShaderStr[] =
"#version 300 es \n"
"layout(location = 0) in vec4 a_position; \n"
"layout(location = 1) in vec2 a_texCoord; \n"
"out vec2 v_texCoord; \n"
"void main() \n"
"{ \n"
" gl_Position = a_position; \n"
" v_texCoord = a_texCoord; \n"
"} \n";
片元著色器:
const char fShaderStr0[] =
"#version 300 es \n"
"precision mediump float; \n"
"in vec2 v_texCoord; \n"
"layout(location = 0) out vec4 outColor; \n"
"uniform sampler2D s_TextureMap; \n"
"void main() \n"
"{ \n"
" outColor = texture(s_TextureMap, v_texCoord); \n"
"}";
其中version代表OpenGL的版本,layout在GLSL中是用於著色器的輸入或者輸出,uniform為一致變數。在著色器執行期間一致變數的值是不變的,只能在全域性範圍進行宣告,gl_Position是OpenGL內建的變數(輸出屬性-變換後的頂點的位置,用於後面的固定的裁剪等操作。所有的頂點著色器都必須寫這個值),texture函式是openGL採用2D紋理繪製。然後,我們還需要定義好初始的頂點座標資料等;
//頂點座標
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
};
//正常紋理座標
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 紋理座標與正常紋理方向不同(上下映象)
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
};
下面就進行OpenGL的初始化操作,獲取display,用來建立EGLSurface的
m_eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
初始化 EGL 方法
eglInitialize(m_eglDisplay, &eglMajVers, &eglMinVers)
獲取 EGLConfig 物件,確定渲染表面的配置資訊
eglChooseConfig(m_eglDisplay, confAttr, &m_eglConf, 1, &numConfigs)
建立渲染表面 EGLSurface,使用 eglCreatePbufferSurface 建立螢幕外渲染區域
m_eglSurface = eglCreatePbufferSurface(m_eglDisplay, m_eglConf, surfaceAttr)
建立渲染上下文 EGLContext
m_eglCtx = eglCreateContext(m_eglDisplay, m_eglConf, EGL_NO_CONTEXT, ctxAttr);
繫結上下文
eglMakeCurrent(m_eglDisplay, m_eglSurface, m_eglSurface, m_eglCtx)
透過預設的頂點著色器與片元著色器,載入到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;
}
建立一個空的著色器程式物件program = glCreateProgram();
將著色器物件附加到program物件
glAttachShader(program, vertexShaderHandle);glAttachShader(program, fragShaderHandle);
連線一個program物件glLinkProgram(program);
建立並初始化緩衝區物件的資料儲存
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]);
到這,整個OpenGL的初始化操作,差不多完成了,接下來,我們就要去基於OpenGL去實現我們想要的模糊效果。
考慮到模糊的效果,那麼我們需要給開發者提供模糊半徑blurRadius、模糊偏移量blurOffset、模糊的權重sumWeight。所以我們需要在我們模糊的片元著色器上,定義開發者輸入,其模糊的片元著色器程式碼如下:
const char blurShaderStr[] =
"#version 300 es\n"
"precision highp float;\n"
"uniform lowp sampler2D s_TextureMap;\n"
"in vec2 v_texCoord;\n"
"layout(location = 0) out vec4 outColor;\n"
"uniform highp int blurRadius;\n"
"uniform highp vec2 blurOffset;\n"
"\n"
"uniform highp float sumWeight;\n"
"float PI = 3.1415926;\n"
"float getWeight(int i)\n"
"{\n"
"float sigma = float(blurRadius) / 3.0;\n"
"return (1.0 / sqrt(2.0 * PI * sigma * sigma)) * exp(-float(i * i) / (2.0 * sigma * sigma)) / sumWeight;\n"
"}\n"
"vec2 clampCoordinate(vec2 coordinate)\n"
"{\n"
" return vec2(clamp(coordinate.x, 0.0, 1.0), clamp(coordinate.y, 0.0, 1.0));\n"
"}\n"
"\n"
"void main()\n"
"{\n"
"vec4 sourceColor = texture(s_TextureMap, v_texCoord);\n"
"if (blurRadius <= 1)\n"
"{\n"
"outColor = sourceColor;\n"
"return;\n"
"}\n"
"float weight = getWeight(0);\n"
"vec3 finalColor = sourceColor.rgb * weight;\n"
"for (int i = 1; i < blurRadius; i++)\n"
"{\n"
"weight = getWeight(i);\n"
"finalColor += texture(s_TextureMap, clampCoordinate(v_texCoord - blurOffset * float(i))).rgb * weight;\n"
"finalColor += texture(s_TextureMap, clampCoordinate(v_texCoord + blurOffset * float(i))).rgb * weight;\n"
"}\n"
"outColor = vec4(finalColor, sourceColor.a);\n"
"}\n";
裡面的邏輯暫時就不介紹了,有興趣的朋友可以去研究研究。
透過上述的LoadShader函式將其片元著色器載入到GPU的執行單元中去。
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");
然後我們就需要將圖片的整個畫素資料傳入;
定義好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資料傳入NAPI層。透過napi_get_arraybuffer_info NAPI獲取ArrayBuffer資料。
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;
}
將其資料繫結到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);
....
}
}
然後就是讓開發者自己定義模糊半徑及其模糊偏移量,透過OpenGL提供的
glUniform1i(location,(int)value); 設定int 片元著色器blurRadius變數
glUniform2f(location,value[0],value[1]); 設定float陣列 片元著色器blurOffset變數
將半徑及其偏移量設定到模糊的片元著色器上。之後,透過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資料了,透過glReadPixels讀取到指定區域內的畫素點了
glReadPixels(x,y,surfaceWidth,surfaceHeight,GL_RGBA,GL_UNSIGNED_BYTE,pixels);
但是,在這裡,因為OpenGL裡面的座標系,在2D的思維空間上,與我們通常認知的是倒立的,所以需要對畫素點進行處理,得到我們想要的畫素點集
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);
最後在上層,透過系統提供的createPixelMap得到我們想要的圖片,也就是模糊的圖片。
getPixelMap(x: number, y: number, width: number, height: number): Promise<image.PixelMap>{
.....
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)
})
}
})
}
綜上,本篇文章介紹了由單純的在JS中用正態分佈公式操作畫素點實現模糊效果,引出效能問題,最後到基於OpenGL實現模糊效果的最佳化,最後效能上也從模糊一張大圖片要十幾秒提升到100ms內,文章就介紹到這了,歡迎有興趣的朋友,可以參考學習下,下面提供具體的專案原始碼地址。
專案地址:https://gitee.com/openharmony-tpc/ImageKnife/tree/master/gpu_...