おもちゃラボ

Unityで遊びを作ってます

【Unityシェーダ入門】フラットシェーディングでローポリっぽく見せる

シェーディングの方法は大きく分けると、次の二つの方法があります。

  • スムースシェーディング
  • フラットシェーディング

f:id:nn_hokuson:20170213195605p:plain:w400

スムースシェーディングは少ないポリゴン数でも、表面がなめらかに見えるように法線情報を補間して表示する方法です。

一方フラットシェーディングは法線情報を補間せずにエッジを強調したシェーディングです。
ほとんどのゲームではスムースシェーディングを使うと思いますが、ローポリのモデルを利用した味のある表現をしたい場合は、わざとフラットシェーディングにすることもあります(↓こんなのとか)

Unityでフラットシェーディングの設定をするには

UnityはBlenderなど3DCGソフトで設定したシェーディング方法でモデルをインポートします
スムースシェーディングで提供されているモデルをUnity上でフラットシェーディングに変更するには次のようにします。

プロジェクトビューでモデルを選択して、インスペクタから「Normal & Tangents」の「Normals」 を「Import」から「Calculate」に変更し、続けて「Smoothing Angle」を「0」に設定してください。
右下のApplyボタンを押すとモデルがフラットシェーディングで表示されます。

f:id:nn_hokuson:20170213194138g:plain

ゲーム実行時にフラットシェーディングに変更する

ゲームの実行中にスムースシェーディングからフラットシェーディングに変更したい場合は、スクリプトを使って変更することができます。
次のスクリプトを作成してモデルにアタッチしてください。

f:id:nn_hokuson:20170213194405p:plain:w250

public class Flat : MonoBehaviour 
{
	void FlatShading ()
	{
		MeshFilter mf = GetComponent<MeshFilter>();
		Mesh mesh = Instantiate (mf.sharedMesh) as Mesh;
		mf.sharedMesh = mesh;

		Vector3[] oldVerts = mesh.vertices;
		int[] triangles = mesh.triangles;
		Vector3[] vertices = new Vector3[triangles.Length];

		for (int i = 0; i < triangles.Length; i++) 
		{
			vertices[i] = oldVerts[triangles[i]];
			triangles[i] = i;
		}

		mesh.vertices = vertices;
		mesh.triangles = triangles;
		mesh.RecalculateNormals();
	}
	
	void Update () 
	{
		if (Input.GetMouseButtonDown (0))
		{
			FlatShading ();
		}
	}
}

このスクリプトでは、現在のメッシュから新しいメッシュを作成し、最後にRecalculateNormalsを読んでいます。
RecalculateNormalsメソッドではスムースされていない法線情報が生成されるため、結果的にフラットシェーディングのモデルが生成されます。

f:id:nn_hokuson:20170213194425g:plain

スクリプトからシェーディングを変更する方法は、こちらの記事を参考にさせていただきました。
answers.unity3d.com

RecalculateNormalsの仕組みについてはこちらの記事が詳しかったです。
schemingdeveloper.com

まとめ

Unityでフラットシェーディングをするための設定方法を紹介しました。
ローポリのモデルを使ったゲームを作るときは参考にしてみてくださいね。

【OpenGLでゲームを作る】三角形のポリゴンを表示する

ポリゴンを画面上に表示するためには、OpenGLのプログラムでポリゴンの各頂点座標(ローカル座標)を定義し、それをバーテックスシェーダに渡す必要がありました(前回の記事を参照)。

nn-hokuson.hatenablog.com

今回はOpenGLからシェーダに頂点情報を渡すして画面に三角形のポリゴンを表示するプログラムを見ていきましょう。

三角形を表示するプログラムを書く

次のプログラムは画面上に3角形を描画するプログラムです。必要な部分の解説はコメントに書いているので、あわせて読んでみてください。

#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.0, 0.5,
           0.4, -0.25,
           -0.4,-0.25,
        };
     
        // 何番目のattribute変数か
        int attLocation = glGetAttribLocation(programId, "position");
        
        // attribute属性を有効にする
        glEnableVertexAttribArray(attLocation);
        
        // OpenGLからシェーダに頂点情報を渡す
        glVertexAttribPointer(attLocation, 2, GL_FLOAT, false, 0, vertex_position);
        
        // モデルの描画
        glDrawArrays(GL_TRIANGLES, 0, 3);

        // バッファの入れ替え
        glfwSwapBuffers(window);
        
        glfwPollEvents();
    }
    
    glfwTerminate();
    
    return 0;
}


上のプログラムを実行すると次のように三角形が描画されます。
f:id:nn_hokuson:20170203234041p:plain
ようやく画面上にポリゴンを描くことができました・・・!ここまで長い道のりでしたね〜。次は、このプログラムの説明します。今回は(そして、これからもしばらく)crateShader関数の中身とシェーダの内容は変更しませんので、main関数の中身を説明していきます。

OpenGLで三角形を表示する方法

今回のプログラムでは、OpenGL側で三角形の頂点情報を定義して、それをバーテックスシェーダに渡しています。追加したプログラムを詳しくみていきましょう。

まずは、次のように頂点情報をfloat型の配列として定義しています。

// 頂点データ
float vertex_position[] = {
           0.0, 0.5, 0.0,
           0.4, -0.25, 0.0,
           -0.4,-0.25, 0.0
};

ここでは3つの頂点を定義していて、一つ目の頂点が(0.0, 0.5, 0.0)、二つ目が(0.4, -0.25, 0.0)、三つ目が(-0.4, -0.25, 0.0)です。OpenGLではウインドウの中央が(0, 0)上下左右は±1の大きさであることに注意しましょう。
f:id:nn_hokuson:20170204001444p:plain

頂点データを渡すattribute変数を決める

頂点座標の定義に続けて、バーテックスシェーダに頂点情報を渡すのですが、頂点データ渡す前にバーテックスシェーダで定義されている変数のうち、上から何番めのattribute変数に値を渡すのかを指定する必要があります。

// 何番目のattribute変数か
int attLocation = glGetAttribLocation(programId, "position");    

「 指定したattribute変数がバーテックスシェーダで上から何番目に定義されているか」はglGetAttribLocationを使って調べます。第二引数に調べたいattribute変数名を渡すと、上から何番目のattribute変数かを返してくれます。

OpenGLからのデータを受け取れる設定にする

シェーダに頂点情報を渡す前に、受け取り側のattribute変数をデータを受け取れる状態にしておく必要があります。そのために、glEnableVertexAttribArray関数を使っています。引数には、何番目のattribute変数を「受け取れる状態にするか」を指定します。

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

 

ポリゴンを描画する

これで、頂点情報の受け渡し可能な状態になったので、glVertexAttribPointer関数を使って頂点情報をシェーダに渡しています。第一引数にはattribute変数の位置番号、第二引数にはattribute変数の要素数(今回は2Dなので2)を渡しています。

// OpenGLからシェーダに頂点情報を渡す
glVertexAttribPointer(attLocation, 2, GL_FLOAT, false, 0, vertex_position);

まとめ

今回はようやくポリゴンが表示できました。ただ、プログラムも複雑になってきましたね。でも、やっていることは「OpenGLで定義した頂点データをシェーダに渡して描画している」だけです。これからもこのプログラムがベースになるのでしっかりと理解しておきましょう。

【OpenGL】VBOを使って描画を高速化しよう

前回はOpenGLからシェーダに頂点データを渡して三角形を表示しました。

nn-hokuson.hatenablog.com

今回の記事は、この頂点データの受け渡しを効率的に行うためのVBOという仕組みの説明をします。

VBOとは

前回はポリゴンを画面上に表示するために、OpenGLのプログラムで定義した頂点座標のデータをバーテックスシェーダに配列として渡していました。シェーダに頂点データを渡す場合、配列よりもVBOと呼ばれるものを使ったほうが効率が良くなります。VBOはVertex Berffer Objectの頭文字をとったもので、OpenGLからシェーダに渡す値を詰める専用オブジェクトのようなものです。

VBOを利用する流れは次のようになります。

  1. VBOの生成
  2. VBOに頂点データを格納する
  3. VBOと頂点シェーダのattribute変数を紐付ける

これだけでは、どのようにしてVBOを使えばよいのかイメージしにくいと思うので、早速プログラムを見ていきましょう。

VBOを使うプログラムを書く

次のプログラムは画面上に3角形を描画するプログラムです。必要な部分の解説はコメントに書いているので、あわせて読んでみてください。

#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.0, 0.5,
           0.4, -0.25,
           -0.4,-0.25, 
        };
        
        // VBOの生成
        GLuint vbo;
        glGenBuffers(1, &vbo);
        
        // バッファをバインドする
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        
        // バッファにデータをセット
        glBufferData(GL_ARRAY_BUFFER, 6 * sizeof(float), vertex_position, GL_STATIC_DRAW);
        
        // 何番目のattribute変数か
        int attLocation = glGetAttribLocation(programId, "position");
        
        // attribute属性を有効にする
        glEnableVertexAttribArray(attLocation);
        
        // attribute属性を登録
        glVertexAttribPointer(attLocation, 2, GL_FLOAT, false, 0, NULL);
        
        // モデルの描画
        glDrawArrays(GL_TRIANGLES, 0, 3);

        // バッファの入れ替え
        glfwSwapBuffers(window);
        
        // Poll for and process events
        glfwPollEvents();
    }
    
    glfwTerminate();
    
    return 0;
}


上のプログラムを実行すると前回と同様、画面上に三角形が描画されます。

f:id:nn_hokuson:20170203234041p:plain

VBOの作成

頂点座標の定義に続けてVBOを作成します。VBOの生成はクラスをnewするイメージに近いですね。

// VBOの生成
GLuint vbo;
glGenBuffers(1, &vbo);

glGenBuffersはVBOを作成するための関数です。第一引数には作成するvboの個数、第二引数にはvboのインデックスを渡します。ここでは作成するVBOは1つだけなので、第二引数にはポインタを渡しています。

VBOに頂点データを格納する

続いて、VBOに頂点データを格納します。VBOに頂点データを格納する前に、まずは今作成したVBOをバインドする必要があります。バインドとは、頂点データを格納するVBOを指定する作業です。

// バッファをバインドする
glBindBuffer(GL_ARRAY_BUFFER, vbo);

// バッファにデータをセット
glBufferData(GL_ARRAY_BUFFER, 6 * sizeof(float), vertex_position, GL_STATIC_DRAW);

glBindBufferを使って、作成したVBOをOpenGLにバインドしてから、glBufferDataで作成したVBOに頂点情報を書き込んでいます。第二引数に全頂点情報のサイズ、第三引数には頂点情報へのポインタを渡しています。

VBOを使って三角形を描画する

これで、ようやくVBO経由で受け渡し可能な状態になったので、glVertexAttribPointer関数を使って、頂点情報をシェーダに渡しています。今回はVBOを使っているため、 最後の引数にはNULLを渡しています。

// attribute属性を登録
glVertexAttribPointer(attLocation, 2, GL_FLOAT, false, 0, NULL);

まとめ

今回はVBOを使ってポリゴンの表示を行いました。前回のプログラムと比べると、少々複雑になってしまいましたね。でも、やっていることは「OpenGLで定義した頂点データをVBO経由でシェーダに渡して描画している」だけです。