虚拟 Art Session

美术课程详情

摘要

我们邀请了 6 位艺术家在 VR 中绘画、设计和雕塑。这是我们记录用户会话、转换数据并通过网络浏览器实时呈现数据的过程。

https://g.co/VirtualArtSessions

多么美好的一天啊!随着虚拟现实作为消费产品的推出,我们发现了许多尚未探索的新可能性。Tilt Brush 是 HTC Vive 上推出的一款 Google 产品,可让您在三维空间中绘图。当我们第一次尝试 Tilt Brush 时,那种使用动作跟踪控制器绘图的感觉,以及“置身于拥有超能力的房间”的感觉,会一直留在您的脑海中;没有任何体验能像在周围的空白空间中绘图一样令人兴奋。

虚拟艺术品

Google 的数据艺术团队面临着一个挑战,即如何在 Tilt Brush 尚不支持的 Web 上向没有 VR 头戴设备的用户展示这一体验。为此,该团队邀请了雕塑家、插画家、概念设计师、时尚艺术家、装置艺术家和街头艺术家,让他们在这项新媒介中创作出各自风格的艺术作品。

在虚拟现实中录制绘图

Tilt Brush 软件本身是内置于 Unity 中的桌面应用,它使用全局范围 VR 跟踪头部位置(头盔式显示器 [HMD])以及您双手中的控制器。在 Tilt Brush 中创建的图形默认会导出为 .tilt 文件。为了将这种体验引入到网络上,我们意识到,除了海报图片数据之外,我们还需要其他数据。我们与 Tilt Brush 团队密切合作,修改了 Tilt Brush,使其以每秒 90 次的速度导出撤消/删除操作以及艺术家的头部和手部位置。

在绘制时,倾斜画笔会获取控制器的位置和角度,并将一段时间内的多个点转换为“笔触”。您可以点击此处查看示例。我们编写了插件来提取这些笔触并将其输出为原始 JSON。

    {
      "metadata": {
        "BrushIndex": [
          "d229d335-c334-495a-a801-660ac8a87360"
        ]
      },
      "actions": [
        {
          "type": "STROKE",
          "time": 12854,
          "data": {
            "id": 0,
            "brush": 0,
            "b_size": 0.081906750798225,
            "color": [
              0.69848710298538,
              0.39136275649071,
              0.211316883564
            ],
            "points": [
              [
                {
                  "t": 12854,
                  "p": 0.25791856646538,
                  "pos": [
                    [
                      1.9832634925842,
                      17.915264129639,
                      8.6014995574951
                    ],
                    [
                      -0.32014992833138,
                      0.82291424274445,
                      -0.41208130121231,
                      -0.22473378479481
                    ]
                  ]
                }, ...many more points
              ]
            ]
          }
        }, ... many more actions
      ]
    }

上述代码段概述了草图 JSON 格式的格式。

在这里,每个笔触都保存为操作,类型为“STROKE”。除了笔触操作之外,我们还希望展示艺术家在素描过程中犯错和改变主意的过程,因此保存“DELETE”操作至关重要,因为它可以用作对整个笔触进行擦除或撤消操作。

系统会保存每条笔触的基本信息,因此会收集画笔类型、画笔大小、颜色 RGB 等信息。

最后,系统会保存笔触的每个顶点,其中包括位置、角度、时间以及控制器的触发器压力强度(在每个点内注明为 p)。

请注意,旋转是一个 4 分量四元数。这在稍后我们渲染笔触时非常重要,以避免出现万向锁定。

使用 WebGL 播放草图

为了在 Web 浏览器中显示草图,我们使用了 THREE.js,并编写了几何图形生成代码,以模仿 Tilt Brush 在后台执行的操作。

虽然 Tilt Brush 会根据用户的手部动作实时生成三角形条状图形,但在我们在网络上显示草图时,整个草图已经“完成”了。这样一来,我们就可以绕过大部分实时计算,并在加载时烘焙几何图形。

WebGL 草图

笔画中的每对顶点都会产生一个方向矢量(如上所示连接每个点的蓝线,在以下代码段中为 moveVector)。每个点还包含一个方向,即一个四元数,表示控制器的当前角度。为了生成三角形条带,我们会迭代这些点中的每一个点,生成垂直于方向和控制器方向的法向量。

计算每条笔触的三角形条纹的过程与 Tilt Brush 中使用的代码几乎完全相同:

const V_UP = new THREE.Vector3( 0, 1, 0 );
const V_FORWARD = new THREE.Vector3( 0, 0, 1 );

function computeSurfaceFrame( previousRight, moveVector, orientation ){
    const pointerF = V_FORWARD.clone().applyQuaternion( orientation );

    const pointerU = V_UP.clone().applyQuaternion( orientation );

    const crossF = pointerF.clone().cross( moveVector );
    const crossU = pointerU.clone().cross( moveVector );

    const right1 = inDirectionOf( previousRight, crossF );
    const right2 = inDirectionOf( previousRight, crossU );

    right2.multiplyScalar( Math.abs( pointerF.dot( moveVector ) ) );

    const newRight = ( right1.clone().add( right2 ) ).normalize();
    const normal = moveVector.clone().cross( newRight );
    return { newRight, normal };
}

function inDirectionOf( desired, v ){
    return v.dot( desired ) >= 0 ? v.clone() : v.clone().multiplyScalar(-1);
}

仅将笔触方向和方向组合在一起会返回在数学上模糊不清的结果;可能会派生多个法向量,并且通常会在几何图形中产生“扭曲”。

在迭代笔触的点时,我们会维护一个“首选右侧”矢量,并将其传递给函数 computeSurfaceFrame()。此函数会为我们提供一个法向量,我们可以根据笔触的方向(从上一个点到当前点)和控制器的方向(四元数)从中派生出四边形条带中的四边形。更重要的是,它还会为下一组计算返回新的“首选右侧”矢量。

笔画

基于每个笔触的控制点生成四边形后,我们会通过从一个四边形到下一个四边形对其角进行插值,合并四边形

function fuseQuads( lastVerts, nextVerts) {
    const vTopPos = lastVerts[1].clone().add( nextVerts[0] ).multiplyScalar( 0.5
);
    const vBottomPos = lastVerts[5].clone().add( nextVerts[2] ).multiplyScalar(
0.5 );

    lastVerts[1].copy( vTopPos );
    lastVerts[4].copy( vTopPos );
    lastVerts[5].copy( vBottomPos );
    nextVerts[0].copy( vTopPos );
    nextVerts[2].copy( vBottomPos );
    nextVerts[3].copy( vBottomPos );
}
融合四边形
合并的四边形。

每个四边形还包含 UV,这些 UV 将在下一步生成。有些画笔包含各种笔触图案,给人以每笔触感都不同于画笔的印象。这通过使用“纹理图集”来实现,其中每个画笔纹理都包含所有可能的变体。通过修改笔触的 UV 值,可以选择正确的纹理。

function updateUVsForSegment( quadVerts, quadUVs, quadLengths, useAtlas,
atlasIndex ) {
    let fYStart = 0.0;
    let fYEnd = 1.0;

    if( useAtlas ){
    const fYWidth = 1.0 / TEXTURES_IN_ATLAS;
    fYStart = fYWidth * atlasIndex;
    fYEnd = fYWidth * (atlasIndex + 1.0);
    }

    //get length of current segment
    const totalLength = quadLengths.reduce( function( total, length ){
    return total + length;
    }, 0 );

    //then, run back through the last segment and update our UVs
    let currentLength = 0.0;
    quadUVs.forEach( function( uvs, index ){
    const segmentLength = quadLengths[ index ];
    const fXStart = currentLength / totalLength;
    const fXEnd = ( currentLength + segmentLength ) / totalLength;
    currentLength += segmentLength;

    uvs[ 0 ].set( fXStart, fYStart );
    uvs[ 1 ].set( fXEnd, fYStart );
    uvs[ 2 ].set( fXStart, fYEnd );
    uvs[ 3 ].set( fXStart, fYEnd );
    uvs[ 4 ].set( fXEnd, fYStart );
    uvs[ 5 ].set( fXEnd, fYEnd );

    });

}
油画笔纹理图集中的四种纹理
油画笔纹理图集中的四种纹理
在 Tilt Brush 中
在倾斜笔刷中
在 WebGL 中
在 WebGL 中

由于每个草图的笔触数量不受限制,并且笔触无需在运行时修改,因此我们会提前预计算笔触几何图形,并将其合并为单个网格。即使每种新画笔类型都必须有自己的材质,但这仍然会将我们的绘制调用次数减少到每个画笔一个。

上述整个草图是在 WebGL 中通过一次绘制调用执行的
上述整个草图是在 WebGL 中通过一次绘制调用执行的

为了对系统进行压力测试,我们花了 20 分钟创建了一张草图,在其中尽可能填充了许多顶点。生成的草图仍在 WebGL 中以 60 fps 的速度播放。

由于笔触的每个原始顶点也包含时间,因此我们可以轻松地回放数据。重新计算每帧的笔触会非常慢,因此我们改为在加载时预计算整个草图,并在需要时直接显示每个四边形。

隐藏四边形只是指将其顶点收缩到 0,0,0 点。当时间达到应显示四边形的位置时,我们会将顶点重新放回原位。

一个有待改进的方面是,完全使用着色器在 GPU 上操控顶点。当前实现是通过从当前时间戳开始循环遍历顶点数组、检查需要显示哪些顶点,然后更新几何图形来放置这些顶点。这会给 CPU 带来大量负载,导致风扇旋转并浪费电池电量。

虚拟艺术品

录制音乐人

我们认为仅提供草图是不够的。我们希望在简笔画中展示艺术家绘制每笔画作的画面。

为了捕捉舞者的动作,我们使用了 Microsoft Kinect 摄像头来记录舞者在空间中的身体深度数据。这样,我们就可以在绘图出现的同一空间中显示其三维图形。

由于舞者的身体会遮挡自己,使我们无法看到背后的情况,因此我们使用了双 Kinect 系统,两个 Kinect 分别位于房间的两侧,朝向房间中央。

除了深度信息之外,我们还使用标准 DSLR 相机捕获了场景的颜色信息。我们使用出色的 DepthKit 软件校准并合并深度相机和彩色相机拍摄的视频片段。Kinect 能够录制彩色视频,但我们选择使用 DSLR 相机,因为我们可以控制曝光设置、使用美观的高端镜头,并以高清画质进行录制。

为了录制视频,我们专门建造了一个房间,用于放置 HTC Vive、舞者和摄像机。所有表面都覆盖了吸收红外光的材料,以便获得更清晰的点云(墙壁上覆盖了杜邦防水布,地板上覆盖了带肋橡胶垫)。为防止材料出现在点云视频片段中,我们选择了黑色材料,以免其像白色材料一样分散注意力。

音乐人

获得的视频录制内容为我们提供了足够的信息,以投影粒子系统。我们在 openFrameworks 中编写了一些其他工具,以进一步清理视频片段,尤其是移除地板、墙壁和天花板。

录制的视频会话的所有四个通道(上面两个色彩通道,下面两个深度通道)
录制的视频会话的所有四个声道(上面两个是彩色声道,下面两个是深度声道)

除了展示音乐人之外,我们还希望以 3D 方式渲染头盔式显示器和控制器。这不仅对于在最终输出中清晰显示头戴式显示器至关重要(HTC Vive 的反射镜片会干扰 Kinect 的红外读数),还为我们提供了调试粒子输出和将视频与草图对齐的接触点。

头戴式显示器、控制器和粒子排列在一起
头戴式显示器、控制器和粒子排列在一起

为此,我们在 Tilt Brush 中编写了一个自定义插件,用于在每一帧中提取头盔显示器和控制器的位置。由于 Tilt Brush 以 90fps 的速度运行,因此会流出大量数据,而草图的未压缩输入数据超过 20MB。我们还使用此技术捕获了未记录在典型 Tilt Brush 保存文件中的事件,例如艺术家在工具面板上选择选项的时间和镜像 widget 的位置。

在处理我们捕获的 4 TB 数据时,最大的挑战之一是协调所有不同的视觉/数据源。来自 DSLR 相机的每个视频都需要与相应的 Kinect 保持一致,以便像素在空间和时间上保持一致。然后,需要将这两个摄像头平台拍摄的画面对齐,以形成单个舞者的画面。然后,我们需要将 3D 设计师与从其绘图中捕获的数据对齐。大功告成!我们编写了基于浏览器的工具来帮助完成大多数此类任务,您可以点击此处自行试用

录音艺术家

数据对齐后,我们使用了一些用 NodeJS 编写的脚本来处理所有数据,并输出一个视频文件和一系列经过剪裁和同步的 JSON 文件。为了缩减文件大小,我们做了三件事。首先,我们降低了每个浮点数的精度,使其精度最多为 3 位小数。其次,我们将点数减少了三分之一,降至 30fps,并在客户端插值了位置。最后,我们对数据进行了序列化,因此系统会为头盔显示器和控制器的位置和旋转创建一个值顺序,而不是使用包含键值对的纯 JSON。这样一来,文件大小就缩减到了略低于 3MB 的大小,可以通过网络传输。

音乐人

由于视频本身是作为 HTML5 视频元素提供的,该元素由 WebGL 纹理读取以转换为粒子,因此视频本身需要在后台播放。着色器会将深度图像中的颜色转换为 3D 空间中的位置。James George 分享了一个绝佳示例,展示了如何使用直接从 DepthKit 导出的素材。

iOS 对内嵌视频播放有限制,我们推测这是为了防止用户被自动播放的网络视频广告骚扰。我们使用了与网络上的其他权宜解决方法类似的技术,即将视频帧复制到画布中,并每 1/30 秒手动更新一次视频跳转时间。

videoElement.addEventListener( 'timeupdate', function(){
    videoCanvas.paintFrame( videoElement );
});

function loopCanvas(){

    if( videoElement.readyState === videoElement.HAVE\_ENOUGH\_DATA ){

    const time = Date.now();
    const elapsed = ( time - lastTime ) / 1000;

    if( videoState.playing && elapsed >= ( 1 / 30 ) ){
        videoElement.currentTime = videoElement.currentTime + elapsed;
        lastTime = time;
    }

    }

}

frameLoop.add( loopCanvas );

由于从视频复制像素缓冲区到画布非常耗费 CPU,因此我们的方法有一个不幸的副作用,即会显著降低 iOS 帧速率。为了解决此问题,我们只需提供相同视频的较小版本,以便在 iPhone 6 上至少达到 30 fps。

总结

截至 2016 年,VR 软件开发领域的普遍共识是,应尽量简化几何图形和着色器,以便在头戴式显示器 (HMD) 中以 90 帧/秒以上的速度运行。事实证明,这是一个非常适合 WebGL 演示的目标平台,因为 Tilt Brush 中使用的技术非常适合 WebGL。

虽然浏览器显示复杂的 3D 网格本身并不令人兴奋,但这证明了 VR 内容和 Web 内容之间的相互交叉完全可行。