כדי להעביר את האפליקציות ל-Android ממסכים דו-ממדיים רגילים לסביבות מרחביות, צריך גישה חדשה לבניית צורות.
בשיעור הזה נסביר על Custom Mesh API ב-Jetpack XR SDK, שמאפשר ליצור באופן פרוגרמטי גיאומטריה תלת-ממדית פרוצדורלית ישירות בזמן הריצה. נעבור על תהליך העבודה העיקרי, מהגדרת פריסות של קודקודים ועד לעיבוד (rendering) של אובייקטים דינמיים בצורה חלקה במרחב הפיזי של המשתמש.
בסוף ההרצאה הזו, יהיו לכם את הכישורים המעשיים ליצור גיאומטריה תלת-ממדית אינטראקטיבית בהתאמה אישית, לגמרי באמצעות קוד.
MeshBuffer יוצרים מודלים משולש אחר משולש בלי כלים תלת-ממדיים.תמליל
שלום לכולם,
אני ג'יי.
ואני איגור.
אנחנו מהנדסי תוכנה שעובדים על Jetpack XR ב-Google.
בקרוב נשיק את XREAL Aura, ואנחנו רוצים לוודא שיש לכם את כל הכלים שאתם צריכים כדי ליצור חוויות מרחביות נהדרות בפלטפורמה.
והכי טוב: אתם לא צריכים חומרה של XR כדי להתחיל.
כל התכונות שנציג זמינות היום באמולטור Android XR ב-Android Studio.
המטרה שלנו היא להראות לכם שבעזרת ספריית Jetpack SceneCore, אתם לא צריכים ידע מעמיק בתלת-ממד כדי ליצור חוויות מרחביות עשירות בפלטפורמה.
כל מה שמוצג פועל באפליקציה ל-Android, שנכתבה ב-Kotlin אידיומטית ישירות ב-Android Studio.
לאחרונה קידמנו את Jetpack SceneCore לגרסה יציבה, והיום נסקור כמה פיתוחים מגניבים של ממשקי ה-API שלנו למודלים תלת-ממדיים, וגם את ממשקי ה-API החדשים שלנו לרשתות, שמאפשרים לכם ליצור גיאומטריה תלת-ממדית בהתאמה אישית תוך כדי תנועה.
אבל נתחיל עם מודלים תלת-ממדיים.
כשמדובר במודלים תלת-ממדיים, הדבר הראשון שצריך הוא פורמט קובץ.
תקנים פתוחים הם חלק מרכזי ב-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.
תודה, ג'יי.
בואו נגרום לכך שהמשתמש יוכל לצייר את מסלול המרוצים שלו עם האצבע ב-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 ממפה את זה באמצעות ארבע אבני בניין: 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.
ב-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.
{
"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))
}