Virtuelle Objekte in realen Ansichten positionieren

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

Joe Medley
Joe Medley

Die WebXR Device API wurde im letzten Herbst in Chrome 79 veröffentlicht. Wie bereits erwähnt, ist die Implementierung der API in Chrome noch in Arbeit. Wir freuen uns, Ihnen mitteilen zu können, dass einige der Arbeiten bereits abgeschlossen sind. In Chrome 81 gibt es zwei neue Funktionen:

In diesem Artikel geht es um die WebXR-API für den Kollisionstest, mit der virtuelle Objekte in eine reale Kameraansicht eingefügt werden können.

In diesem Artikel wird davon ausgegangen, dass Sie bereits wissen, wie Sie eine AR-Sitzung erstellen und einen Frame-Loop ausführen. Wenn Sie mit diesen Konzepten nicht vertraut sind, sollten Sie die vorherigen Artikel dieser Reihe lesen.

Beispiel für immersive AR-Sitzung

Der Code in diesem Artikel basiert auf dem Beispiel für den Hit-Test der Immersive Web Working Group, ist aber nicht identisch mit diesem (Demo, Quellcode). In diesem Beispiel können Sie virtuelle Sonnenblumen auf Oberflächen in der realen Welt platzieren.

Wenn Sie die App zum ersten Mal öffnen, sehen Sie einen blauen Kreis mit einem Punkt in der Mitte. Der Punkt ist die Schnittstelle zwischen einer imaginären Linie von Ihrem Gerät zum Punkt in der Umgebung. Sie bewegt sich, wenn Sie das Gerät bewegen. Wenn er Überschneidungspunkte findet, scheint er an Oberflächen wie Böden, Tischplatten und Wänden anzuspringen. Dies liegt daran, dass Treffertests die Position und Ausrichtung des Schnittpunkts liefern, aber nichts über die Oberflächen selbst.

Dieser Kreis wird als Absehen bezeichnet. Es ist ein temporäres Bild, das beim Platzieren eines Objekts in Augmented Reality hilft. Wenn Sie auf den Bildschirm tippen, wird eine Sonnenblume an der Position des Fadenkreuzes und an der Ausrichtung des Fadenkreuzes auf der Oberfläche platziert, unabhängig davon, wo Sie auf den Bildschirm getippt haben. Das Fadenkreuz bewegt sich weiterhin mit Ihrem Gerät.

Ein Fadenkreuz, das je nach Kontext an einer Wand gerendert wird (Lax oder Strict)
Das Fadenkreuz ist ein temporäres Bild, das beim Platzieren eines Objekts in Augmented Reality hilft.

Fadenkreuz erstellen

Sie müssen das Fadenkreuzbild selbst erstellen, da es nicht vom Browser oder von der API bereitgestellt wird. Die Methode zum Laden und Zeichnen ist frameworkabhängig. Wenn Sie die Grafik nicht direkt mit WebGL oder WebGL2 zeichnen, lesen Sie die Dokumentation Ihres Frameworks. Aus diesem Grund gehe ich nicht näher darauf ein, wie das Fadenkreuz im Beispiel gezeichnet wird. Unten zeige ich nur eine Zeile 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 du eine Sitzung anforderst, musst du 'hit-test' im Array requiredFeatures anfordern, wie unten dargestellt.

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

An einer Sitzung teilnehmen

In früheren Artikeln habe ich Code für die Eingabe einer XR-Sitzung vorgestellt. Unten sehen Sie eine Version davon mit einigen Ergänzungen. Zuerst habe ich den Event-Listener select hinzugefügt. Wenn der Nutzer auf das Display tippt, wird in der Kameraansicht eine Blume platziert, die der Position des Fadenkreuzes entspricht. Diesen Ereignis-Listener werde ich 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 Referenzbereiche

Der hervorgehobene Code ruft XRSession.requestReferenceSpace() zweimal auf. Anfangs fand ich das verwirrend. Ich fragte, warum der Code für den Kollisionstest keinen Animationsframe anfordert (um die Frame-Schleife zu starten) und warum die Frame-Schleife anscheinend keine Kollisionstests enthält. Ursache der Verwirrung war ein Missverständnis von Referenzräumen. Referenzräume drücken die Beziehungen zwischen einem Ursprung und der Welt aus.

Um zu verstehen, was dieser Code bewirkt, stellen Sie sich vor, Sie sehen sich dieses Beispiel auf einem eigenständigen Rig an und haben sowohl ein Headset als auch einen Controller. Um Entfernungen vom Controller zu messen, verwenden Sie einen auf dem Controller zentrierten Referenzrahmen. Um etwas auf dem Bildschirm zu zeichnen, verwenden Sie nutzungsorientierte Koordinaten.

In diesem Beispiel sind der Betrachter 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“, mit dem ich zeichne, bewegt sich.

Zum Zeichnen verwende ich den Referenzraum local, der mir Stabilität in Bezug auf die Umgebung gibt. Danach starte ich die Frame-Schleife, indem ich requestAnimationFrame() aufrufe.

Für Treffertests verwende ich den viewer-Referenzraum, der auf der Position des Geräts zum Zeitpunkt des Treffertests basiert. Das Label „Viewer“ ist hier verwirrend, weil ich von einem Controller spreche. Das macht Sinn, wenn Sie den Controller als elektronischen Betrachter betrachten. Danach rufe ich xrSession.requestHitTestSource() auf, wodurch die Quelle der Kollisionstestdaten erstellt wird, die ich beim Zeichnen verwende.

Frame-Schleife ausführen

Der requestAnimationFrame()-Callback erhält auch neuen Code zum Ausführen von Treffertests.

Wenn Sie das Gerät bewegen, muss sich auch das Fadenkreuz bewegen, damit Oberflächen gesucht werden können. Zeichnen Sie das Fadenkreuz in jedem Frame neu, um die Illusion von Bewegung zu erzeugen. Zeigen Sie das Fadenkreuz jedoch nicht an, wenn der Treffertest fehlschlägt. Für das zuvor erstellte Fadenkreuz habe ich die Eigenschaft visible auf false festgelegt.

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
}

Wenn ich etwas in AR zeichnen möchte, muss ich wissen, wo sich der Betrachter befindet und wohin er schaut. Ich prüfe also, 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. Es nimmt hitTestSource als Argument entgegen und gibt ein Array von HitTestResult-Instanzen zurück. Der Kollisionstest kann mehrere Oberflächen finden. Der erste im Array ist der, der der Kamera am nächsten ist. In den meisten Fällen wird es verwendet, aber für erweiterte Anwendungsfälle wird ein Array zurückgegeben. Angenommen, Ihre Kamera ist auf einen Karton auf einem Tisch auf dem Boden gerichtet. Es ist möglich, dass der Kollisionstest alle drei Oberflächen im Array zurückgibt. In den meisten Fällen ist es das Feld, das mich interessiert. Wenn die Länge des zurückgegebenen Arrays 0 ist, d. h., wenn kein Treffertest zurückgegeben wird, fahren Sie fort. Versuche 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
}

Zum Schluss muss ich die Ergebnisse des Treffertests verarbeiten. So funktioniert es im Grunde: Rufen Sie eine Pose aus dem Ergebnis des Treffertests ab, transformieren (bewegen) Sie das Fadenkreuzbild an die Position des Treffertests und setzen Sie dann die visible-Property auf „wahr“. Die Pose stellt die Pose 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

Ein Objekt wird in AR platziert, wenn der Nutzer auf den Bildschirm tippt. Ich habe der Sitzung bereits einen select-Ereignis-Handler hinzugefügt. (Siehe oben.)

Wichtig ist dabei, zu wissen, wo Sie sie platzieren. Da das sich bewegende Fadenkreuz Ihnen eine konstante Quelle für Kollisionstests bietet, ist die einfachste Methode zum Platzieren eines Objekts, es an der Position des Fadenkreuzes beim letzten Kollisionstest 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

Um das herauszufinden, gehen Sie am besten den Beispielcode durch oder probieren Sie das Codelab aus. Ich hoffe, ich konnte Ihnen genügend Hintergrundinformationen geben, um beides zu verstehen.

Wir sind noch lange nicht fertig mit der Entwicklung von immersiven Web-APIs. Wir veröffentlichen hier nach und nach neue Artikel.

Foto von Daniel Frank bei Unsplash