在过去的几个月中,Facebook充斥了
3D照片 。 如果您看不到它们,那么我将解释:3D照片是帖子内的图像,当滚动页面或将鼠标移到它们上方时,它们会平滑地改变角度。
在此功能出现前的几个月,Facebook用3D模型测试了类似的功能。 尽管您可以轻松了解Facebook如何渲染3D模型并根据鼠标的位置旋转3D模型,但使用3D照片时,情况可能并非如此直观。
Facebook用于创建二维图像的三维度的技术有时称为
高程图偏移量 。 它使用一种称为
视差的光学现象。
什么是视差
如果您玩过《超级马里奥》,那么您确切地知道什么是视差。 尽管Mario的运行速度相同,但似乎背景中的远处物体移动得较慢(请参见下文)。
这种效果产生了一种幻觉,即某些元素(例如山和云)位于更远的地方。 之所以有效,是因为我们的大脑使用视差(以及其他视觉线索)来估计到远处物体的距离。
大脑如何评估距离?假设人脑使用多种机制来估计距离。 在中短距离范围内,通过比较右眼和左眼可见物体的位置差异来计算距离。 这称为
立体视觉,并且在自然界很普遍。
但是,对于足够远的物体,仅凭一个立体视觉是不够的。 山,云和恒星的差异太小,以至于不同的眼睛看不到明显的差异。 因此,相对视差起作用。 后台的对象移动少于前景的对象。 它们的相对运动使您可以设置相对距离。
在距离的感知中,使用了许多其他机制。 其中最著名的是大气雾度,它使远处的物体呈现蓝色。 在其他世界中,大多数这些大气线索都不存在,因此很难评估其他行星和月球上物体的规模。 YouTube用户Alex McCulgan在其
Astrum频道上对此进行了解释,表明确定视频中显示的月球物体的尺寸有多么困难。
视差转变
如果您熟悉线性代数,那么您可能知道3D旋转的数学是多么复杂和不平凡。 因此,有一种更简单的理解视差的方法,它只需要移动即可。
假设我们正在看一个多维数据集(见下文)。 如果我们精确地将其中心对齐,则正面和背面将看起来像两个大小不同的正方形。 这是
前景 。
但是,如果我们向下移动摄像机或向上抬起立方体会发生什么? 应用相同的原理,我们可以看到正面和背面相对于其先前位置发生了偏移。 更有趣的是,他们彼此之间已经相对移动。 离我们更远的背面,好像移动少了。
如果我们要计算投影范围内立方体的这些顶点的真实位置,那么我们将不得不认真考虑三角学。 但是,这并不是必须的。 如果摄影机的运动足够小,那么我们可以近似估计顶点的位移,并按其距离成比例地移动它们。
我们唯一需要确定的是规模。 如果我们向右移动X米,则看起来Y米以外的对象已经移动了Z米。 如果X保持较小,则视差将成为
线性插值而不是三角函数的任务。 从本质上讲,这意味着我们可以根据像素与相机之间的距离移动像素来模拟3D旋转。
生成深度图
原则上,Facebook所做的与Super Mario中所做的没有太大不同。 对于给定的图片,某些像素会根据到相机的距离在运动方向上移动。 要创建Facebook的3D照片,您只需要照片本身和一张地图即可知道每个像素距相机的距离。 这样的地图具有预期的名称-
“深度图” 。 根据上下文
,它也称为
高度图 。
拍摄照片非常简单,但是生成正确的深度图则要困难得多。 现代设备使用各种技术。 大多数情况下使用两个摄像头。 每个人都拍摄相同主题的照片,但视角略有不同。
立体视觉中使用相同的原理,人们用来评估中短距离的深度。 下图显示了iPhone 7如何从两个非常接近的图像创建深度图。
Peter Hedman和
Johannes Kopf在SIGGRAPH2018上发表的文章
Instant 3D Photography中描述了这种重建的实现细节。
创建高质量的深度图后,模拟三维几乎成为一件微不足道的任务。 该技术的真正局限性在于,即使您可以重新创建粗糙的3D模型,它也缺少有关如何渲染原始照片中不可见的零件的信息。 目前,此问题无法解决,因此,在3D照片中可见的所有运动都微不足道。
我们熟悉3D照片的概念,并简要讨论了现代智能手机如何创建它们。 在第二部分中,我们将学习如何使用相同的技术使用着色器在Unity中实现3D照片。
第2部分。视差着色器和深度图
着色器模板
如果要使用着色器重新创建Facebook的3D照片,则必须首先决定要做什么。 由于此效果最适合2D图像,因此实现与Unity Sprite兼容的解决方案是合乎逻辑的。 我们将创建一个可以与
Sprite Renderer一起使用的着色
器 。
尽管可以从头开始创建这样的着色器,但通常最好从现成的模板开始。 最好通过复制现有的精灵漫反射着色器来开始前进,Unity默认将其用于所有精灵。 不幸的是,该引擎没有附带可自行编辑的
着色器文件。
要获取它,您需要转到
Unity下载档案并下载所使用引擎版本的“
内置着色器”包(见下文)。
解压缩软件包后,您可以查看Unity随附的所有着色器的源代码。 我们对
Sprites-Diffuse.shader文件感兴趣,该文件默认情况下用于所有已创建
的Sprite 。
图片
需要形式化的第二个方面是我们拥有的数据。 想象一下,我们既要设置动画图像,又要设置深度图。 后者将是黑白图像,其中黑白像素表示它们距相机的距离或距离。
本教程中使用的图像取自
Dennis Hotson的 Pickle cat 项目 ,这无疑是您今天所能看到的最好的图像。
与此图像相关联的高度图反映了猫的脸距相机的距离。
如此简单的深度图可以很容易地看到如何获得良好的效果。 这意味着为现有图像创建自己的深度图很容易。
属性
现在我们拥有所有资源,我们可以开始编写视差着色器代码。 如果将主图像导入为精灵,则Unity将通过
_MainTex
属性将其自动传递到着色器。 但是,我们需要使深度图可用于着色器。 这可以使用名为
_HeightTex
的新
着色器属性来实现。 我故意决定不将其
_DepthTex
为
_DepthTex
,以免将其与
深度纹理混淆(这是用于渲染场景深度图的类似Unity概念)。
要更改效果的强度,我们还将添加
_Scale
属性。
Properties { ... _HeightTex ("Heightmap (R)", 2D) = "gray" {} _Scale ("Scale", Vector) = (0,0,0,0) }
这两个新属性还应该对应于两个具有相同名称的变量,需要将其添加到
CGPROGRAM
/
ENDCG
:
sampler2D _HeightTex; fixed2 _Scale;
现在一切就绪,我们可以开始编写将执行偏移的代码。
第一步是从深度图采样值,这可以使用
tex2D
函数完成。 由于
_HeightTex
是黑白纹理,因此我们可以采用其红色通道并丢弃其余通道。 结果值测量从当前像素到相机的任意单位距离。
深度值介于
之前
但我们将其延伸到
之前
。 这使您可以提供正(白色)和负(黑色)视差。
理论
为了在此阶段模拟视差效果,我们需要使用深度信息来移动图像的像素。 像素越近,需要移动的越强。 下图说明了此过程。 根据深度图的信息,
原始图像的红色像素应向左移动两个像素。 同样,蓝色像素应向右移动两个像素。
尽管从
理论上讲这应该可行,但是没有简单的方法可以在着色器中实现它。 事实是,着色器根据其原理只能更改
当前像素的颜色。 当执行着色器代码时,它必须在屏幕上绘制一个特定的像素。 我们不能只是将像素移动到另一个地方或更改相邻像素的颜色。
局部性的这种
限制提供了着色器的非常有效的并行操作,但是,如果可以
随机访问记录到图像中的每个像素,则不允许我们实现所有琐碎的效果。
如果我们想精确,那么我们需要对所有相邻像素的深度图进行采样,以找出应该(如果应该)移动到当前位置的像素。 如果几个像素应位于同一位置,则可以平均它们的影响。 尽管这样的系统可以工作并提供最好的结果,但它效率极低,并且可能比我们最初使用的原始漫射着色器慢数百倍。
最好的替代方法是以下解决方案:从深度图获取当前像素的深度; 然后,如果需要将其
向右移动 ,则将当前颜色替换
为左侧的像素(请参见下图)。 在这里,我们假设如果您想向右移动像素,那么左侧的相邻像素也应该以相同的方式移动。
显而易见,这只是我们真正想要实现的目标的低成本近似。 但是,它非常有效,因为深度图通常证明是平滑的。
代号
按照上一节中描述的算法,我们可以使用简单
的UV坐标偏移来实现视差着色器。
这将导致以下代码:
void surf (Input IN, inout SurfaceOutput o) {
如下面的动画所示,该技术几乎可以处理几乎平坦的对象。
但这确实适用于3D模型,因为为3D场景渲染深度纹理非常容易。 以下是3D渲染的图像及其深度图。
完成的结果如下所示: