ARKit PlaneDetectionProvider: creating simple planes from anchors

We can use the extent of the anchor to create simple planes and colliders.

Overview

When I started this mini-series on PlaneDetectionProvide, I was a bit confused. My first attempt was to use the extents (bounds) of the anchor for the size of the mesh. I posted a question to the Apple Developer Forums and shared it online.

I'm having issues with plan tracking in ARKit. Can you see what I'm getting wrong here? developer.apple.com/forums/threa…

Joseph Simpson (@vrhermit.com) 2025-04-14T17:59:44.495Z

The issue was that the transform for the anchor is the center of an n-gon, whereas the extent was offset to surround the shape. As PlatformGoblin pointed out, I needed to incorporate the transform for the extent into my calculations. An anonymous Vision Pro Engineer followed up with a way to simplify things even more.

// ❌ Bad
entity.transform = Transform(matrix: anchor.originFromAnchorTransform)

// ✅ Better
entity.setTransformMatrix(anchor.originFromAnchorTransform * anchor.geometry.extent.anchorFromExtentTransform,relativeTo: nil)

// 🚀 Best
let extent = anchor.geometry.extent
let mesh = MeshResource.generatePlane(width: extent.width, height: extent.height)
let material = SimpleMaterial(color: .green, isMetallic: false)
let entity = ModelEntity(mesh: mesh, materials: [material])
entity.transform = Transform(matrix: matrix_multiply(anchor.originFromAnchorTransform, extent.anchorFromExtentTransform))

Make sure to read the whole thread for some extra details.

Let’s see the results.

Simple planes showing my desk and a corner of my office

We can compare this to the more complex shape by rendering them both.

Complex shapes in dark red, simple in beige, offset to prevent z-fighting

Example Code

This is just a hack to render both sets of entities.

struct Example072: View {
    @State var session = ARKitSession()
    @State private var planeAnchors: [UUID: Entity] = [:]
    @State private var planeAnchorsSimple: [UUID: Entity] = [:]


    var body: some View {
        RealityView { content in

        } update: { content in
            for (_, entity) in planeAnchors {
                if !content.entities.contains(entity) {
                    content.add(entity)
                }
            }
            for (_, entity) in planeAnchorsSimple {
                if !content.entities.contains(entity) {
                    content.add(entity)
                }
            }
        }
        .task {
            try! await setupAndRunPlaneDetection()
        }
    }

    func setupAndRunPlaneDetection() async throws {
        let planeData = PlaneDetectionProvider(alignments: [.horizontal, .vertical, ])
        if PlaneDetectionProvider.isSupported {
            do {
                try await session.run([planeData])
                for await update in planeData.anchorUpdates {
                    switch update.event {
                    case .added, .updated:
                        let anchor = update.anchor

                        let planeEntity = createPlaneEntity(for: anchor)
                        planeAnchors[anchor.id] = planeEntity
                        let planeEntitySimple = createSimplePlaneEntity(for: anchor)
                        planeAnchorsSimple[anchor.id] = planeEntitySimple

                    case .removed:
                        let anchor = update.anchor
                        planeAnchors.removeValue(forKey: anchor.id)
                        planeAnchorsSimple.removeValue(forKey: anchor.id)

                    }
                }
            } catch {
                print("ARKit session error \(error)")
            }
        }
    }

    private func createSimplePlaneEntity(for anchor: PlaneAnchor) -> Entity {

        let extent = anchor.geometry.extent
        let mesh = MeshResource.generatePlane(width: extent.width, height: extent.height)
        var material = PhysicallyBasedMaterial()
        material.baseColor.tint = UIColor(.stepBackgroundSecondary)
        material.blending =
            .transparent(opacity: PhysicallyBasedMaterial.Opacity(floatLiteral: 0.5))

        let entity = ModelEntity(mesh: mesh, materials: [material])
        entity.transform = Transform(matrix: matrix_multiply(anchor.originFromAnchorTransform, extent.anchorFromExtentTransform))

        // Just a hack to prevent z fighting with the other planes
        if(anchor.alignment == .vertical) {
            entity.position.z -= 0.05
        } else if(anchor.alignment == .horizontal) {
            entity.position.y -= 0.05
        }


        return entity
    }

    private func createMeshResource(anchor: PlaneAnchor) -> MeshResource? {
        // Generate a mesh for the plane (for occlusion).
        var meshResource: MeshResource? = nil
        do {
            var contents = MeshResource.Contents()
            contents.instances = [MeshResource.Instance(id: "main", model: "model")]
            var part = MeshResource.Part(id: "part", materialIndex: 0)

            // Convert vertices to SIMD3<Float>
            let vertices = anchor.geometry.meshVertices
            var vertexArray: [SIMD3<Float>] = []
            for i in 0..<vertices.count {
                let vertex = vertices.buffer.contents().advanced(by: vertices.offset + vertices.stride * i).assumingMemoryBound(to: (Float, Float, Float).self).pointee
                vertexArray.append(SIMD3<Float>(vertex.0, vertex.1, vertex.2))
            }
            part.positions = MeshBuffers.Positions(vertexArray)

            print("vertices \(vertices)")
            print("was converted to \(vertexArray)")

            // Convert faces to UInt32
            let faces = anchor.geometry.meshFaces
            var faceArray: [UInt32] = []
            let totalFaces = faces.count * faces.primitive.indexCount
            for i in 0..<totalFaces {
                let face = faces.buffer.contents().advanced(by: i * MemoryLayout<Int32>.size).assumingMemoryBound(to: Int32.self).pointee
                faceArray.append(UInt32(face))
            }
            part.triangleIndices = MeshBuffer(faceArray)

            print("faces \(faces)")
            print("was converted to \(faceArray)")

            contents.models = [MeshResource.Model(id: "model", parts: [part])]
            meshResource = try MeshResource.generate(from: contents)
            return meshResource
        } catch {
            print("Failed to create a mesh resource for a plane anchor: \(error).")
        }
        return nil
    }

    private func createPlaneEntity(for anchor: PlaneAnchor) -> Entity {
        let entity = Entity()
        entity.name = "Plane \(anchor.id)"
        entity.setTransformMatrix(anchor.originFromAnchorTransform, relativeTo: nil)

        var material = PhysicallyBasedMaterial()
        material.baseColor.tint = UIColor(.stepRed)

        if let meshResource = createMeshResource(anchor: anchor) {
            entity.components.set(ModelComponent(mesh: meshResource, materials: [material]))
        }

        return entity
    }
}

Support our work so we can continue to bring you new examples and articles.

Download the Xcode project with this and many more examples from Step Into Vision.
Some examples are provided as standalone Xcode projects. You can find those here.

Questions or feedback?