WebXRでARアプリを作ろうと思ったのですが、iOSはWebXRに正式対応していないのが現状です。
そこで、React Three XRの公式でも紹介されていた Variant Launch を使って、iOSでもWebXRのARが動作するように対応してみました。
Does it work on iOS?
WebXR for VR experiences is supported on Safari for Apple Vision Pro. WebXR is not supported on iOS Safari yet. The alternative is to use products such as Variant Launch, which allow to build WebXR experiences for iOS.–翻訳
iOSで動作しますか?
VR体験のためのWebXRは、Apple Vision ProのSafariでサポートされています。iOSのSafariではWebXRはまだサポートされていません。代わりに、iOS用のWebXR体験を構築できるVariant Launchなどの製品を使用することができます。
Three.js単体での実装例は多少あるものの、React Three Fiberを使った実装例はほとんど見つからず、Next.jsで構成する例もあまりなかったので、今回整理してまとめておきます。
実装中にいくつかハマりどころもあったので、参考になれば幸いです。
Variant Launchとは何か?
Variant Launchは、iOS Safariが正式にWebXRに対応していない中で、iOS上でWebXRを実現するためのソリューションです。
仕組みのポイントは「App Clip」を使っている点にあります。通常のWebアプリとしてiOSのSafari上でWebXRを動かすのではなく、App Clip経由で専用のネイティブコンポーネントを起動し、その上でWebXRのAPIを提供しています。これにより、通常のiOS制限では使えないカメラアクセスやセンサー情報も取得でき、標準のWebXR互換APIとしてWeb側から利用できるようになっています。
App ClipはiOSにプリインストールされている機能で、ユーザーはアプリを事前にダウンロードする必要がありません。Webページから直接App Clipが呼び出され、そのままWebXRのセッションが開始されます。
なお、もし対象のアプリ(Variant Launchの専用アプリ)が既にインストールされている場合は、App Clipではなくそのアプリ上でWebXRが動作する仕組みになっています。

使用ライブラリの一覧
今回のプロジェクトで使用したライブラリは以下です。
ライブラリのバージョンや、WebXR以外の開発用途で使用しているライブラリについては、後述の package.json
にも掲載しています。
- Next.js
Reactのフレームワーク。今回はサーバーサイドレンダリング(SSR)回避のために、"use client"
やdynamic import
を活用してWebXRの描画部分をクライアント側に限定しています。 - React Three Fiber
Three.jsをReactで扱うためのラッパー。Three.jsのレンダリングやシーン管理をReactコンポーネントの形で扱えます。 - React Three XR
React Three Fiber上でWebXRを扱うための追加ライブラリ。<XR>
コンポーネントやcreateXRStore
を使って、WebXRセッションを簡単に管理できます。 - Three.js
3D描画のコアライブラリ。React Three Fiberの内部でThree.jsが動いており、通常はFiberのコンポーネント経由で操作します。必要に応じて、Three.jsの低レベルAPIを直接利用することもできますが、今回の実装では主にFiber経由で利用しています。 - Variant Launch
iOSでWebXRを動作させるためのサービス。内部でApp Clipや専用アプリを利用して、WebXR互換のAPIを提供します。 - Vercel
今回は、デプロイ先にVercelを利用します。
Variant Launchのドメインにデプロイした際に発行されるドメインを設定しましょう。
その他ライブラリやバージョンはpackage.jsonを読んでください。
※threeは、最新バージョンだとうまく動かなかったので、少しバージョンを下げました。
{
"name": "glb-ar-viewer",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@react-three/drei": "^10.1.2",
"@react-three/fiber": "^9.1.2",
"@react-three/xr": "^6.6.17",
"next": "15.3.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"three": "^0.171.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.3.3",
"tailwindcss": "^4",
"typescript": "^5"
}
}
環境構築
環境構築に関しては、create-next-appでひな形を作成しました。
npx create-next-app@latest glb-ar-viewer
npm install three @react-three/fiber @react-three/xr @react-three/drei
基本は、このコマンドで大丈夫なはずですが、AR表示時にエラーが出た場合はthreeのバージョンを下げるといいかと思います。
iOS対応のWebXR-ARアプリ実装
環境構築ができたら、実際にiOS対応のWebXR-ARアプリ実装をしていきたいと思います。
フォルダ構成は下記になっています。
glb-ar-viewer/
├── .next/
├── node_modules/
├── public/
├── src/
│ ├── app/
│ │ ├── favicon.ico
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ └── page.tsx
│ └── components/
│ └── ARCanvas.tsx
├── .gitignore
├── eslint.config.mjs
├── next-env.d.ts
├── next.config.ts
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── README.md
└── tsconfig.json
他に見たい箇所があれば、GitHubを確認してください。
app/globals.css:背景透過の設定
WebXRのARでは、背景を透過させてカメラ映像を背後に表示する必要があります。
今回は TailwindCSS を使いながら、以下のようにCSSを設定しています。
next.jsであれば、「globals.css」を下記の内容にしてもらえれば問題ないかなと思います。
@import "tailwindcss";
:root {
--background: transparent;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
app/layout.tsx:Variant LaunchのSDKをScriptタグで事前読み込み
Variant Launchを利用するために、layout.tsxでVariant LaunchのSDKをScriptタグで事前読み込みさせておきます。
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Script from "next/script";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "GLB AR Viewer",
description: "Application to display GLB models in AR",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
{children}
</body>
{/* Variant Launch SDKの読み込み */}
<Script
src="https://launchar.app/sdk/v1?key=xxxxxx&redirect=true"
strategy="beforeInteractive"
/>
</html>
);
}
ポイントは、下記になります。適切に設定できると、iOSの場合にVariant Launchに自動でリダイレクトしてくれます。
strategy="beforeInteractive"
→ クライアント側のスクリプトより先にSDKをロードする指定。WebXR互換APIが正しく生えるために必要。redirect=true
→ Variant LaunchのURLリダイレクト有効化(初回起動時のApp Clip誘導処理用)
app/page.tsx:dynamic import を使ってCanvasを表示
Next.jsのページ本体では、SSR(サーバーサイドレンダリング)時にWebXR関連のコードが評価されないよう、dynamic import
を使ってCanvas部分をクライアント側限定で読み込んでいます。
"use client";
import dynamic from "next/dynamic";
const ARCanvas = dynamic(() => import("@/components/ARCanvas"), {
ssr: false,
});
export default function Page() {
return <ARCanvas />;
}
ssr: false
にすることで、サーバーサイドでは一切Canvasを描画せず、クライアントマウント時にのみ描画処理が行われます。
SSRになってしまうと、Variant Launchでは動かないようなので注意が必要です。
components/ARCanvas.tsx:AR表示部分の実装
WebXRのセッション管理には createXRStore
を使用し、セッション開始時に必要な requiredFeatures
として local
、hit-test
、dom-overlay
を指定しています。CanvasはReact Three Fiberの<Canvas>
コンポーネントで描画します。
"use client";
import { Canvas } from "@react-three/fiber";
import { XR, createXRStore } from "@react-three/xr";
import { useState } from "react";
export default function ARCanvas() {
const [red, setRed] = useState(false);
const [store] = useState(() =>
createXRStore({
customSessionInit: {
requiredFeatures: ["local", "hit-test", "dom-overlay"],
optionalFeatures: ["anchors"],
domOverlay: { root: document.body },
},
})
);
const handleEnterAR = async () => {
if (store) {
await store.enterAR();
}
};
return (
<div className="w-screen h-screen relative">
<div className="absolute top-4 left-4 z-10 flex gap-4">
<button
onClick={handleEnterAR}
className="p-3 bg-white text-black rounded"
>
Enter AR
</button>
</div>
<Canvas
style={{ backgroundColor: "transparent" }}
onCreated={({ gl }) => {
gl.xr.enabled = true;
gl.xr.setReferenceSpaceType("local");
}}
>
<XR store={store}>
<ambientLight />
<directionalLight position={[1, 2, 3]} />
<mesh
pointerEventsType={{ deny: "grab" }}
onClick={() => setRed(!red)}
position={[0, 1, -1]}
>
<boxGeometry />
<meshBasicMaterial color={red ? "red" : "blue"} />
</mesh>
</XR>
</Canvas>
</div>
);
}
Variant LaunchのSDKガイド通りに対応していくだけですが、React Three XRのcreateXRStoreを使った手順はないので、読み替えてあげれば対応は大丈夫です。

注意点まとめ
バージョンによって挙動は異なると思いますが、備忘録としてハマったポイントを整理しておきます。
SSR(サーバーサイドレンダリング)によるエラー
Next.jsはデフォルトでSSRを行うため、document
や window
を参照すると ReferenceError
になります。
WebXRは基本的にクライアントサイドのAPIなので、描画部分はすべて "use client"
を指定し、さらに dynamic import
を使ってSSRを無効化しています。
※AndroidはSSRでも動くので、iOSで動かない場合にチェックしてみるといいです。
const ARCanvas = dynamic(() => import("@/components/ARCanvas"), {
ssr: false,
});
gl.xr.setReferenceSpaceType(“local”) は明示的に指定
createXRStore
で referenceSpaceType
を間接的に指定はできますが、Three.jsの gl.xr
側には自動で反映されないこともあります。iOSやVariant Launchの動作安定性のため、明示的に指定しています。
理由は詳しくはわかりませんが、これを指定しないとiOSでは表示ができませんでした。
gl.xr.setReferenceSpaceType("local");
背景透過の設定
WebXRでARを行う場合、背景が透過になっていないとカメラ映像が見えません。CSSで body
を透過に設定し、さらにCanvasでも backgroundColor: "transparent"
を指定しています。
:root {
--background: transparent;
}
body {
background: var(--background);
}
Variant Launch SDKの事前読み込み
iOS上でWebXR互換APIを使うために、Variant LaunchのSDKを事前に読み込んでいます。Next.jsのRootLayout
内に記述。
これを記載するだけで、勝手にVariant Launchにリダイレクトしてくれます。
<Script
src="https://launchar.app/sdk/v1?key=..."
strategy="beforeInteractive"
/>
最後に動作確認(iPhone 16で確認)
私の愛用端末であるiPhone 16で動作確認を行いました。(Androidでも正常に動くことは確認しています。)
下記のように無事AR表示ができています!もちろん、タップ操作もうごいていました!


iOSはまだWebXRには対応していないのですが、Variant Launchを使えば疑似的にiOSでもWebXRを使うことができるというのがわかりました。
ちょっと手間ではありますが、Variant Launchは無料で3000ビュー対応しているのでぜひ試してみてください!
早く、iOSもWebXRに対応するといいですね。。。