Scene with over 12,000 duplicate entities

I am creating a RealityKit scene that will contain over 12,000 duplicate cubes arranged in a circle (see image below). This is for some high-energy physical simulations I am doing. I accomplish this scene by creating a single cube and cloning it a bunch of times. So, I there is a single MeshResource and Material even though there are a lot of entities. I have confirmed this by checking with Swift's === operator. Even with this, the program is unworkably slow.

Any suggestions or tricks that could help with this type of scene?

Using a single geometry was the trick to getting SceneKit to work fast with geometries like this. I've been updating my software to RealityKit because I far prefer the structure of RealityKit over SceneKit.

Glad to know your prefer RealityKit over SceneKit 😉. A few questions: what OS and Mac hardware are you running on? Also, can you provide an example of how you are creating your cube entities? Do you know if your cubes have any physics simulation enabled? If you turn off physics, does the performance improve? It would be great to know if this is a rendering or physics issue (or something else entirely).

I apologize for forgetting important information.

Machine: MacBook Pro 16" M1Max w/ 64 Gigs of memory (brand spanking new and loving the GPU for raytracing!) OS: Monterey 12.0.1

There are no RealityKit physics simulations on. I have confirmed this by checking that the PhysicsBodyComponent and PhysicsMotionComponent are both nil. I only have ModelComponent, TransformComponent, and the synchronization component that is always present I believe.

I am including the important code at the end of this post. I should note that I put this entity into an ARView that has an environment.lighting.resource and an environment.background as you can probably see from my original attached image. I am currently testing whether getting rid of these helps performance but I am not expecting a big difference. I generated a RKView subclass of ARView that is basically recreating SceneKits display mode that allowed for mouse camera control, and 3D display without augmented reality since my applications are not really augmented-reality based and more virtual-reality based.

Finally, I summarize the code by noting that I create a single Box Entity with one material. I then clone this entity in an 8x6 grid. Finally, I clone this 8x6 grid 64x4 times to create the ring of detectors to model my system. The entity variable is what is displayed.

I appreciate all the support on these forums. It's a great help to get direct feedback from people well connected with the Apple developers!

-Matt

public class PETRingSystem: ObjectProviding {
    let ringDiameter:Float = 0.81   // 81 cm

    let crystalSize = SIMD3<Float>(0.0047, 0.0063, 0.03) // (4.7mm x 6.3mm x 3cm)

    // Spacing between crysals.  0.2 mm
    let spacing: Float = 0.000274

    // Crystal blocks are 8x6
    let blockDim = SIMD2<Int>(8, 6)  // in number of detectors
    
    // Compute the block size from the crystal size, blockDim, and spacing
    var blockSize: SIMD2<Float> {
        get {
            let ang = Float(blockDim.x) * (crystalSize.x + spacing)
            let z = (Float(blockDim.y) * crystalSize.y) + (Float(blockDim.y - 1) * spacing)
            return(SIMD2<Float>(ang,z))
        }
    }

    // How many blocks in the ring
    let blocksInRing = SIMD2<Int>(64, 4)

    // The axian field of view
    let axialFOV:Float = 0.1576
    
    // blanks for the crystal Entity that is cloned and the block entity that is also cloned
    var crystal = Entity()
    var block = Entity()
    
    // The final entity that is displayed
    public var entity = Entity()

    
    
    /// Duplicate the blocks around the origin (0,0,0) to create the ring of detectors around the patient's head
    public func populateRing() {
        let s = blockSize.y
        let f = axialFOV
        for zIdx in 0..<(blocksInRing.y) {
            for angIdx in 0..<(blocksInRing.x) {
                let node = block.clone(recursive: true)
                entity.addChild(node)

                var offset:Float = 0.0
                if (blocksInRing.y == 1) {
                    offset = 0.0
                } else {
                    offset = (Float(zIdx) * (f - s) / (Float(blocksInRing.y - 1)))
                    offset -= (f - s) / 2.0
                }

                let ang = Float(angIdx) * 2.0 * Float.pi / Float(blocksInRing.x)

                // Move the block to -ringDiameters/2 in the y direction, then rotate to the proper angle
                node.transform.matrix = Transform(translation: SIMD3<Float>(0, -ringDiameter/2.0, offset)).matrix * node.transform.matrix
                node.transform.matrix = Transform(pitch: 0.0, yaw: 0.0, roll: ang).matrix * node.transform.matrix

            }
        }
    }


        
    
    /// Create the ring of PET detectors
    public init() {
        // Create the mesh for the cubic detector
        let box = MeshResource.generateBox(size: crystalSize)
        // The display material
        let material = SimpleMaterial(color: .blue, roughness: 0.1, isMetallic: false)
        
        // Create the base entity
        let crystalRaw = ModelEntity(mesh: box, materials: [material])
        // Shift the raw crystal so that the origin is centered on the entrance face
        crystalRaw.position = SIMD3<Float>(0.0, 0.0, crystalSize.z / 2.0)
        
        // The actual Entity used to create the ring of detectors
        crystal = Entity()
        crystal.addChild(crystalRaw)
        
        // The ring of detector is organized in 8x6 blocks.  This creates one block by
        // cloning the crystal

        let blockRaw = Entity()

        for i in 0..<blockDim.x { // 8
            for j in 0..<blockDim.y { // 6
                let crystalInBlock = crystal.clone(recursive: true)
                
                blockRaw.addChild(crystalInBlock)
                crystalInBlock.position = SIMD3<Float>(Float((crystalSize.x + spacing) * Float(i)), Float((crystalSize.y + spacing) * Float(j)), Float(0.0))
            }
        }

        block.addChild(blockRaw)
        // Center the origin of the block directly in the middle of all the crystal detectors
        blockRaw.position = SIMD3<Float>(-Float(blockDim.x - 1) * (crystalSize.x + spacing) / 2.0,
                                     -Float(blockDim.y - 1) * (crystalSize.y + spacing) / 2.0,
                                     0.0)
        // Transform so that the detectors are facign up
        blockRaw.transform.matrix = Transform(pitch: Float.pi/2, yaw: 0.0, roll: 0.0).matrix * blockRaw.transform.matrix

        // This function clones the blocks and places them in the ring around (0,0,0)
        populateRing()
        
    }

}

Thanks Matt!

I asked the team, and instancing support is not exposed through the RealityKit API, so you are likely issuing 12K draw calls each frame. Instead of creating all of these entities, would you be able to use the custom MeshDescriptor APIs to generate a single mesh containing all of the 12K cubes? It's possible that might improve your performance bit more. Also I agree the background lighting is not the cause of your performance issues.

Additionally, could you file a bug report about this on Feedback Assistant explaining your use case? That way we can try to improve the performance for you in future OS releases. Attach a sample project if you can, and also post the feedback number here so I can send it to the graphics team directly.

Now that RealityKit 2 is out, is there a way to properly do instanced rendering? Witha large number of meshes that are more complicated then these cubes (ex. 1-2k verts), instancing becomes more important and combining them into a single mesh becomes more intensive, assuming the objects will move at all then wouldn’t they need to be recombined every frame?

The LowLevelMesh might be the solution to your problem. You have to manage the meshes yourself. This means you cannot use the boxes or balls provided by RealityKit.

Scene with over 12,000 duplicate entities
 
 
Q