Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android開發 >> 關於android開發 >> OpenGL ES學習筆記(三)——紋理,es學習筆記

OpenGL ES學習筆記(三)——紋理,es學習筆記

編輯:關於android開發

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 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)紋理不能直接傳遞,需要被綁定到紋理單元,然後將紋理單元傳遞給著色器;

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