はじめに
この記事は以下記事の続きから作業を開始します。
Metal-cppとGLFWの開発環境をセットアップする
この記事ではGLMの行列をシェーダーに渡して矩形を描画するところまでを実装します。
開発環境
macOS Monterey(v12.5.1)
事前準備
まず事前知識として知っておく必要があるのですが、MetalはOpenGLとは違って生のシェーダーコードをそのままプログラムから読み込むわけではありません。
詳細は公式ドキュメントを読んでほしいのですが、とりあえずこんなスクリプトを用意しておきます。
xcrun -sdk macosx metal -c Sprite.metal -o Sprite.air
xcrun -sdk macosx metallib Sprite.air -o Sprite.metallib
これは Sprite.metal
を入力として最終的に Sprite.metallib
を出力します。
MetalのAPIから読み込むのは生のソースコードである Sprite.metal
ではなく Sprite.metallib
の方です。
*.air
と*.metallib
はリポジトリに含まれないよう無視設定をしておくと良いと思います。
ビルド設定
基本は前回プロジェクトの設定をそのまま使いますが、追加でGLMも参照できるようにします。
まずはGLMを検索するためのスクリプトを用意します。
こちらのリポジトリを参考に少し手を加えたものです。
# - FindGLM
#
# Author: Andre.Brodtkorb@sintef.no
#
# This module looks for the OpenGL Mathematics library
# It will define the following variable(s)
# GLM_INCLUDE_DIRS = where glm/glm.hpp can be found
set(GLMEX_VERSION "0.9.9.800")
find_path(
GLM_INCLUDE_DIR
NAMES
glm/glm.hpp
PATHS
"/opt/homebrew/include"
)
if(NOT GLM_INCLUDE_DIR OR GLM_INCLUDE_DIR MATCHES "NOTFOUND")
set(GLM_FOUND false)
if(NOT GLM_QUIET)
message( STATUS "GLM was not found")
elseif(GLM_REQUIRED)
message( SEND_ERROR "GLM was not found")
endif()
else()
set(GLM_FOUND true)
set(GLM_INCLUDE_DIRS ${GLM_INCLUDE_DIR})
endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(GLM DEFAULT_MSG GLM_INCLUDE_DIR)
mark_as_advanced(GLM_INCLUDE_DIR)
CMakeLists.txt
の方も手を加えます。
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b89b464..474e2d4 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -7,6 +7,7 @@ project(
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
#find_package(OpenGL REQUIRED)
find_package(GLFW REQUIRED)
+find_package(GLM REQUIRED)
add_executable(
MetalStudy
@@ -41,6 +42,7 @@ target_include_directories(
PRIVATE
#${OPENGL_INCLUDE_DIR}
${GLFW_INCLUDE_DIR}
+ ${GLM_INCLUDE_DIR}
)
target_link_libraries(
シェーダーの実装
まずシェーダーに渡すデータのための型定義を作成します。
#ifndef SHADER_TYPES_H
#define SHADER_TYPES_H
#include <simd/simd.h>
enum {
kShaderVertexInputIndexVertices = 0,
kShaderVertexInputIndexCamera = 1,
};
typedef struct {
simd::float4x4 modelMatrix;
simd::float4x4 viewMatrix;
simd::float4x4 projectionMatrix;
} CameraData;
#endif /* SHADER_TYPES_H */
シェーダーは以下のようになります。
ここで buffer
を二つ定義していますが、これをC++側から対応するインデックスで渡す必要があります。
このファイルで ShaderTypes.h
をインクルードしていますが、これをC++側からもインクルードすることで簡単に同じ番号を参照できます。
#include <metal_stdlib>
#include "ShaderTypes.h"
using namespace metal;
// Vertex関数が出力するデータの型定義
typedef struct {
// 座標
float4 position [[position]];
// 色
float4 color;
} RasterizerData;
vertex RasterizerData vertexShader(
uint vertexID [[vertex_id]],
device const float2 *vertices
[[buffer(kShaderVertexInputIndexVertices)]],
device const CameraData& cameraData
[[buffer(kShaderVertexInputIndexCamera)]])
{
RasterizerData result = {};
float4x4 mvp = cameraData.projectionMatrix * cameraData.modelMatrix;
result.color = float4(1, 1, 1, 1);
result.position = mvp * float4(vertices[vertexID], 0, 1);
return result;
}
fragment float4 fragmentShader(RasterizerData in [[stage_in]])
{
return in.color;
}
レンダラーの実装
main()にごちゃごちゃ書いていくと煩雑なのでクラスにまとめておきます。
#pragma once
// このマクロ定義が必要。詳細は以下
// https://stackoverflow.com/questions/54930382/is-the-glm-math-library-compatible-with-apples-metal-shading-language
#define GLM_FORCE_DEPTH_ZERO_TO_ONE 1
#include "CAMetalLayer.hpp"
#include "CocoaWindow.hpp"
#include "Metal.hpp"
#include "NSView.hpp"
#include "NSWindow.hpp"
#include "ShaderTypes.h"
#include <glm/glm.hpp>
#include <string>
#include <vector>
struct GLFWwindow;
class Renderer {
public:
explicit Renderer();
~Renderer();
void setup(GLFWwindow *window);
void render();
private:
void initShader(MTL::RenderPipelineDescriptor *desc, const std::string &file,
const std::string &vert, const std::string &frag);
void initBuffer();
static NS::String *makeString(const std::string &chars);
static simd::float4x4 glm2simd(const glm::mat4 &mat);
MTL::Device *m_device;
MTL::Library *m_library;
MTL::CommandQueue *m_commandQueue;
MTL::RenderPipelineState *m_renderPipelineState;
MTL::Buffer *m_vertexBuffer;
MTL::Buffer *m_indexBuffer;
MTL::Buffer *m_cameraBuffer;
CA::MetalLayer *m_layer;
};
描画の流れとしては以下のようになります。
- シェーダー初期化
- バッファー初期化
- 描画ループごとに
- バッファをシェーダーと同じ番号に割り当て
- 描画命令呼び出し
バッファーはなんとなくOpneGLの VBO
に近い感じです。
#include "Renderer.hpp"
#include <glm/ext.hpp>
#include <iostream>
#include <string>
Renderer::Renderer()
: m_device(nullptr), m_library(nullptr), m_commandQueue(nullptr),
m_renderPipelineState(nullptr), m_vertexBuffer(nullptr),
m_indexBuffer(nullptr), m_cameraBuffer(nullptr){};
Renderer::~Renderer() {
m_vertexBuffer->release();
m_indexBuffer->release();
m_library->release();
m_renderPipelineState->release();
m_commandQueue->release();
m_device->release();
m_layer->release();
}
void Renderer::setup(GLFWwindow *window) {
m_device = MTL::CreateSystemDefaultDevice();
m_commandQueue = m_device->newCommandQueue();
// シェーダー初期化
MTL::RenderPipelineDescriptor *desc =
MTL::RenderPipelineDescriptor::alloc()->init();
initShader(desc, "Sprite.metallib", "vertexShader", "fragmentShader");
// パイプライン初期化
NS::Error *pipError = nullptr;
m_renderPipelineState = m_device->newRenderPipelineState(desc, &pipError);
if (pipError) {
std::abort();
}
desc->release();
// レイヤー初期化
m_layer = CA::MetalLayer::alloc()->init();
m_layer->setDevice(m_device);
m_layer->setOpaque(true);
NS::Window *nsWindow =
NS::Window::bridgingCast(GLFW::Private::getCocoaWindow(window));
NS::View *contentView = nsWindow->contentView();
contentView->setLayer(m_layer);
contentView->setWantsLayer(true);
// 頂点情報を作成
initBuffer();
}
void Renderer::render() {
CA::MetalDrawable *surface = m_layer->nextDrawable();
MTL::CommandBuffer *buffer = m_commandQueue->commandBuffer();
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::RenderCommandEncoder *encoder = buffer->renderCommandEncoder(pass);
encoder->setRenderPipelineState(m_renderPipelineState);
encoder->setVertexBuffer(m_vertexBuffer, 0, kShaderVertexInputIndexVertices);
encoder->setVertexBuffer(m_cameraBuffer, 0, kShaderVertexInputIndexCamera);
encoder->drawIndexedPrimitives(MTL::PrimitiveType::PrimitiveTypeTriangle, 6,
MTL::IndexType::IndexTypeUInt16, m_indexBuffer,
0);
encoder->endEncoding();
buffer->presentDrawable(surface);
buffer->commit();
pass->release();
}
// private
void Renderer::initShader(MTL::RenderPipelineDescriptor *desc,
const std::string &file, const std::string &vert,
const std::string &frag) {
// シェーダーのコンパイル
NS::Error *libError = nullptr;
m_library = m_device->newLibrary(makeString(file), &libError);
if (libError) {
std::abort();
}
MTL::Function *vFunc = m_library->newFunction(makeString(vert));
MTL::Function *fFunc = m_library->newFunction(makeString(frag));
desc->setVertexFunction(vFunc);
desc->setFragmentFunction(fFunc);
desc->colorAttachments()->object(0)->setPixelFormat(
MTL::PixelFormatBGRA8Unorm);
desc->setDepthAttachmentPixelFormat(MTL::PixelFormat::PixelFormatInvalid);
}
void Renderer::initBuffer() {
const float left = 0;
const float right = 1;
const float top = 0;
const float bottom = 1;
const float verts[] = {left, top, right, top, right, bottom, left, bottom};
const uint16_t index[] = {0, 1, 2, 2, 3, 0};
const MTL::ResourceOptions bufOption = MTL::ResourceStorageModeManaged;
m_vertexBuffer = m_device->newBuffer(sizeof(float) * 8, bufOption);
m_indexBuffer = m_device->newBuffer(sizeof(uint16_t) * 6, bufOption);
m_cameraBuffer = m_device->newBuffer(sizeof(CameraData), bufOption);
::memcpy(m_vertexBuffer->contents(), verts, sizeof(float) * 8);
::memcpy(m_indexBuffer->contents(), index, sizeof(uint16_t) * 6);
m_vertexBuffer->didModifyRange(NS::Range::Make(0, m_vertexBuffer->length()));
m_indexBuffer->didModifyRange(NS::Range::Make(0, m_indexBuffer->length()));
// カメラを作成
CameraData *camera =
reinterpret_cast<CameraData *>(m_cameraBuffer->contents());
camera->modelMatrix = glm2simd(
glm::scale(glm::translate(glm::mat4(1.0f), glm::vec3((1280 - 100) / 2,
(720 - 100) / 2, 0)),
glm::vec3(100, 100, 1)));
camera->viewMatrix = glm2simd(glm::mat4(1.0f));
camera->projectionMatrix =
glm2simd(glm::ortho(0.0f, 1280.0f, 720.0f, 0.0f /* lrbt*/, 0.0f, 1.0f));
m_cameraBuffer->didModifyRange(NS::Range(0, m_cameraBuffer->length()));
}
NS::String *Renderer::makeString(const std::string &chars) {
return NS::String::alloc()->init(chars.c_str(),
NS::StringEncoding::UTF8StringEncoding);
}
simd::float4x4 Renderer::glm2simd(const glm::mat4 &mat) {
const glm::vec4 &v0 = glm::row(mat, 0);
const glm::vec4 &v1 = glm::row(mat, 1);
const glm::vec4 &v2 = glm::row(mat, 2);
const glm::vec4 &v3 = glm::row(mat, 3);
return simd_matrix_from_rows((simd::float4){v0.x, v0.y, v0.z, v0.w},
(simd::float4){v1.x, v1.y, v1.z, v1.w},
(simd::float4){v2.x, v2.y, v2.z, v2.w},
(simd::float4){v3.x, v3.y, v3.z, v3.w});
}
忘れずにCMakeにも追加しておきます。
add_executable(
MetalStudy
main.cpp
CocoaWindow.cpp
NSWindow.cpp
NSView.cpp
CAMetalLayer.cpp
Renderer.cpp
)
レンダラーの解説
GLM
※ソース中にもリンクを貼ってあるのですが、こちらの解答を参考にしています。
GLMは元々OpenGL用に作られたライブラリです。
Metalとは使っている座標系が違うためにそのままではMetal上でGLMを動かすことはできないのですが、
このマクロ定義を使うとそのあたり辻褄を合わせてくれるらしいです。
(正直私はよく分かってません。しかしこれを消すと描画がおかしくなります。)
#define GLM_FORCE_DEPTH_ZERO_TO_ONE 1
加えて、GLMとMetalの4x4行列はアライメントが異なるためにそのままでは使えないようです。
これはなんかもっといい解決方法があるかもしれないのですが、ひとまず以下で回避できています。
simd::float4x4 Renderer::glm2simd(const glm::mat4 &mat) {
const glm::vec4 &v0 = glm::row(mat, 0);
const glm::vec4 &v1 = glm::row(mat, 1);
const glm::vec4 &v2 = glm::row(mat, 2);
const glm::vec4 &v3 = glm::row(mat, 3);
return simd_matrix_from_rows((simd::float4){v0.x, v0.y, v0.z, v0.w},
(simd::float4){v1.x, v1.y, v1.z, v1.w},
(simd::float4){v2.x, v2.y, v2.z, v2.w},
(simd::float4){v3.x, v3.y, v3.z, v3.w});
}
main()
ここまでくると main()
でやることはほとんどありません。
単に描画ループごとに render()
を呼び出すだけです。
int main(int argc, char *argv[]) {
GLFWwindow *window;
int status = gl_init(argc, argv, "Sample", 1280, 720, false, &window);
if (status != 0) {
return status;
}
Renderer r;
r.setup(window);
while (!glfwWindowShouldClose(window)) {
r.render();
glfwPollEvents();
}
glfwDestroyWindow(window);
glfwTerminate();
return 0;
}
最後に
ここまで実装してビルドすると冒頭の画像のように中央に矩形が描画されます。
こちらの feature/rect
ブランチにここで実装したのと同じものを置いています。