Help us understand the problem. What is going on with this article?

MetalシェーダでSceneKitのマテリアルを描画する

More than 1 year has passed since last update.

SCNProgramというクラスを使って、SceneKitのマテリアルとかジオメトリの描画をMetalシェーダで行うことができます。

let program = SCNProgram()
program.fragmentFunctionName = "myFragment"
program.vertexFunctionName = "myVertex"
material.program = program

この program というプロパティは SCNShadable というプロトコルに用意されていて、そのプロトコルにSCNMaterialやSCNGeometryが適合しています。

SCNMaterial
open class SCNMaterial : NSObject, SCNAnimatable, SCNShadable, NSCopying, NSSecureCoding {

このへんはなかなか情報がないのですが、以下の記事が参考になりました。

最終的にはAppleの公式ドキュメントが頼りになります。

本記事では主にマテリアルの描画をMetalシェーダで行う場合のアレコレについて書きます。

基本的な実装:テクスチャ描画をMetalで行う

画像ファイルから読み込んだテクスチャをMetalで描画する場合、Metalシェーダ、SceneKitコードを以下のように実装します。

Metalシェーダ側

Shaders.metal
#include <metal_stdlib>
using namespace metal;

#include <SceneKit/scn_metal>

struct MyNodeBuffer {
    float4x4 modelViewProjectionTransform;
};

struct MyVertexInput {
    float3 position [[attribute(SCNVertexSemanticPosition)]];
    float2 texCoords [[attribute(SCNVertexSemanticTexcoord0)]];
};

struct SimpleVertex
{
    float4 position [[position]];
    float2 texCoords;
};

vertex SimpleVertex myVertex(MyVertexInput in [[stage_in]],
                             constant SCNSceneBuffer& scn_frame [[buffer(0)]],
                             constant MyNodeBuffer& scn_node [[buffer(1)]])
{
    SimpleVertex vert;
    vert.position = scn_node.modelViewProjectionTransform * float4(in.position, 1.0);
    vert.texCoords = in.texCoords;

    return vert;
}


fragment half4 myFragment(SimpleVertex in [[stage_in]],
                          texture2d<float, access::sample> diffuseTexture [[texture(0)]])
{
    constexpr sampler sampler2d(coord::normalized, filter::linear, address::repeat);
    float4 color = diffuseTexture.sample(sampler2d, in.texCoords);
    return half4(color);
}

通常のMetalシェーダ実装との違いとしては、

  • #include <SceneKit/scn_metal> が必要
  • 頂点の属性は、[[attribute(SCNVertexSemanticPosition)]][[attribute(SCNVertexSemanticTexcoord0)]] 等、必要なものを構造体で持っておく
  • modelViewProjectionTransformとかの変換行列は、あらかじめ用意されたものがあるので、必要なものをピックアップして構造体を定義する。このデータは [[buffer(1)]] に渡されてくる
Available_Fields_for_Per-Node_Shader_Data
float4x4 modelTransform;
float4x4 inverseModelTransform;
float4x4 modelViewTransform;
float4x4 inverseModelViewTransform;
float4x4 normalTransform; // Inverse transpose of modelViewTransform
float4x4 modelViewProjectionTransform;
float4x4 inverseModelViewProjectionTransform;
float2x3 boundingBox;
float2x3 worldBoundingBox;

といったあたりです。

参考記事に示したStackOverflowの回答ほぼそのまんまですが、これでSCNMaterialのテクスチャ描画がMetalによって行われるようになります。個人的な用途としてはフラグメントシェーダをカスタムしたかったので、頂点シェーダの実装はこれをそのまま使いまわしています。

SceneKitコード側

SomeClass.swift
let program = SCNProgram()
program.fragmentFunctionName = "myFragment"
program.vertexFunctionName = "myVertex"
material.program = program

let image = UIImage(named: filename)!
let imageProperty = SCNMaterialProperty(contents: image)
material.setValue(imageProperty, forKey: "diffuseTexture")

こちらはものすごくシンプルです。フラグメントシェーダにテクスチャを渡すために、

  • SCNMaterialPropertyオブジェクトとして用意
  • SCNMaterialの"diffuseTexture"にセット

しています。こうすることで、シェーダ側の引数 diffuseTexture [[texture(0)]] にテクスチャデータが渡されます。キー名/引数名は一致していれば何でもokです。

SceneKitコードからMetalシェーダに任意の値を渡す

浮動小数点数を渡す

たとえばフラグメントシェーダの引数にFloat型の「経過時間」を渡したい場合、次のようにします。

Metalシェーダ側

Shaders.metal
fragment half4 myFragment(SimpleVertex in [[stage_in]],
                          constant float &time [[buffer(0)]])

SceneKitコード側

SomeClass.swift
private lazy var startTime = Date()
SomeClass.swift
Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true, block: { (timer) in
    self.updateTime()
})
SomeClass.swift
private func updateTime() {
    var time = Float(Date().timeIntervalSince(startTime))
    let timeData = Data(bytes: &time, count: MemoryLayout<Float>.size)
    node.geometry?.firstMaterial?.setValue(timeData, forKey: "time")
}

テクスチャを渡したときとほぼ一緒ですが、今回は Float の値を渡したいので、以下の点が違います。

  • SCNMaterialProperty ではなく Data で渡す
  • [[texture(0)]] ではなく [[buffer(0)]] で受け取る

構造体を渡す

上の例では Float 型の値をMetalシェーダに渡したわけですが、たとえば Uniforms という構造体を次のように定義して、これをシェーダに渡したいとします。

struct Uniforms {
    var resolution: float2
}

やることはほぼ同じです。

SomeClasss.swift
let screenSize = UIScreen.main.nativeBounds.size
var uniforms = Uniforms(resolution: float2(Float(screenSize.width), Float(screenSize.height)))
let uniformsData = Data(bytes: &uniforms, count: MemoryLayout<Uniforms>.size)
node.geometry?.firstMaterial?.setValue(uniformsData, forKey: "uniforms")

Metalシェーダ側では Uniforms &uniforms [[buffer(0)]] という引数で値を受け取れます。

配列を渡す

例えばLineという線分の描画情報(始点・終点etc)の入った構造体を配列でMetalシェーダに渡したいとします。

SceneKitコード側

ここでのポイントは、バッファサイズを handleBindingOfBufferNamed:frequency:usingBlock: で渡してやること。こうしないと、GPUで処理するときにバッファサイズが合わないっていう実行時エラーになります。

SomeClass.mm
std::vector<Line> lines = // 略;
NSData *linesData = [Utils dataFromLines:lines];
NSUInteger numLines = lines.count;
NSData *numLinesData = [NSData dataWithBytes:&numLines length:sizeof(NSUInteger)];
[program handleBindingOfBufferNamed:@"lines" frequency:SCNBufferFrequencyPerFrame usingBlock:^(id<SCNBufferStream>  _Nonnull buffer, SCNNode * _Nonnull node, id<SCNShadable>  _Nonnull shadable, SCNRenderer * _Nonnull renderer) {
    [buffer writeBytes:(void *)linesData.bytes length:linesData.length];
}];
Utils.mm
+ (NSData *)dataFromLines:(std::vector<Line>)lines
{
    NSMutableData *data = [NSMutableData data];
    for (int i=0; i<lines.size(); i++) {
        Line line = lines[i];
        NSData *lineData = [NSData dataWithBytes:&line length:sizeof(Line)];
        [data appendData:lineData];
    }
    return data;
}

この実装をしていたときC++のコードとの連携の関係で、ここだけObjective-C++ですが、まぁポイントはSwiftでも同様です。

このAPIはググってもAppleのリファレンスしか出てこなくて、本当に情報がなかったです...

Metalシェーダ側

Shaders.metal
fragment half4 myFragment(SimpleVertex in [[stage_in]],
                                 const device Line *lines [[buffer(0)]],
                                 constant uint &numLines [[buffer(1)]])

要素数も別引数で受け取る必要がある、という点に注意。

透過を有効にする

シェーダでアルファを指定したり、discard_fragment してもデフォルトでは透過にならないので、

program.isOpaque = false;

をやっておきます。

作例

SceneKitでSCNBoxをジオメトリとするノードを置いたサンプルをベースに、

none.gif

GLSL Sandboxを参考にMetalシェーダを書いてみます。

参考: [iOS] Metalシェーダことはじめ - WebGL/GLSLの豊富なサンプルを参考にする - Qiita

color.gif

voronoi.gif

※実際の開発でこういうシェーダを利用したかったのではなく、円とか線を3D空間上に描画するのにSceneKit+Metalを利用しました。

shu223
フリーランスiOSエンジニア 著書:『iOS×BLE Core Bluetooth プログラミング』『Metal入門』『実践ARKit』『Depth in Depth』『iOSアプリ開発 達人のレシピ100』他 GitHubの累計スター数24,000超
http://shu223.hatenablog.com/
engineerlife
技術力をベースに人生を謳歌する人たちのコミュニティです。
https://community.camp-fire.jp/projects/view/280040
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away