🎨

GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (3) 〜変数〜

2024/06/08に公開

はしがき

SwiftUIでSpriteKitのSKShaderを使って遊んでみようというテーマの記事の3回目です。
前回の記事 では、SKShaderの座標についてまとめました。
今回は 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)で撮影したものをベースにしています。

SKShader の変数

SpriteKit から SKShader へ渡せる変数の種類

SpriteKitでは2種類の変数をシェーダーへ渡すことができます。

種類 概要
uniform変数 シェーダーオブジェクト に対して値を定義する変数です
シェーダー間で同じ値を共有できます
attribute変数 ノードオブジェクト に対して値を定義する変数です
同一のシェーダーオブジェクトへ、ノードごとに異なる値を渡せます

それぞれ解説していきます。

uniform 変数

ひとつのシェーダーオブジェクトを複数のノードへ適用する場合に、どのノードのシェーダーでも共通で使える値を、 uniform変数 としてシェーダーへ渡すことができます。

uniform変数を渡す処理の流れは、

  1. 使いたい 変数名 を指定して SKUniform オブジェクトを作成する
  2. 作成した SKUniform オブジェクトを、 SKShader オブジェクトの addUniform メソッドでシェーダーへ渡す
    または SKShader オブジェクトの uniforms プロパティに配列データで格納する

の2ステップです。これで渡した値がシェーダーで使えるようになります。
変数名は任意の文字列を指定でき、シェーダーのコード内であらためて変数を宣言する必要はありません。

// ノードを作成します
let node = SKSpriteNode(
    color: .clear,
    size: CGSize(width: 250, height: 250)
)

// シェーダーへ渡したいデータでSKUniformオブジェクトを作成します
let position = SKUniform(
    // シェーダー内で使用する変数名を指定します
    name: "u_position",
    // 値を指定します
    vectorFloat2: vector_float2(
        Float(0.65),
        Float(0.65)
    )
)

let radius = SKUniform(
    // シェーダー内で使用する変数名を指定します
    name: "u_radius",
    // 値を指定します
    float: Float(0.2)
)

// シェーダーを作成します
let shader = SKShader(
    source: """
        void main() {
            // 渡されたuniform変数を利用して座標を調整します
            // uniform変数はあらためて宣言をせずに使えます
            vec2 st = (v_tex_coord - u_position) / u_radius;
            // 三日月型を作ります
            float d = step(length(st), 1.0) - step(length(st + vec2(0.4, -0.3)), 1.0);
            // 色を用意します
            vec4 yellow = vec4(0.99, 0.83, 0.46, 1.0);
            // 三日月型を黄色で出力します
            gl_FragColor = mix(vec4(0.0), yellow, d);
        }
    """
)

// 作成したSKUniformオブジェクトを、
// SKShaderオブジェクトのaddUniformメソッドでシェーダーに渡します
shader.addUniform(position)
shader.addUniform(radius)

// SKShaderオブジェクトのuniformsプロパティに、
// 配列データでまとめて格納することもできます
// shader.uniforms = [position, radius]

// SKShaderオブジェクト作成時に渡すこともできます
// let shader = SKShader(
//     source: 〜略〜,
//     uniforms: [position, radius]
// )

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

uniform変数を渡して三日月を描く

uniform変数のサンプルコード全体
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 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 node = SKSpriteNode(
            color: .clear,
            size: CGSize(width: 250, height: 250)
        )

        let position = SKUniform(
            name: "u_position",
            vectorFloat2: vector_float2(
                Float(0.65),
                Float(0.65)
            )
        )

        let radius = SKUniform(
            name: "u_radius",
            float: Float(0.2)
        )

        let shader = SKShader(
            source: """
                void main() {
                    vec2 st = (v_tex_coord - u_position) / u_radius;
                    float d = step(length(st), 1.0) - step(length(st + vec2(0.4, -0.3)), 1.0);
                    vec4 yellow = vec4(0.99, 0.83, 0.46, 1.0);
                    gl_FragColor = mix(vec4(0.0), yellow, d);
                }
            """
        )

        // addUniformメソッドでシェーダーに渡す例
        shader.addUniform(position)
        shader.addUniform(radius)

        // uniformsプロパティに配列データで格納する例
        // shader.uniforms = [position, radius]

        // SKShaderオブジェクト作成時に渡す例
        // let shader = SKShader(
        //     source: 〜略〜,
        //     uniforms: [position, radius]
        // )

        node.shader = shader
        self.addChild(node)
    }
}

渡したuniform変数の値は、 SKUniform オブジェクトのプロパティを更新するかたちで、シェーダー実行中に外部から変更することもできます。

radius.floatValue = Float(0.3)

// または

shader.uniforms.first(
    where: { $0.name == "u_position" }
)?.vectorFloat2Value = vector_float2(
    Float(0.6),
    Float(0.5)
)

ただし、GLSLの標準仕様と同じく、シェーダー側ではuniform変数は読み取り専用なので、シェーダーのコード内で値を変更することはできません。

attribute 変数

ひとつのシェーダーオブジェクトを複数のノードへ適用する場合に、ノードごとに異なる値を attribute変数 として渡して、各ノードのシェーダーを実行することができます。

attribute変数を渡すには、シェーダーオブジェクト側とノードオブジェクト側でそれぞれ準備が必要です。

シェーダー側の準備

シェーダー側でのattribute変数を受け取る準備としては、

  1. 使いたい 変数名変数の型型の詳細については後述)を指定して SKAttribute オブジェクトを作成する
  2. 作成した SKAttribute オブジェクトを、 SKShader オブジェクトの attributes プロパティに配列データで格納する

の2ステップが必要です。変数名はuniform変数と同じく任意の文字列を指定できます。

// シェーダー側で受け取りたい変数の名前と型で、
// SKAttributeオブジェクトを作成します
let positionAttribute = SKAttribute(
    // シェーダー内で使用する変数名を指定します
    name: "a_position",
    // 変数の型を指定します
    type: .vectorFloat2
)
let radiusAttribute = SKAttribute(
    // シェーダー内で使用する変数名を指定します
    name: "a_radius",
    // 変数の型を指定します
    type: .float
)

// シェーダーを作成します
let shader = SKShader(
    source: """
        void main() {
            // 渡されたattribute変数を利用して座標を調整します
            // attribute変数はあらためて宣言をせずに使えます
            vec2 st = (v_tex_coord - a_position) / a_radius;
            // 花型を作ります
            float d = step(length(st), abs(cos(atan(st.y, st.x) * 3.0)));
            // 色を用意します
            vec4 yellow = vec4(0.99, 0.83, 0.46, 1.0);
            // 花型を黄色で出力します
            gl_FragColor = mix(vec4(0.0), yellow, d);
        }
    """
)

// 作成したSKAttributeオブジェクトを、
// SKShaderオブジェクトのattributesプロパティに配列データで格納します
shader.attributes = [positionAttribute, radiusAttribute]

ノード側の準備

ノード側では、シェーダーへ渡したいattribute変数の値を、以下の2ステップで設定します。

  1. 渡したい を指定して SKAttributeValue オブジェクトを作成する
  2. 作成した SKAttributeValue オブジェクトを、紐づけたい 変数名 (シェーダー側の SKAttribute で準備したもの)とあわせて、 SKNode オブジェクト (*)setValue メソッドでノードに設定する
    または SKNode オブジェクトの attributeValues プロパティに [変数名 : SKAttributeValue] という辞書データで格納する
    * SKShaderに対応している種類のSKNode に限ります。
// ノードを作成します
let node1 = SKSpriteNode(
    color: .clear,
    size: CGSize(width: 250, height: 250)
)
let node2 = SKSpriteNode(
    color: .clear,
    size: CGSize(width: 250, height: 250)
)

// シェーダーへ渡したい値でSKAttributeValueオブジェクトを作成します
let positionValue1 = SKAttributeValue(
    vectorFloat2: vector_float2(
        Float(0.8),
        Float(0.8)
    )
)
let radiusValue1 = SKAttributeValue(
    float: Float(0.12)
)

let positionValue2 = SKAttributeValue(
    vectorFloat2: vector_float2(
        Float(0.35),
        Float(0.4)
    )
)
let radiusValue2 = SKAttributeValue(
    float: Float(0.3)
)

// 作成したSKAttributeValueオブジェクトを、
// SKSpriteNodeオブジェクトのsetValueメソッドでノードに設定します
node1.setValue(
    // SKAttributeValueオブジェクトを指定します
    positionValue1,
    // シェーダー側のSKAttributeで準備した変数名を指定します
    forAttribute: "a_position"
)
node1.setValue(
    // SKAttributeValueオブジェクトを指定します
    radiusValue1,
    // シェーダー側のSKAttributeで準備した変数名を指定します
    forAttribute: "a_radius"
)

// SKSpriteNodeオブジェクトのattributeValuesプロパティに
// [シェーダー側のSKAttributeで準備した変数名 : SKAttributeValueオブジェクト]
// という辞書データでまとめて格納することもできます
node2.attributeValues = [
    "a_position": positionValue2,
    "a_radius": radiusValue2
]

// attribute変数を受け取る準備をしたSKShaderオブジェクトを、ノードに適用します
node1.shader = shader
node2.shader = shader

attribute変数を渡して花を描く

このように、同じシェーダーを別々のノードで使いまわしつつ、一部のパラメータをノードによって変えて実行したい、というケースでattribute変数が使えます。

attribute変数のサンプルコード全体
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 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 positionAttribute = SKAttribute(
            name: "a_position",
            type: .vectorFloat2
        )
        let radiusAttribute = SKAttribute(
            name: "a_radius",
            type: .float
        )

        let shader = SKShader(
            source: """
                void main() {
                    vec2 st = (v_tex_coord - a_position) / a_radius;
                    float d = step(length(st), abs(cos(atan(st.y, st.x) * 3.0)));
                    vec4 yellow = vec4(0.99, 0.83, 0.46, 1.0);
                    gl_FragColor = mix(vec4(0.0), yellow, d);
                }
            """
        )

        shader.attributes = [positionAttribute, radiusAttribute]

        // ノード側の準備

        let node1 = SKSpriteNode(
            color: .clear,
            size: CGSize(width: 250, height: 250)
        )
        let node2 = SKSpriteNode(
            color: .clear,
            size: CGSize(width: 250, height: 250)
        )

        let positionValue1 = SKAttributeValue(
            vectorFloat2: vector_float2(
                Float(0.8),
                Float(0.8)
            )
        )
        let radiusValue1 = SKAttributeValue(
            float: Float(0.12)
        )

        let positionValue2 = SKAttributeValue(
            vectorFloat2: vector_float2(
                Float(0.35),
                Float(0.4)
            )
        )
        let radiusValue2 = SKAttributeValue(
            float: Float(0.3)
        )

        // setValueメソッドでノードに設定する例
        node1.setValue(
            positionValue1,
            forAttribute: "a_position"
        )
        node1.setValue(
            radiusValue1,
            forAttribute: "a_radius"
        )

        // attributeValuesプロパティに辞書データで格納する例
        node2.attributeValues = [
            "a_position": positionValue2,
            "a_radius": radiusValue2
        ]

        node1.shader = shader
        node2.shader = shader

        self.addChild(node1)
        self.addChild(node2)
    }
}

渡したattribute変数の値は、ノードオブジェクトの attributeValues プロパティを更新するかたちで、シェーダー実行中に外部から変更することもできます。

node1.attributeValues["a_radius"] = SKAttributeValue(
    float: Float(0.2)
)

// または

node1.setValue(
    SKAttributeValue(
        vectorFloat2: vector_float2(
            Float(0.75),
            Float(0.7)
        )
    ),
    forAttribute: "a_position"
)

uniform変数とは異なり、attribute変数の値はシェーダーのコード内でも変更可能です。

変数の型

ここまでのサンプルコードを読んでお気づきかと思いますが、GLSL(OpenGL ES 2.0)、SKUniform、SKAttributeのそれぞれで、変数の型名や引数ラベルの名称が異なります。
対応を表にまとめると以下のようになります。Metalシェーディング言語(MSL)もあとで話に絡んでくるので列に加えています。

型の種類と対応

GLSL SKUniform / SKAttributeValue
(引数ラベル)
SKUniform / SKAttributeValue
(引数の型)
SKUniformType (*1) SKAttributeType (*2) MSL
float float Float float float / halfFloat float
vec2 vectorFloat2 vector_float2 floatVector2 vectorFloat2 / vectorHalfFloat2 float2
vec3 vectorFloat3 vector_float3 floatVector3 vectorFloat3 / vectorHalfFloat3 float3
vec4 vectorFloat4 vector_float4 floatVector4 vectorFloat4 / vectorHalfFloat4 float4
mat2 matrixFloat2x2 / − (*3) matrix_float2x2 / − (*3) floatMatrix2 (*3) float2x2
mat3 matrixFloat3x3 / − (*3) matrix_float3x3 / − (*3) floatMatrix3 (*3) float3x3
mat4 matrixFloat4x4 / − (*3) matrix_float4x4 / − (*3) floatMatrix4 (*3) float4x4
bool (*4) bool
int (*4) int
bvec2 × (*5) bool2
bvec3 × (*5) bool3
bvec4 × (*5) bool4
ivec2 × (*5) int2
ivec3 × (*5) int3
ivec4 × (*5) int4

*1. SKUniformTypeは、SKUniformオブジェクトの uniformType プロパティで使われる列挙型です。
*2. SKAttributeTypeは、SKAttributeオブジェクトを作成するときの type 引数で指定する列挙型です。
*3. mat は、SKAttributeでは型が指定できないため渡せません。
*4. bool や int は、SKUniform、SKAttribute のどちらも型が指定できないため渡せません。
*5. bvec や ivec は、SKShaderのGLSLでは使えません。

halfがあったりなかったり、matrixがあったりなかったりと、一貫性のない状態です。

boolint は、フラグメントシェーダーでの使用頻度がそもそも高くはなさそうですが、もしサポートされてない型の変数をシェーダーへ渡す必要が生じた際には、 float への置きかえなどの代替手段を検討することになります。
変数として渡すことができないというだけで、どちらも シェーダーのソースコード内ではふつうに使えます

bvecivec は、SKShaderのGLSLがサポートしていないようで シェーダーのソースコード内でも使えません 。他所のGLSLのコードを移植するときは、 boolnintn といった MSLの型がSKShaderで使える ので、そちらで代用できます。

MSLの floatnfloatnxm も動くので、HLSLのコードを移植するときは、これらの型の表記はそのままで大丈夫です(行列は列優先になります)。

今回の主題である変数からは少しそれますが、MSLの型が使えるという話の流れで書きますと、 saturatesinpi といった GLSLにはないけどMSLにはある関数もSKShaderで使えますM_PI_F みたいな定数も使えます(MSLのすべての関数や定数が使えるかどうかは未検証です)。
他方で GLSLにあるけどMSLにはない関数のうちの一部がSKShaderでは使えません 。具体的には radiansdegrees が使えず、 mod は使えるようになっています。

演算精度

halfの話がちらっと出ましたので、演算精度についても少しだけ触れたいと思います。

GLSLには精度修飾子があります。 precision mediump float;highp float xxx = ...; のように書くあれですね。
SKShaderのGLSLでは 精度修飾子は無視されます 。また、 precision宣言があるとエラーになります (ただし #ifdef GL_ES 〜 #endif で囲まれている場合は無視されるのでエラーになりません)。

精度を下げたいときには、MSLにある半精度浮動小数点 half などが使えます。実際の演算精度はGLSLのバージョンやGPUの環境依存が大きいので一概には言えませんが、GLSLとMSLとの精度の対応は、だいたい以下の表のようになります。

GLSL MSL
highp float float
mediump float half
lowp float −(該当なし)
highp int int
mediump int short
lowp int char

ためしに精度による違いが見える簡単なシェーダーを実行させてみます。もとになるシェーダーのソースコードはこちらです。

void main() {
    vec2 st = (v_tex_coord * 2.0 - 1.0) * a_aspect_ratio;
    float l = length(st);
    float m = pow(l, 50.0);  // --- (1) 大きな数でべき乗して桁あふれを発生させる
    l = abs(cos(m));
    vec4 blue = vec4(0.29, 0.59, 0.78, 1.0);
    vec4 yellow = vec4(0.99, 0.83, 0.46, 1.0);
    gl_FragColor = mix(blue, yellow, max(0.0, l));
}

このコードの (1) の行を、

  • highp float m = pow(l, 50.0);
  • mediump float m = pow(l, 50.0);
  • lowp float m = pow(l, 50.0);
  • float m = pow(l, 50.0);
  • half m = pow(l, 50.0);

の5パターンに変えて描画してみます。

演算精度による違い

5つのノードを縦に並べて、それぞれに (1) の箇所を変更したシェーダーを適用したものです。
ご覧のとおり、上から1〜4段目の highp floatmediump floatlowp floatfloat の結果はどれも同じで、最下段の half のみ異なっています。
mediump floatlowp float と指定してコードを書いても、内部で half へ変換されたりはせず、 float として扱われているらしいのがわかります。

まとめ

今回はSKShaderへの変数の渡しかたや、型についてご紹介しました。

シェーダーだけでは生成が難しい高品質な乱数をあらかじめSKScene側で用意して渡したり、 SKAction (アニメーション機能)と組み合わせて渡した変数の値をあとから書きかえて変化を加えたりと、工夫次第で多彩な表現ができるので、いろいろ試して遊んでみるときっと楽しいですよ。

次回は、シェーダーのコード共有サービスでもよく使われる 経過時間の変数マウス座標 の話をしたいと思います。よろしければ引き続きご覧ください。


次記事 → GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (4) 〜経過時間とマウス座標〜

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

参考リンク

Discussion