How to change model's color on tap and then change it back to original?
Asked Answered
C

1

2

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)
    }

Cunningham answered 17/12, 2022 at 22:53 Comment(0)
I
1

Solution for a single model

🥧 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)
            }
        }
    }
}


Solution for multiple models

Here I used three boxes scene built in Reality Composer (I merely copy-pasted original box).

enter image description here

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)
    }
}

enter image description here

This answer might be helpful as well.

Incomprehension answered 18/12, 2022 at 10:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.