摘要
六位艺术家受邀在 VR 中绘画、设计和雕刻。这是 关于我们如何记录专题演讲、转换数据以及展示 并可通过网络浏览器实时处理
https://g.co/VirtualArtSessions
多么幸存的生活!随着虚拟现实的出现, 也不断探索新的和尚未探索的可能性。Tilt Brush HTC Vive 上提供的 Google 产品 二维空间。我们首次试用 Tilt Brush 时, 使用动作跟踪控制器,再加上“处于 拥有超能力的房间”一直与您相伴; 比如能够在周围的空白空间中绘画
向 Google 数据艺术团队展示这一挑战, 没有 VR 头戴设备的用户也能通过 Tilt Brush 无法在网页上 运行。为此,该团队邀请了一位雕塑家、一位插画家和一位 概念设计师、时尚艺术家、装置艺术家和街头艺术家 在这个新媒介中以自己独特的风格创作艺术作品。
在虚拟实境中录制绘图
Tilt Brush 软件是基于 Unity 构建的桌面应用,
使用可配备房间级别的 VR 追踪头部位置(头戴式显示屏,简称 HMD)
和每只手中的控制器使用 Tilt Brush 创建的海报图片是
默认导出为 .tilt
文件。为了将这种体验引入到网上,我们
意识到我们需要的不仅仅是艺术作品数据。我们与
Tilt Brush 团队对 Tilt Brush 进行了修改,以便同时导出撤消/删除操作
以每秒 90 次的频率展示艺术家的头部和手部位置。
绘图时,Tilt Brush 会获取控制器的位置和角度,并进行转换 形成一个“笔画”您可以参阅 此处。 我们编写了可提取这些笔画并将其输出为原始 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
)
)。
请注意,旋转是一个四分量四元数。这在以后 我们将笔画呈现出来,以避免万向锁定
使用 WebGL 播放素描
为了在网络浏览器中显示这些草图,我们使用了 THREE.js,并编写了模仿 Tilt Brush 在后台执行的操作。
Tilt Brush 根据用户的手部实时生成三角形条纹 整个草图都已经“完成”显示 。这让我们可以绕过许多实时计算, 加载几何图形时
笔画中的每对顶点都会生成一个方向矢量(蓝色线条
连接每个点,如上文代码段中的 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 值来选择正确的纹理 。
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 );
});
}
由于每个草图的笔画数量没有限制,因此笔画无需 我们会提前计算描边几何图形, 将它们放到一个网格中尽管每种新画笔类型都必须有自己的 这样仍然将绘制调用减少到每个笔刷一个。
<ph type="x-smartling-placeholder">为了对系统进行压力测试,我们创建了一个草图,该草图需要 20 分钟来填充 添加尽可能多的顶点生成的素描仍继续播放 WebGL 中为 60fps。
由于描边的每个原始顶点也包含时间,因此我们可以 轻松回放数据重新计算每帧的笔画 所以我们在加载时预先计算了整个草图,然后显示 每个组。
隐藏四边形仅仅是将其顶点收缩到 0,0,0 点。当 显示四边形的时间到了, 调整顶点的位置
一个有待改进的方面是,使用以下代码完全在 GPU 上操控顶点: 着色器。当前实现通过循环遍历顶点来进行放置 从当前时间戳中获取的数组,检查需要显示哪些顶点 然后更新该几何图形。这会给 CPU 带来很大的负载 而且会浪费电池电量
录制音乐人作品
我们认为单靠草图是不够的。我们希望展示 绘画作品中
为了拍摄艺术家的作品 我们使用 Microsoft Kinect 照相机来记录 音乐人的数据,在太空中的身体这样,我们就能够展示 同时呈现三维图形
因为艺术家的身体会自我遮挡,使我们看不到 在它后面,我们使用了双 Kinect 系统,两边分别位于房间的两边 都指向中心
除了深度信息,我们还捕获了 使用标准数码单反相机拍摄场景我们使用了 DepthKit 软件:用于校准和合并 用深度相机和彩色相机拍摄的视频画面。Kinect 能够 但我们选择使用数码单反相机,因为我们可以控制 曝光设置,使用精美的高端镜头,并以高清画质进行录制。
为了录制视频片段,我们建造了一个特殊房间来存放艺术家 HTC Vive 和相机。所有表面都覆盖了能够吸收红外的材料 赋予我们更清洁的点云 铺地板)。如果材料出现在点云中 因此我们选择了黑色材质 那是白色的
所生成的视频录像为我们提供了足够的信息,以便投射出粒子 系统。我们编写了一些额外的工具, openFrameworks,以便进一步清理视频片段, 尤其是要去除地板、墙壁和天花板
<ph type="x-smartling-placeholder">除了显示艺术家的画面外,我们还想渲染 HMD 和 以及 3D 游戏控制器。这不仅对于在 Android Vitals 中显示 HMD 很重要 确保最终输出清晰(HTC Vive 的反光镜头脱落 Kinect 的红外线读数),也就为我们提供了调试粒子的接触点 输出并将视频与简笔画对齐。
<ph type="x-smartling-placeholder">方法是将一个自定义插件写入 Tilt Brush 以提取 每一帧中 HMD 和控制器的位置。由于 Tilt Brush 的运行速度是 90fps, 输入大量数据,而一个草图的输入数据超过 20 MB 未压缩。我们还使用这种方法来捕获未记录的事件 保存文件,例如当音乐人选择某个选项时 以及镜像微件的位置
在处理我们捕获的 4TB 数据时,最大的挑战之一就是 调整所有不同的可视化/数据源。来自数码单反相机的每个视频 需要与相应的 Kinect 对齐 空间和时间然后,这两个摄像机绑定的视频片段 相互对齐,形成一位艺术家。然后,我们需要对齐 3D 对象 生成自己的绘图数据大功告成!我们编写了基于浏览器的 可帮助完成其中的大部分任务的工具,您也可以亲自试用 此处
对齐数据后,我们使用一些以 NodeJS 编写的脚本来处理数据 并输出视频文件和一系列 JSON 文件 已同步。为了缩减文件大小,我们做了三项工作。首先,我们减少了 每个浮点数的准确性,使其不超过 3 十进制的精度值。其次,我们将点数减三分之一 30fps,并在客户端插入位置。最后,我们将 因此值的顺序是 为 HMD 和控制器的位置和旋转而创建的。这会剪切文件 小到仅 3MB,通过网络传输是可以接受的。
由于视频本身以 HTML5 视频元素的形式投放, WebGL 纹理才能成为粒子,视频本身需要播放隐藏在 背景。着色器将深度图像中的颜色转换为 3D 空间。James George 分享了一个很好的例子 如何直接利用 DepthKit 中的视频片段。
iOS 对内嵌视频播放有一些限制,我们假设 用户避免被自动播放的网络视频广告所困扰。我们采用了一种技术 与网站上的其他解决方法类似, web,也就是将 并手动更新视频跳转时间,即每隔 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 );
不幸的是,我们的方法有一个副作用,即大幅降低 iOS 因为将像素缓冲区从视频复制到画布 CPU 密集型。为了解决这个问题,我们只提供较小尺寸版本的 在 iPhone 6 上允许帧速率至少为 30fps 的视频。
总结
截至 2016 年,有关 VR 软件开发工作的共识是: 几何和着色器,以便您可以在 HMD 中以 90+fps 的速度运行。这个 后来被证明是 WebGL 演示的理想目标 Tilt Brush
虽然在一些国家/地区,网络浏览器显示复杂的 3D 网格并不令人振奋, 这也是一种概念验证,对 VR 作品和 网络是可能的