Skysphere flickering w attachment at finial display of scene

There is a flickering and slight dimming occurring specifically on skysphere, at initial load of the scene, when using Attachment. This is observed in the simulator and on the real device.

Since we cannot upload a video illustrating the undesirable behaviour, I have to describe how to setup the project for you to observe it.

To replicate the issue, follow these steps:

  1. Create a new visionOS app using Xcode template, see image.
  2. Configure the project to launch directly into an immersive space (set Preferred Default Scene Session Role to Immersive Space Application Session Role in Info.plist), see image.
  3. Replace all swift files with those you will find in the attached texts.
  4. Add the skysphere image asset Skydome_8k found at this Apple Sample App Presenting an artist’s scene.
  5. Launch the app in debug mode via Xcode and onto the AVP device or simulator
  6. Continuously open and dismiss the skysphere by pressing on buttons Open Skysphere and Close.
  7. Observe the skysphere flicker and dim upon display of the skysphere.

The current workaround is commented in file ThreeSixtySkysphereRealityView at lines 65, 70, 71, and 72. Uncomment these lines, and the flickering and dimming do not occur.

  • Are we using attachments wrongly?
  • Is this behavior known and documented?
  • Or, is there really a bug in visionOS?

import SwiftUI

enum RealityViewScene {
    case intro
    case threeSixtySkyShpere
}

/// Maintains app-wide state
@MainActor
@Observable
class AppModel {
    var showingScene: RealityViewScene = .intro
}

import AVKit
import OSLog
import SwiftUI
import RealityKit
import RealityKitContent

struct InitialImmersiveView: View {
    let logger = Logger(subsystem: SUBSYSTEM, category: "InitialImmersiveView")
    @Environment(\.scenePhase) var scenePhase
    @Environment(AppModel.self) var appModel
    
    private let rootEntity = Entity()
    
    var body: some View {
        RealityView { content, attachments in
            logger.info("\(#function) \(#line) `content, attachments in`")
            updateScene(content: content, attachments: attachments)
            
        } update: { content, attachments in
            
            logger.info("\(#function) \(#line) `update: { content, attachments in`")
            updateScene(content: content, attachments: attachments)

        } placeholder: {
            
            let _ = logger.info("\(#function) \(#line) `placeholder`")
            ProgressView()
            
        } attachments: {
            
            let _ = logger.info("\(#function) \(#line) `attachments`")
            
            Attachment(id: "OpenSkysphereView") {
                Button {
                    appModel.showingScene = .threeSixtySkyShpere
                } label: {
                    Text("Open Skysphere")
                }
            }
        }
        .onChange(of: scenePhase, { oldValue, newValue in
            logger.info("\(#function) \(#line) `onChange(of: scenePhase` scenePhase oldValue \(String(describing: oldValue)) scenePhase newValue \(String(describing: newValue))")
            switch newValue {
            case .active:
                logger.info("\(#function) \(#line) scenePhase newValue \(String(describing: newValue))")
            case .background:
                logger.info("\(#function) \(#line) scenePhase newValue \(String(describing: newValue))")
                unloadScenes()
            case .inactive:
                logger.info("\(#function) \(#line) scenePhase newValue \(String(describing: newValue))")
            @unknown default:
                logger.warning("\(#function) \(#line) scenePhase newValue \(String(describing: newValue))")
            }
        })
    }
    
    func updateScene(content: RealityViewContent, attachments: RealityViewAttachments) {
        logger.info("\(#function) \(#line)")
        guard scenePhase == .active else {
            logger.info("\(#function) \(#line) escaping since app backgrounded")
            return
        }

        if rootEntity.children.isEmpty {
            Task{
                await loadScenes(content: content, attachments: attachments)
            }
        }
    }
    
    func loadScenes(content: RealityViewContent, attachments: RealityViewAttachments) async {
        logger.info("\(#function) \(#line)")
        
        guard let openSkysphereViewButton = attachments.entity(for: "OpenSkysphereView") else {
            logger.error("\(#function) \(#line) missing attachment")
            assertionFailure("missing attachment")
            return
        }
        openSkysphereViewButton.position = [0.0, 0.1, -2.5]
        openSkysphereViewButton.scale = scaleForAttachements
        rootEntity.addChild(openSkysphereViewButton)
        content.add(rootEntity)
    }
    
    func unloadScenes() {
        logger.info("\(#function) \(#line)")
        rootEntity.children.removeAll()
    }
}

import SwiftUI
import RealityKit
import RealityKitContent

struct MainImmersiveView: View {
    @Environment(AppModel.self) private var appModel
    var body: some View {
        switch appModel.showingScene {
        case .intro:
            InitialImmersiveView()
        case .threeSixtySkyShpere:
            ThreeSixtySkysphereRealityView()
        }
        
    }
}

import OSLog
import SwiftUI

let SUBSYSTEM = Bundle.main.bundleIdentifier!
let scaleForAttachements = SIMD3(2.5, 2.5, 2.5)
@main
struct TestSkysphereAttachmentFlickerApp: App {
    let logger = Logger(subsystem: SUBSYSTEM, category: "TestVideoLeakOneImmersiveViewApp")
    @State private var appModel = AppModel()
    @Environment(\.openImmersiveSpace) var openImmersiveSpace
    @Environment(\.dismissImmersiveSpace) var dismissImmersiveSpace

    var body: some Scene {
        Group {
            ImmersiveSpace(id: "MainImmersiveView") {
                MainImmersiveView()
                    .environment(appModel)
            }
            .immersionStyle(selection: .constant(.mixed), in: .mixed)
        }
     }
}

import OSLog
import RealityKit
import RealityKitContent
import SwiftUI

struct ThreeSixtySkysphereRealityView: View {
    let logger = Logger(subsystem: SUBSYSTEM, category: "ThreeSixtySkysphereRealityView")
    
    @Environment(\.scenePhase) var scenePhase
    @Environment(AppModel.self) var appModel
    
    let rootEntity = Entity()
    let threeSixtySkysphereEntity = Entity()
    
    var body: some View {
        RealityView { content, attachments in
            logger.info("\(#function) \(#line) `RealityView { content, attachments in`")
            await loadScenes(content: content, attachments: attachments)
            
        } update: { content, attachments in
            logger.info("\(#function) \(#line) `update: { content, attachments in`")
            
        } placeholder: {
            
            let _ = logger.info("\(#function) \(#line) `placeholder`")
            ProgressView()
            
        } attachments: {
            
            let _ = logger.info("\(#function) \(#line) `attachments`")
            
            Attachment(id: "AttachmentID.exitThreeSixty") {
                Button(action: {
                    logger.info("\(#function) \(#line) button pressed")
                    appModel.showingScene = .intro
                }) {
                        Text("Close")
                }
                .glassBackgroundEffect()
                .cornerRadius(25)

            }
        }
    }
    
    func loadScenes(content: RealityViewContent, attachments: RealityViewAttachments) async {
        logger.info("\(#function) \(#line)")
        addSkyboxImage(entity: threeSixtySkysphereEntity)
        rootEntity.addChild(threeSixtySkysphereEntity)
        
        guard let exitThreeSixtyAttachement = attachments.entity(for: "AttachmentID.exitThreeSixty") else {
            logger.error("\(#function) \(#line) missing attachment")
            assertionFailure("missing attachment")
            return
        }
        exitThreeSixtyAttachement.scale = scaleForAttachements
        let anchorEntity = AnchorEntity(world: [0, 0.6, -1.8])
        anchorEntity.addChild(exitThreeSixtyAttachement)
        rootEntity.addChild(anchorEntity)
//        exitThreeSixtyAttachement.isEnabled = false
        content.add(rootEntity)
        
        // This delay in displaying the attachment resolves an issue whereby there would
        // otherwise be a slight flicker and dimming upon initial load of the scene.
//        DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
//            exitThreeSixtyAttachement.isEnabled = true
//        }
    }
    
    func addSkyboxImage(entity: Entity) {
        do {
            let uiImage = UIImage(named: "Skydome_8k")!
            
            // There is a hard limit of 8,192px width images in this `TextureResource.generate` method
            // If we try to generate a texture from an image of 8,193px width, texture generation will fail.
            let texture = try TextureResource(image: uiImage.cgImage!, options: TextureResource.CreateOptions.init(semantic: nil))
            
            var material = UnlitMaterial()
            material.color = .init(texture: .init(texture))
            entity.components.set(ModelComponent(
                mesh: .generateSphere(radius: 1000),
                materials: [material])
            )
            
            entity.scale *= .init(x: -1, y: 1, z: 1)
            entity.transform.translation += SIMD3(0.0, 1.0, 0.0)
            entity.transform.rotation = simd_quatf(angle: 89.5, axis: [0, 1, 0]) // rotate so that user faces the sea
            
        } catch (let error) {
            logger.error("\(#function) \(#line) texture not loaded with error \(error.localizedDescription)")
            assertionFailure("texture not loaded with error \(error.localizedDescription)")
        }
    }
}

#Preview {
    ThreeSixtySkysphereRealityView()
}

Skysphere flickering w attachment at finial display of scene
 
 
Q