使用SceneKit編寫VR全景播放器

最近用SceneKit做了全景看房的功能,現(xiàn)總結(jié)下如何實現(xiàn)的。
先看下最終的效果:

gif1.gif

VR圖片全景播放器有以下功能:

  • 360度
  • 手勢滑動,縮放
  • 陀螺儀
  • 分屏(VR眼鏡)
  • 熱點hotpot
  • 頭控/eyepick

手勢滑動,縮放,陀螺儀功能都是調(diào)節(jié)球面圖片顯示的位置;
熱點和頭控功能本質(zhì)是一樣的,都是在原有模型上增加3維的視圖。它們用途不一樣,頭控功能(全景圖片一般就是eyepick功能)一般是戴VR眼鏡后,通過模型的位置觸發(fā)控制事件。
展示全景圖的原理很簡單:將圖片渲染至球體模型內(nèi)表面上,手機處于球體中心(圖中紅色區(qū)域),當旋轉(zhuǎn)手機的時候,
球體向相反的方向旋轉(zhuǎn),這樣我們就可以看到球體上的畫面了。

怎么將圖片繪制于球體上呢?

這需要使用openGL這個框架,openGL渲染球體圖片步驟大致如下:

  1. 生成頂點數(shù)據(jù),也就是球面上點坐標數(shù)據(jù)。頂點越多生成的球體越平滑,但也有極限,當頂點大于一定值的時候再多的頂點也看不出差別來反而會影響性能。
頂點數(shù)據(jù)
  1. 生成紋理數(shù)據(jù),也就是圖片的顏色緩存數(shù)據(jù)。
  2. 著色器將顏色數(shù)據(jù)渲染至頂點上。

全景播放器第三方庫

  • MD360Player4iOS:支持全景圖片/視頻,有分屏/陀螺儀/手勢移動功能,但沒有熱點及頭控功能;

  • Panorama:只支持全景圖片,比較輕量。也只有分屏/陀螺儀/手勢功能;

  • PanoramaGL:只支持全景圖片,具有陀螺儀/手勢/熱點功能,但這個庫比較久遠仍是MRC,沒人維護;

  • 得圖SDK:支持全景圖片/視頻,也只有分屏/陀螺儀/手勢移動功能

現(xiàn)在主流的和全景圖片有關(guān)的三方庫,基本上都沒有熱點及頭控功能;之前有試過在MD360Player4iOS基礎(chǔ)上增加這兩個功能,但因為自己openGL零基礎(chǔ)后來還是暫時放棄了。
后來發(fā)現(xiàn)系統(tǒng)SceneKit框架也可以實現(xiàn)以上所有功能,使用起來也非常簡單。接下來我們來了解下SceneKit,看如何實現(xiàn)全景播放功能。

SceneKit

(全景視頻播放器需使用SpriteKit,這里主要先介紹圖片播放器,之后再講視頻播放器)
SceneKit是什么?

SceneKit is a high-level 3D graphics framework that helps you create 3D animated scenes and effects in your apps. It incorporates a physics engine, a particle generator, and easy ways to script the actions of 3D objects so you can describe your scene in terms of its content — geometry, materials, lights, and cameras — then animate it by describing changes to those objects.
SceneKit是一個高級的3D圖形框架,它幫助您在應(yīng)用程序中創(chuàng)建3D動畫場景和效果。它包含了一個物理引擎,一個粒子發(fā)生器,以及簡單的方法來編寫3D對象的動作腳本,這樣你就可以用它的內(nèi)容來描述你的場景——幾何,材料,燈光和攝像機——然后通過描述這些對象的變化來動畫它。

SceneKit是處理3D圖形的,在介紹怎么使用SceneKit 時。我們先來看下與3D有關(guān)的知識:坐標系與旋轉(zhuǎn)表達式。

  • SceneKit的3D坐標系為右手坐標系:
    這個坐標系沒有單位,而是根據(jù)屏幕的寬度和高度進行相對運算,屏幕上邊為1 下邊為-1 左邊為 -1 右邊為 1 。
    請牢記這個坐標系,接下來有關(guān)圖形處理都繞不開它。
坐標系
  • 旋轉(zhuǎn)表達式
    旋轉(zhuǎn)表達式主要有四種:
    1. 軸角 2. 歐拉角 3. 四元素 4. 旋轉(zhuǎn)矩陣
      這篇博客大概介紹了這四種表達式。旋轉(zhuǎn)表達式主要處理模型在空間位置的旋轉(zhuǎn),全景圖片播放時需要用到。

SceneKit比較強大,類比較多,接下來只主要介紹與實現(xiàn)全景有關(guān)的幾個類:

  • SCNView
    SCNView主要負責(zé)顯示3D模型對象的視圖,能夠添加到UIView類型的視圖上。
  • SCNScene
    場景:由幾何模型,燈光,照相機及其他屬性組成的環(huán)境。場景能添加各種節(jié)點,
    他包含了一個rootNode(根節(jié)點)屬性,可以添加各種node。
  • SCNNOde
    節(jié)點:一個抽象的概念,是個看不見摸不到的東西,沒有幾何形狀,但是有位置,以及自身坐標系。在場景中添加節(jié)點后,就可以在這個節(jié)點上放我們的元素了,比如幾何模型,燈光,攝像機等。節(jié)點上可以添加子節(jié)點的,每個節(jié)點都有自身坐標系。
    它的屬性包含:camera geometry position rotation eulerAngles pivot orientation等,其中rotation eulerAngles pivot orientation就是各種旋轉(zhuǎn)表達式,可以處理模型在空間的角度。
  • SCNGeometry
    幾何模型:全景圖片就是渲染在模型上的然后顯示在屏幕上。系統(tǒng)自帶的模型有很多種:SCNPlane SCNBox SCNSphere SCNCylinder SCNText。我們也可以通過SCNShape自定義各種奇形怪狀的模型。
  • SCNCamera
    相機(觀察者):這個類似我們現(xiàn)實中的相機,它也有焦距、視角等。圖形渲染到模型后,要添加相機我們才能看見。
    1. 視角:xFov yFov(默認60度),視角越大,屏幕上顯示的體積越?。?/li>
    2. 焦距:focusDistance(默認2.5),焦距越大,視角越?。?/li>
camera
  • SCNAction
    動畫:可以為節(jié)點添加各種動畫,包括:移動,旋轉(zhuǎn),縮放,自定義…

怎么設(shè)置才能將圖片渲染至模型上呢?這里需要先理解SCNGeometry的相關(guān)幾個屬性:

  • materials(SCNMaterial類):材質(zhì),要渲染的圖片就是添加到材質(zhì)上。一個模型可以添加多個材質(zhì),默認有一個材質(zhì),可以通過firstMaterial屬性獲取。
  • cullMode(SCNMaterial屬性):渲染時剔除的表面,SCNCullModeBack內(nèi)表面,SCNCullModeFront外表面。
  • diffuse(SCNMaterial屬性):

    The diffuse property specifies the amount of light diffusely reflected from the surface. The diffuse light is reflected equally in all directions and is therefore independent of the point of view.
    漫反射屬性指定從表面漫反射的光量。漫射光在各個方向上反射均勻,因此與視點無關(guān)。

  • contents(diffuse.contents):渲染的內(nèi)容,可以是顏色,圖片,圖層,路徑,紋理等。
    全景圖片渲染設(shè)置:geometry.firstMaterial.diffuse.contents = image;就可以了。

理解了一些基本知識后,開始編寫代碼:

顯示圖片
    // 初始化scene
    _scnView = [[SCNView alloc] init];
    _scnView.scene = [SCNScene scene];
    [self.view addSubview:_scnView];

    // 繪制球體
    SCNSphere *sphere = [SCNSphere sphereWithRadius:_config.shpereRadius];
    // 前面提過坐標系是根據(jù)屏幕相對運算的,具體值可以根據(jù)顯示效果調(diào)節(jié),這里球體radius設(shè)置為10,

    sphere.firstMaterial.cullMode = SCNCullModeFront; // 剔除球體外表面
    sphere.firstMaterial.doubleSided = NO; // 只渲染一個表面
    // 相機是處于球體內(nèi)部的,
    _sphereNode = [SCNNode node]; // 節(jié)點
    _sphereNode.geometry = sphere;
    _sphereNode.position = SCNVector3Make(0, 0, 0); // 位置(屏幕中心)
    // 渲染圖片
    sphere.firstMaterial.diffuse.contents = _config.contents;
    [_scnView.scene.rootNode addChildNode:_sphereNode]; // 添加至場景根節(jié)點

到這里,一個內(nèi)表面顯示圖片的球體創(chuàng)建并添加成功,但是現(xiàn)在view上面并不顯示,還需要添加相機節(jié)點:

    // 相機
    _camera = [SCNCamera camera];
    _camera.automaticallyAdjustsZRange = YES; // 自動添加可視距離
    _camera.xFov = _config.cameraFocalX; // 相機視角
    _camera.yFov = _config.cameraFocalY;
    _camera.focalBlurRadius = 0; // 模糊
    _cameraNode = [SCNNode node];
    _cameraNode.camera = _camera;
    [_scnView.scene.rootNode addChildNode:_cameraNode];

然后運行代碼,手機屏幕上就能看到圖片了。

demo

如果仔細對比原始的平鋪圖片會發(fā)現(xiàn),現(xiàn)在顯示的圖片是反過來的,是鏡像的;這是因為圖片是貼在球體上,而我們的相機是從球體中心往外觀察的,類似于現(xiàn)實世界中我們在房間里看貼在窗戶玻璃外的窗花一樣
我們?nèi)绾巫屗o@示呢?前面分析過圖片渲染的原理,關(guān)鍵的一點就是紋理,那么翻轉(zhuǎn)紋理坐標就能解決這個問題了:

    sphere.firstMaterial.diffuse.contentsTransform = SCNMatrix4MakeScale(-1, 1, 1);
    sphere.firstMaterial.diffuse.contentsTransform = SCNMatrix4Translate(sphere.firstMaterial.diffuse.contentsTransform, 1, 0, 0);

這里使用了矩陣操作,先把坐標沿y軸翻轉(zhuǎn)實現(xiàn)鏡像,翻轉(zhuǎn)后坐標偏移了所以接著需要平移回來。
還有一種方式,翻轉(zhuǎn)后不平移,而是指定超出紋理坐標范圍的紋理映射行為SCNWrapMode:mode有以下四種

wrap

指定repeat即可

    sphere.firstMaterial.diffuse.contentsTransform = SCNMatrix4MakeScale(-1, 1, 1);
    sphere.firstMaterial.diffuse.wrapS = SCNWrapModeRepeat;
    sphere.firstMaterial.diffuse.wrapT = SCNWrapModeRepeat;

但這時僅僅顯示了全景圖的一部分,并不支持360度查看及陀螺儀查看等功能。我們可以添加手勢及陀螺儀來控制全景圖的360度滑動:

手勢滑動,縮放功能

在scnView父視圖上添加兩個手勢:pinchGesture,panGesture。根據(jù)手勢操作,調(diào)節(jié)相機的參數(shù)實現(xiàn)相應(yīng)功能:

- (void)addGesture {
    self.pinchGesture = [[UIPinchGestureRecognizer alloc]initWithTarget:self action:@selector(pinchGesture:)];
    self.panGesture = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panGesture:)];
    [self addGestureRecognizer:_pinchGesture];
    [self addGestureRecognizer:_panGesture];
    _pinchGesture.enabled = _config.pinchEnabled;
    _panGesture.enabled = _config.panEnabled;
}

- (void)pinchGesture:(UIPinchGestureRecognizer *)gesture {
    if (gesture.state != UIGestureRecognizerStateEnded && gesture.state != UIGestureRecognizerStateFailed) {
        if (gesture.scale != NAN && gesture.scale != 0.0) {
            float scale = gesture.scale - 1;
            if (scale < 0) {
                scale *= (_config.scaleMax - _config.scaleMin);
            }
            _currentScale = scale + _prevScale;
            _currentScale = [self validateScale:_currentScale]; // 控制縮放的最小最大比例
            CGFloat valScale = [self validateScale:_currentScale];
            double xFov = _config.cameraFocalX * (1 - (valScale - 1));
            double yFov = _config.cameraFocalY * (1 - (valScale - 1));
            // 調(diào)節(jié)相機視角,前面分析了視角越大看到的體積越小,所以這里要反過來。即手勢放大時,視角要調(diào)小這樣看到的圖像才是放大的效果;
            _camera.xFov = xFov;
            _camera.yFov = yFov;
        }
    } else if(gesture.state == UIGestureRecognizerStateEnded){
        _prevScale = _currentScale;
    }
}

- (void)panGesture:(UIPanGestureRecognizer *)gesture {
    // 控制圖片滑動原理:手勢滑動,效果是手機屏幕上的圖片要跟著滑動,
    //  因為我們的圖片是渲染至球體上的,所以可以控制球體轉(zhuǎn)動來實現(xiàn)滑動效果。
    // 一般的,我們都是控制相機(觀察者)。因為相機處于球體內(nèi)部,相機需要往相反的方向轉(zhuǎn)動。
    if (gesture.state == UIGestureRecognizerStateBegan){
        CGPoint currentPoint = [gesture locationInView:gesture.view];
        self.lastPointX = currentPoint.x;
        self.lastPointY = currentPoint.y;
    }else{
        CGPoint currentPoint = [gesture locationInView:gesture.view];
        float distX = currentPoint.x - self.lastPointX;
        float distY = currentPoint.y - self.lastPointY;
        self.lastPointX = currentPoint.x;
        self.lastPointY = currentPoint.y;
        // 手勢滑動角度的微調(diào)
        distX *= - 0.005 * 0.5;
        distY *= - 0.005 * 0.5;
        SCNMatrix4 modelMatrix = SCNMatrix4Identity;
        if (fabs(distX)  > fabs(distY)) {
            self.fingerRotationY += distX;
        }else {
            self.fingerRotationX += distY;
        }
        // 因為是右手坐標系,所以相機水平轉(zhuǎn)動時是繞Y軸轉(zhuǎn)動,垂直方向轉(zhuǎn)動時需繞X軸轉(zhuǎn)動。Z軸保持不變。這里旋轉(zhuǎn)表達式用的是旋轉(zhuǎn)矩陣
        modelMatrix = SCNMatrix4Rotate(modelMatrix, self.fingerRotationY, 0, 1, 0);
        modelMatrix = SCNMatrix4Rotate(modelMatrix, self.fingerRotationX,1, 0, 0);
        _cameraNode.pivot = modelMatrix;
    }
}

- (float)validateScale:(float)scale{
    if (scale < _config.scaleMin) {
        scale = _config.scaleMin;
    }else if (scale > _config.scaleMax) {
        scale = _config.scaleMax;
    }
    return scale;
}

陀螺儀功能

陀螺儀功能是讓圖片跟著手機的方位轉(zhuǎn)動,原理和手勢滑動一樣:

- (void)addMotionFunction {
    _motionManager = [[CMMotionManager alloc]init];
    _motionManager.deviceMotionUpdateInterval = 1.0 / 30.0;
    _motionManager.gyroUpdateInterval = 1.0f / 30;
    _motionManager.showsDeviceMovementDisplay = YES;
    if (_motionManager.isDeviceMotionAvailable) {
        [_motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMDeviceMotion * _Nullable motion, NSError * _Nullable error) {
            if (!self.config.motionEnabled) {
                return;
            }
            CMAttitude *attitude = motion.attitude;
            if (attitude == nil) {
                return;
            }
            //                self.cameraNode.eulerAngles = SCNVector3Make(attitude.pitch - M_PI / 2 , attitude.roll, attitude.yaw);
            // 這里旋轉(zhuǎn)表達式用的是四元素(陀螺儀返回的attitude.quaternion就是四元素)
            self.cameraNode.orientation = [self orientationFromCMQuaternion:attitude.quaternion];
        }];
    }
}

- (SCNQuaternion)orientationFromCMQuaternion:(CMQuaternion)quaternion {
    GLKQuaternion gq1 = GLKQuaternionMakeWithAngleAndAxis(GLKMathDegreesToRadians(- 90), 1, 0, 0);
    // 這里x軸要同時旋轉(zhuǎn)90度,這是因為手機陀螺儀的坐標系不一致:手機正放于桌面上的坐標為(0,0,0);而scnView坐標系是手機正立的時候為(0,0,0);

    GLKQuaternion gq2 = GLKQuaternionMake(quaternion.x, quaternion.y, quaternion.z, quaternion.w);
    GLKQuaternion qp  = GLKQuaternionMultiply(gq1, gq2);
    return SCNVector4Make(qp.x, qp.y, qp.z, qp.w);
}
添加遮罩

大部分全景圖片都是由全景相機拍攝出來的,全景相機是360度的,在拍攝時相機底部的支架也會拍攝進去:

支架

為了美觀,不影響整體效果 ,我們需要用一張圖片蓋住。怎么在球面圖形上面加張圖片呢?其實我們只要在創(chuàng)建一個渲染圖片的平面模型,找準位置添加到場景rootNode上就可以了:

    _overlayNode = [SCNNode node];
    _overlayNode.geometry= [SCNPlane planeWithWidth:1 height:1];
    _overlayNode.geometry.firstMaterial.diffuse.contents = overlayIcon; // 圖片
    _overlayNode.position = SCNVector3Make(0, - 4, 0);  // 支架位于相機正下方,也就是坐標系Y軸負方向
    _overlayNode.rotation = SCNVector4Make(1, 0, 0, - M_PI / 2); // 旋轉(zhuǎn) 否則看不到
    // 這里旋轉(zhuǎn)90度 還是坐標的原因:默認情況下添加的SCNPlane模型是平鋪在XY平面,而我們添加的遮罩X,Z都是0,所以需要旋轉(zhuǎn)至XZ平面才能看到遮罩

    _overlayNode.geometry.firstMaterial.cullMode = SCNCullModeBack;
    [_scnView.scene.rootNode addChildNode:_overlayNode];
遮罩
頭控功能(eyepick)

其原理和上面的添加遮罩是一樣的,都是在場景中添加節(jié)點。不過這些節(jié)點需要觸發(fā)事件,實現(xiàn)相關(guān)的控制功能。這里的控制功能基本都是控制切換上一張圖片,下一張圖片,實現(xiàn)頭戴設(shè)備后也能實現(xiàn)查看圖集的需求。

    // 添加頭控節(jié)點 
    _potNode = [SCNNode node]; // 選擇pick節(jié)點
    _potNode.geometry= [SCNPlane planeWithWidth:0.3 height:0.3];
    _potNode.geometry.firstMaterial.diffuse.contents = potIcon;
    _potNode.position = SCNVector3Make(0, 0, - 9); 
    _potNode.geometry.firstMaterial.cullMode = SCNCullModeBack;
    [_cameraNode addChildNode:_potNode]; // 加在_camera上,camera轉(zhuǎn)動時保持不變
    
    _preNode = [SCNNode node]; // 上一張圖片function節(jié)點
    _preNode.geometry= [SCNPlane planeWithWidth:0.3 height:0.3];
    _preNode.geometry.firstMaterial.diffuse.contents = preIcon;
    _preNode.position = SCNVector3Make(- 1.5, 0.5, - 9);
    _preNode.geometry.firstMaterial.cullMode = SCNCullModeBack;
    [_sphereNode addChildNode:_preNode];

    _nextNode = [SCNNode node]; // 下一張圖片function節(jié)點
    _nextNode.geometry= [SCNPlane planeWithWidth:0.3 height:0.3];
    _nextNode.geometry.firstMaterial.diffuse.contents = nextIcon;
    _nextNode.position = SCNVector3Make(1.5, 0.5, - 9);
    _nextNode.geometry.firstMaterial.cullMode = SCNCullModeBack;
    [_sphereNode addChildNode:_nextNode];

節(jié)點添加完后,并正常顯示了,接下來就要加上觸發(fā)事件,觸發(fā)的時機就是當function節(jié)點和pick節(jié)點重合的時候。只判斷重合還不夠,因為在瀏覽圖片時,相機轉(zhuǎn)動時偶發(fā)情況下function節(jié)點和pick節(jié)點碰巧重合。因此在重合的基礎(chǔ)上,還需加上延時動畫,當重合的時間達到動畫的時間后才觸發(fā)事件。

// 添加頭控動畫
- (void)addEyepickerAnimation {
    _animationNode = [SCNNode node];
    _animationNode.geometry = [SCNPlane planeWithWidth:0.3 height:0.3];
    _animationNode.hidden = YES;
    [_potNode addChildNode:_animationNode];
    __weak typeof(self) weakSelf = self;
    _animationAction = [SCNAction customActionWithDuration:3.f actionBlock:^(SCNNode * _Nonnull node, CGFloat elapsedTime) {
        int time = (int) (elapsedTime * (images.count - 1) / 3.0);
        node.geometry.firstMaterial.diffuse.contents = images[time];
        if (time == images.count - 1 && (weakSelf.isPreAnimating || weakSelf.isNextAnimating)) { // 動畫結(jié)束
            FWPanoramaHotpotType type = [weakSelf.animationKey isEqualToString:@"pre"] ? FWPanoramaHotpotTypePrev : FWPanoramaHotpotTypeNext;
            if (type == FWPanoramaHotpotTypePrev) {
                weakSelf.preAnimationEnd = YES;
                [weakSelf removePreAnimation];
            }else {
                weakSelf.nextAnimationEnd = YES;
                [weakSelf removeNextAnimation];
            }
            if ([weakSelf.delegate respondsToSelector:@selector(renderView:didPickHotpot:)]) {
                [weakSelf.delegate renderView:weakSelf didPickHotpot:type];
            }
        }
    }];
}

// scnView的代理方法,圖片渲染都會走這里
- (void)renderer:(id <SCNSceneRenderer>)renderer updateAtTime:(NSTimeInterval)time {
    SCNVector3 prePosition = [_preNode convertPosition:_preNode.position toNode:_cameraNode]; // 計算相對坐標
    SCNVector3 nextPosition = [_nextNode convertPosition:_nextNode.position toNode:_cameraNode];
//    NSLog(@"camera  x;%f,y:%f,z:%f",prePosition.x,prePosition.y,prePosition.z);
    BOOL preOverlap = prePosition.x > - 0.3 / 2 && prePosition.x < 0.3 / 2 && prePosition.y > - 0.3 / 2 && prePosition.y < 0.3 / 2;
    if (!_preAnimationEnd && preOverlap) {
        // 兩個node基本重合
        if (!_isPreAnimating) {
            [self runPreAnimation];
        }
    }else if (!_isNextAnimating && !preOverlap) {
        _preAnimationEnd = NO;
        [self removePreAnimation];
    }
    
    BOOL nextOverlap = nextPosition.x > - 0.3 / 2 && nextPosition.x < 0.3 / 2 && nextPosition.y > - 0.3 / 2 && nextPosition.y < 0.3 / 2;
    if (!_nextAnimationEnd && nextOverlap) {
        // 兩個node基本重合
        if (!_isNextAnimating) {
            [self runNextAnimation];
        }
    }else if (!_isPreAnimating && !nextOverlap) {
        _nextAnimationEnd = NO;
        [self removeNextAnimation];
    }
}
節(jié)點點擊事件

上面兩個eyepick節(jié)點的事件,是由頭控觸發(fā)的;那如果我們要做到通過手動點擊節(jié)點來觸發(fā)事件,該怎么做呢?

  1. 首先,我們需要拿到手點擊屏幕的坐標;
  2. 然后通過這個坐標,計算該點對應(yīng)的節(jié)點;
  3. 如果有對應(yīng)的節(jié)點,再判斷是否是我們需要的目標節(jié)點;
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 獲取到手勢的對象
    UITouch *touch = [touches allObjects].firstObject;
    
    // 手勢在SCNView中的位置
    CGPoint touchPoint = [touch locationInView:self.scnView];
    
    // 該方法會返回一個SCNHitTestResult數(shù)組,這個數(shù)組中每個元素的node都包含了指定的點
    NSArray *hitResults = [self.scnView hitTest:touchPoint options:nil];
    if (hitResults.count > 0) {
        SCNHitTestResult *hit = [hitResults firstObject];
        SCNNode *node = hit.node;
        if (node == _preNode) {
            NSLog(@"hit prenode");
        }else if (node == _nextNode) {
            NSLog(@"hit nextnode");
        }
    }
}

(以上代碼片段由樓下junior_a提供)

分屏功能

實現(xiàn)分屏,就是將1個scnView分成兩個,這兩個scnView的顯示和操作都是一樣的。要實現(xiàn)這種效果,可以添加兩個subview并將scnView的contents賦值給兩個subview。

@property (nonatomic, strong) SCNView *leftView;
@property (nonatomic, strong) SCNView *rightView;

 [_leftView mas_makeConstraints:^(MASConstraintMaker *make) {
            make.left.right.top.mas_equalTo(0);
            make.height.mas_equalTo(self.bounds.size.height / 2);
        }];
 [_rightView mas_makeConstraints:^(MASConstraintMaker *make) {
            make.left.right.bottom.mas_equalTo(0);
            make.height.mas_equalTo(self.bounds.size.height / 2);
        }];
_leftView.layer.contents = self.scnView.layer.contents;
_rightView.layer.contents = self.scnView.layer.contents;
[self.view addSubview:_leftView];
[self.view addSubview:_rightView];
視頻播放器

視頻播放器,原理和圖片播放器是一樣的:改動上面的一小段代碼,就能實現(xiàn)和圖片同樣功能的視頻播放器;
改動的地方就是將渲染在球體模型上的圖片,換成skView包裝的視頻播放器AVPlayer:

- (void)createSphere {
    SCNSphere *sphere = [SCNSphere sphereWithRadius:_config.shpereRadius];
    sphere.firstMaterial.cullMode = SCNCullModeFront; // 剔除球體外表面
    sphere.firstMaterial.doubleSided = NO; // 只渲染一個表面
    _sphereNode = [SCNNode node]; // 節(jié)點
    _sphereNode.geometry = sphere;
    _sphereNode.position = SCNVector3Make(0, 0, 0);
    
    // 渲染圖片
//    sphere.firstMaterial.diffuse.contents = _config.contents;
//    [_scnView.scene.rootNode addChildNode:_sphereNode];
    
    // 渲染視頻
    NSString *path = [[NSBundle mainBundle] pathForResource:@"360" ofType:@"mp4"];
    NSURL *url = [NSURL fileURLWithPath:path];
     AVPlayerItem *item = [AVPlayerItem playerItemWithURL:url];
     _player = [AVPlayer playerWithPlayerItem:item];
    [_player play];

    // 需要使用SpriteKit
    _videoNode = [[SKVideoNode alloc] initWithAVPlayer:_player]; // 播放器節(jié)點  
    _videoNode.size = CGSizeMake(self.frame.size.width, self.frame.size.height); // 這里的size的單位和上面講的SceneKit不一樣,這里就是實際的像素點單位 這里設(shè)置和當前view一樣
    _videoNode.position = CGPointMake(_videoNode.size.width / 2, _videoNode.size.height / 2);
    _skScene = [SKScene sceneWithSize:_videoNode.size];
    _skScene.scaleMode = SKSceneScaleModeAspectFit;
    [_skScene addChild:_videoNode];
    sphere.firstMaterial.diffuse.contents = _skScene;
    [_scnView.scene.rootNode addChildNode:_sphereNode];
}

另外,和普通的視頻播放器一樣,我們可以通過_player對象控制視頻的播放(播放/暫停/快進等)

至此,全景播放器的所有功能都實現(xiàn)了。所有代碼也就400行,是不是很簡單呢

覺得有用的點個贊

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1、通過CocoaPods安裝項目名稱項目信息 AFNetworking網(wǎng)絡(luò)請求組件 FMDB本地數(shù)據(jù)庫組件 SD...
    陽明AI閱讀 16,171評論 3 119
  • 看了成都樓盤9月成交價,主城區(qū)樓盤每平方均突破一萬八,地理位置好點,開發(fā)商稍微大點,基本二三萬。周邊城市,原來不受...
    功夫HR閱讀 192評論 0 0
  • 子曰:巧言令色。鮮矣仁! 子曰:道千乘之國,敬事而信,節(jié)用而愛人,使民以時。 曾子曰:慎終追遠,民德歸厚矣。 子曰...
    _人間客閱讀 909評論 0 2

友情鏈接更多精彩內(nèi)容