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]
}
}
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!
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]
}
}
}
}
}
}