本章實(shí)現(xiàn)對(duì)模型(Demo中用了一個(gè)汽車模型)的交互操作澡谭,包括對(duì)汽車模型換膚、零件拆卸损俭、輪胎運(yùn)轉(zhuǎn)蛙奖、后視鏡開(kāi)合、以及車窗的升降等等杆兵。在與AR世界的交互之前對(duì)AR世界的構(gòu)建雁仲,以及模型的展示在另一篇文章中(AR世界的構(gòu)建:http://www.reibang.com/p/f7c26b058348)這里就不再講述。
構(gòu)建出AR世界并且在AR世界中展示3D模型后就可以開(kāi)始對(duì)模型進(jìn)行各種操作及交互:
思考:如何實(shí)現(xiàn)對(duì)汽車模型或者其身上子模型部件進(jìn)行操作琐脏?
一個(gè)復(fù)雜模型的制作原理是由多個(gè)材質(zhì)球或者模型(SceneKit中的節(jié)點(diǎn)SCNNode)拼接而成的的攒砖。在SceneKit中我們可以通過(guò)檢索模型的名稱對(duì)其進(jìn)行交互。
Demo中相關(guān)屬性:列出以便文章閱讀
@property (nonatomic, strong) UIButton *backButton;//返回按鈕
@property (nonatomic, strong) ARSCNView *sceneView;//AR視圖(AR場(chǎng)景填在在其上)
@property (nonatomic, strong) ARWorldTrackingConfiguration *configuration;//AR世界追蹤
@property (nonatomic, strong) SCNScene *scene;//AR場(chǎng)景
@property (nonatomic, strong) ARPlaneAnchor *planAnchor;//平面錨點(diǎn)
@property (nonatomic, strong) SCNNode *planParanNode;//地面節(jié)點(diǎn)(模型放上面)
@property (nonatomic, assign) BOOL modelShowing;//是否已經(jīng)顯示模型(已經(jīng)顯示模型后不繼續(xù)重新布置平面)
@property (nonatomic, assign) BOOL isSeachPlan;//是否已經(jīng)找到平面
@property (nonatomic, strong) SCNNode *carModelNode;//汽車模型節(jié)點(diǎn)
@property (nonatomic, assign) BOOL tireSpared;//是否已經(jīng)拆下輪胎
//顏色面板
@property (nonatomic, strong) HCColorPanelView *colorPanelView;
//菜單面板
@property (nonatomic, strong) UIButton *menuButton;
@property (nonatomic, strong) HCMenuPanelView *menuPanelView;
·碰撞檢測(cè) (點(diǎn)擊手機(jī)屏幕日裙,檢測(cè)是否點(diǎn)擊了模型)
給汽車模型起個(gè)名字:
self.carModelNode.name = @"modelCarNode";//很重要吹艇,根據(jù)這個(gè)那么做對(duì)比,是否點(diǎn)擊了模型
點(diǎn)擊屏幕后監(jiān)聽(tīng)- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event方法昂拂,遍歷點(diǎn)擊事件受神,檢測(cè)是否與模型進(jìn)行了碰撞:
//點(diǎn)擊檢測(cè)(碰撞檢測(cè))
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
if (self.arType == ARWorldTrackingConfigurationType_planeDetection_CarDemo && self.modelShowing) {
//已經(jīng)放置了汽車模型,檢測(cè)點(diǎn)擊汽車事件
UITouch *touch = [touches anyObject];
CGPoint tapPoint = [touch locationInView:self.sceneView];//該點(diǎn)就是手指的點(diǎn)擊位置
NSDictionary *hitTestOptions = [NSDictionary dictionaryWithObjectsAndKeys:@(true),SCNHitTestBoundingBoxOnlyKey, nil];
NSArray<SCNHitTestResult *> * results= [self.sceneView hitTest:tapPoint options:hitTestOptions];
for (SCNHitTestResult *res in results) {//遍歷所有的返回結(jié)果中的node
if ([self isNodeCarModelObject:res.node]) {
// [[HCToast shareInstance] showToast:@"點(diǎn)擊了汽車"];
NSLog(@"點(diǎn)擊了汽車模型...............");
break;
}
}
}
}
//上溯找尋指定的node(是否點(diǎn)擊了汽車)
-(BOOL) isNodeCarModelObject:(SCNNode*)node {
if ([@"modelCarNode" isEqualToString:node.name]) {
return true;
}
if (node.parentNode != nil) {
return [self isNodeCarModelObject:node.parentNode];
}
return false;
}
·給汽車模型換膚
給模型換膚原理就是修改汽車模型的材質(zhì)貼圖格侯,那么久同樣需要找到汽車車身的模型:
上圖中鼻听,我們可以打開(kāi)汽車模型,選中車身联四,從左側(cè)的模型列表中可以看到撑碴,車身的模型名稱為“body_01”,那么我們就先去除“body_01”的SCNNode節(jié)點(diǎn)朝墩。
//修改汽車顏色
SCNNode *bodyNode = [weakSelf.carModelNode childNodeWithName:@"body_01" recursively:YES];
bodyNode.childNodes[0].geometry.firstMaterial.diffuse.contents = color;//這里的顏色值可以設(shè)置純色或者設(shè)置圖片醉拓。這樣就達(dá)到了給汽車換皮膚的功能。
汽車換膚效果:
·雙指捏合縮放模型、拖拽旋轉(zhuǎn)模型
首先捏合廉嚼、拖拽就需要用到手勢(shì)玫镐,給SCNView添加手勢(shì)
縮放模型原理:當(dāng)捏合開(kāi)始時(shí),記錄開(kāi)始捏合時(shí)模型的縮放比例怠噪,然后在手勢(shì)變化的過(guò)程中計(jì)算當(dāng)前手勢(shì)scale除以手勢(shì)開(kāi)始時(shí)的scale, 以開(kāi)始時(shí)模型的scale為基準(zhǔn)相乘, 實(shí)現(xiàn)圓潤(rùn)的放大縮小效果。這個(gè)比例大小可以自己調(diào)整杜跷,以達(dá)到自己理想的縮放范圍傍念。
//給場(chǎng)景視圖添加手勢(shì)
- (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];
}
監(jiān)聽(tīng)手勢(shì)觸發(fā)方法
// 處理拖拉手勢(shì) - 移動(dòng) 旋轉(zhuǎn)
- (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:{
//旋轉(zhuǎn)模型
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;
}
}
}
}
// 處理縮放手勢(shì)
CGFloat oldGesScale = Car_Model_Scale;
CGFloat oldModelScale = Car_Model_Scale;
- (void)pinchView:(UIPinchGestureRecognizer *)pinchGestureRecognizer{
if (self.modelShowing){
// NSLog(@"縮放.....................");
if (pinchGestureRecognizer.state == UIGestureRecognizerStateBegan) {//手勢(shì)開(kāi)始
oldGesScale = pinchGestureRecognizer.scale;//手勢(shì)開(kāi)始時(shí),獲取模型的比例
oldModelScale = self.carModelNode.scale.x;//手勢(shì)開(kāi)始時(shí)葛闷,獲取模型的scale
}
if (pinchGestureRecognizer.state == UIGestureRecognizerStateChanged) {
//計(jì)算, 當(dāng)前手勢(shì)scale除以手勢(shì)開(kāi)始時(shí)的scale, 以開(kāi)始時(shí)模型的scale為基準(zhǔn)相乘, 實(shí)現(xiàn)圓潤(rùn)的放大縮小效果
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);
}
}
}
·汽車零件拆卸 - 拆卸輪胎
零件拆卸原理:同樣需要從模型中讀取輪胎的模型憋槐,同樣可以再模型中查看輪胎的模型名稱。輪胎模型又是由許多小零件組成淑趾,一般模型師會(huì)將其放在一個(gè)組內(nèi)阳仔,組成一個(gè)輪胎模型:如下圖:輪胎模型組為"Group002"
拿到輪胎模型后,進(jìn)行拆卸動(dòng)作:將模型進(jìn)行位移和旋轉(zhuǎn)扣泊,造成輪胎與車身存在位置與角度的差別近范,從而實(shí)現(xiàn)輪胎(或其他零件)拆卸的功能。
同理延蟹,零件復(fù)原可以將拆卸下的零件經(jīng)過(guò)位移和旋轉(zhuǎn)進(jìn)行復(fù)位评矩。
零件拆卸和復(fù)位方法:
/**
拆汽車零件
@param sparePartsName 汽車零件模型名稱
@param spareDistance 拆卸偏離距離 (為0時(shí),使用默認(rèn)距離)
@param beFlip 是否翻轉(zhuǎn)模型
*/
- (void)removePartsCar:(NSString *)sparePartsName spareDistance:(CGFloat)spareDistance beFlip:(BOOL)beFlip{
for (SCNNode *partsNode in self.carModelNode.childNodes) {
if ([partsNode.name isEqualToString:sparePartsName]) {
//找到對(duì)應(yīng)的零件模型
[UIView animateWithDuration:1.0 animations:^{
//零件往外移動(dòng)
partsNode.position = SCNVector3Make(partsNode.position.x + spareDistance ,partsNode.position.y,partsNode.position.z);
} completion:^(BOOL finished) {
if (beFlip) {
//零件翻轉(zhuǎn)
partsNode.eulerAngles = SCNVector3Make(0, 0, M_PI/2);
}
}];
}
}
}
/**
安裝拆下的零件
@param sparePartsName 汽車零件模型名稱
@param spareDistance 拆卸偏離距離 (為0時(shí)阱飘,使用默認(rèn)距離)
@param beFlip 是否翻轉(zhuǎn)模型
*/
- (void)recoveryPartsCar:(NSString *)sparePartsName spareDistance:(CGFloat)spareDistance beFlip:(BOOL)beFlip{
for (SCNNode *partsNode in self.carModelNode.childNodes) {
if ([partsNode.name isEqualToString:sparePartsName]) {
//找到對(duì)應(yīng)的零件模型 Group002:左前輪
[UIView animateWithDuration:1.0 animations:^{
if (beFlip) {
//零件翻轉(zhuǎn)
partsNode.eulerAngles = SCNVector3Make(0, 0, 0);
}
} completion:^(BOOL finished) {
//零件回到原來(lái)位置
partsNode.position = SCNVector3Make(partsNode.position.x - spareDistance ,partsNode.position.y,partsNode.position.z);
}];
}
}
}
拆卸零件:
·輪胎運(yùn)轉(zhuǎn)
拿到四個(gè)輪胎模型后斥杜,對(duì)齊進(jìn)行旋轉(zhuǎn)。正常邏輯沥匈,輪胎旋轉(zhuǎn)是繞X軸進(jìn)行無(wú)限循環(huán)轉(zhuǎn)動(dòng)蔗喂。使用貝塞爾動(dòng)畫(CABasicAnimation)進(jìn)行旋轉(zhuǎn):
//開(kāi)始輪胎轉(zhuǎn)動(dòng)
- (void)startTireTurnningModel:(NSString *)modelName duration:(NSTimeInterval)duration{
for (SCNNode *partsNode in self.carModelNode.childNodes) {
if ([partsNode.name isEqualToString:modelName]) {
//創(chuàng)建自轉(zhuǎn)動(dòng)畫
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]]];//輪胎自轉(zhuǎn) 繞X軸自轉(zhuǎn)
}
}
}
想要停止輪胎轉(zhuǎn)動(dòng),移除其動(dòng)畫:
//停止輪胎轉(zhuǎn)動(dòng)
- (void)stopTireTurnningModel:(NSString *)modelName{
for (SCNNode *partsNode in self.carModelNode.childNodes) {
if ([partsNode.name isEqualToString:modelName]) {
//需要同時(shí)remove Animation和Actions高帖,只移除其中一個(gè)無(wú)效
[partsNode removeAnimationForKey:@"tire rotation"];
[partsNode removeAllActions];
}
}
}
輪胎運(yùn)轉(zhuǎn)效果:
·后視鏡折疊與車窗升降效果的實(shí)現(xiàn):
后視鏡開(kāi)合的原理與輪胎轉(zhuǎn)動(dòng)的原理是一樣的缰儿,位移的差別就是圍繞的旋轉(zhuǎn)軸(后視鏡圍繞Y軸旋轉(zhuǎn),右手坐標(biāo)系)棋恼、旋轉(zhuǎn)角度返弹、旋轉(zhuǎn)次數(shù)不一樣:
//合上后視鏡
- (void)closeRearviewMirrorModel:(NSString *)modelName angle:(CGFloat)angle Duration:(NSTimeInterval)duration{
for (SCNNode *partsNode in self.carModelNode.childNodes) {
if ([partsNode.name isEqualToString:modelName]) {
//創(chuàng)建自轉(zhuǎn)動(dòng)畫
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"rotation"];//執(zhí)行的是旋轉(zhuǎn)
animation.duration = duration;
// animation.toValue = [NSValue valueWithSCNVector4:SCNVector4Make(0,0,0, 0)];//旋轉(zhuǎn)角度
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軸旋轉(zhuǎn) angle角度
}
}
}
//打開(kāi)后視鏡
- (void)openRearviewMirrorModel:(NSString *)modelName angle:(CGFloat)angle Duration:(NSTimeInterval)duration{
for (SCNNode *partsNode in self.carModelNode.childNodes) {
if ([partsNode.name isEqualToString:modelName]) {
//創(chuàng)建自轉(zhuǎn)動(dòng)畫
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軸旋轉(zhuǎn)
}
}
}
后視鏡折疊:
而車窗升降與后視鏡旋轉(zhuǎn)存在不一樣的地方是,車窗的升降使用的是CABasicAnimation的平移而不是旋轉(zhuǎn)爪飘。
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];//執(zhí)行平移動(dòng)畫
核心代碼是從哪里(fromValue)移動(dòng)到哪里(toValue):
/**
降下車窗
@param modelName 模型對(duì)象名稱
@param duration 執(zhí)行周期
*/
- (void)downWindowsWithModelName:(NSString *)modelName offsetY:(CGFloat)offsetY duration:(NSTimeInterval)duration{
for (SCNNode *windowNode in self.carModelNode.childNodes) {
if ([windowNode.name isEqualToString:modelName]) {
//創(chuàng)建自轉(zhuǎn)動(dòng)畫
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];//執(zhí)行平移動(dòng)畫
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]) {
//創(chuàng)建自轉(zhuǎn)動(dòng)畫
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];//執(zhí)行平移動(dòng)畫
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