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上的项目站点下载本课程的完整源代码

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