GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (1)
はしがき
iOS 17でSwiftUIのViewにMetalシェーダーが適用できる Modifier が追加されました。リッチなUIが手軽に描けるようになって、うれしいアップデートです。
とはいってもMetalシェーダーのハードルが高そうに思えたり(さわってみると意外に簡単なのですが)、古いバージョンのiOSにも対応したかったりと、すぐには活用できないケースもあるでしょう。
SwiftUIでシェーダーを使う方法は他にもあります。そのひとつが SpriteKit(SpriteView) です。
SpriteView を介して SpriteKit の SKShader を利用することで、SwiftUIにシェーダーを表示することができます。 GLSLで書ける ので、シェーダーの解説サイトにあるGLSLのコードや、Shadertoyなどのサービスで共有されているコードを移植して動かしてみることも比較的やりやすいです。
でも、「じゃあちょっと試してみようかな」とその気になってSKShaderについてネットで調べると、まとまった日本語の情報がなかなか見つからない。『ことはじめ』や『ハウツー』や『備忘録』は何処ーー。
……だったら自分で書くか! というのが本記事の執筆動機です。
はたして需要があるのかわかりませんが、どこかでだれかのお役に立つことがあれば幸いです。
GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit シリーズの記事一覧
- GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (1)(本記事)
- GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (2) 〜座標〜
- GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (3) 〜変数〜
- GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (4) 〜経過時間とマウス座標〜
- GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (5) 〜テクスチャと色 前編〜
- GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (6) 〜テクスチャと色 後編〜
- GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (7) 〜ノード〜
- GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (8) 〜3D〜
- GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (9) 〜落穂拾い〜
必要な前提知識
以下に該当するようなプログラマーを想定読者としています。
- 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を用意した状態から始めます。
必要になる最小限のコードは次の例のようになります。各コードの意味をコメント行で記します。
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
}
}
- SpriteKitを
import
する - SpriteKitの
SKScene
を継承したクラスを作成する - SwiftUIで
SKScene
オブジェクトを作成する - SwiftUIの
SpriteView
でシーンオブジェクトを描画する
この4ステップでSwiftUIにSpriteKitのコンテンツが表示できます。
シミュレータで実行すると、以下のスクリーンショットのようになります。
SwiftUIの灰色の背景の中央に、SpriteKitの SKScene
の背景色である青い四角が表示されました。
さきに少し説明したとおり、SKScene
はSpriteKitのコンテンツを配置するためのベースとなるオブジェクトです。このシーン上に SKNode
オブジェクトを配置し、そのノードにシェーダーを適用していく流れになります。
2. SKScene に SKSpriteNode を追加する
次に、SpriteKitのノードをシーンへ追加します。
SKScene
の didMove
メソッドに、ノードを作成・表示するコードを加えます。今回は SKSpriteNode という種類のノードを使って説明します。
// 〜略〜
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)
}
}
-
SKSpriteNode
オブジェクトを作成する - ノードオブジェクトの color プロパティで色を指定する
- ノードオブジェクトの size プロパティでサイズを指定する
- addChild メソッドでノードオブジェクトをシーンへ追加する
この4ステップでシーンにノードが表示されます。
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 を適用する
最後に、フラグメントシェーダーを作成してノードに適用しましょう。
// 〜略〜
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)
}
}
-
SKShader
オブジェクトを作成する - ノードオブジェクトの shader プロパティに、シェーダーオブジェクトを格納する
この2ステップでシェーダーを表示する準備は完了です。
緑色のノード(=板ポリゴン)にフラグメントシェーダーが適用されて、黄色に変わりました。
SKShader
ではOpenGL ES 2.0のGLSLと同様に gl_FragColor
が組み込み変数として用意されています。この変数に格納された色が最終的な出力になります。
(1) の箇所では、シェーダーのソースコードを別のファイルに用意して読みこませる方法も使えます。
// .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
今回のサンプルコード全体
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 シリーズの記事一覧
- GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (1)(本記事)
- GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (2) 〜座標〜
- GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (3) 〜変数〜
- GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (4) 〜経過時間とマウス座標〜
- GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (5) 〜テクスチャと色 前編〜
- GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (6) 〜テクスチャと色 後編〜
- GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (7) 〜ノード〜
- GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (8) 〜3D〜
- GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (9) 〜落穂拾い〜
参考リンク
- Drawing SpriteKit Content in a View | Apple Developer Documentation
- Applying Shaders to a Sprite | Apple Developer Documentation
- Creating a Custom Fragment Shader | Apple Developer Documentation
- The Book of Shaders
- OpenGL ES 2.0 API Quick Reference Card (PDF)
-
Xcode Playgroundでも実行できれば理想的だったのですが、執筆時点の筆者の手元のバージョンではSpriteViewがサポートされていないようでした。Swift PlaygroundsのほうのXcodeプレイグラウンドでは動きます。 ↩︎
Discussion