Подробное объяснение принципов компьютерной графики и программирования на OpenGL с примерами на C++.
Key Takeaways
- OpenGL — это спецификация, реализуемая производителями видеокарт, а не просто библиотека.
- Графический конвейер состоит из нескольких этапов, большинство из которых можно программировать самостоятельно.
- Вершинные и фрагментарные шейдеры — ключевые программируемые части для создания графики.
- Для работы с OpenGL обычно используют GLFW для окон и GLEW для доступа к функциям видеокарты.
- Понимание линейной алгебры и тригонометрии важно для работы с 3D-графикой.
What the video covers
- Автор рассказывает о создании 3D-графики с использованием OpenGL и C++ с нуля.
- Объясняется, что OpenGL — это спецификация, а не библиотека, и как видеокарты реализуют её функции.
- Рассматривается графический конвейер: от передачи данных в видеопамять до вывода пикселей на экран.
- Подробно описываются этапы конвейера: вершинный шейдер, тесселяция, геометрический шейдер, растеризация и фрагментарный шейдер.
- Поясняется работа с буферами и форматами данных для вершин и текстур.
- Демонстрируется создание окна с помощью GLFW и подключение GLEW для доступа к функциям OpenGL.
- Обсуждается нормализованная система координат устройства (NDC) и её особенности.
- Автор делится практическими советами по программированию шейдеров на GLSL.
- Рассматриваются основы освещения и работа с цветом в OpenGL.
- В конце приводятся полезные ресурсы для дальнейшего изучения OpenGL и графического программирования.
Chapters
- 00:00Введение и цели видео
- 03:34Что такое OpenGL и его спецификация
- 06:29Графический конвейер и этапы обработки
- 09:44Вершинный и фрагментарный шейдеры
- 13:08Практическое создание окна и настройка OpenGL
- 20:12Работа с координатами и буферами
- 30:12Основы освещения и цвета в OpenGL
- 51:04Заключение и рекомендации по ресурсам
Full Transcript — Download SRT & Markdown
Speaker A
Всем привет. Год назад я выпускал видео про свой трёхмерный движок, который очень многим понравился. И в нём я пообещал, что, возможно, сделаю его по-нормальному, а не поколхозному, как в прошлый раз. Так вот, этот момент настал. Сегодня я постараюсь максимально
Speaker A
подробно и понятно рассказать с нуля, как устроена компьютерная трёхмерная графика на самом деле, так как её используют на практике.
Speaker A
Мы поговорим о том, как запрограммировать вашу видеокарту, где пригодится линейная алгебра и тригонометрия в жизни, и как работает почти реалистичное освещение. Видео будет долгое, но очень интересное, так что запасайтесь попкорном. С вами Мимоторк, и мы начинаем.
Speaker A
Перед началом немного объясню, что вообще будет происходить в этом видео. Я буду рассказывать про графическое программирование с применением графического интерфейса OpenGL. У меня нет цели сделать какой-то конечный продукт, там, игру или движок. Я хочу скорее показать, как создавать свои игры
Speaker A
или свои движки. Поскольку почти вся инфа про OpenGL в интернете выложена примерно по одной схеме, я решил ничего нового не придумывать и тоже рассказывать всё в рамках этой схемы. Также это видео не является полным пошаговым руководством по OpenGL, но в конце видео я оставлю
Speaker A
ресурсы, которыми я пользовался сам и которые уже таковыми являются. Вроде бы всё, что нужно, я сказал, так что переходим к делу.
Speaker A
Давайте поговорим о том, что вообще такое OpenGL. Некоторые думают, что это какая-то библиотека, но это не совсем так. На самом деле это спецификация, то есть просто описание каких-то функций и того, что они должны делать. А реализацию этих функций пишут
Speaker A
производители видеокарт, например, Nvidia, и вшивают внутрь конкретной видеокарты. В итоге OpenGL предоставляет относительно низкоуровневое управление видеокартой, что позволяет достичь огромной гибкости и быстрой работы программы. Но с другой стороны, это делает изучение графического программирования очень сложным. Ведь, как мы видим, чтобы
Speaker A
нарисовать только один треугольник, нужно очень много знаний и где-то 160 строк кода. Но в любом случае, давайте сначала создадим окно, в котором будет работать наша программа. В прошлый раз я использовал SFML, но эта библиотека перегружена всяким ненужным для нас
Speaker A
функционалом, и поэтому обычно для OpenGL используют библиотеку GLFW. Она очень лёгкая, быстрая, но при этом в ней есть все функции, которые нам понадобятся.
Speaker A
Чтобы получить доступ ко всем функциям из OpenGL, которые находятся на видеокарте, воспользуемся другой небольшой библиотекой, которая называется GLEW. Теперь можно подключить эти библиотеки, создать окно и вызвать GLEW. Ну и убедиться, что ничего не сломалось. Дальше вот этой строчкой
Speaker A
назначаем видеокарте это окно и в основном цикле закрашиваем его в чёрный и очищаем экран. Если запустить программу, то увидим правильно чёрное окно. Можно его сделать какого-нибудь другого цвета, например, RGB 42, 53.
Speaker A
Правда, в OpenGL цвет хранится не как целое число от нуля до 255, а как вещественное от нуля до единицы. Поэтому не забываем поделить значение RGB на 255. И получается вот такое сиреневое окно.
Speaker A
Теперь было бы неплохо что-то нарисовать в этом новом окне. Но как это сделать? На самом деле это не так уж и просто. И давайте сначала поймём, как вообще работает OpenGL. Мы подаём на вход какие-то данные, например, координаты
Speaker A
вершин треугольника или цвет вершины треугольника. А на выходе хотим получить нарисованный примитив на экране.
Speaker A
Примитивами могут быть точки, линии, треугольники, четырёхугольники, многоугольники и так далее, но почти всегда ими будут именно треугольники.
Speaker A
Процесс, который всё это делает, называется графическим конвейером. И графический конвейер нужно очень хорошо понимать. Так что поговорим о нём поподробнее. Итак, сначала мы должны передать в графическую память видеокарты входные данные. И эти данные передаются с помощью специальных пакетов, которые
Speaker A
называются буферами. Первым делом происходит конфигурация этого буфера. Видеокарта получает просто кучу байтов какой-то информации, но нам нужно самим задать, как эту информацию интерпретировать. Например, мы подаём некий набор точек, которые правильно называть вершинами. И пусть каждая вершина занимает пять флотов или 20
Speaker A
байт. При этом первые три флота отведены для координаты этой точки в пространстве, а последние два флота отведены для координаты текстуры. И вот это соотношение мы сами должны задать для правильной работы программы. Далее исполняется вершинный шейдер.
Speaker A
Вообще шейдер — это мини-программа, которая исполняется на видеокарте. В OpenGL они пишутся на языке GLSL, что расшифровывается как OpenGL Shading Language. Существует довольно много видов шейдеров, но пока что мы подробно обсудим только два: вершинный и фрагментарный. Но пока что о вершинном.
Speaker A
Такой шейдер исполняется для каждой вершины в модели, и его главная цель — определить итоговое положение этой вершины на экране. Но кроме того, он ещё может, например, передать какие-то данные дальше по конвейеру для других шейдеров, например, цвет этой вершины.
Speaker A
Далее происходит тесселяция. Тесселяция — это процесс как бы искусственного увеличения количества геометрии. Например, с её помощью можно превращать лоуполи модели во вполне детализированные и таким образом экономить память. Далее исполняется геометрический шейдер. Он также дополнительно изменяет некоторую геометрию. Например, это используется
Speaker A
для отрисовки систем частиц. Изначально каждая частица представляет собой просто точку, а геометрический шейдер превращает её в квадратик или шарик.
Speaker A
Тесселяция и геометрический шейдер — не обязательные элементы конвейера. И вообще это довольно сложный процесс, который используется на продвинутом уровне для довольно специфических задач.
Speaker A
Так что подробно мы их обсуждать не будем. Дальше происходит постобработка вершин и сборка формы примитива. Опять же, это может быть важным шагом после геометрического шейдера или тесселяции, но пока что там ничего интересного не происходит, потому что мы рисуем обычные
Speaker A
треугольники. Дальше происходит растеризация и клиппинг. На этом этапе сначала определяется, какие треугольники находятся в области видимости, и затем на каких конкретных пикселях будут нарисованы видимые части примитивов.
Speaker A
Также на этом этапе происходит интерполяция, то есть нахождение промежуточных значений цвета, координат и других параметров для каждого пикселя.
Speaker A
Например, у нас есть линия, у которой левая граница синяя, а правая красная. Если проинтерполировать цвет по этой линии, то получится какой-то градиент.
Speaker A
Что-то похожее будет происходить и для координат. И ещё это будет происходить в 3D, а не в 1D. Дальше идёт фрагментарный шейдер. Этот шейдер исполняется для каждого фрагмента, по сути пикселя, и в итоге должен определить его цвет. Ну и в
Speaker A
конце происходят различные проверки пикселя, например, проверка глубины, о которой мы подробно поговорим попозже. И ещё может, например, происходить смешивание цветов. Это нужно для отображения полупрозрачных объектов.
Speaker A
Итак, давайте подведём некоторые итоги. В графическом конвейере есть множество различных этапов, и при этом половину из них мы можем сами запрограммировать. Их я выделил красным. И хоть это и кажется очень сложным, но в реальности в большинстве случаев нам нужно самим
Speaker A
программировать только вершинные и фрагментарные шейдеры. Но это всё была теория, так что давайте переходить к практике.
Speaker A
Теперь с этими знаниями давайте уже нарисуем наш первый треугольник. Сначала нужно задать информацию об этом треугольнике. Я это сделал в одном большом массиве. И для каждой вершины я задаю её координаты и цвет. В окне в OpenGL, да и во всей компьютерной
Speaker A
графике применяется такая система координат. В ней точка 0,0 находится в центре экрана, а координаты края окна являются либо единицей, либо минус единицей. Это называется нормализованные координаты устройства, что по-английски звучит normalized device coordinates или сокращённо NDC.
Speaker A
И также, хоть экран двумерный, нам всё равно нужна третья координата. Так что просто пока что оставим её нулевой.
Speaker A
Дальше нужно выделить место в памяти виде...
Speaker A
Сейчас не буду супер подробно рассказывать о каждой функции. Лучше почитайте о ней в документации. Ссылку оставлю на экране и в описании. Теперь нужно задать конфигурацию этого буфера, потому что видеокарта вот этих комментариев не видит. Для этого нужно вызвать вот эти две функции, которые
Speaker A
сначала создают конфигурацию атрибута или параметра буфера, а потом разрешают её к использованию. И поскольку у нас есть два атрибута: позиция и цвет, мы повторяем эти две строчки два раза.
Speaker A
Сейчас для нас самое важное понять, что означают первый, пятый и шестой аргументы этой длинной функции. Первый задаёт номер атрибута буфера, пятый - расстояние или количество байт между двумя последовательными параметрами.
Speaker A
Ашесто сдвиг этого параметра от начала буфера. Возможно, я это объяснил ужасно, но надеюсь, на этой картинке это изображено более наглядно. И также нам ещё нужно подключить так называемый объект массива вершин. Сейчас не буду рассказывать, что это такое. Мы могли бы
Speaker A
обойтись без него, но в таком случае OpenJL просто выдаст ошибку, так что нам придётся его создать. Отлично. Теперь в графической памяти видеокарты есть данные о нашем треугольнике. Причём они нами разделены на два атрибута. И настало время написать шейдер, чтобы
Speaker A
дать видеокарте понять, каким конкретно образом использовать наши данные. Как я уже сказал, мы будем писать вершинный и фрагментарный шейдеer. Давайте сначала посмотрим на вершинный. И вот так выглядит его код. JSL очень похож на C или C++, но в нём есть встроенные типы
Speaker A
для векторов и матриц и разные другие полезные функции для работы с 3D. Так что писать на нём вполне приятно. Первой строчкой мы задаём версию языка JLSL. В данный момент я использую OpenJL версии 33.0 и у JLSL такая же версия. Дальше мы
Speaker A
задаём входные атрибуты шейдера. Это как раз атрибуты того самого буфера, который мы задали ранее. Далее я задаю выходной атрибут шейдера, который потом интерполируется и передаётся во фрагментарный шейдер. Функции main с помощью ключевого слова position задаём расположение точки на экране. В будущем
Speaker A
мы будем применять к этим координатам какие-то преобразования, но пока что просто передадим их без изменений. И возможно, вы заметили, что по какой-то причине этот JL position является четырёхмерным вектором. Хотя мы вроде находимся в 3D, но почему так происходит, я расскажу чуть-чуть
Speaker A
попозже. И также давайте передадим данные из входного атрибута в выходной, то есть во фрагментарный шейдер. И теперь давайте посмотрим как раз на него. В нём мы также задаём версию JLSL, потом входной атрибут, который должен совпадать по имени и типу с выходным из
Speaker A
вершинного шейдера, иначе шейдеры просто не скомпонуются. И также задаём выходной параметр, который будет итоговым цветом пикселя. Функции main делаем этот выходной цвет равным входному цвету, который, как мы помним, был проинтерполирован. И, по идее, мы должны получить какой-то градиент по всему
Speaker A
треугольнику. Теперь надо подключить эти шейдеры к графическому конвейеру. Для этого сначала прочитаем их исходные коды из файла и запишем в две строки.
Speaker A
Дальше нужно создать шейдерный объект и загрузить в него исходный код. Ну и также на всякий случай проверим его на предмет ошибок компиляции. И то же самое делаем для фрагментарного шейдера.
Speaker A
Дальше нужно создать шейдерную программу, которая объединит эти шейдеры. Дальше привязать к ней шейдеры и проверить на ошибке. Теперь можно удалить отдельные шейдеры, так как они уже друг с другом скомпоновались и по отдельности больше не нужны. И теперь самый ответственный момент. Подключаем
Speaker A
объект массива вершин, подключаем шейдеры и рисуем первые три вершины в буфере как треугольник. И, наконец, получаем вот такой разноцветный треугольник. Сколько там прошло с начала видео?
Speaker A
Треугольник - это, конечно, хорошо, но вообще я хотел более-менее равносторонний треугольник, а не такой растянутый. А растянулся он из-за того, что наш экран также не квадратный, и поэтому единичный отрезок по оси X больше, чем единичный отрезок по оси Y.
Speaker A
Если сделать экран квадратным, то и треугольник станет тоже нормальным. Чтобы убрать это искажение, нужно найти соотношение сторон экрана и домножить все координаты по иксу на это соотношение. Это прямо стандартный приём, который применяется абсолютно в каждой графической программе. Однако
Speaker A
данные о координатах уже передались в буфер, и получить к ним доступ в следующий раз мы сможем только в шейдере. Но как передать в шейдер - это соотношение сторон. Не создавать же для одной переменной отдельный буфер. На самом деле этого делать не нужно. Ведь
Speaker A
для таких целей существуют специальные unниформ переменные. значения которых не меняются от вершины к вершине или от пикселя к пикселю, если это фрагментарный шейдер, но оно может меняться каждый раз при вызове шейдера, и это нас вполне устраивает. Поэтому вот
Speaker A
этими двумя строчками создаём типа flot с именем unilog и передаём ему значение соотношения сторон. В самом шейдере мы также должны его объявить и домножить координату X на этотниформ. И теперь треугольник стал уже равносторонним.
Speaker A
Вообще юниформы - это прямо очень крутая штука, которой мы будем постоянно пользоваться. Как ещё один прикольный пример, можно сделать, который будет передавать время исполнения программы и в самом шейдере сделать вот такие хитрые преобразования, которые будут поворачивать точки треугольника, и тогда треугольник будет
Speaker A
вращаться с периодом 2 пили или примерно 6,28 секунд. Но вообще прямо вот так делать не очень хорошо, потому что вычислять регонометрию на видеокарте - это очень долго, и лучше было бы заранее вычислить синус и косинус и передать в двух разных
Speaker A
уюниформах. Или можно сразу передать это преобразование в виде матрицы. Кстати, так мы будем делать, когда перейдём в 3D. Теперь давайте нарисуем квадрат.
Speaker A
Квадрат, как и любой четырёхугольник, можно разложить на два треугольника и потом по отдельности нарисовать каждый из них. Для этого нужно увеличить наш буфер, добавив в него ещё три точки, и также поменять функцию рисования, изменив там 3 наше. И тогда нарисуется
Speaker A
вот такой квадрат. Но возможно, вы знаете, что вообще-то в квадрате четыре точки, а не шесть.
Speaker A
И да, тут есть повторяющиеся данные, и, возможно, это не такая огромная проблема, но как-нибудь хотелось бы от этого избавиться, и это можно частично исправить. Теперь в основном буфере мы будем хранить только уникальные вершины, но создадим ещё один буфер, в котором
Speaker A
будем хранить номера уникальных вершин в том порядке, в котором мы хотим их нарисовать. Например, создаём четыре вершины и говорим: "Нарисуй сначала нулевую, потом первую, потом вторую, потом снова нулевую, потом снова вторую и потом третью вершину". Понятное дело, что надо загрузить этот буфер и потом
Speaker A
немного поменять функцию рисования, но я сейчас не буду это разёвывать. Почитайте лучше документацию. Результат никак не изменился, но давайте посмотрим, насколько мы выиграли в памяти. Раньше мы использовали шесть вершин, каждая по шесть флотов. В сумме это 144 байта. А
Speaker A
сейчас мы используем четыре вершины по шесть флотов и ещё шестьнтов, что равно в сумме 120 байт. То есть мы выиграли 24 байта. И возможно это не так много, но, во-первых, можно было бы использовать шорты вместов и выиграть ещё 12 байт. А
Speaker A
во-вторых, в огромных моделях такой приём позволяется сэкономить уже ощутимое количество видеопамяти. Теперь давайте поговорим о текстурах, потому что это прямо очень важный элемент любой графической игры или программы. Вообще текстуры представляют собой намного больше, чем просто картинку, которую мы
Speaker A
натягиваем на какую-то геометрию. Информацию из текстуры можно очень поразному интерпретировать, например, представить в виде карты отражений или карты нормалей. Но пока у нас нет никакого освещения, это не имеет вообще никакого смысла. Поэтому пока что мы будем использовать текстуры классическим
Speaker A
образом. Я нашёл вот такую текстуру кирпича. И давайте попробуем её натянуть на наш разноцветный квадрат. Сперва нужно изменить данные из буфера, добавив в них координату текстуры. У текстур в Open GL тоже есть своя система координат. В ней левый нижний угол имеет
Speaker A
координату 0, а верхний правый 1. Но в реальности при хранении и загрузке у изображений ось Y начинается сверху, а не снизу. Поэтому мы должны поменять координаты по игреку. Хоть в данной картинке это не совсем критично, но всё равно стоит поменять. Также нужно не
Speaker A
забыть поменять конфигурацию атрибутов буфера, так как теперь у нас каждая вершина хранится как восемь флотов, и добавить ещё один атрибут для текстуры.
Speaker A
Далее нужно прочитать файл с текстурой и загрузить во временный массив данные. Для этого я решил воспользоваться классической для этой цели библиотекой STB Image и вот этой её функцией.
Speaker A
Теперь можно создать объект текстуры, подключить его и загрузить туда данные. После всего этого можно освободить занятую память. В вершинном шейдере нужно принять новый атрибут и передать его во фрагментарный шейдер. А во фрагментарном шейдере уже немного интереснее. Мы, во-первых, должны
Speaker A
принять текстурные координаты, а во-вторых, саму текстуру. Делается это через типа sampler 2D. В этом юниформе будут данные, переданные в наш текстурный объект. И теперь в функции main задаём цвет пикселю через встроенную функцию текture, которая принимает смпlerлер и координаты, а
Speaker A
возвращает цвет этой текстуры как четырёхмерный вектор формата RGBA. Если запустить программу, то увидим, что текстура прекрасно наложилась. Чтобы результат получился ещё более интересным, можно скомбинировать цвет из текстуры с цветом из буфера. Это можно сделать по компонентным умножениям векторов, отвечающих за два цвета. И
Speaker A
получается вот такая разноцветная текстура. Но как бы это всё ни было круто, пока что 3D даже и не пахнет. Так что давайте переходить в 3D.
Speaker A
Но перед тем, как переходить в 3D, я вам всем советую подписаться на мой Telegram-канал. Там посты выходят почаще, чем раз в год. И там в целом классно, интересно. Так что обязательно подписывайтесь, ссылочка будет в описании.
Speaker A
Для реализации 3D нам потребуется очень много математики, а именно линейной алгебры. Сейчас чисто математику я рассказывать не буду, потому что это ещё разговор на час-полтора, но возможно в будущем я сниму видео про всю эту математику, так что если вам интересно,
Speaker A
то пишите в комментариях. А пока можете посмотреть первые несколько видео вот этого плейлиста. Он прямо хороший. Так вот, все функции для работы с векторами и матрицами можно было бы реализовать самому, как я сделал в прошлый раз. Но в
Speaker A
этот раз я воспользуюсь ещё одной довольно базовой библиотекой, которая называется GLM. Она была специально разработана для работы с OpenJL и имеет все нужные функции для работы с трёхмерной графикой. Они ещё и оптимизированы, так что вообще прекрасно. Давайте посмотрим, как
Speaker A
происходит процесс преобразования уже настоящих трёхмерных координат в нормализованные координаты устройства, о которых я говорил ранее, и которые должны получиться после вершинного шейдера и пойти дальше по графическому конвейеру. Обычно это проходит в три этапа. На первом этапе локальные координаты модели преобразуются в
Speaker A
глобальные мировые координаты, и таким образом задаётся расположение этой модели в пространстве. Дальше эти координаты преобразовываются в локальные координаты камеры. Таким образом учитывается расположение уже камеры. И на третьем этапе происходит проецирование, которое уже всё переводит в нормализованные координаты устройства
Speaker A
и определяет, что конкретно мы увидим. Давайте рассмотрим каждый этап поподробнее. Итак, как я уже сказал, изначально все координаты треугольников модели находятся в локальной системе отчёта, связанной с этой моделью.
Speaker A
Например, если мы экспортируем в Блендере какую-то модель, как Objectфайл, то в нём будут записаны координаты вершин относительно вот этой точки. Но обычно мы хотим нашу модель как-нибудь или передвинуть, или отмасштабировать, или повернуть. То есть хотим изменить эти координаты. И тогда к
Speaker A
этим координатам нужно применить так называемую матрицу модели, которая и перевела бы их в глобальные координаты.
Speaker A
Например, давайте как-то переместим наш разноцветный квадратик. Скажем, повернём его вокруг оси X на угол, зависящий от времени, а потом передвинем немного назад. Нужно обратить внимание, что из-за некоторых особенностей матриц мы задаём эти преобразования в обратном порядке. И ещё из-за одной особенности
Speaker A
матриц. Трёхмерный вектор можно передвинуть или более правильно применить к нему параллельный перенос, используя только четырёхмерную матрицу.
Speaker A
Поэтому тут мы используем матрицу 4х4. Но тогда нам придётся использовать и четырёхмерный вектор, и сделать его четвёртую координату W. Она ещё называется гомогенная координата, равна единице. И как раз именно это мы и видели в шейдере ранее. Но на самом деле
Speaker A
у этой четвёртой координаты есть ещё одно применение, о котором мы узнаем, когда будем говорить про проецирование.
Speaker A
Дальше нужно учесть положение камеры. Во всей трёхмерной графике камера на самом деле всегда находится в начале координат, а всё вокруг перемещается относительно камеры таким образом, что создаётся иллюзия движения камеры. И вот это перемещение производится матрицей взгляда. Но пока что давайте камеру
Speaker A
оставим в покое и вернёмся к ней чуть позже. И поэтому наша матрица будет пока что просто единичной матрицей, то есть всё оставлять на месте, и тогда камера будет находиться в начале координат и смотреть противоположно оси Z. Третий этап - это проецирование, которое все
Speaker A
координаты приводит к нормализованным координатам устройства, которые уже очень легко перевести к координатам пикселя на экране. Есть два вида проекции: ортографическая, которая не учитывает расстояние до объекта, и перспективная, которая расстояние уже учитывает. Ортографическая проекция используется там, где важно избежать
Speaker A
искажения размеров, например, в начертательной геометрии. Но обычно в играх и движках по типу нашего используют именно перспективную проекцию, потому что она создаёт реалистичную картинку. Ведь в реальном мире чем дальше объект, тем меньше он нам кажется. С точки зрения геометрии
Speaker A
преобразование перспективной проекции таково, что переводит поле зрения камеры, которая является усечённой пирамидой в прямоугольный параллелепипед. Это преобразование уже нелинейное и включает в себя деление на координату Z. И нам тут опять очень сильно помогают четырёхмерные матрицы.
Speaker A
Матрица перспективной проекции устроена так, что когда мы применяем её к четырёхмерному вектору с гомогенной координатой, эта координата становится равна изначальной координате Z. После чего нужно разделить первые три координаты на четвёртую, и тогда мы получим ту самую перспективу. Причём это
Speaker A
деление производится open автоматически. Тогда первые две координаты будут являться координатами пикселя на экране, а третья координата будет являться дальностью этого пикселя или вершины от камеры. Это нам тоже пригодится. Также матрица проецирования ещё учитывает угол поля зрения и ближнюю и дальнюю границу
Speaker A
отрисовки. Но сейчас я так подробно это не буду объяснять. Опять же пишите про отдельное видео про математику. Так вот, в GLM матрица проекции тоже задаётся одной вот такой строчкой. Тут мы задаём угол поля зрения, соотношение сторон экрана и ближнюю и дальнюю границу
Speaker A
отрисовки. В итоге теперь нам нужно передать три созданных матрицы в шейдер через юниформы, а в самом шейдере все эти матрицы умножить на входные координаты. Причём все эти матрицы записаны в обратном порядке, так как их принято умножать справа налево.
Speaker A
Получается, мы сначала применяем матрицу модели, потом матрицу взгляда, а потом матрицу проекции. Если составить из названий матриц аббревиатуру, то и на русском, и на английском получится MVP.
Speaker A
И поэтому иногда все эти перемноженные матрицы называют МВП матрицей. Ну, по крайней мере, в англоязычных ресурсах.
Speaker A
Ну ладно, давайте уже запустим нашу программу. И получается вот такой вращающийся квадрат. Тут уже видно, что это 3D как раз-таки из-за перспективы, когда более дальняя часть квадрата становится меньше. И, кстати, тут теперь нет этого бага с неправильной проекцией
Speaker A
текстуры. В прошлый раз проблема была в том, что текстура накладывалась линейная относительно экранных X и Y, а должна была откладываться линейно относительно XY в пространстве камеры. И из-за того, что проецирование нелинейное, XY в пространстве камеры отображаются нелинейно на экранные XY. А мы
Speaker A
накладываем текстуру так, как будто бы это отображение линейное, и поэтому и происходит этот баг. Исправить это можно, если интерлировать по треугольнику координаты не относительно экранных x и y, а относительно экранных x / z и y / z. И, к счастью, open делает
Speaker A
всё это автоматически. Поэтому никакого кода дополнительно писать для этого не надо. О'кей. Теперь давайте вернёмся к движению камерой, то есть сделаем уже настоящую матрицу взгляда. В GLM для этого есть встроенная функция look AD, то есть посмотри на эта функция
Speaker A
принимает позицию камеры, координаты цели камеры, то есть куда камера смотрит и локальное направление вверх для камеры. Дальше, используя ортогонализацию грамма шмита, достраиваются локальные направления вверх и вправо, а уже используя их и направление вверх, составляется матрица, которая передвинула бы систему координат
Speaker A
так, чтобы камера была в начале координат и смотрела вдоль оси Z. Теперь создадим переменные для этих параметров камеры, а именно позицию, направление взгляда, не путать с координатами цели, ведь это сумма позиции и направление взгляда, и направление вверх. Если
Speaker A
запустить программу, то получится то же самое. Но на самом деле это я просто выставил параметры так, чтобы матрица взгляда получилась единичной, то есть какой и была до этого. Но используя ввод с клавиатуры и мышки, эти параметры можно изменять. И таким образом мы
Speaker A
сделаем управление камерой. Начнём, пожалуй, с управления клавиатурой. Для этого в главном цикле каждый раз, когда будут нажаты кнопки WASD, пробелы, Shift, мы будем сдвигать камеру в соответствующую сторону. С движением вперёд и назад всё вроде понятно, вверх-вниз тоже всё вроде логично, а для
Speaker A
движения влево и вправо нужно найти векторное произведение от векторов вперёд и вверх. Опять же, если это непонятно, то пишите про видео про математику.
Speaker A
Теперь создадим матрицу взгляда и запустим программу. И действительно, теперь камеру можно двигать, а этот квадрат продолжает крутиться сам собой.
Speaker A
Но такая реализация движения обладает одним, на самом деле, важным недостатком. Мы сдвигаем камеру в каждый кадр на одно и то же расстояние, то есть скорость зависит от частоты кадров. А частота кадров, как вы знаете, это не очень стабильная вещь, поэтому умнее
Speaker A
будет домножать перемещение на время между кадрами, и тогда скорость будет независима от частоты кадров. Теперь давайте сделаем управление поворотами с помощью мышки. Для этого введём ещё две переменных. Горизонтальный поворот или йо и вертикальный поворот или пич.
Speaker A
По-русски это звучит, ну, слишком кринжово, поэтому лучше буду говорить по-английски. Ну, а кроме них ещё понадобится переменная типа бул для определения того, двинулась ли мышка в первый раз или нет. Это нужно, чтобы во время запуска программы не дёргал экран,
Speaker A
так как при запуске курсор резко перемещается в центр экрана и происходит примерно вот это. И также ещё понадобятся переменные для последней локации курсора. функции main где-то сверху надо привязать курсор к окну и скрыть его, а также выбрать функцию для
Speaker A
обработки движения мышки. И вот так выглядит эта функция. Сначала вычисляем сдвиг мышки с прошлого раза. Если мышка двигается первый раз, то этот сдвиг просто игнорируем. Дальше, используя коэффициент чувствительности, изменяем горизонтальный и вертикальный угол взгляда. Проверяем, что вертикальный угол не стал больше, чем 90°, иначе
Speaker A
экран просто перевернётся. И по вот такой формуле, которую я тоже сейчас не буду объяснять, вычисляем новое направление взгляда и всё готово. Теперь можно запустить программу и по-настоящему полетать вокруг этого квадратика.
Speaker A
[музыка] Теперь давайте нарисуем что-нибудь трёхмерное, например, куб. просто потому, что его легко захардкодить. С этого момента я уже не буду так подробно объяснять каждую строчку, потому что всё делается аналогично. Если запустить программу, то мы увидим, что куб, конечно, рисуется, но как-то очень
Speaker A
странно. Дело в том, что Open GL рисуют треугольники каждый раз в одном и том же порядке, невзирая на то, что более дальние треугольники могут находиться поверх более ближних, чего, как бы, быть не должно. Чтобы исправить это, используется так называемый Z-буфер или
Speaker A
буфер глубины. Размер этого буфера соответствует размеру окна Open JL, и в нём записана глубина или удалённость каждого пикселя от плоскости камеры. И перед тем, как поменять значение цвета пикселя в цветовом буфере, то есть в том, который мы видим, Open GL проверяет
Speaker A
значение из Zбуфера. Если глубина текущего пикселя меньше, чем значение Z-буфера, то пиксель перерисовывается и Zбуфер обновляется. А иначе пиксель не рисуется и Zбуфер не меняется. Глубина пикселя - это функция от его координаты Z в пространстве камеры, которая диапазон между ближней и дальней
Speaker A
границей отрисовки переводит к диапазону от нуля до единицы, но по обратной пропорциональности. Такая сложная формула используется, во-первых, потому что так удобнее из-за проецирования, а во-вторых, при такой формуле повышается точность вычисления глубины для близких объектов, потому что на довольно
Speaker A
маленький диапазон по Z попадает почти весь диапазон по глубине. Конечно, в таком случае для более дальних объектов точность будет наоборот меньше, но на то они и более дальние. Тестирование глубины включается вот этой строчкой. И также нужно не забыть каждый раз очищать
Speaker A
не только экран, правильно сказать цветовой буфер, а ещё и буфер глубины. И теперь куб выглядит уже как надо.
Speaker A
Кстати, так как значения Zбуфера меняются от нуля до единицы, можно представить их в качестве оттенка серого и сделать довольно прикольную его визуализацию. Но из-за нелинейности глубины от расстояния до объекта весь куб получается белым. И только если подлететь очень близко, будут видны
Speaker A
тёмные места. Поэтому для визуализации лучше искусственно привести буфер к линейному виду. Код, который вы видите, делает именно это.
Speaker A
Я специально сделал дальнюю границу от рисовки довольно маленькой для большей наглядности. Итак, сначала мы видим куб таким сереньким, но если мы будем отдаляться, то он будет становиться всё белее и белее, пока не выйдет за границу отрисовки. И наоборот, если к нему
Speaker A
приближаться, то он будет чернеть. Если использовать нормальную 3D-модельку, то получится, как будто мы летаем в каком-то тумане. И всё это выглядит довольно эпично.
Speaker A
Наконец можно сделать несколько вращающихся кубов с текстурированием. И, как мы видим, всё работает прекрасно.
Speaker A
Так что теперь пора переходить к тому, чего все эти полчаса ждали. К освещению. Итак, я думаю, всем известно, что мы видим объекты, потому что к нам в глаза или в камеру попадают лучи света, испускаемые какими-то источниками и отражённые от этих объектов. Понятное
Speaker A
дело, что при разных источниках света и разных свойствах поверхности объекта мы будем видеть эти объекты как-то по-разному. Иными словами, мы можем задать некоторое уравнение освещения, в котором переменными будут различные параметры источника света. объекта и даже наблюдателя.
Speaker A
В реальном мире такое гипотетическое уравнение выглядело бы очень сложно, поэтому одна из целей компьютерной графики- найти вычислительно не очень сложное уравнение освещения, которые при этом давали бы красивый и реалистичный результат. Одна из таких моделей названа в честь вьетнамского учёного Фонга Буи
Speaker A
Тонга, который опубликовал эту модель аж в 1973 году. Эта модель включает в себя три компонента освещения: фоновый, рассеянный и отражённый свет. И при сложении всех этих компонент получаются итоговое изображения.
Speaker A
Давайте поговорим подробно про каждый из них. Для демонстрации буду использовать вот такую сцену с кубом и пирамидой. На первых этапах я пока не буду использовать текстурирование, чтобы сконцентрироваться именно на освещении.
Speaker A
Для начала давайте немного отвлечёмся и поймём, как вообще формируются цвета объектов в реальности. Свет состоит из непрерывного спектра электромагнитных волн различной длины, и наш глаз воспринимает эти разные волны по-разному. Например, волну с длиной 760 нм наш глаз воспринимает как красный, а
Speaker A
волну 530 нм как зелёный. Все остальные цвета получаются как комбинация волн различной длины и интенсивности, ну, то есть яркости. В программировании мы не можем использовать непрерывные спектры, и поэтому обычно используется всем привычный формат RGB, который используют комбинацию красного, зелёного и синего
Speaker A
цвета. Благо, наш глаз устроен так, что комбинации этих трёх цветов хватает, чтобы покрыть все доступные нашему глазу цвета. Объект может отражать не весь падающий на него свет. И то, какой свет он может отражать, а какой не может, задаётся его собственным цветом. Тогда
Speaker A
цвет объекта при заданном освещении будет являться комбинацией цвета света и цвета объекта. Если представить цвет как трёхмерный вектор, то эту операцию можно представить как покомпонентное умножение векторов. Пример сейчас на экране.
Speaker A
Цвет объекта и света мы будем передавать в шейдер с помощью юнимов. Освещение будем вычислять во фрагментарном шейдере, то есть отдельно для каждого пикселя. Свет обычно у нас будет белый, то есть 11, но ничего не мешает нам сделать его равным любому другому. Ради
Speaker A
эксперимента в конце это тоже мы сделаем. Чтобы понять, где находится источник света, мы также его будем рисовать как маленький кубик, цвет которого будет совпадать с цветом испускаемого им света. И рисовать мы его будем вот таким простеньким шейдером.
Speaker A
Вроде бы всё, что нужно, я сказал, так что давайте поговорим про фоновый свет. Его суть заключается в том, что даже в довольно тёмном помещении не будет абсолютной темноты. Откуда-то небольшой свет всё-таки будет проникать, и мы будем, хоть и очень тускло, но всё-таки
Speaker A
видеть объекты. Например, если мы будем где-нибудь за городом ночью, то мы будем видеть небольшой фоновый свет от звёзд.
Speaker A
При этом этот свет будет светить примерно одинаково со всех сторон. Так что фоновый свет вычисляется очень просто. Для этого примем в шейдер цвет фонового света и умножим на цвет объекта. как я показывал в примере, при этом такой свет должен быть довольно
Speaker A
тусклым, иначе убивается весь смысл фонового света. Получается, по сути, такая же картинка, но только сильно тусклее.
Speaker A
Рассеянный и отражённый свет вычисляются уже посложнее, и для них нам нужно определить вектор нормали. Вектор нормали - это такой вектор, который перпендикулярен поверхности в данной точке. Мы будем задавать нормали в нашем буфере до каждой точки, точно так же,
Speaker A
как и другие параметры, например, координаты или цвет. В шейдер они передаются аналогично. В данном случае мы не сможем задать только восемь уникальных вершин для куба или пять для пирамиды, потому что для каждого треугольника нормаль должна быть своя.
Speaker A
Например, если рассмотреть угол куба, то нормаль синие грани обозначена синим вектором, красный красным, а зелёный зелёным.
Speaker A
Если попробовать создать общую нормаль, то непонятно, куда она должна быть направлена, чтобы она ещё и была правильной для всех трёх граней. Ну а чуть позже я покажу, где всё-таки это применяется и даже как из этого можно извлечь пользу. Вообще нормально чисто
Speaker A
теоретически можно найти через векторное произведение, но обычно в графике используются всё-таки заранее сохранённые нормали. Давайте нормали также визуализируем.
Speaker A
Так как нормаль - это трёхмерный вектор, а цвет - это тоже трёхмерный вектор, то можно представить нормаль как цвет очень легко. Надо вектор нормали перевести в глобальные координаты, умножив на матрицу модели, так как нормаль задаётся в локальных координатах модели. Тут мы
Speaker A
её приводим к размеру 3х3, потому что изначальная матрица была размером 4х4, но четвёртая координата нам тут не нужна. И также надо нормализовать результат, потому что после преобразования получившийся вектор может перестать быть единичным. А нам удобнее работать с единичными векторами. Во
Speaker A
фрагментарном шейдере выходной цвет делаем равным координатам нормалии. Только надо вот таким образом перейти от диапазона от минус единицы до единицы к диапазону от нуля до единицы. Так как отрицательный цвет - это довольно странно. И получается вот такая забавная
Speaker A
картинка. Чем краснее поверхность, тем в этом месте норма больше направлена по оси X. Чем зеленее, тем больше по оси Y.
Speaker A
И чем синее, тем больше по оси Z. Можно даже включить вращение и посмотреть, как меняются цвета наших фигур. Но есть один нюанс. При, так скажем, неравномерном масштабировании, то есть когда одна ось сжимается или растягивается больше, чем другие, преобразованная нормаль уже может
Speaker A
потерять перпендикулярность, и это будет неправильно. Исправить это можно, если использовать не обычную матрицу модели, а транспонированную обратную матрицу.
Speaker A
Также лучше вот такую матрицу вычислить на процессоре и передать через uniform. Но я этого вообще делать не буду, так как в рамках этого видео я не буду применять такое неравномерное масштабирование.
Speaker A
Но чтобы вы имели в виду, я про этот момент решил рассказать. Переходим к рассеянному свету. Теперь мы уже предполагаем, что свет исходит от довольно мощного источника, сильно мощнее, чем фоновый свет. При этом поверхность освещена тем больше, чем меньше угол между направлением к свету и
Speaker A
нормалью этой поверхности. В целом, в реальном мире, при отражении от шероховатых поверхностей свет ведёт себя очень схоже. Можно доказать, что зависимость освещённости от угла падения света будет пропорционально косинусу угла между вектором нормалии и вектором, который направлен в сторону источника
Speaker A
света. Угол между двумя векторами равен их скалярному произведению, делённому на произведение их длин. Но если оба вектора нормализованы, то есть их длина равна единице, то угол между такими векторами равен просто их скалярному произведению. Давайте посмотрим на код.
Speaker A
Начнём с вершинного шейдера. Тут мы ничего связанного с освещением не вычисляем, но передаём координаты нормалии и вершины в глобальных координатах во фрагментарный шейдер, чтобы получить параметры конкретной точки на треугольнике. Причём тут важно не ошибиться, как это я сначала ошибся,
Speaker A
и не написать вот так, потому что тогда мы потеряем важную информацию о сдвиге вершин от начала координат, потому что так уж устроена матрица модели, что эта информация хранится в четвёртом столбце.
Speaker A
И когда мы её приводим к размеру 3х3, мы этот четвёртый столбец просто отрезаем и информация теряется. Для нормально же мы наоборот должны специально эту информацию потерять, потому что мы не хотим её никуда двигать. Это тоже было бы неправильно.
Speaker A
Во фрагментарном шейдере вычисляем само освещение. Передадим координаты источника какниформ. Нормализуем нормаль и найдём вектор направления к свету как разность координат источника и точки на треугольнике. И тоже нормализуем. Чтобы найти рассеянный свет, как и в прошлый раз, найдём покомпонентное произведение
Speaker A
цвета света, уже не фонового, и цвета объекта, и домножим на скалярное произведение двух полученных векторов.
Speaker A
Только с помощью этого макса уберём отрицательные значения, потому что освещённость не может быть отрицательной.
Speaker A
И итоговый цвет пикселя будет суммой цвета фонового света и рассеянного света. В итоге получается такой результат, и это уже выглядит более-менее реалистично. И также видно, что сзади хоть объекты и не освещены непосредственно источником из-за фонового света, они не абсолютно чёрные.
Speaker A
И последний компонент модели фонга - это отражённый свет. В рассеяном свете мы предполагали, что поверхность шероховатая, и поэтому свет отражается почти одинаково во все стороны. Но всё-таки у большинства материалов значимая часть светы отражается преимущественно в одну сторону, почти по
Speaker A
закону отражения. В итоге, если мы встанем на пути у отражённых лучей, мы увидим блик. И тут возникает похожая логика с рассеянным светом. Чем больше направление взгляда совпадает с направлением отражённого света, тем блик будет ярче. Сразу посмотрим на код.
Speaker A
Вычисляем направление взгляда как нормализованную разность координат камеры и координат точки на треугольнике. Дальше с помощью устроенной функции reflect вычисляем направление отражённых лучей. Да, тут нужен минус, потому что подразумевается направление распространения света, а не направление к свету. И второй аргумент -
Speaker A
это нормаль. Дальше вычисляем отражённый цвет как цвет света, умноженный на уже знакомое выражение со скалярным произведением. Пока что давайте отобразим только отражённый свет, чтобы было понятнее, что он из себя вообще представляет.
Speaker A
Видно, что вроде отражения работают, но блик оказывается слишком большим, и это выглядит очень странно. Поэтому полученное выражение ещё возводят в некоторую степень. Обычно в диапазоне от 16 до 256.
Speaker A
Тогда значение ближе к единице изменятся не очень сильно, и блик там будет оставаться довольно ярким, а все остальные значения сильно уменьшатся, и получится, что блик станет сильно меньше по размерам и чуть-чуть тусклее, но это не страшно.
Speaker A
Вот так выглядит блик, возведённый в шестьдесят четвёртую степень. Думаю, что это выглядит приемлемо. В полноценном освещении я решил отражённый свет ещё умножить на 0,7, чтобы он не так сильно пересвечивал другой свет. В итоге со всеми тремя компонентами света сцена выглядит
Speaker A
примерно вот так. Можно добавить текстуры и оценить результат. Теперь, как я и обещал, быстренько расскажу, где полезно применять нормали вместе с пассивами индексов вершин.
Speaker A
Сейчас мы задавали нормали честно. Вот таким образом. Эти нормали всё равно интерлировались по треугольникам. Но поскольку в вершинах все нормали имели одинаковое значение, то интерполируемые значения получались тоже все одинаковые.
Speaker A
И у такого распределения нормали есть минусы. Во-первых, поверхность получается как бы слишком плоской. Во-вторых, тут виден явный стык поверхностей, и освещение тут будет только усиливать эффект, и модель будет казаться слишком треугольной. И с реальной геометрией мы ничего сделать не
Speaker A
можем, но можем сделать с нормами, если их вот так растопырим. Когда нормали проинтерполируются, их распределение будет соответствовать примерно вот такой поверхности, и освещение будет ложиться на эту поверхность уже более гладким. И такой лайфхак вовсю используется в графике для искусственного сглаживания
Speaker A
поверхностей. Но в нашей программе, как по мне, лучше оставить всё, как было прежде, потому что весь смысл куба в том, что он как раз-таки имеет шесть явно выделенных граней. Если бы он выглядел сглаженным, то это было бы наоборот более странно.
Speaker A
Но пока что тут участвует только один источник света. Давайте обсудим, как сделать их произвольное количество.
Speaker A
Итак, мы обсудили свет в модели фонга, но не особо обсудили, какими могут быть его источники.
Speaker A
Мы поговорим о трёх типах этих источников хотя наверное можно придумать их и больше. Первый из них - это точечный источник. У такого источника весь свет исходит из одной точки и распространяется во все стороны.
Speaker A
Причём интенсивность такого света уменьшается при удалении от источника. Это называется затуханием света. Такой свет хорошо моделирует лампочки, факелы и тому подобное. В целом, мы уже реализовали точечный свет. Надо только добавить его затухание. В физике существует закон обратных квадратов, по
Speaker A
которому интенсивность света уменьшается обратно пропорционально квадрату расстояния до источника света. Мы будем использовать даже чуть более сложную формулу, в которой в знаменателе есть не только квадратичное выражение, но ещё и линейное и константные. Дело в том, что если использовать формулу как в физике,
Speaker A
то при маленьких расстояниях этот коэффициент становится очень большим, и всё просто дико засвечивается. В связи с этим добавляется константный параметр, который не даёт освещённости улетать в бесконечность. Например, если он будет равен единице, то тогда коэффициент затухания не будет больше единицы. Так
Speaker A
что я ставлю у себя его равным единице. Линейные и квадратичные коэффициенты вот так изменяют форму графика. Тут можно поэкспериментировать, но я решил взять вот такие значения. В итоге мы можем вычислить коэффициент затухания и домножить на него рассеянный и
Speaker A
отражённый свет. Чтобы было лучше видно результат, источник мы отправим туда-сюда двигаться по синусоиде, а сами посмотрим на затухание.
Speaker A
[музыка] Второй тип источника - это удалённый источник. Допустим, у нас есть довольно мощный точечный источник, который располагается так далеко, что в масштабах нашей сцены он испускает почти параллельные лучи. В нашем мире аналогом такого света является, например, солнце.
Speaker A
Тогда такой источник описывается не координатами, а сразу вектором направления к нему. И также такой свет не подвержен затуханию, потому что затухание - это эффект чисто для расходящихся лучей, а не для параллельных. В коде меняется только то, что мы не вычисляем направление света, а
Speaker A
сразу принимаем его черезниформ. Ну и также мы не вычисляем затухание света. Получается что-то типа такого.
Speaker A
Не забываем, что в основной программе можно с вектором направления к свету сделать что-нибудь интересное. Например, заставить его крутиться.
Speaker A
И третий тип источника - это источник сфокусированного света. Он является частным случаем точечного источника, но испускает лучи не во все стороны, а только внутри некоторого угла. В реальности такими являются фонарики и всякие хай-тек-светильники.
Speaker A
Такой источник можно определить тремя параметрами: координатами источника, скажем так, направлением фокусировки и углом, в котором этот источник светит. Я его буду называть максимальным углом фокусировки, но в шейдер мы передадим сразу косинус этого угла. Сейчас объясню, зачем. Теперь посмотрим на
Speaker A
шейдеer. Функции main вычисляем направление к свету и косинус угла, который составляют вектор, противоположный направлению к свету, и вектор направления фокусировки света.
Speaker A
Если этот угол меньше, чем максимальный угол фокусировки, то спокойно вычисляем рассеянный и отражённый свет, а иначе оставляем только фоновый. Но скалярное произведение выдаёт косинус угла, и чтобы получить сам угол, нужно было бы взять аркосинус. Но вычислять аркосинус ещё и на видеокарте слишком
Speaker A
неэффективно. Поэтому нам удобнее вычислить косинус максимального угла, причём на процессоре, и сравнивать уже косинусы. Тут главное учесть тот факт, что на интервале от 0 до 180° косинус убывает. А это значит, что если текущий угол меньше максимального, то его
Speaker A
косинус будет больше. И внутри ифа мы уже весь код разбирали ранее. Получается вот такой интересный результат. Как будто мы светим фонариком.
Speaker A
Но вообще ифы в шейдерах не очень хорошо сказываются на их производительности, и от них желательно избавляться, где это возможно. Мы это можем спокойно сделать, если введём переменную интенсивности, которая будет равна сравнению в бывшем ифе, приведённому к флоту. То есть, если
Speaker A
текущий угол меньше максимального, то интенсивность равна единице, если больше, то нулю. И в последней строчке домножаем рассеянный и отражённый цвет, то есть который исходит из источника, на эту интенсивность. Получается абсолютно то же самое, но без ифа. Опять же, можно
Speaker A
каждый кадр поворачивать вектор фокусировки или менять угол свечения или вообще привязать источник света к камере.
Speaker A
Но, возможно, кому-то не понравится, что у этого фонарика слишком резкая граница. Поэтому введём ещё второй угол, скажем так, внешний угол фокусировки. Если текущий угол будет находиться между внешним и внутренним углом, то значение интенсивности будет находиться между нулём и единицей. Если больше внешнего,
Speaker A
то нулю, а если меньше внутреннего, то единица. Это можно реализовать с помощью вот такого выражения. Здесь с помощью функции М мы ограничиваем получившиеся значения нулём и единицей снизу и сверху, соответственно. Вот так выглядит такой фонарик с внутренней границей 20°
Speaker A
и внешней границей 25°. Теперь можно добавить и несколько источников одновременно. Но чтобы код не превратился не пойми во что, давайте для каждого источника сделаем отдельную структуру, в которой определим все нужные для него параметры. Определяются структуры точно так же, как и в C++. И
Speaker A
теперь принимать будем не кучу юнимов, как раньше, а только один типа новосозданной структуры. В основной программе нам нужно будет передавать каждое поле структурного объекта отдельно, используя вот такой синтаксис.
Speaker A
Опять же, он очень похож на C++, так что, надеюсь, вопросов не возникнет. При этом, если мы хотим определить несколько источников одного типа, то можно в качестве юниформа сделать целый массив.
Speaker A
А в основной программе нужно передавать значение ещё и для каждого элемента массива. Вот таким образом. Тут я сделал это без цикла, для понятности, но для удобства, конечно, лучше сделать это в цикле. В шейдере я также сделал отдельные функции для вычисления
Speaker A
рассеянного и отражённого света, так как мы ими будем очень часто пользоваться и удобнее выделить их в отдельную функцию.
Speaker A
В мейне мы сначала вычисляем фоновый свет. То есть подразумевается, что он живёт как бы отдельно от реальных источников и один для всей сцены. Дальше вычисляем свет от удалённого источника.
Speaker A
Также подразумевается, что он один, потому что это что-то типа солнца, а солнце у нас одно. И дальше в цикле вычисляем освещение от каждого точечного и сфокусированного источника и всё суммируем в результирующее освещение.
Speaker A
В итоговой сцене я задал один удалённый источник. три точечных источника и три источника сфокусированного света. Пока я тут буду летать и показывать итоговый результат, скажу пару финальных слов про модель фонга. Да, она не является самой физически точной или, может, красивой,
Speaker A
но она очень простая для понимания и программирования, и поэтому она стала бессмертной классикой при изучении компьютерной графики. Возможно, моя реализация может кому-то не очень понравиться, но благодаря шейдерам вы всё можете переписать и сделать так, как ближе вам.
Speaker A
Ну что ж, вот и содержательная часть видео подошла к концу. Мы почти с самого нуля разобрали принципы программирования на Open JL и дошли до уровня, на котором уже вполне можно создавать прикладные программы.
Speaker A
Понятное дело, что тут можно кучу всего ещё сделать, например, добавить загрузку 3D-моделей, о которой я всколь упомянул, или тени. Но, пожалуй, я это всё оставлю на следующий раз, если, конечно, эта тема вам интересна. Так что пишите в комментарии, если хотите вторую часть.
Speaker A
Теперь расскажу про ресурсы, которые я использовал. Самый основной - это, конечно же, сайт Learn Oppen. GL. Оттуда я, наверное, брал процентов 80 всей информации и почти весь код. Но также я могу посоветовать вот эти YouTube каналы. Конечно, это всё на английском,
Speaker A
но я думаю, что сайт-то точно не проблема перевести на русский в 2026 году. Также весь код из этого видео есть на моём гитхабе. Можете перейти в историю коммитов и посмотреть коды из различных частей видео. Ну а на этом
Speaker A
данное видео подошло к концу. Если вам было интересно, то обязательно влепите лайк и подписку. Это для меня очень важно. Ну и также не забывайте про Telegram-канал. С вами был Мимотортик. И до новых встреч.
Speaker A
[музыка]
Topics:OpenGLкомпьютерная графикаC++графический конвейершейдерывершинный шейдерфрагментарный шейдерGLFWGLEW3D-графика











