Skip to main content
Donovan’s Vision Blog

Bringing 3D to window content

We can add depth to windowed content in visionOS with 3D objects and 3D padding.

In the post we will look at ways to use use RealityView to generate some 3D artwork, and position it alongside some buttons and inputs, using 3D positioning.

3D theme selector #

When working on BlockStack I wanted to make a theme-selector screen that showed how the textures would look in 3D. The resulting screen looks like this:

BlockStack Theme Selector

The view includes a pair of 3D objects generated in code to which I've applied textures. Then it uses a Picker UI element to change the selected texture. Let's build something similar.

Example #

Let's build a simplified version of the above to see how 3D positioning works:

A 3D window content view

Adding a 3D object #

To start we can set up some initial code that generates and displays different shapes based on a given selectedShape:

struct ContentView: View {
    @State private var selectedShape = "cube"

    var body: some View {
        VStack {
            if selectedShape == "sphere" {
                RealityView { content in
                    content.add(generateEntity("sphere"))
                }
            } else if selectedShape == "cone" {
                RealityView { content in
                    content.add(generateEntity("cone"))
                }
            } else {
                RealityView { content in
                    content.add(generateEntity())
                }
            }
        }
        .padding()
    }

    func generateEntity(_ shape: String = "cube") -> ModelEntity {
        var modelEntity = ModelEntity(mesh: .generateBox(size: 0.1))
        if shape == "sphere" {
            modelEntity = ModelEntity(mesh: .generateSphere(radius: 0.1))
        }
        if shape == "cone" {
            modelEntity = ModelEntity(mesh: .generateCone(height: 0.1, radius: 0.1))
        }
        modelEntity.model?.materials = [SimpleMaterial(color: .red, isMetallic: true)]
        return modelEntity
    }

}

We can generate a shape and show it in the content view. Let's add some UI. Adjust the above by adding a "Done" button and a Picker to select different shapes:

var body: some View {
        VStack {
            Button("Done") {
                // Perform relevant action for "Done"
            }
            .tint(.blue)

            if selectedShape == "sphere" {
                // ... existing code
            }

            Picker("Piece texture", selection: $selectedShape) {
                ForEach(["cube", "sphere", "cone"], id: \.self) { shape in
                    Text(shape.uppercased()).tag(shape)
                }
            }
        }
    }

This adds some 2D content to our view, but the 3D shape is in the way!

The UI is blocked by the 3D shape

We could tweak the size of the 3D object to not block the button and Picker, but then when the picker opens it'll still be behind the object. Let's instead bring those 2D elements forward to make them easier to see and interact with.

Adding UI depth #

We can use padding3D to bring the UI forward:

Picker("Piece texture", selection: $selectedShape) {
    ForEach(["cube", "sphere", "cone"], id: \.self) { shape in
        Text(shape.uppercased()).tag(shape)
    }
}
.padding3D(.back, 400)

Adding padding3D with a back value of 400 will bring it forward. Apply this to both the picker and the "Done" button makes them easier to see:

UI positioned in front

Finished code #

Here's what we have for the complete demo:

ContentView

struct ContentView: View {
    @State private var selectedShape = "cube"

    var body: some View {
        VStack {
            Button("Done") {
                // Perform relevant action for "Done"
            }
            .tint(.blue)
            .padding3D(.back, 400)

            if selectedShape == "sphere" {
                RealityView { content in
                    content.add(generateEntity("sphere"))
                }
            } else if selectedShape == "cone" {
                RealityView { content in
                    content.add(generateEntity("cone"))
                }
            } else {
                RealityView { content in
                    content.add(generateEntity())
                }
            }

            Picker("Piece texture", selection: $selectedShape) {
                ForEach(["cube", "sphere", "cone"], id: \.self) { shape in
                    Text(shape.uppercased()).tag(shape)
                }
            }
            .padding3D(.back, 400)
        }
        .padding()
    }

    func generateEntity(_ shape: String = "cube") -> ModelEntity {
        var modelEntity = ModelEntity(mesh: .generateBox(size: 0.1))
        if shape == "sphere" {
            modelEntity = ModelEntity(mesh: .generateSphere(radius: 0.1))
        }
        if shape == "cone" {
            modelEntity = ModelEntity(mesh: .generateCone(height: 0.1, radius: 0.1))
        }
        modelEntity.model?.materials = [SimpleMaterial(color: .red, isMetallic: true)]
        return modelEntity
    }

}

App View

struct ExampleApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .defaultSize(width: 420, height: 300)
    }
}