在完成了OpenGL ES和ARCore基本的實現(xiàn)后,下一步肯定就是圖像繪制了。在Google為ARCore提供的Demo中,主要進(jìn)行了相機(jī)預(yù)覽、檢測點(diǎn)云、檢測平面和Android小機(jī)器人的繪制。本節(jié)我們將跟大家介紹相機(jī)預(yù)覽背景的繪制過程和原理。
一、OpenGL ES渲染管線
OpenGL ES渲染管線其實就是渲染流水線,實質(zhì)上指的是一系列的繪制過程。這些過程輸入的是待渲染3D物理的相關(guān)描述信息數(shù)據(jù),經(jīng)過渲染管線,輸出一幀想要的圖像。
OpenGL ES渲染管線,如下圖所示:
1.基本處理
在該階段設(shè)定3D空間中物體的頂點(diǎn)坐標(biāo)、頂點(diǎn)對應(yīng)的顏色、頂點(diǎn)紋理坐標(biāo)等屬性,并指定繪制方式,如點(diǎn)繪制、線段繪制或者三角形繪制等。
OpenGL ES支持2D和3D圖形的繪制。當(dāng)你希望在2D界面繪制一條任意的曲線的時候,把這邊曲線放的足夠大來看,會發(fā)現(xiàn)這條曲線其實是由許多足夠短的直線連接起來的。那么繪制3D圖形,即使你看見的是一個“圓滑曲面”的3D圖形,實際上足夠放大它依然是由多個小平面組成的。所以對于2D圖形,可能是由很多“曲線”組成;而3D圖形可能是由很多“曲面”組成。
2.頂點(diǎn)緩沖對象
該階段在應(yīng)用程序中是可選的。在整個場景中頂點(diǎn)的基本數(shù)據(jù)不變的情況。可以在初始化階段將頂點(diǎn)數(shù)據(jù)經(jīng)過基本處理后送入頂點(diǎn)緩沖對象,在繪制每一幀想要的圖像就省去了頂點(diǎn)IO的麻煩,直接從頂點(diǎn)緩沖對象中獲取頂點(diǎn)數(shù)據(jù)即可。
3.頂點(diǎn)著色器
頂點(diǎn)著色器是一個可編程處理單元,為執(zhí)行頂點(diǎn)變換、光照、材質(zhì)的應(yīng)用與計算等頂點(diǎn)相關(guān)的操作,每個頂點(diǎn)執(zhí)行一次。工作過程首先將原始的幾何信息及其他屬性傳送到頂點(diǎn)著色器中,經(jīng)過自己開發(fā)的頂點(diǎn)著色器處理后產(chǎn)生紋理坐標(biāo)、顏色、點(diǎn)位置等后流程需要的各項頂點(diǎn)屬性信息,然后將其傳遞給圖元裝配階段。
頂點(diǎn)著色器輸入主要為待處理頂點(diǎn)相應(yīng)的attribute(屬性)變量、uniform(一致)變量、采樣器以及臨時變量;輸出主要為經(jīng)過頂點(diǎn)著色器后生成的varying(易變)變量以及一些內(nèi)建輸出變量。
4.圖元裝配
該階段主要任務(wù)是圖元組裝和圖元處理。
圖元組裝是指頂點(diǎn)數(shù)據(jù)根據(jù)設(shè)置的繪制方式被結(jié)合成完整的圖元。如點(diǎn)繪制每個頂點(diǎn)為一個圖元;線段繪制方式每個圖元則為兩個頂點(diǎn);三角形繪制方式下需要3個頂點(diǎn)構(gòu)造成一個圖元。
圖元處理最重要的工作是剪裁,其任務(wù)是消除位于半空間之外的部分幾何圖元。之所以進(jìn)行剪裁,是因為隨著觀察位置、角度不同,并不能總看到特定3D物體某個圖元的全部。剪裁時,若圖元完全位置視景體以及自定義裁剪平面的內(nèi)部,則將圖元傳遞到后面步驟進(jìn)行處理;如果其完全位于視景體或者自定義剪裁平面的外部,則丟棄該圖元。
5.光柵化
虛擬3D世界中的物體的幾何信息一般采用連續(xù)的數(shù)學(xué)向量來表示,因此投影的平面結(jié)果也是用連續(xù)的數(shù)學(xué)向量表示的。但目前的顯示設(shè)備都是離散化的(由一個一個的像素組成),因此還需要將投影的結(jié)果離散化。將其分解為一個一個離散化的小單元。
6.片元著色器
片元著色器是用于處理片元值以及相關(guān)數(shù)據(jù)的可編程單元,其可以執(zhí)行紋理的采樣、顏色的匯總、計算霧顏色等操作,每片元執(zhí)行一次。片元著色器通過重復(fù)執(zhí)行,將3D物體中的圖元光柵化后產(chǎn)生的每個片元的顏色等屬性計算出來后送入后續(xù)階段,如剪裁測試、深度測試及模板測試等。
片元著色器輸入是從頂點(diǎn)著色器傳遞到片元著色器的易變變量(Varying0~n),輸出為內(nèi)建變量(gl_FragColor)是片元的最終顏色。
7.剪裁測試
OpenGL ES會檢查每個片元在幀緩沖中對應(yīng)的位置,若對應(yīng)位置在剪裁窗口中則將此片元送入下一階段,否則丟棄此片元。
8.深度測試和模板測試
深度測試指將輸入片元的深度值與緩沖區(qū)中存儲的對應(yīng)位置的深度值進(jìn)行比較,若輸入片元的深度值小于則將輸入片元送入下一個階段準(zhǔn)備覆蓋幀緩沖中的原片元或幀緩沖中的原片元緩沖,否則丟棄輸入片元。
模板測試的主要功能是為將繪制區(qū)域限定在一定范圍內(nèi),一般應(yīng)用在湖面倒影、鏡像等場合。
9.顏色緩沖混合
若程序開啟的Alpha混合,則根據(jù)混合因子將上一階段送來的片元幀緩沖對應(yīng)的位置的片元進(jìn)行Alpha混合,否則送入的片元將覆蓋幀緩沖中對應(yīng)位置的片元。
10.抖動
抖動是一種簡單的操作,允許只使用少量的顏色模擬出更寬的顏色顯示范圍,從而使顏色視覺更豐富。
11.幀緩沖
OpenGL ES中的物體繪制并不是直接繪制在屏幕上進(jìn)行的,而是預(yù)先在幀緩沖區(qū)中進(jìn)行繪制,每繪制完一幀再將繪制的結(jié)果交換到屏幕上。因此,在每次繪制更新一幀都需要清除緩沖區(qū)中的相關(guān)數(shù)據(jù)。
二、紋理映射
除了基本基本圖形的繪制,如果想要繪制更加真實、炫酷的3D物體,就需要用到紋理映射。它就是把一幅紋理應(yīng)用到相應(yīng)的幾何圖元,告知渲染系統(tǒng)如何進(jìn)行紋理映射。告知的方式就是為圖元中的每個頂點(diǎn)指定恰當(dāng)紋理坐標(biāo),然后通過紋理坐標(biāo)在紋理圖中可以確定選中的紋理區(qū)域,最后將選中的紋理區(qū)域中的內(nèi)容根據(jù)紋理坐標(biāo)映射到指定的圖元上。
三 、相機(jī)預(yù)覽背景繪制步驟
1.繪制背景準(zhǔn)備
在該過程,完成了頂點(diǎn)和紋理坐標(biāo)數(shù)據(jù)的提供,紋理id的生成獲取、綁定和設(shè)置,創(chuàng)建頂點(diǎn)坐標(biāo)和紋理坐標(biāo)頂點(diǎn)緩沖對象,加載并綁定背景頂點(diǎn)和片元著色器,獲取頂點(diǎn)和紋理坐標(biāo)位置屬性等。
2.背景紋理和Session攝像頭紋理綁定
在該過程中,將背景紋理id和Session攝像頭紋理id進(jìn)行綁定。
3.執(zhí)行背景繪制
該階段處理顯示旋轉(zhuǎn)紋理坐標(biāo)的變換,設(shè)置深度測試,告知OpenGL應(yīng)用的紋理id和使用的程序,將頂點(diǎn)和紋理坐標(biāo)傳送渲染管線,啟動頂點(diǎn)和紋理id數(shù)據(jù),執(zhí)行背景繪制和禁用頂點(diǎn)數(shù)據(jù)等。
四、案例源碼分析
1.繪制背景準(zhǔn)備
com\google\ar\core\examples\java\helloar\rendering\BackgroundRender.java
public class BackgroundRenderer {
private static final String TAG = BackgroundRenderer.class.getSimpleName();
private static final int COORDS_PER_VERTEX = 3;
private static final int TEXCOORDS_PER_VERTEX = 2;
private static final int FLOAT_SIZE = 4;
//頂點(diǎn)坐標(biāo)數(shù)據(jù)
public static final float[] QUAD_COORDS = new float[]{
-1.0f, -1.0f, 0.0f, //左下頂點(diǎn)
-1.0f, +1.0f, 0.0f, //左上頂點(diǎn)
+1.0f, -1.0f, 0.0f, //右下頂點(diǎn)
+1.0f, +1.0f, 0.0f, //右上頂點(diǎn)
};
//紋理坐標(biāo)數(shù)據(jù)
public static final float[] QUAD_TEXCOORDS = new float[]{
0.0f, 1.0f,
0.0f, 0.0f,
1.0f, 1.0f,
1.0f, 0.0f,
};
//頂點(diǎn)坐標(biāo),紋理坐標(biāo)和頂點(diǎn)變換數(shù)據(jù)內(nèi)存
private FloatBuffer mQuadVertices;
private FloatBuffer mQuadTexCoord;
private FloatBuffer mQuadTexCoordTransformed;
//背景著色器程序
private int mQuadProgram;
//背景繪制頂點(diǎn)和紋理屬性
private int mQuadPositionParam;
private int mQuadTexCoordParam;
//背景紋理渲染id
private int mTextureId = -1;
private int mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES;
public BackgroundRenderer() {
}
... ...
/**
* 分配和初始化背景渲染器的需要的OpenGL資源。必須在OpenGL線程中調(diào)用,通常在GLSurfaceView.Render
* (GL10,EGLConfig)
*/
public void createOnGlThread(Context context) {
//生成背景紋理
int textures[] = new int[1];
GLES20.glGenTextures(1, textures, 0);
//獲取加載圖像后OpenGL紋理id
mTextureId = textures[0];
//告訴OpenGL后面的紋理調(diào)用應(yīng)該應(yīng)用mTextureId這個紋理對象
GLES20.glBindTexture(mTextureTarget, mTextureId);
//當(dāng)紋理大小被擴(kuò)大或者縮小的時候,我們就會使用到紋理過濾
//為mTextureTarget紋理設(shè)置屬性,紋理放大和縮小的濾波方式,橫向和縱向平鋪方式
GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
int numVertices = 4;
if (numVertices != QUAD_COORDS.length / COORDS_PER_VERTEX) {
throw new RuntimeException("Unexpected number of vertices in BackgroundRenderer.");
}
//OpenGL作為本地系統(tǒng)直接運(yùn)行在硬件上,沒有虛擬機(jī)。所以需要把Java虛擬機(jī)中的內(nèi)存復(fù)制到本地堆中
//分配一塊本地內(nèi)存bbVertices,告訴緩沖區(qū)按照本地字節(jié)組織它的內(nèi)容
ByteBuffer bbVertices = ByteBuffer.allocateDirect(QUAD_COORDS.length * FLOAT_SIZE);
bbVertices.order(ByteOrder.nativeOrder());
//獲取一個可以反映底層字節(jié)的FloatBuffer類實例,把頂點(diǎn)QUAD_COORDS數(shù)據(jù)放到本地內(nèi)存中
mQuadVertices = bbVertices.asFloatBuffer();
mQuadVertices.put(QUAD_COORDS);
mQuadVertices.position(0);
//同上
ByteBuffer bbTexCoords = ByteBuffer.allocateDirect(numVertices * TEXCOORDS_PER_VERTEX * FLOAT_SIZE);
bbTexCoords.order(ByteOrder.nativeOrder());
mQuadTexCoord = bbTexCoords.asFloatBuffer();
mQuadTexCoord.put(QUAD_TEXCOORDS);
mQuadTexCoord.position(0);
//同上
ByteBuffer bbTexCoordsTransformed = ByteBuffer.allocateDirect(numVertices * TEXCOORDS_PER_VERTEX * FLOAT_SIZE);
bbTexCoordsTransformed.order(ByteOrder.nativeOrder());
mQuadTexCoordTransformed = bbTexCoordsTransformed.asFloatBuffer();
//加載背景頂點(diǎn)和片元著色器
int vertexShader = ShaderUtil.loadGLShader(TAG, context,GLES20.GL_VERTEX_SHADER, R.raw.screenquad_vertex);
int fragmentShader = ShaderUtil.loadGLShader(TAG, context,GLES20.GL_FRAGMENT_SHADER, R.raw.screenquad_fragment_oes);
//把頂點(diǎn)和Fragment著色器綁定放在一個單個的程序中一起工作:
//新建程序?qū)ο髆QuadProgram,并把vertexShader和fragmentShader附加到程序中
mQuadProgram = GLES20.glCreateProgram();
GLES20.glAttachShader(mQuadProgram, vertexShader);
GLES20.glAttachShader(mQuadProgram, fragmentShader);
//把這些著色器聯(lián)合起來,并且告訴OpenGL在繪制背景的時候使用這個定義的程序
GLES20.glLinkProgram(mQuadProgram);
GLES20.glUseProgram(mQuadProgram);
//獲取頂點(diǎn)位置紋理坐標(biāo)屬性
ShaderUtil.checkGLError(TAG, "Program creation");
mQuadPositionParam = GLES20.glGetAttribLocation(mQuadProgram, "a_Position");
mQuadTexCoordParam = GLES20.glGetAttribLocation(mQuadProgram, "a_TexCoord");
ShaderUtil.checkGLError(TAG, "Program parameters");
}
... ...
}
com\google\ar\core\examples\java\helloar\rendering\ShaderUtil.java
public class ShaderUtil {
/**
* 將原始文本文件(保存為資源)轉(zhuǎn)換為OpenGL ES著色程序。
*/
public static int loadGLShader(String tag, Context context, int type, int resId) {
//通過著色器代碼原始資源文件id,讀取代碼為字符串
String code = readRawTextFile(context, resId);
//創(chuàng)建一個類型的著色器對象
int shader = GLES20.glCreateShader(type);
//把著色器源代碼上傳上著色器對象里,它與shader引用的著色器對象關(guān)聯(lián)起來
GLES20.glShaderSource(shader, code);
//編譯著色器
GLES20.glCompileShader(shader);
//取出編譯狀態(tài),檢查OpenGL是否能成功地編譯這個著色器,如果編譯失敗則刪除著色器
final int[] compileStatus = new int[1];
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
if (compileStatus[0] == 0) {
Log.e(tag, "Error compiling shader: " + GLES20.glGetShaderInfoLog(shader));
GLES20.glDeleteShader(shader);
shader = 0;
}
//編譯成功,返回著色器引用id
if (shader == 0) {
throw new RuntimeException("Error creating shader.");
}
return shader;
}
/**
* 將原始文本文件轉(zhuǎn)換為字符串:從原始資源中通過流讀取字符串
*/
private static String readRawTextFile(Context context, int resId) {
InputStream inputStream = context.getResources().openRawResource(resId);
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\n");
}
reader.close();
return sb.toString();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
java_arcore_hello_ar\app\src\main\res\raw\screenquad_vertex.shader
attribute vec4 a_Position; //3D物理每個頂點(diǎn)各自不同的頂點(diǎn)位置信息
attribute vec2 a_TexCoord; //3D物理每個頂點(diǎn)各自不同的紋理坐標(biāo)信息
varying vec2 v_TexCoord; //從頂點(diǎn)著色器計算產(chǎn)生并傳遞到片元著色器的紋理坐標(biāo)信息
void main() {
gl_Position = a_Position; //頂點(diǎn)著色器從渲染管線中獲得原始頂點(diǎn)信息a_Position,寫入gl_Position內(nèi)建變量傳遞到渲染管線的后階段繼續(xù)處理
v_TexCoord = a_TexCoord; //頂點(diǎn)著色器從渲染管線中獲得紋理坐標(biāo)信息a_TexCoord,傳遞給片元著色器繼續(xù)處理
}
java_arcore_hello_ar\app\src\main\res\raw\screenquad_fragment_oes.shader
precision mediump float; //給出默認(rèn)的浮點(diǎn)精度
varying vec2 v_TexCoord; //從頂點(diǎn)著色器傳來的紋理坐標(biāo)
uniform samplerExternalOES sTexture; //紋理內(nèi)容數(shù)據(jù)
void main() {
gl_FragColor = texture2D(sTexture, v_TexCoord); //根據(jù)紋理坐標(biāo)采樣出顏色值
}
2.背景紋理和Session攝像頭紋理綁定
com\google\ar\core\examples\java\helloar\HelloArActivity.java
public class HelloArActivity extends AppCompatActivity implements GLSurfaceView.Renderer {
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//OpenGL環(huán)境被設(shè)置的時候,調(diào)用一次
//設(shè)置背景幀的顏色
GLES20.glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
//準(zhǔn)備背景渲染對象
mBackgroundRenderer.createOnGlThread(this);
//創(chuàng)建紋理,并且將它傳遞給ARCore Session,在update()的時候填充,允許GPU訪問相機(jī)圖像
mSession.setCameraTextureName(mBackgroundRenderer.getTextureId());
}
}
com\google\ar\core\examples\java\helloar\rendering\BackgroundRender.java
public class BackgroundRenderer {
... ...
//背景紋理id
private int mTextureId = -1;
private int mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES;
... ...
//獲取背景紋理id
public int getTextureId() {
return mTextureId;
}
... ...
}
3.執(zhí)行背景繪制
com\google\ar\core\examples\java\helloar\rendering\BackgroundRender.java
public class BackgroundRenderer {
private static final String TAG = BackgroundRenderer.class.getSimpleName();
private static final int COORDS_PER_VERTEX = 3;
private static final int TEXCOORDS_PER_VERTEX = 2;
private static final int FLOAT_SIZE = 4;
private FloatBuffer mQuadVertices;
private FloatBuffer mQuadTexCoord;
private FloatBuffer mQuadTexCoordTransformed;
private int mQuadProgram;
private int mQuadPositionParam;
private int mQuadTexCoordParam;
private int mTextureId = -1;
private int mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES;
/**
* 繪制AR背景圖片。這個圖片將會使用Frame.getViewMatrix(float[], int)和Session.getProjectionMatrix(
* float[], int, float, float)提供的矩陣?yán)L制,將準(zhǔn)確的跟蹤靜態(tài)物理對象,它必須在繪制虛擬對象之前調(diào)用
* @param frame 通過Session.update()返回的最新Frame
*/
public void draw(Frame frame) {
//如果顯示旋轉(zhuǎn)(也包括尺寸的改變),我們需要重新查詢屏幕rect的uv坐標(biāo),因為它們可能發(fā)生了變化
if (frame.isDisplayRotationChanged()) {
frame.transformDisplayUvCoords(mQuadTexCoord, mQuadTexCoordTransformed);
}
//不需要測試或者寫入深度,屏幕頂點(diǎn)具有任意深度,預(yù)計將首先繪制
GLES20.glDisable(GLES20.GL_DEPTH_TEST);
GLES20.glDepthMask(false);
//告訴OpenGL紋理調(diào)用應(yīng)該應(yīng)用mTextureId這個紋理對象
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureId);
//告訴OpenGL在繪制背景的時候使用這個定義的程序
GLES20.glUseProgram(mQuadProgram);
//將頂點(diǎn)位置數(shù)據(jù)傳送進(jìn)渲染管線
GLES20.glVertexAttribPointer(mQuadPositionParam, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, mQuadVertices);
//將頂點(diǎn)紋理坐標(biāo)數(shù)據(jù)傳送進(jìn)渲染管線
GLES20.glVertexAttribPointer(mQuadTexCoordParam, TEXCOORDS_PER_VERTEX,GLES20.GL_FLOAT, false, 0, mQuadTexCoordTransformed);
//啟動頂點(diǎn)位置和著色數(shù)據(jù)
GLES20.glEnableVertexAttribArray(mQuadPositionParam);
GLES20.glEnableVertexAttribArray(mQuadTexCoordParam);
//執(zhí)行背景繪制
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
//禁用頂點(diǎn)數(shù)組
GLES20.glDisableVertexAttribArray(mQuadPositionParam);
GLES20.glDisableVertexAttribArray(mQuadTexCoordParam);
//恢復(fù)深度狀態(tài)以作進(jìn)一步繪圖。
GLES20.glDepthMask(true);
GLES20.glEnable(GLES20.GL_DEPTH_TEST);
ShaderUtil.checkGLError(TAG, "Draw");
}
}
1.新技術(shù),新未來!盡在1024工場。時刻關(guān)注最前沿技術(shù)資訊,發(fā)布最棒技術(shù)博文!(甭客氣!盡情的掃描或者長按?。?/em>
2.完整和持續(xù)更新的《使用Android打開AR的開發(fā)大門—ARCore》文檔,歡迎大家閱讀!
https://www.kancloud.cn/p3243986735/arcore_develop/457951