在学习完 RealityKit 进行物理模拟的相关理论知识后,下面通过使用 PhysicsBodyComponent 组件进行物理模拟演示,主要代码如下所示,稍后对代码进行详细解析。
//
// PhysicsBodyView.swift
// ARKitDeamo
//
// Created by zhaoquan du on 2024/3/14.
//
import SwiftUI
import ARKit
import RealityKit
struct PhysicsBodyView: View {
var body: some View {
PhysicsBodyViewContainer().navigationTitle("物理模拟").edgesIgnoringSafeArea(.all)
}
}
struct PhysicsBodyViewContainer:UIViewRepresentable {
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeUIView(context: Context) -> some ARView {
let arView = ARView(frame: .zero)
let config = ARWorldTrackingConfiguration()
config.planeDetection = .horizontal
context.coordinator.arView = arView
arView.session.delegate = context.coordinator
arView.session.run(config)
return arView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
class Coordinator: NSObject, ARSessionDelegate{
var sphereEntity : ModelEntity!
var arView:ARView? = nil
let gameController = GameController()
func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
guard let anchor = anchors.first as? ARPlaneAnchor,
let arView = arView else{
return
}
let planeAnchor = AnchorEntity(anchor:anchor)
//sample1
let boxCollider: ShapeResource = .generateBox(size: [0.1,0.2,1])
let box: MeshResource = .generateBox(size: [0.1,0.2,1], cornerRadius: 0.02)
let boxMaterial = SimpleMaterial(color: .yellow,isMetallic: true)
let boxEntity = ModelEntity(mesh: box, materials: [boxMaterial], collisionShape: boxCollider, mass: 0.05)
boxEntity.physicsBody?.mode = .dynamic
boxEntity.name = "Box"
boxEntity.transform.translation = [0.2,planeAnchor.transform.translation.y+0.15,0]
let sphereCollider : ShapeResource = .generateSphere(radius: 0.05)
let sphere: MeshResource = .generateSphere(radius: 0.05)
let sphereMaterial = SimpleMaterial(color:.red,isMetallic: true)
sphereEntity = ModelEntity(mesh: sphere, materials: [sphereMaterial], collisionShape: sphereCollider, mass: 0.04)
sphereEntity.physicsBody?.mode = .dynamic
sphereEntity.name = "Sphere"
sphereEntity.transform.translation = [-0.3,planeAnchor.transform.translation.y+0.15,0]
sphereEntity.physicsBody?.material = .generate(friction: 0.001, restitution: 0.01)
//平面
let plane :MeshResource = .generatePlane(width: 1.2, depth: 1.2)
let planeCollider : ShapeResource = .generateBox(width: 1.2, height: 0.01, depth: 1.2)
let planeMaterial = SimpleMaterial(color:.gray,isMetallic: false)
let planeEntity = ModelEntity(mesh: plane, materials: [planeMaterial], collisionShape: planeCollider, mass: 0.01)
planeEntity.physicsBody?.mode = .static//静态平面不具备碰撞性
planeEntity.physicsBody?.material = .generate(friction: 0.001, restitution: 0.1)
planeAnchor.addChild(planeEntity)
planeAnchor.addChild(boxEntity)
planeAnchor.addChild(sphereEntity)
//添加碰撞订阅
let subscription = arView.scene.subscribe(to: CollisionEvents.Began.self, { event in
print("box发生碰撞")
print("entityA.name: \(event.entityA.name)")
print("entityB.name: \(event.entityB.name)")
print("Force : \(event.impulse)")
print("Collision Position: \(event.position)")
})
gameController.collisionEventStreams.append(subscription)
arView.scene.addAnchor(planeAnchor)
let gestureRecognizers = arView.installGestures(.translation, for: sphereEntity)
if let gestureRecognizer = gestureRecognizers.first as? EntityTranslationGestureRecognizer {
gameController.gestureRecognizer = gestureRecognizer
gestureRecognizer.removeTarget(nil, action: nil)
gestureRecognizer.addTarget(self, action: #selector(handleTranslation))
}
arView.session.delegate = nil
arView.session.run(ARWorldTrackingConfiguration())
}
@objc func handleTranslation(_ recognizer: EntityTranslationGestureRecognizer){
guard let ball = sphereEntity else { return }
let settings = gameController.settings
if recognizer.state == .ended || recognizer.state == .cancelled {
gameController.gestureStartLocation = nil
ball.physicsBody?.mode = .dynamic
return
}
guard let gestureCurrentLocation = recognizer.translation(in: nil) else { return }
guard let gestureStartLocation = gameController.gestureStartLocation else {
gameController.gestureStartLocation = gestureCurrentLocation
return
}
let delta = gestureStartLocation - gestureCurrentLocation
let distance = ((delta.x * delta.x) + (delta.y * delta.y) + (delta.z * delta.z)).squareRoot()
if distance > settings.ballPlayDistanceThreshold {
gameController.gestureStartLocation = nil
ball.physicsBody?.mode = .dynamic
return
}
//ball.physicsBody?.mode = .kinematic
let realVelocity = recognizer.velocity(in: nil)
let ballParentVelocity = ball.parent!.convert(direction: realVelocity, from: nil)
var clampedX = ballParentVelocity.x
var clampedZ = ballParentVelocity.z
// 夹断
if clampedX > settings.ballVelocityMaxX {
clampedX = settings.ballVelocityMaxX
} else if clampedX < settings.ballVelocityMinX {
clampedX = settings.ballVelocityMinX
}
// 夹断
if clampedZ > settings.ballVelocityMaxZ {
clampedZ = settings.ballVelocityMaxZ
} else if clampedZ < settings.ballVelocityMinZ {
clampedZ = settings.ballVelocityMinZ
}
let clampedVelocity: SIMD3<Float> = [clampedX, 0.0, clampedZ]
//ball.physicsMotion?.linearVelocity = clampedVelocity
ball.addForce(clampedVelocity*0.1, relativeTo: nil)
}
}
}
#Preview {
PhysicsBodyView()
}
在代码清单中,实现的功能如下:
(1)构建模拟环境。
(2)通过施加力,对物体运动进行物理模拟。
在功能1中,我们通过 session(_ session:ARSession, didAdd anchors: [ARAnchor])方法对平面检测情况进行监视,当 ARKit 检测到符合要求的水平平面后,手动生成一个长方体、一个球体、一个承载这两个物体的平面,构建了基本的模拟环境,如图所示。由于生成的长方体与球体均是带有质量与碰撞器的实体,在使用物理引擎时,它们会在重力作用下下坠,生成的平面主要用于承载这两个物体。在设置好物理模拟相关属性后,我们还订阅(subscriptions)了长方体的碰撞事件,当长方体与其他物体发生碰撞时会打印出发生碰撞的两个实体对象名称、碰撞时的受力和碰撞位置信息。
在功能2中,为方便控制,我们使用了 RealityKit 中的平移手势(Entity Lranslation GrestureRecogniner),通过计算使用者手指在犀幕上滑动的速度生威作用力,并将该作用力施加在球体上,通过施加作用力就可以观察球体与长方体在物理引擎作用下的运动效果(为防止施加的力过大,我们使用了GameSettings结构体并定义了几个边界值,具体可以参看本节源码)。
编译后测试,使用平移手势操作球体,当球体撞击到长方体后,会发生物理交互并触发长方体的碰撞事件。读者可以修改使用不同的物理参数和碰撞形状,看一看物理参数如何影响物体的运动,以及碰撞形状如何影响碰撞位置。
这个例子综合演示了物理参数和属性的设置、物理事件的处理、物理材质对物理模拟的影响,同时也是最简单的物理引擎使用案例,没有使用 group 和 mask设置碰撞分组,仅演示了 PhysicsBodyComponent组件的最基本使用方法。