跳轉到內容

OpenGL 程式設計/Glescraft 7

來自華夏公益教科書
一個體素世界。

我們已經看到,透過僅繪製可見的立方體面,甚至透過合併相鄰面,我們可以極大地減少體素世界中要繪製的頂點數。我們可以透過使用幾何著色器進一步減少傳送到 GPU 的資料量。這個想法是將體素面的最緊湊表示傳送到幾何著色器,並讓它生成六個頂點(用於組成一個面的兩個三角形)以及我們可能需要的任何其他資料。我們可以用一個頂點來表示整個體素,但這將意味著幾何著色器不知道要渲染哪些邊,因此它將渲染所有六個邊,無論它們是否被遮擋。繪製被遮擋面的可能性是,GPU 的花費時間比透過這種幾何著色器節省的處理頂點的時間要多。一個更好的方法是為每個面傳送兩個頂點(下圖中的 A 和 C)到頂點著色器,這兩個頂點來自該面的兩個對角線。這樣我們也可以表示合併的面。知道面是矩形並位於 x、y 或 z 平面中,我們可以重建另外兩個角(B 和 D),並且從四個角我們可以建立一個帶狀三角形(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.

以前,我們必須在面的所有六個頂點中傳遞相同的紋理座標。使用幾何著色器,著色器可以同時訪問兩個輸入頂點,因此我們只需要在一個頂點中傳遞紋理座標。著色器可以將其複製到所有六個輸出頂點。這也意味著我們可以將第二個輸入頂點的 w 座標用於其他目的,例如強度資訊。

啟用幾何著色器

[編輯 | 編輯原始碼]

在使用幾何著色器之前,我們可以使用 GLEW 檢查它是否實際受您的 GPU 支援。

  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 會處理其餘的工作。

為幾何著色器建立頂點

[編輯 | 編輯原始碼]

以前,在我們的 update() 函式中,我們必須生成六個頂點,如下所示(對於從負 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);
          }

我們可以簡單地修改程式碼段以生成我們幾何著色器的兩個頂點

          // 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);
          }

注意我們如何在第二個頂點中傳遞強度資訊。

著色器

[編輯 | 編輯原始碼]

幾何著色器如下所示

#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 語句來重新對映紋理座標。我們可以將其移到幾何著色器中嗎?
  • 兩個輸入頂點的順序重要嗎?
華夏公益教科書