스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
Xocde에서 Reality Composer Pro 콘텐츠 작업하기
Xcode에서 Reality Composer Pro 콘텐츠에 생명을 불어넣는 법을 알아보세요. Xcode에 3D 씬을 로딩하는 법, 콘텐츠를 코드와 통합하는 법, 앱에 상호작용성을 추가하는 법을 보여드립니다. 그리고 개발 작업 흐름에서 이 툴을 사용할 수 있는 모범 사례와 팁을 공유합니다. 이 세션을 최대한 활용하시려면, 먼저 'Reality Composer Pro 알아보기', 'Reality Composer Pro의 머티리얼 탐색하기' 세션을 시청하여 3D 씬 만들기에 대해 더 알아보시길 권장합니다 .
챕터
- 0:00 - Introduction
- 2:37 - Load 3D content
- 6:27 - Components
- 12:00 - User Interface
- 27:51 - Play audio
- 30:18 - Material properties
- 33:25 - Wrap-up
리소스
관련 비디오
WWDC23
- 첫 몰입형 앱 개발하기
- Reality Composer Pro 알아보기
- Reality Composer Pro의 머티리얼 살펴보기
- RealityKit으로 공간 경험 빌드하기
- RealityKit으로 공간 컴퓨팅 앱 강화하기
WWDC21
-
다운로드
♪ 감미로운 인스트루멘탈 힙합 ♪ ♪ 안녕하세요 저는 Amanda입니다 RealityKit과 Reality Composer Pro를 담당하는 엔지니어죠 이번 세션에서는 Reality Composer Pro에서 조립한 3D 콘텐츠를 사용해 공간 컴퓨팅 경험을 만들어보죠 Reality Composer Pro는 RealityKit 콘텐츠가 공간 컴퓨팅 앱에서 사용되도록 준비하는 개발자 도구입니다 이번 세션에서는 제 동료 Eric과 Niels가 만들었던 프로젝트를 가져다 쓸 건데요 오늘은 그걸 어떻게 코드로 대화하는지 알아보죠 이번에 다룰 에디터 UI와 Reality Composer Pro 기능에 더 친숙해질 수 있도록 아직 못 보신 분들은 다음 세션을 먼저 시청해 주시길 바랍니다 먼저 우리가 만든 완성품을 볼게요 그다음 각 부분을 만든 방법을 알려드리죠 지금 보시는 것은 요세미티 국립공원의 지형도입니다 헤드셋을 쓰고 보시면 이전에 직접 가보지 않고서는 느낄 수 없었던 광활함이 느껴지실 거예요 이전 Reality Composer Pro 세션에서 이 씬은 Eric이 조립했고 Niels는 지형도에 사용한 머티리얼을 만들었습니다 여기서 슬라이더를 추가해 두 캘리포니아 랜드마크를 왔다갔다 할 수 있었죠 지금은 또 LA 인근 해역에 있는 카탈리나 섬이 보이네요 2D SwiftUI 버튼도 이렇게 3D 공간에 배치했고요 여러분이 이 지도 내 관심 지점들에 대해 더 알아보시라고 그렇게 해 놓았었습니다 이번 세션에서는 경험으로 이끌어낼 수 있도록 Reality Composer Pro에서 이 콘텐츠를 어떻게 배치했는지 알아볼 겁니다 이 슬라이더와 관심 지점 버튼을 어떻게 만들었는지 보여드릴게요 추후 Reality Composer Pro에서 만들 씬에 영향을 줄 수 있도록 말이죠 먼저 우리 Reality Composer Pro 프로젝트에서 3D 콘텐츠를 로딩하는 것으로 시작할게요 그다음 RealityKit 컴포넌트 작동 방식과 코드에서 사용하는 방식을 살펴보겠습니다 우리만의 커스텀 컴포넌트도 만들어 보고요 그리고 SwiftUI 속 새 RealityView API에 대해 알아보고 Attachments API를 사용해 사용자 인터페이스 요소를 우리 씬에 추가하는 방법도 발견해 보도록 하죠 그다음 Reality Composer Pro에서 우리가 설정한 오디오와 작동시키는 법을 알아보고요 그다음 Niels가 남겨놓은 프로젝트를 계쇽할 건데요 Shader Graph를 사용해 커스텀 머티리얼을 연결하고 코드에서 그 요소를 도출할 겁니다 그럼 시작해 봅시다 Eric이 진행한 세션에서는 우리가 원하는 대로 배열한 디오라마의 모든 에셋을 포함하고 있는 Reality Composer Pro 프로젝트를 만들었었는데요 상단 탭은 각각 런타임에 우리가 로딩할 수 있는 하나의 루트 엔티티를 나타냅니다 우리는 많은 것들을 하나의 씬에 넣고 그걸 완전 조립 씬으로 처리할 수 있습니다 아니면 몇 개만 넣어서 재사용되는 아상블라쥬 같은 씬으로 처리할 수도 있죠 원하는 만큼 만들 수 있습니다 DioramaAssembled라는 이름의 이 씬이 런타임에 어떻게 로딩되는지 살펴봅시다 우리는 엔티티의 비동기 이니셜라이저를 사용해 Reality Composer Pro 패키지의 콘텐츠가 있는 엔티티를 만들 겁니다 문자열 이름을 사용해 로딩할 엔티티를 지정하고 우리 패키지의 번들을 엔티티에 주도록 하겠습니다 그 이름으로 Reality Composer Pro에서 아무것도 찾지 못하면 아마 던질 겁니다 realityKitContentBundle은 Reality Composer Pro 패키지에서 여러분을 위해 자동생성하는 상수 값인데요 이건 RealityView make 클로저에 들어가죠 RealityView는 SwiftUI의 새로운 뷰인데요 이건 RealityKit의 시작점입니다 SwiftUI와 RealityKit 세계를 연결해주는 다리 역할을 하죠 이 RealityView에 대해서는 세션 후반에 더 이야기해 보도록 하죠 Xcode 프로젝트에서 사용 중이나 Reality Composer Pro 프로젝트에는 추가하지 않을 USD 에셋이 있다면 그 에셋을 Swift Package에 추가하시길 권장합니다 이렇게 이 안에 .rkassets 디렉토리로요 Xcode는 .rkassets 폴더를 런타임에 더 빠르게 로딩되는 포맷으로 컴파일합니다 우리가 막 로딩한 엔티티는 실제로 더 큰 엔티티 계층의 루트일 뿐입니다 이건 자식 엔티티를 가지고 이들도 자식 엔티티를 가집니다 이건 Reality Composer Pro 씬에 우리가 배열한 걸 보여주는데요 엔티티 중 하나를 계층에서 더 낮게 호출하고 싶다면 Reality Composer Pro에서 이름을 주면 됩니다 그다음 런타임에 이름을 통해 엔티티를 찾으라고 요청하면 되고요 엔티티는 ECS의 일부입니다 '엔티티 컴포넌트 시스템'의 약자이죠 이건 RealityKit과 Reality Composer Pro에 전력을 공급합니다 다시 돌아사서 ECS에 대해 더 알아볼까요? ECS는 객체 지향 프로그래밍과 거의 유사하지만 주요한 방식에서 차이를 보입니다 객체 지향 프로그래밍에서는 객체가 그 특성을 정의하는 속성인 프로퍼티를 가지는데요 자체 함수도 있습니다 객체를 정의하는 클래스에 이 프로퍼티와 함수를 쓰죠 하지만 ECS에서 엔티티는 씬에서 보이는 모든 걸 가지죠 보이지 않을 수도 있습니다 그럼에도 속성이나 데이터를 보유하지 않고요 대신 우리의 데이터를 컴포넌트에 넣죠 컴포넌트는 앱 실행 중 언제든지 엔티티에 추가되거나 엔티티에서 삭제 가능합니다 엔티티의 특성을 역동적으로 변화시킬 수 있는 하나의 방법을 제공하죠 시스템은 우리의 동작이 사는 곳이고요 프레임 마다 한 번 호출되는 업데이트 함수를 가집니다 여기서 진행 중인 논리를 집어넣죠 시스템에서 여러분은 특정 컴포넌트를 가지는 모든 엔티티나 컴포넌트의 구성을 문의해 어떤 행위를 수행하고 업데이트된 데이터를 다시 그 컴포넌트에 저장합니다 ECS에 대해 더 깊은 논의를 원하신다면 첫 번째 줄의 2021년 세션과 두 번째 줄의 올해 세션을 시청해 주세요 이제 컴포넌트에 대해 알아보도록 하겠습니다 우리의 Reality Composer Pro 프로젝트에서 컴포넌트를 엔티티에 추가하는 법을 살펴볼게요 그 다음 디오라마에 위치 표시를 하기 위해 커스텀 컴포넌트를 만들어보도록 하겠습니다 Swift에서는 엔티티에 컴포넌트를 추가하기 위해 entity.components.set()를 말하고 컴포넌트 값을 제공할 겁니다 Reality Composer Pro에서는 뷰포트나 계층에서 원하는 엔티티를 선택해야 하는데요
그 다음 Inspector Panel의 하단에서 Add Component 버튼을 클릭해 RealityKit에서 이용 가능한 컴포넌트의 목록을 가져옵니다
하나의 엔티티에 원하는 만큼 컴포넌트를 추가할 수 있고 각 타입에서 하나만 추가할 수도 있습니다 세트죠 이 목록에서도 여러분이 만든 커스텀 컴포넌트가 보일 텐데요 이제 Reality Composer Pro를 사용해 커스텀 컴포넌트 만드는 방법을 알아볼게요 이 지형의 특정 지점 위에 플로팅 버튼을 만들 겁니다 그걸 선택하면 그 지점에 대한 추가 정보를 확인할 수 있도록 말이죠 코드에 많은 UI와 함수를 준비할 것이긴 하지만 Reality Composer Pro에서 이 엔티티들을 우리가 그 플로팅 버튼에 보여주고 싶은 위치로 표시하는 방법을 보여드리려고 합니다 이를 위해 지형도 위 여러 지역에 엔티티를 추가해서 앱한테 신호할 겁니다 그 공간이 우리가 플로팅 버튼을 보여주고 싶은 곳이라고요 그리고 나서 관심 지점 컴포넌트를 만들어 각 장소에 대한 정보를 보관할 겁니다 그다음 편집을 위해 Xcode에서 PointOfInterestComponent.swift를 열어 이름과 설명 같은 프로퍼티들을 추가할게요 그다음 Reality Composer Pro에서 PointOfInterestComponent를 새 엔티티 각각에 추가해 프로퍼티 값을 채울 겁니다 첫 위치 표시 엔티티를 카탈리나 섬에 있는 리본 비치에 만들어 볼게요 눈에 보이지 않는 엔티티를 찾기 위해서 + 메뉴를 클릭해 Transform을 선택할게요
엔티티 이름은 Ribbon_Beach로 하죠 그리고 이 엔티티를 실제 리본 비치가 있는 곳에 설정하도록 하죠 Add Component 버튼을 클릭해서 이번에는 New Component를 선택할게요 커스텀 컴포넌트를 만들 거니까요
이름은 PointOfInterest라고 합시다
이제 다른 컴포넌트들처럼 Inspector Panel에 나타나네요
그런데 count 프로퍼티는 뭐죠? Xcode에서 새 컴포넌트를 열어볼게요 Xcode에서 보니 Reality Composer Pro가 PointOfInterestComponent.swift를 만든 게 보이네요 Reality Composer Pro 프로젝트는 Swift 패키지이고 우리가 생성한 Swift 코드는 이 패키지에 살고 있습니다 템플릿 코드를 보니 count 프로퍼티가 여기서 나왔는지 보이네요 이 프로퍼티 대신 다른 프로퍼티를 가져볼게요 우리는 각 관심 지점이 지도의 어떤 부분과 연관되는지 알기를 원합니다 지도를 바꾸더라도 기존의 관심 지점을 펼쳐서 적절한 곳으로 뚜렷하게 나타날 수 있도록요 그래서 enumeration 프로퍼티인 var region을 추가할게요 enum Region은 여기에 만들어볼게요 그리고 두 개의 케이스를 주겠습니다 지금 카탈리나와 요세미티 지도 두 개를 만들고 있으니까요 이건 하나의 문자열로 직렬화될 수 있습니다 Codable 프로토콜에 따를 거고요 Reality Composer Pro가 이걸 보고 인스턴스를 직렬화할 수 있도록 말이죠 다시 Reality Composer Pro로 돌아오니 count 프로퍼티가 사라졌네요 region 프로퍼티가 새롭게 생겨났고요 이건 yosemite라는 기본값을 가집니다 우리가 코드에서 초기화한 것이니까요 여기서는 특정 엔티티를 위해 재정의할 수 있습니다 재정의하면 이 값은 이 특정 엔티티에서만 효과를 발휘할 겁니다 관심 지점 컴포넌트 나머지도 재정의하지 않는 한 yosemite라는 기본값을 가지게 될 거고요 우리는 PointOfInterestComponent를 이 엔티티에 고정시킨 상징물처럼 사용하고 있는데요 이 엔티티들은 런타임에 SwiftUI 버튼을 놓은 곳에서 플레이스홀더처럼 작용합니다 방금 리본 비치를 추가했던 것과 동일하게 카탈리나 섬 관심 지점도 추가해 보겠습니다 그럼 새 컴포넌트가 실행되는지 볼까요?
앗! 아무 반응이 없네요 그건 이 관심 지점 컴포넌트를 처리하는 코드를 아직 쓰지 않았기 때문이죠 한번 해봅시다 SwiftUI 콘텐츠를 RealityKit 씬에 넣는 새로운 방법이 있는데요 Attachments API라고 부르는 것이죠 attachments와 PointOfInterestComponent를 결합해 커스텀 데이터에 대해 런타임시 떠다니는 버튼을 만들어 보도록 하죠 먼저 코드로 확인하고 데이터 흐름으로 안내해 드릴게요 attachments는 RealityView의 일부입니다 먼저 단순한 예를 통해 RealityView의 구조를 살펴보도록 하죠 SwiftUI 뷰가 RealityKit 씬이 되는 걸 보여드릴게요 우리가 사용할 RealityView 이니셜라이저에는 3가지 매개변수가 있습니다 make 클로저 update 클로저 attachments ViewBuilder입니다 이걸 더 자세히 보기 위해 Attachment View 만들기를 아주 최소한으로 구현해 보겠습니다 초록색 SwiftUI 버튼으로요 그리고 이걸 RealityKit 씬에 추가해 보도록 할게요 attachments ViewBuilder에 보통의 SwiftUI 뷰를 만들었는데요 우리는 뷰 제어자와 제스처 SwiftUI가 주는 모든 것을 사용할 수 있습니다 View 버튼에 해시 가능한 독특한 것을 태그할 건데요 저는 물고기 이모티콘을 이 버튼에 태그하려 합니다 SwiftUI가 update 클로저를 호출했을 때 View 버튼은 하나의 엔티티가 됩니다 이건 이 클로저에 대한 첨부 매개변수에 저장되고요 그리고 우리는 전에 준 태그로 물고기를 보여줄 겁니다 그럼 이것도 다른 엔티티처럼 처리 가능하죠 이걸 씬에 있는 기존 엔티티에 자식으로 추가하거나 콘텐츠의 엔티티 컬렉션에서 새로운 상위 엔티티로 추가할 수도 있습니다 정규 엔티티가 되기 때문에 우리가 원하는 곳에 3D로 나타나게 하도록 포지션 설정도 되고 컴포넌트 추가도 가능합니다 다음은 RealityView의 한 부분에서 다른 부분으로 데이터가 흘러가는 방식입니다 이 RealityView 이니셜라이저에 대한 세 가지 매개변수를 보세요 먼저 make는 Reality Composer Pro 번들에서 하나의 엔티티로 초기 설정 씬을 로딩해 RealityKit 씬에 추가할 수 있습니다 두 번째 update는 뷰 상태에 변화가 생길 때 호출되는 클로저인데요 여기서는 컴포넌트 속 프로퍼티 엔티티의 포지션 등 엔티티의 포지션에 대해 모든 걸 바꿀 수 있고 씬에서 엔티티를 추가하거나 삭제할 수도 있습니다 update 클로저는 모든 프레임에서 실행되지 않고 SwiftUI 뷰 상태가 변할 때만 호출됩니다 세 번째는 attachments ViewBuilder인데요 여기서는 SWiftUI 뷰를 RealityKit 씬에 넣을 수 있습니다 SwiftUI 뷰는 attachments ViewBuilder에서 시작되어 attachments 매개변수에 있는 update 클로저로 전달되죠 여기서 attachments 매개변수에 attachments ViewBuilder에서 버튼에 준 태그를 갖는 엔티티가 존재하는지 물어보게 됩니다 있다면 RealityKit 엔티티를 넘기는 것이죠 update 클로저에서 3D 포지션을 설정해 RealityKit 씬에 추가하면 원하는 곳 어디에서든지 이게 떠다닐 겁니다 저는 여기에 버튼 엔티티를 구 엔티티의 자식으로 추가했습니다 자식을 부모보다 0.2m 높게 위치시켰죠 make 클로저도 attachments 매개변수를 갖습니다 이건 뷰가 처음 평가될 때 시작할 준비가 되어 있는 attachments를 추가하기 위한 겁니다 make 클로저는 한 번만 실행되거든요 이제 RealityView의 일반적인 흐름을 알았으니 update 클로저에 대해 더 알아보도록 하죠 make, update 클로저에 대한 매개변수는 RealityKitContent입니다 RealityKit 콘텐츠에 엔티티를 추가하면 씬에서 상위 엔티티가 되죠 이와 같이 update 함수에서 하나의 엔티티를 콘텐츠에 추가하면 씬에 새로운 상위 엔티티가 주어지는 겁니다 make 클로저는 한 번만 호출되는 반면 update 클로저는 한 번 이상 호출되죠 update 클로저에 새 엔티티를 만들어서 거기 있는 콘텐츠에 추가하면 그 엔티티의 복제 엔티티를 가지게 됩니다 그건 여러분이 원하는 건 아닐지 모르지만요 이를 방지하기 위해 한 번만 실행되는 어딘가에 만들어지는 콘텐츠에만 엔티티를 추가하세요 content.entities가 엔티티를 포함하고 있는지 확인하실 필요는 없습니다 동일한 엔티티를 세트처럼 두 번 부르게 되면 실행되지 않거든요 새 엔티티를 씬에 있는 기존 엔티티의 부모 엔티티로 지정할 때도 똑같이 두 번 추가되지 않고요 attachment 엔티티는 여러분이 만들지 않습니다 attachments ViewBuilder에서 여러분이 제공하는 각 첨부 뷰에 대해 RealityView가 만들거든요 즉 엔티티가 있는지 확인하지 않고 update 클로저 콘텐츠에 추가해도 안전하다는 겁니다 attachments ViewBuilder에서 관심 지점을 하드코딩하고 싶을 때 이렇게 첨부 코드를 쓰시면 되는데요 하지만 우리는 우리 프로젝트에 있는 데이터가 경험을 이끌어내기를 원하기 때문에 더 유연하게 만들어 보려고 합니다 그렇게 해야 디자이너나 제작자들이 Reality Composer Pro 프로젝트에서 관심 지점을 만들 수 있고 우리 코드는 추가한 데이터가 무엇이든지 수용할 수 있죠 데이터 기반으로 만들려면 우리의 코드가 Reality Composer Pro 씬에 설정한 데이터를 읽을 수 있어야 합니다 첨부 뷰를 역동적으로 만들 수 있으려면요 먼저 맨 위에서부터 차례대로 내려가죠 Reality Composer Pro에서 이미 리본 비치에 대한 플레이스홀더 엔티티를 설정했습니다 그리고 디오라마에서 강조하고픈 다른 관심 지점에서도 똑같이 진행할 거고요 각각이 필요로 하는 정보를 모두 채울 겁니다 어느 지도에 속하는지와 이름 같은 것들 말이죠 그리고 코드에서는 그 엔티티에 대해 질문하고 각 엔티티에 대해 새로운 SwiftUI 버튼을 만들 겁니다 SwiftUI가 attachments ViewBuilder를 호출하려면 컬렉션에 새 버튼을 추가할 때마다 그 컬렉션에 @State 프로퍼티 래퍼를 추가해야 합니다 버튼들은 attachments ViewBuilder에 제공할 거고요 마지막으로 RealityView의 update 클로저에서 버튼을 엔티티로 받아 새 버튼 엔티티를 씬에 추가할 겁니다 각 엔티티에 Reality Composer Pro에서 설정한 마커 엔티티의 자식 엔티티로 추가할 거고요 더 자세한 그림을 통해 다음 6단계를 알아보고 코드를 살펴보겠습니다 먼저 Reality Composer Pro 씬에 보이지 않는 엔티티를 추가합니다 그 엔티티들을 x, y, z 축 위 버튼이 나타났으면 하는 곳에 위치시킵니다 Transform Component를 여기서 사용할 건데요 이건 모든 엔티티가 기본으로 가지는 컴포넌트입니다 그리고 각각에 PointOfInterestComponent를 추가할 거고요 코드에서는 PointOfInterestComponent를 가진 모든 엔티티에 대해 질문하여 이 엔티티에 대한 참조를 얻을 겁니다 질문은 우리가 Reality Composer Pro에서 설정한 세 개의 보이지 않는 엔티티를 반환할 거고요 우리는 각각에 대해 새 SwiftUI 버튼을 만들고 컬렉션에 저장할 겁니다 그 버튼을 RealityView로 가져오기 위해 SwiftUI 뷰 업데이트 플로우를 사용할 건데요 @State 프로퍼티 래퍼를 우리 View의 버튼 컬렉션에 추가할 거란 의미입니다 @State 프로퍼티 래퍼는 SwiftUI에게 이 컬렉션에 항목을 추가할 때 SwiftUI가 ImmersiveView에서 뷰 업데이트를 유도해야 한다고 말할 겁니다 SwiftUI는 attachments ViewBuilder와 update 클로저를 다시 평가하게 될 거고요 RealityView attachments ViewBuilder는 우리가 SwiftUI에게 이 버튼들이 엔티티로 만들어졌으면 좋겠다고 선언할 곳입니다 RealityView의 update 클로저는 다음에 호출될 것이고 버튼은 우리에게 엔티티로 전달되겠죠 버튼은 더 이상 SwiftUI Views가 아니게 됩니다 그래서 우리의 엔티티 계층에 추가하는 거죠 update 클로저에서 attachment 엔티티를 추가할 건데 보이지 않는 엔티티 각각의 위에서 떠다니는 것으로 배치가 될 겁니다 이제 디오라마 씬을 보면 시각적으로 나타나 있겠죠? 이제 각 단계가 어떻게 실행되는지 봅시다 먼저 Reality Composer Pro 씬에서 보이지 않는 엔티티를 표시합니다 표시한 엔티티를 찾기 위해 EntityQuery를 만들 건데요 이걸 사용해 PointOfInterestComponent를 가진 모든 엔티티에 대해 질문할 겁니다 QueryResult를 통해 이를 반복하고 우리의 씬에서 PointOfInterestComponent를 갖는 각 엔티티에 대해 새 SwiftUI View를 만들 거고요 컴포넌트에서 얻은 데이터로 여기를 채울 건데요 그건 Reality Composer Pro에 입력한 데이터죠 그 뷰는 우리 첨부 뷰 중 하나가 될 겁니다 그래니 태그를 달아보죠 이 경우 좀 진지해져야겠죠? 물고기 이모티콘 대신 ObjectIdentifier를 사용할게요 여기서 SwiftUI Views 컬렉셔닝 만들어집니다 이걸 attachmentsProvider라고 부를게요 우리의 첨부 뷰를 RealityView의 attachments ViewBuilder에 제공할 테니까요 그다음 attachmentsProvider에 우리의 뷰를 저장할 겁니다 그 컬렉션 타입을 한번 살펴볼게요 AttachmentsProvider는 뷰에 대한 첨부 태그의 딕셔너리를 가집니다 우리는 LearnMoreView 외에도 다른 류의 뷰를 넣을 수 있게 뷰 타입을 삭제했습니다 그리고 sortedTagViewPairs라는 계산 프로퍼티를 갖습니다 튜플 배열을 반환하죠 태그와 그에 일치하는 값을요 매번 똑같은 순서로 반환합니다 attachments ViewBuilder에는 우리가 만든 첨부 컬렉션을 통해 ForEach를 하게 될 겁니다 이건 SwiftUI에게 우리가 SwiftUI에게 준 쌍 각각에 대해 하나의 뷰를 원하며 컬렉션에서 뷰를 제공할 거라고 말합니다 여기서 우리는 ObjectIdentifier가 이중 역할을 하게 하는데 하나는 뷰에 대한 첨부 태그 하나는 ForEach 구조에 대한 식별자입니다 왜 우리는 그 대신 PointOfInterestComponent에 tag 프로퍼티를 추가하지 않았을까요? 첨부 뷰 태그는 유일무이해야 합니다 ForEach 구조에 대해서도 작동할 첨부 뷰 메커니즘에 대해서도 말이죠 커스텀 컴포넌트에 있는 모든 프로퍼티는 Reality Composer Pro의 Inspector Panel에 보이니 컴포넌트를 엔티티에 추가한다는 것은 attachmentTag도 나타날 거란 뜻입니다 Reality Composer Pro에서 각 관심 지점을 추가할 때 모든 태그를 특수하게 만들어야 한다고 기억해야 하는 건 싫습니다 그런데 우리가 편하도록 엔티티는 Identifiable 프로토콜을 따라 자동으로 유일무이한 식별자를 가지게 되죠 Reality Composer Pro에서 씬을 디자인할 때 사전에 식별자를 알아야 할 필요 없이 런타임에 엔티티에서 식별자를 얻을 수 있다는 거죠 attachmentTag 프로퍼티를 Reality Composer Pro에서 나타나지 않게 하려면 이 테크닉을 사용해야 합니다 '디자인 타임 컴포넌트와 런타임 컴포넌트의 대결' 우리는 우리의 데이터를 두 컴포넌트로 나눌 겁니다 Reality Composer Pro에서 배치하고자 하는 디자인타임 데이터용 하나와 런타임에 동일한 엔티티에 동적으로 첨부될 런타임용 하나로요 이건 Reality Composer Pro의 Inspector Panel에서 보이고 싶지 않은 프로퍼티를 위한 건데요 그래서 새 컴포넌트인 PointOfInterestRuntimeComponent를 정의하고 이 안으로 첨부 뷰 태그를 옮길 겁니다 Reality Composer Pro는 Swift 패키지에서 읽은 것을 기반으로 하여 컴포넌트 UI를 자동 구축합니다 패키지 안의 Swift 코드를 검사하고 여러분이 씬에서 사용할 수 있다고 판단한 코딩 가능한 컴포넌트를 만들죠 4개의 컴포넌트 보이시나요? 컴포넌트 A와 B는 Xcode 프로젝트에는 있지만 Reality Composer Pro 패키지에는 없어서 Reality Composer Pro에 있는 엔티티에는 첨부할 수가 없을 겁니다 컴포넌트 C는 패키지에 있지만 코딩이 불가능해서 Reality Composer Pro가 무시할 거고요 여기 보이는 4개의 컴포넌트 중 컴포넌트 D만이 Swift 패키지에도 있고 코딩 가능한 컴포넌트이기에 Reality Composer Pro의 목록에 보여질 겁니다 저게 우리의 디자인타임 컴포넌트입니다 다른 건 모두 런타임 컴포넌트로 사용되고요 디자인타임 컴포넌트는 정수형, 문자열, SIMD 값 등 3D 아티스트와 디자이너들이 사용하는 것들 같이 더 단순한 데이터를 위한 것입니다 Reality Composer Pro가 직렬화하지 않은 타입인 커스텀 컴포넌트에 대해 프로퍼티를 추가한다면 Xcode 프로젝트에는 오류가 나타날 겁니다 이제 코드로 돌아가보죠 먼저 PointOfInterest 런타임 컴포넌트를 엔티티에 추가하고 나서 그 런타임 컴포넌트를 사용해 우리의 attachment 엔티티를 디오라마에서 일치하는 관심 지점에 맞춰봅시다 런타임 컴포넌트가 들어갈 때입니다 지금은 PointOfInterest 엔티티에서 읽고 첨부 뷰를 만드는 단계인데요 모든 디자인타임 컴포넌트에 질의를 했었죠 그래서 각각에 맞는 새로운 런타임 컴포넌트를 만들 겁니다 런타임 컴포넌트에 attachmentTag를 저장하고 런타임 컴포넌트를 그 동일한 엔티티에 저장하죠 이 방법으로 디자인타임 컴포넌트는 신호 같아집니다 앱에게 자신을 위한 첨부 뷰를 원한다고 말하죠 런타임 컴포넌트는 앱 실행 중 우리에게 필요한 모든 종류의 데이터를 처리하나 디자인타임 컴포넌트에는 저장하고 싶어 하지 않습니다 RealityView에서는 attachment 엔티티가 씬에 나타나는 걸 보기 전에 한 단계 더 남았는데요 attachments ViewBuilder에 SwiftUI Views를 제공하면 SwiftUI는 RealityView의 update 클로저를 호출하여 우리의 첨부 뷰를 RealityKit 엔티티로 줍니다 하지만 그들을 위치시키지 않고 콘텐츠에 추가하면 씬의 원점인 좌표 (0, 0, 0)에 나타날 겁니다 거긴 우리가 원하는 곳이 아니죠 우리는 첨부 뷰가 지형도의 관심 지점 위에 떠있길 원합니다 attachment 엔티티를 우리가 Reality Composer Pro에서 설정한 보이지 않는 point of interest 엔티티에 매칭해야 할 필요가 있습니다 보이지 않는 엔티티에 넣은 런타임 컴포넌트는 안에 태그를 가지는데요 그렇게 point of interest 엔티티 각각에 대해 attachmentEntity를 맞추게 됩니다 모든 PointOf InterestRuntimeComponents에 질의를 하여 질의에서 반환된 각 엔티티로부터 런타임 컴포넌트를 얻게 되죠 그 다음 컴포넌트의 attachmentTag 프로퍼티를 사용해 attachments 매개변수에서 update 클로저까지 attachmentEntity를 얻게 됩니다 그럼 attachmentEntity를 콘텐츠에 추가해 point of interest 엔티티보다 0.5m 위에 배치하죠 다시 앱을 다시 실행해서 어떻게 나타나는지 볼까요? 오, 아주 좋네요! Reality Composer Pro 프로젝트에 넣은 지점 위로 각각의 장소명이 떠다니는 게 보이네요 다음으로 Reality Composer Pro에서 설정한 오디오 재생 방법을 알아보도로 하겠습니다 Reality Composer Pro에서 오디오 재생하는 걸 설정하려면 audio 엔티티를 가져오는데 + 버튼을 클릭해 Audio 선택 후 Ambient Audio를 선택하면 됩니다 그럼 AmbientAudioComponent가 있는 보통의 보이지 않는 엔티티를 만들죠 엔티티의 이름을 OceanEmitter라고 합시다 카탈리나 섬의 바다 사운드를 내기 위해 이 엔티티를 사용할 거니까요 씬에 오디오 파일도 추가해야 합니다 바다 사운드를 가지고 올게요
Inspector Panel에서 컴포넌트의 Preview 메뉴에서 사운드를 선택해 오디오 컴포넌트를 미리 볼 수는 있지만 앱에 엔티티가 로딩되면 자동으로 선택한 사운드를 재생하지는 않을 겁니다 그래서 오디오 리소스를 로딩해 재생하라고 말해줘야 하죠 사운드 재생을 위해 오디오 컴포넌트가 들어간 엔티티에 대한 참조를 얻을 겁니다 엔티티에 OceanEmitter라는 이름을 붙였으니 이름으로 엔티티를 찾을 수 있을 거예요 AudioFileResource 이니셜라이저를 사용해 사운드 파일을 로딩합니다 전체 경로를 지나 씬에 있는 오디오 파일 리소스 프림까지 보내죠 Reality Composer Pro에서 이 파일을 포함한 .usda 파일의 이름을 주도록 하겠습니다 이 경우 DioramaAssembled.usda라는 우리의 메인 씬이네요 entity.prepareAudio를 호출함으로써 audioPlaybackController를 만들면 이 소리를 재생하고 정지하고 끌 수 있습니다 이제 재생하라고 호출할 준비가 된 거죠 이제 앱에서 재생되는 바다 사운드 소리를 들어보죠 앱의 슬라이더는 요세미티와 카탈리나 섬 두 개의 지형도 사이에서 변환하게 만들죠
씬에 오디오를 추가했으니 두 오디오 소스를 크로스페이딩 해볼게요 ocean emitter 엔티티를 추가했던 것과 같이 숲 오디오도 추가해 보겠습니다 슬라이더를 사용해 지형을 변환하는 모습이 어떤지 살펴 보고 이 변화 속에 오디오를 포함시키겠습니다 두 지형 사이를 전환하려면 Shader Graph 머티리얼의 프로퍼티를 사용해야 하는데요 한번 시작해 보죠 Niels가 우리가 Shader Graph를 사용할 수 있게 이 아름다운 기하 제어자를 만들어 놓았는데요 이제 이걸 우리 씬에 연결해 런타임에 매개변수 일부를 도출할 겁니다 이 Shader Graph 머티리얼을 슬라이더와 연결하고 싶은데요 이를 위해서는 입력 노드를 승격시켜야 합니다 노드에서 커맨드 키를 눌러 Promote를 선택하세요 그럼 프로젝트에게 런타임에 머리티얼의 이 부분으로 데이터를 공급할 것이라고 말할 겁니다 이 승격된 노드의 이름은 Progress라고 하죠 런타임에 이름으로 호출할 수 있도록요 이제 코드에서 이 값을 역동적으로 바꿀 수 있습니다 머티리얼이 있는 엔티티에 대해 참조를 얻습니다 그럼 이 ModelComponent를 얻게 되는데요 이건 머티리얼을 보관하는 RealityKit 컴포넌트죠 ModelComponent에서 첫 번째 머티리얼을 얻고요 이 특정 엔티티에는 머티리얼이 하나 뿐입니다 이걸 ShaderGraphMaterial 타입에 던져 보죠 이제 Progress라는 이름의 매개변수에 대해 새 값을 설정할 수 있습니다 마지막으로 머티리얼을 다시 ModelComponent에 저장하고 ModelComponent를 다시 terrain 엔티티에 저장하죠 이제 이걸 SwiftUI 슬라이더에 연결해 보죠 슬라이더의 값이 바뀔 때마다 우리는 그 값을 찾을 건데요 범위는 0~1 사이입니다 그리고 이걸 ShaderGraphMaterial에 전달할 겁니다 다음으로 두 지형에 대한 온화한 오디오 트랙 두 가지를 크로스페이딩 해봅시다 두 audio 엔티티인 ocean과 forest에 AmbientAudioComponent를 넣었기 때문에 그에 대한 gain 프로퍼티를 사용해 소리 크기를 조정할 수 있죠 AmbientAudioComponent를 가진 모든 엔티티에 질의를 할 건데요 여기선 ocean과 forest 두 가지 모두 해당되겠죠 그리고 RegionSpecificComponent라는 다른 커스텀 컴포넌트를 추가해 어떤 지역에 대한 엔티티인지 표시할 겁니다 audioComponent의 복사 컴포넌트를 얻습니다 이걸 바꾸고 다시 우리 엔티티에 저장할 거거든요 어떤 이익이 region과 sliderValue에 주어지는지 계산하는 함수를 호출합니다 이익 값은 AmbientAudioComponent에 설정하고 그 컴포넌트는 다시 엔티티에 저장하죠 어떻게 작동하는지 봅시다
좋습니다! 슬라이더를 움직이니 Shader Graph 머티리얼이 지형도의 기하를 변화시키는 게 보이네요 숲 소리가 사라지고 바닷소리가 커지는 것도 들을 수가 있고요
오늘 다룬 정보가 정말 많은데요 한번 정리해 봅시다 우선 Reality Composer Pro 콘텐츠를 Xcode 앱에 로딩하는 법을 배웠고요 Reality Composer Pro에서 커스텀 컴포넌트를 만드는 법도 살펴봤습니다 SwiftUI Attachments API의 작동 방법과 우리에게 엔티티로 전달되는 방식도 살펴보았고요 오디오를 설정하여 코딩에서 재생하는 법도 알아봤습니다 마지막으로 승격된 material 프로퍼티를 가져와 코드에서 도출하는 법도 살펴보았고요 이 작업 흐름을 통해 여러분의 삶에 공간 경험을 가져올 수 있었으면 합니다 우리 새 플랫폼에서 여러분이 만드실 놀라운 것들을 기대하고 있을게요 감사합니다 ♪
-
-
3:12 - Loading an entity
RealityView { content in do { let entity = try await Entity(named: "DioramaAssembled", in: realityKitContentBundle) content.add(entity) } catch { // Handle error } }
-
6:39 - Adding a component
let component = MyComponent() entity.components.set(component)
-
12:21 - Attachments data flow
RealityView { _, _ in // load entities from your Reality Composer Pro package bundle } update: { content, attachments in if let attachmentEntity = attachments.entity(for: "🐠") { content.add(attachmentEntity) } } attachments: { Button { ... } .background(.green) .tag("🐠") }
-
15:48 - Adding attachments
let myEntity = Entity() RealityView { content, _ in if let entity = try? await Entity(named: "MyScene", in: realityKitContentBundle) { content.add(entity) } } update: { content, attachments in if let attachmentEntity = attachments.entity(for: "🐠") { content.add(attachmentEntity) } content.add(myEntity) } attachments: { Button { ... } .background(.green) .tag("🐠") }
-
20:43 - Adding point of interest attachment entities
static let markersQuery = EntityQuery(where: .has(PointOfInterestComponent.self)) @State var attachmentsProvider = AttachmentsProvider() rootEntity.scene?.performQuery(Self.markersQuery).forEach { entity in guard let pointOfInterest = entity.components[PointOfInterestComponent.self] else { return } let attachmentTag: ObjectIdentifier = entity.id let view = LearnMoreView(name: pointOfInterest.name, description: pointOfInterest.description) .tag(attachmentTag) attachmentsProvider.attachments[attachmentTag] = AnyView(view) }
-
21:40 - AttachmentsProvider
@Observable final class AttachmentsProvider { var attachments: [ObjectIdentifier: AnyView] = [:] var sortedTagViewPairs: [(tag: ObjectIdentifier, view: AnyView)] { ... } } ... @State var attachmentsProvider = AttachmentsProvider() RealityView { _, _ in } update: { _, _ in } attachments: { ForEach(attachmentsProvider.sortedTagViewPairs, id: \.tag) { pair in pair.view } }
-
22:31 - Design-time and Run-time components
// Design-time component public struct PointOfInterestComponent: Component, Codable { public var region: Region = .yosemite public var name: String = "Ribbon Beach" public var description: String? } // Run-time component public struct PointOfInterestRuntimeComponent: Component { public let attachmentTag: ObjectIdentifier }
-
25:38 - Adding a run-time component for each design-time component
static let markersQuery = EntityQuery(where: .has(PointOfInterestComponent.self)) @State var attachmentsProvider = AttachmentsProvider() rootEntity.scene?.performQuery(Self.markersQuery).forEach { entity in guard let pointOfInterest = entity.components[PointOfInterestComponent.self] else { return } let attachmentTag: ObjectIdentifier = entity.id let view = LearnMoreView(name: pointOfInterest.name, description: pointOfInterest.description) .tag(attachmentTag) attachmentsProvider.attachments[attachmentTag] = AnyView(view) let runtimeComponent = PointOfInterestRuntimeComponent(attachmentTag: attachmentTag) entity.components.set(runtimeComponent) }
-
26:19 - Adding and positioning the attachment entities
static let runtimeQuery = EntityQuery(where: .has(PointOfInterestRuntimeComponent.self)) RealityView { _, _ in } update: { content, attachments in x rootEntity.scene?.performQuery(Self.runtimeQuery).forEach { entity in guard let component = entity.components[PointOfInterestRuntimeComponent.self], let attachmentEntity = attachments.entity(for: component.attachmentTag) else { return } content.add(attachmentEntity) attachmentEntity.setPosition([0, 0.5, 0], relativeTo: entity) } } attachments: { ForEach(attachmentsProvider.sortedTagViewPairs, id: \.tag) { pair in pair.view } }
-
28:55 - Audio Playback
func playOceanSound() { guard let entity = entity.findEntity(named: "OceanEmitter"), let resource = try? AudioFileResource(named: "/Root/Resources/Ocean_Sounds_wav", from: "DioramaAssembled.usda", in: RealityContent.realityContentBundle) else { return } let audioPlaybackController = entity.prepareAudio(resource) audioPlaybackController.play() }
-
31:02 - Terrain material transition using the slider
@State private var sliderValue: Float = 0.0 Slider(value: $sliderValue, in: (0.0)...(1.0)) .onChange(of: sliderValue) { _, _ in guard let terrain = rootEntity.findEntity(named: "DioramaTerrain"), var modelComponent = terrain.components[ModelComponent.self], var shaderGraphMaterial = modelComponent.materials.first as? ShaderGraphMaterial else { return } do { try shaderGraphMaterial.setParameter(name: "Progress", value: .float(sliderValue)) modelComponent.materials = [shaderGraphMaterial] terrain.components.set(modelComponent) } catch { } } }
-
31:57 - Audio transition using the slider
@State private var sliderValue: Float = 0.0 static let audioQuery = EntityQuery(where: .has(RegionSpecificComponent.self) && .has(AmbientAudioComponent.self)) Slider(value: $sliderValue, in: (0.0)...(1.0)) .onChange(of: sliderValue) { _, _ in // ... Change the terrain material property ... rootEntity?.scene?.performQuery(Self.audioQuery).forEach({ audioEmitter in guard var audioComponent = audioEmitter.components[AmbientAudioComponent.self], let regionComponent = audioEmitter.components[RegionSpecificComponent.self] else { return } let gain = regionComponent.region.gain(forSliderValue: sliderValue) audioComponent.gain = gain audioEmitter.components.set(audioComponent) }) } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.