Zenn
🎨

SpriteKitのブレンドモード(alpha & blendMode プロパティとシェーダー)

に公開
1

はしがき

『GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit』シリーズの 第5回の記事 で説明を省略した、ノードの色の透過(アルファブレンド)を中心としたブレンドモードについてと、SKShaderで半透明な色を計算・出力する方法についてまとめたいと思います。

環境

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

  • Xcode 16.2 (16C5032a)
  • iOS Deployment Target 15.0
  • Apple Swift version 6.0.3 (swiftlang-6.0.3.1.10 clang-1600.0.30.1)
  • Swift Playgrounds 4.6.3 (*)
  • macOS Sonoma 14.7.3
  • MacBook Air M2 2022

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

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

ノードの色のブレンドに関連するプロパティ

ノードの色の表示について、背後の色とのブレンド(合成)に関連するプロパティには次のようなものがあります。

プロパティ名 概要
alpha ノードの透明度を指定できます
blendMode ノードの色とノードの背後にある色をどのような方法で合成するか指定できます
このプロパティは SKSpriteNode , SKShapeNode , SKEmitterNode (particleBlendMode) , SKLabelNode SKTileMapNode , SKEffectNode のノードが対応しています
color ノードの色を指定できます
指定した UIColor (または SKColor 。以降、UIColorのみ例として記載)のRGB値とあわせて、A値が合成に影響します
texture ノードに貼るテクスチャを指定できます
テクスチャ画像の色のRGB値とあわせて、A値が合成に影響します
colorBlendFactor テクスチャの色に対して、colorプロパティの色を混ぜる割合を指定できます
colorプロパティで指定したUIColorのA値も混ぜる対象になるため合成に影響します [1]

本セクションでは alphablendMode のプロパティについて説明します。

color , texture , colorBlendFactor の3プロパティの基本については 以前の記事 でまとめていますので、適宜ご参照ください。

alpha プロパティ

ノードの alpha プロパティで、 ノードの透明度 を指定できます。指定した値に応じてノードの色とノードの背後にある色が合成され、透明(0.0)〜半透明〜不透明(1.0)に見える色でノードが表示されます。デフォルト値は 1.0 (不透明)です。

alpha プロパティの値が異なる SKSpriteNode を並べて比べてみましょう。

alphaプロパティごとの表示の違い

上のスクリーンショットでは、白色のシーンに以下のようなノードを配置しています。

<背後にある縦長のノード>

  • color プロパティで 青色 を指定する
    backNode.color = UIColor(red: 0.2, green: 0.5, blue: 1.0, alpha: 1.0)

<前面の横長のノード ×7>

  • color プロパティで オレンジ色 を指定する
    frontNode.color = UIColor(red: 1.0, green: 0.4, blue: 0.1, alpha: 1.0)
  • alpha プロパティで 透明度 を指定する(上側のノードから下側のノードへ徐々に値を小さくする)
    frontNode.alpha = <1.0 〜 0.0>

alpha プロパティの値が小さいほど前面のノードのオレンジ色が薄くなり、背後のノードの青色が透けて見える量が多くなっています。

計算式

出力される色は次の式で計算できます。 [2]

\begin{align} \mathrm{Node'_{\,A}} &= \mathrm{Node_{\,A}} \times \mathrm{Alpha} \\ \mathrm{Node'_{\,RGB}} &= \mathrm{Node_{\,RGB}} \times \mathrm{Node'_{\,A}} \\ \mathrm{Out_{\,RGB}} &= \mathrm{Node'_{\,RGB}} + \mathrm{Back_{\,RGB}} \times (1 - \mathrm{Node'_{\,A}}) \\ \mathrm{Out_{\,A}} &= \mathrm{Node'_{\,A}} + \mathrm{Back_{\,A}} \times (1 - \mathrm{Node'_{\,A}}) \end{align}
\footnotesize \textbf{\textsf{<変数の意味>}} \\[0.6em] \begin{array}{ll} \bullet\,\mathrm{\small Node_{\,RGBA}} & \textsf{: ノードの色のRGBA値です(合成元、Source color)} \\[0.5em] & \enspace\textsf{ここでの『ノードの色』とは、} \\[0.3em] & \enspace\texttt{color, texture, colorBlendFactor} \\[0.3em] & \enspace\textsf{の3プロパティを合わせた色のことです} \\[0.6em] \bullet\,\mathrm{\small Alpha} & \textsf{: ノードの\texttt{alpha}プロパティの値です} \\[0.6em] \bullet\,\mathrm{\small Back_{\,RGBA}} & \textsf{: ノードの背後の色のRGBA値です(合成先、Destination color)} \\[0.6em] \bullet\,\mathrm{\small Out_{\,RGBA}} & \textsf{: 出力される色のRGBA値です} \end{array}

(1)(2) の式であらかじめノードの色のRGB値にalpha値を掛けてから、 (3) の式で背後にある色と合成する計算を行っています。このように、合成元となる色のRGB値とalpha値を先に掛け算しておく方式を 乗算済みアルファ(Premultiplied alpha) と呼びます。

先ほどのスクリーンショットにある alpha = 0.7 の例でRGBA値を計算してみると、次のようになります。 [3]

RGBAの種類 前面の
ノードの色
背後の
ノードの色
計算
\textcolor{red}{\mathrm{Red}} \colorbox{#ff6619}{\textcolor{white}{1.0}} \colorbox{#337fff}{\textcolor{white}{0.2}}
\begin{gather} \colorbox{#ff6619}{\textcolor{white}{1.0}} \times \colorbox{#dcdcdc}{\textcolor{black}{0.7}} = 0.7 \qquad\tag{2} \\[0.32em] 0.7 + \colorbox{#337fff}{\textcolor{white}{0.2}} \times (1.0 - \colorbox{#dcdcdc}{\textcolor{black}{0.7}}) = \colorbox{#c16d5e}{\textcolor{white}{0.76}} \qquad\tag{3} \end{gather}
\textcolor{green}{\mathrm{Green}} \colorbox{#ff6619}{\textcolor{white}{0.4}} \colorbox{#337fff}{\textcolor{white}{0.5}}
\begin{gather} \colorbox{#ff6619}{\textcolor{white}{0.4}} \times \colorbox{#dcdcdc}{\textcolor{black}{0.7}} = 0.28 \qquad\tag{2} \\[0.32em] 0.28 + \colorbox{#337fff}{\textcolor{white}{0.5}} \times (1.0 - \colorbox{#dcdcdc}{\textcolor{black}{0.7}}) = \colorbox{#c16d5e}{\textcolor{white}{0.43}} \qquad\tag{3} \end{gather}
\textcolor{blue}{\mathrm{Blue}} \colorbox{#ff6619}{\textcolor{white}{0.1}} \colorbox{#337fff}{\textcolor{white}{1.0}}
\begin{gather} \colorbox{#ff6619}{\textcolor{white}{0.1}} \times \colorbox{#dcdcdc}{\textcolor{black}{0.7}} = 0.07 \qquad\tag{2} \\[0.32em] 0.07 + \colorbox{#337fff}{\textcolor{white}{1.0}} \times (1.0 - \colorbox{#dcdcdc}{\textcolor{black}{0.7}}) = \colorbox{#c16d5e}{\textcolor{white}{0.37}} \qquad\tag{3} \end{gather}
\textcolor{dimgray}{\mathrm{Alpha}} \colorbox{#ff6619}{\textcolor{white}{1.0}} \colorbox{#337fff}{\textcolor{white}{1.0}}
\begin{gather} \colorbox{#ff6619}{\textcolor{white}{1.0}} \times \colorbox{#f2f2f2}{\textcolor{black}{0.7}} = \colorbox{#dcdcdc}{\textcolor{black}{0.7}} \qquad\tag{1} \\[0.32em] \colorbox{#dcdcdc}{\textcolor{black}{0.7}} + \colorbox{#337fff}{\textcolor{white}{1.0}} \times (1.0 - \colorbox{#dcdcdc}{\textcolor{black}{0.7}}) = \colorbox{#c16d5e}{\textcolor{white}{1.0}} \qquad\tag{4} \end{gather}

計算の内容を簡単にまとめると、オレンジ色の70%分と青色の30%分を合計した色がノードに表示される色になっています。

サンプルコード

alphaプロパティのサンプルコード
ContentView.swift
import SwiftUI
import SpriteKit

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

    var body: some View {
        SpriteView(scene: self.currentScene)
            .ignoresSafeArea()
            .statusBarHidden()
    }
}

class MySKScene: SKScene {
    override func didMove(to view: SKView) {
        self.backgroundColor = .white
        self.anchorPoint = CGPoint(x: 0.5, y: 1.0)

        // 透明度の値の配列を用意します
        let alphaValues = [1.0, 0.9, 0.7, 0.5, 0.3, 0.1, 0.0]

        // 青色のノードを作成して配置します
        let backNode = {
            let node = SKSpriteNode()
            node.size = CGSize(
                width: self.frame.width * 0.86,
                height: self.frame.height
            )
            node.anchorPoint = CGPoint(x: 0.5, y: 1.0)
            node.color = UIColor(red: 0.2, green: 0.5, blue: 1.0, alpha: 1.0)
            return node
        }()
        self.addChild(backNode)

        // オレンジ色のノードの基本形(テンプレ用)を作成します
        let frontNode = {
            let node = SKSpriteNode()
            node.size = CGSize(
                width: self.frame.width,
                height: self.frame.height * 0.9 / CGFloat(alphaValues.count)
            )
            node.anchorPoint = CGPoint(x: 0.5, y: 1.0)
            node.position.y = self.frame.height * -0.05
            node.color = UIColor(red: 1.0, green: 0.4, blue: 0.1, alpha: 1.0)
            return node
        }()

        // オレンジ色のノードを、配列で用意した透明度ごとに作成し
        // 上から下へ順番に並べて配置します
        for (i, alphaValue) in alphaValues.enumerated() {
            let node = frontNode.copy() as! SKSpriteNode
            node.position.y -= node.frame.height * CGFloat(i)

            // ノードの透明度を指定します
            node.alpha = alphaValue

            self.addChild(node)
        }
    }
}

alpha プロパティの子ノードへの伝播

alpha プロパティは、親ノードに指定した値が 子ノードにも伝播して適用されます 。計算上は、子ノードの alpha プロパティの値に親ノードの alpha プロパティの値が掛け算されることになります。

たとえば、親ノード、子ノード、孫ノードを配置して、それぞれの alpha プロパティを 0.80.60.7 と指定したとします。

// 親ノード
let parentNode = SKSpriteNode(color: .white, size: self.frame.size)
parentNode.alpha = 0.8
self.addChild(parentNode)

// 子ノード
let childNode = SKSpriteNode(color: .white, size: self.frame.size)
childNode.alpha = 0.6
parentNode.addChild(childNode)

// 孫ノード
let grandchildNode = SKSpriteNode(color: .white, size: self.frame.size)
grandchildNode.alpha = 0.7
childNode.addChild(grandchildNode)

この場合、各ノードの色と背後にある色との合成で使われる \mathrm{\small Alpha} の値は、

ノード alpha
プロパティ
ノードの色と背後にある色との
合成で使われる \textrm{\textbf{\small Alpha}} の値
親ノード 0.8 0.8
子ノード 0.6 0.6 \times 0.8 = 0.48
孫ノード 0.7 0.7 \times 0.6 \times 0.8 = 0.336

になります。

blendMode プロパティ

ノードの blendMode プロパティで、 ノードの色とノードの背後にある色との混ぜかた を指定できます。
プロパティに指定できる SKBlendMode は全部で8種類あります。デフォルト値は SKBlendMode.alpha です。

種類 概要
alpha ノードの色と背後の色をノードのalpha値に基づいて足します
背後の色が透けているような色になります
add 背後の色にノードの色を足します
subtract 背後の色からノードの色を引きます
multiply ノードの色と背後の色を掛けます
もとの色と同じか、より暗い色になります
multiplyX2 ノードの色と背後の色を掛けて、それを2倍します
screen ノードの色の反転色(1−ノードの色)と背後の色を掛けて、その色をノードの色と足します
multiplyと対になるモードで、もとの色と同じか、より明るい色になります
replace ノードの色を表示します
alpha値が1.0未満であっても背後の色は混ざりません
multiplyAlpha multiplyとalphaを組み合わせたモードです
ノードの色と背後の色を掛けて(multiply)、その色と背後の色をノードのalpha値に基づいて足します(alpha)

blendMode プロパティの値が異なる SKSpriteNode を並べて比べてみましょう。

alpha = 1.0 alpha = 0.4
blendModeプロパティごとの表示の違い(alpha=1.0) blendModeプロパティごとの表示の違い(alpha=0.4)

上のスクリーンショットでは、白色のシーンに以下のようなノードを配置しています。

<背後にある縦長のノード>

  • color プロパティで 青色 を指定する
    backNode.color = UIColor(red: 0.2, green: 0.5, blue: 1.0, alpha: 1.0)

<前面の横長のノード ×8>

  • color プロパティで オレンジ色 を指定する
    frontNode.color = UIColor(red: 1.0, green: 0.4, blue: 0.1, alpha: 1.0)
  • blendMode プロパティで 合成方法 を指定する
    frontNode.blendMode = <各ブレンドモード>
  • alpha プロパティで 透明度 を指定する
    frontNode.alpha = 1.0 (左側のスクリーンショット)
    frontNode.alpha = 0.4 (右側のスクリーンショット)

計算式

出力される色はそれぞれ次の式で計算できます。 [2:1]

\begin{align*} \mathrm{Node'_{\,A}} &= \mathrm{Node_{\,A}} \times \mathrm{Alpha} \\ \mathrm{Node'_{\,RGB}} &= \mathrm{Node_{\,RGB}} \times \mathrm{Node'_{\,A}} \\ \mathrm{Out_{\,RGB}} &= \textrm{\footnotesize ※下記の表を参照} \\ \mathrm{Out_{\,A}} &= \textrm{\footnotesize ※下記の表を参照} \end{align*}
種類 ※計算式
alpha \\[0.3em] \begin{aligned} \mathrm{Out_{\,RGB}} &= \mathrm{Node'_{\,RGB}} + \mathrm{Back_{\,RGB}} \times (1 - \mathrm{Node'_{\,A}}) \\ \mathrm{Out_{\,A}} &= \mathrm{Node'_{\,A}} + \mathrm{Back_{\,A}} \times (1 - \mathrm{Node'_{\,A}}) \end{aligned}
add \\[0.3em] \begin{aligned} \mathrm{Out_{\,RGB}} &= \min(1, \mathrm{Node'_{\,RGB}} + \mathrm{Back_{\,RGB}}) \\ \mathrm{Out_{\,A}} &= \min(1, \mathrm{Node'_{\,A}} + \mathrm{Back_{\,A}}) \end{aligned} \\[0.3em]
※加算後のRGBA値が1を上回った場合は1に切り捨てられます
subtract \\[0.3em] \begin{aligned} \mathrm{Out_{\,RGB}} &= \max(0, \mathrm{Back_{\,RGB}} - \mathrm{Node'_{\,RGB}} \times \mathrm{Node'_{\,A}}) \\ \mathrm{Out_{\,A}} &= \mathrm{Back_{\,A}} \end{aligned} \\[0.3em]
※引く側のノードのRGB値は 乗算済みアルファでalpha値を掛けた上に、さらにalpha値を掛けたもの になります
※減算後のRGB値が0を下回った場合は0に切り上げられます
multiply \\[0.3em] \begin{aligned} \mathrm{Out_{\,RGB}} &= \mathrm{Node'_{\,RGB}} \times \mathrm{Back_{\,RGB}} \\ \mathrm{Out_{\,A}} &= \mathrm{Node'_{\,A}} \end{aligned}
multiplyX2 \\[0.3em] \begin{aligned} \mathrm{Out_{\,RGB}} &= \min(1, \mathrm{Node'_{\,RGB}} \times \mathrm{Back_{\,RGB}} \times 2) \\ \mathrm{Out_{\,A}} &= \mathrm{Node'_{\,A}} \end{aligned} \\[0.3em]
※乗算後のRGB値が1を上回った場合は1に切り捨てられます
screen \\[0.3em] \begin{aligned} \mathrm{Out_{\,RGB}} &= (1 - \mathrm{Node'_{\,RGB}}) \times \mathrm{Back_{\,RGB}} + \mathrm{Node'_{\,RGB}} \\ \mathrm{Out_{\,A}} &= \mathrm{Node'_{\,A}} + \mathrm{Back_{\,A}} \times (1 - \mathrm{Node'_{\,A}}) \end{aligned}
replace \\[0.3em] \begin{aligned} \mathrm{Out_{\,RGB}} &= \mathrm{Node'_{\,RGB}} \\ \mathrm{Out_{\,A}} &= \mathrm{Node'_{\,A}} \end{aligned}
multiplyAlpha \\[0.3em] \begin{aligned} \mathrm{Out_{\,RGB}} &= \mathrm{Node'_{\,RGB}} \times \mathrm{Back_{\,RGB}} + \mathrm{Back_{\,RGB}} \times (1 - \mathrm{Node'_{\,A}}) \\ \mathrm{Out_{\,A}} &= \mathrm{Node'_{\,A}} + \mathrm{Back_{\,A}} \times (1 - \mathrm{Node'_{\,A}}) \end{aligned}

サンプルコード

blendModeプロパティのサンプルコード
ContentView.swift
import SwiftUI
import SpriteKit

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

    var body: some View {
        SpriteView(scene: self.currentScene)
            .ignoresSafeArea()
            .statusBarHidden()
    }
}

class MySKScene: SKScene {
    override func didMove(to view: SKView) {
        self.backgroundColor = .white
        self.anchorPoint = CGPoint(x: 0.5, y: 1.0)

        // ブレンドモードの値の配列を用意します
        let blendModeValues: [SKBlendMode] = [
            .alpha,
            .add,
            .subtract,
            .multiply,
            .multiplyX2,
            .screen,
            .replace,
            .multiplyAlpha
        ]

        // 青色のノードを作成して配置します
        let backNode = {
            let node = SKSpriteNode()
            node.size = CGSize(
                width: self.frame.width * 0.86,
                height: self.frame.height
            )
            node.anchorPoint = CGPoint(x: 0.5, y: 1.0)
            node.color = UIColor(red: 0.2, green: 0.5, blue: 1.0, alpha: 1.0)
            return node
        }()
        self.addChild(backNode)

        // オレンジ色のノードの基本形(テンプレ用)を作成します
        let frontNode = {
            let node = SKSpriteNode()
            node.size = CGSize(
                width: self.frame.width,
                height: self.frame.height * 0.9 / CGFloat(blendModeValues.count)
            )
            node.anchorPoint = CGPoint(x: 0.5, y: 1.0)
            node.position.y = self.frame.height * -0.05
            node.color = UIColor(red: 1.0, green: 0.4, blue: 0.1, alpha: 1.0)

            // ノードの透明度を指定します
            node.alpha = 0.4

            return node
        }()

        // オレンジ色のノードを、配列で用意したブレンドモードごとに作成し
        // 上から下へ順番に並べて配置します
        for (i, blendModeValue) in blendModeValues.enumerated() {
            let node = frontNode.copy() as! SKSpriteNode
            node.position.y -= node.frame.height * CGFloat(i)

            // ノードのブレンドモードを指定します
            node.blendMode = blendModeValue

            self.addChild(node)
        }
    }
}

シェーダーで半透明な色を出力

gl_FragColor に格納する色

ノードにシェーダーを適用して半透明な色を出力したいとき、 gl_FragColor.a に格納する 透明度の値だけを指定しても期待する色にならない ということを意識しておきましょう。透明度だけでなく gl_FragColor.rgb も前述の 乗算済みアルファ 方式の値にして格納する必要があります。

これもスクリーンショットを並べて比べてみます。

1. プロパティで色を指定
(期待する表示)
2. シェーダーで色を出力
(乗算済みアルファにしない)
3. シェーダーで色を出力
(乗算済みアルファにする)
プロパティで色と透明度を指定 シェーダーで色を出力(乗算済みアルファの計算なし) シェーダーで色を出力(乗算済みアルファの計算あり)

上のスクリーンショットでは、白色のシーンに以下のようなノードを配置しています。

<背後のノード>

  • color プロパティで 青色 を指定する
    backNode.color = UIColor(red: 0.2, green: 0.5, blue: 1.0, alpha: 1.0)

<前面のノード>

  • 3つのスクリーンショットの左から順に、次の方法で 透明度0.7のオレンジ色 を表示する
    1. ノードのプロパティでcolorとalphaの値を指定(期待する表示)
      frontNode.color = UIColor(red: 1.0, green: 0.4, blue: 0.1, alpha: 1.0)
      frontNode.alpha = 0.7
    2. シェーダーで色を出力(期待する表示と異なる例)
    • オレンジ色のRGB値のままで、A値のみ透明度の0.7にする
      gl_FragColor = vec4(1.0, 0.4, 0.1, 0.7);
    1. シェーダーで色を出力(期待する表示と同じになる例)
    • オレンジ色のRGB値に透明度の0.7を掛けて 乗算済みアルファ 方式の値(red: 0.7, green: 0.28, blue: 0.07, alpha: 0.7)にする
      gl_FragColor = vec4(1.0, 0.4, 0.1, 1.0) * 0.7;

gl_FragColor 変数に格納する色を乗算済みアルファにしなかった2番目のスクリーンショットでは、背後の色と合成した結果、

\begin{align*} \mathrm{Out_{\,RGB}} &= [\, \colorbox{#ff6619}{\textcolor{white}{1.0, 0.4, 0.1}} \,] + [\, \colorbox{#337fff}{\textcolor{white}{0.2, 0.5, 1.0}} \,] \times (1 - 0.7) \\ &= [\, \colorbox{#ff8c66}{\textcolor{white}{1.0, 0.55, 0.4}} \,] \end{align*}

の色が表示されており、期待する表示である

\begin{align*} \mathrm{Out_{\,RGB}} &= [\, \colorbox{#ff6619}{\textcolor{white}{1.0, 0.4, 0.1}} \,] \times 0.7 + [\, \colorbox{#337fff}{\textcolor{white}{0.2, 0.5, 1.0}} \,] \times (1 - 0.7) \\ &= [\, \colorbox{#c16d5e}{\textcolor{white}{0.76, 0.43, 0.37}} \,] \end{align*}

の色よりも明るいオレンジ色になっています。 [3:1]

サンプルコード

シェーダーで半透明な色を出力するサンプルコード
ContentView.swift
import SwiftUI
import SpriteKit

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

    var body: some View {
        SpriteView(scene: self.currentScene)
            .ignoresSafeArea()
            .statusBarHidden()
    }
}

class MySKScene: SKScene {
    override func didMove(to view: SKView) {
        self.backgroundColor = .white
        self.anchorPoint = CGPoint(x: 0.5, y: 0.5)

        // 青色のノードを作成して配置します
        let backNode = {
            let node = SKSpriteNode()
            node.size = CGSize(
                width: self.frame.width * 0.86,
                height: self.frame.height
            )
            node.color = UIColor(red: 0.2, green: 0.5, blue: 1.0, alpha: 1.0)
            return node
        }()
        self.addChild(backNode)

        // オレンジ色のノードを作成して配置します
        let frontNode = {
            let node = SKSpriteNode()
            node.size = CGSize(
                width: self.frame.width,
                height: self.frame.height * 0.8
            )

            // 透明度0.7のオレンジ色をシェーダーで出力します
            node.shader = SKShader(source: """
                void main() {
                    vec4 orange = vec4(1.0, 0.4, 0.1, 1.0);
                    float alpha = 0.7;
                    gl_FragColor = orange * alpha;
                }
            """)

            return node
        }()
        self.addChild(frontNode)
    }
}

ノードの色の取得に関連する組み込み変数と関数

ノードの色が取得できる SKShaderの組み込み変数と関数 が3種類あります。

変数名 / 関数名 概要
u_texture ノードのtextureプロパティに格納されたテクスチャに紐づくサンプラーです
v_color_mix ノードのcolorプロパティの色にcolorBlendFactorプロパティの比率が反映された色の値です
alphaプロパティの指定がある場合はその値も反映されます
SKDefaultShading() ノードがデフォルトで表示する色が得られる関数です

これらの変数および関数の基本は 以前の記事 でも説明していますので、適宜ご参照ください。

本記事では、取得できる色の値と透明度について説明します。

u_texture 変数

u_texture 変数を介してノードに貼られたテクスチャの色を取得することができます。
この色の値はノードの alpha プロパティの影響を受けません。 テクスチャデータのRGBA値がそのまま返ってきます

画像データに半透明な色が含まれている場合は、 SKTexture(imageNamed:)SKTexture(image:) のようなイニシャライザでSKTextureオブジェクトを作成した時点で 自動的に乗算済みアルファになる ので、 u_texture で取得できる色も乗算済みアルファになっています。 [4]

v_color_mix 変数

v_color_mix 変数では、ノードの color プロパティで指定した色に、 colorBlendFactor プロパティで指定したブレンド比率を反映させた色を取得できます。
この色は、RGB値にalpha値を掛けた 乗算済みアルファ 方式の値になっています

計算式

v_color_mix で取得できる色がどのように計算されているのかを、 color , alpha , colorBlendFactor のプロパティを順番に追加しながら見てみます。 [2:2]

ノードにcolorプロパティのみ指定した場合

ノードの color プロパティで半透明な色を指定したときは、次のような計算の結果が v_color_mix の値になります。
※ 計算式中の \mathrm{\small Color_{\,RGBA}} はノードの color プロパティで指定したUIColorのRGBA値です。

\begin{align} \mathrm{Color'_{\,A}} &= \colorbox{#f2f2f2}{Color\raisebox{-0.23em}{\scriptstyle \,A}} \notag \\ \mathrm{Color'_{\,RGB}} &= \colorbox{#f2f2f2}{Color\raisebox{-0.23em}{\scriptstyle \,RGB}} \times \mathrm{Color'_{\,A}} \\ \mathrm{v\_color\_mix} &= \mathrm{Color'_{\,RGBA}} \notag \end{align}

(5) の式で乗算済みアルファの計算が行われて、 color プロパティで指定したUIColorのA値が、RGB値に反映されます。

ノードにcolorプロパティとalphaプロパティを指定した場合

color プロパティに加えて alpha プロパティも指定したときは、次のような計算の結果が v_color_mix の値になります。
※ 計算式中の \mathrm{\small Alpha} はノードの alpha プロパティの値です。親ノードからの alpha プロパティの伝播を反映した状態です。

\begin{align} \mathrm{Color'_{\,A}} &= \mathrm{Color_{\,A}} \times \colorbox{#f2f2f2}{Alpha} \\ \mathrm{Color'_{\,RGB}} &= \mathrm{Color_{\,RGB}} \times \mathrm{Color'_{\,A}} \notag \\ \mathrm{v\_color\_mix} &= \mathrm{Color'_{\,RGBA}} \notag \end{align}

(6) の式で alpha プロパティの値が反映され、そのalpha値を使ってRGB値の乗算済みアルファの計算が行われます。

ノードにcolorプロパティ、alphaプロパティ、colorBlendFactorプロパティを指定した場合

さらに、ノードにテクスチャを貼った上で colorBlendFactor プロパティを指定すると、次のような計算の結果が v_color_mix の値になります。
※ノードにテクスチャを貼っていない場合は colorBlendFactor プロパティで指定した値は無視されます。
※ 計算式中の \mathrm{\small Factor} はノードの colorBlendFactor プロパティの値です。

\begin{align} \mathrm{Color'_{\,RGBA}} &= \mathrm{Color_{\,RGBA}} \times \colorbox{#f2f2f2}{Factor} + (1 - \colorbox{#f2f2f2}{Factor}) \\ \mathrm{Color''_{\,A}} &= \mathrm{Color'_{\,A}} \times \mathrm{Alpha} \notag \\ \mathrm{Color''_{\,RGB}} &= \mathrm{Color'_{\,RGB}} \times \mathrm{Color''_{\,A}} \notag \\ \mathrm{v\_color\_mix} &= \mathrm{Color''_{\,RGBA}} \notag \end{align}

(7) の式で colorBlendFactor プロパティの値を使って 1.0RGBA値 とのあいだでの線形補間が行われます。そして線形補間後のRGBA値にalpha値を掛けて、乗算済みアルファの計算をします。

具体的な値で確認してみましょう。ノードに以下のプロパティを指定する例で考えます。

frontNode.texture = <任意のSKTextureオブジェクト>
frontNode.color = UIColor(red: 1.0, green: 0.4, blue: 0.1, alpha: 0.9)
frontNode.colorBlendFactor = 0.4
frontNode.alpha = 0.7
frontNode.blendMode = .replace

v_color_mixの色を表示

blendMode プロパティで .replace を指定しているので、前面のノードの半透明な色が、背後の緑色とは合成されずにそのまま表示されている状態です。

各プロパティの値をさきほどの計算式に当てはめると、次のようになります。 [3:2]

\begin{align*} \mathrm{Color_{\,RGBA}} &= [\, \colorbox{#ff6619}{\textcolor{white}{1.0, 0.4, 0.1, 0.9}} \,] \\ \mathrm{Alpha} &= 0.7 \\ \mathrm{Factor} &= 0.4 \\[0.6em] \mathrm{Color'_{\,RGBA}} &= [\, \colorbox{#ff6619}{\textcolor{white}{1.0, 0.4, 0.1, 0.9}} \,] \times 0.4 + (1 - 0.4) \\ &= [\, \colorbox{#ffc1a3}{\textcolor{black}{1.0, 0.76, 0.64, 0.96}} \,] \\ \mathrm{Color''_{\,A}} &= \colorbox{#ffc1a3}{\textcolor{black}{0.96}} \times 0.7 \\ &= 0.672 \\ \mathrm{Color''_{\,RGB}} &= [\, \colorbox{#ffc1a3}{\textcolor{black}{1.0, 0.76, 0.64}} \,] \times 0.672 \\ &= [\, \colorbox{#ab826d}{\textcolor{white}{0.672, 0.51072, 0.43008}} \,] \\ \mathrm{v\_color\_mix} &= [\, \colorbox{#ab826d}{\textcolor{white}{0.672, 0.51072, 0.43008, 0.672}} \,] \end{align*}

サンプルコード

v_color_mixの色を出力するサンプルコード
ContentView.swift
import SwiftUI
import SpriteKit

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

    var body: some View {
        SpriteView(scene: self.currentScene)
            .ignoresSafeArea()
            .statusBarHidden()
    }
}

class MySKScene: SKScene {
    override func didMove(to view: SKView) {
        self.backgroundColor = .white
        self.anchorPoint = CGPoint(x: 0.5, y: 0.5)

        // 緑色のノードを作成して配置します
        let backNode = {
            let node = SKSpriteNode()
            node.size = CGSize(
                width: self.frame.width * 0.86,
                height: self.frame.height
            )
            node.color = UIColor(red: 0.6, green: 0.8, blue: 0.4, alpha: 1.0)
            return node
        }()
        self.addChild(backNode)

        // オレンジ色のノードを作成して配置します
        let frontNode = {
            let node = SKSpriteNode()
            node.size = CGSize(
                width: self.frame.width,
                height: self.frame.height * 0.8
            )
            // 白色のデータオブジェクトから
            // テクスチャオブジェクトを作成してノードに適用します
            node.texture = SKTexture(
                data: Data([UInt8](repeating: 255, count: 4)),
                size: CGSize(width: 1, height: 1)
            )
            node.color = UIColor(red: 1.0, green: 0.4, blue: 0.1, alpha: 0.9)
            node.colorBlendFactor = 0.4
            node.alpha = 0.7
            node.blendMode = .replace

            // v_color_mixの色をシェーダーで出力します
            node.shader = SKShader(source: """
                void main() {
                    gl_FragColor = v_color_mix;
                }
            """)

            return node
        }()
        self.addChild(frontNode)
    }
}

SKDefaultShading 関数

u_texture 変数を介して取得したテクスチャの色に v_color_mix 変数で取得した色を掛けると、 texture , color , colorBlendFactor , alpha の4プロパティを合わせた色が算出できます。
SKShaderの組み込み関数 SKDefaultShading() で取得できるのがこの値で、ノードがデフォルトで表示する色になります。

取得したノードの色を使った計算

SKDefaultShading() などの組み込み関数や変数で取得した色をもとにシェーダーでRGB値を計算するときも、 乗算済みアルファ 方式の値になっていること考慮しないと目的の色を作れません。

ノードの色を反転させる例で比べてみましょう。

1. CIColorInvertフィルタで
色を反転
(期待する表示)
2. シェーダーで色を反転
(乗算済みアルファのまま
計算)
3. シェーダーで色を反転
(ストレートアルファ [5]
してから計算)
CIColorInvertフィルタで色を反転 シェーダーで色を反転(乗算済みアルファのまま計算) シェーダーで色を反転(ストレートアルファにしてから計算)

上のスクリーンショットでは、白色のシーンに以下のようなノードを配置しています。

<背後のノード>

  • color プロパティで 緑色 を指定する
    backNode.color = UIColor(red: 0.6, green: 0.8, blue: 0.4, alpha: 1.0)

<前面のノード>

  • texture プロパティで 透明度 0.8の灰色のテクスチャ を指定する
    frontNode.texture = <UIColor(white: 0.6, alpha: 0.8) の色で塗りつぶしたSKTextureオブジェクト>
  • color プロパティで 透明度 0.9のオレンジ色 を指定する
    frontNode.color = UIColor(red: 1.0, green: 0.4, blue: 0.1, alpha: 0.9)
  • colorBlendFactor プロパティで テクスチャの色にオレンジ色を0.4の割合で混ぜる
    frontNode.colorBlendFactor = 0.4
  • alpha プロパティで 透明度 0.7 を指定する
    frontNode.alpha = 0.7
  • 3つのスクリーンショットの左から順に、次の方法でノードの色を反転する
    1. CIFilterCIColorInvert フィルタで色を反転(期待する表示)
    • 前面のノードの親ノードとして SKEffectNode を配置し、 filter プロパティでフィルタを適用する
      effectNode.addChild(frontNode)
      effectNode.filter = CIFilter(name: "CIColorInvert")
    1. シェーダーで色を出力(期待する表示と異なる例)
    • 乗算済みアルファのRGB値のままで反転色を計算する
      gl_FragColor = vec4(1.0 - SKDefaultShading().rgb, 1.0) * SKDefaultShading().a;
    1. シェーダーで色を出力(期待する表示と同じになる例)
    • 乗算済みアルファのRGB値をA値で割り、ストレートアルファ [5:1] の値にしてから反転色を計算する
      gl_FragColor = vec4(1.0 - SKDefaultShading().rgb / SKDefaultShading().a, 1.0) * SKDefaultShading().a;
          = vec4(SKDefaultShading().a - SKDefaultShading().rgb, SKDefaultShading().a);

乗算済みアルファのRGB値のままで反転色を計算した2番目のスクリーンショットでは、背後の色と合成した結果、

\begin{align*} \mathrm{texture} &= [\, \colorbox{#7a7a7a}{\textcolor{white}{0.48, 0.48, 0.48, 0.8}} \,] \\ \mathrm{v\_color\_mix} &= [\, \colorbox{#ab826d}{\textcolor{white}{0.672, 0.51072, 0.43008, 0.672}} \,] \\ \mathrm{SKDefaultShading()} &= \mathrm{texture} \times \mathrm{v\_color\_mix} \\ &= [\, \colorbox{#523e34}{\textcolor{white}{0.32256, 0.2451456, 0.2064384, 0.5376}} \,] \\ \mathrm{gl\_FragColor}_\mathrm{\,RGB} &= ( 1.0 - [\, \colorbox{#523e34}{\textcolor{white}{0.32256, 0.2451456, 0.2064384}} \,] ) \times \colorbox{#523e34}{\textcolor{white}{0.5376}} \\ &= [\, \colorbox{#5c676c}{\textcolor{white}{0.364191744, 0.40580972544, 0.42661871616}} \,] \\ \mathrm{gl\_FragColor}_\mathrm{\,A} &= \colorbox{#5c676c}{\textcolor{white}{0.5376}} \\ \mathrm{Back_{\,RGB}} &= [\, \colorbox{#99cc66}{\textcolor{black}{0.6, 0.8, 0.4}} \,] \\ \mathrm{Out_{\,RGB}} &= [\, \colorbox{#5c676c}{\textcolor{white}{0.364191744, 0.40580972544, 0.42661871616}} \,] \\ & \hspace{1.5em} + [\, \colorbox{#99cc66}{\textcolor{black}{0.6, 0.8, 0.4}} \,] \times ( 1 - \colorbox{#5c676c}{\textcolor{white}{0.5376}} ) \\ &= [\, \colorbox{#a3c59b}{\textcolor{black}{0.641631744, 0.77572972544, 0.61157871616}} \,] \end{align*}

の色が表示されており、期待する表示である

\begin{align*} \mathrm{texture} &= [\, \colorbox{#7a7a7a}{\textcolor{white}{0.48, 0.48, 0.48, 0.8}} \,] \\ \mathrm{v\_color\_mix} &= [\, \colorbox{#ab826d}{\textcolor{white}{0.672, 0.51072, 0.43008, 0.672}} \,] \\ \mathrm{SKDefaultShading()} &= \mathrm{texture} \times \mathrm{v\_color\_mix} \\ &= [\, \colorbox{#523e34}{\textcolor{white}{0.32256, 0.2451456, 0.2064384, 0.5376}} \,] \\[0.2em] \mathrm{gl\_FragColor}_\mathrm{\,RGB} &= \Bigg( 1.0 - \frac{[\, \colorbox{#523e34}{\textcolor{white}{0.32256, 0.2451456, 0.2064384}} \,]}{\colorbox{#523e34}{\textcolor{white}{0.5376}}} \Bigg) \times \colorbox{#523e34}{\textcolor{white}{0.5376}} \\[1.0em] &= [\, \colorbox{#364a54}{\textcolor{white}{0.21504, 0.2924544, 0.3311616}} \,] \\ \mathrm{gl\_FragColor}_\mathrm{\,A} &= \colorbox{#364a54}{\textcolor{white}{0.5376}} \\ \mathrm{Back_{\,RGB}} &= [\, \colorbox{#99cc66}{\textcolor{black}{0.6, 0.8, 0.4}} \,] \\ \mathrm{Out_{\,RGB}} &= [\, \colorbox{#364a54}{\textcolor{white}{0.21504, 0.2924544, 0.3311616}} \,] \\ & \hspace{1.5em} + [\, \colorbox{#99cc66}{\textcolor{black}{0.6, 0.8, 0.4}} \,] \times ( 1 - \colorbox{#364a54}{\textcolor{white}{0.5376}} ) \\ &= [\, \colorbox{#7da883}{\textcolor{white}{0.49248, 0.6623744, 0.5161216}} \,] \end{align*}

の色よりも淡くなっています。 [3:3]

サンプルコード

SKDefaultShading関数を使うサンプルコード
ContentView.swift
import SwiftUI
import SpriteKit

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

    var body: some View {
        SpriteView(scene: self.currentScene)
            .ignoresSafeArea()
            .statusBarHidden()
    }
}

class MySKScene: SKScene {
    override func didMove(to view: SKView) {
        self.backgroundColor = .white
        self.anchorPoint = CGPoint(x: 0.5, y: 0.5)

        // 緑色のノードを作成して配置します
        let backNode = {
            let node = SKSpriteNode()
            node.size = CGSize(
                width: self.frame.width * 0.86,
                height: self.frame.height
            )
            node.color = UIColor(red: 0.6, green: 0.8, blue: 0.4, alpha: 1.0)
            return node
        }()
        self.addChild(backNode)

        // オレンジ色のノードを作成して配置します
        let frontNode = {
            let node = SKSpriteNode()
            node.size = CGSize(
                width: self.frame.width,
                height: self.frame.height * 0.8
            )
            // 透明度0.8の60%グレーで塗りつぶしたUIImageオブジェクトから
            // テクスチャオブジェクトを作成してノードに適用します
            node.texture = SKTexture(
                image: UIGraphicsImageRenderer(
                    size: node.size
                ).image { context in
                    context.cgContext.setFillColor(
                        UIColor(white: 0.6, alpha: 0.8).cgColor
                    )
                    context.fill(CGRect(origin: .zero, size: node.size))
                }
            )
            node.color = UIColor(red: 1.0, green: 0.4, blue: 0.1, alpha: 0.9)
            node.colorBlendFactor = 0.4
            node.alpha = 0.7

            // ノードの色の反対色をシェーダーで出力します
            node.shader = SKShader(source: """
                void main() {
                    vec3 premultiplied = SKDefaultShading().rgb;
                    float alpha = SKDefaultShading().a;
                    vec3 unpremultiplied = premultiplied / alpha;
                    vec3 inverted = vec3(1.0 - unpremultiplied);
                    gl_FragColor = vec4(inverted, 1.0) * alpha;
                }
            """)

            return node
        }()
        self.addChild(frontNode)
    }
}

参考リンク

脚注
  1. colorプロパティの公式ドキュメントでは テクスチャが貼られているときはalphaプロパティは無視される と記載されています。しかし筆者の環境では、colorBlendFactorプロパティの指定に従ってUIColorのA値がブレンドされており、alphaプロパティの指定も反映されていました。 ↩︎

  2. 計算式は、Appleの公式ドキュメント、一般的なブレンディングの計算方法、SpriteKitのヘッダファイルのコメントや実動作、Metalワークロードなどから推測したものであり、フレームワークの実際の処理とは異なる場合があります。また、説明をわかりやすくシンプルにするため計算のわけかたや順序を調整しています。 ↩︎ ↩︎ ↩︎

  3. 演算誤差により、理論上の値と実際の値が完全には一致しないことがあります。 ↩︎ ↩︎ ↩︎ ↩︎

  4. SKTexture(data:size:) などでRAWピクセルデータからSKTextureオブジェクトを作成する場合は、公式ドキュメントにも記載されているとおり、あらかじめ乗算済みアルファにしたRGBA値でピクセルデータを用意しないといけません。 ↩︎

  5. RGB値にalpha値を事前に掛け算しておく乗算済みアルファとは逆に、事前に乗算しない方式を ストレートアルファ と呼びます。 ↩︎ ↩︎

1

Discussion

ログインするとコメントできます