Machineboy空

RealityKit의 Gesture를 살펴보자 - Pinch, Scale 본문

언어/iOS

RealityKit의 Gesture를 살펴보자 - Pinch, Scale

안녕도라 2024. 11. 11. 13:47

ARView에 큐브를 넣고 여러 가지 제스처를 통해 그 큐브를 살펴보는 기능을 만드려고 한다.


EntityGestures의 종류 - 기본으로 제공하는 제스처 

 

Realitykit에서 제공하는 기본 제스처들은 다음과 같다.

  • rotation: 멀티 터치로 물체 회전
  • scale: pinch 동작으로 물체 스케일 조절
  • translation: 싱글 터치로 물체 이동

https://developer.apple.com/documentation/realitykit/arview/entitygestures

 

ARView.EntityGestures | Apple Developer Documentation

The set of possible entity gesture recognizers.

developer.apple.com


EntityGestures 코드

Translation, Scale, Rotation 제스처

import SwiftUI
import ARKit
import RealityKit

struct ContentView : View {
    var body: some View {
        ARViewContainer().ignoresSafeArea()
    }
}

struct ARViewContainer: UIViewRepresentable {
    func makeUIView(context: Context) -> some UIView {
        
        // cameraMode: .nonAR
        let arView = ARView(frame: .zero, cameraMode: .nonAR, automaticallyConfigureSession: false)
        arView.environment.background = .color(.white)
        
        let cubeMaterial = SimpleMaterial(color: .orange, isMetallic: false)
        let cubeMesh = MeshResource.generateBox(size: 0.3)
        let cubeModel = ModelEntity(mesh: cubeMesh, materials: [cubeMaterial])
        
        // Collision이 있어야 충돌 체크 가능
        cubeModel.generateCollisionShapes(recursive: true)
        
        let cubeAnchor = AnchorEntity(world: [0, 0, 0])
        cubeAnchor.addChild(cubeModel)
        arView.scene.anchors.append(cubeAnchor)
        
        // Gesture 적용
        arView.installGestures(.all, for: cubeModel)
        
        // .nonAR일 때 가상 카메라가 있어야 화면 보임
        let camera = PerspectiveCamera()
        camera.position = [0, 0.5, 1]
        // relativeTo nil하게 회전
        camera.look(at: cubeAnchor.position, from: camera.position, relativeTo: nil)
        
        let cameraAnchor = AnchorEntity(world: [0, 0, 0])
        cameraAnchor.addChild(camera)
        arView.scene.addAnchor(cameraAnchor)
        
        return arView
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) { }
}

#Preview {
    ContentView()
}

EntityGestures 의 한계

우선 나는 물체를 이동시킬 필요가 없기 때문에  translation 제스처가 필요없다.

rotation의 경우 Y축 기준으로만 회전하는데, 난 상하로도 즉 X축 기준으로도 회전시킬 필요가 있다.

그리고 rotation과 scale은 동일한 multitouch로 작동하여 동작을 구분하기 어렵다.


Custom Gesture 1 - EntityGesture.scale에 제한 범위 주기!

큐브가 gesture를 통해 조절되는 스케일의 최대, 최소 범위를 규정하고 싶다!

일정 사이즈 이상 커지지 않았으면 좋겠고, 일정 사이즈 이하로 작아지지 않았으면 좋겠다는 것!

큐브가 좀더 뽀용뽀용해보이도록 easeInOut과 같은 scale 애니메이션 효과도 넣으면 좋겠다만.. 아직은!


제스처에 따른 크기 변화를 감지하는 방법 1

  • Combine을 활용해 SceneEvent를 구독하도록 설정하고
  • updateUIView에서는 따로 동작을 주지 않는다.
import SwiftUI
import ARKit
import RealityKit
import Combine

struct ContentView : View {
    var body: some View {
        ARViewContainer().ignoresSafeArea()
    }
}

struct ARViewContainer: UIViewRepresentable {
    func makeUIView(context: Context) -> some UIView {
        
        // cameraMode: .nonAR
        let arView = ARView(frame: .zero, cameraMode: .nonAR, automaticallyConfigureSession: false)
        arView.environment.background = .color(.white)
        
        let cubeMaterial = SimpleMaterial(color: .orange, isMetallic: false)
        let cubeMesh = MeshResource.generateBox(size: 0.3)
        let cubeModel = ModelEntity(mesh: cubeMesh, materials: [cubeMaterial])
        
        // Collision이 있어야 충돌 체크 가능
        cubeModel.generateCollisionShapes(recursive: true)
        
        let cubeAnchor = AnchorEntity(world: [0, 0, 0])
        cubeAnchor.addChild(cubeModel)
        arView.scene.anchors.append(cubeAnchor)
        
        // coordinator에 저장
        context.coordinator.cubeModel = cubeModel
        
        // Gesture 적용
        arView.installGestures(.scale, for: cubeModel)
        
        // .nonAR일 때 가상 카메라가 있어야 화면 보임
        let camera = PerspectiveCamera()
        camera.position = [0, 0.5, 1]
        camera.look(at: cubeAnchor.position, from: camera.position, relativeTo: nil)
        
        let cameraAnchor = AnchorEntity(world: [0, 0, 0])
        cameraAnchor.addChild(camera)
        arView.scene.addAnchor(cameraAnchor)
        
        // 변화 감지 방법1: sceneEvent를 구독하도록 설정
        context.coordinator.cancellable = arView.scene.subscribe(to: SceneEvents.Update.self) { _ in
                    context.coordinator.clampModelSize()
        } as? AnyCancellable
        
        return arView
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) { }

    func makeCoordinator() -> Coordinator {
        return Coordinator()
    }
}

class Coordinator: NSObject {
    var cubeModel: ModelEntity?
    var cancellable: AnyCancellable?    // 구독 취소 가능
    
    func clampModelSize() {
        guard let model = cubeModel else { return }
        
        let currentScale = model.scale.x
        var newScale = currentScale
        
        if currentScale < 0.5 {
            newScale = max(currentScale, 0.5)
        }
        
        if currentScale > 1.0 {
            newScale = min(currentScale, 1.0)
        }
        
        if newScale != currentScale {
            model.scale = SIMD3(repeating: newScale)
        }
    }
}

#Preview {
    ContentView()
}

 


제스처에 따른 크기 변화를 감지하는 방법 2 

  • updateUIView함수를 활용해 보기
  • 이 코드는 왜 작동을 하지 않는가

난 updateUIView가 매 프레임 호출되는 Unity의 Update구문과 비슷한 것이겠거니 생각했다.

 

updateUIView 함수 내부에 print를 찍어보니 scale이 변화되어도 호출되지 않는데,

무언가 uiView에 anchor가 추가되거나 내부 로직에서 감지하는 큰 이벤트가 일어날 때만 뷰를 update해주는 함수였던 것이다.

공식문서를 찾아봤으나 구체적으로 어떤 이벤트를 감지하는지에 관해서는 찾을 수가 없었다.

 

따라서, 제스처 변화에 따른 scale 변화를 감지해 뷰를 새로 그려주려면 방법 1번 처럼 uiView내의 sceneEvent를 따로 구독해줘야 하나보다.

import SwiftUI
import ARKit
import RealityKit

struct ContentView : View {
    var body: some View {
        ARViewContainer().ignoresSafeArea()
    }
}

struct ARViewContainer: UIViewRepresentable {
    var currentScale: SIMD3<Float> = [0,0,0]
    
    func makeUIView(context: Context) -> some UIView {
        
        // cameraMode: .nonAR
        let arView = ARView(frame: .zero, cameraMode: .nonAR, automaticallyConfigureSession: false)
        arView.environment.background = .color(.white)
        
        let cubeMaterial = SimpleMaterial(color: .orange, isMetallic: false)
        let cubeMesh = MeshResource.generateBox(size: 0.3)
        let cubeModel = ModelEntity(mesh: cubeMesh, materials: [cubeMaterial])
        
        // Collision이 있어야 충돌 체크 가능
        cubeModel.generateCollisionShapes(recursive: true)
        
        let cubeAnchor = AnchorEntity(world: [0, 0, 0])
        cubeAnchor.addChild(cubeModel)
        arView.scene.anchors.append(cubeAnchor)
        
        // coordinator에 저장
        context.coordinator.cubeModel = cubeModel
        
        // Gesture 적용
        arView.installGestures(.scale, for: cubeModel)
        
        // .nonAR일 때 가상 카메라가 있어야 화면 보임
        let camera = PerspectiveCamera()
        camera.position = [0, 0.5, 1]
        camera.look(at: cubeAnchor.position, from: camera.position, relativeTo: nil)
        
        let cameraAnchor = AnchorEntity(world: [0, 0, 0])
        cameraAnchor.addChild(camera)
        arView.scene.addAnchor(cameraAnchor)
        
        return arView
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) {
        guard let cubeModel = context.coordinator.cubeModel else { return }
        
        let clampedScale = max(min(currentScale.x, 1.0), 0.5)
        cubeModel.transform.scale = SIMD3<Float>(repeating: clampedScale)
        
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator()
    }
}

class Coordinator: NSObject {
    var cubeModel: ModelEntity?
}

#Preview {
    ContentView()
}