SpriteKit Advanced - Erstellen eines 2,5D-Spiels (Teil II)

Intro

Dieser Artikel zeigt, wie grundlegende Shader im SpriteKit geschrieben werden. Es besteht aus zwei Teilen: Erst spielen, dann lernen wir.

Es enthält auch Informationen zur Verwendung der Klassen SKAttribute und SKAttributeValue, die in iOS SDK 10.0 hinzugefügt wurden.

Wenn Sie es noch nicht gelesen haben, finden Sie hier Teil 1 dieser Artikelserie.

Bereiten Sie das Projekt vor

Lass uns schnell und schmutzig werden.

  • Öffnen Sie XCode 8 und erstellen Sie ein neues Projekt aus der Vorlage: iOS> Spiel.
  • Öffne die GameScene.sks und entferne das Etikett in der Mitte des Bildschirms.
  • Laden Sie dies herunter und fügen Sie es in Assets.xcassets ein
  • Nennen Sie es "Bäume"
  • Öffne die GameScene.m
  • entfernen Sie alle Instanzvariablen
  • entfernen Sie alle Methoden

Der Fragment-Shader

Jetzt erstellen wir einen leeren Fragment-Shader in XCode:

  • Wählen Sie im Projektnavigator Unterstützende Dateien
  • Wählen Sie: Datei> Neu> Datei…
  • Wählen Sie: Andere> Leer
  • Nennen Sie es "myShader.fsh" und klicken Sie auf "Erstellen".
  • Setzen Sie dieses in:
// derzeit ein langweiliger Pass-Thru-Shader void main (void) {vec4 color = texture2D (utexture, vtexcoord); // hier entsteht etwas Würdiges glFragColor = color;}

Über Fragment Shader macht sich nichts bemerkbar. Schnelle Erklärung:

  • void main ()
     Diese Funktion wird für jedes Pixel des Sprites aufgerufen und gibt die Farbe für dieses Pixel aus
    Ruft Eingabedaten von umgebenden Globalen ab und muss die Variable gl_FragColor festlegen
  • vec2, vec3 und vec4 sind die ähnlichen Typen wie C: float array [2], float array [3] und float array [4]
  • u_texture ist eine Textur-ID
    Lass es in Ruhe :-)
  • v_tex_coord ist ein vec2, das unsere aktuelle Position in der Textur enthält
  • texture2D (tex, p) ist eine Funktion, die die Farbe des Texturtex in Punkt p als vec4 zurückgibt
    welches rgba enthält
  • gl_FragColor ist eine Ausgabefarbe
     Wir müssen ihm ein vec4 zuweisen

Code wird geladen

Was bleibt, ist der Ladecode.

  • Öffne die GameScene.m
  • Methode -didMoveToView hinzufügen:
- (void) didMoveToView: (SKView *) view {
// 1. lade die Quelle des Shaders von myShaderFile.fsh
NSString * file = [[NSBundle mainBundle] pathForResource: @ "myShader" ofType: @ "fsh"];
NSString * sourceString = [NSString stringWithContentsOfFile: Dateicodierung: NSUTF8StringEncoding error: nil];

// 2. Erstelle den Shader
SKShader * shader = [SKShader shaderWithSource: sourceString];

// 3. Weisen Sie den Shader einem neu erstellten Sprite-Knoten zu
SKSpriteNode * spriteNode = [SKSpriteNode spriteNodeWithImageNamed: @ "Trees"];
spriteNode.shader = shader;

// 4. füge endlich das Sprite zur Szene hinzu
[self addChild: spriteNode];
}

Stellen Sie sicher, dass myShader.fsh in ProjectFile> Target> Build Phases> Copy Bundle Resources!

Sie können das Projekt jetzt auf dem iOS-Gerät ausführen. Es dürfen keine Fehler in der XCode-Konsole vorhanden sein, und Sie sollten einen ähnlichen Bildschirm wie den folgenden sehen:

Lass uns ein bisschen spielen!

Jetzt ist der lustige Teil. Wir ersetzen die Hauptfunktion des Shaders.

Farbe mit Rot mit Alpha-Erhaltung

void main (nichtig)
{
    vec4 color = texture2D (u_texture, v_tex_coord);
    float alpha = color.a;
    gl_FragColor = vec4 (1,0,0, 1,0) * alpha; // google "vorvervielfachtes Alpha"
}

2x verkleinern

void main (nichtig)
{
    vec4 color = texture2D (u_texture, v_tex_coord * 2.0);
    gl_FragColor = color;
}

Tauschen Sie die Farben nach 1 Sekunde

void main (nichtig)
{
    vec4 color = texture2D (u_texture, v_tex_coord);
    float alpha = color.a;
    float phase = mod (u_time, 3);
    vec3 outputColor = color.rgb;
    if (Phase <1.0) {
        outputColor = color.bgr;
    } else if (Phase <2.0) {
        outputColor = color.brg;
    }
    gl_FragColor = vec4 (outputColor, 1.0) * alpha;
}

Mit der Zeit kolorieren

void main (nichtig)
{
    vec4 color = texture2D (u_texture, v_tex_coord);
    float alpha = color.a;
    float r = (sin (u_time + 3,14 * 0,00) +1,0) * 0,5;
    float g = (sin (u_time + 3,14 · 0,33) + 1,0) · 0,5;
    float b = (sin (u_time + 3,14 * 0,66) +1,0) * 0,5;
    gl_FragColor = vec4 (r, g, b, 1,0) * alpha;
}

Wellen

void main (nichtig)
{
    float deltaX = sin (v_tex_coord.y * 3,14 * 10 + u_time * 4) * 0,01;
    vec2 coord = v_tex_coord;
    coord.x = coord.x + deltaX;
    vec4 color = texture2D (u_texture, coord);
    gl_FragColor = color;
}

Neue Attribute

Auf der WWDC 2016 stellte Apple ein wichtiges Update für SpriteKit vor - die Klassen SKAttribute und SKAttributeValue.

Wenn wir vor diesem SDK-Update benutzerdefinierte Parameter an das Shader-Programm übergeben wollten, mussten wir die Daten über einen einheitlichen Wert übergeben.

Dies hatte zwei gravierende Nachteile:

  • Jede einheitliche Änderung verursachte eine erneute Kompilierung des Shaders
  • Das Shader-Programm hat jedes Sprite genau gleich behandelt

Zum Beispiel: Wenn wir eine Gruppe von Sprites rot und eine von ihnen blau färben wollten, hatten wir zwei Möglichkeiten. Zuerst erstellen wir zwei separate SKShader-Instanzen und ändern unsere benutzerdefinierte myColor-Uniform.

Zweitens erstellen wir eine Shader-Instanz und ändern ihre Uniform, was zu einer Neukompilierung führt.

Beide Wege können nicht im selben Durchgang gezogen werden. Und der zweite erfordert komplexen Verwaltungscode.

In SDK 10.0 wurden die Klassen SKAttribute und SKAttributeValue eingeführt. Diese beiden ermöglichen (endlich!) Die Weitergabe von Daten an die Shader-Programme ohne Neukompilierung. Der Verwendungsalgorithmus ist einfach:

  • Der Shader-Teil:
  1. Erstellen Sie ein Shader-Programm
    SKShader
  2. Erstellen Sie ein Array mit SKAttributen
  3. Weisen Sie dem Shader-Programm ein Array von Attributen zu
  • Der Sprite-Teil:
  1. Weisen Sie das Shader-Programm einem Sprite zu
  2. Weisen Sie ein Wörterbuch mit SKAttributeValues ​​zu

Beispiel mit Attributen

Im letzten Beispiel werden zwei weitere Sprites hinzugefügt. Jeder von ihnen hat das gleiche Shader-Programm und unterscheidet sich nur in Attributen. Ändern wir die Datei -didMoveToView: inGameScene.m:

- (void) didMoveToView: (SKView *) view {
    NSString * file = [[NSBundle mainBundle] pathForResource: @ "myShader" ofType: @ "fsh"];
    NSString * sourceString = [NSString stringWithContentsOfFile: Dateicodierung: NSUTF8StringEncoding error: nil];
    SKShader * shader = [SKShader shaderWithSource: sourceString];
    
    // 1. Fügen Sie dem Shader ein benutzerdefiniertes Attribut hinzu
    SKAttribute * attrProgress = [SKAttribute attributeWithName: @ "THE_MIGHTY_DARK_FACTOR" Typ: SKAttributeTypeFloat];
    shader.attributes = @ [attrProgress];

    // 2. Erstelle Tree Sprites
    NSArray * trees = @ [
                       [self createTreeWithShader: shader mightyFactor: 0.3f zPosition: 1],
                       [self createTreeWithShader: shader mightyFactor: 0.6f zPosition: 2],
                       [self createTreeWithShader: shader mightyFactor: 0.9f zPosition: 3],
                       ];
    für (SKSpriteNode * Baum in Bäumen) {
        [self addChild: tree];
    }
}

- (SKSpriteNode *) createTreeWithShader: (SKShader *) shader mightyFactor: (CGFloat) mightyFactor zPosition: (CGFloat) zPosition {
    SKSpriteNode * treeNode = [SKSpriteNode spriteNodeWithImageNamed: @ "Trees"];
    treeNode.shader = shader;
    // 3. Fülle das benutzerdefinierte Attribut des Sprites aus
    treeNode.attributeValues ​​= @ {@ "THE_MIGHTY_DARK_FACTOR": [SKAttributeValue valueWithFloat: mightyFactor]};
    treeNode.zPosition = zPosition;
return treeNode;
}

… Und das Shaderprogramm:

void main (nichtig)
{
    vec4 color = texture2D (u_texture, v_tex_coord * (2.5 * THE_MIGHTY_DARK_FACTOR));
    float alpha = color.a;
    vec3 baseColor = color.rgb * THE_MIGHTY_DARK_FACTOR;
    gl_FragColor = vec4 (baseColor, 1.0) * alpha;
}

... und sehen Sie das parametrierte Ergebnis!

Vorbehalte

  • Der Quellcode des Shaders wird normalerweise aus einer .fsh-Datei in einen einfachen NSString geladen
    Dieser Code muss zur Laufzeit auf dem Zielgerät kompiliert werden
    Keine Bauzeitkontrollen!
  • Ältere Geräte verwenden möglicherweise eine andere Version von OpenGL ES. Achten Sie daher auf Unterschiede in der GLSL-Syntax!
    Im Fall von Raft Challenge musste __constant (gültig in OpenGL ES 3.0) durch const für OpenGL ES 2.0 ersetzt werden.
  • Es ist eine gute Idee, einen Verweis auf ein SKShader-Objekt irgendwo aufzubewahren und ihn so oft wie nötig wiederzuverwenden, um sichtbare Bildratenverluste zu vermeiden
    Während die Zuweisung und Shader-Kompilierung weniger als 1/60 Sekunden dauert, kann dies eine enorme Belastung für die Render-Schleife darstellen
  • Bei der Verwendung von SpriteKit-Texturatlanten ist auf vtexcoord zu achten
    XCode kann einige Texturen drehen, die die X- und Y-Achse vertauschen
    Farbmodifikation ist sicher, Geometrie nicht

Zusammenfassung

Wir haben anhand von Beispielen gelernt, wie Fragment-Shader im Sprite-Kit verwendet werden. Wir haben den Sprites Parameter hinzugefügt, damit unser Shader-Programm jede Instanz auf andere Weise rendern kann, ohne dass es zu Leistungseinbußen kommt.

Das komplette Projekt steht zum Download bereit.

Sie können Teil 3 dieser Serie hier lesen.

Über den Autor: Kamil Ziętek ist iOS-Entwickler auf www.allinmobile.co