시작
오늘은 SceneKit으로 주사위 생성 및 굴리기를 해봅시다.
순서는 이전에 만든 Cube와 달 만들기와 거의 동일하니 못 보셨다면 여기를 참고하세요.
바로 시작하겠습니다.
주사위로 사용할 모델 다운 받기
모델을 모아둔 홈페이지입니다. (유료, 무료)
검색을 통하여 원하는 모델 검색 → Formats에서 확장자 .dae(Collada) 선택 -> 다운로드 시 Collada폴더만 다운로드하면 됩니다
dae파일은 ScencKit과 호환이 되며 실제 scn파일로 변환이 가능합니다.
다운로드 받은 Collada 폴더를 확인해보면 dae파일 및 여러 텍스쳐 색상이 있습니다.
dae파일과 원하는 색상을 Xcode - art.scnassets으로 옮겨줍시다.
SceneKit에서 사용하기 위해선 scn파일이 필요합니다.
그러므로 우리는 다운받은 dae파일을 scn파일로 변환을 시켜줘야 합니다.
확장자 변경방법 2가지 중 택 1
1. 파일에서 직접적으로 확장자 이름을 변경시키기
2. Xcode내부의 Editor -> convert(.scn)을 통하여 변환
수평면 감지하기
1. 수평 감지를 하기 위해선 configuration에서 planeDetection(바닥 감지)를 설정해 줘야 합니다.
planeDetection에 대하여 더 자세히 알고 싶으시면 공식문서를 참고하세요
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Create a session configuration
let configuration = ARWorldTrackingConfiguration()
// 수평면을 감지하기 위해 configuration에 접근 후 표면을 감지하는 프로퍼티(planeDetection)를 추가합니다
configuration.planeDetection = .horizontal
// Run the view's session
sceneView.session.run(configuration)
}
또한 아래 코드를 통하여 위치추적을 하는 데 있어 무슨 일이 일어나고 있는지 확인이 가능합니다.
// viewDidLoad에서
// 위치를 추적하는데 있어 실제로 무슨일이 일어나고 있는지 점으로 표현을 해준다
self.sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints]
2. SceneKit의 델리게이트에서 renderer(didAdd node:) 를 통하여 Plane을 생성합니다
준비물 : Grid PNG 파일 (다운로드)
(팁. png 파일은 투명도가 제공이 됩니다)
// SceneKit의 델리게이트를 설정해준다.
extension ViewController: ARSCNViewDelegate {
// renderer : 새로운 anchor의 node가 Scene의 추가 되었을을 알린다.
// didAdd node : 새로 추가될 노드
// ARAnchor : ARScene의 객체를 배치하는데 있어 카메라를 기준으로 실제 위치 및 방향 추적함 (일종의 바닥의 깔린 타일과 동일)
// 이 메서드는 해당 타일을 사용하여 개채를 원하는 곳에 배치합니다.
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
// ARAnchor는 광범위한 범주이다. (ARPlaneAnchor(평면), ARObjectAnchor(3D 객체), ARImageAnchor(이미지), ARFaceAnchor(얼굴))
// 우리는 바닥에 주사위를 놓고 싶으니까 이중에서 평면감지를 사용해야 한다.
// ARAnchor를 다운캐스팅하여 ARPlaneAnchor로 변환시킨다.
// ARPlaneAnchor 속성으로는 planeExtent이 존재한다 (평면에서 감지된 너비와 높이)
guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
// ScneneKit에 평면을 생성할 수 있도록 SCNPlane을 사용하여 만들어준다
// 감지된 속성(planeExtent)을 갖고 SCNPlane을 생성시킨다
let plane = SCNPlane(width: CGFloat(planeAnchor.planeExtent.width), height: CGFloat(planeAnchor.planeExtent.height))
// 노드를 만들어주고 위치와 지오메트리를 설정한다
let planeNode = SCNNode()
// x: center로 맞춤 , y: 수평면을 위하므로 0, z: center로 맞춤
planeNode.position = SCNVector3(x: planeAnchor.center.x, y: 0, z: planeAnchor.center.z)
// 여기서 한가지 문제점이 있습니다. 위의 그림과 함께 보면서 이해해 보세요.
// 기본적으로 SCNPlane은 수직 평면으로 형성이 됩니다. (x와 y축으로 되어있는 수직평면)
// 따라서 이 수직면을 x와 z축을 사용하는 수평 평면으로 변환을 해야 합니다. (90°로 눞여야 된다)
// angle : 기본 단위는 radians입니다, (1 rad = 57.3°, 1π rad = 180° 입니다)
// 또한 양수일 때 시계방향, 음수일 때 반시계방향임을 고려해야 해야 한다.
// x, y, z의 축으로 회전 시킨다. (1이면 사용, 0이면 사용하지 않음)
planeNode.transform = SCNMatrix4MakeRotation(-Float.pi/2 , 1, 0, 0)
let gridMaterial = SCNMaterial()
// 팁으로 png 파일은 투명도가 제공이 됩니다. grid의 빈곳은 배경이 보이죠.
// grid.png 불러온다.
gridMaterial.diffuse.contents = UIImage(named: "art.scnassets/grid.png")
plane.materials = [gridMaterial]
// 뼈대 설정
planeNode.geometry = plane
// addChildNode를 추가 해준다.
node.addChildNode(planeNode)
}
}
모양 및 노드 생성
클릭 시 주사위가 생성이 돼야 하므로 touchesBegan에다가 만들겠습니다.
1. 터치를 할 때 그 위치를 3D공간으로 계산하여 얻습니다
2. 다운로드한 모델 파일(scn)로 직접 생성합니다.
3. node를 생성을 해줍니다. (childNode의 이름은 인스펙터의 Identity입니다)
4. 순서는 전 예제와 거의 동일합니다.
// 터치 대리자 메서드를 통해 실제 위치로 객체를 생성
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Set<UITouch>로 구성이 되어있다. 다중 터치를 할경우 isMultipleTouchEnabled 설정이 필요하다.
// 우리는 단일 터치만 원하므로 첫번재로 접근해서 사용
if let touch = touches.first {
// 터치의 위치를 감지하기 위해서 location(in: SKNode)을 사용한다. (in: 감지 된 위치)
let touchLocation = touch.location(in: sceneView)
// 화면은 실제로 3D가 아닌 2D이다
// ARKit의 raycastQuery는 2D 공간에서 터치하는 지점을 3D좌표로 변환시켜준다.
// from: sceneView의 위치, allowing: 2D공간에서 z축을 추가하여 3D 터치로 만들어줍니다, alignment: 수평
guard let query = sceneView.raycastQuery(from: touchLocation, allowing: .estimatedPlane, alignment: .horizontal) else { return }
let results = sceneView.session.raycast(query)
// hitResult를 프린트해보면 클릭시 좌표마다 worldTransform에서 다른 translation=(x,y,z) translation=(x°,y°,z°)가 나옵니다.
guard let hitResult = results.first else { return }
// 이제 주사위를 불러 오겠습니다.
// .scn 파일 직접 불러오기
let diceScene = SCNScene(named: "art.scnassets/diceCollada.scn")!
// node를 만들어 줘야 한다.
// withName은 위의 diceCollada로 파일로 접근 후 인스펙터창에서 Identity 이름을 입력 해주면 됩니다.
// recursively는 재귀적으로 수행하여 여기서 예를들면 Dice의 childNode 모두를 검색 및 사용합니다.
// (childNode가 없더라도 나중에 추가할 가능성이 있기 때문에 보통은 true로 설정해줍니다)
guard let diceNode = diceScene.rootNode.childNode(withName: "Dice", recursively: true) else {
fatalError("Failed load SCNScene")
}
// 위치를 정해줍시다. (터치한 곳의 위치)
// worldTransform 은 4x4 위치, 회전, 스케일에 대한 행렬입니다.
// 우리는 클릭 시 주사위를 그 위치에 생성을 시키기 위해 터치 위치(hitResult)를 받아서 만들어보겠다.
// columns의 마지막 4행은 화면의 원근법이라고 알고 있으면 될꺼 같습니다.
// y축을 이대로 (hitResult.worldTransform.columns.3.y) 설정해주면 객체가 평면 기준으로 반이 짤리는 현상이 발생하게 된다.
// 즉 y축의 높이를 객체의 크기의 절반만큼 더해줍시다 (diceNode.boundingSphere.radius)
diceNode.position = SCNVector3(x: hitResult.worldTransform.columns.3.x,
y: hitResult.worldTransform.columns.3.y + diceNode.boundingSphere.radius,
z: hitResult.worldTransform.columns.3.z)
// 각 위치마다 전역변수에 diceNode를 추가합니다.
diceArray.append(diceNode)
// addChildNode에 추가
sceneView.scene.rootNode.addChildNode(diceNode)
sceneView.autoenablesDefaultLighting = true
}
}
runAction을 통한 애니메이션 만들기 (주사위 굴리기)
// 주사위 굴리는 동작 설정
private func roll(dice: SCNNode) {
// 한 축당 4개의 면이 나오므로 랜덤 숫자를 1~4사이로 설정합니다.
// 또한 주사위는 90도 마다 숫자가 위로 향하므로 90°를 곱해줍시다.
let randonX = Float(arc4random_uniform(4) + 1) * (Float.pi/2)
let randonZ = Float(arc4random_uniform(4) + 1) * (Float.pi/2)
// AR의 애니메이션 효과를 위해여 runAction이라는 메서드를 사용합니다.
// rotateBy 회전하는 애니메이션을 (x, y, z축을 기준으로 얼마동안(duration) 회전할지 정해줍니다)
// 랜덤 숫자에 곱하기 5를 해준 이유는 회전이 단조로워서 5바퀴를 더 회전 시키기 위해서 곱해줌
// y축을 설정안한 이유는 y축기준으로 회전을 한다고 해도 나오는 숫자는 그대로이기 때문에 0으로 설정
dice.runAction(SCNAction.rotateBy(x: CGFloat(randonX * 5),
y: 0,
z: CGFloat(randonZ * 5),
duration: 1))
}
// 모든 주사위 굴리기
func rollAll() {
if !diceArray.isEmpty {
for dice in diceArray {
roll(dice: dice)
}
}
}
위의 코드를 전체 주사위 제거 및 리셋 버튼에 연결해 주고
기계를 흔들 때 모션감지로 주사위를 구르게도 설정해 봅시다.
@IBAction func rollDiceButtonTapped(_ sender: UIBarButtonItem) {
rollAll()
}
@IBAction func removeDiceButtonTapped(_ sender: UIBarButtonItem) {
// 전체 주사위를 제거하기 위해
if !diceArray.isEmpty {
for dice in diceArray {
// 노드를 제거하는 메서드 removeFromParentNode
dice.removeFromParentNode()
}
}
}
// 디바이스의 움직임을 관측하여 동작을 실행하는 메서드
override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
rollAll()
}
리펙토링한 전체 코드를 보고 싶다면 Github 를 참고하세요.
ScnenKit의 다른 예제를 보고 싶다면 아래 링크를 참고하세요
참고
'Xcode > Framework' 카테고리의 다른 글
[iOS/Swift] SceneKit의 사용법 (4) - 이미지를 인식하여 3D형상 만들기 (0) | 2023.01.11 |
---|---|
[iOS/Swift] SceneKit의 사용법 (3) - 카메라 줄자, 거리 측정하기 (0) | 2023.01.10 |
[iOS/Swift] SceneKit의 사용법 (1) - 정육면체와 달을 만들어 보자 (0) | 2023.01.07 |
[iOS/Swift] ARKit의 종류 (0) | 2023.01.07 |
[iOS/Swift] CoreML (2) - 훈련 된 Model 사용하기 (3) | 2023.01.04 |