OpenGLプログラミング/モダンOpenGL チュートリアル 02

出典: フリー教科書『ウィキブックス(Wikibooks)』

さて、理解のための動作例はできたので、新しい機能やさらなる堅牢性の追加にとりかかれます。

以前のシェーダーは、できるだけ簡単になるように意図的に最小限にしていましたが、実世界の例ではもっと付属コードを使用します。


シェーダの管理[編集]

シェーダのロード[編集]

最初に追加するものは、シェーダをロードするためのより便利な方法です:外部ファイルのロードがとても簡単になるはずです(Cの文字列としてコードにコピーペーストするよりも)。 それに加えて、こうすることでCコードを再コンパイルせずにGLSLコードを変更できるようになります!

まず、ファイルを文字列としてロードする関数が必要になります。 基本的なC言語のコードで、ファイルのサイズに割り当てられたバッファの中にファイルの内容を読み取ります。

/**
 * Store all the file's contents in memory, useful to pass shaders
 * source code to OpenGL
 */
/* Problem:
 *  We should close the input file before the return NULL; statements but this would lead to a lot of repetition (DRY)
 *   -you could solve this by using goto or by abusing switch/for/while + break or by building an if else mess
 *  better solution: let the user handle the File: char* file_read(const FILE* input)
*/
char* file_read(const char* filename)
{
  FILE* input = fopen(filename, "rb");
  if(input == NULL) return NULL;
  
  if(fseek(input, 0, SEEK_END) == -1) return NULL;
  long size = ftell(input);
  if(size == -1) return NULL;
  if(fseek(input, 0, SEEK_SET) == -1) return NULL;
  	
  /*if using c-compiler: dont cast malloc's return value*/
  char *content = (char*) malloc( (size_t) size +1  ); 
  if(content == NULL) return NULL;
  
  fread(content, 1, (size_t)size, input);
  if(ferror(input)) {
    free(content);
    return NULL;
  }

  fclose(input);
  content[size] = '\0';
  return content;
}

シェーダのデバッグ[編集]

現時点でシェーダにエラーがある場合、プログラムは何のエラーかを特に説明することなく、ただ停止します。 infologを使用することで、OpenGLからより多くの情報を得ることができます:

/**
 * Display compilation errors from the OpenGL shader compiler
 */
void print_log(GLuint object)
{
  GLint log_length = 0;
  if (glIsShader(object))
    glGetShaderiv(object, GL_INFO_LOG_LENGTH, &log_length);
  else if (glIsProgram(object))
    glGetProgramiv(object, GL_INFO_LOG_LENGTH, &log_length);
  else {
    fprintf(stderr, "printlog: Not a shader or a program\n");
    return;
  }

  char* log = (char*)malloc(log_length);

  if (glIsShader(object))
    glGetShaderInfoLog(object, log_length, NULL, log);
  else if (glIsProgram(object))
    glGetProgramInfoLog(object, log_length, NULL, log);

  fprintf(stderr, "%s", log);
  free(log);
}

OpenGL と GLES2の間の抽象化の違い[編集]

GLES2関数だけを使用する場合、アプリケーションはデスクトップとモバイルデバイスの両方にほぼ移植可能です。 対処する問題がまだひと組あります:

  • GLSL #version が異なっている
  • が異なっている

The #version needs to be the very first line in some GLSL compilers (for instance on the PowerVR SGX540), so we cannot use #ifdef directives to abstract it in the GLSL shader. Instead, we'll prepend the version in the C++ code:

  const GLchar* sources[2] = {
#ifdef GL_ES_VERSION_2_0
    "#version 100\n"
    // Note: OpenGL ES automatically defines this:
    // #define GL_ES
#else
    "#version 120\n",
#endif
    source };
  glShaderSource(res, 2, sources, NULL);

私たちはすべてのチュートリアルで同じバージョンのGLSLを使用するため、これが最も簡単な解決策です。

#ifdef と精度のヒントは、次のセクションで説明します。

シェーダを作成するときに再利用可能な関数[編集]

これらの新しいユーティリティ関数と知識を使って、シェーダのロードとデバッグのための別の関数を作ることができます:

/**
 * Compile the shader from file 'filename', with error handling
 */
GLuint create_shader(const char* filename, GLenum type)
{
  const GLchar* source = file_read(filename);
  if (source == NULL) {
    fprintf(stderr, "Error opening %s: ", filename); perror("");
    return 0;
  }
  GLuint res = glCreateShader(type);
  const GLchar* sources[2] = {
#ifdef GL_ES_VERSION_2_0
    "#version 100\n"
    "#define GLES2\n",
#else
    "#version 120\n",
#endif
    source };
  glShaderSource(res, 2, sources, NULL);
  free((void*)source);

  glCompileShader(res);
  GLint compile_ok = GL_FALSE;
  glGetShaderiv(res, GL_COMPILE_STATUS, &compile_ok);
  if (compile_ok == GL_FALSE) {
    fprintf(stderr, "%s:", filename);
    print_log(res);
    glDeleteShader(res);
    return 0;
  }

  return res;
}

これでもうシェーダーをシンプルに使用してコンパイルできます:

  GLuint vs, fs;
  if ((vs = create_shader("triangle.v.glsl", GL_VERTEX_SHADER))   == 0) return 0;
  if ((fs = create_shader("triangle.f.glsl", GL_FRAGMENT_SHADER)) == 0) return 0;

同様にリンクエラーも表示します:

  if (!link_ok) {
    fprintf(stderr, "glLinkProgram:");
    print_log(program);

新しい関数を別のファイルに配置する[編集]

これらの新しい関数を、 shader_utils.cppに配置します。

これらの関数の記述を可能な限り少なくしようとしている点にお気づきでしょうか: OpenGL Wikibookの目標は、OpenGLがどのように動作するかを理解することであって、開発しているツールキットの使用方法を理解することではありません。

shader_utils.hヘッダファイルを作成しましょう:

#ifndef _CREATE_SHADER_H
#define _CREATE_SHADER_H
#include <GL/glew.h>
char* file_read(const char* filename);
void print_log(GLuint object);
GLuint create_shader(const char* filename, GLenum type);
#endif

triangle.cpp内の新しいファイルを参照 :

#include "shader_utils.h"

そして Makefileの中で :

triangle: shader_utils.o

効率化のために頂点バッファオブジェクト(VBO)を使用する[編集]

頂点バッファオブジェクト(VBO)を使用して、グラフィックカードに頂点を直接格納することは、良い習慣です。

そのうえ、 "client-side arrays"のサポートはOpenGL 3.0以降は公式に削除され、WebGLには存在せず、そして低速なので、少し単純さには欠けても、今のうちからVBOを使用してみましょう。 たまたま出くわした既存のOpenGLコードで使用されていたりするかもしれないですから、両方の方法について知っておくことが重要です。

2つ​​のステップでこれを実装します:

  • VBOを頂点といっしょに作成する
  • glDrawArray呼び出す前に、VBOをバインドする

グローバル変数を作成 (#includeの下) してVBOハンドルを格納する:

GLuint vbo_triangle;

init_resourcesで、 triangle_verticesを定義しているところに移動して、ひとつ(1)のデータバッファを作成し、それを現在のアクティブなバッファにします:

  GLfloat triangle_vertices[] = {
     0.0,  0.8,
    -0.8, -0.8,
     0.8, -0.8,
  };
  glGenBuffers(1, &vbo_triangle);
  glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle);

さて、このバッファに頂点をプッシュすることができます。 データの編成方法や使用される頻度を指定します。 GL_STATIC_DRAW は、このバッファにはあまり頻繁に書き込みはせず、そしてGPUが自身のメモリ内にコピーを保持する必要があるということを示しています。 VBOに新しい値を書き込むことは常に可能です。フレームあたりに1度かそれ以上頻繁にデータを変更する場合は、GL_DYNAMIC_DRAWまたはGL_STREAM_DRAWを使用することができます。

  glBufferData(GL_ARRAY_BUFFER, sizeof(triangle_vertices), triangle_vertices, GL_STATIC_DRAW);

どんな時でも、アクティブなバッファの設定解除を、このように行えます:

  glBindBuffer(GL_ARRAY_BUFFER, 0);

とりわけ、C言語の配列を常に直接渡す必要がある場合は、アクティブバッファを無効にしていることを確認してください。

onDisplayで、コードを少し適合させます。 glBindBufferを呼び出して、 glVertexAttribPointerの最後の2つのパラメータを変更します :

  glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle);
  glEnableVertexAttribArray(attribute_coord2d);
  /* Describe our vertices array to OpenGL (it can't guess its format automatically) */
  glVertexAttribPointer(
    attribute_coord2d, // attribute
    2,                 // number of elements per vertex, here (x,y)
    GL_FLOAT,          // the type of each element
    GL_FALSE,          // take our values as-is
    0,                 // no extra data between each position
    0                  // offset of first element
  );

終了時のクリーンアップを忘れずにやっておきましょう:

void free_resources()
{
  glDeleteProgram(program);
  glDeleteBuffers(1, &vbo_triangle);
}

今はもう、シーンを描画するたびに、OpenGLがすべての頂点をGPU側で既に持っていることになります。 数千ポリゴンもの大きなシーンの場合、これは莫大なスピードアップになりえます。

OpenGLのバージョンを確認する[編集]

一部のユーザーが持っているグラフィックカードは、OpenGL 2をサポートしていない可能性があります。 これはおそらくプログラムをクラッシュさせたり、不完全なシーンを表示したり、といったことにつながります。 GLEWを使用してこれを確認することができます(glewInit()の呼び出しが成功した後に):

  if (!GLEW_VERSION_2_0) {
    fprintf(stderr, "Error: your graphic card does not support OpenGL 2.0\n");
    return 1;
  }

注意点として、一部のチュートリアルは2.0付近のカードでしか動作しないこともあり、たとえばIntel 945GMが公式でサポートしているOpenGL 1.4以外はシェーダのサポートが限定的です。

GLEWの代用[編集]

他のOpenGLのコードの中で、次のようなヘッダーに出会うかもしれません:

#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glext.h>

OpenGLの拡張機能をロードする必要がない場合で、 かつヘッダが十分に最近のものである場合は、GLEWの代わりにこれを使用することができます。 私たちのテストによると、Windowsユーザーは古いヘッダを持っているかもしれず、GL_VERTEX_SHADERなどのようなシンボルを見失ってしまうので、これらのチュートリアルではGLEWを使用します(プラス私たちが拡張をロードするときのための準備にもなります)。

GLEWとGLeeの比較については、 APIs, Libraries and acronyms セクションもご覧ください。

ユーザーの報告によると、Intel 945GM GPUのGLEWのかわりにこのテクニックを使用することで、シンプルなチュートリアルでは不完全なOpenGL 2.0サポートをバイパスすることができたそうです。 GLEW自体は部分的なサポートを有効にすることもでき、glutInitを呼び出す前に glewExperimental = GL_TRUE;を追加することで行えます。

透明度を有効にする[編集]

今私たちのプログラムはさらにメンテナンスしやすくなっていますが、やっていることは以前とまったく同じです! そういうわけで透明度でちょっと実験しながら、 "昔のテレビ "のエフェクトをつけて三角形を表示してみましょう。

まず、GLUTをアルファといっしょに初期化します:

glutInitDisplayMode(GLUT_RGBA|GLUT_ALPHA|GLUT_DOUBLE|GLUT_DEPTH);

その後、OpenGLで透明度(デフォルトではオフです)を明示的に有効にします:

// Enable alpha
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
The rendered triangle, partially transparent

そして最後に、フラグメントシェーダを変更し、アルファの透明度を定義します:

  gl_FragColor[0] = gl_FragCoord.x/640.0;
  gl_FragColor[1] = gl_FragCoord.y/480.0;
  gl_FragColor[2] = 0.5;
  gl_FragColor[3] = floor(mod(gl_FragCoord.y, 2.0));

modfloor は一般的な数学演算子で、偶数か奇数のどちらのラインにいるかの判断に使用しています。 したがって、2つに1つラインが透明になり、他は不透明になります。