- Версия(и) Minecraft
- 0+
Статья посвящается уже смешарикам в области графического программирования.
Не претендую на лучшую реализацию, всегда можно сделать что-то лучше чем у меня. Гайд описывает в общих красках возможности OpenGL, о которых обычный тессалятор энджоер не в курсе.
Введение:
Никому не секрет, что вызовы нативных методов openGL очень ресурсозатратны - поэтому следует их минимизировать
Представим сцену, где у вас есть много повторяющихся объектов.
У нас есть потребность рисовать вокруг игрока густой лес, но решившись нарисовать это при помощи тессолятора или дисплей листов мы столкнемся с просадками фпс`а, что обусловлено постоянным смещением матриц и отрисовки под каждый объект в сцене.
Тут-то и стоит вспомнить про Инстансинг
Минутка ликбеза:
Geometry Instancing (дублирование геометрии) — программная техника (методика) в трёхмерной компьютерной графике преимущественно реального времени. Суть Geometry Instancing состоит в визуализации множества копий одной полигональной сетки в трёхмерной сцене за один подход. Эта методика используется, как правило, для множества однотипных объектов на сцене, которые расположены достаточно далеко от виртуальной камеры: деревьев, кустов, травы, одинаковых сооружениях и предметах.
Geometry Instancing является прежде всего оптимизационной методикой, то есть он предназначен прежде всего для увеличения скорости визуализации без уменьшения качества.
Geometry Instancing стал основной функцией OpenGL, начиная с версии 3.1 в 2009 году.
Применение на практике:
Для упрощения работы с матрицами использовался JOML
Для входных данных модели использовался JglTF
Начнем с загрузки модели
- Я предпочту использовать загрузчик из коробки JglTF`а.
Стоит упомянуть, что для более эффективного результата вам стоит реализовать асинхронную подгрузку ресурсов, а так-же прикрутить Mip-Mapping вместе с True Impostors.
Увы, это придется реализовать вам. В рамках гайда слива кода как такового - не будет.
В качестве подсказки могу дать наводку, что:
- WorldSavedData в связке с чанками хорошо подойдет для подгрузки/отгрузки моделей, вычисления дистанции и выбора упрощенной геометрии для объекта, а так-же как способ хранения нашей YourNBT.class с матрицей трансформации.
Что потребуется дальше
YourNBT.class
YourWordRender.class
YourShader.class
YourModel.class
Когда подготовка закончена мы можем перейти к самому главному - рендеру нашего меша вместе с GLSL шейдером.
Наш меш должен содержать следующие данные.
Ключевыми методами у нас будут renderInstanced и delete.
Первый будет собирать и вызывать рендер, а второй при отгрузке удалять лишние ресурсы с GPU
Давайте ближе рассмотрим компиляцию.
Мы создаем и биндим наш VAO
Инициализируем буферы
Заполняем буферы нашими массивами и флипаем
Вишенкой на торте будет сохранением наших VBO с получением индексов
Теперь когда компиляция позади, нам необходимо вызвать VAO
Вся изюминка содержится в методе setupMatrixsBuffer, который позволяет нам устанавливать уникальные матрицы под каждый экземпляр нашего меша.
Здесь мы пакуем всю информацию о матрицах в единый VBO благодаря glVertexAttribDivisor и glVertexAttribPointer
glVertexAttribDivisor - задает экземпляр данных, а не данные вершин. Она принимает 2 параметра: первый - атрибут вершины, а второй говорит OpenGL скорость, с которой будут изменяться данные для образцов
glVertexAttribPointer - говорит, откуда брать данные для массива атрибутов, а также в каком формате эти данные находятся
Таким магическим образом наши матрицы будут запакованы внутрь VAO и мы не будем ограничены в количестве uniform, нежели бы мы использовали gl_InstanceID (Плохая практика, никогда так не делайте, если вы не можете предсказать количество входных данных).
В шейдере нам лишь остается помножить текущую матрицу проекции на VBO матрицы
Демонстрация
В заключение этой статьи о технике instanced рендеринга, мы разобрали подходы и реализации, которые основываются как на псевдокоде, так и на фрагментах кода из моего собственного проекта. Использование instanced рендеринга может значительно улучшить производительность вашего рендера за счёт уменьшения количества дравколлов и нагрузки на CPU/GPU.
Хотелось бы поблагодарить сообщество за поддержку. Отдельная благодарность выражается fukkivdan за вдохновение и идею распространения знаний среди широких масс. Спасибо Dioxide за информирование о технике instanced рендера, и большое спасибо GloomyFolken за его вклад в образование и погружение сообщества форума в мир OpenGL.
Буду рад любой рецензии и комментариям по поводу этой статьи. Ваши мнения и предложения помогут улучшить качество представленного материала и способствовать дальнейшему развитию темы.
Контакты для связи:
Моя группа с услугами
Моя группа с демками модов
Мой дискорд - liray0
Всем удачного рендера!
Не претендую на лучшую реализацию, всегда можно сделать что-то лучше чем у меня. Гайд описывает в общих красках возможности OpenGL, о которых обычный тессалятор энджоер не в курсе.
Введение:
Никому не секрет, что вызовы нативных методов openGL очень ресурсозатратны - поэтому следует их минимизировать
Представим сцену, где у вас есть много повторяющихся объектов.
У нас есть потребность рисовать вокруг игрока густой лес, но решившись нарисовать это при помощи тессолятора или дисплей листов мы столкнемся с просадками фпс`а, что обусловлено постоянным смещением матриц и отрисовки под каждый объект в сцене.
Тут-то и стоит вспомнить про Инстансинг
Минутка ликбеза:
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'
}
Начнем с загрузки модели
- Я предпочту использовать загрузчик из коробки 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
Наш child класс
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;
Первый будет собирать и вызывать рендер, а второй при отгрузке удалять лишние ресурсы с 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();
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);
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);
}
glVertexAttribDivisor - задает экземпляр данных, а не данные вершин. Она принимает 2 параметра: первый - атрибут вершины, а второй говорит OpenGL скорость, с которой будут изменяться данные для образцов
glVertexAttribPointer - говорит, откуда брать данные для массива атрибутов, а также в каком формате эти данные находятся
Таким магическим образом наши матрицы будут запакованы внутрь VAO и мы не будем ограничены в количестве uniform, нежели бы мы использовали gl_InstanceID (Плохая практика, никогда так не делайте, если вы не можете предсказать количество входных данных).
В шейдере нам лишь остается помножить текущую матрицу проекции на VBO матрицы
C:
gl_Position = gl_ModelViewProjectionMatrix * (vec4(position, 1.0) * instanceMatrix);
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;
}
Демонстрация
Хотелось бы поблагодарить сообщество за поддержку. Отдельная благодарность выражается fukkivdan за вдохновение и идею распространения знаний среди широких масс. Спасибо Dioxide за информирование о технике instanced рендера, и большое спасибо GloomyFolken за его вклад в образование и погружение сообщества форума в мир OpenGL.
Буду рад любой рецензии и комментариям по поводу этой статьи. Ваши мнения и предложения помогут улучшить качество представленного материала и способствовать дальнейшему развитию темы.
Контакты для связи:
Моя группа с услугами
Моя группа с демками модов
Мой дискорд - liray0
Всем удачного рендера!