Иконка ресурса

Рендеринг через VAO/VBO

Версия(и) Minecraft
1.7+
Доброго времени суток, господа программисты. Я хочу рассказать о том, как можно рисовать треугольники в кубаче, не прибегая к устаревшим технологиям.

Вообще, стоит начать с краткого ликбеза, и первое, сама причина написания этой статьи - Immediate Rendering в кубаче.

В майне используется обертка над возможностями OpenGL для рисования каких-либо данных, называемая Tessellator (статья писалась с опорой на 1.7.10, на версиях выше некоторые названия могут отличаться, поэтому сорре).
Уважаемый GloomyFolken уже писал в своей статье Сжатый, но длинный туториал по OpenGL, почему стоит использовать именно тесселятор, а не glBegin/End.

Я немного дополню, конкретно почему стоит использовать именно его: когда вы рисуете через glBegin/End, а потом указываете вершину через glVertex3f, то каждый кадр вы отправляете через нативный метод какие то данные, а еще вы их отправляете еще через медленную шину CPU -> GPU, то при отрисовке кучи таких данных, вы очень сильно теряете в производительности. Затраты на проход в натив, затраты на передачу данных видеокарте по медленной шине - убивает всю производительность при отрисовке кучи моделей. Тесселятор же, сначала формирует буффер данных, которые вы хотите отрисовать, и за один нативный вызов отправляет их на видеокарту, что быстрее. Однако, такая оптимизация все еще недостаточна, если в вашем кадре повторяется отрисовка этих данных раз так 50 (например, вы рисуете зеленый кубик вокруг вашего блока, и таких блоков в текущем кадре находится 50, то вы поймете, что ваш фпс чувствует себя немного хуже).

- Хорошо, я просто хочу рисовать какие то данные быстро! Как это сделать?
На форуме уже давно висит статья о том, как можно ускорить рендер, через, конечно, устаревшую технологию, однако она прекрасно справляется с нужной задачей - Ускорение рендера моделей. Ее очень просто интегрировать себе в проект, и по сути, это мастхэв для тех, кто не хочет или не может понять мир шейдеров, и мир хранения данных на GPU. При данном способе отрисовки данные уже хранятся в видеокарте, из-за чего у вас отпадает нужна гонять буффер с данными между CPU и GPU, вам нужно просто вызывать ИД нужных данных, и они отрисуются.

Однако, существует еще способ рисовать наши треугольники - индексированные VBO + Шейдеры.
Это действительно современный подход к тому, как нужно рисовать данные, как их можно обработать и получить на выходе приятную картинку. Пример такого подхода:
1713161495923.png
Здесь используется рендеринг через VAO + Шейдеры. Вся магия освещения происходит именно в шейдерах. Однако, это не самый идеальный выхлоп, и можно сделать еще лучше.

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

Допустим, у вас уже есть какая то модель, которую вы хотите отрисовать. Можно, конечно, запариться, и написать свой загрузчик для какого-то формата, но зачем городить велосипеды? На форуме уже есть готовая библиотека, которая позволит загрузить кучу кастомных форматов, да еще и некоторые доп. данные при импорте рассчитает - Jassimp. Спасибо Undead на этом форуме, что вообще поделился с работягами ссылкой.

Там же лежит джарник sources.jar, который вы можете скачать и положить себе в проект.
Все нативные библиотеки положите по пути src/main/resources/jassimp

На гитхабе уже описано кодом, как базово загрузить модель из файла и получить из этого готовые для отправки на видеокарту данные. Имхо, это описано там немного неудобно, я пилил для себя свой формат, но будем использовать то, что есть.

1) Обработка данных загружаемой модели
Оперируем кодом, данным нам на гитхабе:
Java:
public static final Set<AiPostProcessSteps> ASSIMP_POST = new HashSet<AiPostProcessSteps>() {
      private static final long serialVersionUID = 1L;

      {
          add(AiPostProcessSteps.TRIANGULATE);
          add(AiPostProcessSteps.GEN_SMOOTH_NORMALS);
          add(AiPostProcessSteps.FLIP_UVS);
          add(AiPostProcessSteps.CALC_TANGENT_SPACE);
          add(AiPostProcessSteps.JOIN_IDENTICAL_VERTICES);
          add(AiPostProcessSteps.OPTIMIZE_MESHES);
          add(AiPostProcessSteps.FIX_INFACING_NORMALS);
      }
  };
Здесь формируется список команд, которые будут применены к загружаемой модели. Лично у себя я убрал строчку с JOIN_IDENTICAL_VERTICES, уже не помню, почему, вроде как у меня были сбитыми позиции вершин, вы можете поиграться с ней и посмотреть, что получится.

CALC_TANGENT_SPACE нужно, если вы будете писать свое освещение.
FLIP_UVS - обязательно.
TRIANGULATE - опционально, но удобно, если вы вдруг забыли триангулировать свою модель.
GEN_SMOOTH_NORMALS - нормали генерируются как интерполированные между вершинами.
OPTIMIZE_MESHES - на самом деле не понял, за что отвечает данная команда.
FIX_INFACING_NORMALS - если нормали на модели направлены вовнутрь, а не наружу, то данная команда это исправит.

Все, что идет после метода load, является простым заполнением массивов вершин, нормалей, текстурных координат из сырых данных.

Стоит только коснуться статического класса Mesh, я его вынес в отдельный класс, который хранит данные для каждого меша из модели. И у меня каждый Mesh - отдельный ВАО.

2) Отправка данных в память ГПУ
Вы, поглядев на код на гитхабе, поняли (надеюсь), как нужно загрузить данные и запихнуть эти данные в свой класс Mesh, чтобы потом отправить их на гпу. Но как их отправить? Легко.
Я использую такие небольшие методы для удобного запихивания данных:
Java:
    public int createVAO() {
        //Создаем ВАО, биндим его и возвращаем сгенерированный ИД для нашего вао
        int vaoID = GL30.glGenVertexArrays();
        GL30.glBindVertexArray(vaoID);
        return vaoID;
    }

    public void unbindVAO() {
        //Анбиндим ВАО
        GL30.glBindVertexArray(0);
    }

    public void bindIndicesBuffer(int[] indices) {
        //Utility метод для отправки буффера с индексами на ГПУ
        int vboID = GL15.glGenBuffers();
        GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, vboID);
        IntBuffer buffer = storeDataInIntBuffer(indices);
        GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, buffer, GL15.GL_STATIC_DRAW);
    }

    public IntBuffer storeDataInIntBuffer(int[] data) {
        //Конвертируем массив индексов в буффер Integer
        IntBuffer buffer = BufferUtils.createIntBuffer(data.length);
        buffer.put(data);
        buffer.flip(); //Не забывайте флипать буффер, перед тем как отправлять данные в память
        return buffer;
    }

    public int storeDataInAttributeList(int attributeNumber, int coordinateSize, float[] data) {
        //Utility метод для отправки данных на ГПУ
        int vboID = GL15.glGenBuffers();
        GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboID);
        FloatBuffer buffer = storeDataInFloatBuffer(data);
        GL15.glBufferData(GL15.GL_ARRAY_BUFFER, buffer, GL15.GL_STATIC_DRAW);
        GL20.glVertexAttribPointer(attributeNumber, coordinateSize, GL11.GL_FLOAT, false, 0, 0);
        GL20.glEnableVertexAttribArray(attributeNumber);
        GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
        return vboID;
    }

    public FloatBuffer storeDataInFloatBuffer(float[] data) {
        //Utility метод для конверта кастомных данных в виде float массива в Buffer Float
        FloatBuffer buffer = BufferUtils.createFloatBuffer(data.length);
        buffer.put(data);
        buffer.flip(); //Обязательно флип
        return buffer;
    }

Теперь данные нужно отправить данные в память ГПУ, вызвав эти процедуры
Перед тем как это сделать, желательно добавить в класс Mesh поле vaoID (ранее я упоминал, что у меня для каждого Mesh - отдельный ид VAO), и поле verticesCount - для того, чтобы далее передать количество вертексов в метод отрисовки.

Бтв, еще не все могут понять, зачем мы биндим еще и какой-то индексный буффер, ведь у нас в модели хранятся только вершины, нормали, uv, опционально цвет, и все!
Вся суть в том, чтобы не хранить все три вершины для каждого треугольника, а записывать для треугольника индекс нужной вершины. Представим, у нас есть квад:
1713239488110.png

Его описывают 4 вершины, однако, так как есть стандарт рисовать все треугольниками, данный квад разбивается на два треугольника при триангуляции, на каждый уходит уже по 3 вершины. А так как их получилось 2, то умножаем 3 на 2, получаем уже 6 вершин! Причем 2 из них дублируют друг друга. Это не очень хорошо с точки зрения использования памяти, поэтому, можно сохранить лишь индекс нужной нам вершины при отрисовке треугольника, а сами данные в массиве вершин не дублировать. Наглядная экономия памяти в еще одном примере: Небольшой гайд от Равена.

Итак, продолжим. При создании объекта Mesh в конструкторе вызовем все наши вышеупомянутые методы:
Java:
vaoid = createVAO();
verticesCount = vertices.length;
bindIndicesBuffer(indices);
storeDataInAttributeList(0, 3, vertices);
storeDataInAttributeList(1, 2, textureCoords);
storeDataInAttributeList(2, 3, normals);
unbindVAO();
И далее, желательно очистить все массивы с данными, чтобы не хранить их в оперативной памяти. Они нам больше не понадобятся.

3) Отрисовка наших данных

Отлично, данные отправлены в память, пора их отрисовать!
Но для отрисовки нужно хранить ссылку на наш Mesh с нашими VaoID и verticesCount. Я это храню как привычный многим IModelCustom model поле.

Далее, рисуется все очень просто:
Java:
GL30.glBindVertexArray(mesh.vaoid);  
GL11.glDrawElements(GL11.GL_TRIANGLES, mesh.verticesCount,GL11.GL_UNSIGNED_INT, 0);
GL30.glBindVertexArray(0);
mesh - это ссылка на ранее созданный вами объект Mesh.

Разберем код:
Сначала мы биндим ВАО нашего меша, который сгенерировали ранее, чтобы видеокарта поняла, какие именно данные нужно отрисовать. Затем вызываем сам метод отрисовки glDrawElements. Первый параметр указывает, что рисовать будем треугольники, вторым указываем количество вертексов из нашего меша. Это единственные параметры, которые нам сейчас нужны
Ну и собственно, потом анбиндим ВАО через бинд нулевого значения.

4) Применение шейдера

Я очень надеюсь, что вы знаете, что такое шейдеры, и как их применять. На форуме лежит довольно много примеров кода по загрузке шейдера в игру, поэтому этот момент я пропущу.
Уточню только пару моментов.
Да поправят меня люди знающие более меня, но если я не ошибаюсь, то в версиях шейдера с 120 по 130 для получения данных, которые мы указывали при выгрузке данных на ГПУ, нужно использовать ключевое слово attribute, причем в том порядке, в котором мы биндили наши данные. В версиях старше стали использовать ключевое слово layout(location=0,1,2...etc), где связываются наши буфферы с данными в шейдере, например:

Мы биндим буффер вершин вот так storeDataInAttributeList(0, 3, vertices);
Первым параметром мы указали 0, а, например, для буффера текстурных координат мы указали цифру 1. Это и будет нашим указателем в шейдере, какие данные будут привязываться к конкретному layout.
А в шейдере уже потом пишем layout(location=0) in vec3 pos //аттрибут вершин
Повторюсь, это для версий >130, на версиях старше нужно соблюдать порядок в шейдере так, как вы биндили данные при выгрузке в память ГПУ.

Небольшая помарочка. В Jassimp в классе AiMesh есть геттер getTangentBuffer(). Если вы указали при загрузке модели AiPostProcessSteps.CALC_TANGENT_SPACE, то вы сможете получить рассчитанные тангенты через данный геттер. Не помню, но там так же можно рассчитать и битангенты.

Вторая небольшая помарочка. В ActiveRenderInfo классе майна есть вся информация о матрицах ModelView и Projection.
Юзкейс для перекидывания в шейдер такой:
Java:
FloatBuffer projBuffer = ActiveRenderInfo.projection;
Matrix4f projMatrix = new Matrix4f();
projMatrix.load(projBuffer);
//Далее просто передаем projMatrix через юниформу. С матрицей modelview все абсолютно так же

Не знаю, чем еще дополнить еще гайд, возможно, я где-то что-то не дописал, расписал не так, или правильно понимаю идею технологии, но неверно передал ее суть. Я открыт к адекватной критике, дополнениям, помощи, друзья, особенно со стороны @GloomyFolken and @tox1cozZ
Так же, не стесняйтесь задавать уточняющие вопросы, ну или просто вопросы.
Автор
fukkivdan
Просмотры
586
Первый выпуск
Обновление
Оценка
5.00 звёзд 1 оценок

Другие ресурсы пользователя fukkivdan

Последние рецензии

Потанцевал есть. Хорош!
fukkivdan
fukkivdan
Лучше б код разобрал и отревьюил)0
Сверху