
खास जानकारी
छह कलाकारों को वीआर में पेंटिंग, डिज़ाइन, और मूर्ति बनाने का न्योता दिया गया था. हमने उनके सेशन को रिकॉर्ड करने, डेटा को बदलने, और उसे वेब ब्राउज़र के साथ रीयल-टाइम में दिखाने के लिए, यह प्रोसेस अपनाई.
https://g.co/VirtualArtSessions
वाह! क्या शानदार समय है! उपभोक्ता के लिए वर्चुअल रिएलिटी के तौर पर उपलब्ध कराने के बाद, नई और अनजान संभावनाएं खोजी जा रही हैं. Tilt Brush, HTC Vive पर उपलब्ध Google का एक प्रॉडक्ट है. इसकी मदद से, तीन डाइमेंशन वाले स्पेस में ड्रॉ किया जा सकता है. जब हमने पहली बार Tilt Brush आज़माया, तो मोशन ट्रैक किए गए कंट्रोलर से ड्रॉ करने के साथ-साथ, "सुपर पावर वाले कमरे में होने" की भावना का अनुभव हमें काफ़ी अच्छा लगा. सच में, अपने आस-पास के खाली स्पेस में ड्रॉ करने जैसा अनुभव किसी और चीज़ में नहीं मिलता.

Google की डेटा आर्ट टीम को यह चुनौती मिली थी कि वे इस अनुभव को उन लोगों को दिखाएं जिनके पास वीआर हेडसेट नहीं है. साथ ही, वे इसे वेब पर दिखाएं, जहां Tilt Brush अभी काम नहीं करता. इसके लिए, टीम ने एक मूर्तिकार, एक इलस्ट्रेटर, एक कॉन्सेप्ट डिज़ाइनर, एक फ़ैशन आर्टिस्ट, एक इंस्टॉलेशन आर्टिस्ट, और स्ट्रीट आर्टिस्ट को शामिल किया. इन कलाकारों ने इस नए मीडियम में अपने स्टाइल में आर्टवर्क बनाया.
वर्चुअल रिएलिटी में ड्रॉइंग रिकॉर्ड करना
Unity में बनाया गया Tilt Brush सॉफ़्टवेयर, एक डेस्कटॉप ऐप्लिकेशन है. यह आपके सिर की पोज़िशन (हेड माउंटेड डिसप्ले या एचएमडी) और आपके दोनों हाथों में मौजूद कंट्रोलर को ट्रैक करने के लिए, रूम-स्केल वीआर का इस्तेमाल करता है. 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" होता है. हम चाहते थे कि स्ट्रोक ऐक्शन के साथ-साथ, कलाकार की गलतियों और स्केच के बीच में उसकी सोच में बदलाव को भी दिखाया जाए. इसलिए, "मिटाएं" ऐक्शन को सेव करना ज़रूरी था. यह ऐक्शन, पूरे स्ट्रोक को मिटाने या पहले जैसा करने के तौर पर काम करता है.
हर स्ट्रोक की बुनियादी जानकारी सेव की जाती है, ताकि ब्रश टाइप, ब्रश का साइज़, रंग का आरजीबी वगैरह इकट्ठा किया जा सके.
आखिर में, स्ट्रोक के हर वर्टिक्स को सेव कर दिया जाता है. इसमें पोज़िशन, ऐंगल, समय के साथ-साथ कंट्रोलर के ट्रिगर पर दबाव की तीव्रता भी शामिल होती है. इसे हर पॉइंट में 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 );
}

हर क्वॉड में यूवी भी होते हैं, जो अगले चरण के तौर पर जनरेट होते हैं. कुछ ब्रश में अलग-अलग तरह के स्ट्रोक पैटर्न होते हैं. इससे यह एहसास होता है कि हर स्ट्रोक, पेंट ब्रश के अलग-अलग स्ट्रोक जैसा है. ऐसा _टेक्स्चर एटलसिंग_ का इस्तेमाल करके किया जाता है. इसमें हर ब्रश टेक्स्चर में सभी संभावित वैरिएशन होते हैं. सही टेक्स्चर चुनने के लिए, स्ट्रोक की यूवी वैल्यू में बदलाव किया जाता है.
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 में 60fps पर चलता है.
किसी स्ट्रोक के हर ओरिजनल वर्टिक्स में समय भी शामिल होता है. इसलिए, हम डेटा को आसानी से फिर से चला सकते हैं. हर फ़्रेम के लिए स्ट्रोक का फिर से हिसाब लगाना काफ़ी धीमा होगा. इसलिए, हमने लोड होने पर पूरे स्केच का हिसाब पहले से लगा दिया है. साथ ही, जब भी ज़रूरत पड़ी, हमने हर क्वॉड को दिखा दिया है.
किसी क्वॉड को छिपाने का मतलब है कि उसके वर्टिसेस को 0,0,0 पॉइंट पर ले जाना. जब समय उस बिंदु पर पहुंच जाता है जहां क्वाड को दिखाना होता है, तो हम वर्टिसेस को फिर से अपनी जगह पर ले जाते हैं.
शेडर की मदद से, जीपीयू पर पूरी तरह से वर्टिसेस में बदलाव करने की सुविधा को बेहतर बनाया जा सकता है. फ़िलहाल, मौजूदा टाइमस्टैंप के वर्टिक्स ऐरे में लूप करके, उन्हें दिखाया जाता है. साथ ही, यह भी देखा जाता है कि किन वर्टिक्स को दिखाना है. इसके बाद, ज्यामिति को अपडेट किया जाता है. इससे सीपीयू पर काफ़ी लोड पड़ता है, जिसकी वजह से फ़ैन घूमता है और बैटरी लाइफ़ भी खराब होती है.

कलाकारों को रिकॉर्ड करना
हमें लगा कि सिर्फ़ स्केच से काम नहीं चलेगा. हम चाहते थे कि कलाकारों को उनके स्केच में दिखाया जाए, ताकि हर ब्रशस्ट्रोक को पेंट करते हुए देखा जा सके.
कलाकारों को कैप्चर करने के लिए, हमने Microsoft Kinect कैमरों का इस्तेमाल किया. इससे, हमने कलाकारों के शरीर के डेप्थ डेटा को रिकॉर्ड किया. इससे हमें उनके तीन डाइमेंशन वाले आंकड़े, उसी जगह पर दिखाने की सुविधा मिलती है जहां ड्रॉइंग दिखती हैं.
कलाकार के शरीर के बीच में आने की वजह से, हम उसके पीछे की चीज़ें नहीं देख पाते. इसलिए, हमने कमरे के दोनों तरफ़, बीच की ओऱ फ़ोकस करने वाले दो Kinect सिस्टम का इस्तेमाल किया.
डीपथ की जानकारी के अलावा, हमने स्टैंडर्ड डीएसएलआर कैमरों से सीन की कलर जानकारी भी कैप्चर की है. हमने बेहतरीन DepthKit सॉफ़्टवेयर का इस्तेमाल किया है. इसकी मदद से, हमने डेप्थ कैमरे और कलर कैमरों से मिले फ़ुटेज को कैलिब्रेट और मर्ज किया है. Kinect, कलर रिकॉर्ड कर सकता है. हालांकि, हमने डीएसएलआर का इस्तेमाल किया, क्योंकि हम एक्सपोज़र सेटिंग को कंट्रोल कर सकते थे, बेहतरीन हाई-एंड लेंस का इस्तेमाल कर सकते थे, और हाई डेफ़िनिशन में रिकॉर्ड कर सकते थे.
फ़ुटेज रिकॉर्ड करने के लिए, हमने एक खास रूम बनाया. इसमें HTC Vive, कलाकार, और कैमरा मौजूद था. सभी सतहों को ऐसे मटीरियल से ढका गया था जो इन्फ़्रेरेड लाइट को सोख लेता है. इससे हमें बेहतर पॉइंट क्लाउड (दीवारों पर ड्यूवटीन, फ़्लोर पर रिब वाली रबर मैटिंग) मिलता है. अगर पॉइंट क्लाउड फ़ुटेज में कोई कॉन्टेंट दिखता है, तो हम काले रंग का कॉन्टेंट चुनते हैं, ताकि वह सफ़ेद रंग के कॉन्टेंट की तरह ध्यान न भटका पाए.

वीडियो रिकॉर्डिंग से हमें कण वाले सिस्टम को प्रोजेक्ट करने के लिए ज़रूरी जानकारी मिली. हमने openFrameworks में कुछ और टूल लिखे हैं, ताकि फ़ुटेज को और बेहतर बनाया जा सके. खास तौर पर, फ़र्श, दीवारों, और छत को हटाया जा सके.

हम कलाकारों के साथ-साथ, एचएमडी और कंट्रोलर को भी 3D में रेंडर करना चाहते थे. यह सिर्फ़ आखिरी आउटपुट में एचएमडी को साफ़ तौर पर दिखाने के लिए ज़रूरी नहीं था. HTC Vive के रिफ़्लेक्टिव लेंस, Kinect की आईआर रीडिंग को गच्चा दे रहे थे. इससे हमें पार्टिकल आउटपुट को डीबग करने और वीडियो को स्केच के साथ अलाइन करने के लिए पॉइंट ऑफ़ कॉन्टैक्ट मिले.

ऐसा करने के लिए, Tilt Brush में एक कस्टम प्लग इन लिखा गया था. यह हर फ़्रेम में, एचएमडी और कंट्रोलर की पोज़िशन को निकालता था. Tilt Brush 90fps पर काम करता है. इसलिए, इसमें काफ़ी डेटा स्ट्रीम किया जाता है. साथ ही, स्केच का इनपुट डेटा बिना कंप्रेस किए 20 एमबी से ज़्यादा का होता है. हमने इस तकनीक का इस्तेमाल उन इवेंट को कैप्चर करने के लिए भी किया है जो Tilt Brush की सेव की गई सामान्य फ़ाइल में रिकॉर्ड नहीं किए जाते. जैसे, जब कलाकार टूल पैनल पर कोई विकल्प चुनता है और मिरर विजेट की पोज़िशन तय करता है.
हमने 4 टीबी डेटा कैप्चर किया था. उसे प्रोसेस करने में, अलग-अलग विज़ुअल/डेटा सोर्स को अलाइन करना सबसे बड़ी चुनौती थी. DSLR कैमरे से लिए गए हर वीडियो को, उससे जुड़े Kinect के साथ अलाइन करना ज़रूरी है, ताकि पिक्सल समय के साथ-साथ जगह के हिसाब से भी अलाइन हो जाएं. इसके बाद, इन दोनों कैमरा रिग से मिले फ़ुटेज को एक साथ अलाइन करना पड़ा, ताकि एक ही कलाकार को दिखाया जा सके. इसके बाद, हमें अपने 3D आर्टिस्ट को ड्रॉइंग से कैप्चर किए गए डेटा के साथ अलाइन करना था. वाह! हमने ब्राउज़र पर काम करने वाले टूल बनाए हैं, ताकि इनमें से ज़्यादातर टास्क आसानी से पूरे किए जा सकें. इन्हें यहां आज़माएं

डेटा अलाइन होने के बाद, हमने NodeJS में लिखी गई कुछ स्क्रिप्ट का इस्तेमाल करके, उसे प्रोसेस किया और एक वीडियो फ़ाइल और JSON फ़ाइलों की सीरीज़ को आउटपुट किया. ये सभी फ़ाइलें ट्रिम की गई थीं और सिंक की गई थीं. फ़ाइल का साइज़ कम करने के लिए, हमने तीन काम किए. सबसे पहले, हमने हर फ़्लोटिंग पॉइंट संख्या की सटीक वैल्यू को कम कर दिया, ताकि वे ज़्यादा से ज़्यादा तीन दशमलव वाली वैल्यू पर हों. दूसरा, हमने पॉइंट की संख्या को एक तिहाई घटाकर 30fps कर दिया और क्लाइंट-साइड पर पोज़िशन को इंटरपोल किया. आखिर में, हमने डेटा को क्रम से लगाया, ताकि कुंजी/वैल्यू पेयर के साथ सादे JSON का इस्तेमाल करने के बजाय, एचएमडी और कंट्रोलर की पोज़िशन और रोटेशन के लिए वैल्यू का क्रम बनाया जा सके. इससे फ़ाइल का साइज़ 3 एमबी से भी कम हो गया, जो वायर के ज़रिए डिलीवर करने के लिए ज़रूरी था.

वीडियो को HTML5 वीडियो एलिमेंट के तौर पर दिखाया जाता है. इसे पार्टिकल में बदलने के लिए, WebGL टेक्सचर इसे पढ़ता है. इसलिए, वीडियो को बैकग्राउंड में छिपाकर चलाना ज़रूरी है. शेडर, डीपथ इमेजरी के रंगों को 3D स्पेस में पोज़िशन में बदल देता है. जेम्स जॉर्ज ने एक शानदार उदाहरण शेयर किया है, जिसमें बताया गया है कि 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 के फ़्रेमरेट में काफ़ी कमी आई. इसकी वजह यह है कि वीडियो से कैनवस में पिक्सल बफ़र को कॉपी करने के लिए, सीपीयू का काफ़ी इस्तेमाल होता है. इस समस्या को हल करने के लिए, हमने उन वीडियो के छोटे वर्शन दिखाए जिन पर iPhone 6 पर कम से कम 30fps की सुविधा काम करती है.
नतीजा
साल 2016 तक, VR सॉफ़्टवेयर डेवलपमेंट के लिए आम तौर पर यह सुझाव दिया जाता था कि ज्यामिति और शेडर को आसान बनाएं, ताकि एचएमडी में 90 से ज़्यादा फ़्रेम प्रति सेकंड (एफ़पीएस) पर चलाया जा सके. यह WebGL डेमो के लिए काफ़ी अच्छा विकल्प साबित हुआ, क्योंकि Tilt Brush में इस्तेमाल की गई तकनीकें, WebGL के साथ बहुत अच्छी तरह से काम करती हैं.
वेब ब्राउज़र पर जटिल 3D मेश दिखाना अपने-आप में दिलचस्प नहीं है. हालांकि, यह इस बात का सबूत था कि वेब और वर्चुअल रिएलिटी (वीआर) के बीच क्रॉस-पॉलिनेशन (एक-दूसरे से फ़ायदा पाना) पूरी तरह से मुमकिन है.