Özet
Altı sanatçı, VR'de resim, tasarım ve heykel yapmaya davet edildi. Kullanıcıların oturumlarını kaydetme, verileri dönüştürme ve web tarayıcılarında gerçek zamanlı olarak sunma sürecimiz budur.
https://g.co/VirtualArtSessions
Yaşamak için ne güzel bir zaman! Tüketici ürünü olarak sanal gerçekliğin kullanıma sunulmasıyla birlikte yeni ve keşfedilmemiş olanaklar ortaya çıkıyor. HTC Vive'de kullanılabilen bir Google ürünü olan Tilt Brush, üç boyutlu alanda çizim yapmanıza olanak tanır. Tilt Brush'ı ilk kez denediğimizde, hareket izlemeli kontrol cihazlarıyla çizim yapmanın verdiği hisle"süper güçlere sahip bir odada" olmanın verdiği his birleşti. Etrafınızdaki boşlukta çizim yapabilmek gibi bir deneyim gerçekten yok.
Google'daki Veri Sanatları Ekibi, Tilt Brush'ın henüz kullanılamadığı web'de bu deneyimi sanal gerçeklik başlığı olmayanlara gösterme görevini üstlendi. Bu amaçla ekip, heykeltıraş, illüstratör, konsept tasarımcısı, moda sanatçısı, enstalasyon sanatçısı ve sokak sanatçılarından oluşan bir ekip oluşturdu. Bu ekip, yeni ortamda kendi tarzlarında sanat eserleri oluşturdu.
Sanal gerçeklikte çizimleri kaydetme
Unity'de geliştirilen Tilt Brush yazılımı, kafanızın konumunu (kafaya takılan ekran veya HMD) ve her iki elinizdeki denetleyicileri izlemek için oda ölçekli sanal gerçeklik kullanan bir masaüstü uygulamasıdır. Tilt Brush'ta oluşturulan çalışmalar varsayılan olarak .tilt
dosyası olarak dışa aktarılır. Bu deneyimi web'e sunmak için yalnızca poster verilerinden daha fazlasına ihtiyacımız olduğunu fark ettik. Tilt Brush ekibiyle yakın bir şekilde çalışarak Tilt Brush'u, sanatçının kafa ve el konumlarının yanı sıra geri alma/silme işlemlerini de saniyede 90 kez dışa aktaracak şekilde değiştirdik.
Tilt Brush, çizim yaparken kumandanızın konumunu ve açısını alır ve zaman içinde birden fazla noktayı "çizgi"ye dönüştürür. Bir örneği burada görebilirsiniz. Bu vuruşları ayıklayıp ham JSON olarak yayınlayan eklentiler yazdık.
{
"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
]
}
Yukarıdaki snippet'te, taslak JSON biçiminin yapısı özetlenmiştir.
Burada her bir vuruş, "STROKE" türüne sahip bir işlem olarak kaydedilir. Bir sanatçının hataları olduğunu ve eskiz sırasında fikrini değiştirdiğini göstermek için, bir çizginin tamamını silmek veya geri almak için kullanılan "SİL" işlemlerini kaydetmemiz gerekiyordu.
Her bir vuruşla ilgili temel bilgiler kaydedilir. Bu nedenle fırça türü, fırça boyutu ve RGB rengi toplanır.
Son olarak, vuruşun her köşesi kaydedilir. Bu kayıtta konum, açı, zaman ve denetleyicinin tetikleyici basınç gücü (her noktada p
olarak belirtilir) yer alır.
Döndürme işleminin 4 bileşenli bir dört boyutlu vektör olduğunu unutmayın. Bu, daha sonra jiroskop kilidini önlemek için vuruşları oluşturduğumuzda önemlidir.
WebGL ile eskiz oynatma
Eskizleri bir web tarayıcısında göstermek için THREE.js'i kullandık ve Tilt Brush'ın arka planda yaptığı işlemleri taklit eden bir geometri oluşturma kodu yazdık.
Tilt Brush, kullanıcının el hareketine göre gerçek zamanlı olarak üçgen şeritler oluştururken, taslağın tamamı web'de gösterilmeden önce "bitirilmiş" olur. Bu sayede gerçek zamanlı hesaplamanın büyük bir kısmını atlayabilir ve geometriyi yükleme sırasında derleyebiliriz.
Bir vuruştaki her köşe çifti bir yön vektörü oluşturur (yukarıda gösterildiği gibi her noktayı birbirine bağlayan mavi çizgiler, aşağıdaki kod snippet'inde moveVector
).
Her nokta, kontrol cihazının mevcut açısını temsil eden bir dört boyutlu vektör de içerir. Üçgen şerit oluşturmak için bu noktaların her birini iteratif olarak tarar ve yöne ve kontrol cihazı yönelimine dik normaller oluştururuz.
Her vuruş için üçgen şeridini hesaplama işlemi, Tilt Brush'ta kullanılan koda neredeyse aynıdır:
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);
}
Vuruş yönünü ve yönünü tek başına birleştirmek matematiksel olarak belirsiz sonuçlar verir. Birden fazla normal türetilebilir ve bu genellikle geometride bir "büküm" oluşturur.
Bir çizginin noktaları üzerinde iterasyon yaparken "tercih edilen sağ" bir vektör tutar ve bunu computeSurfaceFrame()
işlevine iletiriz. Bu işlev, bize vuruşun yönüne (son noktadan mevcut noktaya) ve kontrol cihazının yönelimine (bir dört boyutlu vektör) göre dörtgen şeritte dörtgen elde edebileceğimiz bir normal verir. Daha da önemlisi, sonraki hesaplama grubu için yeni bir "tercih edilen sağ" vektör de döndürür.
Her bir çizginin kontrol noktalarına göre dörtgenler oluşturduktan sonra, bir dörtgenden diğerine köşelerini interpole ederek dörtgenleri birleştiririz.
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 );
}
Her dörtlü, sonraki adım olarak oluşturulan UV'ler de içerir. Bazı fırçalar, her bir darbenin farklı bir fırça darbesi gibi görünmesi için çeşitli vuruş kalıpları içerir. Bu, her fırça dokusunun tüm olası varyasyonları içerdiği _doku atlaslama_ kullanılarak yapılır. Vuruşun UV değerleri değiştirilerek doğru doku seçilir.
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 );
});
}
Her eskizin sınırsız sayıda konturu olduğundan ve bu konturların çalışma zamanında değiştirilmesi gerekmediğinden, konturlar geometrisini önceden hesaplar ve tek bir örgeye birleştiririz. Her yeni fırça türünün kendi malzemesi olması gerekse de bu, çizim çağrılarımızı fırça başına bire indirir.
Sistemin stres testini yapmak için, alanı mümkün olduğunca çok sayıda köşe noktasıyla doldurduğumuz ve 20 dakika süren bir eskiz oluşturduk. Elde edilen eskiz, WebGL'de 60 fps'de oynatılmaya devam etti.
Bir çizginin orijinal köşe noktalarının her biri zaman da içerdiğinden verileri kolayca oynatabiliriz. Vuruşları kare başına yeniden hesaplamak çok yavaş olacağından, bunun yerine eskizin tamamını yükleme sırasında önceden hesapladık ve zamanı geldiğinde her dörtgeni gösterdik.
Bir dörtgen gizlendiğinde, köşeleri 0,0,0 noktasına toplanır. Zaman, dörtgenlerin gösterilmesi gereken noktaya ulaştığında köşeleri tekrar yerine yerleştiririz.
İyileştirmeye açık bir alan, GPU'da kenarları tamamen gölgelendiricilerle değiştirmek. Mevcut uygulama, mevcut zaman damgasından itibaren köşe dizisini döngü yaparak, hangi köşelerin gösterilmesi gerektiğini kontrol edip ardından geometriyi güncelleyerek bunları yerleştirir. Bu durum, CPU'ya çok fazla yük bindirir. Bu da fanın dönmesine ve pil ömrünün azalmasına neden olur.
Sanatçıları kaydetme
Taslakların tek başına yeterli olmayacağını düşündük. Sanatçıları, her fırça darbesini boyarken eskizlerinin içinde göstermek istedik.
Sanatçıları çekmek için Microsoft Kinect kameralarını kullanarak sanatçıların uzayda bulunan vücutlarının derinlik verilerini kaydettik. Bu sayede, çizimlerin yer aldığı alanda üç boyutlu figürleri gösterebiliriz.
Sanatçının vücudu, arkasındakileri görmemizi engelleyeceği için odanın iki zıt tarafında, ortayı gösteren çift Kinect sistemi kullandık.
Standart DSLR kameralarla derinlik bilgilerine ek olarak sahnenin renk bilgilerini de yakaladık. Derinlik kamerası ve renkli kameralardan gelen görüntüleri kalibre etmek ve birleştirmek için mükemmel DepthKit yazılımını kullandık. Kinect renk kaydedebilir ancak pozlama ayarlarını kontrol edebildiğimiz, yüksek kaliteli lensler kullanabildiğimiz ve yüksek çözünürlükte kayıt yapabildiğimiz için DSLR'leri tercih ettik.
Görüntüleri kaydetmek için HTC Vive, sanatçı ve kamerayı barındıracak özel bir oda oluşturduk. Daha net bir nokta bulutu elde etmek için tüm yüzeyler kızılötesi ışığı emen bir malzemeyle kaplandı (duvarlarda kadife, zeminde nervürlü kauçuk paspas). Materyalin nokta bulutu kamera kaydında görünmesi ihtimaline karşı, dikkat dağıtıcı olmaması için siyah bir materyal seçtik.
Elde edilen video kayıtları, bir parçacık sistemi projelendirecek kadar bilgi sağladı. Görüntüleri daha da temizlemek ve özellikle de zeminleri, duvarları ve tavanı kaldırmak için openFrameworks'da bazı ek araçlar yazdık.
Sanatçıların yanı sıra HMD'yi ve denetleyicileri de 3D olarak oluşturmak istedik. Bu, yalnızca HMD'yi nihai çıkışta net bir şekilde göstermek için önemli değildi (HTC Vive'in yansıtıcı lensleri Kinect'in kızılötesi okumalarını bozuyordu). Ayrıca, parçacık çıkışında hata ayıklama ve videoları taslakla hizalama için bize temas noktaları sağladı.
Bu işlem, Tilt Brush'a her karede HMD ve kontrol cihazlarının konumlarını ayıklayan özel bir eklenti yazılarak gerçekleştirildi. Tilt Brush 90 fps'de çalıştığından çok fazla veri aktarıldı ve bir eskizin giriş verileri sıkıştırılmamış halde 20 MB'tan fazlaydı. Bu tekniği, sanatçının araç panelinde bir seçenek belirlemesi ve ayna widget'ının konumu gibi, tipik Tilt Brush kayıt dosyasında kaydedilmeyen etkinlikleri yakalamak için de kullandık.
Topladığımız 4 TB veriyi işleme sürecinde en büyük zorluklardan biri, tüm farklı görsel/veri kaynaklarını hizalamaktı. DSLR kameradan gelen her videonun, piksellerin hem zamanda hem de uzayda hizalanması için ilgili Kinect ile hizalanması gerekir. Ardından, bu iki kameradan alınan görüntülerin tek bir sanatçıyı oluşturacak şekilde birbirine hizalanması gerekiyordu. Ardından 3D sanatçımızı, çizimlerinden alınan verilerle uyumlu hale getirmemiz gerekiyordu. Bora Bu görevlerin çoğunda size yardımcı olmak için tarayıcı tabanlı araçlar yazdık. Bu araçları burada deneyebilirsiniz.
Veriler hizalandıktan sonra, hepsini işlemek ve bir video dosyası ile bir dizi JSON dosyası (tümü kırpılmış ve senkronize edilmiş) oluşturmak için NodeJS'de yazılmış bazı komut dosyaları kullandık. Dosya boyutunu küçültmek için üç şey yaptık. Öncelikle, her kayan nokta sayısının doğruluğunu en fazla 3 ondalık basamak olacak şekilde azalttık. İkinci olarak, nokta sayısını 30 fps'ye düşürmek için üçte bir oranında azalttık ve konumları istemci tarafında ara değerle doldurduk. Son olarak, verileri serileştirdik. Böylece, anahtar/değer çiftleri içeren düz JSON yerine HMD ve kontrol cihazlarının konumu ve dönüşümü için bir değer sırası oluşturulur. Bu işlem, dosya boyutunu 3 MB'ın biraz altına düşürdü. Bu boyut, kablo üzerinden aktarılması için kabul edilebilir bir boyuttur.
Videonun kendisi, parçacık haline gelmek için bir WebGL dokusu tarafından okunan bir HTML5 video öğesi olarak sunulduğundan videonun arka planda gizli olarak oynatılması gerekiyordu. Gölgelendirici, derinlik görüntülerindeki renkleri 3D uzayda konumlara dönüştürür. James George, DepthKit'ten doğrudan alınan görüntülerle neler yapabileceğinize dair harika bir örnek paylaştı.
iOS'te satır içi video oynatmayla ilgili kısıtlamalar vardır. Bu kısıtlamaların, kullanıcıların otomatik olarak oynatılan web video reklamlarından rahatsız olmasını önlemek için uygulandığını varsayıyoruz. Web'deki diğer geçici çözümlere benzer bir teknik kullandık. Bu teknikte, video çerçevesini bir tuvale kopyalayıp videoyu saniyenin 1/30'unda bir manuel olarak güncelliyoruz.
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 );
Videodan tuvale piksel arabelleğinin kopyalanması çok fazla CPU gerektirdiğinden, yaklaşımımızın üzücü bir yan etkisi olarak iOS kare hızı önemli ölçüde düşürüldü. Bu sorunun üstesinden gelmek için, aynı videoların iPhone 6'ta en az 30 fps'ye izin veren daha küçük boyutlu sürümlerini yayınladık.
Sonuç
2016'dan itibaren VR yazılımı geliştirmeyle ilgili genel fikir birliği, HMD'de 90 fps'den fazla hızda çalışabilmek için geometrileri ve gölgelendiricileri basit tutmaktır. Tilt Brush'ta kullanılan teknikler WebGL ile çok iyi eşleştiğinden bu, WebGL denemeleri için gerçekten mükemmel bir hedef oldu.
Karmaşık 3D ağlar görüntüleyen web tarayıcıları başlı başına heyecan verici olmasa da bu, VR çalışmalarının ve web'in çapraz tozlanmasının tamamen mümkün olduğunun kanıtıydı.