La transizione delle app per Android dai tradizionali schermi 2D agli ambienti spaziali richiede un nuovo approccio alla creazione delle forme.
In questa sessione esploreremo l'API Custom Mesh nell'SDK Jetpack XR, che consente di generare in modo programmatico la geometria 3D procedurale direttamente in fase di runtime. Esamineremo il flusso di lavoro principale, dalla definizione dei layout dei vertici al rendering di oggetti dinamici senza interruzioni nello spazio fisico dell'utente.
Al termine di questo intervento, avrai acquisito le competenze pratiche per creare geometrie 3D interattive e personalizzate interamente a partire dal codice.
MeshBuffer creano modelli triangolo per triangolo senza strumenti 3D.Trascrizione
Ciao!
Sono Jay.
E io sono Egor.
Siamo software engineer che lavorano su Jetpack XR presso Google.
Con XREAL Aura alle porte, vogliamo assicurarci che tu abbia tutti gli strumenti necessari per creare esperienze spaziali eccezionali sulla piattaforma.
Ma soprattutto, non hai nemmeno bisogno di hardware XR per iniziare.
Tutte le funzionalità che mostreremo sono disponibili oggi nell'emulatore Android XR in Android Studio.
Il nostro obiettivo è dimostrarti che con la libreria Jetpack SceneCore non hai bisogno di conoscenze approfondite del 3D per creare esperienze spaziali complete sulla piattaforma.
Tutto ciò che vedrai viene eseguito in un'app per Android, scritta in Kotlin idiomatico direttamente in Android Studio.
Di recente abbiamo promosso Jetpack SceneCore alla versione stabile e oggi parleremo di alcuni interessanti progressi delle nostre API per i modelli 3D, nonché delle nostre nuovissime API per le mesh, che ti consentono di creare geometrie 3D personalizzate in tempo reale.
Ma iniziamo con i modelli 3D.
Per quanto riguarda i modelli 3D, la prima cosa di cui abbiamo bisogno è un formato file.
Gli standard aperti sono una parte fondamentale del DNA di Android XR.
Quindi, naturalmente, abbiamo scelto il formato glTF di Khronos Group.
Se non hai familiarità con il mondo del 3D, potresti chiederti: "Che cos'è un glTF?"
Puoi considerarlo come un JPEG, ma per i modelli 3D.
La struttura degli asset glTF fornisce dati per elementi come animazioni, nodi secondari e mesh.
{
"asset" : {...},
"nodes" : [
{
"name" : "Right_Arm",
"rotation" : [...],
"scale" : [...],
"translation" : [...],
},
],
"animations" : [
{
"name" : "Wave",
},
],
"meshes" : [
{
"name" : "Torso",
"primitives" : [ ... ],
},
],
// Way way more!
}Ma ai fini di questo discorso, puoi pensare ai nodi secondari come a parti di un modello, ciascuna con la propria posizione e rotazione.
Le mesh sono le superfici che vedi e le animazioni sono i movimenti di questi nodi secondari e mesh che l'autore dell'asset ha creato in precedenza.
Perfetto.
A questo punto, potresti chiederti: "Dove posso trovare un modello glTF?".
Oltre ai numerosi modelli disponibili, puoi anche crearne di personalizzati.
Ora che abbiamo trattato le nozioni di base del formato glTF, procediamo e utilizziamolo nella nostra app.
Iniziamo con un progetto Android XR di base utilizzando Jetpack SceneCore.
Puoi vedere un pannello spaziale con un'attività che fluttua nello spazio dell'utente.
Puoi selezionarlo, spostarlo e ridimensionarlo a tuo piacimento.
Anche se questo intervento riguarda il 3D, dai un'occhiata anche al resto dell'SDK Jetpack XR.
Ok, i pannelli spaziali sono divertenti, ma vogliamo più magia.
E quale modo migliore per aggiungere un po' di magia se non inserire un modello 3D?
Il nostro designer ha creato questo modello glTF di un'auto.
// 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
)Hanno anche incorporato delle animazioni, che utilizzeremo in seguito.
Utilizzando SceneCore, posso usare le nostre API dei modelli 3D per caricarlo direttamente nella nostra scena.
Ma rendiamo l'auto un po' più viva.
Ricordi tutti i metadati dettagliati all'interno di un file glTF?
Con una nuova API di introspezione glTF, ora puoi accedervi, a partire dalle animazioni.
// 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()Con poche righe di codice, posso riprodurre singolarmente qualsiasi animazione glTF integrata nel modello dell'auto, come l'apertura e la chiusura delle portiere, del cofano o del bagagliaio.
Ma le animazioni non sono l'unico tipo di metadati a cui puoi accedere.
L'API di introspezione glTF espone anche i sottonodi nel modello come GltfModelNode, consentendoti di spostarli, ruotarli e scalarli a tuo piacimento.
// 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) }
}Ad esempio, qui posso aggiornare singolarmente la posizione e la rotazione delle ruote mentre l'auto è in movimento.
Per chiarire, ciò che vedi non è un'animazione glTF incorporata.
In questo caso, sto facendo girare e sterzare le ruote singolarmente in fase di runtime.
Un ultimo metadato a cui puoi accedere è qualsiasi mesh nel modello glTF.
Egor approfondirà l'argomento delle mesh tra un attimo.
Per ora, però, personalizziamo la nostra auto.
Utilizzando la nostra API per le texture, possiamo caricare una texture di un'immagine PNG, ad esempio un motivo metallico.
// 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)Poi, con la nostra API Material, possiamo collegarlo direttamente a un KhronosPbrMaterial.
Se non hai dimestichezza con il 3D, puoi pensare a una mesh come a un tavolo, a un materiale come a una tovaglia e a una texture come a un motivo stampato su quella tovaglia.
In questo caso, però, anziché un piano d'appoggio, utilizziamo gli oggetti Material che abbiamo creato per eseguire l'override delle mesh associate ai nodi secondari del cofano e delle portiere dell'auto.
Con poche righe di codice, abbiamo trasformato l'aspetto della nostra auto, sempre in fase di runtime.
È un'idea fantastica, ma ecco un'idea ancora migliore.
Non sarebbe fantastico disegnare una pista su cui far correre l'auto?
In questo caso, un modello glTF predefinito non è la scelta giusta.
Abbiamo invece bisogno della capacità di generare la geometria 3D in tempo reale.
Quindi, lascio la parola a Egor per parlarvi delle nostre nuovissime API mesh Jetpack SceneCore.
Grazie, Jay.
Facciamo in modo che l'utente possa disegnare la pista con il dito nel suo salotto.
La pista non esiste finché non viene disegnata, quindi nessuno può modellarla in anticipo.
Lo creeremo nel codice, un triangolo alla volta.
Una mesh inizia con i vertici, ovvero i punti nello spazio.
// 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
)Ognuno di questi contiene attributi come posizione, normale per l'illuminazione, colore e coordinate della texture.
Gli indici collegano i punti.
Ogni tre formano un triangolo.
Due triangoli formano un quadrato, sei indici, ma solo quattro vertici, perché ne condividono due.
Continua così e otterrai un cubo.
Ogni modello 3D, inclusa l'auto di Jay, è costruito allo stesso modo, solo con più triangoli.
L'API Mesh esegue la mappatura con quattro componenti di base: VertexLayout, MeshBuffer, CustomMesh e MeshEntity.
Vediamoli uno alla volta.
Innanzitutto, 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()I dati dei vertici sono solo byte in un ByteBuffer e ogni mesh li raggruppa in modo diverso.
VertexLayout indica come leggere i tuoi: quali attributi, di che tipo e dove si trova ciascuno.
In questo caso, si tratta di una posizione, una normale e UV, le coordinate della texture, per un totale di 32 byte.
Gli attributi possono anche risiedere in buffer separati, in modo da poter aggiornare solo quelli che cambiano.
In secondo luogo, MeshBuffer, un container per i dati non elaborati.
// 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)Fornisci il layout, i byte dei vertici e gli indici a 32 bit.
I dati vengono copiati immediatamente nella GPU, in modo da poter riutilizzare o rilasciare i ByteBuffer.
Per impostazione predefinita, MeshBuffer è statico.
Viene impostato una volta e ottimizzato per un rendering rapido.
Se devi modificarla in fase di runtime, createDynamic alloca una capacità fissa che puoi aggiornare in un secondo momento.
Terzo, CustomMesh, una vista di un 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)Non copia alcun dato.
Indica quale intervallo di indici disegnare e come, triangoli o strisce di triangoli.
In questo caso, l'asfalto e la segnaletica orizzontale si trovano nello stesso MeshBuffer.
Più mesh personalizzate possono condividere lo stesso MeshBuffer e, quando non è necessaria la condivisione, BuilderFromMeshData crea la mesh e il buffer in un unico passaggio.
Infine, MeshEntity posiziona la mesh nella scena.
// 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))Il mesh è il "cosa".
L'entità è il luogo.
Come il pannello e l'auto di Jay, è un'entità, quindi ha una posizione e un elemento principale, l'oggetto a cui è collegata.
Sposta l'elemento principale e la strada si sposterà di conseguenza.
Inoltre, fornisci un materiale per sottoinsieme, in modo che la stessa mesh possa apparire con materiali diversi in entità diverse.
Ora mettiamo tutto insieme e creiamo la mesh, la mesh della strada.
// 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)Definiamo il vertice: la posizione, la normale e gli UV, le coordinate della texture.
Una piccola funzione helper trasforma il percorso dell'utente in un nastro.
Per ogni punto del percorso, aggiunge due vertici su ciascun lato e due triangoli che li collegano alla coppia successiva.
È un semplice Kotlin che scrive byte in un ByteBuffer.
Non è necessario alcuno strumento 3D.
BuilderFromMeshData, la scorciatoia che abbiamo appena visto, crea la mesh e l'MeshBuffer e calcola i limiti per noi.
Infine, MeshEntity.create posiziona roadMesh nella scena con il materiale asfalto.
Ma vogliamo che la nostra traccia cresca in tempo reale mentre l'utente la disegna.
Per questo, utilizziamo una mesh dinamica.
// 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)L'unica differenza è che forniamo al builder la capacità massima in anticipo, in modo che i buffer della GPU possano essere allocati.
Poi, mentre l'utente disegna, updateVertexData e updateIndexData caricano solo i nuovi segmenti, non l'intera mesh.
Un piccolo problema: il renderer utilizza i limiti per saltare tutto ciò che è fuori dallo schermo, quindi imposta i limiti in modo che coprano tutto ciò che la traccia potrebbe diventare, come l'area di gioco qui.
Come puoi notare, l'API non è la parte difficile.
La parte difficile è la matematica dei vertici, le normali, le coordinate delle texture, l'ordine dei triangoli.
Questo è esattamente il tipo di codice che l'AI è in grado di scrivere.
Ma supponiamo di voler fare un ulteriore passo avanti.
Ora voglio una bandiera a scacchi che sventola al vento nella destinazione.
La matematica dell'animazione dei vertici è la parte difficile, quindi ho chiesto a Gemini.
In Android Studio, ho fornito un solo prompt: "Utilizzando l'API Jetpack SceneCore Mesh, crea una griglia di bandiere 20x12 con UV a scacchiera, anima un'onda di vento in ogni frame con updateVertexData e ricalcola le normali in modo che l'illuminazione segua le pieghe".
Pochi secondi dopo, ho ottenuto un codice funzionante.
Vediamo cosa ha fatto.
Ha creato una mesh della bandiera come griglia di 20 x 12 celle, due triangoli per cella, e ha fatto una scelta intelligente con il layout.
// 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()Le posizioni e le normali sono inserite nel Buffer 0 e vengono aggiornate a ogni frame.
Le coordinate delle texture vengono inserite nel Buffer 1 e non cambiano mai.
La mesh è dinamica e i limiti sono impostati per coprire l'intera oscillazione della bandiera.
In ogni frame, ogni vertice riceve un'onda sinusoidale che cresce verso il bordo libero, in modo che il lato del palo rimanga fermo.
// 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))
}Le normali vengono ricalcolate dalle posizioni vicine, il che fa sì che le pieghe catturino la luce.
updateVertexData carica circa 6,5 kB di dati sulla GPU per ogni frame.
Non ricreiamo il mesh o l'entità.
Ora vediamo tutto insieme.
Posizioniamo il modello di auto sul tavolo, poi utilizziamo ARCore per disegnare la pista.
La mesh della bandiera che abbiamo appena creato appare nella destinazione.
L'auto si muove lungo la pista e utilizziamo l'API glTF Introspection per sterzare e far girare le ruote dell'auto.
La mesh viene visualizzata con il materiale asfalto, ma possiamo anche utilizzare l'API Mesh insieme all'API Material per modificare l'aspetto della strada, come puoi vedere nel video.
È una funzionalità davvero potente a portata di mano.
Oggi abbiamo parlato di molti modi per trasformare le tue app da 2D a spaziali.
Con l'API glTF Introspection, ora puoi accedere direttamente ai metadati interni di un modello glTF, attivando animazioni incorporate, trasformando singoli nodi secondari e persino scambiando materiali per alterarne l'aspetto, il tutto in fase di runtime.
Poi, abbiamo visto che quando i modelli 3D predefiniti non erano sufficienti, potevamo sfruttare la nostra nuovissima API Mesh per creare una geometria personalizzata.
Che tu debba creare forme statiche da zero o trasmettere in streaming forme dinamiche e geometria dinamica in tempo reale, ora hai gli strumenti per farlo.
E come ha appena mostrato Egor, l'abbinamento di questi componenti di base con Gemini significa che non devi nemmeno fare i calcoli da solo.
Ora, ripeto una cosa che ho detto prima: tutto ciò che hai visto era in esecuzione in un'app per Android, scritta in Kotlin idiomatico, direttamente in Android Studio.
Oggi vedo molti laptop tra il pubblico e spero che io ed Egor vi abbiamo entusiasmato abbastanza da aprire Android Studio e avviare l'emulatore Android XR oggi stesso.
Dai libero sfogo alla tua immaginazione e utilizza le tue competenze Android esistenti, insieme a ciò che abbiamo mostrato oggi, per creare esperienze XR straordinarie.
A nome di Egor e mio, grazie mille per aver partecipato oggi e buon proseguimento con l'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))
}