swiftscenekithittest

Identify face of a cube hit on touches began in Swift - SceneKit


I'm tying to create an app with SceneKit to solve a Rubik's Cube. I've made my own dae file for the cube. Upon touches began I have the object that's been hit

  func tapGesture(sender: UITapGestureRecognizer){

    // check what nodes are tapped
    var p = sender.locationInView(sceneView)
    var hitResults = sceneView.hitTest(p, options: nil)
    if hitResults.count > 0
    {

        var hitnode = (hitResults.first)!.node
        print("\nName of node hit is \(hitnode.name)")
        
        //var indexvalue = hitResults.first?.faceIndex
        //print(indexvalue)
    }
  }

How can I find exactly which face of the cube is being hit?


Solution

  • faceIndex looks promising, but will not actually get something you're likely to consider useful. The "faces" counted by that property are the tessellation of the mesh, so a cube won't be a collection of six quads, it'll be twelve triangles. (Or more: in some cases, even a flat-sided cube will be tessellated with more than one quad / two triangles per side. If you're using SCNBox you control these with widthSegmentCount etc.)

    Instead — especially if your cube is an SCNBox — the easiest solution might be to leverage this interesting behavior of that class:

    You can assign up to six SCNMaterial instances to a box—one for each side—with its materials property. The SCNBox class automatically creates SCNGeometryElement objects as needed to handle the number of materials.

    So, if you assign six materials, you'll get one for each side:

    let front = SCNMaterial()
    let right = SCNMaterial()
    let back = SCNMaterial()
    let left = SCNMaterial()
    let top = SCNMaterial()
    let bottom = SCNMaterial()
    cube.materials = [ front, right, back, left, top, bottom ]
    

    And in so doing, your SCNBox will have six geometry elements — one for each material, which corresponds to one for each side.

    Now, you can use hit testing to find out which geometry element was clicked:

    if let result = hitResults.first {
        let node = result.node
    
        // Find the material for the clicked element
        // (Indices match between the geometryElements and materials arrays)
        let material = node.geometry!.materials[result.geometryIndex]
    
        // Do something with that material, for example:
        let highlight = CABasicAnimation(keyPath: "diffuse.contents")
        highlight.toValue = NSColor.redColor()
        highlight.duration = 1.0
        highlight.autoreverses = true
        highlight.removedOnCompletion = true
        material.addAnimation(highlight, forKey: nil)
    }
    

    Or if you're not highlighting and want to use the face index for logic, here's the beginning of something you could use for that:

    enum CubeFace: Int {
        case Front, Right, Back, Left, Top, Bottom
    }
    
    // when processing hit test result:
    print("hit face: \(CubeFace(rawValue: result.geometryIndex))")