Android OpenGL ES 2.0(四)---紋理基礎

原文鏈接:https://www.learnopengles.com/android-lesson-four-introducing-basic-texturing/

本文從下面鏈接翻譯過來:

Android Lesson Four: Introducing Basic Texturing

這是我們的第四個教程。在本課中,我們將添加我們在第三課中學到的內容,並學習如何添加紋理。我們將看看如何從應用程序資源中讀取圖像,將此圖像加載到OpenGL ES中,並將其顯示在屏幕。

跟着我,你會立刻理解基本的紋理

前提條件

本系列每個課程構建都是以前一個課程爲基礎,這節課是第三課的擴展,因此請務必在繼續之前複習該課程。

紋理的基礎知識

紋理貼圖(以及光照)的藝術是構建逼真3D世界的最重要部分之一。沒有紋理映射,一切都是平滑的陰影,看起來很虛假,就像90年代的老式控制檯遊戲。

第一款大量使用紋理的遊戲,如Doom和Duke Nukem 3D,通過增加的視覺效果,大大提升了遊戲的真實感 - 這些遊戲如果在黑暗中夜晚玩,可能會真正嚇到我們。

這裏是一個沒有紋理和有紋理的場景:

片元光照;正方形四個頂點中心位置

添加紋理;正方形四個頂點中心位置

看左邊的圖片,這個場景通過每像
素照明和着色點亮。這個場景看起
來非常平滑,現實生活中我們走進
一個房間有充滿了光滑陰影的東西
就像是這個立方體。

在看右邊的圖片,同樣的場景現在
紋理化了。環境光也增加了,因爲
紋理的使用使整個場景變暗,也可
以看到紋理對側面立方體的影響。
立方體具有和以前相同數量的多邊
形,但它們有新紋理看起來更加詳
細。

滿足於那些好奇的人,這個紋理的
資源來自於公共領域的資源

紋理座標

在OpengGL中,紋理座標時常使用座標(s,t)代替(x,y)。(s,t)表示紋理上的一個紋理元素,然後映射到多邊形。另外需要注意這些紋理座標和其他OpengGL座標相似:t(或y)軸指向上方,所以值越高你獲取的紋理越大。

大多數計算機圖形,y軸指向下方。這意味着左上角是圖片的原點(0,0),並且y值向下遞增。換句話說,OpenGL的座標系和大多數計算機圖形相反,這是您需要考慮到的。

               OpenGL的紋理座標系

紋理映射的基礎知識

在本課中,我們將查看具有紅色,綠色和藍色信息(GL_RGB)格式的2D紋理(GL_TEXTURE_2D)。 OpenGL ES還提供其他紋理模式,讓您可以進行不同的和更專業的效果。 我們將使用GL_NEAREST查看點採樣。 GL_LINEAR和mip-mapping將在以後的課程中介紹。

讓我們開始深入研究代碼,看看如何開始在Android中使用基本紋理!

頂點着色器

我們將採用上一課中的頂點着色器,並添加紋理支持。 以下是新的變化:

attribute vec2 a_TexCoordinate; // Per-vertex texture coordinate information we will pass in.
 
...
 
varying vec2 v_TexCoordinate;   // This will be passed into the fragment shader.
 
...
// Pass through the texture coordinate.
v_TexCoordinate = a_TexCoordinate;

在頂點着色器中,我們添加一個類型爲vec2的新屬性(一個包含兩個組件的數組),它將紋理座標信息作爲輸入。 它保存的是每個頂點紋理座標,跟頂點的位置,顏色和法向量一樣。 我們還添加了一個新的變量,它將通過三角形表面上的線性插值將此數據傳遞到片元着色器。

片元着色器

uniform sampler2D u_Texture;    // The input texture.
 
...
 
varying vec2 v_TexCoordinate; // Interpolated texture coordinate per fragment.
 
...
 
// Add attenuation.
 diffuse = diffuse * (1.0 / (1.0 + (0.10 * distance)));
 
...
 
// Add ambient lighting
 diffuse = diffuse + 0.3;
 
...
 
// Multiply the color by the diffuse illumination level and texture value to get final output color.
 
gl_FragColor = (v_Color * diffuse * texture2D(u_Texture, v_TexCoordinate));

我們添加一個uniform sampler2D類型來表示實際紋理數據(與紋理座標對應)。來自頂點着色器插值的varying類型紋理座標,我們調用texture2D(texture,textureCoordinate)來讀取當前座標處紋理的值。 然後我們取這個值並將其與其他項相乘以得到最終的輸出顏色。

以這種方式添加紋理會使整個場景變暗,因此我們還會稍微增強環境光照並減少光照衰減。

從圖像文件加載紋理

public static int loadTexture(final Context context, final int resourceId)
{
    final int[] textureHandle = new int[1];
 
    GLES20.glGenTextures(1, textureHandle, 0);
 
    if (textureHandle[0] != 0)
    {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inScaled = false;   // No pre-scaling
 
        // Read in the resource
        final Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), resourceId, options);
 
        // Bind to the texture in OpenGL
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandle[0]);
 
        // Set filtering
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
 
        // 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();
    }
 
    if (textureHandle[0] == 0)
    {
        throw new RuntimeException("Error loading texture.");
    }
 
    return textureHandle[0];
}

這段代碼將從Android res文件夾中讀取圖形文件並將其加載到OpenGL中。 我將解釋每個部分的作用。

我們首先需要讓OpenGL爲我們創建一個新的句柄。 這個句柄作爲一個唯一的標識符,每當我們想在OpenGL中引用相同的紋理時我們就會使用它。

final int[] textureHandle = new int[1];
GLES20.glGenTextures(1, textureHandle, 0);

這個OpenGL方法可用於同時生成多個句柄; 這裏我們只生成一個。

一旦我們有了紋理句柄,我們就用它來加載紋理。首先,我們需要以OpenGL能夠理解的格式獲取紋理。 我們不能直接使用PNG或JPG提供原始數據,因爲OpenGL不會理解。 我們需要做的第一步是將圖像文件解碼爲Android Bitmap對象:

final BitmapFactory.Options options = new BitmapFactory.Options();
options.inScaled = false;   // No pre-scaling
 
// Read in the resource
final Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), resourceId, options);

默認情況下,Android會根據設備的分辨率和你放置圖片的資源文件目錄而預先縮放位圖。我們不希望Android根據我們的情況對位圖進行縮放,因此我們將inScaled設置爲false

// Bind to the texture in OpenGL
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandle[0]);
 
// Set filtering
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);

然後我們綁定紋理,並設置幾個參數,告訴OpenGL後續OpenGL調用需要這樣過濾這個紋理。我們將默認過濾器設置爲GL_NEAREST,這是最快,也是最粗糙的過濾形式。它所做的就是在屏幕的每個點選擇最近的紋素,這可能導致圖像僞像和鋸齒。

  • GL_TEXTURE_MIN_FILTER — This tells OpenGL what type of filtering to apply when drawing the texture smaller than the original size in pixels.
  • GL_TEXTURE_MAG_FILTER — This tells OpenGL what type of filtering to apply when magnifying the texture beyond its original size in pixels.
// 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();

Android有一個非常有用的實用程序,可以直接將位圖加載到OpenGL中。 一旦您將資源讀入Bitmap對象,GLUtils.texImage2D()將負責其餘部分。 這是方法簽名:

public static void texImage2D (int target, int level, Bitmap bitmap, int border)

我們想要一個常規的2D位圖,所以我們傳入GL_TEXTURE_2D作爲第一個參數。 第二個參數用於mip-mapping,並允許您指定要在每個級別使用的圖像。 我們這裏沒有使用mip-mapping,因此我們將0設置爲默認級別。 我們沒有使用邊框,所以第三個參數我們傳入0。

然後我們在原始位圖上調用recycle(),這是釋放內存的重要一步。 紋理已加載到OpenGL中,因此我們不需要保留它的副本。Android應用程序在執行垃圾收集的Dalvik VM下運行,但Bitmap對象包含駐留在native內存中的數據,如果你不明確的回收它們,它們需要幾個週期來進行垃圾收集。 這意味着如果您忘記執行此操作,實際上可能會因內存不足錯誤而崩潰,即使您不再持有對位圖的任何引用。

將紋理應用於場景

首先,我們需要在類中添加各種成員來保存紋理所需的東西:

/** Store our model data in a float buffer. */
private final FloatBuffer mCubeTextureCoordinates;
 
/** This will be used to pass in the texture. */
private int mTextureUniformHandle;
 
/** This will be used to pass in model texture coordinate information. */
private int mTextureCoordinateHandle;
 
/** Size of the texture coordinate data in elements. */
private final int mTextureCoordinateDataSize = 2;
 
/** This is a handle to our texture data. */
private int mTextureDataHandle;

我們基本上是需要添加新成員變量來跟蹤我們添加到着色器的內容,以及保持對紋理的引用。

定義紋理座標

我們在構造函數中定義紋理座標:

// S, T (or X, Y)
// Texture coordinate data.
// Because images have a Y axis pointing downward (values increase as you move down the image) while
// OpenGL has a Y axis pointing upward, we adjust for that here by flipping the Y axis.
// What's more is that the texture coordinates are the same for every face.
final float[] cubeTextureCoordinateData =
{
        // Front face
        0.0f, 0.0f,
        0.0f, 1.0f,
        1.0f, 0.0f,
        0.0f, 1.0f,
        1.0f, 1.0f,
        1.0f, 0.0f,
 
...

這些座標數據看起來可能有點混亂。如果您返回去看第三課中點的位置是如何定義的,您將會發現我們爲正方體每個面都定義了兩個三角形。點的定義方式像下面這樣:

(三角形1)
左上角,
左下角,
右上角,

(三角形2)
左下角,
右下角,
右上角,

紋理座標和正面的位置座標對應,但是由於Android的圖象Y軸座標與OpenGL的紋理的Y軸座標方向相反,所以我們需要翻轉紋理的Y軸座標。我自己畫了個圖理解:

設置紋理

我們在onSurfaceCreated()方法中加載紋理。

@Override
public void onSurfaceCreated(GL10 glUnused, EGLConfig config)
{
 
    ...
 
    // The below glEnable() call is a holdover from OpenGL ES 1, and is not needed in OpenGL ES 2.
    // Enable texture mapping
    // GLES20.glEnable(GLES20.GL_TEXTURE_2D);
 
    ...
 
    mProgramHandle = ShaderHelper.createAndLinkProgram(vertexShaderHandle, fragmentShaderHandle,
            new String[] {"a_Position",  "a_Color", "a_Normal", "a_TexCoordinate"});
 
    ...
 
    // Load the texture
    mTextureDataHandle = TextureHelper.loadTexture(mActivityContext, R.drawable.bumpy_bricks_public_domain);

我們傳入“a_TexCoordinate”作爲要在我們的着色器程序中綁定的新屬性,並使用我們在上面創建的loadTexture()方法加載我們的紋理。

使用紋理

我們還在onDrawFrame(GL10 glUnused)方法中添加了一些代碼。

@Override
public void onDrawFrame(GL10 glUnused)
{
 
    ...
 
    mTextureUniformHandle = GLES20.glGetUniformLocation(mProgramHandle, "u_Texture");
    mTextureCoordinateHandle = GLES20.glGetAttribLocation(mProgramHandle, "a_TexCoordinate");
 
    // Set the active texture unit to texture unit 0.
    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
 
    // Bind the texture to this unit.
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureDataHandle);
 
    // Tell the texture uniform sampler to use this texture in the shader by binding to texture unit 0.
    GLES20.glUniform1i(mTextureUniformHandle, 0);

我們得到紋理數據和紋理座標在片元着色器中的位置。 在OpenGL中,紋理需要綁定到紋理單元才能在渲染中使用。 紋理單元是讀取紋理數據並將其傳遞到着色器。不同的圖形芯片具有不同數量的紋理單元,因此在使用它們之前,您需要檢查是否存在其他紋理單元。

首先,我們告訴OpenGL我們要激活紋理單元0.然後通過調用glBindTexture(),將會綁定一個2D紋理數據到第0個紋理單元。最後,我們告訴OpenGL片元着色器我們使用的是第0個紋理單元,程序中的mTextureUniformHandle它指的是片元着色器中的“u_Texture”的引用。

簡而言之:

  1. 設置活動紋理單元。
  2. 將紋理綁定到此單元。
  3. 告訴片元着色器你使用的那個紋理單元。

根據需要重複多個紋理。

進一步練習

一旦您做到這兒,您就完成的差不多了!作爲下一個練習,嘗試通過加載另一個紋理,將其綁定到另一個單元,並在着色器中使用它。

回顧

現在我們回顧一下所有的着色器代碼,以及我們添加了一個新的功能用來從資源目錄讀取着色器代碼,而不是存儲在java字符串中:

頂點着色器

uniform mat4 u_MVPMatrix;       // A constant representing the combined model/view/projection matrix.
uniform mat4 u_MVMatrix;        // A constant representing the combined model/view matrix.
 
attribute vec4 a_Position;      // Per-vertex position information we will pass in.
attribute vec4 a_Color;         // Per-vertex color information we will pass in.
attribute vec3 a_Normal;        // Per-vertex normal information we will pass in.
attribute vec2 a_TexCoordinate; // Per-vertex texture coordinate information we will pass in.
 
varying vec3 v_Position;        // This will be passed into the fragment shader.
varying vec4 v_Color;           // This will be passed into the fragment shader.
varying vec3 v_Normal;          // This will be passed into the fragment shader.
varying vec2 v_TexCoordinate;   // This will be passed into the fragment shader.
 
// The entry point for our vertex shader.
void main()
{
    // Transform the vertex into eye space.
    v_Position = vec3(u_MVMatrix * a_Position);
 
    // Pass through the color.
    v_Color = a_Color;
 
    // Pass through the texture coordinate.
    v_TexCoordinate = a_TexCoordinate;
 
    // Transform the normal's orientation into eye space.
    v_Normal = vec3(u_MVMatrix * vec4(a_Normal, 0.0));
 
    // gl_Position is a special variable used to store the final position.
    // Multiply the vertex by the matrix to get the final point in normalized screen coordinates.
    gl_Position = u_MVPMatrix * a_Position;
}

片元着色器

precision mediump float;        // Set the default precision to medium. We don't need as high of a
                                // precision in the fragment shader.
uniform vec3 u_LightPos;        // The position of the light in eye space.
uniform sampler2D u_Texture;    // The input texture.
 
varying vec3 v_Position;        // Interpolated position for this fragment.
varying vec4 v_Color;           // This is the color from the vertex shader interpolated across the
                                // triangle per fragment.
varying vec3 v_Normal;          // Interpolated normal for this fragment.
varying vec2 v_TexCoordinate;   // Interpolated texture coordinate per fragment.
 
// The entry point for our fragment shader.
void main()
{
    // Will be used for attenuation.
    float distance = length(u_LightPos - v_Position);
 
    // Get a lighting direction vector from the light to the vertex.
    vec3 lightVector = normalize(u_LightPos - v_Position);
 
    // Calculate the dot product of the light vector and vertex normal. If the normal and light vector are
    // pointing in the same direction then it will get max illumination.
    float diffuse = max(dot(v_Normal, lightVector), 0.0);
 
    // Add attenuation.
    diffuse = diffuse * (1.0 / (1.0 + (0.10 * distance)));
 
    // Add ambient lighting
    diffuse = diffuse + 0.3;
 
    // Multiply the color by the diffuse illumination level and texture value to get final output color.
    gl_FragColor = (v_Color * diffuse * texture2D(u_Texture, v_TexCoordinate));
  }

怎樣從raw資源目錄中讀取着色器文本?

public static String readTextFileFromRawResource(final Context context,
            final int resourceId)
    {
        final InputStream inputStream = context.getResources().openRawResource(
                resourceId);
        final InputStreamReader inputStreamReader = new InputStreamReader(
                inputStream);
        final BufferedReader bufferedReader = new BufferedReader(
                inputStreamReader);
 
        String nextLine;
        final StringBuilder body = new StringBuilder();
 
        try
        {
            while ((nextLine = bufferedReader.readLine()) != null)
            {
                body.append(nextLine);
                body.append('\n');
            }
        }
        catch (IOException e)
        {
            return null;
        }
 
        return body.toString();
    }

代碼下載

可以從GitHub上的項目站點下載本課程的完整源代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章