Machineboy空
RealityKit의 Gesture를 살펴보자 - Pan, Rotate 본문
기본 제스처의 한계
- 문제점1: 기본으로 제공하는 rotation의 경우 Y축 기준으로만 회전하는데, 상하로도 즉 X축 기준으로도 회전시켜 윗면을 보고 싶다!
- 문제점2: 그리고 scale조정하는 제스처와 rotate 제스처가 같아 동작 구분이 필요하다.
문제점2: pinch와 rotate의 구분
두 손가락을 사용하는 pinch를 통해 scale을 조절하고 한 손가락을 사용하는 pan을 통해 rotate를 시키려니
많은 이들이 수차례 시행착오를 거쳐 완성했을 제스처 종류와 그에 따른 동작들에 대한 정립을 무시한 바보가 된 기분이 들었다.
이미 수차례 유저테스트를 거쳐 완성되었을 제스처와 동작간 매핑일 것이라 생각하여,
사용자가 가장 매끄럽게 사용하기 위해서는 기본 제스처들을 잘 사용하는 것이 맞겠다고 판단했다.
그래서 EntityGestures [.rotate, .scale]을 그대로 사용하기로 했다.
다만 scale의 경우, 물체가 프레임보다 커지는 경우나, 너무 작아져 살펴볼 수 없는 경우가 생기지 않도록 크기 제한을 두었다.
문제점1: x축 회전
남은 문제는 기본 rotation 제스처가 y축으로만 회전하여 좌우로 밖에 살펴볼 수 없다는 점인데,
내가 구현하고자 하는 기능은 box의 윗면도 충분히 살펴볼 수 있어야 하기 때문에,
제스처와 별개로 camera의 높이를 조절하는 기능을 추가했다.
rotate 제스처를 통한 좌우 회전과,카메라의 높이를 매핑한 슬라이더를 통해 윗면을 잘 살펴 볼 수 있게 구현하였다.
구현 코드
import SwiftUI
import ARKit
import RealityKit
import Combine
struct ContentView: View {
@State private var cameraHeight: Float = 0.5 // 처음 카메라 높이
var body: some View {
VStack {
ARViewContainer(cameraHeight: $cameraHeight)
.ignoresSafeArea()
// 카메라 높이 조절 슬라이더 0.1 ~ 1.0
Slider(value: Binding(
get: { Double(cameraHeight)},
set: { cameraHeight = Float($0) }
), in: 0.5...1.0)
.padding()
.accentColor(.orange)
}
}
}
struct ARViewContainer: UIViewRepresentable {
@Binding var cameraHeight: Float
func makeUIView(context: Context) -> ARView {
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])
cubeModel.generateCollisionShapes(recursive: true)
let cubeAnchor = AnchorEntity(world: [0, 0, 0])
cubeAnchor.addChild(cubeModel)
arView.scene.anchors.append(cubeAnchor)
// translation제외 제스체
arView.installGestures([.rotation,.scale],for: cubeModel)
context.coordinator.cubeModel = cubeModel
// .nonAR일 때 가상 카메라가 있어야 화면 보임
let camera = PerspectiveCamera()
camera.position = [0, cameraHeight, 1]
let cameraAnchor = AnchorEntity(world: [0, 0, 0])
cameraAnchor.addChild(camera)
arView.scene.addAnchor(cameraAnchor)
context.coordinator.camera = camera
// sceneEvent 구독
context.coordinator.cancellable = arView.scene.subscribe(to: SceneEvents.Update.self) { _ in
// camera가 cube바라보도록!
camera.look(at: cubeModel.position, from: camera.position, relativeTo: nil)
context.coordinator.clampModelSize()
} as? AnyCancellable
return arView
}
//View의 State변수에 Binding된 카메라 높이가 바뀔 때마다 호출됨
func updateUIView(_ uiView: ARView, context: Context) {
context.coordinator.camera?.position.y = cameraHeight
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
}
class Coordinator: NSObject {
var cubeModel: ModelEntity?
var camera: PerspectiveCamera?
var cancellable: AnyCancellable?
func clampModelSize() {
guard let model = cubeModel else { return }
let currentScale = model.scale.x
var newScale = currentScale
if currentScale < 1.0 {
newScale = max(currentScale, 1.0)
}
if currentScale > 2.5 {
newScale = min(currentScale, 2.5 )
}
if newScale != currentScale {
model.scale = SIMD3(repeating: newScale)
}
}
}
#Preview {
ContentView()
}
수직 슬라이더 버전
이 화면을 넣어야 하는 부분의 아랫부분에 정보가 꽤 많아 가로 슬라이더를 배치할 경우,
화면이 복잡해질 것 같았다.
그리고 슬라이더를 통해 카메라의 높이를 높은대서 낮은 곳으로 이동하는 동작이니,
직관적으로 세로가 더 잘 맞겠다 싶어 GPT의 도움을 받아 만든 수직 슬라이더 코드이다!
import SwiftUI
import ARKit
import RealityKit
import Combine
struct ContentView: View {
@State private var cameraHeight: Float = 0.5
var body: some View {
HStack {
ARViewContainer(cameraHeight: $cameraHeight)
.ignoresSafeArea()
VerticalSlider(value: $cameraHeight, range: 0.5...2.0)
.frame(width: 15, height: 300)
.padding()
}
}
}
struct ARViewContainer: UIViewRepresentable {
@Binding var cameraHeight: Float
func makeUIView(context: Context) -> ARView {
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])
cubeModel.generateCollisionShapes(recursive: true)
let cubeAnchor = AnchorEntity(world: [0, 0, 0])
cubeAnchor.addChild(cubeModel)
arView.installGestures([.rotation, .scale], for: cubeModel)
arView.scene.anchors.append(cubeAnchor)
context.coordinator.cubeModel = cubeModel
// .nonAR일 때 가상 카메라가 있어야 화면 보임
let camera = PerspectiveCamera()
camera.position = [0, cameraHeight, 1]
let cameraAnchor = AnchorEntity(world: [0, 0, 0])
cameraAnchor.addChild(camera)
arView.scene.addAnchor(cameraAnchor)
context.coordinator.camera = camera
context.coordinator.cancellable = arView.scene.subscribe(to: SceneEvents.Update.self) { _ in
camera.look(at: cubeModel.position, from: camera.position, relativeTo: nil)
context.coordinator.clampModelSize()
} as? AnyCancellable
return arView
}
func updateUIView(_ uiView: ARView, context: Context) {
context.coordinator.camera?.position.y = cameraHeight
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
}
class Coordinator: NSObject {
var cubeModel: ModelEntity?
var camera: PerspectiveCamera?
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 > 2.5 {
newScale = min(currentScale, 2.5)
}
if newScale != currentScale {
model.scale = SIMD3(repeating: newScale)
}
}
}
//세로 슬라이더
struct VerticalSlider: View {
@Binding var value: Float
var range: ClosedRange<Float>
var body: some View {
GeometryReader { geometry in
VStack {
Spacer(minLength: 0)
Rectangle()
.fill(Color.orange)
.frame(height: CGFloat((value - range.lowerBound) / (range.upperBound - range.lowerBound)) * geometry.size.height)
.cornerRadius(5)
}
.frame(width: geometry.size.width)
.background(Color.gray.opacity(0.3))
.cornerRadius(5)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { gesture in
let sliderHeight = geometry.size.height
let dragY = max(0, min(sliderHeight, sliderHeight - gesture.location.y))
let newValue = Float(dragY / sliderHeight) * (range.upperBound - range.lowerBound) + range.lowerBound
value = newValue
}
)
}
}
}
#Preview {
ContentView()
}
카메라와 큐브사이의 거리에 따른 보정값을 큐브사이즈에 곱해주고 싶은데.. 도전하다가 실패했다!
지금까지 구현한 걸 기본으로 좀 더 매끄럽게 다듬는 건 나중에!
'언어 > iOS' 카테고리의 다른 글
RealityKit - Plane에 벡터이미지 texture 적용하기, UnlitMaterial, opacityThreshold (1) | 2024.11.12 |
---|---|
RealityKit - TextField에 입력한 글자를 3D Entity로 출력하기! (0) | 2024.11.11 |
RealityKit의 Gesture를 살펴보자 - Pinch, Scale (0) | 2024.11.11 |
비콘이 감지되면 버튼을 활성화 시키자! 여러 개의 비컨 감지 로직 (0) | 2024.11.03 |
비컨별로 다른 화면 띄우기! (0) | 2024.11.01 |