小叽导读:AR技术在增强现实方向上的不断努力,目的都是为了让虚拟模型能够更好地与现实环境融为一体,真假分不清楚,达到真实感渲染的体验,从而创造出更好的场景,本文就围绕这种虚拟与真实环境结合的环境纹理渲染技术实现探索来开展。
背景
先来看两个AR效果,真实地面打开效果:
地面被挖开效果:
可以看到真实世界的"地面"作用了虚拟模型的动画效果慢慢打开或铲开,进而再做出虚拟模型的一系列动画渲染效果,给用户一种真实的地面被打开或挖开的效果。那么如何实现这种酷炫的渲染效果呢?
实战初探分步走
第一步:制作简单模型
为了实现这种与地面融合的渲染效果,我们制作一个简单的模型及动画:一个立方体从地面钻出,然后顶部屏幕从中间慢慢左右打开。
使用Maya简单制作一个模型,这里用最普通的立方体模型,让它的最顶端的平面节点位于水平面之下(模型坐标系),同时紧贴在其上增加两个半平面节点用于左右打开效果的动画实现,为了效果看起来不是那么挫(实际最终效果还是很挫,我不是一个合格的UED),在两个半平面节点与立方体顶端平面之间增加一个视频节点,这样当"地面"打开时,从地面钻出个箱子,有个视频在箱子上面播放。
第二步:节点纹理与实景纹理绑定
模型资源已经准备好了,所以为了能与实景融合,我们将相机实时采集的图像每一帧不断的渲染至"模型的平面"(模型节点)所在的纹理上,也就是之前模型制作时的用于左右分开的模型节点上,OpenGL部分核心代码如下:
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _dstTexture, 0);
_dstTexture为获取到的用于左右分开动画的节点目标纹理,我们将相机帧渲染至此纹理,至此,我们可以看到这样的效果:
整个场景都被渲染上来了,我们最终想要的,仅仅是此时该平面真正占据的实景部分,即立方体顶部平面所占据的一小块实景部分,那么意味着,我们需要对纹理进行“裁剪处理”。
第三步:对实景纹理进行裁剪
如何进行准确的裁剪?并且随着模型的旋转、平移、缩放都能够不断更新且准确地裁剪到对应的纹理区域?
那么我们首先想到的是模型的顶部平面在未结合环境纹理是怎么被渲染到屏幕的?
首先模型通过mesh文件提取出模型顶点信息,渲染管线会在所有顶点着色器运行后,将我们所有可见的顶点都变为标准化设备坐标。也就是说,每个顶点的x,y,z坐标都应该在-1.0到1.0之间,超出这个坐标范围的顶点都将不可见,顶点坐标经过不同的坐标系变换类似流水线的方式拿到标准化设备坐标,即经过如下不同的坐标系空间:
- 局部空间(Local Space,或者称为物体空间(Object Space))
- 世界空间(World Space)
- 观察空间(View Space,或者称为视觉空间(Eye Space))
- 裁剪空间(Clip Space)
- 屏幕空间(Screen Space)
这些就是我们将所有顶点转换为片段之前,顶点需要处于的不同的状态,即大家熟知的MVP。
如下图的变换过程:
局部坐标是对象相对于局部原点的坐标,也是对象开始的坐标。即该例中立方体的上平面在局部坐标中的位置。
将局部坐标转换为世界坐标,世界坐标是作为一个更大空间范围的坐标系统。这些坐标是相对于世界的原点的。
接下来我们将世界坐标转换为观察坐标,观察坐标是指以摄像机或观察者的角度观察的坐标
在将坐标处理到观察空间之后,我们需要将其投影到裁剪坐标。裁剪坐标是处理-1.0到1.0范围内并判断哪些顶点将会出现在屏幕上。
最后,我们需要将裁剪坐标转换为屏幕坐标,我们将这一过程称为视口变换(Viewport Transform)。视口变换将位于-1.0到1.0范围的坐标转换到由glViewport函数所定义的坐标范围内。最后转换的坐标将会送到光栅器,由光栅器将其转化为片段。
矩阵公式为:
顶点着色器的输出需要所有的顶点都在裁剪空间内,然后OpenGL在裁剪空间中执行透视划分从而将它们转换到标准化设备坐标,在这个过程中将位置向量的x,y,z分量分别除以向量的齐次w分量;透视划分是将4维裁剪空间坐标转换为3维标准化设备坐标。这一步会在每一个顶点着色器运行的最后被自动执行。OpenGL会使用glViewPort内部的参数来将标准化设备坐标映射到屏幕坐标,每个坐标都关联了一个屏幕上的点。
顶点坐标信息通过以上的MVP变换最终被赋予顶点着色器中的gl_Position且OpenGL将会自动进行透视划分和裁剪。
通过以上过程,我们已经知道了箱子的顶部平面是经过 MVP矩阵变换+透视划分的顶点着色器处理送给光栅化处理的了,那么我们可以将相同的处理作用给环境纹理,从而确定了我们纹理应该裁剪得到哪一部分环境纹理。但这里有一个问题,我们通过MVP矩阵变换+透视划分拿到的坐标区域是OpenGL坐标系下的,而纹理映射是UV坐标下的。
20180919172334.png
OpenGL坐标系是从(-1,-1)到(1,1)而UV坐标是(0,0)到(1,1)
所以我们需要对结果坐标position从OpenGL变换至UV坐标系下:
x = position.x/2.0 + 0.5,
y = position.y/2.0 + 0.5
至此,我们通过最初的顶点信息经过
- MVP矩阵变换
- 透视划分
- OpenGL至UV坐标系变换
拿到了需要裁剪的纹理坐标。
通过以上处理最终作用到纹理坐标v_texCoord上,看下效果:
目前环境纹理渲染效果已经在支付宝10.1.35版本的星宝中使用(星宝入口:支付宝首页搜索“AR星宝”生活号→进入“AR星宝”),效果如下:
遇到的问题
在实际的应用中,我们绑定了环境纹理后,便不希望随着模型节点的RT(旋转、移动)再去更新节点的环境纹理,即不希望环境纹理有更新,从而才能产生真实的“地面被挖开”等效果。我们通过记下需要停止更新时刻的MVP,后续都使用该MVP去计算纹理裁剪坐标即可。
但是在设计节点动画时,设计师为了酷炫的动画效果,会针对要绑定环境纹理的节点做一系列骨骼动画的处理,当我们对有骨骼动画的节点做环境纹理处理的时候,遇到环境纹理还是会不断更新的问题,细究原因,骨骼动画即是对顶点坐标在MVP处理之前有一系列M的动画矩阵运算达到动画效果,那么,我们要么去缓存下每个顶点在骨骼动画前的信息,面对上万的顶点信息,这不太现实,于是,我们做一个取舍,不管有没有骨骼动画,在停止更新时都使用未经过处理(骨骼动画)的position,这样就避免了骨骼动画对于环境纹理的影响。
扩展
这种将现实世界环境纹理与虚拟物体结合起来渲染的技术目前应用的场景也较多,比如虚拟球体的环境镜面反射效果,通过将环境纹理映射到球体表面:
包括WWDC2018苹果结合ARKit2.0做的环境纹理(Environment Texturing),现实世界环境被渲染在立方体贴图中,可被用于作为渲染引擎中的反射探针(Reflection Probe)。此反射探针可应用环境纹理在虚拟物体上,这将大大提升了会反射的物体的视觉效果。