在相机动作之前快速防止场景视角重置

问题描述 投票:0回答:1

我有一个用 SceneKit 构建的 3D 交互式地球仪,其中国家/地区用点表示。下面的函数会获取一个位置并将相机动画移动到该位置。

如果用户不与地球仪交互,那么我可以连续调用该函数并将相机设置为新位置。

但是,如果用户在场景上执行任何手势,则相机动画将不起作用。

在不同的 SO 线程(下面链接)中找到的解决方案在函数开头使用了行

sceneView.pointOfView = cameraNode

这确实解决了手势后相机没有动画的问题。

但是,这条线会导致地球仪在设置动画之前重置到其原始位置。我一直在试图找到一种方法来绕过这个场景重置,但没有运气。

我假设在地球上执行手势会为场景创建一个新的视角并覆盖相机的视角。因此,在动画之前将场景的视点设置回相机可以解决该问题。

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

    func centerCameraOnDot(dotPosition: SCNVector3) {
        sceneView.pointOfView = cameraNode //HERE RESETS
        
        let fixedDistance: Float = 5.0
        let newCameraPosition = dotPosition.normalized().scaled(to: fixedDistance)

        let moveAction = SCNAction.move(to: newCameraPosition, duration: 1.5)

        let constraint = SCNLookAtConstraint(target: earthNode)
        constraint.isGimbalLockEnabled = true

        sceneView.gestureRecognizers?.forEach { $0.isEnabled = false }
        
        SCNTransaction.begin()
        SCNTransaction.animationDuration = 1.5

        self.cameraNode.constraints = [constraint]
        self.cameraNode.runAction(moveAction) {
            DispatchQueue.main.async {
                self.sceneView.gestureRecognizers?.forEach { $0.isEnabled = true }
            }
        }
        SCNTransaction.commit()
    }

    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)
            
            //performing gestures before this causes the bug
            DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
                if let highlightedNode = highlightedNode {
                    self.centerCameraOnDot(dotPosition: highlightedNode.position)
                }
            }
        }
   }
}
ios swift swiftui scene scnnode
1个回答
0
投票

当设置

sceneView.pointOfView
时,相机的位置和方向立即更改为
pointOfView
节点的变换,这会导致观察到的重置。

尝试并保留当前相机变换:在设置

sceneView.pointOfView = cameraNode
之前,存储当前相机变换。这包括它的位置、旋转以及与场景设置相关的任何其他属性。
然后,在设置视点后,将存储的变换重新应用到相机。这应该会抵消重置效果并保持用户所看到的场景的连续性。

您的

centerCameraOnDot
功能将是:

func centerCameraOnDot(dotPosition: SCNVector3) {
    // Store the current camera transformation
    let currentCameraTransform = cameraNode.transform

    // Set the point of view to the camera node
    sceneView.pointOfView = cameraNode

    // Reapply the stored transformation
    cameraNode.transform = currentCameraTransform

    // Rest of your animation code
}

看看这是否有助于将相机转换到新的视角,而无需重置地球仪的位置。


这并没有解决问题。

替代方法:更新相机节点而不改变

pointOfView

您可以尝试根据用户交互来更新

pointOfView
的位置和方向,而不是直接操作
sceneView
cameraNode
属性。该方法涉及拦截用户手势并手动将其转换应用于
cameraNode
。以下是如何实现这一点的概述:

将自定义手势识别器添加到

sceneView
或利用 SceneKit 的默认手势处理来检测用户交互。
当检测到用户交互时,计算必要的转换并将其应用于
cameraNode
。这使
cameraNode
与用户的视角保持同步。
将相机移动到新位置时,直接为
cameraNode
的位置和方向设置动画,而不是使用
sceneView.pointOfView

可能看起来像:

override func viewDidLoad() {
    super.viewDidLoad()
    setupGestureRecognizers()
    // Other setup code 
}

private func setupGestureRecognizers() {
    let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
    sceneView.addGestureRecognizer(panGesture)
    // Add other gestures as needed
}

@objc func handlePanGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
    // Calculate the transformation based on the gesture
    // Apply the transformation to the cameraNode
    // Rest of the code
}

func centerCameraOnDot(dotPosition: SCNVector3) {
    // Directly animate cameraNode to new position
    // No need to alter sceneView.pointOfView
    // Rest of the code
}

该方法需要更多地手动处理相机转换,但可以更好地控制相机响应用户交互的行为。它还避免了更改

pointOfView
时重置地球仪位置的问题。

还尝试添加日志来跟踪用户交互前后以及动画到新位置时相机的位置和方向。这可以帮助识别意外的变化。 并使用 SceneKit 的调试工具,例如显示统计信息或 SCNView 的调试选项,以更好地了解场景的状态。
分别测试手势处理和相机动画代码的每个部分,以找出问题的原因。

© www.soinside.com 2019 - 2024. All rights reserved.