Android XR

מעבר מדו-ממד לתלת-ממד עם גיאומטריה תלת-ממדית מותאמת אישית ב-Jetpack XR SDK

רמקולים

  • Egor Yusov

    מהנדס תוכנה

  • ג'יי אבי-סעד

    מהנדס תוכנה

מידע כללי

מידע על ההרצאה

כדי להעביר את האפליקציות ל-Android ממסכים דו-ממדיים רגילים לסביבות מרחביות, צריך גישה חדשה לבניית צורות.

בשיעור הזה נסביר על Custom Mesh API ב-Jetpack XR SDK, שמאפשר ליצור באופן פרוגרמטי גיאומטריה תלת-ממדית פרוצדורלית ישירות בזמן הריצה. נעבור על תהליך העבודה העיקרי, מהגדרת פריסות של קודקודים ועד לעיבוד (rendering) של אובייקטים דינמיים בצורה חלקה במרחב הפיזי של המשתמש.

בסוף ההרצאה הזו, יהיו לכם את הכישורים המעשיים ליצור גיאומטריה תלת-ממדית אינטראקטיבית בהתאמה אישית, לגמרי באמצעות קוד.

סיכום

  • ‫glTF introspection API ב-Jetpack SceneCore חושף מטא-נתונים של מודלים בזמן ריצה. אתם יכולים להפעיל אנימציות מוטמעות, לשנות צומתי משנה או לשנות רשתות באמצעות חומרים וטקסטורות.
  • ‫Jetpack SceneCore כולל ממשקי API של רשתות ליצירת גיאומטריה תלת-ממדית בהתאמה אישית ב-Kotlin. ארבעה אבני בניין כמו VertexLayout ו-MeshBuffer יוצרים מודלים משולש אחר משולש בלי כלים תלת-ממדיים.
  • רשתות דינמיות תומכות בעדכונים בזמן אמת כשהמשתמשים משרטטים נתיבים או מוסיפים אנימציה למשטחים. עדכון רק של נתוני הקודקודים שהשתנו מעביר את השינויים ישירות ל-GPU בלי ליצור מחדש את הרשת או הישות.
  • ‫Gemini ב-Android Studio יכול לכתוב אנימציית קודקודים מורכבת וחישובים מתמטיים של תאורה לממשקי ה-API של הרשת. הנחיה אחת יוצרת קוד פעיל שמנפיש רשתות של קודקודים ומחשב מחדש את הנורמלים בכל פריים.

משאבים

Gemini Notebook

נושאים

  • jetpack-xr
  • jetpack-scenecore
  • android-xr
  • גיאומטריה תלת-ממדית
  • mesh-api
  • gltf

תמליל

הורדת התמליל

מבוא ל-Android XR ול-SceneCore

  • שלום לכולם,

  • אני ג'יי.

  • ואני איגור.

  • אנחנו מהנדסי תוכנה שעובדים על Jetpack XR ב-Google.

  • בקרוב נשיק את XREAL Aura, ואנחנו רוצים לוודא שיש לכם את כל הכלים שאתם צריכים כדי ליצור חוויות מרחביות נהדרות בפלטפורמה.

  • והכי טוב: אתם לא צריכים חומרה של XR כדי להתחיל.

  • כל התכונות שנציג זמינות היום באמולטור Android XR ב-Android Studio.

  • המטרה שלנו היא להראות לכם שבעזרת ספריית Jetpack SceneCore, אתם לא צריכים ידע מעמיק בתלת-ממד כדי ליצור חוויות מרחביות עשירות בפלטפורמה.

  • כל מה שמוצג פועל באפליקציה ל-Android, שנכתבה ב-Kotlin אידיומטית ישירות ב-Android Studio.

  • לאחרונה קידמנו את Jetpack SceneCore לגרסה יציבה, והיום נסקור כמה פיתוחים מגניבים של ממשקי ה-API שלנו למודלים תלת-ממדיים, וגם את ממשקי ה-API החדשים שלנו לרשתות, שמאפשרים לכם ליצור גיאומטריה תלת-ממדית בהתאמה אישית תוך כדי תנועה.

עבודה עם מודלים תלת-ממדיים בפורמט glTF

  • אבל נתחיל עם מודלים תלת-ממדיים.

  • כשמדובר במודלים תלת-ממדיים, הדבר הראשון שצריך הוא פורמט קובץ.

  • תקנים פתוחים הם חלק מרכזי ב-DNA של Android XR.

  • לכן, באופן טבעי, בחרנו בפורמט glTF של Khronos Group.

  • אם אתם לא מגיעים מעולם התלת-ממד, אולי אתם שואלים את עצמכם "מה זה glTF?"

  • אפשר לחשוב על זה כמו על קובץ JPEG, אבל למודלים תלת-ממדיים.

  • מבנה הנכס של 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.

  • אתם יכולים לראות חלונית מרחבית עם פעילות שצפה במרחב של המשתמש.

  • אתם יכולים להזיז אותו ולשנות את הגודל שלו לפי הצורך.

  • למרות שההרצאה הזו עוסקת בתלת-ממד, כדאי לבדוק גם את שאר חלקי Jetpack XR SDK.

  • אוקיי, חלוניות מרחביות זה נחמד, אבל אנחנו רוצים עוד אפשרויות מגניבות.

  • ואיזו דרך טובה יותר להוסיף קצת קסם מאשר להוסיף מודל תלת-ממדי?

  • במקרה, המעצב שלנו יצר את מודל ה-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 של המודלים התלת-ממדיים שלנו כדי לטעון אותו ישירות לסצנה.

אנימציות של מודלים ושינוי צומתי משנה

  • אבל בואו נגרום למכונית להרגיש קצת יותר חיה.

  • זוכרים את כל המטא-נתונים העשירים בתוך 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 מוטמעת.

  • כאן אני מסובב ומכוון את הגלגלים בנפרד בזמן הריצה.

הוספת חומרים וטקסטורות

  • החלק האחרון של המטא-נתונים שאפשר לגשת אליו הוא כל רשת במודל 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)
  • ואז, באמצעות ה-API של החומרים, אנחנו יכולים לחבר אותו ישירות ל-KhronosPbrMaterial.

  • אם אתם חדשים בתחום התלת-ממד, אתם יכולים לחשוב על רשת כעל שולחן, על חומר כעל מפה ועל טקסטורה כעל דוגמה שמודפסת על המפה.

  • אבל כאן, במקום משטח שולחן, אנחנו משתמשים באובייקטים החומריים שיצרנו כדי לשנות את הרשתות שמשויכות לצמתי המשנה של מכסה המנוע והדלתות של המכונית.

  • בכמה שורות קוד בלבד, שינינו את המראה והתחושה של המכונית, שוב, בזמן ריצה.

  • זה מגניב, אבל יש לי רעיון עוד יותר טוב.

  • איזה כיף יהיה לצייר מסלול מרוצים שהמכונית תוכל לנסוע עליו?

  • במקרה כזה, מודל glTF מוכן מראש לא מתאים.

  • במקום זאת, אנחנו צריכים את היכולת ליצור גיאומטריה תלת-ממדית תוך כדי תנועה.

  • אז אני אעביר את רשות הדיבור לאיגור, שיספר לכם על ממשקי ה-API החדשים של Jetpack SceneCore mesh.

מבוא לממשקי API של רשת שירותים

  • תודה, ג'יי.

  • בואו נגרום לכך שהמשתמש יוכל לצייר את מסלול המרוצים שלו עם האצבע ב-Living Room.

  • המסלול לא קיים עד שהוא משורטט, כך שאף אחד לא יכול למדל אותו מראש.

  • אנחנו נבנה אותו בקוד, משולש אחד בכל פעם.

  • רשת מתחילה בקודקודים, נקודות במרחב.

    // 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
    )
  • לכל אחד מהם יש מאפיינים כמו מיקום, נורמל לתאורה, צבע וקואורדינטות של טקסטורה.

  • אינדקסים מחברים בין הנקודות.

  • כל שלוש נקודות יוצרות משולש.

  • שני משולשים יוצרים ריבוע, שישה אינדקסים, אבל רק ארבעה קודקודים, כי שניים מהם משותפים.

  • אם ממשיכים, מקבלים קובייה.

  • כל מודל תלת-ממדי, כולל המכונית של ג'יי, בנוי באותו אופן, רק עם יותר משולשים.

אבני הבניין של 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 ביט.

  • הנתונים מועתקים ל-GPU באופן מיידי, כך שתוכלו להשתמש שוב ב-ByteBuffers או לשחרר אותם.

  • כברירת מחדל, 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)
  • ההבדל היחיד הוא שאנחנו נותנים ל-builder את הקיבולת המקסימלית מראש, כדי שיהיה אפשר להקצות את מאגרי הנתונים הזמניים של ה-GPU.

  • לאחר מכן, כשהמשתמש מצייר, updateVertexData ו-updateIndexData מעלים רק את הקטעים החדשים, ולא את כל הרשת.

  • חשוב לזכור: כלי הרינדור משתמש בגבולות כדי לדלג על כל מה שמחוץ למסך, אז צריך להגדיר את הגבולות כך שיכסו את כל מה שהמסלול יכול להפוך להיות, כמו אזור המשחק כאן.

  • אולי תשימו לב שה-API הוא לא החלק הקשה כאן.

  • החלק הקשה הוא המתמטיקה של הקודקודים, הנורמלים, קואורדינטות הטקסטורה, סדר המשולשים.

  • זה בדיוק סוג הקוד ש-AI מצוין בכתיבה שלו.

  • אבל נניח שאנחנו רוצים לקחת את הדברים צעד אחד קדימה.

  • עכשיו אני רוצה דגל משובץ שמתנופף ברוח ביעד.

יצירת רשתות מונפשות באמצעות Gemini

  • המתמטיקה של אנימציית קודקודים היא החלק המסובך, אז שאלתי את Gemini.

  • ב-Android Studio, נתתי לו הנחיה אחת: "באמצעות Jetpack SceneCore Mesh API, בנה רשת דגלים בגודל 20x12 עם UVs של לוח שחמט, הנפש גל רוח בכל פריים באמצעות 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.5KB של נתונים ל-GPU בכל פריים.

  • אנחנו לא יוצרים מחדש את הרשת או את הישות.

  • עכשיו נראה את הכול ביחד.

  • אנחנו מניחים את דגם המכונית על השולחן, ואז משתמשים ב-ARCore כדי לצייר את המסלול.

  • רשת הדגל שיצרנו מופיעה ביעד.

  • המכונית נוסעת לאורך המסלול, ואנחנו משתמשים ב-glTF Introspection API כדי לנווט ולסובב את גלגלי המכונית.

  • הרשת מופיעה עם חומר האספלט, אבל אנחנו יכולים גם להשתמש ב-API של הרשת יחד עם ה-API של החומר כדי לשנות את המראה והתחושה של הכביש, כמו שאפשר לראות בסרטון.

  • זה כמו כלי רב עוצמה שנמצא ממש בקצות האצבעות.

סיכום והתחלת העבודה

  • היום דיברנו על הרבה דרכים להעביר את האפליקציות שלכם מדו-ממד למרחב.

  • בעזרת glTF Introspection API, אתם יכולים עכשיו לגשת ישירות למטא-נתונים הפנימיים של מודל glTF, להפעיל אנימציות מוטמעות, לשנות צמתים משניים ספציפיים ואפילו להחליף חומרים כדי לשנות את המראה והתחושה של המודל בזמן הריצה.

  • אחר כך ראינו שכאשר מודלים תלת-ממדיים מוכנים מראש לא מספיקים, אנחנו יכולים להשתמש ב-Mesh API החדש שלנו כדי ליצור גיאומטריה מותאמת אישית.

  • עכשיו יש לכם את הכלים ליצור צורות סטטיות מאפס או להזרים גיאומטריה דינמית בזמן אמת.

  • וכמו שאגור הראה, אם משלבים את אבני הבניין האלה עם Gemini, אפילו לא צריך לעשות את החישובים לבד.

  • עכשיו אחזור על משהו שאמרתי קודם: כל מה שראיתם פעל באפליקציה ל-Android, שנכתבה ב-Kotlin אידיומטית, ממש בתוך Android Studio.

  • אני רואה הרבה מחשבים ניידים בקהל היום, ואני מקווה שאני ואגור הצלחנו לעורר בכם מספיק עניין כדי שתפתחו את Android Studio ותפעילו את אמולטור Android XR עוד היום.

  • תנו לדמיון שלכם להשתולל, והשתמשו במיומנויות הקיימות שלכם ב-Android, יחד עם מה שהראינו היום, כדי ליצור חוויות XR מדהימות.

  • בשם איגור ובשמי, תודה רבה שהצטרפתם אלינו היום, ותיהנו משאר המפגש של מפתחי Android.

קוד

קטעי קוד

קטע קוד ב-JSON בדקה 01:22 בסרטון "עבודה עם מודלים תלת-ממדיים בפורמט glTF"
{
    "asset" : {...},
    "nodes" : [
        {
            "name" : "Right_Arm",
            "rotation" : [...],
            "scale" : [...],
            "translation" : [...],
        },
    ],
    "animations" : [
        {
            "name" : "Wave",
        },
    ],
    "meshes" : [
        {
            "name" : "Torso",
            "primitives" : [ ... ],
        },
    ],

    // Way way more!

}
קטע קוד ב-Kotlin בדקה 02:28 בסרטון "עבודה עם מודלים תלת-ממדיים בפורמט 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 בסרטון "Model animations and subnode manipulation"
// 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 בסרטון "Model animations and subnode manipulation"
// 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 בסרטון "Applying materials and 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)
קטע קוד ב-Kotlin בדקה 05:05 בסרטון "מבוא לממשקי 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 בסרטון "אבני הבניין של 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 בסרטון "אבני הבניין של 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 בסרטון "אבני הבניין של 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 בסרטון "אבני הבניין של 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 בסרטון "יצירת רשתות דינמיות בהתאמה אישית"
// 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 בסרטון "בניית רשתות מותאמות אישית ודינמיות"
// 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 בסרטון "יצירת רשתות מונפשות באמצעות 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 בסרטון "Generating animated meshes with 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))
}