GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (6) 〜テクスチャと色 後編〜
はしがき
SwiftUIでSpriteKitのSKShaderを使って遊んでみようというテーマの記事の6回目です。
前回の記事 ではテクスチャと色の基本をまとめました。
今回は テクスチャ と 色 について、少し応用的な使いかたやTips的なものをいくつか紹介したいと思います。
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)で撮影したものをベースにしています。
テクスチャの応用
テクスチャを uniform 変数としてシェーダーへ渡す(マルチテクスチャ)
シェーダーでテクスチャを表示する方法として、 前回の記事 で紹介した、 texture
プロパティでノードに貼ったテクスチャを u_texture
の組み込み変数を介して取得・表示するやりかた以外に、テクスチャの SKUniform
オブジェクトを作成してシェーダーへ渡し、そのuniform変数を介して取得・表示するやりかたがあります。この方法を利用すると、 複数のテクスチャをシェーダーで扱うことができます 。
※uniform変数の渡しかたの基本については 第3回の記事 をご参照ください。
// テクスチャオブジェクトを引数に指定してSKUniformオブジェクトを作成します
let additionalTexture = SKUniform(
name: "u_additional_texture",
texture: SKTexture(imageNamed: "fileName")
)
let shader = SKShader(
source: """
void main() {
// uniform変数で渡されたテクスチャを出力します
gl_FragColor = texture2D(u_additional_texture, v_tex_coord);
}
""",
// 作成したSKUniformオブジェクトをシェーダーに渡します
uniforms: [additionalTexture]
)
ノイズのテクスチャを作成する SKTexture(noiseWithSmoothness smoothness: CGFloat, size: CGSize, grayscale: Bool) と、テクスチャのSKUniformオブジェクトを作成する SKUniform(name: String, texture: SKTexture?) を使用して、ノードに貼ったテクスチャを手書きイラスト風に加工してみましょう。
ノードに貼るテクスチャ用の画像は、前回の記事 と同じSVGファイルを使用しています。
// 〜略〜
class MySKScene: SKScene {
override func didMove(to view: SKView) {
let white = UIColor(red: 0.99, green: 0.97, blue: 0.93, alpha: 1.0)
let orange = UIColor(red: 0.93, green: 0.73, blue: 0.38, alpha: 1.0)
self.backgroundColor = white
self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
let node = SKSpriteNode()
node.size = CGSize(width: 200, height: 200)
// 画像ファイルからテクスチャオブジェクトを作成してノードに適用します
let texture = SKTexture(imageNamed: "yacht")
node.texture = texture
// ノードの色とブレンドの比率を指定します
node.color = orange
node.colorBlendFactor = 0.5
// シェーダーを作成します
let shader = SKShader(
source: """
void main() {
// uniform変数で渡されたノイズのテクスチャの色を取得します
vec4 noise = texture2D(u_noise_texture, v_tex_coord);
// ノードに適用されたテクスチャの色を取得します
// ノイズを利用して座標をランダムにずらし、輪郭を粗くします
vec4 texel = texture2D(u_texture, v_tex_coord + (noise.rg - 0.5) * 0.01);
// ノイズを利用してノードに適用されたテクスチャと色をmixし、ムラを出します
gl_FragColor = mix(texel, texel * v_color_mix, noise.b * 0.4);
}
"""
)
// ノイズのテクスチャオブジェクトを作成します
let noiseTexture = SKTexture(
noiseWithSmoothness: 0.05,
size: node.frame.size,
grayscale: true
)
// ノイズのテクスチャオブジェクトをuniform変数としてシェーダーに渡します
shader.addUniform(
SKUniform(
name: "u_noise_texture",
texture: noiseTexture
)
)
// 作成したシェーダーをノードに適用します
node.shader = shader
self.addChild(node)
}
}
輪郭線や塗りの部分に、画用紙に描いたような凸凹が足されました。
マルチテクスチャのサンプルコード全体
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 white = UIColor(red: 0.99, green: 0.97, blue: 0.93, alpha: 1.0)
let orange = UIColor(red: 0.93, green: 0.73, blue: 0.38, alpha: 1.0)
self.backgroundColor = white
self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
let node = SKSpriteNode()
node.size = CGSize(width: 200, height: 200)
let texture = SKTexture(imageNamed: "yacht")
node.texture = texture
node.color = orange
node.colorBlendFactor = 0.5
let shader = SKShader(
source: """
void main() {
vec4 noise = texture2D(u_noise_texture, v_tex_coord);
vec4 texel = texture2D(u_texture, v_tex_coord + (noise.rg - 0.5) * 0.01);
gl_FragColor = mix(texel, texel * v_color_mix, noise.b * 0.4);
}
"""
)
let noiseTexture = SKTexture(
noiseWithSmoothness: 0.05,
size: node.frame.size,
grayscale: true
)
shader.addUniform(
SKUniform(
name: "u_noise_texture",
texture: noiseTexture
)
)
node.shader = shader
self.addChild(node)
}
}
テクスチャにフィルタをかける
SKTexture
の applying メソッドを使うと、テクスチャに CIFilter をかけることができます。
// テクスチャオブジェクトを作成します
let texture = SKTexture(imageNamed: "fileName")
// 作成したテクスチャオブジェクトをもとにして、
// フィルタをかけた新たなテクスチャオブジェクトを作成します
let filteredTexture = texture.applying(
CIFilter(
// フィルタの種類として、ガウシアンブラーを指定します
name: "CIGaussianBlur",
// パラメータとして、ぼかす半径をピクセル値で指定します
parameters: ["inputRadius": 10.0]
)!
)
フィルタをかけたテクスチャをノードに貼り、そのノードにシェーダーを適用してさらに描画をカスタマイズするようなこともできます。
実際にフィルタを使ってみましょう。さきほど説明したマルチテクスチャの仕組みも利用して、ノードに貼ったテクスチャと、 CIPhotoEffectTonal
のフィルタをかけてからuniform変数として渡したテクスチャを、シェーダーを使って合成する例をお見せします。
処理の流れをフロー図にすると上記のような感じです。
// 〜略〜
class MySKScene: SKScene {
override func didMove(to view: SKView) {
let white = UIColor(red: 0.99, green: 0.97, blue: 0.93, alpha: 1.0)
self.backgroundColor = white
self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
let node = SKSpriteNode()
node.size = CGSize(width: 200, height: 200)
// 画像ファイルからテクスチャオブジェクトを作成してノードに適用します
let texture = SKTexture(imageNamed: "yacht")
node.texture = texture
// シェーダーを作成します
let shader = SKShader(
source: """
void main() {
// ノードに適用されたテクスチャの色を取得します
vec4 texel = texture2D(u_texture, v_tex_coord);
// uniform変数で渡された、フィルタをかけたテクスチャの色を取得します
vec4 filtered = texture2D(u_filtered_texture, v_tex_coord);
// ノードの中心からの距離を計算します
float l = length(v_tex_coord * 2.0 - 1.0);
// 中心から離れるほどフィルタのかかった褪せた色で出力します
gl_FragColor = mix(texel, filtered, min(1.0, pow(l, 2.0)));
}
"""
)
// フィルタをかけたテクスチャオブジェクトを作成します
let filteredTexture = texture.applying(
CIFilter(name: "CIPhotoEffectTonal")!
)
// フィルタをかけたテクスチャオブジェクトを
// uniform変数としてシェーダーに渡します
shader.addUniform(
SKUniform(
name: "u_filtered_texture",
texture: filteredTexture
)
)
// 作成したシェーダーをノードに適用します
node.shader = shader
self.addChild(node)
}
}
ほのかに褪せたような雰囲気が出せました。
テクスチャにフィルタをかけるサンプルコード全体
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 white = UIColor(red: 0.99, green: 0.97, blue: 0.93, alpha: 1.0)
self.backgroundColor = white
self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
let node = SKSpriteNode()
node.size = CGSize(width: 200, height: 200)
let texture = SKTexture(imageNamed: "yacht")
node.texture = texture
let shader = SKShader(
source: """
void main() {
vec4 texel = texture2D(u_texture, v_tex_coord);
vec4 filtered = texture2D(u_filtered_texture, v_tex_coord);
float l = length(v_tex_coord * 2.0 - 1.0);
gl_FragColor = mix(texel, filtered, min(1.0, pow(l, 2.0)));
}
"""
)
let filteredTexture = texture.applying(
CIFilter(name: "CIPhotoEffectTonal")!
)
shader.addUniform(
SKUniform(
name: "u_filtered_texture",
texture: filteredTexture
)
)
node.shader = shader
self.addChild(node)
}
}
ノードをテクスチャにする(マルチシェーダー)
SKTexture
オブジェクトを作成する手順として、 SKView の texture メソッドを使ってノードをテクスチャにする方法があります。 前回の記事の最初 で、SVGファイルを使う代わりとして例示したテクスチャを作成するコードの中でも、このやりかたを利用しています。
// 画像ファイルからテクスチャオブジェクトを作成します
let texture = SKTexture(imageNamed: "fileName")
// テクスチャオブジェクトを引数で渡してノードを作成します
let node = SKSpriteNode(texture: texture)
// ノードに貼ったテクスチャに色をブレンドします
node.color = .purple
node.colorBlendFactor = 0.1
// ノードからテクスチャオブジェクトを作成します
// ブレンドした色が反映された新たなテクスチャが作られます
let newTexture = self.view?.texture(from: node)
// didMove(to view: SKView)のメソッド内であればview変数が使えるので、
// 以下のように書くこともできます
let newTexture = view.texture(from: node)
この方法を使うと、以下のフロー図のように、シェーダーを適用したノードをテクスチャ化して、そのテクスチャを貼ったノードにさらにシェーダーを適用する、という マルチシェーダーのような処理も実現できます 。
2つのシェーダーを適用させるサンプルコードを例示します。1段階目のシェーダーでテクスチャを変形させ、2段階目のシェーダーで残像風のぼかし処理を加えています。
※長くなるのでサンプルコードを折りたたみます。
2つのシェーダーを適用させるサンプルコード
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 white = UIColor(red: 0.99, green: 0.97, blue: 0.93, alpha: 1.0)
self.backgroundColor = white
self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
// ノードからテクスチャオブジェクトを作成します
let texture = {
// 画像ファイルから作成したテクスチャオブジェクトを使って、
// 1段階目のシェーダーの適用先となるノードを作成します
let node = SKSpriteNode(
texture: SKTexture(imageNamed: "yacht"),
size: CGSize(width: 200, height: 200)
)
// 1段階目のシェーダーを作成します
let shader = SKShader(
source: """
void main() {
// テクスチャのヨットの位置を右下方向へ移動します
vec2 uv = v_tex_coord + vec2(-0.12, 0.24);
// ヨットの幅を狭めます
uv.x *= 1.15;
// ヨットの帆の部分の高さを増やします
uv.y -= max(0.0, (uv.y - 0.37) * 0.4);
// ヨットの船体部分の高さを減らします
uv.y += min(0.0, (uv.y - 0.37) * 1.2);
// 位置と大きさを調整したテクスチャを出力します
gl_FragColor = texture2D(u_texture, uv);
}
"""
)
// 作成した1段階目のシェーダーをノードに適用します
node.shader = shader
// SKView.textureメソッドでノードをテクスチャオブジェクトにします
return self.view?.texture(from: node)
}()
// 作成したテクスチャオブジェクトを使って、
// 2段階目のシェーダーの適用先となるノードを作成します
let node = SKSpriteNode(texture: texture)
// 2段階目のシェーダーを作成します
let shader = SKShader(
source: """
void main() {
// ノードに適用されたテクスチャの色を取得します
vec4 color = texture2D(u_texture, v_tex_coord);
for (int i = 1; i <= 2; i++) {
// ノードに適用されたテクスチャの色を
// 左へずらし、高さをやや小さくして取得します
vec4 texel = texture2D(
u_texture,
vec2(
v_tex_coord.x + exp(i * 0.6) / 30.0,
v_tex_coord.y / exp(i * -0.02) - exp(i * 0.002) + 1.0
)
);
// 色をぼかす割合を計算します
float weight = exp(i * -0.9);
// 取得したテクスチャの色をぼかします
texel = mix(vec4(0.0), texel, weight);
// 元のテクスチャと、色をぼかしたテクスチャの
// どちらを反映させるかを計算します
float d = v_tex_coord.x > 0.55 || color.a > 0.0 ? 1.0 : 0.0;
// 元のテクスチャの左側に、ぼかしたテクスチャを追加します
color = mix(texel, color, d);
}
// 残像風の加工を足したテクスチャを出力します
gl_FragColor = color;
}
"""
)
// 作成した2段階目のシェーダーをノードに適用します
node.shader = shader
self.addChild(node)
}
}
シンボルをテクスチャにする
SF Symbolsやカスタムシンボルを、単純に SKTexture(image: UIImage(systemName: "シンボル名"))
などで SKTexture
オブジェクトにしてノードに適用すると、 黒の単色で塗られたシンボルが表示されます 。 UIImage.SymbolConfiguration
や UIImage.withTintColor
で シンボルに色を設定しても無視されてしまい 、 SKSpriteNode
の color
プロパティや colorBlendFactor
プロパティで色を調整することも難しくなるので不便です。
シンボル自体の色やcolorプロパティの設定を活かせるかたちでテクスチャにしたい場合の対応策として、シンボルのUIImageを UIGraphicsImageRenderer の image(actions:) メソッドで別のUIImageに書き出してから SKTexture
オブジェクトを作成する方法があります。 [1]
実装例は以下のようになります。
※長くなるのでサンプルコードを折りたたみます。
※コードの途中、シンボルをリサイズする処理で泥くさい書きかたをしているのと、シンボルの種類や実行環境によってはうまくリサイズされないケースもあるかもしれないので、参考程度にご覧ください。
シンボルをテクスチャにするサンプルコード
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 white = UIColor(red: 0.99, green: 0.97, blue: 0.93, alpha: 1.0)
let orange = UIColor(red: 0.93, green: 0.73, blue: 0.38, alpha: 1.0)
self.backgroundColor = white
self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
// テクスチャを適用するノードのサイズです
let textureSize = 200.0
// シンボルからテクスチャオブジェクトを作成します
let symbolTexture: SKTexture? = {
guard let image = UIImage(
// 別途用意したカスタムシンボルを読みこみます
named: "custom.info.circle.fill"
// Swift Playgroundsで試す場合は、適当な既存のシンボルを指定してください
// systemName: "info.circle.fill"
)?.applyingSymbolConfiguration(
// シンボルのサイズを指定します
UIImage.SymbolConfiguration(
pointSize: textureSize
)
)?.applyingSymbolConfiguration(
// パレットカラーで色づけします
UIImage.SymbolConfiguration(
paletteColors: [
.systemBlue,
.init(white: 0.82, alpha: 1.0),
.init(white: 0.95, alpha: 1.0)
]
)
) else { return nil }
// モノクロにしたいときは withTintColor で色を指定できます
// 白色にしておくと、シェーダーやノードのプロパティを利用した色のコントロールがしやすいです
// .withTintColor(.white)
// デフォルトで付与されるシンボルの周囲の余白をつめ、
// シンボルのアスペクト比を保ったまま画像を正方形にするためにリサイズします
// テクスチャにしたときの余白やアスペクト比が問題にならない場合は、
// このリサイズの処理は省略できます
let renderer = UIGraphicsImageRenderer(
size: CGSize(width: textureSize, height: textureSize)
)
let coloredSymbolImage = renderer.image { _ in
// デフォルトの余白部分を含むシンボルのサイズを取得します
let originalSize = [
image.size.width,
image.size.height
]
// 余白部分を除いたシンボルのサイズを取得します
let cgImageSize: [CGFloat] = [
CGFloat(image.cgImage?.width ?? 0),
CGFloat(image.cgImage?.height ?? 0)
].enumerated().map {
$0.element.isZero
? originalSize[$0.offset]
: $0.element / image.scale
}
// 余白部分を除いたシンボルの大きさを、
// textureSizeで指定した大きさへリサイズするための倍率を計算します
let scale = textureSize / cgImageSize.max()!
// リサイズ時のシンボルの描画サイズを計算します
let drawSize = originalSize.map { $0 * scale }
// シンボルのUIImageオブジェクトに含まれるcontentInsetsの値を
// 取得できるプロパティがなさそうなので、
// 正規表現でUIImage.descriptionの文字列から抽出します
let contentInsets = {
let pattern = "contentInsets[={ ]+(?<insets>(?:[0-9.]+[, ]*){4})"
let description = image.description
let defaultValue = [Double](repeating: 0.0, count: 4)
guard let regex = try? NSRegularExpression(
pattern: pattern
) else { return defaultValue }
guard let match = regex.firstMatch(
in: description,
range: NSRange(location: 0, length: description.utf16.count)
) else { return defaultValue }
let matchedString = NSString(string: description)
.substring(with: match.range(withName: "insets"))
return matchedString.components(separatedBy: ",").map {
Double($0.trimmingCharacters(in: .whitespaces)) ?? 0.0
}
}()
// デフォルトの余白の大きさをもとに
// 配置のバランスを調整するためのオフセット率を計算します
let offsetRatio = [
contentInsets[1] / (contentInsets[1] + contentInsets[3]),
contentInsets[0] / (contentInsets[0] + contentInsets[2])
].map { $0.isNaN ? 0.5 : $0 }
// リサイズ時の原点の位置のオフセット値を計算します
let offset = drawSize.enumerated().map {
(textureSize - $0.element) * offsetRatio[$0.offset]
}
// シェーダーでテクスチャを移動や縮小させたときに
// 端のピクセルが引きのばされて描画がくずれるのを防ぐために、
// 周囲に最大1ポイントの余白を付与するための値を計算します
let padding = drawSize.map { $0 / drawSize.max()! }
// シンボルをリサイズして描画します
image.draw(
in: CGRect(
x: offset[0] + padding[0],
y: offset[1] + padding[1],
width: drawSize[0] - padding[0] * 2.0,
height: drawSize[1] - padding[1] * 2.0
)
)
}
// リサイズの処理はここまで
// リサイズしたシンボルのUIImageオブジェクトから
// SKTextureオブジェクトを作成します
return SKTexture(image: coloredSymbolImage)
// リサイズ処理が不要な場合は、次の3行で済みます
// let renderer = UIGraphicsImageRenderer(size: image.size)
// let coloredSymbolImage = renderer.image { _ in image.draw(at: CGPoint.zero) }
// return SKTexture(image: coloredSymbolImage)
}()
// シンボルのテクスチャ・色・サイズを引数で渡してノードを作成します
let node = SKSpriteNode(
texture: symbolTexture,
color: orange,
size: symbolTexture?.size() ?? CGSize.zero
)
node.colorBlendFactor = 1.0
let shader = SKShader(
source: """
void main() {
// uniform変数で渡されたノイズのテクスチャの色を取得します
vec4 noise = texture2D(u_noise_texture, v_tex_coord);
// シンボルのテクスチャの色を、ランダムにRGBをずらして取得します
vec4 texel = vec4(
texture2D(u_texture, v_tex_coord + vec2(noise.r * 0.02, -0.002)).r,
texture2D(u_texture, v_tex_coord - vec2(noise.g * 0.02, 0.004)).g,
texture2D(u_texture, v_tex_coord + vec2(0.001, noise.b * 0.004)).b,
texture2D(u_texture, v_tex_coord).a
);
// シンボルの輪郭をランダムに崩します
texel.a = min(1.0, texel.a + noise.a * dot(texel.rgb, vec3(1.0)));
// 走査線風の模様を作ります
float l = 160.0;
l = smoothstep(0.3, 0.8, sin(v_tex_coord.y * l));
l = 0.3 + l * mix(0.6, 1.2, noise.a);
// シンボルと模様を重ねて出力します
gl_FragColor = mix(texel, texel * v_color_mix, l);
}
"""
)
shader.addUniform(
SKUniform(
name: "u_noise_texture",
texture: SKTexture(
noiseWithSmoothness: 0.2,
size: node.frame.size,
grayscale: false
)
)
)
node.shader = shader
self.addChild(node)
}
}
※スクリーンショットは Light Appearance で撮影しています。
色の応用
UIColor を変数としてシェーダーへ渡す
uniform変数やattribute変数でUIColorの色をシェーダーへ渡したいときは、RGBAの各数値でvec4の変数を作ります。
UIColorのRGBAの各数値を取得する手段として UIColor.getRed メソッドがあります。
var red = CGFloat.zero
var green = CGFloat.zero
var blue = CGFloat.zero
var alpha = CGFloat.zero
UIColor.blue.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
let color = SKUniform(
name: "u_color",
vectorFloat4: vector_float4(
Float(red),
Float(green),
Float(blue),
Float(alpha)
)
)
このようにしてRGBAの各数値を使った変数が作れます。
ただ、 UIColor.getRed
メソッドは、色の値を格納するための変数をあらかじめ用意しておかなければならないので、少しばかり手間です(いちどextensionでも書いてしまえばあとはラクにすみますが)。
別の手段として、いったん CIColor オブジェクトに変換するという方法もあります。このオブジェクトでは直接RGBAの各数値が取得できます。
let ciColor = CIColor(color: UIColor.blue)
let color = SKUniform(
name: "u_color",
vectorFloat4: vector_float4(
Float(ciColor.red),
Float(ciColor.green),
Float(ciColor.blue),
Float(ciColor.alpha)
)
)
さらに別の手段として、UIColor.cgColor.components プロパティを利用する方法もあります。このプロパティでも直接RGBAの各数値が取得できます。
let color = SKUniform(
name: "u_color",
vectorFloat4: vector_float4(
Float(UIColor.blue.cgColor.components![0]),
Float(UIColor.blue.cgColor.components![1]),
Float(UIColor.blue.cgColor.components![2]),
Float(UIColor.blue.cgColor.alpha)
)
)
CGColor.components
プロパティは、 取得元の色の種類によってはnilやエラーが返ってくることもあるので注意が必要 [2] です。パフォーマンスも UIColor.getRed
や CIColor
のやりかたに比べると遅くなりますが、別の変数を作らなくてよいので、確実にcomponentsの配列に値が入るとわかっているケースならばシンプルに書けて便利です。 [3]
シェーダーで出力する色をSwiftUI側から指定したいとき、View上で色を Color
から UIColor
に変換してSKSceneへ渡し、上記のメソッドやプロパティを利用してuniform変数やattribute変数を作成してシェーダーで使う、といったことも可能です。
Asset CatalogのColor Setに登録した色も同様に変数にしてシェーダーへ渡せますし、Appearanceによってシェーダーの出力する色を変更させることもできます。
以下の飾り枠を表示するサンプルコードでは、
- SwiftUIから渡したインディゴ色(uniform変数)
- シェーダーを適用するノードの
color
プロパティで指定した黒色(v_color_mix変数) - 内枠の部分を描画しているノードで使っているオレンジ色(uniform変数)
の3色を使ってシェーダーで模様を描画しています。
※長くなるのでサンプルコードを折りたたみます。
UIColorをuniform変数で渡すサンプルコード
import SwiftUI
import SpriteKit
struct ContentView: View {
var currentScene: SKScene {
let scene = MySKScene()
scene.scaleMode = .resizeFill
// インディゴ色をシーンへ渡します
scene.baseColor = UIColor(Color.indigo) // --- (1)
return scene
}
var body: some View {
ZStack {
Color(white: 0.8)
SpriteView(scene: self.currentScene)
.frame(width: 300, height: 250)
}
}
}
class MySKScene: SKScene {
// SwiftUI側から色を渡すための変数を用意します
var baseColor: UIColor = .clear
override func didMove(to view: SKView) {
// シーンの背景色をSwiftUI側から渡された色にします (optional)
self.backgroundColor = baseColor
self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
// シェーダーの適用先となるノードを作成します
// colorプロパティには黒色を指定しておきます
let node = SKSpriteNode(
color: UIColor.black, // --- (2)
size: CGSize(width: 250, height: 200)
)
// オレンジ色の内枠を表示するためのノードを作成してシーンに追加します
let borderNode = SKSpriteNode(
color: UIColor.systemOrange, // --- (3)
size: CGSize(
width: node.frame.width + 10,
height: node.frame.height + 10
)
)
self.addChild(borderNode)
// UIColor.getRedメソッドの使用例
// SwiftUI側から渡された色のRGBA値を取得します
var red = CGFloat.zero
var green = CGFloat.zero
var blue = CGFloat.zero
var alpha = CGFloat.zero
baseColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
// 取得したRGBA値を使ってuniform変数を作成します
let mainColor = SKUniform(
name: "u_main_color",
vectorFloat4: vector_float4(
Float(red),
Float(green),
Float(blue),
Float(alpha)
)
)
// CIColorオブジェクトの使用例
// 内枠のノードの色に指定されているUIColorオブジェクトから
// CIColorオブジェクトを作成します
let ciColor = CIColor(color: borderNode.color)
// CIColorオブジェクトのRGBA値のプロパティを使って
// uniform変数を作成します
let subColor = SKUniform(
name: "u_sub_color",
vectorFloat4: vector_float4(
Float(ciColor.red),
Float(ciColor.green),
Float(ciColor.blue),
Float(ciColor.alpha)
)
)
let shader = SKShader(
source: """
void main() {
// 上端と下端に半円型を並べます
float n = 4.6; // 半円の数
vec2 p = v_tex_coord * 2.0 - 1.0;
p.x *= n / 2.0;
float d = fract(p.x) - 0.5;
d = step(tan(d * 1.5708) * d * 2.0, abs(p.y) * 4.0 - 2.7);
// 地の部分と半円型の部分を、uniform変数で渡された色で塗りわけます
vec4 color = mix(u_main_color, u_sub_color, d);
// 半円型を1つおきにノードの色で塗りかさねて出力します
d *= mod(floor(p.x) + floor(p.y), 2.0);
gl_FragColor = mix(color, v_color_mix, d * 0.8);
}
""",
// 2色のuniform変数をシェーダーに渡します
uniforms: [mainColor, subColor]
)
node.shader = shader
self.addChild(node)
}
}
※スクリーンショットは Light Appearance で撮影しています。
SKScene(SpriteView)の背景を透過する
SpriteView
をSwiftUIの他のViewと重ねて表示させるとき、シーンの背景を透過したいケースがあると思います。
-
SKScene
のbackgroundColor
プロパティで.clear
を指定する -
SpriteView
のoptions
引数で .allowsTransparency を指定する
この2つの設定で、シーンの背景を透過できます。
import SwiftUI
import SpriteKit
struct ContentView: View {
var currentScene: SKScene {
let scene = MySKScene()
scene.scaleMode = .resizeFill
// シーンの背景色を透明にします --- (1)
// SKSceneのdidMoveメソッド内で指定してもかまいません
scene.backgroundColor = .clear
return scene
}
var body: some View {
SpriteView(
scene: self.currentScene,
// 背景の透過を有効にします --- (2)
options: [.allowsTransparency]
)
}
}
下の例のような感じで、SwiftUIのViewにシェーダーを重ねて見せることできます。
ミント色とティール色のグラデーションをかけた角丸四角形と白色のテキスト部分がSwiftUIで、ドット柄のアニメーション部分がSKShaderです。
※長くなるのでサンプルコードを折りたたみます。
シーンの背景を透過するサンプルコード
import SwiftUI
import SpriteKit
struct ContentView: View {
var currentScene: SKScene {
let scene = MySKScene()
scene.scaleMode = .resizeFill
// シーンの背景色を透明にします
scene.backgroundColor = .clear
return scene
}
var body: some View {
ZStack {
Color(white: 0.8)
Text("NEW ARRIVAL")
.font(.largeTitle)
.fontWeight(.semibold)
.foregroundStyle(.white)
.frame(width: 300, height: 100)
.background {
RoundedRectangle(cornerRadius: 10)
.fill(RadialGradient(
gradient: Gradient(colors: [.mint, .teal]),
center: .top,
startRadius: 2,
endRadius: 200
))
.overlay {
SpriteView(
scene: self.currentScene,
// 背景の透過を有効にします
options: [.allowsTransparency]
)
.frame(width: 290, height: 90)
.clipShape(RoundedRectangle(cornerRadius: 5))
}
}
}
}
}
class MySKScene: SKScene {
override func didMove(to view: SKView) {
self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
let node = SKSpriteNode(color: .clear, size: self.frame.size)
let shader = SKShader(
source: """
void main() {
// 正規化済みの座標にアスペクト比を反映させます
vec2 p = v_tex_coord * a_aspect_ratio;
// 右方向へ移動する斜線を作ります
float m = sin(p.x * 3.1416 * -0.36 - p.y * 1.2 + u_time * 1.7);
// 水玉模様を作ります
// 移動する斜線の 中心 > 左右端 で水玉の大きさが変化するようにします
float r = pow(m, 3.0) * 0.35; // 水玉の半径
p *= 12.0; // 1列あたりの水玉の数
float d = step(length(fract(p) - 0.5), r);
// 水玉を斜め格子状に置きます
d *= mod(floor(p.x) + floor(p.y), 2.0);
// 水玉模様を薄い紺色で出力します
vec4 navy = vec4(0.18, 0.34, 0.45, 1.0);
gl_FragColor = mix(vec4(0.0), navy, d * 0.4);
}
"""
)
shader.attributes = [
SKAttribute(
name: "a_aspect_ratio",
type: .vectorFloat2
)
]
node.setValue(
SKAttributeValue(
vectorFloat2: vector_float2(
Float(node.frame.width),
Float(node.frame.height)
) / Float(min(node.frame.width, node.frame.height))
),
forAttribute: "a_aspect_ratio"
)
node.shader = shader
self.addChild(node)
}
}
※スクリーンショットは Light Appearance で撮影しています。
まとめ
テクスチャと色を扱う方法について、前後編の2回にわたってとりあげました。
SKShaderでもマルチテクスチャやマルチシェーダーのような作りこみもできますし、SwiftUIで出番が多いであろうシンボルも、テクスチャにしてシェーダーで個性的なエフェクトを追加できます。ぜひ試してみてください。
次回は、SKSpriteNode以外にもある SKShaderが使えるノードオブジェクト を紹介したいと思います。よろしければ引き続きご覧ください。
次記事 → GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (7) 〜ノード〜
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
- Loading and Using Textures | Apple Developer Documentation
- Core Image Filter Reference | Apple Developer Documentation Archive
- Configuring and displaying symbol images in your UI | Apple Developer Documentation
- UIGraphicsImageRenderer | Apple Developer Documentation
- The Book of Shaders
- OpenGL ES 2.0 API Quick Reference Card (PDF)
-
(2024/11/22 更新)本記事の初版では、シンボルをテクスチャにするやりかたとして
UIImage.pngData
メソッドを使う方法を紹介していました。しかしながら、UIGraphicsImageRenderer
を利用して別のUIImageに書き出した時点でシンボルに設定した色が反映される挙動であったことがわかり、なおかつ、パフォーマンスの面でもこちらのほうが有利だったため、UIImage.pngData
メソッドの記述を削除し、UIGraphicsImageRenderer
を使う内容に変更しました。 ↩︎ -
CGColor.components
プロパティでRGBA値が取得できないパターンとしては、Color.accentColor.cgColor
の戻り値がnil
になる例が挙げられます。この場合、 UIColor.init(_:) でColorからUIColorに変換すると値が取得できるようになります。また、UIColor.white
やUIColor(white: 0.5, alpha: 1.0)
などのグレースケールの色は、componentsの配列が[0.5, 1.0]
のような2要素のみとなり、components![2]
の値を取得しようとするとFatal error: Index out of range
が発生します。 ↩︎ -
(2024/11/22 更新)本記事の初版では、飾り枠を表示するサンプルコードで
UIColor.cgColor.components
プロパティを使用する例を掲載していました。しかしながら、エラーになるケースがある点と、UIColor.getRed
やCIColor
を使う方法よりも処理が重い点を考慮して、CIColor
を使用する例に変更しました。 ↩︎
Discussion