術(shù)語Tessellation(鑲嵌)是指一大類設(shè)計(jì)活動(dòng),通常是指在平坦的表面上,用各種幾何形狀的瓷磚相鄰排列以形成圖案。它的目的可以是藝術(shù)性的或?qū)嵱眯缘?,很多例子可以追溯到幾千年前[TS16]。
在3D圖形學(xué)中,Tessellation指的是有點(diǎn)不同的東西(曲面細(xì)分),但顯然是由它的經(jīng)典對(duì)應(yīng)物(鑲嵌)啟發(fā)而成的。在這里,曲面細(xì)分指的是生成并且操控大量三角形以渲染復(fù)雜的形狀和表面,尤其是使用硬件進(jìn)行渲染。曲面細(xì)分是OpenGL核心近期才增加的新功能,在2010年的4.0版本中出現(xiàn)。
12.1 OpenGL中的曲面細(xì)分
OpenGL對(duì)硬件曲面細(xì)分的支持,通過3個(gè)管線階段提供:
(1)曲面細(xì)分控制著色器;
(2)曲面細(xì)分器;
(3)曲面細(xì)分評(píng)估著色器。
第(1)和第(3)階段是可編程的;而中間的第(2)階段不是。為了使用曲面細(xì)分,程序員通常會(huì)提供控制著色器和評(píng)估著色器。
曲面細(xì)分器(其全名是曲面細(xì)分圖元生成器,或TPG)是硬件支持的引擎,可以生成固定的三角形網(wǎng)格。控制著色器允許我們配置曲面細(xì)分器要構(gòu)建什么樣的三角形網(wǎng)格。然后,評(píng)估著色器允許我們以各種方式操控網(wǎng)格。然后,被操控過的三角形網(wǎng)格,會(huì)作為通過管線前進(jìn)的頂點(diǎn)的源數(shù)據(jù)。回想一下圖2.2,在管線上,曲面細(xì)分著色器位于頂點(diǎn)著色器和幾何著色器階段之間。
讓我們從一個(gè)簡單的應(yīng)用程序開始,該應(yīng)用程序只使用曲面細(xì)分器創(chuàng)建頂點(diǎn)的三角形網(wǎng)格,然后在不進(jìn)行任何操作的情況下顯示它。 為此,我們需要以下模塊。
(1)C++/OpenGL應(yīng)用程序:
創(chuàng)建一個(gè)攝像機(jī)和相關(guān)的MVP矩陣,視圖(v)和投影§矩陣確定攝像機(jī)朝向,模型(m)矩陣可用于修改網(wǎng)格的位置和方向。
(2)頂點(diǎn)著色器:在這個(gè)例子中基本上什么都不做,頂點(diǎn)將在曲面細(xì)分器中生成。
(3)曲面細(xì)分控制著色器:指定曲面細(xì)分器要構(gòu)建的網(wǎng)格。
(4)曲面細(xì)分評(píng)估著色器:將MVP矩陣應(yīng)用于網(wǎng)格中的頂點(diǎn)。
(5)片段著色器:只需為每個(gè)像素輸出固定顏色。
程序12.1顯示了整個(gè)應(yīng)用程序的代碼。即使像這樣的簡單示例也相當(dāng)復(fù)雜,因此許多代碼元素都需要解釋。請(qǐng)注意,這是我們第一次使用除頂點(diǎn)和片段著色器之外的組件構(gòu)建GLSL渲染程序。因此,我們實(shí)現(xiàn)了createShaderProgram()的4參數(shù)重載版本。
程序12.1 基本曲面細(xì)分器網(wǎng)格
.
完整源代碼
vertShader.glsl
#version 430
uniform mat4 mvp_matrix;
void main(void)
{
}
fragShader.glsl
#version 430
out vec4 color;
uniform mat4 mvp_matrix;
void main(void)
{
color = vec4(1.0, 1.0, 0.0, 1.0);
}
曲面細(xì)分控制著色器tessCShader.glsl
#version 430
uniform mat4 mvp_matrix;
layout (vertices = 1) out;
void main(void)
{ gl_TessLevelOuter[0] = 6;
gl_TessLevelOuter[2] = 6;
gl_TessLevelOuter[1] = 6;
gl_TessLevelOuter[3] = 6;
gl_TessLevelInner[0] = 12;
gl_TessLevelInner[1] = 12;
}
曲面細(xì)分評(píng)估著色器tessEShader.glsl
#version 430
layout (quads, equal_spacing, ccw) in;
uniform mat4 mvp_matrix;
void main (void)
{
float u = gl_TessCoord.x;
float v = gl_TessCoord.y;
gl_Position = mvp_matrix * vec4(u,0,v,1);
}
main.cpp
#include <GL\glew.h>
#include <GLFW\glfw3.h>
#include <SOIL2\soil2.h>
#include <string>
#include <iostream>
#include <fstream>
#include <glm\gtc\type_ptr.hpp> // glm::value_ptr
#include <glm\gtc\matrix_transform.hpp> // glm::translate, glm::rotate, glm::scale, glm::perspective
#include "Utils.h"
using namespace std;
float toRadians(float degrees) { return (degrees * 2.0f * 3.14159f) / 360.0f; }
#define numVAOs 1
//-------------------------------------------Utils util = Utils();
float cameraX, cameraY, cameraZ;
float terLocX, terLocY, terLocZ;
GLuint renderingProgram;
GLuint vao[numVAOs];
// variable allocation for display
GLuint mvpLoc;
int width, height;
float aspect;
glm::mat4 pMat, vMat, mMat, mvpMat;
float tessInner = 30.0f;
float tessOuter = 20.0f;
void init(GLFWwindow* window) {
renderingProgram = Utils::createShaderProgram("vertShader.glsl", "tessCShader.glsl", "tessEShader.glsl", "fragShader.glsl");
cameraX = 0.5f; cameraY = -0.5f; cameraZ = 2.0f;
terLocX = 0.0f; terLocY = 0.0f; terLocZ = 0.0f;
glfwGetFramebufferSize(window, &width, &height);
aspect = (float)width / (float)height;
pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);
glGenVertexArrays(numVAOs, vao);
glBindVertexArray(vao[0]);
}
void display(GLFWwindow* window, double currentTime) {
glClear(GL_DEPTH_BUFFER_BIT);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(renderingProgram);
vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ));
mMat = glm::translate(glm::mat4(1.0f), glm::vec3(terLocX, terLocY, terLocZ));
mMat = glm::rotate(mMat, toRadians(35.0f), glm::vec3(1.0f, 0.0f, 0.0f));
mvpMat = pMat * vMat * mMat;
mvpLoc = glGetUniformLocation(renderingProgram, "mvp_matrix");
glUniformMatrix4fv(mvpLoc, 1, GL_FALSE, glm::value_ptr(mvpMat));
glFrontFace(GL_CCW);
glPatchParameteri(GL_PATCH_VERTICES, 1);
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // FILL or LINE
glDrawArrays(GL_PATCHES, 0, 1);
}
void window_size_callback(GLFWwindow* win, int newWidth, int newHeight) {
aspect = (float)newWidth / (float)newHeight;
glViewport(0, 0, newWidth, newHeight);
pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);
}
int main(void) {
if (!glfwInit()) { exit(EXIT_FAILURE); }
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
GLFWwindow* window = glfwCreateWindow(800, 800, "Chapter12 - program1", NULL, NULL);
glfwMakeContextCurrent(window);
if (glewInit() != GLEW_OK) { exit(EXIT_FAILURE); }
glfwSwapInterval(1);
glfwSetWindowSizeCallback(window, window_size_callback);
init(window);
while (!glfwWindowShouldClose(window)) {
display(window, glfwGetTime());
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwDestroyWindow(window);
glfwTerminate();
exit(EXIT_SUCCESS);
}
得到的輸出網(wǎng)格如圖12.1所示(見彩插)。
曲面細(xì)分器生成由兩個(gè)參數(shù)定義的頂點(diǎn)網(wǎng)格:內(nèi)層級(jí)別和外層級(jí)別。在這種情況下,內(nèi)層級(jí)別為12,外層級(jí)別為6——網(wǎng)格的外邊緣被分為6段,而跨越內(nèi)部的線被分為12段。
程序12.1中的特別相關(guān)的新結(jié)構(gòu)被高亮顯示。讓我們首先討論第一部分——C++/ OpenGL代碼。
編譯這兩個(gè)新著色器,跟頂點(diǎn)和片段著色器完全相同。然后將它們附加到同一個(gè)渲染程序,并且鏈接調(diào)用保持不變。唯一的新項(xiàng)目是用于指定要實(shí)例化的著色器類型的常量——新常量如下:
GL_TESS_CONTROL_SHADER
GL_TESS_EVALUATION_SHADER
請(qǐng)注意display()函數(shù)中的新項(xiàng)目。glDrawArrays()
調(diào)用現(xiàn)在指定 GL_PATCHES
。當(dāng)使用曲面細(xì)分時(shí),從C++/OpenGL應(yīng)用程序發(fā)送到管線(即在VBO中)的頂點(diǎn)不會(huì)被渲染,但通常會(huì)被當(dāng)作控制點(diǎn),就像我們 在貝塞爾曲線中看到的那些一樣。一組控制點(diǎn)被稱作“補(bǔ)丁”,并且在使用曲面細(xì)分的代碼段中,GL_PATCHES
是唯一允許的圖元類型。 “補(bǔ)丁”中頂點(diǎn)的數(shù)量在glPatchParameteri()
的調(diào)用中指定。在這個(gè)特定示例中,沒有任何控制點(diǎn)被發(fā)送,但我們?nèi)匀恍枰付ㄖ辽僖粋€(gè)。類似地,在glDrawArrays()
調(diào)用中,我們指示起始值為0,頂點(diǎn)數(shù)量為1,即使我們實(shí)際上沒有從C++程序發(fā)送任何頂點(diǎn)。
對(duì)glPolygonMode()
的調(diào)用指定了如何光柵化網(wǎng)格。默認(rèn)值為 GL_FILL
。而我們的代碼中顯示的是GL_LINE
,如我們?cè)趫D12.1中看到的那樣,它只會(huì)導(dǎo)致連接線被光柵化(因此我們可以看到由曲面細(xì)分器生成的網(wǎng)格本身)。如果我們將該行代碼更改為GL_FILL
(或?qū)⑵渥?釋掉,從而使用默認(rèn)行為GL_FILL
),我們將得到如圖12.2所示的版本。
現(xiàn)在讓我們來過一遍4個(gè)著色器。如前所述,頂點(diǎn)著色器幾乎沒什么可做的,因?yàn)镃++/OpenGL應(yīng)用程序沒有提供任何頂點(diǎn)。它包含的是一個(gè)統(tǒng)一變量聲明,以和其他著色器相匹配,以及一個(gè)空的main()。 在任何情況下,所有著色器程序都必須包含頂點(diǎn)著色器。
曲面細(xì)分控制著色器指定曲面細(xì)分器要生成的三角形網(wǎng)格的拓?fù)浣Y(jié)構(gòu)。通過將值分配給名為gl_TessLevelxxx
的保留字,設(shè)置6個(gè)“級(jí)別”參數(shù)——兩個(gè)“內(nèi)部”和4個(gè)“外部”級(jí)別。我們這里細(xì)分了一個(gè) 由三角形組成的大矩形網(wǎng)格,稱為四邊形。[3]級(jí)別參數(shù)告訴曲面細(xì)分器在形成三角形時(shí)如何細(xì)分網(wǎng)格,它們的排列如圖12.3所示。
請(qǐng)注意控制著色器中的代碼行:
layout (vertices=1) out;
這與之前的GL_PATCHES
討論有關(guān),用來指定從頂點(diǎn)著色器傳遞給控制著色器(以及“輸出”給評(píng)估著色器)的每個(gè)“補(bǔ)丁”的頂點(diǎn)數(shù)。在我們現(xiàn)在這個(gè)程序中沒有任何頂點(diǎn),但我們?nèi)匀槐仨氈付ㄖ辽僖粋€(gè),因?yàn)樗矔?huì)影響控制著色器被執(zhí)行的次數(shù)。稍后這個(gè)值將反映控制點(diǎn)的數(shù)量,并且必須與C++/OpenGL應(yīng)用程序中glPatchParameteri()
調(diào)用中的值匹配。
接下來讓我們看一下曲面細(xì)分評(píng)估著色器。它以一行代碼開頭,形如:
layout (quads, equal_spacing, ccw) in;
乍一看這好像與控件著色器中的“out”布局語句有關(guān),但實(shí)際上它們是無關(guān)的。相反,這行代碼是我們指示曲面細(xì)分器去生成排列在一個(gè)大矩形(“四邊形”)中頂點(diǎn)的位置。它還指定了細(xì)分線段(包括內(nèi)部和外部)具有相等的長度(稍后我們將看到長度不等的細(xì)分的應(yīng)用場景)?!癱cw”參數(shù)指定生成曲面細(xì)分網(wǎng)格頂點(diǎn)的纏繞順序(在當(dāng)前情況下,是逆時(shí)針)。
然后,由曲面細(xì)分器生成的頂點(diǎn)被發(fā)送到評(píng)估著色器。因此,評(píng)估著色器既可以從控制著色器(通常作為控制點(diǎn)),又可以從曲面細(xì)分器(曲面細(xì)分網(wǎng)格)接收頂點(diǎn)。在程序12.1中,僅從曲面細(xì)分器接收頂點(diǎn)。
評(píng)估著色器對(duì)曲面細(xì)分器生成的每個(gè)頂點(diǎn)執(zhí)行一次??梢允褂脙?nèi)置變量gl_TessCoord
訪問頂點(diǎn)位置。曲面細(xì)分網(wǎng)格的朝向使得它位于X-Z平面中,因此gl_TessCoord
的X和Y分量被應(yīng)用于網(wǎng)格的X和Z坐標(biāo)。網(wǎng)格坐標(biāo),以及 gl_TessCoord
的值,范圍為0.0~1.0(這在計(jì)算紋理坐標(biāo)時(shí)會(huì)很方便)。然后,評(píng)估著色器使用MVP矩陣定向每個(gè)頂點(diǎn)(這在前面章節(jié)的示例中,是由頂點(diǎn)著色器完成的)。
最后,片段著色器只為每個(gè)像素輸出一個(gè)恒定的黃色。當(dāng)然,我們也可以使用它來為我們的場景應(yīng)用紋理或光照,就像我們?cè)谇懊娴恼鹿?jié)中看到的那樣。
12.2 貝塞爾曲面細(xì)分
現(xiàn)在讓我們擴(kuò)展我們的程序,使它將我們簡單的矩形網(wǎng)格轉(zhuǎn)換為貝塞爾曲面。細(xì)分網(wǎng)格應(yīng)該為我們提供了足夠的頂點(diǎn)來對(duì)曲面進(jìn)行采樣(如果我們想要更多的話,我們可以增加內(nèi)部/外部細(xì)分級(jí)別)。我們現(xiàn)在需要的是通過管線發(fā)送控制點(diǎn),然后使用這些控制點(diǎn)執(zhí)行計(jì)算以將細(xì)分網(wǎng)格轉(zhuǎn)換為我們所需的貝塞爾曲面。
假設(shè)我們希望建立一個(gè)立方體貝塞爾曲面,我們將需要16個(gè)控制點(diǎn)。我們可以通過VBO從C++端發(fā)送它們,或者我們可以在頂點(diǎn)著色器中硬編碼寫死它們。圖12.4概述了來自C++端的控制點(diǎn)的過程。
現(xiàn)在是更準(zhǔn)確地解釋曲面細(xì)分控制著色器(TCS)如何工作的好時(shí)機(jī)。與頂點(diǎn)著色器類似,TCS對(duì)每個(gè)傳入頂點(diǎn)執(zhí)行一次。另外,回想一下第2章,OpenGL提供了一個(gè)名為·gl_VertexID·的內(nèi)置變量,它保存一個(gè)計(jì)數(shù)器,指示頂點(diǎn)著色器當(dāng)前正在執(zhí)行哪次調(diào)用。曲面細(xì)分控制著色器中存在一個(gè)類似的內(nèi)置變量gl_InvocationID
。
曲面細(xì)分的一個(gè)強(qiáng)大功能是TCS(以及TES)著色器可以同時(shí)訪問數(shù)組中的所有控制點(diǎn)頂點(diǎn)。首先,當(dāng)每個(gè)調(diào)用都可以訪問所有頂點(diǎn)時(shí),TCS對(duì)每個(gè)頂點(diǎn)執(zhí)行一次可能會(huì)讓人感到困惑。在每個(gè)TCS調(diào)用中,冗余地在賦值語句中指定曲面細(xì)分級(jí)別也是違反直覺的。盡管所 有這些看起來都很奇怪,但這樣做是因?yàn)榍婕?xì)分的架構(gòu)設(shè)計(jì)使得TCS 調(diào)用可以并行運(yùn)行。
OpenGL提供了幾個(gè)用于TCS和TES著色器的內(nèi)置變量。我們已經(jīng)提到過的是gl_InvocationID
,當(dāng)然還有gl_TessLevelInner
和gl_TessLevelOuter
。以下是一些最有用的內(nèi)置變量的更多細(xì)節(jié)和描述。
-
曲面細(xì)分控制著色器(TCS)內(nèi)置變量。
gl_in[ ]
——包含每個(gè)傳入的控制點(diǎn)頂點(diǎn)的數(shù)組——每個(gè)傳入頂點(diǎn)是一個(gè)數(shù)組元素??梢允褂谩?”表示法將特定頂點(diǎn)屬性作為字段進(jìn)行訪問。一個(gè)內(nèi)置屬性是gl_Position
——因此,輸入頂點(diǎn)“i”的位置可以通過gl_in[i].gl_Position
訪問。gl_out[ ]
——用于將輸出控制點(diǎn)的頂點(diǎn)發(fā)送到TES的一個(gè)數(shù)組——每個(gè)輸出頂點(diǎn)是一個(gè)數(shù)組元素??梢允褂谩?”表示法將特定 頂點(diǎn)屬性作為字段進(jìn)行訪問。一個(gè)內(nèi)置屬性是gl_Position
——因此,輸出頂點(diǎn)“i”的位置可以通過gl_out[i].gl_Position
訪問。gl_InvocationID
——整型ID計(jì)數(shù)器,指示TCS當(dāng)前正在執(zhí)行哪個(gè)調(diào)用。一個(gè)常見的用途是用于傳遞頂點(diǎn)屬性;例如,將當(dāng)前調(diào)用 的頂點(diǎn)位置從TCS傳遞到TES可以用如下方式完成:gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position
。 -
曲面細(xì)分評(píng)估著色器(TES)內(nèi)置變量。
gl_in[ ]
——包含每個(gè)傳入的控制點(diǎn)頂點(diǎn)的數(shù)組——每個(gè)傳入頂點(diǎn)是一個(gè)數(shù)組元素??梢允褂谩?”表示法將特定頂點(diǎn)屬性作為字段進(jìn)行訪問。一個(gè)內(nèi)置屬性是gl_Position
——因此,輸入頂點(diǎn) “i”的位置可以通過gl_in[i].gl_Position
訪問。gl_Position
——曲面細(xì)分網(wǎng)格頂點(diǎn)的輸出位置,可能在TES中被修改。重要的是要注意gl_Position
和gl_in[xxx].gl_Position
是不同的——gl_Position
是起源于曲面細(xì)分器的輸出頂點(diǎn)的位置,而gl_in[xxx].gl_Position
是一個(gè)從TCS進(jìn)入TES的控制點(diǎn)頂點(diǎn)位置。
值得注意的是,TCS中的輸入和輸出控制點(diǎn)頂點(diǎn)屬性是數(shù)組。不同的是,TES中的輸入控制點(diǎn)頂點(diǎn)和頂點(diǎn)屬性是數(shù)組,但輸出頂點(diǎn)是標(biāo)量。此外,很容易混淆哪些頂點(diǎn)來自于控制點(diǎn),哪些頂點(diǎn)是細(xì)分建立的,然后移動(dòng)以形成結(jié)果曲面。總而言之,TCS的所有頂點(diǎn)輸入和輸出都是控制點(diǎn),而在TES中,gl_in[ ]
保存輸入控制點(diǎn),gl_TessCoord
保存輸入的細(xì)分網(wǎng)格點(diǎn),gl_Position
保存用于渲染的輸出表面頂點(diǎn)。
我們的曲面細(xì)分控制著色器現(xiàn)在有兩個(gè)任務(wù):指定曲面細(xì)分級(jí)別并將控制點(diǎn)從頂點(diǎn)著色器傳遞到評(píng)估著色器。然后,評(píng)估著色器可以根據(jù)貝塞爾控制點(diǎn)修改網(wǎng)格點(diǎn)(gl_TessCoords
)的位置。
程序12.2顯示了所有4個(gè)著色器——頂點(diǎn)、TCS、TES和片段——用于指定控制點(diǎn)補(bǔ)丁,生成平坦的曲面細(xì)分頂點(diǎn)網(wǎng)格,在控制點(diǎn)指定的曲面上重新定位這些頂點(diǎn),并使用紋理圖像繪制生成的曲面。它還顯示了C++/OpenGL應(yīng)用程序的相關(guān)部分,特別是在display()函數(shù)中。在 此示例中,控制點(diǎn)源自頂點(diǎn)著色器(它們?cè)谀抢镉簿幋a寫死),而不是從C++/OpenGL應(yīng)用程序進(jìn)入OpenGL管線。代碼后面會(huì)講述其他詳細(xì)信息。
程序12.2 貝塞爾曲面的曲面細(xì)分
頂點(diǎn)著色器現(xiàn)在指定代表特定貝塞爾曲面的16個(gè)控制點(diǎn)(“補(bǔ)丁”頂點(diǎn))。在這個(gè)例子中,它們都被歸一化到范圍[?1…+1]。頂點(diǎn)著色器還使用控制點(diǎn)來確定適合細(xì)分網(wǎng)格的紋理坐標(biāo),其值在[0…1]范圍內(nèi)。很重要的是,要重申頂點(diǎn)著色器輸出的頂點(diǎn)不是將要用來光柵化的頂點(diǎn),而是貝塞爾控制點(diǎn)。使用曲面細(xì)分時(shí),補(bǔ)丁頂點(diǎn)永遠(yuǎn)不會(huì)被光柵化——只有曲面細(xì)分頂點(diǎn)會(huì)被光柵化。
控制著色器仍然會(huì)指定內(nèi)部和外部曲面細(xì)分級(jí)別。它現(xiàn)在還負(fù)責(zé)將控制點(diǎn)和紋理坐標(biāo)發(fā)送到評(píng)估著色器。請(qǐng)注意,曲面細(xì)分級(jí)別只需要指定一次,因此該步驟僅在第0次調(diào)用期間完成(回想一下TCS每個(gè)頂點(diǎn)運(yùn)行一次,因此在此示例中有16次調(diào)用)。為方便起見,我們?yōu)槊總€(gè)細(xì)分級(jí)別指定了32個(gè)細(xì)分。
接下來,評(píng)估著色器執(zhí)行所有貝塞爾曲面計(jì)算。main()開頭的大塊賦值語句從每個(gè)傳入gl_in
的gl_Position
中提取控制點(diǎn)(請(qǐng)注意,這些控制點(diǎn)對(duì)應(yīng)于控制著色器的gl_out
變量)。然后使用來自曲面細(xì)分器的網(wǎng)格點(diǎn)計(jì)算混合函數(shù)的權(quán)重,從而生成一個(gè)新的outputPosition
,然后應(yīng)用模型-視圖-投影矩陣,為每個(gè)網(wǎng)格點(diǎn)生成輸出gl_Position
并形成貝塞爾曲面。
另外,還需要?jiǎng)?chuàng)建紋理坐標(biāo)。頂點(diǎn)著色器僅為每個(gè)控制點(diǎn)位置提供一個(gè)紋理坐標(biāo)。但我們并不是要渲染控制點(diǎn),我們最終需要更多的曲面細(xì)分網(wǎng)格點(diǎn)的紋理坐標(biāo)。有很多方法可以做到這一點(diǎn),在這里我們利用GLSL方便的混合功能對(duì)它們進(jìn)行線性插值。mix()函數(shù)需要3個(gè)參數(shù):
(a)起始點(diǎn);
(b)結(jié)束點(diǎn);
(c)內(nèi)插值,范圍為0~1。
它返 回與內(nèi)插值對(duì)應(yīng)的起點(diǎn)和終點(diǎn)之間的值。由于細(xì)分網(wǎng)格坐標(biāo)的范圍也是0~1,所以它們可以直接用于此目的。
這次在片段著色器中,不再是輸出單一顏色,而是應(yīng)用標(biāo)準(zhǔn)紋理。屬性texCoord_TESou
t中的紋理坐標(biāo)是在評(píng)估著色器中生成的紋理坐標(biāo)。對(duì)C++程序的更改同樣很簡單——請(qǐng)注意,現(xiàn)在指定的補(bǔ)丁大小為16。結(jié)果輸出如圖12.5所示(應(yīng)用了[LU16]的平鋪紋理)。
12.3 地形、高度圖的細(xì)分
回想一下,在頂點(diǎn)著色器中執(zhí)行高度貼圖可能會(huì)遇到頂點(diǎn)數(shù)量不足以用來渲染所需的細(xì)節(jié)的情況?,F(xiàn)在我們有了生成大量頂點(diǎn)的方 法,讓我們回到Hastings-Trew的月球表面紋理貼圖[HT16]并將其用作高度貼圖,提升曲面細(xì)分頂點(diǎn)來生成月球表面細(xì)節(jié)。正如我們將看到的,這具有一些優(yōu)點(diǎn),可以讓頂點(diǎn)的幾何形狀更好地匹配月亮圖像, 并且提升輪廓(邊緣)細(xì)節(jié)。
我們的策略是修改程序12.1,在X-Z平面中放置細(xì)分網(wǎng)格,并使用高度貼圖來設(shè)置每個(gè)細(xì)分網(wǎng)格點(diǎn)的Y坐標(biāo)。要做到這一點(diǎn),我們不需要補(bǔ)丁,因?yàn)榭梢杂簿幋a細(xì)分網(wǎng)格的位置,因此我們將在glDrawArrays()
和glPatchParameteri()
中為每個(gè)補(bǔ)丁指定所需的最少的1個(gè)頂點(diǎn),如程序12.1中所做的那樣。Hastings-Trew的月亮紋理圖像既用于顏色,也用作高度圖。
我們通過將曲面細(xì)分網(wǎng)格的gl_TessCoord
值映射到頂點(diǎn)和紋理的適當(dāng)范圍,在評(píng)估著色器中生成頂點(diǎn)和紋理坐標(biāo)。[4]評(píng)估著色器也通過添加月亮紋理的一小部分顏色分量到輸出頂點(diǎn)的Y分量,來實(shí)現(xiàn)高度貼圖。著色器的更改顯示在程序12.3中。
程序12.3 簡單的地形曲面細(xì)分
這里的片段著色器類似于程序12.2的,只是根據(jù)紋理圖像輸出顏色。C++/OpenGL應(yīng)用程序基本上沒有變化——它加載紋理(用作紋理 和高度圖)并為其啟用采樣器。圖12.6顯示了紋理圖像(左側(cè))和第一次嘗試的最終輸出,遺憾的是,它還沒有實(shí)現(xiàn)正確的高度貼圖。
第一次結(jié)果存在嚴(yán)重缺陷。雖然我們現(xiàn)在可以看到遠(yuǎn)處地平線上的輪廓細(xì)節(jié),但是那里的凸起與紋理貼圖中的實(shí)際細(xì)節(jié)不對(duì)應(yīng)。回想一下,在高度圖中,白色應(yīng)該表示“高”,而黑色應(yīng)該表示“低”。 特別是圖像右上方的區(qū)域顯示的大山丘與其中的淺色和深色無關(guān)。
導(dǎo)致此問題的原因是細(xì)分網(wǎng)格的分辨率。曲面細(xì)分器可以生成的最大頂點(diǎn)數(shù)取決于硬件。要符合OpenGL標(biāo)準(zhǔn),唯一的要求是每個(gè)曲面細(xì)分級(jí)別的最大值至少為64。我們的程序指定了一個(gè)內(nèi)部和外部曲面細(xì)分級(jí)別均為32的單一細(xì)分網(wǎng)格,因此我們生成了大約32×32或者說剛剛超過1 000個(gè)頂點(diǎn),這不足以準(zhǔn)確反映圖像中的細(xì)節(jié)。這在圖12.6右上方(圖中放大)尤其明顯——邊緣細(xì)節(jié)僅在沿地平線的32個(gè)點(diǎn)處采樣,這會(huì)產(chǎn)生巨大而看起來很隨機(jī)的山丘。即使我們將曲面細(xì)分值增加到64,總共64×64或剛剛超過4 000個(gè)頂點(diǎn)仍然不足以滿足使用月球圖像進(jìn)行高度貼圖的需要。
增加頂點(diǎn)數(shù)量的一個(gè)好方法是使用我們?cè)诘?章中看到的實(shí)例化。 我們的策略是讓曲面細(xì)分器生成網(wǎng)格,并使用實(shí)例化重復(fù)數(shù)次。在頂點(diǎn)著色器中,我們構(gòu)建了一個(gè)由4個(gè)頂點(diǎn)定義的補(bǔ)丁,每個(gè)頂點(diǎn)用于細(xì)分網(wǎng)格的每個(gè)角。在我們的C++/OpenGL應(yīng)用程序中,我們將glDrawArrays()
調(diào)用更改為glDrawArraysInstanced()
。如此,我們指定一個(gè)64×64個(gè)補(bǔ)丁的網(wǎng)格,每個(gè)補(bǔ)丁包含一個(gè)細(xì)分級(jí)別為32的網(wǎng)格。這將帶給我們總共64×64×32×32個(gè),或者說超過400萬個(gè)頂點(diǎn)。
頂點(diǎn)著色器首先指定4個(gè)紋理坐標(biāo)(0,0)、(0,1)、(1,0)和(1,1)。 使用實(shí)例化時(shí),請(qǐng)回想一下,頂點(diǎn)著色器可以訪問整數(shù)變量 gl_InstanceID
,它包含一個(gè)對(duì)應(yīng)于當(dāng)前正在處理的 glDrawArraysInstanced()
調(diào)用的計(jì)數(shù)器。我們使用此ID值來分配大網(wǎng)格中各個(gè)補(bǔ)丁的位置。補(bǔ)丁位于行和列中,第一個(gè)補(bǔ)丁位于(0,0),第 二個(gè)位于(1,0),下一個(gè)位于(2,0),依此類推,第一列中的最后一個(gè) 補(bǔ)丁在(63,0)。下一列的補(bǔ)丁位于(0,1)、(1,1),依此類推,直至 (63,1)。最后一列的補(bǔ)丁位于(0,63)、(1,63),依此類推,最后是(63,63)。給定補(bǔ)丁的X坐標(biāo)是實(shí)例ID整除64,Y坐標(biāo)是實(shí)例ID除以64(整數(shù)除法)。然后著色器將坐標(biāo)向下縮放到范圍[0…1]。
控制著色器沒有更改,除了它將頂點(diǎn)和紋理坐標(biāo)傳遞下去。
接下來,評(píng)估著色器獲取傳入的細(xì)分網(wǎng)格頂點(diǎn)(由gl_TessCoord
指定)并將它們移動(dòng)到傳入補(bǔ)丁指定的坐標(biāo)范圍內(nèi)。它對(duì)紋理坐標(biāo)也進(jìn)行一樣的處理,并且也會(huì)以與程序12.3中相同的方式應(yīng)用高度貼圖。片段著色器沒有修改。
每個(gè)組件的更改顯示在程序12.4中。結(jié)果如圖12.7所示。請(qǐng)注意,高點(diǎn)和低點(diǎn)現(xiàn)在更接近于圖像的亮部和暗部。
程序12.4 實(shí)例化細(xì)分地形
現(xiàn)在我們已經(jīng)實(shí)現(xiàn)了高度貼圖,我們可以著手改進(jìn)它并整合光照。一個(gè)挑戰(zhàn)是我們的頂點(diǎn)還沒有與它們相關(guān)的法向量。另一個(gè)挑戰(zhàn)是簡單地使用紋理圖像作為高度圖產(chǎn)生了過度“鋸齒狀”的結(jié)果——在這種情況下是因?yàn)椴⒎羌y理圖像中的所有灰度變化都是由高度引起的。對(duì)于這個(gè)特定的紋理貼圖,Hastings-Trew已經(jīng)生成了一個(gè)改進(jìn)的高度貼圖,我們可以使用[HT16]。如圖12.8左圖所示。我們可以通過生成相鄰頂點(diǎn)(或高度圖中的相鄰紋素)的高度,構(gòu)建連接它們的向量以及使用叉積來計(jì)算法向量,以動(dòng)態(tài)計(jì)算和創(chuàng)建法向量。這需要一些細(xì)微的調(diào)整,具體取決于場景的精度(和/或高度圖圖像)。在這里,我們使用GIMP“normalmap”插件[GP16]來根據(jù)Hastings-Trew的高度圖生成法線貼圖,如圖12.8右圖所示。
我們對(duì)代碼進(jìn)行的大部分更改現(xiàn)在只是為了實(shí)現(xiàn)Phong著色的標(biāo)準(zhǔn)方法。
- C++/OpenGL應(yīng)用程序。
我們加載并激活一個(gè)額外的紋理來保存法線貼圖,還添加了代碼來指定光照和材質(zhì),就像我們?cè)谝郧暗膽?yīng)用程序中所做的那樣。
- 頂點(diǎn)著色器。
唯一的增補(bǔ)是光照統(tǒng)一變量的聲明和法線貼圖的采樣器。通常在頂點(diǎn)著色器中完成的光照代碼被移動(dòng)到曲面細(xì)分評(píng)估著色器,因?yàn)橹钡角婕?xì)分階段才生成頂點(diǎn)。
- 曲面細(xì)分控制著色器。
唯一的增補(bǔ)是光照統(tǒng)一變量的聲明和法線貼圖的采樣器。
- 曲面細(xì)分評(píng)估著色器。
Phong光照的準(zhǔn)備代碼現(xiàn)在放在評(píng)估著色器中:
varyingVertPos = (mv_matrix * position).xyz;
varyingLightDir = light.position - varyingVertPos;
- 片段著色器。
這里完成了用于計(jì)算Phong(或Blinn-Phong)照明的典型代碼段,以及從法線貼圖中提取法向量的代碼。然后將光照結(jié)果與紋理圖像用加權(quán)求和的方式結(jié)合起來。
帶有高度和法線貼圖以及Phong照明的最終結(jié)果如圖12.9所示。地形現(xiàn)在會(huì)響應(yīng)光照。在此示例中,位置光已放置在左側(cè)圖像中心的左側(cè),右側(cè)圖像中心的右側(cè)。
盡管從靜止圖像很難判斷出對(duì)光的移動(dòng)的響應(yīng),但是讀者應(yīng)該能夠辨別出漫射光的變化,并且山峰的鏡面高光在兩個(gè)圖像中是非常不同的。當(dāng)攝像機(jī)或光源移動(dòng)時(shí),這當(dāng)然會(huì)更明顯。結(jié)果仍然不完美,因?yàn)闊o論什么樣的光照,輸出中包含的原始紋理都包括了將出現(xiàn)在渲染結(jié)果上的陰影。
12.4 控制細(xì)節(jié)級(jí)別(LOD)
在程序12.4中,使用實(shí)例化來實(shí)時(shí)生成數(shù)百萬個(gè)頂點(diǎn),即使是裝備精良的現(xiàn)代計(jì)算機(jī)也可能會(huì)感受到負(fù)擔(dān)。幸運(yùn)的是,將地形劃分為單獨(dú)的補(bǔ)丁的策略,正如我們?yōu)樵黾由傻木W(wǎng)格頂點(diǎn)的數(shù)量所做的那樣,也為我們提供了一種減少負(fù)擔(dān)的好機(jī)制。
在生成的數(shù)百萬個(gè)頂點(diǎn)中,許多頂點(diǎn)不是必需的??拷鼣z像機(jī)的補(bǔ)丁中的頂點(diǎn)非常重要,因?yàn)槲覀兿M軌蜃R(shí)別附近物體的細(xì)節(jié)。但是,補(bǔ)丁越遠(yuǎn)離攝像機(jī),甚至光柵化過程中有足夠的像素來體現(xiàn)我們生成的頂點(diǎn)數(shù)量的可能性就越?。?/p>
根據(jù)距攝像機(jī)的距離更改補(bǔ)丁中的頂點(diǎn)數(shù)量是一種稱為細(xì)節(jié)級(jí)別或LOD的技術(shù)。Sellers等人描述了一種通過修改控制著色器來控制實(shí)例化曲面細(xì)分中的LOD的方法[SW15]。程序12.5顯示了Sellers等人的方法的簡化版本。策略是使用補(bǔ)丁的感知大小來確定其曲面細(xì)分級(jí)別的值。由于補(bǔ)丁的細(xì)分網(wǎng)格最終將放置在由進(jìn)入控制著色器的4個(gè)控制點(diǎn)定義的方格內(nèi),我們可以使用控制點(diǎn)相對(duì)于攝像機(jī)的位置來確定應(yīng)該為補(bǔ)丁生成多少個(gè)頂點(diǎn)。其步驟如下。
(1)通過將MVP矩陣應(yīng)用于4個(gè)控制點(diǎn),計(jì)算它們的屏幕位置。
(2)計(jì)算由控制點(diǎn)(在屏幕上的空間中)定義的正方形邊長(即寬度和高度)。請(qǐng)注意,即使4個(gè)控制點(diǎn)形成正方形,這些邊長也可能不同,因?yàn)閼?yīng)用了透視矩陣。
(3)根據(jù)曲面細(xì)分級(jí)別所需的精度(基于高度圖中的細(xì)節(jié)數(shù)量),將長度的值按可調(diào)整常數(shù)進(jìn)行縮放。
(4)將縮放長度值加1,以避免將曲面細(xì)分級(jí)別指定為0(這將導(dǎo)致不生成頂點(diǎn))。
(5)將曲面細(xì)分級(jí)別設(shè)置為相應(yīng)的計(jì)算寬度和高度值。
回想一下,在我們的實(shí)例中,我們不是只創(chuàng)建一個(gè)網(wǎng)格,而是創(chuàng)建64×64個(gè)網(wǎng)格。因此,對(duì)每個(gè)補(bǔ)丁執(zhí)行以上列表中的5個(gè)步驟,細(xì)節(jié)級(jí)別因補(bǔ)丁而異。所有更改都在控制著色器中,并顯示在程序12.5中,生成的輸出如圖12.10所示。請(qǐng)注意,變量gl_InvocationID
指的是正在處理補(bǔ)丁中的哪個(gè)頂點(diǎn)(而不是正在處理哪個(gè)補(bǔ)?。R虼?,告訴曲面細(xì)分器在每個(gè)補(bǔ)丁中生成多少個(gè)頂點(diǎn)的LOD計(jì)算發(fā)生在每個(gè)補(bǔ)丁的第0個(gè)頂點(diǎn)期間
程序12.5 曲面細(xì)分細(xì)節(jié)級(jí)別(LOD)
將這些控制著色器的更改應(yīng)用于圖12.7中我們場景的實(shí)例化(但不帶光照)版本,并將高度圖替換為Hastings-Trew的更精細(xì)調(diào)整的版本(如圖12.8所示),將會(huì)生成改善的場景,帶有更逼真的地平線細(xì)節(jié)(如圖12.10所示)。
在此示例中,更改評(píng)估著色器中的布局說明符也很有用:
layout (quads, equal_spacing) in;
更改為:
layout (quads, fractional_even_spacing) in;
在靜止圖像中難以說明這種修改的原因。在動(dòng)畫場景中,當(dāng)曲面細(xì)分對(duì)象在3D空間中移動(dòng)時(shí),如果使用LOD,有時(shí)可以在對(duì)象表面上看到曲面細(xì)分級(jí)別的變化,看起來像一種叫作“彈出”的擺動(dòng)偽影。從等間距變?yōu)榉謹(jǐn)?shù)間距,通過使相鄰補(bǔ)丁實(shí)例的網(wǎng)格幾何體更相似,達(dá)成了即使它們的細(xì)節(jié)級(jí)別不同,也可以減少此影響的目的。(參見習(xí) 題12.2和12.3。)
使用LOD可以顯著降低系統(tǒng)負(fù)載。例如,在動(dòng)畫時(shí),如果不控制 LOD,場景可能會(huì)出現(xiàn)不穩(wěn)定或滯后的情況。
將這種簡單的LOD技術(shù)應(yīng)用于包含Phong著色的版本(程序12.4)有點(diǎn)棘手。這是因?yàn)橄噜徰a(bǔ)丁實(shí)例之間的LOD變化反過來會(huì)導(dǎo)致相關(guān)法向量的突然變化,從而導(dǎo)致光照中的彈出偽影!與以往一樣,在構(gòu)建復(fù)雜的3D場景時(shí)需要權(quán)衡和妥協(xié)。
補(bǔ)充說明
將曲面細(xì)分與LOD組合在實(shí)時(shí)虛擬現(xiàn)實(shí)應(yīng)用中特別有用,例如在計(jì)算機(jī)游戲中,其需要復(fù)雜的現(xiàn)實(shí)主義細(xì)節(jié)和頻繁的物體移動(dòng)和/或攝像機(jī)位置的變化。在本章中,我們已經(jīng)說明了曲面細(xì)分和LOD用于實(shí)時(shí)地 形生成的應(yīng)用場景,盡管它也可以應(yīng)用于其他領(lǐng)域,例如3D模型的位移貼圖(曲面細(xì)分頂點(diǎn)被添加到模型的表面,然后被移動(dòng)以便添加細(xì)節(jié))在計(jì)算機(jī)輔助設(shè)計(jì)應(yīng)用程序中也很有用。
Sellers等人通過消除攝像機(jī)后方的補(bǔ)丁中的頂點(diǎn)(他們通過將內(nèi) 部和外部級(jí)別設(shè)置為零來實(shí)現(xiàn)這一點(diǎn))[SW15],進(jìn)一步擴(kuò)展了LOD技術(shù)(在程序12.5中顯示)。這是一個(gè)剔除技術(shù)的示例,是一項(xiàng)非常有用 的技術(shù),因?yàn)閷?shí)例化細(xì)分的負(fù)載仍然可以在系統(tǒng)上正常運(yùn)行。文章來源:http://www.zghlxwxcb.cn/news/detail-481998.html
程序12.1中描述的createShaderProgram()的4參數(shù)版本被添加到Utils.cpp文件中。稍后,我們將添加其他版本以適應(yīng)幾何著色器階段。文章來源地址http://www.zghlxwxcb.cn/news/detail-481998.html
到了這里,關(guān)于計(jì)算機(jī)圖形學(xué)與opengl C++版 學(xué)習(xí)筆記 第12章 曲面細(xì)分的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!