diff --git a/Source/Game-Lib/Game-Lib/ECS/Components/AABB.h b/Source/Game-Lib/Game-Lib/ECS/Components/AABB.h
index e9ce95d..b9d06d5 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 0000000..00e6c19
--- /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 f017015..e407c71 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 2577125..1e37e41 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 e43f501..08b2497 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 db4073d..7ac976f 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 b9f7719..3be5a05 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 a00b306..7f3e63f 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 0000000..0ce813d
--- /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 0000000..3bee256
--- /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 490a7f4..cf17529 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 7c87583..20d5eb6 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 311d71f..bdf022d 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 35b3e7a..b9c19e3 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 03c449b..10fe56a 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 e8d5454..4ca2820 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 c6ea513..82e5bd0 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 22be6fa..4c701df 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 d50b0ba..20e7eba 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 a9e785b..c54a137 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 acfc59f..130cb73 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 5a9892f..a3295cb 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 0000000..8eedb50
--- /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 0000000..1063eb0
--- /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 59b7bd8..5dae35a 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 25acb27..5620bfa 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 82efe20..f7990dd 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 e936457..a83044a 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 e62e4ab..4df9f31 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 dc501dc..8b670c1 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 9ecb6d9..4f754a2 160000
--- a/Submodules/Engine
+++ b/Submodules/Engine
@@ -1 +1 @@
-Subproject commit 9ecb6d9ca756bcf853f1e5f720ff0784ca005762
+Subproject commit 4f754a25f8f359570291a3567f00947a5f6f0873