Framebuffer: Начало

Framebuffer: Начало

Версия(и) Minecraft
1.0+
Эта статья посвящена буферам кадра(framebuffers), как их использовать, зачем нужны, и какие в майнкрафте есть средства для удобной работы с ними.

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


Итак, что же все такие такое фреймбуфер?
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();
}
Запускаем игру и видим...
1565006648464.png

...
Ничего не нарисовалось

Дело в том что мы рисовали не на экран, а в буфер кадра.
Магическим способом посмотрим содержимое того буфера кадра:
1565006662704.png

То что мы заказывали, квадрат 2x2 зеленого цвета, находится на блок ниже игрока(трансформации в связи с использованием RenderWorldLastEvent)

Но смотреть на буферы по отдельности занятие не из приятных, да и магией мы не владеем.
Поэтому вовместим, наложим, 2 изображения друг на друга.
Для этого нам нужно понять суровую правду: все объекты, чанки, энтити в майнкрафте на самом деле тоже рендерятся в framebuffer😱 Правда этот же фреймбуфер сразу же рендерится на дисплей. Это просто некая обертка, нужная для некоторых спецэффектов.

Значит, чтобы совместить картинки буферов, надо получить нарисованную в текстурку сцену нашего фреймбуфера, и с этой текстуркой что-нибудь нарисовать. Вперед!
Java:
mc.getFramebuffer().bindFramebuffer(false); // переключим фбо на стандартный
//вообще он по дефолту включен, но мы только что включали-выключали
//свой буфер, поэтому без включения майновского фбо, мы бы рисовали
//напрямик на дисплей, нам это не нужно

GL11.glBindTexture(GL11.GL_TEXTURE_2D, framebuffer.framebufferTexture); // достаем текстурку нашей сцены
//Рисуем...
Текстурку мы забиндили, но что дальше, что бы нам нарисовать чтобы увидеть сцену в нашем fbo?
Мы нарисуем квадрат размером с окно майнкрафта, да так, что его не отличить будет от обычного мира
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();
}
Запускаем...
1565006734893.png

Ура, квадрат рендерится во фреймбуфер, а он в свою очередь рендерится в майнкрафтовский буфер, который рендерится на дисплей.
Но погодите-ка, при движении камеры, старые пиксели не стираются:
1565006769344.png

В самом деле, в конце(или начале) каждого кадра фреймбуфер надо стрирать, дабы все пиксели из него удалились, и в следущем кадре мы их не видели.
Добавим в конец эвента
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();
1565006815343.png

Неплохо, посмотрим на сие чудо издалека
1565006829626.png

Что случилось? почему наш портал просвечивает сквозь все?
Дело в том что мы рисовали объект во фреймбуфер, который ничего не знает о том что творится в основном буфере, не знает какой там буфер глубины.
А посему рисует изображение прямо сверху.
Исправим это. Сделать это довольно просто, для начала, просто скажем нашему фреймбуферу, чтобы он работал с глубиной, для этого вернемся к коду инициализации fbo и заменим последний аргумент на true:
Java:
if (framebuffer == null)
    framebuffer = new Framebuffer(mc.displayWidth, mc.displayHeight, true);
Но этого как ни странно, недостаточно. Теперь наш fbo будет лишь стирать глубину. Для того что бы фреймбуфер мог работать с глубиной из майнкрафтовского буфера кадра, мы должны скопировать эту самую глубину.
Делается это так
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();
}
Запускаем...
1565006854998.png

Отлично, теперь мы можем рендерить объекты в нашем 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);
}
Запускаем...
1565009170913.png

Впечатляюще.
Но мы заблюрили наши объекты только по горизонтали, до полного счастья не хватает вертикального блюра.
Тут не все так просто, ибо гауссовский блюр(по двум осям) это такой эффект который недостижим(или очень затратен) в один проход шейдера. Но мы просто не можем взять и применить к одному рендерюжемуя объекту две шейдерных программы.
И тут нас опять выручают фреймбуферы. Как мы сделали это только что, мы можем изображение из нашего заблюренного буфера рендрить не напрямую на дисплей, а еще в один отдельный буфер, и как раз он уже будет финальным изображением сцены.
Звучит сложно, но это только на первый взгляд, реализуем же.
Java:
private static Framebuffer framebufferSub;
Инициализируем так же, только в этот раз глубина нам уже не нужна(в этом буфере будет рисовать всего 1 объект - экранный прямоугольник с шейдером, ничто его не заслонит)
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();
И конечно, каждый fbo надо очистить:
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);
}
Проверяем...
1565010952786.png

Теперь на все объекты, нарисованные внутри нашего буфера, будет накладываться эффект блюра. Разве не прекрасно?

На этом все.
В заключение хочу сказать что буфер кадра очень полезная и не особо сложная вещь, поторая позволяет не только применять какие-то спецэффекты к изображению, но и завертывать целые сцены в небольшие объекты.
Автор
implicit
Просмотры
391
Первый выпуск
Обновление
Оценка
4.67 звёзд 6 оценок

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

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

Название эпичное, ждем "Возрождение легенды")
4 звезды потому *мало* сказано о задачах, решаемых при помощи дополнительных рендер-таргетах
implicit
implicit
Какие там ещё задачи, кроме пост процессинга да заветрывания изображения сцены в текстурку?
Все отлчино, за исключением отсутствия класса ShaderRegister и репозитория с кодом. А так молодцом, так держать! Побольше бы туториалов в этом направлении.
implicit
implicit
ShaderRegister есть в спойлере с блюром. А что там репозиторий? Это всего 1 метод и пара полей, которые целиком есть пол последним спойлером с кодом
Всё понятно и по полочкам... Давно ждал этой статьи.
Гуднота, все по полочкам даже для самых маленьких
Неплохо, неплохо, хороший товар
Хороший тутор с примерами, кодом, комментариями! Ну как всегда, у тебя гайды выходят редко, но метко!
~~~
"Так он мне понравился, так он мне понравился, что я его наверное даже Самсунгу не отдам!"
Сверху