本章實現對模型(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;
}
·給汽車模型換膚
給模型換膚原理就是修改汽車模型的材質貼圖,那么久同樣需要找到汽車車身的模型:
上圖中,我們可以打開汽車模型,選中車身,從左側的模型列表中可以看到,車身的模型名稱為“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