main() blog

プログラムやゲーム、旅、愛する家族について綴っていきます。

【Metal】mtlppで始めるC++でMetalプログラミング入門

f:id:takezoh_1127:20210111122322p:plain

はじめに

MacのLow Level Graphics APIのMetalをC++で開発したい。
MetalはObjective-CもしくはSwiftでしか使用することができない。
MetalをC++でも使用できる様にするためのラッパークラスがGitHubにあるので、それを利用してC++の環境でMetalで開発してみる。

動作環境

macOS Big Sur 11.1
Xcode 12.3

サンプルコード

ここでのソース等は以下のGitHubで公開している。

github.com

セットアップ

空のプロジェクトの作成方法

Xcodeを起動して起動画面の "Create a new Xcode project" を選択する。
f:id:takezoh_1127:20210111114917p:plain

プロジェクトのテンプレートの選択で "Application" の "Command Line Tool" を選択する。
f:id:takezoh_1127:20210111115054p:plain

オプションの選択でプロジェクト名を入力。
言語はC++を選択。
TeamやOrganization Identifer等は適宜設定。
f:id:takezoh_1127:20210111115415p:plain

プロジェクトを保存するフォルダを選択して作成。
空のプロジェクトが作成される。
f:id:takezoh_1127:20210111115528p:plain

フレームワーク、ライブラリの設定

TARGETSの[General]もしくは[Build Phases]のタブを選択。
f:id:takezoh_1127:20210111115722p:plain f:id:takezoh_1127:20210111115736p:plain

その中の[General]の場合は Framework and Libraries、[Build Phases]の場合は Link Binary With Libraries の項目に以下のフレームワークを追加する。

Cocoa.framework
Metal.framework
MetalKit.framework

f:id:takezoh_1127:20210111115807p:plain f:id:takezoh_1127:20210111115822p:plain

mtlppをプロジェクトに追加

GitHubのmtlppからソースをクローン、もしくはダウンロードしてくる。

github.com

ダウンロードして解凍すると以下様なファイルがある。
f:id:takezoh_1127:20210111120012p:plain

元のソースはsrc以下にあるが、ビルドするとmtlpp.hpp、mtlpp.mmというファイルにまとめられる。
まとめられたファイルも含まれているのでこのファイルを使用する。

www.main-function.com

mtlppのmtlpp.hppとmtlpp.mmをプロジェクトに追加する。
同様にmtlppのサンプルプログラムに含まれているwindow.hppとwindow.mmもそのままプロジェクトに追加する。
f:id:takezoh_1127:20210111120128p:plain f:id:takezoh_1127:20210111120208p:plain

ARCの設定

mtlppはARC非対応となっているためにそのままビルドするとビルドエラーになってしまう。
f:id:takezoh_1127:20210111120326p:plain

[Build Phases]の Compile Sources のCompiler Flagsに以下のオプションを追加する。

-fno-objc-arc

f:id:takezoh_1127:20210111120419p:plain f:id:takezoh_1127:20210111120455p:plain

ビルドするとエラーが解消される。
f:id:takezoh_1127:20210111120541p:plain

ウィンドウの表示

#include "mtlpp.hpp"
#include "window.hpp"

mtlpp::Device g_device;

/**
   描画用のコールバック関数.
 */
void Render(const Window& window)
{
}

int main(int argc, const char * argv[])
{
    // デバイスの生成.
    g_device = mtlpp::Device::CreateSystemDefaultDevice();
    assert(g_device);
    
    // ウィンドウの生成.
    Window window(g_device, &Render, 320, 240);
    
    // 実行.
    Window::Run();
    
    return 0;
}

空のウィンドウが表示されることを確認。
f:id:takezoh_1127:20210111120759p:plain

Metalのデバイスの生成にはmtlpp::Device::CreateSystemDefaultDevice()を使用して生成する。

mtlppのサンプルプログラムにあるWindowクラスでウィンドウを生成することができる。
コンストラクタには生成したMetalのデバイスとフレーム毎に呼ばれるコールバック関数、画面サイズを指定指定生成。
Window::Run()で実行され、コールバック関数が呼ばれる様になる。

三角形の描画

コマンドキューの生成

コマンドバッファの実行順を管理するためのキュー。
基本的にはアプリで一つ生成しておく。

 // コマンドキューの生成.
    g_commandQueue = g_device.NewCommandQueue();
    assert(g_commandQueue);

シェーダーの初期化

シェーダーファイル(.metal)ではなく直接ソースにシェーダーを記述してみる。
バイス生成後にシェーダーの初期化を行う。

 // シェーダーを記述.
    // Rは生文字列リテラル.
    const char shaderSrc[] = R"""(
      #include <metal_stdlib>
      using namespace metal;
  
      // 頂点シェーダー.
      vertex float4 vertFunc(const device packed_float3* vertexArray [[buffer(0)]], unsigned int vID [[vertex_id]])
      {
          return float4(vertexArray[vID], 1.0);
      }

      // フラグメントシェーダー.
      fragment half4 fragFunc()
      {
          return half4(1.0, 0.0, 0.0, 1.0);
      }
  )""";
    
    // シェーダーの初期化.
    mtlpp::Library library = g_device.NewLibrary(shaderSrc, mtlpp::CompileOptions(), nullptr);
    assert(library);
    
    mtlpp::Function vertFunc = library.NewFunction("vertFunc");
    assert(vertFunc.GetPtr());
    
    mtlpp::Function fragFunc = library.NewFunction("fragFunc");
    assert(fragFunc.GetPtr());

MSLについては後で説明。

www.main-function.com

とりあえず受け取った頂点バッファをそのままフラグメントシェーダーに渡す頂点シェーダーと、赤のみを出力するフラグメントシェーダーを用意。

mtlp::DeviceのNewLibrary()でシェーダーを記述した文字列を渡すことでシェーダーを扱うmtlpp::Libraryのインスタンスを生成。

NewFunction()で頂点シェーダー、フラグメントシェーダーそれぞれの関数を取得。

頂点バッファの生成

 // 頂点バッファの生成.
    {
        const float vertexData[] =
        {
            0.0f, 1.0f, 0.0f,
            -1.0f, -1.0f, 0.0f,
            1.0f, -1.0f, 0.0f,
        };
    
        g_vertexBuffer = g_device.NewBuffer(vertexData, sizeof(vertexData), mtlpp::ResourceOptions::CpuCacheModeDefaultCache);
        assert(g_vertexBuffer.GetPtr());
    }

今回は座標のみ。
スクリーン座標は中心が(0.0,0.0)で上下左右にそれぞれ-1.0〜1.0の範囲なのでそれぞれの頂点は以下の様になる。

0.0f, 1.0f, 0.0f,   // 上の座標.
-1.0f, -1.0f, 0.0f, // 左下の座標.
1.0f, 1.0f, 0.0f,   // 右下の座標.

mtlpp::DeviceのNewBuffer()で頂点バッファとして生成。

レンダーパイプラインの生成

 // レンダーパイプラインの生成.
    {
        mtlpp::RenderPipelineDescriptor desc;
    
        desc.SetVertexFunction(vertFunc);
        desc.SetFragmentFunction(fragFunc);
        
        auto attach = desc.GetColorAttachments();
        attach[0].SetPixelFormat(mtlpp::PixelFormat::BGRA8Unorm);
    
        g_renderPipelineState = g_device.NewRenderPipelineState(desc, nullptr);
        assert(g_renderPipelineState.GetPtr());
    }

レンダーコマンドのエンコード

void Render(const Window& window)
{
    mtlpp::CommandBuffer commandBuffer = g_commandQueue.CommandBuffer();
    assert(commandBuffer.GetPtr());
    
    mtlpp::RenderPassDescriptor renderPassDesc = window.GetRenderPassDescriptor();
    
    if(renderPassDesc)
    {
        mtlpp::RenderCommandEncoder encoder = commandBuffer.RenderCommandEncoder(renderPassDesc);
        assert(encoder.GetPtr());
        
        encoder.SetRenderPipelineState(g_renderPipelineState);
        encoder.SetVertexBuffer(g_vertexBuffer, 0, 0);
        encoder.Draw(mtlpp::PrimitiveType::Triangle, 0, 3);
        encoder.EndEncoding();
        
        commandBuffer.Present(window.GetDrawable());
    }
    
    commandBuffer.Commit();
    commandBuffer.WaitUntilCompleted();
}

コマンドキューからコマンドバッファを生成。
作成されたコマンドバッファからレンダーコマンドエンコーダーを生成。
エンコーダーに対してさまざまな設定を行う。
レンダーパイプラインステートを設定、頂点バッファを渡して、描画関数を登録。
コマンドのエンコードの完了、表示するドローアブルの登録、コマンドバッファのコミット。

実行

画面全体に赤の三角形が描画。

f:id:takezoh_1127:20210111121147p:plain

Metalシェーダーファイル

Metalのシェーダーを記述するためのファイルを追加。 Choose a template for your new file でMetal File を選択。

f:id:takezoh_1127:20210111233858p:plain

ファイル名を指定してファイルを保存する場所を選択。 プロジェクトに*.metalファイルが追加される。

f:id:takezoh_1127:20210111233941p:plain

先程のシェーダーのプログラムをmetalファイルに記述する。

#include <metal_stdlib>
using namespace metal;

// 頂点シェーダー.
vertex float4 vertFunc(const device packed_float3* vertexArray [[buffer(0)]], unsigned int vID [[vertex_id]])
{
    return float4(vertexArray[vID], 1.0);
}

// フラグメントシェーダー.
fragment half4 fragFunc()
{
    return half4(1.0, 0.0, 0.0, 1.0);
}

シェーダーの初期化

シェーダーの初期化はNewDefaultLibrary()で行う。

 // シェーダーの初期化.
    // プロジェクトに組み込んでビルドした場合はデフォルトで取得できるようになる.
    mtlpp::Library library = g_device.NewDefaultLibrary();
    assert(library);
    
    mtlpp::Function vertFunc = library.NewFunction("vertFunc");
    assert(vertFunc);
    
    mtlpp::Function fragFunc = library.NewFunction("fragFunc");
    assert(fragFunc);

後は先ほどと同じコードで三角形が描画される。

Metalライブラリファイル

MSL(Metal Sharding Language)の .metal ファイルをコマンドラインでビルドして .metallib を作成。

developer.apple.com

metallibのビルド方法

ターミナルなどから以下のコマンドを実行する。
.metalをコンパイルして中間フォーマットの .air を作成。

$ xcrun -sdk macosx metal basic.metal -c -o basic.air

.metallibをビルドする. 複数の .air をまとめてビルドすることも可能。
その際、関数名が重複してしまうとビルドエラーになってしまうので注意が必要。

$ xcrun -sdk macosx metallib basic.air basic2.air -o basic.metallib

ワーキングディレクトリの設定

プロジェクトファイル直下にあるリソースを読み込むためにワーキングディレクトリの設定を行う。
メインメニューの [Product] -> [Scheme] -> [Edit Scheme] の Run の Optionタブの Working Directory の Use custom working directory: にチェックを入れる。
項目に $(SRCROOT) と入力。

シェーダーの初期化

NewLibrary()でパスを指定して生成。

 // シェーダーの初期化.
    // パス指定だとMetalのライブラリで勝手に読み込んです初期化される.
    // 上位で読み込んでバッファを渡して初期化は newLibraryWithData を使うとできそうだがmtlppではそれを使ったAPIが用意されていないので拡張する必要が出てくる.
    mtlpp::Library library = g_device.NewLibrary("shader/basic.metallib", nullptr);
    assert(library);
    
    mtlpp::Function vertFunc = library.NewFunction("vertFunc");
    assert(vertFunc);
    
    mtlpp::Function fragFunc = library.NewFunction("fragFunc");
    assert(fragFunc);

他は先ほどと同じコードで三角形が描画される。

テクスチャの読み込み

MTKTextureLoaderのラッパークラス

テクスチャを適用した矩形の描画を行う。
mtlppにはテクスチャを読み込むためのAPIが用意されていない。
MetalKitのMTKTextureLoaderを使用してと外部ファイルのテクスチャを読み込めるようにする。
mtlppに倣ってMTKTextureLoaderのラッパークラスを以下のファイルで定義。

mtlpp/textureloader.hpp
mtlpp/textureloader.mm

簡易的に対応するためにパス指定で読み込めるnewTextureWithContentsOfURLを使用して実装。

#include "mtlpp.hpp"

namespace mtlpp
{

/** MTKTextureLoaderのラッパー.
   mtlppには用意されていないようなので自前で用意.
   namespaceは統一するためにmtlppにしておく.
   
   https://developer.apple.com/documentation/metalkit/mtktextureloader?language=objc
   
   PNG、JPEG、TIFFなどのフォーマットの読み込み.
   KTX、PVRファイル、アセットカタログ、Core Graphics画像、その他のソースから画像データを読み込むことも可能.
   画像データから出力テクスチャ形式とピクセル形式を推測.
   オプションを指定して、画像の読み込みとテクスチャの作成プロセスを変更できる.
*/
class TextureLoader : public ns::Object
{
public:
    TextureLoader(const Device device);
    
    Texture NewTextureWithPath(const ns::String& filePath, ns::Error* error = nullptr);
};

}
#import <MetalKit/MetalKit.h>

#include "mtlpp.hpp"
#include "textureloader.hpp"

namespace mtlpp
{

/**
*/
TextureLoader::TextureLoader(const Device device)
 : ns::Object(ns::Handle{ (__bridge void*)[[MTKTextureLoader alloc] initWithDevice:(__bridge id<MTLDevice>)device.GetPtr()]})
{
}

/**
*/
Texture TextureLoader::NewTextureWithPath(const ns::String& filePath, ns::Error* error)
{
    Validate();
    
    // sRGB?一部のフォーマットで読み込んだテクスチャのRGBがおかしくなるケースがあるのでSRGBをNOにしてみる.
    NSDictionary *textureLoaderOptions =
    @{
      MTKTextureLoaderOptionTextureUsage       : @(MTLTextureUsageShaderRead),
      MTKTextureLoaderOptionTextureStorageMode : @(MTLStorageModePrivate),
      MTKTextureLoaderOptionSRGB               : @(NO)
      };

    NSString* path = (__bridge NSString*)filePath.GetPtr();
    NSURL* url = [NSURL fileURLWithPath:path];
    
    // Error
    NSError* nsError = NULL;
    NSError** nsErrorPtr = error ? &nsError : nullptr;
    
    id<MTLTexture> texture;
        
    texture = [(__bridge MTKTextureLoader*)m_ptr newTextureWithContentsOfURL:url options:textureLoaderOptions error:nsErrorPtr];
    
    return ns::Handle{ (__bridge void*)texture };
}

}

C++側での読み込み

C++側では以下の様にしてテクスチャを読み込むことができる。

mtlpp::Texture g_texture;

func()
{
    // テクスチャの読み込み.
    {
        mtlpp::TextureLoader textureLoader(g_device);
        assert(textureLoader);
        
        mtlpp::Texture texture = textureLoader.NewTextureWithPath("data/brickwall.jpg");
        assert(texture);
        
        g_texture = texture;
    }
}

テクスチャ付きの2Dの矩形の描画

シェーダー

テクスチャを描画するためのシェーダーを用意。
頂点シェーダーに渡す頂点データの定義にテクスチャ座標を追加。
フラグメントシェーダに渡す値にもテクスチャ座標を追加。

#include <metal_stdlib>
using namespace metal;

// 頂点定義.
typedef struct
{
    float3 position [[attribute(0)]];
    float2 texCoord [[attribute(1)]];
} Vertex;

// フラグメントシェーダーに渡す値.
typedef struct
{
    float4 position [[position]];
    float2 texCoord;
} ColorInOut;

// 頂点シェーダー.
vertex ColorInOut vertFunc(Vertex in [[stage_in]])
{
    ColorInOut out;
    
    out.position = float4(in.position, 1.0);
    out.texCoord = in.texCoord;
    
    return out;
}

// フラグメントシェーダー.
fragment float4 fragFunc(ColorInOut in [[stage_in]], texture2d<half> colorMap [[texture(0)]])
{
    constexpr sampler colorSampler(mip_filter::linear, mag_filter::linear, min_filter::linear);
    
    half4 colorSample = colorMap.sample(colorSampler, in.texCoord.xy);
    
    return float4(colorSample);
}

頂点バッファの定義

 // 頂点バッファの定義.
    {
        // シェーダーの頂点の定義の [[attribute(n)]] に対応した設定を行う.
        mtlpp::VertexDescriptor desc;
    
        auto attributes = desc.GetAttributes();
        auto layouts = desc.GetLayouts();
    
        // position.
        {
            auto attr = attributes[0];
                
            attr.SetFormat(mtlpp::VertexFormat::Float3);
            attr.SetOffset(0);
            attr.SetBufferIndex(0);
        }
            
        // texcoord.
        {
            auto attr = attributes[1];
                
            attr.SetFormat(mtlpp::VertexFormat::Float2);
            //attr.SetOffset(0);   // SetVertexBufferのオフセットでも指定できる.
            attr.SetOffset(12);        // texcoordのオフセット(byte).
            attr.SetBufferIndex(1);
        }
            
        {
            auto layout = layouts[0];
                
            layout.SetStride(20);  // 頂点バッファのサイズ. float(4byte) * 3 + float(4byte) * 2
            layout.SetStepRate(1);
            layout.SetStepFunction(mtlpp::VertexStepFunction::PerVertex);
        }
            
        {
            auto layout = layouts[1];
                
            layout.SetStride(20);  // 頂点バッファのサイズ. float(4byte) * 3 + float(4byte) * 2
            layout.SetStepRate(1);
            layout.SetStepFunction(mtlpp::VertexStepFunction::PerVertex);
        }

        g_vertexDesc = desc;
    }

頂点バッファの生成

 // 頂点バッファの生成.
    // uv
    // 左上 : 0, 0
    // 左下 : 0, 1
    // 右上 : 1, 0
    // 右下 : 1, 1
    {
        VertexData v[] =
        {
            { {-0.5f, 0.5f, 0.0f}, {0.0f, 0.0f} },     // 左上.
            { {0.5f, 0.5f, 0.0f}, {1.0f, 0.0f} },      // 右上.
            { {-0.5f, -0.5f, 0.0f}, {0.0f, 1.0f} },        // 左下.
            
            { {0.5f, 0.5f, 0.0f}, {1.0f, 0.0f} },      // 右上.
            { {0.5f, -0.5f, 0.0f}, {1.0f, 1.0f} },     // 右下.
            { {-0.5f, -0.5f, 0.0f}, {0.0f, 1.0f} },        // 左下.
        };
        
        g_vertexBuffer = g_device.NewBuffer(v, sizeof(v), mtlpp::ResourceOptions::CpuCacheModeDefaultCache);
        assert(g_vertexBuffer);
    }

描画処理

エンコーダーで頂点シェーダーに頂点バッファをセットする際にattribute[0],[1]に対応したバッファを渡す。
フラグメントシェーダーにテクスチャを渡すためにSetFragmentTexture()で読み込んだテクスチャを渡す。

 mtlpp::CommandBuffer commandBuffer = g_commandQueue.CommandBuffer();
    assert(commandBuffer.GetPtr());
    
    mtlpp::RenderPassDescriptor desc = window.GetRenderPassDescriptor();
    
    if(desc)
    {
        mtlpp::RenderCommandEncoder encoder = commandBuffer.RenderCommandEncoder(desc);
        assert(encoder);
        
        encoder.SetRenderPipelineState(g_renderPipelineState);
        
        // 面の向き.
        // 左手系はCW(時計回り)が表.
        //encoder.SetFrontFacingWinding(mtlpp::Winding::CounterClockwise);
        encoder.SetFrontFacingWinding(mtlpp::Winding::Clockwise);
        
        // カリングモード.
        // Back : 裏面を消す.
        encoder.SetCullMode(mtlpp::CullMode::Back);
        
        encoder.SetVertexBuffer(g_vertexBuffer, 0, 0);
        encoder.SetVertexBuffer(g_vertexBuffer, 0, 1);
        encoder.SetFragmentTexture(g_texture, 0);
        
        encoder.Draw(mtlpp::PrimitiveType::Triangle, 0, 3 * 2);
        
        encoder.EndEncoding();
        
        commandBuffer.Present(window.GetDrawable());
    }
    
    commandBuffer.Commit();
    commandBuffer.WaitUntilCompleted();

実行

画面中央にテクスチャが貼られた矩形が描画される。

f:id:takezoh_1127:20210113000547p:plain

アルファブレンディング

アルファブレンディングを行わないと透明にすべきピクセルが黒く描画されてしまう。

f:id:takezoh_1127:20210119020939p:plain

アルファブレンディングを行うことで半透明なピクセルを合成することができる。

レンダーパイプラインの生成

レンダーパイプラインの生成でアルファブレンドの設定を追加する。

 // レンダーパイプラインの生成.
    {
        mtlpp::RenderPipelineDescriptor desc;
            
        desc.SetVertexFunction(vertFunc);
        desc.SetFragmentFunction(fragFunc);
            
        desc.SetVertexDescriptor(g_vertexDesc);
            
        auto attach = desc.GetColorAttachments();
        attach[0].SetPixelFormat(mtlpp::PixelFormat::BGRA8Unorm);
        
        // アルファブレンド設定.
        attach[0].SetBlendingEnabled(true);
        attach[0].SetSourceRgbBlendFactor(mtlpp::BlendFactor::SourceAlpha);
        attach[0].SetDestinationRgbBlendFactor(mtlpp::BlendFactor::OneMinusSourceAlpha);       
        attach[0].SetRgbBlendOperation(mtlpp::BlendOperation::Add);

        g_renderPipelineState = g_device.NewRenderPipelineState(desc, nullptr);
        assert(g_renderPipelineState);
    }

実行

f:id:takezoh_1127:20210119021709p:plain

サンプラー

サンプラーをシェーダー関数の引数で渡すこともできる。

サンプラーステートの生成

mtlpp::SamplerState g_samplerState;


func()
{
    // サンプラーステートの生成.
    {
        mtlpp::SamplerDescriptor desc;
        
        desc.SetMipFilter(mtlpp::SamplerMipFilter::NotMipmapped);
        desc.SetMinFilter(mtlpp::SamplerMinMagFilter::Nearest);
        desc.SetMagFilter(mtlpp::SamplerMinMagFilter::Nearest);
        //desc.SetMinFilter(mtlpp::SamplerMinMagFilter::Linear);
        //desc.SetMagFilter(mtlpp::SamplerMinMagFilter::Linear);
        
        g_samplerState = g_device.NewSamplerState(desc);
        assert(g_samplerState);
    }
}

エンコーダー

     mtlpp::RenderCommandEncoder encoder = commandBuffer.RenderCommandEncoder(desc);
        assert(encoder);
        
        encoder.SetRenderPipelineState(g_renderPipelineState);
        
        :
        :

        encoder.SetVertexBuffer(g_vertexBuffer, 0, 0);
        encoder.SetVertexBuffer(g_vertexBuffer, 0, 1);
        encoder.SetFragmentTexture(g_texture, 0);

        // フラグメントシェーダーにサンプラーを渡す.
        encoder.SetFragmentSamplerState(g_samplerState, 0);
        
        encoder.DrawIndexed(mtlpp::PrimitiveType::TriangleStrip, 4, mtlpp::IndexType::UInt32, g_indexBuffer, 0);
        encoder.EndEncoding();
        
        commandBuffer.Present(window.GetDrawable());
    }

シェーダー

// フラグメントシェーダー.
fragment float4 fragFunc(ColorInOut in [[stage_in]], texture2d<half> colorMap [[texture(0)]], sampler colorSampler[[sampler(0)]])
{
    //constexpr sampler colorSampler(mip_filter::linear, mag_filter::linear, min_filter::linear);
    half4 colorSample = colorMap.sample(colorSampler, in.texCoord.xy);
    
    return float4(colorSample);
}

実行

Nearestを指定したのでLinerと異なり補間がかからずに引き伸ばされていることが確認できる。

f:id:takezoh_1127:20210120022110p:plain

立方体の描画

VectorやMatrix、Quaternion等の数学系の処理をとりあえず自前で用意。
commonlib/Math/以下にまとめている。
左手座標系として扱う。

window.mmの変更

デプスバッファを使用するため、window.mmのMTKViewの設定に以下を追加。

view.depthStencilPixelFormat = MTLPixelFormatDepth32Float_Stencil8;

シェーダー

ローカル座標系として処理を行う。
lambertで描画。

頂点バッファ、インデックスバッファの生成

 // 頂点バッファの生成.
    {
        VertexData vertex[] =
        {
            // 手前.
            { {-1.0f, -1.0f, -1.0f}, {0.0f, 0.0f, -1.0f}, {0.0f, 1.0f} },
            { {-1.0f, 1.0f, -1.0f}, {0.0f, 0.0f, -1.0f}, {0.0f, 0.0f} },
            { {1.0f, -1.0f, -1.0f}, {0.0f, 0.0f, -1.0f}, {1.0f, 1.0f} },
            { {1.0f, 1.0f, -1.0f}, {0.0f, 0.0f, -1.0f}, {1.0f, 0.0f} },
            
            // 右.
            { {1.0f, -1.0f, -1.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f} },
            { {1.0f, 1.0f, -1.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f} },
            { {1.0f, -1.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, {1.0f, 1.0f} },
            { {1.0f, 1.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f} },
            
            // 奥.
            { {1.0f, -1.0f, 1.0f}, {0.0f, 0.0f, 1.0f}, {0.0f, 1.0f} },
            { {1.0f, 1.0f, 1.0f}, {0.0f, 0.0f, 1.0f}, {0.0f, 0.0f} },
            { {-1.0f, -1.0f, 1.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 1.0f} },
            { {-1.0f, 1.0f, 1.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f} },
            
            // 左.
            { {-1.0f, -1.0f, 1.0f}, {-1.0f, 0.0f, 0.0f}, {0.0f, 1.0f} },
            { {-1.0f, 1.0f, 1.0f}, {-1.0f, 0.0f, 0.0f}, {0.0f, 0.0f} },
            { {-1.0f, -1.0f, -1.0f}, {-1.0f, 0.0f, 0.0f}, {1.0f, 1.0f} },
            { {-1.0f, 1.0f, -1.0f}, {-1.0f, 0.0f, 0.0f}, {1.0f, 0.0f} },
            
            // 上.
            { {-1.0f, 1.0f, -1.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 1.0f} },
            { {-1.0f, 1.0f, 1.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f} },
            { {1.0f, 1.0f, -1.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 1.0f} },
            { {1.0f, 1.0f, 1.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 0.0f} },
            
            // 下.
            { {-1.0f, -1.0f, 1.0f}, {0.0f, -1.0f, 0.0f}, {0.0f, 1.0f} },
            { {-1.0f, -1.0f, -1.0f}, {0.0f, -1.0f, 0.0f}, {0.0f, 0.0f} },
            { {1.0f, -1.0f, 1.0f}, {0.0f, -1.0f, 0.0f}, {1.0f, 1.0f} },
            { {1.0f, -1.0f, -1.0f}, {0.0f, -1.0f, 0.0f}, {1.0f, 0.0f} },
        };
        
        g_vertexBuff = g_device.NewBuffer(vertex, sizeof(vertex), mtlpp::ResourceOptions::CpuCacheModeDefaultCache);
        assert(g_vertexBuff);
    }
    
    // インデックスバッファの生成.
    {
        int32_t index[] =
        {
            // 手前.
            0, 1, 2,
            2, 1, 3,
            
            // 右.
            4, 5, 6,
            6, 5, 7,
            
            // 奥.
            8, 9, 10,
            10, 9, 11,
            
            // 左.
            12, 13, 14,
            14, 13, 15,
            
            // 上.
            16, 17, 18,
            18, 17, 19,
            
            // 下.
            20, 21, 22,
            22, 21, 23,
        };
        
        g_indexBuff = g_device.NewBuffer(index, sizeof(index), mtlpp::ResourceOptions::CpuCacheModeDefaultCache);
        assert(g_indexBuff);
    }

レンダーパイプラインの生成

デプス、ステンシルを使用するので以下を追加。

 // レンダーパイプラインの生成.
    {
        mtlpp::RenderPipelineDescriptor desc;
        
        desc.SetVertexFunction(vertFunc);
        desc.SetFragmentFunction(fragFunc);
        
        // インデックスバッファの場合は頂点ディスクリプタを設定する必要がある?.
        desc.SetVertexDescriptor(g_vertexDesc);
        
        auto attach = desc.GetColorAttachments();
        attach[0].SetPixelFormat(mtlpp::PixelFormat::BGRA8Unorm);
        
        desc.SetDepthAttachmentPixelFormat(mtlpp::PixelFormat::Depth32Float_Stencil8);
        desc.SetStencilAttachmentPixelFormat(mtlpp::PixelFormat::Depth32Float_Stencil8);
        
        g_renderPipelineState = g_device.NewRenderPipelineState(desc, nullptr);
        assert(g_renderPipelineState);
    }

デプスステートの生成

エンコーダーでセットするDepthステートを生成。

 // Depthステートの生成.
    {
        mtlpp::DepthStencilDescriptor desc;
        
        desc.SetDepthCompareFunction(mtlpp::CompareFunction::Less);
        desc.SetDepthWriteEnabled(true);
        
        g_depthState = g_device.NewDepthStencilState(desc);
        assert(g_depthState);
    }

ユニフォームバッファの生成

頂点シェーダーに渡すためのユニフォームバッファの生成。

// 頂点シェーダーに渡すユニフォームバッファ.
typedef struct
{
    Math::Matrix4 VP;               // view -> projection
    Math::Matrix4 LW;               // local -> world
    Math::Matrix4 WVP;              // local -> world -> view -> projection
    Math::Matrix4 invLW;            // invert LW
    Math::Vector3 lightDir;
    //float pad0;
    Math::Vector3 cameraPos;
    //float pad1;
    Math::Vector3 diffuseColor;
    Math::Vector3 specularColor;
    Math::Vector3 ambientColor;
    float specularPower;
}UniformBuffer;
 // ユニフォームバッファの生成.
    {
        g_uniformBuff = g_device.NewBuffer(sizeof(UniformBuffer), mtlpp::ResourceOptions::StorageModeShared);
        assert(g_uniformBuff);
    }

描画

カメラのプロジェクションとビューマトリクスの計算。

 // カメラ処理.
    Math::Matrix4 projection;
    Math::Matrix4 view;
    
    {
        float fovY = Math::ToRadians(60.0f);
        float width = static_cast<float>(SCREEN_WIDTH);
        float height = static_cast<float>(SCREEN_HEIGHT);
        float near = 1.0f;
        float far = 10000.0f;
        
        projection = Math::Matrix4::CreatePerspectiveFOV(fovY, width, height, near, far);
    }
    
    {
        Math::Vector3 cameraPos = Math::Vector3(0.0f, 0.0f, -4.0f);
        Math::Vector3 target = cameraPos + Math::Vector3::UnitZ() * 1.0f;
        Math::Vector3 up = Math::Vector3::UnitY();
        
        view = Math::Matrix4::CreateLookAt(cameraPos, target, up);
    }

モデルのワールドマトリクの計算。

 // モデルのマトリクスの更新.
    {
        float angle = -1.0f * deltaTime;
    
        Math::Quaternion incY(Math::Vector3::UnitY(), angle * 0.5f);
        Math::Quaternion incX(Math::Vector3::UnitX(), angle * 0.2f);
        
        auto rot = g_transform.GetRotation();
        
        rot = Math::Quaternion::Concatenate(rot, incY);
        rot = Math::Quaternion::Concatenate(rot, incX);
                
        g_transform.SetRotation(rot);
    }

計算したプロジェクション、ビューマトリクスやモデルのマトリクスを頂点シェーダーに渡すユニフォームバッファにセットする。

 // ユニフォームバッファの設定.
    {
        UniformBuffer* buffer = static_cast<UniformBuffer*>(g_uniformBuff.GetContents());
        
        buffer->VP = view * projection;
    
        buffer->LW = g_transform.GetWorldMatrix();
        buffer->invLW = buffer->LW;
        buffer->invLW.Invert();
        
        buffer->WVP = g_transform.GetWorldMatrix() * view * projection;
        
        // ライトの向きをローカル座標系に変換.
        {
            auto lightDir = Math::Vector3::Normalize(Math::Vector3(0.0f, -0.7f, 0.7f));
            
            auto invLW = buffer->invLW;
            
            buffer->lightDir = Math::Vector3::Transform(lightDir, invLW, 0.0f);
        }
        
        // 光源色を頂点シェーダーに渡す.
        {
            buffer->ambientColor = Math::Vector3(0.4f, 0.4f, 0.4f);
        
            buffer->diffuseColor = Math::Vector3(1.0f, 1.0f, 1.0f);
            buffer->specularColor = Math::Vector3(0.6f, 0.6f, 0.6f);
        }
    }

エンコーダーに対してデプスステートをセットを追加。 ユニフォームバッファのセットを追加。

 //
    mtlpp::CommandBuffer commandBuffer = g_commandQueue.CommandBuffer();
    assert(commandBuffer);
        
    mtlpp::RenderPassDescriptor desc = window.GetRenderPassDescriptor();
        
    if(desc)
    {
        mtlpp::RenderCommandEncoder encoder = commandBuffer.RenderCommandEncoder(desc);
        assert(encoder);
            
        encoder.SetRenderPipelineState(g_renderPipelineState);
            
        encoder.SetDepthStencilState(g_depthState);
            
        // 面の向き.
        // 左手系はCW(時計周り)が表.
        encoder.SetFrontFacingWinding(mtlpp::Winding::Clockwise);
            
        // カリングモード.
        encoder.SetCullMode(mtlpp::CullMode::Back);
            
        {
            // ユニフォームバッファをセット.
            encoder.SetVertexBuffer(g_uniformBuff, 0, 3);
            
            // 頂点バッファをセット.
            encoder.SetVertexBuffer(g_vertexBuff, 0, 0);
            encoder.SetVertexBuffer(g_vertexBuff, 0, 1);
            encoder.SetVertexBuffer(g_vertexBuff, 0, 2);
            
            // テクスチャをセット.
            encoder.SetFragmentTexture(g_texture, 0);
            
            // インデックスバッファによる描画.
            encoder.DrawIndexed(mtlpp::PrimitiveType::Triangle, 36, mtlpp::IndexType::UInt32, g_indexBuff, 0);
        }
            
        encoder.EndEncoding();
            
        commandBuffer.Present(window.GetDrawable());
    }
        
    commandBuffer.Commit();
    commandBuffer.WaitUntilCompleted();

実行

f:id:takezoh_1127:20210114000406p:plain

参考

booth.pm

フリー素材リンク

texturehaven.com


【Mac】スクリーンショットの撮り方

・「画面全体」のスクリーンショットの撮影

 Shift + Command + 3

 

・「指定範囲」のスクリーンショットの撮影

 Shift + Command + 4

 

・「ウィンドウ」のスクリーンショットの撮影

 Shift + Command + 4 の後にスペースバー

 

スクリーンショットの保存先

 基本はデスクトップに保存される。

 macOS Mojave以降はスクリーンショットAppのオプションで保存先を変更できる。

 

support.apple.com

【Metal】Metal Shading Language 仕様 Version2.2

Metal のシェーダー言語の仕様は以下のURLにあります。

https://developer.apple.com/metal/Metal-Shading-Language-Specification.pdf

必要なところから抜粋して翻訳してみます。

6 Metal Standard Library

この章では Metal Standard Library(MSLib)の関数について説明します。

6.1 Namespace and Header Files

MSLib関数と列挙は、metal名前空間で宣言されています。
MSLib関数で説明されているヘッダーファイル<metal_stdlib>ヘッダーが利用可能で、
MSLibでサポートされているすべての関数にアクセスできます。

6.2 Common Functions

表6.1の関数は、ヘッダー <metal_common> で定義されています。
Tはスカラーまたはベクトル浮動小数点型です。

ビルトイン共通関数 説明
T clamp(T x, T minval, T maxval) fmin(fmax(x, minval), maxval) を返す。
minval>maxvalの場合、結果は未定義。
T mix(T x, T y, T a) xとyの線形ブレンドの値を返す。
x +(y – x)* a

aは0.0から1.0の範囲の値でなければならない。
0.0から1.0の範囲でない場合は戻り値は未定義。
T saturate(T x) 指定された値を0.0から1.0の範囲内にクランプする。
T sign(T x) x> 0の場合は1.0、x = -0.0の場合は-0.0、x = +0.0の場合は+0.0、またはx <0の場合は-1.0を返す。
xがNaNの場合は0.0を返す。
T smoothstep(T edge0, T edge1m T x) x <= edge0の場合は0.0を、x >= edge1の場合は1.0を返す。
edge0 < x <edge1の場合はエルミート補間を実行し、0から1の値を返す。

以下と同等。
t = clamp( (x – edge0) / (edge1 – edge0), 0, 1);
return t * t * (3 – 2 * t);

edge0> = edge1の場合、またはx、edge0もしくはedge1がNaNの場合は結果は未定義。
T step(T edge, T x) x < edgeの場合は0.0を返し、それ以外の場合は1.0を返す。

6.3 Integer Functions

ビルトイン整数関数 説明
T abs(T x) |x|を返す。
Tu absdiff(T x, T y) 剰余オーバーフローなしで|x-y|を返す。
T addsat(T x, T y) x+yのsaturateした結果を返す。
T clamp(T x, T minval, T maxval) min(max(x, minval), maxval)を返す。

minval > maxvalの場合、結果は未定義。
T clz(T x) 最上位ビット位置から開始して、xの先頭の0ビットの数を返す。
T ctz(T x) xの末尾の0ビットの数を返す。
T extract_bits(T x, uint offset, uint bits)
T hadd(T x, T y) (x + y) >> 1を返す。
The intermediate sum does not modulo overflow.
T insert_bits(T base, T insert, uint offset, uint bits)
T32 mad24(T32 x, T32 y, T32 z)
T madhi(T a, T b, T c) mulhi(a, b) + cを返す。
T madsat(T a, T b, T c) a * b + cのsaturateした結果を返す。
T max(T x, T y) x < yの場合はyを返し、それ以外の場合はxを返す。
T max3(T x, T y, T z) max(x, max(y, z))を返す。
T median3(T x, T y, T z)
T min(T x, T y) y < xの場合はyを返し、それ以外の場合はxを返す。
T min3(T x, T y, T z) min(x, min(y, z))を返す。
T32 mul24(T32 x, T32 y)
T mulhi(T x, T y) x * yを計算し、xとyの積の上位半分を返す。
T popcount(T x) xの非ゼロビットの数を返す。
T reverse_bits(T x)
T rhadd(T x, T y) (x + y + 1) >> 1を返す。
The intermediate sum does not modulo overflow.
T rotate(T v, T i)
T subsat(T x, T y) x – yのsaturateした結果を返す。

6.4 Relational Functions

ビルトイン共通関数 説明
bool all(Tb x) xのすべてのコンポーネントがtrueの場合にのみtrueを返す。
bool any(Tb x) xのいずれかのコンポーネントがtrueの場合にのみtrueを返す。
Tb isfinite(T x) 有限値をテストする。
Tb isinf(T x) 無限値(正または負)をテストする。
Tb isnan(T x) NaNのテスト。
Tb isnormal(T x)
Tb isordered(T x, T y)
Tb isunordered(T x, T y)
Tb not(Tb x) xのコンポーネント単位の論理補数を返す。
T select(T a, T b, Tb c)
Ti select(Ti a, Ti b, Tb c)
ベクトル型の各コンポーネントについて
result [i] = c [i]? b [i]:a [i]

スカラー型の場合
result = c? b:a
Tb signbit(T x) 符号ビットをテスト。

xの浮動小数点値に符号ビットが設定されている場合はtrueを返す。
それ以外の場合はfalseを返す。

6.5 Math Functions

ビルトイン数学関数 説明
T acos(T x) xの逆余弦を計算。
T acosh(T x) xの逆双曲線余弦を計算。
T asin(T x) xの逆正弦を計算。
T asinh(T x) xの逆双曲線正弦を計算。
T atan(T y_over_x) xの逆正接を計算。
T atan2(T y, T x) xに対するyの逆正接を計算。
T atanh(T x) xの双曲線逆正接を計算。
T ceil(T x) xを整数値に丸める。
T copysign(T x, T y)
T cos(T x) xの余弦を計算。
T cosh(T x) xの双曲線余弦を計算。
T cospi(T x) cos(πx)を計算。
T divide(T x, T y) x / yを計算。
T exp(T x) eが底の指数関数。
T exp2(T x) 2が底の指数関数。
T exp10(T x) 10が底の指数関数。
T fabs(T x)
T abs(T x)
浮動小数点数の絶対値を計算。
T fdim(T x, T y)
T floor(T x)
T fma(T a, T b, T c)
T fmax(T x, T y)
T max(T x, T y)
fmax(x, fmax(y, z))を返す。
T fmax3(T x, T y, T z)
T max3(T x, T y, T z)
T fmedian3(T x, T y, T z)
T median3(T x, T y, T z)
T fmin(T x, T y)
T min(T x, T y)
T fmin3(T x, T y, T z)
T min3(T x, T y, T z)
fmin(x, fmin(y, z))を返す。
T fmod(T x, T y) x – y * trunc(x / y)を返す。
T fract(T x)
T frexp(T x, Ti &exponent)
Ti ilogb(T x)
T ldexp(T x, Ti k)
T log(T x) xの自然対数を計算。
T log2(T x) xの2を底とする対数を計算。
T log10(T x) xの10を底とする対数を計算
T modf(T x, T &intval)
T pow(T x, T y) xのy乗を計算。
T powr(T x, T y) x >= 0の時、xのy乗を計算。
T rint(T x)
T round(T x)
T rsqrt(T x) xの逆平方根を計算。
T sin(T x) xの正弦を計算。
T sincos(T x, T &cosval)
T sinh(T x) xの双曲線正弦を計算。
T sinpi(T x) sin(πx)を計算。
T sqrt(T x) xの平方根を計算。
T tan(T x) xの正接を計算。
T tanh(T x) xの双曲線正接を計算。
T tanpi(T x) tan(πx)を計算。
T trunc(T x) xを整数値に丸める。
定数名 説明
MAXFLOAT
HUGE_VALF
INFINITY
NAN
M_E_F
M_LOG2E_F
M_LOG10E_F
M_LN2_F
M_LN10_F
M_PI_F
M_PI_2_F
M_PI_4_F
M_1_PI_F
M_2_PI_F
M_2_SQRTPI_F
M_SQRT2_F
M_SQRT1_2_F
定数名 説明
MAXHALF
M_E_H
M_LOG2E_H
M_LOG10E_H
M_LN2_H
M_LN10_H
M_PI_H
M_PI_2_H
M_PI_4_H
M_1_PI_H
M_2_PI_H
M_2_SQRTPI_H
M_SQRT2_H
M_SQRT1_2_H

6.6 Matrix Functions

ビルトインマトリクス関数 説明
float determinant(floatnxn)
half determinant(halfnxn)
floatmxn transpose(floatnxm)
halfmxn transpose(halfnxm)

6.7 Geometric Functions

ビルトイン幾何関数 説明
T cross(T x, T y) xとyの外積を返す。

Tは3成分のベクトル型でなければならない。
Ts distance(T x, T y) xとyの間の距離を返す。
Ts distance_squared(T x, T y) xとyの間の距離の2乗を返す。
Ts dot(T x, T y) xとyの内積を返す。
T faceforward(T N, T I, T Nref) dot(Nref, I) < 0.0はNを返し、それ以外は–Nを返す。
Ts length(T x) ベクトルxの長さを返す。
Ts length_squared(T x) ベクトルxの長さの2乗を返す。
T normalize(T x) xと同じ方向で長さが1のベクトルを返す。
T reflect(T I, T N) 入射ベクトルIと表面方向Nについて、正規化されたN(NN)を計算し、反射方向を返す。
I – 2 * dot(NN, I) * NN
T refract(T I, T N, Ts eta) 入射ベクトルIと表面法線N、および屈折率etaの屈折ベクトルを返す。

6.8 Compute Functions

6.9 Graphics Functions

6.10 Texture Functions

6.11 Imageblock Functions

6.12 Pack and Unpack Functions

6.13 Atomic Functions

6.14 Encoding Commands for Indirect Command Buffers

6.15 Variable Rasterization Rate

7 Numerical Compliance

7.1 INF, NaN, and Denormalized Numbers

7.2 Rounding Mode

7.3 Floating-Point Exceptions

7.4 Relative Error as ULPs

7.5 Edge Case Behavior in Flush to Zero Mode

7.6 Conversion Rules for Floating-Point and Integer Types

7.7 Texture Addressing and Conversion Rules

7.7.1 Conversion Rules for Normalized Integer Pixel Data Types
7.7.2 Conversion Rules for Half-Precision Floating-Point Pixel Data Type
7.7.3 Conversion Rules for Single-Precision Floating-Point Pixel Data Type
7.7.4 Conversion Rules for 10- and 11-bit Floating-Point Pixel Data Type
7.7.5 Conversion Rules for 9-bit Floating-Point Pixel Data Type with a 5-bit Exponent
7.7.6 Conversion Rules for Signed and Unsigned Integer Pixel Data Types
7.7.7 Conversion Rules for sRGBA and sBGRA Textures

Xcodeで実行時に必要なdylibなどをコピーする方法

GLEWやSDLなどのライブラリをdylibでプロジェクトに組み込んだ場合、
Xcodeで実行するに際に実行ファイルと同じ場所にdylibがないと実行時にエラーになる。

そのような場合にXcodeで実行時に実行ファイルと同じフォルダにdylibをコピーするには以下の方法で行える。

[Build Phase] → [Run Script] に以下のシェルスクリプトを記述する。

if [ -d "$BUILD_DIR/Debug" ]; then
    cp "$SRCROOT"/GLEW/lib/mac/*.dylib $BUILD_DIR/Debug
    cp "$SRCROOT"/SDL/lib/mac/*.dylib $BUILD_DIR/Debug
fi

if [ -d "$BUILD_DIR/Release" ]; then
    cp "$SRCROOT"/GLEW/lib/mac/*.dylib $BUILD_DIR/Release
    cp "$SRCROOT"/SDL/lib/mac/*.dylib $BUILD_DIR/Release
fi

f:id:takezoh_1127:20190831004045p:plain

Xcodeで実行時の作業ディレクトリの変更方法

Xcodeでデバッガで実行した際のファイルの読み込みの作業ディレクトリは以下の方法で変更できる。

1.Xcodeのメニューバーの [Product] → [Scheme] → [Edit Scheme] を開く。

f:id:takezoh_1127:20190830000913p:plain

2.[Run] の [Options] を選択。
3.その中のWorking Directory の Use custom working directory: にチェックを入れる。
4.その下の項目にデータを読み込ませるパスを指定する。
 例えばプロジェクト直下から読みたい場合は、環境変数の $(SRCROOT) を指定することで読み込むことができる。

f:id:takezoh_1127:20190830000930p:plain

*.xcodeprojのあるディレクトリで以下のコマンドを実行すると環境変数を確認することができる。

xcodebuild -showBuildSettings

XcodeでARCの有効/無効を設定する方法

Xcodeはデフォルトでプロジェクト全体の設定でARCが有効になっている。

ファイル単位でARCの設定を行う

プロジェクト内で特定のファイルのみARCの設定を変更したい場合、以下の設定を行う。

1.各ファイルの [Build Phase] → [Compile Source] を開く
2.ARCを無効にしたいファイルを選択してエンターキーもしくはダブルクリックする
3.入力欄が表示されるので無効にする場合は -fno-objc-arc と入力する
(※有効にする場合は -fobjc-arc と入力する)

f:id:takezoh_1127:20190829233002p:plain

プロジェクト全体でARCの設定を行う

プロジェクト全体のARCの設定は [Build Setting] → [Apple Clang - Language - Objective-C] の
"Objective-C Automatic Reference Counting"で変更できる。

f:id:takezoh_1127:20190829233045p:plain