OpenGL ES學習筆記(三)——紋理,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 resourcefinal 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 OpenGLglBindTexture(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;}
初始設定變數主要包括清理螢幕、初始化頂點數組和著色器程式,載入紋理等。
@Overridepublic 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()中繪製物體,繪製的方法就是通過調用前面著色器類和物體類(頂點資料)的方法來實現的。
@Overridepublic 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)紋理不能直接傳遞,需要被綁定到紋理單元,然後將紋理單元傳遞給著色器;