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 theextrusionMethod
we want. In this case, we use .linear
, which takes adepth
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!