Metalシェーダー開発
- とにかく綺麗なグラフィックを描画してみたい!
- 数百万単位の大量のパーティクルを描画したい!
- 3Dを描画してみたい!
- オリジナルの画像処理プログラムを書きたい!
- 機械学習モデルをGPUを使って自前で実装してみたい!
- 物理シミュレーションを実装してみたい!
そんな方はぜひMetalでシェーダーを書いてみましょう!
ただ、今回の記事は入門編ということでめちゃめちゃ簡単なところからやっていきます。
今回つくるもの
シェーダーの基礎、三角形を描画していきます。
macOSアプリとして実装していくので、Swiftでのアプリ開発経験があるとこの記事は読みやすいと思います。
コードはiOSの実機でも動きます。(シミュレーターでは動きません)
ちなみに完成コードはたったの50行!
準備
プロジェクトの作成
Xcodeを開いて、Create New Project...をクリックします。
macOSのAppを選択してNext
ProductName、Organization Identifierを適当にいれて、InterfaceはSwiftUI、LanguageはSwiftを選択してNext
適当な保存先を選択してCreate
こんな画面がでてきたらOKです
パッケージの追加
この記事ではMetalシェーダー入門ということで、0からすべてを書くわけではなくEasyMetalShaderという、Metalシェーダーを書く上でめんどくさい部分をすべて裏でやってくれるライブラリを使います。
File > Add Package Dependencies...
右上の検索バーに下のリンクをコピペして検索
https://github.com/yukiny0811/EasyMetalShader
EasyMetalShaderというライブラリがでてきたら、Dependency Ruleのところを"Up to Next Minor Version 3.0.0" < にした上で Add Packageをクリックしましょう。
こんな感じででてきたらAdd Packageで
この状態で一度実行してみると、このようなエラーがでてくると思います。
EasyMetalShaderが信頼できるライブラリかどうかを確認してね、というエラーです。
このライブラリは僕が作っているので大丈夫ですが、気になる方はEasyMetalShaderの元実装を見に行ってください。
このエラーのところをクリックしましょう。
こんな画面がでてくるので、Trust & Enableで。
最後に、Previewは今回使わないので消しましょう。
これで準備OKです!
レンダラーの作成
これからシェーダーを書いていく前に、その書いたシェーダーを画面上に描画するためのレンダラーを作ります。
まず、ライブラリをimportします。
import SwiftUI
import EasyMetalShader //ここ
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
}
}
レンダラークラスを作ります。
このクラスがアニメーションのループなどをすべて裏で管理してくれます。
class MyRenderer: ShaderRenderer {
override func draw(view: MTKView, drawable: CAMetalDrawable) {
//この中身が毎フレーム実行される
}
}
SwiftUIのViewとして表示できるようにします。
ContentViewを書き換えてください。
struct ContentView: View {
let renderer = MyRenderer()
var body: some View {
EasyShaderView(renderer: renderer)
}
}
これで一度実行してみましょう!
こんなかんじの何もない画面が出ればいったんOKです!
シェーダーの作成
ついにシェーダー本体を書いていきます!
シェーダーを定義するクラスの作成
下のコードのように、MyShaderという名前のクラスをつくりましょう!
@EMRenderShader
をつけることによって、面倒なシェーダーの設定をすべて裏でやってくれるようになります。
@EMRenderShader
class MyShader {
// ここにvertexシェーダーを書く
var vertImpl: String {
""
}
// ここにfragmentシェーダーを書く
var fragImpl: String {
""
}
// ここは今は使わない
var customMetalCode: String {
""
}
}
シェーダーパイプラインの作成
MyRendererクラスを編集しましょう。
シェーダーパイプラインについてはいつか話すので、ひとまずコピペしてみましょう。
解説はコード中にコメントとして書いておきます。
class MyRenderer: ShaderRenderer {
let myShader = MyShader(targetPixelFormat: .bgra8Unorm)
override func draw(view: MTKView, drawable: CAMetalDrawable) {
// コマンドバッファの作成
let dispatch = EMMetalDispatch()
// RenderCommandEncoderの作成
// renderTargetTextureは描画先のテクスチャ。今回はViewの画面。
// needsClearは今回はtrueでもfalseでもどっちでも大丈夫です。
dispatch.render(renderTargetTexture: drawable.texture, needsClear: true) { [self] encoder in
// シェーダーを使って実際に描画を予約します。
// 画面上(drawable.texture) に対して、verticesで渡した頂点データをもとに、三角形(.triangle)を描画します。
myShader.dispatch(encoder, textureSizeReference: drawable.texture, primitiveType: .triangle, vertices: [])
}
// 画面への描画を予約する
dispatch.present(drawable: drawable)
// コマンドバッファの送信
dispatch.commit()
}
}
パイプラインはこれで完成です!
コードはこんな感じになっているはずです。
シェーダーの基礎知識
コードの続きを書いていく前に、今回必要なシェーダーについての基礎知識を軽く解説します。
シェーダーにはいろいろと種類があるのですが、今回使うのはvertex shaderと呼ばれるものとfragment shaderと呼ばれるものです。
vertex shaderは、CPUから受け取った図形の頂点データを加工し、fragment shaderに渡す役割を担っています。
vertex shaderからfragment shaderに値が渡される際、頂点データがピクセルのデータに変換されます。(ここらへんは雑に説明してます)
fragment shaderは、この変換された後のピクセルごとに処理を行い、各ピクセルの色を決めるシェーダーです。
難しいと思うので、いったん書いてみましょう。
頂点データを作成する
今回は2Dの平面上に三角形を書いていきますが、そのためにはMetalの画面の座標系を知っておく必要があります。
Metalではデフォルトでこのような座標系になっています。
画面の中心を(0, 0)として、画面右方向にx軸、画面上方向にy軸が伸びている感じです。
このことを踏まえて、記事の最初にも載せたこの三角形を見てみましょう。
座標はこのようになります。
- 左下:(-1, -1)
- 上:(0, 1)
- 右下:(1, -1)
これをもとにコードを書いていきます。
EasyMetalShaderでは、頂点データはVertexInputという型にしてシェーダーにわたす必要があります。
下のコードのようにMyRendererのdraw()の中に書いてみましょう。
class MyRenderer: ShaderRenderer {
let myShader = MyShader(targetPixelFormat: .bgra8Unorm)
override func draw(view: MTKView, drawable: CAMetalDrawable) {
// vvvvvvv 追加
var vertex1 = VertexInput()
vertex1.input0 = simd_float4(-1, -1, 0, 1) //左下
var vertex2 = VertexInput()
vertex2.input0 = simd_float4(0, 1, 0, 1) //上
var vertex3 = VertexInput()
vertex3.input0 = simd_float4(1, -1, 0, 1) //右下
// ^^^^^^^
let dispatch = EMMetalDispatch()
dispatch.render(renderTargetTexture: drawable.texture, needsClear: true) { [self] encoder in
myShader.dispatch(encoder, textureSizeReference: drawable.texture, primitiveType: .triangle, vertices: [])
}
dispatch.present(drawable: drawable)
dispatch.commit()
}
}
上のコードのように、VertexInput型にはinput0~input9まで、それぞれsimd_float4型の値を入れることができます。
simd_float4型は、Floatが4つ入る型という風に考えてもらえれば大丈夫です。
simd_float4(-1, -1, 0, 1)
とすると、x座標が-1、y座標が-1、z座標が0として頂点データを作成することができます。
4つめの1
はいったん気にしないでおきましょう。いつか記事で紹介したいと思います。
次に、作った3つの頂点を、シェーダーに渡します。
コードが横に長くなってしまったので、少し見やすいように整形しています。
class MyRenderer: ShaderRenderer {
let myShader = MyShader(targetPixelFormat: .bgra8Unorm)
override func draw(view: MTKView, drawable: CAMetalDrawable) {
var vertex1 = VertexInput()
vertex1.input0 = simd_float4(-1, -1, 0, 1)
var vertex2 = VertexInput()
vertex2.input0 = simd_float4(0, 1, 0, 1)
var vertex3 = VertexInput()
vertex3.input0 = simd_float4(1, -1, 0, 1)
let dispatch = EMMetalDispatch()
dispatch.render(renderTargetTexture: drawable.texture, needsClear: true) { [self] encoder in
myShader.dispatch(
encoder,
textureSizeReference: drawable.texture,
primitiveType: .triangle,
vertices: [vertex1, vertex2, vertex3] //ここ編集!
)
}
dispatch.present(drawable: drawable)
dispatch.commit()
}
}
vertex shaderを書く
vertex shaderを書いていきます!
長い道のりでしたが、もう少しです!
EasyMetalShaderでは、Metalシェーダーのコードは文字列として書いていきます。
MyShaderクラスのvertImplの中身を編集しましょう。
@EMRenderShader
class MyShader {
var vertImpl: String {
"rd.position = vertexInput.input0;" //ここ!
}
var fragImpl: String {
""
}
var customMetalCode: String {
""
}
}
EasyMetalShaderのvertex shaderには、rd
という変数が事前に定義されています。
このrd
という変数に設定した値がfragment shaderに渡されるので、vertex shaderではこのrd
のパラメーターを設定していくことが主目的です。
今回はrd.position
という値に頂点座標を設定しています。
rd.position
に代入しているvertexInput.input0
という変数も、EasyMetalShaderで事前に定義されている変数です。この中には、先程書いたVertexInputの中の値が入っています。
先程はVertexInput型のinput0というパラメーターに頂点座標を設定したので、vertex shaderの中でもvertexInput.input0という変数の中に頂点データがそのまま入っています。
vertex shaderで三角形(.triangle)を描画するときは、CPUから送られてきた頂点の数だけこのvertex shaderが呼ばれます。
それぞれの頂点に対して1回このvertex shaderのプログラムが実行されます。つまり今回は3回呼ばれています。
fragment shaderを書く
今回は三角形を描画するように設定しているので、vertex shaderから送られてきた3つの頂点データをもとに三角形が形成されます。
fragment shaderでは、この3つの頂点で囲われているすべてのピクセル1つ1つに対して行っていく処理を書きます。
つまり、1つの3角形を描画するたびにこのfragment shaderは数十万回呼ばれます。
下の画像はイメージ図です。
いったん単色で塗りつぶしてみましょう。
MyShaderクラスをこのように編集してみてください。
@EMRenderShader
class MyShader {
var vertImpl: String {
"rd.position = vertexInput.input0;"
}
var fragImpl: String {
"return float4(1, 0, 0, 1);" //ここ!
}
var customMetalCode: String {
""
}
}
fragment shaderでは、色のデータをreturnすることが主目的です。
EasyMetalShaderでは、色のRGBAに対応するfloat4型の値を返す必要があります。
今回はreturn float4(1, 0, 0, 1)
なので、赤色になるようなコードを書いてみます。
ここまで書けたら実行してみましょう!
画面に三角形が描画されました!
グラデーションを書いていく
ここまでできたら、次は記事最初の画像のようなグラデーションを書いてみましょう。
頂点データを書き換える
MyRendererクラスのdraw()の中で定義した、頂点データを編集します。
今までは座標のデータしかいれていなかったので、ここに色のデータを追加しようと思います。
// MyRendererクラス > drawメソッドの中の一部
var vertex1 = VertexInput()
vertex1.input0 = simd_float4(-1, -1, 0, 1)
vertex1.input1 = simd_float4(1, 0, 0, 1) // ここ追加!赤色
var vertex2 = VertexInput()
vertex2.input0 = simd_float4(0, 1, 0, 1)
vertex2.input1 = simd_float4(0, 1, 0, 1) // ここ追加!緑色
var vertex3 = VertexInput()
vertex3.input0 = simd_float4(1, -1, 0, 1)
vertex3.input1 = simd_float4(0, 0, 1, 1) // ここ追加!青色
このように、それぞれの頂点に別々の色を入れてみましょう。
input1に入れてあげます。
vertexシェーダーとfragmentシェーダーを書き換える
@EMRenderShader
class MyShader {
var vertImpl: String {
"rd.position = vertexInput.input0;"
"rd.color = vertexInput.input1;" // ここ追加!
}
var fragImpl: String {
"return rd.color;" // ここ編集!
}
var customMetalCode: String {
""
}
}
vertex shaderで、rd
のcolor
というプロパティに頂点データのinput1
の値を代入します。
fragment shaderでは、この送られてきたrd
のcolor
プロパティをそのままreturn
してあげます。
それでは実行してみましょう!
完成です!
このように、vertex shaderで代入した値は、fragment shaderに届くときには基本的にすべて勝手にグラデーションになるように線形補完されます。
さいごに
今回はシェーダーの基本となる三角形を色付きで描画してみました。
vertexシェーダーとfragmentシェーダーの役割をなんとなく理解していただけたかと思います。
ちなみに、シェーダーで何か作品を作るとなると、作業時間の9割はシェーダーパイプラインの作成に時間を割かれます。
EasyMetalShaderライブラリはこの時間を大幅に短縮してくれるライブラリですので、よければスターをつけていただけると励みになります!
完成コード
たったの50行!
import SwiftUI
import EasyMetalShader
struct ContentView: View {
let renderer = MyRenderer()
var body: some View {
EasyShaderView(renderer: renderer)
}
}
class MyRenderer: ShaderRenderer {
let myShader = MyShader(targetPixelFormat: .bgra8Unorm)
override func draw(view: MTKView, drawable: CAMetalDrawable) {
var vertex1 = VertexInput()
vertex1.input0 = simd_float4(-1, -1, 0, 1)
vertex1.input1 = simd_float4(1, 0, 0, 1)
var vertex2 = VertexInput()
vertex2.input0 = simd_float4(0, 1, 0, 1)
vertex2.input1 = simd_float4(0, 1, 0, 1)
var vertex3 = VertexInput()
vertex3.input0 = simd_float4(1, -1, 0, 1)
vertex3.input1 = simd_float4(0, 0, 1, 1)
let dispatch = EMMetalDispatch()
dispatch.render(renderTargetTexture: drawable.texture, needsClear: true) { [self] encoder in
myShader.dispatch(encoder, textureSizeReference: drawable.texture, primitiveType: .triangle, vertices: [vertex1, vertex2, vertex3])
}
dispatch.present(drawable: drawable)
dispatch.commit()
}
}
@EMRenderShader
class MyShader {
var vertImpl: String {
"rd.position = vertexInput.input0;"
"rd.color = vertexInput.input1;"
}
var fragImpl: String {
"return rd.color;"
}
var customMetalCode: String {
""
}
}
今後書くかもしれないMetal入門記事
- Compute Shaderで発光している感じをアニメーションしてみよう
- 大量のパーティクルを描画してみよう
- 3D空間に描画してみよう
- 四角形を書いてみよう
- 3Dモデルを描画してみよう
- 3Dモデルにテクスチャを貼って描画してみよう
- Compute Shaderで画像処理プログラムを書いてみよう
- インタラクションできるシェーダーを書いてみよう
- Stable Fluidをシェーダーで実装してみよう