Android XR

Vượt ra ngoài không gian 2D bằng hình học 3D tuỳ chỉnh trong SDK Jetpack XR

Loa

  • Egor Yusov

    Kỹ sư phần mềm

  • Jay Abi-Saad

    Kỹ sư phần mềm

Giới thiệu

Giới thiệu về bài thuyết trình

Để chuyển đổi ứng dụng Android từ màn hình 2D truyền thống sang môi trường không gian, bạn cần có một phương pháp mới để tạo hình dạng.

Trong phiên này, chúng ta sẽ tìm hiểu về Custom Mesh API trong Jetpack XR SDK. API này cho phép bạn tạo hình học 3D theo quy trình ngay trong thời gian chạy. Chúng ta sẽ tìm hiểu quy trình công việc cốt lõi, từ việc xác định bố cục đỉnh đến việc kết xuất các đối tượng động một cách liền mạch trong không gian thực của người dùng.

Sau khi xem xong bài nói chuyện này, bạn sẽ có kỹ năng thực tế để xây dựng hình học 3D tuỳ chỉnh, có tính tương tác hoàn toàn bằng mã.

Tóm tắt

  • API xem xét nội bộ glTF trong Jetpack SceneCore hiển thị siêu dữ liệu mô hình trong thời gian chạy. Bạn có thể phát ảnh động được nhúng, biến đổi nút phụ hoặc ghi đè lưới bằng chất liệu và hoạ tiết.
  • Jetpack SceneCore bao gồm các API lưới để tạo hình học 3D tuỳ chỉnh trong Kotlin. Bốn khối dựng như VertexLayout và MeshBuffer xây dựng mô hình theo từng tam giác mà không cần công cụ 3D.
  • Lưới động hỗ trợ cập nhật theo thời gian thực khi người dùng vẽ đường dẫn hoặc tạo ảnh động cho các bề mặt. Việc chỉ cập nhật dữ liệu đỉnh đã thay đổi sẽ truyền trực tiếp các nội dung sửa đổi đến GPU mà không cần tạo lại lưới hoặc thực thể.
  • Gemini trong Android Studio có thể viết ảnh động đỉnh phức tạp và toán học chiếu sáng cho các API lưới. Một câu lệnh tạo ra mã hoạt động giúp tạo ảnh động cho lưới đỉnh và tính toán lại các pháp tuyến cho mỗi khung hình.

Tài nguyên

Gemini Notebook

Chủ đề

  • jetpack-xr
  • jetpack-scenecore
  • android-xr
  • hình học 3D
  • mesh-api
  • gltf

Bản chép lời

Giới thiệu về Android XR và SceneCore

  • Chào các bạn.

  • Tôi là Jay.

  • Còn tôi là Egor.

  • Chúng tôi là kỹ sư phần mềm làm việc về Jetpack XR tại Google.

  • XREAL Aura sắp ra mắt, nên chúng tôi muốn đảm bảo rằng bạn có mọi công cụ cần thiết để xây dựng trải nghiệm không gian tuyệt vời trên nền tảng này.

  • Nhưng điều tuyệt vời nhất là bạn thậm chí không cần phần cứng XR để bắt đầu.

  • Tất cả tính năng mà chúng tôi sẽ giới thiệu đều có trong Trình mô phỏng Android XR trong Android Studio.

  • Mục tiêu của chúng tôi là cho bạn thấy rằng với thư viện Jetpack SceneCore, bạn không cần có kiến thức chuyên sâu về 3D để xây dựng trải nghiệm không gian phong phú trên nền tảng này.

  • Mọi thứ bạn thấy đều đang chạy trong một ứng dụng Android, được viết bằng Kotlin đặc trưng ngay trong Android Studio.

  • Gần đây, chúng tôi đã nâng cấp Jetpack SceneCore lên phiên bản ổn định. Hôm nay, chúng tôi sẽ giới thiệu một số điểm cải tiến thú vị của API mô hình 3D, cũng như API lưới hoàn toàn mới, cho phép bạn tạo hình học 3D tuỳ chỉnh một cách nhanh chóng.

Làm việc với mô hình 3D glTF

  • Nhưng hãy bắt đầu với mô hình 3D.

  • Khi nói đến mô hình 3D, điều đầu tiên chúng ta cần là định dạng tệp.

  • Tiêu chuẩn mở là một phần cốt lõi của Android XR.

  • Vì vậy, chúng tôi đã dồn toàn lực cho định dạng glTF của Khronos Group.

  • Nếu không quen thuộc với thế giới 3D, bạn có thể thắc mắc "glTF là gì?"

  • Bạn có thể coi đây là một tệp JPEG, nhưng dành cho mô hình 3D.

  • Cấu trúc thành phần glTF cung cấp dữ liệu cho những nội dung như ảnh động, nút phụ và lưới.

    {
        "asset" : {...},
        "nodes" : [
            {
                "name" : "Right_Arm",
                "rotation" : [...],
                "scale" : [...],
                "translation" : [...],
            },
        ],
        "animations" : [
            {
                "name" : "Wave",
            },
        ],
        "meshes" : [
            {
                "name" : "Torso",
                "primitives" : [ ... ],
            },
        ],
    
        // Way way more!
    
    }
  • Nhưng trong phạm vi của bài nói chuyện này, bạn có thể coi nút phụ là các phần của mô hình, mỗi phần có vị trí và độ xoay riêng.

  • Lưới là các bề mặt mà bạn nhìn thấy, còn ảnh động là chuyển động của các nút phụ và lưới mà tác giả của thành phần đã tạo sẵn.

  • Chúng ta tìm hiểu tiếp nhé.

  • Bây giờ, có thể bạn đang thắc mắc: "Tôi có thể lấy mô hình glTF ở đâu?"

  • Ngoài nhiều mô hình có sẵn, bạn cũng có thể tạo mô hình của riêng mình.

  • Giờ đây, khi đã nắm được thông tin cơ bản về định dạng glTF, hãy tiếp tục và sử dụng định dạng này trong ứng dụng.

  • Chúng ta sẽ bắt đầu bằng một dự án Android XR cơ bản sử dụng Jetpack SceneCore.

  • Bạn có thể thấy một bảng điều khiển không gian có một hoạt động nổi trong không gian của người dùng.

  • Bạn có thể chọn, di chuyển và đổi kích thước cửa sổ theo ý muốn.

  • Mặc dù bài nói chuyện này chỉ xoay quanh 3D, nhưng bạn cũng nên tìm hiểu phần còn lại của Jetpack XR SDK.

  • Được rồi, bảng điều khiển không gian rất thú vị, nhưng chúng ta muốn có thêm nhiều điều kỳ diệu hơn.

  • Còn gì tuyệt vời hơn khi thêm mô hình 3D để tạo nên điều kỳ diệu?

  • Thật tình cờ, nhà thiết kế của chúng tôi đã tạo mô hình glTF này cho một chiếc ô tô.

    // 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
    )
  • Họ thậm chí còn nhúng ảnh động vào đó, chúng ta sẽ sử dụng ảnh động này sau.

  • Bằng cách sử dụng SceneCore, tôi có thể dùng API mô hình 3D để tải mô hình đó ngay vào cảnh.

Ảnh động mô hình và thao tác với nút phụ

  • Nhưng hãy làm cho chiếc xe trông sống động hơn một chút.

  • Bạn có nhớ tất cả siêu dữ liệu phong phú bên trong glTF không?

  • Với API xem xét nội bộ glTF mới, giờ đây, bạn có thể truy cập vào API này, bắt đầu bằng ảnh động.

    // 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()
  • Chỉ với vài dòng mã, tôi có thể phát riêng lẻ bất kỳ ảnh động glTF nào được tích hợp vào mô hình ô tô, chẳng hạn như mở và đóng cửa, mui xe hoặc cốp xe.

  • Tuy nhiên, ảnh động không phải là siêu dữ liệu duy nhất mà bạn có thể truy cập.

  • API xem xét nội bộ glTF cũng hiển thị các nút phụ trong mô hình dưới dạng GltfModelNode, cho phép bạn di chuyển, xoay và điều chỉnh tỷ lệ theo ý muốn.

    // 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) }
    }
  • Ví dụ: tại đây, tôi có thể cập nhật riêng vị trí và độ xoay của bánh xe khi xe chạy.

  • Và để làm rõ, những gì bạn đang thấy không phải là ảnh động glTF được nhúng.

  • Tại đây, tôi đang xoay và điều khiển từng bánh xe trong thời gian chạy.

Áp dụng chất liệu và hoạ tiết

  • Một phần siêu dữ liệu cuối cùng mà bạn có thể truy cập là bất kỳ lưới nào trong mô hình glTF.

  • Bây giờ, Egor sẽ tìm hiểu sâu hơn về lưới trong giây lát.

  • Nhưng bây giờ, hãy tạo kiểu cho chiếc xe của chúng ta.

  • Bằng cách sử dụng API kết cấu, chúng ta có thể tải kết cấu hình ảnh PNG, chẳng hạn như mẫu kim loại.

    // 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)
  • Sau đó, bằng API Material, chúng ta có thể đưa trực tiếp vào KhronosPbrMaterial.

  • Nếu mới làm quen với 3D, bạn có thể hình dung lưới như một chiếc bàn, chất liệu như một chiếc khăn trải bàn và hoạ tiết như hoa văn in trên chiếc khăn đó.

  • Nhưng ở đây, thay vì mặt bàn, chúng ta sử dụng các đối tượng Material đã tạo để ghi đè các lưới liên kết với các nút phụ của mui xe và cửa xe.

  • Chỉ với vài dòng mã, chúng ta đã biến đổi giao diện của chiếc ô tô, một lần nữa, tất cả đều trong thời gian chạy.

  • Đó là một số ý tưởng hay, nhưng sau đây là một ý tưởng thậm chí còn hay hơn.

  • Sẽ tuyệt vời biết bao nếu bạn có thể vẽ một đường đua cho ô tô chạy trên đó?

  • Trong trường hợp này, mô hình glTF tạo sẵn không phù hợp.

  • Thay vào đó, chúng ta cần có khả năng tạo hình học 3D một cách nhanh chóng.

  • Vì vậy, tôi sẽ nhường lời cho Egor để giới thiệu cho các bạn về các API lưới Jetpack SceneCore hoàn toàn mới.

Giới thiệu về API lưới

  • Cảm ơn Jay.

  • Hãy làm sao để người dùng có thể vẽ đường đua bằng ngón tay trong phòng khách của họ.

  • Đường đua không tồn tại cho đến khi được vẽ, vì vậy không ai có thể lập mô hình trước.

  • Chúng ta sẽ xây dựng hình này trong mã, từng tam giác một.

  • Lưới bắt đầu bằng các đỉnh, điểm trong không gian.

    // 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
    )
  • Mỗi đỉnh mang các thuộc tính như vị trí, pháp tuyến cho ánh sáng, màu sắc, toạ độ hoạ tiết.

  • Chỉ mục kết nối các thông tin.

  • Cứ 3 điểm tạo thành một tam giác.

  • Hai tam giác tạo thành một hình vuông, sáu chỉ mục, nhưng chỉ có bốn đỉnh, vì chúng có chung hai đỉnh.

  • Tiếp tục và bạn sẽ nhận được một khối lập phương.

  • Mọi mô hình 3D, bao gồm cả xe của Jay, đều được xây dựng theo cùng một cách, chỉ là có nhiều hình tam giác hơn.

Các thành phần của API lưới

  • Mesh API liên kết với mô hình đó bằng 4 thành phần: VertexLayout, MeshBuffer, CustomMesh và MeshEntity.

  • Hãy cùng tìm hiểu từng bước.

  • Đầu tiên là 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()
  • Dữ liệu đỉnh chỉ là các byte trong ByteBuffer và mỗi lưới sẽ đóng gói các byte này theo cách khác nhau.

  • VertexLayout cho biết cách đọc dữ liệu của bạn: thuộc tính nào, loại nào và vị trí của từng thuộc tính.

  • Ở đây, đó là một vị trí, một pháp tuyến và UV, toạ độ kết cấu, tổng cộng 32 byte.

  • Các thuộc tính cũng có thể nằm trong các vùng đệm riêng biệt, nhờ đó bạn chỉ có thể cập nhật những thuộc tính thay đổi.

  • Thứ hai, MeshBuffer, một vùng chứa dữ liệu thô.

    // 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)
  • Bạn cung cấp cho nó bố cục, byte đỉnh và chỉ mục 32 bit.

  • Dữ liệu được sao chép ngay vào GPU để bạn có thể sử dụng lại hoặc giải phóng ByteBuffer.

  • Theo mặc định, MeshBuffer là tĩnh.

  • Nó được đặt một lần và tối ưu hoá để hiển thị nhanh.

  • Và nếu bạn cần thay đổi dung lượng trong thời gian chạy, createDynamic sẽ phân bổ một dung lượng cố định mà bạn có thể cập nhật sau.

  • Thứ ba, CustomMesh, một khung hiển thị vào 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)
  • Thao tác này không sao chép dữ liệu.

  • Nó cho biết dải chỉ mục nào cần vẽ và cách vẽ, hình tam giác hoặc dải tam giác.

  • Tại đây, nhựa đường và vạch kẻ đường nằm trong cùng một MeshBuffer.

  • Nhiều lưới tuỳ chỉnh có thể dùng chung một MeshBuffer và khi bạn không cần chia sẻ, BuilderFromMeshData sẽ tạo lưới và vùng đệm trong một bước.

  • Cuối cùng, MeshEntity đặt lưới vào cảnh.

    // 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))
  • Lưới là gì.

  • Thực thể là nơi chốn.

  • Giống như bảng điều khiển và xe của Jay, đây là một thực thể nên có vị trí và thực thể mẹ, tức là đối tượng mà nó được gắn vào.

  • Khi bạn di chuyển đường chính, đường phụ cũng sẽ di chuyển theo.

  • Bạn cũng cung cấp một chất liệu cho mỗi tập hợp con để cùng một lưới có thể xuất hiện với chất liệu khác nhau trong các thực thể khác nhau.

Xây dựng lưới tuỳ chỉnh và linh động

  • Bây giờ, hãy kết hợp tất cả lại và xây dựng lưới, lưới đường.

    // 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)
  • Chúng ta xác định đỉnh: vị trí, pháp tuyến và UV, toạ độ kết cấu.

  • Một hàm trợ giúp nhỏ biến đường dẫn của người dùng thành một dải băng.

  • Đối với mỗi điểm trên đường dẫn, nó sẽ thêm hai đỉnh ở mỗi bên và hai hình tam giác kết nối chúng với cặp tiếp theo.

  • Đây là Kotlin thuần tuý, ghi byte vào ByteBuffer.

  • Bạn không cần bất kỳ loại công cụ 3D nào.

  • BuilderFromMeshData, phím tắt mà chúng ta vừa thấy, tạo lưới và MeshBuffer, đồng thời tính toán các giới hạn cho chúng ta.

  • Cuối cùng, MeshEntity.create đặt roadMesh vào cảnh có chất liệu nhựa đường.

  • Nhưng chúng ta muốn đường đua phát triển trực tiếp khi người dùng vẽ đường đua đó.

  • Để làm được điều đó, chúng ta sử dụng lưới động.

    // 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)
  • Điểm khác biệt duy nhất là chúng ta cung cấp cho trình tạo dung lượng tối đa ngay từ đầu để có thể phân bổ vùng đệm GPU.

  • Sau đó, khi người dùng vẽ, updateVertexData và updateIndexData chỉ tải các phân đoạn mới lên chứ không tải toàn bộ lưới.

  • Một lưu ý: trình kết xuất sử dụng các giới hạn để bỏ qua mọi thứ ngoài màn hình, vì vậy, hãy đặt các giới hạn để bao quát mọi thứ mà đường đua có thể trở thành, chẳng hạn như khu vực chơi ở đây.

  • Bạn có thể nhận thấy rằng API không phải là phần khó ở đây.

  • Phần khó là toán học đỉnh, pháp tuyến, toạ độ kết cấu, thứ tự tam giác.

  • Đó chính xác là loại mã mà AI rất giỏi viết.

  • Nhưng giả sử chúng ta muốn tiến thêm một bước nữa.

  • Giờ thì tôi muốn có một lá cờ ca rô tung bay trong gió tại điểm đến.

Tạo lưới ảnh động bằng Gemini

  • Phép toán tạo ảnh động đỉnh là phần khó, nên tôi đã hỏi Gemini.

  • Trong Android Studio, tôi đã đưa ra một câu lệnh: "Sử dụng Jetpack SceneCore Mesh API, hãy tạo một lưới cờ 20x12 có UV bàn cờ, tạo ảnh động cho sóng gió ở mỗi khung hình bằng updateVertexData và tính toán lại các pháp tuyến để ánh sáng đi theo các nếp gấp".

  • Vài giây sau, tôi đã có mã hoạt động được.

  • Hãy xem những gì nó đã làm.

  • Công cụ này đã tạo một lưới cờ dưới dạng lưới gồm 20 x 12 ô, mỗi ô có hai hình tam giác và đưa ra lựa chọn thông minh về bố cục.

    // 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()
  • Vị trí và pháp tuyến được đặt trong Vùng đệm 0 và được cập nhật mỗi khung hình.

  • Toạ độ kết cấu được đặt trong Bộ đệm 1 và không bao giờ thay đổi.

  • Lưới này là động và các ranh giới được đặt để bao phủ toàn bộ chuyển động của lá cờ.

  • Mỗi khung hình, mỗi đỉnh đều có một sóng hình sin phát triển về phía cạnh tự do, sao cho phía cột giữ nguyên.

    // 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))
    }
  • Các pháp tuyến được tính toán lại từ các vị trí lân cận, giúp các nếp gấp bắt sáng.

  • updateVertexData tải khoảng 6,5 KB dữ liệu lên GPU mỗi khung hình.

  • Chúng tôi không tạo lại lưới hoặc thực thể.

  • Giờ hãy xem tất cả cùng nhau.

  • Chúng ta đặt mô hình ô tô lên bàn, sau đó dùng ARCore để vẽ đường đua.

  • Lưới cờ mà chúng ta vừa tạo sẽ xuất hiện tại điểm đến.

  • Chiếc ô tô chạy dọc theo đường đua và chúng tôi sử dụng API Kiểm tra glTF để điều khiển và quay bánh xe của ô tô.

  • Lưới xuất hiện cùng với vật liệu nhựa đường, nhưng chúng ta cũng có thể sử dụng API lưới cùng với API vật liệu để thay đổi giao diện của đường, như bạn có thể thấy trong video.

  • Đó là một công cụ mạnh mẽ ngay trong tầm tay bạn.

Tóm tắt và bắt đầu

  • Hôm nay, chúng ta đã thảo luận về nhiều cách để chuyển ứng dụng từ 2D sang không gian.

  • Với glTF Introspection API, giờ đây, bạn có thể khai thác trực tiếp siêu dữ liệu bên trong của mô hình glTF, kích hoạt ảnh động được nhúng, biến đổi từng nút phụ và thậm chí hoán đổi chất liệu để thay đổi giao diện của mô hình trong thời gian chạy.

  • Sau đó, chúng tôi nhận thấy rằng khi các mô hình 3D tạo sẵn không đủ, chúng tôi có thể tận dụng Mesh API hoàn toàn mới để tạo hình học tuỳ chỉnh.

  • Dù cần tạo hình dạng tĩnh từ đầu hay truyền trực tuyến hình dạng động theo thời gian thực, bạn hiện đều có các công cụ để thực hiện.

  • Và như Egor vừa trình bày, khi kết hợp các khối xây dựng đó với Gemini, bạn thậm chí không cần phải tự tính toán.

  • Bây giờ, tôi sẽ nhắc lại một điều đã nói trước đó: mọi thứ bạn thấy đều đang chạy trong một ứng dụng Android, được viết bằng Kotlin đặc trưng, ngay trong Android Studio.

  • Tôi thấy nhiều người trong số các bạn hôm nay có mang theo máy tính xách tay. Tôi hy vọng rằng tôi và Egor đã truyền đủ cảm hứng để các bạn mở Android Studio và khởi động Trình mô phỏng Android XR ngay hôm nay.

  • Hãy thoả sức tưởng tượng và sử dụng các kỹ năng Android hiện có, cùng với những gì chúng tôi đã trình bày hôm nay, để xây dựng trải nghiệm XR tuyệt vời.

  • Thay mặt cho Egor và bản thân tôi, xin cảm ơn các bạn đã tham gia cùng chúng tôi hôm nay và chúc các bạn có một khoảng thời gian tuyệt vời tại Hội nghị Nhà phát triển Android.

Mã

Đoạn mã

Đoạn mã JSON ở phút 01:22 trong video "Làm việc với mô hình 3D glTF"
{
    "asset" : {...},
    "nodes" : [
        {
            "name" : "Right_Arm",
            "rotation" : [...],
            "scale" : [...],
            "translation" : [...],
        },
    ],
    "animations" : [
        {
            "name" : "Wave",
        },
    ],
    "meshes" : [
        {
            "name" : "Torso",
            "primitives" : [ ... ],
        },
    ],

    // Way way more!

}
Đoạn mã Kotlin ở phút 02:28 trong video "Làm việc với mô hình 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
)
Đoạn mã Kotlin ở phút 02:50 trong video "Ảnh động mô hình và thao tác với nút phụ"
// 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()
Đoạn mã Kotlin ở phút 03:12 trong video "Ảnh động mô hình và thao tác với nút phụ"
// 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) }
}
Đoạn mã Kotlin tại 03:51 trong phần "Áp dụng chất liệu và hoạ tiết"
// 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)
Đoạn mã Kotlin ở phút 05:05 trong video "Giới thiệu về API lưới"
// 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
)
Đoạn mã Kotlin ở phút 05:45 trong video "Building blocks of the mesh API" (Thành phần của API lưới)
// 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()
Đoạn mã Kotlin ở phút 06:10 trong video "Building blocks of the mesh API" (Thành phần của API lưới)
// 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)
Đoạn mã Kotlin ở phút 06:37 trong video "Building blocks of the mesh API" (Các khối dựng của API lưới)
// 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)
Đoạn mã Kotlin ở phút 07:04 trong video "Building blocks of the mesh API" (Thành phần của API lưới)
// 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))
Đoạn mã Kotlin ở phút 07:31 trong video "Tạo lưới tuỳ chỉnh và lưới động"
// 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)
Đoạn mã Kotlin ở phút 08:18 trong video "Tạo lưới tuỳ chỉnh và lưới động"
// 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)
Đoạn mã Kotlin ở phút 09:34 trong video "Tạo lưới ảnh động bằng 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()
Đoạn mã Kotlin ở phút 09:56 trong video "Tạo lưới ảnh động bằng 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))
}