Reputation: 38
Source: https://github.com/DominicHolmes/dot-globe
I am trying to make the 3d globe from the repo above lock about the z axis. I want the globe to only rotate horizontally and ignore unwanted rotations. If it is possible I'd like to allow up to -30 degrees of rotation to the bottom of the globe, and 30 degrees of rotation to the top of the globe.
I'm not very skilled with SCNScene or SCNCamera. Currently horizontal swipes also rotate the whole globe instead of spinning it.
In the repo the code below was added in the function setupCamera to prevent unwanted globe rotations. But this does not work.
constraint.isGimbalLockEnabled = true
cameraNode.constraints = [constraint]
sceneView.scene?.rootNode.addChildNode(cameraNode)
I also tried doing this but it also didn't work.
let constraint = SCNTransformConstraint.orientationConstraint(inWorldSpace: true) { (_, orientation) -> SCNQuaternion in
// Keep the same orientation around x and z axes, allow rotation around y-axis
return SCNQuaternion(x: 0, y: orientation.y, z: 0, w: orientation.w)
}
Here is the code to set up the camera (where these constraints should be added). The rest of the code is in the repository above. Everything relevant to this question and code is in the file linked above.
import SwiftUI
import SceneKit
typealias GenericControllerRepresentable = UIViewControllerRepresentable
@available(iOS 13.0, *)
private struct GlobeViewControllerRepresentable: GenericControllerRepresentable {
var particles: SCNParticleSystem? = nil
//@Binding public var showProf: Bool
func makeUIViewController(context: Context) -> GlobeViewController {
let globeController = GlobeViewController(earthRadius: 1.0)//, showProf: $showProf
updateGlobeController(globeController)
return globeController
}
func updateUIViewController(_ uiViewController: GlobeViewController, context: Context) {
updateGlobeController(uiViewController)
}
private func updateGlobeController(_ globeController: GlobeViewController) {
globeController.dotSize = CGFloat(0.005)
globeController.enablesParticles = true
if let particles = particles {
globeController.particles = particles
}
}
}
@available(iOS 13.0, *)
public struct GlobeView: View {
//@Binding public var showProf: Bool
public var body: some View {
GlobeViewControllerRepresentable()//showProf: $showProf
}
}
import Foundation
import SceneKit
import CoreImage
import SwiftUI
import MapKit
public typealias GenericController = UIViewController
public typealias GenericColor = UIColor
public typealias GenericImage = UIImage
public class GlobeViewController: GenericController {
public var earthNode: SCNNode!
private var sceneView : SCNView!
private var cameraNode: SCNNode!
private var worldMapImage : CGImage {
guard let path = Bundle.module.path(forResource: "earth-dark", ofType: "jpg") else { fatalError("Could not locate world map image.") }
guard let image = GenericImage(contentsOfFile: path)?.cgImage else { fatalError() }
return image
}
private lazy var imgData: CFData = {
guard let imgData = worldMapImage.dataProvider?.data else { fatalError("Could not fetch data from world map image.") }
return imgData
}()
public var particles: SCNParticleSystem? {
didSet {
if let particles = particles {
sceneView.scene?.rootNode.removeAllParticleSystems()
sceneView.scene?.rootNode.addParticleSystem(particles)
}
}
}
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 viewDidLoad() {
super.viewDidLoad()
setupScene()
setupParticles()
setupCamera()
setupGlobe()
setupDotGeometry()
}
private func setupScene() {
var scene = SCNScene()
sceneView = SCNView(frame: view.frame)
sceneView.scene = scene
sceneView.showsStatistics = true
sceneView.backgroundColor = .black
sceneView.allowsCameraControl = 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)
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 * 5)
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)
highlightedNode = dotNode
} else if (pixelColor.red < threshold && pixelColor.green < threshold && pixelColor.blue < threshold) {
let dotNode = SCNNode(geometry: dotGeometry)
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)
//this moves the camera to show the top of the earth
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
if let highlightedNode = highlightedNode {
self.alignPointToPositiveZ(for: pointCloudNode, targetPoint: highlightedNode.position)
}
}
}
}
func alignPointToPositiveZ(for sphereNode: SCNNode, targetPoint: SCNVector3) {
// Compute normalized vector from Earth's center to the target point
let targetDirection = targetPoint.normalized()
// Compute quaternion rotation
let up = SCNVector3(0, 0, 1)
let rotationQuaternion = SCNQuaternion.fromVectorRotate(from: up, to: targetDirection)
sphereNode.orientation = rotationQuaternion
}
typealias MapDot = (position: SCNVector3, x: Int, y: Int)
private func generateTextureMap(dots: Int, sphereRadius: CGFloat) -> [MapDot] {
let phi = Double.pi * (sqrt(5) - 1)
var positions = [MapDot]()
for i in 0..<dots {
let y = 1.0 - (Double(i) / Double(dots - 1)) * 2.0 // y is 1 to -1
let radiusY = sqrt(1 - y * y)
let theta = phi * Double(i) // Golden angle increment
let x = cos(theta) * radiusY
let z = sin(theta) * radiusY
let vector = SCNVector3(x: Float(sphereRadius * x),
y: Float(sphereRadius * y),
z: Float(sphereRadius * z))
let pixel = equirectangularProjection(point: Point3D(x: x, y: y, z: z),
imageWidth: 2048,
imageHeight: 1024)
let position = MapDot(position: vector, x: pixel.u, y: pixel.v)
positions.append(position)
}
return positions
}
struct Point3D {
let x: Double
let y: Double
let z: Double
}
struct Pixel {
let u: Int
let v: Int
}
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)
}
private func getPixelColor(x: Int, y: Int) -> (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
let data: UnsafePointer<UInt8> = CFDataGetBytePtr(imgData)
let pixelInfo: Int = ((worldMapWidth * y) + x) * 4
let r = CGFloat(data[pixelInfo]) / CGFloat(255.0)
let g = CGFloat(data[pixelInfo + 1]) / CGFloat(255.0)
let b = CGFloat(data[pixelInfo + 2]) / CGFloat(255.0)
let a = CGFloat(data[pixelInfo + 3]) / CGFloat(255.0)
return (r, g, b, a)
}
}
Upvotes: 2
Views: 237