How to Create a Custom 3D Shape for RealityKit on visionOS from a SwiftUI Shape

Hello, everyone! đź‘‹ Today, I want to share a super interesting way to transform a 2D Shape created with SwiftUI into a 3D model for RealityKit on visionOS.

In this tutorial, we will use a fantastic feature that Apple introduced to the RealityKit API in visionOS 2.0, which allows you to create a MeshResource from a Path: the MeshResource(extruding:extrusionOptions:).

With this, you can add depth and create a Mesh of the resulting 3D object from any Path, including texts in AttributedStrings.

Let’s go step-by-step:

Step 1: Requirements

Since this feature was added in visionOS 2.0, you will need:

  • Xcode 16 beta
  • visionOS 2.0

Step 2: Creating a 2D Shape in SwiftUI

To create our 2D shape, we will use Circles from SwiftUI.

To get a tube shape, we need to create a circle with a hole in the middle, which will be the interior of our tube.

With SwiftUI, it’s very easy to do this:

let diameter = CGFloat(radius * 2.0)
let internalDiameter = diameter - CGFloat(thick * 2.0)
let innerShape = Circle().size(width: internalDiameter, height: internalDiameter)
let roundShape = Circle().size(width: diameter, height: diameter)

let hollowCircle = roundShape
    .symmetricDifference(innerShape.offset(x: CGFloat(thick), y: CGFloat(thick)))

Step 3: Converting the 2D Shape into a 3D Mesh

To convert our 2D shape into a 3D object, we will use the extruding method from MeshResource.

var options = MeshResource.ShapeExtrusionOptions()
options.extrusionMethod = .linear(depth: height)
            
let mesh = try await MeshResource(
    extruding: hollowCircle.path(
        in:CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)
    ),
    extrusionOptions: options
)

In the code above, we do two essential things to create the object:

  • We created ShapeExtrusionOptions to specify the extrusionMethod we want. In this case, we use .linear, which takes a depth value. In the example, the tube’s depth can be considered its height when placed vertically.
  • We created a MeshResource, performing the extruding from the path of our hollow circle and specifying the options we want for the extruding procedure.

Step 4: Creating and Configuring our ModelEntity

Now that we have our shape converted into a MeshResource, we can use it to create a ModelEntity and configure its physical body and collision structure.

let entity = ModelEntity(mesh: mesh, materials: [SimpleMaterial(color: .gray, isMetallic: false)])
            
let shape = try await ShapeResource.generateStaticMesh(from: mesh)

let colision = CollisionComponent(shapes: [shape], isStatic: false)

entity.components.set(colision)

entity.components.set(PhysicsBodyComponent(
    massProperties: .default,
    mode: .static
))

Step 5 (Optional): Making Our Tube Stand

If you want your tube to stand vertically instead of horizontally, you can simply move it to the correct position.

entity.move(to: simd_float4x4(simd_quatf(.init(angle: Angle2D(degrees: 90), axis: .x))), relativeTo: nil)

Our Final Implementation

In the end, we will have an implementation similar to this:

import RealityKit
import SwiftUI

extension ModelEntity {
    @MainActor
    enum Tube {

        enum Position {
            case vertical
            case horizontal
        }
        
        static func generateTube(
            radius: Float,
            thick: Float,
            height: Float,
            position: Tube.Position = .horizontal
        ) async throws -> some Entity {
            let diameter = CGFloat(radius * 2.0)
            let internalDiameter = diameter - CGFloat(thick * 2.0)
            let innerShape = Circle().size(width: internalDiameter, height: internalDiameter)
            let roundShape = Circle().size(width: diameter, height: diameter)
            
            let hollowCircle = roundShape
                .symmetricDifference(innerShape.offset(x: CGFloat(thick), y: CGFloat(thick)))
            
            var options = MeshResource.ShapeExtrusionOptions()
            options.extrusionMethod = .linear(depth: height)
            
            let mesh = try await MeshResource(
                extruding: hollowCircle.path(
                    in:CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)
                ),
                extrusionOptions: options
            )
            
            let entity = ModelEntity(mesh: mesh, materials: [SimpleMaterial(color: .gray, isMetallic: false)])
            
            let shape = try await ShapeResource.generateStaticMesh(from: mesh)
            
            let collision = CollisionComponent(shapes: [shape], isStatic: false)
            
            entity.components.set(collision)
            
            entity.components.set(PhysicsBodyComponent(
              massProperties: .default,
              mode: .static
            ))
            
            if position == .vertical {
                entity.move(to: simd_float4x4(simd_quatf(.init(angle: Angle2D(degrees: 90), axis: .x))), relativeTo: nil)
            }
            
            return entity
        }
    }
}

Want to Try It Out?

All the code and its application in a demo project are available on my GitHub: https://github.com/salla-andre/TubeExample

And That’s It, Folks! 🎉

I hope you enjoyed this little tutorial and that it will be useful for your next project on visionOS. Experiment with different shapes and materials to create your own immersive experiences. See you next time!