Wenn Sie Ihre Android-Apps von herkömmlichen 2D-Bildschirmen in räumliche Umgebungen übertragen, müssen Sie Formen auf eine neue Weise erstellen.
In dieser Session geht es um die Custom Mesh API im Jetpack XR SDK, mit der Sie prozedurale 3D-Geometrie direkt zur Laufzeit programmatisch generieren können. Wir gehen den gesamten Workflow durch, von der Definition von Vertex-Layouts bis hin zum nahtlosen Rendern dynamischer Objekte im physischen Raum des Nutzers.
Am Ende dieses Vortrags verfügen Sie über die praktischen Fähigkeiten, um interaktive, benutzerdefinierte 3D-Geometrie vollständig aus Code zu erstellen.
MeshBuffer erstellen Modelle Dreieck für Dreieck ohne 3D-Tools.Transkript
Hallo allerseits.
Ich bin Jay.
Und ich bin Egor.
Wir sind Softwareentwickler und arbeiten bei Google an Jetpack XR.
Die XREAL Aura steht in den Startlöchern und wir möchten, dass ihr alle Tools habt, die ihr braucht, um tolle räumliche Erlebnisse auf der Plattform zu entwickeln.
Das Beste daran ist, dass Sie nicht einmal XR-Hardware benötigen, um loszulegen.
Alle Funktionen, die wir vorstellen, sind bereits im Android XR-Emulator in Android Studio verfügbar.
Wir möchten Ihnen zeigen, dass Sie mit der Jetpack SceneCore-Bibliothek kein tiefgreifendes 3D-Wissen benötigen, um auf der Plattform umfangreiche räumliche Erlebnisse zu erstellen.
Alles, was Sie sehen, wird in einer Android-App ausgeführt, die in idiomatischem Kotlin direkt in Android Studio geschrieben wurde.
Vor Kurzem haben wir Jetpack SceneCore auf die stabile Version aktualisiert. Heute stellen wir Ihnen einige interessante Neuerungen bei unseren 3D-Modell-APIs sowie unsere brandneuen Mesh-APIs vor, mit denen Sie im Handumdrehen benutzerdefinierte 3D-Geometrie erstellen können.
Beginnen wir mit 3D-Modellen.
Für 3D-Modelle benötigen wir zuerst ein Dateiformat.
Offene Standards sind ein wichtiger Bestandteil von Android XR.
Daher haben wir uns für das glTF-Format der Khronos Group entschieden.
Wenn Sie nicht aus der 3D-Welt kommen, fragen Sie sich vielleicht: „Was ist ein glTF?“
Sie können sich das wie ein JPEG vorstellen, aber für 3D-Modelle.
Die glTF-Asset-Struktur liefert Daten für Animationen, Unterknoten und Meshes.
{
"asset" : {...},
"nodes" : [
{
"name" : "Right_Arm",
"rotation" : [...],
"scale" : [...],
"translation" : [...],
},
],
"animations" : [
{
"name" : "Wave",
},
],
"meshes" : [
{
"name" : "Torso",
"primitives" : [ ... ],
},
],
// Way way more!
}Für diesen Vortrag können Sie sich Unterknoten als Teile eines Modells vorstellen, die jeweils eine eigene Position und Drehung haben.
Meshes sind die Oberflächen, die Sie sehen, und die Animationen sind Bewegungen dieser Unterknoten und Meshes, die der Autor des Assets im Voraus eingebunden hat.
Cool.
An dieser Stelle fragen Sie sich vielleicht: „Woher bekomme ich überhaupt ein glTF-Modell?“
Zusätzlich zu den vielen Modellen, die es gibt, können Sie auch eigene erstellen.
Nachdem wir die Grundlagen des glTF-Formats behandelt haben, können wir es jetzt in unserer App verwenden.
Wir beginnen mit einem einfachen Android XR-Projekt, das Jetpack SceneCore verwendet.
Sie sehen ein räumliches Steuerfeld mit einer Aktivität, die im Raum des Nutzers schwebt.
Sie können es nach Belieben verschieben und die Größe anpassen.
Auch wenn es in diesem Vortrag nur um 3D geht, sollten Sie sich auf jeden Fall auch den Rest des Jetpack XR SDK ansehen.
Okay, räumliche Bereiche sind ganz nett, aber wir wollen mehr Magie.
Und wie könnte man das besser tun, als ein 3D-Modell hinzuzufügen?
Unser Designer hat dieses glTF-Modell eines Autos erstellt.
// 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
)Sie haben sogar Animationen eingebettet, die wir später verwenden werden.
Mit SceneCore kann ich unsere 3D-Modell-APIs verwenden, um es direkt in unsere Szene zu laden.
Aber lassen Sie uns das Auto etwas lebendiger machen.
Erinnern Sie sich noch an die vielen Metadaten in einer glTF-Datei?
Mit der neuen glTF Introspection API können Sie jetzt darauf zugreifen, beginnend mit Animationen.
// 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()Mit nur wenigen Codezeilen kann ich jede im Automodell integrierte glTF-Animation einzeln abspielen, z. B. das Öffnen und Schließen der Türen, der Motorhaube oder des Kofferraums.
Animationen sind jedoch nicht die einzigen Metadaten, auf die Sie zugreifen können.
Die glTF-Introspektions-API stellt auch Unterknoten im Modell als GltfModelNode bereit, sodass Sie sie nach Belieben verschieben, drehen und skalieren können.
// 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) }
}Hier kann ich zum Beispiel die Position und die Drehung der Räder während der Fahrt einzeln aktualisieren.
Und um es klarzustellen: Was Sie hier sehen, ist keine eingebettete glTF-Animation.
Hier drehe und steuere ich die Räder einzeln zur Laufzeit.
Ein letztes Metadatenelement, auf das Sie zugreifen können, ist ein beliebiges Mesh im glTF-Modell.
Egor wird gleich näher auf Meshes eingehen.
Aber jetzt stylen wir erst einmal unser Auto.
Mit unserer Textur-API können wir eine PNG-Bildtextur laden, z. B. ein Metallmuster.
// 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)Mit unserer Material API können wir es dann direkt in ein KhronosPbrMaterial einfügen.
Wenn Sie neu im Bereich 3D sind, können Sie sich ein Mesh als Tischplatte, ein Material als Tischdecke und eine Textur als Muster auf dieser Decke vorstellen.
Aber hier verwenden wir anstelle einer Tischplatte die von uns erstellten Materialobjekte, um die Meshes zu überschreiben, die mit den Unterknoten für die Motorhaube und die Türen des Autos verknüpft sind.
Mit nur wenigen Codezeilen haben wir das Aussehen unseres Autos verändert – und das alles zur Laufzeit.
Das ist schon ziemlich cool, aber ich habe eine noch bessere Idee.
Wie cool wäre es, eine Rennstrecke zu zeichnen, auf der das Auto fahren kann?
Hier ist ein vorgefertigtes glTF-Modell nicht die richtige Wahl.
Stattdessen müssen wir in der Lage sein, 3D-Geometrie im laufenden Betrieb zu generieren.
Ich übergebe jetzt an Egor, der euch alles über unsere brandneuen Jetpack SceneCore Mesh APIs erzählen wird.
Vielen Dank, Jay.
Lassen Sie uns die App so gestalten, dass der Nutzer die Rennstrecke mit dem Finger in seinem Wohnzimmer zeichnen kann.
Die Strecke existiert erst, wenn sie gezeichnet wird. Niemand kann sie also im Voraus modellieren.
Wir erstellen es im Code, Dreieck für Dreieck.
Ein Mesh beginnt mit Eckpunkten, also Punkten im Raum.
// 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
)Jeder hat Attribute wie eine Position, eine Normale für die Beleuchtung, eine Farbe und Texturkoordinaten.
Indizes stellen die Verbindung her.
Jeweils drei bilden ein Dreieck.
Zwei Dreiecke ergeben ein Quadrat, sechs Indizes, aber nur vier Eckpunkte, da sie sich zwei teilen.
Wenn du weitermachst, erhältst du einen Würfel.
Jedes 3D-Modell, einschließlich des Autos von Jay, wird auf die gleiche Weise erstellt, nur mit mehr Dreiecken.
Die Mesh API wird mit vier Bausteinen darauf abgebildet: VertexLayout, MeshBuffer, CustomMesh und MeshEntity.
Gehen wir sie der Reihe nach durch.
Zuerst 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()Die Vertexdaten sind nur Byte in einem ByteBuffer und jedes Mesh packt sie anders.
VertexLayout gibt an, wie Ihre Daten gelesen werden sollen: welche Attribute, welcher Typ und wo sich die einzelnen Attribute befinden.
Hier sind es eine Position, eine Normale und UVs, die Texturkoordinaten, insgesamt 32 Byte.
Attribute können auch in separaten Puffern gespeichert werden, sodass Sie nur die Attribute aktualisieren können, die sich ändern.
Zweitens: MeshBuffer, ein Container für Rohdaten.
// 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)Sie geben das Layout, die Vertex-Bytes und 32-Bit-Indizes an.
Die Daten werden sofort auf die GPU kopiert, sodass Sie Ihre ByteBuffers wiederverwenden oder freigeben können.
Standardmäßig ist die MeshBuffer statisch.
Sie wird einmal festgelegt und ist für schnelles Rendering optimiert.
Wenn Sie die Kapazität zur Laufzeit ändern müssen, weist createDynamic eine feste Kapazität zu, die Sie später aktualisieren können.
Drittens: CustomMesh, ein Einblick in eine 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)Es werden keine Daten kopiert.
Es gibt an, welcher Indexbereich gezeichnet werden soll und wie, also Dreiecke oder Dreiecksstreifen.
Hier befinden sich der Asphalt und die Fahrbahnmarkierungen im selben MeshBuffer.
Mehrere benutzerdefinierte Meshes können denselben MeshBuffer-Wert verwenden. Wenn Sie keine Freigabe benötigen, erstellt BuilderFromMeshData das Mesh und den Zwischenspeicher in einem Schritt.
Schließlich platziert MeshEntity das Mesh in der Szene.
// 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))Das Mesh ist das „Was“.
Die Entität ist das „Wo“.
Wie das Panel und Jays Auto ist es eine Entität, hat also eine Position und ein übergeordnetes Element, an dem es befestigt ist.
Wenn Sie das übergeordnete Element verschieben, wird die Straße mit verschoben.
Außerdem stellen Sie ein Material pro Teilmenge bereit, sodass dasselbe Mesh mit unterschiedlichem Material in verschiedenen Entitäten angezeigt werden kann.
Jetzt fügen wir alles zusammen und erstellen das Mesh, das Straßen-Mesh.
// 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)Wir definieren den Scheitelpunkt: die Position, die Normale und die UVs, die Texturkoordinaten.
Eine kleine Hilfsfunktion wandelt den Nutzerpfad in ein Band um.
Für jeden Punkt auf dem Pfad werden auf jeder Seite zwei Eckpunkte und zwei Dreiecke hinzugefügt, die sie mit dem nächsten Paar verbinden.
Es ist einfaches Kotlin, das Byte in einen ByteBuffer schreibt.
Sie benötigen kein 3D-Tool.
BuilderFromMeshData, die Abkürzung, die wir gerade gesehen haben, erstellt das Mesh und die MeshBuffer und berechnet die Grenzen für uns.
Schließlich platziert MeshEntity.create die roadMesh in der Szene mit dem Asphaltmaterial.
Wir möchten aber, dass die Strecke live wächst, während der Nutzer sie zeichnet.
Dafür verwenden wir ein dynamisches Mesh.
// 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)Der einzige Unterschied besteht darin, dass wir dem Builder im Voraus die maximale Kapazität zuweisen, damit die GPU-Puffer zugewiesen werden können.
Wenn der Nutzer dann zeichnet, werden von updateVertexData und updateIndexData nur die neuen Segmente hochgeladen, nicht das gesamte Mesh.
Ein Problem: Der Renderer verwendet Grenzen, um alles außerhalb des Bildschirms zu überspringen. Legen Sie die Grenzen also so fest, dass alles abgedeckt wird, was die Spur werden könnte, wie hier der Spielbereich.
Ihnen ist vielleicht aufgefallen, dass die API hier nicht der schwierige Teil ist.
Der schwierige Teil ist die Vertex-Mathematik, die Normalen, die Texturkoordinaten und die Dreiecksreihenfolge.
Das ist genau die Art von Code, die KI gut schreiben kann.
Aber nehmen wir an, wir möchten noch einen Schritt weiter gehen.
Now Now, I want a checkered flag waving at the wind at the destination.
Die Mathematik hinter der Vertex-Animation ist der knifflige Teil, also habe ich Gemini gefragt.
In Android Studio habe ich einen Prompt eingegeben: „Erstelle mit der Jetpack SceneCore Mesh API ein 20 x 12-Flaggenraster mit Schachbrett-UVs, animiere in jedem Frame eine Windwelle mit updateVertexData und berechne die Normalen neu, damit die Beleuchtung den Falten folgt.“
Ein paar Sekunden später hatte ich einen funktionierenden Code.
Sehen wir uns an, was passiert ist.
Es hat ein Flaggennetz als Raster mit 20 x 12 Zellen und zwei Dreiecken pro Zelle erstellt und eine gute Wahl beim Layout getroffen.
// 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()Positionen und Normalen werden in Puffer 0 platziert und bei jedem Frame aktualisiert.
Texturkoordinaten werden in Puffer 1 platziert und ändern sich nie.
Das Mesh ist dynamisch und die Grenzen sind so festgelegt, dass die Flagge vollständig schwingen kann.
In jedem Frame erhält jeder Scheitelpunkt eine Sinuswelle, die sich in Richtung der offenen Kante ausbreitet, sodass die Seite am Mast an Ort und Stelle bleibt.
// 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))
}Die Normalen werden anhand der benachbarten Positionen neu berechnet, wodurch die Falten das Licht einfangen.
updateVertexData lädt pro Frame etwa 6,5 KB Daten auf die GPU hoch.
Wir erstellen das Mesh oder die Entität nicht neu.
Sehen wir uns nun alles zusammen an.
Wir platzieren das Automodell auf dem Tisch und zeichnen dann mit ARCore die Strecke.
Das Flag-Mesh, das wir gerade erstellt haben, wird am Ziel angezeigt.
Das Auto fährt die Strecke entlang und wir verwenden die glTF Introspection API, um die Räder des Autos zu lenken und zu drehen.
Das Mesh wird mit dem Asphaltmaterial angezeigt. Wir können aber auch die Mesh API zusammen mit der Material API verwenden, um das Aussehen der Straße zu ändern, wie im Video zu sehen ist.
Das ist eine sehr leistungsstarke Funktion, die Sie direkt nutzen können.
Heute haben wir viele Möglichkeiten kennengelernt, wie Sie Ihre Apps von 2D auf Spatial umstellen können.
Mit der glTF Introspection API können Sie jetzt direkt auf die internen Metadaten eines glTF-Modells zugreifen, eingebettete Animationen auslösen, einzelne Unterknoten transformieren und sogar Materialien austauschen, um das Erscheinungsbild zur Laufzeit zu ändern.
Wenn vorgefertigte 3D-Modelle nicht ausreichen, können wir mit der neuen Mesh API benutzerdefinierte Geometrie erstellen.
Egal, ob Sie statische Formen von Grund auf neu erstellen oder dynamische Formen und Geometrie in Echtzeit streamen möchten – jetzt haben Sie die richtigen Tools dafür.
Und wie Egor gerade gezeigt hat, müssen Sie die Berechnungen nicht einmal selbst durchführen, wenn Sie diese Bausteine mit Gemini kombinieren.
Ich wiederhole noch einmal, was ich vorhin gesagt habe: Alles, was Sie gesehen haben, wurde in einer Android-App ausgeführt, die in idiomatischem Kotlin direkt in Android Studio geschrieben wurde.
Ich sehe heute viele Laptops im Publikum und hoffe, dass Egor und ich Sie genug begeistert haben, um Android Studio zu öffnen und den Android XR-Emulator zu starten.
Lassen Sie Ihrer Fantasie freien Lauf und nutzen Sie Ihre vorhandenen Android-Kenntnisse zusammen mit dem, was wir heute gezeigt haben, um beeindruckende XR-Erlebnisse zu entwickeln.
Im Namen von Egor und mir vielen Dank, dass Sie heute dabei waren. Wir wünschen Ihnen noch viel Spaß beim 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))
}