SpriteKit Advanced - Cómo construir un juego 2,5D (Parte III)

Introducción

Este artículo trata sobre cómo mejorar las imágenes de Raft Challenge mediante la aplicación de sombreadores de GPU al paisaje inmóvil. Explica los algoritmos y las posibles dificultades al usar GLSL en SpriteKit.

El lector debe tener experiencia básica en escribir sombreadores de fragmentos. Discutimos esto en la parte 1 y la parte 2 de esta serie.

El problema

Después de que el juego entró en la etapa beta, recibimos comentarios de varias personas. Con frecuencia escuchamos que los gráficos eran buenos, pero también estáticos, lo que a la larga llevó al aburrimiento.

Mi reacción instantánea fue como: "¿Dijeron que es estático? ¡Entonces agregaremos algo de viento para mover todo! ”Después de eso, pensamos más sobre el problema.

Los objetos tan grandes como los árboles no pueden animarse cuadro por cuadro, ya que esto provocaría problemas de memoria. Estábamos considerando agregar pequeños objetos animados como animales. Pero complicaría aún más el gráfico de la escena. Y tendría un impacto en el rendimiento desconocido.

La solución que se me ocurrió fue animar todo el bosque utilizando los sombreadores de fragmentos. Quería crear el efecto del viento.

La idea era aplicar una distorsión horizontal a la textura del sprite con una fuerza proporcional a la distancia desde la base de los troncos. Esa fuerza también ha ido cambiando en el tiempo e influenciada por la "profundidad" de la escena.

Otros pros de esta solución:

  • integración fácil
    Es tan simple como llenar las propiedades de un objeto existente
  • actuación
  • gran flexibilidad

Aquí está la fuente (GLSL):

vacío principal (vacío)
{
    float horizonAbsoluteOffset = 0.64; // 1
    float distanceFromTrunksBase = abs (v_tex_coord [1] - horizonAbsoluteOffset); // 2
    float maxDivergence = mix (0.0,1.0, distanceFromTrunksBase) * 0.038; // 3
    factor flotante = sin (u_time * 2 + (attrDepth * 1.3)); // 4
    vec2 deltaUV = vec2 (maxDivergence * factor, 0); // 5
    
    gl_FragColor = texture2D (u_texture, v_tex_coord + deltaUV); // 6
}
  1. Este flotador mantiene la posición vertical de todas las bases de los troncos
     - Este valor es específico de nuestra textura.
  2. Calculamos la distancia entre el punto de muestreo actual y el valor anterior
     - Este valor es menor que 1.0 y puede ser negativo
  3. Calculamos divergencia máxima
     - El número mágico al final se modificó mediante prueba y error
  4. Calculamos la fuerza cambiante y la dirección del viento.
     - La función sin es una buena base ya que devuelve valores predecibles (-1 a 1)
     - También es una función continua
     - Esto último significa que podemos poner cualquier basura como argumento y seguirá funcionando
     - En este caso "la basura" es el tiempo actual más la "profundidad" del sprite actual
     - Se añaden números mágicos para dar forma a la animación.
  5. Se crea el vector delta
     - La divergencia máxima multiplicada por el factor entra en la posición X mientras que Y se deja con 0.
  6. Esta línea toma el color de un punto específico en la textura y lo envía a la pantalla
     - Al agregar delta a nuestra posición actual con vtexcoord, modificamos el punto desde el cual la muestra extrae el valor de color

Resultado:

Tenga en cuenta que los reflejos en el agua también se mueven. Esto se debe a que los árboles y los reflejos son parte del mismo sprite y textura. No hay brujería aquí.

Mejorando la niebla

¿Hay algo más que podamos hacer? Bueno, si no podemos inventar nada nuevo, siempre podemos mejorar algo que existe. Nuestro diseñador dijo una vez que los árboles más alejados deberían tener un color sólido para fusionarse mejor con la niebla.

La imagen de arriba es casi autoexplicativa. Anteriormente, he mencionado sobre la "profundidad". Cada capa del bosque tiene un atributo attrDepth. Representa la distancia entre las montañas (0.0) y el espectador (6.0).

¡Vamos a ajustar esta niebla!

__constant vec3 colorLightMountains = vec3 (0.847, 0.91, 0.8);
__ vec3 colorDarkMountains constantes = vec3 (0.729, 0.808, 0.643);
vacío principal (vacío)
{
    // obtener color
    vec4 color = texture2D (u_texture, v_tex_coord);
    flotador alfa = color.a; // 1
    //niebla
    vec3 outputColor = vec3 (color.rgb);
    if (attrDepth <1.0) {// 2
        outputColor = colorLightMountains;
        alfa = min (attrDepth, alfa);
    } else if (attrDepth <2.0) {// 3
        outputColor = mix (colorLightMountains, colorDarkMountains, attrDepth - 1.0);
    } else if (attrDepth <= 3.0) {// 4
        outputColor = mix (colorDarkMountains, color.rgb, attrDepth - 2.0);
    }
    
    gl_FragColor = vec4 (outputColor, 1.0) * alfa; // 5
}

El código anterior es bastante sencillo, por lo que me centraré solo en las cosas más importantes:

  1. Extraer alfa de la textura.
  2. La etapa lejana
    Cuando el bosque está lo más lejos posible, tiene el color de las Montañas de Luz y 0 alfa
     A medida que se acerca, emerge aumentando alfa hasta profundidad == 1.0
  3. La distancia media
    El color cambia hacia las Montañas Oscuras a medida que los sprites se acercan al espectador.
  4. La corta distancia
    El color es una mezcla entre las Montañas Oscuras y el color de la textura nativa.
    Naturalmente, cuanto más cerca está, más normal se ve
  5. Pase el color final a la salida utilizando el alfa extraído al principio

De nuevo, el resultado:

Combinando ambos efectos

Lo mejor que me gusta de los sombreadores es su flexibilidad. No solo es posible fusionar ambos efectos sin sacrificar nada. Incluso se recomienda hacerlo.

La combinación de sombreadores disminuye las llamadas de extracción y eso aumenta la velocidad de fotogramas.

__constant vec3 colorLightMountains = vec3 (0.847, 0.91, 0.8);
__ vec3 colorDarkMountains constantes = vec3 (0.729, 0.808, 0.643);
vacío principal (vacío)
{
    //viento
    float horizonAbsoluteOffset = 0.64;
    float distanceFromTrunksBase = abs (v_tex_coord [1] - horizonAbsoluteOffset);
    float maxDivergence = mix (0.0,1.0, distanceFromTrunksBase) * 0.038;
    factor flotante = sin (u_time * 2 + (attrDepth * 1.3));
    vec2 deltaUV = vec2 (maxDivergence * factor, 0);
    
    // obtener color
    vec4 color = texture2D (u_texture, v_tex_coord + deltaUV);
    flotador alfa = color.a;
    //niebla
    vec3 outputColor = vec3 (color.rgb);
    if (attrDepth <1.0) {
        outputColor = colorLightMountains;
        alfa = min (attrDepth, alfa);
    } else if (attrDepth <2.0) {
        outputColor = mix (colorLightMountains, colorDarkMountains, attrDepth - 1.0);
    } else if (attrDepth <= 3.0) {
        outputColor = mix (colorDarkMountains, color.rgb, attrDepth - 2.0);
    }
    
    //salida
    gl_FragColor = vec4 (outputColor, 1.0) * alfa;
}

El resultado final:

Trampas

No hay rosa sin espina.

  • El uso de sombreadores en múltiples sprites grandes con canal alfa puede causar una caída visible de la velocidad de fotogramas.
  • La misma GPU puede dar 60 fps en el iPhone pero solo 20 fps en iPad con más píxeles
    Pruebe su código con frecuencia en diferentes dispositivos, especialmente los iPads con pantallas retina
  • No hay una forma confiable de estimar el rendimiento del dispositivo a partir del código
    Ejecute su juego en múltiples dispositivos físicos y haga una lista blanca de aquellos que son capaces de ejecutar sombreadores con un rendimiento decente
    Para distinguir dispositivos, puede usar UIDevice-Hardware.m
  • ¿Su textura parcialmente transparente pierde color y se vuelve gris? Google premultiplied alpha!
  • Tenga cuidado con el uso de SKTextureAtlases si está alterando las coordenadas como en el ejemplo del viento
    Durante la generación del atlas, XCode puede rotar y mover algunas texturas.
    Es imposible detectar dicha anomalía en el código, o al menos no sé cómo
  • ¡Para algunos sprites, puede recibir una textura con coordenadas X e Y intercambiadas!
  • ¡Puede deformarse accidentalmente a una sub textura completamente diferente!

Resumen

Hemos aprendido a usar sombreadores de fragmentos para crear viento y niebla. Al escribir su propio código GLSL, seguramente producirá muchos artefactos de visualización. Algunos de ellos son molestos, y otros son divertidos. ¡Tenga en cuenta que algunos de ellos pueden convertirse en una característica!

Sobre el autor: Kamil Ziętek es desarrollador de iOS en www.allinmobile.co