Sanal nesneleri gerçek dünya görünümlerinde konumlandırma

Hit Test API, sanal öğeleri gerçek dünyada konumlandırmanıza olanak tanır.

Ali Porsuk
Ali Polat

WebXR Device API geçen sonbaharda Chrome 79'da kullanıma sunuldu. O zaman da belirtildiği gibi, Chrome'un API'yi uygulaması devam eden bir çalışmadır. Chrome, bu çalışmanın bir kısmının tamamlandığını duyurmaktan mutluluk duyuyor. Chrome 81'de iki yeni özellik kullanıma sunuldu:

Bu makalede, sanal nesneleri gerçek dünyadaki kamera görünümüne yerleştirme yöntemi olan WebXR Hit Test API ele alınmaktadır.

Bu makalede artırılmış gerçeklik oturumu oluşturmayı ve bir kare döngüsü çalıştırmayı bildiğinizi varsayıyorum. Bu kavramlara aşina değilseniz bu dizinin önceki makalelerini okumalısınız.

Etkileyici AR oturumu örneği

Bu makaledeki kod, Immersive Web Working Group'un İsabet Testi örneğindeki (demo, source) bulunan koda dayanır ancak aynı değildir. Bu örnek, sanal ayçiçeklerini gerçek dünyadaki yüzeylere yerleştirmenize olanak tanır.

Uygulamayı ilk açtığınızda, ortasında nokta olan mavi bir daire görürsünüz. Nokta, cihazınızdan ortamdaki noktaya kadar hayali bir çizgi arasındaki kesişim noktasıdır. Siz cihazı hareket ettirdikçe hareket eder. Kesişme noktaları bulduğunda zemin, masa üstü ve duvar gibi yüzeylere yapıştığı anlaşılıyor. Bunu yapar çünkü isabet testi, kesişim noktasının konumunu ve yönünü sağlar, ancak yüzeylerle ilgili hiçbir şey yoktur.

Bu daireye retikül adı verilir. Bu daire, bir nesnenin artırılmış gerçeklikteki yerini belirlemeye yardımcı olan geçici bir görüntüdür. Ekrana dokunduğunuzda, ekrana nereden dokunduğunuzdan bağımsız olarak, retikül konumuna ve retikül noktasının yönüne yüzeye bir ayçiçeği yerleştirilir. Direksiyon, cihazınızla birlikte hareket etmeye devam eder.

Bağlamına göre duvarda oluşturulan retikül, Lax veya Strict (Katı)
Retikül, bir nesnenin artırılmış gerçeklik içine yerleştirilmesine yardımcı olan geçici bir resimdir.

Retikülü oluştur

Tarayıcı veya API tarafından sağlanmadığı için retikül görüntüsünü kendiniz oluşturmanız gerekir. Yükleme ve çizim yöntemi çerçeveye özeldir. Doğrudan WebGL veya WebGL2 kullanarak çizmiyorsanız çerçeve dokümanlarınıza bakın. Bu nedenle, örnekte retikülün nasıl nasıl çizildiğine değinmeyeceğim. Aşağıda bunun bir satırını yalnızca bir nedenden dolayı gösteriyorum. Böylece sonraki kod örneklerinde, reticle değişkenini kullandığımda neyi kastettiğimi anlayabilirsiniz.

let reticle = new Gltf2Node({url: 'media/gltf/reticle/reticle.gltf'});

Oturum iste

Oturum isteğinde bulunurken, aşağıda gösterildiği gibi requiredFeatures dizisinde 'hit-test' isteğinde bulunmanız gerekir.

navigator.xr.requestSession('immersive-ar', {
  requiredFeatures: ['local', 'hit-test']
})
.then((session) => {
  // Do something with the session
});

Oturuma girme

Önceki makalelerde, XR oturumuna girmek için kullanılacak kod sunmuştuk. Bunun bir versiyonunu bazı eklemeler yaparak aşağıda görebilirsiniz. İlk olarak select etkinlik işleyicisini ekledim. Kullanıcı ekrana dokunduğunda, retikinin pozuna göre kamera görünümüne bir çiçek yerleştirilir. Etkinlik işleyiciyi daha sonra açıklayacağım.

function onSessionStarted(xrSession) {
  xrSession.addEventListener('end', onSessionEnded);
  xrSession.addEventListener('select', onSelect);

  let canvas = document.createElement('canvas');
  gl = canvas.getContext('webgl', { xrCompatible: true });

  xrSession.updateRenderState({
    baseLayer: new XRWebGLLayer(session, gl)
  });

  xrSession.requestReferenceSpace('viewer').then((refSpace) => {
    xrViewerSpace = refSpace;
    xrSession.requestHitTestSource({ space: xrViewerSpace })
    .then((hitTestSource) => {
      xrHitTestSource = hitTestSource;
    });
  });

  xrSession.requestReferenceSpace('local').then((refSpace) => {
    xrRefSpace = refSpace;
    xrSession.requestAnimationFrame(onXRFrame);
  });
}

Birden fazla referans alanı

Vurgulanan kodun XRSession.requestReferenceSpace() öğesini iki kez çağırdığına dikkat edin. İlk başta bu kafa karıştırıcı buldum. İsabet test kodunun neden animasyon karesi istemediğini (kare döngüsünü başlatır) ve kare döngüsünün neden isabet testleri içermediğini sordum. Karışıklığın kaynağı referans alanlarının yanlış anlaşılmasıydı. Referans alanları, köken ile dünya arasındaki ilişkileri ifade eder.

Bu kodun ne işe yaradığını anlamak için bu örneği bağımsız bir düzenek kullanarak görüntülüyor ve hem mikrofonlu kulaklık hem de kumandanız varmış gibi düşünün. Kumandayla arasındaki mesafeleri ölçmek için kumanda merkezli bir referans çerçevesi kullanırsınız. Ancak ekrana bir şey çizmek için kullanıcı merkezli koordinatlar kullanırsınız.

Bu örnekte izleyici ile kumanda aynı cihazdır. Ama bir sorunum var. Çizdiğim şey ortama göre sabit olmalı ama çiziminde kullandığım 'denetleyici' hareket ediyor.

Resim çizimi için ortam açısından kararlılık sağlayan local referans alanını kullanıyorum. Bunu aldıktan sonra, requestAnimationFrame() çağrısı yaparak kare döngüsünü başlatıyorum.

İsabet testi için cihazın isabet testi sırasındaki pozisyonuna dayanan viewer referans alanını kullanıyorum. "Görüntüleyici" etiketi bu bağlamda biraz kafa karıştırıcıdır, çünkü denetleyiciden bahsediyorum. Kumandayı elektronik bir izleyici olarak düşünün. Bunu aldıktan sonra, çizim sırasında kullanacağım isabet testi verilerinin kaynağını oluşturan xrSession.requestHitTestSource() adını veriyorum.

Kare döngüsü çalıştırma

requestAnimationFrame() geri çağırma işlevi, isabet testini gerçekleştirmek için yeni kod da alır.

Cihazınızı hareket ettirdikçe retikülün yüzeyleri bulmaya çalışırken onunla birlikte hareket etmesi gerekir. Hareket illüzyonu yaratmak için retikülü her karede yeniden çizin. Ancak, isabet testi başarısız olursa retiküleyi göstermeyin. Bu nedenle, daha önce oluşturduğum retikül için visible özelliğini false olarak ayarladım.

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);

  reticle.visible = false;

  // Reminder: the hitTestSource was acquired during onSessionStart()
  if (xrHitTestSource && xrViewerPose) {
    let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
    if (hitTestResults.length > 0) {
      let pose = hitTestResults[0].getPose(xrRefSpace);
      reticle.visible = true;
      reticle.matrix = pose.transform.matrix;
    }
  }

  // Draw to the screen
}

Artırılmış gerçeklikte bir şey çizmek için görüntüleyenin nerede olduğunu ve nereye baktığını bilmem gerekir. Bu nedenle, hitTestSource ve xrViewerPose öğelerinin hâlâ geçerli olup olmadığını test ediyorum.

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);

  reticle.visible = false;

  // Reminder: the hitTestSource was acquired during onSessionStart()
  if (xrHitTestSource && xrViewerPose) {
    let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
    if (hitTestResults.length > 0) {
      let pose = hitTestResults[0].getPose(xrRefSpace);
      reticle.visible = true;
      reticle.matrix = pose.transform.matrix;
    }
  }

  // Draw to the screen
}

Şimdi getHitTestResults() numaralı telefonu arıyorum. hitTestSource öğesini bağımsız değişken olarak alır ve HitTestResult örneklerden oluşan bir dizi döndürür. İsabet testi birden fazla yüzeyi bulabilir. Dizideki ilk, kameraya en yakın olandır. Çoğu zaman bunu kullanacaksınız ancak gelişmiş kullanım alanları için bir dizi döndürülür. Örneğin, kameranızın zemindeki masada duran bir kutuya doğru durduğunu düşünün. İsabet testinin dizideki üç yüzeyi de döndürmesi mümkündür. Çoğu durumda, önemsediğim kutu bu olacak. Döndürülen dizinin uzunluğu 0 ise yani isabet testi döndürülmezse çalışmaya devam edin. Sonraki karede tekrar deneyin.

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);

  reticle.visible = false;

  // Reminder: the hitTestSource was acquired during onSessionStart()
  if (xrHitTestSource && xrViewerPose) {
    let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
    if (hitTestResults.length > 0) {
      let pose = hitTestResults[0].getPose(xrRefSpace);
      reticle.visible = true;
      reticle.matrix = pose.transform.matrix;
    }
  }

  // Draw to the screen
}

Son olarak, isabet testi sonuçlarını işlemem gerekiyor. Temel süreç budur. İsabet testi sonucundan bir poz alın, retikül resmini isabet testi konumuna dönüştürün (taşıyın), ardından visible özelliğini true olarak ayarlayın. Poz, yüzey üzerindeki bir noktanın pozisyonunu temsil eder.

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);

  reticle.visible = false;

  // Reminder: the hitTestSource was acquired during onSessionStart()
  if (xrHitTestSource && xrViewerPose) {
    let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
    if (hitTestResults.length > 0) {
      let pose = hitTestResults[0].getPose(xrRefSpace);
      reticle.matrix = pose.transform.matrix;
      reticle.visible = true;

    }
  }

  // Draw to the screen
}

Nesne yerleştirme

Kullanıcı ekrana dokunduğunda artırılmış gerçeklik (AR) moduna yerleştiriliyor. Oturuma zaten bir select etkinlik işleyici ekledim. (Yukarı bakın.)

Bu adımda önemli olan, etiketin nereye yerleştirileceğini bilmektir. Hareketli retikül size sabit bir isabet testi kaynağı sağladığından, bir nesneyi yerleştirmenin en kolay yolu, son vuruş testinde retikülün bulunduğu yere çizmektir.

function onSelect(event) {
  if (reticle.visible) {
    // The reticle should already be positioned at the latest hit point,
    // so we can just use its matrix to save an unnecessary call to
    // event.frame.getHitTestResults.
    addARObjectAt(reticle.matrix);
  }
}

Sonuç

Bunu belirlemenin en iyi yolu örnek kodu görmek veya codelab'i denemektir. Umarım size her ikisini de anlamak için yeterli deneyim sunabilmişimdir.

Tam kapsamlı web API'leri oluşturma işimiz yakın zamanda değil. İlerledikçe burada yeni makaleler yayınlayacağız.

Fotoğraf: Daniel Frank'in Unsplash'teki fotoğrafı