🎨

GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (1)

2024/06/01に公開

はしがき

iOS 17でSwiftUIのViewにMetalシェーダーが適用できる Modifier が追加されました。リッチなUIが手軽に描けるようになって、うれしいアップデートです。
とはいってもMetalシェーダーのハードルが高そうに思えたり(さわってみると意外に簡単なのですが)、古いバージョンのiOSにも対応したかったりと、すぐには活用できないケースもあるでしょう。

SwiftUIでシェーダーを使う方法は他にもあります。そのひとつが SpriteKitSpriteView です。
SpriteView を介して SpriteKitSKShader を利用することで、SwiftUIにシェーダーを表示することができます。 GLSLで書ける ので、シェーダーの解説サイトにあるGLSLのコードや、Shadertoyなどのサービスで共有されているコードを移植して動かしてみることも比較的やりやすいです。

でも、「じゃあちょっと試してみようかな」とその気になってSKShaderについてネットで調べると、まとまった日本語の情報がなかなか見つからない。『ことはじめ』や『ハウツー』や『備忘録』は何処ーー。
……だったら自分で書くか! というのが本記事の執筆動機です。

はたして需要があるのかわかりませんが、どこかでだれかのお役に立つことがあれば幸いです。

GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit シリーズの記事一覧

必要な前提知識

以下に該当するようなプログラマーを想定読者としています。

  • Swiftの基本文法をおおむね理解している
  • SwiftUIの基本的な書きかたを知っている
  • フラグメントシェーダー(GLSL, HLSL, etc.)のコードを多少でも書いて動かしてみた経験がある

SwiftUIを使っていてシェーダーにも興味があるけれど、SpriteKitやSKShaderにはなじみがない、ぐらいのレベル感です。
シェーダーの入門知識の説明は省きますので、たとえばGLSLの変数の種類や型名(uniform, float, vec4, ...)、関数名(length, mix, ...)、スウィズル演算子や座標の正規化といった用語が前置きなしに出てきます。

とりあげる内容

SwiftUIにSpriteKitを表示させる方法から始めて、SpriteKitやSKShaderの基本的な書きかた、SKShaderのGLSLの独自仕様についてなどを、ステップ・バイ・ステップで紹介します。のちのち3D(SK3DNode × SceneKit × SCNShadable)についても少し触れられたらと思っています。

読了した時点での到達目標を『GLSLシェーダーをSwiftUI上で動かして遊べるようになろう』あたりに据えて書きすすめますので、ややこしい数学の解説であるとか、実務ですぐに役立つシェーダーテクニック的な話題はほぼ出てきません。

環境

以下の環境で動作確認を行っています。

  • Xcode 15.2 (15C500b)
  • iOS Deployment Target 15.0
  • Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 clang-1500.1.0.2.5)
  • Swift Playgrounds 4.5 (*)
  • macOS Venture 13.6.6
  • MacBook Air M2 2022

* XcodeのPlayground(PlaygroundSupport)では動作しません

貼付しているスクリーンショットはシミュレータの iPhone 13(iOS 15.5)で撮影したものをベースにしています。

What is SpriteKit

SpriteKit は、2Dを扱うフレームワークです。標準で用意されているので import SpriteKit をコードに追加すれば使えます。
2Dゲーム開発を主な用途としており、図形や文字やテクスチャの表示、アニメーション、物理演算といった機能が提供されています。ゲームに限らず、UIアニメーションやパーティクルの演出を画面に加えるようなことにも使えます。
SKScene というシーンオブジェクトに SKNode などのノードオブジェクトを配置して画面を作っていきます。

SKShader は、SpriteKitで使えるフラグメントシェーダーです。前述のノードオブジェクトに適用して、描画をカスタマイズすることができます。
シェーディング言語はGLSL(OpenGL ES 2.0)で、OpenGL Extensionには対応していません。
レンダラーはOpenGLではなく Metal です。GLSLのコードが内部でMetalシェーディング言語(MSL)に変換されて実行されます。この影響とおぼしき OpenGLとの文法の差異や挙動の違いがいくつかあり 、他所のGLSLのコードを移植するときに修正が求められます。本シリーズ記事では、そういったポイントにも焦点をあてて説明に加えていきたいと思います。
SKShader独自の変数や関数 もありますので、それらも順次とりあげます。

ファーストステップ SwiftUI × SpriteKit × SKShader

1. SwiftUI に SpriteKit の SKScene を表示する

さっそくSwiftUIにSpriteKitを表示させましょう。
SwiftUIのデフォルトのテンプレートでXcode Projectを用意した状態から始めます。
必要になる最小限のコードは次の例のようになります。各コードの意味をコメント行で記します。

ContentView.swift
import SwiftUI

// SpriteKitをインポートします --- (1)
import SpriteKit

struct ContentView: View {

    // SpriteKitのSKSceneを用意します --- (3)
    var currentScene: SKScene {

        // SKSceneオブジェクトを作成します
        let scene = MySKScene()

        // シーンがViewのframeサイズいっぱいに表示されるようにリサイズします
        scene.scaleMode = .resizeFill

        return scene
    }

    var body: some View {
        ZStack {
            // SwiftUIのViewの背景色を薄いグレーにしておきます (optional)
            Color(white: 0.8)

            // SpriteKitのシーンを300x250のサイズで表示します --- (4)
            SpriteView(scene: self.currentScene)
                .frame(width: 300, height: 250)
        }
    }
}

// SKSceneを継承したクラスを作成します --- (2)
class MySKScene: SKScene {

    // シーンがViewに表示されたときに実行する処理をdidMoveメソッド内に書きます
    override func didMove(to view: SKView) {

        // 色を用意します (optional)
        let blue = UIColor(red: 0.29, green: 0.59, blue: 0.78, alpha: 1.0)

        // シーンの背景色を青にします
        self.backgroundColor = blue
    }
}
  1. SpriteKitを import する
  2. SpriteKitの SKScene を継承したクラスを作成する
  3. SwiftUIで SKScene オブジェクトを作成する
  4. SwiftUIの SpriteView でシーンオブジェクトを描画する

この4ステップでSwiftUIにSpriteKitのコンテンツが表示できます。
シミュレータで実行すると、以下のスクリーンショットのようになります。

SKSceneの青い四角の表示

SwiftUIの灰色の背景の中央に、SpriteKitの SKScene の背景色である青い四角が表示されました。

さきに少し説明したとおり、SKScene はSpriteKitのコンテンツを配置するためのベースとなるオブジェクトです。このシーン上に SKNode オブジェクトを配置し、そのノードにシェーダーを適用していく流れになります。

2. SKScene に SKSpriteNode を追加する

次に、SpriteKitのノードをシーンへ追加します。
SKScenedidMove メソッドに、ノードを作成・表示するコードを加えます。今回は SKSpriteNode という種類のノードを使って説明します。

ContentView.swift
  // 〜略〜

  class MySKScene: SKScene {
      override func didMove(to view: SKView) {
          let blue = UIColor(red: 0.29, green: 0.59, blue: 0.78, alpha: 1.0)

+         // ノード用に色を用意します (optional)
+         let green = UIColor(red: 0.64, green: 0.81, blue: 0.46, alpha: 1.0)

          self.backgroundColor = blue

+         // シーンの中央を基準にノードが配置されるようにします
+         // デフォルトではシーンの左下の原点が基準になっています
+         self.anchorPoint = CGPoint(x: 0.5, y: 0.5)

+         // シェーダーの適用先となるノードを作成します --- (1)
+         let node = SKSpriteNode()

+         // ノードを緑色に塗ります --- (2)
+         // デフォルトでは透明です
+         node.color = green

+         // ノードのサイズを250x200にします --- (3)
+         node.size = CGSize(width: 250, height: 200)

+         // 作成したノードをシーンへ追加します --- (4)
+         self.addChild(node)
      }
  }
  1. SKSpriteNode オブジェクトを作成する
  2. ノードオブジェクトの color プロパティで色を指定する
  3. ノードオブジェクトの size プロパティでサイズを指定する
  4. addChild メソッドでノードオブジェクトをシーンへ追加する

この4ステップでシーンにノードが表示されます。

SKSpriteNodeの緑の四角の表示

SKScene の青い背景の中央に、 SKSpriteNode の緑色の四角形が現れました。この緑色の四角形が、フラグメントシェーダーを適用する板ポリゴンの役割を果たします。

(1) 〜 (3) をひとまとめにする書きかたもあります。

// 色とサイズを指定してノードを作成します --- (1) 〜 (3)
let node = SKSpriteNode(
    color: UIColor(red: 0.64, green: 0.81, blue: 0.46, alpha: 1.0),
    size: CGSize(width: 250, height: 200)
)

// 作成したノードをシーンへ追加します --- (4)
self.addChild(node)

SKSpriteNode 以外のシェーダーが使えるノードの種類については、別の回で紹介します。

3. SKSpriteNode に SKShader を適用する

最後に、フラグメントシェーダーを作成してノードに適用しましょう。

ContentView.swift
  // 〜略〜

  class MySKScene: SKScene {
      override func didMove(to view: SKView) {
          let blue = UIColor(red: 0.29, green: 0.59, blue: 0.78, alpha: 1.0)
          let green = UIColor(red: 0.64, green: 0.81, blue: 0.46, alpha: 1.0)

          self.backgroundColor = blue
          self.anchorPoint = CGPoint(x: 0.5, y: 0.5)

          let node = SKSpriteNode()
          node.color = green
          node.size = CGSize(width: 250, height: 200)

+         // シェーダーを作成します --- (1)
+         let shader = SKShader(
+             // シェーダーのソースコードをテキストで書きます
+             source: """
+                 void main() {
+                     // 色を用意します
+                     vec4 yellow = vec4(0.99, 0.83, 0.46, 1.0);
+                     // 黄色を出力します
+                     gl_FragColor = yellow;
+                 }
+             """
+         )

+         // 作成したシェーダーをノードに適用します --- (2)
+         node.shader = shader

          self.addChild(node)
      }
  }
  1. SKShader オブジェクトを作成する
  2. ノードオブジェクトの shader プロパティに、シェーダーオブジェクトを格納する

この2ステップでシェーダーを表示する準備は完了です。

SKShaderの黄色の表示

緑色のノード(=板ポリゴン)にフラグメントシェーダーが適用されて、黄色に変わりました。
SKShader ではOpenGL ES 2.0のGLSLと同様に gl_FragColor が組み込み変数として用意されています。この変数に格納された色が最終的な出力になります。

(1) の箇所では、シェーダーのソースコードを別のファイルに用意して読みこませる方法も使えます。

MyShader.fsh
// .fsh という拡張子のファイルを作成してXcodeプロジェクトに追加し、
// そのファイルにシェーダーのソースコードを書きます
void main() {
    vec4 yellow = vec4(0.99, 0.83, 0.46, 1.0);
    gl_FragColor = yellow;
}
// fshファイルを指定してシェーダーを作成します --- (1)
// ファイル名の拡張子は省略できます
let shader = SKShader(fileNamed: "MyShader")

// 作成したシェーダーをノードに適用します --- (2)
node.shader = shader

今回のサンプルコード全体
ContentView.swift
import SwiftUI
import SpriteKit

struct ContentView: View {
    var currentScene: SKScene {
        let scene = MySKScene()
        scene.scaleMode = .resizeFill
        return scene
    }

    var body: some View {
        ZStack {
            Color(white: 0.8)

            SpriteView(scene: self.currentScene)
                .frame(width: 300, height: 250)
        }
    }
}

class MySKScene: SKScene {
    override func didMove(to view: SKView) {
        let blue = UIColor(red: 0.29, green: 0.59, blue: 0.78, alpha: 1.0)
        let green = UIColor(red: 0.64, green: 0.81, blue: 0.46, alpha: 1.0)

        self.backgroundColor = blue
        self.anchorPoint = CGPoint(x: 0.5, y: 0.5)

        let node = SKSpriteNode()
        node.color = green
        node.size = CGSize(width: 250, height: 200)

        let shader = SKShader(
            source: """
                void main() {
                    vec4 yellow = vec4(0.99, 0.83, 0.46, 1.0);
                    gl_FragColor = yellow;
                }
            """
        )

        node.shader = shader
        self.addChild(node)
    }
}

まとめ

SwiftUIでのSpriteKitの表示からSKSpriteNodeの追加、SKShaderの適用までをざっとご紹介しました。

コメントを除くと50行足らずでとりあえずシェーダーが表示できてしまいました。オフラインで用意できるシェーダーの実行環境としては、だいぶお手軽だったのではないでしょうか。
シェーダーの実行結果はSwiftUIのPreview上でも確認できるので、ソースコードをいじってあれこれ試行錯誤するのにも便利。なんならiPadのSwift Playgrounds [1] でも動かせるから、モバイル環境でもシェーダー三昧できちゃいます!

次回は SKShaderの座標 について書きたいと思います。よろしければ引き続きご覧ください。


次記事 → GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (2) 〜座標〜

GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit シリーズの記事一覧

参考リンク

脚注
  1. Xcode Playgroundでも実行できれば理想的だったのですが、執筆時点の筆者の手元のバージョンではSpriteViewがサポートされていないようでした。Swift PlaygroundsのほうのXcodeプレイグラウンドでは動きます。 ↩︎

Discussion