Это вторая статья из четырёх. Полный цикл выглядит так:

Ссылки на следующие части добавлю позже — по мере появления материала

Исходный Код

Все исходники второй главы можно потыкать на GitHub.


В предыдущей статье мы открыли окно. Теперь давайте его не просто игнорировать.

Структура проекта

Если вы заглядывали в исходники из первой части, то могли заметить одну небольшую, но важную деталь:

Я вынес GLFW в отдельный файл src/glfw3.php и добавил туда поддержку разных ОС, хоть и не писал об этом в самой статье.

Получилось примерно так:

<?php

return FFI::cdef(
    code: (string) file_get_contents(__FILE__, offset: __COMPILER_HALT_OFFSET__),
    lib: $_SERVER['GLFW3_LIB'] ?? match (PHP_OS_FAMILY) {
        'Windows' => 'glfw3.dll',
        'Linux' => 'libglfw.so.3',
        'Darwin' => 'glfw3.dylib',
    },
);

__halt_compiler();

// ...заголовки...

А структура проекта стала такой:

app/
 ├── src/
 ├    └── glfw3.php
 ├── app.php
 └── glfw3.dll # в .gitignore

Т.е. я создал "мусорку" в src, куда будет скидываться всякий "shared" код.

Поддержка разных ОС

Сразу честно: Такая поддержка, что я там впихнул — довольно маргинальная.

Так как в коде указаны относительные пути, то поведение самого код зависит от:

  • CWD (текущей рабочей директории)

  • PATH (параметра окружения с путями ко всяким бинарям и проч)

  • возможно ещё от всяких LD_LIBRARY_PATH (параметра окружения для линкера С), но это не точно (не проверял)

  • фазы луны (иногда кажется, что и от неё)

Более того, в TS и ZTS сборках PHP поведение параметров окружения и рабочей директории отличается. Сборка PHP TS вместо работы непосредственно с данными — реализует "костыль", виртуальную хешмапу, которая вообще никак не связана с реальными переменными ОС. Но в нашем случае проблем никаких из-за этого не должно возникнуть.

В любом случае, в мире PHP такое обращение с кодом считается незаконным, потому что он начинает зависеть от внешних факторов и ведёт себя как хочет. Гораздо надёжнее использовать явные пути:

return FFI::cdef(..., lib: __DIR__ . '/path/to/lib.<dll/so/dylib/etc>');

Но так как у нас и так тут хватает извращений с кодом, то "и так сойдёт".

При желании, вы всегда можете указать явный путь к библиотеке в переменной окружения GLFW3_LIB как явно:

$ GLFW3_LIB=/path/to/lib.so php ./app.php

Так и через PHP код:

// app.php

$_SERVER['GLFW3_LIB'] = '/path/to/your/glfw3.dll';

$glfw = require __DIR__ . '/src/glfw3.php';

Будем считать что такой вариант решения всяких проблем делает написанный код очень гибким и масштабируемым.

Наконец-то, OpenGL

В любом случае, сейчас предстоит сделать тоже самое, что было проделано для GLFW, но уже для OpenGL!

Windows-режим "повезло"

Говоря о Windows — тут всё просто:

  • Используем opengl32.dll

  • Ничего устанавливать не нужно

  • Базовая версия (1.0 и даже куски 1.1) есть "из коробки"

  • Остальное подтянется через драйвер видеокарты (вплоть до 4.6)

Как именно "подтянется", что за загрузчики, причём тут сам GLFW и прочие WGL/GLX - будет в следующей статье.

Минимальный пример:

return FFI::cdef(
    code: (string) file_get_contents(__FILE__, offset: __COMPILER_HALT_OFFSET__),
    lib: $_SERVER['OPENGL_LIB'] ?? match (PHP_OS_FAMILY) {
        'Windows' => 'opengl32.dll',
        // TODO
    },
);

__halt_compiler();

// TODO

Важно (!!!)
Сейчас мы подключаем OpenGL отдельно от GLFW — как будто это два независимых мира и если у вас такое не работает (я про Linux, macOS и прочие, которые я просто проигнорировал) — не переживайте.
Позже мы всё равно перейдём на использование OpenGL через GLFW и починим работоспособность на других ОС.

Подключаем

Всё просто: Берём свежесозданный src/opengl.php в правую руку и app.php в левую. Скрещиваем и получаем:

<?php
// app.php

$glfw = require __DIR__ . '/src/glfw3.php';
$opengl = require __DIR__ . '/src/opengl.php';

// ...а дальше инициализация и запуск окна, как раньше

Ура, мы подключили OpenGL и можем начать его интегрировать внутрь программы!

Интегрируем

Чтобы OpenGL начал работать с нашим окном, нужно:

  1. Добавить константы GLFW и OpenGL

  2. Добавить функции оных

  3. Вызвать всё это в нужном порядке

Звучит просто. Как и всё в программировании, до первого запуска.

Конфигурация OpenGL

Во-первых, начнём с добавления некоторых правил нашего контекста в которых я не буду сильно уходить от классических рекомендаций.

Для GLFW, так как он предоставляет глубокую интеграцию графического API (не только OpenGL), достаточно прописать несколько правил конфигурации сразу после инициализации:

// app.php
// ...наша инициализация была тут, т.е.
// if (!$glfw->glfwInit()) { ... }


// Включаем мультисемплинг (сглаживание). Значение 4 означает 4х-кратное MSAA
// сглаживание, чтобы наши будущие шедевры выглядели чуть менее "зубастыми".
//
// Конечно, это всё можно сделать через OpenGL, а не через GLFW,
// но почему бы и нет?
$glfw->glfwWindowHint(GLFW_SAMPLES, 4);

// Тут мы выставляем версию OpenGL, которую мы хотим использовать.
// Версия 3.3 и довольно современная, и довольно хорошо распространённая версия.
// При желании в�� можете выставить и самую максмальную.
//
// Если указанная версия не поддерживается видеокартой,
// то при создании окна вы получите ошибку.
//
// Предлагаю выставить версию 42 и убедиться в этом самостоятельно ;)
$glfw->glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
$glfw->glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);

// А вот эта опция включает режим "прямой совместимости".
// Для OpenGL 3+ означает, что мы НЕ будем использовать старые функции OpenGL.
$glfw->glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, 1);

// Выбираем профиль OpenGL.
// Значение GLFW_OPENGL_CORE_PROFILE говорит о том, что мы хотим использовать
// исключительно современные возможности OpenGL, без обратной совместимости
// со старым API.
//
// Нахрена 100500 разных опций для того, чтобы убедить рантайм в том,
// что мы не будем заниматься некромантией — покрыто мраком.
$glfw->glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);


// ...а тут мы создавали окно, т.е.
// $window = $glfw->glfwCreateWindow(640, 480, 'Hello World', null, null);

Откуда взялись эти константы и функции?

Ого, вы внимательны, если заметили, что я начал использовать какие-то новые константы, да и функции тоже.

Конечно же они не появились из воздуха. Их требуется определить в src/glfw3.php:

<?php
// src/glfw3.php

// Константы, которые мы использовали можно добавить сверху
//
// Значения для них взяты прямо из исходников GLFW, например:
// https://github.com/glfw/glfw/blob/3.4/include/GLFW/glfw3.h#L1008
const GLFW_SAMPLES = 0x0002100D;
const GLFW_CONTEXT_VERSION_MAJOR = 0x00022002;
const GLFW_CONTEXT_VERSION_MINOR = 0x00022003;
const GLFW_OPENGL_FORWARD_COMPAT = 0x00022006;
const GLFW_OPENGL_PROFILE = 0x00022008;
const GLFW_OPENGL_CORE_PROFILE = 0x00032001;

// return FFI::cdef(...);

__halt_compiler();

// ...

// Новая функция "glfwWindowHint()" так же скопипащена из заголовков:
// https://github.com/glfw/glfw/blob/3.4/include/GLFW/glfw3.h#L3053
void glfwWindowHint(int hint, int value);

Подсветка PHP-кода на Хабре, как вы могли заметить, великолепно справляется с таким!

Если вы всё сделали правильно, то результат так же не изменится.

Главное что никаких ошибок не возникает! В мире низкоуровневой графики отсутствие ошибок — уже успех.

Привязываем OpenGL к окну

Теперь нам нужно проделать ещё 2 операции.

Во-первых, нужно сказать OpenGL в каком окне он вообще должен работать с помощью метода/функции glfwMakeContextCurrent():

// app.php

// ...сразу после создания окна, т.е.
// $window = $glfw->glfwCreateWindow(640, 480, 'Hello World', null, null);

// Сообщаем о том, что вот этот `$window` теперь текущий контекст
$glfw->glfwMakeContextCurrent($window);

// ...а тут у нас идёт уже обработка оного, т.е.
// while (!$glfw->glfwWindowShouldClose($window)) { ... }

И, соответсвенно, объявляем оную функцию в src/glfw3.php:

// src/glfw3.php

__halt_compiler();

// ...
void glfwMakeContextCurrent(GLFWwindow* window);

Теперь OpenGL знает: "Ага, вот сюда рисовать".

Проверяем!

Давайте проверим?

Например, функцию очистки, которую мы сейчас добавим:

// app.php

// ...там, где мы начинаем обрабатывать события окна
while (!$glfw->glfwWindowShouldClose($window)) {
    // Очищает цветовой буфер (то есть всё, что было нарисовано в предыдущем
    // кадре) с использованием текущего цвета очистки (чёрного, т.к. мы его
    // не задали)
    $opengl->glClear(GL_COLOR_BUFFER_BIT);

    // $glfw->glfwSwapBuffers($window);
    // etc...
}

И, соответсвенно, её декларация в src/opengl.php:

<?php
// src/opengl.php

// Плюс константа
const GL_COLOR_BUFFER_BIT = 0x00004000;

// ...этот ретурн не трогаем
return FFI::cdef(...);

__halt_compiler();

// Плюс функция OpenGL (и тайпдеф)
typedef unsigned int GLbitfield;
void glClear(GLbitfield mask);

Итоги

Вы не поверите, но хоть результат и не поменялся, но...

  • У нас есть окно (оно и было раньше)

  • У нас есть OpenGL

  • У нас есть связь между ними (присобачили одно к другому)

  • Мы уже можем рисовать! Просто пока не рисуем...

Дальше начинается немного жести. Будем кочевряжиться с буферами, шейдерами и чуть-чуть страдать.


По традиции, полные исходники второй части, доступны на GitHub.