はじめに
点群データとは、3次元空間内の多数の点(点群)の座標情報を持つデータのことです。
各点には位置情報(X, Y, Z)や色情報(R, G, B)が含まれ、これらの点が集まることで物体や環境の3Dモデルを形成します。建設業や自動運転、AR/VR、文化財保存など、さまざまな用途で広く利用されています。
一方で、点群データを扱う際の課題の1つとして、データ量の大きさが挙げられます。非常に大きなデータ量を持つ点群データをスムーズに描画するには、高速描画に長けたライブラリの使用や高性能なハードウェアが必要です。
今回は、スムーズに点群を描画する一つの手段として、OpenGLを使用する方法についてご紹介します。
OpenGLとは...
OpenGLは、2Dおよび3Dグラフィックスを描画するためのクロスプラットフォームAPIです。ゲーム開発やシミュレーション、CADソフトウェアなど、さまざまな分野で広く使用されています。
開発環境
- Visual Studio 2017
- OpenGL 4.6
- GLFW 3.3.8
- glm 0.9.9.800
- nupengl.core 0.1.0.1
- nupengl.core.redist 0.1.0.1
幾何変換
まず、3次元の座標において、2次元と大きく異なるのは視点です。2次元座標は、大抵左上が (0, 0) となりますが、3次元の世界では視点そのものが移動できてしまいます。たとえオブジェクトが動かなくても、カメラを移動させれば映るものが変化するということです。
そのため、以下の3つの行列を使用して幾何変換を行う必要があります。
- モデル行列(Model Matrix) : オブジェクトの位置、回転、スケーリングを表します。
- ビュー行列(View Matrix) : カメラの位置と向きを表します。
- 射影行列(Projection Matrix) : 3D空間を2Dスクリーンに投影するための行列です。
この記事では、以下の手順で処理していきます。
頂点情報格納→モデル行列→ビュー行列→射影行列→描画
ソースコードで書くとこんな感じになります。
// モデル行列の例
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, glm::vec3(1.0f, 0.0f, 0.0f)); // 平行移動
model = glm::rotate(model, glm::radians(45.0f), glm::vec3(0.0f, 1.0f, 0.0f)); // 回転
// ビュー行列の例
glm::mat4 view = glm::lookAt(
glm::vec3(0.0f, 0.0f, 3.0f), // カメラの位置
glm::vec3(0.0f, 0.0f, 0.0f), // 注視点
glm::vec3(0.0f, 1.0f, 0.0f) // 上方向
);
// 射影行列の例
glm::mat4 projection = glm::perspective(
glm::radians(45.0f), // 視野角
800.0f / 600.0f, // アスペクト比
0.1f, // 近クリップ面
100.0f // 遠クリップ面
);
シェーダー
シェーダーとは、3Dオブジェクトがディスプレイに映し出されるときの陰影処理をするためのプログラムのことです。シェーダーは、GLSL(OpenGL Shading Language)と呼ばれる言語でプログラムできます。CやJavaとは異なり、GLSLはアプリケーションを起動するたびにコンパイルされます。
OpenGLでは「どのモデルをどこに描画するか」を決め、シェーダーではそのモデルを正しい位置に、指定された色で描画します。
主に以下の2種類があります。
- バーテックスシェーダー(Vertex Shader) : 頂点ごとに実行され、頂点の位置や属性を計算します。
- フラグメントシェーダー(Fragment Shader) : ピクセルごとに実行され、ピクセルの色を計算します。
以下にGLSLで記述されたシェーダーの簡単な例を示します。
バーテックスシェーダーの例:
#version 150 core
in vec4 position;
uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
void main()
{
gl_Position = projection * view * model * position;
}
フラグメントシェーダーの例:
#version 150 core
out vec4 fragment;
void main()
{
fragment = vec4(1.0, 0.0, 0.0, 1.0); // 赤色
}
OpenGLを使用して点群を表示する
OpenGLを使用して点群を表示してみます。以下にソースコードの例を記載します。
- main.cpp
#include <iostream>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtx/transform.hpp>
#include <vector>
#include "window_manager.hpp"
#include "Object.h"
#include "utilityh.h"
#include "shader_reader.h"
GLfloat scale = 45.0f;
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
scale += yoffset;
std::cout << scale << std::endl;
}
int main(int argc, char* argv[])
{
// ウィンドウサイズ
int windowWidth = 1280;
int windowHeight = 960;
// OpenGL Windows初期化
WindowManager windowManager;
if (!windowManager.InitializeGLFW(windowWidth, windowHeight))
{
std::cerr << "[ERROR] GLFW initialization failed." << std::endl;
exit(1);
}
glfwSetScrollCallback(windowManager.window, scroll_callback);
//! 描画用データの読み込み
std::string filename = R"(.\data\test_bunny.ply)";
//std::vector<PointCloudObject::Vertex> points = { { -0.5f, -0.5f, 0.f }, { 0.5f, -0.5f, 0.f }, { 0.5f, 0.5f, 0.f }, { -0.5f, 0.5f, 0.f } };
std::vector<PointCloudObject::Vertex> points;
points = ReadPLYData(filename);
// プログラムオブジェクトを作成する
Shader shader;
const GLuint program(shader.loadProgram("point.vert", "point.frag"));
// "MVP" uniformへのハンドルを取得
GLuint modelMatrixID = glGetUniformLocation(program, "model");
GLuint viewMatrixID = glGetUniformLocation(program, "view");
GLuint projectionMatrixID = glGetUniformLocation(program, "projection");
// 矩形の頂点の位置
//constexpr PointCloudObject::Vertex rectangleVertex[] =
//{
// { -0.5f, -0.5f, 0.f },
// { 0.5f, -0.5f, 0.f },
// { 0.5f, 0.5f, 0.f },
// { -0.5f, 0.5f, 0.f }
//};
PointCloudObject object(3, points.size(), points);
int renderBufferWidth, renderBufferHeight;
// フレームループ
while (!glfwWindowShouldClose(windowManager.window))
{
glfwGetFramebufferSize(windowManager.window, &renderBufferWidth, &renderBufferHeight);
// ビューポートを更新する
glViewport(0, 0, renderBufferWidth, renderBufferHeight);
// 陰面消去を有効にする
glEnable(GL_DEPTH_TEST);
// バッファのクリア
glClearColor(0.f, 0.f, 0.f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// シェーダープログラムの使用開始
glUseProgram(program);
// モデル行列
glm::mat4 modelMat = glm::mat4(1.0f);
//glm::mat4 modelMat;
// 現在バインドしているシェーダーの model uniformに変換を送る
glUniformMatrix4fv(modelMatrixID, 1, GL_FALSE, &modelMat[0][0]);
// View行列
glm::mat4 viewMat = glm::lookAt(
glm::vec3(4.f, 3.f, 3.f),
glm::vec3(0.f, 0.f, 0.f),
glm::vec3(0.f, 1.f, 0.f)
);
// 現在バインドしているシェーダーの view uniformに変換を送る
glUniformMatrix4fv(viewMatrixID, 1, GL_FALSE, &viewMat[0][0]);
// Projection行列
glm::mat4 projectionMat = glm::perspective(
glm::radians(scale),
4.0f / 3.0f,
0.1f,
100.0f
);
// 現在バインドしているシェーダーの projection uniformに変換を送る
glUniformMatrix4fv(projectionMatrixID, 1, GL_FALSE, &projectionMat[0][0]);
// 図形を描画する
object.Draw();
glPointSize(8.0f);
// ダブルバッファのスワップ
glfwSwapBuffers(windowManager.window);
glfwPollEvents();
}
return 0;
}
- Object.h
#pragma once
#include <iostream>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
const int VERTEX_SIZE = 3;
class PointCloudObject
{
// 頂点配列オブジェクト名
GLuint vao;
// 頂点バッファオブジェクト名
GLuint vbo;
// 頂点の次元数
GLint vertexSize;
// 頂点数
GLsizei vertexCount;
public:
struct Vertex
{
// 位置
GLfloat position[3];
};
PointCloudObject(GLint size, GLsizei count, std::vector<Vertex>& vertex) :vertexSize(size), vertexCount(count)
//PointCloudObject(GLint size, GLsizei count, const Vertex *vertex) : vertexSize(size), vertexCount(count)
{
// 頂点配列オブジェクト
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
// 頂点バッファオブジェクト
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
//glBufferData(GL_ARRAY_BUFFER, vertexCount * sizeof(Vertex), vertex, GL_STATIC_DRAW);
glBufferData(GL_ARRAY_BUFFER, vertexCount * sizeof(Vertex), vertex.data(), GL_STATIC_DRAW);
// 結合されている頂点バッファオブジェクトを in 変数から参照できるようにする
glVertexAttribPointer(0, vertexSize, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(0);
};
~PointCloudObject()
{
// 頂点配列オブジェクトを削除する
glDeleteVertexArrays(1, &vao);
// 頂点バッファオブジェクトを削除する
glDeleteBuffers(1, &vbo);
}
void Draw()
{
// 描画する頂点配列オブジェクトを指定する
glBindVertexArray(vao);
glDrawArrays(GL_POINTS, 0, vertexCount);
}
private:
// コピーコンストラクタによるコピー禁止
PointCloudObject(const PointCloudObject &o);
// 代入によるコピー禁止
PointCloudObject &operator=(const PointCloudObject &o);
};
表示結果
OpenGLを使用して点群データを表示することができました。
上記のデータとは異なりますが、実際に約30万個の点で構成される点群を表示した際に、OpenGL使用前は35msほどかかっていたのですが、OpenGLを使用したところ0.87msまで高速化することができました。
大きなデータ量の点群を高速に表示したい際には、OpenGLの使用も検討してみるといいかと思います。
ここまでお読みいただきありがとうございました。
今回の記事がお役に立てれば幸いです。