
สรุป
โดยได้รับเชิญให้วาดภาพ ออกแบบ และปั้นใน VR นี่คือกระบวนการบันทึกเซสชัน แปลงข้อมูล และแสดงข้อมูลแบบเรียลไทม์ด้วยเว็บเบราว์เซอร์
https://g.co/VirtualArtSessions
ช่างเป็นช่วงเวลาที่ยอดเยี่ยม การเปิดตัวเทคโนโลยีเสมือนจริงเป็นผลิตภัณฑ์สําหรับผู้บริโภคได้เปิดโอกาสให้ค้นพบความเป็นไปได้ใหม่ๆ ที่ไม่เคยมีใครค้นพบมาก่อน Tilt Brush เป็นผลิตภัณฑ์ของ Google ที่พร้อมให้บริการบน HTC Vive ซึ่งช่วยให้คุณวาดภาพในอวกาศ 3 มิติได้ เมื่อลองใช้ Tilt Brush เป็นครั้งแรก ความรู้สึกของการวาดด้วยตัวควบคุมการติดตามการเคลื่อนไหวประกอบกับการได้ "อยู่ในห้องที่มีพลังพิเศษ" จะยังคงอยู่กับคุณไปนาน ประสบการณ์นี้ไม่เหมือนใครจริงๆ กับการวาดภาพในพื้นที่ว่างรอบตัวคุณ

ทีมศิลปะจากข้อมูลของ Google พบปัญหาในการนำเสนอประสบการณ์นี้แก่ผู้ที่ไม่มีชุดหูฟัง VR บนเว็บที่ Tilt Brush ยังไม่พร้อมใช้งาน ด้วยเหตุนี้ ทีมจึงเชิญชวนประติมากร นักวาดภาพประกอบ นักออกแบบคอนเซปต์ ศิลปินแฟชั่น ศิลปินอินสตอลเลชัน และศิลปินสตรีทมาสร้างงานศิลปะในสไตล์ของตนเองภายในสื่อรูปแบบใหม่นี้
การบันทึกภาพวาดใน Virtual Reality
ซอฟต์แวร์ 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" นอกจากการวาดเส้นแล้ว เรายังต้องการแสดงให้เห็นว่าศิลปินทำผิดพลาดและเปลี่ยนใจระหว่างการร่างภาพ ดังนั้นจึงจำเป็นต้องบันทึกการดำเนินการ "ลบ" ซึ่งทำหน้าที่เป็นการดำเนินการลบหรือเลิกทำสำหรับเส้นทั้งหมด
ระบบจะบันทึกข้อมูลพื้นฐานของเส้นแต่ละเส้นไว้ ดังนั้นจึงจะรวบรวมข้อมูลประเภทแปรง ขนาดแปรง สี RGB ทั้งหมด
สุดท้าย ระบบจะบันทึกจุดยอดแต่ละจุดของเส้น ซึ่งรวมถึงตําแหน่ง มุม เวลา ตลอดจนแรงกดของทริกเกอร์ตัวควบคุม (ระบุเป็น p
ภายในแต่ละจุด)
โปรดทราบว่าการหมุนคือควอตเทอร์เนิออน 4 องค์ประกอบ ซึ่งสำคัญในภายหลังเมื่อเราแสดงผลเส้นเพื่อหลีกเลี่ยงการล็อกของ Gimbal
เล่นภาพสเก็ตช์ด้วย 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()
ฟังก์ชันนี้จะให้ค่าปกติซึ่งเรานำมาใช้สร้างรูปสี่เหลี่ยมจัตุรัสในแถบรูปสี่เหลี่ยมจัตุรัสได้ โดยอิงตามทิศทางของเส้น (จากจุดสุดท้ายไปยังจุดปัจจุบัน) และการวางแนวของตัวควบคุม (Quaternion) และที่สำคัญกว่านั้น ฟังก์ชันนี้จะแสดงผลเวกเตอร์ "ขวาที่แนะนำ" ใหม่สําหรับชุดการคํานวณถัดไปด้วย

หลังจากสร้างสี่เหลี่ยมจัตุรัสตามจุดควบคุมของเส้นแต่ละเส้นแล้ว เราจะผสานสี่เหลี่ยมจัตุรัสโดยการหาค่าเฉลี่ยเชิงเส้นของมุมจากสี่เหลี่ยมจัตุรัสหนึ่งไปยังอีกสี่เหลี่ยมจัตุรัสหนึ่ง
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 );
}

แต่ละ Quad ยังมี UV ที่สร้างขึ้นเป็นขั้นตอนถัดไปด้วย แปรงบางรายการมีลายเส้นที่หลากหลายเพื่อให้ดูเหมือนว่าทุกเส้นเป็นเส้นที่วาดด้วยแปรงทาสีที่แตกต่างกัน ซึ่งทำได้โดยใช้ _Texture Atlasing_ โดยพื้นผิวแปรงแต่ละแบบจะมีรูปแบบที่เป็นไปได้ทั้งหมด เลือกพื้นผิวที่ถูกต้องโดยการแก้ไขค่า 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 );
});
}



เนื่องจากภาพสเก็ตช์แต่ละภาพมีจำนวนเส้นขีดไม่จำกัด และไม่จำเป็นต้องแก้ไขเส้นขีดในรันไทม์ เราจึงคำนวณเรขาคณิตของเส้นขีดล่วงหน้าและผสานเข้าด้วยกันเป็นเมชเดียว แม้ว่าแปรงแต่ละประเภทใหม่จะต้องเป็นวัสดุของตัวเอง แต่ก็ยังลดการเรียกใช้การวาดภาพให้เหลือเพียง 1 ครั้งต่อแปรง

ในการทดสอบระบบด้วยภาวะเครียด เราสร้างภาพสเก็ตช์ที่ใช้เวลา 20 นาทีในการเติมพื้นที่ด้วยจุดยอดมากที่สุดเท่าที่จะทำได้ ภาพสเก็ตช์ที่ได้จะยังคงเล่นที่ 60 fps ใน WebGL
เนื่องจากจุดยอดเดิมแต่ละจุดของเส้นขีดมีเวลาด้วย เราจึงเล่นข้อมูลย้อนกลับได้ง่ายๆ การคํานวณเส้นต่อเฟรมใหม่จะช้ามาก เราจึงประมวลผลภาพสเก็ตช์ทั้งหมดล่วงหน้าเมื่อโหลด และแสดงแต่ละรูปสี่เหลี่ยมจัตุรัสเมื่อถึงเวลา
การซ่อนรูปสี่เหลี่ยมหมายถึงการยุบจุดยอดของรูปสี่เหลี่ยมไปยังจุด 0,0,0 เมื่อถึงเวลาที่ควรจะแสดงภาพสี่เหลี่ยมจัตุรัส เราจะจัดตำแหน่งจุดยอดกลับเข้าที่
พื้นที่ที่ควรปรับปรุงคือการดัดแปลงจุดยอดทั้งหมดใน GPU ด้วยโปรแกรมเปลี่ยนสี การใช้งานปัจจุบันจะวางจุดโดยวนผ่านอาร์เรย์เวิร์กเท็กซ์จากการประทับเวลาปัจจุบัน ตรวจสอบว่าต้องแสดงเวิร์กเท็กซ์ใด แล้วอัปเดตเรขาคณิต ซึ่งจะเพิ่มภาระให้กับ CPU เป็นอย่างมาก ทําให้พัดลมหมุนอยู่ตลอดเวลาและทำให้แบตเตอรี่หมดเร็ว

การบันทึกศิลปิน
เราคิดว่าภาพสเก็ตช์เพียงอย่างเดียวนั้นไม่เพียงพอ เราต้องการแสดงให้เห็นศิลปินภายในภาพสเก็ตช์ที่กำลังวาดภาพแต่ละพู่กัน
เราใช้กล้อง Microsoft Kinect เพื่อบันทึกข้อมูลเชิงลึกของร่างกายศิลปินในอวกาศเพื่อจับภาพศิลปิน ซึ่งช่วยให้เราแสดงรูปทรงสามมิติของศิลปินในพื้นที่เดียวกับที่ภาพวาดปรากฏ
เนื่องจากร่างกายของศิลปินจะบดบังกันและทำให้เราไม่เห็นสิ่งที่อยู่ด้านหลัง เราจึงใช้ระบบ Kinect 2 ตัวโดยวางไว้คนละฝั่งของห้องโดยหันตรงกลาง
นอกจากข้อมูลความลึกแล้ว เรายังจับข้อมูลสีของฉากด้วยกล้อง DSLR มาตรฐาน เราใช้ซอฟต์แวร์ DepthKit ที่ยอดเยี่ยมในการปรับเทียบและผสานฟุตเทจจากกล้องวัดความลึกและกล้องสี Kinect สามารถบันทึกสีได้ แต่เราเลือกที่จะใช้กล้อง DSLR เนื่องจากสามารถควบคุมการตั้งค่าการเปิดรับแสง ใช้เลนส์ระดับสูงที่สวยงาม และบันทึกด้วยความละเอียดสูง
เราได้สร้างห้องพิเศษสำหรับติดตั้ง HTC Vive, ศิลปิน และกล้องเพื่อบันทึกฟุตเทจ พื้นผิวทั้งหมดถูกปกคลุมด้วยวัสดุที่ดูดซับแสงอินฟราเรดเพื่อให้เราได้รับจุดเมฆที่สะอาดยิ่งขึ้น (ผ้าคลุมเตียงบนผนัง แผ่นยางร่องบนพื้น) ในกรณีที่วัสดุแสดงในฟุตเทจจุดเมฆ เราเลือกวัสดุสีดําเพื่อไม่ให้รบกวนสายตาเท่ากับวัสดุสีขาว

วิดีโอที่บันทึกไว้ให้ข้อมูลเพียงพอที่จะฉายระบบอนุภาค เราได้เขียนเครื่องมือเพิ่มเติมใน openFrameworks เพื่อล้างฟุตเทจเพิ่มเติม โดยเฉพาะการนำพื้น ผนัง และเพดานออก

นอกจากการแสดงภาพศิลปินแล้ว เรายังต้องการแสดงภาพ HMD และตัวควบคุมเป็นโมเดล 3 มิติด้วย ไม่เพียงแต่จะสำคัญสำหรับการแสดง HMD ในเอาต์พุตสุดท้ายอย่างชัดเจน (เลนส์สะท้อนแสงของ HTC Vive ทำให้การอ่าน IR ของ Kinect ผิดพลาด) แต่ยังทำให้เรามีจุดติดต่อสำหรับการแก้ไขข้อบกพร่องของเอาต์พุตอนุภาคและจัดแนววิดีโอให้สอดคล้องกับภาพร่าง

ซึ่งทำได้โดยการเขียนปลั๊กอินที่กำหนดเองลงใน Tilt Brush เพื่อดึงข้อมูลตำแหน่งของ HMD และตัวควบคุมในแต่ละเฟรม เนื่องจาก Tilt Brush ทำงานที่ 90 fps ระบบจึงสตรีมข้อมูลจำนวนมากออกและข้อมูลอินพุตของภาพสเก็ตช์มีขนาดใหญ่กว่า 20 MB โดยไม่ผ่านการบีบอัด นอกจากนี้ เรายังใช้เทคนิคนี้เพื่อบันทึกเหตุการณ์ที่ไม่ได้บันทึกไว้ในไฟล์บันทึกของ Tilt Brush ทั่วไป เช่น เมื่อศิลปินเลือกตัวเลือกในแผงเครื่องมือและตำแหน่งวิดเจ็ตกระจก
ในการประมวลผลข้อมูล 4 TB ที่เราบันทึกไว้ หนึ่งในความท้าทายที่ใหญ่ที่สุดคือการจับคู่แหล่งข้อมูล/ภาพต่างๆ ทั้งหมด วิดีโอแต่ละรายการจากกล้อง DSLR ต้องจัดแนวกับ Kinect ที่เกี่ยวข้องเพื่อให้พิกเซลจัดแนวกันทั้งในด้านพื้นที่และเวลา จากนั้นต้องจัดแนวฟุตเทจจากกล้อง 2 ตัวนี้ให้สอดคล้องกันเพื่อรวมเป็นศิลปินคนเดียว จากนั้นเราต้องปรับให้ศิลปิน 3 มิติของเราทำงานร่วมกับข้อมูลที่บันทึกไว้จากภาพวาด ในที่สุด เราได้เขียนเครื่องมือที่ใช้เบราว์เซอร์เพื่อช่วยในการทำงานเหล่านี้ส่วนใหญ่ และคุณสามารถลองใช้เครื่องมือเหล่านั้นได้ที่นี่

เมื่อจัดแนวข้อมูลแล้ว เราใช้สคริปต์ที่เขียนด้วย NodeJS เพื่อประมวลผลข้อมูลทั้งหมดและแสดงผลเป็นไฟล์วิดีโอและชุดไฟล์ JSON ที่ตัดและซิงค์แล้ว เราได้ดำเนินการ 3 อย่างเพื่อลดขนาดไฟล์ ขั้นแรก เราลดความแม่นยำของตัวเลขทศนิยมแต่ละตัวเพื่อให้มีความแม่นยำสูงสุด 3 ทศนิยม ประการที่ 2 เราลดจำนวนจุดลง 1 ใน 3 เหลือ 30 fps และหาค่าเฉลี่ยตำแหน่งฝั่งไคลเอ็นต์ สุดท้าย เราจะจัดรูปแบบข้อมูลแทนการใช้ JSON ธรรมดาที่มีคู่คีย์/ค่า โดยระบบจะสร้างลําดับค่าสําหรับตําแหน่งและการหมุนของ HMD และตัวควบคุม ซึ่งทำให้ไฟล์มีขนาดลดลงเหลือเพียง 3 MB ซึ่งยอมรับได้สำหรับการส่งผ่านทางออนไลน์

เนื่องจากวิดีโอเองแสดงเป็นองค์ประกอบวิดีโอ HTML5 ที่อ่านโดยพื้นผิว WebGL เพื่อกลายเป็นอนุภาค วิดีโอจึงต้องเล่นแบบซ่อนอยู่ในเบื้องหลัง โปรแกรมเปลี่ยนสีจะแปลงสีในภาพความลึกเป็นตำแหน่งในพื้นที่ 3 มิติ James George ได้แชร์ตัวอย่างที่ยอดเยี่ยมเกี่ยวกับวิธีใช้ฟุตเทจจาก DepthKit
iOS มีข้อจํากัดในการเล่นวิดีโอในหน้าเว็บ ซึ่งเราคิดว่ามีไว้เพื่อป้องกันไม่ให้ผู้ใช้ถูกรบกวนจากโฆษณาวิดีโอบนเว็บที่เล่นอัตโนมัติ เราใช้เทคนิคที่คล้ายกับวิธีแก้ปัญหาอื่นๆ บนเว็บ ซึ่งก็คือการคัดลอกเฟรมวิดีโอลงใน Canvas และอัปเดตเวลากรอวิดีโอด้วยตนเองทุกๆ 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 เป็นอย่างมาก วิธีแก้ปัญหานี้ก็คือ เราแสดงวิดีโอเดียวกันในเวอร์ชันขนาดเล็กซึ่งเล่นได้อย่างน้อย 30 fps ใน iPhone 6
บทสรุป
ความเห็นทั่วไปสำหรับการพัฒนาซอฟต์แวร์ VR ในปี 2016 คือให้ใช้เรขาคณิตและชิเดอร์แบบง่ายเพื่อให้สามารถทำงานที่ 90+ fps ใน HMD ได้ ซึ่งกลายเป็นเป้าหมายที่ยอดเยี่ยมมากสำหรับการแสดงตัวอย่าง WebGL เนื่องจากเทคนิคที่ใช้ใน Tilt Brush ทำงานร่วมกับ WebGL ได้อย่างลงตัว
แม้ว่าเว็บเบราว์เซอร์ที่แสดงเมช 3 มิติที่ซับซ้อนจะไม่น่าตื่นเต้น แต่นี่เป็นการพิสูจน์แนวคิดว่าการนำงาน VR มาใช้กับเว็บนั้นเป็นไปได้