本章实现对模型(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;
}
·给汽车模型换肤
给模型换肤原理就是修改汽车模型的材质贴图,那么久同样需要找到汽车车身的模型:
上图中,我们可以打开汽车模型,选中车身,从左侧的模型列表中可以看到,车身的模型名称为“body_01”,那么我们就先去除“body_01”的SCNNode节点。
//修改汽车颜色
SCNNode *bodyNode = [weakSelf.carModelNode childNodeWithName:@"body_01" recursively:YES];
bodyNode.childNodes[0].geometry.firstMaterial.diffuse.contents = color;//这里的颜色值可以设置纯色或者设置图片。这样就达到了给汽车换皮肤的功能。
汽车换肤效果:
·双指捏合缩放模型、拖拽旋转模型
首先捏合、拖拽就需要用到手势,给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"
拿到轮胎模型后,进行拆卸动作:将模型进行位移和旋转,造成轮胎与车身存在位置与角度的差别,从而实现轮胎(或其他零件)拆卸的功能。
同理,零件复原可以将拆卸下的零件经过位移和旋转进行复位。
零件拆卸和复位方法:
/**
拆汽车零件
@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);
}];
}
}
}
拆卸零件:
·轮胎运转
拿到四个轮胎模型后,对齐进行旋转。正常逻辑,轮胎旋转是绕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];
}
}
}
轮胎运转效果:
·后视镜折叠与车窗升降效果的实现:
后视镜开合的原理与轮胎转动的原理是一样的,位移的差别就是围绕的旋转轴(后视镜围绕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轴旋转
}
}
}
后视镜折叠:
而车窗升降与后视镜旋转存在不一样的地方是,车窗的升降使用的是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"];
}
}
}
升降车窗效果:
本文Demo Git下载地址:https://github.com/heqican/ARKitCarModel