Android XR 기본사항 알아보기: 2부. Orbiter 및 공간 환경

1. 시작하기 전에

학습할 내용

  • Android XR로 제공되는 고유한 사용자 환경
  • Jetpack Compose XR 라이브러리를 사용하여 Android XR 헤드셋에 맞게 앱을 최적화하는 방법
  • Jetpack Compose XR 라이브러리의 UI 요소를 사용하는 방법
  • Android XR용 앱 빌드에 관해 자세히 알아볼 수 있는 곳

이 Codelab의 의도하지 않은 용도

필요한 항목

빌드할 항목

이 Codelab에서는 플로팅 UI 요소를 추가하고 앱을 사용하는 동안 사용자를 둘러싼 가상 환경을 맞춤설정하여 기존 XR 기능을 사용하여 앱을 더욱 최적화합니다.

시작점

최종 결과

2. 설정

코드 가져오기

  1. 이 Codelab의 코드는 xr-codelabs GitHub 저장소의 xr-fundamentals 디렉터리에서 찾을 수 있습니다. 저장소를 클론하려면 다음 명령어를 실행하세요.
git clone https://github.com/android/xr-codelabs.git
  1. 또는 저장소를 ZIP 파일로 다운로드할 수도 있습니다.

프로젝트 열기

  • Android 스튜디오를 시작한 후 xr-fundamentals/part1 디렉터리를 가져옵니다. xr-fundamentals/part2 디렉터리에는 솔루션 코드가 포함되어 있습니다. 솔루션 코드는 도움이 필요한 경우 또는 전체 프로젝트를 살펴보고 싶을 때 언제든지 참조할 수 있습니다.

코드 숙지하기

  • Android 스튜디오에서 프로젝트를 열고 시작 코드를 살펴봅니다.
  • 첫 번째 Codelab을 수강하지 않았거나 아직 Android XR 에뮬레이터를 사용하지 않은 경우 Android XR 에뮬레이터에서 앱 실행의 단계에 따라 앱을 실행합니다.

3. XR 개념 알아보기: Orbiter

Orbiter는 전체 공간 모드에서 사용할 수 있는 플로팅 UI 요소로, 일반적으로 공간 패널 내의 콘텐츠 또는 궤도가 고정된 다른 항목을 제어하는 데 사용됩니다. 콘텐츠 컨트롤에 Orbiter를 사용하면 콘텐츠에 더 많은 공간이 확보되므로 사용자가 기본 콘텐츠가 계속 표시되는 동안 Orbiter에 포함된 기능에 빠르게 액세스할 수 있습니다. Orbiter를 사용하면 기존 UI 구성요소(예: 탐색 메뉴)를 통합하거나 새 구성요소를 만들 수 있습니다.

또한 Orbiter API를 사용하면 홈 공간 모드 또는 XR 기기가 아닌 기기에서 실행할 때와 같이 일반적으로 Orbiter의 콘텐츠를 렌더링하고 전체 공간 모드에서 실행할 때는 자동으로 Orbiter로 분할할 수 있습니다.

홈 공간 모드에서는 이 탐색 레일이 기본 앱 패널 내에 렌더링됩니다.

전체 공간 모드에서는 탐색 레일이 기본 패널에 연결된 Orbiter로 분할됩니다.

이 시점에서 앱의 상단 앱 바에는 홈 공간 모드와 전체 공간 모드 간에 전환하는 버튼이 포함됩니다. 이 버튼은 전체 공간 모드에서 실행할 때 Orbiter 내부에 포함될 수 있는 컨트롤의 완벽한 예입니다. 컨트롤을 이동하여 기본 패널을 공전하면 컨트롤이 눈에 띄게 표시되는 동시에 클릭 시 앱의 콘텐츠가 해당 패널로 접히는 것을 시각적으로 나타낼 수 있기 때문입니다.

현재 상태입니다.

구현할 내용

Orbiter의 설계 고려사항에 관한 자세한 내용은 공간 UI를 참고하세요.

4. Orbiter 추가

공간 모드 전환 버튼 래핑

공간 모드 전환 버튼을 Orbiter로 전환하려면 ToggleSpaceModeButton 컴포저블을 Orbiter 컴포저블 내에 래핑합니다.

ui/component/XRFundamentalsTopAppBar .kt

import androidx.compose.foundation.shape.CornerSize
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
import androidx.xr.compose.spatial.EdgeOffset
import androidx.xr.compose.spatial.Orbiter
import androidx.xr.compose.spatial.OrbiterEdge
import androidx.xr.compose.subspace.layout.SpatialRoundedCornerShape

... 

Orbiter(
    position = OrbiterEdge.Top,
    alignment = Alignment.End,
    offset = EdgeOffset.inner(16.dp),
    shape = SpatialRoundedCornerShape(
        CornerSize(percent = 100)
    )
) {
    ToggleSpaceModeButton()
}

이제 앱을 실행합니다. 홈 공간 모드에서 실행하면 아무것도 변경되지 않은 것을 알 수 있습니다. 하지만 전환 버튼을 클릭하고 앱이 전체 공간 모드로 전환되면 버튼이 더 이상 상단 앱 바에 있지 않고 기본 공간 패널의 오른쪽 상단 가장자리에 있는 것을 볼 수 있습니다.

Orbiter는 기본 공간 패널에 고정됩니다. UI 트리의 가장 가까운 상위 공간 항목이기 때문입니다. 기본 공간 패널을 기준으로 한 Orbiter의 정확한 위치는 position, alignment, offset 매개변수에 의해 결정됩니다. 이러한 매개변수를 수정하여 지원되는 동작의 범위를 확인해 보세요.

홈 공간 모드

전체 공간 모드

5. XR 개념 알아보기: 공간 환경

Orbiter를 사용하여 UI 요소의 3D 공간에서 위치를 맞춤설정하는 것은 XR 기기의 사용자 환경을 개선하는 좋은 방법입니다. 앱을 사용할 때 사용자가 있는 공간 환경을 맞춤설정하여 환경을 더욱 개선할 수 있습니다.

공간 환경은 깊이, 텍스처, 3D 도형 애셋을 통합하여 시각적으로 몰입도 높은 풍부한 환경을 만들 수 있습니다. 이는 구형 스카이박스 이미지(EXR 형식)를 사용하여 먼 파노라마 배경을 제공하거나 도형 애셋(glTF 형식)을 사용하여 스카이박스에 블렌딩할 수 있는 전경 및 중경 요소를 제공하는 방식으로 이루어집니다. 예를 들어 동영상 스트리밍 앱은 프로젝션 화면과 자동차가 있는 드라이브인 영화관의 glTF와 함께 야간 스카이박스를 사용할 수 있습니다. 사용자의 공간 환경을 설정하기 위한 애셋을 만들 때는 적절한 파일 크기를 유지하면서 애셋의 해상도를 높여야 합니다. 자세한 내용은 환경 애셋 최적화를 참고하세요.

또한 공간 환경의 불투명도를 제어할 수 있습니다. 이렇게 하면 실제 환경의 동영상 스트림이 가상 환경을 통과하여 혼합되므로 사용자가 방향을 유지하는 데 도움이 됩니다.

바위가 많은 공간 환경에 서 있는 사람으로, 중간에 큰 UI 패널이 있습니다.

다음 단계에서는 앱에 도형 애셋을 추가하고 사용자가 환경을 선택할 수 있는 메뉴를 만듭니다.

공간 환경 설계 및 구현에 관한 모든 세부정보는 공간 환경앱에 공간 환경 추가를 참고하세요.

6. 사용자가 공간 환경을 변경하도록 허용

앱이 공간 환경을 제어하는 방법

시작하기 전에 앱이 공간 환경을 정확하게 제어하는 방법을 이해하는 것이 좋습니다.

패널 내 콘텐츠와 달리 앱은 환경을 직접 제어하지 않습니다. 대신 SceneCore 세션과 상호작용하여 시스템에서 사용하려는 환경에 대한 환경설정을 제공할 수 있습니다. 이 환경설정은 스카이박스 EXR 이미지 또는 도형 glTF로 구성된 SpatialEnvironmentPreference로 표시됩니다. 앱이 환경설정을 제공할 때 어떤 일이 발생하는지는 앱이 환경설정을 지정할 때의 기능에 따라 다릅니다. 앱에 환경을 변경하는 기능이 있는 경우 시스템은 즉시 사용합니다. 기능이 없는 경우에는 앱에서 해당 기능을 획득할 때 환경설정이 적용됩니다.

예를 들어 앱은 일반적으로 홈 공간 모드에서 실행되는 동안 환경을 변경할 수 없지만 전체 공간 모드에서 실행되는 동안에는 일반적으로 환경을 변경할 수 있습니다. 따라서 사용자가 홈 공간 모드에서 환경 설정을 설정하도록 허용하면 일반적으로 앱이 전체 공간 모드에서 실행될 때까지 해당 설정이 적용되지 않습니다.

XR SceneCore 라이브러리의 종속 항목 추가

공간 환경 수정을 시작하려면 환경 애셋을 로드하고 환경 설정을 지정하는 데 사용할 XR SceneCore 라이브러리의 종속 항목을 추가합니다. 애셋을 로드하는 일부 API에서 ListenableFuture 데이터 유형을 사용하므로 kotlinx-coroutines-guava 아티팩트의 종속 항목도 추가해야 합니다.

libs.version.toml

[versions]
...
xrSceneCore = "1.0.0-alpha04"
kotlinxCoroutinesGuava = "1.10.2"

[libraries]
...
androidx-xr-scenecore = { group = "androidx.xr.scenecore", name = "scenecore", version.ref = "xrSceneCore"}
jetbrains-kotlinx-coroutines-guava = {group = "org.jetbrains.kotlinx", name="kotlinx-coroutines-guava", version.ref = "kotlinxCoroutinesGuava"}

app/build.gradle.kts

dependencies {
    ...
    implementation(libs.androidx.xr.scenecore)
    implementation(libs.jetbrains.kotlinx.coroutines.guava)
    ...
}

프로젝트에 환경 애셋 추가

고유한 환경 설정을 지정하려면 스카이박스 또는 도형 애셋이 필요합니다. 이 Codelab에서는 green_hills_ktx2_mipmap.glb 도형 애셋만 사용합니다. 이 애셋은 솔루션 코드가 포함된 part2 폴더 또는 GitHub에서 찾을 수 있습니다.

  1. Android 스튜디오의 Project 창에서 앱 모듈을 마우스 오른쪽 버튼으로 클릭합니다. 그런 다음 New > Folder > Assets Folder를 선택하고 Finish를 클릭하여 폴더를 만듭니다.
  2. 방금 만든 app/src/main/assets 폴더에 GLB 파일을 추가합니다.

환경 옵션 모델링

UI 코드와 시스템 API 간의 상호작용을 간소화하려면 Kotlin 데이터 클래스를 만들어 각 환경 옵션을 모델링할 수 있습니다.

  1. Project 창에서 com.example.android.xrfundamentals 패키지를 마우스 오른쪽 버튼으로 클릭하고 New > Package를 선택합니다. 패키지 이름으로 com.example.android.xrfundamentals.environment를 입력합니다.
  2. 패키지를 마우스 오른쪽 버튼으로 클릭하고 New > Kotlin Class/File을 선택합니다. 이름으로 EnvironmentOption을 입력하고 Data class 유형을 클릭합니다.
  3. 방금 만든 파일에 다음 코드를 추가합니다.

EnvironmentOption.kt

data class EnvironmentOption(val name: String, val skyboxPath: String?, val geometryPath: String?)

val DEFAULT_ENVIRONMENT = EnvironmentOption("Default", null, null)

val ENVIRONMENT_OPTIONS = listOf(
    DEFAULT_ENVIRONMENT,
    EnvironmentOption("Green Hills", null, "green_hills_ktx2_mipmap.glb")
)

로드 애셋을 만들고 SpatialEnvironmentPreference를 반환하는 도우미 추가

이제 데이터 클래스에 도우미 메서드를 추가하면 EnvironmentOption을 해당하는 SpatialEnvrionmentPreference로 쉽게 변환할 수 있습니다.

EnvironmentOption.kt

import androidx.xr.runtime.Session
import androidx.xr.scenecore.ExrImage
import androidx.xr.scenecore.GltfModel
import androidx.xr.scenecore.SpatialEnvironment
import kotlinx.coroutines.guava.await

...

data class EnvironmentOption(val name: String, val skyboxPath: String?, val geometryPath: String?) {
    suspend fun toSpatialEnvironmentPreference(session: Session): SpatialEnvironmentPreference? {
        if (skyboxPath == null && geometryPath == null) {
            return null
        } else {
            val skybox = skyboxPath?.let {
                ExrImage.create(session, it).await()
            }

            val geometry = geometryPath?.let {
                GltfModel.create(session, it).await()
            }

            return SpatialEnvironmentPreference(skybox, geometry)
        }
    }
}

여기서 몇 가지 주목할 점이 있습니다.

  • 스카이박스와 도형이 모두 null이면 기본 시스템 환경 설정을 사용해야 함을 나타내기 위해 null이 반환됩니다. 자세한 내용은 setSpatialEnvironmentPreference를 참고하세요.
  • skyboxgeometry 리소스는 비동기식으로 생성됩니다. 이러한 애셋은 상당히 클 수 있고 메모리로 읽는 데 시간이 걸리기 때문입니다. 프로덕션 앱에서는 환경을 자주 전환하는 경우 이러한 애셋을 메모리에 캐시하는 것이 좋습니다.

환경 선택 UI 구현

UI를 구현하려면 클릭 시 환경 옵션을 순환하는 두 번째 Orbiter를 추가합니다.

Orbiter 추가

  1. Project 창에서 app 모듈을 마우스 오른쪽 버튼으로 클릭하고 New > Vector Asset을 선택합니다. Clip art 필드를 클릭하고 landscape 애셋(채워진 아이콘 모음)을 검색하여 선택한 다음 OK를 클릭하고 Next를 클릭하여 애셋을 만듭니다.
  2. com.example.android.xrfundamentals.ui.component 패키지를 마우스 오른쪽 버튼으로 클릭하고 New > Kotlin Class/File을 선택합니다. 이름으로 EnvironmentSelectionOrbiter를 입력하고 File 유형을 클릭합니다.
  3. 방금 만든 파일 내에 다음과 같은 EnvironmentSelectionOrbiter 컴포저블 구현을 추가합니다.

EnvironmentSelectionOrbiter.kt

import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.xr.compose.spatial.EdgeOffset
import androidx.xr.compose.spatial.Orbiter
import androidx.xr.compose.spatial.OrbiterEdge
import androidx.xr.compose.subspace.layout.SpatialRoundedCornerShape
import com.example.android.xrfundamentals.R

@Composable
fun EnvironmentSelectionOrbiter(
    modifier: Modifier = Modifier,
    onClick: () -> Unit = {},
) {
    Orbiter(
        position = OrbiterEdge.Top,
        alignment = Alignment.Start,
        offset = EdgeOffset.inner(16.dp),
        shape = SpatialRoundedCornerShape(
            CornerSize(100)
        )
    ) {
        FilledTonalIconButton(
            modifier = modifier,
            onClick = onClick,
        ) {
            Icon(painterResource(R.drawable.baseline_landscape_24), "Show environment selection dialog")
        }
    }
}
  1. 마지막으로 기본 공간 패널 내에 EnvironmentSelectionOrbiter를 추가합니다.

XRFundamentalsApp.kt

import androidx.xr.compose.platform.LocalSpatialCapabilities
import com.example.android.xrfundamentals.ui.component.EnvironmentSelectionOrbiter

...

SpatialPanel(...) {

    // Only show the environment selection orbiter if the app is actually able to
    // change the environment
    if (LocalSpatialCapabilities.current.isAppEnvironmentEnabled) {
        EnvironmentSelectionOrbiter(
            onClick = { TODO() }
        )
    }
    ...
}

Orbiter를 클릭할 때 환경 변경

모든 것이 작동하도록 하려면 마지막 단계로 EnvironmentSelectionOrbiter 클릭 핸들러에서 setSpatialEnvironmentPreference를 호출해야 합니다.

  1. 현재 환경 옵션을 추적하는 변수를 설정합니다(홈 공간 모드와 전체 공간 모드 간에 전환할 때 상태가 유지되도록 Subspace 외부). 또한 현재 XR 세션의 변수와 toSpatialEnvironmentPreference 도우미를 호출하는 코루틴 범위를 만듭니다.

XRFundamentalsApp.kt

import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.xr.compose.platform.LocalSession

... 

var currentEnvironmentOptionIndex by remember { mutableStateOf(0) }

Subspace {
    val session = checkNotNull(LocalSession.current)
    val scope = rememberCoroutineScope()
    ...
}
  1. onClick 콜백을 구현하여 환경 옵션을 순환합니다.

XRFundamentalsApp.kt

EnvironmentSelectionOrbiter(
    onClick = {
        scope.launch {
            currentEnvironmentOptionIndex =
                (currentEnvironmentOptionIndex + 1) % ENVIRONMENT_OPTIONS.size
            session.scene.spatialEnvironment.setSpatialEnvironmentPreference(
                ENVIRONMENT_OPTIONS[currentEnvironmentOptionIndex].toSpatialEnvironmentPreference(
                    session
                )
            )
        }
    }
)

앱을 다시 실행하면 Green Hills 환경과 기본 환경 간에 전환할 수 있습니다.

b0e9571ef5f5597b.gif

7. 축하합니다

XR을 최대한 활용하는 방법에 관해 계속 알아보려면 다음 리소스와 연습을 확인하세요.

추가 자료

도전과제

  • 추가 환경 애셋을 찾거나 만들고 옵션으로 추가합니다.
  • 사용자가 setPassthroughOpacityPreference API를 사용하여 패스 스루 환경설정을 지정할 수 있도록 환경 컨트롤러와 UI를 수정합니다. 패스 스루를 제어하는 것은 환경 애셋을 변경하는 것과는 다른 기능에 의해 관리됩니다.

참조 문서