🎨

GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (5) 〜テクスチャと色 前編〜

2024/06/22に公開

はしがき

SwiftUIでSpriteKitのSKShaderを使って遊んでみようというテーマの記事の5回目です。
前回の記事 では、SKShaderの経過時間の変数とマウス座標の渡しかたについて紹介しました。
今回は テクスチャ をとりあげます。

まず SKSpriteNode にテクスチャを貼る方法と色のプロパティについて説明してから、 SKShader でノードのテクスチャと色を扱う方法を説明していきます。

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

必要な前提知識

この記事では、以下に該当するようなプログラマーを想定読者としています。

  • 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)で撮影したものをベースにしています。

SKSpriteNode のテクスチャと色

SKSpriteNode にテクスチャを貼る(texture プロパティ)

SKSpriteNode にテクスチャを貼るには、 SKTexture オブジェクトを使います。

  1. テクスチャの画像データを用意する
  2. 1の画像データで SKTexture オブジェクトを作成する
  3. 2の SKTexture オブジェクトを SKSpriteNodetexture プロパティに格納する

この手順でノードにテクスチャを表示させることができます。

実際に試してみましょう。今回はテクスチャ用の画像として、シンプルなSVGファイルを用意しました。

yacht.svg
<svg xmlns="http://www.w3.org/2000/svg"
    width="200" height="200"
    viewBox="0 0 200 200">
    <polygon fill="#f4a581" points="4 124, 100 24, 100 124" />
    <polygon fill="#fdda83" points="104 124, 104 54, 170 124" />
    <polygon fill="#84cccc" points="0 128, 200 128, 150 176, 50 176" />
</svg>

このファイルをXcodeプロジェクトのアセットに追加します。

SVGファイルをアセットに追加

画像データを用意したら、それを使って SKTexture オブジェクトを作成してノードに適用します。

ContentView.swift
// 〜略〜

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

        self.addChild(node)
    }
}

SKSpriteNodeにテクスチャを貼る

サンプルコード全体
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 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

        self.addChild(node)
    }
}

別の書きかたとして、 SKSpriteNode オブジェクトを作成する際に SKTexture オブジェクトを引数で渡す方法や、画像ファイルを直接引数で渡す方法もあります。

// ノードオブジェクト作成時にテクスチャオブジェクトを引数で渡すことができます
let node = SKSpriteNode(
    texture: SKTexture(imageNamed: "yacht"),
    size: CGSize(width: 200, height: 200)
)

// または、画像ファイルを直接指定することもできます
// 指定した画像が自動的にSKTextureに変換されて適用されます
let node = SKSpriteNode(
    imageNamed: "yacht"
)
node.size = CGSize(width: 200, height: 200)

SKSpriteNode に色を塗る(color プロパティ)

前回までのサンプルコード中にも何度も出てきていますが、SKSpriteNode に色を塗るには、 color プロパティを使います。

ContentView.swift
// 〜略〜

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)

        // ノードをオレンジ色に塗ります
        node.color = orange

        self.addChild(node)
    }
}

SKSpriteNodeをオレンジ色に塗る

サンプルコード全体
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 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)
        node.color = orange
        self.addChild(node)
    }
}

色とサイズを指定して SKSpriteNode オブジェクトを作成する書きかたもあります。

let node = SKSpriteNode(
    color: UIColor(red: 0.93, green: 0.73, blue: 0.38, alpha: 1.0),
    size: CGSize(width: 200, height: 200)
)

color プロパティのデフォルト値は UIColor.clear なので、色の指定がない場合にはノードは無色透明で表示されます。

SKSpriteNode のテクスチャに色をブレンドする(colorBlendFactor プロパティ)

テクスチャと色を組み合わせてみましょう。
colorBlendFactor プロパティを使うと、ノードに貼ったテクスチャの色と、color プロパティで指定した色をブレンドすることができます。

ContentView.swift
// 〜略〜

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

        self.addChild(node)
    }
}

SKSpriteNodeに貼ったテクスチャにオレンジ色をブレンドする

サンプルコード全体
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 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

        self.addChild(node)
    }
}

SKSpriteNode オブジェクトを作成する際に texturecolorsize の各プロパティの値を引数でまとめて渡す書きかたもあります。

let node = SKSpriteNode(
    texture: SKTexture(imageNamed: "yacht"),
    color: UIColor(red: 0.93, green: 0.73, blue: 0.38, alpha: 1.0),
    size: CGSize(width: 200, height: 200)
)
node.colorBlendFactor = 0.5

colorBlendFactor で指定できる値は 0.0 〜 1.0 で、値を変えると色の混ざり具合が変わります。

colorBlendFactorの値による色の違い

並べて比べてみるとこんな感じです。
colorBlendFactor プロパティのデフォルト値は 0.0 で、テクスチャの画像の色がそのまま表示されます(上段左のNo.1)。値が大きくなるにつれて color プロパティで指定したオレンジ色の混ざり具合が大きくなっていきます。

ノードにテクスチャが貼られていない場合は、 colorBlendFactor プロパティの値は無視されて、 color プロパティで指定した色がそのまま表示されます。

SKShader のテクスチャと色

さて、ここからが本題になります。 SKSpriteNode に適用したテクスチャと色を SKShader で利用する方法です。

SpriteKitが用意しているSKShaderで使える関数と変数 のうち、 SKSpriteNode のテクスチャと色に関係があるものを表にまとめてみます。

変数名 / 関数名 種類 概要
u_texture sampler2D uniform変数 ノードのtextureプロパティに格納されたテクスチャに紐づくサンプラーです。
v_tex_coord vec2 varying変数 テクスチャにアクセスするための座標です。詳細は 第2回の記事 をご参照ください。
v_color_mix vec4 varying変数 ノードのcolorプロパティの色にcolorBlendFactorプロパティの比率が反映された色の値です。
SKDefaultShading() vec4 関数 ノードがデフォルトで表示する色が得られる関数です。

それぞれサンプルコードとスクリーンショットの画像と一緒に紹介していきます。

u_texture 変数と v_tex_coord 変数

u_texture 変数と v_tex_coord 変数を使って、ノードに貼られたテクスチャの色を取得することができます。OpenGL ES 2.0のGLSLと同じく、組み込み関数の texture2D が使えるので、これを利用して texture2D(u_texture, v_tex_coord) と書いて、テクスチャの各座標の色を取得できます。
この関数と変数を使って得られるテクスチャの色の値には、ノードの color プロパティと colorBlendFactor プロパティで指定した色のブレンドは反映されていません。

ContentView.swift
  // 〜略〜

  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() {
+                     // ノードに適用されたテクスチャの色を出力します
+                     gl_FragColor = texture2D(u_texture, v_tex_coord);
+                 }
+             """
+         )

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

          self.addChild(node)
      }
  }

SKShaderでノードのテクスチャの色を出力する

テクスチャを貼っただけのとき と同じ表示になりました。

v_color_mix 変数

v_color_mix 変数では、ノードの color プロパティで指定した色に、 colorBlendFactor プロパティで指定したブレンド比率を反映させた色を取得できます。
テクスチャの色は反映されません。

ContentView.swift
  // 〜略〜

  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() {
-                     // ノードに適用されたテクスチャの色を出力します
-                     gl_FragColor = texture2D(u_texture, v_tex_coord);
+                     // ノードのcolorプロパティの色に
+                     // colorBlendFactorプロパティの比率を反映させた色を出力します
+                     gl_FragColor = v_color_mix;
                  }
              """
          )

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

          self.addChild(node)
      }
  }

SKShaderでノードの色にブレンド比率を反映させた色を出力する

colorBlendFactor プロパティの値が 0.5 になっているぶん、 オレンジ色をそのまま出力したとき よりも、色味が薄くなっていることがわかるでしょうか。この色とテクスチャの色が掛けあわされて ノードがデフォルトで表示する色 になっています。

colorBlendFactor の項目でも書きましたが、ノードにテクスチャが貼られていない場合は、 colorBlendFactor プロパティの値は無視されて、 color プロパティで指定した色がそのまま出力されます。

SKDefaultShading 関数

SKDefaultShading 関数では、ノードがデフォルトで表示する色を取得できます。
つまり、ノードの texturecolorcolorBlendFactor各プロパティの値が反映された色 が戻り値になります。

ContentView.swift
  // 〜略〜

  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() {
-                     // ノードのcolorプロパティの色に
-                     // colorBlendFactorプロパティの比率を反映させた色を出力します
-                     gl_FragColor = v_color_mix;
+                     // ノードがデフォルトで表示する色を出力します
+                     gl_FragColor = SKDefaultShading();
                  }
              """
          )

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

          self.addChild(node)
      }
  }

SKShaderでノードのデフォルトの色を出力する

シェーダーを適用しないとき と同じ表示になりましたね。


今回のサンプルコード全体
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 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() {
                    // ノードに適用されたテクスチャの色を出力したい場合
                    gl_FragColor = texture2D(u_texture, v_tex_coord);

                    // ノードのcolorプロパティの色に
                    // colorBlendFactorプロパティの比率を反映させた色を出力したい場合
                    gl_FragColor = v_color_mix;

                    // ノードがデフォルトで表示する色を出力したい場合
                    gl_FragColor = SKDefaultShading();
                }
            """
        )

        node.shader = shader

        self.addChild(node)
    }
}

まとめ

今回は、SKSpriteNodeとSKShaderでテクスチャと色を扱う方法について説明しました。 [1]

この第5回までの内容で、SKShaderでフラグメントシェーダーを書くにあたっての基本事項はだいぶ把握してもらえたのではないでしょうか。

次回は テクスチャ の後編として、SKShaderを使ったマルチテクスチャの表示など、少し応用的な例をお見せできればと思っています。よろしければ引き続きご覧ください。


次記事 → GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (6) 〜テクスチャと色 後編〜

参考リンク

脚注
  1. ノードの表示に関しては、 alpha プロパティや blendMode プロパティが指定されているとその影響も受けるのですが、説明を複雑にしないために本記事ではalpha関連の話は省略しました。 ↩︎

Discussion