🥽

[visionOS] ARKitで検出したシーンのメッシュを可視化する

2023/12/21に公開

本記事でやりたいこと: visionOSで、ARKitのScene Reconstructionで検出したシーンのメッシュを可視化したい。


iOSでScene Reconstructionのメッシュを可視化した例。これをvisionOSでやりたい。

方法のひとつとしては、Xcodeの "Visualizations" 機能を使えば、ポチッとチェックを入れるだけで可視化できる。

https://note.com/shu223/n/n38e7a8f345dc

が、そうではなくて、プログラムからシーンのメッシュを描画するにはどうするか、という話。

公式サンプルの実装は何が足りないか

visionOSにおけるARKitのScene Reconstructionについて、公式チュートリアルが公開されている:

https://developer.apple.com/documentation/visionos/incorporating-real-world-surroundings-in-an-immersive-experience

以前はドキュメントだけだったが、サンプルコードも最近公開された。(※ ARKitなのでVision Pro実機でしか動作確認できない(シミュレータでは動かない))

このチュートリアルおよびサンプルコードでは、次のように MeshAnchor から ModelEntity を生成している:

for await update in sceneReconstruction.anchorUpdates {
    let meshAnchor = update.anchor

    guard let shape = try? await ShapeResource.generateStaticMesh(from: meshAnchor) else { continue }
    switch update.event {
    case .added:
        let entity = ModelEntity()
        entity.transform = Transform(matrix: meshAnchor.originFromAnchorTransform)
        entity.collision = CollisionComponent(shapes: [shape], isStatic: true)
        entity.components.set(InputTargetComponent())
        
        entity.physicsBody = PhysicsBodyComponent(mode: .static)
        
        meshEntities[meshAnchor.id] = entity
        contentEntity.addChild(entity)
    case .updated:
        guard let entity = meshEntities[meshAnchor.id] else { continue }
        entity.transform = Transform(matrix: meshAnchor.originFromAnchorTransform)
        entity.collision?.shapes = [shape]
    case .removed:
        meshEntities[meshAnchor.id]?.removeFromParent()
        meshEntities.removeValue(forKey: meshAnchor.id)
    }
}

複雑に見えるかもしれないが、次のように CollisionComponentInputTargetComponentPhysicsBodyComponent を持つ(つまり、インタラクションや物理演算が可能な)ModelEntity を生成してるだけだ。

let entity = ModelEntity()
entity.transform = Transform(matrix: meshAnchor.originFromAnchorTransform)
entity.collision = CollisionComponent(shapes: [shape], isStatic: true)
entity.components.set(InputTargetComponent())

entity.physicsBody = PhysicsBodyComponent(mode: .static)

関連:

https://note.com/shu223/n/ne60d3e0b83d4

ここでのポイントは、CollisionComponent の形状(shape)として、Scene Reconstructionによって検出されたシーンのメッシュ形状を利用している点:

guard let shape = try? await ShapeResource.generateStaticMesh(from: meshAnchor) else { continue }

...

entity.collision = CollisionComponent(shapes: [shape], isStatic: true)

衝突判定が効いているので、床やテーブルなどScene Reconstructionによって検出されたシーンの表面上に3Dオブジェクトを設置できる:

func addCube(tapLocation: SIMD3<Float>) {
    let placementLocation = tapLocation + SIMD3<Float>(0, 0.2, 0)

    let entity = ModelEntity(
        mesh: .generateBox(size: 0.1, cornerRadius: 0.0),
        materials: [SimpleMaterial(color: .systemPink, isMetallic: false)],
        collisionShape: .generateBox(size: SIMD3<Float>(repeating: 0.1)),
        mass: 1.0)

    entity.setPosition(placementLocation, relativeTo: nil)
    entity.components.set(InputTargetComponent(allowedInputTypes: .indirect))

    let material = PhysicsMaterialResource.generate(friction: 0.8, restitution: 0.0)
    entity.components.set(
        PhysicsBodyComponent(
            shapes: entity.collision!.shapes,
            mass: 1.0,
            material: material,
            mode: .dynamic)
    )

    contentEntity.addChild(entity)
}

(スクショは用意できないが、ぜひ実機で公式サンプル "SceneReconstructionExample" を動かしてみてください)

ただ、このコードではシーンのメッシュは可視化されない

InputTargetComponentCollisionComponent が与えられているだけで、ModelEntity にメッシュ情報が与えられていないからだ。

MeshAnchor から MeshResource を生成する

SceneReconstructionProvider からは、MeshAnchor という構造体が得られる。

https://developer.apple.com/documentation/arkit/meshanchor

シーンのメッシュを可視化するには、この MeshAnchor が持っているメッシュ情報を ModelEntity に渡す必要があるわけだが、そのためには MeshResource という型にする必要がある。

@MainActor
init(
    mesh: MeshResource,
    materials: [Material] = []
)

https://developer.apple.com/documentation/realitykit/modelentity

ShapeResource であれば次のようにメソッド一発で MeshAnchor から ShapeResource インスタンスを得られるのだが、

let shape = try? await ShapeResource.generateStaticMesh(from: meshAnchor)

MeshResource には残念ながらそのような便利メソッドがない。

ではどうするかというと、

MeshAnchor.Geometry から頂点情報(vertices)や法線情報(normals)を取り出し、MeshDescriptor に詰め直して、それを用いて MeshResource を生成する:

var desc = MeshDescriptor()
let posValues = geometry.vertices.asSIMD3(ofType: Float.self)
desc.positions = .init(posValues)
let normalValues = geometry.normals.asSIMD3(ofType: Float.self)
desc.normals = .init(normalValues)
do {
    desc.primitives = .polygons(
        (0..<geometry.faces.count).map { _ in UInt8(3) },
        (0..<geometry.faces.count * 3).map {
            geometry.faces.buffer.contents()
                .advanced(by: $0 * geometry.faces.bytesPerIndex)
                .assumingMemoryBound(to: UInt32.self).pointee
        }
    )
}
let meshResource = try MeshResource.generate(from: [desc])

これで MeshAnchor のメッシュを持つ ModelEntity を生成できる。

なお、MeshDescriptor を作成する実装はこちらのリポジトリを参考にした:
https://github.com/XRealityZone/what-vision-os-can-do

サンプル

サンプルコードをGitHubで公開しています。Xcode 15.1 beta 3 で動作確認済み。要実機。

https://github.com/shu223/visionOS-Sampler

(参考になったらスター、記事へのLikeいただけると嬉しいです)

脚注
  1. 12/21時点で未投稿だったため「代わりに投稿」させていただきました。 ↩︎

Discussion