読者です 読者をやめる 読者になる 読者になる

おもちゃラボ

Unityで遊びを作っていきます

【OpenGLでゲームを作る】四角形のポリゴンにシェーダで色を塗る

OpenGL

前回は、OpenGLで四角形のポリゴンと円のポリゴンを描画する方法を紹介しました。ただ、ポリゴンの色は真っ白のままでした。

nn-hokuson.hatenablog.com

今回はそのポリゴンに色を塗ってみましょう。ポリゴンに色を塗るためにはシェーダの修正が必要になります。

色をつけるシェーダを作る

描画するポリゴンの色を決めるためには、次のような流れになります。

  1. まず頂点ごとに何色にするかをOpenGLで定義します。
  2. 定義した色情報をバーテックスシェーダに渡します。
  3. 受け取った値をそのままピクセルシェーダに渡して描画します。

f:id:nn_hokuson:20170221195845p:plain:w450

ポリゴンに色をつけるプログラムは次のようになります。

#include <GLFW/glfw3.h>
#include <iostream>
#include <fstream>

using namespace std;

const int g_width  = 640;
const int g_height = 480;

GLuint crateShader()
{
    //バーテックスシェーダのコンパイル
    GLuint vShaderId = glCreateShader(GL_VERTEX_SHADER);
    string vertexShader = R"#(
    attribute vec3 position;
    attribute vec4 color;
    varying vec4 vColor;
    void main(void){
        gl_Position = vec4(position, 1.0);
        vColor = color;
    }
    )#";
    const char* vs = vertexShader.c_str();
    glShaderSource(vShaderId, 1, &vs, NULL);
    glCompileShader(vShaderId);
    
    //フラグメントシェーダのコンパイル
    GLuint fShaderId = glCreateShader(GL_FRAGMENT_SHADER);
    string fragmentShader = R"#(
    varying vec4 vColor;
    void main(void){
        gl_FragColor = vColor;
    }
    )#";
    const char* fs = fragmentShader.c_str();
    glShaderSource(fShaderId, 1, &fs, NULL);
    glCompileShader(fShaderId);
    
    //プログラムオブジェクトの作成
    GLuint programId = glCreateProgram();
    glAttachShader(programId,vShaderId);
    glAttachShader(programId,fShaderId);
    
    // リンク
    glLinkProgram(programId);
    
    glUseProgram(programId);
    
    return programId;
}

int main()
{
    if( !glfwInit() ){
        return -1;
    }
    
    GLFWwindow* window = glfwCreateWindow(g_width, g_height, "Simple", NULL, NULL);
    if (!window)
    {
        glfwTerminate();
        return -1;
    }
    
    // モニタとの同期
    glfwMakeContextCurrent(window);
    glfwSwapInterval(1);
    
    GLuint programId = crateShader();
    
    // ゲームループ
    while (!glfwWindowShouldClose(window)) {
        
        // 画面の初期化
        glClearColor(0.2f, 0.2f, 0.2f, 0.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glClearDepth(1.0);
        
        // 頂点データ
        float vertex_position[] = {
            0.5f, 0.5f,
            -0.5f, 0.5f,
            -0.5f, -0.5f,
            0.5f, -0.5f
        };
        
        // 色情報データ
        float vertex_color[] = {
            1.0, 0.0, 0.0, 1.0,
            0.0, 1.0, 0.0, 1.0,
            0.0, 0.0, 1.0, 1.0,
            1.0, 0.0, 1.0, 1.0,
        };
        
        // 何番目のattribute変数か
        int positionLocation = glGetAttribLocation(programId, "position");
        int colorLocation = glGetAttribLocation(programId, "color");

        // attribute属性を有効にする
        glEnableVertexAttribArray(positionLocation);
        glEnableVertexAttribArray(colorLocation);

        // attribute属性を登録
        glVertexAttribPointer(positionLocation, 2, GL_FLOAT, false, 0, vertex_position);
        glVertexAttribPointer(colorLocation, 4, GL_FLOAT, false, 0, vertex_color);
       
        // モデルの描画
        glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
        
        // バッファの入れ替え
        glfwSwapBuffers(window);
        
        // Poll for and process events
        glfwPollEvents();
    }
    
    glfwTerminate();
    
    return 0;
}

だんだんプログラムが複雑になってきましたが、やっていることは頂点情報をシェーダに渡した時と同様の方法で色情報をシェーダに渡しているだけです。

実行結果は次の通りです。

f:id:nn_hokuson:20170221194818p:plain:w400

ポリゴンに色をつけるシェーダの内容

まずは、バーテックスシェーダの部分から見ていきましょう。バーテックスシェーダではOpenGLから色情報を受け取るためattribute変数を定義しています。

f:id:nn_hokuson:20170221195304p:plain:w450

この色情報はバーテックスシェーダからフラグメントシェーダに渡したいので、シェーダ間の値の受け渡しのためにvarying変数を用意しています。

f:id:nn_hokuson:20170221195308p:plain:w450

バーテックスシェーダではOpenGLから受け取った色情報をvarying変数に代入して、フラグメントシェーダに渡しています。

    attribute vec3 position;
    attribute vec4 color;
    varying vec4 vColor;
    void main(void){
        gl_Position = vec4(position, 1.0);
        vColor = color;
    }


次にフラグメントシェーダの内容を見て見ましょう。フラグメントシェーダではバーテックスシェーダから渡される色情報を受け取るため、こちらでもvarying変数を宣言しています。

フラグメントシェーダの中ではバーテックスシェーダから受け取った値をgl_FragColorにそのまま代入して画面に出力しています。

    varying vec4 vColor;
    void main(void){
        gl_FragColor = vColor;
    }

シェーダに色情報を渡すプログラムを作る

最後にOpenGLからシェーダに頂点色情報を渡す部分を見ていきましょう。色情報はRGBAで0.0〜1.0で表現します。4頂点にそれぞれ異なる色を設定しています。

        float vertex_color[] = {
            1.0, 0.0, 0.0, 1.0,
            0.0, 1.0, 0.0, 1.0,
            0.0, 0.0, 1.0, 1.0,
            1.0, 0.0, 1.0, 1.0,
        };

この頂点ごとの色情報をシェーダに渡します。頂点情報を渡すプログラムと並べて書いて見ると、ほとんど同じことをしているのがわかると思います。必要なところだけを抜き出すと次のようになります。

  1. glGetAttribLocationでバーテックスシェーダで宣言されたどのattribute変数に代入するかを調べてます。
  2. glEnableVertexAttribArrayでattribute変数を有効にしています。
  3. glVertexAttribPointer関数で色情報を渡しています。第二引数が色の情報サイズである4になっていることに注意してください。
        // 何番目のattribute変数か
        int colorLocation = glGetAttribLocation(programId, "color");

        // attribute属性を有効にする
        glEnableVertexAttribArray(colorLocation);

        // attribute属性を登録
        glVertexAttribPointer(colorLocation, 4, GL_FLOAT, false, 0, vertex_color);

まとめ

ようやくポリゴンに色をつけるところまで到達しました。次はついにポリゴンにテクスチャを貼る方法を紹介します!

【OpenGLでゲームを作る】四角形と円のポリゴンを書く

OpenGL

前回はOpenGLとGSLSを使って三角形のポリゴンを書くところまでプログラムを作りました。

nn-hokuson.hatenablog.com

全ての図形は三角形に分割できるため、三角形さえ書ければ、その他の図形はかけたも同然です(もちろん立方体や球もかけますが、これは後ほど・・・)

OpenGLで四角形を描く

こちらが、OpenGLで四角形を書くプログラムです。

#include <GLFW/glfw3.h>
#include <iostream>
#include <fstream>

using namespace std;

const int g_width  = 640;
const int g_height = 480;

GLuint crateShader()
{
    //バーテックスシェーダのコンパイル
    GLuint vShaderId = glCreateShader(GL_VERTEX_SHADER);
    string vertexShader = R"#(
    attribute vec3 position;
    void main(void){
        gl_Position = vec4(position, 1.0);
    }
    )#";
    const char* vs = vertexShader.c_str();
    glShaderSource(vShaderId, 1, &vs, NULL);
    glCompileShader(vShaderId);
    
    //フラグメントシェーダのコンパイル
    GLuint fShaderId = glCreateShader(GL_FRAGMENT_SHADER);
    string fragmentShader = R"#(
    void main(void){
        gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
    }
    )#";
    const char* fs = fragmentShader.c_str();
    glShaderSource(fShaderId, 1, &fs, NULL);
    glCompileShader(fShaderId);
    
    //プログラムオブジェクトの作成
    GLuint programId = glCreateProgram();
    glAttachShader(programId,vShaderId);
    glAttachShader(programId,fShaderId);
    
    // リンク
    glLinkProgram(programId);
    
    glUseProgram(programId);
    
    return programId;
}

int main()
{
    if( !glfwInit() ){
        return -1;
    }
    
    GLFWwindow* window = glfwCreateWindow(g_width, g_height, "Simple", NULL, NULL);
    if (!window)
    {
        glfwTerminate();
        return -1;
    }
    
    // モニタとの同期
    glfwMakeContextCurrent(window);
    glfwSwapInterval(1);
    
    GLuint programId = crateShader();
    
    // ゲームループ
    while (!glfwWindowShouldClose(window)) {
        
        // 画面の初期化
        glClearColor(0.2f, 0.2f, 0.2f, 0.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glClearDepth(1.0);
        
        // 頂点データ
        float vertex_position[] = {
            0.5f, 0.5f,
            -0.5f, 0.5f,
            -0.5f, -0.5f,
            0.5f, -0.5f
        };
       
        // 何番目のattribute変数か
        int attLocation = glGetAttribLocation(programId, "position");
        
        // attribute属性を有効にする
        glEnableVertexAttribArray(attLocation);
        
        // attribute属性を登録
        glVertexAttribPointer(attLocation, 2, GL_FLOAT, false, 0, vertex_position);
        
        // モデルの描画
        glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
        
        // バッファの入れ替え
        glfwSwapBuffers(window);
        
        // Poll for and process events
        glfwPollEvents();
    }
    
    glfwTerminate();
    
    return 0;
}

シェーダの関連のメソッドは一切書き換えていません。書き換えたのは頂点座標を定義するOpenGLの部分のみです。

実行すると次のように四角形が表示されます。

f:id:nn_hokuson:20170217190256p:plain:w300

今回のプログラムでは頂点座標を3つから4つに変更しています。

        float vertex_position[] = {
            0.5f, 0.5f,
            -0.5f, 0.5f,
            -0.5f, -0.5f,
            0.5f, -0.5f
        };

画面の座標は中央が(0,0)で、±1の大きさだったので、今回の座標はそれぞれ次のようになります。

f:id:nn_hokuson:20170217190442p:plain:w350

今回は四角形を書くためにglDrawArraysの第一引数を変更しています。前回はGL_TRIANGLESでしたが今回はGL_TRIANGLE_FANになっています。

        glDrawArrays(GL_TRIANGLE_FAN, 0, 4);

GL_TRIANGLEのオプションは頂点を3つ単位で読み出して、一つの三角形を作ります。この方法で四角形を書こうとすると、6つの頂点を宣言する必要があります。ダブっちゃう頂点が無駄ですね。

f:id:nn_hokuson:20170217190510p:plain:w300

GL_TRIANGLE_FANを使うと次のように、一つの頂点を中心として三角形を作っていくので、四角形を書く場合でも頂点の宣言は4つで大丈夫です。

f:id:nn_hokuson:20170217190518p:plain:w400

このほかにはGL_TRIANGLE_STRIPというものもあり、次の順番で頂点座標を配置していきます。それぞれ図形にあった方法を指定すると良いでしょう。

f:id:nn_hokuson:20170217190527p:plain:w400

OpenGLで円を描く

四角形が描けたので、この調子で円も描いてみましょう。三角形を基本にして円を描くには、ピザを切るみたいに円を細かく分割します。これを応用すると、球も曲面もポリゴンも、細かく分割すると三角形で作るというわけです。

f:id:nn_hokuson:20170217190539p:plain:w400

では円を描くプログラムも載せておきます。今回もシェーダの変更はないため、main関数だけ載せておきますね。

int main()
{
    if( !glfwInit() ){
        return -1;
    }
    
    GLFWwindow* window = glfwCreateWindow(g_width, g_height, "Simple", NULL, NULL);
    if (!window)
    {
        glfwTerminate();
        return -1;
    }
    
    // モニタとの同期
    glfwMakeContextCurrent(window);
    glfwSwapInterval(1);
    
    GLuint programId = crateShader();
    
    // ゲームループ
    while (!glfwWindowShouldClose(window)) {
        
        // 画面の初期化
        glClearColor(0.2f, 0.2f, 0.2f, 0.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glClearDepth(1.0);
        
        // 頂点データ
        float vertex_position[32*2];
        float radius = 0.5f;
        
        for(int i = 0; i < 32; ++i){
            GLfloat angle = static_cast<GLfloat>((M_PI*2.0*i)/32);
            vertex_position[i*2]   = radius * sin(angle);
            vertex_position[i*2+1] = radius * cos(angle);
        }
       
        // 何番目のattribute変数か
        int attLocation = glGetAttribLocation(programId, "position");
        
        // attribute属性を有効にする
        glEnableVertexAttribArray(attLocation);
        
        // attribute属性を登録
        glVertexAttribPointer(attLocation, 2, GL_FLOAT, false, 0, vertex_position);
        
        // モデルの描画
        glDrawArrays(GL_TRIANGLE_FAN, 0, 32);
        
        // バッファの入れ替え
        glfwSwapBuffers(window);
        
        // Poll for and process events
        glfwPollEvents();
    }
    
    glfwTerminate();
    
    return 0;
}

実行すると次のように円が表示されます。円が楕円になっているのはウインドウのサイズが長方形だからです。この問題は座標変換のところで説明しますね。

f:id:nn_hokuson:20170217190924p:plain:w300

円を作る座標を手打ちするのは少々大変なので、プログラムで頂点座標を生成しています。

GLfloat vtx[32*2];
    for(int i = 0; i < 32; ++i){
        GLfloat angle = static_cast<GLfloat>((M_PI*2.0*i)/32);
        vtx[i*2]   = radius * std::sin(angle) + g_windowWidth/2;
        vtx[i*2+1] = radius * std::cos(angle) + g_windowHeight/2;
    }

今回は半径0.5の円を32当等分して描画しています。円周上の頂点は媒介変数表示で表すと、次のように表せる(rは円の半径、θは円周上の角度)ので、これを利用して32個の頂点を生成しています。

x = r*cosθ
y = r*sinθ

頂点を生成できたら、あとはglDrawArrayを使って円を描画しています。頂点の個数以外は変更点はありません。

アンチエイリアスとは

最後にアンチエイリアス( anti aliasing )について説明したいと思います。アンチエイリアスは画像のエッジ部分のギザギザをいい感じにぼかして、なめらかに見せる技術です。

f:id:nn_hokuson:20170217191418p:plain:w400

OpenGLアンチエイリアスを使うには、glfwOpenWindowHintの引数にGLFW_SAMPLESを渡します。次のプログラムをウインドウの生成(glfwCreateWindow)の前に書くことで、アンチエイリアスをかけることができます。

   glfwWindowHint(GLFW_SAMPLES, 4);
   GLFWwindow* window = glfwCreateWindow(g_width, g_height, "Simple", NULL, NULL);
   ・・・・

アンチエイリアスを有効にした時の実行結果は下のようになります。円のエッジがなめらかになっているのがわかると思います。

f:id:nn_hokuson:20170217193321p:plain

ただ、基本的にアンチエイリアスは比較的重たい処理です。2Dゲームに限って言えば、基本的に四角形のポリゴンにテクスチャを貼り付けることが多いのでアンチエイリアスの効果はさほど出ません。したがって、2Dゲームではアンチエイリアスは使わずに進めていきます。

まとめ

今回はポリゴンの四角形と円の表示を行いました。また、アンチエイリアスを使って図形を綺麗に見せる方法を学びました。

次回はシェーダを書き換えて、ポリゴンに色をつけてみましょう。

【Unityシェーダ入門】粘性のある液体をシェーダで作る

Unity シェーダ

瓶に入った、粘性の高い液体をシェーダで表現してみます。RPGの回復系ドリンク・・・・とはちょっと違いますね。笑

f:id:nn_hokuson:20170215201235g:plain

今回はいちからシェーダを描くのではなく、ShaderToyで紹介されているシェーダを利用して液体を作ってみました。

ShaderToyのシェーダを利用する

ShaderToyにあるシェーダはOpenGLで使われるGLSLというシェーダ言語で書かれています。一方UnityはHLSLと呼ばれるシェーダ言語を使っているため、少し変換作業が必要になります。

といっても機械的に置き換えていくだけなので、簡単簡単!今回、液体表現として今回使ったシェーダはこちらで紹介されているものです。

https://www.shadertoy.com/view/Xts3WH

なんと、この液体シェーダは次の4行だけで作られています。なんというか、、、神業ですね。このGLSLで書かれたシェーダをUnity用に書き換えていきましょう。

void mainImage( out vec4 c, in vec2 w )
{
	vec2 p = w/iResolution.xy, a = p*5.; a.y -= iGlobalTime;
	vec2 f = fract(a); a -= f; f = f*f*(3.-2.*f);
        vec4 r = fract(sin(vec4(a.x + a.y*1e3) + vec4(0, 1, 1e3, 1001)) * 1e5)*30./p.y;
	c.rgb = p.y + vec3(1,.5,.2) * clamp(mix(mix(r.x, r.y, f.x), mix(r.z, r.w, f.x), f.y)-30., -.2, 1.);
}

液体を入れるボトルとマテリアルを作る

まずはプロジェクトを作って、ボトルになる円柱を配置しておきます。また、シェーダにはUnlitシェーダを使うので、プロジェクトビューから右クリックで「Create」→「Shader」→「Unlit Shader」を選択して下さい。(Unlitシェーダはunlightingの略、だと思っています)

f:id:nn_hokuson:20170215203719g:plain

作成したシェーダに対応するマテリアルを作成します。シェーダファイルを選択した状態で、右クリックして「Create」→「Material」を選択します。作られたマテリアルは円柱にアタッチしておきます。

f:id:nn_hokuson:20170215210629p:plain

UnityにShaderToyのシェーダを移植する

では、いま作成したlavaシェーダにShaderToyのシェーダを移植しましょう。移植する場所はフラグメントシェーダ内(fragメソッド)です。まずは機械的に修正できる点から見ていきましょう。

  • fact → frac
  • vec2/vec3/vec4 → half2/half3/half4
  • mix → lerp

また、ShaderToyでは引数にスクリーン座標が渡されてくるため、正規座標系に直すために画面解像度(iResolution)で割り算しています。UnityではテクスチャのUV座標が使えるので、そちらを使います。

修正後のシェーダスクリプトは次のとおりです。

fixed4 frag (v2f i) : SV_Target
{
        half2 p = i.uv.xy; half2 a = p*4.; a.y -= _Time.w*0.5;
        half2 f = frac(a); a -= f; f = f*f*(3.-2.*f);
        half4 r = frac(sin((a.x + a.y*1e3) + half4(0, 1, 1e3, 1001)) * 1e5)*30./p.y;
        return half4(p.y+half3(1,.5,.2) * clamp(lerp(lerp(r.x, r.y, f.x), lerp(r.z, r.w, f.x), f.y)-30., -.2, 1.),1);
}

実行結果

シェーダを保存できたら実行してみましょう。次のように液体が湧き上がってくる感じになると思います。あとは瓶の蓋とかガラス瓶とかを作ったら完成です。

f:id:nn_hokuson:20170215210232g:plain

OpenGL 4.0 シェーディング言語 -実例で覚えるGLSLプログラミング-

OpenGL 4.0 シェーディング言語 -実例で覚えるGLSLプログラミング-