diff --git a/Assets/VFX/RibbonGarden.json b/Assets/VFX/RibbonGarden.json index 1c21bd69..4a9af940 100644 --- a/Assets/VFX/RibbonGarden.json +++ b/Assets/VFX/RibbonGarden.json @@ -38,7 +38,7 @@ { "name": "PathRibbon", "renderMode": "Ribbon", - "material": { "color": "$canopy_color_start", "emissive": [0.10, 0.25, 0.50, 0.0] }, + "material": { "color": "$canopy_color_start", "emissive": [10.0, 25.0, 50.0, 0.0] }, "maxParticles": 256, "spawnRate": "$spawn_rate", "parameters": { diff --git a/Assets/VFX/RibbonVortex.json b/Assets/VFX/RibbonVortex.json index 18599546..0fd36d74 100644 --- a/Assets/VFX/RibbonVortex.json +++ b/Assets/VFX/RibbonVortex.json @@ -38,7 +38,7 @@ { "name": "PathRibbon", "renderMode": "Ribbon", - "material": { "color": "$canopy_color_start", "emissive": [0.10, 0.25, 0.50, 0.0] }, + "material": { "color": "$canopy_color_start", "emissive": [10.0, 25.0, 50.0, 0.0] }, "maxParticles": 256, "spawnRate": "$spawn_rate", "parameters": { diff --git a/Dissolve/Dissolve.cmake b/Dissolve/Dissolve.cmake index 0dfab6e6..589b2933 100644 --- a/Dissolve/Dissolve.cmake +++ b/Dissolve/Dissolve.cmake @@ -7,6 +7,7 @@ file(GLOB_RECURSE SOURCES "${CMAKE_CURRENT_LIST_DIR}/Source/*.h" "${CMAKE_CURRENT_LIST_DIR}/Source/*.cpp" ) +list(APPEND SOURCES "${CMAKE_SOURCE_DIR}/Elixir/Vendor/stb/stb_image.cpp") add_executable(${PROJECT_NAME} ${SOURCES}) # Set output name @@ -58,6 +59,7 @@ endif() target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/Elixir/Source + ${CMAKE_SOURCE_DIR}/Elixir/Vendor/stb ) # Linking @@ -66,4 +68,4 @@ add_dependencies(${PROJECT_NAME} Elixir ) -link_target_to_engine(${PROJECT_NAME}) \ No newline at end of file +link_target_to_engine(${PROJECT_NAME}) diff --git a/Dissolve/Source/Dissolve.cpp b/Dissolve/Source/Dissolve.cpp index 5b450d6f..e134c9af 100644 --- a/Dissolve/Source/Dissolve.cpp +++ b/Dissolve/Source/Dissolve.cpp @@ -1,9 +1,9 @@ #include "Dissolve.h" +#include "Engine/Materials/Nodes/Color.h" #include "Engine/Materials/Nodes/Parameter.h" #include -#include #include #include @@ -16,20 +16,21 @@ using namespace Elixir::Materials::Nodes; -Ref pipeline; std::array, 2> m_ParticleSystems; std::array, 2> m_ParticleSystemInstances; Ref graphMaterial; +Ref mesh; +SStaticMeshLighting s_LightingSetup; + Dissolve::Dissolve() { EE_PROFILE_ZONE_SCOPED() m_Window->SetTitle("Dissolve"); - m_DrawExtent = m_Window->GetFramebufferExtent(); - - const auto aspectRatio = (float)m_DrawExtent.Width / (float)m_DrawExtent.Height; + const auto drawExtent = m_Window->GetFramebufferExtent(); + const auto aspectRatio = (float)drawExtent.Width / (float)drawExtent.Height; // m_CameraController = CreateScope(aspectRatio); // m_CameraController->AddKeyframe({ { 0.0f, 0.5f, 5.0f }, { 0.0f, 0.0f, 0.0f }, 60.0f }); // m_CameraController->AddKeyframe({ { 1.0f, 0.3f, 2.5f }, { 0.0f, 0.0f, 0.0f }, 50.0f }); @@ -38,34 +39,36 @@ Dissolve::Dissolve() // m_CameraController->SetLooping(true); // m_CameraController->Play(); m_CameraController = CreateScope(60.0f, aspectRatio); - m_FrameData.ViewProj = m_CameraController->GetCamera().GetViewProjectionMatrix(); - - const auto sampler = SamplerBuilder() - .Build(m_GraphicsContext.get()); - const auto tex = TextureLoader::Load("./Assets/Bricks.png"); - const auto shader = m_ShaderLoader->LoadShader("./Shaders/", "FixedTriangle"); - shader->BindTexture("texture", tex); - shader->BindSampler("sampl", sampler); - - PipelineBuilder builder; - builder.SetShader(shader); - builder.SetInputTopology(EPrimitiveTopology::TriangleList); - builder.SetPolygonMode(EPolygonMode::Fill); - builder.DisableBlending(); - builder.DisableDepthTest(); - builder.SetColorAttachmentFormat(EImageFormat::R8G8B8A8_SRGB); - builder.SetBufferLayout({}); - pipeline = builder.Build(m_GraphicsContext.get()); - - m_FrameConstantBuffer = UniformBuffer::Create( + m_Environment = Environment::Load( + *m_GraphicsContext, + "./Assets/Textures/white_home_studio_2k.hdr" + ); + EE_CORE_ASSERT(m_Environment, "Could not load the Environment.") + + m_StaticMeshRenderer = CreateScope( m_GraphicsContext.get(), - sizeof(SFrameData), - &m_FrameData + GetMaterialSystem(), + StaticMeshLoaderRegistry::GetGeometryPool() ); - shader->BindConstantBuffer("cbFrame", m_FrameConstantBuffer); + s_LightingSetup = { + .Environment = { + .Environment = m_Environment->GetEnvironment(), + .Irradiance = m_Environment->GetIrradiance(), + .Prefiltered = m_Environment->GetPrefiltered(), + .Sampler = m_Environment->GetSampler(), + .Intensity = 1.5f, + .MaxLod = Environment::GetMaxLod(), + }, + .DirectionalLightDirection = glm::normalize( + glm::vec3{ -0.5f, 0.65f, -0.55f } + ), + .DirectionalLightColor = { 1.0f, 0.96f, 0.9f }, + .DirectionalLightIntensity = 2.2f, + .DebugView = ESurfaceDebugView::Composite, + }; m_ParticleSystems[0] = GetAetherManager().LoadEffect("./Assets/VFX/FireAndFireworks.json"); EE_CORE_ASSERT( @@ -82,10 +85,10 @@ Dissolve::Dissolve() { MaterialGraph graph; - graphMaterial = CreateRef("DissolveGraph"); + graphMaterial = CreateRef("DissolveGraph", EMaterialUsage::Particle); EE_CORE_ASSERT( - graphMaterial->SetUsage(EMaterialUsage::ParticleSprite, true), - "Dissolve graph material must enable ParticleSprite usage." + graphMaterial->SetBlendMode(EMaterialBlendMode::Translucent), + "Dissolve graph material must enable translucent blending." ) EE_CORE_ASSERT(graphMaterial->DefineParameter("Tint", { @@ -126,52 +129,37 @@ Dissolve::Dissolve() { EE_CORE_ERROR("Dissolve particle emitter 'FlameCore' was not found.") } + + if (auto loaded = StaticMeshLoaderRegistry::Load("./Assets/Meshes/McLaren/scene.gltf")) + mesh = std::move(*loaded); } { MaterialGraph graph1; - const auto ribbonMaterial = CreateRef("RibbonEnergy"); - EE_CORE_ASSERT( - ribbonMaterial->SetUsage(EMaterialUsage::ParticleRibbon, true), - "Ribbon material must enable ParticleRibbon usage." - ) + const auto ribbonMaterial = CreateRef("RibbonEnergy", EMaterialUsage::Particle); EE_CORE_ASSERT( - ribbonMaterial->SetUsage(EMaterialUsage::ParticleMesh, true), - "Ribbon material must enable ParticleRibbon usage." + ribbonMaterial->SetBlendMode(EMaterialBlendMode::Translucent), + "Ribbon material must enable translucent blending." ) EE_CORE_ASSERT(ribbonMaterial->DefineParameter("Tint", { .Kind = EMaterialParameterKind::Value, .ValueType = EMaterialValueType::Float4, - .DefaultValue = SMaterialParameter::MakeVector({ 0.2f, 0.5f, 1.0f, 1.0f }), - }), "") - - EE_CORE_ASSERT(ribbonMaterial->DefineParameter("Glow", { - .Kind = EMaterialParameterKind::Value, - .ValueType = EMaterialValueType::Float4, - .DefaultValue = SMaterialParameter::MakeVector({ 0.05f, 0.2f, 1.0f, 1.0f }), + .DefaultValue = SMaterialParameter::MakeVector({ 1.0f, 1.0f, 1.0f, 1.0f }), }), "") - EE_CORE_ASSERT(ribbonMaterial->DefineParameter("Albedo", { - .Kind = EMaterialParameterKind::Texture, - .DefaultValue = SMaterialParameter::MakeTexture(tex), - }), "") - - const auto panner = graph1.AddNode(glm::vec2{ 0.08f, -0.35f }); - - const auto albedo1 = graph1.AddNode("Albedo"); - graph1.Connect(panner, albedo1, 0); - - const auto tint1 = graph1.AddNode( + const auto tint = graph1.AddNode( "Tint", EMaterialValueType::Float4 ); + const auto color = graph1.AddNode(); + const auto multiply = graph1.AddNode(); - graph1.Connect(albedo1, multiply, 0); - graph1.Connect(tint1, multiply, 1); - graph1.SetChannel(EMaterialChannel::BaseColor, albedo1); + graph1.Connect(tint, multiply, 0); + graph1.Connect(color, multiply, 1); + graph1.SetChannel(EMaterialChannel::BaseColor, multiply); // const auto glow = graph1.AddNode("Glow", EMaterialValueType::Float4); //graph1.SetChannel(EMaterialChannel::Emissive, glow); @@ -179,23 +167,11 @@ Dissolve::Dissolve() ribbonMaterial->SetGraph(std::move(graph1)); EE_CORE_ASSERT(GetMaterialRegistry().Register(ribbonMaterial), "RibbonEnergy must be unique.") - const auto instance = ribbonMaterial->CreateInstance(); - EE_CORE_ASSERT( - instance->SetVector("Tint", { 0.15f, 0.6f, 1.0f, 1.0f }), - "Ribbon tint override must match the schema." - ) - if (auto* emitter = m_ParticleSystems[1]->FindEmitter("PathRibbon")) { - emitter->SetMaterial(instance); + //emitter->SetMaterial(ribbonMaterial); EE_CORE_INFO("Published graph material to the PathRibbon particle emitter.") } - - if (auto* emitter = m_ParticleSystems[1]->FindEmitter("CrystalShards")) - { - emitter->SetMaterial(instance); - EE_CORE_INFO("Published graph material to the CrystalShards particle emitter.") - } } m_ParticleSystemInstances[0] = m_ParticleSystems[0]->CreateInstance(); @@ -216,7 +192,12 @@ Dissolve::Dissolve() Dissolve::~Dissolve() { - pipeline.reset(); + m_StaticMeshRenderer.reset(); + mesh.reset(); + s_LightingSetup = {}; + graphMaterial.reset(); + m_ParticleSystemInstances = {}; + m_ParticleSystems = {}; } void Dissolve::OnGUI(const Timestep frameTime) @@ -237,19 +218,16 @@ void Dissolve::Render(const Timestep frameTime) Application::Render(frameTime); m_CameraController->Update(frameTime); - m_FrameData.ViewProj = m_CameraController->GetCamera().GetViewProjectionMatrix(); - m_FrameConstantBuffer->UpdateData(&m_FrameData, sizeof(SFrameData)); - auto& aether = GetAetherManager(); aether.BeginFrame(frameTime); m_GraphicsContext->Clear(); - //DrawGeometry(); + m_StaticMeshRenderer->BeginFrame(m_CameraController->GetCamera(), s_LightingSetup); + DrawGeometry(); + m_StaticMeshRenderer->EndFrame(); - aether.Render(m_CameraController->GetCamera()); - const auto& simulationMetrics = aether.GetLastSimulationMetrics(); - const auto& renderMetrics = aether.GetLastRenderingMetrics(); + aether.Render(m_CameraController->GetCamera(), m_SceneTarget); } void Dissolve::OnEvent(Event& event) @@ -260,37 +238,10 @@ void Dissolve::OnEvent(Event& event) void Dissolve::DrawGeometry() { - const auto renderingInfo = SRenderingInfo - { - .ColorAttachment = m_GraphicsContext->GetRenderTarget(), - .RenderArea = m_DrawExtent - }; - - Viewport viewport = {}; - viewport.X = 0; - viewport.Y = 0; - viewport.Width = m_DrawExtent.Width; - viewport.Height = m_DrawExtent.Height; - viewport.MinDepth = 0.0f; - viewport.MaxDepth = 1.0f; - - Rect2D scissor = {}; - scissor.Offset = { 0, 0 }; - scissor.Extent = m_DrawExtent; - - m_Executor.Enqueue([this, renderingInfo, viewport, scissor]() - { - const auto cmd = this->m_GraphicsContext->GetSecondaryCommandBuffer(); - cmd->BeginRendering(renderingInfo); - cmd->SetViewports({ viewport }); - cmd->SetScissors({ scissor }); - pipeline->Bind(cmd); - cmd->Draw(3); - cmd->EndRendering(); - this->m_GraphicsContext->EnqueueSecondaryCommandBuffer(cmd); - }, &m_WaitGroup); - - m_WaitGroup.Wait(); + if (m_StaticMeshRenderer) + { + m_StaticMeshRenderer->Render(mesh); + } } Application* Elixir::CreateApplication() diff --git a/Dissolve/Source/Dissolve.h b/Dissolve/Source/Dissolve.h index d7c3c3e7..c79c2a46 100644 --- a/Dissolve/Source/Dissolve.h +++ b/Dissolve/Source/Dissolve.h @@ -2,10 +2,7 @@ #include -struct SFrameData -{ - glm::mat4 ViewProj; -}; +#include "Environment.h" class Dissolve final : public Elixir::Application { @@ -22,11 +19,9 @@ class Dissolve final : public Elixir::Application private: void DrawGeometry(); - WaitGroup m_WaitGroup; - Extent2D m_DrawExtent; + Scope m_Environment; - SFrameData m_FrameData; - Ref m_FrameConstantBuffer; + Scope m_StaticMeshRenderer; Scope m_CameraController; -}; \ No newline at end of file +}; diff --git a/Dissolve/Source/Environment.cpp b/Dissolve/Source/Environment.cpp new file mode 100644 index 00000000..51add99f --- /dev/null +++ b/Dissolve/Source/Environment.cpp @@ -0,0 +1,243 @@ +#include "Environment.h" + +#include +#include +#include + +#include + +#include +#include +#include + +namespace +{ + constexpr float Pi = 3.14159265359f; + constexpr float TwoPi = 6.28318530718f; + constexpr float InversePi = 0.31830988618f; + constexpr float InverseTwoPi = 0.15915494309f; + + constexpr uint32_t IrradianceWidth = 48; + constexpr uint32_t IrradianceHeight = 24; + constexpr uint32_t IrradianceSamples = 256; + constexpr uint32_t PrefilterWidth = 128; + constexpr uint32_t PrefilterHeight = 64; + constexpr uint32_t PrefilterSamples = 256; + + glm::vec3 EquirectangularToDirection(const float u, const float v) + { + const float phi = (u - 0.5f) * TwoPi; + const float theta = v * Pi; + const float sinTheta = std::sin(theta); + return { sinTheta * std::cos(phi), std::cos(theta), sinTheta * std::sin(phi) }; + } + + glm::vec3 SampleEquirectangular( + const float* pixels, + const int width, + const int height, + const glm::vec3& direction + ) + { + const float u = std::atan2(direction.z, direction.x) * InverseTwoPi + 0.5f; + const float v = std::acos(std::clamp(direction.y, -1.0f, 1.0f)) * InversePi; + const int x = std::clamp(static_cast(u * width), 0, width - 1); + const int y = std::clamp(static_cast(v * height), 0, height - 1); + const float* pixel = pixels + static_cast(y * width + x) * 4; + return { pixel[0], pixel[1], pixel[2] }; + } + + float RadicalInverse(uint32_t bits) + { + bits = (bits << 16u) | (bits >> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); + bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); + bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); + return static_cast(bits) * 2.3283064365386963e-10f; + } + + glm::vec3 ImportanceSampleGGX( + const float xi0, + const float xi1, + const glm::vec3& normal, + const float roughness + ) + { + const float alpha = roughness * roughness; + const float phi = TwoPi * xi0; + const float cosTheta = std::sqrt((1.0f - xi1) / (1.0f + (alpha * alpha - 1.0f) * xi1)); + const float sinTheta = std::sqrt(std::max(0.0f, 1.0f - cosTheta * cosTheta)); + const glm::vec3 halfVector( + sinTheta * std::cos(phi), + sinTheta * std::sin(phi), + cosTheta + ); + + const glm::vec3 up = std::abs(normal.z) < 0.999f + ? glm::vec3(0.0f, 0.0f, 1.0f) + : glm::vec3(1.0f, 0.0f, 0.0f); + const glm::vec3 tangent = glm::normalize(glm::cross(up, normal)); + const glm::vec3 bitangent = glm::cross(normal, tangent); + return glm::normalize( + tangent * halfVector.x + bitangent * halfVector.y + normal * halfVector.z + ); + } + + std::vector BakeIrradiance(const float* pixels, const int width, const int height) + { + std::vector irradiance(IrradianceWidth * IrradianceHeight); + + for (uint32_t y = 0; y < IrradianceHeight; ++y) + { + for (uint32_t x = 0; x < IrradianceWidth; ++x) + { + const float u = (static_cast(x) + 0.5f) / IrradianceWidth; + const float v = (static_cast(y) + 0.5f) / IrradianceHeight; + const glm::vec3 normal = glm::normalize(EquirectangularToDirection(u, v)); + const glm::vec3 up = std::abs(normal.y) < 0.999f + ? glm::vec3(0.0f, 1.0f, 0.0f) + : glm::vec3(1.0f, 0.0f, 0.0f); + const glm::vec3 tangent = glm::normalize(glm::cross(up, normal)); + const glm::vec3 bitangent = glm::cross(normal, tangent); + + glm::vec3 sum(0.0f); + for (uint32_t sample = 0; sample < IrradianceSamples; ++sample) + { + const float xi0 = static_cast(sample) / IrradianceSamples; + const float xi1 = RadicalInverse(sample); + const float phi = TwoPi * xi0; + const float cosTheta = std::sqrt(1.0f - xi1); + const float sinTheta = std::sqrt(xi1); + const glm::vec3 sampleDirection = + tangent * (sinTheta * std::cos(phi)) + + bitangent * (sinTheta * std::sin(phi)) + + normal * cosTheta; + sum += SampleEquirectangular(pixels, width, height, sampleDirection); + } + + irradiance[y * IrradianceWidth + x] = glm::vec4( + sum / static_cast(IrradianceSamples), + 1.0f + ); + } + } + + return irradiance; + } + + std::vector BakePrefiltered( + const float* pixels, + const int width, + const int height + ) + { + std::vector prefiltered( + PrefilterWidth * PrefilterHeight * Environment::PrefilterLevels + ); + + for (uint32_t level = 0; level < Environment::PrefilterLevels; ++level) + { + const float roughness = static_cast(level) / (Environment::PrefilterLevels - 1); + for (uint32_t y = 0; y < PrefilterHeight; ++y) + { + for (uint32_t x = 0; x < PrefilterWidth; ++x) + { + const float u = (static_cast(x) + 0.5f) / PrefilterWidth; + const float v = (static_cast(y) + 0.5f) / PrefilterHeight; + const glm::vec3 normal = glm::normalize(EquirectangularToDirection(u, v)); + const glm::vec3 view = normal; + + glm::vec3 sum(0.0f); + float totalWeight = 0.0f; + for (uint32_t sample = 0; sample < PrefilterSamples; ++sample) + { + const float xi0 = static_cast(sample) / PrefilterSamples; + const float xi1 = RadicalInverse(sample); + const glm::vec3 halfVector = ImportanceSampleGGX( + xi0, + xi1, + normal, + roughness + ); + const glm::vec3 light = glm::normalize( + 2.0f * glm::dot(view, halfVector) * halfVector - view + ); + const float normalDotLight = std::max(glm::dot(normal, light), 0.0f); + if (normalDotLight > 0.0f) + { + sum += SampleEquirectangular(pixels, width, height, light) * normalDotLight; + totalWeight += normalDotLight; + } + } + + const glm::vec3 color = totalWeight > 0.0f + ? sum / totalWeight + : SampleEquirectangular(pixels, width, height, normal); + const size_t index = (static_cast(level) * PrefilterHeight + y) * PrefilterWidth + x; + prefiltered[index] = glm::vec4(color, 1.0f); + } + } + } + + return prefiltered; + } +} + +Scope Environment::Load( + const GraphicsContext& context, + const std::filesystem::path& path +) +{ + int width = 0; + int height = 0; + int channels = 0; + float* pixels = stbi_loadf(path.string().c_str(), &width, &height, &channels, STBI_rgb_alpha); + if (!pixels) + { + EE_CORE_ERROR("Failed to load HDR environment '{0}': {1}", path.string(), stbi_failure_reason()) + return nullptr; + } + + auto environment = Scope(new Environment()); + const auto* graphicsContext = &context; + environment->m_Environment = Texture2D::Create( + graphicsContext, + EImageFormat::R32G32B32A32_SFLOAT, + static_cast(width), + static_cast(height), + pixels, + path.string() + ); + + const auto irradiance = BakeIrradiance(pixels, width, height); + environment->m_Irradiance = Texture2D::Create( + graphicsContext, + EImageFormat::R32G32B32A32_SFLOAT, + IrradianceWidth, + IrradianceHeight, + irradiance.data() + ); + + const auto prefiltered = BakePrefiltered(pixels, width, height); + environment->m_Prefiltered = Texture2D::Create( + graphicsContext, + EImageFormat::R32G32B32A32_SFLOAT, + PrefilterWidth, + PrefilterHeight * PrefilterLevels, + prefiltered.data() + ); + stbi_image_free(pixels); + + environment->m_Sampler = SamplerBuilder() + .SetMagFilter(ESamplerFilter::Linear) + .SetMinFilter(ESamplerFilter::Linear) + .SetMaxLod(GetMaxLod()) + .SetAddressModeU(ESamplerAddressMode::Repeat) + .SetAddressModeV(ESamplerAddressMode::ClampToEdge) + .SetAddressModeW(ESamplerAddressMode::ClampToEdge) + .Build(graphicsContext); + + EE_CORE_INFO("Loaded HDR environment '{0}' ({1}x{2}).", path.filename().string(), width, height) + return environment; +} diff --git a/Dissolve/Source/Environment.h b/Dissolve/Source/Environment.h new file mode 100644 index 00000000..dd54c391 --- /dev/null +++ b/Dissolve/Source/Environment.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include + +#include + +namespace Elixir +{ + class GraphicsContext; +} + +/** + * @brief Stores image-based lighting textures used by Dissolve. + * + * The textures use equirectangular projection. The irradiance and prefiltered + * textures are derived from the source HDR environment when it is loaded. + */ +class Environment final +{ + public: + /** @brief Number of roughness levels stored in the prefiltered texture. */ + static constexpr uint32_t PrefilterLevels = 6; + + /** + * @brief Load an HDR environment and create its lighting textures. + * @param context Graphics context used to create the textures and sampler. + * @param path Path to an HDR equirectangular image. + * @return The loaded environment, or null when the image cannot be loaded. + */ + static Scope Load( + const GraphicsContext& context, + const std::filesystem::path& path + ); + + /** @brief Return the source HDR environment texture. */ + const Ref& GetEnvironment() const { return m_Environment; } + + /** @brief Return the diffuse irradiance texture derived from the environment. */ + const Ref& GetIrradiance() const { return m_Irradiance; } + + /** @brief Return the roughness-filtered reflection texture. */ + const Ref& GetPrefiltered() const { return m_Prefiltered; } + + /** @brief Return the sampler shared by the environment textures. */ + const Ref& GetSampler() const { return m_Sampler; } + + /** @brief Return the highest roughness level in the prefiltered texture. */ + static float GetMaxLod() { return static_cast(PrefilterLevels - 1); } + + private: + Environment() = default; + + Ref m_Environment; + Ref m_Irradiance; + Ref m_Prefiltered; + Ref m_Sampler; +}; diff --git a/Elixir/Source/Engine.h b/Elixir/Source/Engine.h index 3ca022df..df7fae42 100644 --- a/Elixir/Source/Engine.h +++ b/Elixir/Source/Engine.h @@ -60,5 +60,10 @@ #include #include +#include +#include +#include +#include + #include #include diff --git a/Elixir/Source/Engine/Aether/Effect/MaterialFactory.cpp b/Elixir/Source/Engine/Aether/Effect/MaterialFactory.cpp index 43aa3aaa..104e2627 100644 --- a/Elixir/Source/Engine/Aether/Effect/MaterialFactory.cpp +++ b/Elixir/Source/Engine/Aether/Effect/MaterialFactory.cpp @@ -3,40 +3,28 @@ #include -#include -#include #include -#include #include #include +#include #include namespace Elixir::Aether::Effect { using namespace Elixir::Materials::Nodes; - EMaterialUsage GetMaterialUsage(const Core::EParticleRenderMode mode) - { - switch (mode) - { - case Core::EParticleRenderMode::Sprite: return EMaterialUsage::ParticleSprite; - case Core::EParticleRenderMode::Ribbon: return EMaterialUsage::ParticleRibbon; - case Core::EParticleRenderMode::Mesh: return EMaterialUsage::ParticleMesh; - } - - return EMaterialUsage::ParticleSprite; - } - Ref CreateMaterial( std::string name, - const Core::EParticleRenderMode renderMode, const SMaterialDescription& desc ) { - const auto material = CreateRef(std::move(name)); + const auto material = CreateRef( + std::move(name), + EMaterialUsage::Particle + ); - const auto result = material->SetUsage(GetMaterialUsage(renderMode), true); - EE_CORE_ASSERT(result, "Particle material usage must be enabled.") + const auto result = material->SetBlendMode(EMaterialBlendMode::Translucent); + EE_CORE_ASSERT(result, "Particle materials must enable translucent blending.") MaterialGraph graph; @@ -58,8 +46,21 @@ namespace Elixir::Aether::Effect ); graph.SetChannel(EMaterialChannel::Emissive, emissive); - if (renderMode == Core::EParticleRenderMode::Sprite && - !desc.BaseColorTexturePath.empty()) + const auto color = graph.AddNode(); + const auto particleAlpha = graph.AddNode(3); + graph.Connect(color, particleAlpha, 0); + + const auto coloredBase = graph.AddNode(); + graph.Connect(baseColor, coloredBase, 0); + graph.Connect(color, coloredBase, 1); + graph.SetChannel(EMaterialChannel::BaseColor, coloredBase); + + const auto coloredOpacity = graph.AddNode(); + graph.Connect(opacity, coloredOpacity, 0); + graph.Connect(particleAlpha, coloredOpacity, 1); + graph.SetChannel(EMaterialChannel::Opacity, coloredOpacity); + + if (!desc.BaseColorTexturePath.empty()) { constexpr auto texParam = "BaseColorTexture"; const auto tex = TextureLoader::Load(desc.BaseColorTexturePath); @@ -73,12 +74,12 @@ namespace Elixir::Aether::Effect graph.Connect(texture, alpha, 0); const auto baseColorMul = graph.AddNode(); - graph.Connect(baseColor, baseColorMul, 0); + graph.Connect(coloredBase, baseColorMul, 0); graph.Connect(texture, baseColorMul, 1); graph.SetChannel(EMaterialChannel::BaseColor, baseColorMul); const auto opacityMul = graph.AddNode(); - graph.Connect(opacity, opacityMul, 0); + graph.Connect(coloredOpacity, opacityMul, 0); graph.Connect(alpha, opacityMul, 1); graph.SetChannel(EMaterialChannel::Opacity, opacityMul); } diff --git a/Elixir/Source/Engine/Aether/Effect/MaterialFactory.h b/Elixir/Source/Engine/Aether/Effect/MaterialFactory.h index babe086d..991f50b4 100644 --- a/Elixir/Source/Engine/Aether/Effect/MaterialFactory.h +++ b/Elixir/Source/Engine/Aether/Effect/MaterialFactory.h @@ -8,29 +8,18 @@ namespace Elixir::Aether::Effect { using namespace Materials; - /** - * @brief Returns the material usage required by an Aether render mode. - * - * @param mode Particle geometry mode selected by an emitter. - * @return Material usage compatible with the selected render mode. - * - * @note An invalid enum value falls back to EMaterialUsage::ParticleSprite. - */ - EMaterialUsage GetMaterialUsage(Core::EParticleRenderMode mode); - /** * @brief Creates a material from Aether effect authoring data. * - * The function creates a raw Material with the usage required by renderMode and - * builds its material graph from desc. BaseColor, Opacity, and Emissive become + * The function creates a raw Material with Particle usage and builds its material + * graph from desc. BaseColor, Opacity, and Emissive become * constant graph inputs. * - * For sprite emitters, a non-empty BaseColorTexturePath creates a texture + * A non-empty BaseColorTexturePath creates a texture * parameter and multiplies its sampled RGB and alpha values into BaseColor and * Opacity, respectively. * * @param name Name assigned to the created material. - * @param renderMode Particle geometry mode that determines material usage. * @param desc Serialized material data from the effect asset. * @return A new unregistered Material. * @@ -38,7 +27,6 @@ namespace Elixir::Aether::Effect */ Ref CreateMaterial( std::string name, - Core::EParticleRenderMode renderMode, const SMaterialDescription& desc ); } diff --git a/Elixir/Source/Engine/Aether/Effect/MaterialResolver.cpp b/Elixir/Source/Engine/Aether/Effect/MaterialResolver.cpp index bf6a7732..8f344fb0 100644 --- a/Elixir/Source/Engine/Aether/Effect/MaterialResolver.cpp +++ b/Elixir/Source/Engine/Aether/Effect/MaterialResolver.cpp @@ -14,21 +14,32 @@ namespace Elixir::Aether::Effect { for (const auto& emitter : system.GetEmitters()) { - // A caller may replace an effect-authored instance before creating a - // SystemInstance. Do not overwrite that explicit choice. - if (emitter->GetMaterial()) - continue; - Ref material; if (const auto& desc = emitter->GetMaterialDescription()) { const auto name = "Aether." + system.GetId() + "." + emitter->GetName(); + // Preserve a caller-selected material, but recreate the material + // previously generated for this emitter when its asset data changes. + if (emitter->GetMaterial() && !emitter->HasResolvedMaterial()) + continue; + material = m_Registry.Find(name); - if (!material) + const auto refreshed = CreateMaterial(name, *desc); + if (material) { - material = CreateMaterial(name, emitter->GetRenderMode(), *desc); + if (!m_Registry.Replace(refreshed)) + { + EE_CORE_ERROR("Aether material '{}' could not be refreshed.", name) + return false; + } + + material = refreshed; + } + else + { + material = std::move(refreshed); if (!m_Registry.Register(material)) { EE_CORE_ERROR("Aether material '{}' could not be registered.", name) @@ -38,10 +49,15 @@ namespace Elixir::Aether::Effect } else { - material = m_Registry.GetDefault(GetMaterialUsage(emitter->GetRenderMode())); + // A caller may replace a default instance before creating a + // SystemInstance. Do not overwrite that explicit choice. + if (emitter->GetMaterial() && !emitter->HasResolvedMaterial()) + continue; + + material = m_Registry.GetDefault(EMaterialUsage::Particle); } - emitter->SetMaterial(material); + emitter->SetResolvedMaterial(material->CreateInstance()); if (!emitter->GetMaterial()) return false; } diff --git a/Elixir/Source/Engine/Aether/Emitter.h b/Elixir/Source/Engine/Aether/Emitter.h index 251b338d..e5e39ba7 100644 --- a/Elixir/Source/Engine/Aether/Emitter.h +++ b/Elixir/Source/Engine/Aether/Emitter.h @@ -12,6 +12,8 @@ namespace Elixir::Aether { + namespace Effect { class MaterialResolver; } + using namespace Core; using namespace Modules; using namespace Materials; @@ -228,7 +230,11 @@ namespace Elixir::Aether * @param material Material instance to assign. * @note Pass a null reference to clear the current selection. */ - void SetMaterial(Ref material) { m_Material = std::move(material); } + void SetMaterial(Ref material) + { + m_Material = std::move(material); + m_MaterialIsResolved = false; + } /** * @brief Configure periodic burst emission. @@ -321,6 +327,18 @@ namespace Elixir::Aether void SetSpawnRateParamName(const std::string& paramName) { m_SpawnRateParamName = paramName; } private: + friend class Effect::MaterialResolver; + + /** Assigns an instance created by the effect material resolver. */ + void SetResolvedMaterial(Ref material) + { + m_Material = std::move(material); + m_MaterialIsResolved = true; + } + + /** Checks whether the current material was assigned by the effect resolver. */ + bool HasResolvedMaterial() const { return m_MaterialIsResolved; } + // Compiles this emitter into internal GPU-ready runtime data. SCompiledEmitter Compile( const ParameterStore& paramStore, @@ -334,6 +352,7 @@ namespace Elixir::Aether EParticleSimulationSpace m_SimulationSpace = EParticleSimulationSpace::World; std::optional m_MaterialDescription; Ref m_Material; + bool m_MaterialIsResolved = false; uint32_t m_MaxParticles; std::vector> m_SpawnModules; diff --git a/Elixir/Source/Engine/Aether/Manager.cpp b/Elixir/Source/Engine/Aether/Manager.cpp index 2c3f138c..3c4a5e38 100644 --- a/Elixir/Source/Engine/Aether/Manager.cpp +++ b/Elixir/Source/Engine/Aether/Manager.cpp @@ -1,6 +1,7 @@ #include "epch.h" #include "Manager.h" +#include #include #include #include @@ -56,17 +57,19 @@ namespace Elixir::Aether RetireDestroyedInstances(); } - void Manager::Render(const Camera& camera) + void Manager::Render(const Camera& camera, const Ref& sceneTarget) { const auto submission = GetRuntime().AcquireSubmission(); if (!submission) return; + EE_CORE_ASSERT(sceneTarget, "Aether rendering requires a scene target.") + const auto cmd = m_GraphicsContext->GetSecondaryCommandBuffer(); cmd->Begin({ - .ColorAttachment = m_GraphicsContext->GetRenderTarget(), + .ColorAttachment = sceneTarget, .DepthStencilAttachment = m_GraphicsContext->GetDepthStencilRenderTarget(), - .RenderArea = m_GraphicsContext->GetRenderTarget()->GetExtent(), + .RenderArea = sceneTarget->GetExtent(), }); const auto frame = GetSimulator().Simulate(*submission, cmd); diff --git a/Elixir/Source/Engine/Aether/Manager.h b/Elixir/Source/Engine/Aether/Manager.h index 015dc1c7..d7f45b4a 100644 --- a/Elixir/Source/Engine/Aether/Manager.h +++ b/Elixir/Source/Engine/Aether/Manager.h @@ -7,6 +7,7 @@ namespace Elixir { class Camera; class GraphicsContext; + class Image; class ShaderLoader; class Timestep; @@ -171,10 +172,11 @@ namespace Elixir::Aether * The method does nothing when no submission is available. * * @param camera Camera used to render particle geometry. + * @param sceneTarget Color target that receives particle geometry. * * @note Call this method from the render-frame path after BeginFrame(). */ - void Render(const Camera& camera); + void Render(const Camera& camera, const Ref& sceneTarget); /** * @brief Returns statistics from the most recent particle simulation. diff --git a/Elixir/Source/Engine/Aether/Rendering/Renderer.cpp b/Elixir/Source/Engine/Aether/Rendering/Renderer.cpp index 44a3c3ed..2695c8e0 100644 --- a/Elixir/Source/Engine/Aether/Rendering/Renderer.cpp +++ b/Elixir/Source/Engine/Aether/Rendering/Renderer.cpp @@ -60,7 +60,7 @@ namespace Elixir::Aether::Rendering m_FrameData.Time = frame.GetElapsedTimeSeconds(); m_FrameConstantBuffer->UpdateData(&m_FrameData, sizeof(m_FrameData)); - auto scene = BuildScene(frame); + auto scene = BuildScene(frame, camera); m_LastMetrics.SubmittedMaterialCount = scene.GetItems().size(); return scene; @@ -199,10 +199,15 @@ namespace Elixir::Aether::Rendering return nullptr; } - MaterialRenderScene Renderer::BuildScene(const RenderFrame& frame) const + MaterialRenderScene Renderer::BuildScene( + const RenderFrame& frame, + const Camera& camera + ) const { MaterialRenderScene scene; + const auto view = camera.GetViewMatrix(); + static const BufferLayout ribbonVertexLayout; struct SGeometryIndices @@ -290,6 +295,11 @@ namespace Elixir::Aether::Rendering const auto geometry = getGeometry(item.ParticleStateLayout); if (!geometry) continue; + const float sortDepth = -( + view * item.WorldTransform * + glm::vec4(0.0f, 0.0f, 0.0f, 1.0f) + ).z; + switch (item.RenderMode) { case EParticleRenderMode::Sprite: @@ -301,6 +311,7 @@ namespace Elixir::Aether::Rendering scene.Add({ .Pass = EMaterialPass::ParticleSprite, .Material = item.Material, + .SortDepth = sortDepth, .GeometryIndex = geometry->Sprite, .PushConstants = SMaterialPushConstants::Create( constants, @@ -327,6 +338,7 @@ namespace Elixir::Aether::Rendering scene.Add({ .Pass = EMaterialPass::ParticleRibbon, .Material = item.Material, + .SortDepth = sortDepth, .GeometryIndex = geometry->Ribbon, .PushConstants = SMaterialPushConstants::Create( constants, @@ -346,6 +358,7 @@ namespace Elixir::Aether::Rendering scene.Add({ .Pass = EMaterialPass::ParticleMesh, .Material = item.Material, + .SortDepth = sortDepth, .GeometryIndex = geometry->Mesh, .PushConstants = SMaterialPushConstants::Create( constants, diff --git a/Elixir/Source/Engine/Aether/Rendering/Renderer.h b/Elixir/Source/Engine/Aether/Rendering/Renderer.h index 2f29e35c..149afecc 100644 --- a/Elixir/Source/Engine/Aether/Rendering/Renderer.h +++ b/Elixir/Source/Engine/Aether/Rendering/Renderer.h @@ -111,7 +111,10 @@ namespace Elixir::Aether::Rendering ); // Converts particle render items into material geometry and draw commands. - MaterialRenderScene BuildScene(const RenderFrame& frame) const; + MaterialRenderScene BuildScene( + const RenderFrame& frame, + const Camera& camera + ) const; SFrameData m_FrameData{}; Ref m_FrameConstantBuffer; diff --git a/Elixir/Source/Engine/Aether/Simulation/Simulator.cpp b/Elixir/Source/Engine/Aether/Simulation/Simulator.cpp index 5a02ee78..52bc8608 100644 --- a/Elixir/Source/Engine/Aether/Simulation/Simulator.cpp +++ b/Elixir/Source/Engine/Aether/Simulation/Simulator.cpp @@ -650,6 +650,15 @@ namespace Elixir::Aether::Simulation const auto allocation = m_ResourcePool.Allocate(system); if (!allocation) { + // A replacement can fail to fit beside its previous allocation even + // when it fits by itself. Retire the stale allocation so a later + // frame can retry after this frame slot's fence completes. + if (found != m_InstanceRecords.end()) + { + QueueRetirement(found->second.Allocation); + m_InstanceRecords.erase(found); + } + if (m_AllocationFailures.insert(proxy.GetKey()).second) { EE_CORE_ERROR( @@ -682,7 +691,7 @@ namespace Elixir::Aether::Simulation } // The replacement is fully allocated and uploaded before retiring the - // previous record. If allocation fails, the old record remains intact. + // previous record. QueueRetirement(found->second.Allocation); found->second = replacement; m_AllocationFailures.erase(proxy.GetKey()); diff --git a/Elixir/Source/Engine/Core/Application.cpp b/Elixir/Source/Engine/Core/Application.cpp index 4c37f1e0..b775f655 100644 --- a/Elixir/Source/Engine/Core/Application.cpp +++ b/Elixir/Source/Engine/Core/Application.cpp @@ -12,9 +12,12 @@ #include #include #include +#include #include +#include #include #include +#include #include namespace Elixir @@ -35,8 +38,14 @@ namespace Elixir m_GraphicsContext = GraphicsContext::Create(EGraphicsAPI::Vulkan, &m_Executor, m_Window.get()); m_GraphicsContext->Init(); + CreateSceneTarget(m_GraphicsContext->GetSwapchainExtent()); m_ShaderLoader = CreateScope(m_GraphicsContext.get()); + m_PostProcessor = CreateRef( + m_GraphicsContext.get(), + m_ShaderLoader.get(), + m_SceneTarget->GetExtent() + ); TextureLoader::Initialize(m_GraphicsContext.get()); FontManager::Initialize(m_GraphicsContext.get()); @@ -46,9 +55,12 @@ namespace Elixir m_MaterialSystem = CreateScope( m_GraphicsContext.get(), m_ShaderLoader.get(), + m_SceneTarget, SMaterialSystemConfig{ .InitialFrameCapacity = 256 } ); + StaticMeshLoaderRegistry::Initialize(*m_GraphicsContext); + m_GUIManager = CreateScope(); m_GUIManager->Initialize( m_GraphicsContext.get(), @@ -146,6 +158,7 @@ namespace Elixir Application::~Application() { EE_PROFILE_ZONE_SCOPED() + StaticMeshLoaderRegistry::Shutdown(); IconManager::Shutdown(); FontManager::Shutdown(); Platform::Shutdown(); @@ -200,8 +213,10 @@ namespace Elixir m_GraphicsContext->RenderFrame([this, frameTime]() { m_MaterialSystem->BeginFrame(); + m_GraphicsContext->Clear(m_SceneTarget); Render(frameTime); m_MaterialSystem->RenderFrame(); + m_PostProcessor->Apply(m_SceneTarget, m_GraphicsContext->GetRenderTarget()); m_GUIManager->Render(); }); @@ -217,6 +232,7 @@ namespace Elixir EventDispatcher dispatcher(event); dispatcher.Dispatch(EE_BIND_EVENT_FN(Application::OnWindowClose)); dispatcher.Dispatch(EE_BIND_EVENT_FN(Application::OnWindowResize)); + dispatcher.Dispatch(EE_BIND_EVENT_FN(Application::OnFramebufferResize)); m_GraphicsContext->ProcessEvent(event); ::InputManager::OnEvent(event); @@ -277,4 +293,42 @@ namespace Elixir return false; } + + bool Application::OnFramebufferResize(const FramebufferResizeEvent& event) + { + const Extent2D extent = event.GetExtent(); + if (extent.Width == 0 || extent.Height == 0) + return false; + + // TODO: When GLFW is replaced with native window backends, defer target recreation + // until each platform reports that its live resize operation has finished. + m_GraphicsContext->EnqueueRenderTask([ + context = m_GraphicsContext.get(), + sceneTarget = m_SceneTarget, + postProcessor = m_PostProcessor, + extent + ]() + { + context->Resize(extent); + sceneTarget->Resize({ extent.Width, extent.Height, 1 }); + postProcessor->Resize({ extent.Width, extent.Height, 1 }); + }); + + return false; + } + + void Application::CreateSceneTarget(const Extent3D& extent) + { + m_SceneTarget = Image::Create(m_GraphicsContext.get(), { + .Width = extent.Width, + .Height = extent.Height, + .Depth = extent.Depth, + .Type = EImageType::_2D, + .Format = EImageFormat::R16G16B16A16_SFLOAT, + .Usage = EImageUsage::ColorAttachment | EImageUsage::Sampled | + EImageUsage::TransferSrc | EImageUsage::TransferDst, + .InitialLayout = EImageLayout::General, + }); + EE_CORE_ASSERT(m_SceneTarget, "Application could not create the HDR scene target.") + } } diff --git a/Elixir/Source/Engine/Core/Application.h b/Elixir/Source/Engine/Core/Application.h index fc7e1c49..194d5fe9 100644 --- a/Elixir/Source/Engine/Core/Application.h +++ b/Elixir/Source/Engine/Core/Application.h @@ -14,11 +14,14 @@ namespace Elixir { namespace GUI { class TextBlock; } namespace Aether { class Manager; } + namespace Materials { class MaterialSystem; class MaterialRegistry; } + + class PostProcessor; } namespace Elixir @@ -58,11 +61,17 @@ namespace Elixir protected: bool OnWindowClose(WindowCloseEvent& event); bool OnWindowResize(WindowResizeEvent& event); + bool OnFramebufferResize(const FramebufferResizeEvent& event); + + // Creates the application-owned HDR target used by world renderers. + void CreateSceneTarget(const Extent3D& extent); Executor& m_Executor; Scope m_Window; Scope m_GraphicsContext; + Ref m_SceneTarget; + Scope m_ShaderLoader; Scope m_GUIManager; @@ -71,6 +80,8 @@ namespace Elixir Scope m_AetherManager; + Ref m_PostProcessor; + Timer m_Timer; FrameProfiler m_Profiler; diff --git a/Elixir/Source/Engine/Core/Core.h b/Elixir/Source/Engine/Core/Core.h index df4a7541..f3062370 100644 --- a/Elixir/Source/Engine/Core/Core.h +++ b/Elixir/Source/Engine/Core/Core.h @@ -32,8 +32,8 @@ DEBUG_BREAK() \ } #else - #define EE_ASSERT(x, message, ...) - #define EE_CORE_ASSERT(x, message, ...) + #define EE_ASSERT(x, message, ...) x; + #define EE_CORE_ASSERT(x, message, ...) x; #endif // EE_ENABLE_ASSERTS #if defined(_MSC_VER) @@ -56,6 +56,13 @@ constexpr bool operator&(EnumClass lhs, EnumClass rhs) return (static_cast(lhs) & static_cast(rhs)) == static_cast(rhs); \ } \ \ +/** @brief Reports whether two flag sets share at least one flag. */ \ +constexpr bool HasAnyFlags(EnumClass lhs, EnumClass rhs) \ +{ \ + using T = std::underlying_type_t; \ + return (static_cast(lhs) & static_cast(rhs)) != 0; \ +} \ + \ constexpr EnumClass operator|(EnumClass lhs, EnumClass rhs) \ { \ using T = std::underlying_type_t; \ @@ -171,4 +178,4 @@ namespace Elixir ); } } -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/Core/Handle.h b/Elixir/Source/Engine/Core/Handle.h new file mode 100644 index 00000000..b9a66870 --- /dev/null +++ b/Elixir/Source/Engine/Core/Handle.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +namespace Elixir +{ + /** + * @brief Identifies a pooled resource while detecting stale references. + * @tparam T Resource type identified by this handle. + */ + template + struct SHandle + { + /** Slot occupied by the resource in its pool. */ + uint32_t Index = std::numeric_limits::max(); + + /** Version of the slot when this handle was created. */ + uint32_t Generation = 0; + + /** @brief Check whether this handle identifies a pool slot. */ + bool IsValid() const + { + return Index != std::numeric_limits::max(); + } + }; +} diff --git a/Elixir/Source/Engine/GUI/Renderer/Renderer.cpp b/Elixir/Source/Engine/GUI/Renderer/Renderer.cpp index f06f0ca3..0095117e 100644 --- a/Elixir/Source/Engine/GUI/Renderer/Renderer.cpp +++ b/Elixir/Source/Engine/GUI/Renderer/Renderer.cpp @@ -28,6 +28,15 @@ namespace Elixir::GUI void Renderer::Resize(const Extent2D& extent) { + if (!m_GraphicsContext->IsRenderThread() || m_GraphicsContext->IsFrameRecording()) + { + m_GraphicsContext->EnqueueRenderTask([this, extent]() + { + Resize(extent); + }); + return; + } + EE_CORE_ASSERT(extent.Width > 0 && extent.Height > 0, "Render extent must be greater than zero!") EE_CORE_INFO("Resizing GUI Renderer {}.", extent) @@ -129,23 +138,25 @@ namespace Elixir::GUI void Renderer::BeginRendering(const Ref& cmd) const { + const auto renderTarget = m_GraphicsContext->GetRenderTarget(); + const auto extent = renderTarget->GetExtent(); const auto renderingInfo = SRenderingInfo { - .ColorAttachment = m_GraphicsContext->GetRenderTarget(), - .RenderArea = m_RenderExtent + .ColorAttachment = renderTarget, + .RenderArea = extent }; Viewport viewport = {}; viewport.X = 0; viewport.Y = 0; - viewport.Width = m_RenderExtent.Width; - viewport.Height = m_RenderExtent.Height; + viewport.Width = extent.Width; + viewport.Height = extent.Height; viewport.MinDepth = 0.0f; viewport.MaxDepth = 1.0f; Rect2D scissor = {}; scissor.Offset = { 0, 0 }; - scissor.Extent = m_RenderExtent; + scissor.Extent = extent; cmd->BeginRendering(renderingInfo); cmd->SetViewports({ viewport }); @@ -163,4 +174,4 @@ namespace Elixir::GUI // Orthographic projection m_PerFrameData.Proj = glm::ortho(0.0f, (float)m_RenderExtent.Width, 0.0f, (float)m_RenderExtent.Height); } -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/GUI/Renderer/Renderer.h b/Elixir/Source/Engine/GUI/Renderer/Renderer.h index 68178eef..aea4ccb0 100644 --- a/Elixir/Source/Engine/GUI/Renderer/Renderer.h +++ b/Elixir/Source/Engine/GUI/Renderer/Renderer.h @@ -32,6 +32,7 @@ namespace Elixir::GUI const Extent2D& extent ); + /** @brief Updates GUI rendering state for a new framebuffer extent. */ void Resize(const Extent2D& extent); /** @@ -71,4 +72,4 @@ namespace Elixir::GUI Extent2D m_RenderExtent{}; const GraphicsContext* m_GraphicsContext; }; -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/Graphics/GraphicsContext.cpp b/Elixir/Source/Engine/Graphics/GraphicsContext.cpp index 3378a3bb..eb5e7b2b 100644 --- a/Elixir/Source/Engine/Graphics/GraphicsContext.cpp +++ b/Elixir/Source/Engine/Graphics/GraphicsContext.cpp @@ -5,6 +5,16 @@ namespace Elixir { + void GraphicsContext::Clear() + { + ClearImage(m_RenderTarget); + } + + void GraphicsContext::Clear(const Ref& image) + { + ClearImage(image); + } + float GraphicsContext::GetDPIScale() const { return m_Window->GetDPIScale(); diff --git a/Elixir/Source/Engine/Graphics/GraphicsContext.h b/Elixir/Source/Engine/Graphics/GraphicsContext.h index b87a804e..cebf7c15 100644 --- a/Elixir/Source/Engine/Graphics/GraphicsContext.h +++ b/Elixir/Source/Engine/Graphics/GraphicsContext.h @@ -8,8 +8,7 @@ namespace Elixir { class Executor; class Window; - class DepthStencilImage; - class Texture2D; + class Image; class CommandBuffer; class Pipeline; @@ -33,8 +32,53 @@ namespace Elixir virtual void RenderFrame(std::function callback) = 0; virtual void DrainRenderQueue() = 0; + /** @brief Queues a task that must run on the rendering thread. */ + virtual bool EnqueueRenderTask(std::function task) const = 0; + + /** @brief Runs a rendering-thread task and waits until it completes. */ + virtual bool RunRenderTaskAndWait(std::function task) const = 0; + + /** + * @brief Runs a resource-release task after submitted frames complete. + * + * Backends can defer the task without blocking. The default implementation waits + * for submitted frames and then runs the task. + * @param task Resource-release task. + * @return True when the task was accepted. + */ + virtual bool DeferResourceRelease(std::function task) const + { + if (!task) return false; + + WaitForSubmittedFrames(); + task(); + return true; + } + + /** @brief Reports whether the caller is the rendering thread for this context. */ + virtual bool IsRenderThread() const = 0; + + /** @brief Reports whether the rendering thread is currently recording a frame. */ + virtual bool IsFrameRecording() const = 0; + + /** + * @brief Waits for submitted rendering frames without waiting for unrelated GPU work. + * + * Resource operations that read or change an image already used by a frame must call + * this before recording conflicting commands. + */ + virtual void WaitForSubmittedFrames() const = 0; + virtual void SetClearColor(const glm::vec4& color) = 0; - virtual void Clear() = 0; + + /** @brief Clears the context-owned LDR render target with the configured clear color. */ + void Clear(); + + /** + * @brief Clears a color target with the configured clear color. + * @param image Target image to clear; it must be in the General layout. + */ + void Clear(const Ref& image); virtual void Resize(Extent2D extent) = 0; @@ -61,7 +105,7 @@ namespace Elixir */ virtual void WaitDeviceIdle() const {} - [[nodiscard]] EGraphicsAPI GetAPI() const { return m_API; } + EGraphicsAPI GetAPI() const { return m_API; } const Window* GetWindow() const { return m_Window; } float GetDPIScale() const; @@ -72,25 +116,28 @@ namespace Elixir * The number of frames being processed at a concurrent time. Double buffering. * @return the number of frames. */ - [[nodiscard]] uint32_t GetFramesInFlight() const { return m_FramesInFlight; } + uint32_t GetFramesInFlight() const { return m_FramesInFlight; } /** * Returns the number of frames rendered since the app started. * @return the number of frames since app start. */ - [[nodiscard]] uint32_t GetFrameNumber() const { return m_FrameNumber; } + uint32_t GetFrameNumber() const { return m_FrameNumber; } /** * Returns the index of the current frame. * @return the index of the current frame. */ - [[nodiscard]] uint32_t GetFrameIndex() const { return m_FrameNumber % m_FramesInFlight; } + uint32_t GetFrameIndex() const { return m_FrameNumber % m_FramesInFlight; } virtual void SetVSyncEnabled(const bool enabled) { m_VSyncEnabled = enabled; } bool IsVSyncEnabled() const { return m_VSyncEnabled; } - Ref GetRenderTarget() const { return m_RenderTarget; } - Ref GetDepthStencilRenderTarget() const { return m_DepthStencilRenderTarget; } + /** Returns the frame's color attachment as an image resource. */ + Ref GetRenderTarget() const { return m_RenderTarget; } + + /** Returns the frame's depth/stencil attachment as an image resource. */ + Ref GetDepthStencilRenderTarget() const { return m_DepthStencilRenderTarget; } virtual Extent3D GetSwapchainExtent() const = 0; @@ -104,6 +151,7 @@ namespace Elixir } private: + virtual void ClearImage(const Ref& image) = 0; virtual void CreateRenderTargets() = 0; protected: @@ -112,10 +160,10 @@ namespace Elixir EGraphicsAPI m_API; const Window* m_Window; - Ref m_RenderTarget; - Ref m_DepthStencilRenderTarget; + Ref m_RenderTarget; + Ref m_DepthStencilRenderTarget; Scope m_ShaderBackend = nullptr; bool m_VSyncEnabled = false; }; -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/Graphics/GraphicsTypes.h b/Elixir/Source/Engine/Graphics/GraphicsTypes.h index a46918c3..b2da8589 100644 --- a/Elixir/Source/Engine/Graphics/GraphicsTypes.h +++ b/Elixir/Source/Engine/Graphics/GraphicsTypes.h @@ -3,7 +3,6 @@ namespace Elixir { class Image; - class DepthStencilImage; enum class EPrimitiveTopology : uint8_t { @@ -234,10 +233,14 @@ namespace Elixir struct SRenderingInfo { + /** Image receiving color output. */ Ref ColorAttachment; - Ref DepthStencilAttachment = nullptr; + + /** Optional image used for depth and stencil testing. */ + Ref DepthStencilAttachment = nullptr; + float DepthClearValue = 1.0f; + Extent2D RenderArea; }; } - diff --git a/Elixir/Source/Engine/Graphics/Image.cpp b/Elixir/Source/Engine/Graphics/Image.cpp index bb78a5b9..82434a1b 100644 --- a/Elixir/Source/Engine/Graphics/Image.cpp +++ b/Elixir/Source/Engine/Graphics/Image.cpp @@ -2,34 +2,126 @@ #include "Image.h" #include -#include #include #include +#include + namespace Elixir { using namespace Elixir::Graphics; - uint32_t CalculateBitsPerPixel(const Image* image) + namespace { - EE_PROFILE_ZONE_SCOPED() + uint32_t CalculateBitsPerPixel(const Image* image) + { + const auto block = Utils::GetFormatBlockExtent(image); + return Utils::GetFormatBlockSizeBits(image) / (block.x * block.y * block.z); + } - const auto blockSize = Utils::GetFormatBlockSizeBits(image); - const auto blockExtent = Utils::GetFormatBlockExtent(image); - const uint32_t texels = blockExtent.x * blockExtent.y * blockExtent.z; + struct SNormalMapMipChain + { + std::vector Data; + std::vector Regions; + }; - return blockSize / texels; - } + uint8_t EncodeNormalComponent(const float value) + { + return static_cast(std::round( + std::clamp(value * 0.5f + 0.5f, 0.0f, 1.0f) * 255.0f)); + } - size_t CalculateSize(const Image* image) - { - EE_PROFILE_ZONE_SCOPED() + SNormalMapMipChain BuildNormalMapMipChain(const Image* image, const void* baseData) + { + EE_CORE_ASSERT( + image->GetType() == EImageType::_2D, + "Normal-map mip generation requires a 2D image." + ) + EE_CORE_ASSERT( + image->GetFormat() == EImageFormat::R8G8B8A8_UNORM, + "Normal-map mip generation requires R8G8B8A8_UNORM data." + ) + EE_CORE_ASSERT( + image->GetArrayLayers() == 1, + "Normal-map mip generation does not support array images." + ) + + SNormalMapMipChain result; + uint32_t sourceWidth = image->GetWidth(); + uint32_t sourceHeight = image->GetExtent().Height; + size_t sourceOffset = 0; + + for (uint32_t level = 0; level < image->GetMipLevels(); ++level) + { + const uint32_t destinationWidth = level == 0 + ? sourceWidth + : std::max(1u, sourceWidth >> 1u); + const uint32_t destinationHeight = level == 0 + ? sourceHeight + : std::max(1u, sourceHeight >> 1u); + const size_t destinationOffset = result.Data.size(); + const size_t destinationSize = size_t(destinationWidth) * destinationHeight * 4; + + result.Data.resize(destinationOffset + destinationSize); + auto* destination = reinterpret_cast( + result.Data.data() + destinationOffset); + + if (level == 0) + { + std::memcpy(destination, baseData, destinationSize); + } + else + { + const auto* source = reinterpret_cast(result.Data.data() + sourceOffset); + + for (uint32_t y = 0; y < destinationHeight; ++y) + { + for (uint32_t x = 0; x < destinationWidth; ++x) + { + glm::vec3 averageNormal(0.0f); + float averageAlpha = 0.0f; + + for (uint32_t offsetY = 0; offsetY < 2; ++offsetY) + { + for (uint32_t offsetX = 0; offsetX < 2; ++offsetX) + { + const uint32_t sampleX = std::min(x * 2 + offsetX, sourceWidth - 1); + const uint32_t sampleY = std::min(y * 2 + offsetY, sourceHeight - 1); + const auto* sample = source + (sampleY * sourceWidth + sampleX) * 4; + averageNormal += glm::vec3(sample[0], sample[1], sample[2]) / 255.0f * 2.0f - 1.0f; + averageAlpha += static_cast(sample[3]); + } + } + + const float normalLengthSquared = glm::dot(averageNormal, averageNormal); + const glm::vec3 normal = normalLengthSquared > 0.0f + ? averageNormal * glm::inversesqrt(normalLengthSquared) + : glm::vec3(0.0f, 0.0f, 1.0f); + auto* pixel = destination + (y * destinationWidth + x) * 4; + pixel[0] = EncodeNormalComponent(normal.x); + pixel[1] = EncodeNormalComponent(normal.y); + pixel[2] = EncodeNormalComponent(normal.z); + pixel[3] = static_cast(std::round(averageAlpha * 0.25f)); + } + } + } + + SBufferImageCopy region = {}; + region.BufferOffset = destinationOffset; + region.ImageSubresource.AspectMask = EImageAspect::Color; + region.ImageSubresource.MipLevel = level; + region.ImageExtent = { destinationWidth, destinationHeight, 1 }; + result.Regions.push_back(region); + + sourceOffset = destinationOffset; + sourceWidth = destinationWidth; + sourceHeight = destinationHeight; + } - const auto extent = image->GetExtent(); - return extent.Width * extent.Height * extent.Depth * image->GetBytesPerPixel(); - } + return result; + } - /* Image */ + } void Image::Transition(const Ref& cmd, const EImageLayout layout) { @@ -113,48 +205,193 @@ namespace Elixir CopyFrom(cmd, src.get(), regions); } + void Image::Copy( + const CommandBuffer* cmd, + Image* dst, + const Extent3D& srcExtent, + const Extent3D& dstExtent + ) + { + CopyMip(cmd, dst, srcExtent, dstExtent, 0); + } + + Extent3D Image::GetMipExtent(const uint32_t level) const + { + EE_CORE_ASSERT(level < m_MipLevels, "Image mip level is outside the allocated chain.") + if (level >= m_MipLevels) + return { 0, 0, 0 }; + + return { + std::max(1u, m_Extent.Width >> level), + std::max(1u, m_Extent.Height >> level), + std::max(1u, m_Extent.Depth >> level) + }; + } + + size_t Image::GetMipSize(const uint32_t level) const + { + const auto extent = GetMipExtent(level); + const auto block = Utils::GetFormatBlockExtent(this); + return ((size_t(extent.Width) + block.x - 1) / block.x) * + ((size_t(extent.Height) + block.y - 1) / block.y) * + ((size_t(extent.Depth) + block.z - 1) / block.z) * + (Utils::GetFormatBlockSizeBits(this) / CHAR_BIT); + } + SImageCreateInfo Image::GetCreateInfo() const { - const auto extent = GetExtent(); - const auto info = SImageCreateInfo{ - .Width = extent.Width, - .Height = extent.Height, - .Depth = extent.Depth, + return { + .Width = m_Extent.Width, + .Height = m_Extent.Height, + .Depth = m_Extent.Depth, .Type = m_Type, .Format = m_Format, + .MipmapMode = EImageMipmapMode::LeaveExistingMips, .MipLevels = m_MipLevels, .ArrayLayers = m_ArrayLayers, .Usage = m_Usage, .InitialLayout = m_Layout, - .AllocationInfo = { - .RequiredFlags = EMemoryProperty::DeviceLocal - } + .AllocationInfo = m_AllocationInfo, }; + } - return info; + uint32_t Image::GetFullMipLevelCount(const Extent3D& extent) + { + uint32_t largestDimension = std::max({ extent.Width, extent.Height, extent.Depth }); + uint32_t levelCount = 1; + while (largestDimension > 1) + { + largestDimension >>= 1; + ++levelCount; + } + return levelCount; } - void Image::SetSampler(const Ref& sampler) + uint32_t Image::GetMipLevelCount(const SImageCreateInfo& info) { - m_Sampler = sampler; - UpdateSampler(); + EE_CORE_ASSERT( + info.Width > 0 && info.Height > 0 && info.Depth > 0, + "Image dimensions must be greater than zero." + ) + if (!info.Width || !info.Height || !info.Depth) + return 0; + + const auto count = GetFullMipLevelCount({ info.Width, info.Height, info.Depth }); + switch (info.MipmapMode) + { + case EImageMipmapMode::NoMipmaps: + return 1; + case EImageMipmapMode::SimpleAverage: + case EImageMipmapMode::NormalMap: + return count; + case EImageMipmapMode::LeaveExistingMips: + EE_CORE_ASSERT( + info.MipLevels > 0 && info.MipLevels <= count, + "The supplied mip count does not fit the image extent." + ) + if (info.MipLevels == 0 || info.MipLevels > count) + return 0; + return info.MipLevels; + } + + EE_CORE_ERROR("Unknown image mipmap mode.") + return 0; } - Ref Image::Create( - const GraphicsContext* context, - EImageFormat format, - uint32_t width, - const void* data - ) + Ref Image::Create(const GraphicsContext* context, SImageCreateInfo info) { + info.MipLevels = GetMipLevelCount(info); + if (info.MipLevels == 0) + return nullptr; + + if (info.Format == EImageFormat::Undefined || !info.ArrayLayers) + { + EE_CORE_ERROR("An image requires a format and at least one array layer.") + return nullptr; + } + + if (info.InitialLayout == EImageLayout::PreInitialized) + { + EE_CORE_ERROR("PreInitialized is not a final layout for an image created by upload.") + return nullptr; + } + + if ((info.Type == EImageType::_1D && (info.Height != 1 || info.Depth != 1)) || + (info.Type == EImageType::_2D && info.Depth != 1) || + (info.Type == EImageType::_3D && info.ArrayLayers != 1)) + { + EE_CORE_ERROR("Image dimensions and layers do not match its type.") + return nullptr; + } + + const bool generatesMips = info.MipmapMode == EImageMipmapMode::SimpleAverage || + info.MipmapMode == EImageMipmapMode::NormalMap; + if (generatesMips && !info.InitialData) + { + EE_CORE_ERROR("Mip generation requires base-level pixels.") + return nullptr; + } + + if (info.InitialData && !info.InitialMipData.empty()) + { + EE_CORE_ERROR("Use either InitialData or InitialMipData, not both.") + return nullptr; + } + + if (info.MipmapMode == EImageMipmapMode::LeaveExistingMips && + info.MipLevels > 1 && info.InitialData) + { + EE_CORE_ERROR("Use InitialMipData to supply an existing mip chain.") + return nullptr; + } + + if (info.MipmapMode == EImageMipmapMode::NormalMap && + (info.Type != EImageType::_2D || info.ArrayLayers != 1 || + info.Format != EImageFormat::R8G8B8A8_UNORM)) + { + EE_CORE_ERROR("Normal-map mips require a single R8G8B8A8_UNORM 2D image.") + return nullptr; + } + + if (info.InitialData || !info.InitialMipData.empty()) + { + info.Usage |= EImageUsage::TransferDst; + if (info.InitialLayout == EImageLayout::Undefined) + info.InitialLayout = EImageLayout::General; + } + + info.Usage |= EImageUsage::TransferSrc | EImageUsage::TransferDst; + + if (!context) + { + EE_CORE_ERROR("Image creation requires a graphics context.") + return nullptr; + } + + Ref image; switch (context->GetAPI()) { case EGraphicsAPI::Vulkan: - return CreateRef(context, format, width, data); + image = CreateRef(context, info); + break; default: - EE_CORE_ASSERT(false, "Unknown GraphicsAPI!") + EE_CORE_ERROR("Unknown graphics API.") return nullptr; } + + image->Initialize(info); + + return image->IsValid() ? image : nullptr; + } + + Ref Image::Create( + const GraphicsContext* context, + const EImageFormat format, + const uint32_t width, + const void* data + ) + { + return Create(context, CreateImageInfo(format, width, data)); } SImageCreateInfo Image::CreateImageInfo( @@ -164,116 +401,275 @@ namespace Elixir ) { return { - .InitialData = data, .Width = width, .Type = EImageType::_1D, .Format = format, .Usage = EImageUsage::Sampled, - .AllocationInfo = { - .RequiredFlags = EMemoryProperty::DeviceLocal - } + .InitialData = data, }; } Image::Image(const GraphicsContext* context, const SImageCreateInfo& info) - : m_GraphicsContext(context) + : m_Type(info.Type), + m_Layout(EImageLayout::Undefined), + m_Format(info.Format), + m_Usage(info.Usage), + m_Aspect(Utils::CalculateImageAspect(info.Usage, info.Format)), + m_Extent(info.Width, info.Height, info.Depth), + m_MipLevels(info.MipLevels), + m_ArrayLayers(info.ArrayLayers), + m_AllocationInfo(info.AllocationInfo), + m_GraphicsContext(context) { - EE_PROFILE_ZONE_SCOPED() - - m_Extent = Extent3D(info.Width, info.Height, info.Depth); - m_Type = info.Type; - m_Format = info.Format; - m_MipLevels = info.MipLevels; - m_ArrayLayers = info.ArrayLayers; - m_Usage = info.Usage; - m_Layout = EImageLayout::Undefined; - m_Aspect = Utils::CalculateImageAspect(m_Usage, m_Format); - m_BitsPerPixel = CalculateBitsPerPixel(this); - m_Size = CalculateSize(this); - + RecalculateSize(); #ifdef EE_DEBUG m_DebugName = "Image[" + m_UUID.ToString() + "]"; #endif } - void Image::RecalculateSize() + void Image::Initialize(const SImageCreateInfo& info) { - m_BitsPerPixel = CalculateBitsPerPixel(this); - m_Size = CalculateSize(this); - } + SNormalMapMipChain upload; + const void* pixels = info.InitialData; + size_t uploadSize = GetSize(); + + if (info.MipmapMode == EImageMipmapMode::NormalMap) + { + upload = BuildNormalMapMipChain(this, pixels); + pixels = upload.Data.data(); + uploadSize = upload.Data.size(); + } + else if (!info.InitialMipData.empty()) + { + const auto count = static_cast(m_MipLevels) * m_ArrayLayers; + if (info.InitialMipData.size() != count) + { + EE_CORE_ERROR("InitialMipData must contain every mip level and array layer.") + return; + } - /* DepthStencilImage */ + std::vector supplied(count, false); + const size_t alignment = std::lcm( + size_t{4}, + size_t(Utils::GetFormatBlockSizeBits(this) / CHAR_BIT) + ); + + for (const auto& mip : info.InitialMipData) + { + if (!mip.Data || mip.MipLevel >= m_MipLevels || mip.ArrayLayer >= m_ArrayLayers || + mip.Size != GetMipSize(mip.MipLevel)) + { + EE_CORE_ERROR("Invalid initial mip subresource.") + return; + } + + const size_t index = static_cast(mip.ArrayLayer) * m_MipLevels + mip.MipLevel; + if (supplied[index]) + { + EE_CORE_ERROR("Duplicate initial mip subresource.") + return; + } + + supplied[index] = true; + + const size_t offset = (upload.Data.size() + alignment - 1) / alignment * alignment; + upload.Data.resize(offset + mip.Size); + std::memcpy(upload.Data.data() + offset, mip.Data, mip.Size); + + SBufferImageCopy region{}; + region.BufferOffset = offset; + region.ImageSubresource = { m_Aspect, mip.MipLevel, mip.ArrayLayer, 1 }; + region.ImageExtent = GetMipExtent(mip.MipLevel); + + upload.Regions.push_back(region); + } - Ref DepthStencilImage::Create( - const GraphicsContext* context, - EDepthStencilImageFormat format, - uint32_t width, - uint32_t height - ) - { - switch (context->GetAPI()) + pixels = upload.Data.data(); + uploadSize = upload.Data.size(); + } + else if (pixels) { - case EGraphicsAPI::Vulkan: - return CreateRef( - context, format, width, height - ); - default: - EE_CORE_ASSERT(false, "Unknown GraphicsAPI!") - return nullptr; + SBufferImageCopy region{}; + region.ImageSubresource = { m_Aspect, 0, 0, m_ArrayLayers }; + region.ImageExtent = m_Extent; + upload.Regions.push_back(region); + } + + if (pixels && (m_Aspect & EImageAspect::Depth) && (m_Aspect & EImageAspect::Stencil)) + { + EE_CORE_ERROR("Combined depth/stencil uploads require separate aspect regions.") + return; + } + + CreateResource(info); + + if (!pixels && info.InitialLayout == EImageLayout::Undefined) + return; + + Ref staging; + if (pixels) + staging = StagingBuffer::Create(m_GraphicsContext, uploadSize, pixels); + + const auto cmd = m_GraphicsContext->GetUploadCommandBuffer(); + cmd->Begin(); + + if (staging) + { + Transition(cmd, EImageLayout::TransferDst); + CopyFrom(cmd, staging, upload.Regions); } + + if (info.MipmapMode == EImageMipmapMode::SimpleAverage && m_MipLevels > 1) + GenerateMipmaps(cmd.get(), info.InitialLayout); + else + Transition(cmd, info.InitialLayout); + + cmd->Flush(); } - SImageCreateInfo DepthStencilImage::CreateImageInfo( - const EDepthStencilImageFormat format, - const uint32_t width, - const uint32_t height - ) + void Image::Resize(const Extent3D& extent) { - const auto imageFormat = Converters::GetImageFormat(format); - const auto usage = EImageUsage::Sampled | EImageUsage::DepthStencilAttachment; - return { - .Width = width, - .Height = height, - .Type = EImageType::_2D, - .Format = imageFormat, - .Usage = usage, - .InitialLayout = Utils::CalculateImageLayout(usage, imageFormat), - .AllocationInfo = { - .RequiredFlags = EMemoryProperty::DeviceLocal + if (m_GraphicsContext->IsRenderThread() && !m_GraphicsContext->IsFrameRecording()) + { + ResizeOnRenderThread(m_GraphicsContext->GetUploadCommandBuffer(), extent); + return; + } + + { + std::scoped_lock lock(m_ResizeMutex); + m_PendingResize = extent; + if (m_ResizeQueued) + return; + + m_ResizeQueued = true; + } + + const auto enqueued = m_GraphicsContext->EnqueueRenderTask( + [self = shared_from_this()]() + { + self->ApplyPendingResize(); } - }; + ); + + if (!enqueued) + { + std::scoped_lock lock(m_ResizeMutex); + m_ResizeQueued = false; + EE_CORE_ERROR("Could not queue image resize because the graphics context is unavailable.") + } } - DepthStencilImage::DepthStencilImage( - const GraphicsContext* context, - const SImageCreateInfo& info - ) : Image(context, info) + void Image::RecalculateSize() { - EE_PROFILE_ZONE_SCOPED() + m_BitsPerPixel = CalculateBitsPerPixel(this); + m_Size = GetMipSize(0) * m_ArrayLayers; + } - m_Size = CalculateSize(this); + void Image::ApplyPendingResize() + { + while (true) + { + std::optional extent; + { + std::scoped_lock lock(m_ResizeMutex); + extent.swap(m_PendingResize); + if (!extent) + { + m_ResizeQueued = false; + return; + } + } -#ifdef EE_DEBUG - m_DebugName = "DepthStencilImage[" + m_UUID.ToString() + "]"; -#endif + ResizeOnRenderThread( + m_GraphicsContext->GetUploadCommandBuffer(), + *extent + ); + } } - /* StorageImage */ - - // StorageImage::StorageImage( - // const GraphicsContext* context, - // const EImageFormat format, - // const uint32_t width, - // const uint32_t height - // ) : Image(context, format, width), m_Height(height) - // { - // EE_PROFILE_ZONE_SCOPED() - // m_Type = EImageType::_2D; - // m_Layout = EImageLayout::General; - // m_Usage = EImageUsage::Storage | - // EImageUsage::TransferSrc | - // EImageUsage::TransferDst | - // EImageUsage::ColorAttachment; - // m_Aspect = Utils::CalculateImageAspect(m_Usage, m_Format); - // } -} \ No newline at end of file + void Image::ResizeOnRenderThread(const Ref& cmd, const Extent3D& extent) + { + if (!extent.Width || !extent.Height || !extent.Depth) + { + EE_CORE_ERROR("Image dimensions must be greater than zero.") + return; + } + + if (extent.Width == m_Extent.Width && extent.Height == m_Extent.Height && + extent.Depth == m_Extent.Depth) + return; + + if ((m_Type == EImageType::_1D && (extent.Height != 1 || extent.Depth != 1)) || + (m_Type == EImageType::_2D && extent.Depth != 1)) + { + EE_CORE_ERROR("Resize extent does not match the image type.") + return; + } + + if (!(m_Usage & EImageUsage::TransferSrc) || !(m_Usage & EImageUsage::TransferDst)) + { + EE_CORE_ERROR("Preserving image contents requires both transfer usages.") + return; + } + + auto info = GetCreateInfo(); + const auto originalLayout = m_Layout; + const bool preserve = originalLayout != EImageLayout::Undefined; + Ref staging; + + if (preserve) + { + info.InitialLayout = EImageLayout::TransferDst; + staging = Create(m_GraphicsContext, info); + if (!staging) + return; + + cmd->Begin(); + Transition(cmd, EImageLayout::TransferSrc); + + for (uint32_t level = 0; level < m_MipLevels; ++level) + CopyMip( + cmd.get(), + staging.get(), + GetMipExtent(level), + GetMipExtent(level), + level + ); + + staging->Transition(cmd, EImageLayout::TransferSrc); + cmd->Flush(); + } + + info.Width = extent.Width; + info.Height = extent.Height; + info.Depth = extent.Depth; + info.MipLevels = std::min(m_MipLevels, GetFullMipLevelCount(extent)); + info.MipLevels = GetMipLevelCount(info); + info.InitialLayout = EImageLayout::Undefined; + + Destroy(); + m_Extent = extent; + m_MipLevels = info.MipLevels; + m_Layout = EImageLayout::Undefined; + RecalculateSize(); + CreateResource(info); + + if (preserve) + { + cmd->Begin(); + Transition(cmd, EImageLayout::TransferDst); + + for (uint32_t level = 0; level < m_MipLevels; ++level) + staging->CopyMip( + cmd.get(), + this, + staging->GetMipExtent(level), + GetMipExtent(level), + level + ); + + Transition(cmd, originalLayout); + cmd->Flush(); + } + } +} diff --git a/Elixir/Source/Engine/Graphics/Image.h b/Elixir/Source/Engine/Graphics/Image.h index ef8fa0e9..cefb72b0 100644 --- a/Elixir/Source/Engine/Graphics/Image.h +++ b/Elixir/Source/Engine/Graphics/Image.h @@ -2,18 +2,15 @@ #include #include -#include + +#include +#include +#include namespace Elixir { class Image; - namespace Vulkan - { - template - class VulkanBaseImage; - } - enum class EImageLayout { Undefined, @@ -65,6 +62,22 @@ namespace Elixir _1D, _2D, _3D }; + /** @brief Defines how an image receives its mip chain. */ + enum class EImageMipmapMode + { + /** Keeps only the base mip level. */ + NoMipmaps, + + /** Generates lower mip levels by averaging pixel values. */ + SimpleAverage, + + /** Generates lower mip levels by averaging and normalizing normal vectors. */ + NormalMap, + + /** Preserves the mip levels supplied by the image source. */ + LeaveExistingMips + }; + enum class EImageFormat { Undefined = 1, @@ -173,6 +186,7 @@ namespace Elixir Offset3D ImageOffset; Extent3D ImageExtent; + /** @param extent Dimensions of the image region to copy, in texels. */ static SBufferImageCopy Default(const Extent3D& extent = Extent3D()) { return { @@ -182,119 +196,354 @@ namespace Elixir } }; + /** @brief Supplies tightly packed pixels for one mip level and array layer. */ + struct SImageMipData + { + /** Pixel storage borrowed until Image::Create returns. */ + const void* Data = nullptr; + + /** Size of the subresource in bytes, including compressed blocks. */ + size_t Size = 0; + + /** Zero-based mip level. */ + uint32_t MipLevel = 0; + + /** Zero-based array layer; use zero for volume images. */ + uint32_t ArrayLayer = 0; + }; + + /** @brief Describes image storage and optional synchronous initialization. */ struct SImageCreateInfo { - const void* InitialData = nullptr; + /** Base-level width in texels; must be greater than zero. */ uint32_t Width = 0; + + /** Base-level height in texels; must be one for a 1D image. */ uint32_t Height = 1; + + /** Base-level depth in texels; must be one unless Type is _3D. */ uint32_t Depth = 1; - EImageType Type; - EImageFormat Format; + + EImageType Type = EImageType::_2D; + + EImageFormat Format = EImageFormat::Undefined; + + /** Defines how the image receives its mip chain. */ + EImageMipmapMode MipmapMode = EImageMipmapMode::NoMipmaps; + + /** Used only by LeaveExistingMips; other modes determine the count. */ uint32_t MipLevels = 1; + + /** Number of array layers; volume images require one layer. */ uint32_t ArrayLayers = 1; - EImageUsage Usage; + + /** Requested uses; Image adds transfer flags needed for initialization and resize. */ + EImageUsage Usage = EImageUsage::Sampled; + + /** Layout after initialization; Undefined with initial pixels resolves to General. */ EImageLayout InitialLayout = EImageLayout::Undefined; - SAllocationInfo AllocationInfo; + + /** Memory properties requested from the backend allocator. */ + SAllocationInfo AllocationInfo{ .RequiredFlags = EMemoryProperty::DeviceLocal }; + + /** + * Tightly packed base-level pixels for all layers; mutually exclusive with + * InitialMipData. + */ + const void* InitialData = nullptr; + + /** Complete supplied mip chain, or empty to allocate without uploading it. */ + std::span InitialMipData; }; - class ELIXIR_API Image + /** + * @brief Owns a graphics image and coordinates initialization independently of its + * backend. + */ + class ELIXIR_API Image : public std::enable_shared_from_this { - friend class TextureLoader; - - template - friend class Vulkan::VulkanBaseImage; - public: + public: virtual ~Image() = default; + /** + * @brief Releases storage after all GPU work using the image has completed; repeated + * calls are safe. + */ virtual void Destroy() = 0; - virtual void Resize(const Ref& cmd, Extent3D extent) = 0; + /** + * @brief Recreates image storage at a new size and preserves its current pixels. + * @param extent New base-level dimensions in texels. + */ + void Resize(const Extent3D& extent); + /** + * @brief Records a layout transition for all mip levels and layers. + * @param cmd Recording command buffer that receives the transition. + * @param layout Target layout for all mip levels and array layers. + */ void Transition(const Ref& cmd, EImageLayout layout); + + /** + * @brief Records a layout transition for all mip levels and layers. + * @param cmd Recording command buffer that receives the transition. + * @param layout Target layout for all mip levels and array layers. + */ virtual void Transition(const CommandBuffer* cmd, EImageLayout layout) = 0; + /** + * @brief Makes earlier image writes visible to later image accesses. + * @param cmd Recording command buffer that receives the memory dependency. + * @pre The image layout does not change. + */ + virtual void Barrier(const CommandBuffer* cmd) = 0; + + /** + * @brief Blits the base level into dst. + * + * Images must already use transfer layouts and usages. + * + * @param cmd Recording command buffer that receives the blit. + * @param dst Destination image whose base level receives the scaled source pixels. + */ void Copy(const Ref& cmd, const Ref& dst); + + /** + * @brief Blits the base level into dst. + * + * Images must already use transfer layouts and usages. + * + * @param cmd Recording command buffer that receives the blit. + * @param dst Destination image whose base level receives the scaled source pixels. + */ void Copy(const Ref& cmd, Image* dst); + + /** + * @brief Blits the base level into dst. + * + * Images must already use transfer layouts and usages. + * + * @param cmd Recording command buffer that receives the blit. + * @param dst Destination image whose base level receives the scaled source pixels. + */ void Copy(const CommandBuffer* cmd, const Ref& dst); + + /** + * @brief Blits the base level into dst. + * + * Images must already use transfer layouts and usages. + * + * @param cmd Recording command buffer that receives the blit. + * @param dst Destination image whose base level receives the scaled source pixels. + */ void Copy(const CommandBuffer* cmd, Image* dst); + /** + * @brief Blits between base-level extents without resizing either image. + * + * Transfer layouts are required. + * + * @param cmd Recording command buffer that receives the blit. + * @param dst Destination image that receives the scaled source pixels. + * @param srcExtent Source region dimensions in texels, measured from the base-level + * origin. + * @param dstExtent Destination region dimensions in texels, measured from the + * base-level origin. + */ void Copy( const Ref& cmd, const Ref& dst, const Extent3D& srcExtent, const Extent3D& dstExtent ); + + /** + * @brief Blits between base-level extents without resizing either image. + * + * Transfer layouts are required. + * + * @param cmd Recording command buffer that receives the blit. + * @param dst Destination image that receives the scaled source pixels. + * @param srcExtent Source region dimensions in texels, measured from the base-level + * origin. + * @param dstExtent Destination region dimensions in texels, measured from the + * base-level origin. + */ void Copy( const Ref& cmd, Image* dst, const Extent3D& srcExtent, const Extent3D& dstExtent ); + + /** + * @brief Blits between base-level extents without resizing either image. + * + * Transfer layouts are required. + * + * @param cmd Recording command buffer that receives the blit. + * @param dst Destination image that receives the scaled source pixels. + * @param srcExtent Source region dimensions in texels, measured from the base-level + * origin. + * @param dstExtent Destination region dimensions in texels, measured from the + * base-level origin. + */ void Copy( const CommandBuffer* cmd, const Ref& dst, const Extent3D& srcExtent, const Extent3D& dstExtent ); - virtual void Copy( + + /** + * @brief Blits between base-level extents without resizing either image. + * + * Transfer layouts are required. + * + * @param cmd Recording command buffer that receives the blit. + * @param dst Destination image that receives the scaled source pixels. + * @param srcExtent Source region dimensions in texels, measured from the base-level + * origin. + * @param dstExtent Destination region dimensions in texels, measured from the + * base-level origin. + */ + void Copy( const CommandBuffer* cmd, Image* dst, const Extent3D& srcExtent, const Extent3D& dstExtent - ) = 0; + ); + /** + * @brief Records a buffer upload into TransferDst storage. + * + * An empty region list selects the base level. + * + * @param cmd Recording command buffer that receives the upload. + * @param src Source buffer containing the pixel data. + * @param regions Buffer-to-image copy regions; empty selects the full base level of + * every array layer. + */ void CopyFrom( const Ref& cmd, const Ref& src, std::span regions = {} ); + + /** + * @brief Records a buffer upload into TransferDst storage. + * + * An empty region list selects the base level. + * + * @param cmd Recording command buffer that receives the upload. + * @param src Source buffer containing the pixel data. + * @param regions Buffer-to-image copy regions; empty selects the full base level of + * every array layer. + */ void CopyFrom( const Ref& cmd, const Buffer* src, std::span regions = {} ); + + /** + * @brief Records a buffer upload into TransferDst storage. + * + * An empty region list selects the base level. + * + * @param cmd Recording command buffer that receives the upload. + * @param src Source buffer containing the pixel data. + * @param regions Buffer-to-image copy regions; empty selects the full base level of + * every array layer. + */ void CopyFrom( const CommandBuffer* cmd, const Ref& src, std::span regions = {} ); + + /** + * @brief Records a buffer upload into TransferDst storage. + * + * An empty region list selects the base level. + * + * @param cmd Recording command buffer that receives the upload. + * @param src Source buffer containing the pixel data. + * @param regions Buffer-to-image copy regions; empty selects the full base level of + * every array layer. + */ virtual void CopyFrom( const CommandBuffer* cmd, const Buffer* src, std::span regions = {} ) = 0; - [[nodiscard]] virtual bool IsValid() const = 0; + /** @brief Reports whether this image is valid. */ + virtual bool IsValid() const = 0; - [[nodiscard]] const UUID& GetUUID() const { return m_UUID; } + /** @brief Returns the identity of this resource. */ + const UUID& GetUUID() const { return m_UUID; } - [[nodiscard]] EImageType GetType() const { return m_Type; } - [[nodiscard]] EImageLayout GetLayout() const { return m_Layout; } - [[nodiscard]] EImageFormat GetFormat() const { return m_Format; } - [[nodiscard]] EImageUsage GetUsage() const { return m_Usage; } - [[nodiscard]] EImageAspect GetAspect() const { return m_Aspect; } + /** @brief Returns the dimensionality of the image. */ + EImageType GetType() const { return m_Type; } - [[nodiscard]] Extent3D GetExtent() const { return m_Extent; } - [[nodiscard]] uint32_t GetWidth() const { return m_Extent.Width; } + /** @brief Returns the layout tracked for all mip levels and layers. */ + EImageLayout GetLayout() const { return m_Layout; } - [[nodiscard]] uint32_t GetMipLevels() const { return m_MipLevels; } - [[nodiscard]] uint32_t GetArrayLayers() const { return m_ArrayLayers; } + /** @brief Returns the pixel or compressed-block format. */ + EImageFormat GetFormat() const { return m_Format; } - [[nodiscard]] uint32_t GetBitsPerPixel() const { return m_BitsPerPixel; } - [[nodiscard]] uint32_t GetBytesPerPixel() const { return m_BitsPerPixel / CHAR_BIT; } + /** @brief Returns the resolved usage flags, including upload requirements. */ + EImageUsage GetUsage() const { return m_Usage; } - SImageCreateInfo GetCreateInfo() const; + /** @brief Returns the color, depth, or stencil aspects stored by the image. */ + EImageAspect GetAspect() const { return m_Aspect; } - /** - * Returns the image/texture size in bytes. - * @return The image/texture size in bytes. + /** @brief Returns the base-level dimensions in texels. */ + Extent3D GetExtent() const { return m_Extent; } + + /** @brief Returns the base-level width in texels. */ + uint32_t GetWidth() const { return m_Extent.Width; } + + /** @brief Returns the base-level height in texels. */ + uint32_t GetHeight() const { return m_Extent.Height; } + + /** @brief Returns the base-level depth in texels. */ + uint32_t GetDepth() const { return m_Extent.Depth; } + + /** @brief Returns the extent of an allocated mip level. + * @param level Zero-based mip level, less than GetMipLevels(). + */ + Extent3D GetMipExtent(uint32_t level) const; + + /** @brief Returns tightly packed bytes for one array layer of an allocated mip level. + * @param level Zero-based mip level, less than GetMipLevels(). */ - [[nodiscard]] size_t GetSize() const { return m_Size; } + size_t GetMipSize(uint32_t level) const; - [[nodiscard]] bool IsHDR() const { return m_HDR; } + /** @brief Returns the allocated mip-level count. */ + uint32_t GetMipLevels() const { return m_MipLevels; } - [[nodiscard]] const Ref& GetSampler() const { return m_Sampler; } - void SetSampler(const Ref& sampler); + /** @brief Returns the allocated array-layer count. */ + uint32_t GetArrayLayers() const { return m_ArrayLayers; } + + /** @brief Returns bits per texel, averaged across a block for compressed formats. */ + uint32_t GetBitsPerPixel() const { return m_BitsPerPixel; } + + /** @brief Returns whole bytes per texel; use GetMipSize for compressed storage sizes. */ + uint32_t GetBytesPerPixel() const { return m_BitsPerPixel / CHAR_BIT; } + + /** + * @brief Returns the resolved allocation description without borrowed upload data + * or generation requests. + */ + SImageCreateInfo GetCreateInfo() const; + + /** + * @brief Returns tightly packed base-level bytes across all layers, including + * compressed blocks. + */ + size_t GetSize() const { return m_Size; } bool operator==(const Image& other) const { @@ -304,6 +553,28 @@ namespace Elixir Image& operator=(const Image&) = delete; Image& operator=(Image&&) = delete; + /** + * @brief Resolves the description, creates the backend, uploads data, and prepares + * the final layout. + * + * Upload storage is borrowed only for this call. + * + * @pre The context outlives the image, and no upload command buffer is recording on + * this thread. + * @param context Graphics context used to create the image; must outlive it. + * @param info Image description; supplied pixel storage must remain valid until this + * call returns. + */ + static Ref Create(const GraphicsContext* context, SImageCreateInfo info); + + /** + * @brief Creates a one-dimensional sampled image. + * @param context Graphics context that must outlive the image. + * @param format Pixel or compressed-block format. + * @param width Base-level width in texels; must be greater than zero. + * @param data Optional tightly packed base-level pixels, borrowed until this call + * returns. + */ static Ref Create( const GraphicsContext* context, EImageFormat format, @@ -311,19 +582,38 @@ namespace Elixir const void* data = nullptr ); + /** + * @brief Builds a one-dimensional sampled-image request without resolving mip levels. + * @param format Pixel or compressed-block format. + * @param width Base-level width in texels; must be greater than zero when creating + * the image. + * @param data Optional base-level pixels; the returned description borrows this + * storage. + */ static SImageCreateInfo CreateImageInfo( EImageFormat format, uint32_t width, const void* data = nullptr ); + /** @brief Returns the number of mip levels from a base image extent. + * @param extent Base-level dimensions in texels; the largest dimension determines + * the chain length. + */ + static uint32_t GetFullMipLevelCount(const Extent3D& extent); + + /** + * @brief Resolves the mip count from the requested mode and validates its range. + * @param info Requested dimensions, mipmap mode, and explicit count for + * LeaveExistingMips. + */ + static uint32_t GetMipLevelCount(const SImageCreateInfo& info); + protected: Image(const GraphicsContext* context, const SImageCreateInfo& info); Image(const Image&) = delete; Image(Image&&) = delete; - virtual void UpdateSampler() = 0; - /** * Recompute cached size fields (bits-per-pixel and byte size) from the * current format and extent. Call after changing the extent (e.g. on @@ -348,34 +638,38 @@ namespace Elixir uint32_t m_BitsPerPixel; size_t m_Size; - bool m_HDR = false; - - Ref m_Sampler; + SAllocationInfo m_AllocationInfo; const GraphicsContext* m_GraphicsContext; - }; - class ELIXIR_API DepthStencilImage : public Image - { - public: - ~DepthStencilImage() override = default; + std::mutex m_ResizeMutex; + std::optional m_PendingResize; + bool m_ResizeQueued = false; - [[nodiscard]] virtual uint32_t GetHeight() const { return m_Extent.Height; } + private: + // Creates storage and completes the requested upload after backend construction. + void Initialize(const SImageCreateInfo& info); - static Ref Create( - const GraphicsContext* context, - EDepthStencilImageFormat format, - uint32_t width, - uint32_t height - ); + // Allocates backend resources from the already resolved description. + virtual void CreateResource(const SImageCreateInfo& info) = 0; - static SImageCreateInfo CreateImageInfo( - EDepthStencilImageFormat format, - uint32_t width, - uint32_t height - ); + // Generates simple-average mip levels on the device and transitions the whole image. + virtual void GenerateMipmaps(const CommandBuffer* cmd, EImageLayout finalLayout) = 0; - protected: - DepthStencilImage(const GraphicsContext* context, const SImageCreateInfo& info); + // Scales one mip level across all array layers without changing either allocation. + virtual void CopyMip( + const CommandBuffer* cmd, + Image* dst, + const Extent3D& srcExtent, + const Extent3D& dstExtent, + uint32_t level + ) = 0; + + // Applies queued resize requests on the rendering thread. + void ApplyPendingResize(); + + // Recreates storage on the rendering thread. + void ResizeOnRenderThread(const Ref& cmd, const Extent3D& extent); }; -} \ No newline at end of file + +} diff --git a/Elixir/Source/Engine/Graphics/PostProcessor.cpp b/Elixir/Source/Engine/Graphics/PostProcessor.cpp new file mode 100644 index 00000000..bc53c32e --- /dev/null +++ b/Elixir/Source/Engine/Graphics/PostProcessor.cpp @@ -0,0 +1,206 @@ +#include "epch.h" +#include "PostProcessor.h" + +#include +#include +#include +#include +#include + +namespace Elixir +{ + namespace + { + struct alignas(16) SPostProcessFrameData + { + glm::vec2 InverseSceneSize{}; + float BloomThreshold = 1.0f; + float BloomKnee = 0.5f; + float BloomRadius = 6.0f; + float BloomIntensity = 0.08f; + float Exposure = 0.0f; + float Padding = 0.0f; + }; + + Ref CreateFullscreenPipeline( + const GraphicsContext* context, + const Ref& shader, + const EImageFormat colorFormat + ) + { + PipelineBuilder builder; + builder.SetShader(shader); + builder.SetInputTopology(EPrimitiveTopology::TriangleList); + builder.SetPolygonMode(EPolygonMode::Fill); + builder.DisableBlending(); + builder.DisableDepthTest(); + builder.SetColorAttachmentFormat(colorFormat); + builder.SetBufferLayout({}); + return builder.Build(context); + } + + bool HasEqualExtent(const Extent3D& lhs, const Extent3D& rhs) + { + return lhs.Width == rhs.Width && lhs.Height == rhs.Height && lhs.Depth == rhs.Depth; + } + } + + PostProcessor::PostProcessor( + const GraphicsContext* context, + const ShaderLoader* shaderLoader, + const Extent3D& extent + ) : m_Context(context), + m_FrameBuffers(*context) + { + EE_CORE_ASSERT(m_Context, "PostProcessor requires a graphics context.") + EE_CORE_ASSERT(shaderLoader, "PostProcessor requires a shader loader.") + + m_BloomShader = shaderLoader->LoadShader("./Shaders/", "PostProcessBloom"); + m_ToneMapShader = shaderLoader->LoadShader("./Shaders/", "PostProcessToneMap"); + + EE_CORE_ASSERT(m_BloomShader, "PostProcessBloom shader could not be loaded.") + EE_CORE_ASSERT(m_ToneMapShader, "PostProcessToneMap shader could not be loaded.") + + m_BloomPipeline = CreateFullscreenPipeline( + m_Context, + m_BloomShader, + EImageFormat::R16G16B16A16_SFLOAT + ); + m_ToneMapPipeline = CreateFullscreenPipeline( + m_Context, + m_ToneMapShader, + EImageFormat::R8G8B8A8_SRGB + ); + + constexpr SPostProcessFrameData frameData{}; + m_FrameBuffers.ForEach([this, &frameData](Ref& frameBuffer) + { + frameBuffer = UniformBuffer::Create(m_Context, sizeof(frameData), &frameData); + }); + m_Sampler = SamplerBuilder() + .SetAddressModeU(ESamplerAddressMode::ClampToEdge) + .SetAddressModeV(ESamplerAddressMode::ClampToEdge) + .SetAddressModeW(ESamplerAddressMode::ClampToEdge) + .Build(m_Context); + + m_BloomShader->BindSampler("postProcessSampler", m_Sampler); + m_ToneMapShader->BindSampler("postProcessSampler", m_Sampler); + + EE_CORE_ASSERT( + extent.Width > 0 && extent.Height > 0 && extent.Depth > 0, + "Post-process targets require a non-zero extent." + ) + m_BloomTarget = Image::Create(m_Context, { + .Width = extent.Width, + .Height = extent.Height, + .Depth = extent.Depth, + .Type = EImageType::_2D, + .Format = EImageFormat::R16G16B16A16_SFLOAT, + .Usage = EImageUsage::ColorAttachment | EImageUsage::Sampled | + EImageUsage::TransferSrc | EImageUsage::TransferDst, + .InitialLayout = EImageLayout::General, + }); + EE_CORE_ASSERT(m_BloomTarget, "PostProcessor could not create its bloom target.") + } + + void PostProcessor::Resize(const Extent3D& extent) + { + EE_CORE_ASSERT( + m_Context->IsRenderThread() && !m_Context->IsFrameRecording(), + "Post-process targets must be resized on the rendering thread before frame recording." + ) + EE_CORE_ASSERT( + extent.Width > 0 && extent.Height > 0 && extent.Depth > 0, + "Post-process targets require a non-zero extent." + ) + if (!HasEqualExtent(m_BloomTarget->GetExtent(), extent)) + m_BloomTarget->Resize(extent); + } + + void PostProcessor::Apply( + const Ref& sceneTarget, + const Ref& renderTarget + ) + { + EE_CORE_ASSERT(sceneTarget, "Post processing requires a scene target.") + EE_CORE_ASSERT(renderTarget, "Post processing requires a render target.") + EE_CORE_ASSERT( + HasEqualExtent(sceneTarget->GetExtent(), renderTarget->GetExtent()), + "Post-process targets must have equal extents." + ) + + const auto extent = sceneTarget->GetExtent(); + EE_CORE_ASSERT( + m_BloomTarget && HasEqualExtent(m_BloomTarget->GetExtent(), extent), + "Post-process targets must be resized before Apply." + ) + + const SPostProcessFrameData frameData{ + .InverseSceneSize = { + 1.0f / static_cast(extent.Width), + 1.0f / static_cast(extent.Height), + }, + .BloomThreshold = m_Settings.BloomThreshold, + .BloomKnee = m_Settings.BloomKnee, + .BloomRadius = m_Settings.BloomRadius, + .BloomIntensity = m_Settings.BloomIntensity, + .Exposure = m_Settings.Exposure, + }; + const auto& frameBuffer = m_FrameBuffers.GetCurrent(); + frameBuffer->UpdateData(&frameData, sizeof(frameData)); + m_BloomShader->BindConstantBuffer("cbPostProcess", frameBuffer); + m_ToneMapShader->BindConstantBuffer("cbPostProcess", frameBuffer); + + const SRenderingInfo bloomRenderingInfo{ + .ColorAttachment = m_BloomTarget, + .RenderArea = extent, + }; + + const auto bloomCommandBuffer = m_Context->GetSecondaryCommandBuffer(); + bloomCommandBuffer->Begin(bloomRenderingInfo); + + sceneTarget->Barrier(bloomCommandBuffer.get()); + m_BloomTarget->Barrier(bloomCommandBuffer.get()); + m_BloomShader->BindImage("sceneTarget", sceneTarget); + bloomCommandBuffer->BeginRendering(bloomRenderingInfo); + bloomCommandBuffer->SetViewports({{ + .Width = static_cast(extent.Width), + .Height = static_cast(extent.Height), + .MaxDepth = 1.0f, + }}); + bloomCommandBuffer->SetScissors({{ .Extent = { extent.Width, extent.Height } }}); + m_BloomPipeline->Bind(bloomCommandBuffer); + bloomCommandBuffer->Draw(3); + bloomCommandBuffer->EndRendering(); + + bloomCommandBuffer->End(); + m_Context->EnqueueSecondaryCommandBuffer(bloomCommandBuffer); + + const SRenderingInfo toneMapRenderingInfo{ + .ColorAttachment = renderTarget, + .RenderArea = extent, + }; + + const auto toneMapCommandBuffer = m_Context->GetSecondaryCommandBuffer(); + toneMapCommandBuffer->Begin(toneMapRenderingInfo); + + m_BloomTarget->Barrier(toneMapCommandBuffer.get()); + renderTarget->Barrier(toneMapCommandBuffer.get()); + m_ToneMapShader->BindImage("sceneTarget", sceneTarget); + m_ToneMapShader->BindImage("bloomTarget", m_BloomTarget); + toneMapCommandBuffer->BeginRendering(toneMapRenderingInfo); + toneMapCommandBuffer->SetViewports({{ + .Width = static_cast(extent.Width), + .Height = static_cast(extent.Height), + .MaxDepth = 1.0f, + }}); + toneMapCommandBuffer->SetScissors({{ .Extent = { extent.Width, extent.Height } }}); + m_ToneMapPipeline->Bind(toneMapCommandBuffer); + toneMapCommandBuffer->Draw(3); + toneMapCommandBuffer->EndRendering(); + + toneMapCommandBuffer->End(); + m_Context->EnqueueSecondaryCommandBuffer(toneMapCommandBuffer); + } + +} diff --git a/Elixir/Source/Engine/Graphics/PostProcessor.h b/Elixir/Source/Engine/Graphics/PostProcessor.h new file mode 100644 index 00000000..e3372b3d --- /dev/null +++ b/Elixir/Source/Engine/Graphics/PostProcessor.h @@ -0,0 +1,82 @@ +#pragma once + +#include +#include + +namespace Elixir +{ + class GraphicsContext; + class GraphicsPipeline; + class Sampler; + class Shader; + class ShaderLoader; + class UniformBuffer; + + /** @brief Configures HDR bloom, exposure, and tone mapping. */ + struct SPostProcessSettings + { + /** Exposure adjustment in stops before tone mapping. */ + float Exposure = 0.0f; + + /** HDR luminance at which bloom starts contributing. */ + float BloomThreshold = 1.0f; + + /** Width of the smooth bloom threshold transition. */ + float BloomKnee = 0.5f; + + /** Bloom blur radius in source-image texels. */ + float BloomRadius = 6.0f; + + /** Multiplier applied to the HDR bloom contribution. */ + float BloomIntensity = 0.08f; + }; + + /** @brief Converts an HDR scene image into the application's LDR render target. */ + class ELIXIR_API PostProcessor final + { + public: + /** + * @brief Creates post-processing resources. + * @param context Graphics context used to create and submit GPU resources. + * @param shaderLoader Loader used to obtain post-process shaders. + * @param extent Dimensions of the HDR scene target in texels. + * @pre context and shaderLoader remain valid for this object's lifetime. + */ + PostProcessor( + const GraphicsContext* context, + const ShaderLoader* shaderLoader, + const Extent3D& extent + ); + + /** + * @brief Resizes post-process targets before recording the next frame. + * @param extent Dimensions of the HDR scene target in texels. + */ + void Resize(const Extent3D& extent); + + /** + * @brief Applies HDR bloom, exposure, and tone mapping to a scene. + * @param sceneTarget HDR scene image produced by world renderers. + * @param renderTarget LDR render target that receives the display-ready image. + * @pre sceneTarget and renderTarget have equal extents. + */ + void Apply(const Ref& sceneTarget, const Ref& renderTarget); + + /** @brief Returns the settings used by future post-process passes. */ + SPostProcessSettings& GetSettings() { return m_Settings; } + + /** @brief Returns the settings used by future post-process passes. */ + const SPostProcessSettings& GetSettings() const { return m_Settings; } + + private: + const GraphicsContext* m_Context = nullptr; + Ref m_BloomTarget; + Ref m_BloomShader; + Ref m_ToneMapShader; + Ref m_BloomPipeline; + Ref m_ToneMapPipeline; + Ref m_Sampler; + FrameSlotState> m_FrameBuffers; + SPostProcessSettings m_Settings; + }; +} diff --git a/Elixir/Source/Engine/Graphics/Sampler.h b/Elixir/Source/Engine/Graphics/Sampler.h index 35da90f0..6c9b0006 100644 --- a/Elixir/Source/Engine/Graphics/Sampler.h +++ b/Elixir/Source/Engine/Graphics/Sampler.h @@ -46,7 +46,7 @@ namespace Elixir bool CompareEnable = false; ECompareOp CompareOp = ECompareOp::Always; float MinLod = 0.0f; - float MaxLod = 0.0f; + float MaxLod = 1000.0f; ESamplerBorderColor BorderColor = ESamplerBorderColor::FloatTransparentBlack; bool UnnormalizedCoordinates = false; }; diff --git a/Elixir/Source/Engine/Graphics/SamplerBuilder.cpp b/Elixir/Source/Engine/Graphics/SamplerBuilder.cpp index 9d1c1b5f..970b4d46 100644 --- a/Elixir/Source/Engine/Graphics/SamplerBuilder.cpp +++ b/Elixir/Source/Engine/Graphics/SamplerBuilder.cpp @@ -46,7 +46,7 @@ namespace Elixir m_MipLodBias = 0; m_MinLod = 0; - m_MaxLod = 0; + m_MaxLod = 1000.0f; m_AnisotropyEnable = false; m_MaxAnisotropy = 0; @@ -127,4 +127,4 @@ namespace Elixir m_BorderColor = color; return *this; } -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/Graphics/SamplerBuilder.h b/Elixir/Source/Engine/Graphics/SamplerBuilder.h index 8162f1b3..de7558bd 100644 --- a/Elixir/Source/Engine/Graphics/SamplerBuilder.h +++ b/Elixir/Source/Engine/Graphics/SamplerBuilder.h @@ -1,32 +1,34 @@ #pragma once +#include + namespace Elixir { - class ELIXIR_API SamplerBuilder + class ELIXIR_API SamplerBuilder final { public: SamplerBuilder(); - virtual Ref Build(const GraphicsContext* context); - virtual SamplerBuilder& Clear(); + Ref Build(const GraphicsContext* context); + SamplerBuilder& Clear(); - virtual SamplerBuilder& SetMagFilter(ESamplerFilter filter); - virtual SamplerBuilder& SetMinFilter(ESamplerFilter filter); + SamplerBuilder& SetMagFilter(ESamplerFilter filter); + SamplerBuilder& SetMinFilter(ESamplerFilter filter); - virtual SamplerBuilder& SetMipmapMode(ESamplerMipmapMode mode); + SamplerBuilder& SetMipmapMode(ESamplerMipmapMode mode); - virtual SamplerBuilder& SetAddressModeU(ESamplerAddressMode mode); - virtual SamplerBuilder& SetAddressModeV(ESamplerAddressMode mode); - virtual SamplerBuilder& SetAddressModeW(ESamplerAddressMode mode); + SamplerBuilder& SetAddressModeU(ESamplerAddressMode mode); + SamplerBuilder& SetAddressModeV(ESamplerAddressMode mode); + SamplerBuilder& SetAddressModeW(ESamplerAddressMode mode); - virtual SamplerBuilder& SetMipLodBias(float bias); - virtual SamplerBuilder& SetMinLod(float lod); - virtual SamplerBuilder& SetMaxLod(float lod); + SamplerBuilder& SetMipLodBias(float bias); + SamplerBuilder& SetMinLod(float lod); + SamplerBuilder& SetMaxLod(float lod); - virtual SamplerBuilder& SetAnisotropyEnable(bool enable); - virtual SamplerBuilder& SetMaxAnisotropy(float maxAnisotropy); + SamplerBuilder& SetAnisotropyEnable(bool enable); + SamplerBuilder& SetMaxAnisotropy(float maxAnisotropy); - virtual SamplerBuilder& SetBorderColor(ESamplerBorderColor color); + SamplerBuilder& SetBorderColor(ESamplerBorderColor color); protected: ESamplerFilter m_MagFilter; @@ -47,4 +49,4 @@ namespace Elixir ESamplerBorderColor m_BorderColor; }; -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/Graphics/Shader/Shader.h b/Elixir/Source/Engine/Graphics/Shader/Shader.h index b4dc05f2..2455963c 100644 --- a/Elixir/Source/Engine/Graphics/Shader/Shader.h +++ b/Elixir/Source/Engine/Graphics/Shader/Shader.h @@ -66,6 +66,13 @@ namespace Elixir virtual void SetConstantBuffer(const std::string& name, void* data, size_t size) = 0; + /** + * @brief Binds an image resource for shader sampling. + * @param name Name of the shader resource binding. + * @param image Image to sample; it must remain valid while the shader uses it. + */ + virtual void BindImage(const std::string& name, const Ref& image) = 0; + virtual void BindTexture(const std::string& name, const Ref& texture) = 0; virtual void BindTextureSet(const std::string& name, const Ref& set) = 0; virtual void BindSampler(const std::string& name, const Ref& sampler) = 0; @@ -135,6 +142,7 @@ namespace Elixir // Shader resources std::unordered_map m_BindingLookup; + std::unordered_map> m_Images; std::unordered_map> m_Textures; std::unordered_map> m_TextureSets; std::unordered_map> m_Samplers; @@ -146,4 +154,4 @@ namespace Elixir const GraphicsContext* m_GraphicsContext; }; -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/Graphics/Shader/ShaderLoader.cpp b/Elixir/Source/Engine/Graphics/Shader/ShaderLoader.cpp index 30f81712..d6339990 100644 --- a/Elixir/Source/Engine/Graphics/Shader/ShaderLoader.cpp +++ b/Elixir/Source/Engine/Graphics/Shader/ShaderLoader.cpp @@ -72,7 +72,7 @@ namespace Elixir const auto path = entry.path(); const auto filename = RemoveFileExtension(path); const auto isIncluded = std::ranges::find(filenames, filename) != filenames.end(); - if (entry.is_regular_file() && path.has_extension() && isIncluded) + if (entry.is_regular_file() && path.extension() == ".spirv" && isIncluded) { if (const auto stage = GetShaderStage(path)) files.push_back({ stage.value(), path }); @@ -177,4 +177,4 @@ namespace Elixir const auto& backend = m_GraphicsContext->GetShaderBackend(); return backend->LoadModule(filepath, stage); } -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/Graphics/Texture.cpp b/Elixir/Source/Engine/Graphics/Texture.cpp index cfc369e9..c68d7980 100644 --- a/Elixir/Source/Engine/Graphics/Texture.cpp +++ b/Elixir/Source/Engine/Graphics/Texture.cpp @@ -1,201 +1,157 @@ #include "epch.h" #include "Texture.h" -#include -#include - namespace Elixir { - /* Texture */ + Texture::Texture(Ref image, const STextureCreateInfo& info) + : m_Image(std::move(image)), m_Path(info.Path), m_HDR(info.HDR) + { + } + + Ref Texture::Create(const GraphicsContext* context, const STextureCreateInfo& info) + { + const auto image = Image::Create(context, CreateImageInfo(info)); + if (!image) return nullptr; + + return Ref(new Texture(image, info)); + } Ref Texture::Create( - const GraphicsContext* context, - EImageFormat format, - uint32_t width, - const void* data, - const std::string& path + const GraphicsContext* context, const EImageFormat format, const uint32_t width, + const void* data, const std::string& path ) { - switch (context->GetAPI()) - { - case EGraphicsAPI::Vulkan: - return CreateRef(context, format, width, data, path); - default: - EE_CORE_ASSERT(false, "Unknown GraphicsAPI!") - return nullptr; - } + STextureCreateInfo info; + info.InitialData = data; + info.Format = format; + info.Width = width; + info.Path = path; + return Create(context, info); } - SImageCreateInfo Texture::CreateImageInfo( - const EImageFormat format, - const uint32_t width, - const void* data, - const std::string& path - ) + SImageCreateInfo Texture::CreateImageInfo(const STextureCreateInfo& info) { return { - .InitialData = data, - .Width = width, + .Width = info.Width, .Type = EImageType::_1D, - .Format = format, - .Usage = EImageUsage::Sampled | EImageUsage::TransferDst, + .Format = info.Format, + .MipmapMode = info.MipmapMode, + .MipLevels = info.MipLevels, + .Usage = EImageUsage::Sampled, .InitialLayout = EImageLayout::ShaderReadOnly, - .AllocationInfo = { - .RequiredFlags = EMemoryProperty::DeviceLocal - } + .InitialData = info.InitialData, + .InitialMipData = info.InitialMipData, }; } - Texture::Texture( - const GraphicsContext* context, - const EImageFormat format, - const uint32_t width, - const void* data, - const std::string& path - ) : Texture(context, CreateImageInfo(format, width, data), path) {} - - Texture::Texture( - const GraphicsContext* context, - const SImageCreateInfo& info, - const std::string& path - ) : Image(context, info), m_Path(std::move(path)) + SImageCreateInfo Texture::CreateImageInfo( + const EImageFormat format, const uint32_t width, + const void* data, const std::string& path + ) { - EE_PROFILE_ZONE_SCOPED() + STextureCreateInfo info; + info.InitialData = data; + info.Format = format; + info.Width = width; + info.Path = path; + return CreateImageInfo(info); + } -#ifdef EE_DEBUG - m_DebugName = "Texture[" + m_UUID.ToString() + "]"; -#endif + Texture2D::Texture2D(Ref image, const STextureCreateInfo& info) + : Texture(std::move(image), info) + { } - /* Texture2D */ + Ref Texture2D::Create(const GraphicsContext* context, const STexture2DCreateInfo& info) + { + const auto image = Image::Create(context, CreateImageInfo(info)); + if (!image) return nullptr; + + return Ref(new Texture2D(image, info)); + } Ref Texture2D::Create( - const GraphicsContext* context, - EImageFormat format, - uint32_t width, - uint32_t height, - const void* data, - const std::string& path + const GraphicsContext* context, const EImageFormat format, + const uint32_t width, const uint32_t height, const void* data, const std::string& path ) { - switch (context->GetAPI()) - { - case EGraphicsAPI::Vulkan: - return CreateRef(context, format, width, height, data, path); - default: - EE_CORE_ASSERT(false, "Unknown GraphicsAPI!") - return nullptr; - } + STexture2DCreateInfo info; + info.InitialData = data; + info.Format = format; + info.Width = width; + info.Height = height; + info.Path = path; + return Create(context, info); + } + + SImageCreateInfo Texture2D::CreateImageInfo(const STexture2DCreateInfo& info) + { + auto image = Texture::CreateImageInfo(info); + image.Type = EImageType::_2D; + image.Height = info.Height; + return image; } SImageCreateInfo Texture2D::CreateImageInfo( - const EImageFormat format, - const uint32_t width, - const uint32_t height, - const void* data + const EImageFormat format, const uint32_t width, const uint32_t height, const void* data ) { - return { - .InitialData = data, - .Width = width, - .Height = height, - .Type = EImageType::_2D, - .Format = format, - .Usage = EImageUsage::Sampled | EImageUsage::TransferDst, - .InitialLayout = EImageLayout::ShaderReadOnly, - .AllocationInfo = { - .RequiredFlags = EMemoryProperty::DeviceLocal - } - }; + STexture2DCreateInfo info; + info.InitialData = data; + info.Format = format; + info.Width = width; + info.Height = height; + return CreateImageInfo(info); } - Texture2D::Texture2D( - const GraphicsContext* context, - const EImageFormat format, - const uint32_t width, - const uint32_t height, - const void* data, - const std::string& path - ) : Texture2D(context, CreateImageInfo(format, width, height, data), path) {} - - Texture2D::Texture2D( - const GraphicsContext* context, - const SImageCreateInfo& info, - const std::string& path - ) : Texture(context, info, path) + Texture3D::Texture3D(Ref image, const STextureCreateInfo& info) + : Texture(std::move(image), info) { - EE_PROFILE_ZONE_SCOPED() - -#ifdef EE_DEBUG - m_DebugName = "Texture2D[" + m_UUID.ToString() + "]"; -#endif } - /* Texture3D */ + Ref Texture3D::Create(const GraphicsContext* context, const STexture3DCreateInfo& info) + { + const auto image = Image::Create(context, CreateImageInfo(info)); + if (!image) return nullptr; + + return Ref(new Texture3D(image, info)); + } Ref Texture3D::Create( - const GraphicsContext* context, - EImageFormat format, - uint32_t width, - uint32_t height, - uint32_t depth, - const void* data, - const std::string& path + const GraphicsContext* context, const EImageFormat format, + const uint32_t width, const uint32_t height, const uint32_t depth, + const void* data, const std::string& path ) { - switch (context->GetAPI()) - { - case EGraphicsAPI::Vulkan: - return CreateRef(context, format, width, height, depth, data, path); - default: - EE_CORE_ASSERT(false, "Unknown GraphicsAPI!") - return nullptr; - } + STexture3DCreateInfo info; + info.InitialData = data; + info.Format = format; + info.Width = width; + info.Height = height; + info.Depth = depth; + info.Path = path; + return Create(context, info); } - SImageCreateInfo Texture3D::CreateImageInfo( - const EImageFormat format, - const uint32_t width, - const uint32_t height, - const uint32_t depth, - const void* data - ) + SImageCreateInfo Texture3D::CreateImageInfo(const STexture3DCreateInfo& info) { - return { - .InitialData = data, - .Width = width, - .Height = height, - .Depth = depth, - .Type = EImageType::_3D, - .Format = format, - .Usage = EImageUsage::Sampled | EImageUsage::TransferDst, - .InitialLayout = EImageLayout::ShaderReadOnly, - .AllocationInfo = { - .RequiredFlags = EMemoryProperty::DeviceLocal - } - }; + auto image = Texture2D::CreateImageInfo(info); + image.Type = EImageType::_3D; + image.Depth = info.Depth; + return image; } - Texture3D::Texture3D( - const GraphicsContext* context, - const EImageFormat format, - const uint32_t width, - const uint32_t height, - const uint32_t depth, - const void* data, - const std::string& path - ) : Texture3D(context, CreateImageInfo(format, width, height, depth, data), path) {} - - Texture3D::Texture3D( - const GraphicsContext* context, - const SImageCreateInfo& info, - const std::string& path - ) : Texture2D(context, info, path) + SImageCreateInfo Texture3D::CreateImageInfo( + const EImageFormat format, const uint32_t width, const uint32_t height, + const uint32_t depth, const void* data + ) { - EE_PROFILE_ZONE_SCOPED() - -#ifdef EE_DEBUG - m_DebugName = "Texture3D[" + m_UUID.ToString() + "]"; -#endif + STexture3DCreateInfo info; + info.InitialData = data; + info.Format = format; + info.Width = width; + info.Height = height; + info.Depth = depth; + return CreateImageInfo(info); } -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/Graphics/Texture.h b/Elixir/Source/Engine/Graphics/Texture.h index be881586..80baca27 100644 --- a/Elixir/Source/Engine/Graphics/Texture.h +++ b/Elixir/Source/Engine/Graphics/Texture.h @@ -4,13 +4,122 @@ namespace Elixir { - class ELIXIR_API Texture : public Image + /** @brief Defines image properties and asset metadata shared by texture dimensions. */ + struct STextureCreateInfo { - public: - ~Texture() override = default; + /** Pixel format of the texture. */ + EImageFormat Format = EImageFormat::Undefined; + + /** Base-level width in texels. */ + uint32_t Width = 0; + + /** Number of supplied mip levels when using LeaveExistingMips. */ + uint32_t MipLevels = 1; + + /** Mip policy forwarded to Image without resolving it. */ + EImageMipmapMode MipmapMode = EImageMipmapMode::NoMipmaps; + + /** Source path retained for asset tracking. */ + std::string Path; + + /** Records whether the source pixels were decoded as HDR data. */ + bool HDR = false; + + /** Base-level pixels borrowed until creation returns. */ + const void* InitialData = nullptr; + + /** Complete existing mip chain; mutually exclusive with InitialData. */ + std::span InitialMipData; + }; + + /** @brief Defines a two-dimensional texture asset. */ + struct STexture2DCreateInfo : STextureCreateInfo + { + /** Base-level height in texels. */ + uint32_t Height = 0; + }; + + /** @brief Defines a three-dimensional texture asset. */ + struct STexture3DCreateInfo : STexture2DCreateInfo + { + /** Base-level depth in texels. */ + uint32_t Depth = 0; + }; + + /** + * @brief Owns asset metadata and a shared image resource, independently of the + * graphics backend. + */ + class ELIXIR_API Texture + { + public: + /** @brief Releases the asset's reference to its image. */ + virtual ~Texture() = default; + + Texture(const Texture&) = delete; + Texture(Texture&&) = delete; + Texture& operator=(const Texture&) = delete; + Texture& operator=(Texture&&) = delete; + + /** @brief Returns the image used for rendering and resource operations. */ + const Ref& GetImage() const { return m_Image; } + + /** @brief Returns this asset's identity, independent of the image resource. */ + const UUID& GetUUID() const { return m_UUID; } + + /** @brief Returns the source path, or an empty string for procedural assets. */ + const std::string& GetPath() const { return m_Path; } + + /** @brief Reports whether the source was decoded as HDR data. */ + bool IsHDR() const { return m_HDR; } + + /** @brief Reports whether the image has allocated storage. */ + bool IsValid() const { return m_Image && m_Image->IsValid(); } + + /** @brief Returns the image dimension. */ + EImageType GetType() const { return m_Image->GetType(); } + + /** @brief Returns the image pixel format. */ + EImageFormat GetFormat() const { return m_Image->GetFormat(); } - [[nodiscard]] const std::string& GetPath() const { return m_Path; } + /** @brief Returns the base-level dimensions in texels. */ + Extent3D GetExtent() const { return m_Image->GetExtent(); } + /** @brief Returns the base-level width in texels. */ + uint32_t GetWidth() const { return m_Image->GetWidth(); } + + /** @brief Returns the base-level height in texels. */ + uint32_t GetHeight() const { return m_Image->GetHeight(); } + + /** @brief Returns the base-level depth in texels. */ + uint32_t GetDepth() const { return m_Image->GetDepth(); } + + /** @brief Returns the allocated mip-level count. */ + uint32_t GetMipLevels() const { return m_Image->GetMipLevels(); } + + /** @brief Returns the base-level storage size in bytes. */ + size_t GetSize() const { return m_Image->GetSize(); } + + /** + * @brief Creates a one-dimensional texture and delegates image initialization to + * Image. + * @param context Graphics context that must outlive the texture's image. + * @param info Texture properties, pixel data, mip policy, and asset metadata. + * @return The initialized texture, or null when image creation fails. + */ + static Ref Create( + const GraphicsContext* context, + const STextureCreateInfo& info + ); + + /** + * @brief Creates a one-dimensional texture with a single mip level. + * @param context Graphics context that must outlive the texture's image. + * @param format Pixel format of the image and supplied data. + * @param width Base-level width in texels; must be greater than zero. + * @param data Optional base-level pixels, borrowed until creation returns. + * @param path Source path retained for asset tracking; empty for procedural assets. + */ static Ref Create( const GraphicsContext* context, EImageFormat format, @@ -18,7 +127,22 @@ namespace Elixir const void* data = nullptr, const std::string& path = "" ); - + + /** + * @brief Maps texture properties to an image request without resolving mip levels or + * upload usage. + * @param info Texture properties and borrowed pixel data to forward to Image. + */ + static SImageCreateInfo CreateImageInfo(const STextureCreateInfo& info); + + /** + * @brief Builds an unresolved one-dimensional image request. + * @param format Pixel format of the image and supplied data. + * @param width Base-level width in texels; must be greater than zero. + * @param data Optional base-level pixels that must remain valid until + * Image::Create returns. + * @param path Source path; not included in the returned image description. + */ static SImageCreateInfo CreateImageInfo( EImageFormat format, uint32_t width, @@ -26,30 +150,40 @@ namespace Elixir const std::string& path = "" ); - protected: - Texture( - const GraphicsContext* context, - EImageFormat format, - uint32_t width, - const void* data = nullptr, - const std::string& path = "" - ); - Texture( - const GraphicsContext* context, - const SImageCreateInfo& info, - const std::string& path = "" - ); + protected: + Texture(Ref image, const STextureCreateInfo& info); + private: + UUID m_UUID; + Ref m_Image; std::string m_Path; + bool m_HDR; }; - class ELIXIR_API Texture2D : public Texture + /** @brief Wraps a two-dimensional image as a texture asset. */ + class ELIXIR_API Texture2D final : public Texture { - public: - ~Texture2D() override = default; - - [[nodiscard]] uint32_t GetHeight() const { return m_Extent.Height; } + public: + /** + * @brief Creates a two-dimensional asset and initializes its image. + * @param context Graphics context that must outlive the texture's image. + * @param info Texture properties, pixel data, mip policy, and asset metadata. + * @return The initialized texture, or null when image creation fails. + */ + static Ref Create( + const GraphicsContext* context, + const STexture2DCreateInfo& info + ); + /** + * @brief Creates a two-dimensional asset with a single mip level. + * @param context Graphics context that must outlive the texture's image. + * @param format Pixel format of the image and supplied data. + * @param width Base-level width in texels; must be greater than zero. + * @param height Base-level height in texels; must be greater than zero. + * @param data Optional base-level pixels, borrowed until creation returns. + * @param path Source path retained for asset tracking; empty for procedural assets. + */ static Ref Create( const GraphicsContext* context, EImageFormat format, @@ -59,6 +193,20 @@ namespace Elixir const std::string& path = "" ); + /** + * @brief Maps texture properties to an unresolved two-dimensional image request. + * @param info Texture properties and borrowed pixel data to forward to Image. + */ + static SImageCreateInfo CreateImageInfo(const STexture2DCreateInfo& info); + + /** + * @brief Builds an unresolved two-dimensional image request. + * @param format Pixel format of the image and supplied data. + * @param width Base-level width in texels; must be greater than zero. + * @param height Base-level height in texels; must be greater than zero. + * @param data Optional base-level pixels that must remain valid until + * Image::Create returns. + */ static SImageCreateInfo CreateImageInfo( EImageFormat format, uint32_t width, @@ -66,29 +214,36 @@ namespace Elixir const void* data = nullptr ); - protected: - Texture2D( - const GraphicsContext* context, - EImageFormat format, - uint32_t width, - uint32_t height, - const void* data = nullptr, - const std::string& path = "" - ); - Texture2D( - const GraphicsContext* context, - const SImageCreateInfo& info, - const std::string& path = "" - ); + private: + // Retains the initialized image and its asset metadata. + Texture2D(Ref image, const STextureCreateInfo& info); }; - class ELIXIR_API Texture3D : public Texture2D + /** @brief Wraps a three-dimensional image as a texture asset. */ + class ELIXIR_API Texture3D final : public Texture { public: - ~Texture3D() override = default; - - [[nodiscard]] uint32_t GetDepth() const { return m_Extent.Depth; } + /** + * @brief Creates a three-dimensional asset and initializes its image. + * @param context Graphics context that must outlive the texture's image. + * @param info Texture properties, pixel data, mip policy, and asset metadata. + * @return The initialized texture, or null when image creation fails. + */ + static Ref Create( + const GraphicsContext* context, + const STexture3DCreateInfo& info + ); + /** + * @brief Creates a three-dimensional asset with a single mip level. + * @param context Graphics context that must outlive the texture's image. + * @param format Pixel format of the image and supplied data. + * @param width Base-level width in texels; must be greater than zero. + * @param height Base-level height in texels; must be greater than zero. + * @param depth Base-level depth in texels; must be greater than zero. + * @param data Optional base-level pixels, borrowed until creation returns. + * @param path Source path retained for asset tracking; empty for procedural assets. + */ static Ref Create( const GraphicsContext* context, EImageFormat format, @@ -99,6 +254,21 @@ namespace Elixir const std::string& path = "" ); + /** + * @brief Maps texture properties to an unresolved three-dimensional image request. + * @param info Texture properties and borrowed pixel data to forward to Image. + */ + static SImageCreateInfo CreateImageInfo(const STexture3DCreateInfo& info); + + /** + * @brief Builds an unresolved three-dimensional image request. + * @param format Pixel format of the image and supplied data. + * @param width Base-level width in texels; must be greater than zero. + * @param height Base-level height in texels; must be greater than zero. + * @param depth Base-level depth in texels; must be greater than zero. + * @param data Optional base-level pixels that must remain valid until + * Image::Create returns. + */ static SImageCreateInfo CreateImageInfo( EImageFormat format, uint32_t width, @@ -107,20 +277,8 @@ namespace Elixir const void* data = nullptr ); - protected: - Texture3D( - const GraphicsContext* context, - EImageFormat format, - uint32_t width, - uint32_t height, - uint32_t depth, - const void* data = nullptr, - const std::string& path = "" - ); - Texture3D( - const GraphicsContext* context, - const SImageCreateInfo& info, - const std::string& path = "" - ); + private: + // Retains the initialized image and its asset metadata. + Texture3D(Ref image, const STextureCreateInfo& info); }; -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/Graphics/TextureLoader.cpp b/Elixir/Source/Engine/Graphics/TextureLoader.cpp index 2ca37a59..bfd041a2 100644 --- a/Elixir/Source/Engine/Graphics/TextureLoader.cpp +++ b/Elixir/Source/Engine/Graphics/TextureLoader.cpp @@ -7,19 +7,22 @@ namespace Elixir { - using namespace Elixir::Graphics; + using namespace Graphics; - void TraceTextureInfo( - const std::string& path, - bool isHdr, - const EImageFormat format, - int channelCount - ) + namespace { - EE_CORE_TRACE( - "Creating texture {0} [HDR = {1}, Format = {2}, Channels = {3}].", path, isHdr, - format, channelCount + void TraceTextureInfo( + const std::string& path, + bool isHdr, + const EImageFormat format, + int channelCount ) + { + EE_CORE_TRACE( + "Creating texture {0} [HDR = {1}, Format = {2}, Channels = {3}].", path, isHdr, + format, channelCount + ) + } } const GraphicsContext* TextureLoader::s_Context = nullptr; @@ -35,12 +38,24 @@ namespace Elixir const std::filesystem::path& path, const EImageFormat format ) + { + STexture2DCreateInfo info; + info.Format = format; + return Load(path, info); + } + + Ref TextureLoader::Load( + const std::filesystem::path& path, + const STexture2DCreateInfo& info + ) { EE_CORE_ASSERT(s_Initialized, "TextureLoader is not initialized!") - void* data; + void* data = nullptr; bool isHdr = false; - int width, height, channels; + int width = 0; + int height = 0; + int channels = 0; const auto pathStr = path.string(); @@ -68,23 +83,31 @@ namespace Elixir ); } - EE_CORE_ASSERT(data, "Could not read texture data!") + if (!data) + { + EE_CORE_ERROR( + "Could not read texture data from {0}: {1}.", + pathStr, + stbi_failure_reason() + ) + return nullptr; + } + + auto createInfo = info; + createInfo.InitialData = data; + createInfo.Width = width; + createInfo.Height = height; + createInfo.Path = pathStr; + createInfo.HDR = isHdr; - TraceTextureInfo(pathStr, isHdr, format, channels); + TraceTextureInfo(pathStr, isHdr, createInfo.Format, channels); - auto texture = Texture2D::Create( - s_Context, - format, - width, - height, - data, - pathStr - ); - texture->m_HDR = isHdr; - EE_CORE_TRACE("Loaded texture: {0} [{1}].", pathStr, texture->GetUUID()) + auto texture = Texture2D::Create(s_Context, createInfo); + if (texture) + EE_CORE_TRACE("Loaded texture: {0} [{1}].", pathStr, texture->GetUUID()) stbi_image_free(data); return std::move(texture); } -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/Graphics/TextureLoader.h b/Elixir/Source/Engine/Graphics/TextureLoader.h index c7727832..6f5dd89f 100644 --- a/Elixir/Source/Engine/Graphics/TextureLoader.h +++ b/Elixir/Source/Engine/Graphics/TextureLoader.h @@ -14,6 +14,17 @@ namespace Elixir EImageFormat format = EImageFormat::R8G8B8A8_SRGB ); + /** + * @brief Loads a two-dimensional texture using the supplied configuration. + * @param path Path to the source image. + * @param info Texture configuration. Decoded pixels and dimensions replace + * the corresponding fields before creation. + */ + static Ref Load( + const std::filesystem::path& path, + const STexture2DCreateInfo& info + ); + private: static const GraphicsContext* s_Context; static bool s_Initialized; diff --git a/Elixir/Source/Engine/Materials/Compilation/Compiler.cpp b/Elixir/Source/Engine/Materials/Compilation/Compiler.cpp index 77c4034a..9fedd159 100644 --- a/Elixir/Source/Engine/Materials/Compilation/Compiler.cpp +++ b/Elixir/Source/Engine/Materials/Compilation/Compiler.cpp @@ -113,7 +113,7 @@ namespace Elixir::Materials::Compilation } const auto compiled = CreateRef(); - compiled->UsageMask = material.GetUsageMask(); + compiled->Usage = material.GetUsage(); compiled->MaterialRevision = material.GetRevision(); compiled->Parameters = std::move(layout); return { .Material = compiled }; @@ -124,23 +124,18 @@ namespace Elixir::Materials::Compilation auto result = Build(material); if (!result) return result; - result = CompileSurface(loader, material, std::move(result)); - if (!result) return result; - - if (material.SupportsUsage(EMaterialUsage::ParticleSprite)) + if (material.GetUsage() == EMaterialUsage::Surface) { - result = CompileParticleSprite(loader, material, std::move(result)); + result = CompileSurface(loader, material, std::move(result)); if (!result) return result; } - if (material.SupportsUsage(EMaterialUsage::ParticleRibbon)) + if (material.GetUsage() == EMaterialUsage::Particle) { + result = CompileParticleSprite(loader, material, std::move(result)); + if (!result) return result; result = CompileParticleRibbon(loader, material, std::move(result)); if (!result) return result; - } - - if (material.SupportsUsage(EMaterialUsage::ParticleMesh)) - { result = CompileParticleMesh(loader, material, std::move(result)); if (!result) return result; } @@ -148,13 +143,28 @@ namespace Elixir::Materials::Compilation return result; } - std::string Compiler::InjectBody(const std::string& hlsl, const std::string& graphBody) + std::string Compiler::InjectBody( + const std::string& hlsl, + const std::string& graphBody, + const EMaterialShadingModel shadingModel + ) { std::string out = hlsl; - constexpr std::string_view marker = "// __GRAPH_BODY__"; - if (const auto pos = out.find(marker); pos != std::string::npos) - out.replace(pos, marker.size(), graphBody); + constexpr std::string_view graphMarker = "// __GRAPH_BODY__"; + if (const auto pos = out.find(graphMarker); pos != std::string::npos) + out.replace(pos, graphMarker.size(), graphBody); + + constexpr std::string_view shadingModelMarker = "// __SHADING_MODEL__"; + if (const auto pos = out.find(shadingModelMarker); pos != std::string::npos) + { + const auto value = (uint8_t)shadingModel; + out.replace( + pos, + shadingModelMarker.size(), + "#define MATERIAL_SHADING_MODEL " + std::to_string(value) + ); + } return out; } @@ -165,12 +175,13 @@ namespace Elixir::Materials::Compilation SCompileResult result ) { - const auto hlsl = ReadFile(s_ShadersDir / "Material" / "Material.ps.hlsl"); + const auto vertexHlsl = ReadFile(s_ShadersDir / "Material" / "Surface.vs.hlsl"); + const auto pixelHlsl = ReadFile(s_ShadersDir / "Material" / "Material.ps.hlsl"); - if (hlsl.empty()) + if (vertexHlsl.empty() || pixelHlsl.empty()) { - EE_CORE_ERROR("Material graph: template Material.ps.hlsl not found.") - result.Diagnostics = "Material template Material.ps.hlsl was not found."; + EE_CORE_ERROR("Surface material shader template was not found.") + result.Diagnostics = "Surface material shader template was not found."; result.Material.reset(); return result; } @@ -186,26 +197,54 @@ namespace Elixir::Materials::Compilation std::error_code error; fs::create_directories(loadDir, error); - const fs::path hlslPath = s_GeneratedDir / (name + ".src.ps.hlsl"); + const fs::path vertexSourcePath = s_GeneratedDir / (name + ".src.vs.hlsl"); { - std::ofstream out(hlslPath, std::ios::binary); + std::ofstream out(vertexSourcePath, std::ios::binary); + out << vertexHlsl; + } + + const fs::path pixelSourcePath = s_GeneratedDir / (name + ".src.ps.hlsl"); + { + std::ofstream out(pixelSourcePath, std::ios::binary); const auto graphHlsl = GenerateGraphHLSL(material.GetGraph(), *result.Material); - out << InjectBody(hlsl, graphHlsl); + out << InjectBody( + pixelHlsl, + graphHlsl, + material.GetShadingModel() + ); } - // Compile the generated pixel shader to SPIR-V with DXC. const fs::path dxc = FindDXC(); - const fs::path spvPath = loadDir / (name + ".ps.spirv"); - const std::string cmd = - "\"" + dxc.string() + "\" -spirv -T ps_6_0 -E main \"" - + hlslPath.string() + "\" -Fo \"" + spvPath.string() + "\""; - const int rc = std::system(cmd.c_str()); - if (rc != 0 || !fs::exists(spvPath)) + const auto compileStage = [&dxc]( + const fs::path& sourcePath, + const fs::path& spvPath, + const std::string_view profile + ) + { + const std::string cmd = + "\"" + dxc.string() + "\" -spirv -T " + std::string(profile) + " -E main \"" + + sourcePath.string() + "\" -Fo \"" + spvPath.string() + "\""; + + return std::system(cmd.c_str()) == 0 && fs::exists(spvPath); + }; + + if (!compileStage( + vertexSourcePath, + loadDir / (name + ".vs.spirv"), + "vs_6_0" + ) || !compileStage( + pixelSourcePath, + loadDir / (name + ".ps.spirv"), + "ps_6_0" + )) { - EE_CORE_ERROR("Material graph: DXC compilation failed (rc={0}) for {1}.", rc, name) - result.Diagnostics = "DXC failed while compiling material."; + EE_CORE_ERROR( + "Surface material: DXC compilation failed for {}.", + name + ) + result.Diagnostics = "DXC failed while compiling the surface material."; result.Material.reset(); return result; } @@ -213,7 +252,7 @@ namespace Elixir::Materials::Compilation result.Material->SurfaceShader = loader->LoadShader(loadDir, name); if (!result.Material->SurfaceShader) { - result.Diagnostics = "Shader loader could not load the compiled material."; + result.Diagnostics = "Shader loader could not load the compiled surface material."; result.Material.reset(); } @@ -226,11 +265,12 @@ namespace Elixir::Materials::Compilation SCompileResult result ) { - const auto hlsl = ReadFile(s_ShadersDir / "Material" / "ParticleSprite.ps.hlsl"); + const auto vertexHlsl = ReadFile(s_ShadersDir / "Material" / "ParticleSprite.vs.hlsl"); + const auto pixelHlsl = ReadFile(s_ShadersDir / "Material" / "ParticleSprite.ps.hlsl"); - if (hlsl.empty()) + if (vertexHlsl.empty() || pixelHlsl.empty()) { - result.Diagnostics = "Material template ParticleSprite.ps.hlsl was not found."; + result.Diagnostics = "Material sprite shader template was not found."; result.Material.reset(); return result; } @@ -243,27 +283,46 @@ namespace Elixir::Materials::Compilation std::error_code error; fs::create_directories(loadDir, error); - const fs::path spriteSourcePath = s_GeneratedDir / (name + ".src.ps.hlsl"); + const fs::path vertexSourcePath = s_GeneratedDir / (name + ".src.vs.hlsl"); + { + std::ofstream out(vertexSourcePath, std::ios::binary); + out << vertexHlsl; + } + + const fs::path pixelSourcePath = s_GeneratedDir / (name + ".src.ps.hlsl"); { - std::ofstream out(spriteSourcePath, std::ios::binary); + std::ofstream out(pixelSourcePath, std::ios::binary); const auto graphHlsl = GenerateGraphHLSL(material.GetGraph(), *result.Material); - out << InjectBody(hlsl, graphHlsl); + out << InjectBody(pixelHlsl, graphHlsl); } - // Compile the generated pixel shader to SPIR-V with DXC. const fs::path dxc = FindDXC(); - const fs::path spvPath = loadDir / (name + ".ps.spirv"); - const std::string cmd = - "\"" + dxc.string() + "\" -spirv -T ps_6_0 -E main \"" - + spriteSourcePath.string() + "\" -Fo \"" + spvPath.string() + "\""; + const auto compileStage = [&dxc]( + const fs::path& sourcePath, + const fs::path& spvPath, + const std::string_view profile + ) + { + const std::string cmd = + "\"" + dxc.string() + "\" -spirv -T " + std::string(profile) + " -E main \"" + + sourcePath.string() + "\" -Fo \"" + spvPath.string() + "\""; + + return std::system(cmd.c_str()) == 0 && fs::exists(spvPath); + }; - const int rc = std::system(cmd.c_str()); - if (rc != 0 || !fs::exists(spvPath)) + if (!compileStage( + vertexSourcePath, + loadDir / (name + ".vs.spirv"), + "vs_6_0" + ) || !compileStage( + pixelSourcePath, + loadDir / (name + ".ps.spirv"), + "ps_6_0" + )) { EE_CORE_ERROR( - "Particle sprite material: DXC compilation failed (rc={0}) for {1}.", - rc, + "Particle sprite material: DXC compilation failed for {}.", name ) result.Diagnostics = "DXC failed while compiling the particle sprite material."; @@ -271,23 +330,6 @@ namespace Elixir::Materials::Compilation return result; } - // The generated pixel stage shares the existing Aether sprite vertex ABI. - // Put both stages in an isolated directory so ShaderLoader sees one shader. - const fs::path spriteVertexSpv = s_ShadersDir / "Aether" / "Sprite.vs.spirv"; - fs::copy_file( - spriteVertexSpv, - loadDir / (name + ".vs.spirv"), - fs::copy_options::overwrite_existing, - error - ); - - if (error) - { - result.Diagnostics = "Could not prepare the particle sprite vertex shader."; - result.Material.reset(); - return result; - } - result.Material->ParticleSpriteShader = loader->LoadShader(loadDir, name); if (!result.Material->ParticleSpriteShader) { @@ -340,7 +382,6 @@ namespace Elixir::Materials::Compilation const fs::path dxc = FindDXC(); const fs::path spvPath = loadDir / (name + ".ps.spirv"); - const auto compileStage = [&dxc]( const fs::path& sourcePath, const fs::path& spvPath, diff --git a/Elixir/Source/Engine/Materials/Compilation/Compiler.h b/Elixir/Source/Engine/Materials/Compilation/Compiler.h index de11dbb7..40ed96dd 100644 --- a/Elixir/Source/Engine/Materials/Compilation/Compiler.h +++ b/Elixir/Source/Engine/Materials/Compilation/Compiler.h @@ -5,6 +5,15 @@ namespace Elixir::Materials::Compilation { + /** @brief Identifies a shader variant produced for a compiled material. */ + enum class EMaterialShaderVariant : uint8_t + { + Surface, + ParticleSprite, + ParticleRibbon, + ParticleMesh, + }; + /** * @brief Describes one material parameter in compiled GPU data. */ @@ -31,51 +40,43 @@ namespace Elixir::Materials::Compilation /** @brief Source material revision used during compilation. */ uint32_t MaterialRevision = 0; - /** @brief Bit mask of supported material usages. */ - uint32_t UsageMask = 0; + /** @brief Renderer usage compiled for this material. */ + EMaterialUsage Usage = EMaterialUsage::Surface; /** @brief Shader used by surface material rendering. */ Ref SurfaceShader; - /** @brief Shader used by particle sprite rendering. */ + /** @brief Shader variant used by particle sprite rendering. */ Ref ParticleSpriteShader; - /** @brief Shader used by particle ribbon rendering. */ + /** @brief Shader variant used by particle ribbon rendering. */ Ref ParticleRibbonShader; - /** @brief Shader used by particle mesh rendering. */ + /** @brief Shader variant used by particle mesh rendering. */ Ref ParticleMeshShader; /** @brief Parameter layout shared by the material graph and GPU data. */ std::vector Parameters; - /** - * @brief Checks whether the compiled material supports a usage. - * @param usage Material usage to check. - * @return True when the usage is present in UsageMask. - */ - bool SupportsUsage(const EMaterialUsage usage) const - { - return (UsageMask & GetMaterialUsageMask(usage)) != 0; - } + /** @brief Returns the renderer usage compiled for this material. */ + EMaterialUsage GetUsage() const { return Usage; } /** - * @brief Gets the particle shader for a material usage. - * - * Surface rendering uses SurfaceShader directly. - * - * @param usage Particle material usage. - * @return The matching particle shader, or null for an unsupported usage. + * @brief Gets the shader for a compiled material variant. + * @param variant Shader variant. + * @return The matching shader, or null when the variant is unavailable. */ - const Ref& GetShader(const EMaterialUsage usage) const + const Ref& GetShader(const EMaterialShaderVariant variant) const { - switch (usage) + switch (variant) { - case EMaterialUsage::ParticleSprite: + case EMaterialShaderVariant::Surface: + return SurfaceShader; + case EMaterialShaderVariant::ParticleSprite: return ParticleSpriteShader; - case EMaterialUsage::ParticleRibbon: + case EMaterialShaderVariant::ParticleRibbon: return ParticleRibbonShader; - case EMaterialUsage::ParticleMesh: + case EMaterialShaderVariant::ParticleMesh: return ParticleMeshShader; default: static const Ref unsupportedUsageShader; @@ -126,8 +127,12 @@ namespace Elixir::Materials::Compilation static SCompileResult Compile(const ShaderLoader* loader, const Material& material); private: - /** @brief Replaces the graph-body marker in a shader template. */ - static std::string InjectBody(const std::string& hlsl, const std::string& graphBody); + /** @brief Replaces shader-template markers with generated material code. */ + static std::string InjectBody( + const std::string& hlsl, + const std::string& graphBody, + EMaterialShadingModel shadingModel = EMaterialShadingModel::Lit + ); /** @brief Compiles the surface shader program. */ static SCompileResult CompileSurface( diff --git a/Elixir/Source/Engine/Materials/DefaultMaterials.cpp b/Elixir/Source/Engine/Materials/DefaultMaterials.cpp index 699ea011..1bffc52b 100644 --- a/Elixir/Source/Engine/Materials/DefaultMaterials.cpp +++ b/Elixir/Source/Engine/Materials/DefaultMaterials.cpp @@ -4,6 +4,9 @@ #include #include #include +#include +#include +#include namespace Elixir::Materials { @@ -14,41 +17,24 @@ namespace Elixir::Materials Ref MakeMaterial( std::string name, const EMaterialUsage usage, - MaterialGraph graph + MaterialGraph graph, + const EMaterialBlendMode blendMode = EMaterialBlendMode::Opaque ) { - const auto material = CreateRef(std::move(name)); - const auto result = material->SetUsage(usage, true); - EE_CORE_ASSERT(result, "Default material usage must be enabled.") - material->SetGraph(std::move(graph)); - return material; - } + const auto material = CreateRef(std::move(name), usage); - Ref CreateDefaultSpriteMaterial() - { - MaterialGraph graph; + if (blendMode != EMaterialBlendMode::Opaque) + { + const auto result = material->SetBlendMode(blendMode); + EE_CORE_ASSERT(result, "Default material blend mode must be enabled.") + } - const auto baseColor = graph.AddNode( - glm::vec4{ 1.0f, 1.0f, 1.0f, 0.0f }, - EMaterialValueType::Float3 - ); - graph.SetChannel(EMaterialChannel::BaseColor, baseColor); - - const auto opacity = graph.AddNode( - glm::vec2{ 0.5f, 0.5f }, - 0.5f, - 2.0f - ); - graph.SetChannel(EMaterialChannel::Opacity, opacity); + material->SetGraph(std::move(graph)); - return MakeMaterial( - "Engine.Materials.Defaults.ParticleSprite", - EMaterialUsage::ParticleSprite, - std::move(graph) - ); + return material; } - Ref CreateDefaultRibbonMaterial() + Ref CreateDefaultSurfaceMaterial() { MaterialGraph graph; @@ -56,23 +42,43 @@ namespace Elixir::Materials graph.SetChannel(EMaterialChannel::BaseColor, checkerboard); return MakeMaterial( - "Engine.Materials.Defaults.ParticleRibbon", - EMaterialUsage::ParticleRibbon, + "Engine.Materials.Defaults.Surface", + EMaterialUsage::Surface, std::move(graph) ); } - Ref CreateDefaultMeshMaterial() + Ref CreateDefaultParticleMaterial() { MaterialGraph graph; - const auto checkerboard = graph.AddNode(8.0f); - graph.SetChannel(EMaterialChannel::BaseColor, checkerboard); + const auto baseColor = graph.AddNode( + glm::vec4{ 1.0f, 1.0f, 1.0f, 0.0f }, + EMaterialValueType::Float3 + ); + const auto color = graph.AddNode(); + const auto coloredBase = graph.AddNode(); + graph.Connect(baseColor, coloredBase, 0); + graph.Connect(color, coloredBase, 1); + graph.SetChannel(EMaterialChannel::BaseColor, coloredBase); + + const auto opacity = graph.AddNode( + glm::vec2{ 0.5f, 0.5f }, + 0.5f, + 2.0f + ); + const auto particleAlpha = graph.AddNode(3); + graph.Connect(color, particleAlpha, 0); + const auto coloredOpacity = graph.AddNode(); + graph.Connect(opacity, coloredOpacity, 0); + graph.Connect(particleAlpha, coloredOpacity, 1); + graph.SetChannel(EMaterialChannel::Opacity, coloredOpacity); return MakeMaterial( - "Engine.Materials.Defaults.ParticleMesh", - EMaterialUsage::ParticleMesh, - std::move(graph) + "Engine.Materials.Defaults.Particle", + EMaterialUsage::Particle, + std::move(graph), + EMaterialBlendMode::Translucent ); } } @@ -80,9 +86,8 @@ namespace Elixir::Materials DefaultMaterialArray CreateDefaultMaterials() { return { - CreateDefaultSpriteMaterial(), - CreateDefaultRibbonMaterial(), - CreateDefaultMeshMaterial() + CreateDefaultSurfaceMaterial(), + CreateDefaultParticleMaterial() }; } } diff --git a/Elixir/Source/Engine/Materials/Material.cpp b/Elixir/Source/Engine/Materials/Material.cpp index 6fc5076a..9830658a 100644 --- a/Elixir/Source/Engine/Materials/Material.cpp +++ b/Elixir/Source/Engine/Materials/Material.cpp @@ -16,24 +16,45 @@ namespace Elixir::Materials ++m_Revision; } - bool Material::SetUsage(const EMaterialUsage usage, const bool enabled) + bool Material::SetShadingModel(EMaterialShadingModel model) { - const uint32_t mask = GetMaterialUsageMask(usage); - const uint32_t updatedMask = enabled - ? m_UsageMask | mask - : m_UsageMask & ~mask; + if (m_ShadingModel == model) + return false; + + m_ShadingModel = model; + ++m_Revision; + return true; + } + + bool Material::SetBlendMode(const EMaterialBlendMode mode) + { + if (m_BlendMode == mode) + return false; + + m_BlendMode = mode; + ++m_Revision; + return true; + } - if (updatedMask == m_UsageMask) + bool Material::SetAlphaCutoff(const float cutoff) + { + const auto normalized = std::min(std::max(0.0f, cutoff), 1.0f); + if (m_AlphaCutoff == normalized) return false; - m_UsageMask = updatedMask; + m_AlphaCutoff = normalized; ++m_Revision; return true; } - bool Material::SupportsUsage(const EMaterialUsage usage) const + bool Material::SetDoubleSided(bool enabled) { - return (m_UsageMask & GetMaterialUsageMask(usage)) != 0; + if (m_DoubleSided == enabled) + return false; + + m_DoubleSided = enabled; + ++m_Revision; + return true; } bool Material::SetDefaultParameter(const std::string& name, const SMaterialParameter& value) @@ -105,6 +126,11 @@ namespace Elixir::Materials return parameter && parameter->Kind == EMaterialParameterKind::Texture; } + EMaterialUsage GetUsage() const override + { + return m_Material.GetUsage(); + } + private: const Material& m_Material; }; diff --git a/Elixir/Source/Engine/Materials/Material.h b/Elixir/Source/Engine/Materials/Material.h index 8e25774f..74c56955 100644 --- a/Elixir/Source/Engine/Materials/Material.h +++ b/Elixir/Source/Engine/Materials/Material.h @@ -8,37 +8,56 @@ namespace Elixir::Materials class MaterialInstance; /** - * @brief Identifies a shader permutation supported by a material. + * @brief Identifies the shader permutation used by a material. * - * A usage selects a renderer-specific implementation without changing the + * A usage selects one renderer-specific implementation without changing the * material graph or its parameter schema. */ enum class EMaterialUsage : uint8_t { - /** Material for particle sprites. */ - ParticleSprite = 0, + /** Material for surface geometry. */ + Surface = 0, - /** Material for particle ribbons. */ - ParticleRibbon, - - /** Material for particle meshes. */ - ParticleMesh, + /** Material for every particle geometry mode. */ + Particle, /** Number of supported usages. */ Count }; - /** - * @brief Defines the numeric value type used by a material. - */ + /** @brief Defines how a surface material evaluates lighting. */ + enum class EMaterialShadingModel : uint8_t + { + /** Emits material color without evaluating scene lighting. */ + Unlit, + + /** Evaluates the engine metallic-roughness PBR lighting model. */ + Lit, + + /** Evaluates Lit with a dielectric clear-coat layer over a base layer. */ + ClearCoat, + }; + + /** @brief Defines how a material combines with the color target. */ + enum class EMaterialBlendMode : uint8_t + { + /** Fully opaque material. */ + Opaque, + + /** Discards fragments whose opacity is below the alpha cutoff. */ + Masked, + + /** Blends fragments with the color already in the target. */ + Translucent, + }; + + /** @brief Defines the numeric value type used by a material. */ enum class EMaterialValueType : uint8_t { Float, Float2, Float3, Float4, }; - /** - * @brief Defines one parameter in a material schema. - */ + /** @brief Defines one parameter in a material schema. */ struct SMaterialParameterDefinition { /** Parameter category. */ @@ -52,20 +71,24 @@ namespace Elixir::Materials }; /** - * @brief Defines a material graph, parameter schema, and supported usages. + * @brief Defines a material graph, parameter schema, and render usage. * * Instances created from a material inherit its parameter definitions and - * default values. Changing the graph, schema, or usages increments the + * default values. Changing the graph, schema, or usage increments the * material revision. */ class ELIXIR_API Material : public std::enable_shared_from_this { public: /** - * @brief Creates a material with a name. + * @brief Creates a material with a name and renderer usage. * @param name Material name. + * @param usage Renderer usage to compile and render for this material. */ - explicit Material(std::string name) : m_Name(std::move(name)) {} + explicit Material( + std::string name, + EMaterialUsage usage = EMaterialUsage::Surface + ) : m_Name(std::move(name)), m_Usage(usage) {} /** * @brief Creates an instance of this material. @@ -86,20 +109,51 @@ namespace Elixir::Materials */ const MaterialGraph& GetGraph() const { return m_Graph; } + /** @brief Returns the renderer usage for this material. */ + EMaterialUsage GetUsage() const { return m_Usage; } + + /** + * @brief Sets the lighting model used when rendering this material as a surface. + * @param model Lighting model to use. + * @return True when the shading model changed. + */ + bool SetShadingModel(EMaterialShadingModel model); + + /** @brief Returns the lighting model used by this material as a surface. */ + EMaterialShadingModel GetShadingModel() const { return m_ShadingModel; } + /** - * @brief Enables or disables a material usage. - * @param usage Usage to update. - * @param enabled Whether the usage is supported. - * @return `true` if the supported-usage set changed. + * @brief Sets how renderers combine this material with the color target. + * @param mode Blend mode to use for every compatible material pass. + * @return True when the blend mode changed. */ - bool SetUsage(EMaterialUsage usage, bool enabled); + bool SetBlendMode(EMaterialBlendMode mode); + + /** @brief Returns the blend mode used by compatible material passes. */ + EMaterialBlendMode GetBlendMode() const { return m_BlendMode; } /** - * @brief Checks whether a material usage is supported. - * @param usage Usage to check. - * @return `true` if the usage is enabled. + * @brief Sets the opacity threshold used by masked material passes. + * + * The cutoff is clamped to [0, 1] range. + * + * @param cutoff Opacity required for a fragment to remain visible. + * @return True when the cutoff changed. */ - bool SupportsUsage(EMaterialUsage usage) const; + bool SetAlphaCutoff(float cutoff); + + /** @brief Returns the opacity threshold used by masked material passes. */ + float GetAlphaCutoff() const { return m_AlphaCutoff; } + + /** + * @brief Enables or disables back-face culling for compatible mesh passes. + * @param enabled Whether both faces should be rendered. + * @return True when the setting changed. + */ + bool SetDoubleSided(bool enabled); + + /** @brief Checks whether compatible mesh passes render both triangle faces. */ + bool IsDoubleSided() const { return m_DoubleSided; } /** * @brief Updates a parameter default value. @@ -160,9 +214,6 @@ namespace Elixir::Materials /** @brief Returns the parameter schema. */ const auto& GetParameters() const { return m_Parameters; } - /** @brief Returns the bit mask of supported usages. */ - uint32_t GetUsageMask() const { return m_UsageMask; } - /** @brief Returns the current material revision. */ uint32_t GetRevision() const { return m_Revision; } @@ -176,17 +227,12 @@ namespace Elixir::Materials std::string m_Name; MaterialGraph m_Graph; std::unordered_map m_Parameters; - uint32_t m_UsageMask = 0; + EMaterialUsage m_Usage = EMaterialUsage::Surface; + EMaterialShadingModel m_ShadingModel = EMaterialShadingModel::Lit; + EMaterialBlendMode m_BlendMode = EMaterialBlendMode::Opaque; + bool m_DoubleSided = false; + float m_AlphaCutoff = 0.5f; uint32_t m_Revision = 1; }; - /** - * @brief Returns the bit mask for one material usage. - * @param usage Material usage. - * @return Bit mask that represents @p usage. - */ - constexpr uint32_t GetMaterialUsageMask(const EMaterialUsage usage) - { - return 1u << static_cast(usage); - } } diff --git a/Elixir/Source/Engine/Materials/MaterialGraph.cpp b/Elixir/Source/Engine/Materials/MaterialGraph.cpp index 5ac852a8..44ed85df 100644 --- a/Elixir/Source/Engine/Materials/MaterialGraph.cpp +++ b/Elixir/Source/Engine/Materials/MaterialGraph.cpp @@ -11,12 +11,18 @@ namespace Elixir::Materials { switch (channel) { - case EMaterialChannel::BaseColor: return "BaseColor"; - case EMaterialChannel::Normal: return "Normal"; - case EMaterialChannel::Metallic: return "Metallic"; - case EMaterialChannel::Roughness: return "Roughness"; - case EMaterialChannel::Opacity: return "Opacity"; - case EMaterialChannel::Emissive: return "Emissive"; + case EMaterialChannel::BaseColor: return "BaseColor"; + case EMaterialChannel::Normal: return "Normal"; + case EMaterialChannel::Metallic: return "Metallic"; + case EMaterialChannel::Roughness: return "Roughness"; + case EMaterialChannel::Opacity: return "Opacity"; + case EMaterialChannel::Emissive: return "Emissive"; + case EMaterialChannel::AmbientOcclusion: return "AmbientOcclusion"; + case EMaterialChannel::Specular: return "Specular"; + case EMaterialChannel::SpecularColor: return "SpecularColor"; + case EMaterialChannel::ClearCoat: return "ClearCoat"; + case EMaterialChannel::ClearCoatRoughness: return "ClearCoatRoughness"; + case EMaterialChannel::ClearCoatBottomNormal: return "ClearCoatBottomNormal"; } return "BaseColor"; @@ -29,7 +35,11 @@ namespace Elixir::Materials { const bool isScalar = channel == EMaterialChannel::Metallic || channel == EMaterialChannel::Roughness || - channel == EMaterialChannel::Opacity; + channel == EMaterialChannel::Opacity || + channel == EMaterialChannel::AmbientOcclusion || + channel == EMaterialChannel::Specular || + channel == EMaterialChannel::ClearCoat || + channel == EMaterialChannel::ClearCoatRoughness; if (isScalar) return expression.ValueType == EMaterialValueType::Float @@ -98,6 +108,44 @@ namespace Elixir::Materials return false; } + std::unordered_set visiting; + std::unordered_set visited; + std::function visit = [&](const uint32_t id) + { + if (visiting.contains(id)) + { + if (error) + *error = "Material graph contains a cycle involving node " + std::to_string(id) + "."; + + return false; + } + + if (visited.contains(id)) + return true; + + visiting.insert(id); + + const auto nodeIt = m_Nodes.find(id); + if (nodeIt != m_Nodes.end()) + { + for (const int32_t input : nodeIt->second.Inputs) + { + if (input >= 0 && !visit(static_cast(input))) + return false; + } + } + + visiting.erase(id); + visited.insert(id); + return true; + }; + + for (const auto& [id, _] : m_Nodes) + { + if (!visit(id)) + return false; + } + return true; } diff --git a/Elixir/Source/Engine/Materials/MaterialGraph.h b/Elixir/Source/Engine/Materials/MaterialGraph.h index 585bfb68..49b4ec87 100644 --- a/Elixir/Source/Engine/Materials/MaterialGraph.h +++ b/Elixir/Source/Engine/Materials/MaterialGraph.h @@ -26,6 +26,24 @@ namespace Elixir::Materials /** @brief Surface emissive color. */ Emissive, + + /** @brief Ambient-occlusion multiplier for indirect and direct lighting. */ + AmbientOcclusion, + + /** @brief Dielectric specular intensity. */ + Specular, + + /** @brief Dielectric specular tint. */ + SpecularColor, + + /** @brief Coverage of the clear-coat layer. */ + ClearCoat, + + /** @brief Roughness of the clear-coat layer. */ + ClearCoatRoughness, + + /** @brief Tangent-space normal of the layer below clear coat. */ + ClearCoatBottomNormal, }; /** @brief Maps material parameter names to generated HLSL expressions. */ @@ -78,6 +96,12 @@ namespace Elixir::Materials */ void SetChannel(EMaterialChannel channel, uint32_t nodeId); + /** + * @brief Checks that nodes and their connections form a valid graph. + * @param parameters Defines the material parameters available to graph nodes. + * @param error Receives a description when validation fails. + * @return `true` when the graph can be compiled safely. + */ bool Validate( const MaterialNodeValidationContext& parameters, std::string* error = nullptr diff --git a/Elixir/Source/Engine/Materials/MaterialNode.h b/Elixir/Source/Engine/Materials/MaterialNode.h index 2e237f33..6b81d0e3 100644 --- a/Elixir/Source/Engine/Materials/MaterialNode.h +++ b/Elixir/Source/Engine/Materials/MaterialNode.h @@ -7,6 +7,7 @@ namespace Elixir::Materials { struct SMaterialGraphBindings; enum class EMaterialValueType : uint8_t; + enum class EMaterialUsage : uint8_t; /** * @brief Describes one input accepted by a material node. @@ -58,6 +59,9 @@ namespace Elixir::Materials * @return True if the texture parameter exists. */ virtual bool HasTextureParameter(std::string_view name) const = 0; + + /** @brief Returns the renderer usage of the material being validated. */ + virtual EMaterialUsage GetUsage() const = 0; }; /** diff --git a/Elixir/Source/Engine/Materials/MaterialRegistry.cpp b/Elixir/Source/Engine/Materials/MaterialRegistry.cpp index aec7154c..a14b55cb 100644 --- a/Elixir/Source/Engine/Materials/MaterialRegistry.cpp +++ b/Elixir/Source/Engine/Materials/MaterialRegistry.cpp @@ -21,6 +21,17 @@ namespace Elixir::Materials return m_Materials.emplace(material->GetName(), material).second; } + bool MaterialRegistry::Replace(const Ref& material) + { + if (!material || material->GetName().empty()) return false; + + const auto found = m_Materials.find(material->GetName()); + if (found == m_Materials.end()) return false; + + found->second = material; + return true; + } + Ref MaterialRegistry::Find(const std::string_view name) const { const auto found = m_Materials.find(std::string(name)); diff --git a/Elixir/Source/Engine/Materials/MaterialRegistry.h b/Elixir/Source/Engine/Materials/MaterialRegistry.h index c19d0648..865b0435 100644 --- a/Elixir/Source/Engine/Materials/MaterialRegistry.h +++ b/Elixir/Source/Engine/Materials/MaterialRegistry.h @@ -21,6 +21,13 @@ namespace Elixir::Materials */ bool Register(const Ref& material); + /** + * @brief Replaces a registered material with the same name. + * @param material Material that replaces the current registration. + * @return True when a registered material was replaced. + */ + bool Replace(const Ref& material); + /** * @brief Finds a registered material by name. * @param name Material name. diff --git a/Elixir/Source/Engine/Materials/MaterialSystem.cpp b/Elixir/Source/Engine/Materials/MaterialSystem.cpp index be86e744..84affc73 100644 --- a/Elixir/Source/Engine/Materials/MaterialSystem.cpp +++ b/Elixir/Source/Engine/Materials/MaterialSystem.cpp @@ -6,10 +6,15 @@ namespace Elixir::Materials MaterialSystem::MaterialSystem( const GraphicsContext* context, const ShaderLoader* shaderLoader, + const Ref& sceneTarget, const SMaterialSystemConfig config ) : m_ProxyResolver(shaderLoader), m_ProxyCache(m_ProxyResolver), - m_Renderer(CreateScope(context, config.InitialFrameCapacity)) {} + m_Renderer(CreateScope( + context, + sceneTarget, + config.InitialFrameCapacity + )) {} void MaterialSystem::BeginFrame() { diff --git a/Elixir/Source/Engine/Materials/MaterialSystem.h b/Elixir/Source/Engine/Materials/MaterialSystem.h index b35770da..de566410 100644 --- a/Elixir/Source/Engine/Materials/MaterialSystem.h +++ b/Elixir/Source/Engine/Materials/MaterialSystem.h @@ -5,7 +5,11 @@ #include #include -namespace Elixir { class ShaderLoader; } +namespace Elixir +{ + class Image; + class ShaderLoader; +} namespace Elixir::Materials { @@ -30,6 +34,7 @@ namespace Elixir::Materials * @brief Creates a material system. * @param context Graphics context used by the material renderer. * @param shaderLoader Loader used to obtain material shaders. + * @param sceneTarget Color target that receives material rendering. * @param config Initial frame-data storage configuration. * @pre context and shaderLoader are valid. * @pre config.InitialFrameCapacity is greater than zero. @@ -37,6 +42,7 @@ namespace Elixir::Materials MaterialSystem( const GraphicsContext* context, const ShaderLoader* shaderLoader, + const Ref& sceneTarget, SMaterialSystemConfig config ); @@ -70,7 +76,9 @@ namespace Elixir::Materials * @param instance Material instance to resolve. * @return The render proxy, or null when the instance cannot be resolved. */ - Ref ResolveMaterialProxy(const Ref& instance); + Ref ResolveMaterialProxy( + const Ref& instance + ); MaterialProxyResolver m_ProxyResolver; MaterialProxyCache m_ProxyCache; diff --git a/Elixir/Source/Engine/Materials/Nodes/Append.h b/Elixir/Source/Engine/Materials/Nodes/Append.h new file mode 100644 index 00000000..72651784 --- /dev/null +++ b/Elixir/Source/Engine/Materials/Nodes/Append.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +namespace Elixir::Materials::Nodes +{ + /** @brief Appends two scalar or vector values into one vector. */ + class Append final : public MaterialNode + { + public: + /** @brief Creates a vector append node. */ + Append() + : MaterialNode({{ + "A", + EMaterialValueType::Float4, + "0.0", + EMaterialValueType::Float + }, { + "B", + EMaterialValueType::Float4, + "0.0", + EMaterialValueType::Float + }}) {} + + std::string_view GetTypeName() const override { return "Material.Append"; } + + SMaterialExpression Emit(const MaterialEmitContext& context) const override + { + const auto& a = context.Input(0); + const auto& b = context.Input(1); + const int componentCount = + MaterialEmitContext::Components(a.ValueType) + + MaterialEmitContext::Components(b.ValueType); + + if (componentCount < 2 || componentCount > 4) + { + return { + .Code = "float2(0.0, 0.0)", + .ValueType = EMaterialValueType::Float2 + }; + } + + const auto valueType = static_cast(componentCount - 1); + return { + .Code = std::string(MaterialEmitContext::TypeName(valueType)) + + "(" + a.Code + ", " + b.Code + ")", + .ValueType = valueType + }; + } + }; +} diff --git a/Elixir/Source/Engine/Materials/Nodes/Color.h b/Elixir/Source/Engine/Materials/Nodes/Color.h new file mode 100644 index 00000000..ad9ecfaa --- /dev/null +++ b/Elixir/Source/Engine/Materials/Nodes/Color.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +namespace Elixir::Materials::Nodes +{ + /** + * @brief Outputs the RGBA color supplied by the particle renderer. + */ + class Color final : public MaterialNode + { + public: + std::string_view GetTypeName() const override { return "Material.Color"; } + + bool Validate( + const MaterialNodeValidationContext& context, + std::string& error + ) const override + { + if (context.GetUsage() == EMaterialUsage::Particle) + return true; + + error = "Material.Color requires particle material usage."; + return false; + } + + SMaterialExpression Emit(const MaterialEmitContext&) const override + { + return { + .Code = "input.Color", + .ValueType = EMaterialValueType::Float4 + }; + } + }; +} diff --git a/Elixir/Source/Engine/Materials/Nodes/Cosine.h b/Elixir/Source/Engine/Materials/Nodes/Cosine.h new file mode 100644 index 00000000..0b2041dd --- /dev/null +++ b/Elixir/Source/Engine/Materials/Nodes/Cosine.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +namespace Elixir::Materials::Nodes +{ + /** @brief Applies cosine to every component. */ + class Cosine final : public UnaryOperationNode + { + public: + std::string_view GetTypeName() const override { return "Material.Cosine"; } + + SMaterialExpression Emit(const MaterialEmitContext& context) const override + { + const auto& input = context.Input(0); + return { + .Code = "cos(" + input.Code + ")", + .ValueType = input.ValueType, + }; + } + }; +} diff --git a/Elixir/Source/Engine/Materials/Nodes/Dot.h b/Elixir/Source/Engine/Materials/Nodes/Dot.h index 978e98c4..146ac38d 100644 --- a/Elixir/Source/Engine/Materials/Nodes/Dot.h +++ b/Elixir/Source/Engine/Materials/Nodes/Dot.h @@ -1,6 +1,6 @@ #pragma once -#include +#include namespace Elixir::Materials::Nodes { diff --git a/Elixir/Source/Engine/Materials/Nodes/FlattenNormal.h b/Elixir/Source/Engine/Materials/Nodes/FlattenNormal.h new file mode 100644 index 00000000..7bab18f5 --- /dev/null +++ b/Elixir/Source/Engine/Materials/Nodes/FlattenNormal.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include + +namespace Elixir::Materials::Nodes +{ + /** + * @brief Blends a tangent-space normal toward the flat normal. + * + * A flatness of zero returns the flat tangent-space normal. A flatness of one + * preserves the input normal. + */ + class FlattenNormal final : public MaterialNode + { + public: + FlattenNormal() + : MaterialNode({{ + "Normal", + EMaterialValueType::Float3, + "float3(0.0, 0.0, 1.0)", + EMaterialValueType::Float3 + }, + { + "Flatness", + EMaterialValueType::Float, + "1.0", + EMaterialValueType::Float + }}) {} + + std::string_view GetTypeName() const override { return "Material.FlattenNormal"; } + + SMaterialExpression Emit(const MaterialEmitContext& context) const override + { + const auto normal = context.Widen( + context.Input(0), + EMaterialValueType::Float3 + ); + + const auto flatness = context.Widen( + context.Input(1), + EMaterialValueType::Float + ); + + return { + .Code = "normalize(float3((" + normal + ").xy * " + + flatness + ", lerp(1.0, (" + normal + ").z, " + + flatness + ")))", + .ValueType = EMaterialValueType::Float3 + }; + } + }; +} diff --git a/Elixir/Source/Engine/Materials/Nodes/Fresnel.h b/Elixir/Source/Engine/Materials/Nodes/Fresnel.h index 1e2e53e8..79593a45 100644 --- a/Elixir/Source/Engine/Materials/Nodes/Fresnel.h +++ b/Elixir/Source/Engine/Materials/Nodes/Fresnel.h @@ -1,7 +1,7 @@ #pragma once -#include -#include +#include +#include namespace Elixir::Materials::Nodes { diff --git a/Elixir/Source/Engine/Materials/Nodes/Lerp.h b/Elixir/Source/Engine/Materials/Nodes/Lerp.h index 2e8abf3c..190e1d9f 100644 --- a/Elixir/Source/Engine/Materials/Nodes/Lerp.h +++ b/Elixir/Source/Engine/Materials/Nodes/Lerp.h @@ -1,7 +1,6 @@ #pragma once -#include -#include +#include namespace Elixir::Materials::Nodes { diff --git a/Elixir/Source/Engine/Materials/Nodes/OneMinus.h b/Elixir/Source/Engine/Materials/Nodes/OneMinus.h index 20669db2..27cd1eda 100644 --- a/Elixir/Source/Engine/Materials/Nodes/OneMinus.h +++ b/Elixir/Source/Engine/Materials/Nodes/OneMinus.h @@ -1,6 +1,6 @@ #pragma once -#include +#include namespace Elixir::Materials::Nodes { diff --git a/Elixir/Source/Engine/Materials/Nodes/Power.h b/Elixir/Source/Engine/Materials/Nodes/Power.h index e962aa73..81cc1da0 100644 --- a/Elixir/Source/Engine/Materials/Nodes/Power.h +++ b/Elixir/Source/Engine/Materials/Nodes/Power.h @@ -1,6 +1,6 @@ #pragma once -#include +#include namespace Elixir::Materials::Nodes { diff --git a/Elixir/Source/Engine/Materials/Nodes/Saturate.h b/Elixir/Source/Engine/Materials/Nodes/Saturate.h index 7b053a28..1a8a1431 100644 --- a/Elixir/Source/Engine/Materials/Nodes/Saturate.h +++ b/Elixir/Source/Engine/Materials/Nodes/Saturate.h @@ -1,6 +1,6 @@ #pragma once -#include +#include namespace Elixir::Materials::Nodes { diff --git a/Elixir/Source/Engine/Materials/Nodes/ScaleNormal.h b/Elixir/Source/Engine/Materials/Nodes/ScaleNormal.h new file mode 100644 index 00000000..1ccb252c --- /dev/null +++ b/Elixir/Source/Engine/Materials/Nodes/ScaleNormal.h @@ -0,0 +1,45 @@ +#pragma once + +#include + +namespace Elixir::Materials::Nodes +{ + /** @brief Scales the XY components of a tangent-space normal. */ + class ScaleNormal final : public MaterialNode + { + public: + ScaleNormal() + : MaterialNode({{ + "Normal", + EMaterialValueType::Float3, + "float3(0.0, 0.0, 1.0)", + EMaterialValueType::Float3 + }, + { + "Scale", + EMaterialValueType::Float, + "1.0", + EMaterialValueType::Float + }}) {} + + std::string_view GetTypeName() const override { return "Material.ScaleNormal"; } + + SMaterialExpression Emit(const MaterialEmitContext& context) const override + { + const auto normal = context.Widen( + context.Input(0), + EMaterialValueType::Float3 + ); + const auto scale = context.Widen( + context.Input(1), + EMaterialValueType::Float + ); + + return { + .Code = "normalize(float3((" + normal + ").xy * " + scale + + ", (" + normal + ").z))", + .ValueType = EMaterialValueType::Float3 + }; + } + }; +} diff --git a/Elixir/Source/Engine/Materials/Nodes/Sine.h b/Elixir/Source/Engine/Materials/Nodes/Sine.h index 1b7ce2e4..2d4ab3ec 100644 --- a/Elixir/Source/Engine/Materials/Nodes/Sine.h +++ b/Elixir/Source/Engine/Materials/Nodes/Sine.h @@ -1,6 +1,6 @@ #pragma once -#include +#include namespace Elixir::Materials::Nodes { diff --git a/Elixir/Source/Engine/Materials/Nodes/TexCoord.h b/Elixir/Source/Engine/Materials/Nodes/TexCoord.h index 8c439692..68b17393 100644 --- a/Elixir/Source/Engine/Materials/Nodes/TexCoord.h +++ b/Elixir/Source/Engine/Materials/Nodes/TexCoord.h @@ -1,24 +1,38 @@ #pragma once -#include -#include +#include "glm/ext/scalar_uint_sized.hpp" + +#include +#include namespace Elixir::Materials::Nodes { /** - * @brief Outputs the input texture coordinates. + * @brief Outputs one texture-coordinate channel of a static mesh. */ class TexCoord final : public MaterialNode { public: + /** + * @brief Creates a texture-coordinate node. + * @param coordinateIndex Zero-based texture-coordinate channel. + */ + explicit TexCoord(const uint32_t coordinateIndex = 0) + : m_CoordinateIndex(std::min(coordinateIndex, 1u)) {} + std::string_view GetTypeName() const override { return "Material.TexCoord"; } SMaterialExpression Emit(const MaterialEmitContext& context) const override { return { - .Code = "input.TexCoord", + .Code = m_CoordinateIndex == 0 + ? "input.TexCoord" + : "input.TexCoord1", .ValueType = EMaterialValueType::Float2 }; } + + private: + uint32_t m_CoordinateIndex; }; } \ No newline at end of file diff --git a/Elixir/Source/Engine/Materials/Nodes/TextureSample.h b/Elixir/Source/Engine/Materials/Nodes/TextureSample.h index 8f598628..60162013 100644 --- a/Elixir/Source/Engine/Materials/Nodes/TextureSample.h +++ b/Elixir/Source/Engine/Materials/Nodes/TextureSample.h @@ -6,7 +6,28 @@ namespace Elixir::Materials::Nodes { /** - * @brief Samples a named texture material parameter. + * @brief Defines how a texture is interpreted by a material. + * + * The type lets the compiler select semantic decoding without exposing that + * implementation detail in the material graph. + */ + enum class ETextureSampleType : uint8_t + { + /** Texture stores display color. */ + Color, + + /** Texture stores linear color data. */ + LinearColor, + + /** Texture stores a tangent-space normal map. */ + Normal, + + /** Texture stores non-color mask data. */ + Mask, + }; + + /** + * @brief Samples a named texture material parameter using a declared semantic. */ class TextureSample final : public MaterialNode { @@ -14,33 +35,48 @@ namespace Elixir::Materials::Nodes /** * @brief Creates a texture sampler for a material texture parameter. * @param parameterName Texture parameter name. + * @param sampleType Semantic type used to interpret the sampled texture. */ - explicit TextureSample(std::string parameterName) - : MaterialNode({{ + explicit TextureSample( + std::string parameterName, + const ETextureSampleType sampleType = ETextureSampleType::Color + ) : MaterialNode({{ "UV", EMaterialValueType::Float2, "input.TexCoord", EMaterialValueType::Float2 }}), - m_ParameterName(std::move(parameterName)) {} + m_ParameterName(std::move(parameterName)), + m_SampleType(sampleType) {} std::string_view GetTypeName() const override { return "Material.TextureSample"; } SMaterialExpression Emit(const MaterialEmitContext& context) const override { const auto index = context.TextureParameter(m_ParameterName); + const auto uv = context.Widen( + context.Input(0), + EMaterialValueType::Float2 + ); + + if (m_SampleType == ETextureSampleType::Normal) + { + return { + .Code = "SampleNormal(" + index + ", " + uv + ")", + .ValueType = EMaterialValueType::Float3, + }; + } + return { .Code = "(" + index + " == 0xFFFFFFFFu " + "? float4(1.0, 1.0, 1.0, 1.0) " + - ": SampleTex(" + - index + ", " + - context.Widen(context.Input(0), EMaterialValueType::Float2) + - "))", + ": SampleTex(" + index + ", " + uv + "))", .ValueType = EMaterialValueType::Float4 }; } private: std::string m_ParameterName; + ETextureSampleType m_SampleType; }; -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/Materials/Nodes/UnaryOperationNode.h b/Elixir/Source/Engine/Materials/Nodes/UnaryOperationNode.h index b90f0d76..5a48eae9 100644 --- a/Elixir/Source/Engine/Materials/Nodes/UnaryOperationNode.h +++ b/Elixir/Source/Engine/Materials/Nodes/UnaryOperationNode.h @@ -5,17 +5,15 @@ namespace Elixir::Materials::Nodes { - /** - * @brief Applies a single-input operation while preserving input width. - */ + /** @brief Applies a single-input operation while preserving input width. */ class UnaryOperationNode : public MaterialNode { protected: UnaryOperationNode() - : m_Inputs{{ + : MaterialNode({{ "Value", EMaterialValueType::Float4, "0.0" - }} {} + }}) {} }; -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/Materials/Rendering/FrameTable.cpp b/Elixir/Source/Engine/Materials/Rendering/FrameTable.cpp index e89b37aa..3d8f60fc 100644 --- a/Elixir/Source/Engine/Materials/Rendering/FrameTable.cpp +++ b/Elixir/Source/Engine/Materials/Rendering/FrameTable.cpp @@ -39,6 +39,9 @@ namespace Elixir::Materials::Rendering SMaterialFrameData data{}; std::ranges::fill(data.TextureIndices, m_FallbackTextureIndex); + data.BlendMode = (uint32_t)material.GetBlendMode(); + data.AlphaCutoff = material.GetAlphaCutoff(); + const auto& values = material.GetValues(); const auto valueCount = std::min(values.size(), data.Values.size()); std::copy_n(values.begin(), valueCount, data.Values.begin()); diff --git a/Elixir/Source/Engine/Materials/Rendering/FrameTable.h b/Elixir/Source/Engine/Materials/Rendering/FrameTable.h index bcca3e94..499aa06f 100644 --- a/Elixir/Source/Engine/Materials/Rendering/FrameTable.h +++ b/Elixir/Source/Engine/Materials/Rendering/FrameTable.h @@ -19,6 +19,12 @@ namespace Elixir::Materials::Rendering /** @brief Bindless texture indices, stored by material texture slot. */ std::array TextureIndices{}; + + /** @brief Material blend mode encoded as EMaterialBlendMode. */ + uint32_t BlendMode = 0; + + /** @brief Opacity threshold used when BlendMode is EMaterialBlendMode::Masked. */ + float AlphaCutoff = 0.5f; }; /** diff --git a/Elixir/Source/Engine/Materials/Rendering/MaterialRenderProxy.cpp b/Elixir/Source/Engine/Materials/Rendering/MaterialRenderProxy.cpp index fc4669fa..3cb41468 100644 --- a/Elixir/Source/Engine/Materials/Rendering/MaterialRenderProxy.cpp +++ b/Elixir/Source/Engine/Materials/Rendering/MaterialRenderProxy.cpp @@ -10,14 +10,19 @@ namespace Elixir::Materials::Rendering const MaterialInstance& instance ) { + const auto& parent = instance.GetParent(); + if (!material || - !instance.GetParent() || - material->MaterialRevision != instance.GetParent()->GetRevision()) + !parent || + material->MaterialRevision != parent->GetRevision()) return nullptr; auto proxy = CreateRef(); proxy->m_CompiledMaterial = std::move(material); proxy->m_InstanceRevision = instance.GetRevision(); + proxy->m_BlendMode = parent->GetBlendMode(); + proxy->m_DoubleSided = parent->IsDoubleSided(); + proxy->m_AlphaCutoff = parent->GetAlphaCutoff(); for (const auto& parameter : proxy->m_CompiledMaterial->Parameters) { diff --git a/Elixir/Source/Engine/Materials/Rendering/MaterialRenderProxy.h b/Elixir/Source/Engine/Materials/Rendering/MaterialRenderProxy.h index 385d6398..64a9027e 100644 --- a/Elixir/Source/Engine/Materials/Rendering/MaterialRenderProxy.h +++ b/Elixir/Source/Engine/Materials/Rendering/MaterialRenderProxy.h @@ -60,10 +60,22 @@ namespace Elixir::Materials::Rendering */ const std::vector>& GetTextures() const { return m_Textures; } + /** @brief Returns the blend mode captured from the source material. */ + EMaterialBlendMode GetBlendMode() const { return m_BlendMode; } + + /** @brief Checks whether the source material renders both triangle faces. */ + bool IsDoubleSided() const { return m_DoubleSided; } + + /** @brief Returns the alpha cutoff captured from the source material. */ + float GetAlphaCutoff() const { return m_AlphaCutoff; } + private: Ref m_CompiledMaterial; uint32_t m_InstanceRevision = 0; std::vector m_Values; std::vector> m_Textures; + EMaterialBlendMode m_BlendMode = EMaterialBlendMode::Opaque; + bool m_DoubleSided = false; + float m_AlphaCutoff = 0.5f; }; } \ No newline at end of file diff --git a/Elixir/Source/Engine/Materials/Rendering/MaterialRenderScene.h b/Elixir/Source/Engine/Materials/Rendering/MaterialRenderScene.h index aede595d..e13de5f0 100644 --- a/Elixir/Source/Engine/Materials/Rendering/MaterialRenderScene.h +++ b/Elixir/Source/Engine/Materials/Rendering/MaterialRenderScene.h @@ -98,6 +98,19 @@ namespace Elixir::Materials::Rendering uint32_t FirstInstance = 0; }; + /** @brief Describes one indexed range of a draw call. */ + struct SIndexedDrawCommand + { + /** Number of indices to draw. */ + uint32_t IndexCount = 0; + + /** First index in the bound index buffer. */ + uint32_t FirstIndex = 0; + + /** Offset added to each index before vertex fetch. */ + uint32_t VertexOffset = 0; + }; + /** * @brief Stores shared resources for render items with compatible geometry. */ @@ -112,8 +125,20 @@ namespace Elixir::Materials::Rendering /** Storage buffers required by the material pass. */ std::vector StorageBuffers; + /** Textures required by the material pass independently of the material. */ + std::vector Textures; + + /** Samplers required by the material pass independently of the material. */ + std::vector Samplers; + /** Vertex buffers required by the draw. */ std::vector VertexBuffers; + + /** Index buffer used by indexed draws, or null for non-indexed geometry. */ + const Buffer* IndexBuffer = nullptr; + + /** Index element type used by @ref IndexBuffer. */ + EIndexType IndexType = EIndexType::UInt32; }; /** @@ -127,6 +152,12 @@ namespace Elixir::Materials::Rendering /** Material instance used by the pass. */ Ref Material; + /** + * Camera-space distance used to order translucent draws. + * Greater values are rendered first. + */ + float SortDepth = 0.0f; + /** Index of the geometry used by this item. */ uint32_t GeometryIndex = UINT32_MAX; @@ -135,6 +166,9 @@ namespace Elixir::Materials::Rendering /** Draw range for the item. */ SDrawCommand Draw; + + /** Optional indexed draw range. */ + std::optional IndexedDraw; }; /** diff --git a/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp b/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp index f4ec6d2d..9ad66ff8 100644 --- a/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp +++ b/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp @@ -1,6 +1,7 @@ #include "epch.h" #include "Renderer.h" +#include #include #include #include @@ -9,13 +10,16 @@ namespace Elixir::Materials::Rendering { Renderer::Renderer( const GraphicsContext* context, + const Ref& sceneTarget, const uint32_t materialCapacity ) : m_MaterialCapacity(materialCapacity), m_FrameSlots(*context), m_Textures(context), + m_SceneTarget(sceneTarget), m_Context(context) { EE_CORE_ASSERT(context, "Material renderer requires a graphics context.") + EE_CORE_ASSERT(m_SceneTarget, "Material renderer requires a scene target.") EE_CORE_ASSERT( m_MaterialCapacity > 0, "Material renderer frame capacity must be greater than zero." @@ -38,21 +42,55 @@ namespace Elixir::Materials::Rendering m_Textures.BeginFrame(m_CurrentFrameNumber); } - SRenderResult Renderer::RenderFrame(std::span scenes) + SRenderResult Renderer::RenderFrame(const std::span scenes) { SRenderResult result{}; - if (scenes.empty()) return result; + + EE_CORE_ASSERT(m_SceneTarget, "Material rendering requires a scene target.") EE_CORE_ASSERT( m_CurrentFrameNumber == m_Context->GetFrameNumber(), "Material rendering requires BeginFrame for the current graphics frame." ) + if (scenes.empty()) + { + m_Textures.EndFrame(); + return result; + } + + FrameTable materialTable{ + m_MaterialCapacity, + m_Textures.GetFallbackIndex(), + [this](const Ref& texture) + { + return m_Textures.Resolve(texture); + } + }; + + std::vector preparedScenes; + preparedScenes.reserve(scenes.size()); + + for (const auto& scene : scenes) + preparedScenes.push_back(PrepareScene(scene, materialTable)); + + m_Textures.EndFrame(); + + if (!materialTable.GetData().empty()) + { + GetActiveMaterialBuffer()->UpdateData( + materialTable.GetData().data(), + materialTable.GetData().size() * sizeof(SMaterialFrameData) + ); + } + + result.MaterialCount = materialTable.GetCount(); + const auto cmd = m_Context->GetSecondaryCommandBuffer(); - const auto extent = m_Context->GetRenderTarget()->GetExtent(); + const auto extent = m_SceneTarget->GetExtent(); const SRenderingInfo renderingInfo{ - .ColorAttachment = m_Context->GetRenderTarget(), + .ColorAttachment = m_SceneTarget, .DepthStencilAttachment = m_Context->GetDepthStencilRenderTarget(), .RenderArea = extent, }; @@ -72,11 +110,9 @@ namespace Elixir::Materials::Rendering .Extent = extent, }}); - for (const auto& scene : scenes) + for (const auto& scene : preparedScenes) { - const auto prepared = PrepareScene(scene); - const auto sceneResult = RecordScene(cmd, prepared); - result.MaterialCount += sceneResult.MaterialCount; + const auto sceneResult = RecordScene(cmd, scene); result.BatchCount += sceneResult.BatchCount; result.DrawCount += sceneResult.DrawCount; } @@ -95,10 +131,10 @@ namespace Elixir::Materials::Rendering { const auto usage = GetUsage(pass); const auto compiled = material.GetCompiledMaterial(); - if (!compiled || !compiled->SupportsUsage(usage)) + if (!compiled || compiled->GetUsage() != usage) return std::nullopt; - const auto& shader = compiled->GetShader(usage); + const auto& shader = compiled->GetShader(GetShaderVariant(pass)); if (!shader) return std::nullopt; @@ -109,28 +145,51 @@ namespace Elixir::Materials::Rendering { switch (pass) { - case EMaterialPass::ParticleSprite: return EMaterialUsage::ParticleSprite; - case EMaterialPass::ParticleRibbon: return EMaterialUsage::ParticleRibbon; - case EMaterialPass::ParticleMesh: return EMaterialUsage::ParticleMesh; + case EMaterialPass::Surface: return EMaterialUsage::Surface; + case EMaterialPass::ParticleSprite: + case EMaterialPass::ParticleRibbon: + case EMaterialPass::ParticleMesh: return EMaterialUsage::Particle; } EE_CORE_ASSERT(false, "Material pass does not have a material usage.") - return EMaterialUsage::ParticleSprite; + return EMaterialUsage::Particle; + } + + Compilation::EMaterialShaderVariant Renderer::GetShaderVariant(const EMaterialPass pass) + { + switch (pass) + { + case EMaterialPass::Surface: + return Compilation::EMaterialShaderVariant::Surface; + case EMaterialPass::ParticleSprite: + return Compilation::EMaterialShaderVariant::ParticleSprite; + case EMaterialPass::ParticleRibbon: + return Compilation::EMaterialShaderVariant::ParticleRibbon; + case EMaterialPass::ParticleMesh: + return Compilation::EMaterialShaderVariant::ParticleMesh; + } + + EE_CORE_ASSERT(false, "Material pass does not have a shader variant.") + return Compilation::EMaterialShaderVariant::ParticleSprite; } uint32_t Renderer::GetPassOrder(const EMaterialPass pass) { switch (pass) { - case EMaterialPass::ParticleSprite: return 2; - case EMaterialPass::ParticleRibbon: return 1; - case EMaterialPass::ParticleMesh: return 0; + case EMaterialPass::Surface: return 0; + case EMaterialPass::ParticleSprite: return 3; + case EMaterialPass::ParticleRibbon: return 2; + case EMaterialPass::ParticleMesh: return 1; } return UINT32_MAX; } - Renderer::SPreparedRenderScene Renderer::PrepareScene(const SPreparedScene& scene) + Renderer::SPreparedRenderScene Renderer::PrepareScene( + const SPreparedScene& scene, + FrameTable& materialTable + ) { SPreparedRenderScene prepared{ .Scene = scene.Scene, @@ -138,20 +197,11 @@ namespace Elixir::Materials::Rendering if (!scene.Scene) return prepared; - const auto table = CreateRef( - m_MaterialCapacity, - m_Textures.GetFallbackIndex(), - [this](const Ref& texture) - { - return m_Textures.Resolve(texture); - } - ); - for (const auto& resolved : scene.Items) { if (!resolved.Item || !resolved.Proxy) continue; - const auto materialIndex = table->Add(*resolved.Proxy); + const auto materialIndex = materialTable.Add(*resolved.Proxy); EE_CORE_ASSERT(materialIndex, "Material frame capacity was exceeded.") if (!materialIndex) continue; @@ -162,15 +212,6 @@ namespace Elixir::Materials::Rendering }); } - if (!table->GetData().empty()) - { - GetActiveMaterialBuffer()->UpdateData( - table->GetData().data(), - table->GetData().size() * sizeof(SMaterialFrameData) - ); - } - - prepared.MaterialCount = table->GetCount(); return prepared; } @@ -179,19 +220,25 @@ namespace Elixir::Materials::Rendering const SPreparedRenderScene& scene ) { - SRenderResult result{ - .MaterialCount = scene.MaterialCount, - }; + SRenderResult result{}; if (!cmd || !scene.Scene) return result; std::vector batches; + std::vector translucentItems; for (const auto& prepared : scene.Items) { if (!prepared.Item || !prepared.Proxy) continue; const auto& item = *prepared.Item; + + if (prepared.Proxy->GetBlendMode() == EMaterialBlendMode::Translucent) + { + translucentItems.push_back(&prepared); + continue; + } + const auto* geometry = scene.Scene->FindGeometry(item.GeometryIndex); EE_CORE_ASSERT(geometry, "Material render item geometry is unavailable."); if (!geometry) continue; @@ -202,7 +249,10 @@ namespace Elixir::Materials::Rendering const SBatchKey key{ .Pass = item.Pass, + .BlendMode = prepared.Proxy->GetBlendMode(), + .DoubleSided = prepared.Proxy->IsDoubleSided(), .GeometryIndex = item.GeometryIndex, + .Indexed = item.IndexedDraw.has_value(), .Program = *program }; @@ -230,6 +280,9 @@ namespace Elixir::Materials::Rendering if (left.Key.Pass != right.Key.Pass) return GetPassOrder(left.Key.Pass) < GetPassOrder(right.Key.Pass); + if (left.Key.BlendMode != right.Key.BlendMode) + return left.Key.BlendMode < right.Key.BlendMode; + if (left.Key.GeometryIndex != right.Key.GeometryIndex) return left.Key.GeometryIndex < right.Key.GeometryIndex; @@ -240,6 +293,38 @@ namespace Elixir::Materials::Rendering } ); + std::ranges::stable_sort( + translucentItems, + [](const SPreparedRenderItem* left, const SPreparedRenderItem* right) + { + return left->Item->SortDepth > right->Item->SortDepth; + } + ); + + for (const auto& prepared : translucentItems) + { + const auto& item = *prepared->Item; + const auto* geometry = scene.Scene->FindGeometry(item.GeometryIndex); + EE_CORE_ASSERT(geometry, "Material render item geometry is unavailable.") + if (!geometry) continue; + + const auto program = GetProgramKey(item.Pass, *prepared->Proxy); + EE_CORE_ASSERT(program, "Material render item does not support its requested pass.") + if (!program) continue; + + batches.push_back({ + .Key = { + .Pass = item.Pass, + .BlendMode = prepared->Proxy->GetBlendMode(), + .DoubleSided = prepared->Proxy->IsDoubleSided(), + .GeometryIndex = item.GeometryIndex, + .Indexed = item.IndexedDraw.has_value(), + .Program = *program, + }, + .Items = { prepared }, + }); + } + for (const auto& batch : batches) { if (batch.Items.empty()) continue; @@ -255,6 +340,8 @@ namespace Elixir::Materials::Rendering .ExternalResources = { .ConstantBuffers = geometry->ConstantBuffers, .StorageBuffers = geometry->StorageBuffers, + .Textures = geometry->Textures, + .Samplers = geometry->Samplers, }, .MaterialBuffer = GetActiveMaterialBuffer(), .InitialPushConstants = std::span{ @@ -277,6 +364,11 @@ namespace Elixir::Materials::Rendering ); } + if (batch.Key.Indexed) + { + cmd->BindIndexBuffer(geometry->IndexBuffer, geometry->IndexType); + } + for (const auto* resolved : batch.Items) { const auto constants = resolved->Item->PushConstants.Resolve( @@ -290,12 +382,25 @@ namespace Elixir::Materials::Rendering resolved->Item->PushConstants.Size ); - cmd->Draw( - resolved->Item->Draw.VertexCount, - resolved->Item->Draw.InstanceCount, - resolved->Item->Draw.FirstVertex, - resolved->Item->Draw.FirstInstance - ); + if (const auto& indexed = resolved->Item->IndexedDraw) + { + cmd->DrawIndexed( + indexed->IndexCount, + resolved->Item->Draw.InstanceCount, + indexed->FirstIndex, + indexed->VertexOffset, + resolved->Item->Draw.FirstInstance + ); + } + else + { + cmd->Draw( + resolved->Item->Draw.VertexCount, + resolved->Item->Draw.InstanceCount, + resolved->Item->Draw.FirstVertex, + resolved->Item->Draw.FirstInstance + ); + } ++result.DrawCount; } @@ -314,7 +419,7 @@ namespace Elixir::Materials::Rendering return std::nullopt; const auto compiled = request.Material->GetCompiledMaterial(); - const auto& shader = compiled->GetShader(GetUsage(request.Pass)); + const auto& shader = compiled->GetShader(GetShaderVariant(request.Pass)); if (!BindDescriptorResources(shader, request)) return std::nullopt; @@ -330,7 +435,13 @@ namespace Elixir::Materials::Rendering return SPreparedPass{ .Shader = shader, - .Pipeline = GetPipeline(request.Pass, shader, request.Pipeline), + .Pipeline = GetPipeline( + request.Pass, + request.Material->GetBlendMode(), + request.Material->IsDoubleSided(), + shader, + request.Pipeline + ), }; } @@ -342,12 +453,16 @@ namespace Elixir::Materials::Rendering Ref Renderer::GetPipeline( const EMaterialPass pass, + const EMaterialBlendMode blendMode, + const bool doubleSided, const Ref& shader, const SPipelineRequest& request ) { const SPipelineKey key{ .Pass = pass, + .BlendMode = blendMode, + .DoubleSided = doubleSided, .Shader = shader.get(), .VertexLayoutKey = request.VertexLayoutKey, }; @@ -355,38 +470,45 @@ namespace Elixir::Materials::Rendering if (const auto found = m_Pipelines.find(key); found != m_Pipelines.end()) return found->second; + const auto depthTarget = m_Context->GetDepthStencilRenderTarget(); + + const auto colorFormat = m_SceneTarget->GetFormat(); + const auto depthFormat = (EDepthStencilImageFormat)depthTarget->GetFormat(); + PipelineBuilder builder; builder.SetShader(shader); builder.SetInputTopology(EPrimitiveTopology::TriangleList); builder.SetPolygonMode(EPolygonMode::Fill); - builder.SetColorAttachmentFormat(EImageFormat::R8G8B8A8_SRGB); - builder.SetDepthAttachmentFormat(EDepthStencilImageFormat::D32_SFLOAT); + builder.SetColorAttachmentFormat(colorFormat); + builder.SetDepthAttachmentFormat(depthFormat); builder.SetBufferLayout(*request.VertexLayout); + builder.DisableBlending(); switch (pass) { - case EMaterialPass::ParticleSprite: - builder.SetCullMode(ECullMode::None, EFrontFace::CounterClockwise); - builder.EnableAlphaBlending(); - builder.DisableDepthTest(); + case EMaterialPass::Surface: + case EMaterialPass::ParticleMesh: + builder.SetCullMode( + doubleSided ? ECullMode::None : ECullMode::Back, + EFrontFace::CounterClockwise + ); break; + case EMaterialPass::ParticleSprite: case EMaterialPass::ParticleRibbon: builder.SetCullMode(ECullMode::None, EFrontFace::CounterClockwise); - builder.EnableAlphaBlendingMax(); builder.DisableDepthTest(); break; - case EMaterialPass::ParticleMesh: - builder.SetCullMode(ECullMode::Back, EFrontFace::CounterClockwise); - builder.EnableAlphaBlendingMax(); - break; } + if (blendMode == EMaterialBlendMode::Translucent) + builder.EnableAlphaBlending(); + auto info = builder.GetCreateInfo(); - if (pass == EMaterialPass::ParticleMesh) + if (pass == EMaterialPass::Surface || pass == EMaterialPass::ParticleMesh) { info.DepthStencil.DepthTestEnable = true; - info.DepthStencil.DepthWriteEnable = true; + info.DepthStencil.DepthWriteEnable = blendMode != EMaterialBlendMode::Translucent; info.DepthStencil.DepthCompareOp = ECompareOp::LessOrEqual; } @@ -412,7 +534,6 @@ namespace Elixir::Materials::Rendering { state.ExternalResources.push_back({ .Name = std::string(binding.Name), - .Resource = binding.Buffer.get(), .Type = EDescriptorBindingType::ConstantBuffer, }); } @@ -426,7 +547,6 @@ namespace Elixir::Materials::Rendering state.ExternalResources.push_back({ .Name = std::string(binding.Name), - .Resource = buffer.get(), .Type = std::is_same_v> ? EDescriptorBindingType::StorageBuffer : EDescriptorBindingType::DynamicStorageBuffer @@ -436,6 +556,22 @@ namespace Elixir::Materials::Rendering ); } + for (const auto& binding : request.ExternalResources.Textures) + { + state.ExternalResources.push_back({ + .Name = std::string(binding.Name), + .Type = EDescriptorBindingType::Texture + }); + } + + for (const auto& binding : request.ExternalResources.Samplers) + { + state.ExternalResources.push_back({ + .Name = std::string(binding.Name), + .Type = EDescriptorBindingType::Sampler + }); + } + const auto found = m_DescriptorBindings.find(shader.get()); if (found != m_DescriptorBindings.end()) { @@ -444,37 +580,43 @@ namespace Elixir::Materials::Rendering EE_CORE_ERROR("Material shader descriptor bindings changed after initialization.") return false; } - - if (shader->HasBinding("materials")) - shader->BindStorageBuffer("materials", request.MaterialBuffer); - - return true; } - - for (const auto& binding : request.ExternalResources.ConstantBuffers) + else { - shader->BindConstantBuffer(std::string(binding.Name), binding.Buffer); + m_DescriptorBindings.emplace(shader.get(), std::move(state)); } + for (const auto& binding : request.ExternalResources.ConstantBuffers) + shader->BindConstantBuffer(binding.Name, binding.Buffer); + for (const auto& binding : request.ExternalResources.StorageBuffers) { std::visit( [&shader, &binding](const auto& buffer) { - shader->BindStorageBuffer(std::string(binding.Name), buffer); + shader->BindStorageBuffer(binding.Name, buffer); }, binding.Buffer ); } + for (const auto& binding : request.ExternalResources.Textures) + shader->BindTexture(binding.Name, binding.Texture); + + for (const auto& binding : request.ExternalResources.Samplers) + shader->BindSampler(binding.Name, binding.Sampler); + if (shader->HasBinding("materials")) shader->BindStorageBuffer("materials", request.MaterialBuffer); if (shader->HasBinding("sprites")) shader->BindTextureSet("sprites", m_Textures.GetTextureSet()); if (shader->HasBinding("spriteSampler")) shader->BindSampler("spriteSampler", m_Textures.GetSampler()); + if (shader->HasBinding("textures")) + shader->BindTextureSet("textures", m_Textures.GetTextureSet()); + if (shader->HasBinding("texSampler")) + shader->BindSampler("texSampler", m_Textures.GetSampler()); - m_DescriptorBindings.emplace(shader.get(), std::move(state)); return true; } } diff --git a/Elixir/Source/Engine/Materials/Rendering/Renderer.h b/Elixir/Source/Engine/Materials/Rendering/Renderer.h index 490c9d94..77395158 100644 --- a/Elixir/Source/Engine/Materials/Rendering/Renderer.h +++ b/Elixir/Source/Engine/Materials/Rendering/Renderer.h @@ -7,7 +7,12 @@ #include #include -namespace Elixir { class ShaderLoader; } +namespace Elixir +{ + class Image; + class ShaderLoader; +} + namespace Elixir::Materials { struct SMaterialSystemConfig; } namespace Elixir::Materials::Rendering @@ -20,6 +25,7 @@ namespace Elixir::Materials::Rendering */ enum class EMaterialPass : uint8_t { + Surface, ParticleSprite, ParticleRibbon, ParticleMesh, @@ -69,7 +75,7 @@ namespace Elixir::Materials::Rendering */ struct SRenderResult { - /** Number of materials uploaded for the rendered scene. */ + /** Number of unique materials uploaded for the rendered frame. */ uint32_t MaterialCount = 0; /** Number of material batches rendered. */ @@ -91,33 +97,37 @@ namespace Elixir::Materials::Rendering const BufferLayout* VertexLayout = nullptr; }; - /** - * @brief Associates a constant buffer with a shader binding name. - */ + /** @brief Associates a constant buffer with a shader binding name. */ struct SConstantBufferBinding { - /** Shader binding name. */ - std::string_view Name; - - /** Constant buffer to bind. */ + std::string Name; Ref Buffer; }; /** @brief Holds a storage-buffer type supported by material passes. */ using MaterialStorageBuffer = std::variant, Ref>; - /** - * @brief Associates a storage buffer with a shader binding name. - */ + /** @brief Associates a storage buffer with a shader binding name. */ struct SStorageBufferBinding { - /** Shader binding name. */ - std::string_view Name; - - /** Storage buffer to bind. */ + std::string Name; MaterialStorageBuffer Buffer; }; + /** @brief Associates a texture with a shader binding name. */ + struct STextureBinding + { + std::string Name; + Ref Texture; + }; + + /** @brief Associates a sampler with a shader binding name. */ + struct SSamplerBinding + { + std::string Name; + Ref Sampler; + }; + /** * @brief Groups external buffers required by a material pass. */ @@ -125,6 +135,8 @@ namespace Elixir::Materials::Rendering { std::span ConstantBuffers; std::span StorageBuffers; + std::span Textures; + std::span Samplers; /** * @brief Returns the number of external resource bindings. @@ -132,7 +144,12 @@ namespace Elixir::Materials::Rendering */ uint32_t GetResourceCount() const { - return (uint32_t)(ConstantBuffers.size() + StorageBuffers.size()); + return (uint32_t)( + ConstantBuffers.size() + + StorageBuffers.size() + + Textures.size() + + Samplers.size() + ); } }; @@ -142,7 +159,7 @@ namespace Elixir::Materials::Rendering struct SPassRequest { /** Material pass to prepare. */ - EMaterialPass Pass = EMaterialPass::ParticleSprite; + EMaterialPass Pass = EMaterialPass::Surface; /** Resolved material data for the pass. */ const MaterialRenderProxy* Material = nullptr; @@ -187,11 +204,13 @@ namespace Elixir::Materials::Rendering /** * @brief Creates a material renderer. * @param context Graphics context used to create pipelines. - * @param materialCapacity Maximum unique materials supported by one scene. + * @param sceneTarget Color target that receives material rendering. + * @param materialCapacity Maximum unique materials supported by one frame. * @pre All arguments are valid for the renderer lifetime. */ Renderer( const GraphicsContext* context, + const Ref& sceneTarget, uint32_t materialCapacity ); @@ -234,19 +253,23 @@ namespace Elixir::Materials::Rendering static uint32_t GetPassOrder(EMaterialPass pass); private: + // Selects the shader variant that implements a material render pass. + static Compilation::EMaterialShaderVariant GetShaderVariant(EMaterialPass pass); + /** Identifies the type of a cached descriptor binding. */ enum class EDescriptorBindingType : uint8_t { ConstantBuffer, StorageBuffer, DynamicStorageBuffer, + Texture, + Sampler, }; /** Describes one descriptor binding used by a shader. */ struct SDescriptorBinding { std::string Name; - const void* Resource = nullptr; EDescriptorBindingType Type = EDescriptorBindingType::ConstantBuffer; bool operator==(const SDescriptorBinding&) const = default; @@ -265,6 +288,8 @@ namespace Elixir::Materials::Rendering struct SPipelineKey { EMaterialPass Pass = EMaterialPass::ParticleSprite; + EMaterialBlendMode BlendMode = EMaterialBlendMode::Opaque; + bool DoubleSided = false; const Shader* Shader = nullptr; uint64_t VertexLayoutKey = 0; @@ -278,6 +303,8 @@ namespace Elixir::Materials::Rendering { size_t hash = Hash::Hash(static_cast(key.Pass)); Hash::HashCombine(hash, Hash::Hash(key.Shader)); + Hash::HashCombine(hash, Hash::Hash((uint32_t)key.BlendMode)); + Hash::HashCombine(hash, Hash::Hash(key.DoubleSided)); Hash::HashCombine(hash, Hash::Hash(key.VertexLayoutKey)); return hash; } @@ -291,12 +318,11 @@ namespace Elixir::Materials::Rendering uint32_t MaterialIndex = UINT32_MAX; }; - /** Stores frame-buffer indices assigned to one render scene. */ + /** Stores frame-table indices assigned to one render scene. */ struct SPreparedRenderScene { const MaterialRenderScene* Scene = nullptr; std::vector Items; - uint32_t MaterialCount = 0; }; /** Stores resources that are safe to reuse for one graphics frame slot. */ @@ -308,7 +334,10 @@ namespace Elixir::Materials::Rendering struct SBatchKey { EMaterialPass Pass = EMaterialPass::ParticleSprite; + EMaterialBlendMode BlendMode = EMaterialBlendMode::Opaque; + bool DoubleSided = false; uint32_t GeometryIndex = UINT32_MAX; + bool Indexed = false; SProgramKey Program; bool operator==(const SBatchKey&) const = default; @@ -320,8 +349,11 @@ namespace Elixir::Materials::Rendering std::vector Items; }; - /** Builds and uploads the material table for one resolved scene. */ - SPreparedRenderScene PrepareScene(const SPreparedScene& scene); + /** Builds render items for one scene using the frame material table. */ + static SPreparedRenderScene PrepareScene( + const SPreparedScene& scene, + FrameTable& materialTable + ); /** Records draw commands for a scene with prepared material indices. */ SRenderResult RecordScene( @@ -338,6 +370,8 @@ namespace Elixir::Materials::Rendering /** Returns a cached pipeline or creates one for the request. */ Ref GetPipeline( EMaterialPass pass, + EMaterialBlendMode blendMode, + bool doubleSided, const Ref& shader, const SPipelineRequest& request ); @@ -351,6 +385,7 @@ namespace Elixir::Materials::Rendering std::unordered_map, SPipelineKeyHasher> m_Pipelines; std::unordered_map m_DescriptorBindings; + Ref m_SceneTarget; uint64_t m_CurrentFrameNumber = UINT64_MAX; const GraphicsContext* m_Context = nullptr; }; diff --git a/Elixir/Source/Engine/Materials/Rendering/TextureRegistry.cpp b/Elixir/Source/Engine/Materials/Rendering/TextureRegistry.cpp index 5b834105..2e52329c 100644 --- a/Elixir/Source/Engine/Materials/Rendering/TextureRegistry.cpp +++ b/Elixir/Source/Engine/Materials/Rendering/TextureRegistry.cpp @@ -9,6 +9,7 @@ namespace Elixir::Materials::Rendering TextureRegistry::TextureRegistry(const GraphicsContext* context) : m_Textures(TextureSet::Create(context)), m_Sampler(SamplerBuilder().Build(context)), + m_BindingState(CreateRef()), m_GraphicsContext(context) { const auto whiteTexture = Texture2D::Create( @@ -25,6 +26,38 @@ namespace Elixir::Materials::Rendering void TextureRegistry::BeginFrame(const uint64_t submissionSerial) { m_SubmissionSerial = submissionSerial; + m_ReferencedTextures.clear(); + } + + void TextureRegistry::EndFrame() + { + auto& bindings = m_BindingState->Bindings; + for (auto binding = bindings.begin(); binding != bindings.end();) + { + if (m_ReferencedTextures.contains(binding->first)) + { + ++binding; + continue; + } + + const Ref texture = binding->first; + const SResourceHandle handle = binding->second.Handle; + binding = bindings.erase(binding); + + const Ref state = m_BindingState; + const Ref textures = m_Textures; + const uint64_t retirement = state->NextRetirement++; + state->Retirements[texture] = retirement; + m_GraphicsContext->DeferResourceRelease([state, textures, texture, handle, retirement]() + { + const auto retired = state->Retirements.find(texture); + if (retired == state->Retirements.end() || retired->second != retirement) + return; + + state->Retirements.erase(retired); + textures->RemoveTexture(handle); + }); + } } uint32_t TextureRegistry::Resolve(const Ref& texture) @@ -32,13 +65,16 @@ namespace Elixir::Materials::Rendering if (!texture) return GetFallbackIndex(); - const auto found = m_Bindings.find(texture); - if (found != m_Bindings.end()) + m_ReferencedTextures.insert(texture); + m_BindingState->Retirements.erase(texture); + + const auto found = m_BindingState->Bindings.find(texture); + if (found != m_BindingState->Bindings.end()) return Find(texture); // Bindless descriptor updates become visible before the next render callback. const auto handle = m_Textures->AddTexture(texture); - m_Bindings.emplace(texture, STextureBinding{ + m_BindingState->Bindings.emplace(texture, STextureBinding{ .Handle = handle, .ReadySubmission = m_SubmissionSerial + 1, }); @@ -51,8 +87,8 @@ namespace Elixir::Materials::Rendering if (!texture) return GetFallbackIndex(); - const auto found = m_Bindings.find(texture); - if (found == m_Bindings.end()) + const auto found = m_BindingState->Bindings.find(texture); + if (found == m_BindingState->Bindings.end()) return GetFallbackIndex(); return found->second.GetIndexForSubmission( diff --git a/Elixir/Source/Engine/Materials/Rendering/TextureRegistry.h b/Elixir/Source/Engine/Materials/Rendering/TextureRegistry.h index 5f10e82c..79fbd0f6 100644 --- a/Elixir/Source/Engine/Materials/Rendering/TextureRegistry.h +++ b/Elixir/Source/Engine/Materials/Rendering/TextureRegistry.h @@ -5,34 +5,6 @@ namespace Elixir::Materials::Rendering { - /** - * @brief Stores a texture binding and submission where it becomes available. - */ - struct STextureBinding - { - /** @brief Handle of the texture in the texture set. */ - SResourceHandle Handle{}; - - /** @brief First submission that can use Handle. */ - uint64_t ReadySubmission = 0; - - /** - * @brief Gets the texture index that is safe for a submission. - * @param submissionSerial Serial of the submission being prepared. - * @param fallbackIndex Index to use before the binding is available. - * @return The texture index, or fallbackIndex when the binding is not ready. - */ - uint32_t GetIndexForSubmission( - const uint64_t submissionSerial, - const uint32_t fallbackIndex - ) const - { - return ReadySubmission <= submissionSerial - ? Handle.Index - : fallbackIndex; - } - }; - /** * @brief Manages bindless texture bindings used by material rendering. * @@ -55,6 +27,13 @@ namespace Elixir::Materials::Rendering */ void BeginFrame(uint64_t submissionSerial); + /** + * @brief Releases bindings that were not used by the current frame. + * + * Releases wait for submitted frames before changing bindless descriptors. + */ + void EndFrame(); + /** * @brief Resolves a texture to an index in the material texture set. * @@ -82,13 +61,48 @@ namespace Elixir::Materials::Rendering const Ref& GetSampler() const { return m_Sampler; } private: + /** @brief Stores a texture binding and submission where it becomes available. */ + struct STextureBinding + { + /** @brief Handle of the texture in the texture set. */ + SResourceHandle Handle{}; + + /** @brief First submission that can use Handle. */ + uint64_t ReadySubmission = 0; + + /** + * @brief Gets the texture index that is safe for a submission. + * @param submissionSerial Serial of the submission being prepared. + * @param fallbackIndex Index to use before the binding is available. + * @return The texture index, or fallbackIndex when the binding is not ready. + */ + uint32_t GetIndexForSubmission( + const uint64_t submissionSerial, + const uint32_t fallbackIndex + ) const + { + return ReadySubmission <= submissionSerial + ? Handle.Index + : fallbackIndex; + } + }; + + /** @brief Keeps bindings alive while deferred releases are pending. */ + struct SBindingState + { + std::unordered_map, STextureBinding> Bindings; + std::unordered_map, uint64_t> Retirements; + uint64_t NextRetirement = 1; + }; + Ref m_Textures; Ref m_Sampler; SResourceHandle m_FallbackTextureHandle; - std::unordered_map, STextureBinding> m_Bindings; + Ref m_BindingState; + std::unordered_set> m_ReferencedTextures; uint64_t m_SubmissionSerial = 0; const GraphicsContext* m_GraphicsContext; }; -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/Mesh/GeometryAllocation.cpp b/Elixir/Source/Engine/Mesh/GeometryAllocation.cpp new file mode 100644 index 00000000..e40a89c4 --- /dev/null +++ b/Elixir/Source/Engine/Mesh/GeometryAllocation.cpp @@ -0,0 +1,59 @@ +#include "epch.h" +#include "GeometryAllocation.h" + +#include + +namespace Elixir +{ + GeometryAllocation::~GeometryAllocation() + { + Reset(); + } + + GeometryAllocation::GeometryAllocation(GeometryAllocation&& other) noexcept + : m_Pool(other.m_Pool), + m_Handle(other.m_Handle) + { + other.m_Pool = nullptr; + other.m_Handle = {}; + } + + GeometryAllocation& GeometryAllocation::operator=(GeometryAllocation&& other) noexcept + { + if (this == &other) + return *this; + + Reset(); + + m_Pool = other.m_Pool; + m_Handle = other.m_Handle; + + other.m_Pool = nullptr; + other.m_Handle = {}; + + return *this; + } + + void GeometryAllocation::Reset() + { + if (m_Pool && m_Handle.IsValid()) + m_Pool->Free(m_Handle); + + m_Pool = nullptr; + m_Handle = {}; + } + + bool GeometryAllocation::IsValid() const + { + return m_Pool && m_Pool->IsValid(m_Handle); + } + + const SGeometry* GeometryAllocation::Get() const + { + return m_Pool ? m_Pool->Get(m_Handle) : nullptr; + } + + GeometryAllocation::GeometryAllocation(GeometryPool* pool, const SHandle handle) + : m_Pool(pool), + m_Handle(handle) {} +} diff --git a/Elixir/Source/Engine/Mesh/GeometryAllocation.h b/Elixir/Source/Engine/Mesh/GeometryAllocation.h new file mode 100644 index 00000000..3f47ef1d --- /dev/null +++ b/Elixir/Source/Engine/Mesh/GeometryAllocation.h @@ -0,0 +1,57 @@ +#pragma once + +#include + +namespace Elixir +{ + class GeometryPool; + struct SGeometry; + + /** + * @brief Owns one geometry range allocated from a GeometryPool. + * + * Destroying or resetting this object returns its range to the pool. The pool + * must outlive every allocation created from it. + */ + class ELIXIR_API GeometryAllocation final + { + friend class GeometryPool; + + public: + /** @brief Create an empty allocation. */ + GeometryAllocation() = default; + + /** @brief Return the owned geometry range to its pool. */ + ~GeometryAllocation(); + + GeometryAllocation(const GeometryAllocation&) = delete; + GeometryAllocation& operator=(const GeometryAllocation&) = delete; + + /** @brief Transfer ownership from another allocation. */ + GeometryAllocation(GeometryAllocation&&) noexcept; + + /** @brief Release the current range and take ownership from another allocation. */ + GeometryAllocation& operator=(GeometryAllocation&&) noexcept; + + /** @brief Return the geometry range to the pool and make this allocation empty. */ + void Reset(); + + /** + * @brief Check whether this allocation still identifies live pool geometry. + * @return True when the pool recognizes the allocation handle. + */ + bool IsValid() const; + + /** @brief Get metadata for the owned geometry range, when it remains valid. */ + const SGeometry* Get() const; + + /** @brief Get the handle owned by this allocation. */ + SHandle GetHandle() const { return m_Handle; } + + private: + GeometryAllocation(GeometryPool* pool, SHandle handle); + + GeometryPool* m_Pool = nullptr; + SHandle m_Handle; + }; +} diff --git a/Elixir/Source/Engine/Mesh/GeometryPool.cpp b/Elixir/Source/Engine/Mesh/GeometryPool.cpp new file mode 100644 index 00000000..08df804f --- /dev/null +++ b/Elixir/Source/Engine/Mesh/GeometryPool.cpp @@ -0,0 +1,273 @@ +#include "epch.h" +#include "GeometryPool.h" + +#include + +namespace Elixir +{ + GeometryPool::GeometryPool( + const GraphicsContext& context, + const SGeometryPoolConfig config + ) : m_State(CreateRef(SGeometryPoolState{ + .VertexCapacity = config.VertexCapacity, + .IndexCapacity = config.IndexCapacity, + })), + m_GraphicsContext(context) + { + EE_CORE_ASSERT(m_State->VertexCapacity > 0, "GeometryPool vertex capacity must be greater than 0.") + EE_CORE_ASSERT(m_State->IndexCapacity > 0, "GeometryPool index capacity must be greater than 0.") + + m_VertexBuffer = DynamicVertexBuffer::Create( + &context, + m_State->VertexCapacity * sizeof(SStaticMeshVertex) + ); + + m_IndexBuffer = DynamicIndexBuffer::Create( + &context, + m_State->IndexCapacity * sizeof(uint32_t), + nullptr, + EIndexType::UInt32 + ); + + EE_CORE_ASSERT(m_VertexBuffer, "GeometryPool failed to create the vertex buffer.") + EE_CORE_ASSERT(m_IndexBuffer, "GeometryPool failed to create the index buffer.") + + m_VertexBuffer->SetLayout(StaticMesh::GetVertexLayout()); + } + + GeometryAllocation GeometryPool::Upload(const SStaticMeshData& data) + { + if (data.Vertices.empty() || data.Indices.empty()) + { + EE_CORE_ERROR("GeometryPool cannot upload empty geometry.") + return {}; + } + + if (data.Vertices.size() > std::numeric_limits::max() || + data.Indices.size() > std::numeric_limits::max()) + { + EE_CORE_ERROR("GeometryPool geometry exceeds the supported element count.") + return {}; + } + + const uint32_t vertexCount = (uint32_t)data.Vertices.size(); + const uint32_t indexCount = (uint32_t)data.Indices.size(); + + const auto vertexOffset = AllocateRange( + m_State->FreeVertexRanges, + m_State->NextVertexOffset, + m_State->VertexCapacity, + vertexCount + ); + if (!vertexOffset) + { + EE_CORE_ERROR("GeometryPool has no free vertex range for '{}'.", data.Name) + return {}; + } + + const auto indexOffset = AllocateRange( + m_State->FreeIndexRanges, + m_State->NextIndexOffset, + m_State->IndexCapacity, + indexCount + ); + if (!indexOffset) + { + FreeRange( + m_State->FreeVertexRanges, + { + .Offset = *vertexOffset, + .Count = vertexCount, + } + ); + + EE_CORE_ERROR("GeometryPool has no free index range for '{}'.", data.Name) + return {}; + } + + m_VertexBuffer->UpdateData( + data.Vertices.data(), + data.Vertices.size() * sizeof(SStaticMeshVertex), + (size_t)*vertexOffset * sizeof(SStaticMeshVertex) + ); + m_IndexBuffer->UpdateData( + data.Indices.data(), + data.Indices.size() * sizeof(uint32_t), + (size_t)*indexOffset * sizeof(uint32_t) + ); + + uint32_t slotIndex; + if (!m_State->FreeSlots.empty()) + { + slotIndex = m_State->FreeSlots.back(); + m_State->FreeSlots.pop_back(); + } + else + { + slotIndex = (uint32_t)m_State->Slots.size(); + m_State->Slots.push_back({}); + } + + SGeometryPoolSlot& slot = m_State->Slots[slotIndex]; + slot.Geometry = { + .VertexOffset = *vertexOffset, + .IndexOffset = *indexOffset, + .VertexCount = vertexCount, + .IndexCount = indexCount, + }; + slot.Allocated = true; + + return GeometryAllocation( + this, + { + .Index = slotIndex, + .Generation = slot.Generation, + } + ); + } + + const SGeometry* GeometryPool::Get(const SHandle handle) const + { + if (!handle.IsValid() || handle.Index >= m_State->Slots.size()) + return nullptr; + + const SGeometryPoolSlot& slot = m_State->Slots[handle.Index]; + if (!slot.Allocated || slot.Generation != handle.Generation) + return nullptr; + + return &slot.Geometry; + } + + void GeometryPool::Free(const SHandle handle) + { + if (!handle.IsValid() || handle.Index >= m_State->Slots.size()) + return; + + const SGeometryPoolSlot& slot = m_State->Slots[handle.Index]; + if (!slot.Allocated || slot.Generation != handle.Generation) + return; + + const Ref state = m_State; + m_GraphicsContext.DeferResourceRelease([state, handle]() + { + FreeCompleted(*state, handle); + }); + } + + void GeometryPool::FreeCompleted( + SGeometryPoolState& state, + const SHandle handle + ) + { + if (!handle.IsValid() || handle.Index >= state.Slots.size()) + return; + + SGeometryPoolSlot& slot = state.Slots[handle.Index]; + if (!slot.Allocated || slot.Generation != handle.Generation) + return; + + FreeRange( + state.FreeVertexRanges, + { + .Offset = slot.Geometry.VertexOffset, + .Count = slot.Geometry.VertexCount, + } + ); + + FreeRange( + state.FreeIndexRanges, + { + .Offset = slot.Geometry.IndexOffset, + .Count = slot.Geometry.IndexCount, + } + ); + + slot.Allocated = false; + ++slot.Generation; + slot.Geometry = {}; + state.FreeSlots.push_back(handle.Index); + } + + bool GeometryPool::IsValid(const SHandle& handle) const + { + return IsHandleValid(*m_State, handle); + } + + bool GeometryPool::IsHandleValid( + const SGeometryPoolState& state, + const SHandle& handle + ) + { + return handle.IsValid() && + handle.Index < state.Slots.size() && + state.Slots[handle.Index].Allocated && + state.Slots[handle.Index].Generation == handle.Generation; + } + + std::optional GeometryPool::AllocateRange( + std::vector& freeRanges, + uint32_t& nextOffset, + const uint32_t capacity, + const uint32_t count + ) + { + for (auto it = freeRanges.begin(); it != freeRanges.end(); ++it) + { + if (it->Count < count) + continue; + + const uint32_t offset = it->Offset; + it->Offset += count; + it->Count -= count; + + if (it->Count == 0) + freeRanges.erase(it); + + return offset; + } + + if (count > capacity - nextOffset) + return std::nullopt; + + const uint32_t offset = nextOffset; + nextOffset += count; + return offset; + } + + void GeometryPool::FreeRange( + std::vector& freeRanges, + const SGeometryPoolRange range + ) + { + auto it = std::lower_bound( + freeRanges.begin(), + freeRanges.end(), + range.Offset, + [](const SGeometryPoolRange& entry, const uint32_t offset) + { + return entry.Offset < offset; + } + ); + + it = freeRanges.insert(it, range); + + if (it != freeRanges.begin()) + { + const auto previous = std::prev(it); + if (previous->Offset + previous->Count == it->Offset) + { + previous->Count += it->Count; + it = freeRanges.erase(it); + it = previous; + } + } + + const auto next = std::next(it); + if (next != freeRanges.end() && + it->Offset + it->Count == next->Offset) + { + it->Count += next->Count; + freeRanges.erase(next); + } + } +} diff --git a/Elixir/Source/Engine/Mesh/GeometryPool.h b/Elixir/Source/Engine/Mesh/GeometryPool.h new file mode 100644 index 00000000..ce6fc7c0 --- /dev/null +++ b/Elixir/Source/Engine/Mesh/GeometryPool.h @@ -0,0 +1,131 @@ +#pragma once + +#include +#include +#include +#include + +namespace Elixir +{ + /** @brief Identifies a contiguous geometry range in GeometryPool buffers. */ + struct SGeometry + { + uint32_t VertexOffset = 0; + uint32_t IndexOffset = 0; + uint32_t VertexCount = 0; + uint32_t IndexCount = 0; + }; + + /** @brief Defines the fixed capacity of one geometry pool. */ + struct SGeometryPoolConfig + { + uint32_t VertexCapacity = 4'000'000; + uint32_t IndexCapacity = 12'000'000; + }; + + /** + * @brief Owns shared GPU buffers used by static meshes. + * + * Geometry remains valid until all submitted draws using it complete. + */ + class ELIXIR_API GeometryPool final + { + friend class GeometryAllocation; + + public: + /** + * @brief Create shared GPU buffers for static mesh geometry. + * @param context Graphics context that owns the buffers. + * @param config Capacity of the shared buffers. + */ + explicit GeometryPool( + const GraphicsContext& context, + SGeometryPoolConfig config = {} + ); + + /** + * @brief Upload CPU mesh data into the shared GPU buffers. + * @return An allocation that owns the uploaded geometry, or an empty allocation + * when the data is invalid or the pool lacks capacity. + */ + GeometryAllocation Upload(const SStaticMeshData& data); + + /** @brief Get geometry metadata for a valid handle. */ + const SGeometry* Get(SHandle handle) const; + + /** @brief Get the shared vertex buffer. */ + const Ref& GetVertexBuffer() const { return m_VertexBuffer; } + + /** @brief Get the shared index buffer. */ + const Ref& GetIndexBuffer() const { return m_IndexBuffer; } + + private: + struct SGeometryPoolRange + { + uint32_t Offset = 0; + uint32_t Count = 0; + }; + + struct SGeometryPoolSlot + { + SGeometry Geometry; + uint32_t Generation = 0; + bool Allocated = false; + }; + + /** @brief Holds allocation metadata that can outlive the pool. */ + struct SGeometryPoolState + { + uint32_t VertexCapacity = 0; + uint32_t IndexCapacity = 0; + uint32_t NextVertexOffset = 0; + uint32_t NextIndexOffset = 0; + + std::vector FreeVertexRanges; + std::vector FreeIndexRanges; + std::vector Slots; + std::vector FreeSlots; + }; + + /** @brief Defers a geometry release until submitted draws no longer use it. */ + void Free(SHandle handle); + + /** @brief Releases a geometry range when its handle is valid. */ + static void FreeCompleted(SGeometryPoolState& state, SHandle handle); + + /** @brief Check whether this allocation still identifies live pool geometry. */ + bool IsValid(const SHandle& handle) const; + + /** @brief Check whether a handle identifies live geometry in a pool state. */ + static bool IsHandleValid( + const SGeometryPoolState& state, + const SHandle& handle + ); + + /** + * @brief Allocate one contiguous range in a pool buffer. + * @return First element offset, or std::nullopt when capacity is exhausted. + */ + static std::optional AllocateRange( + std::vector& freeRanges, + uint32_t& nextOffset, + uint32_t capacity, + uint32_t count + ); + + /** @brief Release a range and merge it with adjacent free ranges. */ + static void FreeRange( + std::vector& freeRanges, + SGeometryPoolRange range + ); + + Ref m_State; + + Ref m_VertexBuffer; + Ref m_IndexBuffer; + std::vector m_Geometries; + std::vector m_Generations; + + const GraphicsContext& m_GraphicsContext; + }; +} diff --git a/Elixir/Source/Engine/Mesh/StaticMesh.cpp b/Elixir/Source/Engine/Mesh/StaticMesh.cpp new file mode 100644 index 00000000..f2c7d9cb --- /dev/null +++ b/Elixir/Source/Engine/Mesh/StaticMesh.cpp @@ -0,0 +1,84 @@ +#include "epch.h" +#include "StaticMesh.h" + +#include + +namespace Elixir +{ + Ref StaticMesh::Create( + std::string name, + GeometryAllocation geometry, + std::vector sections, + std::vector> materials, + SStaticMeshBounds localBounds + ) + { + if (!geometry.IsValid()) + { + EE_CORE_ERROR("StaticMesh requires a valid geometry handle.") + return nullptr; + } + + if (sections.empty()) + { + EE_CORE_ERROR("StaticMesh requires at least one section.") + return nullptr; + } + + if (materials.empty()) + { + EE_CORE_ERROR("StaticMesh requires at least one material.") + return nullptr; + } + + for (const auto& material : materials) + { + if (!material) + { + EE_CORE_ERROR("StaticMesh cannot contain a null material.") + return nullptr; + } + } + + for (const auto& section : sections) + { + if (section.MaterialIndex >= materials.size()) + { + EE_CORE_ERROR("StaticMesh section references an invalid material index.") + return nullptr; + } + } + + return Ref(new StaticMesh( + std::move(name), + std::move(geometry), + std::move(sections), + std::move(materials), + localBounds + )); + } + + const BufferLayout& StaticMesh::GetVertexLayout() + { + static const BufferLayout layout = {{ + { EDataType::Vec3, "Position" }, + { EDataType::Vec3, "Normal" }, + { EDataType::Vec4, "Tangent" }, + { EDataType::Vec2, "TexCoord" }, + { EDataType::Vec2, "TexCoord1" }, + }}; + return layout; + } + + StaticMesh::StaticMesh( + std::string name, + GeometryAllocation geometry, + std::vector sections, + std::vector> materials, + const SStaticMeshBounds& localBounds + ) : m_Name(std::move(name)), + m_Geometry(std::move(geometry)), + m_Sections(std::move(sections)), + m_Materials(std::move(materials)), + m_LocalBounds(localBounds) {} +} diff --git a/Elixir/Source/Engine/Mesh/StaticMesh.h b/Elixir/Source/Engine/Mesh/StaticMesh.h new file mode 100644 index 00000000..fb1f5885 --- /dev/null +++ b/Elixir/Source/Engine/Mesh/StaticMesh.h @@ -0,0 +1,118 @@ +#pragma once + +#include + +namespace Elixir +{ + namespace Materials { class Material; } + using namespace Materials; + + struct SGeometry; + + /** @brief Stores the vertex attributes supported by static meshes. */ + struct SStaticMeshVertex + { + /** Vertex position in mesh-local units. */ + glm::vec3 Position{}; + + /** Surface normal in mesh-local space. */ + glm::vec3 Normal{ 0.0f, 1.0f, 0.0f }; + + /** Tangent xyz and bitangent handedness in w. */ + glm::vec4 Tangent{ 1.0f, 0.0f, 0.0f, 1.0f }; + + /** First texture coordinate channel. */ + glm::vec2 TexCoord{}; + + /** Second texture coordinate channel. */ + glm::vec2 TexCoord1{}; + }; + + /** @brief Defines an axis-aligned bound in mesh-local space. */ + struct SStaticMeshBounds + { + glm::vec3 Min{}; + glm::vec3 Max{}; + }; + + /** @brief Describes one drawable range in a static mesh. */ + struct SStaticMeshSection + { + uint32_t FirstIndex = 0; + uint32_t IndexCount = 0; + uint32_t VertexOffset = 0; + uint32_t MaterialIndex = 0; + SStaticMeshBounds LocalBounds; + }; + + /** @brief Stores CPU geometry loaded from one mesh asset. */ + struct SStaticMeshData + { + std::string Name; + std::vector Vertices; + std::vector Indices; + std::vector Sections; + std::vector> Materials; + SStaticMeshBounds LocalBounds; + }; + + /** + * @brief Represents reusable, non-deforming geometry in the runtime. + * + * A static mesh references geometry stored by GeometryPool. It contains no scene + * transform. Its sections correspond to source draw primitives. + */ + class ELIXIR_API StaticMesh final + { + public: + /** + * @brief Create a mesh that references uploaded pooled geometry. + * @param name Display name for the mesh. + * @param geometry Allocation that owns the uploaded geometry range. + * @param sections Draw ranges that belong to the geometry. + * @param materials Materials referenced by the mesh sections. + * @param localBounds Bound of the mesh in mesh-local space. + * @return A mesh, or nullptr when geometry, sections, or materials are invalid. + */ + static Ref Create( + std::string name, + GeometryAllocation geometry, + std::vector sections, + std::vector> materials, + SStaticMeshBounds localBounds + ); + + /** @brief Get the vertex layout used by every static mesh section. */ + static const BufferLayout& GetVertexLayout(); + + /** @brief Get the display name supplied by the creator. */ + const std::string& GetName() const { return m_Name; } + + /** @brief Get metadata for the mesh geometry, or nullptr when it is no longer valid. */ + const SGeometry* GetGeometry() const { return m_Geometry.Get(); } + + /** @brief Get the immutable sections of this mesh. */ + const std::vector& GetSections() const { return m_Sections; } + + /** @brief Get the materials referenced by mesh sections. */ + const std::vector>& GetMaterials() const { return m_Materials; } + + /** @brief Get the mesh bound in mesh-local space. */ + const SStaticMeshBounds& GetLocalBounds() const { return m_LocalBounds; } + + private: + explicit StaticMesh( + std::string name, + GeometryAllocation geometry, + std::vector sections, + std::vector> materials, + const SStaticMeshBounds& localBounds + ); + + std::string m_Name; + GeometryAllocation m_Geometry; + std::vector m_Sections; + std::vector> m_Materials; + SStaticMeshBounds m_LocalBounds; + }; +} diff --git a/Elixir/Source/Engine/Mesh/StaticMeshLoader.h b/Elixir/Source/Engine/Mesh/StaticMeshLoader.h new file mode 100644 index 00000000..2641b0cb --- /dev/null +++ b/Elixir/Source/Engine/Mesh/StaticMeshLoader.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +namespace Elixir +{ + class GraphicsContext; + + /** + * @brief Loads runtime data for one static mesh from an asset source. + * + * Implementations must not expose parser-specific types through this contract. + */ + class ELIXIR_API StaticMeshLoader + { + public: + virtual ~StaticMeshLoader() = default; + + /** + * @brief Load static mesh data from one source file. + * + * The graphics context enable loaders to resolve material textures and + * other graphics resources required by the loaded materials. + * + * @param context Graphics context used by the loader. + * @param path Local source file path. + * @return Loaded mesh data, or std::nullopt when loading fails. + */ + virtual std::optional Load( + const GraphicsContext& context, + std::filesystem::path path + ) const = 0; + }; +} diff --git a/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp b/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp new file mode 100644 index 00000000..22ad664b --- /dev/null +++ b/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp @@ -0,0 +1,88 @@ +#include "epch.h" +#include "StaticMeshLoaderRegistry.h" + +#include +#include + +namespace Elixir +{ + namespace + { + const GraphicsContext* s_GraphicsContext; + Scope s_Loader; + Scope s_GeometryPool; + + std::optional> MakeError(const std::string_view message) + { + EE_CORE_ERROR("{}", message) + return std::nullopt; + } + } + + void StaticMeshLoaderRegistry::Initialize(const GraphicsContext& context) + { + if (s_GraphicsContext == &context) return; + + s_GraphicsContext = &context; + s_GeometryPool = CreateScope(context); + s_Loader.reset(); + RegisterLoader(CreateScope()); + } + + void StaticMeshLoaderRegistry::Shutdown() + { + s_Loader.reset(); + s_GeometryPool.reset(); + s_GraphicsContext = nullptr; + } + + bool StaticMeshLoaderRegistry::RegisterLoader(Scope loader) + { + if (!loader || s_Loader) return false; + + s_Loader = std::move(loader); + return true; + } + + bool StaticMeshLoaderRegistry::ReplaceLoader(Scope loader) + { + if (!loader) return false; + + s_Loader = std::move(loader); + return true; + } + + std::optional> StaticMeshLoaderRegistry::Load( + const std::filesystem::path& path + ) + { + if (!s_GraphicsContext) + return MakeError("StaticMeshLoaderRegistry must be initialized before loading meshes."); + + if (!s_Loader) + return MakeError("No static mesh loader is registered."); + + const auto data = s_Loader->Load(*s_GraphicsContext, path); + if (!data) return std::nullopt; + + auto geometry = s_GeometryPool->Upload(*data); + if (!geometry.IsValid()) + { + EE_CORE_ERROR("Could not upload static mesh geometry '{}'.", data->Name) + return std::nullopt; + } + + return StaticMesh::Create( + std::move(data->Name), + std::move(geometry), + std::move(data->Sections), + std::move(data->Materials), + data->LocalBounds + ); + } + + const GeometryPool& StaticMeshLoaderRegistry::GetGeometryPool() + { + return *s_GeometryPool; + } +} diff --git a/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.h b/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.h new file mode 100644 index 00000000..efeb8c2c --- /dev/null +++ b/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include + +namespace Elixir +{ + class GraphicsContext; + + /** + * @brief Creates runtime static meshes through the active loader and GeometryPool. + * + * The application selects the implementation during startup. + */ + class ELIXIR_API StaticMeshLoaderRegistry final + { + public: + StaticMeshLoaderRegistry() = delete; + + /** + * @brief Initialize the registry for one graphics context. + * @param context Graphics context used to upload imported geometry. + */ + static void Initialize(const GraphicsContext& context); + + /** @brief Release the loader, geometry pool, and graphics context. */ + static void Shutdown(); + + /** + * @brief Register the active static mesh loader. + * @param loader Loader to register. + * @return False when loader is null or another loader is already active. + */ + static bool RegisterLoader(Scope loader); + + /** + * @brief Replace the active static mesh loader. + * @param loader Loader that becomes active. + * @return False when loader is null. + */ + static bool ReplaceLoader(Scope loader); + + /** + * @brief Load CPU data and create one runtime static mesh. + * @param path Local source file. + * @return Loaded mesh, or std::nullopt when loading fails. + */ + static std::optional> Load(const std::filesystem::path& path); + + /** @brief Get the pool that owns static mesh GPU geometry. */ + static const GeometryPool& GetGeometryPool(); + }; +} diff --git a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp new file mode 100644 index 00000000..5583a263 --- /dev/null +++ b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp @@ -0,0 +1,179 @@ +#include "epch.h" +#include "StaticMeshRenderer.h" + +#include +#include +#include +#include +#include + +namespace Elixir +{ + namespace + { + struct alignas(16) SSurfaceFrameData + { + glm::mat4 View{ 1.0f }; + glm::mat4 Proj{ 1.0f }; + glm::mat4 ViewProj{ 1.0f }; + glm::vec3 CameraPos{}; + float Time = 0.0f; + float EnvIntensity = 0.0f; + float EnvMaxLod = 0.0f; + uint32_t SceneColorIndex = UINT32_MAX; + uint32_t DebugView = 0; + float ScreenWidth = 1.0f; + float ScreenHeight = 1.0f; + glm::vec2 Padding{}; + glm::vec4 LightDirection{}; + glm::vec4 LightColor{}; + }; + + struct SSurfacePushConstants + { + glm::mat4 Model{ 1.0f }; + uint32_t MaterialIndex = 0; + }; + } + + StaticMeshRenderer::StaticMeshRenderer( + const GraphicsContext* context, + MaterialSystem& materialSystem, + const GeometryPool& geometryPool + ) : m_MaterialSystem(materialSystem), + m_GeometryPool(geometryPool), + m_Context(context), + m_FrameBuffers(*context) + { + EE_CORE_ASSERT(m_Context, "StaticMeshRenderer requires a graphics context") + + constexpr SSurfaceFrameData frameData; + m_FrameBuffers.ForEach([this, &frameData](Ref& frameBuffer) + { + frameBuffer = UniformBuffer::Create(m_Context, sizeof(frameData), &frameData); + }); + } + + void StaticMeshRenderer::BeginFrame( + const Camera& camera, + const SStaticMeshLighting& lighting + ) + { + const auto extent = m_Context->GetRenderTarget()->GetExtent(); + + m_View = camera.GetViewMatrix(); + + const auto lightDirection = glm::normalize(lighting.DirectionalLightDirection); + + const SSurfaceFrameData frameData{ + .View = m_View, + .Proj = camera.GetProjectionMatrix(), + .ViewProj = camera.GetViewProjectionMatrix(), + .CameraPos = camera.GetPosition(), + .EnvIntensity = lighting.Environment.Intensity, + .EnvMaxLod = lighting.Environment.MaxLod, + .DebugView = (uint32_t)lighting.DebugView, + .ScreenWidth = (float)extent.Width, + .ScreenHeight = (float)extent.Height, + .LightDirection = glm::vec4(lightDirection, 0.0f), + .LightColor = glm::vec4( + lighting.DirectionalLightColor, + lighting.DirectionalLightIntensity + ), + }; + const auto& frameBuffer = m_FrameBuffers.GetCurrent(); + frameBuffer->UpdateData(&frameData, sizeof(frameData)); + + m_Scene = {}; + m_GeometryIndex = m_Scene.AddGeometry({ + .Pipeline = { + .VertexLayoutKey = reinterpret_cast(&StaticMesh::GetVertexLayout()), + .VertexLayout = &StaticMesh::GetVertexLayout(), + }, + .ConstantBuffers = {{ + .Name = "cbFrame", + .Buffer = frameBuffer, + }}, + .Textures = { + { + .Name = "environmentTexture", + .Texture = lighting.Environment.Environment, + }, + { + .Name = "irradianceTexture", + .Texture = lighting.Environment.Irradiance, + }, + { + .Name = "prefilteredTexture", + .Texture = lighting.Environment.Prefiltered, + } + }, + .Samplers = {{ + .Name = "environmentSampler", + .Sampler = lighting.Environment.Sampler, + }}, + .VertexBuffers = {{ + .Buffer = m_GeometryPool.GetVertexBuffer().get(), + .Binding = 0, + }}, + .IndexBuffer = m_GeometryPool.GetIndexBuffer().get(), + }); + } + + void StaticMeshRenderer::Render(const Ref& mesh) + { + if (!mesh) return; + + const auto geometry = mesh->GetGeometry(); + if (!geometry) return; + + const auto& materials = mesh->GetMaterials(); + + for (const auto& section : mesh->GetSections()) + { + if (section.MaterialIndex >= materials.size()) continue; + + const auto instance = GetDefaultInstance(materials[section.MaterialIndex]); + if (!instance) continue; + + const glm::vec3 center = (section.LocalBounds.Min + section.LocalBounds.Max) * 0.5f; + const float sortDepth = -(m_View * glm::vec4(center, 1.0f)).z; + + m_Scene.Add({ + .Pass = EMaterialPass::Surface, + .Material = instance, + .SortDepth = sortDepth, + .GeometryIndex = m_GeometryIndex, + .PushConstants = SMaterialPushConstants::Create( + SSurfacePushConstants{}, + offsetof(SSurfacePushConstants, MaterialIndex) + ), + .IndexedDraw = SIndexedDrawCommand{ + .IndexCount = section.IndexCount, + .FirstIndex = geometry->IndexOffset + section.FirstIndex, + .VertexOffset = geometry->VertexOffset + section.VertexOffset, + }, + }); + } + } + + void StaticMeshRenderer::EndFrame() + { + m_MaterialSystem.Submit(std::move(m_Scene)); + } + + Ref StaticMeshRenderer::GetDefaultInstance(const Ref& material) + { + if (!material) return nullptr; + + const auto existing = m_DefaultInstances.find(material.get()); + if (existing != m_DefaultInstances.end()) + { + return existing->second; + } + + const auto instance = material->CreateInstance(); + m_DefaultInstances.emplace(material.get(), instance); + return instance; + } +} diff --git a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h new file mode 100644 index 00000000..497dceb3 --- /dev/null +++ b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h @@ -0,0 +1,153 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace Elixir +{ + namespace Materials { class MaterialSystem; } + using namespace Materials; + + class Camera; + class GraphicsContext; + class ShaderLoader; + class GeometryPool; + + /** + * @brief Stores image-based lighting resources for Surface materials. + * + * All textures use equirectangular projection. The prefiltered texture stores + * roughness-filtered specular reflections. + */ + struct SSurfaceEnvironment + { + /** Raw environment texture used for sharp reflections. */ + Ref Environment; + + /** Diffuse irradiance texture derived from @ref Environment. */ + Ref Irradiance; + + /** Roughness-filtered specular reflection texture. */ + Ref Prefiltered; + + /** Sampler used by all environment textures. */ + Ref Sampler; + + /** Multiplier applied to environment lighting. */ + float Intensity = 1.0f; + + /** Highest valid mip level in @ref Environment. */ + float MaxLod = 0.0f; + }; + + /** @brief Selects the Surface lighting contribution shown by the renderer. */ + enum class ESurfaceDebugView : uint32_t + { + /** Render the fully lit material. */ + Composite, + + /** Render the material base color before lighting. */ + BaseColor, + + /** Render the diffuse contribution from image-based lighting. */ + DiffuseIBL, + + /** Render the specular contribution from image-based lighting. */ + SpecularIBL, + + /** Render the diffuse contribution from the directional light. */ + DirectDiffuse, + + /** Render the specular contribution from the directional light. */ + DirectSpecular, + + /** Render the clear-coat reflection and direct-light contribution. */ + ClearCoat, + }; + + /** @brief Stores frame lighting used by static mesh Surface materials. */ + struct SStaticMeshLighting + { + /** Image-based lighting resources for the frame. */ + SSurfaceEnvironment Environment; + + /** Direction from the shared point toward the directional light. */ + glm::vec3 DirectionalLightDirection{ -0.5f, 0.65f, -0.55f }; + + /** Linear RGB color of the directional light. */ + glm::vec3 DirectionalLightColor{ 1.0f, 0.96f, 0.9f }; + + /** Intensity multiplier of the directional light. */ + float DirectionalLightIntensity = 2.2f; + + /** Surface lighting contribution to display for diagnostics. */ + ESurfaceDebugView DebugView = ESurfaceDebugView::Composite; + }; + + /** + * @brief Collects static mesh surface draws for MaterialSystem. + */ + class ELIXIR_API StaticMeshRenderer final + { + public: + /** + * @brief Create a static mesh renderer for one graphics context. + * @param context Graphics context used to create frame resources. + * @param materialSystem Material system that records submitted surface draws. + * @param geometryPool Pool that stores the geometry referenced by rendered meshes. + * @pre All arguments remain valid for the renderer lifetime. + */ + StaticMeshRenderer( + const GraphicsContext* context, + MaterialSystem& materialSystem, + const GeometryPool& geometryPool + ); + + /** + * @brief Begin recording static mesh draws for one frame. + * + * Updates camera data and begins collecting material draw commands. + * + * @param camera Camera used to build the view-projection matrix. + * @param lighting Lighting resources used by Surface materials. + * @pre EndFrame was called after the previous BeginFrame. + */ + void BeginFrame( + const Camera& camera, + const SStaticMeshLighting& lighting + ); + + /** + * @brief Record draw commands for one static mesh. + * + * Each section produces one indexed surface draw. + * + * @param mesh Static mesh to draw. + * @pre BeginFrame was called and EndFrame was not called yet. + */ + void Render(const Ref& mesh); + + /** + * @brief Submit collected static mesh draws to MaterialSystem. + * @pre BeginFrame was called and EndFrame was not called yet. + */ + void EndFrame(); + + private: + Ref GetDefaultInstance(const Ref& material); + + MaterialSystem& m_MaterialSystem; + const GeometryPool& m_GeometryPool; + + Rendering::MaterialRenderScene m_Scene; + uint32_t m_GeometryIndex = UINT32_MAX; + std::unordered_map> m_DefaultInstances; + glm::mat4 m_View{ 1.0f }; + + const GraphicsContext* m_Context = nullptr; + FrameSlotState> m_FrameBuffers; + }; +} diff --git a/Elixir/Source/Graphics/SpirV/SpirVShaderBackend.cpp b/Elixir/Source/Graphics/SpirV/SpirVShaderBackend.cpp index 58078c49..ed1c103e 100644 --- a/Elixir/Source/Graphics/SpirV/SpirVShaderBackend.cpp +++ b/Elixir/Source/Graphics/SpirV/SpirVShaderBackend.cpp @@ -526,4 +526,4 @@ namespace Elixir::SpirV return cstruct; } -} \ No newline at end of file +} diff --git a/Elixir/Source/Graphics/Vulkan/Initializers.h b/Elixir/Source/Graphics/Vulkan/Initializers.h index 923d6be7..26b13753 100644 --- a/Elixir/Source/Graphics/Vulkan/Initializers.h +++ b/Elixir/Source/Graphics/Vulkan/Initializers.h @@ -67,7 +67,7 @@ namespace Elixir::Vulkan::Initializers static VkImageCreateInfo ImageCreateInfo( const SImageCreateInfo& info, - const uint32_t queueFamily + const uint32_t& queueFamily ) { VkImageCreateInfo imageInfo = {}; @@ -139,7 +139,7 @@ namespace Elixir::Vulkan::Initializers } static VkRenderingAttachmentInfo DepthStencilAttachmentInfo( - const Ref& image, + const Ref& image, const float depthClearValue = 1.0f, const VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED ) @@ -179,4 +179,4 @@ namespace Elixir::Vulkan::Initializers return info; } -} \ No newline at end of file +} diff --git a/Elixir/Source/Graphics/Vulkan/Utils.h b/Elixir/Source/Graphics/Vulkan/Utils.h index 0d5647a2..5f5ebfc7 100644 --- a/Elixir/Source/Graphics/Vulkan/Utils.h +++ b/Elixir/Source/Graphics/Vulkan/Utils.h @@ -181,7 +181,9 @@ namespace Elixir::Vulkan const VkImage image, const VkImageLayout currentLayout, const VkImageLayout newLayout, - const VkImageAspectFlags aspectMask + const VkImageAspectFlags aspectMask, + const uint32_t baseMipLevel = 0, + const uint32_t levelCount = VK_REMAINING_MIP_LEVELS ) { EE_PROFILE_ZONE_SCOPED() @@ -196,6 +198,8 @@ namespace Elixir::Vulkan barrier.oldLayout = currentLayout; barrier.newLayout = newLayout; barrier.subresourceRange = Initializers::ImageSubresourceRange(aspectMask); + barrier.subresourceRange.baseMipLevel = baseMipLevel; + barrier.subresourceRange.levelCount = levelCount; barrier.image = image; VkDependencyInfo depInfo = {}; @@ -214,7 +218,10 @@ namespace Elixir::Vulkan const VkExtent3D srcExtent, const VkExtent3D dstExtent, const VkImageAspectFlags aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, - const VkFilter filter = VK_FILTER_LINEAR + const VkFilter filter = VK_FILTER_LINEAR, + const uint32_t srcMipLevel = 0, + const uint32_t dstMipLevel = 0, + const uint32_t layerCount = 1 ) { EE_PROFILE_ZONE_SCOPED() @@ -233,13 +240,13 @@ namespace Elixir::Vulkan blitRegion.srcSubresource.aspectMask = aspectMask; blitRegion.srcSubresource.baseArrayLayer = 0; - blitRegion.srcSubresource.layerCount = 1; - blitRegion.srcSubresource.mipLevel = 0; + blitRegion.srcSubresource.layerCount = layerCount; + blitRegion.srcSubresource.mipLevel = srcMipLevel; blitRegion.dstSubresource.aspectMask = aspectMask; blitRegion.dstSubresource.baseArrayLayer = 0; - blitRegion.dstSubresource.layerCount = 1; - blitRegion.dstSubresource.mipLevel = 0; + blitRegion.dstSubresource.layerCount = layerCount; + blitRegion.dstSubresource.mipLevel = dstMipLevel; VkBlitImageInfo2 blitInfo = {}; blitInfo.sType = VK_STRUCTURE_TYPE_BLIT_IMAGE_INFO_2; @@ -255,4 +262,4 @@ namespace Elixir::Vulkan vkCmdBlitImage2(cmd, &blitInfo); } } -} \ No newline at end of file +} diff --git a/Elixir/Source/Graphics/Vulkan/VulkanDescriptorPool.cpp b/Elixir/Source/Graphics/Vulkan/VulkanDescriptorPool.cpp index 1a048afc..f79ff491 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanDescriptorPool.cpp +++ b/Elixir/Source/Graphics/Vulkan/VulkanDescriptorPool.cpp @@ -403,8 +403,8 @@ namespace Elixir::Vulkan for (const auto& texture : textures) { - const auto vkTexture = TryToGetVulkanImage(texture.get()); - imageInfos.push_back(vkTexture->GetVulkanDescriptorInfo()); + const auto image = TryToGetVulkanImage(texture->GetImage().get()); + imageInfos.push_back(image->GetVulkanDescriptorInfo()); VkWriteDescriptorSet writeSet = {}; writeSet.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; diff --git a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp index ddeff100..df4e097c 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp +++ b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include @@ -17,6 +17,7 @@ #include "GLFW/glfw3.h" #include +#include namespace Elixir { @@ -24,39 +25,94 @@ namespace Elixir using namespace Vulkan::Converters; using namespace SpirV; - template - void LogError(vkb::Result result, std::string preMessage = "") + namespace { - if (!result) + struct SDeferredResourceRelease { - const auto message = preMessage + result.error().message(); - EE_CORE_FATAL(message) + explicit SDeferredResourceRelease(std::function task) + : Task(std::move(task)) {} + + std::function Task; + std::atomic PendingFrames = 0; + }; + + thread_local const VulkanGraphicsContext* s_RenderThreadContext = nullptr; + + class SRenderThreadScope + { + public: + explicit SRenderThreadScope(const VulkanGraphicsContext* context) + : m_PreviousContext(s_RenderThreadContext) + { + s_RenderThreadContext = context; + } + + ~SRenderThreadScope() + { + s_RenderThreadContext = m_PreviousContext; + } + + private: + const VulkanGraphicsContext* m_PreviousContext; + }; + + struct SRetiredImage + { + VkDevice Device = VK_NULL_HANDLE; + VmaAllocator Allocator = VK_NULL_HANDLE; + VkImageView View = VK_NULL_HANDLE; + VkImage Image = VK_NULL_HANDLE; + VmaAllocation Allocation = VK_NULL_HANDLE; + std::atomic PendingFrames = 0; + + void Release() const + { + if (View) + vkDestroyImageView(Device, View, nullptr); + + if (Image) + vmaDestroyImage(Allocator, Image, Allocation); + } + }; + + template + void LogError(vkb::Result result, std::string preMessage = "") + { + if (!result) + { + const auto message = preMessage + result.error().message(); + EE_CORE_FATAL(message) + } } - } - void DumpAllocatorStats( - const VmaAllocator allocator, - const std::string& filename = "AllocatorStats.json" - ) - { - std::ofstream statsFile(filename); - if (statsFile.is_open()) + void DumpAllocatorStats( + const VmaAllocator allocator, + const std::string& filename = "AllocatorStats.json" + ) { - char* stats; - vmaBuildStatsString(allocator, &stats, VK_TRUE); - statsFile << stats; + std::ofstream statsFile(filename); + if (statsFile.is_open()) + { + char* stats; + vmaBuildStatsString(allocator, &stats, VK_TRUE); + statsFile << stats; - vmaFreeStatsString(allocator, stats); - statsFile.close(); + vmaFreeStatsString(allocator, stats); + statsFile.close(); - EE_CORE_INFO("Allocator stats dumped to {0}.", filename); + EE_CORE_INFO("Allocator stats dumped to {0}.", filename); + } } } /* VulkanGraphicsContext */ - VulkanGraphicsContext::VulkanGraphicsContext(const EGraphicsAPI api, Executor* executor, const Window* window) - : GraphicsContext(api, window), m_Executor(executor) + VulkanGraphicsContext::VulkanGraphicsContext( + const EGraphicsAPI api, + Executor* executor, + const Window* window + ) : GraphicsContext(api, window), + m_Executor(executor) { EE_PROFILE_ZONE_SCOPED() EE_CORE_ASSERT(executor, "Invalid executor!") @@ -130,12 +186,9 @@ namespace Elixir } } - void VulkanGraphicsContext::ProcessEvent(Event& event) + void VulkanGraphicsContext::ProcessEvent(Event&) { - EventDispatcher dispatcher(event); - dispatcher.Dispatch( - EE_BIND_EVENT_FN(VulkanGraphicsContext::HandleFramebufferResize) - ); + // Framebuffer resize work is queued by Application and runs on the rendering thread. } void VulkanGraphicsContext::RenderFrame(std::function callback) @@ -148,7 +201,7 @@ namespace Elixir m_FrameSemaphore.acquire(); - m_Executor->Enqueue(EThreadName::Rendering, [this, callback]() + if (!EnqueueRenderTask([this, callback]() { if (!Prepare()) { @@ -163,7 +216,10 @@ namespace Elixir Present(); m_FrameSemaphore.release(); - }); + })) + { + m_FrameSemaphore.release(); + } } void VulkanGraphicsContext::DrainRenderQueue() @@ -178,28 +234,109 @@ namespace Elixir m_Executor->ShutdownRenderPool(); WaitDeviceIdle(); + for (auto& frame : m_Frames) + frame.DeletionQueue.Flush(); + ResetFrameUsageState(); } - void VulkanGraphicsContext::SetClearColor(const glm::vec4& color) + bool VulkanGraphicsContext::EnqueueRenderTask(std::function task) const { - EE_PROFILE_ZONE_SCOPED() - m_ClearColor = {{ color.r, color.g, color.b, color.a }}; + if (!task || !m_AcceptingFrames.load()) + return false; + + m_Executor->Enqueue(EThreadName::Rendering, [this, task = std::move(task)]() + { + SRenderThreadScope scope(this); + task(); + }); + return true; } - void VulkanGraphicsContext::Clear() + bool VulkanGraphicsContext::RunRenderTaskAndWait(std::function task) const { - EE_PROFILE_ZONE_SCOPED() + if (!task) + return false; - const auto range = Initializers::ImageSubresourceRange(EImageAspect::Color); - vkCmdClearColorImage( - m_MainCommandBuffer->GetVulkanCommandBuffer(), - TryToGetVulkanImage(m_RenderTarget.get())->GetVulkanImage(), - VK_IMAGE_LAYOUT_GENERAL, - &m_ClearColor, - 1, - &range - ); + if (IsRenderThread()) + { + task(); + return true; + } + + const auto completion = CreateRef>(); + auto result = completion->get_future(); + if (!EnqueueRenderTask([task = std::move(task), completion]() mutable + { + try + { + task(); + completion->set_value(); + } + catch (...) + { + completion->set_exception(std::current_exception()); + } + })) + { + return false; + } + + result.get(); + return true; + } + + bool VulkanGraphicsContext::DeferResourceRelease(std::function task) const + { + if (!task) return false; + + if (!IsRenderThread()) + { + return EnqueueRenderTask([this, task = std::move(task)]() mutable + { + DeferResourceRelease(std::move(task)); + }); + } + + uint32_t pendingFrames = 0; + for (const auto& frame : m_Frames) + { + if (frame.InUseByRenderThread.load()) + ++pendingFrames; + } + + if (pendingFrames == 0) + { + task(); + return true; + } + + const auto deferred = CreateRef(std::move(task)); + deferred->PendingFrames = pendingFrames; + for (auto& frame : m_Frames) + { + if (!frame.InUseByRenderThread.load()) + continue; + + frame.DeletionQueue.Push([deferred]() + { + if (deferred->PendingFrames.fetch_sub(1) == 1) + deferred->Task(); + }); + } + + return true; + } + + bool VulkanGraphicsContext::IsRenderThread() const + { + return s_RenderThreadContext == this; + } + + void VulkanGraphicsContext::SetClearColor(const glm::vec4& color) + { + EE_PROFILE_ZONE_SCOPED() + m_ClearColor = {{ color.r, color.g, color.b, color.a }}; } void VulkanGraphicsContext::Resize(const Extent2D extent) @@ -210,9 +347,8 @@ namespace Elixir m_SwapchainExtent = extent; m_SwapchainRecreateRequested = true; - const auto cmd = GetUploadCommandBuffer(); - m_RenderTarget->Resize(cmd, extent); - m_DepthStencilRenderTarget->Resize(cmd, extent); + m_RenderTarget->Resize(extent); + m_DepthStencilRenderTarget->Resize(extent); } Ref VulkanGraphicsContext::GetSecondaryCommandBuffer() const @@ -237,6 +373,83 @@ namespace Elixir VK_CHECK_RESULT(vkDeviceWaitIdle(m_Device)); } + void VulkanGraphicsContext::WaitForSubmittedFrames() const + { + EE_CORE_ASSERT(IsRenderThread(), "Submitted frame fences must be waited on the rendering thread.") + + for (auto& frame : m_Frames) + { + if (!frame.InUseByRenderThread.load()) + continue; + + VK_CHECK_RESULT( + vkWaitForFences( + m_Device, + 1, + &frame.RenderFence, + VK_TRUE, UINT64_MAX + ) + ); + + frame.InUseByRenderThread = false; + frame.DeletionQueue.Flush(); + } + } + + void VulkanGraphicsContext::RetireImage( + const VkImageView imageView, + const VkImage image, + const VmaAllocation allocation + ) const + { + if (!imageView && !image) + return; + + if (!IsRenderThread()) + { + const auto enqueued = EnqueueRenderTask([this, imageView, image, allocation]() + { + RetireImage(imageView, image, allocation); + }); + + if (enqueued) return; + } + + const auto retired = CreateRef( + m_Device, + m_Allocator, + imageView, + image, + allocation + ); + + uint32_t pendingFrames = 0; + for (const auto& frame : m_Frames) + { + if (frame.InUseByRenderThread.load()) + ++pendingFrames; + } + + if (pendingFrames == 0) + { + retired->Release(); + return; + } + + retired->PendingFrames = pendingFrames; + for (auto& frame : m_Frames) + { + if (!frame.InUseByRenderThread.load()) + continue; + + frame.DeletionQueue.Push([retired]() + { + if (retired->PendingFrames.fetch_sub(1) == 1) + retired->Release(); + }); + } + } + void VulkanGraphicsContext::InitVulkan() { EE_PROFILE_ZONE_SCOPED() @@ -400,7 +613,7 @@ namespace Elixir { VK_DESCRIPTOR_TYPE_SAMPLER, 0.05 } }; - m_DescriptorPool = CreateRef(*this, 128 * FRAMES, sizes); + m_DescriptorPool = CreateRef(*this, 512 * FRAMES, sizes); m_BindlessDescriptorPool = CreateRef(*this); } @@ -485,24 +698,45 @@ namespace Elixir m_SwapchainRecreateRequested = false; } - void VulkanGraphicsContext::CreateRenderTargets() + void VulkanGraphicsContext::ClearImage(const Ref& image) { - auto colorInfo = Texture2D::CreateImageInfo( - EImageFormat::R8G8B8A8_SRGB, - m_SwapchainExtent.Width, - m_SwapchainExtent.Height - ); - colorInfo.Usage = EImageUsage::ColorAttachment | EImageUsage::TransferSrc | EImageUsage::TransferDst; - colorInfo.InitialLayout = EImageLayout::General; - m_RenderTarget = CreateRef(this, colorInfo); - - auto depthStencilInfo = DepthStencilImage::CreateImageInfo( - EDepthStencilImageFormat::D32_SFLOAT, - m_SwapchainExtent.Width, - m_SwapchainExtent.Height + EE_PROFILE_ZONE_SCOPED() + EE_CORE_ASSERT(image, "An target image is required for clearing.") + + const auto range = Initializers::ImageSubresourceRange(EImageAspect::Color); + vkCmdClearColorImage( + m_MainCommandBuffer->GetVulkanCommandBuffer(), + TryToGetVulkanImage(image.get())->GetVulkanImage(), + VK_IMAGE_LAYOUT_GENERAL, + &m_ClearColor, + 1, + &range ); - depthStencilInfo.Usage = depthStencilInfo.Usage | EImageUsage::TransferSrc | EImageUsage::TransferDst; - m_DepthStencilRenderTarget = CreateRef(this, depthStencilInfo); + } + + void VulkanGraphicsContext::CreateRenderTargets() + { + const SImageCreateInfo colorInfo{ + .Width = m_SwapchainExtent.Width, + .Height = m_SwapchainExtent.Height, + .Type = EImageType::_2D, + .Format = EImageFormat::R8G8B8A8_SRGB, + .Usage = EImageUsage::ColorAttachment | EImageUsage::TransferSrc | + EImageUsage::TransferDst, + .InitialLayout = EImageLayout::General, + }; + m_RenderTarget = Image::Create(this, colorInfo); + + const SImageCreateInfo depthInfo{ + .Width = m_SwapchainExtent.Width, + .Height = m_SwapchainExtent.Height, + .Type = EImageType::_2D, + .Format = EImageFormat::D32_SFLOAT, + .Usage = EImageUsage::Sampled | EImageUsage::DepthStencilAttachment | + EImageUsage::TransferSrc | EImageUsage::TransferDst, + .InitialLayout = EImageLayout::DepthAttachment, + }; + m_DepthStencilRenderTarget = Image::Create(this, depthInfo); } void VulkanGraphicsContext::ResetFrameUsageState() @@ -574,6 +808,7 @@ namespace Elixir VK_CHECK_RESULT(result); m_MainCommandBuffer->Begin(); + m_IsFrameRecording = true; m_RenderTarget->Transition(m_MainCommandBuffer, EImageLayout::General); @@ -630,6 +865,7 @@ namespace Elixir swapchain.RenderSemaphore, frame.RenderFence ); + m_IsFrameRecording = false; m_CommandPoolManager->RecycleCommandBuffer(m_MainCommandBuffer); @@ -643,11 +879,11 @@ namespace Elixir presentInfo.pImageIndices = &m_CurrentSwapchainImageIndex; const auto result = vkQueuePresentKHR(m_GraphicsQueue, &presentInfo); - if (result == VK_ERROR_OUT_OF_DATE_KHR) { m_SwapchainRecreateRequested = true; - frame.InUseByRenderThread = false; + // Submission already signalled RenderFence. Keep this frame tracked until its + // fence is waited so deferred resource releases cannot destroy in-use images. return; } @@ -659,9 +895,4 @@ namespace Elixir EE_PROFILE_FRAME_MARK_NAMED(EE_PROFILE_RENDER) } - bool VulkanGraphicsContext::HandleFramebufferResize(const FramebufferResizeEvent& event) - { - Resize(event.GetExtent()); - return true; - } } diff --git a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.h b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.h index 2662ec5e..718e8fe4 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.h +++ b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.h @@ -60,9 +60,14 @@ namespace Elixir::Vulkan void RenderFrame(std::function callback) override; void DrainRenderQueue() override; + bool EnqueueRenderTask(std::function task) const override; + bool RunRenderTaskAndWait(std::function task) const override; + bool DeferResourceRelease(std::function task) const override; + bool IsRenderThread() const override; + bool IsFrameRecording() const override { return m_IsFrameRecording; } + void WaitForSubmittedFrames() const override; void SetClearColor(const glm::vec4& color) override; - void Clear() override; void Resize(Extent2D extent) override; @@ -72,6 +77,12 @@ namespace Elixir::Vulkan void WaitDeviceIdle() const override; + /** + * @brief Defers native image destruction until every frame that can use it has + * completed. + */ + void RetireImage(VkImageView imageView, VkImage image, VmaAllocation allocation) const; + Extent3D GetSwapchainExtent() const override { return m_SwapchainExtent;} SFrameData& GetCurrentFrame() { return m_Frames[GetFrameIndex()]; } @@ -103,6 +114,7 @@ namespace Elixir::Vulkan void DestroySwapchain(); void RecreateSwapchain(); + void ClearImage(const Ref& image) override; void CreateRenderTargets() override; void ResetFrameUsageState(); @@ -111,8 +123,6 @@ namespace Elixir::Vulkan void Submit(); void Present(); - bool HandleFramebufferResize(const FramebufferResizeEvent& event); - bool m_IsInitialized = false; #ifdef EE_DEBUG @@ -147,8 +157,9 @@ namespace Elixir::Vulkan Scope m_CommandPoolManager; Ref m_MainCommandBuffer; + bool m_IsFrameRecording = false; - std::vector m_Frames; + mutable std::vector m_Frames; VkClearColorValue m_ClearColor; SDeletionQueue m_DeletionQueue; diff --git a/Elixir/Source/Graphics/Vulkan/VulkanImage.cpp b/Elixir/Source/Graphics/Vulkan/VulkanImage.cpp index b42d967b..884cd703 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanImage.cpp +++ b/Elixir/Source/Graphics/Vulkan/VulkanImage.cpp @@ -1,401 +1,298 @@ #include "epch.h" #include "VulkanImage.h" -#include -#include -#include #include -#include #include -#include +#include #include namespace Elixir::Vulkan { - using namespace Elixir::Graphics::Converters; - using namespace Elixir::Graphics::Utils; - using namespace Elixir::Vulkan; - - namespace + const VulkanImage* TryToGetVulkanImage(const Image* image) { - using VulkanImageTypes = std::tuple< - VulkanBaseImage, - VulkanBaseImage, - VulkanBaseImage, - VulkanBaseImage, - VulkanBaseImage - >; - - template - const VulkanBaseImageBase* TryToGetVulkanImageImpl(const Image* image) - { - if constexpr (I < std::tuple_size_v) - { - using ImageType = std::tuple_element_t; - - if (auto* img = dynamic_cast(image)) - { - return img; - } - - return TryToGetVulkanImageImpl(image); - } - - EE_CORE_ASSERT(false, "Unsupported image type!") - return nullptr; - } - - template - VkImage TryToGetVulkanImageHandleImpl(const Image* image) - { - return TryToGetVulkanImageImpl(image)->GetVulkanImage(); - } - } - - const VulkanBaseImageBase* TryToGetVulkanImage(const Image* image) - { - if (!image) return nullptr; - return TryToGetVulkanImageImpl(image); + return dynamic_cast(image); } VkImage TryToGetVulkanImageHandle(const Image* image) { - if (!image) return VK_NULL_HANDLE; - return TryToGetVulkanImageHandleImpl(image); + const auto* vkImage = TryToGetVulkanImage(image); + return vkImage ? vkImage->GetVulkanImage() : VK_NULL_HANDLE; } - /* VulkanBaseImage */ - - template - void VulkanBaseImage::Destroy() - { - EE_PROFILE_ZONE_SCOPED() - - if (!IsValid()) return; - - vkDestroyImageView(m_GraphicsContext->GetDevice(), m_ImageView, nullptr); - vmaDestroyImage(m_GraphicsContext->GetAllocator(), m_Image, m_Allocation); - m_Image = VK_NULL_HANDLE; - m_ImageView = VK_NULL_HANDLE; - m_Allocation = VK_NULL_HANDLE; - m_DescriptorInfo = {}; - } + VulkanImage::VulkanImage(const GraphicsContext* context, const SImageCreateInfo& info) + : Image(context, info), + m_Context((const VulkanGraphicsContext*)context) {} - template - void VulkanBaseImage::Resize(const Ref& cmd, const Extent3D extent) + VulkanImage::~VulkanImage() { - EE_PROFILE_ZONE_SCOPED() - if (extent.Width <= 0 || extent.Height <= 0 || extent.Depth <= 0) return; - if (extent.Width == this->m_Extent.Width && - extent.Height == this->m_Extent.Height && - extent.Depth == this->m_Extent.Depth) return; - - // Remember the layout the image is currently used in (e.g. depth/color - // attachment) so it can be restored after the resize. Otherwise, the image - // is left in TransferDst, which is invalid for a rendering attachment. - const EImageLayout originalLayout = this->GetLayout(); - const Extent3D oldExtent = this->m_Extent; - - // 1. Preserve the current content in a staging image of the SAME size. - // (The underlying VkImage allocation is fixed at creation, so the image - // must be recreated to actually change its dimensions.) - SImageCreateInfo stagingInfo = this->GetCreateInfo(); - stagingInfo.InitialLayout = EImageLayout::TransferDst; - const auto staging = CreateRef(m_GraphicsContext, stagingInfo); - - cmd->Begin(); - Transition(cmd, EImageLayout::TransferSrc); - Copy(cmd, staging, oldExtent, oldExtent); - staging->Transition(cmd, EImageLayout::TransferSrc); - cmd->Flush(); // Waits for the GPU; the old image is safe to destroy after. - - // 2. Reallocate this image at the new extent. Destroy(); - this->m_Extent = extent; - this->m_Layout = EImageLayout::Undefined; - this->RecalculateSize(); - - SImageCreateInfo info = this->GetCreateInfo(); - info.InitialLayout = EImageLayout::Undefined; - CreateImage(info); - CreateImageView(); - - // 3. Blit the preserved content into the resized image, scaling as needed. - cmd->Begin(); - Transition(cmd, EImageLayout::TransferDst); - staging->Copy(cmd, this, oldExtent, extent); - Transition(cmd, originalLayout); - cmd->Flush(); - - // Refresh the descriptor to point at the new view and final layout. - CreateDescriptorInfo(); - if (this->GetSampler()) UpdateSampler(); - - EE_CORE_TRACE("Image ({0}) resized: {1}.", this->m_UUID, this->m_Extent) } - template - void VulkanBaseImage::Transition(const CommandBuffer* cmd, const EImageLayout layout) + void VulkanImage::Destroy() { - EE_PROFILE_ZONE_SCOPED() - - const auto vk_Cmd = static_cast(cmd); - - CommandUtils::TransitionImage( - vk_Cmd->GetVulkanCommandBuffer(), - m_Image, - Converters::GetImageLayout(this->GetLayout()), - Converters::GetImageLayout(layout), - Converters::GetImageAspect(this->GetAspect()) - ); - - this->m_Layout = layout; - } - - template - void VulkanBaseImage::Copy( - const CommandBuffer* cmd, - Image* dst, - const Extent3D& srcExtent, - const Extent3D& dstExtent - ) - { - EE_PROFILE_ZONE_SCOPED() - - const auto vk_Cmd = static_cast(cmd); - const auto vk_Dst = TryToGetVulkanImageHandle(dst); + const auto imageView = std::exchange(m_ImageView, VK_NULL_HANDLE); + const auto image = std::exchange(m_Image, VK_NULL_HANDLE); + const auto allocation = std::exchange(m_Allocation, VK_NULL_HANDLE); - EE_CORE_ASSERT(vk_Dst != nullptr, "Invalid destination image!") - EE_CORE_ASSERT(vk_Dst != VK_NULL_HANDLE, "Invalid destination image!") - - // Blit with the image's own aspect. Depth/stencil images must use the depth - // aspect and the NEAREST filter (linear filtering is invalid for depth formats). - const auto aspect = this->GetAspect(); - const bool isDepthStencil = (aspect & EImageAspect::Depth) || (aspect & EImageAspect::Stencil); - - CommandUtils::CopyImageToImage( - vk_Cmd->GetVulkanCommandBuffer(), - m_Image, - vk_Dst, - Converters::GetExtent3D(srcExtent), - Converters::GetExtent3D(dstExtent), - Converters::GetImageAspect(aspect), - isDepthStencil ? VK_FILTER_NEAREST : VK_FILTER_LINEAR - ); + m_DescriptorInfo = {}; - dst->m_Extent = dstExtent; + m_Context->RetireImage(imageView, image, allocation); } - template - void VulkanBaseImage::CopyFrom( - const CommandBuffer* cmd, - const Buffer* src, - const std::span regions - ) + void VulkanImage::CreateResource(const SImageCreateInfo& info) { - EE_PROFILE_ZONE_SCOPED() - - const auto vk_Cmd = static_cast(cmd); - const auto vk_Src = TryToGetVulkanBuffer(src); - - EE_CORE_ASSERT(vk_Src != VK_NULL_HANDLE, "Invalid source buffer!") - - std::span copyRegions = regions; - - if (copyRegions.empty()) + if (info.MipmapMode == EImageMipmapMode::SimpleAverage && info.MipLevels > 1) { - SBufferImageCopy defaultRegion[] = { - SBufferImageCopy::Default(this->GetExtent()) - }; - copyRegions = std::span(defaultRegion); - } - - std::vector imageCopies(copyRegions.size()); - - std::ranges::transform( - copyRegions, imageCopies.begin(), Converters::GetBufferImageCopy - ); - - vkCmdCopyBufferToImage( - vk_Cmd->GetVulkanCommandBuffer(), vk_Src, m_Image, - Converters::GetImageLayout(this->GetLayout()), imageCopies.size(), - imageCopies.data() - ); - } - - template - VulkanBaseImage::VulkanBaseImage( - const GraphicsContext* context, - const SImageCreateInfo& info - ) : Base(context, info), VulkanBaseImageBase() - { - EE_PROFILE_ZONE_SCOPED() - - m_GraphicsContext = static_cast(context); - - CreateImage(info); - InitImage(info); - CreateImageView(); - CreateDescriptorInfo(); - } + VkFormatProperties properties{}; + vkGetPhysicalDeviceFormatProperties( + m_Context->GetGPU(), + Converters::GetFormat(info.Format), + &properties + ); - template - void VulkanBaseImage::CreateImage(const SImageCreateInfo& info) - { - EE_PROFILE_ZONE_SCOPED() + constexpr VkFormatFeatureFlags required = VK_FORMAT_FEATURE_BLIT_SRC_BIT | + VK_FORMAT_FEATURE_BLIT_DST_BIT | + VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT; - const uint32_t queueFamily = m_GraphicsContext->GetGraphicsQueueFamily(); + if (m_Aspect != EImageAspect::Color || + (properties.optimalTilingFeatures & required) != required) + { + EE_CORE_ERROR("The Vulkan format does not support linear mip generation.") + return; + } + } + const uint32_t queueFamily = m_Context->GetGraphicsQueueFamily(); const auto imageInfo = Initializers::ImageCreateInfo(info, queueFamily); const auto allocInfo = Initializers::AllocationCreateInfo(info.AllocationInfo); - VK_CHECK_RESULT( vmaCreateImage( - m_GraphicsContext->GetAllocator(), - &imageInfo, - &allocInfo, + m_Context->GetAllocator(), + &imageInfo, &allocInfo, &m_Image, &m_Allocation, nullptr ) ); - if (!this->m_DebugName.empty()) - { + if (!m_DebugName.empty()) vmaSetAllocationName( - m_GraphicsContext->GetAllocator(), + m_Context->GetAllocator(), m_Allocation, - this->m_DebugName.c_str() + m_DebugName.c_str() ); - } - } - - template - void VulkanBaseImage::InitImage(const SImageCreateInfo& info) - { - EE_PROFILE_ZONE_SCOPED() - auto cmd = m_GraphicsContext->GetUploadCommandBuffer(); - auto vk_Cmd = std::static_pointer_cast(cmd); + constexpr auto viewUsage = EImageUsage::Sampled | + EImageUsage::Storage | + EImageUsage::ColorAttachment | + EImageUsage::DepthStencilAttachment | + EImageUsage::InputAttachment; - cmd->Begin(); - - if (info.InitialData) + if (HasAnyFlags(m_Usage, viewUsage)) { - this->Transition(cmd.get(), EImageLayout::TransferDst); - - const auto stagingBuffer = StagingBuffer::Create( - m_GraphicsContext, - this->GetSize(), - info.InitialData + const auto viewInfo = Initializers::ImageViewCreateInfo(this); + VK_CHECK_RESULT( + vkCreateImageView( + m_Context->GetDevice(), + &viewInfo, + nullptr, + &m_ImageView + ) ); + } - SBufferImageCopy copyRegion = {}; - copyRegion.ImageSubresource.AspectMask = this->GetAspect(); - copyRegion.ImageSubresource.LayerCount = this->GetArrayLayers(); - copyRegion.ImageExtent = this->GetExtent(); + m_DescriptorInfo = {}; + m_DescriptorInfo.imageLayout = Converters::GetImageLayout(m_Layout); + m_DescriptorInfo.imageView = m_ImageView; + ++m_ResourceGeneration; + } - SBufferImageCopy regions[] = { copyRegion }; - this->CopyFrom(cmd, stagingBuffer, regions); + void VulkanImage::Transition(const CommandBuffer* cmd, const EImageLayout layout) + { + if (!IsValid()) + { + EE_CORE_ERROR("Cannot transition an image without native storage.") + return; + } - this->Transition(cmd.get(), info.InitialLayout); - cmd->Flush(); + if (layout == m_Layout) + return; - stagingBuffer->Destroy(); - } - else + if (layout == EImageLayout::Undefined || layout == EImageLayout::PreInitialized) { - this->Transition(cmd.get(), info.InitialLayout); - cmd->Flush(); + EE_CORE_ERROR("Cannot transition an image into an initial-only layout.") + return; } + + const auto* vkCmd = static_cast(cmd); + CommandUtils::TransitionImage( + vkCmd->GetVulkanCommandBuffer(), + m_Image, + Converters::GetImageLayout(m_Layout), + Converters::GetImageLayout(layout), + Converters::GetImageAspect(m_Aspect) + ); + + m_Layout = layout; + m_DescriptorInfo.imageLayout = Converters::GetImageLayout(layout); } - template - void VulkanBaseImage::CreateImageView() + void VulkanImage::Barrier(const CommandBuffer* cmd) { - EE_PROFILE_ZONE_SCOPED() + if (!IsValid()) + { + EE_CORE_ERROR("Cannot synchronize an image without native storage.") + return; + } - const auto viewInfo = Initializers::ImageViewCreateInfo(this); - VK_CHECK_RESULT( - vkCreateImageView( - m_GraphicsContext->GetDevice(), - &viewInfo, - nullptr, - &m_ImageView - ) + const auto* vkCmd = static_cast(cmd); + CommandUtils::TransitionImage( + vkCmd->GetVulkanCommandBuffer(), + m_Image, + Converters::GetImageLayout(m_Layout), + Converters::GetImageLayout(m_Layout), + Converters::GetImageAspect(m_Aspect) ); } - template - void VulkanBaseImage::CreateDescriptorInfo() + void VulkanImage::CopyMip( + const CommandBuffer* cmd, Image* dst, + const Extent3D& srcExtent, const Extent3D& dstExtent, + const uint32_t level + ) { - EE_PROFILE_ZONE_SCOPED() - m_DescriptorInfo = VkDescriptorImageInfo{}; - m_DescriptorInfo.imageLayout = Converters::GetImageLayout(this->GetLayout()); - m_DescriptorInfo.imageView = m_ImageView; - } + const auto* vkCmd = static_cast(cmd); + const auto target = TryToGetVulkanImageHandle(dst); - template - void VulkanBaseImage::UpdateSampler() - { - auto vk_Sampler = std::static_pointer_cast(this->GetSampler()); - m_DescriptorInfo.sampler = vk_Sampler->GetVulkanSampler(); - // TODO: Should update in shader too! - } + if (!IsValid() || !target || dst->GetArrayLayers() != m_ArrayLayers || + dst->GetAspect() != m_Aspect || + level >= m_MipLevels || level >= dst->GetMipLevels()) + { + EE_CORE_ERROR("Invalid source image or incompatible image blit destination.") + return; + } - /* VulkanImage */ + const bool depthStencil = (m_Aspect & EImageAspect::Depth) || + (m_Aspect & EImageAspect::Stencil); - VulkanImage::VulkanImage( - const GraphicsContext* context, - const EImageFormat format, - const uint32_t width, - const void* data - ) : VulkanImage(context, CreateImageInfo(format, width, data)) {} + for (const auto aspect : { EImageAspect::Color, EImageAspect::Depth, EImageAspect::Stencil }) + { + if (!(m_Aspect & aspect)) + continue; + + CommandUtils::CopyImageToImage( + vkCmd->GetVulkanCommandBuffer(), + m_Image, + target, + Converters::GetExtent3D(srcExtent), + Converters::GetExtent3D(dstExtent), + Converters::GetImageAspect(aspect), + depthStencil ? VK_FILTER_NEAREST : VK_FILTER_LINEAR, + level, + level, + m_ArrayLayers + ); + } + } - VulkanImage::VulkanImage(const GraphicsContext* context, const SImageCreateInfo& info) - : VulkanBaseImage(context, info) + void VulkanImage::CopyFrom( + const CommandBuffer* cmd, const Buffer* src, + const std::span regions + ) { - EE_PROFILE_ZONE_SCOPED() + if (!IsValid()) + { + EE_CORE_ERROR("Cannot upload to an image without native storage.") + return; + } - // TODO: Set a default sampler - // auto sampler = SamplerBuilder() - // .SetMagFilter(ESamplerFilter::Nearest) - // .SetMinFilter(ESamplerFilter::Nearest) - // .Build(); + const auto* vkCmd = static_cast(cmd); + const auto source = TryToGetVulkanBuffer(src); + EE_CORE_ASSERT(source != VK_NULL_HANDLE, "Invalid source buffer!") - // SetSampler(sampler); + SBufferImageCopy defaultRegion{}; + defaultRegion.ImageSubresource = { m_Aspect, 0, 0, m_ArrayLayers }; + defaultRegion.ImageExtent = m_Extent; + + const auto copyRegions = regions.empty() + ? std::span(&defaultRegion, 1) + : regions; + std::vector copies(copyRegions.size()); + std::ranges::transform( + copyRegions, + copies.begin(), + Converters::GetBufferImageCopy + ); + + vkCmdCopyBufferToImage( + vkCmd->GetVulkanCommandBuffer(), + source, + m_Image, + Converters::GetImageLayout(m_Layout), + uint32_t(copies.size()), + copies.data() + ); } - VulkanImage::~VulkanImage() + void VulkanImage::GenerateMipmaps(const CommandBuffer* cmd, const EImageLayout finalLayout) { - EE_PROFILE_ZONE_SCOPED() - VulkanBaseImage::Destroy(); - } + if (!IsValid()) + { + EE_CORE_ERROR("Cannot generate mipmaps for an image without native storage.") + return; + } - /* VulkanDepthStencilImage */ + const auto vkCmd = static_cast(cmd); + const auto aspect = Converters::GetImageAspect(m_Aspect); + const auto targetLayout = Converters::GetImageLayout(finalLayout); - VulkanDepthStencilImage::VulkanDepthStencilImage( - const GraphicsContext* context, - const EDepthStencilImageFormat format, - const uint32_t width, - const uint32_t height - ) : VulkanDepthStencilImage(context, CreateImageInfo(format, width, height)) {} + for (uint32_t level = 1; level < m_MipLevels; ++level) + { + CommandUtils::TransitionImage( + vkCmd->GetVulkanCommandBuffer(), + m_Image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + aspect, + level - 1, + 1 + ); - VulkanDepthStencilImage::VulkanDepthStencilImage( - const GraphicsContext* context, - const SImageCreateInfo& info - ) - : VulkanBaseImage(context, info) - { - } + CommandUtils::CopyImageToImage( + vkCmd->GetVulkanCommandBuffer(), + m_Image, + m_Image, + Converters::GetExtent3D(GetMipExtent(level - 1)), + Converters::GetExtent3D(GetMipExtent(level)), + aspect, + VK_FILTER_LINEAR, + level - 1, + level, + m_ArrayLayers + ); - VulkanDepthStencilImage::~VulkanDepthStencilImage() - { - EE_PROFILE_ZONE_SCOPED() - VulkanBaseImage::Destroy(); - } + CommandUtils::TransitionImage( + vkCmd->GetVulkanCommandBuffer(), + m_Image, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + targetLayout, + aspect, + level - 1, + 1 + ); + } + + CommandUtils::TransitionImage( + vkCmd->GetVulkanCommandBuffer(), + m_Image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + targetLayout, + aspect, + m_MipLevels - 1, + 1 + ); - template class VulkanBaseImage; - template class VulkanBaseImage; - template class VulkanBaseImage; -} \ No newline at end of file + m_Layout = finalLayout; + m_DescriptorInfo.imageLayout = targetLayout; + } +} diff --git a/Elixir/Source/Graphics/Vulkan/VulkanImage.h b/Elixir/Source/Graphics/Vulkan/VulkanImage.h index bc66b186..c8f299f0 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanImage.h +++ b/Elixir/Source/Graphics/Vulkan/VulkanImage.h @@ -1,112 +1,100 @@ #pragma once #include - #include #include namespace Elixir::Vulkan { - using namespace Elixir; - class VulkanGraphicsContext; + class VulkanImage; + /** Returns the native image handle, or null for a null image. */ ELIXIR_API VkImage TryToGetVulkanImageHandle(const Image* image); - class VulkanBaseImageBase - { - public: - virtual ~VulkanBaseImageBase() = default; - - virtual VkImage GetVulkanImage() const = 0; - virtual VkImageView GetVulkanImageView() const = 0; - virtual const VkDescriptorImageInfo& GetVulkanDescriptorInfo() const = 0; + /** Returns the Vulkan backend, or null when the image does not use Vulkan. */ + ELIXIR_API const VulkanImage* TryToGetVulkanImage(const Image* image); - protected: - VulkanBaseImageBase() = default; - }; - - const VulkanBaseImageBase* TryToGetVulkanImage(const Image* image); - - template - class VulkanBaseImage : public Base, public VulkanBaseImageBase + /** @brief Implements image storage and device operations for Vulkan. */ + class ELIXIR_API VulkanImage final : public Image { public: - virtual void Destroy() override; + /** + * Stores the description resolved by Image::Create without allocating resources. + * Use Image::Create to obtain a fully initialized image. + * @param context Vulkan graphics context that must outlive the image. + * @param info Image description with mip levels and usage flags already resolved. + */ + VulkanImage(const GraphicsContext* context, const SImageCreateInfo& info); - using Image::Resize; - virtual void Resize(const Ref& cmd, Extent3D extent) override; + /** @brief Releases the native image and view. */ + ~VulkanImage() override; + + /** @brief Releases storage after all GPU work that uses it has completed. */ + void Destroy() override; using Image::Transition; - virtual void Transition(const CommandBuffer* cmd, EImageLayout layout) override; - using Image::Copy; - virtual void Copy( - const CommandBuffer* cmd, - Image* dst, - const Extent3D& srcExtent, - const Extent3D& dstExtent - ) override; + /** @brief Records a layout transition for every mip level and layer. + * @param cmd Recording Vulkan command buffer that receives the transition. + * @param layout Target layout for all mip levels and array layers. + */ + void Transition(const CommandBuffer* cmd, EImageLayout layout) override; + + /** @brief Records a memory dependency without changing the image layout. */ + void Barrier(const CommandBuffer* cmd) override; using Image::CopyFrom; - virtual void CopyFrom( + + /** @brief Records a buffer upload; an empty region list copies the base level of all layers. + * @param cmd Recording Vulkan command buffer that receives the upload. + * @param src Vulkan buffer containing the pixel data, with TransferSrc usage. + * @param regions Buffer-to-image copy regions; empty selects the full base level + * of every array layer. + */ + void CopyFrom( const CommandBuffer* cmd, const Buffer* src, std::span regions = {} ) override; + /** @brief Reports whether native storage is allocated. */ bool IsValid() const override { return m_Image != VK_NULL_HANDLE; } - VkImage GetVulkanImage() const override { return m_Image; } - VkImageView GetVulkanImageView() const override { return m_ImageView; } - const VkDescriptorImageInfo& GetVulkanDescriptorInfo() const override { return m_DescriptorInfo; } + /** @brief Returns the owned image handle. */ + VkImage GetVulkanImage() const { return m_Image; } + + /** @brief Returns the view spanning every mip level and layer. */ + VkImageView GetVulkanImageView() const { return m_ImageView; } - VulkanBaseImage& operator=(const VulkanBaseImage&) = delete; - VulkanBaseImage& operator=(VulkanBaseImage&&) = delete; + /** @brief Returns the generation of the current native image allocation. */ + uint64_t GetVulkanResourceGeneration() const { return m_ResourceGeneration; } - protected: - VulkanBaseImage(const GraphicsContext* context, const SImageCreateInfo& info); - VulkanBaseImage(const VulkanBaseImage&) = delete; - VulkanBaseImage(VulkanBaseImage&&) = delete; + /** @brief Returns the current view and layout; samplers are bound separately. */ + const VkDescriptorImageInfo& GetVulkanDescriptorInfo() const { return m_DescriptorInfo; } - void CreateImage(const SImageCreateInfo& info); - void InitImage(const SImageCreateInfo& info); - void CreateImageView(); - void CreateDescriptorInfo(); - void UpdateSampler() override; + private: + // Creates native storage and its view from the resolved description. + void CreateResource(const SImageCreateInfo& info) override; + + // Generates simple-average mip levels with linear image blits. + void GenerateMipmaps(const CommandBuffer* cmd, EImageLayout finalLayout) override; + + // Blits one mip level across all array layers. + void CopyMip( + const CommandBuffer* cmd, + Image* dst, + const Extent3D& srcExtent, + const Extent3D& dstExtent, + uint32_t level + ) override; VkImage m_Image = VK_NULL_HANDLE; VkImageView m_ImageView = VK_NULL_HANDLE; VkDescriptorImageInfo m_DescriptorInfo{}; - VmaAllocation m_Allocation = VK_NULL_HANDLE; + uint64_t m_ResourceGeneration = 0; - const VulkanGraphicsContext* m_GraphicsContext = nullptr; - }; - - class ELIXIR_API VulkanImage final : public VulkanBaseImage - { - public: - VulkanImage( - const GraphicsContext* context, - EImageFormat format, - uint32_t width, - const void* data = nullptr - ); - VulkanImage(const GraphicsContext* context, const SImageCreateInfo& info); - ~VulkanImage() override; - }; - - class ELIXIR_API VulkanDepthStencilImage final : public VulkanBaseImage - { - public: - VulkanDepthStencilImage( - const GraphicsContext* context, - EDepthStencilImageFormat format, - uint32_t width, - uint32_t height - ); - VulkanDepthStencilImage(const GraphicsContext* context, const SImageCreateInfo& info); - ~VulkanDepthStencilImage() override; + const VulkanGraphicsContext* m_Context; }; } diff --git a/Elixir/Source/Graphics/Vulkan/VulkanShader.cpp b/Elixir/Source/Graphics/Vulkan/VulkanShader.cpp index dd4e3630..b6e9ab0b 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanShader.cpp +++ b/Elixir/Source/Graphics/Vulkan/VulkanShader.cpp @@ -13,6 +13,23 @@ namespace Elixir::Vulkan { + namespace + { + uint64_t GetResourceGeneration(const Ref& image) + { + const auto vkImage = TryToGetVulkanImage(image.get()); + return vkImage ? vkImage->GetVulkanResourceGeneration() : 0; + } + + VkImageLayout GetDescriptorImageLayout(const Ref& image) + { + const auto vkImage = TryToGetVulkanImage(image.get()); + return vkImage + ? vkImage->GetVulkanDescriptorInfo().imageLayout + : VK_IMAGE_LAYOUT_UNDEFINED; + } + } + VulkanShader::VulkanShader(const GraphicsContext* context, SShaderCreateInfo&& info) : Shader(context, std::move(info)), m_DescriptorSets(*context) @@ -149,14 +166,41 @@ namespace Elixir::Vulkan { if (const auto binding = GetShaderBinding(name)) { - if (m_DescriptorSets.Set(*binding, DescriptorValue{ texture })) - m_Textures[*binding] = texture; + m_Textures[*binding] = texture; + m_DescriptorSets.Set( + *binding, + DescriptorValue{ STextureDescriptorValue{ + texture, + texture ? GetResourceGeneration(texture->GetImage()) : 0, + texture ? GetDescriptorImageLayout(texture->GetImage()) + : VK_IMAGE_LAYOUT_UNDEFINED + } } + ); return; } EE_CORE_ERROR("No texture binding named \"{0}\" found in shader...", name) } + void VulkanShader::BindImage(const std::string& name, const Ref& image) + { + if (const auto binding = GetShaderBinding(name)) + { + m_Images[*binding] = image; + m_DescriptorSets.Set( + *binding, + DescriptorValue{ SImageDescriptorValue{ + image, + GetResourceGeneration(image), + GetDescriptorImageLayout(image) + } } + ); + return; + } + + EE_CORE_ERROR("No image binding named \"{0}\" found in shader...", name) + } + void VulkanShader::BindTextureSet(const std::string& name, const Ref& set) { if (const auto binding = GetShaderBinding(name)) @@ -228,10 +272,10 @@ namespace Elixir::Vulkan std::vector VulkanShader::GetDescriptorSets() const { std::vector sets(m_DescriptorSets.GetCurrent()); - if (m_BindlessSet) + if (m_BindlessSetIndex) { const auto bindlessPool = m_GraphicsContext->GetBindlessDescriptorPool(); - sets.push_back(bindlessPool->GetDescriptorSet()); + sets[*m_BindlessSetIndex] = bindlessPool->GetDescriptorSet(); } return std::move(sets); @@ -289,7 +333,13 @@ namespace Elixir::Vulkan { if (resource.IsBindless()) { - m_BindlessSet = true; + const auto set = resource.GetSet(); + EE_CORE_ASSERT( + !m_BindlessSetIndex || *m_BindlessSetIndex == set, + "A shader can use bindless resources from only one descriptor set." + ) + m_BindlessSetIndex = set; + sets.try_emplace(set); continue; } @@ -371,11 +421,11 @@ namespace Elixir::Vulkan std::vector layouts(m_DescriptorSetLayouts); const std::vector ranges = GetPushConstantRanges(); - if (m_BindlessSet) + if (m_BindlessSetIndex) { const auto bindlessPool = m_GraphicsContext->GetBindlessDescriptorPool(); const auto bindlessLayout = bindlessPool->GetDescriptorSetLayout(); - layouts.push_back(bindlessLayout); + layouts[*m_BindlessSetIndex] = bindlessLayout; } VkPipelineLayoutCreateInfo info = {}; @@ -397,6 +447,8 @@ namespace Elixir::Vulkan void VulkanShader::ApplyPendingDescriptorState() { + RefreshImageDescriptorState(); + m_DescriptorSets.ApplyPendingState([this](auto&, const auto changes) { std::vector writes; @@ -408,8 +460,10 @@ namespace Elixir::Vulkan [this, &writes, binding = change.Key](const auto& value) { using Value = std::decay_t; - if constexpr (std::is_same_v>) - writes.push_back(GetWriteDescriptorSet(binding, value.get())); + if constexpr (std::is_same_v) + writes.push_back(GetWriteDescriptorSet(binding, value.Image.get())); + else if constexpr (std::is_same_v) + writes.push_back(GetWriteDescriptorSet(binding, value.Texture.get())); else writes.push_back(GetWriteDescriptorSet(binding, value)); }, @@ -427,6 +481,57 @@ namespace Elixir::Vulkan }); } + void VulkanShader::RefreshImageDescriptorState() + { + for (const auto& [binding, image] : m_Images) + { + m_DescriptorSets.Set( + binding, + DescriptorValue{ SImageDescriptorValue{ + image, + GetResourceGeneration(image), + GetDescriptorImageLayout(image) + } } + ); + } + + for (const auto& [binding, texture] : m_Textures) + { + m_DescriptorSets.Set( + binding, + DescriptorValue{ STextureDescriptorValue{ + texture, + texture ? GetResourceGeneration(texture->GetImage()) : 0, + texture ? GetDescriptorImageLayout(texture->GetImage()) + : VK_IMAGE_LAYOUT_UNDEFINED + } } + ); + } + } + + VkWriteDescriptorSet VulkanShader::GetWriteDescriptorSet( + const SShaderBinding binding, + const Image* image + ) const + { + const auto& resource = m_Resources.Resources.at(binding); + const auto vkImage = TryToGetVulkanImage(image); + + EE_CORE_ASSERT(vkImage, "Image bindings require a Vulkan image.") + + m_ImageInfoCache[binding] = { vkImage->GetVulkanDescriptorInfo() }; + + VkWriteDescriptorSet writeSet = {}; + writeSet.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writeSet.dstSet = m_DescriptorSets.GetCurrent()[resource.GetSet()]; + writeSet.dstBinding = resource.GetBinding(); + writeSet.descriptorType = Converters::GetDescriptorType(resource.GetType()); + writeSet.descriptorCount = (uint32_t)m_ImageInfoCache[binding].size(); + writeSet.pImageInfo = m_ImageInfoCache[binding].data(); + + return writeSet; + } + VkWriteDescriptorSet VulkanShader::GetWriteDescriptorSet( const SShaderBinding binding, const Texture* texture @@ -441,8 +546,8 @@ namespace Elixir::Vulkan for (auto i = 0; i < count; i++) { - const auto tex = TryToGetVulkanImage(texture); - imageInfos.push_back(tex->GetVulkanDescriptorInfo()); + const auto image = TryToGetVulkanImage(texture->GetImage().get()); + imageInfos.push_back(image->GetVulkanDescriptorInfo()); } m_ImageInfoCache[binding] = std::move(imageInfos); diff --git a/Elixir/Source/Graphics/Vulkan/VulkanShader.h b/Elixir/Source/Graphics/Vulkan/VulkanShader.h index c65928f1..3028ebab 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanShader.h +++ b/Elixir/Source/Graphics/Vulkan/VulkanShader.h @@ -29,6 +29,7 @@ namespace Elixir::Vulkan void SetConstantBuffer(const std::string& name, void* data, size_t size) override; + void BindImage(const std::string& name, const Ref& image) override; void BindTexture(const std::string& name, const Ref& texture) override; void BindTextureSet(const std::string& name, const Ref& set) override; void BindSampler(const std::string& name, const Ref& sampler) override; @@ -47,7 +48,11 @@ namespace Elixir::Vulkan void CreatePipelineLayout(); void ApplyPendingDescriptorState(); + void RefreshImageDescriptorState(); + VkWriteDescriptorSet GetWriteDescriptorSet( + SShaderBinding binding, const Image* image + ) const; VkWriteDescriptorSet GetWriteDescriptorSet( SShaderBinding binding, const Texture* texture ) const; @@ -68,8 +73,27 @@ namespace Elixir::Vulkan const Ref& buffer ) const; + struct SImageDescriptorValue + { + Ref Image; + uint64_t ResourceGeneration = 0; + VkImageLayout ImageLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + bool operator==(const SImageDescriptorValue&) const = default; + }; + + struct STextureDescriptorValue + { + Ref Texture; + uint64_t ResourceGeneration = 0; + VkImageLayout ImageLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + bool operator==(const STextureDescriptorValue&) const = default; + }; + using DescriptorValue = std::variant< - Ref, + SImageDescriptorValue, + STextureDescriptorValue, Ref, Ref, Ref, @@ -82,10 +106,9 @@ namespace Elixir::Vulkan DescriptorValue >; - bool m_BindlessSet = false; - DescriptorSetState m_DescriptorSets; std::vector m_DescriptorSetLayouts; + std::optional m_BindlessSetIndex; VkPipelineLayout m_PipelineLayout; diff --git a/Elixir/Source/Graphics/Vulkan/VulkanTexture.cpp b/Elixir/Source/Graphics/Vulkan/VulkanTexture.cpp deleted file mode 100644 index 396ebf67..00000000 --- a/Elixir/Source/Graphics/Vulkan/VulkanTexture.cpp +++ /dev/null @@ -1,94 +0,0 @@ -#include "epch.h" -#include "VulkanTexture.h" - -namespace Elixir::Vulkan -{ - /* VulkanTexture */ - - VulkanTexture::VulkanTexture( - const GraphicsContext* context, - const EImageFormat format, - const uint32_t width, - const void* data, - const std::string& path - ) : VulkanTexture(context, CreateImageInfo(format, width, data), path) {} - - VulkanTexture::VulkanTexture( - const GraphicsContext* context, - const SImageCreateInfo& info, - const std::string& path - ) : VulkanBaseImage(context, info) - { - EE_PROFILE_ZONE_SCOPED() - m_Path = path; - } - - VulkanTexture::~VulkanTexture() - { - EE_PROFILE_ZONE_SCOPED() - VulkanBaseImage::Destroy(); - } - - /* VulkanTexture2D */ - - VulkanTexture2D::VulkanTexture2D( - const GraphicsContext* context, - const EImageFormat format, - const uint32_t width, - const uint32_t height, - const void* data, - const std::string& path - ) : VulkanTexture2D(context, CreateImageInfo(format, width, height, data), path) {} - - VulkanTexture2D::VulkanTexture2D( - const GraphicsContext* context, - const SImageCreateInfo& info, - const std::string& path - ) : VulkanBaseImage(context, info) - { - EE_PROFILE_ZONE_SCOPED() - m_Path = path; - } - - VulkanTexture2D::~VulkanTexture2D() - { - EE_PROFILE_ZONE_SCOPED() - VulkanBaseImage::Destroy(); - } - - /* VulkanTexture3D */ - - VulkanTexture3D::VulkanTexture3D( - const GraphicsContext* context, - const EImageFormat format, - const uint32_t width, - const uint32_t height, - const uint32_t depth, - const void* data, - const std::string& path - ) : VulkanTexture3D(context, CreateImageInfo(format, width, height, depth, data), path) {} - - VulkanTexture3D::VulkanTexture3D( - const GraphicsContext* context, - const SImageCreateInfo& info, - const std::string& path - ) : VulkanBaseImage(context, info) - { - EE_PROFILE_ZONE_SCOPED() - m_Path = path; - - // TODO: Set a default sampler - // auto sampler = SamplerBuilder() - // .SetMagFilter(ESamplerFilter::Nearest) - // .SetMinFilter(ESamplerFilter::Nearest) - // .Build(); - - // SetSampler(sampler); - } - - VulkanTexture3D::~VulkanTexture3D() - { - EE_PROFILE_ZONE_SCOPED() - VulkanBaseImage::Destroy(); - } -} \ No newline at end of file diff --git a/Elixir/Source/Graphics/Vulkan/VulkanTexture.h b/Elixir/Source/Graphics/Vulkan/VulkanTexture.h deleted file mode 100644 index d518e421..00000000 --- a/Elixir/Source/Graphics/Vulkan/VulkanTexture.h +++ /dev/null @@ -1,74 +0,0 @@ -#pragma once - -#include -#include - -namespace Elixir::Vulkan -{ - class ELIXIR_API VulkanTexture final : public VulkanBaseImage - { - public: - VulkanTexture( - const GraphicsContext* context, - EImageFormat format, - uint32_t width, - const void* data = nullptr, - const std::string& path = "" - ); - - VulkanTexture( - const GraphicsContext* context, - const SImageCreateInfo& info, - const std::string& path = "" - ); - - ~VulkanTexture() override; - - using Image::Transition; - using Image::Copy; - using Image::CopyFrom; - }; - - class ELIXIR_API VulkanTexture2D final : public VulkanBaseImage - { - public: - VulkanTexture2D( - const GraphicsContext* context, - EImageFormat format, - uint32_t width, - uint32_t height, - const void* data = nullptr, - const std::string& path = "" - ); - - VulkanTexture2D( - const GraphicsContext* context, - const SImageCreateInfo& info, - const std::string& path = "" - ); - - ~VulkanTexture2D() override; - }; - - class ELIXIR_API VulkanTexture3D final : public VulkanBaseImage - { - public: - VulkanTexture3D( - const GraphicsContext* context, - EImageFormat format, - uint32_t width, - uint32_t height, - uint32_t depth, - const void* data = nullptr, - const std::string& path = "" - ); - - VulkanTexture3D( - const GraphicsContext* context, - const SImageCreateInfo& info, - const std::string& path = "" - ); - - ~VulkanTexture3D() override; - }; -} \ No newline at end of file diff --git a/Elixir/Source/Graphics/Vulkan/VulkanTextureSet.cpp b/Elixir/Source/Graphics/Vulkan/VulkanTextureSet.cpp index 02245cb2..cf765fb0 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanTextureSet.cpp +++ b/Elixir/Source/Graphics/Vulkan/VulkanTextureSet.cpp @@ -1,7 +1,7 @@ #include "epch.h" #include "VulkanTextureSet.h" -#include +#include #include #include #include @@ -65,4 +65,4 @@ namespace Elixir::Vulkan { return m_Pool->GetDescriptorSetLayout(); } -} \ No newline at end of file +} diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp new file mode 100644 index 00000000..20f64a77 --- /dev/null +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp @@ -0,0 +1,1617 @@ +#include "epch.h" +#include "GLTFStaticMeshLoader.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace Elixir +{ + namespace + { + constexpr auto GLTF_OPTIONS = fastgltf::Options::LoadExternalBuffers | + fastgltf::Options::GenerateMeshIndices; + + constexpr auto GLTF_CATEGORIES = fastgltf::Category::Meshes | + fastgltf::Category::Accessors | + fastgltf::Category::BufferViews | + fastgltf::Category::Buffers | + fastgltf::Category::Images | + fastgltf::Category::Textures | + fastgltf::Category::Materials | + fastgltf::Category::Nodes | + fastgltf::Category::Scenes; + + constexpr auto GLTF_EXTENSIONS = + fastgltf::Extensions::KHR_materials_emissive_strength | + fastgltf::Extensions::KHR_materials_specular | + fastgltf::Extensions::KHR_texture_transform | + fastgltf::Extensions::KHR_materials_clearcoat; + + bool IsPathWithinAssetDirectory(const std::filesystem::path& path) + { + if (path.is_absolute()) + return false; + + for (const auto& component : path.lexically_normal()) + { + if (component == "..") + return false; + } + + return true; + } + + class MaterialLoader final + { + public: + MaterialLoader( + const GraphicsContext& context, + fastgltf::Asset& asset, + std::filesystem::path directory, + std::string_view meshName + ); + + std::vector> Load(); + + private: + struct SMaterialTextures + { + Ref BaseColor; + Ref MetallicRoughness; + Ref Normal; + Ref Occlusion; + Ref Emissive; + Ref Specular; + Ref SpecularColor; + Ref ClearCoat; + Ref ClearCoatRoughness; + Ref ClearCoatNormal; + }; + + Ref DecodeImage( + const std::byte* bytes, + size_t size, + const STexture2DCreateInfo& info + ) const; + + Ref LoadImage( + size_t imageIndex, + const STexture2DCreateInfo& info + ) const; + + Ref LoadTexture( + size_t textureIndex, + const STexture2DCreateInfo& info + ); + + Ref CreateSurfaceMaterial( + const fastgltf::Material& source, + size_t materialIndex, + const SMaterialTextures& textures + ) const; + + static EMaterialBlendMode GetBlendMode(fastgltf::AlphaMode mode); + static uint32_t GetTextureCoordinateIndex(const fastgltf::TextureInfo& source); + static uint32_t AddTextureSample( + MaterialGraph& graph, + Material& material, + std::string_view parameterName, + const fastgltf::TextureInfo& source, + Nodes::ETextureSampleType sampleType = Nodes::ETextureSampleType::Color + ); + + const GraphicsContext& m_Context; + fastgltf::Asset& m_Asset; + std::filesystem::path m_Directory; + std::string_view m_MeshName; + std::unordered_map> m_TextureCache; + }; + + MaterialLoader::MaterialLoader( + const GraphicsContext& context, + fastgltf::Asset& asset, + std::filesystem::path directory, + const std::string_view meshName + ) : m_Context(context), + m_Asset(asset), + m_Directory(std::move(directory)), + m_MeshName(meshName) {} + + Ref MaterialLoader::DecodeImage( + const std::byte* bytes, + const size_t size, + const STexture2DCreateInfo& info + ) const + { + int width = 0; + int height = 0; + int channels = 0; + + stbi_uc* pixels = stbi_load_from_memory( + reinterpret_cast(bytes), + (int)size, + &width, + &height, + &channels, + STBI_rgb_alpha + ); + + if (!pixels) + { + EE_CORE_WARN( + "Failed to decode embedded glTF image: {}.", + stbi_failure_reason() + ) + return nullptr; + } + + auto createInfo = info; + createInfo.InitialData = pixels; + createInfo.Width = width; + createInfo.Height = height; + + const auto texture = Texture2D::Create(&m_Context, createInfo); + + stbi_image_free(pixels); + + return texture; + } + + Ref MaterialLoader::LoadImage( + const size_t imageIndex, + const STexture2DCreateInfo& info + ) const + { + if (imageIndex >= m_Asset.images.size()) + { + EE_CORE_WARN("glTF material references an invalid image index.") + return nullptr; + } + + Ref texture; + auto& image = m_Asset.images[imageIndex]; + + std::visit( + fastgltf::visitor{ + [](const auto&) {}, + [&](const fastgltf::sources::URI& source) + { + const auto relativePath = source.uri.fspath(); + if (!IsPathWithinAssetDirectory(relativePath)) + { + EE_CORE_WARN("glTF image URI escapes the asset directory.") + return; + } + + const auto path = m_Directory / relativePath; + texture = TextureLoader::Load(path, info); + }, + [&](const fastgltf::sources::Array& source) + { + texture = DecodeImage( + source.bytes.data(), + source.bytes.size(), + info + ); + }, + [&](const fastgltf::sources::Vector& source) + { + texture = DecodeImage( + source.bytes.data(), + source.bytes.size(), + info + ); + }, + [&](const fastgltf::sources::BufferView& viewSource) + { + if (viewSource.bufferViewIndex >= m_Asset.bufferViews.size()) + { + EE_CORE_WARN("glTF image references an invalid buffer view.") + return; + } + + const auto& view = m_Asset.bufferViews[viewSource.bufferViewIndex]; + if (view.bufferIndex >= m_Asset.buffers.size()) + { + EE_CORE_WARN("glTF image references an invalid buffer.") + return; + } + + auto& buffer = m_Asset.buffers[view.bufferIndex]; + std::visit( + fastgltf::visitor{ + [](const auto&) {}, + [&](const fastgltf::sources::Array& source) + { + texture = DecodeImage( + source.bytes.data() + view.byteOffset, + view.byteLength, + info + ); + }, + [&](const fastgltf::sources::Vector& source) + { + texture = DecodeImage( + source.bytes.data() + view.byteOffset, + view.byteLength, + info + ); + } + }, + buffer.data + ); + } + }, + image.data + ); + + return texture; + } + + Ref MaterialLoader::LoadTexture( + const size_t textureIndex, + const STexture2DCreateInfo& info + ) + { + const uint64_t cacheKey = + (uint64_t(textureIndex) << 3) | + (info.Format == EImageFormat::R8G8B8A8_SRGB ? 1ull : 0ull) | + (static_cast(info.MipmapMode) << 1); + + const auto existing = m_TextureCache.find(cacheKey); + if (existing != m_TextureCache.end()) + { + return existing->second; + } + + if (textureIndex >= m_Asset.textures.size()) + { + EE_CORE_WARN("glTF material references an invalid texture index.") + return nullptr; + } + + const auto& texture = m_Asset.textures[textureIndex]; + if (!texture.imageIndex) + { + EE_CORE_WARN("glTF texture does not reference an image.") + return nullptr; + } + + const auto result = LoadImage( + *texture.imageIndex, + info + ); + m_TextureCache.emplace(cacheKey, result); + + return result; + } + + EMaterialBlendMode MaterialLoader::GetBlendMode(const fastgltf::AlphaMode mode) + { + using namespace Materials; + + switch (mode) + { + case fastgltf::AlphaMode::Mask: + return EMaterialBlendMode::Masked; + + case fastgltf::AlphaMode::Blend: + return EMaterialBlendMode::Translucent; + + case fastgltf::AlphaMode::Opaque: + default: + return EMaterialBlendMode::Opaque; + } + } + + uint32_t MaterialLoader::GetTextureCoordinateIndex(const fastgltf::TextureInfo& source) + { + const size_t index = source.transform && source.transform->texCoordIndex + ? *source.transform->texCoordIndex + : source.texCoordIndex; + + if (index <= 1) + return static_cast(index); + + EE_CORE_WARN( + "glTF texture coordinate channel {} is unsupported; using TEXCOORD_0.", + index + ) + return 0; + } + + uint32_t MaterialLoader::AddTextureSample( + MaterialGraph& graph, + Material& material, + const std::string_view parameterName, + const fastgltf::TextureInfo& source, + const Nodes::ETextureSampleType sampleType + ) + { + using namespace Materials; + using namespace Nodes; + + const auto sample = graph.AddNode( + std::string(parameterName), + sampleType + ); + const auto texCoord = graph.AddNode( + GetTextureCoordinateIndex(source) + ); + + if (!source.transform) + { + graph.Connect(texCoord, sample, 0); + return sample; + } + + const std::string scaleParameter = std::string(parameterName) + "UVScale"; + const std::string offsetParameter = std::string(parameterName) + "UVOffset"; + const std::string rotationParameter = std::string(parameterName) + "UVRotation"; + + EE_CORE_ASSERT(material.DefineParameter(scaleParameter, { + .Kind = EMaterialParameterKind::Value, + .ValueType = EMaterialValueType::Float2, + .DefaultValue = SMaterialParameter::MakeVector({ + source.transform->uvScale.x(), + source.transform->uvScale.y(), + 0.0f, + 0.0f, + }), + }), "Could not define texture UV scale parameter.") + + EE_CORE_ASSERT(material.DefineParameter(offsetParameter, { + .Kind = EMaterialParameterKind::Value, + .ValueType = EMaterialValueType::Float2, + .DefaultValue = SMaterialParameter::MakeVector({ + source.transform->uvOffset.x(), + source.transform->uvOffset.y(), + 0.0f, + 0.0f, + }), + }), "Could not define texture UV offset parameter.") + + EE_CORE_ASSERT(material.DefineParameter(rotationParameter, { + .Kind = EMaterialParameterKind::Value, + .ValueType = EMaterialValueType::Float, + .DefaultValue = SMaterialParameter::MakeScalar(source.transform->rotation), + }), "Could not define texture UV rotation parameter.") + + const auto scale = graph.AddNode( + scaleParameter, + EMaterialValueType::Float2 + ); + const auto offset = graph.AddNode( + offsetParameter, + EMaterialValueType::Float2 + ); + const auto rotation = graph.AddNode( + rotationParameter, + EMaterialValueType::Float + ); + const auto scaled = graph.AddNode(); + graph.Connect(texCoord, scaled, 0); + graph.Connect(scale, scaled, 1); + + const auto sine = graph.AddNode(); + const auto cosine = graph.AddNode(); + graph.Connect(rotation, sine, 0); + graph.Connect(rotation, cosine, 0); + + const auto zero = graph.AddNode( + glm::vec4(0.0f), + EMaterialValueType::Float + ); + const auto negativeSine = graph.AddNode(); + graph.Connect(zero, negativeSine, 0); + graph.Connect(sine, negativeSine, 1); + + const auto firstRow = graph.AddNode(); + graph.Connect(cosine, firstRow, 0); + graph.Connect(negativeSine, firstRow, 1); + + const auto secondRow = graph.AddNode(); + graph.Connect(sine, secondRow, 0); + graph.Connect(cosine, secondRow, 1); + + const auto rotatedX = graph.AddNode(); + graph.Connect(scaled, rotatedX, 0); + graph.Connect(firstRow, rotatedX, 1); + + const auto rotatedY = graph.AddNode(); + graph.Connect(scaled, rotatedY, 0); + graph.Connect(secondRow, rotatedY, 1); + + const auto rotated = graph.AddNode(); + graph.Connect(rotatedX, rotated, 0); + graph.Connect(rotatedY, rotated, 1); + + const auto transformed = graph.AddNode(); + graph.Connect(rotated, transformed, 0); + graph.Connect(offset, transformed, 1); + graph.Connect(transformed, sample, 0); + + return sample; + } + + Ref MaterialLoader::CreateSurfaceMaterial( + const fastgltf::Material& source, + const size_t materialIndex, + const SMaterialTextures& textures + ) const + { + using namespace Materials; + using namespace Nodes; + + const std::string name = source.name.empty() + ? std::string(m_MeshName) + ".Material." + std::to_string(materialIndex) + : std::string(source.name); + + const auto material = CreateRef(name, EMaterialUsage::Surface); + material->SetBlendMode(GetBlendMode(source.alphaMode)); + material->SetDoubleSided(source.doubleSided); + + if (source.clearcoat) + material->SetShadingModel(EMaterialShadingModel::ClearCoat); + + if (source.alphaMode == fastgltf::AlphaMode::Mask) + material->SetAlphaCutoff(source.alphaCutoff); + + const auto& pbr = source.pbrData; + const auto baseColorFactor = glm::vec4( + pbr.baseColorFactor.x(), + pbr.baseColorFactor.y(), + pbr.baseColorFactor.z(), + pbr.baseColorFactor.w() + ); + const auto emissiveFactor =glm::vec4( + source.emissiveFactor.x(), + source.emissiveFactor.y(), + source.emissiveFactor.z(), + 0.0f + ) * source.emissiveStrength; + + EE_CORE_ASSERT(material->DefineParameter("BaseColorFactor", { + .Kind = EMaterialParameterKind::Value, + .ValueType = EMaterialValueType::Float4, + .DefaultValue = SMaterialParameter::MakeVector(baseColorFactor), + }), "Could not define BaseColorFactor.") + + EE_CORE_ASSERT(material->DefineParameter("MetallicFactor", { + .Kind = EMaterialParameterKind::Value, + .ValueType = EMaterialValueType::Float, + .DefaultValue = SMaterialParameter::MakeScalar(pbr.metallicFactor), + }), "Could not define MetallicFactor.") + + EE_CORE_ASSERT(material->DefineParameter("RoughnessFactor", { + .Kind = EMaterialParameterKind::Value, + .ValueType = EMaterialValueType::Float, + .DefaultValue = SMaterialParameter::MakeScalar(pbr.roughnessFactor), + }), "Could not define RoughnessFactor.") + + EE_CORE_ASSERT(material->DefineParameter("EmissiveFactor", { + .Kind = EMaterialParameterKind::Value, + .ValueType = EMaterialValueType::Float3, + .DefaultValue = SMaterialParameter::MakeVector(emissiveFactor), + }), "Could not define EmissiveFactor.") + + EE_CORE_ASSERT(material->DefineParameter("NormalScale", { + .Kind = EMaterialParameterKind::Value, + .ValueType = EMaterialValueType::Float, + .DefaultValue = SMaterialParameter::MakeScalar( + source.normalTexture ? source.normalTexture->scale : 1.0f), + }), "Could not define NormalScale.") + + EE_CORE_ASSERT(material->DefineParameter("OcclusionStrength", { + .Kind = EMaterialParameterKind::Value, + .ValueType = EMaterialValueType::Float, + .DefaultValue = SMaterialParameter::MakeScalar( + source.occlusionTexture ? source.occlusionTexture->strength : 1.0f), + }), "Could not define OcclusionStrength.") + + if (source.specular) + { + EE_CORE_ASSERT(material->DefineParameter("SpecularFactor", { + .Kind = EMaterialParameterKind::Value, + .ValueType = EMaterialValueType::Float, + .DefaultValue = SMaterialParameter::MakeScalar( + source.specular->specularFactor + ), + }), "Could not define SpecularFactor.") + + const auto& colorFactor = source.specular->specularColorFactor; + EE_CORE_ASSERT(material->DefineParameter("SpecularColorFactor", { + .Kind = EMaterialParameterKind::Value, + .ValueType = EMaterialValueType::Float3, + .DefaultValue = SMaterialParameter::MakeVector({ + colorFactor.x(), + colorFactor.y(), + colorFactor.z(), + 0.0f + }), + }), "Could not define SpecularColorFactor.") + } + + if (source.clearcoat) + { + EE_CORE_ASSERT(material->DefineParameter("ClearCoatFactor", { + .Kind = EMaterialParameterKind::Value, + .ValueType = EMaterialValueType::Float, + .DefaultValue = SMaterialParameter::MakeScalar( + source.clearcoat->clearcoatFactor + ), + }), "Could not define ClearCoatFactor.") + + EE_CORE_ASSERT(material->DefineParameter("ClearCoatRoughnessFactor", { + .Kind = EMaterialParameterKind::Value, + .ValueType = EMaterialValueType::Float, + .DefaultValue = SMaterialParameter::MakeScalar( + source.clearcoat->clearcoatRoughnessFactor + ), + }), "Could not define ClearCoatRoughnessFactor.") + + EE_CORE_ASSERT(material->DefineParameter("ClearCoatNormalScale", { + .Kind = EMaterialParameterKind::Value, + .ValueType = EMaterialValueType::Float, + .DefaultValue = SMaterialParameter::MakeScalar( + source.clearcoat->clearcoatNormalTexture + ? source.clearcoat->clearcoatNormalTexture->scale + : 1.0f + ), + }), "Could not define ClearCoatNormalScale.") + } + + MaterialGraph graph; + + auto baseColor = graph.AddNode( + "BaseColorFactor", + EMaterialValueType::Float4 + ); + + if (textures.BaseColor) + { + EE_CORE_ASSERT(material->DefineParameter("BaseColorTexture", { + .Kind = EMaterialParameterKind::Texture, + .DefaultValue = SMaterialParameter::MakeTexture(textures.BaseColor), + }), "Could not define BaseColorTexture."); + + const auto texture = AddTextureSample( + graph, + *material, + "BaseColorTexture", + *pbr.baseColorTexture + ); + const auto multiply = graph.AddNode(); + graph.Connect(baseColor, multiply, 0); + graph.Connect(texture, multiply, 1); + baseColor = multiply; + } + + auto metallic = graph.AddNode( + "MetallicFactor", + EMaterialValueType::Float + ); + + auto roughness = graph.AddNode( + "RoughnessFactor", + EMaterialValueType::Float + ); + + if (textures.MetallicRoughness) + { + EE_CORE_ASSERT(material->DefineParameter("MetallicRoughnessTexture", { + .Kind = EMaterialParameterKind::Texture, + .DefaultValue = SMaterialParameter::MakeTexture(textures.MetallicRoughness), + }), "Could not define MetallicRoughnessTexture."); + + const auto sample = AddTextureSample( + graph, + *material, + "MetallicRoughnessTexture", + *pbr.metallicRoughnessTexture, + ETextureSampleType::LinearColor + ); + + const auto textureRoughness = graph.AddNode(1); + graph.Connect(sample, textureRoughness, 0); + + const auto textureMetallic = graph.AddNode(2); + graph.Connect(sample, textureMetallic, 0); + + const auto roughnessMultiply = graph.AddNode(); + graph.Connect(roughness, roughnessMultiply, 0); + graph.Connect(textureRoughness, roughnessMultiply, 1); + roughness = roughnessMultiply; + + const auto metallicMultiply = graph.AddNode(); + graph.Connect(metallic, metallicMultiply, 0); + graph.Connect(textureMetallic, metallicMultiply, 1); + metallic = metallicMultiply; + } + + auto emissive = graph.AddNode( + "EmissiveFactor", + EMaterialValueType::Float3 + ); + + if (textures.Emissive) + { + EE_CORE_ASSERT(material->DefineParameter("EmissiveTexture", { + .Kind = EMaterialParameterKind::Texture, + .DefaultValue = SMaterialParameter::MakeTexture(textures.Emissive), + }), "Could not define EmissiveTexture."); + + const auto sample = AddTextureSample( + graph, + *material, + "EmissiveTexture", + *source.emissiveTexture + ); + const auto multiply = graph.AddNode(); + graph.Connect(emissive, multiply, 0); + graph.Connect(sample, multiply, 1); + emissive = multiply; + } + + const auto opacity = graph.AddNode(3); + graph.Connect(baseColor, opacity, 0); + + graph.SetChannel(EMaterialChannel::BaseColor, baseColor); + graph.SetChannel(EMaterialChannel::Metallic, metallic); + graph.SetChannel( EMaterialChannel::Roughness, roughness); + graph.SetChannel( EMaterialChannel::Opacity, opacity); + graph.SetChannel(EMaterialChannel::Emissive, emissive); + + if (textures.Normal) + { + EE_CORE_ASSERT(material->DefineParameter("NormalTexture", { + .Kind = EMaterialParameterKind::Texture, + .DefaultValue = SMaterialParameter::MakeTexture(textures.Normal), + }), "Could not define NormalTexture."); + + const auto sample = AddTextureSample( + graph, + *material, + "NormalTexture", + *source.normalTexture, + ETextureSampleType::Normal + ); + + const auto scale = graph.AddNode( + "NormalScale", + EMaterialValueType::Float + ); + + const auto scaledNormal = graph.AddNode(); + graph.Connect(sample, scaledNormal, 0); + graph.Connect(scale, scaledNormal, 1); + + graph.SetChannel( + source.clearcoat + ? EMaterialChannel::ClearCoatBottomNormal + : EMaterialChannel::Normal, + scaledNormal + ); + } + + if (textures.Occlusion) + { + EE_CORE_ASSERT(material->DefineParameter("OcclusionTexture", { + .Kind = EMaterialParameterKind::Texture, + .DefaultValue = SMaterialParameter::MakeTexture(textures.Occlusion), + }), "Could not define OcclusionTexture."); + + const auto sample = AddTextureSample( + graph, + *material, + "OcclusionTexture", + *source.occlusionTexture, + ETextureSampleType::LinearColor + ); + const auto red = graph.AddNode(0); + graph.Connect(sample, red, 0); + + const auto strength = graph.AddNode( + "OcclusionStrength", + EMaterialValueType::Float + ); + + const auto one = graph.AddNode( + glm::vec4{ 1.0f }, + EMaterialValueType::Float + ); + + const auto occlusion = graph.AddNode(); + graph.Connect(one, occlusion, 0); + graph.Connect(red, occlusion, 1); + graph.Connect(strength, occlusion, 2); + + graph.SetChannel(EMaterialChannel::AmbientOcclusion, occlusion); + } + + if (source.specular) + { + auto specular = graph.AddNode( + "SpecularFactor", + EMaterialValueType::Float + ); + + if (textures.Specular) + { + EE_CORE_ASSERT(material->DefineParameter("SpecularTexture", { + .Kind = EMaterialParameterKind::Texture, + .DefaultValue = SMaterialParameter::MakeTexture(textures.Specular), + }), "Could not define SpecularTexture."); + + const auto sample = AddTextureSample( + graph, + *material, + "SpecularTexture", + *source.specular->specularTexture, + ETextureSampleType::LinearColor + ); + const auto alpha = graph.AddNode(3); + graph.Connect(sample, alpha, 0); + + const auto multiply = graph.AddNode(); + graph.Connect(specular, multiply, 0); + graph.Connect(alpha, multiply, 1); + specular = multiply; + } + + auto specularColor = graph.AddNode( + "SpecularColorFactor", + EMaterialValueType::Float3 + ); + + if (textures.SpecularColor) + { + EE_CORE_ASSERT(material->DefineParameter("SpecularColorTexture", { + .Kind = EMaterialParameterKind::Texture, + .DefaultValue = SMaterialParameter::MakeTexture(textures.SpecularColor), + }), "Could not define SpecularColorTexture."); + + const auto sample = AddTextureSample( + graph, + *material, + "SpecularColorTexture", + *source.specular->specularColorTexture + ); + + const auto multiply = graph.AddNode(); + graph.Connect(specularColor, multiply, 0); + graph.Connect(sample, multiply, 1); + specularColor = multiply; + } + + graph.SetChannel(EMaterialChannel::Specular, specular); + graph.SetChannel(EMaterialChannel::SpecularColor, specularColor); + } + + if (source.clearcoat) + { + auto clearCoat = graph.AddNode( + "ClearCoatFactor", + EMaterialValueType::Float + ); + + if (textures.ClearCoat) + { + EE_CORE_ASSERT(material->DefineParameter("ClearCoatTexture", { + .Kind = EMaterialParameterKind::Texture, + .DefaultValue = SMaterialParameter::MakeTexture(textures.ClearCoat), + }), "Could not define ClearCoatTexture."); + + const auto sample = AddTextureSample( + graph, + *material, + "ClearCoatTexture", + *source.clearcoat->clearcoatTexture, + ETextureSampleType::LinearColor + ); + const auto red = graph.AddNode(0); + graph.Connect(sample, red, 0); + + const auto multiply = graph.AddNode(); + graph.Connect(clearCoat, multiply, 0); + graph.Connect(red, multiply, 1); + clearCoat = multiply; + } + + auto clearCoatRoughness = graph.AddNode( + "ClearCoatRoughnessFactor", + EMaterialValueType::Float + ); + + if (textures.ClearCoatRoughness) + { + EE_CORE_ASSERT(material->DefineParameter("ClearCoatRoughnessTexture", { + .Kind = EMaterialParameterKind::Texture, + .DefaultValue = SMaterialParameter::MakeTexture( + textures.ClearCoatRoughness + ), + }), "Could not define ClearCoatRoughnessTexture."); + + const auto sample = AddTextureSample( + graph, + *material, + "ClearCoatRoughnessTexture", + *source.clearcoat->clearcoatRoughnessTexture, + ETextureSampleType::LinearColor + ); + const auto green = graph.AddNode(1); + graph.Connect(sample, green, 0); + + const auto multiply = graph.AddNode(); + graph.Connect(clearCoatRoughness, multiply, 0); + graph.Connect(green, multiply, 1); + clearCoatRoughness = multiply; + } + + graph.SetChannel(EMaterialChannel::ClearCoat, clearCoat); + graph.SetChannel(EMaterialChannel::ClearCoatRoughness, clearCoatRoughness); + + // In glTF, clearCoatNormalTexture belongs to the external + // clear-coat layer. + if (textures.ClearCoatNormal) + { + EE_CORE_ASSERT(material->DefineParameter("ClearCoatNormalTexture", { + .Kind = EMaterialParameterKind::Texture, + .DefaultValue = SMaterialParameter::MakeTexture( + textures.ClearCoatNormal + ), + }), "Could not define ClearCoatNormalTexture."); + + const auto sample = AddTextureSample( + graph, + *material, + "ClearCoatNormalTexture", + *source.clearcoat->clearcoatNormalTexture, + ETextureSampleType::Normal + ); + + const auto scale = graph.AddNode( + "ClearCoatNormalScale", + EMaterialValueType::Float + ); + + const auto scaledNormal = graph.AddNode(); + graph.Connect(sample, scaledNormal, 0); + graph.Connect(scale, scaledNormal, 1); + + graph.SetChannel(EMaterialChannel::Normal, scaledNormal); + } + } + + material->SetGraph(std::move(graph)); + return material; + } + + std::vector> MaterialLoader::Load() + { + // Index zero is the fallback for primitives without a glTF material. + std::vector> materials; + materials.push_back(CreateDefaultMaterials()[0]); + + materials.reserve(m_Asset.materials.size() + 1); + + for (size_t i = 0; i < m_Asset.materials.size(); ++i) + { + const auto& source = m_Asset.materials[i]; + SMaterialTextures textures; + + if (source.pbrData.baseColorTexture) + { + STexture2DCreateInfo info; + info.Format = EImageFormat::R8G8B8A8_SRGB; + textures.BaseColor = LoadTexture( + source.pbrData.baseColorTexture->textureIndex, + info + ); + } + + if (source.pbrData.metallicRoughnessTexture) + { + STexture2DCreateInfo info; + info.Format = EImageFormat::R8G8B8A8_UNORM; + textures.MetallicRoughness = LoadTexture( + source.pbrData.metallicRoughnessTexture->textureIndex, + info + ); + } + + if (source.normalTexture) + { + STexture2DCreateInfo info; + info.Format = EImageFormat::R8G8B8A8_UNORM; + info.MipmapMode = EImageMipmapMode::NormalMap; + textures.Normal = LoadTexture( + source.normalTexture->textureIndex, + info + ); + } + + if (source.occlusionTexture) + { + STexture2DCreateInfo info; + info.Format = EImageFormat::R8G8B8A8_UNORM; + textures.Occlusion = LoadTexture( + source.occlusionTexture->textureIndex, + info + ); + } + + if (source.emissiveTexture) + { + STexture2DCreateInfo info; + info.Format = EImageFormat::R8G8B8A8_SRGB; + textures.Emissive = LoadTexture( + source.emissiveTexture->textureIndex, + info + ); + } + + if (source.specular) + { + if (source.specular->specularTexture) + { + STexture2DCreateInfo info; + info.Format = EImageFormat::R8G8B8A8_UNORM; + textures.Specular = LoadTexture( + source.specular->specularTexture->textureIndex, + info + ); + } + + if (source.specular->specularColorTexture) + { + STexture2DCreateInfo info; + info.Format = EImageFormat::R8G8B8A8_SRGB; + textures.SpecularColor = LoadTexture( + source.specular->specularColorTexture->textureIndex, + info + ); + } + } + + if (source.clearcoat) + { + if (source.clearcoat->clearcoatTexture) + { + STexture2DCreateInfo info; + info.Format = EImageFormat::R8G8B8A8_UNORM; + textures.ClearCoat = LoadTexture( + source.clearcoat->clearcoatTexture->textureIndex, + info + ); + } + + if (source.clearcoat->clearcoatRoughnessTexture) + { + STexture2DCreateInfo info; + info.Format = EImageFormat::R8G8B8A8_UNORM; + textures.ClearCoatRoughness = LoadTexture( + source.clearcoat->clearcoatRoughnessTexture->textureIndex, + info + ); + } + + if (source.clearcoat->clearcoatNormalTexture) + { + STexture2DCreateInfo info; + info.Format = EImageFormat::R8G8B8A8_UNORM; + info.MipmapMode = EImageMipmapMode::NormalMap; + textures.ClearCoatNormal = LoadTexture( + source.clearcoat->clearcoatNormalTexture->textureIndex, + info + ); + } + } + + materials.push_back(CreateSurfaceMaterial(source, i, textures)); + } + + return materials; + } + + class Loader final + { + public: + Loader(const GraphicsContext& context, std::filesystem::path path); + + std::optional Load(); + + private: + bool ParseAsset(); + bool LoadGeometry(); + void LoadPrimitive( + const fastgltf::Primitive& primitive, + const glm::mat4& localTransform + ); + void LoadNodeMesh( + const fastgltf::Node& node, + const glm::mat4& localTransform + ); + + static SStaticMeshBounds GetBounds( + const std::vector& vertices + ); + static void ExpandBounds( + SStaticMeshBounds& target, + const SStaticMeshBounds& source + ); + static glm::mat4 ToGlmMatrix(const fastgltf::math::fmat4x4& source); + static bool ApplyLocalTransform( + std::vector& vertices, + std::vector& indices, + const glm::mat4& localTransform + ); + static void GenerateTangents( + std::vector& vertices, + const std::vector& indices + ); + + const GraphicsContext& m_Context; + std::filesystem::path m_Path; + std::optional m_Asset; + SStaticMeshData m_Mesh; + bool m_HasBounds = false; + }; + + SStaticMeshBounds Loader::GetBounds(const std::vector& vertices) + { + SStaticMeshBounds bounds{ + .Min = glm::vec3(std::numeric_limits::max()), + .Max = glm::vec3(std::numeric_limits::lowest()), + }; + + for (const auto& vertex : vertices) + { + bounds.Min = glm::min(bounds.Min, vertex.Position); + bounds.Max = glm::max(bounds.Max, vertex.Position); + } + + return bounds; + } + + void Loader::ExpandBounds( + SStaticMeshBounds& target, + const SStaticMeshBounds& source + ) + { + target.Min = glm::min(target.Min, source.Min); + target.Max = glm::max(target.Max, source.Max); + } + + glm::mat4 Loader::ToGlmMatrix(const fastgltf::math::fmat4x4& source) + { + glm::mat4 result{ 1.0f }; + for (size_t column = 0; column < 4; ++column) + for (size_t row = 0; row < 4; ++row) + result[column][row] = source.col(column)[row]; + + return result; + } + + namespace + { + constexpr float NORMAL_EPSILON_SQUARED = 1.0e-12f; + + glm::vec3 NormalizeOrFallback( + const glm::vec3 value, + const glm::vec3 fallback + ) + { + if (glm::dot(value, value) <= NORMAL_EPSILON_SQUARED) + return fallback; + + return glm::normalize(value); + } + + glm::vec3 GetFallbackTangent(const glm::vec3& normal) + { + const glm::vec3 axis = std::abs(normal.z) < 0.999f + ? glm::vec3(0.0f, 0.0f, 1.0f) + : glm::vec3(0.0f, 1.0f, 0.0f); + return NormalizeOrFallback(glm::cross(axis, normal), glm::vec3(1.0f, 0.0f, 0.0f)); + } + } + + void Loader::GenerateTangents( + std::vector& vertices, + const std::vector& indices + ) + { + std::vector tangentSums(vertices.size()); + std::vector bitangentSums(vertices.size()); + + for (size_t index = 0; index < indices.size(); index += 3) + { + const auto first = indices[index]; + const auto second = indices[index + 1]; + const auto third = indices[index + 2]; + + const auto& firstVertex = vertices[first]; + const auto& secondVertex = vertices[second]; + const auto& thirdVertex = vertices[third]; + const glm::vec3 positionDelta1 = secondVertex.Position - firstVertex.Position; + const glm::vec3 positionDelta2 = thirdVertex.Position - firstVertex.Position; + const glm::vec2 texCoordDelta1 = secondVertex.TexCoord - firstVertex.TexCoord; + const glm::vec2 texCoordDelta2 = thirdVertex.TexCoord - firstVertex.TexCoord; + const float determinant = texCoordDelta1.x * texCoordDelta2.y - + texCoordDelta1.y * texCoordDelta2.x; + + if (std::abs(determinant) <= NORMAL_EPSILON_SQUARED) + continue; + + const float inverseDeterminant = 1.0f / determinant; + const glm::vec3 tangent = (positionDelta1 * texCoordDelta2.y - + positionDelta2 * texCoordDelta1.y) * inverseDeterminant; + const glm::vec3 bitangent = (positionDelta2 * texCoordDelta1.x - + positionDelta1 * texCoordDelta2.x) * inverseDeterminant; + + for (const auto vertexIndex : { first, second, third }) + { + tangentSums[vertexIndex] += tangent; + bitangentSums[vertexIndex] += bitangent; + } + } + + for (size_t index = 0; index < vertices.size(); ++index) + { + auto& vertex = vertices[index]; + const glm::vec3 normal = NormalizeOrFallback(vertex.Normal, glm::vec3(0.0f, 1.0f, 0.0f)); + glm::vec3 tangent = tangentSums[index] - normal * glm::dot(normal, tangentSums[index]); + tangent = NormalizeOrFallback(tangent, GetFallbackTangent(normal)); + const float handedness = glm::dot(glm::cross(normal, tangent), bitangentSums[index]) < 0.0f + ? -1.0f + : 1.0f; + vertex.Tangent = { tangent, handedness }; + } + } + + bool Loader::ApplyLocalTransform( + std::vector& vertices, + std::vector& indices, + const glm::mat4& localTransform + ) + { + const glm::mat3 linearTransform{ localTransform }; + const float determinant = glm::determinant(linearTransform); + if (determinant == 0.0f) + { + EE_CORE_WARN("glTF node has a singular local transform; skipping its primitive.") + return false; + } + + const glm::mat3 normalTransform = glm::transpose(glm::inverse(linearTransform)); + const float handedness = determinant < 0.0f ? -1.0f : 1.0f; + + for (auto& vertex : vertices) + { + vertex.Position = glm::vec3(localTransform * glm::vec4(vertex.Position, 1.0f)); + vertex.Normal = NormalizeOrFallback( + normalTransform * vertex.Normal, + glm::vec3(0.0f, 1.0f, 0.0f) + ); + + glm::vec3 tangent = linearTransform * glm::vec3(vertex.Tangent); + tangent -= vertex.Normal * glm::dot(vertex.Normal, tangent); + vertex.Tangent = { + NormalizeOrFallback(tangent, GetFallbackTangent(vertex.Normal)), + vertex.Tangent.w * handedness + }; + } + + if (handedness < 0.0f) + { + for (size_t index = 0; index < indices.size(); index += 3) + std::swap(indices[index + 1], indices[index + 2]); + } + + return true; + } + + void Loader::LoadPrimitive( + const fastgltf::Primitive& primitive, + const glm::mat4& localTransform + ) + { + const auto& asset = *m_Asset; + auto& mesh = m_Mesh; + + if (primitive.type != fastgltf::PrimitiveType::Triangles) + { + EE_CORE_WARN( + "Skipping primitive in mesh '{}': only triangles are supported.", + mesh.Name + ) + return; + } + + const auto position = primitive.findAttribute("POSITION"); + + if (position == primitive.attributes.end() || + position->accessorIndex >= asset.accessors.size()) + { + EE_CORE_WARN("Skipping primitive in mesh '{}': POSITION is missing.", mesh.Name) + return; + } + + const auto& positionAccessor = asset.accessors[position->accessorIndex]; + if (positionAccessor.count > std::numeric_limits::max()) + { + EE_CORE_WARN( + "Skipping primitive in mesh '{}': it exceeds the vertex limit.", + mesh.Name + ) + return; + } + + std::vector vertices(positionAccessor.count); + fastgltf::iterateAccessorWithIndex( + asset, + positionAccessor, + [&vertices](const auto value, const size_t index) + { + vertices[index].Position = { value.x(), value.y(), value.z() }; + } + ); + + const auto loadAttribute = [&]( + const std::string_view name, + auto&& assign + ) + { + const auto attribute = primitive.findAttribute(name); + + if (attribute == primitive.attributes.end() || + attribute->accessorIndex >= asset.accessors.size()) + { + return; + } + + const auto& accessor = asset.accessors[attribute->accessorIndex]; + if (accessor.count != vertices.size()) + { + EE_CORE_WARN( + "Ignoring {} in primitive of mesh '{}': its count differs from POSITION.", + name, + mesh.Name + ) + return; + } + + fastgltf::iterateAccessorWithIndex( + asset, + accessor, + [&assign](const auto value, const size_t index) + { + assign(value, index); + } + ); + }; + + loadAttribute.operator()( + "NORMAL", + [&vertices](const auto value, const size_t index) + { + vertices[index].Normal = { value.x(), value.y(), value.z() }; + } + ); + + loadAttribute.operator()( + "TANGENT", + [&vertices](const auto value, const size_t index) + { + vertices[index].Tangent = { value.x(), value.y(), value.z(), value.w() }; + } + ); + + loadAttribute.operator()( + "TEXCOORD_0", + [&vertices](const auto value, const size_t index) + { + vertices[index].TexCoord = { value.x(), value.y() }; + } + ); + + loadAttribute.operator()( + "TEXCOORD_1", + [&vertices](const auto value, const size_t index) + { + vertices[index].TexCoord1 = { value.x(), value.y() }; + } + ); + + std::vector indices; + + if (primitive.indicesAccessor && + *primitive.indicesAccessor < asset.accessors.size()) + { + const auto& accessor = asset.accessors[*primitive.indicesAccessor]; + + if (accessor.count > std::numeric_limits::max()) + { + EE_CORE_WARN( + "Skipping primitive in mesh '{}': it exceeds the index limit.", + mesh.Name + ) + return; + } + + indices.resize(accessor.count); + fastgltf::iterateAccessorWithIndex( + asset, + accessor, + [&indices](const uint32_t value, const size_t index) + { + indices[index] = value; + } + ); + } + else + { + indices.resize(vertices.size()); + std::iota(indices.begin(), indices.end(), 0u); + } + + if (vertices.empty() || indices.empty()) + { + EE_CORE_WARN("Skipping empty primitive in mesh '{}'.", mesh.Name) + return; + } + + if (indices.size() % 3 != 0 || std::ranges::any_of(indices, [&vertices](const uint32_t index) + { + return index >= vertices.size(); + })) + { + EE_CORE_WARN("Skipping invalid indexed primitive in mesh '{}'.", mesh.Name) + return; + } + + if (primitive.findAttribute("TANGENT") == primitive.attributes.end()) + GenerateTangents(vertices, indices); + + if (!ApplyLocalTransform(vertices, indices, localTransform)) + return; + + SStaticMeshSection section; + section.FirstIndex = (uint32_t)mesh.Indices.size(); + section.IndexCount = (uint32_t)indices.size(); + section.VertexOffset = (uint32_t)mesh.Vertices.size(); + section.MaterialIndex = + primitive.materialIndex && *primitive.materialIndex < asset.materials.size() + ? uint32_t(*primitive.materialIndex + 1) + : 0; + section.LocalBounds = GetBounds(vertices); + + mesh.Vertices.insert( + mesh.Vertices.end(), + std::make_move_iterator(vertices.begin()), + std::make_move_iterator(vertices.end()) + ); + + mesh.Indices.insert( + mesh.Indices.end(), + std::make_move_iterator(indices.begin()), + std::make_move_iterator(indices.end()) + ); + + if (!m_HasBounds) + { + mesh.LocalBounds = section.LocalBounds; + m_HasBounds = true; + } + else + { + ExpandBounds(mesh.LocalBounds, section.LocalBounds); + } + + mesh.Sections.push_back(std::move(section)); + } + + void Loader::LoadNodeMesh( + const fastgltf::Node& node, + const glm::mat4& localTransform + ) + { + const auto& asset = *m_Asset; + + if (!node.meshIndex) + return; + + if (*node.meshIndex >= asset.meshes.size()) + { + EE_CORE_WARN("glTF node references an invalid mesh index.") + return; + } + + for (const auto& primitive : asset.meshes[*node.meshIndex].primitives) + LoadPrimitive(primitive, localTransform); + } + + Loader::Loader(const GraphicsContext& context, std::filesystem::path path) + : m_Context(context), + m_Path(std::move(path)) {} + + bool Loader::ParseAsset() + { + auto data = fastgltf::GltfDataBuffer::FromPath(m_Path); + if (data.error() != fastgltf::Error::None) + { + EE_CORE_ERROR( + "Failed to open glTF file '{}': {}.", + m_Path.string(), + fastgltf::getErrorMessage(data.error()) + ) + return false; + } + + fastgltf::Parser parser{ GLTF_EXTENSIONS }; + + auto loadResult = parser.loadGltf( + data.get(), + m_Path.parent_path(), + GLTF_OPTIONS, + GLTF_CATEGORIES + ); + + if (loadResult.error() != fastgltf::Error::None) + { + EE_CORE_ERROR( + "Failed to parse glTF file '{}': {}.", + m_Path.string(), + fastgltf::getErrorMessage(loadResult.error()) + ) + return false; + } + + m_Asset.emplace(std::move(loadResult.get())); + return true; + } + + bool Loader::LoadGeometry() + { + const auto& asset = *m_Asset; + + if (!asset.scenes.empty()) + { + const size_t sceneIndex = asset.defaultScene.value_or(0); + if (sceneIndex >= asset.scenes.size()) + { + EE_CORE_ERROR("glTF default scene index is invalid.") + return false; + } + + fastgltf::iterateSceneNodes( + asset, + sceneIndex, + fastgltf::math::fmat4x4{}, + [&](const fastgltf::Node& node, const fastgltf::math::fmat4x4& transform) + { + LoadNodeMesh(node, ToGlmMatrix(transform)); + } + ); + } + + else + { + if (asset.nodes.empty()) + { + for (const auto& assetMesh : asset.meshes) + for (const auto& primitive : assetMesh.primitives) + LoadPrimitive(primitive, glm::mat4{ 1.0f }); + } + else + { + std::vector childNodes(asset.nodes.size()); + for (const auto& node : asset.nodes) + { + for (const auto childIndex : node.children) + { + if (childIndex < childNodes.size()) + childNodes[childIndex] = true; + else + EE_CORE_WARN("glTF node references an invalid child node index.") + } + } + + std::vector visitingNodes(asset.nodes.size()); + const auto visitNode = [&](auto&& self, const size_t nodeIndex, const glm::mat4& parentTransform) -> void + { + if (nodeIndex >= asset.nodes.size()) + { + EE_CORE_WARN("glTF node references an invalid child node index.") + return; + } + + if (visitingNodes[nodeIndex]) + { + EE_CORE_WARN("glTF node hierarchy contains a cycle.") + return; + } + + visitingNodes[nodeIndex] = true; + const auto& node = asset.nodes[nodeIndex]; + const glm::mat4 transform = parentTransform * + ToGlmMatrix(fastgltf::getTransformMatrix(node)); + LoadNodeMesh(node, transform); + + for (const auto childIndex : node.children) + self(self, childIndex, transform); + + visitingNodes[nodeIndex] = false; + }; + + for (size_t nodeIndex = 0; nodeIndex < asset.nodes.size(); ++nodeIndex) + { + if (!childNodes[nodeIndex]) + visitNode(visitNode, nodeIndex, glm::mat4(1.0f)); + } + } + } + + return true; + } + + std::optional Loader::Load() + { + m_Mesh.Name = m_Path.stem().string(); + + if (!ParseAsset()) + return std::nullopt; + + m_Mesh.Materials = MaterialLoader( + m_Context, + *m_Asset, + m_Path.parent_path(), + m_Mesh.Name + ).Load(); + + if (!LoadGeometry()) + return std::nullopt; + + if (m_Mesh.Sections.empty()) + { + EE_CORE_ERROR("No valid static meshes were found in '{}'.", m_Path.string()) + return std::nullopt; + } + + return std::move(m_Mesh); + } + } + + std::optional GLTFStaticMeshLoader::Load( + const GraphicsContext& context, + std::filesystem::path path + ) const + { + return Loader(context, std::move(path)).Load(); + } +} diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h new file mode 100644 index 00000000..e3aa8477 --- /dev/null +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h @@ -0,0 +1,38 @@ +#pragma once + +#include + +namespace Elixir +{ + /** @brief Imports static meshes from glTF and GLB source files. */ + class GLTFStaticMeshLoader final : public StaticMeshLoader + { + public: + /** + * @brief Combine all glTF meshes into one static mesh data object. + * + * The loader creates one Surface material for every glTF material and + * maps glTF alpha modes to the material blend mode. It + * imports metallic-roughness PBR factors and base-color, + * metallic-roughness, normal, occlusion, and emissive maps. Materials + * that use `KHR_materials_clearcoat` use the ClearCoat shading model + * and import the clear-coat factor, roughness, and normal map. + * `KHR_materials_specular` imports the dielectric specular factor, + * tint, and optional textures. + * `KHR_materials_emissive_strength` scales the imported emissive + * color factor. + * `KHR_texture_transform` selects TEXCOORD_0 or TEXCOORD_1 and maps + * each texture transform to generic material graph nodes. + * Node transforms are composed within the glTF scene and baked into + * mesh-local vertex data. + * + * @param context Graphics context available for material resolution. + * @param path Local glTF or GLB file path. + * @return Loaded mesh data, or std::nullopt when loading fails. + */ + std::optional Load( + const GraphicsContext& context, + std::filesystem::path path + ) const override; + }; +} diff --git a/Elixir/Source/epch.h b/Elixir/Source/epch.h index ff25d73a..a999a4dc 100644 --- a/Elixir/Source/epch.h +++ b/Elixir/Source/epch.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include diff --git a/Elixir/Tests/Engine/Aether/Effect/MaterialResolverTest.cpp b/Elixir/Tests/Engine/Aether/Effect/MaterialResolverTest.cpp index a8bb1bed..37eb42c1 100644 --- a/Elixir/Tests/Engine/Aether/Effect/MaterialResolverTest.cpp +++ b/Elixir/Tests/Engine/Aether/Effect/MaterialResolverTest.cpp @@ -28,12 +28,53 @@ TEST(MaterialResolverTest, CreatesAuthoredMaterialsAndUsesUsageDefaults) ASSERT_TRUE(resolver.Resolve(system)); ASSERT_TRUE(sprite.GetMaterial()); - EXPECT_NE(sprite.GetMaterial()->GetParent(), registry.GetDefault(EMaterialUsage::ParticleSprite)); + EXPECT_NE(sprite.GetMaterial()->GetParent(), registry.GetDefault(EMaterialUsage::Particle)); - EXPECT_TRUE(sprite.GetMaterial()->GetParent()->SupportsUsage(EMaterialUsage::ParticleSprite)); + EXPECT_EQ(sprite.GetMaterial()->GetParent()->GetUsage(), EMaterialUsage::Particle); ASSERT_TRUE(ribbon.GetMaterial()); - EXPECT_EQ(ribbon.GetMaterial()->GetParent(), registry.GetDefault(EMaterialUsage::ParticleRibbon)); + EXPECT_EQ(ribbon.GetMaterial()->GetParent(), registry.GetDefault(EMaterialUsage::Particle)); EXPECT_TRUE(resolver.Resolve(system)); } + +TEST(MaterialResolverTest, RefreshesAuthoredMaterialsWhenDescriptionsChange) +{ + MaterialRegistry registry; + const Effect::MaterialResolver resolver{ registry }; + System system{ "Effect material refresh" }; + + auto& sprite = system.AddEmitter("Sprite", 8, 0.0f); + sprite.SetMaterialDescription({ .BaseColor = { 0.25f, 0.5f, 0.75f } }); + ASSERT_TRUE(resolver.Resolve(system)); + + const auto initialMaterial = sprite.GetMaterial()->GetParent(); + + sprite.SetMaterialDescription({ .BaseColor = { 0.75f, 0.5f, 0.25f } }); + ASSERT_TRUE(resolver.Resolve(system)); + + ASSERT_TRUE(sprite.GetMaterial()); + EXPECT_NE(sprite.GetMaterial()->GetParent(), initialMaterial); + EXPECT_EQ( + registry.Find("Aether." + system.GetId() + ".Sprite"), + sprite.GetMaterial()->GetParent() + ); +} + +TEST(MaterialResolverTest, PreservesExplicitMaterialOverrides) +{ + MaterialRegistry registry; + const Effect::MaterialResolver resolver{ registry }; + System system{ "Effect material override" }; + + auto& sprite = system.AddEmitter("Sprite", 8, 0.0f); + sprite.SetMaterialDescription({ .BaseColor = { 0.25f, 0.5f, 0.75f } }); + ASSERT_TRUE(resolver.Resolve(system)); + + const auto override = registry.GetDefault(EMaterialUsage::Particle)->CreateInstance(); + sprite.SetMaterial(override); + sprite.SetMaterialDescription({ .BaseColor = { 0.75f, 0.5f, 0.25f } }); + + ASSERT_TRUE(resolver.Resolve(system)); + EXPECT_EQ(sprite.GetMaterial(), override); +} diff --git a/Elixir/Tests/Engine/Aether/SystemTest.cpp b/Elixir/Tests/Engine/Aether/SystemTest.cpp index 5a051060..4734c7b4 100644 --- a/Elixir/Tests/Engine/Aether/SystemTest.cpp +++ b/Elixir/Tests/Engine/Aether/SystemTest.cpp @@ -127,10 +127,9 @@ TEST(SystemTest, FindsNamedEmitterForMaterialPublication) EXPECT_EQ(system.FindEmitter("Missing"), nullptr); } -TEST(SystemTest, CompilePublishesParticleSpriteMaterialInstance) +TEST(SystemTest, CompilePublishesParticleMaterialInstance) { - const auto material = CreateRef("Particle tint"); - ASSERT_TRUE(material->SetUsage(EMaterialUsage::ParticleSprite, true)); + const auto material = CreateRef("Particle tint", EMaterialUsage::Particle); ASSERT_TRUE(material->DefineParameter("Tint", { .Kind = EMaterialParameterKind::Value, .ValueType = EMaterialValueType::Float4, @@ -148,9 +147,7 @@ TEST(SystemTest, CompilePublishesParticleSpriteMaterialInstance) ASSERT_EQ(first.Emitters.size(), 1); ASSERT_TRUE(first.Emitters[0].Material); - EXPECT_TRUE(first.Emitters[0].Material->GetParent()->SupportsUsage( - EMaterialUsage::ParticleSprite - )); + EXPECT_EQ(first.Emitters[0].Material->GetParent()->GetUsage(), EMaterialUsage::Particle); EXPECT_EQ(first.Emitters[0].Material, instance); EXPECT_FLOAT_EQ(first.Emitters[0].Material->GetVector("Tint").x, 0.25f); @@ -166,8 +163,7 @@ TEST(SystemTest, CompilePublishesParticleSpriteMaterialInstance) TEST(SystemTest, CompileSnapshotsParticleRibbonMaterialForRenderData) { - const auto material = CreateRef("Particle ribbon"); - ASSERT_TRUE(material->SetUsage(EMaterialUsage::ParticleRibbon, true)); + const auto material = CreateRef("Particle ribbon", EMaterialUsage::Particle); const auto system = CreateRef("Ribbon material snapshot contract"); auto& emitter = system->AddEmitter("Ribbon", 8, 0.0f); @@ -180,15 +176,36 @@ TEST(SystemTest, CompileSnapshotsParticleRibbonMaterialForRenderData) ASSERT_EQ(compiled.Emitters.size(), 1); ASSERT_TRUE(compiled.Emitters[0].Material); - EXPECT_TRUE(compiled.Emitters[0].Material->GetParent()->SupportsUsage( - EMaterialUsage::ParticleRibbon - )); + EXPECT_EQ( + compiled.Emitters[0].Material->GetParent()->GetUsage(), + EMaterialUsage::Particle + ); +} + +TEST(SystemTest, CompilePreservesParticleMaterialWhenRenderModeChanges) +{ + const auto material = CreateRef("Particle material", EMaterialUsage::Particle); + + const auto system = CreateRef("Particle render mode contract"); + auto& emitter = system->AddEmitter("Emitter", 8, 0.0f); + const auto instance = material->CreateInstance(); + emitter.SetMaterial(instance); + emitter.SetRenderMode(EParticleRenderMode::Ribbon); + + const auto compiled = Compile(system); + + ASSERT_EQ(compiled.Emitters.size(), 1); + EXPECT_EQ(compiled.Emitters[0].RenderMode, EParticleRenderMode::Ribbon); + EXPECT_EQ(compiled.Emitters[0].Material, instance); + EXPECT_EQ( + compiled.Emitters[0].Material->GetParent()->GetUsage(), + EMaterialUsage::Particle + ); } TEST(SystemTest, CompileSnapshotsParticleMeshMaterialForRenderData) { - const auto material = CreateRef("Particle mesh"); - material->SetUsage(EMaterialUsage::ParticleMesh, true); + const auto material = CreateRef("Particle mesh", EMaterialUsage::Particle); const auto system = CreateRef("Mesh material"); auto& emitter = system->AddEmitter("Mesh", 8, 0.0f); @@ -201,9 +218,10 @@ TEST(SystemTest, CompileSnapshotsParticleMeshMaterialForRenderData) ASSERT_EQ(compiled.Emitters.size(), 1); ASSERT_TRUE(compiled.Emitters[0].Material); - EXPECT_TRUE(compiled.Emitters[0].Material->GetParent()->SupportsUsage( - EMaterialUsage::ParticleMesh - )); + EXPECT_EQ( + compiled.Emitters[0].Material->GetParent()->GetUsage(), + EMaterialUsage::Particle + ); } TEST(SystemTest, CompileAssignsTheDefaultMaterialWhenNoneIsExplicit) @@ -215,7 +233,8 @@ TEST(SystemTest, CompileAssignsTheDefaultMaterialWhenNoneIsExplicit) ASSERT_EQ(compiled.Emitters.size(), 1); ASSERT_TRUE(compiled.Emitters[0].Material); - EXPECT_TRUE(compiled.Emitters[0].Material->GetParent()->SupportsUsage( - EMaterialUsage::ParticleSprite - )); + EXPECT_EQ( + compiled.Emitters[0].Material->GetParent()->GetUsage(), + EMaterialUsage::Particle + ); } diff --git a/Elixir/Tests/Engine/Graphics/FrameSlotStateTest.cpp b/Elixir/Tests/Engine/Graphics/FrameSlotStateTest.cpp index e383967b..1471397b 100644 --- a/Elixir/Tests/Engine/Graphics/FrameSlotStateTest.cpp +++ b/Elixir/Tests/Engine/Graphics/FrameSlotStateTest.cpp @@ -18,8 +18,12 @@ namespace Elixir void ProcessEvent(Event&) override {} void RenderFrame(std::function) override {} void DrainRenderQueue() override {} + bool EnqueueRenderTask(std::function task) const override { task(); return true; } + bool RunRenderTaskAndWait(std::function task) const override { task(); return true; } + bool IsRenderThread() const override { return true; } + bool IsFrameRecording() const override { return false; } + void WaitForSubmittedFrames() const override {} void SetClearColor(const glm::vec4&) override {} - void Clear() override {} void Resize(Extent2D) override {} Ref GetSecondaryCommandBuffer() const override { return nullptr; } Ref GetUploadCommandBuffer() const override { return nullptr; } @@ -27,6 +31,7 @@ namespace Elixir Extent3D GetSwapchainExtent() const override { return {}; } private: + void ClearImage(const Ref&) override {} void CreateRenderTargets() override {} }; diff --git a/Elixir/Tests/Engine/Graphics/ImageTest.cpp b/Elixir/Tests/Engine/Graphics/ImageTest.cpp index 4bf32df5..d65e1c70 100644 --- a/Elixir/Tests/Engine/Graphics/ImageTest.cpp +++ b/Elixir/Tests/Engine/Graphics/ImageTest.cpp @@ -1,23 +1,168 @@ #include -using namespace testing; #include +#include + using namespace Elixir; -TEST(ImageTest, Image_IsNotConstructibleAndAssignable) +namespace +{ + class ImageMetadata final : public Image + { + public: + explicit ImageMetadata(const SImageCreateInfo& info) : Image(nullptr, info) {} + void Destroy() override {} + void Transition(const CommandBuffer*, EImageLayout) override {} + void Barrier(const CommandBuffer*) override {} + void CopyFrom(const CommandBuffer*, const Buffer*, std::span) override {} + bool IsValid() const override { return false; } + + private: + void CreateResource(const SImageCreateInfo&) override {} + void GenerateMipmaps(const CommandBuffer*, EImageLayout) override {} + void CopyMip(const CommandBuffer*, Image*, const Extent3D&, const Extent3D&, uint32_t) override {} + }; +} + +TEST(ImageTest, ImageCannotBeCopiedOrMoved) { - EXPECT_FALSE(std::is_constructible_v); + EXPECT_TRUE(std::is_abstract_v); EXPECT_FALSE(std::is_copy_constructible_v); EXPECT_FALSE(std::is_copy_assignable_v); EXPECT_FALSE(std::is_move_constructible_v); EXPECT_FALSE(std::is_move_assignable_v); } -TEST(ImageTest, DepthStencilImage_IsNotConstructibleAndAssignable) +TEST(ImageTest, TextureAssetsUseComposition) +{ + EXPECT_FALSE((std::is_base_of_v)); + EXPECT_FALSE((std::is_base_of_v)); + EXPECT_TRUE((std::is_base_of_v)); + EXPECT_TRUE((std::is_base_of_v)); + EXPECT_FALSE(std::is_copy_constructible_v); +} + +TEST(ImageTest, TextureMappingDoesNotResolveMipmapsOrUploadUsage) +{ + STexture2DCreateInfo textureInfo; + textureInfo.Format = EImageFormat::R8G8B8A8_UNORM; + textureInfo.Width = 8; + textureInfo.Height = 4; + textureInfo.MipLevels = 99; + textureInfo.MipmapMode = EImageMipmapMode::NormalMap; + const auto imageInfo = Texture2D::CreateImageInfo(textureInfo); + EXPECT_EQ(imageInfo.MipLevels, 99u); + EXPECT_EQ(imageInfo.MipmapMode, EImageMipmapMode::NormalMap); + EXPECT_EQ(imageInfo.Usage, EImageUsage::Sampled); + EXPECT_EQ(Image::GetMipLevelCount(imageInfo), 4u); +} + +TEST(ImageTest, TextureCreationPropagatesImageCreationFailure) +{ + STextureCreateInfo textureInfo; + textureInfo.Width = 1; + EXPECT_FALSE(Texture::Create(nullptr, textureInfo)); + + STexture2DCreateInfo texture2DInfo; + texture2DInfo.Width = 1; + texture2DInfo.Height = 1; + EXPECT_FALSE(Texture2D::Create(nullptr, texture2DInfo)); + + STexture3DCreateInfo texture3DInfo; + texture3DInfo.Width = 1; + texture3DInfo.Height = 1; + texture3DInfo.Depth = 1; + EXPECT_FALSE(Texture3D::Create(nullptr, texture3DInfo)); +} + +TEST(ImageTest, MipCountUsesTheLargestDimension) +{ + EXPECT_EQ(Image::GetFullMipLevelCount({ 8, 4, 1 }), 4u); + EXPECT_EQ(Image::GetFullMipLevelCount({ 1, 1, 1 }), 1u); + EXPECT_EQ(Image::GetFullMipLevelCount({ 2, 8, 16 }), 5u); + EXPECT_EQ(Image::GetFullMipLevelCount({ 7, 3, 1 }), 3u); +} + +TEST(ImageTest, GeneratedModesIgnoreExplicitMipCount) { - EXPECT_FALSE(std::is_constructible_v); - EXPECT_FALSE(std::is_copy_constructible_v); - EXPECT_FALSE(std::is_copy_assignable_v); - EXPECT_FALSE(std::is_move_constructible_v); - EXPECT_FALSE(std::is_move_assignable_v); -} \ No newline at end of file + SImageCreateInfo info{ .Width = 8, .Height = 4 }; + for (const auto mode : { EImageMipmapMode::SimpleAverage, EImageMipmapMode::NormalMap }) + { + info.MipmapMode = mode; + info.MipLevels = 0; + EXPECT_EQ(Image::GetMipLevelCount(info), 4u); + info.MipLevels = 99; + EXPECT_EQ(Image::GetMipLevelCount(info), 4u); + } +} + +TEST(ImageTest, NoMipmapsAlwaysResolvesOneLevel) +{ + SImageCreateInfo info{ .Width = 8, .Height = 4, .MipLevels = 99 }; + EXPECT_EQ(Image::GetMipLevelCount(info), 1u); +} + +TEST(ImageTest, LeaveExistingMipsPreservesTheCount) +{ + SImageCreateInfo info{ + .Width = 8, .Height = 4, + .MipmapMode = EImageMipmapMode::LeaveExistingMips, .MipLevels = 3 + }; + EXPECT_EQ(Image::GetMipLevelCount(info), 3u); +} + +TEST(ImageTest, ImageUsageCanTestAnyFlags) +{ + const auto usage = EImageUsage::Sampled | EImageUsage::TransferDst; + EXPECT_TRUE(HasAnyFlags(usage, EImageUsage::Sampled | EImageUsage::Storage)); + EXPECT_FALSE(HasAnyFlags(usage, EImageUsage::Storage | EImageUsage::ColorAttachment)); +} + +TEST(ImageTest, AllocationDescriptionDoesNotKeepUploadDataOrGenerationRequests) +{ + std::array pixels{}; + SImageCreateInfo info{ + .Width = 8, .Height = 4, + .Format = EImageFormat::R8G8B8A8_UNORM, + .MipmapMode = EImageMipmapMode::NormalMap, + .MipLevels = 4, + .AllocationInfo = { .RequiredFlags = EMemoryProperty::HostVisible }, + .InitialData = pixels.data(), + }; + ImageMetadata image(info); + const auto allocation = image.GetCreateInfo(); + EXPECT_EQ(allocation.InitialData, nullptr); + EXPECT_TRUE(allocation.InitialMipData.empty()); + EXPECT_EQ(allocation.MipmapMode, EImageMipmapMode::LeaveExistingMips); + EXPECT_EQ(allocation.MipLevels, 4u); + EXPECT_EQ(allocation.AllocationInfo.RequiredFlags, EMemoryProperty::HostVisible); +} + +TEST(ImageTest, BaseSizeIncludesArrayLayersAndCompressedEdgeBlocks) +{ + ImageMetadata image({ + .Width = 5, .Height = 3, + .Format = EImageFormat::BC1_RGB_UNORM_BLOCK, + .MipLevels = 3, .ArrayLayers = 2, + }); + EXPECT_EQ(image.GetMipSize(0), 16u); + EXPECT_EQ(image.GetMipSize(1), 8u); + EXPECT_EQ(image.GetMipSize(2), 8u); + EXPECT_EQ(image.GetSize(), 32u); +} + +TEST(ImageTest, Texture3DMapsAllDimensionsWithoutResolvingMipCount) +{ + STexture3DCreateInfo info; + info.Width = 4; + info.Height = 2; + info.Depth = 8; + info.MipmapMode = EImageMipmapMode::SimpleAverage; + const auto image = Texture3D::CreateImageInfo(info); + EXPECT_EQ(image.Type, EImageType::_3D); + EXPECT_EQ(image.Width, 4u); + EXPECT_EQ(image.Height, 2u); + EXPECT_EQ(image.Depth, 8u); + EXPECT_EQ(image.MipLevels, 1u); + EXPECT_EQ(Image::GetMipLevelCount(image), 4u); +} diff --git a/Elixir/Tests/Engine/Materials/Compilation/CompilationCacheTest.cpp b/Elixir/Tests/Engine/Materials/Compilation/CompilationCacheTest.cpp index 25072c78..b0092459 100644 --- a/Elixir/Tests/Engine/Materials/Compilation/CompilationCacheTest.cpp +++ b/Elixir/Tests/Engine/Materials/Compilation/CompilationCacheTest.cpp @@ -9,9 +9,7 @@ using namespace Elixir::Materials::Compilation; TEST(CompilationCacheTest, ReusesACompiledMaterialUntilTheSourceRevisionChanges) { CompilationCache cache{ nullptr }; - const auto material = CreateRef("Cache test"); - - ASSERT_TRUE(material->SetUsage(EMaterialUsage::ParticleSprite, true)); + const auto material = CreateRef("Cache test", EMaterialUsage::Particle); const auto first = cache.GetOrCompile(material); const auto second = cache.GetOrCompile(material); @@ -19,11 +17,11 @@ TEST(CompilationCacheTest, ReusesACompiledMaterialUntilTheSourceRevisionChanges) ASSERT_TRUE(first); EXPECT_EQ(first, second); - ASSERT_TRUE(material->SetUsage(EMaterialUsage::ParticleRibbon, true)); + ASSERT_TRUE(material->SetBlendMode(EMaterialBlendMode::Translucent)); const auto rebuilt = cache.GetOrCompile(material); ASSERT_TRUE(rebuilt); EXPECT_NE(first, rebuilt); - EXPECT_TRUE(rebuilt->SupportsUsage(EMaterialUsage::ParticleRibbon)); + EXPECT_EQ(rebuilt->GetUsage(), EMaterialUsage::Particle); } diff --git a/Elixir/Tests/Engine/Materials/Compilation/CompilerTest.cpp b/Elixir/Tests/Engine/Materials/Compilation/CompilerTest.cpp index 128756a6..1a36831d 100644 --- a/Elixir/Tests/Engine/Materials/Compilation/CompilerTest.cpp +++ b/Elixir/Tests/Engine/Materials/Compilation/CompilerTest.cpp @@ -32,29 +32,26 @@ TEST(CompilerTest, AssignsStableSlotsByParameterKindAndName) EXPECT_EQ(result.Material->Parameters[1].Slot, 0); } -TEST(CompilerTest, PreservesEnabledRendererUsages) +TEST(CompilerTest, CompilesOnlyTheSelectedRendererUsage) { - const auto material = CreateRef("Particle material"); - ASSERT_TRUE(material->SetUsage(EMaterialUsage::ParticleSprite, true)); - ASSERT_TRUE(material->SetUsage(EMaterialUsage::ParticleRibbon, true)); - ASSERT_TRUE(material->SetUsage(EMaterialUsage::ParticleMesh, true)); + const auto material = CreateRef("Material", EMaterialUsage::Particle); const auto result = Compiler::Build(*material); ASSERT_TRUE(result); - EXPECT_TRUE(result.Material->SupportsUsage(EMaterialUsage::ParticleSprite)); - EXPECT_TRUE(result.Material->SupportsUsage(EMaterialUsage::ParticleRibbon)); - EXPECT_TRUE(result.Material->SupportsUsage(EMaterialUsage::ParticleMesh)); + EXPECT_EQ(result.Material->GetUsage(), EMaterialUsage::Particle); } TEST(CompilerTest, DoesNotAliasParticleUsageShadersToSurfaceShader) { SCompiledMaterial material; - const auto& spriteShader = material.GetShader(EMaterialUsage::ParticleSprite); - const auto& ribbonShader = material.GetShader(EMaterialUsage::ParticleRibbon); - const auto& meshShader = material.GetShader(EMaterialUsage::ParticleMesh); + const auto& surfaceShader = material.GetShader(EMaterialShaderVariant::Surface); + const auto& spriteShader = material.GetShader(EMaterialShaderVariant::ParticleSprite); + const auto& ribbonShader = material.GetShader(EMaterialShaderVariant::ParticleRibbon); + const auto& meshShader = material.GetShader(EMaterialShaderVariant::ParticleMesh); + EXPECT_EQ(&surfaceShader, &material.SurfaceShader); EXPECT_EQ(&spriteShader, &material.ParticleSpriteShader); EXPECT_EQ(&ribbonShader, &material.ParticleRibbonShader); EXPECT_EQ(&meshShader, &material.ParticleMeshShader); diff --git a/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp b/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp index 02f13201..0148ffe4 100644 --- a/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp +++ b/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp @@ -2,12 +2,25 @@ #include +#include +#include +#include +#include #include +#include #include #include +#include +#include +#include #include #include +#include +#include +#include +#include #include +#include using namespace Elixir; using namespace Elixir::Materials; @@ -62,6 +75,18 @@ TEST(MaterialGraphTest, ScalarChannelsAndSharedNode) EXPECT_EQ(hlsl.find("float n", first + 1), std::string::npos); } +TEST(MaterialGraphTest, UsesDefaultsForUnconnectedUnaryNodes) +{ + MaterialGraph graph; + graph.SetChannel(EMaterialChannel::BaseColor, graph.AddNode()); + graph.SetChannel(EMaterialChannel::Emissive, graph.AddNode()); + + const auto hlsl = graph.GenerateHLSL(); + + EXPECT_NE(hlsl.find("(1.0 - 0.0)"), std::string::npos); + EXPECT_NE(hlsl.find("saturate(0.0)"), std::string::npos); +} + TEST(MaterialGraphTest, RoutesTextureAlphaToOpacity) { MaterialGraph graph; @@ -78,6 +103,211 @@ TEST(MaterialGraphTest, RoutesTextureAlphaToOpacity) EXPECT_NE(hlsl.find(".w"), std::string::npos); } +TEST(MaterialGraphTest, EmitsColor) +{ + MaterialGraph graph; + graph.SetChannel(EMaterialChannel::BaseColor, graph.AddNode()); + + const auto hlsl = graph.GenerateHLSL(); + + EXPECT_NE(hlsl.find("input.Color"), std::string::npos); +} + +TEST(MaterialGraphTest, RoutesSpecularInputs) +{ + MaterialGraph graph; + + const auto specular = graph.AddNode( + "SpecularFactor", + EMaterialValueType::Float + ); + const auto specularColor = graph.AddNode( + "SpecularColorFactor", + EMaterialValueType::Float3 + ); + graph.SetChannel(EMaterialChannel::Specular, specular); + graph.SetChannel(EMaterialChannel::SpecularColor, specularColor); + + const auto hlsl = graph.GenerateHLSL({ + .Values = { + { "SpecularFactor", "mat.Values[0].x" }, + { "SpecularColorFactor", "mat.Values[1].xyz" }, + }, + }); + + EXPECT_NE(hlsl.find("mat.Values[0].x"), std::string::npos); + EXPECT_NE(hlsl.find("mat.Values[1].xyz"), std::string::npos); + EXPECT_NE(hlsl.find("surface.Specular = n"), std::string::npos); + EXPECT_NE(hlsl.find("surface.SpecularColor = n"), std::string::npos); +} + +TEST(MaterialGraphTest, SelectsSecondStaticMeshTextureCoordinate) +{ + MaterialGraph graph; + + const auto texCoord = graph.AddNode(1); + graph.SetChannel(EMaterialChannel::BaseColor, texCoord); + + const auto hlsl = graph.GenerateHLSL(); + + EXPECT_NE(hlsl.find("input.TexCoord1"), std::string::npos); + EXPECT_NE(hlsl.find("surface.BaseColor"), std::string::npos); +} + +TEST(MaterialGraphTest, GeneratesStaticTextureCoordinateTransform) +{ + MaterialGraph graph; + + const auto texCoord = graph.AddNode(1); + const auto scale = graph.AddNode("UVScale", EMaterialValueType::Float2); + const auto offset = graph.AddNode("UVOffset", EMaterialValueType::Float2); + const auto rotation = graph.AddNode("UVRotation", EMaterialValueType::Float); + const auto scaled = graph.AddNode(); + graph.Connect(texCoord, scaled, 0); + graph.Connect(scale, scaled, 1); + + const auto sine = graph.AddNode(); + const auto cosine = graph.AddNode(); + graph.Connect(rotation, sine, 0); + graph.Connect(rotation, cosine, 0); + + const auto zero = graph.AddNode(glm::vec4(0.0f), EMaterialValueType::Float); + const auto negativeSine = graph.AddNode(); + graph.Connect(zero, negativeSine, 0); + graph.Connect(sine, negativeSine, 1); + + const auto firstRow = graph.AddNode(); + graph.Connect(cosine, firstRow, 0); + graph.Connect(negativeSine, firstRow, 1); + + const auto secondRow = graph.AddNode(); + graph.Connect(sine, secondRow, 0); + graph.Connect(cosine, secondRow, 1); + + const auto rotatedX = graph.AddNode(); + graph.Connect(scaled, rotatedX, 0); + graph.Connect(firstRow, rotatedX, 1); + + const auto rotatedY = graph.AddNode(); + graph.Connect(scaled, rotatedY, 0); + graph.Connect(secondRow, rotatedY, 1); + + const auto rotated = graph.AddNode(); + graph.Connect(rotatedX, rotated, 0); + graph.Connect(rotatedY, rotated, 1); + + const auto transformed = graph.AddNode(); + graph.Connect(rotated, transformed, 0); + graph.Connect(offset, transformed, 1); + + const auto texture = graph.AddNode("Albedo"); + graph.Connect(transformed, texture, 0); + graph.SetChannel(EMaterialChannel::BaseColor, texture); + + const auto hlsl = graph.GenerateHLSL({ + .Values = { + { "UVScale", "mat.Values[0].xy" }, + { "UVOffset", "mat.Values[1].xy" }, + { "UVRotation", "mat.Values[2].x" }, + }, + .Textures = {{ "Albedo", "mat.TextureIndices[0]" }}, + }); + + EXPECT_NE(hlsl.find("input.TexCoord1"), std::string::npos); + EXPECT_NE(hlsl.find("cos("), std::string::npos); + EXPECT_NE(hlsl.find("sin("), std::string::npos); + EXPECT_NE(hlsl.find("mat.Values[0].xy"), std::string::npos); + EXPECT_NE(hlsl.find("mat.Values[1].xy"), std::string::npos); + EXPECT_NE(hlsl.find("SampleTex(mat.TextureIndices[0]"), std::string::npos); +} + +TEST(MaterialGraphTest, GeneratesNormalTextureSamplingAndFlattening) +{ + MaterialGraph graph; + + const auto texture = graph.AddNode( + "NormalTexture", + ETextureSampleType::Normal + ); + const auto flatness = graph.AddNode( + "NormalScale", + EMaterialValueType::Float + ); + const auto flatten = graph.AddNode(); + graph.Connect(texture, flatten, 0); + graph.Connect(flatness, flatten, 1); + graph.SetChannel(EMaterialChannel::Normal, flatten); + + const auto hlsl = graph.GenerateHLSL({ + .Values = {{ "NormalScale", "mat.Values[0].x" }}, + .Textures = {{ "NormalTexture", "mat.TextureIndices[0]" }} + }); + + EXPECT_NE(hlsl.find("SampleNormal(mat.TextureIndices[0], input.TexCoord)"), std::string::npos); + EXPECT_NE(hlsl.find("lerp(1.0"), std::string::npos); + EXPECT_NE(hlsl.find("surface.Normal ="), std::string::npos); +} + +TEST(MaterialGraphTest, ScalesNormalMapXYComponentsWithoutChangingZ) +{ + MaterialGraph graph; + + const auto texture = graph.AddNode( + "NormalTexture", + ETextureSampleType::Normal + ); + const auto scale = graph.AddNode( + "NormalScale", + EMaterialValueType::Float + ); + const auto scaledNormal = graph.AddNode(); + graph.Connect(texture, scaledNormal, 0); + graph.Connect(scale, scaledNormal, 1); + graph.SetChannel(EMaterialChannel::Normal, scaledNormal); + + const auto hlsl = graph.GenerateHLSL({ + .Values = {{ "NormalScale", "mat.Values[0].x" }}, + .Textures = {{ "NormalTexture", "mat.TextureIndices[0]" }} + }); + + EXPECT_NE(hlsl.find("normalize(float3("), std::string::npos); + EXPECT_NE(hlsl.find("SampleNormal(mat.TextureIndices[0], input.TexCoord)"), std::string::npos); + EXPECT_EQ(hlsl.find("lerp(1.0"), std::string::npos); +} + +TEST(MaterialGraphTest, InterpolatesAmbientOcclusionFromOne) +{ + MaterialGraph graph; + + const auto one = graph.AddNode( + glm::vec4{ 1.0f }, + EMaterialValueType::Float + ); + const auto occlusion = graph.AddNode( + "Occlusion", + EMaterialValueType::Float + ); + const auto strength = graph.AddNode( + "OcclusionStrength", + EMaterialValueType::Float + ); + const auto lerp = graph.AddNode(); + graph.Connect(one, lerp, 0); + graph.Connect(occlusion, lerp, 1); + graph.Connect(strength, lerp, 2); + graph.SetChannel(EMaterialChannel::AmbientOcclusion, lerp); + + const auto hlsl = graph.GenerateHLSL({ + .Values = { + { "Occlusion", "mat.Values[0].x" }, + { "OcclusionStrength", "mat.Values[1].x" } + } + }); + + EXPECT_NE(hlsl.find("lerp("), std::string::npos); + EXPECT_NE(hlsl.find("surface.AmbientOcclusion ="), std::string::npos); +} + TEST(MaterialGraphTest, GeneratesExponentialRadialGradientForOpacity) { MaterialGraph graph; diff --git a/Elixir/Tests/Engine/Materials/MaterialProxyCacheTest.cpp b/Elixir/Tests/Engine/Materials/MaterialProxyCacheTest.cpp index 66d3a925..db33d541 100644 --- a/Elixir/Tests/Engine/Materials/MaterialProxyCacheTest.cpp +++ b/Elixir/Tests/Engine/Materials/MaterialProxyCacheTest.cpp @@ -76,7 +76,10 @@ TEST(MaterialProxyCacheTest, RebuildsTheProxyWhenTheParentMaterialRevisionChange const auto instance = CreateInstance(); const auto first = cache.Resolve(instance); - ASSERT_TRUE(instance->GetParent()->SetUsage(EMaterialUsage::ParticleSprite, true)); + ASSERT_TRUE(instance->GetParent()->SetDefaultParameter( + "Tint", + SMaterialParameter::MakeVector({ 0.2f, 0.4f, 0.6f, 1.0f }) + )); const auto rebuilt = cache.Resolve(instance); ASSERT_TRUE(first); diff --git a/Elixir/Tests/Engine/Materials/MaterialRegistryTest.cpp b/Elixir/Tests/Engine/Materials/MaterialRegistryTest.cpp index ae291d69..7f8feede 100644 --- a/Elixir/Tests/Engine/Materials/MaterialRegistryTest.cpp +++ b/Elixir/Tests/Engine/Materials/MaterialRegistryTest.cpp @@ -10,15 +10,14 @@ TEST(MaterialRegistryTest, RegistersAndFindsDefaultMaterials) const MaterialRegistry registry; for (const auto usage : { - EMaterialUsage::ParticleSprite, - EMaterialUsage::ParticleRibbon, - EMaterialUsage::ParticleMesh + EMaterialUsage::Surface, + EMaterialUsage::Particle }) { const auto& material = registry.GetDefault(usage); ASSERT_TRUE(material); EXPECT_EQ(registry.Find(material->GetName()), material); - EXPECT_TRUE(material->SupportsUsage(usage)); + EXPECT_EQ(material->GetUsage(), usage); EXPECT_TRUE(material->ValidateGraph()); EXPECT_TRUE(material->GetParameters().empty()); } diff --git a/Elixir/Tests/Engine/Materials/MaterialTest.cpp b/Elixir/Tests/Engine/Materials/MaterialTest.cpp index afc85ccc..713ef099 100644 --- a/Elixir/Tests/Engine/Materials/MaterialTest.cpp +++ b/Elixir/Tests/Engine/Materials/MaterialTest.cpp @@ -1,13 +1,72 @@ #include #include +#include #include #include +#include using namespace Elixir; using namespace Elixir::Materials; using namespace Elixir::Materials::Nodes; +TEST(MaterialTest, StoresSurfaceShadingModel) +{ + Material material("ClearCoated"); + + EXPECT_EQ(material.GetUsage(), EMaterialUsage::Surface); + EXPECT_EQ(material.GetShadingModel(), EMaterialShadingModel::Lit); + + const auto revision = material.GetRevision(); + EXPECT_TRUE(material.SetShadingModel(EMaterialShadingModel::ClearCoat)); + EXPECT_EQ(material.GetShadingModel(), EMaterialShadingModel::ClearCoat); + EXPECT_EQ(material.GetRevision(), revision + 1); + EXPECT_FALSE(material.SetShadingModel(EMaterialShadingModel::ClearCoat)); +} + +TEST(MaterialTest, StoresTransparencySettings) +{ + Material material("Transparency"); + + EXPECT_EQ(material.GetBlendMode(), EMaterialBlendMode::Opaque); + EXPECT_FLOAT_EQ(material.GetAlphaCutoff(), 0.5f); + + const auto revision = material.GetRevision(); + EXPECT_TRUE(material.SetBlendMode(EMaterialBlendMode::Masked)); + EXPECT_EQ(material.GetBlendMode(), EMaterialBlendMode::Masked); + EXPECT_EQ(material.GetRevision(), revision + 1); + + EXPECT_TRUE(material.SetAlphaCutoff(0.35f)); + EXPECT_FLOAT_EQ(material.GetAlphaCutoff(), 0.35f); + EXPECT_EQ(material.GetRevision(), revision + 2); +} + +TEST(MaterialTest, StoresDoubleSidedSetting) +{ + Material material("DoubleSided"); + + EXPECT_FALSE(material.IsDoubleSided()); + + const auto revision = material.GetRevision(); + EXPECT_TRUE(material.SetDoubleSided(true)); + EXPECT_TRUE(material.IsDoubleSided()); + EXPECT_EQ(material.GetRevision(), revision + 1); + EXPECT_FALSE(material.SetDoubleSided(true)); +} + +TEST(MaterialTest, ClampsAlphaCutoff) +{ + Material material("Transparency"); + + EXPECT_TRUE(material.SetAlphaCutoff(-0.01f)); + EXPECT_FLOAT_EQ(material.GetAlphaCutoff(), 0.0f); + + EXPECT_TRUE(material.SetAlphaCutoff(1.01f)); + EXPECT_FLOAT_EQ(material.GetAlphaCutoff(), 1.0f); + + EXPECT_FALSE(material.SetAlphaCutoff(1.5f)); +} + TEST(MaterialTest, ValidateGraphParametersAgainstMaterialSchema) { MaterialGraph graph; @@ -28,6 +87,21 @@ TEST(MaterialTest, ValidateGraphParametersAgainstMaterialSchema) EXPECT_TRUE(material->ValidateGraph()); } +TEST(MaterialTest, RejectsCyclicGraphConnections) +{ + MaterialGraph graph; + const auto add = graph.AddNode(); + graph.Connect(add, add, 0); + graph.SetChannel(EMaterialChannel::BaseColor, add); + + Material material("Cyclic"); + material.SetGraph(std::move(graph)); + + std::string error; + EXPECT_FALSE(material.ValidateGraph(&error)); + EXPECT_NE(error.find("cycle"), std::string::npos); +} + TEST(MaterialTest, RejectsOverridesThatDoNotMatchTheSchema) { const auto material = CreateRef("Tinted"); @@ -46,6 +120,26 @@ TEST(MaterialTest, RejectsOverridesThatDoNotMatchTheSchema) EXPECT_EQ(instance.GetRevision(), revision + 1); } +TEST(MaterialTest, ColorNodeRequiresParticleUsage) +{ + MaterialGraph surfaceGraph; + surfaceGraph.SetChannel(EMaterialChannel::BaseColor, surfaceGraph.AddNode()); + + Material surfaceMaterial("Surface color"); + surfaceMaterial.SetGraph(std::move(surfaceGraph)); + + std::string error; + EXPECT_FALSE(surfaceMaterial.ValidateGraph(&error)); + EXPECT_NE(error.find("requires particle"), std::string::npos); + + MaterialGraph particleGraph; + particleGraph.SetChannel(EMaterialChannel::BaseColor, particleGraph.AddNode()); + + Material particleMaterial("Particle color", EMaterialUsage::Particle); + particleMaterial.SetGraph(std::move(particleGraph)); + EXPECT_TRUE(particleMaterial.ValidateGraph()); +} + TEST(MaterialTest, ValidatesTextureSampleAgainstTextureParameter) { MaterialGraph graph; diff --git a/Elixir/Tests/Engine/Materials/Rendering/FrameTableTest.cpp b/Elixir/Tests/Engine/Materials/Rendering/FrameTableTest.cpp index e7f405f4..ee80a4d9 100644 --- a/Elixir/Tests/Engine/Materials/Rendering/FrameTableTest.cpp +++ b/Elixir/Tests/Engine/Materials/Rendering/FrameTableTest.cpp @@ -16,36 +16,13 @@ namespace { public: TestTexture() - : Texture(nullptr, EImageFormat::R8G8B8A8_UNORM, 1) {} - - void Destroy() override {} - void Resize(const Ref& cmd, Extent3D extent) override {} - void Transition(const CommandBuffer* cmd, EImageLayout layout) override {} - - void Copy( - const CommandBuffer* cmd, - Image* dst, - const Extent3D& srcExtent, - const Extent3D& dstExtent - ) override {} - - void CopyFrom( - const CommandBuffer* cmd, - const Buffer* src, - std::span regions - ) override {} - - bool IsValid() const override { return true; } - - protected: - void UpdateSampler() override {} + : Texture(nullptr, {}) {} }; } TEST(FrameTableTest, DeduplicatesAProxyAndPreservesItsValues) { - auto material = CreateRef("Particle material"); - ASSERT_TRUE(material->SetUsage(EMaterialUsage::ParticleSprite, true)); + auto material = CreateRef("Particle material", EMaterialUsage::Particle); ASSERT_TRUE(material->DefineParameter("Tint", { .Kind = EMaterialParameterKind::Value, .ValueType = EMaterialValueType::Float4, diff --git a/Elixir/Tests/Engine/Materials/Rendering/MaterialRenderProxyTest.cpp b/Elixir/Tests/Engine/Materials/Rendering/MaterialRenderProxyTest.cpp index 9edc7bf3..3a952b74 100644 --- a/Elixir/Tests/Engine/Materials/Rendering/MaterialRenderProxyTest.cpp +++ b/Elixir/Tests/Engine/Materials/Rendering/MaterialRenderProxyTest.cpp @@ -50,3 +50,18 @@ TEST(MaterialRenderProxyTest, RejectsACompiledMaterialForAnOldSchema) MaterialInstance instance(material); EXPECT_FALSE(MaterialRenderProxy::Create(compiled, instance)); } + +TEST(MaterialRenderProxyTest, CapturesDoubleSidedSetting) +{ + const auto material = CreateRef("DoubleSided"); + ASSERT_TRUE(material->SetDoubleSided(true)); + + const auto compiled = Compiler::Build(*material).Material; + ASSERT_TRUE(compiled); + + const MaterialInstance instance(material); + const auto proxy = MaterialRenderProxy::Create(compiled, instance); + + ASSERT_TRUE(proxy); + EXPECT_TRUE(proxy->IsDoubleSided()); +} diff --git a/Elixir/Tests/Engine/Materials/Rendering/RendererTest.cpp b/Elixir/Tests/Engine/Materials/Rendering/RendererTest.cpp index 0f122821..ddaef32a 100644 --- a/Elixir/Tests/Engine/Materials/Rendering/RendererTest.cpp +++ b/Elixir/Tests/Engine/Materials/Rendering/RendererTest.cpp @@ -36,15 +36,15 @@ TEST(RendererTest, MapsParticlePassesToMaterialUsages) { EXPECT_EQ( Renderer::GetUsage(EMaterialPass::ParticleSprite), - EMaterialUsage::ParticleSprite + EMaterialUsage::Particle ); EXPECT_EQ( Renderer::GetUsage(EMaterialPass::ParticleRibbon), - EMaterialUsage::ParticleRibbon + EMaterialUsage::Particle ); EXPECT_EQ( Renderer::GetUsage(EMaterialPass::ParticleMesh), - EMaterialUsage::ParticleMesh + EMaterialUsage::Particle ); } diff --git a/Elixir/Tests/Engine/Materials/Rendering/TextureRegistryTest.cpp b/Elixir/Tests/Engine/Materials/Rendering/TextureRegistryTest.cpp deleted file mode 100644 index d1f79e59..00000000 --- a/Elixir/Tests/Engine/Materials/Rendering/TextureRegistryTest.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include - -#include - -using namespace Elixir; -using namespace Elixir::Materials::Rendering; - -TEST(TextureRegistryTest, UsesFallbackUntilDescriptorIsVisible) -{ - constexpr uint32_t fallbackIndex = 3; - - const STextureBinding binding{ - .Handle = SResourceHandle::Texture(17), - .ReadySubmission = 8, - }; - - EXPECT_EQ(binding.GetIndexForSubmission(7, fallbackIndex), fallbackIndex); - EXPECT_EQ(binding.GetIndexForSubmission(8, fallbackIndex), 17); - EXPECT_EQ(binding.GetIndexForSubmission(9, fallbackIndex), 17); -} diff --git a/Elixir/Tests/Engine/Mesh/StaticMeshLoaderRegistryTest.cpp b/Elixir/Tests/Engine/Mesh/StaticMeshLoaderRegistryTest.cpp new file mode 100644 index 00000000..e090d130 --- /dev/null +++ b/Elixir/Tests/Engine/Mesh/StaticMeshLoaderRegistryTest.cpp @@ -0,0 +1,31 @@ +#include + +#include + +using namespace Elixir; + +namespace +{ + class TestStaticMeshLoader final : public StaticMeshLoader + { + public: + std::optional Load( + const GraphicsContext&, + std::filesystem::path + ) const override + { + return SStaticMeshData{}; + } + }; +} + +TEST(StaticMeshLoaderRegistryTest, RegistrationKeepsOneActiveImplementation) +{ + StaticMeshLoaderRegistry::Shutdown(); + + EXPECT_TRUE(StaticMeshLoaderRegistry::RegisterLoader(CreateScope())); + EXPECT_FALSE(StaticMeshLoaderRegistry::RegisterLoader(CreateScope())); + EXPECT_TRUE(StaticMeshLoaderRegistry::ReplaceLoader(CreateScope())); + + StaticMeshLoaderRegistry::Shutdown(); +} diff --git a/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp b/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp index 52aebb9d..728c5173 100644 --- a/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp +++ b/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp @@ -1,18 +1,17 @@ #include -using namespace testing; -#include -#include -#include -#include +#include #include - +#include +#include #include "VulkanTestContext.h" +#include + using namespace Elixir; using namespace Elixir::Vulkan; -class VulkanImageTest : public Test +class VulkanImageTest : public testing::Test { protected: static void SetUpTestSuite() @@ -20,150 +19,263 @@ class VulkanImageTest : public Test Context = VulkanTestContext::Get().GetGraphicsContext(); } + static std::vector ReadMip(const Ref& image, uint32_t level, uint32_t layer = 0) + { + SBufferCreateInfo bufferInfo{}; + bufferInfo.Buffer = SBuffer(image->GetMipSize(level)); + bufferInfo.Usage = EBufferUsage::TransferDst; + bufferInfo.AllocationInfo.RequiredFlags = EMemoryProperty::HostVisible | EMemoryProperty::HostCoherent; + const Ref buffer = CreateRef(Context, bufferInfo); + auto cmd = Context->GetUploadCommandBuffer(); + const auto layout = image->GetLayout(); + cmd->Begin(); + image->Transition(cmd, EImageLayout::TransferSrc); + VkBufferImageCopy region{}; + region.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, level, layer, 1 }; + const auto extent = image->GetMipExtent(level); + region.imageExtent = { extent.Width, extent.Height, extent.Depth }; + vkCmdCopyImageToBuffer( + static_cast(cmd.get())->GetVulkanCommandBuffer(), + TryToGetVulkanImageHandle(image.get()), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + static_cast(buffer.get())->GetVulkanBuffer(), 1, ®ion + ); + VkMemoryBarrier barrier{ VK_STRUCTURE_TYPE_MEMORY_BARRIER }; + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT; + vkCmdPipelineBarrier( + static_cast(cmd.get())->GetVulkanCommandBuffer(), + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, + 1, &barrier, 0, nullptr, 0, nullptr + ); + image->Transition(cmd, layout); + cmd->Flush(); + const auto* data = static_cast(buffer->Map()); + std::vector result(data, data + image->GetMipSize(level)); + buffer->Unmap(); + return result; + } + static GraphicsContext* Context; }; GraphicsContext* VulkanImageTest::Context = nullptr; -TEST_F(VulkanImageTest, VulkanBaseImage_IsNotConstructibleAndAssignable) -{ - EXPECT_FALSE(std::is_constructible_v>); - EXPECT_FALSE(std::is_copy_constructible_v>); - EXPECT_FALSE(std::is_copy_assignable_v>); - EXPECT_FALSE(std::is_move_constructible_v>); - EXPECT_FALSE(std::is_move_assignable_v>); -} - -TEST_F(VulkanImageTest, VulkanImage_CopyConstructorIsDeleted) +TEST_F(VulkanImageTest, VulkanImageCannotBeCopiedOrMoved) { EXPECT_FALSE(std::is_copy_constructible_v); EXPECT_FALSE(std::is_copy_assignable_v); -} - -TEST_F(VulkanImageTest, VulkanImage_MoveConstructorIsDeleted) -{ EXPECT_FALSE(std::is_move_constructible_v); EXPECT_FALSE(std::is_move_assignable_v); } -TEST_F(VulkanImageTest, VulkanImage_CreationAndDestruction) +TEST_F(VulkanImageTest, CreatesAndDestroysAnImage) { const auto image = Image::Create(Context, EImageFormat::R8G8B8A8_SRGB, 800); - - const auto vk_Image = dynamic_cast*>(image.get()); - ASSERT_TRUE(vk_Image != nullptr); - ASSERT_NE(vk_Image->GetVulkanImage(), VK_NULL_HANDLE); - - EXPECT_EQ(image->GetWidth(), 800); - EXPECT_EQ(image->GetSize(), 800 * 4); + ASSERT_NE(TryToGetVulkanImageHandle(image.get()), VK_NULL_HANDLE); + EXPECT_EQ(image->GetWidth(), 800u); + EXPECT_EQ(image->GetSize(), 800u * 4u); EXPECT_EQ(image->GetType(), EImageType::_1D); EXPECT_EQ(image->GetFormat(), EImageFormat::R8G8B8A8_SRGB); - EXPECT_EQ(image->GetMipLevels(), 1); - EXPECT_EQ(image->GetArrayLayers(), 1); - EXPECT_EQ(image->GetUsage(), EImageUsage::Sampled); + EXPECT_EQ(image->GetMipLevels(), 1u); + EXPECT_EQ( + image->GetUsage(), + EImageUsage::Sampled | EImageUsage::TransferSrc | EImageUsage::TransferDst + ); EXPECT_EQ(image->GetLayout(), EImageLayout::Undefined); - EXPECT_EQ(image->GetAspect(), EImageAspect::Color); - image->Destroy(); - SUCCEED(); + EXPECT_FALSE(image->IsValid()); + EXPECT_NO_THROW(image->Destroy()); + EXPECT_EQ(TryToGetVulkanImageHandle(nullptr), VK_NULL_HANDLE); } -TEST_F(VulkanImageTest, VulkanDepthStencilImage_DepthOnly) +TEST_F(VulkanImageTest, DepthImagesUseTheSameBackend) { - const auto image = DepthStencilImage::Create( - Context, - EDepthStencilImageFormat::D32_SFLOAT, - 800, 600 - ); - - const auto vk_Image = dynamic_cast*>(image.get()); - ASSERT_TRUE(vk_Image != nullptr); - ASSERT_NE(vk_Image->GetVulkanImage(), VK_NULL_HANDLE); - - EXPECT_EQ(image->GetWidth(), 800); - EXPECT_EQ(image->GetHeight(), 600); - EXPECT_EQ(image->GetSize(), 800 * 600 * 4); - EXPECT_EQ(image->GetType(), EImageType::_2D); - EXPECT_EQ(image->GetFormat(), EImageFormat::D32_SFLOAT); - EXPECT_EQ(image->GetMipLevels(), 1); - EXPECT_EQ(image->GetArrayLayers(), 1); - EXPECT_EQ(image->GetUsage(), EImageUsage::Sampled | EImageUsage::DepthStencilAttachment); - EXPECT_EQ(image->GetLayout(), EImageLayout::DepthAttachment); - EXPECT_EQ(image->GetAspect(), EImageAspect::Depth); + for (const auto format : { EImageFormat::D32_SFLOAT, EImageFormat::D32_SFLOAT_S8_UINT }) + { + const bool stencil = format == EImageFormat::D32_SFLOAT_S8_UINT; + SImageCreateInfo info{ + .Width = 64, .Height = 32, .Format = format, + .Usage = EImageUsage::Sampled | EImageUsage::DepthStencilAttachment, + .InitialLayout = stencil ? EImageLayout::DepthStencilAttachment : EImageLayout::DepthAttachment + }; + const auto image = Image::Create(Context, info); + ASSERT_NE(TryToGetVulkanImage(image.get()), nullptr); + EXPECT_EQ(image->GetHeight(), 32u); + EXPECT_EQ(image->GetLayout(), info.InitialLayout); + EXPECT_EQ(image->GetAspect(), stencil ? EImageAspect::Depth | EImageAspect::Stencil : EImageAspect::Depth); + } +} - image->Destroy(); - SUCCEED(); +TEST_F(VulkanImageTest, TextureOwnsAnInitializedImageAndMetadata) +{ + std::array pixels; + pixels.fill(128); + STexture2DCreateInfo info; + info.InitialData = pixels.data(); + info.Format = EImageFormat::R8G8B8A8_UNORM; + info.Width = 8; + info.Height = 4; + info.MipLevels = 99; + info.MipmapMode = EImageMipmapMode::SimpleAverage; + info.Path = "procedural/test"; + info.HDR = true; + auto texture = Texture2D::Create(Context, info); + const auto image = texture->GetImage(); + ASSERT_TRUE(texture->IsValid()); + EXPECT_EQ(texture->GetPath(), info.Path); + EXPECT_TRUE(texture->IsHDR()); + EXPECT_NE(texture->GetUUID(), image->GetUUID()); + EXPECT_EQ(image->GetMipLevels(), 4u); + EXPECT_EQ(image->GetLayout(), EImageLayout::ShaderReadOnly); + EXPECT_EQ(image->GetUsage(), EImageUsage::Sampled | EImageUsage::TransferSrc | EImageUsage::TransferDst); + EXPECT_EQ(ReadMip(image, 3), (std::vector{128, 128, 128, 128})); + texture.reset(); + EXPECT_TRUE(image->IsValid()); } -TEST_F(VulkanImageTest, VulkanDepthStencilImage_DepthStencil) +TEST_F(VulkanImageTest, NormalMapMipsAreGeneratedByImageAndUploaded) { - const auto image = DepthStencilImage::Create( - Context, - EDepthStencilImageFormat::D32_SFLOAT_S8_UINT, - 800, 600 - ); + const std::array pixels{ + 255, 128, 128, 20, 0, 127, 128, 40, + 128, 255, 128, 60, 127, 0, 255, 80 + }; + SImageCreateInfo info{ + .Width = 2, .Height = 2, + .Format = EImageFormat::R8G8B8A8_UNORM, + .MipmapMode = EImageMipmapMode::NormalMap, + .MipLevels = 99, + .Usage = EImageUsage::Sampled | EImageUsage::TransferSrc, + .InitialLayout = EImageLayout::ShaderReadOnly, + .InitialData = pixels.data(), + }; + const auto image = Image::Create(Context, info); + EXPECT_EQ(image->GetMipLevels(), 2u); + EXPECT_EQ(ReadMip(image, 0), (std::vector(pixels.begin(), pixels.end()))); + EXPECT_EQ(ReadMip(image, 1), (std::vector{128, 128, 255, 50})); + const auto description = image->GetCreateInfo(); + EXPECT_EQ(description.MipmapMode, EImageMipmapMode::LeaveExistingMips); + EXPECT_EQ(description.InitialData, nullptr); + EXPECT_TRUE(description.InitialMipData.empty()); + EXPECT_NO_THROW(Image::Create(Context, description)); +} - const auto vk_Image = dynamic_cast*>(image.get()); - ASSERT_TRUE(vk_Image != nullptr); - ASSERT_NE(vk_Image->GetVulkanImage(), VK_NULL_HANDLE); - - EXPECT_EQ(image->GetWidth(), 800); - EXPECT_EQ(image->GetHeight(), 600); - EXPECT_EQ(image->GetSize(), 800 * 600 * 5); - EXPECT_EQ(image->GetType(), EImageType::_2D); - EXPECT_EQ(image->GetFormat(), EImageFormat::D32_SFLOAT_S8_UINT); - EXPECT_EQ(image->GetMipLevels(), 1); - EXPECT_EQ(image->GetArrayLayers(), 1); - EXPECT_EQ(image->GetUsage(), EImageUsage::Sampled | EImageUsage::DepthStencilAttachment); - EXPECT_EQ(image->GetLayout(), EImageLayout::DepthStencilAttachment); - EXPECT_EQ(image->GetAspect(), EImageAspect::Depth | EImageAspect::Stencil); +TEST_F(VulkanImageTest, LeaveExistingMipsUploadsEveryLevelAndLayerWithoutRegeneration) +{ + std::array base; + base.fill(10); + const std::array lower{20, 40, 60, 80}; + const std::array mips{{ + { base.data(), base.size(), 0, 0 }, + { lower.data(), lower.size(), 1, 0 }, + { base.data(), base.size(), 0, 1 }, + { lower.data(), lower.size(), 1, 1 }, + }}; + SImageCreateInfo info{ + .Width = 2, .Height = 2, + .Format = EImageFormat::R8G8B8A8_UNORM, + .MipmapMode = EImageMipmapMode::LeaveExistingMips, + .MipLevels = 2, .ArrayLayers = 2, + .Usage = EImageUsage::Sampled | EImageUsage::TransferSrc, + .InitialLayout = EImageLayout::ShaderReadOnly, + .InitialMipData = mips, + }; + const auto image = Image::Create(Context, info); + EXPECT_EQ(ReadMip(image, 0), (std::vector(base.begin(), base.end()))); + EXPECT_EQ(ReadMip(image, 1), (std::vector(lower.begin(), lower.end()))); + EXPECT_EQ(ReadMip(image, 1, 1), (std::vector(lower.begin(), lower.end()))); +} - image->Destroy(); - SUCCEED(); +TEST_F(VulkanImageTest, SimpleAverageHandlesEveryArrayLayer) +{ + std::array pixels; + std::fill_n(pixels.begin(), 16, 40); + std::fill_n(pixels.begin() + 16, 16, 80); + const auto image = Image::Create(Context, { + .Width = 2, .Height = 2, + .Format = EImageFormat::R8G8B8A8_UNORM, + .MipmapMode = EImageMipmapMode::SimpleAverage, + .ArrayLayers = 2, + .InitialLayout = EImageLayout::ShaderReadOnly, + .InitialData = pixels.data(), + }); + EXPECT_EQ(ReadMip(image, 1, 0), (std::vector(4, 40))); + EXPECT_EQ(ReadMip(image, 1, 1), (std::vector(4, 80))); } -TEST_F(VulkanImageTest, VulkanImage_LayoutTransition) { - SImageCreateInfo info = Image::CreateImageInfo(EImageFormat::R8G8B8A8_UNORM, 64); - info.Usage = EImageUsage::Sampled | EImageUsage::TransferDst; - info.InitialLayout = EImageLayout::TransferDst; +TEST_F(VulkanImageTest, NoMipmapsIgnoresRequestedCount) +{ + auto image = Image::Create(Context, { + .Width = 8, .Height = 4, + .Format = EImageFormat::R8G8B8A8_UNORM, + .MipLevels = 99, + }); + EXPECT_EQ(image->GetMipLevels(), 1u); +} - // Initially the image is transitioned to layout defined in "InitialLayout" - VulkanImage image(Context, info); - EXPECT_EQ(image.GetLayout(), EImageLayout::TransferDst); +TEST_F(VulkanImageTest, TransferOnlyImagesDoNotRequireAView) +{ + const auto image = Image::Create(Context, { + .Width = 4, .Height = 4, .Format = EImageFormat::R8G8B8A8_UNORM, + .Usage = EImageUsage::TransferSrc | EImageUsage::TransferDst + }); + ASSERT_TRUE(image->IsValid()); + EXPECT_EQ(TryToGetVulkanImage(image.get())->GetVulkanImageView(), VK_NULL_HANDLE); +} +TEST_F(VulkanImageTest, TransitionUpdatesTheDescriptorLayout) +{ + auto info = Image::CreateImageInfo(EImageFormat::R8G8B8A8_UNORM, 64); + info.Usage |= EImageUsage::TransferDst; + info.InitialLayout = EImageLayout::TransferDst; + const auto image = Image::Create(Context, info); const auto cmd = Context->GetUploadCommandBuffer(); cmd->Begin(); - - // Now perform a manual transition - image.Transition(cmd, EImageLayout::ShaderReadOnly); + image->Transition(cmd, EImageLayout::ShaderReadOnly); cmd->Flush(); - - EXPECT_EQ(image.GetLayout(), Elixir::EImageLayout::ShaderReadOnly); + EXPECT_EQ(image->GetLayout(), EImageLayout::ShaderReadOnly); + EXPECT_EQ(TryToGetVulkanImage(image.get())->GetVulkanDescriptorInfo().imageLayout, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); } -TEST_F(VulkanImageTest, VulkanImage_ImageDestruction) { - const auto image = Image::Create(Context, EImageFormat::R8G8B8A8_SRGB, 128); - EXPECT_TRUE(image->IsValid()); - - image->Destroy(); - EXPECT_FALSE(image->IsValid()); - - // Calling destroy again should be safe - EXPECT_NO_THROW(image->Destroy()); - EXPECT_FALSE(image->IsValid()); +TEST_F(VulkanImageTest, ResizePreservesContentAndClampsTheAllocatedMipCount) +{ + std::array pixels; + pixels.fill(80); + const auto image = Image::Create(Context, { + .Width = 4, .Height = 4, + .Format = EImageFormat::R8G8B8A8_UNORM, + .MipmapMode = EImageMipmapMode::SimpleAverage, + .InitialLayout = EImageLayout::ShaderReadOnly, + .InitialData = pixels.data(), + }); + ASSERT_TRUE(Context->RunRenderTaskAndWait([image]() { image->Resize({ 2, 2, 1 }); })); + EXPECT_EQ(image->GetWidth(), 2u); + EXPECT_EQ(image->GetMipLevels(), 2u); + EXPECT_EQ(image->GetSize(), 16u); + EXPECT_EQ(image->GetLayout(), EImageLayout::ShaderReadOnly); + EXPECT_EQ(ReadMip(image, 0), (std::vector(16, 80))); + EXPECT_EQ(ReadMip(image, 1), (std::vector(4, 80))); } -TEST_F(VulkanImageTest, TryToGetVulkanImageHandle) { - SImageCreateInfo info = Image::CreateImageInfo(EImageFormat::R8G8B8A8_UNORM, 32); - VulkanImage image(Context, info); - - SImageCreateInfo dsInfo = DepthStencilImage::CreateImageInfo(EDepthStencilImageFormat::D16_UNORM, 64, 64); - VulkanDepthStencilImage dstImage(Context, dsInfo); - - VkImage imgHandle = TryToGetVulkanImageHandle(&image); - VkImage dstImgHandle = TryToGetVulkanImageHandle(&dstImage); - - EXPECT_EQ(imgHandle, image.GetVulkanImage()); - EXPECT_EQ(dstImgHandle, dstImage.GetVulkanImage()); - EXPECT_NE(imgHandle, VK_NULL_HANDLE); +TEST_F(VulkanImageTest, CopyDoesNotChangeDestinationExtent) +{ + std::array pixels{}; + const auto source = Image::Create(Context, { + .Width = 4, .Height = 4, + .Format = EImageFormat::R8G8B8A8_UNORM, + .Usage = EImageUsage::Sampled | EImageUsage::TransferSrc, + .InitialLayout = EImageLayout::TransferSrc, + .InitialData = pixels.data(), + }); + const auto target = Image::Create(Context, { + .Width = 4, .Height = 4, .Format = EImageFormat::R8G8B8A8_UNORM, + .Usage = EImageUsage::Sampled | EImageUsage::TransferDst, + .InitialLayout = EImageLayout::TransferDst, + }); + auto cmd = Context->GetUploadCommandBuffer(); + cmd->Begin(); + source->Copy(cmd, target, { 4, 4, 1 }, { 2, 2, 1 }); + cmd->Flush(); + EXPECT_EQ(target->GetWidth(), 4u); + EXPECT_EQ(target->GetHeight(), 4u); } diff --git a/Shaders/Aether/Mesh.ps.hlsl b/Shaders/Aether/Mesh.ps.hlsl deleted file mode 100644 index 1ac01108..00000000 --- a/Shaders/Aether/Mesh.ps.hlsl +++ /dev/null @@ -1,33 +0,0 @@ -[[vk::binding(0, 0)]] -cbuffer cbFrame : register(b0) -{ - float4x4 View; - float4x4 Proj; - float4x4 ViewProj; - float3 CameraPos; - float _Padding; -}; - -struct PSInput -{ - float4 ClipPos : SV_POSITION; - float4 Color : COLOR0; - float3 Normal : NORMAL0; - float3 WorldPos : POSITION0; -}; - -static const float3 LIGHT_DIRECTION = float3(-0.45, 0.8, 0.55); -static const float3 RIM_COLOR = float3(0.35, 0.42, 0.52); - -float4 main(PSInput input) : SV_Target0 -{ - float3 normal = normalize(input.Normal); - float3 lightDirection = normalize(LIGHT_DIRECTION); - float3 viewDirection = normalize(CameraPos - input.WorldPos); - - float diffuse = max(dot(normal, lightDirection), 0.0); - float rim = pow(1.0 - max(dot(normal, viewDirection), 0.0), 2.8); - float3 litColor = (input.Color.rgb * (0.24 + diffuse * 0.92)) + (RIM_COLOR * rim * 0.35); - - return float4(litColor, input.Color.a); -} \ No newline at end of file diff --git a/Shaders/Aether/Mesh.vs.hlsl b/Shaders/Aether/Mesh.vs.hlsl deleted file mode 100644 index 0b0790c1..00000000 --- a/Shaders/Aether/Mesh.vs.hlsl +++ /dev/null @@ -1,119 +0,0 @@ -float3x3 RotationX(float angle) -{ - float c = cos(angle); - float s = sin(angle); - - return float3x3( - 1.0, 0.0, 0.0, - 0.0, c, -s, - 0.0, s, c - ); -} - -float3x3 RotationY(float angle) -{ - float c = cos(angle); - float s = sin(angle); - - return float3x3( - c, 0.0, s, - 0.0, 1.0, 0.0, - -s, 0.0, c - ); -} - -float3x3 RotationZ(float angle) -{ - float c = cos(angle); - float s = sin(angle); - - return float3x3( - c, -s, 0.0, - s, c, 0.0, - 0.0, 0.0, 1.0 - ); -} - -[[vk::binding(0, 0)]] -cbuffer cbFrame : register(b0) -{ - float4x4 View; - float4x4 Proj; - float4x4 ViewProj; - float3 CameraPos; - float _Padding; -}; - -struct PushConstants -{ - float4x4 WorldTransform; - uint MaterialIndex; -}; - -[[vk::push_constant]] -PushConstants pc; - -struct VSInput -{ - float3 LocalPos : POSITION0; - float3 LocalNormal : NORMAL0; - float4 PositionSize : POSITION1; - float4 VelocityAge : TEXCOORD0; - float4 Transform : TEXCOORD1; - float4 TangentRibbonId : TANGENT; - float4 Color : COLOR; - float4 Metadata : TEXCOORD2; -}; - -struct VSOutput -{ - float4 ClipPos : SV_POSITION; - float4 Color : COLOR0; - float3 Normal : NORMAL0; - float3 WorldPos : POSITION0; -}; - -float Hash01(uint x) -{ - x ^= x >> 16; - x *= 0x7feb352du; - x ^= x >> 15; - x *= 0x846ca68bu; - x ^= x >> 16; - return float(x) * (1.0 / 4294967296.0); // / 2^32 -} - -VSOutput main(VSInput input) -{ - VSOutput output; - - uint id = asuint(input.Metadata.y); - float alive = input.Metadata.w >= 0.5 ? 1.0 : 0.0; - float age = input.VelocityAge.w; - float lifetime = max(input.Metadata.z, 0.001); - float seed = Hash01(id); - float phase = Hash01(id ^ 0x9e3779b9u); - - float scale = lerp(0.045, 0.085, clamp(input.PositionSize.w / 18.0, 0.0, 1.0)); - scale *= max(input.Transform.y, 0.0); - - float baseRotation = input.Transform.x; - float rotX = baseRotation + (seed * 1.7); - float rotY = baseRotation * 0.7 + (phase * 6.28318); - float rotZ = baseRotation * 1.2 + (seed * 2.3); - float3x3 rotation = mul(RotationY(rotY), mul(RotationX(rotX), RotationZ(rotZ))); - - float3 localPos = input.PositionSize.xyz + - mul(mul(rotation, input.LocalPos * scale), alive); - float3 localNormal = normalize(mul(rotation, input.LocalNormal)); - - float3 worldPos = mul(pc.WorldTransform, float4(localPos, 1.0f)).xyz; - float3 worldNormal = normalize(mul((float3x3)pc.WorldTransform, localNormal)); - - output.ClipPos = mul(ViewProj, float4(worldPos, 1.0)); - output.Color = float4(input.Color.rgb, input.Color.a * alive); - output.Normal = worldNormal; - output.WorldPos = worldPos; - - return output; -} \ No newline at end of file diff --git a/Shaders/Aether/Ribbon.ps.hlsl b/Shaders/Aether/Ribbon.ps.hlsl deleted file mode 100644 index 27f134e2..00000000 --- a/Shaders/Aether/Ribbon.ps.hlsl +++ /dev/null @@ -1,19 +0,0 @@ -struct PSInput -{ - float4 ClipPos : SV_POSITION; - float4 Color : COLOR0; - float2 UV : TEXCOORD0; - nointerpolation float Valid : TEXCOORD1; -}; - -float4 main(PSInput input) : SV_Target0 -{ - clip(input.Valid - 0.5); - - float centeredAcrossRibbon = abs((input.UV.x * 2.0) - 1.0); - float edgeFade = 1.0 - smoothstep(0.72, 1.0, centeredAcrossRibbon); - float coreGlow = 1.0 - smoothstep(0.0, 0.52, centeredAcrossRibbon); - float3 color = input.Color.rgb + (coreGlow * 0.22); - - return float4(color, input.Color.a * edgeFade); -} \ No newline at end of file diff --git a/Shaders/Aether/Ribbon.vs.hlsl b/Shaders/Aether/Ribbon.vs.hlsl deleted file mode 100644 index d32cc377..00000000 --- a/Shaders/Aether/Ribbon.vs.hlsl +++ /dev/null @@ -1,146 +0,0 @@ -struct ParticleState -{ - float4 PositionSize; // xyz = position, w = size - float4 VelocityAge; // xyz = velocity, w = age - float4 Transform; // x = rotation, y = scale - float4 TangentRibbonId; // xyz = tangent, w = ribbon id - float4 Color; - float4 Metadata; // x = emitter index, y = ribbon link order, z = lifetime, w = alive -}; - -[[vk::binding(1, 0)]] -StructuredBuffer particles; - -struct Emitter -{ - float4 MetaA; // x = offset in particle buffer, y = max particles, z = module offset(spawn), w = module count(spawn) - float4 MetaB; // x = module offset(update), y = module count(update), z = buffer cursor, w = spawn count - float4 MetaC; // x = render mode, y = spawn rate seconds, z = gravity scale, w = next buffer cursor - float4 MetaD; // x = emission index -}; - - -[[vk::binding(2, 0)]] -StructuredBuffer emitters; - -[[vk::binding(0, 0)]] -cbuffer cbFrame : register(b0) -{ - float4x4 View; - float4x4 Proj; - float4x4 ViewProj; - float3 CameraPos; - float _Padding; -}; - -struct PushConstants -{ - float4x4 WorldTransform; - uint EmitterIndex; - uint ParticleBaseOffset; - uint MaterialIndex; -}; - -[[vk::push_constant]] -PushConstants pc; - -struct VSOutput -{ - float4 ClipPos : SV_POSITION; - float4 Color : COLOR0; - float2 UV : TEXCOORD0; - nointerpolation float Valid : TEXCOORD1; -}; - -#include "Common.hlsl" - -static const float RIBBON_WORLD_SIZE_SCALE = 0.01; - -VSOutput EmptyVertex() -{ - VSOutput output; - output.ClipPos = float4(0.0, 0.0, 0.0, 1.0); - output.Color = float4(0.0, 0.0, 0.0, 0.0); - output.UV = float2(0.5, 0.0); - output.Valid = 0.0; - return output; -} - -// TODO: TEMP approximation, replace by a better solution! -float MaxAxisScale(float4x4 transform) -{ - return max( - length(transform[0].xyz), - max(length(transform[1].xyz), length(transform[2].xyz)) - ); -} - -VSOutput main(uint vertexId : SV_VertexID) -{ - Emitter emitter = emitters[pc.EmitterIndex]; - uint particleCount = (uint)emitter.MetaA.y; - if (particleCount == 0u) - return EmptyVertex(); - - uint segmentIndex = vertexId / 6u; - uint vertexInSegment = vertexId % 6u; - if (segmentIndex >= particleCount) - return EmptyVertex(); - - ParticleState startParticle; - ParticleState endParticle; - uint endLocalIndex; - - if (!TryBuildSegment( - segmentIndex, - pc.ParticleBaseOffset, - emitter, - startParticle, - endParticle, - endLocalIndex - )) - return EmptyVertex(); - - float3x3 worldLinearTransform = (float3x3)pc.WorldTransform; - - startParticle.PositionSize.xyz = - mul(pc.WorldTransform, float4(startParticle.PositionSize.xyz, 1.0f)).xyz; - startParticle.VelocityAge.xyz = - mul(worldLinearTransform, startParticle.VelocityAge.xyz); - startParticle.TangentRibbonId.xyz = - mul(worldLinearTransform, startParticle.TangentRibbonId.xyz); - - endParticle.PositionSize.xyz = - mul(pc.WorldTransform, float4(endParticle.PositionSize.xyz, 1.0f)).xyz; - endParticle.VelocityAge.xyz = - mul(worldLinearTransform, endParticle.VelocityAge.xyz); - endParticle.TangentRibbonId.xyz = - mul(worldLinearTransform, endParticle.TangentRibbonId.xyz); - - float3 p0 = startParticle.PositionSize.xyz; - float3 p1 = endParticle.PositionSize.xyz; - float scale0 = max(startParticle.Transform.y, 0.0); - float scale1 = max(endParticle.Transform.y, 0.0); - float transformScale = MaxAxisScale(pc.WorldTransform); - float width0 = max(startParticle.PositionSize.w * scale0 * transformScale * RIBBON_WORLD_SIZE_SCALE, 0.0001); - float width1 = max(endParticle.PositionSize.w * scale1 * transformScale * RIBBON_WORLD_SIZE_SCALE, 0.0001); - float3 segmentSide = BuildSegmentSide(p0, p1, startParticle, endParticle); - float3 startSide = BuildParticleSide(startParticle, segmentSide); - float3 endSide = BuildParticleSide(endParticle, segmentSide); - - bool useEndParticle = vertexInSegment == 1u || vertexInSegment == 2u || vertexInSegment == 4u; - bool usePositiveSide = vertexInSegment == 2u || vertexInSegment == 4u || vertexInSegment == 5u; - - float3 center = useEndParticle ? p1 : p0; - float width = useEndParticle ? width1 : width0; - float3 side = useEndParticle ? endSide : startSide; - float sideSign = usePositiveSide ? 1.0 : -1.0; - - VSOutput output; - output.ClipPos = mul(ViewProj, float4(center + side * (width * 0.5 * sideSign), 1.0)); - output.Color = useEndParticle ? endParticle.Color : startParticle.Color; - output.UV = float2(usePositiveSide ? 1.0 : 0.0, useEndParticle ? 1.0 : 0.0); - output.Valid = 1.0; - - return output; -} \ No newline at end of file diff --git a/Shaders/Aether/Sprite.ps.hlsl b/Shaders/Aether/Sprite.ps.hlsl deleted file mode 100644 index 1a40bdee..00000000 --- a/Shaders/Aether/Sprite.ps.hlsl +++ /dev/null @@ -1,32 +0,0 @@ -// Global bindless resources (binding 0 = cis, binding 1 = textures, binding 2 = samplers) -[[vk::binding(1, 1)]] -Texture2D sprites[] : register(t0); - -[[vk::binding(1, 0)]] -SamplerState spriteSampler : register(s0); - -struct PushConstants -{ - float4x4 WorldTransform; - uint MaterialIndex; - uint SpriteIndex; -}; -[[vk::push_constant]] -PushConstants pc; - -struct PSInput -{ - float4 ClipPos : SV_POSITION; - float4 Color : COLOR; - float2 TexCoord : TEXCOORD0; -}; - -float4 main(PSInput input) : SV_Target0 -{ - float4 sprite = sprites[pc.SpriteIndex].Sample(spriteSampler, input.TexCoord); - - float3 color = input.Color.rgb * sprite.rgb; - float alpha = input.Color.a * sprite.a; - - return float4(color, alpha); -} \ No newline at end of file diff --git a/Shaders/Material/Material.ps.hlsl b/Shaders/Material/Material.ps.hlsl index 40a1f6b0..e79d8c07 100644 --- a/Shaders/Material/Material.ps.hlsl +++ b/Shaders/Material/Material.ps.hlsl @@ -1,6 +1,14 @@ -// Template pixel shader for node-graph materials. The graph codegen fills the -// surface struct at the __GRAPH_BODY__ marker; the rest is fixed shading shared -// by every graph material (IBL diffuse + specular, ACES tonemap). +// Template pixel shader for node-graph surface materials. +// +// The compiler replaces both markers below. The graph marker provides the +// material values, while the shading-model marker selects Unlit, Lit, or +// ClearCoat at shader-compilation time. + +// __SHADING_MODEL__ + +#define MATERIAL_SHADING_MODEL_UNLIT 0 +#define MATERIAL_SHADING_MODEL_LIT 1 +#define MATERIAL_SHADING_MODEL_CLEAR_COAT 2 [[vk::binding(0, 0)]] cbuffer cbFrame : register(b0) @@ -10,12 +18,10 @@ cbuffer cbFrame : register(b0) float4x4 ViewProj; float3 CameraPos; float Time; - uint EnvIndex; - uint IrradianceIndex; float EnvIntensity; float EnvMaxLod; - uint PrefIndex; uint SceneColorIndex; + uint DebugView; float ScreenWidth; float ScreenHeight; float4 LightDirection; @@ -29,6 +35,8 @@ struct CompiledMaterial { float4 Values[32]; uint TextureIndices[32]; + uint BlendMode; + float AlphaCutoff; }; [[vk::binding(2, 0)]] @@ -37,6 +45,18 @@ StructuredBuffer materials; [[vk::binding(1, 1)]] Texture2D textures[] : register(t0); +[[vk::binding(0, 2)]] +Texture2D environmentTexture : register(t1); + +[[vk::binding(1, 2)]] +Texture2D irradianceTexture : register(t2); + +[[vk::binding(2, 2)]] +Texture2D prefilteredTexture : register(t3); + +[[vk::binding(3, 2)]] +SamplerState environmentSampler : register(s1); + struct PushConstants { float4x4 Model; @@ -52,6 +72,7 @@ struct PSInput float3 Normal : NORMAL0; float4 Tangent : TANGENT0; float2 TexCoord : TEXCOORD0; + float2 TexCoord1 : TEXCOORD1; float3 WorldPos : POSITION0; bool FrontFace : SV_IsFrontFace; }; @@ -64,15 +85,38 @@ struct Surface float Roughness; float Opacity; float3 Emissive; + float AmbientOcclusion; + float Specular; + float3 SpecularColor; + float ClearCoat; + float ClearCoatRoughness; + float3 ClearCoatBottomNormal; }; static const uint NO_TEXTURE = 0xFFFFFFFFu; +static const uint PREFILTER_LEVEL_COUNT = 6u; +static const uint SURFACE_DEBUG_COMPOSITE = 0u; +static const uint SURFACE_DEBUG_BASE_COLOR = 1u; +static const uint SURFACE_DEBUG_DIFFUSE_IBL = 2u; +static const uint SURFACE_DEBUG_SPECULAR_IBL = 3u; +static const uint SURFACE_DEBUG_DIRECT_DIFFUSE = 4u; +static const uint SURFACE_DEBUG_DIRECT_SPECULAR = 5u; +static const uint SURFACE_DEBUG_CLEAR_COAT = 6u; float4 SampleTex(uint index, float2 uv) { return textures[index].Sample(texSampler, uv); } +float3 SampleNormal(uint index, float2 uv) +{ + if (index == NO_TEXTURE) + return float3(0.0f, 0.0f, 1.0f); + + const float3 packedNormal = SampleTex(index, uv).xyz; + return normalize(packedNormal * 2.0f - 1.0f); +} + float2 DirToEquirect(float3 dir) { float u = atan2(dir.z, dir.x) * 0.15915494f + 0.5f; @@ -82,22 +126,129 @@ float2 DirToEquirect(float3 dir) float3 SampleIrradiance(float3 dir) { - if (IrradianceIndex == NO_TEXTURE) - return float3(0.1f, 0.12f, 0.15f); - return textures[IrradianceIndex].SampleLevel(texSampler, DirToEquirect(dir), 0).rgb * EnvIntensity; + return irradianceTexture.SampleLevel( + environmentSampler, + DirToEquirect(dir), + 0 + ).rgb * EnvIntensity; +} + +float3 SampleEnvironment(float3 dir, float lod) +{ + float3 color = environmentTexture.SampleLevel( + environmentSampler, + DirToEquirect(dir), + lod + ).rgb; + + // Limit extreme HDR texels before they are scattered by a glossy BRDF. + // This prevents isolated light sources from becoming fireflies. + return min(color, 3.0f) * EnvIntensity; +} + +float3 SamplePrefilteredLevel(float3 dir, uint level) +{ + uint width = 0; + uint height = 0; + prefilteredTexture.GetDimensions(width, height); + + const float levelCount = EnvMaxLod + 1.0f; + const float blockTexelSize = levelCount / float(height); + + // Each vertical block contains one equirectangular GGX convolution. Keep + // bilinear filtering inside the selected block so it cannot sample another + // roughness level at the top or bottom edge. + float2 uv = DirToEquirect(dir); + + uv.y = clamp(uv.y, 0.5f * blockTexelSize, 1.0f - 0.5f * blockTexelSize); + uv.y = (float(level) + uv.y) / levelCount; + + return prefilteredTexture.SampleLevel( + environmentSampler, + uv, + 0 + ).rgb * EnvIntensity; +} + +float3 SampleSpecular(float3 dir, float roughness) +{ + const float r = saturate(roughness); + const float3 env = SampleEnvironment(dir, r * EnvMaxLod); + if (r < 0.12f) + return env; + + const float prefilterLevel = r * EnvMaxLod; + const uint lowerLevel = uint(floor(prefilterLevel)); + const uint upperLevel = min(lowerLevel + 1u, uint(EnvMaxLod)); + const float blend = frac(prefilterLevel); + + const float3 lower = SamplePrefilteredLevel(dir, lowerLevel); + const float3 upper = SamplePrefilteredLevel(dir, upperLevel); + const float3 prefiltered = lerp(lower, upper, blend); + + return lerp(env, prefiltered, smoothstep(0.12f, 0.35f, r)); +} + +float DistributionGGX(float NdotH, float roughness) +{ + const float alpha = roughness * roughness; + const float alphaSquared = alpha * alpha; + const float denominator = NdotH * NdotH * (alphaSquared - 1.0f) + 1.0f; + return alphaSquared / max(3.14159265359f * denominator * denominator, 1e-7f); } -float3 SampleEnv(float3 dir, float roughness) +float GeometrySchlickGGX(float NdotX, float roughness) { - if (EnvIndex == NO_TEXTURE) - return float3(0.1f, 0.12f, 0.15f); - return textures[EnvIndex].SampleLevel(texSampler, DirToEquirect(dir), roughness * EnvMaxLod).rgb * EnvIntensity; + const float roughnessPlusOne = roughness + 1.0f; + const float k = (roughnessPlusOne * roughnessPlusOne) * 0.125f; + return NdotX / max(NdotX * (1.0f - k) + k, 1e-7f); } -float3 ACESFilm(float3 x) +float GeometrySmith(float NdotV, float NdotL, float roughness) { - const float a = 2.51f, b = 0.03f, c = 2.43f, d = 0.59f, e = 0.14f; - return saturate((x * (a * x + b)) / (x * (c * x + d) + e)); + return GeometrySchlickGGX(NdotV, roughness) * GeometrySchlickGGX(NdotL, roughness); +} + +float3 FresnelSchlick(float VdotH, float3 F0) +{ + return F0 + (1.0f - F0) * pow(saturate(1.0f - VdotH), 5.0f); +} + +float3 FresnelSchlickRoughness(float NdotV, float3 F0, float roughness) +{ + const float3 roughnessF0 = max((1.0f - roughness).xxx, F0); + return F0 + (roughnessF0 - F0) * pow(saturate(1.0f - NdotV), 5.0f); +} + +/** + * Filters specular highlights where the final shading normal changes sharply + * between adjacent screen pixels. + */ +float FilterSpecularRoughness(float roughness, float3 normal) +{ + const float3 ndx = ddx(normal); + const float3 ndy = ddy(normal); + const float variance = max(dot(ndx, ndx), dot(ndy, ndy)); + return sqrt(saturate(roughness * roughness + variance)); +} + +/** + * Kari's analytical approximation for the split-sum environment BRDF. + * + * This replaces a precomputed BRDF LUT while retaining the roughness and + * view-angle response needed by image-based specular lighting. + */ +float2 EnvBRDFApprox(float roughness, float NdotV) +{ + const float4 c0 = float4(-1.0f, -0.0275f, -0.572f, 0.022f); + const float4 c1 = float4(1.0f, 0.0425f, 1.04f, -0.04f); + const float4 coefficients = roughness * c0 + c1; + const float a004 = min( + coefficients.x * coefficients.x, + exp2(-9.28f * NdotV) + ) * coefficients.x + coefficients.y; + + return float2(-1.04f, 1.04f) * a004 + coefficients.zw; } float4 main(PSInput input) : SV_Target0 @@ -117,30 +268,142 @@ float4 main(PSInput input) : SV_Target0 surface.Roughness = 0.5f; surface.Opacity = 1.0f; surface.Emissive = float3(0.0f, 0.0f, 0.0f); + surface.AmbientOcclusion = 1.0f; + surface.Specular = 1.0f; + surface.SpecularColor = float3(1.0f, 1.0f, 1.0f); + surface.ClearCoat = 0.0f; + surface.ClearCoatRoughness = 0.0f; + surface.ClearCoatBottomNormal = float3(0.0f, 0.0f, 1.0f); // __GRAPH_BODY__ + static const uint MATERIAL_BLEND_MASK = 1u; + if (mat.BlendMode == MATERIAL_BLEND_MASK) + { + clip(surface.Opacity - mat.AlphaCutoff); + } + +#if MATERIAL_SHADING_MODEL == MATERIAL_SHADING_MODEL_UNLIT + return float4(surface.BaseColor + surface.Emissive, surface.Opacity); +#endif + float roughness = clamp(surface.Roughness, 0.045f, 1.0f); - float3 F0 = lerp(0.04f.xxx, surface.BaseColor, surface.Metallic); - float NdotV = saturate(dot(N, V)) + 1e-4f; - float3 diffuse = SampleIrradiance(N) * surface.BaseColor * (1.0f - surface.Metallic); - float3 R = reflect(-V, N); + float3 dielectricF0 = min( + 0.04f.xxx * surface.SpecularColor * surface.Specular, + 1.0f.xxx + ); + float3 F0 = lerp(dielectricF0, surface.BaseColor, surface.Metallic); + + // surface.Normal is tangent-space. Transform it into world space through + // the orthonormal tangent basis reconstructed from the mesh vertex data. + float3 tangentNormal = normalize(surface.Normal); + float3 tangent = normalize(input.Tangent.xyz - N * dot(N, input.Tangent.xyz)); + float3 bitangent = cross(N, tangent) * input.Tangent.w; + const float3x3 tangentBasis = float3x3(tangent, bitangent, N); - float3 fresnel = F0 + (max((1.0f - roughness).xxx, F0) - F0) * pow(saturate(1.0f - NdotV), 5.0f); - float3 specular = SampleEnv(R, roughness) * fresnel; +#if MATERIAL_SHADING_MODEL == MATERIAL_SHADING_MODEL_CLEAR_COAT + const float3 coatNormal = normalize(mul(tangentNormal, tangentBasis)); + N = normalize(mul(normalize(surface.ClearCoatBottomNormal), tangentBasis)); +#else + N = normalize(mul(tangentNormal, tangentBasis)); +#endif - float3 color = diffuse + specular + surface.Emissive; + roughness = FilterSpecularRoughness(roughness, N); - // Direcional light: Lambert diffuse + a simple spec. + float3 R = reflect(-V, N); float3 L = normalize(LightDirection.xyz); + float NdotV = saturate(dot(N, V)) + 1e-4f; float NdotL = saturate(dot(N, L)); - float3 H = normalize(V + L); - float spec = pow(saturate(dot(N, H)), max(2.0f, (1.0f - roughness) * 128.0f)); - color += (surface.BaseColor * (1.0f - surface.Metallic) + F0 * spec) * LightColor.rgb * LightColor.w * NdotL; + float ao = saturate(surface.AmbientOcclusion); + + // Image-based lighting uses the split-sum approximation. The irradiance map + // provides the diffuse hemisphere integral, while the prefiltered map and + // analytical BRDF approximate the specular hemisphere integral. + const float3 fresnelIBL = FresnelSchlickRoughness(NdotV, F0, roughness); + const float3 diffuseWeightIBL = (1.0f - fresnelIBL) * (1.0f - surface.Metallic); + const float3 diffuseIBL = SampleIrradiance(N) * surface.BaseColor; + const float2 environmentBRDF = EnvBRDFApprox(roughness, NdotV); + const float3 specularIBL = SampleSpecular(R, roughness) * (F0 * environmentBRDF.x + environmentBRDF.y); + + const float3 diffuseIBLContribution = diffuseWeightIBL * diffuseIBL * ao; + const float3 specularIBLContribution = specularIBL * ao; + float3 directDiffuseContribution = 0.0f.xxx; + float3 directSpecularContribution = 0.0f.xxx; + float3 clearCoatContribution = 0.0f.xxx; + float3 color = diffuseIBLContribution + specularIBLContribution + surface.Emissive; + + // Cook-Torrance microfacet BRDF for the directional light. + if (NdotL > 0.0f) + { + const float3 H = normalize(V + L); + const float NdotH = saturate(dot(N, H)); + const float VdotH = saturate(dot(V, H)); + + const float distribution = DistributionGGX(NdotH, roughness); + const float geometry = GeometrySmith(NdotV, NdotL, roughness); + const float3 fresnelDirect = FresnelSchlick(VdotH, F0); + const float3 specularDirect = (distribution * geometry * fresnelDirect) / + max(4.0f * NdotV * NdotL, 1e-4f); + + const float3 diffuseWeightDirect = (1.0f - fresnelDirect) * (1.0f - surface.Metallic); + const float3 radiance = LightColor.rgb * LightColor.a; + + directDiffuseContribution = diffuseWeightDirect * surface.BaseColor / + 3.14159265359f * radiance * NdotL * ao; + directSpecularContribution = specularDirect * radiance * NdotL * ao; + color += directDiffuseContribution + directSpecularContribution; + } - // Tone mapping - color = ACESFilm(color); +#if MATERIAL_SHADING_MODEL == MATERIAL_SHADING_MODEL_CLEAR_COAT + const float clearCoat = saturate(surface.ClearCoat); + if (clearCoat > 0.0f) + { + float coatRoughness = saturate(surface.ClearCoatRoughness); + const float coatNdotV = saturate(dot(coatNormal, V)) + 1e-4f; + const float coatNdotL = saturate(dot(coatNormal, L)); + + const float3 coatF0 = 0.04f.xxx; + const float coatFresnel = clearCoat * FresnelSchlick(coatNdotV, coatF0).x; + const float2 coatEnvBRDF = EnvBRDFApprox(coatRoughness, coatNdotV); + const float3 coatReflection = SampleSpecular(reflect(-V, coatNormal), coatRoughness) * + (coatF0 * coatEnvBRDF.x + coatEnvBRDF.y); + + clearCoatContribution = coatReflection * clearCoat; + color = color * (1.0f - coatFresnel) + clearCoatContribution; + + if (coatNdotL > 0.0f) + { + const float3 coatHalfVector = normalize(V + L); + const float coatNdotH = saturate(dot(coatNormal, coatHalfVector)); + const float coatVdotH = saturate(dot(V, coatHalfVector)); + + const float coatDistribution = DistributionGGX(coatNdotH, coatRoughness); + const float coatGeometry = GeometrySmith(coatNdotV, coatNdotL, coatRoughness); + const float coatFresnelDirect = clearCoat * FresnelSchlick(coatVdotH, coatF0).x; + const float coatSpecular = coatDistribution * coatGeometry * coatFresnelDirect / + max(4.0f * coatNdotV * coatNdotL, 1e-4f); + + const float3 coatDirectContribution = coatSpecular * LightColor.rgb * + LightColor.a * coatNdotL; + clearCoatContribution += coatDirectContribution; + color += coatDirectContribution; + } + } +#endif + + if (DebugView == SURFACE_DEBUG_BASE_COLOR) + color = surface.BaseColor; + else if (DebugView == SURFACE_DEBUG_DIFFUSE_IBL) + color = diffuseIBLContribution; + else if (DebugView == SURFACE_DEBUG_SPECULAR_IBL) + color = specularIBLContribution; + else if (DebugView == SURFACE_DEBUG_DIRECT_DIFFUSE) + color = directDiffuseContribution; + else if (DebugView == SURFACE_DEBUG_DIRECT_SPECULAR) + color = directSpecularContribution; + else if (DebugView == SURFACE_DEBUG_CLEAR_COAT) + color = clearCoatContribution; return float4(color, surface.Opacity); -} \ No newline at end of file +} diff --git a/Shaders/Material/ParticleMesh.ps.hlsl b/Shaders/Material/ParticleMesh.ps.hlsl index 0b3208de..f26a8a29 100644 --- a/Shaders/Material/ParticleMesh.ps.hlsl +++ b/Shaders/Material/ParticleMesh.ps.hlsl @@ -1,5 +1,5 @@ -// Template for EMaterialUsage::ParticleMesh. The generated graph body writes -// Surface fields using mesh colors, planar UVs, time, material values and textures. +// Shader variant for EMaterialUsage::Particle. The generated graph body writes +// Surface fields using planar UVs, time, material values and textures. [[vk::binding(0, 0)]] cbuffer cbFrame : register(b0) @@ -19,8 +19,10 @@ Texture2D sprites[] : register(t0); struct CompiledMaterial { - float4 Values[32]; - uint TextureIndices[32]; + float4 Values[32]; + uint TextureIndices[32]; + uint BlendMode; + float AlphaCutoff; }; [[vk::binding(2, 0)]] @@ -40,8 +42,9 @@ struct PSInput float4 ClipPos : SV_POSITION; float3 WorldPos : POSITION0; float3 Normal : NORMAL0; - float4 Color : COLOR0; float2 TexCoord : TEXCOORD0; + float2 TexCoord1 : TEXCOORD1; + float4 Color : COLOR; }; static const float3 LIGHT_DIRECTION = float3(-0.45, 0.8, 0.55); @@ -74,17 +77,51 @@ float4 main(PSInput input) : SV_Target0 surface.Opacity = 1.0f; surface.Emissive = float3(0.0f, 0.0f, 0.0f); + const float3 geometricNormal = normalize(input.Normal); + float3 N = geometricNormal; + const float3 V = normalize(CameraPos - input.WorldPos); + // __GRAPH_BODY__ - const float3 normal = normalize(input.Normal); + const float3 positionX = ddx(input.WorldPos); + const float3 positionY = ddy(input.WorldPos); + const float2 texCoordX = ddx(input.TexCoord); + const float2 texCoordY = ddy(input.TexCoord); + const float determinant = texCoordX.x * texCoordY.y - texCoordX.y * texCoordY.x; + + if (abs(determinant) > 1e-6f) + { + const float3 uvTangent = (positionX * texCoordY.y - positionY * texCoordX.y) / determinant; + const float3 uvBitangent = (positionY * texCoordX.x - positionX * texCoordY.x) / determinant; + const float tangentLengthSquared = dot(uvTangent, uvTangent); + + if (tangentLengthSquared > 1e-6f) + { + const float3 tangent = normalize( + uvTangent - geometricNormal * dot(geometricNormal, uvTangent) + ); + const float handedness = dot(cross(tangent, uvBitangent), geometricNormal) < 0.0f + ? -1.0f + : 1.0f; + const float3 bitangent = cross(geometricNormal, tangent) * handedness; + const float3x3 tangentBasis = float3x3(tangent, bitangent, geometricNormal); + N = normalize(mul(normalize(surface.Normal), tangentBasis)); + } + } + + static const uint MATERIAL_BLEND_MASK = 1u; + if (mat.BlendMode == MATERIAL_BLEND_MASK) + { + clip(surface.Opacity - mat.AlphaCutoff); + } + const float3 lightDirection = normalize(LIGHT_DIRECTION); - const float3 viewDirection = normalize(CameraPos - input.WorldPos); - const float diffuse = max(dot(normal, lightDirection), 0.0f); - const float rim = pow(1.0f - max(dot(normal, viewDirection), 0.0f), 2.8f); + const float diffuse = max(dot(N, lightDirection), 0.0f); + const float rim = pow(1.0f - max(dot(N, V), 0.0f), 2.8f); const float3 litColor = (surface.BaseColor * (0.24f + diffuse * 0.92f)) + surface.Emissive + (RIM_COLOR * rim * 0.35f); return float4(litColor, surface.Opacity); -} \ No newline at end of file +} diff --git a/Shaders/Material/ParticleMesh.vs.hlsl b/Shaders/Material/ParticleMesh.vs.hlsl index 1f6b77cd..5996c91d 100644 --- a/Shaders/Material/ParticleMesh.vs.hlsl +++ b/Shaders/Material/ParticleMesh.vs.hlsl @@ -1,5 +1,5 @@ -// Template for EMaterialUsage::ParticleMesh. It mirrors Aether/Mesh.vs.hlsl -// while exposing MaterialIndex and planar UVs to the generated fragment graph. +// Shader variant for EMaterialUsage::Particle. It exposes MaterialIndex and +// planar UVs to the generated fragment graph. float3x3 RotationX(float angle) { @@ -73,8 +73,9 @@ struct VSOutput float4 ClipPos : SV_POSITION; float3 WorldPos : POSITION0; float3 Normal : NORMAL0; - float4 Color : COLOR0; float2 TexCoord : TEXCOORD0; + float2 TexCoord1 : TEXCOORD1; + float4 Color : COLOR; }; float Hash01(uint x) @@ -115,8 +116,9 @@ VSOutput main(VSInput input) output.WorldPos = mul(pc.WorldTransform, float4(localPos, 1.0f)).xyz; output.ClipPos = mul(ViewProj, float4(output.WorldPos, 1.0)); output.Normal = normalize(mul((float3x3)pc.WorldTransform, localNormal)); - output.Color = float4(input.Color.rgb, input.Color.a * alive); output.TexCoord = input.LocalPos.xy * 0.5f + 0.5f; + output.TexCoord1 = output.TexCoord; + output.Color = input.Color; return output; -} \ No newline at end of file +} diff --git a/Shaders/Material/ParticleRibbon.ps.hlsl b/Shaders/Material/ParticleRibbon.ps.hlsl index d034e78f..98bc7d33 100644 --- a/Shaders/Material/ParticleRibbon.ps.hlsl +++ b/Shaders/Material/ParticleRibbon.ps.hlsl @@ -1,5 +1,5 @@ -// Template for EMaterialUsage::ParticleRibbon. The generated graph body writes -// Surface fields using ribbon vertex color, UV, material values and textures. +// Shader variant for EMaterialUsage::Particle. The generated graph body writes +// Surface fields using UV, material values and textures. [[vk::binding(0, 0)]] cbuffer cbFrame : register(b0) @@ -19,8 +19,10 @@ Texture2D sprites[] : register(t0); struct CompiledMaterial { - float4 Values[32]; - uint TextureIndices[32]; + float4 Values[32]; + uint TextureIndices[32]; + uint BlendMode; + float AlphaCutoff; }; [[vk::binding(4, 0)]] @@ -40,9 +42,11 @@ MaterialPushConstants pc; struct PSInput { float4 ClipPos : SV_POSITION; - float4 Color : COLOR0; float2 TexCoord : TEXCOORD0; nointerpolation float Valid : TEXCOORD1; + float2 TexCoord1 : TEXCOORD2; + float3 WorldPos : POSITION0; + float4 Color : COLOR; }; struct Surface @@ -74,15 +78,17 @@ float4 main(PSInput input) : SV_Target0 surface.Opacity = 1.0f; surface.Emissive = float3(0.0f, 0.0f, 0.0f); + float3 V = normalize(CameraPos - input.WorldPos); + float3 N = normalize(cross(ddy(input.WorldPos), ddx(input.WorldPos))); + N = faceforward(N, -V, N); + // __GRAPH_BODY__ - //const float centeredAcrossRibbon = abs((input.TexCoord.x * 2.0f) - 1.0f); - //const float edgeFade = 1.0f - smoothstep(0.72f, 1.0f, centeredAcrossRibbon); - //const float coreGlow = 1.0f - smoothstep(0.0f, 0.52f, centeredAcrossRibbon); -// const float3 color = (input.Color.rgb * surface.BaseColor) + -// surface.Emissive + (coreGlow * 0.22f); - const float3 color = surface.BaseColor + surface.Emissive; + static const uint MATERIAL_BLEND_MASK = 1u; + if (mat.BlendMode == MATERIAL_BLEND_MASK) + { + clip(surface.Opacity - mat.AlphaCutoff); + } -// return float4(color, input.Color.a * edgeFade); - return float4(color, surface.Opacity); -} \ No newline at end of file + return float4(surface.BaseColor + surface.Emissive, surface.Opacity); +} diff --git a/Shaders/Material/ParticleRibbon.vs.hlsl b/Shaders/Material/ParticleRibbon.vs.hlsl index a778db6c..39b2ed4e 100644 --- a/Shaders/Material/ParticleRibbon.vs.hlsl +++ b/Shaders/Material/ParticleRibbon.vs.hlsl @@ -1,4 +1,4 @@ -// Template for EMaterialUsage::ParticleRibbon. +// Shader variant for EMaterialUsage::Particle. struct ParticleState { @@ -50,9 +50,11 @@ PushConstants pc; struct VSOutput { float4 ClipPos : SV_POSITION; - float4 Color : COLOR0; float2 TexCoord : TEXCOORD0; nointerpolation float Valid : TEXCOORD1; + float2 TexCoord1 : TEXCOORD2; + float3 WorldPos : POSITION0; + float4 Color : COLOR; }; float3 SafeNormalize(float3 value, float3 fallback) @@ -190,9 +192,11 @@ VSOutput EmptyVertex() { VSOutput output; output.ClipPos = float4(0.0, 0.0, 0.0, 1.0); - output.Color = float4(0.0, 0.0, 0.0, 0.0); output.TexCoord = float2(0.5, 0.0); output.Valid = 0.0; + output.TexCoord1 = float2(0.5, 0.0); + output.WorldPos = float3(0.0, 0.0, 0.0); + output.Color = float4(0.0, 0.0, 0.0, 0.0); return output; } @@ -267,10 +271,12 @@ VSOutput main(uint vertexId : SV_VertexID) float sideSign = usePositiveSide ? 1.0 : -1.0; VSOutput output; - output.ClipPos = mul(ViewProj, float4(center + side * (width * 0.5 * sideSign), 1.0)); - output.Color = useEndParticle ? endParticle.Color : startParticle.Color; + output.WorldPos = center + side * (width * 0.5 * sideSign); + output.ClipPos = mul(ViewProj, float4(output.WorldPos, 1.0)); output.TexCoord = float2(usePositiveSide ? 1.0 : 0.0, useEndParticle ? 1.0 : 0.0); output.Valid = 1.0; + output.TexCoord1 = output.TexCoord; + output.Color = useEndParticle ? endParticle.Color : startParticle.Color; return output; -} \ No newline at end of file +} diff --git a/Shaders/Material/ParticleSprite.ps.hlsl b/Shaders/Material/ParticleSprite.ps.hlsl index 569063a8..c1573e16 100644 --- a/Shaders/Material/ParticleSprite.ps.hlsl +++ b/Shaders/Material/ParticleSprite.ps.hlsl @@ -1,5 +1,5 @@ -// Template for EMaterialUsage::ParticleSprite. The generated graph body writes -// Surface fields using particle color, UV, time, material values and textures. +// Shader variant for EMaterialUsage::Particle. The generated graph body writes +// Surface fields using UV, time, material values and textures. [[vk::binding(0, 0)]] cbuffer cbFrame : register(b0) @@ -19,8 +19,10 @@ Texture2D sprites[] : register(t0); struct CompiledMaterial { - float4 Values[32]; - uint TextureIndices[32]; + float4 Values[32]; + uint TextureIndices[32]; + uint BlendMode; + float AlphaCutoff; }; [[vk::binding(2, 0)]] @@ -38,8 +40,10 @@ MaterialPushConstants pc; struct PSInput { float4 ClipPos : SV_POSITION; - float4 Color : COLOR; float2 TexCoord : TEXCOORD0; + float2 TexCoord1 : TEXCOORD1; + float3 ViewPos : POSITION0; + float4 Color : COLOR; }; struct Surface @@ -69,7 +73,17 @@ float4 main(PSInput input) : SV_Target0 surface.Opacity = 1.0f; surface.Emissive = float3(0.0f, 0.0f, 0.0f); + float3 V = normalize(-input.ViewPos); + float3 N = normalize(cross(ddy(input.ViewPos), ddx(input.ViewPos))); + N = faceforward(N, -V, N); + // __GRAPH_BODY__ + static const uint MATERIAL_BLEND_MASK = 1u; + if (mat.BlendMode == MATERIAL_BLEND_MASK) + { + clip(surface.Opacity - mat.AlphaCutoff); + } + return float4(surface.BaseColor + surface.Emissive, surface.Opacity); -} \ No newline at end of file +} diff --git a/Shaders/Aether/Sprite.vs.hlsl b/Shaders/Material/ParticleSprite.vs.hlsl similarity index 60% rename from Shaders/Aether/Sprite.vs.hlsl rename to Shaders/Material/ParticleSprite.vs.hlsl index 8c463f04..9464128f 100644 --- a/Shaders/Aether/Sprite.vs.hlsl +++ b/Shaders/Material/ParticleSprite.vs.hlsl @@ -1,3 +1,6 @@ +// Shader variant for EMaterialUsage::Particle. It shares sprite geometry while +// exposing only the varyings used by material particle fragment shaders. + #include "../Quad.hlsl" [[vk::binding(0, 0)]] @@ -32,42 +35,39 @@ struct VSInput struct VSOutput { - float4 ClipPos : SV_POSITION; - float4 Color : COLOR; - float2 TexCoord : TEXCOORD0; + float4 ClipPos : SV_POSITION; + float2 TexCoord : TEXCOORD0; + float2 TexCoord1 : TEXCOORD1; + float3 ViewPos : POSITION0; + float4 Color : COLOR; }; -// TODO: TEMP approximation, replace by a better solution! float MaxAxisScale(float4x4 transform) { return max( length(transform[0].xyz), - max(length(transform[1].xyz),length(transform[2].xyz)) + max(length(transform[1].xyz), length(transform[2].xyz)) ); } VSOutput main(VSInput input, uint vertexId : SV_VertexID) { VSOutput output; - - // Generate quad positions using bit manipulation - float2 normalizedPos = CalculateQuadPosition(vertexId % 6); - - float size = input.PositionSize.w * + const float2 normalizedPos = CalculateQuadPosition(vertexId % 6); + const float size = input.PositionSize.w * max(input.Transform.y, 0.0f) * MaxAxisScale(pc.WorldTransform); - - float3 worldPosition = mul( + const float3 worldPosition = mul( pc.WorldTransform, float4(input.PositionSize.xyz, 1.0f) ).xyz; - - float3 viewPos = mul(View, float4(worldPosition, 1.0f)).xyz; - viewPos.xy += (normalizedPos - 0.5f) * size; // Center around origin + const float3 viewPos = mul(View, float4(worldPosition, 1.0f)).xyz + + float3((normalizedPos - 0.5f) * size, 0.0f); output.ClipPos = mul(Proj, float4(viewPos, 1.0f)); - output.Color = input.Color; output.TexCoord = normalizedPos; - + output.TexCoord1 = normalizedPos; + output.ViewPos = viewPos; + output.Color = input.Color; return output; -} \ No newline at end of file +} diff --git a/Shaders/Material/Surface.vs.hlsl b/Shaders/Material/Surface.vs.hlsl new file mode 100644 index 00000000..0c6183e3 --- /dev/null +++ b/Shaders/Material/Surface.vs.hlsl @@ -0,0 +1,64 @@ +// Template for EMaterialUsage::Surface. + +[[vk::binding(0, 0)]] +cbuffer cbFrame : register(b0) +{ + float4x4 View; + float4x4 Proj; + float4x4 ViewProj; + float3 CameraPos; + float Time; + float EnvIntensity; + float EnvMaxLod; + uint SceneColorIndex; + float ScreenWidth; + float ScreenHeight; + float4 LightDirection; + float4 LightColor; +}; + +struct MaterialPushConstants +{ + float4x4 WorldTransform; + uint MaterialIndex; +}; + +[[vk::push_constant]] +MaterialPushConstants pc; + +struct VSInput +{ + float3 Position : POSITION0; + float3 Normal : NORMAL0; + float4 Tangent : TANGENT0; + float2 TexCoord : TEXCOORD0; + float2 TexCoord1 : TEXCOORD1; +}; + +struct VSOutput +{ + float4 ClipPos : SV_Position; + float3 Normal : NORMAL0; + float4 Tangent : TANGENT0; + float2 TexCoord : TEXCOORD0; + float2 TexCoord1 : TEXCOORD1; + float3 WorldPos : POSITION0; +}; + +VSOutput main(VSInput input) +{ + VSOutput output; + + float3 worldPos = mul(pc.WorldTransform, float4(input.Position, 1.0f)).xyz; + float3 worldNormal = normalize(mul((float3x3)pc.WorldTransform, input.Normal)); + float3 worldTangent = normalize(mul((float3x3)pc.WorldTransform, input.Tangent.xyz)); + + output.ClipPos = mul(ViewProj, float4(worldPos, 1.0)); + output.WorldPos = worldPos; + output.Normal = worldNormal; + output.Tangent = float4(worldTangent, input.Tangent.w); + output.TexCoord = input.TexCoord; + output.TexCoord1 = input.TexCoord1; + + return output; +} \ No newline at end of file diff --git a/Shaders/PostProcessBloom.ps.hlsl b/Shaders/PostProcessBloom.ps.hlsl new file mode 100644 index 00000000..b2b031cf --- /dev/null +++ b/Shaders/PostProcessBloom.ps.hlsl @@ -0,0 +1,76 @@ +[[vk::binding(0, 0)]] +cbuffer cbPostProcess : register(b0) +{ + float2 InverseSceneSize; + float BloomThreshold; + float BloomKnee; + float BloomRadius; + float BloomIntensity; + float Exposure; + float Padding; +}; + +[[vk::binding(0, 1)]] +Texture2D sceneTarget : register(t0); + +[[vk::binding(1, 1)]] +SamplerState postProcessSampler : register(s0); + +struct PSInput +{ + float4 Position : SV_Position; + float2 UV : TEXCOORD0; +}; + +float SanitizeHDRChannel(const float value) +{ + static const float maxHDRValue = 65504.0f; + if (value >= 0.0f && value <= maxHDRValue) + return value; + + return value > maxHDRValue ? maxHDRValue : 0.0f; +} + +float3 ExtractBloom(const float3 color) +{ + const float3 sanitizedColor = float3( + SanitizeHDRChannel(color.r), + SanitizeHDRChannel(color.g), + SanitizeHDRChannel(color.b) + ); + const float brightness = max(sanitizedColor.r, max(sanitizedColor.g, sanitizedColor.b)); + const float knee = max(BloomKnee, 1e-4f); + const float soft = saturate((brightness - BloomThreshold + knee) / (2.0f * knee)); + const float softContribution = soft * soft * knee; + const float contribution = max(brightness - BloomThreshold, softContribution); + return sanitizedColor * contribution / max(brightness, 1e-4f); +} + +float4 main(PSInput input) : SV_Target0 +{ + static const float2 offsets[] = { + float2(-1.0f, -1.0f), float2(0.0f, -1.0f), float2(1.0f, -1.0f), + float2(-1.0f, 0.0f), float2(0.0f, 0.0f), float2(1.0f, 0.0f), + float2(-1.0f, 1.0f), float2(0.0f, 1.0f), float2(1.0f, 1.0f), + }; + static const float weights[] = { + 1.0f, 2.0f, 1.0f, + 2.0f, 4.0f, 2.0f, + 1.0f, 2.0f, 1.0f, + }; + + const float2 radius = InverseSceneSize * BloomRadius; + float3 bloom = 0.0f.xxx; + + [unroll] + for (uint sampleIndex = 0; sampleIndex < 9; ++sampleIndex) + { + const float3 sampleColor = sceneTarget.Sample( + postProcessSampler, + saturate(input.UV + offsets[sampleIndex] * radius) + ).rgb; + bloom += ExtractBloom(sampleColor) * weights[sampleIndex]; + } + + return float4(bloom / 16.0f, 1.0f); +} diff --git a/Shaders/PostProcessBloom.vs.hlsl b/Shaders/PostProcessBloom.vs.hlsl new file mode 100644 index 00000000..d973f308 --- /dev/null +++ b/Shaders/PostProcessBloom.vs.hlsl @@ -0,0 +1,18 @@ +struct VSOutput +{ + float4 Position : SV_Position; + float2 UV : TEXCOORD0; +}; + +VSOutput main(const uint vertexIndex : SV_VertexID) +{ + const float2 uv = float2( + float((vertexIndex << 1u) & 2u), + float(vertexIndex & 2u) + ); + + VSOutput output; + output.Position = float4(uv * 2.0f - 1.0f, 0.0f, 1.0f); + output.UV = uv; + return output; +} diff --git a/Shaders/PostProcessToneMap.ps.hlsl b/Shaders/PostProcessToneMap.ps.hlsl new file mode 100644 index 00000000..81562736 --- /dev/null +++ b/Shaders/PostProcessToneMap.ps.hlsl @@ -0,0 +1,63 @@ +[[vk::binding(0, 0)]] +cbuffer cbPostProcess : register(b0) +{ + float2 InverseSceneSize; + float BloomThreshold; + float BloomKnee; + float BloomRadius; + float BloomIntensity; + float Exposure; + float Padding; +}; + +[[vk::binding(0, 1)]] +Texture2D sceneTarget : register(t0); + +[[vk::binding(1, 1)]] +Texture2D bloomTarget : register(t1); + +[[vk::binding(2, 1)]] +SamplerState postProcessSampler : register(s0); + +struct PSInput +{ + float4 Position : SV_Position; + float2 UV : TEXCOORD0; +}; + +float SanitizeHDRChannel(const float value) +{ + static const float maxHDRValue = 65504.0f; + if (value >= 0.0f && value <= maxHDRValue) + return value; + + return value > maxHDRValue ? maxHDRValue : 0.0f; +} + +float3 ACESFilm(const float3 color) +{ + const float a = 2.51f; + const float b = 0.03f; + const float c = 2.43f; + const float d = 0.59f; + const float e = 0.14f; + return saturate((color * (a * color + b)) / (color * (c * color + d) + e)); +} + +float4 main(PSInput input) : SV_Target0 +{ + const float3 sampledScene = sceneTarget.Sample(postProcessSampler, input.UV).rgb; + const float3 sampledBloom = bloomTarget.Sample(postProcessSampler, input.UV).rgb; + const float3 scene = float3( + SanitizeHDRChannel(sampledScene.r), + SanitizeHDRChannel(sampledScene.g), + SanitizeHDRChannel(sampledScene.b) + ); + const float3 bloom = float3( + SanitizeHDRChannel(sampledBloom.r), + SanitizeHDRChannel(sampledBloom.g), + SanitizeHDRChannel(sampledBloom.b) + ); + const float3 hdrColor = (scene + bloom * BloomIntensity) * exp2(Exposure); + return float4(ACESFilm(hdrColor), 1.0f); +} diff --git a/Shaders/PostProcessToneMap.vs.hlsl b/Shaders/PostProcessToneMap.vs.hlsl new file mode 100644 index 00000000..d973f308 --- /dev/null +++ b/Shaders/PostProcessToneMap.vs.hlsl @@ -0,0 +1,18 @@ +struct VSOutput +{ + float4 Position : SV_Position; + float2 UV : TEXCOORD0; +}; + +VSOutput main(const uint vertexIndex : SV_VertexID) +{ + const float2 uv = float2( + float((vertexIndex << 1u) & 2u), + float(vertexIndex & 2u) + ); + + VSOutput output; + output.Position = float4(uv * 2.0f - 1.0f, 0.0f, 1.0f); + output.UV = uv; + return output; +} diff --git a/Shaders/Shaders.cmake b/Shaders/Shaders.cmake index 4697f713..b45cc846 100644 --- a/Shaders/Shaders.cmake +++ b/Shaders/Shaders.cmake @@ -28,8 +28,8 @@ file(MAKE_DIRECTORY "${SHADER_STAGING_DIR}") # --- Collect shader sources --- -file(GLOB_RECURSE HLSL_FILES "${SHADER_SOURCE_DIR}/*.hlsl") -file(GLOB_RECURSE GLSL_FILES "${SHADER_SOURCE_DIR}/*.glsl") +file(GLOB_RECURSE HLSL_FILES CONFIGURE_DEPENDS "${SHADER_SOURCE_DIR}/*.hlsl") +file(GLOB_RECURSE GLSL_FILES CONFIGURE_DEPENDS "${SHADER_SOURCE_DIR}/*.glsl") # --- Separate compilable shaders from include/utility files --- # Compilable shaders have a stage suffix: Name.{vs,ps,cs,gs,hs,ds}.{hlsl,glsl} @@ -201,14 +201,13 @@ function(copy_shaders_for_targets) COMMAND "${CMAKE_COMMAND}" -E copy_directory "${SHADER_STAGING_DIR}" "${TARGET_SHADER_DIR}" - # The node-graph material templates are compiled at runtime, so its HLSL - # sources must be available next to the compiled shaders. + # Runtime shader compilation needs templates and shared HLSL includes. COMMAND "${CMAKE_COMMAND}" -E copy_directory - "${SHADER_SOURCE_DIR}/Material" - "${TARGET_SHADER_DIR}/Material" + "${SHADER_SOURCE_DIR}" + "${TARGET_SHADER_DIR}" COMMAND "${CMAKE_COMMAND}" -E touch "${CMAKE_CURRENT_BINARY_DIR}/${target}_copy_shaders.stamp" - DEPENDS ${ALL_SPIRV_OUTPUTS} + DEPENDS ${ALL_SPIRV_OUTPUTS} ${HLSL_FILES} ${GLSL_FILES} COMMENT "Copying compiled shaders to ${TARGET_SHADER_DIR}..." VERBATIM ) diff --git a/Shaders/StaticMesh.ps.hlsl b/Shaders/StaticMesh.ps.hlsl new file mode 100644 index 00000000..14266894 --- /dev/null +++ b/Shaders/StaticMesh.ps.hlsl @@ -0,0 +1,10 @@ +struct PSInput +{ + float4 Position : SV_Position; + float4 Color : COLOR0; +}; + +float4 main(PSInput input) : SV_Target0 +{ + return input.Color; +} diff --git a/Shaders/StaticMesh.vs.hlsl b/Shaders/StaticMesh.vs.hlsl new file mode 100644 index 00000000..dd689ae8 --- /dev/null +++ b/Shaders/StaticMesh.vs.hlsl @@ -0,0 +1,35 @@ +[[vk::binding(0, 0)]] +cbuffer cbFrame : register(b0) +{ + float4x4 ViewProjection; +}; + +struct PushConstants +{ + float4 Color; +}; + +[[vk::push_constant]] +PushConstants pc; + +struct VSInput +{ + float3 Position : POSITION0; + float3 Normal : NORMAL0; + float4 Tangent : TANGENT0; + float2 TexCoord : TEXCOORD0; +}; + +struct VSOutput +{ + float4 Position : SV_Position; + float4 Color : COLOR0; +}; + +VSOutput main(VSInput input) +{ + VSOutput output; + output.Position = mul(ViewProjection, float4(input.Position, 1.0f)); + output.Color = pc.Color; + return output; +}