跳转到内容

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 语句来重新映射纹理坐标。我们可以将其移到几何着色器中吗?
  • 两个输入顶点的顺序重要吗?
华夏公益教科书