Virtuelle Objekte in realen Ansichten positionieren

Mit der Hit Test API können Sie virtuelle Elemente in einer realen Ansicht positionieren.

Joe Medley
Joe Medley

Die WebXR Device API wurde letzten Herbst in Chrome 79 ausgeliefert. Wie bereits erwähnt, ist die Implementierung der API in Chrome noch in der Entwicklung. Chrome freut sich, mitteilen zu können, dass einige Arbeiten abgeschlossen sind. In Chrome 81 sind zwei neue Funktionen verfügbar:

In diesem Artikel wird die WebXR Hit Test API beschrieben, mit der Sie virtuelle Objekte in einer realen Kameraansicht platzieren können.

Bei diesem Artikel nehme ich an, dass Sie bereits wissen, wie Sie eine Augmented Reality-Sitzung erstellen, und dass Sie wissen, wie Sie eine Frameschleife ausführen. Wenn Sie mit diesen Konzepten nicht vertraut sind, sollten Sie die vorherigen Artikel in dieser Reihe lesen.

Das Beispiel für eine immersive AR-Sitzung

Der Code in diesem Artikel basiert auf dem Treffertest-Beispiel der Immersive Web Working Group (Demo, Quelle). In diesem Beispiel können Sie virtuelle Sonnenblumen auf Oberflächen in der realen Welt platzieren.

Wenn du die App zum ersten Mal öffnest, siehst du einen blauen Kreis mit einem Punkt in der Mitte. Der Punkt ist der Schnittpunkt einer imaginären Linie von Ihrem Gerät zum Punkt in der Umgebung. Es bewegt sich, wenn du das Gerät bewegst. Bei der Erkennung von Schnittpunkten scheint es an Oberflächen wie Böden, Tischplatten und Wänden anzudocken. Grund hierfür ist, dass bei Treffertests die Position und Ausrichtung des Schnittpunkts angegeben werden, nicht aber die Oberflächen selbst.

Dieser Kreis wird als Retikel bezeichnet. Dabei handelt es sich um ein temporäres Bild, das dabei hilft, ein Objekt in Augmented Reality zu platzieren. Wenn Sie auf das Display tippen, wird an der Position des Fadenkreuzes und in der Ausrichtung des Fadenkreuzpunkts eine Sonnenblume auf der Oberfläche platziert, unabhängig davon, wo Sie auf das Display getippt haben. Das Fadenkreuz bewegt sich mit dem Gerät weiter.

Ein Fadenkreuz, das je nach Kontext an einer Wand, Lax oder Strikt platziert wird
Das Fadenkreuz ist ein temporäres Bild, mit dem ein Objekt in Augmented Reality platziert werden kann.

Fadenkreuz erstellen

Sie müssen das Fadenkreuzbild selbst erstellen, da es weder vom Browser noch von der API bereitgestellt wird. Die Methode, sie zu laden und zu zeichnen, ist Framework-spezifisch. Wenn Sie es nicht direkt mit WebGL oder WebGL2 zeichnen, lesen Sie die zugehörige Framework-Dokumentation. Aus diesem Grund werde ich im Beispiel nicht ins Detail gehen, wie das Fadenkreuz gezeichnet wird. Unten zeige ich nur eine Zeile davon aus einem einzigen Grund: Damit Sie in späteren Codebeispielen wissen, worauf ich mich beziehe, wenn ich die Variable reticle verwende.

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

Sitzung anfordern

Wenn Sie eine Sitzung anfordern, müssen Sie 'hit-test' im requiredFeatures-Array anfordern, wie unten gezeigt.

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

Sitzung starten

In früheren Artikeln habe ich Ihnen den Code für die Teilnahme an einer XR-Sitzung vorgestellt. Ich habe unten eine Version davon mit einigen Ergänzungen gezeigt. Zuerst habe ich den Event-Listener select hinzugefügt. Wenn der Nutzer auf den Bildschirm tippt, wird basierend auf der Pose des Fadenkreuzes in der Kameraansicht eine Blume platziert. Ich werde diesen Event-Listener später beschreiben.

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);
  });
}

Mehrere Referenzräume

Mit dem hervorgehobenen Code wird XRSession.requestReferenceSpace() zweimal aufgerufen. Ich fand das anfangs verwirrend. Ich habe gefragt, warum der Treffertestcode keinen Animationsframe anfordert (die Frameschleife startet) und warum die Frameschleife anscheinend keine Treffertests beinhaltet. Ursache für die Verwirrung war ein Missverständnis über Referenzräume. Referenzräume drücken die Beziehungen zwischen einem Ursprung und der Welt aus.

Wenn Sie die Funktionsweise dieses Codes besser verstehen möchten, nehmen Sie an, dass Sie sich dieses Beispiel in einem eigenständigen Rig ansehen und sowohl ein Headset als auch einen Controller haben. Um Entfernungen vom Controller zu messen, würden Sie einen Controller-zentrierten Referenzframe verwenden. Um etwas auf dem Bildschirm zu zeichnen, verwenden Sie nutzungsorientierte Koordinaten.

In diesem Beispiel sind der Viewer und der Controller dasselbe Gerät. Aber ich habe ein Problem. Was ich zeichne, muss in Bezug auf die Umgebung stabil sein, aber der Controller, den ich zeichne, bewegt sich.

Zum Zeichnen von Bildern verwende ich den Referenzraum local, der mir Stabilität in Bezug auf die Umgebung verleiht. Danach starte ich die Frameschleife durch Aufrufen von requestAnimationFrame().

Für Treffertests verwende ich den Referenzraum viewer, der auf der Pose des Geräts zum Zeitpunkt des Treffertests basiert. Das Label „Betrachter“ ist in diesem Kontext etwas verwirrend, da es sich um einen Controller handelt. Dies ist sinnvoll, wenn Sie sich den Controller als elektronisches Betrachter vorstellen. Danach rufe ich xrSession.requestHitTestSource() auf. Damit wird die Quelle der Treffertestdaten erstellt, die ich beim Zeichnen verwende.

Frameschleife ausführen

Der Callback requestAnimationFrame() erhält ebenfalls neuen Code für Treffertests.

Wenn du dein Gerät bewegst, muss sich das Fadenkreuz mit ihm bewegen, um Oberflächen zu finden. Zeichne das Fadenkreuz in jedem Frame, um den Eindruck von Bewegung zu erzeugen. Lassen Sie das Fadenkreuz jedoch nicht, wenn der Treffertest fehlschlägt. Für das zuvor erstellte Fadenkreuz habe ich die Eigenschaft visible auf false gesetzt.

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
}

Um etwas in AR zu zeichnen, muss ich wissen, wo sich der Betrachter befindet und wo er hinsieht. Deshalb teste ich, ob hitTestSource und xrViewerPose noch gültig sind.

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
}

Jetzt rufe ich getHitTestResults() an. Sie verwendet hitTestSource als Argument und gibt ein Array mit HitTestResult-Instanzen zurück. Der Treffertest kann mehrere Oberflächen finden. Die erste im Array ist diejenige, die der Kamera am nächsten ist. In den meisten Fällen werden Sie sie verwenden, aber für komplexere Anwendungsfälle wird ein Array zurückgegeben. Stellen Sie sich beispielsweise vor, Ihre Kamera ist auf einen Kasten auf einem Tisch auf einem Boden gerichtet. Es ist möglich, dass bei der Trefferprüfung alle drei Oberflächen im Array zurückgegeben werden. In den meisten Fällen ist dies die Box, die mich interessiert. Wenn die Länge des zurückgegebenen Arrays 0 ist, d. h. wenn kein Treffertest zurückgegeben wird, fahren Sie fort. Versuchen Sie es im nächsten Frame noch einmal.

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
}

Schließlich muss ich die Ergebnisse des Treffertests verarbeiten. Der grundlegende Prozess sieht so aus. Rufen Sie eine Pose aus dem Treffertestergebnis auf, transformieren (verschieben) Sie das Fadenkreuzbild an die Treffertestposition und setzen Sie dann die Eigenschaft visible auf „true“. Die Pose stellt die Position eines Punkts auf einer Oberfläche dar.

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
}

Objekt platzieren

Wenn der Nutzer auf den Bildschirm tippt, wird ein Objekt im AR-Modus platziert. Ich habe der Sitzung bereits einen select-Event-Handler hinzugefügt. (siehe oben)

Wichtig bei diesem Schritt ist zu wissen, wo sie platziert werden soll. Da Sie über das bewegliche Retikel eine ständige Quelle von Treffertests haben, ist es am einfachsten, ein Objekt an der Position des Retikels beim letzten Treffertest zu zeichnen.

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);
  }
}

Fazit

Am besten gehen Sie den Beispielcode durch oder probieren das Codelab aus. Ich hoffe, ich konnte Ihnen genug Hintergrundwissen vermitteln, um beides zu verstehen.

Die Entwicklung immersiver Web-APIs ist noch lange nicht abgeschlossen. Hier werden wir regelmäßig neue Artikel veröffentlichen.

Foto von Daniel Frank auf Unsplash