Android XR

Wyjdź poza 2D dzięki niestandardowej geometrii 3D w pakiecie SDK Jetpack XR

Głośniki

  • Egor Yusov

    Inżynier oprogramowania

  • Jay Abi-Saad

    Inżynier oprogramowania

Informacje

Informacje o wystąpieniu

Przeniesienie aplikacji na Androida z tradycyjnych ekranów 2D do środowisk przestrzennych wymaga nowego podejścia do tworzenia kształtów.

Podczas tej sesji omówimy interfejs Custom Mesh API w pakiecie Jetpack XR SDK, który umożliwia automatyczne generowanie proceduralnej geometrii 3D bezpośrednio w czasie działania. Omówimy podstawowy przepływ pracy, od definiowania układów wierzchołków po płynne renderowanie obiektów dynamicznych w przestrzeni fizycznej użytkownika.

Po zakończeniu tej prezentacji będziesz mieć praktyczne umiejętności tworzenia interaktywnej, niestandardowej geometrii 3D całkowicie za pomocą kodu.

Podsumowanie

  • Interfejs API introspekcji glTF w Jetpack SceneCore udostępnia metadane modelu w czasie działania. Możesz odtwarzać umieszczone animacje, przekształcać podwęzły lub zastępować siatki materiałami i teksturami.
  • Jetpack SceneCore zawiera interfejsy API siatki do generowania niestandardowej geometrii 3D w Kotlinie. 4 elementy, takie jak VertexLayout i MeshBuffer, tworzą modele trójkąt po trójkącie bez użycia narzędzi 3D.
  • Siatki dynamiczne obsługują aktualizacje w czasie rzeczywistym, gdy użytkownicy rysują ścieżki lub animują powierzchnie. Aktualizowanie tylko zmienionych danych wierzchołków przesyła modyfikacje bezpośrednio do GPU bez ponownego tworzenia siatki lub elementu.
  • Gemini w Android Studio może pisać złożone animacje wierzchołków i obliczenia oświetlenia dla interfejsów API siatki. Jeden prompt generuje działający kod, który animuje siatki wierzchołków i ponownie oblicza wektory normalne w każdej klatce.

Zasoby

Gemini Notebook

Tematy

  • jetpack-xr
  • jetpack-scenecore
  • android-xr
  • 3d-geometry
  • mesh-api
  • gltf

Transkrypcja

Pobierz transkrypcję

Wprowadzenie do Androida XR i SceneCore

  • Cześć.

  • Nazywam się Jay.

  • A ja Egor.

  • Pracujemy w Google jako inżynierowie oprogramowania nad Jetpack XR.

  • Premiera XREAL Aura zbliża się wielkimi krokami, dlatego chcemy mieć pewność, że masz wszystkie narzędzia potrzebne do tworzenia świetnych doświadczeń przestrzennych na tej platformie.

  • Co najlepsze, nie potrzebujesz nawet sprzętu XR, aby zacząć.

  • Wszystkie funkcje, które pokażemy, są już dostępne w emulatorze Androida XR w Android Studio.

  • Chcemy pokazać, że dzięki bibliotece Jetpack SceneCore nie musisz mieć dogłębnej wiedzy o 3D, aby tworzyć na platformie bogate środowiska przestrzenne.

  • Wszystko, co zobaczysz, działa w aplikacji na Androida, napisanej w idiomatycznym języku Kotlin bezpośrednio w Android Studio.

  • Niedawno udostępniliśmy stabilną wersję Jetpack SceneCore. Dzisiaj omówimy kilka ciekawych ulepszeń interfejsów API modeli 3D, a także nasze zupełnie nowe interfejsy API siatki, które umożliwiają tworzenie niestandardowej geometrii 3D na bieżąco.

Praca z modelami 3D glTF

  • Zacznijmy jednak od modeli 3D.

  • Jeśli chodzi o modele 3D, pierwszą rzeczą, której potrzebujemy, jest format pliku.

  • Otwarte standardy są kluczowym elementem Androida XR.

  • Dlatego naturalnie zdecydowaliśmy się na format glTF od Khronos Group.

  • Jeśli nie znasz się na świecie 3D, możesz się zastanawiać, czym jest glTF.

  • Można go porównać do formatu JPEG, ale dla modeli 3D.

  • Struktura zasobu glTF dostarcza dane dotyczące m.in. animacji, podwęzłów i siatek.

    {
        "asset" : {...},
        "nodes" : [
            {
                "name" : "Right_Arm",
                "rotation" : [...],
                "scale" : [...],
                "translation" : [...],
            },
        ],
        "animations" : [
            {
                "name" : "Wave",
            },
        ],
        "meshes" : [
            {
                "name" : "Torso",
                "primitives" : [ ... ],
            },
        ],
    
        // Way way more!
    
    }
  • Na potrzeby tej prezentacji możesz jednak traktować węzły podrzędne jako części modelu, z których każda ma własną pozycję i obrót.

  • Siatki to widoczne powierzchnie, a animacje to ruchy tych węzłów podrzędnych i siatek, które autor zasobu zapisał wcześniej.

  • Świetnie.

  • W tym momencie możesz się zastanawiać, skąd wziąć model glTF.

  • Oprócz wielu dostępnych modeli możesz też tworzyć własne.

  • Teraz, gdy znamy już podstawy formatu glTF, użyjmy go w naszej aplikacji.

  • Zaczniemy od podstawowego projektu na Androida XR z użyciem Jetpack SceneCore.

  • Możesz zobaczyć panel przestrzenny z aktywnością unoszącą się w przestrzeni użytkownika.

  • Możesz go chwycić, przenieść i zmienić jego rozmiar według własnych upodobań.

  • Chociaż ta prezentacja dotyczy wyłącznie 3D, warto też zapoznać się z pozostałymi elementami pakietu Jetpack XR SDK.

  • Panele przestrzenne są fajne, ale chcemy więcej magii.

  • A czy jest lepszy sposób na dodanie odrobiny magii niż model 3D?

  • Nasz projektant stworzył model samochodu w formacie glTF.

    // Load assets/models/car.glb
    val carModel = GltfModel.create(
        session,
        Paths.get("models", "car.glb")
    )
    
    // Create an entity in the scene
    val carEntity = GltfModelEntity.create(
        session,
        carModel,
        parent = session.scene.activitySpace
    )
  • Dodali nawet animacje, których użyjemy później.

  • Dzięki SceneCore mogę użyć interfejsów API modeli 3D, aby załadować go bezpośrednio do sceny.

Animacje modeli i manipulowanie podwęzłami

  • Sprawmy jednak, aby samochód wyglądał bardziej realistycznie.

  • Pamiętasz te wszystkie bogate metadane w pliku glTF?

  • Dzięki nowemu interfejsowi API introspekcji glTF możesz teraz uzyskać do niego dostęp, zaczynając od animacji.

    // Access every animation in car.glb
    val animations = carEntity.getAnimations()
       .associateBy { it.name }
    
    // Open the doors...
    animations["lf_door_open"]?.start()
    animations["rt_door_open"]?.start()
    
    // ...and close them again
    animations["lf_door_close"]?.start()
    animations["rt_door_close"]?.start()
    
    // Same for the hood and trunk
    animations["hood_open"]?.start()
    animations["trunk_open"]?.start()
  • Za pomocą kilku linijek kodu mogę odtworzyć dowolną animację glTF wbudowaną w model samochodu, np. otwieranie i zamykanie drzwi, maski lub bagażnika.

  • Animacje to jednak nie jedyne metadane, do których masz dostęp.

  • Interfejs API introspekcji glTF udostępnia też podwęzły w modelu jako GltfModelNode, co pozwala na ich przenoszenie, obracanie i skalowanie zgodnie z preferencjami.

    // Wheel sub nodes, named by our designer
    val nodes = carEntity.nodes.associateBy { it.name }
    val front = listOf("lf_frontWheel_geo",
       "rt_fontWheel_geo").map { nodes[it]!! }
    val rear = listOf("lf_RearWheel_geo",
       "rt_RearWheel_geo").map { nodes[it]!! }
    
    // Update a sub node's transform at runtime
    fun GltfModelNode.rotate(q: Quaternion) {
       localPose = localPose.copy(rotation = q)
    }
    
    // Each frame: steer front, spin all four
    fun drive(steerDeg: Float, spinDeg: Float) {
       val steer = Quaternion.fromAxisAngle(
           Vector3.Up, steerDeg)
       val spin = Quaternion.fromAxisAngle(
           Vector3.Right, spinDeg)
       front.forEach { it.rotate(steer * spin) }
       rear.forEach { it.rotate(spin) }
    }
  • Na przykład tutaj mogę osobno aktualizować pozycję i obrót kół podczas jazdy samochodu.

  • Żeby było jasne, to, co widzisz, nie jest osadzoną animacją glTF.

  • Tutaj obracam i steruję kołami indywidualnie w czasie działania.

stosowanie materiałów i tekstur;

  • Ostatnim elementem metadanych, do którego możesz uzyskać dostęp, jest dowolna siatka w modelu glTF.

  • Za chwilę Egor omówi siatki bardziej szczegółowo.

  • Na razie jednak zajmijmy się stylizacją samochodu.

  • Za pomocą interfejsu API tekstur możemy wczytać teksturę obrazu PNG, np. wzór metalu.

    // Load a PNG with the Texture API
    val metal = Texture.create(
       session, Paths.get("textures", "metal.png"))
    
    // Create KhronosPbrMaterials
    val (body, doors, hood) = List(3) {
       KhronosPbrMaterial
           .create(session, AlphaMode.OPAQUE)
    }
    body.setBaseColorFactor(CYAN)
    doors.setBaseColorFactor(PINK)
    hood.setBaseColorTexture(metal)
    
    // Override meshes by sub node name
    val nodes = carEntity.nodes.associateBy { it.name }
    nodes["mainBody_geo"]?.setMaterialOverride(body)
    nodes["lf_door_geo"]?.setMaterialOverride(doors)
    nodes["rt_door_geo"]?.setMaterialOverride(doors)
    nodes["hood_geo"]?.setMaterialOverride(hood)
  • Następnie za pomocą interfejsu API materiałów możemy podłączyć go bezpośrednio do KhronosPbrMaterial.

  • Jeśli dopiero zaczynasz pracę z grafiką 3D, możesz wyobrazić sobie siatkę jako blat stołu, materiał jako obrus, a teksturę jako wzór nadrukowany na tym obrusie.

  • W tym przypadku zamiast blatu stołu używamy utworzonych obiektów materiałów, aby zastąpić siatki powiązane z podwęzłami maski i drzwi samochodu.

  • Wystarczyło kilka linijek kodu, aby zmienić wygląd i styl samochodu – i to w czasie działania.

  • To świetne pomysły, ale mam jeszcze lepszy.

  • Jak fajnie byłoby narysować tor wyścigowy, po którym będzie jeździł samochód?

  • W tym przypadku gotowy model glTF nie jest odpowiedni.

  • Zamiast tego potrzebujemy możliwości generowania geometrii 3D na bieżąco.

  • Przekazuję głos Egorowi, który opowie o naszych nowych interfejsach API siatki Jetpack SceneCore.

Wprowadzenie do interfejsów API siatki

  • Dziękuję, Jay.

  • Zróbmy tak, żeby użytkownik mógł narysować tor wyścigowy palcem w Living Room.

  • Tor nie istnieje, dopóki nie zostanie narysowany, więc nikt nie może go wcześniej wymodelować.

  • Zbudujemy go w kodzie, trójkąt po trójkącie.

  • Siatka zaczyna się od wierzchołków, czyli punktów w przestrzeni.

    // 4 vertices: x, y, z
    val positions = floatArrayOf(
        0f, 0f, 0f,   // v0
        1f, 0f, 0f,   // v1
        0f, 1f, 0f,   // v2
        1f, 1f, 0f    // v3
    )
    
    // 2 triangles = 6 indices
    val indices = intArrayOf(
        0, 1, 2,
        2, 1, 3
    )
  • Każdy z nich ma atrybuty takie jak pozycja, normalna do oświetlenia, kolor i współrzędne tekstury.

  • Indeksy łączą kropki.

  • Każde 3 tworzą trójkąt.

  • Dwa trójkąty tworzą kwadrat, 6 indeksów, ale tylko 4 wierzchołki, ponieważ 2 są wspólne.

  • Jeśli będziesz kontynuować, otrzymasz sześcian.

  • Każdy model 3D, w tym samochód Jaya, jest zbudowany w ten sam sposób, tylko z większej liczby trójkątów.

Elementy składowe interfejsu Mesh API

  • Mesh API mapuje te elementy na 4 elementy składowe: VertexLayout, MeshBuffer, CustomMesh i MeshEntity.

  • Omówmy je po kolei.

  • Najpierw VertexLayout.

    // How to read each vertex from bytes
    val layout = VertexLayout.Builder()
        .addAttribute(POSITION, FLOAT3) // 12 B
        .addAttribute(NORMAL, FLOAT3)   // 12 B
        .addAttribute(UV0, FLOAT2)      //  8 B
        .build()              // stride: 32 B
    
    // Offsets and stride are computed,
    // or set them explicitly.
    
    // Attributes can live in separate buffers
    val split = VertexLayout.Builder()
        .addAttribute(POSITION, FLOAT3)
        .startNextBuffer()
        .addAttribute(COLOR, UBYTE4_NORM)
        .build()
  • Dane wierzchołków to tylko bajty w ByteBuffer, a każda siatka pakuje je inaczej.

  • VertexLayout określa, jak odczytywać dane: które atrybuty, jakiego typu i gdzie się znajdują.

  • W tym przypadku jest to pozycja, wektor normalny i współrzędne UV, czyli współrzędne tekstury, o łącznym rozmiarze 32 bajtów.

  • Atrybuty mogą też znajdować się w osobnych buforach, dzięki czemu można aktualizować tylko te, które się zmieniają.

  • Po drugie, MeshBuffer, czyli kontener na nieprzetworzone dane.

    // Vertex bytes + 32-bit indices
    val meshBuffer = MeshBuffer.create(
        session, layout,
        vertexData = listOf(ByteBufferRegion(
            vertexBytes, 0, vertexBytes.capacity())),
        indexData = ByteBufferRegion(
            indexBytes, 0, indexBytes.capacity()))
    
    // Copied to the GPU: reuse your
    // ByteBuffers right away
    
    // Need updates at runtime?
    val dynamic = MeshBuffer.createDynamic(
        session, layout,
        vertexCount = 1000, indexCount = 3000)
  • Podajesz układ, bajty wierzchołków i 32-bitowe indeksy.

  • Dane są natychmiast kopiowane do GPU, dzięki czemu możesz ponownie użyć lub zwolnić ByteBuffers.

  • Domyślnie MeshBuffer jest statyczny.

  • Jest ustawiany raz i zoptymalizowany pod kątem szybkiego renderowania.

  • Jeśli musisz zmienić go w czasie działania, createDynamic przydziela stałą pojemność, którą możesz później zaktualizować.

  • Po trzecie, CustomMesh, czyli widok MeshBuffer.

    // A view: which indices to draw, and how
    val road = CustomMesh
        .BuilderFromMeshBuffer(session, meshBuffer)
        // subset 0: asphalt
        .addSubset(TRIANGLES, indexOffset = 0,
            indexCount = roadCount)
        // subset 1: lane markings
        .addSubset(TRIANGLES,
            indexOffset = roadCount,
            indexCount = lineCount)
        .build()
    
    // Shortcut: builds MeshBuffer + CustomMesh
    CustomMesh.BuilderFromMeshData(session, layout)
  • Nie kopiuje żadnych danych.

  • Określa, jaki zakres indeksów ma zostać narysowany i w jaki sposób – trójkąty czy pasy trójkątów.

  • W tym przypadku asfalt i oznaczenia pasów ruchu znajdują się w tym samym MeshBuffer.

  • Wiele niestandardowych siatek może współdzielić ten sam obiekt MeshBuffer, a gdy nie potrzebujesz współdzielenia, BuilderFromMeshData tworzy siatkę i bufor w jednym kroku.

  • Na koniec MeshEntity umieszcza siatkę w scenie.

    // Place the mesh in the scene graph
    val roadEntity = MeshEntity.create(
        session,
        mesh = road,
        // one material per subset
        materials = listOf(asphalt, paint),
        pose = Pose(Vector3(0f, 0f, -1f)),
        parent = session.scene.activitySpace)
    
    // It's an Entity: move it, re-parent it
    roadEntity.setPose(Pose(Vector3(0f, -0.5f, -1f)))
    roadEntity.parent = tableAnchor
    
    // Same mesh, another entity
    val ghost = MeshEntity.create(
        session, road, listOf(glass, glass))
  • Siatka to „co”.

  • Encja to „gdzie”.

  • Podobnie jak panel i samochód Jaya, jest to element, więc ma pozycję i element nadrzędny, do którego jest dołączony.

  • Przesuń element nadrzędny, a droga przesunie się razem z nim.

  • Możesz też podać 1 materiał na podzbiór, aby ta sama siatka mogła pojawiać się z różnymi materiałami w różnych elementach.

Tworzenie niestandardowych i dynamicznych siatek

  • Teraz połączmy wszystko i utwórzmy siatkę drogi.

    // Describe a vertex
    val layout = VertexLayout.Builder()
        .addAttribute(POSITION, FLOAT3)
        .addAttribute(NORMAL, FLOAT3)
        .addAttribute(UV0, FLOAT2)
        .build()
    
    // Path points -> ribbon of triangles
    val (vertices, indices) = buildRoad(path)
    
    // Build the mesh...
    val roadMesh = CustomMesh
        .BuilderFromMeshData(session, layout)
        .addVertexData(vertices)
        .setIndexData(indices)
        .setTopology(TRIANGLES)
        .build()
    
    // ...and put it in the scene
    val road = MeshEntity.create(
        session, roadMesh, listOf(asphalt),
        parent = session.scene.activitySpace)
  • Definiujemy wierzchołek: pozycję, normalną i UV, czyli współrzędne tekstury.

  • Mała funkcja pomocnicza zamienia ścieżkę użytkownika we wstążkę.

  • W przypadku każdego punktu na ścieżce dodaje 2 wierzchołki po każdej stronie i 2 trójkąty, które łączą je z następną parą.

  • To zwykły Kotlin, który zapisuje bajty w ByteBuffer.

  • Nie potrzebujesz żadnego narzędzia do tworzenia obiektów 3D.

  • BuilderFromMeshData, czyli skrót, który właśnie widzieliśmy, tworzy siatkę i MeshBuffer oraz oblicza granice.

  • Na koniec MeshEntity.create umieszcza roadMesh w scenie z asfaltem.

  • Chcemy jednak, aby ścieżka rosła na żywo, gdy użytkownik ją rysuje.

  • W tym celu używamy siatki dynamicznej.

    // Fixed GPU capacity, allocated once
    val track = CustomMesh.BuilderFromMeshData(
            session, layout,
            maxVertices = 2 * MAX_POINTS,
            maxIndices = 6 * MAX_POINTS)
        .setTopology(TRIANGLES)
        // Covers everything the track can become
        .setBounds(playArea)
        .build()
    
    // Hand moved: upload only the new segment
    val buffer = track.meshBuffer
    buffer.updateVertexData(
        bufferIndex = 0,
        vertexData = segmentVertices,
        offsetInBytes = n * VERTEX_BYTES)
    buffer.updateIndexData(
        segmentIndices,
        offsetInBytes = n * INDEX_BYTES)
  • Jedyna różnica polega na tym, że od razu podajemy konstruktorowi maksymalną pojemność, aby można było przydzielić bufory GPU.

  • Następnie, gdy użytkownik rysuje, updateVertexData i updateIndexData przesyłają tylko nowe segmenty, a nie całą siatkę.

  • Jedna uwaga: moduł renderowania używa granic, aby pomijać wszystko, co znajduje się poza ekranem, więc ustaw granice tak, aby obejmowały wszystko, co może stać się ścieżką, np. obszar gry.

  • Jak widzisz, interfejs API nie jest tu najtrudniejszą częścią.

  • Trudna część to matematyka wierzchołków, wektory normalne, współrzędne tekstury, kolejność trójkątów.

  • To dokładnie taki rodzaj kodu, który AI potrafi świetnie pisać.

  • Załóżmy jednak, że chcemy pójść o krok dalej.

  • Teraz chcę, żeby w miejscu docelowym powiewała na wietrze flaga w szachownicę.

Generowanie animowanych siatek za pomocą Gemini

  • Matematyka związana z animacją wierzchołków jest skomplikowana, więc poprosiłem o pomoc Gemini.

  • W Android Studio podałem 1 prompt: „Korzystając z interfejsu Jetpack SceneCore Mesh API, utwórz siatkę flagi 20 x 12 z teksturą UV w szachownicę, animuj falę wiatru w każdej klatce za pomocą updateVertexData i ponownie oblicz wektory normalne, aby oświetlenie podążało za fałdami”.

  • Po kilku sekundach otrzymałem działający kod.

  • Zobaczmy, co się stało.

  • Utworzył siatkę flagi jako siatkę 20 na 12 komórek, po 2 trójkąty na komórkę, i dokonał mądrego wyboru układu.

    // 20 x 12 cells = 273 vertices
    val layout = VertexLayout.Builder()
        // buffer 0: updated every frame
        .addAttribute(POSITION, FLOAT3)
        .addAttribute(NORMAL, FLOAT3)
        .startNextBuffer()
        // buffer 1: never changes
        .addAttribute(UV0, FLOAT2)
        .build()
    
    val flagMesh = CustomMesh.BuilderFromMeshData(
            session, layout,
            maxVertices = 273, maxIndices = 1440)
        .addVertexData(restPose)   // buffer 0
        .addVertexData(checkerUVs) // buffer 1
        .setIndexData(gridIndices)
        .setTopology(TRIANGLES)
        .setBounds(swingBounds)
        .build()
  • Pozycje i wektory normalne są umieszczane w buforze 0 i aktualizowane w każdej klatce.

  • Współrzędne tekstury są umieszczane w buforze 1 i nigdy się nie zmieniają.

  • Siatka jest dynamiczna, a granice są ustawione tak, aby obejmowały pełny ruch flagi.

  • W każdej klatce każdy wierzchołek otrzymuje falę sinusoidalną, która rośnie w kierunku wolnej krawędzi, dzięki czemu strona masztu pozostaje na miejscu.

    // Gemini wrote the math
    fun wave(t: Float) {
        for (i in 0 until 273) {
            val u = (i % 21) / 20f  // 0 at the pole
            setZ(i, 0.06f * u * sin(6f * u - 4f * t))
        }
        recomputeNormals()          // from neighbors
    }
    
    // Every frame: one upload (~6.5 KB)
    while (isActive) withFrameNanos { ns ->
        wave(ns / 1e9f)
        flagMesh.meshBuffer.updateVertexData(
            bufferIndex = 0,
            vertexData = ByteBufferRegion(
                posNormals, 0, 273 * 24))
    }
  • Wektory normalne są ponownie obliczane na podstawie sąsiednich pozycji, dzięki czemu fałdy odbijają światło.

  • updateVertexData przesyła do GPU około 6,5 KB danych na klatkę.

  • Nie odtwarzamy siatki ani elementu.

  • Teraz zobaczmy, jak to wygląda w całości.

  • Umieszczamy model samochodu na stole, a następnie rysujemy tor za pomocą ARCore.

  • Siatka flagi, którą właśnie utworzyliśmy, pojawia się w miejscu docelowym.

  • Samochód jedzie po torze, a my używamy interfejsu glTF Introspection API do sterowania i obracania kół samochodu.

  • Siatka jest wyświetlana z materiałem asfaltowym, ale możemy też użyć interfejsu Mesh API razem z interfejsem Material API, aby zmienić wygląd drogi, jak widać na filmie.

  • To naprawdę potężne narzędzie, które masz na wyciągnięcie ręki.

Podsumowanie i pierwsze kroki

  • Dzisiaj omówiliśmy wiele sposobów na przekształcenie aplikacji z 2D na przestrzenną.

  • Dzięki interfejsowi glTF Introspection API możesz teraz bezpośrednio korzystać z wewnętrznych metadanych modelu glTF, uruchamiać osadzone animacje, przekształcać poszczególne podwęzły, a nawet zamieniać materiały, aby zmieniać wygląd i styl modelu w czasie działania.

  • Następnie przekonaliśmy się, że gdy gotowe modele 3D nie wystarczają, możemy wykorzystać nasz nowy interfejs Mesh API do tworzenia niestandardowych kształtów.

  • Niezależnie od tego, czy chcesz tworzyć statyczne kształty od podstaw, czy przesyłać strumieniowo dynamiczne kształty i geometrię w czasie rzeczywistym, masz teraz do dyspozycji odpowiednie narzędzia.

  • Jak pokazał Egor, połączenie tych elementów z Gemini oznacza, że nie musisz nawet samodzielnie wykonywać obliczeń.

  • Powtórzę to, co powiedziałem wcześniej: wszystko, co widzieliście, działało w aplikacji na Androida, napisanej w idiomatycznym języku Kotlin, bezpośrednio w Android Studio.

  • Widzę, że wielu z Was ma dziś ze sobą laptopy. Mam nadzieję, że razem z Egorem zachęciliśmy Was do otwarcia Android Studio i uruchomienia emulatora Androida XR.

  • Puśćcie wodze fantazji i wykorzystajcie swoje dotychczasowe umiejętności związane z Androidem oraz to, co dziś pokazaliśmy, aby tworzyć niesamowite rozwiązania XR.

  • W imieniu Egora i swoim dziękuję za udział w dzisiejszej sesji. Życzę udanego Android Dev Summit.

Kod

Fragmenty kodu

Fragment kodu JSON w 01:22 w filmie „Praca z modelami 3D glTF”
{
    "asset" : {...},
    "nodes" : [
        {
            "name" : "Right_Arm",
            "rotation" : [...],
            "scale" : [...],
            "translation" : [...],
        },
    ],
    "animations" : [
        {
            "name" : "Wave",
        },
    ],
    "meshes" : [
        {
            "name" : "Torso",
            "primitives" : [ ... ],
        },
    ],

    // Way way more!

}
Fragment kodu w języku Kotlin w 02:28 w filmie „Praca z modelami 3D glTF”
// Load assets/models/car.glb
val carModel = GltfModel.create(
    session,
    Paths.get("models", "car.glb")
)

// Create an entity in the scene
val carEntity = GltfModelEntity.create(
    session,
    carModel,
    parent = session.scene.activitySpace
)
Fragment kodu w języku Kotlin w 02:50 w filmie „Animacje modeli i manipulowanie węzłami podrzędnymi”
// Access every animation in car.glb
val animations = carEntity.getAnimations()
   .associateBy { it.name }

// Open the doors...
animations["lf_door_open"]?.start()
animations["rt_door_open"]?.start()

// ...and close them again
animations["lf_door_close"]?.start()
animations["rt_door_close"]?.start()

// Same for the hood and trunk
animations["hood_open"]?.start()
animations["trunk_open"]?.start()
Fragment kodu w języku Kotlin w 03:12 w filmie „Animacje modeli i manipulowanie podwęzłami”
// Wheel sub nodes, named by our designer
val nodes = carEntity.nodes.associateBy { it.name }
val front = listOf("lf_frontWheel_geo",
   "rt_fontWheel_geo").map { nodes[it]!! }
val rear = listOf("lf_RearWheel_geo",
   "rt_RearWheel_geo").map { nodes[it]!! }

// Update a sub node's transform at runtime
fun GltfModelNode.rotate(q: Quaternion) {
   localPose = localPose.copy(rotation = q)
}

// Each frame: steer front, spin all four
fun drive(steerDeg: Float, spinDeg: Float) {
   val steer = Quaternion.fromAxisAngle(
       Vector3.Up, steerDeg)
   val spin = Quaternion.fromAxisAngle(
       Vector3.Right, spinDeg)
   front.forEach { it.rotate(steer * spin) }
   rear.forEach { it.rotate(spin) }
}
Fragment kodu w języku Kotlin w 03:51 w filmie „Stosowanie materiałów i tekstur”
// Load a PNG with the Texture API
val metal = Texture.create(
   session, Paths.get("textures", "metal.png"))

// Create KhronosPbrMaterials
val (body, doors, hood) = List(3) {
   KhronosPbrMaterial
       .create(session, AlphaMode.OPAQUE)
}
body.setBaseColorFactor(CYAN)
doors.setBaseColorFactor(PINK)
hood.setBaseColorTexture(metal)

// Override meshes by sub node name
val nodes = carEntity.nodes.associateBy { it.name }
nodes["mainBody_geo"]?.setMaterialOverride(body)
nodes["lf_door_geo"]?.setMaterialOverride(doors)
nodes["rt_door_geo"]?.setMaterialOverride(doors)
nodes["hood_geo"]?.setMaterialOverride(hood)
Fragment kodu w języku Kotlin w 05:05 w filmie „Wprowadzenie do interfejsów API siatki”
// 4 vertices: x, y, z
val positions = floatArrayOf(
    0f, 0f, 0f,   // v0
    1f, 0f, 0f,   // v1
    0f, 1f, 0f,   // v2
    1f, 1f, 0f    // v3
)

// 2 triangles = 6 indices
val indices = intArrayOf(
    0, 1, 2,
    2, 1, 3
)
Fragment kodu w Kotlinie w 05:45 w filmie „Building blocks of the mesh API”
// How to read each vertex from bytes
val layout = VertexLayout.Builder()
    .addAttribute(POSITION, FLOAT3) // 12 B
    .addAttribute(NORMAL, FLOAT3)   // 12 B
    .addAttribute(UV0, FLOAT2)      //  8 B
    .build()              // stride: 32 B

// Offsets and stride are computed,
// or set them explicitly.

// Attributes can live in separate buffers
val split = VertexLayout.Builder()
    .addAttribute(POSITION, FLOAT3)
    .startNextBuffer()
    .addAttribute(COLOR, UBYTE4_NORM)
    .build()
Fragment kodu w języku Kotlin w 06:10 w filmie „Elementy składowe interfejsu API siatki”
// Vertex bytes + 32-bit indices
val meshBuffer = MeshBuffer.create(
    session, layout,
    vertexData = listOf(ByteBufferRegion(
        vertexBytes, 0, vertexBytes.capacity())),
    indexData = ByteBufferRegion(
        indexBytes, 0, indexBytes.capacity()))

// Copied to the GPU: reuse your
// ByteBuffers right away

// Need updates at runtime?
val dynamic = MeshBuffer.createDynamic(
    session, layout,
    vertexCount = 1000, indexCount = 3000)
Fragment kodu w języku Kotlin w 06:37 w filmie „Elementy składowe interfejsu API siatki”
// A view: which indices to draw, and how
val road = CustomMesh
    .BuilderFromMeshBuffer(session, meshBuffer)
    // subset 0: asphalt
    .addSubset(TRIANGLES, indexOffset = 0,
        indexCount = roadCount)
    // subset 1: lane markings
    .addSubset(TRIANGLES,
        indexOffset = roadCount,
        indexCount = lineCount)
    .build()

// Shortcut: builds MeshBuffer + CustomMesh
CustomMesh.BuilderFromMeshData(session, layout)
Fragment kodu w języku Kotlin w 07:04 w filmie „Elementy składowe interfejsu API siatki”
// Place the mesh in the scene graph
val roadEntity = MeshEntity.create(
    session,
    mesh = road,
    // one material per subset
    materials = listOf(asphalt, paint),
    pose = Pose(Vector3(0f, 0f, -1f)),
    parent = session.scene.activitySpace)

// It's an Entity: move it, re-parent it
roadEntity.setPose(Pose(Vector3(0f, -0.5f, -1f)))
roadEntity.parent = tableAnchor

// Same mesh, another entity
val ghost = MeshEntity.create(
    session, road, listOf(glass, glass))
Fragment kodu w języku Kotlin w 07:31 w filmie „Tworzenie niestandardowych i dynamicznych siatek”
// Describe a vertex
val layout = VertexLayout.Builder()
    .addAttribute(POSITION, FLOAT3)
    .addAttribute(NORMAL, FLOAT3)
    .addAttribute(UV0, FLOAT2)
    .build()

// Path points -> ribbon of triangles
val (vertices, indices) = buildRoad(path)

// Build the mesh...
val roadMesh = CustomMesh
    .BuilderFromMeshData(session, layout)
    .addVertexData(vertices)
    .setIndexData(indices)
    .setTopology(TRIANGLES)
    .build()

// ...and put it in the scene
val road = MeshEntity.create(
    session, roadMesh, listOf(asphalt),
    parent = session.scene.activitySpace)
Fragment kodu w języku Kotlin w 08:18 w filmie „Tworzenie niestandardowych i dynamicznych siatek”
// Fixed GPU capacity, allocated once
val track = CustomMesh.BuilderFromMeshData(
        session, layout,
        maxVertices = 2 * MAX_POINTS,
        maxIndices = 6 * MAX_POINTS)
    .setTopology(TRIANGLES)
    // Covers everything the track can become
    .setBounds(playArea)
    .build()

// Hand moved: upload only the new segment
val buffer = track.meshBuffer
buffer.updateVertexData(
    bufferIndex = 0,
    vertexData = segmentVertices,
    offsetInBytes = n * VERTEX_BYTES)
buffer.updateIndexData(
    segmentIndices,
    offsetInBytes = n * INDEX_BYTES)
Fragment kodu w języku Kotlin w 09:34 w filmie „Generowanie animowanych siatek za pomocą Gemini”
// 20 x 12 cells = 273 vertices
val layout = VertexLayout.Builder()
    // buffer 0: updated every frame
    .addAttribute(POSITION, FLOAT3)
    .addAttribute(NORMAL, FLOAT3)
    .startNextBuffer()
    // buffer 1: never changes
    .addAttribute(UV0, FLOAT2)
    .build()

val flagMesh = CustomMesh.BuilderFromMeshData(
        session, layout,
        maxVertices = 273, maxIndices = 1440)
    .addVertexData(restPose)   // buffer 0
    .addVertexData(checkerUVs) // buffer 1
    .setIndexData(gridIndices)
    .setTopology(TRIANGLES)
    .setBounds(swingBounds)
    .build()
Fragment kodu w Kotlinie w 09:56 w filmie „Generowanie animowanych siatek za pomocą Gemini”
// Gemini wrote the math
fun wave(t: Float) {
    for (i in 0 until 273) {
        val u = (i % 21) / 20f  // 0 at the pole
        setZ(i, 0.06f * u * sin(6f * u - 4f * t))
    }
    recomputeNormals()          // from neighbors
}

// Every frame: one upload (~6.5 KB)
while (isActive) withFrameNanos { ns ->
    wave(ns / 1e9f)
    flagMesh.meshBuffer.updateVertexData(
        bufferIndex = 0,
        vertexData = ByteBufferRegion(
            posNormals, 0, 273 * 24))
}