WebXRでかんたんAR体験
WebXRとは
WebXRは、仮想空間を3D空間として表示するVR(Virtual Reality)や、現実世界に3Dオブジェクトを表示するAR(Augumented Reality)をまとめた総称です。(ざっくり)
細かいことはさておき、今回はAndroidデバイスを利用してWebXR(AR)をあっさり体験してみましょう。
WebXR Device APIを利用する
"WebXR Device API"は、WebXRの機能を実装しデバイスの管理と3Dシーンの描画を管理します。
WebXRへの対応状況は、次のサイトで確認する事ができます。
(いよいよ広がってきました!!)
プロジェクトを作る
では早速作っていきましょう。
サンプルプロジェクトを、次のサイトから一式ダウンロードします。
ダウンロードした圧縮ファイルにある"webxr-samples/js/"フォルダをまるごとコピーし、
次の様にプロジェクトを作ります。
MyProject01/
├ assets
| └ gltf/ (GLTF形式のモデリングデータを保存するフォルダです)
├ libs
| └ threejs/ ("webxr-samples/js/"の中身をコピーします)
| ま├ render/
| る├ third-party/
| ご├ third-party/
| と├ util/
| コ├ wglu/
| ピ├ hittest.js
| |├ stereo-util.js
| !├ webxr-sample-app.js
├ custom.css (スタイルシートを記述するファイルです)
├ index.html (プログラムを起動するファイルです)
└ main.js (メインのプログラムを記述するファイルです)
HTMLを編集する
"index.html"ファイルを次の様に編集します。
<!DOCTYPE html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="custom.css" rel="stylesheet"></link>
</head>
<body>
<div id="log"><ul></ul></div>
<div id="outer">
<div id="inner"></div>
</div>
<div id="xrview"></div>
<script type="module" src="./main.js" defer></script>
</body>
</html>
idが"xrview"となっているタグに、カメラの映像を描画します。
CSSファイルを編集する
"custom.css"ファイルを次の様に編集します。
/* CSS */
html{
width: 100%; height: 100%;
}
body{
width: 100%; height: 100%;
margin: 0px; padding: 0px;
}
#log{
width: auto; height: auto;
margin: 0px; padding: 5px;
position: absolute;
}
#log ul{
margin: 0px; padding: 0px;
}
#log li{
list-style-type:none;
}
#outer{
width: 100%; height: 100%;
margin: 0px; padding: 0px;
display: flex;
justify-content: center;
align-items: center;
}
#inner{
width: auto; height: auto;
margin: 0px; padding: 20px;
text-align: center;
}
こちらは特に説明することはありませんね。
JavaScriptファイルを編集する
いよいよメインの処理です。
次の流れで処理が進行します。
- Sceneに3Dオブジェクトを配置する
- WebXRに対応しているかどうか判定する
- VR/ARいずれかをリクエストする
- WebXRを描画する準備をする
- WebXRの描画を開始する
1,Sceneに3Dオブジェクトを配置する
Three.jsを利用して3Dオブジェクトを配置します。
同ライブラリについては、かじる程度に解説をした記事も御座います。
Three.jsをかじる本
次に示すコードは、3Dオブジェクトを作り"scene"(3D空間だと思ってください)に追加するものです。
// 省略
// GLTF
let model = new Gltf2Node({url: location.gltf});
model.scale = [0.1, 0.1, 0.1];
scene.addNode(model);
// 省略
2,WebXRに対応しているか判定する
実行中の端末がWebXRに対応しているかどうかを判定します。
"navigator.xr.isSessionSupported"関数の第一引数に、
VRの場合は"immersive-vr"、ARの場合は"immersive-ar"のいずれかを指定します。
(今回はARですので、"immersive-ar"を指定します)
実行後に続く"then"の仮引数"supprted"に利用の可否が格納されます。
// 省略
if(navigator.xr){
navigator.xr.isSessionSupported(XR_SESSION_STRING).then((supported)=>{
isXRAvailable = supported;
xrButton.enabled = supported;
appendLog("isXRAvailable:" + supported);
});
navigator.xr.requestSession("inline").then(onSessionStarted);
}
// 省略
3,VR/ARいずれかをリクエストする
"navigator.xr.requestSession"関数で、WebXRセッションをリクエストします。
こちらの第一引数には、判定で利用した内容と同じ文字列を指定します。
(今回は先程と同じく"immersive-ar"ですね)
// 省略
navigator.xr.requestSession(XR_SESSION_STRING, {requiredFeatures: ["anchors"]}).then((session)=>{
session.isImmersive = true;
xrButton.setSession(session);
onSessionStarted(session);
});
// 省略
4,WebXRを描画する準備をする
セッションが開始されたタイミングで、実際にWebXRを描画する準備をします。
// 省略
if(gl) return;
gl = createWebGLContext({xrCompatible: true});
document.querySelector("#xrview").appendChild(gl.canvas);
function onResize(){
gl.canvas.width = gl.canvas.clientWidth * window.devicePixelRatio;
gl.canvas.height = gl.canvas.clientHeight * window.devicePixelRatio;
}
window.addEventListener("resize", onResize);
onResize();
renderer = new Renderer(gl);
scene.setRenderer(renderer);
// 省略
5,WebXRの描画を開始する
最後は簡単です。
"session.requestAnimationFrame"で、描画を開始するだけです。
// 省略
scene.inputRenderer.useProfileControllerMeshes(session);
session.updateRenderState({baseLayer: new XRWebGLLayer(session, gl)});
session.requestReferenceSpace("local").then((refSpace)=>{
if(session.isImmersive) xrImmersiveRefSpace = refSpace;
session.requestAnimationFrame(onXRFrame);
});
// 省略
"https"環境でのサーバーにプログラム一式をUploadし、
Android端末からアクセスする事でWebXRを体験する事ができます。(やりました!!)
全体のコード
最後に、全体のコードを載せておきますね。
複数の関数が用意してありますが、それぞれ次の処理が実装されています。
関数名 | 処理内容 |
---|---|
initXR() | WebXRのスタートボタン、WebXR実行環境の判定 |
onRequestSession() | WebXRのセッションをリクエストします |
onSessionStarted() | WebXRが開始されたタイミングで実行されます |
onSessionEnded() | WebXRが終了したタイミングで実行されます |
initGL() | onSessionStartedで、画面全体にVR/AR空間を描画する準備をします |
onXRFrame() | WebXR空間を実際に描画します |
console.log("Hello WebXR!!");
// Modules
import {Gltf2Node} from "./libs/threejs/render/nodes/gltf2.js";
import {InlineViewerHelper} from "./libs/threejs/util/inline-viewer-helper.js";
import {QueryArgs} from "./libs/threejs/util/query-args.js";
import {Renderer, createWebGLContext} from "./libs/threejs/render/core/renderer.js";
import {Scene} from "./libs/threejs/render/scenes/scene.js";
import {SkyboxNode} from "./libs/threejs/render/nodes/skybox.js";
import {WebXRButton} from "./libs/threejs/util/webxr-button.js";
const XR_SESSION_STRING = "immersive-ar";
window.onload = (e)=>{
appendLog("Onload");
startWebXR("./assets/gltf/space/space.gltf");// Start WebXR!!
}
function startWebXR(location){
appendLog("startWebXR");
// XR
let xrButton = null;
let xrImmersiveRefSpace = null;
let isXRAvailable = false;
// WebGL
let gl = null;
let renderer = null;
let scene = new Scene();
// GLTF
let model = new Gltf2Node({url: location.gltf});
model.scale = [0.1, 0.1, 0.1];
scene.addNode(model);
let all_previous_anchors = new Set();
initXR();// Start the XR application.
function initXR(){
appendLog("initXR");
// Button
xrButton = new WebXRButton({
onRequestSession: onRequestSession,
onEndSession: onEndSession,
textXRNotFoundTitle: "XR NOT FOUND",
textEnterXRTitle: "START XR",
textExitXRTitle: "EXIT XR"
});
document.querySelector("#inner").appendChild(xrButton.domElement);
if(navigator.xr){
navigator.xr.isSessionSupported(XR_SESSION_STRING).then((supported)=>{
isXRAvailable = supported;
xrButton.enabled = supported;
appendLog("isXRAvailable:" + supported);
});
navigator.xr.requestSession("inline").then(onSessionStarted);
}
}
function onRequestSession(){
appendLog("onRequestSession");
return navigator.xr.requestSession(XR_SESSION_STRING, {requiredFeatures: ["anchors"]}).then((session)=>{
session.isImmersive = true;
xrButton.setSession(session);
onSessionStarted(session);
});
}
function onSessionStarted(session){
appendLog("onSessionStarted");
session.addEventListener("end", onSessionEnded);
if(session.isImmersive) initGL();
scene.inputRenderer.useProfileControllerMeshes(session);
session.updateRenderState({baseLayer: new XRWebGLLayer(session, gl)});
session.requestReferenceSpace("local").then((refSpace)=>{
if(session.isImmersive) xrImmersiveRefSpace = refSpace;
session.requestAnimationFrame(onXRFrame);
});
}
function onEndSession(session){
appendLog("onEndSession");
session.end();
}
function onSessionEnded(event){
appendLog("onSessionEnded");
document.querySelector("#xrview").innerHTML = "";
if(event.session.isImmersive) xrButton.setSession(null);
}
function initGL(){
appendLog("initGL");
if(gl) return;
gl = createWebGLContext({xrCompatible: true});
document.querySelector("#xrview").appendChild(gl.canvas);
function onResize(){
gl.canvas.width = gl.canvas.clientWidth * window.devicePixelRatio;
gl.canvas.height = gl.canvas.clientHeight * window.devicePixelRatio;
}
window.addEventListener("resize", onResize);
onResize();
renderer = new Renderer(gl);
scene.setRenderer(renderer);
}
function onXRFrame(t, frame){
let session = frame.session;
let xrRefSpace = xrImmersiveRefSpace;
let pose = frame.getViewerPose(xrRefSpace);
const tracked_anchors = frame.trackedAnchors;
if(tracked_anchors){
all_previous_anchors.forEach((anchor)=>{
if(!tracked_anchors.has(anchor)){
scene.removeNode(anchor.sceneObject);
}
});
tracked_anchors.forEach((anchor)=>{
const anchorPose = frame.getPose(anchor.anchorSpace, xrRefSpace);
if(anchorPose){
anchor.context.sceneObject.matrix = anchorPose.transform.matrix;
anchor.context.sceneObject.visible = true;
}else{
anchor.context.sceneObject.visible = false;
}
});
all_previous_anchors = tracked_anchors;
}else{
all_previous_anchors.forEach((anchor)=>{
scene.removeNode(anchor.sceneObject);
});
all_previous_anchors = new Set();
}
scene.updateInputSources(frame, xrRefSpace);
scene.startFrame();
session.requestAnimationFrame(onXRFrame);
scene.drawXRFrame(frame, pose);
scene.endFrame();
}
}
function appendLog(str){
let elem = document.createElement("li");
elem.innerText = str;
document.querySelector("#log ul").appendChild(elem);
}
最後に
駆け足ではありましたが、思ったより簡単にWebXRを体験する事ができる事が伝われば幸いです。
数年前にも試していたのですが、実行可能環境を含めとても扱いやすくなっていると感じました。
Webでの取り扱いもシンプルなので、色々試して遊べそうですね。
ここまで読んでいただき有難うございました。
Discussion