Android uygulamalarınızı geleneksel 2D ekranlardan uzamsal ortamlara geçirmek için şekil oluşturma konusunda yeni bir yaklaşım gerekir.
Bu oturumda, Jetpack XR SDK'daki Custom Mesh API'yi inceleyeceğiz. Bu API, doğrudan çalışma zamanında programatik olarak prosedürel 3D geometri oluşturmanıza olanak tanır. Tepe noktası düzenlerini tanımlamaktan dinamik nesneleri kullanıcının fiziksel alanında sorunsuz bir şekilde oluşturmaya kadar temel iş akışını inceleyeceğiz.
Bu konuşmanın sonunda, tamamen kod kullanarak etkileşimli ve özel 3D geometri oluşturma konusunda pratik beceriler edinmiş olacaksınız.
MeshBuffer gibi dört yapı taşı, 3D araçlar olmadan üçgen üçgen model oluşturur.Transkript
Merhaba arkadaşlar.
Ben Jay.
Ben de Egor.
Google'da Jetpack XR üzerinde çalışan yazılım mühendisleriyiz.
XREAL Aura'nın kullanıma sunulmasına az bir zaman kaldı. Bu nedenle, platformda mükemmel uzamsal deneyimler oluşturmak için ihtiyacınız olan tüm araçlara sahip olduğunuzdan emin olmak istiyoruz.
En iyi tarafı ise başlamak için XR donanımına bile ihtiyacınız olmaması.
Göstereceğimiz tüm özellikler, Android Studio'daki Android XR emülatöründe kullanılabilir.
Amacımız, Jetpack SceneCore kitaplığı sayesinde platformda zengin uzamsal deneyimler oluşturmak için derin 3D bilgisine ihtiyacınız olmadığını göstermektir.
Göreceğiniz her şey, doğrudan Android Studio'da deyimsel Kotlin dilinde yazılmış bir Android uygulamasında çalışır.
Kısa süre önce Jetpack SceneCore'u kararlı sürüme geçirdik. Bugün, 3D model API'lerimizin bazı heyecan verici gelişmelerini ve anında özel 3D geometri oluşturmanıza olanak tanıyan yepyeni mesh API'lerimizi ele alacağız.
Ancak 3D modellerle başlayalım.
3D modeller söz konusu olduğunda ilk olarak bir dosya biçimine ihtiyacımız vardır.
Açık standartlar, Android XR'ın temel bir parçasıdır.
Bu nedenle, Khronos Group'un glTF biçimini kullanmaya karar verdik.
3D dünyasından değilseniz "glTF nedir?" diye soruyor olabilirsiniz.
Bunu 3D modeller için JPEG gibi düşünebilirsiniz.
glTF öğe yapısı; animasyonlar, alt düğümler ve mesh'ler gibi öğeler için veri sağlar.
{
"asset" : {...},
"nodes" : [
{
"name" : "Right_Arm",
"rotation" : [...],
"scale" : [...],
"translation" : [...],
},
],
"animations" : [
{
"name" : "Wave",
},
],
"meshes" : [
{
"name" : "Torso",
"primitives" : [ ... ],
},
],
// Way way more!
}Ancak bu konuşma kapsamında, alt düğümleri bir modelin parçaları olarak düşünebilirsiniz. Her birinin kendi konumu ve dönüşü vardır.
Ağlar, gördüğünüz yüzeylerdir. Animasyonlar ise öğe yazarının önceden hazırladığı alt düğümlerin ve ağların hareketleridir.
Mükemmel.
Bu noktada, "glTF modelini nereden bulabilirim?" diye soruyor olabilirsiniz.
Mevcut birçok modelin yanı sıra kendi modelinizi de oluşturabilirsiniz.
glTF biçiminin temel özelliklerini öğrendiğimize göre artık bu biçimi uygulamamızda kullanabiliriz.
Jetpack SceneCore'u kullanan temel bir Android XR projesiyle başlıyoruz.
Kullanıcının alanında etkinliğin yer aldığı bir uzamsal panel görebilirsiniz.
Bu pencereyi tutup taşıyabilir ve dilediğiniz gibi yeniden boyutlandırabilirsiniz.
Bu konuşma tamamen 3D ile ilgili olsa da Jetpack XR SDK'nın geri kalanına da mutlaka göz atın.
Üç boyutlu paneller eğlenceli ancak daha fazlasını istiyoruz.
Sihirli bir dokunuş katmak için 3D model eklemekten daha iyi bir yol olabilir mi?
Tasarımcımız bu araba glTF modelini oluşturdu.
// 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
)Hatta daha sonra kullanacağımız animasyonları da eklemişler.
SceneCore'u kullanarak 3D model API'lerimizle bu modeli doğrudan sahnemize yükleyebilirim.
Ancak arabayı biraz daha canlı hale getirelim.
glTF'deki tüm o zengin meta verileri hatırlıyor musunuz?
Yeni glTF introspection API ile artık animasyonlardan başlayarak bu verilere erişebilirsiniz.
// 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()Yalnızca birkaç satır kodla, araba modelinde yerleşik olarak bulunan tüm glTF animasyonlarını (ör. kapıları, kaputu veya bagajı açıp kapatma) ayrı ayrı oynatabilirim.
Ancak erişebileceğiniz tek meta veri türü animasyonlar değildir.
glTF introspection API, modeldeki alt düğümleri GltfModelNode olarak da gösterir. Böylece bunları istediğiniz gibi taşıyabilir, döndürebilir ve ölçeklendirebilirsiniz.
// 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) }
}Örneğin, burada araba hareket ederken tekerleklerin konumunu ve dönüşünü ayrı ayrı güncelleyebilirim.
Gördüğünüz şeyin yerleştirilmiş bir glTF animasyonu olmadığını belirtmek isteriz.
Burada, çalışma zamanında tekerlekleri ayrı ayrı döndürüp yönlendiriyorum.
Erişebileceğiniz son meta veri parçası, glTF modelindeki herhangi bir örgüdür.
Egor, birazdan mesh'leri daha ayrıntılı bir şekilde ele alacak.
Şimdilik arabamızı şekillendirelim.
Doku API'mizi kullanarak metal desenli bir PNG resim dokusu yükleyebiliriz.
// 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)Ardından, malzeme API'mizle bunu doğrudan bir KhronosPbrMaterial'e bağlayabiliriz.
3D konusunda yeniyseniz ağı masa, malzemeyi masa örtüsü, dokuyu ise örtüye basılmış desen olarak düşünebilirsiniz.
Ancak burada, masaüstü yerine, arabanın kaputu ve kapı alt düğümleriyle ilişkili mesh'leri geçersiz kılmak için oluşturduğumuz malzeme nesnelerini kullanırız.
Yalnızca birkaç satır kodla, arabamızın görünümünü ve tarzını değiştirdik. Bu değişikliklerin tümü çalışma zamanında gerçekleşti.
Bunlar güzel fikirler ama daha da iyi bir fikrim var.
Arabanın üzerinde gideceği bir yarış pisti çizmek ne kadar eğlenceli olurdu?
Bu durumda, önceden hazırlanmış bir glTF modeli uygun değildir.
Bunun yerine, anında 3D geometri oluşturma özelliğine ihtiyacımız var.
Şimdi sözü Egor'a bırakıyorum. Kendisi size yepyeni Jetpack SceneCore mesh API'lerimiz hakkında her şeyi anlatacak.
Teşekkürler Jay.
Kullanıcının oturma odasında parmağıyla yarış pistini çizebilmesini sağlayalım.
Pist çizilene kadar mevcut olmadığından kimse önceden modelleyemez.
Bu şekli kodla, her seferinde bir üçgen oluşturarak inşa edeceğiz.
Bir ağ, uzaydaki noktalar olan köşelerle başlar.
// 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
)Her birinde konum, ışıklandırma için normal, renk ve doku koordinatları gibi özellikler bulunur.
Dizinler noktaları birleştirir.
Her üçü bir üçgen oluşturur.
İki üçgen bir kare oluşturur. Altı dizin vardır ancak iki dizin paylaşıldığı için yalnızca dört köşe bulunur.
Devam ederseniz küp elde edersiniz.
Jay'in arabası da dahil olmak üzere her 3D model aynı şekilde, yalnızca daha fazla üçgen kullanılarak oluşturulur.
Mesh API, dört yapı taşıyla bu modele eşlenir: VertexLayout, MeshBuffer, CustomMesh ve MeshEntity.
Bunları tek tek ele alalım.
İlk olarak 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()Tepe noktası verileriniz ByteBuffer'da yalnızca baytlardan oluşur ve her ağ bunları farklı şekilde paketler.
VertexLayout, sizinkilerin nasıl okunacağını belirtir: hangi özellikler, hangi tür ve her birinin nerede olduğu.
Burada bir konum, bir normal ve UV'ler (doku koordinatları) var. Toplam 32 bayt.
Özellikler ayrı arabelleklerde de bulunabilir. Böylece yalnızca değişen özellikleri güncelleyebilirsiniz.
İkincisi, ham veriler için bir kapsayıcı olan 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)Düzeni, köşe baytlarını ve 32 bit dizinleri sağlarsınız.
Veriler hemen GPU'ya kopyalanır. Böylece ByteBuffer'larınızı yeniden kullanabilir veya serbest bırakabilirsiniz.
Varsayılan olarak MeshBuffer statiktir.
Bir kez ayarlanır ve hızlı oluşturma için optimize edilir.
Çalışma zamanında değiştirmeniz gerekirse createDynamic, daha sonra güncelleyebileceğiniz sabit bir kapasite ayırır.
Üçüncü olarak, CustomMesh, MeshBuffer görünümü.
// 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)Hiçbir veri kopyalanmaz.
Hangi dizin aralığının nasıl çizileceğini (üçgenler veya üçgen şeritler) belirtir.
Burada asfalt ve şerit işaretleri aynı MeshBuffer içinde yer alır.
Birden fazla özel ağ aynı MeshBuffer öğesini paylaşabilir ve paylaşıma ihtiyacınız olmadığında BuilderFromMeshData, ağı ve arabelleği tek adımda oluşturur.
Son olarak MeshEntity, mesh'i sahneye yerleştirir.
// 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))Ağ, "ne" sorusunun yanıtıdır.
Varlık, "nerede" sorusunun yanıtıdır.
Panel ve Jay'in arabası gibi bu da bir varlıktır. Bu nedenle bir konumu ve bağlı olduğu bir üst öğesi vardır.
Üst öğeyi taşıdığınızda yol da onunla birlikte taşınır.
Ayrıca, her alt küme için bir malzeme sağlarsınız. Böylece aynı ağ, farklı varlıklarda farklı malzemelerle görünebilir.
Şimdi hepsini bir araya getirip yol ağını oluşturalım.
// 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)Köşe noktasını tanımlarız: konum, normal ve UV'ler, doku koordinatları.
Küçük bir yardımcı işlev, kullanıcı yolunu şeride dönüştürür.
Yoldaki her nokta için her iki tarafa iki köşe ve bunları bir sonraki çiftle birleştiren iki üçgen ekler.
Bu, ByteBuffer'a bayt yazan basit bir Kotlin kodudur.
Herhangi bir 3D aracına ihtiyacınız yoktur.
Az önce gördüğümüz BuilderFromMeshData kısayolu, mesh'i ve MeshBuffer oluşturur ve bizim için sınırları hesaplar.
Son olarak MeshEntity.create, roadMesh öğesini asfalt malzemesiyle sahneye yerleştirir.
Ancak kullanıcı çizim yaparken yolun canlı olarak büyümesini istiyoruz.
Bunun için dinamik bir ağ kullanırız.
// 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)Tek fark, GPU arabelleklerinin tahsis edilebilmesi için oluşturucuya maksimum kapasiteyi önceden vermemizdir.
Ardından, kullanıcı çizim yaparken updateVertexData ve updateIndexData, tüm ağı değil yalnızca yeni segmentleri yükler.
Dikkat edilmesi gereken bir nokta var: Oluşturucu, ekran dışındaki her şeyi atlamak için sınırları kullanır. Bu nedenle sınırları, buradaki oyun alanı gibi, pistin dönüşebileceği her şeyi kapsayacak şekilde ayarlayın.
Burada zor olanın API olmadığını fark edebilirsiniz.
Zor olan kısım köşe matematiği, normaller, doku koordinatları ve üçgen sırasıdır.
Yapay zeka, tam da bu tür kodları yazma konusunda çok başarılıdır.
Ancak işleri bir adım daha ileri götürmek istediğimizi varsayalım.
Şimdi, varış noktasında rüzgarda dalgalanan bir damalı bayrak istiyorum.
Vertex animasyon matematiği zor bir konu olduğu için Gemini'a sordum.
Android Studio'da şu istemi verdim: "Jetpack SceneCore Mesh API'yi kullanarak dama tahtası UV'leri olan 20x12 boyutunda bir bayrak ızgarası oluştur, updateVertexData ile her karede rüzgar dalgası animasyonu oluştur ve ışıklandırmanın kıvrımları takip etmesi için normalleri yeniden hesapla."
Birkaç saniye sonra çalışan bir kod aldım.
Şimdi ne yaptığına bakalım.
20'ye 12 hücreden oluşan bir ızgara olarak bayrak ağı oluşturdu. Her hücrede iki üçgen vardı ve düzen konusunda akıllıca bir seçim yaptı.
// 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()Konumlar ve normaller, Buffer 0'a yerleştirilir ve her karede güncellenir.
Doku koordinatları 1. arabelleğe yerleştirilir ve hiçbir zaman değişmez.
Ağ dinamiktir ve sınırlar, bayrağın tam salınımını kapsayacak şekilde ayarlanmıştır.
Her karede, her tepe noktası serbest kenara doğru büyüyen bir sinüs dalgası alır, böylece direk tarafı sabit kalır.
// 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))
}Normaller, komşu konumlardan yeniden hesaplanır. Bu sayede kıvrımlar ışığı yakalar.
updateVertexData, her karede GPU'ya yaklaşık 6,5 KB veri yükler.
Ağı veya öğeyi yeniden oluşturmayız.
Şimdi hepsini bir arada görelim.
Araba modelini masaya yerleştiririz, ardından ARCore'u kullanarak pisti çizeriz.
Oluşturduğumuz bayrak ağı hedefte görünür.
Araba pistte ilerlerken arabanın tekerleklerini yönlendirmek ve döndürmek için glTF Introspection API'yi kullanırız.
Örgü, asfalt malzemesiyle görünür ancak videoda görebileceğiniz gibi, yolun görünümünü ve tarzını değiştirmek için örgü API'sini malzeme API'siyle birlikte de kullanabiliriz.
Bu, parmaklarınızın ucunda çok güçlü bir araç olduğu anlamına geliyor.
Bugün, uygulamalarınızı 2D'den uzamsal ortama taşımanın birçok yolunu ele aldık.
glTF Introspection API ile artık bir glTF modelinin iç meta verilerine doğrudan erişebilir, yerleşik animasyonları tetikleyebilir, bağımsız alt düğümleri dönüştürebilir ve hatta çalışma zamanında görünümünü ve tarzını değiştirmek için malzemeleri değiştirebilirsiniz.
Ardından, önceden hazırlanmış 3D modellerin yeterli olmadığı durumlarda özel geometri oluşturmak için yepyeni Mesh API'mizden yararlanabileceğimizi gördük.
İster sıfırdan statik şekiller oluşturmanız ister dinamik şekilleri ve dinamik geometriyi gerçek zamanlı olarak yayınlamanız gereksin, artık bunu yapacak araçlara sahipsiniz.
Egor'un az önce gösterdiği gibi, bu yapı taşlarını Gemini ile eşleştirdiğinizde hesaplamaları tek başınıza yapmanız bile gerekmez.
Daha önce söylediğim bir şeyi tekrarlayacağım: Gördüğünüz her şey, Android Studio'da deyimsel Kotlin ile yazılmış bir Android uygulamasında çalışıyordu.
Bugün salonda çok sayıda dizüstü bilgisayar görüyorum. Umarım Egor ve ben, Android Studio'yu açıp Android XR emülatörü kullanmaya başlamanız için sizi yeterince heyecanlandırmışızdır.
Hayal gücünüzü serbest bırakın ve mevcut Android becerilerinizin yanı sıra bugün gösterdiklerimizi kullanarak muhteşem XR deneyimleri oluşturun.
Egor ve ben, bugün bize katıldığınız için çok teşekkür ederiz. Android Dev Summit'in geri kalanında iyi eğlenceler dileriz.
{
"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))
}