[Гайд] Сжатый, но длинный туториал по openGL

808
3
124
[h]Предисловие[/h]
Здесь я постарался как можно более емко и кратко рассказать о тех возможностях openGL, которые могут пригодиться при создании модов для Minecraft. Несмотря на это, объем текста получился весьма внушительным.
Для того, чтобы по-настоящему владеть openGL, этого туториала вам все равно не хватит. Нужно будет прочитать раз в 10 больше литературы. Кроме того, нужны базовые, но уверенные познания в математике и достаточное количество терпения, чтобы понять суть матриц трансформации.
Туториал носит по большей части теоретический характер, реальных примеров в нем очень мало. Он призван немного сбалансировать огромное количество туториалов, в которых используется openGL-код без толкового объяснения происходящего.
Описываются по большей части возможности openGL 1, на котором построен Minecraft. Да, я в курсе, что он давным-давно устарел, но в процессе модификации Minecraft'а избежать использования openGL 1 всё равно не выйдет. Про новые возможности будет сказано немного и в самом конце.


[h]Небольшое FAQ по определениям[/h]
q: Что такое openGL?
a: OpenGL - это API, предоставляющий программистам доступ к возможностям видеокарты. В случае c Minecraft'ом, этот API реализует библиотека LWJGL. 

q: Что делают функции openGL?
a: Функции openGL делятся на два типа: одни меняют его глобальное состояние (openGL state), в то время как другие передают в него данные (позиции вершин, UV-координаты и нормали) для отрисовки.

q: Что такое глобальное состояние openGL?
a: Состояние openGL - это набор глобальных параметров, в зависимости от которого один и тот же набор вершин отрисовывается по-разному. Описание отдельных элементов состояния openGL будет ниже.

q: В чем суть того, что делает openGL?
a: OpenGL строит изображение на экране (или в памяти) на основе поступающих в него вершин (координат) и своего состояния. В зависимости от состояния он может рисовать точки, линии и многоугольники.

q: Что такое UV-координаты?
a: UV-координаты - пары вещественных чисел от 0 до 1, соответствующие некоторым координатам на текстуре. U - горизонтальная координата, V - вертикальная. Если при рендеринге используется текстура, то, передавая вершину на отрисовку, необходимо указать еще и ее UV-координаты.

q: Что такое нормали?
a: Нормали - вектора, перпендикулярные поверхности. Они нужны для рассчета освещения. Нормалью к вершине может быть как перпендикуляр к полигону, к которому она принадлежит, так и средний вектор между перпендикулярами к нескольким полигонам, если вершина принадлежит к нескольким полигонам одновременно. Нормали должны быть нормализованы (то есть иметь единичную длину).

Подсказка: можно прописать в импортах import static org.lwjgl.opengl.GL11.* (и другие нужные версии) и не заморачиваться с постоянным повторением GL11.


[h]Немного практики[/h]
Наша пробная задача - отрендерить tile entity в виде квадрата. Будем считать, что вы уже умеете создавать блок с моделью :D. Код ниже должен быть в методе renderTileEntityAt(x, y, z, f, partialTickTime) нашего TileEntitySpecialRenderer.

Используя только openGL:
Код:
glPushMatrix(); //подробности ниже
glTranslatef(x, y, z); //подробности ниже
glBegin(GL_QUADS); //начинаем отрисовку квадратов.
//Вершины надо указывать против часовой стрелки.
glVertex3f(0.0F, 0.0F, 0.0F); //добавляем вершину на координатах (0, 0, 0)
glVertex3f(0.0F, 0.0F, 1.0F);
glVertex3f(1.0F, 0.0F, 1.0F);
glVertex3f(1.0F, 0.0F, 0.0F);
//не обязательно рисовать только один квадрат, здесь можно добавить больше вершин. Естественно, при отрисовке треугольников общее количество вершин должно быть кратно трём, а при отрисовке квадратов - четырём.
glEnd();
glPopMatrix(); //подробности ниже

Используя возможности Minecraft:
Код:
glPushMatrix();
glTranslatef(x, y, z);
Tessellator t = Tessellator.instance;
t.startDrawingQuads();
//тут можно было бы задать нормаль к этому квадрату, если было бы включено освещение
t.addVertex(0.0F, 0.0F, 0.0F); //добавляем вершину на координатах (0, 0, 0)
t.addVertex(0.0F, 0.0F, 1.0F);
t.addVertex(1.0F, 0.0F, 1.0F);
t.addVertex(1.0F, 0.0F, 0.0F);
//здесь тоже можно добавить еще вершин к отрисовке.
t.draw();
glPopMatrix();
Этой возможностью стоит пользоваться, потому что вызывать каждый раз функции openGL - очень и очень медленно, а здесь применены некоторые оптимизации. Если наложена текстура, то вместо addVertex() нужно использовать addVertexWithUV(), а если включено освещение, то перед добавлением вершин, принадлежащих новому полигону, нужно применять setNormal().

Если с добавлением вершин все должно быть более-менее понятно, то остальное - понятно не всем. В openGL есть такое понятие, как "матрицы трансформации". Я вряд ли опишу лучше, чем здесь, но всё же попробую вкратце пересказать, а также немного дополнить.


[h]Матрицы трансформации[/h]
Матрица трансформации в openGL - это двумерный массив чисел 4х4. По сути матрицы в комьютерной графике используется для того, чтобы перейти от одной системы координат (иногда используется слово "пространство" или "space") к другой. Я не буду здесь подробно описывать арифметику матриц, потому что это займет кучу места и не очень нужно в целом, но для глубокого понимания лучше все-таки прочитать про неё отдельно.
Чаще всего применяется две операции. Их суть вы поймете чуть позже.
1) Умножение вектора на матрицу. Как результат этой операции мы получаем вектор в другой системе координат.
2) Умножение матриц. Как результат мы получаем новую матрицу, которая трансформирует вектора так же, как если бы мы умножили сначала на одну матрицу, а потом на другую. 
При умножении матриц (как и при умножении вектора на две матрицы последовательно) важен порядок действий. Это легко понять, если представить две последовательности действий:
1) Пройти десять метров вперед, повернуть на 90 градусов вправо, пройти пять метров вперед.
2) Повернуть на 90 градусов вправо, пройти десять метров вперед, пройти пять метров вперед.
Очевидно, что в результате мы окажемся на разных координатах. Так же и с матрицами.

Основных систем координат (и, соответственно, матриц) три - MODEL (для перехода из пространства текущего объекта в мировое), VIEW или CAMERA (для перехода из мирового пространства в пространство камеры) и PROJECTION (для проекции сцены на 2d изображение).
По поводу MODEL и VIEW матриц стоит сделать два замечания. Во-первых, в openGL эти матрицы объединены в одну, которая называется MODELVIEW. Во-вторых, из-за огромных размеров мира в Minecraft'e пришлось отказаться от использования мирового пространства вообще.
При построении матрицы MODELVIEW для отрисовки блока с моделью выполняется последовательность действий (упрощённо):
1) Если включен вид от третьего лица, сместить на несколько метров вперед или назад
2) Если включен вид спереди, повернуть на 180 градусов вокруг оси Y
3) Повернуть камеру в соответствии со взглядом игрока
4) Сместиться на разность координат между позицией блока и камеры. Именно эта разность передается как x, y и z в метод renderTileEntityAt().
Первые три действия делает Minecraft, четвертое нужно делать самому. При умножении координат какой-либо вершины в пространстве модели на матрицу MODELVIEW эти операции будут применены в обратном порядке и мы перейдем в пространство камеры. Непосредственно перед отрисовкой модели можно применить и другие операции с матрицей MODELVIEW, такие как масштабирование или поворот.

Я почти уверен, что из моего посредственного объяснения вы вынесли очень немногое, и настоятельно рекомендую сейчас перейти по этой ссылке и прочитать ВСЮ статью. Три раза.


[h]Функции openGL, связанные с матрицами[/h]
Систему координат мы можем сдвигать, поворачивать и масштабировать. Для этого используются функции glTranslate*(x, y, z), glRotate*(angle, x, y, z) и glScale*(x, y, z). Вместо * в названии функции используется "f" или "d" в зависимости от того, какой тип данных она принимает в качестве аргументов - float или double. В glRotate аргумент angle - это угол в градусах (положительный угол = поворот против часовой стрелки), а x, y и z - вектор, вокруг которого поворачивать. Например, glRotatef(-90, 0, 1, 0) - повернуть на 90 градусов по часовой стрелке вокруг вертикальной оси.

Часто необходимо сохранить текущую матрицу с возможностью последующего восстановления. Это возможно при помощи функций glPushMatrix() и glPopMatrix(). glPushMatrix() добавляет матрицу в стек (список) сохраненных матриц. glPopMatrix() восстанавливает последнюю матрицу из стека и удаляет ее оттуда. Например:
Код:
glPushMatrix();
glTranslatef(10, 0, 0); //сместили на 10 метров
glPushMatrix();
glTranslatef(5, 0, 0); //сместили еще на 5 метров (то есть на 15 всего)
glPopMatrix(); //вернулись к смещению на 10 метров
glPopMatrix(); //вернулись к матрице, которая была до начала наших трансформаций

Функция glLoadIdentity() задает единичную матрицу (которая не делает никаких трансформаций). На практике при создании модов она вряд ли понадобится.


[h]Другие параметры openGL[/h]
Кроме матриц трансформации, есть огромное количество других составляющих состояния openGL. Я попробую рассказать о тех, что наиболее важны при создании модов.
В openGL есть несколько boolean-параметров, которые включаются и отключаются при помощи функций glEnable() и glDisable(), куда в качестве аргумента передается опкод этого параметра. С некоторыми из этих параметров связаны другие функции.

GL_DEPTH_TEST включает тест глубины. В большинстве случаев он включен и нужен. Его задача - отсекать те полигоны, которые заслонены другими, даже если "дальний" полигон рисуется после "ближнего". При отрисовке кадра кроме того изображения, что позже будет выведено на экран, есть еще один буфер, который называется буфер глубины, depth buffer или z-буфер. При отрисовке пикселя в этот буфер добавляется информация о том, как далеко он расположен от камеры. Подробнее можно прочитать здесь, и я очень советую это сделать.
Сопутствующие функции: 
glDepthMask(true/false) - включает/выключает запись в этот буфер. Если тест глубины выключен, то никакого влияния этот параметр не оказывает, в буфер глубины все равно ничего не будет записываться. Единственная функция в этом списке, которая часто нужна в моддинге.
glClearDepth(value) - очищает буфер глубины и заполняет его указанным числом. 1.0 - "пустой" буфер.
glDepthFunc(func) - устанавливает функцию, по которой проверяется, нужно ли отрисовывать пиксель или он чем-то заслонен. GL_LEQUAL используется в майне по умолчанию, это "меньше или равно" (less or equal). Насколько оправдано использовать другие функции - затрудняюсь ответить.
glDepthRange(zNear, zFar) - устанавливает "ближнюю" и "дальнюю" глубину в буфере. 0.0 соответствует ближней глубине, 1.0 - дальней.

GL_BLEND включает полупрозрачность (смешивание цветов). С ней есть две проблемы. Во-первых, эта опция серьезно снижает производительность, и включать ее для всего не стоит. Во-вторых, она плохо сочетается с буфером глубины. При использовании буфера глубины абсолютно безразлично, в каком порядке отрисовывать полигоны, но для корректного смешивания их нужно сортировать от дальнего к ближнему. Обычный путь - это отрисовать сначала всю непрозрачную геометрию со включенными GL_DEPTH_TEST и glDepthMask, потом выключить glDepthMask и отрисовать полупрозрачную геометрию, немножко сортируя ее по ходу дела :D. В Minecraft'e используется этот подход. Реализуется это через фичу, которая называется render pass. Нулевой pass - это непрозрачная геометрия, первый - полупрозрачная. Например, у сущностей есть метод shouldRenderInPass(). Получить текущий render pass можно через MinecraftForgeClient.getRenderPass().
Сопутствующая функция:
glBlendFunc(sfactor, dfactor) - параметры, по которым считается итоговый цвет пикселя.
finalColor = sfactor * srcColor + dfactor * destColor, где srcColor - цвет рисуемого полигона (например, с текстуры), а destColor - цвет того, что уже нарисовано на месте этого пикселя. Обычные применения:
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) - "нормальное" смешивание.
glBlendFunc(GL_ONE, GL_ONE) - аддиктивное смешивание, то есть сложение нового и старого цвета. Полезно для "энергетических" эффектов вроде огня и электричества.
glBlendFunc(GL_SRC_ALPHA, GL_ONE) - то же самое, но с учетом прозрачности с текстуры.

GL_TEXTURE_2D включает использование текстуры. В качестве "сопутствующей" функции можно было бы указать Minecraft.getMinecraft().renderEngine.bindTexture().

GL_LIGHTING включает простое освещение (с двумя источниками света на постоянных позициях). Оно НИКАК не связано с тем, как затеняются блоки ночью и в тени. Про освещение в Minecraft расскажу внизу.
Когда GL_LIGHTING нужно:
1) При рендеринге сущностей
2) При рендеринге tile entity
3) При рендеринге блоков в GUI
4) При рендеринге предмета в руках
Когда оно не нужно:
1) При рендеринге обычных блоков в мире. Там всё работает на костылях.
2) При рендеринге частиц
3) При рендеринге GUI
Сопутствующих фунций и параметров много, а рассказывать о них в контексте Minecraft'a толку мало. Включается дефолтное освещение через RenderHelper.enableStandardItemLighting(), выключается через RenderHelper.disableStandardItemLighting(), вспомогательные функции там и смотрите.

GL_ALPHA_TEST включает запрет на отрисовку пикселей в зависимости от значения alpha (непрозрачности). Обычное использование - запрет на отрисовку полностью прозрачных пикселей, потому что иначе при выключенном смешивании они будут рисоваться. Например, нарисовав квадрат с текстурой круга (на текстуре всё за пределами круга полностью прозрачно), вы всё равно получите квадрат. С этим же параметром связана ситуация, когда при включенном смешивании почти прозрачные пиксели вообще не видны. Тогда нужно отключить альфа-тест.
Сопутствующая функция:
glAlphaFunc(func, ref) - задает параметры альфа-теста. Первое значение - функция сравнения, второе - число, с которым сравнивать. Обычное использование - что-нибудь вроде glAlphaFunc(GL_GREATER, 0.1F).

GL_CULL_FACE включает запрет на отрисовку полигонов с обратной стороны. По умолчанию включен, имеет смысл выключать в случаях, когда иначе придется дублировать полигоны с обеих сторон. Сопутствующие функции указывают, что считать передней и обратной сторонами полигона и какие стороны обрезать при включенном GL_CULL_FACE. Никакого смысла трогать эти функции нет, по умолчанию передней стороной считается та, которая рисуется против часовой стрелки.

GL_NORMALIZE включает автоматическую нормализацию нормалей. Довольно затратно, на практике использовать не стоит.

GL_RESCALE_NORMAL - упрощенный и ускоренный вариант GL_NORMALIZE. Он подразумевает, что переданные в openGL нормали уже были нормализованы, но вы масшабировали матрицу трансформации (использовали glScale()). Работает верно только в тех случаях, когда матрица была масштабирована без искажений, то есть x, y и z, которые вы передали в glScale(), были равны.

GL_POLYGON_OFFSET_FILL включает смещение данных из буфера глубины при отрисовке. Звучит немного непонятно, зато решает гораздо более понятную проблему. Если попробовать отрендерить что-то поверх уже отрисованной поверхности (пример из ванильного майна - текстура разрушения блока поверх самого блока), то начнутся проблемы, связанные с точностью буфера глубины. Подробнее про них можно почитать по ссылке про этот буфер, которую я уже давал.
Сопутствующая функция:
glPolygonOffset(factor, units) - задает смещение. Обычное использование в майне - glPolygonOffset(-3.0F, -3.0F). Кроме того, перед рендерингом с использованием этой возможности обычно отключают glDepthMask().

GL_SCISSOR_TEST запрещает отрисовку за пределами указанной квадратной зоны на экране. Естественно, основное применение этой фичи - GUI (например, довольно сложно реализовать скроллящуюся панель без этой возможности).
Сопутствующая функция:
glScissor(x, y, width, height). Координаты и размеры указываются в пикселях в окне, а не в том, что называется "пикселями" в ГУИ и на практике обычно оказывается больше реальных пикселей. Кроме того, ось Y идет снизу, а не сверху. Пример использования (запретить отрисовку за пределами квадрата 100х100 в верхнем левом углу экрана):
glScissor(0, mc.diplayHeight - 100, 100, 100);

glColor4f() и tessellator.setColorRGBA_F() - задает RGBA цвет. Аргументы должны быть от 0 до 1. Со включенной текстурой тоже работает, в таком случае цвет текстуры домножается на указанный здесь цвет.

glShadeModel(GL_FLAT/GL_SMOOTH) задает простое или сглаженное освещение. GL_FLAT стоит использовать, если в качестве нормалей вы используете перпендикуляр к полигону, GL_SMOOTH - если средний вектор между перпенндикулярами к нескольким полигонам.

Я рассказал только о тех функциях и параметрах, которые кажутся мне наиболее важными для моддинга. Это где-то треть от всех возможностей даже openGL 1, не говоря о современных версиях. Если этого вам недостаточно, то ваш лучший друг - это официальная документация по openGL.

Далее о некоторых вещах, которым не место в гайде по openGL 1, но о которых стоило бы рассказать.


[h]Освещение в Minecraft[/h]
В Minecraft есть два типа освещения: от блоков и от неба. У каждого блока в игре есть два параметра от 0 до 15: освещенность от неба и от блоков-источников света. С каждым пройденным от источника света блоком освещенность падает на 1. Освещенность от неба под открытым небом равна 15 даже глубокой ночью.
Теперь о том, как же всё это отрисовывается.
Есть динамическая текстура 16х16 (mc.renderEngine.lightmapTexture) и соответствующий ей массив размером 256 int'ов (mc.renderEngine.lightmapColors). В ней хранится информация о том, какой цвет соответствует каждой комбинации освещения от неба и освещения от блоков. Каждый кадр эта динамическая текстура обновляется исходя из текущего времени суток, рандомной "мигалки" (освещение от блоков немного колеблется), настроек игры (гаммы) и наложенного ночного зрения. Некоторые объекты (например, сущности и tile entity) рендерятся в режиме мультитекстурирования: в первой текстуре забиндена обычная текстура этого объекта, а во второй - лайтмапа. UV-координаты на лайтмапе задаются через OpenGLHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, u, v). Для сущностей и тайлов эта операция производится перед рендерингом.
Отключить использование лайтмапы можно через mc.renderEngine.disableLightmap(), включить - через mc.renderEngine.enableLightmap().


[h]Введение в шейдеры[/h] (а не рассказ о том, как их использовать)
В openGL 1 вы не можете написать ни строчки кода, которая исполнялась бы на видеокарте. Всё, что вам доступно - это стандартные возможности openGL. В openGL 2.0 такая возможность появилась. Хотя официально Minecraft поддерживает все версии openGL, начиная с 1.2, использовать возможности openGL 2.0 можно относительно безбоязненно, потому что видеокарты без его поддержки уже настолько устарели, что Minecraft на них вряд ли запустится.
Шейдеры - это короткие программы на простом си-подобном языке, позво В openGL 2.0 доступно два типа шейдеров: вершинные и фрагментные (пиксельные). Вершинные шейдеры позволяют модифицировать координаты вершин и передавать данные во фрагментные шейдеры. Фрагментные шейдеры позволяют модифицировать цвет вершины на основе данных, полученных из вершинного шейдера и текстур.
С помощью шейдеров можно эффективнее использовать видеокарту для различных вычислений. Примеры использования шейдеров: симуляция воды, реалистичное освещение (с тенями), использование различных карт (normal map для рельефа, specular map для бликов, gloss map для "шероховатости" и другое), создание различных графических эффектов, пост-обработка кадра и другое. Про использование шейдеров можно написать намного больше, чем всё то, что я написал в этом гайде.
 
435
1
Отлично! Теперь ждем учебник по шейдерам :D
 
1,683
1
Они написаны на Ansi C. Найти в гугле не так трудно. И я так думаю шейдеры от других проектов в майнкрафт пойдут
 

necauqua

когда-то был anti344
Администратор
1,216
27
172
Красавчик вообще. Описано примерно всё-то, что я знаю, кроме шейдеров разве-что.
 
271
2
0
[font=Verdana, Helvetica, Arial, sans-serif]GloomyFolken[/font][font=Verdana, Helvetica, Arial, sans-serif]Я почти уверен, что из моего посредственного объяснения вы вынесли очень немногое, и настоятельно рекомендую сейчас перейти по[/font][font=Verdana, Helvetica, Arial, sans-serif]этой ссылке[/font][font=Verdana, Helvetica, Arial, sans-serif] и прочитать ВСЮ статью. Три раза.[/font]
Ссылка не работает. Дай пожалуйста другую.
 
699
9
53
20/10, нет, ∞/10! Наконец таки я чтото понял про OpenGL, только вот примеров не хватает.


И ещё я не понял что значит сжатый, но длинный.
 
10
0
[font=Monaco, Consolas, Courier, monospace]glStart(); не находит в либах, есть glBegin()[/font]
 

timaxa007

Модератор
5,831
409
672
npo6ka, да glBegin нужно использовать.
 
476
9
39
Ещё бы что-нибудь по OpenGL почитать. Может есть какие-нибудь книжки на русском?
Я гуглил и что-то пусто(на самом деле я просто хочу получить совет человека знающего).
Может кто знает что-то полезное или хорошее?
 

Icosider

Kotliner
Администратор
3,600
99
663
talosdx написал(а):
Ещё бы что-нибудь по OpenGL почитать. Может есть какие-нибудь книжки на русском?
Я гуглил и что-то пусто(на самом деле я просто хочу получить совет человека знающего).
Может кто знает что-то полезное или хорошее?

Книг по OpenGL много, смотря что тебе нужно. Хочешь писать шейдеры? Можешь прочитать OpenGL Шейдерный язык. Книга рецептов. Хочешь узнать по больше основ? Тык, Тык. Ну и есть красная книга, которая сочетает вторую книгу и чуть более. Тык
 
Сверху