ملخّص
تمت دعوة ستة فنانين للرسم والتصميم والنحت في الواقع الافتراضي. هذه هي العملية الخاصة بكيفية تسجيلنا لجلساتهم، وتحويل البيانات وعرضنا في الوقت الفعلي باستخدام متصفحات الويب.
https://g.co/VirtualArtSessions
يا له من وقت رائع للعيش! ومع إطلاق الواقع الافتراضي كمستهلك المنتج، واكتشاف الاحتمالات الجديدة وغير المستكشفة. إمالة Brush يتيح لك منتج Google المتوفر على هاتف HTC Vive إمكانية الرسم ثلاثة مساحة ذات أبعاد. فعندما جربنا استخدام ميزة "الريشة السحرية" للمرة الأولى، كان الشعور للرسم باستخدام وحدات تحكم تتبع الحركة إلى جانب وجود "في غرفة بقوى خارقة" يبقى معك؛ لا توجد حقًا تجربة مثل القدرة على الرسم في المساحة الفارغة من حولك.
تم توجيه التحدي المتمثل في عرض هذا إلى فريق فنون البيانات في Google تجربة الأشخاص الذين لا يستخدمون سماعة رأس VR، على الويب حيث لا تتوفر تعمل. لتحقيق هذه الغاية، أحضر الفريق نحاتًا ورسامًا مصمم مفهوم، وفنان أزياء، وفنان تركيبات، وفنانو شوارع لإنشاء أعمال فنية بأسلوبهم الخاص ضمن هذه المنصة الجديدة.
تسجيل الرسومات في الواقع الافتراضي
برنامج إمالة Brush المصمَّم في Unity هو تطبيق سطح مكتب
يستخدم الواقع الافتراضي على مستوى الغرفة لتتبع موضع رأسك (شاشة مثبتة على الرأس أو 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" الإجراءات التي تعمل يمكنك إما محو أو التراجع عن الإجراءات للضربة بأكملها.
يتم حفظ المعلومات الأساسية لكل فرشاة، وبالتالي نوع الفرشاة وحجم الفرشاة ولونها. التي يتم جمعها باستخدام نموذج أحمر أخضر أزرق.
أخيرًا، يتم حفظ كل رأس من الحد الخارجي ويشمل الموضع،
والزاوية والوقت وقوة الضغط لتشغيل وحدة التحكم (يشار إليها بالاختصار p
داخل كل نقطة).
تجدر الإشارة إلى أنّ الدوران هو أربعة مكونات. يعد ذلك مهمًا لاحقًا عندما نعرض الضغطات لتجنب قفل حامل برأس جمبل.
تشغيل الرسومات الخلفية باستخدام WebGL
لعرض الرسومات في متصفح ويب، استخدمنا THREE.js وكتبت رموز إنشاء الأشكال الهندسية التي تحاكي ما تفعله ميزة "الريشة السحرية" في الداخل.
تقوم ميزة "الريشة السحرية" بإنتاج شرائط مثلثات في الوقت الفعلي بناءً على يد المستخدم الحركة، يتم بالفعل "إنهاء" الرسم بالكامل بحلول الوقت الذي نعرضه فيه على الويب. وهذا يتيح لنا تجاوز الكثير من العمليات الحسابية في الوقت الفعلي الهندسة عند التحميل.
ينتج كل زوج من الرؤوس في السكتة الدماغية متجهًا للاتجاه (الخطوط الزرقاء
تربط كل نقطة كما هو موضح أعلاه، 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 );
}
يحتوي كل تربيع أيضًا على الأشعة فوق البنفسجية التي يتم إنشاؤها كخطوة تالية. استخدام بعض الفرشاة تحتوي على مجموعة متنوعة من أنماط الخطوط لإعطاء انطباع بأن كل ضربة وكأنه ضغط مختلف على فرشاة الطلاء. يتم تحقيق ذلك باستخدام أطلس _texture، _حيث تحتوي كل زخرفة فرشاة على جميع القيم الممكنة المختلفة. يتم تحديد الهيئة الصحيحة عن طريق تعديل قيم الأشعة فوق البنفسجية سكتة دماغية.
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 دقيقة لملء مساحة تحتوي على أكبر عدد ممكن من الرؤوس. لا يزال الرسم الناتج قيد التشغيل 60 لقطة في الثانية في WebGL.
ونظرًا لأن كل رؤوس من الرؤوس الأصلية للرسم يحتوي أيضًا على وقت، فيمكننا تشغيل البيانات بسهولة. ستكون إعادة حساب الضغطات لكل إطار بطيئة، لذلك بدلاً من ذلك قمنا بحساب الرسم بأكمله مسبقًا عند التحميل وكشفنا ببساطة لكل رباعي عندما حان الوقت للقيام بذلك.
كان إخفاء رباعي يعني ببساطة طي رؤوسه إلى نقطة 0,0,0. عندما الوقت قد وصل إلى المرحلة التي من المفترض أن يتم فيها الكشف عن الوحدة، لإعادة ضبط موضع الرؤوس إلى مكانها الصحيح.
هناك مجال للتحسين، وهو معالجة الرؤوس بالكامل على وحدة معالجة الرسومات أدوات تظليل. تضعها عملية التنفيذ الحالية من خلال تكرار رأس الصفحة مصفوفة من الطابع الزمني الحالي، مع التحقق من الرؤوس التي يجب إظهارها ثم تحديث الشكل الهندسي. يضع ذلك الكثير من الحِمل على وحدة المعالجة المركزية (CPU) مما يتسبب في المروحة في الدوران فضلاً عن إهدار عمر البطارية.
تسجيل أعمال الفنّانين
رأينا أن الرسومات ذاتها لن تكون كافية. أردنا أن نعرض الفنانين داخل رسوماتهم، وهم يرسمون كل ضربة فرشاة.
استخدمنا كاميرات Microsoft Kinect لتسجيل أجمل أعمال الفنانين بيانات الفنانَين جسم في الفضاء. هذا يعطينا القدرة على إظهار أشكال ثلاثية الأبعاد في المساحة نفسها التي تظهر بها الرسومات.
نظرًا لأن جسد الفنان سيغرق نفسه مما يمنعنا من رؤية ما وخلفه، استخدمنا نظام Kinect مزدوجًا، كلاهما في الجانبين المعاكسين للغرفة يشيران إلى المنتصف.
بالإضافة إلى معلومات العمق، قمنا أيضًا بتسجيل معلومات لون المشهد باستخدام كاميرات رقمية ذات عدسة أحادية عاكسة (DSLR) استخدمنا برامج DepthKit للمعايرة والدمج اللقطات من كاميرا العمق وكاميرات الألوان. يتميز تطبيق Kinect تسجيل الألوان، لكننا اخترنا استخدام كاميرات DSLR لأننا يمكننا التحكم في مع إعدادات التعرّض للضوء واستخدام عدسات راقية جميلة، والتسجيل بدقة عالية.
بهدف تسجيل اللقطات، صمّمنا غرفة خاصة لاستضافة هاتف HTC Vive، والكاميرا. كانت جميع الأسطح مغطاة بمواد ماصة بالأشعة تحت الحمراء. ضوء خفيف لمنحنا سحابة ذات نقطة أنظف (اللحاف على الجدران، المطاط المضلّع الاقتصاص على الأرض). في حال ظهور المواد في السحابة النقطية اخترنا موادًا سوداء بدون تشتيت انتباهك كان أبيض.
وفّرت لنا تسجيلات الفيديو الناتجة معلومات كافية لإسقاط جسيم. . لقد كتبنا بعض الأدوات الإضافية في openFrameworks لتنظيم اللقطات بشكل أكبر، وبالأخص إزالة الأرضيات والجدران والأسقف.
بالإضافة إلى عرض الفنانين، أردنا عرض صور HMD وحدات التحكم الثلاثية الأبعاد أيضًا. لم يكن هذا مهمًا فقط لعرض HMD في الناتج النهائي بوضوح (كانت العدسات العاكسة لجهاز HTC Vive تطلق بالأشعة تحت الحمراء من Kinect)، فقد ساعدتنا هذه البيانات على التواصل معنا لتصحيح الأخطاء إخراج مقاطع الفيديو وترتيبها مع الرسم.
وقد تم ذلك من خلال كتابة مكوّن إضافي مخصص في "الريشة السحرية" والتي استخرجت مواضع HMD ووحدات التحكم في كل إطار. ونظرًا لأن ميزة "الريشة السحرية" تعمل بسرعة 90 لقطة في الثانية، أطنان من البيانات المتدفقة، ووصل حجم بيانات مدخل الرسم التخطيطي إلى ما يزيد عن 20 ميغابايت غير مضغوط. استخدمنا أيضًا هذا الأسلوب لتسجيل الأحداث التي لم يتم تسجيلها في ملف حفظ "الفرشاة الإمالة" النموذجي، مثلاً عندما يحدد الفنان خيارًا في لوحة الأدوات وموضع الأداة على المرآة.
عند معالجة 4 تيرابايت من البيانات التي جمعناها، كان من أكبر التحديات التي ومواءمة جميع مصادر العناصر المرئية/البيانات المختلفة. كل فيديو من كاميرا رقمية ذات عدسة أحادية عاكسة (DSLR) يجب أن تتماشى مع أداة Kinect المقابلة، بحيث تتم محاذاة وحدات البكسل والفضاء والوقت أيضًا. بعد ذلك، يجب أن تكون اللقطات من هذين الحاسبين مع بعضها البعض لتشكيل فنان واحد. ثم احتجنا إلى محاذاة عناصر فنانًا باستخدام البيانات التي تم الحصول عليها من رسمه. أخيرًا! فكتبنا استنادًا إلى المتصفح أدوات للمساعدة في معظم هذه المهام، ويمكنك تجربتها بنفسك هنا
وبعد محاذاة البيانات، استخدمنا بعض النصوص البرمجية المكتوبة في NodeJS لمعالجتها ثم إخراج ملف فيديو وسلسلة من ملفات JSON، والتي تم قطع كل الملفات متزامنة. لتقليل حجم الملف، قمنا بثلاثة أشياء. أولاً، قمنا بتقليل دقة كل رقم نقطة عائمة بحيث تكون 3 كحد أقصى لدقة العلامة العشرية. ثانيًا، نخفض عدد النقاط بمقدار الثُلث 30 لقطة في الثانية، وتم إقحام المواضع من جانب العميل. أخيرًا، قمنا بتسلسل لذا بدلاً من استخدام JSON العادي مع أزواج المفتاح/القيمة، فإن ترتيب القيم التي تم إنشاؤها لموضع وتدوير HMD ووحدات التحكم. سيؤدي هذا الإجراء إلى قصّ الملف. الحجم إلى حجم أقل من 3 ميغابايت، وهو ما كان مقبولاً لإرساله عبر السلك.
نظرًا لأن الفيديو نفسه يتم عرضه كعنصر فيديو HTML5 تتم قراءته بواسطة زخرفة WebGL لتصبح جسيمات، يجب أن يتم تشغيل الفيديو نفسه مخفيًا في الخلفية. تحوّل أداة التظليل الألوان في صور العمق إلى مواضع في مساحة ثلاثية الأبعاد: لقد شارَك "جيمس جورج" مثالاً رائعًا عن كيفية تصوير لقطات من DepthKit مباشرةً.
يفرض نظام iOS قيودًا على تشغيل الفيديوهات المضمّنة، ونفترض أنّه يمنع ذلك انزعاج المستخدمين من إعلانات الفيديو على الويب التي يتم تشغيلها تلقائيًا. استخدمنا تقنية مماثلة للحلول الأخرى على web، وهو نسخ إطار الفيديو إلى لوحة وتعديل وقت عرض الفيديو يدويًا، كل 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 لقطة في الثانية على الأقل على هاتف iPhone 6.
الخاتمة
يعد الإجماع العام لتطوير برامج الواقع الافتراضي اعتبارًا من عام 2016 هو إبقاء الهندسة وتظليل العناصر البسيطة بحيث يمكنك تشغيلها بمعدل 90+ لقطة في الثانية في HMD. هذا النمط اتضح أنهما هدفًا رائعًا للعروض التوضيحية لـ WebGL نظرًا لأن الأساليب المستخدمة في "الريشة السحرية" بشكل لطيف جدًا مع WebGL.
على الرغم من أن متصفحات الويب التي تعرض شبكات ثلاثية الأبعاد معقدة ليست مثيرة للاهتمام فيما يتعلق نفسه، كان هذا دليلاً على المفهوم الذي يتقاطع بين عمل الواقع الافتراضي الويب ممكن تمامًا.