Lab 055 – Using SF Symbols in Particle Emitters

We can create Texture Resources to be used in our Particle Emitters.

Overview

If you haven’t already done so, take a look at Simulating particles in your visionOS app. Apple provided this sample project to teach us the basics of working with particles. One of the coolest things about it is the ability to convert SF Symbols to Texture Resources. I made this lab to have something to quickly reference when I need to do this. Maybe I’ll even convert this to an extension at some point.

The only change that I made to generateTextureFromSystemName was to deal with aspect ratio. The vast majority of symbols have a square ratio, but from time time we’ll run into one that doesn’t. For example vision.pro.fill is wider than it is tall. Using the version of generateTextureFromSystemName from the project linked above squished it!

We need to get the aspect ratio of the symbol and adjust accordingly. The output still needs to be a square image. We need to scale it, then position the symbol in the center. Here is the modified version.

// Credit: Apple provided this function in the example project called "Simulating particles in your visionOS app"
// Modified to better support wide or tall symbols
    func generateTextureFromSystemName( _ name: String) -> TextureResource? {
        // Create a UIImage from a symbol name.
        guard var symbolImage = UIImage(systemName: name) else {
            return nil
        }

        // Create a new version that always uses the template rendering mode.
        symbolImage = symbolImage.withRenderingMode(.alwaysTemplate)
        
        // Get the natural size of the symbol
        let symbolSize = symbolImage.size
        print("symbol size \(symbolSize)")
        
        // Create a square texture
        let textureSize: CGFloat = 128
        let imageSize = CGSize(width: textureSize, height: textureSize)
        
        // Calculate the aspect ratio and scaled size
        let aspectRatio = symbolSize.width / symbolSize.height
        print("aspect ratio \(aspectRatio)")
        let scaledSize: CGSize
        if aspectRatio > 1 {
            // Wider than tall
            scaledSize = CGSize(width: textureSize, height: textureSize / aspectRatio)
        } else {
            // Taller than wide or square
            scaledSize = CGSize(width: textureSize * aspectRatio, height: textureSize)
        }
        
        // Calculate the position to center the symbol
        let x = (textureSize - scaledSize.width) / 2
        let y = (textureSize - scaledSize.height) / 2
        let drawRect = CGRect(origin: CGPoint(x: x, y: y), size: scaledSize)
        print("draw rect \(drawRect)")

        // Start the graphics context.
        UIGraphicsBeginImageContextWithOptions(imageSize, false, 0)

        // Set the color's texture to white so that the app can apply a color
        // on top of the image.
        UIColor.white.set()

        // Draw the image with the context, centered in the square
        symbolImage.draw(in: drawRect, blendMode: .normal, alpha: 1.0)

        // Retrieve the image from the context.
        let contextImage = UIGraphicsGetImageFromCurrentImageContext()

        // End the graphics context.
        UIGraphicsEndImageContext()

        // Retrieve the Core Graphics version of the image.
        guard let coreGraphicsImage = contextImage?.cgImage else {
            return nil
        }

        // Generate the texture resource from the Core Graphics image.
        let creationOptions = TextureResource.CreateOptions(semantic: .raw)
        return try? TextureResource(image: coreGraphicsImage,
                                             options: creationOptions)
    }

Let’s see this in action

The lab set ups a toolbar with a handful of symbols. We create a particle emitter update the image using generateTextureFromSystemName whenever the selected symbol changes.

Full Lab Code

struct Lab055: View {

    @State private var selectedSymbol: String = "vision.pro.fill"

    let symbols = ["vision.pro.fill", "macpro.gen3.fill",  "macbook.gen2", "iphone", "applewatch", "ipad"]

    var body: some View {
        RealityView { content in

            let subject = Entity()
            subject.name = "Particles"
            content.add(subject)

            // Create the particle emitter
            var particleSystem = ParticleEmitterComponent()
            particleSystem.isEmitting = true
            particleSystem.speed = 0.01
            particleSystem.emitterShape = .sphere
            particleSystem.emitterShapeSize = [0.3, 0.3, 0.3]

            // Set the image to the result of generateTextureFromSystemName using a symbol name
            particleSystem.mainEmitter.image = generateTextureFromSystemName(selectedSymbol)
            particleSystem.mainEmitter.birthRate = 25
            particleSystem.mainEmitter.size = 0.1

            // Add the component to the entity
            subject.components.set(particleSystem)

        } update: { content in

            if let subject = content.entities.first?.findEntity(named: "Particles") {
                if var particleSystem = subject.components[ParticleEmitterComponent.self] {
                    particleSystem.mainEmitter.image = generateTextureFromSystemName(selectedSymbol)
                    subject.components.set(particleSystem)
                }
            }
        }
        .toolbar {
            ToolbarItem(placement: .bottomOrnament, content: {
                HStack {
                    ForEach(symbols, id: \.self) { symbol in
                        Image(systemName: symbol)
                            .frame(width: 32, height: 32)
                            .foregroundColor(.white)
                            .onTapGesture {
                                selectedSymbol = symbol
                            }
                            .padding(6)
                            .background( selectedSymbol == symbol ? .purple : Color.clear)
                            .clipShape(.circle)
                    }
                }
            })
        }
    }

    // Credit: Apple provided this function in the example project called "Simulating particles in your visionOS app"
    // Sources: https://developer.apple.com/documentation/realitykit/simulating-particles-in-your-visionos-app
    func generateTextureFromSystemName( _ name: String) -> TextureResource? {
        // Create a UIImage from a symbol name.
        guard var symbolImage = UIImage(systemName: name) else {
            return nil
        }

        // Create a new version that always uses the template rendering mode.
        symbolImage = symbolImage.withRenderingMode(.alwaysTemplate)
        
        // Get the natural size of the symbol
        let symbolSize = symbolImage.size
        print("symbol size \(symbolSize)")
        
        // Create a square texture
        let textureSize: CGFloat = 128
        let imageSize = CGSize(width: textureSize, height: textureSize)
        
        // Calculate the aspect ratio and scaled size
        let aspectRatio = symbolSize.width / symbolSize.height
        print("aspect ratio \(aspectRatio)")
        let scaledSize: CGSize
        if aspectRatio > 1 {
            // Wider than tall
            scaledSize = CGSize(width: textureSize, height: textureSize / aspectRatio)
        } else {
            // Taller than wide or square
            scaledSize = CGSize(width: textureSize * aspectRatio, height: textureSize)
        }
        
        // Calculate the position to center the symbol
        let x = (textureSize - scaledSize.width) / 2
        let y = (textureSize - scaledSize.height) / 2
        let drawRect = CGRect(origin: CGPoint(x: x, y: y), size: scaledSize)
        print("draw rect \(drawRect)")

        // Start the graphics context.
        UIGraphicsBeginImageContextWithOptions(imageSize, false, 0)

        // Set the color's texture to white so that the app can apply a color
        // on top of the image.
        UIColor.white.set()

        // Draw the image with the context, centered in the square
        symbolImage.draw(in: drawRect, blendMode: .normal, alpha: 1.0)

        // Retrieve the image from the context.
        let contextImage = UIGraphicsGetImageFromCurrentImageContext()

        // End the graphics context.
        UIGraphicsEndImageContext()

        // Retrieve the Core Graphics version of the image.
        guard let coreGraphicsImage = contextImage?.cgImage else {
            return nil
        }

        // Generate the texture resource from the Core Graphics image.
        let creationOptions = TextureResource.CreateOptions(semantic: .raw)
        return try? TextureResource(image: coreGraphicsImage,
                                             options: creationOptions)
    }
}

Support our work so we can continue to bring you new examples and articles.

Questions or feedback?