আপনার Android অ্যাপ্লিকেশনকে প্রথাগত 2D স্ক্রিন থেকে স্পেশিয়াল এনভায়রনমেন্টে ট্রানজিশন করার জন্য আকার তৈরি করার একটি নতুন পদ্ধতির প্রয়োজন।
এই সেশনে, আমরা Jetpack XR SDK-তে Custom Mesh API এক্সপ্লোর করব, যা আপনাকে রানটাইমে সরাসরি প্রোগ্রাম্যাটিকভাবে প্রসিডিউরাল 3D জিওমেট্রি জেনারেট করতে দেয়। আমরা ভার্টেক্স লেআউট নির্ধারণ করা থেকে শুরু করে ব্যবহারকারীর ফিজিক্যাল স্পেসে ডাইনামিক অবজেক্ট নির্বিঘ্নে রেন্ডার করা পর্যন্ত মূল ওয়ার্কফ্লো নিয়ে আলোচনা করব।
এই আলোচনার শেষে, আপনি কোড থেকে সম্পূর্ণভাবে ইন্টার্যাক্টিভ, কাস্টম 3D জিওমেট্রি তৈরি করার প্র্যাক্টিক্যাল স্কিল অর্জন করবেন।
MeshBuffer-এর মতো চারটি বিল্ডিং ব্লক 3D টুল ছাড়াই ত্রিভুজ দ্বারা ত্রিভুজ মডেল তৈরি করে।ট্রান্সস্ক্রিপ্ট করা
হ্যালো বন্ধুরা।
আমি জয়।
আর আমি ইগর।
এবং আমরা Google-এ Jetpack XR-এ কাজ করা সফ্টওয়্যার ইঞ্জিনিয়ার।
XREAL Aura-এর লঞ্চ আসন্ন, তাই আমরা নিশ্চিত করতে চাই যে প্ল্যাটফর্মে দুর্দান্ত স্পেশিয়াল অভিজ্ঞতা তৈরি করার জন্য আপনার কাছে প্রয়োজনীয় সব টুল আছে।
তবে সবচেয়ে ভাল বিষয় হল, শুরু করার জন্য আপনার XR হার্ডওয়্যারেরও প্রয়োজন নেই।
আমরা যেসব ফিচার দেখাব, সেগুলি Android Studio-তে Android XR ইমুলেটরে আজ উপলভ্য।
আমাদের লক্ষ্য হল আপনাকে দেখানো যে Jetpack SceneCore লাইব্রেরির মাধ্যমে, প্ল্যাটফর্মে সমৃদ্ধ স্পেশিয়াল অভিজ্ঞতা তৈরি করার জন্য আপনার গভীর 3D জ্ঞানের প্রয়োজন নেই।
আপনি যা কিছু দেখবেন তা Android অ্যাপে চলছে, Android Studio-র মধ্যে ইডিওমেটিক Kotlin-এ লেখা।
আমরা সম্প্রতি Jetpack SceneCore-কে স্টেবল ভার্সনে উন্নীত করেছি এবং আজ আমরা আমাদের 3D মডেল API-এর কিছু দুর্দান্ত অগ্রগতি কভার করব, সেইসাথে আমাদের ব্র্যান্ড-নিউ মেশ API-এর কথাও বলব, যা আপনাকে চটপট কাস্টম 3D জিওমেট্রি তৈরি করতে দেয়।
তবে, 3D মডেল দিয়ে শুরু করা যাক।
3D মডেলের ক্ষেত্রে, আমাদের প্রথমে ফাইলের ফরম্যাট প্রয়োজন।
ওপেন স্ট্যান্ডার্ড হল Android XR-এর মূল অংশ।
তাই, স্বাভাবিকভাবেই, আমরা Khronos Group-এর glTF ফর্ম্যাট ব্যবহার করেছি।
এখন, আপনি যদি 3D ওয়ার্ল্ডের না হন, তাহলে আপনি জিজ্ঞাসা করতে পারেন, "glTF কী?"
আপনি এটিকে JPEG-এর মতো ভাবতে পারেন, তবে 3D মডেলের জন্য।
glTF অ্যাসেট স্ট্রাকচার অ্যানিমেশন, সাবনোড ও মেশের মতো জিনিসের জন্য ডেটা প্রদান করে।
{
"asset" : {...},
"nodes" : [
{
"name" : "Right_Arm",
"rotation" : [...],
"scale" : [...],
"translation" : [...],
},
],
"animations" : [
{
"name" : "Wave",
},
],
"meshes" : [
{
"name" : "Torso",
"primitives" : [ ... ],
},
],
// Way way more!
}তবে এই আলোচনার উদ্দেশ্যে, আপনি সাবনোডকে একটি মডেলের অংশ হিসেবে ভাবতে পারেন, যার প্রতিটির নিজস্ব অবস্থান এবং ঘূর্ণন রয়েছে।
আপনি যে সারফেস দেখতে পান তা হল মেশ এবং অ্যানিমেশন হল সেইসব সাবনোড ও মেশের মুভমেন্ট যা অ্যাসেটের লেখক আগে থেকেই তৈরি করে রেখেছেন।
ঠিক আছে।
এখন, এই পর্যায়ে, আপনি হয়ত জিজ্ঞাসা করতে পারেন, "আমি glTF মডেল কোথায় পাব?"
অনেক মডেলের পাশাপাশি আপনি নিজের মডেলও তৈরি করতে পারেন।
এখন যেহেতু আমরা glTF ফর্ম্যাটের প্রাথমিক বিষয়গুলি কভার করেছি, আসুন এগিয়ে যাই এবং আমাদের অ্যাপে এটি ব্যবহার করি।
আমরা Jetpack SceneCore ব্যবহার করে একটি বেসিক Android XR প্রোজেক্ট দিয়ে শুরু করি।
আপনি ব্যবহারকারীর স্পেসে অ্যাক্টিভিটি ফ্লোট করা সহ স্পেশাল প্যানেল দেখতে পাবেন।
আপনি এটি ধরে, সরিয়ে এবং আপনার পছন্দ অনুযায়ী ছোট বড় করতে পারবেন।
এই আলোচনাটি 3D সম্পর্কে হলেও, Jetpack XR SDK-এর বাকি অংশও অবশ্যই চেক করে দেখুন।
ঠিক আছে, স্পেশিয়াল প্যানেল মজাদার, তবে আমরা আরও ম্যাজিক চাই।
আর 3D মডেল যোগ করার চেয়ে দুর্দান্ত কিছু ইনপুট যোগ করার আর কী উপায় আছে?
আমাদের ডিজাইনার এই 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 ব্যবহার করে, আমি আমাদের 3D মডেল API ব্যবহার করে এটি সরাসরি আমাদের দৃশ্যে লোড করতে পারি।
কিন্তু গাড়িটিকে আরও একটু জীবন্ত করে তোলা যাক।
glTF-এর মধ্যে থাকা সব রিচ মেটাডেটা মনে আছে?
নতুন glTF ইন্ট্রোস্পেকশন 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 ইন্ট্রোস্পেকশন 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-এ প্লাগ-ইন করতে পারি।
এখন, আপনি যদি 3D-তে নতুন হন, তাহলে আপনি একটি মেশকে টেবিলটপ, একটি মেটেরিয়ালকে টেবিলক্লথ এবং একটি টেক্সচারকে সেই কাপড়ে প্রিন্ট করা প্যাটার্ন হিসেবে ভাবতে পারেন।
কিন্তু এখানে, টেবিলটপের পরিবর্তে, আমরা গাড়ির হুড এবং দরজার সাবনোডের সাথে যুক্ত মেশ ওভাররাইড করার জন্য তৈরি করা মেটেরিয়াল অবজেক্ট ব্যবহার করি।
মাত্র কয়েক লাইন কোড লিখে আমরা গাড়ির লুক ও ফিল পরিবর্তন করেছি, আবার রান টাইমে।
এটা সত্যিই চমকপ্রদ, তবে আমার কাছে আরও ভালো আইডিয়া আছে।
গাড়ি চালানোর জন্য রেস ট্র্যাক আঁকা কতই না দারুণ হবে?
এখানে, আগে থেকে তৈরি glTF মডেল সঠিক নয়।
তার পরিবর্তে, আমাদের প্রয়োজন চটপট 3D জিওমেট্রি তৈরি করার ক্ষমতা।
তাই, আমি ইগরকে আমাদের ব্র্যান্ড-নিউ Jetpack SceneCore মেশ API সম্পর্কে আপনাকে সব কিছু বলার জন্য অনুরোধ করব।
ধন্যবাদ, জয়।
আসুন এমন কিছু করি যাতে ব্যবহারকারী তার বসার ঘরে বসে আঙুল দিয়ে রেস ট্র্যাক আঁকতে পারেন।
ট্র্যাকটি আঁকা না হওয়া পর্যন্ত এর কোনও অস্তিত্ব থাকে না, তাই কেউ সময়ের আগে এটির মডেল তৈরি করতে পারে না।
আমরা কোডের মাধ্যমে এটি তৈরি করব, একবারে একটি ত্রিভুজ।
একটি মেশ শুরু হয় ভার্টিসেস, স্পেসের পয়েন্ট দিয়ে।
// 4 vertices: x, y, z
val positions = floatArrayOf(
0f, 0f, 0f, // v0
1f, 0f, 0f, // v1
0f, 1f, 0f, // v2
1f, 1f, 0f // v3
)
// 2 triangles = 6 indices
val indices = intArrayOf(
0, 1, 2,
2, 1, 3
)প্রতিটিতে অবস্থান, আলোর জন্য স্বাভাবিক, রঙ, টেক্সচার কোঅর্ডিনেটের মতো অ্যাট্রিবিউট থাকে।
সূচক ডট কানেক্ট করে।
প্রতি তিনটি বিন্দু একটি ত্রিভুজ তৈরি করে।
দুটি ত্রিভুজ একটি বর্গক্ষেত্র তৈরি করে, ছয়টি সূচক, কিন্তু মাত্র চারটি শীর্ষবিন্দু, কারণ তারা দুটি শেয়ার করে।
এগিয়ে চলো এবং তুমি একটি কিউব পাবে।
জয়-এর গাড়ি সহ প্রতিটি 3D মডেল একই পদ্ধতিতে তৈরি করা হয়, শুধু এতে আরও বেশি ত্রিভুজ থাকে।
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-তে কপি করা হয়, যাতে আপনি ByteBuffer আবার ব্যবহার করতে বা রিলিজ করতে পারেন।
ডিফল্ট হিসেবে, 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))The mesh is the what.
এন্টিটি হল কোথায়।
প্যানেল এবং জয়ের গাড়ির মতো এটিও একটি এন্টিটি, তাই এটির একটি পজিশন এবং পেরেন্ট আছে, যেটির সাথে এটি অ্যাটাচ করা আছে।
প্যারেন্ট সরান এবং রাস্তাটি এটির সাথে সরে যাবে।
এছাড়াও, আপনি প্রতিটি সাবসেটের জন্য একটি করে মেটেরিয়াল প্রদান করেন, যাতে একই মেশ বিভিন্ন এন্টিটিতে বিভিন্ন মেটেরিয়াল সহ দেখা যেতে পারে।
তাহলে, এখন আসুন আমরা সব একসাথে করে মেশ, রাস্তার মেশ তৈরি করি।
// 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-এ বাইট লেখা।
আপনার কোনও ধরনের 3D টুলের প্রয়োজন নেই।
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 ব্যবহার করা এখানে কঠিন কাজ নয়।
কঠিন অংশটি হল ভার্টেক্স ম্যাথ, নরমাল, টেক্সচার কোঅর্ডিনেট, ত্রিভুজ অর্ডার।
এই ধরনের কোড লেখার ক্ষেত্রে AI-এর জুড়ি মেলা ভার।
তবে, ধরা যাক আমরা আরও এক ধাপ এগিয়ে যেতে চাই।
এখন, আমি চাই গন্তব্যে একটি চেকার্ড পতাকা বাতাসে উড়ুক।
ভার্টেক্স অ্যানিমেশন ম্যাথ হল জটিল অংশ, তাই আমি Gemini-কে জিজ্ঞাসা করেছি।
Android Studio-তে আমি এটিকে একটি প্রম্পট দিয়েছিলাম: "Jetpack SceneCore Mesh API ব্যবহার করে, চেকারবোর্ড UV সহ একটি ২০x১২ ফ্ল্যাগ গ্রিড তৈরি করো, 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 প্রতিটি ফ্রেমে GPU-তে প্রায় ৬.৫ কেবি ডেটা আপলোড করে।
আমরা মেশ বা এন্টিটি আবার তৈরি করি না।
এখন সব একসাথে দেখা যাক।
আমরা টেবিলের উপর গাড়ির মডেল রাখি, তারপর ট্র্যাক আঁকার জন্য ARCore ব্যবহার করি।
আমরা যে মেশ ফ্ল্যাগ-- ফ্ল্যাগ মেশ তৈরি করেছি তা গন্তব্যে দেখা যায়।
গাড়িটি ট্র্যাক বরাবর চলে এবং আমরা গাড়ির চাকা ঘোরাতে ও স্টিয়ারিং ব্যবহার করতে glTF Introspection API ব্যবহার করি।
মেশটি অ্যাসফল্ট মেটেরিয়ালের সাথে দেখা যায়, তবে আমরা মেটেরিয়াল API-এর সাথে মেশ API ব্যবহার করে রাস্তার লুক ও ফিল পরিবর্তন করতে পারি, যেমনটি আপনি ভিডিওতে দেখতে পাচ্ছেন।
এটি যেন আপনার হাতের মুঠোয় থাকা কোনও শক্তিশালী জিনিস।
আজ, আমরা আপনার অ্যাপকে 2D থেকে স্পেশাল করার অনেক উপায় কভার করেছি।
glTF ইন্ট্রোস্পেকশন API-এর মাধ্যমে, আপনি এখন সরাসরি glTF মডেলের ইনার মেটাডেটা ট্যাপ করতে পারবেন, যা রানটাইমে এম্বেড করা অ্যানিমেশন ট্রিগার করে, স্বতন্ত্র সাবনোড ট্রান্সফর্ম করে এবং এমনকি এর লুক ও ফিল পরিবর্তন করার জন্য মেটেরিয়াল সোয়্যাপ করে।
তারপর, আমরা দেখেছি যে আগে থেকে তৈরি 3D মডেল যথেষ্ট না হলে, কাস্টম জিওমেট্রি তৈরি করতে আমরা ব্র্যান্ডের নতুন Mesh API ব্যবহার করতে পারি।
আপনি স্ক্র্যাচ থেকে স্ট্যাটিক আকার তৈরি করতে চান বা রিয়েল টাইমে ডাইনামিক জ্যামিতির ডাইনামিক আকার স্ট্রিম করতে চান, আপনার কাছে এখন তা করার টুল রয়েছে।
এবং ইগর যেমন দেখিয়েছেন, Gemini-র সাথে সেইসব বিল্ডিং ব্লক পেয়ার করার অর্থ হল, আপনাকে একা একা অঙ্ক করতে হবে না।
এখন, আমি আগে যা বলেছি তা আবার বলব: আপনি যা দেখেছেন তার সবকিছুই Android অ্যাপে চলছে, যা Android Studio-র মধ্যে ইডিওমেটিক Kotlin-এ লেখা হয়েছে।
আজকে দর্শকদের মধ্যে অনেক ল্যাপটপ দেখতে পাচ্ছি এবং আমি আশা করি যে ইগর এবং আমি আপনাকে Android Studio খুলতে এবং আজই Android XR এমুলেটর চালু করতে যথেষ্ট উৎসাহিত করেছি।
আপনার কল্পনাশক্তিকে কাজে লাগান এবং আপনার আগে থেকে থাকা Android দক্ষতা ব্যবহার করুন, সাথে আমরা আজ যা দেখিয়েছি তা ব্যবহার করে অসাধারণ XR অভিজ্ঞতা তৈরি করুন।
ইগর এবং আমার পক্ষ থেকে, আজ আমাদের সাথে যোগ দেওয়ার জন্য আপনাকে অনেক ধন্যবাদ এবং 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))
}