خلاصه
شش هنرمند برای نقاشی، طراحی و مجسمه سازی در VR دعوت شدند. این فرآیند نحوه ضبط جلسات آنها، تبدیل دادهها و ارائه آنها در زمان واقعی با مرورگرهای وب است.
https://g.co/VirtualArtSessions
چه روزگاری برای زنده بودن! با معرفی واقعیت مجازی به عنوان یک محصول مصرفی، احتمالات جدید و کشف نشده ای در حال کشف شدن هستند. Tilt Brush، محصول Google موجود در HTC Vive، به شما امکان می دهد در فضای سه بعدی نقاشی کنید. وقتی برای اولین بار Tilt Brush را امتحان کردیم، آن احساس طراحی با کنترلرهای ردیابی حرکت همراه با حضور "در اتاقی با قدرت های فوق العاده" در شما باقی می ماند. واقعاً تجربه ای مثل اینکه بتوانید در فضای خالی اطرافتان نقاشی کنید وجود ندارد.
تیم هنرهای داده در Google با چالش نمایش این تجربه به کسانی که هدست واقعیت مجازی ندارند، در وب که Tilt Brush هنوز در آن کار نمیکند، ارائه شد. برای این منظور، این تیم یک مجسمهساز، یک تصویرگر، یک طراح مفهومی، یک هنرمند مد، یک هنرمند اینستالیشن و هنرمندان خیابانی را برای خلق آثار هنری به سبک خود در این رسانه جدید وارد کرد.
ثبت نقاشی ها در واقعیت مجازی
نرم افزار Tilt Brush که در یونیتی ساخته شده است، خود یک برنامه دسکتاپ است که از 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 جزء است. این بعداً هنگامی که strokes ها را بیرون می آوریم برای جلوگیری از قفل گیمبال مهم است.
پخش طرح ها با 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 );
}
هر چهارگانه همچنین حاوی اشعه ماوراء بنفش است که در مرحله بعدی تولید می شود. برخی از قلمها دارای انواع الگوهای ضربهای هستند تا این تصور را ایجاد کنند که هر ضربهای شبیه یک ضربه متفاوت از قلم مو است. این کار با استفاده از _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 );
});
}
از آنجایی که هر طرح دارای تعداد ضربات نامحدودی است و سکته ها نیازی به اصلاح در زمان اجرا ندارند، هندسه stroke را از قبل محاسبه کرده و آنها را در یک مش واحد ادغام می کنیم. حتی اگر هر نوع قلم موی جدید باید متریال خودش باشد، اما باز هم تماس های قرعه کشی ما را به یک براش کاهش می دهد.
برای تست استرس سیستم، طرحی ایجاد کردیم که 20 دقیقه طول کشید و فضا را با هر تعداد رئوس پر کرد. طرح حاصل هنوز با سرعت 60 فریم در ثانیه در WebGL پخش می شود.
از آنجایی که هر یک از رئوس اصلی یک استروک حاوی زمان نیز بود، میتوانیم به راحتی دادهها را پخش کنیم. محاسبه مجدد ضربات در هر فریم واقعا کند خواهد بود، بنابراین در عوض، کل طرح را در بارگذاری از قبل محاسبه کردیم و به سادگی هر چهار را که زمان انجام آن فرا رسید، آشکار کردیم.
مخفی کردن یک چهار به سادگی به معنای فرو ریختن رئوس آن به نقطه 0,0,0 بود. وقتی زمان به نقطهای رسید که قرار است چهارگوشه آشکار شود، راسها را دوباره در جای خود قرار میدهیم.
یک منطقه برای بهبود، دستکاری رئوس به طور کامل در GPU با سایه زن است. پیاده سازی فعلی آنها را با حلقه زدن در آرایه راس از مهر زمانی فعلی، بررسی اینکه کدام رئوس باید آشکار شوند و سپس هندسه را به روز می کند، قرار می دهد. این باعث می شود بار زیادی بر روی CPU وارد شود که باعث چرخش فن و همچنین هدر رفتن عمر باتری می شود.
ضبط هنرمندان
ما احساس کردیم که خود طرح ها کافی نیستند. ما میخواستیم به هنرمندان داخل طرحهایشان را نشان دهیم و هر قلم مو را نقاشی کنند.
برای عکس گرفتن از هنرمندان، از دوربین های کینکت مایکروسافت برای ثبت داده های عمق بدن هنرمندان در فضا استفاده کردیم. این به ما این توانایی را می دهد که شکل های سه بعدی آنها را در همان فضایی که نقاشی ها ظاهر می شوند نشان دهیم.
از آنجایی که بدن هنرمند خود را مسدود میکند و از دیدن آنچه در پشت آن است جلوگیری میکند، از یک سیستم کینکت دوتایی استفاده کردیم، هر دو در طرف مقابل اتاق که به مرکز اشاره میکنند.
علاوه بر اطلاعات عمق، اطلاعات رنگی صحنه را نیز با دوربین های استاندارد DSLR ثبت کردیم. ما از نرم افزار عالی DepthKit برای کالیبره کردن و ادغام فیلم های دوربین عمقی و دوربین های رنگی استفاده کردیم. کینکت قادر به ضبط رنگ است، اما ما استفاده از دوربینهای DSLR را انتخاب کردیم زیرا میتوانستیم تنظیمات نوردهی را کنترل کنیم، از لنزهای زیبای پیشرفته استفاده کنیم و با وضوح بالا ضبط کنیم.
برای ضبط فیلم، یک اتاق ویژه برای HTC Vive، هنرمند و دوربین ساختیم. تمام سطوح با موادی پوشانده شده بودند که نور مادون قرمز را جذب میکردند تا ابر نقطهای تمیزتر به ما بدهد (لحافی روی دیوارها، تشک لاستیکی آجدار روی زمین). در صورتی که مواد در فیلم ابر نقطه نشان داده شد، ما مواد سیاه را انتخاب کردیم تا به اندازه چیزی که سفید بود حواسپرت کننده نباشد.
ضبطهای ویدئویی بهدستآمده اطلاعات کافی برای نمایش یک سیستم ذرات را به ما میداد. ما چند ابزار اضافی در openFrameworks برای تمیز کردن بیشتر فیلم نوشتیم، به ویژه برداشتن کف، دیوار و سقف.
علاوه بر نمایش هنرمندان، قصد داشتیم HMD و کنترلرها را به صورت سه بعدی نیز رندر کنیم. این نه تنها برای نمایش واضح HMD در خروجی نهایی مهم بود (لنزهای انعکاسی HTC Vive خوانشهای IR کینکت را از بین میبردند)، بلکه به ما نقاط تماس را برای اشکال زدایی خروجی ذرات و ردیف کردن ویدیوها با طرح ارائه میداد.
این کار با نوشتن یک افزونه سفارشی در Tilt Brush انجام شد که موقعیتهای HMD و کنترلکنندههای هر فریم را استخراج میکرد. از آنجایی که Tilt Brush با سرعت 90 فریم بر ثانیه اجرا میشود، حجم زیادی از دادهها پخش میشود و دادههای ورودی یک طرح تا 20 مگابایت فشرده نشده است. ما همچنین از این تکنیک برای ضبط رویدادهایی استفاده کردیم که در فایل ذخیرهسازی Tilt Brush معمولی ثبت نشدهاند، مانند زمانی که هنرمند گزینهای را در پانل ابزار و موقعیت ویجت آینه انتخاب میکند.
در پردازش 4 ترابایت دادهای که ما گرفتهایم، یکی از بزرگترین چالشها تراز کردن همه منابع مختلف بصری/داده بود. هر ویدیو از یک دوربین DSLR باید با Kinect مربوطه تراز شود، به طوری که پیکسل ها در فضا و زمان هم تراز شوند. سپس فیلمهای این دو دستگاه دوربین باید با یکدیگر هماهنگ شوند تا یک هنرمند واحد تشکیل شود. سپس ما نیاز داشتیم که هنرمند سه بعدی خود را با داده های گرفته شده از نقاشی آنها هماهنگ کنیم. اوه! ما ابزارهای مبتنی بر مرورگر را برای کمک به اکثر این کارها نوشتیم، و می توانید خودتان آنها را در اینجا امتحان کنید
هنگامی که دادهها تراز شدند، از برخی اسکریپتهای نوشته شده در NodeJS برای پردازش همه آنها و خروجی یک فایل ویدیویی و مجموعهای از فایلهای JSON استفاده کردیم که همگی بریدهشده و همگامسازی شده بودند. برای کاهش حجم فایل سه کار انجام دادیم. ابتدا، دقت هر عدد ممیز شناور را کاهش دادیم تا حداکثر دقت آنها 3 اعشار باشد. دوم، تعداد نقاط را یک سوم به 30 فریم بر ثانیه کاهش دادیم و موقعیت های سمت کلاینت را درون یابی کردیم. در نهایت، دادهها را سریالسازی کردیم، بنابراین به جای استفاده از JSON ساده با جفتهای کلید/مقدار، ترتیبی از مقادیر برای موقعیت و چرخش HMD و کنترلکنندهها ایجاد میشود. این اندازه فایل را به 3 مگابایت کاهش داد که قابل قبول بود برای ارسال از طریق سیم.
از آنجایی که خود ویدیو به عنوان یک عنصر ویدیویی HTML5 ارائه میشود که توسط یک بافت WebGL خوانده میشود تا به ذرات تبدیل شود، خود ویدیو باید در پسزمینه پخش شود. یک سایه زن رنگ های موجود در تصاویر عمق را به موقعیت هایی در فضای سه بعدی تبدیل می کند. جیمز جورج یک مثال عالی از نحوه انجام فیلمهای مستقیم از 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 فشرده است. برای دور زدن این موضوع، ما به سادگی نسخههای کوچکتر از همان ویدیوها را ارائه کردیم که حداقل 30 فریم در ثانیه را در آیفون 6 امکانپذیر میکنند.
نتیجه گیری
توافق کلی برای توسعه نرمافزار VR از سال 2016 این است که هندسهها و سایهبانها را ساده نگه دارید تا بتوانید با سرعت 90+ فریم در ثانیه در HMD اجرا کنید. معلوم شد که این یک هدف واقعاً عالی برای نمایشهای WebGL است، زیرا تکنیکهای مورد استفاده در Tilt Brush بسیار زیبا به WebGL نگاشته میشوند.
در حالی که مرورگرهای وب که شبکههای سه بعدی پیچیده را نمایش میدهند به خودی خود هیجانانگیز نیستند، این دلیلی بر این مفهوم بود که گرده افشانی متقابل کار VR و وب کاملاً ممکن است.