シェーダの実験で、カメラくらい回せないと困るので、簡単な回転・ズームを入れてみました。
ベクトルや行列を扱うのに GLM を使用しています。

作った時の環境。
大体こんな感じで作ります。
前回作った GLSLApp3.zip を元に作るのでダウンロード。
マウスの左ボタンを押している間はカメラが回せるようにします。
「ボタンを押す」「動かす」「ボタンを離す」で処理を行えるよう以下のイベントを追加。
本来は m_view に追加したいのですが、デザイナに表示されていません。
Panel1 など他のコントロールに追加して流用すると楽です。
[GLSLForm.h] m_view = gcnew GLSLView(); m_view->Dock = System::Windows::Forms::DockStyle::Fill; m_view->SizeChanged += gcnew System::EventHandler(this, &GLSLForm::GLSLForm_SizeChanged); m_view->Paint += gcnew System::Windows::Forms::PaintEventHandler(this, &GLSLForm::GLSLForm_Paint); // デザイナが this->splitContainer1->Panel1 として追加したイベントを m_view に書き換え、 // コンストラクタで m_view を生成した後に持ってきた例。 m_view->MouseDown += gcnew System::Windows::Forms::MouseEventHandler(this, &GLSLForm::splitContainer1_Panel1_MouseDown); m_view->MouseMove += gcnew System::Windows::Forms::MouseEventHandler(this, &GLSLForm::splitContainer1_Panel1_MouseMove); m_view->MouseUp += gcnew System::Windows::Forms::MouseEventHandler(this, &GLSLForm::splitContainer1_Panel1_MouseUp);
m_view = gcnew GLSLView(); m_view->Dock = System::Windows::Forms::DockStyle::Fill; m_view->SizeChanged += gcnew System::EventHandler(this, &GLSLForm::GLSLView_SizeChanged); m_view->Paint += gcnew System::Windows::Forms::PaintEventHandler(this, &GLSLForm::GLSLView_Paint); m_view->MouseDown += gcnew System::Windows::Forms::MouseEventHandler(this, &GLSLForm::GLSLView_MouseDown); m_view->MouseMove += gcnew System::Windows::Forms::MouseEventHandler(this, &GLSLForm::GLSLView_MouseMove); m_view->MouseUp += gcnew System::Windows::Forms::MouseEventHandler(this, &GLSLForm::GLSLView_MouseUp);
private:
// 描画
System::Void GLSLView_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e)
{
if (m_pGL) m_pGL->Render(m_view->ClientSize.Width, m_view->ClientSize.Height);
}
// サイズ変更
System::Void GLSLView_SizeChanged(System::Object^ sender, System::EventArgs^ e)
{
if (m_pGL) m_pGL->Render(m_view->ClientSize.Width, m_view->ClientSize.Height);
}
// マウスボタンが押された
System::Void GLSLView_MouseDown(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e)
{
}
// マウス移動
System::Void GLSLView_MouseMove(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e)
{
}
// マウスボタンが離された
System::Void GLSLView_MouseUp(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e)
{
}
マウスのホイールを回してカメラを近付けたり遠ざけたりできるようにします。
「ホイールを回す」というイベントは何故かデザイナに入ってないので定義を調べて手動で追加。
[GLSLForm.h] m_view = gcnew GLSLView(); m_view->Dock = System::Windows::Forms::DockStyle::Fill; m_view->SizeChanged += gcnew System::EventHandler(this, &GLSLForm::GLSLForm_SizeChanged); m_view->Paint += gcnew System::Windows::Forms::PaintEventHandler(this, &GLSLForm::GLSLForm_Paint); m_view->MouseDown += gcnew System::Windows::Forms::MouseEventHandler(this, &GLSLForm::splitContainer1_Panel1_MouseDown); m_view->MouseMove += gcnew System::Windows::Forms::MouseEventHandler(this, &GLSLForm::splitContainer1_Panel1_MouseMove); m_view->MouseUp += gcnew System::Windows::Forms::MouseEventHandler(this, &GLSLForm::splitContainer1_Panel1_MouseUp); m_view->MouseWheel += gcnew System::Windows::Forms::MouseEventHandler(this, &GLSLForm::GLSLView_MouseWheel);
// マウスホイール
System::Void GLSLView_MouseWheel(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e)
{
}
プロジェクト名を右クリックして「 追加>新しい項目>クラス 」を選択。
「 C++ クラス 」で追加ボタンを押し、クラス名に GLSLCamera を入力, 仮想デストラクタをチェック, マネージを外して完了。
生成された GLSLCamera.h, GLSLCamera.cpp に、カメラ情報の初期化・行列の取得と、マウス処理に必要な各種コードを追加します。( 赤字部分 )
[GLSLCamera.h]
#pragma once
#define GLM_FORCE_PURE // マネージ混合で GLM のコンパイルを通すのに必要
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtx/quaternion.hpp>
class GLSLCamera
{
private:
static const float EyeMin; // カメラ距離 Min
static const float EyeMax; // カメラ距離 Max
static const float EyeDefault; // カメラ初期位置
static const float RollScale; // マウスで回転する量の調整
static const float ZoomScale; // マウスで移動する量の調整
public:
GLSLCamera(void);
virtual ~GLSLCamera(void);
void Reset(); // 初期位置に戻す
const glm::mat4 GetViewMatrix(); // カメラ行列
void MouseDown(int x, int y); // マウスイベント
void MouseUp(int x, int y); // 〃
void MouseMove(int x, int y); // 〃
void MouseWheel(int delta); // 〃
private:
bool m_rolling; // マウスボタンが押されているか
glm::ivec2 m_lastScreenPos; // マウスのスクリーン座標
glm::quat m_eyeRot; // カメラ回転
float m_eyeZ; // カメラ位置
};
カメラは原点からZ方向に m_eyeZ 離れた ( 0, 0, m_eyeZ ) を視線ベクトル, 真上 ( 0, 1, 0 ) をアップベクトルとし、
マウスの移動量・ホイール量に対して、どれだけ回転・ズームするかは、係数 RollScale, ZoomScale で決めています。
適当なので丁度いいように書き換えるなりして下さい。
[GLSLCamera.cpp]
#include "GLSLCamera.h"
const float GLSLCamera::EyeMin = 1.0f;
const float GLSLCamera::EyeMax = 20.0f;
const float GLSLCamera::EyeDefault = 5.0f;
const float GLSLCamera::RollScale = 0.002f;
const float GLSLCamera::ZoomScale = 0.01f;
GLSLCamera::GLSLCamera(void)
{
Reset();
}
GLSLCamera::~GLSLCamera(void)
{
}
void GLSLCamera::Reset()
{
m_rolling = false;
m_lastScreenPos = glm::ivec2(0, 0);
m_eyeRot = glm::quat(1.0f, glm::vec3(0.0f));
m_eyeZ = EyeDefault;
}
// カメラ行列
const glm::mat4 GLSLCamera::GetViewMatrix()
{
// Eye, Up ベクトル
glm::vec3 eye = glm::vec3(0.0f, 0.0f, m_eyeZ);
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
// 現在の向きに回転
eye = glm::rotate(m_eyeRot, eye);
up = glm::rotate(m_eyeRot, up);
// 注視点 ( 常に原点 )
glm::vec3 center = glm::vec3(0.0f);
// ビュー行列を返す
return glm::lookAt(eye, center, up);
}
// 回転開始
void GLSLCamera::MouseDown(int x, int y)
{
// 回転中に設定
m_rolling = true;
// マウス位置を保存
m_lastScreenPos = glm::ivec2(x, y);
}
// 回転中
void GLSLCamera::MouseMove(int x, int y)
{
// 回転中のみ処理
if (m_rolling)
{
// マウス移動情報
glm::ivec2 screenPos = glm::ivec2(x, y);
glm::ivec2 roll = screenPos - m_lastScreenPos;
float rollLen = static_cast<float>(roll.length());
// 移動があった場合のみ処理
if (rollLen != 0.0f)
{
// 回転クォータニオンを計算
float ar = (RollScale * rollLen) * glm::pi<float>();
float as = sin(ar) / rollLen;
glm::quat screenRot = glm::quat(cos(ar), roll.y * as, roll.x * as, 0.0f);
// カメラを回す
m_eyeRot = glm::normalize(m_eyeRot * screenRot);
}
// マウス位置を保存
m_lastScreenPos = screenPos;
}
}
// 回転終了
void GLSLCamera::MouseUp(int x, int y)
{
// 最後の回転処理
MouseMove(x, y);
// 回転終了
m_rolling = false;
}
// マウスホイール
void GLSLCamera::MouseWheel(int delta)
{
// ズーム
m_eyeZ += (-delta * ZoomScale);
// 範囲内に収める
m_eyeZ = glm::clamp(m_eyeZ, EyeMin, EyeMax);
}
プロジェクト名を右クリックして「 追加>新しい項目>クラス 」を選択。
「 C++ クラス 」で追加ボタンを押し、クラス名に GLSLProjection を入力, 仮想デストラクタをチェック, マネージを外して完了。
生成された GLSLProjection.h, GLSLProjection.cpp に透視変換行列を返すコードを追加します。( 赤字 )
画面サイズが変わると透視変換行列も変わるので、コントロールのサイズ変更イベント SizeChanged を処理できるようにしておきます。
[GLSLProjection.h]
#pragma once
#define GLM_FORCE_PURE
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
class GLSLProjection
{
private:
static const float Fovy; // 画角
static const float Near; // Far クリップ
static const float Far; // Nearクリップ
public:
GLSLProjection(int cw, int ch)
virtual ~GLSLProjection(void);
const glm::mat4 GetProjMatrix(); // 透視変換行列
void SizeChanged(int cw, int ch); // 画面サイズ変更イベント
private:
float m_aspect; // アスペクト比
};
中身は glm::perspective を呼び出すだけで、あまりやる事ないです。
[GLSLProjection.cpp]
#include "GLSLProjection.h"
const float GLSLProjection::Fovy = 30.0f;
const float GLSLProjection::Near = 1.0f;
const float GLSLProjection::Far = 100.0f;
GLSLProjection::GLSLProjection(int cw, int ch)
{
SizeChanged(cw, ch);
}
GLSLProjection::~GLSLProjection(void)
{
}
const glm::mat4 GLSLProjection::GetProjMatrix()
{
return glm::perspective(Fovy, m_aspect, Near, Far);
}
const glm::mat4 GLSLProjection::SizeChanged(int cw, int ch)
{
m_aspect = static_cast<float>(cw) / ch;
}
フォームのイベントに対応するメソッドと、GLSLCamera, GLSLProjection を追加します。
イベントにより行列が変更されたら Render を呼んで再描画する必要がありますが、
引数 cw, ch を毎回渡さなくていいようにメンバ変数 m_cw, m_ch で持たせるようにします。
m_cw, m_ch はコンストラクタで初期化する時と、SizeChanged イベントでのみ設定を行います。
[GLSLMain.h]
#pragma once
#include <windows.h>
#include "GLSLCamera.h"
#include "GLSLProjection.h"
#include "GLSLShader.h"
#include "GLSLShaderParam.h"
class GLSLMain
{
public:
GLSLMain(void* hWnd, int cw, int ch); // 引数追加
virtual ~GLSLMain(void);
void GLSLMain::Render(void); // 引数なし
GLSLShaderParam* GetShaderParam(){ return m_pShaderParam; }
void SizeChanged(int cw, int ch); // サイズ変更
void MouseDown(int w, int h); // マウスが押された
void MouseUp(int w, int h); // マウスが放された
void MouseMove(int w, int h); // マウス移動
void MouseWheel(int delta); // マウスホイール
private:
GLSLCamera* m_pCamera; // カメラ行列
GLSLProjection* m_pProjection; // 透視変換行列
GLSLShader* m_pShader;
GLSLShaderParam* m_pShaderParam;
HWND m_hWnd;
HDC m_hDC;
HGLRC m_hRC;
int m_cw; // サイズW
int m_ch; // サイズH
};
コンストラクタ、デストラクタで GLSLCamera, GLSLProjection の生成と破棄。コンストラクタでは画面サイズ m_cw, m_ch の初期化も行います。
[GLSLMain.cpp]
GLSLMain::GLSLMain(void* hWnd, int cw, int ch)
{
:
m_cw = cw;
m_ch = ch;
m_pCamera = new GLSLCamera();
m_pProjection = new GLSLProjection(cw, ch);
:
}
GLSLMain::~GLSLMain(void)
{
:
if (m_pProjection)
delete m_pProjection;
if (m_pCamera)
delete m_pCamera;
:
}
画面サイズが変わったら SizeChanged で m_cw, m_ch と透視変換行列を更新。
void GLSLMain::SizeChanged(int cw, int ch)
{
m_cw = cw;
m_ch = ch;
if (m_pProjection)
{
m_pProjection->SizeChanged(cw, ch);
Render();
}
}
マウス処理は MouseDown, MouseMove, MouseUp, MouseWheel でカメラ行列を更新。
void GLSLMain::MouseDown(int w, int h)
{
if (m_pCamera)
{
m_pCamera->MouseDown(w, h);
Render();
}
}
void GLSLMain::MouseUp(int w, int h)
{
if (m_pCamera)
{
m_pCamera->MouseUp(w, h);
Render();
}
}
void GLSLMain::MouseMove(int w, int h)
{
if (m_pCamera)
{
m_pCamera->MouseMove(w, h);
Render();
}
}
void GLSLMain::MouseWheel(int delta)
{
if (m_pCamera)
{
m_pCamera->MouseWheel(delta);
Render();
}
}
Render は引数なしで呼べるようにして、画面サイズは m_cw, m_ch を使います。
void GLSLMain::Render(void)
{
wglMakeCurrent(m_hDC, m_hRC);
glViewport(0, 0, m_cw, m_ch);
:
}
GLSLMain のコンストラクタにサイズを渡す変更をしたので、フォームでの生成コードを修正します。
m_view に Dock = Fill を設定して Panel1 の子にしてからでないと正しいサイズが渡せないので注意。
[GLSLForm.h] m_pGL = new GLSLMain((void*)m_view->Handle, m_view->ClientSize.Width, m_view->ClientSize.Height);Render を呼び出しているメソッドは引数不要になったので、こちらも変更します。
private:
// プロパティ変更
System::Void propertyGrid1_PropertyValueChanged(System::Object^ s, System::Windows::Forms::PropertyValueChangedEventArgs^ e)
{
if (m_pGL)
{
m_property->Set(m_pGL->GetShaderParam());
m_pGL->Render( /* 引数なし */ );
}
}
// 描画
System::Void GLSLView_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e)
{
if (m_pGL) m_pGL->Render( /* 引数なし */ );
}
サイズ変更時は Render でなく SizeChanged を呼びます。
SizeChanged の中で Render も行われます。
// サイズ変更
System::Void GLSLView_SizeChanged(System::Object^ sender, System::EventArgs^ e)
{
if (m_pGL) m_pGL->SizeChanged(m_view->ClientSize.Width, m_view->ClientSize.Height);
}
マウスイベントも同様なので、GLSLMain の 同名 メソッドを呼び出すよう修正。
// マウスボタンが押された
System::Void GLSLView_MouseDown(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e)
{
if (m_pGL && e->Button == ::MouseButtons::Left)
{
m_pGL->MouseDown(e->X, e->Y);
}
}
// マウスボタンが離された
System::Void GLSLView_MouseUp(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e)
{
if (m_pGL && e->Button == ::MouseButtons::Left)
{
m_pGL->MouseUp(e->X, e->Y);
}
}
// マウス移動
System::Void GLSLView_MouseMove(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e)
{
if (m_pGL && e->Button == ::MouseButtons::Left)
{
m_pGL->MouseMove(e->X, e->Y);
}
}
// マウスホイール
System::Void GLSLView_MouseWheel(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e)
{
if (m_pGL) m_pGL->MouseWheel(e->Delta);
}
頂点シェーダでは、PropertyGrid の Position, Size を適用していました。
この結果を uVP ( ビュー行列 × 透視変換行列 ) で変換してから gl_Position に渡します。
ピクセルシェーダは変更しません。
[basic.vert]
#version 400
layout (location = 0) in vec3 VertexPosition;
uniform vec3 uPosition;
uniform float uSize;
uniform mat4 uVP; // View * Projection
void main()
{
vec3 wpos = VertexPosition * uSize + uPosition;
gl_Position = uVP * vec4(wpos, 1.0f);
}
頂点シェーダに渡す行列をメンバと、設定メソッドを追加します。
[GLSLShaderParam.h]
#pragma once
#include <gl/glew.h>
class GLSLShaderParam
{
public:
GLSLShaderParam(const float vp[]);
virtual ~GLSLShaderParam(void);
private:
float m_position[3];
float m_size;
float m_faceColor[3];
float m_mVP[16]; // View * Projection
public:
void SetPosition(float x, float y, float z){ m_position[0] = x; m_position[1] = y; m_position[2] = z; }
void SetSize(float siz){ m_size = siz; }
void SetFaceColor(float r, float g, float b){ m_faceColor[0] = r; m_faceColor[1] = g; m_faceColor[2] = b; }
void SetViewProjMatrix(const float vp[]){ for (int i = 0; i < 16; i++) m_mVP[i] = vp[i]; }
void Use(GLuint program);
};
行列の初期値は、起動時のカメラ設定や画面サイズなどが影響するので、コンストラクタで渡します。
[GLSLShaderParam.cpp]
#include "GLSLShaderParam.h"
GLSLShaderParam::GLSLShaderParam(const float vp[])
{
SetPosition(0.0f, 0.0f, 0.0f);
SetSize(1.0f);
SetFaceColor(1.0f, 0.0f, 0.0f);
if (vp != nullptr)
SetViewProjMatrix(vp);
}
GLSLShaderParam::~GLSLShaderParam(void)
{
}
void GLSLShaderParam::Use(GLuint program)
{
GLint index;
if ((index = glGetUniformLocation(program, "uPosition")) >= 0)
glUniform3f(index, m_position[0], m_position[1], m_position[2]);
if ((index = glGetUniformLocation(program, "uSize")) >= 0)
glUniform1f(index, m_size);
if ((index = glGetUniformLocation(program, "uFaceColor")) >= 0)
glUniform3f(index, m_faceColor[0], m_faceColor[1], m_faceColor[2]);
if ((index = glGetUniformLocation(program, "uVP")) >= 0)
glUniformMatrix4fv(index, 1, GL_FALSE, m_mVP);
}
GLSLMain では、カメラや画面サイズが変わる毎に行列パラメータを更新して再描画する必要があります。
行列は GLSLCamera::GetViewMatrix()、GLSLProjection::GetProjMatrix() を乗算したものを使うため、
この結果を取得する GetViewProjMatrix メソッドを GLSLMain に追加します。
[GLSLMain.h]
class GLSLMain
{
:
private:
const glm::mat4x4 GetViewProjMatrix();
:
};
[GLSLMain.cpp]
const glm::mat4x4 GLSLMain::GetViewProjMatrix()
{
glm::mat4x4 vp = glm::mat4x4(1.0f);
if (m_pCamera && m_pProjection)
{
vp = m_pProjection->GetProjMatrix() * m_pCamera->GetViewMatrix();
}
return vp;
}
GLSLMain::GLSLMain(void* hWnd, int cw, int ch)
{
:
wglMakeCurrent(m_hDC, m_hRC);
glewInit();
m_pShader = new GLSLShader("basic");
m_pShaderParam = new GLSLShaderParam( &(GetViewProjMatrix()[0][0]) );
wglMakeCurrent(m_hDC, nullptr);
}
void GLSLMain::SizeChanged(int cw, int ch)
{
m_cw = cw;
m_ch = ch;
if (m_pProjection)
{
m_pProjection->SizeChanged(cw, ch);
m_pShaderParam->SetViewProjMatrix( &(GetViewProjMatrix()[0][0]) );
Render();
}
}
void GLSLMain::MouseDown(int w, int h)
{
if (m_pCamera)
{
m_pCamera->MouseDown(w, h);
m_pShaderParam->SetViewProjMatrix( &(GetViewProjMatrix()[0][0]) );
Render();
}
}
void GLSLMain::MouseUp(int w, int h)
{
if (m_pCamera)
{
m_pCamera->MouseUp(w, h);
m_pShaderParam->SetViewProjMatrix( &(GetViewProjMatrix()[0][0]) );
Render();
}
}
void GLSLMain::MouseMove(int w, int h)
{
if (m_pCamera)
{
m_pCamera->MouseMove(w, h);
m_pShaderParam->SetViewProjMatrix( &(GetViewProjMatrix()[0][0]) );
Render();
}
}
void GLSLMain::MouseWheel(int delta)
{
if (m_pCamera)
{
m_pCamera->MouseWheel(delta);
m_pShaderParam->SetViewProjMatrix( &(GetViewProjMatrix()[0][0]) );
Render();
}
}
GLM を使った回転・ズームの簡単なサンプルのつもりでしたが、
過去のサンプルへの追加実装なのでコード増えてきちゃいましたね…。
でもシェーダを色々書いて試せるようなプログラムのベースとしては、
モデル・テクスチャのロード、複数シェーダの切り替え、ベクトル型プロパティの入力対応、
くらいは必要そうですし、まだまだな感じです。
カメラの回転は 床井研究室 - トラックボール を参考にしたというかまんまなので、
シンプルにそこだけ理解したい人は是非そちらへ。