Im trying to create a functionality in ARKit where if the user taps on the modelEntity it changes its colour to lets say blue so it indicates that it's selected. But then if the user taps on another entity, the previously selected entity's material changes back to what it was before it was selected.
So i am able to change its colour with this code:
let selectedMaterial = SimpleMaterial(color: .link, isMetallic: false)
selectedEntity.model?.materials[0] = selectedMaterial
But how do I change it back after I 'deselect' it, when i select another modelEntity?
Because I've been trying to save it's material to a variable, but I'm having a problem with it, because lets say there are two modelEntites, A and B. When I tap the "A" entity it changes colour, then I tap on the "B" entity then "B" entity's colour changes and the "A" entity's material changes back to the original (like how it should be working), but when I tap again on the "A" entity the "B" entity's material goes back to the original but the "A" entity's colour doesn't change.
This is how I'm trying to make it work:
enum EntityState {
case unselected
case selected
case correctName
case wrongName
}
private var entitiesState: [String: EntityState] = [String: EntityState]()
private var modelEntities: [ModelEntity] = [ModelEntity]()
private var modelEntitiesMaterials: [String: [Material]] = [String: [Material]]()
//This is how i place a modelEntity
@objc private func placeObject() {
let modelName = self.model.name ?? ""
let entity = try! Entity.load(named: modelName)
let geomChildrens = entity.findEntity(named: "Geom")
if let geomChildrens = geomChildrens {
for children in geomChildrens.children {
let childModelEntity = children as! ModelEntity
childModelEntity.collision = CollisionComponent(shapes: [ShapeResource.generateConvex(from: childModelEntity.model!.mesh)])
entitiesState[childModelEntity.name] = EntityState.unselected
modelEntities.append(childModelEntity)
}
}
let modelEntity = ModelEntity()
modelEntity.addChild(entity)
let anchorEntity = AnchorEntity(.plane(.horizontal, classification: .any, minimumBounds: .zero))
anchorEntity.addChild(modelEntity)
arView.installGestures([.all],for: modelEntity)
arView.scene.addAnchor(anchorEntity)
}
private func selectEntity(withSelectedEntity: ModelEntity?) {
modelInformationView.isHidden = false
//If we didnt hit any modelEntity
guard let selectedEntity = withSelectedEntity else {
//Unselect the selected entity if there is one.
for entity in modelEntities {
if(entitiesState[entity.name] == .selected) {
entitiesState[entity.name] = .unselected
}
}
colorModelEntities()
return
}
if(entitiesState[selectedEntity.name] == .selected) {
// If its already selected, just unselect
entitiesState[selectedEntity.name] = .unselected
} else {
//First unselect the previously selected entity.
for entity in modelEntities {
if(entitiesState[entity.name] == .selected) {
entitiesState[entity.name] = .unselected
}
}
//Select the entity.
entitiesState[selectedEntity.name] = .selected
}
colorModelEntities()
}
private func colorModelEntities() {
let selectedMaterial = SimpleMaterial(color: .link, isMetallic: false) //Blue
for entity in modelEntities {
let keyExists = modelEntitiesMaterials[entity.name] != nil
if keyExists {
entity.model!.materials = modelEntitiesMaterials[entity.name]!
}
if(entitiesState[entity.name] == .selected) {
//Color blue the selected item
entity.model?.materials[0] = selectedMaterial
}
}
}
@objc private func handleTap(sender: UITapGestureRecognizer) {
let tapLocation: CGPoint = sender.location(in: arView)
let result: [CollisionCastHit] = arView.hitTest(tapLocation)
guard let hitTest: CollisionCastHit = result.first, hitTest.entity.name != "Ground Plane"
else {
selectEntity(withSelectedEntity: nil)
return
}
let entity: ModelEntity = hitTest.entity as! ModelEntity
let keyExists = modelEntitiesMaterials[entity.name] != nil
if !keyExists {
modelEntitiesMaterials[entity.name] = entity.model!.materials
}
selectEntity(withSelectedEntity: entity)
}
🥧 It's easy as Apple pie 🥧
I used a regular single box scene
built in Reality Composer.
import UIKit
import RealityKit
class GameViewController: UIViewController {
@IBOutlet var arView: ARView!
var me: ModelEntity? = nil
var counter = 0
override func viewDidLoad() {
super.viewDidLoad()
let scene = try! Experience.loadBox()
print(scene)
me = scene.findEntity(named: "simpBld_root") as? ModelEntity
me?.model?.materials[0] = UnlitMaterial(color: .red)
me?.generateCollisionShapes(recursive: true)
arView.scene.anchors.append(scene)
}
override func touchesBegan(_ touches: Set<UITouch>,
with event: UIEvent?) {
guard let point = touches.first?.location(in: arView) else { return }
let ray = arView.ray(through: point)
let castHits = arView.scene.raycast(origin: ray?.origin ?? [0,0,0],
direction: ray?.direction ?? [0,0,0])
if castHits.first != nil {
counter += 1
if counter % 2 == 1 {
me?.model?.materials[0] = UnlitMaterial(color: .green)
} else {
me?.model?.materials[0] = UnlitMaterial(color: .red)
}
}
}
}
Here I used three boxes scene
built in Reality Composer (I merely copy-pasted original box).
import UIKit
import RealityKit
class GameViewController: UIViewController {
@IBOutlet var arView: ARView!
typealias ME = ModelEntity
var me: [ModelEntity] = [ME(),ME(),ME()]
override func viewDidLoad() {
super.viewDidLoad()
let scene = try! Experience.loadBox()
for ind in 0...2 {
me[ind] = scene.children[0].children[0]
.children[ind].children[0] as! ModelEntity
me[ind].model?.materials[0] = UnlitMaterial(color: .red)
me[ind].generateCollisionShapes(recursive: true)
me[ind].name = "box \(ind + 1)"
}
arView.scene.anchors.append(scene)
print(scene)
}
override func touchesBegan(_ touches: Set<UITouch>,
with event: UIEvent?) {
guard let point = touches.first?.location(in: arView) else { return }
let ray = arView.ray(through: point)
let castHits = arView.scene.raycast(origin: ray?.origin ?? [0,0,0],
direction: ray?.direction ?? [0,0,0])
guard let name = castHits.first?.entity.name else { return }
if castHits.first != nil {
switch name {
case "box 1": changeTo(0, .green);
changeTo(1, .red); changeTo(2, .red)
case "box 2": changeTo(1, .green);
changeTo(0, .red); changeTo(2, .red)
case "box 3": changeTo(2, .green);
changeTo(0, .red); changeTo(1, .red)
default: break
}
print(name)
}
}
func changeTo(_ element: Int, _ color: UIColor) {
me[element].model?.materials[0] = UnlitMaterial(color: color)
}
}
This answer might be helpful as well.