RealityKit Basics: Placing attachments in a scene

Three options for how to place attachments in a scene.

Overview

We already learned the basics of adding attachments in RealityKit. Let’s think about three ways we can position these in our scenes.

An immersive space with three attachments. One as the text and icon for a caution sign, two as a floating orange sign, and three as a standalone black sign.

Attachments as child entities

Here we have a yellow caution sign model that we load from Reality Composer Pro. We retrieve the attachment, add it as a child entity, then adjust the transform to place it.

// Example 01 - add an attachment as a child of an entity
if let wetFloorSign = scene.findEntity(named: "wet_floor_sign"),
   let wetFloorAttachment = attachments.entity(for: "wet_floor_attachment") {
    // The wet_floor_sign asset was converted from another format. It was scaled to 0.01 on all axes to fit in this scene.
    // We'll have to scale the attachment to compensate for the scale of the entity

    // Add the attachment attachment as a child of the wet floor sign
    wetFloorSign.addChild(wetFloorAttachment)

    // Adjust the transform to position it just in front of the sign
    let transform = Transform(scale: .init(repeating: 200), rotation: simd_quatf(Rotation3D(angle: Angle2D(degrees: 11), axis: RotationAxis3D(x: -1, y: 0, z: 0))), translation: [0, 30, 6.7])
    wetFloorAttachment.transform = transform
}

My choice of a caution sign for this example wasn’t an accident. When adding attachments like this, we need to be mindful of the transform–particularly the scale–of the parent entity. In this case, the model I used was designed for another platform. To get it to fit this scene, I had to scale it down to 0.1 on all axes. When we add an attachment as a child, the attachment is also scaled down with the parent. As a quick hack, we can scale up the attachment. A better solution use an app like Blender to convert the entity size to units that work for USDZ.

Using an entity to position the attachment

The second example adds an orange attachment above the traffic cone. This time, instead of adding the attachment as a child, we add it directly to the RealityView content. We can still use the transform data from the cone to inform the placement of the attachment.

// Example 02 - use an entity to position the attachment. Add the attachment to the scene content
if let trafficCone = scene.findEntity(named: "traffic_cone_02"),
   let traffiConeAttachment = attachments.entity(for: "traffic_cone_attachment") {

    // For this example, we'll add the attachment directly to the scenc content
    content.add(traffiConeAttachment)

    // Then we'll use the data from the traffic cone entity to determine the transform for the attachment
    let transform = Transform(
        scale: .init(repeating: 1.0),
        rotation: simd_quatf(
            Rotation3D(angle: Angle2D(degrees: -24), axis: RotationAxis3D(x: 0, y: 1, z: 0))
        ),
        translation: trafficCone.position + [0, 0.8 , 0]
    )

    traffiConeAttachment.transform = transform
}

The drawback to this option is that if we move the cone, the attachment won’t move with it. We could work around that with a “follow” component or a custom gesture that moves both entities.

Attachments as entities

Sometimes we may want to use an attachment in the scene that isn’t directly related to an entity. In this case, we can simply retrieve the attachment, add it to the context, and position it as needed.

// Example 03 - Add the attachment as a standalone entity
if let warningSign = attachments.entity(for: "warning_sign") {
    warningSign.position = [1, 1.2, -2]
    content.add(warningSign)
}

Example Code

Thanks to The Base Mesh for the amazing CC0 models.

struct Example061: View {
    var body: some View {
        RealityView { content, attachments in
            guard let scene = try? await Entity(named: "Caution", in: realityKitContentBundle) else { return }
            content.add(scene)

            // Example 01 - add an attachment as a child of an entity
            if let wetFloorSign = scene.findEntity(named: "wet_floor_sign"),
                let wetFloorAttachment = attachments.entity(for: "wet_floor_attachment") {
                // The wet_floor_sign asset was converted from another format. It was scaled to 0.01 on all axes to fit in this scene.
                // We'll have to scale the attachment to compensate for the scale of the entity

                // Add the wetFloorAttachment attachment as a child of the wet floor sign
                wetFloorSign.addChild(wetFloorAttachment)

                // Adjust the transform to position it just in front of the sign
                let transform = Transform(scale: .init(repeating: 200), rotation: simd_quatf(Rotation3D(angle: Angle2D(degrees: 11), axis: RotationAxis3D(x: -1, y: 0, z: 0))), translation: [0, 30, 6.7])
                wetFloorAttachment.transform = transform
            }

            // Example 02 - use an entity to position the attachment. Add the attachment to the scene content
            if let trafficCone = scene.findEntity(named: "traffic_cone_02"),
               let traffiConeAttachment = attachments.entity(for: "traffic_cone_attachment") {

                // For this example, we'll add the attachment directly to the scenc content
                content.add(traffiConeAttachment)

                // Then we'll use the data from the traffic cone entity to determine the transform for the attachment
                let transform = Transform(
                    scale: .init(repeating: 1.0),
                    rotation: simd_quatf(
                        Rotation3D(angle: Angle2D(degrees: -24), axis: RotationAxis3D(x: 0, y: 1, z: 0))
                    ),
                    translation: trafficCone.position + [0, 0.8 , 0]
                )

                traffiConeAttachment.transform = transform
            }

            // Example 03 - Add the attachment as a standalone entity
            if let warningSign = attachments.entity(for: "warning_sign") {
                warningSign.position = [1, 1.2, -2]
                content.add(warningSign)
            }

        } update: { content, attachments in

        } attachments: {
            Attachment(id: "wet_floor_attachment") {
                VStack(spacing: 24) {
                    Text("CAUTION")
                        .font(.largeTitle)
                    ZStack {
                        Image(systemName: "triangle")
                            .font(.system(size: 96, weight: .semibold))
                        Image(systemName: "figure.fall")
                            .font(.system(size: 42, weight: .heavy))
                            .offset(y:12)
                    }
                    Text("No Floor")
                        .font(.largeTitle)
                }
                .foregroundStyle(.black)
                .textCase(.uppercase)
                .padding()
            }

            Attachment(id: "traffic_cone_attachment") {
                VStack(spacing: 24) {
                    Text("Watch Out")
                        .font(.extraLargeTitle)
                    ZStack {
                        Image(systemName: "triangle")
                            .font(.system(size: 96, weight: .semibold))
                        Image(systemName: "eyes")
                            .font(.system(size: 36, weight: .heavy))
                            .offset(y:12)
                    }
                    Text("for traffic cones")
                        .font(.extraLargeTitle)
                }
                .padding(24)
                .foregroundStyle(.white)
                .textCase(.uppercase)
                .background(.trafficOrange)
                .clipShape(.rect(cornerRadius: 24.0))
            }

            Attachment(id: "warning_sign") {
                VStack(spacing: 24) {
                    Text("This scene contains gratuitous warnings")
                        .font(.system(size: 96, weight: .bold))
                        .textCase(.uppercase)
                        .multilineTextAlignment(.center)
                }
                .padding(24)
                .foregroundStyle(.white)
                .background(.black)
                .clipShape(.rect(cornerRadius: 24.0))
            }
        }
        .modifier(DragGestureImproved())
    }
}

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?