Это первая статья из четырёх. Полный цикл выглядит так:
Привет, окно
Привет, OpenGL
OpenGL расширения
Рисуем треугольник
Ссылки на следующие части добавлю позже —
как только сам разберусь, что там происходитпо мере появления материала
В мире уже есть примерно бесконечное количество туториалов формата "OpenGL Tutorial" (раз, двас).
Возникает логичный вопрос: Зачем ещё один?
Отвечаю: Чтобы наконец перевести их!
Так что я просто их решил перевести с С/С++ на PHP. А вы что подумали?
Я с английского их переводить собрался? Пфф...
Дисклеймер №1
В статях я постараюсь использовать максимально примитивный и простой процедурный код для очевидности. Минимум абстракций и лишнего "мусора", исключительно выполнение задачи.
Такой код:
легко читать
легко понять
легко уволиться после ревью
легко оформить справку в психдиспансере (или по причине кровавых слёз)
Дисклеймер №2
Изначально статьи публиковались на PHPeople, но потом решили: пусть страдают все. Поэтому теперь это ещё и на Хабре.
Исходный Код
Всё можно посмотреть и потыкать на GitHub.
Приступая к работе
Прежде чем что-то писать, неплохо бы понять, что именно мы вообще собираемся делать.
Если сильно упростить, раз уж решили работать с графикой, то наша задача состоит из двух частей:
Создание и управление окном
Работа с графикой внутри этого окна
Идём по порядку.
Перед тем как создать окно
Первый пункт из списка выше предполагает несколько вариантов решения:
Вариант 1.1: Использование встроенного API: WinAPI, GTK, что-нибудь ещё столь же бодрое и отрезвляющее.
Это сложно, долго, местами больно, но возможно. Выглядит такое... Хм... Своеобразно.
Скажем так: Если вам нравится страдать — отличный выбор!
При желании, могу потом как-нибудь организовать всё дело и написать кроссплатформенную работу с окнами на самом PHP с нуля. Но это уже будет тема отдельных постов с пометкой 18+ (назовём: "я просто хотел открыть окно, а стал писать ОС"). А пока...
Вариант 1.2: Более здравый путь — использовать готовые решения, которые предоставляют унифицированное API для работы с окнами.
Например, можно использовать библиотеки GLFW или SDL — обе позволяют реализовать задуманное "парой строк кода".
Так что я бы предложил не страдать и не писать работу с окнами с нуля, а воспользоваться пунктом 1.2. Т.е. взять что-то готовое. Тем более это нас никак не ограничит.
Муки выбора
Как можно понять из моих мыслей выше, относительно сбалансированный вариант между "троллейбусом из буханки хлеба" и "совсем простым способом" — это просто выбрать готовую абстракцию.
1.2.1. GLFW — легковесная библиотека, сосредоточенная исключительно на работе с окнами и заточенная под GPU-рендер, с поддержкой клавиатуры и мыши. Лёгкое, быстрое, красиво написанное (имхо, конечно же).
1.2.2. SDL — комбайн, предоставляющий ядро, содержащее и тот же функционал, что и GLFW (частично), и огромное количество дополнений сверху: Начиная со всяких мьютексов, атомиков, тредов и заканчивая графикой, и даже аудио (карл!). И как все комбайны подобного рода, делает всё не очень хорошо: То что есть работает, однако возможности каждого компонента крайне ограниченны.
1.2.3. Наверняка есть и другие библиотеки, но они или не столь популярны, либо просто не переносимы (нельзя использовать нигде, кроме тех языков под которые оно было разработано, вроде QT заточенный под C++).
Можно взять SDL, где куча вещей уже решена. И хоть там слишком много "лишнего", но заточен он в основном под 2D графику, но зато "всё и сразу".
Я уже делал когда-то демку с помощью SDL2. Не факт, что оно нормально заведётся на PHP 8.5, т.к. писалось изначально под 7.4, но как факт. Так что там есть куда расти.
В отличие от SDL, как я упоминал, есть ещё GLFW с которым я работал меньше. Но методом магического тыка (т.е. просто "потому что") решил остановиться в этот раз на нём.
Как воспользоваться этим счастьем
Опять "муки выбора", но на этот раз из вариантов установки GLFW.
Есть несколько способов:
Способ 1 — самый очевидный для PHP-разработчика. Просто поставить расширение ext-glfw.
Просто? Да.
Интересно? Нет.
Способ 2 — добираться до API через Composer пакет. Например что-то вроде composer require serafim/ffi-sdl.
Уже веселее.
Пакетов под GLFW нет пока что (только SDL).
Способ 3 — сделать всё самому.
И как бы это ни звучало "стрёмно", но это не так уж и сложно. Кажется, это самый весёлый путь.
Установка GLFW
Для установки GLFW достаточно зайти на официальный сайт и скачать:
*.dllдля Windows x64*.dylibдля MacOS x64/arm64/universalА для любителей Linux свой путь придётся выбрать самостоятельно, например через
apt install libglfw3, который под Ubuntu 24.04 поставит версию 3.3.
В итоге, простейшая инструкция по установке в целом будет выглядеть так:
Качаем архив (у меня Windows, так что пользуюсь первым вариантом).
Далее, берём нужный бинарь из архива (например,
glfw3.dllгде-то там в дебрях).Кладём в директорию с проектом. Можно прямо в корень.
Создаём рядом
app.php
Получаем:
app/ ├── app.php └── glfw3.dll (или libglfw.3.dylib)
Проверяем FFI
Теперь, после того как разложили всё по местам, всё что нужно — это просто подключить в PHP данный бинарь.
Мы как раз создали app.php в корне проекта.
<?php // app.php // Windows: $glfw = FFI::cdef('', __DIR__ . '/glfw3.dll'); // Linux: // $glfw = FFI::cdef('', 'libglfw.so.3'); // MacOS: // $glfw = FFI::cdef('', __DIR__ . '/libglfw.3.dylib'); var_dump($glfw);
После запуска PHP (php app.php), если всё хорошо, то получаем результат:
object(FFI)#1 (0) { }
Такой результат будет в том случае, если вы используете относительно стандартную сборку PHP. Ну, например, PPA для PHP от Ondřej Surý (Linux). В случае же Windows, как и для любых других встроенных расширений — достаточно раскомментировать строчку
extension=ffi. В любом случае, надеюсь, включение FFI в PHP у вас не вызовет проблем.
Если не хорошо — добро пожаловать в мир настройки PHP.
Создаём окно
Наконец-то!
Для создания окна нам потребуется несколько функций этого GLFW, поэтому, просто копипастим их к себе: Например, берём тип GLFWmonitor и прямо из исходника всовывем в наш PHP код.
Да, руками. Да, прямо из заголовочных файлов C. Да, это нормально.
Список всех нужных функций и типов (структур) исключительно для проверки работоспособности следующий:
// Было: // $glfw = FFI::cdef('', __DIR__ . '/glfw3.dll'); // Стало: $glfw = FFI::cdef(<<<'C' typedef struct GLFWmonitor GLFWmonitor; typedef struct GLFWwindow GLFWwindow; int glfwInit(void); void glfwTerminate(void); GLFWwindow* glfwCreateWindow(int width, int height, const char* title, GLFWmonitor* monitor, GLFWwindow* share); void glfwDestroyWindow(GLFWwindow* window); int glfwWindowShouldClose(GLFWwindow* window); void glfwPollEvents(void); void glfwSwapBuffers(GLFWwindow* window); C, __DIR__ . '/glfw3.dll'); var_dump($glfw);
Если всё сделано успешно, то результат не изменится. Код успешно отработает как надо и вы получите тот же самый результат.
Форматирование кода
Можно, к слову, чуть-чуть отформатировать этот код, чтоб было удобнее с ним работать и вынести функции GLFW в "ресурсную секцию", добавив __halt_compiler(), вот так:
<?php // app.php $glfw = FFI::cdef( // Считываем данные из "resource" секции файла code: file_get_contents(__FILE__, offset: __COMPILER_HALT_OFFSET__), lib: __DIR__ . '/glfw3.dll', ); var_dump($glfw); // Завершаем работу компилятора и определяем "resource" секцию __halt_compiler(); typedef struct GLFWmonitor GLFWmonitor; typedef struct GLFWwindow GLFWwindow; int glfwInit(void); void glfwTerminate(void); GLFWwindow* glfwCreateWindow(int width, int height, const char* title, GLFWmonitor* monitor, GLFWwindow* share); void glfwDestroyWindow(GLFWwindow* window); int glfwWindowShouldClose(GLFWwindow* window); void glfwPollEvents(void); void glfwSwapBuffers(GLFWwindow* window);
В таком подходе, к слову, можно словить сразу 4 бага в PhpStorm, начиная с поломок форматирования кода по PER CS для Heredoc/Nowdoc, заканчивая проблемами с подсветкой injected lang (внедряемых языков), которые формально в PhpStorm поддерживаются (баге уже 7 лет), но не работают. Но не буду ныть тут по этому поводу...
Сам процесс создания окна состоит из 3х пунктов:
Инициализация библиотеки
Создание окна
Создание обработчика событий окна
Инициализация (1) совсем простая, требуется вызвать glfwInit():
$glfw = FFI::cdef(...); // Метод (функция) glfwInit() возвращает: // - 0 всё плохо (константа GLFW_FALSE) // - 1 можно делать вид, что всё под контролем (константа GLFW_TRUE) if (!$glfw->glfwInit()) { exit(-1); // Можно добавить исключение "по вкусу" } // ...
Создание самого окна (2) тоже не отличается оригинальностью:
// ...инициализация... // Функция для создания окна glfwCreateWindow() вернёт или: // - Объект окна (указатель на структуру) // - NULL в случае ошибки. Не судьба. $window = $glfw->glfwCreateWindow(640, 480, 'Hello World', null, null); if (!$window) { $glfw->glfwTerminate(); exit(-1); } // ...
А для обработки событий (3) требуется создать цикл:
// ...создание окна... // Бесконечный цикл, пока окно не закрыто while (!$glfw->glfwWindowShouldClose($window)) { // Переключает передний и задний буферы вывода. // // Так как мы не пишем ничего в буфер, то он будет пустым. // Если закомментировать, то окно должно стать белым // (*но не факт, это зависит от драйверов/ОС) $glfw->glfwSwapBuffers($window); // Обрабатываем (поллим, т.е. получаем) события // окна (в т.ч. мышь и клавиатуру) $glfw->glfwPollEvents(); } // Завершаем работу $glfw->glfwTerminate();
К слову, можно сравнить этот код с оригинальным кодом на ANSI C из документации GLFW. Кажется, код на PHP вообще ничем почти не отличается от кода на C, а все говорят что PHP — не подходящий для этого язык... Врут поди!
Итоги
Мы с вами проделали с целый путь:
создали окно
не написали ни строчки C
слегка удивились, что это работает
И хоть не писали всё с нуля, а просто воспользовались готовым кроссплатформенным API, но почти повторили возможности расширения GLFW без самого расширения и это оказалось не так уж и сложно.
А дальше начинается самое интересное: Будем пытаться засунуть в это окно OpenGL
Полные исходники всего этого непотребства, что мы проделали, доступны на GitHub.
