46
45

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 3 years have passed since last update.

OpenGL入門から3DCGレンダラ実装まで その1

Last updated at Posted at 2019-12-20

はじめに

この記事は、LOCAL students Advent Calendar 2019の21日目です。
https://adventar.org/calendars/4020

昨日の記事ははいばらさんによる「▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒」でした。
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒かと思ったら、▓▓▓▓▓▓▓▓▓いました。面白かったです。

一転、全く内容は変わって僕は「OpenGL入門から3DCGレンダラ実装まで 」のその1を書いていきます。学生部でアドベントカレンダーの話がでた当初、やることを先に決めた上でこれなら4日位あれば書けるかなあみたいな軽い気持ちで枠を取りましたが、結局直前まで開発をしていなかったので大変です。

趣旨

OpenGLやDirect Xを始めとするグラフィックスライブラリに全く詳しくない(というかまともに触ったことがない)初心者が、勉強しながら作成する3DCGレンダラを少しづつ解説していく記事です。4本立てです。
なんでこんなことをやろうと思ったかと言うと、趣味のゲーム制作で利用しているゲームエンジンへの理解をより深め、開発に活かしたいと考えたからです。この趣旨から、実装だけでなく文章も多い記事になっていくと思われます1

有識者の方、間違い等あれば指摘していただけると大変助かります。

前提知識

  • ベクトル、内積/外積、行列、線形写像、アフィン写像など線形代数に関わる諸知識
  • 基礎的なC++知識
  • 基礎的なLinux上での開発知識

それぞれの概念への解説は行いませんが、どんな処理をしているのかは説明しますので、詳細は分からなくても読むことはできます。というか僕が完全には理解できていません。

テスト環境

  • Arch Linux x86_64 (linux 5.4.2.arch1-1)
  • OpenGL 4.6.0 (NVIDIA 440.36)
  • gcc 9.2.0

内容・目標

このシリーズは4本立てで、以下の順で簡易的なライティングを施したボックスが表示できるところまでを実装します。

記事 内容
その1 OpenGL入門からウィンドウ表示
その2 平面ポリゴン表示からGLSLシェーダとテクスチャマッピング
その3 立方体ポリゴンの表示と回転・移動
その4 簡易的なライティングの実装

今回は初回ということで、そもそもOpenGLとはなんぞや、OpenGLを使った開発ではどんなものを使うのか、周辺ライブラリは何があるのか?などについての情報を整理してみようと思います。また、次回から本格的な実装を始めるための環境構築も今回行います。

また、ソースコードやリソースなどを各回の内容ごとに纏めたものをGitHubに公開していますので、記事と並べて見ていただくとわかりやすいかと思います。

今回の内容のソースはこちらです。

そもそもOpenGLとは?

概要

OpenGLとは、Khronos Groupという団体が策定している2D/3D グラフィックスライブラリのオープン標準規格です。
つまり、Khronos GroupはOpenGLというライブラリそのものの実装は行なっておらず、どのような命令でどのような効果が得られるのかというAPIの策定と公開を行なっているということになります。

通常、私達が利用することになるライブラリとしてのOpenGLは、GPUの各開発企業が自社のGPU向けに実装を行なったものとなります2
OpenGLはオープン標準でありどの企業も自社のGPUで利用できるようにすることができます。しかし、世に存在するOpenGLを使いたいGPU全てで動く実装を1つの団体が公開するのは事実上不可能であるため、このような形になっていると思われます。

OpenGL自体は、GPUなどのハードウェアやそれを制御するデバイスドライバに近い低レベルなAPIです3

アーキテクチャ

コンテキスト

OpenGLは、それ自体が巨大なステートマシンとなっています。内部的にOpenGLのステートを示す変数を持つ巨大なデータ構造を保持しており、このデータ構造のことをOpenGLではコンテキストと言います。OpenGLを用いたプログラミングでは、用意されたAPIを用いてOpenGLに命令を発行し、コンテキストを更新することで描画を制御することになります。
コンテキストは同時に複数発行することが可能で、例えば同時にOpenGLを用いたウィンドウを複数起動する場合、ウィンドウごとに更新対象とするコンテキストをスイッチングすることでOpenGLを用いたGPUレンダリングを同時に行うことができます。

言葉ではあまりしっくりこないかもしれませんが、この点は実際にOpenGLでプログラムを記述するとよくわかります4

プロファイルとレンダリングパイプライン

OpenGLにおけるプロファイルとは、簡単に言うとコンテキストが保持する状態の種類やそれに対する制御を行うインターフェースの種類などによって区別されるAPIセットのことです。
2019年現在のOpenGLには、コアプロファイル(Core contexts)と互換プロファイルの2つのプロファイルが存在します。これらのプロファイルはそれぞれ別の仕様として独立してOpenGLの各バージョンごとに策定されています。以下の画像はKhronos Groupページで公開されている仕様一覧の様子です。
DeepinScreenshot_select-area_20191215065917.png

同一のバージョンに対して複数のAPIが用意されているのは一見不思議ですが、これにはOpenGLの歴史的経緯が関係しています。OpenGLはバージョン3.0以上と未満で大きく思想が異なっており、そこにAPI的な互換性はありません。つまり、OpenGL 2.xのころのコードをOpenGL 3.xや4.x環境で普通に動かそうとしても、APIが変化しているためビルドできないということです。5
そして、各バージョンが提供しているメインの最新API仕様、これがコア仕様であり、コアプロファイルであるということになります。

しかし、高バージョンのOpenGLが提供する機能を、OpenGL 2.x以前のAPI仕様に則ったコードから利用したい、3.x以上への対応のためにコードを書き換えたくないという需要も存在します。そのようなケースのために今現在も用意されているのが各バージョンの互換プロファイルであるということのようです。

コアプロファイルと互換プロファイルの主な違いとして、主要なレンダリングパイプラインの違いがあります。

レンダリングパイプラインとは頂点データやテクスチャデータなどの元データから、実際に画面に描画する2次元の画像データを生成するまでの過程のことです。

コアプロファイルで採用されているレンダリングパイプラインは**GLSL(OpenGL Shading Language)**と呼ばれるGPU上で実行される専用のプログラミング言語(シェーダー言語)を用いたもので、このようなプログラマによって設計・記述が可能なシェーダー言語を用いたレンダリングパイプラインは「プログラマブルシェーダを用いたレンダリングパイプライン」であると言われます。

一方、互換プロファイルで利用できるレンダリングパイプラインは**固定機能パイプライン(Fixed function pipeline)**であると言われます。
それぞれのパイプラインの特徴を以下に示します。

固定機能パイプライン

  • 単純な命令ですぐに描画を行うことができる。OpenGLにおけるHello World的なところに到達するまでのコストは低く、はじめやすい。
  • 多くの部分がプログラマブルシェーダーを用いる場合と比較して抽象化されOpenGL内部に隠されている。プログラマが細かい制御をいじることが出来ない。
  • OpenGLが実際に内部で何をやっているのかわかりにくく、パフォーマンスチューニング可能な範囲や柔軟性などに限界がある。

プログラマブルシェーダー

  • Hello Worldまでの学習コストが固定機能パイプラインと比較して高い。少し描画を行うだけでもGLSLと呼ばれるシェーダー言語を記述する必要がある。
  • 抽象度が固定機能パイプラインと比較して低く、細かい命令をプログラマがOpenGLに与えなければならない。
  • シェーダーを用いると固定機能パイプラインでは殆ど固定されていたGPU内部でのレンダリングパイプラインの処理をプログラマが設計し記述することができる。6
  • 非常に柔軟性が高く表現の幅が広い。また、適切に記述すれば効率的な処理を作ることができる。

プログラマブルシェーダーを用いたコアプロファイルのレンダリングパイプラインは学習コストが若干高いですが、現在推奨される方法であり、実際にGPUを用いたレンダリングで何をやっているのかの概要を理解するにもうってつけのものになっているため、今回の記事シリーズではコアプロファイルを採用してやっていきます。7

補助ライブラリ

ここまでOpenGLについて記述してきましたが、OpenGLがハードウェアやOSと近いものである特性上、ライブラリとしての取り扱いも特殊な箇所があります。直接的にプラットフォーム間の環境の差異を受けるため、OpenGLを用いたプログラミングでは本来プラットフォーム固有の実装を記述する必要があります。
しかし、これはかなり面倒なことですし、情報共有やクロスプラットフォーム性と言った面でも非常に効率が悪くなります。そこで、OpenGLにはいくつかの領域において補助ライブラリが存在します。

これは公式の仕様によるものではありませんが、一般的に広く使われており、公式もこういった補助ライブラリの利用を強く推奨しています。とはいえ、決してOpenGLを簡単な描画ライブラリとしてラップするような性質のものではなく、様々な種類があるOSやGPUを”GPUを使って描画や処理を行う環境”として抽象化してくれるに過ぎないという点には注意が必要です。

OpenGL Load Library

一般的なC/C++のライブラリであれば、そのライブラリのヘッダをインクルードし、適切にリンカが参照を解決できるように設定すればライブラリの関数を利用することができます。
しかし、OpenGLはそれでは利用できません。これはプラットフォームによってOSやハードウェアの実行可能な命令が異なることに由来しているようで、OpenGLは実行時に利用する関数のポインタを生成し、変数に割り当てることで呼び出し可能オブジェクトの作成を行います。本来、プログラマはこの初期化処理を実装しないとOpenGLの関数を使うことすらできません。更に、このロード処理自体もプラットフォームによって異なる実装が必要となります。

これらは**OpenGL ロードライブラリ(OpenGL Loading Library)**と呼ばれる部類の補助ライブラリを利用することで自動化することが可能で、クロスプラットフォーム対応も容易となります。
以下に、いくつかの主要なOpenGLロードライブラリを紹介します。

GLEW(OpenGL Extension Wrangler)

公式ページ: http://glew.sourceforge.net/

Windows, Linux, Mac OS X, FreeBSD, Irix, Solarisなど多くのプラットフォームをサポートするOpenGLロードライブラリです。
インクルードして以下のように初期化関数を呼び出すことで、そのプラットフォームで利用可能なOpenGLの関数を読み込んで利用可能にすることができます。

Example
GLenum err = glewInit();
if (GLEW_OK != err)
{
  /* 初期化に失敗した場合の処理 */
  fprintf(stderr, "Error: %s\n", glewGetErrorString(err));
}

glad (Multi-Language Vulkan/GL/GLES/EGL/GLX/WGL Loader-Generator)

公式ページ: https://github.com/Dav1dde/glad

多言語対応のロードライブラリジェネレータです。ジェネレータというのはどういうことかと言うと、利用したい機能やプロファイル、APIバージョンなどを指定することで、使いたい機能のみを含んだロードライブラリを生成できるバックエンドであるということです。
このバックエンドのインターフェースはコマンドラインやWebサービスなど複数用意されており、利用したい手段を選んでローダーを生成することができます。
以下の画像はgladのWebインターフェースで、各設定を簡単に選択できるようになっていることがわかります。
DeepinScreenshot_select-area_20191216071639.png

必要なものだけを選んでライブラリを生成することができるため、GLEWと比較してライブラリサイズが小さく軽量です。また、C/C++以外の言語やOpenGL以外のGL系インターフェースへの対応も行なわれているため、そういった利用の場合にも便利です。

今回の実装ではgladを利用します。

Context/Window Toolkits Library

OpenGLはコンテキストを対象としてGPUを利用した処理を記述することができますが、コンテキスト自体の作成や、出来上がった画像をウィンドウとして画面に表示したりするところはサポートしてくれません。コンテキストの作成やウィンドウ表示/管理、入出力などはOS固有の手法が必要とされるため、この部分でも補助ライブラリを利用することが推奨されています。

freeglut

公式ページ: http://freeglut.sourceforge.net/

過去に主流だったGLUTというライブラリの後継で、ウィンドウ管理やキーボード/マウス入力などに対するサポートが含まれています。OpenGLコンテキストの生成にも対応しています。GLUTの後継であるというポリシーから、GLUTのインターフェースをあまり変えないように設計されています。GLUTから遡るとOpenGL自体と同じくらい歴史があり、情報が比較的豊富なようです。

GLFW

公式ページ: https://www.glfw.org/

GLUTよりも新しく、GLUTなど他のライブラリでの経験を踏まえて設計されています。Linux, macOS, Microsoft Windows, FreeBSD, NetBSD, OpenBSDに対応し、頻繁にアップデートされています。マウス/キーボードに加えてジョイスティック入力へのサポートなども存在しています。
freeglutと比較してコンテキスト作成により詳細な属性設定が可能であったり、イベントループの扱いの違いによってより低レイテンシな入力処理対応が可能であったりするようです。

今回の実装ではGLFWを利用します。

環境構築

さて、説明も終わって早速実装と行きたいところですが、まずは環境を作らなければなりません。以下では、今回使用するものについて簡単に触れます。

インストールするもの

OpenGL本体

だいたい入ってるんじゃないかと思いますが、お使いの環境で利用可能な実装をインストールしてください。僕は今回NVIDIAのプロプライエタリドライバに含まれているOpenGL 4.6の実装を利用しています。古いデバイスをお使いの場合4.6が利用できない可能性がありますが、今回の記事では最新の拡張などは利用しないので3.x以上であれば問題ないと思われます。

補助ライブラリ系

以下のものを利用します。

  • glad
  • GLFW
  • glm

上で説明した補助ライブラリに加えて、行列演算など数学系の処理を実装したライブラリであるglmも利用します。また、GLFWはご利用のディスプレイサーバに合ったものを選択するよう注意してください。

GLFWとglmについては、僕はArch Linux公式のリポジトリからパッケージマネージャを用いてインストールしました。
gladは各ディストロのリポジトリに存在しない可能性が高いためセットアップを説明します。
まず、このサイトで以下のような設定を行なった上でGENERATEボタンを押すことで必要なものを含んだライブラリを生成することができます。プロファイルはコアプロファイルで固定ですが、OpenGLのバージョンについてはご自身の環境に合ったものを選択してください。僕と同じ環境で大丈夫な場合は、このパーマリンクから僕が生成したものと全く同じものにアクセスすることもできます。
DeepinScreenshot_select-area_20191217222125.png

ファイルが生成されるとページが遷移し表示されるので、zipファイルをクリックしてダウンロードします。ダウンロードが完了したら、今回の開発で利用する作業ディレクトリをお好きなところに作成し、その直下にglad.zipを以下のように展開します。

作業ディレクトリ直下のようす
advent_gl/
└── glad/
    ├── include/
    └── src/

以上です。この配置を前提にこの後CMakeのセットアップなどを行います。

コンパイラ/ビルドツール系

適当に入れます。僕はArch Linux公式のリポジトリからパッケージマネージャを用いてインストールしました。今回の開発ではビルド等はCUIから行います。

  • CMake
  • make
  • gcc

ビルド環境の作成

今回はCMakeを用いたビルド環境で開発を行います。CMakeについての細かい解説は行いませんので、わからなければ適時調べてください。

CMakeLists.txtの作成

CMakeを用いたビルドを行うため、CMakeLists.txtを作製します。また、gladはプロジェクトと一緒にソースからビルドを行う必要があるため、サブディレクトリとして専用のCMakeLists.txtを用意します。作業ディレクトリ内の構成は以下のようになります。

advent_ogl/
├── CMakeLists.txt
└── glad/
    ├── CMakeLists.txt
    ├── include/
    └── src/

追加した2つのCMakeLists.txtの内容は、以下に添付しておきます。このCMakeLists.txtは、作業ディレクトリ直下に配置されたmain.cppファイルを実行ファイルとしてビルドするように記述されています。

まずはメインとなる、プロジェクトビルド用のCMakeLists.txtです。

./CMakeLists.txt
cmake_minimum_required(VERSION 3.12)

# プロジェクト名の設定
project(advent_gl)

# 必須ライブラリの存在チェック
find_package(glfw3 REQUIRED)
find_package(glm REQUIRED)

# glad関係
include_directories(glad/include)
add_subdirectory(glad)

# コンパイルオプション
add_compile_options(-O2 -Wall)

# 実行ファイルの指定
add_executable(advent_gl main.cpp)

# 実行ファイルにリンクするライブラリの指定
target_link_libraries(advent_gl glad glfw glm ${CMAKE_DL_LIBS})

# c++17を使う
set_property(TARGET advent_gl PROPERTY CXX_STANDARD 17)

続いて、gladをライブラリとして認識させてリンクするためのCMakeLists.txtです。

./glad/CMakeLists.txt
cmake_minimum_required(VERSION 3.12)

# gladという名前でCMakeに認識させるライブラリを作成
# 静的ライブラリとしてコンパイル。含まれるソースを指定。
add_library(glad STATIC
    src/glad.c
)

以上CMakeによるプロジェクトのセットアップです。大変シンプルですね。

ウィンドウを表示する

やっとc++のコードを書くことができます。ここでは、これからレンダラとなっていくソフトウェアのベースであるウィンドウ表示をやってみます。
はじめに今回利用するコードの全文を載せておきます。このコードは作業ディレクトリの直下にmain.cppという名前で作成します。

main.cpp
#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <glm/glm.hpp>
#include <iostream>

const unsigned int WINDOW_WIDTH = 1440;
const unsigned int WINDOW_HEIGHT = 810;

void keyHandler(GLFWwindow*, int, int, int, int);

int main()
{
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 4);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);

    GLFWwindow* window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "advent_gl", nullptr, nullptr);

    if (!window)
    {
        std::cerr << "Failed to create window." << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetKeyCallback(window, keyHandler);

    if (!gladLoadGLLoader(reinterpret_cast<GLADloadproc>(glfwGetProcAddress)))
    {
        std::cerr << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    while (!glfwWindowShouldClose(window))
    {
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glfwTerminate();
    return 0;
}

void keyHandler(GLFWwindow *window, int key, int scancode, int action, int mods)
{
    switch (key)
    {
        case GLFW_KEY_ESCAPE:
            if (action == GLFW_PRESS)
            {
                glfwSetWindowShouldClose(window, GLFW_TRUE);
            }
            break;
    }
}

まだ真っ黒なウィンドウを出すだと言うのに、それなりのコード量がありますね。一つづつ見ていきましょう。

ライブラリのインクルード

以下の部分です。

main.cpp(ライブラリインクルード部)
#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <glm/glm.hpp>

「いや、流石にそんな事知っているしみればわかるが」と思われたかもしれませんが、実は注意が必要です。
gladは自分よりも前にOpenGLを扱う(具体的にはgl.hをインクルードする)ライブラリが存在することを許さないため、基本的にgladを一番始めにインクルードしなければなりません。この順序を変えるとビルドが通らなくなります8

ウィンドウ/コンテキストを作成

まずは以下の箇所です。ここはOSやディスプレイサーバーに合ったウィンドウの作成と、これからOpenGLで操作していくコンテキストを作成している部分になります。

main.cpp(ウィンドウ/コンテキスト作成部)
    glfwInit(); 
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 4);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);

    GLFWwindow* window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "advent_gl", nullptr, nullptr);

    if (!window)
    {
        std::cerr << "Failed to create window." << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetKeyCallback(window, keyHandler);

まずglfwInit()という関数を呼んでいます。これはほぼすべてのGLFWの関数を呼ぶ前に呼んでおく必要のある関数9で、GLFWの初期化処理を行います。この初期化処理では実行されたコンピュータの環境をチェックし、利用可能なOpenGLの機能やキーボードやマウスなど入出力装置の取得、モニタの認識など重要な作業が行なわれます。GLFWを使えば、クロスプラットフォームで動くこれらの処理をこの1行でやってくれてしまうのです。最高ですね。

続いてglfwWindowHint()というのが何やらやっています。これは、GLFWで実際にウィンドウを作成する前に、これから作るウィンドウやそれに関連付けられるコンテキストの設定をGLFWに教えてあげる関数です。設定項目はここに一覧がありますが、もちろんすべてを設定する必要はありません。いくつかの必要な情報を選び取って設定します。今回は以下のような設定を行いました。

設定項目 意味 今回の設定値
GLFW_CONTEXT_VERSION_MAJOR 利用するOpenGLの最大バージョン 4
GLFW_CONTEXT_VERSION_MINOR 利用するOpenGLの最小バージョン 4
GLFW_OPENGL_PROFILE 利用するOpenGLプロファイル。GLFW_OPENGL_FORWARD_COMPAT(互換プロファイル)とGLFW_OPENGL_CORE_PROFILE(コアプロファイル)が指定可能。 GLFW_OPENGL_CORE_PROFILE
GLFW_RESIZABLE 作成するウィンドウのサイズ変更が外部から可能かどうか。GLFW_TRUEGLFW_FALSEを設定可能。10 GLFW_FALSE

これらの設定を行なった上で、満を持してglfwCreateWindow()でウィンドウとそれに紐づけられるコンテキストを作成します。ここではウィンドウサイズやウィンドウタイトルなどを設定しています。nullptrになっている部分は関連付けるウィンドウや対象とするモニタなどを設定できるのですが、今回は特に必要ないため指定していません。

これでOSやディスプレイサーバーに合ったウィンドウ/コンテキストを作成することが出来ました。しかし、まだこれではコンテキストが作成されただけで、これからこれを利用する設定は出来ていません。それを行うのがglfwMakeContextCurrent()です。これが呼び出されると、呼び出し元のスレッドに渡されたウィンドウが持つコンテキストが関連付けられます。今回の開発ではそこまでやりませんが、複数のコンテキストを扱う場合などにはこういった関数を各所で呼び出してコンテキストの取り回しを行うようです。

最後に、glfwSetKeyCallback()で作製したウィンドウに対する入力をハンドリングするコールバック関数を設定しています。 このコールバック関数は以下です。

main.cpp(keyHandler関数)
void keyHandler(GLFWwindow *window, int key, int scancode, int action, int mods)
{
    switch (key)
    {
        case GLFW_KEY_ESCAPE:
            if (action == GLFW_PRESS)
            {
                glfwSetWindowShouldClose(window, GLFW_TRUE);
            }
            break;
    }
}

大変単純ですね。コールバック引数として受けとった値を元に、エスケープキーが押されたらglfwSetWindowShouldClose()を呼び出しています。この関数にGLFW_TRUEをセットすると、対象のウィンドウにクローズのフラグ立ちます。このフラグにはウィンドウのオブジェクトを通して他の場所からアクセスできるため、このフラグを監視することでウィンドウの終了処理を書くことが出来ます。
もちろんこのハンドラはあらゆるキー入力に反応するので、ケースを増やすことで様々なキー入力に対応することができます。GLFWで定義されているキーの一覧はここにあります11

OpenGL関数のロード

いよいよOpenGL関数のロード処理です。これは以下の部分で行なっています。

main.cpp(OpenGL関数のロード部)
    if (!gladLoadGLLoader(reinterpret_cast<GLADloadproc>(glfwGetProcAddress)))
    {
        std::cerr << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

ちょっと不思議なコードに感じないでしょうか。glfwGetProcAddressと言うのに違和感があるかと思います。お察しの通りこれはGLFWの関数で、それをGLADloadprocという型に無理やりキャストした上でgladのgladLoadGLLoader()という関数に渡しています。ライブラリをまたいでいるし、なんだかよくわかりません。

実は、glfwGetProcAddressというのは以下のような文字列を受け取る関数です。この文字列はOpenGLの特定の機能を指し示す固有の文字列で、この関数は渡された文字列に該当する機能の関数があればその関数のポインタを返すという動作をします。

GLFWglproc glfwGetProcAddress(const char * procname)

そして、gladはOpenGLの関数や拡張機能をロードするライブラリで、特に自分が使いたいものだけを選んで生成した上で利用することができるものでした。そう、ここでgladにglfwGetProcAddressを渡すことで、glad内部で読み込むことになっている機能名を片っ端からこれに突っ込んで、OpenGLの関数をロードしているのです。つまりは、GLFWglprocとかいうやつにOpenGLの関数ポインタが入って返ってくるということですね。これを踏まえて読めば納得のコードです。

メイン処理ループ

いよいよメインの処理ループの部分です。以下のコードが該当します。

main.cpp(メイン処理ループ部)
    while (!glfwWindowShouldClose(window))
    {
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

過去最高級にシンプルです。
まず、このループはglfwWindowShouldClose()の返す値がGLFW_FALSEである間だけ回り続けます。似たような関数名を先程見ましたね。これは渡されたウィンドウに存在するクローズのフラグの状態を取得する関数です。つまり、先程設定したコールバック内でエスケープキーが押されるとフラグが立ち、このループが終了するということになります。

内部ではglfwSwapBuffers()glfwPollEvents()というのを呼んでいます。glfwSwapBuffers()は今説明すると話がややこしくなるので次回以降に取っておきます。
glfwPollEvents()の方は、現在GLFWがイベントキューに受け取っているイベントを元に、設定された各種コールバックの呼び出しなどを行う関数です。ループ内で繰り返し呼び出され、新たにキューに入ってきたイベントがあれば処理を行い、キューから取り出すということをやっています。
これを呼び出さないと先程glfwSetKeyCallback()で設定した関数は永遠に呼び出されず、キー入力をハンドリングすることはできません。気をつけましょう。

実行してみる

さて、準備は整いました。ウィンドウを表示するコードも書いたし、ビルド環境も作りました。以下の手順でビルドが可能です。

まず。作業ディレクトリ直下にbuildというディレクトリを作成し、その中に移動します12。この時点で以下のような状態になっているはずです。

作業ディレクトリの状態
advent_ogl/
├── build/ ←いまここにいる
├── CMakeLists.txt
├── glad/
│   ├── CMakeLists.txt
│   ├── include/
│   └── src/
└── main.cpp

そこで以下の順にコマンドを実行します。

ビルドから実行まで
$ cmake ..
$ make
$ ./advent_gl

すると、以下のように真っ黒な画面が表示されるはずです!

DeepinScreenshot_select-area_20191219024326.png

表示された方、やりました!我々の勝利です!されなかった方、残念です。何かわからないことがあれば聞いてください。僕もわからないかもしれません。

一応、先程入力したコマンドについて説明しておきます。
はじめのcmake ..というのは、作業ディレクトリ直下に作成したメインのCMakeLists.txtに対するCMakeの処理を走らせているコマンドです。これにより、記述された設定内容に従ってMakefileなどが生成されます。
2つ目のコマンドは生成されたMakefileに対する処理です。CMakeがビルドのためのMakefileを生成してくれたので、使うだけです。これを実行すると、同じディレクトリにadvent_glという名前の実行バイナリが出来上がるはずです。
3つ目は説明不要ですね。実行しただけです。

まとめ

さて、周辺知識から長々とやってきましたが、まだウィンドウを表示しただけで、OpenGL的にはHelloWorldにも到達できていません。
次回からはいよいよレンダラの実装をやっていきます。はやくHelloWorldしたいですね。今回の内容でOpenGLのイメージや周辺状況を掴んでいただけていたら嬉しいです。僕も大変勉強になりました。
OpenGL、わくわくしますね。

参考文献

この記事の執筆にあたって、以下のページを参考にさせていだきました。

”Khronos OpenGL® Registry” Khronos Group. https://www.khronos.org/registry/OpenGL/index_gl.php
"Rendering Pipeline Overview"
OpenGL Wiki. https://www.khronos.org/opengl/wiki/Rendering_Pipeline_Overview
"Fixed Function Pipeline" OpenGL Wiki. https://www.khronos.org/opengl/wiki/Fixed_Function_Pipeline
"OpenGL Context" OpenGL Wiki. https://www.khronos.org/opengl/wiki/OpenGL_Context
"OpenGL" LEARN OpenGL. https://learnopengl.com/Getting-started/OpenGL
”Load OpenGL Functions” OpenGL Wiki. https://www.khronos.org/opengl/wiki/Load_OpenGL_Functions
"OpenGL Loading Library" OpenGL Wiki. https://www.khronos.org/opengl/wiki/OpenGL_Loading_Library
"Related toolkits and APIs" OpenGL Wiki. https://www.khronos.org/opengl/wiki/Related_toolkits_and_APIs
”The OpenGL Extension Wrangler Library” glew. http://glew.sourceforge.net/
"FreeGlut" wikipedia https://en.wikipedia.org/wiki/FreeGLUT
"GLEW" wikipedia https://en.wikipedia.org/wiki/GLFW
"GLFW: Window Reference" GLFW. https://www.glfw.org/docs/latest/group__window.html
”GLFW: Window guide” GLFW. https://www.glfw.org/docs/latest/window_guide.html
”GLFW: Keyboard keys” GLFW. https://www.glfw.org/docs/latest/group__keys.html

  1. え、なんで今から始めるのにVulkanじゃないのかって?それは、はじめて低レベルグラフィックスAPIを触るには低レベルすぎるからです。OpenGLをある程度理解したら時代に追いつこうと思います。

  2. ただし、必ずしもOpenGLのインターフェース部分を各社が実装しているとは限らず、Linux環境であればNVIDIA公式ドライバ以外はMesaというOpenGL実装を経由し、各デバイスの固有バックエンド実装を叩いているようなこともあります。尤も、OpenGLはPC環境以外でも多用るため、そのような場合には殆どGPUメーカーが規格に則ったOpenGL実装を提供している場合がほとんどなようです。また、組込みシステムなどに向けたOpenGLのサブセットであるOpenGL ESというものも存在します。

  3. OpenGLでもまだ高レベルすぎる!もっと細かくプログラマがGPUに関する処理を制御できるようにしたい!という需要や、長年のアップデートで蓄積したレガシーな設計の刷新のために、OpenGLの後継となるVulkanというAPIも策定されています。こちらはOpenGLが低レベルながら行なってくれている抽象化処理を更に分割し、ハードウェアを直接操作するような更なる低レベルAPIとなっています。

  4. 参照透過性が担保されないことが担保されています。

  5. 具体的にはOpenGL 3.0で従来機能が非推奨となり、OpenGL 3.1で完全に従来機能が削除。しかしOpenGL 3.2でコア仕様のAPIに加えて互換APIが追加され、ここで初めてプロファイルと言う概念が誕生したようです。

  6. 固定機能パイプラインでも内部的にはOpenGLが用意したシェーダーが利用されていますが、このシェーダーはOpenGLによって固定されています。それと比較した語として、プログラマによって記述可能なGLSLなどのシェーダー言語を用いたシェーダーのことをプログラマブルシェーダーと言います。

  7. OpenGLの書籍を購入したり検索したりなどした場合、特に断りがなく固定機能パイプラインを用いたレンダリングの解説がされている場合があります。古い情報であればそれが記述された時代にはそれしかなかったため仕方ないことであり、我々学習者は対象としているプロファイルとバージョンによく気をつけて読み進める必要があります。

  8. 「綺麗なコードを保持するためにLinterとFormatterを使う」という人は結構いると思いますが、多くのc++のFormatterは、インクルード順序に依存するお行儀の悪いヘッダを前提にしていません(この場合仕方ないですが……)。Formatterにソースを破壊されないように気をつけましょう。

  9. glfwInit()を呼ぶ前に呼び出せる関数としては、GLFWのバージョンを取得する関数や、GLFWで発生したエラーをハンドリングするコールバック関数の設定などがあります。

  10. 今回これをわざわざ設定したのは、僕が開発環境にタイル型wmを採用しており、これを設定しないと起動時に勝手に変なサイズに変形させられてしうからです。これを設定することでfloatモードでウィンドウが表示されます。これを探すのにちょっと時間かかった。

  11. なお、終了処理を記述し忘れるとタスクを外部から殺さなければ絶対に閉じられない無限ループ高負荷ウィンドウが生成されます。

  12. こういった、ビルド用のディレクトリを分けてビルドを行う手順のことをout-of-sourceビルドと言ったりします。ビルドの生成物とソースを完全に分離可能であるため大変取り回しが良いです。

46
45
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
46
45

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?