갑자기.. 비전이요?
iOS 길을 걷던 나.. 갑작스럽게 visionOS를 해야 할 일이 생겨서 밀도 있는 공부가 필요해졌다.
안그래도 visionOS가 늘 궁금했어서 좋은 기회다!하고 뛰어들어본다.
우선 첫 프로젝트부터 만들어보고, 이래저래 만져보면서 감을 익혀보려 한다.
오늘부터 쭉 연재할 예정이니 많관부!!
프로젝트 생성하기 (기본 세팅)
xcode에서 new Project -> visionOS -> App 을 눌러 프로젝트를 생성한다.
이때 Initial Scene을 선택하는데, 나는 Window와 Volume 중에 Window를 선택했다. 창을 띄워서 SwiftUI로 뷰를 그리고 여러 요소를 띄워보거나 다양한 창을 띄우거나 하는 등의 구현을 하려고 하기 때문이다. Window는 우리가 생각하는 '창'의 개념이고, Volume은 3D 컨텐츠를 띄울 수 있도록 해준다. 공간 컴퓨팅에 관한 기본적인 개념에 대해서는 추후에 별도의 글로 다룰 예정이다.
Immersive Space는 Mixed, Progressive, Full이 있는데 나는 이 중에서 Mixed로 선택했다. Window/Volume과 마찬가지로 이 부분에 대해서도 추후에 하나의 컨텐츠로 다루겠다. 우선은 Mixed로 하는 게 무난하다.
또, RealityKit을 함께 다뤄보고 싶어서 Immersice Space Renderer로 선택했다. 3D 컨텐츠, 애니메이션, 시각적 효과들을 표현하기 위해서는 3D 렌더링 엔진인 RealityKit을 사용하면 된다고 한다. RealityKit을 통해 물리적 광원 조건, 그림자 생성 등을 쉽게 구현할 수 있다고 해서 공부도 할 겸 추가해봤다.
이렇게 프로젝트를 생성하면 자동으로 기본 세팅이 되면서 초기 화면이 보이게 된다. 시뮬레이터로 run을 하기 전에 프리뷰로 확인하니 이런 모습이다. 아무래도 프리뷰는 완전한 뷰를 잘 못보여주는 것 같다 (약간 깨지거나 의도대로 동작을 안함). 기본적으로 제공하는 'Show Immersive Space' 버튼이 프리뷰에서는 동작하지도 않고, 또 윈도우의 형태도 나타나지 않는다.
그러나 시뮬레이터로 실행해보면 이렇게 Window의 형태도 잘 보이고, 'Show Immersive Space' 버튼을 눌렀을 때도 잘 동작한다(양 옆에 두 개의 구가 나타난 모습).
화면에 여러 창 띄우기
그럼 이제 여러 개의 Windows를 띄워보자.
우선 App 단에서 처음에 세팅된 WindowGroup의 id를 Starting Window로 지정하고, 해당 뷰가 나오도록 구성한다.
그리고 띄울 다른 뷰들도 각각 Window 1, 2와 같은 id를 지정해 WindowGroup으로 추가해둔다. 아직은 다른 윈도우에 띄울 뷰가 없으므로 EmptyView를 넣어둔다.
// App file
import SwiftUI
@main
struct VisionOSApp: App {
var body: some Scene {
WindowGroup(id: "Starting Window") {
StartingWindow()
}
.defaultSize(CGSize(width: 600, height: 450))
WindowGroup(id: "Window 1") {
EmptyView()
}
WindowGroup(id: "Window 2") {
EmptyView()
}
}
}
여기 들어갈 StartingWindow는 기존의 ContentView의 이름을 변경해서 활용한 것이다.
ContentView가 앱 실행시 최초의 창이 될 것이기 때문에, 더 명확하게 StartingWindow로 이름을 바꿔주었다.
이제 사용할 이 뷰를 세팅해준다.
// StartingWindow.swift
import SwiftUI
import RealityKit
import RealityKitContent
struct StartingWindow: View {
@Environment(\.openWindow) private var openWindow
var body: some View {
VStack {
Model3D(named: "Scene", bundle: realityKitContentBundle)
.padding(.bottom, 50)
Text("Hello, world!")
// 창을 열 버튼: openWindow를 action으로 추가
HStack {
Button("Window 1") {
openWindow(id: "Window 1")
}
Button("Window 2") {
openWindow(id: "Window 2")
}
}
}
.padding()
}
}
상단에 환경 변수로 openWindow를 추가해준다.
그리고 창을 여는 두 버튼에 openWindow(id: "아이디")로 탭을 했을 때 각각의 창을 열도록 설정한다.
// SampleView.swift
import SwiftUI
struct SampleView: View {
var color: Color
var text: String
var body: some View {
ZStack {
Circle()
.fill(color)
Text(text)
.font(.extraLargeTitle)
}
.padding(50)
}
}
이제 EmptyView 대신 들어갈 SampleView를 작성해준다.
사실 특별한 건 없고, 컬러와 텍스트를 받아서 띄울 생각이다.
// App file
import SwiftUI
@main
struct VisionOSApp: App {
var body: some Scene {
WindowGroup(id: "Starting Window") {
StartingWindow()
}
.defaultSize(CGSize(width: 600, height: 450))
WindowGroup(id: "Window 1") {
SampleView(color: .blue, text: "Window 1")
}
WindowGroup(id: "Window 2") {
SampleView(color: .blue, text: "Window 2")
}
}
}
App 파일에서 EmptyView 대신에 SampleView에 원하는 컬러와 텍스트를 넣어서 호출해주면 끝이다.
시뮬레이터로 실행해서 각각의 버튼을 누르면 아래와 같이 Window 1, 2 창이 켜진다.
인터렉티브하게 반응하는 Shape 구현하기
이번에는 화면과 사용자가 상호작용할 수 있는 뷰를 만들어보려고 한다.
이를 위해 여러 개의 공 형태의 구를 띄우는 Balls 구조체를 작성한다.
ModelEntity를 통해 3D 모델을 생성할 수 있는데, 아래와 같이 반지름이 0.025인 메탈 재질의 간단한 구 모양을 생성해보았다.
// Balls.swift
import SwiftUI
import RealityKit
struct Balls: View {
var body: some View {
RealityView { content in
// 여러 개의 구 생성
for _ in 1...5 {
let model = ModelEntity(
mesh: .generateSphere(radius: 0.025),
materials: [SimpleMaterial(color: .red, isMetallic: true)]
)
}
}
}
}
이 형태가 x, y, z 축에 대해 랜덤 포지션을 가지도록 하고 싶다.
3D로 표현하기 위해 SIMD3라는 구조체를 사용해서 x, y, z 값을 넘겨준다.
// Balls.swift
import SwiftUI
import RealityKit
struct Balls: View {
@State private var scale = false
var body: some View {
RealityView { content in
// 여러 개의 구 생성
for _ in 1...5 {
let model = ModelEntity(
mesh: .generateSphere(radius: 0.025),
materials: [SimpleMaterial(color: .red, isMetallic: true)]
)
// 랜덤 포지션
let x = Float.random(in: -0.2...0.2)
let y = Float.random(in: -0.2...0.2)
let z = Float.random(in: -0.2...0.2)
model.position = SIMD3(x, y, z)
}
})
}
}
모델(entity)에 InputTargetComponent() 를 사용해서 인터렉션을 활성화하고, CollisionComponent 의 generateSphere 를 활용해서 구 형태를 생성한다. (각각에 대한 자세한 정보는 링크로 달아둔 공식 문서 참고!)
잠깐! CollisionComponent란?
참고로, 공식 문서에 따르면 CollisionComponent는 충돌 감지가 가능한 컴포넌트를 생성하는 역할로 보인다.
ShapeResource를 통해 sphere, capsule, convex, box, static mesh 등을 생성할 수 있다. 앞으로 사용할 일이 종종 있을 듯하다.
// 출차: 공식문서 'ShapeResource'
init(
shapes: [ShapeResource],
mode: CollisionComponent.Mode = .default,
filter: CollisionFilter = .default
)
이어서 설명하자면.. 모델을 컨텐츠에 추가한 후, 각각의 entity가 1(기본 크기) 혹은 2(2배로 커짐)를 가지도록 설정한다.
마지막으로 탭 제스처 액션으로 각 앤티티를 탭하면 스케일 이펙트가 토글되도록 설정한다.
// Balls.swift
import SwiftUI
import RealityKit
struct Balls: View {
@State private var scale = false
var body: some View {
RealityView { content in
// 여러 개의 구 생성
for _ in 1...5 {
let model = ModelEntity(
mesh: .generateSphere(radius: 0.025),
materials: [SimpleMaterial(color: .red, isMetallic: true)]
)
// 랜덤 포지션
let x = Float.random(in: -0.2...0.2)
let y = Float.random(in: -0.2...0.2)
let z = Float.random(in: -0.2...0.2)
model.position = SIMD3(x, y, z)
// entity에 인터렉션 활성화
model.components.set(InputTargetComponent())
model.components.set(CollisionComponent(shapes: [.generateSphere(radius: 0.025)]))
content.add(model)
}
} update: { content in
content.entities.forEach { entity in
entity.transform.scale = scale ? SIMD3<Float>(2, 2, 2) : SIMD3<Float>(1, 1, 1)
}
}
.gesture(TapGesture().targetedToAnyEntity().onEnded { _ in
scale.toggle()
})
}
}
시뮬레이터에서 테스트를 해보면 아래와 같이 나오는 것을 볼 수 있다.
랜덤 위치에 구들이 배치되고, 빨간색의 구를 탭(시뮬레이터에서는 마우스 우클릭)하면 2배로 커진다. 다시 탭하면 작아진다.
마무리하며
오늘은 이렇게 첫 프로젝트 생성부터 화면에 여러 창도 띄워보고, 3D 오브젝트를 추가해서 scale 효과를 넣어보는 것까지 구현해봤다.
앞으로 visionOS를 밀도 있게 쭉 공부할 예정이라 블로그에도 꾸준히 글을 작성해보려고 한다.
자료가 많이 없어서 이런저런 공식문서와 유튜브와 블로그와 약간의 삽질 등..을 곁들이고 있으니 잘못된 부분이 있을 수도 있다. 언제든 댓글로 정정하면 적극 반영할 예정..!!
++ 아직 쌩뉴비지만.. visionOS을 한동안 계속 할 것 같아서 의견을 나눌 곳이 필요합니다..!
visionOS하시는 분들의 의견과 소통 언제든 대환영합니다!! 좋은 커뮤니티가 있거나 이야기 나누고 싶으시다면 언제든 댓글이나 연락주세요 🫶 서로서로 도우면서 성장해요!