Android XR

ก้าวข้ามขีดจำกัดของ 2 มิติด้วยเรขาคณิต 3 มิติที่กำหนดเองใน Jetpack XR SDK

ลำโพง

  • Egor Yusov

    วิศวกรซอฟต์แวร์

  • Jay Abi-Saad

    วิศวกรซอฟต์แวร์

เกี่ยวกับ

เกี่ยวกับการเสวนา

การเปลี่ยนแอปพลิเคชัน Android จากหน้าจอ 2 มิติแบบเดิมไปเป็นสภาพแวดล้อมเชิงพื้นที่ต้องใช้แนวทางใหม่ในการสร้างรูปทรง

ในเซสชันนี้ เราจะสำรวจ Custom Mesh API ใน Jetpack XR SDK ซึ่งช่วยให้คุณสร้างเรขาคณิต 3 มิติแบบกระบวนการได้โดยตรงขณะรันไทม์ เราจะอธิบายเวิร์กโฟลว์หลัก ตั้งแต่การกำหนดเลย์เอาต์เวอร์เท็กซ์ไปจนถึงการแสดงผลออบเจ็กต์แบบไดนามิกอย่างราบรื่นภายในพื้นที่จริงของผู้ใช้

เมื่อจบการพูดคุยนี้ คุณจะมีทักษะที่นำไปใช้ได้จริงในการสร้างเรขาคณิต 3 มิติแบบอินเทอร์แอกทีฟที่กำหนดเองจากโค้ดทั้งหมด

สรุป

  • glTF introspection API ใน Jetpack SceneCore จะแสดงข้อมูลเมตาของโมเดลที่รันไทม์ คุณสามารถเล่นภาพเคลื่อนไหวที่ฝังไว้ เปลี่ยนรูปแบบโหนดย่อย หรือลบล้าง Mesh ด้วยวัสดุและพื้นผิวได้
  • Jetpack SceneCore มี Mesh API สำหรับสร้างเรขาคณิต 3 มิติที่กำหนดเองใน Kotlin Building Block 4 รายการ เช่น VertexLayout และ MeshBuffer จะสร้างโมเดลทีละสามเหลี่ยมโดยไม่ต้องใช้เครื่องมือ 3 มิติ
  • เมชแบบไดนามิกรองรับการอัปเดตแบบเรียลไทม์เมื่อผู้ใช้วาดเส้นทางหรือสร้างภาพเคลื่อนไหวบนพื้นผิว การอัปเดตเฉพาะข้อมูลจุดยอดที่เปลี่ยนแปลงจะสตรีมการแก้ไขไปยัง GPU โดยตรงโดยไม่ต้องสร้าง Mesh หรือเอนทิตีใหม่
  • Gemini ใน Android Studio สามารถเขียนแอนิเมชันเวอร์เท็กซ์ที่ซับซ้อนและคณิตศาสตร์การจัดแสงสำหรับ Mesh API ได้ พรอมต์เดียวสร้างโค้ดที่ใช้งานได้ซึ่งทำให้ตารางกริดเวอร์เท็กซ์เคลื่อนไหวและคำนวณเวกเตอร์แนวฉากใหม่ทุกเฟรม

แหล่งข้อมูล

Gemini Notebook

หัวข้อ

  • jetpack-xr
  • jetpack-scenecore
  • android-xr
  • เรขาคณิต 3 มิติ
  • mesh-api
  • gltf

ข้อความถอดเสียง

ข้อมูลเบื้องต้นเกี่ยวกับ Android XR และ SceneCore

  • สวัสดีทุกคน

  • ฉันชื่อ Jay

  • และผม Egor

  • และเราเป็นวิศวกรซอฟต์แวร์ที่ทำงานเกี่ยวกับ Jetpack XR ที่ Google

  • XREAL Aura กำลังจะเปิดตัวในเร็วๆ นี้ เราจึงต้องการให้คุณมีเครื่องมือทั้งหมดที่จำเป็นสำหรับการสร้างประสบการณ์เชิงพื้นที่ที่ยอดเยี่ยมบนแพลตฟอร์ม

  • แต่สิ่งที่ดีที่สุดคือคุณไม่จำเป็นต้องมีฮาร์ดแวร์ XR เพื่อเริ่มต้นใช้งานด้วยซ้ำ

  • ฟีเจอร์ทั้งหมดที่เราจะแสดงพร้อมให้ใช้งานแล้ววันนี้ในโปรแกรมจำลอง Android XR ใน Android Studio

  • เป้าหมายของเราคือการแสดงให้คุณเห็นว่าเมื่อใช้ไลบรารี Jetpack SceneCore คุณไม่จำเป็นต้องมีความรู้เชิงลึกเกี่ยวกับ 3 มิติเพื่อสร้างประสบการณ์เชิงพื้นที่ที่สมบูรณ์บนแพลตฟอร์ม

  • ทุกสิ่งที่คุณจะได้เห็นจะทำงานในแอป Android ซึ่งเขียนด้วย Kotlin ที่เป็นสำนวนใน Android Studio

  • เมื่อเร็วๆ นี้เราได้โปรโมต Jetpack SceneCore เป็นเวอร์ชันเสถียร และวันนี้เราจะพูดถึงความก้าวหน้าที่น่าสนใจของ API โมเดล 3 มิติ รวมถึง API ของ Mesh ใหม่ล่าสุด ซึ่งช่วยให้คุณสร้างเรขาคณิต 3 มิติที่กำหนดเองได้ทันที

การทำงานกับโมเดล 3 มิติ glTF

  • แต่เรามาเริ่มกันที่โมเดล 3 มิติก่อน

  • เมื่อพูดถึงโมเดล 3 มิติ สิ่งแรกที่เราต้องการคือรูปแบบไฟล์

  • มาตรฐานแบบเปิดเป็นส่วนสำคัญของ Android XR

  • ดังนั้น เราจึงใช้รูปแบบ glTF จาก Khronos Group อย่างเต็มที่

  • หากคุณไม่ได้อยู่ในวงการ 3 มิติ คุณอาจสงสัยว่า "glTF คืออะไร"

  • คุณอาจคิดว่าไฟล์นี้เป็นเหมือนไฟล์ JPEG แต่ใช้สำหรับโมเดล 3 มิติ

  • โครงสร้างชิ้นงาน glTF จะให้ข้อมูลสำหรับสิ่งต่างๆ เช่น ภาพเคลื่อนไหว โหนดย่อย และ Mesh

    {
        "asset" : {...},
        "nodes" : [
            {
                "name" : "Right_Arm",
                "rotation" : [...],
                "scale" : [...],
                "translation" : [...],
            },
        ],
        "animations" : [
            {
                "name" : "Wave",
            },
        ],
        "meshes" : [
            {
                "name" : "Torso",
                "primitives" : [ ... ],
            },
        ],
    
        // Way way more!
    
    }
  • แต่สำหรับการพูดคุยนี้ คุณสามารถคิดว่าโหนดรองเป็นส่วนหนึ่งของโมเดล ซึ่งแต่ละโหนดมีตำแหน่งและการหมุนของตัวเอง

  • เมชคือพื้นผิวที่คุณเห็น และภาพเคลื่อนไหวคือการเคลื่อนไหวของโหนดย่อยและเมชที่ผู้เขียนชิ้นงานอบไว้ล่วงหน้า

  • เยี่ยมเลย

  • ตอนนี้คุณอาจสงสัยว่า "ฉันจะหาโมเดล glTF ได้จากที่ไหน"

  • นอกจากโมเดลที่มีอยู่มากมายแล้ว คุณยังสร้างโมเดลของคุณเองได้ด้วย

  • ตอนนี้เราได้พูดถึงพื้นฐานของรูปแบบ glTF แล้ว มาลองใช้ในแอปกันเลย

  • เราจะเริ่มด้วยโปรเจ็กต์ Android XR พื้นฐานที่ใช้ Jetpack SceneCore

  • คุณจะเห็นแผงเชิงพื้นที่ที่มีกิจกรรมลอยอยู่ในพื้นที่ของผู้ใช้

  • คุณสามารถจับ ย้าย และปรับขนาดได้ตามต้องการ

  • และแม้ว่าการพูดคุยนี้จะเกี่ยวกับ 3 มิติทั้งหมด แต่อย่าลืมดู Jetpack XR SDK ที่เหลือด้วย

  • เอาล่ะ แผงเชิงพื้นที่ก็สนุกดี แต่เราต้องการความมหัศจรรย์มากกว่านี้

  • และจะมีวิธีไหนดีไปกว่าการเพิ่มความมหัศจรรย์ด้วยการใส่โมเดล 3 มิติ

  • พอดีว่านักออกแบบของเราสร้างโมเดล 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 โมเดล 3 มิติเพื่อโหลดโมเดลลงในฉากได้โดยตรง

ภาพเคลื่อนไหวของโมเดลและการจัดการโหนดย่อย

  • แต่มาทำให้รถดูมีชีวิตชีวาขึ้นอีกสักหน่อย

  • ยังจำข้อมูลเมตาที่สมบูรณ์ทั้งหมดใน 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 แต่ละรายการที่สร้างไว้ในโมเดลรถได้ เช่น การเปิดและปิดประตู ฝากระโปรงหน้า หรือฝากระโปรงท้าย

  • แต่ภาพเคลื่อนไหวไม่ใช่ข้อมูลเมตาเพียงอย่างเดียวที่คุณเข้าถึงได้

  • นอกจากนี้ glTF Introspection API ยังแสดงโหนดย่อยในโมเดลเป็น 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 ที่ฝังอยู่

  • ในที่นี้ ฉันจะหมุนและบังคับล้อแต่ละล้อพร้อมกันในรันไทม์

การใช้วัสดุและพื้นผิว

  • ข้อมูลเมตาสุดท้ายที่คุณเข้าถึงได้คือ Mesh ในโมเดล glTF

  • ต่อไป Egor จะมาเจาะลึกเรื่อง Mesh กันในอีกสักครู่

  • แต่ตอนนี้มาแต่งรถกันก่อน

  • เมื่อใช้ 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)
  • จากนั้น เราจะใช้ API ของวัสดุเพื่อเสียบเข้ากับ KhronosPbrMaterial ได้โดยตรง

  • หากคุณเพิ่งเริ่มใช้ 3D ให้คิดว่าเมชเป็นโต๊ะ วัสดุเป็นผ้าปูโต๊ะ และพื้นผิวเป็นลวดลายที่พิมพ์บนผ้าปูโต๊ะนั้น

  • แต่ในกรณีนี้ เราจะใช้วัตถุ Material ที่สร้างขึ้นเพื่อลบล้าง Mesh ที่เชื่อมโยงกับโหนดย่อยของฝากระโปรงและประตูรถแทนที่จะเป็นโต๊ะ

  • โค้ดเพียงไม่กี่บรรทัดช่วยให้เราเปลี่ยนรูปลักษณ์ของรถได้อีกครั้ง ทั้งหมดนี้ทำได้ในรันไทม์

  • นั่นเป็นไอเดียที่เจ๋งมาก แต่เรามีไอเดียที่ดียิ่งกว่านั้น

  • จะดีแค่ไหนถ้าเราวาดสนามแข่งให้รถวิ่งได้

  • ในกรณีนี้ โมเดล glTF ที่สร้างไว้ล่วงหน้าจึงไม่เหมาะสม

  • แต่เราต้องการความสามารถในการสร้างเรขาคณิต 3 มิติได้ทันที

  • ผมจะส่งต่อให้ Egor เพื่อบอกคุณเกี่ยวกับ Mesh API ของ Jetpack SceneCore ใหม่ล่าสุด

ข้อมูลเบื้องต้นเกี่ยวกับ Mesh API

  • ขอบคุณ Jay

  • มาทำให้ผู้ใช้สามารถวาดสนามแข่งรถใน... ด้วยนิ้วมือในห้องนั่งเล่นได้

  • สนามแข่งจะไม่มีอยู่จนกว่าจะมีการวาดขึ้น ดังนั้นจึงไม่มีใครสามารถจำลองสนามแข่งล่วงหน้าได้

  • เราจะสร้างด้วยโค้ดทีละสามเหลี่ยม

  • เมชเริ่มต้นด้วยจุดยอด ซึ่งเป็นจุดในพื้นที่ว่าง

    // 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
    )
  • แต่ละจุดจะมีแอตทริบิวต์ เช่น ตำแหน่ง, เวกเตอร์ปกติสำหรับการจัดแสง, สี, พิกัดพื้นผิว

  • ดัชนีจะเชื่อมโยงข้อมูลต่างๆ เข้าด้วยกัน

  • ทุกๆ 3 จุดจะสร้างสามเหลี่ยมได้ 1 รูป

  • สามเหลี่ยม 2 รูปประกอบกันเป็นสี่เหลี่ยมจัตุรัส มีดัชนี 6 ตัว แต่มีจุดยอดเพียง 4 จุด เนื่องจากใช้จุดยอดร่วมกัน 2 จุด

  • เล่นต่อไปแล้วคุณจะได้ลูกบาศก์

  • โมเดล 3 มิติทุกโมเดล รวมถึงรถของเจย์ สร้างขึ้นในลักษณะเดียวกัน เพียงแต่มีสามเหลี่ยมมากกว่า

องค์ประกอบของ Mesh API

  • Mesh API จะแมปกับสถาปัตยกรรมดังกล่าวโดยใช้องค์ประกอบ 4 อย่าง ได้แก่ 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()
  • ข้อมูล Vertex เป็นเพียงไบต์ใน ByteBuffer และทุก Mesh จะแพ็กข้อมูลแตกต่างกัน

  • VertexLayout จะบอกวิธีอ่านข้อมูลของคุณ เช่น แอตทริบิวต์ใด ประเภทใด และแต่ละแอตทริบิวต์อยู่ที่ใด

  • ในที่นี้คือตำแหน่ง เวกเตอร์แนวฉาก และ UV ซึ่งเป็นพิกัดพื้นผิว รวมทั้งหมด 32 ไบต์

  • นอกจากนี้ แอตทริบิวต์ยังอยู่ในบัฟเฟอร์แยกต่างหากได้ด้วย เพื่อให้คุณอัปเดตเฉพาะแอตทริบิวต์ที่มีการเปลี่ยนแปลง

  • ประการที่ 2 คือ 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 บิต

  • ระบบจะคัดลอกข้อมูลไปยัง GPU ทันที เพื่อให้คุณนำ ByteBuffer กลับมาใช้ใหม่หรือปล่อยได้

  • โดยค่าเริ่มต้น MeshBuffer จะเป็นแบบคงที่

  • โดยจะตั้งค่าเพียงครั้งเดียวและเพิ่มประสิทธิภาพเพื่อการแสดงผลที่รวดเร็ว

  • และหากคุณต้องการเปลี่ยนที่รันไทม์ createDynamic จะจัดสรรความจุคงที่ซึ่งคุณอาจอัปเดตในภายหลัง

  • ประการที่ 3 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 จะวาง 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))
  • ตาข่ายคืออะไร

  • เอนทิตีคือสถานที่

  • เช่นเดียวกับแผงและรถของ Jay ซึ่งเป็นเอนทิตี จึงมีตำแหน่งและรายการหลัก ซึ่งเป็นสิ่งที่ติดอยู่

  • เมื่อย้ายถนนหลัก ถนนที่เชื่อมต่อก็จะย้ายตามไปด้วย

  • นอกจากนี้ คุณยังระบุวัสดุ 1 รายการต่อชุดย่อย เพื่อให้เมชเดียวกันปรากฏพร้อมกับวัสดุที่แตกต่างกันในเอนทิตีที่ต่างกันได้

การสร้าง Mesh ที่กำหนดเองและแบบไดนามิก

  • ตอนนี้เรามาประกอบทุกอย่างเข้าด้วยกันและสร้าง Mesh ซึ่งก็คือ 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)
  • เรากำหนดจุดยอดมุม: ตำแหน่ง, เวกเตอร์ปกติ และ UV, พิกัดพื้นผิว

  • ฟังก์ชันตัวช่วยขนาดเล็กจะเปลี่ยนเส้นทางของผู้ใช้ให้เป็นริบบิ้น

  • สำหรับแต่ละจุดบนเส้นทาง ระบบจะเพิ่มจุดยอด 2 จุดในแต่ละด้านและสามเหลี่ยม 2 รูปที่เชื่อมต่อจุดยอดเหล่านั้นกับคู่ถัดไป

  • ซึ่งเป็น Kotlin ธรรมดาที่เขียนไบต์ลงใน ByteBuffer

  • คุณไม่จำเป็นต้องใช้เครื่องมือ 3 มิติใดๆ

  • BuilderFromMeshData ซึ่งเป็นทางลัดที่เราเพิ่งเห็นจะสร้าง Mesh และ 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)
  • ข้อแตกต่างเพียงอย่างเดียวคือเราให้ความจุสูงสุดแก่ Builder ล่วงหน้าเพื่อให้สามารถจัดสรรบัฟเฟอร์ GPU ได้

  • จากนั้นเมื่อผู้ใช้วาด updateVertexData และ updateIndexData จะอัปโหลดเฉพาะส่วนใหม่ ไม่ใช่ทั้ง Mesh

  • ข้อควรระวัง: ตัวแสดงผลจะใช้ขอบเขตเพื่อข้ามสิ่งต่างๆ ที่อยู่นอกหน้าจอ ดังนั้นให้ตั้งค่าขอบเขตให้ครอบคลุมทุกอย่างที่แทร็กอาจกลายเป็น เช่น พื้นที่เล่นที่นี่

  • คุณอาจสังเกตเห็นว่า API ไม่ใช่ส่วนที่ยากในที่นี้

  • ส่วนที่ยากคือคณิตศาสตร์จุดยอด, เวกเตอร์ปกติ, พิกัดพื้นผิว, ลำดับสามเหลี่ยม

  • ซึ่งเป็นโค้ดประเภทที่ AI เขียนได้ดีมาก

  • แต่สมมติว่าเราต้องการยกระดับไปอีกขั้น

  • ตอนนี้ฉันอยากได้ธงตาหมากรุกที่โบกสะบัดตามลมที่จุดหมายปลายทาง

การสร้างเมชแบบเคลื่อนไหวด้วย Gemini

  • คณิตศาสตร์แอนิเมชันเวอร์เท็กซ์เป็นส่วนที่ยาก ฉันจึงขอความช่วยเหลือจาก Gemini

  • ใน Android Studio ฉันให้พรอมต์เดียวว่า "ใช้ Jetpack SceneCore Mesh API เพื่อสร้างตารางธงขนาด 20x12 ที่มี UV ลายตารางหมากรุก สร้างภาพเคลื่อนไหวคลื่นลมทุกเฟรมด้วย updateVertexData และคำนวณเวกเตอร์แนวฉากใหม่เพื่อให้แสงส่องตามรอยพับ"

  • ไม่กี่วินาทีต่อมา ฉันก็ได้โค้ดที่ใช้งานได้

  • มาดูสิ่งที่เกิดขึ้นกัน

  • โดยสร้างตาข่ายธงเป็นตารางกริดขนาด 20 คูณ 12 เซลล์ โดยมีสามเหลี่ยม 2 รูปต่อเซลล์ และเลือกเลย์เอาต์ได้อย่างชาญฉลาด

    // 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 KB ไปยัง GPU ในแต่ละเฟรม

  • เราจะไม่สร้าง Mesh หรือเอนทิตีขึ้นมาใหม่

  • ทีนี้มาดูภาพรวมกัน

  • เราวางโมเดลรถบนโต๊ะ แล้วใช้ ARCore เพื่อวาดแทร็ก

  • Mesh ของธงที่เราเพิ่งสร้างจะปรากฏที่ปลายทาง

  • รถจะขับไปตามเส้นทาง และเราใช้ glTF Introspection API เพื่อบังคับและหมุนล้อรถ

  • Mesh จะปรากฏขึ้นพร้อมกับวัสดุยางมะตอย แต่เรายังใช้ Mesh API ร่วมกับ Material API เพื่อเปลี่ยนรูปลักษณ์ของถนนได้ด้วย ดังที่เห็นในวิดีโอ

  • ซึ่งเป็นเครื่องมือที่ทรงพลังมากและใช้งานได้ง่ายๆ เพียงปลายนิ้วสัมผัส

สรุปและเริ่มต้นใช้งาน

  • วันนี้เราได้พูดถึงวิธีต่างๆ มากมายในการเปลี่ยนแอปจาก 2 มิติเป็นเชิงพื้นที่

  • glTF Introspection API ช่วยให้คุณเข้าถึงข้อมูลเมตาภายในของโมเดล glTF ได้โดยตรง ซึ่งจะทริกเกอร์ภาพเคลื่อนไหวที่ฝังไว้ เปลี่ยนรูปแบบโหนดย่อยแต่ละโหนด และแม้แต่สลับวัสดุเพื่อเปลี่ยนรูปลักษณ์ทั้งหมดขณะรันไทม์

  • จากนั้นเราเห็นว่าเมื่อโมเดล 3 มิติที่สร้างไว้ล่วงหน้าไม่เพียงพอ เราสามารถใช้ประโยชน์จาก Mesh API ใหม่ล่าสุดเพื่อสร้างเรขาคณิตที่กำหนดเองได้

  • ไม่ว่าคุณจะต้องการสร้างรูปร่างแบบคงที่ตั้งแต่ต้นหรือสตรีมรูปร่างแบบไดนามิกในแบบเรียลไทม์ ตอนนี้คุณก็มีเครื่องมือที่พร้อมใช้งานแล้ว

  • และอย่างที่ Egor เพิ่งแสดงให้เห็น การจับคู่บล็อกเหล่านั้นกับ Gemini หมายความว่าคุณไม่จำเป็นต้องคำนวณด้วยตัวเองด้วยซ้ำ

  • ตอนนี้ผมจะขอย้ำสิ่งที่พูดไปก่อนหน้านี้ว่า ทุกอย่างที่คุณเห็นทำงานในแอป Android ซึ่งเขียนด้วย Kotlin ที่เป็นสำนวนใน Android Studio โดยตรง

  • วันนี้เราเห็นแล็ปท็อปจำนวนมากในกลุ่มผู้ชม และหวังว่าเรากับ Egor จะทำให้คุณตื่นเต้นมากพอที่จะเปิด Android Studio และเริ่มใช้โปรแกรมจำลอง Android XR ในวันนี้

  • ปลดปล่อยจินตนาการของคุณให้เต็มที่ และใช้ทักษะ Android ที่มีอยู่ร่วมกับสิ่งที่เราแสดงให้เห็นในวันนี้เพื่อสร้างประสบการณ์ XR ที่น่าทึ่ง

  • ในนามของ Egor และตัวฉันเอง ขอขอบคุณที่มาร่วมเซสชันของเราในวันนี้ และขอให้สนุกกับงาน Android Dev Summit ที่เหลือ

รหัส

ข้อมูลโค้ด

ข้อมูลโค้ด JSON ที่ 01:22 ใน "การทำงานกับโมเดล 3 มิติ glTF"
{
    "asset" : {...},
    "nodes" : [
        {
            "name" : "Right_Arm",
            "rotation" : [...],
            "scale" : [...],
            "translation" : [...],
        },
    ],
    "animations" : [
        {
            "name" : "Wave",
        },
    ],
    "meshes" : [
        {
            "name" : "Torso",
            "primitives" : [ ... ],
        },
    ],

    // Way way more!

}
ข้อมูลโค้ด Kotlin ที่ 02:28 ใน "การทำงานกับโมเดล 3 มิติ 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 ที่ 02:50 ใน "ภาพเคลื่อนไหวของโมเดลและการจัดการโหนดย่อย"
// 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 ที่ 05:05 ใน "ข้อมูลเบื้องต้นเกี่ยวกับ 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
)
ข้อมูลโค้ด Kotlin ที่นาที 05:45 ใน "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()
ข้อมูลโค้ด Kotlin ที่ 06:10 ใน "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)
ข้อมูลโค้ด Kotlin ที่นาที 06:37 ใน "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)
ข้อมูลโค้ด Kotlin ที่ 07:04 ใน "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))
ข้อมูลโค้ด Kotlin ที่นาที 07:31 ใน "การสร้าง 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)
ข้อมูลโค้ด Kotlin ที่นาที 08:18 ใน "การสร้าง 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)
ข้อมูลโค้ด Kotlin ที่ 09:34 ใน "การสร้าง Animated Mesh ด้วย 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 ที่ 09:56 ใน "การสร้างเมชแบบเคลื่อนไหวด้วย 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))
}