스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
볼륨 및 몰입형 공간 자세히 알아보기
visionOS에서 볼륨 및 몰입형 공간을 맞춤화하는 강력하고 새로운 방법을 살펴보세요. 볼륨의 크기 조정 방식, 볼륨이 주위 사람들에게 반응하는 방식을 세밀하게 조정하는 법을 알아봅니다. 좌표 변환 기능을 활용하여 볼륨 및 몰입형 공간이 상호작용하도록 만들어 보세요. 사용자가 Digital Crown으로 몰입감을 조정할 때 앱이 반응하도록 하는 방법과, 주변 효과를 사용해 몰입형 공간 경험에서 패스스루의 색조를 동적으로 맞춤화하는 방법을 확인할 수 있습니다.
챕터
- 0:00 - Introduction
- 2:04 - Volumes
- 2:06 - Volumes: Baseplate
- 4:08 - Volumes: Size
- 6:59 - Volumes: Toolbars
- 8:48 - Volumes: Ornaments
- 11:36 - Volumes: Viewpoints
- 15:34 - Volumes: World alignment
- 16:52 - Volumes: Dynamic scale
- 18:26 - Intermezzo
- 18:42 - Immersive spaces
- 19:38 - Immersive spaces: Coordinate conversions
- 22:40 - Immersive spaces: Immersion styles
- 26:08 - Immersive spaces: Anchored UI interactions
- 29:03 - Immersive spaces: Surroundings effects
- 31:21 - Next steps
리소스
관련 비디오
WWDC24
WWDC23
-
다운로드
안녕하세요! SwiftUI 엔지니어 Owen입니다 저는 Troy입니다 저도 SwiftUI 엔지니어입니다 이 비디오에서는 visionOS의 볼륨 및 몰입형 공간에서 3D 콘텐츠로 작업하기에 대해 자세히 알아봅니다
visionOS의 장면 유형은 윈도우, 볼륨 몰입형 공간 등 3개입니다 3가지를 함께 사용해 고유하고 흥미로운 경험을 선사할 수 있죠 오늘은 볼륨과 몰입형 공간에 초점을 맞추겠습니다 visionOS 고유 유형으로 풍부하고 몰입감 높은 3D 콘텐츠에 사용되죠 이 유형을 사용하는 볼류메트릭 앱은 Apple Vision Pro의 가장 흥미롭고 고유한 기능 중 하나입니다 앱과 게임에 대해 새로운 차원을 구현하여 사람들이 실제 세계에 존재하는 앱을 경험해 볼 수 있게 합니다
visionOS에서 이미 많은 즐거운 공간 경험이 제공되고 있습니다 이러한 앱은 세 번째 차원을 활용하여 스마트한 방식으로 정보를 표시하고 재미있고 유쾌한 상호작용을 제공합니다 실제 세계를 모방할 수도 있고 완전히 새로운 것일 수도 있습니다
이러한 앱은 visionOS용 공간 API를 활용합니다 이제 visionOS 2에서는 많은 새로운 기능을 추가하여 볼류메트릭 앱을 한층 더 생생하게 구현할 수 있게 합니다 Troy와 저는 Botanist라는 새로운 볼류메트릭 앱을 빌드하는 과정을 안내합니다 우선 저는 볼륨을 추가하고 새로운 API로 빌드하여 멋진 앱을 완성합니다
그 후 Troy는 몰입형 공간으로 앱을 확장하여 공간을 채웁니다
그럼, 볼륨으로 시작하겠습니다! 새로운 볼류메트릭 앱을 만들거나 기존 앱을 visionOS 2 SDK에 대해 연결할 때 가장 먼저 눈에 띄는 것 중 하나는 새로운 베이스플레이트입니다
바라보면 자동으로 나타나며 볼륨의 하단 가장자리가 강조됩니다
새 볼륨에서 베이스플레이트가 작동하는 모습입니다 볼륨의 가장자리로 부드럽게 안내해 주니까 제가 작업하는 공간의 크기가 얼마나 되는지 즉시 알 수 있죠
베이스플레이트는 볼륨 내부에 있는 콘텐츠에 적합하지만 경계를 채우지는 않습니다 사람들이 볼륨의 가장자리를 파악할 수 있게 합니다 콘텐츠가 그 안에서 더 작은 공간을 차지하는 경우에도요 하지만 앱의 콘텐츠가 이미 볼륨의 경계까지 도달했거나 나만의 표면을 이미 그린 경우 베이스플레이트를 비활성화하는 것이 더 좋습니다 그러면 앱과 충돌하지 않고 콘텐츠 자체가 사람들을 가장자리로 안내하게 됩니다
visionOS 2에서 베이스플레이트는 기본적으로 활성화되며 volumeBaseplateVisibility 한정자로 제어될 수 있습니다 visionOS 2 자동 동작은 바라보면 베이스플레이트에 페이드 인됩니다 이는 visible로 한정자를 작성하는 것과 동일합니다 베이스플레이트는 hidden으로 설정해 명시적으로 끌 수도 있죠
베이스플레이트로 시작하면 볼륨의 경계를 즉각적으로 파악할 수 있습니다 추가 콘텐츠가 없는 경우에도요
이제 Botanist 앱에 대해 원형 레벨을 추가할 때 베이스플레이트 덕분에 볼륨의 가장자리와 모서리를 찾을 수 있고 여기에 윈도우 제어기가 있습니다 이는 visionOS 2에서 특히 중요합니다 이제 볼륨의 모서리에 새로운 크기 조정 핸들이 있기 때문입니다 윈도우와 마찬가지로요
베이스플레이트가 모서리의 크기 조정 핸들로 안내해 줍니다 하지만 장면의 크기를 조정하려고 하면 원래 크기로 되돌아갑니다 왜 그럴까요? 볼륨은 최소 및 최대 크기를 기본적으로 콘텐츠의 크기에서 가져옵니다 SwiftUI에서 이 동작은 windowResizability 한정자가 contentSize 동작을 사용해 제공합니다 볼륨의 자동 동작은 이 한정자가 볼륨에 작성된 것처럼 작동합니다 즉, 볼륨의 최소 크기와 최대 크기를 모두 뷰의 크기에서 가져옵니다
ExplorationView에서 특정 너비 높이, 깊이로 프레임을 작성해서 볼륨의 크기가 해당 뷰의 프레임과 일치하도록 고정됩니다
이를 변경하여 그 대신에 프레임의 최소 값을 지정하면 크기를 더 크게 조정할 수 있다고 뷰에 표시됩니다 또한 볼륨은 콘텐츠의 크기를 가져오기 때문에 이제 크기 조정도 가능합니다
최대 크기를 지정하면 볼륨의 크기도 제한됩니다
이제 크기 조정 핸들을 드래그하면 볼륨 크기가 매끄럽게 조정됩니다
아주 깔끔하네요!
이 동작은 코드에서 볼륨의 크기를 조정할 수도 있다는 뜻입니다 앱이 콘텐츠를 업데이트할 때 뷰의 프레임에 대한 모든 변경 사항에 따라 볼륨의 크기도 결정됩니다 따라서 변경되는 콘텐츠를 쉽게 수용할 수 있습니다 장면의 경계에서 잘릴 걱정 없이 말입니다
사람들은 콘텐츠 근처의 모서리에서 크기 조정 핸들을 찾기 때문에 이렇게 하면 해당 제어기가 너무 멀리 떨어져 있지 않습니다
볼륨의 크기를 프로그래밍 방식으로 변경하려면 해당 크기에 대한 새로운 상태 변수를 추가합니다
크기 변수의 값을 변경하면 뷰의 프레임 값이 업데이트되고 해당 새 크기에 맞게 볼륨 크기가 자동으로 조정됩니다
또한 원형 탐색 레벨에 대한 RealityKit 엔티티의 크기도 해당 양만큼 조정합니다 이제 크기를 변경할 제어기가 필요합니다
크기를 토글하는 버튼을 추가하고 지금은 이를 오버레이에 배치하죠
이제 버튼을 누르면 해당 레벨이 작은 크기와 큰 크기 간에 전환되고 볼륨의 경계가 일치하도록 조정됩니다 하지만 버튼 자체가 여기에 좀 적합하지 않은 것 같네요 도구 막대에 완벽하게 어울릴 것 같습니다
볼륨은 그 아래의 오너먼트에 표시되는 도구 막대를 지원합니다 이렇게 유용하고 쉬운 방법으로 일반적인 앱 제어기 모음을 제공할 수 있습니다
볼륨이 이동하면 이 도구 막대의 크기가 자동으로 조정되므로 볼륨의 배치와 관계없이 해당 콘텐츠에 계속 접근할 수 있습니다 도구 막대는 일반적인 앱 제어기를 그룹화하는 최고의 방법인 만큼 앱에 하나를 추가하겠습니다
도구 막대에 제어기를 배치하려면 앱의 뷰에서 도구 막대 한정자를 작성합니다
도구 막대 안에 도구 막대 항목 및 항목 그룹을 만들 수 있습니다
각 항목에 대해 .bottomOrnament 배치를 지정합니다 visionOS 1 호환성을 위한 필수 사항입니다 하지만 visionOS 2에서는 자동 배치도 하단 오너먼트로 해결됩니다 앱이 visionOS 2만 대상으로 하면 인수를 생략해도 됩니다
도구 막대 항목에 버튼을 추가해 다양한 게임 동작을 수행합니다
이렇게 하면 제어기가 도구 막대에 나타납니다!
visionOS 2의 새로운 특징은 도구 막대가 제가 서 있는 면으로 자동으로 이동한다는 것입니다 윈도우 제어기와 함께요 따라서 사람들이 해당 경험에 새로운 각도로 접근할 수 있으며 항상 모든 도구가 정확히 필요한 곳에 유지됩니다 도구 막대와 함께, 오너먼트를 볼륨에 더 추가할 수 있습니다 오너먼트는 추가 제어기 또는 현재 콘텐츠에 대한 더 자세한 정보를 제공하기에 적합합니다
기본 윈도우를 깔끔하게 유지하는 데 도움이 됩니다 보조 정보를 앱 윈도우의 상단과 주위에 떠 있게 하는 방법을 사용합니다 윈도우는 오너먼트를 어디에나 추가할 수 있습니다 도구 모음으로만이 아니라 윈도우 주위의 모든 위치에도 가능합니다 이제 볼륨도 동일하게 할 수 있습니다 볼륨에서 오너먼트가 얼마나 깊이 배치되는지도 제어할 수 있죠 또한 오너먼트는 크기가 동적으로 조정되므로 볼륨이 더 멀리 이동해도 편리한 크기로 유지됩니다
오너먼트는 큰 유연성을 제공하지만 과용하지 않는 것이 중요합니다
볼륨 주위에 많은 오너먼트를 배치하면 앱의 진정한 주인공인 멋진 콘텐츠를 밀어낼 수 있습니다 단일 오너먼트는 제어기 및 정보 그룹을 위한 훌륭한 컨테이너가 됩니다 또한 도구 막대와 탭 뷰 등 시스템에서 제공하는 일부 제어기도 오너먼트로 제공되므로 맞춤형 오너먼트가 이 오너먼트와 충돌하지 않도록 하세요
앱에서 플레이어의 심기 목표 달성 상황을 보여 주는 이 뷰를 추가했습니다 현재, 해당 뷰는 볼륨의 기본 뷰 내부에 있습니다
따라서 제가 볼륨을 돌아다니는 동안 업데이트되지 않습니다 볼륨을 더 멀리 이동하면 이 뷰가 더 작아져서 더 읽기 어려워집니다 이 뷰를 오너먼트로 가져오면 좋을 것 같습니다 자동 동작을 사용할 수 있게 되니까요 여기에 뷰가 볼륨의 본문 내부에 있습니다
뷰를 오너먼트 내부에 배치하려면 뷰를 오너먼트 한정자로 이동하죠
장면 앵커를 UnitPoint3D를 사용해 제공합니다 그러면 오너먼트를 배치할 때 볼륨의 너비, 높이, 깊이를 고려합니다 제 경우에는 topBack 배치를 사용해 오너먼트를 기본 레벨 상단과 뒤쪽의 중앙에 배치합니다
앱에서 이를 확인해 보죠
이렇게 기본 레벨 뒤쪽에 떠 있습니다 도구 막대와 마찬가지로 모든 오너먼트도 볼륨 주위를 따라다니며 항상 모든 방향에서 접근할 수 있도록 해야 합니다 장면에서의 위치는 항상 제가 볼륨을 보고 있는 쪽을 기준으로 결정됩니다
볼륨의 각 면이 하나의 시점입니다 여러분이 볼륨에서 돌아다니면 윈도우 제어기와 오너먼트가 여러분과 가장 가까운 시점으로 자동으로 이동합니다
현재 시점에 따라 시스템이 오너먼트의 위치를 자동으로 업데이트합니다 이제 제가 작은 로봇 친구를 추가했습니다 로봇은 전면을 바라볼 뿐 제가 어디에 있는지 모릅니다 제가 돌아다닐 때 로봇도 저를 향할 수 있다면 앱에 생명감이 더해질 겁니다
먼저, 시점을 업데이트하기 위해 새로운 한정자 onVolumeViewpointChange를 추가합니다 활성 시점이 업데이트될 때마다 이 한정자가 호출됩니다 이를 사용해 앱 상태에서 활성 시점 추적 변수를 설정합니다 로봇이 업데이트되면 이 값을 사용하여 이동하고 현재 시점을 향합니다
시점의 squareAzimuth 값을 사용합니다 이 유형은 볼륨 주위의 위치를 4개 값 중 하나로 정규화합니다 4개 값은 볼륨의 4개 면을 나타냅니다
SquareAzimuth의 4개 면은 전면, 왼쪽, 오른쪽, 후면에 대한 의미 값을 제공합니다 이러한 의미 값에는 특정 Rotation3D도 포함되며 이는 뷰와 엔티티에 회전으로 직접 적용될 수 있습니다
로봇의 움직임을 처리하는 코드에서 로봇이 유휴 모드인 경우 로봇을 해당 위치를 향하게 하는 약간의 코드를 추가했습니다 그런 다음 손 흔들기 애니메이션을 약간 발생시킵니다
이제 로봇이 저를 향해 돌아서고
살짝 손을 흔들어 줍니다! 앱이 살아 있는 듯하고 더욱 생동감 있게 느껴집니다
하지만 모든 앱이 모든 시점을 지원하지는 않습니다 저는 시점을 정면과 양 측면으로만 제한하고 싶습니다
지원되는 시점을 지정하기 위해 또 다른 새로운 뷰 한정자인 supportedVolumeViewpoints를 사용합니다 기본적으로 모든 시점이 지원됩니다
제 경우에는 볼륨의 전면과 양 측면만 지원하고 후면은 지원하고 싶지 않습니다
이렇게 하려면 전면, 왼쪽, 오른쪽 값이 포함된 옵션 세트를 전달합니다
이제 제가 볼륨의 후면으로 이동해도 오너먼트와 윈도우 제어기가 볼륨 후면으로 이동하지 않습니다 이제, 로봇도 멈춥니다 지금까지 로봇이 저의 움직임에 반응해 왔기 때문에 로봇이 제가 잘못된 쪽에 있다고 알려 주는 것 같은 느낌이 드네요
지원되는 시점 세트에 해당 새로운 값이 없는 경우에도 볼륨 시점 변경 블록이 호출되도록 하기 위해 시점 업데이트 전략에 대한 새로운 인수를 추가합니다 모두를 지정하면 모든 시점에 대해 업데이트됩니다 지원되지 않는 시점도 포함해서요 지원되는 세트에 해당 새로운 값이 있는지 확인하고 없는 경우, 로봇에서 새 애니메이션을 트리거하여 지원되는 시점 중 하나로 되돌아가야 한다는 것을 나타냅니다
이제 제가 볼륨의 후면으로 걸어가면 지원된 마지막 면에서 오너먼트가 모두 멈추고
로봇은 저에게 화가 나서 되돌아가라는 표시를 합니다
훨씬 더 나아졌네요!
몇 가지 새로운 옵션을 사용해 해당 환경 내에 볼륨이 표시되는 방식을 제어할 수도 있습니다 그중 첫 번째는 환경 조정입니다 볼륨이 중력 정렬 상태로 유지되어 밑면이 바닥과 평행하게 유지되는지 또는 볼륨이 들어올려지면 아래로 기우는지를 제어합니다 대부분의 경우 적응형 기울이기 동작이 가장 편안합니다 이것이 visionOS 2에서의 기본값입니다 볼륨이 지면과 평행한 상태로 시작되지만 수평선 위로 들어올려지면 기울어지기 시작합니다
이렇게 하면 사용 가능한 볼륨의 콘텐츠를 뒤로 기울어진 위치에서도 사용할 수 있으며 대화형 콘텐츠에 훨씬 더 편안합니다
하지만 일부 볼류메트릭 앱은 상호작용이 많이 필요하지 않거나 더 잔잔한 콘텐츠를 제공하도록 디자인됩니다
이 경우, 중력 정렬 동작이 더 낫습니다
volumeWorldAlignment 한정자로 적응형 조정을 오버라이드하여 볼륨이 바닥과 정렬된 상태로 유지됩니다
또한 이제 볼륨에 동적 크기 조절 기능을 적용할 수 있습니다
visionOS의 윈도우는 환경 내에서 이동함에 따라 크기가 변경됩니다 윈도우는 더 멀리 이동할수록 크기가 커져서 사용자의 시야에서 크기를 유지합니다 이 점은 유용합니다 더 먼 거리에서는 사용하기가 어려워지는 텍스트와 제어기가 윈도우에 종종 포함되니까요
이 동작은 볼륨의 도구 막대 및 오너먼트와 동일합니다
반면에 볼륨 자체는 기본적으로 고정 크기로 설정되므로 해당 환경에서 존재감을 높일 수 있습니다
더 멀리 이동해도 고정된 크기로 유지되므로 먼 거리에서는 해당 콘텐츠가 더 작게 표시됩니다
많은 볼류메트릭 앱의 경우 이렇게 하면 멋져 보입니다 해당 공간에서 가상 콘텐츠를 시각화할 수 있기 때문입니다 마치 실제로 그곳에 있는 것처럼요
하지만 다양한 대화형 영역을 많이 포함하는 밀집된 콘텐츠에 의존하는 볼류메트릭 경험도 동적 크기 조절 기능으로 이점을 얻을 수 있습니다
볼륨에 동적 크기 조절 기능을 적용하려면 defaultWorldScalingBehavior라는 새로운 장면 한정자를 사용합니다 Botanist는 대화형 게임이므로 레벨에 동적 크기 조절 기능을 적용하면 좋을 것입니다 따라서 .dynamic 옵션을 사용해 이 동작을 활성화합니다
시작이 좋네요, 볼륨으로 재밌는 앱을 만들었습니다 확실히 볼륨에 대해 잘 설명해 주셨습니다 그럼, 다음 내용은 무엇인가요? 몰입형 공간을 사용해 로봇을 실제 세계로 가져오려고 합니다 정말 말도 안 되는 이야기네요
몰입형 공간으로 개발자들은 Apple Vision Pro에서 풍부한 경험을 구현하고 있습니다 저는 Owen이 Botanist가 공유 공간에서 볼륨을 탐색할 수 있게 만든 진행 과정을 정말 즐기고 있습니다 다음으로, 저는 윈도우에 그치지 않고 온실을 구현해 공간을 채우는 풍부하고 몰입감 넘치는 경험을 선보이려고 합니다
가장 먼저 할 일은 몰입형 공간 자체를 만드는 것입니다 Xcode의 New Project 대화상자에 이를 구성하는 옵션이 있지만 이 경우에는 제가 직접 추가합니다
몰입형 공간이 하나 있지만 지금은 비어 있습니다
몰입형 공간이 열리면 모든 RealityKit 콘텐츠를 볼륨에서 몰입형 공간으로 이식하겠습니다 이 작업은 매끄럽게 진행되어 Botanist가 실제 세계 탐색을 시작할 수 있게 될 때 놀라움과 기쁨을 선사해야 합니다
특별히 이를 위해 이름이 지정된 좌표 공간이 visionOS 1.1에 도입되었는데 이것이 바로 몰입형 공간입니다
좌표 공간은 특정 기준 프레임을 기준으로 위치를 정밀하게 지정하는 도구입니다
새로운 몰입형 좌표 공간은 SwiftUI의 기존 로컬/글로벌 좌표 공간과 함께 사용할 수 있습니다
로컬은 현재 뷰의 좌표 공간을 지칭하며 원점이 뷰의 왼쪽 상단에 있습니다
글로벌은 윈도우의 좌표 공간을 지칭하며 원점이 윈도우의 왼쪽 상단에 있습니다
볼륨의 경우, 원점은 왼쪽 상단 후면에 있습니다
몰입형 공간은 글로벌 위에 배치되며 원점이 사용자 아래 지면상의 지점으로 정의되고 몰입형 공간은 열려 있습니다
RealityView와 함께 좌표 공간을 사용해 몰입형 공간으로의 전환을 빌드하겠습니다 RealityView는 수많은 변환 함수를 제공합니다 이 함수로 RealityKit과 SwiftUI의 좌표 공간 사이를 이동하겠습니다 먼저, 로봇 변형의 변환을 처리하겠습니다 로봇 변형은 볼륨을 위한 RealityKit 장면 공간에서 SwiftUI 몰입형 공간으로 변환됩니다 RealityView의 업데이트 클로저에서 첫 번째 변환 함수를 호출합니다
볼륨을 위한 로컬 RealityKit 장면 공간에서 SwiftUI의 몰입형 공간으로 로봇의 변형을 변환합니다 변환된 변형을 앱 모델에 저장해 나중에 사용할 수 있도록 합니다
그런 다음 볼륨에서 몰입형 공간의 루트 엔티티로 로봇의 부모를 다시 지정합니다 로봇의 부모가 다시 지정되었고 변형이 계산된 볼륨에서 로봇을 가져왔으니 볼륨 뷰로부터의 변환을 완료된 것으로 표시합니다 몰입형 공간 뷰에서 변환을 계속 진행하겠습니다
몰입형 공간 뷰에서 또 다른 변환 함수를 호출합니다
여기서는 SwiftUI 몰입형 공간에서 RealityKit 장면 공간으로 이동하는 변형을 계산합니다 그런 다음 이 두 변형을 구성합니다 이를 수행하려면 방금 계산한 변형과 앞서 앱 모델에 저장한 변형을 곱합니다 그 결과는 볼륨의 로컬 좌표 공간에서 몰입형 공간의 좌표 공간으로 변환됩니다 로봇의 변환을 업데이트하여 로봇을 몰입형 공간에 배치하고 볼륨에서 나타났던 위치와 일치시킵니다
로봇의 변형이 변환되었으니 점프를 시작합니다
이제 전환을 실행할 준비가 되었습니다! 좌표 변환 API의 강력한 성능 덕분에 로봇이 볼륨 외부의 몰입감 넘치는 경험 환경으로 점프할 수 있습니다 착지가 멈췄네요!
다음으로, Botanist 앱의 몰입 스타일을 선택하겠습니다
기본적으로, 몰입감 넘치는 경험은 혼합 스타일로 시작되어 주변 환경의 맥락에서 앱을 표시합니다 점진적 스타일은 패스스루와 완전 몰입형 사이의 다리 역할을 합니다 방사형 포털을 사용해 앱을 표시하는 동시에 그 주위에 패스스루를 구현합니다 전체 스타일에서는 몰입형 앱이 주변 환경을 완전히 대체합니다 저는 점진적 스타일을 선택하겠습니다
Botanist가 환경을 탐색할 수 있도록 하는 데 적합합니다
Botanist 앱에서 점진적 스타일을 도입하면 기본적으로 포털의 초기 크기가 사람의 시야를 절반 정도 가립니다 또한 시스템은 고정된 최소 및 최대 몰입 양을 제공하여 지원되는 몰입 범위를 정의합니다
Botanist 앱이 더 몰입감 넘치게 시작할 수 있도록 해야 합니다 visionOS 2의 새로운 기능인 맞춤형 몰입 양으로 구현합니다 이를 통해 몰입의 정도를 낮추거나 높여서 Botanist가 볼륨 외부의 몰입형 공간으로 뛰어드는 모습을 실제로 선보일 수 있습니다 이 새로운 API를 자세히 살펴보겠습니다
시작하기 위해 새로운 이니셜라이저를 사용해 맞춤형 몰입 범위와 초기 양에 대한 값을 사용하는 점진적 몰입 스타일을 생성하겠습니다 점진적 스타일이 몰입형 공간에 적용되면 시스템이 제공된 값을 사용하여 장면에 적용된 점진적 효과의 최소, 최대 및 초기 값을 정의합니다
Botanist 앱을 위한 몰입감 넘치는 경험이 더욱 몰입감 높게 시작되도록 초기 몰입 양을 80%로 선택합니다 Botanist 앱의 맞춤형 몰입 범위로 최소 20%, 최대 100%를 지정합니다 100%는 전체 몰입에 해당합니다
맞춤형 몰입 양을 실제로 확인해 보겠습니다 더욱 몰입감 넘치게 시작하면 Botanist가 볼륨 외부로 점프하는 모습을 강조할 수 있습니다 지정한 전체 범위에 걸쳐 멋지게 구현되네요 Digital Crown을 사용해 경험을 조정할 수 있습니다
다음으로, Digital Crown을 사용해 지원되는 몰입 양을 살펴보면 Botanist가 반응하도록 만들려고 합니다
onImmersionChange 한정자를 사용하여 몰입 양 변경 사항에 반응합니다 이는 맥락 값에 새로운 몰입 레벨을 제공합니다
몰입이 변경되면 제공된 맥락에서 값을 읽습니다 Botanist의 경우, 이를 저장해 전후의 값을 비교할 수 있습니다
onChange로 저장된 몰입 양의 변경 사항을 처리하여 클로저에서 새 값과 이전 값을 추출해 전달합니다
로봇이 몰입 양 변경 사항에 반응하도록 하기 위해 몰입 양이 증가하면 로봇이 바깥쪽으로 이동하게 만드는 함수를 호출합니다
또한 몰입 양이 감소하면 로봇이 안쪽으로 이동하게 만드는 함수를 호출합니다 확인해 보죠! 이제 몰입 레벨이 변경되면 Botanist가 반응합니다 몰입을 증가시키면 저를 향해 이동하고 감소시키면 저로부터 먼 곳으로 이동합니다
몰입을 다시 높여 보겠습니다 몰입 양이 증가하면 이제 Botanist가 반응하여 바깥쪽으로 이동하고 확장되는 공간을 탐색하지만 현재 환경의 밀도가 약간 낮은 것 같습니다
이를 해결하기 위해 바닥을 탭해 환경의 바닥을 기준으로 식물을 배치할 수 있게 만들어서 Botanist가 탐색할 수 있게 해보죠 바닥의 특정 위치에 식물을 배치하려면 바닥 앵커를 기준으로 식물을 배치해야 하며 이를 위해 앵커의 3D 위치를 사용할 수 있어야 합니다
앵커의 3D 위치 접근 권한을 앱에 제공할 수 있습니다 RealityKit의 새로운 Spatial Tracking Session API를 사용해 사람들이 추적하려는 앵커 기능을 승인하도록 하면 됩니다
이 API를 사용하기 위해 먼저 공간 추적 세션을 생성합니다
그런 다음 몰입형 공간이 열리면 공간 추적 세션을 실행하는 함수를 호출하는 작업을 생성합니다
세션을 실행하기 위해, 먼저 평면 앵커 추적용 구성을 설정합니다
그런 다음 평면 앵커 변형에 대한 승인을 요청하는 구성으로 세션을 실행합니다
이제 평면 추적을 위해 등록했으니 추적할 앵커를 추가해야 합니다
바닥 분류를 사용해 대상 평면의 수평 정렬을 지정하여 추적할 바닥 앵커 엔티티를 생성합니다
그런 다음 바닥 앵커를 몰입형 공간의 RealityView 콘텐츠에 추가합니다 마지막으로, 새 앵커의 3D 위치를 사용해 식물을 배치해야 합니다
몰입형 공간의 대상 엔티티에 대한 탭을 감지하는 데 사용될 수 있는 공간 탭 제스처를 추가합니다
제스처가 종료되면 탭을 처리하는 함수에 해당 제스처 값을 전달합니다
탭을 처리하려면 먼저 제스처 값에 변환 함수를 사용하여 바닥 앵커를 기준으로 제스처의 위치를 가져옵니다 이 단계에서는 바닥 앵커의 변형을 앱에서 사용할 수 있어야 합니다
마지막으로, 식물을 배치하려면 식물을 바닥 앵커의 자녀로 추가하고 변환된 위치를 사용하여 식물 엔티티의 위치를 설정합니다
이제 목록에서 식물을 선택합니다 바닥의 호버 효과는 식물이 배치될 위치를 나타냅니다 탭하기만 하면 식물이 배치됩니다! 공간에 식물을 배치하니 Botanist 앱이 더 생생해졌네요
Spatial Tracking Session API에 대한 자세한 내용은 ’RealityKit으로 공간 드로잉 앱 빌드하기’ 세션을 참고하세요
몰입감 넘치는 온실 경험이 실제로 결합되기 시작합니다
Botanist가 식물을 방문하면 식물이 성장 축하 애니메이션을 어떻게 재생하는지 확인해 보세요
저도 축하에 동참하고 싶네요 현재, 이 환경의 각 식물은 연관된 색조 색상의 화분에 배치됩니다 화분의 색조 색상과 일치하도록 패스스루의 색조를 지정하는 것은 축하에 동참하는 좋은 방법입니다
선호 Surroundings Effect API를 사용해 주변 패스스루의 색조를 지정할 수 있습니다 Botanist 앱에서 몰입감 넘치는 경험을 업데이트해 활성 상태로 축하 중인 식물의 화분 색조 색상을 사용해 패스스루의 색조를 지정하겠습니다
먼저, 화분에 어울릴 색조 색상을 선택합니다
맞춤형 PlantComponent에 색조 색상 속성을 추가합니다 그런 다음 식물 유형을 전환하여 색조 색상을 선택합니다 예를 들어, 커피 베리에는 연청색을 사용합니다
색조 효과를 트리거하려면 Botanist가 화분 근처에 있는 경우를 감지해야 합니다 RealityKit을 사용한 충돌 감지가 이 작업에 적합한 도구입니다
시간 경과에 따른 로봇의 움직임을 처리할 때 충돌 클로저를 사용하여 히트 엔티티를 처리합니다 그런 다음 충돌 값을 헬퍼 함수에 전달합니다
RealityKit을 사용한 충돌 감지에 대해 자세히 알아보려면 ’첫 몰입형 앱 개발하기’를 확인하세요
헬퍼 함수에서는 먼저 Botanist가 식물과 충돌했는지를 확인하고 아니라면 돌아갑니다
그런 다음 축하 애니메이션을 재생합니다
마지막으로, 몰입형 공간에 있다면 나중에 사용할 수 있도록 활성 색조 색상을 앱 모델에 저장합니다
몰입형 뷰로 돌아와서 저장된 활성 색조 색상에 따라 colorMultiply SurroundingsEffect를 생성합니다 또 해당 주변 환경 효과를 사용해 패스스루에 색조를 지정합니다
업데이트된 축하 색상을 확인해 보겠습니다 이제 Botanist가 식물을 방문하면 패스스루에 색조를 적용합니다
양귀비에는 자홍색
유카에는 초록색
커피 베리에는 연청색 잘했어요, Botanist!
이 세션에서는 앱에서 볼륨과 몰입형 공간을 빌드하는 여러 가지 새로운 방식을 살펴봤습니다 새로운 크기 조절 동작으로 볼륨 크기 조정 방식을 미세 조정합니다 시점을 사용하여 앱이 볼륨을 돌아다니는 사람들에게 응답할 수 있게 합니다 좌표 변환의 강력한 기능을 통해 볼륨에서 나와 몰입형 공간으로 들어갑니다 몰입형 공간에서 몰입 레벨 변경 사항에 응답합니다 공간 앱은 사람들이 꿈꿔 본 적 없는 경험을 위한 완전히 새로운 영역입니다 SwiftUI와 RealityKit의 강력하고 표현력이 풍부한 도구 덕분에 가능성은 무한합니다 약간의 기발한 재치와 상상력을 발휘하면 여러분은 놀라운 것을 만들 수 있습니다 감사합니다!
-
-
3:09 - Baseplate
// Baseplate WindowGroup(id: "RobotExploration") { ExplorationView() .volumeBaseplateVisibility(.visible) // Default! } .windowStyle(.volumetric)
-
4:29 - Enabling resizability
// Enabling resizability WindowGroup(id: "RobotExploration") { let initialSize = Size3D(width: 900, height: 500, depth: 900) ExplorationView() .frame(minWidth: initialSize.width, maxWidth: initialSize.width * 2, minHeight: initialSize.height, maxHeight: initialSize.height * 2) .frame(minDepth: initialSize.depth, maxDepth: initialSize.depth * 2) } .windowStyle(.volumetric) .windowResizability(.contentSize) // Default!
-
6:10 - Programmatic resize
// Programmatic resize struct ExplorationView: View { @State private var levelScale: Double = 1.0 var body: some View { RealityView { content in // Level code here } update: { content in appState.explorationLevel?.setScale( [levelScale, levelScale, levelScale], relativeTo: nil) } .frame(width: levelSize.value.width * levelScale, height: levelSize.value.height * levelScale) .frame(depth: levelSize.value.depth * levelScale) .overlay { Button("Change Size") { levelScale = levelScale == 1.0 ? 2.0 : 1.0 } } } }
-
7:39 - Toolbar ornament
// Toolbar ornament ExplorationView() .toolbar { ToolbarItem { Button("Next Size") { levelScale = levelScale == 1.0 ? 2.0 : 1.0 } } ToolbarItemGroup { Button("Replay") { resetExploration() } Button("Exit Game") { exitExploration() openWindow(id: "RobotCreation") } } }
-
10:41 - Ornaments
// Ornaments WindowGroup(id: "RobotExploration") { ExplorationView() .ornament(attachmentAnchor: .scene(.topBack)) { ProgressView() } } .windowStyle(.volumetric)
-
12:08 - Volume viewpoint
// Volume viewpoint struct ExplorationView: View { var body: some View { RealityView { content in // Some RealityKit code } .onVolumeViewpointChange { oldValue, newValue in appState.robot?.currentViewpoint = newValue.squareAzimuth } } }
-
13:06 - Using volume viewpoint
// Volume viewpoint class RobotCharacter { func handleMovement(deltaTime: Float) { if self.robotState == .idle { characterModel.performRotation(toFace: self.currentViewpoint, duration: 0.5) self.animationState.transition(to: .wave) } else { // Handle normal movement } } }
-
13:43 - Supported viewpoints
// Supported viewpoints struct ExplorationView: View { let supportedViewpoints: Viewpoint3D.SquareAzimuth.Set = [.front, .left, .right] var body: some View { RealityView { content in // Some RealityKit code } .supportedVolumeViewpoints(supportedViewpoints) .onVolumeViewpointChange { _, newValue in appState.robot?.currentViewpoint = newValue.squareAzimuth } } }
-
14:30 - Viewpoint update strategy
// Viewpoint update strategy struct ExplorationView: View { let supportedViewpoints: Viewpoint3D.SquareAzimuth.Set = [.front, .left, .right] var body: some View { RealityView { content in // Some RealityKit code } .supportedVolumeViewpoints(supportedViewpoints) .onVolumeViewpointChange(updateStrategy: .all) { _, newValue in appState.robot?.currentViewpoint = newValue.squareAzimuth if !supportedViewpoints.contains(newValue) { appState.robot?.animationState.transition(to: .annoyed) } } } }
-
16:42 - World alignment
// World alignment WindowGroup { ExplorationView() .volumeWorldAlignment(.gravityAligned) } .windowStyle(.volumetric)
-
18:05 - Dynamic scale
// Dynamic scale WindowGroup { ContentView() } .windowStyle(.volumetric) .defaultWorldScalingBehavior(.dynamic)
-
19:16 - Starting with an empty immersive space
struct BotanistApp: App { var body: some Scene { // Volume WindowGroup(id: "Exploration") { VolumeExplorationView() } .windowStyle(.volumetric) // Immersive Space ImmersiveSpace(id: "Immersive") { EmptyView() } } }
-
20:52 - Callout to convert function from volume view
// Coordinate conversions // Convert from RealityKit entity in volume to SwiftUI space struct VolumeExplorationView: View { @Environment(ImmersiveSpaceAppModel.self) var appModel var body: some View { RealityView { content in content.add(appModel.volumeRoot) // ... } update: { content in guard appModel.convertingRobotFromVolume else { return } // Convert the robot transform from RealityKit scene space for // the volume to SwiftUI immersive space convertRobotFromRealityKitToImmersiveSpace(content: content) } } }
-
21:08 - Convert robot's transform to SwiftUI immersive space
// Coordinate conversions // Convert from RealityKit entity in volume to SwiftUI space func convertRobotFromRealityKitToImmersiveSpace(content: RealityViewContent) { // Convert the robot transform from RealityKit scene space for // the volume to SwiftUI immersive space appModel.immersiveSpaceFromRobot = content.transform(from: appModel.robot, to: .immersiveSpace) // Reparent robot from volume to immersive space appModel.robot.setParent(appModel.immersiveSpaceRoot) // Handoff to immersive space view to continue conversions. appModel.convertingRobotFromVolume = false appModel.convertingRobotToImmersiveSpace = true }
-
21:42 - Callout to convert function from immersive space view
// Coordinate conversions // Convert from SwiftUI immersive space back to RealityKit local space struct ImmersiveExplorationView: View { @Environment(ImmersiveSpaceAppModel.self) var appModel var body: some View { RealityView { content in content.add(appModel.immersiveSpaceRoot) } update: { content in guard appModel.convertingRobotToImmersiveSpace else { return } // Convert the robot transform from SwiftUI space for the immersive // space to RealityKit scene space convertRobotFromSwiftUIToRealityKitSpace(content: content) } } }
-
21:48 - Compute transform to place robot in matching position in immersive space
// Coordinate conversions // Calculate transform from SwiftUI to RealityKit scene space func convertRobotFromSwiftUIToRealityKitSpace(content: RealityViewContent) { // Calculate transform from SwiftUI immersive space to RealityKit // scene space let realityKitSceneFromImmersiveSpace = content.transform(from: .immersiveSpace, to: .scene) // Multiply with the robot's transform in SwiftUI immersive space to build a // transformation which converts from the robot's local // coordinate space in the volume and ends with the robot's local // coordinate space in an immersive space. let realityKitSceneFromRobot = realityKitSceneFromImmersiveSpace * appModel.immersiveSpaceFromRobot // Place the robot in the immersive space to match where it // appeared in the volume appModel.robot.transform = Transform(realityKitSceneFromRobot) // Start the jump! appModel.startJump() }
-
23:54 - Customizing immersion
// Customizing immersion struct BotanistApp: App { // Custom immersion amounts @State private var immersionStyle: ImmersionStyle = .progressive(0.2...1.0, initialAmount: 0.8) var body: some Scene { // Immersive Space ImmersiveSpace(id: "ImmersiveSpace") { ImmersiveSpaceExplorationView() } .immersionStyle(selection: $immersionStyle, in: .mixed, .progressive, .full) } }
-
25:17 - Callout to function to handle immersion amount changed
// Reacting to immersion struct ImmersiveView: View { @State var immersionAmount: Double? var body: some View { ImmersiveSpaceExplorationView() .onImmersionChange { context in immersionAmount = context.amount } .onChange(of: immersionAmount) { oldValue, newValue in handleImmersionAmountChanged(newValue: newValue, oldValue: oldValue) } } }
-
25:39 - Handle function to make robot react to changed immersion amount
// Reacting to immersion func handleImmersionAmountChanged(newValue: Double?, oldValue: Double?) { guard let newValue, let oldValue else { return } if newValue > oldValue { // Move the robot outward to react to increasing immersion moveRobotOutward() } else if newValue < oldValue { // Move the robot inward to react to decreasing immersion moveRobotInward() } }
-
26:57 - Create spatial tracking session
// Create and run spatial tracking session struct ImmersiveExplorationView { @State var spatialTrackingSession: SpatialTrackingSession = SpatialTrackingSession() var body: some View { RealityView { content in // ... } .task { await runSpatialTrackingSession() } } }
-
27:11 - Run spatial tracking session
// Create and run the spatial tracking session func runSpatialTrackingSession() async { // Configure the session for plane anchor tracking let configuration = SpatialTrackingSession.Configuration(tracking: [.plane]) // Run the session to request plane anchor transforms let _ = await spatialTrackingSession.run(configuration) }
-
27:32 - Create a floor anchor to track
// Create a floor anchor to track struct ImmersiveExplorationView { @State var spatialTrackingSession: SpatialTrackingSession = SpatialTrackingSession() let floorAnchor = AnchorEntity( .plane(.horizontal, classification: .floor, minimumBounds: .init(x: 0.01, y: 0.01)) ) var body: some View { RealityView { content in content.add(floorAnchor) } .task { await runSpatialTrackingSession() } } }
-
27:54 - Detect taps on entities in immersive space
// Detect taps on entities in immersive space RealityView { content in // ... } .gesture( SpatialTapGesture( coordinateSpace: .immersiveSpace ) .targetedToAnyEntity() .onEnded { value in handleTapOnFloor(value: value) } )
-
28:09 - Handle tap event to place plant
// Handle tap event func handleTapOnFloor(value: EntityTargetValue<SpatialTapGesture.Value>) { let location = value.convert(value.location3D, from: .immersiveSpace, to: floorAnchor) plantEntity.position = location floorAnchor.addChild(plantEntity) }
-
29:47 - Add tint color to custom plant component
// Add tint color to custom plant component struct PlantComponent: Component { var tintColor: Color { switch plantType { case .coffeeBerry: // Light blue return Color(red: 0.3, green: 0.3, blue: 1.0) case .poppy: // Magenta return Color(red: 1.0, green: 0.0, blue: 1.0) case .yucca: // Light green return Color(red: 0.2, green: 1.0, blue: 0.2) } } }
-
30:09 - Handle collisions with robot
// Handle collisions with robot // // Handle movement of the robot between frames func handleMovement(deltaTime: Float) { // Move character in the collision world appModel.robot.moveCharacter(by: SIMD3<Float>(...), deltaTime: deltaTime, relativeTo: nil) { collision in handleCollision(collision) } }
-
30:29 - Set active tint color when colliding with plant
// Set active tint color when colliding with plant // // Handle collision between robot and hit entity func handleCollision(_ collision: CharacterControllerComponent.Collision) { guard let plantComponent = collision.hitEntity.components[PlantComponent.self] else { return } // Play the plant growth celebration animation playPlantGrowthAnimation(plantComponent: plantComponent) if inImmersiveSpace { appModel.tintColor = plantComponent.tintColor } }
-
30:48 - Apply effect to tint passthrough
// Apply effect to tint passthrough struct ImmersiveExplorationView: View { var body: some View { RealityView { content in // ... } .preferredSurroundingsEffect(surroundingsEffect) } // The resolved surroundings effect based on tint color var surroundingsEffect: SurroundingsEffect? { if let color = appModel.tintColor { return SurroundingsEffect.colorMultiply(color) } else { return nil } } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.