انتقال برنامههای Android از صفحهنمایشهای دوبعدی سنتی به محیطهای فضایی نیازمند رویکردی جدید برای ساختن شکلها است.
در این جلسه، Custom Mesh API را در Jetpack XR SDK کاوش خواهیم کرد که به شما امکان میدهد هندسه سهبعدی رویهای را بهطور برنامهنویسیشده مستقیماً در زمان اجرا تولید کنید. ما گردش کار اصلی را از تعریف چیدمانهای رأس تا رندر کردن یکپارچه اشیای پویا در فضای فیزیکی کاربر بررسی خواهیم کرد.
در پایان این گفتگو، مهارتهای عملی برای ساختن هندسه سهبعدی تعاملی و سفارشی را بهطور کامل ازطریق کدنویسی خواهید داشت.
MeshBuffer مدلها را مثلث به مثلث بدون ابزارهای سهبعدی میسازند.آوانویسی
سلام دوستان.
من جی هستم.
و من ایگور هستم.
و ما مهندسان نرمافزار هستیم که روی Jetpack XR در Google کار میکنیم.
با نزدیک شدن به عرضه XREAL Aura، میخواهیم مطمئن شویم که همه ابزارهای لازم برای ساختن تجربههای فضایی عالی در این پلاتفرم را دراختیار دارید.
اما از همه بهتر اینکه برای شروع حتی به سختافزار واقعیت گسترده نیاز ندارید.
همه ویژگیهایی که نشان خواهیم داد امروز در «شبیهساز Android XR» در «استودیو Android» دردسترس است.
هدف ما این است که به شما نشان دهیم با کتابخانه Jetpack SceneCore، برای ساختن تجربههای فضایی غنی در این پلاتفرم به دانش عمیق سهبعدی نیاز ندارید.
هر چیزی که خواهید دید در برنامه Android اجرا میشود که به زبان اصطلاحی Kotlin درست در Android Studio نوشته شده است.
اخیراً Jetpack SceneCore را به نسخه پایدار ارتقا دادیم و امروز برخیاز پیشرفتهای جالب میاناهای برنامهسازی کاربردی مدل سهبعدی و همچنین میاناهای برنامهسازی کاربردی مش کاملاً جدیدمان را پوشش خواهیم داد که به شما امکان میدهد هندسه سهبعدی سفارشی را درلحظه بسازید.
اما بیایید با مدلهای سهبعدی شروع کنیم.
وقتی صحبت از مدلهای سهبعدی میشود، اولین چیزی که نیاز داریم قالب فایل است.
استانداردهای باز بخش اصلی 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، میتوانم از میاناهای برنامهسازی کاربردی مدل سهبعدیمان برای بار کردن آن مستقیماً در صحنهمان استفاده کنم.
اما بیایید کاری کنیم که خودرو کمی زندهتر بهنظر برسد.
همه آن فرادادههای غنی داخل glTF را بهخاطر دارید؟
با میانای برنامهسازی کاربردی دروننگری 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 همچنین زیرگرهها را در مدل بهعنوان 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 جدیدمان به شما بگوید.
ممنون، جی.
بیایید کاری کنیم که کاربر بتواند پیست مسابقه خود را در-- با انگشتش در اتاق نشیمن خود بکشد.
مسیر تا زمانی که کشیده نشود وجود ندارد، بنابراین هیچکس نمیتواند آن را ازقبل مدلسازی کند.
ما آن را در کد، یک مثلث در هر بار، خواهیم ساخت.
مش با رأسها، یعنی نقاطی در فضا، شروع میشود.
// 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ها، مختصات بافت، در مجموع ۳۲ بایت است.
ویژگیها همچنین میتوانند در میانگیرهای جداگانه قرار بگیرند تا بتوانید فقط آنهایی را که تغییر میکنند بهروز کنید.
دوم، 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)چیدمان، بایتهای رأس، و نمایههای ۳۲ بیتی را به آن میدهید.
دادهها فوراً در 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))مش همان «چیزی» است که میخواهید ایجاد کنید.
نهاد همان «کجا» است.
مثل پانل و ماشین Jay، این هم یک نهاد است، بنابراین موقعیت و والد دارد، چیزی که به آن متصل است.
والد را جابهجا کنید، جاده هم با آن جابهجا میشود.
همچنین برای هر زیرمجموعه یک ماده ارائه میدهید تا همان مش بتواند با مادهای متفاوت در نهادهای مختلف ظاهر شود.
بنابراین، حالا بیایید همه چیز را کنار هم بگذاریم و مش، مش جاده را بسازیم.
// 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)تنها تفاوت این است که ما حداکثر ظرفیت را ازقبل به سازنده میدهیم تا میانگیرهای GPU بتوانند اختصاص داده شوند.
سپس، وقتی کاربر طراحی میکند، updateVertexData و updateIndexData فقط بخشهای جدید را بارگذاری میکنند، نه کل مش را.
یک نکته: موتور پردازش از مرزها برای رد شدن از هر چیزی که خارج از صفحه است استفاده میکند، بنابراین مرزها را طوری تنظیم کنید که هر چیزی را که قطعه ممکن است به آن تبدیل شود، مانند منطقه پخش در اینجا، پوشش دهد.
ممکن است متوجه شوید که API بخش دشوار در اینجا نیست.
بخش دشوار آن ریاضیات رأس، بردار نرمال، مختصات بافت، و ترتیب مثلث است.
این دقیقاً همان نوع کدی است که هوش مصنوعی در نوشتن آن عالی است.
اما بیایید فرض کنیم میخواهیم یک قدم فراتر برویم.
حالا حالا، میخواهم یک پرچم شطرنجی در مقصد در باد تکان بخورد.
ریاضیات پویانمایی رأس بخش دشوار کار است، بنابراین از Gemini کمک خواستم.
در Android Studio، یک پیامواره به آن دادم: «بااستفاده از Jetpack SceneCore Mesh API، یک شبکه پرچم ۲۰ در ۱۲ با UVهای شطرنجی بساز، در هر فریم با updateVertexData یک موج باد را پویانمایی کن، و نرمالها را دوباره محاسبه کن تا نورپردازی از چینها پیروی کند.»
چند ثانیه بعد، کد کارآمدی دریافت کردم.
بیایید ببینیم چه کاری انجام داد.
این مدل یک مش پرچم را بهعنوان شبکهای از ۲۰ در ۱۲ سلول، دو مثلث در هر سلول، ساخت و انتخاب هوشمندانهای در چیدمان انجام داد.
// 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))
}بردار نرمال از موقعیتهای مجاور دوباره محاسبه میشود، که باعث میشود چینها نور را جذب کنند.
updateVertexData حدود ۶٫۵ کیلوبایت داده را در هر فریم به واحد پردازش گرافیکی بارگذاری میکند.
ما مش یا نهاد را بازسازی نمیکنیم.
اکنون بیایید همه را با هم ببینیم.
مدل خودرو را روی میز قرار میدهیم، سپس از ARCore برای ترسیم مسیر استفاده میکنیم.
پرچم مشی که همین الان-- مش پرچمی که همین الان ساختیم در مقصد ظاهر میشود.
خودرو در مسیر حرکت میکند و ما از glTF Introspection API برای هدایت و چرخاندن چرخهای خودرو استفاده میکنیم.
مش با ماده آسفالت ظاهر میشود، اما میتوانیم از میانای برنامهسازی کاربردی مش بههمراه میانای برنامهسازی کاربردی ماده نیز برای تغییر ظاهر و حس جاده استفاده کنیم، همانطور که در ویدیو میبینید.
این مثل این است که چیزهای قدرتمندی درست در نوک انگشتان شما باشد.
امروز روشهای زیادی را برای تبدیل برنامههایتان از دوبعدی به فضایی پوشش دادیم.
با glTF Introspection API، اکنون میتوانید مستقیماً به فراداده داخلی مدل glTF دسترسی پیدا کنید، پویانماییهای جاسازیشده را راهاندازی کنید، زیرگرههای جداگانه را تبدیل کنید، و حتی مواد را برای تغییر ظاهر و حس آن در زمان اجرا تعویض کنید.
سپس دیدیم که وقتی مدلهای سهبعدی ازپیشساخته کافی نبودند، میتوانستیم از Mesh API جدیدمان برای ساختن هندسه سفارشی استفاده کنیم.
چه بخواهید اشکال ایستا را از ابتدا بسازید یا اشکال پویا را بهصورت همزمان جاریسازی کنید، اکنون ابزارهای لازم برای این کار را دارید.
و همانطور که ایگور نشان داد، جفت کردن این واحدهای سازنده با Gemini به این معنی است که حتی لازم نیست محاسبات را بهتنهایی انجام دهید.
اکنون، چیزی را که قبلاً گفتم تکرار میکنم: هر چیزی که دیدید در یک برنامه Android اجرا میشد، که به زبان اصطلاحی Kotlin، درست در داخل Android Studio نوشته شده بود.
امروز رایانههای کیفی زیادی را در میان مخاطبان میبینم و امیدوارم من و ایگور شما را بهاندازه کافی هیجانزده کرده باشیم تا Android Studio را باز کنید و «شبیهساز Android XR» را همین امروز راهاندازی کنید.
تصوراتتان را آزاد و رها کنید و از مهارتهای Android موجودتان، همراه با آنچه امروز نشان دادیم، برای ساختن تجربههای شگفتانگیز واقعیت گسترده استفاده کنید.
ازطرف ایگور و خودم، از اینکه امروز به ما پیوستید بسیار سپاسگزارم و امیدوارم از ادامه «نشست توسعهدهندگان 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))
}