在上一篇中我们通过ARKit检测现实中的水平屏幕并覆盖上网格,接着上一篇的内容继续进行实践,在之前的平台检测中通过点击屏幕加入一些虚拟正方体,实现物理检测。
Hit Testing
正如上一篇中看到的,我们可以在任何X、Y、Z位置插入虚拟3D内容,它将在现实世界中呈现和跟踪。现在我们有了平面检测,我们想添加与这些平面相互交互的内容。在这个项目中,我另开了一个分支AR_Physics,实现的内容有。
当点击屏幕时,会执行一个hit test,会从网格上方掉落正方体。这里设计到2D屏幕坐标,这涉及到2D屏幕坐标和3D坐标,通过2D屏幕点(在投影平面上有一个3D位置)从相机原点发射一束光线到场景中。如果射线与任何平面相交,我们得到一个坠落点,然后我们取三维坐标,在那里射线和平面相交并将我们的正方体容放置在那个3D位置。 创建单击长按等的手势不多说,代码中也有。ARSCNView有一个hitTest方法,通过传入点击的2D左边返回一个一个3D坐标数组NSArray
- 单击屏幕的方法
//单击方法
- (void)handleTapFrom: (UITapGestureRecognizer *)recognizer {
//获取屏幕空间tap坐标,并将它传递给ARSCNView的hitTest方法
CGPoint tapPoint = [recognizer locationInView:self.sceneView];
NSArray *result = [self.sceneView hitTest:tapPoint types:ARHitTestResultTypeExistingPlaneUsingExtent];
if (result.count == 0) {
return;
}
// 插入正方体
ARHitTestResult * hitResult = [result firstObject];
[self insertGeometry:hitResult];
}
复制代码
// 插入正方体
- (void)insertGeometry:(ARHitTestResult *)hitResult {
float dimension = 0.1;
SCNBox *cube = [SCNBox boxWithWidth:dimension height:dimension length:dimension chamferRadius:0];
SCNNode *node = [SCNNode nodeWithGeometry:cube];
// SCNPhysicsBody告诉SceneKit这个几何图形应该被物理引擎操纵
node.physicsBody = [SCNPhysicsBody bodyWithType:SCNPhysicsBodyTypeDynamic shape:nil];
node.physicsBody.mass = 2.0;
node.physicsBody.categoryBitMask = CollisionCategoryCube;
// 将几何图形略高于用户点击的点,这样就可以制造出正方体在平面上降落的效果
float insertionYOffset = 0.5;
node.position = SCNVector3Make(
hitResult.worldTransform.columns[3].x,
hitResult.worldTransform.columns[3].y + insertionYOffset,
hitResult.worldTransform.columns[3].z
);
[self.sceneView.scene.rootNode addChildNode:node];
[self.boxes addObject:node];
}
复制代码
为了效果看起来更加接近真实,我们会增加一些物理学来展示一种重力感。 在Plane类中,为每一个正方体加上physicsBody,表明正方体由SceneKit的物理引擎控制,更多的细节查看Plane.m中的代码
- 清除world平面中的正方体
通过一个手指长按,所有的正方体呈现爆炸的方式掉落到最下面的节点平面上, 首先,需要创造一个bottomNode,当所有的正方体和这个bottomNode产生碰撞时,便表示掉落出我们所创建的虚拟world,移除掉落的正方体
//将一个大的节点放到虚拟世界的下面,当正方体爆炸掉落到这个节点上时,就将正方体移除
SCNBox *bottomPlane = [SCNBox boxWithWidth:1000 height:0.5 length:1000 chamferRadius:0];
SCNMaterial *bottomMaterial = [SCNMaterial new];
bottomMaterial.diffuse.contents = [UIColor colorWithWhite:1.0 alpha:0.2];
bottomPlane.materials = @[bottomMaterial];
SCNNode *bottomNode = [SCNNode nodeWithGeometry:bottomPlane];
bottomNode.position = SCNVector3Make(0, -10, 0);
bottomNode.physicsBody = [SCNPhysicsBody
bodyWithType:SCNPhysicsBodyTypeKinematic
shape: nil];
bottomNode.physicsBody.categoryBitMask = CollisionCategoryBottom;
bottomNode.physicsBody.contactTestBitMask = CollisionCategoryCube;
[self.sceneView.scene.rootNode addChildNode:bottomNode];
self.sceneView.scene.physicsWorld.contactDelegate = self;
复制代码
一个手指长按的方法
//一个手指长按方法
- (void)handleHoldFrom: (UILongPressGestureRecognizer *)recognizer {
if (recognizer.state != UIGestureRecognizerStateBegan) {
return;
}
//使用屏幕坐标执行 hit test,以查看是否点击了平面
CGPoint holdPoint = [recognizer locationInView:self.sceneView];
NSArray *result = [self.sceneView hitTest:holdPoint types:ARHitTestResultTypeExistingPlaneUsingExtent];
if (result.count == 0) {
return;
}
//将正方体以爆炸的方式清除
ARHitTestResult * hitResult = [result firstObject];
dispatch_async(dispatch_get_main_queue(), ^{
[self explode:hitResult];
});
}
复制代码
//爆炸的方法
- (void)explode:(ARHitTestResult *)hitResult {
float explosionYOffset = 0.1;
//取explosion在worldTransform的坐标
SCNVector3 position = SCNVector3Make(
hitResult.worldTransform.columns[3].x,
hitResult.worldTransform.columns[3].y - explosionYOffset,
hitResult.worldTransform.columns[3].z
);
//取每一个正方体的坐标,然后计算正方体和博炸点的距离
for (SCNNode *cubeNode in self.boxes) {
SCNVector3 distance = SCNVector3Make(cubeNode.worldPosition.x - position.x,
cubeNode.worldPosition.y - position.y,
cubeNode.worldPosition.z - position.z);
float length = sqrtf(distance.x * distance.x + distance.y * distance.y + distance.z * distance.z);
//设置最大距离,当距离超过maxDistance后,便不会受到force的影响
float maxDistance = 2;
float scale = MAX(0, (maxDistance - length));
scale = scale * scale * 2;
// 将距离矢量缩放到合适的尺度
distance.x = distance.x / length * scale;
distance.y = distance.y / length * scale;
distance.z = distance.z / length * scale;
// 对几何图形施加一个force,将force设置到正方体的角使其旋转
[cubeNode.physicsBody applyForce:distance atPosition:SCNVector3Make(0.05, 0.05, 0.05) impulse:YES];
}
}
复制代码
- 清除网格平面
通过两个手指长按,移除平面并停止检测平面
//两个手指长按方法
- (void)handleHidePlaneFrom: (UILongPressGestureRecognizer *)recognizer {
if (recognizer.state != UIGestureRecognizerStateBegan) {
return;
}
//隐藏所有平面
for(NSUUID *planeId in self.planes) {
[self.planes[planeId] hide];
}
//停止检测或者更新存在的平面
ARWorldTrackingConfiguration *configuration = (ARWorldTrackingConfiguration *)self.sceneView.session.configuration;
configuration.planeDetection = ARPlaneDetectionNone;
[self.sceneView.session runWithConfiguration:configuration];
}
复制代码
- 正方体和bottomNode的碰撞检测
#pragma mark - SCNPhysicsContactDelegate
- (void)physicsWorld:(SCNPhysicsWorld *)world didBeginContact:(SCNPhysicsContact *)contact{
//检测正方体和下面底部的碰撞,当正方体掉到了bottomNode下面,就移除正方体
CollisionCategory contactMask = contact.nodeA.physicsBody.categoryBitMask | contact.nodeB.physicsBody.categoryBitMask;
if (contactMask == (CollisionCategoryBottom | CollisionCategoryCube)) {
if (contact.nodeA.physicsBody.categoryBitMask == CollisionCategoryBottom) {
[contact.nodeB removeFromParentNode];
} else {
[contact.nodeA removeFromParentNode];
}
}
}
复制代码
项目github地址在上一个项目的AR_Physics分支。