Explore Spatial Scenes with Image Presentation Component

We can pre or post-generate Spatial Scenes, depending on the needs of our app.

Overview

When converting photos to Spatial Scenes, we have two different paths we can take.

Pre-generate

We can choose to pre-generate the Spatial Scene before adding the component to an entity. This will let us present the image in our preferred viewing mode, but it can feel a bit slower. We provide an image URL to ImagePresentationComponent.Spatial3DImage and call generate() on the result. We can check availableViewingModes and set desiredViewingMode. Then we add the component to an entity.

let converted = try await ImagePresentationComponent.Spatial3DImage(contentsOf: url)
try await converted.generate()
var component = ImagePresentationComponent(spatial3DImage: converted)
if component.availableViewingModes.contains(.spatial3D) {
  component.desiredViewingMode = .spatial3D
}
entity.components.set(component)

This works well, but it can leave us with empty UI. You may want to consider showing a Progress View or loading indicator during generation. Let’s look at another option.

Post-generate

The set up is the same. We provide a URL and create an instance of Spatial3DImage. Then we create the component and add it to the entity. This will display the image in mono viewing mode. Next we call generate() on the image. If this succeeds, our component will be updated with spatial3D and spatial3DImmersive viewing modes.

let converted = try await ImagePresentationComponent.Spatial3DImage(contentsOf: url)
var component = ImagePresentationComponent(spatial3DImage: converted)
entity.components.set(component)

try await converted.generate()

// Provide some UI to switch modes later
if component.availableViewingModes.contains(.spatial3D) {
  component.desiredViewingMode = .spatial3D
}

This option can provide a mono image as soon as visionOS can load it, making the UI feed more responsive. From here we can provide some controls to change viewing mode.

Dealing with Errors

The process to generate a Spatial Scene can throw errors. We can handle them just like any other error.

generationState = .loading
do {
    try await converted.generate()
    generationState = .success
} catch {
    generationState = .failure
    print("Generation failed: \(error.localizedDescription)")
    return
}

Video Demo

Example Code

struct Example130: View {
    @State var photoEntity = Entity()

    @State private var generationState: GenerationState = .empty
    @State private var currentMode = ImagePresentationComponent.ViewingMode.mono
    @State private var availableModes: Set<ImagePresentationComponent.ViewingMode> = []


    var body: some View {
        RealityView { content in
            photoEntity.setPosition([0, 1.6, -2.0], relativeTo: nil)
            photoEntity.scale = .init(repeating: 0.6)
            content.add(photoEntity)

            // Attach SwiftUI controls into the scene
            let controlMenu = Entity()
            let controlAttachment = ViewAttachmentComponent(
                rootView: ControlsPanel(
                    generationState: $generationState,
                    currentMode: $currentMode,
                    availableModes: $availableModes,
                    clearPhoto: { clearPhoto() },
                    loadPhotoPreGen: { Task { await loadPhotoPreGen(entity: photoEntity) } },
                    loadPhotoPostGen: { Task { await loadPhotoPostGen(entity: photoEntity) } }
                )
            )
            controlMenu.components.set(controlAttachment)
            controlMenu.setPosition([0, 1.2, -1.8], relativeTo: nil)
            content.add(controlMenu)

        }

        // Listen for changes to mode and update the component
        .onChange(of: currentMode, { _, newValue in
            photoEntity.components[ImagePresentationComponent.self]?.desiredViewingMode = newValue
        })
    }

    // Clear the current component
    func clearPhoto() {
        photoEntity.components.remove(ImagePresentationComponent.self)
        generationState = .empty
        currentMode = .mono
    }

    /// Load a regular (non-spatial) photo, then convert it to a Spatial Scene
    func loadPhotoPreGen(entity: Entity) async {
        guard let url = Bundle.main.url(forResource: "bell-01", withExtension: "jpeg") else { return }
        do {
            let converted = try await ImagePresentationComponent.Spatial3DImage(contentsOf: url)

            generationState = .loading

            do {
                try await converted.generate()
                var component = ImagePresentationComponent(spatial3DImage: converted)
                availableModes = component.availableViewingModes

                if availableModes.contains(.spatial3D) {
                    component.desiredViewingMode = .spatial3D
                    currentMode = .spatial3D
                }
                entity.components.set(component)
                generationState = .success
            } catch {
                let component = ImagePresentationComponent(spatial3DImage: converted)
                entity.components.set(component)
                availableModes = component.availableViewingModes
                currentMode = .mono
                generationState = .failure
                print("Generation failed: \(error.localizedDescription)")
                return
            }

        } catch {
            print("Failed to load image: \(error)")
        }
    }

    /// Load a regular (non-spatial) photo, but do not generate it yet.
    func loadPhotoPostGen(entity: Entity) async {
        guard let url = Bundle.main.url(forResource: "bell-01", withExtension: "jpeg") else { return }
        do {
            let converted = try await ImagePresentationComponent.Spatial3DImage(contentsOf: url)
            let component = ImagePresentationComponent(spatial3DImage: converted)
            entity.components.set(component)

            generationState = .loading
            do {
                try await converted.generate()
                availableModes = component.availableViewingModes
                currentMode = .mono
                generationState = .success
            } catch {
                availableModes = component.availableViewingModes
                currentMode = .mono
                generationState = .failure
                print("Generation failed: \(error.localizedDescription)")
                return
            }

        } catch {
            print("Failed to load image: \(error)")
        }
    }
}

fileprivate enum GenerationState {
    case empty
    case loading
    case success
    case failure
}

// Moving the control panel to a view
fileprivate struct ControlsPanel: View {
    @Binding var generationState: GenerationState
    @Binding var currentMode: ImagePresentationComponent.ViewingMode
    @Binding var availableModes: Set<ImagePresentationComponent.ViewingMode>

    let clearPhoto: () -> Void
    let loadPhotoPreGen: () -> Void
    let loadPhotoPostGen: () -> Void

    var body: some View {
        VStack(spacing: 12) {
            HStack {
                switch generationState {
                case .empty:
                    Text("No photo loaded.")
                case .loading:
                    Text("Generating Spatial Scene...")
                case .success:
                    Text("Spatial Scene generated.")
                case .failure:
                    Text("Failed to generate Spatial Scene.")
                }
            }

            HStack(spacing: 12) {

                Button(action: {
                    clearPhoto()
                }, label: {
                    Text("Clear")
                })

                Button(action: {
                    loadPhotoPreGen()
                }, label: {
                    Text("Pre Gen")
                })

                Button(action: {
                    loadPhotoPostGen()
                }, label: {
                    Text("Post Gen")
                })
            }
            .controlSize(.extraLarge)

            HStack {
                Button(action: {
                    currentMode = .mono
                }, label: {
                    Text("mono")
                })
                .disabled(!availableModes.contains(.mono))

                Button(action: {
                    currentMode = .spatial3D
                }, label: {
                    Text("spatial3D")
                })
                .disabled(!availableModes.contains(.spatial3D))

                Button(action: {
                    currentMode = .spatial3DImmersive
                }, label: {
                    Text("spatial3DImmersive")
                })
                .disabled(!availableModes.contains(.spatial3DImmersive))

            }
            .controlSize(.small)
            .padding()

        }
        .padding()
        .background(.black)
        .clipShape(.capsule)
    }
}

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?