本文从下面链接翻译过来:
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上的项目站点下载本课程的完整源代码。