摘要
六位藝術家受邀在 VR 中繪畫、設計及雕塑。這是我們如何記錄工作階段、轉換資料,並透過網路瀏覽器即時呈現這些資料的程序。
https://g.co/VirtualArtSessions
「活起來」!隨著虛擬實境技術成為消費性產品問世,人們開始探索無限的可能。「傾斜筆刷」是 HTC Vive 上提供的 Google 產品,可讓您在 3D 空間中畫出圖形。我們第一次嘗試傾斜筆刷時,如果選擇使用動作追蹤控制器來繪圖,就會感受到「在房間有超能力的房間」下待你,因此真正有如能在周遭空白空間中畫出來的效果。
Google 的資料藝術團隊提出了一項挑戰,希望讓沒有 VR 頭戴式裝置的使用者能夠在網路上使用傾斜筆觸功能。為了達成這個目標,我們的團隊邀請了雕塑家、插畫家、概念設計師、時尚藝術家、裝置藝術藝術家和街頭藝術家,打造符合自身風格的新媒介。
以虛擬實境技術錄製繪圖
內建於 Unity 中的傾斜筆刷軟體是電腦應用程式,透過空間縮放 VR 追蹤頭部位置 (頭戴式螢幕或 HMD) 和每隻手中的控制器。根據預設,使用傾斜筆刷建立的圖片會匯出為 .tilt
檔案。為了將這項服務引進網路
我們實現了除了圖片資料以外我們與魔幻畫筆團隊密切合作,以修改傾斜筆刷,以每秒 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 播放後素描寫
為了在網路瀏覽器中顯示素描,我們使用 THREE.js 並編寫幾何產生程式碼,讓「Tlt 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 );
});
}
由於每個草圖的筆劃數量不受限,而且筆劃不需要在執行階段中修改,因此我們預先計算筆劃幾何圖形,然後將這些筆劃合併為一個網格。即使每種新筆刷類型都必須具備自己的材質,仍會減少每個筆刷的繪製呼叫次數。
為了對系統進行壓力測試,我們建立了一套草圖,花費 20 分鐘來填滿空間,並盡可能加入最多的頂點。產生的草圖仍會在 WebGL 中以 60 FPS 播放。
由於筆劃的原始頂點也包含時間,因此可以輕鬆播放資料。重新計算每個影格的筆觸會非常緩慢,因此我們在載入時預先計算整個草圖,並在需要時逐一顯示每個四元組。
隱藏四角形就是將頂點收合到 0,0,0 點。當時間達到四點應顯示的點時,我們就會將頂點重新定位。
改善的部分是用著色器完全操控 GPU 上的頂點。目前的實作方式是循環檢視目前時間戳記的頂點陣列,檢查需要顯示哪些端點,然後更新幾何圖形。這會對 CPU 造成大量負載,導致風扇旋轉以及浪費電池續航力。
為藝人錄製
我們覺得自己草圖是不夠的。我們想讓藝人親自體驗他們素描的畫面,為每次筆刷畫出筆跡。
為了拍攝這些藝術家,我們使用 Microsoft Kinect 相機記錄藝術家在太空中人的深度資料。如此一來,我們就能在顯示繪圖的相同空間中 呈現三個維度圖形
因為藝術家的身體會遭到遮擋,導致我們無法查看背後的內容,因此我們在房間的對面使用雙 Kinect 系統,讓兩個 Kinect 的系統位於房間中心的對面。
除了深度資訊外,我們也使用標準數位單眼相機擷取場景的色彩資訊。我們使用出色的 DepthKit 軟體校正及合併深度相機和彩色鏡頭拍攝的片段。Kinect 可以記錄色彩,但我們選擇使用數位單眼相機,因為我們可以控制曝光設定、使用美麗的高階鏡頭,以及錄製高畫質內容。
為了錄製影片,我們特別建立一個特別的房間,來存放 HTC Vive、藝術家和相機。所有表面都上面有吸收紅外線的材料,可提供更乾淨的點雲 (牆壁上的灰泥,在地板上灌木叢)。如果素材出現在雲端片段中,我們選擇了黑色材料,因此不會像白色的東西一樣令人分心。
產生的錄影結果提供了充足資訊,可用來投影粒子系統。我們在 openFrameworks 中編寫了一些其他工具,希望進一步清理影像片段,尤其是移除地板、牆壁和天花板。
除了展示藝人作品之外,我們也希望以 3D 模式算繪 HMD 和控制器。這不僅有助於清楚呈現最終輸出內容中的 HMD (HMD Vive 的反光鏡射掉了 Kinect 的 IR 讀數),還讓我們的聯絡窗口能對粒子輸出內容進行偵錯,以及將影片與草圖彙整在一起。
做法是將自訂外掛程式寫入 Tilt Brush,然後擷取 HMD 的位置和每個影格的位置。自從傾斜筆刷以每秒 90 個影格的速度執行,因此串流的大量資料已串流,且草圖的輸入資料超過 20 MB 未壓縮。我們也使用這項技巧擷取未記錄於一般「傾斜筆刷」儲存檔案中的事件,例如藝術家在工具面板中選取選項時,以及鏡像小工具的位置。
在處理我們擷取的 4 TB 資料時,最大的挑戰之一,就是必須對齊不同的視覺/資料來源。DSLR 相機的每部影片都必須與對應的 Kinect 對齊,以便讓像素在空間中與時間對齊。然後,這兩個鏡頭架的片段必須彼此對齊,才能形成單一藝術家。然後,我們需要將 3D 藝人與從圖畫擷取的資料對齊。大功告成!我們編寫了瀏覽器式工具,來協助處理大部分的工作,您可以在這裡自行試用這些工具