GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (3) 〜変数〜
はしがき
SwiftUIでSpriteKitのSKShaderを使って遊んでみようというテーマの記事の3回目です。
前回の記事 では、SKShaderの座標についてまとめました。
今回は SKShaderに変数を渡す方法 を中心に紹介していきます。
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 の変数
SpriteKit から SKShader へ渡せる変数の種類
SpriteKitでは2種類の変数をシェーダーへ渡すことができます。
種類 | 概要 |
---|---|
uniform変数 |
シェーダーオブジェクト に対して値を定義する変数です シェーダー間で同じ値を共有できます |
attribute変数 |
ノードオブジェクト に対して値を定義する変数です 同一のシェーダーオブジェクトへ、ノードごとに異なる値を渡せます |
それぞれ解説していきます。
uniform 変数
ひとつのシェーダーオブジェクトを複数のノードへ適用する場合に、どのノードのシェーダーでも共通で使える値を、 uniform変数 としてシェーダーへ渡すことができます。
uniform変数を渡す処理の流れは、
- 使いたい 変数名 と 値 を指定して SKUniform オブジェクトを作成する
- 作成した
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変数のサンプルコード全体
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変数を受け取る準備としては、
- 使いたい 変数名 と 変数の型 (型の詳細については後述)を指定して SKAttribute オブジェクトを作成する
- 作成した
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ステップで設定します。
- 渡したい 値 を指定して SKAttributeValue オブジェクトを作成する
- 作成した
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変数のサンプルコード全体
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があったりなかったりと、一貫性のない状態です。
bool
や int
は、フラグメントシェーダーでの使用頻度がそもそも高くはなさそうですが、もしサポートされてない型の変数をシェーダーへ渡す必要が生じた際には、 float
への置きかえなどの代替手段を検討することになります。
変数として渡すことができないというだけで、どちらも シェーダーのソースコード内ではふつうに使えます 。
bvec
や ivec
は、SKShaderのGLSLがサポートしていないようで シェーダーのソースコード内でも使えません 。他所のGLSLのコードを移植するときは、 booln
や intn
といった MSLの型がSKShaderで使える ので、そちらで代用できます。
MSLの floatn
や floatnxm
も動くので、HLSLのコードを移植するときは、これらの型の表記はそのままで大丈夫です(行列は列優先になります)。
今回の主題である変数からは少しそれますが、MSLの型が使えるという話の流れで書きますと、 saturate
や sinpi
といった GLSLにはないけどMSLにはある関数もSKShaderで使えます 。 M_PI_F
みたいな定数も使えます(MSLのすべての関数や定数が使えるかどうかは未検証です)。
他方で GLSLにあるけどMSLにはない関数のうちの一部がSKShaderでは使えません 。具体的には radians
や degrees
が使えず、 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 float
、 mediump float
、 lowp float
、 float
の結果はどれも同じで、最下段の half
のみ異なっています。
mediump float
や lowp float
と指定してコードを書いても、内部で half
へ変換されたりはせず、 float
として扱われているらしいのがわかります。
まとめ
今回はSKShaderへの変数の渡しかたや、型についてご紹介しました。
シェーダーだけでは生成が難しい高品質な乱数をあらかじめSKScene側で用意して渡したり、 SKAction (アニメーション機能)と組み合わせて渡した変数の値をあとから書きかえて変化を加えたりと、工夫次第で多彩な表現ができるので、いろいろ試して遊んでみるときっと楽しいですよ。
次回は、シェーダーのコード共有サービスでもよく使われる 経過時間の変数 と マウス座標 の話をしたいと思います。よろしければ引き続きご覧ください。
次記事 → GLSLシェーダーで遊ぼう with SwiftUI × SpriteKit (4) 〜経過時間とマウス座標〜
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
- The Book of Shaders
- OpenGL ES 2.0 API Quick Reference Card (PDF)
- Metal Shading Language Specification (PDF)
Discussion