Développement de shaders GLSL sur Kotlin



Bonjour à tous!

Notre société développe des jeux en ligne et maintenant nous travaillons sur une version mobile de notre projet principal. Dans cet article, nous voulons partager l'expérience de développement de shaders GLSL pour un projet Android avec des exemples et des sources .

À propos du projet


Initialement, le jeu était basé sur un navigateur basé sur Flash, mais la nouvelle de l'arrêt imminent de la prise en charge de Flash nous a obligés à transférer le projet en HTML5. Kotlin a été utilisé comme langage de développement et six mois plus tard, nous avons pu lancer le projet sur Android. Malheureusement, sans optimisation sur les appareils mobiles, le jeu manquait de performances.

Pour augmenter le FPS, il a été décidé de repenser le moteur graphique. Nous avions l'habitude d'utiliser plusieurs shaders universels, mais maintenant, pour chaque effet, nous avons décidé d'écrire un shader séparé, affûté pour une tâche spécifique, afin de pouvoir rendre leur travail plus efficace.

Ce qui nous manquait


Les shaders peuvent être stockés dans une chaîne, mais cette méthode élimine la vérification de la syntaxe et la correspondance de type, donc les shaders sont généralement stockés dans des fichiers Assets ou Raw, car cela vous permet d'activer la validation en installant le plug-in pour Android Studio. Mais cette approche a aussi un inconvénient - le manque de réutilisation: pour faire de petites modifications, vous devez créer un nouveau fichier shader.

Alors que:

- développer des shaders sur Kotlin,
- avoir une vérification de la syntaxe au stade de la compilation,
- pouvoir réutiliser le code entre shaders,
J'avais besoin d'écrire un «convertisseur» Kotlin en GLSL.

Résultat souhaité: le code de shader est décrit comme une classe Kotlin, dans laquelle les attributs, les variations, les uniformes sont les propriétés de cette classe. Les paramètres du constructeur principal de la classe sont utilisés pour les branches statiques et vous permettent de réutiliser le reste du code du shader. Le bloc init est le corps du shader.

Solution


Pour la mise en œuvre, des délégués Kotlin ont été utilisés. Ils ont permis au runtime de trouver le nom de la propriété déléguée, de capturer les moments get et set des appels et de les informer de ShaderBuilder - la classe de base de tous les shaders.

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

Délégation de la mise en œuvre
Délégué variable:

 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)) } } 

Implémentation des délégués restants sur GitHub .

Exemple de 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 } } 

Et voici la source GLSL résultante (le résultat de FragmentShader (useAlphaTest = true) .getSource ()). Le contenu et la structure du code sont préservés:

 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; } 

Il est pratique de réutiliser le code du shader en définissant différents paramètres lors de l'assemblage de la source, mais cela ne résout pas complètement le problème de réutilisation. Dans le cas où vous devez écrire le même code dans différents shaders, vous pouvez mettre ces instructions dans un ShaderBuilderComponent séparé et les ajouter si nécessaire aux ShaderBuilders principaux:

 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) } } 

Hourra, la fonctionnalité reçue vous permet d'écrire des shaders sur Kotlin, de réutiliser du code, de vérifier la syntaxe!

Maintenant, rappelons Swizzling dans GLSL et regardons son implémentation dans 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) ... } 

Dans notre projet, la compilation des shaders peut se produire dans le cycle de jeu à la demande, et de telles allocations d'objets génèrent des appels GC majeurs, des décalages apparaissent. Par conséquent, nous avons décidé de transférer l'assemblage des sources de shader à l'étape de compilation à l'aide du processeur d'annotation.

Nous marquons la classe avec l'annotation ShaderProgram:

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

Et le processeur d'annotation recueille toutes sortes de shaders en fonction des paramètres des constructeurs de classes de vertex et de fragments pour nous:

 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 } } 

Vous pouvez maintenant obtenir le texte du shader de la classe générée:

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

Comme le résultat de la fonction get - ShaderProgramSources est la valeur de enum, il est pratique de l'utiliser comme clés dans le registre du programme (ShaderProgramSources) -> CompiledShaderProgram.

GitHub a le code source du projet, y compris le processeur d'annotation et des exemples simples de shaders et de composants.

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


All Articles