From 080cf49fff239dcc9483696fb9ffc49f20cb4532 Mon Sep 17 00:00:00 2001 From: ercmine Date: Sun, 5 Apr 2026 20:19:01 -0500 Subject: [PATCH] Add forward-plus style lighting planning foundation --- CMakeLists.txt | 11 +- docs/rendering.md | 47 ++++- engine/render/README.md | 12 ++ engine/render/lighting.cpp | 208 +++++++++++++++++++++++ engine/render/lighting.hpp | 136 +++++++++++++++ engine/scene/scene.cpp | 31 ++++ engine/scene/scene.hpp | 30 ++++ engine/shell/main.cpp | 105 +++++++++++- tests/render/lighting_pipeline_tests.cpp | 91 ++++++++++ tests/scene/scene_tests.cpp | 14 ++ 10 files changed, 682 insertions(+), 3 deletions(-) create mode 100644 engine/render/lighting.cpp create mode 100644 engine/render/lighting.hpp create mode 100644 tests/render/lighting_pipeline_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d0af22b..b19016c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -119,6 +119,8 @@ add_library(render_renderer STATIC engine/render/shader_types.hpp engine/render/shader_library.hpp engine/render/shader_library.cpp + engine/render/lighting.hpp + engine/render/lighting.cpp engine/render/texture_types.hpp engine/render/bgfx/renderer_bgfx.cpp ) @@ -212,6 +214,11 @@ if(RENDER_BUILD_TESTS) render_apply_project_options(render_geometry_submission_tests) render_apply_warnings(render_geometry_submission_tests) + add_executable(render_lighting_pipeline_tests tests/render/lighting_pipeline_tests.cpp) + target_link_libraries(render_lighting_pipeline_tests PRIVATE render::renderer) + render_apply_project_options(render_lighting_pipeline_tests) + render_apply_warnings(render_lighting_pipeline_tests) + add_executable(render_scene_tests tests/scene/scene_tests.cpp) target_link_libraries(render_scene_tests PRIVATE render::scene) render_apply_project_options(render_scene_tests) @@ -231,6 +238,8 @@ if(RENDER_BUILD_TESTS) set_tests_properties(unit.renderer.shader_pipeline PROPERTIES LABELS "unit;renderer;shader") add_test(NAME unit.renderer.geometry_submission COMMAND render_geometry_submission_tests) set_tests_properties(unit.renderer.geometry_submission PROPERTIES LABELS "unit;renderer;geometry") + add_test(NAME unit.renderer.lighting_pipeline COMMAND render_lighting_pipeline_tests) + set_tests_properties(unit.renderer.lighting_pipeline PROPERTIES LABELS "unit;renderer;lighting") add_test(NAME unit.scene.runtime COMMAND render_scene_tests) set_tests_properties(unit.scene.runtime PROPERTIES LABELS "unit;scene") add_test(NAME headless.smoke.startup COMMAND render_headless_validation) @@ -238,7 +247,7 @@ if(RENDER_BUILD_TESTS) add_custom_target(render_test_unit COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure --label-regex unit - DEPENDS render_platform_types_tests render_core_runtime_tests render_serialization_tests render_filesystem_tests render_renderer_lifecycle_tests render_shader_pipeline_tests render_geometry_submission_tests render_scene_tests + DEPENDS render_platform_types_tests render_core_runtime_tests render_serialization_tests render_filesystem_tests render_renderer_lifecycle_tests render_shader_pipeline_tests render_geometry_submission_tests render_lighting_pipeline_tests render_scene_tests WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" COMMENT "Running render unit tests" ) diff --git a/docs/rendering.md b/docs/rendering.md index 02dd6e3..baf4e0e 100644 --- a/docs/rendering.md +++ b/docs/rendering.md @@ -1,9 +1,12 @@ -# Rendering + Shader Pipeline (Statements 9-10) +# Rendering + Shader Pipeline (Statements 9-13) ## Scope - Statement 9: renderer lifecycle hardening (startup/frame/resize/recovery). - Statement 10: canonical shader source/build/runtime pipeline with variants, metadata, staging, and hot reload. +- Statement 11: scene graph integration (camera/light/renderable extraction). +- Statement 12: geometry submission + batching foundation. +- Statement 13: forward-plus style lighting data path (light selection, fog/bloom/shadow/outline settings, and diagnostics). ## Engine-owned shader architecture @@ -156,3 +159,45 @@ These diagnostics are intended for profiling/debug overlays and frame-level inst - Dynamic GPU buffer update path currently uses a placeholder API contract and is staged for a follow-up statement. - Full material parameter/uniform binding and texture set binding beyond fixed slots is deferred. - GPU-driven culling/indirect draw remains deferred by design. + +## Forward-plus style lighting path (Statement 13) + +Statement 13 introduces the first engine-owned lighting frame builder under `engine/render/lighting.*`. + +### What \"forward-plus style\" means in this repo right now + +This is a **forward renderer with explicit light selection**, not a full clustered/tiled implementation yet. + +Current strategy: + +1. Collect scene lights per-view (`Scene::collect_visible_lights`). +2. Rank point lights by camera-relative importance (intensity + range-weighted distance). +3. Keep only a capped point-light set (`LightingSelectionConfig::max_point_lights`, hard-clamped by `kMaxPointLights`). +4. Build per-object point-light lists capped by `max_lights_per_object`. +5. Upload deterministic packed arrays (`DirectionalLightGpu`, `PointLightGpu`, `ObjectLightList`) to the rendering path. + +This avoids unbounded \"every object loops every light\" behavior while preserving a clean extension path toward tiled/clustered lists later. + +### Implemented engine-facing controls + +- Directional lights (color/intensity/direction, shadow flag). +- Point lights (position/color/intensity/range, shadow flag placeholder). +- Fog settings (`FogSettings`) with linear/exp/exp2 modes and validation. +- Bloom settings (`BloomSettings`) with threshold/intensity/downsample controls and validation. +- Directional shadow settings (`ShadowSettings`) with map size + bias/filter defaults and validation. +- Outline settings (`OutlineSettings`) for stylized highlight composition controls. +- Diagnostics (`LightingDiagnostics`) for selected/culled counts and highlight/shadow stats. + +### Current integration state + +- Scene components now include `RenderableComponent` emissive/highlight fields and `LightComponent::casts_shadows`. +- Scene-level lighting knobs are stored in `SceneLightingSettings` (`fog` + `bloom`) and consumed by the shell. +- `render_shell` now builds a stylized validation setup with directional + multiple point lights, emissive/highlighted renderables, and lighting frame extraction each frame. + +### Deferred to follow-up statements + +- GPU-side uniform/buffer binding of packed lighting data to lit shaders. +- Dedicated shadow-map render passes and shader sampling. +- Bloom extraction/blur/composite and outline composition passes. +- Full lit material shader programs (current shell still renders via debug triangle program). +- Debug view rendering modes (normals, emissive-only, bloom extraction, shadow atlas visualization). diff --git a/engine/render/README.md b/engine/render/README.md index df5e23b..889ed8f 100644 --- a/engine/render/README.md +++ b/engine/render/README.md @@ -37,3 +37,15 @@ New engine-owned geometry submission files: `Renderer` now exposes mesh/index/instance buffer creation + update hooks and both unique and instanced submission entry points. bgfx handles remain internal to backend implementation (`engine/render/bgfx/renderer_bgfx.cpp`). + +## Lighting frame builder (Statement 13) + +`lighting.hpp/.cpp` defines the engine-owned forward-plus style lighting planning layer. + +- Input: visible directional lights, point lights, and renderable bounds/flags. +- Planning: capped light ranking and per-object light list generation. +- Output: deterministic packed CPU structures suitable for GPU upload in future passes. +- Controls: fog, bloom, shadows, and outlines with validation helpers. +- Diagnostics: selected/culled light counts and highlighted/shadowed object metrics. + +This module intentionally keeps bgfx API details out of gameplay-facing code while preparing a scalable path to tiled/clustered improvements later. diff --git a/engine/render/lighting.cpp b/engine/render/lighting.cpp new file mode 100644 index 0000000..a168865 --- /dev/null +++ b/engine/render/lighting.cpp @@ -0,0 +1,208 @@ +#include "engine/render/lighting.hpp" + +#include "engine/core/math.hpp" + +#include +#include +#include +#include +#include + +namespace render::rendering { +namespace { + +struct RankedPointLight { + std::uint32_t source_index{0}; + float score{0.0F}; +}; + +float saturate(const float value) { + return core::clamp(value, 0.0F, 1.0F); +} + +float point_light_importance(const PointLightInput& light, const core::Vec3 camera_position) { + const core::Vec3 to_light = light.position - camera_position; + const float distance = core::length(to_light); + const float normalized = distance / std::max(light.range, core::kEpsilon); + const float attenuation = 1.0F / (1.0F + normalized * normalized); + return std::max(0.0F, light.intensity) * attenuation; +} + +float light_to_object_score(const PointLightGpu& light, const RenderableLightingInput& renderable) { + const core::Vec3 light_position{light.position_range[0], light.position_range[1], light.position_range[2]}; + const core::Vec3 to_object = renderable.position - light_position; + const float distance = core::length(to_object); + const float effective_range = light.position_range[3] + std::max(0.0F, renderable.bounding_radius); + if (distance > effective_range) { + return 0.0F; + } + + const float normalized = distance / std::max(effective_range, core::kEpsilon); + const float falloff = 1.0F - saturate(normalized * normalized); + return light.color_intensity[3] * falloff; +} + +DirectionalLightGpu pack_directional(const DirectionalLightInput& light) { + const core::Vec3 normalized = core::normalize(light.direction); + return DirectionalLightGpu{ + .direction_intensity = {normalized.x, normalized.y, normalized.z, std::max(0.0F, light.intensity)}, + .color_shadow = { + std::max(0.0F, light.color.x), + std::max(0.0F, light.color.y), + std::max(0.0F, light.color.z), + light.casts_shadows ? 1.0F : 0.0F, + }, + }; +} + +PointLightGpu pack_point(const PointLightInput& light) { + const float clamped_range = std::max(light.range, 0.01F); + const float inv_range2 = 1.0F / (clamped_range * clamped_range); + return PointLightGpu{ + .position_range = {light.position.x, light.position.y, light.position.z, clamped_range}, + .color_intensity = { + std::max(0.0F, light.color.x), + std::max(0.0F, light.color.y), + std::max(0.0F, light.color.z), + std::max(0.0F, light.intensity), + }, + .metadata = {inv_range2, light.casts_shadows ? 1.0F : 0.0F, 0.0F, 0.0F}, + }; +} + +} // namespace + +bool validate_fog_settings(const FogSettings& settings) { + if (!settings.enabled) { + return true; + } + + if (settings.mode == FogMode::Linear) { + return settings.far_distance > settings.near_distance; + } + + return settings.density >= 0.0F; +} + +bool validate_bloom_settings(const BloomSettings& settings) { + if (!settings.enabled) { + return true; + } + + return settings.threshold >= 0.0F && settings.intensity >= 0.0F && settings.downsample_steps > 0; +} + +bool validate_shadow_settings(const ShadowSettings& settings) { + if (!settings.enabled) { + return true; + } + + return settings.map_size >= 256U && settings.depth_bias >= 0.0F && settings.normal_bias >= 0.0F && settings.filter_radius >= 0.0F; +} + +LightingFrameData build_forward_plus_frame_data( + const core::Vec3& camera_position, + const std::span directional_lights, + const std::span point_lights, + const std::span renderables, + const LightingSelectionConfig& config, + FogSettings fog, + BloomSettings bloom, + ShadowSettings shadows, + OutlineSettings outlines) { + LightingFrameData result{}; + result.fog = fog; + result.bloom = bloom; + result.shadows = shadows; + result.outlines = outlines; + result.diagnostics.input_directional_lights = static_cast(directional_lights.size()); + result.diagnostics.input_point_lights = static_cast(point_lights.size()); + + const std::uint32_t directional_cap = std::min(config.max_directional_lights, kMaxDirectionalLights); + for (std::uint32_t i = 0; i < directional_cap && i < directional_lights.size(); ++i) { + const DirectionalLightInput& source = directional_lights[i]; + if (source.intensity <= 0.0F) { + continue; + } + result.directional_lights.push_back(pack_directional(source)); + if (source.casts_shadows) { + result.diagnostics.shadowed_directional_lights += 1; + } + } + + std::vector ranked; + ranked.reserve(point_lights.size()); + for (std::uint32_t i = 0; i < point_lights.size(); ++i) { + const PointLightInput& source = point_lights[i]; + if (source.intensity <= 0.0F || source.range <= 0.0F) { + continue; + } + ranked.push_back({.source_index = i, .score = point_light_importance(source, camera_position)}); + } + std::sort(ranked.begin(), ranked.end(), [](const RankedPointLight& a, const RankedPointLight& b) { + if (a.score == b.score) { + return a.source_index < b.source_index; + } + return a.score > b.score; + }); + + const std::uint32_t point_cap = std::min(config.max_point_lights, kMaxPointLights); + for (std::uint32_t i = 0; i < point_cap && i < ranked.size(); ++i) { + result.point_lights.push_back(pack_point(point_lights[ranked[i].source_index])); + } + + result.per_object_lights.reserve(renderables.size()); + for (const RenderableLightingInput& renderable : renderables) { + ObjectLightList object_list{}; + object_list.object_id = renderable.object_id; + object_list.point_light_indices.fill(kInvalidLightIndex); + + std::vector> candidates; + candidates.reserve(result.point_lights.size()); + for (std::uint32_t i = 0; i < result.point_lights.size(); ++i) { + const float score = light_to_object_score(result.point_lights[i], renderable); + if (score > 0.0F) { + candidates.push_back({i, score}); + } + } + std::sort(candidates.begin(), candidates.end(), [](const auto& a, const auto& b) { + if (a.second == b.second) { + return a.first < b.first; + } + return a.second > b.second; + }); + + const std::uint32_t per_object_cap = std::min(config.max_lights_per_object, kMaxLightsPerObject); + const std::uint32_t selected_count = std::min(per_object_cap, static_cast(candidates.size())); + object_list.point_light_count = selected_count; + for (std::uint32_t i = 0; i < selected_count; ++i) { + object_list.point_light_indices[i] = candidates[i].first; + } + + if (renderable.highlighted) { + result.diagnostics.highlighted_objects += 1; + } + result.per_object_lights.push_back(object_list); + } + + result.diagnostics.selected_directional_lights = static_cast(result.directional_lights.size()); + result.diagnostics.selected_point_lights = static_cast(result.point_lights.size()); + result.diagnostics.culled_point_lights = + result.diagnostics.input_point_lights > result.diagnostics.selected_point_lights + ? (result.diagnostics.input_point_lights - result.diagnostics.selected_point_lights) + : 0; + + if (!validate_fog_settings(result.fog)) { + result.fog.enabled = false; + } + if (!validate_bloom_settings(result.bloom)) { + result.bloom.enabled = false; + } + if (!validate_shadow_settings(result.shadows)) { + result.shadows.enabled = false; + } + + return result; +} + +} // namespace render::rendering diff --git a/engine/render/lighting.hpp b/engine/render/lighting.hpp new file mode 100644 index 0000000..1ea23b5 --- /dev/null +++ b/engine/render/lighting.hpp @@ -0,0 +1,136 @@ +#pragma once + +#include "engine/core/math.hpp" + +#include +#include +#include +#include + +namespace render::rendering { + +constexpr std::uint32_t kMaxDirectionalLights = 2; +constexpr std::uint32_t kMaxPointLights = 128; +constexpr std::uint32_t kMaxLightsPerObject = 8; +constexpr std::uint32_t kInvalidLightIndex = 0xFFFFFFFFu; + +enum class FogMode : std::uint8_t { + Disabled = 0, + Exponential, + ExponentialSquared, + Linear, +}; + +struct FogSettings { + bool enabled{false}; + FogMode mode{FogMode::ExponentialSquared}; + core::Vec3 color{0.07F, 0.08F, 0.10F}; + float density{0.025F}; + float near_distance{4.0F}; + float far_distance{40.0F}; +}; + +struct BloomSettings { + bool enabled{true}; + float threshold{1.0F}; + float intensity{0.75F}; + std::uint8_t downsample_steps{4}; +}; + +struct ShadowSettings { + bool enabled{true}; + std::uint16_t map_size{2048}; + float depth_bias{0.0015F}; + float normal_bias{0.015F}; + float filter_radius{1.5F}; +}; + +struct OutlineSettings { + bool enabled{true}; + core::Vec3 color{1.0F, 0.92F, 0.35F}; + float thickness_px{2.5F}; + float pulse_hz{0.0F}; +}; + +struct DirectionalLightInput { + core::Vec3 direction{0.0F, -1.0F, 0.0F}; + core::Vec3 color{1.0F, 1.0F, 1.0F}; + float intensity{1.0F}; + bool casts_shadows{false}; +}; + +struct PointLightInput { + core::Vec3 position{}; + core::Vec3 color{1.0F, 1.0F, 1.0F}; + float intensity{1.0F}; + float range{8.0F}; + bool casts_shadows{false}; +}; + +struct RenderableLightingInput { + std::uint32_t object_id{0}; + core::Vec3 position{}; + float bounding_radius{1.0F}; + bool highlighted{false}; +}; + +struct DirectionalLightGpu { + std::array direction_intensity{}; // xyz + intensity + std::array color_shadow{}; // rgb + shadow-enabled flag +}; + +struct PointLightGpu { + std::array position_range{}; // xyz + range + std::array color_intensity{}; // rgb + intensity + std::array metadata{}; // x:invRange^2 y:shadow z/w:reserved +}; + +struct ObjectLightList { + std::uint32_t object_id{0}; + std::array point_light_indices{}; + std::uint32_t point_light_count{0}; +}; + +struct LightingDiagnostics { + std::uint32_t input_directional_lights{0}; + std::uint32_t input_point_lights{0}; + std::uint32_t selected_directional_lights{0}; + std::uint32_t selected_point_lights{0}; + std::uint32_t culled_point_lights{0}; + std::uint32_t highlighted_objects{0}; + std::uint32_t shadowed_directional_lights{0}; +}; + +struct LightingFrameData { + std::vector directional_lights{}; + std::vector point_lights{}; + std::vector per_object_lights{}; + FogSettings fog{}; + BloomSettings bloom{}; + ShadowSettings shadows{}; + OutlineSettings outlines{}; + LightingDiagnostics diagnostics{}; +}; + +struct LightingSelectionConfig { + std::uint32_t max_directional_lights{kMaxDirectionalLights}; + std::uint32_t max_point_lights{kMaxPointLights}; + std::uint32_t max_lights_per_object{kMaxLightsPerObject}; +}; + +[[nodiscard]] bool validate_fog_settings(const FogSettings& settings); +[[nodiscard]] bool validate_bloom_settings(const BloomSettings& settings); +[[nodiscard]] bool validate_shadow_settings(const ShadowSettings& settings); + +[[nodiscard]] LightingFrameData build_forward_plus_frame_data( + const core::Vec3& camera_position, + std::span directional_lights, + std::span point_lights, + std::span renderables, + const LightingSelectionConfig& config, + FogSettings fog, + BloomSettings bloom, + ShadowSettings shadows, + OutlineSettings outlines); + +} // namespace render::rendering diff --git a/engine/scene/scene.cpp b/engine/scene/scene.cpp index 9c0b551..fcedb19 100644 --- a/engine/scene/scene.cpp +++ b/engine/scene/scene.cpp @@ -439,6 +439,37 @@ std::vector Scene::collect_visible_lights(const std::uint32_t laye return out; } +std::vector Scene::collect_highlighted_renderables(const std::uint32_t layer_mask) const { + std::vector out; + out.reserve(nodes_.size()); + walk_depth_first([this, &out, layer_mask](const SceneNodeId node) { + const NodeRecord* record = lookup(node); + if (record == nullptr || !record->visibility.enabled || !record->renderable.has_value()) { + return; + } + + const RenderableComponent& renderable_component = record->renderable.value(); + if (!renderable_component.enabled || !renderable_component.highlighted) { + return; + } + + if ((record->visibility.layer_mask & layer_mask) == 0U || (renderable_component.layer_mask & layer_mask) == 0U) { + return; + } + + out.push_back({.node = node, .renderable = &renderable_component, .world_transform = &record->world}); + }); + return out; +} + +void Scene::set_lighting_settings(const SceneLightingSettings& settings) { + lighting_settings_ = settings; +} + +const SceneLightingSettings& Scene::lighting_settings() const noexcept { + return lighting_settings_; +} + void Scene::walk_depth_first(const std::function& visitor) const { std::vector stack = root_nodes(); while (!stack.empty()) { diff --git a/engine/scene/scene.hpp b/engine/scene/scene.hpp index 31210da..abfd783 100644 --- a/engine/scene/scene.hpp +++ b/engine/scene/scene.hpp @@ -58,6 +58,7 @@ struct LightComponent { core::Vec3 color{1.0F, 1.0F, 1.0F}; float intensity{1.0F}; float range{10.0F}; + bool casts_shadows{false}; }; struct RenderableComponent { @@ -68,6 +69,30 @@ struct RenderableComponent { std::uint32_t index_count{0}; rendering::MaterialBinding material{}; rendering::DrawState draw_state{}; + bool lit{true}; + core::Vec3 albedo_tint{1.0F, 1.0F, 1.0F}; + core::Vec3 emissive_color{0.0F, 0.0F, 0.0F}; + float emissive_intensity{0.0F}; + bool highlighted{false}; +}; + +struct SceneFogSettings { + bool enabled{false}; + core::Vec3 color{0.07F, 0.08F, 0.10F}; + float density{0.025F}; + float near_distance{4.0F}; + float far_distance{40.0F}; +}; + +struct SceneBloomSettings { + bool enabled{true}; + float threshold{1.0F}; + float intensity{0.75F}; +}; + +struct SceneLightingSettings { + SceneFogSettings fog{}; + SceneBloomSettings bloom{}; }; struct CameraView { @@ -134,6 +159,10 @@ class Scene { [[nodiscard]] std::optional build_camera_view(float aspect_ratio) const; [[nodiscard]] std::vector collect_visible_renderables(std::uint32_t layer_mask = kVisibilityAll) const; [[nodiscard]] std::vector collect_visible_lights(std::uint32_t layer_mask = kVisibilityAll) const; + [[nodiscard]] std::vector collect_highlighted_renderables(std::uint32_t layer_mask = kVisibilityAll) const; + + void set_lighting_settings(const SceneLightingSettings& settings); + [[nodiscard]] const SceneLightingSettings& lighting_settings() const noexcept; void walk_depth_first(const std::function& visitor) const; @@ -167,6 +196,7 @@ class Scene { std::vector nodes_{}; std::vector free_indices_{}; std::optional active_camera_{}; + SceneLightingSettings lighting_settings_{}; }; } // namespace render::scene diff --git a/engine/shell/main.cpp b/engine/shell/main.cpp index 86cf06a..4ddd579 100644 --- a/engine/shell/main.cpp +++ b/engine/shell/main.cpp @@ -2,6 +2,7 @@ #include "engine/platform/platform_log.hpp" #include "engine/platform/platform_runtime.hpp" #include "engine/render/draw_submission.hpp" +#include "engine/render/lighting.hpp" #include "engine/render/renderer.hpp" #include "engine/render/shader_library.hpp" #include "engine/scene/scene.hpp" @@ -45,7 +46,7 @@ int main(int argc, char** argv) { render::platform::RuntimeConfig platform_config{}; platform_config.app_name = "render-shell"; platform_config.org_name = "render"; - platform_config.window.title = "render :: Statement 12 geometry shell"; + platform_config.window.title = "render :: Statement 13 stylized lighting shell"; platform_config.window.width = 1280; platform_config.window.height = 720; platform_config.window.resizable = true; @@ -104,6 +105,23 @@ int main(int argc, char** argv) { scene.set_camera(camera_node, {}); scene.set_active_camera(camera_node); + render::scene::SceneLightingSettings scene_lighting{}; + scene_lighting.fog.enabled = true; + scene_lighting.fog.density = 0.04F; + scene_lighting.bloom.enabled = true; + scene_lighting.bloom.threshold = 0.8F; + scene_lighting.bloom.intensity = 1.2F; + scene.set_lighting_settings(scene_lighting); + + const auto sun_node = scene.create_node("sun_directional"); + scene.set_parent(sun_node, root, render::scene::ReparentPolicy::KeepLocalTransform); + render::scene::LightComponent sun{}; + sun.type = render::scene::LightType::Directional; + sun.color = {1.0F, 0.95F, 0.85F}; + sun.intensity = 1.8F; + sun.casts_shadows = true; + scene.set_light(sun_node, sun); + constexpr std::uint32_t kInstanceRows = 12; std::vector nodes; nodes.reserve(kInstanceRows * kInstanceRows); @@ -120,11 +138,28 @@ int main(int argc, char** argv) { rc.index_buffer = index_buffer; rc.index_count = static_cast(indices.size()); rc.material.program = program; + rc.highlighted = (x == kInstanceRows / 2U && y == kInstanceRows / 2U); + rc.emissive_color = {0.1F, 0.4F, 1.0F}; + rc.emissive_intensity = (x + y) % 5U == 0U ? 2.0F : 0.0F; scene.set_renderable(node, rc); nodes.push_back(node); } } + for (std::uint32_t i = 0; i < 10U; ++i) { + const auto light_node = scene.create_node("biolum_point"); + scene.set_parent(light_node, root, render::scene::ReparentPolicy::KeepLocalTransform); + render::core::Transform t{}; + t.translation = {static_cast(i) - 5.0F, 1.5F, ((i % 2U) == 0U) ? 1.25F : -1.25F}; + scene.set_local_transform(light_node, t); + render::scene::LightComponent point{}; + point.type = render::scene::LightType::Point; + point.color = {0.2F, 0.7F, 1.0F}; + point.intensity = 2.0F; + point.range = 5.0F; + scene.set_light(light_node, point); + } + while (!runtime.should_quit()) { runtime.begin_frame(); runtime.pump_events(); @@ -148,14 +183,19 @@ int main(int argc, char** argv) { const float aspect_ratio = static_cast(window.width) / static_cast(window.height); const auto camera_view = scene.build_camera_view(aspect_ratio); + render::core::Vec3 camera_position{}; if (camera_view.has_value()) { const auto view_matrix = to_array(camera_view->view); const auto projection_matrix = to_array(camera_view->projection); renderer.set_view_transform(kMainView, std::span{view_matrix}, std::span{projection_matrix}); + if (const auto* camera_world = scene.world_transform(camera_view->node); camera_world != nullptr) { + camera_position = camera_world->translation; + } } std::vector submissions; const auto visible = scene.collect_visible_renderables(); + const auto visible_lights = scene.collect_visible_lights(); submissions.reserve(visible.size()); for (const auto& vr : visible) { if (vr.renderable == nullptr || vr.world_transform == nullptr || !vr.renderable->material.valid()) continue; @@ -172,6 +212,69 @@ int main(int argc, char** argv) { submissions.push_back(draw); } + std::vector directional_lights; + std::vector point_lights; + for (const auto& visible_light : visible_lights) { + if (visible_light.light == nullptr || visible_light.world_transform == nullptr) { + continue; + } + if (visible_light.light->type == render::scene::LightType::Directional) { + const render::core::Vec3 direction = render::core::forward(*visible_light.world_transform) * -1.0F; + directional_lights.push_back(render::rendering::DirectionalLightInput{ + .direction = direction, + .color = visible_light.light->color, + .intensity = visible_light.light->intensity, + .casts_shadows = visible_light.light->casts_shadows, + }); + } else { + point_lights.push_back(render::rendering::PointLightInput{ + .position = visible_light.world_transform->translation, + .color = visible_light.light->color, + .intensity = visible_light.light->intensity, + .range = visible_light.light->range, + .casts_shadows = visible_light.light->casts_shadows, + }); + } + } + + std::vector lit_inputs; + lit_inputs.reserve(visible.size()); + for (const auto& vr : visible) { + if (vr.world_transform == nullptr || vr.renderable == nullptr) { + continue; + } + lit_inputs.push_back(render::rendering::RenderableLightingInput{ + .object_id = vr.node.index, + .position = vr.world_transform->translation, + .bounding_radius = 1.0F, + .highlighted = vr.renderable->highlighted, + }); + } + + render::rendering::FogSettings fog{}; + fog.enabled = scene.lighting_settings().fog.enabled; + fog.color = scene.lighting_settings().fog.color; + fog.density = scene.lighting_settings().fog.density; + fog.near_distance = scene.lighting_settings().fog.near_distance; + fog.far_distance = scene.lighting_settings().fog.far_distance; + + render::rendering::BloomSettings bloom{}; + bloom.enabled = scene.lighting_settings().bloom.enabled; + bloom.threshold = scene.lighting_settings().bloom.threshold; + bloom.intensity = scene.lighting_settings().bloom.intensity; + + const render::rendering::LightingFrameData lighting_frame = render::rendering::build_forward_plus_frame_data( + camera_position, + directional_lights, + point_lights, + lit_inputs, + {}, + fog, + bloom, + {}, + {}); + (void)lighting_frame; + render::rendering::SubmissionDiagnostics diagnostics{}; const auto batches = render::rendering::build_draw_batches(submissions, {}, &diagnostics); for (const auto& batch : batches) { diff --git a/tests/render/lighting_pipeline_tests.cpp b/tests/render/lighting_pipeline_tests.cpp new file mode 100644 index 0000000..1fa42e2 --- /dev/null +++ b/tests/render/lighting_pipeline_tests.cpp @@ -0,0 +1,91 @@ +#include "engine/render/lighting.hpp" + +#include +#include + +int main() { + using namespace render; + using namespace render::rendering; + + std::vector directional = { + {.direction = {0.0F, -1.0F, 0.0F}, .color = {1.0F, 0.95F, 0.9F}, .intensity = 2.0F, .casts_shadows = true}, + {.direction = {1.0F, -0.5F, 0.0F}, .color = {0.2F, 0.3F, 0.8F}, .intensity = 0.4F, .casts_shadows = false}, + {.direction = {-1.0F, -0.5F, 0.0F}, .color = {0.2F, 0.8F, 0.3F}, .intensity = 0.1F, .casts_shadows = false}, + }; + + std::vector points; + for (std::uint32_t i = 0; i < 24; ++i) { + points.push_back(PointLightInput{ + .position = {static_cast(i) * 1.5F, 0.0F, 0.0F}, + .color = {1.0F, 0.3F, 0.2F}, + .intensity = 2.0F - (static_cast(i) * 0.04F), + .range = 6.0F, + .casts_shadows = false, + }); + } + + std::vector objects = { + {.object_id = 1, .position = {0.0F, 0.0F, 0.0F}, .bounding_radius = 0.5F, .highlighted = true}, + {.object_id = 2, .position = {6.0F, 0.0F, 0.0F}, .bounding_radius = 1.0F, .highlighted = false}, + {.object_id = 3, .position = {20.0F, 0.0F, 0.0F}, .bounding_radius = 1.0F, .highlighted = true}, + }; + + LightingSelectionConfig config{}; + config.max_directional_lights = 1; + config.max_point_lights = 8; + config.max_lights_per_object = 4; + + FogSettings fog{}; + fog.enabled = true; + fog.mode = FogMode::Linear; + fog.near_distance = 2.0F; + fog.far_distance = 60.0F; + + BloomSettings bloom{}; + bloom.enabled = true; + bloom.threshold = 0.8F; + bloom.intensity = 1.25F; + + ShadowSettings shadows{}; + shadows.enabled = true; + shadows.map_size = 1024; + + const LightingFrameData frame = build_forward_plus_frame_data( + core::Vec3{0.0F, 0.0F, 0.0F}, + directional, + points, + objects, + config, + fog, + bloom, + shadows, + OutlineSettings{}); + + assert(frame.directional_lights.size() == 1); + assert(frame.diagnostics.shadowed_directional_lights == 1); + assert(frame.point_lights.size() == 8); + assert(frame.per_object_lights.size() == objects.size()); + assert(frame.per_object_lights[0].point_light_count > 0); + assert(frame.per_object_lights[0].point_light_count <= 4); + assert(frame.diagnostics.highlighted_objects == 2); + assert(frame.diagnostics.culled_point_lights == (points.size() - frame.point_lights.size())); + + FogSettings invalid_fog{}; + invalid_fog.enabled = true; + invalid_fog.mode = FogMode::Linear; + invalid_fog.near_distance = 10.0F; + invalid_fog.far_distance = 3.0F; + assert(!validate_fog_settings(invalid_fog)); + + BloomSettings invalid_bloom{}; + invalid_bloom.enabled = true; + invalid_bloom.downsample_steps = 0; + assert(!validate_bloom_settings(invalid_bloom)); + + ShadowSettings invalid_shadow{}; + invalid_shadow.enabled = true; + invalid_shadow.map_size = 128; + assert(!validate_shadow_settings(invalid_shadow)); + + return 0; +} diff --git a/tests/scene/scene_tests.cpp b/tests/scene/scene_tests.cpp index 8729baa..a4ce73f 100644 --- a/tests/scene/scene_tests.cpp +++ b/tests/scene/scene_tests.cpp @@ -62,6 +62,9 @@ int main() { renderable.material.program.idx = 11; renderable.index_count = 6; renderable.layer_mask = 0x1u; + renderable.highlighted = true; + renderable.emissive_color = {0.5F, 0.8F, 1.0F}; + renderable.emissive_intensity = 3.0F; assert(graph.set_renderable(grandchild, renderable)); const auto default_visible = graph.collect_visible_renderables(0x2u); @@ -72,6 +75,17 @@ int main() { assert(graph.set_visibility(grandchild, visible)); const auto filtered_visible = graph.collect_visible_renderables(0x1u); assert(filtered_visible.size() == 1); + const auto highlighted = graph.collect_highlighted_renderables(0x1u); + assert(highlighted.size() == 1); + + scene::SceneLightingSettings lighting{}; + lighting.fog.enabled = true; + lighting.fog.density = 0.03F; + lighting.bloom.enabled = true; + lighting.bloom.intensity = 1.2F; + graph.set_lighting_settings(lighting); + assert(graph.lighting_settings().fog.enabled); + assert(core::nearly_equal(graph.lighting_settings().bloom.intensity, 1.2F)); assert(graph.set_debug_name(child, "duplicate")); assert(graph.set_debug_name(grandchild, "duplicate"));