Android XR

Выход за рамки 2D с помощью пользовательской 3D-геометрии в Jetpack XR SDK

Колонки

  • Егор Юсов

    Разработчик ПО

  • Джей Аби-Саад (Jay Abi-Saad)

    Разработчик ПО

О приложении

О выступлении

Переход приложений для Android от традиционных 2D-экранов к пространственным средам требует нового подхода к созданию фигур.

На этом занятии мы рассмотрим Custom Mesh API в Jetpack XR SDK, который позволяет программно генерировать процедурную 3D-геометрию непосредственно во время выполнения. Мы рассмотрим основные рабочие процессы, от определения макетов вершин до рендеринга динамических объектов в физическом пространстве пользователя.

К концу этого выступления вы получите практические навыки создания интерактивной настраиваемой 3D-геометрии полностью на основе кода.

Сводка

  • API интроспекции glTF в Jetpack SceneCore предоставляет метаданные модели во время выполнения. Вы можете воспроизводить встроенные анимации, преобразовывать дочерние узлы или переопределять сетки с помощью материалов и текстур.
  • Jetpack SceneCore включает API сеток для создания пользовательской 3D-геометрии в Kotlin. Четыре строительных блока, такие как VertexLayout и MeshBuffer, создают модели треугольник за треугольником без использования 3D-инструментов.
  • Динамические сетки поддерживают обновления в реальном времени, когда пользователи рисуют пути или анимируют поверхности. Обновление только измененных данных вершин передает изменения непосредственно в графический процессор без повторного создания сетки или объекта.
  • Gemini в Android Studio может писать сложные математические вычисления для анимации вершин и освещения в API сеток. Один запрос генерирует рабочий код, который анимирует сетки вершин и пересчитывает нормали для каждого кадра.

Ресурсы

Gemini Notebook

Темы

  • jetpack-xr
  • jetpack-scenecore
  • android-xr
  • 3D-геометрия
  • mesh-api
  • gltf

Расшифровка

Знакомство с Android XR и SceneCore

  • Привет, ребята.

  • Меня зовут Джей.

  • А я Егор.

  • Мы разработчики ПО, которые работают над Jetpack XR в Google.

  • XREAL Aura скоро выйдет, и мы хотим, чтобы у вас были все инструменты, необходимые для создания отличных пространственных приложений на этой платформе.

  • Но самое главное – вам даже не нужно специальное оборудование.

  • Все функции, которые мы покажем, уже доступны в эмуляторе Android XR в Android Studio.

  • Наша цель – показать, что с библиотекой Jetpack SceneCore вам не нужны глубокие знания в области 3D, чтобы создавать на платформе качественные пространственные интерфейсы.

  • Все, что вы увидите, работает в приложении для Android, написанном на идиоматическом языке Kotlin прямо в Android Studio.

  • Недавно мы выпустили стабильную версию Jetpack SceneCore. Сегодня мы расскажем о некоторых интересных улучшениях в API 3D-моделей, а также о наших новых API сеток, которые позволяют создавать собственные 3D-объекты на лету.

Как работать с 3D-моделями glTF

  • Но начнем с 3D-моделей.

  • Для начала нужно выбрать формат файла.

  • Открытые стандарты – это основа Android XR.

  • Поэтому мы решили использовать формат glTF от Khronos Group.

  • Если вы не работаете с 3D-моделями, то можете спросить: "Что такое glTF?"

  • Это аналог формата JPEG для 3D-моделей.

  • Структура объектов glTF содержит данные для анимации, дочерних узлов, сеток и т. д.

    {
        "asset" : {...},
        "nodes" : [
            {
                "name" : "Right_Arm",
                "rotation" : [...],
                "scale" : [...],
                "translation" : [...],
            },
        ],
        "animations" : [
            {
                "name" : "Wave",
            },
        ],
        "meshes" : [
            {
                "name" : "Torso",
                "primitives" : [ ... ],
            },
        ],
    
        // Way way more!
    
    }
  • Но в рамках этого доклада вы можете считать дочерние узлы частями модели, у каждой из которых есть свое положение и поворот.

  • Сетки – это видимые поверхности, а анимации – это движения дочерних узлов и сеток, которые автор объекта запек заранее.

  • то идем дальше.

  • Возможно, вы спросите: "Где взять модель glTF?"

  • Помимо множества существующих моделей, вы также можете создать свою собственную.

  • Теперь, когда мы рассмотрели основы формата glTF, давайте продолжим и используем его в нашем приложении.

  • Мы начнем с базового проекта Android XR, в котором используется Jetpack SceneCore.

  • Вы можете увидеть пространственную панель с действием, плавающим в пространстве пользователя.

  • Вы можете перемещать его и изменять его размер.

  • И хотя этот доклад посвящен 3D, обязательно ознакомьтесь с остальной частью Jetpack XR SDK.

  • Пространственные панели – это здорово, но хочется чего-то большего.

  • А что может быть лучше, чем добавить в него 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
    )
  • Они даже встроили в него анимацию, которую мы будем использовать позже.

  • С помощью SceneCore я могу использовать наши API 3D-моделей, чтобы загрузить ее прямо в нашу сцену.

Анимация моделей и управление дочерними узлами

  • Но давайте сделаем машину более живой.

  • Помните все эти метаданные внутри glTF?

  • Новый API для интроспекции glTF позволяет получить доступ к этим данным, начиная с анимаций.

    // 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()
  • Всего несколько строк кода – и я могу воспроизводить любые анимации glTF, встроенные в модель автомобиля, например открывать и закрывать двери, капот или багажник.

  • Но анимации – не единственные метаданные, к которым у вас есть доступ.

  • API glTF также предоставляет доступ к дочерним узлам модели в виде GltfModelNode, что позволяет перемещать, поворачивать и масштабировать их по своему усмотрению.

    // 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) }
    }
  • Например, здесь я могу отдельно обновить положение и вращение колес по мере движения автомобиля.

  • И чтобы было понятно, то, что вы видите, не является встроенной анимацией glTF.

  • Здесь я индивидуально вращаю и управляю колесами во время выполнения.

Применение материалов и текстур

  • Последний тип метаданных, к которым у вас есть доступ, – это сетки в модели glTF.

  • Сейчас Егор подробнее расскажет о сетках.

  • Но пока давайте украсим нашу машину.

  • С помощью API текстур мы можем загрузить текстуру изображения PNG, например металлический узор.

    // 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)
  • Затем с помощью Material API мы можем подключить его прямо к KhronosPbrMaterial.

  • Если вы не знакомы с 3D-моделированием, представьте, что сетка – это столешница, материал – скатерть, а текстура – узор на ней.

  • Но здесь вместо столешницы мы используем созданные нами материальные объекты, чтобы переопределить сетки, связанные с подузлами капота и дверей автомобиля.

  • Всего несколько строк кода – и мы полностью изменили внешний вид автомобиля. И все это во время выполнения.

  • Это здорово, но у меня есть идея получше.

  • Как здорово было бы нарисовать гоночную трассу, по которой будет ездить машина!

  • В этом случае готовая модель glTF не подойдет.

  • Вместо этого нам нужна возможность генерировать 3D-геометрию на лету.

  • А теперь я передаю слово Егору, который расскажет о новых API Jetpack SceneCore для работы с сетками.

Введение в Mesh API

  • Спасибо, Джей.

  • Давайте сделаем так, чтобы пользователь мог нарисовать гоночную трассу пальцем в своей гостиной.

  • Трасса не существует, пока ее не нарисуют, поэтому никто не может смоделировать ее заранее.

  • Мы создадим его в коде, по одному треугольнику за раз.

  • Сетка начинается с вершин, точек в пространстве.

    // 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
    )
  • Каждая из них имеет такие атрибуты, как положение, нормаль для освещения, цвет, координаты текстуры.

  • Индексы соединяют точки.

  • Каждые три образуют треугольник.

  • Два треугольника образуют квадрат, шесть индексов, но только четыре вершины, потому что они делят две.

  • Продолжайте, и вы получите куб.

  • Все 3D-модели, включая автомобиль Джея, создаются одинаково, просто в них больше треугольников.

Компоненты Mesh API

  • Mesh API сопоставляет их с четырьмя строительными блоками: VertexLayout, MeshBuffer, CustomMesh и MeshEntity.

  • Давайте рассмотрим их по очереди.

  • Сначала 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()
  • Данные вершин – это просто байты в ByteBuffer, и каждая сетка упаковывает их по-разному.

  • VertexLayout указывает, как читать ваши данные: какие атрибуты, какого типа и где они находятся.

  • В данном случае это позиция, нормаль и UV-координаты текстуры, всего 32 байта.

  • Атрибуты также могут находиться в отдельных буферах, чтобы вы могли обновлять только те, которые меняются.

  • Второй – MeshBuffer, контейнер для первичных данных.

    // 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)
  • Вы передаете ему макет, байты вершин и 32-битные индексы.

  • Данные сразу копируются в графический процессор, поэтому вы можете повторно использовать или освободить ByteBuffer.

  • По умолчанию MeshBuffer статичен.

  • Оно задается один раз и оптимизировано для быстрой отрисовки.

  • Если вам нужно изменить его во время выполнения, createDynamic выделяет фиксированную емкость, которую вы можете обновить позже.

  • Третий – CustomMesh, представление 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)
  • Данные не копируются.

  • Она указывает, какой диапазон индексов рисовать и как: треугольники или полосы треугольников.

  • В этом случае асфальт и разметка находятся в одном MeshBuffer.

  • Несколько пользовательских сеток могут использовать один и тот же MeshBuffer, а когда совместное использование не требуется, BuilderFromMeshData создает сетку и буфер за один шаг.

  • Наконец, MeshEntity размещает сетку в сцене.

    // 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)
  • Мы определяем вершину: положение, нормаль и UV-координаты текстуры.

  • Небольшая вспомогательная функция превращает путь пользователя в ленту.

  • Для каждой точки на пути добавляются две вершины с каждой стороны и два треугольника, которые соединяют их со следующей парой.

  • Это обычный код Kotlin, который записывает байты в ByteBuffer.

  • Для этого не нужны специальные инструменты.

  • BuilderFromMeshData, который мы только что видели, создает сетку и MeshBuffer, а также вычисляет границы.

  • Наконец, MeshEntity.create помещает roadMesh в сцену с асфальтом.

  • Но мы хотим, чтобы наш трек рос вживую, когда пользователь рисует его.

  • Для этого мы используем динамическую сетку.

    // 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)
  • Единственное отличие заключается в том, что мы заранее предоставляем конструктору максимальную емкость, чтобы можно было выделить буферы графического процессора.

  • Затем, когда пользователь рисует, updateVertexData и updateIndexData загружают только новые сегменты, а не всю сетку.

  • Есть один нюанс: рендерер использует границы, чтобы пропускать все, что находится за пределами экрана, поэтому установите границы так, чтобы они охватывали все, чем может стать трек, например игровую зону.

  • Как видите, API – это не самая сложная часть.

  • Сложная часть – это математика вершин, нормали, координаты текстуры, порядок треугольников.

  • Именно такой код ИИ пишет лучше всего.

  • Но давайте представим, что мы хотим пойти ещё дальше.

  • Теперь я хочу, чтобы в пункте назначения на ветру развевался клетчатый флаг.

Генерирование анимированных сеток с помощью Gemini

  • Математика вершинной анимации – это сложная часть, поэтому я обратился к Gemini.

  • В Android Studio я дал ему одну подсказку: "Используя Jetpack SceneCore Mesh API, создайте сетку флагов 20x12 с шахматными UV-координатами, анимируйте ветровую волну в каждом кадре с помощью updateVertexData и пересчитайте нормали, чтобы освещение следовало за складками".

  • Через несколько секунд я получил рабочий код.

  • Давайте посмотрим, что получилось.

  • Он создал сетку флага размером 20 на 12 ячеек, по два треугольника на ячейку, и сделал разумный выбор с макетом.

    // 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()
  • Позиции и нормали помещаются в буфер 0 и обновляются в каждом кадре.

  • Координаты текстуры помещаются в буфер 1 и никогда не меняются.

  • Сетка динамическая, а границы установлены так, чтобы охватить весь размах флага.

  • В каждом кадре каждая вершина получает синусоидальную волну, которая растет по направлению к свободному краю, так что сторона полюса остается на месте.

    // 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))
    }
  • Нормали пересчитываются на основе соседних позиций, благодаря чему складки улавливают свет.

  • updateVertexData загружает в графический процессор около 6,5 КБ данных на кадр.

  • Мы не воссоздаем сетку или объект.

  • Теперь давайте посмотрим, как это выглядит в целом.

  • Мы ставим модель автомобиля на стол, а затем используем ARCore, чтобы нарисовать трассу.

  • Сетка флага, которую мы только что создали, появляется в месте назначения.

  • Машина едет по трассе, а мы используем glTF Introspection API, чтобы управлять ею и вращать колеса.

  • Сетка появляется с материалом асфальта, но мы также можем использовать API сетки вместе с API материала, чтобы изменить внешний вид дороги, как вы можете видеть на видео.

  • Это очень мощный инструмент, который всегда под рукой.

Обзор и начало работы

  • Сегодня мы рассмотрели множество способов превратить ваши приложения из 2D в пространственные.

  • С помощью glTF Introspection API вы можете напрямую использовать внутренние метаданные модели glTF, запуская встроенные анимации, преобразуя отдельные дочерние узлы и даже заменяя материалы, чтобы изменить внешний вид модели во время выполнения.

  • Затем мы увидели, что когда готовых 3D-моделей недостаточно, мы можем использовать наш совершенно новый Mesh API для создания пользовательской геометрии.

  • Теперь у вас есть инструменты, позволяющие создавать статические фигуры с нуля или транслировать динамические фигуры и геометрию в реальном времени.

  • И, как только что показал Егор, если вы будете использовать эти строительные блоки вместе с Gemini, вам даже не придется выполнять вычисления самостоятельно.

  • Повторю то, что я уже говорил: все, что вы видели, работало в приложении для Android, написанном на идиоматическом Kotlin прямо в Android Studio.

  • Я вижу, что у многих из вас с собой ноутбуки. Надеюсь, мы с Егором вдохновили вас открыть Android Studio и запустить эмулятор Android XR.

  • Дайте волю воображению и используйте свои навыки разработки для Android и то, что вы узнали сегодня, чтобы создавать потрясающие XR-приложения.

  • От имени Егора и от себя лично большое спасибо, что присоединились к нам сегодня. Желаю вам приятного продолжения Саммита для разработчиков Android.

Код

Фрагменты кода

Фрагмент JSON на 01:22 в видео "Работа с 3D-моделями glTF"
{
    "asset" : {...},
    "nodes" : [
        {
            "name" : "Right_Arm",
            "rotation" : [...],
            "scale" : [...],
            "translation" : [...],
        },
    ],
    "animations" : [
        {
            "name" : "Wave",
        },
    ],
    "meshes" : [
        {
            "name" : "Torso",
            "primitives" : [ ... ],
        },
    ],

    // Way way more!

}
Фрагмент кода на Kotlin в видео "Работа с 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
)
Фрагмент кода на Kotlin в видео "Анимация моделей и управление дочерними узлами"
// 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()
Фрагмент кода Kotlin на отметке 03:12 в разделе "Анимация моделей и управление дочерними узлами"
// 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) }
}
Фрагмент кода на Kotlin в разделе "Применение материалов и текстур" (03:51)
// 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)
Фрагмент кода на Kotlin в видео "Введение в 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
)
Фрагмент кода на Kotlin в разделе "Строительные блоки 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()
Фрагмент кода на Kotlin в видео "Строительные блоки API Mesh"
// 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)
Фрагмент кода на Kotlin в видео "Строительные блоки 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)
Фрагмент кода Kotlin на 07:04 в "Строительные блоки API mesh"
// 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))
Фрагмент кода на Kotlin в видео "Создание пользовательских и динамических сеток" (07:31)
// 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)
Фрагмент кода на Kotlin в видео "Создание пользовательских и динамических сеток"
// 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)
Фрагмент кода на Kotlin в видео "Генерирование анимированных сеток с помощью 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()
Фрагмент кода на Kotlin в видео "Генерирование анимированных сеток с помощью 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))
}