美文网首页SceneKit + ARKitiOS 好东西
ios-ARKit 实现与3D模型交互(模型换肤、运动)

ios-ARKit 实现与3D模型交互(模型换肤、运动)

作者: 爱迪生的小跟班 | 来源:发表于2019-04-29 17:30 被阅读10次

    本章实现对模型(Demo中用了一个汽车模型)的交互操作,包括对汽车模型换肤、零件拆卸、轮胎运转、后视镜开合、以及车窗的升降等等。在与AR世界的交互之前对AR世界的构建,以及模型的展示在另一篇文章中(AR世界的构建:https://www.jianshu.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

    相关文章

      网友评论

        本文标题:ios-ARKit 实现与3D模型交互(模型换肤、运动)

        本文链接:https://www.haomeiwen.com/subject/ukpngqtx.html