OpenGL (一)OpenGL ES 繪制基礎
OpenGL (二)GLSurface 視頻錄制
OpenGL (三)濾鏡filter 應用
OpenGL (四)貼紙和磨皮理論
Open Graphics Library
圖形領域的工業(yè)標準雳殊,是一套與硬件無關的跨編程語言、跨平臺的俺孙、專業(yè)的圖形編程(軟件)接口。它用于二維聚谁、三維圖像搪搏,是一個功能強大,調(diào)用方便的底層圖形庫花盐。
OpenGL ES(OpenGL for Embedded Systems)
針對手機执虹、PDA和游戲主機等嵌入式設備而設計的OpenGL API 子集拓挥。
在Android SDK與NDK中均有提供OpenGL ES的類庫。所以我們可以借助Java袋励、C/C++來使用OpenGL
和面向?qū)ο蟛煌钠。琌penGL 是個狀態(tài)機,面向過程編程茬故。代碼量比較多盖灸,難度較大。但是現(xiàn)在流 行的各種美顏相機均牢、短視頻APP實現(xiàn)各種圖像渲染效果都需要OpenGL來完成糠雨。
圖像處理流程一般的方式如下才睹;
很多圖形處理的書都有這樣一個經(jīng)典的圖
抓了一張圖徘跪,還是說明一下,OpenGL管線琅攘,通俗 的說所有的圖形都是由三角行構(gòu)成垮庐,只要給定定點就好。
把三角形細分成像素點坞琴,每個像素點放大哨查,就是我們第三個Fragments。根據(jù)像素點的位置剧辐,用著色器上色寒亥。
最后精工Fragment處理邮府,行程圖像。
EGL
OpenGL ES只是圖形API溉奕,不負責管理(顯示)窗口褂傀,窗口的管理交由各個設備自己來完成。OpenGL ES調(diào)用用于渲染紋理多邊形加勤,而 EGL 調(diào)用用于將渲染放到屏幕上仙辟。
GLSurfaceView
調(diào)用任何 OpenGL 函數(shù)前,必須已經(jīng)創(chuàng)建了 OpenGL 上下文鳄梅。Android中GLSurfaceView中會為我們初始化OpenGL上下文叠国。
內(nèi)部啟動一個子線程(GL線 程)來初始化ELG環(huán)境,并完成OpenGL的繪制戴尸。使用GLSurfaceView粟焊,我們只能在這個EGL線程調(diào)用 OpenGL函數(shù)。
繼承至SurfaceView校赤,它內(nèi)嵌的Surface專門負責OpenGL渲染吆玖。
- 管理Surface與EGL;
- 允許自定義渲染器(render)马篮;
- 支持按需渲染和連續(xù)渲染沾乘;
GLThread定義在GLSurfaceView內(nèi)部,我們怎么在這個線程中去執(zhí)行我們編寫的代碼?
GLSurfaceView并不是一創(chuàng)建就會啟動GL線程初始化環(huán)境浑测。你可以把GLSurfaceView當成普通的 SurfaceView來使用翅阵。但是如果我們需要使用OpenGL ES在GLSurfaceView中繪制,那么我們需要調(diào) 用:
//使用OpenGL ES 2.0 context.
setEGLContextClientVersion(2);
//設置渲染回調(diào)接口
renderer = new CameraRender(this);
setRenderer(renderer);
/**
* 刷新方式:注意必須在setRenderer 后面設置
* RENDERMODE_WHEN_DIRTY 手動刷新迁央,調(diào)用requestRender()回調(diào)一次渲染器的onDraw方
* 法;
* RENDERMODE_CONTINUOUSLY 自動刷新掷匠,大概16ms自動回調(diào)一次渲染器的onDraw方法
*/
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
其中 CameraRender 是一個實現(xiàn)了 GLSurfaceView.Renderer 接口的類。這個接口定義為:
public interface Renderer {
//窗口畫布準備完成
void onSurfaceCreated(GL10 gl, EGLConfig config);
//畫布發(fā)生改變(如:橫豎切換)
void onSurfaceChanged(GL10 gl, int width, int height);
//繪制
void onDrawFrame(GL10 gl);
}
所謂的刷新就是回調(diào)這個接口的 onDrawFrame 方法讓我們進行OpenGL ES的繪制調(diào)用岖圈。其實 GLSurfaceView的setRenderer方法源碼實現(xiàn)為:
public void setRenderer(Renderer renderer) {
checkRenderThreadState();
if (mEGLConfigChooser == null) {
mEGLConfigChooser = new SimpleEGLConfigChooser(true);
}
if (mEGLContextFactory == null) {
mEGLContextFactory = new DefaultContextFactory();
}
if (mEGLWindowSurfaceFactory == null) {
mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory();
}
mRenderer = renderer;
mGLThread = new GLThread(mThisWeakRef);
mGLThread.start();
}
可以看到讹语,如果需要在GLSurfaceView中配置的ELG環(huán)境并使用OpenGL ES就必須設置渲染接口。這樣 才會啟動線程初始化OpenGL ES的環(huán)境蜂科。而渲染接口需要實現(xiàn)的方法就是在GLThread回調(diào)的顽决。
GLThread定義在GLSurfaceView內(nèi)部,我們怎么在這個線程中去執(zhí)行我們編寫的代碼?
渲染接口Renderer的三個回調(diào)方法就是在該線程中回調(diào)的导匣。
下面我們需要使用OpenGL ES在GLSurfaceView中顯示攝像頭的圖像才菠。 這里在之前的文章有說過
請轉(zhuǎn)到 RTMP(四)交叉編譯與CameraX 結(jié)尾有提到
Renderer渲染
前面我們說了,我們需要在GLThread中去調(diào)用OpenGL ES的方法來完成渲染贡定。因此我們的主要工作就 是在實現(xiàn)了Renderer接口的類中我們需要實現(xiàn)的: onSurfaceCreated 赋访、 onSurfaceChanged 與 onDrawFrame 三個方法中完成繪制。
onSurfaceCreated
在OpenGL ES環(huán)境準備完成之后,會在GLThread中回調(diào) onSurfaceCreated 方法蚓耽。在這個方法中我們 需要準備我們需要繪制的圖像渠牲。
OpenGL紋理可以看成是能在OpenGL中使用的圖像,在代碼中我們通過id來操作紋理步悠。
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//創(chuàng)建OpenGL 紋理 ,把攝像頭的數(shù)據(jù)與這個紋理關聯(lián)
textures = new int[1]; //當做能在opengl用的一個圖片的ID
mCameraTexure.attachToGLContext(textures[0]);
// 當攝像頭數(shù)據(jù)有更新回調(diào) onFrameAvailable
mCameraTexure.setOnFrameAvailableListener(this);
screenFilter = new ScreenFilter(cameraView.getContext());
}
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
// 請求執(zhí)行一次 onDrawFrame
cameraView.requestRender();
}
讓攝像頭數(shù)據(jù)綁定到我們創(chuàng)建的紋理 textures[0] 之后嘱兼,后續(xù)我們會將 textures[0] 交給OpenGL ES 的Sharder著色器去進行渲染,并且在渲染時也能去實現(xiàn)各種圖像效果贤徒。
OpenGL Sharder(著色器)需要使用GLSL(著色器語言)編寫
上述代碼中的 mCameraTexure 就是攝像頭預覽數(shù)據(jù)繪制的窗口芹壕。 attachToGLContext 方法能夠讓攝像
頭預覽的窗口與我們的紋理綁定起來。
攝像頭捕獲
由于我們本次會使用CameraX來完成攝像機的使用接奈,使用CameraX踢涌,將捕獲的圖像與OpenGL紋理關聯(lián) 起來也非常簡單。再來回顧下我們之前直接通過CameraX與TextureView顯示預覽的代碼序宦。
// 預覽配置
private Preview getPreView() {
// 分辨率并不是最終的分辨率睁壁,CameraX會自動根據(jù)設備的支持情況,結(jié)合你的參數(shù)互捌,設置一個最為接近的分辨率
PreviewConfig previewConfig = new PreviewConfig.Builder()
.setTargetResolution(new Size(640, 480))
.setLensFacing(currentFacing) //前置或者后置攝像頭
.build();
Preview preview = new Preview(previewConfig);
preview.setOnPreviewOutputUpdateListener(listener);
return preview;
}
// 綁定聲明周期
CameraX.bindToLifecycle(lifecycleOwner, getPreView());
@Override
public void onUpdated(Preview.PreviewOutput output) {
mCameraTexure = output.getSurfaceTexture();
// if (mCameraV.getSurfaceTexture() != surfaceTexture) {
// if (textureView.isAvailable()) {
// ViewGroup parent = (ViewGroup) displayer.getParent();
// parent.removeView(displayer);
// parent.addView(displayer, 0);
// parent.requestLayout(); }
// //設置布局中TextureView中的紋理畫布完成預覽
// textureView.setSurfaceTexture(mCameraTexure); }
// }
}
著色器
著色器(Shader)是運行在GPU上的小程序潘明。
頂點著色器(vertex shader)
如何處理頂點、法線等數(shù)據(jù)的小程序秕噪。
#version 120
attribute vec4 vPosition;//變量 float[4] 一個頂點 java 傳過來
attribute vec2 vCoord;//傳給片元進行采樣的紋理坐標
varying vec2 aCoord;//易變變量 和片元寫的一模一樣 會傳給片元
void main() {
// gl_Position = vec4(vec3(0.0), 1.0);
// 內(nèi)置變量:把坐標點賦值給gl_position就ok
gl_Position = vPosition;
aCoord = vCoord;
}
這段程序就是一個頂點著色器的代碼钳降。與Java或者C一樣,程序入口就是 main 方法腌巾。在 main 方法中只 要我們把要畫的物體形狀的點坐標給到 gl_position 即可遂填。 vPosition 就是我們在CPU中確定的物體 形狀坐標點。現(xiàn)在我們需要繪制的是攝像頭采集圖像澈蝙,也就是矩形吓坚,那就需要傳遞4個頂點坐標數(shù)據(jù)到 著色器中。
與我們熟悉的Java語法類似灯荧, vPosition 是變量名礁击, vec4 是類型,而應用程序傳遞到頂點著色器中的 變量值需要使用 attribute 修飾逗载《吡可以看成是 in 頂點著色器輸入變量的修飾。
vec4 其實就是vector向量撕贞,4表示這個向量包含4個數(shù)據(jù)更耻。需要注意的是vec4只能存儲float數(shù)據(jù)测垛, 如果需要存儲整型則需要使用ivec4捏膨。
aCoord 定義為存儲2個float的vec2類型,被varying修飾。如果說 attribute 是 in 輸入變量的修飾号涯, 那么 varying 就是 out 輸出變量目胡。我們會在片元著色器使用這個變量。
這個頂點著色器中沒有任何邏輯链快,只進行了賦值的操作誉己,那么作為 attribute 輸入的值 vPosition 與 vCoord 就需要我們在Android中傳遞進來。這兩個值分別為頂點坐標與紋理坐標域蜗。
我們需要根據(jù)OpenGL世界坐標系傳遞頂點坐標點來確定繪制的幾何形狀巨双,需要在繪制的幾何表面進行 貼圖,就需要按照紋理坐標貼合幾何頂點霉祸。頂點著色器執(zhí)行4次筑累, vPostion 接收的點坐標與 vCoord 分 別為:
-1.0,-1.0 與 0,0
1.0,-1.0 與 1.0,0
-1.0,1.0 與 0,1.0
1.0,1.0 與 1.0,1.0
和U3D 坐標系類似
片元著色器(fragment shader)
如何處理光、陰影丝蹭、遮擋慢宗、環(huán)境等等對物體表面的影響,最終生成一副圖像的小程序奔穿。
#version 120
precision mediump float;//數(shù)據(jù)精度
varying vec2 aCoord;
// uniform 片元著色器必須用這個
uniform samplerExternalOES vTexture;//samplerExternalOES 圖片镜沽,采樣器
void main() {
// gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
gl_FragColor = texure2D(vTexture,aCoord);//rgba
}
片元著色器中,使用內(nèi)置函數(shù) texture2D 采集對應坐標點的像素贱田,賦值給 gl_FragColor 即可缅茉。
#extension GL_OES_EGL_image_external : require :Android攝像頭只能用samplerExternalOES 類型的紋理去接收攝像頭的畫面,而使用samplerExternalOES需要開啟 GL_OES_EGL_image_external功能男摧。
uniform 變量是Android中需要傳遞給著色器的變量宾舅,不能被著色器修改〔室校可以用于修飾共享變 量(在頂點和片元中聲明方式完全一樣)筹我。
aCoord 的定義需要和頂點著色器一模一樣,但是在片元著色器能讀取之前會通過光柵化傳遞帆离, 光柵化程序在三角形的三個頂點之間進行插值處理蔬蕊,會訪問三個頂點之間每一個像素,然后對每 個像素點執(zhí)行片元著色器哥谷。所以在片元著色器中 aCoord 的值可以看成幾何中每一個像素點的坐 標岸夯。
texture2D(vTexture,aCoord) ,則是利用攝像頭紋理的采樣器 vTexture 獲取 aCoord 坐標的 像素RGBA值并賦值給 gl_FragColor 们妥,OpenGL就知道當前處理的片元是什么顏色從而繪制猜扮。
實際上,在Android中我們就是通過OpenGL接口給著色器傳參就好啦监婶。我們可以在渲染接口的 onDraw 中調(diào)用OpenGL動態(tài)的給著色器傳遞參數(shù)旅赢。而各種繪制效果的處理在著色器中來完成齿桃。
GLSL
OpenGL著色語言(OpenGL Shading Language)
attribute
屬性變量,頂點著色器輸入數(shù)據(jù)煮盼。如:頂點坐標短纵、紋理坐標、顏色等僵控。
uniforms
一致變量香到。在著色器執(zhí)行期間一致變量的值是不變的。類似常量报破,但不同的是悠就,這個值在編譯時期是未知的,由著色器外部初始化充易。
varying
易變變量理卑,頂點著色器輸出數(shù)據(jù)。是從頂點著色器傳遞到片元著色器的數(shù)據(jù)變量