3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Metal-cppとGLFWの開発環境をセットアップする

Last updated at Posted at 2022-11-05

はじめに

OpenGLという有名なグラフィックスライブラリがありますが、かなり前からAppleの端末では非推奨のライブラリとなっています。
では何を使えば良いのかというと、Apple製のグラフィックスライブラリであるMetalを使うのが推奨されているようです。
このライブラリは長らく公式ではObjCかSwiftを使って書くのが唯一の方法で、他の言語向けのバインディングなどは提供されていませんでした。
なのですが、いつの間にか公式からMetal-cppというC++バインディングが提供されていました。
これとGLFWを使って基本的な描画処理を実装します。
参考:glfw-metal-example.m

開発環境

.sh
macOS Monterey(v12.5.1)

事前準備

metal-cpp_macOS12_iOS15.zip をダウンロードして展開する。
下記コマンドでシングルヘッダーにまとめる。

.sh
./SingleHeader/MakeSingleHeader.py Foundation/Foundation.hpp QuartzCore/QuartzCore.hpp Metal/Metal.hpp

ただし、この変換スクリプトの中身を見るとわかるのですが、
/usr/bin/python を実行するよう指定しています。
新しめのMacOSでは組み込みの python が削除されていて、/usr/bin/pythonは存在しません。

.py
#!/usr/bin/python

以下のように書き換えるとよしなに python のパスを解決してくれます。

.py
#!/usr/bin/env python

ビルド設定

適当な場所にリポジトリ作成

.sh
mkdir Metal-Study
cd Metal-Study
git init
gibo dump CMake c++ VisualStudioCode > .gitignore
git add .
git commit -m "Add: 最初のコミット"

諸々の環境をCMakeでセットアップ
CMake で GLFW を見つけるのライブラリ検索スクリプトを使います。

.sh
mkdir CMake
curl https://raw.githubusercontent.com/benikabocha/FindGLFW_Test/master/cmake/FindGLFW.cmake > CMake/FindGLFW.cmake
touch CMakeLists.txt
touch main.cpp

CMakeの内容はこんな感じに設定。

CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(
    MetalStudy
    VERSION 0.0.1
    LANGUAGES CXX
)
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
#find_package(OpenGL REQUIRED)
find_package(GLFW REQUIRED)

add_executable(
    MetalStudy
    main.cpp
)
target_compile_definitions(
    MetalStudy
    PRIVATE
    $<$<CONFIG:Release>:MetalStudy_NDEBUG>
    $<$<CONFIG:Debug>:MetalStudy_DEBUG _DEBUG>
)
target_compile_options(
    MetalStudy
    PRIVATE
    $<$<CONFIG:Release>:-O3>
    $<$<CONFIG:Debug>:-O0 -g -Wall -Werror -fsanitize=address>
)
target_link_options(
    MetalStudy
    PRIVATE
    $<$<CONFIG:Release>:>
    $<$<CONFIG:Debug>: -fsanitize=address>
)
target_compile_features(MetalStudy PRIVATE cxx_std_17)

target_include_directories(
    MetalStudy
    PRIVATE
    #${OPENGL_INCLUDE_DIR}
    ${GLFW_INCLUDE_DIR}
)

target_link_libraries(
    MetalStudy
    PRIVATE
    #${OPENGL_LIBRARIES}
    ${GLFW_LIBRARIES}
)

まずはこんな感じのコードを用意してGLFWの動作をひとまず確認。

main.cpp
#include <GLFW/glfw3.h>
#include <iostream>
#include <string>

int gl_init(int argc, char *argv[], const char *title, int width, int height,
            bool fullScreen, GLFWwindow **outWindow) {
  (*outWindow) = NULL;
  if (!glfwInit())
    return -1;
  glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
  // create window
  GLFWwindow *window = NULL;
  if (fullScreen) {
    GLFWmonitor *monitor = glfwGetPrimaryMonitor();
    const GLFWvidmode *mode = glfwGetVideoMode(monitor);
    glfwWindowHint(GLFW_RED_BITS, mode->redBits);
    glfwWindowHint(GLFW_GREEN_BITS, mode->greenBits);
    glfwWindowHint(GLFW_BLUE_BITS, mode->blueBits);
    glfwWindowHint(GLFW_REFRESH_RATE, mode->refreshRate);
    window = glfwCreateWindow(mode->width, mode->height, title, monitor, NULL);
  } else {
    glfwWindowHint(GLFW_RESIZABLE, 0);
    window = glfwCreateWindow(width, height, title, NULL, NULL);
  }
  if (!window) {
    glfwTerminate();
    return -1;
  }
  glfwMakeContextCurrent(window);
  glfwSetTime(0.0);
  glfwSwapInterval(1);
  (*outWindow) = window;
  return 0;
}

int main(int argc, char *argv[]) {
  GLFWwindow *window;
  int status = gl_init(argc, argv, "Sample", 1280, 720, false, &window);
  if (status != 0) {
    return status;
  }

  while (!glfwWindowShouldClose(window)) {
    glfwSwapBuffers(window);
    glfwPollEvents();
  }
  glfwDestroyWindow(window);
  glfwTerminate();
  return 0;
}

こんな感じでビルドして実行できるはず。

.sh
cmake .
make
./MetalStudy

一旦コミット。

.sh
git add .
git commit -m "Add: GLFWの動作確認できるところまで"

Metalも組み込む

事前準備で用意したヘッダーをリポジトリにコピーして、コードからも読み込みます。

main.cpp
// Metal
#define NS_PRIVATE_IMPLEMENTATION
#define CA_PRIVATE_IMPLEMENTATION
#define MTL_PRIVATE_IMPLEMENTATION
#include "Metal.hpp"
// GLFW
#include <GLFW/glfw3.h>
#include <iostream>
#include <string>

これだけだとまだビルドできないので、CMakeも変更が必要です。
※ビルドして実行しても結果は特に変わりません。

CMakeLists.txt
target_link_libraries(
    MetalStudy
    PRIVATE
    #${OPENGL_LIBRARIES}
    "-framework Cocoa"
    "-framework Foundation"
    "-framework Metal"
    "-framework QuartzCore"
    ${GLFW_LIBRARIES}
)

一旦この時点でのファイル構成を以下に示します。

.sh
> ls
 CMake                 CMakeFiles       Makefile
 cmake_install.cmake   CMakeLists.txt   Metal.hpp
 CMakeCache.txt        main.cpp         MetalStudy

一旦コミット。

.sh
git add .
git commit -m "Add: Metalの組み込み"

足りない機能を追加

Metal-cpp は基本的に Metal の主要な機能を提供してくれていますが、
Metalとウィンドウシステムのつなぎ込みの部分?は提供していないようです。
もしかしたら私が知らないだけで他に適切な方法があるのかもしれないのですが、私はいくつか追加でコードを用意して解決しました。
Metal-cpp は内部的にはObjC経由で Metal を呼び出しているだけなのですが、
できるだけC++のスタイルとフィットするように簡単なラップ用の機能を提供しています。
例えば、ObjCのメソッド呼び出しやプロパティアクセスは Object::sendMessage で実装できます。
他にもObjCではお馴染みの alloc, init も Object クラスに対応するメソッドが用意されています。
基本的には Metal-cpp のコードを参考にしながらこれらのラップ用機能を使えば実装は難しくありません。

追記
この記事を投稿してから気づいたのですが、こちらのページの サンプルコードをダウンロード のリンクから以下で説明するNSView,NSWindowの拡張と同じようなものがダウンロードできるようです。
こちらはウィンドウ自体にGLFWではなくMac用の専用APIを使うのと、CAMetalLayerではなくMTKViewを使うようになってるので微妙に違うのですが、こちらも大変参考になるので目を通しておいた方がよさそうです。

CocoaWindow

まず Metal とウィンドウシステムを繋ぎ込むにあたって NSWindow を取得する必要があります。
これは単に GLFWglfwGetCocoaWindow を呼び出すだけなのですが、一つ問題があります。
MacOSX.sdkでは次のように id 型が宣言されています。

objc.h
/*
 * Copyright (c) 1999-2007 Apple Inc.  All Rights Reserved.
 * 
 * @APPLE_LICENSE_HEADER_START@
 * 
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apple Public Source License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. Please obtain a copy of the License at
 * http://www.opensource.apple.com/apsl/ and read it before using this
 * file.
 * 
 * The Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 * 
 * @APPLE_LICENSE_HEADER_END@
 */

//
// 中略...
//

typedef struct objc_object *id;

そして GLFW でも次のように id 型が宣言されています。

glfw3native.h
/*************************************************************************
 * GLFW 3.3 - www.glfw.org
 * A library for OpenGL, window and input
 *------------------------------------------------------------------------
 * Copyright (c) 2002-2006 Marcus Geelnard
 * Copyright (c) 2006-2018 Camilla Löwy <elmindreda@glfw.org>
 *
 * This software is provided 'as-is', without any express or implied
 * warranty. In no event will the authors be held liable for any damages
 * arising from the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you must not
 *    claim that you wrote the original software. If you use this software
 *    in a product, an acknowledgment in the product documentation would
 *    be appreciated but is not required.
 *
 * 2. Altered source versions must be plainly marked as such, and must not
 *    be misrepresented as being the original software.
 *
 * 3. This notice may not be removed or altered from any source
 *    distribution.
 *
 *************************************************************************/

//
// 中略...
//

typedef void* id;

というわけで、この二つのヘッダーを公開ヘッダーからインクルードしてしまうと同じ名前で二つの型が存在することになり、
コンパイルエラーとなってしまいます。対処としてはごく単純で、.cppからのみidが参照されるような作りにすればOKです。

ヘッダーでは基本ヘッダーのみをインクルードして

CocoaWindow.hpp
#pragma once
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>

namespace GLFW::Private {
void *getCocoaWindow(GLFWwindow *window);
}

実装ファイルからのみネイティブ機能のヘッダー(idを定義しているヘッダー)をインクルードします。

CocoaWindow.cpp
#include "CocoaWindow.hpp"
#define GLFW_EXPOSE_NATIVE_COCOA
#include <GLFW/glfw3native.h>

namespace GLFW::Private {
void *getCocoaWindow(GLFWwindow *window) { return glfwGetCocoaWindow(window); }
} // namespace GLFW::Private

この CocoaWindow.hppMetal.hpp なら、同時にインクルードしてもid型の衝突が起こりません。

NSWindow

この記事の冒頭でも参考コードとしてリンクを貼ってあるのですが、
Metalとウィンドウシステムを繋ぎ込むためにはNSWindow.cotentView.layerCAMetalLayerで置き換える必要があります。
これらのクラスは Metal-cpp で提供されていないので自分で用意する必要があります。

まずヘッダー側でクラスとセレクタを事前に定義しておく必要があります。

NSWindow.hpp
#pragma once
#include "Metal.hpp"

namespace NS::Private::Class {
_NS_PRIVATE_DEF_CLS(NSWindow);
} // namespace NS::Private::Class

namespace NS::Private::Selector {
_NS_PRIVATE_DEF_SEL(contentView_, "contentView");
} // namespace NS::Private::Selector

namespace NS {
class View;
class Window : public Referencing<Window> {
public:
  static Window *bridgingCast(const void *ptr);
  class View *contentView() const;
};
} // namespace NS

ヘッダーで定義したセレクタを使って sendMessage を呼び出すことでObjCとの繋ぎ込みを実装できます。

NSWindow.cpp
#include "NSWindow.hpp"
#include "NSView.hpp"

namespace NS {
Window *Window::bridgingCast(const void *ptr) {
  return Object::bridgingCast<Window *>(ptr);
}
View *Window::contentView() const {
  return Object::sendMessage<View *>(this, _NS_PRIVATE_SEL(contentView_));
}
} // namespace NS

セレクタとして定義する文字列などはAppleの公式ドキュメントを参考にしてください。

NSView

NSWindow.contentView に対応するクラスの定義です。
ほぼ先ほどと同じなのであまり説明することはありません。

NSView.hpp
#pragma once
#include "Metal.hpp"

namespace NS::Private::Class {
_NS_PRIVATE_DEF_CLS(NSView);
} // namespace NS::Private::Class

namespace NS::Private::Selector {
_NS_PRIVATE_DEF_SEL(setLayer_, "setLayer:");
_NS_PRIVATE_DEF_SEL(setWantsLayer_, "setWantsLayer:");
} // namespace NS::Private::Selector

namespace CA {
class MetalLayer;
}
namespace NS {
class View : public Referencing<View> {
public:
  void setLayer(const CA::MetalLayer *layer);
  void setWantsLayer(bool yes);
};
} // namespace NS

sendMessageは可変長引数を取るようになっているのでそのまま引数を渡せます。

NSView.cpp
#include "NSView.hpp"

namespace NS {
void View::setLayer(const CA::MetalLayer *layer) {
  Object::sendMessage<void>(this, _NS_PRIVATE_SEL(setLayer_), layer);
}
void View::setWantsLayer(bool yes) {
  Object::sendMessage<void>(this, _NS_PRIVATE_SEL(setWantsLayer_), yes);
}
} // namespace NS

CAMetalLayer

最後に CAMetalLayer を定義します。
これも基本的には今までと同じなのですが、使うマクロが違います。

CAMetalLayer.hpp
#pragma once
#include "Metal.hpp"

namespace CA::Private::Class {
_CA_PRIVATE_DEF_CLS(CAMetalLayer);
} // namespace CA::Private::Class

namespace CA::Private::Selector {
_CA_PRIVATE_DEF_SEL(setDevice_, "setDevice:");
_CA_PRIVATE_DEF_SEL(setOpaque_, "setOpaque:");
_CA_PRIVATE_DEF_SEL(nextDrawable_, "nextDrawable");
} // namespace CA::Private::Selector

namespace CA {
class MetalLayer : public NS::Referencing<MetalLayer> {
public:
  static MetalLayer *alloc();
  MetalLayer *init();
  void setDevice(const MTL::Device *device);
  void setOpaque(bool yes);
  CA::MetalDrawable *nextDrawable();
};
} // namespace CA

alloc/initは sendMessage とは別で専用のメソッドが用意されています。

CAMetalLayer.cpp
#include "CAMetalLayer.hpp"

namespace CA {
MetalLayer *MetalLayer::alloc() {
  return NS::Object::alloc<MetalLayer>(_CA_PRIVATE_CLS(CAMetalLayer));
}

MetalLayer *MetalLayer::init() { return NS::Object::init<MetalLayer>(); }
void MetalLayer::setDevice(const MTL::Device *device) {
  Object::sendMessage<void>(this, _CA_PRIVATE_SEL(setDevice_), device);
}
void MetalLayer::setOpaque(bool yes) {
  Object::sendMessage<void>(this, _CA_PRIVATE_SEL(setOpaque_), yes);
}
CA::MetalDrawable *MetalLayer::nextDrawable() {
  return Object::sendMessage<CA::MetalDrawable *>(
      this, _CA_PRIVATE_SEL(nextDrawable_));
}
} // namespace CA

一旦コミット。

.sh
git add .
git commit -m "Add: 繋ぎ込みのための機能を追加"

最後に

あとは冒頭の参考コードをそのままC++の機能に置き換えていくだけです。

main.cpp
// Metal
#define NS_PRIVATE_IMPLEMENTATION
#define CA_PRIVATE_IMPLEMENTATION
#define MTL_PRIVATE_IMPLEMENTATION
#include "Metal.hpp"
// Metal拡張
#include "CAMetalLayer.hpp"
#include "CocoaWindow.hpp"
#include "NSView.hpp"
#include "NSWindow.hpp"
// その他標準ライブラリ
#include <iostream>
#include <string>

int gl_init(int argc, char *argv[], const char *title, int width, int height,
            bool fullScreen, GLFWwindow **outWindow) {
  (*outWindow) = NULL;
  if (!glfwInit())
    return -1;
  glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
  // create window
  GLFWwindow *window = NULL;
  if (fullScreen) {
    GLFWmonitor *monitor = glfwGetPrimaryMonitor();
    const GLFWvidmode *mode = glfwGetVideoMode(monitor);
    glfwWindowHint(GLFW_RED_BITS, mode->redBits);
    glfwWindowHint(GLFW_GREEN_BITS, mode->greenBits);
    glfwWindowHint(GLFW_BLUE_BITS, mode->blueBits);
    glfwWindowHint(GLFW_REFRESH_RATE, mode->refreshRate);
    window = glfwCreateWindow(mode->width, mode->height, title, monitor, NULL);
  } else {
    glfwWindowHint(GLFW_RESIZABLE, 0);
    window = glfwCreateWindow(width, height, title, NULL, NULL);
  }
  if (!window) {
    glfwTerminate();
    return -1;
  }
  glfwMakeContextCurrent(window);
  glfwSetTime(0.0);
  glfwSwapInterval(1);
  (*outWindow) = window;
  return 0;
}

int main(int argc, char *argv[]) {
  GLFWwindow *window;
  int status = gl_init(argc, argv, "Sample", 1280, 720, false, &window);
  if (status != 0) {
    return status;
  }

  MTL::Device *device = MTL::CreateSystemDefaultDevice();
  MTL::CommandQueue *queue = device->newCommandQueue();
  CA::MetalLayer *layer = CA::MetalLayer::alloc();
  layer->init();
  layer->setDevice(device);
  layer->setOpaque(true);
  NS::Window *nsWindow =
      NS::Window::bridgingCast(GLFW::Private::getCocoaWindow(window));
  NS::View *contentView = nsWindow->contentView();
  contentView->setLayer(layer);
  contentView->setWantsLayer(true);

  while (!glfwWindowShouldClose(window)) {
    CA::MetalDrawable *surface = layer->nextDrawable();

    MTL::RenderPassDescriptor *pass = MTL::RenderPassDescriptor::alloc();
    pass->init();
    MTL::RenderPassColorAttachmentDescriptor *desc =
        pass->colorAttachments()->object(0);
    desc->setClearColor(MTL::ClearColor(1.0, 0.0, 0.0, 1.0));
    desc->setLoadAction(MTL::LoadAction::LoadActionClear);
    desc->setStoreAction(MTL::StoreAction::StoreActionStore);
    desc->setTexture(surface->texture());

    MTL::CommandBuffer *buffer = queue->commandBuffer();
    MTL::RenderCommandEncoder *encoder = buffer->renderCommandEncoder(pass);
    encoder->endEncoding();
    buffer->presentDrawable(surface);
    buffer->commit();
    glfwPollEvents();
  }
  glfwDestroyWindow(window);
  glfwTerminate();
  return 0;
}

CMakeListsにも忘れずにcppを追加します。

CMakeLists.txt
add_executable(
    MetalStudy
    main.cpp
    CocoaWindow.cpp
    NSWindow.cpp
    NSView.cpp
    CAMetalLayer.cpp
)

実行すると真っ赤に塗りつぶされたウィンドウが表示されるはずです。
2022-11-05_16-17-49.png

サンプルリポジトリはこちらに置いています。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?