从零开始的太空入侵者——第 2 部分

从零开始的太空入侵者——第 2 部分

首页枪战射击太空入侵者重生更新时间:2024-05-01

在本系列文章中,我将使用 C 创建经典街机游戏太空入侵者的克隆,仅使用几个依赖项。 在这篇文章中,我将设置所需的 OpenGL 着色器来绘制外星精灵!

基于 CPU 的渲染

GPU擅长对大量数据执行操作。 权衡是对 GPU 进行编程并不像对 CPU 进行编程那么容易。 对于我在这里构建的简单空间入侵者克隆,使用缓冲区渲染 CPU 上的所有内容更容易,即表示游戏屏幕上像素的一块内存。 然后可以将缓冲区作为纹理传递给 GPU,并绘制到计算机屏幕上。

struct Buffer { size_t width, height; uint32_t* data; };

Buffer 有一定的宽度和高度。 我们将每个像素表示为 uint32_t,这允许我们为每个像素存储 4 个 8 位颜色值。 在这里,我们将只使用 24 位,分别代表红色、绿色和蓝色通道。 尽管有人可能认为使用 uint8_t 会更好,但我使用 32 位值,因为它使索引更容易。 对于整数类型,我们还需要包含以下标准头,

#include <cstdint>

为了帮助我们将颜色定义为 uint32_t 值,我们定义了以下函数,

uint32_t rgb_to_uint32(uint8_t r, uint8_t g, uint8_t b) { return (r << 24) | (g << 16) | (b << 8) | 255; }

它将最左边的 24 位分别设置为 r、g 和 b 值。 最右边的 8 位设置为 255,虽然我前面提到过,但没有使用 alpha 通道。 如果您想了解更多信息,可以阅读本教程。 然后我们创建一个函数将缓冲区清除为某种颜色,

void buffer_clear(Buffer* buffer, uint32_t color) { for(size_t i = 0; i < buffer->width * buffer->height; i) { buffer->data[i] = color; } }

该函数遍历所有像素并将每个像素设置为给定的颜色。

在主函数中,我们现在初始化缓冲区,

uint32_t clear_color = rgb_to_uint32(0, 128, 0); Buffer buffer; buffer.width = buffer_width; buffer.height = buffer_height; buffer.data = new uint32_t[buffer.width * buffer.height]; buffer_clear(&buffer, clear_color);

这将创建一个 buffer_width 宽度和 buffer_height 高度的缓冲区,并将颜色设置为 clear_color,即绿色。

OpenGL 着色器

创建缓冲区后,我们现在需要设置 OpenGL 以便能够在屏幕上显示缓冲区。在现代 OpenGL 中,大部分职责已从 OpenGL 驱动程序转移到必须编写由 GPU 执行的程序的用户。这些程序称为着色器。此外,OpenGL 定义了渲染管线,着色器可以在管线的不同阶段执行。 Legacy OpenGL 也被称为“固定管线”,因为管线的阶段不是可编程的,而是固定的。两种最重要的着色器类型是顶点着色器和片段着色器。顶点着色器处理顶点数据的处理。最常见的是,它们用于将对象转换为屏幕空间坐标。在几个阶段之后,由顶点着色器处理的图元被光栅化。在这个阶段,片段着色器处理光栅化生成的片段。片段着色器可以输出深度值、可能的模板值和颜色值。顶点着色器和片段着色器是渲染管道运行所需的唯一着色器。虽然你可以用着色器制作漂亮的东西,但在这里我们将构建一对简单的着色器,它们将输出我们在上一节中创建的缓冲区的内容。

对于顶点着色器,我们想要生成一个覆盖屏幕的四边形。不过,有一个技巧可以让我们生成一个全屏三角形,而无需将任何顶点数据传递给着色器。你可以在这篇文章中阅读更多关于这个技巧的信息。这里我只显示我在程序中使用的代码,

const char* vertex_shader = "\n" "#version 330\n" "\n" "noperspective out vec2 TexCoord;\n" "\n" "void main(void){\n" "\n" " TexCoord.x = (gl_VertexID == 2)? 2.0: 0.0;\n" " TexCoord.y = (gl_VertexID == 1)? 2.0: 0.0;\n" " \n" " gl_Position = vec4(2.0 * TexCoord - 1.0, 0.0, 1.0);\n" "}\n";

对于片段着色器,我们只需要对缓冲纹理进行采样并输出采样结果即可,

const char* fragment_shader = "\n" "#version 330\n" "\n" "uniform sampler2D buffer;\n" "noperspective in vec2 TexCoord;\n" "\n" "out vec3 outColor;\n" "\n" "void main(void){\n" " outColor = texture(buffer, TexCoord).rgb;\n" "}\n";

注意顶点着色器的输出,TexCoord,现在是片段着色器的输入。 尽管顶点着色器不需要任何顶点数据传递给它,但我们仍然需要告诉它我们要绘制三个顶点。 为此,我们创建了一个顶点数组对象(VAO),

GLuint fullscreen_triangle_vao; glGenVertexArrays(1, &fullscreen_triangle_vao); glBindVertexArray(fullscreen_triangle_vao);

粗略地说,VAO 是 OpenGL 中的一种结构,它将顶点数据的格式与顶点数据一起存储。

最后,需要将两个着色器编译成 GPU 可以理解的代码并链接到一个着色器程序中。

GLuint shader_id = glCreateProgram();//Create vertex shader { GLuint shader_vp = glCreateShader(GL_VERTEX_SHADER); glShaderSource(shader_vp, 1, &vertex_shader, 0); glCompileShader(shader_vp); validate_shader(shader_vp, vertex_shader); glAttachShader(shader_id, shader_vp); glDeleteShader(shader_vp); }//Create fragment shader { GLuint shader_fp = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(shader_fp, 1, &fragment_shader, 0); glCompileShader(shader_fp); validate_shader(shader_fp, fragment_shader); glAttachShader(shader_id, shader_fp); glDeleteShader(shader_fp); }glLinkProgram(shader_id);if(!validate_program(shader_id)) { fprintf(stderr, "Error while validating shader.\n"); glfwTerminate(); glDeleteVertexArrays(1, &fullscreen_triangle_vao); delete[] buffer.data; return -1; }

在上面的代码块中,首先使用 glCreateProgram 创建了一个着色器程序。 单个着色器使用函数 glCreateShader 创建,并使用 glCompileShader 编译。 使用 glAttachShader 将它们附加到程序后,可以将它们删除。 该程序使用 glLinkProgram 链接。 OpenGL 在编译过程中会输出各种信息,例如 一个 C 编译器,但是我们需要截取这些信息。 为此,我创建了两个简单的函数,validate_shader 和 validate_program,

void validate_shader(GLuint shader, const char* file = 0) { static const unsigned int BUFFER_SIZE = 512; char buffer[BUFFER_SIZE]; GLsizei length = 0; glGetShaderInfoLog(shader, BUFFER_SIZE, &length, buffer); if(length > 0) { printf("Shader %d(%s) compile error: %s\n", shader, (file ? file: ""), buffer); } }bool validate_program(GLuint program) { static const GLsizei BUFFER_SIZE = 512; GLchar buffer[BUFFER_SIZE]; GLsizei length = 0; glGetProgramInfoLog(program, BUFFER_SIZE, &length, buffer); if(length > 0) { printf("Program %d link error: %s\n", program, buffer); return false; } return true; }

缓冲纹理

要将图像数据传输到 GPU,我们使用 OpenGL 纹理。 与 VAO 的情况一样,纹理也是一个对象,它与图像数据一起保存有关数据格式的信息。 我们首先使用 glGenTextures 函数生成纹理,

GLuint buffer_texture; glGenTextures(1, &buffer_texture);

并指定图像格式和一些关于纹理采样行为的标准参数,

glBindTexture(GL_TEXTURE_2D, buffer_texture); glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB8, buffer.width, buffer.height, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, buffer.data ); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

在这里,我们指定图像应该使用 8 位 rgb 格式在内部表示纹理。 调用 glTexImage2D 的最后三个参数指定我们传递给纹理的数据的像素格式; 每个像素都采用 rgba 格式,并表示为 4 个无符号 8 位整数。 前两个 glTexParameteri 调用告诉 GPU 在读取像素时不应用任何过滤(平滑)。 最后两个调用告诉它,如果它试图读取纹理边界之外的内容,它将使用边缘处的值。

我们现在需要将纹理附加到片段着色器中的 uniform sampler2D 变量。 OpenGL 有许多可以附加制服的纹理单元。 我们使用 glGetUniformLocation 获得了统一在着色器中的位置(统一位置可以看作是一种“指针”),并使用 glUniform1i 将统一设置为纹理单元'0',

GLint location = glGetUniformLocation(shader_id, "buffer"); glUniform1i(location, 0);

缓冲区显示

我们终于设置了在屏幕上显示缓冲区所需的一切。 就在游戏循环之前,我们禁用深度测试并绑定我们之前创建的顶点数组,

glDisable(GL_DEPTH_TEST); glBindVertexArray(fullscreen_triangle_vao);

如果我们现在打电话

glDrawArrays(GL_TRIANGLES, 0, 3);

创建的窗口的内容应该是绿色的。

精灵绘图

在上一篇文章中,我们已经创建了一个窗口并在其上显示了一些颜色。 以同样的结果结束这篇文章会有点无聊,所以让我们画点东西。

我首先定义一个简单的精灵,

struct Sprite { size_t width, height; uint8_t* data; };

这只是一堆堆分配的数据,以及精灵的宽度和高度。 sprite 表示为位图,即每个像素由一个位表示,1 表示 sprite 像素“打开”。 然后我们创建一个函数来用指定的颜色在缓冲区中绘制精灵,

void buffer_sprite_draw( Buffer* buffer, const Sprite& sprite, size_t x, size_t y, uint32_t color ){ for(size_t xi = 0; xi < sprite.width; xi) { for(size_t yi = 0; yi < sprite.height; yi) { size_t sy = sprite.height - 1 y - yi; size_t sx = x xi; if(sprite.data[yi * sprite.width xi] && sy < buffer->height && sx < buffer->width) { buffer->data[sy * buffer->width sx] = color; } } } }

该函数只是遍历精灵像素并在指定坐标处绘制“on”像素(如果它们在缓冲区范围内)。 或者,可以用我们定义缓冲区的相同方式来表示精灵,即每个像素都是一个 32 位 rgba 值,alpha 值用于透明度/混合。

在主函数中,我们创建了一个外星精灵,

Sprite alien_sprite; alien_sprite.width = 11; alien_sprite.height = 8; alien_sprite.data = new uint8_t[11 * 8] { 0,0,1,0,0,0,0,0,1,0,0, // ..@.....@.. 0,0,0,1,0,0,0,1,0,0,0, // ...@...@... 0,0,1,1,1,1,1,1,1,0,0, // ..@@@@@@@.. 0,1,1,0,1,1,1,0,1,1,0, // .@@.@@@.@@. 1,1,1,1,1,1,1,1,1,1,1, // @@@@@@@@@@@ 1,0,1,1,1,1,1,1,1,0,1, // @.@@@@@@@.@ 1,0,1,0,0,0,0,0,1,0,1, // @.@.....@.@ 0,0,0,1,1,0,1,1,0,0,0 // ...@@.@@... };

要在 (112, 128) 位置绘制红色精灵,我们调用,

buffer_sprite_draw(&buffer, alien_sprite, 112, 128, rgb_to_uint32(128, 0, 0));

为了准备接下来的步骤,我们清除缓冲区并在游戏循环的每一帧中绘制精灵。 为了更新 OpenGL 纹理,我们调用 glTexSubImage2D

glTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, buffer.width, buffer.height, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, buffer.data );

如果您编译并运行这篇文章的最终代码,在这里,您应该会看到绿色背景中间的红色纹理。

结论

在这篇文章中,我们设置了一些管道,以便我们可以在 CPU 上绘制精灵并将它们显示在我们在上一篇文章中创建的窗口上。 在现代 OpenGL 中,这是一项相当大的工作,因为我们接触了顶点缓冲区对象、着色器和纹理。 设置好我们的绘图程序后,我们现在可以开始专注于更令人兴奋的事情,例如编写游戏逻辑。

查看全文
大家还看了
也许喜欢
更多游戏

Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved