Android XR

Aller au-delà de la 2D avec une géométrie 3D personnalisée dans le SDK Jetpack XR

Haut-parleurs

  • Egor Yusov

    Ingénieur logiciel

  • Jay Abi-Saad

    Ingénieur logiciel

À propos

À propos de l'intervention

Pour faire passer vos applications Android d'écrans 2D traditionnels à des environnements spatiaux, vous devez adopter une nouvelle approche pour construire des formes.

Dans cette session, nous allons explorer l'API Custom Mesh du SDK Jetpack XR, qui vous permet de générer de manière programmatique une géométrie 3D procédurale directement au moment de l'exécution. Nous allons passer en revue le workflow de base, de la définition des mises en page de sommets au rendu fluide d'objets dynamiques dans l'espace physique de l'utilisateur.

À la fin de cette présentation, vous disposerez des compétences pratiques nécessaires pour créer une géométrie 3D interactive et personnalisée entièrement à partir de code.

Résumé

  • L'API d'introspection glTF de Jetpack SceneCore expose les métadonnées du modèle au moment de l'exécution. Vous pouvez lire des animations intégrées, transformer des sous-nœuds ou remplacer des maillages par des matériaux et des textures.
  • Jetpack SceneCore inclut des API de maillage pour générer une géométrie 3D personnalisée en Kotlin. Quatre composants de base, tels que VertexLayout et MeshBuffer, construisent des modèles triangle par triangle sans outils 3D.
  • Les maillages dynamiques sont compatibles avec les mises à jour en temps réel lorsque les utilisateurs dessinent des tracés ou animent des surfaces. La mise à jour des seuls flux de données de vertex modifiés transmet les modifications directement au GPU sans recréer le maillage ni l'entité.
  • Gemini dans Android Studio peut écrire des calculs complexes d'animation de sommets et d'éclairage pour les API de maillage. Une seule requête génère un code fonctionnel qui anime les grilles de sommets et recalcule les normales à chaque frame.

Ressources

Gemini Notebook

Thèmes

  • jetpack-xr
  • jetpack-scenecore
  • android-xr
  • géométrie-3d
  • mesh-api
  • gltf

Transcription

Présentation d'Android XR et de SceneCore

  • Bonjour à tous.

  • Je m'appelle Jay.

  • Et moi, c'est Egor.

  • Nous sommes ingénieurs logiciels et nous travaillons sur Jetpack XR chez Google.

  • Avec l'arrivée imminente de XREAL Aura, nous voulons nous assurer que vous disposez de tous les outils nécessaires pour créer d'excellentes expériences spatiales sur la plate-forme.

  • Et le mieux, c'est que vous n'avez même pas besoin de matériel XR pour commencer.

  • Toutes les fonctionnalités que nous allons vous présenter sont disponibles dès aujourd'hui dans l'émulateur Android XR d'Android Studio.

  • Notre objectif est de vous montrer qu'avec la bibliothèque Jetpack SceneCore, vous n'avez pas besoin de connaissances approfondies en 3D pour créer des expériences spatiales riches sur la plate-forme.

  • Tout ce que vous verrez s'exécute dans une application Android, écrite en Kotlin idiomatique directement dans Android Studio.

  • Nous avons récemment publié la version stable de Jetpack SceneCore. Aujourd'hui, nous allons vous présenter quelques avancées intéressantes de nos API de modèles 3D, ainsi que nos toutes nouvelles API de maillage, qui vous permettent de créer des géométries 3D personnalisées à la volée.

Utiliser des modèles 3D glTF

  • Mais commençons par les modèles 3D.

  • Pour les modèles 3D, la première chose dont nous avons besoin est un format de fichier.

  • Les standards ouverts sont au cœur de l'ADN d'Android XR.

  • Nous avons donc naturellement opté pour le format glTF du groupe Khronos.

  • Si vous n'êtes pas familier de l'univers de la 3D, vous vous demandez peut-être ce qu'est un fichier glTF.

  • Vous pouvez le considérer comme un JPEG, mais pour les modèles 3D.

  • La structure de l'élément glTF fournit des données pour des éléments tels que les animations, les sous-nœuds et les maillages.

    {
        "asset" : {...},
        "nodes" : [
            {
                "name" : "Right_Arm",
                "rotation" : [...],
                "scale" : [...],
                "translation" : [...],
            },
        ],
        "animations" : [
            {
                "name" : "Wave",
            },
        ],
        "meshes" : [
            {
                "name" : "Torso",
                "primitives" : [ ... ],
            },
        ],
    
        // Way way more!
    
    }
  • Mais pour les besoins de cette présentation, vous pouvez considérer les sous-nœuds comme des parties d'un modèle, chacune ayant sa propre position et sa propre rotation.

  • Les maillages sont les surfaces que vous voyez, et les animations sont les mouvements de ces sous-nœuds et maillages que l'auteur de l'asset a intégrés à l'avance.

  • Parfait.

  • À ce stade, vous vous demandez peut-être où trouver un modèle glTF.

  • En plus des nombreux modèles existants, vous pouvez également créer les vôtres.

  • Maintenant que nous avons vu les principes de base du format glTF, nous allons l'utiliser dans notre application.

  • Nous commençons par un projet Android XR de base utilisant Jetpack SceneCore.

  • Vous pouvez voir un panneau spatial avec une activité flottant dans l'espace de l'utilisateur.

  • Vous pouvez la saisir, la déplacer et la redimensionner à votre guise.

  • Même si cette présentation est axée sur la 3D, n'hésitez pas à explorer le reste du SDK Jetpack XR.

  • Les panneaux spatiaux sont amusants, mais nous voulons plus de magie.

  • Et quoi de mieux qu'un modèle 3D pour ajouter un peu de magie ?

  • Il se trouve que notre concepteur a créé ce modèle glTF d'une voiture.

    // 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
    )
  • Ils y ont même intégré des animations, que nous utiliserons plus tard.

  • Avec SceneCore, je peux utiliser nos API de modèle 3D pour le charger directement dans notre scène.

Animations de modèles et manipulation de sous-nœuds

  • Mais rendons la voiture un peu plus vivante.

  • Vous vous souvenez de toutes ces métadonnées enrichies dans un fichier glTF ?

  • Grâce à une nouvelle API d'introspection glTF, vous pouvez désormais y accéder, en commençant par les animations.

    // 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()
  • En quelques lignes de code, je peux lire individuellement n'importe quelle animation glTF intégrée au modèle de voiture, comme l'ouverture et la fermeture des portes, du capot ou du coffre.

  • Mais les animations ne sont pas les seules métadonnées auxquelles vous pouvez accéder.

  • L'API d'introspection glTF expose également les sous-nœuds du modèle en tant que GltfModelNode, ce qui vous permet de les déplacer, de les faire pivoter et de les redimensionner à votre guise.

    // 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) }
    }
  • Par exemple, ici, je peux mettre à jour individuellement la position et la rotation des roues pendant que la voiture roule.

  • Pour être clair, ce que vous voyez n'est pas une animation glTF intégrée.

  • Ici, je fais tourner et je dirige les roues individuellement, le tout au moment de l'exécution.

Appliquer des matériaux et des textures

  • Le dernier élément de métadonnées auquel vous pouvez accéder est un maillage du modèle glTF.

  • Egor va vous en dire plus sur les maillages dans un instant.

  • Mais pour l'instant, stylisons notre voiture.

  • Grâce à notre API de texture, nous pouvons charger une texture d'image PNG, par exemple un motif métallique.

    // 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)
  • Ensuite, avec notre API Material, nous pouvons l'intégrer directement dans un KhronosPbrMaterial.

  • Si vous débutez avec la 3D, vous pouvez considérer un maillage comme une table, un matériau comme une nappe et une texture comme un motif imprimé sur cette nappe.

  • Mais ici, au lieu d'un plateau, nous utilisons les objets de matériau que nous avons créés pour remplacer les maillages associés aux sous-nœuds du capot et des portes de la voiture.

  • En quelques lignes de code, nous avons transformé l'apparence de notre voiture, toujours au moment de l'exécution.

  • C'est une bonne idée, mais j'en ai une encore meilleure.

  • Ce serait génial de dessiner un circuit de course sur lequel la voiture pourrait rouler, n'est-ce pas ?

  • Ici, un modèle glTF prédéfini n'est pas adapté.

  • Nous devons plutôt pouvoir générer une géométrie 3D à la volée.

  • Je vais donc laisser la parole à Egor, qui va vous présenter nos toutes nouvelles API de maillage Jetpack SceneCore.

Présentation des API de maillage

  • Merci, Jay.

  • Faisons en sorte que l'utilisateur puisse dessiner son circuit de course avec son doigt dans sa salle de séjour.

  • Le circuit n'existe pas tant qu'il n'est pas dessiné. Personne ne peut donc le modéliser à l'avance.

  • Nous allons le créer dans le code, un triangle à la fois.

  • Un maillage commence par des sommets, des points dans l'espace.

    // 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
    )
  • Chacun comporte des attributs tels qu'une position, une normale pour l'éclairage, une couleur et des coordonnées de texture.

  • Les indices relient les points.

  • Trois points forment un triangle.

  • Deux triangles forment un carré, six indices, mais seulement quatre sommets, car ils en partagent deux.

  • Continuez et vous obtiendrez un cube.

  • Chaque modèle 3D, y compris la voiture de Jay, est construit de la même manière, mais avec plus de triangles.

Composants de base de l'API Mesh

  • L'API Mesh s'appuie sur quatre composants de base : VertexLayout, MeshBuffer, CustomMesh et MeshEntity.

  • Examinons-les un par un.

  • Tout d'abord, 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()
  • Vos données de sommet ne sont que des octets dans un ByteBuffer, et chaque maillage les regroupe différemment.

  • VertexLayout indique comment lire les vôtres : quels attributs, quel type et où chacun se trouve.

  • Ici, il s'agit d'une position, d'une normale et d'UV, les coordonnées de texture, soit 32 octets au total.

  • Les attributs peuvent également se trouver dans des tampons distincts, ce qui vous permet de ne mettre à jour que ceux qui changent.

  • Ensuite, MeshBuffer, un conteneur pour les données brutes.

    // 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)
  • Vous lui fournissez la mise en page, les octets de sommet et les index 32 bits.

  • Les données sont copiées immédiatement sur le GPU, ce qui vous permet de réutiliser ou de libérer vos ByteBuffers.

  • Par défaut, MeshBuffer est statique.

  • Il est défini une seule fois et optimisé pour un rendu rapide.

  • Si vous devez la modifier au moment de l'exécution, createDynamic alloue une capacité fixe que vous pouvez mettre à jour ultérieurement.

  • Troisièmement, CustomMesh, une vue d'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)
  • Il ne copie aucune donnée.

  • Il indique la plage d'indices à dessiner et comment (triangles ou bandes de triangles).

  • Ici, l'asphalte et les marquages au sol se trouvent dans le même MeshBuffer.

  • Plusieurs maillages personnalisés peuvent partager le même MeshBuffer. Lorsque vous n'avez pas besoin de partage, BuilderFromMeshData crée le maillage et le tampon en une seule étape.

  • Enfin, MeshEntity place le maillage dans la scène.

    // 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))
  • Le maillage est le "quoi".

  • L'entité est le "où".

  • Comme le panneau et la voiture de Jay, il s'agit d'une entité. Elle a donc une position et un parent, l'élément auquel elle est rattachée.

  • Déplacez le parent, et la route se déplace avec lui.

  • Vous fournissez également un matériau par sous-ensemble, de sorte que le même maillage puisse apparaître avec un matériau différent dans différentes entités.

Créer des maillages personnalisés et dynamiques

  • Maintenant, rassemblons tous ces éléments et créons le maillage, le maillage de route.

    // 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)
  • Nous définissons le sommet : la position, la normale et les UV, les coordonnées de texture.

  • Une petite fonction d'assistance transforme le chemin de l'utilisateur en ruban.

  • Pour chaque point du tracé, il ajoute deux sommets de chaque côté et deux triangles qui les relient à la paire suivante.

  • Il s'agit d'un code Kotlin simple, qui écrit des octets dans un ByteBuffer.

  • Vous n'avez besoin d'aucun outil 3D.

  • BuilderFromMeshData, le raccourci que nous venons de voir, crée le maillage et le MeshBuffer, et calcule les limites pour nous.

  • Enfin, MeshEntity.create place le roadMesh dans la scène avec le matériau asphalte.

  • Mais nous voulons que notre tracé s'allonge en direct à mesure que l'utilisateur dessine.

  • Pour cela, nous utilisons un maillage dynamique.

    // 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 seule différence est que nous indiquons au compilateur la capacité maximale à l'avance, afin que les tampons GPU puissent être alloués.

  • Ensuite, lorsque l'utilisateur dessine, updateVertexData et updateIndexData n'importent que les nouveaux segments, et non l'intégralité du maillage.

  • Un piège : le moteur de rendu utilise des limites pour ignorer tout ce qui se trouve hors de l'écran. Définissez donc les limites de manière à couvrir tout ce que la piste peut devenir, comme la zone de jeu ici.

  • Vous remarquerez peut-être que l'API n'est pas la partie la plus difficile ici.

  • La partie difficile concerne les calculs des sommets, les normales, les coordonnées de texture et l'ordre des triangles.

  • C'est exactement le genre de code que l'IA est capable d'écrire.

  • Mais imaginons que nous voulions aller encore plus loin.

  • Maintenant, je veux un drapeau à damier qui flotte au vent à l'arrivée.

Générer des maillages animés avec Gemini

  • Les calculs mathématiques de l'animation de vertex sont la partie la plus complexe. J'ai donc demandé à Gemini.

  • Dans Android Studio, je lui ai donné une seule requête : "À l'aide de l'API Jetpack SceneCore Mesh, crée une grille de drapeaux de 20x12 avec des UV en damier, anime une vague de vent à chaque frame avec updateVertexData et recalcule les normales pour que l'éclairage suive les plis."

  • Quelques secondes plus tard, j'ai obtenu un code fonctionnel.

  • Voyons ce qu'il a fait.

  • Il a créé un maillage de drapeau sous la forme d'une grille de 20 cellules sur 12, avec deux triangles par cellule, et a fait un choix judicieux pour la mise en page.

    // 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()
  • Les positions et les normales sont placées dans le tampon 0, et sont mises à jour à chaque frame.

  • Les coordonnées de texture sont placées dans le tampon 1 et ne changent jamais.

  • Le maillage est dynamique et les limites sont définies pour couvrir l'ensemble du mouvement du drapeau.

  • À chaque image, chaque sommet reçoit une onde sinusoïdale qui se propage vers le bord libre, de sorte que le côté du mât reste immobile.

    // 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))
    }
  • Les normales sont recalculées à partir des positions voisines, ce qui permet aux plis de capter la lumière.

  • updateVertexData importe environ 6,5 Ko de données sur le GPU à chaque frame.

  • Nous ne recréons pas le maillage ni l'entité.

  • Voyons maintenant comment tout cela fonctionne ensemble.

  • Nous plaçons le modèle de voiture sur la table, puis nous utilisons ARCore pour dessiner le circuit.

  • Le flag mesh que nous venons de créer apparaît à la destination.

  • La voiture roule sur la piste, et nous utilisons l'API glTF Introspection pour diriger et faire tourner les roues de la voiture.

  • Le maillage apparaît avec le matériau asphalte, mais nous pouvons également utiliser l'API Mesh avec l'API Material pour modifier l'apparence de la route, comme vous pouvez le voir dans la vidéo.

  • C'est un outil très puissant à portée de main.

Résumé et démarrage

  • Aujourd'm, nous avons vu de nombreuses façons de faire passer vos applications de la 2D à l'espace.

  • Avec l'API glTF Introspection, vous pouvez désormais exploiter directement les métadonnées internes d'un modèle glTF, déclencher des animations intégrées, transformer des sous-nœuds individuels et même échanger des matériaux pour modifier son apparence, le tout au moment de l'exécution.

  • Ensuite, nous avons vu que lorsque les modèles 3D prédéfinis ne suffisaient pas, nous pouvions utiliser notre toute nouvelle API Mesh pour créer une géométrie personnalisée.

  • Que vous ayez besoin de créer des formes statiques à partir de zéro ou de diffuser des formes dynamiques en temps réel, vous disposez désormais des outils nécessaires.

  • Et comme Egor vient de le montrer, en associant ces composants de base à Gemini, vous n'avez même pas besoin de faire les calculs vous-même.

  • Je vais maintenant répéter ce que j'ai dit plus tôt : tout ce que vous avez vu s'exécutait dans une application Android, écrite en Kotlin idiomatique, directement dans Android Studio.

  • Je vois beaucoup d'ordinateurs portables dans le public aujourd'hui, et j'espère qu'Egor et moi vous avons donné envie d'ouvrir Android Studio et de lancer l'émulateur Android XR dès aujourd'hui.

  • Laissez libre cours à votre imagination et utilisez vos compétences Android existantes, ainsi que ce que nous avons présenté aujourd'hui, pour créer des expériences XR incroyables.

  • Au nom d'Egor et en mon nom, merci beaucoup de nous avoir rejoints aujourd'hui, et profitez bien du reste du Sommet des développeurs Android.

Code

Extraits de code

Extrait JSON à 01:22 dans "Utiliser des modèles 3D glTF"
{
    "asset" : {...},
    "nodes" : [
        {
            "name" : "Right_Arm",
            "rotation" : [...],
            "scale" : [...],
            "translation" : [...],
        },
    ],
    "animations" : [
        {
            "name" : "Wave",
        },
    ],
    "meshes" : [
        {
            "name" : "Torso",
            "primitives" : [ ... ],
        },
    ],

    // Way way more!

}
Extrait de code Kotlin à 02:28 dans "Utiliser des modèles 3D glTF"
// Load assets/models/car.glb
val carModel = GltfModel.create(
    session,
    Paths.get("models", "car.glb")
)

// Create an entity in the scene
val carEntity = GltfModelEntity.create(
    session,
    carModel,
    parent = session.scene.activitySpace
)
Extrait Kotlin à 02:50 dans "Animations de modèles et manipulation de sous-nœuds"
// 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()
Extrait Kotlin à 03:12 dans "Animations de modèles et manipulation de sous-nœuds"
// 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) }
}
Extrait de code Kotlin à 03:51 dans "Appliquer des matériaux et des textures"
// 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)
Extrait de code Kotlin à 05:05 dans "Présentation des API Mesh"
// 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
)
Extrait de code Kotlin à 05:45 dans "Building blocks of the mesh API"
// 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()
Extrait de code Kotlin à 06:10 dans "Building blocks of the mesh API"
// 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)
Extrait de code Kotlin à 06:37 dans "Building blocks of the mesh API"
// 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)
Extrait de code Kotlin à 07:04 dans "Building blocks of the mesh API"
// 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))
Extrait de code Kotlin à 07:31 dans "Créer des maillages personnalisés et dynamiques"
// 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)
Extrait de code Kotlin à 08:18 dans "Créer des maillages personnalisés et dynamiques"
// 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)
Extrait Kotlin à 09:34 dans "Générer des maillages animés avec Gemini"
// 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()
Extrait Kotlin à 09:56 dans "Générer des maillages animés avec Gemini"
// 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))
}