
Hola a todos!
Nuestra empresa está desarrollando juegos en línea y ahora estamos trabajando en una versión móvil de nuestro proyecto principal. En este artículo queremos compartir la experiencia de desarrollar sombreadores GLSL para un proyecto de Android con ejemplos y
fuentes .
Sobre el proyecto
Inicialmente, el juego estaba basado en un navegador basado en Flash, pero las noticias sobre el cese inminente de soporte para Flash nos obligaron a transferir el proyecto a HTML5. Se utilizó Kotlin como lenguaje de desarrollo, y seis meses después pudimos lanzar el proyecto en Android. Desafortunadamente, sin optimización en dispositivos móviles, el juego carecía de rendimiento.
Para aumentar el FPS, se decidió rediseñar el motor de gráficos. Solíamos usar varios sombreadores universales, pero ahora para cada efecto decidimos escribir un sombreador separado, afilado para una tarea específica, a fin de poder hacer su trabajo más eficiente.
Lo que nos faltaba
Los sombreadores se pueden almacenar en una cadena, pero este método elimina la verificación de sintaxis y la coincidencia de tipos, por lo que los sombreadores generalmente se almacenan en archivos Assets o Raw, ya que esto le permite habilitar la validación instalando el complemento para Android Studio. Pero este enfoque también tiene un inconveniente: la falta de reutilización: para realizar pequeñas ediciones, debe crear un nuevo archivo de sombreador.
Entonces eso:
- desarrollar sombreadores en Kotlin,
- tener una verificación de sintaxis en la etapa de compilación,
- poder reutilizar el código entre sombreadores,
Necesitaba escribir un "convertidor" Kotlin a GLSL.
Resultado deseado: el código del sombreador se describe como una clase de Kotlin, en la que los atributos, las variaciones y los uniformes son las propiedades de esta clase. Los parámetros del constructor primario de la clase se usan para ramas estáticas y le permiten reutilizar el resto del código del sombreador. El bloque init es el cuerpo del sombreador.
Solución
Para la implementación, se utilizaron
delegados de Kotlin. Permitieron que el tiempo de ejecución descubriera el nombre de la propiedad delegada, capturara los momentos de obtención y establecimiento de las llamadas y les notificara sobre ShaderBuilder, la clase base de todos los sombreadores.
class ShaderBuilder { val uniforms = HashSet<String>() val attributes = HashSet<String>() val varyings = HashSet<String>() val instructions = ArrayList<Instruction>() ... fun getSource(): String = ... }
Implementación delegadaDelegado 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)) } }
Implementación de los delegados restantes en
GitHub .
Ejemplo 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 } }
Y aquí está la fuente GLSL resultante (el resultado de FragmentShader (useAlphaTest = true) .getSource ()). El contenido y la estructura del código se conservan:
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 conveniente reutilizar el código del sombreador configurando diferentes parámetros al ensamblar la fuente, pero esto no resuelve completamente el problema de reutilización. En el caso de que necesite escribir el mismo código en diferentes sombreadores, puede colocar estas instrucciones en un componente ShaderBuilderComponent separado y agregarlas según sea necesario a los ShaderBuilders principales:
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, la funcionalidad recibida le permite escribir sombreadores en Kotlin, reutilizar código, verificar la sintaxis!
Ahora, recordemos
Swizzling en GLSL y veamos su implementación en 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) ... }
En nuestro proyecto, la compilación de sombreadores puede ocurrir en el ciclo del juego a pedido, y tales asignaciones de objetos generan grandes llamadas de GC, aparecen retrasos. Por lo tanto, decidimos transferir el ensamblaje de las fuentes de sombreado a la etapa de compilación utilizando el procesador de anotación.
Marcamos la clase con la anotación ShaderProgram:
@ShaderProgram(VertexShader::class, FragmentShader::class) class ShaderProgramName(alphaTest: Boolean)
Y el procesador de anotaciones recopila todo tipo de sombreadores dependiendo de los parámetros de los constructores de clases de vértices y fragmentos para nosotros:
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 } }
Ahora puede obtener el texto del sombreador de la clase generada:
val sources = ShaderProgramNameSources.get(replaceAlpha = true) println(sources.vertex) println(sources.fragment)
Dado que el resultado de la función get - ShaderProgramSources es el valor de enum, es conveniente usarlo como claves en el registro del programa (ShaderProgramSources) -> CompiledShaderProgram.
GitHub tiene el código fuente del proyecto, incluido el procesador de anotaciones y ejemplos simples de sombreadores y componentes.