本文從下面鏈接翻譯過來:
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”的引用。
簡而言之:
- 設置活動紋理單元。
- 將紋理綁定到此單元。
- 告訴片元着色器你使用的那個紋理單元。
根據需要重複多個紋理。
進一步練習
一旦您做到這兒,您就完成的差不多了!作爲下一個練習,嘗試通過加載另一個紋理,將其綁定到另一個單元,並在着色器中使用它。
回顧
現在我們回顧一下所有的着色器代碼,以及我們添加了一個新的功能用來從資源目錄讀取着色器代碼,而不是存儲在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上的項目站點下載本課程的完整源代碼。