opengl - 如何实施 "portal rendering"

opengl - how to implement "portal rendering"

过去一周我一直在尝试实现类似游戏 Antichamber 的东西(更准确地说是下面显示的这个技巧):

这是我希望实现的视频(尽管它是用 Unreal Engine 4 完成的;我没有使用它):https://www.youtube.com/watch?v=Of3JcoWrMZs

我查找了执行此操作的最佳方法,并找到了有关模板缓冲区的信息。我在网上找到的 this article and this 代码("drawPortals()" 函数)之间我几乎实现了它。

它与一个通往另一个房间的传送门配合得很好(不是可交叉的传送门,这意味着你不能穿过它并被传送到另一个房间)。在我的例子中,我正在绘制一个通往一个简单的方形房间的入口,里面有一个球体;在入口后面还有另一个球体,我用来检查深度缓冲区是否正常工作并将其绘制在入口后面:

当我在这个门户附近添加另一个门户时,问题就出现了。在那种情况下,我设法正确显示另一个门户(灯光关闭,但右侧的球体颜色不同,表明它是另一个球体):

但是如果我转动相机以便第一个传送门必须绘制在第二个传送门之上,那么第一个传送门的深度就会变得错误,第二个传送门将绘制在第一个传送门之上,如下所示:

虽然它应该是这样的:

所以,这就是问题所在。我可能在深度缓冲区上做错了什么,但我找不到什么。

我的渲染部分代码大致是这样的:

glClear(GL_DEPTH_BUFFER_BIT);
glEnable(GL_STENCIL_TEST);

// First portal
glPushMatrix();

// Disable writing to the color and depht buffer; disable depth testing
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glDepthMask(GL_FALSE);
glDisable(GL_DEPTH_TEST);

// Make sure that the stencil always fails
glStencilFunc(GL_NEVER, 1, 0xFF);

// On fail, put 1 on the buffer
glStencilOp(GL_REPLACE, GL_KEEP, GL_KEEP);

// Enable writing to the stencil buffer
glStencilMask(0xFF);

// Clean the buffer
glClear(GL_STENCIL_BUFFER_BIT);

// Finally draw the portal's frame, so that it will have only 1s in the stencil buffer; the frame is basically the square you can see in the pictures
portalFrameObj1.Draw();

/* Now I compute the position of the camera so that it will be positioned at the portal's room; the computation is correct, so I'm skipping it */

// I'm going to render the portal's room from the new perspective, so I'm going to need the depth and color buffers again
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glDepthMask(GL_TRUE);
glEnable(GL_DEPTH_TEST);
glClear(GL_DEPTH_BUFFER_BIT);

// Disable writing to the stencil buffer and enable drawing only where the stencil values are 1s (so only on the portal frame previously rendered)
glStencilMask(0x00);
glStencilFunc(GL_EQUAL, 1, 0xFF);

// Draw the room from this perspective
portalRoomObj1.Draw();

glPopMatrix();


// Now the second portal; the procedure is the same, so I'm skipping the comments
glPushMatrix();
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_TRUE);
glDepthMask(GL_FALSE);
glDisable(GL_DEPTH_TEST);

glStencilFunc(GL_NEVER, 1, 0xFF);
glStencilOp(GL_REPLACE, GL_KEEP, GL_KEEP);
glStencilMask(0xFF);
glClear(GL_STENCIL_BUFFER_BIT);

portalFrameObj2.Draw();

/* New camera perspective computation */

glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glDepthMask(GL_TRUE);
glEnable(GL_DEPTH_TEST);
glClear(GL_DEPTH_BUFFER_BIT);

glStencilMask(0x00);
glStencilFunc(GL_EQUAL, 1, 0xFF);

portalRoomObj2.Draw();

glPopMatrix();


// Finally, I have to draw the portals' frames once again but this time on the depth buffer, so that they won't get drawn over; first off, disable the stencil buffer
glDisable(GL_STENCIL_TEST);

// Disable the color buffer
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);

glClear(GL_DEPTH_BUFFER_BIT);

// Draw portals' frames
portalFrameObj1.Draw();
portalFrameObj2.Draw();

// Enable the color buffer again
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);


/* Here I draw the rest of the scene */

更新

我设法找出问题所在,但仍然无法解决。它实际上与深度缓冲区无关,而是与模板有关。

基本上,我绘制第一个传送门的方式是这样的: 1) 用1s填充模板缓冲区中的门户框架位;在门户之外只有 0 2) 在模板有 1s 的地方绘制门户房间(以便它被绘制到框架的门户

我在第二个门户中重复此操作。

对于第一个门户,我在第 1 步得到了这样的东西(请原谅愚蠢的图画,我很懒):

然后在第 2 步之后:

那我从第二个传送门开始:

但是现在,在第 1 步和第 2 步之间,我告诉模板只在位为 1 的地方绘制;由于缓冲区现在已清除,我失去了对第一个门户的 1s 的跟踪,因此如果第二个门户的框架的一部分落后于前一个框架,第二个框架的 1 仍将与第二个门户的房间一起绘制,无论深度缓冲区。有点像这张图片:

不知道我解释的对不对...

好久不见了。由于不会有答案(可能),我正在写我现在正在使用的解决方法。

我尝试使用不同的模板功能解决问题,并且在渲染新门户时不清空模板缓冲区;这个想法是利用我通过查看渲染先前门户的模板缓冲区而知道的事实。

最后没能做到;在我在原始问题中发布的示例中,2 个门户之一总是遮挡另一个门户的一部分。

所以,我决定做一些更简单的事情:我检查一个门户是否可见,然后我完全按照我在问题中发布的代码绘制它(所以每次都清空模板缓冲区)。

在伪代码中,我这样做:

for(var i = 0; i < PORTALS_NUMBER; i++) 
{
    // I get the normal to the portal's frame and its position
    var normal = mPortalFramesNormals[i];
    var framePos = mPortalFrames[i].GetPosition();

    // I compute the scalar product between the normal and the direction vector between the camera's position and the frame's position
    var dotProduct = normal * (currentCameraPosition - framePos);

    // If the dot product is 0 or positive, the portal is visible
    if(dotProduct >= 0)
    {
        // I render the portal
        DrawPortal(mPortalFrames[i], mPortalRooms[i]);
    }
}

glDisable(GL_STENCIL_TEST);

// Now I draw the portals' frames in the depth buffer, so they don't get overwritten by other objects in the scene
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glDepthMask(GL_TRUE);
glEnable(GL_DEPTH_TEST);
glClear(GL_DEPTH_BUFFER_BIT);

for(var i = 0; i < PORTALS_NUMBER; i++)
{
    mPortalFrames[i].Draw();
}

glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);

在我的例子中,我需要显示总共 4 个传送门,定位为一个立方体(所以我知道一次最多可以直接看到 2 个传送门)并且框架只有一个面显示传送门,而另一个没有,它工作得很好。

我知道可能有一种更优雅的方法可以通过仅使用模板缓冲区来执行此操作,但现在这对我有用。

如果有人知道更好的方法,我仍然很想知道!

编辑: 我发现了其他版本的模板函数(即 glStencilFuncSeparate、glStencilOpSeparate、glStencilMaskSeparate),它们允许对背面和正面做不同的事情.这似乎是解决我所描述的问题所需要的,但我不会很快尝试这个。我只是指出来,以防有人遇到我同样的问题。

我晚了几年,但是在搜索这个问题时这个问题仍然出现,我也不得不修复它,所以我想我会放弃我的解决方案给任何来的人通过.

1。不清除深度缓冲区(帧之间除外)

您的解决方案的主要问题是您定期清除整个深度缓冲区。每次您这样做时,您都会摆脱任何信息,您可能需要弄清楚要绘制哪些门户网站以及哪些门户网站被遮盖了。您真正需要破坏的唯一深度数据是在设置模板的像素上;剩下的你可以留着。

就在“通过”门户绘制对象之前,像这样设置深度缓冲区:

    // first, make sure you're writing to the depth buffer
    glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
    glDepthMask(GL_TRUE);
    glStencilMask(0x00);
    // you can have the stencil enabled for this step, if you did anything fancy with it earlier (like depth testing)
    glStencilFunc(GL_EQUAL, 1, 0xFF);
    // the depth test has to be enabled to write to the depth mask. But don't worry; it'll always pass.
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_ALWAYS);
    // set the depth range so we only draw on the far plane, leaving a "hole" for later
    glDepthRange(1, 1);
    // now draw the portal object again
    portalFrameObj1.Draw();
    // and reset what you changed so the rest of the code works
    glDepthFunc(GL_LESS);
    glDepthRange(0, 1);
    glColorMask(GL_TRUE,GL_TRUE,GL_TRUE,GL_TRUE); 

现在,当您“通过”门户绘制对象时,它们会显示在需要的位置,但屏幕的其余部分仍将具有过去的深度信息!大家都赢了!

当然,不要忘记用 third portalFrameObj1.Draw() 覆盖深度信息,就像您一直在做的那样。这将有助于下一部分:

2。在设置模板之前检查您的门户是否被遮挡

在代码的开头,当您设置模板时,您会禁用深度测试。你不需要!

glStencilOp 有三个参数:

  • sfail 适用于模板测试失败的情况。
  • dpfail 适用于模板测试成功但深度测试失败的情况。
  • dppass 适用于模板和深度测试都成功(或模板成功但深度测试禁用或没有数据)的情况。

开启深度测试。设置 glStencilFunc(GL_ALWAYS, 0, 0xFF); 而不是 GL_NEVER,因此模板测试成功,并使用 dppass 设置模板而不是 sfailglStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);

现在,您也可以尝试 glStencilFuncSeparate 来稍微优化一下,但您不需要它来让 Portal 相互遮挡。