chore: 优化config设计,将光线追踪配置类直接作为RendererConfig成员变量
parent
08910e48d7
commit
fdbe96af40
Binary file not shown.
|
|
@ -299,7 +299,7 @@ void setup_cornell_box() {
|
||||||
g_scene->add_mesh(tall_box);
|
g_scene->add_mesh(tall_box);
|
||||||
|
|
||||||
// Metal sphere (replacing the glass box, positioned on the right side)
|
// Metal sphere (replacing the glass box, positioned on the right side)
|
||||||
auto metal_sphere = create_sphere(0.5f, 64, 32, /*metal_id*/white_id);
|
auto metal_sphere = create_sphere(0.5f, 32, 16, /*metal_id*/white_id);
|
||||||
metal_sphere->set_position(Vec3(0.55f, -1.5f, 0.35f));
|
metal_sphere->set_position(Vec3(0.55f, -1.5f, 0.35f));
|
||||||
metal_sphere->upload_to_gpu();
|
metal_sphere->upload_to_gpu();
|
||||||
g_scene->add_mesh(metal_sphere);
|
g_scene->add_mesh(metal_sphere);
|
||||||
|
|
@ -529,12 +529,12 @@ int main() {
|
||||||
|
|
||||||
ARE_LOG_INFO("Initializing renderer...");
|
ARE_LOG_INFO("Initializing renderer...");
|
||||||
RendererConfig config;
|
RendererConfig config;
|
||||||
config.width_ = WINDOW_WIDTH;
|
config.output_width = WINDOW_WIDTH;
|
||||||
config.height_ = WINDOW_HEIGHT;
|
config.output_height = WINDOW_HEIGHT;
|
||||||
config.samples_per_pixel_ = 1;
|
config.rt_config.samples_per_pixel = 1;
|
||||||
config.max_ray_depth_ = 4;
|
config.rt_config.max_depth = 4;
|
||||||
config.enable_accumulation_ = true;
|
config.rt_config.enable_accumulation = true;
|
||||||
config.enable_denoising_ = false;
|
config.enable_denoising = false;
|
||||||
|
|
||||||
g_renderer = std::make_unique<Renderer>(config);
|
g_renderer = std::make_unique<Renderer>(config);
|
||||||
if (!g_renderer->initialize()) {
|
if (!g_renderer->initialize()) {
|
||||||
|
|
|
||||||
|
|
@ -7,21 +7,12 @@
|
||||||
#include "resource/buffer.h"
|
#include "resource/buffer.h"
|
||||||
#include "resource/shader.h"
|
#include "resource/shader.h"
|
||||||
#include "scene/scene.h"
|
#include "scene/scene.h"
|
||||||
|
#include "utils/config.h"
|
||||||
#include <glad/glad.h>
|
#include <glad/glad.h>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
namespace are {
|
namespace are {
|
||||||
|
|
||||||
// Ray tracing configuration
|
|
||||||
struct RayTracerConfig {
|
|
||||||
uint samples_per_pixel_;
|
|
||||||
uint max_depth_;
|
|
||||||
bool enable_shadows_;
|
|
||||||
bool enable_reflections_;
|
|
||||||
bool enable_accumulation_;
|
|
||||||
bool use_bvh_;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Compute shader based ray tracer
|
// Compute shader based ray tracer
|
||||||
class RayTracer {
|
class RayTracer {
|
||||||
public:
|
public:
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,25 @@
|
||||||
|
|
||||||
namespace are {
|
namespace are {
|
||||||
|
|
||||||
|
// Ray tracing configuration
|
||||||
|
struct RayTracerConfig {
|
||||||
|
uint samples_per_pixel = 1;
|
||||||
|
uint max_depth = 4;
|
||||||
|
bool enable_shadows = true;
|
||||||
|
bool enable_reflections = true;
|
||||||
|
bool enable_accumulation = true;
|
||||||
|
bool use_bvh = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// Configuration struct for renderer
|
// Configuration struct for renderer
|
||||||
struct RendererConfig {
|
struct RendererConfig {
|
||||||
uint width_;
|
uint output_width;
|
||||||
uint height_;
|
uint output_height;
|
||||||
uint samples_per_pixel_;
|
RayTracerConfig rt_config;
|
||||||
uint max_ray_depth_;
|
bool enable_denoising;
|
||||||
bool enable_denoising_;
|
bool enable_sr; // Enable the super resolution mode
|
||||||
bool enable_accumulation_;
|
double sr_scaling; // The magnification of super-resolution
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace are
|
} // namespace are
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ bool RayTracer::initialize(const std::shared_ptr<Shader> &shader) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize BVH if enabled
|
// Initialize BVH if enabled
|
||||||
if (config_.use_bvh_) {
|
if (config_.use_bvh) {
|
||||||
bvh_ = std::make_unique<BVH>();
|
bvh_ = std::make_unique<BVH>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,7 +134,7 @@ void RayTracer::release() {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RayTracer::rebuild_bvh(const Scene &scene) {
|
bool RayTracer::rebuild_bvh(const Scene &scene) {
|
||||||
if (!config_.use_bvh_) {
|
if (!config_.use_bvh) {
|
||||||
ARE_LOG_WARN("BVH is disabled in configuration");
|
ARE_LOG_WARN("BVH is disabled in configuration");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -173,7 +173,7 @@ void RayTracer::trace(const Scene &scene, const GBuffer &gbuffer, TextureHandle
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build BVH if enabled and not built yet
|
// Build BVH if enabled and not built yet
|
||||||
if (config_.use_bvh_ && !bvh_built_) {
|
if (config_.use_bvh && !bvh_built_) {
|
||||||
rebuild_bvh(scene);
|
rebuild_bvh(scene);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,7 +222,7 @@ void RayTracer::trace(const Scene &scene, const GBuffer &gbuffer, TextureHandle
|
||||||
glBindImageTexture(4, accumulation_texture_, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
|
glBindImageTexture(4, accumulation_texture_, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
|
||||||
|
|
||||||
// Bind BVH buffers if enabled
|
// Bind BVH buffers if enabled
|
||||||
if (config_.use_bvh_ && bvh_built_) {
|
if (config_.use_bvh && bvh_built_) {
|
||||||
bvh_node_buffer_.bind_base(2);
|
bvh_node_buffer_.bind_base(2);
|
||||||
bvh_triangle_buffer_.bind_base(3);
|
bvh_triangle_buffer_.bind_base(3);
|
||||||
bvh_attr_buffer_.bind_base(4);
|
bvh_attr_buffer_.bind_base(4);
|
||||||
|
|
@ -234,10 +234,10 @@ void RayTracer::trace(const Scene &scene, const GBuffer &gbuffer, TextureHandle
|
||||||
|
|
||||||
// Set uniforms
|
// Set uniforms
|
||||||
compute_shader_->set_uint("u_frame_count", frame_count_);
|
compute_shader_->set_uint("u_frame_count", frame_count_);
|
||||||
compute_shader_->set_uint("u_samples_per_pixel", config_.samples_per_pixel_);
|
compute_shader_->set_uint("u_samples_per_pixel", config_.samples_per_pixel);
|
||||||
compute_shader_->set_uint("u_max_depth", config_.max_depth_);
|
compute_shader_->set_uint("u_max_depth", config_.max_depth);
|
||||||
compute_shader_->set_uint("u_light_count", static_cast<uint>(scene.get_lights().size()));
|
compute_shader_->set_uint("u_light_count", static_cast<uint>(scene.get_lights().size()));
|
||||||
compute_shader_->set_bool("u_enable_accumulation", config_.enable_accumulation_);
|
compute_shader_->set_bool("u_enable_accumulation", config_.enable_accumulation);
|
||||||
|
|
||||||
// Enable/disable textures based on material usage
|
// Enable/disable textures based on material usage
|
||||||
compute_shader_->set_bool("u_enable_textures", has_textures);
|
compute_shader_->set_bool("u_enable_textures", has_textures);
|
||||||
|
|
@ -257,7 +257,7 @@ void RayTracer::trace(const Scene &scene, const GBuffer &gbuffer, TextureHandle
|
||||||
glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
|
glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
|
||||||
|
|
||||||
// Increment frame count for accumulation
|
// Increment frame count for accumulation
|
||||||
if (config_.enable_accumulation_) {
|
if (config_.enable_accumulation) {
|
||||||
frame_count_++;
|
frame_count_++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -288,16 +288,16 @@ void RayTracer::reset_accumulation() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void RayTracer::set_config(const RayTracerConfig &config) {
|
void RayTracer::set_config(const RayTracerConfig &config) {
|
||||||
bool bvh_changed = (config.use_bvh_ != config_.use_bvh_);
|
bool bvh_changed = (config.use_bvh != config_.use_bvh);
|
||||||
|
|
||||||
config_ = config;
|
config_ = config;
|
||||||
reset_accumulation();
|
reset_accumulation();
|
||||||
|
|
||||||
if (bvh_changed) {
|
if (bvh_changed) {
|
||||||
if (config_.use_bvh_ && !bvh_) {
|
if (config_.use_bvh && !bvh_) {
|
||||||
bvh_ = std::make_unique<BVH>();
|
bvh_ = std::make_unique<BVH>();
|
||||||
bvh_built_ = false;
|
bvh_built_ = false;
|
||||||
} else if (!config_.use_bvh_) {
|
} else if (!config_.use_bvh) {
|
||||||
bvh_.reset();
|
bvh_.reset();
|
||||||
bvh_built_ = false;
|
bvh_built_ = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,54 +18,48 @@ Renderer::~Renderer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Renderer::initialize() {
|
bool Renderer::initialize() {
|
||||||
if (initialized_) {
|
if (initialized_) {
|
||||||
ARE_LOG_WARN("Renderer already initialized");
|
ARE_LOG_WARN("Renderer already initialized");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ARE_LOG_INFO("Initializing Aurora Rendering Engine...");
|
ARE_LOG_INFO("Initializing Aurora Rendering Engine...");
|
||||||
|
|
||||||
// Initialize shader manager
|
// Initialize shader manager
|
||||||
shader_manager_ = std::make_unique<ShaderManager>();
|
shader_manager_ = std::make_unique<ShaderManager>();
|
||||||
if (!shader_manager_->initialize()) {
|
if (!shader_manager_->initialize()) {
|
||||||
ARE_LOG_ERROR("Failed to initialize shader manager");
|
ARE_LOG_ERROR("Failed to initialize shader manager");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize G-Buffer
|
// Initialize G-Buffer
|
||||||
gbuffer_ = std::make_unique<GBuffer>(config_.width_, config_.height_);
|
gbuffer_ = std::make_unique<GBuffer>(config_.output_width, config_.output_height);
|
||||||
if (!gbuffer_->initialize()) {
|
if (!gbuffer_->initialize()) {
|
||||||
ARE_LOG_ERROR("Failed to initialize G-Buffer");
|
ARE_LOG_ERROR("Failed to initialize G-Buffer");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize ray tracer
|
|
||||||
RayTracerConfig rt_config;
|
|
||||||
rt_config.samples_per_pixel_ = config_.samples_per_pixel_;
|
|
||||||
rt_config.max_depth_ = config_.max_ray_depth_;
|
|
||||||
rt_config.enable_shadows_ = true;
|
|
||||||
rt_config.enable_reflections_ = true;
|
|
||||||
rt_config.enable_accumulation_ = config_.enable_accumulation_;
|
|
||||||
rt_config.use_bvh_ = true;
|
|
||||||
|
|
||||||
// Initialize ray tracer
|
// Initialize ray tracer
|
||||||
raytracer_ = std::make_unique<RayTracer>(config_.width_, config_.height_, rt_config);
|
RayTracerConfig rt_config = config_.rt_config;
|
||||||
const auto& rt_shader = shader_manager_->get_raytracing_shader();
|
|
||||||
if (!raytracer_->initialize(rt_shader)) {
|
|
||||||
ARE_LOG_ERROR("Failed to initialize ray tracer");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize screen blit
|
// Initialize ray tracer
|
||||||
screen_blit_ = std::make_unique<ScreenBlit>();
|
raytracer_ = std::make_unique<RayTracer>(config_.output_width, config_.output_height, rt_config);
|
||||||
const auto& screen_blit_shader = shader_manager_->get_screen_blit_shader();
|
const auto &rt_shader = shader_manager_->get_raytracing_shader();
|
||||||
if (!screen_blit_->initialize(screen_blit_shader)) {
|
if (!raytracer_->initialize(rt_shader)) {
|
||||||
ARE_LOG_ERROR("Failed to initialize screen blit");
|
ARE_LOG_ERROR("Failed to initialize ray tracer");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
denoiser_ = std::make_unique<Denoiser>(config_.width_, config_.height_);
|
// Initialize screen blit
|
||||||
const auto& denoise_shader = shader_manager_->get_denoise_shader();
|
screen_blit_ = std::make_unique<ScreenBlit>();
|
||||||
|
const auto &screen_blit_shader = shader_manager_->get_screen_blit_shader();
|
||||||
|
if (!screen_blit_->initialize(screen_blit_shader)) {
|
||||||
|
ARE_LOG_ERROR("Failed to initialize screen blit");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
denoiser_ = std::make_unique<Denoiser>(config_.output_width, config_.output_height);
|
||||||
|
const auto &denoise_shader = shader_manager_->get_denoise_shader();
|
||||||
if (!denoiser_->initialize(denoise_shader)) {
|
if (!denoiser_->initialize(denoise_shader)) {
|
||||||
ARE_LOG_ERROR("Failed to initialize denoiser");
|
ARE_LOG_ERROR("Failed to initialize denoiser");
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -73,7 +67,7 @@ bool Renderer::initialize() {
|
||||||
|
|
||||||
// Create ray tracing output texture (reused every frame)
|
// Create ray tracing output texture (reused every frame)
|
||||||
ResourceManager &rm = ResourceManager::instance();
|
ResourceManager &rm = ResourceManager::instance();
|
||||||
rt_output_texture_ = rm.create_texture(config_.width_, config_.height_, TextureFormat::RGBA32F);
|
rt_output_texture_ = rm.create_texture(config_.output_width, config_.output_height, TextureFormat::RGBA32F);
|
||||||
|
|
||||||
initialized_ = true;
|
initialized_ = true;
|
||||||
ARE_LOG_INFO("Aurora Rendering Engine initialized successfully");
|
ARE_LOG_INFO("Aurora Rendering Engine initialized successfully");
|
||||||
|
|
@ -103,79 +97,79 @@ void Renderer::shutdown() {
|
||||||
ARE_LOG_INFO("Aurora Rendering Engine shut down");
|
ARE_LOG_INFO("Aurora Rendering Engine shut down");
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderStats Renderer::render(const Scene& scene, TextureHandle output_texture) {
|
RenderStats Renderer::render(const Scene &scene, TextureHandle output_texture) {
|
||||||
RenderStats stats = {};
|
RenderStats stats = {};
|
||||||
|
|
||||||
if (!initialized_) {
|
if (!initialized_) {
|
||||||
ARE_LOG_ERROR("Renderer not initialized");
|
ARE_LOG_ERROR("Renderer not initialized");
|
||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start timing
|
// Start timing
|
||||||
auto start_time = std::chrono::high_resolution_clock::now();
|
auto start_time = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
// Phase 1: G-Buffer pass
|
// Phase 1: G-Buffer pass
|
||||||
auto gbuffer_start = std::chrono::high_resolution_clock::now();
|
auto gbuffer_start = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
const auto& gbuffer_shader = shader_manager_->get_gbuffer_shader();
|
const auto &gbuffer_shader = shader_manager_->get_gbuffer_shader();
|
||||||
if (!gbuffer_shader || !gbuffer_shader->is_valid()) {
|
if (!gbuffer_shader || !gbuffer_shader->is_valid()) {
|
||||||
ARE_LOG_ERROR("G-Buffer shader is invalid");
|
ARE_LOG_ERROR("G-Buffer shader is invalid");
|
||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
gbuffer_->render(scene, *gbuffer_shader);
|
gbuffer_->render(scene, *gbuffer_shader);
|
||||||
|
|
||||||
auto gbuffer_end = std::chrono::high_resolution_clock::now();
|
auto gbuffer_end = std::chrono::high_resolution_clock::now();
|
||||||
stats.gbuffer_time_ms_ = std::chrono::duration<float, std::milli>(gbuffer_end - gbuffer_start).count();
|
stats.gbuffer_time_ms_ = std::chrono::duration<float, std::milli>(gbuffer_end - gbuffer_start).count();
|
||||||
|
|
||||||
// Phase 2: Ray tracing pass
|
// Phase 2: Ray tracing pass
|
||||||
auto raytrace_start = std::chrono::high_resolution_clock::now();
|
auto raytrace_start = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
// Use output texture if provided, otherwise use internal texture
|
// Use output texture if provided, otherwise use internal texture
|
||||||
TextureHandle rt_output = (output_texture != 0) ? output_texture : rt_output_texture_;
|
TextureHandle rt_output = (output_texture != 0) ? output_texture : rt_output_texture_;
|
||||||
|
|
||||||
raytracer_->trace(scene, *gbuffer_, rt_output);
|
raytracer_->trace(scene, *gbuffer_, rt_output);
|
||||||
|
|
||||||
auto raytrace_end = std::chrono::high_resolution_clock::now();
|
auto raytrace_end = std::chrono::high_resolution_clock::now();
|
||||||
stats.raytrace_time_ms_ = std::chrono::duration<float, std::milli>(raytrace_end - raytrace_start).count();
|
stats.raytrace_time_ms_ = std::chrono::duration<float, std::milli>(raytrace_end - raytrace_start).count();
|
||||||
|
|
||||||
// Phase 3: Denoise texture
|
// Phase 3: Denoise texture
|
||||||
TextureHandle final_output = rt_output;
|
TextureHandle final_output = rt_output;
|
||||||
|
|
||||||
if (config_.enable_denoising_ && denoiser_) {
|
if (config_.enable_denoising && denoiser_) {
|
||||||
// Use temporal accumulation with weight 0.1 (10% blend of new frame)
|
// Use temporal accumulation with weight 0.1 (10% blend of new frame)
|
||||||
float temporal_weight = 0.1f;
|
float temporal_weight = 0.1f;
|
||||||
final_output = denoiser_->denoise(rt_output, 1, temporal_weight);
|
final_output = denoiser_->denoise(rt_output, 1, temporal_weight);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 4: Blit to screen if output is default framebuffer
|
// Phase 4: Blit to screen if output is default framebuffer
|
||||||
if (output_texture == 0) {
|
if (output_texture == 0) {
|
||||||
screen_blit_->blit_fullscreen(final_output);
|
screen_blit_->blit_fullscreen(final_output);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate total frame time
|
// Calculate total frame time
|
||||||
auto end_time = std::chrono::high_resolution_clock::now();
|
auto end_time = std::chrono::high_resolution_clock::now();
|
||||||
stats.frame_time_ms_ = std::chrono::duration<float, std::milli>(end_time - start_time).count();
|
stats.frame_time_ms_ = std::chrono::duration<float, std::milli>(end_time - start_time).count();
|
||||||
|
|
||||||
// Count triangles
|
// Count triangles
|
||||||
const auto& meshes = scene.get_meshes();
|
const auto &meshes = scene.get_meshes();
|
||||||
for (const auto& mesh : meshes) {
|
for (const auto &mesh : meshes) {
|
||||||
stats.triangle_count_ += mesh->get_indices().size() / 3;
|
stats.triangle_count_ += mesh->get_indices().size() / 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Estimate ray count (very rough)
|
// Estimate ray count (very rough)
|
||||||
stats.ray_count_ = config_.width_ * config_.height_ * config_.samples_per_pixel_ * config_.max_ray_depth_;
|
stats.ray_count_ = config_.output_width * config_.output_height * config_.rt_config.samples_per_pixel * config_.rt_config.max_depth;
|
||||||
|
|
||||||
frame_count_++;
|
frame_count_++;
|
||||||
|
|
||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Renderer::resize(uint width, uint height) {
|
void Renderer::resize(uint width, uint height) {
|
||||||
if (width == config_.width_ && height == config_.height_)
|
if (width == config_.output_width && height == config_.output_height)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
config_.width_ = width;
|
config_.output_width = width;
|
||||||
config_.height_ = height;
|
config_.output_height = height;
|
||||||
|
|
||||||
if (initialized_) {
|
if (initialized_) {
|
||||||
ResourceManager &rm = ResourceManager::instance();
|
ResourceManager &rm = ResourceManager::instance();
|
||||||
|
|
@ -195,21 +189,17 @@ void Renderer::resize(uint width, uint height) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Renderer::set_config(const RendererConfig &config) {
|
void Renderer::set_config(const RendererConfig &config) {
|
||||||
bool size_changed = (config.width_ != config_.width_ || config.height_ != config_.height_);
|
bool size_changed = (config.output_width != config_.output_width || config.output_height != config_.output_height);
|
||||||
|
|
||||||
config_ = config;
|
config_ = config;
|
||||||
|
|
||||||
if (initialized_) {
|
if (initialized_) {
|
||||||
if (size_changed) {
|
if (size_changed) {
|
||||||
resize(config_.width_, config_.height_);
|
resize(config_.output_width, config_.output_height);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update ray tracer config
|
// Update ray tracer config
|
||||||
RayTracerConfig rt_config = raytracer_->get_config();
|
raytracer_->set_config(config_.rt_config);
|
||||||
rt_config.samples_per_pixel_ = config_.samples_per_pixel_;
|
|
||||||
rt_config.max_depth_ = config_.max_ray_depth_;
|
|
||||||
rt_config.enable_accumulation_ = config_.enable_accumulation_;
|
|
||||||
raytracer_->set_config(rt_config);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue