シェーダを書かずにモダンOpenGL

# はじめに
OpenGL3.1以降のモダンOpenGLではシェーダを書かないといけないことになりましたが，私的にごく簡単な3DCGだけしたいなど，シェーダを書きたくない場面も稀にあります．

つまり，**VAOを使いながら，固定機能パイプラインを使う方法**について説明します．

SDLやGLFWをOpenGLコンテキストツールキットとして使いたいけど，GLUTみたいに昔の機能を使いたいパターンもこれ（SDL/GLFWでクラシックOpenGL）．

方針は：

1. GLコンテキストをCompatibility Profileにする
2. glClientState，gl*Pointer（glVertexPointerなど）を使う

※注意：この機能が使えるかどうかはOSやハードウェア等の環境に依存します．使えない可能性もあるかもしれません．

# サンプルコード

```C++
#include<GL/glew.h>
#include<GL/gl.h>
#include<SDL.h>
#include<glm/glm.hpp>
#include<glm/ext.hpp>
#include<Eigen/Core>
#include<Eigen/Geometry>

#define M_PI (3.1415926535897932)

using MatrixXfR = Eigen::Matrix<float,Eigen::Dynamic,Eigen::Dynamic,Eigen::RowMajor>;
using MatrixXuiR = Eigen::Matrix<uint32_t,Eigen::Dynamic,Eigen::Dynamic,Eigen::RowMajor>;

//OpenGL用点群モデル構造体
class PointsVAO{
public:
    GLuint vao;
    GLuint vbo_color;
    GLuint vbo_pos;
    GLuint ebo_index;
    PointsVAO(const MatrixXfR &pos_buf, const MatrixXfR &color_buf, const MatrixXuiR &index_buf){
        glGenVertexArrays(1, &vao);
        glGenBuffers(1, &vbo_pos);
        glGenBuffers(1, &vbo_color);
        glGenBuffers(1, &ebo_index);

        glBindVertexArray(vao);
        glBindBuffer(GL_ARRAY_BUFFER, vbo_pos);
        glEnableClientState(GL_VERTEX_ARRAY);
        glBufferData(GL_ARRAY_BUFFER, sizeof(float)*pos_buf.size(), pos_buf.data(), GL_DYNAMIC_DRAW);
        glVertexPointer(pos_buf.cols(), GL_FLOAT, 0, 0);
        glBindBuffer(GL_ARRAY_BUFFER, vbo_color);
        glEnableClientState(GL_COLOR_ARRAY);
        glBufferData(GL_ARRAY_BUFFER, sizeof(float)*color_buf.size(), color_buf.data(), GL_DYNAMIC_DRAW);
        glColorPointer(pos_buf.cols(), GL_FLOAT, 0, 0);
        if(index_buf.size()>0){
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo_index);
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(uint32_t)*index_buf.size(), index_buf.data(), GL_DYNAMIC_DRAW);
        }
        glBindVertexArray(0);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    }
    ~PointsVAO(){
        glDeleteVertexArrays(1, &vao);
        glDeleteBuffers(1, &vbo_pos);
        glDeleteBuffers(1, &vbo_color);
        glDeleteBuffers(1, &ebo_index);
    }

    void updatePos(const MatrixXfR &pos_buf){
        glBindBuffer(GL_ARRAY_BUFFER, vbo_pos);
        glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(float)*pos_buf.size(), pos_buf.data());
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }
    void updateColor(const MatrixXfR &color_buf){
        glBindBuffer(GL_ARRAY_BUFFER, vbo_color);
        glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(float)*color_buf.size(), color_buf.data());
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }
    void updateIndex(const MatrixXuiR &index_buf){
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo_index);
        glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, sizeof(uint32_t)*index_buf.size(), index_buf.data());
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    }
};

#undef main
int main(){
    const int WINDOWWIDTH=640;
    const int WINDOWHEIGHT=480;

    //OpenGL設定
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);

    //ウィンドウ用意
    SDL_Window *window=SDL_CreateWindow("sdl2test", 20,20,WINDOWWIDTH,WINDOWHEIGHT,SDL_WINDOW_SHOWN|SDL_WINDOW_OPENGL);
    SDL_GLContext context = SDL_GL_CreateContext(window);

    //OpenGL設定その２
    glewInit();

    //各行列
    glm::mat4 mat_proj = glm::perspective<float>(M_PI/4.0,(float)WINDOWWIDTH/WINDOWHEIGHT,0.1,50.0);
    glm::mat4 mat_view = glm::lookAt<float>(glm::vec3(5,5,5),glm::vec3(0,0,0), glm::vec3(0,0,1));
    glm::mat4 mat_model(1.f);
    glm::mat4 mat_modelview(1.f);
    Eigen::Map<Eigen::Matrix4f> emat_model(glm::value_ptr(mat_model));

    //例：正四面体モデル
    //点の位置
    MatrixXfR pos_buf(4, 3);
    pos_buf <<
        -2, -1, -1,
         2, -1, -1,
         0,  2, -1,
         0,  0,  2
    ;
    //点の色（0～1，RGB）
    MatrixXfR color_buf(4, 3);
    color_buf <<
        1, 0, 0,
        0, 1, 0,
        0, 0, 1,
        0, 1, 1
    ;
    //線で描く時の，何番と何番の点を結ぶか？の情報
    //この例では，四面体なので6本の辺で構成．
    MatrixXuiR index_buf(6, 2);
    index_buf <<
        0, 1,
        1, 2,
        2, 0,
        0, 3,
        1, 3,
        2, 3
    ;
    //OpenGL用に固める
    PointsVAO pvao(pos_buf,color_buf,index_buf);

    //画面クリア時の色
    glClearColor(0,0,0,0);

    //デプステスト有効化
    glEnable(GL_DEPTH_TEST);

    //射影行列をシェーダに送信
    glMatrixMode(GL_PROJECTION);
    glLoadMatrixf(glm::value_ptr(mat_proj));

    //点の形状の設定
    glPointSize(10); //サイズ
    glEnable(GL_POINT_SMOOTH); //円にする

    //メインループ
    bool loopFlg=true;
    int fCount=0;
    while(loopFlg){
        //画面クリア
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        //例：Z軸回転
        float zrot=(fCount%60)/60.0f*2.0f*M_PI;
        emat_model.topLeftCorner(3,3) = Eigen::AngleAxisf(zrot,Eigen::Vector3f(0,0,1)).matrix();

        //モデルビュー行列をシェーダに送信
        mat_modelview=mat_view*mat_model;
        glMatrixMode(GL_MODELVIEW);
        glLoadMatrixf(glm::value_ptr(mat_modelview));

        //モデルの描画指示
        glBindVertexArray(pvao.vao);
        glDrawElements(GL_LINES, index_buf.size(), GL_UNSIGNED_INT, 0);
        glDrawArrays(GL_POINTS, 0, pos_buf.rows());
        glBindVertexArray(0);

        //画面更新
        SDL_GL_SwapWindow(window);

        //イベント処理
        SDL_Delay(30);
        SDL_Event event;
        while(SDL_PollEvent(&event)){
            if(event.type==SDL_KEYDOWN){
                if(event.key.keysym.sym==SDLK_q){
                    loopFlg=false;
                }
            }
        }
        ++fCount;
    }

    return 0;
}
```

※SDL2, GLEW使用．また，行列ライブラリとしてglmとEigenを使っています．

<details><summary>行列ライブラリにOpenCVを使った版</summary>

```C++
#include<opencv2/opencv.hpp>
#include<opencv2/core/quaternion.hpp>
#include<GL/glew.h>
#include<GL/gl.h>
#include<SDL.h>

#define M_PI (3.1415926535897932)

//ビュー行列の作成（右手系）
//eye: 視点位置，center: 注視点位置，up: 頭上方向ベクトル
cv::Matx44f lookAtRH(cv::Vec3f const& eye, cv::Vec3f const& center, cv::Vec3f const& up)
{
	cv::Vec3f f(center - eye); f/=cv::norm(f);
	cv::Vec3f s(f.cross(up)); s/=cv::norm(s);
	cv::Vec3f u(s.cross(f));

	cv::Matx44f Result=cv::Matx44f::eye();
	Result(0,0) = s(0);
	Result(0,1) = s(1);
	Result(0,2) = s(2);
	Result(1,0) = u(0);
	Result(1,1) = u(1);
	Result(1,2) = u(2);
	Result(2,0) =-f(0);
	Result(2,1) =-f(1);
	Result(2,2) =-f(2);
	Result(0,3) =-s.dot(eye);
	Result(1,3) =-u.dot(eye);
	Result(2,3) = f.dot(eye);
	return Result;
}

//射影行列の作成（右手系）
//fovy: 縦方向のField of view(rad), aspect：画面の縦/横比，zNear: カメラからの最短距離，zFar: カメラからの最長距離
//出力：カメラ座標系→画面座標系（XY範囲：-1～1，Z範囲：0～1）の変換行列　※変換後の点は第3要素で割ってください．
cv::Matx44f perspectiveRH_ZO(float fovy, float aspect, float zNear, float zFar)
{
	CV_Assert(::abs(aspect - std::numeric_limits<float>::epsilon()) > static_cast<float>(0));

	float const tanHalfFovy = tan(fovy / static_cast<float>(2));

	cv::Matx44f Result=cv::Matx44f::zeros();
	Result(0,0) = static_cast<float>(1) / (aspect * tanHalfFovy);
	Result(1,1) = static_cast<float>(1) / (tanHalfFovy);
	Result(2,2) = zFar / (zNear - zFar);
	Result(3,2) = - static_cast<float>(1);
	Result(2,3) = -(zFar * zNear) / (zFar - zNear);
	return Result;
}

//回転行列の作成（XYZ軸回転）
cv::Matx44f rotateYPR(float yaw, float pitch, float roll){
    cv::Quatf rq = cv::Quatf::createFromEulerAngles(cv::Vec3f(yaw, pitch, roll), cv::QuatEnum::EulerAnglesType::INT_XYZ);
    return rq.toRotMat4x4();
}

//OpenGL用点群モデル構造体
class PointsVAO{
public:
    GLuint vao;
    GLuint vbo_color;
    GLuint vbo_pos;
    GLuint ebo_index;
    PointsVAO(cv::Mat1f pos_buf, cv::Mat1f color_buf, cv::Mat1i index_buf){
        glGenVertexArrays(1, &vao);
        glGenBuffers(1, &vbo_pos);
        glGenBuffers(1, &vbo_color);
        glGenBuffers(1, &ebo_index);

        glBindVertexArray(vao);
        glBindBuffer(GL_ARRAY_BUFFER, vbo_pos);
        glEnableClientState(GL_VERTEX_ARRAY);
        glBufferData(GL_ARRAY_BUFFER, sizeof(float)*pos_buf.elemSize(), pos_buf.data, GL_DYNAMIC_DRAW);
        glVertexPointer(pos_buf.cols, GL_FLOAT, 0, 0);
        glBindBuffer(GL_ARRAY_BUFFER, vbo_color);
        glEnableClientState(GL_COLOR_ARRAY);
        glBufferData(GL_ARRAY_BUFFER, sizeof(float)*color_buf.elemSize(), color_buf.data, GL_DYNAMIC_DRAW);
        glColorPointer(pos_buf.cols, GL_FLOAT, 0, 0);
        
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo_index);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(uint32_t)*index_buf.elemSize(), index_buf.data, GL_DYNAMIC_DRAW);
        
        glBindVertexArray(0);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    }
    ~PointsVAO(){
        glDeleteVertexArrays(1, &vao);
        glDeleteBuffers(1, &vbo_pos);
        glDeleteBuffers(1, &vbo_color);
        glDeleteBuffers(1, &ebo_index);
    }

    void updatePos(cv::Mat1f pos_buf){
        glBindBuffer(GL_ARRAY_BUFFER, vbo_pos);
        glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(float)*pos_buf.elemSize(), pos_buf.data);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }
    void updateColor(cv::Mat1f color_buf){
        glBindBuffer(GL_ARRAY_BUFFER, vbo_color);
        glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(float)*color_buf.elemSize(), color_buf.data);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }
    void updateIndex(cv::Mat1i index_buf){
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo_index);
        glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, sizeof(uint32_t)*index_buf.elemSize(), index_buf.data);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    }
};

#undef main
int main(){
    const int WINDOWWIDTH=640;
    const int WINDOWHEIGHT=480;

    //OpenGL設定
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);

    //ウィンドウ用意
    SDL_Window *window=SDL_CreateWindow("sdl2test", 20,20,WINDOWWIDTH,WINDOWHEIGHT,SDL_WINDOW_SHOWN|SDL_WINDOW_OPENGL);
    SDL_GLContext context = SDL_GL_CreateContext(window);

    //OpenGL設定その２
    glewInit();

    //各行列
    cv::Matx44f mat_proj = perspectiveRH_ZO(M_PI/4.0f,(float)WINDOWWIDTH/WINDOWHEIGHT,0.1f,50.0f);
    cv::Matx44f mat_view = lookAtRH(cv::Vec3f(5,5,5),cv::Vec3f(0,0,0), cv::Vec3f(0,0,1));
    cv::Matx44f mat_model = cv::Matx44f::eye();
    cv::Matx44f mat_modelview = cv::Matx44f::eye();

    //例：正四面体モデル
    //点の位置
    cv::Mat1f pos_buf(4,3);
    pos_buf <<
        -2, -1, -1,
         2, -1, -1,
         0,  2, -1,
         0,  0,  2
    ;
    //点の色（0～1，RGB）
    cv::Mat1f color_buf(4,3);
    color_buf <<
        1, 0, 0,
        0, 1, 0,
        0, 0, 1,
        0, 1, 1
    ;
    //線で描く時の，何番と何番の点を結ぶか？の情報
    //この例では，四面体なので6本の辺で構成．
    cv::Mat1i index_buf(6,2);
    index_buf <<
        0, 1,
        1, 2,
        2, 0,
        0, 3,
        1, 3,
        2, 3
    ;
    //OpenGL用に固める
    PointsVAO pvao(pos_buf,color_buf,index_buf);

    //画面クリア時の色
    glClearColor(0,0,0,0);

    //デプステスト有効化
    glEnable(GL_DEPTH_TEST);

    //射影行列をシェーダに送信
    glMatrixMode(GL_PROJECTION);
    glLoadMatrixf(mat_proj.t().val);

    //点の形状の設定
    glPointSize(10); //サイズ
    glEnable(GL_POINT_SMOOTH); //円にする

    //メインループ
    bool loopFlg=true;
    int fCount=0;
    while(loopFlg){
        //画面クリア
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        //例：Z軸回転
        float zrot=(fCount%60)/60.0f*2.0f*M_PI;
        mat_model=rotateYPR(0,0,zrot);

        //モデルビュー行列をシェーダに送信
        mat_modelview=mat_view*mat_model;
        glMatrixMode(GL_MODELVIEW);
        glLoadMatrixf(mat_modelview.t().val);

        //モデルの描画指示
        glBindVertexArray(pvao.vao);
        glDrawElements(GL_LINES, index_buf.elemSize(), GL_UNSIGNED_INT, 0);
        glDrawArrays(GL_POINTS, 0, pos_buf.rows);
        glBindVertexArray(0);

        //画面更新
        SDL_GL_SwapWindow(window);

        //イベント処理
        SDL_Delay(30);
        SDL_Event event;
        while(SDL_PollEvent(&event)){
            if(event.type==SDL_KEYDOWN){
                if(event.key.keysym.sym==SDLK_q){
                    loopFlg=false;
                }
            }
        }
        ++fCount;
    }

    return 0;
}
```

</details>

# 解説

GLコンテキスト生成前に，これから作るコンテキストのプロファイルをCompatibility Profileにする要求をするのが大事です．
ちなみに，VAOはOpenGL3.0以降からなので，それ以降のバージョン要求をしておく必要があります．その前のバージョンを要求すれば問題なく固定機能が使えはしますが，VAOが使えません．

```C++
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY);
```

GLFWならこんな感じ。

```C++
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE);
```

VAOはだいたい同じ雰囲気で作りますが，
`glEnableVertexAttribArray`の代わりに`glEnableClientState`を，
`glVertexAttribPointer`の代わりに`glVertexPointer`/`glColorPointer`/`glNormalPointer`/`glTexCoordPointer`を使います．

```C++
glBindVertexArray(vao);

glBindBuffer(GL_ARRAY_BUFFER, vbo_pos);
glEnableClientState(GL_VERTEX_ARRAY);
glBufferData(GL_ARRAY_BUFFER, sizeof(float)*pos_buf.size(), pos_buf.data(), GL_DYNAMIC_DRAW);
glVertexPointer(pos_buf.cols, GL_FLOAT, 0, 0);

glBindBuffer(GL_ARRAY_BUFFER, vbo_color);
glEnableClientState(GL_COLOR_ARRAY);
glBufferData(GL_ARRAY_BUFFER, sizeof(float)*color_buf.size(), color_buf.data(), GL_DYNAMIC_DRAW);
glColorPointer(pos_buf.cols, GL_FLOAT, 0, 0);
        
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo_index);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(uint32_t)*index_buf.size(), index_buf.data(), GL_DYNAMIC_DRAW);
        
glBindVertexArray(0);
```

他（行列、光源，マテリアルなど）はクラシックOpenGLの形式で書くことになります．
（多分）BufferObjectは適用できません．UBOに詰めたりできません．

VAO（VBO）を使えるだけでもかなりメモリ転送が省けて高速になるのでうれしいと思います。