lgvv98

[SceneKit] #3 Animating SceneKit Content 본문

apple/VisionOS

[SceneKit] #3 Animating SceneKit Content

lgvv 2023. 8. 13. 16:26

#3 Animating SceneKit Content

내가 만든 지오메트리들에 이벤트나 애니메이션 등도 사용할 수 있다.

 

# Base Code

일단 기본코드는 이렇게 여기서 하나하나 추가해보자.

func make_image_control() {
        let scene = SCNScene()
        arSceneView.scene = scene
       
        // 카메라 생성
        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        cameraNode.position = SCNVector3(x: 0, y: 0, z: 5)
        scene.rootNode.addChildNode(cameraNode)
        
        // Capsule 생성 및 추가
        let capsuleGeometry = SCNCapsule(capRadius: 0.1, height: 0.3)
        let capsuleMaterial = SCNMaterial()
        capsuleMaterial.diffuse.contents = UIColor.systemGreen
        capsuleMaterial.diffuse.contents = UIImage(named: "tiger") // 이미지
        
        let capsuleNode = SCNNode(geometry: capsuleGeometry)
        capsuleNode.geometry?.materials = [capsuleMaterial]
        capsuleNode.position = SCNVector3(0, 0, -0.5) // 시작 위치 설정
        scene.rootNode.addChildNode(capsuleNode)
        
//        // 빛 추가
        let lightNode = SCNNode()
        lightNode.light = SCNLight()
        lightNode.light?.type = .probe
        lightNode.position = SCNVector3(0, 1, 0) // 빛의 위치 설정
        scene.rootNode.addChildNode(lightNode)
    }

 

우선 현재 이렇게만 해두면 아이템을 만져서 움직일 수 없어서 내가 카메라를 직접 움직여야 한다.

하지만, 카메라의 위치를 바꾼다면?

 

func make_image_control() { 
   ... 
   arSceneView.allowsCameraControl = true
}

arSceneView.allowsCameraControl = true

 

 

# TapGesture

그럼 어떤 지오메트리 객체를 선택했는지는 어떻게 알 수 있을까?

func hitTest(
    _ point: CGPoint, // Scene 공간에서의 좌표계에서의 한 포인트
    options: [SCNHitTestOption : Any]? = nil // 검색에 영향을 미치는 옵션들
) -> [SCNHitTestResult] // 검색 결과를 반환

// https://developer.apple.com/documentation/scenekit/scnscenerenderer/1522929-hittest

해당 메서드를 통해서 찾을 수 있음.

 

리턴이 배열로 나오는 걸로 유추할 수 있듯이 렌더링 된 화면 공간에 2D혹은 3D 좌표공간의 선분을 따라서 있는 모든 점을 반환함. 즉, hitTest는 이 선분을 따라 위치한 모든 Scene에 위치한 요소들을 찾는 프로세스임. 

 

 - SCNHitTestOption 클래스

struct SCNHitTestOption

 

옵션에 대한 한글과 영어 설명

 

아래 코드는 히트 테스트 코드이다. baseCode와 변동이 있음.

extension ViewController {
    func make_image_control() {
        let scene = SCNScene()
        arSceneView.scene = scene
       
        // 카메라 생성
        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        cameraNode.position = SCNVector3(x: 0, y: 0, z: 5)
        scene.rootNode.addChildNode(cameraNode)
        
        // Capsule 생성 및 추가
        let capsuleGeometry = SCNCapsule(capRadius: 0.1, height: 0.3)
        let capsuleMaterial = SCNMaterial()
        capsuleMaterial.diffuse.contents = UIColor.systemGreen
        
        let capsuleNode = SCNNode(geometry: capsuleGeometry)
        capsuleNode.geometry?.materials = [capsuleMaterial]
        capsuleNode.position = SCNVector3(0, 0, -0.5) // 시작 위치 설정
        scene.rootNode.addChildNode(capsuleNode)
        
        // Box 생성 및 추가
        let boxGeometry = SCNBox(width: 0.2, height: 0.2, length: 0.2, chamferRadius: 0)
        
        let boxMaterial = SCNMaterial()
        boxMaterial.diffuse.contents = UIImage(named: "pyramid_full") // 이미지 이름으로 설정
        
        let boxNode = SCNNode(geometry: boxGeometry)
        boxNode.geometry?.firstMaterial = boxMaterial
        boxNode.position = SCNVector3(0.0, 0, -1.5) // 시작 위치 설정
        scene.rootNode.addChildNode(boxNode)
        
        // 빛 추가
        let lightNode = SCNNode()
        lightNode.light = SCNLight()
        lightNode.light?.type = .probe
        lightNode.position = SCNVector3(0, 1, 0) // 빛의 위치 설정
        scene.rootNode.addChildNode(lightNode)
        
        // 탭 Gesture Recognizer 추가
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
        arSceneView.addGestureRecognizer(tapGesture)
    }
    
    @objc func handleTap(_ gesture: UITapGestureRecognizer) {
        // 탭한 위치에서 노드 검색
        let location = gesture.location(in: arSceneView)
        let hitTestResults = arSceneView.hitTest(location, options: nil)
        
        print("✅ \(hitTestResults.count)")
        
        // 가장 가까운 노드 선택
        if let closestNode = hitTestResults.first?.node {
            // 선택된 노드를 저장하거나 처리할 작업 수행
            let material = closestNode.geometry?.firstMaterial
            
            // 선택된 노드에 대한 작업 수행
            // 예: 노드 색상 변경, 애니메이션 등
            material?.diffuse.contents = UIColor.random()
        }
    }
}

히트한거 찾기

 

만약 히트 테스트에서 x,y가 같고 z만 다른 경우에는 어떻게 될까?

 

아래 사진을 보면 그래도 1로 나온다.

겹치는 경우가 있지만, 모든 케이스를 하나하나 다 해볼 수는 없으니 직접해보기. 혹은 필요할 때 더 조사해보기

예상으로는 blendMode의 subtract를 사용한 경우에 이것과 유사할 것으로 보인다.

좌: 해당케이스 우: 터치 결과의 갯수

 

# SceneKit Animation

SceneKit animation은 Core Animation 프레임워크를 기반으로 지원됩니다. 몇몇 SceneKit 클래스들은 animatable 프로퍼티들을 정의하고 있으며, 단순히 새 값을 프로퍼티에 할당하는 것 외에도 속성의 두 값 사이를 부드럽게 전환하는 애니메이션을 만들 수 있음. 예를들어서 Node의 opacity 속성에 애니메이션을 적용하면, 노드의 보이는 컨텐츠가 페이드 인 또는 페이드 아웃된다. 암시적 혹은 명시적으로 애니메이션을 적용할 수 있다.

 

- Animate Content Changes Implicitly

 

animatable 프로퍼티들의 값이 변화함으로써 우리는 암시적 애니메이션을 만들 수 있음. 암시적 애니메이션은 많은 애니메이션 코드를 작성하지 않아도, 일회성 변경 사항을 빠르게 애니메이션화하거나 여러 속성 변경 사항을 함께 애니메이션화하려는 경우에 유용함.

 

SCNTransaction 클래스는 암시적으로 애니메이션을 포함하여 scene의 콘텐츠 변경사항에 대한 SceneKit 아키텍처를 정의. 트랙잭션은 노드, 지오메트리, material 또는 다른 scene의 콘텐츠를 원자단위로 결합하는 작업. 기본적으로 SceneKit은 자동으로 scene이 한번의 runLoop를 거치는동안 모든 변화에 대해 기본 트랙잭션을 생성한다.

 

기본 트랜잭션의 duration은 0이다. 그래서 animatable 프로퍼티들은 즉시 변화한다. 하지만 만약 우리가 트랜잭션의 animationDuration을 증가시킨다면, 모든 변화 anibatable 프로퍼티에 모두 적용된다.

 

아래 예시 코드를 보자. 아까 우리가 작성한 코드 부분에서 트랜잭션을 적용하였다.

@objc func handleTap(_ gesture: UITapGestureRecognizer) {
        // 탭한 위치에서 노드 검색
        let location = gesture.location(in: arSceneView)
        let hitTestResults = arSceneView.hitTest(location, options: nil)
        
        print("✅ \(hitTestResults.count)")
        
        // 가장 가까운 노드 선택
        if let closestNode = hitTestResults.first?.node {
            // 선택된 노드를 저장하거나 처리할 작업 수행

            // 선택된 노드에 대한 작업 수행
            // 예: 노드 색상 변경, 애니메이션 등
            SCNTransaction.animationDuration = 5.0
            closestNode.geometry?.firstMaterial?.diffuse.contents = UIColor.random()
            closestNode.position.y = 2.0
        }
    }

 

색마저도 트랜잭션이 적용된다.

 

- Explicitly Create an Animation

더 복잡한 애니메이션을 하기 위해선 명시적으로 애니메이션 객체를 만들고 애니메이션 될 scene 요소에 붙여줘야 한다. 애니메이션 객체를 만들면 애니메이션을 재사용할 수 있으므로 언제든지 동일한 애니메이션을 재생하거나 scene의 다른 요소에 적용할 수 있다.

 

CAAnimation을 만드려는 애니메이션 타입의 subclass를 선택하고 key-value coding을 사용해 애니메이션을 할 프로퍼티를 지정하고 파라미터를 설정한다. 그런 다음에 애니메이션을 scene 장면의 하나 이상의 요소에 첨부해 동작 중인 애니메이션을 설정한다.

 

다양한 Core Animation 클래스들을 사용함으로써, 여러 애니메이션을 결합 또는 순차적으로 여러 키 프레임 값 사이에 속성 값을 지정하여 보간하는 애니메이션을 만들 수 있다. SceneKit 객체에 애니메이션을 첨부하는 방법은 SCNAnimatable을 참고하세요.

 

<SCNAnimatable>

https://developer.apple.com/documentation/scenekit/scnanimatable

 

SCNAnimatable | Apple Developer Documentation

The common interface for attaching animations to nodes, geometries, materials, and other SceneKit objects.

developer.apple.com

 

SceneKit는 또한 외부 3D 제작 도구를 사용하여 생성되고 장면 파일에 저장된 애니메이션에 CAAnimation 객체를 사용한다. 예를 들어, 디자이너는 걷기, 점프 및 기타 액션을 위한 애니메이션으로 게임 캐릭터를 만들 수 있습니다. SCNSeceneSource 클래스를 사용하여 장면 파일에서 애니메이션 객체를 로드하고 게임 캐릭터를 나타내는 SCNNode 객체에 첨부하여 이러한 애니메이션을 게임에 결합한다.

 

 

 

아래는 사용하는 예시이다.

// 텍스트 블록의 3D 돌출 깊이를 변경하는 명시적 애니메이션

private funcprivate func make_text() {
        let scene = SCNScene()
        arSceneView.scene = scene
        
        var textNode = SCNNode()
        let textGeometry = SCNText(string: "Hello. 🥬 야채맨", extrusionDepth: 0.1)
        textNode = SCNNode(geometry: textGeometry)
        textNode.position = SCNVector3(0, 0, -30)
        scene.rootNode.addChildNode(textNode)
        
        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        cameraNode.position = SCNVector3(0, 0, 30)
        scene.rootNode.addChildNode(cameraNode)
        
        let lightNode = SCNNode()
        lightNode.light = SCNLight()
        lightNode.light?.type = .omni
        lightNode.position = SCNVector3(0, 10, 10)
        scene.rootNode.addChildNode(lightNode)
        
        let animation = CABasicAnimation(keyPath: "geometry.extrusionDepth")
        animation.fromValue = 0.0
        animation.toValue = 10
        animation.duration = 1.0
        animation.autoreverses = true
        animation.repeatCount = .infinity
        textNode.addAnimation(animation, forKey: "extrude")
    }

텍스트 애니메이션

 

 - SCNText

 init에서 extrusionDepth이 있는데, extrusionDepth은 z 축의 영점에 중심을 두고 있습니다. 예를 들어,  extrusionDepth 1.0이면 z 축을 따라 -0.5에서 0.5로 확장되는 형상이 생성됩니다. 압출 깊이가 0이면 평평한 단면 형상이 생성됩니다. 

요 부분은 직접 열어서 보면 간단히 이해할 수 있으므로 직접 찾아보자.

 

다시 돌아가서 

 # SCNSeceneSource

파일 또는 데이터에서 scene 콘텐츠를 가져오는(Load)것과 관련된 데이터 읽기 작업을 관리하는 객체이다.

class SCNSceneSource : NSObject

 

또한 SceneSource를 이용하여 scene 파일의 내용을 검사하거나 scene의 선택적으로 외부 특정 요소를 전체 scene 그리고 모든 에셋들을 포함하지 않고도 추출할 수 있다.

 

SceneKit은 지원되는 형식의 파일 또는 해당 파일의 내용을 보유한 NSData 개체에서 scene 내용을 읽을 수 있습니다. 지원되는 형식은 다음과 같습니다:

 

지원되는 파일 확장자

 

Xcode 프로젝트에 DAE 또는 Alembic 형식의 장면 파일을 포함하면 Xcode는 자동으로 파일을 빌드된 앱에서 사용할 수 있도록 SceneKit의 압축된 scene 형식으로 변환합니다. 압축 파일은 원본 .dae또는 .abc확장자를 유지합니다.

 

SCNSceneSource 클래스는 또한 SCeneKit 아카이브 파일들을 로드할 수 있고 그리고 그것들은 Xcdoe scene editor 또는 NSKeyed Archiver 클래스를 사용해 SCNScene 객체를 직렬화하고, scene 그래프를 포함하여 프로그래밍적인 방법으로 만들 수 있다.

 

 -📝 Note

가장 좋은 결과를 얻으려면 앱 번들에 포함된 scene 파일을 확장자가 있는 폴더에 배치 .scnassets하고 해당 scene에서 텍스처로 참조되는 이미지 파일을 assets 카탈로그에 배치하십시오. 그런 다음 Xcode는 각 대상 장치에서 최상의 성능을 위해 장면 및 텍스처 리소스를 최적화하고 App Thinning 및 On-Demand 리소스와 같은 제공 기능을 위해 텍스처 리소스를 준비합니다.

 

# Advanced Study

 - SCNText 색상주기

: Material로 주면 된다. NSAtributedString은 먹히지 않았고, 이모지도 들어가지 않는 모습

텍스트

 

private func make_plain() {
        let scene = SCNScene()
        arSceneView.scene = scene
        
        let plainNode = SCNNode()
        let plainGeometry = SCNPlane(width: 15, height: 15)
        
        let material = SCNMaterial()
        material.diffuse.contents = UIImage(resource: .tiger) // Xcode 15
        
        plainNode.geometry = plainGeometry
        plainNode.geometry?.materials = [material]
        plainNode.position = SCNVector3(0, 0, -30)
        
        scene.rootNode.addChildNode(plainNode)
    }

 

우리집에 걸린 호랑이 Plain

 

 

 

 

 

(참고)

https://developer.apple.com/documentation/scenekit/scnscenerenderer/1522929-hittest

 

hitTest(_:options:) | Apple Developer Documentation

Searches the renderer’s scene for objects corresponding to a point in the rendered image.

developer.apple.com

https://developer.apple.com/documentation/scenekit/scnhittestoption

 

SCNHitTestOption | Apple Developer Documentation

Options affecting the behavior of SceneKit hit-testing methods.

developer.apple.com

https://developer.apple.com/documentation/scenekit/animation/animating_scenekit_content

 

Animating SceneKit Content | Apple Developer Documentation

Learn about implicit animations, explicit animations, and actions, and when to choose each in your app.

developer.apple.com

https://developer.apple.com/documentation/scenekit/scnanimatable

 

SCNAnimatable | Apple Developer Documentation

The common interface for attaching animations to nodes, geometries, materials, and other SceneKit objects.

developer.apple.com

https://developer.apple.com/documentation/scenekit/scnscenesource

 

SCNSceneSource | Apple Developer Documentation

An object that manages the data-reading tasks associated with loading scene contents from a file or data.

developer.apple.com

 

'apple > VisionOS' 카테고리의 다른 글

[ARKit] #6 ARKit in iOS  (1) 2023.08.15
[SceneKit] #5 SCNSceneRendererDelegate  (0) 2023.08.15
[SceneKit] #4 SCNAction  (1) 2023.08.13
[SceneKit] #2 Geometry 다뤄보기  (0) 2023.08.13
[ARKit] #1 ARKit 시작하기  (0) 2023.08.12
Comments