Минимизируем вызовы OpenGL | Instancing

Минимизируем вызовы OpenGL | Instancing

Версия(и) Minecraft
0+
Статья посвящается уже смешарикам в области графического программирования.

Не претендую на лучшую реализацию, всегда можно сделать что-то лучше чем у меня. Гайд описывает в общих красках возможности OpenGL, о которых обычный тессалятор энджоер не в курсе.

Введение:
Никому не секрет, что вызовы нативных методов openGL очень ресурсозатратны - поэтому следует их минимизировать

Представим сцену, где у вас есть много повторяющихся объектов.
1713291373705.png


У нас есть потребность рисовать вокруг игрока густой лес, но решившись нарисовать это при помощи тессолятора или дисплей листов мы столкнемся с просадками фпс`а, что обусловлено постоянным смещением матриц и отрисовки под каждый объект в сцене.

Тут-то и стоит вспомнить про Инстансинг

Минутка ликбеза:
Geometry Instancing (дублирование геометрии) — программная техника (методика) в трёхмерной компьютерной графике преимущественно реального времени. Суть Geometry Instancing состоит в визуализации множества копий одной полигональной сетки в трёхмерной сцене за один подход. Эта методика используется, как правило, для множества однотипных объектов на сцене, которые расположены достаточно далеко от виртуальной камеры: деревьев, кустов, травы, одинаковых сооружениях и предметах.

Geometry Instancing является прежде всего оптимизационной методикой, то есть он предназначен прежде всего для увеличения скорости визуализации без уменьшения качества.
Geometry Instancing стал основной функцией OpenGL, начиная с версии 3.1 в 2009 году.

Применение на практике:

Для упрощения работы с матрицами использовался JOML
Для входных данных модели использовался JglTF
Gradle (Groovy):
repositories {
    mavenCentral()
}

dependencies {
    implementation 'de.javagl:jgltf-model:2.0.0'
    implementation 'org.joml:joml:1.10.5'
}

Начнем с загрузки модели
- Можем воспользоваться Jassimp если вы планируете использовать несколько форматов. Мой коллега по цеху fukkivdan кратко описал возможности данной библиотеки в своей статье

- Но я предпочту использовать загрузчик из коробки JglTF`а.
Loader:
    private static final GltfModelReader READER = new GltfModelReader();

    public static YourModel load(URI uri) {
        // Ваша модель
        YourModel model = new YourModel();
        try {
            // Парсер из коробки JglTF
            GltfModel gltfModel = READER.read(uri);

            for (NodeModel nodeModel : gltfModel.getNodeModels()) {
                // Получаем смещение нодов (если они имеются, иначе задаем стандартные значения)
                float[] translation = nodeModel.getTranslation() == null ? new float[]{0, 0, 0} : nodeModel.getTranslation();
                float[] rotation = nodeModel.getRotation() == null ? new float[]{0, 0, 0, 1} : nodeModel.getRotation();
                float[] scale = nodeModel.getScale() == null ? new float[]{1, 1, 1} : nodeModel.getScale();
                for (MeshModel meshGModel : nodeModel.getMeshModels()) {
                    for (MeshPrimitiveModel meshModel : meshGModel.getMeshPrimitiveModels()) {
                        if (meshModel.getMode() != 4) {
                            LOGGER.log("Некоторые сетки не являются треугольниками");
                            continue;
                        }
                        if (meshModel.getAttributes().get("POSITION").getCount() >= Integer.MAX_VALUE - 1) {
                            LOGGER.log("Слишком много точек в одной сетке");
                            continue;
                        }

                        // Читаем входные данные модели
                        // readAccessorToList() вам в помощь
                        meshModel.getAttributes().get("POSITION").getBufferViewModel().getBufferViewData();
                        meshModel.getAttributes().get("TEXCOORD_0").getBufferViewModel().getBufferViewData();
                        meshModel.getAttributes().get("NORMAL").getBufferViewModel().getBufferViewData();

                        ByteBuffer buffer = meshModel.getIndices().getBufferViewModel().getBufferViewData();
                        int indicesType = meshModel.getIndices().getComponentType();
                        while (buffer.hasRemaining()) {
                            // Читаем индексы модели
                            int indice = getIndice(buffer, indicesType);
                            // После чтения необходимо его записать в массив
                        }

                        // Тут мы должны применить необходимые трансформации текущей ноды к мешу
                        for (Vector3f vector3f : new ArrayList<Vector3f>() /* Лист вершин из #readAccessorToList */) {
                            Matrix4f transformMatrix = new Matrix4f()
                                    .setTranslation(translation[0], translation[1], translation[2])
                                    .rotate(new Quaternionf(rotation[0], rotation[1], rotation[2], rotation[3]))
                                    .scale(scale[0], scale[1], scale[2]);
                            Vector3f vertex = transformMatrix.transformPosition(vector3f);
                            // После чтения необходимо его записать в массив
                        }

                        for (Vector2f vector2f : new ArrayList<Vector2f>() /* Лист UV из #readAccessorToList */) {
                            // После чтения необходимо его записать в массив
                        }

                        for (Vector3f vector3f : new ArrayList<Vector3f>() /* Лист нормалей из #readAccessorToList */) {
                            // После чтения необходимо его записать в массив
                        }

                        YourMesh yourMesh = new YourMesh();
                        // Заполняем YourMesh входными данными модели
                        // Реализация за вами

                        // Дополняем нашу модель новым мешем
                        // model.meshes.add(yourMesh);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return model;
    }

    public static int getIndice(ByteBuffer buf, int type) {
        if (type == GL11.GL_UNSIGNED_BYTE) {
            return buf.get() & 0xff;
        } else if (type == GL11.GL_UNSIGNED_SHORT) {
            return buf.getShort() & 0xffff;
        } else {
            return buf.getInt();
        }
    }

    @SuppressWarnings({"rawtypes"})
    public static void readAccessorToList(ByteBuffer buf, List list, int type) {
        readAccessorToList(buf, list, type, GL11.GL_FLOAT);
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    public static void readAccessorToList(ByteBuffer buf, List list, int type, int mode) {
        while (buf.hasRemaining()) {
            if (type == 2) {
                list.add(new Vector2f(buf.getFloat(), buf.getFloat()));
            } else if (type == 3) {
                list.add(new Vector3f(buf.getFloat(), buf.getFloat(), buf.getFloat()));
            } else if (type == 4) {
                if (mode == GL11.GL_UNSIGNED_BYTE || mode == GL11.GL_BYTE) {
                    list.add(new Vector4i(buf.get() & 0xff, buf.get() & 0xff, buf.get() & 0xff, buf.get() & 0xff));
                } else if (mode == GL11.GL_UNSIGNED_SHORT || mode == GL11.GL_SHORT) {
                    list.add(new Vector4i(buf.getShort() & 0xffff, buf.getShort() & 0xffff, buf.getShort() & 0xffff,
                            buf.getShort() & 0xffff));
                } else if (mode == GL11.GL_UNSIGNED_INT || mode == GL11.GL_INT) {
                    list.add(new Vector4f(buf.getInt(), buf.getInt(), buf.getInt(), buf.getInt()));
                } else if (mode == GL11.GL_FLOAT) {
                    list.add(new Vector4f(buf.getFloat(), buf.getFloat(), buf.getFloat(), buf.getFloat()));
                } else {
                    throw new Error("Неожиданный тип: " + mode);
                }
            } else {
                throw new Error("Неожиданная единица");
            }
        }
    }

Стоит упомянуть, что для более эффективного результата вам стоит реализовать асинхронную подгрузку ресурсов, а так-же прикрутить Mip-Mapping вместе с True Impostors.
Увы, это придется реализовать вам. В рамках гайда слива кода как такового - не будет.
В качестве подсказки могу дать наводку, что:
- WorldSavedData в связке с чанками хорошо подойдет для подгрузки/отгрузки моделей, вычисления дистанции и выбора упрощенной геометрии для объекта, а так-же как способ хранения нашей YourNBT.class с матрицей трансформации.

Что потребуется дальше

YourNBT.class
YourWordRender.class
YourShader.class
YourModel.class

Java:
@Data
public class YourNBT implements net.minecraftforge.common.util.INBTSerializable<NBTTagCompound> {

    protected String id = UUID.randomUUID().toString();
    protected Vector3f position = new Vector3f();
    protected Vector3f rotation = new Vector3f();
    protected float scale = 1F;

    public YourNBT() {
    }

    public YourNBT(NBTTagCompound tag) {
        deserializeNBT(tag);
    }

    @Override
    public NBTTagCompound serializeNBT() {
        NBTTagCompound tag = new NBTTagCompound();
        tag.setString("id", id);
        tag.setFloat("dX", position.x);
        tag.setFloat("dY", position.y);
        tag.setFloat("dZ", position.z);
        tag.setFloat("dRX", rotation.x);
        tag.setFloat("dRY", rotation.y);
        tag.setFloat("dRZ", rotation.z);
        tag.setFloat("s", scale);
        return tag;
    }

    @Override
    public void deserializeNBT(NBTTagCompound tag) {
        setId(tag.getString("id"));
        setPosition(new Vector3f(
                tag.getFloat("dX"),
                tag.getFloat("dY"),
                tag.getFloat("dZ")
        ));
        setRotation(new Vector3f(
                tag.getFloat("dRX"),
                tag.getFloat("dRY"),
                tag.getFloat("dRZ")
        ));
        setScale(tag.getFloat("s"));
    }

    public Matrix4f getMatrix() {
        Matrix4f model = new Matrix4f().identity();
        model.translateLocal(position.x, position.y, position.z);
        model.rotateX((float) Math.toRadians(this.rotation.x));
        model.rotateY((float) Math.toRadians(this.rotation.y));
        model.rotateZ((float) Math.toRadians(this.rotation.z));
        model.scale(this.scale);
        return model;
    }

}
Java:
public class YourWordRender {

    public void generalRender(double xCoord, double yCoord, double zCoord, ConcurrentHashMap<String, ScrumModel> loadedModels) {
        DecorationClient client = DecorationClient.getInstance();
        HashMap<String, ChunkModels> copy = new HashMap<>(client.resources.loadedChunkModels);
        // Моя реализация асинхронности в продакшене, естественно вам придется попыхтеть и написать свою
        for (ChunkModels models : copy.values()) {
            models.forEach((model, decorationNBTs) -> {
                if (!client.resources.avalibleModel(model)) return;
             
                // Заполняем массив матриц
                Matrix4f[] matrixsRender = calculateMatrixBuffer(decorationNBTs, true);
                if (matrixsRender == null) return;
             
                // Получаем количество уникальных матриц
                int amount = matrixsRender.length;
             
                // Заполняем буфер матриц
                FloatBuffer matrixBuffer = createMatrixBuffer(matrixsRender);
                // Заполняем буфер освещения
                FloatBuffer lightMapBuffer = createLightMapBuffer(decorationNBTs);

                ScrumModel renderModel = loadedModels.get(model);
                GL11.glPushMatrix();
                GL11.glTranslated(-xCoord, -yCoord, -zCoord);
                renderModel.renderInstanced(matrixBuffer, lightMapBuffer, amount);
                GL11.glPopMatrix();
            });
        }
    }

    public static Matrix4f[] calculateMatrixBuffer(Collection<DecorationNBT> posList, boolean render) {
        ArrayList<Matrix4f> matrix4fs = new ArrayList<>();
        for (DecorationNBT decoration : posList) matrix4fs.add(decoration.getMatrix(render));
        if (matrix4fs.isEmpty()) return null;
        return matrix4fs.toArray(new Matrix4f[0]);
    }

    public static FloatBuffer createMatrixBuffer(Matrix4f[] matrixs) {
        FloatBuffer buffer = BufferUtils.createFloatBuffer(matrixs.length * 16);
        for (Matrix4f matrix : matrixs) {
            buffer.put(matrixToArray(matrix));
        }
        buffer.flip();
        return buffer;
    }

    public static FloatBuffer createLightMapBuffer(Collection<DecorationNBT> posList) {
        FloatBuffer buffer = BufferUtils.createFloatBuffer(posList.size() * 2);
        for (DecorationNBT nbt : posList) {
            Vector3f position = nbt.getPosition();
            BlockPos blockPos = new BlockPos(position.x, position.y, position.z);
            int blockLight = mc.world.getRawLight(blockPos, EnumSkyBlock.BLOCK);
            int skyLight = mc.world.getRawLight(blockPos, EnumSkyBlock.SKY);
            buffer.put(1.0f - (1.0f / blockLight));
            buffer.put(1.0f - (1.0f / skyLight));
        }
        buffer.flip();
        return buffer;
    }

    public static float[] matrixToArray(Matrix4f matrix) {
        float[] floatTemp = new float[16];
        floatTemp[0] = matrix.m00();
        floatTemp[1] = matrix.m01();
        floatTemp[2] = matrix.m02();
        floatTemp[3] = matrix.m03();
        floatTemp[4] = matrix.m10();
        floatTemp[5] = matrix.m11();
        floatTemp[6] = matrix.m12();
        floatTemp[7] = matrix.m13();
        floatTemp[8] = matrix.m20();
        floatTemp[9] = matrix.m21();
        floatTemp[10] = matrix.m22();
        floatTemp[11] = matrix.m23();
        floatTemp[12] = matrix.m30();
        floatTemp[13] = matrix.m31();
        floatTemp[14] = matrix.m32();
        floatTemp[15] = matrix.m33();
        return floatTemp;
    }

}
База под шейдер позаимствовано у ThinMatrix
Java:
/**
 * Общая шейдерная программа содержащая все атрибуты и методы, которые
 * каждый шейдер будет иметь
 */
public abstract class ShaderProgram {

    /**
     * идентификатор программы
     */
    public int programId;
    /**
     * идентификатор вершинного шейдера
     */
    public int vertexShaderId;
    /**
     * идентификатор фрагментного шейдера
     */
    public int fragmentShaderId;

    public boolean hasError = false;

    public String errorFileName;
    private final Timer errorTimer = new Timer(10000);

    /**
     * Конструктор шейдерной программы
     *
     * @param vertexFile   путь к файлу вершинного шейдера
     * @param fragmentFile путь к файлу фрагментного шейдера
     */
    public ShaderProgram(ResourceLocation vertexFile, ResourceLocation fragmentFile) {
        errorFileName = vertexFile.getPath().toUpperCase() + "\nOR\n" + fragmentFile.getPath().toUpperCase();
        // загрузка шейдеров в OpenGL
        vertexShaderId = loadShader(vertexFile, GL20.GL_VERTEX_SHADER, this);
        fragmentShaderId = loadShader(fragmentFile, GL20.GL_FRAGMENT_SHADER, this);

        // создание шейдерной программы
        programId = GL20.glCreateProgram(); // инициализация шейдерной программы
        if (programId == 0) {
            this.hasError = true;
            Decoration.log("Немогу создать шейдер");
        }

        // связываем шейдеры с программой
        GL20.glAttachShader(programId, vertexShaderId);
        GL20.glAttachShader(programId, fragmentShaderId);

        bindAttributes();

        // присоединяем шейдерную программу
        GL20.glLinkProgram(programId);
        if (GL20.glGetProgrami(programId, GL20.GL_LINK_STATUS) == GL11.GL_FALSE) {
            this.hasError = true;
            Decoration.log("Ошибка присоединения шейдерной программы: " + GL20.glGetProgramInfoLog(programId, 1024));
        }

        // проверяем шейдерную программу
        GL20.glValidateProgram(programId);
        if (GL20.glGetProgrami(programId, GL20.GL_VALIDATE_STATUS) == GL11.GL_FALSE) {
            this.hasError = true;
            Decoration.log("Ошибка проверки шейдерной программы: " + GL20.glGetProgramInfoLog(programId, 1024));
        }
    }

    /**
     * Метод загрузки и компиляция шейдера в OpenGL в зависимости от типа
     *
     * @param location      путь к файлу
     * @param type          тип шейдера
     * @param shaderProgram
     * @return идентификатор созданного шейдера
     */
    private static int loadShader(ResourceLocation location, int type, ShaderProgram shaderProgram) {
        StringBuilder shaderSource = new StringBuilder();
        try {
            IResource resource = Minecraft.getMinecraft().getResourceManager().getResource(location);
            BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()));
            String line;
            while ((line = reader.readLine()) != null) {
                shaderSource.append(line).append("//\n");
            }
            reader.close();
        } catch (IOException e) {
            Decoration.log(e.getMessage());
            System.exit(-1);
        }
        // инициализируем шейдер определенного типа
        int shaderID = GL20.glCreateShader(type);
        if (shaderID == 0) {
            shaderProgram.hasError = true;
            Decoration.log("Ошибка создания шейдера. Тип: " + type);
        }
        // загружаем в шейдер исходный код
        GL20.glShaderSource(shaderID, shaderSource);
        // компилируем шейдерный код
        GL20.glCompileShader(shaderID);
        if (GL20.glGetShaderi(shaderID, GL20.GL_COMPILE_STATUS) == GL11.GL_FALSE) {
            shaderProgram.hasError = true;
            Decoration.log("Ошибка компиляции шейдерной программы: " + GL20.glGetShaderInfoLog(shaderID, 1024));
        }
        return shaderID;
    }

    public void bindSampler() {
        glUniform1i(glGetUniformLocation(programId, "sampler"), 0);
    }

    /**
     * Регистрируем имя юниформы для шейдерной программы
     *
     * @param uniformName имя юниформы
     * @return идентификатор юниформы
     */
    protected int getUniformLocation(String uniformName) {
        return GL20.glGetUniformLocation(programId, uniformName);
    }

    /**
     * Подключение атрибутов
     */
    protected abstract void bindAttributes();

    /**
     * Связывание атрибута с программой
     *
     * @param attributeNumber номер списка атрибута в VAO
     * @param variableName    наименование переменной
     */
    protected void bindAttribute(int attributeNumber, String variableName) {
        GL20.glBindAttribLocation(programId, attributeNumber, variableName);
    }

    /**
     * Загрузка float переменной в юниформу
     *
     * @param location идентификатор юниформы
     * @param value    значение
     */
    protected void loadFloat(int location, float value) {
        GL20.glUniform1f(location, value);
    }

    /**
     * Загрузка int переменной в юниформу
     *
     * @param location идентификатор юниформы
     * @param value    значение
     */
    protected void loadInt(int location, int value) {
        GL20.glUniform1i(location, value);
    }

    /**
     * Загрузка Vector3f переменной в юниформу
     *
     * @param location идентификатор юниформы
     * @param vector   значение
     */
    protected void loadVector(int location, Vector3f vector) {
        GL20.glUniform3f(location, vector.x, vector.y, vector.z);
    }

    /**
     * Загрузка Vector2f переменной в юниформу
     *
     * @param location идентификатор юниформы
     * @param vector   значение
     */
    protected void loadVector(int location, Vector2f vector) {
        GL20.glUniform2f(location, vector.x, vector.y);
    }

    /**
     * Загрузка float переменной в юниформу
     *
     * @param location идентификатор юниформы
     * @param value    значение
     */
    protected void loadBoolean(int location, boolean value) {
        GL20.glUniform1f(location, value ? 1 : 0);
    }

    public void run(Runnable runnable) {
        if (!hasError) {
            start();
            runnable.run();
            stop();
        } else {
            if (errorTimer.isOver(true)) {
                Decoration.log("SHADER ERROR:\n" + this.errorFileName);
            }
        }
    }

    public void run(Runnable runnable, boolean stop) {
        if (!hasError) {
            start();
            runnable.run();
            if (stop) {
                stop();
            }
        } else {
            if (errorTimer.isOver(true)) {
                Decoration.log("SHADER ERROR:\n" + this.errorFileName);
            }
        }
    }

    /**
     * Запуск шейдерной программы.
     */
    public void start() {
        GL20.glUseProgram(programId);
    }

    /**
     * Прекращение использования программы
     */
    public void stop() {
        GL20.glUseProgram(0);
    }

    /**
     * Очистка и освобождение ресурсов
     */
    public void cleanUp() {
        stop(); // остановка программы
        // отсоединяем от программы и удаляем шейдеры
        if (vertexShaderId != 0) {
            GL20.glDetachShader(programId, vertexShaderId);
            GL20.glDeleteShader(vertexShaderId);
        }
        if (fragmentShaderId != 0) {
            GL20.glDetachShader(programId, fragmentShaderId);
            GL20.glDeleteShader(fragmentShaderId);
        }
        // освобождение ресурсов программы
        if (programId != 0)
            GL20.glDeleteProgram(programId);
    }
}

Наш child класс
Java:
public class YourShader extends ShaderProgram {

    private static final ResourceLocation VERTEX_FILE = new ResourceLocation(MOD_ID, "shaders/vertexshader.vert");;
    private static final ResourceLocation FRAGMENT_FILE = new ResourceLocation(MOD_ID, "shaders/fragmentshader.frag");;

    public InstanceModelsShader() {
        super(VERTEX_FILE, FRAGMENT_FILE);
    }

    @Override
    protected void bindAttributes() {
        super.bindAttribute(0, "position");
        super.bindAttribute(1, "textureCoords");
        super.bindAttribute(2, "normal");
        // 3 - 6 = 4
        super.bindAttribute(3, "instanceMatrix");
        // 7 - 8 = 2
        super.bindAttribute(7, "lightCoords");
    }

}
Java:
public class YourModel {

    public HashMap<String, ScrumMesh> meshes = new HashMap<>();
    public String name = "unknown";
    public boolean loaded = false;

    public void delete() {
        if (!loaded) {
            LOGGER.log("Попытка удаления модели, но она не инициализирована. " + name);
            return;
        }
        meshes.forEach((n, m) -> {
            m.delete();
        });
    }

    public void renderInstanced(FloatBuffer matrixBuffer, FloatBuffer lightMapBuffer, int amount) {
        if (!loaded) {
            LOGGER.log("Попытка рендера модели, но она не инициализирована. " + name);
            return;
        }
        meshes.forEach((n, m) -> {
            m.renderInstanced(matrixBuffer, lightMapBuffer, amount);
        });
    }
 
}

Когда подготовка закончена мы можем перейти к самому главному - рендеру нашего меша вместе с GLSL шейдером.

Наш меш должен содержать следующие данные.
Java:
protected final int[] indicies;
protected final float[] verticies;
protected final float[] normals;
protected final float[] uvs;
Ключевыми методами у нас будут renderInstanced и delete.

Первый будет собирать и вызывать рендер, а второй при отгрузке удалять лишние ресурсы с GPU

Давайте ближе рассмотрим компиляцию.

Мы создаем и биндим наш VAO
Java:
vao = createVao();
Инициализируем буферы
Java:
IntBuffer indicesBuffer = BufferUtils.createIntBuffer(indicies.length);
FloatBuffer verticiesBuffer = BufferUtils.createFloatBuffer(verticies.length);
FloatBuffer uvsBuffer = BufferUtils.createFloatBuffer(uvs.length);
FloatBuffer normalBuffer = BufferUtils.createFloatBuffer(normals.length);
Заполняем буферы нашими массивами и флипаем
Java:
indicesBuffer.put(indicies).flip();
verticiesBuffer.put(verticies).flip();
uvsBuffer.put(uvs).flip();
normalBuffer.put(normals).flip();
Вишенкой на торте будет сохранением наших VBO с получением индексов
Java:
ibo = bindIndicesBuffer(indicesBuffer);
pos_vbo = storeDataInAttributeList(0, 3, verticiesBuffer);
tex_vbo = storeDataInAttributeList(1, 2, uvsBuffer);
normal_vbo = storeDataInAttributeList(2, 3, normalBuffer);

Теперь когда компиляция позади, нам необходимо вызвать VAO
Java:
setupMatrixsBuffer(matrixBuffer);
setupLightMapBuffer(lightMapBuffer);

GL30.glBindVertexArray(vao);
GL31.glDrawElementsInstanced(GL11.GL_TRIANGLES, indicies.length, GL11.GL_UNSIGNED_INT, 0, amount);
GL30.glBindVertexArray(0);
Вся изюминка содержится в методе setupMatrixsBuffer, который позволяет нам устанавливать уникальные матрицы под каждый экземпляр нашего меша.
Java:
private void setupMatrixsBuffer(FloatBuffer buffer) {
    int mbo = GL15.glGenBuffers();
    GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, mbo);
    GL15.glBufferData(GL15.GL_ARRAY_BUFFER, buffer, GL15.GL_STATIC_DRAW);
    GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);

    GL30.glBindVertexArray(vao);
    GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, mbo);

    int stride = 16 * Float.BYTES;
    for (int i = 0; i < 4; i++) {
        GL20.glEnableVertexAttribArray(3 + i);
        GL20.glVertexAttribPointer(3 + i, 4, GL11.GL_FLOAT, false, stride, i * 4 * Float.BYTES);
        GL33.glVertexAttribDivisor(3 + i, 1);
    }

    GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
    GL30.glBindVertexArray(0);
    GL15.glDeleteBuffers(mbo);
}
Здесь мы пакуем всю информацию о матрицах в единый VBO благодаря glVertexAttribDivisor и glVertexAttribPointer

glVertexAttribDivisor - задает экземпляр данных, а не данные вершин. Она принимает 2 параметра: первый - атрибут вершины, а второй говорит OpenGL скорость, с которой будут изменяться данные для образцов

glVertexAttribPointer - говорит, откуда брать данные для массива атрибутов, а также в каком формате эти данные находятся

Таким магическим образом наши матрицы будут запакованы внутрь VAO и мы не будем ограничены в количестве uniform, нежели бы мы использовали gl_InstanceID (Плохая практика, никогда так не делайте, если вы не можете предсказать количество входных данных).

В шейдере нам лишь остается помножить текущую матрицу проекции на VBO матрицы
C:
gl_Position = gl_ModelViewProjectionMatrix * (vec4(position, 1.0) * (instanceMatrix - offsetMatrix));

Java:
@Data
public class YourMesh {

    protected final String name;
    protected final int[] indicies;
    protected final float[] verticies;
    protected final float[] normals;
    protected final float[] uvs;

    @Setter
    @Nullable
    private ResourceLocation texture;

    private boolean deleted = false;
    private boolean compiled = false;
    private boolean compiling = false;

    private int vao = -1;
    private int ibo = -1;
    private int pos_vbo = -1;
    private int tex_vbo = -1;
    private int normal_vbo = -1;

    public YourMesh(ScrumUnit geometry) {
        this.name = geometry.name;
        this.indicies = geometry.indicies;
        this.verticies = geometry.verticies;
        this.normals = geometry.normals;
        this.uvs = geometry.uvs;
    }

    private int createVao() {
        int vaoId = GL30.glGenVertexArrays();
        GL30.glBindVertexArray(vaoId);
        return vaoId;
    }

    private void compileVAO() {
        if (compiling) {
            return;
        }
        compiling = true;

        vao = createVao();


        IntBuffer indicesBuffer = BufferUtils.createIntBuffer(indicies.length);
        FloatBuffer verticiesBuffer = BufferUtils.createFloatBuffer(verticies.length);
        FloatBuffer uvsBuffer = BufferUtils.createFloatBuffer(uvs.length);
        FloatBuffer normalBuffer = BufferUtils.createFloatBuffer(normals.length);

        indicesBuffer.put(indicies).flip();
        verticiesBuffer.put(verticies).flip();
        uvsBuffer.put(uvs).flip();
        normalBuffer.put(normals).flip();

        ibo = bindIndicesBuffer(indicesBuffer);
        pos_vbo = storeDataInAttributeList(0, 3, verticiesBuffer);
        tex_vbo = storeDataInAttributeList(1, 2, uvsBuffer);
        normal_vbo = storeDataInAttributeList(2, 3, normalBuffer);
        GL30.glBindVertexArray(0);

        this.compiled = true;
        this.compiling = false;
    }

    private int storeDataInAttributeList(int attributeNumber, int coordinateSize, FloatBuffer data) {
        int vboId = GL15.glGenBuffers();
        GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboId);
        GL15.glBufferData(GL15.GL_ARRAY_BUFFER, data, 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 int bindIndicesBuffer(IntBuffer indices) {
        int ibo = GL15.glGenBuffers();
        GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ibo);
        GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, indices, GL15.GL_STATIC_DRAW);
        return ibo;
    }

    public void renderInstanced(FloatBuffer matrixBuffer, FloatBuffer lightMapBuffer, int amount) {
        if (deleted) return;

        if (!compiled) {
            try {
                compileVAO();
                return;
            } catch (Throwable t) {
                t.printStackTrace();
            }
        }
        try {
            callVAOInstanced(matrixBuffer, lightMapBuffer, amount);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void callVAOInstanced(FloatBuffer matrixBuffer, FloatBuffer lightMapBuffer, int amount) {
        if (!compiled) {
            return;
        }
        int programId = DecorationWorldRender.instancedShader.programId;

        DecorationWorldRender.instancedShader.run(() -> {
            if (texture != null) {
                GL13.glActiveTexture(GL13.GL_TEXTURE0);
                Minecraft.getMinecraft().getTextureManager().bindTexture(texture);
            } else {
                Minecraft.getMinecraft().getTextureManager().bindTexture(DecorationResources.defaultTexture);
            }

            glUniform1i(glGetUniformLocation(programId, "samplerTexture"), 0);
            glUniform1i(glGetUniformLocation(programId, "lightMap"), 1);

            setupMatrixsBuffer(matrixBuffer);
            setupLightMapBuffer(lightMapBuffer);

            GL30.glBindVertexArray(vao);
            GL31.glDrawElementsInstanced(GL11.GL_TRIANGLES, indicies.length, GL11.GL_UNSIGNED_INT, 0, amount);
            GL30.glBindVertexArray(0);

            GlStateManager.bindTexture(0);
        });

    }

    private void setupMatrixsBuffer(FloatBuffer buffer) {
        int mbo = GL15.glGenBuffers();
        GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, mbo);
        GL15.glBufferData(GL15.GL_ARRAY_BUFFER, buffer, GL15.GL_STATIC_DRAW);
        GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);

        GL30.glBindVertexArray(vao);
        GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, mbo);

        int stride = 16 * Float.BYTES;
        for (int i = 0; i < 4; i++) {
            GL20.glEnableVertexAttribArray(3 + i);
            GL20.glVertexAttribPointer(3 + i, 4, GL11.GL_FLOAT, false, stride, i * 4 * Float.BYTES);
            GL33.glVertexAttribDivisor(3 + i, 1);
        }

        GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
        GL30.glBindVertexArray(0);
        GL15.glDeleteBuffers(mbo);
    }

    private void setupLightMapBuffer(FloatBuffer buffer) {
        int lbo = GL15.glGenBuffers();
        GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, lbo);
        GL15.glBufferData(GL15.GL_ARRAY_BUFFER, buffer, GL15.GL_STATIC_DRAW);
        GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);

        GL30.glBindVertexArray(vao);
        GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, lbo);

        int index = 7;

        int stride = 2 * Float.BYTES;
        GL20.glEnableVertexAttribArray(index);
        GL20.glVertexAttribPointer(index, 2, GL11.GL_FLOAT, false, stride, 0);
        GL33.glVertexAttribDivisor(index, 1);


        GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
        GL30.glBindVertexArray(0);
        GL15.glDeleteBuffers(lbo);
    }

    public void delete() {
        GL30.glDeleteVertexArrays(vao);
        if (ibo != -1) {
            GL15.glDeleteBuffers(ibo);
        }
        if (pos_vbo != -1) {
            GL15.glDeleteBuffers(pos_vbo);
        }
        if (tex_vbo != -1) {
            GL15.glDeleteBuffers(tex_vbo);
        }
        if (normal_vbo != -1) {
            GL15.glDeleteBuffers(normal_vbo);
        }
        deleted = true;
    }

}
vertexshader.vert:
#version 130

in vec3 position;
in vec2 textureCoords;
in vec3 normal;
in mat4 instanceMatrix;
in vec2 lightCoords;

out vec2 pass_textureCoords;
out vec2 pass_lightCoords;
out vec3 surfaceNormal;

uniform mat4 offsetMatrix;

void main()
{
    gl_Position = gl_ModelViewProjectionMatrix * (vec4(position, 1.0) * (instanceMatrix - offsetMatrix));

    vec3 actualNormal = normal;
    surfaceNormal = (gl_Position * vec4(actualNormal, 0.0)).xyz;

    pass_textureCoords = textureCoords;
    pass_lightCoords = lightCoords;
}
fragmentshader.frag:
#version 400

in vec3 surfaceNormal;
in vec2 pass_textureCoords;
in vec2 pass_lightCoords;

uniform sampler2D samplerTexture;
uniform sampler2D lightMap;

uniform float shading;

void main()
{
    vec4 textureSampler = texture(samplerTexture, pass_textureCoords.st);

    // Light
    vec4 skyLightSampler = texture(lightMap, vec2(0.0, pass_lightCoords.t));
    vec4 blockLightSampler = texture(lightMap, vec2(pass_lightCoords.s, 1.0));
    vec4 interpolatedSkyLight = mix(skyLightSampler, vec4(1.0), skyLightSampler.r);
    vec4 interpolatedBlockLight = mix(blockLightSampler, vec4(0.5), blockLightSampler.r);
    // Light

    vec4 finalColor = textureSampler * (interpolatedBlockLight + interpolatedSkyLight);

    gl_FragColor = finalColor;
}

Демонстрация
В заключение этой статьи о технике instanced рендеринга, мы разобрали подходы и реализации, которые основываются как на псевдокоде, так и на фрагментах кода из моего собственного проекта. Использование instanced рендеринга может значительно улучшить производительность вашего рендера за счёт уменьшения количества дравколлов и нагрузки на CPU/GPU.

Хотелось бы поблагодарить сообщество за поддержку. Отдельная благодарность выражается fukkivdan за вдохновение и идею распространения знаний среди широких масс. Спасибо Dioxide за информирование о технике instanced рендера, и большое спасибо GloomyFolken за его вклад в образование и погружение сообщества форума в мир OpenGL.

Буду рад любой рецензии и комментариям по поводу этой статьи. Ваши мнения и предложения помогут улучшить качество представленного материала и способствовать дальнейшему развитию темы.

Контакты для связи:
Моя группа с услугами
Моя группа с демками модов
Мой дискорд - liray0

Всем удачного рендера!
Автор
LIRAY
Просмотры
362
Первый выпуск
Обновление
Оценка
5.00 звёзд 2 оценок

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

Ну вроде ты чё-то знаешь
LIRAY
LIRAY
Само cобой =)
Все таки оправдал потанцевал!
LIRAY
LIRAY
А то!
Сверху