I\'m tying to create an app with Scene kit to solve Rubix Cube. I\'ve my own dae file for the cube. Upon touches began I have the object that\'s been hit
f
We use this function to determine the face hit based on the localNormal value from the SCNHitResult
.
This function concludes a face has been hit if the magnitude of an axis is 1.
It assumes exactly one and only one axis will have a magnitude equal to 1. The code will break otherwise. It also assumes a SCNBox geometry.
In testing, this seems to work (for SCNBox geometries). The only complication is the localNormal
value doesn't always return clean 0 values. Sometimes it returns values like -5.96046448e-08 so we use the round
function to be safe in case the same applies to values near 1 but not exactly 1.
We are new to SceneKit and 3D in general, so the code could be flawed. Please comment if you spot issues or potential optimizations.
private func getHitFaceFromNormal(normal: SCNVector3) {
if round(normal.x) == -1 {
// Left face hit
} else if round(normal.x) == 1 {
// Right face hit
} else if round(normal.y) == -1 {
// Bottom face hit
} else if round(normal.y) == 1 {
// Top face hit
} else if round(normal.z) == -1 {
// Back face hit
} else if round(normal.z) == 1 {
// Front face hit
} else {
// Error, no face detected
}
}
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 itsmaterials
property. TheSCNBox
class automatically createsSCNGeometryElement
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))")