portal是我玩过的优秀解密游戏之一,其中最为人称道的就是它的传送门机制以及其中的物理计算。这次就记录如何使用opengl实现他的传送门图形效果。
实现方法应该是不唯一的。这里记录使用模板测试和虚拟相机实现的方法。
模板测试:把画面分割为门内和门外两部分。
虚拟相机:使用虚拟相机渲染门内的场景,使用原始相机(就是玩家位置的相机)渲染门外的场景。
最后把实现过程写为递归函数,实现两个门恰好相对视线无线延伸的效果。
如果称局部坐标变换到世界坐标使用的是模型变换矩阵model,从世界坐标变换到相机坐标使用的相机变换矩阵view。那么相机位置所带来的影响表现为渲染过程中相机变换矩阵view的不同。我们使用两个传送门的model矩阵将原始的view矩阵变换虚拟相机位置对应新的view‘矩阵,使用新的view’矩阵对门内的场景进行渲染。
如上图所示,本来从蓝色相机位置渲染门外场景,我们要计算绿色虚拟相机位置对应的view,这个位置就如同人站在另一个传送门能看到的内容。
约定矩阵对坐标进行右乘,即perspectiveviewmodel*vertex_pos的顺序。那么新的相机变换矩阵计算公式如下
因为玩家在这一次看的是门的正面,在另外一侧就如同从们的背面看过去,所以我们要在中间额外乘一个旋转矩阵。代码看起来就是这个样子。
glm::mat4 destView = viewMat * portal.model
* glm::rotate(glm::mat4(1.0f), glm::radians(180.0f), glm::vec3(0.0f, 1.0f, 0.0f))
* glm::inverse(portal.destPortal->model);
伪代码:
模板缓冲记录数值与递归的层数直接作为模板测试条件,按由内到外的顺序进行渲染,开始渲染前要更新门的深度值,避免门后的顶点对门内已经渲染的物体发生遮挡。
当传送门位于墙壁上时,我们计算得到新的视图变换矩阵,虚拟相机的位置是可能位于一个墙壁的后面的。这时进行渲染可能渲染的结果会被墙壁完全遮挡。为了解决这个问题我们就要使用斜视锥体方法更改近平面位置为门所在平面,让渲染的结果不再包括门后的内容。
斜视锥体的理论推导看这里
具体的就是更改原始投影矩阵的第三列,代码如下:
glm::mat4 const clippedProjMat(glm::mat4 const &viewMat, glm::mat4 projMat) const
{
glm::vec3 Normal = glm::normalize(model * glm::vec4(0, 0, 1, 0));
glm::vec4 clipPlane(Normal,glm::dot(Normal, d_position));//四维向量表示门平面的解析式
clipPlane = glm::inverse(glm::transpose(viewMat)) * clipPlane;
glm::vec4 q;
q.x = (glm::sign(clipPlane.x) + projMat[2][0]) / projMat[0][0];
q.y = (glm::sign(clipPlane.y) + projMat[2][1]) / projMat[1][1];
q.z = -1.0F;
q.w = (1.0F + projMat[2][2]) / projMat[3][2];
glm::vec4 c = clipPlane * (2.0f / glm::dot(clipPlane, q));
projMat[0][2] = c.x;
projMat[1][2] = c.y;
projMat[2][2] = c.z + 1.0F;
projMat[3][2] = c.w;
return projMat;
}
主体代码如下:
//@参数 当前看向的门 当前相机位置对应的view矩阵 投影阵
void drawRecursivePortals(Portal &portal, glm::mat4 const &viewMat, glm::mat4 const &projMat, size_t maxRecursionLevel, size_t recursionLevel)
{
// 禁止颜色缓冲和深度缓冲
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glDepthMask(GL_FALSE);
glDisable(GL_DEPTH_TEST);
//启用模板测试
glEnable(GL_STENCIL_TEST);
//模板测试不通过时加1
glStencilFunc(GL_NOTEQUAL, recursionLevel, 0xFF);
glStencilOp(GL_INCR, GL_KEEP, GL_KEEP);
glStencilMask(0xFF);
//对门框范围进行渲染 更新区域模板数值
portal.draw(viewMat, projMat);
//计算新的view矩阵
glm::mat4 destView = viewMat * portal.model
* glm::rotate(glm::mat4(1.0f), glm::radians(180.0f), glm::vec3(0.0f, 1.0f, 0.0f))
* glm::inverse(portal.destPortal->model);
//最后一层门 渲染景物
if (recursionLevel == maxRecursionLevel)
{
//打开颜色缓冲深度缓冲
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glDepthMask(GL_TRUE);
glClear(GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
//模板测试检验但不可写
glEnable(GL_STENCIL_TEST);
glStencilMask(0x00);
//等于模板数值的通过测试
glStencilFunc(GL_EQUAL, recursionLevel + 1, 0xFF);
//渲染所有普通景物
drawNonPortals(destView, portal.clippedProjMat(destView, projMat));
}
else
{
//更新递归层数 用新的投影矩阵和相机矩阵进行渲染
drawRecursivePortals(portal, destView, portal.clippedProjMat(destView, projMat), maxRecursionLevel, recursionLevel + 1);
}
//关闭深度测试和颜色缓冲 重新把模板缓冲数值减一
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glDepthMask(GL_FALSE);
glEnable(GL_STENCIL_TEST);
glStencilMask(0xFF);
glStencilFunc(GL_NOTEQUAL, recursionLevel + 1, 0xFF);
glStencilOp(GL_DECR, GL_KEEP, GL_KEEP);
portal.draw(viewMat, projMat);
if (recursionLevel > 0) {
//渲染景物
// 关闭模板测试和颜色缓冲 在门的位置记录门的深度
glDisable(GL_STENCIL_TEST);
glStencilMask(0x00);
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glEnable(GL_DEPTH_TEST);
glDepthMask(GL_TRUE);
glDepthFunc(GL_ALWAYS);
glClear(GL_DEPTH_BUFFER_BIT);
portal.draw(viewMat, projMat);
glDepthFunc(GL_LESS);
//打开模板测试 的呢关于模板缓冲数值的通过测试 渲染这一层门内的内容
glEnable(GL_STENCIL_TEST);
glStencilMask(0x00);
glStencilFunc(GL_EQUAL, recursionLevel, 0xFF);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glDepthMask(GL_TRUE);
glEnable(GL_DEPTH_TEST);
drawNonPortals(viewMat, projMat);
}
}
参考资料
斜视锥体深度投影与裁剪
learnopengl中文版