OpenGLプログラミング/Glescraft 7

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

はじめに[編集]

見えるキューブ面をだけ描画することによって、さらには隣接する面をマージすることによって、ボクセル世界で描画する頂点の数を大幅に減らせることはすでに見てきました。 さらに、ジオメトリシェーダを使うことで、GPUに送信するデータ量を低減することができます。 このアイデアは、ジオメトリシェーダーにボクセル面の最もコンパクトな表現を送信し、そして6頂点(面を構成する2つの三角形のために)と他の必要になるかもしれないデータを生成させることです。 1つの頂点でボクセル全体を表すこともできますが、それではジオメトリシェーダーがどちらの側面をレンダリングしたらよいか判断できないことを意味し、なのでそれらが隠されているかどうかにかかわらす、6つの面すべてをレンダリングしてしまいます。 隠された面の描画にかかる時間は、ジオメトリシェーダーなどで頂点を扱うことで節約したぶんよりもGPU時間がかかってしまう可能性があります。 より良い方法は、その面の正反対の2つのコーナーから、2つの頂点(下図のAとC)の各面を頂点シェーダに送信することです。 この方法なら、マージされた面を表現することもできます。 面が矩形であり、x、yまたはz平面にあることがわかれば、他の2つの角(BとD)を再構築することができ、そして4つの角からはぎ取って2つの三角形(BACとACD)を作成することができます。 この方法で、面の法線を再構築することも可能で(ラインACとABの外積を使用して)、ライティング計算のために使用することもできます。

A voxel where the red line with vertices A and C spans the top face. The vertices B and D can be reconstructed in the geometry shader, as well as the normal (blue line) which is the cross product of the red and green lines.

以前は、面の6つの頂点すべてに同じテクスチャ座標を渡す必要がありました。 ジオメトリシェーダーを使用するとき、シェーダは両方の入力頂点へのアクセス権を同時に持っているので、そのうちのひとつのテクスチャ座標だけを渡す必要があります。 シェーダは、6つの出力頂点すべてにそれをコピーすることができます。 また、wの座標を何か他のもののための第2の入力頂点として、例えば強度情報として使用できることも意味します。

Enabling geometry shading[編集]

ジオメトリシェーダーを使用する前に、GPUがそれをサポートしているのかを、GLEWを使ってチェックすることができます:

  if(!GLEW_EXT_geometry_shader4) {
    fprintf(stderr, "No support for geometry shaders found\n");
    exit(1);
  }

どんな種類の入力をジオメトリシェーダーが期待しているのか、そしてどのような出力を生成するのかということを、OpenGLに伝える必要があることを除けば、頂点シェーダやフラグメントシェーダの場合と同じようにジオメトリシェーダーをコンパイルしてリンクします。 私たちの場合では、入力としてLINESを期待し、出力としてTRIANGLE_STRIPSを生成します。 これは次のように行われます:

  GLuint vs, fs, gs;
  if ((vs = create_shader("glescraft.v.glsl", GL_VERTEX_SHADER))   == 0) return 0;
  if ((gs = create_shader("glescraft.g.glsl", GL_GEOMETRY_SHADER_EXT)) == 0) return 0;
  if ((fs = create_shader("glescraft.f.glsl", GL_FRAGMENT_SHADER)) == 0) return 0;

  GLuint program = glCreateProgram();
  glAttachShader(program, vs);
  glAttachShader(program, fs);
  glAttachShader(program, gs);

  glProgramParameteriEXT(program, GL_GEOMETRY_INPUT_TYPE_EXT, GL_LINES);
  glProgramParameteriEXT(program, GL_GEOMETRY_OUTPUT_TYPE_EXT, GL_TRIANGLE_STRIP);

  glLinkProgram(program);

描画するときには、ちょうどGL_LINESを描いたようにすればよく、GPUが残りの面倒をみてくれます。

Creating vertices for the geometry shader[編集]

以前に、update()関数で、以下のような6つの頂点を生成する必要がありました(負のx方向から見た面において):

          // Same block as previous one? Extend it.
          if(vis && z != 0 && blk[x][y][z] == blk[x][y][z - 1]) {
            vertex[i - 5] = byte4(x, y, z + 1, side);
            vertex[i - 2] = byte4(x, y, z + 1, side);
            vertex[i - 1] = byte4(x, y + 1, z + 1, side);
            merged++;
          // Otherwise, add a new quad.
          } else {
            vertex[i++] = byte4(x, y, z, side);
            vertex[i++] = byte4(x, y, z + 1, side);
            vertex[i++] = byte4(x, y + 1, z, side);
            vertex[i++] = byte4(x, y + 1, z, side);
            vertex[i++] = byte4(x, y, z + 1, side);
            vertex[i++] = byte4(x, y + 1, z + 1, side);
          }

シンプルにコードのその部分を変更することで、ジオメトリシェーダ用の2つの頂点を生成することができます:

          // Same block as previous one? Extend it.
          if(vis && z != 0 && blk[x][y][z] == blk[x][y][z - 1]) {
            vertex[i - 2].y = y + 1;
            vertex[i - 1].z = z + 1;
            merged++;
          // Otherwise, add a new quad.
          } else {
            vertex[i++] = byte4(x, y + 1, z, side);
            vertex[i++] = byte4(x, y, z + 1, intensity);
          }

2つめの頂点で強度情報を渡す方法に注目しましょう。

Shaders[編集]

ジオメトリシェーダーは次のとおりです:

#version 120
#extension GL_EXT_geometry_shader4 : enable

varying out vec4 texcoord;
varying out vec3 normal;
varying out float intensity;
uniform mat4 mvp;

const vec3 sundir = normalize(vec3(0.5, 1, 0.25));
const float ambient = 0.5;

void main(void) {
  // Two input vertices will be the first and last vertex of the quad
  vec4 a = gl_PositionIn[0];
  vec4 d = gl_PositionIn[1];

  // Save intensity information from second input vertex
  intensity = d.w / 127.0;
  d.w = a.w;

  // Calculate the middle two vertices of the quad
  vec4 b = a;
  vec4 c = a;

  if(a.y == d.y) { // y same
    c.z = d.z;
    b.x = d.x;
  } else { // x or z same
    b.y = d.y;
    c.xz = d.xz;
  }

  // Calculate surface normal
  normal = normalize(cross(a.xyz - b.xyz, b.xyz - c.xyz));

  // Surface intensity depends on angle of solar light
  // This is the same for all the fragments, so we do the calculation in the geometry shader
  intensity *= ambient + (1 - ambient) * clamp(dot(normal, sundir), 0, 1);

  // Emit the vertices of the quad
  texcoord = a; gl_Position = mvp * vec4(a.xyz, 1); EmitVertex();
  texcoord = b; gl_Position = mvp * vec4(b.xyz, 1); EmitVertex();
  texcoord = c; gl_Position = mvp * vec4(c.xyz, 1); EmitVertex();
  texcoord = d; gl_Position = mvp * vec4(d.xyz, 1); EmitVertex();
  EndPrimitive();
}

頂点シェーダは何もする必要はありませんが、ジオメトリシェーダによって計算された頂点を通り過ぎます:

#version 120

attribute vec4 coord;

void main(void) {
  gl_Position = coord;
}

フラグメントシェーダは次のとおりです:

#version 120

varying vec4 texcoord;
varying vec3 normal;
varying float intensity;
uniform sampler3D texture;

const vec4 fogcolor = vec4(0.6, 0.8, 1.0, 1.0);
const float fogdensity = .00003;

void main(void) {
  vec4 color;

  // Look at normal to see how to map texture coordinates
  if(normal.y != 0) {
    color = texture3D(texture, vec3(texcoord.x, texcoord.z, (texcoord.w + 0.5) / 16.0));
  } else {
    color = texture3D(texture, vec3(texcoord.x + texcoord.z, -texcoord.y, (texcoord.w + 0.5) / 16.0));
  }
  
  // Very cheap "transparency": don't draw pixels with a low alpha value
  if(color.a < 0.4)
    discard;

  // Attenuate
  color *= intensity;

  // Calculate strength of fog
  float z = gl_FragCoord.z / gl_FragCoord.w;
  float fog = clamp(exp(-fogdensity * z * z), 0.2, 1);

  // Final color is a mix of the actual color and the fog color
  gl_FragColor = mix(fogcolor, color, fog);
}

エクササイズ[編集]

  • ボクセルに明度を割り当てるさまざまな方法を試してみましょう。
  • フラグメントシェーダには、テクスチャ座標を再マップするためのif文がまだ含まれています。 ジオメトリシェーダにそれを移動することはできるでしょうか?
  • 2つの頂点入力の順序は重要でしょうか?