OpenGL ES SDK for Android - 2

Asset Loading

使用Open Asset Importer将模型加载到OpenGL ES中。

Using external libraries in Android

为了使用Open Asset Importer库,我们首先需要为Android构建它,然后将其作为构建过程的一部分进行引用。 Open Asset Importer库使用基于CMake的构建系统,可以轻松地为大多数平台构建。

要使用这个预构建的库,我们必须告诉CMake在哪里可以找到构建的库及其头文件。 用于导入库的CMake代码如下所示:

set(FF ${CMAKE_CURRENT_SOURCE_DIR}/libs/${ANDROID_ABI})
add_library(assimp SHARED IMPORTED)
set_target_properties(assimp PROPERTIES IMPORTED_LOCATION ${FF}/libassimp.so)
target_link_libraries(assimp)
复制代码

Using the Open Asset Importer library.

Open Asset Importer可以以各种不同的格式加载模型存储。 它将这些转换为标准的内部格式,我们可以以一致的方式访问它。 不同的模型格式可以存储各种不同的功能,例如:

  • Geometry
  • Normals
  • Textures
  • Materials
  • Bones
  • Animation

Open Asset Importer将几何图形加载到一个或多个网格中,并存储索引到几何图形中的面列表。 这与OpenGL ES与glDrawElements的工作方式非常相似,为OpenGL ES提供了所需顶点的列表,然后在该列表中为其绘制多边形的索引列表。

我们将在setupGraphics函数中加载模型。 首先,我们将数据传递给Open Asset Importer:

std::string sphere = "s 0 0 0 10";
scene = aiImportFileFromMemory(sphere.c_str(), sphere.length(), 0, ".nff");
if(!scene)
{
    LOGE("Open Asset Importer could not load scene. \n");
    return false;
}
复制代码

Open Asset Importer能够直接加载模型文件,但是,因为从本地代码加载Android上的文件并非易事,我们使用的是缓冲区。 我们定义一个表示模型文件的缓冲区,将其传递给Open Asset Importer并附上提示,告诉它我们正在使用哪种文件格式。 这里我们使用Neutral File Format。 该特定缓冲区表示半径为10的原点(0 0 0)处的球体。

在我们将文件加载到Open Asset Importer之后,我们只需将网格中的所有顶点提取到数组中,然后将所有索引提取到另一个中。

int vertices_accumulation = 0;
/* Go through each mesh in the scene. */
for (int i = 0; i < scene->mNumMeshes; i++)
{
    /* Add all the vertices in the mesh to our array. */
    for (int j = 0; j < scene->mMeshes[i]->mNumVertices; j++)
    {
        const aiVector3D& vector = scene->mMeshes[i]->mVertices[j];
        vertices.push_back(vector.x);
        vertices.push_back(vector.y);
        vertices.push_back(vector.z);
    }
    /*
     * Add all the indices in the mesh to our array.
     * Indices are listed in the Open Asset importer relative to the mesh they are in.
     * Because we are adding all vertices from all meshes to one array we must add an offset
     * to the indices to correct for this.
     */
    for (unsigned int j = 0 ; j < scene->mMeshes[i]->mNumFaces ; j++)
    {
        const aiFace& face = scene->mMeshes[i]->mFaces[j];
        indices.push_back(face.mIndices[0] + vertices_accumulation);
        indices.push_back(face.mIndices[1] + vertices_accumulation);
        indices.push_back(face.mIndices[2] + vertices_accumulation);
    }
    /* Keep track of number of vertices loaded so far to use as an offset for the indices. */
    vertices_accumulation += scene->mMeshes[i]->mNumVertices;
}
复制代码

然后我们将这些数组传递给renderFrame函数中的OpenGL ES:

glUseProgram(glProgram);
/* Use the vertex data loaded from the Open Asset Importer. */
glVertexAttribPointer(vertexLocation, 3, GL_FLOAT, GL_FALSE, 0, &vertices[0]);
glEnableVertexAttribArray(vertexLocation);
/* We're using vertices as the colour data here for simplicity. */
glVertexAttribPointer(vertexColourLocation, 3, GL_FLOAT, GL_FALSE, 0, &vertices[0]);
glEnableVertexAttribArray(vertexColourLocation);
glUniformMatrix4fv(projectionLocation, 1, GL_FALSE, projectionMatrix);
glUniformMatrix4fv(modelViewLocation, 1, GL_FALSE, modelViewMatrix);
/* Use the index data loaded from the Open Asset Importer. */
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_SHORT, &indices[0]);
复制代码

既然你已经拥有它,现在不用手动指定你的模型,你可以从你最喜欢的建模软件加载它们。 这意味着您可以在3D建模应用程序中创建,编辑和微调模型,然后直接导入到您的应用程序中。 Open Asset Importer支持大约40种不同的导入格式,包括所有最流行的格式,因此它应该涵盖您可能想要使用的大多数资产。

Vertex Buffer Objects

如何使用Vertex Buffer Objects(顶点缓冲区对象)来减少应用程序中的带宽。

与桌面相比,移动设备的资源有限。 必须考虑的最重要的考虑因素之一是应用程序将使用的带宽量。 可以显着减少带宽占用的一种方法是使用称为顶点缓冲对象(VBO)的东西。 在前面的所有示例中,我们都在主内存中定义了顶点位置,顶点颜色和顶点法线等属性。 然后在每个帧中我们将它们上传到GPU,以便它可以渲染您的场景。 如果我们只能上传所有这些数据而不用保存它,那不是很好吗? 这样你就可以大大减少带宽,因为每帧都不会再将数据发送到GPU。 好消息是有一种方法可以做到这一点,那就是使用VBO。

Generating the buffers

我们需要做的第一件事是创建缓冲区来保存我们的数据。 创建顶点缓冲区的方法与在Texture Cube示例中创建纹理对象相同。 首先,您需要定义一个整数数组,它将保存缓冲区对象的ID。 在我们的例子中,我们将定义2个,一个用于我们所有与顶点相关的数据,一个用于我们的索引。

static GLuint vboBufferIds[2];
复制代码

现在我们需要给我们的整数有效值,因为它们尚未初始化。 为此,我们需要为setupGraphics函数添加一个新的函数调用。 这个函数叫做glGenBuffers,就像glGenTextures一样工作。 第一个参数是要创建的缓冲区数,第二个参数是指向将保存缓冲区对象的ID的整数数组的指针。

glGenBuffers(2, vboBufferIds);
glBindBuffer(GL_ARRAY_BUFFER, vboBufferIds[0]);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboBufferIds[1]);
复制代码

下一阶段是我们需要将新创建的ID绑定到适当的目标。 有两种不同的目标类型:GL_ARRAY_BUFFER和GL_ELEMENT_ARRAY_BUFFER。 它们分别用于顶点和索引。 如果您没有在代码中调用上面的绑定缓冲区函数,OpenGL ES假定您不想使用VBO,并希望您每次都上传信息。 注意,如果你想在应用程序中使用VBO后再回到这种工作方式,那么你需要用0调用glBindBuffer函数。这个例子如下所示:

glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
复制代码

现在我们已经绑定了两个缓冲区,我们需要分配空间来填充数据。 我们使用一个名为glBufferData的函数来完成此操作。 glBufferData需要4个参数作为目标,需要与上面的函数的大小,缓冲区的数据以及缓冲区的使用方式相同。

glBufferData(GL_ARRAY_BUFFER, vertexBufferSize, cubeVertices, GL_STATIC_DRAW);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, elementBufferSize, indices, GL_STATIC_DRAW);
复制代码

第一个有趣的参数是大小。 这里我们将它设置为vertexBufferSize,它在程序的前面定义。 为方便起见,它显示在下面:

static GLushort vertexBufferSize = 48 * 3 * sizeof (GLfloat);
复制代码

我们将此变量设置为48,然后将此数字乘以sizeof(GLfloat)的3倍,以得到我们需要的缓冲区大小(以字节为单位)。这是因为每个顶点位置都由3个GLfloat组件X,Y和Z定义。现在在前面的立方体示例中我们只有24个顶点,但我们将此数字设置为48,这是我们的顶点位置数量的两倍一直在使用。我们的顶点位置通常定义为24,因为我们使用4个顶点位置来定义立方体的每个面。但是,我们正在组合顶点颜色数据,每个面也有4个值。我们通过使用称为stride的东西来做到这一点,这将在后面的部分中进行深入解释。请注意,可以将每个顶点定义的任何内容包含在一个缓冲区中。我们没有的原因是在这个例子中我们只处理位置和颜色。另外需要注意的是,如果你想要,你仍然可以将所有阵列分开并创建大量的VBO。类似地,如果你想回到前面的例子,只是创建一个数组而不是多个数组,那么这同样有效。

回到glBufferData,只有一个奇数参数,这是最后一个。 这简单地为OpenGL ES提供了打算如何使用缓冲区的提示。 有三个选项:GL_STATIC_DRAW,GL_DYNAMIC_DRAW和GL_STREAM_DRAW。 第一个意味着您只需修改一次数据,然后它将被多次使用。 这是我们想要的选项,因为我们的几何体在场景中不会改变。 第二个意味着您将多次修改缓冲区,然后使用它多次绘制。 最后一个选项意味着你将再次只修改一次,但这次你说你只需要几次。 值得注意的是,这只是一个提示,您完全有权使用GL_STATIC_DRAW并根据需要修改数据。

我们对indices做了完全相同的事情。 唯一的区别是我们使用GL_ELEMENT_ARRAY_BUFFER而不是GL_ARRAY_BUFFER,而elementBufferSize是36乘以GLushort,因为我们将绘制12个trianges,每个有3个indicies,我们定义的indicies的类型是GLushort。

static GLushort elementBufferSize = 36 * sizeof(GLushort);
复制代码

Changing the Way we Store Data

在前面的部分中,我们提到过我们要将所有的每个顶点信息添加到一个VBO中。 我们这样做的方式是交错数据。 这意味着我们首先有一个顶点位置,然后是与该位置相关联的颜色。 然后我们放置第二个顶点位置。 如果您使用更多的每个顶点属性,那么您也需要交错。 因此,在Lighting示例中,我们将编写一个顶点位置,后跟一个颜色位置,后跟一个顶点法线。 下面的代码可以帮助您更好地可视化。

static GLfloat cubeVertices[] = { -1.0f,  1.0f, -1.0f,  /* Back Face First Vertex Position */
                            1.0f, 0.0f, 0.0f,           /* Back Face First Vertex Colour */
                            1.0f,  1.0f, -1.0f,         /* Back Face Second Vertex Position */
                            1.0f, 0.0f, 0.0f,           /* Back Face Second Vertex Colour */
                            -1.0f, -1.0f, -1.0f,        /* Back Face Third Vertex Position */
                            1.0f, 0.0f, 0.0f,           /* Back Face Third Vertex Colour */
                            1.0f, -1.0f, -1.0f,         /* Back Face Fourth Vertex Position */
                            1.0f, 0.0f, 0.0f,           /* Back Face Fourth Vertex Colour */
                            -1.0f,  1.0f,  1.0f,        /* Front. */
                            0.0f, 1.0f, 0.0f,
                            1.0f,  1.0f,  1.0f,
                            0.0f, 1.0f, 0.0f,
                            -1.0f, -1.0f,  1.0f,
                            0.0f, 1.0f, 0.0f,
                            1.0f, -1.0f,  1.0f,
                            0.0f, 1.0f, 0.0f,
                            -1.0f,  1.0f, -1.0f,        /* Left. */
                            0.0f, 0.0f, 1.0f,
                            -1.0f, -1.0f, -1.0f,
                            0.0f, 0.0f, 1.0f,
                            -1.0f, -1.0f,  1.0f,
                            0.0f, 0.0f, 1.0f,
                            -1.0f,  1.0f,  1.0f,
                            0.0f, 0.0f, 1.0f,
                            1.0f,  1.0f, -1.0f,         /* Right. */
                            1.0f, 1.0f, 0.0f,
                            1.0f, -1.0f, -1.0f,
                            1.0f, 1.0f, 0.0f,
                            1.0f, -1.0f,  1.0f,
                            1.0f, 1.0f, 0.0f,
                            1.0f,  1.0f,  1.0f,
                            1.0f, 1.0f, 0.0f,
                            -1.0f, -1.0f, -1.0f,         /* Top. */
                            0.0f, 1.0f, 1.0f,
                            -1.0f, -1.0f,  1.0f,
                            0.0f, 1.0f, 1.0f,
                            1.0f, -1.0f,  1.0f,
                            0.0f, 1.0f, 1.0f,
                            1.0f, -1.0f, -1.0f,
                            0.0f, 1.0f, 1.0f,
                            -1.0f,  1.0f, -1.0f,         /* Bottom. */
                            1.0f, 0.0f, 1.0f,
                            -1.0f,  1.0f,  1.0f,
                            1.0f, 0.0f, 1.0f,
                            1.0f,  1.0f,  1.0f,
                            1.0f, 0.0f, 1.0f,
                            1.0f,  1.0f, -1.0f,
                            1.0f, 0.0f, 1.0f,
};
复制代码

Changes in glVertexAttribPointer

现在我们需要用glVertexAttribPointer来解决两个问题。 首先,现在我们每次都使用VBO而不是发送数据。 因此,不需要发送指向顶点的指针,因为我们不再使用该数据。 真正我们想要做的是将值更改为我们的VBO中可以找到数据的偏移量。 我们遇到的第二个问题是我们现在在另一个顶点位置旁边没有顶点位置。 我们有一种颜色,所以我们需要告诉OpenGL,下一个相关组件在数组中。

glVertexAttribPointer(vertexLocation, 3, GL_FLOAT, GL_FALSE, strideLength, 0);
glEnableVertexAttribArray(vertexLocation);
glVertexAttribPointer(vertexColourLocation, 3, GL_FLOAT, GL_FALSE, strideLength, (const void *) vertexColourOffset);
glEnableVertexAttribArray(vertexColourLocation);
复制代码

上面代码中的第一行处理我们的顶点位置。 不是将数组传递给最终参数作为值,而是将它设置为0.这是因为VBO中的第一个元素是顶点元素,因此不需要偏移量。 如果你看一下与颜色相关的glVertexAttribPointer函数调用,你会发现我们使用的是vertexColourOffset。 注意,仅针对编译器类型检查,我们需要将其转换为const void *。 vertexColourOffset的值是3 * sizeof(GLfloat),如下所示:

static GLushort vertexColourOffset = 3  * sizeof (GLfloat);
复制代码

原因是我们需要以字节为单位定义VBO的偏移量。 现在颜色前面有一个顶点位置,每个顶点位置有3个GLfloat组件(X,Y和Z)。 第二个问题是由一个叫做stride的东西来处理的。 这基本上是第一个组件中第一个元素和下一个组件中第一个元素之间的字节数,定义如下:

static GLushort strideLength = 6 * sizeof(GLfloat);
复制代码

值为6乘以GLfloat的大小。 这样做的原因是如果值不像前面的例子那样是0,它应该是组件中第一个元素的值与下一个组件中第一个元素之间的字节差异,因为在我们的例子中我们有XYZRGBXYZ.第一个元素的X和第二个元素的X之间有6个GLfloats的明显间隙。 颜色具有相同的stride长度,再次是6乘以第一R颜色分量和第二R颜色分量之间的GLfloat的尺寸。

Changes in glDrawElements

我们将对glVertexAttrib指针执行相同的操作,因为我们不再需要传递数组。 相反,我们只需要在VBO中提供一个偏移量。 在这种情况下,因为我们想要绘制所有元素(从开始开始),此偏移量为0。

glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_SHORT, 0);
复制代码

Android File Loading

如何将资产打包到apk并从文件系统加载文件。

您经常需要将文件中的资源加载到游戏或应用程序中。 如果您一直使用建模软件生成内容,这些资产可能是某些模型的纹理形式,甚至是模型本身。

  • 私人位置,只能由您的应用程序访问。
  • 可由不同应用程序共享的公共位置。
  • 不应用于永久存储的临时位置。

然后展示如何将字符串传递到应用程序的本地端,显示已提取的文件的位置,然后在应用程序的本地端打开它们。

Saving Preferences and Settings in your Application

在标准游戏或应用程序中,通常希望在正在运行的应用程序的实例之间保存一些持久状态。 这样的示例将是用户设置或偏好,诸如正在使用的图像的质量,游戏的难度,或甚至应用程序应该将内容保存到的默认目录。 Android提供了一种非常简单的Java机制。 对于我们的示例,我们将简单地存储应用程序在其生命周期中运行的次数。

SharedPreferences savedValues = getSharedPreferences("savedValues", MODE_PRIVATE);
int programRuns = savedValues.getInt("programRuns", 1);
Log.d(LOGTAG, "This application has been run " + programRuns + " times");
programRuns++;
SharedPreferences.Editor editor = savedValues.edit();
editor.putInt("programRuns", programRuns);
editor.commit();
复制代码

Locations of the Public, Private and Cache directories

根据您要使用的文件和信息,您的文件和信息应该有不同的位置。 根据Android版本,有时可以存储数据的位置也会发生变化。 因此,有一些特殊方法可以获取公共,私有和缓存目录的位置。

String privateAssetDirectory = getFilesDir().getAbsolutePath();
String publicAssetDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
String cacheAssetDirectory = getCacheDir().getAbsolutePath();
Log.i(LOGTAG, "privateAssetDirectory's path is equal to: " + privateAssetDirectory);
Log.i(LOGTAG, "publicAssetDirectory's path is equal to: " + publicAssetDirectory);
Log.i(LOGTAG, "cacheAssetDirectory's path is equal to: " + cacheAssetDirectory);
复制代码

第一行是您将在大多数时间使用的目录。 这是为了获取您的私人数据的位置。 通常您不希望其他应用程序使用您的资产,因此这是您应该使用的资产。 但请注意,如果用户在其设备上拥有root权限,他们仍然可以通过shell访问该位置。 存储私有文件的当前位置是/ data / data / PackageName / files。

第二行为您提供公共目录的位置。 您需要为getExternalStoragePublicDirectory函数提供要获取的公共目录的名称。 无论您是想获取音乐目录,下载目录还是照片目录,都有许多不同的选择。 在这种情况下,我们刚刚选择了下载目录作为示例。 您可能想要公开的原因是您有其他应用程序应该能够访问的数据。 一个例子是你希望其他音乐播放器能够播放的音乐。 存储公共下载文件的当前位置是/ mnt / sdcard / Download。

第三行为您提供应用程序缓存目录的位置。 您应该只在此处放置临时文件,因为用户可以在需要时删除缓存,如果内存不足,Android也会删除缓存文件夹。 应用程序缓存目录的当前位置是/ data / data / PackageName / cache。

Adding Files to an Apk and Extracting them out

TextureLoading中我们现在已经将像纹理这样的资源直接嵌入到源代码中。 这适用于非常小的纹理,但是现实生活中的应用程序将具有多种纹理,并且它们也将非常大。 我们可以将这些文件与应用程序分开,并期望用户手动将文件推送到设备上,但这样就不会给用户提供任何保护,这是最终用户必须执行的另一个步骤工作,你想尽可能减少这些。

解决此问题的一种方法是将资产包含在apk文件中。 这样,该文件将通过apk进行压缩,并始终可用。 将资产放置在apk中非常简单。 如果查看项目目录结构,您将看到有一个名为assets的文件夹。 您在此文件夹中放置的所有文件都将在构建时压缩到apk中。 在本教程中,我们将添加三个文本文件,下面是文件名称及其内容的列表:

privateApplicationFile.txt:这是私有应用程序文件。 只有此应用程序才能使用其内容。 publicApplicationFile.txt:这是存储在公共场所的文件。 这意味着除此之外的其他应用程序可以访问它。 cacheApplicationFile.txt:这是一个可以在缓存中找到的文件。 这意味着它不能保证在那里,因为系统可以在内存不足时删除它。

更难的问题是从apk中提取文件。

String privateApplicationFileName = "privateApplicationFile.txt";
String publicApplicationFileName = "publicApplicationFile.txt";
String cacheApplicationFileName = "cacheApplicationFile.txt";
extractAsset(privateApplicationFileName, privateAssetDirectory);
extractAsset(publicApplicationFileName, publicAssetDirectory);
extractAsset(cacheApplicationFileName, cacheAssetDirectory);
复制代码

首先,我们定义了三个字符串,它们包含我们想要从apk中获取的三个文件的名称。 然后我们调用一个名为extractAsset的函数,稍后我们将定义该函数,它接收文件名和我们要将文件解压缩到的目录。

private void extractAsset(String assetName, String assetPath)
{
    File fileTest = new File(assetPath, assetName);

    if(fileTest.exists())
    {
        Log.d(LOGTAG, assetName +  " already exists no extraction needed\n");
    }
    else
    {
        Log.d(LOGTAG, assetName + " doesn't exist extraction needed \n");
/* [extractAssetBeginning] */
/* [extractAssets] */
        try
        {
            RandomAccessFile out = new RandomAccessFile(fileTest,"rw");
            AssetManager am = getResources().getAssets();

            InputStream inputStream = am.open(assetName);
            byte buffer[] = new byte[1024];
            int count = inputStream.read(buffer, 0, 1024);

            while (count > 0)
            {
                out.write(buffer, 0, count);
                count = inputStream.read(buffer, 0, 1024);
            }
            out.close();
            inputStream.close();
        }
  /* [extractAssets] */
        /*  [extractAssetsErrorChecking] */
        catch(Exception e)
        {
            Log.e(LOGTAG, "Failure in extractAssets(): " + e.toString() + " " + assetPath+assetName);
        }
        if(fileTest.exists())
        {
            Log.d(LOGTAG,"File Extracted successfully");
        }
        /*  [extractAssetsErrorChecking] */
    }
}
复制代码

Using files and Passing Strings to the Native Side of the Application

这一切都很好,能够将文件提取到Android设备上的不同位置,但如果我们无法从应用程序的本地部分访问它们,则它无用。 作为应用程序中所有图形的本地部分。

我们选择绕过这个的方法是将要打开的文件的路径作为字符串传递给本地端,以便您可以使用C直接打开它们。

NativeLibrary.init(privateAssetDirectory + "/" + privateApplicationFileName, publicAssetDirectory + "/" +  publicApplicationFileName, cacheAssetDirectory + "/" + cacheApplicationFileName);
复制代码

我们的init函数现在需要3个字符串参数来表示我们刚刚提取的每个文件。 每个字符串都需要获取目录,然后是文件。 因为在Java中连接字符串更容易,所以我们选择在此处执行此操作而不是在C.现在您可能还记得我们的init函数最初没有采用任何参数,因此我们现在需要打开NativeLibrary.java文件并更新我们的函数原型。

 public static native void init(String privateFile, String publicFile, String cacheFile);
复制代码

现在我们需要更新本地端的函数原型和定义。 现在在C中我们将Java字符串视为jstrings,这就是我们需要在函数中解决它们。

Java_com_zpw_audiovideo_arm_GraphicsSetup_NativeLibrary_init(JNIEnv * env, jobject obj, jstring privateFile, jstring publicFile, jstring cacheFile)
复制代码

为了使用Java字符串,我们需要将它们转换为cstrings。 我们使用名为GetStringUTFChars的函数来做到这一点。 一旦我们完成它们,我们需要使用ReleaseStringUTFChars再次释放它们。

const char* privateFileC = env->GetStringUTFChars(privateFile, NULL);
const char* publicFileC = env->GetStringUTFChars(publicFile, NULL);
const char* cacheFileC = env->GetStringUTFChars(cacheFile, NULL);

readFile(privateFileC, PRIVATE_FILE_SIZE);
readFile(publicFileC, PUBLIC_FILE_SIZE);
readFile(cacheFileC, CACHE_FILE_SIZE);

env->ReleaseStringUTFChars(privateFile, privateFileC);
env->ReleaseStringUTFChars(publicFile, publicFileC);
env->ReleaseStringUTFChars(cacheFile, cacheFileC);
复制代码

readFile函数是我们将在下一节中创建的函数。 为了使代码保持最小,我们手动将PRIVATE_FILE_SIZE,PUBLIC_FILE_SIZE和CACHE_FILE_SIZE定义为每个文件的大小。 这些分别是82,105和146。 读取文件功能就像任何其他C文件读取实现一样。 为方便起见,以下提供:

void readFile(const char * fileName, int size)
{
    FILE * file = fopen(fileName, "r");
    char * fileContent =(char *) malloc(sizeof(char) * size);
    if(file == NULL)
    {
        LOGE("Failure to load the file");
        return;
    }
    fread(fileContent, size, 1, file);
    LOGI("%s",fileContent);
    free(fileContent);
    fclose(file);
}
复制代码

Mipmapping and Compressed Textures

本教程介绍了mipmapping和压缩纹理的概念。

带宽是开发移动设备时需要解决的主要问题之一。 与桌面相比,带宽是一种有限的资源,是许多桌面开发人员都在努力解决的问题。 现在,你不再拥有每秒100个千兆位的数字,而是将其减少到大约5的更适中的数字。此外,带宽是电池电量的严重消耗的原因。 这些是您希望尽可能减少它的一些原因。 将通过两种不同的带宽节省技术:Mipmapping和压缩纹理。

The Idea of Mipmapping

假设您已为应用程序上传了一系列纹理。 您希望使用相当高质量的纹理,因此每个纹理至少为512 x 512像素。 问题是,有时您使用此纹理的对象可能不是屏幕上的512 x 512像素。 事实上,如果对象很远,那么它甚至可能不是100像素的可能性。目前,OpenGL ES将采用512像素纹理并尝试在运行时将其适合100个像素。 您当然可以选择以质量或速度来优化此选择,但您仍然必须将整个纹理发送到GPU。

如果您可以在512 x 512,256 x 256,128 x 128等一系列尺寸中使用相同的纹理,那会不会很好。 所有这些都是离线生产的,因此它们可以达到最佳质量。 然后,OpenGL ES只使用最接近对象的实际大小。 这正是mipmapping的功能,不仅可以从场景中删除不必要的带宽,还可以提高场景的质量。

这是OpenGL ES的一个重要特性,它为您提供了一个自动为您生成mipmap的功能。 注意,要使用它,纹理的宽度和高度必须是2的幂。您唯一需要提供的是您正在使用的目标。 那么您使用的唯一一个是GL_TEXTURE_2D。

void glGenerateMipmap(GLenum target)
复制代码

本教程的其余部分假定您要手动加载mipmapped纹理。

New Texture Function

首先让我们看看纹理文件中的纹理上传功能:

void loadTexture( const char * texture, unsigned int level, unsigned int width, unsigned int height)
{
    GLubyte * theTexture;
    theTexture = (GLubyte *)malloc(sizeof(GLubyte) * width * height * CHANNELS_PER_PIXEL);
    FILE * theFile = fopen(texture, "r");
    if(theFile == NULL)
    {
        LOGE("Failure to load the texture");
        return;
    }
    fread(theTexture, width * height * CHANNELS_PER_PIXEL, 1, theFile);
    /* Load the texture. */
    glTexImage2D(GL_TEXTURE_2D, level, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, theTexture);
    /* Set the filtering mode. */
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST_MIPMAP_NEAREST);
    free(theTexture);
}
复制代码

我们修改了函数以获取纹理的文件名以及等级,宽度和高度。 这样做是因为我们需要使用相同的函数加载一系列纹理。 在手动mipmapping定义纹理时,需要在glTexImage2d调用中使用level参数。 您在上面的示例中为512 x 512的基本纹理称为0级。这就是为什么在此之前我们始终提供0作为第二个参数。

OpenGL ES期望纹理中的每个级别都是原始级别的一半。 因此级别0是512 x 512,级别1是256 x 256,级别2是128 x 128等。这必须包含1 x 1纹理。 如果你没有提供所有级别,那么OpenGL ES认为纹理不完整,它会产生错误。

另外需要注意的是,我们现在已经更改了glTexParameters以包含mipmapping。 我们使用GL_NEAREST_MIPMAP_NEAREST。 这意味着它使用最接近对象大小的mipmapping级别,而不在两者之间进行任何插值,并且还使用最接近纹理中所需像素的像素,同样没有插值。 为了获得更好看的图像,您可以使用GL_LINEAR_MIPMAP_LINEAR,但是您将牺牲性能。

Loading Textures

glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
/* Generate a texture object. */
glGenTextures(2, textureIds);
/* Activate a texture. */
glActiveTexture(GL_TEXTURE0);
/* Bind the texture object. */
glBindTexture(GL_TEXTURE_2D, textureIds[0]);
/* Load the Texture. */
loadTexture("/data/data/com.zpw.audiovideo/files/level0.raw", 0, 512, 512);
loadTexture("/data/data/com.zpw.audiovideo/files/level1.raw", 1, 256, 256);
loadTexture("/data/data/com.zpw.audiovideo/files/level2.raw", 2, 128, 128);
loadTexture("/data/data/com.zpw.audiovideo/files/level3.raw", 3, 64, 64);
loadTexture("/data/data/com.zpw.audiovideo/files/level4.raw", 4, 32, 32);
loadTexture("/data/data/com.zpw.audiovideo/files/level5.raw", 5, 16, 16);
loadTexture("/data/data/com.zpw.audiovideo/files/level6.raw", 6, 8, 8);
loadTexture("/data/data/com.zpw.audiovideo/files/level7.raw", 7, 4, 4);
loadTexture("/data/data/com.zpw.audiovideo/files/level8.raw", 8, 2, 2);
loadTexture("/data/data/com.zpw.audiovideo/files/level9.raw", 9, 1, 1);
复制代码

现在可以在setupGraphics函数中找到此代码。 我们需要提取纹理的生成,因为每个mipmap级别需要引用相同的纹理ID。 我们正在生成两个ID,因为稍后我们将使用第二个ID作为压缩纹理。 如您所见,我们将loadTexture函数调用10次,每个级别使用不同的纹理。 如前所述,我们一直降低到大小为1 x 1的纹理。

Adjusting Other Parts of the Code

对于本教程,我们不会使用旋转立方体而是使用单个正方形,它可以移动得更远,更靠近屏幕。 随着方块在屏幕上变大,mipmap级别将更改为更合适的大小纹理。 因此,我们可以删除很多索引,纹理坐标和顶点代码。

GLfloat squareVertices[] = { -1.0f,  1.0f,  1.0f,
                           1.0f,  1.0f,  1.0f,
                          -1.0f, -1.0f,  1.0f,
                           1.0f, -1.0f,  1.0f,
                         };
GLfloat textureCords[] = { 0.0f, 1.0f,
                           1.0f, 1.0f,
                           0.0f, 0.0f,
                           1.0f, 0.0f,
};
GLushort indicies[] = {0, 2, 3, 0, 3, 1};
复制代码

当我们将物体移动得更远时,我们需要在matrixPerspective调用中调整Zfar距离。 否则,在显示某些mipmap级别之前,对象将被剪切。

matrixPerspective(projectionMatrix, 45, (float)width / (float)height, 0.1f, 170);
复制代码

我们还需要在本教程中包含一些新的全局变量。 由于我们已经移除了所有旋转,因此不需要角度全局。 相反,我们需要添加:全局距离,显示增加或减少距离的速度全局,以及用作切换的整数,显示我们是否使用压缩纹理。

float distance = 1;
float velocity = 0.1;
GLuint textureModeToggle = 0;
复制代码

现在我们需要编辑translate函数以考虑我们的新距离变量并删除旋转函数,因为角度不再存在。

matrixTranslate(modelViewMatrix, 0.0f, 0.0f, -distance);
复制代码

最后的调整是关于我们如何在帧的末尾移动对象。 我们提供了一系列可接受的值,一旦距离超出这些值,我们就会切换是否使用压缩纹理和速度符号来使对象沿相反方向移动。 请注意我们如何将采样器位置从0更改为压缩。

glUniform1i(samplerLocation, textureModeToggle);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indicies);
distance += velocity;
if (distance > 160 || distance < 1)
{
    velocity *= -1;
    textureModeToggle = !textureModeToggle;
}
复制代码

Compressed Textures

您可能有一个要发送给同事的大文件。 发送此文件的一个明智的步骤是首先压缩它,也许压缩成zip格式,这样可以大大减小文件大小。 纹理也是如此。 我们可以为OpenGL ES提供压缩版本的纹理,通过节省大量带宽,可以比未压缩版本具有更小的文件大小。

我们将在此示例中使用的压缩格式是Ericsson Texture Compression或ETC. 还有其他可以选择,但这个可以在大多数GPU上运行。 使用OpenGL ES 3.0以及新的压缩格式进入未来将是ASTC。 这可以提供比ETC更大的压缩结果,但并非所有设备都支持它,所以我们在本教程中坚持使用ETC。

Generating ETC Compressed Textures

第一项工作是压缩纹理。 有许多工具可供您使用,但我们使用Mali纹理压缩工具。 不幸的是,许多软件不支持打开.raw文件,因为格式不包括图像的宽度和高度。 因此,建议您将图像转换为更常见的格式:bmp,png或jpeg工作正常。

在工具中打开纹理,然后单击压缩选定的图像。 应出现一个对话框。 将选项卡更改为ETC1 / ETC2。 确保选择PKM,slow,ETC1和perceptual,然后按确定。 然后,该工具应为您选择的每个纹理生成.pkm文件。 这些是您将要使用的压缩纹理。 如果您不想手动生成mipmap级别,此工具还可以为您生成mipmap。 您所要做的就是给它原始纹理,选择一种压缩方法,然后选中生成mipmap的方框。 请注意对话框中还有一个ASTC选项卡。 该工具还支持使用新的ASTC标准压缩纹理。

如果查看已生成的pkm文件,它们应该比原始文件小得多。

Compressed Texture Loading Function

现在我们需要将压缩的纹理加载到OpenGL ES中。 为此,我们生成了一个新的纹理加载函数。

void loadCompressedTexture( const char * texture, unsigned int level)
{
    GLushort paddedWidth;
    GLushort paddedHeight;
    GLushort width;
    GLushort height;
    GLubyte textureHead[16];
    GLubyte * theTexture;
    FILE * theFile = fopen(texture, "rb");
    if(theFile == NULL)
    {
        LOGE("Failure to load the texture");
        return;
    }
    fread(textureHead, 16, 1, theFile);
    paddedWidth = (textureHead[8] << 8) | textureHead[9];
    paddedHeight = (textureHead[10] << 8) | textureHead[11];
    width = (textureHead[12] << 8) | textureHead[13];
    height = (textureHead[14] << 8) | textureHead[15];
    theTexture = (GLubyte *)malloc(sizeof(GLubyte) * ((paddedWidth * paddedHeight) >> 1));
    fread(theTexture, (paddedWidth * paddedHeight) >> 1, 1, theFile);
    /* Load the texture. */
    glCompressedTexImage2D(GL_TEXTURE_2D, level, GL_ETC1_RGB8_OES, width, height, 0, (paddedWidth * paddedHeight) >> 1, theTexture);
    /* Set the filtering mode. */
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST_MIPMAP_NEAREST);
    free(theTexture);
    fclose(theFile);
}
复制代码

不幸的是,这需要更复杂,因为我们不再使用简单的原始格式。 ETC文件格式有一个16字节的头,我们首先加载它并放入一个名为textureHead的新指针中。 16字节头的格式如下所示:

前6个字节是文件格式的名称和文件的版本。 我们不需要担心这一点,因为我们感兴趣的是padded width,padded height,宽度和高度。 每个都存储在2个字节以上。 出于这个原因,我们通过将最高有效字节移位8位,然后使用按位或者将它与最低有效字节组合,将它们转换为无符号shorts。

padded width和height可以与实际宽度和高度不同,因为ETC一次可以在4个块上工作。 因此,如果您的宽度和高度不是4的倍数,则填充值将是四舍五入到最接近4的倍数的宽度或高度。

然后我们在malloc调用中使用此值,右移1,因为ETC为每个像素分配半个字节。 代码的最后一个变化是使用glCompressedTexImage2d而不是glTexImage2D。 参数非常相似,我们使用的内部格式的差异是GL_ETC1_RGB8_OES,它是ETC1的内部类型。 唯一的新字段是imageSize字段,我们用与上述malloc完全相同的计算填充它。 如果您想使用压缩纹理,那就是它的全部内容。

Final bits of Code.

还有一点工作要做,就是用我们生成的所有压缩纹理调用上面的函数。 我们再次使用Android文件加载教程中描述的技术将压缩纹理打包到apk中并再次提取它们。

/* Activate a texture. */
glActiveTexture(GL_TEXTURE1);
/* Bind the texture object. */
glBindTexture(GL_TEXTURE_2D, textureIds[1]);
loadCompressedTexture("/data/data/com.zpw.audiovideo/files/level0.pkm", 0);
loadCompressedTexture("/data/data/com.zpw.audiovideo/files/level1.pkm", 1);
loadCompressedTexture("/data/data/com.zpw.audiovideo/files/level2.pkm", 2);
loadCompressedTexture("/data/data/com.zpw.audiovideo/files/level3.pkm", 3);
loadCompressedTexture("/data/data/com.zpw.audiovideo/files/level4.pkm", 4);
loadCompressedTexture("/data/dacom.zpw.audiovideo/files/level5.pkm", 5);
loadCompressedTexture("/data/data/com.zpw.audiovideo/files/level6.pkm", 6);
loadCompressedTexture("/data/data/com.zpw.audiovideo/files/level7.pkm", 7);
loadCompressedTexture("/data/data/com.zpw.audiovideo/files/level8.pkm", 8);
loadCompressedTexture("/data/data/com.zpw.audiovideo/files/level9.pkm", 9);
复制代码

注意我们如何使用GL_TEXTURE1调用glActiveTexture,然后将第二个纹理ID绑定到它。 这意味着我们在GL_TEXTURE0上拥有所有原始纹理,在GL_TEXTURE1上拥有所有压缩纹理。 我们提到过我们有一个称为压缩的变量,它在帧中被切换。 这意味着我们可以使用此值来确定要加载的纹理单元,就像我们之前在glUniform1i调用中所做的那样。 当您运行应用程序时,您应该看到一个更远的距离。 您应该看到的第一个纹理上有一个0。 然后纹理应该计数到5.每个数字都响应OpenGL ES当前使用的mipmap级别。 然后,正方形应该再次靠近,从5倒数到0.这就是压缩纹理和mipmapping的全部内容,使用这些新技术来减少所有应用程序的带宽。

Projected Lights

使用OpenGL ES 3.0實現投射光效果。

投影光效果:投影期間投影光的方向會發生變化。

该应用程序显示投射的灯光效果。 调整了聚光灯效果以显示纹理而不是正常的浅色。 还有一种阴影贴图技术,通过应用一些阴影使场景更逼真。

投影灯效果通过两个基本步骤实现,如下所述:

  • 计算阴影贴图。
    • 从聚光灯的角度渲染场景。
    • 结果存储在深度纹理中,称为阴影贴图。
    • 阴影贴图将在后续步骤中用于验证片段是应该被聚光灯照亮还是应该被阴影遮挡。
  • 场景渲染。
    • 从相机的角度渲染场景(由一个平面组成,其上放置一个立方体)。
    • 实现定向照明以用透视强调3D场景。
    • 实现了聚光灯效果,但是它被调整为显示纹理而不是简单的颜色。
    • 为点光源计算阴影(第一步的结果现在将被使用)。

Render geometry

在应用程序中,我们渲染一个水平放置的平面,在其上面我们放置一个立方体。 现在让我们专注于生成将要渲染的几何体。

要渲染的几何体的顶点坐标。

首先,我们需要具有构成立方体或平面形状的顶点坐标。 请注意,也会应用照明,这意味着我们也需要法线。

几何数据将被存储,然后由以下命令生成的对象使用:

/* Generate buffer objects. */
GL_CHECK(glGenBuffers(1, &renderSceneObjects.renderCube.coordinatesBufferObjectId));
GL_CHECK(glGenBuffers(1, &renderSceneObjects.renderCube.normalsBufferObjectId));
GL_CHECK(glGenBuffers(1, &renderSceneObjects.renderPlane.coordinatesBufferObjectId));
GL_CHECK(glGenBuffers(1, &renderSceneObjects.renderPlane.normalsBufferObjectId));
/* Generate vertex array objects. */
GL_CHECK(glGenVertexArrays(1, &renderSceneObjects.renderCube.vertexArrayObjectId));
GL_CHECK(glGenVertexArrays(1, &renderSceneObjects.renderPlane.vertexArrayObjectId));
复制代码

然后生成几何数据并将其复制到特定缓冲区对象。

/* Please see the specification above. */
static void setupGeometryData()
{
    /* Get triangular representation of the scene cube. Store the data in the cubeCoordinates array. */
    CubeModel::getTriangleRepresentation(&cubeGeometryProperties.coordinates,
                                         &cubeGeometryProperties.numberOfElementsInCoordinatesArray,
                                          CUBE_SCALING_FACTOR);
    /* Calculate normal vectors for the scene cube created above. */
    CubeModel::getNormals(&cubeGeometryProperties.normals,
                          &cubeGeometryProperties.numberOfElementsInNormalsArray);
    /* Get triangular representation of a square to draw plane in XZ space. Store the data in the planeCoordinates array. */
    PlaneModel::getTriangleRepresentation(&planeGeometryProperties.coordinates,
                                          &planeGeometryProperties.numberOfElementsInCoordinatesArray,
                                           PLANE_SCALING_FACTOR);
    /* Calculate normal vectors for the plane. Store the data in the planeNormals array. */
    PlaneModel::getNormals(&planeGeometryProperties.normals,
                           &planeGeometryProperties.numberOfElementsInNormalsArray);
    /* Fill buffer objects with data. */
    /* Buffer holding coordinates of triangles which make up the scene cubes. */
    GL_CHECK(glBindBuffer(GL_ARRAY_BUFFER,
                          renderSceneObjects.renderCube.coordinatesBufferObjectId));
    GL_CHECK(glBufferData(GL_ARRAY_BUFFER,
                          cubeGeometryProperties.numberOfElementsInCoordinatesArray * sizeof(GLfloat),
                          cubeGeometryProperties.coordinates,
                          GL_STATIC_DRAW));
    /* Buffer holding coordinates of normal vectors for each vertex of the scene cubes. */
    GL_CHECK(glBindBuffer(GL_ARRAY_BUFFER,
                          renderSceneObjects.renderCube.normalsBufferObjectId));
    GL_CHECK(glBufferData(GL_ARRAY_BUFFER,
                          cubeGeometryProperties.numberOfElementsInNormalsArray * sizeof(GLfloat),
                          cubeGeometryProperties.normals,
                          GL_STATIC_DRAW));
    /* Buffer holding coordinates of triangles which make up the plane. */
    GL_CHECK(glBindBuffer(GL_ARRAY_BUFFER,
                          renderSceneObjects.renderPlane.coordinatesBufferObjectId));
    GL_CHECK(glBufferData(GL_ARRAY_BUFFER,
                          planeGeometryProperties.numberOfElementsInCoordinatesArray * sizeof(GLfloat),
                          planeGeometryProperties.coordinates,
                          GL_STATIC_DRAW));
    /* Buffer holding coordinates of the plane's normal vectors. */
    GL_CHECK(glBindBuffer(GL_ARRAY_BUFFER,
                          renderSceneObjects.renderPlane.normalsBufferObjectId));
    GL_CHECK(glBufferData(GL_ARRAY_BUFFER,
                          planeGeometryProperties.numberOfElementsInNormalsArray * sizeof(GLfloat),
                          planeGeometryProperties.normals,
                          GL_STATIC_DRAW));
}
复制代码

在程序对象中,几何顶点通过属性引用,这是相当明显的。

/* ATTRIBUTES */
in vec4 vertexCoordinates; /* Attribute: holding coordinates of triangles that make up a geometry. */
in vec3 vertexNormals;     /* Attribute: holding normals. */
复制代码

这就是我们需要在负责场景渲染的程序对象中查询属性位置的原因(请注意,需要在被激活的程序对象调用以下所有函数)。

locationsStoragePtr->attributeVertexCoordinates            = GL_CHECK(glGetAttribLocation   (programObjectId, "vertexCoordinates"));
locationsStoragePtr->attributeVertexNormals                = GL_CHECK(glGetAttribLocation   (programObjectId, "vertexNormals"));
复制代码

如所见,我们仅查询坐标,而不指定立方体或平面坐标。 这是因为我们只使用一个程序对象来渲染平面和立方体。 通过使用适当的Vertex Attrib Arrays来渲染特定几何体。

/* Enable cube VAAs. */
GL_CHECK(glBindVertexArray        (renderSceneObjects.renderCube.vertexArrayObjectId));
GL_CHECK(glBindBuffer             (GL_ARRAY_BUFFER,
                                   renderSceneObjects.renderCube.coordinatesBufferObjectId));
GL_CHECK(glVertexAttribPointer    (renderSceneProgramLocations.attributeVertexCoordinates,
                                   NUMBER_OF_POINT_COORDINATES,
                                   GL_FLOAT,
                                   GL_FALSE,
                                   0,
                                   NULL));
GL_CHECK(glBindBuffer             (GL_ARRAY_BUFFER,
                                   renderSceneObjects.renderCube.normalsBufferObjectId));
GL_CHECK(glVertexAttribPointer    (renderSceneProgramLocations.attributeVertexNormals,
                                   NUMBER_OF_POINT_COORDINATES,
                                   GL_FLOAT,
                                   GL_FALSE,
                                   0,
                                   NULL));
GL_CHECK(glEnableVertexAttribArray(renderSceneProgramLocations.attributeVertexCoordinates));
GL_CHECK(glEnableVertexAttribArray(renderSceneProgramLocations.attributeVertexNormals));
/* Enable plane VAAs. */
GL_CHECK(glBindVertexArray        (renderSceneObjects.renderPlane.vertexArrayObjectId));
GL_CHECK(glBindBuffer             (GL_ARRAY_BUFFER,
                                   renderSceneObjects.renderPlane.coordinatesBufferObjectId));
GL_CHECK(glVertexAttribPointer    (renderSceneProgramLocations.attributeVertexCoordinates,
                                   NUMBER_OF_POINT_COORDINATES,
                                   GL_FLOAT,
                                   GL_FALSE,
                                   0,
                                   NULL));
GL_CHECK(glBindBuffer             (GL_ARRAY_BUFFER,
                                   renderSceneObjects.renderPlane.normalsBufferObjectId));
GL_CHECK(glVertexAttribPointer    (renderSceneProgramLocations.attributeVertexNormals,
                                   NUMBER_OF_POINT_COORDINATES,
                                   GL_FLOAT,
                                   GL_FALSE,
                                   0,
                                   NULL));
GL_CHECK(glEnableVertexAttribArray(renderSceneProgramLocations.attributeVertexCoordinates));
GL_CHECK(glEnableVertexAttribArray(renderSceneProgramLocations.attributeVertexNormals));
复制代码

现在,通过使用适当的参数调用glBindVertexArray(),我们可以控制哪个对象将要渲染(立方体或平面)。

/* Set cube's coordinates to be used within a program object. */
GL_CHECK(glBindVertexArray(renderSceneObjects.renderCube.vertexArrayObjectId));
复制代码
/* Set plane's coordinates to be used within a program object. */
GL_CHECK(glBindVertexArray(renderSceneObjects.renderPlane.vertexArrayObjectId));
复制代码

最后进行实际的绘制调用,可以通过以下方式实现:

GL_CHECK(glDrawArrays(GL_TRIANGLES, 0, cubeGeometryProperties.numberOfElementsInCoordinatesArray / NUMBER_OF_POINT_COORDINATES));
复制代码
GL_CHECK(glDrawArrays(GL_TRIANGLES, 0, planeGeometryProperties.numberOfElementsInCoordinatesArray / NUMBER_OF_POINT_COORDINATES));
复制代码

Calculate a shadow map

要计算阴影贴图,我们需要创建一个深度纹理,用于存储结果。 它是在一些基本步骤中实现的,您应该已经知道了,但让我们再次描述这一步骤。

生成纹理对象并将其绑定到GL_TEXTURE_2D目标。

GL_CHECK(glGenTextures  (1, &renderSceneObjects.depthTextureObjectId));
GL_CHECK(glBindTexture  (GL_TEXTURE_2D, renderSceneObjects.depthTextureObjectId));
复制代码

指定纹理存储数据类型。

GL_CHECK(glTexStorage2D(GL_TEXTURE_2D,
                        1,
                        GL_DEPTH_COMPONENT24,
                        shadowMapWidth,
                        shadowMapHeight));
复制代码

我们希望阴影更精确,这就是深度纹理分辨率大于普通场景大小的原因。

/* Store window size. */
windowHeight = height;
windowWidth  = width;
复制代码
/* Calculate size of a shadow map texture that will be used. */
shadowMapHeight = 3 * windowHeight;
shadowMapWidth  = 3 * windowWidth;
复制代码

设置纹理对象参数。 这里的新功能是将GL_TEXTURE_COMPARE_MODE设置为GL_COMPARE_REF_TO_TEXTURE的值,这导致r纹理坐标与当前绑定的深度纹理中的值进行比较。

GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
                         GL_TEXTURE_MIN_FILTER,
                         GL_LINEAR));
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
                         GL_TEXTURE_MAG_FILTER,
                         GL_LINEAR));
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
                         GL_TEXTURE_WRAP_S,
                         GL_CLAMP_TO_EDGE));
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
                         GL_TEXTURE_WRAP_T,
                         GL_CLAMP_TO_EDGE));
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
                         GL_TEXTURE_WRAP_R,
                         GL_CLAMP_TO_EDGE));
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
                         GL_TEXTURE_COMPARE_FUNC,
                         GL_LEQUAL));
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
                         GL_TEXTURE_COMPARE_MODE,
                         GL_COMPARE_REF_TO_TEXTURE));
复制代码

我们要实现渲染到纹理机制的下一步是:

  • 生成帧缓冲对象。
GL_CHECK(glGenFramebuffers     (1,
                               &renderSceneObjects.framebufferObjectId));
GL_CHECK(glBindFramebuffer     (GL_FRAMEBUFFER,
                                renderSceneObjects.framebufferObjectId));
复制代码
  • 将深度纹理对象绑定到帧缓冲对象的深度附属。
GL_CHECK(glFramebufferTexture2D(GL_FRAMEBUFFER,
                                GL_DEPTH_ATTACHMENT,
                                GL_TEXTURE_2D,
                                renderSceneObjects.depthTextureObjectId,
                                0));
复制代码

我们必须在渲染时使用适当的视图投影矩阵。 这里必须要提到的是,我们的聚光灯位置在渲染过程中是恒定的,但是它的方向是变化的,这意味着每帧更新聚光灯所指向的点。

lightViewProperties.projectionMatrix  = Matrix::matrixPerspective(degreesToRadians(LIGHT_PERSPECTIVE_FOV_IN_DEGREES), 1.0f, NEAR_PLANE, FAR_PLANE);
复制代码
/* Please see the specification above. */
static void updateSpotLightDirection()
{
    /* Time used to set light direction and position. */
    const float currentAngle = timer.getTime() / 4.0f;
    /* Update the look at point coordinates. */
    lightViewProperties.lookAtPoint.x = SPOT_LIGHT_TRANSLATION_RADIUS * sinf(currentAngle);
    lightViewProperties.lookAtPoint.y = -1.0f;
    lightViewProperties.lookAtPoint.z = SPOT_LIGHT_TRANSLATION_RADIUS * cosf(currentAngle);
    /* Update all the view, projection matrixes athat are connected with updated look at point coordinates. */
    Vec4f lookAtPoint = {lightViewProperties.lookAtPoint.x,
                         lightViewProperties.lookAtPoint.y,
                         lightViewProperties.lookAtPoint.z,
                         1.0f};
    /* Get lookAt matrix from the light's point of view, directed at the center of a plane.
     * Store result in viewMatrixForShadowMapPass. */
    lightViewProperties.viewMatrix = Matrix::matrixLookAt(lightViewProperties.position,
                                                          lightViewProperties.lookAtPoint,
                                                          lightViewProperties.upVector);
    lightViewProperties.cubeViewProperties.modelViewMatrix            = lightViewProperties.viewMatrix * lightViewProperties.cubeViewProperties.modelMatrix;
    lightViewProperties.planeViewProperties.modelViewMatrix           = lightViewProperties.viewMatrix * lightViewProperties.planeViewProperties.modelMatrix;
    lightViewProperties.cubeViewProperties.modelViewProjectionMatrix  = lightViewProperties.projectionMatrix * lightViewProperties.cubeViewProperties.modelViewMatrix;
    lightViewProperties.planeViewProperties.modelViewProjectionMatrix = lightViewProperties.projectionMatrix * lightViewProperties.planeViewProperties.modelViewMatrix;
    cameraViewProperties.spotLightLookAtPointInEyeSpace               = Matrix::vertexTransform(&lookAtPoint, &cameraViewProperties.viewMatrix);
    Matrix inverseCameraViewMatrix       = Matrix::matrixInvert(&cameraViewProperties.viewMatrix);
    /* [Define colour texture translation matrix] */
    Matrix colorTextureTranslationMatrix = Matrix::createTranslation(COLOR_TEXTURE_TRANSLATION,
                                                                     0.0f,
                                                                     COLOR_TEXTURE_TRANSLATION);
    /* [Define colour texture translation matrix] */
    /* [Calculate matrix for shadow map sampling: colour texture] */
    cameraViewProperties.viewToColorTextureMatrix = Matrix::biasMatrix                   *
                                                    lightViewProperties.projectionMatrix *
                                                    lightViewProperties.viewMatrix       *
                                                    colorTextureTranslationMatrix        *
                                                    inverseCameraViewMatrix;
    /* [Calculate matrix for shadow map sampling: colour texture] */
    /* [Calculate matrix for shadow map sampling: depth texture] */
    cameraViewProperties.viewToDepthTextureMatrix = Matrix::biasMatrix                   *
                                                    lightViewProperties.projectionMatrix *
                                                    lightViewProperties.viewMatrix       *
                                                    inverseCameraViewMatrix;
    /* [Calculate matrix for shadow map sampling: depth texture] */
}
复制代码

有不同的矩阵用于渲染立方体和平面形式的聚光点视角。 调用glUniformMatrix4fv()来更新统一值。

/* Use matrices specific for rendering a scene from spot light perspective. */
GL_CHECK(glUniformMatrix4fv(renderSceneProgramLocations.uniformModelViewMatrix,
                            1,
                            GL_FALSE,
                            lightViewProperties.cubeViewProperties.modelViewMatrix.getAsArray()));
GL_CHECK(glUniformMatrix4fv(renderSceneProgramLocations.uniformModelViewProjectionMatrix,
                            1,
                            GL_FALSE,
                            lightViewProperties.cubeViewProperties.modelViewProjectionMatrix.getAsArray()));
GL_CHECK(glUniformMatrix4fv(renderSceneProgramLocations.uniformNormalMatrix,
                            1,
                            GL_FALSE,
                            lightViewProperties.cubeViewProperties.normalMatrix.getAsArray()));
复制代码
/* Use matrices specific for rendering a scene from spot light perspective. */
GL_CHECK(glUniformMatrix4fv(renderSceneProgramLocations.uniformModelViewMatrix,
                            1,
                            GL_FALSE,
                            lightViewProperties.planeViewProperties.modelViewMatrix.getAsArray()));
GL_CHECK(glUniformMatrix4fv(renderSceneProgramLocations.uniformModelViewProjectionMatrix,
                            1,
                            GL_FALSE,
                            lightViewProperties.planeViewProperties.modelViewProjectionMatrix.getAsArray()));
GL_CHECK(glUniformMatrix4fv(renderSceneProgramLocations.uniformNormalMatrix,
                            1,
                            GL_FALSE,
                            lightViewProperties.planeViewProperties.normalMatrix.getAsArray()));
复制代码

由于阴影贴图纹理比正常场景大(如上所述),我们必须记住调整视口。

/* Set the view port to size of shadow map texture. */
GL_CHECK(glViewport(0, 0, shadowMapWidth, shadowMapHeight));
复制代码

我们的场景很简单:平面顶部只有一个立方体。 我们可以在这里引入一些优化,这意味着背面将被剔除。 我们还设置了多边形偏移以消除阴影中的z-fighting。 这些设置仅在启用时使用。

/* Set the Polygon offset, used when rendering the into the shadow map
 * to eliminate z-fighting in the shadows (if enabled). */
GL_CHECK(glPolygonOffset(1.0f, 0.0f));
/* Set back faces to be culled (only when GL_CULL_FACE mode is enabled). */
GL_CHECK(glCullFace(GL_BACK));
复制代码
GL_CHECK(glEnable(GL_POLYGON_OFFSET_FILL));
复制代码

我们需要做的是启用深度测试。 启用此选项后,将比较深度值,并将结果存储在深度缓冲区中。

/* Enable depth test to do comparison of depth values. */
GL_CHECK(glEnable(GL_DEPTH_TEST));
复制代码

在这一步中,我们只想生成深度值,这意味着我们可以禁止写入每个帧缓冲颜色组件。

/* Disable writing of each frame buffer colour component. */
GL_CHECK(glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE));
复制代码

最后,我们已准备好进行实际渲染。

drawCubeAndPlane(false);
复制代码

如果我们想在程序对象中使用生成的深度纹理数据,则查询阴影采样器uniform位置并将深度纹理对象设置为此uniform的输入值就足够了。

locationsStoragePtr->uniformShadowMap = GL_CHECK(glGetUniformLocation  (programObjectId, "shadowMap"));
复制代码
GL_CHECK(glActiveTexture(GL_TEXTURE0 + TEXTURE_UNIT_FOR_SHADOW_MAP_TEXTURE));
GL_CHECK(glBindTexture  (GL_TEXTURE_2D,
                     renderSceneObjects.depthTextureObjectId));
复制代码
GL_CHECK(glUniform1i       (renderSceneProgramLocations.uniformShadowMap,
                            TEXTURE_UNIT_FOR_SHADOW_MAP_TEXTURE));
复制代码

有关程序对象和场景渲染的更多详细信息将在以下部分中介绍:生成并使用颜色纹理和投影纹理。

Generate and use colour texture

因为颜色纹理要投影到场景上,这就是我们需要生成填充数据的纹理对象的原因。

为颜色纹理设置活动纹理单元。

GL_CHECK(glActiveTexture(GL_TEXTURE0 + TEXTURE_UNIT_FOR_COLOR_TEXTURE));
复制代码

生成并绑定纹理对象。

GL_CHECK(glGenTextures  (1,
                    &renderSceneObjects.colorTextureObjectId));
GL_CHECK(glBindTexture  (GL_TEXTURE_2D,
                     renderSceneObjects.colorTextureObjectId));
复制代码

加载BMP图像数据。

Texture::loadBmpImageData(COLOR_TEXTURE_NAME, &imageWidth, &imageHeight, &textureData);
复制代码

设置纹理对象数据。

GL_CHECK(glTexStorage2D (GL_TEXTURE_2D,
                         1,
                         GL_RGB8,
                         imageWidth,
                         imageHeight));
GL_CHECK(glTexSubImage2D(GL_TEXTURE_2D,
                         0,
                         0,
                         0,
                         imageWidth,
                         imageHeight,
                         GL_RGB,
                         GL_UNSIGNED_BYTE,
                         textureData));
复制代码

设置纹理对象参数。

GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
                     GL_TEXTURE_MIN_FILTER,
                     GL_LINEAR));
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
                     GL_TEXTURE_MAG_FILTER,
                     GL_LINEAR));
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
                     GL_TEXTURE_WRAP_R,
                     GL_REPEAT));
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
                     GL_TEXTURE_WRAP_S,
                     GL_REPEAT));
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
                     GL_TEXTURE_WRAP_T,
                     GL_REPEAT));
复制代码

现在,如果我们想在程序对象中使用纹理,我们需要查询颜色纹理对象uniform采样器位置(注意以下命令是为激活的程序对象调用的)。

locationsStoragePtr->uniformColorTexture = GL_CHECK(glGetUniformLocation  (programObjectId, "colorTexture"));
复制代码

然后我们准备通过调用将uniform采样器与纹理对象相关联。

GL_CHECK(glUniform1i       (renderSceneProgramLocations.uniformColorTexture,
                                TEXTURE_UNIT_FOR_COLOR_TEXTURE));
复制代码

Projecting a texture

最后,我们准备描述投影纹理的机制。

如果按照前面部分(渲染几何体,计算阴影贴图,生成和使用颜色纹理)中所述的说明进行操作,将可以专注于投影光源机制。

我们在本教程中只使用一个程序对象。 顶点着色器相当简单(如下所示)。 它用于将坐标转换为眼睛和NDC空间(这是应用透视的眼睛空间)。

/* [Define attributes] */
/* ATTRIBUTES */
in vec4 vertexCoordinates; /* Attribute: holding coordinates of triangles that make up a geometry. */
in vec3 vertexNormals;     /* Attribute: holding normals. */
/* [Define attributes] */
/* UNIFORMS */
uniform mat4 modelViewMatrix;           /* Model * View matrix */
uniform mat4 modelViewProjectionMatrix; /* Model * View * Projection matrix */
uniform mat4 normalMatrix;              /* transpose(inverse(Model * View)) matrix */
/* OUTPUTS */
out vec3 normalInEyeSpace; /* Normal vector for the coordinates. */
out vec4 vertexInEyeSpace; /* Vertex coordinates expressed in eye space. */
void main()
{
    /* Calculate and set output vectors. */
    normalInEyeSpace = mat3x3(normalMatrix) * vertexNormals;
    vertexInEyeSpace = modelViewMatrix      * vertexCoordinates;
    /* Multiply model-space coordinates by model-view-projection matrix to bring them into eye-space. */
    gl_Position = modelViewProjectionMatrix * vertexCoordinates;
}
复制代码

请注意,深度值是根据聚光灯的角度计算的。 如果我们想在从相机的角度渲染场景时使用它们,我们将不得不应用从一个空间到另一个空间的平移。 请看下面的架构。

相机和聚光灯空间架构。

我们的阴影贴图(包含深度值的纹理对象)是在聚光灯的NDC空间中计算的,但是我们需要在相机眼睛空间中使用深度值。 为了实现这一点,我们将从相机眼睛空间拍摄片段并将其转换为聚光NDC空间,以便查询其深度值。 我们需要计算一个矩阵来帮助我们。 这个想法用红色箭头标记在架构上。

cameraViewProperties.viewToDepthTextureMatrix = Matrix::biasMatrix *
        lightViewProperties.projectionMatrix *
        lightViewProperties.viewMatrix       *
        inverseCameraViewMatrix;
复制代码

偏差矩阵用于将范围<-1,1>(眼睛空间坐标)的值映射到<0,1>(纹理坐标)。

/* Bias matrix. */
const float Matrix::biasArray[16] =
{
    0.5f, 0.0f, 0.0f, 0.0f,
    0.0f, 0.5f, 0.0f, 0.0f,
    0.0f, 0.0f, 0.5f, 0.0f,
    0.5f, 0.5f, 0.5f, 1.0f,
};
复制代码

需要使用类似的机制来对颜色纹理进行采样。 唯一的区别是我们想要在视图中调整颜色纹理,以便纹理更小并重复多次。

Matrix colorTextureTranslationMatrix = Matrix::createTranslation(COLOR_TEXTURE_TRANSLATION,
                     0.0f,
                     COLOR_TEXTURE_TRANSLATION);
复制代码
cameraViewProperties.viewToColorTextureMatrix = Matrix::biasMatrix *
                lightViewProperties.projectionMatrix *
                lightViewProperties.viewMatrix       *
                colorTextureTranslationMatrix        *
                inverseCameraViewMatrix;
复制代码

在片段着色器中,我们处理两种类型的光照:

  • 定向照明,如下所示。
vec4 calculateLightFactor()
{
    vec3  normalizedNormal         = normalize(normalInEyeSpace);
    vec3  normalizedLightDirection = normalize(directionalLightPosition - vertexInEyeSpace.xyz);
    vec4  result                   = vec4(directionalLightColor, 1.0) * max(dot(normalizedNormal, normalizedLightDirection), 0.0);
    return result * directionalLightAmbient;
}
复制代码
  • 聚光灯,与投影纹理相同。

首先,我们需要验证片段是放在点光锥内部还是外部。 通过计算从光源到片段的矢量和从光源到光导向的点之间的角度来检查这一点。 如果角度大于聚光角度,则意味着碎片位于聚光灯锥外,相反则在内部。

float getFragmentToLightCosValue()
{
    vec4  fragmentToLightdirection = normalize(vertexInEyeSpace - spotLightPositionInEyeSpace);
    vec4  spotLightDirection       = normalize(spotLightLookAtPointInEyeSpace- spotLightPositionInEyeSpace);
    float cosine                   = dot(spotLightDirection, fragmentToLightdirection);
    return cosine;
}
复制代码

下一步是验证片段是否应该被聚光灯遮蔽或点亮。 这是通过采样阴影贴图纹理并将结果与场景深度进行比较来完成的。

/* Depth value retrieved from the shadow map. */
float shadowMapDepth = textureProj(shadowMap, normalizedVertexPositionInTexture);
复制代码
/* Depth value retrieved from drawn model. */
float modelDepth = normalizedVertexPositionInTexture.z;
复制代码

如果碎片位于光锥内而不是阴影中,则应在其上应用投影纹理颜色。

vec4 calculateProjectedTexture()
{
    vec3 textureCoordinates           = (viewToColorTextureMatrix * vertexInEyeSpace).xyz;
    vec3 normalizedTextureCoordinates = normalize(textureCoordinates);
    vec4 textureColor                 = textureProj(colorTexture, normalizedTextureCoordinates);
    return textureColor;
}
复制代码
vec4 calculateSpotLight(float fragmentToLightCosValue)
{
    const float constantAttenuation  = 0.01;
    const float linearAttenuation    = 0.001;
    const float quadraticAttenuation = 0.0004;
    vec4        result               = vec4(0.0);
    /* Calculate the distance from a spot light source to fragment. */
    float distance             = distance(vertexInEyeSpace.xyz, spotLightPositionInEyeSpace.xyz);
    float factor               = clamp((fragmentToLightCosValue - spotLightCosAngle), 0.0, 1.0);
    float attenuation          = 1.0 / (constantAttenuation             +
                                        linearAttenuation    * distance +
                                        quadraticAttenuation * distance * distance);
    vec4 projectedTextureColor = calculateProjectedTexture();
    result = (spotLightColor * 0.5 + projectedTextureColor)* factor * attenuation;
    return result;
}
复制代码
/* Apply spot lighting and shadowing if needed). */
if ((fragmentToLightCosValue > spotLightCosAngle) && /* If fragment is in spot light cone. */
     modelDepth < shadowMapDepth + EPSILON)
{
    vec4 spotLighting = calculateSpotLight(fragmentToLightCosValue);
    color += spotLighting;
}
复制代码

应用这些操作后,我们得到如下图所示的结果。

渲染的结果:仅应用定向光照(在左侧)和应用投影光时(在右侧)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值