La transición de tus aplicaciones para Android de las pantallas 2D tradicionales a los entornos espaciales requiere un nuevo enfoque para construir formas.
En esta sesión, exploraremos la API de Custom Mesh en el SDK de Jetpack XR, que te permite generar de forma programática geometría 3D procedimental directamente en el entorno de ejecución. Analizaremos el flujo de trabajo principal, desde la definición de los diseños de vértices hasta la renderización de objetos dinámicos sin problemas dentro del espacio físico del usuario.
Al final de esta charla, tendrás las habilidades prácticas para crear geometría 3D interactiva y personalizada completamente a partir de código.
MeshBuffer, construyen modelos triángulo por triángulo sin herramientas 3D.Transcripción
Hola.
Soy Jay.
Y yo soy Egor.
Somos ingenieros de software que trabajan en Jetpack XR en Google.
Con XREAL Aura a la vuelta de la esquina, queremos asegurarnos de que tengas todas las herramientas que necesitas para crear excelentes experiencias espaciales en la plataforma.
Pero lo mejor de todo es que ni siquiera necesitas hardware de RE para comenzar.
Todas las funciones que mostraremos están disponibles hoy en el emulador de Android XR en Android Studio.
Nuestro objetivo es mostrarte que, con la biblioteca de Jetpack SceneCore, no necesitas conocimientos profundos de 3D para crear experiencias espaciales enriquecidas en la plataforma.
Todo lo que verás se ejecuta en una app para Android, escrita en Kotlin idiomático directamente en Android Studio.
Hace poco, promovimos Jetpack SceneCore a la versión estable y, hoy, hablaremos de algunos avances interesantes de nuestras APIs de modelos 3D, así como de nuestras nuevas APIs de mallas, que te permiten compilar geometría 3D personalizada sobre la marcha.
Pero comencemos con los modelos en 3D.
En lo que respecta a los modelos 3D, lo primero que necesitamos es un formato de archivo.
Los estándares abiertos son una parte fundamental del ADN de Android XR.
Por lo tanto, naturalmente, apostamos por el formato glTF de Khronos Group.
Ahora bien, si no eres del mundo del 3D, tal vez te preguntes qué es un glTF.
Puedes pensar en él como un JPEG, pero para modelos 3D.
La estructura de recursos de glTF proporciona datos para elementos como animaciones, subnodos y mallas.
{
"asset" : {...},
"nodes" : [
{
"name" : "Right_Arm",
"rotation" : [...],
"scale" : [...],
"translation" : [...],
},
],
"animations" : [
{
"name" : "Wave",
},
],
"meshes" : [
{
"name" : "Torso",
"primitives" : [ ... ],
},
],
// Way way more!
}Pero, para los fines de esta charla, puedes pensar en los subnodos como partes de un modelo, cada uno con su propia posición y rotación.
Las mallas son las superficies que ves, y las animaciones son movimientos de esos subnodos y mallas que el autor del recurso integró con anticipación.
Genial.
En este punto, tal vez te preguntes: "¿Dónde puedo obtener un modelo glTF?".
Además de los muchos modelos que existen, también puedes crear los tuyos.
Ahora que ya vimos los conceptos básicos del formato glTF, vamos a usarlo en nuestra app.
Comenzaremos con un proyecto básico de Android XR que usa Jetpack SceneCore.
Puedes ver un panel espacial con una actividad que flota en el espacio del usuario.
Puedes tomarla, moverla y cambiar su tamaño como quieras.
Y, aunque esta charla se trata de 3D, no dejes de consultar el resto del SDK de Jetpack XR.
Muy bien, los paneles espaciales son divertidos, pero queremos más magia.
¿Y qué mejor manera de agregar un poco de magia que incluir un modelo en 3D?
Resulta que nuestro diseñador creó este modelo glTF de 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
)Incluso incorporaron animaciones, que usaremos más adelante.
Con SceneCore, puedo usar nuestras APIs de modelos 3D para cargarlo directamente en nuestra escena.
Pero hagamos que el auto se sienta un poco más vivo.
¿Recuerdas todos esos metadatos enriquecidos dentro de un glTF?
Con una nueva API de introspección de glTF, ahora puedes acceder a ella, comenzando con las animaciones.
// 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 solo unas pocas líneas de código, puedo reproducir de forma individual cualquier animación de glTF integrada en el modelo de automóvil, como abrir y cerrar las puertas, el capó o el maletero.
Sin embargo, las animaciones no son el único fragmento de metadatos al que puedes acceder.
La API de introspección de glTF también expone subnodos en el modelo como GltfModelNode, lo que te permite moverlos, rotarlos y escalarlos como quieras.
// 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) }
}Por ejemplo, aquí puedo actualizar de forma individual la posición y la rotación de las ruedas mientras el auto avanza.
Para que quede claro, lo que estás viendo no es una animación glTF incorporada.
Aquí, hago girar y dirijo las ruedas de forma individual, todo en tiempo de ejecución.
Un último dato de metadatos al que puedes acceder es cualquier malla del modelo glTF.
Egor profundizará en las mallas en un momento.
Pero, por ahora, vamos a darle estilo a nuestro auto.
Con nuestra API de texturas, podemos cargar una textura de imagen PNG, por ejemplo, de un patrón de metal.
// 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)Luego, con nuestra API de Material, podemos conectarla directamente a un KhronosPbrMaterial.
Si no tienes experiencia con el 3D, puedes pensar en una malla como una mesa, en un material como un mantel y en una textura como un patrón impreso en ese mantel.
Sin embargo, aquí, en lugar de una mesa, usamos los objetos de material que creamos para anular las mallas asociadas con los subnodos del capó y las puertas del auto.
Con solo unas pocas líneas de código, transformamos el aspecto de nuestro auto, de nuevo, todo en el tiempo de ejecución.
Eso es genial, pero aquí hay una idea aún mejor.
¿No sería genial dibujar una pista de carreras para que el auto la recorra?
En este caso, un modelo glTF prediseñado no es la opción adecuada.
En cambio, necesitamos la capacidad de generar geometría 3D sobre la marcha.
Así que le daré la palabra a Egor para que les cuente todo sobre nuestras nuevas APIs de mallas de Jetpack SceneCore.
Gracias, Jay.
Hagamos que el usuario pueda dibujar su pista de carreras con el dedo en su sala de estar.
La pista no existe hasta que se dibuja, por lo que nadie puede modelarla con anticipación.
Lo crearemos en código, un triángulo a la vez.
Una malla comienza con vértices, puntos en el espacio.
// 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
)Cada uno tiene atributos como una posición, una normal para la iluminación, el color y las coordenadas de textura.
Los índices conectan los puntos.
Cada tres, se forma un triángulo.
Dos triángulos forman un cuadrado, seis índices, pero solo cuatro vértices, porque comparten dos.
Sigue así y obtendrás un cubo.
Todos los modelos 3D, incluido el auto de Jay, se crean de la misma manera, solo que con más triángulos.
La API de Mesh se asigna a eso con cuatro componentes básicos: VertexLayout, MeshBuffer, CustomMesh y MeshEntity.
Analicémoslas una por una.
Primero, 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()Tus datos de vértices son solo bytes en un ByteBuffer, y cada malla los empaqueta de manera diferente.
VertexLayout indica cómo leer los tuyos: qué atributos, qué tipo y dónde se ubica cada uno.
Aquí, es una posición, una normal y UV, las coordenadas de textura, 32 bytes en total.
Los atributos también pueden residir en búferes separados, de modo que solo puedas actualizar los que cambian.
Segundo, MeshBuffer, un contenedor de datos sin procesar.
// 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)Le proporcionas el diseño, los bytes de vértices y los índices de 32 bits.
Los datos se copian en la GPU de inmediato, por lo que puedes reutilizar o liberar tus ByteBuffers.
De forma predeterminada, el MeshBuffer es estático.
Se establece una vez y se optimiza para una renderización rápida.
Y si necesitas cambiarlo en el entorno de ejecución, createDynamic asigna una capacidad fija que puedes actualizar más adelante.
Tercero, CustomMesh, una vista de 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)No copia ningún dato.
Indica qué rango de índices se debe dibujar y cómo, triángulos o tiras de triángulos.
Aquí, el asfalto y las marcas de carril se encuentran en el mismo MeshBuffer.
Varias mallas personalizadas pueden compartir el mismo MeshBuffer y, cuando no necesitas compartir, BuilderFromMeshData crea la malla y el búfer en un solo paso.
Por último, MeshEntity coloca la malla en la escena.
// 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))La malla es el qué.
La entidad es el dónde.
Al igual que el panel y el auto de Jay, es una entidad, por lo que tiene una posición y un elemento superior, que es el objeto al que está conectado.
Mueve el elemento superior y la ruta se moverá con él.
También proporcionas un material por subconjunto, de modo que la misma malla pueda aparecer con un material diferente en distintas entidades.
Ahora, unamos todo y compilemos la malla, la malla de ruta.
// 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)Definimos el vértice: la posición, la normal y las UV, las coordenadas de textura.
Una pequeña función auxiliar convierte la ruta del usuario en una cinta.
Para cada punto de la ruta de acceso, agrega dos vértices en cada lado y dos triángulos que los conectan con el siguiente par.
Es Kotlin simple, que escribe bytes en un ByteBuffer.
No necesitas ningún tipo de herramienta de 3D.
BuilderFromMeshData, el atajo que acabamos de ver, crea la malla y el MeshBuffer, y calcula los límites por nosotros.
Por último, MeshEntity.create coloca el roadMesh en la escena con el material de asfalto.
Sin embargo, queremos que nuestra pista crezca en vivo a medida que el usuario la dibuja.
Para ello, usamos una malla dinámica.
// 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)La única diferencia es que le damos al compilador la capacidad máxima por adelantado, de modo que se puedan asignar los búferes de GPU.
Luego, a medida que el usuario dibuja, updateVertexData y updateIndexData suben solo los segmentos nuevos, no toda la malla.
Un detalle: el renderizador usa límites para omitir todo lo que esté fuera de la pantalla, así que establece los límites para que cubran todo lo que pueda convertirse en la pista, como el área de juego aquí.
Es posible que notes que la API no es la parte difícil aquí.
La parte difícil son las matemáticas de los vértices, las normales, las coordenadas de textura y el orden de los triángulos.
Ese es exactamente el tipo de código que la IA es excelente para escribir.
Pero supongamos que queremos ir un paso más allá.
Ahora, quiero una bandera a cuadros ondeando al viento en el destino.
La matemática de la animación de vértices es la parte difícil, así que le pregunté a Gemini.
En Android Studio, le di una instrucción: "Con la API de Mesh de SceneCore de Jetpack, crea una cuadrícula de bandera de 20 x 12 con UV de tablero de ajedrez, anima una ola de viento en cada fotograma con updateVertexData y vuelve a calcular las normales para que la iluminación siga los pliegues".
Unos segundos después, obtuve un código que funcionaba.
Veamos qué hizo.
Creó una malla de bandera como una cuadrícula de 20 por 12 celdas, con dos triángulos por celda, y tomó una decisión inteligente con el diseño.
// 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()Las posiciones y las normales se colocan en el búfer 0 y se actualizan en cada fotograma.
Las coordenadas de textura se colocan en el búfer 1 y nunca cambian.
La malla es dinámica y los límites se establecen para cubrir todo el movimiento de la bandera.
En cada fotograma, cada vértice recibe una onda sinusoidal que crece hacia el borde libre, de modo que el lado del poste se mantiene en su lugar.
// 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))
}Las normales se vuelven a calcular a partir de las posiciones vecinas, lo que hace que los pliegues capten la luz.
updateVertexData sube alrededor de 6.5 KB de datos a la GPU en cada fotograma.
No volvemos a crear la malla ni la entidad.
Ahora, veamos todo junto.
Colocamos el modelo de auto en la mesa y, luego, usamos ARCore para dibujar la pista.
La malla de la bandera que acabamos de crear aparece en el destino.
El auto avanza por la pista y usamos la API de Introspection de glTF para dirigir y girar las ruedas del auto.
La malla aparece con el material de asfalto, pero también podemos usar la API de Mesh junto con la API de Material para cambiar el aspecto de la ruta, como se puede ver en el video.
Eso es como tener algo muy poderoso al alcance de la mano.
Hoy, vimos muchas formas de llevar tus apps de 2D a espacial.
Con la API de Introspection de glTF, ahora puedes acceder directamente a los metadatos internos de un modelo de glTF, activar animaciones incorporadas, transformar subnodos individuales y hasta intercambiar materiales para alterar su aspecto, todo durante el tiempo de ejecución.
Luego, vimos que, cuando los modelos 3D prediseñados no eran suficientes, podíamos aprovechar nuestra nueva API de Mesh para crear geometría personalizada.
Ya sea que necesites crear formas estáticas desde cero o transmitir formas dinámicas con geometría dinámica en tiempo real, ahora tienes las herramientas para hacerlo.
Y, como acaba de mostrar Egor, vincular esos componentes básicos con Gemini significa que ni siquiera tienes que hacer los cálculos por tu cuenta.
Ahora, repetiré algo que dije antes: todo lo que viste se ejecutaba en una app para Android, escrita en Kotlin idiomático, directamente en Android Studio.
Veo muchas laptops en el público hoy, y espero que Egor y yo los hayamos entusiasmado lo suficiente como para que abran Android Studio y prueben el emulador de Android XR hoy mismo.
Deja volar tu imaginación y usa tus habilidades de Android existentes, junto con lo que mostramos hoy, para crear experiencias de RE increíbles.
En nombre de Egor y en el mío, muchas gracias por acompañarnos hoy. Disfruten del resto de la 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))
}