Swift Modify SCNode children

67 views Asked by At

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

There are 1 answers

0
VonC On BEST ANSWER

To modify the geometry of dots (represented as SCNNodes) already added to your SCNNode (in this case, earthNode), you can access the child nodes of earthNode using its childNodes property. That property returns an array of all child nodes.

+ EarthNode (rootNode)
  |
  +-- CameraNode
  |
  +-- InteriorNode
  |   |
  |   +-- SphereGeometry
  |
  +-- DotNode (e.g., "NewYorkDot")
      |
      +-- SCNSphere

EarthNode is the root node containing all other nodes. CameraNode is for the camera setup, InteriorNode represents an internal sphere, and DotNode represents individual dots (like "NewYorkDot") with their respective geometries.

Use your existing function to find the closest dot to a given SCNVector3 position. That function should return either the SCNNode itself or some identifier (like the name or position) that you can use to find the node within earthNode'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 the radius property of its geometry, assuming it is a SCNSphere.

func modifyDotGeometry(closestTo position: SCNVector3, newRadius: CGFloat) {
    // Find the closest dot to the given position
    let closestDot = findClosestDot(to: position)

    // Assuming closestDot is the name of the node
    let dotNode = earthNode.childNode(withName: closestDot, recursively: true)
    
    // Modify the geometry if the node is found and is a SCNSphere
    if let dotNode = dotNode, let sphereGeometry = dotNode.geometry as? SCNSphere {
        sphereGeometry.radius = newRadius
    }
}

func findClosestDot(to position: SCNVector3) -> String {
    // Implement logic to find the closest dot
    // That should return the name or some identifier of the closest dot node
    // 
}

modifyDotGeometry takes a position and a new radius as inputs. It uses a hypothetical findClosestDot function 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 findClosestDot with your actual logic for finding the closest dot. That could involve iterating over earthNode.childNodes, comparing their positions to the target position, and selecting the closest one.