From d0d97032dbe48e13ea19957caef700738c7b73d1 Mon Sep 17 00:00:00 2001 From: ternaryop8479 Date: Tue, 10 Feb 2026 17:10:59 +0800 Subject: [PATCH] =?UTF-8?q?Fix&Add=EF=BC=9A=E4=BF=AE=E5=A4=8D=E6=9C=80?= =?UTF-8?q?=E5=A4=9A=E5=8F=AA=E8=83=BD=E4=B8=8A=E4=BC=A0256=E4=B8=AAmateri?= =?UTF-8?q?al=E7=9A=84=E8=99=AB=EF=BC=8C=E6=B7=BB=E5=8A=A0BVH=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E4=BB=A3=E7=A0=81=E5=92=8C=E5=85=89=E7=BA=BF=E8=BF=BD?= =?UTF-8?q?=E8=B8=AA=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- all_files.md | 2820 +++++++++++++++++++++++++++++++++++ all_headers.md | 1762 ++++++++++++++++++++++ examples/cornell_box | Bin 410304 -> 410304 bytes examples/cornell_box.cpp | 2 +- include/basic/constants.h | 4 +- include/core/bvh.h | 18 + shaders/gbuffer.frag | 24 +- shaders/raytracing.comp | 854 +++++------ src/core/bvh.cpp | 55 +- src/core/gbuffer.cpp | 85 +- src/core/raytracer.cpp | 7 +- src/core/shader_manager.cpp | 11 +- write.sh | 54 + 13 files changed, 5129 insertions(+), 567 deletions(-) create mode 100644 all_files.md create mode 100644 all_headers.md create mode 100644 write.sh diff --git a/all_files.md b/all_files.md new file mode 100644 index 0000000..9121de8 --- /dev/null +++ b/all_files.md @@ -0,0 +1,2820 @@ +### 文件:src/basic/math.cpp + +```cpp +#include "basic/math.h" + +namespace are { + +Mat4 MathUtils::perspective(float fov, float aspect, float near, float far) { + return glm::perspective(fov, aspect, near, far); +} + +Mat4 MathUtils::look_at(const Vec3& eye, const Vec3& center, const Vec3& up) { + return glm::lookAt(eye, center, up); +} + +Vec3 MathUtils::normalize(const Vec3& v) { + return glm::normalize(v); +} + +float MathUtils::dot(const Vec3& a, const Vec3& b) { + return glm::dot(a, b); +} + +Vec3 MathUtils::cross(const Vec3& a, const Vec3& b) { + return glm::cross(a, b); +} + +Vec3 MathUtils::reflect(const Vec3& incident, const Vec3& normal) { + return glm::reflect(incident, normal); +} + +const float* MathUtils::value_ptr(const Mat4& mat) { + return glm::value_ptr(mat); +} + +} // namespace are +``` + +### 文件:src/core/bvh.cpp + +```cpp +#include "core/bvh.h" +#include "utils/logger.h" +#include "basic/constants.h" +#include +#include + +namespace are { + +// AABB implementation +void AABB::expand(const Vec3& point) { + min_ = glm::min(min_, point); + max_ = glm::max(max_, point); +} + +void AABB::expand(const AABB& other) { + min_ = glm::min(min_, other.min_); + max_ = glm::max(max_, other.max_); +} + +float AABB::surface_area() const { + Vec3 extent = max_ - min_; + return 2.0f * (extent.x * extent.y + extent.y * extent.z + extent.z * extent.x); +} + +bool AABB::is_valid() const { + return min_.x <= max_.x && min_.y <= max_.y && min_.z <= max_.z; +} + +// Triangle implementation +AABB Triangle::get_bounds() const { + AABB bounds(v0_, v0_); + bounds.expand(v1_); + bounds.expand(v2_); + return bounds; +} + +Vec3 Triangle::get_centroid() const { + return (v0_ + v1_ + v2_) / 3.0f; +} + +// BVH implementation +BVH::BVH() { +} + +BVH::~BVH() { + clear(); +} + +bool BVH::build(const std::vector>& meshes) { + clear(); + + Logger::info("Building BVH..."); + + // Extract all triangles from meshes + for (const auto& mesh : meshes) { + const auto& vertices = mesh->get_vertices(); + const auto& indices = mesh->get_indices(); + uint material_id = mesh->get_material(); + Mat4 transform = mesh->get_transform(); + + for (size_t i = 0; i < indices.size(); i += 3) { + Triangle tri; + + // Transform vertices + Vec4 v0 = transform * Vec4(vertices[indices[i]].position_, 1.0f); + Vec4 v1 = transform * Vec4(vertices[indices[i + 1]].position_, 1.0f); + Vec4 v2 = transform * Vec4(vertices[indices[i + 2]].position_, 1.0f); + + tri.v0_ = Vec3(v0) / v0.w; + tri.v1_ = Vec3(v1) / v1.w; + tri.v2_ = Vec3(v2) / v2.w; + + // Transform normals + Mat3 normal_matrix = glm::transpose(glm::inverse(Mat3(transform))); + tri.n0_ = glm::normalize(normal_matrix * vertices[indices[i]].normal_); + tri.n1_ = glm::normalize(normal_matrix * vertices[indices[i + 1]].normal_); + tri.n2_ = glm::normalize(normal_matrix * vertices[indices[i + 2]].normal_); + + // Copy UVs + tri.uv0_ = vertices[indices[i]].texcoord_; + tri.uv1_ = vertices[indices[i + 1]].texcoord_; + tri.uv2_ = vertices[indices[i + 2]].texcoord_; + + tri.material_id_ = material_id; + + triangles_.push_back(tri); + } + } + + if (triangles_.empty()) { + Logger::warning("No triangles to build BVH"); + return false; + } + + // Initialize triangle indices + triangle_indices_.resize(triangles_.size()); + for (size_t i = 0; i < triangles_.size(); ++i) { + triangle_indices_[i] = static_cast(i); + } + + // Reserve space for nodes (estimate) + nodes_.reserve(triangles_.size() * 2); + + // Create root node + nodes_.emplace_back(); + + // Build BVH recursively + build_recursive_(0, 0, static_cast(triangles_.size())); + + Logger::info("BVH built: " + std::to_string(nodes_.size()) + " nodes, " + + std::to_string(triangles_.size()) + " triangles"); + + return true; +} + +void BVH::build_recursive_(uint node_idx, uint first_prim, uint prim_count) { + BVHNode& node = nodes_[node_idx]; + + // Calculate bounds + AABB bounds = calculate_bounds_(first_prim, prim_count); + node.aabb_min_ = bounds.min_; + node.aabb_max_ = bounds.max_; + + // Leaf node threshold + const uint LEAF_SIZE = 4; + + if (prim_count <= LEAF_SIZE) { + // Create leaf node + node.left_first_ = first_prim; + node.count_ = prim_count; + return; + } + + // Find best split + int axis; + float split_pos; + float split_cost = find_best_split_(first_prim, prim_count, axis, split_pos); + + // Check if split is beneficial + float no_split_cost = prim_count * bounds.surface_area(); + if (split_cost >= no_split_cost) { + // Create leaf node + node.left_first_ = first_prim; + node.count_ = prim_count; + return; + } + + // Partition primitives + uint mid = first_prim; + for (uint i = first_prim; i < first_prim + prim_count; ++i) { + Triangle& tri = triangles_[triangle_indices_[i]]; + float centroid = tri.get_centroid()[axis]; + + if (centroid < split_pos) { + std::swap(triangle_indices_[i], triangle_indices_[mid]); + mid++; + } + } + + // Ensure we have primitives on both sides + if (mid == first_prim || mid == first_prim + prim_count) { + mid = first_prim + prim_count / 2; + } + + // Create interior node + uint left_count = mid - first_prim; + uint right_count = prim_count - left_count; + + node.left_first_ = static_cast(nodes_.size()); + node.count_ = 0; + + // Create child nodes + nodes_.emplace_back(); + nodes_.emplace_back(); + + // Recursively build children + build_recursive_(node.left_first_, first_prim, left_count); + build_recursive_(node.left_first_ + 1, mid, right_count); +} + +float BVH::find_best_split_(uint first_prim, uint prim_count, int& axis, float& split_pos) { + float best_cost = std::numeric_limits::max(); + + AABB centroid_bounds = calculate_centroid_bounds_(first_prim, prim_count); + + // Try each axis + for (int a = 0; a < 3; ++a) { + float extent = centroid_bounds.max_[a] - centroid_bounds.min_[a]; + if (extent < EPSILON) continue; + + // Try multiple split positions + const int NUM_BINS = 16; + for (int i = 1; i < NUM_BINS; ++i) { + float t = static_cast(i) / NUM_BINS; + float pos = centroid_bounds.min_[a] + t * extent; + + // Count primitives and calculate bounds for each side + AABB left_bounds, right_bounds; + uint left_count = 0, right_count = 0; + + for (uint j = first_prim; j < first_prim + prim_count; ++j) { + Triangle& tri = triangles_[triangle_indices_[j]]; + float centroid = tri.get_centroid()[a]; + + if (centroid < pos) { + left_bounds.expand(tri.get_bounds()); + left_count++; + } else { + right_bounds.expand(tri.get_bounds()); + right_count++; + } + } + + // Calculate SAH cost + if (left_count == 0 || right_count == 0) continue; + + float cost = left_count * left_bounds.surface_area() + + right_count * right_bounds.surface_area(); + + if (cost < best_cost) { + best_cost = cost; + axis = a; + split_pos = pos; + } + } + } + + return best_cost; +} + +AABB BVH::calculate_bounds_(uint first_prim, uint prim_count) { + AABB bounds{Vec3(std::numeric_limits::max()), + Vec3(std::numeric_limits::lowest())}; + + for (uint i = first_prim; i < first_prim + prim_count; ++i) { + Triangle& tri = triangles_[triangle_indices_[i]]; + bounds.expand(tri.get_bounds()); + } + + return bounds; +} + +AABB BVH::calculate_centroid_bounds_(uint first_prim, uint prim_count) { + AABB bounds{Vec3(std::numeric_limits::max()), + Vec3(std::numeric_limits::lowest())}; + + for (uint i = first_prim; i < first_prim + prim_count; ++i) { + Triangle& tri = triangles_[triangle_indices_[i]]; + bounds.expand(tri.get_centroid()); + } + + return bounds; +} + +bool BVH::upload_to_gpu(Buffer& node_buffer, Buffer& triangle_buffer) { + if (nodes_.empty() || triangles_.empty()) { + Logger::error("Cannot upload empty BVH to GPU"); + return false; + } + + // Reorder triangles according to BVH layout + std::vector ordered_triangles; + ordered_triangles.reserve(triangles_.size()); + + for (uint idx : triangle_indices_) { + ordered_triangles.push_back(triangles_[idx]); + } + + // Upload nodes + if (!node_buffer.create(BufferType::SHADER_STORAGE_BUFFER, + nodes_.size() * sizeof(BVHNode), + nodes_.data(), + BufferUsage::STATIC_DRAW)) { + Logger::error("Failed to upload BVH nodes to GPU"); + return false; + } + + // Upload triangles + if (!triangle_buffer.create(BufferType::SHADER_STORAGE_BUFFER, + ordered_triangles.size() * sizeof(Triangle), + ordered_triangles.data(), + BufferUsage::STATIC_DRAW)) { + Logger::error("Failed to upload BVH triangles to GPU"); + return false; + } + + Logger::info("BVH uploaded to GPU successfully"); + return true; +} + +void BVH::clear() { + nodes_.clear(); + triangles_.clear(); + triangle_indices_.clear(); +} + +} // namespace are +``` + +### 文件:src/core/gbuffer.cpp + +```cpp +#include "core/gbuffer.h" +#include "utils/logger.h" +#include + +namespace are { + +GBuffer::GBuffer(uint width, uint height) + : width_(width) + , height_(height) + , fbo_(INVALID_HANDLE) + , depth_texture_(INVALID_HANDLE) + , initialized_(false) { + for (int i = 0; i < GBUFFER_COUNT; ++i) { + textures_[i] = INVALID_HANDLE; + } +} + +GBuffer::~GBuffer() { + release(); +} + +bool GBuffer::initialize() { + if (initialized_) { + Logger::warning("GBuffer already initialized"); + return true; + } + + // Create framebuffer + glGenFramebuffers(1, &fbo_); + glBindFramebuffer(GL_FRAMEBUFFER, fbo_); + + // Create G-Buffer textures + textures_[GBUFFER_POSITION] = create_texture_(GL_RGBA32F, GL_RGBA, GL_FLOAT); + textures_[GBUFFER_NORMAL] = create_texture_(GL_RGBA32F, GL_RGBA, GL_FLOAT); + textures_[GBUFFER_ALBEDO] = create_texture_(GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE); + + // Attach textures to framebuffer + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + GBUFFER_POSITION, + GL_TEXTURE_2D, textures_[GBUFFER_POSITION], 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + GBUFFER_NORMAL, + GL_TEXTURE_2D, textures_[GBUFFER_NORMAL], 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + GBUFFER_ALBEDO, + GL_TEXTURE_2D, textures_[GBUFFER_ALBEDO], 0); + + // Create depth texture + glGenTextures(1, &depth_texture_); + glBindTexture(GL_TEXTURE_2D, depth_texture_); + glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, width_, height_, 0, + GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, + GL_TEXTURE_2D, depth_texture_, 0); + + // Set draw buffers + GLenum draw_buffers[GBUFFER_COUNT] = { + GL_COLOR_ATTACHMENT0 + GBUFFER_POSITION, + GL_COLOR_ATTACHMENT0 + GBUFFER_NORMAL, + GL_COLOR_ATTACHMENT0 + GBUFFER_ALBEDO + }; + glDrawBuffers(GBUFFER_COUNT, draw_buffers); + + // Check framebuffer completeness + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + Logger::error("GBuffer framebuffer is not complete"); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + return false; + } + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + initialized_ = true; + Logger::info("GBuffer initialized successfully"); + return true; +} + +void GBuffer::release() { + if (!initialized_) return; + + if (fbo_ != INVALID_HANDLE) { + glDeleteFramebuffers(1, &fbo_); + fbo_ = INVALID_HANDLE; + } + + for (int i = 0; i < GBUFFER_COUNT; ++i) { + if (textures_[i] != INVALID_HANDLE) { + glDeleteTextures(1, &textures_[i]); + textures_[i] = INVALID_HANDLE; + } + } + + if (depth_texture_ != INVALID_HANDLE) { + glDeleteTextures(1, &depth_texture_); + depth_texture_ = INVALID_HANDLE; + } + + initialized_ = false; + Logger::info("GBuffer released"); +} + +void GBuffer::render(const Scene& scene, const Shader& shader) { + if (!initialized_) { + Logger::error("GBuffer not initialized"); + return; + } + + // Bind framebuffer + glBindFramebuffer(GL_FRAMEBUFFER, fbo_); + glViewport(0, 0, width_, height_); + + // Clear buffers + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // Enable depth test + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); + + // Use shader + shader.use(); + + // Set camera matrices + const Camera& camera = scene.get_camera(); + Mat4 view = camera.get_view_matrix(); + Mat4 projection = camera.get_projection_matrix(); + + shader.set_mat4("u_view", view); + shader.set_mat4("u_projection", projection); + + // Render all meshes + const auto& meshes = scene.get_meshes(); + const auto& materials = scene.get_materials(); + + for (const auto& mesh : meshes) { + if (!mesh->is_uploaded()) { + Logger::warning("Mesh not uploaded to GPU, skipping"); + continue; + } + + // Set model matrix + Mat4 model = mesh->get_transform(); + shader.set_mat4("u_model", model); + + // Set material properties + uint material_id = mesh->get_material(); + if (material_id < materials.size()) { + const auto& material = materials[material_id]; + + shader.set_vec3("u_albedo", material->get_albedo()); + shader.set_float("u_metallic", material->get_metallic()); + shader.set_float("u_roughness", material->get_roughness()); + shader.set_uint("u_material_id", material_id); + + // Bind textures + auto albedo_tex = material->get_albedo_texture(); + if (albedo_tex && albedo_tex->is_valid()) { + albedo_tex->bind(0); + shader.set_int("u_albedo_map", 0); + shader.set_int("u_has_albedo_map", 1); + } else { + shader.set_int("u_has_albedo_map", 0); + } + + auto normal_tex = material->get_normal_texture(); + if (normal_tex && normal_tex->is_valid()) { + normal_tex->bind(1); + shader.set_int("u_normal_map", 1); + shader.set_int("u_has_normal_map", 1); + } else { + shader.set_int("u_has_normal_map", 0); + } + } + + // Draw mesh + glBindVertexArray(mesh->get_vao()); + glDrawElements(GL_TRIANGLES, mesh->get_indices().size(), GL_UNSIGNED_INT, 0); + glBindVertexArray(0); + } + + // Unbind framebuffer + glBindFramebuffer(GL_FRAMEBUFFER, 0); +} + +void GBuffer::resize(uint width, uint height) { + if (width == width_ && height == height_) return; + + width_ = width; + height_ = height; + + if (initialized_) { + release(); + initialize(); + } +} + +TextureHandle GBuffer::get_texture(int index) const { + if (index < 0 || index >= GBUFFER_COUNT) { + Logger::error("Invalid G-Buffer texture index"); + return INVALID_HANDLE; + } + return textures_[index]; +} + +void GBuffer::get_dimensions(uint& width, uint& height) const { + width = width_; + height = height_; +} + +TextureHandle GBuffer::create_texture_(uint internal_format, uint format, uint type) { + TextureHandle texture; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D(GL_TEXTURE_2D, 0, internal_format, width_, height_, 0, format, type, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + return texture; +} + +} // namespace are +``` + +### 文件:src/core/raytracer.cpp + +```cpp +#include "core/raytracer.h" +#include "utils/logger.h" +#include "basic/constants.h" +#include + +namespace are { + +RayTracer::RayTracer(uint width, uint height, const RayTracerConfig& config) + : width_(width) + , height_(height) + , config_(config) + , accumulation_texture_(INVALID_HANDLE) + , scene_buffer_(INVALID_HANDLE) + , material_buffer_(INVALID_HANDLE) + , light_buffer_(INVALID_HANDLE) + , bvh_(nullptr) + , bvh_built_(false) + , frame_count_(0) + , initialized_(false) { +} + +RayTracer::~RayTracer() { + release(); +} + +bool RayTracer::initialize() { + if (initialized_) { + Logger::warning("RayTracer already initialized"); + return true; + } + + // Create accumulation texture + glGenTextures(1, &accumulation_texture_); + glBindTexture(GL_TEXTURE_2D, accumulation_texture_); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width_, height_, 0, GL_RGBA, GL_FLOAT, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + // Create shader storage buffers + glGenBuffers(1, &material_buffer_); + glGenBuffers(1, &light_buffer_); + + // Load compute shader + Logger::info("Loading ray tracing compute shader in RayTracer..."); + if (!compute_shader_.load_compute("shaders/raytracing.comp")) { + Logger::error("Failed to load ray tracing compute shader in RayTracer"); + return false; + } + Logger::info("Ray tracing compute shader loaded in RayTracer"); + + // Initialize BVH if enabled + if (config_.use_bvh_) { + bvh_ = std::make_unique(); + } + + initialized_ = true; + Logger::info("RayTracer initialized successfully"); + return true; +} + +void RayTracer::release() { + if (!initialized_) return; + + if (accumulation_texture_ != INVALID_HANDLE) { + glDeleteTextures(1, &accumulation_texture_); + accumulation_texture_ = INVALID_HANDLE; + } + + if (material_buffer_ != INVALID_HANDLE) { + glDeleteBuffers(1, &material_buffer_); + material_buffer_ = INVALID_HANDLE; + } + + if (light_buffer_ != INVALID_HANDLE) { + glDeleteBuffers(1, &light_buffer_); + light_buffer_ = INVALID_HANDLE; + } + + bvh_node_buffer_.release(); + bvh_triangle_buffer_.release(); + + compute_shader_.release(); + + bvh_.reset(); + bvh_built_ = false; + + initialized_ = false; + Logger::info("RayTracer released"); +} + +bool RayTracer::rebuild_bvh(const Scene& scene) { + if (!config_.use_bvh_) { + Logger::warning("BVH is disabled in configuration"); + return false; + } + + if (!bvh_) { + bvh_ = std::make_unique(); + } + + Logger::info("Building BVH for ray tracing..."); + + if (!bvh_->build(scene.get_meshes())) { + Logger::error("Failed to build BVH"); + return false; + } + + if (!bvh_->upload_to_gpu(bvh_node_buffer_, bvh_triangle_buffer_)) { + Logger::error("Failed to upload BVH to GPU"); + return false; + } + + bvh_built_ = true; + Logger::info("BVH built and uploaded successfully"); + return true; +} + +void RayTracer::trace(const Scene& scene, const GBuffer& gbuffer, TextureHandle output_texture) { + if (!initialized_) { + Logger::error("RayTracer not initialized"); + return; + } + + if (!compute_shader_.is_valid()) { + Logger::error("Ray tracing compute shader not loaded"); + return; + } + + // Build BVH if enabled and not built yet + if (config_.use_bvh_ && !bvh_built_) { + rebuild_bvh(scene); + } + + // Upload scene data + upload_scene_data_(scene); + + // Use compute shader + compute_shader_.use(); + + // Bind G-Buffer textures + bind_gbuffer_(gbuffer); + + // Bind output and accumulation textures + glBindImageTexture(3, output_texture, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA32F); + glBindImageTexture(4, accumulation_texture_, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F); + + // Bind BVH buffers if enabled + if (config_.use_bvh_ && bvh_built_) { + bvh_node_buffer_.bind_base(2); + bvh_triangle_buffer_.bind_base(3); + compute_shader_.set_bool("u_use_bvh", true); + compute_shader_.set_uint("u_bvh_node_count", bvh_->get_node_count()); + } else { + compute_shader_.set_bool("u_use_bvh", false); + } + + // Set uniforms + 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_max_depth", config_.max_depth_); + compute_shader_.set_uint("u_light_count", static_cast(scene.get_lights().size())); + compute_shader_.set_bool("u_enable_accumulation", config_.enable_accumulation_); + + // Set camera data + const Camera& camera = scene.get_camera(); + compute_shader_.set_vec3("u_camera_position", camera.get_position()); + + Mat4 inv_vp = glm::inverse(camera.get_view_projection_matrix()); + compute_shader_.set_mat4("u_inv_view_projection", inv_vp); + + // Dispatch compute shader + uint num_groups_x = (width_ + COMPUTE_GROUP_SIZE_X - 1) / COMPUTE_GROUP_SIZE_X; + uint num_groups_y = (height_ + COMPUTE_GROUP_SIZE_Y - 1) / COMPUTE_GROUP_SIZE_Y; + + glDispatchCompute(num_groups_x, num_groups_y, 1); + + // Memory barrier + glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); + + // Increment frame count for accumulation + if (config_.enable_accumulation_) { + frame_count_++; + } +} + +void RayTracer::resize(uint width, uint height) { + if (width == width_ && height == height_) return; + + width_ = width; + height_ = height; + + if (initialized_) { + // Recreate accumulation texture + if (accumulation_texture_ != INVALID_HANDLE) { + glDeleteTextures(1, &accumulation_texture_); + } + + glGenTextures(1, &accumulation_texture_); + glBindTexture(GL_TEXTURE_2D, accumulation_texture_); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width_, height_, 0, GL_RGBA, GL_FLOAT, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + reset_accumulation(); + } +} + +void RayTracer::reset_accumulation() { + frame_count_ = 0; +} + +void RayTracer::set_config(const RayTracerConfig& config) { + bool bvh_changed = (config.use_bvh_ != config_.use_bvh_); + + config_ = config; + reset_accumulation(); + + if (bvh_changed) { + if (config_.use_bvh_ && !bvh_) { + bvh_ = std::make_unique(); + bvh_built_ = false; + } else if (!config_.use_bvh_) { + bvh_.reset(); + bvh_built_ = false; + } + } +} + +void RayTracer::upload_scene_data_(const Scene& scene) { + // Upload materials + const auto& materials = scene.get_materials(); + if (!materials.empty()) { + struct MaterialData { + Vec3 albedo; + float metallic; + Vec3 emission; + float roughness; + int type; + float ior; + Vec2 padding; + }; + + std::vector material_data; + material_data.reserve(materials.size()); + + for (const auto& mat : materials) { + MaterialData data; + data.albedo = mat->get_albedo(); + data.metallic = mat->get_metallic(); + data.emission = mat->get_emission(); + data.roughness = mat->get_roughness(); + data.type = static_cast(mat->get_type()); + data.ior = mat->get_ior(); + material_data.push_back(data); + } + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, material_buffer_); + glBufferData(GL_SHADER_STORAGE_BUFFER, + material_data.size() * sizeof(MaterialData), + material_data.data(), GL_DYNAMIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, material_buffer_); + } + + // Upload lights + const auto& lights = scene.get_lights(); + if (!lights.empty()) { + struct LightData { + Vec3 position; + int type; + Vec3 direction; + float intensity; + Vec3 color; + float range; + Vec2 spot_angles; + Vec2 padding; + }; + + std::vector light_data; + light_data.reserve(lights.size()); + + for (const auto& light : lights) { + LightData data; + data.position = light->get_position(); + data.type = static_cast(light->get_type()); + data.direction = light->get_direction(); + data.intensity = light->get_intensity(); + data.color = light->get_color(); + data.range = light->get_range(); + data.spot_angles = Vec2(light->get_inner_angle(), light->get_outer_angle()); + light_data.push_back(data); + } + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, light_buffer_); + glBufferData(GL_SHADER_STORAGE_BUFFER, + light_data.size() * sizeof(LightData), + light_data.data(), GL_DYNAMIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, light_buffer_); + } +} + +void RayTracer::bind_gbuffer_(const GBuffer& gbuffer) { + glBindImageTexture(0, gbuffer.get_texture(GBUFFER_POSITION), 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA32F); + glBindImageTexture(1, gbuffer.get_texture(GBUFFER_NORMAL), 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA32F); + glBindImageTexture(2, gbuffer.get_texture(GBUFFER_ALBEDO), 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8); +} + +void RayTracer::set_compute_shader(const Shader& shader) { + compute_shader_ = shader; + Logger::info("Compute shader set for RayTracer"); +} + +} // namespace are +``` + +### 文件:src/core/renderer.cpp + +```cpp +#include "core/renderer.h" +#include "utils/logger.h" +#include +#include + +namespace are { + +Renderer::Renderer(const RendererConfig &config) + : config_(config) + , initialized_(false) + , frame_count_(0) { +} + +Renderer::~Renderer() { + shutdown(); +} + +bool Renderer::initialize() { + if (initialized_) { + Logger::warning("Renderer already initialized"); + return true; + } + + Logger::info("Initializing Aurora Rendering Engine..."); + + // Initialize shader manager + shader_manager_ = std::make_unique(); + if (!shader_manager_->initialize()) { + Logger::error("Failed to initialize shader manager"); + return false; + } + + // Initialize G-Buffer + gbuffer_ = std::make_unique(config_.width_, config_.height_); + if (!gbuffer_->initialize()) { + Logger::error("Failed to initialize G-Buffer"); + 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; + + raytracer_ = std::make_unique(config_.width_, config_.height_, rt_config); + if (!raytracer_->initialize()) { + Logger::error("Failed to initialize ray tracer"); + return false; + } + + // Pass compute shader to ray tracer + const Shader& rt_shader = shader_manager_->get_raytracing_shader(); + if (!rt_shader.is_valid()) { + Logger::error("Ray tracing shader is invalid"); + return false; + } + raytracer_->set_compute_shader(rt_shader); + + // Initialize screen blit + screen_blit_ = std::make_unique(); + if (!screen_blit_->initialize()) { + Logger::error("Failed to initialize screen blit"); + return false; + } + + initialized_ = true; + Logger::info("Aurora Rendering Engine initialized successfully"); + return true; +} + +void Renderer::shutdown() { + if (!initialized_) + return; + + Logger::info("Shutting down Aurora Rendering Engine..."); + + if (screen_blit_) { + screen_blit_->release(); + screen_blit_.reset(); + } + + if (raytracer_) { + raytracer_->release(); + raytracer_.reset(); + } + + if (gbuffer_) { + gbuffer_->release(); + gbuffer_.reset(); + } + + if (shader_manager_) { + shader_manager_->release(); + shader_manager_.reset(); + } + + initialized_ = false; + Logger::info("Aurora Rendering Engine shut down"); +} + +RenderStats Renderer::render(const Scene& scene, TextureHandle output_texture) { + RenderStats stats = {}; + + if (!initialized_) { + Logger::error("Renderer not initialized"); + return stats; + } + + // Start timing + auto start_time = std::chrono::high_resolution_clock::now(); + + // Phase 1: G-Buffer pass + auto gbuffer_start = std::chrono::high_resolution_clock::now(); + + const Shader& gbuffer_shader = shader_manager_->get_gbuffer_shader(); + gbuffer_->render(scene, gbuffer_shader); + + auto gbuffer_end = std::chrono::high_resolution_clock::now(); + stats.gbuffer_time_ms_ = std::chrono::duration(gbuffer_end - gbuffer_start).count(); + + // Phase 2: Ray tracing pass + auto raytrace_start = std::chrono::high_resolution_clock::now(); + + // Create output texture if not provided + TextureHandle rt_output = output_texture; + bool created_temp_texture = false; + + if (rt_output == 0) { + glGenTextures(1, &rt_output); + glBindTexture(GL_TEXTURE_2D, rt_output); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, config_.width_, config_.height_, + 0, GL_RGBA, GL_FLOAT, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + created_temp_texture = true; + } + + raytracer_->trace(scene, *gbuffer_, rt_output); + + auto raytrace_end = std::chrono::high_resolution_clock::now(); + stats.raytrace_time_ms_ = std::chrono::duration(raytrace_end - raytrace_start).count(); + + // Phase 3: Blit to screen if output is default framebuffer + if (created_temp_texture && output_texture == 0) { + screen_blit_->blit_fullscreen(rt_output); + glDeleteTextures(1, &rt_output); + } + + // Calculate total frame time + auto end_time = std::chrono::high_resolution_clock::now(); + stats.frame_time_ms_ = std::chrono::duration(end_time - start_time).count(); + + // Count triangles + const auto& meshes = scene.get_meshes(); + for (const auto& mesh : meshes) { + stats.triangle_count_ += mesh->get_indices().size() / 3; + } + + // Estimate ray count (very rough) + stats.ray_count_ = config_.width_ * config_.height_ * config_.samples_per_pixel_ * config_.max_ray_depth_; + + frame_count_++; + + return stats; +} + +void Renderer::resize(uint width, uint height) { + if (width == config_.width_ && height == config_.height_) + return; + + config_.width_ = width; + config_.height_ = height; + + if (initialized_) { + gbuffer_->resize(width, height); + raytracer_->resize(width, height); + + Logger::info("Renderer resized to " + std::to_string(width) + "x" + std::to_string(height)); + } +} + +void Renderer::set_config(const RendererConfig &config) { + bool size_changed = (config.width_ != config_.width_ || config.height_ != config_.height_); + + config_ = config; + + if (initialized_) { + if (size_changed) { + resize(config_.width_, config_.height_); + } + + // Update ray tracer config + RayTracerConfig rt_config = raytracer_->get_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); + } +} + +} // namespace are +``` + +### 文件:src/core/screen_blit.cpp + +```cpp +#include "core/screen_blit.h" +#include "utils/logger.h" +#include + +namespace are { + +namespace { + const char* VERTEX_SHADER_SOURCE = R"( + #version 430 core + layout(location = 0) in vec2 a_position; + layout(location = 1) in vec2 a_texcoord; + + out vec2 v_texcoord; + + void main() { + v_texcoord = a_texcoord; + gl_Position = vec4(a_position, 0.0, 1.0); + } + )"; + + const char* FRAGMENT_SHADER_SOURCE = R"( + #version 430 core + in vec2 v_texcoord; + out vec4 frag_color; + + uniform sampler2D u_texture; + + void main() { + frag_color = texture(u_texture, v_texcoord); + } + )"; +} + +ScreenBlit::ScreenBlit() + : vao_(0) + , vbo_(0) + , initialized_(false) { +} + +ScreenBlit::~ScreenBlit() { + release(); +} + +bool ScreenBlit::initialize() { + if (initialized_) { + Logger::warning("ScreenBlit already initialized"); + return true; + } + + // Compile shader + if (!shader_.compile(VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE)) { + Logger::error("Failed to compile screen blit shader"); + return false; + } + + // Create fullscreen quad + create_quad_(); + + initialized_ = true; + Logger::info("ScreenBlit initialized successfully"); + return true; +} + +void ScreenBlit::release() { + if (!initialized_) return; + + shader_.release(); + + if (vao_ != 0) { + glDeleteVertexArrays(1, &vao_); + vao_ = 0; + } + + if (vbo_ != 0) { + glDeleteBuffers(1, &vbo_); + vbo_ = 0; + } + + initialized_ = false; +} + +void ScreenBlit::blit(TextureHandle texture, int x, int y, uint width, uint height) { + if (!initialized_) { + Logger::error("ScreenBlit not initialized"); + return; + } + + // Set viewport + glViewport(x, y, width, height); + + // Disable depth test + glDisable(GL_DEPTH_TEST); + + // Use shader + shader_.use(); + shader_.set_int("u_texture", 0); + + // Bind texture + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, texture); + + // Draw quad + glBindVertexArray(vao_); + glDrawArrays(GL_TRIANGLES, 0, 6); + glBindVertexArray(0); + + // Re-enable depth test + glEnable(GL_DEPTH_TEST); +} + +void ScreenBlit::blit_fullscreen(TextureHandle texture) { + GLint viewport[4]; + glGetIntegerv(GL_VIEWPORT, viewport); + blit(texture, viewport[0], viewport[1], viewport[2], viewport[3]); +} + +void ScreenBlit::create_quad_() { + // Fullscreen quad vertices (position + texcoord) + float vertices[] = { + // Position // TexCoord + -1.0f, -1.0f, 0.0f, 0.0f, + 1.0f, -1.0f, 1.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + + -1.0f, -1.0f, 0.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + -1.0f, 1.0f, 0.0f, 1.0f + }; + + glGenVertexArrays(1, &vao_); + glGenBuffers(1, &vbo_); + + glBindVertexArray(vao_); + glBindBuffer(GL_ARRAY_BUFFER, vbo_); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + + // Position attribute + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0); + + // TexCoord attribute + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float))); + + glBindVertexArray(0); +} + +} // namespace are +``` + +### 文件:src/core/shader_manager.cpp + +```cpp +#include "core/shader_manager.h" +#include "utils/logger.h" + +namespace are { + +ShaderManager::ShaderManager() + : initialized_(false) { +} + +ShaderManager::~ShaderManager() { + release(); +} + +bool ShaderManager::initialize() { + if (initialized_) { + Logger::warning("ShaderManager already initialized"); + return true; + } + + Logger::info("Loading built-in shaders..."); + + if (!load_builtin_shaders_()) { + Logger::error("Failed to load built-in shaders"); + return false; + } + + initialized_ = true; + Logger::info("ShaderManager initialized successfully"); + return true; +} + +void ShaderManager::release() { + if (!initialized_) return; + + gbuffer_shader_.release(); + raytracing_shader_.release(); + + for (auto& pair : shader_cache_) { + pair.second.release(); + } + shader_cache_.clear(); + + initialized_ = false; + Logger::info("ShaderManager released"); +} + +Shader ShaderManager::load_shader(const std::string& name, + const std::string& vertex_path, + const std::string& fragment_path) { + // Check cache + auto it = shader_cache_.find(name); + if (it != shader_cache_.end()) { + Logger::info("Shader '" + name + "' loaded from cache"); + return it->second; + } + + // Load shader + Shader shader; + if (!shader.load(vertex_path, fragment_path)) { + Logger::error("Failed to load shader '" + name + "'"); + return Shader(); + } + + shader_cache_[name] = shader; + Logger::info("Shader '" + name + "' loaded successfully"); + return shader; +} + +Shader ShaderManager::load_compute_shader(const std::string& name, + const std::string& compute_path) { + // Check cache + auto it = shader_cache_.find(name); + if (it != shader_cache_.end()) { + Logger::info("Compute shader '" + name + "' loaded from cache"); + return it->second; + } + + // Load shader + Shader shader; + if (!shader.load_compute(compute_path)) { + Logger::error("Failed to load compute shader '" + name + "'"); + return Shader(); + } + + shader_cache_[name] = shader; + Logger::info("Compute shader '" + name + "' loaded successfully"); + return shader; +} + +Shader ShaderManager::get_shader(const std::string& name) const { + auto it = shader_cache_.find(name); + if (it != shader_cache_.end()) { + return it->second; + } + + Logger::warning("Shader '" + name + "' not found in cache"); + return Shader(); +} + +bool ShaderManager::load_builtin_shaders_() { + // Load G-Buffer shader + if (!gbuffer_shader_.load("shaders/gbuffer.vert", "shaders/gbuffer.frag")) { + Logger::error("Failed to load G-Buffer shader"); + return false; + } + shader_cache_["gbuffer"] = gbuffer_shader_; + + // Load ray tracing compute shader + if (!raytracing_shader_.load_compute("shaders/raytracing.comp")) { + Logger::error("Failed to load ray tracing shader"); + return false; + } + shader_cache_["raytracing"] = raytracing_shader_; + + // Load ray tracing compute shader + Logger::info("Loading ray tracing compute shader..."); + if (!raytracing_shader_.load_compute("shaders/raytracing.comp")) { + Logger::error("Failed to load ray tracing shader"); + return false; + } + shader_cache_["raytracing"] = raytracing_shader_; + Logger::info("Ray tracing shader loaded successfully"); + + return true; +} + +} // namespace are +``` + +### 文件:src/scene/camera.cpp + +```cpp +#include "scene/camera.h" +#include "basic/math.h" +#include + +namespace are { + +Camera::Camera() + : position_(0.0f, 0.0f, 5.0f) + , target_(0.0f, 0.0f, 0.0f) + , up_(0.0f, 1.0f, 0.0f) + , projection_type_(ProjectionType::PERSPECTIVE) + , fov_(glm::radians(45.0f)) + , aspect_(16.0f / 9.0f) + , left_(-1.0f) + , right_(1.0f) + , bottom_(-1.0f) + , top_(1.0f) + , near_(0.1f) + , far_(100.0f) + , view_dirty_(true) + , projection_dirty_(true) { +} + +Camera::~Camera() { +} + +void Camera::set_perspective(float fov, float aspect, float near, float far) { + projection_type_ = ProjectionType::PERSPECTIVE; + fov_ = glm::radians(fov); + aspect_ = aspect; + near_ = near; + far_ = far; + projection_dirty_ = true; +} + +void Camera::set_orthographic(float left, float right, float bottom, float top, float near, float far) { + projection_type_ = ProjectionType::ORTHOGRAPHIC; + left_ = left; + right_ = right; + bottom_ = bottom; + top_ = top; + near_ = near; + far_ = far; + projection_dirty_ = true; +} + +void Camera::set_position(const Vec3& position) { + position_ = position; + view_dirty_ = true; +} + +void Camera::set_target(const Vec3& target) { + target_ = target; + view_dirty_ = true; +} + +void Camera::set_up(const Vec3& up) { + up_ = up; + view_dirty_ = true; +} + +Mat4 Camera::get_view_matrix() const { + if (view_dirty_) { + view_matrix_ = MathUtils::look_at(position_, target_, up_); + view_dirty_ = false; + } + return view_matrix_; +} + +Mat4 Camera::get_projection_matrix() const { + if (projection_dirty_) { + if (projection_type_ == ProjectionType::PERSPECTIVE) { + projection_matrix_ = MathUtils::perspective(fov_, aspect_, near_, far_); + } else { + projection_matrix_ = glm::ortho(left_, right_, bottom_, top_, near_, far_); + } + projection_dirty_ = false; + } + return projection_matrix_; +} + +Mat4 Camera::get_view_projection_matrix() const { + return get_projection_matrix() * get_view_matrix(); +} + +Vec3 Camera::get_forward() const { + return MathUtils::normalize(target_ - position_); +} + +Vec3 Camera::get_right() const { + Vec3 forward = get_forward(); + return MathUtils::normalize(MathUtils::cross(forward, up_)); +} + +Vec3 Camera::get_up() const { + Vec3 forward = get_forward(); + Vec3 right = get_right(); + return MathUtils::cross(right, forward); +} + +} // namespace are +``` + +### 文件:src/scene/material.cpp + +```cpp +#include "scene/material.h" + +namespace are { + +Material::Material() + : albedo_(1.0f, 1.0f, 1.0f) + , emission_(0.0f, 0.0f, 0.0f) + , metallic_(0.0f) + , roughness_(0.5f) + , ior_(1.5f) + , type_(MaterialType::DIFFUSE) + , albedo_texture_(nullptr) + , normal_texture_(nullptr) { +} + +Material::~Material() { +} + +void Material::set_albedo(const Vec3& albedo) { + albedo_ = albedo; +} + +void Material::set_emission(const Vec3& emission) { + emission_ = emission; +} + +void Material::set_metallic(float metallic) { + metallic_ = glm::clamp(metallic, 0.0f, 1.0f); +} + +void Material::set_roughness(float roughness) { + roughness_ = glm::clamp(roughness, 0.0f, 1.0f); +} + +void Material::set_ior(float ior) { + ior_ = ior; +} + +void Material::set_type(MaterialType type) { + type_ = type; +} + +void Material::set_albedo_texture(std::shared_ptr texture) { + albedo_texture_ = texture; +} + +void Material::set_normal_texture(std::shared_ptr texture) { + normal_texture_ = texture; +} + +} // namespace are +``` + +### 文件:src/scene/light.cpp + +```cpp +#include "scene/light.h" +#include + +namespace are { + +Light::Light() + : type_(LightType::POINT) + , position_(0.0f, 5.0f, 0.0f) + , direction_(0.0f, -1.0f, 0.0f) + , color_(1.0f, 1.0f, 1.0f) + , intensity_(1.0f) + , range_(10.0f) + , inner_angle_(glm::radians(30.0f)) + , outer_angle_(glm::radians(45.0f)) { +} + +Light::~Light() { +} + +void Light::set_type(LightType type) { + type_ = type; +} + +void Light::set_position(const Vec3& position) { + position_ = position; +} + +void Light::set_direction(const Vec3& direction) { + direction_ = glm::normalize(direction); +} + +void Light::set_color(const Vec3& color) { + color_ = color; +} + +void Light::set_intensity(float intensity) { + intensity_ = intensity; +} + +void Light::set_range(float range) { + range_ = range; +} + +void Light::set_spot_angles(float inner_angle, float outer_angle) { + inner_angle_ = glm::radians(inner_angle); + outer_angle_ = glm::radians(outer_angle); +} + +} // namespace are +``` + +### 文件:src/scene/scene.cpp + +```cpp +#include "scene/scene.h" + +namespace are { + +Scene::Scene() { + // Create default camera + camera_ = std::make_shared(); +} + +Scene::~Scene() { + clear(); +} + +uint Scene::add_mesh(std::shared_ptr mesh) { + meshes_.push_back(mesh); + return static_cast(meshes_.size() - 1); +} + +uint Scene::add_material(std::shared_ptr material) { + materials_.push_back(material); + return static_cast(materials_.size() - 1); +} + +uint Scene::add_light(std::shared_ptr light) { + lights_.push_back(light); + return static_cast(lights_.size() - 1); +} + +void Scene::set_camera(std::shared_ptr camera) { + camera_ = camera; +} + +void Scene::clear() { + meshes_.clear(); + materials_.clear(); + lights_.clear(); +} + +void Scene::update(float delta_time) { + // Reserved for future animation/physics updates + (void)delta_time; // Suppress unused parameter warning +} + +} // namespace are +``` + +### 文件:src/scene/mesh.cpp + +```cpp +#include "scene/mesh.h" +#include "utils/logger.h" +#include + +namespace are { + +Mesh::Mesh() + : material_id_(0) + , transform_(1.0f) + , vao_(0) + , vbo_(0) + , ebo_(0) + , uploaded_(false) { +} + +Mesh::~Mesh() { + release_gpu_resources(); +} + +void Mesh::set_vertices(const std::vector& vertices) { + vertices_ = vertices; + uploaded_ = false; +} + +void Mesh::set_indices(const std::vector& indices) { + indices_ = indices; + uploaded_ = false; +} + +void Mesh::set_material(uint material_id) { + material_id_ = material_id; +} + +void Mesh::set_transform(const Mat4& transform) { + transform_ = transform; +} + +bool Mesh::upload_to_gpu() { + if (uploaded_) { + Logger::warning("Mesh already uploaded to GPU"); + return true; + } + + if (vertices_.empty()) { + Logger::error("Cannot upload mesh: no vertices"); + return false; + } + + if (indices_.empty()) { + Logger::error("Cannot upload mesh: no indices"); + return false; + } + + // Generate VAO + glGenVertexArrays(1, &vao_); + glBindVertexArray(vao_); + + // Generate and upload VBO + glGenBuffers(1, &vbo_); + glBindBuffer(GL_ARRAY_BUFFER, vbo_); + glBufferData(GL_ARRAY_BUFFER, vertices_.size() * sizeof(Vertex), + vertices_.data(), GL_STATIC_DRAW); + + // Generate and upload EBO + glGenBuffers(1, &ebo_); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo_); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices_.size() * sizeof(uint), + indices_.data(), GL_STATIC_DRAW); + + // Set vertex attributes + // Location 0: Position + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), + (void*)offsetof(Vertex, position_)); + + // Location 1: Normal + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), + (void*)offsetof(Vertex, normal_)); + + // Location 2: TexCoord + glEnableVertexAttribArray(2); + glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), + (void*)offsetof(Vertex, texcoord_)); + + // Location 3: Tangent + glEnableVertexAttribArray(3); + glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), + (void*)offsetof(Vertex, tangent_)); + + glBindVertexArray(0); + + uploaded_ = true; + Logger::info("Mesh uploaded to GPU successfully"); + return true; +} + +void Mesh::release_gpu_resources() { + if (!uploaded_) return; + + if (vao_ != 0) { + glDeleteVertexArrays(1, &vao_); + vao_ = 0; + } + + if (vbo_ != 0) { + glDeleteBuffers(1, &vbo_); + vbo_ = 0; + } + + if (ebo_ != 0) { + glDeleteBuffers(1, &ebo_); + ebo_ = 0; + } + + uploaded_ = false; +} + +} // namespace are +``` + +### 文件:src/resource/buffer.cpp + +```cpp +#include "resource/buffer.h" +#include "utils/logger.h" +#include + +namespace are { + +namespace { + GLenum get_gl_buffer_type(BufferType type) { + switch (type) { + case BufferType::VERTEX_BUFFER: return GL_ARRAY_BUFFER; + case BufferType::INDEX_BUFFER: return GL_ELEMENT_ARRAY_BUFFER; + case BufferType::UNIFORM_BUFFER: return GL_UNIFORM_BUFFER; + case BufferType::SHADER_STORAGE_BUFFER: return GL_SHADER_STORAGE_BUFFER; + default: return GL_ARRAY_BUFFER; + } + } + + GLenum get_gl_usage(BufferUsage usage) { + switch (usage) { + case BufferUsage::STATIC_DRAW: return GL_STATIC_DRAW; + case BufferUsage::DYNAMIC_DRAW: return GL_DYNAMIC_DRAW; + case BufferUsage::STREAM_DRAW: return GL_STREAM_DRAW; + default: return GL_STATIC_DRAW; + } + } +} + +Buffer::Buffer() + : handle_(INVALID_HANDLE) + , type_(BufferType::VERTEX_BUFFER) + , size_(0) + , usage_(BufferUsage::STATIC_DRAW) { +} + +Buffer::~Buffer() { + // Don't auto-release, let user control lifetime +} + +bool Buffer::create(BufferType type, size_t size, const void* data, BufferUsage usage) { + if (handle_ != INVALID_HANDLE) { + Logger::warning("Buffer already created, releasing old buffer"); + release(); + } + + type_ = type; + size_ = size; + usage_ = usage; + + glGenBuffers(1, &handle_); + + GLenum gl_type = get_gl_buffer_type(type); + GLenum gl_usage = get_gl_usage(usage); + + glBindBuffer(gl_type, handle_); + glBufferData(gl_type, size, data, gl_usage); + glBindBuffer(gl_type, 0); + + Logger::info("Buffer created successfully"); + return true; +} + +void Buffer::update(size_t offset, size_t size, const void* data) { + if (handle_ == INVALID_HANDLE) { + Logger::error("Cannot update invalid buffer"); + return; + } + + if (offset + size > size_) { + Logger::error("Buffer update out of bounds"); + return; + } + + GLenum gl_type = get_gl_buffer_type(type_); + + glBindBuffer(gl_type, handle_); + glBufferSubData(gl_type, offset, size, data); + glBindBuffer(gl_type, 0); +} + +void Buffer::bind() const { + if (handle_ == INVALID_HANDLE) { + Logger::warning("Attempting to bind invalid buffer"); + return; + } + + GLenum gl_type = get_gl_buffer_type(type_); + glBindBuffer(gl_type, handle_); +} + +void Buffer::bind_base(uint binding_point) const { + if (handle_ == INVALID_HANDLE) { + Logger::warning("Attempting to bind invalid buffer"); + return; + } + + GLenum gl_type = get_gl_buffer_type(type_); + glBindBufferBase(gl_type, binding_point, handle_); +} + +void Buffer::unbind() const { + GLenum gl_type = get_gl_buffer_type(type_); + glBindBuffer(gl_type, 0); +} + +void Buffer::release() { + if (handle_ != INVALID_HANDLE) { + glDeleteBuffers(1, &handle_); + handle_ = INVALID_HANDLE; + } + + size_ = 0; +} + +} // namespace are +``` + +### 文件:src/resource/model_loader.cpp + +```cpp +#include "resource/model_loader.h" +#include "utils/logger.h" +#include "resource/texture.h" + +// Note: This is a simplified implementation without Assimp +// For full implementation, include Assimp and implement properly + +namespace are { + +bool ModelLoader::load(const std::string& path, + std::vector>& meshes, + std::vector>& materials, + bool flip_uvs) { + Logger::error("ModelLoader requires Assimp library (not implemented in this version)"); + Logger::info("To implement: include , , "); + + // Placeholder implementation + // TODO: Implement with Assimp + /* + Assimp::Importer importer; + const aiScene* scene = importer.ReadFile(path, + aiProcess_Triangulate | + aiProcess_GenNormals | + aiProcess_CalcTangentSpace | + (flip_uvs ? aiProcess_FlipUVs : 0)); + + if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) { + Logger::error("Failed to load model: " + std::string(importer.GetErrorString())); + return false; + } + + std::string directory = path.substr(0, path.find_last_of('/')); + process_node_(scene->mRootNode, scene, meshes, materials, directory); + + Logger::info("Model loaded: " + path); + return true; + */ + + return false; +} + +bool ModelLoader::load_and_upload(const std::string& path, + std::vector>& meshes, + std::vector>& materials, + bool flip_uvs) { + if (!load(path, meshes, materials, flip_uvs)) { + return false; + } + + // Upload all meshes to GPU + for (auto& mesh : meshes) { + if (!mesh->upload_to_gpu()) { + Logger::error("Failed to upload mesh to GPU"); + return false; + } + } + + return true; +} + +void ModelLoader::process_node_(void* node, void* scene, + std::vector>& meshes, + std::vector>& materials, + const std::string& directory) { + // TODO: Implement with Assimp + /* + aiNode* ai_node = static_cast(node); + const aiScene* ai_scene = static_cast(scene); + + // Process all meshes in this node + for (uint i = 0; i < ai_node->mNumMeshes; ++i) { + aiMesh* ai_mesh = ai_scene->mMeshes[ai_node->mMeshes[i]]; + meshes.push_back(process_mesh_(ai_mesh, ai_scene, materials, directory)); + } + + // Process children recursively + for (uint i = 0; i < ai_node->mNumChildren; ++i) { + process_node_(ai_node->mChildren[i], ai_scene, meshes, materials, directory); + } + */ +} + +std::shared_ptr ModelLoader::process_mesh_(void* mesh, void* scene, + std::vector>& materials, + const std::string& directory) { + // TODO: Implement with Assimp + /* + aiMesh* ai_mesh = static_cast(mesh); + const aiScene* ai_scene = static_cast(scene); + + std::vector vertices; + std::vector indices; + + // Process vertices + for (uint i = 0; i < ai_mesh->mNumVertices; ++i) { + Vertex vertex; + + vertex.position_ = Vec3(ai_mesh->mVertices[i].x, + ai_mesh->mVertices[i].y, + ai_mesh->mVertices[i].z); + + if (ai_mesh->HasNormals()) { + vertex.normal_ = Vec3(ai_mesh->mNormals[i].x, + ai_mesh->mNormals[i].y, + ai_mesh->mNormals[i].z); + } + + if (ai_mesh->mTextureCoords[0]) { + vertex.texcoord_ = Vec2(ai_mesh->mTextureCoords[0][i].x, + ai_mesh->mTextureCoords[0][i].y); + } + + if (ai_mesh->HasTangentsAndBitangents()) { + vertex.tangent_ = Vec3(ai_mesh->mTangents[i].x, + ai_mesh->mTangents[i].y, + ai_mesh->mTangents[i].z); + } + + vertices.push_back(vertex); + } + + // Process indices + for (uint i = 0; i < ai_mesh->mNumFaces; ++i) { + aiFace face = ai_mesh->mFaces[i]; + for (uint j = 0; j < face.mNumIndices; ++j) { + indices.push_back(face.mIndices[j]); + } + } + + // Process material + uint material_id = materials.size(); + if (ai_mesh->mMaterialIndex >= 0) { + aiMaterial* ai_material = ai_scene->mMaterials[ai_mesh->mMaterialIndex]; + + auto material = std::make_shared(); + + // Load diffuse color + aiColor3D color; + if (ai_material->Get(AI_MATKEY_COLOR_DIFFUSE, color) == AI_SUCCESS) { + material->set_albedo(Vec3(color.r, color.g, color.b)); + } + + // Load textures + load_material_textures_(ai_material, aiTextureType_DIFFUSE, material, directory); + load_material_textures_(ai_material, aiTextureType_NORMALS, material, directory); + + materials.push_back(material); + } + + auto mesh_obj = std::make_shared(); + mesh_obj->set_vertices(vertices); + mesh_obj->set_indices(indices); + mesh_obj->set_material(material_id); + + return mesh_obj; + */ + + return nullptr; +} + +void ModelLoader::load_material_textures_(void* material, int type, + std::shared_ptr& mat, + const std::string& directory) { + // TODO: Implement with Assimp + /* + aiMaterial* ai_material = static_cast(material); + aiTextureType ai_type = static_cast(type); + + for (uint i = 0; i < ai_material->GetTextureCount(ai_type); ++i) { + aiString str; + ai_material->GetTexture(ai_type, i, &str); + + std::string filename = directory + "/" + std::string(str.C_Str()); + + auto texture = std::make_shared(); + if (texture->load_from_file(filename)) { + if (ai_type == aiTextureType_DIFFUSE) { + mat->set_albedo_texture(texture); + } else if (ai_type == aiTextureType_NORMALS) { + mat->set_normal_texture(texture); + } + } + } + */ +} + +} // namespace are +``` + +### 文件:src/resource/shader.cpp + +```cpp +#include "resource/shader.h" +#include "utils/logger.h" +#include "basic/math.h" // 修改为math.h +#include +#include +#include + +namespace are { + +Shader::Shader() + : handle_(INVALID_HANDLE) { +} + +Shader::~Shader() { + // Don't auto-release, let user control lifetime +} + +bool Shader::load(const std::string& vertex_path, const std::string& fragment_path) { + std::string vertex_source = read_file_(vertex_path); + std::string fragment_source = read_file_(fragment_path); + + if (vertex_source.empty() || fragment_source.empty()) { + Logger::error("Failed to read shader files"); + return false; + } + + return compile(vertex_source, fragment_source); +} + +bool Shader::load_compute(const std::string& compute_path) { + std::string compute_source = read_file_(compute_path); + + if (compute_source.empty()) { + Logger::error("Failed to read compute shader file"); + return false; + } + + return compile_compute(compute_source); +} + +bool Shader::compile(const std::string& vertex_source, const std::string& fragment_source) { + uint vertex_shader = compile_shader_(vertex_source, GL_VERTEX_SHADER); + if (vertex_shader == 0) return false; + + uint fragment_shader = compile_shader_(fragment_source, GL_FRAGMENT_SHADER); + if (fragment_shader == 0) { + glDeleteShader(vertex_shader); + return false; + } + + uint shaders[] = { vertex_shader, fragment_shader }; + bool success = link_program_(shaders, 2); + + glDeleteShader(vertex_shader); + glDeleteShader(fragment_shader); + + return success; +} + +bool Shader::compile_compute(const std::string& compute_source) { + uint compute_shader = compile_shader_(compute_source, GL_COMPUTE_SHADER); + if (compute_shader == 0) return false; + + uint shaders[] = { compute_shader }; + bool success = link_program_(shaders, 1); + + glDeleteShader(compute_shader); + + return success; +} + +void Shader::use() const { // 改为const + if (handle_ != INVALID_HANDLE) { + glUseProgram(handle_); + } +} + +void Shader::release() { + if (handle_ != INVALID_HANDLE) { + glDeleteProgram(handle_); + handle_ = INVALID_HANDLE; + } + uniform_cache_.clear(); +} + +void Shader::set_bool(const std::string& name, bool value) const { // 新增 + glUniform1i(get_uniform_location_(name), static_cast(value)); +} + +void Shader::set_int(const std::string& name, int value) const { // 改为const + glUniform1i(get_uniform_location_(name), value); +} + +void Shader::set_uint(const std::string& name, uint value) const { // 改为const + glUniform1ui(get_uniform_location_(name), value); +} + +void Shader::set_float(const std::string& name, float value) const { // 改为const + glUniform1f(get_uniform_location_(name), value); +} + +void Shader::set_vec2(const std::string& name, const Vec2& value) const { // 改为const + glUniform2fv(get_uniform_location_(name), 1, &value[0]); +} + +void Shader::set_vec3(const std::string& name, const Vec3& value) const { // 改为const + glUniform3fv(get_uniform_location_(name), 1, &value[0]); +} + +void Shader::set_vec4(const std::string& name, const Vec4& value) const { // 改为const + glUniform4fv(get_uniform_location_(name), 1, &value[0]); +} + +void Shader::set_mat3(const std::string& name, const Mat3& value) const { // 改为const + glUniformMatrix3fv(get_uniform_location_(name), 1, GL_FALSE, &value[0][0]); +} + +void Shader::set_mat4(const std::string& name, const Mat4& value) const { // 改为const + glUniformMatrix4fv(get_uniform_location_(name), 1, GL_FALSE, MathUtils::value_ptr(value)); +} + +int Shader::get_uniform_location_(const std::string& name) const { // 改为const + auto it = uniform_cache_.find(name); + if (it != uniform_cache_.end()) { + return it->second; + } + + int location = glGetUniformLocation(handle_, name.c_str()); + uniform_cache_[name] = location; // mutable允许修改 + + if (location == -1) { + Logger::warning("Uniform '" + name + "' not found in shader"); + } + + return location; +} + +uint Shader::compile_shader_(const std::string& source, uint type) { + uint shader = glCreateShader(type); + const char* source_cstr = source.c_str(); + glShaderSource(shader, 1, &source_cstr, nullptr); + glCompileShader(shader); + + int success; + glGetShaderiv(shader, GL_COMPILE_STATUS, &success); + if (!success) { + char info_log[512]; + glGetShaderInfoLog(shader, 512, nullptr, info_log); + + std::string type_str = (type == GL_VERTEX_SHADER) ? "VERTEX" : + (type == GL_FRAGMENT_SHADER) ? "FRAGMENT" : "COMPUTE"; + Logger::error("Shader compilation failed (" + type_str + "): " + std::string(info_log)); + + glDeleteShader(shader); + return 0; + } + + return shader; +} + +bool Shader::link_program_(const uint* shaders, uint count) { + handle_ = glCreateProgram(); + + for (uint i = 0; i < count; ++i) { + glAttachShader(handle_, shaders[i]); + } + + glLinkProgram(handle_); + + int success; + glGetProgramiv(handle_, GL_LINK_STATUS, &success); + if (!success) { + char info_log[512]; + glGetProgramInfoLog(handle_, 512, nullptr, info_log); + Logger::error("Shader linking failed: " + std::string(info_log)); + + glDeleteProgram(handle_); + handle_ = INVALID_HANDLE; + return false; + } + + return true; +} + +std::string Shader::read_file_(const std::string& path) { + std::ifstream file(path); + if (!file.is_open()) { + Logger::error("Failed to open file: " + path); + return ""; + } + + std::stringstream buffer; + buffer << file.rdbuf(); + return buffer.str(); +} + +} // namespace are +``` + +### 文件:src/resource/texture.cpp + +```cpp +#include "resource/texture.h" +#include "utils/logger.h" +#include +#include + +namespace are { + +namespace { + GLenum get_gl_internal_format(TextureFormat format) { + switch (format) { + case TextureFormat::R8: return GL_R8; + case TextureFormat::RG8: return GL_RG8; + case TextureFormat::RGB8: return GL_RGB8; + case TextureFormat::RGBA8: return GL_RGBA8; + case TextureFormat::R16F: return GL_R16F; + case TextureFormat::RG16F: return GL_RG16F; + case TextureFormat::RGB16F: return GL_RGB16F; + case TextureFormat::RGBA16F: return GL_RGBA16F; + case TextureFormat::R32F: return GL_R32F; + case TextureFormat::RG32F: return GL_RG32F; + case TextureFormat::RGB32F: return GL_RGB32F; + case TextureFormat::RGBA32F: return GL_RGBA32F; + case TextureFormat::DEPTH24_STENCIL8: return GL_DEPTH24_STENCIL8; + default: return GL_RGBA8; + } + } + + GLenum get_gl_format(TextureFormat format) { + switch (format) { + case TextureFormat::R8: + case TextureFormat::R16F: + case TextureFormat::R32F: + return GL_RED; + case TextureFormat::RG8: + case TextureFormat::RG16F: + case TextureFormat::RG32F: + return GL_RG; + case TextureFormat::RGB8: + case TextureFormat::RGB16F: + case TextureFormat::RGB32F: + return GL_RGB; + case TextureFormat::RGBA8: + case TextureFormat::RGBA16F: + case TextureFormat::RGBA32F: + return GL_RGBA; + case TextureFormat::DEPTH24_STENCIL8: + return GL_DEPTH_STENCIL; + default: + return GL_RGBA; + } + } + + GLenum get_gl_type(TextureFormat format) { + switch (format) { + case TextureFormat::R8: + case TextureFormat::RG8: + case TextureFormat::RGB8: + case TextureFormat::RGBA8: + return GL_UNSIGNED_BYTE; + case TextureFormat::R16F: + case TextureFormat::RG16F: + case TextureFormat::RGB16F: + case TextureFormat::RGBA16F: + case TextureFormat::R32F: + case TextureFormat::RG32F: + case TextureFormat::RGB32F: + case TextureFormat::RGBA32F: + return GL_FLOAT; + case TextureFormat::DEPTH24_STENCIL8: + return GL_UNSIGNED_INT_24_8; + default: + return GL_UNSIGNED_BYTE; + } + } + + GLenum get_gl_filter(TextureFilter filter) { + switch (filter) { + case TextureFilter::NEAREST: return GL_NEAREST; + case TextureFilter::LINEAR: return GL_LINEAR; + case TextureFilter::NEAREST_MIPMAP_NEAREST: return GL_NEAREST_MIPMAP_NEAREST; + case TextureFilter::LINEAR_MIPMAP_NEAREST: return GL_LINEAR_MIPMAP_NEAREST; + case TextureFilter::NEAREST_MIPMAP_LINEAR: return GL_NEAREST_MIPMAP_LINEAR; + case TextureFilter::LINEAR_MIPMAP_LINEAR: return GL_LINEAR_MIPMAP_LINEAR; + default: return GL_LINEAR; + } + } + + GLenum get_gl_wrap(TextureWrap wrap) { + switch (wrap) { + case TextureWrap::REPEAT: return GL_REPEAT; + case TextureWrap::MIRRORED_REPEAT: return GL_MIRRORED_REPEAT; + case TextureWrap::CLAMP_TO_EDGE: return GL_CLAMP_TO_EDGE; + case TextureWrap::CLAMP_TO_BORDER: return GL_CLAMP_TO_BORDER; + default: return GL_REPEAT; + } + } +} + +Texture::Texture() + : handle_(INVALID_HANDLE) + , width_(0) + , height_(0) + , format_(TextureFormat::RGBA8) + , has_mipmaps_(false) { +} + +Texture::~Texture() { + // Don't auto-release, let user control lifetime +} + +bool Texture::load_from_file(const std::string& path, bool generate_mipmaps) { + // Load image using stb_image + int width, height, channels; + stbi_set_flip_vertically_on_load(true); + unsigned char* data = stbi_load(path.c_str(), &width, &height, &channels, 0); + + if (!data) { + Logger::error("Failed to load texture: " + path); + return false; + } + + // Determine format based on channels + TextureFormat format; + switch (channels) { + case 1: format = TextureFormat::R8; break; + case 2: format = TextureFormat::RG8; break; + case 3: format = TextureFormat::RGB8; break; + case 4: format = TextureFormat::RGBA8; break; + default: + Logger::error("Unsupported channel count: " + std::to_string(channels)); + stbi_image_free(data); + return false; + } + + // Create texture + bool success = create(width, height, format); + if (!success) { + stbi_image_free(data); + return false; + } + + // Upload data + success = upload(data, width, height, format); + stbi_image_free(data); + + if (!success) { + return false; + } + + // Generate mipmaps if requested + if (generate_mipmaps) { + this->generate_mipmaps(); + } + + Logger::info("Texture loaded successfully: " + path); + return true; +} + +bool Texture::create(uint width, uint height, TextureFormat format) { + if (handle_ != INVALID_HANDLE) { + Logger::warning("Texture already created, releasing old texture"); + release(); + } + + width_ = width; + height_ = height; + format_ = format; + + glGenTextures(1, &handle_); + glBindTexture(GL_TEXTURE_2D, handle_); + + GLenum internal_format = get_gl_internal_format(format); + GLenum gl_format = get_gl_format(format); + GLenum type = get_gl_type(format); + + glTexImage2D(GL_TEXTURE_2D, 0, internal_format, width, height, 0, gl_format, type, nullptr); + + // Set default parameters + 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_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + + glBindTexture(GL_TEXTURE_2D, 0); + + return true; +} + +bool Texture::upload(const void* data, uint width, uint height, TextureFormat format) { + if (handle_ == INVALID_HANDLE) { + Logger::error("Cannot upload to invalid texture"); + return false; + } + + if (width != width_ || height != height_ || format != format_) { + Logger::warning("Upload parameters differ from texture creation, recreating texture"); + create(width, height, format); + } + + glBindTexture(GL_TEXTURE_2D, handle_); + + GLenum gl_format = get_gl_format(format); + GLenum type = get_gl_type(format); + + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, gl_format, type, data); + + glBindTexture(GL_TEXTURE_2D, 0); + + return true; +} + +void Texture::set_filter(TextureFilter min_filter, TextureFilter mag_filter) { + if (handle_ == INVALID_HANDLE) { + Logger::error("Cannot set filter on invalid texture"); + return; + } + + glBindTexture(GL_TEXTURE_2D, handle_); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, get_gl_filter(min_filter)); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, get_gl_filter(mag_filter)); + glBindTexture(GL_TEXTURE_2D, 0); +} + +void Texture::set_wrap(TextureWrap wrap_s, TextureWrap wrap_t) { + if (handle_ == INVALID_HANDLE) { + Logger::error("Cannot set wrap mode on invalid texture"); + return; + } + + glBindTexture(GL_TEXTURE_2D, handle_); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, get_gl_wrap(wrap_s)); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, get_gl_wrap(wrap_t)); + glBindTexture(GL_TEXTURE_2D, 0); +} + +void Texture::generate_mipmaps() { + if (handle_ == INVALID_HANDLE) { + Logger::error("Cannot generate mipmaps for invalid texture"); + return; + } + + glBindTexture(GL_TEXTURE_2D, handle_); + glGenerateMipmap(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, 0); + + has_mipmaps_ = true; +} + +void Texture::bind(uint unit) const { + if (handle_ == INVALID_HANDLE) { + Logger::warning("Attempting to bind invalid texture"); + return; + } + + glActiveTexture(GL_TEXTURE0 + unit); + glBindTexture(GL_TEXTURE_2D, handle_); +} + +void Texture::unbind() const { + glBindTexture(GL_TEXTURE_2D, 0); +} + +void Texture::release() { + if (handle_ != INVALID_HANDLE) { + glDeleteTextures(1, &handle_); + handle_ = INVALID_HANDLE; + } + + width_ = 0; + height_ = 0; + has_mipmaps_ = false; +} + +} // namespace are +``` + +### 文件:src/utils/config.cpp + +```cpp +#include "utils/config.h" +#include "utils/logger.h" +#include +#include +#include + +namespace are { + +// Static storage +static std::unordered_map g_config_map; + +// Helper function to trim whitespace +static std::string trim(const std::string& str) { + size_t first = str.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) return ""; + size_t last = str.find_last_not_of(" \t\r\n"); + return str.substr(first, last - first + 1); +} + +bool Config::load(const std::string& path) { + std::ifstream file(path); + if (!file.is_open()) { + Logger::error("Failed to open config file: " + path); + return false; + } + + g_config_map.clear(); + + std::string line; + std::string current_section; + + while (std::getline(file, line)) { + line = trim(line); + + // Skip empty lines and comments + if (line.empty() || line[0] == '#' || line[0] == ';') { + continue; + } + + // Section header + if (line[0] == '[' && line.back() == ']') { + current_section = line.substr(1, line.length() - 2); + continue; + } + + // Key-value pair + size_t pos = line.find('='); + if (pos != std::string::npos) { + std::string key = trim(line.substr(0, pos)); + std::string value = trim(line.substr(pos + 1)); + + // Add section prefix if in a section + if (!current_section.empty()) { + key = current_section + "." + key; + } + + g_config_map[key] = value; + } + } + + Logger::info("Config loaded: " + path + " (" + std::to_string(g_config_map.size()) + " entries)"); + return true; +} + +bool Config::save(const std::string& path) { + std::ofstream file(path); + if (!file.is_open()) { + Logger::error("Failed to open config file for writing: " + path); + return false; + } + + for (const auto& pair : g_config_map) { + file << pair.first << "=" << pair.second << std::endl; + } + + Logger::info("Config saved: " + path); + return true; +} + +std::string Config::get_string(const std::string& key, const std::string& default_value) { + auto it = g_config_map.find(key); + if (it != g_config_map.end()) { + return it->second; + } + return default_value; +} + +int Config::get_int(const std::string& key, int default_value) { + auto it = g_config_map.find(key); + if (it != g_config_map.end()) { + try { + return std::stoi(it->second); + } catch (...) { + Logger::warning("Failed to parse int for key: " + key); + } + } + return default_value; +} + +float Config::get_float(const std::string& key, float default_value) { + auto it = g_config_map.find(key); + if (it != g_config_map.end()) { + try { + return std::stof(it->second); + } catch (...) { + Logger::warning("Failed to parse float for key: " + key); + } + } + return default_value; +} + +bool Config::get_bool(const std::string& key, bool default_value) { + auto it = g_config_map.find(key); + if (it != g_config_map.end()) { + std::string value = it->second; + std::transform(value.begin(), value.end(), value.begin(), ::tolower); + + if (value == "true" || value == "1" || value == "yes" || value == "on") { + return true; + } + if (value == "false" || value == "0" || value == "no" || value == "off") { + return false; + } + } + return default_value; +} + +void Config::set_string(const std::string& key, const std::string& value) { + g_config_map[key] = value; +} + +void Config::set_int(const std::string& key, int value) { + g_config_map[key] = std::to_string(value); +} + +void Config::set_float(const std::string& key, float value) { + g_config_map[key] = std::to_string(value); +} + +void Config::set_bool(const std::string& key, bool value) { + g_config_map[key] = value ? "true" : "false"; +} + +} // namespace are +``` + +### 文件:src/utils/logger.cpp + +```cpp +#include "utils/logger.h" +#include +#include +#include +#include +#include + +namespace are { + +// Static members +static LogLevel g_min_level = LogLevel::DEBUG; +static std::ofstream g_log_file; +static bool g_initialized = false; + +bool Logger::initialize(const std::string& log_file) { + if (g_initialized) { + return true; + } + + if (!log_file.empty()) { + g_log_file.open(log_file, std::ios::out | std::ios::app); + if (!g_log_file.is_open()) { + std::cerr << "Failed to open log file: " << log_file << std::endl; + return false; + } + } + + g_initialized = true; + return true; +} + +void Logger::shutdown() { + if (g_log_file.is_open()) { + g_log_file.close(); + } + g_initialized = false; +} + +static std::string get_current_time() { + auto now = std::time(nullptr); + auto tm = *std::localtime(&now); + std::ostringstream oss; + oss << std::put_time(&tm, "%H:%M:%S"); + return oss.str(); +} + +static std::string level_to_string(LogLevel level) { + switch (level) { + case LogLevel::DEBUG: return "DEBUG"; + case LogLevel::INFO: return "INFO"; + case LogLevel::WARNING: return "WARN"; + case LogLevel::ERROR: return "ERROR"; + case LogLevel::FATAL: return "FATAL"; + default: return "UNKNOWN"; + } +} + +void Logger::log(LogLevel level, const std::string& message) { + if (level < g_min_level) return; + + std::string time_str = get_current_time(); + std::string level_str = level_to_string(level); + std::string formatted = "[" + time_str + "] [" + level_str + "] " + message; + + // Console output + if (level >= LogLevel::ERROR) { + std::cerr << formatted << std::endl; + } else { + std::cout << formatted << std::endl; + } + + // File output + if (g_log_file.is_open()) { + g_log_file << formatted << std::endl; + g_log_file.flush(); + } +} + +void Logger::debug(const std::string& message) { + log(LogLevel::DEBUG, message); +} + +void Logger::info(const std::string& message) { + log(LogLevel::INFO, message); +} + +void Logger::warning(const std::string& message) { + log(LogLevel::WARNING, message); +} + +void Logger::error(const std::string& message) { + log(LogLevel::ERROR, message); +} + +void Logger::fatal(const std::string& message) { + log(LogLevel::FATAL, message); +} + +void Logger::set_level(LogLevel level) { + g_min_level = level; +} + +} // namespace are +``` + diff --git a/all_headers.md b/all_headers.md new file mode 100644 index 0000000..fd42fdd --- /dev/null +++ b/all_headers.md @@ -0,0 +1,1762 @@ +### 文件:include/basic/types.h + +```cpp +#ifndef ARE_INCLUDE_BASIC_TYPES_H +#define ARE_INCLUDE_BASIC_TYPES_H + +#include +#include +#include +#include +#include + +namespace are { + +/// @brief Basic vector types using GLM +using Vec2 = glm::vec2; +using Vec3 = glm::vec3; +using Vec4 = glm::vec4; + +/// @brief Basic matrix types using GLM +using Mat3 = glm::mat3; +using Mat4 = glm::mat4; + +/// @brief Basic integer types +using uint = uint32_t; +using uchar = uint8_t; + +/// @brief Handle types for GPU resources +using TextureHandle = uint; +using BufferHandle = uint; +using ShaderHandle = uint; +using FramebufferHandle = uint; + +/// @brief Invalid handle constant +constexpr uint INVALID_HANDLE = 0; + +/// @brief Vertex structure for mesh data +struct Vertex { + Vec3 position_; + Vec3 normal_; + Vec2 texcoord_; + Vec3 tangent_; +}; + +/// @brief Ray structure for ray tracing +struct Ray { + Vec3 origin_; + Vec3 direction_; + float t_min_; + float t_max_; +}; + +/// @brief Hit information for ray-surface intersection +struct HitInfo { + bool hit_; + float t_; + Vec3 position_; + Vec3 normal_; + Vec2 texcoord_; + uint material_id_; +}; + +/// @brief Rendering statistics +struct RenderStats { + float frame_time_ms_; + uint triangle_count_; + uint ray_count_; + float gbuffer_time_ms_; + float raytrace_time_ms_; +}; + +} // namespace are + +#endif // ARE_INCLUDE_BASIC_TYPES_H +``` + +### 文件:include/basic/constants.h + +```cpp +#ifndef ARE_INCLUDE_BASIC_CONSTANTS_H +#define ARE_INCLUDE_BASIC_CONSTANTS_H + +namespace are { + +/// @brief Maximum number of lights in scene +constexpr int MAX_LIGHTS = 16; + +/// @brief Maximum ray tracing depth +constexpr int MAX_RAY_DEPTH = 8; + +/// @brief Default samples per pixel for ray tracing +constexpr int DEFAULT_SPP = 1; + +/// @brief G-Buffer attachment indices +constexpr int GBUFFER_POSITION = 0; +constexpr int GBUFFER_NORMAL = 1; +constexpr int GBUFFER_ALBEDO = 2; +constexpr int GBUFFER_COUNT = 3; + +/// @brief Compute shader work group size +constexpr int COMPUTE_GROUP_SIZE_X = 16; +constexpr int COMPUTE_GROUP_SIZE_Y = 16; + +/// @brief Mathematical constants +constexpr float PI = 3.14159265359f; +constexpr float INV_PI = 0.31830988618f; +constexpr float EPSILON = 1e-4f; + +} // namespace are + +#endif // ARE_INCLUDE_BASIC_CONSTANTS_H +``` + +### 文件:include/basic/math.h + +```cpp +#ifndef ARE_INCLUDE_BASIC_MATH_UTILS_H +#define ARE_INCLUDE_BASIC_MATH_UTILS_H + +#include "types.h" +#include +#include + +namespace are { + +/// @brief Math utility functions wrapping GLM +class MathUtils { +public: + /// @brief Create perspective projection matrix + /// @param fov Field of view in radians + /// @param aspect Aspect ratio + /// @param near Near plane distance + /// @param far Far plane distance + /// @return Projection matrix + static Mat4 perspective(float fov, float aspect, float near, float far); + + /// @brief Create look-at view matrix + /// @param eye Camera position + /// @param center Look-at target + /// @param up Up vector + /// @return View matrix + static Mat4 look_at(const Vec3& eye, const Vec3& center, const Vec3& up); + + /// @brief Normalize a vector + /// @param v Input vector + /// @return Normalized vector + static Vec3 normalize(const Vec3& v); + + /// @brief Calculate dot product + /// @param a First vector + /// @param b Second vector + /// @return Dot product + static float dot(const Vec3& a, const Vec3& b); + + /// @brief Calculate cross product + /// @param a First vector + /// @param b Second vector + /// @return Cross product + static Vec3 cross(const Vec3& a, const Vec3& b); + + /// @brief Reflect vector around normal + /// @param incident Incident vector + /// @param normal Surface normal + /// @return Reflected vector + static Vec3 reflect(const Vec3& incident, const Vec3& normal); + + /// @brief Get pointer to matrix data (for OpenGL) + /// @param mat Input matrix + /// @return Pointer to matrix data + static const float* value_ptr(const Mat4& mat); +}; + +} // namespace are + +#endif // ARE_INCLUDE_BASIC_MATH_UTILS_H +``` + +### 文件:include/core/bvh.h + +```cpp +#ifndef ARE_INCLUDE_CORE_BVH_H +#define ARE_INCLUDE_CORE_BVH_H + +#include "basic/types.h" +#include "scene/mesh.h" +#include "resource/buffer.h" +#include +#include + +namespace are { + +/// @brief Axis-aligned bounding box +struct AABB { + Vec3 min_; + Vec3 max_; + + /// @brief Construct AABB from min and max points + AABB(const Vec3& min = Vec3(0.0f), const Vec3& max = Vec3(0.0f)) + : min_(min), max_(max) {} + + /// @brief Expand AABB to include point + void expand(const Vec3& point); + + /// @brief Expand AABB to include another AABB + void expand(const AABB& other); + + /// @brief Get center of AABB + Vec3 center() const { return (min_ + max_) * 0.5f; } + + /// @brief Get surface area of AABB + float surface_area() const; + + /// @brief Check if AABB is valid + bool is_valid() const; +}; + +/// @brief Triangle primitive for BVH +struct Triangle { + Vec3 v0_, v1_, v2_; + Vec3 n0_, n1_, n2_; + Vec2 uv0_, uv1_, uv2_; + uint material_id_; + + /// @brief Get bounding box of triangle + AABB get_bounds() const; + + /// @brief Get centroid of triangle + Vec3 get_centroid() const; +}; + +/// @brief BVH node for GPU +struct BVHNode { + Vec3 aabb_min_; + uint left_first_; // Left child index or first primitive index + Vec3 aabb_max_; + uint count_; // 0 for interior node, >0 for leaf node +}; + +/// @brief Bounding Volume Hierarchy for ray tracing acceleration +class BVH { +public: + /// @brief Constructor + BVH(); + + /// @brief Destructor + ~BVH(); + + /// @brief Build BVH from meshes + /// @param meshes Mesh list + /// @return True if build succeeded + bool build(const std::vector>& meshes); + + /// @brief Upload BVH to GPU + /// @param node_buffer Buffer for BVH nodes + /// @param triangle_buffer Buffer for triangles + /// @return True if upload succeeded + bool upload_to_gpu(Buffer& node_buffer, Buffer& triangle_buffer); + + /// @brief Get total node count + /// @return Node count + uint get_node_count() const { return static_cast(nodes_.size()); } + + /// @brief Get total triangle count + /// @return Triangle count + uint get_triangle_count() const { return static_cast(triangles_.size()); } + + /// @brief Clear BVH data + void clear(); + +private: + std::vector nodes_; + std::vector triangles_; + std::vector triangle_indices_; + + /// @brief Recursively build BVH + /// @param node_idx Current node index + /// @param first_prim First primitive index + /// @param prim_count Primitive count + void build_recursive_(uint node_idx, uint first_prim, uint prim_count); + + /// @brief Find best split using SAH + /// @param first_prim First primitive index + /// @param prim_count Primitive count + /// @param axis Split axis (output) + /// @param split_pos Split position (output) + /// @return Split cost + float find_best_split_(uint first_prim, uint prim_count, int& axis, float& split_pos); + + /// @brief Calculate node bounds + /// @param first_prim First primitive index + /// @param prim_count Primitive count + /// @return Bounding box + AABB calculate_bounds_(uint first_prim, uint prim_count); + + /// @brief Calculate centroid bounds + /// @param first_prim First primitive index + /// @param prim_count Primitive count + /// @return Centroid bounding box + AABB calculate_centroid_bounds_(uint first_prim, uint prim_count); +}; + +} // namespace are + +#endif // ARE_INCLUDE_CORE_BVH_H +``` + +### 文件:include/core/gbuffer.h + +```cpp +#ifndef ARE_INCLUDE_CORE_GBUFFER_H +#define ARE_INCLUDE_CORE_GBUFFER_H + +#include "basic/types.h" +#include "basic/constants.h" +#include "scene/scene.h" +#include "resource/shader.h" + +namespace are { + +/// @brief G-Buffer manager for deferred rendering +class GBuffer { +public: + /// @brief Constructor + /// @param width Buffer width + /// @param height Buffer height + GBuffer(uint width, uint height); + + /// @brief Destructor + ~GBuffer(); + + /// @brief Initialize G-Buffer (create framebuffer and textures) + /// @return True if initialization succeeded + bool initialize(); + + /// @brief Release G-Buffer resources + void release(); + + /// @brief Render scene to G-Buffer + /// @param scene Scene to render + /// @param shader Shader program for G-Buffer pass + void render(const Scene& scene, const Shader& shader); + + /// @brief Resize G-Buffer + /// @param width New width + /// @param height New height + void resize(uint width, uint height); + + /// @brief Get texture handle for specific buffer + /// @param index Buffer index (GBUFFER_POSITION, GBUFFER_NORMAL, etc.) + /// @return Texture handle + TextureHandle get_texture(int index) const; + + /// @brief Get framebuffer handle + /// @return Framebuffer handle + FramebufferHandle get_framebuffer() const { return fbo_; } + + /// @brief Get buffer dimensions + /// @param width Output width + /// @param height Output height + void get_dimensions(uint& width, uint& height) const; + +private: + uint width_; + uint height_; + FramebufferHandle fbo_; + TextureHandle textures_[GBUFFER_COUNT]; + TextureHandle depth_texture_; + + bool initialized_; + + /// @brief Create texture for G-Buffer attachment + /// @param internal_format OpenGL internal format + /// @param format OpenGL format + /// @param type OpenGL type + /// @return Texture handle + TextureHandle create_texture_(uint internal_format, uint format, uint type); +}; + +} // namespace are + +#endif // ARE_INCLUDE_CORE_GBUFFER_H +``` + +### 文件:include/core/raytracer.h + +```cpp +#ifndef ARE_INCLUDE_CORE_RAYTRACER_H +#define ARE_INCLUDE_CORE_RAYTRACER_H + +#include "basic/types.h" +#include "core/bvh.h" // 添加 +#include "core/gbuffer.h" +#include "resource/buffer.h" +#include "resource/shader.h" +#include "scene/scene.h" + +namespace are { + +/// @brief Ray tracing configuration +struct RayTracerConfig { + uint samples_per_pixel_; + uint max_depth_; + bool enable_shadows_; + bool enable_reflections_; + bool enable_accumulation_; + bool use_bvh_; // 添加BVH开关 +}; + +/// @brief Compute shader based ray tracer +class RayTracer { +public: + /// @brief Constructor + /// @param width Output width + /// @param height Output height + /// @param config Ray tracer configuration + RayTracer(uint width, uint height, const RayTracerConfig &config); + + /// @brief Destructor + ~RayTracer(); + + /// @brief Initialize ray tracer + /// @return True if initialization succeeded + bool initialize(); + + /// @brief Release resources + void release(); + + /// @brief Trace rays using G-Buffer as input + /// @param scene Scene data + /// @param gbuffer G-Buffer containing geometry information + /// @param output_texture Output texture for ray traced result + void trace(const Scene &scene, const GBuffer &gbuffer, TextureHandle output_texture); + + /// @brief Resize output + /// @param width New width + /// @param height New height + void resize(uint width, uint height); + + /// @brief Reset accumulation buffer + void reset_accumulation(); + + /// @brief Get current configuration + /// @return Current configuration + const RayTracerConfig &get_config() const { + return config_; + } + + /// @brief Update configuration + /// @param config New configuration + void set_config(const RayTracerConfig &config); + + /// @brief Rebuild BVH from scene + /// @param scene Scene to build BVH from + /// @return True if build succeeded + bool rebuild_bvh(const Scene &scene); + + /// @brief Set compute shader (called by renderer) + /// @param shader Compute shader + void set_compute_shader(const Shader &shader); + +private: + uint width_; + uint height_; + RayTracerConfig config_; + + Shader compute_shader_; + TextureHandle accumulation_texture_; + BufferHandle scene_buffer_; + BufferHandle material_buffer_; + BufferHandle light_buffer_; + + // BVH related + std::unique_ptr bvh_; // 添加 + Buffer bvh_node_buffer_; // 添加 + Buffer bvh_triangle_buffer_; // 添加 + bool bvh_built_; // 添加 + + uint frame_count_; + bool initialized_; + + /// @brief Upload scene data to GPU buffers + /// @param scene Scene to upload + void upload_scene_data_(const Scene &scene); + + /// @brief Bind G-Buffer textures to compute shader + /// @param gbuffer G-Buffer to bind + void bind_gbuffer_(const GBuffer &gbuffer); +}; + +} // namespace are + +#endif // ARE_INCLUDE_CORE_RAYTRACER_H +``` + +### 文件:include/core/renderer.h + +```cpp +#ifndef ARE_INCLUDE_CORE_RENDERER_H +#define ARE_INCLUDE_CORE_RENDERER_H + +#include "basic/types.h" +#include "scene/scene.h" +#include "core/gbuffer.h" +#include "core/raytracer.h" +#include "core/screen_blit.h" +#include "core/shader_manager.h" +#include + +namespace are { + +/// @brief Main renderer configuration +struct RendererConfig { + uint width_; + uint height_; + uint samples_per_pixel_; + uint max_ray_depth_; + bool enable_denoising_; + bool enable_accumulation_; +}; + +/// @brief Main rendering engine interface +class Renderer { +public: + /// @brief Constructor + /// @param config Renderer configuration + Renderer(const RendererConfig& config); + + /// @brief Destructor + ~Renderer(); + + /// @brief Initialize renderer (OpenGL context must be current) + /// @return True if initialization succeeded + bool initialize(); + + /// @brief Shutdown renderer and release resources + void shutdown(); + + /// @brief Render a frame + /// @param scene Scene to render + /// @param output_texture Output texture handle (0 for default framebuffer) + /// @return Rendering statistics + RenderStats render(const Scene& scene, TextureHandle output_texture = 0); + + /// @brief Resize render targets + /// @param width New width + /// @param height New height + void resize(uint width, uint height); + + /// @brief Get current configuration + /// @return Current configuration + const RendererConfig& get_config() const { return config_; } + + /// @brief Update configuration + /// @param config New configuration + void set_config(const RendererConfig& config); + +private: + RendererConfig config_; + std::unique_ptr gbuffer_; + std::unique_ptr raytracer_; + std::unique_ptr shader_manager_; + std::unique_ptr screen_blit_; + + bool initialized_; + uint frame_count_; +}; + +} // namespace are + +#endif // ARE_INCLUDE_CORE_RENDERER_H +``` + +### 文件:include/core/screen_blit.h + +```cpp +#ifndef ARE_INCLUDE_CORE_SCREEN_BLIT_H +#define ARE_INCLUDE_CORE_SCREEN_BLIT_H + +#include "basic/types.h" +#include "resource/shader.h" + +namespace are { + +/// @brief Screen blit utility for rendering texture to screen +class ScreenBlit { +public: + /// @brief Constructor + ScreenBlit(); + + /// @brief Destructor + ~ScreenBlit(); + + /// @brief Initialize screen blit + /// @return True if initialization succeeded + bool initialize(); + + /// @brief Release resources + void release(); + + /// @brief Blit texture to screen + /// @param texture Texture to blit + /// @param x Screen X position + /// @param y Screen Y position + /// @param width Blit width + /// @param height Blit height + void blit(TextureHandle texture, int x, int y, uint width, uint height); + + /// @brief Blit texture to full screen + /// @param texture Texture to blit + void blit_fullscreen(TextureHandle texture); + +private: + Shader shader_; + uint vao_; + uint vbo_; + bool initialized_; + + /// @brief Create fullscreen quad + void create_quad_(); +}; + +} // namespace are + +#endif // ARE_INCLUDE_CORE_SCREEN_BLIT_H +``` + +### 文件:include/core/shader_manager.h + +```cpp +#ifndef ARE_INCLUDE_CORE_SHADER_MANAGER_H +#define ARE_INCLUDE_CORE_SHADER_MANAGER_H + +#include "basic/types.h" +#include "resource/shader.h" +#include +#include + +namespace are { + +/// @brief Shader manager for loading and caching shaders +class ShaderManager { +public: + /// @brief Constructor + ShaderManager(); + + /// @brief Destructor + ~ShaderManager(); + + /// @brief Initialize shader manager and load built-in shaders + /// @return True if initialization succeeded + bool initialize(); + + /// @brief Release all shaders + void release(); + + /// @brief Load shader from files + /// @param name Shader name for caching + /// @param vertex_path Vertex shader file path + /// @param fragment_path Fragment shader file path + /// @return Shader object + Shader load_shader(const std::string& name, + const std::string& vertex_path, + const std::string& fragment_path); + + /// @brief Load compute shader from file + /// @param name Shader name for caching + /// @param compute_path Compute shader file path + /// @return Shader object + Shader load_compute_shader(const std::string& name, + const std::string& compute_path); + + /// @brief Get cached shader by name + /// @param name Shader name + /// @return Shader object (invalid if not found) + Shader get_shader(const std::string& name) const; + + /// @brief Get G-Buffer shader + /// @return G-Buffer shader + const Shader& get_gbuffer_shader() const { return gbuffer_shader_; } + + /// @brief Get ray tracing compute shader + /// @return Ray tracing shader + const Shader& get_raytracing_shader() const { return raytracing_shader_; } + +private: + std::unordered_map shader_cache_; + Shader gbuffer_shader_; + Shader raytracing_shader_; + + bool initialized_; + + /// @brief Load built-in shaders + /// @return True if loading succeeded + bool load_builtin_shaders_(); +}; + +} // namespace are + +#endif // ARE_INCLUDE_CORE_SHADER_MANAGER_H +``` + +### 文件:include/scene/camera.h + +```cpp +#ifndef ARE_INCLUDE_SCENE_CAMERA_H +#define ARE_INCLUDE_SCENE_CAMERA_H + +#include "basic/types.h" + +namespace are { + +/// @brief Camera projection type +enum class ProjectionType { + PERSPECTIVE, + ORTHOGRAPHIC +}; + +/// @brief Camera for rendering +class Camera { +public: + /// @brief Constructor + Camera(); + + /// @brief Destructor + ~Camera(); + + /// @brief Set perspective projection + /// @param fov Field of view in degrees + /// @param aspect Aspect ratio + /// @param near Near plane + /// @param far Far plane + void set_perspective(float fov, float aspect, float near, float far); + + /// @brief Set orthographic projection + /// @param left Left plane + /// @param right Right plane + /// @param bottom Bottom plane + /// @param top Top plane + /// @param near Near plane + /// @param far Far plane + void set_orthographic(float left, float right, float bottom, float top, float near, float far); + + /// @brief Set camera position + /// @param position Position + void set_position(const Vec3& position); + + /// @brief Set camera target + /// @param target Target position + void set_target(const Vec3& target); + + /// @brief Set camera up vector + /// @param up Up vector + void set_up(const Vec3& up); + + /// @brief Get view matrix + /// @return View matrix + Mat4 get_view_matrix() const; + + /// @brief Get projection matrix + /// @return Projection matrix + Mat4 get_projection_matrix() const; + + /// @brief Get view-projection matrix + /// @return View-projection matrix + Mat4 get_view_projection_matrix() const; + + /// @brief Get camera position + /// @return Position + const Vec3& get_position() const { return position_; } + + /// @brief Get camera forward direction + /// @return Forward direction + Vec3 get_forward() const; + + /// @brief Get camera right direction + /// @return Right direction + Vec3 get_right() const; + + /// @brief Get camera up direction + /// @return Up direction + Vec3 get_up() const; + +private: + Vec3 position_; + Vec3 target_; + Vec3 up_; + + ProjectionType projection_type_; + + // Perspective parameters + float fov_; + float aspect_; + + // Orthographic parameters + float left_, right_, bottom_, top_; + + // Common parameters + float near_; + float far_; + + mutable Mat4 view_matrix_; + mutable Mat4 projection_matrix_; + mutable bool view_dirty_; + mutable bool projection_dirty_; +}; + +} // namespace are + +#endif // ARE_INCLUDE_SCENE_CAMERA_H +``` + +### 文件:include/scene/material.h + +```cpp +#ifndef ARE_INCLUDE_SCENE_MATERIAL_H +#define ARE_INCLUDE_SCENE_MATERIAL_H + +#include "basic/types.h" +#include "resource/texture.h" +#include + +namespace are { + +/// @brief Material type enumeration +enum class MaterialType { + DIFFUSE = 0, + METAL = 1, + DIELECTRIC = 2, + EMISSIVE = 3 +}; + +/// @brief Material properties +class Material { +public: + /// @brief Constructor + Material(); + + /// @brief Destructor + ~Material(); + + /// @brief Set albedo color + /// @param albedo Albedo color + void set_albedo(const Vec3& albedo); + + /// @brief Set emission color + /// @param emission Emission color + void set_emission(const Vec3& emission); + + /// @brief Set metallic value + /// @param metallic Metallic (0-1) + void set_metallic(float metallic); + + /// @brief Set roughness value + /// @param roughness Roughness (0-1) + void set_roughness(float roughness); + + /// @brief Set index of refraction + /// @param ior Index of refraction + void set_ior(float ior); + + /// @brief Set material type + /// @param type Material type + void set_type(MaterialType type); + + /// @brief Set albedo texture + /// @param texture Albedo texture + void set_albedo_texture(std::shared_ptr texture); + + /// @brief Set normal map + /// @param texture Normal map texture + void set_normal_texture(std::shared_ptr texture); + + /// @brief Get albedo color + /// @return Albedo color + const Vec3& get_albedo() const { return albedo_; } + + /// @brief Get emission color + /// @return Emission color + const Vec3& get_emission() const { return emission_; } + + /// @brief Get metallic value + /// @return Metallic + float get_metallic() const { return metallic_; } + + /// @brief Get roughness value + /// @return Roughness + float get_roughness() const { return roughness_; } + + /// @brief Get index of refraction + /// @return IOR + float get_ior() const { return ior_; } + + /// @brief Get material type + /// @return Material type + MaterialType get_type() const { return type_; } + + /// @brief Get albedo texture + /// @return Albedo texture (nullptr if none) + std::shared_ptr get_albedo_texture() const { return albedo_texture_; } + + /// @brief Get normal texture + /// @return Normal texture (nullptr if none) + std::shared_ptr get_normal_texture() const { return normal_texture_; } + +private: + Vec3 albedo_; + Vec3 emission_; + float metallic_; + float roughness_; + float ior_; + MaterialType type_; + + std::shared_ptr albedo_texture_; + std::shared_ptr normal_texture_; +}; + +} // namespace are + +#endif // ARE_INCLUDE_SCENE_MATERIAL_H +``` + +### 文件:include/scene/light.h + +```cpp +#ifndef ARE_INCLUDE_SCENE_LIGHT_H +#define ARE_INCLUDE_SCENE_LIGHT_H + +#include "basic/types.h" + +namespace are { + +/// @brief Light type enumeration +enum class LightType { + DIRECTIONAL = 0, + POINT = 1, + SPOT = 2 +}; + +/// @brief Light source +class Light { +public: + /// @brief Constructor + Light(); + + /// @brief Destructor + ~Light(); + + /// @brief Set light type + /// @param type Light type + void set_type(LightType type); + + /// @brief Set light position (for point and spot lights) + /// @param position Light position + void set_position(const Vec3 &position); + + /// @brief Set light direction (for directional and spot lights) + /// @param direction Light direction + void set_direction(const Vec3 &direction); + + /// @brief Set light color + /// @param color Light color + void set_color(const Vec3 &color); + + /// @brief Set light intensity + /// @param intensity Light intensity + void set_intensity(float intensity); + + /// @brief Set light range (for point and spot lights) + /// @param range Light range + void set_range(float range); + + /// @brief Set spot light angles + /// @param inner_angle Inner cone angle in degrees + /// @param outer_angle Outer cone angle in degrees + void set_spot_angles(float inner_angle, float outer_angle); + + /// @brief Get light type + /// @return Light type + LightType get_type() const { + return type_; + } + + /// @brief Get light position + /// @return Light position + const Vec3 &get_position() const { + return position_; + } + + /// @brief Get light direction + /// @return Light direction + const Vec3 &get_direction() const { + return direction_; + } + + /// @brief Get light color + /// @return Light color + const Vec3 &get_color() const { + return color_; + } + + /// @brief Get light intensity + /// @return Light intensity + float get_intensity() const { + return intensity_; + } + + /// @brief Get light range + /// @return Light range + float get_range() const { + return range_; + } + + /// @brief Get spot light inner angle + /// @return Inner angle in radians + float get_inner_angle() const { + return inner_angle_; + } + + /// @brief Get spot light outer angle + /// @return Outer angle in radians + float get_outer_angle() const { + return outer_angle_; + } + +private: + LightType type_; + Vec3 position_; + Vec3 direction_; + Vec3 color_; + float intensity_; + float range_; + float inner_angle_; + float outer_angle_; +}; + +} // namespace are + +#endif // ARE_INCLUDE_SCENE_LIGHT_H +``` + +### 文件:include/scene/scene.h + +```cpp +#ifndef ARE_INCLUDE_SCENE_SCENE_H +#define ARE_INCLUDE_SCENE_SCENE_H + +#include "basic/types.h" +#include "scene/camera.h" +#include "scene/mesh.h" +#include "scene/material.h" +#include "scene/light.h" +#include +#include + +namespace are { + +/// @brief Scene container holding all scene objects +class Scene { +public: + /// @brief Constructor + Scene(); + + /// @brief Destructor + ~Scene(); + + /// @brief Add mesh to scene + /// @param mesh Mesh to add + /// @return Mesh index + uint add_mesh(std::shared_ptr mesh); + + /// @brief Add material to scene + /// @param material Material to add + /// @return Material index + uint add_material(std::shared_ptr material); + + /// @brief Add light to scene + /// @param light Light to add + /// @return Light index + uint add_light(std::shared_ptr light); + + /// @brief Set active camera + /// @param camera Camera to set + void set_camera(std::shared_ptr camera); + + /// @brief Get active camera + /// @return Active camera + const Camera& get_camera() const { return *camera_; } + + /// @brief Get all meshes + /// @return Mesh list + const std::vector>& get_meshes() const { return meshes_; } + + /// @brief Get all materials + /// @return Material list + const std::vector>& get_materials() const { return materials_; } + + /// @brief Get all lights + /// @return Light list + const std::vector>& get_lights() const { return lights_; } + + /// @brief Clear all scene objects + void clear(); + + /// @brief Update scene (animations, transforms, etc.) + /// @param delta_time Time since last update + void update(float delta_time); + +private: + std::shared_ptr camera_; + std::vector> meshes_; + std::vector> materials_; + std::vector> lights_; +}; + +} // namespace are + +#endif // ARE_INCLUDE_SCENE_SCENE_H +``` + +### 文件:include/scene/mesh.h + +```cpp +#ifndef ARE_INCLUDE_SCENE_MESH_H +#define ARE_INCLUDE_SCENE_MESH_H + +#include "basic/types.h" +#include + +namespace are { + +/// @brief Mesh data container +class Mesh { +public: + /// @brief Constructor + Mesh(); + + /// @brief Destructor + ~Mesh(); + + /// @brief Set vertex data + /// @param vertices Vertex array + void set_vertices(const std::vector& vertices); + + /// @brief Set index data + /// @param indices Index array + void set_indices(const std::vector& indices); + + /// @brief Set material index + /// @param material_id Material index + void set_material(uint material_id); + + /// @brief Set transform matrix + /// @param transform Transform matrix + void set_transform(const Mat4& transform); + + /// @brief Get vertices + /// @return Vertex array + const std::vector& get_vertices() const { return vertices_; } + + /// @brief Get indices + /// @return Index array + const std::vector& get_indices() const { return indices_; } + + /// @brief Get material index + /// @return Material index + uint get_material() const { return material_id_; } + + /// @brief Get transform matrix + /// @return Transform matrix + const Mat4& get_transform() const { return transform_; } + + /// @brief Upload mesh data to GPU + /// @return True if upload succeeded + bool upload_to_gpu(); + + /// @brief Release GPU resources + void release_gpu_resources(); + + /// @brief Get VAO handle + /// @return VAO handle + uint get_vao() const { return vao_; } + + /// @brief Check if mesh is uploaded to GPU + /// @return True if uploaded + bool is_uploaded() const { return uploaded_; } + +private: + std::vector vertices_; + std::vector indices_; + uint material_id_; + Mat4 transform_; + + uint vao_; + uint vbo_; + uint ebo_; + bool uploaded_; +}; + +} // namespace are + +#endif // ARE_INCLUDE_SCENE_MESH_H +``` + +### 文件:include/resource/buffer.h + +```cpp +#ifndef ARE_INCLUDE_RESOURCE_BUFFER_H +#define ARE_INCLUDE_RESOURCE_BUFFER_H + +#include "basic/types.h" + +namespace are { + +/// @brief Buffer usage hint +enum class BufferUsage { + STATIC_DRAW, + DYNAMIC_DRAW, + STREAM_DRAW +}; + +/// @brief Buffer type +enum class BufferType { + VERTEX_BUFFER, + INDEX_BUFFER, + UNIFORM_BUFFER, + SHADER_STORAGE_BUFFER +}; + +/// @brief GPU buffer resource +class Buffer { +public: + /// @brief Constructor + Buffer(); + + /// @brief Destructor + ~Buffer(); + + /// @brief Create buffer + /// @param type Buffer type + /// @param size Buffer size in bytes + /// @param data Initial data (nullptr for empty buffer) + /// @param usage Usage hint + /// @return True if creation succeeded + bool create(BufferType type, size_t size, const void* data, BufferUsage usage); + + /// @brief Update buffer data + /// @param offset Offset in bytes + /// @param size Size in bytes + /// @param data Data to upload + void update(size_t offset, size_t size, const void* data); + + /// @brief Bind buffer + void bind() const; + + /// @brief Bind buffer to binding point (for UBO/SSBO) + /// @param binding_point Binding point index + void bind_base(uint binding_point) const; + + /// @brief Unbind buffer + void unbind() const; + + /// @brief Release buffer resources + void release(); + + /// @brief Get buffer handle + /// @return Buffer handle + BufferHandle get_handle() const { return handle_; } + + /// @brief Get buffer size + /// @return Size in bytes + size_t get_size() const { return size_; } + + /// @brief Get buffer type + /// @return Buffer type + BufferType get_type() const { return type_; } + + /// @brief Check if buffer is valid + /// @return True if valid + bool is_valid() const { return handle_ != INVALID_HANDLE; } + +private: + BufferHandle handle_; + BufferType type_; + size_t size_; + BufferUsage usage_; +}; + +} // namespace are + +#endif // ARE_INCLUDE_RESOURCE_BUFFER_H +``` + +### 文件:include/resource/model_loader.h + +```cpp +#ifndef ARE_INCLUDE_RESOURCE_MODEL_LOADER_H +#define ARE_INCLUDE_RESOURCE_MODEL_LOADER_H + +#include "basic/types.h" +#include "scene/mesh.h" +#include "scene/material.h" +#include +#include +#include + +namespace are { + +/// @brief Model loader using Assimp +class ModelLoader { +public: + /// @brief Load model from file + /// @param path Model file path + /// @param meshes Output mesh list + /// @param materials Output material list + /// @param flip_uvs Flip UV coordinates vertically + /// @return True if loading succeeded + static bool load(const std::string& path, + std::vector>& meshes, + std::vector>& materials, + bool flip_uvs = true); + + /// @brief Load model and automatically upload to GPU + /// @param path Model file path + /// @param meshes Output mesh list + /// @param materials Output material list + /// @param flip_uvs Flip UV coordinates vertically + /// @return True if loading succeeded + static bool load_and_upload(const std::string& path, + std::vector>& meshes, + std::vector>& materials, + bool flip_uvs = true); + +private: + /// @brief Process Assimp node recursively + static void process_node_(void* node, void* scene, + std::vector>& meshes, + std::vector>& materials, + const std::string& directory); + + /// @brief Process Assimp mesh + static std::shared_ptr process_mesh_(void* mesh, void* scene, + std::vector>& materials, + const std::string& directory); + + /// @brief Load material textures + static void load_material_textures_(void* material, int type, + std::shared_ptr& mat, + const std::string& directory); +}; + +} // namespace are + +#endif // ARE_INCLUDE_RESOURCE_MODEL_LOADER_H +``` + +### 文件:include/resource/shader.h + +```cpp +#ifndef ARE_INCLUDE_RESOURCE_SHADER_H +#define ARE_INCLUDE_RESOURCE_SHADER_H + +#include "basic/types.h" +#include +#include + +namespace are { + +/// @brief Shader program resource +class Shader { +public: + /// @brief Constructor + Shader(); + + /// @brief Destructor + ~Shader(); + + /// @brief Load and compile shader from files + /// @param vertex_path Vertex shader path + /// @param fragment_path Fragment shader path + /// @return True if compilation succeeded + bool load(const std::string& vertex_path, const std::string& fragment_path); + + /// @brief Load and compile compute shader + /// @param compute_path Compute shader path + /// @return True if compilation succeeded + bool load_compute(const std::string& compute_path); + + /// @brief Compile shader from source strings + /// @param vertex_source Vertex shader source + /// @param fragment_source Fragment shader source + /// @return True if compilation succeeded + bool compile(const std::string& vertex_source, const std::string& fragment_source); + + /// @brief Compile compute shader from source + /// @param compute_source Compute shader source + /// @return True if compilation succeeded + bool compile_compute(const std::string& compute_source); + + /// @brief Use/activate shader program + void use() const; // 改为const + + /// @brief Release shader resources + void release(); + + /// @brief Set uniform boolean + /// @param name Uniform name + /// @param value Value + void set_bool(const std::string& name, bool value) const; // 新增,const + + /// @brief Set uniform integer + /// @param name Uniform name + /// @param value Value + void set_int(const std::string& name, int value) const; // 改为const + + /// @brief Set uniform unsigned integer + /// @param name Uniform name + /// @param value Value + void set_uint(const std::string& name, uint value) const; // 改为const + + /// @brief Set uniform float + /// @param name Uniform name + /// @param value Value + void set_float(const std::string& name, float value) const; // 改为const + + /// @brief Set uniform vec2 + /// @param name Uniform name + /// @param value Value + void set_vec2(const std::string& name, const Vec2& value) const; // 改为const + + /// @brief Set uniform vec3 + /// @param name Uniform name + /// @param value Value + void set_vec3(const std::string& name, const Vec3& value) const; // 改为const + + /// @brief Set uniform vec4 + /// @param name Uniform name + /// @param value Value + void set_vec4(const std::string& name, const Vec4& value) const; // 改为const + + /// @brief Set uniform mat3 + /// @param name Uniform name + /// @param value Value + void set_mat3(const std::string& name, const Mat3& value) const; // 改为const + + /// @brief Set uniform mat4 + /// @param name Uniform name + /// @param value Value + void set_mat4(const std::string& name, const Mat4& value) const; // 改为const + + /// @brief Get shader program handle + /// @return Shader handle + ShaderHandle get_handle() const { return handle_; } + + /// @brief Check if shader is valid + /// @return True if valid + bool is_valid() const { return handle_ != INVALID_HANDLE; } + +private: + ShaderHandle handle_; + mutable std::unordered_map uniform_cache_; // 改为mutable + + /// @brief Get uniform location (with caching) + /// @param name Uniform name + /// @return Uniform location + int get_uniform_location_(const std::string& name) const; // 改为const + + /// @brief Compile shader stage + /// @param source Shader source code + /// @param type Shader type (GL_VERTEX_SHADER, etc.) + /// @return Shader object handle (0 on failure) + uint compile_shader_(const std::string& source, uint type); + + /// @brief Link shader program + /// @param shaders Array of shader object handles + /// @param count Number of shaders + /// @return True if linking succeeded + bool link_program_(const uint* shaders, uint count); + + /// @brief Read file content + /// @param path File path + /// @return File content + std::string read_file_(const std::string& path); +}; + +} // namespace are + +#endif // ARE_INCLUDE_RESOURCE_SHADER_H +``` + +### 文件:include/resource/texture.h + +```cpp +#ifndef ARE_INCLUDE_RESOURCE_TEXTURE_H +#define ARE_INCLUDE_RESOURCE_TEXTURE_H + +#include "basic/types.h" +#include + +namespace are { + +/// @brief Texture format enumeration +enum class TextureFormat { + R8, + RG8, + RGB8, + RGBA8, + R16F, + RG16F, + RGB16F, + RGBA16F, + R32F, + RG32F, + RGB32F, + RGBA32F, + DEPTH24_STENCIL8 +}; + +/// @brief Texture filter mode +enum class TextureFilter { + NEAREST, + LINEAR, + NEAREST_MIPMAP_NEAREST, + LINEAR_MIPMAP_NEAREST, + NEAREST_MIPMAP_LINEAR, + LINEAR_MIPMAP_LINEAR +}; + +/// @brief Texture wrap mode +enum class TextureWrap { + REPEAT, + MIRRORED_REPEAT, + CLAMP_TO_EDGE, + CLAMP_TO_BORDER +}; + +/// @brief Texture resource +class Texture { +public: + /// @brief Constructor + Texture(); + + /// @brief Destructor + ~Texture(); + + /// @brief Load texture from file + /// @param path File path + /// @param generate_mipmaps Generate mipmaps + /// @return True if loading succeeded + bool load_from_file(const std::string& path, bool generate_mipmaps = true); + + /// @brief Create empty texture + /// @param width Texture width + /// @param height Texture height + /// @param format Texture format + /// @return True if creation succeeded + bool create(uint width, uint height, TextureFormat format); + + /// @brief Upload data to texture + /// @param data Pixel data + /// @param width Data width + /// @param height Data height + /// @param format Data format + /// @return True if upload succeeded + bool upload(const void* data, uint width, uint height, TextureFormat format); + + /// @brief Set texture filter mode + /// @param min_filter Minification filter + /// @param mag_filter Magnification filter + void set_filter(TextureFilter min_filter, TextureFilter mag_filter); + + /// @brief Set texture wrap mode + /// @param wrap_s Wrap mode for S coordinate + /// @param wrap_t Wrap mode for T coordinate + void set_wrap(TextureWrap wrap_s, TextureWrap wrap_t); + + /// @brief Generate mipmaps + void generate_mipmaps(); + + /// @brief Bind texture to texture unit + /// @param unit Texture unit + void bind(uint unit) const; + + /// @brief Unbind texture + void unbind() const; + + /// @brief Release texture resources + void release(); + + /// @brief Get texture handle + /// @return Texture handle + TextureHandle get_handle() const { return handle_; } + + /// @brief Get texture width + /// @return Width + uint get_width() const { return width_; } + + /// @brief Get texture height + /// @return Height + uint get_height() const { return height_; } + + /// @brief Get texture format + /// @return Format + TextureFormat get_format() const { return format_; } + + /// @brief Check if texture is valid + /// @return True if valid + bool is_valid() const { return handle_ != INVALID_HANDLE; } + +private: + TextureHandle handle_; + uint width_; + uint height_; + TextureFormat format_; + bool has_mipmaps_; +}; + +} // namespace are + +#endif // ARE_INCLUDE_RESOURCE_TEXTURE_H +``` + +### 文件:include/utils/config.h + +```cpp +#ifndef ARE_INCLUDE_UTILS_CONFIG_H +#define ARE_INCLUDE_UTILS_CONFIG_H + +#include +#include + +namespace are { + +/// @brief Configuration manager for loading engine settings +/// @note This module should be implemented by the user +class Config { +public: + /// @brief Load configuration from file + /// @param path Configuration file path + /// @return True if loading succeeded + static bool load(const std::string& path); + + /// @brief Save configuration to file + /// @param path Configuration file path + /// @return True if saving succeeded + static bool save(const std::string& path); + + /// @brief Get string value + /// @param key Configuration key + /// @param default_value Default value if key not found + /// @return Configuration value + static std::string get_string(const std::string& key, const std::string& default_value = ""); + + /// @brief Get integer value + /// @param key Configuration key + /// @param default_value Default value if key not found + /// @return Configuration value + static int get_int(const std::string& key, int default_value = 0); + + /// @brief Get float value + /// @param key Configuration key + /// @param default_value Default value if key not found + /// @return Configuration value + static float get_float(const std::string& key, float default_value = 0.0f); + + /// @brief Get boolean value + /// @param key Configuration key + /// @param default_value Default value if key not found + /// @return Configuration value + static bool get_bool(const std::string& key, bool default_value = false); + + /// @brief Set string value + /// @param key Configuration key + /// @param value Configuration value + static void set_string(const std::string& key, const std::string& value); + + /// @brief Set integer value + /// @param key Configuration key + /// @param value Configuration value + static void set_int(const std::string& key, int value); + + /// @brief Set float value + /// @param key Configuration key + /// @param value Configuration value + static void set_float(const std::string& key, float value); + + /// @brief Set boolean value + /// @param key Configuration key + /// @param value Configuration value + static void set_bool(const std::string& key, bool value); +}; + +} // namespace are + +#endif // ARE_INCLUDE_UTILS_CONFIG_H +``` + +### 文件:include/utils/logger.h + +```cpp +#ifndef ARE_INCLUDE_UTILS_LOGGER_H +#define ARE_INCLUDE_UTILS_LOGGER_H + +#include + +namespace are { + +/// @brief Log level enumeration +enum class LogLevel { + DEBUG, + INFO, + WARNING, + ERROR, + FATAL +}; + +/// @brief Logger interface for engine logging +/// @note This module should be implemented by the user +class Logger { +public: + /// @brief Initialize logger + /// @param log_file Log file path (empty for console only) + /// @return True if initialization succeeded + static bool initialize(const std::string& log_file = ""); + + /// @brief Shutdown logger + static void shutdown(); + + /// @brief Log message + /// @param level Log level + /// @param message Message content + static void log(LogLevel level, const std::string& message); + + /// @brief Log debug message + /// @param message Message content + static void debug(const std::string& message); + + /// @brief Log info message + /// @param message Message content + static void info(const std::string& message); + + /// @brief Log warning message + /// @param message Message content + static void warning(const std::string& message); + + /// @brief Log error message + /// @param message Message content + static void error(const std::string& message); + + /// @brief Log fatal message + /// @param message Message content + static void fatal(const std::string& message); + + /// @brief Set minimum log level + /// @param level Minimum level to log + static void set_level(LogLevel level); +}; + +} // namespace are + +#endif // ARE_INCLUDE_UTILS_LOGGER_H +``` + diff --git a/examples/cornell_box b/examples/cornell_box index c8bcff203fdc3491710faa3d2209ddeab48586f2..befa73fdfd211459f8b36e00490711ae176dba60 100644 GIT binary patch delta 93402 zcmZ@>34Be*_s>icg76ZONJw~iNUVunDwbVR8(x28^-v4{%-YlYjpN~5;XPYx;X3m_MJMShhGbk@JC_9iR z+gH@PUHzpm+}-HkOJ8WUS(|DTyL@kcp^Nrc>p^9Z`&OpAD`9-Y!{cZd52_0_y z{LY*P(wu$z^vwY|H{CnbW>`L%t1HQTv+|8(KO(l4V0pC~&ZZ&jx zE7aFkvQF1CuM$nTQP;sr@I(WCyb`>`fS;=bPcz_!mEc=VsxNXHhAlDM92u5n7!eb# zEj^lsB9rQE&5Aq#&1Cd_mrHs-bh)gK#TGBQgmRg;QfkH9Z(Hrf*09_1;|DHRzf4z( zkJXW4F~{3o@nKKo8wz+AbYXYm?f0zqOL9Iy)@LfJ8E?NXZSnuOTyge&MO)>k|4LJ>tP6MR>x?kJCexpNaQM1pG~5}|!C z34$u@8+W((taX+&=H@2*-&Nv@o|k@#LJRNfZ9DiibhgK!fm5XIB zq)u4CC3lckNM0rb0Kfg7_U*8MZa*5u_nsxMP2@B@4+)$BfjZ^y+U$P*othEm4`smXau<;OGH;nO=E8{aQ1VsD+?Y@%pLfp?#wwvmTuNu(bc z66ZZbLnM=kWD!aGq}{ zga>szjeRfq$sq0)@0e)uvN}dhwPs#O@h2(ouVrq#tVUaJ-+{HzWp_t`6s9?f&QM7B zF?r&S_E!ITw$A_~npYvbY>4Y_GL!^n8u&5K;)fG{67Vw5sHn@cL`q1MAtdk_e1p`{ zB*jcp7D0-fM~~TtJf}7=p>DN5$!JjVkTyG^Zv7J|$yP+woF0vdR87*=ceIUt1IDf) z{GO773{4I;i8 z#5dVsx%B^3{s-9tLUuiiS2N%M@Ujt*GMFB9@yLd7GzueBZu^tQ+&%dTsNf?a-&zX? zZYd#=d;4WxO{rzI-$s|mhy5X!gOs$IM-^s89wo|$#fq}%Isiim-~<3`lv@3)VMS1k zR<*&Zmh19)omC^s-NoMevUHt>6xR|4`fF)}JgZj)4M7V2)CLX=GF_$4>0$8a63kDL z?Esv}R_dUQ13iP@CDOm3yDX;14LpcqyrM9v$vws3>hY)c=fDtC6mh(7(AB9x7iiFx zlz`5^u3F^K^gqdh@do|Ak}_ou0ZvtYqCkFQ$V{ITEi-6eDX0C9K|4%=S_BjAD5Bj0 z)5%TrSVxa9=rHx&w!V1 zMy(5;B~t5H8i;mL*hXzgzg4*nsd`6{q%A+XOH#Ssy zcuPAmC%}&r-x#7BVbC4Fr6mpxsCo|emhEq8uMQ2e-Ts(jq?wjP*}VwRYKl^YI9_rw z3N(UJ2&jUd=_;3DS^d;%tDJL^>f61fP7J7ucF3JKbv7Seej9=o(gIh3yEzt{0BcqZ zqQ9)j38ahi|2a3H%YTSpdB4AIYCbapy8Qzy%He>jz#&}zq$+qSGFeRZpC)T7#;=J2 zWBJpY+VWum)xTEVQ6QJxq>!ch8%A;bP3`EgfPT|S{C=`R9zA}b#}+)S_Ix?ZiH_=G zjER8+#9;F(4>&xfH<~AxD)PPZ*exw$cz|zJk~p4VN}F>6%{4Z2S93#8=M3kGT{Mg-Mei@uRA()=McO;zKLWObaS9;7nM`y;r81rm5lsRSnKxN} zqv|h^|3ym1yD)%^7qEuR{fM-@t6Eb)Jw5|9`FWc%>tJ@0jx))aWme314}yQaR?&(t z5oim63@g4s0C5ds$19`hR%}4fxr$6#(e*CMLqJt##k!R%1^iA1Xv$d+(9(Ce~=#a$sI(_CH8|?pR%J};RmLYC1VNy9`F?`bpb+jOAbU% zEky^ZTFjZLWpl=q>8FfSAb(PZEtYGwS!3$Hw9Y7HJj0QyqNO*1m)EI1m#N)q`D)smzPTa4PYq%i@p#j1acWj=wQYKy)) z93#`WAkCW6!%UC*^a#L1SM<$ma3p@-#d4)SR^-fUTKL!iQwp&jgB8ga^e!bDU=TIG zrge`G2x?12Z;+`o>5-<$i{<^RvF#|7J3y@P@D+V7-%hDG8`tXv>%oNEFdpS8o1hVGOjK_;7m zna%(p>^;h=Wigv_Xj_zgUBN1gN+JV0s6Xs`X?v0gs-hjJQtO#mr~Z4ZphIyXiP&Mu zLrrBhw$NrL*6%Qp@Y{fw8JF>};o%%g9wU{C$#_uR;qnh7=OZs(*76dAs{BrN@1_+b z2K0Tj5;78~4qf+EMEfuO^&zp`r-t4kqbRx4KwESeEe5X~eOZfoDIjj5Vc-h=9SVD; zQ#I!bo{>#+dJ5ro04JA{{3Y~w^>^*#mqKhE2xV7nJ1lEp0qgUNEnQN4sZ%IOvt}hT z=_KtP9u_jWnU>8Faa61Yt8cP3z*fOq3kJ_69UFPj=3)*6jg@h zT-h;+_Wby29p_QcVWlTp8H9qf7&}Y38?_=hqo#L&#zmXudO+eFy`3>Fh0n3Kv7k!77a`71jAf@8?En5TG1LJ*@A=KDyi4o)EOH>F^+G#8LVs2)n-bOLL&CIwXOz5>?L zE@~?l`k86HZb;>ni=gQN8u=U^Mt^O%qz!zPyJjtW#PubvUR4!vZ zl{v}y`8PZc;$g^$R5EI38Zx@WO4Jbnmq?aL9e>m}V0V5aSOaxZcptWrgCRyY^C2bh zp4_8IwBkvg<^x30h-wR_M_qc<#)BBla@lXx^!C|AR0O>Y7nw)|=vHf4x2CqBPMQij zow2(%W^y@>A<@aaGj{%>pz%h>+DNWQ+&W3u1~vcq6{^d|}S;$4NR_50=R3)-d0 z^_tB&%G~18_xTk4VRbBuvf=>Q+v=E`V8!8a?ggrIQK(%0w$@-uz4q-YlKPaAnu&Bi zkvbN65ztds10l6!;3aLwl!iRp?pgdRnf^w+{RwsEx97m! za)wuY*K2d>#M^^NUQ{HPZ~fTIy^s3 zhEH(R{4XVNUO4TJl2&`X2Qu9(a9Hmp={}Ug&L?U2>qrXDb?@XKc)KJhL6_u`-^JXjn11KZJKq!`ToCa z=tWw+i<&EHuZ2vn8hVp#_91GNp(LrvGy1L|@aXfjl?`4=Bc&LP%hor6d*yhd$54<# zT8~-&f!Jge$g!ZJvD-=E=&eI5EonwTCx4@CmNkr=m+E?2sf?Fs+n7p=j}Q17n+m9EnO z9R5X%n&sz1^X_#5ULfG-3Rr^%*LVWvo+hHfy@J+IC!R10UNYav z`N*BfIdj$6U2F-JZB!dQ=g(3TXu&M)pP8YiPBZlB5=BewE2pTgka}dZS*5lj>Cq%< zh6|PEl2w& zG}p^jtE?rUiNrRnZ?&Jm_?m=v#@io8g7c;M4)x`QUGq9q#E*NDpKi& z&T-E85<$l2unkq*fU3x)v1#Ay^r1NfU^N1|t3rYN=d`wFR*;E-RgP9%1@iQ1T2&ig zq{=!2mM!p9Qb5&}KuOmrEhH(>_QUH?$%jpPD6tD!`vp{rh&IlCK-Sf%OQ622q37>Y-USPSG~Rwk{)KGOC-P;2 zIAXQUNdaw}K?kcnQ*ou70C^8G_;d8=hzA50eMXO!WVS2EMMx+2y0$mS^h5E8Q}JMe zjAxgf@8pqd%(tfBV5W?HGzCT11-MMQ|4Ck zzg?AvJJ*0SzH2ePAkExJ`v7`S)OgD(m>1{0kq5^Hv5NEq1Dk70b$6Y zX@vd2QwQzzA}eQkI;JR|JmNWaO#3Z4$hHp_lLg0!@mYoMDR>1nQ+vy~s;<37h|>1T zPGs+_ol(?NH$Y6A4E_w0dV4yGNWtQcwCIRjM>hZBL=aV6u>w!ogvRLd6B@(Tv3~|e z%6}nH&N`ueJG*J?5CG6>)xk3e-U)J^me2~b;BGqj`o2BK|G5j43n$7)Wc|DJ_*3=I z)3v@#`kYe;ec;7w1Lic~w@zqDa{^ke0s;dYuSd&AHo2kHy^!98q|YT*%$lO(Dk#09 zCS0*Ra-4!v+bL>O8<5B_RRzLvjEizvGa&2Q658|8*-OWx%2a9{mkWpi;BYxhvC=X( z@n3~BqKPq$D#l(UP#5(aDC6y2U9ufAMc>MANY~Htp!N4@9eP-^Vi4D7MUH_Y`tjiU zpS6$Y28NFM5v^vD=A(JIFnxo|ny$OXzcBj+P~sQR?zlSs+8GLl;14QlaE1A+)>{&+30Giy-3GK|SVs2Al*)wx;%l(xVAI8Ypz# zer1Z|gf30DOQ>wDrOo&65e!>>3mCm0FQgVcI-;!O+(+|k*Uxxt!-IV4mw=ESVGvEN z_mK^i{GchMs>M+0AEH*+|NTt*2H{Y&K++^6-eYCh{R$hZ-c=k1RyL)&%=D;_2N7#! z#CAojn!enSG+76s>3(Vs9ry{Hd+;F6uI2RI4f+5Ds-xcI)TjF98ML>_4t2sw%RAJv ztR0ppa$^Sw9-{O+LdI-H6*Qtpu)(>KC`hSl?}C8povk3y$$K8LoPaImPe;fMbc4Yd zJz0@y|0(T+@Q@lHM1zX}U^Tm^DzIH*!AmxRR_DqW8~`sr1_w zq4G;m6m6G{6k7H0Z?BSQV~nQr#Smk#Ib;;TzP^M}LuLNj27MQ%7mJy^LRNKg+1>OM<7MF6x z5f_@~CEq%%omdpqVkH^5m<(`3SCz1sNl=w?m`O_Md$KR8Xu#utBB8zMipW$5rMns> zG?Ks1hlDf|v#6q&+f-0ut{OQXdEUsG<#8>0p}$u6if7X^WDz=Yx+lLTG#adqQ>djy z|CIrNNnb_XaG`h8{fqSGn592eTWDBfI>INTU(BTL!e;dr8kFJk@BvV+Dr#3q70uVYFW@TH6#whUGZ*0aP+O2T`QfSj%1%6nbePd|i8z{shYC zwoL&8wPmJJpIMGqq62%-$e&UwTSgjm9kt_&>iIrLaQMnkWHvR+k*}>-6d2l@7ki8v~oDQ?^4SHRtwxOo}(0_2=~umaOquh9{`2vMgx zz@}xp+@{;G%Z0;7$WUR?!LnQ|N2>n8-apR%hx{2_MS1dbG@nj0ms4@uuGO#;$?kDL z%UT>1>PNkoq)2c)wO?j{iqP>-Rr%3Y?dam5h*Qee7y9aqcJRroXsFfk>S}D&O&MotTsB6Oe7~92`5Bk<0Y}76DQ= z5`G(GN`P|C@`bFPN8Wjha`(3&cLh11)*#t6f{gXTJ_1w-b4(eW2KvH%sK3;J8GQ(}l>|i5 zgwySs^1A$tK#K|aKIJZzilv4)_dX-}E8vShL;YlZz5czRa!y7OQ0fOAx}Km~V_R)o zCz5+w;f+4Nfr4yE;KGVv7lFM9{C7q0|0wGG^Lv0Lg}1s@^h%zmN)G7IW#G}FO$oXK zP<4qVm^P|)2y|B`q&n4^lYIQ6cFgAA_jQteUm*)~Lt;Ijf`Md8+=tp%u*x_#TgE z@#=kpw{kE)Le_XZKuLbi<|WTi3D+Ir2aDxFRj4eyKpv-Y1L0pjiyuUM8-bTKbh$G9 zS)x!Pau`A;l;c~JUKK@PP%LMVlmmP4*h7yUc-Bs@~WT|1eKm@mhe58^!a)`c!Y3GQTin zUxFQER}jb!^k}QsBENWsMepDZhIyAhi&xfri^Sd7jmNd!HarWFHM-eL4pvM?FZX&@ zf_g*NpCo)m64uh=V|u)Y$K*Dqh8a*AJ(1-ZHXa+sEdLhckmP$7zvMwNJRr%(5iu2F4TM$8kOf-ZnP<0f`WTdk1Ztj+mXUcd@jcHQ*SE@M0plxNOF+^M7sRV&$ z7%7lo;Jl2~44TO{Q<7&OjvXWNc1NL8CR3PW3X8!K+;}JHo?`K#GYjbZ={Z_aJo{2l z)BC2@(Ui-~OKGAAXcm}$ClW-NYJ$p=9gyIN3<8~832xH+Au7=gws0eK+G+bKxGJDO0tJ~F_=jC58RddFO#OSwB6=mBNu5R#r~fCG&*9B5hp3J$cY=r*usg>-zK z1eFRX-mI%rl_=l-O#9&VCbpeAH_Ui$4oGhsBnOOi7g{OfKI3j%UHDh@ov!_5BHe3{ zylq?VSz4zhb}#>M7k=3YRfNl+`g10>~Q< zRX7?-0SZze(?j*4RNvsPqogAG(-{k9>x{u=jAs<2K=v^hFXwQZ8jRr}eeS!k5+8>#;T9p4(MbCW?dzyQ*nYGPhyq+c279wWsaZkr-_ z2!OM}L!#d63{~lVXN-}KHqxa=`Y}@HAywGxJcTsdsyv2&BzcLfjrT*w)Q$6l#uA9ap$j zYmn8j^X~7-bOn;A((k)c`6yfwxMa^C<(%}L%Vla9gUZQ9_6ODpC$08->O4}0X(?HK z_}Q0o^0J(aUl^x-^VaylcgDG6b71#qioSG3 zL%W^bg;mLEzG5Zk12j7vAn`l`EK;y}#;yh6N9r@R4kedGP_|IzX@|R2avenw;uLdS`BxtNDNdnch zi%!xyoKoreX$)*R_NRt2EmD;&=Z7jd6RQw%}Qi}Gp{#yq`{-y)|_B_2( zYq8#DrP~V-)_R6Y=UnkndbXw#T5*(k|0GbykvgIOc%AU7B5Z2Wp&eUkf39y_?N?0+ ztJx?g_}dKLU`==O1@PUbE%#q5rl0Jfb8x*AJtY$~ZP&)vnOjcmCQr_jH7!18z4`aI zU9N3jphJ(;#fyco6Bs$CicK53;bpMr{$*Ow=AO(=8?(7Szq(voxVb6cxjg5S&7X3< zrHdAu`+1EgoiV`%lPjsK-OcUG9gduyTWyR-cgoqft#K8;dX%9y9wV>HVK^Rpq4*?YI6C!j2;?21c68EP;J`b@Azv?b3%?x z=B!ap#?K$R^Ka&8O-}UT>r=I)6VHWYr>a4JdK$W5g2xgoJ5C-%Z4_3|&N*^eT5RQ+Bea3V zbD+?^;;!5_P;(VGU=6hfCH=VFE$8KuWX4_na|-@k@4>VCXv^xRK zRoNxR2TC!#8au>qb`g!MGYcQqOIWM3U_Ru!m|UH?_)C9?DK*$%eB!m-G2V%pK@7jGO;=Vy)Hjx z2wIG@JUi!}H?j8^Ynz+u%c2asd@x1FyjY=|yw_4(Xy>aw=g7|*?6W*@mTcd^jI#xpK{v#{jq#r56Y?0ZmYv)tiL zSs>&059Q8o#_XIg2^9Z?vXOjQ12H0uZQ^t5=GJJzS~DIKko#OqR+aH10l7U}F*9q> zeLv!~`o=0Apf`;k}ft%4~(8*}mp*-;*g7vB19a{n#SbzG8e(hLO_Pm(ej>YlBI$~El45vG_ zMa%YVQ=|MJxSO*+s?^u(-|%M9>8*-U)frmHG5#h@wppBw`jJ9 zwG~gIS+M&Ht|z%6FR(_81?BeW$vixG?l&T~H#^HLxgl1T%_3a)p19&BJl5Z_+GD-w zZE%r-oi2r8mvc6bE&%&kjQ)G-@g#yeOY_n=d;{Nec5HsFRm4F z{aH32u~uB@&kFdbe~KLgSfuxaJ5Q7yeB_IFgl7WlU!xiz5f_S9`d8kCMY^vOCuJ51HOol%-i$P{6-*gMre#EBsk-JBr~&SEpFh3Q3WhUk{W zT6%qUN?|*Sq$Jk3&JmOC*NzOltZGm&3F5J9AhAzWkCf&tb3d3CG3zb6D%ZnBz~>l%S)Cyfp4M zn4)dGsTGBD;GWkW6A$OG(R|o%V#HjwW{lr4o%)W+*Rybzd)YpM%0E>on?euXG5L4~ zE#hu>Ok?*D`YRos0e#%u4E%$I<@n55VrUBM%DRiUQdnjU>rsr)yrSc>_-D~Dl?~x9 z9u=vn%vOCW8VJNuX%bITStx5Gn$Kf5dB);g|M~0{7-nB?_5xPNoo~y}{cI77WI+oL zz@!L1aD<&0s2_$o7Wv2t2XY@RW(zp~Hbtb`SQ}PfthKSJ{F7u+D~-Ltf^!F^(e%DC zC-?i;P_Dl7XO1X0spgp7f~B)MBd5Z@eFnE7{4&{dtdt1Sz*){ zFIQZ1GE0<~>hKgWSqCOQ1snyS!}F`L;?V1?iS76(#9TkyTOUMPK3VGNj0>M)^wfb3 zp8}Q?t9p}XR#fJK4xIKB@PH0%@D%%K`e__y!Of>gKhS~RPmwOwfl*IU&e4JWo&t{1 zfz3qzQjFOryG7GwNN>y#>C0G879#SOvHl2K0_?En?QJ62&K7a|C~?cq+E=@|Rn5D_ zlSJ4XESQ;cd%eMGV&$EhJJbPh$R|z_nOTTWewdt_u$yuPxMPUyO-e?z4G|ixtHEy z)fvC~P3{eW3?i(*S7TTgf03K?F5Ayd$wMl1k*p_re!!koGvRkrn4$jeqR)q{ZLM*! z?r#0ET5SZV=I~gt^g|ZSI*3CbvfucYwc^WZcWJb`iwQ^{QOGM>I>G0r}q;BzF?2} zxp>k4OSZo5nZ6aYb9T6+M#bFt+}d9;?<%aGXt;rW$ycw=J+y%hbZ5*rubu&QF% zW;WP&!g5*-isd&*F_^xM%H6)1-SiCox&wE^fng}M@IB<=e{d}2oOOj3{Cp|!Mg`<& zJBXv(*|7FqP$VhgI{3@3XlUTCP=qcO*4KffAjRok3alcA|G>`j)b=8L2W!F;+UE}4 z!Im;!k|{3jWUH$G8m_DxvOiqBx{Li%{qMGV`C8lDF}v9^cRr+b?)4v8Z|2p;rc?>a z?X-{DRZGm?51-avytkkA@JoO7iEE<6qbcSePEb;NJ3O-*id*~H=xTcxE4IO5V$cB= z$+LXKiUWv!XZwhq2UtCR#x8z6z&_`%E)uWhv*(%*T=c}1J#?_ueg|IurWA7$(L#kr#0F=oLcGW-~8*!`Eer~`3e#$C)y#DwuUHtwR9 zM0nqKtI|0`lILqkaq}6GU_6c)&}^JE1N|^fY(Bd}5_6Qk^~9^k*|@+x z(7wEDOqu&pfy484Kk?UbHj~esBgUU#J@^l^#eYv=C-B^C#d|nJTsXlx@S2`_cm3(1 zch}JWh!;!F4{ zvbKx6?VViuQpXP-FKI<_ZP`)ErS^j&a?T5ml)kjC3L>+3E0 zH5|CKX`;<_1WYlb$k=}3=yleZ1&cqgv$}QOtf6d;S%LFEtKHio+ll&;HLX2*q%v|~ z>6GYIQw)-? zdXudQvp^@>}M?b>=@XvGf*eQ}a636EK`N4G+0EMErP*O~U>r z^fr5uJzsyYLOr?@Z5px1G~BZWm19i4?meI zJb#clTZ~1zer~f8?6>`v4IqL4$uo`kQ6*zN=M4~D?y$&~y$8Tjjzxi`KaB{bpGLG( zz~>288^5gZ83mv-M69{P>ely!s5txmiB>pN`r$au^FaGS{IbAZoV>%bTYudjeOdnQ z5FXML>KWUgM)or-y~JC8vCtZS_fzfj@x0kj{P-8k)+`8>+$03 z--x;=JP`M&9P>Z~+-1)-ni;QlDQaD^;J7Ii19x<^+VjCMK25uc*>~B;JpO)e?R)Hy zJBtt(AF^m0g%`TpcHr&{PC%hr2ctEc)#r??j`?2JuKzP-EcOJcW1KnO(K|2>uC41{ zQ^r)l;=^#ho zMaAl2NgeO}8p>eHe#ewlB`$rx2Y%QxG1}xCWBCA(UN}WZb{%ESD!YTS;VSzRWusK~ zB4ra)_NJp(VloOwj)E@aZMTLh9I-r zoz^g^$gE&SBYdP-3;WY8F0had6RmdqS_KPCVr<4$Q^o=hLSr?Gz)KCh#w1HanJ%P_ z{J~0NdkDEQoO=^8krpZsTQ8cTLoCTAU$t_1pltGu!X;~P5(6s1Z*~uO;6+|&cm0)T zqV@T17JT>vB+2R)_PAIG$NeC@&Qb4Bi)BARp81+p7X^IkYgSEd#EM&3bO>K|_1Bac zO+)RqeKa^@?1iJoM30Uh6FoM1T)EG-ZY@LzgMT)w{cr1@$DY=%C8i}AeIbrMl%QpGeHs#L*aK^R)B@yecuk(}Oxxph zzhnIddRWsXGi5fwxGh^Ym+GWHctWs)Lpd~LP;?c)Tx4@4RQ5duak2!f@Pc!+^ z4*ms9W}k&u@fdmE>KOSY99D|OE5M(Pyh;p>2n4pyw}2g^qg~$ zp3{G#=lJ9F9C@0agU->j&qaE^PzcH6OYoRZ)}AA&qQB5{X;%A)>=^rq)%j5$pnVuu z32S2P*0uHpP{9X>`A}p5F?X}%N3El8-?#x{-3Xz+3pUd;d24>u4q~Ok)I547l3uk?h`&g>K{ttyZ&xk zl1oSKcH152Zk#lhy;L2InTR=`jLD5j9n}ET>Ycgay3niWPYlaMc@RUM26q%q@FVK` z74XZ-U6jxBy?{eMHHo7t&GX^UVV>LIC!U$;v!b8C%UyLw<789!g#YGnnnE|()6fFH zOrff`%{8YanR_P;C8dUwQo0{1*J!-15e_G@p$>YhBo_ zcsuTMI&adE{hq?A%E81lNr6*^12-&Bu~=SirD2JRO`{SmY_ZvtqjfEI4dudZl-)>K zTGnFoC`YSW>~YE^Qm&A4$&`avlBr+dI;p>#^JVmn3?zl@`~oRt$)nlC z+-g<$48{XHi+NRf4}P|j$gRqsuk~IhiqA$o#!PJed8c>0cn3bBlj!8d+xSoQK^MdR zK5^({Z%i;B&(@v9D_(pyyDW;l_^Z5IM=`w`k3ewqK{YP;><*%1b-tR}#L?<}2rCp# zYVfv^)A1(P+V#L({Cd%26I^DBN1Q#ji#7AEDPvi6THO1EN90A_J-|}xs0#-9bQA}U zl9T{bL-BD9-aGU_J7rS+D^~U!@PPDIA{?k&9~c39rVb) zb2}RTIOc&|Q&@%tJA1YEwgsHZ7=}-k6DpMxE0vQgmD4Jfv-R>|&(aFkR3g||shn4- ze7sV*u&kV0T$A@_w&DoAWp%^#mPJ)+nF;WUEh{Z27=lX6iIvL9mC9+A%Gr8(uxF`& zHI)c9Rx0OJDj%;@E-WhxlMnyaI|#SDUO3=kc9Wjr;*bxY*yKn@^l?^(B@s>Bj)4S6 z)NxB8{DK~G1Ks0zbI8d$P^ zd{>u_O(?~7Gq7t2fZx&SA9N_iHfSl<)ZzWuQgOQuAIj~mzKHeXJbb=lTB&{(K%V=0 zDD8gXKr$>P#A;t@af8W>;PKZ`ks838u&d(T08IQUp}AWF_$w^>LNndx9BZwoH;uGb z8@=2GsdKeaXi0@2qp*z%g>7=@1oF|0U$ThfL3|8%H4$y=@w$ATMOf?c##OtVWp484 z#$t9op2!`I#i@FH1-CX4lk4-r{J)LF5A}Ih-leg)Tc5Y%D;tXz!MuIdac97E-Yg~t z^U3^jBk@x(Z^9!Ri9drOd2=ICJ%op}U33~mK4u!-vku_@;r!labQz}S_&o=DTi~d$ zk@T%f5vIwvw~rVW!Y#}q7KQNOc4kMN%#$g8@D00RD%^2OIfui23jA?aw$~%B>&4XH z9IL;7%lj*s};;#_ilJ^S|jT-O(o@o|c8}P9H+s{HwteNCE zham7PU5Mv<8TI_C2xx4&IUggKkPFZNgnZ6FLXpzD7!_iq@AKL_Ia{o0$lLh#Kf~P+ zyL;^Zy(Vee`MfA-$Qy^(Il|qxyESuj+r>iN+!PW=31*l7_QRzJw^D@5|Fgp)!p!T` z%_d~q*TCgoj$CwDj571a+}2Ro%sjALqG?N#<*9A(2squ0Tc*suFcD4-tXX9ZdhIl{ zo9neZDvOj`^s6{!=KT`3Rq>-K7(~KOijNxcdba-^EH!T>yT4n8s9^UD z0F}ySz4Cf!tqZz!R0Y(pYFQlQm{Oa;QO+>mCPW-~%BG2?AT;>+A zMch@0_^gSRD3||A46Fk^EzFuV+7eE?%mNaNcVH873+FO=?&c>7(M9wCCU!q;o zUT`mNT`3KeUaICM#8goq8CFq^pF(ssn(YhG)yR)(ICE0UB&w&fHkUY)2eF)6Xv!`}-85w_7Cr4| zq(hME6TWNblvei6_Te6BHj7&-+lyg@Lq=@wjgLRmHa~q|p!2kfn4{df4);hsws&&1 zvg0VRu>lXlnd#vMyl?gSA-dBU7c3e!cdt^13Dap_w@q+8I!twrfCXeC z@;JqQ{6?79Lu~;%3SiUrl^q;f-zGX%5$M^!13i8{u;K z-@}y*c+Cr@&{L++Bc{;3F8`Y+F$~?JX^IEYh@4%6vfmyty#uxrT)8AIo7NKa zTI#rY(6X(qeW~p-U-R#X1tO4<80J`2sX;vwLm{XNl;7o7jRPY$#jz{O*A~NhvSQoJpsH735Ov2Du~gy9`=sD)edt^_y`tbG0E@*Nw#Dmg#75GL(mg^{K>E z%JTwv;2r~Y9wKiN#@pI{gio$mM=ALYKVA3oD$Ghxv^*ho+_WTUIAo5p11$q64aFxe z{vTN%8M3w|%~Ep;rqJ7*m0$I0YJfqWNODn8O(OgXoQ+29cS|Qp>GtCFBktP0mwV`^o z!M*;UYCo#|#3k0h=^os;7Mhe6nfslpZx7Yyi1o@!z@53FUg`I7LG`?N*n&5&<*Ddq z>2%9P+m<|_wGHItSDYE@npy@lpkR&^3Ba$gwo|Trpe48Dp`GLLCp7-e&})6kE#R#F z-&(cQPjA)kI%Ld{W=gO;kATei7M3%0JQ#6TzW6!Z7*!V8s;#OD*Q*Mos#@WMly3ZCQ4cJkQQ%yM!;tI})j`fUg^dwUTJiR#mgrS= zBt$_ohl@|PLdi$ z*{+i|1s)18EfNSO3S1|x4)>z%u9MEmbgEPft*5i2Zdmvzoo1vCBmL4y4;raorSzOp zUS^~xjr5kD4t7=#(gk-g(g8*~*GRLCbfb|TG14Lpe!7Z44dybG7rURb!1BQ~DJT_e%3+K(Nwt#NVI)UPG zIPc~&0n^VhcZ3nV_d6isB6wK+#<~Abw0>xd$cn(8=d&$pV`R(T@^2d>0I@$vH1-Eq zaYTXQM5A~HMT)c$>fk!TM8p{IBwp=E!+N#!A?&rSO&jQKFQ%cKT9cM)ibd#6oUuqJ z131}ktr1QEI8i0n6#d)rdMrgGwdZvs)>TJrhiQpVwV|HUO6pQ2D{2~*;7f8C5x0pu zq^8)^j<*XoJhBfi@6us}I!iXIaHef_;cCYln`)GD6Wb+k5#F8$`2FIohu{P|ujd9;?RkBk>@6;}=M8uv(}@GCiGU6~#&ohq z1>)2iVp<1ot~CK~-%A@krG^k4c!LJLnSP4v1EozF!&P`fuXAXAkMa_yI`D8)SVdur zt1HF5)>c!(YAR)IU%DYy#~m&5IJ>Q6s{>V`fju9unPgwMf_TS_sqywjQ`H}}z*c}J zn;Zm$@#g}Hic0nPlYKq)xiE@f}>Qt<|D zpA~n0WE|nJp=nr0M0Vyu&nY=jY%FCQi|~gfTq!~N{$rZ49sxnKXsgBu}EZugB|8(K4O|MZ$l#2D!#hSdm^ihOOW9rPAuc-cacmAW-!5evq znq4sfFR0cV``-RlMPgUp+C8b1YN@;UqATxU8d#Akql!MV7FW?L(5J!iN20-VyosrZ z8eXbPEq9%Cm`++lAA}3B+i+IF5LWAnJ`7*yV54EUPu9pGOVTi$00ZN1t2Cl@34K}{ z2xVO=OagsHaa2d~>a4-JlX z3`@jL<|OubF8?pK!XlVYwaWj<#XfM)=)uG4)F!{S=Ni{5ES3v^_@W2@mH$&+EQ#WM z-3R>hSR9Yy^W29$dMx6ic~kdY$jpw$T${EGB^1|)p1Dx9c-Iit=FTg2gL-Y~T1 zqsJ-^O5eAj!gk5I3=`P7o*vuiajBZ{eSw>88YIa8yiKQrD4Zij9GXfeY0=xZ;u&$$ zegON+Gb0cUj8t`59j#v?j;jUb949Ms9I_wK7q2rff@v%q5ly$&t%zxZ!4z>Q;^fp3 zX;h1iszJ5jB1+epaq8AXG}Q>UyNK)MD;4U}wIjWB4b}0M>r6CN7Cmy8hhE!mVte5! zHaY|>Z`5wu;JeOP)2r4xCcs?`Kn;hGouS)r z;~h~ej<@j7fNd)E+e`OEzc}7(W;HCG&Z*c-$+zx+2(Iuk(_Lbq;X1n*>0l#$)ks$x z=?)`3Yow2iG{9Y#)5%B&x{HE1KBjsaOit$8dRO%7!_7gT-=%j=*x=yG2yW>NvG}MX z)M8xJrX{#JJ^=IoEy1G*hb4Fu&;6kfzs~(0-ez={!6(IX4sTrd<940AEw=Q-Q10`- zxZe-%ZSHN+vOk}~tKSwY`|~;com;|d0H4bn-4d@3;PL#_O>txZ-pHif6fF`EhU_g8 zGZGMR2NsLZ6Zkqly+{lh$dmYk8)E-JyjObrh6oNGz8%D8o3fmw z+KmWApm>ccMVrCs#j#G2Fc`$2Nnsz1moq*$#kYg`lvdZT>us#?LHRav$sZ&kU%rP_ z{c#YQ!>>vaJ%nE8NHKB<-lO%BV!;sJ!d4F-$HB^H(A|>NQ7+0ScArvq8e(^zsR*YD z7=`j{rEY=oM@Wj~VGtHAk(vVUE%L;`D5@@p+`y=vM)Z9sHx9WtI&Y2>jfV0_-v5S3 z9ExUjy)H6`@|S(LQwwOh*R3$>ns6V+$MfO%mEtho%+$7`W{v+49}MGGzTm3(V;FB& z_v+PWOfo^*u86k7;h&aX5xs}Qo!C=|J7{u|m;8a+NumFuP#QZG z?0QNTUl+TF^WfUQQwu^4PUI5A#sYyCS;0$XnZn1A;&~lzOXvXE*iTjJ?>dI@&u1d+ZJcDURX1 z?f9iy2q>|RBgpWE*7*IJbg0jvEP{KTq3&$r%dfaye~rEpLlFTn%X^oVe9+*#lY8~} z&mEosp9J;V&X~CRBHpllcv<+4atie5<*Ou#fFhMSvaq{0$!gC@$(QIBkxmmBFfM!MHXZx|^r(dj~sG|EUP8)>?c zt})WBNK0REL=VGn-07!m!akOdG5u0lK||+d@%LD6_BsorvjHx#u+{% zSGGG2SDVbF-^74%ypy%_ZzN)onYKP{%T|`b)e+z1gSA_0$g`bRF&IMaS{HQ%v;P$MoY}*%hhPflo;}Ud8q01J*!0<&CsMpU1~Kd-*{S3Tlbdsb|pgg1BDxLrY)caEbg$fuQ-3oDgN zDwT0Q@)W&KrLwtFIlNLiN-wuBtB|g;R8%0bQaQP-oV$E7ufc4Z>eVFAA8^$p{m}%N z2`8WU>$=pMJVV@GM8)cl6{+=OPC*k}nI8*eYw0TD{B?%hDb`7hj(an@Y5r?9;`jD~e(Ul!_AVApf zlIYbw;J9#530$eW4ms-6$0ywvkil)tIx6DVJz=MyRam!40i{C$;A zOM_@CKG2KVM)9F4hNZ1B%8zvPMx*$TUd%I!kB^Gz6yCadf7R)#AsuUP(|311>Y_ef z&!=O2{4t{kTg9pr9>Y&PFD|6;pn3=MsrC4VEKO457B?--8}mi2RNl_C_^@FeQnTSv zF(4JwvhGnaC6za-cXhYEVpe7j&KIAi^4_LXhbk}+`&no1|FiI!#~U>mRe{-*F$J3% z>Zm9^vN<3I%;OQZZlwa~s8|{L@DaUXh$(S1-H127$IV*wic%k*`KgF=+b#rod9-r% zH~?+1ZT%hV5~YufG~~Qq{@WJiL;naEnPGlzJoNl>gp#rU8}kXCpK zKR$_-WnPE)<4LSy$AVP6J-H|!Y(v`&j2LV)UgfmTY>nZLJ^=fCG}SIX1$T&u!8iTc zZ52U`lj+v!OD@uCll)%LebfICA2AtAm`?cagF!%nYHd&{{Be*kn+$fA@8P#6vqB|* zOhJkeq({xsGN9&L;Ek@&99ND9Phpb-J_U9yzaV9m0oV2Y1-@YlRv7-=&%LLzUhJpC z{G+MJv?XQgD>MD}V`BTJ`E)-&63+@2cx9dLkGOcJC(iMh0#5O5i7dF#HF)kTDC3M* zahC7rx2HmWk=Wa_v%>6?gM8XFR;W<7gKyo@&Zgnblg42fI>px!iyIgV^H1$;6!Y27 zAJ|!`8n^Z$6FQKGZE$mR80i5uf4rhZ9=|t`)8Nlw8r2t;?1g#bc${VZ@{laVh4b$V zF=3=E#KFB(h(e8Li@q5q5dx<&EJkLwJ%zwM{zL_KK?Md_XSSt80^teqk5GvW+DoM> z%BxLdHJI;CS+vOgqG-j-?8gk8widytF4{RHOiULp&38@1T20hJ{&*V1Jmnk`50Z(u z9U(dFyGMw*AjJG5g5Qfdau1K5&I%SX(-4U|1aS0)iWC4vAdV8JgQE*OBuB2jf}`B$ zsB9R*d{Hv_#dJ2@J8IJ&sRD!1?^1ght=z6~UH#8W&k$te>H^?uPd}3>tT{c|&+@Mm zSQqwuC(lU0QR1|{eBLY+rUUUs9T@ns4wZ%ZYp16$L5TalFhA|&+Y=G#clc_hnK(p7 zTD4Oa=5-bX3Z%^nj@8_Ge$+U zw>3{H4YiGg0;Dv!_h}Z^I9DdiqWp%!>Z05jB)K}eLu$_FKS4gUiK8^QLITdUlja9x9`BZX%uvT^kQaCN4icnWJtPcu&Oubr%S!2a(+h<1!a8<^fvx9dpe zfpb{n!h<{^V#1AJF%GGg%!kjx0@5c{`B!sTVI$|Zx4dK$3$#AjN?GEBRBQrXN%7{_ z=df^g!k6br!ccGKRvs~z6}1kgQc!l3)0yqO+gyxx_ig8s=d$8GPvLBj7@uc5X2_7T zJo|^ee9Zsp6c)}C1XDvb?~)iOMbQtcz5Y?#Nut!h?;Sj1V3ocjhmYcul2~Yko>f&A z(U?kSJ}QOaqQ z@(HR)T}PY0|L>I7e9!ZI&Wg0}jFhp4oQ8yGX#c4g^&E38Qtl!y6fQ#HPc$SUYXZG6c}Mfk9dJojSO*#D0W6zQfI-F0_e z+`zjoX0dGUM!sq>8yb`!p&up%+=W760w|WDkDK4F2JFciK9XY>;`S|`#F6TU>yj^U7U^j#J!&3a#~XctN;$lq_xgf0w$1}To^Pg4U(3Ix zK(>aT`GQRgKa1q>#<1LX_~dKK_lV8e9TU`+Scbed<`aw|ea(|M_}DK|j5#;>jxSmL zn(-hsE3Ot?l06+IhOkr6NycJgkt)A&ng_zbqMNVn$UVc%w}yudj! z>O&k`QKG6&>-1ZN_f7LYauRh(f;vUKW))P5nYe~e_zJQ}T+6@simeD<+=+Q5HF58! z3GR$nYi@mA78bDwh1LJIza>%Q24Ev^|Z^(j7Rq8!ZRmv|QlO?tIyD80T>i zD3OJeac;grE?-A);J+=$+T7ixa{0d4DxQA@Yhyi4ylUbR>jiOz0qaG3^rr-d$mbB^ z9jljFrb&%tVhsnNpwZ48bTLgc)C0D5)y(<@$`hSRs}x=?ymsX_=YWz7hz~|{ywaw;ULwo`%fEsbTg93LoLMD{{XX8+Z)CiS==wdjD5UEW zaNZqe)jYeLIR4S~xfu2R=yL4kDXXyQcw_UD)nMrN&OB^2s}^vX3J58^PuL<8W?tX$ zsjD$o@3@%fyTF3WP5O!`aYX$6Dg(VU(WQHIuVdyG1kx#yrF;05)tEm1yovjNjciVJ zlG&uL$ezuMHPSRaT|?Ov@m?fyX+3n=|090H%&uQz2B5tMNbMGIERe$eVd~a#`m9WHm_2PM#K5wzhg}jnP%B9 z%mwk)t#+6We0@iusIB|Er0a&u|5(dzuw_g5v2WNWYYNX~lXh@Ec^wNYG6$Y?a8thL zGhn{6G4?@nyUYCDnD1K$YjC#}zrBt%v|2MqW*j$xVU=&%V%~B+2BFhh@#X7Tv5<)$ z;L~YAkG@|lwSL3gCQ)PmW(q0T*yD#z&f$T3;Es+Y*fe5;LWxtgxZ{% zat*?itB7@G4-DG&6@@1z64z{EJYX(aK*W$V{fRmAATehi09rj~hH+iMr*6a$c{B!T z`{YT6T5DAbO&0LY8`(EO*JvPj8 z!)fL=C;xc|)G+=7Ui^FXEm2P1=zCVq8}|->&+1zf8dBNtwPnX+98jCew|>tWblgEX z?a7U*!=IR@;~tS$zSf!NFy`1t>o3rmc60*^y4s7gh&Z#(1jp68Wu5K-t+@JwPV$;t zXYs~6p)(IKywKCNTq=d>v-q%`ta(Ts9EzgajmV@2Qan8!{M=4-SJ4js=T4STDUSn{ zc8|Jq$RQ7sbQQqQdn?o4ah?v4;mFqN>U{YwR)u|Bo&Q8`wd(xsF6@wpRp%9U1AkkM zcO$o9HU1g7XCgSh3{ss%MDWMES&h7>t3p25_V+bc*W#6v*|0KA5|Ay9kd>g0mUhCYyd9e!pARA0>9uA{7^FMn5!#^Cdrb!7!TQlqj33W62vQDVqTnXmP9ay9{Sa7 zyr+Z!b7&&(yN8tu8b6z;6U67I)uu^=$!74c_TWs+;aU8~9u{Q(2;(g+&UoAiL1zK} zn2v~9D#R8?_tD%G0|A<%KPR#SAS3sODN^cDB=z9TJ1AGFq)AP1%)1FKQb7&|O7lYz zdhya9$S2GK{9REhU2!`ehd>PC-7G4{-D!wTwP`LRF*to-#}9-BGfD@qZxPA!L<)#C zi3|h0Z^N+g5BUW?7wIhW5irSeTD|f`FUZp?aaTY^eSIa%^~11&RMH3Wsnru5Dtwy7YS;r+G@WH z09qxviY?4n?_*K^PN4vPJ`~9r_%@h7+sBGkzK4?899FZF2n|do==8e+hZ4fj3HjNa zADGCeK@r|yKa0v&lJo&Z$F70j@S@zYpG60JJwy6oz#tXHTxI7s_OrNL1w?IFniS?W z4zRjSj|lOia7ZAOrZ8R1Mk21Ib&z0elyGW&BpkmFg@Z4Pi~JlNe4=nyTx1a6et?y% z`k)YEVD4=rsZ#Bbw9GI&+SLraYoQLhImd_2_}RyNJA->4WJP-O`F1(AH&RP>;&#Q#wra>ACDQ@ z*OQ5(ER)MIW+d2rXu%QjhZSgs`WS?2cKF0RHz9k3v1iRW;6P}Dxc#9?xH$EXIH(G`$r@@KwS5idHREkw>#PST4Q@Qc_;wb? zw|S@I$vT_2{*hG}m?uo1ddT(y{EpH(Ak%S(<-!viB8OPGx~9M#I2)etFCf_GjbL}+ zXn11hR=#|{t~$THZi3F$?%cySd6X$+e9`?E2P;ZHamGZQf@YZT8{9cLA) zw}b4Q25zsQS{VMk3cYV4|NCDj^J%huo>l>;}teBiz1=qotlk3#hDNM#bF^HaFPYHr(<}P zldM#Mkg>9;X^W+6F6RF`$?8Nugjiq#*PJ2M zjU)@0!LOdgM*X-d-2W6_9K3@v6N|z^i8^6Zq=iDqhHX>D>~ z=k1jJ6o!_)cM;!zidAX0tTI*sVzI6atwEO(R~+(Fld&t)h}r3xRP7wr3qVv3L?VKw z+%kqf>y+PhTn*Vj4)u{E73V>$)pNlx=2fiCyKqZbJANVm9J zMswE%95gx{n4IGxZX9Elg1p`(bm@N$e9&dKnlU6Od?1Br_~S7Ek4=a2E7#d@h8Jg>{l@xMo{l|FYr$p_ z$1;zUa2Cljfi^>hpbsM%lJNK;{PJ%&nGrICm%4%GaeXlV-wig{nD6%HKi|Mxbbavf z?gkpxmNvZ4O$hbtHhl6;Ow`A<;d^ef#YVn&-n`8%*3dZn7BAS`!U?QSt@)l?sN<%s z`4w^twdSwL&C!|%-)50TF1La>oIYVLUl=yI@`esD*p8^1j>tO-55<2+ z-g8^Zyr;L6d3S2bul$a05EUvwuUg`jVmg=hsXCYTsak$*D*`bhMqrc47$wl5waF^5 zW&yt74_3GOf__4ZH6aR)M+irx2EoR_YZ1Isn@y%9;L`yJqF?@Womk5{s@jJKdOpK}lKUUcPq?_pzdb0_}CJ(gxaiC1l% zlW=Lt27mPONu#v{o{>M0JTX=@KMes2f?Hp4YD{?J9$j~!w~VG`vrj~uoI^^5Y~i z`?weFkTqb7+VjsIu=)XA{zZAIr)1=IPyXiv7FMczd!&V-!C)FE!JVPpua$dX9Ix?^ ztqPv~91VNyaeaFM!=(~qkK+PQ{_r6i6zuz-p~sr=p^sQUHs={X_Xs$u_Zd(R}t}Hi>n4!k0gRxy&2QOFcnHa;X)!J;8~V!;hu^m?*yQ z33`Out++}4q00Y4W8Up4{Fk)kv!7z!d()r%`=@w!X+b0Y)iYMRX#F@*HVha0bVG-h z)E{adivgkK$SwZoGrWmZ>k$us&c<2iMq)K`(nuwVk^H;ouy?Z`@{7;eT=wb#@BbIv zK@a$gzu4!cmNeI^@kr@)SL%XZ(mqUSLZEdjgs9hy7-uJ+4bA3W11eLh4&bh?bT)S_bfiZ@tjFF}c4 z2bXjJ-WNAlWu|+fuk}zIMH~WYJV%^Ul^kRoFid)~ZA{CA`p(qLByESk%2~-i*RHi#kh4 zs}>{!dU_lC1iV2O0W!Srbl==Vn7*TgqD7f4o!|P<@KanD@Lhjw=fzK{>os;J4Zl{1 zn7E)QH-Gh(wd~l8O0x&2Xy{KY7KdZ8*v~q1=m6xL51z>Rv%e|lC*~01ILm-CT&zuo zf>x|c;;7pbGbWOM`HmGI+)vieT@o;@TVYYpl7`yQ)dcYtrn<9PN1Pa79d?Aq>`~_2 zhR6ft{$`A7Io5Qa>KU^(+1(# zfE)cw|7h-TWA=15il>0mKC2&!hUq;Ot$Hms0Ik+{i#f*k*=nX!a8D*GH zYyKrOinD-7zKtDMbRTh0L#cV+# zdUc+f?U!}QV^LTXT9eJ_hW39`hADQ4ZP{khE_|)iM}DwM_jRRT@oc-~@y0Nc`W_0n;%8Tn&sKT1S?T8UT5Z?wRBx_nZ8 zqe!7QDBC{TWQRnS-i8BhtWkk)&TkYhyssnKl;6d@+)vEh6;LRI7AWJz|IB3+%@tOO zWM&CG#=Q#|#qAf5$xf&Zq-3@!PZFT-d*-MrsBRuXD7rH2ejpEMGEsFCj9U8sKw3^a z8cx67^h}13=+L3t9CLicbCRVJC82F3hAr5*7k_47%8ailVANxQRrs$3jCZV~sJdeO zWkKT&t5`nyuK?qX51SgspB6Tn*yF@__n2!mT-Qxzt>{SKwd%cBG5*M6X-*rmnf=aOK>MqY)TyWPYX6tD z+hJ^$+icESQ}^@3#f{SJ^$~uzxG|KSDbCxKFnY6V#rXFnj7IiJ#bs(iA7TXyeK@w* zqY}{JQYD-{?CmwSvT1|eh~8sC!pJEf-iAeo?8GH+Fb3%TU9Z89XE1LT6~r`_2XUEy z5uYJq&%K(s9(q)$5y?gt6aA*OW3f#ArmYyi6bjBu7fXH^1M{G4Vcw$yUpqVbnkC`b&h~lQ1 z6O?LyHa;@fe$?pLe6|^OU$3$L$1Sh-*}c8IMKvG8LE!fL@{_1>o%M`Ip*Ck7)S5S9 zm}7!SMuC5#*G4@&Cdb*%#F|3>S2TZ%z>s4aWtS9LpU49P-{R`3G3Ae2z67Z^-V#Hi z*spP%=#E@su(%{Qid!O1%ZI0s@ji6tNV?XEGvq{C5CtGU{5J-A}&C*HQ`Uo8im8p7bKEYGY^Dg-Hj=pYZz*GK5erE&!%H%4mzIN zGl*9!XLRaW20WTx0mSmyyvHqx#4f2KPEtm$1~4yBa`^n<9~dysh6i^3o{>XVGifrL zuS!!4f`6DY|GhpsBD?M9cgh)cS?GRVJlv>JyFJcpTMPMhAg6qiKaL#5C4ED$jm0G$ zY(uTF8zg5-Iv7E3LOBx2F_Q!MtZ<`2NN(c7S8rAaf*I0yoS7dP|5<&6(>m4}`HPW%p+ks5XK??tBc>Q2%@ zOat8Jse*iGd81}NAIdN(^58CrU}*vVvb+(RuYVI_;$SGzeHO^WDnR99Px8(cjAAS} zkdLcie4Q&ml)>`NMqaX_QR$(?Lgu*H0$q<})LE?iCKBsG zU!rwj zzCvQwgwiM)>Y1X{x^$d`bJO&*@^h7pkbr&&vpE+Nhj=}mLVnA~y(=4q3x!ZDm>`NZ zR>j(qk4IECMhEwAj95vD{zAk!GLra;b`?tJ5|cQo zfT>X;t_?#T80dT{!WLgan9aE>4Gu)ataIC81cn+cx(zjmbpEMkgw&f;pXTaz42lyd zINEw>d=oM#UU`r{+Du(PVm$H)-lQ!DF1k@>l0A}8_+W<`Mx$Y?scVErj*`Bs$(N3znpF+-2}9s3?&TD3`_P*-q2f&%e@ZA4&5gz-f&Sc8 z*NA4%eR!pMMv($#DJc|lGo(T8v6{R~JtNNA0>wuU;Ye?eQaTy`zMgR|);9+g2Sdvk z$M23C$8%!f5eN4MefnB5u{k3CR8yezLyNF?2%> zB!xpYZFIxQnG9+N+e!sb&1v@sETm5$xQo1bq86>=NAM5IxY#d+}$ zB8`@8IZOUL66P=D(7O!0p<4ynLs%wUSNp0Gd|QQ|YHURK9(|MHHkZH4;4I3h>;JnK zaoQoQ3kmu>%)f&h-nv*px>$vM(B;t^ zJEB;D9~i}hU62M8zQ}4c?xcS*++v%_G!$cW?%3Q#S?IK8=Tst^h^@W%Y3)Q>Ek2ZK zwdY7{#Z)gZTI6a4WlQnG3wrLrztCx>c;gYx)g5@35EJM}ci?40b8yrIJFo7*li%@T zO^n*i^@?|FVl)eEw2gExL0s`p=f$9>Q!@CWSfdb6ZGt5yuYdTc7O-HqUh_sVMhN>o zo%e`Ayk=Yf6>r=d8Sm#9qoen0KC*>TBJ@{u8gkT!JVMdr#R_dfa8Bz<*Ix3zu}0It z_FMjy*r9`o zvbBesb)HF{4}NHLY?S*i64n7hK?ev#`}8Mr>PsFym!Y@712t?BDu#v>`tx(%hU#g{ zb5U;pxkQb-G9IF_y92ka<11PiYYMmcTc$xZL5A$HFl0~6g}hswQ6_IOtdrpAL-|qws;LM+Qw+iI;`P6 z+F(gGYz?2&#wcqYf&q%B*-d#RGG-%R@tbXoM$G+4SdIXiDVVpPWTFfZl!3f=TckNv zkcC+LWTse?F478EjBN(AV1KfFU&!ZBWf|H*Iti2(6}h3~4He_q zW3J}Py|3J4<^H2w8t=Ns)7TV|K3DE;<*rli>1=Mg!rBd>!7}34^BXK5Iw+4Z%AE(- z6{cdn5~Jf|uI(!D3BOP4RxddC5GwRC%KSZp zzJBdbXQG{vEfIjjHXXu3%=*6~t&5j4+%tT_ykqd*z8QQm!~L8-YV7~q+5^M30oJOP zy!X(eW?Vw*PD?N%V;|LFvEpGcZ#TfG(D*QpAlZ@*h}*a@?z=mOK2z#%iA*53R}9YJ zVJaR<3ruGJeKfqSH5V~xZVT$xDK?L^Uw2EgN(t&bD>J&l6H#Eyy#SxV+=sOZm#qE z;vi!p})L(@+d>-+UC6 z*KeGIXACwf=5F>GCFwTbUgXt=7#|hdiEQ;VF2)AMxEehR*is=kS3xag>%voMw7&2IX zPG(>~Co*6ko#O|G88ZW#pO@{F`pOu`Ke$24T*5mKH!9`KjjdvH?JxYZ;fP))O+>Gk zGmXe2>yGvIIz$cM%<&VH1Yh<@T%ImwFvTeZ0bb@DN$Lj=(+iRxVbriLL$N%HyX6|6 zKf>s2)H<6X&i}m|VO(Lm)A*H<7*MCzCp{Ie#Py zzUC)>dbClP)jh@UjW*h{7pF-Di}6lljGHX|RC1lMM!}qgKKLO^B}n-t@jjD{I`)f4 z5C#*mGaWs7OByi({6XLx{ajK$9h95@wWO%8a7AEO3;Nlqd}b<&6y<(Lffp5+q}*5J zSpnnnf|T7`KYf*2A-}(%%a&6c!TD-`=Hf$-m;br{DyF9i51&n-rir=yu zfv6>(Za1p3+9!B;ys@@q6!BXg{VQfeV6IH4;D2q{0brzZ`#AqI-biDu5Ac*}M*kw1 zghAWplf|;p`V!=FkIeS16TH!MBe?se6BLl?u>M7OV0aWtM8C@C2zvfaK1b2>ZTT$h z|6Tdqhx}dgc@#bWd4g}BZZr(8N(|7oL|73}Up+7X&Mds2TV@y)OEsiW>;Od5;1}*E z%AG3QW3KqWdAk`#hoS@aiEz~R9ORMd@KTx9{KyQWWT`KIfVY$0pc;w=+Z^<6_T*@1 zYhUU!20;ZjrE;GH%&piDykP=XMoOphWeJ#3m)pzlCm4rv58RU>79vLe$hRgM!;5$U zQ$xhvR*LUX8~Ao5XDnPjq9AWR(+Db&7c{P=&m`w(lzUaU$6Pm_ar;c8r1u`adZtmx z-Z7cT#1}8F4k1%jY*G(8RvbA74&5Ap*Ya{};ny@}deGbZT!`wdC>rO(${#MYJYN z4C3()BcN#|>?lh9s_)Li-%`n6sN^q}YR1D~km7G^N5Nmz54_N9qf9Z15+7M46KCn& zG-rvt%WR{LbxHv7a2%~0JVYoSDtLHkvnvY^^Cb_o1_~MVJ(ih=S|3Uo{e*9VLPi6Q z@mJuX21UVPFv>tlC(y&ge%#?BK9&XPq~oG$DV07NvhS2UY1reazvO9+Y81fALdsbm`#m5%F`W2 zcnyjZA6c97Q4ALBs)h71q@x*pF-SAmLo!$%D=qPnzqc0*4&&241A}FYXa@_vnqvo zq-$oz6DbJ%FN8J||Flzvla9;U2>$&r+0(NBwI6>w*CU3F=ejI-APx#&wdKR|vX@stVf2`_G3>AH`?QH`*x{4vHF?!tc#DLL1RD zDbmkoI~ZV6B*jccD79qfB`H#kmV%k2+{8>%sYs(f60M-K3!d?j&*KC|2w2Nn{;Q}P z@bVXyv8k=)*`GykFwuIAagHr%*Hf^9wqbLi_MMjN`}pC{jer6@;EP#S;-_c|6n*zT ze(!UmRu0lG^8;RT0SZCSRET$G5Ug~jLOg|bWGcif*mBt70=Y63qE;VKh{{VuA(AD< zGsrw^A?BfRfU8@35s;L|&n+;@_um5mZsX0W4`i;Rz@xRmQ{7lel4z{Et|<0inzdoT zGqGpQ${uw!V6UrhCiXh;-V2Sg|N9Lr$46d5KLCQze{r`0N(b(&k~?H4-X^+-EwEbv zXrTiJ3}_5iNwCjR52=atYQ#@x;u^tldg0NGBCN^eUP6BMi|~w(e20Dk9Cpc*U4FZvq$#&(Y%ph|$9iv1CW|PB2qHsA zQX1u(`7EX&r)BE*R7n)q zv(p9feW`9)=4Nc=<){l6txeUS=BQj6FE86(3oO3a2_UH`)UPig8FR zs%A!1KG`Iy!IDr{%RioN6t}(+GPyuUf~*OLa*0uh{Zlj$`8B6r8)!!GgDoiveUxf< zmI=GDu8lQixKUjwZ#Q_ZxlDxIG-cf79 z^c^4;{w|;39%GXsGfKyFH2^R5{+z9r02nzY7&WEA_Cdkl6 zWI(cgsq-j*&5g48Mhm)zlI|X_ywnKxO_ceZShCb8QRO9M6Ce2`R%GJQr|1aXLeEye4u_~m8=yjS1W_%>^?0*r@ zkj%^%bzG&Eh_gk;*?F zCF0nB13U4NWo4YFn7`ws!77R)dTEF1q=Q79*b|;Bs;r53$t$V)=x zPs$+upFZbjR~Ur~PJikNN5&XV;^P<*Um2w>6_NjZ-sdZ$sCN*b{FPD8`y0OYE2Dz< z6@He`7M}i<5oVe7n8>z$Wfb>L;Ek8#@bQoryyI6!SxadYNrtc-ulTm*IEC=nEB?(_ zMtNp`&C@_qWuJ?>COQyhbLO-;4ibBKoenjMO(}d>)hT_PIo)l7UFUg+70CA?pS%LY zpU!;63L}VZdCA=)8LlS5XTl3t^v!7vJoqblPO`Unc$EOSUOIAaT2T z`RRSY?N?bjPRNGKWy6=URmp^GI3ycR5mgEX(}=-Jo~j-w8ZvR-Cky9!wEyxGJYzLl zOK!e!wNW)+5~brg!O7!SV4*jVKVEHA%Do;UY@M|MH z*GR+%&GN&-*C!_+AMiC^eK?)Y?|f}kDbSf1_hbOd4T6%jd&y(gV9)d73m!o3jF)^Z zT)Q<3Ll{7r;dAi$!PGxQ4bludnDFxF3g0(<2~QGs#>;%;1c)O2746!5NVp5>x5U>7 z4}{TB_%z`VQYR4}XGaEm$nQ7NqP@(`1a^qgI2j6a8R2-3;hzS5Z-^sgKdOQ$^zPHyP__6ltdd6I(v|;sToFo>xExD*(cP?Ve-#Q z{yWkBJm%yenG^Hx&)`2lvsFGw=EuBF{s-RslY~!@pO26zw|`><*~!pk?K4~pK_l6@ zMad+*k0RJ1IgBE|YO-R$9(oP8*@OIs2tUpCHu8@l|A|@Hjwk<0w@Q}Vw<{v;Q($Wn~pu!LTPc0S-nDmO{F?UY-S_F)h{>bzY(*pb&h|v-l%Wyw1ifMN&m%{Q(aBp z<1WxeS8Wfr%}Dik1vPxHj)X7Q%!+f?eUFnYgV#Zuv%WFA#k7e82`HqV1B!S(%yn9t&p1d9cmdn3k^bw&Nqb6-8%`ug20CpU&_RDNY#&yP7TL z_cs_7n^nMZd>Is_gK{Dz)vU7IOF#pLBuiIfGArLR*Lv}dc-7Z?2_Lu7s9fmv+JBLh zl_W*kmBswhM!e^CWD)n z9Fb^J~5g&G#P$B8$PS?bQzwkv0dVHjVIf@>{NoA+GBzwD2KVN$nax{>3UIt zN2mlf9xicjjfY4)Pvb!ndot`NaZVlH$7UBItfMpRrXpw&wwJhs#&Hs-Vk##XijjD$ z#!(9E4C+gqtR-4k;_ChbNW?EGG1u50B0cQdBUs|z8W)l{r^W>&o~M<_U*gUh2S{8(W0hV`jTHkr+#Xp$ zWuOUE3EaAbrB#MP#NAPrT{XPDFjb49pLl9DPRk0&n_9_LAk|9Jo2gdv1`i|wOm&3> z{*gFYVtZ1g@|e|75vT~QbrodD2z4~}QYF;bM-?C&=5k=zmM~h71k-8Zklk|16;j@|{ zB{8nCl2BERbrpL`K-vRUtE!vf6^Y9UkwGV}N$jcfTN0<}I=>_FdX4{(*kh>fOWayF z>$?)$JL}HHlpeh`ejssUji0ClbR9pIxSYm+N^I5mnZ(K3626dlrN*xXwp(Ut57i9k zY3!B}2BL{&HXKzoekH@7)KlSPaUj8zS_=OyaXGxIE#g0wI2qovB>-f*7AIVB#iac+rw zYwRa+GmX^%rK-TxoBmiaQ*Vm;rvXGem_4%nAHKK!WN(Dl{v}IrQ(G>DQ?wmW*sbk= zGDMzHv8sZV+AygqDp#8%LjAw0f|1(PDof=tOv}itt3v>xUIzVDtOyv2|G%U z%bK98#OpQgDe-uXKa#kr#{DF2t?@vK%V|7RVyng@CB9!?_y5YHv+g{`sRVSLVX8!y z>#VL*HB;)WwOrBX)OfU{Pp+gSHcH}w8mpO<)vhB9lLRT6Kux7aYOJPGB{Wvyo_4RM zVxD&YiA=AtF3_hEm&=CjLAqI}5_u+<{}=3;%c&9v=>*5i47TddYNEu+I(;>HU9X$z z3=wX(a4k}YjIdH$t|b!Z)EOu|QfIJKhL6`7sOk7Xoxw^O-dd->LE?eBm2D-A{@>G1 zcT0k1I>JGTt7@EMoX~;B8Yj!}WX<3niC5}o`EIPBFQGHsr08{(?*yj)KUGKAB?*Fb z_e+af2*0nx4@>N^6F*8^PS0#o6&CblYG0tE%{Fnl2X4sHZjuWc_BXI6s}N5Z8pVJo z1688?dK|ALbW@jDNx;)8zLyNF*Zuo;iMMJ~tGgVa!e-BT8E#M zxU$0ky z=a9H+8HIC89Hgqtj0kG$=8r*^~@M7TgX-7Gbc_dtL>2U>x-Z9;|{ zZyNbJW9Ulk5l8W#<_Ii~d$d7^O-#lT&U)`THAylYRj#Ct+N@q`-IgpMMM?w-Jwo~Op1r)r)EKGsA! zC=GKsy9bg@wG>c6)`U(1zeA0Pobeh@GVkn-PL)j)I=>rqWK^BfrKI z>T!cMv@0e4RGS%vpUOtHOj;X{<^!M&lDQJV4{0Bo33< zVR;xTGp~XDL}%q`cH~JydMayi7pO(HM`g6fdIWV!;&mDym-vu2TE^E^e7m-Ue@Ktr8b6SDosRIg#4B{Tmz>Hi(fCn4p~fpvJW+th5|`ClGDPCy zT1%#2f)V2^W}ga=*u-mi*8nXMf=@v{B7w6q!{geNza1(vn5wZ7>`aZXN%}2X0>3G& zCG@Mrmo&ptLUv0&ttKkNLR!R1ZMNvSpi;w2MYF2qA>Dhbc2G!5OsVlY9j>aRte#n2 zCWbI*&8IoKDLI_0Rp-9MBQ#c3QdTDDuuLf>RjDPm+GCu7RWR0!P5i~0K(97Il9a!Y zq-qa`v_-if)4fCmvs0&~MD?>S=~YRvL}SgE#&=|RFU|Nvi3@407++FCNlNKfQ;k)6 zFN^C*tE#(o@DLndkqNfa5iUynPKPT|ZqZm(T`!GQcs`9){GW?uO@D4NJ!#bv&;-BB z47X^kN;FmDM>4#Z#?K_KqVY?K^JzSMuu$GcS~>5^@Rw!L|A-PRg2$R*ij2@oE0b#f zO*K|^*hpj5p67ew6P&Y3o+xug!&AAoU zM%rKE-MYCImbj5lue8KBbd^?=*uF)Vpq})Yt8t{np61wC;wm!SVQC3b#W?-yqq)W= zoIt<^CSchXg)@Jns+ zY%=_Y#w`>sq9oi_;t^V+T?nK7SJ5KvB?%7c34I$SO5dkm3UmA|Cz!b!V~a7cTo&7|WRr^#@SAyTX8 zyLGr)C10Vj+FKf+v0CJArLo!y@)#<++FE(64aYl`p~k*)mYhQ;pjJeW>u|N{JyYXw zNq{XLl;#=qZ6k99f&wK7sXNVVIeGQ5>;W(k9jQ+U5wMR1bsy;Em$3vgWaG12m2#z z^`Ce`o#fLASo#WlTz4jF@oAmLfik?d9vQ8d40x(YEzix>mQvvxS|>J2`o}@K|KA`z zF6lb|O)|7w;};U=kPJ91f1*Ur$YV&KE~Pxoz;03`t<#F)Op>Vu>z*QBVlR!SOKd}G zvix(TM}#ilJc+kyd2oq+wF<70c$7BOn^k;`)#BYMU0${Lw^U=L(%V9G`PE|F22F5I zCOBK;>k{|TSgF(~-DXscM``?zqz~7I*+(vmcxkK_imnv5E0t4gWrwxl-1?EwfD;<4 zwY6;;t96!OT|sJP=x`yGUbxCoCgJ!(h-U)_1um3RY|RYaXOlK82= z!Z8xh*Z4z;XKVVl5(n$>&Ju^}D(oS#-BYK1rH2O(lK6@)@nDIc>JlluG*~e-P=?k{mEjr&P#-=Py!<=>~VD$gm29hSj(aQd|-_AuI8l~Xi14}0BZasfK~0TN%( zEyCGb=u=H?16B4RcI_d>VY#kHYvWY{8qbjUOg8+3&S16-f20-iGl}nLJWt{uG+rvP z{ftgvrS$MDG;EZ3j<#`|B(ACJZ=1v+8hE~!1 zps;STnqJ!&RiMclyPL~H8x3^=UNV77Ist|AYNMh`^tY~KU&XM-c_jAefMRfr4iAyx z!!)k&p**w^r#-4jf|?pvmpDY@+7kO{9HZzp!|fEk#(fmM#)BmORx9NwyYyJ3JwBCq zvc{7oeq>cvZkogwG)|QGfW}UV&uF|r;+-0Ei4!!ouaq8@bQP>tSZnzjiNmy#ZI!r( zHnRsL&d^GFQsO%rpOg52##ggqd*mJI;b|t1Bu>x~o=KcncfA=B*VN52M+?zvdgub= zmpCS`l2}oRD`{L-;sA*;{;eiG8ftzvHDx2VJ6giF;(jF&aB%xW|wzlGtM>R;l<}f}13c(Rhc%*9)Kkb{hYl z?tC8+Cso5XoF-Yap49)RzW_*>nVD7+{eo}3N$1D4}Im0+AENR|oyollAQ zdx<>>Zk5=R;AV*_LD5-%D{+j*TO_Weu_{nNK|TK4B?&S}BqWjD*%CM;ahOiv2Z?{s z^a^|8ACci_WVpl99m?YjTn-(^w``)2!FCW(PCX+hbQQwy=BKiDNL)(Wyk#<}=hEaj zEJx6EXGHEsQp`GU4rUT5MhT=9=mKx}vd#ho;IlVl8aXZV@M`XFM$z&MKFV;{D>e)P zc6@N~i8&n@+i0!fL4Qt5G@kNU8i(+{zZq346jJ`UFvg0{H5j#w+abjYaCImhU}Y*x?%{ zyuOlNc0`zqNBVQ&)+l;KNxon5-apk6uP9P2pLm}7d7e7y_yx1ZZ{?wg@;p`7wCC}H z86*z*lgJAIrCQ!$s7I_=6Y>iDKw%+rLwKE6Ui*lSwlx#eae=yvu9Q!;EI}asx%}}W z;QUm}X9}lUChL%wzk6PrO0|p#_8uxCq+0rTNZV=BGlCR4k!qQ){UWoLu(~H!nCGdG z=gC*cnvRsrDm{6d+xS*SIjldKMXBL9EHR*g>nQ@uslWnI_B49U z>X@f)&~oK=RjOI7hai3q(@jl}Y}<&N$Zjuwa5=>9@OK=gH@NoW){4C5g1p_uPIz9} zhhss>>9>u&zPq0qw_4aABOzY|?&+H0w&%(E-%N0gZJHbA+Gwji-(@dv+fre?tS>rL?E79EX#18!aw=9|LWi2 zDe(Jnd4~JKN@RjtC)(hD8{IzfO@@2b+6?y&Lb$>84`@da!)EM#BU4L};z^pj@ z|08_My0`9QEB^a`Ey5hx!)s(Co|>KDKf)K5#N`YT4RZJZ7-9G%$9KTjmF)2^-nb{1_wrf=_bJ>W>2KV#E8wZBxa|Yi`CHw^vm!ueESz!!7dmjXNR2 z%Zs_+xWBFD_*C%ifz%9$5Of|i{TEUIS0g}gX18RdI zBf~um?hv?t!<_?nXVG8&1T>MAm3-q?9pzP6ipMN4UwejLcGzl z-8>4c2X4I!4&4j~QiW&*)&t)OYB$^O7Qz|QZkDHEf&pFxE&*oE5F#Jg1PtuoZt_>{ zi~ug4Da7x9?ZA5mw43>}gcu$UM}TkLf}vfDPkr7tq}`0gHl_}E6nJ%5yBUr>rein| z21|fB=|WTkTOgOQ?WPOzB;Zzj(N6kps4-wY(65^ijljOZ@KNn%G_V|)1Uv`K0H%+I zW6Y0fFEZC6Qa`5M+zD(09tJvYM@fL$cfepEnj{{M03Bn|SAgl`+s!gy+g&Ibu<>rR zDZV>Oro%B{BPPCdzQ)v>53C240UemkDp?NP56r*62pO@${qPvypp~7441jIGOkg=? z!mYq_z$#!PX2p77_=0v*^hAy1AVXkVF4`DazXX#NUoS_WaUk7eumenb90decu0#z3 z8-dNh*rGysels!#MgtpxNx=GNkRh-dA2012gbd3e2Rb&PWWXd$yJf)2t!NTp$!l-~ z-yG-Ig;4`6e*+ou^}cpT5?pyN8K4A=&223DKk@j8;kPjO@c9r!J$DquLU9vBNe2}}Yu1G9kwZyeR*3&1mh z>GhRPJ$@M4`;OlD}jM{YpEI-4r~OD0k#3B0qdRkbxP)eTY;6K z9cB|><2OL^`y*W*^ajAeWpD_Xj_F-Gkq#IQ%+7}#n7X%LmbtHA2V zJIwe{7+l$5<^jVCJIq>O$?rSN*f8)QHNOpJO``=wiTnK1Ch}NWB_!Op`^e{ zU^y`R`400iunl+)C^sU%B19aBbPY$<0iD2fU@Wlw1sDLz@(!~S=y(w|15nK8P*!x9j$4o+FdCST#jg;^D`of)q}$tJ zMq;fgd8?zy+=__fFxr9@ftA2E;C`Uv2r@(hc@&)qSPzWH;?xFA1;!rhFn0ozfc1R+ z50ngxRqQ|UQ$WCc;AUWYBdQ#j4UEU)mDB_Wp`YG_wi|*-Hi)ZGXan|zLgl|Y%xYlx zSu_n+x3=>z1kAn&L%@2VUlcNGMh!833rB!Sz*?Z=3hWHW^?%XJf!S?ns%Vt>I%;AB zGH!=mTsIbZxy_x3GZvTxOa*2Gvw`6Q-R4PPQ3)dQHY5y3#=vrXe@rDX zdngWNTFql5oZPo(or@;XjI0tM2wgKfGsOr1i zW+AXG)onHcWf~lhNBS9X0GI^a3@is$1Jlz{QkLK2Hp9olG2j?rEO6RbjQ=D=vOuH* z^I2i0+uRMz2G#)^fmeZTz^>RAl+S_z-Y47-Jz&y2RP}hsfzyE552A)#m`CQrQQ$dX zCD5?|O$tl`wgK~jj-muqJtDEdMqoNnE`%e%Sm0J*3GgVe@?p1`ln6sh(Px19dD!$J z!}LesDDYf9YG?w|FNZ@w2QU&C`27uu}{`_4mkUvwE1msVjRRRO? z=&l+V4y*@`0X72pIeiKMpPXz{{@sBcn(+qOe%*#U^$Q9;m<~(=mfi!si?BBn_AenlP_`iBSxASIn$5F{P_kQ1GbaNIBTcgjSU(D12YfGZ zmT8v2P$O_N@EmYAunkxTlo_Vk2y_510>gprz*t})GEM@91Ji+HfYrcMU_CG!*a$2r zLgXAGr-9-3nr0g?8`u?|)&s+V;rR05wZM`&FwBl-nx=~#1*QYrfSI%5NS0|90_6j6 z7??f}--88A$}!E8z{-bB^C~d=5!3872Mz)K#~@PjJQC8L2jxIp#UMKVpqyEhF}U5F zFH{Hwti@k7IN6EYig*<`%pg`^y(;`QgUeQwV6Gm2lX0(sDa;DYoy1=WxFw1by@1X5 zYXTSJCFr$md9zIpr4OH(!R>{32=gd>MwH ze)|1fF~B6$tzQ~lV>5kc(5M_a&bIg- zT9G4De6nV>o5#Fqb`^dJ_H2$E>~)d+ACgnOj?z63$vbSt57NemWQwgko6bChifACO zTsgq2i9&PHrhOMua<1$j6uGe7T&Od|+$QRM(5?$+XiQ7;)nYl&tB$-LmW#X^>EVar_T-mo*TZt9ExwWlFOeH1GjL}O+sXVGb~1N z8Q}QG<8)`S^cI6l1$VR7!vGY;;4)1b^@yC^FH7JTDp@wkUTm{H25%F(MC0-*bQ)>T zBXXosgG(34Xsh+6phr>r8NsycQF+3abvLcf zmn&>z&=>R32Pe;cvK2DMH}hWn>@N3|_0mayUm*wCvSw533V1Pj4&^=uFETP| z*JJQvYv%vmYPTH-;$2sG;{%4Sl+;ua;t;$G(OjT1unXF;AlFy7^Ju zfneg|z+U78qX*!Y77LISm@5R=L?eDDdyZ{~D6^BO5~9f~I?TsAas18m)!;A>S%FRP z8A2VcViSEI>o6bcBx-}G030^^R$z5MbjnI_!#Z(&!PU{HFcEPYB5c{M!0Jg5`Qf&k zXJHPE8Q>fRWG}>QoK!$jg{Yp3G^J4XvF(1G78J@9OrUQS!dhWPhl%CP3T!RBw-ATH z6>5&>xG~^jx6zeCncJn{i4HT&4~@ELq%|cO4PM@xZS+8a45mGm(l??23EKXC-Nch& zA+tsbVQ=tSm6M|0Mc`2$8pqj{jax4+wmj`*rqSFg*^3gNfX@|A z(!3|+bgy=L>j^m^py_EeDVk5F)2SygasP$9isZn6oyZ1LqmXw~RFMpsG~}HQ6PH%t zvh3`G@_@q>{R>wMu6TQgd5h*^fvM;MTfw>5R+kuhu1MZGZr#!AmXbU_0%gcrBcJjv z<*tI76+00rf!~-Mgggd$l09&W-A{03C87QcUZvT^GI1D}H4t-d-^vd28c^m!H_;wg zYD=)=_lOeg&NVX0UhL()(2E{fEgf?Pcm>#<6YRYb?171Pzr-$9V`d@cDHL@n=Qf_b zDfaTXg8?f$SA+*#P-;>&NNVJmNq z_XeBQ`?FyuXJ^MR6S@;R1>mx)DPooE@52X04b?P#l?>??kN)k1?s2Rha#ghI3N1G8 zb>6fsN8U_JH%R}OGAJb?vpJ|P`59-{V(Z)$xbCZ=bFms{DD2RC)VfMW4NLswy6NKU z7dTC{r>xd;)fG!i;M*PMCrBrM7fnUEauDUMmSdu>p~3LPS;%Zog(~XQ>8$EN%Jg?T z%%!X|g-)!NgQxC8PF7V6;WSxmyq~fOCvrXq-GT!hCidP!Iv_)Xtg%h9J8dp|;6%G$ z5_ms!t7_=X=Z3vHoaR)?0kmL^>>rQ@tKV?SA85lGOp^QGr(J7gO#hM(I!x4;6iTAmo?eZ#G?bK~UTx-XW`7U&LyXYhV@iJTYVfbI&`HKU+g z^_voBf{zTv;`b3wX}susi5%8*iamOUeF#c{|0z~{B6WRI_8M3UMHh~Piea2X7XF+V zrP$3subkr5@3Y(QmSClrhs@rMkmwnn{ zzRpcHHHzMR80*u(r(|fiNG#JWpLLk)xPwg6c`ScQ4(L|M3pft0J^{!9QS>U*C+r3} z-P2+nw<2TJV$iWVFdrC-lO5(DCJ<8b*Mu~E!EtNJPj97e*xJ$1r{%zySZrLbpcp5) z7zP)k;y1;JN9qjdRDXqKl^wiHPdqJSqBet@ckH^E0=cYbFVNSFr(`Lbiu>7q$Sc3@ zFn6$gH=TJ}hS@KI3&*k8Ww!Gq+1JW6JAe6jJ4(ae%G+tlS{c-BF}Sj`9cBl-mi=y} zMTPM8?X_}11aH&!Lc5x^+mMgF$-8q8=V>VmrusIM8Ifd8Dx7_MnpI;gCB>bbmm5~rC(EKWr*2LkC zK%!q)2Ts%#rHZz@&E@p3jnYAXc~6{Qv7KmZ z0>T0!0A6J`KeYIq z9AMuM$uSJ>P0&Kw&paoCMrESUHldEJ;Q}7T7J1;B!66eXq~gEqi7v-=$yU=Sq70?U z09WssEc($CWfnLTA7TB2y_VXgh!u`_w_=~`jL<|c=Xp=`e`}frhD{f|EkZ0kx{rA3UXT=5tr6H z&tR{IWh`VTgJ|5VGNcFUL7ar`{9w2FXT)VIxu2I|MShqhPM{f5 zm^+D5*;B3ZaDJ6Ye-ZkAh;v|WKe%(?P(&**cNAO`I81%Na82M&chZZ6qz&9jaJ-I+ z>Gr^bcEYn$9WK)XD$$v{Ir2?a}syN$7s}xGS1F-1b*<(8o*QN zi5F#%Px}buH;A^ph+F&c82B}aF25-IQ`3u5`b-1ivWT#ve8eXYa+?@UdL~M;J2UNp z$>$9Yh;9!PCAYG3^YZ z1Dj+-x1+c-sY1fOJl$Lv$ZLd?dXgM8k{`bc>^&>FJ-)-nz+U0kdA)Q&oCKYWVQzB) z>-=Y+ng&)8_8V&z3XxvNa zrSaghZgca|fsl{Vyq7S$@WbT@=vlK-GUuXpT0FayLzXcLNBFECLkC#D0o*lA?v}ps zt3G##X0|^Xqk#3lryg5mSVUjkN}TZM|05Lk)on;B^!Yh;_!u0RbDO?LbGOLkm{M>X zpl>ZTFSCB}Z+1WIR~2+>@!$-viV9gqC$`9_qGoWfc*-&F*L=9%*N(=lHO~FZk&Xl0 zR&aUX?nInty(Q?ttXmC!CwLe02+SP@*9Z>TS%JCJ;2Oa39D*>%QfvX)3=&LWMc$67WTM-|ylDmY-34wMxXu-n4sJ3y?@oF-;P}a=y%Sdm z&IRtiPFxwd(i`+D!L6N$Ct&C-R^W{HgDiofhvP~<3a%I&meOAW>z-2vOl+~Nz}!`E zmC(h8@fWUZ9OmI0GIxU83T}k8DnOMJ$AUa;k-sLM46Y5F$063s1lIx%TN*21l!!cV z%@fJ~iVX27Lp**GMZJP;aV_F?lbH{qyjP^0&ChfLQ!v6gsnvY8dAvYd-b0oR*=)CuL@iuIC%tv!bPg4o%=Dzkw?aAk17 z(?4C{%D|PTQdET;H?$G)oHX6%AF)TTwENv-k50AwW!R%Lq1y&|#tgUVrd9K0Puf`_ zlYG+0g1eVmE3o(D*HCufqbH;ll)O#O#CPB0w)W%e=F3Pg2lYXPM0@TXt<$4$>{4=2 zfJyXyAs!>#xm^wj%>&mAJ8O_e*rwT?_uKs@Bg*gDWur`;bsNF;n)$!F&ET`vR3LsdledBWyCn_A1X+LGtbvYP z+MAx+gRZ*jRXNPZ;eto^(LSUYG!603p~X21KK#;WYL;cl>Q}kVC)Hh3ze?H1rvhBx z)ie$dMeLKYwm9x{n=ukO{En7XVp-S<&Ix;q(1da+tny~6)3MJqWK##7W|ZO#;&Kt4 zujCC9=I7EZw|RiM0W|7Qyg>rj09$`y?gBmWCoGMH;F_SfU2)r?7f}yxFk0{<=H3kE zp_F&y1}-NNOV|VCvjZErt%zqmKyf?dD0>DrP_gjkbsN%-rzdv6m!06s=V3F6jUg8K zV>@K>jCOGQ;7cwKrTeWsT7I*8LEc=4l@l=86}Zg-h^rwzlV<_z;KPL(troJQY-4FK z--$Wt>DRC|Du<*5gAxTnAB1&#!*7n|crfpWjsveY+{ZQRA534rCPNc0g3LfdYwvL$ z{_0&pUnD3=!l4gdfyhRjk3*MWZkgT%Jr$QcGAkj!NC`V-PoG*u#TuHuQ}&a5}aVYf=-q z(nHtHKX45^26wyq1fqA?N`2NF8{~7|+{a5`>*B`W&I_Btodef-`ri$%=?1P2-02&* zMsOz;SH!+qg}DgQ@#S?srLzh%%c>PM$Ye4Gp||pZ2Ff$Mt%R|&3HCypC)KR5@t49+#z{>v=O-=35S z-g4+R?a?`q86Hw|`~pjQCoUR%*GX8To~K>AFlbzePd-d%cHtx|6LA4u-d79Eei^7e zb|N1pQa#zTm&V(ka5@N$+#Sbc|(lndM#ewckw0+yC^jh`JFcIPm6t!b~GJ(Lne=G z2lp84onm_dY_H-sV=xuG6Pu~io3VZjf=)+IeFD21N0<4Ke8H(DBu6-P)o-@LoO&~K z^51uxQ!!ECX7PjFGI>}txI1u*vYJQ4XzVj@*hX`8luZ$0vA0RX_sF56>u@nmnr0{j z9Jq%yg3IZ|ab`KuSPmPpAK)SyH}mvAF{T#ew>WYrfr4zXy!7upJ9EM&=5~(kCEd z_}#x|7~*#fnf^M7${nhw{Df(NC|NJviOxTunF`#$gOk zxph;aPxrkkgMCjUT4popcW=txMeTfH;A1z?5qI&^0^29pU46tIq!-qqY#;n&Fn173 z=6$Od3vtQvF_4%0BCYX@{1c;-JcHBUWSaN>Dj(ZfUNB8e5S2Yl^IbTsGkDp%lYz}J zkQ8L9!<=+>E)^X#smt__?0zmJ*$;Vs5Y4QT<9yl?7o%uPmF!nEaXxZ<*=DQ@6i<0C z-4ZDNER7$!iSvfh(M_1XMoTyGO&6$Cb~RQ9ii=$hsJwfVu`y8m_a=@Xup1YlXnHw9 zj1qhj52`!8j9M?z>V+uyJNJ4SA4lROjgK#O2N|D5ilZUMSCQgGKjU&_l=!N@(Hber zod%8;{t#;XJ5scT8b3vf3;Y=5csSEXZ&{COeQl`m2~BxRjw=csu?NRbt5YO0*?ZM)j%{Uk& zc1vSju=q}{w9Ksz7C-r*r+?#XTniHK`5DK9#Pj~ffnagTzsT4ZEZz<u-MUqHy z=PxdK85REGpSA(}{l#`~(BFF-hyBG4iRcw+?DQ9ZGs3F;#UFe?T=p><{KZGUh_3ZB z$`JKKw8Y=o>~Cc_V0F!icjagwuOal%hj^jgn@f_YQdVMF;!!e#8_v zr3=TryjvNJ)(NsSwrDKgQ*-LDbW2ic<*9eHS@JAE}%`=MGHO9 zD1(axuPO*h3PdrG#i^Rl*8EP**J(aM^G7rvSgMF6n*Un!#hSkIWzM7w|dDjMt`%Ffql$9yLVJ+XU%^DoUw}^At1c306SytP&KVPA){;+)pX#5T){6RevZa0dt2$P*6MXq zsXO0M#7oGJ!*`mm+pn%CDiHpdvN+qocNK9~^HVi1wByOTT#so!^*u{*7?yvPf`6cy z4K-=U$7=p-%{%L;^>Y~oze7*T*QPlCu97utyOF-yk53doNAqc#&pW30OwBLXd;@j= zLI$UZ1|_~!86?+&H^j&`~~ zZt}R{qQ@!TdP5TWX&ydN@!l8|95!m+m1^-5M3Y711syGL-mMZG(0&g207 zV2HCw9}eudz`+O7DIxho>b^Q_@=$i2e?xwM6sS16d(K z^Ji|*U!ZxPJC)uX8^txr!A0}#Qr9j8;w!DUUGrVFS2pad*{i*&O3~xTK+T`f{5RT8 zy5NLQ!*bPJQ?$Gb&R96Vv{{Pq6m+=eSK{G0(iPSHUZtzo z=_+u;!RgALQ2YtaPwuVwkPJm=_r(vIU!r+WK7B%z{wB?bX*+jle!t>pix0Kn4_Z)g zuOj$aJO?ivSh7QfbJVqmU#$7!`xMX5-Z^a3`~kXjRt}#MorZZB0b|h$pJ{RG{p#9N z&p}xKIGZh+_mpF_<`2>X-=G^`rpLd*7^&m8@epd15z(f#dSubTZ&0hnyOi8ht4I4Q z>zP?9fz-|p9iaHE2NWNx?cJ^Ua?O9J`D)Fl&r?LDw$sI_>|A}&;(LmConWpO#Aeg# zb8>FTf@~%5bR+*zm9R9MuARdiaDs;Z8@1-vYMxqnU#ktCuabNC^O~Qod45*LVM3TP z`vGl(x!x5QRdr~qVv`o1r%MpqRxhNG^D^pzR8y&WYVAo~t4^UNHcxH$2v^pfIVv{~ zpQHIEt^c|bi#?kEoL)bV!nNpRe1gm2epCiGeP*t@{)+O_0Ll1 z1*}zk-^O8x0DZ;3e@*kJwEiB=ugX^hpX+e=2)q|w@2tCM z5%l3j*}Z7a<4VYfI2_zM@mGcFxm|9N$hl7$>#c3RJWSXB z+lsiP?R=p5=yw!votQxOwdRX!w7#JeL@GOZ?1RyA#GMn6csm0FwOcgt>y`&NEn%SxbY-{J-Ovh%d;3d`T1<5wVe znwGJ&@jJO9MBJ|iji;L}h`~1(p!<~FMqw>TR&kwjTX-A$J#A@` z8$w$226eeE&A%|`uzt%PO8j0XcW=0^h6TTH&EZ*k>wCOsUG=>j6B8Y(@~X2WLi8LZ zL=U8OUQlU#H6H_>Z5CHhNUPjoOO2<4t?(uF6b1Yj*Z4fezW>U(DMBiHuPG3{wD*5%_T*hZ&mBs-@X&uwQ&PZEuq&Laa~yK+aDRIPk|w+0QJz&hdjx z4k`Xr+41aEQpYNJgF&r7$PK;A-ca&~RdUe>PqH|D9li9UoD>qR_t2hRU#i8D)#o$VQlxr&w!JVG0tZc`vRrqC)CkQDRR&EX7XEYNMks9ygL> z%JkNMgHD}*jhWR=_07B9R)~GzIa%s1O7Sw1L+ajAeIrz-+lLLBW%e7|u`5p614 zw*oN@`!ts4eWQpGI^DyXZ)v4Cn=#jx_bhF(8N5lx>Cz-y-dXD5Z6w_vYVwSSIMe7&MR{E)V)ar|BO(iAgw--5l2{|`M>Rw9oGop%8b@l0c0HX{%_VGzYGot+d5qtO#k+jfGnwD^xF5$@B7RB5pSJ zdfBoD(y1WyzSKPw5RAcIJXBR+fKE_=Es@n*(NQ^I0kuMz=0zK=4mP4v8jdOXow{bf z^~jqP|AS7~Cr{VM7RA?V{p?3nKE?m0Yrz;56(7^GUd9I@(Y3$LcLw%I+}oX_D5Wj6Z#wX|IeKFcA+v?rmLMlP{ZLocrK=E03Gj-cE$3saR7>yx3i{ofZ^k1tHJ{3 zG?GVnPFS^jBB)pytouUwt#8gnw1=lhLXF`zv7%;ss4)s1{tY@6hWc<_r?i3W?_OFx zkTZIQ&JIMq1)imdaLA(<(e`lEUsVBZ9E6V5(4y@3Q6S!YQstXkP!l%TIOJuEK0=po zG3JI;omU;al8c3~bgg!9EzKK(21?DOfT75sZb;4Qp~iHqtV8H{1Zu2_dJn_&j_G;c zFzBJ91w^7I%8t{KNEBS3RSVDR^Lm-C$%V?J9V$EVf#%Tww;Ipcq9@YvTT!m8cnXO^ zd7bf;5{2yZR?tDlra=@poUbp{>>F+@@Cu36JE8zx!|%PQTV@Z%j6lb)@=^Km2h=z` zxJmhum8f{#pyFB0SEVZc6QwRb1kWvpspNBiG^C(YT6ac6F>vCGM4S7xZBWP(SKF`dseY- zuP8Sg&eDI*s! zxh?f9+2XjluxK+z@1c!xTpYd4;?MPPIP@o#E_F18-GN@`dBEVA#@<19V7tYKY0Dkl z>^tZPV_q9wyMv3rf)2#v+I6|+ay*Z()HkVhEcb&%${h!R_>#7agJN_%%^r`Hq~THJ zakI)_oPI+W{9iRT7rJpsRjX3Kd~JnIdz1$aqbN54B@yRnUjhu5{f*A@b(JqgB%+@? z_t5b~uAx?nnE-k81)4X3GrmM&NhrCCKbL`^=OwXxpR(&*LHm-BF4{>wCIa*PY1u^B zEjvXA7#rRrTQcnAHP#$V=8<1Emaa`Qt^~9^tD4a5q}IvC;E=#eDg$gQtdO~1Wq{}2 zF;ig38AdCoz%kq;?V5tBcKNFG4`}`V2UJa99~G7Y{j3X=o5K3rXiExKygkPj5K$kdP@k@2z`9bqyxgvUL{Vq7N z;kK{1N@;hpL;s~McN!aFZtQzQ>O3P+*-g~x@BE8OU;H}Fo{j=kRjC5|>3q)|ROu@E zQOI3zyzUgG++|z|$s4TnJjeD+KUQ|?E>T)4{6J6Gl8P2@`jQgwM$6QFMYc3;NC7jrr{q%Z3|!-uZXaJ~VMCnGU3U+yOUFLjrBBGh zba|#6RrZ{_RXH&MSz(^$%U0Bc+=DUjKRw|y%vn6RE7~b;CNdCvXx~gY)N~7-gNMhUBGY5Q!{m~%PJn}bc)jW>x~PN;MZkJCZOc{dl3$@!k8I7W=A+)ONH z&NDRoKFAw>rtOS@=jif%oX=SbnG4L?Ln(|cp*1V#8p9oQY?(2TUSDR6sOeZ{d~?L+ zsGzfBeWC(9ySVfV6f(}|kXK*&*EpZy-H%qO?X3rwQ|Ne~;Ws6HtY`~gc&l5_n!NEo Hsowtw9vCicBEm~#4hJUSY46OOZO)uIGjnF{+?UK`-^^s+G#{S1aEaeN zXSdJo?dYG|=UO$^q*D7+s$|z*i)K&UYV3DV#o{!*H($U!)+EJOujM+Y-S&Q+vzN5~ zHEqE&Q8E3FeTynQE1aw9Dn;sXDzZ2M^Vj)bsYUGq6v~VdK8l_SS1+iG9cwx6hffDkd86fO7CS z10GxsUSPmO%fZtOcw{-a&qY9`DW`Sne_~ylum)uN17Wt1O>xs5*oxH%q8aU4#$uK zsAqr=eH9-}_%;UK^;LW|!c))6si;Lxq{q8dA9>Z~p2(T!p%d)lo%PP>agEtggM-nm z5-p;s_CL`(#PtK1y2EV`#6WaS(C6(SUMIQq$paeo z@j8)KSB~42NIxgiPw~(txf8mMCrR=#I0}HYMN-F%E+)I6h4t{P+K|Yupy^c2HUjx5 z!MgyGz9iTK4?=!KNY^n$zeJgev>sC;WP%~26M-yBNHhug@(~`N;Xy*YNs%!x0Z9df z>`1=lBq#OYb}=a<1D(t%gU6ba&&MnEpMA;fcA075p;8wxP=D-u3#1Tr2bNUG7;B-- ze@Lx2dm&oa~5?FI95N>j`#F({=z8Ee5Bp)>1CRSqP#?WAnC72!iO0ot*EY2 z;xLm8Ast>Lo>kzH%jog3!Cg_Wz+`aWdZ?xK_Ntyw)_a5KI@2SPh?Rr?`cUf;!*Ag_ zCF2mIIag+YL{9 z0_2*AbeutY^}bU07OY8}yYFio`uLjmk%em+Btnsx*BB)60Kckn4bpf9v7LlHGF+|SFBg`%u! z)Uo0fc?pflqH964E5H!5{$6>F6Lhnaq?16(WAr#okNxz>#Di*VV@NNx-UPQH@sWl~ zKCcp}Suubl&m>XP3{m&)X&?6Ys`!|!+xwojy1(xPm-Se7Qzrz*${}|Et*bzET}lV>%5aZ zo`(w85Rsa#s@>CGj`JGgN6IAts#fVxd0R;-<4?1wlE10dit!*(z#f^eQoco$2Z<8+ zb$8)(9+rIhtpP}Ul>vs!mkWWH*T@`~=y8@FC-E>_v*ky((Wh_D@C3q?<#|prqG5DsNfbvH6#*^YQf_>G_EW!iE|*JG_LgZ>Nl)c=9CFouTWjp z2o#{-Uxv`q9)Y><(OOJsZ$Lj%sT9r)xU+<70-StCR)2^GB$}=J<-0~3%p)zu(JU70 zd^wqFvF@jeyA;zDle8p~9;EPhB&%$V{-|**)wt6J9GVy)?I9vBUCyG~-^T;tEZhio zEHPbB6xf;(+X90vP9D(}mHUy>60xDXwsD}Z_g?gz^e0(9IsvsLHBYNO$kW*!zDde0 zzDvcb bBfkC=a{rY8)u=F!@@KTj!JZ};phi-0n}o0=jScb8EJGYCL=tQ{k<_3# zvNjc`fduu~?o(J1PPS~J=#LF?tX{AN1f5BSDgu8 zO^X`r;pYX}%Zc^&EyOF65IEV!QS|f!p?wC8A=D0_N?GIu*llq{G!?o3Fe6#92{FzC zBN;zV9wK|9ANL?7D}L8aS#cK`&=)Z2p%ai5t+p~&T%?RzWW~={L++ECWW|aW%AdSR z?NDZnrSdN~AuW;YrX>z>sMwxhOB9W=qd%3WfTq}vEy-q~iXmr+L-jkKfFO#5xDcYE zZgSFn7SSvn>{xXUYzs4&A*U9y zn`AFI^4JoWMBtt)?{YM4*n;K$V&}HCGRss-up!aCH1M$ z!9w!Rkqu7KBS)DlUz%@eFNgZ|h%vYWU*%TP8yR?qSMit)5#EyYF^0@st@beQHn)_O z^W|=WmNkHCU^aC{9n!_09zF(h8Zlc|8q9I8GON~1C+0uN_($n+2oE*02guh9jIUs0tG!0x zRVCo63jC%QfhQ|47Fs18+6R|-1o{*)m9qFPd5~%^i&qO4xrl5Cq^x&c8x&ik;-wYv z_~uu&*|9zYcOxH@x+5?V6QOju3Zm23sJUvXj!_b6ga10^;J}fxje;cwN*srkC^Ctx z9ibG-qMcB+>)O3oKT8bNwEhb5?g-386~$FuKrMNhG|iV4L{qL8ag7I;Yy~PAhKH>m z&1v}sisAjNvksAX-fAc+(h7HI91Vt z9!;-mz2EV-+*}4!JH@sma2@PrZZ|8iO}rbmjp9>tY8;cRsR1btffRHFP#`cs`j9@U zEkoXSQ1S1!OGJU4u{qW8#WV*jcuC6oGBTIwb_C{N6_nQ$RZ`%Y;=>~C?mLxQ22%$k zmLXdCI|SQ$W3MYGqgKS$)cTvq=KU;(BZ*GwBF!nDJG6!)>QteQGN*JVz=t!mK_dc9 zkw)b?=odNfl9oEc*D_sEm97>IOK1hdEYr|>8)-$sN1kk=S1IR16MbD>1Jh7|?*WW0 zmsVP3V|YU^QtSm;PPxVd%7HLX*|PLuT-O8)#9aA+tlq<*kCgk2T!7qeaEvW+ZZ&fE z5`#aGI4*c1mtCe5HnfpgPr~$6B z&SeVvsx=@~GF701|H&n=8z=D4B5;5_p*0@m?R*Yaku3?TWsdTySg-`1ToXYhsg62MKEbri9bm-f;Ao6tnn>-BqzH>$K3?JoK@k8SI^Fon~>cgr0^98V5 zNX7>xGV(Ro(KRa-zy#Dm2T}4cC8%?qBL?g%f@u>+SMd2B5a|eBYE-10siZuSQx(Cw z(G>!w6TzRP>QQr)s+ z_9de{+}HGkJ-WuyIa-32Xej2}OFQ2TL= zZ{>Gk5p(LyV0k%J%K>=VLG8{MU!M>Hr_6LRCl|&$%JBzrE(hD#=W70AYw>F5wAaQ4 z@W@MA?AYp-vx{NaCg;d{w_~g?sT*R?g1g~(rjP0$$SQyzAaCp$hHv?vNq60wZ6?17vwe)hwfgnNuA5PR3M|Ql%dy zn3f{VDUs4_jd4JxeITZym^ghZYD2Tn$7dkfzjmUsYqnf@24d@?U@KUz?_;{44%?fB zRKH6?C^Vm)iuRu%HuvfYWRPJ5NGHY*VHG(858FR439r=}=U6e6(8-G4l0Xx$8v#ZW zeHhUtkpiXor9oC}nIRB=05 zrWKrtR`f{F8aFR2OoU5FK zoGs&}-gHbDs;Q14lhn{@FiHFAz2K^HygpDOY1wS3-Jj@L(~m3{pv-9qe}Kl>#uuW_ zcmJXmI+J!mq|FD!5%>m*6{tA)Bxve^nvA}dxB|ch{cJv3?gU@2BMU)PhluV{GbA3i zb}va}Rp*%0kvRP6Pj=j++FYx;*?Jtq!W(9YvHla5iCUZvLO^r0ODd0jpnRUnmy(yDj^Q4{bO?x}tq0^Gow@++l{@wP zQ_63qyfrt*`aph-Y~FfVOJO@}qvO1qo|#YHoUFL&K^1wQY}7fHIH(VzUACa@ zG(D7RgeiF|P1bp+2@PEbw~YzUk9RevByS=Yrf@W$?31!nbziFEm9gZy$pe6uqg2B_ z?WLT(Bf>)end1n%#!(0DbRwIknj9l^rc7d*lcRk+*|*{}>V_pbO26-NsD@HnC3KZR zN_JKrVzVqMsgSxelKNo{h-nA%^ReP_HhzlvA!Xfiq1Wopy;9Ibb}N7#6X7xQSNNasbjjw%!COMe5B z0dS}cP||3j^g2#l4Qi9Ut1HjJl%@0DF?#1Z(!8JKP^x5rszhraU&+OUO7x@6;YT6E zC%8%$8kK_zWo3|}X(7*$op%n@-69W$T3TkkqecMM$Sk6tNe&+xv!dwIG?ywnd`z2@;Nby3 z%$3IglMTQ?h|d+mq(b0t3}Oqxd_9whE|*$$jvlAzk)tIhc=~iN);l#KX^w7;V0rQn zszWPCaA+F~AieRvOfA^(hq8%n9}S#1k}@6-noPe1gp@c>Wo4t|O1^q=`j={Wq5P>B z`6VH>xM_~=G}ef5PBHF%!qr!&HM4+}r;p%qf*wckAllBwwC#vif14X=2=W2Q&qIxn z>BKbEV9Gl}ZPLwCV3_Qnf_V%jTOJ`#4mOC_>4;E5d_^6CkRXyKrs)b%Q)xGwIL^Q% z@=tmk#e=l#QmUeEMn!cA=Iatn9P^0yLuKJIVMH@ak@@Ey21R#8(VEVCD*;p72mJ|I z8!}*a+WD-YEG+ILKQeNza<1a_QxnhvGM0XCB3KRrN#1t(r^cpxQuns|_(KwHIOv5# ziqvLLtHfu&>&@{dsimDe?@MZ>ovNa;azjT{A!A8qP;DYjgs(tt56Gs$WtJli!P>td ztp7W0{oR|p&0n@LA_~CGKzTI!xGeV(S^EoJ(*z*9!A?R zn4mBj0)t7Z{zSewOf^+nOX{E_Uuv~K@a^OW@iD1C2NnP-$Djt;j6^jqEovPVl&BR( z&Q*S7~^U&w=Kxy;fVm-9iDhVRePTO1&> zvw>M^7-pOvAW@wG@&$l6Y)}Tt`Jh6bG~Jwy4AxLNi&(o7E9jl%P9@%3KWEsBwo8A) zsku|rt4^F*q25ZvK57LUuPBlNbChj51qNW}14e8wY4^T+rekp0Ow5Eg4gk2RxEtz$ zi&|RKs5?M<=<*9^R`BpGk}vbYKxDU+G;h7v?vQmbpRSj$lhGqeYI^noYU)8X`4-po zu_0oC0SS<^6+<;$7>y7Tc2u6pF4EYtjO5t%L(b1IkZzj>;J59;I!#06y-;*L^lFK; zWShJoUBAA9n4nb1hq03{T~v>vPok|iWm9nFWy%308=HcHrYLUPMK6s=_Wc7|huOZt z2VfalTan=CYQMY*DnkE+w4_SAbS-hVuO(S^0z5VbuRLAl*CcV5JO)eXo4rv4_(fGg z+KeW|f5oP`vK>{sLg8nx#O~jebeh~dX$!Nn9ic8x(ZR{;byUjEM4G_sjRqQ#)Iur@ z((`GQ@1f^cQvUTV+G`*BTC!C?7(#q76N2wD;j4m{ZuU4)D;Y7)+j#~EyLcCnP)_D; zR?wVcDCOiS9lEs`N;$b3(C#?BfH(y3Itijciy;qLjCvisshE>;a=s3YD~3`|#t}4S z=XA=CMBa9na)T&$4LP7zBH1;7to3jo0ZN5srX)^-qS1cTUu40gHwffM0wQV9>V}P< zFDnvg79n43Lhe$LSZc`XS0#T3eBM{6pRBLfp8zV`XcPfWD~1jysCLp)&37fqJ*n_U zC*MXv&e^0DT0GwyN7zTDv42x|+K0fB;$GbfdR2F`AoPF^C122?rwO_P(Bhjp`w8+` zr=#vx$5C=)mNqBRvzy!q(N7i9&}SA3@;HH~lz?#(wF@BHNdHPGQGzlhtssuiHfmQA zJuM3fJGV48n6Q%xTvLH}4xk2KhEjR!X!89FIde+s(^edSzz;8B5+MAqui~+S(+Qoa6#UKZM?I=gSXXWrpxvIoA+A=vDli#P<#f-?>-UpyjJXO2Y_4 zh-WE$(d|uj9OzF{rbCLHN{@+n?24q3j2hhbYPIJ2S#FS5K_OLFSt~Rmvi%1BQ_`gj z-jrOIhA?Q>gGN5zqYH|Eg)l)0nq>&;TL!Ofom=(Y0mPqf@HZ@jS2s}=e{+Ms@+){# z@_D08CK5DG7oCiRQ5tAu zUM3zl>2W1fyF1s%qWjI;27mY~{0XX4qKJ15u?c!C#lvtOC+S#P-KT^pV{(1nvLgY5 ziMKyJdeb8Y56kE#rdmnR(9Ui&PM00ezJq;hs^_`kNU|M2Lkax_NL&u$sz27yzS=RF zXtEs_A)$ZBE-Mk&*^+VA)Ub~r8+w#j6(j`}JSg)ryAel^(+A@6$R^t^0x`&Y- zC`Rui^!o<5sgZtDj7}i*00Zo5qzeh18qp>JxCts>IU*1RT4;yv)|MpKwm2(NI(}}U z5V_J^j|g(*rp>yB!K#jY8Gux-#`!~1rYUKT8V;nkl`7t6(9SZ_hiJBJ%9J3`R3HV` z8MpyPnnR|sm?k-H{hbcHcSoX6Mq^qDFokr&;(lT$=^h{GN+%-Fcj*aQm>mCwRWT0D z*2ecKmz)`2R}tWjjx0P!B#7|Tz?bVtLQ2FT>ZK%blivSO$18I;*bJSjSPW(MZ_*nZ z0i|^OpM<|>fWJ1ZlK~!Uq?dt~<1gWSt-oHaIn}>q zD-u*Bpo2k~L6le5X)z1xTB;P$wp=(Bq^}tyUPd~RNLQ`nc3TVaFK>%p|574#H%Rg} z>W%A2q`Lr=?}8rh29&CIrhrr$B$-C)15zDr=|W98WWd+!DP0K*l@E7-D7EE52%xF) zD}^IhI-nrA@>*TpmBt&~S;PR!-*m>04aNz@jL8P$_XcCLjU3nL;f#w6&bdf!9%!h1 z`WwmGZ(I6QPrp}bmhF;Jer%-f&vbl4q_&L)QFjA4(MabT={H8Y+ej}zqeup|JO#j3 z;~x^0Wvi#s{kGSQbdZrI8|kM=ZP}`jWjl^E4KoJK40i{qnW1JZB@tr+VAv_!Av4wU zPaX4|rVcfWL{tLS)^~fMHr`sVTULJuyN9JFpwx&%iotO&CE-tT4?4y1ClJwqs8o2_ zR4WmVZkJ`GsBD#9!O}t61#5LayTA6#>fdnx5KM&$&HI_>MbrT^EPW%s3Wm(%fev$Yi^Qfsxg{7R<6 z>nTvpds>*l2jjihC2RIzTTpsop{Z6UR7N(jJ~NLvVYWWmMN*_qYq9tZF5XE`Tx?@} zb*wf%?H!-~vG&+zSRI1UcZFe@s0qe%jE>cAq%~z{G|wf?*agkJWE3mZep=Fo9Z0{m zWEtmfo3z=>8dUFx+N=kym`0*Z8#Aqa1D#A8_olYDlOJu;_AU#n6bwAf|M@rC^JPBl zV7lk>yAHhS)^vwYc;!leeMU$}ub6bxSHm1y=B~208{Nq?>Dg%0r2F{);AqpMD(3(+ zg^Ubb2tb|Dru)@ByMw=#{*=lFU7*%Xt zneO>RJU3-6C)#H>NRsET%e9pongp&;utzNyQ;PO9{zV5we5wN;IWAkS-QHj^chupH z8>)25H3y~VU0i{nRvabX`vhv~rxSYC(Fw0B!l3I!2<#ng^G|gutMDL?>aHAGIC0&%A8T!Y3F5OqPJjEC&pDqKt~qY~y7I0z z7*Ks^>Nu_ay0shc_EEb1HVfmdb$ZhFIu-adOZvB&-58(NH2u+@M~r`-sNKnGU1^m% z{s^izTWhp$1{X8azuDK4vCG=U{onDo8)^#=Y~!nEY8|s<`1A(pOR{@2c1ycaU;w(#1tZHm0+&>^zL#^nz8i|6@vDd1 zynh@`3s&Pz8NQKfF(ke1mEnxLzpbslI*qN?KDp|rHM*9}{?)czdySpc?q93T4ruPz zFZ(!*#MFUfUAzl9fZ2hv_NL@UBhxG2Nar1D4lOK9i3xPF)yK4;&nKyl)0|7GMv;Jz zd^`kmvHB(`rEg%M+4_>U-J1t%FK!*|H*gL&o2nt!Mn* z*TixM){W2TB(6HJzU+W#?a1o$VG$zMkqzUoMTksC=Etsz^NuWv54e+2vjX$s>}p19 zCw7T(H=FQwW{3EX;o_<@3*;5L3a84f2KSJnUS(FuYu^&}UD!h&cr(Ml3S*47xFG_n zvP8b~y7;^*3*lc}7e}hH2E5fZ@vJIq%zIrI!6uf&Bb$kTOe}%dza}QTGIy^E*UH#} z*56deCmEl(vQHSx&uCeVMKV79vUuN}O<*U)b$1rbm*i$t^E9zKQE$u zn7j9k^QDFL)rAc>pE2HtHLb+@qnU(X`eZ`ItHi(;R37-aRGjHyb@p}W-m~jX1jDH%kij1dtXV^Dp0j%cB z{Rqvf8w+=*l&+zjlQT`pDf+~BHY2JD3t@cezKjV?VRN36B^HLVA6brQ8OBEN7S+Tj zVeB>DK0urcV@>@kBZ$cE>`{xjt({$LJK;)Ls2qQA6Lp)hQDJBHC?}!#I>f!OFv_|g zOZqHCaEBElU%D4VN26WVJz`rk_J;Gz-MVjWt|qE9XF=>=(Y!f}<^dk!!{%_Pba#>0 zoNcbXUHN_GbXlX`Fv2mGrdmDMvP-h4yOqdBfO1#7f_o&I!J{e3e+DUI!5Y2s1whNC_5P7 z)<_|D6UL6MGNuptm&j<#2EYeg+p)LodF5X--hB<-%A0S?IMM+g$D3~w&!gBLmM?a_ z&T80myTXi%ud~{Woy{oh$Q&H_`#%atGds<$XIwP1G#0k|>C3|C5ie9&1Is(n>*XI5 z>|_xP8=KS7bQj1YP^LWJu)U;nKr=*F6J=dUw@@M@UzHD8HJJy{y} z`AV$r$#VHSe~VeYScFU6hcA^KT=9vR*x!rwsC?+b%fhhpc`Iey15vX#26Dgq!rGgC z$#*{#P2Xfq_{R^$@HbgQUgy5}=uI|+?GrXCw)tCl^pel2S@(WE-=#4f&b*!7ZCFtS*$!G@HvvbT|om0fi? z30w2j=P(0etPX*)nL-Cs3xbO&YpNXXz<45~*P4)$w;10bWt{2DeyG5|Uy+eKm>oqY ztPuT%v7>BHM&NL^!&K?{DWzH6`NB4lt*A6qFUBW}g_GF$N_6xHy`7T9^GU3s(Dz$oBBUVhy7#YWEBK}AaNt0PD|MrBqIT_P*jT2(Z6xO_Y*Q4OT*v%hB zk<+E^i5S%MnOutFU6DD3)#R_`ipx{j3|{xR7(A6V_Hj7=QjG~ZeMp?c?fOu}Et?z{ z>!xCU9+@MyPh~@Rg-gOGo~;{t@|aHjz~tt*ZjycR-hs*=l`5Np4?ZxtI-Z@$?H-th z?IHBLIywmo+1VNRZKd#?Ckf947S3LZo(U|uvJ+|w%goD>8;^?I1lE`P9uqC6F^h9B zS_s5Zc}(n@#)8>Bacdg8!#mB+IQ2f80n6;q=rx16+w&O*Ge*r~5zM!3HaQaZ{xQ~A zAN}krWu~jFo1O97Y&MRIC5`RsD!n2Dtqo#X;F zO+9?CG%!vF29%*33}A}myTgTb0jq0C8-lp&h_&%Tq$Q(;KFoqQwk^Zxr~`A$R8f$x zC_~Et&+EXDGQa~m@KPDzkj*;u$1>10I`EG&K=m15s-s;Q;1r#*N*Ul#9e7hLSO}lp zl__pe`uzma(aJiqi(-M5^+4eAmlf9hX}btr#Afp7A!5TK*1Xb>+tfI0KU&xpu^Q}H zMuiks1xxR{8J-_uI^=<4Md!r`P-c$FaQ&EBS&fcAlUM2YyJah*K<}+&$j=$Sq%p+* zPek4l_MSuPNNy*34`=>j#!_~l2ad>?ps|&VhYS@Jma|&C=i8$Ga)cfG#n|P{pEvC& z7A$A|_>iIE(sBd{H^r0X>f~9ea~TrmJU`a9+!rR;|^=-mZIUqfG!+ z@$MqJu4Pf|p|GxH7x}cW#MsYRh+ENGrstYS?zhdKBCDN%$tk#qU#rI zD7W?yUw?rpuzin=2Vby18F%WPaqufvhx1RCi#y-2+Pq^AVfvQ6;48a}s^75<-pgZ3 zX=iKkK*?|3J>$f8%%uW5CvrEkZ~1^#8P-j#w>`U=aXuX*f@KS@UsxZvx*yYWfNz_T z!fhu;Wqj}pyWX2fyF9IGA-aFW7-m--#*um;@*A^L`JJ|xpH_aDocd_Np ztHP9Z{gcB)#BTPN^Dm)#`K!fo_jRO_$G^WWlr}js?M_+kM~i#j*74YnA0AJ zK?hg|kB%R{EF78Q5QOoEQW?P+xl*^UKXbH>tnV)z%2h!BTPc`?%;(Fauy8Irir+GkbTNcGlk_48^@b36nTf(1~|y7!)yij|3HNO z&La8XATjlK7E)=Sk3G3ol}z!~@2nkP^MSbfJ9OL-C~E(K<$c0*G35_7vQmZVstH{J zMeZLgtlEQ+Qht#4dy3;}QRfI-&5un}mBJ(qjm)tM%H7c+`PFFzGSIC4#~j92K`wYa(Xff%th1S}LQE;vzOe z!5ADhpxHQG2KxIsB0h&%c*s=oD2GKly){+odrm|gXTyEwK>L!eF(vOu1u2eQJjKt) z*#zEbs;F~K6nh!H2S75Hbe;$2iztUke7@FR|TcWrjmyUWoceE&pu?TpjAYe`(G z?sBR9-`(|NoZel>CP8-T?%G;M#GXWVeLP9GT)G%{3YLqXDE$7y`g&IM{R^9bpowDJ zUo3`qohT}vW}SK3d!ol_7|2Q(^YA$=sdAF+K+*Q_`ozJLx4IRe&4i{~2 zu*C={58YtRc<nCKzVPe5ewxmYsyjAUIG>MLOi|SeHB0jjy z*6}F=MU6bHTHlVQIIN@Cmd7UXkpo17J8V8Fe+TP@*C^q1m#quA-e0ewk15!Ov&QQf z>#h$>LmGQvNAke5_eKR%@FUY(ExY#@$L}(~y1j?IEPP-Z=y;J+6aK|iIV8{pPgnrY zm_P@aFRI>St@%tYy53_=s(gd>1Ptd)Z~3q3C+6N`qp-n|_t-$z?tCAG`u9PUX}})S zTaLd|xs%ClxKk%n@P$s`*rq7lx`ryS55VUL{COWStAP3P1$~4lz#@G<<7NRi+#cp$ zB=Eyb(}2Ay8Rpokmw5VsMKr9~3zkZm=|j;!?nF!tHix{FfA=I{JAzdM%ylq;wu>V6 zA@i=jzbD5O@N}dZ6Dqw#rE%_KJ%}$I(#6t;EUoc4FqfQ^(F|z{c60>Sf3Q3jJs+{) z%D?nb?Q?bfv4@!Zh$Zvn9-`7?_8x!dA2IzgdjFg5LOe#)UH2c6L1l-3#9xnD+uDt~ zt8I!}mjZF-6b#255~Y@P7{)c|xd?m0KI1OWGEO{UhwRyXvF2YEWuX($n3UG)08~xi zBV$qmRcm6bBLk<$q>ORu9)8o5gfAEnUk(q5N$Khnjd?9R%ak+^u$T~Bsp>?ooh$9l zuocUSPU+bO8_Z~&A;eIOY!2UJN_q?5l3_C%W4)U<$T(zpV476H5=KaUB&A3v}=REtV!Wgh={ zWDGkH9ey!kt-63p3lj9havg(58+zdL6Mt$?j3v;CG@x(gp)9cKca4Fg zFAkfMaYM+?JZB&IM|4sD34p?+%3zq=%b4^#e4j+UTi&F#4P|~)r(DMM09_NKcd27*D}{(#DRieR zDP(!2j~z*^QVLP|Kx9C_|JBc)P|9{5HDhSmhSNdpwN5xru} zRlf8?kP;K~HZ~6+WB0v=wcu@-P-9CRF8uM}-SV6JNOvQsuDO9KJW+A==uxjXC+TDQTrMW~Y?yp<$Vkj}Nf;y6WX(Hf_PdQ4n9RC{iey z6U1;=-ZglBYh_}6JYnctFVWF=K`R&>KN3~h_!uv)wK(buJr0YyZhX7tSQ~wXf6@v) zP1`-xV+u(ML`1Jfbw1!!jx1Nk!c_(zTdo{euAEq|oTiuiI2Ki~t{lOpa^=i&<(zWm z%f;o4jn#M$X4!~_(^x;kV}O@#Sy8!{A*iSvTdo{euAEq|oTiuiI2H+5 zSB_v)xpHQ?a!$GO<>In9=D~k*IeUZIy?($Uz)tROA*`N!WZk>r=;PF+z*sbKI~)sh zP0MMZGA@Pq$a_tNs~7*O+C%g<-3drGovzwNSjcaJ9enbW5RFgpMl3gkecEB3@lkc zCcIF`_A0_YZ>saIBLF_x(LW(C!d_}BVtsgb_P+SZhxg;rVc$6V;#FvCC32a%g-jj% zO=F6JF{>m677(j-S)d(EW-Lpq8jF_Id0o~?46Kfs;6S5{>DBoR7PYz|s>VcgKTJ=n zHq+BkBOQd)w%sUnr$RfUaGnahc}6pTK7{eLAtJ3NAIf(Ji~O40o3{!PPPKTQicilo ze3n;Vgw^7)yjy*t)#6LIQ?RHP!29qK^~B5o9?qZE6Tb%VX1s5GaXWxFuUHG0cjPBQ zBCs|e%|EXvKB~>@@)tp3b8SeDuO|-I=Kf9Fodr=&AbI!11Ni^&fvz<$7T?*E-PMOC z35q1m*ExgZ07mPd)K-9o~>{@DW$)a4+6DNIa{TP|ooyT^DZa1X9?_|`oN)eE6+Sy)Ufsk#2$lm*HPK}Y$nXLNSaNa(rtkG#=T`>1$ zTx<{K-JHkN*Q?GA5~dIyz>frpun_KPzrve|NoTots0&OHmT4P+YGp6v5!uR{!x$%0lQt}bulR`V>kONkG3*nOo6*`eYkv9T_%6MF3^x7%)4&(3Za z3%0XUNNiXF3OyGUBkYP0g`NpV1zzU4d&d&8=^Sv`mms?x6%7M<9p1C9=o!d;+V?X3 zI;lpPVB3r-KIyJ08Sm`iw*QtgSMM{>ZmQGn$W&6U+ePt3An#rSfm}*Am+hpnwWG(b z%O62Qor0eckwM&--#RS@BdztvX-s;lQ+*8UzlUN9N_t!0x*!e(@eoU+BmbkVf8xVj z-3X=Hx(h(1fR9pOm*3pt+S@_=ZgL|1u3C!Y2TkD)*neDwh7<+57J8b{B12ghyjF}j z^^pbcnY;~LVUHDpzTylm^gIUafsQqpIdw>203s>6pA}Ds+aqyq0ijL-V3C#3(G(Fm z(F|~*4uvVXR8s(jE1|~NKM8nIsNzJ_z#{ljA*qjtMGv3?Xwou5Ec#M#Q^3o@oisQo z4lMa)6jO2ynq21X>t&S`fa;fhbRCuEO1HYiS6XBFcHL5ndX!ewQc6=7L(}ZYS`$Xa zkD{u}D{RFrd&u|8VZlPrW#@2?*9jw`_|lUyVu^y)OBA%|HkEtol+v&-9mo8K zDPsVKDJ%b%W|ylW>Px3VeV`UZVNl=1n5Fckk$PeJ!28dVh{Z0vb$q)<*4Eay91<-x z>=IZO<^Y^a;4X};hHGZjP$wAM$JPRGF6pIa_*)JM$Fhnxuwrot53W$d&Xn|y6W%KG zI+kHHp50TrIfOk9D=hT%-bY6y`R7f+f0}}SHw9-Edb*GY+C|ZT3a9Mk%9K5N5?O`; zSm>E`iWZV5@(T>)?kV+gNiPOpaD26i*2L(Rza#J= zObd3}y~45+&{0YbW?)Dx(c<%5ZE;Fow+GXH3ceg29#nPaNGum>cpMY=RiokhemzrXjCgYf>8d zEq<vtbF@01ncEMq-;?6UQhNjo+Y}@4?O zaX{ZwuWn5k{GP3FpREpTxX3 zD*o*W6R1mjjmBxf05X)Bg02Id0tew^K}`AoB{$Pgm#gdZpdw`As@MeaPh%b&Qs;j# zjRO;`?PD|oJ4J9~nwY;s`1;-YqerC2l=qywTaoyhtZT zhfvNb-pMCS$?-s#Q_XCl$^+5yfQyx(+=U;jE{-?j{{CSn zk`H0J*yk?!gXkTK)#jUEsY^pG&vr}p>Tnp*vH18?yVI3C%VZ46N-+@Yu zSflP*%c|- zZHGU+Y54w+MsD=e8+qE7>{&Np3w~JMwgl6$igv;t%i5@otW#9go6UG|hjF0#FOyfJ zs{X&uyz8r1>8MxPtY9IkY=ZqhCiw!@L$<-HL_r|dGM@r+8QWUS<;Fp`sM(x1H+A!+ zcEKrV7I@7^#5U)VmI;+g206A2rlfv!@u+B!(*Qr`i3q%dy6u7rwqlGI7MS)hTPxH# zI<2Hk-xJX`XBN!gJWC-e)n#(%vq;T=wR3mL+D z5}pJQaAaXGEYR+@A8>VsMmz8~z!sg|*q;b?N}I6$5s4|wWta1lFsY`mz|n%A#51VU|{{YCfIyuE8WhFQwg0Y-HC z=^tWMYaUX4%Fd!7v|J?ayHi|jjkx;J4i$V`F5`yPE5Y~g03ra6E!yUw3KT~g#UK6(!^A~qoplA(7%oboQO`8B5Z8cX$v>EA2 zRGZwz+BV#ek8}{3;oK|ixhoik>JF5w|w!AvuR82H#%WL8i-R^K~HpiO8=(fC*sZMFyU);pDwmhKf7kD35 zRMj_b;uiR7w&r?x>I$8)N?=IRz}NNup{l6cj)$6NxRt7ShpW=t$-z|#tD|!U65?qruaZSp&~+KGu~dyRfI#J-P=c`MsVM@qYTA{ zQ6^<3ejdIs-d7K+^M2FkKPUYM%b!;U=e&Kc1KTQ;uBltMeT(TG@WVNl@Usd%G z-&s{mevSLtFL`Mv_JniKh9SfY>CvePu5uQ>B2K~^gg?kARAs70Lmi_nPNwY@M_=QO zO~=tC#bW#FVofnsbuYr6Ct<>b8LI#7&%V&>;FF!j&<=3GhNW2-R}q^!@W%GTi>OXl z5>Gnt7FE|P8yR}<6Bf9z(+<4tBrnwmLFRKYF_PCcwJS|Itdj17D|FJzx(|+fj`g7e zJXkHDx)=VXJOf_1Pp-xSL;V|L9Rd5o3v1KLf-e0W2xVL<`~n7BMPTH=hEtuVnFZeW z)JXk|8NHd2_B2vH##8i(;#~qXT&tp0zvv7_!;DxmPtM+}=E=}E)Bndj`3zsRy|RnH zzC}N=I7Z0wCl=eci1x2@&GjOExPsdux~Jq<5V^1OhQV7Zkp9_df_z^Qcm$eLJ{W7Z z4k9;8Se>_>PF}XUi)J0U+1*BSRBDY#x^jpOrEFH=rAY0__ww7-MDI?#yM0Z^mtuV< z9&caY;id5D%$wRLBlAXQ9>zm7 z)VA#qce`?LuXVqv3Pw#erwQVb&)&pTDu&p{Z0EQW)>N2M^-b@^wN^zVh%W+6w zyG{)R(=gb9MqA@q#AGVPbZG2=L{#IW>IpkxJpdnqcH@nC=wz|0JNJgQS9HS`^|Dz123D2`XYvhFfS#w%FE|3&_Jy1x1{?r z(XBhL=kR;7aM}pd$0sI8%!Ko_h|d(@lHw9SS|$W0>s@ zAX|Ed|0rH`=fgT)d1*+bbu_6^eXUMWLM%lIaYmFd3vkRrl~fo^mI4;P&#ca%T=T`+ z9^B6}>E9QHwr*;|LFb`k(I08zNDtn`6jizoxVm#sbk5b6qESy?lfQFM#PsCVeOeBr zlSg7qoe&sT6l>KQC@ejBOTKb~-i2}hh@(Au1J6z`UNMB*OHrj4uQy=^)-c;x1lF?W zKOn+n^;t!93)1aIdd^568EGwJa%*d(!;Ex>k$!5VTaEOLkv?z|pY`HHog-m#GT+!2 zqC#&T;M@NN!U)=Rn%rK;tK`0cuF6t9@91mcE&Ou8|5^*HKO>d*+xj?V%%^!)pzI+Tn{804m$EWcA55>WLd@8^4 zKt%NCFuPENU zFD?w=t@)|~;W-fROmZHGiTEJK)V+XIyB&r|5O3t}iF*Uli!}>`>mU%1$`{dtaL%9e zx0pDHk7@MHJ-v;kenGj3eElv-$d-eUs^9LX;fqT+Y%skU$QOQt@k*vbzGyR;H?W)| z3HTY%HgE~NiNzmecu|;#i91{7+%ee6#nNA?$drt)qVPq%Qa9K58v%K85%JH(U%nu* zS$U51aF!nT;kEA)y))&8BNvS@#VJ}`8O$Si)%zk~2%7cmuIN03zvuP=wSYnk-3rz3 zh^!&}9qxTs)E~<0net0(cJ+=JGL)Nnn>_LLP~Ob@%RJ)6(QQcxG(vW`E%JwAYU+Ah zR2&9-dfpKs!+8Cs?IDR=$SH3Hnw;l^zaK#Wgy1uAs8L49pK{|pVHw72R9jCqlrq_p zJh5RI59ICcia&?(Ev5`Z`v7uQ=X|kjIBI-)Py9L@<8bRm@%B61%ca-t7lpesVRYH* zws<<6H@0{Kf`~PkdMgFpL4i^h_K_*gQ~Efl=>(^RE>?W~aM6aj$0HESy$iI)l@*zS z&;Aim;p}F-x(0;bG{l8X_4^I9HV05W8Q_9^(BOk&{E`dFx5rFiPU+~hoiXucEMC?O zwTa`g+`Z|5zbJg3832I+^mTzReI8ZPSG)`{wVtvB2BU((;e_8-4_BEm-S>K>}!((&Ue#R}Fl>X~?3Y#Pb^ zELYEfJdHxftvARk@e`Pj^fc5+yBp~!BmKxozckXrMtapqo$zZcBqzv7UpLZ0NQ+*R zMfJzGlk|Cnh#tj2dOey+QUXp{jL zToW0ngZI29@<;PpjhoyorLG&C1B00+fq>HQgTevF^_7WA{Wjr$m8fr`B^Wx;9ZEW3 zI&u{=8vOu{bSGtCx^55kbi_lEGKPopIakHbF+8B_a8MRC(%5OasFoHtCqhvSMsv=` zt57&*C#s^Y7yft@HD<8Q@2UtK%i~R7UN6;((>KL8V|fd|<0nh>Ed8P?`igiombdD} zF1?^H!sN4y#Ejo0NtumXaR1=eykM)@wO7De-|t?;68D z5$}!T4Wg^0V86lsEUn z(d9 zNqjH?Z;pCh!n}YhE;;B5n$(KQm&=t4%9ZUfkC&!*EmsaGR}L*#rYjNPZ(dv>U5h9! zFt%JduDG1h<2_!PS^A*oiDBjiW;gHO5ik?ZB;)DFLy`hB4Q(d@SGsjY<;&&pMXf8s z7qzaaT-3UvGFtbR(zXat)V!jy(LBeZa#8Dw%0;a!Di^hG(go3U5})mw5626m2>yUW zu$^3bLF|~s`vz>G#U01fkt#^XHI=UWXsjc+!GbT3o))2TJk2zV))&OWv6uy9)6-(| z6z(tX#PQAngK zt0p4=n0H3}Hktp-FP_an6(O8gJ0sS_^N#$;X>lcolE6y}lMG?iisS&r29p;3sw zB^oC1@x03su`+?TsGWK8MIpXm_*Z7MMtIOC64x~FYv5BOjT6iDDdKJdPNFWP2#Snv*Q)*b0#Qm5 z?PR?1;-ZuFo^kT}b}EMM%*aB|HppPl?EtfEr#F>_p1Z92i@=(xV(k08K7X`OEJ5m= zg!YjK{&89l-*E*WyPenZHy#nWt2kb*`|0~t)y*27AiI`1%BqZ+5Jy?`%kWupFyUdT z44Ma#ZoDo!PRC^XELkK>=Pj#WJ&x+q`rx8OBo?YTTwpGs3WKYh{Y&hh&TCr^P%*?M z&~zXZDW-pVM1pRoLd4)|006xTJyYkyIH^-R8Rp%EqH6k3%u^` z>nQDtg(VDlHcU(JMQbgfI}wM%4n=Ks1$Mh%1-wF4dvn50nMSXc0ms3`d|{cvYX@}# zh)@^Q1Xt$o^i+T$;s$5QLpfsq3_i@m7u`f=#BZkH2#AdQ%_-6P158-jDRTLRmm#_6 z6gCYYG@d7f2rr=DF3a>CniEP6~EJqX-4sTRSZd7 zXOw@?(VL9ok9slFC~i0<*i7EIepS`!sv#{;z#illICh+l_xrb=ssnZWDUNNsMZcN6 z6Ym%yR?p+vgx#V@iW)d-C{Dvr+N%}nj#3CcR8=CPB)Gz-Hr`=qEpi`Vw+ zv`1et%QIg%BHo$ByPC9|Qp^<$=9hnp95C1X4f+?=Y)Tq~T@87xUVGCc!Ze$QHGO`p zxNo2$sqg{)G7}nxn6fV%PQMbJd{p&HRS^)(JzDreuq~6;s=`0Oj-Bc}wPx+NeQKn; zjZ_%vv0RZcn}5Qq9~ON+KkGSff=$yzSoVP=LYV(vF@lhfV56#}8TMTc@@JfC= zT1XZPQ2jOd5LEky8oeGlYP9b~@hFkM*)5?VxKd|MmlKdVFmEfApzAesN=0OsFM#n7 z9vJSJ6`xReM${kbY;C0Cx8@GE{oyN?&cQX|kM@hJbKpYrYl`5xu$J5}=FjEzc;-Iw z(_G%L-=Czw)=E?lUfr7Qv0#|zJ*{Y(fB*c8!lG-FTRxzEyd@v+BVy=H|L|AkKpYb; zO>^6Tw&;&v+>$x_M3Z?qH*8E2F!a%&mSYVGU+fcu=Rutp+r;{L+^cpnemRj2Rf`=O zV*pFeM1bj->SnMwGmp>mXoIq?!#UknYmwUeofBh|aG0=Ruh^5shw;>GQ9Bux<^x%z z_JrxAHh!;cub8udySqO%&E0aA1pgCby+?>ed&T6Bz%-kf+;vS`8%#s@iuK7b;HMel zeljkRHQOhm7IH5iKiD8DrAu&h>NG^fvz@G+aiobm*4mYU?xOpAKA7*@Bfg){>oi}N ziMlXNx#DHEycR_6K;u27MIZs*jLEiEPZZ9_MaV(>^|nldvPEqPpQg8E zb|$sOt3v~|7-J5k?-|gPeyGg6)r)%3=*I!*M-S8d2O!+;2_Sl*i6JC7liF5Y++V<( z^Rd6^4ScajHL&Il{KhLh73WMX@RvXI21fail24>HP$VqG>5Lt`XCVw%>okPZyE_PX zOVBl}ySv!hV}LHP_KRHtzj)#9wdgM}(Fy>jfdq6Wh#xFmr&af zvs9{^@r#we$0En=);*yz!UGx>o<{mFRtH-}tiA-?MN!5-z}Ozqqb5D_=u!KWcqfGq z;9GtZJ5q2DH7!&0T#Uv%2E%q*v7pzn$$Debe=BaxHt?1-CiOSvI-a!F7r9PbgDF<5 zOXfAiwvW)3b({3IwA`t?kN?hB-Dd_|Pr1(Q19yiFWmjFIu)PpWA+WR8R{)V#g@Rln)tru)ree-ZaFZ_n*^il07)sq13( z+g?jI_4pqRzu8)B>hVCAnEFMA?l+6Ex)uA)M4jnmMrpsGvwwXdu4vrTwDDJ}a!6og=OldM zfbYFD9?aV~ivt?MyDq=o{i-$#um$K;bbTn zq_@ZLztxT6q;v3BTm{mf~#G&t{P8>Sy&5`J^u@OVBHmQSH zMkn+V?IFn2q|5^$X}s{Jy4yTEV>W9Wh2d8qmxE4xGs_dn?-b3_9#8#d`>PPAj}<|3qcIqcIVm}IQn&kioZ*{DP7rN*IdOFG8H&_C&o`mr8gVvTqz3r05Twk#gW zx4VTNsPu~GzRG^qKO2ju!`AaWH@W)1BD_WQm|`65T;{c8V3CG~1yk|qqGH=_bVi&~ zT1o}2#C>buf2TBTAG?~Z`IpcC90Rby=+6;a$XPt}x4Zs3bd`NvPU|oV{eng^tsp7Q zPL9xc3G}L@PbKxoaE#pllDL(`y(E2G(vKuvE9nVIe+JERF3ZuCW>}VEeVG>5s0o#= z%I_OgJ^tgYZa=uguFA8YN5H1qu47-^*L=!d+(|9X9pmNG?4?>m%W9ODGo~sztj$ss zv`Y>fxD;iw<})5fYqgUtS*j(}eLy)@$#&p|eS^$7mt#t%^2c5Bua3t$M>rYoXa((ro`7q`7Yj>~gI5xul0B zy>f#6`ia)jvVME9Ve2Q`S>!ToG~fn_e#^9Wp3UG1weVpru3FsAHZ9ZQwCmg1!)4lA zzC*VaIl5$e{RXqb9G5S(A2SNKu!)~)!?Y(`*`-glrBzmJDRLMIn@M4K^A>AsQ9Ibo z{v81AMjSCyui8av0g-xtrjkeP7xBi+bv!mlHn#0d}s!gc?>2P1- zF1Bi|)}Y3fA+)xLMmpB^43&q@S2(=C8B8l4wi`I=JcQj{i-zzR%7@rOsV%o{6FUJj zwy{Q^VfpdGQnBCg_UCNqXIiqQT5;y>TRC(6_2})@{Cu-`4j|dFKwDz(p|OZ8YR@_v zFO{MD9O6+9*uIvF2iQI{wa=%87*he>mT?%@z)20vT0S_$=4>wOu}({>e*y2w+5_hB zcZ{iB@CtEw|2c@V%U1@yLdfyH!~45|?8G{3_U{|S{#d8g@;X2kKq_%o(PdkB6?I>- zh35fhm8dBx$QI4Q(9hW0pKBex0{#`wUxxD*;Y1S@&^a5D_eaBP1bs%5e^b!pkf3!B zG0S>vtk(I0C9j7>O$V~n^;)=BT@-priQ+bkMk8i3yS@Rf%n~;4N6oM9w`+(I2dJH0 z1`Ku9(Z2oL4?1GJgeRRzX?~D}Zos_RXD1u70jc;85UKc*9!umqa+63UY!f@Z9#f&n z#XOnrU#_FK>%?y9+sNJbQ8?q6r zg?IZf`$lb#W?#!XZqjPhyu6YU!2&zp@xQyk*tDuxH-P)Aytgf6vo~ptt#k2e)_=?- zUBC~ny$h*VxRYROh&j!~Eyfk9)2KR4|2x#TJkb)nqGh4Lmf?AHzO0CRQC=A|+yh+! z%9?G~u4{KcWmUFln=O_V#abn81v|Ax3-Q0Uoc5Js_<8yxStGOT?XcNX&QUuyj+NVr z_8>Bag>TiOEQ7)4{1SbS^ z*o*Dj=f3aJm_&{`o!=q8l+E_;&<4BIVZJ+|q3orsfK#T zfg69}V5a>(bkDaXFF3kPlBP?JlibwYN~Hq<>5u_*>}1v35XD#{PvYVz<_@?8JC1|DQ7k zEn#W9wS<7w*<|y4Jy4GHV|YgIeGG&Axt-=!{yd(Lj>CIc7pCvgT5AiI>}|J4^K+^E z%|apZr+i>=q+rE%_U2ygO>Im9JHJen1!7e&++9NrD_WZ4rNQvU7?!?ci9&w&}9 zjc4O>(7R-1vH3Y#U6)I2Pmb2wa=ty~4PQ;Q-N%8ramZWC8UGSnS7w3iz$2_)RA8cmsR>U9|H(r-9 zoSVlE?9)02)Sao6^U4l{dbZ7HHTI*!S~#CY@7K~B_MeYJOXow7BL$m43wGkiIoRyg z)Aj=$Si{M(KU=Ur_G^u`uUoLduRv$CU`b!$96)Ld_CDdl=4><3dCl1sq5;iWxdV8W zH7%00J)kwKKsQPy(5p~Jcx(3I0c~vUMKw~t!yN2u)+1}DBg;r zLzu@wtykGi^T3*E?pKqIJ&2=sL+u3dsuf=bcw)Q*Zwx-vyUutY2p+~)AF(eFYBhcH zNw1mdA@t2yxib`I^q9-;9mL5W&rH_%kmhT3U?if{dA2Pb-Fjd&n~Q)Y3M3)1)kDk7 z4|(rSYgvC0TLrKY-Swf+dJNS*zv9!+`X7-59dW9>qdQqbf=x$myn^Y)Mo%Eu5HoO5 zFy*eg6_5L&2L1vRK zdI$b~;bK<~GtJe4M6~oqIlXyy8C>*d^x#Q|uUoT`xmu$^t-wkj6pF#y!xe|~XeeE= zk7)6!fQ@Uh$7ZRQ7=rIl&xQ`{kzZD)O5i;Yghrl=L-G(i6>a#rkwB}3R7-FUJy(nI zyvhxrVhRPadnZ+A?enz2M$HM@EXI27>+QyUD~5tlL>PS`y##v)1L@d0?Y$7 zQqxhdW)(W$`LRz4o^{&QNV2il4r|p0JtXh4=qHTz&>}UZ7cdSf2*r@6paPA1;4*_o zCZM$??#k&m1z$;gR?@zbR+n`B=j`NRZMrp`a)i7kC)f_t@Cceh<4Qx>{n%C|pCOh? zn!?zz!;76It{3@oDj5_t&FV$MA-uh9kWqw-RmlfS!Zf3Al`kw`v^N#t4NpH@(kb7gWmex_$0Cku0gw`yD(B}tPTnhbTe77kD*N&Xk}j< z)2g}TvG0y)?X^QQm}fpR@Hmb2&Bt0;pP6h9sF&XiKJ)yCY(>n|_E_18dU`|lC?7-M zO=&FhxOPnYc_u45j=9r?>8$e!t>IgJKExbNn@JLtm|mtDE38;@G2=-3-TE{~?FL3U zn0jD@!>?q3NoS0EJYTexLDwa83^lHT%NgY5bawNE=2Q710xi)@_1FyNeo~v( z;CM51lg;E_@s}+%)2yNjmN~y8H#VxAtzcO83N!IEK9lV@sns9#7VfT?9c!}(an-QqdE}&OUj4R+H)$Cc$2AtBy zCU|iz%S7*_mr8X1MA)ia>$?Kak+$R*l`vsE2a^?FPiLOrX-%6h`=AJ4=)P?$h^4Qq zJB;7agtWJ}kQu#&4wY*h<0zr=F(0wi?{MTQBW@Tx

$A(~{q3^G|CvD^Gi0WOe#7L9>^!!>6?t?YrXI6tsY@*hA!H zJFCdd=Qp3uMe%!Wesyhv<1E;ILT4}7UI@0-xvcdW97edO$7gu9Rqp01yf$}6d zd(0FK6#BJS?^-3zWM3m#ojX&By~F_m=esb~-XlrBDH!z$t9Djv9RH#zmI30hjt-l` z*kWrf=ub|@TFxP1XBM-x@3CM2ra55Z0Tidl9F}z!@}@Oq$Ifc?o82R|l#tgSp8-nP z7o0j`YU-qkBgo`RNM3#htM$DWUT5x<|6B3{kt(1X}va-}Cv%yV}AoOaT>j?0cEeoRg)P#GP;Ue>WPjS^j6 zwx3lvjE+iz`lz*P7Z^;PGmI`pQ6nAFi;0pf7?0-0*iVw9g?j@*3%-^Ur_DT` zBLx#A%_2{qkJ-x0T0^a719s{%dcz*|*`v!^c;mejDO%XjK5G6*@fW6u8Xe4+m#A8+ zCFAn%v7SF_%_4hK22id4a}~!1PcqnpkS0sD`?c-$*jGPdT8681uKuXuvdpP2rX0LH zi1Ao_Fw}$AcFu7S+<=4QKV9K#sTA*t$7JzHw;%SJ$~s@cVXEM2dq-Zuja`_2vrn#~ zNALMIyLuJp{*$Y+*4Hq78au6QI-eUgOF($a@!``^A)%Q#GrQ;0KLPADbF2;t0 z@S7+b8k>FCy6adT88(J}dtJjjGd|4k29Cks^JZ_{(87GW_)w3GmuwIwfrm*p)_b#! zH!v?N>%%VGKrw{B$%<}ZQOQ=B?Y@b9$Tp+*{(VymcB%ad4%p?pK|!Yc_=u~P&h4NN zS14g2-0|pz$LtBLMq~QGV zti`W5k}`HYoB1nhN5k>#@UPmN`a@TZHNJ&c_VVyB=oaeLq3$g27IgZ*?(EtvOw(s~ zXMVqFOZ0KN#&-UuMd?lO@ZdKbG8@^A`Q1hl_wL4860OsX^(E@ljZM9+Mf=}Lf;#N3 zAqDPe*W|n0I6zWxCW)Q7txeOt6dty8c&yLeDNFdK?(sQvd`^(T;HvQ#IuEhwb;tn3693C4K z8w;mfaLSLLMp3{e1uiw;WCIGc7QUSaljZX(J#qP!ywWpbFxy;!*HZcqWmSIH!YbPa z5fhGA(3K+?={+0BzPpPXL&gkZslTJ_zZ=5V{0@bs4`e5Q*A8g62eY|%wPuwo4J;XM z#sCrSuL11dU4-j3m{q-pec18?SjT(XS!+GK*K7X(7lXL_pp$>4f!D|#`VfcWJ|Itf z6-evO55{rlgnAbp-b1~4Fr^!R9&B1Jy$|Z}j^)96;!N+m!6F>F^KkVL4yWQ8c+Wrx zoTqD_(-4}%jU@ay9W9j6nYr`0)k|vq^-ApEA6kuy>&Xd|TSVWW61z)IQ^|>fIJ}EI zS=IZvys8VFD1u+SSnPc*#4DAnh4*r@`>Z@dEc;d*!4}>}ZT9ZRPTbexy-p1v1s3P@ zAI>@5y@ykI$B2b-*7E_@to#0p`PeWK?bpL-XRM9(s4x5df!5k9r+?{a>qySPQnp#%H&Pi?q%?=cJi3#%~ROMM>x9^EZx)FG5>$i5oGsd3ID)7?Qg+ z`MpdQ{z7}#r7QdVg%;#;m)OSwfjg;85t!fW%rXi!t4kMVE&{{vtacF?IlfB>{AUI7RR8Z6X8!Q zQF!N{|0THGNU*sX|4=S6%^o*cn5NgLzNHflj4(!eV&w7sVX4m({4^=DbYkN*-M@aV z&J;v_H=VCdc^cb07CGj|h6K@>p$8fKlOQ0F>(+l6Ba0Xi#*zh9P9m9_>eRKp_i!qYyTU}|$$uA`6 zKL&evX&<&UO`1(3yO(hkGR}0U9P6v=Nxe2uZgMO<>*5GvDAtE7T4q;$3n_cR9mVwf zGfMe^agro1F^CN1%aNy%249NA!M+E^x(@8Qu2+Bas3@N}QZOx5VI9vLhBq5jF+siW zQQ0L}#vLDN8T&Pj(_@VL?U4r9J&kpu2tK@mAeje3(nuB*T7vNcc|gfsBM*DUpRTYO zuDY+~JT4(rQ5(J;*alZUM*F4{D{$2#v?iTckXiS&M!tz$VKPs-`nWj-cwqFP6iep{ z2P;Ft%Xdm{6IVmx0zKaxJBPDq>b)67ThL%Meg@%(hN%4|9VqEL@y@b&M(mW-F9S+6pBxVF9nn_ET?t9t7eq7C|2v_G|F-GK5iU0h8$R!?ejAqtAiG;cpoJ1evTmCmnqMd5C_Bx*G++Tv?jRM4w0vD;@zct9T_d_5T#!<;DAu*S-cW0Glf747uODEBGyNr?%2LZjmmd$| zS2)ZY#d6E*xJWdH-7l}N)e@uF#})L>+MiMErwaNAuVoOA1_Gly&&qd6Z1vnS(|086 zT~Y6@wP?dORMdNG-63T~y?XSbVZ=%6(6;-=#FhZBjJ;Jl|C){^{R_pUTAPJ`q8*NH z$@+Qd^(wrkd<{!Ww!lOGQA=&b##r=#;NGn$avBI`MwdfvHfmR;++df5nzrLD*m{c| zqE&9iP7%G=lKo}T>w1MDK5CP2+ai514ArVeE7r(Uzpbsg!n`Z#o2{9Mmu70aTTmqz z_p4F!oeWzb_ib<|jIWTEz!OU0v{E>$6kc5l)3y)d@99h+uN1@CQut;m{IC=@p|2AH zvxSxQ->er~PytZifj^x2P7m3L2ei2!mf1Ew^u`EqKQ+i`qTa=1BI4dNezv7X*SZ1H+L-xuPJ9Tx4{VxNtv!=|=OW)zUgvN@f19{{p>VT*5 zeK`BhOMkQ0t^UH(1Z!2BhSv~UGWr>D;p}y9{R7Rt88-{-?<5F{jmL(Qko+3mvpiXx z%sVB0SCa#_s`|=nc9s8D>3>uDCkTJ))A*|WJn5e%{gZ`%mhvAP$QiH=joaNV=~u`d zbNLB8aB@>N-$(DNy&J)<`sn^Xn~?GS0X%5r)7zA%2gaf%th}#Ywd&}8)TaExFRsR= zT}_ZF_*SlMVo4UgTG{k)Qj2-gaW>RfuiiQGgy?~ip(R{WOd6ms;f;*oTk2n@-FQ)zCAv*H5sxn)>@% zU>){#P5p>=JcP}zr6(o5R~IUxak0oxK@!Fgq=bDmmWZlN`os04-G$aLIIh?bcDuhd zVyAA&)bnpkzdY=OCnVS-ujjGg5WSZ6dOnK_(cjVER>S5Z- zAQn~+(ROagdey@)sCO`%TTgG~S zAIXd!iV(*60Me0n5}r{G-H6zG7+AEc^ps|OVHjWhOvz?OFC+Fz{zbTbA+_Ey^K&dx zxZo%^*S^ zqZypL!PzKFPB!f1LI&E3KA=6Muo*bq{y3f`H`J?!Hmgc3`NmsNj%5!fa|L5jx>>Z^ z>Nl6pl-V#TFb)Q=g$?!I12Z6_F$6)<^Cnd|X8sL{khJbNC>iZSei0NMKFakgMu!G` zv90$S+g_-6n|6`{b z>n$o(g)hB}lzA|Q60Bd9c{R}^Jcd((nbCn?K?P5JS??x#ut${y65=3C3R`miEWHU# z{`1ppZxcOGo9fRlHqqCXP33ton|H7oVS2-%O}N|#DYHnk#&#&qmeH3o`#I^=-VtMB zA*em@s~x1)gYLv@TW;p!XPQ6U#>4Q}1;6&jq;l3vSIZoCYR)sglS7$DxbCkl_G0zJ z^(&1MDAHoX%#5dq3^$cxN~i{y&FQ^oOmW}#V&P5o0I$k?R=0!~2QD{;PqY^s+ElOV zbCN>Qpa!OS;ad^P!;57#)!+52!Y7#-o?OM4b7wrIJ%d?QShZ&Q;E+XpV4o2NUp44C zcakC?&WFTl!VQVd^a+*ThSE{Z6Kt8Gq*ZHW78HSIqK6lo7lC{=_hK6(^y(daytw8c zf`^VFU>=l#tDm4!jB1`&A-^S;Np1GsXF;GM%`(3ShFd7X4|byj5l_DsdO)kz?PykR z#Rxc^yxUt2P3cGt$`E9q>dey0pqbG%_99Fn+)$6lqTJT(VFM#EH9J~`y&tIuB z<-IkV>$P0GMiD$rjpfdv*+Yt}7kVh&pdkejlHn-B=V=Z)i zq1x=R_sv%NeitpfHLKkQ&9kcq>(WN=qWO8U>^6GK?t>~K{`{8sF}1?a=#oF50Mf+y zCvu+O8b2Ow@KY8)G}KrE7orP*iLFd)78s>Z@=W3WFA`|8{U2jMIkq-RAE^E0!Ca#C z;Ia+Mqj^|rxK?2GqxE-QYY&Lo*_fTMZ5#-VG{Wjo=|#Uz4*BM8AiNU4x*23}C^4Yr zIurvR!z3c8A^35e_a0J-K3k4Q>ms8qXDmQvGETyk-|@-cU7~l7g6XJ^pC?7M0=l4a z$p~-Oxvkz_>rsx)YpeTLo=s69o3BF)MBiz}a@y*NmS@1|A#6`NBbRsGSe17A_i>}S zG#E$5+J3QJKjwyklfxJ`9`;9l8g=p5>1v;Y*(@xwV)7v(H0s2%3cn5q#~Z&X$LhuC zy@GqASm?w|vZ^?v4uD%s4BQP<8MZP;|D=*X&1MkA1Uf1?!RT#fe|FFV*@*V~us|<2 z_(4+>$l&+{AdROBi+fgvza-Z3=Q4sN%=s0j7;cd#xch;SEDC^n#zU`Gp|_M_%{su0 z2A5^l4tfo3tC_9speJcXZhI+pEugw9?BtDRh#Lya1}$)*F*rcKW~_dk9_C)t1?5qs z7qK_u^p>6-cyf5@iVC2%hYS0Wn>slwOHSU20&t2rK zA{!+ltLFY@S)Q-*B43l@_3BkSBWhUv`|DA||8NvK(61uaVJ=cKI$CUa7mw_61*xQy z6^cjZm7rJey4l1d`*JxV+dkc7qJ^z)Fu7b4UOIGm_x#cYn2Rb9*hF$*(c+n&SHtvFeufOC0gIm2UgdS-4hh&1G1imO+1;W!R zQkz0{q>J9k`^zu?72R0}tJGQdVG&&sThAgPMzbQer7Of~_JV!eRqx`R^S^(EJNi{6n>QTwil%}COH@v*5-lJtw( zPgoc426&#?y6M5*&31~^j-&9Tw!#XT=Ll5KtKIa3>IWL3$CC%tRiC^KVQ7_j;1kxq zI~D~FHAGQn%I_qLx*uw=$3o`7WW870G3akG`r}_xro#ffrh3xYje3?u>3b70P!;FC z4*|Gu|To^Q|lPY@UG{3@9@UUMlX}~&#G>DCQ_i{pl0npZ`K|dpRxU~ z>9s3-h$RvnlL$jadM4y@=>c8Nek{sh1)yn5AuZLbkziM(}H+$+q zOz#OXzWRsN>ZwPUoyLX68BgX&(W|=KPSTx4M<20|dg`H;`rK2-v*H>1x~CqXKM?VJ zsttX@K1@N2wEb_kItBf7jVJ6N;c$T`sX-=;lBa~V+#=Slx87D8{8x4#-Penz z4MzLojKevjH_PpfD6jtst^iBkqhc}rUC3kd`VE^4>Lgf!cn+2gL?7s;GX?eaqD@ev zLGkeOMRSFZoY|S88RM77%cMuBP zQNV5t(*3mG3Pc_KrhwNcx6ca-9mdWA)^@P|o)&qRQup3_daz!gYms*(8m7m%XxD$+ zi?$}t)q3d$q(awwnvY746g-iXM*8GFLehso)taYEz#%|d)(+LAGi=1+F- zgLK5u9(f2P`~;a_j4=~$_Tn=gbOzcUy$v2XUXv0MU@W_ixFQkZEY}d1Slkm~OubU* z_{&lW$Nc`X48TZkq@{^@_+8ZJSr-wz-8>b6v59IkkH^EC?0;|R_1gu%M}5ixe#IDu zf^m2i3yGOm*oY2_#i2XQ&Vy;q$r!PpM!b>tr-~gJaQlADs*Tl~xP6OI#)|9gwXyoR zTK|x~V4rBbP5Z^dQe<~9$Vw4r9Qcv_GFIR1JC3vtM)WnxGr@RxnQebtf3x~tc+z*@ zsFP5!6d_9Lf8V^!BFE`(Ypt)apWlJ423%o>rh>lwk=+~zo4x*H_INb5U;W5xjn_lG zm;6|W%{1f!!f(RSKL~ztJ{vGzZ%}T-0*cXL47|z~j@O6!ya97jGt6l2_r%D)$lNC& zwNaNvYMn2$)kw{|!R1%cwoB#Fj?JWK{n+&hdX;kB@D&z{`|lqqZXZ_Z9lfj1B3PJ8 z;0Jseg|gG|0-N{_5-7ST61Z`ZC!l?Okp)iFXM1fzLR6he6|uJGxE0G-&-PB#8@i2| z2fe#qX1`BF@Qe#Qc&kwth&_7g`!1$KCiIoY$aAm}d>)UWJ zhCuP}>dh=KE)}!emp9qNclAE{lJkZ9yx*Wy{j%nBk+qzJ;k8?9)@PD_Tx)cJ)tIaY z;_d2oll3641#_u5(?e+0Lyk8-U_XyV@49fZ-nG{2G#ikmu0H7J&x7!t@&sem8RnR* zZ}UAU8P6gfX*1o-#0@*<#v7+u)KvWqt?gN%!QivPFtoG6 zFn&41RaE8Z8OmcSW+e9LhbTHj|A94lPp_&iJ;OS^r@yB4`kt&XknMR-zoGRxyO+JM zS8?-Md!j@^2pz6qc{B7D*2sMLp<#FfW43}9G;jl0NWe-z=|#dtlQdUSLvsEIT=0y3 z-jF^KGz3K)Z%f)A6#LD>v7V&s$?-CNsu1fI>H3&l4;3=oOua(~mt%#P7Jq}qvY_)A zPiMaEiO+e`S(dzhuvyy!Ad?X=5<`$sb0l<~U~7Oa<+R+~V9akUp^<-n&nnK+LtEcD zLj!}dgzu;t!z}tA5Yo()AxgQy`3`(z=kbnxIRG zhSnZP&h@Yn$YC^!IwnK+*5c2yJ{fuwZT5F;Zic?DMiz-%4}C3WN06=d1TOx%kORPo zCHxew3DVDMJHBQ?bMz7Z31368nORxf)@YxFFKr$-7JKV$!!tEW^umdo8y*tfnz5+nYkJ$~oUc==$bD5|6wA+1v*!b5pu8g6ED)8!1Iz$|O9)vCmz+3n?j9grMcBn5e zrW?k`2$D;{n0kOkW$M*}W?@wiLo3`{0u!$|@HH}VcVkI!iqAaxB+Hto*K4*ED|Knn z0|vn05^W>0&$6p}_nF;QRnM1Isosq@YA~XGn*ek9e%{wKVk}M?K(ThvA z^aPunsn-sqAZgL>(`SIBWPOGx$(FF3OudCA(~D%NLoAl`7%4+*rwlvymyltukRepa z@YV6+GA!vXWcUuB#e{Ya9cO*#>$RIvkhJKfXdRRclLjjpzQt>vN;^+*!M4y&U#=aG zVYoplEjpG0TGGd{8}oJVP6=2t5Xw2dj|7_ltE+f7A5jY(rE&Ro)50tkb@$_)Wu+q< zUNY>-1v~W(YrO!Iru=-?XMrBtjKZWvXTSbRz;_2J0cSuvO2B_cD*;o4fUPQ%fE_5H zC4CzEc>x4$?ym$ql_NBVQ9{8n5^w?);#4kRW=k$$#T>j6jV?REhJ3)T3VLt>e?P{e zvh;w;yN;5|o){G%Hw~g1j$mW5bpJ4lBQ1JiUoP+@vbS*qmB5RkG$rsu*tRVr`6b() zrPuB-3$wVi=qL(hNuP~;+2i>ay!yCPXPzFy&sUfO4#$AieSNGs6p`0JKgSp4f!x+s{(@EFBljx75p64GtXOUNV7bXUp_U zMnONpNsj&m^ckv*TlU9#Esrm`==XYa|4uCY zW4%h$VN@(w*;OuY^ew20wj<@{NTR*b6}Uk9{5XsSOnKP-2&OGSTU?eMwJ+^sJ<-ZD zV&6>}F|Pk3BqOHw{rie6_-5>cpUjEz)$7HD=U&LeA23hK18tPy({caw;xbx1UX{WR z!zr^LE~OT&b%MPQ8j|kr2{|t8?ZtY9I!!#ORB}YHnlPx6rILmgf&~6>kv^e>zy}`* zX~Ib0*W33~5|e5naSUbZlfHt7K!Tl@2PFO$_F7EhA5yr)W}H}3cJsxs|CE@GU949O z9tcL%77;2L(w7vt8~bFjKDlCX-G0gfmgu#bP3a?~8N#KB-U&l2R`@wRxfuQxiWUA6 zHW2~q*MKt7hzW*RvJQJE@twNf!gPaS%rJ$axh!`H+W)@n=Oua%DZ)Wsyi-_AwjSJ; zp2-qV8;NMa$!Mi888AH|%ww{|%pP2r1?5PXPQnt$4CU3x`ZpZYq7wy29f(#k|F=+t zkR=*R;8bM?=9REZ%*eWuOXlu}5C!H*1_}1YaKtn$ZZ2>dS_$&CXQbd4t{GEN#uS-5 zxL60KH!DNxHKN!D44I;5%G67v5*q(vnd*Z2QY=$F(0QTedzCGgsU|~srW&p0nW`i> zUZ4z0X6h5vKSt7liEW`m6CRCDxR^-~Y}OYIC`octNU{*dD6S@8|rWx#tQ(b)gp zjx|}Pd%I@E7&g{;84eyUh#^IpkLR%DWx7}2j{#Bafpz+GTk`e?MR+(>oP!}XS%d{n z#>sZPdQ)O)(bLf7fs2nqUPqNl;G>Z4P^hZi4cLKYdTomticgDfOzf8Q<7N5S!#Wf8 zEWQGjvyF!NM-wt*&@;(uV*d6ktXyNkm@dEUjJ+Ki_FwAYf_Bz5#5mRoBPqm&)Snp7 z0FafM2gpdS!^OF49hYU?6P}s-N_eKVOGrj-FxuhWJ<418ej+M`U%dVbr$vuO&xw>K zWg)LiVO{M}AuZ`&a%+NMyz}TakiEM^uUWGyFGYU}VM#wqETtNdAvB|A`}_Fh@A8*@FV8;V>_$2)DkcFNaDYmuwH*J=#g@y#35V_V!teh2jCRh0P; zSJP@3#@=x|`Kom6Rz#RX3Zj2;@ocNZmh^dLs31lpx`0~JpNNuhdB#qYNW*x=3`yjo zuS6*(*jG?N@Ms>#V+0L0s)V;`b5-5p(czwFwmB*3*Q2I-s zrw{>p2&%i(k>L#j9NEFhligUUTbOl~UM4hKgnkJ1K?dS1DZ>I0#aVV{r5@DS#8d1n z!YmSDsG%wuW~~U5!^70od0q>U zR_gS4+@X;??s0;BF4&QW)&GjyT7gYlsn_$$YY6TJY{yF7&%K9;Z-U6*3(j%A zGne%kN)GW&_*Z;!Xu2T7RQBg8-KX&!9wtkK86(0>{a2WMBAqlI#)AwRy^RRt!7^7t zl1#xdmve-+;~egS<9}EmgCsQME~)7^%dxww^nkh_@Tzb$fy>>Sb7y7|cl+XBbB$(= zSHt-3aLdGDa5Bv2t`zm|rNqp;Uz_ZGYe2Oy^5f2HsVo@}$SHHg<^FWndHX@_2M@%- zpfp${X@kt*AvHz?H(za3$WN)x!I=PlgouohRam2!wSI!(<*UGC4Wv|t%yQH!Oa40{74=#h!k$)cATooDT z99rR%?9Xt3q2MX|XN_J@oAitYuhr``-dw=b6EY;&4pKJoq8*AFJ6QPMs$IC)-5kk& z1t-|L&yf1h?9^u%{UovbpXt8Zy2mVNo$jMeyvqi!0UdmgbqBT%Ld>t!MusBP5c<2* z9crK!v>={qK8DJSN?q_p8BV?&^swSR{9>_ZG-!@vqdjdSWfTrrf$p)5a>-!7ZqP%^jzDN|tjJ-B8?Z=U^bZ@p5&N27KVgrECOu(CK`SSeNQ9bw!T;?) zEMyb>-z)7uf%{v{CEPLYGD7Y6>fzpBc>&QF$B*zL-mG9Ga>sBd{0?t^Ova0k0Gg9I z^QOeOOSl`EyTDfo4@65L@wbF4lXY?bL@N@=A-5l3k+2A33xSPft|Ed@2~T#m6STWN zmz=_R1vMET5IA>R@sog~$?4#4G6Q#vzJ#~ja^eKS`$PtreE@GljaYJ@P(tjf#NUwI zt<6j1GmSjF$m5L??qu0UA#z)gR3d>1>3XeeAumOXo^f)gWzmeEq`UgqeNYW7L-c-^UNxMs0P4Wdx+D*C-lyt0BKD;mK zTuDEcbhV^AB%LM&n<43SN$*LzRq_?mJ`pm+ccMUZqPCn#aVE4A_t*z@n zrR8GkoB0P@3$kBfS}`q{?ZgSQ@;wXO@H(yP>sPCbIS}6I#JCqyU)U&hDQP5z zzxaE;7Y=07+rBuoXf((s(<-1N7yLG>J4V@M?7~jG%knInUHC$8Y&`)E*bt4zOs3N6 zw3{nqB1tF9%3))NN17RZHwmMzA8=Cj4?HB;%-u0*=D0xMAWY9VzA63VCB7!`oK6y7 z2DT!CJF(K?qF^|q@L7RJVNT5>l(>6)iN6>AElS}ah0h6pH-*m#9Ix=HB$JhL&_p?$ z5DZgP4!#zct{LS59Fh?#d{E#WN(1`^zM*iAz|I7B3EW5d?@Y3C70yu!Zk7QQ{!HNU zm^pF*mJ95w@M3`-n459`EQwVD9|`OhC;jINoF5}`x>YzZ<&Y*LPzg*IcnTJ4cmmS| z9<11H9V;BpD2LGk?@)N8z)K3)}37lGXbwq{04hf7LjGQcWT1c@TR9EFnwcG^UWzB^N8lFR#z>>J@x(Q1ePu+; zATBo|tV2kZ{+%dQs)-2Vl|Uwe&nOkUN*pB*5pQ;P!)TG~kdSb?YD6>e^XF#@}x z`QkW=u+?VX+fq764UAG)s?=9usd6WOLlL2qUly5@U)6IZfUM?L6<>9Pr~bc*a;PRE zyrBa43A{jIS?!$s(#V|r(yE>O6-4-UD!i(e3L`x4|MPK0Me%lEj>5hog1QPzf$}3I zzidc$C@eF;6qXH-Rbk38GCWG*VA=n7R}NA|EflUL0t6{6RqCd&tThiJq(F5A|0RX% z37o62Y>GE1+)(!a3zS165nzhKO#~jSu(W}Ag_{chCJM`H<*Tr?DMvFYU>m`IM`5e1 zMrV{m2f?sc;Z6dtQdri4ISR|(alFDwg1?W#vKF;dI9d4DmDn2HLpWFzLyEvpno5a# z34BB0J_6?}+)v;g3J(;RDJ%ylR*tFd_YN=Cn_~P&1BkU~_NeXe@~Z8p_J$i}M-gEY z)v`%!QSF4p-Bmjv8zSeZSQfzp)iB8-n!?)&>i=aCXhXr1sGIU(vDW$M=l&TgoN6yLR|AC!zIho^8D#Cn`fJ1dI zCk1v>}ZUSTe zFC883pfkn$p{r?o zKKXi3*(W~W&op05Vr}#BZgKMnHY6W!y1bvnrswO`s`SK;15W+$tBLN@9@j)G+X@eB zZ!{Q&3BGs($=x1HcVmw`jQ_JX?{R3XMq1tgx@hi(=ULBjnluRqh%NKG2jWncxe**m zlzelL^Qn^aNq0W|18oRC8u!rCEG)ODkEw=6Gv9pH>3i7et6S20@g%l5Jy$rN7C4_~ zI0Lv^G8#fMG>3BsAliKM8`8^?-kalYij|JUCUK`0aKfMg(maJUD|vdsPBw_pl{=Mn zI=NcX{kYR#XhlUpGtS^3-F)*^>6CAd1ivd{r9{WF~`78?sOIH zv^DxKJfR$)s)qJ~z{gc9A#serHuEa9SF!fUt%xg*E`X#Jc0_nhMi~982pl*2>vYIkW;1g77jyHt58+omI_xBIJl9_QFVdc6%H2oVMFO(OW?~2OaElibJ)!7 z@IZw!9viUev~L)679iGMI}ytXaT&K#hd`zxD1oc5tcdJU9T~ZYz`GQdxjCw^pYR{9 zaG=1c0^7`O@IYxc!-k^0(f721kO3t<6s2eeH&3>*2-a3jMEnJQtOguD0(+>YTfqFDl$X z@V`_VXe4o{6sVrSbriqQkkvd$nTbqrq)Jel%}Z4T(hTd=kP%Akp(Z-A9*k5PlVnmpIxPe~q_8ZK$s$6V`MbKpl#;O59&7iWjuB#9#yLwm zz3>E0QuyT|&sne3Gs;-QszF@ema;^kZI70YuSAw?@c%;Ho@Y^9$ zQ_{Aw6_)XJ0v3~2S$2kEoE~6TxHwB)r;(J%%#;SezKw#@j6@g7STvs?#U}tq)An5)5@zeO)MUZ)Mes1zxXI zDsd-OJ(mjq#tMHbaj?|z8i5a~252K;)c@%!!R>;qp)1$Ua4?v5#HHQS>@JBTgA{#Ca4;cz5@4F{&Gds zN8z!;|DxibDzJz0PZ#*K^3M`jHB{E<<-%dQV%R9~e1+wDU^j*3^4lJT<#OI+g)fWn zvlYH2u(R8y6%rIlbK#HiZ=rB_tm>&;85tKK>uqH*rCX`0nU}!T{iVN;z%Q#x>?iPb zh5ZFSq;R0X7dfW!uUz@qqZn!lhHQoF2s~NgMgsR%xT(O-YS&EQ^?p*I<^s=GxV6BO z3G@D6I&=$?0on+L<_bp(TwUQ9fqhg%6D#mb)%MGkmrg=sHghjL#M;fbF<_6Q1*<$U z*sVSq?GJCOqwz%5L`Bd`U>}9$;?rX_mK!1bXRD_6i6}y65y|Db>#C)cxUn+IXM(?N zpz8nSf?}OOsewi!fieom3EW!pU^S`HnF5C?EEj^NT2(EcB^dfDmCqIUDV9}uk<1r3 zM&ZQ*-}ILL%LKlx{MQIPTKR7fc!DaztpZz}W%{LXaN-<+LsgFV2^^zxD6yeTbg%GF zt}X?V;e%CAA^Ag<|3S`gH9r-V0%zCoV2^AM1r%FWSPw3q|7{}rlgdOTE~oHL;s2M) z-~R;uNLAt80#8(Um%vs{ImrB%Q&{H5UtpVgA5yYcTm?v@z18%lPiC}Jye%TS;VSt( z0*9y;;_7g2PwA=+l*x~^DhHtsb3ami3w%o%TqS`cRQU%A9IL8u1CFicDCH0# z7y=ZI64+JYuZHmi15_guF8P%~wk?IX2>wT^%C!{!*A$K;jPY-wa_A`-ZYgXR5gb+c zGl64Oj$;IWLxsBtyjTg?OW3j!?LUz!em(UlLoRBZPyqnnVeFURiy6fd{B+m?&_%YKVFZyj5jj zsKARW$sE2V@HB-d3p`w4jDKechk1&@E^w;C*#Zwxc#XiZ3U3p*p~5=_UaYKqzrgbp zJ|=KF$29)U7Y^%Gj?N0awG>{g@KxdOY)Ebi>})3<%J52q&jntru=W}+vWCv_Z>=HR zVStkGiHuN{nMni~pm14%$EpFiyTFC2FOm3GDSWCF-UTeie=@@5ios1p7^|#Q7uXr0 zLkQ@M@P)t>A@8gUWsVhoDeyFfWrl|PsqwE_B#@{W$_TtzVGn^*74{O?s}y#IuO$2f zgul(a1;%0b{sT6QZ`{P7%w51hq7IBcF^DUFb7d*}j{;9rZQNZUdk2AS=1Qo_v!ctO zfzT`q=3yq0s|O)2?^Y-cd~xR&q=C=jq_(x2Z`5Gpf6=Se`(=ug7X0A|!wfF$+eC0lC2j}>ld)$w zh5Z__?SggVlBJ(;*~t-?R)O1s*_vPVs@6epCNVAPxAB{A?jmu%ISNnomv3(Dd8LPyY*odC*_MwOjN$4PHv2{k0f`$v0mI z8wIwcKjrwO#ItCBEu1@j>2%U88BiiZEq6vf-}#j0e46Nd8ZMu#1>SfS6>o**o7=*P z63jP;IiG4epS+w;Wt>k>(MD5RUHj1{u2ikI$q-c6eM2C3LRnif4Zd5u^asI-^w3WNXMyoQ|dYXi!tzLJKiZPhu7uE zeUYTac^q*2XnCQ-YZVe%U+BpCywLFw|8v(BI&OYe=f+;5@_`5FIg^=q!Hv5vF1br08LF2Tt^y>xiuKMViCc+d3gwU-WF z7vvB7*|kg!mi&deK0A}*T4_<3?z%TE#Wlvo`pcsij%oEwCheaWj)$Pr9=~wRt#2}| z1APQ~3iLn&lPT)S3&*^MCX?T@7mj_dM?h0x#|uYVQ%ootn^3OJOr{jjb)fDpg^tw`CQ}&b z=tz_4ENCHUh*s!G!M6z$L9c<115ImbGBws=R)T-8Fyv>ql? zHt1Q<1E2{#O{T-3$3X`L7CJWe!ekLNrMJm+0`wN>XVnWG1NxXuAJ&2(ppH7I*6Z+! z=&ZVhjsVJ+zphUhfw=?t!LSKY--8 zd~h9T7^v4Ys0!58h6F($eGI{Tkl-TtgQhG&&Op=A+-8B^`V__sdK4{V3~r)J*@RjF zdVVt!BK!qvgCFAC4NZWi9soaR#3k5RRoF<%HK+m)>A%5R{h=}gk`ujG=!gPMc@C9< zM!bN;ppRa{9p{l^@wJ2@pegta&rZV>a2wFHd-O$2qtW#eg z&~>1$wGq)0@Po!?L$&{}t*e2Hs!HFN;ao((0d+)0#1T;u2}dF|G98dm(Ts@9%ycv| zBN8$)aw#SyGebo)Uo$dNQZh3#3b)M2s5RGGGIY(<$XGK&Bg1kP=Ki1eo->C2_xyhF zocp{V=RKeI+>e<7HUJBNb%d4yRJOV5RUIeDR+HTkeqrh@BIj|ZhzCs|M zC8MIzLfhJnSe$FBx3?D=WyoZHioRe)Uf+A0>!`3#lRHcD4e`nzeRx17e~=|(a6}2wi~Tb zr~vkYLgshvMm4Yj7=*K1MPs|M9%ws>z60h1{o_!h|Dc5!PazOsD{wck?x%Jmd>D?K zG0K4zXQA%^-`Z|u4M!JWKzZP8MVC=?2&#c~z%0#U*q~s;2DuuT0^A48?Cdd|@o*IA zG0K6N*i+X7Q-VE4=Jmkd&;vFBn}7*n*Z?Fz4;T(C295$&04D>p!aYVUu&551W@MWB zqTm})@^aBIuQuamh3T>jl}#3D2_Qr|B4!2PTA=NAk5LIM1~vjKfUUrk3^>64py_Vt0b6IFsmDToFB}4GGtol8 zEEfWGq5{AYU>&d;*a|!bjGB!K7a^00On4Fs0*(e&17`r6fW^RsIWPof&hr?t$#C=_ zHhMtYLmp!q3Tg#b0*iCeLf|Wa+BoZ^z;Iw4&;e`!rT~Q-0RR(#IlxSy8<-DV3tS7VWd3P% zB`{$LT9SES8!)BVQ)Jl3!{8Y-72{Hmkq#_=77l>bz)D~pa5u04SPyIhHUU%cohBE7 znZTe4XmX$(m=8<24FsrzYAptX=D=d03)lqA2PUjS%Ka10b%(bu=3fHx5!P;5u! z>2L((JDyQ*p=P(E1$H0+VDV?@f(!%*j0Wa^fk1%``!OhiDgQ(uC?^wG5C2U-e}15l z`6c=o1tffh6^A(9sl2NQHH$Wk^l4CtGmL$}tl@^?yc;#hFpL~HDh9fN zYk_Nl6~IbhHE=hu4p&TfkujF%#=m;V%}~6#!cdxizlih^^yK|wykFaO zZN{r1WPMN!ptJXjaXtYt6SoH3VDvqeNG z-^zVJm4l^BtMk*eeRFA1w#e2eKiX!Tqu=I>DC+W%7}uj7mmhPLt{qr~W{RczAA-A! zPto#+5L&aFK7UBe?9^P`W*iF8Xx1|#l5WirgDr)tXkm^R)9JwKHscBOp(#0@Bk(kh zd>$4L`Pk^;hY|L>QdASY#>OOoQf&HI~Yc=!5wp-m-Qtotuy96#SDCb46I+@UL)- z3GJq(Se=&C0IPp$AofSN-VMY*dL&moV5$6`j^>KgPR_RQ*T` z@|CIUo5l~U^M1q6fVo4 z#K%NT#DR=Np)t5|GSk$oNLs%hzWP~1Y1NV^{s{yyRK*<)f-glp>m)-ULWRdOop zCtIrSq6G^@ykFTg{7A14ew{>w)8`9CjHUc;I=>J>ESpZ*k0Xfn_t3V-5yY{3{?O23aZl|voiIGE&;JVfyD;Fn@VqQ-S z1zgkO%ws0z3c=ZF_}@hLn@2x}i^LA13W%10d!hrk3tTHWEJS8v7km>zqo>)$jD_vS zgHmFa*b31uaMh48!k<#q#SHZ!qb{t*FxT*VeD zj@P|W;PT7pT%pJg-1Q{75Y-fSTyHK@qSgoZT~{y1Y%x}f&M^hhZ~fGe08{pBt3U1^a^6#8m1F7&-?DK~KixCyV) zr6Q4B#K(D9y_=!41}M%taI4cAQesK6_FC$jWVJ65Db_qoyR`tziO=oUWj?o9OMULO z=HUx#1FZIO)?P{0kYua>ErI5+;2*g0M=ZG<-{d=MJid-g*>=U8W`;r}IyXxS>%8 zE*nlhzK!i1SL%=jE(j-~TIhcXJ2BEhMcHB?$8>9)k(beq+cIBF=hYan!#DZq9IISVKSp8GL z`(x~7LFaz%xoS#XBE|$Hz~7H-^A4?Cg7tG=4Q*Q@;v*7vwHtg&=H$fRZrD1&)gdpY z(4{3JanL;2@|r1nb1V*X6wSkNI2PP;=nmlcb(&r*=EVGgx-Mkh596R)byZzCw%GnC z=c9IGt461a#n2nltd2X)!IH$?Xb#SNsI14$BRAY~a^a7E$$Kr#x}r#N7tMS|OpVC_ zw+zLd=4N|{i)*>6I5f5v5{ZR$Kb?3+#0B%0wx7n$r8l^P`o+=P4`aV^(^Apj%0Fi? z0k@ZyW6Fp=?0o>OSStDjXF)Ix_qD!+EAjhv^dYp9*Rr;^d%nI7<;w1Xjv0T#02qDU zZVY4sDII_NV3QAyyGFcD5zmU$_;8HV(}@2F$Nw$+DZeWI(XewuC+OceyK-cEXx+0S zKF$qp#+R3kG{^oBnuKL|W^?72?L(8TNBG4O%8L zu6Ll{|3E!BPSHjA@ms2wn5!ff2=JNEIsIL`(T@1VZ?STZ_RnRa|C|a)Dq&sC*85Pd zwb8fZG~y*|0$k_gE?YVB=0oozRskMR!2|64SDSSqfIDFMZs?FD7vUl~EgBgeB;(>?KT25E`YPAUU3rD`ZE1g*_BCNIWGrF6{co%x&B!#XK zkwq=cd6&|Ch{&35#?HP=U|R)!e|C(-TsgSy;Ji)CTs62#aNNn9nA-=gqJv%nxMSeT z!SR_@n_>-FY8g*MH;exL*}ygoqocdWz%6Mru|YJrEN~A~{}sYUU0x7<`ntfCfx~Ds z6PiQI2UiJhoz%Pg1rcnuW8)fb!~M}tsO)B1_<~4|&jaU&(I>bwp2h-at@7(Qclh+o z8{P?hP#Iq99-}iah{Tv<;5NdJV-n-pu2f&r;XL60@5aC8zBFa6h>NdAeqIFTA9%S+ zOtd;IvjVIkSSOMK&6Q#r25j|Ek8y&1enUIfVx_1AmlEqSmUG$9l83pA;B4?Up2zJ2 zH04Fa zj!*vLJ?tcS52emh+ySMK#}4)wx3T3bbc(Qq?E|+IHmI{Eo}h^JB6MU9luOZ=5Sb|i z|J~rqz`@i^Gw|PrOOy5BBDjHX7(lsCU}0RkUW|y(ha?DI(ZHJcITDdz`7xV$1z$#( zcXQT5Cm{|um)ZJDI=5a-kLNSV7Sz{VNQ&6*>Lr@zc@%VdIXt*&0K5BW(MwntT;NJ! zXRZq)*iLS%Sicamkm0zy&iavbko9+gb9wbI^uMBy)lX|+|M3{ctp5YWY!Hz#0+$b? zz4{-sKGrEzBpv#8$c{ljgnREzTDSq*Grsg_#fiZ@jjUjO_f;Dlj*Blz@<-vj^Ps-n-FrXpU2wzEqgNEeLAp)yFU|pOJ2-DZ zTu2JIN^nA|nVY+5;3~jnaVsydIu=>|Gp&wvtN%2sgU3z*7B_$qH6xMbwT z2beRa&7&xDvbmq+kb2#OGZJ(#1?iWKqQ74Xh$E@g?Nu>oL>}^OS=6Tt2I{IXF7xrjjFYR|%**#sYzlm0rzf-)=72EGcv zrl$s}$%TwY+k@-ON=O_-6u3%o0Vad8fuq1xfSbl#wl&e7r6zeg1kD|k*+CY#2?)Sj zkP}=1xD;@G=;`~hW1C$r2Kem;XTOz}my58@$B+-1;xRld?@qPlBE@g?&EPUA;5CtI z^+$_jrK^Q%F3o&ROdXU1&hgo0;}!T9J^G*#9Nx=FZ34I;Y)0&vG-i`XjhKdf1?-oA z*DRB*_E}bc*5P;V!taziyOFQ|A36=-drf7X7wEJSeZAjDaB*{Fe{nAMwp$;NqhGqzEFZBVVhi}u4z zPjG75&E!)dditd!TUbaD73>Z9hC-TJfvY-xx8=ZH9wT1B(L=rD1CKW<#DMr>NS1zj z*?0&YAZ8(A-tSy<|F9A|6^Q!;hj9;iDtNB~F6ADN@d0zeH05=X81KX|9)WN_Vea2O zd6egM+^eL3&w|c+*ljX(=mXfrYzJ2YyHA;W_5a&Bz^7yC9J_}iw&1pOG4j1;P{tN9 z!s^84y}8h1ykUXuFj}_-*FUA;CgUB4GVJ94lQAd3xMNiCggi ztz;Z}_EC?~52eU?IhCifd8*_$Zt@{J!1}Z3sjVXQ`a*DLFr5%IMiQd6)?al?ji+Hb zbPnL9g?qVG{~JnwZWa9_>p|{;p}EI6jM&sUKO6=*Xq(2zDX~(7O)o({{kzLo9&bUc z+3{BaeiypIT+?Rrov_K+YGz=oInI3I#B*;SbaGIJjyw->w2FD1&$G>;_i` zuHz!V7To%4xJqzquitP^CaBl%<7`I;ItLwyb{{G4ZE)B@pzf{AD4~(;5DoB%AHv*JO#3S zD4h4l8rH697|hJi0M9kQmd^*@3x#zImi0@(yRPARXh+3qfm%Tcu3UI*KTeC@6p;fm zak)1D0i{7f+yirUis7N%0!K4E9{TuA(L1;liab_w{Gd44CKsa4+p!QeL2?`;_b9u# zgw2Ju{Hke{4&EP|qO~tzVIK&cvov!%rsE87V^Q=`0qz4Xy6UQ(E8DrD6T8b}Ov3OI z-`MD;Rai&=+%5(VZGhx8>|Yo27$1y{;WhhSu5rOc?E6uH@o$MCBP(%WGd!~CA>}>10~#dAF9ac4O8i#nE5#~b zM5i5rPSl~x#%y(#W(F{p^IJLA8;Y;;YL2z3!ve zOTdV#rL_4GbrL1I$5uo^LOk$e9~$4$9O}esiys7;pbbl#i`u~ zi*-KwMyFQe6THr;wfN}glC&=@I`VB6{d1@Gl5g<2vD!gjeM^$|rqGc;A@n^??Xn0y zoTOFj`X5ehg`bXmt)IR$NjvU0Y-zIgekc9JSZ$TRj{IJK-Q(20_mBT$toC7N{T#{) z)R8Yb8K{4rq^%9ompZwrkuSCCdy=$$R{gM3TN%Rnogw-ur*_Lf*!C!=}IQ)ZXc#dr)Lg&VSHT-{I7nd+HmLwDY|<|8j5r{Uq(n-ukH|?NAu! zf1ePh??}>K@1vhc(k}Gjd}X-4E=k)Pp0(1c)%Vp;jm4)qbmR}=f0A}GV%Aql+NplJ zCrLYO*O6b{U;iUXYp9u6CvtsXXdD?`RP`KBLF@~bNYsiYx|d0GDYUt>4Hw2Q(9LK!qF4DlK$bX3+H-6uT4B2LyE{Zfqa!+R{f8^o~QwW?|E zSD`q87R{S>#+U`4lB~D{-mfvlPz_$Y$E}l1Nhge#NINe!k+rSA4DF@m`#n ze1WDP*)#NKgXkLX?XIE97~eBzDtWszT=7#B?_NQPheTXj{VFNgr{ueoyu4Hr-zc6R zb#tNa)sh%1dtLjl;%i@^o$y!Ws*v*U!E?%i9@{y$$m0RZZk^)2!^-!y?Yo_%3~LSS;k1 z1pkfBtgn`Oiv6@G z$_fqDN>u@sN?=!k->mrW74Q0(&i`A)A@2T1#MX%}9z{sa%5Q8Z6~_U|-=p{p#aDeP z`5B6TOz|Pq^;^+1&G|1Wo~6VeDsg$eazf4?vXsd zTf^x$#cOoqcbLX*Kgn09b!B^?G;`3?5L@t&XXkf3f@7sxcQiDo9YM-Fe@q;yKZ4@) zhvKUeCI7w(I--lz&%0Uj<|~ZQ&+zg|lJ~_*z-f)*-F!b@!;AZk3`j?nz?Ciy@Peb6 z`oWNgm_1YSx2ZtKDL(IR+H_1DMr%EBT>NTrE~o4h=mI;vbV3XZYy3i5J*&bU-%WZe zt*4Vv>+L!u<^0T<)1ON2pdsIj2f98iS?yU|OK=cYdhL7B( zirzQ|D4xI2h0~7`v`od9rAq>bW?H5Aosu7_EmQ*DgL47Vw@ZRw95EAaJTM<}2L=3q zWp&gYlI9mPIAwd~cgo{53EK0DpRV|Uihp16?mKBQ{Kbdl%I=z|t5bI6*!{Czz2XlmenM}_w<&*_ioc-v^t-Mur%#yFFHn4>(!WXZ z8zetn`&bEHR)Xo%B*729Ir-qGAqO;bx;*ysIf|b%L-PE*o6~EG_otsvieVF@Gq4UL zVJ@2KkP>g4DUZGF9E$TF7js(i-u4=)_z;@?BZl!Pdg4dSk-W_^x*FxRRcX0q(cT}? zs*cyMZq)-3()+eq(m<$ShxC*D4wvK;l)u{*e^T+@Mygi)rrA=lLiq`_OFyainS6I` zw3KTzm0QcDw`LH%26w%kKg#A%TG~1#PN1J5w$#t1uu~##cFTXSZnbArtJYqUytlQx zMM>}TACRSa`FV8%sk2Sy;n}N&=?%-wg=_$Cla)G6`zwW ziBA>Z8Rs_E$BFePoU1m>mwdDmzoo=`74L0oUoG`z(C#9$F}mizR4@F*3Nf75}{A-HK0qQWCt! z;#8*i1#a_DjT&i^b{ z7S(T;&dggJuz6*ujIHHu$^WeU>`{DUmE>_-!%W{R-mz2Z>&hTj`myhl{N0N0uXy)I zCf{BAyOe9mO5ps2y8R-y_*Bu~e-Zz-guG2_nlTPq2GjXw#9h9X63>b~mXMq1(pkLM zUjH9ja1L^93mrTMxjUD_e#J*TG@AV@??y(_nqS4jFy}Nm*uBGSZoH;NV)!_RQDh6u z8ZS|H3vXjvXhVxw8CJel>OZDx^8?l#)-SK5^ zQ}t-+SF_Wa3nDel&KCPwWk_BdVo15NF2rIDal5V%jmJ z9K+~LI~<3+N?{&zvpvxx#$&^`+Jli)`yTCOR-@neF#Uas#HTvrThx_8iwsu#?_}w; zK3CR$nrgi6w@Tjr6KybLEiM_TtzGSRu~e98%oM3tp8Mi*JuWOyT_}0SVF`9#JeIV7 zym(Mgjj#V%x?icRld#b;hpD=FSGxp_H(;jO>y+lBr-tRJS>IpT?ZiIJl%Jq&KKjh~ z@^i9HJ*p2TXGnR-2}ulBb`LAQ^eiP>bo5OzZLsKkHHAA&<1Hb_shh8!YDsUQ>ArfL zC2u}0$9<--kU?_fWvRx=!?ul!FYiZbLPyWJXs>|2>vuZO=>CWzbzu2+%Fy92?*QRG zT3md~*V2L9^5a1xwoB}%^tcSuPlb7x;;~p4mDGvl-^#n7uz?EAWJ92 z;rtgZ>V$G@x6(FXn3F#$fV4sB7fn~?<#WaAt2!o?hbiUzXUJe2ZIb85v7D+D?^2hU-iw4U6koN7K7c<<%S#j%fW~bZ zO>-E_kJGjQ@L0wI0`<7grFQ9e4J8HYgDq9(DLYW#VsST7w;+9GSiL%f^OGA+F|%bj zu3u_C2+|{bEG?~cvFiPyo zy>mgzzgF^KY>c>fT&=VLa!bhTbds_36N>4E(IHN%1yt!zc<@SZp6RCJ67ReW6?d%7 zv>S3BJ-7wZUGHhB-$`lR;jaEXt?91su{eLB3>%JJztIMpzA((KIm zomKMkqmsWMX{}G5tY4lQzI94J>k(Owy^${U;J8J?-fdSc~P<${xA!tQ{Z{nxWnjR1o!JATGJ2JvTv?A-%t1Rv811%OLjeV zxc3gL3Qr4lONTBsOL0aplSA?NFlK*!n8o>6&8GhP2p`_k9F0VKG+v;L0f@J@k`@o( z_FG0L2cX@mj!{e$w^tTzibDIfJVI**VqoR{A_MLtL3{fdS#IegHIak#&wY6LJ2P0H z8Rq&y4)6*UNG`UHJb+zGXihXbsPs+>7=i*^18Noz(Wm%Wsv_uM3|h=iy@z6X$MQU9 zD9^y(DIgXt;rNpF$D-ouUNs(#$@E64^jq(e5p7ZY9>t>ruG3dp>c`T->rgLkB!$Is zeMeAQ9Ez{XrM--9J0%VShBVde9H!6p32S^!`U_Ak{NanTXIi#W{BR8XMzkR(ewCKf zed}Z#A)_T9C~56^#kWk9{FjRV7(Dm1{YBa~TwiH%R8nF*uDTKk1MJDt9-pW=f4x2k1FLreI@CRz(h{KNc#^gypnkRc={%$J4NAO$`|EQVzxON_ zyitakcbxX$fM#{~qU9rC?{1+pBT)aw!8B*2d6Hc{lE-bCEVZYK`hnM^xx+{H?k2_W z0?)N6zetgz;I2NI7LS5E=eu-i6l&A*E5+OhzVR3>zY)`?{5R=0Shem13=PjA5Ga~ASgdX8^%DfegsV)i!({c zqcY-VS-y7cO;z#lYAj9+V^S3eG!CktwKoW1VcV2npI_JEdP&F6~U=77wFtw*W%~Ew}}K z9f#-xM)&(On>zC1nG@S8j)gv75_U_<^ z(!V2$rcZ)?{Q}xF3HtU=Y7S09plPM=$*M-F0>0fN4eD=@yxIh77ZmTzmo48*=?CJL z4fj1(kc?Y7p!2lhR()mswPP>3);klW-(+Qf>nAF}3YtCz6>z;N8_Zu8qn+3*?HYSg z*lh^hb%@e#)6a#4@Cyt`-rM%MpGiNiCdx=h92f~3($VAY!<2kGdd78xEE(wGmI_(E zzbb$4K51v)Noz9TKW{7T&47LS1q!%>=Ta7B-+^OX((UA94IAQ29=esZJQMrumQB)M zr1CfMfb>_dF0HTtnQ4xf&#ei&6LaAIjf6v3vv_hl|D?pJD4=R9?VO5$+>vyWk86+D zOuvf_HMQ%AR-UIHR5j0~Mbp^e6m6S^nqya&aW_Z4hc?`ep;Y^bY*O#;Z*YUOt16&w z)7j2W$W>)MON4Tq&&$fL9ua^B4a%z!;sokYew49OWdnbjVl z>GwjO*Giiht4`3Fd%2wB6gCqWQb}oyj=nXEX6nOibYOu#oOaccHfy=>9eB z3-llNTk1B@$(#M+0=)Yl-jNj+@o{JyG_zmk?T_rcVUc8_BkM$eYCHyllE{vQn#*XjTO diff --git a/examples/cornell_box.cpp b/examples/cornell_box.cpp index 752de40..3b20586 100644 --- a/examples/cornell_box.cpp +++ b/examples/cornell_box.cpp @@ -225,7 +225,7 @@ void setup_cornell_box() { g_scene->add_mesh(tall_box); // Short box (metal, right side) - auto short_box = create_box(Vec3(0.2f, -room_size, 0.2f), Vec3(0.9f, -0.4f, 0.9f), metal_id); + auto short_box = create_box(Vec3(0.2f, -room_size, 0.2f), Vec3(0.9f, -0.4f, 0.9f), /*metal_id*/white_id); short_box->upload_to_gpu(); g_scene->add_mesh(short_box); diff --git a/include/basic/constants.h b/include/basic/constants.h index 12c631d..60733fa 100644 --- a/include/basic/constants.h +++ b/include/basic/constants.h @@ -16,7 +16,9 @@ constexpr int DEFAULT_SPP = 1; constexpr int GBUFFER_POSITION = 0; constexpr int GBUFFER_NORMAL = 1; constexpr int GBUFFER_ALBEDO = 2; -constexpr int GBUFFER_COUNT = 3; +constexpr int GBUFFER_MATERIAL = 3; +constexpr int GBUFFER_MATERIAL_ID = 4; +constexpr int GBUFFER_COUNT = 5; /// @brief Compute shader work group size constexpr int COMPUTE_GROUP_SIZE_X = 16; diff --git a/include/core/bvh.h b/include/core/bvh.h index e45bd07..d71f841 100644 --- a/include/core/bvh.h +++ b/include/core/bvh.h @@ -56,6 +56,24 @@ struct BVHNode { uint count_; // 0 for interior node, >0 for leaf node }; +/// @brief GPU-friendly BVH node layout (std430 aligned) +struct BVHNodeGpu { + Vec4 aabb_min_left_first_; ///< xyz = aabb min, w = left_first (uint) + Vec4 aabb_max_count_; ///< xyz = aabb max, w = count (uint, 0 for interior) +}; + +/// @brief GPU-friendly triangle layout (std430 aligned) +struct TriangleGpu { + Vec4 v0_material_; ///< xyz = v0, w = material_id (uint) + Vec4 v1_; ///< xyz = v1, w = reserved + Vec4 v2_; ///< xyz = v2, w = reserved + Vec4 n0_; ///< xyz = n0, w = reserved + Vec4 n1_; ///< xyz = n1, w = reserved + Vec4 n2_; ///< xyz = n2, w = reserved + Vec4 uv0_uv1_; ///< xy = uv0, zw = uv1 + Vec4 uv2_; ///< xy = uv2, zw = reserved +}; + /// @brief Bounding Volume Hierarchy for ray tracing acceleration class BVH { public: diff --git a/shaders/gbuffer.frag b/shaders/gbuffer.frag index 61a7c4a..fbed580 100644 --- a/shaders/gbuffer.frag +++ b/shaders/gbuffer.frag @@ -1,11 +1,5 @@ #version 430 core -// Material types -const uint MATERIAL_DIFFUSE = 0u; -const uint MATERIAL_METAL = 1u; -const uint MATERIAL_DIELECTRIC = 2u; -const uint MATERIAL_EMISSIVE = 3u; - in VS_OUT { vec3 frag_pos; vec3 normal; @@ -17,33 +11,31 @@ layout(location = 0) out vec4 g_position; layout(location = 1) out vec4 g_normal; layout(location = 2) out vec4 g_albedo; layout(location = 3) out vec4 g_material; +layout(location = 4) out uint g_material_id; -// Material uniforms uniform vec3 u_albedo; uniform float u_metallic; uniform float u_roughness; uniform float u_ior; uniform vec3 u_emission; uniform uint u_material_type; +uniform uint u_material_id; uniform bool u_has_albedo_map; uniform sampler2D u_albedo_map; void main() { - // Position g_position = vec4(fs_in.frag_pos, 1.0); - - // Normal - vec3 normal = normalize(fs_in.normal); - g_normal = vec4(normal, 0.0); - - // Albedo + + vec3 n = normalize(fs_in.normal); + g_normal = vec4(n, 0.0); + vec3 albedo = u_albedo; if (u_has_albedo_map) { albedo *= texture(u_albedo_map, fs_in.texcoord).rgb; } g_albedo = vec4(albedo, 1.0); - - // Material properties + g_material = vec4(u_metallic, u_roughness, u_ior, float(u_material_type)); + g_material_id = u_material_id; } diff --git a/shaders/raytracing.comp b/shaders/raytracing.comp index afb40ba..52b9d93 100644 --- a/shaders/raytracing.comp +++ b/shaders/raytracing.comp @@ -1,26 +1,35 @@ #version 430 core -// Constants #define PI 3.14159265359 #define INV_PI 0.31830988618 #define EPSILON 1e-4 #define MAX_FLOAT 3.402823466e38 -#define MAX_RAY_DEPTH 8 -#define MAX_LIGHTS 16 #define RR_THRESHOLD 0.1 -// Material types #define MATERIAL_DIFFUSE 0 #define MATERIAL_METAL 1 #define MATERIAL_DIELECTRIC 2 #define MATERIAL_EMISSIVE 3 -// Light types #define LIGHT_DIRECTIONAL 0 #define LIGHT_POINT 1 #define LIGHT_SPOT 2 -// Structures +layout(local_size_x = 16, local_size_y = 16) in; + +// G-Buffer inputs +layout(binding = 0, rgba32f) uniform readonly image2D g_position; +layout(binding = 1, rgba32f) uniform readonly image2D g_normal; +layout(binding = 2, rgba8) uniform readonly image2D g_albedo; + +// New: material params + material id +layout(binding = 5, rgba32f) uniform readonly image2D g_material; +layout(binding = 6, r32ui) uniform readonly uimage2D g_material_id; + +// Output +layout(binding = 3, rgba32f) uniform image2D output_image; +layout(binding = 4, rgba32f) uniform image2D accumulation_image; + struct Material { vec3 albedo; float metallic; @@ -60,31 +69,29 @@ struct ScatterResult { bool scattered; vec3 attenuation; Ray scattered_ray; - float pdf; }; - -layout(local_size_x = 16, local_size_y = 16) in; - -// G-Buffer inputs -layout(binding = 0, rgba32f) uniform readonly image2D g_position; -layout(binding = 1, rgba32f) uniform readonly image2D g_normal; -layout(binding = 2, rgba8) uniform readonly image2D g_albedo; - -// Output -layout(binding = 3, rgba32f) uniform image2D output_image; -layout(binding = 4, rgba32f) uniform image2D accumulation_image; - -// Scene data -layout(std430, binding = 0) readonly buffer MaterialBuffer { - Material materials[]; +struct BVHNodeGpu { + vec4 aabb_min_left_first; // xyz min, w = left_first (uint bits in float) + vec4 aabb_max_count; // xyz max, w = count (uint bits in float) }; -layout(std430, binding = 1) readonly buffer LightBuffer { - Light lights[]; +struct TriangleGpu { + vec4 v0_material; // xyz v0, w material_id (uint bits in float) + vec4 v1; + vec4 v2; + vec4 n0; + vec4 n1; + vec4 n2; + vec4 uv0_uv1; // xy uv0, zw uv1 + vec4 uv2; // xy uv2 }; -// Uniforms +layout(std430, binding = 0) readonly buffer MaterialBuffer { Material materials[]; }; +layout(std430, binding = 1) readonly buffer LightBuffer { Light lights[]; }; +layout(std430, binding = 2) readonly buffer BVHNodeBuffer { BVHNodeGpu bvh_nodes[]; }; +layout(std430, binding = 3) readonly buffer TriangleBuffer { TriangleGpu bvh_tris[]; }; + uniform uint u_frame_count; uniform uint u_samples_per_pixel; uniform uint u_max_depth; @@ -93,25 +100,12 @@ uniform vec3 u_camera_position; uniform mat4 u_inv_view_projection; uniform bool u_enable_accumulation; uniform bool u_use_bvh; +uniform uint u_bvh_node_count; // ============================================================================ -// Utility Functions +// Utility // ============================================================================ -/** - * @brief Saturate float value to [0, 1] - */ -float saturate(float x) { - return clamp(x, 0.0, 1.0); -} - -/** - * @brief Saturate vec3 value to [0, 1] - */ -vec3 saturate(vec3 x) { - return clamp(x, vec3(0.0), vec3(1.0)); -} - /** * @brief Check if vector is near zero */ @@ -136,44 +130,28 @@ vec3 refract_vector(vec3 uv, vec3 n, float etai_over_etat) { return r_out_perp + r_out_parallel; } +uint as_uint(float f) { return floatBitsToUint(f); } +float as_float(uint u) { return uintBitsToFloat(u); } + // ============================================================================ -// Random Number Generation +// RNG (PCG) // ============================================================================ -/** - * @brief PCG hash function for random number generation - */ uint pcg_hash(uint seed) { uint state = seed * 747796405u + 2891336453u; uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u; return (word >> 22u) ^ word; } -/** - * @brief Generate random float in [0, 1) - */ float random_float(inout uint seed) { seed = pcg_hash(seed); return float(seed) / 4294967296.0; } -/** - * @brief Generate random vec2 - */ -vec2 random_vec2(inout uint seed) { - return vec2(random_float(seed), random_float(seed)); -} - -/** - * @brief Generate random vec3 - */ vec3 random_vec3(inout uint seed) { return vec3(random_float(seed), random_float(seed), random_float(seed)); } -/** - * @brief Generate random vector in unit sphere - */ vec3 random_in_unit_sphere(inout uint seed) { while (true) { vec3 p = 2.0 * random_vec3(seed) - vec3(1.0); @@ -181,506 +159,408 @@ vec3 random_in_unit_sphere(inout uint seed) { } } -/** - * @brief Generate random unit vector - */ vec3 random_unit_vector(inout uint seed) { return normalize(random_in_unit_sphere(seed)); } // ============================================================================ -// Sampling Functions +// Camera ray // ============================================================================ /** - * @brief Cosine-weighted hemisphere sampling - * @return Sampled direction in local space (z-up) + * @brief Generate primary ray in world space */ -vec3 cosine_weighted_hemisphere(inout uint seed) { - vec2 r = random_vec2(seed); - float phi = 2.0 * PI * r.x; - float cos_theta = sqrt(r.y); - float sin_theta = sqrt(1.0 - r.y); - - return vec3(cos(phi) * sin_theta, sin(phi) * sin_theta, cos_theta); -} +Ray generate_camera_ray(ivec2 pixel_coords, ivec2 image_size, inout uint seed) { + vec2 jitter = vec2(random_float(seed), random_float(seed)); + vec2 uv = (vec2(pixel_coords) + jitter) / vec2(image_size); + vec2 ndc = uv * 2.0 - 1.0; -/** - * @brief Build orthonormal basis from normal - */ -void build_onb(vec3 normal, out vec3 tangent, out vec3 bitangent) { - vec3 up = abs(normal.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); - tangent = normalize(cross(up, normal)); - bitangent = cross(normal, tangent); -} + vec4 p_near = u_inv_view_projection * vec4(ndc, 0.0, 1.0); + vec4 p_far = u_inv_view_projection * vec4(ndc, 1.0, 1.0); + vec3 near_ws = p_near.xyz / p_near.w; + vec3 far_ws = p_far.xyz / p_far.w; -/** - * @brief Transform direction from local to world space - */ -vec3 local_to_world(vec3 local_dir, vec3 normal) { - vec3 tangent, bitangent; - build_onb(normal, tangent, bitangent); - return tangent * local_dir.x + bitangent * local_dir.y + normal * local_dir.z; -} - -/** - * @brief Sample GGX distribution for microfacet normal - */ -vec3 sample_ggx(vec3 normal, float roughness, inout uint seed) { - vec2 r = random_vec2(seed); - float a = roughness * roughness; - float a2 = a * a; - - float phi = 2.0 * PI * r.x; - float cos_theta = sqrt((1.0 - r.y) / (1.0 + (a2 - 1.0) * r.y)); - float sin_theta = sqrt(1.0 - cos_theta * cos_theta); - - vec3 local_h = vec3(cos(phi) * sin_theta, sin(phi) * sin_theta, cos_theta); - return local_to_world(local_h, normal); + Ray r; + r.origin = near_ws; + r.direction = normalize(far_ws - near_ws); + return r; } // ============================================================================ -// BRDF Functions +// Intersection // ============================================================================ /** - * @brief Schlick's approximation for Fresnel reflectance + * @brief Ray-AABB intersection */ +bool intersect_aabb(Ray ray, vec3 aabb_min, vec3 aabb_max, float t_max) { + vec3 inv_d = 1.0 / ray.direction; + vec3 t0 = (aabb_min - ray.origin) * inv_d; + vec3 t1 = (aabb_max - ray.origin) * inv_d; + + vec3 tmin3 = min(t0, t1); + vec3 tmax3 = max(t0, t1); + + float tmin = max(max(tmin3.x, tmin3.y), tmin3.z); + float tmax2 = min(min(tmax3.x, tmax3.y), tmax3.z); + + return (tmax2 >= max(tmin, 0.0)) && (tmin <= t_max); +} + +/** + * @brief Moller-Trumbore triangle intersection + */ +bool intersect_triangle(Ray ray, TriangleGpu tri, inout HitInfo hit) { + vec3 v0 = tri.v0_material.xyz; + vec3 v1 = tri.v1.xyz; + vec3 v2 = tri.v2.xyz; + + vec3 e1 = v1 - v0; + vec3 e2 = v2 - v0; + vec3 pvec = cross(ray.direction, e2); + float det = dot(e1, pvec); + + if (abs(det) < EPSILON) return false; + float inv_det = 1.0 / det; + + vec3 tvec = ray.origin - v0; + float u = dot(tvec, pvec) * inv_det; + if (u < 0.0 || u > 1.0) return false; + + vec3 qvec = cross(tvec, e1); + float v = dot(ray.direction, qvec) * inv_det; + if (v < 0.0 || u + v > 1.0) return false; + + float t = dot(e2, qvec) * inv_det; + if (t < EPSILON || t >= hit.t) return false; + + // Interpolate normal/uv + float w = 1.0 - u - v; + vec3 n0 = tri.n0.xyz; + vec3 n1 = tri.n1.xyz; + vec3 n2 = tri.n2.xyz; + + vec2 uv0 = tri.uv0_uv1.xy; + vec2 uv1 = tri.uv0_uv1.zw; + vec2 uv2 = tri.uv2.xy; + + hit.hit = true; + hit.t = t; + hit.position = ray.origin + t * ray.direction; + hit.normal = normalize(n0 * w + n1 * u + n2 * v); + hit.texcoord = uv0 * w + uv1 * u + uv2 * v; + hit.material_id = as_uint(tri.v0_material.w); + return true; +} + +/** + * @brief BVH traversal (closest hit) + */ +HitInfo trace_ray_bvh(Ray ray) { + HitInfo hit; + hit.hit = false; + hit.t = MAX_FLOAT; + + if (!u_use_bvh || u_bvh_node_count == 0u) { + return hit; + } + + // Small fixed stack + uint stack[64]; + int sp = 0; + stack[sp++] = 0u; + + while (sp > 0) { + uint node_idx = stack[--sp]; + if (node_idx >= u_bvh_node_count) continue; + + BVHNodeGpu node = bvh_nodes[node_idx]; + vec3 bmin = node.aabb_min_left_first.xyz; + vec3 bmax = node.aabb_max_count.xyz; + uint left_first = as_uint(node.aabb_min_left_first.w); + uint count = as_uint(node.aabb_max_count.w); + + if (!intersect_aabb(ray, bmin, bmax, hit.t)) continue; + + if (count > 0u) { + for (uint i = 0u; i < count; ++i) { + TriangleGpu tri = bvh_tris[left_first + i]; + intersect_triangle(ray, tri, hit); + } + } else { + // Interior: push children + uint left = left_first; + uint right = left_first + 1u; + + // Depth-first; no sorting (simple) + if (sp < 63) stack[sp++] = right; + if (sp < 63) stack[sp++] = left; + } + } + + return hit; +} + +/** + * @brief Any-hit BVH for shadow ray + */ +bool trace_any_bvh(Ray ray, float t_max) { + if (!u_use_bvh || u_bvh_node_count == 0u) return false; + + uint stack[64]; + int sp = 0; + stack[sp++] = 0u; + + HitInfo hit; + hit.hit = false; + hit.t = t_max; + + while (sp > 0) { + uint node_idx = stack[--sp]; + if (node_idx >= u_bvh_node_count) continue; + + BVHNodeGpu node = bvh_nodes[node_idx]; + vec3 bmin = node.aabb_min_left_first.xyz; + vec3 bmax = node.aabb_max_count.xyz; + uint left_first = as_uint(node.aabb_min_left_first.w); + uint count = as_uint(node.aabb_max_count.w); + + if (!intersect_aabb(ray, bmin, bmax, hit.t)) continue; + + if (count > 0u) { + for (uint i = 0u; i < count; ++i) { + TriangleGpu tri = bvh_tris[left_first + i]; + if (intersect_triangle(ray, tri, hit)) { + return true; + } + } + } else { + uint left = left_first; + uint right = left_first + 1u; + if (sp < 63) stack[sp++] = right; + if (sp < 63) stack[sp++] = left; + } + } + + return false; +} + +// ============================================================================ +// Material + scattering +// ============================================================================ + vec3 fresnel_schlick(float cos_theta, vec3 f0) { return f0 + (1.0 - f0) * pow(1.0 - cos_theta, 5.0); } -/** - * @brief Fresnel reflectance for dielectrics - */ float fresnel_dielectric(float cos_theta, float ior) { float r0 = (1.0 - ior) / (1.0 + ior); r0 = r0 * r0; return r0 + (1.0 - r0) * pow(1.0 - cos_theta, 5.0); } -/** - * @brief GGX normal distribution function - */ -float distribution_ggx(vec3 N, vec3 H, float roughness) { - float a = roughness * roughness; - float a2 = a * a; - float NdotH = max(dot(N, H), 0.0); - float NdotH2 = NdotH * NdotH; - - float nom = a2; - float denom = (NdotH2 * (a2 - 1.0) + 1.0); - denom = PI * denom * denom; - - return nom / max(denom, EPSILON); -} - -/** - * @brief Smith's geometry function for GGX - */ -float geometry_smith(vec3 N, vec3 V, vec3 L, float roughness) { - float NdotV = max(dot(N, V), 0.0); - float NdotL = max(dot(N, L), 0.0); - float r = roughness + 1.0; - float k = (r * r) / 8.0; - - float ggx1 = NdotV / (NdotV * (1.0 - k) + k); - float ggx2 = NdotL / (NdotL * (1.0 - k) + k); - - return ggx1 * ggx2; -} - -// ============================================================================ -// Material Scattering -// ============================================================================ - -/** - * @brief Scatter ray for diffuse material - */ ScatterResult scatter_diffuse(Ray ray_in, HitInfo hit, Material mat, inout uint seed) { - ScatterResult result; - result.scattered = true; - result.attenuation = mat.albedo; - - // Cosine-weighted hemisphere sampling - vec3 local_dir = cosine_weighted_hemisphere(seed); - vec3 scatter_direction = local_to_world(local_dir, hit.normal); - - // Prevent degenerate scatter direction - if (near_zero(scatter_direction)) { - scatter_direction = hit.normal; - } - - result.scattered_ray.origin = hit.position + hit.normal * EPSILON; - result.scattered_ray.direction = normalize(scatter_direction); - result.pdf = max(dot(hit.normal, result.scattered_ray.direction), 0.0) * INV_PI; - - return result; + ScatterResult r; + r.scattered = true; + r.attenuation = mat.albedo; + + vec3 dir = hit.normal + random_unit_vector(seed); + if (near_zero(dir)) dir = hit.normal; + + r.scattered_ray.origin = hit.position + hit.normal * EPSILON; + r.scattered_ray.direction = normalize(dir); + return r; } -/** - * @brief Scatter ray for metal material - */ ScatterResult scatter_metal(Ray ray_in, HitInfo hit, Material mat, inout uint seed) { - ScatterResult result; - + ScatterResult r; + vec3 reflected = reflect_vector(normalize(ray_in.direction), hit.normal); - - // Add roughness perturbation vec3 fuzz = mat.roughness * random_in_unit_sphere(seed); - vec3 scatter_direction = reflected + fuzz; - - result.scattered = dot(scatter_direction, hit.normal) > 0.0; - - if (result.scattered) { - result.scattered_ray.origin = hit.position + hit.normal * EPSILON; - result.scattered_ray.direction = normalize(scatter_direction); - - vec3 V = -normalize(ray_in.direction); - vec3 H = normalize(V + result.scattered_ray.direction); - vec3 F0 = mat.albedo; - - result.attenuation = fresnel_schlick(max(dot(H, V), 0.0), F0); - result.pdf = 1.0; // Delta distribution approximation - } else { - result.attenuation = vec3(0.0); - result.pdf = 0.0; - } - - return result; + vec3 dir = reflected + fuzz; + + r.scattered = dot(dir, hit.normal) > 0.0; + r.attenuation = mat.albedo; + r.scattered_ray.origin = hit.position + hit.normal * EPSILON; + r.scattered_ray.direction = normalize(dir); + return r; } -/** - * @brief Scatter ray for dielectric material (glass) - */ ScatterResult scatter_dielectric(Ray ray_in, HitInfo hit, Material mat, inout uint seed) { - ScatterResult result; - result.scattered = true; - result.attenuation = vec3(1.0); - - float refraction_ratio = dot(ray_in.direction, hit.normal) < 0.0 ? - (1.0 / mat.ior) : mat.ior; - - vec3 unit_direction = normalize(ray_in.direction); - float cos_theta = min(dot(-unit_direction, hit.normal), 1.0); - float sin_theta = sqrt(1.0 - cos_theta * cos_theta); - + ScatterResult r; + r.scattered = true; + r.attenuation = vec3(1.0); + + vec3 unit_dir = normalize(ray_in.direction); + float cos_theta = min(dot(-unit_dir, hit.normal), 1.0); + float sin_theta = sqrt(max(0.0, 1.0 - cos_theta * cos_theta)); + + float refraction_ratio = dot(unit_dir, hit.normal) < 0.0 ? (1.0 / mat.ior) : mat.ior; bool cannot_refract = refraction_ratio * sin_theta > 1.0; - float reflectance = fresnel_dielectric(cos_theta, refraction_ratio); - - vec3 direction; - if (cannot_refract || reflectance > random_float(seed)) { - direction = reflect_vector(unit_direction, hit.normal); + float reflect_prob = fresnel_dielectric(cos_theta, refraction_ratio); + + vec3 dir; + if (cannot_refract || random_float(seed) < reflect_prob) { + dir = reflect_vector(unit_dir, hit.normal); } else { - direction = refract_vector(unit_direction, hit.normal, refraction_ratio); + dir = refract_vector(unit_dir, hit.normal, refraction_ratio); } - - result.scattered_ray.origin = hit.position + direction * EPSILON; - result.scattered_ray.direction = normalize(direction); - result.pdf = 1.0; // Delta distribution - - return result; + + r.scattered_ray.origin = hit.position + dir * EPSILON; + r.scattered_ray.direction = normalize(dir); + return r; } -/** - * @brief Scatter ray based on material type - */ ScatterResult scatter_ray(Ray ray_in, HitInfo hit, Material mat, inout uint seed) { - if (mat.type == MATERIAL_DIFFUSE) { - return scatter_diffuse(ray_in, hit, mat, seed); - } else if (mat.type == MATERIAL_METAL) { - return scatter_metal(ray_in, hit, mat, seed); - } else if (mat.type == MATERIAL_DIELECTRIC) { - return scatter_dielectric(ray_in, hit, mat, seed); - } else { - // Emissive material doesn't scatter - ScatterResult result; - result.scattered = false; - result.attenuation = vec3(0.0); - result.pdf = 0.0; - return result; - } + if (mat.type == MATERIAL_DIFFUSE) return scatter_diffuse(ray_in, hit, mat, seed); + if (mat.type == MATERIAL_METAL) return scatter_metal(ray_in, hit, mat, seed); + if (mat.type == MATERIAL_DIELECTRIC) return scatter_dielectric(ray_in, hit, mat, seed); + + ScatterResult r; + r.scattered = false; + r.attenuation = vec3(0.0); + return r; } // ============================================================================ -// Scene Intersection (G-Buffer based) +// Direct lighting (with shadow ray) // ============================================================================ -/** - * @brief Trace ray against G-Buffer (single bounce only) - * @note This is a simplified version - full path tracing needs scene geometry - */ -HitInfo trace_ray_gbuffer(Ray ray, ivec2 pixel_coords) { - HitInfo hit; - - // Read from G-Buffer at current pixel - vec4 position_data = imageLoad(g_position, pixel_coords); - vec4 normal_data = imageLoad(g_normal, pixel_coords); - vec4 albedo_data = imageLoad(g_albedo, pixel_coords); - - if (position_data.w > 0.5) { - hit.hit = true; - hit.position = position_data.xyz; - hit.normal = normalize(normal_data.xyz); - hit.material_id = uint(albedo_data.a * 255.0 + 0.5); - hit.t = length(hit.position - ray.origin); - } else { - hit.hit = false; - hit.t = MAX_FLOAT; - } - - return hit; -} - -// ============================================================================ -// Path Tracing Core -// ============================================================================ - - -/** - * @brief Sample direct lighting from light sources - */ -vec3 sample_direct_lighting(vec3 position, vec3 normal, Material mat, inout uint seed) { +vec3 eval_direct_lighting(HitInfo hit, Material mat, inout uint seed) { if (u_light_count == 0u) return vec3(0.0); - - vec3 direct_light = vec3(0.0); - - // Sample one random light (could be improved with MIS) + + // sample one light uint light_idx = uint(random_float(seed) * float(u_light_count)) % u_light_count; Light light = lights[light_idx]; - - vec3 light_dir; - float light_distance; - float pdf_light = 1.0 / float(u_light_count); - + + vec3 L; + float dist = MAX_FLOAT; + vec3 radiance = vec3(0.0); + if (light.type == LIGHT_POINT) { - vec3 to_light = light.position - position; - light_distance = length(to_light); - light_dir = to_light / light_distance; - - if (light_distance > light.range) return vec3(0.0); - - float NdotL = max(dot(normal, light_dir), 0.0); - if (NdotL > 0.0) { - float attenuation = 1.0 / max(light_distance * light_distance, 0.01); - vec3 radiance = light.color * light.intensity * attenuation; - - // Simple BRDF evaluation (diffuse) - direct_light = mat.albedo * INV_PI * radiance * NdotL / pdf_light; - } + vec3 to_light = light.position - hit.position; + dist = length(to_light); + if (dist > light.range) return vec3(0.0); + L = to_light / dist; + + float atten = 1.0 / max(dist * dist, 0.01); + radiance = light.color * light.intensity * atten; } else if (light.type == LIGHT_DIRECTIONAL) { - light_dir = normalize(-light.direction); - float NdotL = max(dot(normal, light_dir), 0.0); - - if (NdotL > 0.0) { - vec3 radiance = light.color * light.intensity; - direct_light = mat.albedo * INV_PI * radiance * NdotL / pdf_light; - } + L = normalize(-light.direction); + radiance = light.color * light.intensity; + } else { + return vec3(0.0); } - - return direct_light; + + float n_dot_l = max(dot(hit.normal, L), 0.0); + if (n_dot_l <= 0.0) return vec3(0.0); + + // shadow ray + Ray shadow_ray; + shadow_ray.origin = hit.position + hit.normal * EPSILON; + shadow_ray.direction = L; + + float t_max = (light.type == LIGHT_POINT) ? (dist - EPSILON) : MAX_FLOAT; + if (trace_any_bvh(shadow_ray, t_max)) { + return vec3(0.0); + } + + float pdf_light = 1.0 / float(u_light_count); + vec3 brdf = mat.albedo * INV_PI; // diffuse direct only (simple) + return brdf * radiance * n_dot_l / max(pdf_light, EPSILON); } -/** - * @brief Trace path and accumulate radiance - */ -vec3 trace_path(Ray initial_ray, ivec2 pixel_coords, inout uint seed) { +// ============================================================================ +// Path tracing +// ============================================================================ + +Material fetch_material(uint material_id) { + uint cnt = uint(materials.length()); + if (material_id < cnt) return materials[material_id]; + + Material m; + m.albedo = vec3(0.5); + m.metallic = 0.0; + m.emission = vec3(0.0); + m.roughness = 0.5; + m.type = MATERIAL_DIFFUSE; + m.ior = 1.5; + return m; +} + +vec3 environment_color(vec3 dir) { + // simple dark sky + return vec3(0.1, 0.1, 0.15); +} + +vec3 trace_path(Ray ray, inout uint seed) { vec3 radiance = vec3(0.0); vec3 throughput = vec3(1.0); - Ray current_ray = initial_ray; - - uint mat_count = uint(materials.length()); - - for (uint depth = 0u; depth < u_max_depth; depth++) { - // Trace ray (only first bounce uses G-Buffer) - HitInfo hit; - if (depth == 0u) { - hit = trace_ray_gbuffer(current_ray, pixel_coords); - } else { - // For subsequent bounces, we can't trace without full scene geometry - // This is a limitation of G-Buffer based approach - // In a full path tracer, you'd trace against the actual scene here - hit.hit = false; - } - - if (!hit.hit) { - // Hit sky/background - vec3 sky_color = vec3(0.1, 0.1, 0.15); - radiance += throughput * sky_color; - break; - } - - // Get material - Material mat; - if (hit.material_id < mat_count) { - mat = materials[hit.material_id]; - } else { - // Fallback material - mat.albedo = vec3(0.5); - mat.metallic = 0.0; - mat.roughness = 0.5; - mat.emission = vec3(0.0); - mat.type = MATERIAL_DIFFUSE; - mat.ior = 1.5; - } - - // Add emission - radiance += throughput * mat.emission; - - // Sample direct lighting (only for diffuse surfaces) - if (mat.type == MATERIAL_DIFFUSE && depth == 0u) { - radiance += throughput * sample_direct_lighting(hit.position, hit.normal, mat, seed); - } - - // Scatter ray - ScatterResult scatter = scatter_ray(current_ray, hit, mat, seed); - - if (!scatter.scattered || scatter.pdf < EPSILON) { - break; - } - - // Update throughput - throughput *= scatter.attenuation; - - // Russian roulette path termination - if (depth > 3u) { - float rr_probability = max(throughput.r, max(throughput.g, throughput.b)); - if (rr_probability < RR_THRESHOLD || random_float(seed) > rr_probability) { - break; - } - throughput /= rr_probability; - } - - // Continue with scattered ray - current_ray = scatter.scattered_ray; - - // Safety check for throughput - if (all(lessThan(throughput, vec3(EPSILON)))) { - break; - } - } - - return radiance; -} -/** - * @brief Enhanced direct lighting with G-Buffer - */ -vec3 render_direct_lighting(ivec2 pixel_coords, inout uint seed) { - // Read G-Buffer - vec4 position_data = imageLoad(g_position, pixel_coords); - vec4 normal_data = imageLoad(g_normal, pixel_coords); - vec4 albedo_data = imageLoad(g_albedo, pixel_coords); - - if (position_data.w < 0.5) { - return vec3(0.1, 0.1, 0.15); // Sky - } - - vec3 position = position_data.xyz; - vec3 normal = normalize(normal_data.xyz); - uint material_id = uint(albedo_data.a * 255.0 + 0.5); - - // Get material - Material mat; - uint mat_count = uint(materials.length()); - - if (material_id < mat_count) { - mat = materials[material_id]; - } else { - // Fallback: use G-Buffer albedo - mat.albedo = albedo_data.rgb; - mat.metallic = 0.0; - mat.roughness = 0.5; - mat.emission = vec3(0.0); - mat.type = MATERIAL_DIFFUSE; - mat.ior = 1.5; - } - - vec3 color = vec3(0.0); - - // Emission - color += mat.emission; - - // View direction - vec3 view_dir = normalize(u_camera_position - position); - - // Direct lighting from all lights - for (uint i = 0u; i < u_light_count; i++) { - Light light = lights[i]; - vec3 light_dir; - float light_distance; - float attenuation = 1.0; - - if (light.type == LIGHT_POINT) { - vec3 to_light = light.position - position; - light_distance = length(to_light); - light_dir = to_light / light_distance; - attenuation = 1.0 / max(light_distance * light_distance, 0.01); - - if (light_distance > light.range) continue; - } else if (light.type == LIGHT_DIRECTIONAL) { - light_dir = normalize(-light.direction); - light_distance = MAX_FLOAT; - } else { - continue; + for (uint depth = 0u; depth < u_max_depth; ++depth) { + HitInfo hit = trace_ray_bvh(ray); + if (!hit.hit) { + radiance += throughput * environment_color(ray.direction); + break; } - - float NdotL = max(dot(normal, light_dir), 0.0); - - if (NdotL > 0.0) { - vec3 H = normalize(view_dir + light_dir); - float NdotV = max(dot(normal, view_dir), 0.0); - float NdotH = max(dot(normal, H), 0.0); - float HdotV = max(dot(H, view_dir), 0.0); - - // Cook-Torrance BRDF - vec3 F0 = mix(vec3(0.04), mat.albedo, mat.metallic); - vec3 F = fresnel_schlick(HdotV, F0); - float D = distribution_ggx(normal, H, max(mat.roughness, 0.04)); - float G = geometry_smith(normal, view_dir, light_dir, mat.roughness); - - vec3 numerator = D * G * F; - float denominator = 4.0 * NdotV * NdotL + EPSILON; - vec3 specular = numerator / denominator; - - vec3 kS = F; - vec3 kD = (vec3(1.0) - kS) * (1.0 - mat.metallic); - - vec3 radiance = light.color * light.intensity * attenuation; - color += (kD * mat.albedo * INV_PI + specular) * radiance * NdotL; + + Material mat = fetch_material(hit.material_id); + + // emission + radiance += throughput * mat.emission; + + // direct light (only for diffuse to keep simple) + if (mat.type == MATERIAL_DIFFUSE) { + radiance += throughput * eval_direct_lighting(hit, mat, seed); } + + ScatterResult sc = scatter_ray(ray, hit, mat, seed); + if (!sc.scattered) break; + + throughput *= sc.attenuation; + + // RR + if (depth > 3u) { + float p = max(throughput.r, max(throughput.g, throughput.b)); + p = clamp(p, 0.0, 0.95); + if (p < RR_THRESHOLD || random_float(seed) > p) break; + throughput /= p; + } + + ray = sc.scattered_ray; + + if (all(lessThan(throughput, vec3(EPSILON)))) break; } - - // Ambient occlusion approximation - color += mat.albedo * 0.03; - - return color; + + return radiance; } void main() { ivec2 pixel_coords = ivec2(gl_GlobalInvocationID.xy); ivec2 image_size = imageSize(output_image); - - if (pixel_coords.x >= image_size.x || pixel_coords.y >= image_size.y) { - return; - } - - // Initialize random seed + if (pixel_coords.x >= image_size.x || pixel_coords.y >= image_size.y) return; + uint base_seed = uint(pixel_coords.x) + uint(pixel_coords.y) * uint(image_size.x); uint seed = base_seed + u_frame_count * 719393u; - - vec3 color = render_direct_lighting(pixel_coords, seed); - - // Clamp + + vec3 color = vec3(0.0); + + // Multi-sample + uint spp = max(u_samples_per_pixel, 1u); + for (uint s = 0u; s < spp; ++s) { + Ray cam_ray = generate_camera_ray(pixel_coords, image_size, seed); + color += trace_path(cam_ray, seed); + } + color /= float(spp); + color = clamp(color, vec3(0.0), vec3(10.0)); - - // Accumulation + if (u_enable_accumulation && u_frame_count > 0u) { vec3 accumulated = imageLoad(accumulation_image, pixel_coords).rgb; - float weight = 1.0 / float(u_frame_count + 1u); - color = mix(accumulated, color, weight); + float w = 1.0 / float(u_frame_count + 1u); + color = mix(accumulated, color, w); } - + imageStore(accumulation_image, pixel_coords, vec4(color, 1.0)); imageStore(output_image, pixel_coords, vec4(color, 1.0)); } diff --git a/src/core/bvh.cpp b/src/core/bvh.cpp index d8dfbf6..e1c747e 100644 --- a/src/core/bvh.cpp +++ b/src/core/bvh.cpp @@ -257,33 +257,62 @@ bool BVH::upload_to_gpu(Buffer& node_buffer, Buffer& triangle_buffer) { Logger::error("Cannot upload empty BVH to GPU"); return false; } - + // Reorder triangles according to BVH layout std::vector ordered_triangles; ordered_triangles.reserve(triangles_.size()); - for (uint idx : triangle_indices_) { ordered_triangles.push_back(triangles_[idx]); } - - // Upload nodes + + // Pack nodes to GPU layout + std::vector node_gpu; + node_gpu.resize(nodes_.size()); + for (size_t i = 0; i < nodes_.size(); ++i) { + const BVHNode& n = nodes_[i]; + BVHNodeGpu g; + g.aabb_min_left_first_ = Vec4(n.aabb_min_, glm::uintBitsToFloat(n.left_first_)); + g.aabb_max_count_ = Vec4(n.aabb_max_, glm::uintBitsToFloat(n.count_)); + node_gpu[i] = g; + } + + // Pack triangles to GPU layout + std::vector tri_gpu; + tri_gpu.resize(ordered_triangles.size()); + for (size_t i = 0; i < ordered_triangles.size(); ++i) { + const Triangle& t = ordered_triangles[i]; + + TriangleGpu g{}; + g.v0_material_ = Vec4(t.v0_, glm::uintBitsToFloat(t.material_id_)); + g.v1_ = Vec4(t.v1_, 0.0f); + g.v2_ = Vec4(t.v2_, 0.0f); + + g.n0_ = Vec4(t.n0_, 0.0f); + g.n1_ = Vec4(t.n1_, 0.0f); + g.n2_ = Vec4(t.n2_, 0.0f); + + g.uv0_uv1_ = Vec4(t.uv0_.x, t.uv0_.y, t.uv1_.x, t.uv1_.y); + g.uv2_ = Vec4(t.uv2_.x, t.uv2_.y, 0.0f, 0.0f); + + tri_gpu[i] = g; + } + if (!node_buffer.create(BufferType::SHADER_STORAGE_BUFFER, - nodes_.size() * sizeof(BVHNode), - nodes_.data(), - BufferUsage::STATIC_DRAW)) { + node_gpu.size() * sizeof(BVHNodeGpu), + node_gpu.data(), + BufferUsage::STATIC_DRAW)) { Logger::error("Failed to upload BVH nodes to GPU"); return false; } - - // Upload triangles + if (!triangle_buffer.create(BufferType::SHADER_STORAGE_BUFFER, - ordered_triangles.size() * sizeof(Triangle), - ordered_triangles.data(), - BufferUsage::STATIC_DRAW)) { + tri_gpu.size() * sizeof(TriangleGpu), + tri_gpu.data(), + BufferUsage::STATIC_DRAW)) { Logger::error("Failed to upload BVH triangles to GPU"); return false; } - + Logger::info("BVH uploaded to GPU successfully"); return true; } diff --git a/src/core/gbuffer.cpp b/src/core/gbuffer.cpp index b3e01d0..fb96df0 100644 --- a/src/core/gbuffer.cpp +++ b/src/core/gbuffer.cpp @@ -24,51 +24,57 @@ bool GBuffer::initialize() { Logger::warning("GBuffer already initialized"); return true; } - - // Create framebuffer + glGenFramebuffers(1, &fbo_); glBindFramebuffer(GL_FRAMEBUFFER, fbo_); - - // Create G-Buffer textures + textures_[GBUFFER_POSITION] = create_texture_(GL_RGBA32F, GL_RGBA, GL_FLOAT); - textures_[GBUFFER_NORMAL] = create_texture_(GL_RGBA32F, GL_RGBA, GL_FLOAT); - textures_[GBUFFER_ALBEDO] = create_texture_(GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE); - - // Attach textures to framebuffer + textures_[GBUFFER_NORMAL] = create_texture_(GL_RGBA32F, GL_RGBA, GL_FLOAT); + textures_[GBUFFER_ALBEDO] = create_texture_(GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE); + + // New: material params (metallic, roughness, ior, type) + textures_[GBUFFER_MATERIAL] = create_texture_(GL_RGBA32F, GL_RGBA, GL_FLOAT); + + // New: material id (integer) + textures_[GBUFFER_MATERIAL_ID] = create_texture_(GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + GBUFFER_POSITION, - GL_TEXTURE_2D, textures_[GBUFFER_POSITION], 0); + GL_TEXTURE_2D, textures_[GBUFFER_POSITION], 0); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + GBUFFER_NORMAL, - GL_TEXTURE_2D, textures_[GBUFFER_NORMAL], 0); + GL_TEXTURE_2D, textures_[GBUFFER_NORMAL], 0); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + GBUFFER_ALBEDO, - GL_TEXTURE_2D, textures_[GBUFFER_ALBEDO], 0); - - // Create depth texture + GL_TEXTURE_2D, textures_[GBUFFER_ALBEDO], 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + GBUFFER_MATERIAL, + GL_TEXTURE_2D, textures_[GBUFFER_MATERIAL], 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + GBUFFER_MATERIAL_ID, + GL_TEXTURE_2D, textures_[GBUFFER_MATERIAL_ID], 0); + glGenTextures(1, &depth_texture_); glBindTexture(GL_TEXTURE_2D, depth_texture_); glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, width_, height_, 0, - GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, nullptr); + GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, nullptr); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, - GL_TEXTURE_2D, depth_texture_, 0); - - // Set draw buffers + GL_TEXTURE_2D, depth_texture_, 0); + GLenum draw_buffers[GBUFFER_COUNT] = { GL_COLOR_ATTACHMENT0 + GBUFFER_POSITION, GL_COLOR_ATTACHMENT0 + GBUFFER_NORMAL, - GL_COLOR_ATTACHMENT0 + GBUFFER_ALBEDO + GL_COLOR_ATTACHMENT0 + GBUFFER_ALBEDO, + GL_COLOR_ATTACHMENT0 + GBUFFER_MATERIAL, + GL_COLOR_ATTACHMENT0 + GBUFFER_MATERIAL_ID }; glDrawBuffers(GBUFFER_COUNT, draw_buffers); - - // Check framebuffer completeness + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { Logger::error("GBuffer framebuffer is not complete"); glBindFramebuffer(GL_FRAMEBUFFER, 0); return false; } - + glBindFramebuffer(GL_FRAMEBUFFER, 0); - + initialized_ = true; Logger::info("GBuffer initialized successfully"); return true; @@ -76,28 +82,40 @@ bool GBuffer::initialize() { void GBuffer::release() { if (!initialized_) return; - + if (fbo_ != INVALID_HANDLE) { glDeleteFramebuffers(1, &fbo_); fbo_ = INVALID_HANDLE; } - + for (int i = 0; i < GBUFFER_COUNT; ++i) { if (textures_[i] != INVALID_HANDLE) { glDeleteTextures(1, &textures_[i]); textures_[i] = INVALID_HANDLE; } } - + if (depth_texture_ != INVALID_HANDLE) { glDeleteTextures(1, &depth_texture_); depth_texture_ = INVALID_HANDLE; } - + initialized_ = false; Logger::info("GBuffer released"); } +TextureHandle GBuffer::create_texture_(uint internal_format, uint format, uint type) { + TextureHandle texture; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D(GL_TEXTURE_2D, 0, internal_format, width_, height_, 0, format, type, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + return texture; +} + void GBuffer::render(const Scene& scene, const Shader& shader) { if (!initialized_) { Logger::error("GBuffer not initialized"); @@ -150,6 +168,9 @@ void GBuffer::render(const Scene& scene, const Shader& shader) { shader.set_float("u_metallic", material->get_metallic()); shader.set_float("u_roughness", material->get_roughness()); shader.set_uint("u_material_id", material_id); + shader.set_float("u_ior", material->get_ior()); + shader.set_vec3("u_emission", material->get_emission()); + shader.set_uint("u_material_type", static_cast(material->get_type())); // Bind textures auto albedo_tex = material->get_albedo_texture(); @@ -206,16 +227,4 @@ void GBuffer::get_dimensions(uint& width, uint& height) const { height = height_; } -TextureHandle GBuffer::create_texture_(uint internal_format, uint format, uint type) { - TextureHandle texture; - glGenTextures(1, &texture); - glBindTexture(GL_TEXTURE_2D, texture); - glTexImage2D(GL_TEXTURE_2D, 0, internal_format, width_, height_, 0, format, type, nullptr); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - return texture; -} - } // namespace are diff --git a/src/core/raytracer.cpp b/src/core/raytracer.cpp index a425b58..4525357 100644 --- a/src/core/raytracer.cpp +++ b/src/core/raytracer.cpp @@ -304,8 +304,11 @@ void RayTracer::upload_scene_data_(const Scene& scene) { void RayTracer::bind_gbuffer_(const GBuffer& gbuffer) { glBindImageTexture(0, gbuffer.get_texture(GBUFFER_POSITION), 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA32F); - glBindImageTexture(1, gbuffer.get_texture(GBUFFER_NORMAL), 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA32F); - glBindImageTexture(2, gbuffer.get_texture(GBUFFER_ALBEDO), 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8); + glBindImageTexture(1, gbuffer.get_texture(GBUFFER_NORMAL), 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA32F); + glBindImageTexture(2, gbuffer.get_texture(GBUFFER_ALBEDO), 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8); + + glBindImageTexture(5, gbuffer.get_texture(GBUFFER_MATERIAL), 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA32F); + glBindImageTexture(6, gbuffer.get_texture(GBUFFER_MATERIAL_ID), 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI); } void RayTracer::set_compute_shader(const Shader& shader) { diff --git a/src/core/shader_manager.cpp b/src/core/shader_manager.cpp index 15c41a9..88cd629 100644 --- a/src/core/shader_manager.cpp +++ b/src/core/shader_manager.cpp @@ -104,15 +104,8 @@ bool ShaderManager::load_builtin_shaders_() { return false; } shader_cache_["gbuffer"] = gbuffer_shader_; - - // Load ray tracing compute shader - if (!raytracing_shader_.load_compute("shaders/raytracing.comp")) { - Logger::error("Failed to load ray tracing shader"); - return false; - } - shader_cache_["raytracing"] = raytracing_shader_; - // Load ray tracing compute shader + // Load ray tracing compute shader Logger::info("Loading ray tracing compute shader..."); if (!raytracing_shader_.load_compute("shaders/raytracing.comp")) { Logger::error("Failed to load ray tracing shader"); @@ -120,7 +113,7 @@ bool ShaderManager::load_builtin_shaders_() { } shader_cache_["raytracing"] = raytracing_shader_; Logger::info("Ray tracing shader loaded successfully"); - + return true; } diff --git a/write.sh b/write.sh new file mode 100644 index 0000000..8d08aa4 --- /dev/null +++ b/write.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# query.sh - 遍历指定文件夹中的 .h 文件并生成 all_headers.md + +# 检查是否提供了目录参数 +if [ $# -ne 1 ]; then + echo "用法: $0 <目标文件夹路径>" + exit 1 +fi + +TARGET_DIR="$1" + +# 检查提供的路径是否为一个存在的目录 +if [ ! -d "$TARGET_DIR" ]; then + echo "错误: 目录 '$TARGET_DIR' 不存在。" + exit 1 +fi + +# 输出文件 +OUTPUT_FILE="all_files.md" + +# 清空或创建输出文件 +> "$OUTPUT_FILE" + +echo "正在扫描目录: $TARGET_DIR" +# 使用 find 命令查找所有 .h 文件 +H_FILES=$(find "$TARGET_DIR" -type f -name "*.cpp") + +# 检查是否找到了 .h 文件 +if [ -z "$H_FILES" ]; then + echo "在目录 '$TARGET_DIR' 及其子目录中未找到任何 .h 文件。" + exit 0 +fi + +# 遍历找到的每个 .h 文件 +for header_file in $H_FILES; do + # 获取相对于脚本执行位置的相对路径 + RELATIVE_PATH=$(realpath --relative-to=. "$header_file") + + # 写入分隔符和文件名 + { + echo "### 文件:$RELATIVE_PATH" + echo "" + echo '```cpp' + cat "$header_file" + echo '```' + echo "" # 添加一个空行,使文件之间有分隔 + } >> "$OUTPUT_FILE" + + echo "已处理: $RELATIVE_PATH" +done + +echo "" +echo "处理完成!所有头文件内容已合并到 $OUTPUT_FILE 中。"