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;
}
複製程式碼
應用這些操作後,我們得到如下圖所示的結果。
渲染的結果:僅應用定向光照(在左側)和應用投影光時(在右側)。