- Версия(и) Minecraft
- 1.0+
Эта статья посвящена буферам кадра(framebuffers), как их использовать, зачем нужны, и какие в майнкрафте есть средства для удобной работы с ними.
В интернете есть много туториалов на эту тему, но использование буфера кадра в контексте уже написанной игры не так очевидно, как на чистом приложении.
Итак, что же все такие такое фреймбуфер?
Framebuffer object(FBO) - объект opengl, который позволяет рисовать объекты не напрямую на экран, а внутрь себя.
Таким образом мы может рисовать целые сцены никак не влияя на визуальный вид экрана.
После того как объект был нарисован в буфер кадра, его финальное изображение со всеми трансформациями накладывается
на текстурку буфера. Эту текстурку потом можно наложить на какой-нибудь объект или сохранить в изображения.
Основное применение фреймбуферов - post processing. Это наложения различных фильтров(таких как blur или bloom) на финальное изображение сцены.
Практика
Примечание - я буду использовать возможности версии 1.12.2 для рисования геометрических фигур, остальное почти не менялось на протяжении многих версий.
Первым делом создадим наш framebuffer:
Прекрасно, теперь надо понять, что мы хотим нарисовать. Для примера, я нарисую какие-нибудь объекты в мире, для этого подойдет
Пусть наш буфер будет такого же размера как и окно игры, чтобы впоследствии его можно было нарисовать в основной фбо майнкрафта.
Отлично, давайте же в него что-нибудь нарисуем!
Для этого сначала надо его забиндить:
Рисуем...
Вот и все! мы нарисовали объект в собственный буфер кадра.
Запускаем игру и видим...
...
Ничего не нарисовалось
Дело в том что мы рисовали не на экран, а в буфер кадра.
Магическим способом посмотрим содержимое того буфера кадра:
То что мы заказывали, квадрат 2x2 зеленого цвета, находится на блок ниже игрока(трансформации в связи с использованием
Но смотреть на буферы по отдельности занятие не из приятных, да и магией мы не владеем.
Поэтому вовместим, наложим, 2 изображения друг на друга.
Для этого нам нужно понять суровую правду: все объекты, чанки, энтити в майнкрафте на самом деле тоже рендерятся в framebuffer Правда этот же фреймбуфер сразу же рендерится на дисплей. Это просто некая обертка, нужная для некоторых спецэффектов.
Значит, чтобы совместить картинки буферов, надо получить нарисованную в текстурку сцену нашего фреймбуфера, и с этой текстуркой что-нибудь нарисовать. Вперед!
Текстурку мы забиндили, но что дальше, что бы нам нарисовать чтобы увидеть сцену в нашем fbo?
Мы нарисуем квадрат размером с окно майнкрафта, да так, что его не отличить будет от обычного мира
Квадрат рисуем так же как и в первый раз:
Запускаем...
Ура, квадрат рендерится во фреймбуфер, а он в свою очередь рендерится в майнкрафтовский буфер, который рендерится на дисплей.
Но погодите-ка, при движении камеры, старые пиксели не стираются:
В самом деле, в конце(или начале) каждого кадра фреймбуфер надо стрирать, дабы все пиксели из него удалились, и в следущем кадре мы их не видели.
Добавим в конец эвента
Уже лучше, но тут же появляется еще одна проблема: при изменени замеров окна игры, наш буфер кадра ведет себя очень странно, а все потому что мы его одн раз создали с определенными размерами и больше никогда не меняли. Сейчас же исправим. Добавим после ленивого создания буфера, но перед биндом, его обновление
Теперь наш квадрат в своем fbo ренедриттся так же как и обычный, на первый взгляд...
Теперь сделаем что-нибудь осмысленное. Например 'портал в другое измерение'.
Для этого просто отодвиним нарисованный нами ранее квадрат от игрока, в мир, скажем на координаты 0, 3, 0.
Неплохо, посмотрим на сие чудо издалека
Что случилось? почему наш портал просвечивает сквозь все?
Дело в том что мы рисовали объект во фреймбуфер, который ничего не знает о том что творится в основном буфере, не знает какой там буфер глубины.
А посему рисует изображение прямо сверху.
Исправим это. Сделать это довольно просто, для начала, просто скажем нашему фреймбуферу, чтобы он работал с глубиной, для этого вернемся к коду инициализации fbo и заменим последний аргумент на true:
Но этого как ни странно, недостаточно. Теперь наш fbo будет лишь стирать глубину. Для того что бы фреймбуфер мог работать с глубиной из майнкрафтовского буфера кадра, мы должны скопировать эту самую глубину.
Делается это так
Теперь перед рисованием нашего 'портала', дабы избежать рисования объектов на одной плоскости в одном месте, чуть сместим портал вперед:
Запускаем...
Отлично, теперь мы можем рендерить объекты в нашем fbo, как будто они рендерятся без него.
Но... Подождите, какой вообще в этом смысл?
Дело в том, что каждый уважающий себя портал обладает аурой, а у нашего портала ее нет. В ее недостатке и теряется смысл.
Так сделаем же ауру!
Для этого нам понадобятся шейдеры
И собственно блюр
Накладывание эффекта(блюра) на объекты происходит так:
Рендерим объекты в framebuffer, достаем текстурку этого буфер, накладываем ее на прямоугольник размером с экран(это мы уже сделали выше), применяем на этот прямоугольник шейдеры.
Теперь, имея все необходимые ингредиенты и знания, приступим к реализации.
Начнем с приготовлений(которые происходят после рендера всех объектов в наш буфер):
Далее включаем блюр и выставляем ему все параметры
Далее код рендера нашего фбо на квадрат остается прежним.
И конечно после рендера выключаем шейдер
Вот и весь пост процессинг, ничего сложного.
Запускаем...
Впечатляюще.
Но мы заблюрили наши объекты только по горизонтали, до полного счастья не хватает вертикального блюра.
Тут не все так просто, ибо гауссовский блюр(по двум осям) это такой эффект который недостижим(или очень затратен) в один проход шейдера. Но мы просто не можем взять и применить к одному рендерюжемуя объекту две шейдерных программы.
И тут нас опять выручают фреймбуферы. Как мы сделали это только что, мы можем изображение из нашего заблюренного буфера рендрить не напрямую на дисплей, а еще в один отдельный буфер, и как раз он уже будет финальным изображением сцены.
Звучит сложно, но это только на первый взгляд, реализуем же.
Инициализируем так же, только в этот раз глубина нам уже не нужна(в этом буфере будет рисовать всего 1 объект - экранный прямоугольник с шейдером, ничто его не заслонит)
После включения шейдера, рисуем в новый фбо:
Переключаем шейдер на вертикальный
И далее старый код, только рендерим мы уже текстурку из нового буфера
И конечно, каждый fbo надо очистить:
Проверяем...
Теперь на все объекты, нарисованные внутри нашего буфера, будет накладываться эффект блюра. Разве не прекрасно?
На этом все.
В заключение хочу сказать что буфер кадра очень полезная и не особо сложная вещь, поторая позволяет не только применять какие-то спецэффекты к изображению, но и завертывать целые сцены в небольшие объекты.
В интернете есть много туториалов на эту тему, но использование буфера кадра в контексте уже написанной игры не так очевидно, как на чистом приложении.
Итак, что же все такие такое фреймбуфер?
Framebuffer object(FBO) - объект opengl, который позволяет рисовать объекты не напрямую на экран, а внутрь себя.
Таким образом мы может рисовать целые сцены никак не влияя на визуальный вид экрана.
После того как объект был нарисован в буфер кадра, его финальное изображение со всеми трансформациями накладывается
на текстурку буфера. Эту текстурку потом можно наложить на какой-нибудь объект или сохранить в изображения.
Основное применение фреймбуферов - post processing. Это наложения различных фильтров(таких как blur или bloom) на финальное изображение сцены.
Практика
Примечание - я буду использовать возможности версии 1.12.2 для рисования геометрических фигур, остальное почти не менялось на протяжении многих версий.
Первым делом создадим наш framebuffer:
Java:
private static Framebuffer framebuffer;
RenderWorldLastEvent
Пусть наш буфер будет такого же размера как и окно игры, чтобы впоследствии его можно было нарисовать в основной фбо майнкрафта.
Java:
@SubscribeEvent
public void fboTest(RenderWorldLastEvent event) {
//инициализировать фбо лучше лениво, т.к. скорее всего класс
//в котором он содержится, загрузится не в том контексте opengl,
//в котором он потом будет использоваться
if (framebuffer == null)
framebuffer = new Framebuffer(mc.displayWidth, mc.displayHeight, false);
//про последний параметр(false) я расскажу позже
}
Для этого сначала надо его забиндить:
Java:
framebuffer.bindFramebuffer(false);
//рисуем...
framebuffer.unbindFramebuffer();
Java:
framebuffer.bindFramebuffer(false); //теперь рисуем в наш fbo
GL11.glBindTexture(GL_TEXTURE_2D, 0); // мало ли, кто рисовал до нас
GL11.glColor4f(0f, 1f, 0f, 1f);
//квадрат 2x2
Tessellator tessellator = Tessellator.getInstance();
BufferBuilder buffer = tessellator.getBuffer();
buffer.begin(7, DefaultVertexFormats.POSITION_TEX);
buffer.pos(-1, -1, 0).tex(0, 0).endVertex();
buffer.pos(1, -1, 0).tex(1, 0).endVertex();
buffer.pos(1, 1, 0).tex(1, 1).endVertex();
buffer.pos(-1, 1, 0).tex(0, 1).endVertex();
tessellator.draw();
framebuffer.unbindFramebuffer(); //больше не рисуем в наш fbo
Java:
private static Framebuffer framebuffer;
private static Minecraft mc = Minecraft.getMinecraft();
@SideOnly(Side.CLIENT)
@SubscribeEvent
public void fboTest(RenderWorldLastEvent event) {
if (framebuffer == null)
framebuffer = new Framebuffer(mc.displayWidth, mc.displayHeight, false);
framebuffer.bindFramebuffer(false);
GL11.glBindTexture(GL_TEXTURE_2D, 0);
GL11.glColor4f(0f, 1f, 0f, 1f);
Tessellator tessellator = Tessellator.getInstance();
BufferBuilder buffer = tessellator.getBuffer();
buffer.begin(7, DefaultVertexFormats.POSITION_TEX);
buffer.pos(-1, -1, 0).tex(0, 0).endVertex();
buffer.pos(1, -1, 0).tex(1, 0).endVertex();
buffer.pos(1, 1, 0).tex(1, 1).endVertex();
buffer.pos(-1, 1, 0).tex(0, 1).endVertex();
tessellator.draw();
framebuffer.unbindFramebuffer();
}
...
Ничего не нарисовалось
Дело в том что мы рисовали не на экран, а в буфер кадра.
Магическим способом посмотрим содержимое того буфера кадра:
То что мы заказывали, квадрат 2x2 зеленого цвета, находится на блок ниже игрока(трансформации в связи с использованием
RenderWorldLastEvent
)Но смотреть на буферы по отдельности занятие не из приятных, да и магией мы не владеем.
Поэтому вовместим, наложим, 2 изображения друг на друга.
Для этого нам нужно понять суровую правду: все объекты, чанки, энтити в майнкрафте на самом деле тоже рендерятся в framebuffer Правда этот же фреймбуфер сразу же рендерится на дисплей. Это просто некая обертка, нужная для некоторых спецэффектов.
Значит, чтобы совместить картинки буферов, надо получить нарисованную в текстурку сцену нашего фреймбуфера, и с этой текстуркой что-нибудь нарисовать. Вперед!
Java:
mc.getFramebuffer().bindFramebuffer(false); // переключим фбо на стандартный
//вообще он по дефолту включен, но мы только что включали-выключали
//свой буфер, поэтому без включения майновского фбо, мы бы рисовали
//напрямик на дисплей, нам это не нужно
GL11.glBindTexture(GL11.GL_TEXTURE_2D, framebuffer.framebufferTexture); // достаем текстурку нашей сцены
//Рисуем...
Мы нарисуем квадрат размером с окно майнкрафта, да так, что его не отличить будет от обычного мира
Java:
GL11.glPushMatrix(); //сохраняем матрицу
//далее нам надо очистить все трансформации
//чтобы наш квадрат был ровно под размер экрана
GL11.glMatrixMode(GL_PROJECTION); //матрица проекций
GL11.glLoadIdentity(); //очистить!
GL11.glMatrixMode(GL_MODELVIEW); //матрица трансформаций
GL11.glLoadIdentity(); //очистить!
drawQuad(); //нарисовать квадрат
GL11.glPopMatrix();
Java:
private static void drawQuad(){
Tessellator tessellator = Tessellator.getInstance();
BufferBuilder buffer = tessellator.getBuffer();
buffer.begin(7, DefaultVertexFormats.POSITION_TEX);
buffer.pos(-1, -1, 0).tex(0, 0).endVertex();
buffer.pos(1, -1, 0).tex(1, 0).endVertex();
buffer.pos(1, 1, 0).tex(1, 1).endVertex();
buffer.pos(-1, 1, 0).tex(0, 1).endVertex();
tessellator.draw();
}
Ура, квадрат рендерится во фреймбуфер, а он в свою очередь рендерится в майнкрафтовский буфер, который рендерится на дисплей.
Но погодите-ка, при движении камеры, старые пиксели не стираются:
В самом деле, в конце(или начале) каждого кадра фреймбуфер надо стрирать, дабы все пиксели из него удалились, и в следущем кадре мы их не видели.
Добавим в конец эвента
Java:
framebuffer.framebufferClear();
mc.getFramebuffer().bindFramebuffer(false);
Java:
if (mc.displayWidth != framebuffer.framebufferWidth
|| mc.displayHeight != framebuffer.framebufferHeight)
framebuffer.createBindFramebuffer(mc.displayWidth, mc.displayHeight);
Теперь наш квадрат в своем fbo ренедриттся так же как и обычный, на первый взгляд...
Java:
private static Framebuffer framebuffer;
private static Minecraft mc = Minecraft.getMinecraft();
@SubscribeEvent
public void fboTest(RenderWorldLastEvent event) {
int current = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); // в 1.12 нужно сохранять текстур атлас
if (framebuffer == null)
framebuffer = new Framebuffer(mc.displayWidth, mc.displayHeight, false);
if (mc.displayWidth != framebuffer.framebufferWidth
|| mc.displayHeight != framebuffer.framebufferHeight)
framebuffer.createBindFramebuffer(mc.displayWidth, mc.displayHeight);
framebuffer.bindFramebuffer(false);
GL11.glBindTexture(GL_TEXTURE_2D, 0);
GL11.glColor4f(0f, 1f,0f,1f);
drawQuad();
mc.getFramebuffer().bindFramebuffer(false);
GL11.glPushMatrix();
GL11.glBindTexture(GL11.GL_TEXTURE_2D, framebuffer.framebufferTexture);
GL11.glMatrixMode(GL_PROJECTION);
GL11.glLoadIdentity();
GL11.glMatrixMode(GL_MODELVIEW);
GL11.glLoadIdentity();
drawQuad();
GL11.glPopMatrix();
framebuffer.framebufferClear();
mc.getFramebuffer().bindFramebuffer(false);
GL11.glBindTexture(GL_TEXTURE_2D, current);
}
private static void drawQuad(){
Tessellator tessellator = Tessellator.getInstance();
BufferBuilder buffer = tessellator.getBuffer();
buffer.begin(7, DefaultVertexFormats.POSITION_TEX);
buffer.pos(-1, -1, 0).tex(0, 0).endVertex();
buffer.pos(1, -1, 0).tex(1, 0).endVertex();
buffer.pos(1, 1, 0).tex(1, 1).endVertex();
buffer.pos(-1, 1, 0).tex(0, 1).endVertex();
tessellator.draw();
}
Теперь сделаем что-нибудь осмысленное. Например 'портал в другое измерение'.
Для этого просто отодвиним нарисованный нами ранее квадрат от игрока, в мир, скажем на координаты 0, 3, 0.
Java:
//рисуем когда наш fbo забинжен
GL11.glPushMatrix();
GL11.glBindTexture(GL_TEXTURE_2D, 0);
GL11.glColor4f(0f, 0f, 1f, 1f);
//сдвиг в мир
GL11.glTranslated(-Particle.interpPosX, -Particle.interpPosY, -Particle.interpPosZ);
GL11.glTranslatef(0, 3, 0);
drawQuad();
GL11.glPopMatrix();
Неплохо, посмотрим на сие чудо издалека
Что случилось? почему наш портал просвечивает сквозь все?
Дело в том что мы рисовали объект во фреймбуфер, который ничего не знает о том что творится в основном буфере, не знает какой там буфер глубины.
А посему рисует изображение прямо сверху.
Исправим это. Сделать это довольно просто, для начала, просто скажем нашему фреймбуферу, чтобы он работал с глубиной, для этого вернемся к коду инициализации fbo и заменим последний аргумент на true:
Java:
if (framebuffer == null)
framebuffer = new Framebuffer(mc.displayWidth, mc.displayHeight, true);
Делается это так
Java:
//Пишем этот код после бинда своего буфера, но до рисования в него
//биндим майновский буфер как читаемый, мы будем из него читать данные о глубине
glBindFramebuffer(GL_READ_FRAMEBUFFER, mc.getFramebuffer().framebufferObject);
glBlitFramebuffer(0, 0, // копируем пиксели из области (0, 0, width, height)
mc.displayWidth, mc.displayHeight,
0, 0, // копируем пиксели в область (0, 0, width, height)
mc.displayWidth, mc.displayHeight,
GL_DEPTH_BUFFER_BIT, // копируем пиксели глубины
GL_NEAREST);
Java:
//после трансформации координат мира
GL11.glTranslatef(0, 0, 0.01f);
Java:
private static Framebuffer framebuffer;
private static Minecraft mc = Minecraft.getMinecraft();
@SubscribeEvent
public void fboTest(RenderWorldLastEvent event) {
int current = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
if (framebuffer == null)
framebuffer = new Framebuffer(mc.displayWidth, mc.displayHeight, true);
if (mc.displayWidth != framebuffer.framebufferWidth
|| mc.displayHeight != framebuffer.framebufferHeight)
framebuffer.createBindFramebuffer(mc.displayWidth, mc.displayHeight);
framebuffer.bindFramebuffer(false);
glBindFramebuffer(GL_READ_FRAMEBUFFER, mc.getFramebuffer().framebufferObject);
glBlitFramebuffer(0, 0,
mc.displayWidth, mc.displayHeight,
0, 0,
mc.displayWidth, mc.displayHeight,
GL_DEPTH_BUFFER_BIT,
GL_NEAREST);
GL11.glPushMatrix();
GL11.glBindTexture(GL_TEXTURE_2D, 0);
GL11.glColor4f(0f, 0f,1f,1f);
GL11.glTranslated(-Particle.interpPosX, -Particle.interpPosY, -Particle.interpPosZ);
GL11.glTranslatef(0, 3, 0);
GL11.glTranslatef(0, 0, 0.01f);
drawQuad();
GL11.glPopMatrix();
mc.getFramebuffer().bindFramebuffer(false);
GL11.glPushMatrix();
GL11.glBindTexture(GL11.GL_TEXTURE_2D, framebuffer.framebufferTexture);
GL11.glMatrixMode(GL_PROJECTION);
GL11.glLoadIdentity();
GL11.glMatrixMode(GL_MODELVIEW);
GL11.glLoadIdentity();
drawQuad();
GL11.glPopMatrix();
framebuffer.framebufferClear();
mc.getFramebuffer().bindFramebuffer(false);
glBindTexture(GL_TEXTURE_2D, current);
}
private static void drawQuad(){
Tessellator tessellator = Tessellator.getInstance();
BufferBuilder buffer = tessellator.getBuffer();
buffer.begin(7, DefaultVertexFormats.POSITION_TEX);
buffer.pos(-1, -1, 0).tex(0, 0).endVertex();
buffer.pos(1, -1, 0).tex(1, 0).endVertex();
buffer.pos(1, 1, 0).tex(1, 1).endVertex();
buffer.pos(-1, 1, 0).tex(0, 1).endVertex();
tessellator.draw();
}
Отлично, теперь мы можем рендерить объекты в нашем fbo, как будто они рендерятся без него.
Но... Подождите, какой вообще в этом смысл?
Дело в том, что каждый уважающий себя портал обладает аурой, а у нашего портала ее нет. В ее недостатке и теряется смысл.
Так сделаем же ауру!
Для этого нам понадобятся шейдеры
Шейдеры - небольшие программки(в нашем случае на языке glsl), выполняемые на GPU, которые обрабатывают входные данные, преобразовывая их в выходные данные.
Шейдеры бывают вершинные, фрагментные, геометричные, тесселяторные, вычислительные.
Самые основные задачи которые они выполняют:
Создадим класс шейдерной программы
Шейдеры бывают вершинные, фрагментные, геометричные, тесселяторные, вычислительные.
Самые основные задачи которые они выполняют:
- вершинные - преобразовывание позиции вершины модели в позиции вершины на экране, применяя различные трансформации
- фрагментные - преобразование цвета картинки в цвета пикселя на экране, применяя освещение и другие фильтры
Создадим класс шейдерной программы
Java:
import net.minecraft.client.Minecraft;
import net.minecraft.util.ResourceLocation;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@SuppressWarnings("WeakerAccess") //idea only
public abstract class ShaderProgram {
private int programId;
private int vertexShaderID = -1;
private int fragmentShaderID = -1;
protected static InputStream getStream(ResourceLocation location) {
try {
return Minecraft.getMinecraft().getResourceManager().getResource(location).getInputStream();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
public ShaderProgram(InputStream vertexShaderSource, InputStream fragmentShaderSource) {
if (vertexShaderSource != null)
vertexShaderID = loadShader(vertexShaderSource, GL20.GL_VERTEX_SHADER);
if (fragmentShaderSource != null)
fragmentShaderID = loadShader(fragmentShaderSource, GL20.GL_FRAGMENT_SHADER);
programId = GL20.glCreateProgram();
if (hasVertexShader())
GL20.glAttachShader(programId, vertexShaderID);
if (hasFragmentShader())
GL20.glAttachShader(programId, fragmentShaderID);
GL20.glLinkProgram(programId);
GL20.glValidateProgram(programId);
cleanShader();
}
private static int loadShader(InputStream file, int type) {
StringBuilder stringBuilder = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(file))) {
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line).append("\n");
}
} catch (Exception e) {
e.printStackTrace();
}
int shaderId = GL20.glCreateShader(type);
GL20.glShaderSource(shaderId, stringBuilder);
GL20.glCompileShader(shaderId);
if (GL20.glGetShaderi(shaderId, GL20.GL_COMPILE_STATUS) == GL11.GL_FALSE) {
System.err.print("Could not compile " + (type == GL20.GL_FRAGMENT_SHADER ? "fragment" : "vertex") + " shader.");
System.err.println();
System.err.print(new Throwable().getStackTrace()[2].getClassName());
System.err.println();
System.err.print(GL20.glGetShaderInfoLog(shaderId, 2000));
System.err.println();
} else {
try {
System.out.print("Compiled " + Class.forName(new Throwable().getStackTrace()[2].getClassName()).getSimpleName() + "(" + (type == GL20.GL_FRAGMENT_SHADER ? "fragment" : "vertex") + ")");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
String str = GL20.glGetShaderInfoLog(shaderId, 2000);
if (!str.trim().isEmpty()) {
System.out.println();
System.out.print(str);
}
System.out.println();
}
return shaderId;
}
public boolean hasVertexShader() {
return vertexShaderID != -1;
}
public boolean hasFragmentShader() {
return fragmentShaderID != -1;
}
public void start() {
GL20.glUseProgram(programId);
}
public void stop() {
GL20.glUseProgram(0);
}
public void cleanShader() {
if (hasVertexShader()) {
GL20.glDetachShader(programId, vertexShaderID);
GL20.glDeleteShader(vertexShaderID);
}
if (hasFragmentShader()) {
GL20.glDetachShader(programId, fragmentShaderID);
GL20.glDeleteShader(fragmentShaderID);
}
}
public int pid() {
return programId;
}
}
Примечание - хороший блюр можно сделать и без использования текстурки заднего фона, но дабы картина смотрелась чуть эффектнее, да и продемострировать такую возможность, я беру из майнкрафтовского fbo фон и клею его по третьему текстурному юниту к своему шейдеру, после чего делаю в шейдере делаю плавный переход от цвета заблюренного портала к цвету этого бэкграунда.
Java:
public class ShaderProgramGaussianBlur extends ShaderProgram {
private static ResourceLocation fragmentShader =
new ResourceLocation(yourmodidhere, "shaders/program/fragment/gaussianblur.frag");
ShaderProgramGaussianBlur() {
super(null, getStream(fragmentShader));
}
public void horizontal(){
GL20.glUniform1i(GL20.glGetUniformLocation(pid(), "horizontal"), 1);
}
public void vertical(){
GL20.glUniform1i(GL20.glGetUniformLocation(pid(), "horizontal"), 0);
}
public void textureUnits(){
GL20.glUniform1i(GL20.glGetUniformLocation(pid(), "image"), 0);
GL20.glUniform1i(GL20.glGetUniformLocation(pid(), "imageBack"), 3);
}
public void loadDisplaySize(){
Minecraft mc = Minecraft.getMinecraft();
GL20.glUniform1f(GL20.glGetUniformLocation(pid(), "width"), mc.displayWidth);
GL20.glUniform1f(GL20.glGetUniformLocation(pid(), "height"), mc.displayHeight);
}
}
Java:
public class ShaderRegister {
public static ShaderProgramGaussianBlur BLUR;
public static void registerPreInit(){
BLUR = new ShaderProgramGaussianBlur();
}
}
C-like:
#version 130
out vec4 out_Color;
uniform sampler2D image;
uniform sampler2D imageBack;
uniform bool horizontal;
uniform float width;
uniform float height;
//gaussian kernel
const float weight[41] = float[] (
0.005633, 0.006845, 0.008235, 0.009808, 0.011566,
0.013504, 0.015609, 0.017863, 0.020239, 0.022704,
0.025215, 0.027726, 0.030183, 0.032532, 0.034715,
0.036676, 0.038363, 0.039728, 0.040733, 0.041348,
0.041555,
0.041348, 0.040733, 0.039728, 0.038363, 0.036676,
0.034715, 0.032532, 0.030183, 0.027726, 0.025215,
0.022704, 0.020239, 0.017863, 0.015609, 0.013504,
0.011566, 0.009808, 0.008235, 0.006845, 0.005633
);
void main()
{
vec2 tex_offset = 1.0 / vec2(width, height);
vec4 result = texture(image, gl_TexCoord[0].xy) * weight[20];
vec2 texPos = horizontal ? vec2(tex_offset.x, 0.0) : vec2(0.0, tex_offset.y);
for (int i = -20; i < 21; ++i) {
vec2 actualPos = gl_TexCoord[0].xy + texPos * i;
vec4 up = texture(image, actualPos);
vec4 upBack = texture(imageBack, actualPos);
vec4 mixUp = vec4(mix(upBack.rgb, up.rgb, up.a), up.a);
result += mixUp * weight[20 + i];
}
out_Color = result;
}
Накладывание эффекта(блюра) на объекты происходит так:
Рендерим объекты в framebuffer, достаем текстурку этого буфер, накладываем ее на прямоугольник размером с экран(это мы уже сделали выше), применяем на этот прямоугольник шейдеры.
Теперь, имея все необходимые ингредиенты и знания, приступим к реализации.
Начнем с приготовлений(которые происходят после рендера всех объектов в наш буфер):
Java:
GL11.glColor4f(1f, 1f, 1f, 1f); //вернем цвет в изначальное состояние
//включим смешивание, дабы наш экранный прямоугольник хорошо
//накладывался на изображение самой игры(майнкрафтовского фбо)
GL11.glEnable(GL11.GL_BLEND);
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
//кладем текущую текстуру майновского буфера в юнит 3
GL13.glActiveTexture(GL13.GL_TEXTURE3);
GL11.glBindTexture(GL_TEXTURE_2D, mc.getFramebuffer().framebufferTexture);
GL13.glActiveTexture(GL13.GL_TEXTURE0); // обратно в юнит 0,
//здесь будет хранится текстурка нашего фбо
Java:
ShaderRegister.BLUR.start(); //включаем шейдер
//загружаем соответствие между sampler'ми и текстурными юнитами
ShaderRegister.BLUR.textureUnits();
ShaderRegister.BLUR.loadDisplaySize(); //загружаем размер дисплея
ShaderRegister.BLUR.horizontal(); //горизонатальный блюр
И конечно после рендера выключаем шейдер
Java:
ShaderRegister.BLUR.stop();
Java:
@SubscribeEvent
public void fboTest(RenderWorldLastEvent event) {
int current = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
if (framebuffer == null)
framebuffer = new Framebuffer(mc.displayWidth, mc.displayHeight, true);
if (mc.displayWidth != framebuffer.framebufferWidth
|| mc.displayHeight != framebuffer.framebufferHeight)
framebuffer.createBindFramebuffer(mc.displayWidth, mc.displayHeight);
framebuffer.bindFramebuffer(false);
GL30.glBindFramebuffer(GL_READ_FRAMEBUFFER, mc.getFramebuffer().framebufferObject);
GL30.glBlitFramebuffer(0, 0,
mc.displayWidth, mc.displayHeight,
0, 0,
mc.displayWidth, mc.displayHeight,
GL_DEPTH_BUFFER_BIT,
GL_NEAREST);
GL11.glPushMatrix();
GL11.glBindTexture(GL_TEXTURE_2D, 0);
GL11.glColor4f(0f, 0f,1f,1f);
GL11.glTranslated(-Particle.interpPosX, -Particle.interpPosY, -Particle.interpPosZ);
GL11.glTranslatef(0, 3, 0);
GL11.glTranslatef(0, 0, 0.01f);
drawQuad();
GL11.glPopMatrix();
GL11.glColor4f(1f, 1f, 1f,1f);
GL11.glEnable(GL11.GL_BLEND);
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
GL13.glActiveTexture(GL13.GL_TEXTURE3);
GL11.glBindTexture(GL_TEXTURE_2D, mc.getFramebuffer().framebufferTexture);
GL13.glActiveTexture(GL13.GL_TEXTURE0);
ShaderRegister.BLUR.start();
ShaderRegister.BLUR.textureUnits();
ShaderRegister.BLUR.loadDisplaySize();
ShaderRegister.BLUR.horizontal();
mc.getFramebuffer().bindFramebuffer(false);
GL11.glPushMatrix();
GL11.glBindTexture(GL11.GL_TEXTURE_2D, framebuffer.framebufferTexture);
GL11.glMatrixMode(GL_PROJECTION);
GL11.glLoadIdentity();
GL11.glMatrixMode(GL_MODELVIEW);
GL11.glLoadIdentity();
drawQuad();
GL11.glPopMatrix();
ShaderRegister.BLUR.stop();
framebuffer.framebufferClear();
mc.getFramebuffer().bindFramebuffer(false);
glBindTexture(GL_TEXTURE_2D, current);
}
Впечатляюще.
Но мы заблюрили наши объекты только по горизонтали, до полного счастья не хватает вертикального блюра.
Тут не все так просто, ибо гауссовский блюр(по двум осям) это такой эффект который недостижим(или очень затратен) в один проход шейдера. Но мы просто не можем взять и применить к одному рендерюжемуя объекту две шейдерных программы.
И тут нас опять выручают фреймбуферы. Как мы сделали это только что, мы можем изображение из нашего заблюренного буфера рендрить не напрямую на дисплей, а еще в один отдельный буфер, и как раз он уже будет финальным изображением сцены.
Звучит сложно, но это только на первый взгляд, реализуем же.
Java:
private static Framebuffer framebufferSub;
Java:
if (framebufferSub == null)
framebufferSub = new Framebuffer(mc.displayWidth, mc.displayHeight, false);
if (mc.displayWidth != framebufferSub.framebufferWidth
|| mc.displayHeight != framebufferSub.framebufferHeight)
framebufferSub.createBindFramebuffer(mc.displayWidth, mc.displayHeight);
Java:
framebufferSub.bindFramebuffer(false);
GL11.glPushMatrix();
GL11.glBindTexture(GL11.GL_TEXTURE_2D, framebuffer.framebufferTexture);
GL11.glMatrixMode(GL_PROJECTION);
GL11.glLoadIdentity();
GL11.glMatrixMode(GL_MODELVIEW);
GL11.glLoadIdentity();
drawQuad();
GL11.glPopMatrix();
Java:
ShaderRegister.BLUR.vertical();
Java:
mc.getFramebuffer().bindFramebuffer(false);
GL11.glPushMatrix();
GL11.glBindTexture(GL11.GL_TEXTURE_2D, framebufferSub.framebufferTexture); //рисуем наш новый буфер
GL11.glMatrixMode(GL_PROJECTION);
GL11.glLoadIdentity();
GL11.glMatrixMode(GL_MODELVIEW);
GL11.glLoadIdentity();
drawQuad();
GL11.glPopMatrix();
Java:
framebufferSub.framebufferClear();
Java:
private static int mcCopiedTexture = -1;
@SubscribeEvent
public void fboTest(RenderWorldLastEvent event) {
int current = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
if(mcCopiedTexture == -1) {
//не на всех видеокартах запись и чтение пикселей в фбо одновременно,
//работает корректно, поэтому требуется скопировать текстурку fbo майна
mcCopiedTexture = glGenTextures();
glBindTexture(GL11.GL_TEXTURE_2D, mcCopiedTexture);
GL13.glActiveTexture(GL13.GL_TEXTURE0);
glTexParameteri(GL11.GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL11.GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL11.GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE);
glTexParameteri(GL11.GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE);
}
GL11.glBindTexture(GL_TEXTURE_2D, mcCopiedTexture);
glCopyTexImage2D(GL_TEXTURE_2D,
0, GL_RGBA, 0, 0,
mc.getFramebuffer().framebufferTextureWidth,
mc.getFramebuffer().framebufferTextureHeight, 0);
if (framebuffer == null)
framebuffer = new Framebuffer(mc.displayWidth, mc.displayHeight, true);
if (mc.displayWidth != framebuffer.framebufferWidth
|| mc.displayHeight != framebuffer.framebufferHeight)
framebuffer.createBindFramebuffer(mc.displayWidth, mc.displayHeight);
if (framebufferSub == null)
framebufferSub = new Framebuffer(mc.displayWidth, mc.displayHeight, false);
if (mc.displayWidth != framebufferSub.framebufferWidth
|| mc.displayHeight != framebufferSub.framebufferHeight)
framebufferSub.createBindFramebuffer(mc.displayWidth, mc.displayHeight);
framebuffer.bindFramebuffer(false);
GL30.glBindFramebuffer(GL_READ_FRAMEBUFFER, mc.getFramebuffer().framebufferObject);
GL30.glBlitFramebuffer(0, 0,
mc.displayWidth, mc.displayHeight,
0, 0,
mc.displayWidth, mc.displayHeight,
GL_DEPTH_BUFFER_BIT,
GL_NEAREST);
GL11.glPushMatrix();
GL11.glBindTexture(GL_TEXTURE_2D, 0);
GL11.glColor4f(0f, 0f,1f,1f);
GL11.glTranslated(-Particle.interpPosX, -Particle.interpPosY, -Particle.interpPosZ);
GL11.glTranslatef(0, 3, 0);
GL11.glTranslatef(0, 0, 0.01f);
drawQuad();
GL11.glPopMatrix();
GL11.glColor4f(1f, 1f, 1f,1f);
GL11.glEnable(GL11.GL_BLEND);
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
GL13.glActiveTexture(GL13.GL_TEXTURE3);
GL11.glBindTexture(GL_TEXTURE_2D, mcCopiedTexture);
GL13.glActiveTexture(GL13.GL_TEXTURE0);
ShaderRegister.BLUR.start();
ShaderRegister.BLUR.textureUnits();
ShaderRegister.BLUR.loadDisplaySize();
ShaderRegister.BLUR.horizontal();
framebufferSub.bindFramebuffer(false);
GL11.glPushMatrix();
GL11.glBindTexture(GL11.GL_TEXTURE_2D, framebuffer.framebufferTexture);
GL11.glMatrixMode(GL_PROJECTION);
GL11.glLoadIdentity();
GL11.glMatrixMode(GL_MODELVIEW);
GL11.glLoadIdentity();
drawQuad();
GL11.glPopMatrix();
ShaderRegister.BLUR.vertical();
mc.getFramebuffer().bindFramebuffer(false);
GL11.glPushMatrix();
GL11.glBindTexture(GL11.GL_TEXTURE_2D, framebufferSub.framebufferTexture);
GL11.glMatrixMode(GL_PROJECTION);
GL11.glLoadIdentity();
GL11.glMatrixMode(GL_MODELVIEW);
GL11.glLoadIdentity();
drawQuad();
GL11.glPopMatrix();
ShaderRegister.BLUR.stop();
framebuffer.framebufferClear();
framebufferSub.framebufferClear();
mc.getFramebuffer().bindFramebuffer(false);
GL11.glBindTexture(GL_TEXTURE_2D, current);
}
Теперь на все объекты, нарисованные внутри нашего буфера, будет накладываться эффект блюра. Разве не прекрасно?
На этом все.
В заключение хочу сказать что буфер кадра очень полезная и не особо сложная вещь, поторая позволяет не только применять какие-то спецэффекты к изображению, но и завертывать целые сцены в небольшие объекты.