Android XR

Melampaui 2D dengan geometri 3D kustom di Jetpack XR SDK

Speaker

Tentang

Tentang konferensi

Mentransisikan aplikasi Android Anda dari layar 2D konvensional ke lingkungan spasial memerlukan pendekatan baru untuk membuat bentuk.

Dalam sesi ini, kita akan mempelajari Custom Mesh API di Jetpack XR SDK, yang memungkinkan Anda membuat geometri 3D prosedural secara terprogram langsung saat runtime. Kita akan membahas alur kerja inti, mulai dari menentukan tata letak vertex hingga merender objek dinamis dengan lancar dalam ruang fisik pengguna.

Di akhir presentasi ini, Anda akan memiliki keterampilan praktis untuk membangun geometri 3D kustom yang interaktif sepenuhnya dari kode.

Ringkasan

  • API introspeksi glTF di Jetpack SceneCore mengekspos metadata model saat runtime. Anda dapat memutar animasi tersemat, mengubah subnode, atau mengganti mesh dengan material dan tekstur.
  • Jetpack SceneCore menyertakan mesh API untuk membuat geometri 3D kustom di Kotlin. Empat elemen penyusun seperti VertexLayout dan MeshBuffer membuat model segitiga demi segitiga tanpa alat 3D.
  • Mesh dinamis mendukung update real-time saat pengguna menggambar jalur atau menganimasikan permukaan. Memperbarui hanya data vertex yang diubah akan mengalirkan modifikasi langsung ke GPU tanpa membuat ulang mesh atau entity.
  • Gemini di Android Studio dapat menulis animasi vertex dan matematika pencahayaan yang kompleks untuk API mesh. Satu perintah menghasilkan kode yang berfungsi untuk menganimasikan petak verteks dan menghitung ulang normal setiap frame.

Resource

Gemini Notebook

Topik

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

Transkrip

Pengantar Android XR dan SceneCore

  • Halo semuanya.

  • Saya Jay.

  • Dan saya Egor.

  • Kami adalah software engineer yang mengerjakan Jetpack XR di Google.

  • Dengan XREAL Aura yang akan segera hadir, kami ingin memastikan Anda memiliki semua alat yang diperlukan untuk membangun pengalaman spasial yang luar biasa di platform ini.

  • Namun, yang terbaik adalah Anda bahkan tidak memerlukan hardware XR untuk memulai.

  • Semua fitur yang akan kami tunjukkan tersedia saat ini di Emulator Android XR di Android Studio.

  • Tujuan kami adalah menunjukkan kepada Anda bahwa dengan library Jetpack SceneCore, Anda tidak memerlukan pengetahuan 3D yang mendalam untuk membangun pengalaman spasial yang kaya di platform.

  • Semua yang akan Anda lihat berjalan di aplikasi Android, yang ditulis dalam Kotlin idiomatis langsung di dalam Android Studio.

  • Kami baru-baru ini mempromosikan Jetpack SceneCore ke versi stabil, dan hari ini kami akan membahas beberapa peningkatan keren pada API model 3D kami, serta API mesh baru kami, yang memungkinkan Anda membuat geometri 3D kustom dengan cepat.

Menggunakan model 3D glTF

  • Namun, mari kita mulai dengan model 3D.

  • Terkait model 3D, hal pertama yang kita perlukan adalah format file.

  • Standar terbuka adalah bagian inti dari DNA Android XR.

  • Jadi, tentu saja, kami menggunakan format glTF dari Khronos Group.

  • Jika Anda tidak berasal dari dunia 3D, Anda mungkin bertanya, "Apa itu glTF?"

  • Anda dapat menganggapnya sebagai JPEG, tetapi untuk model 3D.

  • Struktur aset glTF menyediakan data untuk hal-hal seperti animasi, subnode, dan mesh.

    {
        "asset" : {...},
        "nodes" : [
            {
                "name" : "Right_Arm",
                "rotation" : [...],
                "scale" : [...],
                "translation" : [...],
            },
        ],
        "animations" : [
            {
                "name" : "Wave",
            },
        ],
        "meshes" : [
            {
                "name" : "Torso",
                "primitives" : [ ... ],
            },
        ],
    
        // Way way more!
    
    }
  • Namun, untuk tujuan pembahasan ini, Anda dapat menganggap subnode sebagai bagian dari model, yang masing-masing memiliki posisi dan rotasinya sendiri.

  • Mesh adalah permukaan yang Anda lihat, dan animasi adalah gerakan subnode dan mesh yang telah dipanggang oleh pembuat aset sebelumnya.

  • Keren!

  • Sekarang, pada tahap ini, Anda mungkin bertanya, "Di mana saya bisa mendapatkan model glTF?"

  • Selain banyak model yang ada, Anda juga dapat membuat model sendiri.

  • Setelah membahas dasar-dasar format glTF, mari kita gunakan format ini di aplikasi kita.

  • Kita akan memulai dengan project Android XR dasar menggunakan Jetpack SceneCore.

  • Anda dapat melihat panel spasial dengan aktivitas yang mengambang di ruang pengguna.

  • Anda dapat mengambil, memindahkan, dan mengubah ukurannya sesuai keinginan.

  • Meskipun pembahasan ini sepenuhnya tentang 3D, pastikan untuk melihat juga bagian lain dari Jetpack XR SDK.

  • Baiklah, panel spasial memang menyenangkan, tetapi kita ingin lebih banyak keajaiban.

  • Dan tidak ada cara yang lebih baik untuk menambahkan sentuhan ajaib selain memasukkan model 3D.

  • Kebetulan, desainer kami membuat model glTF mobil ini.

    // 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
    )
  • Mereka bahkan menyematkan animasi di dalamnya, yang akan kita gunakan nanti.

  • Dengan SceneCore, saya dapat menggunakan API model 3D kami untuk memuatnya langsung ke scene.

Animasi model dan manipulasi subnode

  • Namun, mari kita buat mobilnya terasa lebih hidup.

  • Ingat semua metadata lengkap di dalam glTF?

  • Dengan API introspeksi glTF baru, Anda kini dapat mengaksesnya, dimulai dengan animasi.

    // 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()
  • Hanya dengan beberapa baris kode, saya dapat memutar setiap animasi glTF yang disertakan dalam model mobil, seperti membuka dan menutup pintu, kap mesin, atau bagasi.

  • Namun, animasi bukanlah satu-satunya bagian metadata yang dapat Anda akses.

  • API introspeksi glTF juga mengekspos subnode dalam model sebagai GltfModelNode, sehingga Anda dapat memindahkan, memutar, dan menskalakannya sesuai keinginan.

    // 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) }
    }
  • Misalnya, di sini, saya dapat memperbarui posisi dan rotasi roda satu per satu saat mobil melaju.

  • Dan agar jelas, yang Anda lihat bukanlah animasi glTF yang disematkan.

  • Di sini, saya memutar dan mengarahkan roda satu per satu, semuanya saat runtime.

Menerapkan material dan tekstur

  • Satu bagian terakhir metadata yang dapat Anda akses adalah mesh apa pun dalam model glTF.

  • Egor akan membahas mesh lebih mendalam sebentar lagi.

  • Namun, untuk saat ini, mari kita tata mobil kita.

  • Dengan menggunakan API tekstur, kita dapat memuat tekstur gambar PNG, misalnya pola logam.

    // 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)
  • Kemudian, dengan API material, kita dapat memasukkannya langsung ke KhronosPbrMaterial.

  • Jika Anda baru mengenal 3D, Anda dapat menganggap mesh sebagai permukaan meja, material sebagai taplak meja, dan tekstur sebagai pola yang dicetak pada taplak meja tersebut.

  • Namun di sini, alih-alih permukaan meja, kita menggunakan objek material yang kita buat untuk mengganti mesh yang terkait dengan subnode kap dan pintu mobil.

  • Hanya dengan beberapa baris kode, kita telah mengubah tampilan dan nuansa mobil, sekali lagi, semuanya saat runtime.

  • Itu ide yang keren, tapi ini ide yang lebih baik.

  • Betapa serunya jika kita bisa menggambar trek balap untuk mobil?

  • Di sini, model glTF siap pakai tidak cocok.

  • Sebagai gantinya, kita memerlukan kemampuan untuk membuat geometri 3D dengan cepat.

  • Jadi, saya akan menyerahkan kepada Egor untuk memberi tahu Anda semua tentang API mesh Jetpack SceneCore baru kami.

Pengantar API mesh

  • Terima kasih, Jay.

  • Mari kita buat agar pengguna dapat menggambar trek balap di-- dengan jari mereka di ruang keluarga.

  • Trek tidak ada sampai digambar, jadi tidak ada yang bisa memodelkannya terlebih dahulu.

  • Kita akan membuatnya dalam kode, satu segitiga pada satu waktu.

  • Mesh dimulai dengan verteks, titik dalam ruang.

    // 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
    )
  • Setiap verteks memiliki atribut seperti posisi, normal untuk pencahayaan, warna, dan koordinat tekstur.

  • Indeks menghubungkan berbagai informasi.

  • Setiap tiga titik membentuk segitiga.

  • Dua segitiga membentuk persegi, enam indeks, tetapi hanya empat verteks, karena keduanya berbagi dua verteks.

  • Lanjutkan dan Anda akan mendapatkan kubus.

  • Setiap model 3D, termasuk mobil Jay, dibuat dengan cara yang sama, hanya dengan lebih banyak segitiga.

Elemen penyusun mesh API

  • Mesh API dipetakan ke dalam empat elemen penyusun: VertexLayout, MeshBuffer, CustomMesh, dan MeshEntity.

  • Mari kita bahas satu per satu.

  • Pertama, 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()
  • Data verteks Anda hanyalah byte dalam ByteBuffer, dan setiap mesh mengemasnya secara berbeda.

  • VertexLayout memberi tahu cara membaca vertex Anda: atribut apa, jenis apa, dan di mana masing-masing berada.

  • Di sini, ada posisi, normal, dan UV, koordinat tekstur, total 32 byte.

  • Atribut juga dapat berada di buffer terpisah, sehingga Anda hanya dapat memperbarui atribut yang berubah.

  • Kedua, MeshBuffer, penampung untuk data mentah.

    // 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)
  • Anda memberinya tata letak, byte verteks, dan indeks 32-bit.

  • Data akan segera disalin ke GPU, sehingga Anda dapat menggunakan kembali atau merilis ByteBuffer.

  • Secara default, MeshBuffer bersifat statis.

  • Warna ini ditetapkan sekali dan dioptimalkan untuk rendering cepat.

  • Dan jika Anda perlu mengubahnya saat runtime, createDynamic akan mengalokasikan kapasitas tetap yang dapat Anda perbarui nanti.

  • Ketiga, CustomMesh, tampilan ke dalam 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)
  • Tindakan ini tidak menyalin data apa pun.

  • Fungsi ini menentukan rentang indeks yang akan digambar dan caranya, yaitu segitiga atau strip segitiga.

  • Di sini, aspal dan marka jalur berada di MeshBuffer yang sama.

  • Beberapa mesh kustom dapat menggunakan MeshBuffer yang sama, dan jika Anda tidak perlu berbagi, BuilderFromMeshData akan membuat mesh dan buffer dalam satu langkah.

  • Terakhir, MeshEntity menempatkan mesh di scene.

    // 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))
  • Mesh adalah "apa".

  • Entity adalah tempatnya.

  • Seperti panel dan mobil Jay, orbiter adalah entity, sehingga memiliki posisi dan induk, yaitu objek yang dikaitkan dengannya.

  • Pindahkan induk, dan jalan akan ikut berpindah.

  • Anda juga menyediakan satu material per subset, sehingga mesh yang sama dapat muncul dengan material yang berbeda di entity yang berbeda.

Membuat mesh kustom dan dinamis

  • Sekarang, mari kita gabungkan semuanya dan buat mesh, mesh jalan.

    // 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)
  • Kita menentukan verteks: posisi, normal, dan UV, koordinat tekstur.

  • Fungsi bantuan kecil mengubah jalur pengguna menjadi pita.

  • Untuk setiap titik di jalur, fungsi ini menambahkan dua verteks di setiap sisi dan dua segitiga yang menghubungkannya ke pasangan berikutnya.

  • Ini adalah Kotlin biasa, yang menulis byte ke dalam ByteBuffer.

  • Anda tidak memerlukan alat 3D apa pun.

  • BuilderFromMeshData, pintasan yang baru saja kita lihat, membuat mesh dan MeshBuffer, serta menghitung batas untuk kita.

  • Terakhir, MeshEntity.create menempatkan roadMesh di scene dengan material aspal.

  • Namun, kita ingin jalur kita bertambah secara live saat pengguna menggambarnya.

  • Untuk itu, kita menggunakan mesh dinamis.

    // 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)
  • Satu-satunya perbedaan adalah kita memberikan kapasitas maksimum di awal kepada builder, sehingga buffer GPU dapat dialokasikan.

  • Kemudian, saat pengguna menggambar, updateVertexData dan updateIndexData hanya mengupload segmen baru, bukan seluruh mesh.

  • Satu hal yang perlu diperhatikan: perender menggunakan batas untuk melewati apa pun yang berada di luar layar, jadi tetapkan batas untuk mencakup semua yang mungkin menjadi trek, seperti area bermain di sini.

  • Anda mungkin menyadari bahwa API bukanlah bagian yang sulit di sini.

  • Bagian yang sulit adalah matematika verteks, normal, koordinat tekstur, urutan segitiga.

  • Itulah jenis kode yang sangat baik ditulis oleh AI.

  • Namun, anggaplah kita ingin melangkah lebih jauh.

  • Sekarang, saya ingin bendera kotak-kotak berkibar tertiup angin di tempat tujuan.

Membuat mesh animasi dengan Gemini

  • Matematika animasi vertex adalah bagian yang rumit, jadi saya minta Gemini.

  • Di Android Studio, saya memberikan satu perintah: "Menggunakan Jetpack SceneCore Mesh API, buat petak bendera 20x12 dengan UV papan catur, animasikan gelombang angin di setiap frame dengan updateVertexData, dan hitung ulang normal sehingga pencahayaan mengikuti lipatan."

  • Beberapa detik kemudian, saya mendapatkan kode yang berfungsi.

  • Mari kita lihat hasilnya.

  • Ia membuat mesh bendera sebagai petak sel berukuran 20 x 12, dua segitiga per sel, dan membuat pilihan cerdas dengan tata letaknya.

    // 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()
  • Posisi dan normal ditempatkan di Buffer 0, dan diperbarui setiap frame.

  • Koordinat tekstur ditempatkan di Buffer 1, dan tidak pernah berubah.

  • Mesh bersifat dinamis, dan batasnya ditetapkan untuk mencakup ayunan penuh bendera.

  • Setiap frame, setiap verteks mendapatkan gelombang sinus yang tumbuh ke arah tepi bebas, sehingga sisi tiang tetap diam.

    // 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))
    }
  • Normal dihitung ulang dari posisi di sekitarnya, sehingga lipatan dapat menangkap cahaya.

  • updateVertexData mengupload sekitar 6,5 KB data ke GPU setiap frame.

  • Kami tidak membuat ulang mesh atau entity.

  • Sekarang mari kita lihat semuanya bersama-sama.

  • Kita menempatkan model mobil di atas meja, lalu menggunakan ARCore untuk menggambar trek.

  • Mesh bendera yang baru saja kita-- mesh bendera yang baru saja kita buat muncul di tujuan.

  • Mobil melaju di sepanjang trek, dan kami menggunakan glTF Introspection API untuk mengemudikan dan memutar roda mobil.

  • Mesh muncul dengan material aspal, tetapi kita juga dapat menggunakan mesh API bersama dengan material API untuk mengubah tampilan dan nuansa jalan, seperti yang dapat Anda lihat di video.

  • Itu adalah hal yang luar biasa yang bisa Anda lakukan dengan mudah.

Ringkasan dan cara memulai

  • Hari ini, kita telah membahas banyak cara untuk mengubah aplikasi Anda dari 2D menjadi spasial.

  • Dengan glTF Introspection API, Anda kini dapat langsung memanfaatkan metadata internal model glTF, memicu animasi tersemat, mengubah subnode individual, dan bahkan menukar materi untuk mengubah tampilan dan nuansanya, semuanya saat runtime.

  • Kemudian, kita melihat bahwa jika model 3D siap pakai tidak cukup, kita dapat memanfaatkan Mesh API baru untuk membuat geometri kustom.

  • Baik Anda perlu membuat bentuk statis dari awal atau men-streaming geometri dinamis bentuk dinamis secara real time, kini Anda memiliki alat untuk melakukannya.

  • Seperti yang baru saja ditunjukkan Egor, memasangkan elemen penyusun tersebut dengan Gemini berarti Anda bahkan tidak perlu melakukan penghitungan sendiri.

  • Sekarang, saya akan mengulangi hal yang saya katakan sebelumnya: semua yang Anda lihat berjalan di aplikasi Android, yang ditulis dalam Kotlin idiomatis, langsung di dalam Android Studio.

  • Saya melihat banyak laptop di antara audiens hari ini, dan saya harap Egor dan saya telah membuat Anda cukup bersemangat untuk membuka Android Studio dan menjalankan Emulator Android XR hari ini.

  • Biarkan imajinasi Anda berkembang, dan gunakan keterampilan Android yang sudah ada, bersama dengan apa yang kami tunjukkan hari ini, untuk membangun pengalaman XR yang luar biasa.

  • Atas nama Egor dan saya sendiri, terima kasih banyak telah bergabung bersama kami hari ini, dan selamat menikmati sisa Android Dev Summit.

Kode

Cuplikan kode

Cuplikan JSON pada menit 01:22 di "Bekerja dengan model 3D glTF"
{
    "asset" : {...},
    "nodes" : [
        {
            "name" : "Right_Arm",
            "rotation" : [...],
            "scale" : [...],
            "translation" : [...],
        },
    ],
    "animations" : [
        {
            "name" : "Wave",
        },
    ],
    "meshes" : [
        {
            "name" : "Torso",
            "primitives" : [ ... ],
        },
    ],

    // Way way more!

}
Cuplikan Kotlin pada menit 02:28 di "Bekerja dengan model 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
)
Cuplikan Kotlin pada 02:50 di "Model animations and subnode manipulation"
// 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()
Cuplikan Kotlin pada 03:12 di "Model animations and subnode manipulation"
// 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) }
}
Cuplikan Kotlin pada 03:51 di "Menerapkan material dan 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)
Cuplikan Kotlin pada 05:05 di "Pengantar mesh API"
// 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
)
Cuplikan Kotlin pada 05:45 di "Elemen penyusun 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()
Cuplikan Kotlin pada menit 06:10 di "Elemen penyusun mesh API"
// 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)
Cuplikan Kotlin pada 06:37 di "Elemen penyusun mesh API"
// 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)
Cuplikan Kotlin pada menit 07.04 di "Elemen penyusun mesh API"
// 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))
Cuplikan Kotlin pada 07:31 di "Membuat mesh kustom dan dinamis"
// 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)
Cuplikan Kotlin pada 08:18 di "Membuat mesh kustom dan dinamis"
// 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)
Cuplikan Kotlin pada 09:34 di "Membuat mesh animasi dengan 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()
Cuplikan Kotlin pada 09:56 di "Membuat mesh animasi dengan 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))
}