1. 程式人生 > >android平臺下OpenGL ES 3.0實現2D紋理貼圖顯示bitmap

android平臺下OpenGL ES 3.0實現2D紋理貼圖顯示bitmap

OpenGL ES 3.0學習實踐

目錄

2D紋理簡介

2D紋理是OpenGL ES中最基本和常用的紋理形式。2D紋理本質上其實:是一個影象資料的二維陣列。一個紋理的單獨資料元素稱作"紋素(texel)"。用2D紋理渲染時,紋理座標用作紋理影象中的索引。2D紋理的紋理座標用一對2D座標(s,t)

指定,有時也 稱作(u,v)座標。

紋理影象的左下角由對座標(0.0, 0.0)指定,右上角由對座標(1.0,1.0)指定。在[0.0,1.0]區間之外的座標是允許的,在該區間之外的紋理讀取行為由紋理包裝模式定義。

OpenGL 2D紋理座標:

通過指定紋理座標,可以對映到紋素。例如一個256x256大小的二維紋理,座標(0.5,1.0)對應的紋素即是(256x0.5 = 128, 256x1.0 = 256)

紋理對映時只需要為物體的頂點指定紋理座標即可,其餘部分由片元著色器插值完成。

在這裡插入圖片描述

模型變換和紋理座標

模型變換,就是對物體進行縮放、旋轉、平移等操作,後面會著重介紹。當對物體進行這些操作時,頂點對應的紋理座標不會進行改變,通過插值後,物體的紋理也像緊跟著物體發生了變化一樣。

經過旋轉等變換後,物體和對應的紋理座標如下圖所示,可以看出上面圖中紋理部分的房子也跟著發生了旋轉。

紋理座標和OpenGL座標

由於對一個OpenGL紋理來說,它沒有內在的方向性,因此我們可以使用不同的座標把它定向到任何我們喜歡的方向上,然而大多數計算機影象都有一個預設的方向,它們通常被規定為y軸向下,y的值隨著向影象的底部移動而增加。

如果想用正確的方向觀看影象,那紋理座標就必須要考慮這點。

可以對照這個座標系檢視本例程式碼中定義的座標點:

灰色為OpenGL座標系中的頂點座標
紅色為紋理座標系中的紋理座標

紋理貼圖顯示bitmap實踐

還是以一個示例來實踐一下,基於之前的專案工程,新增一個新的工具類TextureUtils.java

/**
 * @anchor: andy
 * @date: 18-11-10
 */

public class TextureUtils {

    private static final String TAG = "TextureUtils";

    public static int loadTexture(Context context, int resourceId) {
        final int[] textureIds = new int[1];
        //建立一個紋理物件
        GLES30.glGenTextures(1, textureIds, 0);
        if (textureIds[0] == 0) {
            Log.e(TAG, "Could not generate a new OpenGL textureId object.");
            return 0;
        }
        final BitmapFactory.Options options = new BitmapFactory.Options();
        //這裡需要載入原圖未經縮放的資料
        options.inScaled = false;
        final Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), resourceId, options);
        if (bitmap == null) {
            Log.e(TAG, "Resource ID " + resourceId + " could not be decoded.");
            GLES30.glDeleteTextures(1, textureIds, 0);
            return 0;
        }
        // 繫結紋理到OpenGL
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureIds[0]);

		//設定預設的紋理過濾引數
        GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR_MIPMAP_LINEAR);
        GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);

        // 載入bitmap到紋理中
        GLUtils.texImage2D(GLES30.GL_TEXTURE_2D, 0, bitmap, 0);

        // 生成MIP貼圖
        GLES30.glGenerateMipmap(GLES30.GL_TEXTURE_2D);

        // 資料如果已經被載入進OpenGL,則可以回收該bitmap
        bitmap.recycle();

        // 取消繫結紋理
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0);

        return textureIds[0];
    }

}

新建TextureRenderer.java檔案:

/**
 * @anchor: andy
 * @date: 2018-11-09
 * @description: 基於紋理貼圖顯示bitmap
 */
public class TextureRenderer implements GLSurfaceView.Renderer {

    private static final String TAG = "TextureRenderer";

    private final FloatBuffer vertexBuffer, mTexVertexBuffer;

    private final ShortBuffer mVertexIndexBuffer;

    private int mProgram;

    private int textureId;

        /**
     * 頂點座標
     * (x,y,z)
     */
    private float[] POSITION_VERTEX = new float[]{
            0f, 0f, 0f,     //頂點座標V0
            1f, 1f, 0f,     //頂點座標V1
            -1f, 1f, 0f,    //頂點座標V2
            -1f, -1f, 0f,   //頂點座標V3
            1f, -1f, 0f     //頂點座標V4
    };

    /**
     * 紋理座標
     * (s,t)
     */
    private static final float[] TEX_VERTEX = {
            0.5f, 0.5f, //紋理座標V0
            1f, 0f,     //紋理座標V1
            0f, 0f,     //紋理座標V2
            0f, 1.0f,   //紋理座標V3
            1f, 1.0f    //紋理座標V4
    };

    /**
     * 索引
     */
    private static final short[] VERTEX_INDEX = {
            0, 1, 2,  //V0,V1,V2 三個頂點組成一個三角形
            0, 2, 3,  //V0,V2,V3 三個頂點組成一個三角形
            0, 3, 4,  //V0,V3,V4 三個頂點組成一個三角形
            0, 4, 1   //V0,V4,V1 三個頂點組成一個三角形
    };

    public TextureRenderer() {
        //分配記憶體空間,每個浮點型佔4位元組空間
        vertexBuffer = ByteBuffer.allocateDirect(POSITION_VERTEX.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        //傳入指定的座標資料
        vertexBuffer.put(POSITION_VERTEX);
        vertexBuffer.position(0);

        mTexVertexBuffer = ByteBuffer.allocateDirect(TEX_VERTEX.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer()
                .put(TEX_VERTEX);
        mTexVertexBuffer.position(0);

        mVertexIndexBuffer = ByteBuffer.allocateDirect(VERTEX_INDEX.length * 2)
                .order(ByteOrder.nativeOrder())
                .asShortBuffer()
                .put(VERTEX_INDEX);
        mVertexIndexBuffer.position(0);
    }


    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        //設定背景顏色
        GLES30.glClearColor(0.5f, 0.5f, 0.5f, 0.5f);
        //編譯
        final int vertexShaderId = ShaderUtils.compileVertexShader(ResReadUtils.readResource(R.raw.vertex_texture_shader));
        final int fragmentShaderId = ShaderUtils.compileFragmentShader(ResReadUtils.readResource(R.raw.fragment_texture_shader));
        //連結程式片段
        mProgram = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId);

        //載入紋理
        textureId = TextureUtils.loadTexture(AppCore.getInstance().getContext(), R.drawable.main);
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        GLES30.glViewport(0, 0, width, height);
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);

        //使用程式片段
        GLES30.glUseProgram(mProgram);
		//啟用頂點座標屬性
        GLES30.glEnableVertexAttribArray(0);
        GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 0, vertexBuffer);
		//啟用紋理座標屬性
        GLES30.glEnableVertexAttribArray(1);
        GLES30.glVertexAttribPointer(1, 2, GLES30.GL_FLOAT, false, 0, mTexVertexBuffer);
		//啟用紋理
        GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
        //繫結紋理
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId);

        // 繪製
        GLES20.glDrawElements(GLES20.GL_TRIANGLES, VERTEX_INDEX.length, GLES20.GL_UNSIGNED_SHORT, mVertexIndexBuffer);

    }
}

頂點著色器

#version 300 es
layout (location = 0) in vec4 vPosition;
layout (location = 1) in vec2 aTextureCoord;
//輸出紋理座標(s,t)
out vec2 vTexCoord;
void main() { 
     gl_Position  = vPosition;
     gl_PointSize = 10.0;
     vTexCoord = aTextureCoord;
}

片段著色器

#version 300 es
precision mediump float;
uniform sampler2D uTextureUnit;
//接收剛才頂點著色器傳入的紋理座標(s,t)
in vec2 vTexCoord;
out vec4 vFragColor;
void main() {
     vFragColor = texture(uTextureUnit,vTexCoord);
}

注意:在OpenGL ES 2.0中這裡的方法是texture2D

在這裡插入圖片描述

紋理過濾的介紹

由於紋理座標與解析度無關,因此它們並不總是精確匹配畫素。當紋理影象拉伸超過其原始大小或尺寸縮小時,會發生這種情況。當發生這種情況時,OpenGL提供了各種方法來決定取樣顏色。此過程稱為過濾,針對上述情況,我們可以配置OpenGL使用一個紋理過濾器。

  • GL_NEAREST:返回最接近座標的畫素。
  • GL_LINEAR:返回給定座標周圍4個畫素的加權平均值。
  • GL_NEAREST_MIPMAP_NEAREST,GL_LINEAR_MIPMAP_NEAREST, GL_NEAREST_MIPMAP_LINEAR,GL_LINEAR_MIPMAP_LINEAR:從MIP貼圖,而不是樣品。

最近和線性插值的區別:

雖然線性插值可以提供更平滑的結果,但它並不總是最理想的選擇。由於畫素化的外觀,最近鄰插值更適合想要模仿8點陣圖形的遊戲。

可以指定哪種插值應用於兩種不同的情況:縮小影象並向上縮放影象。這兩個案例由關鍵字GL_TEXTURE_MIN_FILTER和GL_TEXTURE_MAG_FILTER

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

當然了,還有另一種過濾紋理的方法:mipmapMipmap是紋理的較小副本,已預先調整大小並進行過濾。推薦使用它們,因為它們可以帶來更高的質量和更高的效能。

glGenerateMipmap(GL_TEXTURE_2D);

要使用mipmap,請選擇四種mipmap過濾方法之一:

  • GL_NEAREST_MIPMAP_NEAREST:使用最接近匹配紋理畫素大小的mipmap和最近鄰插值的樣本。
  • GL_LINEAR_MIPMAP_NEAREST:使用線性插值對最近的mipmap進行取樣。
  • GL_NEAREST_MIPMAP_LINEAR:使用與紋理畫素大小最匹配的兩個mipmap和最近鄰插值的樣本。
  • GL_LINEAR_MIPMAP_LINEAR:採用線性插值的樣本最接近兩個mipmap。

正交投影調整顯示效果

剛才也看到最終的顯示效果有種拉伸的感覺,現在使用正交投影來調整這個顯示效果,修改一下頂點著色器,加入矩陣,可以參考之前的例子:android平臺下OpenGL ES 3.0從矩形中看矩陣和正交投影

#version 300 es
layout (location = 0) in vec4 vPosition;
layout (location = 1) in vec2 aTextureCoord;
//矩陣
uniform mat4 u_Matrix;
out vec2 vTexCoord;
void main() { 
     gl_Position  = u_Matrix * vPosition;
     gl_PointSize = 10.0;
     vTexCoord = aTextureCoord;
}

onSurfaceChanged方法中

@Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        GLES30.glViewport(0, 0, width, height);

        final float aspectRatio = width > height ?
                (float) width / (float) height :
                (float) height / (float) width;
        if (width > height) {
            //橫屏
            Matrix.orthoM(mMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, -1f, 1f);
        } else {
            //豎屏
            Matrix.orthoM(mMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, -1f, 1f);
        }

    }

onDrawFrame回撥中修改如下:

@Override
    public void onDrawFrame(GL10 gl) {
        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
        //使用程式片段
        GLES30.glUseProgram(mProgram);

		//正交投影矩陣
        GLES30.glUniformMatrix4fv(uMatrixLocation, 1, false, mMatrix, 0);

        ......
        
        GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
        //繫結紋理
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId);

        // 繪製
        GLES20.glDrawElements(GLES20.GL_TRIANGLES, VERTEX_INDEX.length, GLES20.GL_UNSIGNED_SHORT, mVertexIndexBuffer);

    }

顯示效果如下:

專案地址:opengles-texture
https://github.com/byhook/opengles4android

參考:
https://open.gl/textures
http://www.opengl-tutorial.org/cn/beginners-tutorials/tutorial-5-a-textured-cube/
https://blog.piasy.com/2016/06/14/Open-gl-es-android-2-part-2/

《OpenGL ES 3.0 程式設計指南第2版》
《OpenGL ES應用開發實踐指南Android卷》