В данной статье будет рассказано, как можно довольно просто сделать маленькое интро, используя язык Rust. Будет очень много Unsafe и WinAPI кода, а так же предполагается, что читатель уже хоть немного знаком с OpenGL 3.3
Вот что должно получиться на выходе:

Подготовка инструментов
Необходимое ПО:
-
Nightly-версия Rust (для нестабильных функций и unsafe)
rustup toolchain install nightly
-
Установка целевой архитектуры
rustup target add i686-pc-windows-msvc
-
Crinkler - компрессирующий линкер и упаковщик
Скачать можно здесь: github.com/runestubbe/Crinkler
Создаем проект:
cargo new --bin 4kb
Теперь нам нужно убрать почти всё, что обычно поставляется по умолчанию в обычную программу.
main.rs
// Сами определяем точку входа для нашей программы
#![no_main]
// Отключаем поддержку стандартной библиотеки
#![no_std]
// Заставляем компилятор не менять имя нашей функции на своё
#[unsafe(no_mangle)]
fn main() -> ! {
}
// Теперь нам придется самим определять функцию при ошибках
/*
rust-analyzer может ругаться и указывать на ошибку:
found duplicate lang item `panic_impl`
the lang item is first defined in crate `std` (which `test` depends on)
Но мы не обращаем на это внимание
*/
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_: &PanicInfo<'_>) -> ! {
loop {}
}
Автоматизируем сборку, чтобы избавиться от ручного удаления файлов и запускать всё одной командой
build.bat
del demo.exe
del demo.o
cargo +nightly rustc -Z build-std=core --target i686-pc-windows-msvc --release -Z build-std-features=panic_immediate_abort --bin 4kb -- --emit obj="demo.o"
Crinkler.exe demo.o /OUT:demo.exe /SUBSYSTEM:WINDOWS /ENTRY:main "/LIBPATH:C:\Program Files (x86)\Windows Kits\10\Lib\10.0.22000.0\um\x86" gdi32.lib user32.lib opengl32.lib kernel32.lib winmm.lib
demo.exe
echo %ErrorLevel%
Разберем по порядку:
-Z build-std=core
- сборка только с core версией--target i686-pc-window-msvc
- сборка под x86-Z build-std-features=panic_immediate_abort
- Это сгенерирует каждую панику как инструкцию прерывания без форматирования сообщения о панике.--emit obj="demo.o"
- Создаем объектный файл, который потом слинкуется с windows lib и потом только создастся exe файл/OUT:demo.exe
- имя выходного файла/SUBSYSTEM:WINDOWS
- Здесь может быть два варианта Windows и Console. Windows подразумевает, что мы будем использовать окно/ENTRY:main
- Указываем имя нашей входной точки"/LIBPATH:C:\Program Files (x86)\Windows Kits\10\Lib\10.0.22000.0\um\x86" gdi32.lib user32.lib opengl32.lib kernel32.lib winmm.lib
- Путь до системных файлов
Хорошо, у нас пустая программа которая ничего не делает
Изменим наш Cargo.toml, добавив лишь одну внешнюю зависимость и так же укажем в release сборке, что хотим убрать:
[dependencies]
windows-sys = { version = "0.52.0", features = [
"Win32_Foundation",
"Win32_System",
"Win32_System_Threading",
"Win32_System_Memory",
"Win32_UI_WindowsAndMessaging",
"Win32_Graphics_Gdi",
"Win32_System_LibraryLoader",
"Win32_Graphics_OpenGL",
"Win32_Media",
"Win32_Media_Audio",
"Win32_Media_Multimedia",
"Win32_System_Console",
]}
[profile.release]
# В случаи panic просто аварийно закрываем нашу программу
panic = "abort"
# Никаких отладочных символом в release сборке
strip = true
# Уменьшить количество единиц codegen для повышения оптимизации.
codegen-units = 1
# Оптимизируем размер
opt-level = "z"
Сейчас мы можем уже создать простое окно, я постарался хорошо продукоментировать код
window.rs
use windows_sys::{
Win32::Foundation::*,
Win32::Graphics::{Gdi::*, OpenGL::*},
Win32::System::LibraryLoader::GetModuleHandleA,
Win32::UI::WindowsAndMessaging::*,
};
use core::{mem, ptr};
/// Обработка сообщений от Windows
pub fn handle_message(_window: HWND) -> bool {
let mut msg: mem::MaybeUninit<MSG> = mem::MaybeUninit::uninit();
loop {
unsafe {
// Проверяем есть ли сообщения?
if PeekMessageA(msg.as_mut_ptr(), 0 as HWND, 0, 0, PM_REMOVE) == 0 {
// Если нет, то выходим
return true;
}
// Преобразуем MaybeUninit в инициализированную структуру MSG
let msg = msg.assume_init();
// Проверяем, не является ли сообщение командой на выход
if msg.message == WM_QUIT {
// Получен сигнал завершения работы приложения
return false;
}
// Преобразуем виртуальные клавиши в символы (для клавиатурного ввода)
TranslateMessage(&msg);
// Отправляем сообщение в процедуру окна для обработки
DispatchMessageA(&msg);
}
}
}
/*
HWND - идентификатор окна
HDC - контекст устройства для рисования
*/
#[must_use]
pub fn create(width: i32, height: i32) -> (HWND, HDC) {
unsafe {
let instance = GetModuleHandleA(ptr::null());
// Сами выбираем имя окну
let window_class = b"window\0";
let wc = WNDCLASSA {
hCursor: LoadCursorW(0, IDC_ARROW), // курсор мыши - стрелка
hInstance: instance, // идентификатор программы
lpszClassName: window_class.as_ptr(),// имя класса окна
style: CS_HREDRAW | CS_VREDRAW, // перерисовывать при изменении размера
lpfnWndProc: Some(wndproc), // функция обработки сообщений
/*
Остальные параметры нас не интересуют, подробнее о них
можно узнать из документации к WNDCLASS:
https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassa
*/
cbClsExtra: 0,
cbWndExtra: 0,
hIcon: 0,
hbrBackground: 0,
lpszMenuName: ptr::null(),
};
let _atom = RegisterClassA(&wc);
let title = c"Pixel";
// Создаем окно с расширенными параметрами
let h_wnd = CreateWindowExA(
// Расширенные стили окна (0 - стили по умолчанию)
0,
// Указатель на имя зарегистрированного класса окна
window_class.as_ptr(),
// Заголовок окна (преобразуем в указатель на C-строку)
title.as_ptr() as *const _,
// Стили окна: стандартное перекрывающееся окно + сразу видимое
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
// Позиция окна: используем значения по умолчанию
CW_USEDEFAULT, // X-позиция
CW_USEDEFAULT, // Y-позиция
// Размеры окна в пикселях:
width,
height,
// Родительское окно (None - нет родителя)
0 as HWND,
// Меню (None - нет меню)
0 as HMENU,
// Дескриптор экземпляра приложения
instance,
// Дополнительные параметры создания (None)
ptr::null(),
);
let h_dc: HDC = GetDC(h_wnd);
let mut pfd: PIXELFORMATDESCRIPTOR = mem::zeroed();
pfd.nSize = mem::size_of::<PIXELFORMATDESCRIPTOR>() as u16;
pfd.nVersion = 1;
// Отрисовка в экран, поддрежка OpenGL, Двойная буферизация
pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
pfd.iPixelType = PFD_TYPE_RGBA;
// Количество бит на цвет
pfd.cColorBits = 32;
// 8 бит на Alpha канал(Прозрачность)
pfd.cAlphaBits = 8;
// 24 бита на глубину цвета
pfd.cDepthBits = 24;
// Выбираем формат пикселей из нашего предыдущего описания
let pfd_id = ChoosePixelFormat(h_dc, &pfd);
// устанавливаем формат
SetPixelFormat(h_dc, pfd_id, &pfd);
// wglCreateContext и wglMakeCurrent получаем из Opengl32.lib
let gl_context: HGLRC = wglCreateContext(h_dc);
// Делаем gl context текущим
wglMakeCurrent(h_dc, gl_context);
(h_wnd, h_dc)
}
}
// Внешняя функция с соглашением о вызовах "system" для WinAPI
extern "system" fn wndproc(
window: HWND, // Дескриптор окна, к которому пришло сообщение
message: u32, // Код сообщения (например, WM_PAINT, WM_DESTROY)
wparam: WPARAM, // Дополнительный параметр сообщения (зависит от типа сообщения)
lparam: LPARAM // Дополнительный параметр сообщения (зависит от типа сообщения)
) -> LRESULT { // Возвращаемое значение - результат обработки сообщения
unsafe { // Блок unsafe, так как работаем с сырыми указателями WinAPI
match message { // Обработка различных типов сообщений
WM_PAINT => { // Сообщение о необходимости перерисовать окно
// Сообщаем системе, что вся клиентская область валидна
// и не требует дальнейшей перерисовки
ValidateRect(window, ptr::null());
0 // Возвращаем 0, сообщая об успешной обработке
}
WM_DESTROY => { // Сообщение о закрытии/уничтожении окна
// Помещаем в очередь сообщения сообщение о выходе
// с кодом возврата 0
PostQuitMessage(0);
0 // Возвращаем 0 после инициализации закрытия приложения
}
// Все остальные сообщения передаем стандартной процедуре окна
// для обработки по умолчанию
_ => DefWindowProcA(window, message, wparam, lparam),
}
}
}
обновим main.rs
#[unsafe(no_mangle)]
fn main() -> ! {
let (HWND, HDC) = window::create();
loop {
if !window::handle_message(HWND) {
break;
}
}
unsafe { ExitProcess(0) };
}
Теперь у нас должно появится такое окно

Теперь нужно загрузить функции OpenGL 3.3, для этого придется слинковаться с opengl32.lib для получения функций, которые дадут адреса функций OpenGL 3.3
#![allow(non_snake_case)]
// Доступ к статичной изменяемой переменной
#![allow(static_mut_refs)]
// rust-analyer - Да, мы будем использовать usafe функции в unsafe функциях
#![allow(unsafe_op_in_unsafe_fn)]
use core::mem;
use windows_sys::Win32::{
Graphics::OpenGL::wglGetProcAddress,
System::LibraryLoader::{GetProcAddress, LoadLibraryA},
};
// Используем CVoid как ffi::с_void
pub struct CVoid;
// сдедаем удобные типы
pub type GlBoolean = u8;
pub type GlChar = u8;
pub type GlFloat = f32;
pub type GlEnum = u32;
pub type GlInt = i32;
pub type GlUint = u32;
pub type GlSizeI = i32;
pub type GlSizeIPtr = isize;
/// Определения найдем в каком нибудь gl.h файле
pub const FALSE: GlBoolean = 0;
pub const TRIANGLE_STRIP: GlEnum = 0x0005;
pub const FLOAT: GlEnum = 0x1406;
pub const COLOR: GlEnum = 0x1800;
pub const FRAGMENT_SHADER: GlEnum = 0x8B30;
pub const VERTEX_SHADER: GlEnum = 0x8B31;
pub const COMPILE_STATUS: GlEnum = 0x8B81;
pub const LINK_STATUS: GlEnum = 0x8B82;
pub const ARRAY_BUFFER: GlEnum = 0x8892;
pub const STATIC_DRAW: GlEnum = 0x88E4;
/// Определям порядок наших функций
const GEN_BUFFERS_IDX: u8 = 0;
const GEN_VERTEX_ARRAYS_IDX: u8 = 1;
const BIND_VERTEX_ARRAY_IDX: u8 = 2;
const BIND_BUFFER_IDX: u8 = 3;
const BUFFER_DATA_IDX: u8 = 4;
const CREATE_PROGRAM_IDX: u8 = 5;
const ATTACH_SHADER_IDX: u8 = 6;
const LINK_PROGRAM_IDX: u8 = 7;
const DETACH_SHADER_IDX: u8 = 8;
const CREATE_SHADER_IDX: u8 = 9;
const SHADER_SOURCE_IDX: u8 = 10;
const COMPILE_SHADER_IDX: u8 = 11;
const ENABLE_VERTEX_ATTRIB_ARRAY_IDX: u8 = 12;
const VERTEX_ATTRIB_POINTER_IDX: u8 = 13;
const CLEAR_BUFFERFV_IDX: u8 = 14;
const GET_PROGRAM_IV_IDX: u8 = 15;
const GET_SHADER_IV_IDX: u8 = 16;
const GET_SHADER_INFO_LOG_IDX: u8 = 17;
const WGL_SWAP_INTERVAL_IDX: u8 = 18;
const USE_PROGRAM_IDX: u8 = 19;
const GET_UNIFORM_LOCATION_IDX: u8 = 20;
const UNIFORM_1F_IDX: u8 = 21;
const DRAW_ARRAYS_IDX: u8 = 22;
const UNIFORM_2F_IDX: u8 = 23;
const N_FUNCTIONS: usize = 24;
static mut GL_API: [usize; N_FUNCTIONS] = [0; N_FUNCTIONS];
/*
Все дальнейшие функции будут построены по следующему алгоритму:
1) Получение адреса функции(числа usize)
*GL_API.get_unchecked(NAME_IDX as usize),
2) Вызов transmute и интепретация числа, как адрес функции и вызов функции
P.S
Использование extern "system" означает использование соглашения о
вызовах функции как в windows os - это обязательно, иначе: Segmentation fault
mem::transmute::<_, extern "system" fn(params) -> ()>(
*GL_API.get_unchecked(NAME_IDX as usize),
)(params)
*/
pub unsafe fn GenBuffers(n: GlSizeI, buffers: *mut GlUint) {
mem::transmute::<_, extern "system" fn(GlSizeI, *mut GlUint) -> ()>(
*GL_API.get_unchecked(GEN_BUFFERS_IDX as usize),
)(n, buffers)
}
pub unsafe fn GenVertexArrays(n: GlSizeI, arrays: *mut GlUint) {
mem::transmute::<_, extern "system" fn(GlSizeI, *mut GlUint) -> ()>(
*GL_API.get_unchecked(GEN_VERTEX_ARRAYS_IDX as usize),
)(n, arrays)
}
pub unsafe fn BindVertexArray(array: GlUint) {
mem::transmute::<_, extern "system" fn(GlUint) -> ()>(
*GL_API.get_unchecked(BIND_VERTEX_ARRAY_IDX as usize),
)(array)
}
pub unsafe fn BindBuffer(target: GlEnum, buffer: GlUint) {
mem::transmute::<_, extern "system" fn(GlEnum, GlUint) -> ()>(
*GL_API.get_unchecked(BIND_BUFFER_IDX as usize),
)(target, buffer)
}
pub unsafe fn BufferData(target: GlEnum, size: GlSizeIPtr, data: *const CVoid, usage: GlEnum) {
mem::transmute::<_, extern "system" fn(GlEnum, GlSizeIPtr, *const CVoid, GlEnum) -> ()>(
*GL_API.get_unchecked(BUFFER_DATA_IDX as usize),
)(target, size, data, usage)
}
pub unsafe fn CreateProgram() -> GlUint {
mem::transmute::<_, extern "system" fn() -> GlUint>(
*GL_API.get_unchecked(CREATE_PROGRAM_IDX as usize),
)()
}
pub unsafe fn AttachShader(program: GlUint, shader: GlUint) {
mem::transmute::<_, extern "system" fn(GlUint, GlUint) -> ()>(
*GL_API.get_unchecked(ATTACH_SHADER_IDX as usize),
)(program, shader)
}
pub unsafe fn LinkProgram(program: GlUint) {
mem::transmute::<_, extern "system" fn(GlUint) -> ()>(
*GL_API.get_unchecked(LINK_PROGRAM_IDX as usize),
)(program)
}
pub unsafe fn DetachShader(program: GlUint, shader: GlUint) {
mem::transmute::<_, extern "system" fn(GlUint, GlUint) -> ()>(
*GL_API.get_unchecked(DETACH_SHADER_IDX as usize),
)(program, shader)
}
#[must_use]
pub unsafe fn CreateShader(kind: GlEnum) -> GlUint {
mem::transmute::<_, extern "system" fn(GlEnum) -> GlUint>(
*GL_API.get_unchecked(CREATE_SHADER_IDX as usize),
)(kind)
}
pub unsafe fn ShaderSource(
shader: GlUint,
count: GlSizeI,
string: *const *const GlChar,
length: *const GlInt,
) {
mem::transmute::<_, extern "system" fn(GlUint, GlSizeI, *const *const GlChar, *const GlInt) -> ()>(
*GL_API.get_unchecked(SHADER_SOURCE_IDX as usize),
)(shader, count, string, length)
}
pub unsafe fn CompileShader(shader: GlUint) {
mem::transmute::<_, extern "system" fn(GlUint) -> ()>(
*GL_API.get_unchecked(COMPILE_SHADER_IDX as usize),
)(shader)
}
pub unsafe fn EnableVertexAttribArray(index: GlUint) {
mem::transmute::<_, extern "system" fn(GlUint) -> ()>(
*GL_API.get_unchecked(ENABLE_VERTEX_ATTRIB_ARRAY_IDX as usize),
)(index)
}
pub unsafe fn ClearBufferfv(buffer: GlEnum, drawbuffer: GlInt, value: *const GlFloat) {
mem::transmute::<_, extern "system" fn(GlEnum, GlInt, *const GlFloat) -> ()>(
*GL_API.get_unchecked(CLEAR_BUFFERFV_IDX as usize),
)(buffer, drawbuffer, value)
}
pub unsafe fn VertexAttribPointer(
index: GlUint,
size: GlInt,
type_: GlEnum,
normalized: GlBoolean,
stride: GlSizeI,
pointer: *const CVoid,
) {
mem::transmute::<
_,
extern "system" fn(GlUint, GlInt, GlEnum, GlBoolean, GlSizeI, *const CVoid) -> (),
>(*GL_API.get_unchecked(VERTEX_ATTRIB_POINTER_IDX as usize))(
index, size, type_, normalized, stride, pointer,
)
}
pub unsafe fn GetProgramIv(program: GlUint, pname: GlEnum, params: *mut GlInt) {
mem::transmute::<_, extern "system" fn(GlUint, GlEnum, *mut GlInt) -> ()>(
*GL_API.get_unchecked(GET_PROGRAM_IV_IDX as usize),
)(program, pname, params)
}
pub unsafe fn glGetShaderIv(shader: GlUint, sname: GlEnum, params: *mut GlInt) {
mem::transmute::<_, extern "system" fn(GlUint, GlEnum, *mut GlInt) -> ()>(
*GL_API.get_unchecked(GET_SHADER_IV_IDX as usize),
)(shader, sname, params)
}
pub unsafe fn wglSwapIntervalEXT(interval: GlInt) -> GlUint {
mem::transmute::<_, extern "system" fn(GlInt) -> GlUint>(
*GL_API.get_unchecked(WGL_SWAP_INTERVAL_IDX as usize),
)(interval)
}
pub unsafe fn UseProgram(program: GlUint) {
mem::transmute::<_, extern "system" fn(GlUint) -> ()>(
*GL_API.get_unchecked(USE_PROGRAM_IDX as usize),
)(program)
}
#[must_use]
pub unsafe fn GetUniformLocation(program: GlUint, name: *const GlChar) -> GlInt {
mem::transmute::<_, extern "system" fn(GlUint, *const GlChar) -> GlInt>(
*GL_API.get_unchecked(GET_UNIFORM_LOCATION_IDX as usize),
)(program, name)
}
pub unsafe fn Uniform1f(location: GlInt, v0: GlFloat) {
mem::transmute::<_, extern "system" fn(GlInt, GlFloat)>(
*GL_API.get_unchecked(UNIFORM_1F_IDX as usize),
)(location, v0)
}
pub unsafe fn DrawArrays(mode: GlEnum, first: GlInt, count: GlSizeI) {
mem::transmute::<_, extern "system" fn(GlEnum, GlInt, GlSizeI)>(
*GL_API.get_unchecked(DRAW_ARRAYS_IDX as usize),
)(mode, first, count)
}
pub unsafe fn Uniform2f(location: GlInt, v0: GlFloat, v1: GlFloat) {
mem::transmute::<_, extern "system" fn(GlInt, GlFloat, GlFloat)>(
*GL_API.get_unchecked(UNIFORM_1F_IDX as usize),
)(location, v0, v1)
}
/// Главная функция инициализация gl функций
pub fn init() {
const LOAD_DESCRIPTOR: [(u8, &'static str); N_FUNCTIONS] = [
(GEN_BUFFERS_IDX, "glGenBuffers\0"),
(GEN_VERTEX_ARRAYS_IDX, "glGenVertexArrays\0"),
(BIND_VERTEX_ARRAY_IDX, "glBindVertexArray\0"),
(BIND_BUFFER_IDX, "glBindBuffer\0"),
(BUFFER_DATA_IDX, "glBufferData\0"),
(CREATE_PROGRAM_IDX, "glCreateProgram\0"),
(ATTACH_SHADER_IDX, "glAttachShader\0"),
(LINK_PROGRAM_IDX, "glLinkProgram\0"),
(DETACH_SHADER_IDX, "glDetachShader\0"),
(CREATE_SHADER_IDX, "glCreateShader\0"),
(SHADER_SOURCE_IDX, "glShaderSource\0"),
(COMPILE_SHADER_IDX, "glCompileShader\0"),
(ENABLE_VERTEX_ATTRIB_ARRAY_IDX,"glEnableVertexAttribArray\0",),
(VERTEX_ATTRIB_POINTER_IDX, "glVertexAttribPointer\0"),
(CLEAR_BUFFERFV_IDX, "glClearBufferfv\0"),
(GET_PROGRAM_IV_IDX, "glGetProgramiv\0"),
(GET_SHADER_IV_IDX, "glGetShaderiv\0"),
(GET_SHADER_INFO_LOG_IDX, "glGetShaderInfoLog\0"),
(WGL_SWAP_INTERVAL_IDX, "wglSwapIntervalEXT\0"),
(USE_PROGRAM_IDX, "glUseProgram\0"),
(GET_UNIFORM_LOCATION_IDX, "glGetUniformLocation\0"),
(UNIFORM_1F_IDX, "glUniform1f\0"),
(DRAW_ARRAYS_IDX, "glDrawArrays\0"),
(UNIFORM_2F_IDX, "glUniform2f\0"),
];
/// Загружаем .dll
let handle = unsafe { LoadLibraryA("Opengl32.dll\0".as_ptr() as *const u8) };
for i in 0..LOAD_DESCRIPTOR.len() {
let (index, name) = LOAD_DESCRIPTOR[i];
unsafe {
// сначала получаем функции с помощью GetProcAddress OpneGL 1.0+, потом
// используем wglGetProcAddress, если функция из более современного стандарта
let addr = GetProcAddress(handle, name.as_ptr() as *const u8)
.or_else(|| wglGetProcAddress(name.as_ptr() as *const u8))
.unwrap() as usize;
/// Записываем результат в глобальную статическую переменную
/// Естественно это unsafe операция
GL_API[index as usize] = addr;
}
}
}
pub fn program_from_shaders(vtx_shader: GlUint, frag_shader: GlUint) -> GlUint {
let program_id;
//Можем полуить статус создания программы
//let mut success: GlInt = 1;
unsafe {
program_id = CreateProgram();
AttachShader(program_id, vtx_shader);
AttachShader(program_id, frag_shader);
LinkProgram(program_id);
DetachShader(program_id, vtx_shader);
DetachShader(program_id, frag_shader);
//GetProgramIv(program_id, LINK_STATUS, &mut success);
}
program_id
}
pub fn shader_from_source(shader_source: &str, kind: GlEnum) -> GlUint {
let shader_id;
//Можем полуить статус создания программы
//let mut success: GlInt = 1;
unsafe {
shader_id = CreateShader(kind);
ShaderSource(shader_id, 1, &shader_source.as_ptr(), 0 as *const _);
CompileShader(shader_id);
//glGetShaderIv(shader_id, COMPILE_STATUS, &mut success);
}
shader_id
}
Осталось лишь найти красивый и подходящий шейдер, обычно кучу красивых шейдеров можно найти на сайте: https://www.shadertoy.com/, но их потребуется немного переписать/дописать под оригинальный glsl
Код вершинного шейдера:
#version 330 core
layout(location = 0) in vec3 Pos;
void main() {
gl_Position = vec4(Pos, 1.0);
}
Фрагментный:
#version 330
uniform float iTime;
void main() {
// Разрешение экрана
vec2 iResolution = vec2(720.0, 720.0);
// нормализованные координаты
vec2 uv = gl_FragCoord.xy/iResolution.xy;
// смещаем, так, чтобы каринка была в центре
uv -= 0.5;
uv *= 3.0;
uv.x *= (iResolution.x/iResolution.y);
// результирующий цвет
vec3 color = vec3(0.0, 0.0, 0.0);
// хотим нарисовать 10 шариков
for(int i = 0; i < 10; i++) {
// Возьмем какой-нибудь угол между шариками
float angle = float(i) * 2.0 * 3.14159 / 5.0;
// Посчитаем центр и добавим смещения
vec2 center = vec2(
cos((angle + iTime) * 0.5) * 0.5,
sin((angle + iTime) * 0.5) * 0.5
);
// Считаем длину от нашего центра
float d = length(uv - center);
// Можно считать эту операцию за вычисления интенсивности света
d = 0.02 / d;
// Здесь можно поиграться с цветами
vec3 circleColor = vec3(
0.3 * sin(iTime/10.0 * float(i)) + 0.1,
0.3 * sin(iTime/10.0 * float(i)) + 0.2,
0.3 * sin(iTime/10.0 * float(i)) + 0.3
);
// Записываем результат
color += circleColor * d;
}
gl_FragColor = vec4(color, 1.0);
}
Теперь переделаем наш main.rs
#![no_main]
#![no_std]
// Обязательная строка! Без нее Crinkler не сможет сделать стандартное приложение с окном
#![windows_subsystem = "windows"]
use music::play;
use windows_sys::Win32::{
Graphics::OpenGL::SwapBuffers,
System::{
Memory::{
GetProcessHeap,
HeapAlloc
},
Threading::ExitProcess
}
};
mod gl;
mod window;
#[unsafe(no_mangle)]
fn main() -> ! {
let (HWND, HDC) = window::create();
gl::init();
unsafe {
// concat! Во время компиляции соединит две строки. Добавим заканчивающий \0
let vertex_shader_src: &'static str = concat!(include_str!("../shaders/vs.glsl"), "\0");
let frag_shader_src: &'static str = concat!(include_str!("../shaders/fs.glsl"), "\0");
/// Два треугольника отрисовываем на весь экран,
// последняя вершина соединяется с первой из за TRIANGLE_STRIP
let vertex_coords: [[gl::GlFloat; 3]; 4] =
[
[-1.0, -1.0, 0.0],
[1.0, -1.0, 0.0],
[-1.0, 1.0, 0.0],
[1.0, 1.0, 0.0]
];
let vertex_shader = gl::shader_from_source(vertex_shader_src, gl::VERTEX_SHADER);
let frag_shader = gl::shader_from_source(frag_shader_src, gl::FRAGMENT_SHADER);
let shader_prog = gl::program_from_shaders(vertex_shader, frag_shader);
// OpenGL setup
let mut vertex_buffer_id: gl::GlUint = 0;
let mut vertex_array_id: gl::GlUint = 0;
// Получаем 1 буффер для вершин
gl::GenBuffers(1, &mut vertex_buffer_id);
// Получаем 1 массив вершин
gl::GenVertexArrays(1, &mut vertex_array_id);
// Привязываем Vertex Array Object (VAO) для хранения конфигурации вершинных атрибутов
gl::BindVertexArray(vertex_array_id);
// Привязываем Vertex Buffer Object (VBO) для работы с ним
gl::BindBuffer(gl::ARRAY_BUFFER, vertex_buffer_id);
// Загружаем данные вершин в видеопамять
gl::BufferData(
gl::ARRAY_BUFFER, // Тип буфера: вершинный буфер
mem::size_of::<gl::GlFloat>() as isize * 12,
// Размер данных: 12 float'ов (4 вершины × 3 координаты)
vtx_coords.as_ptr() as *const gl::CVoid, // Указатель на массив с вершин
gl::STATIC_DRAW, // Режим использования: данные не будут изменяться
);
// Включаем вершинный атрибут с индексом 0
gl::EnableVertexAttribArray(0);
// Настраиваем формат и расположение вершинных данных
gl::VertexAttribPointer(
0, //layout(location = 0) в шейдере)
3, // Количество компонентов на вершину (x, y, z)
gl::FLOAT, // Тип данных каждого компонента
gl::FALSE, // Нормализация не требуется
3 * mem::size_of::<gl::GlFloat>() as gl::GlInt,
// Шаг между вершинами (12 байт для 3×float)
0 as *const gl::CVoid,
// Смещение до первых данных (0 - данные начинаются с начала буфера)
);
// Для работы со временем предпочтительнее использовать специализированные функции,
// но Instant::now() доступен только в стандартной библиотеке (std), а не в core.
// В чисто no_std окружении необходимо использовать средства предоставляемые WinAPI
let mut tick = 0.0;
loop {
if !window::handle_message(HWND) {
break;
}
/// Цвет очистки
let rgba = &[0.0, 0.0, 0.0, 0.0];
/// Очищаем экран
gl::ClearBufferfv(gl::COLOR, 0, rgba.as_ptr());
/// Используем уже скомпилированную программу
gl::UseProgram(shader_prog);
// Получаем Uniform
let tick_loc = gl::GetUniformLocation(shader_prog, "iTime\0".as_ptr());
// Обновляем значение Uniform
gl::Uniform1f(tick_loc, tick);
// Bind вершин
gl::BindVertexArray(vertex_array_id);
// Рисуем
gl::DrawArrays(gl::TRIANGLE_STRIP, 0, 4);
// Обновляем буффер кадра
SwapBuffers(HDC);
tick += 0.01;
}
ExitProcess(0);
}
}
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_: &PanicInfo<'_>) -> ! {
loop {}
}
На этом всё! Если вы спросите, зачем использовать Rust для демосцены и кому вообще нужны эти интро — я процитирую Кейва Джонсона:
? Portal 2 ? https://citaty.info/quote/533173
Весь код можно просмотреть у меня в репозитории: https://github.com/olejaaaaaaaa/4kb, но там код немного изменен
Комментарии (6)
Apoheliy
01.09.2025 05:25Сарказм/Душнизм:
Пардонь-те, ну зачем Вам аудитория, которая пишет на чистом Си (не на Rust-е)? Окучить всех по максимуму? Может лучше просто написать "котики"? И вдруг сразу все ломанутся читать?
---
По сути:
Чистый Си вот зачем?
Системное программирование - каким боком оно здесь?
Про OpenGL можно было бы указать. Или про shader-ы.
---
Ещё немного в терминологию: открываем известный поисковик с ответами и получаем:
Интро — это начальный элемент видеоролика. Само слово образовалось от английского «introduction», «вступление».
Вы точно создаёте "начальный элемент видеоролика"?
Может сразу написать "демосцена"? Тот же поисковик:
Демосцена (англ. demoscene) — направление компьютерного искусства, ориентированное на создание аудиовизуальных представлений — «демок». Особенность — выстраивание сюжетного видеоряда, создаваемого в реальном времени компьютером, по принципу работы компьютерных игр.
По-моему, это уже ближе!
---
Сарказм завершён.
---
Понедельник. Первый день осени.
И вообще: С Днём Знаний!
Jijiki
01.09.2025 05:25под приложение на граффику надо настраивать клиент и ноуклиент чтобы получать сообщения событий и обрабатывать - системное, потомучто уже есть интерес на этом этапе у некоторых разработчиков не используя библиотеку настроить окно и прочее, а это винапи
это обыкновенная загрузка как елемент в библиотеках, просто выполненная на шейдере, её можно кинуть в компонент и вызывать когда загрузка )
RodionGork
Симпатишно, но неясно чем вам тут Rust помог. Этот код и на Си будет выглядеть примерно так же :) Даже на ассемблере, пожалуй, особенно куски вроде "главная функция инициализации gl-функций" :) Ну да не важно, графическая/семантическая составляющая заслуживает одобрения, плюсую.
rendov
Как чем? Кликбейтным заголовком, это же очевидно! Вы же вот перешли почитать. Предлагаю кстати еще идею автору, создать статью типа "Пишем 3D игру на bash", где 90% кода будет на юнити, и 10% обычный start.sh который делает проверки окружения, скачивает плеер (при его отсутствии в системе) и запускает игру.
Jijiki
Раст сейфовее Си, проще менеджмент строк поидее ( хотябы строк, когда на С надо нуль-термнатором отбивать строки )
недавно читал что если в программе выделена память под системное что-то, то после закрытия приложения может не очиститься память(например при ошибке), поэтому надо чистить память, вот интересно Раст очистит такую память(известно что в расте подсчет ссылок, но допустим выделена память которая под системное что-то, и тут же например краш) ), так и получается там где надо у Раста ансейф наверно так чтоли, тоесть вот интересно очистит Раст такую память или там требование ансейф как раз