스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
RealityKit으로 공간 드로잉 앱 빌드하기
공간 드로잉 앱을 제작하는 과정에 강력한 RealityKit을 활용해 보세요. RealityKit과 ARKit 및 SwiftUI을 통합한 매혹적인 공간 경험을 제작하며 RealityKit 내부에서 리소스가 사용되는 방식을 소개하고, 하위 수준 메시 및 텍스처 API 등의 기능을 사용하여 사용자의 붓질을 빠르게 업데이트하는 방법을 설명합니다.
챕터
- 0:00 - Introduction
- 2:43 - Set up spatial tracking
- 5:42 - Build a spatial user interface
- 13:57 - Generate brush geometry
- 26:11 - Create a splash screen
리소스
관련 비디오
WWDC24
- iOS, macOS, visionOS용 RealityKit API 알아보기
- Reality Composer Pro에서 대화식 3D 콘텐츠 만들기
- RealityKit 오디오로 공간 컴퓨팅 앱 향상하기
- visionOS에서 맞춤형 호버 효과 제작하기
WWDC23
-
다운로드
안녕하세요, 저는 RealityKit 팀의 엔지니어인 Adrian입니다 오늘 세션에서는 RealityKit의 새 기능을 사용하여 visionOS를 위한 공간 드로잉 앱을 빌드하는 방법을 알려 드리겠습니다
RealityKit은 고성능 3D 시뮬레이션과 렌더링을 iOS, macOS 및 visionOS에 제공하는 프레임워크죠 visionOS에서 RealityKit은 앱의 공간 기능을 빌드하기 위한 토대입니다
Apple Vision Pro가 발표된 이후로 Apple은 여러분과 같은 멋진 개발자로부터 좋은 의견을 많이 받았으며 플랫폼의 기능을 개선하는 동안 해당 의견을 수용하기 위해 노력했습니다
오늘은 RealityKit에서 최고의 경험을 개발하는 데 유용한 API를 몇 가지 소개해 드릴게요 시작하겠습니다
공간 드로잉 앱은 SwiftUI와 ARKit을 통해 RealityKit의 강력한 3D 기능을 통합하여 근사한 사용자 경험을 지원합니다 흥미로운 맞춤형 메시와 텍스처, 셰이더를 빌드하여 세련된 시각적 디자인을 만드는 방법을 보여드리겠습니다
앱을 열면 눈길을 사로잡는 시작 화면이 표시됩니다
간편한 설정 절차를 마치면 작품을 만들 수 있습니다
공중에서 엄지와 검지를 맞대기만 하면 드로잉을 시작할 수 있죠
팔레트 보기를 통해 붓질의 스타일을 바꿀 수 있습니다
앱에서는 튜브와 같은 모양을 그리는 단색 붓과
반짝이는 붓을 지원합니다
붓질을 맞춤화할 수 있습니다 예를 들면 붓질의 색상이나 두께를 바꿀 수 있죠
오늘은 많은 내용을 다룰 것입니다 이 앱을 함께 빌드할 수 있어 기쁘게 생각합니다 이제 시작하겠습니다 우선 앱이 내 손 및 환경에 대한 데이터를 이해할 수 있도록 공간 추적을 설정합니다
그다음 붓과 캔버스를 완전히 제어할 수 있도록 UI를 빌드합니다 또한 새 기능을 사용하여 호버 시 표시되는 UI를 맞춤화할 것입니다
그다음 RealityKit에서 메시가 작동하는 방법을 자세히 알아보고 앱에서 새 RealityKit API를 사용하여 Metal로 붓 지오메트리를 효율적으로 생성하는 방법을 다룰게요
마지막으로는 동적 텍스처와 공간 UI 요소를 활용하여 앱에서 흥미로운 시작 화면을 만들어 보겠습니다
사용자는 내용을 작성하기 위해 손가락을 맞대고 손을 움직이므로 드로잉 앱은 사용자의 손 자세를 이해해야 합니다 이를 위해 손 앵커에 대한 공간 추적을 설정해야 합니다
visionOS의 앱에서는 윈도우, 볼륨 및 공간에 SwiftUI 또는 RealityKit 콘텐츠를 배치할 수 있습니다
앱에서 몰입형 공간을 사용하면 앵커를 통해 공간 추적 정보를 받을 수 있습니다
여기에는 World 및 Plane 앵커를 통한 장면 이해 정보와 Hand 앵커를 통한 자세 정보가 포함됩니다
visionOS 1.0에서는 ARKit을 통해 이 데이터에 접근할 수 있습니다
visionOS 2.0에서는 더 쉬운 방법으로 RealityKit 앱에서 공간 추적을 사용할 수 있게 됩니다 이 API를 사용하여 드로잉 앱에서 공간 손 추적을 설정해 볼게요
RealityKit에서는 AnchorEntity로 RealityKit 엔티티를 AR 앵커에 붙일 수 있습니다
이 드로잉 앱에서는 양손을 위해 두 개의 AnchorEntity를 만듭니다 엔티티 중 하나는 엄지손가락 끝에 다른 하나는 검지손가락 끝에 고정됩니다
공간 추적 데이터에 접근하려면 사용자에게 승인을 요청하세요 드로잉 앱에서 사용자가 Start를 탭하면 관련 승인을 요청합니다
사용자에게 승인을 요청할 시점은 신중하게 고려해야 합니다
앱에서 필요할 때만 승인을 요청하세요 이 경우에는 사용자가 물체를 그리기 시작할 때죠
RealityKit에서 데이터 추적을 위한 승인을 요청하려면 SpatialTrackingSession을 사용하세요 visionOS 2.0에 새로 도입된 API입니다
앱에서 필요한 추적 기능을 선언하세요 이 경우에는 손 데이터가 필요합니다
다음으로 SpatialTrackingSession에 대해 run을 호출하세요 이 시점에서 추적을 승인하기 위한 알림이 표시됩니다
run 함수는 승인되지 않은 추적 기능의 목록을 반환합니다 이 목록을 확인하여 승인이 부여되었는지 파악할 수 있죠
공간 추적이 승인되면 AnchorEntity의 transform을 통해 추적 데이터에 접근할 수 있습니다
승인이 거부되면 AnchorEntity의 transform이 업데이트되지 않습니다 그래도 AnchorEntity는 여전히 포즈를 시각적으로 업데이트하죠
요약하자면 앱에서 몰입형 공간을 사용하면 World에 RealityKit 콘텐츠를 고정할 수 있습니다
AnchorEntity를 통해 RealityKit 콘텐츠의 앵커를 설정할 수 있습니다
올해 새로 도입된 기능으로 이제 SpatialTrackingSession을 통해 앱에서 AnchorEntity의 transform에 접근할 수 있습니다
또한 SpatialTrackingSession을 사용하면 AnchorEntity가 RealityKit의 물리 시스템과 상호작용할 수 있죠
다음으로 앱의 사용자 인터페이스에 대해 설명할게요
시작 화면에서 Start를 탭하면 드로잉 캔버스를 포함하는 몰입형 공간이 열립니다
캔버스의 크기나 위치는 구형의 핸들을 드래그하여 바꿀 수 있죠
물체를 그릴 준비가 되었다면 팔레트 보기가 표시됩니다
이 보기를 통해 붓의 모양과 색상을 구성할 수 있습니다
물체를 그릴 준비가 되면 캔버스 안으로 들어가 그리기 시작하면 됩니다
이 인터페이스를 빌드하는 방법을 자세히 알아보죠
캔버스 배치 인터페이스부터 설명하겠습니다 이 인터페이스를 통해 드로잉 영역을 정의할 수 있죠
캔버스 배치 중에 몰입형 공간을 구성하는 2가지 요소가 있습니다 바닥에 있는 3D 도형은 캔버스의 가장자리를 나타내고 캔버스 위치를 바꾸는 데 사용되는 핸들이 있죠
먼저 경계 메시를 살펴보겠습니다 이 메시는 실시간으로 생성됩니다 슬라이더를 드래그하여 경계의 크기를 수정할 수 있기 때문이죠
메시는 두 개의 원으로 정의되며 왼쪽의 다이어그램에서 볼 수 있듯이 바깥쪽에 녹색의 원이 있고 안쪽에 빨간 원이 있죠
이 도형을 SwiftUI 경로로 정의할 수 있습니다
원은 중심각이 360도인 호입니다 그러므로 반지름이 서로 다른 2개의 호를 만듭니다
그다음 normalized를 사용하여 even-odd 채우기 모드를 지정하면 만들고자 하는 도형이 정의됩니다
RealityKit에서 메시를 생성할 때 올해 출시된 새 API를 활용할 수 있는데 바로 MeshResource extruding입니다 MeshResource extruding은 강력한 API로 이를 통해 2D 벡터 콘텐츠를 3D 모델로 변환할 수 있죠
원하는 도형의 심도와 해상도를 지정하면 됩니다
한 가지 중요한 사항을 고려해야 합니다 visionOS에서 RealityKit은 포비티드 렌더러를 사용합니다 주변 시야에 있는 이미지 영역은 더 낮은 해상도로 렌더링되죠 이는 앱의 성능을 최적화할 때 도움이 됩니다 장면에 고대비의 얇은 지오메트리가 포함되어 있다면 반짝이는 아티팩트가 나타날 수 있습니다 이 예에서는 링이 너무 얇죠 이처럼 얇은 지오메트리 요소의 사용은 피해야 합니다
특히 고대비의 영역에서요
이 문제를 해결하려면 지오메트리의 두께를 늘리고 얇은 고대비 가장자리를 제거하면 됩니다 왼쪽에서 반짝이는 아티팩트가 완화된 것을 볼 수 있죠
공간 콘텐츠 앨리어싱에 대해 자세히 알아보려면 WWDC23의 ‘공간 컴퓨팅을 위한 렌더링 살펴보기’ 세션을 시청하세요
다음에는 캔버스 핸들에 대해 이야기해 보겠습니다 한 가지 특이한 점이 있죠 사용자가 핸들을 응시할 때 파란색 강조 효과가 나타나죠
visionOS에서 HoverEffectComponent는 사용자가 RealityKit 콘텐츠를 응시할 때 시각적 효과를 더하죠
visionOS 1.0에서 HoverEffectComponent는 기본 스포트라이트 효과를 사용합니다
올해는 HoverEffectComponent에 2개의 호버 효과가 추가됩니다 하이라이트 효과는 엔티티에 균등한 강조 색상을 적용합니다
또한 HoverEffectComponent를 ShaderGraph 셰이더와 사용할 수 있죠 셰이더가 적용된 호버 효과는 매우 유연하므로 호버 시 엔티티가 어떻게 보이는지 세부적으로 제어할 수 있습니다
핸들의 파란색 강조 표시는 하이라이트 호버 효과로 구현되었죠 하이라이트 효과를 사용하려면 .highlight로 HoverEffectComponent를 초기화하고 강조를 위한 색상을 제공하세요
하이라이트 생동감을 높이기 위해 strength 값을 변경할 수도 있죠
캔버스 배치를 위한 UI 요소는 그 뒤에 있는 환경 위에서 반짝이는데 이는 해당 요소가 애디티브 블렌드 모드로 설정되었기 때문입니다 올해 RealityKit에는 내장 머티리얼의 애디티브 블렌드 모드에 대한 지원이 추가되었습니다 UnlitMaterial, PhysicallyBasedMaterial 같은 머티리얼을 말하죠 이를 사용하려면 Program을 만들고 추가할 블렌드 모드를 설정하세요
사용자가 드로잉 캔버스를 선택하면 본격적인 앱 사용이 시작되죠 팔레트 보기가 표시되며 사용자가 붓을 설정할 수 있게 됩니다
팔레트 보기는 SwiftUI에 내장되어 있으며 이를 통해 붓의 유형과 스타일을 맞춤화할 수 있습니다 팔레트 하단에는 선택할 수 있는 프리셋 붓이 있습니다
붓 프리셋 보기를 자세히 살펴보세요 각 붓의 프리셋 썸네일은 완전한 3D 도형이죠
이 메시는 실제 붓질과 같은 방식으로 생성됩니다 SwiftUI 및 RealityKit은 원활하게 통합됩니다 여기에서 각 썸네일에 RealityView를 사용하면 RealityKit의 모든 기능을 활용할 수 있습니다
붓 프리셋을 응시하면 눈길을 사로잡는 호버 효과가 활성화되어 붓을 따라 보라색의 발광이 이동하죠
이는 앞서 언급한 셰이더를 기반으로 하는 호버 효과입니다
이 효과가 어떻게 빌드되었는지 자세히 살펴보죠
셰이더 기반 호버 효과는 셰이더 그래프의 HoverState 노드에 의해 활성화됩니다 이 노드는 셰이더에 호버 효과를 통합할 때 유용한 도구를 제공합니다
예를 들어 Intensity는 시스템에서 제공하는 값으로 응시 상태에 따라 0과 1 사이의 애니메이션 효과를 적용합니다 Intensity 값으로 하이라이트 효과를 재현할 수 있습니다 아까 캔버스 핸들에서 본 효과와 같죠
하지만 프리셋 보기의 경우 더 개선된 효과를 구현하고자 합니다
발광 효과는 붓질의 시작부터 끝까지 붓 메시에 따라 이동해야 합니다
이 복잡한 효과를 구현하기 위해 앱은 셰이더 그래프 머티리얼을 사용하죠 셰이더 그래프를 함께 살펴보겠습니다
HoverState 노드의 Time Since Hover Start를 사용할게요 이는 호버 이벤트가 시작한 후 지난 시간을 초로 보여주는 값이죠
이를 사용하여 발광 하이라이트의 곡선을 따라 위치를 정의합니다 호버 이벤트가 시작되면 발광 위치가 곡선을 따라 이동합니다
붓질을 위한 메시를 생성할 때 앱은 CurveDistance라는 속성을 제공합니다 앱은 UV1 채널을 통해 각 꼭짓점에 대한 곡선 거리 값을 제공합니다
붓질의 곡선 거리에 따른 시각화입니다 붓질의 길이에 따라 이 값은 증가합니다
셰이더는 발광 하이라이트의 위치를 곡선 거리와 비교합니다
이렇게 하면 셰이더가 현재 지오메트리를 기준으로 발광의 위치를 이해할 수 있습니다
다음에는 발광 효과의 크기를 정의해야 합니다
현재 지오메트리는 발광 위치의 범위 안에 있을 때 발광을 보입니다
이제 효과 완화 곡선을 추가할 수 있습니다 이는 발광이 지오메트리를 따라 이동하면서 나타낼 호버 효과의 강도를 정의하죠
마지막 단계는 방금 계산한 강도 값에 따라 호버 효과 색상과 원래 붓질 색상을 혼합하는 것입니다
근사하죠
셰이더 기반 호버 효과를 사용하려면 우선 셰이더 설정을 포함하는 HoverEffectComponent를 만드세요
그런 다음 ShaderGraphMaterial을 사용합니다 HoverState 노드의 업데이트를 받게 되죠
이제 사용자가 붓을 구성할 수단을 구축했으니 앱의 핵심 작업에 대해 이야기할 차례입니다 각 붓질에 대한 지오메트리의 생성이죠
일반적으로 메시는 꼭짓점과 삼각형 등의 프리미티브로 구성된 모음입니다
각 꼭짓점은 해당 꼭짓점의 위치 또는 텍스처 좌표와 같은 여러 속성과 연관되어 있습니다
그리고 이러한 속성은 데이터로 설명되죠 예를 들어, 각 꼭짓점 위치는 3차원 벡터입니다
꼭짓점 데이터를 버퍼로 구성해야 GPU에 전송할 수 있습니다
대부분의 RealityKit 메시의 경우 데이터가 메모리에서 연속적으로 구성되죠 따라서 메모리에서 꼭짓점 위치 0 뒤에 꼭짓점 위치 1이 그 뒤에 꼭짓점 2가 오는 식입니다 다른 모든 꼭짓점 속성도 마찬가지입니다 인덱스 버퍼는 별도로 배치되며 메시 내 각 삼각형의 꼭짓점 인덱스를 포함합니다
RealityKit의 표준 메시 레이아웃은 다양하게 활용할 수 있으므로 여러 용도에 적합합니다 하지만 경우에 따라 도메인별 접근 방식이 더 효율적일 수 있습니다
드로잉 앱은 맞춤형으로 구축된 지오메트리 처리 파이프라인을 이용하여 사용자의 붓질에 대한 메시를 생성합니다
예를 들어, 각 붓질은 메시 곡률을 개선하기 위해 부드럽게 처리되죠
이 알고리즘은 붓질 끝에 점을 추가하는 속도가 최대한 빨라지도록 최적화되어 있습니다 지연 시간을 최소화하는 것이 매우 중요합니다
붓질 메시의 경우 꼭짓점을 배치하는 데 하나의 버퍼를 사용합니다
하지만 표준 메시 레이아웃과 달리 각 꼭짓점은 전체적으로 서로 뒤에 설명됩니다
따라서 속성이 인터리브 처리되죠 첫 꼭짓점의 Position 다음에는 Normal이 오고 그 뒤에 Bitangent가 오는 식으로 모든 속성이 설명될 때까지 이어집니다 이것이 완료되어야 버퍼가 두 번째 꼭짓점을 설명하기 시작합니다
반면에 표준 꼭짓점 버퍼는 각 속성에 대한 모든 데이터를 연속적으로 배치합니다 붓 꼭짓점 버퍼 레이아웃은 특히 드로잉 앱에 유용합니다
붓질을 생성할 때 앱은 꼭짓점 버퍼의 끝에 지속적으로 꼭짓점을 추가합니다 붓의 꼭짓점 버퍼는 이전 데이터의 위치를 수정하지 않고 새 꼭짓점을 추가할 수 있습니다 그러나 표준 꼭짓점 버퍼로 이 작업을 수행하면 버퍼가 커짐에 따라 대부분의 데이터를 이동해야 합니다 붓의 꼭짓점에는 표준 레이아웃에서 볼 수 있는 것과는 다른 속성이 있습니다
Position, Normal, Bitangent 같은 일부 속성은 표준입니다
반면 Color, Material Properties Curve distance와 같은 속성은 맞춤형입니다
앱의 코드에서 붓질 꼭짓점은 Metal 셰이딩 언어에서 이 구조체로 표현됩니다
구조체의 각 항목은 꼭짓점의 속성과 대응됩니다
문제가 하나 있습니다 한편으로는 고성능 지오메트리 엔진의 꼭짓점 레이아웃을 유지하면서 불필요한 변환이나 복사를 피하고 싶었습니다 하지만 다른 한편으로는 지오메트리 엔진의 레이아웃이 RealityKit의 표준 레이아웃과 호환되지 않습니다 지금은 GPU 버퍼를 그대로 RealityKit에 가져와서 RealityKit에 버퍼를 읽는 방법을 안내해야 합니다
LowLevelMesh라는 새 API를 사용하면 가능하죠
LowLevelMesh를 사용하면 꼭짓점 데이터를 다양한 방식으로 정렬할 수 있습니다
꼭짓점 데이터에 사용할 수 있는 Metal 버퍼는 4가지가 있습니다 따라서 RealityKit의 표준 레이아웃과 유사한 레이아웃을 사용할 수 있죠
하지만 때로는 버퍼가 두 개 이상 있는 것이 유용합니다 예를 들어 텍스처 좌표를 다른 속성보다 더 자주 업데이트해야 한다고 가정해 봅시다 이 동적 데이터를 자체 버퍼로 이동하는 것이 더 효율적입니다
꼭짓점 버퍼가 인터리브 처리되도록 재배치할 수 있습니다 또는 인터리브 처리된 버퍼와 처리되지 않은 버퍼를 같이 사용할 수도 있죠
삼각형 스트립과 같은 Metal 프리미티브 유형을 사용할 수도 있죠
LowLevelMesh와 맞춤형 버퍼 레이아웃이 앱에 어떤 이점을 제공할 수 있는지 생각해 보시기 바랍니다
고유한 맞춤형 레이아웃이 있는 바이너리 파일에서 메시 데이터를 가져왔을 수도 있습니다 이제 변환을 위한 오버헤드 없이 해당 데이터를 RealityKit으로 직접 전송할 수 있습니다
또는 디지털 콘텐츠 제작 도구나 CAD 애플리케이션에서 볼 수 있는 것과 같이 사전 정의된 자체 버퍼 레이아웃이 있는 기존 메시 처리 파이프라인을 RealityKit에 브리징할 수도 있습니다
게임 엔진의 메시 데이터를 RealityKit에 효율적으로 브리징하는 방법으로 LowLevelMesh를 사용할 수도 있죠
LowLevelMesh를 사용하면 RealityKit에 메시 데이터를 다양한 방법으로 제공할 수 있어 앱에서 흥미로운 구현을 달성할 수 있죠 코드로 LowLevelMesh를 만드는 방법을 자세히 알아보겠습니다
이제 앱은 추가 변환이나 불필요한 복사 없이 꼭짓점 버퍼를 LowLevelMesh에 그대로 제공할 수 있습니다
LowLevelMesh 속성으로 꼭짓점이 배치되는 방식을 설명할 수 있죠 Swift의 extension에서 SolidBrushVertex 구조를 위한 속성 목록을 설정하겠습니다
우선 position에 대한 속성을 선언하겠습니다
자세히 살펴볼게요 첫 단계는 semantic을 정의하는 것으로 이는 어떻게 속성을 해석할지 LowLevelMesh에 알립니다
이 경우 속성은 position이므로 이 semantic을 사용할게요
그다음 이 속성을 위한 Metal 꼭짓점 형식을 정의합니다 이 경우에는 float3를 사용해야 SolidBrushVertex의 정의와 일치시킬 수 있죠
다음에는 byte 단위의 오프셋을 속성에 제공합니다
마지막으로 레이아웃 인덱스를 제공하겠습니다 이는 나중에 설명할 꼭짓점 레이아웃 목록으로 인덱싱됩니다 드로잉 앱에 레이아웃이 1개만 있으므로 인덱스는 0으로 할게요
이제 다른 메시 속성을 선언하겠습니다
normal 및 bitangent 속성은 position과 유사하지만 사용하는 메모리 오프셋과 semantic이 다르죠
color 속성은 반정밀도 부동소수점 값을 사용합니다 올해 새로 도입된 기능으로 이제 모든 Metal 꼭짓점 형식을 LowLevelMesh와 사용할 수 있죠 압축된 꼭짓점 형식도 포함됩니다
다른 두 매개변수에 대해 UV1 및 UV3 semantic을 사용할게요 또한 올해부터 LowLevelMesh에서 최대 8개의 UV 채널을 사용할 수 있습니다 셰이더 그래프 머티리얼을 통해 이러한 값에 접근할 수 있죠 이제 LowLevelMesh 객체를 생성할 수 있습니다 이를 위해 LowLevelMesh.Descriptor를 만들게요 LowLevelMesh.Descriptor는 개념적으로 Metal의 MTLVertexDescriptor와 유사하지만 RealityKit 메시를 수집하기 위해 필요한 정보도 포함합니다
먼저 vertex 및 index 버퍼에 필요한 Capacity를 선언할게요
그다음 꼭짓점 속성의 목록을 전달합니다 이는 이전 슬라이드에서 만들어 둔 목록을 말하죠
그다음 꼭짓점 레이아웃의 목록을 만듭니다 각 꼭짓점 속성은 레이아웃 중 하나를 사용합니다
LowLevelMesh는 꼭짓점 데이터에 대해 Metal 버퍼를 최대 4개 제공하죠 bufferIndex는 어떤 버퍼를 사용해야 하는지 선언합니다
그다음 각 꼭짓점에 대해 bufferOffset 및 bufferStride를 제공하면 됩니다 대체로 이 경우처럼 버퍼를 하나만 사용하죠
이제 LowLevelMesh를 초기화할 수 있습니다
마지막 단계는 part의 목록을 채우는 것입니다 각 part는 하나의 indexBuffer 영역에 걸쳐 있죠
각 메시 part에 대해 다른 RealityKit 머티리얼 인덱스를 할당할 수 있습니다
이 앱에서는 triangleStrip 형태를 사용하여 메모리 효율을 개선하죠
마지막으로 LowLevelMesh로부터 MeshResource를 만들고 엔티티의 ModelComponent에 할당할 수 있죠
LowLevelMesh의 꼭짓점 데이터를 업데이트해야 하는 경우 withUnsafeMutableBytes API를 사용할 수 있습니다
이 API를 사용하면 렌더링을 위해 GPU에 제출되는 실제 버퍼에 접근할 수 있습니다 메시 데이터를 업데이트할 때 발생하는 오버헤드가 최소화되죠
예를 들어, 메시의 메모리 레이아웃을 미리 알고 있으므로 bindMemory를 사용하여 제공된 원본 포인터를 버퍼 포인터로 변환할 수 있습니다
인덱스 버퍼 데이터의 경우에도 마찬가지입니다 withUnsafeMutableIndices를 통해 LowLevelMesh 인덱스 버퍼를 업데이트할 수 있죠
LowLevelMesh가 앱의 메시 처리 파이프라인을 가속화하는 강력한 도구라는 점을 알게 되었습니다 LowLevelMesh는 꼭짓점 또는 인덱스 버퍼 업데이트를 뒷받침할 수도 있죠 GPU 컴퓨트를 사용하면 됩니다 예를 살펴보겠습니다
이는 드로잉 앱의 Sparkle 붓입니다 사용자의 붓질을 따라 파티클의 필드를 생성하죠 이 파티클 필드는 모든 프레임에서 동적으로 업데이트되므로 아까 단색 붓과는 다른 업데이트 체계를 사용합니다
메시 업데이트의 빈도와 복잡성으로 인해 GPU를 사용하는 것이 좋습니다
자세히 살펴볼게요 Sparkle 붓에는 position, color와 같은 파티클별 속성 목록이 포함되어 있습니다 이전과 마찬가지로 curveDistance 매개변수와 파티클의 size를 포함시킵니다
GPU 파티클 시뮬레이션에서는 SparkleBrushParticle 유형으로 각 파티클의 속성과 속도를 추적합니다 앱에서는 시뮬레이션을 위해 SparkleBrushParticles의 보조 버퍼를 사용합니다
SparkleBrushVertex 구조는 메시의 꼭짓점 데이터를 위해 사용됩니다 이 구조는 셰이더가 3D 공간에서 파티클의 방향을 어떻게 지정해야 하는지 알 수 있도록 각 꼭짓점의 UV 좌표를 포함합니다 파티클마다 4개의 꼭짓점을 갖는 평면이 생성됩니다
앱의 Sparkle 붓 메시를 업데이트하기 위해 버퍼를 2개 유지해야 하는데 SparkleBrushParticles로 채워진 파티클 시뮬레이션 버퍼와 SparkleBrushVertices를 포함하는 LowLevelMesh 꼭짓점 버퍼죠
단색 붓의 경우와 마찬가지로 꼭짓점 버퍼의 사양을 LowLevelMesh 속성 목록과 함께 제공하겠습니다
속성 목록은 SparkleBrushVertex의 구성 요소와 대응됩니다
GPU에서 LowLevelMesh를 채울 때가 되면 Metal 커맨드 버퍼와 compute 커맨드 인코더를 사용하죠
버퍼가 작업을 마치면 RealityKit에서 변경 사항을 자동으로 적용합니다 코드를 보여드릴게요 아까 설명해 드렸듯이 앱에서는 파티클 시뮬레이션을 위해 Metal 버퍼를 사용하고 꼭짓점 버퍼를 위해 LowLevelMesh를 사용합니다
Metal 커맨드 버퍼와 compute 커맨드 인코더를 설정할게요 이를 통해 앱에서 GPU 컴퓨트 커널을 실행하여 메시를 빌드할 수 있습니다
LowLevelMesh에서 replace를 호출하고 커맨드 버퍼를 제공할게요
이렇게 하면 Metal 버퍼가 반환됩니다 이 꼭짓점 버퍼는 렌더링을 위해 RealityKit에서 바로 사용됩니다
GPU에 시뮬레이션을 전달한 다음 커맨드 버퍼를 커밋합니다 커맨드 버퍼가 완료되면 RealityKit에서 자동으로 업데이트된 꼭짓점 데이터를 사용하죠
빠르고 반응성이 좋은 붓질 생성 덕분에 앱이 근사해졌습니다 이제 시선을 끄는 시작 화면을 만들어 앱 빌드를 마무리할게요
시작 화면은 사용자를 앱의 공간으로 환영하는 데 효과적이죠 앱의 시각적 스타일을 흥미롭게 선보일 수도 있죠
앱의 시작 화면은 4가지 시각적 요소를 포함합니다
로고타이프는 2가지의 서체로 작성된 ‘RealityKit Drawing App’이라는 3D 텍스트를 포함하죠
로고마크는 3D 도형이기도 합니다
하단에 물체를 그리기 시작하기 위한 Start 버튼이 있습니다
배경에는 환경에서 발광하는 인상적인 그래픽이 있죠
로고타입을 만드는 것으로 시작해 보겠습니다
우선 기본 시스템 서체로 작성된 ‘RealityKit’을 위한 AttributedString을 만들겠습니다
올해부터 RealityKit에서 AttributedString으로 MeshResource를 만들 수 있습니다, MeshResource extruding을 통해서요
AttributedString을 사용하면 다양한 속성으로 텍스트를 쉽게 추가할 수 있습니다 ‘Drawing App’이라는 텍스트를 작성해 볼게요 다른 서체와 더 큰 서체 크기를 사용하겠습니다
이번에는 paragraphStyle로 텍스트를 가운데에 배치할게요
AttributedString으로 텍스트 스타일을 지정하는 방법은 WWDC21의 ‘Foundation의 새로운 기능’에서 자세히 알아보세요
지금까지 만든 텍스트를 확대해 보겠습니다 현재 3D 모델이 너무 납작하게 보이므로 모델을 맞춤화하겠습니다 ShapeExtrusionOptions 구조를 MeshResource extruding으로 전달하면 됩니다
먼저 depth를 더 크게 지정하여 더 두꺼운 3D 도형을 생성합니다 다음으로 메시에 두 번째 머티리얼을 추가합니다 앞면, 뒷면, 옆면에 할당할 머티리얼 인덱스를 지정할 수 있죠
마지막으로 약간의 chamfer를 추가하여 텍스트를 정면에서 볼 때 윤곽선의 머티리얼이 더 잘 보이도록 할게요 이 경우에는 chamfer 반경을 0.1로 지정하겠습니다
또한 앱에서는 MeshResource extruding으로 로고마크도 생성합니다 SwiftUI 경로를 사용하면 유연하게 도형을 정의할 수 있죠 로고마크는 일련의 베지어 곡선으로 설정됩니다
SwiftUI 경로에 대해 자세히 알아보려면 ‘경로 및 도형 그리기’ SwiftUI 튜토리얼을 확인하세요
이제 시작 화면의 배경에 대해 이야기하겠습니다 시작 화면은 앱에서 미적 요소가 가장 돋보이는 곳이기도 하죠 저는 LowLevelTexture라는 새 API로 시작 화면을 빌드했죠 LowLevelTexture는 LowLevelMesh와 마찬가지로 빠른 리소스 업데이트 체계를 제공하는데 텍스처 애셋을 위해 제공하죠
시작 화면에서 앱은 LowLevelTexture를 사용하여 알약 모양의 도형 모음에 대한 일종의 도형 설명을 생성합니다 이 도형 설명은 텍스처의 빨간색 채널에 저장됩니다
어두운 영역은 알약의 안쪽을 나타내고 밝은 영역은 알약의 겉면을 나타냅니다
앱은 텍스처의 초록색 채널에 시작 화면의 비네트에 대한 설명을 저장합니다
이 텍스처는 Reality Composer Pro의 셰이더 그래프 셰이더를 통해 최종 이미지로 해석됩니다
LowLevelTexture의 Descriptor로부터 LowLevelTexture를 만듭니다 LowLevelTexture.Descriptor는 Metal의 MTLTextureDescriptor와 유사하죠 LowLevelMesh와 마찬가지로 LowLevelTexture를 통해 픽셀 형식과 텍스처 사용을 세밀히 제어할 수 있죠 이제 RealityKit을 통해 압축된 픽셀 형식을 사용할 수 있습니다 시작 화면의 경우 빨간색과 초록색 채널만 필요하므로 RG16Float 픽셀 형식을 사용합니다
설명자로부터 LowLevelTexture를 초기화할 수 있습니다 그런 다음 LowLevelTexture에서 RealityKit의 TextureResource를 만듭니다
이제 이 텍스처를 머티리얼과 사용할 수 있습니다
LowLevelMesh처럼 LowLevelTexture를 GPU에서 업데이트할 수 있죠 먼저 Metal 커맨드 버퍼와 compute 커맨드 인코더를 설정할게요
그다음 커맨드 버퍼로 LowLevelTexture.replace를 호출하세요 이러면 compute 셰이더에 쓸 수 있는 Metal 텍스처가 반환됩니다
마지막으로 GPU 컴퓨트를 실행하고 커맨드 버퍼를 커밋하세요 커맨드 버퍼가 완료되면 Metal 텍스처가 RealityKit에 자동으로 표시됩니다 완료된 시작 화면을 보니 매우 만족스럽네요 눈길을 사로잡는 배경과 맞춤형 3D 지오메트리가 결합되어 독특한 디자인이 되었습니다 우리가 빌드한 경험을 완벽하게 완성했습니다
이제 마무리할게요 오늘은 RealityKit에서 대화형 공간 드로잉 앱을 빌드했습니다 RealityKit의 공간 추적 API를 사용했으므로 공간에서 사용자가 물체를 그리는 위치를 앱이 감지할 수 있죠 SwiftUI 및 고급 호버 효과로 대화형 공간 UI를 빌드하고 붓과 스타일을 맞춤화했습니다 리소스 업데이트가 RealityKit에서 작동하는 방식을 배웠고 고급 LowLevel API로 대화형 메시와 텍스처를 만들었습니다 마지막으로 새 API로 2D 벡터 그래픽을 가져오고 공간 벡터로 전환했죠
‘iOS, macOS, visionOS용 RealityKit API 알아보기’ 및 ‘RealityKit 오디오로 공간 컴퓨팅 앱 향상하기’ 세션에서 올해 출시된 RealityKit 기능에 대해 자세히 알아보세요 여러분이 어떤 작품을 빌드할지 정말 기대됩니다 남은 WWDC24도 재미있게 즐기세요
-
-
4:18 - Using SpatialTrackingSession
// Retain the SpatialTrackingSession while your app needs access let session = SpatialTrackingSession() // Declare needed tracking capabilities let configuration = SpatialTrackingSession.Configuration(tracking: [.hand]) // Request authorization for spatial tracking let unapprovedCapabilities = await session.run(configuration) if let unapprovedCapabilities, unapprovedCapabilities.anchor.contains(.hand) { // User has rejected hand data for your app. // AnchorEntities will continue to remain anchored and update visually // However, AnchorEntity.transform will not receive updates } else { // User has approved hand data for your app. // AnchorEntity.transform will report hand anchor pose }
-
7:07 - Use MeshResource extrusion
// Use MeshResource(extruding:) to generate the canvas edge let path = SwiftUI.Path { path in // Generate two concentric circles as a SwiftUI.Path path.addArc(center: .zero, radius: outerRadius, startAngle: .degrees(0), endAngle: .degrees(360), clockwise: true) path.addArc(center: .zero, radius: innerRadius, startAngle: .degrees(0), endAngle: .degrees(360), clockwise: true) }.normalized(eoFill: true) var options = MeshResource.ShapeExtrusionOptions() options.boundaryResolution = .uniformSegmentsPerSpan(segmentCount: 64) options.extrusionMethod = .linear(depth: extrusionDepth) return try MeshResource(extruding: path, extrusionOptions: extrusionOptions)
-
9:33 - Highlight HoverEffectComponent
// Use HoverEffectComponent with .highlight let placementEntity: Entity = // ... let hover = HoverEffectComponent( .highlight(.init( color: UIColor(/* ... */), strength: 5.0) ) ) placementEntity.components.set(hover)
-
9:54 - Using Blend Modes
// Create an UnlitMaterial with Additive Blend Mode var descriptor = UnlitMaterial.Program.Descriptor() descriptor.blendMode = .add let prog = await UnlitMaterial.Program(descriptor: descriptor) var material = UnlitMaterial(program: prog) material.color = UnlitMaterial.BaseColor(tint: UIColor(/* ... */))
-
13:45 - Shader based hover effects
// Use shader-based hover effects let hoverEffectComponent = HoverEffectComponent(.shader(.default)) entity.components.set(hoverEffectComponent) let material = try await ShaderGraphMaterial(named: "/Root/SolidPresetBrushMaterial", from: "PresetBrushMaterial", in: realityKitContentBundle) entity.components.set(ModelComponent(mesh: /* ... */, materials: [material]))
-
16:56 - Defining a vertex buffer struct for the solid brush
struct SolidBrushVertex { packed_float3 position; packed_float3 normal; packed_float3 bitangent; packed_float2 materialProperties; float curveDistance; packed_half3 color; };
-
19:27 - Defining LowLevelMesh Attributes for solid brush
extension SolidBrushVertex { static var vertexAttributes: [LowLevelMesh.Attribute] { typealias Attribute = LowLevelMesh.Attribute return [ Attribute(semantic: .position, format: MTLVertexFormat.float3, layoutIndex: 0, offset: MemoryLayout.offset(of: \Self.position)!), Attribute(semantic: .normal, format: MTLVertexFormat.float3, layoutIndex: 0, offset: MemoryLayout.offset(of: \Self.normal)!), Attribute(semantic: .bitangent, format: MTLVertexFormat.float3, layoutIndex: 0, offset: MemoryLayout.offset(of: \Self.bitangent)!), Attribute(semantic: .color, format: MTLVertexFormat.half3, layoutIndex: 0, offset: MemoryLayout.offset(of: \Self.color)!), Attribute(semantic: .uv1, format: MTLVertexFormat.float, layoutIndex: 0, offset: MemoryLayout.offset(of: \Self.curveDistance)!), Attribute(semantic: .uv3, format: MTLVertexFormat.float2, layoutIndex: 0, offset: MemoryLayout.offset(of: \Self.materialProperties)!) ] } }
-
21:14 - Make LowLevelMesh
private static func makeLowLevelMesh(vertexBufferSize: Int, indexBufferSize: Int, meshBounds: BoundingBox) throws -> LowLevelMesh { var descriptor = LowLevelMesh.Descriptor() // Similar to MTLVertexDescriptor descriptor.vertexCapacity = vertexBufferSize descriptor.indexCapacity = indexBufferSize descriptor.vertexAttributes = SolidBrushVertex.vertexAttributes let stride = MemoryLayout<SolidBrushVertex>.stride descriptor.vertexLayouts = [LowLevelMesh.Layout(bufferIndex: 0, bufferOffset: 0, bufferStride: stride)] let mesh = try LowLevelMesh(descriptor: descriptor) mesh.parts.append(LowLevelMesh.Part(indexOffset: 0, indexCount: indexBufferSize, topology: .triangleStrip, materialIndex: 0, bounds: meshBounds)) return mesh }
-
22:28 - Creating a MeshResource
let mesh: LowLevelMesh let resource = try MeshResource(from: mesh) entity.components[ModelComponent.self] = ModelComponent(mesh: resource, materials: [...])
-
22:37 - Updating vertex data of LowLevelMesh using withUnsafeMutableBytes API
let mesh: LowLevelMesh mesh.withUnsafeMutableBytes(bufferIndex: 0) { buffer in let vertices: UnsafeMutableBufferPointer<SolidBrushVertex> = buffer.bindMemory(to: SolidBrushVertex.self) // Write to vertex buffer `vertices` }
-
23:07 - Updating LowLevelMesh index buffers using withUnsafeMutableBytes API
let mesh: LowLevelMesh mesh.withUnsafeMutableIndices { buffer in let indices: UnsafeMutableBufferPointer<UInt32> = buffer.bindMemory(to: UInt32.self) // Write to index buffer `indices` }
-
23:58 - Creating a particle brush using LowLevelMesh
struct SparkleBrushAttributes { packed_float3 position; packed_half3 color; float curveDistance; float size; }; // Describes a particle in the simulation struct SparkleBrushParticle { struct SparkleBrushAttributes attributes; packed_float3 velocity; }; // One quad (4 vertices) is created per particle struct SparkleBrushVertex { struct SparkleBrushAttributes attributes; simd_half2 uv; };
-
24:58 - Defining LowLevelMesh Attributes for sparkle brush
extension SparkleBrushVertex { static var vertexAttributes: [LowLevelMesh.Attribute] { typealias Attribute = LowLevelMesh.Attribute return [ Attribute(semantic: .position, format: .float3, layoutIndex: 0, offset: MemoryLayout.offset(of: \Self.attributes.position)!), Attribute(semantic: .color, format: .half3, layoutIndex: 0, offset: MemoryLayout.offset(of: \Self.attributes.color)!), Attribute(semantic: .uv0, format: .half2, layoutIndex: 0, offset: MemoryLayout.offset(of: \Self.uv)!), Attribute(semantic: .uv1, format: .float, layoutIndex: 0, offset: MemoryLayout.offset(of: \Self.attributes.curveDistance)!), Attribute(semantic: .uv2, format: .float, layoutIndex: 0, offset: MemoryLayout.offset(of: \Self.attributes.size)!) ] } }
-
25:28 - Populate LowLevelMesh on GPU
let inputParticleBuffer: MTLBuffer let lowLevelMesh: LowLevelMesh let commandBuffer: MTLCommandBuffer let encoder: MTLComputeCommandEncoder let populatePipeline: MTLComputePipelineState commandBuffer.enqueue() encoder.setComputePipelineState(populatePipeline) let vertexBuffer: MTLBuffer = lowLevelMesh.replace(bufferIndex: 0, using: commandBuffer) encoder.setBuffer(inputParticleBuffer, offset: 0, index: 0) encoder.setBuffer(vertexBuffer, offset: 0, index: 1) encoder.dispatchThreadgroups(/* ... */) // ... encoder.endEncoding() commandBuffer.commit()
-
27:01 - Use MeshResource extrusion to generate 3D text
// Use MeshResource(extruding:) to generate 3D text var textString = AttributedString("RealityKit") textString.font = .systemFont(ofSize: 8.0) let secondLineFont = UIFont(name: "ArialRoundedMTBold", size: 14.0) let attributes = AttributeContainer([.font: secondLineFont]) textString.append(AttributedString("\nDrawing App", attributes: attributes)) let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.alignment = .center let centerAttributes = AttributeContainer([.paragraphStyle: paragraphStyle]) textString.mergeAttributes(centerAttributes) var extrusionOptions = MeshResource.ShapeExtrusionOptions() extrusionOptions.extrusionMethod = .linear(depth: 2) extrusionOptions.materialAssignment = .init(front: 0, back: 0, extrusion: 1, frontChamfer: 1, backChamfer: 1) extrusionOptions.chamferRadius = 0.1 let textMesh = try await MeshResource(extruding: textString extrusionOptions: extrusionOptions)
-
28:25 - Use MeshResource extrusion to turn a SwiftUI Path into 3D mesh
// Use MeshResource(extruding:) to bring SwiftUI.Path to 3D let graphic = SwiftUI.Path { path in path.move(to: CGPoint(x: -0.7, y: 0.135413)) path.addCurve(to: CGPoint(x: -0.7, y: 0.042066), control1: CGPoint(x: -0.85, y: 0.067707), control2: CGPoint(x: -0.85, y: 0.021033)) // ... } var options = MeshResource.ShapeExtrusionOptions() // ... let graphicMesh = try await MeshResource(extruding: graphic extrusionOptions: options)
-
29:44 - Defining a LowLevelTexture
let descriptor = LowLevelTexture.Descriptor(pixelFormat: .rg16Float, width: textureResolution, height: textureResolution, textureUsage: [.shaderWrite, .shaderRead]) let lowLevelTexture = try LowLevelTexture(descriptor: descriptor) var textureResource = try TextureResource(from: lowLevelTexture) var material = UnlitMaterial() material.color = .init(tint: .white, texture: .init(textureResource))
-
30:27 - Update a LowLevelTexture on the GPU
let lowLevelTexture: LowLevelTexture let commandBuffer: MTLCommandBuffer let encoder: MTLComputeCommandEncoder let computePipeline: MTLComputePipelineState commandBuffer.enqueue() encoder.setComputePipelineState(computePipeline) let writeTexture: MTLTexture = lowLevelTexture.replace(using: commandBuffer) encoder.setTexture(writeTexture, index: 0) // ... encoder.dispatchThreadgroups(/* ... */) encoder.endEncoding() commandBuffer.commit()
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.