Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> android openGL ES2 一切從繪制紋理開始

android openGL ES2 一切從繪制紋理開始

編輯:關於Android編程

紋理,在openGL中,可以理解為加載到顯卡顯存中的圖片。Android設備在2.2開始支持openGL ES2.0,從前都是ES1.0 和 ES1.1的版本。簡單來說,openGL ES是為了嵌入設備進行功能剪裁後的openGL版本。ES2.0是和1.x版本不兼容的,區別和兼容性參見android 官方文檔。

首先,android使用openGL提供了特殊的view作為基礎叫做GLSurfaceView。我們的view需要繼承GLSurfaceView。如下簡單示例:

public class MyGLSurfaceView extends GLSurfaceView {
 
    public MyGLSurfaceView(Context context) {
        super(context);
        setFocusableInTouchMode(true);
 
        // Tell the surface view we want to create an OpenGL ES 2.0-compatible
        // context, and set an OpenGL ES 2.0-compatible renderer.
        this.setEGLContextClientVersion(2);
 
        this.setRenderer(new MyRenderer());
    }
 
}

並沒有什麼特別之處,android view的渲染操作需要實現一個render接口,GLSurfaceView的渲染接口為android.opengl.GLSurfaceView.Renderer。我們需要實現接口的方法。

public class MyRenderer implements Renderer {
 
public void onDrawFrame(GL10 gl) {}
 
public void onSurfaceChanged(GL10 gl, int width, int height) {}
 
public void onSurfaceCreated(GL10 gl, EGLConfig config) {}
 
}

接口實現3個方法,對應繪制,繪制區域變化,區域創建。需要說明的是參數GL10 gl是openGL es1.x版本的對象。這裡我們不會使用到。還有一點就是,onDrawFrame方法的調用是有系統調用的,不需要手動調用。系統會以一定的頻率不斷的回調。

接下來我們進入ES2.0的使用,先上代碼:

public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    GLES20.glEnable(GLES20.GL_TEXTURE_2D);
    // Active the texture unit 0
    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
 
    loadVertex();
    initShader();
    loadTexture();
}



繪制區域創建的時候,我們設置了啟用2D的紋理,並且激活了紋理單元unit0。什麼意思呢,說起來話長,以後慢慢說。簡單說一下,記住openGL是基於狀態的,就是很多狀態的設置和切換,這裡啟用GL_TEXTURE_2D就是一個狀態的開啟,表明openGL可以使用2D紋理。

那神馬是激活紋理單元,這個和硬件有點關系,openGL要顯卡會劃分存儲紋理的存儲區域不止一個區域。這裡是使用區域 unit 0,多重紋理繪制可以開啟多個,這個以後說。接下來,調用了三個函數,載入頂點,初始化著色器,載入紋理。


第一,載入頂點,openGL繪制圖形是根據頂點以後鏈接起來的。為什麼要這樣,其實這樣很強大是一種設計吧。頂點可以暫時簡單理解為含有位置信息的坐標點。展開代碼如下:

private void loadVertex() {
    // float size  = 4
    this.vertex = ByteBuffer.allocateDirect(quadVertex.length * 4)
                            .order(ByteOrder.nativeOrder())
                            .asFloatBuffer();
 
    this.vertex.put(quadVertex).position(0);
 
    // short size = 2
    this.index = ByteBuffer.allocateDirect(quadIndex.length * 2)
                           .order(ByteOrder.nativeOrder())
                           .asShortBuffer();
 
    this.index.put(quadIndex).position(0);
}
 
private FloatBuffer vertex;
private ShortBuffer index;
 
private float[] quadVertex = new float[] {
        -0.5f, 0.5f, 0.0f, // Position 0
        0, 1.0f, // TexCoord 0
 
        -0.5f, -0.5f, 0.0f, // Position 1
        0, 0, // TexCoord 1
 
        0.5f , -0.5f, 0.0f, // Position 2
        1.0f, 0, // TexCoord 2
 
        0.5f, 0.5f, 0.0f, // Position 3
        1.0f, 1.0f, // TexCoord 3
};
 
private short[] quadIndex = new short[] {
        (short)(0), // Position 0
        (short)(1), // Position 1
        (short)(2), // Position 2
 
        (short)(2), // Position 2  
        (short)(3), // Position 3  
        (short)(0), // Position 0
};

FloatBuffer,ShortBuffer是封裝了本地數據結構的封裝對象。是的,這個2個對象裡面的數據不被java虛擬機管理,相當於C語言的存儲方式。具體的介紹可以參看這裡(想了解的猛擊)。 quadVertex的數據就是一個矩形的坐標,和紋理坐標。一兩句話很難解釋清楚,這裡涉及到openGL的幾個經典的坐標系,下次說。概括的說,openGL的坐標是單位化的,都是0.0-1.0的浮點型,屏幕的中心點是(0,0)。而紋理的坐標左下角是(0,0)。 這裡的quadVertex是在屏幕中大概花了一個矩形貼了一個圖片, position0 是左上點,以後左下,右下,右上的順序,紋理坐標同理。

quadIndx神馬意思呢,就是這剛才的這些頂點索引排列。這裡一個矩形也就4個頂點,每個頂點3個位置坐標,2個紋理坐標。也就是說一個頂點有5個float數據。至於為什麼頂點為什麼這麼排列下次說,是2個三角形合成了一個矩形,幾句話很難解釋清楚。

所以說,這段代碼就是把矩形的位置和紋理坐標,存儲到本地數據,准備後面使用而已。

第二,初始化著色器。這個著色器就是ES2.0的特色,又叫可編程著色器,也是區別於ES1.x的本質。這裡只做簡單的介紹。可編程著色器是一種腳本,語法類似C語言,腳本分為頂點著色器和片段著色器,分別對應了openGL不同的渲染流程。如下:

頂點著色器:

uniform mat4 u_MVPMatrix;
 
attribute vec4 a_position;
attribute vec2 a_texCoord;
 
varying vec2 v_texCoord;
 
 void main() 
 {
    gl_Position = a_position;
    v_texCoord  = a_texCoord;   
 }

片段著色器:

precision lowp float;       
 
varying vec2 v_texCoord;                       
uniform sampler2D u_samplerTexture;
 
void main()                                          
{                                                    
  gl_FragColor = texture2D(u_samplerTexture, v_texCoord);
}

這裡記住一句話,頂點著色器,會在頂點上執行;片段著色器會在像素點上執行。剛才的矩形就有4個頂點,每個頂點都會應用這個腳本。也就是說,頂點是位置相關信息,片段是色彩紋理相關信息。

這個2段腳本都是文本,需要編譯,鏈接,等等一些操作才能被ES2.0所使用。過程就像C語言的編譯運行過程。openGL 提供了相關函數去做這些事情。如下:

private void initShader() {
    String vertexSource   = Tools.readFromAssets("VertexShader.glsl");
    String fragmentSource = Tools.readFromAssets("FragmentShader.glsl");
 
    // Load the shaders and get a linked program
    program = GLHelper.loadProgram(vertexSource, fragmentSource);
 
    // Get the attribute locations
    attribPosition = GLES20.glGetAttribLocation(program, "a_position");
    attribTexCoord = GLES20.glGetAttribLocation(program, "a_texCoord");
 
    uniformTexture = GLES20.glGetUniformLocation(program, "u_samplerTexture");
 
    GLES20.glUseProgram(program);
    GLES20.glEnableVertexAttribArray(attribPosition);
    GLES20.glEnableVertexAttribArray(attribTexCoord);
    // Set the sampler to texture unit 0
    GLES20.glUniform1i(uniformTexture, 0);
}

可以看到,頂點和片段一起構成一個program,它可以被openGL所使用,是一個編譯好的腳本程序,存儲在顯存。 GLES20.glGetAttribLocation 和 GLES20.glGetUniformLocation 這句話是神馬作用呢。簡單說就是,java程序和著色器腳本數據通信的。把就像參數的傳遞一樣,這樣腳本就能根據外界的參數變化,實時的改變openGL流水線渲染的處理流程。


以下是我封裝的載入著色器的輔助方法:

public static int loadProgram(String vertexSource, String fragmentSource) { 
    // Load the vertex shaders
    int vertexShader = GLHelper.loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
 
    // Load the fragment shaders
    int fragmentShader = GLHelper.loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
 
    // Create the program object
    int program = GLES20.glCreateProgram();
 
    if (program == 0) {
        throw new RuntimeException("Error create program.");
    }
 
    GLES20.glAttachShader(program, vertexShader);
    GLES20.glAttachShader(program, fragmentShader);
 
    // Link the program
    GLES20.glLinkProgram(program);
 
    int[] linked = new int[1];
 
    // Check the link status
    GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linked, 0);
 
    if (linked[0] == 0) {
        GLES20.glDeleteProgram(program);
        throw new RuntimeException("Error linking program: " +  GLES20.glGetProgramInfoLog(program));
    }
 
    // Free up no longer needed shader resources
    GLES20.glDeleteShader(vertexShader);
    GLES20.glDeleteShader(fragmentShader);
 
    return program;
}

public static int loadShader(int shaderType, String source) { 
 
    // Create the shader object
    int shader = GLES20.glCreateShader(shaderType);
 
    if (shader == 0) {
        throw new RuntimeException("Error create shader.");
    }
 
    int[] compiled = new int[1];
 
    // Load the shader source
    GLES20.glShaderSource(shader, source);
 
    // Compile the shader
    GLES20.glCompileShader(shader);
 
    // Check the compile status
    GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
 
    if (compiled[0] == 0) {
        GLES20.glDeleteShader(shader);
        throw new RuntimeException("Error compile shader: " + GLES20.glGetShaderInfoLog(shader));
    }
 
    return shader;
}

為什麼openGL的很多操作目標都是int類型的,因為openGL只會在顯存生成或綁定地址,返回id,以後用id相當於句柄去改變它的內部狀態。

第三,就是載入紋理了。載入紋理,就是把圖片的數據上傳到顯存,以後在使用它。請注意紋理圖片的長和寬最好是2的N次方,不然不一定能繪制出來。

static int[] loadTexture(String path) {
     int[] textureId = new int[1];
 
     // Generate a texture object
     GLES20.glGenTextures(1, textureId, 0);
 
     int[] result = null;
 
     if (textureId[0] != 0) {
 
         InputStream is = Tools.readFromAsserts(path);
 
         Bitmap bitmap;
         try {
             bitmap = BitmapFactory.decodeStream(is);
         } finally {
             try {
                 is.close();
             } catch (IOException e) {
                 throw new RuntimeException("Error loading Bitmap.");
             }
         }
 
         result = new int[3];
         result[TEXTURE_ID] = textureId[0]; // TEXTURE_ID
         result[TEXTURE_WIDTH] = bitmap.getWidth(); // TEXTURE_WIDTH
         result[TEXTURE_HEIGHT] = bitmap.getHeight(); // TEXTURE_HEIGHT
 
         // Bind to the texture in OpenGL
         GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId[0]);
 
         // Set filtering
         GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
         GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
 
         GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
         GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
 
         // Load the bitmap into the bound texture.
         GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
 
         // Recycle the bitmap, since its data has been loaded into OpenGL.
         bitmap.recycle();
 
     } else {
         throw new RuntimeException("Error loading texture.");
     }
 
     return result;
 }

代碼一目了然,這裡使用了android的工具類吧bitmap直接轉換成openGL紋理需要的格式了。過程是,先生成一個紋理的id在顯卡上的,以後根據id上傳紋理數據,以後保存這個id就可以操作這個紋理了。至於紋理的一些過濾特性設置,下次再說。


現在貌似就剩下繪制了,准備好了頂點信息,頂點對應的紋理坐標。初始化了著色器,上傳了紋理圖片。接下來就已把他們合起來繪制了。

public void onDrawFrame(GL10 gl) {
    // clear screen to black
    GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
 
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
 
    vertex.position(0);
    // load the position
    // 3(x , y , z)
    // (2 + 3 )* 4 (float size) = 20
    GLES20.glVertexAttribPointer(attribPosition, 
                                 3, GLES20.GL_FLOAT, 
                                 false, 20, vertex);
 
    vertex.position(3);
    // load the texture coordinate
    GLES20.glVertexAttribPointer(attribTexCoord, 
                                  2, GLES20.GL_FLOAT,
                                  false, 20, vertex);
 
    GLES20.glDrawElements(GLES20.GL_TRIANGLES, 6, GLES20.GL_UNSIGNED_SHORT, index);
}

我盡力保持了代碼的簡單,openGL的基於狀態體現,bind這個函數無處不在,這裡bindTexture就是通知openGL使用那個id的紋理圖片。接下來的操作就是針對bind的圖片的。繪制就需要讓openGL知道繪制神馬。所以這裡需要用到vertex這個本地數據容器,裡面裝在的是頂點和紋理坐標信息。 GLES20.glVertexAttribPointer就是把頂點數據,按照openGL喜歡的格式上傳到顯卡存儲。draw方法的調用,是在前面應用了紋理id的情況下,所以繪制紋理坐標的時候,會使用上傳的紋理圖片。


是的,每次都需要把數據上傳到openGL,畢竟顯存和內存不是同一個地方,openGL采用了客戶端-服務端的設計模式。當然使用VBO等技術可以把數據緩存在顯存,提高運行性能。這個以後再說吧。



  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved