mirror of
https://github.com/MadeOfJelly/MushMachine.git
synced 2025-04-08 05:42:59 +02:00
new bloom render tasks, setup util and hdr bloom code example
This commit is contained in:
parent
8f44e09f32
commit
5e58a61c93
@ -179,6 +179,49 @@ target_link_libraries(blur_render_task
|
|||||||
engine
|
engine
|
||||||
)
|
)
|
||||||
|
|
||||||
|
############# bloom_extraction render task ###########
|
||||||
|
|
||||||
|
add_library(bloom_extraction_render_task
|
||||||
|
src/mm/opengl/render_tasks/bloom_extraction.hpp
|
||||||
|
src/mm/opengl/render_tasks/bloom_extraction.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(bloom_extraction_render_task PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||||
|
|
||||||
|
target_link_libraries(bloom_extraction_render_task
|
||||||
|
opengl_renderer_s
|
||||||
|
engine
|
||||||
|
)
|
||||||
|
|
||||||
|
############# bloom_combine render task ###########
|
||||||
|
|
||||||
|
add_library(bloom_combine_render_task
|
||||||
|
src/mm/opengl/render_tasks/bloom_combine.hpp
|
||||||
|
src/mm/opengl/render_tasks/bloom_combine.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(bloom_combine_render_task PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||||
|
|
||||||
|
target_link_libraries(bloom_combine_render_task
|
||||||
|
opengl_renderer_s
|
||||||
|
engine
|
||||||
|
)
|
||||||
|
|
||||||
|
############# composition render task ###########
|
||||||
|
# intendet for bloom compositing and tonemapping
|
||||||
|
|
||||||
|
add_library(composition_render_task
|
||||||
|
src/mm/opengl/render_tasks/composition.hpp
|
||||||
|
src/mm/opengl/render_tasks/composition.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(composition_render_task PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||||
|
|
||||||
|
target_link_libraries(composition_render_task
|
||||||
|
opengl_renderer_s
|
||||||
|
engine
|
||||||
|
)
|
||||||
|
|
||||||
############# tilemap renderer ###########
|
############# tilemap renderer ###########
|
||||||
|
|
||||||
add_library(tilemap_render_task
|
add_library(tilemap_render_task
|
||||||
@ -210,6 +253,21 @@ target_link_libraries(fast_sky_render_task
|
|||||||
engine
|
engine
|
||||||
)
|
)
|
||||||
|
|
||||||
|
############# bloom ###########
|
||||||
|
|
||||||
|
add_library(bloom
|
||||||
|
src/mm/opengl/bloom.hpp
|
||||||
|
src/mm/opengl/bloom.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(bloom PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||||
|
|
||||||
|
target_link_libraries(bloom
|
||||||
|
bloom_extraction_render_task
|
||||||
|
blur_render_task
|
||||||
|
bloom_combine_render_task
|
||||||
|
)
|
||||||
|
|
||||||
########################
|
########################
|
||||||
|
|
||||||
if (BUILD_TESTING)
|
if (BUILD_TESTING)
|
||||||
|
158
framework/opengl_renderer/src/mm/opengl/bloom.cpp
Normal file
158
framework/opengl_renderer/src/mm/opengl/bloom.cpp
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
#include "./bloom.hpp"
|
||||||
|
|
||||||
|
#include <mm/opengl/render_tasks/bloom_extraction.hpp>
|
||||||
|
#include <mm/opengl/render_tasks/blur.hpp>
|
||||||
|
#include <mm/opengl/render_tasks/bloom_combine.hpp>
|
||||||
|
|
||||||
|
#include <mm/opengl/fbo_builder.hpp>
|
||||||
|
#include <mm/opengl/texture_loader.hpp>
|
||||||
|
|
||||||
|
namespace MM::OpenGL {
|
||||||
|
|
||||||
|
using namespace entt::literals;
|
||||||
|
|
||||||
|
void setup_bloom(
|
||||||
|
MM::Engine& engine,
|
||||||
|
const std::string color_src_tex,
|
||||||
|
const size_t bloom_phases,
|
||||||
|
const float bloom_in_scale,
|
||||||
|
const float bloom_phase_scale
|
||||||
|
) {
|
||||||
|
assert(bloom_phases > 1);
|
||||||
|
auto& rs = engine.getService<MM::Services::OpenGLRenderer>();
|
||||||
|
auto& rm_t = MM::ResourceManager<MM::OpenGL::Texture>::ref();
|
||||||
|
auto [w, h] = engine.getService<MM::Services::SDLService>().getWindowSize();
|
||||||
|
|
||||||
|
#ifdef MM_OPENGL_3_GLES
|
||||||
|
#if 0 // NOPE!!
|
||||||
|
// TODO: caps at 1, invest in half float?
|
||||||
|
const auto bloom_internal_format = GL_RGB565; // prolly fine
|
||||||
|
const auto bloom_format_type = GL_UNSIGNED_BYTE;
|
||||||
|
#else
|
||||||
|
const auto bloom_internal_format = GL_RGB16F;
|
||||||
|
const auto bloom_format_type = GL_FLOAT;
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
const auto bloom_internal_format = GL_RGB16F;
|
||||||
|
const auto bloom_format_type = GL_FLOAT;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
{ // bloom in (bloom extraction)
|
||||||
|
rm_t.reload<MM::OpenGL::TextureLoaderEmpty>(
|
||||||
|
"bloom_in",
|
||||||
|
bloom_internal_format,
|
||||||
|
w * bloom_in_scale, h * bloom_in_scale,
|
||||||
|
GL_RGB, bloom_format_type
|
||||||
|
);
|
||||||
|
{ // filter
|
||||||
|
rm_t.get("bloom_in"_hs)->bind(0);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
rs.targets["bloom_extraction"] = MM::OpenGL::FBOBuilder::start()
|
||||||
|
.attachTexture(rm_t.get("bloom_in"_hs), GL_COLOR_ATTACHMENT0)
|
||||||
|
.setResizeFactors(bloom_in_scale, bloom_in_scale)
|
||||||
|
.setResize(true)
|
||||||
|
.finish();
|
||||||
|
assert(rs.targets["bloom_extraction"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// blur textures and fbos
|
||||||
|
for (size_t i = 1; i <= bloom_phases; i++) {
|
||||||
|
// TODO: further dedup
|
||||||
|
std::string tex_out_name {"blur_out" + std::to_string(i)};
|
||||||
|
auto tex_out_id = entt::hashed_string::value(tex_out_name.c_str());
|
||||||
|
rm_t.reload<MM::OpenGL::TextureLoaderEmpty>(
|
||||||
|
tex_out_id,
|
||||||
|
bloom_internal_format,
|
||||||
|
w * bloom_in_scale * glm::pow(bloom_phase_scale, i), h * bloom_in_scale * glm::pow(bloom_phase_scale, i),
|
||||||
|
GL_RGB, bloom_format_type
|
||||||
|
);
|
||||||
|
{ // filter
|
||||||
|
rm_t.get(tex_out_id)->bind(0);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string tex_tmp_name {"blur_tmp" + std::to_string(i)};
|
||||||
|
auto tex_tmp_id = entt::hashed_string::value(tex_tmp_name.c_str());
|
||||||
|
rm_t.reload<MM::OpenGL::TextureLoaderEmpty>(
|
||||||
|
tex_tmp_id,
|
||||||
|
bloom_internal_format,
|
||||||
|
w * bloom_in_scale * glm::pow(bloom_phase_scale, i), h * bloom_in_scale * glm::pow(bloom_phase_scale, i),
|
||||||
|
GL_RGB, bloom_format_type
|
||||||
|
);
|
||||||
|
{ // filter
|
||||||
|
rm_t.get(tex_tmp_id)->bind(0);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
rs.targets[tex_out_name] = MM::OpenGL::FBOBuilder::start()
|
||||||
|
.attachTexture(rm_t.get(tex_out_id), GL_COLOR_ATTACHMENT0)
|
||||||
|
.setResizeFactors(bloom_in_scale * glm::pow(bloom_phase_scale, i), bloom_in_scale * glm::pow(bloom_phase_scale, i))
|
||||||
|
.setResize(true)
|
||||||
|
.finish();
|
||||||
|
assert(rs.targets[tex_out_name]);
|
||||||
|
|
||||||
|
rs.targets[tex_tmp_name] = MM::OpenGL::FBOBuilder::start()
|
||||||
|
.attachTexture(rm_t.get(tex_tmp_id), GL_COLOR_ATTACHMENT0)
|
||||||
|
.setResizeFactors(bloom_in_scale * glm::pow(bloom_phase_scale, i), bloom_in_scale * glm::pow(bloom_phase_scale, i))
|
||||||
|
.setResize(true)
|
||||||
|
.finish();
|
||||||
|
assert(rs.targets[tex_tmp_name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // render tasks
|
||||||
|
auto& extraction = rs.addRenderTask<MM::OpenGL::RenderTasks::BloomExtraction>(engine);
|
||||||
|
extraction.src_tex = color_src_tex;
|
||||||
|
extraction.target_fbo = "bloom_extraction";
|
||||||
|
|
||||||
|
const glm::vec2 blur_factor {1.f, 1.f};
|
||||||
|
|
||||||
|
{ // blur rt
|
||||||
|
std::string prev_out_tex = "bloom_in";
|
||||||
|
for (size_t i = 1; i <= bloom_phases; i++) {
|
||||||
|
auto& blur = rs.addRenderTask<MM::OpenGL::RenderTasks::Blur>(engine);
|
||||||
|
// h
|
||||||
|
blur.in_tex = prev_out_tex;
|
||||||
|
blur.temp_fbo = "blur_tmp" + std::to_string(i);
|
||||||
|
// v
|
||||||
|
blur.temp_tex = "blur_tmp" + std::to_string(i);
|
||||||
|
blur.out_fbo = "blur_out" + std::to_string(i);
|
||||||
|
blur.out_tex = "blur_out" + std::to_string(i);
|
||||||
|
blur.tex_offset_factor = blur_factor * glm::vec2{2.f, 1.f}; // the input texture is double the size
|
||||||
|
|
||||||
|
// old blur:
|
||||||
|
//blur.tex_offset = {1.f/(w * bloom_in_scale * bloom_in_scale), 1.f/(h * bloom_in_scale * bloom_in_scale)};
|
||||||
|
|
||||||
|
prev_out_tex = blur.out_tex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// combine passes
|
||||||
|
for (size_t i = bloom_phases; i > 1; i--) {
|
||||||
|
auto& combine = rs.addRenderTask<MM::OpenGL::RenderTasks::BloomCombine>(engine);
|
||||||
|
if (i == bloom_phases) {
|
||||||
|
combine.tex0 = "blur_out" + std::to_string(i);
|
||||||
|
} else {
|
||||||
|
combine.tex0 = "blur_tmp" + std::to_string(i);
|
||||||
|
}
|
||||||
|
combine.tex1 = "blur_out" + std::to_string(i-1);
|
||||||
|
combine.target_fbo = "blur_tmp" + std::to_string(i-1); // -> blur_tmpi-1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // MM::OpenGL
|
||||||
|
|
27
framework/opengl_renderer/src/mm/opengl/bloom.hpp
Normal file
27
framework/opengl_renderer/src/mm/opengl/bloom.hpp
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "./render_task.hpp"
|
||||||
|
|
||||||
|
namespace MM::OpenGL {
|
||||||
|
|
||||||
|
// helper function to setup a bloom blur and recombine chain
|
||||||
|
// creates (texture) rendertarget, fbos and rendertasks
|
||||||
|
// outputs blur to "blur_tmp1" texture
|
||||||
|
//
|
||||||
|
// you still need to add the Composition rendertask (or eqv) your self, eg:
|
||||||
|
// auto& comp = rs.addRenderTask<MM::OpenGL::RenderTasks::Composition>(engine);
|
||||||
|
// comp.color_tex = "hdr_color";
|
||||||
|
// comp.bloom_tex = "blur_tmp1";
|
||||||
|
// comp.target_fbo = "display";
|
||||||
|
void setup_bloom(
|
||||||
|
MM::Engine& engine,
|
||||||
|
const std::string color_src_tex = "hdr_color", // texture to extract color from
|
||||||
|
const size_t bloom_phases = 5, // number of downsampled blurs (4 prob fine for 720, 5 for 1080)
|
||||||
|
const float bloom_in_scale = 0.5f, // scale of bloom extraction layer (1 - 0.5 best, lower for more perf)
|
||||||
|
const float bloom_phase_scale = 0.5f // ammount of scaling per downsampling
|
||||||
|
);
|
||||||
|
|
||||||
|
} // MM::OpenGL
|
||||||
|
|
@ -30,5 +30,5 @@ namespace MM::OpenGL {
|
|||||||
//virtual void reload(void) {} // TODO: remove
|
//virtual void reload(void) {} // TODO: remove
|
||||||
//virtual std::vector<const char*> getShaderPaths(void) {return {};} // TODO: remove
|
//virtual std::vector<const char*> getShaderPaths(void) {return {};} // TODO: remove
|
||||||
};
|
};
|
||||||
}
|
} // MM:OpenGL
|
||||||
|
|
||||||
|
@ -0,0 +1,161 @@
|
|||||||
|
#include "./bloom_combine.hpp"
|
||||||
|
|
||||||
|
#include <mm/fs_const_archiver.hpp>
|
||||||
|
#include <mm/services/opengl_renderer.hpp>
|
||||||
|
#include <mm/opengl/texture.hpp>
|
||||||
|
|
||||||
|
#include <tracy/Tracy.hpp>
|
||||||
|
|
||||||
|
namespace MM::OpenGL::RenderTasks {
|
||||||
|
|
||||||
|
BloomCombine::BloomCombine(Engine& engine) {
|
||||||
|
float vertices[] = {
|
||||||
|
-1.f, 1.f,
|
||||||
|
-1.f, -1.f,
|
||||||
|
1.f, -1.f,
|
||||||
|
1.f, -1.f,
|
||||||
|
1.f, 1.f,
|
||||||
|
-1.f, 1.f,
|
||||||
|
};
|
||||||
|
|
||||||
|
_vertexBuffer = std::make_unique<Buffer>(vertices, 2 * 6 * sizeof(float), GL_STATIC_DRAW);
|
||||||
|
_vao = std::make_unique<VertexArrayObject>();
|
||||||
|
_vao->bind();
|
||||||
|
_vertexBuffer->bind(GL_ARRAY_BUFFER);
|
||||||
|
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), 0);
|
||||||
|
|
||||||
|
_vertexBuffer->unbind(GL_ARRAY_BUFFER);
|
||||||
|
_vao->unbind();
|
||||||
|
|
||||||
|
setupShaderFiles();
|
||||||
|
_shader = Shader::createF(engine, vertexPath, fragmentPath);
|
||||||
|
assert(_shader != nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
BloomCombine::~BloomCombine(void) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void BloomCombine::render(Services::OpenGLRenderer& rs, Engine& engine) {
|
||||||
|
ZoneScopedN("RenderTasks::BloomCombine::render");
|
||||||
|
|
||||||
|
rs.targets[target_fbo]->bind(FrameBufferObject::W);
|
||||||
|
{ // TODO: move to fbo
|
||||||
|
GLsizei width {0};
|
||||||
|
GLsizei height {0};
|
||||||
|
{ // get size of fb <.<
|
||||||
|
auto& fbo_tex_arr = rs.targets[target_fbo]->_texAttachments;
|
||||||
|
if (fbo_tex_arr.empty()) {
|
||||||
|
//GLint o_type {0};
|
||||||
|
//glGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &o_type);
|
||||||
|
//if (o_type == GL_NONE) {
|
||||||
|
//// default framebuffer or error
|
||||||
|
//SPDLOG_INFO("gl none");
|
||||||
|
//}
|
||||||
|
|
||||||
|
// nah, just assume screen res
|
||||||
|
std::tie(width, height) = engine.getService<Services::SDLService>().getWindowSize();
|
||||||
|
} else {
|
||||||
|
auto& target_fbo_tex = rs.targets[target_fbo]->_texAttachments.front();
|
||||||
|
width = target_fbo_tex->width;
|
||||||
|
height = target_fbo_tex->height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
glViewport(0, 0, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
|
||||||
|
_shader->bind();
|
||||||
|
_vertexBuffer->bind(GL_ARRAY_BUFFER);
|
||||||
|
_vao->bind();
|
||||||
|
|
||||||
|
auto& rm = MM::ResourceManager<Texture>::ref();
|
||||||
|
|
||||||
|
auto tex_0 = rm.get(entt::hashed_string::value(tex0.c_str()));
|
||||||
|
tex_0->bind(0);
|
||||||
|
|
||||||
|
auto tex_1 = rm.get(entt::hashed_string::value(tex1.c_str()));
|
||||||
|
tex_1->bind(1);
|
||||||
|
|
||||||
|
// assign image units
|
||||||
|
_shader->setUniform1i("_tex0", 0);
|
||||||
|
_shader->setUniform1i("_tex1", 1);
|
||||||
|
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
|
|
||||||
|
_vao->unbind();
|
||||||
|
_vertexBuffer->unbind(GL_ARRAY_BUFFER);
|
||||||
|
_shader->unbind();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BloomCombine::reloadShaders(Engine& engine) {
|
||||||
|
auto tmp_shader = Shader::createF(engine, vertexPath, fragmentPath);
|
||||||
|
if (tmp_shader) {
|
||||||
|
_shader = tmp_shader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BloomCombine::setupShaderFiles(void) {
|
||||||
|
FS_CONST_MOUNT_FILE(vertexPath,
|
||||||
|
GLSL_VERSION_STRING
|
||||||
|
R"(
|
||||||
|
#ifdef GL_ES
|
||||||
|
precision mediump float;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
in vec2 _vertexPosition;
|
||||||
|
out vec2 _uv;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(_vertexPosition, 0, 1);
|
||||||
|
_uv = vec2(_vertexPosition.x * 0.5 + 0.5, _vertexPosition.y * 0.5 + 0.5);
|
||||||
|
})")
|
||||||
|
|
||||||
|
FS_CONST_MOUNT_FILE(fragmentPath,
|
||||||
|
GLSL_VERSION_STRING
|
||||||
|
R"(
|
||||||
|
#ifdef GL_ES
|
||||||
|
precision mediump float;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// tent sampling
|
||||||
|
#include "/shaders/builtin/sampling.glsl"
|
||||||
|
|
||||||
|
uniform sampler2D _tex0;
|
||||||
|
uniform sampler2D _tex1;
|
||||||
|
|
||||||
|
in vec2 _uv;
|
||||||
|
|
||||||
|
out vec3 _out_color;
|
||||||
|
|
||||||
|
vec3 tentSampling() {
|
||||||
|
// i hope the pipeline caches this
|
||||||
|
ivec2 tex0_size = textureSize(_tex0, 0); // TODO: lod
|
||||||
|
vec2 tex0_texel_step = vec2(1.0) / vec2(tex0_size);
|
||||||
|
|
||||||
|
return sampling2D_tent3x3_vec3(
|
||||||
|
_tex0,
|
||||||
|
_uv,
|
||||||
|
tex0_texel_step
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 simpleSampling() {
|
||||||
|
return texture(_tex0, _uv).rgb;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
#ifdef SIMPLE_SAMPLING
|
||||||
|
_out_color = texture(_tex1, _uv).rgb + simpleSampling();
|
||||||
|
#else
|
||||||
|
_out_color = texture(_tex1, _uv).rgb + tentSampling();
|
||||||
|
#endif
|
||||||
|
})")
|
||||||
|
}
|
||||||
|
|
||||||
|
} // MM::OpenGL::RenderTasks
|
||||||
|
|
@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <mm/opengl/render_task.hpp>
|
||||||
|
#include <mm/opengl/shader.hpp>
|
||||||
|
#include <mm/opengl/buffer.hpp>
|
||||||
|
#include <mm/opengl/vertex_array_object.hpp>
|
||||||
|
|
||||||
|
namespace MM::OpenGL::RenderTasks {
|
||||||
|
|
||||||
|
class BloomCombine : public RenderTask {
|
||||||
|
private:
|
||||||
|
std::shared_ptr<Shader> _shader;
|
||||||
|
std::unique_ptr<Buffer> _vertexBuffer;
|
||||||
|
std::unique_ptr<VertexArrayObject> _vao;
|
||||||
|
|
||||||
|
public:
|
||||||
|
BloomCombine(Engine& engine);
|
||||||
|
~BloomCombine(void);
|
||||||
|
|
||||||
|
const char* name(void) override { return "BloomCombine"; }
|
||||||
|
|
||||||
|
void render(Services::OpenGLRenderer& rs, Engine& engine) override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
const char* vertexPath {"shader/combine_render_task/vert.glsl"};
|
||||||
|
const char* fragmentPath {"shader/combine_render_task/frag.glsl"};
|
||||||
|
|
||||||
|
std::string target_fbo {"display"};
|
||||||
|
|
||||||
|
std::string tex0 {"tex0"}; // lower res
|
||||||
|
std::string tex1 {"tex1"};
|
||||||
|
|
||||||
|
void reloadShaders(Engine& engine);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void setupShaderFiles(void);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // MM::OpenGL::RenderTasks
|
||||||
|
|
@ -0,0 +1,114 @@
|
|||||||
|
#include "./bloom_extraction.hpp"
|
||||||
|
|
||||||
|
#include <mm/services/opengl_renderer.hpp>
|
||||||
|
#include <mm/fs_const_archiver.hpp>
|
||||||
|
#include <mm/opengl/texture.hpp>
|
||||||
|
|
||||||
|
#include <tracy/Tracy.hpp>
|
||||||
|
|
||||||
|
namespace MM::OpenGL::RenderTasks {
|
||||||
|
|
||||||
|
BloomExtraction::BloomExtraction(Engine& engine) {
|
||||||
|
float vertices[] = {
|
||||||
|
-1.f, 1.f,
|
||||||
|
-1.f, -1.f,
|
||||||
|
1.f, -1.f,
|
||||||
|
1.f, -1.f,
|
||||||
|
1.f, 1.f,
|
||||||
|
-1.f, 1.f,
|
||||||
|
};
|
||||||
|
|
||||||
|
_vertexBuffer = std::make_unique<Buffer>(vertices, 2 * 6 * sizeof(float), GL_STATIC_DRAW);
|
||||||
|
_vao = std::make_unique<VertexArrayObject>();
|
||||||
|
_vao->bind();
|
||||||
|
_vertexBuffer->bind(GL_ARRAY_BUFFER);
|
||||||
|
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), 0);
|
||||||
|
|
||||||
|
_vertexBuffer->unbind(GL_ARRAY_BUFFER);
|
||||||
|
_vao->unbind();
|
||||||
|
|
||||||
|
setupShaderFiles();
|
||||||
|
_shader = Shader::createF(engine, vertexPath, fragmentPath);
|
||||||
|
assert(_shader != nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
BloomExtraction::~BloomExtraction(void) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void BloomExtraction::render(Services::OpenGLRenderer& rs, Engine&) {
|
||||||
|
ZoneScopedN("RenderTasks::BloomExtraction::render");
|
||||||
|
|
||||||
|
auto& target_fbo_ = rs.targets[target_fbo];
|
||||||
|
target_fbo_->bind(FrameBufferObject::W);
|
||||||
|
auto& target_fbo_tex = target_fbo_->_texAttachments.front();
|
||||||
|
glViewport(0, 0, target_fbo_tex->width, target_fbo_tex->height);
|
||||||
|
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
|
||||||
|
_shader->bind();
|
||||||
|
_vertexBuffer->bind(GL_ARRAY_BUFFER);
|
||||||
|
_vao->bind();
|
||||||
|
|
||||||
|
auto& rm = MM::ResourceManager<Texture>::ref();
|
||||||
|
|
||||||
|
auto tex = rm.get(entt::hashed_string::value(src_tex.c_str()));
|
||||||
|
tex->bind(0);
|
||||||
|
|
||||||
|
_shader->setUniform1i("color_tex", 0);
|
||||||
|
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
|
|
||||||
|
_vao->unbind();
|
||||||
|
_vertexBuffer->unbind(GL_ARRAY_BUFFER);
|
||||||
|
_shader->unbind();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BloomExtraction::reloadShaders(Engine& engine) {
|
||||||
|
auto tmp_shader = Shader::createF(engine, vertexPath, fragmentPath);
|
||||||
|
if (tmp_shader) {
|
||||||
|
_shader = tmp_shader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BloomExtraction::setupShaderFiles(void) {
|
||||||
|
FS_CONST_MOUNT_FILE(vertexPath,
|
||||||
|
GLSL_VERSION_STRING
|
||||||
|
R"(
|
||||||
|
#ifdef GL_ES
|
||||||
|
precision mediump float;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
in vec2 _vertexPosition;
|
||||||
|
out vec2 _uv;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(_vertexPosition, 0, 1);
|
||||||
|
_uv = vec2(_vertexPosition.x * 0.5 + 0.5, _vertexPosition.y * 0.5 + 0.5);
|
||||||
|
})")
|
||||||
|
|
||||||
|
FS_CONST_MOUNT_FILE(fragmentPath,
|
||||||
|
GLSL_VERSION_STRING
|
||||||
|
R"(
|
||||||
|
#ifdef GL_ES
|
||||||
|
precision mediump float;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
uniform sampler2D color_tex;
|
||||||
|
|
||||||
|
in vec2 _uv;
|
||||||
|
|
||||||
|
out vec3 _out_color;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec3 color = texture(color_tex, _uv).rgb;
|
||||||
|
|
||||||
|
// TODO: expose threshold
|
||||||
|
_out_color = max(vec3(0.0), color - vec3(1.0));
|
||||||
|
})")
|
||||||
|
}
|
||||||
|
|
||||||
|
} // MM::OpenGL::RenderTasks
|
||||||
|
|
@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <mm/opengl/render_task.hpp>
|
||||||
|
#include <mm/opengl/shader.hpp>
|
||||||
|
#include <mm/opengl/buffer.hpp>
|
||||||
|
#include <mm/opengl/vertex_array_object.hpp>
|
||||||
|
|
||||||
|
namespace MM::OpenGL::RenderTasks {
|
||||||
|
|
||||||
|
class BloomExtraction : public RenderTask {
|
||||||
|
private:
|
||||||
|
std::shared_ptr<Shader> _shader;
|
||||||
|
std::unique_ptr<Buffer> _vertexBuffer;
|
||||||
|
std::unique_ptr<VertexArrayObject> _vao;
|
||||||
|
|
||||||
|
public:
|
||||||
|
BloomExtraction(Engine& engine);
|
||||||
|
~BloomExtraction(void);
|
||||||
|
|
||||||
|
const char* name(void) override { return "BloomExtraction"; }
|
||||||
|
|
||||||
|
void render(Services::OpenGLRenderer& rs, Engine& engine) override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
const char* vertexPath {"shader/bloom_extraction_render_task/vert.glsl"};
|
||||||
|
const char* fragmentPath {"shader/bloom_extraction_render_task/frag.glsl"};
|
||||||
|
|
||||||
|
std::string target_fbo {"display"};
|
||||||
|
|
||||||
|
std::string src_tex {"hdr_color"};
|
||||||
|
|
||||||
|
void reloadShaders(Engine& engine);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void setupShaderFiles(void);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // MM::OpenGL::RenderTasks
|
||||||
|
|
@ -36,10 +36,8 @@ Blur::Blur(Engine& engine) {
|
|||||||
_vao->unbind();
|
_vao->unbind();
|
||||||
|
|
||||||
setupShaderFiles();
|
setupShaderFiles();
|
||||||
_hShader = Shader::createF(engine, vertexPath, fragmentHPath);
|
_shader = Shader::createF(engine, vertexPath, fragmentPath);
|
||||||
assert(_hShader != nullptr);
|
assert(_shader != nullptr);
|
||||||
_vShader = Shader::createF(engine, vertexPath, fragmentVPath);
|
|
||||||
assert(_vShader != nullptr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Blur::~Blur(void) {
|
Blur::~Blur(void) {
|
||||||
@ -57,17 +55,19 @@ void Blur::render(Services::OpenGLRenderer& rs, Engine&) {
|
|||||||
auto tex_out = rm_t.get(entt::hashed_string::value(out_tex.c_str())); // TODO: perf problems
|
auto tex_out = rm_t.get(entt::hashed_string::value(out_tex.c_str())); // TODO: perf problems
|
||||||
auto tex_temp = rm_t.get(entt::hashed_string::value(temp_tex.c_str())); // TODO: perf problems
|
auto tex_temp = rm_t.get(entt::hashed_string::value(temp_tex.c_str())); // TODO: perf problems
|
||||||
|
|
||||||
|
_shader->bind();
|
||||||
|
_vertexBuffer->bind(GL_ARRAY_BUFFER);
|
||||||
|
_vao->bind();
|
||||||
|
|
||||||
|
_shader->setUniform2f("tex_offset_factor", tex_offset_factor);
|
||||||
|
|
||||||
{ // horizontal
|
{ // horizontal
|
||||||
rs.targets[temp_fbo]->bind(FrameBufferObject::W);
|
rs.targets[temp_fbo]->bind(FrameBufferObject::W);
|
||||||
|
|
||||||
_hShader->bind();
|
|
||||||
_vertexBuffer->bind(GL_ARRAY_BUFFER);
|
|
||||||
_vao->bind();
|
|
||||||
|
|
||||||
glViewport(0, 0, tex_temp->width, tex_temp->height);
|
glViewport(0, 0, tex_temp->width, tex_temp->height);
|
||||||
tex_in->bind(0); // read
|
tex_in->bind(0); // read
|
||||||
|
|
||||||
_hShader->setUniform2f("tex_offset", tex_offset);
|
_shader->setUniform1i("horizontal", 1);
|
||||||
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
}
|
}
|
||||||
@ -75,21 +75,15 @@ void Blur::render(Services::OpenGLRenderer& rs, Engine&) {
|
|||||||
{ // vertical
|
{ // vertical
|
||||||
rs.targets[out_fbo]->bind(FrameBufferObject::W);
|
rs.targets[out_fbo]->bind(FrameBufferObject::W);
|
||||||
|
|
||||||
_vShader->bind();
|
|
||||||
_vertexBuffer->bind(GL_ARRAY_BUFFER);
|
|
||||||
_vao->bind();
|
|
||||||
|
|
||||||
glViewport(0, 0, tex_out->width, tex_out->height);
|
glViewport(0, 0, tex_out->width, tex_out->height);
|
||||||
tex_temp->bind(0); // read
|
tex_temp->bind(0); // read
|
||||||
|
|
||||||
_vShader->setUniform2f("tex_offset", tex_offset);
|
_shader->setUniform1i("horizontal", 0);
|
||||||
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
_vao->unbind();
|
_vao->unbind();
|
||||||
_vertexBuffer->unbind(GL_ARRAY_BUFFER);
|
|
||||||
_vShader->unbind();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Blur::setupShaderFiles(void) {
|
void Blur::setupShaderFiles(void) {
|
||||||
@ -108,9 +102,7 @@ void main() {
|
|||||||
_tex_uv = _vertexPosition * 0.5 + 0.5;
|
_tex_uv = _vertexPosition * 0.5 + 0.5;
|
||||||
})")
|
})")
|
||||||
|
|
||||||
// TODO: deduplicate
|
FS_CONST_MOUNT_FILE(fragmentPath,
|
||||||
|
|
||||||
FS_CONST_MOUNT_FILE(fragmentHPath,
|
|
||||||
GLSL_VERSION_STRING
|
GLSL_VERSION_STRING
|
||||||
R"(
|
R"(
|
||||||
#ifdef GL_ES
|
#ifdef GL_ES
|
||||||
@ -120,54 +112,19 @@ R"(
|
|||||||
uniform sampler2D tex0;
|
uniform sampler2D tex0;
|
||||||
in vec2 _tex_uv;
|
in vec2 _tex_uv;
|
||||||
|
|
||||||
//uniform bool horizontal;
|
uniform bool horizontal;
|
||||||
const bool horizontal = true;
|
//const bool horizontal = true;
|
||||||
uniform vec2 tex_offset;
|
uniform vec2 tex_offset_factor;
|
||||||
|
|
||||||
const float weight[5] = float[] (0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);
|
const float weight[5] = float[] (0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);
|
||||||
|
|
||||||
out vec4 _out_color;
|
out vec4 _out_color;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
//vec2 tex_offset = vec2(1.0) / vec2(textureSize(tex0, 0)); // gets size of single texel
|
|
||||||
vec3 result = texture(tex0, _tex_uv).rgb * weight[0]; // current fragment's contribution
|
vec3 result = texture(tex0, _tex_uv).rgb * weight[0]; // current fragment's contribution
|
||||||
|
|
||||||
if (horizontal) {
|
vec2 tex_offset = vec2(1.0) / vec2(textureSize(tex0, 0).xy); // gets size of single texel
|
||||||
for (int i = 1; i < 5; i++) {
|
tex_offset *= tex_offset_factor;
|
||||||
result += texture(tex0, _tex_uv + vec2(tex_offset.x * float(i), 0.0)).rgb * weight[i];
|
|
||||||
result += texture(tex0, _tex_uv - vec2(tex_offset.x * float(i), 0.0)).rgb * weight[i];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (int i = 1; i < 5; i++) {
|
|
||||||
result += texture(tex0, _tex_uv + vec2(0.0, tex_offset.y * float(i))).rgb * weight[i];
|
|
||||||
result += texture(tex0, _tex_uv - vec2(0.0, tex_offset.y * float(i))).rgb * weight[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_out_color = vec4(result, 1.0);
|
|
||||||
})")
|
|
||||||
|
|
||||||
FS_CONST_MOUNT_FILE(fragmentVPath,
|
|
||||||
GLSL_VERSION_STRING
|
|
||||||
R"(
|
|
||||||
#ifdef GL_ES
|
|
||||||
precision mediump float;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
uniform sampler2D tex0;
|
|
||||||
in vec2 _tex_uv;
|
|
||||||
|
|
||||||
//uniform bool horizontal;
|
|
||||||
const bool horizontal = false;
|
|
||||||
uniform vec2 tex_offset;
|
|
||||||
|
|
||||||
const float weight[5] = float[] (0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);
|
|
||||||
|
|
||||||
out vec4 _out_color;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
//vec2 tex_offset = vec2(1.0) / vec2(textureSize(tex0, 0)); // gets size of single texel
|
|
||||||
vec3 result = texture(tex0, _tex_uv).rgb * weight[0]; // current fragment's contribution
|
|
||||||
|
|
||||||
if (horizontal) {
|
if (horizontal) {
|
||||||
for (int i = 1; i < 5; i++) {
|
for (int i = 1; i < 5; i++) {
|
||||||
|
@ -17,8 +17,7 @@ namespace MM::OpenGL::RenderTasks {
|
|||||||
// this task expects to read and write to textures
|
// this task expects to read and write to textures
|
||||||
class Blur : public RenderTask {
|
class Blur : public RenderTask {
|
||||||
private:
|
private:
|
||||||
std::shared_ptr<Shader> _hShader;
|
std::shared_ptr<Shader> _shader;
|
||||||
std::shared_ptr<Shader> _vShader;
|
|
||||||
std::unique_ptr<Buffer> _vertexBuffer;
|
std::unique_ptr<Buffer> _vertexBuffer;
|
||||||
std::unique_ptr<VertexArrayObject> _vao;
|
std::unique_ptr<VertexArrayObject> _vao;
|
||||||
|
|
||||||
@ -31,20 +30,19 @@ namespace MM::OpenGL::RenderTasks {
|
|||||||
void render(Services::OpenGLRenderer& rs, Engine& engine) override;
|
void render(Services::OpenGLRenderer& rs, Engine& engine) override;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
const char* vertexPath = "shader/blur_render_task/vert.glsl";
|
const char* vertexPath {"shader/blur_render_task/vert.glsl"};
|
||||||
const char* fragmentHPath = "shader/blur_render_task/frag_h.glsl";
|
const char* fragmentPath {"shader/blur_render_task/frag_h.glsl"};
|
||||||
const char* fragmentVPath = "shader/blur_render_task/frag_v.glsl";
|
|
||||||
|
|
||||||
std::string out_fbo = "blur_io";
|
std::string out_fbo {"blur_io"};
|
||||||
std::string temp_fbo = "blur_temp";
|
std::string temp_fbo {"blur_temp"};
|
||||||
|
|
||||||
// bc of it beeing a 2 pass, we need to flipflop
|
// bc of it beeing a 2 pass, we need to flipflop
|
||||||
std::string in_tex = "blur_io";
|
std::string in_tex {"blur_io"};
|
||||||
std::string out_tex = "blur_io";
|
std::string out_tex {"blur_io"};
|
||||||
std::string temp_tex = "blur_temp";
|
std::string temp_tex {"blur_temp"};
|
||||||
|
|
||||||
// determines the kernel lookup offset. "ideal" is 1/tex_size
|
// kernel lookup offset factor
|
||||||
glm::vec2 tex_offset {0.001f, 0.001f};
|
glm::vec2 tex_offset_factor {1.f, 1.f};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setupShaderFiles(void);
|
void setupShaderFiles(void);
|
||||||
|
@ -0,0 +1,164 @@
|
|||||||
|
#include "./composition.hpp"
|
||||||
|
|
||||||
|
#include <mm/fs_const_archiver.hpp>
|
||||||
|
#include <mm/services/opengl_renderer.hpp>
|
||||||
|
#include <mm/opengl/texture.hpp>
|
||||||
|
|
||||||
|
#include <tracy/Tracy.hpp>
|
||||||
|
|
||||||
|
namespace MM::OpenGL::RenderTasks {
|
||||||
|
|
||||||
|
Composition::Composition(MM::Engine& engine) {
|
||||||
|
float vertices[] = {
|
||||||
|
-1.f, 1.f,
|
||||||
|
-1.f, -1.f,
|
||||||
|
1.f, -1.f,
|
||||||
|
1.f, -1.f,
|
||||||
|
1.f, 1.f,
|
||||||
|
-1.f, 1.f,
|
||||||
|
};
|
||||||
|
|
||||||
|
_vertexBuffer = std::make_unique<MM::OpenGL::Buffer>(vertices, 2 * 6 * sizeof(float), GL_STATIC_DRAW);
|
||||||
|
_vao = std::make_unique<MM::OpenGL::VertexArrayObject>();
|
||||||
|
_vao->bind();
|
||||||
|
_vertexBuffer->bind(GL_ARRAY_BUFFER);
|
||||||
|
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), 0);
|
||||||
|
|
||||||
|
_vertexBuffer->unbind(GL_ARRAY_BUFFER);
|
||||||
|
_vao->unbind();
|
||||||
|
|
||||||
|
setupShaderFiles();
|
||||||
|
_shader = MM::OpenGL::Shader::createF(engine, vertexPath, fragmentPath);
|
||||||
|
assert(_shader != nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
Composition::~Composition(void) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void Composition::render(MM::Services::OpenGLRenderer& rs, MM::Engine& engine) {
|
||||||
|
ZoneScopedN("RenderTasks::Composition::render");
|
||||||
|
|
||||||
|
rs.targets[target_fbo]->bind(MM::OpenGL::FrameBufferObject::W);
|
||||||
|
{ // TODO: move to fbo
|
||||||
|
GLsizei width {0};
|
||||||
|
GLsizei height {0};
|
||||||
|
{ // get size of fb <.<
|
||||||
|
auto& fbo_tex_arr = rs.targets[target_fbo]->_texAttachments;
|
||||||
|
if (fbo_tex_arr.empty()) {
|
||||||
|
//GLint o_type {0};
|
||||||
|
//glGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &o_type);
|
||||||
|
//if (o_type == GL_NONE) {
|
||||||
|
//// default framebuffer or error
|
||||||
|
//SPDLOG_INFO("gl none");
|
||||||
|
//}
|
||||||
|
|
||||||
|
// nah, just assume screen res
|
||||||
|
std::tie(width, height) = engine.getService<MM::Services::SDLService>().getWindowSize();
|
||||||
|
} else {
|
||||||
|
auto& target_fbo_tex = rs.targets[target_fbo]->_texAttachments.front();
|
||||||
|
width = target_fbo_tex->width;
|
||||||
|
height = target_fbo_tex->height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
glViewport(0, 0, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
|
||||||
|
_shader->bind();
|
||||||
|
_vertexBuffer->bind(GL_ARRAY_BUFFER);
|
||||||
|
_vao->bind();
|
||||||
|
|
||||||
|
auto& rm = MM::ResourceManager<MM::OpenGL::Texture>::ref();
|
||||||
|
|
||||||
|
auto tex_a = rm.get(entt::hashed_string::value(color_tex.c_str()));
|
||||||
|
tex_a->bind(0);
|
||||||
|
|
||||||
|
auto tex_n = rm.get(entt::hashed_string::value(bloom_tex.c_str()));
|
||||||
|
tex_n->bind(1);
|
||||||
|
|
||||||
|
// assign image units
|
||||||
|
_shader->setUniform1i("color_tex", 0);
|
||||||
|
_shader->setUniform1i("bloom_tex", 1);
|
||||||
|
|
||||||
|
_shader->setUniform1f("bloom_factor", bloom_factor);
|
||||||
|
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
|
|
||||||
|
_vao->unbind();
|
||||||
|
_vertexBuffer->unbind(GL_ARRAY_BUFFER);
|
||||||
|
_shader->unbind();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Composition::reloadShaders(MM::Engine& engine) {
|
||||||
|
auto tmp_shader = MM::OpenGL::Shader::createF(engine, vertexPath, fragmentPath);
|
||||||
|
if (tmp_shader) {
|
||||||
|
_shader = tmp_shader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Composition::setupShaderFiles(void) {
|
||||||
|
FS_CONST_MOUNT_FILE(vertexPath,
|
||||||
|
GLSL_VERSION_STRING
|
||||||
|
R"(
|
||||||
|
#ifdef GL_ES
|
||||||
|
precision mediump float;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
in vec2 _vertexPosition;
|
||||||
|
out vec2 _uv;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(_vertexPosition, 0, 1);
|
||||||
|
_uv = vec2(_vertexPosition.x * 0.5 + 0.5, _vertexPosition.y * 0.5 + 0.5);
|
||||||
|
})")
|
||||||
|
|
||||||
|
FS_CONST_MOUNT_FILE(fragmentPath,
|
||||||
|
GLSL_VERSION_STRING
|
||||||
|
R"(
|
||||||
|
#ifdef GL_ES
|
||||||
|
precision mediump float;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "/shaders/builtin/tonemapping.glsl"
|
||||||
|
|
||||||
|
uniform sampler2D color_tex;
|
||||||
|
uniform sampler2D bloom_tex;
|
||||||
|
|
||||||
|
// high bc 32bit on cpu
|
||||||
|
uniform highp float bloom_factor;
|
||||||
|
|
||||||
|
in vec2 _uv;
|
||||||
|
|
||||||
|
out vec3 _out_color;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec3 color = texture(color_tex, _uv).rgb;
|
||||||
|
vec3 bloom = texture(bloom_tex, _uv).rgb;
|
||||||
|
|
||||||
|
vec3 comp = color + bloom * vec3(bloom_factor);
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
const vec3 tint = vec3(1.5, 0.8, 1.1);
|
||||||
|
comp *= tint;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
//// reinhart + gamma
|
||||||
|
//_out_color = pow(comp / (comp + vec3(1.0)), vec3(1.0 / 2.2));
|
||||||
|
|
||||||
|
// makes more sense pre bloom
|
||||||
|
//comp *= vec3(pow(2.0, -1.0)); // pre expose, -1 fstops
|
||||||
|
|
||||||
|
//_out_color = tonemapReinhard(comp);
|
||||||
|
_out_color = tonemapACESFilm(comp); // looks right
|
||||||
|
//_out_color = pow(tonemapACESFilm(pow(comp, vec3(2.2))), vec3(1.0/2.2)); // insane saturation o.O
|
||||||
|
//_out_color = pow(tonemapACESFilm(comp), vec3(1.0/2.2)); // looks just wrong
|
||||||
|
})")
|
||||||
|
}
|
||||||
|
|
||||||
|
} // MM::OpenGL::RenderTasks
|
||||||
|
|
@ -0,0 +1,42 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <mm/opengl/render_task.hpp>
|
||||||
|
#include <mm/opengl/shader.hpp>
|
||||||
|
#include <mm/opengl/buffer.hpp>
|
||||||
|
#include <mm/opengl/vertex_array_object.hpp>
|
||||||
|
|
||||||
|
namespace MM::OpenGL::RenderTasks {
|
||||||
|
|
||||||
|
class Composition : public MM::OpenGL::RenderTask {
|
||||||
|
private:
|
||||||
|
std::shared_ptr<MM::OpenGL::Shader> _shader;
|
||||||
|
std::unique_ptr<MM::OpenGL::Buffer> _vertexBuffer;
|
||||||
|
std::unique_ptr<MM::OpenGL::VertexArrayObject> _vao;
|
||||||
|
|
||||||
|
public:
|
||||||
|
Composition(MM::Engine& engine);
|
||||||
|
~Composition(void);
|
||||||
|
|
||||||
|
const char* name(void) override { return "Composition"; }
|
||||||
|
|
||||||
|
void render(MM::Services::OpenGLRenderer& rs, MM::Engine& engine) override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
const char* vertexPath {"shader/composition_render_task/vert.glsl"};
|
||||||
|
const char* fragmentPath {"shader/composition_render_task/frag.glsl"};
|
||||||
|
|
||||||
|
std::string target_fbo {"display"};
|
||||||
|
|
||||||
|
std::string color_tex {"hdr_color"};
|
||||||
|
std::string bloom_tex {"bloom"};
|
||||||
|
|
||||||
|
float bloom_factor {1.f};
|
||||||
|
|
||||||
|
void reloadShaders(MM::Engine& engine);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void setupShaderFiles(void);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // MM::OpenGL::RenderTasks
|
||||||
|
|
@ -160,3 +160,30 @@ target_link_libraries(fast_sky_render_task_test
|
|||||||
|
|
||||||
add_test(NAME fast_sky_render_task_test COMMAND fast_sky_render_task_test)
|
add_test(NAME fast_sky_render_task_test COMMAND fast_sky_render_task_test)
|
||||||
|
|
||||||
|
################# hdr bloom example
|
||||||
|
|
||||||
|
add_executable(hdr_bloom_pipeline_example ./hdr_bloom_pipeline_example.cpp)
|
||||||
|
|
||||||
|
target_link_libraries(hdr_bloom_pipeline_example
|
||||||
|
opengl_renderer_s
|
||||||
|
organizer_scene
|
||||||
|
clear_render_task
|
||||||
|
blit_fb_render_task
|
||||||
|
simple_rect_render_task
|
||||||
|
|
||||||
|
#bloom_extraction_render_task
|
||||||
|
#blur_render_task
|
||||||
|
#bloom_combine_render_task
|
||||||
|
bloom
|
||||||
|
composition_render_task
|
||||||
|
|
||||||
|
simple_velocity_system
|
||||||
|
transform_system
|
||||||
|
|
||||||
|
random
|
||||||
|
|
||||||
|
gtest_main
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(NAME hdr_bloom_pipeline_example COMMAND hdr_bloom_pipeline_example)
|
||||||
|
|
||||||
|
291
framework/opengl_renderer/test/hdr_bloom_pipeline_example.cpp
Normal file
291
framework/opengl_renderer/test/hdr_bloom_pipeline_example.cpp
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <mm/engine.hpp>
|
||||||
|
|
||||||
|
#include <mm/services/filesystem.hpp>
|
||||||
|
#include <mm/services/sdl_service.hpp>
|
||||||
|
#include <mm/services/organizer_scene.hpp>
|
||||||
|
#include <mm/services/opengl_renderer.hpp>
|
||||||
|
|
||||||
|
#include <entt/entity/registry.hpp>
|
||||||
|
#include <entt/entity/organizer.hpp>
|
||||||
|
|
||||||
|
#include <mm/opengl/render_tasks/clear.hpp>
|
||||||
|
#include <mm/opengl/render_tasks/blit_fb.hpp>
|
||||||
|
#include <mm/opengl/render_tasks/simple_rect.hpp>
|
||||||
|
//#include <mm/opengl/render_tasks/bloom_extraction.hpp>
|
||||||
|
//#include <mm/opengl/render_tasks/blur.hpp>
|
||||||
|
//#include <mm/opengl/render_tasks/bloom_combine.hpp>
|
||||||
|
#include <mm/opengl/bloom.hpp>
|
||||||
|
#include <mm/opengl/render_tasks/composition.hpp>
|
||||||
|
|
||||||
|
#include <mm/components/position2d.hpp>
|
||||||
|
#include <mm/components/position2d_zoffset.hpp>
|
||||||
|
#include <mm/components/scale2d.hpp>
|
||||||
|
#include <mm/components/rotation2d.hpp>
|
||||||
|
#include <mm/components/velocity2d_rotation.hpp>
|
||||||
|
#include <mm/components/position3d.hpp>
|
||||||
|
#include <mm/components/transform4x4.hpp>
|
||||||
|
#include <mm/components/color.hpp>
|
||||||
|
|
||||||
|
#include <mm/systems/simple_velocity_system2d.hpp>
|
||||||
|
#include <mm/systems/transform.hpp>
|
||||||
|
|
||||||
|
#include <mm/opengl/fbo_builder.hpp>
|
||||||
|
#include <mm/opengl/texture_loader.hpp>
|
||||||
|
|
||||||
|
#include <mm/random/srng.hpp>
|
||||||
|
|
||||||
|
using namespace entt::literals;
|
||||||
|
|
||||||
|
const char* argv0;
|
||||||
|
|
||||||
|
static void setup_textures(MM::Engine& engine) {
|
||||||
|
auto& rm_t = MM::ResourceManager<MM::OpenGL::Texture>::ref();
|
||||||
|
auto [w, h] = engine.getService<MM::Services::SDLService>().getWindowSize();
|
||||||
|
|
||||||
|
// we dont have a gbuffer in this example
|
||||||
|
{ // gbuffer
|
||||||
|
// depth
|
||||||
|
#ifdef MM_OPENGL_3_GLES
|
||||||
|
rm_t.reload<MM::OpenGL::TextureLoaderEmpty>(
|
||||||
|
"depth",
|
||||||
|
GL_DEPTH_COMPONENT24, // d16 is the onlyone for gles 2 (TODO: test for 3)
|
||||||
|
w, h,
|
||||||
|
GL_DEPTH_COMPONENT, GL_UNSIGNED_INT
|
||||||
|
);
|
||||||
|
#else
|
||||||
|
rm_t.reload<MM::OpenGL::TextureLoaderEmpty>(
|
||||||
|
"depth",
|
||||||
|
GL_DEPTH_COMPONENT32F, // TODO: stencil?
|
||||||
|
w, h,
|
||||||
|
GL_DEPTH_COMPONENT, GL_FLOAT
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
// albedo
|
||||||
|
rm_t.reload<MM::OpenGL::TextureLoaderEmpty>(
|
||||||
|
"albedo",
|
||||||
|
#ifdef MM_OPENGL_3_GLES
|
||||||
|
GL_RGB565,
|
||||||
|
#else
|
||||||
|
GL_RGBA8, // waste of A
|
||||||
|
#endif
|
||||||
|
w, h,
|
||||||
|
GL_RGB, GL_UNSIGNED_BYTE
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
// normal
|
||||||
|
rm_t.reload<MM::OpenGL::TextureLoaderEmpty>(
|
||||||
|
"normal",
|
||||||
|
// prolly fine, but need to remapp to -1,1
|
||||||
|
#ifdef MM_OPENGL_3_GLES
|
||||||
|
GL_RGB565,
|
||||||
|
#else
|
||||||
|
GL_RGBA8, // waste of A
|
||||||
|
#endif
|
||||||
|
w, h,
|
||||||
|
GL_RGB, GL_UNSIGNED_BYTE
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
const float render_scale = 1.f;
|
||||||
|
|
||||||
|
// hdr color gles3 / webgl2
|
||||||
|
rm_t.reload<MM::OpenGL::TextureLoaderEmpty>(
|
||||||
|
"hdr_color",
|
||||||
|
GL_RGBA16F,
|
||||||
|
w * render_scale, h * render_scale,
|
||||||
|
GL_RGBA,
|
||||||
|
GL_HALF_FLOAT
|
||||||
|
);
|
||||||
|
{ // filter
|
||||||
|
rm_t.get("hdr_color"_hs)->bind(0);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setup_fbos(MM::Engine& engine) {
|
||||||
|
auto& rs = engine.getService<MM::Services::OpenGLRenderer>();
|
||||||
|
auto& rm_t = MM::ResourceManager<MM::OpenGL::Texture>::ref();
|
||||||
|
|
||||||
|
const float render_scale = 1.f;
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
rs.targets["clear_opaque"] = MM::OpenGL::FBOBuilder::start()
|
||||||
|
.attachTexture(rm_t.get("albedo"_hs), GL_COLOR_ATTACHMENT0)
|
||||||
|
.attachTexture(rm_t.get("opaque_depth"_hs), GL_DEPTH_ATTACHMENT)
|
||||||
|
.setResize(true)
|
||||||
|
.finish();
|
||||||
|
assert(rs.targets["clear_opaque"]);
|
||||||
|
|
||||||
|
rs.targets["clear_opaque_normal"] = MM::OpenGL::FBOBuilder::start()
|
||||||
|
.attachTexture(rm_t.get("normal"_hs), GL_COLOR_ATTACHMENT0)
|
||||||
|
.setResize(true)
|
||||||
|
.finish();
|
||||||
|
assert(rs.targets["clear_opaque"]);
|
||||||
|
|
||||||
|
rs.targets["opaque"] = MM::OpenGL::FBOBuilder::start()
|
||||||
|
.attachTexture(rm_t.get("albedo"_hs), GL_COLOR_ATTACHMENT0)
|
||||||
|
.attachTexture(rm_t.get("normal"_hs), GL_COLOR_ATTACHMENT1)
|
||||||
|
.attachTexture(rm_t.get("opaque_depth"_hs), GL_DEPTH_ATTACHMENT)
|
||||||
|
.setResize(true)
|
||||||
|
.finish();
|
||||||
|
assert(rs.targets["opaque"]);
|
||||||
|
|
||||||
|
rs.targets["tmp_read"] = MM::OpenGL::FBOBuilder::start()
|
||||||
|
.attachTexture(rm_t.get("normal"_hs), GL_COLOR_ATTACHMENT0)
|
||||||
|
.setResize(false)
|
||||||
|
.finish();
|
||||||
|
assert(rs.targets["tmp_read"]);
|
||||||
|
|
||||||
|
rs.targets["depth_read"] = MM::OpenGL::FBOBuilder::start()
|
||||||
|
.attachTexture(rm_t.get("opaque_depth"_hs), GL_DEPTH_ATTACHMENT)
|
||||||
|
.setResize(false)
|
||||||
|
.finish();
|
||||||
|
assert(rs.targets["depth_read"]);
|
||||||
|
|
||||||
|
rs.targets["deferred_shading"] = MM::OpenGL::FBOBuilder::start()
|
||||||
|
.attachTexture(rm_t.get("hdr_color"_hs), GL_COLOR_ATTACHMENT0)
|
||||||
|
.setResize(true)
|
||||||
|
.finish();
|
||||||
|
assert(rs.targets["deferred_shading"]);
|
||||||
|
#endif
|
||||||
|
rs.targets["game_view"] = MM::OpenGL::FBOBuilder::start()
|
||||||
|
.attachTexture(rm_t.get("hdr_color"_hs), GL_COLOR_ATTACHMENT0)
|
||||||
|
.attachTexture(rm_t.get("depth"_hs), GL_DEPTH_ATTACHMENT)
|
||||||
|
.setResizeFactors(render_scale, render_scale)
|
||||||
|
.setResize(true)
|
||||||
|
.finish();
|
||||||
|
assert(rs.targets["game_view"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(hdr_bloom_pipeline, it) {
|
||||||
|
MM::Engine engine;
|
||||||
|
|
||||||
|
auto& sdl_ss = engine.addService<MM::Services::SDLService>();
|
||||||
|
ASSERT_TRUE(engine.enableService<MM::Services::SDLService>());
|
||||||
|
|
||||||
|
sdl_ss.createGLWindow("hdr_bloom_pipeline_example", 1280, 720);
|
||||||
|
|
||||||
|
engine.addService<MM::Services::OrganizerSceneService>();
|
||||||
|
ASSERT_TRUE(engine.enableService<MM::Services::OrganizerSceneService>());
|
||||||
|
|
||||||
|
bool provide_ret = engine.provide<MM::Services::SceneServiceInterface, MM::Services::OrganizerSceneService>();
|
||||||
|
ASSERT_TRUE(provide_ret);
|
||||||
|
auto& scene = engine.tryService<MM::Services::SceneServiceInterface>()->getScene();
|
||||||
|
|
||||||
|
engine.addService<MM::Services::FilesystemService>(argv0, "hdr_bloom_pipeline_example");
|
||||||
|
ASSERT_TRUE(engine.enableService<MM::Services::FilesystemService>());
|
||||||
|
|
||||||
|
auto& rs = engine.addService<MM::Services::OpenGLRenderer>();
|
||||||
|
ASSERT_TRUE(engine.enableService<MM::Services::OpenGLRenderer>());
|
||||||
|
|
||||||
|
{ // setup rendering
|
||||||
|
// TODO: split vertically
|
||||||
|
setup_textures(engine);
|
||||||
|
setup_fbos(engine);
|
||||||
|
|
||||||
|
// clear
|
||||||
|
auto& clear_opaque = rs.addRenderTask<MM::OpenGL::RenderTasks::Clear>(engine);
|
||||||
|
clear_opaque.target_fbo = "game_view";
|
||||||
|
// clears all color attachments
|
||||||
|
clear_opaque.r = 0.f;
|
||||||
|
clear_opaque.g = 0.f;
|
||||||
|
clear_opaque.b = 0.f;
|
||||||
|
clear_opaque.a = 1.f;
|
||||||
|
clear_opaque.mask = GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT;
|
||||||
|
|
||||||
|
{ // render, whatever
|
||||||
|
MM::OpenGL::RenderTasks::SimpleRect& bsrr_rend = rs.addRenderTask<MM::OpenGL::RenderTasks::SimpleRect>(engine);
|
||||||
|
bsrr_rend.target_fbo = "game_view";
|
||||||
|
}
|
||||||
|
|
||||||
|
// rn does rt too
|
||||||
|
MM::OpenGL::setup_bloom(engine);
|
||||||
|
|
||||||
|
// not part of setup_bloom
|
||||||
|
auto& comp = rs.addRenderTask<MM::OpenGL::RenderTasks::Composition>(engine);
|
||||||
|
comp.color_tex = "hdr_color";
|
||||||
|
comp.bloom_tex = "blur_tmp1";
|
||||||
|
comp.target_fbo = "display";
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.on_construct<MM::Components::Position2D>().connect<&entt::registry::emplace_or_replace<MM::Components::Position2D_ZOffset>>();
|
||||||
|
scene.on_construct<MM::Components::Position2D>().connect<&entt::registry::emplace_or_replace<MM::Components::Position3D>>();
|
||||||
|
scene.on_construct<MM::Components::Position2D>().connect<&entt::registry::emplace_or_replace<MM::Components::Transform4x4>>();
|
||||||
|
scene.on_construct<MM::Components::Position2D>().connect<&entt::registry::emplace_or_replace<MM::Components::DirtyTransformTag>>();
|
||||||
|
|
||||||
|
scene.on_update<MM::Components::Position2D>().connect<&entt::registry::emplace_or_replace<MM::Components::DirtyTransformTag>>();
|
||||||
|
scene.on_update<MM::Components::Position2D_ZOffset>().connect<&entt::registry::emplace_or_replace<MM::Components::DirtyTransformTag>>();
|
||||||
|
scene.on_update<MM::Components::Position3D>().connect<&entt::registry::emplace_or_replace<MM::Components::DirtyTransformTag>>();
|
||||||
|
scene.on_update<MM::Components::Scale2D>().connect<&entt::registry::emplace_or_replace<MM::Components::DirtyTransformTag>>();
|
||||||
|
scene.on_update<MM::Components::Rotation2D>().connect<&entt::registry::emplace_or_replace<MM::Components::DirtyTransformTag>>(); // in this example only rotation is touched
|
||||||
|
|
||||||
|
|
||||||
|
// setup v system
|
||||||
|
auto& org = scene.set<entt::organizer>();
|
||||||
|
org.emplace<MM::Systems::simple_rotational_velocity_patching>("simple_rotational_velocity_patching");
|
||||||
|
org.emplace<MM::Systems::position3d_from_2d>("position3d_from_2d");
|
||||||
|
org.emplace<MM::Systems::transform3d_translate>("transform3d_translate");
|
||||||
|
org.emplace<MM::Systems::transform3d_rotate2d>("transform3d_rotate2d");
|
||||||
|
org.emplace<MM::Systems::transform3d_scale2d>("transform3d_scale2d");
|
||||||
|
org.emplace<MM::Systems::transform_clear_dirty>("transform_clear_dirty");
|
||||||
|
|
||||||
|
|
||||||
|
// HACK: instead you would switch to this scene
|
||||||
|
engine.getService<MM::Services::OrganizerSceneService>().updateOrganizerVertices(scene);
|
||||||
|
|
||||||
|
MM::Random::SRNG rng{42};
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
auto e = scene.create();
|
||||||
|
auto& p = scene.emplace<MM::Components::Position2D>(e);
|
||||||
|
p.pos.x = 0.f;
|
||||||
|
p.pos.y = 25.f - i * 10.f;
|
||||||
|
|
||||||
|
auto& s = scene.emplace<MM::Components::Scale2D>(e);
|
||||||
|
s.scale = {50.f, i*0.2f + 0.1f};
|
||||||
|
|
||||||
|
auto& col = scene.emplace<MM::Components::Color>(e);
|
||||||
|
col.color = {3.f, 3.f, 3.f, 1.f};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
auto e = scene.create();
|
||||||
|
auto& p = scene.emplace<MM::Components::Position2D>(e);
|
||||||
|
p.pos.x = i * 9.f - 40;
|
||||||
|
|
||||||
|
// zoffset is created by event
|
||||||
|
|
||||||
|
auto& s = scene.emplace<MM::Components::Scale2D>(e);
|
||||||
|
s.scale = {5,5};
|
||||||
|
|
||||||
|
scene.emplace<MM::Components::Rotation2D>(e);
|
||||||
|
|
||||||
|
auto& v = scene.emplace<MM::Components::Velocity2DRotation>(e);
|
||||||
|
v.rot_vel = i * 0.3f;
|
||||||
|
|
||||||
|
auto& col = scene.emplace<MM::Components::Color>(e);
|
||||||
|
col.color = {rng.zeroToOne()*2.f, rng.zeroToOne()*2.f, rng.zeroToOne()*2.f, 1.f};
|
||||||
|
}
|
||||||
|
|
||||||
|
engine.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
argv0 = argv[0];
|
||||||
|
|
||||||
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
|
|
||||||
|
return RUN_ALL_TESTS();
|
||||||
|
}
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user