I have an interactive globe where countries are represented by dots, and I have a function that calculates the position of each dot. This function then adds these dots (SCNNodes) to the rootNode (earthNode) of the scene.
Instead of modifying the dots when I set up the globe, how can I modify their geometry after they have been added to the scene?
I have been having trouble accessing dots from within the scene.
I'd like the modification functions to take a SCNVector3 input and find the closest dot to that region and modify its geometry.
I already have a function that can get the closet dot. I just don't know how to access the node in the scene.
import Foundation
import SceneKit
import CoreImage
import SwiftUI
import MapKit
import Combine
public class GlobeViewController: GenericController {
var viewModel: GlobeViewModel
public var earthNode: SCNNode!
internal var sceneView : SCNView!
private var cameraNode: SCNNode!
private var textureMap: [MapDot]? = nil
var earthRadius: Double = 1.0
public var dotSize: CGFloat = 0.005 {
didSet {
if dotSize != oldValue {
setupDotGeometry()
}
}
}
init() {
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
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 = .black
sceneView.allowsCameraControl = true
sceneView.isUserInteractionEnabled = true
self.view.addSubview(sceneView)
}
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)
let interiorRadius: CGFloat = earthRadius * 0.9
let interiorSphere = SCNSphere(radius: interiorRadius)
let interiorNode = SCNNode(geometry: interiorSphere)
interiorNode.geometry?.firstMaterial?.diffuse.contents = UIColor.black
interiorNode.geometry?.firstMaterial?.isDoubleSided = true
earthNode.addChildNode(interiorNode)
sceneView.scene?.rootNode.addChildNode(earthNode)
}
func modifyDotGeometry(){
}
private func setupDotGeometry() {
self.textureMap = generateTextureMap(dots: dotCount, sphereRadius: CGFloat(earthRadius))
if let textureMap = self.textureMap {
let newYork = CLLocationCoordinate2D(latitude: 44.0682, longitude: -121.3153)
let newYorkDot = closestDotPosition(to: newYork, in: textureMap)
let threshold: CGFloat = 0.03
let dotColor = GenericColor(white: 1, alpha: 1)
let dotGeometry = SCNSphere(radius: dotRadius)
dotGeometry.firstMaterial?.diffuse.contents = dotColor
dotGeometry.firstMaterial?.lightingModel = SCNMaterial.LightingModel.constant
let oceanColor = GenericColor(cgColor: UIColor.systemRed.cgColor)
let oceanGeometry = SCNSphere(radius: dotRadius)
oceanGeometry.firstMaterial?.diffuse.contents = oceanColor
oceanGeometry.firstMaterial?.lightingModel = SCNMaterial.LightingModel.constant
var positions = [SCNVector3]()
var dotNodes = [SCNNode]()
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 lowerCircle = SCNSphere(radius: dotRadius * 5)
lowerCircle.firstMaterial?.diffuse.contents = GenericColor(cgColor: UIColor.white.cgColor)
lowerCircle.firstMaterial?.lightingModel = SCNMaterial.LightingModel.constant
let dotNode = SCNNode(geometry: lowerCircle)
dotNode.name = "NewYorkDot"
dotNode.position = textureMap[i].position
positions.append(dotNode.position)
dotNodes.append(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)
}
}
}
func equirectangularProjection(point: Point3D, imageWidth: Int, imageHeight: Int) -> Pixel {
let theta = asin(point.y)
let phi = atan2(point.x, point.z)
let u = Double(imageWidth) / (2.0 * .pi) * (phi + .pi)
let v = Double(imageHeight) / .pi * (.pi / 2.0 - theta)
return Pixel(u: Int(u), v: Int(v))
}
private func distanceBetweenPoints(x1: Int, y1: Int, x2: Int, y2: Int) -> Double {
let dx = Double(x2 - x1)
let dy = Double(y2 - y1)
return sqrt(dx * dx + dy * dy)
}
private func closestDotPosition(to coordinate: CLLocationCoordinate2D, in positions: [(position: SCNVector3, x: Int, y: Int)]) -> (x: Int, y: Int) {
let pixelPositionDouble = getEquirectangularProjectionPosition(for: coordinate)
let pixelPosition = (x: Int(pixelPositionDouble.x), y: Int(pixelPositionDouble.y))
let nearestDotPosition = positions.min { p1, p2 in
distanceBetweenPoints(x1: pixelPosition.x, y1: pixelPosition.y, x2: p1.x, y2: p1.y) <
distanceBetweenPoints(x1: pixelPosition.x, y1: pixelPosition.y, x2: p2.x, y2: p2.y)
}
return (x: nearestDotPosition?.x ?? 0, y: nearestDotPosition?.y ?? 0)
}
/// Convert a coordinate to an (x, y) coordinate on the world map image
private func getEquirectangularProjectionPosition(
for coordinate: CLLocationCoordinate2D
) -> CGPoint {
let imageHeight = CGFloat(worldMapImage.height)
let imageWidth = CGFloat(worldMapImage.width)
// Normalize longitude to [0, 360). Longitude in MapKit is [-180, 180)
let normalizedLong = coordinate.longitude + 180
// Calculate x and y positions
let xPosition = (normalizedLong / 360) * imageWidth
// Note: Latitude starts from top, hence the `-` sign
let yPosition = (-(coordinate.latitude - 90) / 180) * imageHeight
return CGPoint(x: xPosition, y: yPosition)
}
}
To modify the geometry of dots (represented as
SCNNodes) already added to yourSCNNode(in this case,earthNode), you can access the child nodes ofearthNodeusing itschildNodesproperty. That property returns an array of all child nodes.EarthNodeis the root node containing all other nodes.CameraNodeis for the camera setup,InteriorNoderepresents an internal sphere, andDotNoderepresents individual dots (like "NewYorkDot") with their respective geometries.Use your existing function to find the closest dot to a given
SCNVector3position. That function should return either theSCNNodeitself or some identifier (like the name or position) that you can use to find the node withinearthNode's children.Once you have identified the target
SCNNode, you can modify its geometry. For example, if you want to change the size of the dot, you can adjust theradiusproperty of its geometry, assuming it is aSCNSphere.modifyDotGeometrytakes a position and a new radius as inputs. It uses a hypotheticalfindClosestDotfunction to determine the closest dot's identifier (you can replace this with your existing functionality) and then modifies the radius of the dot's geometry.Do replace the placeholder logic in
findClosestDotwith your actual logic for finding the closest dot. That could involve iterating overearthNode.childNodes, comparing their positions to the target position, and selecting the closest one.