摘要
邀請六名藝術家透過 VR 完成繪畫、設計和雕塑。這是我們記錄工作階段、轉換資料,並在網路瀏覽器中即時呈現的方式。
https://g.co/VirtualArtSessions
真是活著的!隨著虛擬實境做為消費者產品引進 我們發掘了各種新的可能性Tilt Brush 是 HTC Vive 提供的 Google 產品,可讓您運用 3D 空間繪圖。我們第一次使用魔幻畫筆時,對透過動作追蹤的控制器進行繪製的感覺,加上出現「在有超能力的房間中」的感覺。實際上沒有就像在周圍的空白空間中繪圖的經驗。
Google 的資料藝術團隊面臨著挑戰,必須在網路上向沒有 VR 頭戴式裝置的使用者展示這項體驗,但傾斜筆刷尚未運作。為此,我們的團隊邀請了雕塑家、插畫家、概念設計師、時尚藝術家、裝置藝術藝術家、街頭藝術家及街頭藝術家,在這個新媒材中以自己的風格創作藝術作品。
在虛擬實境中錄製繪圖
內建於 Unity 中的傾斜筆刷軟體本身為桌面應用程式,使用空間大小 VR 追蹤頭部位置 (頭戴式顯示器,簡稱 HMD) 和每隻手中的控制器。根據預設,透過 Tilt Brush 建立的圖片會匯出為 .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 播放 Back Sketches
為了在網路瀏覽器中顯示素描,我們使用了 THREE.js 並編寫幾何產生程式碼,以模擬「魔術筆刷」背後的運作方式。
雖然 Tilt Brush 會根據使用者的手部動作即時產生三角形條紋,但在網頁顯示時,草圖的整條內容都已經「完成」。如此一來,在負載時,我們就能略過大部分即時計算作業,並烘焙幾何圖形。
筆劃中的每對頂點都會產生一個方向向量 (連接每個點的藍色線條,如上圖所示,在下方程式碼片段中為 moveVector
)。每個點也都包含方向,一四元代表控制器目前的角度。為了產生三角形列,我們會疊代各個點,產生與方向和控制器方向相關的常態。
每個筆劃計算三角形條紋的程序與傾斜筆刷中所使用的程式碼幾乎相同:
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 點。當到了要顯示 Quad 的時間點時,我們會將頂點重新定位,回到定位。
要改善的部分就是使用著色器完全在 GPU 上處理頂點。目前的實作方式會循環播放目前時間戳記的頂點陣列,檢查需要顯示哪些端點,然後更新幾何圖形。這會對 CPU 造成大量負載,導致風扇旋轉並浪費電池續航力。
錄製藝人
我們認為素描是不夠的。我們想展示藝術家的素描內部,並且為每隻筆刷塗上繪畫。
為了拍攝藝人,我們使用 Microsoft Kinect 攝影機記錄該藝術家在太空中的深度資料。透過這種方式,我們可以在繪圖所在的空間中顯示他們的三維圖形。
由於藝人的身體會導致無法看到幕後的內容,因此我們在房間對中心的對面使用雙 Kinect 系統。
除了深度資訊之外,我們也使用標準數位單眼相機擷取場景的色彩資訊。我們使用優質的 DepthKit 軟體來校正和合併深度相機的影片片段和彩色相機。Kinect 能夠錄製色彩,但我們選擇使用 DSLR,因為我們能控制曝光設定、使用精美的高階鏡頭並錄製高畫質影片。
為了錄製影片,我們打造了特別的房間 其中容納 HTC Vive、藝術家和 相機。所有表面都使用吸收紅外線光的材質,為我們提供更潔淨的點雲 (牆上貼面絲,地板上有絲紋的橡膠墊)。如果資料出現在重點雲片段中,我們選擇了黑色材質,這樣就不會看起來像白色的東西。
產生的錄影內容讓我們有足夠的資訊來投影粒子系統。我們在 openFrameworks 中編寫了一些其他工具,進一步清理影片,尤其是移除樓層、牆壁和天花板。
除了展示藝人,我們也想以 3D 格式呈現 HMD 和控制器。這不僅重要的是,在最終輸出中清楚呈現 HMD (HTC Vive 的反光鏡片從 Kinect 的 IR 讀數丟棄),也讓我們的聯絡窗口提供對粒子輸出內容的偵錯,以及將影片與草圖串連在一起。
具體做法是將自訂外掛程式寫入 Tilt Brush,以擷取每個影格的 HMD 和控制器的位置。由於 Tilt Brush 以 90fps 執行,因此串流處理大量資料,而草圖的輸入資料則在未壓縮的情況下增加 20 MB。我們也使用這項技術擷取一般魔幻筆刷儲存檔案中未記錄的事件,例如藝人在工具面板中選取選項和鏡像小工具位置。
處理我們擷取的 4 TB 資料時,最大的挑戰之一就是協調所有不同的視覺/資料來源。DSLR 相機的每部影片都必須與對應的 Kinect 對齊,這樣像素在空間和時間才會對齊。那麼,這兩款攝影機剪輯的畫面必須對齊,才能形成一位藝術家。接著,我們必須將 3D 藝人與從他們的繪圖擷取的資料對齊大功告成!我們編寫了以瀏覽器為基礎的工具,協助完成大多數的工作。您可以按這裡自行試用。
對齊資料後,我們使用一些以 NodeJS 編寫的指令碼處理資料,並輸出影片檔案和一系列 JSON 檔案,接著已經過修剪及同步處理。為了縮減檔案大小,我們採取了三件事。首先,我們降低每個浮點數的精確度,使其精確度最多只能到小數點後 3 位。第二,我們將路徑點數量減少 30 FPS 到 30 FPS,並在用戶端插入位置。最後,我們將資料序列化,因此不會使用一般 JSON 搭配鍵/值組合,而是建立值的順序,以用於 HMD 和控制器的位置和旋轉。這樣做可將檔案大小縮減為只有 3 MB 的水分,這是可以通過電線傳遞的。
由於影片本身會以 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 );
這種做法原本可大幅降低 iOS 影格速率,因為將像素緩衝區從影片複製到畫布上,會耗用大量 CPU 資源。為瞭解決這個問題,我們只針對在 iPhone 6 上提供 30 FPS 至少 30 FPS 的相同影片,提供相同尺寸的版本。
結論
截至 2016 年為止,VR 軟體開發的普遍共識是讓幾何程式和著色器保持簡單,以便在 HMD 中以 90 fps 執行影片。結果因此成為 WebGL 示範的絕佳目標,因為 Tilt Brush 地圖使用的技術與 WebGL 非常契合。
雖然顯示複雜 3D 網格的網路瀏覽器本身並不令人興奮,但這是一項概念驗證,可以交叉輪詢 VR 工作和網路是否完全沒問題。