[iOS/SceneKit] 모델 교체하기
기존에 구현한 코드를 보자
makeUIView
struct DolView : UIViewRepresentable {
@Binding var selectedFace : Face
// UI가 만들어질때 생성
func makeUIView(context: Context) -> SCNView {
let scnView = SCNView()
scnView.scene = loadScene()
scnView.backgroundColor = UIColor.clear // SCNView의 배경을 투명하게 설정
scnView.allowsCameraControl = true
return scnView
}
updateUIView
func updateUIView(_ uiView: SCNView, context: Context) {
print("업데이트")
// 씬이 로드되어 있다고 가정합니다.
guard let scene = uiView.scene else {
print("씬이 없습니다.")
return
}
// showChic이 false일 때 chic 노드만 보이게 설정
if let chicNode = scene.rootNode.childNode(withName: "\(selectedFace)", recursively: true) {
showAllNodes(rootNode: scene.rootNode)
hideAllNodesExcept(node: chicNode, rootNode: scene.rootNode) // 선택 노드만 보이게 설정
} else {
print("\(selectedFace) 노드가 씬에 존재하지 않습니다.")
}
}
}
loadScene
func loadScene() -> SCNScene {
let scene = SCNScene(named: "Dols.scnassets/sparkle.scn") ?? SCNScene()
...
return scene
}
UI가 만들어질때 SCNView의 scene에 모델을 불러와주고 update문을 활용해서 모델안에 담긴 표정들을 바꿔주는 로직을 만들었었다.
모델을 버꿔주려면 당연히 name : \(바꿔줄이름) 을 넣어주면 되겠지만 어디에 선언할지가 중요하다.
내가 생각했던 후보는 두군데였지만 구현하는데에 각각 구현하는데에 어려움이 있다.
- makeUIView
- updateUIView
1. makeUIView 에서 모델 교체의 문제점
문제는 makeUIView는 UI가 생성될때 최초 한번만 실행된다.
그렇다면 교체 메서드를 실행할때마다 해당 UI를 사용하고 있는 View에서 그떄마다 새로 불러와야한다.
2. updateUIView 에서 모델 교체의 문제점
이전에 포스팅에서도 이야기했지만
func updateUIView(_ uiView: SCNView, context: Context) {
print("업데이트")
// 씬이 로드되어 있다고 가정합니다.
guard let scene = uiView.scene else {
print("씬이 없습니다.")
return
}
// 새로 불러온다.
uiView.scene = loadScene(name: "\(새로 불러올 모델명)")
if let childNode = scene.rootNode.childNode(withName: "\(selectedFace)", recursively: true) {
showAllNodes(rootNode: scene.rootNode)
hideAllNodesExcept(node: childNode, rootNode: scene.rootNode) // 선택 노드만 보이게 설정
} else {
print("\(selectedFace) 노드가 씬에 존재하지 않습니다.")
}
}
uiView.scene에 완전히 새로 모델을 불러오면 update문이 실행이 되지 않는다.
왜냐하면 updateUIView는 이전 상태와 비교해서 UI적으로 변경된 사항이 있을때만 실행되기 떄문이다.
이전 상태를 초기화 시킨것과 다름없으니 실행되지 않는 것 또한 당연하다.
1. scene의 변경 및 상태유지, 상태 저장 및 재적용
이전 씬의 상태를 저장하고 , 새로운 씬을 로드한 후에 상태를 재적용해보았다.
import SwiftUI
import SceneKit
struct SceneView: UIViewRepresentable {
@Binding var selectedFaceShape: String
@Binding var selectedFace: String
func makeUIView(context: Context) -> SCNView {
let scnView = SCNView()
scnView.autoenablesDefaultLighting = true
scnView.allowsCameraControl = true
return scnView
}
func updateUIView(_ uiView: SCNView, context: Context) {
// 기존 씬을 유지하고 새로운 씬을 로드합니다
let newScene = SCNScene(named: "Dols.scnassets/\(selectedFaceShape).scn") ?? SCNScene()
// 현재 씬을 임시로 저장합니다
let oldScene = uiView.scene
uiView.scene = newScene
// 이전 씬의 노드 상태를 유지하려면 상태 정보를 저장합니다
if let oldRootNode = oldScene?.rootNode {
let hiddenNodeNames = oldRootNode.childNodes.filter { $0.isHidden }.map { $0.name }
// 새 씬에서 노드 상태를 재적용합니다
for node in newScene.rootNode.childNodes {
if let name = node.name, hiddenNodeNames.contains(name) {
node.isHidden = true
}
}
}
// 선택된 노드를 찾고 보이게 설정합니다
if let selectedNode = newScene.rootNode.childNode(withName: selectedFace, recursively: true) {
selectedNode.isHidden = false
} else {
print("\(selectedFace) 노드가 씬에 존재하지 않습니다.")
}
}
}
역시나 update문이 적용되지 않았다.
선택한 노드를 찾고 보이게하는 구문 이전에서 return 되어 최초의 모델만 불러와지고 해당 모델의 모든 자식들까지 보이는 문제를 겪었다.
2. Scene Reloading Trigger
상태가 변경 될때마다 씬을 다시 로드하도록 트리거를 설정하고. updateView내에서 상태를 비교하여 필요한 경우에만 씬을 다시 로딩해보았다.
하지만 이또한 되지 않았다.
3. 사용하고 있는 상위 뷰에서 값이 변경됐을때 새로 UIView를 만들게 하기
상태가 변경될때 @State 프로퍼티를 사용해 UIView의 상태를 추적하고 상태가 변경될 때 새로 만들도록 UUID를 사용하여 상태가 변경되었음을 감지하고자 했다.
struct DolView: UIViewRepresentable {
@Binding var selectedFace: Face
@Binding var selectedFaceShape: FaceShape
private let id = UUID() // 상태 변경 감지용 ID
func makeUIView(context: Context) -> SCNView {
let scnView = SCNView()
scnView.backgroundColor = UIColor.clear
scnView.allowsCameraControl = true
scnView.scene = loadScene(faceShape: selectedFaceShape)
return scnView
}
이렇게 만들면 정상적으로 작동하지만 가독성도 떨어지고 추후 어떤 문제가 발생할지 모른다.
그래서 내가 해결한 방식은 한 scn 파일 안에 다른 scn 파일들을 refference하여 래퍼런스 모델에서 부모 자식 노드들을 접근하는 것이었다.
기존 scn 파일과 비교해서 보자
sparkle.scn
Group.scn
하나의 scn에 모델들을 불러왔고 노드들을 출력하면 아래와 같이 나온다.
Node name: meong reference
Node name: sparkle
Node name: sparkle_blush_left
기존의 ForEach문을 한번 더 반복해주면 모델을 불러오는 것은 끝이 나게된다.
구현 코드
if let parentNode = scene.rootNode.childNode(withName: "\(selectedFaceShape) reference", recursively: true) {
moveNodeToPosition(node: parentNode, x: 0.0, y: 0.0, z: 0.0) // x, y, z 값은 원하는 위치로 설정
showAllNodes(rootNode: scene.rootNode)
hideAllNodesExcept(node: parentNode, rootNode: scene.rootNode)
if let childNode = parentNode.childNode(withName: "\(selectedFace)", recursively: true) {
showAllNodes(rootNode: parentNode)
hideAllNodesExcept(node: childNode, rootNode: parentNode) // 선택 노드만 보이게 설정
} else {
print("\(selectedFace) 노드가 씬에 존재하지 않습니다.")
}
}
그리고 한 scn 파일로 불러오면 모델들의 위치가 무작위로 설정되어있는데. 이걸 모두 같은 위치로 옮겨주어야한다.
scn Tool을 사용해 수작업으로 옮겨줄 수 있지만 코드로 구현해보았다.
moveNodeToPosition
func moveNodeToPosition(node: SCNNode, x: Float, y: Float, z: Float) {
node.position = SCNVector3(x, y, z)
}
이렇게 다른 모델을 불러오는 기능을 만들어보았다.
이전보다 모델을 불러오는 속도가 1초정도 느려졌지만 이후에 표정 및 모델전환에 있어서는 지연없는 처리를 할 수 있었다.