Desenvolvimento de shaders GLSL no Kotlin



Olá pessoal!

Nossa empresa está desenvolvendo jogos online e agora estamos trabalhando em uma versão móvel do nosso projeto principal. Neste artigo, queremos compartilhar a experiência de desenvolvimento de shaders GLSL para um projeto Android com exemplos e fontes .

Sobre o projeto


Inicialmente, o jogo era baseado em navegador no Flash, mas as notícias sobre a cessação iminente do suporte ao Flash nos forçaram a transferir o projeto para o HTML5. Kotlin foi usado como a linguagem de desenvolvimento e, seis meses depois, conseguimos lançar o projeto no Android. Infelizmente, sem otimização em dispositivos móveis, o jogo não teve desempenho.

Para aumentar o FPS, foi decidido redesenhar o mecanismo gráfico. Costumávamos usar vários shaders universais, mas agora, para cada efeito, decidimos escrever um shader separado, afiado para uma tarefa específica, a fim de tornar seu trabalho mais eficiente.

O que nos faltava


Os sombreadores podem ser armazenados em uma string, mas esse método elimina a verificação de sintaxe e a correspondência de tipos; portanto, os sombreadores geralmente são armazenados em arquivos Assets ou Raw, pois isso permite ativar a validação instalando o plug-in para o Android Studio. Mas essa abordagem também tem uma desvantagem - a falta de reutilização: para fazer pequenas edições, é necessário criar um novo arquivo de sombreador.

Para que:

- desenvolver shaders no Kotlin,
- faça uma verificação de sintaxe na fase de compilação,
- poder reutilizar o código entre shaders,
Eu precisava escrever um "conversor" Kotlin para GLSL.

Resultado desejado: o código shader é descrito como uma classe Kotlin, na qual atributos, variações e uniformes são as propriedades dessa classe. Os parâmetros do construtor principal da classe são usados ​​para ramificações estáticas e permitem reutilizar o restante do código de sombreador. O bloco init é o corpo do shader.

Solução


Para implementação, foram utilizados delegados da Kotlin. Eles permitiram que o tempo de execução descobrisse o nome da propriedade delegada, capturasse os horários de obtenção e definição das chamadas e notificá-los sobre o ShaderBuilder - a classe base de todos os shaders.

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

Delegar Implementação
Delegado variável:

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

Implementação dos demais delegados no GitHub .

Exemplo de sombreador:

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

E aqui está a fonte GLSL resultante (o resultado de FragmentShader (useAlphaTest = true) .getSource ()). O conteúdo e a estrutura do código são preservados:

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

É conveniente reutilizar o código de sombreador definindo parâmetros diferentes ao montar a fonte, mas isso não resolve completamente o problema de reutilização. No caso em que você precisa escrever o mesmo código em shaders diferentes, você pode colocar essas instruções em um ShaderBuilderComponent separado e adicioná-las conforme necessário aos ShaderBuilders principais:

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

Viva, a funcionalidade recebida permite que você escreva shaders no Kotlin, reutilize o código, verifique a sintaxe!

Agora, lembremos do Swizzling no GLSL e vejamos sua implementação no 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) ... } 

Em nosso projeto, a compilação do shader pode ocorrer no ciclo do jogo sob demanda, e tais alocações de objetos geram chamadas importantes para o GC, atrasos aparecem. Portanto, decidimos transferir a montagem das fontes de sombreador para o estágio de compilação usando o processador de anotação.

Marcamos a classe com a anotação ShaderProgram:

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

E o processador de anotações coleta todos os tipos de shaders, dependendo dos parâmetros dos construtores de classes de vértice e fragmento para nós:

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

Agora você pode obter o texto do sombreador da classe gerada:

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

Como o resultado da função get - ShaderProgramSources é o valor de enum, é conveniente usá-lo como chaves no registro do programa (ShaderProgramSources) -> CompiledShaderProgram.

O GitHub possui o código fonte do projeto, incluindo o processador de anotação e exemplos simples de shaders e componentes.

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


All Articles