LoginSignup
0
1

More than 1 year has passed since last update.

Metal-cppとGLMで矩形を描画する

Last updated at Posted at 2022-11-12

はじめに

この記事は以下記事の続きから作業を開始します。
Metal-cppとGLFWの開発環境をセットアップする

この記事ではGLMの行列をシェーダーに渡して矩形を描画するところまでを実装します。
2022-11-12_09-53-23.png

開発環境

.sh
macOS Monterey(v12.5.1)

事前準備

まず事前知識として知っておく必要があるのですが、MetalはOpenGLとは違って生のシェーダーコードをそのままプログラムから読み込むわけではありません。
詳細は公式ドキュメントを読んでほしいのですが、とりあえずこんなスクリプトを用意しておきます。

mtlcompile.sh
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.cmake
# - 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
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(

シェーダーの実装

まずシェーダーに渡すデータのための型定義を作成します。

ShaderTypes.h
#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++側からもインクルードすることで簡単に同じ番号を参照できます。

Sprite.metal
#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()にごちゃごちゃ書いていくと煩雑なのでクラスにまとめておきます。

Renderer.hpp
#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 に近い感じです。

Renderer.cpp
#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にも追加しておきます。

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

レンダラーの解説

GLM

※ソース中にもリンクを貼ってあるのですが、こちらの解答を参考にしています。

GLMは元々OpenGL用に作られたライブラリです。
Metalとは使っている座標系が違うためにそのままではMetal上でGLMを動かすことはできないのですが、
このマクロ定義を使うとそのあたり辻褄を合わせてくれるらしいです。
(正直私はよく分かってません。しかしこれを消すと描画がおかしくなります。)

.hpp
#define GLM_FORCE_DEPTH_ZERO_TO_ONE 1

加えて、GLMとMetalの4x4行列はアライメントが異なるためにそのままでは使えないようです。
これはなんかもっといい解決方法があるかもしれないのですが、ひとまず以下で回避できています。

.cpp
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() を呼び出すだけです。

main.cpp
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 ブランチにここで実装したのと同じものを置いています。

0
1
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
0
1