GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (7) 〜ノード〜
はしがき
SwiftUIでSpriteKitのSKShaderを使って遊んでみようというテーマの記事の7回目です。
前回の記事 ではSKShaderでテクスチャと色を扱う際の応用例をいくつか紹介しました。
今回は SKShaderが使えるノードオブジェクト の全5種類を、簡単なサンプルコードとスクリーンショットを添えてざっと説明します。
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, ...)、スウィズル演算子や座標の正規化といった用語が前置きなしに出てきます。
環境
以下の環境で動作確認を行っています。
- 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)で撮影したものをベースにしています。
SKShader が使えるノードオブジェクト
SKSpriteNode
SKSpriteNode は、画像や単一の色を表示できるノードです。
前回までの記事で使いかたを紹介していますので、詳細はそちらをご参照ください。
SKSpriteNodeとSKShaderのサンプルコード全体
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 spriteNode = SKSpriteNode()
spriteNode.color = green
spriteNode.size = CGSize(width: 250, height: 200)
let shader = SKShader(
source: """
void main() {
float d = step(0.5, v_tex_coord.x - v_tex_coord.y * 0.3 + 0.15);
vec4 yellow = vec4(0.99, 0.83, 0.46, 1.0);
gl_FragColor = mix(SKDefaultShading(), yellow, d);
}
"""
)
spriteNode.shader = shader
self.addChild(spriteNode)
}
}
SKShapeNode
SKShapeNode は図形を表示できるノードです。直線や曲線、四角・円・楕円といった形状のほか、頂点の座標を指定した多角形やスプライン曲線を使った図形なども表示できます。
図形には 線 と 塗り の部分があり、それぞれに色の指定やテクスチャの貼りつけ、 シェーダーの適用 ができます。
strokeShader プロパティ
strokeShader は、シェイプノードの 線 の部分にシェーダーを適用できるプロパティです。
strokeShader
プロパティに用いるシェーダーには、2種類のSKShader独自の変数があります。どちらも SpriteKitがあらかじめ用意してくれている ので、特別な準備なしでシェーダーのコード内で使えます。
変数名 | 型 | 種類 | 概要 |
---|---|---|---|
u_path_length | float | uniform | 線の全長(単位はポイント)を取得できます。 |
v_path_distance | float | varying | 線の始点からの距離(単位はポイント)を取得できます。 |
これらの変数を使用したサンプルコードが以下になります。
let green = UIColor(red: 0.64, green: 0.81, blue: 0.46, alpha: 1.0)
// シェイプノードの頂点の座標をmutableな配列で用意します
var point = [
CGPoint(x: -100, y: 0),
CGPoint(x: -80, y: 50),
CGPoint(x: -50, y: -50),
CGPoint(x: -10, y: 25),
CGPoint(x: 40, y: -25),
CGPoint(x: 100, y: 0)
]
// シェーダーの適用先となるシェイプノードを作成します
let shapeNode = SKShapeNode(
// 渡した頂点の座標をもとにスプライン曲線を描画します
splinePoints: &point,
// 頂点の数を渡します
count: point.count
)
// 線の幅を指定します
shapeNode.lineWidth = 12
// 線の色を指定します
shapeNode.strokeColor = green
// 線の端の形状を指定します
shapeNode.lineCap = .round
let shader = SKShader(
source: """
void main() {
// X座標が0.0〜1.0、Y座標が-1.0〜1.0になるように正規化します
vec2 st = vec2(
v_path_distance / u_path_length,
// v_tex_coordの値はlineWidthの長さが1.0になるよう正規化されています
v_tex_coord.y * 2.0 - 1.0
);
// 終端に向けて線を細くします
float d = step(pow(st.x, 2.0), 1.0 - abs(st.y));
// 色を用意します
vec4 yellow = vec4(0.99, 0.83, 0.46, 1.0);
// 黄色から緑色(ノードがデフォルトで出力する色)のグラデーションを作ります
vec4 color = mix(yellow, SKDefaultShading(), st.x);
// 始端側を太い黄色、終端側を細い緑色で出力します
gl_FragColor = mix(vec4(0.0), color, d);
}
"""
)
// 線に対してシェーダーを適用します
shapeNode.strokeShader = shader
SKShapeNodeとSKShader(strokeShader)のサンプルコード全体
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)
var point = [
CGPoint(x: -100, y: 0),
CGPoint(x: -80, y: 50),
CGPoint(x: -50, y: -50),
CGPoint(x: -10, y: 25),
CGPoint(x: 40, y: -25),
CGPoint(x: 100, y: 0)
]
let shapeNode = SKShapeNode(
splinePoints: &point,
count: point.count
)
shapeNode.lineWidth = 12
shapeNode.strokeColor = green
shapeNode.lineCap = .round
let shader = SKShader(
source: """
void main() {
vec2 st = vec2(
v_path_distance / u_path_length,
v_tex_coord.y * 2.0 - 1.0
);
float d = step(pow(st.x, 2.0), 1.0 - abs(st.y));
vec4 yellow = vec4(0.99, 0.83, 0.46, 1.0);
vec4 color = mix(yellow, SKDefaultShading(), st.x);
gl_FragColor = mix(vec4(0.0), color, d);
}
"""
)
shapeNode.strokeShader = shader
self.addChild(shapeNode)
}
}
fillShader プロパティ
fillShader は、シェイプノードの 塗り の部分にシェーダーを適用できるプロパティです。
シェーダーのコードの書きかたは基本的にSKSpriteNodeと同じです。
let green = UIColor(red: 0.64, green: 0.81, blue: 0.46, alpha: 1.0)
// シェーダーの適用先となるシェイプノードを作成します
let shapeNode = SKShapeNode(
// シーンの中央に200x200の四角形を描画します
rect: CGRect(
origin: CGPoint(x: -100, y: -100),
size: CGSize(width: 200, height: 200)
),
// 半径20の角丸にします
cornerRadius: 20
)
// 線の幅を指定します
// 0を指定して線を非表示にします (デフォルト値は1.0)
shapeNode.lineWidth = 0
// 塗りの色を指定します
shapeNode.fillColor = green
let shader = SKShader(
source: """
void main() {
// 座標を正規化します
vec2 st = v_tex_coord * 2.0 - 1.0;
// 右上にドッグイア風の形を作ります
float d = step(1.0, dot(st, vec2(0.9)));
// 色を用意します
vec4 yellow = vec4(0.99, 0.83, 0.46, 1.0);
// 右上の部分を黄色で、それ以外の部分を緑色で出力します
gl_FragColor = mix(SKDefaultShading(), yellow, d);
}
"""
)
// 塗りに対してシェーダーを適用します
shapeNode.fillShader = shader
SKShapeNodeとSKShader(fillShader)のサンプルコード全体
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 shapeNode = SKShapeNode(
rect: CGRect(
origin: CGPoint(x: -100, y: -100),
size: CGSize(width: 200, height: 200)
),
cornerRadius: 20
)
shapeNode.lineWidth = 0
shapeNode.fillColor = green
let shader = SKShader(
source: """
void main() {
vec2 st = v_tex_coord * 2.0 - 1.0;
float d = step(1.0, dot(st, vec2(0.9)));
vec4 yellow = vec4(0.99, 0.83, 0.46, 1.0);
gl_FragColor = mix(SKDefaultShading(), yellow, d);
}
"""
)
shapeNode.fillShader = shader
self.addChild(shapeNode)
}
}
SKEmitterNode
SKEmitterNode はパーティクル(粒子)のエフェクトを表示できるノードです。生成座標、角度、速度、色など、さまざまなパラメータを設定してパーティクルを発生させることができます。テクスチャやシェーダーも適用できます。
let green = UIColor(red: 0.64, green: 0.81, blue: 0.46, alpha: 1.0)
// シェーダーの適用先となるエミッターノードを作成します
let emitterNode = SKEmitterNode()
// 粒子の1秒あたりの生成数を指定します
emitterNode.particleBirthRate = 600
// 何個で粒子の出現を終了させるかを指定します
emitterNode.numParticlesToEmit = 60
// 粒子の消えるまでの秒数を指定します
emitterNode.particleLifetime = 2
// 粒子の発生する位置の範囲を指定します
emitterNode.particlePositionRange = CGVector(dx: 125, dy: 125)
// 粒子のサイズを指定します
emitterNode.particleSize = CGSize(width: 50, height: 50)
// 粒子の色を指定します
emitterNode.particleColor = green
// 粒子の1秒あたりの透明度の変化量を指定します
emitterNode.particleAlphaSpeed = -1 / emitterNode.particleLifetime
let shader = SKShader(
source: """
void main() {
// 粒子のアルファ値の1->0への変化を経過時間として利用します
float t = SKDefaultShading().a;
// シーンの中央から外側へ放射状に座標を移動させます
// 移動量は徐々に少なくします
vec2 p = v_tex_coord * 2.0 - 1.0;
vec2 st = gl_FragCoord.xy * 2.0 - u_resolution;
st.y *= -1.0;
vec2 origin = st - p * u_size;
float th = atan(origin.y, origin.x);
p += vec2(cos(th), sin(th)) * (-0.5 + pow(t, 3.0));
// 丸い発光体を作ります
// 出現位置がシーンの外側になるほどサイズを小さくします
float l = length(origin) / min(u_resolution.x, u_resolution.y);
float d = 1.2 - length(p * mix(-5.0, 15.0, l));
d = pow(max(0.0, d), 3.0);
// シーン上の半径0.4の円の輪郭付近に出現した粒子のみ表示します
d *= l < 0.3 || 0.5 < l ? 0.0 : t;
// 色を用意します
vec4 yellow = vec4(0.99, 0.83, 0.46, 1.0);
// 黄色から緑色(ノードがデフォルトで出力する色)のグラデーションを作ります
vec4 color = mix(SKDefaultShading(), yellow, d);
// シーンの外側へ向けて移動する、円型に並んだ発光体を出力します
gl_FragColor = mix(vec4(0.0), color, d);
}
"""
)
// シーンの解像度の値をuniform変数としてシェーダーに渡します
shader.addUniform(
SKUniform(
name: "u_resolution",
vectorFloat2: vector_float2(
Float(self.frame.width * UITraitCollection.current.displayScale),
Float(self.frame.height * UITraitCollection.current.displayScale)
)
)
)
// 粒子のサイズの値をuniform変数としてシェーダーに渡します
shader.addUniform(
SKUniform(
name: "u_size",
vectorFloat2: vector_float2(
Float(emitterNode.particleSize.width * UITraitCollection.current.displayScale),
Float(emitterNode.particleSize.height * UITraitCollection.current.displayScale)
)
)
)
emitterNode.shader = shader
SKEmitterNodeとSKShaderのサンプルコード全体
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 emitterNode = SKEmitterNode()
emitterNode.particleBirthRate = 600
emitterNode.numParticlesToEmit = 60
emitterNode.particleLifetime = 2
emitterNode.particlePositionRange = CGVector(dx: 125, dy: 125)
emitterNode.particleSize = CGSize(width: 50, height: 50)
emitterNode.particleColor = green
emitterNode.particleAlphaSpeed = -1 / emitterNode.particleLifetime
let shader = SKShader(
source: """
void main() {
float t = SKDefaultShading().a;
vec2 p = v_tex_coord * 2.0 - 1.0;
vec2 st = gl_FragCoord.xy * 2.0 - u_resolution;
st.y *= -1.0;
vec2 origin = st - p * u_size;
float th = atan(origin.y, origin.x);
p += vec2(cos(th), sin(th)) * (-0.5 + pow(t, 3.0));
float l = length(origin) / min(u_resolution.x, u_resolution.y);
float d = 1.2 - length(p * mix(-5.0, 15.0, l));
d = pow(max(0.0, d), 3.0);
d *= l < 0.3 || 0.5 < l ? 0.0 : t;
vec4 yellow = vec4(0.99, 0.83, 0.46, 1.0);
vec4 color = mix(SKDefaultShading(), yellow, d);
gl_FragColor = mix(vec4(0.0), color, d);
}
"""
)
shader.addUniform(
SKUniform(
name: "u_resolution",
vectorFloat2: vector_float2(
Float(self.frame.width * UITraitCollection.current.displayScale),
Float(self.frame.height * UITraitCollection.current.displayScale)
)
)
)
shader.addUniform(
SKUniform(
name: "u_size",
vectorFloat2: vector_float2(
Float(emitterNode.particleSize.width * UITraitCollection.current.displayScale),
Float(emitterNode.particleSize.height * UITraitCollection.current.displayScale)
)
)
)
emitterNode.shader = shader
self.addChild(emitterNode)
}
}
SKTileMapNode
SKTileMapNode は、テクスチャをマス目状に敷きつめて表示できるノードです。どこのマスにテクスチャを配置するか配列で指定したり、四隅や上下左右の辺といった場所ごとにどのテクスチャを配置するか自動で指定したりできます。
ノードにシェーダーを適用すると、配置した全マス目のテクスチャでシェーダーが実行されます。
以下のサンプルコードですが、実は筆者自身がこのノードを使った経験がほとんどなく、あまりよい例ではないと思います(少なくともこのノードの機能をうまく活かせてはいないでしょう)。その点を念頭に置いていただき、雰囲気をちょっと眺めてみる程度の気持ちでご覧ください。
let green = UIColor(red: 0.64, green: 0.81, blue: 0.46, alpha: 1.0)
let yellow = UIColor(red: 0.99, green: 0.83, blue: 0.46, alpha: 1.0)
// タイルのサイズを用意します
let tileSize = CGSize(width: 25, height: 25)
// タイルを敷く位置のデータを用意します
// ループ処理しやすいようにタプルの配列でデータを作成しています
// (0, 0) が一番左下のマス目になります
let tilePosGreen: [(column: Int, row: Int)] = [
(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6),
(1, 0), (1, 3), (1, 6),
(2, 0), (2, 3), (2, 6),
(3, 0), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (3, 6),
(5, 0), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5), (5, 6),
(6, 0), (6, 3), (6, 6),
(7, 0), (7, 3), (7, 6),
(8, 0), (8, 1), (8, 2), (8, 3), (8, 4), (8, 5), (8, 6)
]
let tilePosYellow: [(column: Int, row: Int)] = [
(0, 0), (0, 3), (0, 6),
(1, 0), (1, 3), (1, 6),
(2, 0), (2, 3), (2, 6),
(3, 0), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (3, 6),
(5, 6),
(6, 6),
(7, 6),
(8, 0), (8, 1), (8, 2), (8, 3), (8, 4), (8, 5), (8, 6)
]
// 緑色と黄色にそれぞれ塗った四角形のノードからテクスチャを作成します
let textureGreen = self.view?.texture(
from: SKSpriteNode(color: green, size: tileSize)
)
let textureYellow = self.view?.texture(
from: SKSpriteNode(color: yellow, size: tileSize)
)
// テクスチャからタイルグループを作成します
let groupGreen = SKTileGroup(
tileDefinition: SKTileDefinition(
texture: textureGreen!
)
)
let groupYellow = SKTileGroup(
tileDefinition: SKTileDefinition(
texture: textureYellow!
)
)
// タイルグループからタイルセットを作成します
let tileSet = SKTileSet(
tileGroups: [groupGreen, groupYellow]
)
// シェーダーの適用先となるタイルマップノードを作成します
let tileMapNode = SKTileMapNode(
// 作成したタイルセットを渡します
tileSet: tileSet,
// タイルを敷くマス目全体の列数と行数を渡します
columns: 9,
rows: 7,
// タイルのサイズを渡します
tileSize: tileSize
)
// タイルをマス目に配置します
tilePosGreen.forEach {
tileMapNode.setTileGroup(
groupGreen,
forColumn: $0.column,
row: $0.row
)
}
tilePosYellow.forEach {
tileMapNode.setTileGroup(
groupYellow,
forColumn: $0.column,
row: $0.row
)
}
let shader = SKShader(
source: """
void main() {
// 座標を正規化します
vec2 st = v_tex_coord * 2.0 - 1.0;
// 黄色をより強く、緑色を弱く出力するため
// ノードに設定した色のRGB値のうち
// 赤の値を色の強さとして利用します
float l = SKDefaultShading().r;
// 円型を作ります
float d = pow(l, 2.0) - length(st * 1.2);
d = pow(max(0.0, 0.4 + d), 2.0);
// 円型をノードに設定した色で出力します
gl_FragColor = mix(vec4(0.0), SKDefaultShading(), d);
}
"""
)
tileMapNode.shader = shader
SKTileMapNodeとSKShaderのサンプルコード全体
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)
let yellow = UIColor(red: 0.99, green: 0.83, blue: 0.46, alpha: 1.0)
self.backgroundColor = blue
self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
let tileSize = CGSize(width: 25, height: 25)
let tilePosGreen: [(column: Int, row: Int)] = [
(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6),
(1, 0), (1, 3), (1, 6),
(2, 0), (2, 3), (2, 6),
(3, 0), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (3, 6),
(5, 0), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5), (5, 6),
(6, 0), (6, 3), (6, 6),
(7, 0), (7, 3), (7, 6),
(8, 0), (8, 1), (8, 2), (8, 3), (8, 4), (8, 5), (8, 6)
]
let tilePosYellow: [(column: Int, row: Int)] = [
(0, 0), (0, 3), (0, 6),
(1, 0), (1, 3), (1, 6),
(2, 0), (2, 3), (2, 6),
(3, 0), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (3, 6),
(5, 6),
(6, 6),
(7, 6),
(8, 0), (8, 1), (8, 2), (8, 3), (8, 4), (8, 5), (8, 6)
]
let textureGreen = self.view?.texture(
from: SKSpriteNode(color: green, size: tileSize)
)
let textureYellow = self.view?.texture(
from: SKSpriteNode(color: yellow, size: tileSize)
)
let groupGreen = SKTileGroup(
tileDefinition: SKTileDefinition(
texture: textureGreen!
)
)
let groupYellow = SKTileGroup(
tileDefinition: SKTileDefinition(
texture: textureYellow!
)
)
let tileSet = SKTileSet(
tileGroups: [groupGreen, groupYellow]
)
let tileMapNode = SKTileMapNode(
tileSet: tileSet,
columns: 9,
rows: 7,
tileSize: tileSize
)
tilePosGreen.forEach {
tileMapNode.setTileGroup(
groupGreen,
forColumn: $0.column,
row: $0.row
)
}
tilePosYellow.forEach {
tileMapNode.setTileGroup(
groupYellow,
forColumn: $0.column,
row: $0.row
)
}
let shader = SKShader(
source: """
void main() {
vec2 st = v_tex_coord * 2.0 - 1.0;
float l = SKDefaultShading().r;
float d = pow(l, 2.0) - length(st * 1.2);
d = pow(max(0.0, 0.4 + d), 2.0);
gl_FragColor = mix(vec4(0.0), SKDefaultShading(), d);
}
"""
)
tileMapNode.shader = shader
self.addChild(tileMapNode)
}
}
SKEffectNode
SKEffectNode は、自身の子ノード (*) にエフェクトを適用できるノードです。テクスチャ画像を貼った SKSpriteNode
を子ノードに追加して CIFilter をかけたり、SKWarpGeometry で変形させたりすることができます。
* 子ノードというのは、SKNode.addChild などのメソッドを使って、自身の子として追加したノードのことです。
SKLabelNode のような shader
プロパティを持っていないノードを子ノードに追加して、親ノードである SKEffectNode
にシェーダーを適用すれば、直接シェーダーを利用できないノードに対してもシェーダーで描画をカスタマイズできます。
ちなみに SKScene は SKEffectNode
のサブクラスです。シーンオブジェクトの shouldEnableEffects プロパティを true
にしてシェーダーを適用すると、シーン全体にシェーダーを表示させることもできます。
SKEffectNode
はレンダリングを別バッファで行うため、多用すると描画コストが上がる点には注意が必要です。子ノードのレンダリング結果をキャッシュしてパフォーマンスを改善する shouldRasterize プロパティなどもあります。
以下のサンプルコードでは、 SKLabelNode
で作成したテキストに対して SKEffectNode
を利用して CIFilter
をかけて、さらに SKShader
で最終的な色を調整しています。
// エフェクトノードに追加する子ノードとして
// ラベルノードを3つ用意します
// 中央の文字のラベルノードを作ります
let text = SKLabelNode(
// AttributedStringを表示する文字として指定します
attributedText: NSAttributedString(
string: "Special Menu",
attributes: [
NSAttributedString.Key.font: UIFont(
name: "BodoniSvtyTwoOSITCTT-Bold",
size: 38
)!,
NSAttributedString.Key.foregroundColor: UIColor.white,
NSAttributedString.Key.tracking: 2.1
]
)
)
// 文字の上下の位置揃えを中央に指定します
text.verticalAlignmentMode = .center
// 上側の飾り罫のラベルノードを作ります
let decorationTop = SKLabelNode(
text: String(repeating: "\u{007b}", count: 7)
)
// フォント名を指定します
decorationTop.fontName = "BodoniOrnamentsITCTT"
// フォントサイズを指定します
decorationTop.fontSize = text.frame.height * 0.9
// 文字の上下の位置揃えを中央に指定します
decorationTop.verticalAlignmentMode = .center
// ノードの上下の位置を調整します
decorationTop.position.y = text.frame.height * 1.5
// 下側の飾り罫のラベルノードを、上側の飾り罫のノードのコピーをもとに作ります
let decorationBottom = decorationTop.copy() as! SKLabelNode
// ノードの上下の位置を調整します
decorationBottom.position.y *= -1
// ノードを180度回転させます
decorationBottom.zRotation = CGFloat.pi
// シェーダーの適用先となるエフェクトノードを作成します
let effectNode = SKEffectNode()
// 用意したラベルノード3つをエフェクトノードに追加します
effectNode.addChild(text)
effectNode.addChild(decorationTop)
effectNode.addChild(decorationBottom)
// エフェクトノードに CIHeightFieldFromMask のフィルタを適用します
// 子ノードの文字のエッジ付近が黒く、エッジから離れるほど白い
// グレースケール画像が得られます
effectNode.filter = CIFilter(
name: "CIHeightFieldFromMask",
parameters: ["inputRadius": 3]
)
let shader = SKShader(
source: """
void main() {
// エフェクトノードの色を u_texture を使って取得します
vec4 texel = texture2D(u_texture, v_tex_coord);
// 色を用意します
vec4 yellow = vec4(0.99, 0.83, 0.46, 1.0);
vec4 green = vec4(0.64, 0.81, 0.46, 1.0);
// レトロなスタンプ風のグラデーションを作ります
vec4 color = mix(yellow, green, texel.r);
color = mix(color, vec4(0.0), pow(texel.r, 4.0));
// 作成したグラデーションで文字を出力します
gl_FragColor = mix(vec4(0.0), color, texel.a);
}
"""
)
effectNode.shader = shader
SKEffectNodeとSKShaderのサンプルコード全体
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)
self.backgroundColor = blue
self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
let text = SKLabelNode(
attributedText: NSAttributedString(
string: "Special Menu",
attributes: [
NSAttributedString.Key.font: UIFont(
name: "BodoniSvtyTwoOSITCTT-Bold",
size: 38
)!,
NSAttributedString.Key.foregroundColor: UIColor.white,
NSAttributedString.Key.tracking: 2.1
]
)
)
text.verticalAlignmentMode = .center
let decorationTop = SKLabelNode(
text: String(repeating: "\u{007b}", count: 7)
)
decorationTop.fontName = "BodoniOrnamentsITCTT"
decorationTop.fontSize = text.frame.height * 0.9
decorationTop.verticalAlignmentMode = .center
decorationTop.position.y = text.frame.height * 1.5
let decorationBottom = decorationTop.copy() as! SKLabelNode
decorationBottom.position.y *= -1
decorationBottom.zRotation = CGFloat.pi
let effectNode = SKEffectNode()
effectNode.addChild(text)
effectNode.addChild(decorationTop)
effectNode.addChild(decorationBottom)
effectNode.filter = CIFilter(
name: "CIHeightFieldFromMask",
parameters: ["inputRadius": 3]
)
let shader = SKShader(
source: """
void main() {
vec4 texel = texture2D(u_texture, v_tex_coord);
vec4 yellow = vec4(0.99, 0.83, 0.46, 1.0);
vec4 green = vec4(0.64, 0.81, 0.46, 1.0);
vec4 color = mix(yellow, green, texel.r);
color = mix(color, vec4(0.0), pow(texel.r, 4.0));
gl_FragColor = mix(vec4(0.0), color, texel.a);
}
"""
)
effectNode.shader = shader
self.addChild(effectNode)
}
}
前回の記事で書きました が、 SKEffectNode
でもマルチシェーダーが可能です。
下図のような流れで複数のシェーダーを適用させて、アニメーションするマルチシェーダーを実行することもできます。
繰り返しになりますが、 SKEffectNode
を多用すると描画コストが上がることには留意すべきでしょう。
前述の shouldRasterize
プロパティを true
にするとレンダリングの負荷は下がる代わりに、子ノードのシェーダーのアニメーションは動かなくなります(前回紹介したノードをテクスチャにする方法 と同じように、静止画の状態になります)。
また、 SKEffectNode
の子ノードでは、ポイント値からピクセル値に変換する倍率が、Retinaの倍率に関係なく 2.0 になるようなので、ピクセル解像度をシェーダーの座標計算で使う場合は気をつけてください。公式ドキュメントに該当の記述が見あたらず、シミュレータの仕様や不具合の可能性も考えられるので、図中の "First shader" にあたる箇所で実行するシェーダーで座標を扱うときは v_tex_coord
を使っておくほうが無難だと思います。
まとめ
今回はSKShaderが使える各種ノードについて紹介しました。
図形や文字などのさまざまなノードにシェーダー、さらにテクスチャやエフェクトを組み合わせると、表現の幅がいっそう広がって、バラエティ豊かなコンテンツをSwiftUI上に表示できるようになりますね。
次回は、SpriteKitで 3Dコンテンツを表示する方法 についてとりあげたいと思います。よろしければ引き続きご覧ください。
次記事 → GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (8) 〜3D〜
参考リンク
- Drawing SpriteKit Content in a View | Apple Developer Documentation
- Creating a Custom Fragment Shader | Apple Developer Documentation
- Getting Started with Sprite Nodes | Apple Developer Documentation
- Applying Shaders to a Sprite | Apple Developer Documentation
- Getting Started with Shape Nodes | Apple Developer Documentation
- Controlling Shape Drawing with Shaders | Apple Developer Documentation
- Creating Particle Effects | Apple Developer Documentation
- Getting Started with Particle Shaders | Apple Developer Documentation
- Applying Special Effects to a Node's Children | Apple Developer Documentation
- Improving the Performance of Static Content | Apple Developer Documentation
- Accessing and Modifying the Node Tree | Apple Developer Documentation
- The Book of Shaders
- OpenGL ES 2.0 API Quick Reference Card (PDF)
Discussion