SpriteKit Advanced - Comment construire un jeu 2,5D (Partie III)

Intro

Cet article vise à améliorer les graphismes de Raft Challenge en appliquant les shaders de GPU au paysage immobile. Il explique les algorithmes et les pièges potentiels lors de l'utilisation de GLSL dans SpriteKit.

Le lecteur doit avoir une expérience de base de l'écriture de shaders de fragments. Nous en avons discuté dans les parties 1 et 2 de cette série.

Le problème

Après que le jeu soit entré en phase bêta, nous avons reçu les commentaires de différentes personnes. Nous avons souvent entendu dire que les graphismes étaient bons, mais aussi statiques, ce qui à long terme a conduit à l'ennui.

Ma réaction instantanée a été la suivante: «Ils ont dit que c’était statique? Nous allons donc ajouter un peu de vent pour que tout bouge! »Après cela, nous avons réfléchi davantage au problème.

Des objets aussi gros que les arbres ne peuvent pas animer image par image, car cela entraînerait des problèmes de mémoire. Nous envisagions d'ajouter de petits objets animés tels que des animaux. Mais cela compliquerait encore plus le graphe de la scène. Et cela aurait un impact inconnu sur les performances.

La solution que j'ai proposée consistait à animer toute la forêt à l'aide des fragment shaders. Je voulais créer l'effet du vent.

L’idée était d’appliquer une distorsion horizontale à la texture du sprite avec une force proportionnelle à la distance depuis la base du tronc. Cette force a également évolué dans le temps et influencée par la «profondeur» de la scène.

Autres avantages de cette solution:

  • intégration facile
    C’est aussi simple que de remplir les propriétés d’un objet existant
  • performance
  • grande flexibilité

Voici la source (GLSL):

vide principal (vide)
{
    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
    facteur de flottement = sin (u_time * 2 + (attrDepth * 1.3)); // 4
    vec2 deltaUV = vec2 (facteur maxDivergence *, 0); // 5
    
    gl_FragColor = texture2D (u_texture, v_tex_coord + deltaUV); // 6
}
  1. Ce flotteur maintient la position verticale des bases de tous les troncs
     - Cette valeur est spécifique à notre texture
  2. Nous calculons la distance entre le point d'échantillonnage actuel et la valeur ci-dessus
     - Cette valeur est inférieure à 1,0 et peut être négative
  3. Nous calculons la divergence maximale
     - Le nombre magique à la fin a été modifié par essais et erreurs
  4. Nous calculons la force changeante et la direction du vent
     - La fonction sin est une bonne base car elle renvoie des valeurs prévisibles (-1 à 1)
     - C’est aussi une fonction continue
     - Ce dernier signifie que nous pouvons mettre n'importe quel déchet comme argument et que cela fonctionnera toujours
     - Dans ce cas, "la poubelle" est l'heure actuelle plus la "profondeur" de l'image-objet actuelle.
     - Des nombres magiques sont ajoutés pour façonner l'animation
  5. Le vecteur delta est créé
     - La divergence maximale multipliée par le facteur passe à la position X tandis que Y reste à 0.
  6. Cette ligne prend la couleur d'un point spécifique de la texture et la sort à l'écran
     - En ajoutant delta à notre position actuelle avec vtexcoord, nous modifions le point à partir duquel l'échantillonneur extrait la valeur de couleur

Résultat:

Notez que les reflets sur l'eau sont également en mouvement. En effet, les arbres et les reflets font partie du même sprite et de la même texture. Pas de sorcellerie ici.

Améliorer le brouillard

Y a-t-il autre chose que nous puissions faire? Eh bien, si nous ne pouvons rien inventer de nouveau, nous pouvons toujours améliorer quelque chose qui existe. Notre designer a dit une fois que les arbres plus loin devraient avoir une couleur unie pour mieux se fondre dans le brouillard.

L'image ci-dessus est presque explicite. Un peu plus tôt, j'ai parlé de la «profondeur». Chaque couche de la forêt a un attribut attrDepth. Il représente la distance entre les montagnes (0.0) et le spectateur (6.0).

Laissons ce brouillard tordre

__constant vec3 colorLightMountains = vec3 (0,847, 0,91, 0,8);
__constant vec3 colorDarkMountains = vec3 (0,729, 0,808, 0,643);
vide principal (vide)
{
    // obtenir de la couleur
    vec4 color = texture2D (u_texture, v_tex_coord);
    float alpha = color.a; // 1
    //brouillard
    vec3 outputColor = vec3 (color.rgb);
    if (attrDepth <1.0) {// 2
        outputColor = colorLightMountains;
        alpha = min (attrDepth, alpha);
    } 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) * alpha; // 5
}

Le code ci-dessus est assez simple, je ne me concentrerai donc que sur les choses les plus importantes:

  1. Extrait l'alpha de la texture.
  2. L'étape lointaine
    Lorsque la forêt est la plus éloignée possible, elle a la couleur Light Mountains et 0 alpha
     En se rapprochant, il émerge en augmentant l’alpha jusqu’à la profondeur == 1.0
  3. La moyenne distance
    La couleur se déplace vers les montagnes sombres à mesure que l’image-objet se rapproche du spectateur.
  4. La courte distance
    La couleur est un mélange entre les montagnes sombres et la couleur de texture native
    Naturellement, plus il est proche, plus il a l'air normal
  5. Passez la couleur finale à la sortie en utilisant l'alpha extrait au début

Encore une fois, le résultat:

Combinant les deux effets

La meilleure chose que j'aime chez les shaders est leur flexibilité. Il n’est pas seulement possible de fusionner les deux effets sans rien sacrifier. Il est même recommandé de le faire.

La fusion des shaders diminue les appels de dessin et augmente la cadence.

__constant vec3 colorLightMountains = vec3 (0,847, 0,91, 0,8);
__constant vec3 colorDarkMountains = vec3 (0,729, 0,808, 0,643);
vide principal (vide)
{
    //vent
    float horizonAbsoluteOffset = 0,64;
    float distanceFromTrunksBase = abs (v_tex_coord [1] - horizonAbsoluteOffset);
    float maxDivergence = mix (0.0,1.0, distanceFromTrunksBase) * 0,038;
    facteur de flottement = sin (u_time * 2 + (attrDepth * 1.3));
    vec2 deltaUV = vec2 (facteur maxDivergence *, 0);
    
    // obtenir de la couleur
    vec4 color = texture2D (u_texture, v_tex_coord + deltaUV);
    float alpha = color.a;
    //brouillard
    vec3 outputColor = vec3 (color.rgb);
    if (attrDepth <1.0) {
        outputColor = colorLightMountains;
        alpha = min (attrDepth, alpha);
    } 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);
    }
    
    //sortie
    gl_FragColor = vec4 (outputColor, 1.0) * alpha;
}

Le résultat final:

Les pièges

Il n’ya pas de rose sans épine.

  • L'utilisation de shaders sur plusieurs grands sprites dotés d'un canal alpha peut entraîner une baisse visible de la fréquence d'images.
  • Même GPU peut donner 60 images par seconde sur l'iPhone, mais seulement 20 images par seconde sur l'iPad avec plus de pixels
    Testez votre code fréquemment sur différents appareils, en particulier les iPad avec écran Retina
  • Il n'y a pas de moyen fiable d'estimer les performances de l'appareil à partir du code
    Exécuter votre jeu sur plusieurs périphériques physiques et liste blanche ceux qui sont capables d'exécuter des shaders avec des performances décentes
    Pour distinguer les périphériques, vous pouvez utiliser UIDevice-Hardware.m
  • Votre texture partiellement transparente perd sa couleur et devient grise? Alpha alpha prémultiplié!
  • Prenez garde à l’utilisation de SKTextureAtlases si vous modifiez les coordonnées comme dans l’exemple du vent.
    Au cours de la génération d'atlas, XCode peut faire pivoter et déplacer certaines textures.
    Il est impossible de détecter une telle anomalie à partir du code, ou du moins je ne sais pas comment
  • Pour certains sprites, vous pouvez recevoir une texture avec des coordonnées X et Y échangées!
  • Vous pouvez accidentellement vous déformer vers une sous-texture complètement différente!

Résumé

Nous avons appris à utiliser les shaders de fragments pour créer du vent et du brouillard. Lorsque vous écrivez votre propre code GLSL, vous produirez sûrement de nombreux artefacts d’affichage. Certains d'entre eux sont ennuyeux, et d'autres sont hilarants. Gardez à l'esprit que certains d'entre eux peuvent potentiellement devenir une fonctionnalité!

À propos de l'auteur: Kamil Ziętek est un développeur iOS sur www.allinmobile.co.