Pengembangan shader GLSL di Kotlin



Halo semuanya!

Perusahaan kami sedang mengembangkan game online dan sekarang kami sedang mengerjakan versi mobile dari proyek utama kami. Pada artikel ini kami ingin berbagi pengalaman mengembangkan shader GLSL untuk proyek Android dengan contoh dan sumber .

Tentang proyek


Awalnya, game ini berbasis browser pada Flash, tetapi berita tentang penghentian dukungan untuk Flash memaksa kami untuk mentransfer proyek ke HTML5. Kotlin digunakan sebagai bahasa pengembangan, dan enam bulan kemudian kami dapat meluncurkan proyek di Android. Sayangnya, tanpa optimasi pada perangkat seluler, gim ini tidak memiliki kinerja.

Untuk meningkatkan FPS, diputuskan untuk mendesain ulang mesin grafis. Kami dulu menggunakan beberapa shader universal, tetapi sekarang untuk setiap efek kami memutuskan untuk menulis shader terpisah, diasah untuk tugas tertentu, agar dapat membuat pekerjaan mereka lebih efisien.

Apa yang kurang dari kita


Shader dapat disimpan dalam sebuah string, tetapi metode ini menghilangkan pemeriksaan sintaksis dan pencocokan tipe, sehingga shader biasanya disimpan dalam Aset atau file Raw, karena ini memungkinkan Anda untuk mengaktifkan validasi dengan menginstal plug-in untuk Android Studio. Tetapi pendekatan ini juga memiliki kelemahan - kurangnya penggunaan kembali: untuk melakukan pengeditan kecil, Anda harus membuat file shader baru.

Jadi itu:

- Mengembangkan shader di Kotlin,
- lakukan pemeriksaan sintaks pada tahap kompilasi,
- dapat menggunakan kembali kode antara shader,
Saya perlu menulis "konverter" Kotlin ke GLSL.

Hasil yang diinginkan: kode shader digambarkan sebagai kelas Kotlin, di mana atribut, variasi, seragam adalah properti dari kelas ini. Parameter konstruktor utama kelas digunakan untuk cabang statis dan memungkinkan Anda untuk menggunakan kembali sisa kode shader. Blok init adalah badan shader.

Solusi


Untuk implementasi, delegasi Kotlin digunakan. Mereka mengizinkan runtime untuk mengetahui nama properti yang didelegasikan, untuk menangkap get dan set saat panggilan dan untuk memberitahu mereka tentang ShaderBuilder - kelas dasar dari semua shader.

class ShaderBuilder { val uniforms = HashSet<String>() val attributes = HashSet<String>() val varyings = HashSet<String>() val instructions = ArrayList<Instruction>() ... fun getSource(): String = ... } 

Mendelegasikan Implementasi
Variasi delegasi:

 class VaryingDelegate<T : Variable>(private val factory: (ShaderBuilder) -> T) { private lateinit var v: T operator fun provideDelegate(ref: ShaderBuilder, p: KProperty<*>): VaryingDelegate<T> { v = factory(ref) v.value = p.name return this } operator fun getValue(thisRef: ShaderBuilder, property: KProperty<*>): T { thisRef.varyings.add("${v.typeName} ${property.name}") return v } operator fun setValue(thisRef: ShaderBuilder, property: KProperty<*>, value: T) { thisRef.varyings.add("${v.typeName} ${property.name}") thisRef.instructions.add(Instruction.assign(property.name, value.value)) } } 

Implementasi dari delegasi yang tersisa di GitHub .

Contoh shader:

 //    useAlphaTest     , //       , ,  , //   . class FragmentShader(useAlphaTest: Boolean) : ShaderBuilder() { private val alphaTestThreshold by uniform(::GLFloat) private val texture by uniform(::Sampler2D) private val uv by varying(::Vec2) init { var color by vec4() color = texture2D(texture, uv) // static branching if (useAlphaTest) { // dynamic branching If(color.w lt alphaTestThreshold) { discard() } } //     ShaderBuilder. gl_FragColor = color } } 

Dan di sini adalah sumber GLSL yang dihasilkan (hasil dari FragmentShader (useAlphaTest = true) .getSource ()). Konten dan struktur kode dipertahankan:

 uniform sampler2D texture; uniform float alphaTestThreshold; varying vec2 uv; void main(void) { vec4 color; color = texture2D(texture, uv); if ((color.w < alphaTestThreshold)) { discard; } gl_FragColor = color; } 

Lebih mudah untuk menggunakan kembali kode shader dengan mengatur parameter yang berbeda saat merakit sumber, tetapi ini tidak sepenuhnya menyelesaikan masalah penggunaan kembali. Jika Anda perlu menulis kode yang sama di shader yang berbeda, Anda dapat memasukkan instruksi ini ke dalam ShaderBuilderComponent yang terpisah dan menambahkannya seperlunya ke ShaderBuilders utama:

 class ShadowReceiveComponent : ShaderBuilderComponent() { … fun vertex(parent: ShaderBuilder, inp: Vec4) { vShadowCoord = shadowMVP * inp ... parent.appendComponent(this) } fun fragment(parent: ShaderBuilder, brightness: GLFloat) { var pixel by float() pixel = texture2D(shadowTexture, vShadowCoord.xy).x ... parent.appendComponent(this) } } 

Hore, fungsi yang diterima memungkinkan Anda untuk menulis shader di Kotlin, menggunakan kembali kode, memeriksa sintaks!

Sekarang, mari kita ingat Swizzling di GLSL dan lihat implementasinya di Vec2, Vec3, Vec4.

 class Vec2 { var x by ComponentDelegate(::GLFloat) var y by ComponentDelegate(::GLFloat) } class Vec3 { var x by ComponentDelegate(::GLFloat) ... //  9 Vec2 var xx by ComponentDelegate(::Vec2) var xy by ComponentDelegate(::Vec2) ... } class Vec4 { var x by ComponentDelegate(::GLFloat) ... //  16 Vec2 var xy by ComponentDelegate(::Vec2) ... //  64 Vec3 var xxx by ComponentDelegate(::Vec3) ... } 

Dalam proyek kami, kompilasi shader dapat terjadi dalam siklus permainan sesuai permintaan, dan alokasi objek seperti itu menghasilkan panggilan GC besar, kelambatan muncul. Oleh karena itu, kami memutuskan untuk mentransfer perakitan sumber shader ke tahap kompilasi menggunakan prosesor anotasi.

Kami menandai kelas dengan anotasi ShaderProgram:

 @ShaderProgram(VertexShader::class, FragmentShader::class) class ShaderProgramName(alphaTest: Boolean) 

Dan prosesor anotasi mengumpulkan semua jenis shader tergantung pada parameter konstruktor vertex dan kelas fragmen untuk kita:

 class ShaderProgramNameSources { enum class Sources(vertex: String, fragment: String): ShaderProgramSources { Source0("<vertex code>", "<fragment code>") ... } fun get(alphaTest: Boolean) { if (alphaTest) return Source0 else return Source1 } } 

Sekarang Anda bisa mendapatkan teks shader dari kelas yang dihasilkan:

 val sources = ShaderProgramNameSources.get(replaceAlpha = true) println(sources.vertex) println(sources.fragment) 

Karena hasil dari fungsi get - ShaderProgramSources adalah nilai dari enum, mudah untuk menggunakannya sebagai kunci dalam registri program (ShaderProgramSources) -> CompiledShaderProgram.

GitHub memiliki kode sumber untuk proyek tersebut, termasuk prosesor anotasi dan contoh sederhana shader dan komponen.

Source: https://habr.com/ru/post/id425027/


All Articles