发布于 2026-01-06 1 阅读
0

使用 Rust 学习 OpenGL:创建窗口 DEV 全球展示挑战赛,由 Mux 呈现:展示你的项目!

使用 Rust 学习 OpenGL:创建窗口

由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!

动机

计算机图形学融合了技术、艺术和创造力,因此是一个令人兴奋且充满乐趣的学科。近年来,虚拟现实(VR)和增强现实(AR)技术领域发展迅猛,而计算机图形学在其中发挥着重要作用。所有这些都使得图形API的研究比以往任何时候都更加热门。

OpenGL 无疑是目前最简单的图形 API。当然,还有其他一些 API 值得考虑,例如 DirectX、Metal 和 Vulkan。但它们要么不具备跨平台性,要么比 OpenGL 底层得多,因此学习起来更具挑战性。

在本系列文章中,我们将学习 OpenGL 的基础知识,并尝试编写一些图形应用程序。我们将使用 Rust 作为编程语言。传统上,由于性能限制,图形编程与 C/C++ 密切相关。Rust 是 C/C++ 的现代替代方案,它更加安全,并且与 C 语言(我们将使用 C 语言调用 OpenGL API)具有良好的互操作性。阅读本文并不要求您具备 Rust 编程经验,但如果您有编程经验会更好。如果您想了解更多关于 Rust 的知识, Rust 书籍是一个很好的入门资源。

所有文章的代码都可以在我的GitHub 仓库中找到。该项目会根据系列中每篇文章的代码状态创建不同的分支。

本文将探讨如何创建窗口、初始化 OpenGL 上下文以及调用一些基本 API 来使用所需的颜色清除窗口。

一点OpenGL理论

在正式开始我们的探索之旅前,我们首先应该明确OpenGL究竟是什么。OpenGL可以被视为一个API,它提供了一系列可用于操作图形和图像的函数。然而,OpenGL并非API,而仅仅是一个规范,它规定了每个函数的输出结果以及执行方式。OpenGL规范并未提供具体的实现细节,因此,只要最终结果符合规范,库的实现方式可以有所不同。

通常我们可以把 OpenGL 的实现看作是一个大型状态机:一组定义 OpenGL 运行方式的变量。OpenGL 的状态通常被称为OpenGL 上下文。我们经常通过设置一些选项、操作一些缓冲区来改变 OpenGL 的状态,然后使用当前上下文进行渲染。

例如,当我们告诉 OpenGL 要用蓝色而不是黑色清除缓冲区时,我们实际上是通过改变某个上下文变量来改变 OpenGL 的状态,该上下文变量决定了 OpenGL 的绘制方式。一旦我们通过告诉 OpenGL 应该用蓝色清除缓冲区来改变上下文,下一次绘制调用就会默认使用蓝色来填充缓冲区。

OpenGL 的设计理念中包含了多种抽象概念。其中之一就体现object在 OpenGL 内部。你可以把这里的对象理解为一组选项的集合,它代表了 OpenGL 状态的一个子集。

想象一下,如果我们想要一个对象来表示绘图窗口的设置。它可以包含窗口的大小、支持的颜色数量等等。在 OpenGL 中使用对象时,我们通常会遵循以下工作流程:首先创建一个对象并存储其属性id,然后通过绑定将对象绑定到目标位置,设置对象的选项,最后通过将当前对象设置为 `null` 来id解除绑定。以下是一个更改窗口大小的近似示例:id0

// create object
let mut object_id = 0;
gl::GenObject(1, &mut object_id);
// bind/assign object to context
gl::BindObject(gl::WINDOW_TARGET, object_id);
// set options of object currently bound to gl::WINDOW_TARGET
gl::SetObjectOption(gl::WINDOW_TARGET, GL::OPTION_WINDOW_WIDTH,  800);
gl::SetObjectOption(gl::WINDOW_TARGET, GL::OPTION_WINDOW_HEIGHT, 600);
// set context target back to default
gl::BindObject(gl::WINDOW_TARGET, 0);
Enter fullscreen mode Exit fullscreen mode

现在,我们已经对 OpenGL 规范和库以及 OpenGL 的底层工作原理有了一些了解,是时候深入研究一些更实际的东西了。

设置项目

让我们从头开始创建一个新的 Rust 项目。我们将使用Cargo 来完成这项工作。Cargo 是 Rust 的构建系统和包管理器。这里我们假设 Rust 和 Cargo 已经安装在您的系统中,如果您遇到问题,Cargo请参考Rust 书籍。要创建一个新项目,只需运行:

$ cargo new learn_gl_with_rust
$ cd learn_gl_with_rust
Enter fullscreen mode Exit fullscreen mode

如果您列出目录中的文件,您会发现 Cargo 为我们生成了两个文件和一个目录:一个 Cargo.toml 文件和一个包含 main.rs 文件的 src 目录。我们将使用 main.rs 作为入口点,从这里开始编写我们的应用程序。

在开始创建图形之前,我们需要创建一个 OpenGL 上下文和一个用于绘制的应用程序窗口。然而,这些操作因操作系统而异,而 OpenGL 的设计初衷就是为了抽象化这些操作。这意味着我们需要自行创建窗口、定义上下文并处理用户输入。

幸运的是,有很多库提供了这种功能,其中一些是专门针对 OpenGL 的。这些库省去了我们所有与操作系统相关的工作,并为我们提供了一个窗口和一个 OpenGL 上下文来进行渲染。glutin 就是其中一个库它允许我们创建 OpenGL 上下文、定义窗口参数并处理用户输入,这完全满足我们的需求。

为了使用该glutin库,我们需要将其作为依赖项添加到 Cargo.toml 文件中:

[dependencies]
glutin = "0.29.1"
Enter fullscreen mode Exit fullscreen mode

由于 OpenGL 只是一个标准/规范,并且存在许多不同的 OpenGL 实现版本,因此其大多数函数的位置在编译时是未知的,需要在运行时查询。开发者的任务是获取所需函数的位置,并将其存储在函数指针中以供后续使用。

值得庆幸的是,Rust 也为此目的提供了 crate,其中gl是一个很受欢迎的 crate,我们将在项目中使用它。

要将gl库添加到依赖项中,我们需要Cargo.toml按如下方式修改文件:

[dependencies]
glutin = "0.29.1"
gl = "0.14.0"
Enter fullscreen mode Exit fullscreen mode

创建窗口

到目前为止,我们已经搭建了一个项目,并确定了创建应用程序窗口和 OpenGL 上下文所需的依赖项。现在是时候实际创建窗口了。

初始化 OpenGL 窗口glutin可以通过以下步骤完成:

  • 创建一个EventLoop用于处理窗口和设备事件的程序。
  • 使用以下方法指定窗口特定参数glium::glutin::WindowBuilder::new()。我们在这里设置窗口标题。
  • 使用 `OpenGL 上下文` 指定 OpenGL 特定属性glium::glutin::ContextBuilder::new()并构建 OpenGL 上下文。我们指定glutin3.3使用的 OpenGL 版本。
  • 将窗口上下文设置为调用线程的当前上下文。
let event_loop = EventLoop::new();
let window = WindowBuilder::new().with_title("Learn OpenGL with Rust");

let gl_context = ContextBuilder::new()
    .with_gl(GlRequest::Specific(Api::OpenGl, (3, 3)))
    .build_windowed(window, &event_loop)
    .expect("Cannot create windowed context");

let gl_context = unsafe {
    gl_context
        .make_current()
        .expect("Failed to make context current")
};
Enter fullscreen mode Exit fullscreen mode

之前我们提到过,glcrate 管理 OpenGL 的函数指针,因此我们需要gl在调用任何 OpenGL 函数之前进行初始化:

gl::load_with(|ptr| gl_context.get_proc_address(ptr) as *const _);
Enter fullscreen mode Exit fullscreen mode

目前,应用程序一旦创建窗口就会立即退出并关闭窗口。我们希望应用程序能够持续绘制图像并处理用户输入,直到程序被明确告知停止为止。因此,我们需要无限循环,直到检测到事件CloseRequested发生。以下代码展示了如何使用该方法run实现event_loop

event_loop.run(move |event, _, control_flow| {
    *control_flow = ControlFlow::Wait;

    match event {
        Event::LoopDestroyed => (),
        Event::WindowEvent { event, .. } => match event {
            WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
            _ => (),
        },
        _ => (),
    }
});
Enter fullscreen mode Exit fullscreen mode

同样,我们可以处理Resized每次窗口大小调整时触发的窗口事件。我们将窗口的新大小传递给gl_context调整视口的方法:

WindowEvent::Resized(physical_size) => gl_context.resize(physical_size),
Enter fullscreen mode Exit fullscreen mode

OpenGL 使用双缓冲技术。它不是直接在窗口上绘制图像,而是先绘制到内存中存储的图像上。绘制完成后,再将该图像复制到窗口。为了实现这一点,我们会在接收到窗口事件时swap_buffers调用相应的函数gl_contextRedrawRequested

为了测试是否真的有效,我们需要用我们选择的颜色清除屏幕。否则,我们仍然会看到上一帧的结果。我们可以使用gl::Clear传递参数的方法来清除屏幕的颜色缓冲区gl::COLOR_BUFFER_BIT

Event::RedrawRequested(_) => {
    unsafe {
        gl::ClearColor(0.0, 0.0, 1.0, 1.0);
        gl::Clear(gl::COLOR_BUFFER_BIT);
    }
    gl_context.swap_buffers().unwrap();
}
Enter fullscreen mode Exit fullscreen mode

现在运行程序,cargo run你应该会看到一个漂亮的蓝色背景窗口。

概括

今天我们学习了一些 OpenGL 理论,以及如何创建窗口、初始化 OpenGL 上下文并调用一些基本 API 来用所需的颜色清除窗口。

下次我们将讨论OpenGL图形管线的工作原理以及如何使用着色器对其进行配置。敬请期待!

如果您觉得这篇文章有趣,请点赞并订阅以获取更新。

文章来源:https://dev.to/samkevich/learn-opengl-with-rust-creating-a-window-1792