OpenGL 程式設計/迷你傳送門
在本系列中,我們將建立一個類似於 Valve 的傳送門遊戲中使用的傳送系統。[1]

我們想要實現一個傳送裝置,其中源和目的地表示為牆壁上的洞,你可以透過它們看到。進入源傳送門和走出目的地傳送門應該是完全無縫的。也可以將兩個傳送門面對面放置,就像鏡子一樣,以創造無限的深度。
直觀的做法是在 後期處理 中放置第二個攝像機並渲染到紋理。但是,紋理會被扭曲以對映到它們所附加的物體上,特別是當你從側面觀察它時。通常它們看起來像一個平面的電視螢幕[2] - 但我們現在談論的是傳送門!
因為我們想要一個無縫的效果,所以我們將使用不同的方法。場景將被渲染兩次
- 一次就像玩家已經傳送了,考慮到玩家到傳送門的距離。我們將使用 模板緩衝區 將場景剪下到傳送門的邊界。
- 一次正常渲染,不覆蓋傳送門,透過欺騙深度緩衝區實現。
為了實現傳送門,我們需要一些先決條件
以下是我們將解決的實現傳送門系統的一些要點
- 透過傳送門檢視
- 在遠處矩形中繪製模板
- 在矩形與攝像機相交的地方繪製模板
- 碰撞檢測
- 扭曲
- 無限/遞迴傳送門顯示(傳送門相互面對)
- 物體同時出現在兩個位置(複製物體)
- 最佳化
- 物理
繪製傳送門將涉及從另一側傳送門繪製場景,所以為了避免任何問題,讓我們啟用背面剔除,只繪製正面多邊形。
/* main() */
glEnable(GL_CULL_FACE);
所以我們有兩個傳送門
- 源傳送門(portal1)
- 目的地傳送門(portal2)
我們將它們表示為經典的網格物體(參見 基礎 教程):一組頂點來定義傳送門的形狀,以及一個物體到世界變換矩陣。
/* Global */
Mesh portals[2];
/* init_resources() */
glm::vec4 portal_vertices[] = {
glm::vec4(-1, -1, 0, 1),
glm::vec4( 1, -1, 0, 1),
glm::vec4(-1, 1, 0, 1),
glm::vec4( 1, 1, 0, 1),
};
for (unsigned int i = 0; i < sizeof(portal_vertices)/sizeof(portal_vertices[0]); i++) {
portals[0].vertices.push_back(portal_vertices[i]);
portals[1].vertices.push_back(portal_vertices[i]);
}
GLushort portal_elements[] = {
0,1,2, 2,1,3,
};
for (unsigned int i = 0; i < sizeof(portal_elements)/sizeof(portal_elements[0]); i++) {
portals[0].elements.push_back(portal_elements[i]);
portals[1].elements.push_back(portal_elements[i]);
}
// 90° angle + slightly higher
portals[0].object2world = glm::translate(glm::mat4(1), glm::vec3(0, 1, -2));
portals[1].object2world = glm::rotate(glm::mat4(1), -90.0f, glm::vec3(0, 1, 0))
* glm::translate(glm::mat4(1), glm::vec3(0, 1.2, -2));
portals[0].upload();
portals[1].upload();

所以我們需要在傳送門2 位置放置一個新的攝像機,然後向後移動以覆蓋從傳送門1 到原始攝像機的距離。幸運的是,可以透過組合變換矩陣輕鬆完成(記住從後往前讀取矩陣乘法)。
/**
* Compute a world2camera view matrix to see from portal 'dst', given
* the original view and the 'src' portal position.
*/
glm::mat4 portal_view(glm::mat4 orig_view, Mesh* src, Mesh* dst) {
glm::mat4 mv = orig_view * src->object2world;
glm::mat4 portal_cam =
// 3. transformation from source portal to the camera - it's the
// first portal's ModelView matrix:
mv
// 2. object is front-facing, the camera is facing the other way:
* glm::rotate(glm::mat4(1.0), glm::radians(180.0f), glm::vec3(0.0,1.0,0.0))
// 1. go the destination portal; using inverse, because camera
// transformations are reversed compared to object
// transformations:
* glm::inverse(dst->object2world)
;
return portal_cam;
}
我們現在有了新的世界到攝像機 (View) 矩陣。我們可以將其傳遞給我們的著色器,使它們從這個新的視角渲染場景。
/* onDisplay */
glm::mat4 portal_view = portal_view(transforms[MODE_CAMERA], &portals[0], &portals[1]);
glUniformMatrix4fv(uniform_v, 1, GL_FALSE, v);
glUniformMatrix4fv(uniform_v_inv, 1, GL_FALSE, glm::value_ptr(glm::inverse(portal_view)));
main_object.draw();
ground.draw();
...
/* then reset the view and re-draw the scene as usual */
你可以以同樣的方式對第二個傳送門進行操作。
目前我們只是渲染場景兩次:從傳送門 2 和從主攝像機,但第二次渲染覆蓋了第一次。
訣竅是在深度緩衝區中繪製傳送門,但不要寫入顏色緩衝區:OpenGL 會理解在傳送門上顯示了某些內容,但不會用空白矩形覆蓋它。
我們還注意儲存和恢復之前的顏色/深度配置。
// Draw portal in the depth buffer so they are not overwritten
glClear(GL_DEPTH_BUFFER_BIT);
GLboolean save_color_mask[4];
GLboolean save_depth_mask;
glGetBooleanv(GL_COLOR_WRITEMASK, save_color_mask);
glGetBooleanv(GL_DEPTH_WRITEMASK, &save_depth_mask);
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glDepthMask(GL_TRUE);
for (int i = 0; i < 2; i++)
portals[i].draw();
glColorMask(save_color_mask[0], save_color_mask[1], save_color_mask[2], save_color_mask[3]);
glDepthMask(save_depth_mask);

乍一看,我們似乎不需要剪下傳送門場景,因為我們無論如何都會覆蓋它的周圍環境,並且深度緩衝區已經保護了傳送門。
但是
- 這迫使你使用天空盒重寫整個場景背景:你不能再僅僅依靠
glClear(GL_COLOR_BUFFER_BIT)來清除背景,因為它可能被傳送門檢視寫入,而主場景不會覆蓋那部分。 - 這沒有最佳化,因為即使對於一小部分傳送門,你也會重新繪製整個螢幕。
- 最重要的是:當我們顯示兩個傳送門(不僅僅是一個)時,第二個傳送門檢視的深度與第一個的衝突。
即使我們在渲染第二個傳送門檢視時重新繪製第一個傳送門的深度,但這本來是為了在渲染主檢視時保護傳送門 - 第二個傳送門檢視中的物體可能離攝像機更近。因此,深度緩衝區不足以在繪製第二個傳送門時保護第一個傳送門。
有更強大且相關的保護螢幕一部分的方法
- 剪刀 (矩形剪下)
- 模板緩衝區 (任意剪下)
因為我們可以從側面觀察傳送門,或者使用矩形以外的形狀繪製它,所以剪刀不夠,所以我們將使用模板緩衝區。
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glDepthMask(GL_FALSE);
glStencilFunc(GL_NEVER, 0, 0xFF);
glStencilOp(GL_INCR, GL_KEEP, GL_KEEP); // draw 1s on test fail (always)
// draw stencil pattern
glClear(GL_STENCIL_BUFFER_BIT); // needs mask=0xFF
glUniformMatrix4fv(uniform_v, 1, GL_FALSE, transforms[MODE_CAMERA]);
portal->draw();
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glDepthMask(GL_TRUE);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
/* Fill 1 or more */
glStencilFunc(GL_LEQUAL, 1, 0xFF);
glUniformMatrix4fv(uniform_v, 1, GL_FALSE, portal_view);
// -Ready to draw main scene-
想法是檢測攝像機移動(一條線)與傳送門(兩個三角形)之間的交點。
所以我們想要編寫一個函式,檢查由兩個點 la 和 lb 定義的直線是否與傳送門相交。
維基百科來幫忙!
我們有一個很好的矩陣可以計算
- 如果 ,則與平面相交。
- 如果 ,則交點位於三角形內部。
為了在 C++ 中實現它,請注意,在初始化矩陣時,每個 glm::vec3 都是一個 *列* 向量,因此與數學符號相比,這些值是旋轉的。
我們還需要在比較周圍新增一些小值(1e-6),以確保沒有浮點精度問題。
在檢視座標中工作以簡化方程似乎很有誘惑力,但如果你以後想要傳送物體(一個立方體?:),無論如何你都需要在物體座標中工作。
/**
* Checks whether the line defined by two points la and lb intersects
* the portal.
*/
int portal_intersection(glm::vec4 la, glm::vec4 lb, Mesh* portal) {
if (la != lb) { // camera moved
// Check for intersection with each of the portal's 2 front triangles
for (int i = 0; i < 2; i++) {
// Portal coordinates in world view
glm::vec4
p0 = portal->object2world * portal->vertices[portal->elements[i*3+0]],
p1 = portal->object2world * portal->vertices[portal->elements[i*3+1]],
p2 = portal->object2world * portal->vertices[portal->elements[i*3+2]];
// Solve line-plane intersection using parametric form
glm::vec3 tuv =
glm::inverse(glm::mat3(glm::vec3(la.x - lb.x, la.y - lb.y, la.z - lb.z),
glm::vec3(p1.x - p0.x, p1.y - p0.y, p1.z - p0.z),
glm::vec3(p2.x - p0.x, p2.y - p0.y, p2.z - p0.z)))
* glm::vec3(la.x - p0.x, la.y - p0.y, la.z - p0.z);
float t = tuv.x, u = tuv.y, v = tuv.z;
// intersection with the plane
if (t >= 0-1e-6 && t <= 1+1e-6) {
// intersection with the triangle
if (u >= 0-1e-6 && u <= 1+1e-6 && v >= 0-1e-6 && v <= 1+1e-6 && (u + v) <= 1+1e-6) {
return 1;
}
}
}
}
return 0;
}
當此測試檢查時,我們會扭曲相機。這很容易,因為我們已經知道如何使用 portal_view() 計算它的變換矩陣!
/* onIdle() */
glm::mat4 prev_cam = transforms[MODE_CAMERA];
// Update camera position depending on keyboard keys
...
/* Handle portals */
// Movement of the camera in world view
for (int i = 0; i < 2; i++) {
glm::vec4 la = glm::inverse(prev_cam) * glm::vec4(0.0, 0.0, 0.0, 1.0);
glm::vec4 lb = glm::inverse(transforms[MODE_CAMERA]) * glm::vec4(0.0, 0.0, 0.0, 1.0);
if (portal_intersection(la, lb, &portals[i]))
transforms[MODE_CAMERA] = portal_view(transforms[MODE_CAMERA], &portals[i], &portals[(i+1)%2]);
}

至此,您擁有一個基本的傳送門,顯示來自兄弟傳送門的檢視,並在觸碰時進行傳送。
但是,根據速度,您可能會在傳送之前看到螢幕閃爍。在下一節中,我們將嘗試瞭解導致此問題的原因以及如何解決它。
- ↑ 不要與BSP 傳送門混淆。
- ↑ 您可以在BlenderNation(指向BlenderArtists.org 帖子 - 需要論壇註冊)上看到使用 Blender 實現的電視式傳送門。