I have an interactive globe and I want to detect taps on the globe, so I can get their 3d position.
When I override the function public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?), I am able to detect when a gesture starts, and from the touches' parameter I can get the 3d position of the tap on the globe.
The problem is this function executes from any gesture (drag, magnification, etc.), but I only want to do something on a click or tap.
I could add a tap gesture to the scene view, but the function called from a tap gesture does not give me the 3d position of the tap within the globe.
Is there any other function I can override to detect only taps while getting touches: Set?
If not, is there any way I can filter out the touches to only tap gestures?
import Foundation
import SceneKit
import CoreImage
import SwiftUI
import MapKit
public typealias GenericController = UIViewController
public class GlobeViewController: GenericController {
var nodePos: CGPoint? = nil
public var earthNode: SCNNode!
private var sceneView : SCNView!
private var cameraNode: SCNNode!
private var dotCount = 50000
public init(earthRadius: Double) {
self.earthRadius = earthRadius
super.init(nibName: nil, bundle: nil)
}
public init(earthRadius: Double, dotCount: Int) {
self.earthRadius = earthRadius
self.dotCount = dotCount
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first, touch.view == self.sceneView else {
return
}
let touchLocation = touch.location(in: sceneView)
let hitTestResults = sceneView.hitTest(touchLocation, options: nil)
let touchLocation3D = sceneView.unprojectPoint(SCNVector3(Float(touchLocation.x), Float(touchLocation.y), 0.0))
print(touchLocation3D)
if let tappedNode = hitTestResults.first?.node {
print(tappedNode)
// Handle the tapped node
if tappedNode.name == "NewYorkDot" {
// This is the New York dot, perform your action here
print("Tapped on New York! Position: \(tappedNode.position)")
} else if tappedNode.name == "RegularDot" {
// Handle other nodes if needed
print("Tapped on a regular dot. Position: \(tappedNode.position)")
}
}
}
public override func viewDidLoad() {
super.viewDidLoad()
setupScene()
setupParticles()
setupCamera()
setupGlobe()
setupDotGeometry()
}
private func setupScene() {
let scene = SCNScene()
sceneView = SCNView(frame: view.frame)
sceneView.scene = scene
sceneView.showsStatistics = true
sceneView.backgroundColor = .clear
sceneView.allowsCameraControl = true
sceneView.isUserInteractionEnabled = true
self.view.addSubview(sceneView)
}
private func setupParticles() {
guard let stars = SCNParticleSystem(named: "StarsParticles.scnp", inDirectory: nil) else { return }
stars.isLightingEnabled = false
if sceneView != nil {
sceneView.scene?.rootNode.addParticleSystem(stars)
}
}
private func setupCamera() {
self.cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 5)
sceneView.scene?.rootNode.addChildNode(cameraNode)
}
private func setupGlobe() {
self.earthNode = EarthNode(radius: earthRadius, earthColor: earthColor, earthGlow: glowColor, earthReflection: reflectionColor)
sceneView.scene?.rootNode.addChildNode(earthNode)
}
private func setupDotGeometry() {
let textureMap = generateTextureMap(dots: dotCount, sphereRadius: CGFloat(earthRadius))
let newYork = CLLocationCoordinate2D(latitude: 44.0682, longitude: -121.3153)
let newYorkDot = closestDotPosition(to: newYork, in: textureMap)
let dotColor = GenericColor(white: 1, alpha: 1)
let oceanColor = GenericColor(cgColor: UIColor.systemRed.cgColor)
let highlightColor = GenericColor(cgColor: UIColor.systemRed.cgColor)
// threshold to determine if the pixel in the earth-dark.jpg represents terrain (0.03 represents rgb(7.65,7.65,7.65), which is almost black)
let threshold: CGFloat = 0.03
let dotGeometry = SCNSphere(radius: dotRadius)
dotGeometry.firstMaterial?.diffuse.contents = dotColor
dotGeometry.firstMaterial?.lightingModel = SCNMaterial.LightingModel.constant
let highlightGeometry = SCNSphere(radius: dotRadius)
highlightGeometry.firstMaterial?.diffuse.contents = highlightColor
highlightGeometry.firstMaterial?.lightingModel = SCNMaterial.LightingModel.constant
let oceanGeometry = SCNSphere(radius: dotRadius)
oceanGeometry.firstMaterial?.diffuse.contents = oceanColor
oceanGeometry.firstMaterial?.lightingModel = SCNMaterial.LightingModel.constant
var positions = [SCNVector3]()
var dotNodes = [SCNNode]()
var highlightedNode: SCNNode? = nil
for i in 0...textureMap.count - 1 {
let u = textureMap[i].x
let v = textureMap[i].y
let pixelColor = self.getPixelColor(x: Int(u), y: Int(v))
let isHighlight = u == newYorkDot.x && v == newYorkDot.y
if (isHighlight) {
let dotNode = SCNNode(geometry: highlightGeometry)
dotNode.name = "NewYorkDot"
dotNode.position = textureMap[i].position
positions.append(dotNode.position)
dotNodes.append(dotNode)
print("myloc \(textureMap[i].position)")
highlightedNode = dotNode
} else if (pixelColor.red < threshold && pixelColor.green < threshold && pixelColor.blue < threshold) {
let dotNode = SCNNode(geometry: dotGeometry)
dotNode.name = "Other"
dotNode.position = textureMap[i].position
positions.append(dotNode.position)
dotNodes.append(dotNode)
}
}
DispatchQueue.main.async {
let dotPositions = positions as NSArray
let dotIndices = NSArray()
let source = SCNGeometrySource(vertices: dotPositions as! [SCNVector3])
let element = SCNGeometryElement(indices: dotIndices as! [Int32], primitiveType: .point)
let pointCloud = SCNGeometry(sources: [source], elements: [element])
let pointCloudNode = SCNNode(geometry: pointCloud)
for dotNode in dotNodes {
pointCloudNode.addChildNode(dotNode)
}
self.sceneView.scene?.rootNode.addChildNode(pointCloudNode)
}
}
}
typealias GenericControllerRepresentable = UIViewControllerRepresentable
@available(iOS 13.0, *)
private struct GlobeViewControllerRepresentable: GenericControllerRepresentable {
var particles: SCNParticleSystem? = nil
func makeUIViewController(context: Context) -> GlobeViewController {
let globeController = GlobeViewController(earthRadius: 1.0)
updateGlobeController(globeController)
return globeController
}
func updateUIViewController(_ uiViewController: GlobeViewController, context: Context) { }
}
@available(iOS 13.0, *)
public struct GlobeView: View {
public var body: some View {
GlobeViewControllerRepresentable()
}
}
Using ColdLogic's advice, using a
UITapGestureRecognizerfor detecting tap gestures in SceneKit could indeed be the recommended approach for your scenario.You attach a
UITapGestureRecognizerto theSCNView. That recognizer is specifically for detecting tap gestures.In the
handleTapmethod, you perform a hit test using the tap location. That is to find the 3D coordinates of the tap on the globe.The hit test results give you the 3D position, which is what you are interested in for your interactive globe.
That way, you would not have to parse touch events manually with
touchesBegan, which can be more complex and less efficient for this specific use case.Use the hit test result to get the 3D position of the tap on the globe: your
GlobeViewControllerclass would be:UITapGestureRecognizeris used to detect taps on thesceneView.handleTapis triggered when a tap is recognized. It performs a hit test at the tap location and gets the first result.hitResult.worldCoordinatesgives the 3D coordinates of the tap on the globe.That should detect taps specifically, and get their 3D position without being triggered by other gestures like drag or magnification.
To expand on the
handleTapfunction, the hit test results can include multiple hits if there are several objects at the tap location. You can iterate over these results to find the desired node or the first hit, depending on the application's needs.The
forEachloop iterates over each hit test result.result.worldCoordinatesprovides the 3D coordinates of the hit location.You can also access the
nodeproperty of each hit result to perform node-specific actions.