最近用SceneKit做了全景看房的功能灰追,現(xiàn)總結(jié)下如何實(shí)現(xiàn)的。
先看下最終的效果:
VR圖片全景播放器有以下功能:
- 360度
- 手勢(shì)滑動(dòng)惕味,縮放
- 陀螺儀
- 分屏(VR眼鏡)
- 熱點(diǎn)hotpot
- 頭控/eyepick
手勢(shì)滑動(dòng),縮放,陀螺儀功能都是調(diào)節(jié)球面圖片顯示的位置葛家;
熱點(diǎn)和頭控功能本質(zhì)是一樣的,都是在原有模型上增加3維的視圖泌类。它們用途不一樣癞谒,頭控功能(全景圖片一般就是eyepick功能)一般是戴VR眼鏡后,通過(guò)模型的位置觸發(fā)控制事件刃榨。
展示全景圖的原理很簡(jiǎn)單:將圖片渲染至球體模型內(nèi)表面上弹砚,手機(jī)處于球體中心(圖中紅色區(qū)域),當(dāng)旋轉(zhuǎn)手機(jī)的時(shí)候枢希,
球體向相反的方向旋轉(zhuǎn)桌吃,這樣我們就可以看到球體上的畫(huà)面了。
怎么將圖片繪制于球體上呢苞轿?
這需要使用openGL這個(gè)框架茅诱,openGL渲染球體圖片步驟大致如下:
- 生成頂點(diǎn)數(shù)據(jù),也就是球面上點(diǎn)坐標(biāo)數(shù)據(jù)搬卒。頂點(diǎn)越多生成的球體越平滑瑟俭,但也有極限,當(dāng)頂點(diǎn)大于一定值的時(shí)候再多的頂點(diǎn)也看不出差別來(lái)反而會(huì)影響性能契邀。
- 生成紋理數(shù)據(jù)摆寄,也就是圖片的顏色緩存數(shù)據(jù)。
- 著色器將顏色數(shù)據(jù)渲染至頂點(diǎn)上蹂安。
全景播放器第三方庫(kù)
MD360Player4iOS:支持全景圖片/視頻椭迎,有分屏/陀螺儀/手勢(shì)移動(dòng)功能,但沒(méi)有熱點(diǎn)及頭控功能田盈;
Panorama:只支持全景圖片畜号,比較輕量。也只有分屏/陀螺儀/手勢(shì)功能允瞧;
PanoramaGL:只支持全景圖片简软,具有陀螺儀/手勢(shì)/熱點(diǎn)功能,但這個(gè)庫(kù)比較久遠(yuǎn)仍是MRC述暂,沒(méi)人維護(hù)痹升;
得圖SDK:支持全景圖片/視頻,也只有分屏/陀螺儀/手勢(shì)移動(dòng)功能
現(xiàn)在主流的和全景圖片有關(guān)的三方庫(kù)畦韭,基本上都沒(méi)有熱點(diǎn)及頭控功能疼蛾;之前有試過(guò)在MD360Player4iOS基礎(chǔ)上增加這兩個(gè)功能,但因?yàn)樽约簅penGL零基礎(chǔ)后來(lái)還是暫時(shí)放棄了艺配。
后來(lái)發(fā)現(xiàn)系統(tǒng)SceneKit框架也可以實(shí)現(xiàn)以上所有功能察郁,使用起來(lái)也非常簡(jiǎn)單衍慎。接下來(lái)我們來(lái)了解下SceneKit,看如何實(shí)現(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是一個(gè)高級(jí)的3D圖形框架乔夯,它幫助您在應(yīng)用程序中創(chuàng)建3D動(dòng)畫(huà)場(chǎng)景和效果。它包含了一個(gè)物理引擎款侵,一個(gè)粒子發(fā)生器末荐,以及簡(jiǎn)單的方法來(lái)編寫(xiě)3D對(duì)象的動(dòng)作腳本,這樣你就可以用它的內(nèi)容來(lái)描述你的場(chǎng)景——幾何喳坠,材料鞠评,燈光和攝像機(jī)——然后通過(guò)描述這些對(duì)象的變化來(lái)動(dòng)畫(huà)它茂蚓。
SceneKit是處理3D圖形的壕鹉,在介紹怎么使用SceneKit 時(shí)。我們先來(lái)看下與3D有關(guān)的知識(shí):坐標(biāo)系與旋轉(zhuǎn)表達(dá)式聋涨。
- SceneKit的3D坐標(biāo)系為右手坐標(biāo)系:
這個(gè)坐標(biāo)系沒(méi)有單位晾浴,而是根據(jù)屏幕的寬度和高度進(jìn)行相對(duì)運(yùn)算,屏幕上邊為1 下邊為-1 左邊為 -1 右邊為 1 牍白。
請(qǐng)牢記這個(gè)坐標(biāo)系脊凰,接下來(lái)有關(guān)圖形處理都繞不開(kāi)它。
- 旋轉(zhuǎn)表達(dá)式
旋轉(zhuǎn)表達(dá)式主要有四種:- 軸角 2. 歐拉角 3. 四元素 4. 旋轉(zhuǎn)矩陣
這篇博客大概介紹了這四種表達(dá)式茂腥。旋轉(zhuǎn)表達(dá)式主要處理模型在空間位置的旋轉(zhuǎn)狸涌,全景圖片播放時(shí)需要用到。
- 軸角 2. 歐拉角 3. 四元素 4. 旋轉(zhuǎn)矩陣
SceneKit比較強(qiáng)大最岗,類比較多帕胆,接下來(lái)只主要介紹與實(shí)現(xiàn)全景有關(guān)的幾個(gè)類:
- SCNView
SCNView主要負(fù)責(zé)顯示3D模型對(duì)象的視圖,能夠添加到UIView類型的視圖上般渡。 - SCNScene
場(chǎng)景:由幾何模型懒豹,燈光,照相機(jī)及其他屬性組成的環(huán)境驯用。場(chǎng)景能添加各種節(jié)點(diǎn)脸秽,
他包含了一個(gè)rootNode(根節(jié)點(diǎn))屬性,可以添加各種node蝴乔。 - SCNNOde
節(jié)點(diǎn):一個(gè)抽象的概念记餐,是個(gè)看不見(jiàn)摸不到的東西,沒(méi)有幾何形狀薇正,但是有位置片酝,以及自身坐標(biāo)系巩剖。在場(chǎng)景中添加節(jié)點(diǎn)后,就可以在這個(gè)節(jié)點(diǎn)上放我們的元素了钠怯,比如幾何模型佳魔,燈光,攝像機(jī)等晦炊。節(jié)點(diǎn)上可以添加子節(jié)點(diǎn)的鞠鲜,每個(gè)節(jié)點(diǎn)都有自身坐標(biāo)系。
它的屬性包含:camera geometry position rotation eulerAngles pivot orientation等断国,其中rotation eulerAngles pivot orientation就是各種旋轉(zhuǎn)表達(dá)式贤姆,可以處理模型在空間的角度慎恒。 - SCNGeometry
幾何模型:全景圖片就是渲染在模型上的然后顯示在屏幕上雹姊。系統(tǒng)自帶的模型有很多種:SCNPlane SCNBox SCNSphere SCNCylinder SCNText。我們也可以通過(guò)SCNShape自定義各種奇形怪狀的模型愉昆。 - SCNCamera
相機(jī)(觀察者):這個(gè)類似我們現(xiàn)實(shí)中的相機(jī)薄疚,它也有焦距碧信、視角等。圖形渲染到模型后街夭,要添加相機(jī)我們才能看見(jiàn)砰碴。- 視角:xFov yFov(默認(rèn)60度),視角越大板丽,屏幕上顯示的體積越谐释鳌;
- 焦距:focusDistance(默認(rèn)2.5)埃碱,焦距越大猖辫,視角越小砚殿;
- SCNAction
動(dòng)畫(huà):可以為節(jié)點(diǎn)添加各種動(dòng)畫(huà)啃憎,包括:移動(dòng),旋轉(zhuǎn)瓮具,縮放荧飞,自定義…
怎么設(shè)置才能將圖片渲染至模型上呢?這里需要先理解SCNGeometry的相關(guān)幾個(gè)屬性:
- materials(SCNMaterial類):材質(zhì)名党,要渲染的圖片就是添加到材質(zhì)上叹阔。一個(gè)模型可以添加多個(gè)材質(zhì),默認(rèn)有一個(gè)材質(zhì)传睹,可以通過(guò)firstMaterial屬性獲取耳幢。
- cullMode(SCNMaterial屬性):渲染時(shí)剔除的表面,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.
漫反射屬性指定從表面漫反射的光量启上。漫射光在各個(gè)方向上反射均勻,因此與視點(diǎn)無(wú)關(guān)店印。 - contents(diffuse.contents):渲染的內(nèi)容冈在,可以是顏色,圖片按摘,圖層包券,路徑,紋理等炫贤。
全景圖片渲染設(shè)置:geometry.firstMaterial.diffuse.contents = image溅固;就可以了。
理解了一些基本知識(shí)后兰珍,開(kāi)始編寫(xiě)代碼:
顯示圖片
// 初始化scene
_scnView = [[SCNView alloc] init];
_scnView.scene = [SCNScene scene];
[self.view addSubview:_scnView];
// 繪制球體
SCNSphere *sphere = [SCNSphere sphereWithRadius:_config.shpereRadius];
// 前面提過(guò)坐標(biāo)系是根據(jù)屏幕相對(duì)運(yùn)算的侍郭,具體值可以根據(jù)顯示效果調(diào)節(jié),這里球體radius設(shè)置為10掠河,
sphere.firstMaterial.cullMode = SCNCullModeFront; // 剔除球體外表面
sphere.firstMaterial.doubleSided = NO; // 只渲染一個(gè)表面
// 相機(jī)是處于球體內(nèi)部的亮元,
_sphereNode = [SCNNode node]; // 節(jié)點(diǎn)
_sphereNode.geometry = sphere;
_sphereNode.position = SCNVector3Make(0, 0, 0); // 位置(屏幕中心)
// 渲染圖片
sphere.firstMaterial.diffuse.contents = _config.contents;
[_scnView.scene.rootNode addChildNode:_sphereNode]; // 添加至場(chǎng)景根節(jié)點(diǎn)
到這里,一個(gè)內(nèi)表面顯示圖片的球體創(chuàng)建并添加成功口柳,但是現(xiàn)在view上面并不顯示苹粟,還需要添加相機(jī)節(jié)點(diǎn):
// 相機(jī)
_camera = [SCNCamera camera];
_camera.automaticallyAdjustsZRange = YES; // 自動(dòng)添加可視距離
_camera.xFov = _config.cameraFocalX; // 相機(jī)視角
_camera.yFov = _config.cameraFocalY;
_camera.focalBlurRadius = 0; // 模糊
_cameraNode = [SCNNode node];
_cameraNode.camera = _camera;
[_scnView.scene.rootNode addChildNode:_cameraNode];
然后運(yùn)行代碼,手機(jī)屏幕上就能看到圖片了跃闹。
如果仔細(xì)對(duì)比原始的平鋪圖片會(huì)發(fā)現(xiàn),現(xiàn)在顯示的圖片是反過(guò)來(lái)的毛好,是鏡像的望艺;這是因?yàn)閳D片是貼在球體上,而我們的相機(jī)是從球體中心往外觀察的肌访,類似于現(xiàn)實(shí)世界中我們?cè)诜块g里看貼在窗戶玻璃外的窗花一樣
我們?nèi)绾巫屗o@示呢找默?前面分析過(guò)圖片渲染的原理,關(guān)鍵的一點(diǎn)就是紋理吼驶,那么翻轉(zhuǎn)紋理坐標(biāo)就能解決這個(gè)問(wèn)題了:
sphere.firstMaterial.diffuse.contentsTransform = SCNMatrix4MakeScale(-1, 1, 1);
sphere.firstMaterial.diffuse.contentsTransform = SCNMatrix4Translate(sphere.firstMaterial.diffuse.contentsTransform, 1, 0, 0);
這里使用了矩陣操作惩激,先把坐標(biāo)沿y軸翻轉(zhuǎn)實(shí)現(xiàn)鏡像,翻轉(zhuǎn)后坐標(biāo)偏移了所以接著需要平移回來(lái)蟹演。
還有一種方式风钻,翻轉(zhuǎn)后不平移,而是指定超出紋理坐標(biāo)范圍的紋理映射行為SCNWrapMode:mode有以下四種
指定repeat即可
sphere.firstMaterial.diffuse.contentsTransform = SCNMatrix4MakeScale(-1, 1, 1);
sphere.firstMaterial.diffuse.wrapS = SCNWrapModeRepeat;
sphere.firstMaterial.diffuse.wrapT = SCNWrapModeRepeat;
但這時(shí)僅僅顯示了全景圖的一部分酒请,并不支持360度查看及陀螺儀查看等功能骡技。我們可以添加手勢(shì)及陀螺儀來(lái)控制全景圖的360度滑動(dòng):
手勢(shì)滑動(dòng),縮放功能
在scnView父視圖上添加兩個(gè)手勢(shì):pinchGesture,panGesture布朦。根據(jù)手勢(shì)操作囤萤,調(diào)節(jié)相機(jī)的參數(shù)實(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é)相機(jī)視角,前面分析了視角越大看到的體積越小是趴,所以這里要反過(guò)來(lái)涛舍。即手勢(shì)放大時(shí),視角要調(diào)小這樣看到的圖像才是放大的效果唆途;
_camera.xFov = xFov;
_camera.yFov = yFov;
}
} else if(gesture.state == UIGestureRecognizerStateEnded){
_prevScale = _currentScale;
}
}
- (void)panGesture:(UIPanGestureRecognizer *)gesture {
// 控制圖片滑動(dòng)原理:手勢(shì)滑動(dòng)做盅,效果是手機(jī)屏幕上的圖片要跟著滑動(dòng),
// 因?yàn)槲覀兊膱D片是渲染至球體上的窘哈,所以可以控制球體轉(zhuǎn)動(dòng)來(lái)實(shí)現(xiàn)滑動(dòng)效果吹榴。
// 一般的,我們都是控制相機(jī)(觀察者)滚婉。因?yàn)橄鄼C(jī)處于球體內(nèi)部图筹,相機(jī)需要往相反的方向轉(zhuǎn)動(dòng)。
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;
// 手勢(shì)滑動(dòng)角度的微調(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;
}
// 因?yàn)槭怯沂肿鴺?biāo)系让腹,所以相機(jī)水平轉(zhuǎn)動(dòng)時(shí)是繞Y軸轉(zhuǎn)動(dòng)远剩,垂直方向轉(zhuǎn)動(dòng)時(shí)需繞X軸轉(zhuǎn)動(dòng)。Z軸保持不變骇窍。這里旋轉(zhuǎn)表達(dá)式用的是旋轉(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;
}
陀螺儀功能
陀螺儀功能是讓圖片跟著手機(jī)的方位轉(zhuǎn)動(dòng)瓜晤,原理和手勢(shì)滑動(dòng)一樣:
- (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)表達(dá)式用的是四元素(陀螺儀返回的attitude.quaternion就是四元素)
self.cameraNode.orientation = [self orientationFromCMQuaternion:attitude.quaternion];
}];
}
}
- (SCNQuaternion)orientationFromCMQuaternion:(CMQuaternion)quaternion {
GLKQuaternion gq1 = GLKQuaternionMakeWithAngleAndAxis(GLKMathDegreesToRadians(- 90), 1, 0, 0);
// 這里x軸要同時(shí)旋轉(zhuǎn)90度,這是因?yàn)槭謾C(jī)陀螺儀的坐標(biāo)系不一致:手機(jī)正放于桌面上的坐標(biāo)為(0,0,0);而scnView坐標(biāo)系是手機(jī)正立的時(shí)候?yàn)?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);
}
添加遮罩
大部分全景圖片都是由全景相機(jī)拍攝出來(lái)的痢掠,全景相機(jī)是360度的,在拍攝時(shí)相機(jī)底部的支架也會(huì)拍攝進(jìn)去:
為了美觀嘲恍,不影響整體效果 足画,我們需要用一張圖片蓋住。怎么在球面圖形上面加張圖片呢佃牛?其實(shí)我們只要在創(chuàng)建一個(gè)渲染圖片的平面模型淹辞,找準(zhǔn)位置添加到場(chǎng)景rootNode上就可以了:
_overlayNode = [SCNNode node];
_overlayNode.geometry= [SCNPlane planeWithWidth:1 height:1];
_overlayNode.geometry.firstMaterial.diffuse.contents = overlayIcon; // 圖片
_overlayNode.position = SCNVector3Make(0, - 4, 0); // 支架位于相機(jī)正下方,也就是坐標(biāo)系Y軸負(fù)方向
_overlayNode.rotation = SCNVector4Make(1, 0, 0, - M_PI / 2); // 旋轉(zhuǎn) 否則看不到
// 這里旋轉(zhuǎn)90度 還是坐標(biāo)的原因:默認(rèn)情況下添加的SCNPlane模型是平鋪在XY平面俘侠,而我們添加的遮罩X,Z都是0象缀,所以需要旋轉(zhuǎn)至XZ平面才能看到遮罩
_overlayNode.geometry.firstMaterial.cullMode = SCNCullModeBack;
[_scnView.scene.rootNode addChildNode:_overlayNode];
頭控功能(eyepick)
其原理和上面的添加遮罩是一樣的,都是在場(chǎng)景中添加節(jié)點(diǎn)爷速。不過(guò)這些節(jié)點(diǎn)需要觸發(fā)事件央星,實(shí)現(xiàn)相關(guān)的控制功能。這里的控制功能基本都是控制切換上一張圖片遍希,下一張圖片等曼,實(shí)現(xiàn)頭戴設(shè)備后也能實(shí)現(xiàn)查看圖集的需求。
// 添加頭控節(jié)點(diǎn)
_potNode = [SCNNode node]; // 選擇pick節(jié)點(diǎn)
_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)動(dòng)時(shí)保持不變
_preNode = [SCNNode node]; // 上一張圖片function節(jié)點(diǎn)
_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é)點(diǎn)
_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é)點(diǎn)添加完后禁谦,并正常顯示了胁黑,接下來(lái)就要加上觸發(fā)事件,觸發(fā)的時(shí)機(jī)就是當(dāng)function節(jié)點(diǎn)和pick節(jié)點(diǎn)重合的時(shí)候州泊。只判斷重合還不夠丧蘸,因?yàn)樵跒g覽圖片時(shí),相機(jī)轉(zhuǎn)動(dòng)時(shí)偶發(fā)情況下function節(jié)點(diǎn)和pick節(jié)點(diǎn)碰巧重合遥皂。因此在重合的基礎(chǔ)上力喷,還需加上延時(shí)動(dòng)畫(huà),當(dāng)重合的時(shí)間達(dá)到動(dòng)畫(huà)的時(shí)間后才觸發(fā)事件演训。
// 添加頭控動(dòng)畫(huà)
- (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)) { // 動(dòng)畫(huà)結(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的代理方法弟孟,圖片渲染都會(huì)走這里
- (void)renderer:(id <SCNSceneRenderer>)renderer updateAtTime:(NSTimeInterval)time {
SCNVector3 prePosition = [_preNode convertPosition:_preNode.position toNode:_cameraNode]; // 計(jì)算相對(duì)坐標(biāo)
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) {
// 兩個(gè)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) {
// 兩個(gè)node基本重合
if (!_isNextAnimating) {
[self runNextAnimation];
}
}else if (!_isPreAnimating && !nextOverlap) {
_nextAnimationEnd = NO;
[self removeNextAnimation];
}
}
節(jié)點(diǎn)點(diǎn)擊事件
上面兩個(gè)eyepick節(jié)點(diǎn)的事件,是由頭控觸發(fā)的样悟;那如果我們要做到通過(guò)手動(dòng)點(diǎn)擊節(jié)點(diǎn)來(lái)觸發(fā)事件拂募,該怎么做呢?
- 首先窟她,我們需要拿到手點(diǎn)擊屏幕的坐標(biāo)陈症;
- 然后通過(guò)這個(gè)坐標(biāo),計(jì)算該點(diǎn)對(duì)應(yīng)的節(jié)點(diǎn)震糖;
- 如果有對(duì)應(yīng)的節(jié)點(diǎn)录肯,再判斷是否是我們需要的目標(biāo)節(jié)點(diǎn);
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 獲取到手勢(shì)的對(duì)象
UITouch *touch = [touches allObjects].firstObject;
// 手勢(shì)在SCNView中的位置
CGPoint touchPoint = [touch locationInView:self.scnView];
// 該方法會(huì)返回一個(gè)SCNHitTestResult數(shù)組吊说,這個(gè)數(shù)組中每個(gè)元素的node都包含了指定的點(diǎn)
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提供)
分屏功能
實(shí)現(xiàn)分屏论咏,就是將1個(gè)scnView分成兩個(gè),這兩個(gè)scnView的顯示和操作都是一樣的疏叨。要實(shí)現(xiàn)這種效果潘靖,可以添加兩個(gè)subview并將scnView的contents賦值給兩個(gè)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];
視頻播放器
視頻播放器蚤蔓,原理和圖片播放器是一樣的:改動(dòng)上面的一小段代碼,就能實(shí)現(xiàn)和圖片同樣功能的視頻播放器;
改動(dòng)的地方就是將渲染在球體模型上的圖片糊余,換成skView包裝的視頻播放器AVPlayer:
- (void)createSphere {
SCNSphere *sphere = [SCNSphere sphereWithRadius:_config.shpereRadius];
sphere.firstMaterial.cullMode = SCNCullModeFront; // 剔除球體外表面
sphere.firstMaterial.doubleSided = NO; // 只渲染一個(gè)表面
_sphereNode = [SCNNode node]; // 節(jié)點(diǎn)
_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é)點(diǎn)
_videoNode.size = CGSizeMake(self.frame.size.width, self.frame.size.height); // 這里的size的單位和上面講的SceneKit不一樣秀又,這里就是實(shí)際的像素點(diǎn)單位 這里設(shè)置和當(dāng)前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];
}
另外,和普通的視頻播放器一樣贬芥,我們可以通過(guò)_player對(duì)象控制視頻的播放(播放/暫停/快進(jìn)等)
至此吐辙,全景播放器的所有功能都實(shí)現(xiàn)了。所有代碼也就400行蘸劈,是不是很簡(jiǎn)單呢
覺(jué)得有用的點(diǎn)個(gè)贊哈