From d49ff922841c872481b71865541e2367c426a6bd Mon Sep 17 00:00:00 2001 From: Pursche Date: Sat, 11 Oct 2025 00:17:31 +0200 Subject: [PATCH] Add Decal rendering Add Forward+ style Decal rendering through LightRenderer Add Decal component Add Inspectors for AABB and Decal Add function for loading textures into ModelRenderers texture array Add DirtyAABB tag Fix issue where WorldAABB was not updated when AABB changed Update Engine submodule --- .../Game-Lib/Game-Lib/ECS/Components/AABB.h | 2 + .../Game-Lib/Game-Lib/ECS/Components/Decal.h | 18 + .../ECS/Systems/CalculateCameraMatrices.cpp | 2 - .../Game-Lib/ECS/Systems/UpdateAABBs.cpp | 75 +-- Source/Game-Lib/Game-Lib/Editor/Inspector.cpp | 136 +++++- Source/Game-Lib/Game-Lib/Editor/Inspector.h | 2 + .../Game-Lib/Rendering/GameRenderer.cpp | 24 +- .../Game-Lib/Rendering/GameRenderer.h | 2 + .../Rendering/Light/LightRenderer.cpp | 438 ++++++++++++++++++ .../Game-Lib/Rendering/Light/LightRenderer.h | 107 +++++ .../Rendering/Material/MaterialRenderer.cpp | 18 +- .../Rendering/Material/MaterialRenderer.h | 4 +- .../Game-Lib/Rendering/Model/ModelLoader.cpp | 5 +- .../Game-Lib/Rendering/Model/ModelLoader.h | 4 +- .../Rendering/Model/ModelRenderer.cpp | 12 + .../Game-Lib/Rendering/Model/ModelRenderer.h | 2 + .../Game-Lib/Rendering/RenderResources.h | 4 +- .../Game-Lib/Game-Lib/Rendering/RenderUtils.h | 36 +- .../Rendering/Shadow/ShadowRenderer.cpp | 6 +- .../Shaders/Shaders/Include/Culling.inc.hlsl | 283 ++++++++++- Source/Shaders/Shaders/Include/Debug.inc.hlsl | 74 ++- .../Shaders/Shaders/Include/Shadows.inc.hlsl | 6 +- .../Shaders/Light/Classification.cs.hlsl | 323 +++++++++++++ .../Shaders/Light/LightShared.inc.hlsl | 130 ++++++ Source/Shaders/Shaders/MaterialPass.cs.hlsl | 76 ++- .../Shaders/Shaders/Terrain/Culling.cs.hlsl | 4 +- .../Shaders/Terrain/TerrainShared.inc.hlsl | 12 +- Source/Shaders/Shaders/Utils/Culling.cs.hlsl | 6 +- .../Shaders/Utils/CullingInstanced.cs.hlsl | 6 +- Source/Shaders/Shaders/common.inc.hlsl | 57 +++ Submodules/Engine | 2 +- 31 files changed, 1771 insertions(+), 105 deletions(-) create mode 100644 Source/Game-Lib/Game-Lib/ECS/Components/Decal.h create mode 100644 Source/Game-Lib/Game-Lib/Rendering/Light/LightRenderer.cpp create mode 100644 Source/Game-Lib/Game-Lib/Rendering/Light/LightRenderer.h create mode 100644 Source/Shaders/Shaders/Light/Classification.cs.hlsl create mode 100644 Source/Shaders/Shaders/Light/LightShared.inc.hlsl diff --git a/Source/Game-Lib/Game-Lib/ECS/Components/AABB.h b/Source/Game-Lib/Game-Lib/ECS/Components/AABB.h index e9ce95de..b9d06d55 100644 --- a/Source/Game-Lib/Game-Lib/ECS/Components/AABB.h +++ b/Source/Game-Lib/Game-Lib/ECS/Components/AABB.h @@ -16,4 +16,6 @@ namespace ECS::Components vec3 min; vec3 max; }; + + struct DirtyAABB {}; } \ No newline at end of file diff --git a/Source/Game-Lib/Game-Lib/ECS/Components/Decal.h b/Source/Game-Lib/Game-Lib/ECS/Components/Decal.h new file mode 100644 index 00000000..00e6c195 --- /dev/null +++ b/Source/Game-Lib/Game-Lib/ECS/Components/Decal.h @@ -0,0 +1,18 @@ +#pragma once +#include +#include + +namespace ECS::Components +{ + struct Decal + { + std::string texturePath; + Color colorMultiplier = Color::White; + hvec2 thresholdMinMax = hvec2(0.0f, 1.0f); + hvec2 minUV = hvec2(0.0f, 0.0f); + hvec2 maxUV = hvec2(1.0f, 1.0f); + u32 flags = 0; // DecalFlags + }; + + struct DirtyDecal {}; +} \ No newline at end of file diff --git a/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateCameraMatrices.cpp b/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateCameraMatrices.cpp index f0170158..e407c710 100644 --- a/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateCameraMatrices.cpp +++ b/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateCameraMatrices.cpp @@ -83,8 +83,6 @@ namespace ECS::Systems if (CVAR_CameraLockCullingFrustum.Get() == 0) { - mat4x4 m = glm::transpose(camera.worldToClip); - glm::vec3 Front = glm::vec3(0, 0, 1); glm::vec3 Right = glm::vec3(1, 0, 0); glm::vec3 Up = glm::vec3(0, 1, 0); diff --git a/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAABBs.cpp b/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAABBs.cpp index 25771250..1e37e412 100644 --- a/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAABBs.cpp +++ b/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAABBs.cpp @@ -8,42 +8,55 @@ namespace ECS::Systems { + void UpdateWorldAABB(const Components::Transform& transform, const Components::AABB& aabb, Components::WorldAABB& worldAABB) + { + // Calculate the world AABB + glm::vec3 min = aabb.centerPos - aabb.extents; + glm::vec3 max = aabb.centerPos + aabb.extents; + + glm::vec3 corners[8] = { + glm::vec3(min.x, min.y, min.z), + glm::vec3(min.x, min.y, max.z), + glm::vec3(min.x, max.y, min.z), + glm::vec3(min.x, max.y, max.z), + glm::vec3(max.x, min.y, min.z), + glm::vec3(max.x, min.y, max.z), + glm::vec3(max.x, max.y, min.z), + glm::vec3(max.x, max.y, max.z) + }; + + const mat4x4 transformMatrix = transform.GetMatrix(); + for (i32 i = 0; i < 8; ++i) + { + corners[i] = transformMatrix * glm::vec4(corners[i], 1.0f); + } + + worldAABB.min = vec3(1000000000.0f); + worldAABB.max = vec3(-1000000000.0f); + + for (i32 i = 1; i < 8; ++i) + { + worldAABB.min = glm::min(worldAABB.min, corners[i]); + worldAABB.max = glm::max(worldAABB.max, corners[i]); + } + } + void UpdateAABBs::Update(entt::registry& registry, f32 deltaTime) { ZoneScopedN("ECS::UpdateAABBs"); - auto view = registry.view(); - view.each([&](entt::entity entity, Components::Transform& transform, Components::AABB& aabb, Components::WorldAABB& worldAABB, ECS::Components::DirtyTransform& dirtyTransform) + // Update AABBs for entities with dirty transforms + auto dirtyTransformView = registry.view(); + dirtyTransformView.each([&](entt::entity entity, Components::Transform& transform, Components::AABB& aabb, Components::WorldAABB& worldAABB, ECS::Components::DirtyTransform& dirtyTransform) + { + UpdateWorldAABB(transform, aabb, worldAABB); + }); + + // Update AABBs for entities with dirty AABBs + auto dirtyAABBView = registry.view(); + dirtyAABBView.each([&](entt::entity entity, Components::Transform& transform, Components::AABB& aabb, Components::WorldAABB& worldAABB) { - // Calculate the world AABB - glm::vec3 min = aabb.centerPos - aabb.extents; - glm::vec3 max = aabb.centerPos + aabb.extents; - - glm::vec3 corners[8] = { - glm::vec3(min.x, min.y, min.z), - glm::vec3(min.x, min.y, max.z), - glm::vec3(min.x, max.y, min.z), - glm::vec3(min.x, max.y, max.z), - glm::vec3(max.x, min.y, min.z), - glm::vec3(max.x, min.y, max.z), - glm::vec3(max.x, max.y, min.z), - glm::vec3(max.x, max.y, max.z) - }; - - const mat4x4 transformMatrix = transform.GetMatrix(); - for (i32 i = 0; i < 8; ++i) - { - corners[i] = transformMatrix * glm::vec4(corners[i], 1.0f); - } - - worldAABB.min = vec3(1000000000.0f); - worldAABB.max = vec3(-1000000000.0f); - - for (i32 i = 1; i < 8; ++i) - { - worldAABB.min = glm::min(worldAABB.min, corners[i]); - worldAABB.max = glm::max(worldAABB.max, corners[i]); - } + UpdateWorldAABB(transform, aabb, worldAABB); }); } } \ No newline at end of file diff --git a/Source/Game-Lib/Game-Lib/Editor/Inspector.cpp b/Source/Game-Lib/Game-Lib/Editor/Inspector.cpp index e43f5012..08b24975 100644 --- a/Source/Game-Lib/Game-Lib/Editor/Inspector.cpp +++ b/Source/Game-Lib/Game-Lib/Editor/Inspector.cpp @@ -1,20 +1,12 @@ #include "Inspector.h" -#include -#include #include -#include -#include -#include -#include -#include -#include -#include #include #include #include #include #include +#include #include #include #include @@ -23,6 +15,15 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include @@ -322,8 +323,12 @@ namespace Editor InspectEntityTransform(entity); + InspectEntityAABB(entity); + InspectEntityModel(entity); + InspectEntityDecal(entity); + if (ECS::Components::Unit* unit = registry->try_get(entity)) { ImGui::Text("Body ID: %d", unit->bodyID); @@ -521,6 +526,119 @@ namespace Editor ImGui::PopStyleColor(); } + void Inspector::InspectEntityAABB(entt::entity entity) + { + entt::registry* registry = ServiceLocator::GetEnttRegistries()->gameRegistry; + + ECS::Components::AABB* aabb = registry->try_get(entity); + + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.4f, 0.4f, 0.4f, 1.0f)); + + if (aabb) + { + if (Util::Imgui::BeginGroupPanel("AABB")) + { + bool isDirty = false; + + // Center Pos + vec3 centerPos = aabb->centerPos; + if (ImGui::DragFloat3("center pos", ¢erPos.x)) + { + aabb->centerPos = centerPos; + isDirty = true; + } + + // Extents + vec3 extents = aabb->extents; + if (ImGui::DragFloat3("extents", &extents.x)) + { + aabb->extents = extents; + isDirty = true; + } + + if (isDirty) + { + registry->emplace_or_replace(entity); + } + } + Util::Imgui::EndGroupPanel(); + } + ImGui::PopStyleColor(); + } + + void Inspector::InspectEntityDecal(entt::entity entity) + { + entt::registry* registry = ServiceLocator::GetEnttRegistries()->gameRegistry; + + ECS::Components::Decal* decal = registry->try_get(entity); + + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.4f, 0.4f, 0.4f, 1.0f)); + + if (decal) + { + if (Util::Imgui::BeginGroupPanel("Decal")) + { + bool isDirty = false; + + std::string texturePath = decal->texturePath; + if (ImGui::InputText("Texture Path", &texturePath)) + { + decal->texturePath = texturePath; + isDirty = true; + } + + Color color = decal->colorMultiplier; + if (ImGui::ColorEdit3("Color Multiplier", &color.r)) + { + decal->colorMultiplier = color; + isDirty = true; + } + + f32 thresholdMin = f32(decal->thresholdMinMax.x); + if (ImGui::SliderFloat("Threshold Min", &thresholdMin, -1.0f, 1.0f)) + { + decal->thresholdMinMax.x = f16(thresholdMin); + isDirty = true; + } + + f32 thresholdMax = f32(decal->thresholdMinMax.y); + if (ImGui::SliderFloat("Threshold Max", &thresholdMax, -1.0f, 1.0f)) + { + decal->thresholdMinMax.y = f16(thresholdMax); + isDirty = true; + } + + vec2 minUV = vec2(decal->minUV); + if (ImGui::DragFloat2("Min UV", &minUV.x, 0.01f, 0.0f, 1.0f)) + { + decal->minUV = hvec2(minUV); + isDirty = true; + } + + vec2 maxUV = vec2(decal->maxUV); + if (ImGui::DragFloat2("Max UV", &maxUV.x, 0.01f, 0.0f, 1.0f)) + { + decal->maxUV = hvec2(maxUV); + isDirty = true; + } + + u32 flags = decal->flags; + if (ImGui::InputScalar("Flags", ImGuiDataType_U32, &flags, nullptr, nullptr, "%u", ImGuiInputTextFlags_CharsHexadecimal)) + { + decal->flags = flags; + isDirty = true; + } + + if (isDirty) + { + registry->emplace_or_replace(entity); + } + } + Util::Imgui::EndGroupPanel(); + } + ImGui::PopStyleColor(); + } + bool Inspector::DrawGizmo(entt::registry* registry, entt::entity entity, ECS::Components::Transform& transform) { entt::registry::context& ctx = registry->ctx(); diff --git a/Source/Game-Lib/Game-Lib/Editor/Inspector.h b/Source/Game-Lib/Game-Lib/Editor/Inspector.h index db4073d4..7ac976f9 100644 --- a/Source/Game-Lib/Game-Lib/Editor/Inspector.h +++ b/Source/Game-Lib/Game-Lib/Editor/Inspector.h @@ -51,6 +51,8 @@ namespace Editor void InspectEntity(entt::entity entity); void InspectEntityTransform(entt::entity entity); void InspectEntityModel(entt::entity entity); + void InspectEntityAABB(entt::entity entity); + void InspectEntityDecal(entt::entity entity); bool DrawGizmo(entt::registry* registry, entt::entity entity, ECS::Components::Transform& transform); void DrawGizmoControls(); diff --git a/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp index b9f77194..3be5a055 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp @@ -3,6 +3,7 @@ #include "UIRenderer.h" #include "Debug/DebugRenderer.h" #include "Debug/JoltDebugRenderer.h" +#include "Light/LightRenderer.h" #include "Terrain/TerrainRenderer.h" #include "Terrain/TerrainLoader.h" #include "Terrain/TerrainManipulator.h" @@ -145,7 +146,8 @@ GameRenderer::GameRenderer(InputManager* inputManager) _joltDebugRenderer = new JoltDebugRenderer(_renderer, _debugRenderer); _modelRenderer = new ModelRenderer(_renderer, _debugRenderer); - _modelLoader = new ModelLoader(_modelRenderer); + _lightRenderer = new LightRenderer(_renderer, _debugRenderer, _modelRenderer); + _modelLoader = new ModelLoader(_modelRenderer, _lightRenderer); _modelLoader->Init(); _liquidRenderer = new LiquidRenderer(_renderer, _debugRenderer); @@ -159,8 +161,8 @@ GameRenderer::GameRenderer(InputManager* inputManager) _modelLoader->SetTerrainLoader(_terrainLoader); _mapLoader = new MapLoader(_terrainLoader, _modelLoader, _liquidLoader); - - _materialRenderer = new MaterialRenderer(_renderer, _terrainRenderer, _modelRenderer); + + _materialRenderer = new MaterialRenderer(_renderer, _terrainRenderer, _modelRenderer, _lightRenderer); _skyboxRenderer = new SkyboxRenderer(_renderer, _debugRenderer); _editorRenderer = new EditorRenderer(_renderer, _debugRenderer); _canvasRenderer = new CanvasRenderer(_renderer, _debugRenderer); @@ -206,6 +208,7 @@ void GameRenderer::UpdateRenderers(f32 deltaTime) _materialRenderer->Update(deltaTime); _joltDebugRenderer->Update(deltaTime); _debugRenderer->Update(deltaTime); + _lightRenderer->Update(deltaTime); _pixelQuery->Update(deltaTime); _editorRenderer->Update(deltaTime); _canvasRenderer->Update(deltaTime); @@ -383,6 +386,8 @@ f32 GameRenderer::Render() _liquidRenderer->AddCullingPass(&renderGraph, _resources, _frameIndex); _liquidRenderer->AddGeometryPass(&renderGraph, _resources, _frameIndex); + _lightRenderer->AddClassificationPass(&renderGraph, _resources, _frameIndex); + _materialRenderer->AddPreEffectsPass(&renderGraph, _resources, _frameIndex); _effectRenderer->AddSSAOPass(&renderGraph, _resources, _frameIndex); @@ -396,6 +401,8 @@ f32 GameRenderer::Render() _canvasRenderer->AddCanvasPass(&renderGraph, _resources, _frameIndex); _debugRenderer->Add2DPass(&renderGraph, _resources, _frameIndex); + _lightRenderer->AddDebugPass(&renderGraph, _resources, _frameIndex); + Renderer::ImageID finalTarget = isEditorMode ? _resources.finalColor : _resources.sceneColor; _uiRenderer->AddImguiPass(&renderGraph, _resources, _frameIndex, finalTarget); @@ -532,6 +539,17 @@ void GameRenderer::CreatePermanentResources() sceneColorDesc.clearColor = Color(0.43f, 0.50f, 0.56f, 1.0f); // Slate gray _resources.finalColor = _renderer->CreateImage(sceneColorDesc); + // Debug rendertarget + Renderer::ImageDesc debugColorDesc; + debugColorDesc.debugName = "DebugColor"; + debugColorDesc.dimensions = vec2(1.0f, 1.0f); + debugColorDesc.dimensionType = Renderer::ImageDimensionType::DIMENSION_SCALE_RENDERSIZE; + debugColorDesc.format = _renderer->GetSwapChainImageFormat(); + debugColorDesc.sampleCount = Renderer::SampleCount::SAMPLE_COUNT_1; + debugColorDesc.clearColor = Color::Clear; + + _resources.debugColor = _renderer->CreateImage(debugColorDesc); + // SSAO Renderer::ImageDesc ssaoTargetDesc; ssaoTargetDesc.debugName = "SSAOTarget"; diff --git a/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.h index a00b3066..7f3e63fa 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.h @@ -32,6 +32,7 @@ class MaterialRenderer; class SkyboxRenderer; class EditorRenderer; class DebugRenderer; +class LightRenderer; class JoltDebugRenderer; class CanvasRenderer; class LiquidLoader; @@ -134,6 +135,7 @@ class GameRenderer MaterialRenderer* _materialRenderer = nullptr; SkyboxRenderer* _skyboxRenderer = nullptr; DebugRenderer* _debugRenderer = nullptr; + LightRenderer* _lightRenderer = nullptr; JoltDebugRenderer* _joltDebugRenderer = nullptr; EditorRenderer* _editorRenderer = nullptr; CanvasRenderer* _canvasRenderer = nullptr; diff --git a/Source/Game-Lib/Game-Lib/Rendering/Light/LightRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Light/LightRenderer.cpp new file mode 100644 index 00000000..0ce813db --- /dev/null +++ b/Source/Game-Lib/Game-Lib/Rendering/Light/LightRenderer.cpp @@ -0,0 +1,438 @@ +#include "LightRenderer.h" + +#include "Game-Lib/Application/EnttRegistries.h" +#include "Game-Lib/ECS/Components/AABB.h" +#include "Game-Lib/ECS/Components/Decal.h" +#include "Game-Lib/ECS/Components/Events.h" +#include "Game-Lib/ECS/Components/Name.h" +#include "Game-Lib/ECS/Util/EventUtil.h" +#include "Game-Lib/ECS/Util/Transforms.h" +#include "Game-Lib/Editor/EditorHandler.h" +#include "Game-Lib/Editor/TerrainTools.h" +#include "Game-Lib/Rendering/Debug/DebugRenderer.h" +#include "Game-Lib/Rendering/Model/ModelRenderer.h" +#include "Game-Lib/Rendering/RenderResources.h" +#include "Game-Lib/Rendering/RenderUtils.h" +#include "Game-Lib/Rendering/Terrain/TerrainRenderer.h" +#include "Game-Lib/Util/PhysicsUtil.h" +#include "Game-Lib/Util/ServiceLocator.h" + +#include + +#include +#include + +#include + +AutoCVar_ShowFlag CVAR_DebugLightTiles(CVarCategory::Client | CVarCategory::Rendering, "debugLightTiles", "Debug draw light tiles", ShowFlag::DISABLED); + +LightRenderer::LightRenderer(Renderer::Renderer* renderer, DebugRenderer* debugRenderer, ModelRenderer* modelRenderer) + : _renderer(renderer) + , _debugRenderer(debugRenderer) + , _modelRenderer(modelRenderer) +{ + CreatePermanentResources(); +} + +LightRenderer::~LightRenderer() +{ + +} + +void LightRenderer::Update(f32 deltaTime) +{ + ZoneScoped; + + entt::registry* registry = ServiceLocator::GetEnttRegistries()->gameRegistry; + + // Handle remove requests + u32 numDecalRemovals = static_cast(_decalRemoveRequests.try_dequeue_bulk(_decalRemoveWork.begin(), 64)); + if (numDecalRemovals > 0) + { + for (u32 i = 0; i < numDecalRemovals; i++) + { + DecalRemoveRequest& request = _decalRemoveWork[i]; + auto it = _entityToDecalID.find(request.entity); + if (it == _entityToDecalID.end()) + { + continue; + } + + u32 decalID = it->second; + _decals.Remove(decalID); + _decalIDToEntity.erase(decalID); + _entityToDecalID.erase(it); + } + } + + // Update existing decals if dirty transform + auto dirtyDecalTransformView = registry->view(); + dirtyDecalTransformView.each([this, ®istry](entt::entity entity, ECS::Components::Transform& transform, ECS::Components::AABB& aabb, ECS::Components::Decal& decalComp, ECS::Components::DirtyTransform& dirtyTransform) + { + auto it = _entityToDecalID.find(entity); + if (it == _entityToDecalID.end()) + { + // New decal we didn't know about, should we maybe just register it? For now it requires explicit AddDecal call + return; + } + + u32 decalID = it->second; + GPUDecal& decal = _decals[decalID]; + + vec3 position = transform.GetWorldPosition() + aabb.centerPos; + decal.positionAndTextureID = vec4(position, decal.positionAndTextureID.w); + decal.rotation = transform.GetWorldRotation(); + + _decals.SetDirtyElement(decalID); + }); + + // Update existing decals if dirty AABB + auto dirtyDecalAABBView = registry->view(); + dirtyDecalAABBView.each([this, ®istry](entt::entity entity, ECS::Components::Transform& transform, ECS::Components::AABB& aabb, ECS::Components::Decal& decalComp) + { + auto it = _entityToDecalID.find(entity); + if (it == _entityToDecalID.end()) + { + // New decal we didn't know about, should we maybe just register it? For now it requires explicit AddDecal call + return; + } + + u32 decalID = it->second; + GPUDecal& decal = _decals[decalID]; + + vec3 position = transform.GetWorldPosition() + aabb.centerPos; + decal.positionAndTextureID = vec4(position, decal.positionAndTextureID.w); + decal.extentsAndColor = vec4(aabb.extents, decal.extentsAndColor.w); + + _decals.SetDirtyElement(decalID); + }); + + // Update existing decals if dirty decal settings + auto dirtyDecalView = registry->view(); + dirtyDecalView.each([this, ®istry](entt::entity entity, ECS::Components::Transform& transform, ECS::Components::AABB& aabb, ECS::Components::Decal& decalComp) + { + auto it = _entityToDecalID.find(entity); + if (it == _entityToDecalID.end()) + { + // New decal we didn't know about, should we maybe just register it? For now it requires explicit AddDecal call + return; + } + + u32 decalID = it->second; + GPUDecal& decal = _decals[decalID]; + + // Load the texture into the model renderer (bigger chance for texture reuse than having our separate texture array) + _modelRenderer->LoadTexture(decalComp.texturePath, *reinterpret_cast(&decal.positionAndTextureID.w)); + + u32 colorInt = decalComp.colorMultiplier.ToABGR32(); + + decal.extentsAndColor.w = *reinterpret_cast(&colorInt); + decal.thresholdMinMax = decalComp.thresholdMinMax; + decal.minUV = decalComp.minUV; + decal.maxUV = decalComp.maxUV; + decal.flags = decalComp.flags; + + _decals.SetDirtyElement(decalID); + }); + registry->clear(); + + // Handle add requests + u32 numDecalAdds = static_cast(_decalAddRequests.try_dequeue_bulk(_decalAddWork.begin(), 64)); + if (numDecalAdds > 0) + { + for (u32 i = 0; i < numDecalAdds; i++) + { + DecalAddRequest& request = _decalAddWork[i]; + + ECS::Components::Transform& transform = registry->get(request.entity); + ECS::Components::AABB& aabb = registry->get(request.entity); + ECS::Components::Decal& decalComp = registry->get(request.entity); + + // Load the texture into the model renderer (bigger chance for texture reuse than having our separate texture array) + u32 textureID; + _modelRenderer->LoadTexture(decalComp.texturePath, textureID); + + // Add decal + vec3 position = transform.GetWorldPosition() + aabb.centerPos; + quat rotation = transform.GetWorldRotation(); + + u32 colorInt = decalComp.colorMultiplier.ToABGR32(); + + GPUDecal decal + { + .positionAndTextureID = vec4(position, *reinterpret_cast(&textureID)), + .rotation = rotation, + .extentsAndColor = vec4(aabb.extents, *reinterpret_cast(&colorInt)), + .thresholdMinMax = decalComp.thresholdMinMax, + .minUV = decalComp.minUV, + .maxUV = decalComp.maxUV, + .flags = decalComp.flags, + }; + + u32 decalID = _decals.Add(decal); + _decalIDToEntity[decalID] = request.entity; + _entityToDecalID[request.entity] = decalID; + } + } + + SyncToGPU(); + + // Debug draw decals as wireframes + if (CVAR_DebugLightTiles.Get() == ShowFlag::ENABLED) + { + for (u32 i = 0; i < _decals.Count(); i++) + { + const GPUDecal& decal = _decals[i]; + + vec3 min = vec3(decal.positionAndTextureID) - vec3(decal.extentsAndColor); + vec3 max = vec3(decal.positionAndTextureID) + vec3(decal.extentsAndColor); + + _debugRenderer->DrawOBB3D(vec3(decal.positionAndTextureID), vec3(decal.extentsAndColor), decal.rotation, Color::Cyan); + } + } + + /* + ECS::Util::EventUtil::OnEvent([&](const ECS::Components::MapLoadedEvent& event) + { + entt::registry* registry = ServiceLocator::GetEnttRegistries()->gameRegistry; + + entt::entity decalEntity = registry->create(); + + ECS::TransformSystem& ts = ECS::TransformSystem::Get(*registry); + + registry->emplace(decalEntity, "Test Decal"); + + registry->emplace(decalEntity); + ts.SetWorldPosition(decalEntity, vec3(-1414.81f, 343.78f, 1012.89f)); + ts.SetWorldRotation(decalEntity, quat(vec3(glm::radians(90.0f), 0.0f, 0.0f))); + registry->emplace(decalEntity, vec3(0.0f), vec3(5.0f, 5.0f, 10.0f)); + + auto& decal = registry->emplace(decalEntity); + decal.texturePath = "Data/Texture/interface/spellshadow/spell-shadow-acceptable.dds"; + + registry->emplace(decalEntity); + + AddDecal(decalEntity); + });*/ +} + +void LightRenderer::Clear() +{ + DecalAddRequest decalAddRequest; + while (_decalAddRequests.try_dequeue(decalAddRequest)) {} + + DecalRemoveRequest decalRemoveRequest; + while (_decalRemoveRequests.try_dequeue(decalRemoveRequest)) {} + + _decals.Clear(); + _decalIDToEntity.clear(); + _entityToDecalID.clear(); +} + +void LightRenderer::AddClassificationPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) +{ + struct ClassificationPassData + { + Renderer::BufferMutableResource entityTilesBuffer; + + Renderer::DepthImageResource depth; + + Renderer::ImageMutableResource debugColor; + + Renderer::DescriptorSetResource debugSet; + Renderer::DescriptorSetResource globalSet; + Renderer::DescriptorSetResource classifySet; + }; + + renderGraph->AddPass("Light Classification", + [this, &resources](ClassificationPassData& data, Renderer::RenderGraphBuilder& builder) // Setup + { + data.entityTilesBuffer = builder.Write(_entityTilesBuffer, Renderer::BufferPassUsage::COMPUTE | Renderer::BufferPassUsage::TRANSFER); + + data.depth = builder.Read(resources.depth, Renderer::PipelineType::COMPUTE); + + data.debugColor = builder.Write(resources.debugColor, Renderer::PipelineType::COMPUTE, Renderer::LoadMode::CLEAR); + + builder.Read(resources.cameras.GetBuffer(), Renderer::BufferPassUsage::COMPUTE); + builder.Read(_decals.GetBuffer(), Renderer::BufferPassUsage::COMPUTE); + + _debugRenderer->RegisterCullingPassBufferUsage(builder); + + data.debugSet = builder.Use(_debugRenderer->GetDebugDescriptorSet()); + data.globalSet = builder.Use(resources.globalDescriptorSet); + data.classifySet = builder.Use(_classifyPassDescriptorSet); + + return true; // Return true from setup to enable this pass, return false to disable it + }, + [this, &resources, frameIndex](ClassificationPassData& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) // Execute + { + GPU_SCOPED_PROFILER_ZONE(commandList, LightClassificationPass); + + Renderer::ComputePipelineDesc pipelineDesc; + graphResources.InitializePipelineDesc(pipelineDesc); + + Renderer::ComputeShaderDesc shaderDesc; + shaderDesc.path = "Light/Classification.cs.hlsl"; + pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc); + + Renderer::ComputePipelineID pipeline = _renderer->CreatePipeline(pipelineDesc); + commandList.BeginPipeline(pipeline); + + uvec2 outputSize = _renderer->GetImageDimensions(resources.sceneColor, 0); + u32 numTilesX = (outputSize.x + TILE_SIZE - 1) / TILE_SIZE; + u32 numTilesY = (outputSize.y + TILE_SIZE - 1) / TILE_SIZE; + uvec2 tileCount = uvec2(numTilesX, numTilesY); + + struct Constants + { + u32 maxEntitiesPerTile; + u32 numTotalDecals; + uvec2 tileCount; + vec2 screenSize; + }; + + Constants* constants = graphResources.FrameNew(); + constants->maxEntitiesPerTile = MAX_ENTITIES_PER_TILE; + constants->numTotalDecals = static_cast(_decals.Count()); + constants->tileCount = tileCount; + constants->screenSize = static_cast(outputSize); + + commandList.PushConstant(constants, 0, sizeof(Constants)); + + data.classifySet.Bind("_depthRT", data.depth); + data.classifySet.BindStorage("_debugTexture", data.debugColor); + + // Bind descriptorset + commandList.BindDescriptorSet(Renderer::DescriptorSetSlot::DEBUG, data.debugSet, frameIndex); + commandList.BindDescriptorSet(Renderer::DescriptorSetSlot::GLOBAL, data.globalSet, frameIndex); + commandList.BindDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS, data.classifySet, frameIndex); + + commandList.Dispatch(numTilesX, numTilesY, 1); + + commandList.EndPipeline(pipeline); + }); +} + +void LightRenderer::AddDebugPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) +{ + if (CVAR_DebugLightTiles.Get() == ShowFlag::DISABLED) + return; + + struct DebugPassData + { + Renderer::ImageMutableResource color; + Renderer::ImageResource debug; + + Renderer::DescriptorSetResource globalSet; + Renderer::DescriptorSetResource debugSet; + }; + renderGraph->AddPass("Light Debug Overlay", + [this, &resources](DebugPassData& data, Renderer::RenderGraphBuilder& builder) // Setup + { + data.color = builder.Write(resources.sceneColor, Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); + data.debug = builder.Read(resources.debugColor, Renderer::PipelineType::GRAPHICS); + + builder.Read(resources.cameras.GetBuffer(), Renderer::BufferPassUsage::GRAPHICS); + + data.globalSet = builder.Use(resources.globalDescriptorSet); + data.debugSet = builder.Use(_debugPassDescriptorSet); + + return true;// Return true from setup to enable this pass, return false to disable it + }, + [this, frameIndex, &resources](DebugPassData& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) // Execute + { + GPU_SCOPED_PROFILER_ZONE(commandList, DecalDebugOverlay); + + RenderUtils::OverlayParams overlayParams; + overlayParams.baseImage = data.color; + overlayParams.overlayImage = data.debug; + overlayParams.descriptorSet = data.debugSet; + + RenderUtils::Overlay(_renderer, graphResources, commandList, frameIndex, overlayParams); + }); +} + +void LightRenderer::AddDecal(entt::entity entity) +{ + // Make sure entity has required components + entt::registry* registry = ServiceLocator::GetEnttRegistries()->gameRegistry; + if (!registry->all_of(entity)) + { + return; + } + + DecalAddRequest request + { + .entity = entity, + }; + _decalAddRequests.enqueue(request); +} + +void LightRenderer::RemoveDecal(entt::entity entity) +{ + DecalRemoveRequest request + { + .entity = entity, + }; + _decalRemoveRequests.enqueue(request); +} + +void LightRenderer::CreatePermanentResources() +{ + _decals.SetDebugName("Decals"); + _decals.SetUsage(Renderer::BufferUsage::STORAGE_BUFFER); + + _renderer->AddOnRenderSizeChanged([this](const vec2& newSize) + { + RecreateBuffer(newSize); + }); + + _decalAddWork.resize(64); + _decalRemoveWork.resize(64); +} + +u32 LightRenderer::CalculateNumTiles(const vec2& size) +{ + uvec2 numTiles2D = CalculateNumTiles2D(size); + return numTiles2D.x * numTiles2D.y; +} + +uvec2 LightRenderer::CalculateNumTiles2D(const vec2& size) +{ + u32 width = static_cast(size.x); + u32 height = static_cast(size.y); + + u32 numTilesX = (width + TILE_SIZE - 1) / TILE_SIZE; + u32 numTilesY = (height + TILE_SIZE - 1) / TILE_SIZE; + return uvec2(numTilesX, numTilesY); +} + +void LightRenderer::RegisterMaterialPassBufferUsage(Renderer::RenderGraphBuilder& builder) +{ + builder.Read(_entityTilesBuffer, Renderer::BufferPassUsage::COMPUTE); + builder.Read(_decals.GetBuffer(), Renderer::BufferPassUsage::COMPUTE); +} + +void LightRenderer::RecreateBuffer(const vec2& size) +{ + u32 numTiles = CalculateNumTiles(size); + + Renderer::BufferDesc bufferDesc; + bufferDesc.name = "Entity Tiles"; + bufferDesc.size = numTiles * sizeof(u32) * MAX_ENTITIES_PER_TILE * 2; // *2: opaque and transparent arrays + bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_DESTINATION; + bufferDesc.cpuAccess = Renderer::BufferCPUAccess::AccessNone; + + _entityTilesBuffer = _renderer->CreateBuffer(_entityTilesBuffer, bufferDesc); + _classifyPassDescriptorSet.Bind("_entityTiles", _entityTilesBuffer); + _materialPassDescriptorSet.Bind("_entityTiles", _entityTilesBuffer); +} + +void LightRenderer::SyncToGPU() +{ + if (_decals.SyncToGPU(_renderer)) + { + _classifyPassDescriptorSet.Bind("_packedDecals", _decals.GetBuffer()); + _materialPassDescriptorSet.Bind("_packedDecals", _decals.GetBuffer()); + } +} \ No newline at end of file diff --git a/Source/Game-Lib/Game-Lib/Rendering/Light/LightRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Light/LightRenderer.h new file mode 100644 index 00000000..3bee256c --- /dev/null +++ b/Source/Game-Lib/Game-Lib/Rendering/Light/LightRenderer.h @@ -0,0 +1,107 @@ +#pragma once +#include + +#include +#include +#include + +#include + +#include +#include + +namespace Renderer +{ + class Renderer; + class RenderGraph; + +} + +class DebugRenderer; +class ModelRenderer; +struct RenderResources; + +class LightRenderer +{ + static constexpr u32 MAX_ENTITIES_PER_TILE = 8; + static constexpr u32 TILE_SIZE = 16; // in pixels, per axis (16 = 16x16 = 256 pixels per tile) + +public: + LightRenderer(Renderer::Renderer* renderer, DebugRenderer* debugRenderer, ModelRenderer* modelRenderer); + ~LightRenderer(); + + void Update(f32 deltaTime); + void Clear(); + + void AddClassificationPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); + void AddDebugPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); + + // Requires Transform, AABB and Decal components on entity + void AddDecal(entt::entity entity); + void RemoveDecal(entt::entity entity); + + inline u32 CalculateNumTiles(const vec2& size); + inline uvec2 CalculateNumTiles2D(const vec2& size); + + Renderer::DescriptorSet& GetTileDescriptorSet() { return _materialPassDescriptorSet; } + void RegisterMaterialPassBufferUsage(Renderer::RenderGraphBuilder& builder); + +private: + void CreatePermanentResources(); + + void RecreateBuffer(const vec2& size); + + void SyncToGPU(); + +public: + enum DecalFlags + { + DECAL_FLAG_TWOSIDED = 1 << 0, + }; + + struct DecalAddRequest + { + entt::entity entity; + }; + + struct DecalRemoveRequest + { + entt::entity entity; + }; + +private: + struct GPUDecal + { + vec4 positionAndTextureID; // xyz = position, w = texture index + quat rotation; + vec4 extentsAndColor; // xyz = extents, asuint(w) = uint color multiplier + hvec2 thresholdMinMax = hvec2(0.0f, 1.0f); + hvec2 minUV = hvec2(0.0f, 0.0f); + hvec2 maxUV = hvec2(1.0f, 1.0f); + u32 flags; + }; + +private: + Renderer::Renderer* _renderer; + + Renderer::DescriptorSet _classifyPassDescriptorSet; + Renderer::DescriptorSet _debugPassDescriptorSet; + Renderer::DescriptorSet _materialPassDescriptorSet; + + Renderer::GPUVector _decals; + robin_hood::unordered_map _decalIDToEntity; + robin_hood::unordered_map _entityToDecalID; + + Renderer::BufferID _entityTilesBuffer; + + Renderer::GraphicsPipelineID _debugPipeline; + + moodycamel::ConcurrentQueue _decalAddRequests; + std::vector _decalAddWork; + + moodycamel::ConcurrentQueue _decalRemoveRequests; + std::vector _decalRemoveWork; + + DebugRenderer* _debugRenderer = nullptr; + ModelRenderer* _modelRenderer = nullptr; +}; diff --git a/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp index 490a7f4d..cf175291 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp @@ -4,6 +4,7 @@ #include "Game-Lib/Editor/EditorHandler.h" #include "Game-Lib/Editor/TerrainTools.h" #include "Game-Lib/Rendering/Debug/DebugRenderer.h" +#include "Game-Lib/Rendering/Light/LightRenderer.h" #include "Game-Lib/Rendering/Model/ModelRenderer.h" #include "Game-Lib/Rendering/RenderResources.h" #include "Game-Lib/Rendering/Terrain/TerrainRenderer.h" @@ -24,10 +25,11 @@ AutoCVar_VecFloat CVAR_FogColor(CVarCategory::Client | CVarCategory::Rendering, AutoCVar_Float CVAR_FogBeginDist(CVarCategory::Client | CVarCategory::Rendering, "fogBlendBegin", "Fog blending start distance", 200.0f, CVarFlags::EditFloatDrag); AutoCVar_Float CVAR_FogEndDist(CVarCategory::Client | CVarCategory::Rendering, "fogBlendEnd", "Fog blending end distance", 600.0f, CVarFlags::EditFloatDrag); -MaterialRenderer::MaterialRenderer(Renderer::Renderer* renderer, TerrainRenderer* terrainRenderer, ModelRenderer* modelRenderer) +MaterialRenderer::MaterialRenderer(Renderer::Renderer* renderer, TerrainRenderer* terrainRenderer, ModelRenderer* modelRenderer, LightRenderer* lightRenderer) : _renderer(renderer) , _terrainRenderer(terrainRenderer) , _modelRenderer(modelRenderer) + , _lightRenderer(lightRenderer) { CreatePermanentResources(); } @@ -132,7 +134,8 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende Renderer::ImageResource ambientOcclusion; Renderer::DescriptorSetResource globalSet; - Renderer::DescriptorSetResource shadowSet; + Renderer::DescriptorSetResource tilesSet; + Renderer::DescriptorSetResource lightSet; Renderer::DescriptorSetResource materialSet; Renderer::DescriptorSetResource terrainSet; Renderer::DescriptorSetResource modelSet; @@ -155,17 +158,20 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende builder.Read(resources.cameras.GetBuffer(), Renderer::BufferPassUsage::COMPUTE); builder.Read(_directionalLights.GetBuffer(), Renderer::BufferPassUsage::COMPUTE); + Renderer::DescriptorSet& tileDescriptorSet = _lightRenderer->GetTileDescriptorSet(); Renderer::DescriptorSet& terrainDescriptorSet = _terrainRenderer->GetMaterialPassDescriptorSet(); Renderer::DescriptorSet& modelDescriptorSet = _modelRenderer->GetMaterialPassDescriptorSet(); data.globalSet = builder.Use(resources.globalDescriptorSet); - data.shadowSet = builder.Use(resources.shadowDescriptorSet); + data.tilesSet = builder.Use(tileDescriptorSet); + data.lightSet = builder.Use(resources.lightDescriptorSet); data.materialSet = builder.Use(_materialPassDescriptorSet); data.terrainSet = builder.Use(terrainDescriptorSet); data.modelSet = builder.Use(modelDescriptorSet); _terrainRenderer->RegisterMaterialPassBufferUsage(builder); _modelRenderer->RegisterMaterialPassBufferUsage(builder); + _lightRenderer->RegisterMaterialPassBufferUsage(builder); return true; // Return true from setup to enable this pass, return false to disable it }, @@ -201,7 +207,8 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende // Bind descriptorset commandList.BindDescriptorSet(Renderer::DescriptorSetSlot::GLOBAL, data.globalSet, frameIndex); - commandList.BindDescriptorSet(Renderer::DescriptorSetSlot::SHADOWS, data.shadowSet, frameIndex); + commandList.BindDescriptorSet(Renderer::DescriptorSetSlot::TILES, data.tilesSet, frameIndex); + commandList.BindDescriptorSet(Renderer::DescriptorSetSlot::LIGHT, data.lightSet, frameIndex); commandList.BindDescriptorSet(Renderer::DescriptorSetSlot::TERRAIN, data.terrainSet, frameIndex); commandList.BindDescriptorSet(Renderer::DescriptorSetSlot::MODEL, data.modelSet, frameIndex); commandList.BindDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS, data.materialSet, frameIndex); @@ -217,6 +224,7 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende { vec4 renderInfo; // x = Render Width, y = Render Height, z = 1/Width, w = 1/Height uvec4 lightInfo; // x = Directional Light Count, Y = Point Light Count, Z = Cascade Count, W = Shadows Enabled + uvec4 tileInfo; // xy = Num Tiles, zw = UNUSED vec4 fogColor; vec4 fogSettings; // x = Enabled, y = Begin Fog Blend Dist, z = End Fog Blend Dist, w = UNUSED vec4 mouseWorldPos; @@ -238,6 +246,8 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende const u32 shadowEnabled = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled")); constants->lightInfo = uvec4(static_cast(_directionalLights.Count()), 0, numCascades, shadowEnabled); + constants->tileInfo = uvec4(_lightRenderer->CalculateNumTiles2D(outputSize), 0, 0); + constants->fogColor = CVAR_FogColor.Get(); constants->fogSettings.x = CVAR_EnableFog.Get() == ShowFlag::ENABLED; constants->fogSettings.y = CVAR_FogBeginDist.GetFloat(); diff --git a/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.h index 7c87583d..20d5eb6d 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.h @@ -14,12 +14,13 @@ namespace Renderer class TerrainRenderer; class ModelRenderer; +class LightRenderer; struct RenderResources; class MaterialRenderer { public: - MaterialRenderer(Renderer::Renderer* renderer, TerrainRenderer* terrainRenderer, ModelRenderer* modelRenderer); + MaterialRenderer(Renderer::Renderer* renderer, TerrainRenderer* terrainRenderer, ModelRenderer* modelRenderer, LightRenderer* lightRenderer); ~MaterialRenderer(); void Update(f32 deltaTime); @@ -58,4 +59,5 @@ class MaterialRenderer TerrainRenderer* _terrainRenderer = nullptr; ModelRenderer* _modelRenderer = nullptr; + LightRenderer* _lightRenderer = nullptr; }; diff --git a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelLoader.cpp b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelLoader.cpp index 311d71fc..bdf022d0 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelLoader.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelLoader.cpp @@ -16,6 +16,7 @@ #include "Game-Lib/Gameplay/MapLoader.h" #include "Game-Lib/Rendering/GameRenderer.h" #include "Game-Lib/Rendering/Debug/DebugRenderer.h" +#include "Game-Lib/Rendering/Light/LightRenderer.h" #include "Game-Lib/Rendering/Terrain/TerrainLoader.h" #include "Game-Lib/Util/JoltStream.h" #include "Game-Lib/Util/ServiceLocator.h" @@ -47,9 +48,10 @@ namespace fs = std::filesystem; static const fs::path dataPath = fs::path("Data/"); static const fs::path complexModelPath = dataPath / "ComplexModel"; -ModelLoader::ModelLoader(ModelRenderer* modelRenderer) +ModelLoader::ModelLoader(ModelRenderer* modelRenderer, LightRenderer* lightRenderer) : _terrainLoader(nullptr) , _modelRenderer(modelRenderer) + , _lightRenderer(lightRenderer) , _pendingLoadRequests(MAX_PENDING_LOADS_PER_FRAME) , _internalLoadRequests(MAX_INTERNAL_LOADS_PER_FRAME) , _discoveredModels() @@ -202,6 +204,7 @@ void ModelLoader::Clear() _numTerrainModelsToLoad = 0; _numTerrainModelsLoaded = 0; _modelRenderer->Clear(); + _lightRenderer->Clear(); auto& tSystem = ECS::TransformSystem::Get(*registry); tSystem.ProcessMovedEntities([](entt::entity entity) { }); diff --git a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelLoader.h b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelLoader.h index 35b3e7a5..b9c19e3f 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelLoader.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelLoader.h @@ -27,6 +27,7 @@ namespace ECS::Components } class TerrainLoader; +class LightRenderer; class ModelRenderer; class ModelLoader { @@ -102,7 +103,7 @@ class ModelLoader }; public: - ModelLoader(ModelRenderer* modelRenderer); + ModelLoader(ModelRenderer* modelRenderer, LightRenderer* lightRenderer); void Init(); void Clear(); @@ -171,6 +172,7 @@ class ModelLoader private: TerrainLoader* _terrainLoader = nullptr; ModelRenderer* _modelRenderer = nullptr; + LightRenderer* _lightRenderer = nullptr; std::atomic _numDiscoveredModelsToLoad = 0; u32 _numDiscoveredModelsLoaded = 0; diff --git a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp index 03c449bf..10fe56af 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp @@ -1186,6 +1186,18 @@ void ModelRenderer::Reserve(const ReserveInfo& reserveInfo) _transparentSkyboxCullingResources.Reserve(reserveInfo.numTransparentDrawcalls); } +Renderer::TextureID ModelRenderer::LoadTexture(const std::string& path, u32& arrayIndex) +{ + ZoneScopedN("ModelRenderer::LoadTexture"); + + Renderer::TextureDesc textureDesc = + { + .path = path, + }; + + return _renderer->LoadTextureIntoArray(textureDesc, _textures, arrayIndex); +} + u32 ModelRenderer::LoadModel(const std::string& name, Model::ComplexModel& model) { ZoneScopedN("ModelRenderer::LoadModel"); diff --git a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h index e8d54541..4ca2820d 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h @@ -315,6 +315,8 @@ class ModelRenderer : CulledRenderer void Reserve(const ReserveInfo& reserveInfo); + Renderer::TextureID LoadTexture(const std::string& path, u32& arrayIndex); + u32 LoadModel(const std::string& name, Model::ComplexModel& model); u32 AddPlacementInstance(entt::entity entityID, u32 modelID, u32 modelHash, Model::ComplexModel* model, const vec3& position, const quat& rotation, f32 scale, u32 doodadSet, bool canUseDoodadSet); u32 AddInstance(entt::entity entityID, u32 modelID, Model::ComplexModel* model, const mat4x4& transformMatrix, u64 displayInfoPacked = std::numeric_limits().max()); diff --git a/Source/Game-Lib/Game-Lib/Rendering/RenderResources.h b/Source/Game-Lib/Game-Lib/Rendering/RenderResources.h index c6ea513b..82e5bd05 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/RenderResources.h +++ b/Source/Game-Lib/Game-Lib/Rendering/RenderResources.h @@ -22,6 +22,8 @@ struct RenderResources Renderer::ImageID sceneColor; Renderer::ImageID finalColor; + Renderer::ImageID debugColor; + Renderer::ImageID ssaoTarget; Renderer::ImageID transparency; @@ -37,7 +39,7 @@ struct RenderResources Renderer::DepthImageID debugRendererDepth; Renderer::DescriptorSet globalDescriptorSet; - Renderer::DescriptorSet shadowDescriptorSet; + Renderer::DescriptorSet lightDescriptorSet; Renderer::SemaphoreID sceneRenderedSemaphore; // This semaphore tells the present function when the scene is ready to be blitted and presented FrameResource frameSyncSemaphores; // This semaphore makes sure the GPU handles frames in order diff --git a/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.h b/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.h index 22be6fa7..4c701df9 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.h +++ b/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.h @@ -22,9 +22,9 @@ class RenderUtils public: Renderer::ImageResource input; u32 inputMipLevel = 0; - vec4 colorMultiplier; - vec4 additiveColor; - ivec4 channelRedirectors; + vec4 colorMultiplier = vec4(1, 1, 1, 1); + vec4 additiveColor = vec4(0, 0, 0, 0); + ivec4 channelRedirectors = ivec4(0, 1, 2, 3); Renderer::ImageMutableResource output; @@ -36,9 +36,9 @@ class RenderUtils { public: Renderer::DepthImageResource input; - vec4 colorMultiplier; - vec4 additiveColor; - ivec4 channelRedirectors; + vec4 colorMultiplier = vec4(1, 1, 1, 1); + vec4 additiveColor = vec4(0, 0, 0, 0); + ivec4 channelRedirectors = ivec4(0, 1, 2, 3); Renderer::ImageMutableResource output; @@ -51,9 +51,9 @@ class RenderUtils public: Renderer::ImageResource overlayImage; u32 mipLevel = 0; - vec4 colorMultiplier; - vec4 additiveColor; - ivec4 channelRedirectors; + vec4 colorMultiplier = vec4(1,1,1,1); + vec4 additiveColor = vec4(0,0,0,0); + ivec4 channelRedirectors = ivec4(0, 1, 2, 3); Renderer::ImageMutableResource baseImage; @@ -65,9 +65,9 @@ class RenderUtils { public: Renderer::DepthImageResource overlayImage; - vec4 colorMultiplier; - vec4 additiveColor; - ivec4 channelRedirectors; + vec4 colorMultiplier = vec4(1, 1, 1, 1); + vec4 additiveColor = vec4(0, 0, 0, 0); + ivec4 channelRedirectors = ivec4(0, 1, 2, 3); Renderer::ImageMutableResource baseImage; @@ -80,9 +80,9 @@ class RenderUtils public: Renderer::ImageResource pipImage; u32 mipLevel = 0; - vec4 colorMultiplier; - vec4 additiveColor; - ivec4 channelRedirectors; + vec4 colorMultiplier = vec4(1, 1, 1, 1); + vec4 additiveColor = vec4(0, 0, 0, 0); + ivec4 channelRedirectors = ivec4(0, 1, 2, 3); Geometry::Box targetRegion; Renderer::ImageMutableResource baseImage; @@ -95,9 +95,9 @@ class RenderUtils { public: Renderer::DepthImageResource pipImage; - vec4 colorMultiplier; - vec4 additiveColor; - ivec4 channelRedirectors; + vec4 colorMultiplier = vec4(1, 1, 1, 1); + vec4 additiveColor = vec4(0, 0, 0, 0); + ivec4 channelRedirectors = ivec4(0, 1, 2, 3); Geometry::Box targetRegion; Renderer::ImageMutableResource baseImage; diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp index d50b0ba0..20e7eba1 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp @@ -112,7 +112,7 @@ void ShadowRenderer::AddShadowPass(Renderer::RenderGraph* renderGraph, RenderRes data.shadowDepthCascades[i] = builder.Write(resources.shadowDepthCascades[i], Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::CLEAR); } - data.shadowDescriptorSet = builder.Use(resources.shadowDescriptorSet); + data.shadowDescriptorSet = builder.Use(resources.lightDescriptorSet); return true; // Return true from setup to enable this pass, return false to disable it }, @@ -144,7 +144,7 @@ void ShadowRenderer::CreatePermanentResources(RenderResources& resources) samplerDesc.comparisonFunc = Renderer::ComparisonFunc::GREATER; _shadowCmpSampler = _renderer->CreateSampler(samplerDesc); - resources.shadowDescriptorSet.Bind("_shadowCmpSampler"_h, _shadowCmpSampler); + resources.lightDescriptorSet.Bind("_shadowCmpSampler"_h, _shadowCmpSampler); samplerDesc.filter = Renderer::SamplerFilter::MIN_MAG_MIP_POINT; samplerDesc.addressU = Renderer::TextureAddressMode::CLAMP; @@ -152,5 +152,5 @@ void ShadowRenderer::CreatePermanentResources(RenderResources& resources) samplerDesc.comparisonEnabled = false; _shadowPointClampSampler = _renderer->CreateSampler(samplerDesc); - resources.shadowDescriptorSet.Bind("_shadowPointClampSampler"_h, _shadowPointClampSampler); + resources.lightDescriptorSet.Bind("_shadowPointClampSampler"_h, _shadowPointClampSampler); } \ No newline at end of file diff --git a/Source/Shaders/Shaders/Include/Culling.inc.hlsl b/Source/Shaders/Shaders/Include/Culling.inc.hlsl index a9e785b5..c54a1373 100644 --- a/Source/Shaders/Shaders/Include/Culling.inc.hlsl +++ b/Source/Shaders/Shaders/Include/Culling.inc.hlsl @@ -1,7 +1,9 @@ #ifndef CULLING_INCLUDED #define CULLING_INCLUDED +#include "globalData.inc.hlsl" +#include "common.inc.hlsl" -struct AABB +struct AABBMinMax { float3 min; float3 max; @@ -10,4 +12,283 @@ struct AABB #define MAX_SHADOW_CASCADES 8 #define NUM_CULL_VIEWS 1 + MAX_SHADOW_CASCADES // Main view plus max number of cascades +// Based on the work of WickedEngine, thank you! https://github.com/turanszkij/WickedEngine/ +struct Plane +{ + float3 N; // Plane normal. + float d; // Distance to origin. +}; + +inline float PlaneEval(Plane p, float3 x) { return dot(p.N, x) - p.d; } + +// Flip plane so that `insidePoint` evaluates positive (inside = positive half-space) +inline Plane FaceInward(Plane p, float3 insidePoint) +{ + if (PlaneEval(p, insidePoint) < 0) { p.N *= -1.0f; p.d *= -1.0f; } + return p; +} + +// Compute a plane from 3 noncollinear points that form a triangle. +// This equation assumes a right-handed (counter-clockwise winding order) +// coordinate system to determine the direction of the plane normal. +Plane ComputePlane(float3 p0, float3 p1, float3 p2) +{ + Plane plane; + + float3 v0 = p1 - p0; + float3 v2 = p2 - p0; + + plane.N = normalize(cross(v0, v2)); + + // Compute the distance to the origin using p0. + plane.d = dot(plane.N, p0); + + return plane; +} + +// Force tile-frustum planes to face inward (+Z forward in view space). +inline Plane ForceInward(Plane p) +{ + // Flip if normal points opposite to +Z (we want camera at z=0, looking into +Z). + if (p.N.z < 0.0f) { p.N *= -1.0f; p.d *= -1.0f; } + return p; +} + +// Four planes of a view frustum (in view space). +// The planes are: +// * Left, +// * Right, +// * Top, +// * Bottom. +// The back and/or front planes can be computed from depth values in the +// light culling compute shader. +struct Frustum +{ + Plane planes[4]; // left, right, top, bottom frustum planes. +}; + +// Convert clip space coordinates to view space +float4 ClipToView(float4 clip) +{ + // View space position. + float4 view = mul(clip, _cameras[0].clipToView); + // Perspective projection. + view = view / view.w; + + return view; +} + +// Convert screen space coordinates to view space. +float4 ScreenToView(float4 screen, float2 dimRcp) +{ + // Convert to normalized texture coordinates + float2 texCoord = screen.xy * dimRcp; + + // Convert to clip space + float4 clip = float4(float2(texCoord.x, 1.0f - texCoord.y) * 2.0f - 1.0f, screen.z, screen.w); + + return ClipToView(clip); +} + +struct Sphere +{ + float3 c; // Center point. + float r; // Radius. +}; + +// Check to see if a sphere is fully behind (inside the negative halfspace of) a plane. +// Source: Real-time collision detection, Christer Ericson (2005) +bool SphereInsidePlane(Sphere sphere, Plane plane) +{ + return dot(plane.N, sphere.c) - plane.d < -sphere.r; +} + +// Check to see of a light is partially contained within the frustum. +bool SphereInsideFrustum(Sphere sphere, Frustum frustum, float zNear, float zFar) // this can only be used in view space +{ + bool result = true; + + //if (sphere.c.z + sphere.r < zNear || sphere.c.z - sphere.r > zFar) + //{ + // result = false; + //} + + //// Then check frustum planes + //for (int i = 0; i < 4 && result; i++) + //{ + // if (SphereInsidePlane(sphere, frustum.planes[i])) + // { + // result = false; + // } + //} + + // Better to just unroll: + result = ((sphere.c.z + sphere.r < zNear || sphere.c.z - sphere.r > zFar) ? false : result); + result = ((SphereInsidePlane(sphere, frustum.planes[0])) ? false : result); + result = ((SphereInsidePlane(sphere, frustum.planes[1])) ? false : result); + result = ((SphereInsidePlane(sphere, frustum.planes[2])) ? false : result); + result = ((SphereInsidePlane(sphere, frustum.planes[3])) ? false : result); + + return result; +} + +bool SphereInsideFrustum(Sphere sphere, Plane planes[6]) // this can be used in world space +{ + bool result = true; + + //for (int i = 0; i < 6 && result; i++) + //{ + // if (SphereInsidePlane(sphere, planes[i])) + // { + // result = false; + // } + //} + + result = (SphereInsidePlane(sphere, planes[0]) ? false : result); + result = (SphereInsidePlane(sphere, planes[1]) ? false : result); + result = (SphereInsidePlane(sphere, planes[2]) ? false : result); + result = (SphereInsidePlane(sphere, planes[3]) ? false : result); + result = (SphereInsidePlane(sphere, planes[4]) ? false : result); + result = (SphereInsidePlane(sphere, planes[5]) ? false : result); + + return result; +} + +// Check to see if a point is fully behind (inside the negative halfspace of) a plane. +bool PointInsidePlane(float3 p, Plane plane) +{ + return dot(plane.N, p) - plane.d < 0; +} + +struct Cone +{ + float3 T; // Cone tip. + float h; // Height of the cone. + float3 d; // Direction of the cone. + float r; // bottom radius of the cone. +}; + +// Check to see if a cone if fully behind (inside the negative halfspace of) a plane. +// Source: Real-time collision detection, Christer Ericson (2005) +bool ConeInsidePlane(Cone cone, Plane plane) +{ + // Compute the farthest point on the end of the cone to the positive space of the plane. + float3 m = cross(cross(plane.N, cone.d), cone.d); + float3 Q = cone.T + cone.d * cone.h - m * cone.r; + + // The cone is in the negative halfspace of the plane if both + // the tip of the cone and the farthest point on the end of the cone to the + // positive halfspace of the plane are both inside the negative halfspace + // of the plane. + return PointInsidePlane(cone.T, plane) && PointInsidePlane(Q, plane); +} + +bool ConeInsideFrustum(Cone cone, Frustum frustum, float zNear, float zFar) +{ + bool result = true; + + Plane nearPlane = { float3(0, 0, 1), zNear }; + Plane farPlane = { float3(0, 0, -1), -zFar }; + + // First check the near and far clipping planes. + if (ConeInsidePlane(cone, nearPlane) || ConeInsidePlane(cone, farPlane)) + { + result = false; + } + + // Then check frustum planes + for (int i = 0; i < 4 && result; i++) + { + if (ConeInsidePlane(cone, frustum.planes[i])) + { + result = false; + } + } + + return result; +} + +struct AABB +{ + float3 c; // center + float3 e; // half extents + + float3 getMin() { return c - e; } + float3 getMax() { return c + e; } +}; + +bool SphereIntersectsAABB(in Sphere sphere, in AABB aabb) +{ + float3 vDelta = max(0, abs(aabb.c - sphere.c) - aabb.e); + float fDistSq = dot(vDelta, vDelta); + return fDistSq <= sphere.r * sphere.r; +} + +bool IntersectAABB(AABB a, AABB b) +{ + if (abs(a.c[0] - b.c[0]) > (a.e[0] + b.e[0])) + return false; + if (abs(a.c[1] - b.c[1]) > (a.e[1] + b.e[1])) + return false; + if (abs(a.c[2] - b.c[2]) > (a.e[2] + b.e[2])) + return false; + + return true; +} + +void AABBfromMinMax(inout AABB aabb, float3 _min, float3 _max) +{ + aabb.c = (_min + _max) * 0.5f; + aabb.e = abs(_max - aabb.c); +} + +void AABBtransform(inout AABB aabb, float4x4 mat) +{ + float3 _min = aabb.getMin(); + float3 _max = aabb.getMax(); + float3 corners[8]; + corners[0] = _min; + corners[1] = float3(_min.x, _max.y, _min.z); + corners[2] = float3(_min.x, _max.y, _max.z); + corners[3] = float3(_min.x, _min.y, _max.z); + corners[4] = float3(_max.x, _min.y, _min.z); + corners[5] = float3(_max.x, _max.y, _min.z); + corners[6] = _max; + corners[7] = float3(_max.x, _min.y, _max.z); + _min = 1000000; + _max = -1000000; + + [unroll] + for (uint i = 0; i < 8; ++i) + { + corners[i] = mul(float4(corners[i], 1), mat).xyz; + _min = min(_min, corners[i]); + _max = max(_max, corners[i]); + } + + AABBfromMinMax(aabb, _min, _max); +} + +// AABB expected as center+extents: a.c (center), a.e (half-size) +// Fast box transform: e' = |R| * e, c' = R*c + t +inline void AABBTransformQuat(inout AABB a, float4 quat, float3 translation) +{ + float3x3 R = QuatToMat(quat); + + // center + a.c = mul(a.c, R) + translation; // row-vector convention + + // extents + float3 ex = abs(R[0]) * a.e.x; // R[0], R[1], R[2] are row vectors + float3 ey = abs(R[1]) * a.e.y; + float3 ez = abs(R[2]) * a.e.z; + a.e = ex + ey + ez; +} + +// Overload: pure rotation about origin +inline void AABBTransformQuat(inout AABB a, float4 quat) +{ + AABBTransformQuat(a, quat, 0); +} + #endif // CULLING_INCLUDED \ No newline at end of file diff --git a/Source/Shaders/Shaders/Include/Debug.inc.hlsl b/Source/Shaders/Shaders/Include/Debug.inc.hlsl index acfc59f1..130cb73c 100644 --- a/Source/Shaders/Shaders/Include/Debug.inc.hlsl +++ b/Source/Shaders/Shaders/Include/Debug.inc.hlsl @@ -162,7 +162,7 @@ void DrawPlane3D(float3 v0, float3 v1, float3 v2, float3 v3, uint color) DrawLine3D(context, v3, v0, color); } -void DrawAABB3D(inout DebugDrawContext3D context, AABB aabb, uint color) +void DrawAABB3D(inout DebugDrawContext3D context, AABBMinMax aabb, uint color) { // Bottom vertices float3 v0 = float3(aabb.min.x, aabb.min.y, aabb.min.z); @@ -195,12 +195,82 @@ void DrawAABB3D(inout DebugDrawContext3D context, AABB aabb, uint color) DrawLine3D(context, v3, v7, color); } -void DrawAABB3D(AABB aabb, uint color) +void DrawAABB3D(AABBMinMax aabb, uint color) { DebugDrawContext3D context = StartDraw3D(24); DrawAABB3D(context, aabb, color); } +void DrawOBB3D(inout DebugDrawContext3D context, float3 center, float3 extents, float4 quatXYZW, uint color) +{ + // Ensure unit quaternion + float4 q = normalize(quatXYZW); + + // Quaternion -> 3x3 rotation matrix (xyzw; w = scalar) + float xx = q.x * q.x, yy = q.y * q.y, zz = q.z * q.z; + float xy = q.x * q.y, xz = q.x * q.z, yz = q.y * q.z; + float wx = q.w * q.x, wy = q.w * q.y, wz = q.w * q.z; + + float3x3 R; + R[0][0] = 1.0f - 2.0f * (yy + zz); + R[0][1] = 2.0f * (xy + wz); + R[0][2] = 2.0f * (xz - wy); + + R[1][0] = 2.0f * (xy - wz); + R[1][1] = 1.0f - 2.0f * (xx + zz); + R[1][2] = 2.0f * (yz + wx); + + R[2][0] = 2.0f * (xz + wy); + R[2][1] = 2.0f * (yz - wx); + R[2][2] = 1.0f - 2.0f * (xx + yy); + + // Local corners in OBB space (using half-extents) + const float3 e = extents; + float3 c[8]; + c[0] = float3(-e.x, -e.y, -e.z); + c[1] = float3(-e.x, -e.y, +e.z); + c[2] = float3(+e.x, -e.y, +e.z); + c[3] = float3(+e.x, -e.y, -e.z); + c[4] = float3(-e.x, +e.y, -e.z); + c[5] = float3(-e.x, +e.y, +e.z); + c[6] = float3(+e.x, +e.y, +e.z); + c[7] = float3(+e.x, +e.y, -e.z); + + // Rotate + translate to world + float3 v[8]; + [unroll] for (uint i = 0; i < 8; ++i) + { + // Using row-vector convention: v_world = v_local * R + center + v[i] = mul(c[i], R) + center; + } + + // Edges (12 lines) + // Bottom face + DrawLine3D(context, v[0], v[1], color); + DrawLine3D(context, v[1], v[2], color); + DrawLine3D(context, v[2], v[3], color); + DrawLine3D(context, v[3], v[0], color); + + // Top face + DrawLine3D(context, v[4], v[5], color); + DrawLine3D(context, v[5], v[6], color); + DrawLine3D(context, v[6], v[7], color); + DrawLine3D(context, v[7], v[4], color); + + // Vertical edges + DrawLine3D(context, v[0], v[4], color); + DrawLine3D(context, v[1], v[5], color); + DrawLine3D(context, v[2], v[6], color); + DrawLine3D(context, v[3], v[7], color); +} + +// Convenience wrapper that allocates the vertex space (12 lines * 2 verts) +void DrawOBB3D(float3 center, float3 extents, float4 quatXYZW, uint color) +{ + DebugDrawContext3D ctx = StartDraw3D(24); + DrawOBB3D(ctx, center, extents, quatXYZW, color); +} + float3 Unproject(float3 p, float4x4 m) { float4 obj = mul(float4(p, 1.0f), m); diff --git a/Source/Shaders/Shaders/Include/Shadows.inc.hlsl b/Source/Shaders/Shaders/Include/Shadows.inc.hlsl index 5a9892f4..a3295cb0 100644 --- a/Source/Shaders/Shaders/Include/Shadows.inc.hlsl +++ b/Source/Shaders/Shaders/Include/Shadows.inc.hlsl @@ -5,9 +5,9 @@ #define MAX_SHADOW_CASCADES 8 // Has to be kept in sync with the one in RenderSettings.h -[[vk::binding(0, SHADOWS)]] SamplerComparisonState _shadowCmpSampler; -[[vk::binding(1, SHADOWS)]] SamplerState _shadowPointClampSampler; -[[vk::binding(2, SHADOWS)]] Texture2D _shadowCascadeRTs[MAX_SHADOW_CASCADES]; +[[vk::binding(0, LIGHT)]] SamplerComparisonState _shadowCmpSampler; +[[vk::binding(1, LIGHT)]] SamplerState _shadowPointClampSampler; +[[vk::binding(2, LIGHT)]] Texture2D _shadowCascadeRTs[MAX_SHADOW_CASCADES]; struct ShadowSettings { diff --git a/Source/Shaders/Shaders/Light/Classification.cs.hlsl b/Source/Shaders/Shaders/Light/Classification.cs.hlsl new file mode 100644 index 00000000..8eedb506 --- /dev/null +++ b/Source/Shaders/Shaders/Light/Classification.cs.hlsl @@ -0,0 +1,323 @@ +// Based on the work of WickedEngine, thank you! https://github.com/turanszkij/WickedEngine/ +// https://github.com/turanszkij/WickedEngine/blob/master/WickedEngine/shaders/lightCullingCS.hlsl + +#include "common.inc.hlsl" +#include "globalData.inc.hlsl" +#include "Include/Culling.inc.hlsl" +#include "Include/Debug.inc.hlsl" +#include "Light/LightShared.inc.hlsl" + +#define DEBUG_TILEDLIGHTCULLING +#define ADVANCED_CULLING + +struct Constants +{ + uint maxDecalsPerTile; + uint numTotalDecals; + uint2 tileCount; + float2 screenSize; // In pixels +}; + +[[vk::push_constant]] Constants _constants; + +[[vk::binding(0, PER_PASS)]] StructuredBuffer _packedDecals; // All decals in the world +[[vk::binding(1, PER_PASS)]] Texture2D _depthRT; + +[[vk::binding(2, PER_PASS)]] RWStructuredBuffer _entityTiles; + +// Group shared variables. +groupshared uint uMinDepth; +groupshared uint uMaxDepth; +groupshared uint uDepthMask; // Harada Siggraph 2012 2.5D culling +groupshared uint tile_opaque[SHADER_ENTITY_TILE_BUCKET_COUNT]; +groupshared uint tile_transparent[SHADER_ENTITY_TILE_BUCKET_COUNT]; +#ifdef DEBUG_TILEDLIGHTCULLING +groupshared uint entityCountDebug; +[[vk::binding(3, PER_PASS)]] RWTexture2D _debugTexture; +#endif // DEBUG_TILEDLIGHTCULLING + +void AppendEntity_Opaque(uint entityIndex) +{ + const uint bucket_index = entityIndex / 32; + const uint bucket_place = entityIndex % 32; + InterlockedOr(tile_opaque[bucket_index], 1u << bucket_place); + +#ifdef DEBUG_TILEDLIGHTCULLING + InterlockedAdd(entityCountDebug, 1); +#endif // DEBUG_TILEDLIGHTCULLING +} + +void AppendEntity_Transparent(uint entityIndex) +{ + const uint bucket_index = entityIndex / 32; + const uint bucket_place = entityIndex % 32; + InterlockedOr(tile_transparent[bucket_index], 1u << bucket_place); +} + +inline uint ConstructEntityMask(in float depthRangeMin, in float depthRangeRecip, in Sphere bounds) +{ + // We create a entity mask to decide if the entity is really touching something + // If we do an OR operation with the depth slices mask, we instantly get if the entity is contributing or not + // we do this in view space + + const float fMin = bounds.c.z - bounds.r; + const float fMax = bounds.c.z + bounds.r; + const uint __entitymaskcellindexSTART = clamp(floor((fMin - depthRangeMin) * depthRangeRecip), 0, 31); + const uint __entitymaskcellindexEND = clamp(floor((fMax - depthRangeMin) * depthRangeRecip), 0, 31); + + //// Unoptimized mask construction with loop: + //// Construct mask from START to END: + //// END START + //// 0000000000111111111110000000000 + //uint uLightMask = 0; + //for (uint c = __entitymaskcellindexSTART; c <= __entitymaskcellindexEND; ++c) + //{ + // uLightMask |= 1u << c; + //} + + // Optimized mask construction, without loop: + // - First, fill full mask: + // 1111111111111111111111111111111 + uint uLightMask = 0xFFFFFFFF; + // - Then Shift right with spare amount to keep mask only: + // 0000000000000000000011111111111 + uLightMask >>= 31u - (__entitymaskcellindexEND - __entitymaskcellindexSTART); + // - Last, shift left with START amount to correct mask position: + // 0000000000111111111110000000000 + uLightMask <<= __entitymaskcellindexSTART; + + return uLightMask; +} + +inline AABB WorldAABB_To_DecalSpace(AABB worldBox, float3 decalCenter, float4 decalQuat) { + float3 r, u, f; + BasisFromQuat(decalQuat, r, u, f); // 1) center: translate to decal origin, then project onto basis + + float3 cRel = worldBox.c - decalCenter; + AABB outB; + outB.c = float3(dot(cRel, r), dot(cRel, u), dot(cRel, f)); // 2) extents: e' = |M| * e, where M rows are (r,u,f) + + // i.e., abs(row) * ex + abs(row) * ey + abs(row) * ez + float3 row0 = abs(r); + float3 row1 = abs(u); + float3 row2 = abs(f); // multiply rows by source extents components and sum per row + outB.e = float3(dot(row0, worldBox.e), // X' + dot(row1, worldBox.e), // Y' + dot(row2, worldBox.e) // Z' + ); + return outB; +} + +[numthreads(TILED_CULLING_THREADSIZE, TILED_CULLING_THREADSIZE, 1)] +void main(uint3 Gid : SV_GroupID, uint3 DTid : SV_DispatchThreadID, uint3 GTid : SV_GroupThreadID, uint groupIndex : SV_GroupIndex) +{ + uint2 dim; + _depthRT.GetDimensions(dim.x, dim.y); + float2 dim_rcp = rcp(dim); + + // Each thread will zero out one bucket in the LDS: + for (uint i = groupIndex; i < SHADER_ENTITY_TILE_BUCKET_COUNT; i += TILED_CULLING_THREADSIZE * TILED_CULLING_THREADSIZE) + { + tile_opaque[i] = 0; + tile_transparent[i] = 0; + } + + // First thread zeroes out other LDS data: + if (groupIndex == 0) + { + uMinDepth = 0xffffffff; + uMaxDepth = 0; + uDepthMask = 0; + +#ifdef DEBUG_TILEDLIGHTCULLING + entityCountDebug = 0; +#endif // DEBUG_TILEDLIGHTCULLING + } + + // Calculate min & max depth in threadgroup / tile. + float depth[TILED_CULLING_GRANULARITY * TILED_CULLING_GRANULARITY]; + float depthMinUnrolled = 10000000; + float depthMaxUnrolled = -10000000; + + [unroll] + for (uint granularity = 0; granularity < TILED_CULLING_GRANULARITY * TILED_CULLING_GRANULARITY; ++granularity) + { + uint2 pixel = DTid.xy * uint2(TILED_CULLING_GRANULARITY, TILED_CULLING_GRANULARITY) + Unflatten2D(granularity, TILED_CULLING_GRANULARITY); + pixel = min(pixel, dim - 1); // avoid loading from outside the texture, it messes up the min-max depth! + depth[granularity] = _depthRT[pixel]; + depthMinUnrolled = min(depthMinUnrolled, depth[granularity]); + depthMaxUnrolled = max(depthMaxUnrolled, depth[granularity]); + } + + GroupMemoryBarrierWithGroupSync(); + + float wave_local_min = WaveActiveMin(depthMinUnrolled); + float wave_local_max = WaveActiveMax(depthMaxUnrolled); + if (WaveIsFirstLane()) + { + InterlockedMin(uMinDepth, asuint(wave_local_min)); + InterlockedMax(uMaxDepth, asuint(wave_local_max)); + } + + GroupMemoryBarrierWithGroupSync(); + + // reversed depth buffer! + float fMinDepth = asfloat(uMaxDepth); + float fMaxDepth = asfloat(uMinDepth); + + fMaxDepth = max(0.000001, fMaxDepth); // fix for AMD!!!!!!!!! + + Frustum GroupFrustum; + AABB GroupAABB; // frustum AABB around min-max depth in View Space + AABB GroupAABB_WS; // frustum AABB in world space + float3 viewSpace[8]; // View space frustum corners + { + // Top left point, near + viewSpace[0] = ScreenToView(float4(Gid.xy * TILED_CULLING_BLOCKSIZE, fMinDepth, 1.0f), dim_rcp).xyz; + // Top right point, near + viewSpace[1] = ScreenToView(float4(float2(Gid.x + 1, Gid.y) * TILED_CULLING_BLOCKSIZE, fMinDepth, 1.0f), dim_rcp).xyz; + // Bottom left point, near + viewSpace[2] = ScreenToView(float4(float2(Gid.x, Gid.y + 1) * TILED_CULLING_BLOCKSIZE, fMinDepth, 1.0f), dim_rcp).xyz; + // Bottom right point, near + viewSpace[3] = ScreenToView(float4(float2(Gid.x + 1, Gid.y + 1) * TILED_CULLING_BLOCKSIZE, fMinDepth, 1.0f), dim_rcp).xyz; + // Top left point, far + viewSpace[4] = ScreenToView(float4(Gid.xy * TILED_CULLING_BLOCKSIZE, fMaxDepth, 1.0f), dim_rcp).xyz; + // Top right point, far + viewSpace[5] = ScreenToView(float4(float2(Gid.x + 1, Gid.y) * TILED_CULLING_BLOCKSIZE, fMaxDepth, 1.0f), dim_rcp).xyz; + // Bottom left point, far + viewSpace[6] = ScreenToView(float4(float2(Gid.x, Gid.y + 1) * TILED_CULLING_BLOCKSIZE, fMaxDepth, 1.0f), dim_rcp).xyz; + // Bottom right point, far + viewSpace[7] = ScreenToView(float4(float2(Gid.x + 1, Gid.y + 1) * TILED_CULLING_BLOCKSIZE, fMaxDepth, 1.0f), dim_rcp).xyz; + + // Left plane + GroupFrustum.planes[0] = ComputePlane(viewSpace[2], viewSpace[0], viewSpace[4]); + // Right plane + GroupFrustum.planes[1] = ComputePlane(viewSpace[1], viewSpace[3], viewSpace[5]); + // Top plane + GroupFrustum.planes[2] = ComputePlane(viewSpace[0], viewSpace[1], viewSpace[4]); + // Bottom plane + GroupFrustum.planes[3] = ComputePlane(viewSpace[3], viewSpace[2], viewSpace[6]); + + // I construct an AABB around the minmax depth bounds to perform tighter culling: + // The frustum is asymmetric so we must consider all corners! + float3 minAABB = 10000000; + float3 maxAABB = -10000000; + [unroll] + for (uint i = 0; i < 8; ++i) + { + minAABB = min(minAABB, viewSpace[i]); + maxAABB = max(maxAABB, viewSpace[i]); + } + + AABBfromMinMax(GroupAABB, minAABB, maxAABB); + + // We can perform coarse AABB intersection tests with this: + GroupAABB_WS = GroupAABB; + AABBtransform(GroupAABB_WS, _cameras[0].viewToWorld);//AABBtransform(GroupAABB_WS, GetCamera().inverse_view); + } + + // Convert depth values to view space. + float minDepthVS = ScreenToView(float4(0, 0, fMinDepth, 1), dim_rcp).z; + float maxDepthVS = ScreenToView(float4(0, 0, fMaxDepth, 1), dim_rcp).z; + float nearClipVS = ScreenToView(float4(0, 0, 1, 1), dim_rcp).z; + +#ifdef ADVANCED_CULLING + // We divide the minmax depth bounds to 32 equal slices + // then we mark the occupied depth slices with atomic or from each thread + // we do all this in linear (view) space + const float __depthRangeRecip = 31.0f / (maxDepthVS - minDepthVS); + uint __depthmaskUnrolled = 0; + + [unroll] + for (uint granularity = 0; granularity < TILED_CULLING_GRANULARITY * TILED_CULLING_GRANULARITY; ++granularity) + { + float realDepthVS = ScreenToView(float4(0, 0, depth[granularity], 1), dim_rcp).z; + const uint __depthmaskcellindex = max(0, min(31, floor((realDepthVS - minDepthVS) * __depthRangeRecip))); + __depthmaskUnrolled |= 1u << __depthmaskcellindex; + } + + uint wave_depth_mask = WaveActiveBitOr(__depthmaskUnrolled); + if (WaveIsFirstLane()) + { + InterlockedOr(uDepthMask, wave_depth_mask); + } +#endif + + GroupMemoryBarrierWithGroupSync(); + + const uint depth_mask = uDepthMask; // take out from groupshared into register + + // Decals: + for (uint i = 0 + groupIndex; i < _constants.numTotalDecals; i += TILED_CULLING_THREADSIZE * TILED_CULLING_THREADSIZE) + { + PackedDecal packedDecal = _packedDecals[i]; + Decal decal = UnpackDecal(packedDecal); + + float3 positionVS = mul(float4(decal.position, 1), _cameras[0].worldToView).xyz; + const float radius = length(decal.extents.xyz); + Sphere sphere = { positionVS.xyz, radius }; + if (SphereInsideFrustum(sphere, GroupFrustum, nearClipVS, maxDepthVS)) + { + AppendEntity_Transparent(i); + + // unit AABB: + AABB a; + a.c = 0; + a.e = decal.extents.xyz; + + // frustum AABB in world space transformed into the space of the probe/decal OBB + AABB b = GroupAABB_WS; + b = WorldAABB_To_DecalSpace(b, decal.position, decal.rotationQuat); + + // TODO: More accurate culling? We see the decal getting classified into tiles that are outside the AABB, + // probably because we're doing this check in decal space and not viewspace, but this is good enough for now + + if (IntersectAABB(a, b)) + { +#ifdef ADVANCED_CULLING + if (depth_mask & ConstructEntityMask(minDepthVS, __depthRangeRecip, sphere)) +#endif + { + AppendEntity_Opaque(i); + } + } + } + } + + GroupMemoryBarrierWithGroupSync(); + + const uint flatTileIndex = Flatten2D(Gid.xy, _constants.tileCount.xy); + const uint tileBucketsAddress = flatTileIndex * SHADER_ENTITY_TILE_BUCKET_COUNT; + + const uint entityCullingTileBucketCountFlat = _constants.tileCount.x * _constants.tileCount.y * SHADER_ENTITY_TILE_BUCKET_COUNT; + + // Each thread will export one bucket from LDS to global memory: + for (uint i = groupIndex; i < SHADER_ENTITY_TILE_BUCKET_COUNT; i += TILED_CULLING_THREADSIZE * TILED_CULLING_THREADSIZE) + { + _entityTiles[tileBucketsAddress + i] = tile_opaque[i]; + _entityTiles[entityCullingTileBucketCountFlat + tileBucketsAddress + i] = tile_transparent[i]; + } + +#ifdef DEBUG_TILEDLIGHTCULLING + for (uint granularity = 0; granularity < TILED_CULLING_GRANULARITY * TILED_CULLING_GRANULARITY; ++granularity) + { + uint2 pixel = DTid.xy * uint2(TILED_CULLING_GRANULARITY, TILED_CULLING_GRANULARITY) + Unflatten2D(granularity, TILED_CULLING_GRANULARITY); + + const float3 mapTex[] = { + float3(0,0,0), + float3(0,0,1), + float3(0,1,1), + float3(0,1,0), + float3(1,1,0), + float3(1,0,0), + }; + const uint mapTexLen = 5; + const uint maxHeat = _constants.maxDecalsPerTile; + float l = saturate((float)entityCountDebug / maxHeat) * mapTexLen; + float3 a = mapTex[floor(l)]; + float3 b = mapTex[ceil(l)]; + float4 heatmap = float4(lerp(a, b, l - floor(l)), 0.8); + _debugTexture[pixel] = heatmap; + } +#endif // DEBUG_TILEDLIGHTCULLING +} \ No newline at end of file diff --git a/Source/Shaders/Shaders/Light/LightShared.inc.hlsl b/Source/Shaders/Shaders/Light/LightShared.inc.hlsl new file mode 100644 index 00000000..1063eb03 --- /dev/null +++ b/Source/Shaders/Shaders/Light/LightShared.inc.hlsl @@ -0,0 +1,130 @@ +#ifndef LIGHT_SHARED_INCLUDED +#define LIGHT_SHARED_INCLUDED +#include "common.inc.hlsl" + +static const uint TILED_CULLING_BLOCKSIZE = 16; +static const uint TILED_CULLING_THREADSIZE = 8; +static const uint TILED_CULLING_GRANULARITY = TILED_CULLING_BLOCKSIZE / TILED_CULLING_THREADSIZE; +static const uint SHADER_ENTITY_TILE_BUCKET_COUNT = 1; // Arbitrary limit, should be enough for most cases, just make sure this is equal or higher than maxDecalsPerTile + +struct PackedDecal +{ + float4 positionAndTextureID; // xyz = position, w = texture index + float4 rotationQuat; + float4 extentsAndColor; // xyz = extents, asuint(w) = uint color multiplier + float4 thresholds; // x = f16 min/max threshold, y = f16 min UV, z = f16 max UV, asuint(w) = flags +}; + +enum DecalFlags +{ + DECAL_FLAG_TWOSIDED = 1 << 0, +}; + +struct Decal +{ + float3 position; + float4 rotationQuat; + float3 extents; + float3 color; + float minThreshold; + float maxThreshold; + float2 minUV; + float2 maxUV; + uint textureID; + uint flags; + + bool IsTwoSided() { return (flags & DECAL_FLAG_TWOSIDED) != 0; } +}; + +float3 ApplyDecal(float3 pixelWS, float3 normalWS, Decal d, Texture2D decalTexture, SamplerState sampler) +{ + const float3 halfExtents = d.extents.xyz; + const float3 center = d.position.xyz; + const uint texId = d.textureID; + + // world -> local (inverse rotation via conjugate) + const float4 rotQuatInv = float4(-d.rotationQuat.xyz, d.rotationQuat.w); + const float3x3 rotMatInv = QuatToMat(rotQuatInv); + const float3 pixelLocal = mul(pixelWS - center, rotMatInv); // local coords + + // Select planar axes and signed depth along projector axis + float2 planar = pixelLocal.xy; + float2 extents2D = halfExtents.xy; + float depthS = pixelLocal.z; + float extentsDepth = halfExtents.z; + + // Full OBB thickness and footprint + if (depthS < -extentsDepth || depthS > extentsDepth) + return 0; + + if (any(abs(planar) > extents2D)) + return 0; + + if (!d.IsTwoSided()) + { + // Facing gate (reject only the underside) + const float3 localFwd = float3(0, 0, -1); + const float3x3 rotationMat = QuatToMat(d.rotationQuat); + const float3 decalProjDirWS = normalize(mul(normalize(localFwd), rotationMat)); + normalWS = normalize(normalWS); + + // If the surface normal points roughly the SAME way as the projector forward, it's the underside -> reject. + const float minSameDirThreshold = d.minThreshold; + const float maxSameDirThreshold = d.maxThreshold; + + float pixelDotDecalProjDir = dot(normalWS, decalProjDirWS); + if (pixelDotDecalProjDir < minSameDirThreshold) + return 0; + if (pixelDotDecalProjDir > maxSameDirThreshold) + return 0; + } + + // Map planar [-extents2D, +extents2D] -> [0,1] + const float2 uv01 = planar * (0.5f / extents2D) + 0.5f; + + // Map [0,1] -> [minUV,maxUV] + const float2 uvMin = d.minUV; + const float2 uvMax = d.maxUV; + const float2 uvSpan = uvMax - uvMin; + + // Gradients for compute shader sampling + float2 ddx01, ddy01; + ComputeGradients_Quad(uv01, ddx01, ddy01); + + // Scale gradients to keep mip selection correct + const float2 uv = uvMin + uv01 * uvSpan; + const float2 ddx = ddx01 * uvSpan; + const float2 ddy = ddy01 * uvSpan; + + float4 c = decalTexture.SampleGrad(sampler, uv, ddx, ddy); + c.rgb *= d.color; + + return c.rgb * c.a; +} + +Decal UnpackDecal(PackedDecal pd) +{ + Decal d; + d.position = pd.positionAndTextureID.xyz; + d.textureID = asuint(pd.positionAndTextureID.w); + + d.rotationQuat = pd.rotationQuat; + d.extents = pd.extentsAndColor.xyz; + + uint colorUint = asuint(pd.extentsAndColor.w); // ABGR + d.color = float3(((colorUint >> 24) & 0xFF) / 255.0f, ((colorUint >> 16) & 0xFF) / 255.0f, ((colorUint >> 8) & 0xFF) / 255.0f); + + // Unpack minThreshold and maxThreshold from 2xf16 packed into a f32 + d.minThreshold = f16tof32(asuint(pd.thresholds.x) & 0xFFFF); + d.maxThreshold = f16tof32((asuint(pd.thresholds.x) >> 16) & 0xFFFF); + + // Unpack minUV and maxUV from 4xf16 packed into 2xf32 + d.minUV = float2(f16tof32(asuint(pd.thresholds.y) & 0xFFFF), f16tof32((asuint(pd.thresholds.y) >> 16) & 0xFFFF)); + d.maxUV = float2(f16tof32(asuint(pd.thresholds.z) & 0xFFFF), f16tof32((asuint(pd.thresholds.z) >> 16) & 0xFFFF)); + + d.flags = asuint(pd.thresholds.w); + + return d; +} + +#endif // LIGHT_SHARED_INCLUDED \ No newline at end of file diff --git a/Source/Shaders/Shaders/MaterialPass.cs.hlsl b/Source/Shaders/Shaders/MaterialPass.cs.hlsl index 59b7bd84..5dae35ac 100644 --- a/Source/Shaders/Shaders/MaterialPass.cs.hlsl +++ b/Source/Shaders/Shaders/MaterialPass.cs.hlsl @@ -10,11 +10,13 @@ permutation EDITOR_MODE = [0, 1]; // Off, Terrain #include "Include/Lighting.inc.hlsl" #include "Terrain/TerrainShared.inc.hlsl" #include "Model/ModelShared.inc.hlsl" +#include "Light/LightShared.inc.hlsl" struct Constants { float4 renderInfo; // x = Render Width, y = Render Height, z = 1/Width, w = 1/Height uint4 lightInfo; // x = Num Directional Lights, y = Num Point Lights, z = Num Cascades, w = Shadows Enabled + uint4 tileInfo; // xy = Num Tiles float4 fogColor; float4 fogSettings; // x = Enabled, y = Begin Fog Blend Dist, z = End Fog Blend Dist, w = UNUSED float4 mouseWorldPos; @@ -28,13 +30,65 @@ struct Constants [[vk::push_constant]] Constants _constants; +// Tiled culling +[[vk::binding(0, TILES)]] StructuredBuffer _entityTiles; + +// Lighting +[[vk::binding(0, LIGHT)]] StructuredBuffer _packedDecals; // All decals in the world + [[vk::binding(0, PER_PASS)]] SamplerState _sampler; [[vk::binding(3, PER_PASS)]] Texture2D _skyboxColor; [[vk::binding(4, PER_PASS)]] Texture2D _transparency; [[vk::binding(5, PER_PASS)]] Texture2D _transparencyWeights; [[vk::binding(6, PER_PASS)]] RWTexture2D _resolvedColor; -float4 ShadeTerrain(const uint2 pixelPos, const float2 screenUV, const VisibilityBuffer vBuffer, out float3 outPixelWorldPos) +float3 ProcessDecalBucket(uint buckedIndex, uint mask, float3 pixelWorldPos, float3 pixelWorldNormal) +{ + // Walk set bits in mask + float3 color = float3(0, 0, 0); + + while (mask) + { + uint bit = firstbitlow(mask); + uint decalIndex = buckedIndex * 32u + bit; // Global index into _decals + + PackedDecal packedDecal = _packedDecals[decalIndex]; + Decal decal = UnpackDecal(packedDecal); + + Texture2D decalTexture = _modelTextures[NonUniformResourceIndex(decal.textureID)]; // Decals share the same texture array as models + + color += ApplyDecal(pixelWorldPos, pixelWorldNormal, decal, decalTexture, _sampler); + + mask &= (mask - 1); // Clear the least significant bit set + } + + return color; +} + +float3 ApplyDecals(uint tileIndex, float3 pixelWorldPos, float3 pixelWorldNormal) +{ + // Layout reminder: + // [0 .. Tiles*Buckets-1] : Opaque bitmasks per tile, use these when applied to opaque objects + // [Tiles*Buckets .. 2*Tiles*Buckets-1] : Transparent bitmasks per tile, use these whne applied to transparent objects such as fog between camera and opaque (not actually used yet) + + uint numTiles = _constants.tileInfo.x * _constants.tileInfo.y; + uint bucketsPerLayer = SHADER_ENTITY_TILE_BUCKET_COUNT; + + uint opaqueBase = tileIndex * bucketsPerLayer; + uint transparentBase = numTiles * bucketsPerLayer + opaqueBase; // I don't think I need this but lets keep it so we know what to do in the future + + float3 color = float3(0, 0, 0); + for (uint bucketIndex = 0; bucketIndex < bucketsPerLayer; bucketIndex++) + { + uint mask = _entityTiles[opaqueBase + bucketIndex]; + if (mask != 0) + color += ProcessDecalBucket(bucketIndex, mask, pixelWorldPos, pixelWorldNormal); + } + + return saturate(color); +} + +float4 ShadeTerrain(const uint2 pixelPos, const float2 screenUV, const VisibilityBuffer vBuffer, uint tileIndex, out float3 outPixelWorldPos) { // Get the interpolated vertex data from the visibility buffer PixelVertexData pixelVertexData = GetPixelVertexDataTerrain(pixelPos, vBuffer, 0, _constants.renderInfo.xy); @@ -86,12 +140,6 @@ float4 ShadeTerrain(const uint2 pixelPos, const float2 screenUV, const Visibilit // Apply vertex color color.rgb *= pixelVertexData.color; - //if (_constants.numTextureDecals + _constants.numProceduralDecals > 0) - //{ - // // Apply decals - // ApplyDecals(pixelWorldPosition, color, pixelNormal, _constants.numTextureDecals, _constants.numProceduralDecals, _constants.mouseWorldPos.xyz); - //} - // TODO: Don't hardcode this ShadowSettings shadowSettings; shadowSettings.enableShadows = _constants.lightInfo.w == 1; @@ -101,6 +149,9 @@ float4 ShadeTerrain(const uint2 pixelPos, const float2 screenUV, const Visibilit // Apply lighting color.rgb = ApplyLighting(screenUV, color.rgb, pixelVertexData, _constants.lightInfo, shadowSettings); + // Apply decals + color.rgb += ApplyDecals(tileIndex, pixelVertexData.worldPos, pixelVertexData.worldNormal); + /*#if EDITOR_MODE == 1 // Get settings from push constants const float brushHardness = _constants.brushSettings.x; @@ -185,7 +236,7 @@ float4 ShadeTerrain(const uint2 pixelPos, const float2 screenUV, const Visibilit return saturate(color); } -float4 ShadeModel(const uint2 pixelPos, const float2 screenUV, const VisibilityBuffer vBuffer, out float3 outPixelWorldPos) +float4 ShadeModel(const uint2 pixelPos, const float2 screenUV, const VisibilityBuffer vBuffer, uint tileIndex, out float3 outPixelWorldPos) { // Get the interpolated vertex data from the visibility buffer PixelVertexData pixelVertexData = GetPixelVertexDataModel(pixelPos, vBuffer, 0, _constants.renderInfo.xy); @@ -247,6 +298,9 @@ float4 ShadeModel(const uint2 pixelPos, const float2 screenUV, const VisibilityB // Apply highlight color.rgb *= pixelVertexData.highlightIntensity; + // Apply decals + color.rgb += ApplyDecals(tileIndex, pixelVertexData.worldPos, pixelVertexData.worldNormal); + outPixelWorldPos = pixelVertexData.worldPos; return saturate(color); } @@ -293,6 +347,8 @@ void main(uint3 dispatchThreadId : SV_DispatchThreadID) #endif float2 pixelUV = pixelPos / _constants.renderInfo.xy; + uint2 tileCoord = PixelToTile(pixelPos, TILED_CULLING_BLOCKSIZE); + uint tileIndex = Flatten2D(tileCoord, _constants.tileInfo.xy); float4 color = float4(0, 0, 0, 1); @@ -308,11 +364,11 @@ void main(uint3 dispatchThreadId : SV_DispatchThreadID) } else if (vBuffer.typeID == ObjectType::Terrain) { - color = ShadeTerrain(pixelPos, pixelUV, vBuffer, pixelWorldPos); + color = ShadeTerrain(pixelPos, pixelUV, vBuffer, tileIndex, pixelWorldPos); } else if (vBuffer.typeID == ObjectType::ModelOpaque) // Transparent models are not rendered using visibility buffers { - color = ShadeModel(pixelPos, pixelUV, vBuffer, pixelWorldPos); + color = ShadeModel(pixelPos, pixelUV, vBuffer, tileIndex, pixelWorldPos); } else { diff --git a/Source/Shaders/Shaders/Terrain/Culling.cs.hlsl b/Source/Shaders/Shaders/Terrain/Culling.cs.hlsl index 25acb276..5620bfa0 100644 --- a/Source/Shaders/Shaders/Terrain/Culling.cs.hlsl +++ b/Source/Shaders/Shaders/Terrain/Culling.cs.hlsl @@ -72,7 +72,7 @@ struct DrawInput CSInput csInput; InstanceData instance; float4 sphere; - AABB aabb; + AABBMinMax aabb; bool shouldOcclusionCull; bool isShadowPass; }; @@ -143,7 +143,7 @@ void main(CSInput input) const uint chunkID = instance.packedChunkCellID >> 16; const float2 heightRange = ReadHeightRange(instanceIndex); - AABB aabb = GetCellAABB(chunkID, cellID, heightRange); + AABBMinMax aabb = GetCellAABB(chunkID, cellID, heightRange); float4 sphere; sphere.xyz = (aabb.min + aabb.max) / 2.0f; diff --git a/Source/Shaders/Shaders/Terrain/TerrainShared.inc.hlsl b/Source/Shaders/Shaders/Terrain/TerrainShared.inc.hlsl index 82efe204..f7990dd0 100644 --- a/Source/Shaders/Shaders/Terrain/TerrainShared.inc.hlsl +++ b/Source/Shaders/Shaders/Terrain/TerrainShared.inc.hlsl @@ -107,15 +107,15 @@ float2 GetGlobalVertexPosition(uint chunkID, uint cellID, uint vertexID) return float2(finalPos.x, -finalPos.y); } -AABB GetCellAABB(uint chunkID, uint cellID, float2 heightRange) +AABBMinMax GetCellAABB(uint chunkID, uint cellID, float2 heightRange) { float2 pos = GetCellPosition(chunkID, cellID); - float3 aabb_min = float3(pos.x, heightRange.x, pos.y); - float3 aabb_max = float3(pos.x - CELL_SIDE_SIZE, heightRange.y, pos.y - CELL_SIDE_SIZE); + float3 aabbMin = float3(pos.x, heightRange.x, pos.y); + float3 aabbMax = float3(pos.x - CELL_SIDE_SIZE, heightRange.y, pos.y - CELL_SIDE_SIZE); - AABB boundingBox; - boundingBox.min = max(aabb_min, aabb_max); - boundingBox.max = min(aabb_min, aabb_max); + AABBMinMax boundingBox; + boundingBox.min = max(aabbMin, aabbMax); + boundingBox.max = min(aabbMin, aabbMax); return boundingBox; } diff --git a/Source/Shaders/Shaders/Utils/Culling.cs.hlsl b/Source/Shaders/Shaders/Utils/Culling.cs.hlsl index e936457e..a83044ab 100644 --- a/Source/Shaders/Shaders/Utils/Culling.cs.hlsl +++ b/Source/Shaders/Shaders/Utils/Culling.cs.hlsl @@ -32,7 +32,7 @@ struct PackedCullingData struct CullingData { - AABB boundingBox; + AABBMinMax boundingBox; float sphereRadius; }; @@ -121,7 +121,7 @@ struct DrawInput CSInput csInput; IndexedDraw drawCall; float4 sphere; - AABB aabb; + AABBMinMax aabb; float4x4 instanceMatrix; bool shouldOcclusionCull; }; @@ -244,7 +244,7 @@ void main(CSInput input) const CullingData cullingData = LoadCullingData(modelID); - AABB aabb; + AABBMinMax aabb; float4x4 instanceMatrix; if (_constants.cullingDataIsWorldspace) { diff --git a/Source/Shaders/Shaders/Utils/CullingInstanced.cs.hlsl b/Source/Shaders/Shaders/Utils/CullingInstanced.cs.hlsl index e62e4ab3..4df9f31d 100644 --- a/Source/Shaders/Shaders/Utils/CullingInstanced.cs.hlsl +++ b/Source/Shaders/Shaders/Utils/CullingInstanced.cs.hlsl @@ -39,7 +39,7 @@ struct PackedCullingData struct CullingData { - AABB boundingBox; + AABBMinMax boundingBox; float sphereRadius; }; @@ -135,7 +135,7 @@ struct DrawInput CSInput csInput; uint instanceCount; float4 sphere; - AABB aabb; + AABBMinMax aabb; float4x4 instanceMatrix; bool shouldOcclusionCull; uint instanceRefID; @@ -264,7 +264,7 @@ void main(CSInput input) CullingData cullingData = LoadCullingData(modelID); // Get AABB from CullingData - AABB aabb = cullingData.boundingBox; + AABBMinMax aabb = cullingData.boundingBox; float4x4 instanceMatrix = { {1.0f, 0.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 0.0f, 1.0f} }; // Default to identity if (!_constants.cullingDataIsWorldspace) { diff --git a/Source/Shaders/Shaders/common.inc.hlsl b/Source/Shaders/Shaders/common.inc.hlsl index dc501dcd..8b670c17 100644 --- a/Source/Shaders/Shaders/common.inc.hlsl +++ b/Source/Shaders/Shaders/common.inc.hlsl @@ -181,6 +181,63 @@ float4 PackedUnormsToFloat4(uint packed) return f; } +// Use quad ops (SM 6+), best match to pixel-shader ddx/ddy: +inline void ComputeGradients_Quad(float2 uv, out float2 ddx, out float2 ddy) +{ + // Across X + float2 uv_x = QuadReadAcrossX(uv); + ddx = ((WaveGetLaneIndex() & 1) == 0) ? (uv_x - uv) : (uv - uv_x); + + // Across Y + float2 uv_y = QuadReadAcrossY(uv); + ddy = ((WaveGetLaneIndex() & 2) == 0) ? (uv_y - uv) : (uv - uv_y); +} + +// Build orthonormal basis from quaternion (row-major) +inline void BasisFromQuat(float4 q, out float3 r, out float3 u, out float3 f) +{ + // q = (x,y,z,w), unit length + q = normalize(q); + float x = q.x, y = q.y, z = q.z, w = q.w; + float xx = x * x, yy = y * y, zz = z * z; + float xy = x * y, xz = x * z, yz = y * z; + float wx = w * x, wy = w * y, wz = w * z; + + r = float3(1 - 2 * (yy + zz), 2 * (xy + wz), 2 * (xz - wy)); + u = float3(2 * (xy - wz), 1 - 2 * (xx + zz), 2 * (yz + wx)); + f = float3(2 * (xz + wy), 2 * (yz - wx), 1 - 2 * (xx + yy)); +} + +inline uint2 PixelToTile(uint2 pixel, uint2 tileSize) +{ + // Integer division floors automatically: (0..15)->0, (16..31)->1, etc. + return pixel / tileSize; +} + +// 2D array index to flattened 1D array index +inline uint Flatten2D(uint2 coord, uint2 dim) +{ + return coord.x + coord.y * dim.x; +} + +// flattened array index to 2D array index +inline uint2 Unflatten2D(uint idx, uint2 dim) +{ + return uint2(idx % dim.x, idx / dim.x); +} + +inline float3x3 QuatToMat(float4 q) +{ + q = normalize(q); + float x = q.x, y = q.y, z = q.z, w = q.w; + float xx = x * x, yy = y * y, zz = z * z, xy = x * y, xz = x * z, yz = y * z, wx = w * x, wy = w * y, wz = w * z; + return float3x3( + 1 - 2 * (yy + zz), 2 * (xy + wz), 2 * (xz - wy), + 2 * (xy - wz), 1 - 2 * (xx + zz), 2 * (yz + wx), + 2 * (xz + wy), 2 * (yz - wx), 1 - 2 * (xx + yy) + ); +} + enum ObjectType : uint { Skybox = 0, // We clear to this color so we won't be writing it diff --git a/Submodules/Engine b/Submodules/Engine index 9ecb6d9c..4f754a25 160000 --- a/Submodules/Engine +++ b/Submodules/Engine @@ -1 +1 @@ -Subproject commit 9ecb6d9ca756bcf853f1e5f720ff0784ca005762 +Subproject commit 4f754a25f8f359570291a3567f00947a5f6f0873