Portal Rendering,之前在网上看到过有人翻译成视口渲染。Portal Rendering有点类似于漫威电影里面奇异博士的传送门, 透过传送门可以看到另一个地方的物体,如下图:
对于Portal Rendering,我们需要定义两个Portal,一个用于取景,一个用于显示取景,通过显示Portal可以看到,取景的物体,如下图:
也许你会想到通过渲染到纹理来生成一张取景Portal的取景纹理,然后用将纹理贴到显示Portal上。这样会实现会使得显示Portal看上去像一个2D屏幕,不管你眼睛方向如果其显示的内容都一样。实际Portal Rendering要的效果如同显示Portal与取景Portal连通的,改变眼睛方向可以看到不同内容,为了实现这个效果,需要将取景Portal取景处的物体全部再显示Portal处绘制一次。这种方法主要的难点在于如何确定物体在显示Portal处绘制时的位置。
为了确定物体在显示Portal处绘制的位置,首先需要准备一个位于XOY(Z=0)平面的Portal,Portal的形状可以不用管,但是必须位于XOY平面。如下,是一个正方形数据:
//这是一个正方行,位置:3个float, 法线:3个float,纹理:2个float |
这个Portal既是取景Portal的数据,也是显示Portal的数据。当为取景Portal时,Z轴正方向为取景方向,当为显示Portal时,和OpenGL摄像机一样,Z轴负方向为查看方向。
假设所有物体已经在世界坐标系下,用C1代表物体在世界坐标系下的点。要确定C1在显示Portal处的位置,需要以下几个步骤:
|
第1步相当于把物体与Portal“融为”一体,这个过程可以通过乘以显示Portal世界转换矩阵的逆矩阵来实现。因为显示Portal是显示Z轴负方向,所以第2步需要将物体绕Y轴旋转180度。又因为显示Portal和取景Portal是的模型数据是同一个,两者的本地坐标也就相同,所以经过第3步后就可以确定物体的位置。
设SrcToWorld为取景Portal到世界坐标系的转换矩阵,DesToWorld为显示Portal到世界坐标系下的转换矩阵,代码如下:
SrcToWorld * Matrix:: RotationByAxis(0, 180, 0) * SrcToWorld .inverse() * C1 |
Portal是有显示范围的,当在显示Portal处绘制物体时,必须要限制Portal的绘制区域,不能超出显示Portal。限制方式可以通过纹理,也可以使用模板缓存,这里使用的是模板缓存,代码如下:
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); glDepthMask(GL_FALSE); //不写入深度缓存,防止Poratl的深度缓存遮挡显示的物体 glEnable(GL_STENCIL_TEST);//开启模板测试 glStencilFunc(GL_ALWAYS, 0x1, 0xFF);//将portal区域的模板值替换为0x1 glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); //当深度比较测试通过时模板值替换为0x1 glClearStencil(0xFF); glClear(GL_STENCIL_BUFFER_BIT); //将模板缓存设为0xFF //绘制显示Portal矩形 _program.DrawObject(_portalDes); glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); glStencilFunc(GL_EQUAL, 0x1, 0xFF);//模板值为0x1才绘制,只有portal处才会绘制。 glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); glDepthMask(GL_TRUE); //开始绘制显示Portal的物体 … |
以下是一个 Portal的例子,代码下载地址。
蓝色的矩形为取景Portal。
当取景Portal取景时会取到显示Portal,此时显示Portal就需要显示自己,出现一个递归显示,就如同左右两边放两面镜子,中间放一个物体。在递归显示时,显示Portal内部只会出现显示Portal自己,不会出现取景Portal,递归显示的Portal显示的内容依然是同一个取景Portal内容,取景本身不会递归。
首先可以设置以下递归显示的最大层,控制最多显示多少层递归。其次在递归显示是如果某一层Portal没有绘制任何图元,递归也可以终止。如何确定Portal是否有绘制的,答案是使用遮挡查询,使用遮挡查询可以确定绘制过程中是否有图元通过了深度测试绘制在缓存中。
glEnable(GL_STENCIL_TEST);//开启模板测试 glClearStencil(0x0); glClear(GL_STENCIL_BUFFER_BIT); //将模板缓存设为0x00 Matrix4f addtional = Matrix4f();//额外的位置变换,以确定递归后的位置 for (int i = 0; i< 5; ++i)//最多进行5次递归 { glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); glDepthMask(GL_FALSE); glStencilFunc(GL_EQUAL, i, 0xFF); //当模板等于i时,代表片元为第i层递归,此时可以进行绘制 glStencilOp(GL_KEEP, GL_KEEP, GL_INCR); //当深度比较测试通过时模板值加1,加1的代表当前层portal可绘制的区域 glBeginQuery(GL_ANY_SAMPLES_PASSED, querySamples);//遮挡查询 _program.DrawObject(*des, addtional);//绘制显示Portal glEndQuery(GL_ANY_SAMPLES_PASSED); glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); glStencilFunc(GL_EQUAL, i+1, 0xFF); //绘制当前层portal内的物体,因为前面已经将当前层的模板值加1 glDepthMask(GL_TRUE); glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); GLint queryResult = GL_FALSE; glGetQueryObjectiv(querySamples, GL_QUERY_RESULT, &queryResult);//查询遮挡查询 if (queryResult == GL_FALSE) { break;//遮挡查询没有任何图元被绘制,无需继续绘制。 } //先将世界坐标系下的物体移动到取景portal坐标系下 //再将物体绕Y轴旋转180度,因为OpenGL里面绘制时是绘制视图坐标系Z轴负方向的物体 //最后将物体移动到显示portal坐标系下 Matrix4f portalView = addtional* des->toWorld * Matrix4f::createRotationAroundAxis(0, 180, 0) * src->toWorld.inverse(); DrawScene(portalView); addtional = portalView; //更新递归位置 } glDisable(GL_STENCIL_TEST); |
以下是完整例子,代码下载地址:
镜子特效和Portal的原理一样,需要将物体在镜子后面重新绘制一次,在坐标转换时会有所不同。以下是获取镜面转换矩阵的代码:
//先将世界坐标系下的物体移动到镜子坐标系下 //再将物体做镜像处理,即将Z轴乘以-1 //最后将物体重新移动到世界坐标系下 Matrix4f portalView = _mirror.toWorld * Matrix4f::createScale(1.f, 1.f, -1.f) * _mirror.toWorld.inverse(); |
例子,完整代码下载地址: