OpenGL 簡介:3D 文本渲染教程

已發表: 2022-03-11

借助 DirectX 和 OpenGL 等工具的可用性,如今編寫呈現 3D 元素的桌面應用程序並不是很困難。 但是,與許多技術一樣,有時會遇到一些障礙,使開發人員難以進入這一領域。 隨著時間的推移,DirectX 和 OpenGL 之間的競爭使開發人員更容易使用這些技術,同時提供更好的文檔和更容易成為熟練的 DirectX 或 OpenGL 開發人員的過程。

DirectX 由 Microsoft 引入和維護,是一種特定於 Windows 平台的技術。 另一方面,OpenGL 是 3D 圖形領域的跨平台 API,其規範由 Khronos Group 維護。

opengl簡介

在這篇關於 OpenGL 的介紹中,我將解釋如何編寫一個非常簡單的應用程序來渲染 3D 文本模型。 我們將使用 Qt/Qt Creator 來實現 UI,以便在多個平台上輕鬆編譯和運行此應用程序。 為本文構建的原型的源代碼可在 GitHub 上找到。

這個簡單應用程序的目標是生成 3D 模型,將它們保存到具有簡單格式的文件中,然後在屏幕上打開和渲染它們。 渲染場景中的 3D 模型將是可旋轉和可縮放的,以提供更好的深度和維度感。

先決條件

在開始之前,我們需要為這個項目準備一些有用的工具來準備我們的開發環境。 我們首先需要的是 Qt 框架和相關實用程序,可以從 www.qt.io 下載。 它也可以通過操作系統的標準包管理器獲得; 如果是這種情況,您可能想先嘗試一下。 本文需要對 Qt 框架有一定的了解。 但是,如果您不熟悉該框架,請不要氣餒,因為原型依賴於框架的一些相當瑣碎的功能。

您還可以在 Windows 上使用 Microsoft Visual Studio 2013。 在這種情況下,請確保您使用的是適用於 Visual Studio 的適當 Qt 插件。

此時,您可能希望從 GitHub 克隆存儲庫並在閱讀本文時關注它。

OpenGL 概述

我們將從創建一個帶有單個文檔小部件的簡單 Qt 應用程序項目開始。 由於它是一個簡單的小部件,編譯和運行它不會產生任何有用的東西。 使用 Qt 設計器,我們將添加一個包含四個項目的“文件”菜單:“新建...”、“打開...”、“關閉”和“退出”。 您可以在存儲庫中找到將這些菜單項綁定到其相應操作的代碼。

單擊“新建...”應該會彈出一個對話框,如下所示:

opengl 彈出窗口

在這裡,用戶可以輸入一些文本,選擇一種字體,調整生成的模型高度,並生成一個 3D 模型。 單擊“創建”應該保存模型,如果用戶從左下角選擇適當的選項,也應該打開它。 如您所知,這裡的目標是將一些用戶輸入的文本轉換為 3D 模型並將其呈現在顯示器上。

該項目將具有簡單的結構,並且組件將被分解為少量 C++ 和頭文件:

c++ 和頭文件

創建charmodeldlg.h/cpp

文件包含 QDialog 派生對象。 這實現了允許用戶鍵入文本、選擇字體並選擇是否將結果保存到文件和/或以 3D 顯示它的對話框小部件。

gl_widget.h/cpp

包含 QOpenGLWidget 派生對象的實現。 此小部件用於渲染 3D 場景。

主窗口.h/cpp

包含主應用程序小部件的實現。 這些文件自 Qt Creator 嚮導創建以來保持不變。

主文件

包含 main(...) 函數,它創建主應用程序小部件並將其顯示在屏幕上。

model2d_processing.h/cpp

包含創建 2D 場景的功能。

模型3d.h/cpp

包含存儲 3D 模型對象並允許對其進行操作(保存、加載等)的結構。

model_creator.h/cpp

包含允許創建 3D 場景模型對象的類的實現。

OpenGL 實現

為簡潔起見,我們將跳過使用 Qt Designer 實現用戶界面的明顯細節,以及定義交互元素行為的代碼。 這個原型應用程序肯定有一些更有趣的方面,這些方面不僅重要,而且與我們想要涵蓋的 3D 模型編碼和渲染相關。 例如,在此原型中將文本轉換為 3D 模型的第一步是將文本轉換為 2D 單色圖像。 一旦生成了這個圖像,就可以知道圖像的哪個像素構成了文本,而哪些只是“空白”空間。 使用 OpenGL 渲染基本文本有一些更簡單的方法,但我們採用這種方法是為了涵蓋使用 OpenGL 進行 3D 渲染的一些基本細節。

為了生成這個圖像,我們用 QImage::Format_Mono 標誌實例化一個 QImage 對象。 由於我們只需要知道哪些像素是文本的一部分,哪些不是,因此單色圖像應該可以正常工作。 當用戶輸入一些文本時,我們同步更新這個 QImage 對象。 根據字體大小和圖像寬度,我們盡力使文本適合用戶定義的高度。

接下來,我們枚舉作為文本一部分的所有像素 - 在本例中為黑色像素。 這裡的每個像素都被視為單獨的方形單元。 基於此,我們可以生成一個三角形列表,計算它們的頂點坐標,並將它們存儲在我們的 3D 模型文件中。

現在我們有了自己的簡單 3D 模型文件格式,我們可以開始專注於渲染它了。 對於基於 OpenGL 的 3D 渲染,Qt 提供了一個名為 QOpenGLWidget 的小部件。 要使用這個小部件,可以覆蓋三個函數:

  • initializeGl() - 這是初始化代碼的地方
  • paintGl() - 每次重繪小部件時都會調用此方法
  • resizeGl(int w, int h) - 每次調整大小時都會使用小部件的寬度和高度調用此方法

3d模型文件格式

我們將通過在 initializeGl 方法中設置適當的著色器配置來初始化小部件。

 glEnable(GL_DEPTH_TEST); glShadeModel(GL_FLAT); glDisable(GL_CULL_FACE);

第一行讓程序只顯示那些離我們更近的渲染像素,而不是那些在其他像素後面且看不見的像素。 第二行指定平面著色技術。 第三行讓程序渲染三角形,不管它們的法線指向哪個方向。

初始化後,每次調用paintGl 時,我們都會在顯示器上渲染模型。 在我們重寫paintGl 方法之前,我們必須準備緩衝區。 為此,我們首先創建一個緩衝區句柄。 然後我們將句柄綁定到綁定點之一,將源數據複製到緩衝區中,最後我們告訴程序取消綁定緩衝區:

 // Get the Qt object which allows to operate with buffers QOpenGLFunctions funcs(QOpenGLContext::currentContext()); // Create the buffer handle funcs.glGenBuffers(1, &handle); // Select buffer by its handle (so we'll use this buffer // further) funcs.glBindBuffer(GL_ARRAY_BUFFER, handle); // Copy data into the buffer. Being copied, // source data is not used any more and can be released funcs.glBufferData(GL_ARRAY_BUFFER, size_in_bytes, src_data, GL_STATIC_DRAW); // Tell the program we've finished with the handle funcs.glBindBuffer(GL_ARRAY_BUFFER, 0);

在覆蓋的paintGl方法中,我們使用一個頂點數組和一個法線數據數組來為每一幀繪製三角形:

 QOpenGLFunctions funcs(QOpenGLContext::currentContext()); // Vertex data glEnableClientState(GL_VERTEX_ARRAY);// Work with VERTEX buffer funcs.glBindBuffer(GL_ARRAY_BUFFER, m_hVertexes); // Use this one glVertexPointer(3, GL_FLOAT, 0, 0); // Data format funcs.glVertexAttribPointer(m_coordVertex, 3, GL_FLOAT, GL_FALSE, 0, 0); // Provide into shader program // Normal data glEnableClientState(GL_NORMAL_ARRAY);// Work with NORMAL buffer funcs.glBindBuffer(GL_ARRAY_BUFFER, m_hNormals);// Use this one glNormalPointer(GL_FLOAT, 0, 0); // Data format funcs.glEnableVertexAttribArray(m_coordNormal); // Shader attribute funcs.glVertexAttribPointer(m_coordNormal, 3, GL_FLOAT, GL_FALSE, 0, 0); // Provide into shader program // Draw frame glDrawArrays(GL_TRIANGLES, 0, (3 * m_model.GetTriangleCount())); // Rendering finished, buffers are not in use now funcs.glDisableVertexAttribArray(m_coordNormal); funcs.glBindBuffer(GL_ARRAY_BUFFER, 0); glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_NORMAL_ARRAY);

為了提高性能,我們在原型應用程序中使用了頂點緩衝區對象 (VBO)。 這讓我們可以將數據存儲在視頻內存中並直接用於渲染。 另一種方法是從渲染代碼中提供數據(頂點坐標、法線和顏色):

 glBegin(GL_TRIANGLES); // Provide coordinates of triangle #1 glVertex3f( x[0], y[0], z[0]); glVertex3f( x[1], y[1], z[1]); glVertex3f( x[2], y[2], z[2]); // Provide coordinates of other triangles ... glEnd();

這似乎是一個更簡單的解決方案; 但是,它具有嚴重的性能影響,因為這需要數據通過視頻內存總線傳輸 - 一個相對較慢的過程。 實現paintGl方法後,我們必須注意著色器:

 m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Vertex, QString::fromUtf8( "#version 400\r\n" "\r\n" "layout (location = 0) in vec3 coordVertexes;\r\n" "layout (location = 1) in vec3 coordNormals;\r\n" "flat out float lightIntensity;\r\n" "\r\n" "uniform mat4 matrixVertex;\r\n" "uniform mat4 matrixNormal;\r\n" "\r\n" "void main()\r\n" "{\r\n" " gl_Position = matrixVertex * vec4(coordVertexes, 1.0);\r\n" " lightIntensity = abs((matrixNormal * vec4(coordNormals, 1.0)).z);\r\n" "}")); m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Fragment, QString::fromUtf8( "#version 400\r\n" "\r\n" "flat in float lightIntensity;\r\n" "\r\n" "layout (location = 0) out vec4 FragColor;\r\n" "uniform vec3 fragmentColor;\r\n" "\r\n" "void main()\r\n" "{\r\n" " FragColor = vec4(fragmentColor * lightIntensity, 1.0);\r\n" "}")); m_shaderProgram.link(); m_shaderProgram.bind(); m_coordVertex = m_shaderProgram.attributeLocation(QString::fromUtf8("coordVertexes")); m_coordNormal = m_shaderProgram.attributeLocation(QString::fromUtf8("coordNormals")); m_matrixVertex = m_shaderProgram.uniformLocation(QString::fromUtf8("matrixVertex")); m_matrixNormal = m_shaderProgram.uniformLocation(QString::fromUtf8("matrixNormal")); m_colorFragment = m_shaderProgram.uniformLocation(QString::fromUtf8("fragmentColor"));

在 OpenGL 中,著色器是使用稱為 GLSL 的語言實現的。 該語言旨在使 3D 數據在渲染之前易於操作。 在這裡,我們需要兩個著色器:頂點著色器和片段著色器。 在頂點著色器中,我們將使用變換矩陣變換坐標以應用旋轉和縮放,併計算顏色。 在片段著色器中,我們將為片段分配顏色。 然後必須編譯這些著色器程序並將其與上下文鏈接。 OpenGL 提供了橋接兩種環境的簡單方法,以便可以從外部訪問或分配程序內部的參數:

 // Get model transformation matrix QMatrix4x4 matrixVertex; ... // Calculate the matrix here // Set Shader Program object' parameters m_shaderProgram.setUniformValue(m_matrixVertex, matrixVertex);

在頂點著色器代碼中,我們通過對原始頂點應用變換矩陣來計算新的頂點位置:

 gl_Position = matrixVertex * vec4(coordVertexes, 1.0);

為了計算這個變換矩陣,我們計算了幾個單獨的矩陣:屏幕縮放、平移場景、縮放、旋轉和居中。 然後我們找到這些矩陣的乘積,以計算最終的變換矩陣。 首先將模型中心平移到原點 (0, 0, 0),這也是屏幕的中心。 旋轉由用戶使用一些指點設備與場景的交互來確定。 用戶可以單擊場景並拖動以進行旋轉。 當用戶點擊時,我們存儲光標位置,移動後我們有第二個光標位置。 使用這兩個坐標以及場景中心,我們形成了一個三角形。 通過一些簡單的計算,我們可以確定旋轉角度,我們可以更新我們的旋轉矩陣以反映這種變化。 對於縮放,我們只需依靠鼠標滾輪來修改 OpenGL 小部件的 X 和 Y 軸的縮放因子。 模型向後平移 0.5 以使其保持在渲染場景的平面後面。 最後,為了保持自然的縱橫比,我們需要調整模型擴展沿較長邊的減小(與 OpenGL 場景不同,渲染它的小部件可能沿任一軸具有不同的物理尺寸)。 結合所有這些,我們計算最終的變換矩陣如下:

 void GlWidget::GetMatrixTransform(QMatrix4x4& matrixVertex, const Model3DEx& model) { matrixVertex.setToIdentity(); QMatrix4x4 matrixScaleScreen; double dimMin = static_cast<double>(qMin(width(), height())); float scaleScreenVert = static_cast<float>(dimMin / static_cast<double>(height())); float scaleScreenHorz = static_cast<float>(dimMin / static_cast<double>(width())); matrixScaleScreen.scale(scaleScreenHorz, scaleScreenVert, 1.0f); QMatrix4x4 matrixCenter; float centerX, centerY, centerZ; model.GetCenter(centerX, centerY, centerZ); matrixCenter.translate(-centerX, -centerY, -centerZ); QMatrix4x4 matrixScale; float radius = 1.0; model.GetRadius(radius); float scale = static_cast<float>(m_scaleCoeff / radius); matrixScale.scale(scale, scale, 0.5f / radius); QMatrix4x4 matrixTranslateScene; matrixTranslateScene.translate(0.0f, 0.0f, -0.5f); matrixVertex = matrixScaleScreen * matrixTranslateScene * matrixScale * m_matrixRotate * matrixCenter; }

結論

在這篇關於 OpenGL 3D 渲染的介紹中,我們探索了一種允許 ud 使用我們的視頻卡來渲染 3D 模型的技術。 這比將 CPU 週期用於相同目的要高效得多。 我們使用了一種非常簡單的著色技術,並通過處理來自鼠標的用戶輸入使場景具有交互性。 我們避免使用顯存總線在顯存和程序之間來回傳遞數據。 即使我們只是在 3D 中渲染了一行文本,也可以用非常相似的方式渲染更複雜的場景。

公平地說,本教程幾乎沒有觸及 3D 建模和渲染的表面。 這是一個龐大的話題,本 OpenGL 教程不能聲稱這是您構建 3D 遊戲或建模軟件所需的全部知識。 但是,本文的目的是讓您了解這個領域,並展示您可以如何輕鬆地開始使用 OpenGL 構建 3D 應用程序。