Android XR

تجاوز الأبعاد الثنائية باستخدام الأشكال الهندسية الثلاثية الأبعاد المخصّصة في حزمة Jetpack XR SDK

سماعات

  • إيغور يوسوف

    مهندس برمجيات

  • جاي أبي سعد

    مهندس برمجيات

لمحة

لمحة عن المحاضرة

يتطلّب نقل تطبيقات Android من الشاشات التقليدية الثنائية الأبعاد إلى البيئات المكانية اتّباع نهج جديد لإنشاء الأشكال.

في هذه الجلسة، سنستكشف واجهة Custom Mesh API في حزمة Jetpack XR SDK، والتي تتيح لك إنشاء أشكال هندسية ثلاثية الأبعاد بشكل آلي مباشرةً أثناء التشغيل. سنستعرض سير العمل الأساسي، بدءًا من تحديد تخطيطات الرؤوس وحتى عرض العناصر الديناميكية بسلاسة داخل المساحة المادية للمستخدم.

في نهاية هذه الجلسة، ستكتسبون المهارات العملية لإنشاء أشكال هندسية ثلاثية الأبعاد تفاعلية ومخصّصة بالكامل من خلال الرموز البرمجية.

ملخّص

  • تتيح واجهة برمجة التطبيقات glTF introspection API في Jetpack SceneCore عرض البيانات الوصفية للنماذج أثناء التشغيل. يمكنك تشغيل الصور المتحركة المضمّنة أو تحويل العُقد الفرعية أو إلغاء الشبكات باستخدام المواد والزخارف.
  • يتضمّن Jetpack SceneCore واجهات برمجة تطبيقات للشبكات لإنشاء أشكال هندسية ثلاثية الأبعاد مخصّصة في Kotlin. تُنشئ أربع وحدات أساسية، مثل VertexLayout وMeshBuffer، النماذج مثلّثًا تلو الآخر بدون أدوات ثلاثية الأبعاد.
  • تتيح الشبكات الديناميكية إجراء تعديلات في الوقت الفعلي أثناء رسم المستخدمين للمسارات أو تحريك الأسطح. يؤدي تعديل بيانات الرؤوس التي تم تغييرها فقط إلى إرسال التعديلات مباشرةً إلى وحدة معالجة الرسومات بدون إعادة إنشاء الشبكة أو الكيان.
  • يمكن لـ "Gemini في استوديو Android" كتابة عمليات حسابية معقّدة للإضاءة والرسوم المتحركة للرؤوس في واجهات برمجة التطبيقات الخاصة بالشبكات. يؤدي طلب واحد إلى إنشاء رمز برمجي فعّال يحرك شبكات الرؤوس ويعيد حساب المتجهات العمودية في كل إطار.

الموارد

Gemini Notebook

المواضيع

  • jetpack-xr
  • jetpack-scenecore
  • android-xr
  • 3d-geometry
  • mesh-api
  • gltf

النص

تنزيل النص

مقدمة عن Android XR وSceneCore

  • مرحبًا،

  • اسمي جاي.

  • وأنا "إيغور".

  • ونحن مهندسو برمجيات نعمل على Jetpack XR في Google.

  • مع اقتراب موعد إطلاق XREAL Aura، نريد التأكّد من توفّر جميع الأدوات اللازمة لإنشاء تجارب مكانية رائعة على المنصة.

  • والأفضل من ذلك هو أنّك لست بحاجة إلى أجهزة XR للبدء.

  • جميع الميزات التي سنعرضها متوفّرة اليوم في محاكي Android XR في "استوديو Android".

  • هدفنا هو أن نوضّح لك أنّه باستخدام مكتبة Jetpack SceneCore، لن تحتاج إلى معرفة عميقة بالرسومات الثلاثية الأبعاد لإنشاء تجارب مكانية غنية على المنصة.

  • كل ما ستراه يتم تشغيله في تطبيق Android، مكتوب بلغة Kotlin الاصطلاحية داخل "استوديو Android".

  • لقد أطلقنا مؤخرًا الإصدار الثابت من Jetpack SceneCore، واليوم سنتناول بعض التحسينات الرائعة في واجهات برمجة التطبيقات الخاصة بالنماذج الثلاثية الأبعاد، بالإضافة إلى واجهات برمجة التطبيقات الجديدة كليًا الخاصة بالشبكات، والتي تتيح لك إنشاء أشكال هندسية ثلاثية الأبعاد مخصّصة بسرعة.

العمل باستخدام تصاميم glTF الثلاثية الأبعاد

  • لنبدأ بالنماذج الثلاثية الأبعاد.

  • عندما يتعلق الأمر بالتصاميم الثلاثية الأبعاد، أول ما نحتاجه هو تنسيق الملف.

  • تُعدّ المعايير المفتوحة جزءًا أساسيًا من 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.

  • يمكنك رؤية لوحة مكانية مع نشاط عائم في مساحة المستخدم.

  • ويمكنك الإمساك به وتحريكه وتغيير حجمه حسب رغبتك.

  • على الرغم من أنّ هذه الجلسة تركّز على المحتوى الثلاثي الأبعاد، ننصحك بالاطّلاع على بقية ميزات حزمة تطوير البرامج (SDK) لـ Jetpack XR أيضًا.

  • حسنًا، اللوحات المكانية ممتعة، ولكننا نريد المزيد من السحر.

  • وما مِن طريقة أفضل لإضافة بعض السحر من استخدام نموذج ثلاثي الأبعاد.

  • لقد صمّم مصمّمنا نموذج 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، يمكنني استخدام واجهات برمجة التطبيقات الخاصة بالنماذج الثلاثية الأبعاد لتحميلها مباشرةً في المشهد.

الصور المتحركة للنماذج والتحكّم في العُقد الفرعية

  • ولكن لنجعل السيارة تبدو أكثر حيوية.

  • هل تتذكر كل تلك البيانات الوصفية الغنية داخل glTF؟

  • باستخدام واجهة برمجة التطبيقات الجديدة glTF introspection API، يمكنك الآن الوصول إلى هذه البيانات، بدءًا من الصور المتحركة.

    // 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.

  • سيتعمّق "إيغور" الآن في الحديث عن الشبكات.

  • ولكن في الوقت الحالي، لنبدأ بتزيين سيارتنا.

  • باستخدام واجهة برمجة التطبيقات الخاصة بالزخرفة، يمكننا تحميل زخرفة صورة 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)
  • بعد ذلك، باستخدام واجهة برمجة التطبيقات الخاصة بالمواد، يمكننا توصيلها مباشرةً بـ KhronosPbrMaterial.

  • إذا كنتم مبتدئين في مجال التصميم الثلاثي الأبعاد، يمكنكم اعتبار الشبكة كسطح طاولة، والمادة كغطاء للطاولة، والزخرفة كنمط مطبوع على ذلك الغطاء.

  • ولكن هنا، بدلاً من سطح الطاولة، نستخدم العناصر المادية التي أنشأناها لتجاوز الشبكات المرتبطة بالعُقد الفرعية لغطاء محرك السيارة وأبوابها.

  • باستخدام بضعة أسطر من الرموز البرمجية، غيّرنا مظهر السيارة وأسلوبها، وكل ذلك أثناء التشغيل.

  • هذه بعض الأفكار الرائعة، ولكن إليك فكرة أفضل.

  • كم سيكون رائعًا رسم مضمار سباق لتقود السيارة عليه؟

  • في هذه الحالة، لا يكون نموذج glTF المُعدّ مسبقًا مناسبًا.

  • بدلاً من ذلك، نحتاج إلى القدرة على إنشاء أشكال هندسية ثلاثية الأبعاد بسرعة.

  • لذا، سأترك الكلمة لـ "إيغور" ليخبركم عن واجهات برمجة التطبيقات الجديدة كليًا Jetpack SceneCore 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
    )
  • يحمل كل منها سمات مثل الموضع، والاتجاه العمودي للإضاءة، واللون، وإحداثيات الزخرفة.

  • تساعد الفهارس في ربط المعلومات ببعضها.

  • كل ثلاثة يشكلون مثلثًا.

  • مثلثان يشكلان مربعًا، وستة مؤشرات، ولكن أربعة رؤوس فقط، لأنها تشترك في اثنين.

  • استمر في ذلك وستحصل على مكعب.

  • يتم إنشاء كل نموذج ثلاثي الأبعاد، بما في ذلك سيارة "جاي"، بالطريقة نفسها، ولكن باستخدام المزيد من المثلثات.

المكوّنات الأساسية لواجهة برمجة التطبيقات 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 بت.

  • يتم نسخ البيانات إلى وحدة معالجة الرسومات على الفور، حتى تتمكّن من إعادة استخدام 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)
  • نحدّد الرأس: الموضع، والعمودي، والإحداثيات فوق البنفسجية، وإحداثيات الزخرفة.

  • تحوّل دالة مساعِدة صغيرة مسار المستخدم إلى شريط.

  • لكل نقطة على المسار، يضيف رأسين على كل جانب ومثلثين يربطانهما بالزوج التالي.

  • إنّها لغة 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 الأجزاء الجديدة فقط، وليس الشبكة بأكملها.

  • هناك مشكلة واحدة: يستخدم العارض الحدود لتخطّي أي شيء خارج الشاشة، لذا اضبط الحدود لتغطية كل ما قد يصبح عليه المسار، مثل منطقة اللعب هنا.

  • قد تلاحظ أنّ واجهة برمجة التطبيقات ليست الجزء الصعب هنا.

  • الجزء الصعب هو رياضيات الرؤوس، والمتجهات العمودية، وإحداثيات الزخرفة، وترتيب المثلثات.

  • هذا هو بالضبط نوع الرموز التي يبرع الذكاء الاصطناعي في كتابتها.

  • ولكن لنفترض أنّنا نريد الانتقال إلى خطوة أخرى.

  • أريد الآن علمًا مربّعًا يرفرف في الرياح عند الوجهة.

إنشاء شبكات متحركة باستخدام Gemini

  • تُعدّ معادلات الرسوم المتحركة للرؤوس الجزء الأصعب، لذا سألت Gemini.

  • في "استوديو Android"، قدّمتُ له طلبًا واحدًا: "باستخدام 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))
    }
  • تتم إعادة حساب المتجهات العمودية من المواضع المجاورة، ما يجعل الطيات تلتقط الضوء.

  • تحمّل حزمة تطوير البرامج (SDK) لـ "updateVertexData" حوالي 6.5 كيلوبايت من البيانات إلى وحدة معالجة الرسومات في كل لقطة.

  • لا نُعيد إنشاء الشبكة أو الكيان.

  • لنلقِ نظرة على كل هذه الميزات معًا.

  • نضع نموذج السيارة على الطاولة، ثم نستخدم ARCore لرسم المسار.

  • يظهر العلم الشبكي الذي أنشأناه للتو في الوجهة.

  • تسير السيارة على المسار، ونستخدم واجهة glTF Introspection API لتوجيه عجلات السيارة وتدويرها.

  • تظهر الشبكة مع مادة الأسفلت، ولكن يمكننا أيضًا استخدام واجهة برمجة التطبيقات Mesh API مع واجهة برمجة التطبيقات Material API لتغيير مظهر الطريق، كما هو موضّح في الفيديو.

  • هذا أمر رائع ومفيد للغاية.

الملخّص والبدء

  • لقد تناولنا اليوم الكثير من الطرق لنقل تطبيقاتك من ثنائية الأبعاد إلى المكانية.

  • باستخدام glTF Introspection API، يمكنك الآن الاستفادة مباشرةً من البيانات الوصفية الداخلية لنموذج glTF، ما يؤدي إلى تشغيل الصور المتحركة المضمّنة وتحويل العُقد الفرعية الفردية وحتى تبديل المواد لتغيير شكلها وطريقة عملها أثناء التشغيل.

  • بعد ذلك، رأينا أنّه عندما لا تكون النماذج الثلاثية الأبعاد الجاهزة كافية، يمكننا الاستفادة من Mesh API الجديدة كليًا لإنشاء أشكال هندسية مخصّصة.

  • سواء كنت بحاجة إلى إنشاء أشكال ثابتة من البداية أو بث أشكال ديناميكية في الوقت الفعلي، لديك الآن الأدوات اللازمة لذلك.

  • وكما أوضح "إيغور"، فإنّ استخدام هذه الوحدات الأساسية مع Gemini يعني أنّه ليس عليك حتى إجراء العمليات الحسابية بنفسك.

  • سأكرر الآن ما قلته سابقًا: كل ما رأيتموه كان يعمل في تطبيق Android، مكتوبًا بلغة Kotlin الاصطلاحية، داخل استوديو Android مباشرةً.

  • أرى الكثير من أجهزة الكمبيوتر المحمولة بين الحضور اليوم، وآمل أن نكون أنا و"إيغور" قد حمّسناكم بما يكفي لفتح استوديو Android وتشغيل محاكي Android XR اليوم.

  • أطلِق العنان لخيالك واستخدِم مهاراتك الحالية في Android، بالإضافة إلى ما عرضناه اليوم، لإنشاء تجارب رائعة في الواقع الممتد.

  • بالنيابة عن "إيغور" وعن نفسي، أشكركم على انضمامكم إلينا اليوم، وأتمنى لكم الاستمتاع ببقية فعاليات مؤتمر Android Dev Summit.

الرمز

مقتطفات الرمز البرمجي

مقتطف 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 في "النماذج المتحركة والتلاعب بالعُقد الفرعية"
// 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 في فيديو "مقدمة عن واجهات برمجة التطبيقات للشبكة"
// 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"
// 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 في فيديو "إنشاء شبكات متحركة باستخدام 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))
}