はじめに
OpenGLという有名なグラフィックスライブラリがありますが、かなり前からAppleの端末では非推奨のライブラリとなっています。
では何を使えば良いのかというと、Apple製のグラフィックスライブラリであるMetalを使うのが推奨されているようです。
このライブラリは長らく公式ではObjCかSwiftを使って書くのが唯一の方法で、他の言語向けのバインディングなどは提供されていませんでした。
なのですが、いつの間にか公式からMetal-cppというC++バインディングが提供されていました。
これとGLFWを使って基本的な描画処理を実装します。
参考:glfw-metal-example.m
開発環境
macOS Monterey(v12.5.1)
事前準備
metal-cpp_macOS12_iOS15.zip
をダウンロードして展開する。
下記コマンドでシングルヘッダーにまとめる。
./SingleHeader/MakeSingleHeader.py Foundation/Foundation.hpp QuartzCore/QuartzCore.hpp Metal/Metal.hpp
ただし、この変換スクリプトの中身を見るとわかるのですが、
/usr/bin/python
を実行するよう指定しています。
新しめのMacOSでは組み込みの python
が削除されていて、/usr/bin/python
は存在しません。
#!/usr/bin/python
以下のように書き換えるとよしなに python
のパスを解決してくれます。
#!/usr/bin/env python
ビルド設定
適当な場所にリポジトリ作成
mkdir Metal-Study
cd Metal-Study
git init
gibo dump CMake c++ VisualStudioCode > .gitignore
git add .
git commit -m "Add: 最初のコミット"
諸々の環境をCMakeでセットアップ
※CMake で GLFW を見つけるのライブラリ検索スクリプトを使います。
mkdir CMake
curl https://raw.githubusercontent.com/benikabocha/FindGLFW_Test/master/cmake/FindGLFW.cmake > CMake/FindGLFW.cmake
touch CMakeLists.txt
touch main.cpp
CMakeの内容はこんな感じに設定。
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の動作をひとまず確認。
#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;
}
こんな感じでビルドして実行できるはず。
cmake .
make
./MetalStudy
一旦コミット。
git add .
git commit -m "Add: GLFWの動作確認できるところまで"
Metalも組み込む
事前準備で用意したヘッダーをリポジトリにコピーして、コードからも読み込みます。
// 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も変更が必要です。
※ビルドして実行しても結果は特に変わりません。
target_link_libraries(
MetalStudy
PRIVATE
#${OPENGL_LIBRARIES}
"-framework Cocoa"
"-framework Foundation"
"-framework Metal"
"-framework QuartzCore"
${GLFW_LIBRARIES}
)
一旦この時点でのファイル構成を以下に示します。
> ls
CMake CMakeFiles Makefile
cmake_install.cmake CMakeLists.txt Metal.hpp
CMakeCache.txt main.cpp MetalStudy
一旦コミット。
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
を取得する必要があります。
これは単に GLFW
の glfwGetCocoaWindow
を呼び出すだけなのですが、一つ問題があります。
MacOSX.sdkでは次のように id
型が宣言されています。
/*
* 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
型が宣言されています。
/*************************************************************************
* 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です。
ヘッダーでは基本ヘッダーのみをインクルードして
#pragma once
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>
namespace GLFW::Private {
void *getCocoaWindow(GLFWwindow *window);
}
実装ファイルからのみネイティブ機能のヘッダー(idを定義しているヘッダー)をインクルードします。
#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.hpp
と Metal.hpp
なら、同時にインクルードしてもid型の衝突が起こりません。
NSWindow
この記事の冒頭でも参考コードとしてリンクを貼ってあるのですが、
Metal
とウィンドウシステムを繋ぎ込むためにはNSWindow.cotentView.layer
をCAMetalLayer
で置き換える必要があります。
これらのクラスは Metal-cpp
で提供されていないので自分で用意する必要があります。
まずヘッダー側でクラスとセレクタを事前に定義しておく必要があります。
#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との繋ぎ込みを実装できます。
#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
に対応するクラスの定義です。
ほぼ先ほどと同じなのであまり説明することはありません。
#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
は可変長引数を取るようになっているのでそのまま引数を渡せます。
#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
を定義します。
これも基本的には今までと同じなのですが、使うマクロが違います。
#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
とは別で専用のメソッドが用意されています。
#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
一旦コミット。
git add .
git commit -m "Add: 繋ぎ込みのための機能を追加"
最後に
あとは冒頭の参考コードをそのままC++の機能に置き換えていくだけです。
// 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を追加します。
add_executable(
MetalStudy
main.cpp
CocoaWindow.cpp
NSWindow.cpp
NSView.cpp
CAMetalLayer.cpp
)
実行すると真っ赤に塗りつぶされたウィンドウが表示されるはずです。
サンプルリポジトリはこちらに置いています。