يتطلّب نقل تطبيقات Android من الشاشات التقليدية الثنائية الأبعاد إلى البيئات المكانية اتّباع نهج جديد لإنشاء الأشكال.
في هذه الجلسة، سنستكشف واجهة Custom Mesh API في حزمة Jetpack XR SDK، والتي تتيح لك إنشاء أشكال هندسية ثلاثية الأبعاد بشكل آلي مباشرةً أثناء التشغيل. سنستعرض سير العمل الأساسي، بدءًا من تحديد تخطيطات الرؤوس وحتى عرض العناصر الديناميكية بسلاسة داخل المساحة المادية للمستخدم.
في نهاية هذه الجلسة، ستكتسبون المهارات العملية لإنشاء أشكال هندسية ثلاثية الأبعاد تفاعلية ومخصّصة بالكامل من خلال الرموز البرمجية.
MeshBuffer، النماذج مثلّثًا تلو الآخر بدون أدوات ثلاثية الأبعاد.النص
مرحبًا،
اسمي جاي.
وأنا "إيغور".
ونحن مهندسو برمجيات نعمل على Jetpack XR في Google.
مع اقتراب موعد إطلاق XREAL Aura، نريد التأكّد من توفّر جميع الأدوات اللازمة لإنشاء تجارب مكانية رائعة على المنصة.
والأفضل من ذلك هو أنّك لست بحاجة إلى أجهزة XR للبدء.
جميع الميزات التي سنعرضها متوفّرة اليوم في محاكي Android XR في "استوديو Android".
هدفنا هو أن نوضّح لك أنّه باستخدام مكتبة Jetpack SceneCore، لن تحتاج إلى معرفة عميقة بالرسومات الثلاثية الأبعاد لإنشاء تجارب مكانية غنية على المنصة.
كل ما ستراه يتم تشغيله في تطبيق Android، مكتوب بلغة Kotlin الاصطلاحية داخل "استوديو Android".
لقد أطلقنا مؤخرًا الإصدار الثابت من Jetpack SceneCore، واليوم سنتناول بعض التحسينات الرائعة في واجهات برمجة التطبيقات الخاصة بالنماذج الثلاثية الأبعاد، بالإضافة إلى واجهات برمجة التطبيقات الجديدة كليًا الخاصة بالشبكات، والتي تتيح لك إنشاء أشكال هندسية ثلاثية الأبعاد مخصّصة بسرعة.
لنبدأ بالنماذج الثلاثية الأبعاد.
عندما يتعلق الأمر بالتصاميم الثلاثية الأبعاد، أول ما نحتاجه هو تنسيق الملف.
تُعدّ المعايير المفتوحة جزءًا أساسيًا من 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 مع هذه البنية من خلال أربع وحدات أساسية: 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.
في "استوديو 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.
{
"asset" : {...},
"nodes" : [
{
"name" : "Right_Arm",
"rotation" : [...],
"scale" : [...],
"translation" : [...],
},
],
"animations" : [
{
"name" : "Wave",
},
],
"meshes" : [
{
"name" : "Torso",
"primitives" : [ ... ],
},
],
// Way way more!
}// 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
)// 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()// 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) }
}// 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)// 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
)// 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 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)// 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)// 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)// 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)// 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()// 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))
}