การเปลี่ยนแอปพลิเคชัน Android จากหน้าจอ 2 มิติแบบเดิมไปเป็นสภาพแวดล้อมเชิงพื้นที่ต้องใช้แนวทางใหม่ในการสร้างรูปทรง
ในเซสชันนี้ เราจะสำรวจ Custom Mesh API ใน Jetpack XR SDK ซึ่งช่วยให้คุณสร้างเรขาคณิต 3 มิติแบบกระบวนการได้โดยตรงขณะรันไทม์ เราจะอธิบายเวิร์กโฟลว์หลัก ตั้งแต่การกำหนดเลย์เอาต์เวอร์เท็กซ์ไปจนถึงการแสดงผลออบเจ็กต์แบบไดนามิกอย่างราบรื่นภายในพื้นที่จริงของผู้ใช้
เมื่อจบการพูดคุยนี้ คุณจะมีทักษะที่นำไปใช้ได้จริงในการสร้างเรขาคณิต 3 มิติแบบอินเทอร์แอกทีฟที่กำหนดเองจากโค้ดทั้งหมด
MeshBuffer จะสร้างโมเดลทีละสามเหลี่ยมโดยไม่ต้องใช้เครื่องมือ 3 มิติข้อความถอดเสียง
สวัสดีทุกคน
ฉันชื่อ Jay
และผม Egor
และเราเป็นวิศวกรซอฟต์แวร์ที่ทำงานเกี่ยวกับ Jetpack XR ที่ Google
XREAL Aura กำลังจะเปิดตัวในเร็วๆ นี้ เราจึงต้องการให้คุณมีเครื่องมือทั้งหมดที่จำเป็นสำหรับการสร้างประสบการณ์เชิงพื้นที่ที่ยอดเยี่ยมบนแพลตฟอร์ม
แต่สิ่งที่ดีที่สุดคือคุณไม่จำเป็นต้องมีฮาร์ดแวร์ XR เพื่อเริ่มต้นใช้งานด้วยซ้ำ
ฟีเจอร์ทั้งหมดที่เราจะแสดงพร้อมให้ใช้งานแล้ววันนี้ในโปรแกรมจำลอง Android XR ใน Android Studio
เป้าหมายของเราคือการแสดงให้คุณเห็นว่าเมื่อใช้ไลบรารี Jetpack SceneCore คุณไม่จำเป็นต้องมีความรู้เชิงลึกเกี่ยวกับ 3 มิติเพื่อสร้างประสบการณ์เชิงพื้นที่ที่สมบูรณ์บนแพลตฟอร์ม
ทุกสิ่งที่คุณจะได้เห็นจะทำงานในแอป Android ซึ่งเขียนด้วย Kotlin ที่เป็นสำนวนใน Android Studio
เมื่อเร็วๆ นี้เราได้โปรโมต Jetpack SceneCore เป็นเวอร์ชันเสถียร และวันนี้เราจะพูดถึงความก้าวหน้าที่น่าสนใจของ API โมเดล 3 มิติ รวมถึง API ของ Mesh ใหม่ล่าสุด ซึ่งช่วยให้คุณสร้างเรขาคณิต 3 มิติที่กำหนดเองได้ทันที
แต่เรามาเริ่มกันที่โมเดล 3 มิติก่อน
เมื่อพูดถึงโมเดล 3 มิติ สิ่งแรกที่เราต้องการคือรูปแบบไฟล์
มาตรฐานแบบเปิดเป็นส่วนสำคัญของ Android XR
ดังนั้น เราจึงใช้รูปแบบ glTF จาก Khronos Group อย่างเต็มที่
หากคุณไม่ได้อยู่ในวงการ 3 มิติ คุณอาจสงสัยว่า "glTF คืออะไร"
คุณอาจคิดว่าไฟล์นี้เป็นเหมือนไฟล์ JPEG แต่ใช้สำหรับโมเดล 3 มิติ
โครงสร้างชิ้นงาน glTF จะให้ข้อมูลสำหรับสิ่งต่างๆ เช่น ภาพเคลื่อนไหว โหนดย่อย และ Mesh
{
"asset" : {...},
"nodes" : [
{
"name" : "Right_Arm",
"rotation" : [...],
"scale" : [...],
"translation" : [...],
},
],
"animations" : [
{
"name" : "Wave",
},
],
"meshes" : [
{
"name" : "Torso",
"primitives" : [ ... ],
},
],
// Way way more!
}แต่สำหรับการพูดคุยนี้ คุณสามารถคิดว่าโหนดรองเป็นส่วนหนึ่งของโมเดล ซึ่งแต่ละโหนดมีตำแหน่งและการหมุนของตัวเอง
เมชคือพื้นผิวที่คุณเห็น และภาพเคลื่อนไหวคือการเคลื่อนไหวของโหนดย่อยและเมชที่ผู้เขียนชิ้นงานอบไว้ล่วงหน้า
เยี่ยมเลย
ตอนนี้คุณอาจสงสัยว่า "ฉันจะหาโมเดล glTF ได้จากที่ไหน"
นอกจากโมเดลที่มีอยู่มากมายแล้ว คุณยังสร้างโมเดลของคุณเองได้ด้วย
ตอนนี้เราได้พูดถึงพื้นฐานของรูปแบบ glTF แล้ว มาลองใช้ในแอปกันเลย
เราจะเริ่มด้วยโปรเจ็กต์ Android XR พื้นฐานที่ใช้ Jetpack SceneCore
คุณจะเห็นแผงเชิงพื้นที่ที่มีกิจกรรมลอยอยู่ในพื้นที่ของผู้ใช้
คุณสามารถจับ ย้าย และปรับขนาดได้ตามต้องการ
และแม้ว่าการพูดคุยนี้จะเกี่ยวกับ 3 มิติทั้งหมด แต่อย่าลืมดู Jetpack XR SDK ที่เหลือด้วย
เอาล่ะ แผงเชิงพื้นที่ก็สนุกดี แต่เราต้องการความมหัศจรรย์มากกว่านี้
และจะมีวิธีไหนดีไปกว่าการเพิ่มความมหัศจรรย์ด้วยการใส่โมเดล 3 มิติ
พอดีว่านักออกแบบของเราสร้างโมเดล 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 โมเดล 3 มิติเพื่อโหลดโมเดลลงในฉากได้โดยตรง
แต่มาทำให้รถดูมีชีวิตชีวาขึ้นอีกสักหน่อย
ยังจำข้อมูลเมตาที่สมบูรณ์ทั้งหมดใน 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 ที่ฝังอยู่
ในที่นี้ ฉันจะหมุนและบังคับล้อแต่ละล้อพร้อมกันในรันไทม์
ข้อมูลเมตาสุดท้ายที่คุณเข้าถึงได้คือ Mesh ในโมเดล glTF
ต่อไป Egor จะมาเจาะลึกเรื่อง Mesh กันในอีกสักครู่
แต่ตอนนี้มาแต่งรถกันก่อน
เมื่อใช้ 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 ให้คิดว่าเมชเป็นโต๊ะ วัสดุเป็นผ้าปูโต๊ะ และพื้นผิวเป็นลวดลายที่พิมพ์บนผ้าปูโต๊ะนั้น
แต่ในกรณีนี้ เราจะใช้วัตถุ Material ที่สร้างขึ้นเพื่อลบล้าง Mesh ที่เชื่อมโยงกับโหนดย่อยของฝากระโปรงและประตูรถแทนที่จะเป็นโต๊ะ
โค้ดเพียงไม่กี่บรรทัดช่วยให้เราเปลี่ยนรูปลักษณ์ของรถได้อีกครั้ง ทั้งหมดนี้ทำได้ในรันไทม์
นั่นเป็นไอเดียที่เจ๋งมาก แต่เรามีไอเดียที่ดียิ่งกว่านั้น
จะดีแค่ไหนถ้าเราวาดสนามแข่งให้รถวิ่งได้
ในกรณีนี้ โมเดล glTF ที่สร้างไว้ล่วงหน้าจึงไม่เหมาะสม
แต่เราต้องการความสามารถในการสร้างเรขาคณิต 3 มิติได้ทันที
ผมจะส่งต่อให้ Egor เพื่อบอกคุณเกี่ยวกับ Mesh API ของ Jetpack SceneCore ใหม่ล่าสุด
ขอบคุณ Jay
มาทำให้ผู้ใช้สามารถวาดสนามแข่งรถใน... ด้วยนิ้วมือในห้องนั่งเล่นได้
สนามแข่งจะไม่มีอยู่จนกว่าจะมีการวาดขึ้น ดังนั้นจึงไม่มีใครสามารถจำลองสนามแข่งล่วงหน้าได้
เราจะสร้างด้วยโค้ดทีละสามเหลี่ยม
เมชเริ่มต้นด้วยจุดยอด ซึ่งเป็นจุดในพื้นที่ว่าง
// 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
)แต่ละจุดจะมีแอตทริบิวต์ เช่น ตำแหน่ง, เวกเตอร์ปกติสำหรับการจัดแสง, สี, พิกัดพื้นผิว
ดัชนีจะเชื่อมโยงข้อมูลต่างๆ เข้าด้วยกัน
ทุกๆ 3 จุดจะสร้างสามเหลี่ยมได้ 1 รูป
สามเหลี่ยม 2 รูปประกอบกันเป็นสี่เหลี่ยมจัตุรัส มีดัชนี 6 ตัว แต่มีจุดยอดเพียง 4 จุด เนื่องจากใช้จุดยอดร่วมกัน 2 จุด
เล่นต่อไปแล้วคุณจะได้ลูกบาศก์
โมเดล 3 มิติทุกโมเดล รวมถึงรถของเจย์ สร้างขึ้นในลักษณะเดียวกัน เพียงแต่มีสามเหลี่ยมมากกว่า
Mesh API จะแมปกับสถาปัตยกรรมดังกล่าวโดยใช้องค์ประกอบ 4 อย่าง ได้แก่ 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()ข้อมูล Vertex เป็นเพียงไบต์ใน ByteBuffer และทุก Mesh จะแพ็กข้อมูลแตกต่างกัน
VertexLayout จะบอกวิธีอ่านข้อมูลของคุณ เช่น แอตทริบิวต์ใด ประเภทใด และแต่ละแอตทริบิวต์อยู่ที่ใด
ในที่นี้คือตำแหน่ง เวกเตอร์แนวฉาก และ UV ซึ่งเป็นพิกัดพื้นผิว รวมทั้งหมด 32 ไบต์
นอกจากนี้ แอตทริบิวต์ยังอยู่ในบัฟเฟอร์แยกต่างหากได้ด้วย เพื่อให้คุณอัปเดตเฉพาะแอตทริบิวต์ที่มีการเปลี่ยนแปลง
ประการที่ 2 คือ 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 ทันที เพื่อให้คุณนำ ByteBuffer กลับมาใช้ใหม่หรือปล่อยได้
โดยค่าเริ่มต้น MeshBuffer จะเป็นแบบคงที่
โดยจะตั้งค่าเพียงครั้งเดียวและเพิ่มประสิทธิภาพเพื่อการแสดงผลที่รวดเร็ว
และหากคุณต้องการเปลี่ยนที่รันไทม์ createDynamic จะจัดสรรความจุคงที่ซึ่งคุณอาจอัปเดตในภายหลัง
ประการที่ 3 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 จะวาง Mesh ในฉาก
// 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 ซึ่งเป็นเอนทิตี จึงมีตำแหน่งและรายการหลัก ซึ่งเป็นสิ่งที่ติดอยู่
เมื่อย้ายถนนหลัก ถนนที่เชื่อมต่อก็จะย้ายตามไปด้วย
นอกจากนี้ คุณยังระบุวัสดุ 1 รายการต่อชุดย่อย เพื่อให้เมชเดียวกันปรากฏพร้อมกับวัสดุที่แตกต่างกันในเอนทิตีที่ต่างกันได้
ตอนนี้เรามาประกอบทุกอย่างเข้าด้วยกันและสร้าง Mesh ซึ่งก็คือ Mesh ของถนน
// 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, พิกัดพื้นผิว
ฟังก์ชันตัวช่วยขนาดเล็กจะเปลี่ยนเส้นทางของผู้ใช้ให้เป็นริบบิ้น
สำหรับแต่ละจุดบนเส้นทาง ระบบจะเพิ่มจุดยอด 2 จุดในแต่ละด้านและสามเหลี่ยม 2 รูปที่เชื่อมต่อจุดยอดเหล่านั้นกับคู่ถัดไป
ซึ่งเป็น Kotlin ธรรมดาที่เขียนไบต์ลงใน ByteBuffer
คุณไม่จำเป็นต้องใช้เครื่องมือ 3 มิติใดๆ
BuilderFromMeshData ซึ่งเป็นทางลัดที่เราเพิ่งเห็นจะสร้าง Mesh และ 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 จะอัปโหลดเฉพาะส่วนใหม่ ไม่ใช่ทั้ง Mesh
ข้อควรระวัง: ตัวแสดงผลจะใช้ขอบเขตเพื่อข้ามสิ่งต่างๆ ที่อยู่นอกหน้าจอ ดังนั้นให้ตั้งค่าขอบเขตให้ครอบคลุมทุกอย่างที่แทร็กอาจกลายเป็น เช่น พื้นที่เล่นที่นี่
คุณอาจสังเกตเห็นว่า API ไม่ใช่ส่วนที่ยากในที่นี้
ส่วนที่ยากคือคณิตศาสตร์จุดยอด, เวกเตอร์ปกติ, พิกัดพื้นผิว, ลำดับสามเหลี่ยม
ซึ่งเป็นโค้ดประเภทที่ AI เขียนได้ดีมาก
แต่สมมติว่าเราต้องการยกระดับไปอีกขั้น
ตอนนี้ฉันอยากได้ธงตาหมากรุกที่โบกสะบัดตามลมที่จุดหมายปลายทาง
คณิตศาสตร์แอนิเมชันเวอร์เท็กซ์เป็นส่วนที่ยาก ฉันจึงขอความช่วยเหลือจาก Gemini
ใน Android Studio ฉันให้พรอมต์เดียวว่า "ใช้ Jetpack SceneCore Mesh API เพื่อสร้างตารางธงขนาด 20x12 ที่มี UV ลายตารางหมากรุก สร้างภาพเคลื่อนไหวคลื่นลมทุกเฟรมด้วย updateVertexData และคำนวณเวกเตอร์แนวฉากใหม่เพื่อให้แสงส่องตามรอยพับ"
ไม่กี่วินาทีต่อมา ฉันก็ได้โค้ดที่ใช้งานได้
มาดูสิ่งที่เกิดขึ้นกัน
โดยสร้างตาข่ายธงเป็นตารางกริดขนาด 20 คูณ 12 เซลล์ โดยมีสามเหลี่ยม 2 รูปต่อเซลล์ และเลือกเลย์เอาต์ได้อย่างชาญฉลาด
// 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.5 KB ไปยัง GPU ในแต่ละเฟรม
เราจะไม่สร้าง Mesh หรือเอนทิตีขึ้นมาใหม่
ทีนี้มาดูภาพรวมกัน
เราวางโมเดลรถบนโต๊ะ แล้วใช้ ARCore เพื่อวาดแทร็ก
Mesh ของธงที่เราเพิ่งสร้างจะปรากฏที่ปลายทาง
รถจะขับไปตามเส้นทาง และเราใช้ glTF Introspection API เพื่อบังคับและหมุนล้อรถ
Mesh จะปรากฏขึ้นพร้อมกับวัสดุยางมะตอย แต่เรายังใช้ Mesh API ร่วมกับ Material API เพื่อเปลี่ยนรูปลักษณ์ของถนนได้ด้วย ดังที่เห็นในวิดีโอ
ซึ่งเป็นเครื่องมือที่ทรงพลังมากและใช้งานได้ง่ายๆ เพียงปลายนิ้วสัมผัส
วันนี้เราได้พูดถึงวิธีต่างๆ มากมายในการเปลี่ยนแอปจาก 2 มิติเป็นเชิงพื้นที่
glTF Introspection API ช่วยให้คุณเข้าถึงข้อมูลเมตาภายในของโมเดล glTF ได้โดยตรง ซึ่งจะทริกเกอร์ภาพเคลื่อนไหวที่ฝังไว้ เปลี่ยนรูปแบบโหนดย่อยแต่ละโหนด และแม้แต่สลับวัสดุเพื่อเปลี่ยนรูปลักษณ์ทั้งหมดขณะรันไทม์
จากนั้นเราเห็นว่าเมื่อโมเดล 3 มิติที่สร้างไว้ล่วงหน้าไม่เพียงพอ เราสามารถใช้ประโยชน์จาก Mesh API ใหม่ล่าสุดเพื่อสร้างเรขาคณิตที่กำหนดเองได้
ไม่ว่าคุณจะต้องการสร้างรูปร่างแบบคงที่ตั้งแต่ต้นหรือสตรีมรูปร่างแบบไดนามิกในแบบเรียลไทม์ ตอนนี้คุณก็มีเครื่องมือที่พร้อมใช้งานแล้ว
และอย่างที่ Egor เพิ่งแสดงให้เห็น การจับคู่บล็อกเหล่านั้นกับ Gemini หมายความว่าคุณไม่จำเป็นต้องคำนวณด้วยตัวเองด้วยซ้ำ
ตอนนี้ผมจะขอย้ำสิ่งที่พูดไปก่อนหน้านี้ว่า ทุกอย่างที่คุณเห็นทำงานในแอป Android ซึ่งเขียนด้วย Kotlin ที่เป็นสำนวนใน Android Studio โดยตรง
วันนี้เราเห็นแล็ปท็อปจำนวนมากในกลุ่มผู้ชม และหวังว่าเรากับ Egor จะทำให้คุณตื่นเต้นมากพอที่จะเปิด Android Studio และเริ่มใช้โปรแกรมจำลอง Android XR ในวันนี้
ปลดปล่อยจินตนาการของคุณให้เต็มที่ และใช้ทักษะ Android ที่มีอยู่ร่วมกับสิ่งที่เราแสดงให้เห็นในวันนี้เพื่อสร้างประสบการณ์ XR ที่น่าทึ่ง
ในนามของ Egor และตัวฉันเอง ขอขอบคุณที่มาร่วมเซสชันของเราในวันนี้ และขอให้สนุกกับงาน 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))
}