swiftrealitykitreality-composer-pro

Is it possible to programmatically set a material to a non model entity?


I'm trying to duplicate a material and apply it to a non model entity that has model entity children to replicate the material binding behaviour in Reality Composer Pro but it doesn't seem to be possible in code.

In RCP, if I assign a material to the body here, all the child entities will apply the material (unless they have one with a stronger binding)

Entity hierarchy in RCP showing a default entity, Body, with children model entities

Material bound to body getting applied to children as well

Here's the code sample I use to apply the material. It fails on component guard. I wasn't able to find a proper component to apply it to but I also don't know much in RealityKit.

func applyGreeterMaterials(greeter: Entity, material: ShaderGraphMaterial) {
        // Body not in item types, done manually
        applyMaterialOn(on: "body")
        
        for type in ItemCategory.greeterItem.types {
            applyMaterialOn(on: type.rawValue)
        }
        
        func applyMaterialOn(on part: String) {
            guard let entity = greeter.findEntity(named: part) else { fatalError("Couldn't find Greeter \(part)")}
            guard entity.components[ModelComponent.self] != nil else { fatalError("Model entity is required for \(part)") }
            entity.components[ModelComponent.self]!.materials = [material]
        }
    }

Solution

  • Retrieving the required ModelEntities

    Looking at your code, it's hard to say whether you take into account that some models in the scene have a child ModelEntity under the parent Entity or not. For example, look at a Reality Composer Pro's scene containing three primitives, one of which got a MaterialX assigned (sphere) and the other two have no material assigned (cylinder and cone).

    In our particular case, the Sphere contains a ModelComponent without a parent Entity (apparently this initial scene was created by Apple engineers from scratch in Python). The Cylinder and Cone primitives were added by me (in RCP) to the initial scene containing the Sphere. As you can see from the print result of scene's hierarchy, the ModelEntities of cylinder and cone bear identical names usdPrimitiveAxis.

    There's no need to programmatically assign your MaterialX to non-model entities because you can easily find all the model entities in scene hierarchy. To do this, use the following code (pay attention to ModelEntity's type casting).

    import SwiftUI
    import RealityKit
    import RealityKitContent
    
    struct ContentView: View {
        var body: some View {
            RealityView { rvc in
                if let scene = try? await Entity(named: "Scene", in: rkcb) {
                    rvc.add(scene)
                    print(scene)            // Scene hierarchy
                    
                    let sphere = scene.findEntity(named: "Sphere") as! ModelEntity
                    let matX = (sphere.model?.materials.last) ?? UnlitMaterial()
                    print(matX.name ?? "")  // GridMaterial
                    
                    let cylinder = scene.findEntity(named: "Cylinder")?.children[0] as! ModelEntity
                    cylinder.model?.materials = [matX]
                    
                    let cone = scene.findEntity(named: "Cone")?.children[0] as! ModelEntity
                    cone.model?.materials = [matX]
                }
            }
        }
    }
    

    So, as you can see from the screenshot, we were able to assign the Industrial Light & Magic's MaterialX copied from the sphere model to the surfaces of the cylinder and cone primitives. Voila!

    Traversing all the descendants

    If you have a large and branched hierarchical structure of a RealityKit's scene, then you should create an extension for Entity that will help you traverse children entities to find models.

    import SwiftUI
    import RealityKit
    import RealityKitContent
    
    extension Entity {
        func enumeratedHierarchy(_ body: (Entity, UnsafeMutablePointer<Bool>) -> Void) {
            var stop = false
            
            func enumerate(_ body: (Entity, UnsafeMutablePointer<Bool>) -> ()) {
                guard !stop else { return }
                body(self, &stop)
                
                for child in children {
                    guard !stop else { return }
                    child.enumeratedHierarchy(body)
                }
            }
            enumerate(body)
        }
    }
    

    struct ContentView: View {
        var body: some View {
            RealityView { rvc in
                if let scene = try? await Entity(named: "Scene", in: rkcb) {
                    rvc.add(scene)
                                
                    let sphere = scene.findEntity(named: "Sphere") as! ModelEntity
                    let matX = (sphere.model?.materials.last) ?? UnlitMaterial()
                    scene.enumeratedHierarchy { (entity, _) in
                        if entity is ModelEntity {
                            (entity as! ModelEntity).model?.materials = [matX]
                        }
                    }
                }
            }
        }
    }