Android で AR 機能を実装しようとして io.github.sceneview を選んだ方の多くが、最初に AnchorNode 関連のエラーで詰まります。検索しても日本語の情報が少なく、Stack Overflow の回答も古いものが多い——そんな状況でこの記事にたどり着いた方の参考になれば嬉しいです。
SceneView は ARCore をより使いやすく抽象化したライブラリです。Sceneform が2021年に非推奨になって以降、Android AR 開発のデファクト的な選択肢になっています。私自身、個人開発で AR を試した際に最初の数日をライフサイクルとセッション管理のエラーに費やしたので、同じ落とし穴を避けられるよう、つまずきやすい順に並べてあります。
SceneView のセットアップ
まず、最新の SceneView を追加します。
// build.gradle.kts(Module)
dependencies {
implementation("io.github.sceneview:arsceneview:2.3.0")
}AndroidManifest.xml に ARCore 関連の記述を追加します。
<manifest>
<!-- ARCore 必須 -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera.ar" android:required="true" />
<application>
<meta-data
android:name="com.google.ar.core"
android:value="required" />
</application>
</manifest>required を optional にすると、ARCore 非対応端末にも配信できますが、その場合は実行時の可用性チェック(後述)が必須になります。AR が体験の中心なら required、付加機能なら optional という使い分けが実務的です。
レイアウトと ARSceneView
Compose を使っている場合は AndroidView でラップします。
// Compose での使い方
@Composable
fun ARScreen() {
var arSceneView: ARSceneView? = remember { null }
AndroidView(
factory = { context ->
ARSceneView(context).also { sceneView ->
arSceneView = sceneView
}
},
modifier = Modifier.fillMaxSize()
)
}従来の XML レイアウトの場合は直接配置できます。
<io.github.sceneview.ar.ARSceneView
android:id="@+id/arSceneView"
android:layout_width="match_parent"
android:layout_height="match_parent" />AnchorNode エラーの対処
SceneView を使い始めて最初にぶつかる問題が、AnchorNode のライフサイクルエラーです。よく見られるのは以下のようなクラッシュです。
java.lang.IllegalStateException: AnchorNode must be attached to an ARSceneView
原因は、ARSceneView が完全に初期化される前に AnchorNode を追加しようとすること、またはセッションが有効でない状態で操作しようとすることです。
修正前(よくある間違い):
// ❌ onViewCreated で即座に AnchorNode を作ろうとしている
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val anchorNode = AnchorNode(arSceneView.engine, anchor) // クラッシュ
arSceneView.addChild(anchorNode)
}修正後:
// ✅ onSessionResumed コールバックを使う
arSceneView.onSessionResumed = { session ->
// セッションが有効になってから操作する
setupARContent()
}
private fun setupARContent() {
arSceneView.onTapAr = { hitResult, _ ->
val anchor = hitResult.createAnchor()
val anchorNode = AnchorNode(arSceneView.engine, anchor)
anchorNode.addChild(
ModelNode(
modelInstance = modelLoader.createModelInstance(modelFile),
autoAnimate = true
)
)
arSceneView.addChild(anchorNode)
}
}onSessionResumed コールバックで ARCore セッションが確立してから処理を開始するのが基本です。タップ時の hitResult から createAnchor() を呼ぶ流れにしておくと、「まだ平面が検出されていない座標にアンカーを作ろうとしてクラッシュする」という別系統のエラーも同時に防げます。
ARCore のインストールを確実に促す
見落としやすいのが、「ARCore 対応端末なのにアプリが落ちる」ケースです。対応していても、ARCore(Google Play Services for AR)が端末に入っていない、あるいは古いことがあります。requestInstall でインストールを促す処理を入れておくと、この種のクラッシュを丸ごと防げます。
// ARCore の導入状況を確認し、未導入ならインストールを促す
private var installRequested = false
private fun ensureArCore(): Boolean {
return try {
when (ArCoreApk.getInstance().requestInstall(requireActivity(), !installRequested)) {
ArCoreApk.InstallStatus.INSTALL_REQUESTED -> {
// インストール画面に遷移したので、次回 onResume を待つ
installRequested = true
false
}
ArCoreApk.InstallStatus.INSTALLED -> true
}
} catch (e: UnavailableUserDeclinedInstallationException) {
// ユーザーがインストールを拒否した
showFallbackUI()
false
}
}ポイントは installRequested フラグを持っておくことです。requestInstall は一度インストール画面へ遷移すると、戻ってきた直後の onResume で再び呼ばれます。フラグなしで毎回 true(=ユーザーに必ず確認)を渡すと、無限にダイアログが出る挙動になってしまいます。
3Dモデルの読み込み
SceneView では .glb 形式のモデルをシンプルに扱えます。
// assets/ フォルダに model.glb を配置して読み込む
class ARFragment : Fragment() {
private lateinit var modelLoader: ModelLoader
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val sceneView = view.findViewById<ARSceneView>(R.id.arSceneView)
modelLoader = ModelLoader(sceneView.engine, requireContext())
sceneView.onSessionResumed = { _ ->
setupTapToPlace(sceneView)
}
}
private fun setupTapToPlace(sceneView: ARSceneView) {
sceneView.onTapAr = { hitResult, _ ->
lifecycleScope.launch {
val modelInstance = modelLoader.loadModelInstance("models/robot.glb")
?: return@launch
val anchor = hitResult.createAnchor()
val anchorNode = AnchorNode(sceneView.engine, anchor).apply {
addChild(ModelNode(
modelInstance = modelInstance,
scaleToUnits = 0.5f // メートル単位でスケール指定
))
}
sceneView.addChild(anchorNode)
}
}
}
}.glb のポリゴン数が多いと、配置した瞬間にフレームレートが落ちます。AR は実機のカメラ処理と描画を同時に走らせるため、デスクトップでの 3D 表示より余裕がありません。配置用モデルは数万ポリゴン以内に抑え、テクスチャは 1〜2K に圧縮しておくと安定します。
平面検出を可視化してユーザーを誘導する
AR で意外と多いのが「どこをタップすればモデルが置けるのか分からない」という体験上の問題です。SceneView は検出した平面を点群で可視化する機能を持っているので、配置できる場所をユーザーに示せます。
// 検出した平面をドットで可視化する
arSceneView.planeRenderer.isEnabled = true
arSceneView.onTapAr = { hitResult, _ ->
placeModel(hitResult)
// 配置が済んだら視覚的なノイズを減らすため非表示にする
arSceneView.planeRenderer.isEnabled = false
}最初は平面ガイドを表示し、最初のモデルを置いたら消す、という切り替えだけで体験の分かりやすさが大きく変わります。実機で試すと、ガイドがないと多くのユーザーがカメラを動かさずに固まってしまうのが分かります。
Antigravity AI との組み合わせ
Antigravity AI を使って、AR シーンに配置するオブジェクトや配置位置の提案を生成するアプローチも試してみました。
// Antigravity AI でシーン提案を生成して AR に反映する例
suspend fun generateARSceneFromPrompt(prompt: String): ARSceneConfig {
val client = AntigravityClient(apiKey = "YOUR_API_KEY")
val response = client.generateScene(
prompt = prompt,
outputFormat = "structured_json",
parameters = mapOf(
"include_position" to true,
"include_scale" to true,
"coordinate_system" to "ar_world"
)
)
return ARSceneConfig.fromJson(response.content)
}
// 生成された設定を AR シーンに適用
fun applySceneConfig(config: ARSceneConfig, sceneView: ARSceneView) {
config.objects.forEach { objectConfig ->
// 各オブジェクトを指定位置に配置
val position = Float3(
objectConfig.position.x,
objectConfig.position.y,
objectConfig.position.z
)
// ...
}
}AI が提案したレイアウトをそのまま AR 空間に展開できるのは、インタラクティブな体験設計の可能性を大きく広げてくれます。ただし AI が返す座標は必ずしも検出済みの平面上に乗るとは限らないので、各座標をそのまま使う前にヒットテストで近傍の平面へスナップさせる一手間を入れると、宙に浮くオブジェクトを避けられます。
アンカーの後始末とドリフト対策
長く使われる AR 画面では、配置したアンカーが積み上がっていきます。ARCore はアンカーごとに環境を追跡し続けるため、不要なアンカーを残すとトラッキングが重くなり、位置がじわじわずれる「ドリフト」も目立ちやすくなります。使い終わったアンカーは明示的に解放するのが安全です。
// 配置済みモデルをすべて片付ける
fun clearPlacedModels(sceneView: ARSceneView) {
sceneView.children
.filterIsInstance<AnchorNode>()
.forEach { node ->
node.anchor.detach() // ARCore セッションからアンカーを解放
sceneView.removeChild(node)
}
}リセットボタンや画面遷移のタイミングで detach() を呼ぶ運用にしておくと、長時間の利用でも安定します。私が試した範囲では、アンカーを 10 個以上残したまま動かし続けると、古いオブジェクトの位置が実空間から少しずつ離れていく現象がはっきり出ました。
ライフサイクルの管理
SceneView はライフサイクルに敏感です。Fragment / Activity の onResume / onPause に対応した処理が必要です。
override fun onResume() {
super.onResume()
arSceneView.onResume()
}
override fun onPause() {
super.onPause()
arSceneView.onPause()
}
override fun onDestroy() {
super.onDestroy()
arSceneView.destroy() // エンジンリソースの解放
}destroy() の呼び忘れはメモリリークの原因になるので注意が必要です。SceneView が内部で使う Filament エンジンはネイティブリソースを大量に保持するため、GC だけでは解放されません。
ARCore 非対応デバイスへの対応
ARCore が使えないデバイスでのフォールバック処理も忘れずに実装します。
if (!ArCoreApk.getInstance().checkAvailability(requireContext()).isSupported) {
// AR非対応の場合は代替UIを表示
showFallbackUI()
return
}SceneView を使うことで ARCore の低レベルな API を直接扱う部分は大きく減りますが、デバイス対応確認はアプリレイヤーで必ず行う必要があります。
次のステップ
SceneView の基本が理解できたら、平面検出の精度を上げるための環境光推定(Light Estimation)や、複数のアンカーを使った空間マッピングに挑戦してみてください。AR 開発は実機で動かしながら確認するのが一番の近道で、エミュレーターでは再現できない挙動が多くあります。まずは手元の対応端末を 1 台用意し、タップでモデルを 1 つ置く——ここまでを最短で動かすところから始めるのがおすすめです。