Danny의 iOS 컨닝페이퍼
article thumbnail

시작

오늘은 SceneKit으로 주사위 생성 및 굴리기를 해봅시다.

 

순서는 이전에 만든 Cube와 달 만들기와 거의 동일하니 못 보셨다면 여기를 참고하세요.

 

바로 시작하겠습니다.

 

주사위로 사용할 모델 다운 받기

모델을 모아둔 홈페이지입니다. (유료, 무료)

 

검색을 통하여 원하는 모델 검색 → Formats에서 확장자 .dae(Collada) 선택 -> 다운로드 시 Collada폴더만 다운로드하면 됩니다

dae파일은 ScencKit과 호환이 되며 실제 scn파일로 변환이 가능합니다.

다운로드

 

 

3D Models for Professionals :: TurboSquid

Buy 3D models for your project from our vast online catalog of cars, people, textures, architectural models and more.

www.turbosquid.com

 

다운로드 받은 Collada 폴더를 확인해보면 dae파일 및 여러 텍스쳐 색상이 있습니다.

 

dae파일과 원하는 색상을 Xcode - art.scnassets으로 옮겨줍시다.

 

SceneKit에서 사용하기 위해선 scn파일이 필요합니다. 

그러므로 우리는 다운받은 dae파일을 scn파일로 변환을 시켜줘야 합니다.

 

확장자 변경방법 2가지 중 택 1

1. 파일에서 직접적으로 확장자 이름을 변경시키기

2. Xcode내부의 Editor -> convert(.scn)을 통하여 변환

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 파일은 투명도가 제공이 됩니다)

SCNPlane

 

// 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. 순서는 전 예제와 거의 동일합니다.

Identity

 

// 터치 대리자 메서드를 통해 실제 위치로 객체를 생성
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의 다른 예제를 보고 싶다면 아래 링크를 참고하세요

 

[iOS/Swift] SceneKit의 사용법 (1) - 정육면체와 달을 만들어 보자

시작 AR 주사위 던지는 앱을 만들기 앞서 이해를 돕기 위해 들어가기 앞서 간단한 Cube(정육면체)와 Shpere(구)를 만들어 봅시다. 사용법 iOS -> Augmented Reality App 선택 Content Technology : SceneKit을 선택해

ios-daniel-yang.tistory.com

 

[iOS/Swift] SceneKit의 사용법 (3) - 카메라 줄자, 거리 측정하기

시작 오늘은 SceneKit으로 카메라를 통한 줄자를 만들어 보겠습니다. 간단히 설명할 예정이라 자세한 내용은 이전 글을 참고하시길 바랍니다. SceneKit의 사용법 (1) - 정육면체와 달을 만들어 보자 Sc

ios-daniel-yang.tistory.com

 

[iOS/Swift] SceneKit의 사용법 (4) - 이미지를 인식하여 3D형상 만들기

시작 오늘은 SceneKit으로 이미지를 인식하여 그 위에 캐릭터를 올려볼 예정입니다. 간단히 설명할 예정이라 자세한 내용은 이전 글을 참고하시길 바랍니다. SceneKit의 사용법 (1) - 정육면체와 달을

ios-daniel-yang.tistory.com

 

 

참고

 

iOS & Swift - The Complete iOS App Development Bootcamp

From Beginner to iOS App Developer with Just One Course! Fully Updated with a Comprehensive Module Dedicated to SwiftUI!

www.udemy.com

 

반응형
profile

Danny의 iOS 컨닝페이퍼

@Danny's iOS

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!