【iOS ARKit】PhysicsMotionComponent

使用 Physics BodyComponent 组件,通过设置物理参数、物理材质、施加作用力,能完全模拟物体在真实世界中的行为,这种方式的优点是遵循物理学规律、控制精确,但缺点是不直观。使用 PhysicsMotion Component组件则可以通过直接设置速度进行物理模拟,但需要明白的是,对物体施加力与设置物体速度是两种完全不同且不相容的操作,无法混合使用。

下面我们使用 PhysicsMotionComponent组件进行演示。在代上节码中,我们手工构建了模拟环境,这是件枯燥且容易出错的工作,而且很难构建复杂的场景,利用 Reality Composer 工具则可以快速地构建场最模型,本示例我们先使用 Reality Composer 构建基本的场景,然后通过设置速度的方式进行物理模拟。

利用 Reality Composer 工具设置好各实体的大小、物理材质、碰撞属性和位置关系,然后在 Xcode 中导入 Reality 场景,具体代码如下。

//
//  PhysicsMotionView.swift
//  ARKitDeamo
//
//  Created by zhaoquan du on 2024/3/14.
//

import SwiftUI
import RealityKit
import ARKit

struct PhysicsMotionView: View {
    var body: some View {
        PhysicsMotionViewContainer().navigationTitle("物理模拟2").edgesIgnoringSafeArea(.all)
    }
}

struct PhysicsMotionViewContainer: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
        context.coordinator.loadModel()
        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()
        
        @MainActor func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
            guard let anchor = anchors.first as? ARPlaneAnchor,
                  
                  let arView = arView else{
                return
            }
            let planeAnchor = AnchorEntity(anchor:anchor)
            
            
            
            planeAnchor.addChild(gameController.gameAnchor)
            arView.scene.anchors.append(planeAnchor)
            gameController.gameAnchor.backWall?.visit { entity in
                entity.components[ModelComponent.self] = nil
            }
            gameController.gameAnchor.frontWall?.visit { entity in
                entity.components[ModelComponent.self] = nil
            }
            gameController.Ball13?.physicsBody?.massProperties.centerOfMass = ([0.001,0,0.001],simd_quatf(angle: 0, axis:  [0,1,0]))
            gameController.Ball4?.physicsBody?.material = PhysicsMaterialResource.generate(friction: 0.3, restitution: 0.3)
            gameController.Ball6?.physicsBody?.mode = .kinematic
            //gameController.Ball6?.collision?.shapes.removeAll()
            arView.session.delegate = nil
            arView.session.run(ARWorldTrackingConfiguration())
        }
        @MainActor func loadModel(){
            gameController.gameAnchor = try! Ball.loadBallGame()
            if let ball = gameController.gameAnchor.motherBall as? Entity & HasCollision {
                let gestureRecognizers = arView?.installGestures(.translation, for: ball)
                if let gestureRecognizer = gestureRecognizers?.first as? EntityTranslationGestureRecognizer {
                    gameController.gestureRecognizer = gestureRecognizer
                    gestureRecognizer.removeTarget(nil, action: nil)
                    gestureRecognizer.addTarget(self, action: #selector(self.handleTranslation))
                }
            }
        }
       @objc
       func handleTranslation(_ recognizer: EntityTranslationGestureRecognizer) {
           guard let ball = gameController.motherBall 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
       }
    }
}
extension Entity {
    func visit(using block: (Entity) -> Void) {
        block(self)
        for child in children {
            child.visit(using: block)
        }
    }
}
#Preview {
    PhysicsMotionView()
}

在代码中,实现的功能如下:

(1)加载模拟场景并进行相应的处理。

(2) 通过设置物体速度,对物体运动进行物理模拟。

在功能1中,我们首先使用 loadModel()方法加载 Reality 场景,然后通过 session(- session: ARSesion,didAdd anchors: [ARAnchor])方法对平面检测情况进行监视,当ARKit检测到符合要求的水平平面后,将加载的场景挂载到 ARAnchor 下显示,对不需要显示的四周围栏进行了隐藏处理,然后设置了各球体的物理参数、物理材质并重启了 ARSession(为更好组织代码,方便场景管理,我们使用了 GameController类,具体可以参看本节源码)。

在功能2中为方便控制,我们使用了 RealityKit 中的平移手势EntityTranslationGesture Recognizer,通过计算使用者手指在屏幕上滑动的速度生成物体速度,并将其作为母球的速度(为防止速度过大,我们使用了 GameSettings 结构体并定义了几个边界值,具体可以参github源码),通过直接赋予母球速度值就可以观察母球与场景中其他球体在物理引擎作用下的运动效果。

编译后测试,使用平移手势操作母球,当母球与场景中的其他球体发生碰撞时,会产生相应的物理效果。通过本例可以看到,在 Xcode中也可以修改 Reality Composer 工具中设定的各球体的物理属性,如代码清单中第15 行到第17所示,读者也可以修改不同属性看一看它们如何影响物体的行为,取消碰撞体,看一看还能不能发生撞。

具体代码地址:GitHub - duzhaoquan/ARkitDemo

相关推荐
Lsx-codeShare12 分钟前
Uniapp 安装安卓、IOS模拟器并调试
android·前端·javascript·ios·微信小程序·小程序·uni-app
Cedric_Anik4 小时前
Swift——类与结构体
开发语言·ios·swift
码农--xc5 小时前
iOS开发之修改已有项目的项目名和类名前缀
ios
AirDroid_cn5 小时前
Vivo手机投屏到Windows笔记本电脑,支持多台手机投屏、共享音频!
android·windows·ios·ipad·手机投屏·手机使用技巧·手机投屏电脑
二流小码农20 小时前
鸿蒙开发:异步并发操作
android·ios·harmonyos
MavenTalk1 天前
前端技术选型之uniapp
android·前端·flutter·ios·uni-app·前端开发
豪冷啊1 天前
iOS 17.4 Not Installed
ios
Topstip1 天前
iOS 19 重大更新泄露,将带来更“聪明”的 Siri 挑战 ChatGPT
人工智能·ios·ai·chatgpt
豪冷啊1 天前
Xcode15(iOS17.4)打包的项目在 iOS12 系统上启动崩溃
macos·objective-c·cocoa
软件聚导航2 天前
uniapp 安卓和ios震动方法,支持息屏和后台震动,ios和安卓均通过测试
android·ios·uni-app