内部3D Facebook照片:视差着色器

图片

在过去的几个月中,Facebook充斥了3D照片 。 如果您看不到它们,那么我将解释:3D照片是帖子内的图像,当滚动页面或将鼠标移到它们上方时,它们会平滑地改变角度。

在此功能出现前的几个月,Facebook用3D模型测试了类似的功能。 尽管您可以轻松了解Facebook如何渲染3D模型并根据鼠标的位置旋转3D模型,但使用3D照片时,情况可能并非如此直观。

Facebook用于创建二维图像的三维度的技术有时称为高程图偏移量 。 它使用一种称为视差的光学现象。

3D Facebook照片(GIF)的示例

什么是视差


如果您玩过《超级马里奥》,那么您确切地知道什么是视差。 尽管Mario的运行速度相同,但似乎背景中的远处物体移动得较慢(请参见下文)。


这种效果产生了一种幻觉,即某些元素(例如山和云)位于更远的地方。 之所以有效,是因为我们的大脑使用视差(以及其他视觉线索)来估计到远处物体的距离。

大脑如何评估距离?
假设人脑使用多种机制来估计距离。 在中短距离范围内,通过比较右眼和左眼可见物体的位置差异来计算距离。 这称为立体视觉,并且在自然界很普遍。

但是,对于足够远的物体,仅凭一个立体视觉是不够的。 山,云和恒星的差异太小,以至于不同的眼睛看不到明显的差异。 因此,相对视差起作用。 后台的对象移动少于前景的对象。 它们的相对运动使您可以设置相对距离。

在距离的感知中,使用了许多其他机制。 其中最著名的是大气雾度,它使远处的物体呈现蓝色。 在其他世界中,大多数这些大气线索都不存在,因此很难评估其他行星和月球上物体的规模。 YouTube用户Alex McCulgan在其Astrum频道上对此进行了解释,表明确定视频中显示的月球物体的尺寸有多么困难。


视差转变


如果您熟悉线性代数,那么您可能知道3D旋转的数学是多么复杂和不平凡。 因此,有一种更简单的理解视差的方法,它只需要移动即可。

假设我们正在看一个多维数据集(见下文)。 如果我们精确地将其中心对齐,则正面和背面将看起来像两个大小不同的正方形。 这是前景


但是,如果我们向下移动摄像机或向上抬起立方体会发生什么? 应用相同的原理,我们可以看到正面和背面相对于其先前位置发生了偏移。 更有趣的是,他们彼此之间已经相对移动。 离我们更远的背面,好像移动少了。


如果我们要计算投影范围内立方体的这些顶点的真实位置,那么我们将不得不认真考虑三角学。 但是,这并不是必须的。 如果摄影机的运动足够小,那么我们可以近似估计顶点的位移,并按其距离成比例地移动它们。

我们唯一需要确定的是规模。 如果我们向右移动X米,则看起来Y米以外的对象已经移动了Z米。 如果X保持较小,则视差将成为线性插值而不是三角函数的任务。 从本质上讲,这意味着我们可以根据像素与相机之间的距离移动像素来模拟3D旋转。

生成深度图


原则上,Facebook所做的与Super Mario中所做的没有太大不同。 对于给定的图片,某些像素会根据到相机的距离在运动方向上移动。 要创建Facebook的3D照片,您只需要照片本身和一张地图即可知道每个像素距相机的距离。 这样的地图具有预期的名称- “深度图” 。 根据上下文它也称为高度图

拍摄照片非常简单,但是生成正确的深度图则要困难得多。 现代设备使用各种技术。 大多数情况下使用两个摄像头。 每个人都拍摄相同主题的照片,但视角略有不同。 立体视觉中使用相同的原理,人们用来评估中短距离的深度。 下图显示了iPhone 7如何从两个非常接近的图像创建深度图。


Peter HedmanJohannes 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是黑白纹理,因此我们可以采用其红色通道并丢弃其余通道。 结果值测量从当前像素到相机的任意单位距离。

深度值介于 0之前 1但我们将其延伸到 1之前 +1。 这使您可以提供正(白色)和负(黑色)视差。

理论


为了在此阶段模拟视差效果,我们需要使用深度信息来移动图像的像素。 像素越近,需要移动的越强。 下图说明了此过程。 根据深度图的信息, 原始图像的红色像素应向左移动两个像素。 同样,蓝色像素应向右移动两个像素。


尽管从理论上讲这应该可行,但是没有简单的方法可以在着色器中实现它。 事实是,着色器根据其原理只能更改当前像素的颜色。 当执行着色器代码时,它必须在屏幕上绘制一个特定的像素。 我们不能只是将像素移动到另一个地方或更改相邻像素的颜色。 局部性的这种限制提供了着色器的非常有效的并行操作,但是,如果可以随机访问记录到图像中的每个像素,则不允许我们实现所有琐碎的效果。

如果我们想精确,那么我们需要对所有相邻像素的深度图进行采样,以找出应该(如果应该)移动到当前位置的像素。 如果几个像素应位于同一位置,则可以平均它们的影响。 尽管这样的系统可以工作并提供最好的结果,但它效率极低,并且可能比我们最初使用的原始漫射着色器慢数百倍。

最好的替代方法是以下解决方案:从深度图获取当前像素的深度; 然后,如果需要将其向右移动 ,则将当前颜色替换为左侧的像素(请参见下图)。 在这里,我们假设如果您想向右移动像素,那么左侧的相邻像素也应该以相同的方式移动。


显而易见,这只是我们真正想要实现的目标的低成本近似。 但是,它非常有效,因为深度图通常证明是平滑的。

代号


按照上一节中描述的算法,我们可以使用简单的UV坐标偏移来实现视差着色器。

这将导致以下代码:

 void surf (Input IN, inout SurfaceOutput o) { // Displacement fixed height = tex2D(_HeightTex, IN.uv_MainTex).r; fixed2 displacement = _Scale * ((height - 0.5) * 2); fixed4 c = SampleSpriteTexture (IN.uv_MainTex - displacement) * IN.color; ... } 

如下面的动画所示,该技术几乎可以处理几乎平坦的对象。


但这确实适用于3D模型,因为为3D场景渲染深度纹理非常容易。 以下是3D渲染的图像及其深度图。


完成的结果如下所示:

Source: https://habr.com/ru/post/zh-CN444706/


All Articles