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.
MeshBuffer, tworzą modele trójkąt po trójkącie bez użycia narzędzi 3D.Transkrypcja
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.
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.
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.
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.
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.
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.
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ę.
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.
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.
{
"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))
}