/*
|
|
* Copyright (C) 2023 Christopher J. Howard
|
|
*
|
|
* This file is part of Antkeeper source code.
|
|
*
|
|
* Antkeeper source code is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Antkeeper source code is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with Antkeeper source code. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "game/graphics.hpp"
|
|
#include <engine/config.hpp>
|
|
#include <engine/debug/log.hpp>
|
|
#include <engine/gl/framebuffer.hpp>
|
|
#include <engine/gl/texture.hpp>
|
|
#include <engine/render/passes/bloom-pass.hpp>
|
|
#include <engine/render/passes/final-pass.hpp>
|
|
#include <engine/render/passes/resample-pass.hpp>
|
|
#include <engine/render/passes/sky-pass.hpp>
|
|
#include <engine/render/passes/material-pass.hpp>
|
|
#include <chrono>
|
|
#include <filesystem>
|
|
#include <format>
|
|
#include <glad/gl.h>
|
|
#include <stb/stb_image_write.h>
|
|
#include <thread>
|
|
|
|
namespace graphics {
|
|
|
|
static void reroute_framebuffers(::game& ctx);
|
|
|
|
static void rebuild_hdr_framebuffer(::game& ctx)
|
|
{
|
|
// Construct HDR framebuffer sampler
|
|
auto hdr_sampler = std::make_shared<gl::sampler>
|
|
(
|
|
gl::sampler_filter::linear,
|
|
gl::sampler_filter::linear,
|
|
gl::sampler_mipmap_mode::linear,
|
|
gl::sampler_address_mode::clamp_to_edge,
|
|
gl::sampler_address_mode::clamp_to_edge
|
|
);
|
|
|
|
// Construct HDR framebuffer color texture
|
|
ctx.hdr_color_texture = std::make_shared<gl::texture_2d>
|
|
(
|
|
std::make_shared<gl::image_view_2d>
|
|
(
|
|
std::make_shared<gl::image_2d>
|
|
(
|
|
gl::format::r32g32b32_sfloat,
|
|
ctx.render_resolution.x(),
|
|
ctx.render_resolution.y()
|
|
)
|
|
),
|
|
hdr_sampler
|
|
);
|
|
|
|
// Construct HDR framebuffer depth texture
|
|
ctx.hdr_depth_texture = std::make_shared<gl::texture_2d>
|
|
(
|
|
std::make_shared<gl::image_view_2d>
|
|
(
|
|
std::make_shared<gl::image_2d>
|
|
(
|
|
gl::format::d32_sfloat_s8_uint,
|
|
ctx.render_resolution.x(),
|
|
ctx.render_resolution.y()
|
|
)
|
|
),
|
|
hdr_sampler
|
|
);
|
|
|
|
// Construct HDR framebuffer
|
|
const gl::framebuffer_attachment hdr_attachments[2] =
|
|
{
|
|
{
|
|
gl::color_attachment_bit,
|
|
ctx.hdr_color_texture->get_image_view(),
|
|
0
|
|
},
|
|
{
|
|
gl::depth_stencil_attachment_bits,
|
|
ctx.hdr_depth_texture->get_image_view(),
|
|
0
|
|
}
|
|
};
|
|
ctx.hdr_framebuffer = std::make_shared<gl::framebuffer>(hdr_attachments, ctx.render_resolution.x(), ctx.render_resolution.y());
|
|
}
|
|
|
|
static void rebuild_ldr_framebuffers(::game& ctx)
|
|
{
|
|
auto ldr_sampler = std::make_shared<gl::sampler>
|
|
(
|
|
gl::sampler_filter::linear,
|
|
gl::sampler_filter::linear,
|
|
gl::sampler_mipmap_mode::linear,
|
|
gl::sampler_address_mode::clamp_to_edge,
|
|
gl::sampler_address_mode::clamp_to_edge
|
|
);
|
|
|
|
// Construct LDR framebuffer A color texture
|
|
ctx.ldr_color_texture_a = std::make_shared<gl::texture_2d>
|
|
(
|
|
std::make_shared<gl::image_view_2d>
|
|
(
|
|
std::make_shared<gl::image_2d>
|
|
(
|
|
gl::format::r8g8b8_unorm,
|
|
ctx.render_resolution.x(),
|
|
ctx.render_resolution.y()
|
|
)
|
|
),
|
|
ldr_sampler
|
|
);
|
|
|
|
// Construct LDR framebuffer A
|
|
const gl::framebuffer_attachment ldr_attachments_a[1] =
|
|
{
|
|
{
|
|
gl::color_attachment_bit,
|
|
ctx.ldr_color_texture_a->get_image_view(),
|
|
0
|
|
}
|
|
};
|
|
ctx.ldr_framebuffer_a = std::make_shared<gl::framebuffer>(ldr_attachments_a, ctx.render_resolution.x(), ctx.render_resolution.y());
|
|
|
|
// Construct LDR framebuffer B color texture
|
|
ctx.ldr_color_texture_b = std::make_shared<gl::texture_2d>
|
|
(
|
|
std::make_shared<gl::image_view_2d>
|
|
(
|
|
std::make_shared<gl::image_2d>
|
|
(
|
|
gl::format::r8g8b8_unorm,
|
|
ctx.render_resolution.x(),
|
|
ctx.render_resolution.y()
|
|
)
|
|
),
|
|
ldr_sampler
|
|
);
|
|
|
|
// Construct LDR framebuffer B
|
|
const gl::framebuffer_attachment ldr_attachments_b[1] =
|
|
{
|
|
{
|
|
gl::color_attachment_bit,
|
|
ctx.ldr_color_texture_b->get_image_view(),
|
|
0
|
|
}
|
|
};
|
|
ctx.ldr_framebuffer_b = std::make_shared<gl::framebuffer>(ldr_attachments_b, ctx.render_resolution.x(), ctx.render_resolution.y());
|
|
}
|
|
|
|
void rebuild_shadow_framebuffer(::game& ctx)
|
|
{
|
|
// Construct shadow map sampler
|
|
auto shadow_sampler = std::make_shared<gl::sampler>
|
|
(
|
|
gl::sampler_filter::linear,
|
|
gl::sampler_filter::linear,
|
|
gl::sampler_mipmap_mode::linear,
|
|
gl::sampler_address_mode::clamp_to_border,
|
|
gl::sampler_address_mode::clamp_to_border,
|
|
gl::sampler_address_mode::clamp_to_border,
|
|
0.0f,
|
|
0.0f,
|
|
true,
|
|
gl::compare_op::greater,
|
|
-1000.0f,
|
|
1000.0f,
|
|
std::array<float, 4>{0.0f, 0.0f, 0.0f, 0.0f}
|
|
);
|
|
|
|
// Construct shadow map framebuffer depth texture
|
|
ctx.shadow_map_depth_texture = std::make_shared<gl::texture_2d>
|
|
(
|
|
std::make_shared<gl::image_view_2d>
|
|
(
|
|
std::make_shared<gl::image_2d>
|
|
(
|
|
gl::format::d32_sfloat,
|
|
ctx.shadow_map_resolution,
|
|
ctx.shadow_map_resolution
|
|
)
|
|
),
|
|
shadow_sampler
|
|
);
|
|
|
|
// Construct shadow map framebuffer
|
|
const gl::framebuffer_attachment shadow_map_attachments[1] =
|
|
{
|
|
{
|
|
gl::depth_attachment_bit,
|
|
ctx.shadow_map_depth_texture->get_image_view(),
|
|
0
|
|
}
|
|
};
|
|
ctx.shadow_map_framebuffer = std::make_shared<gl::framebuffer>(shadow_map_attachments, ctx.shadow_map_resolution, ctx.shadow_map_resolution);
|
|
}
|
|
|
|
void create_framebuffers(::game& ctx)
|
|
{
|
|
debug::log_trace("Creating framebuffers...");
|
|
|
|
// Calculate render resolution
|
|
const math::ivec2& viewport_size = ctx.window->get_viewport_size();
|
|
ctx.render_resolution = {static_cast<int>(viewport_size.x() * ctx.render_scale + 0.5f), static_cast<int>(viewport_size.y() * ctx.render_scale + 0.5f)};
|
|
|
|
rebuild_hdr_framebuffer(ctx);
|
|
rebuild_ldr_framebuffers(ctx);
|
|
rebuild_shadow_framebuffer(ctx);
|
|
|
|
debug::log_trace("Created framebuffers");
|
|
}
|
|
|
|
void destroy_framebuffers(::game& ctx)
|
|
{
|
|
debug::log_trace("Destroying framebuffers...");
|
|
|
|
// Delete HDR framebuffer and its attachments
|
|
ctx.hdr_framebuffer.reset();
|
|
ctx.hdr_color_texture.reset();
|
|
ctx.hdr_depth_texture.reset();
|
|
|
|
// Delete LDR framebuffers and attachments
|
|
ctx.ldr_framebuffer_a.reset();
|
|
ctx.ldr_color_texture_a.reset();
|
|
|
|
ctx.ldr_framebuffer_b.reset();
|
|
ctx.ldr_color_texture_b.reset();
|
|
|
|
// Delete shadow map framebuffer and its attachments
|
|
ctx.shadow_map_framebuffer.reset();
|
|
ctx.shadow_map_depth_texture.reset();
|
|
|
|
debug::log_trace("Destroyed framebuffers");
|
|
}
|
|
|
|
void change_render_resolution(::game& ctx, float scale)
|
|
{
|
|
// Recalculate render resolution
|
|
const math::ivec2& viewport_size = ctx.window->get_viewport_size();
|
|
const auto render_resolution = math::ivec2{static_cast<int>(viewport_size.x() * scale + 0.5f), static_cast<int>(viewport_size.y() * scale + 0.5f)};
|
|
|
|
if (ctx.render_resolution == render_resolution)
|
|
{
|
|
return;
|
|
}
|
|
|
|
debug::log_trace("Changing render resolution to {}...", scale);
|
|
|
|
// Update render resolution scale
|
|
ctx.render_scale = scale;
|
|
ctx.render_resolution = render_resolution;
|
|
|
|
rebuild_hdr_framebuffer(ctx);
|
|
rebuild_ldr_framebuffers(ctx);
|
|
|
|
// Enable or disable resample pass
|
|
if (viewport_size.x() != ctx.render_resolution.x() || viewport_size.y() != ctx.render_resolution.y())
|
|
{
|
|
ctx.resample_pass->set_enabled(true);
|
|
debug::log_debug("Resample pass enabled");
|
|
}
|
|
else
|
|
{
|
|
ctx.resample_pass->set_enabled(false);
|
|
debug::log_debug("Resample pass disabled");
|
|
}
|
|
|
|
reroute_framebuffers(ctx);
|
|
|
|
debug::log_trace("Changed render resolution to {}", scale);
|
|
}
|
|
|
|
void save_screenshot(::game& ctx)
|
|
{
|
|
// Determine timestamped screenshot filename
|
|
const auto time = std::chrono::floor<std::chrono::milliseconds>(std::chrono::system_clock::now());
|
|
const std::string screenshot_filename = std::format("{0}-{1:%Y%m%d}T{1:%H%M%S}Z.png", config::application_name, time);
|
|
|
|
// Determine path to screenshot file
|
|
std::filesystem::path screenshot_filepath = ctx.screenshots_path / screenshot_filename;
|
|
std::string screenshot_filepath_string = screenshot_filepath.string();
|
|
debug::log_debug("Saving screenshot to \"{}\"...", screenshot_filepath_string);
|
|
|
|
// Get viewport dimensions
|
|
const auto& viewport_size = ctx.window->get_viewport_size();
|
|
|
|
// Allocate screenshot pixel data buffer
|
|
std::unique_ptr<std::byte[]> frame = std::make_unique<std::byte[]>(viewport_size.x() * viewport_size.y() * 3);
|
|
|
|
// Read pixel data from backbuffer into pixel data buffer
|
|
glReadBuffer(GL_BACK);
|
|
glReadPixels(0, 0, viewport_size.x(), viewport_size.y(), GL_RGB, GL_UNSIGNED_BYTE, frame.get());
|
|
|
|
// Write screenshot file in separate thread
|
|
std::thread
|
|
(
|
|
[frame = std::move(frame), w = viewport_size.x(), h = viewport_size.y(), path = std::move(screenshot_filepath_string)]
|
|
{
|
|
stbi_flip_vertically_on_write(1);
|
|
stbi_write_png(path.c_str(), w, h, 3, frame.get(), w * 3);
|
|
|
|
debug::log_debug("Saved screenshot to \"{}\"", path);
|
|
}
|
|
).detach();
|
|
}
|
|
|
|
void select_anti_aliasing_method(::game& ctx, render::anti_aliasing_method method)
|
|
{
|
|
// Switch AA method
|
|
switch (method)
|
|
{
|
|
// Off
|
|
case render::anti_aliasing_method::none:
|
|
debug::log_debug("Anti-aliasing disabled");
|
|
reroute_framebuffers(ctx);
|
|
break;
|
|
|
|
// FXAA
|
|
case render::anti_aliasing_method::fxaa:
|
|
debug::log_debug("Anti-aliasing enabled (FXAA)");
|
|
reroute_framebuffers(ctx);
|
|
break;
|
|
}
|
|
|
|
// Update AA method setting
|
|
ctx.anti_aliasing_method = method;
|
|
}
|
|
|
|
void reroute_framebuffers(::game& ctx)
|
|
{
|
|
if (ctx.resample_pass->is_enabled())
|
|
{
|
|
ctx.common_final_pass->set_framebuffer(ctx.ldr_framebuffer_a.get());
|
|
}
|
|
else
|
|
{
|
|
ctx.common_final_pass->set_framebuffer(nullptr);
|
|
}
|
|
|
|
ctx.sky_pass->set_framebuffer(ctx.hdr_framebuffer.get());
|
|
ctx.surface_material_pass->set_framebuffer(ctx.hdr_framebuffer.get());
|
|
ctx.bloom_pass->set_source_texture(ctx.hdr_color_texture);
|
|
ctx.common_final_pass->set_color_texture(ctx.hdr_color_texture);
|
|
ctx.common_final_pass->set_bloom_texture(ctx.bloom_pass->get_bloom_texture());
|
|
}
|
|
|
|
} // namespace graphics
|