A transição dos apps Android de telas 2D tradicionais para ambientes espaciais exige uma nova abordagem para a criação de formas.
Nesta sessão, vamos conhecer a API Custom Mesh no SDK do Jetpack para XR, que permite gerar de forma programática geometria 3D procedural diretamente no ambiente de execução. Vamos mostrar o fluxo de trabalho principal, desde a definição de layouts de vértices até a renderização de objetos dinâmicos no espaço físico do usuário.
Ao final desta palestra, você terá as habilidades práticas para criar geometrias 3D interativas e personalizadas usando apenas código.
MeshBuffer, criam modelos triângulo por triângulo sem ferramentas 3D.Transcrição
Olá, pessoal.
Sou o Jay.
E eu sou o Egor.
Somos engenheiros de software que trabalham no Jetpack XR no Google.
Com o lançamento do XREAL Aura se aproximando, queremos garantir que você tenha todas as ferramentas necessárias para criar ótimas experiências espaciais na plataforma.
E o melhor de tudo é que você nem precisa de hardware de XR para começar.
Todos os recursos que vamos mostrar estão disponíveis hoje no emulador Android XR no Android Studio.
Nossa meta é mostrar que, com a biblioteca Jetpack SceneCore, você não precisa de um conhecimento profundo em 3D para criar experiências espaciais ricas na plataforma.
Tudo o que você vai ver é executado em um app Android, escrito em Kotlin idiomático no Android Studio.
Recentemente, promovemos o Jetpack SceneCore para estável. Hoje, vamos falar sobre alguns avanços legais das nossas APIs de modelo 3D, além das nossas novas APIs de malha, que permitem criar geometrias 3D personalizadas em tempo real.
Mas vamos começar com os modelos 3D.
Quando se trata de modelos 3D, a primeira coisa que precisamos é de um formato do arquivo.
Os padrões abertos são uma parte fundamental do DNA do Android XR.
Por isso, naturalmente, apostamos tudo no formato glTF do Khronos Group.
Se você não é do mundo 3D, talvez esteja se perguntando: "O que é um glTF?"
Pense nele como um JPEG, mas para modelos 3D.
A estrutura de recursos glTF fornece dados para itens como animações, subnós e malhas.
{
"asset" : {...},
"nodes" : [
{
"name" : "Right_Arm",
"rotation" : [...],
"scale" : [...],
"translation" : [...],
},
],
"animations" : [
{
"name" : "Wave",
},
],
"meshes" : [
{
"name" : "Torso",
"primitives" : [ ... ],
},
],
// Way way more!
}Mas, para os fins desta palestra, pense nos subnós como partes de um modelo, cada um com sua própria posição e rotação.
As malhas são as superfícies que você vê, e as animações são movimentos desses subnós e malhas que o autor do recurso incorporou com antecedência.
Legal.
Agora, você pode estar se perguntando: "Onde posso conseguir um modelo glTF?"
Além dos vários modelos disponíveis, você também pode criar os seus.
Agora que já abordamos os conceitos básicos do formato glTF, vamos usá-lo no app.
Começamos com um projeto básico do Android XR usando o Jetpack SceneCore.
Você pode ver um painel espacial com uma atividade flutuando no espaço do usuário.
Você pode pegar, mover e redimensionar como quiser.
E, embora esta palestra seja sobre 3D, confira também o restante do SDK do Jetpack para XR.
Os painéis espaciais são divertidos, mas queremos mais magia.
E que melhor maneira de adicionar um pouco de magia do que incluir um modelo 3D?
Nosso designer criou este modelo glTF de um carro.
// 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
)Eles até incorporaram animações, que vamos usar mais tarde.
Com o SceneCore, posso usar nossas APIs de modelo 3D para carregá-lo diretamente na cena.
Mas vamos deixar o carro um pouco mais realista.
Lembra de todos aqueles metadados avançados dentro de um glTF?
Com uma nova API de introspecção glTF, agora é possível acessá-lo, começando com animações.
// 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()Com apenas algumas linhas de código, posso reproduzir individualmente qualquer animação glTF integrada ao modelo do carro, como abrir e fechar as portas, o capô ou o porta-malas.
Mas as animações não são os únicos metadados que você pode acessar.
A API de introspecção glTF também expõe subnós no modelo como GltfModelNode, permitindo que você os mova, gire e dimensione como quiser.
// 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 exemplo, aqui, posso atualizar individualmente a posição e a rotação das rodas enquanto o carro se move.
E, para deixar claro, o que você está vendo não é uma animação glTF incorporada.
Aqui, estou girando e direcionando as rodas individualmente durante a execução.
Um último metadado que você pode acessar é qualquer malha no modelo glTF.
O Egor vai falar mais sobre as malhas em breve.
Mas, por enquanto, vamos estilizar nosso carro.
Usando nossa API de textura, podemos carregar uma textura de imagem PNG, por exemplo, de um padrão 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)Em seguida, com nossa API Material, podemos conectá-lo diretamente a um KhronosPbrMaterial.
Se você não conhece o 3D, pense em uma malha como uma mesa, um material como uma toalha de mesa e uma textura como um padrão impresso nessa toalha.
Mas aqui, em vez de uma mesa, usamos os objetos de material que criamos para substituir as malhas associadas aos subnós do capô e das portas do carro.
Com apenas algumas linhas de código, transformamos a aparência do nosso carro, novamente, tudo em tempo de execução.
Isso é legal, mas tenho uma ideia ainda melhor.
Não seria incrível desenhar uma pista de corrida para o carro andar?
Nesse caso, um modelo glTF pré-criado não é a escolha certa.
Em vez disso, precisamos gerar geometria 3D em tempo real.
Agora, vou passar a palavra para o Egor, que vai falar sobre as novas APIs de malha do Jetpack SceneCore.
Obrigado, Jay.
Vamos fazer com que o usuário possa desenhar a pista de corrida com o dedo na sala de estar.
A pista não existe até ser desenhada, então ninguém pode modelá-la com antecedência.
Vamos criar isso no código, um triângulo por vez.
Uma malha começa com vértices, pontos no espaço.
// 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 um tem atributos como posição, normal para iluminação, cor e coordenadas de textura.
Os índices ligam os pontos.
Cada três formam um triângulo.
Dois triângulos formam um quadrado, seis índices, mas apenas quatro vértices, porque eles compartilham dois.
Continue e você terá um cubo.
Todos os modelos 3D, incluindo o carro do Jay, são criados da mesma forma, mas com mais triângulos.
A API Mesh é mapeada para isso com quatro elementos básicos: VertexLayout, MeshBuffer, CustomMesh e MeshEntity.
Vamos analisar um de cada vez.
Primeiro, 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()Os dados de vértice são apenas bytes em um ByteBuffer, e cada malha os empacota de maneira diferente.
O VertexLayout diz como ler os seus: quais atributos, que tipo e onde cada um fica.
Aqui, é uma posição, uma normal e UVs, as coordenadas de textura, totalizando 32 bytes.
Os atributos também podem residir em buffers separados, para que você possa atualizar apenas os que mudam.
Segundo, MeshBuffer, um contêiner para dados brutos.
// 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)Você fornece o layout, os bytes de vértice e os índices de 32 bits.
Os dados são copiados para a GPU imediatamente, para que você possa reutilizar ou liberar seus ByteBuffers.
Por padrão, o MeshBuffer é estático.
Ele é definido uma vez e otimizado para renderização rápida.
Se você precisar mudar isso no ambiente de execução, o createDynamic vai alocar uma capacidade fixa que pode ser atualizada mais tarde.
Terceiro, CustomMesh, uma visualização de um 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)Ele não copia dados.
Ele informa qual intervalo de índices desenhar e como, triângulos ou tiras de triângulos.
Aqui, o asfalto e as marcações de faixa estão no mesmo MeshBuffer.
Várias malhas personalizadas podem compartilhar o mesmo MeshBuffer. Quando você não precisa de compartilhamento, BuilderFromMeshData cria a malha e o buffer em uma única etapa.
Por fim, MeshEntity coloca a malha na cena.
// 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))A malha é o "o que".
A entidade é o "onde".
Como o painel e o carro do Jay, ele é uma entidade, então tem uma posição e um pai, que é o objeto ao qual ele está anexado.
Mova o elemento pai, e a via se moverá com ele.
Você também fornece um material por subconjunto, para que a mesma malha possa aparecer com materiais diferentes em entidades distintas.
Agora, vamos juntar tudo e criar a malha, a malha da estrada.
// 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 o vértice: a posição, a normal e os UVs, as coordenadas de textura.
Uma pequena função auxiliar transforma o caminho do usuário em uma fita.
Para cada ponto no caminho, ele adiciona dois vértices em cada lado e dois triângulos que os conectam ao próximo par.
É Kotlin simples, gravando bytes em um ByteBuffer.
Não é necessário usar nenhum tipo de ferramenta 3D.
BuilderFromMeshData, o atalho que acabamos de ver, cria a malha e o MeshBuffer e calcula os limites para nós.
Por fim, o MeshEntity.create coloca o roadMesh na cena com o material de asfalto.
Mas queremos que a faixa cresça em tempo real conforme o usuário desenha.
Para isso, usamos uma malha 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)A única diferença é que fornecemos ao builder a capacidade máxima antecipadamente para que os buffers de GPU possam ser alocados.
Em seguida, enquanto o usuário desenha, updateVertexData e updateIndexData fazem upload apenas dos novos segmentos, não de toda a malha.
Uma pegadinha: o renderizador usa limites para pular tudo que está fora da tela. Por isso, defina os limites para cobrir tudo o que a faixa pode se tornar, como a área de jogo aqui.
Você vai perceber que a API não é a parte difícil aqui.
A parte difícil é a matemática dos vértices, as normais, as coordenadas de textura e a ordem dos triângulos.
Esse é exatamente o tipo de código que a IA é ótima para escrever.
Mas digamos que queremos ir um pouco além.
Agora, quero uma bandeira quadriculada tremulando ao vento no destino.
A matemática da animação de vértices é a parte complicada, então pedi ajuda ao Gemini.
No Android Studio, dei um comando: "Usando a API Jetpack SceneCore Mesh, crie uma grade de bandeira de 20x12 com UVs de tabuleiro de xadrez, anime uma onda de vento em cada frame com updateVertexData e recalcule as normais para que a iluminação siga as dobras".
Alguns segundos depois, recebi um código funcional.
Vamos ver o que ele fez.
Ele criou uma malha de bandeira como uma grade de 20 por 12 células, dois triângulos por célula, e fez uma escolha inteligente com o 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()As posições e as normais são colocadas no Buffer 0 e atualizadas a cada frame.
As coordenadas de textura são colocadas no Buffer 1 e nunca mudam.
A malha é dinâmica, e os limites são definidos para cobrir todo o movimento da bandeira.
Em cada frame, cada vértice recebe uma onda senoidal que cresce em direção à borda livre, para que o lado do mastro permaneça no 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))
}As normais são recalculadas com base nas posições vizinhas, o que faz com que as dobras capturem a luz.
updateVertexData faz upload de cerca de 6,5 KB de dados para a GPU em cada frame.
Não recriamos a malha nem a entidade.
Agora vamos ver tudo junto.
Colocamos o modelo de carro na mesa e usamos o ARCore para desenhar a pista.
A flag de malha que acabamos de criar aparece no destino.
O carro anda pela pista, e usamos a API glTF Introspection para dirigir e girar as rodas do carro.
A malha aparece com o material de asfalto, mas também podemos usar a API Mesh com a API Material para mudar a aparência da via, como você pode ver no vídeo.
Isso é algo muito poderoso ao seu alcance.
Hoje, falamos sobre várias maneiras de transformar seus apps de 2D em espaciais.
Com a API glTF Introspection, agora é possível acessar diretamente os metadados internos de um modelo glTF, acionando animações incorporadas, transformando subnós individuais e até mesmo trocando materiais para alterar a aparência em tempo de execução.
Depois, vimos que, quando os modelos 3D predefinidos não são suficientes, podemos usar nossa nova API Mesh para criar geometrias personalizadas.
Seja para criar formas estáticas do zero ou transmitir geometria dinâmica em tempo real, agora você tem as ferramentas para fazer isso.
E, como o Egor acabou de mostrar, ao combinar esses elementos básicos com o Gemini, você nem precisa fazer as contas por conta própria.
Agora, vou repetir algo que disse antes: tudo o que você viu estava sendo executado em um app Android, escrito em Kotlin idiomático, dentro do Android Studio.
Vejo muitos laptops na plateia hoje, e espero que o Egor e eu tenhamos deixado vocês animados para abrir o Android Studio e testar o emulador Android XR hoje mesmo.
Deixe sua imaginação correr solta e use suas habilidades atuais do Android, junto com o que mostramos hoje, para criar experiências incríveis de XR.
Em meu nome e do Egor, agradeço a participação de vocês hoje. Aproveitem o restante do 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))
}