首先申明下,本文為筆者學習《OpenGL ES應用開發實踐指南(Android捲)》的筆記,涉及的代碼均出自原書,如有需要,請到原書指定源碼地址下載。 《OpenGL ES學習筆記(二)——平滑著色、自適應寬高及三維圖像生成》中闡述的平滑著色、自適應寬高是為了實現在移動端模擬真實場景採用的方法,並且...
首先申明下,本文為筆者學習《OpenGL ES應用開發實踐指南(Android捲)》的筆記,涉及的代碼均出自原書,如有需要,請到原書指定源碼地址下載。
《OpenGL ES學習筆記(二)——平滑著色、自適應寬高及三維圖像生成》中闡述的平滑著色、自適應寬高是為了實現在移動端模擬真實場景採用的方法,並且通過w分量增加了三維視角,在具體實現上採用了正交投影、透視投影的理論。本文將在此基礎上,構建更加精美的三維場景。三維效果本質上是點、直線和三角形的組合,紋理是將圖像或者照片覆蓋到物體錶面,形成精美的細節。在實現上具體分為兩步:1)將紋理圖片載入進OpenGL;2)OpenGL將其顯示到物體錶面。(有點像把大象裝進冰箱分幾步~~~)不過,在實現過程中,涉及到著色器程式的管理,涉及到不同的紋理過濾模式,涉及到頂點數據新的類結構等問題,下麵將一一對其闡述:
- 紋理載入
- 紋理著色器
- 更新頂點數據類結構
- 著色器程式類
- 紋理繪製
一、紋理載入
將紋理覆蓋到物體錶面,最終是通對齊坐標來實現的。而OpenGL中二維紋理的坐標與電腦圖像的坐標並不一致,因此,首先對比下兩者的不同。
可見,兩者的差別在於繞橫軸翻轉180度。另外,OpenGL ES支持的紋理不必是正方形,但每個維度都必須是2的冪。
載入紋理圖片的方法參數列表應該包括Android上下文(Context)和資源ID,返回值應該是OpenGL紋理的ID,因此,該方法申明如下:
public static int loadTexture(Context context, int resourceId) {}
首先,創建一個紋理對象,與普通OpenGL對象生成模式一樣。生成成功之後,申明紋理調用應該應用於這個紋理對象。其次,載入點陣圖數據,OpenGL讀入點陣圖數據並複製到前面綁定的紋理對象。
final int[] textureObjectIds = new int[1]; glGenTextures(1, textureObjectIds, 0); if (textureObjectIds[0] == 0) { if (LoggerConfig.ON) { Log.w(TAG, "Could not generate a new OpenGL texture object."); } return 0; }
final BitmapFactory.Options options = new BitmapFactory.Options(); options.inScaled = false; // Read in the resource final Bitmap bitmap = BitmapFactory.decodeResource( context.getResources(), resourceId, options); if (bitmap == null) { if (LoggerConfig.ON) { Log.w(TAG, "Resource ID " + resourceId + " could not be decoded."); } glDeleteTextures(1, textureObjectIds, 0); return 0; } // Bind to the texture in OpenGL glBindTexture(GL_TEXTURE_2D, textureObjectIds[0]);
這兩段代碼需要說明的並不多,其中options.inScaled = false表明OpenGL讀入圖像的非壓縮形式的原始數據。OpenGL讀入點陣圖數據需要註意一點:紋理過濾。OpenGL紋理過濾模式如下表:(--內容來自原書)
GL_NEAREST |
最近鄰過濾 |
GL_NEAREST_MIPMAP_NEAREST |
使用MIP貼圖的最近鄰過濾 |
GL_NEAREST_MIPMAP_LINEAR |
使用MIP貼圖級別之間插值的最近鄰過濾 |
GL_LINEAR |
雙線性過濾 |
GL_LINEAR_MIPMAP_NEAREST |
使用MIP貼圖的雙線性過濾 |
GL_LINEAR_MIPMAP_LINEAR |
三線性過濾(使用MIP貼圖級別之間插值的雙線性過濾) |
至於每種過濾具體的解釋及實現,請自行Google吧。這裡對於縮小情況,採用了GL_LINEAR_MIPMAP_LINEAR,對於放大情況,採用了GL_LINEAR。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
載入紋理的最後一步就是將bitmap複製到當前綁定的紋理對象:
texImage2D(GL_TEXTURE_2D, 0, bitmap, 0);
綁定之後,仍然需要做一些後續操作,比如回收bitmap對象(bitmap記憶體占用大戶),生成MIP貼圖,接觸紋理綁定,最後返回紋理對象ID。
glGenerateMipmap(GL_TEXTURE_2D); // Recycle the bitmap, since its data has been loaded into OpenGL. bitmap.recycle(); // Unbind from the texture. glBindTexture(GL_TEXTURE_2D, 0); return textureObjectIds[0];
二、紋理著色器
在繼續採用GLSL編寫著色器程式之前,先說明下之前遺漏的一個問題:
OpenGL著色語言(OpenGL Shading Language)是用來在OpenGL中著色編程的語言,也即開發人員寫的短小的自定義程式,他們是在圖形卡的GPU (Graphic Processor Unit圖形處理單元)上執行的,代替了固定的渲染管線的一部分,使渲染管線中不同層次具有可編程型。比如:視圖轉換、投影轉換等。
GLSL(GL Shading Language)的著色器代碼分成2個部分:Vertex Shader(頂點著色器)和Fragment(片斷著色器),有時還會有Geometry Shader(幾何著色器)。負責運行頂點著色的是頂點著色器。它可以得到當前OpenGL 中的狀態,GLSL內置變數進行傳遞。GLSL其使用C語言作為基礎高階著色語言,避免了使用彙編語言或硬體規格語言的複雜性。
這段內容來自百度百科,有一點需要重視:採用GLSL編寫的程式是在GPU中執行的,意味著著色器程式並不占用CPU時間,這啟發我們在某些耗時的渲染程式(攝像頭實時濾鏡)中可以採用GLSL實現,或許比NDK方式實現數據處理更為高效。後續筆者會在這方面實踐,這裡先說明紋理著色器程式。同樣,為了支持紋理,需對頂點著色器和片段著色器進行更改。
uniform mat4 u_Matrix; attribute vec4 a_Position; attribute vec2 a_TextureCoordinates; varying vec2 v_TextureCoordinates; void main() { v_TextureCoordinates = a_TextureCoordinates; gl_Position = u_Matrix * a_Position; }
precision mediump float; uniform sampler2D u_TextureUnit; varying vec2 v_TextureCoordinates; void main() { gl_FragColor = texture2D(u_TextureUnit, v_TextureCoordinates); }
上述頂點著色器中,變數a_TextureCoordinates的類型為vec2,因為紋理坐標的兩個分量:S坐標和T坐標。片段著色器中,sampler2D類型的u_TextureUnit表示接收二維紋理數據的數組。
三、更新頂點數據類結構
首先將不同類型的頂點數據分配到不同的類中,每個類代表一個物理對象的類型。在類的構造器中初始化VertexArray對象,VertexArray的實現與前述文章中描述的一致,採用FloatBuffer在本地代碼中存儲頂點矩陣數據,並創建通用方法將著色器的屬性與頂點數據關聯。
private final FloatBuffer floatBuffer; public VertexArray(float[] vertexData) { floatBuffer = ByteBuffer .allocateDirect(vertexData.length * BYTES_PER_FLOAT) .order(ByteOrder.nativeOrder()) .asFloatBuffer() .put(vertexData); } public void setVertexAttribPointer(int dataOffset, int attributeLocation, int componentCount, int stride) { floatBuffer.position(dataOffset); glVertexAttribPointer(attributeLocation, componentCount, GL_FLOAT, false, stride, floatBuffer); glEnableVertexAttribArray(attributeLocation); floatBuffer.position(0); }
public Table() { vertexArray = new VertexArray(VERTEX_DATA); }
構造器中傳入的參數VERTEX_DATA就是頂點數據。
private static final float[] VERTEX_DATA = { // Order of coordinates: X, Y, S, T // Triangle Fan 0f, 0f, 0.5f, 0.5f, -0.5f, -0.8f, 0f, 0.9f, 0.5f, -0.8f, 1f, 0.9f, 0.5f, 0.8f, 1f, 0.1f, -0.5f, 0.8f, 0f, 0.1f, -0.5f, -0.8f, 0f, 0.9f };
在該組數據中,x=0,y=0對應紋理S=0.5,T=0.5,x=-0.5,y=-0.8對應紋理S=0,T=0.9,之所以有這種對應關係,看下前面講到的OpenGL紋理坐標與電腦圖像坐標的對比就清楚啦。至於紋理部分的數據使用了0.1和0.9作為T坐標,是為了避免把紋理壓扁,而對紋理進行了裁剪,截取了0.1到0.9的部分。
初始化vertexArray之後,通過其setVertexAttribPointer()方法將頂點數據綁定到著色器程式上。
public void bindData(TextureShaderProgram textureProgram) { vertexArray.setVertexAttribPointer( 0, textureProgram.getPositionAttributeLocation(), POSITION_COMPONENT_COUNT, STRIDE); vertexArray.setVertexAttribPointer( POSITION_COMPONENT_COUNT, textureProgram.getTextureCoordinatesAttributeLocation(), TEXTURE_COORDINATES_COMPONENT_COUNT, STRIDE); }
這個方法為每個頂點調用了setVertexAttribPointer(),並從著色器程式獲取每個屬性的位置。通過getPositionAttributeLocation()把位置數據綁定到被引用的著色器屬性上,並通過getTextureCoordinatesAttributeLocation()把紋理坐標數據綁定到被引用的著色器屬性。
完成上述綁定以後,繪製只需要調用glDrawArrays()實現。
public void draw() { glDrawArrays(GL_TRIANGLE_FAN, 0, 6); }
四、著色器程式類
隨著紋理的使用,著色器程式變得更多,因此需要為著色器程式添加管理類。根據著色器分類,這裡分別創建紋理著色器類和顏色著色器類,且抽象它們的共同點,形成基類ShaderProgram,TextureShaderProgram和ColorShaderProgram分別繼承於此實現。ShaderProgram主要的功能就是根據Android上下文Context和著色器資源ID讀入著色器程式,其構造器參數列表如下:
protected ShaderProgram(Context context, int vertexShaderResourceId, int fragmentShaderResourceId) { …… }
讀入著色器程式的實現應該在ShaderHelper類中,其步驟與之前所述相似,包括編譯、鏈接等步驟。
public static int buildProgram(String vertexShaderSource, String fragmentShaderSource) { int program; // Compile the shaders. int vertexShader = compileVertexShader(vertexShaderSource); int fragmentShader = compileFragmentShader(fragmentShaderSource); // Link them into a shader program. program = linkProgram(vertexShader, fragmentShader); if (LoggerConfig.ON) { validateProgram(program); } return program; }
compileVertexShader(編譯)和linkProgram(鏈接)的實現在之前的筆記中已詳細描述過。ShaderProgram的構造器調用上述buildProgram()方法即可。
program = ShaderHelper.buildProgram(
TextResourceReader.readTextFileFromResource(
context, vertexShaderResourceId),
TextResourceReader.readTextFileFromResource(
context, fragmentShaderResourceId));
得到著色器程式之後,定義OpenGL後續的渲染使用該程式。
public void useProgram() { // Set the current OpenGL shader program to this program. glUseProgram(program); }
著色器程式類TextureShaderProgram和ColorShaderProgram在構造器中調用父類的構造函數,並讀入紋理著色器中uniform和屬性的位置。
public TextureShaderProgram(Context context) { super(context, R.raw.texture_vertex_shader, R.raw.texture_fragment_shader); // Retrieve uniform locations for the shader program. uMatrixLocation = glGetUniformLocation(program, U_MATRIX); uTextureUnitLocation = glGetUniformLocation(program, U_TEXTURE_UNIT); // Retrieve attribute locations for the shader program. aPositionLocation = glGetAttribLocation(program, A_POSITION); aTextureCoordinatesLocation = glGetAttribLocation(program, A_TEXTURE_COORDINATES); }
接下來,傳遞矩陣給uniform,這在之前的筆記中描述過了。
// Pass the matrix into the shader program. glUniformMatrix4fv(uMatrixLocation, 1, false, matrix, 0);
紋理的傳遞相對於矩陣的傳遞要複雜一些,因為紋理並不直接傳遞,而是採用紋理單元(Texture Unit)來保存,因為一個GPU只能同時繪製數量有限的紋理,使用這些紋理單元表示正在被繪製的活動的紋理。
// Set the active texture unit to texture unit 0. glActiveTexture(GL_TEXTURE0); // Bind the texture to this unit. glBindTexture(GL_TEXTURE_2D, textureId); // Tell the texture uniform sampler to use this texture in the shader by // telling it to read from texture unit 0. glUniform1i(uTextureUnitLocation, 0);
glActiveTexture(GL_TEXTURE0)表示把活動的紋理單元設置為紋理單元0,調用glBindTexture將textureId指向的紋理綁定到紋理單元0,最後,調用glUniform1i把選定的紋理單元傳遞給片段著色器中的u_TextureUnit(sampler2D)。
顏色著色器類與紋理著色器類的實現基本類似,同樣在構造器中獲取uniform和屬性的位置,不過設置uniform值只需傳遞矩陣即可。
public void setUniforms(float[] matrix) { // Pass the matrix into the shader program. glUniformMatrix4fv(uMatrixLocation, 1, false, matrix, 0); }
五、紋理繪製
通過前面的準備,頂點數據,著色器程式已經放到了不同的類中,因此,在渲染類中可以通過前面的實現進行紋理繪製了。AirHockeyRenderer類更新後的成員變數和構造函數如下:
private final Context context; private final float[] projectionMatrix = new float[16]; private final float[] modelMatrix = new float[16]; private Table table; private Mallet mallet; private TextureShaderProgram textureProgram; private ColorShaderProgram colorProgram; private int texture; public AirHockeyRenderer(Context context) { this.context = context; }
初始化變數主要包括清理屏幕、初始化頂點數組和著色器程式,載入紋理等。
@Override public void onSurfaceCreated(GL10 glUnused, EGLConfig config) { glClearColor(0.0f, 0.0f, 0.0f, 0.0f); table = new Table(); mallet = new Mallet(); textureProgram = new TextureShaderProgram(context); colorProgram = new ColorShaderProgram(context); texture = TextureHelper.loadTexture(context, R.drawable.air_hockey_surface); }
最後,在onDrawFrame()中繪製物體,繪製的方法就是通過調用前面著色器類和物體類(頂點數據)的方法來實現的。
@Override public void onDrawFrame(GL10 glUnused) { // Clear the rendering surface. glClear(GL_COLOR_BUFFER_BIT); // Draw the table. textureProgram.useProgram(); textureProgram.setUniforms(projectionMatrix, texture); table.bindData(textureProgram); table.draw(); // Draw the mallets. colorProgram.useProgram(); colorProgram.setUniforms(projectionMatrix); mallet.bindData(colorProgram); mallet.draw(); }
總結一下,這篇筆記涉及到一下內容:
1)載入紋理並顯示到物體上;
2)重新組織程式,管理多個著色器和頂點數據之間的切換;
3)調整紋理以適應它們將要被繪製的形狀,既可以調整紋理坐標,也可以通過拉伸或壓扁紋理本身來實現;
4)紋理不能直接傳遞,需要被綁定到紋理單元,然後將紋理單元傳遞給著色器;