iOS 使用 SceneKit 實現全景圖

weixin_33797791發表於2018-05-02

專案裡碰到了展示全景圖的需求,以前沒做過,google了一下已經有不少現成的庫可以拿來用了,不過有前輩提到可以用SceneKit來實現,剛好去年做AR的時候用過SceneKit,一下子來了思路,在這裡記錄一下。

思路

思路就是在SceneKit的場景裡放置一個球體,把全景圖貼在球體表明,然後把相機放置在球體中心不就得了。

實現

實現起來很簡單,往 ViewController 放一個 SCNView,一個相機,一個球體就好。

class PanoramaVC: UIViewController {
    
    let sceneView = SCNView()
    // 相機Node
    let cameraNode = SCNNode()
    // 球體
    let sphere = SCNSphere()
    
    ...
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        sceneView.scene = SCNScene.init()
        sceneView.frame = self.view.frame
        self.addSubView(sceneView)
        
        sphere.radius = 50
        // 只渲染一面,從球體裡面看,外面就不用渲染了
        sphere.firstMaterial?.isDoubleSided = false
        // 剔除外面
        sphere.firstMaterial?.cullMode = .front
        // 把全景圖“貼”到球體上
        sphere.firstMaterial?.diffuse.contents = UIImage.init(named: "全景圖名")
        
        // 球體Node,位置放到場景原點
        let sphereNode = SCNNode.init(geometry: sphere)
        sphereNode.position = SCNVector3Make(0, 0, 0)
        sceneView.scene?.rootNode.addChildNode(sphereNode)
        
        // 相機Node,位置放到場景原點
        cameraNode.camera = SCNCamera.init()
        cameraNode.position = SCNVector3Make(0, 0, 0)
        sceneView.scene?.rootNode.addChildNode(cameraNode)
    }
}
複製程式碼

到這裡就成功了。但是這樣有個問題:全景圖左右是反的。這是因為全景圖是從球體外面“貼”到球體上的,所以從球體內部看到的全景圖是左右反向的。那我就把全圖片給它左右反向就好啦。使用下面這個函式可以把圖片左右反向。

func flipImageLeftRight(_ image: UIImage) -> UIImage? {
    UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale)
    let context = UIGraphicsGetCurrentContext()!
    context.translateBy(x: image.size.width, y: image.size.height)
    context.scaleBy(x: -image.scale, y: -image.scale)
    context.draw(image.cgImage!, in: CGRect(origin:CGPoint.zero, size: image.size))
    let newImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return newImage
}
複製程式碼

此時把左右反向的圖片“貼”到球體上

sphere.firstMaterial?.diffuse.contents = flipImageLeftRight(UIImage.init(named: "全景圖名"))
複製程式碼

全景圖長這樣:

快看下效果吧(實際還是很流暢的,gif 效果有卡頓):

懸浮廣告

除了全景圖,還有個需求就是在全景圖適當的位置顯示廣告圖示,點選後會出現一張海報。要求廣告圖示在全景圖視角變化的時候跟著動。

這個其實也不難,只要知道需要放置廣告的位置座標,在全景圖視角變化的同時移動圖示讓圖示和目標位置同步就好。

但是我怎麼知道全景圖視角怎樣變化呢(其實是相機Node旋轉角度在變化)?我想通過相機旋轉時監聽旋轉角度但是沒找到監聽方法,如果有誰知道望告知,十分感謝!

那我乾脆自己控制相機旋轉角度。使用這句程式碼禁止通過 sceneView 來控制相機旋轉:

sceneView.allowsCameraControl = false
複製程式碼

然後監聽滑動手勢來控制相機和廣告圖示旋轉:

//相機旋轉角度(單位是弧度,轉一圈是 2pi)
var rotateAngle = CGPoint.zero
...

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    if touches.count == 1 {
        let touch = touches.first
        // 手指在x、y方向滑動距離
        let offsetX = (touch?.location(in: self.view).x)! - (touch?.previousLocation(in: self.view).x)!
        let offsetY = (touch?.location(in: self.view).y)! - (touch?.previousLocation(in: self.view).y)!
        // 手指滑動的距離和相機旋轉弧度比值約等於800(這個值是我測試得到的可能不精確)
        rotateAngle.x += offsetX / 800
        rotateAngle.y += offsetY / 800
        // 把rotateAngle.x按2?取模
        rotateAngle.x = rotateAngle.x.truncatingRemainder(dividingBy: CGFloat.pi * 2)
        
        // 控制全景圖上下旋轉不顛倒
        if rotateAngle.y > 1 {
            rotateAngle.y = 1
        }
        if rotateAngle.y < -1 {
            rotateAngle.y = -1
        }
        
        // 旋轉相機
        rotateCamera()
        // 旋轉廣告
        rotateAdvertisement()
    }
}

/// 旋轉相機
func rotateCamera() {
    let rotation = SCNAction.rotateTo(x: rotateAngle.y, y: rotateAngle.x, z: 0, duration: 0)
    cameraNode.runAction(rotation)
}
複製程式碼

要旋轉廣告圖示就要先新增一個廣告圖示,這裡使用一個 UIButton 作為示例。我想把廣告圖示放在右邊的獅子頭上,而獅子頭的位置的x、y座標與圖片寬高比值為0.570和0.456。

... 
var adPosition = CGPoint.init(x: 0.570, y: 0.456)
var adButton = UIButton()
    
func addAdversisement() {
    adButton.setTitle("我是廣告圖示", for: .normal)
    adButton.backgroundColor = UIColor.init(white: 1, alpha: 0.5)
    adButton.layer.cornerRadius = 10
    adButton.frame = CGRect.init(origin: CGPoint.zero, size: CGSize.init(width: 80, height: 40))
    self.view.addSubview(adButton)
    // 把圖示放在螢幕上對應的位置
    adButton.center = positionInScreenOfAd(position: adPosition)
}

/// 從廣告圖示在圖片上的位置計算出在螢幕上的位置
///
/// - Parameter position: 圖片上的位置
/// - Returns: 螢幕上的位置
func positionInScreenOfAd(position: CGPoint) -> CGPoint {
    //廣告點相對於圖片原點的偏移角度
    let diffToOrigin = CGPoint.init(x: position.x * CGFloat.pi, y: (position.y - 0.5) * CGFloat.pi)
    //轉動過程中,廣告點相對於螢幕中心點的偏移角度
    var offsetAngle = CGPoint.init(x: diffToOrigin.x + rotateAngle.x / 2, y: diffToOrigin.y + rotateAngle.y)
    offsetAngle.x = offsetAngle.x.truncatingRemainder(dividingBy: CGFloat.pi)

    //在顯示範圍內,就按角度比例顯示在螢幕上
    let viewSize = self.view.frame.size
    
    // 全景圖顯示在螢幕上的範圍為 -0.20453 < x < 0.20453, -0.53996 < y < 0.53996(單位為弧度),超出範圍就不顯示,其中 x 值正負要分開處理
    //(這個值是我測試得到的可能不精確)
    if abs(offsetAngle.y) > 0.53996 {
        return CGPoint.zero
    }
    if abs(offsetAngle.x) < 0.20453 {
        return CGPoint.init(x: viewSize.width / 2 * (1 + offsetAngle.x / 0.20453), y: viewSize.height / 2 * (1 + offsetAngle.y / 0.53996))
    }
    if abs(offsetAngle.x) >= CGFloat.pi - 0.20453 && abs(offsetAngle.x) < CGFloat.pi {
        return CGPoint.init(x: viewSize.width / 2 * (1 + (abs(offsetAngle.x) - CGFloat.pi) / 0.20453), y: viewSize.height / 2 * (1 + offsetAngle.y / 0.53996))
    }
    return CGPoint.zero
}
複製程式碼

接下來就是旋轉廣告圖示:

/// 旋轉廣告
func rotateAdvertisement() {
    // 實時計算旋轉過程中廣告圖示在螢幕上的位置
    let newP = positionInScreenOfAd(position: adPosition)
    // newP == CGPoint.zero 表示圖示位置在螢幕之外
    adButton.isHidden = newP == CGPoint.zero
    adButton.center = positionInScreenOfAd(position: adPosition)
}
複製程式碼

到這裡就可以看到廣告圖示隨全景圖滑動了:

大功告成!

相關文章