Entwicklung von GLSL-Shadern auf Kotlin



Hallo allerseits!

Unser Unternehmen entwickelt Online-Spiele und arbeitet derzeit an einer mobilen Version unseres Hauptprojekts. In diesem Artikel möchten wir die Erfahrungen bei der Entwicklung von GLSL-Shadern für ein Android-Projekt mit Beispielen und Quellen teilen.

Über das Projekt


Ursprünglich war das Spiel browserbasiert auf Flash, aber die Nachricht über die bevorstehende Einstellung der Unterstützung für Flash zwang uns, das Projekt auf HTML5 zu übertragen. Kotlin wurde als Entwicklungssprache verwendet, und sechs Monate später konnten wir das Projekt auf Android starten. Leider fehlte dem Spiel ohne Optimierung auf Mobilgeräten die Leistung.

Um die FPS zu erhöhen, wurde beschlossen, die Grafik-Engine neu zu gestalten. Früher haben wir mehrere Universal-Shader verwendet, aber jetzt haben wir beschlossen, für jeden Effekt einen separaten Shader zu schreiben, der für eine bestimmte Aufgabe geschärft wurde, um ihre Arbeit effizienter zu gestalten.

Was uns fehlte


Shader können in einer Zeichenfolge gespeichert werden. Diese Methode eliminiert jedoch die Syntaxprüfung und den Typabgleich. Daher werden Shader normalerweise in Assets- oder Raw-Dateien gespeichert, da Sie die Validierung aktivieren können, indem Sie das Plug-In für Android Studio installieren. Dieser Ansatz hat jedoch auch einen Nachteil - die fehlende Wiederverwendung: Um kleine Änderungen vorzunehmen, müssen Sie eine neue Shader-Datei erstellen.

Also das:

- Shader auf Kotlin entwickeln,
- bei der Kompilierung eine Syntaxprüfung durchführen lassen,
- in der Lage sein, den Code zwischen Shadern wiederzuverwenden,
Ich musste einen "Konverter" Kotlin zu GLSL schreiben.

Gewünschtes Ergebnis: Der Shader-Code wird als Kotlin-Klasse beschrieben, in der Attribute, Variationen und Uniformen die Eigenschaften dieser Klasse sind. Die Parameter des primären Konstruktors der Klasse werden für statische Verzweigungen verwendet und ermöglichen es Ihnen, den Rest des Shader-Codes wiederzuverwenden. Der Init-Block ist der Körper des Shaders.

Lösung


Für die Implementierung wurden Kotlin- Delegierte verwendet. Sie erlaubten der Laufzeit, den Namen der delegierten Eigenschaft herauszufinden, die Momente des Abrufens abzurufen und festzulegen und sie über ShaderBuilder zu benachrichtigen - die Basisklasse aller Shader.

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

Implementierung delegieren
Unterschiedlicher Delegierter:

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

Implementierung der verbleibenden Delegaten auf GitHub .

Shader-Beispiel:

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

Und hier ist die resultierende GLSL-Quelle (das Ergebnis von FragmentShader (useAlphaTest = true) .getSource ()). Der Inhalt und die Struktur des Codes bleiben erhalten:

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

Es ist praktisch, den Shader-Code wiederzuverwenden, indem beim Zusammenstellen der Quelle unterschiedliche Parameter festgelegt werden. Dadurch wird das Problem der Wiederverwendung jedoch nicht vollständig gelöst. Wenn Sie denselben Code in verschiedenen Shadern schreiben müssen, können Sie diese Anweisungen in eine separate ShaderBuilderComponent einfügen und sie nach Bedarf zu den wichtigsten ShaderBuilders hinzufügen:

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

Hurra, die empfangene Funktionalität ermöglicht es Ihnen, Shader auf Kotlin zu schreiben, Code wiederzuverwenden und die Syntax zu überprüfen!

Erinnern wir uns jetzt an Swizzling in GLSL und sehen uns die Implementierung in Vec2, Vec3, Vec4 an.

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

In unserem Projekt kann die Shader-Kompilierung bei Bedarf im Spielzyklus erfolgen, und solche Objektzuweisungen erzeugen große GC-Aufrufe, es treten Verzögerungen auf. Aus diesem Grund haben wir beschlossen, die Assembly der Shader-Quellen mithilfe des Anmerkungsprozessors in die Kompilierungsphase zu übertragen.

Wir kennzeichnen die Klasse mit der ShaderProgram-Annotation:

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

Und der Annotationsprozessor sammelt alle Arten von Shadern in Abhängigkeit von den Parametern der Vertex- und Fragmentklassenkonstruktoren für uns:

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

Jetzt können Sie den Shader-Text aus der generierten Klasse abrufen:

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

Da das Ergebnis der Funktion get - ShaderProgramSources der Wert von enum ist, ist es zweckmäßig, ihn als Schlüssel in der Programmregistrierung (ShaderProgramSources) -> CompiledShaderProgram zu verwenden.

GitHub enthält den Quellcode für das Projekt, einschließlich des Anmerkungsprozessors und einfacher Beispiele für Shader und Komponenten.

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


All Articles