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.
MeshBuffer membuat model segitiga demi segitiga tanpa alat 3D.Transkrip
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.
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.
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.
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.
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.
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.
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.
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.
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.
{
"asset" : {...},
"nodes" : [
{
"name" : "Right_Arm",
"rotation" : [...],
"scale" : [...],
"translation" : [...],
},
],
"animations" : [
{
"name" : "Wave",
},
],
"meshes" : [
{
"name" : "Torso",
"primitives" : [ ... ],
},
],
// Way way more!
}// 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
)// 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()// 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) }
}// 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)// 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
)// 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()// 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)// 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)// 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))// 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)// 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)// 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()// 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))
}