ios-ARKit 實現與3D模型交互(模型換膚、運動)

本章實現對模型(Demo中用了一個汽車模型)的交互操作,包括對汽車模型換膚、零件拆卸、輪胎運轉、后視鏡開合、以及車窗的升降等等。在與AR世界的交互之前對AR世界的構建,以及模型的展示在另一篇文章中(AR世界的構建:http://www.lxweimin.com/p/f7c26b058348)這里就不再講述。

構建出AR世界并且在AR世界中展示3D模型后就可以開始對模型進行各種操作及交互:
思考:如何實現對汽車模型或者其身上子模型部件進行操作?
一個復雜模型的制作原理是由多個材質球或者模型(SceneKit中的節點SCNNode)拼接而成的的。在SceneKit中我們可以通過檢索模型的名稱對其進行交互。

Demo中相關屬性:列出以便文章閱讀

@property (nonatomic, strong) UIButton *backButton;//返回按鈕
@property (nonatomic, strong) ARSCNView *sceneView;//AR視圖(AR場景填在在其上)
@property (nonatomic, strong) ARWorldTrackingConfiguration *configuration;//AR世界追蹤
@property (nonatomic, strong) SCNScene *scene;//AR場景

@property (nonatomic, strong) ARPlaneAnchor *planAnchor;//平面錨點
@property (nonatomic, strong) SCNNode *planParanNode;//地面節點(模型放上面)
@property (nonatomic, assign) BOOL modelShowing;//是否已經顯示模型(已經顯示模型后不繼續重新布置平面)
@property (nonatomic, assign) BOOL isSeachPlan;//是否已經找到平面

@property (nonatomic, strong) SCNNode *carModelNode;//汽車模型節點

@property (nonatomic, assign) BOOL tireSpared;//是否已經拆下輪胎

//顏色面板
@property (nonatomic, strong) HCColorPanelView *colorPanelView;
//菜單面板
@property (nonatomic, strong) UIButton *menuButton;
@property (nonatomic, strong) HCMenuPanelView *menuPanelView;

·碰撞檢測 (點擊手機屏幕,檢測是否點擊了模型)

給汽車模型起個名字:

self.carModelNode.name = @"modelCarNode";//很重要,根據這個那么做對比,是否點擊了模型

點擊屏幕后監聽- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event方法,遍歷點擊事件,檢測是否與模型進行了碰撞:

//點擊檢測(碰撞檢測)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    if (self.arType == ARWorldTrackingConfigurationType_planeDetection_CarDemo && self.modelShowing) {
        //已經放置了汽車模型,檢測點擊汽車事件
        UITouch *touch = [touches anyObject];
        CGPoint tapPoint  = [touch locationInView:self.sceneView];//該點就是手指的點擊位置
        NSDictionary *hitTestOptions = [NSDictionary dictionaryWithObjectsAndKeys:@(true),SCNHitTestBoundingBoxOnlyKey, nil];
        NSArray<SCNHitTestResult *> * results= [self.sceneView hitTest:tapPoint options:hitTestOptions];
        for (SCNHitTestResult *res in results) {//遍歷所有的返回結果中的node
            if ([self isNodeCarModelObject:res.node]) {
//                [[HCToast shareInstance] showToast:@"點擊了汽車"];
                NSLog(@"點擊了汽車模型...............");
                break;
            }
        }
    }
}

//上溯找尋指定的node(是否點擊了汽車)
-(BOOL) isNodeCarModelObject:(SCNNode*)node {
    if ([@"modelCarNode" isEqualToString:node.name]) {
        return true;
    }
    if (node.parentNode != nil) {
        return [self isNodeCarModelObject:node.parentNode];
    }
    return false;
}

·給汽車模型換膚

給模型換膚原理就是修改汽車模型的材質貼圖,那么久同樣需要找到汽車車身的模型:


車身模型.png

上圖中,我們可以打開汽車模型,選中車身,從左側的模型列表中可以看到,車身的模型名稱為“body_01”,那么我們就先去除“body_01”的SCNNode節點。

//修改汽車顏色
            SCNNode *bodyNode = [weakSelf.carModelNode childNodeWithName:@"body_01" recursively:YES];
            bodyNode.childNodes[0].geometry.firstMaterial.diffuse.contents = color;//這里的顏色值可以設置純色或者設置圖片。這樣就達到了給汽車換皮膚的功能。

汽車換膚效果:


換膚.gif

·雙指捏合縮放模型、拖拽旋轉模型

首先捏合、拖拽就需要用到手勢,給SCNView添加手勢
縮放模型原理:當捏合開始時,記錄開始捏合時模型的縮放比例,然后在手勢變化的過程中計算當前手勢scale除以手勢開始時的scale, 以開始時模型的scale為基準相乘, 實現圓潤的放大縮小效果。這個比例大小可以自己調整,以達到自己理想的縮放范圍。

//給場景視圖添加手勢
- (void)addRecognizerToSceneView{
    UIPanGestureRecognizer *panGes = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panView:)];
    [self.sceneView addGestureRecognizer:panGes];
    UIPinchGestureRecognizer *pinchGes = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchView:)];
    [self.sceneView addGestureRecognizer:pinchGes];
}

監聽手勢觸發方法

// 處理拖拉手勢 - 移動 旋轉
- (void)panView:(UIPanGestureRecognizer *)panGestureRecognizer{
    if (self.modelShowing) {
        NSLog(@"拖拽.....................");
        UIView *view = panGestureRecognizer.view;
        CGPoint location = [panGestureRecognizer translationInView:self.sceneView];
        CGPoint velocityPoint = [panGestureRecognizer velocityInView:self.sceneView];
        switch (panGestureRecognizer.state) {
            case UIGestureRecognizerStateChanged:{
                
                //旋轉模型
                float xx = velocityPoint.x/5000;
                float yy = velocityPoint.y/5000;
                self.carModelNode.eulerAngles = SCNVector3Make(0, self.carModelNode.eulerAngles.y + (fabs(xx) > fabs(yy) ? xx : -yy), 0);
                
                break;
            }
            case UIGestureRecognizerStateEnded:{
                return;
            }
                
            default:{
                break;
            }
        }
    }
    
}

// 處理縮放手勢
CGFloat oldGesScale = Car_Model_Scale;
CGFloat oldModelScale = Car_Model_Scale;
- (void)pinchView:(UIPinchGestureRecognizer *)pinchGestureRecognizer{
    if (self.modelShowing){
//        NSLog(@"縮放.....................");
        if (pinchGestureRecognizer.state == UIGestureRecognizerStateBegan) {//手勢開始
            oldGesScale = pinchGestureRecognizer.scale;//手勢開始時,獲取模型的比例
            oldModelScale = self.carModelNode.scale.x;//手勢開始時,獲取模型的scale
        }
        
        if (pinchGestureRecognizer.state == UIGestureRecognizerStateChanged) {
            //計算, 當前手勢scale除以手勢開始時的scale, 以開始時模型的scale為基準相乘, 實現圓潤的放大縮小效果
            CGFloat currentGesScale = pinchGestureRecognizer.scale;
            CGFloat scale = oldModelScale *  (float)(currentGesScale / oldGesScale);
            scale = scale < 0.005 ? 0.005 : scale;
            scale = scale > 0.05 ? 0.05 : scale ;
            self.carModelNode.scale = SCNVector3Make(scale, scale, scale);
        }
        
    }
}

·汽車零件拆卸 - 拆卸輪胎

零件拆卸原理:同樣需要從模型中讀取輪胎的模型,同樣可以再模型中查看輪胎的模型名稱。輪胎模型又是由許多小零件組成,一般模型師會將其放在一個組內,組成一個輪胎模型:如下圖:輪胎模型組為"Group002"


輪胎模型.png

拿到輪胎模型后,進行拆卸動作:將模型進行位移和旋轉,造成輪胎與車身存在位置與角度的差別,從而實現輪胎(或其他零件)拆卸的功能。
同理,零件復原可以將拆卸下的零件經過位移和旋轉進行復位。
零件拆卸和復位方法:

/**
拆汽車零件 

@param sparePartsName 汽車零件模型名稱
@param spareDistance 拆卸偏離距離 (為0時,使用默認距離)
@param beFlip 是否翻轉模型
*/
- (void)removePartsCar:(NSString *)sparePartsName spareDistance:(CGFloat)spareDistance beFlip:(BOOL)beFlip{
   for (SCNNode *partsNode in self.carModelNode.childNodes) {
       if ([partsNode.name isEqualToString:sparePartsName]) {
           //找到對應的零件模型
           [UIView animateWithDuration:1.0 animations:^{
               //零件往外移動
               partsNode.position = SCNVector3Make(partsNode.position.x + spareDistance ,partsNode.position.y,partsNode.position.z);
           } completion:^(BOOL finished) {
               if (beFlip) {
                   //零件翻轉
                   partsNode.eulerAngles = SCNVector3Make(0, 0, M_PI/2);
               }
           }];
           
       }
   }
}

/**
安裝拆下的零件

@param sparePartsName 汽車零件模型名稱
@param spareDistance 拆卸偏離距離 (為0時,使用默認距離)
@param beFlip 是否翻轉模型
*/
- (void)recoveryPartsCar:(NSString *)sparePartsName spareDistance:(CGFloat)spareDistance beFlip:(BOOL)beFlip{
   for (SCNNode *partsNode in self.carModelNode.childNodes) {
       if ([partsNode.name isEqualToString:sparePartsName]) {
           //找到對應的零件模型 Group002:左前輪
           [UIView animateWithDuration:1.0 animations:^{
               if (beFlip) {
                   //零件翻轉
                   partsNode.eulerAngles = SCNVector3Make(0, 0, 0);
               }
           } completion:^(BOOL finished) {
               //零件回到原來位置
               partsNode.position = SCNVector3Make(partsNode.position.x - spareDistance ,partsNode.position.y,partsNode.position.z);
           }];
           
       }
   }
}

拆卸零件:


拆卸零件.gif

·輪胎運轉

拿到四個輪胎模型后,對齊進行旋轉。正常邏輯,輪胎旋轉是繞X軸進行無限循環轉動。使用貝塞爾動畫(CABasicAnimation)進行旋轉:

//開始輪胎轉動
- (void)startTireTurnningModel:(NSString *)modelName duration:(NSTimeInterval)duration{
    for (SCNNode *partsNode in self.carModelNode.childNodes) {
        if ([partsNode.name isEqualToString:modelName]) {
            //創建自轉動畫
            CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"rotation"];
            animation.duration = duration;
            animation.toValue = [NSValue valueWithSCNVector4:SCNVector4Make(0,1,0, M_PI *2)];
            animation.repeatCount = FLT_MAX;
            [partsNode addAnimation:animation forKey:@"tire rotation"];
            [partsNode runAction:[SCNAction repeatActionForever:[SCNAction rotateByX:2 y:0 z:0 duration:duration]]];//輪胎自轉 繞X軸自轉
        }
    }
}

想要停止輪胎轉動,移除其動畫:

//停止輪胎轉動
- (void)stopTireTurnningModel:(NSString *)modelName{
    for (SCNNode *partsNode in self.carModelNode.childNodes) {
        if ([partsNode.name isEqualToString:modelName]) {
            //需要同時remove Animation和Actions,只移除其中一個無效
            [partsNode removeAnimationForKey:@"tire rotation"];
            [partsNode removeAllActions];
        }
    }
}

輪胎運轉效果:


hou

·后視鏡折疊與車窗升降效果的實現:

后視鏡開合的原理與輪胎轉動的原理是一樣的,位移的差別就是圍繞的旋轉軸(后視鏡圍繞Y軸旋轉,右手坐標系)、旋轉角度、旋轉次數不一樣:

//合上后視鏡
- (void)closeRearviewMirrorModel:(NSString *)modelName angle:(CGFloat)angle Duration:(NSTimeInterval)duration{
    for (SCNNode *partsNode in self.carModelNode.childNodes) {
        if ([partsNode.name isEqualToString:modelName]) {
            
            //創建自轉動畫
            CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"rotation"];//執行的是旋轉
            animation.duration = duration;
//            animation.toValue = [NSValue valueWithSCNVector4:SCNVector4Make(0,0,0, 0)];//旋轉角度
            animation.repeatCount = 1;
            [partsNode addAnimation:animation forKey:@"rearviewMirror rotation"];
            [partsNode runAction:[SCNAction repeatAction:[SCNAction rotateByX:0 y:angle z:0 duration:duration] count:1]];//后視鏡繞Y軸旋轉 angle角度
        }
    }
}

//打開后視鏡
- (void)openRearviewMirrorModel:(NSString *)modelName angle:(CGFloat)angle Duration:(NSTimeInterval)duration{
    for (SCNNode *partsNode in self.carModelNode.childNodes) {
        if ([partsNode.name isEqualToString:modelName]) {
            //創建自轉動畫
            CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"rotation"];
            animation.duration = duration;
//            animation.toValue = [NSValue valueWithSCNVector4:SCNVector4Make(0,1,0, 0)];
            animation.repeatCount = 1;
            [partsNode addAnimation:animation forKey:@"rearviewMirror2 rotation"];
            [partsNode runAction:[SCNAction repeatAction:[SCNAction rotateByX:0 y:angle z:0 duration:duration] count:1]];//后視鏡繞Y軸旋轉
        }
    }
}

后視鏡折疊:


后視鏡.gif

而車窗升降與后視鏡旋轉存在不一樣的地方是,車窗的升降使用的是CABasicAnimation的平移而不是旋轉。

CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];//執行平移動畫

核心代碼是從哪里(fromValue)移動到哪里(toValue):

/**
 降下車窗

 @param modelName 模型對象名稱
 @param duration 執行周期
 */
- (void)downWindowsWithModelName:(NSString *)modelName offsetY:(CGFloat)offsetY duration:(NSTimeInterval)duration{
    for (SCNNode *windowNode in self.carModelNode.childNodes) {
        if ([windowNode.name isEqualToString:modelName]) {
            //創建自轉動畫
            CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];//執行平移動畫
            animation.duration = duration;
            animation.toValue = [NSValue valueWithSCNVector3:SCNVector3Make(windowNode.position.x, windowNode.position.y-offsetY, windowNode.position.z)];
            animation.removedOnCompletion = NO;
            animation.fillMode = @"forwards";
            [windowNode addAnimation:animation forKey:@"window position"];
        }
    }
}

//升起車窗
- (void)upWindowsWithModelName:(NSString *)modelName offsetY:(CGFloat)offsetY duration:(NSTimeInterval)duration{
    for (SCNNode *windowNode in self.carModelNode.childNodes) {
        if ([windowNode.name isEqualToString:modelName]) {
            //創建自轉動畫
            CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];//執行平移動畫
            animation.duration = duration;
            animation.fromValue = [NSValue valueWithSCNVector3:SCNVector3Make(windowNode.position.x, windowNode.position.y-offsetY, windowNode.position.z)];
            animation.toValue = [NSValue valueWithSCNVector3:SCNVector3Make(windowNode.position.x, windowNode.position.y, windowNode.position.z)];
            animation.removedOnCompletion = NO;
            animation.fillMode = @"forwards";
            [windowNode addAnimation:animation forKey:@"window position"];
        }
    }
}

升降車窗效果:


車窗升降.gif

本文Demo Git下載地址:https://github.com/heqican/ARKitCarModel

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。