*코드는 직접 코드에디터에 붙여넣고 주석을 확인하시는 편이 가독성에 좋습니다
공식문서의 설명을 번역했습니다. 번역한 설명을 토대로 주석을 한줄마다 달았습니다.
override func viewDidLoad() {
super.viewDidLoad()
// Set the view's delegate sceneView.delegate = self
// 씬 생성 let scene = SCNScene()
// 씬 뷰에 SCNScene 인스턴스를 올림 sceneView.scene = scene
// 인벤토리 버튼 추가 setupInventoryButton()
// 제스처 인식기 추가 addGestureRecognizers()
}
func loadUSDZModel(named modelName: String) {
// sceneView의 씬에서 루트 노드의 모든 자식 노드를 제거
// forEach 문으로 루트 노드의 모든 자식노드를 돌면서 씬의 루트 노드에 있는 모든 자식 노드를 제거함
sceneView.scene.rootNode.childNodes.forEach { $0.removeFromParentNode()
}
guard let url = Bundle.main.url(forResource: "art.scnassets/\(modelName)", withExtension: "usdz"),let node = SCNReferenceNode(url: url) else {
print("USDZ 파일을 찾을 수 없습니다: \(modelName)")
return
}
// 일부 객체가 검게 보이는 현상이 있으므로 다시 조명추가해줌
// 씬에서 오브젝트를 갈아낄때 노드가 삭제되면서 조명(라이팅)이 제거된 것으로 보임
addLighting()
currentModelName = modelName
// 소리를 재생합니다
playSound(modelName)
// 노드를 올립니다
node.load()
// Animated_fire 모델의 특성으로 z축으로 떨어져서 보내게 했습니다
node.position = SCNVector3(x: 0, y: 0, z: -15)
// 원하는 스케일로 조정
node.scale = SCNVector3(x: 0.15, y: 0.15, z: 0.15)
// 씬의 루트노드에 자식노드를 추가합니다
sceneView.scene.rootNode.addChildNode(node)
// 핀치 제스쳐 관련으로 selectedNode 속성을 사용했었으나 지금은 쓰지않음(일단 넣어뒀음 - 추후 삭제예정)
// selectedNode = node
// addAnimation(node: node)
originalScale = node.scale
}
해당 프로젝트에서는 기본적으로 UIkit 를 사용하므로 UIkit 에서 어떻게 핸드제스쳐를 이용하는지와 오브젝트를 제스쳐로 인식하는 방법을 설명합니다.
제스쳐를 인식하기 위한 제스쳐 recognizer을 viewDidLoad 부분에 올린 후, 해당 제스쳐가 인식되었을때 구동되는 함수를 따로 작성합니다
예) 하단의 코드에서는 함수 handleTap / handlePan / handlePinch /handleLongPress 등이 그 역할을 수행하고 있습니다
func addGestureRecognizers() { // 탭 제스처 추가 let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(gesture:))) tapGestureRecognizer.delegate = self sceneView.addGestureRecognizer(tapGestureRecognizer) // 팬 제스처 추가 (드래그) let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))) sceneView.addGestureRecognizer(panGestureRecognizer) // 핀치 제스처 추가 let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:))) sceneView.addGestureRecognizer(pinchGestureRecognizer) // 롱프레스 제스처 추가 let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:))) // 최소 2초동안은 프레싱되어야 제스쳐를 인식합니다 longPressGestureRecognizer.minimumPressDuration = 2 // 2초 안에 들어오는 제스쳐는 삭제합니다(버리기) longPressGestureRecognizer.delaysTouchesBegan = true sceneView.addGestureRecognizer(longPressGestureRecognizer) }
@MainActor class UILongPressGestureRecognizer : [UIGestureRecognizer](https://developer.apple.com/documentation/uikit/uigesturerecognizer)UILongPressGestureRecognizer is a concrete subclass of UIGestureRecognizer. UILongPressGestureRecognizer는 UIGestureRecognizer의 구체적인 서브클래스입니다. The user must press one or more fingers on a view and hold them there for a minimum period of time before the action triggers. While down, the userʼs fingers canʼt move more than a specified distance or the gesture fails. 사용자는 동작이 트리거되기 전에 뷰를 하나 이상의 손가락으로 누르고 최소 시간 동안 유지해야 합니다. 누르고 있는 동안 사용자의 손가락이 지정된 거리 이상 움직이지 않으면 제스처가 실패합니다. A long-press gesture is continuous. The gesture begins (UIGestureRecognizer.State.began) when the user presses the number of allowable fingers (numberOfTouchesRequired) for the specified period (minimumPressDuration) and the touches don’t move beyond the allowable range of movement (allowableMovement). The gesture recognizer transitions to the Change state whenever a finger moves, and it ends (UIGestureRecognizer.State.ended) when the user lifts any of the fingers. 길게 누르는 제스처가 계속됩니다. 제스처는 사용자가 지정된 기간(minimumPressDuration) 동안 허용되는 손가락 수(numberOfTouchesRequired)를 누르고 터치가 허용된 이동 범위(allowableMovement)를 벗어나지 않으면 시작됩니다(UIGestureRecognizer.State.began). 제스처 인식기는 손가락이 움직일 때마다 변경 상태로 전환되며, 사용자가 손가락을 떼면 종료(UIGestureRecognizer.State.ended)됩니다.*Configuring the gesture recognizer* 제스처 인식기 구성하기
var minimumPressDuration: TimeInterval
The minimum time that the user must press on the view for the gesture to be recognized.
제스처를 인식하기 위해 사용자가 뷰를 눌러야 하는 최소 시간입니다.
var numberOfTouchesRequired: Int
The number of fingers that must touch the view for gesture recognition.
제스처 인식을 위해 뷰를 터치해야 하는 손가락의 수입니다.
The number of taps on the view necessary for gesture recognition.
제스처 인식에 필요한 뷰의 탭 횟수입니다.
var allowableMovement: CGFloat
The maximum movement of the fingers on the view before the gesture fails.
제스처가 실패하기 전 뷰에서 손가락의 최대 움직임입니다.
let lpgr = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress))lpgr.minimumPressDuration = 0.5lpgr.delaysTouchesBegan = truelpgr.delegate = selfself.colVw.addGestureRecognizer(lpgr)//MARK: - UILongPressGestureRecognizer Action - @objc func handleLongPress(gestureReconizer: UILongPressGestureRecognizer) { if gestureReconizer.state != UIGestureRecognizer.State.ended { //When lognpress is start or running } else { //When lognpress is finish } }
핸들링을 더 잘하기위해 state를 받아서 began, changed, ended로 분기할 수 있다
[추가개념]
delaysTouchesBegan
제스처 인식기가 시작 단계의 터치를 뷰로 전송하는 것을 지연시킬지 여부를 결정하는 부울 값입니다.
var delaysTouchesBegan: [Bool](https://developer.apple.com/documentation/swift/bool) { get set }
이 속성의 값이 false(기본값)이면 뷰는 제스처 인식기와 병렬로 UITouch.Phase.began 와 UITouch.Phase.moved 터치 이벤트를 분석합니다. 이 속성의 값이 참이면 창은 UITouch.Phase.began단계의 터치 개체를 뷰로 전달하는 것을 일시 중단합니다. 이후 제스처 인식기가 제스처를 인식하면 이러한 터치 개체는 버려집니다. 그러나 제스처 인식기가 제스처를 인식하지 못하면 창은 터치의 현재 위치를 알려주는 후속 touchesMoved(_:with:) 메시지와 함께 이러한 개체를 뷰에 전달합니다( touchesMoved(_:with:) 메시지도 포함될 수 있음). 이 속성을 true로 설정하면 뷰에서 이 제스처의 일부로 인식될 수 있는 UITouch.Phase.began단계의 터치를 처리하지 않습니다.
기본적으로 SCNNode 에서 보듯 노드를 찾아서 오브젝트를 변경시킬 수 있습니다. 오브젝트의 위치, 오브젝트의 크기, 오브젝트내에서도 노드가 분리되어 있다면 특정노드(특정 콘텐츠)를 찾아 별개로 애니메이션을 적용할 수 있습니다.
예) 하단의 코드에서는 pan 제스쳐(꾹 누른 상태로 움직이는 제스쳐)에 따라 최상단 노드(최상단 노드가 아니면 일부 노드만/ 즉 오브젝트의 일부분만 옮겨질 수 있습니다)를 찾아서 제스쳐의 위치에 따라 오브젝트의 위치도 변경되는 코드가 구현되어 있습니다.
handlePan 메서드는 hitTest 결과로 반환된 노드의 최상위 부모 노드를 선택하여 이동합니다. 이를 통해 USDZ 모델의 전체 구조를 함께 이동할 수 있습니다.
좌표수정 : 오브젝트 이동 대충가능해짐
@objc func handlePan(_ gesture: UIPanGestureRecognizer) { // gesture.location(in: sceneView)는 제스처 이벤트가 발생한 터치 위치를 sceneView 좌표계에서 반환 let location = gesture.location(in: sceneView) switch gesture.state { case .began: // 제스처의 상태를 확인합니다. 여기서는 제스처가 시작될 때(.began)만 코드를 실행합니다. let hitResults = sceneView.hitTest(location, options: nil) // sceneView.hitTest(location, options: nil)는 터치 위치(location)에서 히트 테스트를 수행하여 해당 위치에 있는 노드들을 반환(히트 테스트는 터치된 지점에 어떤 노드들이 있는지 확인하는 과정임) if let hitResult = hitResults.first { selectedNode = hitResult.node
//히트 테스트 결과에서 첫 번째 노드를 selectedNode로 선택합니다. hitResults.first는 히트된 노드들 중 첫 번째 노드를 반환합니다. while let parent = selectedNode?.parent, parent !== sceneView.scene.rootNode { selectedNode = parent
} /* - 선택된 노드의 최상위 부모 노드를 찾기 위해 `while` 루프를 사용합니다. - `selectedNode?.parent`가 `nil`이 아니고, `selectedNode`의 부모가 `sceneView.scene.rootNode`가 아닐 때까지 루프를 계속 실행합니다. - `selectedNode`를 계속 부모 노드로 갱신하여 최상위 노드에 도달할 때까지 반복합니다. */ originalNodePosition = selectedNode?.position
} case .changed: if let selectedNode = selectedNode, let originalNodePosition = originalNodePosition { // transition은 제스처 인식기에서 현재 팬 동작의 이동 거리를 가져옵니다. 이 거리는 사용자가 화면에서 손가락을 얼마나 이동했는지를 나타냅니다. let translation = gesture.translation(in: sceneView) let newPosition = SCNVector3( x: originalNodePosition.x + Float(translation.x * 0.05), y: originalNodePosition.y + Float(translation.y * -0.05), z: originalNodePosition.z + Float(translation.y * -0.05) ) /* - `translation` 값을 이용하여 `newPosition`을 계산합니다. - `originalNodePosition.x`에 `translation.x`을 더하여 노드의 새로운 x 위치를 계산합니다. `translation.x`에 0.001을 곱한 것은 화면상의 이동 거리를 3D 공간상의 이동 거리로 변환하기 위함입니다. - `originalNodePosition.y`는 그대로 유지됩니다. 이는 노드의 높이(y 축)를 변경하지 않음을 의미합니다. - `originalNodePosition.z`에 `translation.y`을 더하여 노드의 새로운 z 위치를 계산합니다. 마찬가지로 `translation.y`에 0.001을 곱한 것은 화면상의 이동 거리를 3D 공간상의 이동 거리로 변환하기 위함입니다. */ selectedNode.position = newPosition
// addAnimation(node: selectedNode) // addMoveUpDownAnimation(node: selectedNode) } case .ended, .cancelled: selectedNode = nil originalNodePosition = nil default: break } }
추가로 해당 코드에서 사용되는 개념 : translation / SCNVector3 을 아래에서 추가 설명합니다(공식문서 설명)