From 0a0c92d6899151c494b8f66126a02d55f3263b92 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Tue, 8 Sep 2026 01:08:57 -0300 Subject: [PATCH 01/61] Add static mesh loading --- Elixir/Source/Engine.h | 4 + Elixir/Source/Engine/Core/Application.cpp | 4 + Elixir/Source/Engine/Mesh/StaticMesh.cpp | 26 ++ Elixir/Source/Engine/Mesh/StaticMesh.h | 98 +++++ Elixir/Source/Engine/Mesh/StaticMeshLoader.h | 75 ++++ .../Engine/Mesh/StaticMeshLoaderRegistry.cpp | 89 ++++ .../Engine/Mesh/StaticMeshLoaderRegistry.h | 55 +++ .../Platform/GLTF/GLTFStaticMeshLoader.cpp | 386 ++++++++++++++++++ .../Platform/GLTF/GLTFStaticMeshLoader.h | 43 ++ .../Mesh/StaticMeshLoaderRegistryTest.cpp | 30 ++ 10 files changed, 810 insertions(+) create mode 100644 Elixir/Source/Engine/Mesh/StaticMesh.cpp create mode 100644 Elixir/Source/Engine/Mesh/StaticMesh.h create mode 100644 Elixir/Source/Engine/Mesh/StaticMeshLoader.h create mode 100644 Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp create mode 100644 Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.h create mode 100644 Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp create mode 100644 Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h create mode 100644 Elixir/Tests/Engine/Mesh/StaticMeshLoaderRegistryTest.cpp diff --git a/Elixir/Source/Engine.h b/Elixir/Source/Engine.h index 3ca022df..c04c259c 100644 --- a/Elixir/Source/Engine.h +++ b/Elixir/Source/Engine.h @@ -60,5 +60,9 @@ #include #include +#include +#include +#include + #include #include diff --git a/Elixir/Source/Engine/Core/Application.cpp b/Elixir/Source/Engine/Core/Application.cpp index 4c37f1e0..de939476 100644 --- a/Elixir/Source/Engine/Core/Application.cpp +++ b/Elixir/Source/Engine/Core/Application.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include namespace Elixir @@ -49,6 +50,8 @@ namespace Elixir SMaterialSystemConfig{ .InitialFrameCapacity = 256 } ); + StaticMeshLoaderRegistry::Initialize(*m_GraphicsContext); + m_GUIManager = CreateScope(); m_GUIManager->Initialize( m_GraphicsContext.get(), @@ -146,6 +149,7 @@ namespace Elixir Application::~Application() { EE_PROFILE_ZONE_SCOPED() + StaticMeshLoaderRegistry::Shutdown(); IconManager::Shutdown(); FontManager::Shutdown(); Platform::Shutdown(); diff --git a/Elixir/Source/Engine/Mesh/StaticMesh.cpp b/Elixir/Source/Engine/Mesh/StaticMesh.cpp new file mode 100644 index 00000000..5cda6c83 --- /dev/null +++ b/Elixir/Source/Engine/Mesh/StaticMesh.cpp @@ -0,0 +1,26 @@ +#include "epch.h" +#include "StaticMesh.h" + +namespace Elixir +{ + Ref StaticMesh::Create(SStaticMeshCreateInfo info) + { + const bool hasValidSection = std::ranges::any_of(info.Sections, [](const auto& section) + { + return section.Vertices && section.Indices && section.IndexCount > 0; + }); + + if (!hasValidSection) + { + EE_CORE_ERROR("StaticMesh requires at least one valid section.") + return nullptr; + } + + return Ref(new StaticMesh(std::move(info))); + } + + StaticMesh::StaticMesh(SStaticMeshCreateInfo info) + : m_Name(std::move(info.Name)), + m_Sections(std::move(info.Sections)), + m_LocalBounds(info.LocalBounds) {} +} \ No newline at end of file diff --git a/Elixir/Source/Engine/Mesh/StaticMesh.h b/Elixir/Source/Engine/Mesh/StaticMesh.h new file mode 100644 index 00000000..7490f481 --- /dev/null +++ b/Elixir/Source/Engine/Mesh/StaticMesh.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include + +namespace Elixir +{ + /** @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{}; + }; + + /** @brief Defines an axis-aligned bound in mesh-local space. */ + struct SStaticMeshBounds + { + glm::vec3 Min{}; + glm::vec3 Max{}; + }; + + /** @brief Describes one drawable triangle range of a static mesh. */ + struct SStaticMeshSection + { + /** Vertex data used by this section. */ + Ref Vertices; + + /** Triangle indices used by this section. */ + Ref Indices; + + /** Number of indices in this section. */ + uint32_t IndexCount = 0; + + /** Slot of the material assigned by the source asset. */ + uint32_t MaterialSlot = std::numeric_limits::max(); + + /** Bound of this section in mesh-local space. */ + SStaticMeshBounds LocalBounds; + }; + + /** @brief Supplies immutable data for a new static mesh. */ + struct SStaticMeshCreateInfo + { + /** Optional display name of the mesh. */ + std::string Name; + + /** Sections that form the mesh. */ + std::vector Sections; + + /** Bound of all sections in mesh-local space. */ + SStaticMeshBounds LocalBounds; + }; + + /** + * @brief Stores reusable, non-deforming geometry. + * + * A static mesh contains no scene transform. Its sections correspond to source draw + * primitives. + */ + class ELIXIR_API StaticMesh final + { + public: + /** Identifies an unassigned material slot. */ + static constexpr uint32_t NO_MATERIAL_SLOT = std::numeric_limits::max(); + + /** + * @brief Create one static mesh from prepared GPU sections. + * @param info Immutable geometry and metadata for the mesh. + * @return A mesh, or nullptr when no valid section was supplied. + */ + static Ref Create(SStaticMeshCreateInfo info); + + /** @brief Get the display name supplied by the creator. */ + const std::string& GetName() const { return m_Name; } + + /** @brief Get the immutable sections of this mesh. */ + const std::vector& GetSections() const { return m_Sections; } + + /** @brief Get the mesh bound in mesh-local space. */ + const SStaticMeshBounds& GetLocalBounds() const { return m_LocalBounds; } + + private: + explicit StaticMesh(SStaticMeshCreateInfo info); + + std::string m_Name; + std::vector m_Sections; + SStaticMeshBounds m_LocalBounds; + }; +} \ No newline at end of file diff --git a/Elixir/Source/Engine/Mesh/StaticMeshLoader.h b/Elixir/Source/Engine/Mesh/StaticMeshLoader.h new file mode 100644 index 00000000..d0a075c1 --- /dev/null +++ b/Elixir/Source/Engine/Mesh/StaticMeshLoader.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include + +namespace Elixir +{ + class GraphicsContext; + + /** @brief Identifies a source format supported by a static mesh loader. */ + enum class EStaticMeshFormat : uint8_t + { + GLTF, + }; + + /** @brief Supplies the engine state and source file for one mesh import. */ + struct SStaticMeshLoadRequest + { + const GraphicsContext* GraphicsContext = nullptr; + std::filesystem::path Path; + }; + + /** @brief Marks the severity of one static mesh import diagnostic. */ + enum class EStaticMeshLoadDiagnosticSeverity : uint8_t + { + Warning, Error, + }; + + /** @brief Reports an import condition without exposing a parser implementation. */ + struct SStaticMeshLoadDiagnostic + { + EStaticMeshLoadDiagnosticSeverity Severity = EStaticMeshLoadDiagnosticSeverity::Error; + std::string Message; + }; + + /** @brief Contains all static meshes produced from one source file. */ + struct SStaticMeshLoadResult + { + /** Imported meshes, one for each valid source mesh. */ + std::vector> Meshes; + + /** Warnings and errors produced while importing. */ + std::vector Diagnostics; + + /** Check whether the import produced an error. */ + bool HasErrors() const + { + return std::ranges::any_of(Diagnostics, [](const auto& diagnostic) + { + return diagnostic.Severity == EStaticMeshLoadDiagnosticSeverity::Error; + }); + } + }; + + /** + * @brief Imports one static mesh source format into engine assets. + * + * Implementations must not expose parser-specific types through this contract. + */ + class ELIXIR_API StaticMeshLoader + { + public: + virtual ~StaticMeshLoader() = default; + + /** Get the source format accepted by this loader. */ + virtual EStaticMeshFormat GetFormat() const = 0; + + /** + * @brief Import all valid static meshes from one source file. + * @param request Source file and graphics context. + * @return Imported meshes and diagnostics. + */ + virtual SStaticMeshLoadResult Load(const SStaticMeshLoadRequest& request) const = 0; + }; +} \ No newline at end of file diff --git a/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp b/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp new file mode 100644 index 00000000..5888ce45 --- /dev/null +++ b/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp @@ -0,0 +1,89 @@ +#include "epch.h" +#include "StaticMeshLoaderRegistry.h" + +#include +#include + +namespace Elixir +{ + namespace + { + const GraphicsContext* s_GraphicsContext; + std::unordered_map> s_Loaders; + + SStaticMeshLoadResult MakeError(const std::string& message) + { + EE_CORE_ERROR("{}", message) + return { + .Diagnostics = {{ EStaticMeshLoadDiagnosticSeverity::Error, message }} + }; + } + } + + void StaticMeshLoaderRegistry::Initialize(const GraphicsContext& context) + { + if (s_GraphicsContext == &context) return; + + s_GraphicsContext = &context; + s_Loaders.clear(); + RegisterLoader(CreateScope()); + } + + void StaticMeshLoaderRegistry::Shutdown() + { + s_Loaders.clear(); + s_GraphicsContext = nullptr; + } + + bool StaticMeshLoaderRegistry::RegisterLoader(Scope loader) + { + if (!loader) return false; + + const EStaticMeshFormat format = loader->GetFormat(); + if (s_Loaders.contains(format)) return false; + + s_Loaders.emplace(format, std::move(loader)); + return true; + } + + bool StaticMeshLoaderRegistry::ReplaceLoader(Scope loader) + { + if (!loader) return false; + + s_Loaders[loader->GetFormat()] = std::move(loader); + return true; + } + + SStaticMeshLoadResult StaticMeshLoaderRegistry::Load(const std::filesystem::path& path) + { + if (!s_GraphicsContext) + return MakeError("StaticMeshLoaderRegistry must be initialized before loading meshes."); + + const auto format = InferFormat(path); + if (!format) + return MakeError(std::format("Unsupported static mesh format: {}.", path.string())); + + const auto loader = s_Loaders.find(*format); + if (loader == s_Loaders.end()) + return MakeError(std::format("No static mesh loader is registered for: {}.", path.string())); + + return loader->second->Load({ + .GraphicsContext = s_GraphicsContext, + .Path = path + }); + } + + std::optional StaticMeshLoaderRegistry::InferFormat( + const std::filesystem::path& path + ) + { + std::string extension = path.extension().string(); + std::ranges::transform(extension, extension.begin(), [](const auto character) + { + return (char)std::tolower(character); + }); + + if (extension == ".gltf" || extension == ".glb") return EStaticMeshFormat::GLTF; + return std::nullopt; + } +} \ No newline at end of file diff --git a/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.h b/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.h new file mode 100644 index 00000000..ee2254c1 --- /dev/null +++ b/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.h @@ -0,0 +1,55 @@ +#pragma once + +#include + +namespace Elixir +{ + class GraphicsContext; + + /** + * @brief Loads static mesh files through registered format loaders. + * + * The registry installs the glTF loader by default. Applications can add another + * format loader or explicitly replace the implementation for an existing format. + */ + 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 registered loaders and the current graphics context. */ + static void Shutdown(); + + /** + * @brief Register a loader for a previously unsupported source format. + * @param loader Loader to register. + * @return False when loader is null or its format is already registered. + */ + static bool RegisterLoader(Scope loader); + + /** + * @brief Replace the loader registered for one source format. + * @param loader Loader that becomes responsible for its declared format. + * @return False when loader is null. + */ + static bool ReplaceLoader(Scope loader); + + /** + * @brief Import every static mesh defined by one supported source file. + * @param path Local source file. + * @return Imported meshes and diagnostics. + */ + static SStaticMeshLoadResult Load(const std::filesystem::path& path); + + private: + static std::optional InferFormat( + const std::filesystem::path& path + ); + }; +} \ 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..a4534b8a --- /dev/null +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp @@ -0,0 +1,386 @@ +#include "epch.h" +#include "GLTFStaticMeshLoader.h" + +#include +#include + +namespace Elixir +{ + namespace + { + void AddDiagnostic( + SStaticMeshLoadResult& result, + const EStaticMeshLoadDiagnosticSeverity severity, + std::string message + ) + { + if (severity == EStaticMeshLoadDiagnosticSeverity::Error) + EE_CORE_ERROR("{}", message) + else + EE_CORE_WARN("{}", message); + result.Diagnostics.push_back({ severity, std::move(message) }); + } + + BufferLayout GetStaticMeshVertexLayout() + { + return {{ + { EDataType::Vec3, "a_Position" }, + { EDataType::Vec3, "a_Normal" }, + { EDataType::Vec4, "a_Tangent" }, + { EDataType::Vec2, "a_TexCoord" }, + }}; + } + + SStaticMeshBounds 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::min(bounds.Max, vertex.Position); + } + + return bounds; + } + + void ExpandBounds(SStaticMeshBounds& target, const SStaticMeshBounds& source) + { + target.Min = glm::min(target.Min, source.Min); + target.Max = glm::max(target.Max, source.Max); + } + } + + 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; + + SStaticMeshLoadResult GLTFStaticMeshLoader::Load( + const SStaticMeshLoadRequest& request + ) const + { + SStaticMeshLoadResult result; + + if (!request.GraphicsContext) + { + AddDiagnostic( + result, + EStaticMeshLoadDiagnosticSeverity::Error, + "Cannot load a static mesh without a graphics context." + ); + return result; + } + + auto data = fastgltf::GltfDataBuffer::FromPath(request.Path); + if (data.error() != fastgltf::Error::None) + { + AddDiagnostic( + result, + EStaticMeshLoadDiagnosticSeverity::Error, + std::format( + "Failed to open glTF file '{}': {}.", + request.Path.string(), + fastgltf::getErrorMessage(data.error()) + ) + ); + return result; + } + + fastgltf::Parser parser; + + auto loadResult = parser.loadGltf( + data.get(), + request.Path.parent_path(), + GLTF_OPTIONS, + GLTF_CATEGORIES + ); + + if (loadResult.error() != fastgltf::Error::None) + { + AddDiagnostic( + result, + EStaticMeshLoadDiagnosticSeverity::Error, + std::format( + "Failed to parse glTF file '{}': {}.", + request.Path.string(), + fastgltf::getErrorMessage(loadResult.error()) + ) + ); + return result; + } + + const fastgltf::Asset& asset = loadResult.get(); + + for (auto& mesh : asset.meshes) + LoadMesh(request, result, asset, mesh); + + if (result.Meshes.empty() && !result.HasErrors()) + { + AddDiagnostic( + result, + EStaticMeshLoadDiagnosticSeverity::Error, + std::format( + "No valid static meshes were found in '{}'.", + request.Path.string() + ) + ); + } + + return result; + } + + void GLTFStaticMeshLoader::LoadMesh( + const SStaticMeshLoadRequest& request, + SStaticMeshLoadResult& result, + const fastgltf::Asset& asset, + const fastgltf::Mesh& mesh + ) + { + SStaticMeshCreateInfo info; + info.Name = std::string(mesh.name); + + bool hasBounds = false; + + for (const auto& primitive : mesh.primitives) + LoadPrimitive(request, result, info, asset, primitive, hasBounds); + + if (const auto staticMesh = StaticMesh::Create(std::move(info))) + result.Meshes.push_back(staticMesh); + else + AddDiagnostic( + result, + EStaticMeshLoadDiagnosticSeverity::Error, + std::format( + "Skipping mesh '{}' because it has no valid triangle sections.", + info.Name + ) + ); + } + + void GLTFStaticMeshLoader::LoadPrimitive( + const SStaticMeshLoadRequest& request, + SStaticMeshLoadResult& result, + SStaticMeshCreateInfo& mesh, + const fastgltf::Asset& asset, + const fastgltf::Primitive& primitive, + bool& hasBounds + ) + { + if (primitive.type != fastgltf::PrimitiveType::Triangles) + { + AddDiagnostic( + result, + EStaticMeshLoadDiagnosticSeverity::Warning, + std::format( + "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()) + { + AddDiagnostic( + result, + EStaticMeshLoadDiagnosticSeverity::Warning, + std::format( + "Skipping primitive in mesh '{}': POSITION is missing.", + mesh.Name + ) + ); + return; + } + + const auto& positionAccessor = asset.accessors[position->accessorIndex]; + if (positionAccessor.count > std::numeric_limits::max()) + { + AddDiagnostic( + result, + EStaticMeshLoadDiagnosticSeverity::Warning, + std::format( + "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()) + { + AddDiagnostic( + result, + EStaticMeshLoadDiagnosticSeverity::Warning, + std::format( + "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() }; + } + ); + + 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()) + { + AddDiagnostic( + result, + EStaticMeshLoadDiagnosticSeverity::Warning, + std::format( + "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()) + { + AddDiagnostic( + result, + EStaticMeshLoadDiagnosticSeverity::Warning, + std::format( + "Skipping empty primitive in mesh '{}'.", + mesh.Name + ) + ); + return; + } + + SStaticMeshSection section; + section.IndexCount = (uint32_t)indices.size(); + section.MaterialSlot = primitive.materialIndex && + *primitive.materialIndex <= StaticMesh::NO_MATERIAL_SLOT - 1 + ? (uint32_t)*primitive.materialIndex + : StaticMesh::NO_MATERIAL_SLOT; + section.LocalBounds = GetBounds(vertices); + section.Vertices = VertexBuffer::Create( + request.GraphicsContext, + vertices.size() * sizeof(SStaticMeshVertex), + vertices.data() + ); + section.Indices = IndexBuffer::Create( + request.GraphicsContext, + indices.size() * sizeof(uint32_t), + indices.data(), + EIndexType::UInt32 + ); + + if (!section.Vertices || !section.Indices) + { + AddDiagnostic( + result, + EStaticMeshLoadDiagnosticSeverity::Error, + std::format( + "Failed to create GPU buffers for primitive in mesh '{}'.", + mesh.Name + ) + ); + return; + } + + section.Vertices->SetLayout(GetStaticMeshVertexLayout()); + + if (!hasBounds) + { + mesh.LocalBounds = section.LocalBounds; + hasBounds = true; + } + else + { + ExpandBounds(mesh.LocalBounds, section.LocalBounds); + } + + mesh.Sections.push_back(std::move(section)); + } +} \ No newline at end of file diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h new file mode 100644 index 00000000..6abd3d08 --- /dev/null +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h @@ -0,0 +1,43 @@ +#pragma once + +#include "fastgltf/types.hpp" + +#include + +namespace fastgltf { class Mesh; } + +namespace Elixir +{ + /** @brief Imports static meshes from glTF and GLB source files. */ + class GLTFStaticMeshLoader final : public StaticMeshLoader + { + public: + /** + * @brief Get the glTF source format supported by this loader */ + EStaticMeshFormat GetFormat() const override { return EStaticMeshFormat::GLTF; } + + /** + * @brief Import all static meshes stored in one glTF or GLB file. + * @param request Source file and graphics context. + * @return Imported meshes and diagnostics. + */ + SStaticMeshLoadResult Load(const SStaticMeshLoadRequest& request) const override; + + private: + static void LoadMesh( + const SStaticMeshLoadRequest& request, + SStaticMeshLoadResult& result, + const fastgltf::Asset& asset, + const fastgltf::Mesh& mesh + ); + + static void LoadPrimitive( + const SStaticMeshLoadRequest& request, + SStaticMeshLoadResult& result, + SStaticMeshCreateInfo& mesh, + const fastgltf::Asset& asset, + const fastgltf::Primitive& primitive, + bool& hasBounds + ); + }; +} \ No newline at end of file diff --git a/Elixir/Tests/Engine/Mesh/StaticMeshLoaderRegistryTest.cpp b/Elixir/Tests/Engine/Mesh/StaticMeshLoaderRegistryTest.cpp new file mode 100644 index 00000000..86129edf --- /dev/null +++ b/Elixir/Tests/Engine/Mesh/StaticMeshLoaderRegistryTest.cpp @@ -0,0 +1,30 @@ +#include + +#include + +using namespace Elixir; + +namespace +{ + class TestGltfStaticMeshLoader final : public StaticMeshLoader + { + public: + EStaticMeshFormat GetFormat() const override { return EStaticMeshFormat::GLTF; } + + SStaticMeshLoadResult Load(const SStaticMeshLoadRequest&) const override + { + return {}; + } + }; +} + +TEST(StaticMeshLoaderRegistryTest, RegistrationRejectsDuplicatesAndAllowsReplacement) +{ + StaticMeshLoaderRegistry::Shutdown(); + + EXPECT_TRUE(StaticMeshLoaderRegistry::RegisterLoader(CreateScope())); + EXPECT_FALSE(StaticMeshLoaderRegistry::RegisterLoader(CreateScope())); + EXPECT_TRUE(StaticMeshLoaderRegistry::ReplaceLoader(CreateScope())); + + StaticMeshLoaderRegistry::Shutdown(); +} \ No newline at end of file From 291e058f2107331e103442074adfb4c4bdce7c61 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 9 Sep 2026 00:04:56 -0300 Subject: [PATCH 02/61] Add static mesh rendering --- Dissolve/Source/Dissolve.cpp | 79 +----- Dissolve/Source/Dissolve.h | 13 +- Elixir/Source/Engine.h | 1 + Elixir/Source/Engine/Mesh/StaticMesh.cpp | 13 +- Elixir/Source/Engine/Mesh/StaticMesh.h | 5 +- Elixir/Source/Engine/Mesh/StaticMeshLoader.h | 9 - .../Engine/Mesh/StaticMeshLoaderRegistry.cpp | 40 +-- .../Engine/Mesh/StaticMeshLoaderRegistry.h | 14 +- .../Source/Engine/Mesh/StaticMeshRenderer.cpp | 121 +++++++++ .../Source/Engine/Mesh/StaticMeshRenderer.h | 45 ++++ .../Platform/GLTF/GLTFStaticMeshLoader.cpp | 250 ++++++++++-------- .../Platform/GLTF/GLTFStaticMeshLoader.h | 32 +-- .../Mesh/StaticMeshLoaderRegistryTest.cpp | 14 +- Shaders/StaticMesh.ps.hlsl | 10 + Shaders/StaticMesh.vs.hlsl | 35 +++ 15 files changed, 404 insertions(+), 277 deletions(-) create mode 100644 Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp create mode 100644 Elixir/Source/Engine/Mesh/StaticMeshRenderer.h create mode 100644 Shaders/StaticMesh.ps.hlsl create mode 100644 Shaders/StaticMesh.vs.hlsl diff --git a/Dissolve/Source/Dissolve.cpp b/Dissolve/Source/Dissolve.cpp index 5b450d6f..b4df7572 100644 --- a/Dissolve/Source/Dissolve.cpp +++ b/Dissolve/Source/Dissolve.cpp @@ -3,7 +3,6 @@ #include "Engine/Materials/Nodes/Parameter.h" #include -#include #include #include @@ -16,20 +15,20 @@ using namespace Elixir::Materials::Nodes; -Ref pipeline; std::array, 2> m_ParticleSystems; std::array, 2> m_ParticleSystemInstances; Ref graphMaterial; +SStaticMeshLoadResult mesh; + 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,35 +37,13 @@ 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_StaticMeshRenderer = CreateScope( m_GraphicsContext.get(), - sizeof(SFrameData), - &m_FrameData + m_ShaderLoader.get() ); - shader->BindConstantBuffer("cbFrame", m_FrameConstantBuffer); - m_ParticleSystems[0] = GetAetherManager().LoadEffect("./Assets/VFX/FireAndFireworks.json"); EE_CORE_ASSERT( m_ParticleSystems[0], @@ -126,6 +103,8 @@ Dissolve::Dissolve() { EE_CORE_ERROR("Dissolve particle emitter 'FlameCore' was not found.") } + + mesh = StaticMeshLoaderRegistry::Load("./Assets/Meshes/McLaren/scene.gltf"); } { @@ -216,7 +195,7 @@ Dissolve::Dissolve() Dissolve::~Dissolve() { - pipeline.reset(); + m_StaticMeshRenderer.reset(); } void Dissolve::OnGUI(const Timestep frameTime) @@ -237,19 +216,14 @@ 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(); + DrawGeometry(); aether.Render(m_CameraController->GetCamera()); - const auto& simulationMetrics = aether.GetLastSimulationMetrics(); - const auto& renderMetrics = aether.GetLastRenderingMetrics(); } void Dissolve::OnEvent(Event& event) @@ -260,37 +234,8 @@ 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.Meshes, m_CameraController->GetCamera()); } Application* Elixir::CreateApplication() diff --git a/Dissolve/Source/Dissolve.h b/Dissolve/Source/Dissolve.h index d7c3c3e7..52a18707 100644 --- a/Dissolve/Source/Dissolve.h +++ b/Dissolve/Source/Dissolve.h @@ -2,11 +2,6 @@ #include -struct SFrameData -{ - glm::mat4 ViewProj; -}; - class Dissolve final : public Elixir::Application { public: @@ -22,11 +17,7 @@ class Dissolve final : public Elixir::Application private: void DrawGeometry(); - WaitGroup m_WaitGroup; - Extent2D m_DrawExtent; - - SFrameData m_FrameData; - Ref m_FrameConstantBuffer; + Scope m_StaticMeshRenderer; Scope m_CameraController; -}; \ No newline at end of file +}; diff --git a/Elixir/Source/Engine.h b/Elixir/Source/Engine.h index c04c259c..df7fae42 100644 --- a/Elixir/Source/Engine.h +++ b/Elixir/Source/Engine.h @@ -63,6 +63,7 @@ #include #include #include +#include #include #include diff --git a/Elixir/Source/Engine/Mesh/StaticMesh.cpp b/Elixir/Source/Engine/Mesh/StaticMesh.cpp index 5cda6c83..6772cd74 100644 --- a/Elixir/Source/Engine/Mesh/StaticMesh.cpp +++ b/Elixir/Source/Engine/Mesh/StaticMesh.cpp @@ -19,8 +19,19 @@ namespace Elixir return Ref(new StaticMesh(std::move(info))); } + const BufferLayout& StaticMesh::GetVertexLayout() + { + static const BufferLayout layout = {{ + { EDataType::Vec3, "Position" }, + { EDataType::Vec3, "Normal" }, + { EDataType::Vec4, "Tangent" }, + { EDataType::Vec2, "TexCoord" }, + }}; + return layout; + } + StaticMesh::StaticMesh(SStaticMeshCreateInfo info) : m_Name(std::move(info.Name)), m_Sections(std::move(info.Sections)), m_LocalBounds(info.LocalBounds) {} -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/Mesh/StaticMesh.h b/Elixir/Source/Engine/Mesh/StaticMesh.h index 7490f481..79687537 100644 --- a/Elixir/Source/Engine/Mesh/StaticMesh.h +++ b/Elixir/Source/Engine/Mesh/StaticMesh.h @@ -79,6 +79,9 @@ namespace Elixir */ static Ref Create(SStaticMeshCreateInfo info); + /** @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; } @@ -95,4 +98,4 @@ namespace Elixir std::vector m_Sections; SStaticMeshBounds m_LocalBounds; }; -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/Mesh/StaticMeshLoader.h b/Elixir/Source/Engine/Mesh/StaticMeshLoader.h index d0a075c1..b128bc0d 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshLoader.h +++ b/Elixir/Source/Engine/Mesh/StaticMeshLoader.h @@ -7,12 +7,6 @@ namespace Elixir { class GraphicsContext; - /** @brief Identifies a source format supported by a static mesh loader. */ - enum class EStaticMeshFormat : uint8_t - { - GLTF, - }; - /** @brief Supplies the engine state and source file for one mesh import. */ struct SStaticMeshLoadRequest { @@ -62,9 +56,6 @@ namespace Elixir public: virtual ~StaticMeshLoader() = default; - /** Get the source format accepted by this loader. */ - virtual EStaticMeshFormat GetFormat() const = 0; - /** * @brief Import all valid static meshes from one source file. * @param request Source file and graphics context. diff --git a/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp b/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp index 5888ce45..6f62aaa7 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp +++ b/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp @@ -9,7 +9,7 @@ namespace Elixir namespace { const GraphicsContext* s_GraphicsContext; - std::unordered_map> s_Loaders; + Scope s_Loader; SStaticMeshLoadResult MakeError(const std::string& message) { @@ -25,24 +25,21 @@ namespace Elixir if (s_GraphicsContext == &context) return; s_GraphicsContext = &context; - s_Loaders.clear(); + s_Loader.reset(); RegisterLoader(CreateScope()); } void StaticMeshLoaderRegistry::Shutdown() { - s_Loaders.clear(); + s_Loader.reset(); s_GraphicsContext = nullptr; } bool StaticMeshLoaderRegistry::RegisterLoader(Scope loader) { - if (!loader) return false; - - const EStaticMeshFormat format = loader->GetFormat(); - if (s_Loaders.contains(format)) return false; + if (!loader || s_Loader) return false; - s_Loaders.emplace(format, std::move(loader)); + s_Loader = std::move(loader); return true; } @@ -50,7 +47,7 @@ namespace Elixir { if (!loader) return false; - s_Loaders[loader->GetFormat()] = std::move(loader); + s_Loader = std::move(loader); return true; } @@ -59,31 +56,12 @@ namespace Elixir if (!s_GraphicsContext) return MakeError("StaticMeshLoaderRegistry must be initialized before loading meshes."); - const auto format = InferFormat(path); - if (!format) - return MakeError(std::format("Unsupported static mesh format: {}.", path.string())); - - const auto loader = s_Loaders.find(*format); - if (loader == s_Loaders.end()) - return MakeError(std::format("No static mesh loader is registered for: {}.", path.string())); + if (!s_Loader) + return MakeError("No static mesh loader is registered."); - return loader->second->Load({ + return s_Loader->Load({ .GraphicsContext = s_GraphicsContext, .Path = path }); } - - std::optional StaticMeshLoaderRegistry::InferFormat( - const std::filesystem::path& path - ) - { - std::string extension = path.extension().string(); - std::ranges::transform(extension, extension.begin(), [](const auto character) - { - return (char)std::tolower(character); - }); - - if (extension == ".gltf" || extension == ".glb") return EStaticMeshFormat::GLTF; - return std::nullopt; - } } \ No newline at end of file diff --git a/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.h b/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.h index ee2254c1..71df8f09 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.h +++ b/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.h @@ -7,10 +7,9 @@ namespace Elixir class GraphicsContext; /** - * @brief Loads static mesh files through registered format loaders. + * @brief Loads static mesh files through the active loader. * - * The registry installs the glTF loader by default. Applications can add another - * format loader or explicitly replace the implementation for an existing format. + * The application selects the implementation during startup. */ class ELIXIR_API StaticMeshLoaderRegistry final { @@ -27,14 +26,14 @@ namespace Elixir static void Shutdown(); /** - * @brief Register a loader for a previously unsupported source format. + * @brief Register the active static mesh loader. * @param loader Loader to register. * @return False when loader is null or its format is already registered. */ static bool RegisterLoader(Scope loader); /** - * @brief Replace the loader registered for one source format. + * @brief Replace the active static mesh loader. * @param loader Loader that becomes responsible for its declared format. * @return False when loader is null. */ @@ -46,10 +45,5 @@ namespace Elixir * @return Imported meshes and diagnostics. */ static SStaticMeshLoadResult Load(const std::filesystem::path& path); - - private: - static std::optional InferFormat( - const std::filesystem::path& path - ); }; } \ No newline at end of file diff --git a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp new file mode 100644 index 00000000..143485ad --- /dev/null +++ b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp @@ -0,0 +1,121 @@ +#include "epch.h" +#include "StaticMeshRenderer.h" + +#include +#include +#include +#include +#include + +namespace Elixir +{ + namespace + { + struct SStaticMeshFrameData + { + glm::mat4 ViewProjection{ 1.0f }; + }; + + glm::vec3 HsvToRgb(const float hue) + { + const glm::vec3 ramp = glm::clamp( + glm::abs(glm::fract(glm::vec3(hue) + glm::vec3(0.0f, 2.0f / 3.0f, 1.0f / 3.0f)) * + 6.0f - 3.0f) - 1.0f, + 0.0f, + 1.0f + ); + return glm::mix(glm::vec3(0.32f), ramp, 0.82f); + } + } + + StaticMeshRenderer::StaticMeshRenderer( + const GraphicsContext* context, + const ShaderLoader* shaderLoader + ) : m_Context(context) + { + EE_CORE_ASSERT(m_Context, "StaticMeshRenderer requires a graphics context") + EE_CORE_ASSERT(shaderLoader, "StaticMeshRenderer requires a shader loader") + + m_Shader = shaderLoader->LoadShader("./Shaders/", "StaticMesh"); + EE_CORE_ASSERT(m_Shader, "StaticMesh visualization shader could not be loaded") + + PipelineBuilder builder; + builder.SetShader(m_Shader); + builder.SetInputTopology(EPrimitiveTopology::TriangleList); + builder.SetPolygonMode(EPolygonMode::Fill); + builder.DisableBlending(); + builder.DisableDepthTest(); + builder.SetColorAttachmentFormat(EImageFormat::R8G8B8A8_SRGB); + builder.SetBufferLayout(StaticMesh::GetVertexLayout()); + m_Pipeline = builder.Build(m_Context); + + const SStaticMeshFrameData frameData; + m_FrameBuffer = UniformBuffer::Create(m_Context, sizeof(frameData), &frameData); + m_Shader->BindConstantBuffer("cbFrame", m_FrameBuffer); + } + + void StaticMeshRenderer::Render( + const std::span> meshes, + const Camera& camera + ) + { + if (meshes.empty()) return; + + const SStaticMeshFrameData frameData{ .ViewProjection = camera.GetViewProjectionMatrix() }; + m_FrameBuffer->UpdateData(&frameData, sizeof(frameData)); + + const Extent2D extent = m_Context->GetRenderTarget()->GetExtent(); + const SRenderingInfo renderingInfo{ + .ColorAttachment = m_Context->GetRenderTarget(), + .RenderArea = extent, + }; + + const auto commandBuffer = m_Context->GetSecondaryCommandBuffer(); + commandBuffer->Begin(renderingInfo); + commandBuffer->BeginRendering(renderingInfo); + commandBuffer->SetViewports({{ + .Width = static_cast(extent.Width), + .Height = static_cast(extent.Height), + .MinDepth = 0.0f, + .MaxDepth = 1.0f, + }}); + commandBuffer->SetScissors({{ + .Offset = { 0, 0 }, + .Extent = extent, + }}); + m_Pipeline->Bind(commandBuffer); + + for (const Ref& mesh : meshes) + { + if (!mesh) continue; + + glm::vec4 color = GetColor(*mesh); + m_Shader->SetPushConstant(commandBuffer, "pc", &color, sizeof(color)); + + for (const SStaticMeshSection& section : mesh->GetSections()) + { + if (!section.Vertices || !section.Indices || section.IndexCount == 0) continue; + + section.Vertices->Bind(commandBuffer); + section.Indices->Bind(commandBuffer); + commandBuffer->DrawIndexed(section.IndexCount); + } + } + + commandBuffer->EndRendering(); + commandBuffer->End(); + m_Context->EnqueueSecondaryCommandBuffer(commandBuffer); + } + + glm::vec4 StaticMeshRenderer::GetColor(const StaticMesh& mesh) + { + if (const auto existing = m_Colors.find(&mesh); existing != m_Colors.end()) + return existing->second; + + const uint64_t identity = std::hash{}(&mesh); + const float hue = static_cast(identity % 360u) / 360.0f; + const glm::vec4 color(HsvToRgb(hue), 1.0f); + m_Colors.emplace(&mesh, color); + return color; + } +} diff --git a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h new file mode 100644 index 00000000..2b77ca22 --- /dev/null +++ b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h @@ -0,0 +1,45 @@ +#pragma once + +#include + +namespace Elixir +{ + class Camera; + class GraphicsContext; + class ShaderLoader; + + /** + * @brief Renders static meshes with one stable pseudo-random color per mesh. + * + * This renderer is a temporary visualisation path. It does not resolve materials, + * scene transforms, lighting, or ECS data. + */ + class ELIXIR_API StaticMeshRenderer final + { + public: + /** + * @brief Create a static mesh renderer for one graphics context. + * @param context Graphics context used to record commands and create the pipeline. + * @param shaderLoader Loader used to create the visualization shader. + * @pre context and shaderLoader remain valid for the renderer lifetime. + */ + StaticMeshRenderer(const GraphicsContext* context, const ShaderLoader* shaderLoader); + + /** + * @brief Record draws for all supplied meshes using the camera view projection. + * @param meshes Meshes to draw in mesh-local space. + * @param camera Camera used to project mesh vertices. + */ + void Render(std::span> meshes, const Camera& camera); + + private: + /** Return a stable pseudo-random color for one mesh identity. */ + glm::vec4 GetColor(const StaticMesh& mesh); + + const GraphicsContext* m_Context = nullptr; + Ref m_Shader; + Ref m_Pipeline; + Ref m_FrameBuffer; + std::unordered_map m_Colors; + }; +} diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp index a4534b8a..748d381e 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp @@ -21,16 +21,6 @@ namespace Elixir result.Diagnostics.push_back({ severity, std::move(message) }); } - BufferLayout GetStaticMeshVertexLayout() - { - return {{ - { EDataType::Vec3, "a_Position" }, - { EDataType::Vec3, "a_Normal" }, - { EDataType::Vec4, "a_Tangent" }, - { EDataType::Vec2, "a_TexCoord" }, - }}; - } - SStaticMeshBounds GetBounds(const std::vector& vertices) { SStaticMeshBounds bounds{ @@ -41,7 +31,7 @@ namespace Elixir for (const auto& vertex : vertices) { bounds.Min = glm::min(bounds.Min, vertex.Position); - bounds.Max = glm::min(bounds.Max, vertex.Position); + bounds.Max = glm::max(bounds.Max, vertex.Position); } return bounds; @@ -62,109 +52,7 @@ namespace Elixir fastgltf::Category::BufferViews | fastgltf::Category::Buffers; - SStaticMeshLoadResult GLTFStaticMeshLoader::Load( - const SStaticMeshLoadRequest& request - ) const - { - SStaticMeshLoadResult result; - - if (!request.GraphicsContext) - { - AddDiagnostic( - result, - EStaticMeshLoadDiagnosticSeverity::Error, - "Cannot load a static mesh without a graphics context." - ); - return result; - } - - auto data = fastgltf::GltfDataBuffer::FromPath(request.Path); - if (data.error() != fastgltf::Error::None) - { - AddDiagnostic( - result, - EStaticMeshLoadDiagnosticSeverity::Error, - std::format( - "Failed to open glTF file '{}': {}.", - request.Path.string(), - fastgltf::getErrorMessage(data.error()) - ) - ); - return result; - } - - fastgltf::Parser parser; - - auto loadResult = parser.loadGltf( - data.get(), - request.Path.parent_path(), - GLTF_OPTIONS, - GLTF_CATEGORIES - ); - - if (loadResult.error() != fastgltf::Error::None) - { - AddDiagnostic( - result, - EStaticMeshLoadDiagnosticSeverity::Error, - std::format( - "Failed to parse glTF file '{}': {}.", - request.Path.string(), - fastgltf::getErrorMessage(loadResult.error()) - ) - ); - return result; - } - - const fastgltf::Asset& asset = loadResult.get(); - - for (auto& mesh : asset.meshes) - LoadMesh(request, result, asset, mesh); - - if (result.Meshes.empty() && !result.HasErrors()) - { - AddDiagnostic( - result, - EStaticMeshLoadDiagnosticSeverity::Error, - std::format( - "No valid static meshes were found in '{}'.", - request.Path.string() - ) - ); - } - - return result; - } - - void GLTFStaticMeshLoader::LoadMesh( - const SStaticMeshLoadRequest& request, - SStaticMeshLoadResult& result, - const fastgltf::Asset& asset, - const fastgltf::Mesh& mesh - ) - { - SStaticMeshCreateInfo info; - info.Name = std::string(mesh.name); - - bool hasBounds = false; - - for (const auto& primitive : mesh.primitives) - LoadPrimitive(request, result, info, asset, primitive, hasBounds); - - if (const auto staticMesh = StaticMesh::Create(std::move(info))) - result.Meshes.push_back(staticMesh); - else - AddDiagnostic( - result, - EStaticMeshLoadDiagnosticSeverity::Error, - std::format( - "Skipping mesh '{}' because it has no valid triangle sections.", - info.Name - ) - ); - } - - void GLTFStaticMeshLoader::LoadPrimitive( + static void LoadPrimitive( const SStaticMeshLoadRequest& request, SStaticMeshLoadResult& result, SStaticMeshCreateInfo& mesh, @@ -369,7 +257,7 @@ namespace Elixir return; } - section.Vertices->SetLayout(GetStaticMeshVertexLayout()); + section.Vertices->SetLayout(StaticMesh::GetVertexLayout()); if (!hasBounds) { @@ -383,4 +271,134 @@ namespace Elixir mesh.Sections.push_back(std::move(section)); } -} \ No newline at end of file + + static void LoadMesh( + const SStaticMeshLoadRequest& request, + SStaticMeshLoadResult& result, + const fastgltf::Asset& asset, + const fastgltf::Mesh& mesh + ) + { + const auto name = mesh.name; + + SStaticMeshCreateInfo info; + info.Name = name; + + bool hasBounds = false; + + for (const auto& primitive : mesh.primitives) + LoadPrimitive(request, result, info, asset, primitive, hasBounds); + + if (const auto staticMesh = StaticMesh::Create(std::move(info))) + result.Meshes.push_back(staticMesh); + else + AddDiagnostic( + result, + EStaticMeshLoadDiagnosticSeverity::Error, + std::format( + "Skipping mesh '{}' because it has no valid triangle sections.", + name + ) + ); + } + + static void LoadMergedMesh( + const SStaticMeshLoadRequest& request, + SStaticMeshLoadResult& result, + const fastgltf::Asset& asset + ) + { + SStaticMeshCreateInfo info; + info.Name = request.Path.stem().string(); + + bool hasBounds = false; + + for (const auto& mesh : asset.meshes) + for (const auto& primitive : mesh.primitives) + LoadPrimitive(request, result, info, asset, primitive, hasBounds); + + if (!info.Sections.empty()) + if (const auto staticMesh = StaticMesh::Create(std::move(info))) + result.Meshes.push_back(staticMesh); + } + + GLTFStaticMeshLoader::GLTFStaticMeshLoader(SGLTFStaticMeshImportOptions options) + : m_Options(options) {} + + SStaticMeshLoadResult GLTFStaticMeshLoader::Load( + const SStaticMeshLoadRequest& request + ) const + { + SStaticMeshLoadResult result; + + if (!request.GraphicsContext) + { + AddDiagnostic( + result, + EStaticMeshLoadDiagnosticSeverity::Error, + "Cannot load a static mesh without a graphics context." + ); + return result; + } + + auto data = fastgltf::GltfDataBuffer::FromPath(request.Path); + if (data.error() != fastgltf::Error::None) + { + AddDiagnostic( + result, + EStaticMeshLoadDiagnosticSeverity::Error, + std::format( + "Failed to open glTF file '{}': {}.", + request.Path.string(), + fastgltf::getErrorMessage(data.error()) + ) + ); + return result; + } + + fastgltf::Parser parser; + + auto loadResult = parser.loadGltf( + data.get(), + request.Path.parent_path(), + GLTF_OPTIONS, + GLTF_CATEGORIES + ); + + if (loadResult.error() != fastgltf::Error::None) + { + AddDiagnostic( + result, + EStaticMeshLoadDiagnosticSeverity::Error, + std::format( + "Failed to parse glTF file '{}': {}.", + request.Path.string(), + fastgltf::getErrorMessage(loadResult.error()) + ) + ); + return result; + } + + const fastgltf::Asset& asset = loadResult.get(); + + if (m_Options.MergeMeshes) + LoadMergedMesh(request, result, asset); + else + for (auto& mesh : asset.meshes) + LoadMesh(request, result, asset, mesh); + + if (result.Meshes.empty() && !result.HasErrors()) + { + AddDiagnostic( + result, + EStaticMeshLoadDiagnosticSeverity::Error, + std::format( + "No valid static meshes were found in '{}'.", + request.Path.string() + ) + ); + } + + return result; + } +} diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h index 6abd3d08..57b1d317 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h @@ -1,20 +1,20 @@ #pragma once -#include "fastgltf/types.hpp" - #include -namespace fastgltf { class Mesh; } - namespace Elixir { + struct SGLTFStaticMeshImportOptions + { + /** Merge all valid glTF meshes into one static mesh. */ + bool MergeMeshes = true; + }; + /** @brief Imports static meshes from glTF and GLB source files. */ class GLTFStaticMeshLoader final : public StaticMeshLoader { public: - /** - * @brief Get the glTF source format supported by this loader */ - EStaticMeshFormat GetFormat() const override { return EStaticMeshFormat::GLTF; } + explicit GLTFStaticMeshLoader(SGLTFStaticMeshImportOptions = {}); /** * @brief Import all static meshes stored in one glTF or GLB file. @@ -24,20 +24,6 @@ namespace Elixir SStaticMeshLoadResult Load(const SStaticMeshLoadRequest& request) const override; private: - static void LoadMesh( - const SStaticMeshLoadRequest& request, - SStaticMeshLoadResult& result, - const fastgltf::Asset& asset, - const fastgltf::Mesh& mesh - ); - - static void LoadPrimitive( - const SStaticMeshLoadRequest& request, - SStaticMeshLoadResult& result, - SStaticMeshCreateInfo& mesh, - const fastgltf::Asset& asset, - const fastgltf::Primitive& primitive, - bool& hasBounds - ); + SGLTFStaticMeshImportOptions m_Options; }; -} \ No newline at end of file +} diff --git a/Elixir/Tests/Engine/Mesh/StaticMeshLoaderRegistryTest.cpp b/Elixir/Tests/Engine/Mesh/StaticMeshLoaderRegistryTest.cpp index 86129edf..df705e9c 100644 --- a/Elixir/Tests/Engine/Mesh/StaticMeshLoaderRegistryTest.cpp +++ b/Elixir/Tests/Engine/Mesh/StaticMeshLoaderRegistryTest.cpp @@ -6,11 +6,9 @@ using namespace Elixir; namespace { - class TestGltfStaticMeshLoader final : public StaticMeshLoader + class TestStaticMeshLoader final : public StaticMeshLoader { public: - EStaticMeshFormat GetFormat() const override { return EStaticMeshFormat::GLTF; } - SStaticMeshLoadResult Load(const SStaticMeshLoadRequest&) const override { return {}; @@ -18,13 +16,13 @@ namespace }; } -TEST(StaticMeshLoaderRegistryTest, RegistrationRejectsDuplicatesAndAllowsReplacement) +TEST(StaticMeshLoaderRegistryTest, RegistrationKeepsOneActiveImplementation) { StaticMeshLoaderRegistry::Shutdown(); - EXPECT_TRUE(StaticMeshLoaderRegistry::RegisterLoader(CreateScope())); - EXPECT_FALSE(StaticMeshLoaderRegistry::RegisterLoader(CreateScope())); - EXPECT_TRUE(StaticMeshLoaderRegistry::ReplaceLoader(CreateScope())); + EXPECT_TRUE(StaticMeshLoaderRegistry::RegisterLoader(CreateScope())); + EXPECT_FALSE(StaticMeshLoaderRegistry::RegisterLoader(CreateScope())); + EXPECT_TRUE(StaticMeshLoaderRegistry::ReplaceLoader(CreateScope())); StaticMeshLoaderRegistry::Shutdown(); -} \ No newline at end of file +} 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; +} From 65c38f3007068e2cde63d971344801acb10e2d5d Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 9 Sep 2026 00:22:12 -0300 Subject: [PATCH 03/61] Simplify static mesh loading results --- Dissolve/Source/Dissolve.cpp | 7 +- Elixir/Source/Engine/Mesh/StaticMeshLoader.h | 39 +--- .../Engine/Mesh/StaticMeshLoaderRegistry.cpp | 10 +- .../Engine/Mesh/StaticMeshLoaderRegistry.h | 6 +- .../Platform/GLTF/GLTFStaticMeshLoader.cpp | 180 ++++++------------ .../Platform/GLTF/GLTFStaticMeshLoader.h | 4 +- .../Mesh/StaticMeshLoaderRegistryTest.cpp | 4 +- 7 files changed, 75 insertions(+), 175 deletions(-) diff --git a/Dissolve/Source/Dissolve.cpp b/Dissolve/Source/Dissolve.cpp index b4df7572..b1b423f0 100644 --- a/Dissolve/Source/Dissolve.cpp +++ b/Dissolve/Source/Dissolve.cpp @@ -20,7 +20,7 @@ std::array, 2> m_ParticleSystemInstances; Ref graphMaterial; -SStaticMeshLoadResult mesh; +std::vector> meshes; Dissolve::Dissolve() { @@ -104,7 +104,8 @@ Dissolve::Dissolve() EE_CORE_ERROR("Dissolve particle emitter 'FlameCore' was not found.") } - mesh = StaticMeshLoaderRegistry::Load("./Assets/Meshes/McLaren/scene.gltf"); + if (auto loadedMeshes = StaticMeshLoaderRegistry::Load("./Assets/Meshes/McLaren/scene.gltf")) + meshes = std::move(*loadedMeshes); } { @@ -235,7 +236,7 @@ void Dissolve::OnEvent(Event& event) void Dissolve::DrawGeometry() { if (m_StaticMeshRenderer) - m_StaticMeshRenderer->Render(mesh.Meshes, m_CameraController->GetCamera()); + m_StaticMeshRenderer->Render(meshes, m_CameraController->GetCamera()); } Application* Elixir::CreateApplication() diff --git a/Elixir/Source/Engine/Mesh/StaticMeshLoader.h b/Elixir/Source/Engine/Mesh/StaticMeshLoader.h index b128bc0d..66ff4b56 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshLoader.h +++ b/Elixir/Source/Engine/Mesh/StaticMeshLoader.h @@ -14,37 +14,8 @@ namespace Elixir std::filesystem::path Path; }; - /** @brief Marks the severity of one static mesh import diagnostic. */ - enum class EStaticMeshLoadDiagnosticSeverity : uint8_t - { - Warning, Error, - }; - - /** @brief Reports an import condition without exposing a parser implementation. */ - struct SStaticMeshLoadDiagnostic - { - EStaticMeshLoadDiagnosticSeverity Severity = EStaticMeshLoadDiagnosticSeverity::Error; - std::string Message; - }; - - /** @brief Contains all static meshes produced from one source file. */ - struct SStaticMeshLoadResult - { - /** Imported meshes, one for each valid source mesh. */ - std::vector> Meshes; - - /** Warnings and errors produced while importing. */ - std::vector Diagnostics; - - /** Check whether the import produced an error. */ - bool HasErrors() const - { - return std::ranges::any_of(Diagnostics, [](const auto& diagnostic) - { - return diagnostic.Severity == EStaticMeshLoadDiagnosticSeverity::Error; - }); - } - }; + /** @brief Contains the meshes loaded from one asset path, when loading succeeds. */ + using StaticMeshLoadResult = std::optional>>; /** * @brief Imports one static mesh source format into engine assets. @@ -59,8 +30,8 @@ namespace Elixir /** * @brief Import all valid static meshes from one source file. * @param request Source file and graphics context. - * @return Imported meshes and diagnostics. + * @return Loaded meshes, or std::nullopt when loading fails. */ - virtual SStaticMeshLoadResult Load(const SStaticMeshLoadRequest& request) const = 0; + virtual StaticMeshLoadResult Load(const SStaticMeshLoadRequest& request) const = 0; }; -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp b/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp index 6f62aaa7..d9c60f66 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp +++ b/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp @@ -11,12 +11,10 @@ namespace Elixir const GraphicsContext* s_GraphicsContext; Scope s_Loader; - SStaticMeshLoadResult MakeError(const std::string& message) + StaticMeshLoadResult MakeError(const std::string_view message) { EE_CORE_ERROR("{}", message) - return { - .Diagnostics = {{ EStaticMeshLoadDiagnosticSeverity::Error, message }} - }; + return std::nullopt; } } @@ -51,7 +49,7 @@ namespace Elixir return true; } - SStaticMeshLoadResult StaticMeshLoaderRegistry::Load(const std::filesystem::path& path) + StaticMeshLoadResult StaticMeshLoaderRegistry::Load(const std::filesystem::path& path) { if (!s_GraphicsContext) return MakeError("StaticMeshLoaderRegistry must be initialized before loading meshes."); @@ -64,4 +62,4 @@ namespace Elixir .Path = path }); } -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.h b/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.h index 71df8f09..0b1f35e4 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.h +++ b/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.h @@ -42,8 +42,8 @@ namespace Elixir /** * @brief Import every static mesh defined by one supported source file. * @param path Local source file. - * @return Imported meshes and diagnostics. + * @return Loaded meshes, or std::nullopt when loading fails. */ - static SStaticMeshLoadResult Load(const std::filesystem::path& path); + static StaticMeshLoadResult Load(const std::filesystem::path& path); }; -} \ No newline at end of file +} diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp index 748d381e..9646d67a 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp @@ -8,19 +8,6 @@ namespace Elixir { namespace { - void AddDiagnostic( - SStaticMeshLoadResult& result, - const EStaticMeshLoadDiagnosticSeverity severity, - std::string message - ) - { - if (severity == EStaticMeshLoadDiagnosticSeverity::Error) - EE_CORE_ERROR("{}", message) - else - EE_CORE_WARN("{}", message); - result.Diagnostics.push_back({ severity, std::move(message) }); - } - SStaticMeshBounds GetBounds(const std::vector& vertices) { SStaticMeshBounds bounds{ @@ -54,7 +41,6 @@ namespace Elixir static void LoadPrimitive( const SStaticMeshLoadRequest& request, - SStaticMeshLoadResult& result, SStaticMeshCreateInfo& mesh, const fastgltf::Asset& asset, const fastgltf::Primitive& primitive, @@ -63,14 +49,10 @@ namespace Elixir { if (primitive.type != fastgltf::PrimitiveType::Triangles) { - AddDiagnostic( - result, - EStaticMeshLoadDiagnosticSeverity::Warning, - std::format( - "Skipping primitive in mesh '{}': only triangles are supported.", - mesh.Name - ) - ); + EE_CORE_WARN( + "Skipping primitive in mesh '{}': only triangles are supported.", + mesh.Name + ) return; } @@ -79,28 +61,17 @@ namespace Elixir if (position == primitive.attributes.end() || position->accessorIndex >= asset.accessors.size()) { - AddDiagnostic( - result, - EStaticMeshLoadDiagnosticSeverity::Warning, - std::format( - "Skipping primitive in mesh '{}': POSITION is missing.", - mesh.Name - ) - ); + 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()) { - AddDiagnostic( - result, - EStaticMeshLoadDiagnosticSeverity::Warning, - std::format( - "Skipping primitive in mesh '{}': it exceeds the vertex limit.", - mesh.Name - ) - ); + EE_CORE_WARN( + "Skipping primitive in mesh '{}': it exceeds the vertex limit.", + mesh.Name + ) return; } @@ -130,15 +101,11 @@ namespace Elixir const auto& accessor = asset.accessors[attribute->accessorIndex]; if (accessor.count != vertices.size()) { - AddDiagnostic( - result, - EStaticMeshLoadDiagnosticSeverity::Warning, - std::format( - "Ignoring {} in primitive of mesh '{}': its count differs from POSITION.", - name, - mesh.Name - ) - ); + EE_CORE_WARN( + "Ignoring {} in primitive of mesh '{}': its count differs from POSITION.", + name, + mesh.Name + ) return; } @@ -185,14 +152,10 @@ namespace Elixir if (accessor.count > std::numeric_limits::max()) { - AddDiagnostic( - result, - EStaticMeshLoadDiagnosticSeverity::Warning, - std::format( - "Skipping primitive in mesh '{}': it exceeds the index limit.", - mesh.Name - ) - ); + EE_CORE_WARN( + "Skipping primitive in mesh '{}': it exceeds the index limit.", + mesh.Name + ) return; } @@ -214,14 +177,7 @@ namespace Elixir if (vertices.empty() || indices.empty()) { - AddDiagnostic( - result, - EStaticMeshLoadDiagnosticSeverity::Warning, - std::format( - "Skipping empty primitive in mesh '{}'.", - mesh.Name - ) - ); + EE_CORE_WARN("Skipping empty primitive in mesh '{}'.", mesh.Name) return; } @@ -246,14 +202,10 @@ namespace Elixir if (!section.Vertices || !section.Indices) { - AddDiagnostic( - result, - EStaticMeshLoadDiagnosticSeverity::Error, - std::format( - "Failed to create GPU buffers for primitive in mesh '{}'.", - mesh.Name - ) - ); + EE_CORE_ERROR( + "Failed to create GPU buffers for primitive in mesh '{}'.", + mesh.Name + ) return; } @@ -274,7 +226,7 @@ namespace Elixir static void LoadMesh( const SStaticMeshLoadRequest& request, - SStaticMeshLoadResult& result, + std::vector>& meshes, const fastgltf::Asset& asset, const fastgltf::Mesh& mesh ) @@ -287,24 +239,20 @@ namespace Elixir bool hasBounds = false; for (const auto& primitive : mesh.primitives) - LoadPrimitive(request, result, info, asset, primitive, hasBounds); + LoadPrimitive(request, info, asset, primitive, hasBounds); if (const auto staticMesh = StaticMesh::Create(std::move(info))) - result.Meshes.push_back(staticMesh); + meshes.push_back(staticMesh); else - AddDiagnostic( - result, - EStaticMeshLoadDiagnosticSeverity::Error, - std::format( - "Skipping mesh '{}' because it has no valid triangle sections.", - name - ) - ); + EE_CORE_ERROR( + "Skipping mesh '{}' because it has no valid triangle sections.", + name + ) } static void LoadMergedMesh( const SStaticMeshLoadRequest& request, - SStaticMeshLoadResult& result, + std::vector>& meshes, const fastgltf::Asset& asset ) { @@ -315,45 +263,37 @@ namespace Elixir for (const auto& mesh : asset.meshes) for (const auto& primitive : mesh.primitives) - LoadPrimitive(request, result, info, asset, primitive, hasBounds); + LoadPrimitive(request, info, asset, primitive, hasBounds); if (!info.Sections.empty()) if (const auto staticMesh = StaticMesh::Create(std::move(info))) - result.Meshes.push_back(staticMesh); + meshes.push_back(staticMesh); } GLTFStaticMeshLoader::GLTFStaticMeshLoader(SGLTFStaticMeshImportOptions options) : m_Options(options) {} - SStaticMeshLoadResult GLTFStaticMeshLoader::Load( + StaticMeshLoadResult GLTFStaticMeshLoader::Load( const SStaticMeshLoadRequest& request ) const { - SStaticMeshLoadResult result; + std::vector> meshes; if (!request.GraphicsContext) { - AddDiagnostic( - result, - EStaticMeshLoadDiagnosticSeverity::Error, - "Cannot load a static mesh without a graphics context." - ); - return result; + EE_CORE_ERROR("Cannot load a static mesh without a graphics context.") + return std::nullopt; } auto data = fastgltf::GltfDataBuffer::FromPath(request.Path); if (data.error() != fastgltf::Error::None) { - AddDiagnostic( - result, - EStaticMeshLoadDiagnosticSeverity::Error, - std::format( - "Failed to open glTF file '{}': {}.", - request.Path.string(), - fastgltf::getErrorMessage(data.error()) - ) - ); - return result; + EE_CORE_ERROR( + "Failed to open glTF file '{}': {}.", + request.Path.string(), + fastgltf::getErrorMessage(data.error()) + ) + return std::nullopt; } fastgltf::Parser parser; @@ -367,38 +307,28 @@ namespace Elixir if (loadResult.error() != fastgltf::Error::None) { - AddDiagnostic( - result, - EStaticMeshLoadDiagnosticSeverity::Error, - std::format( - "Failed to parse glTF file '{}': {}.", - request.Path.string(), - fastgltf::getErrorMessage(loadResult.error()) - ) - ); - return result; + EE_CORE_ERROR( + "Failed to parse glTF file '{}': {}.", + request.Path.string(), + fastgltf::getErrorMessage(loadResult.error()) + ) + return std::nullopt; } const fastgltf::Asset& asset = loadResult.get(); if (m_Options.MergeMeshes) - LoadMergedMesh(request, result, asset); + LoadMergedMesh(request, meshes, asset); else for (auto& mesh : asset.meshes) - LoadMesh(request, result, asset, mesh); + LoadMesh(request, meshes, asset, mesh); - if (result.Meshes.empty() && !result.HasErrors()) + if (meshes.empty()) { - AddDiagnostic( - result, - EStaticMeshLoadDiagnosticSeverity::Error, - std::format( - "No valid static meshes were found in '{}'.", - request.Path.string() - ) - ); + EE_CORE_ERROR("No valid static meshes were found in '{}'.", request.Path.string()) + return std::nullopt; } - return result; + return meshes; } } diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h index 57b1d317..1f373836 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h @@ -19,9 +19,9 @@ namespace Elixir /** * @brief Import all static meshes stored in one glTF or GLB file. * @param request Source file and graphics context. - * @return Imported meshes and diagnostics. + * @return Loaded meshes, or std::nullopt when loading fails. */ - SStaticMeshLoadResult Load(const SStaticMeshLoadRequest& request) const override; + StaticMeshLoadResult Load(const SStaticMeshLoadRequest& request) const override; private: SGLTFStaticMeshImportOptions m_Options; diff --git a/Elixir/Tests/Engine/Mesh/StaticMeshLoaderRegistryTest.cpp b/Elixir/Tests/Engine/Mesh/StaticMeshLoaderRegistryTest.cpp index df705e9c..25683ca9 100644 --- a/Elixir/Tests/Engine/Mesh/StaticMeshLoaderRegistryTest.cpp +++ b/Elixir/Tests/Engine/Mesh/StaticMeshLoaderRegistryTest.cpp @@ -9,9 +9,9 @@ namespace class TestStaticMeshLoader final : public StaticMeshLoader { public: - SStaticMeshLoadResult Load(const SStaticMeshLoadRequest&) const override + StaticMeshLoadResult Load(const SStaticMeshLoadRequest&) const override { - return {}; + return std::vector>{}; } }; } From ddf4bfcb58d91e0ba1c52ec6ecbb0955962be4eb Mon Sep 17 00:00:00 2001 From: MrChampz Date: Thu, 17 Sep 2026 00:20:46 -0300 Subject: [PATCH 04/61] Pool static mesh geometry --- Dissolve/Source/Dissolve.cpp | 15 +- Elixir/Source/Engine/Core/Handle.h | 26 ++ .../Source/Engine/Mesh/GeometryAllocation.cpp | 61 +++++ .../Source/Engine/Mesh/GeometryAllocation.h | 57 ++++ Elixir/Source/Engine/Mesh/GeometryPool.cpp | 243 ++++++++++++++++++ Elixir/Source/Engine/Mesh/GeometryPool.h | 115 +++++++++ Elixir/Source/Engine/Mesh/StaticMesh.cpp | 40 ++- Elixir/Source/Engine/Mesh/StaticMesh.h | 64 ++--- Elixir/Source/Engine/Mesh/StaticMeshLoader.h | 22 +- .../Engine/Mesh/StaticMeshLoaderRegistry.cpp | 33 ++- .../Engine/Mesh/StaticMeshLoaderRegistry.h | 18 +- .../Source/Engine/Mesh/StaticMeshRenderer.cpp | 73 +++--- .../Source/Engine/Mesh/StaticMeshRenderer.h | 43 +++- .../Platform/GLTF/GLTFStaticMeshLoader.cpp | 121 ++------- .../Platform/GLTF/GLTFStaticMeshLoader.h | 19 +- .../Mesh/StaticMeshLoaderRegistryTest.cpp | 4 +- 16 files changed, 734 insertions(+), 220 deletions(-) create mode 100644 Elixir/Source/Engine/Core/Handle.h create mode 100644 Elixir/Source/Engine/Mesh/GeometryAllocation.cpp create mode 100644 Elixir/Source/Engine/Mesh/GeometryAllocation.h create mode 100644 Elixir/Source/Engine/Mesh/GeometryPool.cpp create mode 100644 Elixir/Source/Engine/Mesh/GeometryPool.h diff --git a/Dissolve/Source/Dissolve.cpp b/Dissolve/Source/Dissolve.cpp index b1b423f0..c9f3407e 100644 --- a/Dissolve/Source/Dissolve.cpp +++ b/Dissolve/Source/Dissolve.cpp @@ -20,7 +20,7 @@ std::array, 2> m_ParticleSystemInstances; Ref graphMaterial; -std::vector> meshes; +Ref mesh; Dissolve::Dissolve() { @@ -41,7 +41,8 @@ Dissolve::Dissolve() m_StaticMeshRenderer = CreateScope( m_GraphicsContext.get(), - m_ShaderLoader.get() + m_ShaderLoader.get(), + StaticMeshLoaderRegistry::GetGeometryPool() ); m_ParticleSystems[0] = GetAetherManager().LoadEffect("./Assets/VFX/FireAndFireworks.json"); @@ -104,8 +105,8 @@ Dissolve::Dissolve() EE_CORE_ERROR("Dissolve particle emitter 'FlameCore' was not found.") } - if (auto loadedMeshes = StaticMeshLoaderRegistry::Load("./Assets/Meshes/McLaren/scene.gltf")) - meshes = std::move(*loadedMeshes); + if (auto loaded = StaticMeshLoaderRegistry::Load("./Assets/Meshes/McLaren/scene.gltf")) + mesh = std::move(*loaded); } { @@ -222,7 +223,9 @@ void Dissolve::Render(const Timestep frameTime) m_GraphicsContext->Clear(); + m_StaticMeshRenderer->BeginFrame(m_CameraController->GetCamera()); DrawGeometry(); + m_StaticMeshRenderer->EndFrame(); aether.Render(m_CameraController->GetCamera()); } @@ -236,7 +239,9 @@ void Dissolve::OnEvent(Event& event) void Dissolve::DrawGeometry() { if (m_StaticMeshRenderer) - m_StaticMeshRenderer->Render(meshes, m_CameraController->GetCamera()); + { + m_StaticMeshRenderer->Render(mesh); + } } Application* Elixir::CreateApplication() 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/Mesh/GeometryAllocation.cpp b/Elixir/Source/Engine/Mesh/GeometryAllocation.cpp new file mode 100644 index 00000000..a919a92e --- /dev/null +++ b/Elixir/Source/Engine/Mesh/GeometryAllocation.cpp @@ -0,0 +1,61 @@ +#include "epch.h" +#include "GeometryAllocation.h" + +#include "VkBootstrap.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..bca6faab --- /dev/null +++ b/Elixir/Source/Engine/Mesh/GeometryPool.cpp @@ -0,0 +1,243 @@ +#include "epch.h" +#include "GeometryPool.h" + +#include + +namespace Elixir +{ + GeometryPool::GeometryPool( + const GraphicsContext& context, + const SGeometryPoolConfig config + ) : m_VertexCapacity(config.VertexCapacity), + m_IndexCapacity(config.IndexCapacity) + { + EE_CORE_ASSERT(m_VertexCapacity > 0, "GeometryPool vertex capacity must be greater than 0.") + EE_CORE_ASSERT(m_IndexCapacity > 0, "GeometryPool index capacity must be greater than 0.") + + m_VertexBuffer = DynamicVertexBuffer::Create( + &context, + m_VertexCapacity * sizeof(SStaticMeshVertex) + ); + + m_IndexBuffer = DynamicIndexBuffer::Create( + &context, + m_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_FreeVertexRanges, + m_NextVertexOffset, + m_VertexCapacity, + vertexCount + ); + if (!vertexOffset) + { + EE_CORE_ERROR("GeometryPool has no free vertex range for '{}'.", data.Name) + return {}; + } + + const auto indexOffset = AllocateRange( + m_FreeIndexRanges, + m_NextIndexOffset, + m_IndexCapacity, + indexCount + ); + if (!indexOffset) + { + FreeRange( + m_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_FreeSlots.empty()) + { + slotIndex = m_FreeSlots.back(); + m_FreeSlots.pop_back(); + } + else + { + slotIndex = (uint32_t)m_Slots.size(); + m_Slots.push_back({}); + } + + SGeometryPoolSlot& slot = m_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_Slots.size()) + return nullptr; + + const SGeometryPoolSlot& slot = m_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_Slots.size()) + return; + + SGeometryPoolSlot& slot = m_Slots[handle.Index]; + if (!slot.Allocated || slot.Generation != handle.Generation) + return; + + FreeRange( + m_FreeVertexRanges, + { + .Offset = slot.Geometry.VertexOffset, + .Count = slot.Geometry.VertexCount, + } + ); + + FreeRange( + m_FreeIndexRanges, + { + .Offset = slot.Geometry.IndexOffset, + .Count = slot.Geometry.IndexCount, + } + ); + + slot.Allocated = false; + ++slot.Generation; + slot.Geometry = {}; + m_FreeSlots.push_back(handle.Index); + } + + bool GeometryPool::IsValid(const SHandle& handle) const + { + return handle.IsValid() && + handle.Index < m_Slots.size() && + m_Slots[handle.Index].Allocated && + m_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); + } + } +} \ No newline at end of file diff --git a/Elixir/Source/Engine/Mesh/GeometryPool.h b/Elixir/Source/Engine/Mesh/GeometryPool.h new file mode 100644 index 00000000..64d0db49 --- /dev/null +++ b/Elixir/Source/Engine/Mesh/GeometryPool.h @@ -0,0 +1,115 @@ +#pragma once + +#include +#include +#include + +namespace Elixir +{ + class GraphicsContext; + + /** @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 Free receives its matching handle. + */ + 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 Release a geometry range when its handle is valid. */ + void Free(SHandle handle); + + /** @brief Check whether this allocation still identifies live pool geometry. */ + bool IsValid(const SHandle& handle) const; + + /** + * @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 + ); + + uint32_t m_VertexCapacity = 0; + uint32_t m_IndexCapacity = 0; + uint32_t m_NextVertexOffset = 0; + uint32_t m_NextIndexOffset = 0; + + std::vector m_FreeVertexRanges; + std::vector m_FreeIndexRanges; + std::vector m_Slots; + std::vector m_FreeSlots; + + Ref m_VertexBuffer; + Ref m_IndexBuffer; + std::vector m_Geometries; + std::vector m_Generations; + }; +} diff --git a/Elixir/Source/Engine/Mesh/StaticMesh.cpp b/Elixir/Source/Engine/Mesh/StaticMesh.cpp index 6772cd74..3f9457a7 100644 --- a/Elixir/Source/Engine/Mesh/StaticMesh.cpp +++ b/Elixir/Source/Engine/Mesh/StaticMesh.cpp @@ -1,22 +1,35 @@ #include "epch.h" #include "StaticMesh.h" +#include + namespace Elixir { - Ref StaticMesh::Create(SStaticMeshCreateInfo info) + Ref StaticMesh::Create( + std::string name, + GeometryAllocation geometry, + std::vector sections, + SStaticMeshBounds localBounds + ) { - const bool hasValidSection = std::ranges::any_of(info.Sections, [](const auto& section) + if (!geometry.IsValid()) { - return section.Vertices && section.Indices && section.IndexCount > 0; - }); + EE_CORE_ERROR("StaticMesh requires a valid geometry handle.") + return nullptr; + } - if (!hasValidSection) + if (sections.empty()) { - EE_CORE_ERROR("StaticMesh requires at least one valid section.") + EE_CORE_ERROR("StaticMesh requires at least one section.") return nullptr; } - return Ref(new StaticMesh(std::move(info))); + return Ref(new StaticMesh( + std::move(name), + std::move(geometry), + std::move(sections), + localBounds + )); } const BufferLayout& StaticMesh::GetVertexLayout() @@ -30,8 +43,13 @@ namespace Elixir return layout; } - StaticMesh::StaticMesh(SStaticMeshCreateInfo info) - : m_Name(std::move(info.Name)), - m_Sections(std::move(info.Sections)), - m_LocalBounds(info.LocalBounds) {} + StaticMesh::StaticMesh( + std::string name, + GeometryAllocation geometry, + std::vector sections, + const SStaticMeshBounds& localBounds + ) : m_Name(std::move(name)), + m_Geometry(std::move(geometry)), + m_Sections(std::move(sections)), + m_LocalBounds(localBounds) {} } diff --git a/Elixir/Source/Engine/Mesh/StaticMesh.h b/Elixir/Source/Engine/Mesh/StaticMesh.h index 79687537..f248d301 100644 --- a/Elixir/Source/Engine/Mesh/StaticMesh.h +++ b/Elixir/Source/Engine/Mesh/StaticMesh.h @@ -1,10 +1,11 @@ #pragma once -#include -#include +#include namespace Elixir { + struct SGeometry; + /** @brief Stores the vertex attributes supported by static meshes. */ struct SStaticMeshVertex { @@ -28,43 +29,31 @@ namespace Elixir glm::vec3 Max{}; }; - /** @brief Describes one drawable triangle range of a static mesh. */ + /** @brief Describes one drawable range in a static mesh. */ struct SStaticMeshSection { - /** Vertex data used by this section. */ - Ref Vertices; - - /** Triangle indices used by this section. */ - Ref Indices; - - /** Number of indices in this section. */ + uint32_t FirstIndex = 0; uint32_t IndexCount = 0; - - /** Slot of the material assigned by the source asset. */ + uint32_t VertexOffset = 0; uint32_t MaterialSlot = std::numeric_limits::max(); - - /** Bound of this section in mesh-local space. */ SStaticMeshBounds LocalBounds; }; - /** @brief Supplies immutable data for a new static mesh. */ - struct SStaticMeshCreateInfo + /** @brief Stores CPU geometry loaded from one mesh asset. */ + struct SStaticMeshData { - /** Optional display name of the mesh. */ std::string Name; - - /** Sections that form the mesh. */ + std::vector Vertices; + std::vector Indices; std::vector Sections; - - /** Bound of all sections in mesh-local space. */ SStaticMeshBounds LocalBounds; }; /** - * @brief Stores reusable, non-deforming geometry. + * @brief Represents reusable, non-deforming geometry in the runtime. * - * A static mesh contains no scene transform. Its sections correspond to source draw - * primitives. + * 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 { @@ -73,11 +62,19 @@ namespace Elixir static constexpr uint32_t NO_MATERIAL_SLOT = std::numeric_limits::max(); /** - * @brief Create one static mesh from prepared GPU sections. - * @param info Immutable geometry and metadata for the mesh. - * @return A mesh, or nullptr when no valid section was supplied. + * @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 localBounds Bound of the mesh in mesh-local space. + * @return A mesh, or nullptr when geometry is invalid or sections are empty. */ - static Ref Create(SStaticMeshCreateInfo info); + static Ref Create( + std::string name, + GeometryAllocation geometry, + std::vector sections, + SStaticMeshBounds localBounds + ); /** @brief Get the vertex layout used by every static mesh section. */ static const BufferLayout& GetVertexLayout(); @@ -85,6 +82,9 @@ namespace Elixir /** @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; } @@ -92,9 +92,15 @@ namespace Elixir const SStaticMeshBounds& GetLocalBounds() const { return m_LocalBounds; } private: - explicit StaticMesh(SStaticMeshCreateInfo info); + explicit StaticMesh( + std::string name, + GeometryAllocation geometry, + std::vector sections, + const SStaticMeshBounds& localBounds + ); std::string m_Name; + GeometryAllocation m_Geometry; std::vector m_Sections; SStaticMeshBounds m_LocalBounds; }; diff --git a/Elixir/Source/Engine/Mesh/StaticMeshLoader.h b/Elixir/Source/Engine/Mesh/StaticMeshLoader.h index 66ff4b56..4e16f26e 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshLoader.h +++ b/Elixir/Source/Engine/Mesh/StaticMeshLoader.h @@ -5,20 +5,8 @@ namespace Elixir { - class GraphicsContext; - - /** @brief Supplies the engine state and source file for one mesh import. */ - struct SStaticMeshLoadRequest - { - const GraphicsContext* GraphicsContext = nullptr; - std::filesystem::path Path; - }; - - /** @brief Contains the meshes loaded from one asset path, when loading succeeds. */ - using StaticMeshLoadResult = std::optional>>; - /** - * @brief Imports one static mesh source format into engine assets. + * @brief Loads CPU data for one static mesh from an asset source. * * Implementations must not expose parser-specific types through this contract. */ @@ -28,10 +16,10 @@ namespace Elixir virtual ~StaticMeshLoader() = default; /** - * @brief Import all valid static meshes from one source file. - * @param request Source file and graphics context. - * @return Loaded meshes, or std::nullopt when loading fails. + * @brief Load CPU mesh data from one source file. + * @param path Local source file path. + * @return Loaded mesh data, or std::nullopt when loading fails. */ - virtual StaticMeshLoadResult Load(const SStaticMeshLoadRequest& request) const = 0; + virtual std::optional Load(std::filesystem::path path) const = 0; }; } diff --git a/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp b/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp index d9c60f66..3c4960a6 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp +++ b/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp @@ -10,8 +10,9 @@ namespace Elixir { const GraphicsContext* s_GraphicsContext; Scope s_Loader; + Scope s_GeometryPool; - StaticMeshLoadResult MakeError(const std::string_view message) + std::optional> MakeError(const std::string_view message) { EE_CORE_ERROR("{}", message) return std::nullopt; @@ -23,6 +24,7 @@ namespace Elixir if (s_GraphicsContext == &context) return; s_GraphicsContext = &context; + s_GeometryPool = CreateScope(context); s_Loader.reset(); RegisterLoader(CreateScope()); } @@ -49,7 +51,9 @@ namespace Elixir return true; } - StaticMeshLoadResult StaticMeshLoaderRegistry::Load(const std::filesystem::path& path) + std::optional> StaticMeshLoaderRegistry::Load( + const std::filesystem::path& path + ) { if (!s_GraphicsContext) return MakeError("StaticMeshLoaderRegistry must be initialized before loading meshes."); @@ -57,9 +61,26 @@ namespace Elixir if (!s_Loader) return MakeError("No static mesh loader is registered."); - return s_Loader->Load({ - .GraphicsContext = s_GraphicsContext, - .Path = path - }); + const auto data = s_Loader->Load(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), + 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 index 0b1f35e4..efeb8c2c 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.h +++ b/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.h @@ -1,5 +1,6 @@ #pragma once +#include #include namespace Elixir @@ -7,7 +8,7 @@ namespace Elixir class GraphicsContext; /** - * @brief Loads static mesh files through the active loader. + * @brief Creates runtime static meshes through the active loader and GeometryPool. * * The application selects the implementation during startup. */ @@ -22,28 +23,31 @@ namespace Elixir */ static void Initialize(const GraphicsContext& context); - /** @brief Release registered loaders and the current graphics 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 its format is already registered. + * @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 responsible for its declared format. + * @param loader Loader that becomes active. * @return False when loader is null. */ static bool ReplaceLoader(Scope loader); /** - * @brief Import every static mesh defined by one supported source file. + * @brief Load CPU data and create one runtime static mesh. * @param path Local source file. - * @return Loaded meshes, or std::nullopt when loading fails. + * @return Loaded mesh, or std::nullopt when loading fails. */ - static StaticMeshLoadResult Load(const std::filesystem::path& path); + 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 index 143485ad..a8cd9d22 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp +++ b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp @@ -1,6 +1,7 @@ #include "epch.h" #include "StaticMeshRenderer.h" +#include #include #include #include @@ -30,8 +31,10 @@ namespace Elixir StaticMeshRenderer::StaticMeshRenderer( const GraphicsContext* context, - const ShaderLoader* shaderLoader - ) : m_Context(context) + const ShaderLoader* shaderLoader, + const GeometryPool& geometryPool + ) : m_Context(context), + m_GeometryPool(geometryPool) { EE_CORE_ASSERT(m_Context, "StaticMeshRenderer requires a graphics context") EE_CORE_ASSERT(shaderLoader, "StaticMeshRenderer requires a shader loader") @@ -54,13 +57,8 @@ namespace Elixir m_Shader->BindConstantBuffer("cbFrame", m_FrameBuffer); } - void StaticMeshRenderer::Render( - const std::span> meshes, - const Camera& camera - ) + void StaticMeshRenderer::BeginFrame(const Camera& camera) { - if (meshes.empty()) return; - const SStaticMeshFrameData frameData{ .ViewProjection = camera.GetViewProjectionMatrix() }; m_FrameBuffer->UpdateData(&frameData, sizeof(frameData)); @@ -70,41 +68,58 @@ namespace Elixir .RenderArea = extent, }; - const auto commandBuffer = m_Context->GetSecondaryCommandBuffer(); - commandBuffer->Begin(renderingInfo); - commandBuffer->BeginRendering(renderingInfo); - commandBuffer->SetViewports({{ + m_CommandBuffer = m_Context->GetSecondaryCommandBuffer(); + m_CommandBuffer->Begin(renderingInfo); + m_CommandBuffer->BeginRendering(renderingInfo); + + m_CommandBuffer->SetViewports({{ .Width = static_cast(extent.Width), .Height = static_cast(extent.Height), .MinDepth = 0.0f, .MaxDepth = 1.0f, }}); - commandBuffer->SetScissors({{ + + m_CommandBuffer->SetScissors({{ .Offset = { 0, 0 }, .Extent = extent, }}); - m_Pipeline->Bind(commandBuffer); - for (const Ref& mesh : meshes) - { - if (!mesh) continue; + m_Pipeline->Bind(m_CommandBuffer); + } + + void StaticMeshRenderer::Render(const Ref& mesh) + { + if (!mesh) return; - glm::vec4 color = GetColor(*mesh); - m_Shader->SetPushConstant(commandBuffer, "pc", &color, sizeof(color)); + glm::vec4 color = GetColor(*mesh); + m_Shader->SetPushConstant(m_CommandBuffer, "pc", &color, sizeof(color)); - for (const SStaticMeshSection& section : mesh->GetSections()) - { - if (!section.Vertices || !section.Indices || section.IndexCount == 0) continue; + const auto geometry = mesh->GetGeometry(); + if (!geometry) return; - section.Vertices->Bind(commandBuffer); - section.Indices->Bind(commandBuffer); - commandBuffer->DrawIndexed(section.IndexCount); - } + std::array vertexBuffers = { + m_GeometryPool.GetVertexBuffer().get() + }; + + m_CommandBuffer->BindVertexBuffers(vertexBuffers); + m_CommandBuffer->BindIndexBuffer(m_GeometryPool.GetIndexBuffer().get()); + + for (const auto& section : mesh->GetSections()) + { + m_CommandBuffer->DrawIndexed( + section.IndexCount, + 1, + geometry->IndexOffset + section.FirstIndex, + geometry->VertexOffset + section.VertexOffset + ); } + } - commandBuffer->EndRendering(); - commandBuffer->End(); - m_Context->EnqueueSecondaryCommandBuffer(commandBuffer); + void StaticMeshRenderer::EndFrame() + { + m_CommandBuffer->EndRendering(); + m_CommandBuffer->End(); + m_Context->EnqueueSecondaryCommandBuffer(m_CommandBuffer); } glm::vec4 StaticMeshRenderer::GetColor(const StaticMesh& mesh) diff --git a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h index 2b77ca22..810b03f8 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h +++ b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h @@ -7,6 +7,7 @@ namespace Elixir class Camera; class GraphicsContext; class ShaderLoader; + class GeometryPool; /** * @brief Renders static meshes with one stable pseudo-random color per mesh. @@ -21,16 +22,43 @@ namespace Elixir * @brief Create a static mesh renderer for one graphics context. * @param context Graphics context used to record commands and create the pipeline. * @param shaderLoader Loader used to create the visualization shader. - * @pre context and shaderLoader remain valid for the renderer lifetime. + * @param geometryPool Pool that stores the geometry referenced by rendered meshes. + * @pre context, shaderLoader, and geometryPool remain valid for the renderer lifetime. */ - StaticMeshRenderer(const GraphicsContext* context, const ShaderLoader* shaderLoader); + StaticMeshRenderer( + const GraphicsContext* context, + const ShaderLoader* shaderLoader, + const GeometryPool& geometryPool + ); /** - * @brief Record draws for all supplied meshes using the camera view projection. - * @param meshes Meshes to draw in mesh-local space. - * @param camera Camera used to project mesh vertices. + * @brief Begin recording static mesh draws for one frame. + * + * Updates camera data, begins rendering, configures viewport and scissor, + * and binds the pipeline and shared geometry buffers. + * + * @param camera Camera used to build the view-projection matrix. + * @pre EndFrame was called after the previous BeginFrame. */ - void Render(std::span> meshes, const Camera& camera); + void BeginFrame(const Camera& camera); + + /** + * @brief Record draw commands for one static mesh. + * + * Each section produces one indexed draw using its geometry range and + * material slot. The mesh remains in mesh-local space. + * + * @param mesh Static mesh to draw. + * @pre BeginFrame was called and EndFrame was not called yet. + */ + void Render(const Ref& mesh); + + /** + * @brief Finish recording and submit static mesh draws for the current frame. + * + * @pre BeginFrame was called and EndFrame was not called yet. + */ + void EndFrame(); private: /** Return a stable pseudo-random color for one mesh identity. */ @@ -40,6 +68,9 @@ namespace Elixir Ref m_Shader; Ref m_Pipeline; Ref m_FrameBuffer; + Ref m_CommandBuffer; + const GeometryPool& m_GeometryPool; + std::unordered_map m_Colors; }; } diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp index 9646d67a..0af6ed82 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp @@ -40,8 +40,7 @@ namespace Elixir fastgltf::Category::Buffers; static void LoadPrimitive( - const SStaticMeshLoadRequest& request, - SStaticMeshCreateInfo& mesh, + SStaticMeshData& mesh, const fastgltf::Asset& asset, const fastgltf::Primitive& primitive, bool& hasBounds @@ -182,34 +181,26 @@ namespace Elixir } SStaticMeshSection section; + section.FirstIndex = (uint32_t)mesh.Indices.size(); section.IndexCount = (uint32_t)indices.size(); + section.VertexOffset = (uint32_t)mesh.Vertices.size(); section.MaterialSlot = primitive.materialIndex && *primitive.materialIndex <= StaticMesh::NO_MATERIAL_SLOT - 1 ? (uint32_t)*primitive.materialIndex : StaticMesh::NO_MATERIAL_SLOT; section.LocalBounds = GetBounds(vertices); - section.Vertices = VertexBuffer::Create( - request.GraphicsContext, - vertices.size() * sizeof(SStaticMeshVertex), - vertices.data() - ); - section.Indices = IndexBuffer::Create( - request.GraphicsContext, - indices.size() * sizeof(uint32_t), - indices.data(), - EIndexType::UInt32 - ); - if (!section.Vertices || !section.Indices) - { - EE_CORE_ERROR( - "Failed to create GPU buffers for primitive in mesh '{}'.", - mesh.Name - ) - return; - } + mesh.Vertices.insert( + mesh.Vertices.end(), + std::make_move_iterator(vertices.begin()), + std::make_move_iterator(vertices.end()) + ); - section.Vertices->SetLayout(StaticMesh::GetVertexLayout()); + mesh.Indices.insert( + mesh.Indices.end(), + std::make_move_iterator(indices.begin()), + std::make_move_iterator(indices.end()) + ); if (!hasBounds) { @@ -224,73 +215,17 @@ namespace Elixir mesh.Sections.push_back(std::move(section)); } - static void LoadMesh( - const SStaticMeshLoadRequest& request, - std::vector>& meshes, - const fastgltf::Asset& asset, - const fastgltf::Mesh& mesh - ) - { - const auto name = mesh.name; - - SStaticMeshCreateInfo info; - info.Name = name; - - bool hasBounds = false; - - for (const auto& primitive : mesh.primitives) - LoadPrimitive(request, info, asset, primitive, hasBounds); - - if (const auto staticMesh = StaticMesh::Create(std::move(info))) - meshes.push_back(staticMesh); - else - EE_CORE_ERROR( - "Skipping mesh '{}' because it has no valid triangle sections.", - name - ) - } - - static void LoadMergedMesh( - const SStaticMeshLoadRequest& request, - std::vector>& meshes, - const fastgltf::Asset& asset - ) + std::optional GLTFStaticMeshLoader::Load(std::filesystem::path path) const { - SStaticMeshCreateInfo info; - info.Name = request.Path.stem().string(); - - bool hasBounds = false; - - for (const auto& mesh : asset.meshes) - for (const auto& primitive : mesh.primitives) - LoadPrimitive(request, info, asset, primitive, hasBounds); - - if (!info.Sections.empty()) - if (const auto staticMesh = StaticMesh::Create(std::move(info))) - meshes.push_back(staticMesh); - } + SStaticMeshData mesh; + mesh.Name = path.stem().string(); - GLTFStaticMeshLoader::GLTFStaticMeshLoader(SGLTFStaticMeshImportOptions options) - : m_Options(options) {} - - StaticMeshLoadResult GLTFStaticMeshLoader::Load( - const SStaticMeshLoadRequest& request - ) const - { - std::vector> meshes; - - if (!request.GraphicsContext) - { - EE_CORE_ERROR("Cannot load a static mesh without a graphics context.") - return std::nullopt; - } - - auto data = fastgltf::GltfDataBuffer::FromPath(request.Path); + auto data = fastgltf::GltfDataBuffer::FromPath(path); if (data.error() != fastgltf::Error::None) { EE_CORE_ERROR( "Failed to open glTF file '{}': {}.", - request.Path.string(), + path.string(), fastgltf::getErrorMessage(data.error()) ) return std::nullopt; @@ -300,7 +235,7 @@ namespace Elixir auto loadResult = parser.loadGltf( data.get(), - request.Path.parent_path(), + path.parent_path(), GLTF_OPTIONS, GLTF_CATEGORIES ); @@ -309,26 +244,26 @@ namespace Elixir { EE_CORE_ERROR( "Failed to parse glTF file '{}': {}.", - request.Path.string(), + path.string(), fastgltf::getErrorMessage(loadResult.error()) ) return std::nullopt; } const fastgltf::Asset& asset = loadResult.get(); + bool hasBounds = false; - if (m_Options.MergeMeshes) - LoadMergedMesh(request, meshes, asset); - else - for (auto& mesh : asset.meshes) - LoadMesh(request, meshes, asset, mesh); + // Combine every glTF mesh into this single static mesh. + for (const auto& assetMesh : asset.meshes) + for (const auto& primitive : assetMesh.primitives) + LoadPrimitive(mesh, asset, primitive, hasBounds); - if (meshes.empty()) + if (mesh.Sections.empty()) { - EE_CORE_ERROR("No valid static meshes were found in '{}'.", request.Path.string()) + EE_CORE_ERROR("No valid static meshes were found in '{}'.", path.string()) return std::nullopt; } - return meshes; + return mesh; } } diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h index 1f373836..2aa7db03 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h @@ -4,26 +4,15 @@ namespace Elixir { - struct SGLTFStaticMeshImportOptions - { - /** Merge all valid glTF meshes into one static mesh. */ - bool MergeMeshes = true; - }; - /** @brief Imports static meshes from glTF and GLB source files. */ class GLTFStaticMeshLoader final : public StaticMeshLoader { public: - explicit GLTFStaticMeshLoader(SGLTFStaticMeshImportOptions = {}); - /** - * @brief Import all static meshes stored in one glTF or GLB file. - * @param request Source file and graphics context. - * @return Loaded meshes, or std::nullopt when loading fails. + * @brief Combine all glTF meshes into one static mesh data object. + * @param path Local glTF or GLB file path. + * @return Loaded mesh data, or std::nullopt when loading fails. */ - StaticMeshLoadResult Load(const SStaticMeshLoadRequest& request) const override; - - private: - SGLTFStaticMeshImportOptions m_Options; + std::optional Load(std::filesystem::path path) const override; }; } diff --git a/Elixir/Tests/Engine/Mesh/StaticMeshLoaderRegistryTest.cpp b/Elixir/Tests/Engine/Mesh/StaticMeshLoaderRegistryTest.cpp index 25683ca9..35ff1037 100644 --- a/Elixir/Tests/Engine/Mesh/StaticMeshLoaderRegistryTest.cpp +++ b/Elixir/Tests/Engine/Mesh/StaticMeshLoaderRegistryTest.cpp @@ -9,9 +9,9 @@ namespace class TestStaticMeshLoader final : public StaticMeshLoader { public: - StaticMeshLoadResult Load(const SStaticMeshLoadRequest&) const override + std::optional Load(std::filesystem::path) const override { - return std::vector>{}; + return SStaticMeshData{}; } }; } From dd666bb9d47c69338136eb2f112baf66c98e4a47 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Sat, 19 Sep 2026 15:45:29 -0300 Subject: [PATCH 05/61] Render static meshes with surface materials --- Dissolve/Source/Dissolve.cpp | 2 +- .../Engine/Materials/Compilation/Compiler.cpp | 67 ++++--- .../Engine/Materials/Compilation/Compiler.h | 11 +- .../Engine/Materials/DefaultMaterials.cpp | 15 ++ Elixir/Source/Engine/Materials/Material.h | 5 +- .../Materials/Rendering/MaterialRenderScene.h | 22 +++ .../Engine/Materials/Rendering/Renderer.cpp | 48 +++-- .../Engine/Materials/Rendering/Renderer.h | 4 +- Elixir/Source/Engine/Mesh/StaticMesh.cpp | 28 +++ Elixir/Source/Engine/Mesh/StaticMesh.h | 18 +- Elixir/Source/Engine/Mesh/StaticMeshLoader.h | 16 +- .../Engine/Mesh/StaticMeshLoaderRegistry.cpp | 3 +- .../Source/Engine/Mesh/StaticMeshRenderer.cpp | 167 +++++++++--------- .../Source/Engine/Mesh/StaticMeshRenderer.h | 40 ++--- .../Platform/GLTF/GLTFStaticMeshLoader.cpp | 13 +- .../Platform/GLTF/GLTFStaticMeshLoader.h | 6 +- .../Materials/Compilation/CompilerTest.cpp | 6 +- .../Engine/Materials/MaterialRegistryTest.cpp | 1 + .../Mesh/StaticMeshLoaderRegistryTest.cpp | 5 +- Shaders/Material/Surface.vs.hlsl | 64 +++++++ 20 files changed, 381 insertions(+), 160 deletions(-) create mode 100644 Shaders/Material/Surface.vs.hlsl diff --git a/Dissolve/Source/Dissolve.cpp b/Dissolve/Source/Dissolve.cpp index c9f3407e..9078962c 100644 --- a/Dissolve/Source/Dissolve.cpp +++ b/Dissolve/Source/Dissolve.cpp @@ -41,7 +41,7 @@ Dissolve::Dissolve() m_StaticMeshRenderer = CreateScope( m_GraphicsContext.get(), - m_ShaderLoader.get(), + GetMaterialSystem(), StaticMeshLoaderRegistry::GetGeometryPool() ); diff --git a/Elixir/Source/Engine/Materials/Compilation/Compiler.cpp b/Elixir/Source/Engine/Materials/Compilation/Compiler.cpp index 77c4034a..25373bdb 100644 --- a/Elixir/Source/Engine/Materials/Compilation/Compiler.cpp +++ b/Elixir/Source/Engine/Materials/Compilation/Compiler.cpp @@ -124,8 +124,11 @@ 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::Surface)) + { + result = CompileSurface(loader, material, std::move(result)); + if (!result) return result; + } if (material.SupportsUsage(EMaterialUsage::ParticleSprite)) { @@ -165,12 +168,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 +190,50 @@ 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(vertexSourcePath, std::ios::binary); + out << vertexHlsl; + } + + const fs::path pixelSourcePath = s_GeneratedDir / (name + ".src.ps.hlsl"); { - std::ofstream out(hlslPath, 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 \"" - + 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 +241,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(); } @@ -340,7 +368,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..1da7f79c 100644 --- a/Elixir/Source/Engine/Materials/Compilation/Compiler.h +++ b/Elixir/Source/Engine/Materials/Compilation/Compiler.h @@ -60,17 +60,16 @@ namespace Elixir::Materials::Compilation } /** - * @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 material usage. + * @param usage Material usage. + * @return The matching shader, or null for an unsupported usage. */ const Ref& GetShader(const EMaterialUsage usage) const { switch (usage) { + case EMaterialUsage::Surface: + return SurfaceShader; case EMaterialUsage::ParticleSprite: return ParticleSpriteShader; case EMaterialUsage::ParticleRibbon: diff --git a/Elixir/Source/Engine/Materials/DefaultMaterials.cpp b/Elixir/Source/Engine/Materials/DefaultMaterials.cpp index 699ea011..38525e83 100644 --- a/Elixir/Source/Engine/Materials/DefaultMaterials.cpp +++ b/Elixir/Source/Engine/Materials/DefaultMaterials.cpp @@ -24,6 +24,20 @@ namespace Elixir::Materials return material; } + Ref CreateDefaultSurfaceMaterial() + { + MaterialGraph graph; + + const auto checkerboard = graph.AddNode(8.0f); + graph.SetChannel(EMaterialChannel::BaseColor, checkerboard); + + return MakeMaterial( + "Engine.Materials.Defaults.Surface", + EMaterialUsage::Surface, + std::move(graph) + ); + } + Ref CreateDefaultSpriteMaterial() { MaterialGraph graph; @@ -80,6 +94,7 @@ namespace Elixir::Materials DefaultMaterialArray CreateDefaultMaterials() { return { + CreateDefaultSurfaceMaterial(), CreateDefaultSpriteMaterial(), CreateDefaultRibbonMaterial(), CreateDefaultMeshMaterial() diff --git a/Elixir/Source/Engine/Materials/Material.h b/Elixir/Source/Engine/Materials/Material.h index 8e25774f..0a377bbc 100644 --- a/Elixir/Source/Engine/Materials/Material.h +++ b/Elixir/Source/Engine/Materials/Material.h @@ -15,8 +15,11 @@ namespace Elixir::Materials */ enum class EMaterialUsage : uint8_t { + /** Material for surface geometry. */ + Surface = 0, + /** Material for particle sprites. */ - ParticleSprite = 0, + ParticleSprite, /** Material for particle ribbons. */ ParticleRibbon, diff --git a/Elixir/Source/Engine/Materials/Rendering/MaterialRenderScene.h b/Elixir/Source/Engine/Materials/Rendering/MaterialRenderScene.h index aede595d..58c2db45 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. */ @@ -114,6 +127,12 @@ namespace Elixir::Materials::Rendering /** 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; }; /** @@ -135,6 +154,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..23277534 100644 --- a/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp +++ b/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp @@ -109,6 +109,7 @@ namespace Elixir::Materials::Rendering { switch (pass) { + case EMaterialPass::Surface: return EMaterialUsage::Surface; case EMaterialPass::ParticleSprite: return EMaterialUsage::ParticleSprite; case EMaterialPass::ParticleRibbon: return EMaterialUsage::ParticleRibbon; case EMaterialPass::ParticleMesh: return EMaterialUsage::ParticleMesh; @@ -122,9 +123,10 @@ namespace Elixir::Materials::Rendering { 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; @@ -203,6 +205,7 @@ namespace Elixir::Materials::Rendering const SBatchKey key{ .Pass = item.Pass, .GeometryIndex = item.GeometryIndex, + .Indexed = item.IndexedDraw.has_value(), .Program = *program }; @@ -277,6 +280,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 +298,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; } @@ -365,6 +386,9 @@ namespace Elixir::Materials::Rendering switch (pass) { + case EMaterialPass::Surface: + builder.SetCullMode(ECullMode::Back, EFrontFace::CounterClockwise); + break; case EMaterialPass::ParticleSprite: builder.SetCullMode(ECullMode::None, EFrontFace::CounterClockwise); builder.EnableAlphaBlending(); @@ -383,7 +407,7 @@ namespace Elixir::Materials::Rendering auto info = builder.GetCreateInfo(); - if (pass == EMaterialPass::ParticleMesh) + if (pass == EMaterialPass::Surface || pass == EMaterialPass::ParticleMesh) { info.DepthStencil.DepthTestEnable = true; info.DepthStencil.DepthWriteEnable = true; @@ -473,6 +497,10 @@ namespace Elixir::Materials::Rendering 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..dd47bec8 100644 --- a/Elixir/Source/Engine/Materials/Rendering/Renderer.h +++ b/Elixir/Source/Engine/Materials/Rendering/Renderer.h @@ -20,6 +20,7 @@ namespace Elixir::Materials::Rendering */ enum class EMaterialPass : uint8_t { + Surface, ParticleSprite, ParticleRibbon, ParticleMesh, @@ -142,7 +143,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; @@ -309,6 +310,7 @@ namespace Elixir::Materials::Rendering { EMaterialPass Pass = EMaterialPass::ParticleSprite; uint32_t GeometryIndex = UINT32_MAX; + bool Indexed = false; SProgramKey Program; bool operator==(const SBatchKey&) const = default; diff --git a/Elixir/Source/Engine/Mesh/StaticMesh.cpp b/Elixir/Source/Engine/Mesh/StaticMesh.cpp index 3f9457a7..74c80999 100644 --- a/Elixir/Source/Engine/Mesh/StaticMesh.cpp +++ b/Elixir/Source/Engine/Mesh/StaticMesh.cpp @@ -9,6 +9,7 @@ namespace Elixir std::string name, GeometryAllocation geometry, std::vector sections, + std::vector> materials, SStaticMeshBounds localBounds ) { @@ -24,10 +25,35 @@ namespace Elixir 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 )); } @@ -47,9 +73,11 @@ namespace Elixir 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 index f248d301..6f28dde3 100644 --- a/Elixir/Source/Engine/Mesh/StaticMesh.h +++ b/Elixir/Source/Engine/Mesh/StaticMesh.h @@ -4,6 +4,9 @@ namespace Elixir { + namespace Materials { class Material; } + using namespace Materials; + struct SGeometry; /** @brief Stores the vertex attributes supported by static meshes. */ @@ -35,7 +38,7 @@ namespace Elixir uint32_t FirstIndex = 0; uint32_t IndexCount = 0; uint32_t VertexOffset = 0; - uint32_t MaterialSlot = std::numeric_limits::max(); + uint32_t MaterialIndex = 0; SStaticMeshBounds LocalBounds; }; @@ -46,6 +49,7 @@ namespace Elixir std::vector Vertices; std::vector Indices; std::vector Sections; + std::vector> Materials; SStaticMeshBounds LocalBounds; }; @@ -58,21 +62,20 @@ namespace Elixir class ELIXIR_API StaticMesh final { public: - /** Identifies an unassigned material slot. */ - static constexpr uint32_t NO_MATERIAL_SLOT = std::numeric_limits::max(); - /** * @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 is invalid or sections are empty. + * @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 ); @@ -88,6 +91,9 @@ namespace Elixir /** @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; } @@ -96,12 +102,14 @@ namespace Elixir 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 index 4e16f26e..2641b0cb 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshLoader.h +++ b/Elixir/Source/Engine/Mesh/StaticMeshLoader.h @@ -5,8 +5,10 @@ namespace Elixir { + class GraphicsContext; + /** - * @brief Loads CPU data for one static mesh from an asset source. + * @brief Loads runtime data for one static mesh from an asset source. * * Implementations must not expose parser-specific types through this contract. */ @@ -16,10 +18,18 @@ namespace Elixir virtual ~StaticMeshLoader() = default; /** - * @brief Load CPU mesh data from one source file. + * @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(std::filesystem::path path) const = 0; + 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 index 3c4960a6..8fef7c1e 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp +++ b/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp @@ -61,7 +61,7 @@ namespace Elixir if (!s_Loader) return MakeError("No static mesh loader is registered."); - const auto data = s_Loader->Load(path); + const auto data = s_Loader->Load(*s_GraphicsContext, path); if (!data) return std::nullopt; auto geometry = s_GeometryPool->Upload(*data); @@ -75,6 +75,7 @@ namespace Elixir std::move(data->Name), std::move(geometry), std::move(data->Sections), + std::move(data->Materials), data->LocalBounds ); } diff --git a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp index a8cd9d22..8b05da6d 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp +++ b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp @@ -3,134 +3,135 @@ #include #include -#include #include -#include -#include +#include +#include namespace Elixir { namespace { - struct SStaticMeshFrameData + struct SSurfaceFrameData { - glm::mat4 ViewProjection{ 1.0f }; + glm::mat4 View{ 1.0f }; + glm::mat4 Proj{ 1.0f }; + glm::mat4 ViewProj{ 1.0f }; + glm::vec3 CameraPos{}; + float Time = 0.0f; + uint32_t EnvIndex = UINT32_MAX; + uint32_t IrradianceIndex = UINT32_MAX; + float EnvIntensity = 0.0f; + float EnvMaxLod = 0.0f; + uint32_t PrefIndex = UINT32_MAX; + uint32_t SceneColorIndex = UINT32_MAX; + float ScreenWidth = 1.0f; + float ScreenHeight = 1.0f; + glm::vec4 LightDirection{}; + glm::vec4 LightColor{}; }; - glm::vec3 HsvToRgb(const float hue) + struct SSurfacePushConstants { - const glm::vec3 ramp = glm::clamp( - glm::abs(glm::fract(glm::vec3(hue) + glm::vec3(0.0f, 2.0f / 3.0f, 1.0f / 3.0f)) * - 6.0f - 3.0f) - 1.0f, - 0.0f, - 1.0f - ); - return glm::mix(glm::vec3(0.32f), ramp, 0.82f); - } + glm::mat4 Model{ 1.0f }; + uint32_t MaterialIndex = 0; + }; } StaticMeshRenderer::StaticMeshRenderer( const GraphicsContext* context, - const ShaderLoader* shaderLoader, + MaterialSystem& materialSystem, const GeometryPool& geometryPool - ) : m_Context(context), - m_GeometryPool(geometryPool) + ) : m_MaterialSystem(materialSystem), + m_GeometryPool(geometryPool), + m_Context(context) { EE_CORE_ASSERT(m_Context, "StaticMeshRenderer requires a graphics context") - EE_CORE_ASSERT(shaderLoader, "StaticMeshRenderer requires a shader loader") - - m_Shader = shaderLoader->LoadShader("./Shaders/", "StaticMesh"); - EE_CORE_ASSERT(m_Shader, "StaticMesh visualization shader could not be loaded") - - PipelineBuilder builder; - builder.SetShader(m_Shader); - builder.SetInputTopology(EPrimitiveTopology::TriangleList); - builder.SetPolygonMode(EPolygonMode::Fill); - builder.DisableBlending(); - builder.DisableDepthTest(); - builder.SetColorAttachmentFormat(EImageFormat::R8G8B8A8_SRGB); - builder.SetBufferLayout(StaticMesh::GetVertexLayout()); - m_Pipeline = builder.Build(m_Context); - - const SStaticMeshFrameData frameData; + + constexpr SSurfaceFrameData frameData; m_FrameBuffer = UniformBuffer::Create(m_Context, sizeof(frameData), &frameData); - m_Shader->BindConstantBuffer("cbFrame", m_FrameBuffer); } void StaticMeshRenderer::BeginFrame(const Camera& camera) { - const SStaticMeshFrameData frameData{ .ViewProjection = camera.GetViewProjectionMatrix() }; - m_FrameBuffer->UpdateData(&frameData, sizeof(frameData)); - - const Extent2D extent = m_Context->GetRenderTarget()->GetExtent(); - const SRenderingInfo renderingInfo{ - .ColorAttachment = m_Context->GetRenderTarget(), - .RenderArea = extent, + const auto extent = m_Context->GetRenderTarget()->GetExtent(); + const SSurfaceFrameData frameData{ + .View = camera.GetViewMatrix(), + .Proj = camera.GetProjectionMatrix(), + .ViewProj = camera.GetViewProjectionMatrix(), + .CameraPos = camera.GetPosition(), + .ScreenWidth = (float)extent.Width, + .ScreenHeight = (float)extent.Height, }; + m_FrameBuffer->UpdateData(&frameData, sizeof(frameData)); - m_CommandBuffer = m_Context->GetSecondaryCommandBuffer(); - m_CommandBuffer->Begin(renderingInfo); - m_CommandBuffer->BeginRendering(renderingInfo); - - m_CommandBuffer->SetViewports({{ - .Width = static_cast(extent.Width), - .Height = static_cast(extent.Height), - .MinDepth = 0.0f, - .MaxDepth = 1.0f, - }}); - - m_CommandBuffer->SetScissors({{ - .Offset = { 0, 0 }, - .Extent = extent, - }}); - - m_Pipeline->Bind(m_CommandBuffer); + m_Scene = {}; + m_GeometryIndex = m_Scene.AddGeometry({ + .Pipeline = { + .VertexLayoutKey = reinterpret_cast(&StaticMesh::GetVertexLayout()), + .VertexLayout = &StaticMesh::GetVertexLayout(), + }, + .ConstantBuffers = {{ + .Name = "cbFrame", + .Buffer = m_FrameBuffer, + }}, + .VertexBuffers = {{ + .Buffer = m_GeometryPool.GetVertexBuffer().get(), + .Binding = 0, + }}, + .IndexBuffer = m_GeometryPool.GetIndexBuffer().get(), + }); } void StaticMeshRenderer::Render(const Ref& mesh) { if (!mesh) return; - glm::vec4 color = GetColor(*mesh); - m_Shader->SetPushConstant(m_CommandBuffer, "pc", &color, sizeof(color)); - const auto geometry = mesh->GetGeometry(); if (!geometry) return; - std::array vertexBuffers = { - m_GeometryPool.GetVertexBuffer().get() - }; - - m_CommandBuffer->BindVertexBuffers(vertexBuffers); - m_CommandBuffer->BindIndexBuffer(m_GeometryPool.GetIndexBuffer().get()); + const auto& materials = mesh->GetMaterials(); for (const auto& section : mesh->GetSections()) { - m_CommandBuffer->DrawIndexed( - section.IndexCount, - 1, - geometry->IndexOffset + section.FirstIndex, - geometry->VertexOffset + section.VertexOffset - ); + if (section.MaterialIndex >= materials.size()) continue; + + const auto instance = GetDefaultInstance(materials[section.MaterialIndex]); + if (!instance) continue; + + m_Scene.Add({ + .Pass = EMaterialPass::Surface, + .Material = instance, + .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_CommandBuffer->EndRendering(); - m_CommandBuffer->End(); - m_Context->EnqueueSecondaryCommandBuffer(m_CommandBuffer); + m_MaterialSystem.Submit(std::move(m_Scene)); } - glm::vec4 StaticMeshRenderer::GetColor(const StaticMesh& mesh) + Ref StaticMeshRenderer::GetDefaultInstance(const Ref& material) { - if (const auto existing = m_Colors.find(&mesh); existing != m_Colors.end()) + if (!material) return nullptr; + + const auto existing = m_DefaultInstances.find(material.get()); + if (existing != m_DefaultInstances.end()) + { return existing->second; + } - const uint64_t identity = std::hash{}(&mesh); - const float hue = static_cast(identity % 360u) / 360.0f; - const glm::vec4 color(HsvToRgb(hue), 1.0f); - m_Colors.emplace(&mesh, color); - return color; + 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 index 810b03f8..65d07d2c 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h +++ b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h @@ -1,41 +1,41 @@ #pragma once #include +#include namespace Elixir { + namespace Materials { class MaterialSystem; } + using namespace Materials; + class Camera; class GraphicsContext; class ShaderLoader; class GeometryPool; /** - * @brief Renders static meshes with one stable pseudo-random color per mesh. - * - * This renderer is a temporary visualisation path. It does not resolve materials, - * scene transforms, lighting, or ECS data. + * @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 record commands and create the pipeline. - * @param shaderLoader Loader used to create the visualization shader. + * @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 context, shaderLoader, and geometryPool remain valid for the renderer lifetime. + * @pre All arguments remain valid for the renderer lifetime. */ StaticMeshRenderer( const GraphicsContext* context, - const ShaderLoader* shaderLoader, + MaterialSystem& materialSystem, const GeometryPool& geometryPool ); /** * @brief Begin recording static mesh draws for one frame. * - * Updates camera data, begins rendering, configures viewport and scissor, - * and binds the pipeline and shared geometry buffers. + * Updates camera data and begins collecting material draw commands. * * @param camera Camera used to build the view-projection matrix. * @pre EndFrame was called after the previous BeginFrame. @@ -45,8 +45,7 @@ namespace Elixir /** * @brief Record draw commands for one static mesh. * - * Each section produces one indexed draw using its geometry range and - * material slot. The mesh remains in mesh-local space. + * Each section produces one indexed surface draw. * * @param mesh Static mesh to draw. * @pre BeginFrame was called and EndFrame was not called yet. @@ -54,23 +53,22 @@ namespace Elixir void Render(const Ref& mesh); /** - * @brief Finish recording and submit static mesh draws for the current frame. - * + * @brief Submit collected static mesh draws to MaterialSystem. * @pre BeginFrame was called and EndFrame was not called yet. */ void EndFrame(); private: - /** Return a stable pseudo-random color for one mesh identity. */ - glm::vec4 GetColor(const StaticMesh& mesh); + Ref GetDefaultInstance(const Ref& material); - const GraphicsContext* m_Context = nullptr; - Ref m_Shader; - Ref m_Pipeline; + MaterialSystem& m_MaterialSystem; Ref m_FrameBuffer; - Ref m_CommandBuffer; const GeometryPool& m_GeometryPool; - std::unordered_map m_Colors; + Rendering::MaterialRenderScene m_Scene; + uint32_t m_GeometryIndex = UINT32_MAX; + std::unordered_map> m_DefaultInstances; + + const GraphicsContext* m_Context = nullptr; }; } diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp index 0af6ed82..e491d258 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp @@ -1,6 +1,8 @@ #include "epch.h" #include "GLTFStaticMeshLoader.h" +#include + #include #include @@ -184,10 +186,7 @@ namespace Elixir section.FirstIndex = (uint32_t)mesh.Indices.size(); section.IndexCount = (uint32_t)indices.size(); section.VertexOffset = (uint32_t)mesh.Vertices.size(); - section.MaterialSlot = primitive.materialIndex && - *primitive.materialIndex <= StaticMesh::NO_MATERIAL_SLOT - 1 - ? (uint32_t)*primitive.materialIndex - : StaticMesh::NO_MATERIAL_SLOT; + section.MaterialIndex = 0; section.LocalBounds = GetBounds(vertices); mesh.Vertices.insert( @@ -215,10 +214,14 @@ namespace Elixir mesh.Sections.push_back(std::move(section)); } - std::optional GLTFStaticMeshLoader::Load(std::filesystem::path path) const + std::optional GLTFStaticMeshLoader::Load( + const GraphicsContext& context, + std::filesystem::path path + ) const { SStaticMeshData mesh; mesh.Name = path.stem().string(); + mesh.Materials.push_back(CreateDefaultMaterials()[0]); auto data = fastgltf::GltfDataBuffer::FromPath(path); if (data.error() != fastgltf::Error::None) diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h index 2aa7db03..0e75ad94 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h @@ -10,9 +10,13 @@ namespace Elixir public: /** * @brief Combine all glTF meshes into one static mesh data object. + * @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(std::filesystem::path path) const override; + std::optional Load( + const GraphicsContext& context, + std::filesystem::path path + ) const override; }; } diff --git a/Elixir/Tests/Engine/Materials/Compilation/CompilerTest.cpp b/Elixir/Tests/Engine/Materials/Compilation/CompilerTest.cpp index 128756a6..9c7f4d6e 100644 --- a/Elixir/Tests/Engine/Materials/Compilation/CompilerTest.cpp +++ b/Elixir/Tests/Engine/Materials/Compilation/CompilerTest.cpp @@ -34,7 +34,8 @@ TEST(CompilerTest, AssignsStableSlotsByParameterKindAndName) TEST(CompilerTest, PreservesEnabledRendererUsages) { - const auto material = CreateRef("Particle material"); + const auto material = CreateRef("Material"); + ASSERT_TRUE(material->SetUsage(EMaterialUsage::Surface, true)); ASSERT_TRUE(material->SetUsage(EMaterialUsage::ParticleSprite, true)); ASSERT_TRUE(material->SetUsage(EMaterialUsage::ParticleRibbon, true)); ASSERT_TRUE(material->SetUsage(EMaterialUsage::ParticleMesh, true)); @@ -42,6 +43,7 @@ TEST(CompilerTest, PreservesEnabledRendererUsages) const auto result = Compiler::Build(*material); ASSERT_TRUE(result); + EXPECT_TRUE(result.Material->SupportsUsage(EMaterialUsage::Surface)); EXPECT_TRUE(result.Material->SupportsUsage(EMaterialUsage::ParticleSprite)); EXPECT_TRUE(result.Material->SupportsUsage(EMaterialUsage::ParticleRibbon)); EXPECT_TRUE(result.Material->SupportsUsage(EMaterialUsage::ParticleMesh)); @@ -51,10 +53,12 @@ TEST(CompilerTest, DoesNotAliasParticleUsageShadersToSurfaceShader) { SCompiledMaterial material; + const auto& surfaceShader = material.GetShader(EMaterialUsage::Surface); const auto& spriteShader = material.GetShader(EMaterialUsage::ParticleSprite); const auto& ribbonShader = material.GetShader(EMaterialUsage::ParticleRibbon); const auto& meshShader = material.GetShader(EMaterialUsage::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/MaterialRegistryTest.cpp b/Elixir/Tests/Engine/Materials/MaterialRegistryTest.cpp index ae291d69..a7d54b61 100644 --- a/Elixir/Tests/Engine/Materials/MaterialRegistryTest.cpp +++ b/Elixir/Tests/Engine/Materials/MaterialRegistryTest.cpp @@ -10,6 +10,7 @@ TEST(MaterialRegistryTest, RegistersAndFindsDefaultMaterials) const MaterialRegistry registry; for (const auto usage : { + EMaterialUsage::Surface, EMaterialUsage::ParticleSprite, EMaterialUsage::ParticleRibbon, EMaterialUsage::ParticleMesh diff --git a/Elixir/Tests/Engine/Mesh/StaticMeshLoaderRegistryTest.cpp b/Elixir/Tests/Engine/Mesh/StaticMeshLoaderRegistryTest.cpp index 35ff1037..e090d130 100644 --- a/Elixir/Tests/Engine/Mesh/StaticMeshLoaderRegistryTest.cpp +++ b/Elixir/Tests/Engine/Mesh/StaticMeshLoaderRegistryTest.cpp @@ -9,7 +9,10 @@ namespace class TestStaticMeshLoader final : public StaticMeshLoader { public: - std::optional Load(std::filesystem::path) const override + std::optional Load( + const GraphicsContext&, + std::filesystem::path + ) const override { return SStaticMeshData{}; } diff --git a/Shaders/Material/Surface.vs.hlsl b/Shaders/Material/Surface.vs.hlsl new file mode 100644 index 00000000..85ae8db7 --- /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; + uint EnvIndex; + uint IrradianceIndex; + float EnvIntensity; + float EnvMaxLod; + uint PrefIndex; + 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; +}; + +struct VSOutput +{ + float4 ClipPos : SV_Position; + float3 Normal : NORMAL0; + float4 Tangent : TANGENT0; + float2 TexCoord : TEXCOORD0; + 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; + + return output; +} \ No newline at end of file From 6dd7b86d455d5e1c8456f7f8842d614b9150a1ec Mon Sep 17 00:00:00 2001 From: MrChampz Date: Sat, 19 Sep 2026 15:45:57 -0300 Subject: [PATCH 06/61] Remove unused geometry allocation include --- Elixir/Source/Engine/Mesh/GeometryAllocation.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/Elixir/Source/Engine/Mesh/GeometryAllocation.cpp b/Elixir/Source/Engine/Mesh/GeometryAllocation.cpp index a919a92e..e40a89c4 100644 --- a/Elixir/Source/Engine/Mesh/GeometryAllocation.cpp +++ b/Elixir/Source/Engine/Mesh/GeometryAllocation.cpp @@ -1,8 +1,6 @@ #include "epch.h" #include "GeometryAllocation.h" -#include "VkBootstrap.h" - #include namespace Elixir From bac31cdc41383555b9b2df639143c0089a179fbd Mon Sep 17 00:00:00 2001 From: MrChampz Date: Tue, 22 Sep 2026 00:22:02 -0300 Subject: [PATCH 07/61] Import glTF PBR material maps --- .../Source/Engine/Materials/MaterialGraph.cpp | 16 +- .../Source/Engine/Materials/MaterialGraph.h | 3 + .../Engine/Materials/Nodes/FlattenNormal.h | 56 ++ Elixir/Source/Engine/Materials/Nodes/Lerp.h | 3 +- .../Engine/Materials/Nodes/TextureSample.h | 54 +- .../Platform/GLTF/GLTFStaticMeshLoader.cpp | 513 +++++++++++++++++- .../Platform/GLTF/GLTFStaticMeshLoader.h | 5 + .../Engine/Materials/MaterialGraphTest.cpp | 62 +++ Shaders/Material/Material.ps.hlsl | 23 +- 9 files changed, 711 insertions(+), 24 deletions(-) create mode 100644 Elixir/Source/Engine/Materials/Nodes/FlattenNormal.h diff --git a/Elixir/Source/Engine/Materials/MaterialGraph.cpp b/Elixir/Source/Engine/Materials/MaterialGraph.cpp index 5ac852a8..ef231573 100644 --- a/Elixir/Source/Engine/Materials/MaterialGraph.cpp +++ b/Elixir/Source/Engine/Materials/MaterialGraph.cpp @@ -11,12 +11,13 @@ 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"; } return "BaseColor"; @@ -29,7 +30,8 @@ namespace Elixir::Materials { const bool isScalar = channel == EMaterialChannel::Metallic || channel == EMaterialChannel::Roughness || - channel == EMaterialChannel::Opacity; + channel == EMaterialChannel::Opacity || + channel == EMaterialChannel::AmbientOcclusion; if (isScalar) return expression.ValueType == EMaterialValueType::Float diff --git a/Elixir/Source/Engine/Materials/MaterialGraph.h b/Elixir/Source/Engine/Materials/MaterialGraph.h index 585bfb68..64cb4631 100644 --- a/Elixir/Source/Engine/Materials/MaterialGraph.h +++ b/Elixir/Source/Engine/Materials/MaterialGraph.h @@ -26,6 +26,9 @@ namespace Elixir::Materials /** @brief Surface emissive color. */ Emissive, + + /** @brief Ambient-occlusion multiplier for indirect and direct lighting. */ + AmbientOcclusion, }; /** @brief Maps material parameter names to generated HLSL expressions. */ diff --git a/Elixir/Source/Engine/Materials/Nodes/FlattenNormal.h b/Elixir/Source/Engine/Materials/Nodes/FlattenNormal.h new file mode 100644 index 00000000..3f7c935d --- /dev/null +++ b/Elixir/Source/Engine/Materials/Nodes/FlattenNormal.h @@ -0,0 +1,56 @@ +#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: + /** @brief Creates a tangent-space normal flattening operation. */ + FlattenNormal() + : MaterialNode({{ + "Normal", + EMaterialValueType::Float3, + "float3(0.0, 0.0, 1.0)", + EMaterialValueType::Float3 + }, + { + "Flatness", + EMaterialValueType::Float, + "1.0", + EMaterialValueType::Float + }}) {} + + /** @brief Gets the stable identifier used by tools and serialization. */ + std::string_view GetTypeName() const override { return "Material.FlattenNormal"; } + + /** @brief Emits HLSL that blends the input normal toward the flat normal. */ + 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/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/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/Platform/GLTF/GLTFStaticMeshLoader.cpp b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp index e491d258..37a33ad1 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp @@ -1,10 +1,22 @@ #include "epch.h" #include "GLTFStaticMeshLoader.h" +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include +#include namespace Elixir { @@ -31,6 +43,492 @@ namespace Elixir target.Min = glm::min(target.Min, source.Min); target.Max = glm::max(target.Max, source.Max); } + + Ref DecodeImage( + const GraphicsContext& context, + const std::byte* bytes, + const size_t size, + const EImageFormat format + ) + { + 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; + } + + const auto texture = Texture2D::Create( + &context, + format, + (uint32_t)width, + (uint32_t)height, + pixels + ); + + stbi_image_free(pixels); + + return texture; + } + + Ref LoadImage( + const GraphicsContext& context, + fastgltf::Asset& asset, + const size_t imageIndex, + const std::filesystem::path& sourceDirectory, + const EImageFormat format + ) + { + if (imageIndex >= asset.images.size()) + { + EE_CORE_WARN("glTF material references an invalid image index.") + return nullptr; + } + + Ref texture; + auto& image = asset.images[imageIndex]; + + std::visit( + fastgltf::visitor{ + [](auto&) {}, + [&](const fastgltf::sources::URI& source) + { + const auto path = sourceDirectory / source.uri.fspath(); + texture = TextureLoader::Load( + path, + format + ); + }, + [&](const fastgltf::sources::Array& source) + { + texture = DecodeImage( + context, + source.bytes.data(), + source.bytes.size(), + format + ); + }, + [&](const fastgltf::sources::Vector& source) + { + texture = DecodeImage( + context, + source.bytes.data(), + source.bytes.size(), + format + ); + }, + [&](fastgltf::sources::BufferView& viewSource) + { + if (viewSource.bufferViewIndex >= asset.bufferViews.size()) + { + EE_CORE_WARN("glTF image references an invalid buffer view.") + return; + } + + const auto& view = asset.bufferViews[viewSource.bufferViewIndex]; + if (view.bufferIndex >= asset.buffers.size()) + { + EE_CORE_WARN("glTF image references an invalid buffer.") + return; + } + + auto& buffer = asset.buffers[view.bufferIndex]; + std::visit( + fastgltf::visitor{ + [](auto&) {}, + [&](const fastgltf::sources::Array& source) + { + texture = DecodeImage( + context, + source.bytes.data() + view.byteOffset, + view.byteLength, + format + ); + }, + [&](const fastgltf::sources::Vector& source) + { + texture = DecodeImage( + context, + source.bytes.data() + view.byteOffset, + view.byteLength, + format + ); + } + }, + buffer.data + ); + } + }, + image.data + ); + + return texture; + } + + Ref LoadTexture( + const GraphicsContext& context, + fastgltf::Asset& asset, + const size_t textureIndex, + const std::filesystem::path& sourceDirectory, + const EImageFormat format, + std::unordered_map>& cache + ) + { + const uint64_t cacheKey = + (uint64_t(textureIndex) << 1) | + (format == EImageFormat::R8G8B8A8_SRGB ? 1ull : 0ull); + + const auto existing = cache.find(cacheKey); + if (existing != cache.end()) + { + return existing->second; + } + + if (textureIndex >= asset.textures.size()) + { + EE_CORE_WARN("glTF material references an invalid texture index.") + return nullptr; + } + + const auto& texture = asset.textures[textureIndex]; + if (!texture.imageIndex) + { + EE_CORE_WARN("glTF texture does not reference an image.") + return nullptr; + } + + const auto result = LoadImage( + context, + asset, + *texture.imageIndex, + sourceDirectory, + format + ); + cache.emplace(cacheKey, result); + + return result; + } + + Ref CreateSurfaceMaterial( + const fastgltf::Material& source, + const size_t materialIndex, + const std::string_view meshName, + const Ref& baseColorTexture, + const Ref& metallicRoughnessTexture, + const Ref& normalTexture, + const Ref& occlusionTexture, + const Ref& emissiveTexture + ) + { + using namespace Materials; + using namespace Materials::Nodes; + + const std::string name = source.name.empty() + ? std::string(meshName) + ".Material." + std::to_string(materialIndex) + : std::string(source.name); + + const auto material = CreateRef(name); + material->SetUsage(EMaterialUsage::Surface, true); + + 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 + ); + + 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.") + + MaterialGraph graph; + + auto baseColor = graph.AddNode( + "BaseColorFactor", + EMaterialValueType::Float4 + ); + + if (baseColorTexture) + { + EE_CORE_ASSERT(material->DefineParameter("BaseColorTexture", { + .Kind = EMaterialParameterKind::Texture, + .DefaultValue = SMaterialParameter::MakeTexture(baseColorTexture), + }), "Could not define BaseColorTexture."); + + const auto texture = graph.AddNode("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 (metallicRoughnessTexture) + { + EE_CORE_ASSERT(material->DefineParameter("MetallicRoughnessTexture", { + .Kind = EMaterialParameterKind::Texture, + .DefaultValue = SMaterialParameter::MakeTexture(metallicRoughnessTexture), + }), "Could not define MetallicRoughnessTexture."); + + const auto sample = graph.AddNode("MetallicRoughnessTexture"); + + 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 (emissiveTexture) + { + EE_CORE_ASSERT(material->DefineParameter("EmissiveTexture", { + .Kind = EMaterialParameterKind::Texture, + .DefaultValue = SMaterialParameter::MakeTexture(emissiveTexture), + }), "Could not define EmissiveTexture."); + + const auto sample = graph.AddNode("EmissiveTexture"); + const auto multiply = graph.AddNode(); + graph.Connect(emissive, multiply, 0); + graph.Connect(sample, multiply, 1); + emissive = multiply; + } + + graph.SetChannel(EMaterialChannel::BaseColor, baseColor); + graph.SetChannel(EMaterialChannel::Metallic, metallic); + graph.SetChannel( EMaterialChannel::Roughness, roughness); + graph.SetChannel(EMaterialChannel::Emissive, emissive); + + if (normalTexture) + { + EE_CORE_ASSERT(material->DefineParameter("NormalTexture", { + .Kind = EMaterialParameterKind::Texture, + .DefaultValue = SMaterialParameter::MakeTexture(normalTexture), + }), "Could not define NormalTexture."); + + const auto sample = graph.AddNode( + "NormalTexture", + ETextureSampleType::Normal + ); + + const auto scale = graph.AddNode( + "NormalScale", + EMaterialValueType::Float + ); + + const auto flatten = graph.AddNode(); + graph.Connect(sample, flatten, 0); + graph.Connect(scale, flatten, 1); + + graph.SetChannel(EMaterialChannel::Normal, flatten); + } + + if (occlusionTexture) + { + EE_CORE_ASSERT(material->DefineParameter("OcclusionTexture", { + .Kind = EMaterialParameterKind::Texture, + .DefaultValue = SMaterialParameter::MakeTexture(occlusionTexture), + }), "Could not define OcclusionTexture."); + + const auto sample = graph.AddNode("OcclusionTexture"); + 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); + } + + material->SetGraph(std::move(graph)); + return material; + } + + void LoadMaterials( + SStaticMeshData& mesh, + const GraphicsContext& context, + fastgltf::Asset& asset, + const std::filesystem::path& sourceDirectory + ) + { + // Index zero is the fallback for primitives without a glTF material. + mesh.Materials.push_back(CreateDefaultMaterials()[0]); + + std::unordered_map> textureCache; + mesh.Materials.reserve(asset.materials.size() + 1); + + for (size_t i = 0; i < asset.materials.size(); ++i) + { + const auto& source = asset.materials[i]; + Ref baseColorTexture; + Ref metallicRoughnessTexture; + Ref normalTexture; + Ref occlusionTexture; + Ref emissiveTexture; + + if (source.pbrData.baseColorTexture) + { + baseColorTexture = LoadTexture( + context, + asset, + source.pbrData.baseColorTexture->textureIndex, + sourceDirectory, + EImageFormat::R8G8B8A8_SRGB, + textureCache + ); + } + + if (source.pbrData.metallicRoughnessTexture) + { + metallicRoughnessTexture = LoadTexture( + context, + asset, + source.pbrData.metallicRoughnessTexture->textureIndex, + sourceDirectory, + EImageFormat::R8G8B8A8_UNORM, + textureCache + ); + } + + if (source.normalTexture) + { + normalTexture = LoadTexture( + context, + asset, + source.normalTexture->textureIndex, + sourceDirectory, + EImageFormat::R8G8B8A8_UNORM, + textureCache + ); + } + + if (source.occlusionTexture) + { + occlusionTexture = LoadTexture( + context, + asset, + source.occlusionTexture->textureIndex, + sourceDirectory, + EImageFormat::R8G8B8A8_UNORM, + textureCache + ); + } + + if (source.emissiveTexture) + { + emissiveTexture = LoadTexture( + context, + asset, + source.emissiveTexture->textureIndex, + sourceDirectory, + EImageFormat::R8G8B8A8_SRGB, + textureCache + ); + } + + mesh.Materials.push_back(CreateSurfaceMaterial( + source, + i, + mesh.Name, + baseColorTexture, + metallicRoughnessTexture, + normalTexture, + occlusionTexture, + emissiveTexture + )); + } + } } constexpr auto GLTF_OPTIONS = fastgltf::Options::LoadExternalBuffers | @@ -39,7 +537,10 @@ namespace Elixir constexpr auto GLTF_CATEGORIES = fastgltf::Category::Meshes | fastgltf::Category::Accessors | fastgltf::Category::BufferViews | - fastgltf::Category::Buffers; + fastgltf::Category::Buffers | + fastgltf::Category::Images | + fastgltf::Category::Textures | + fastgltf::Category::Materials; static void LoadPrimitive( SStaticMeshData& mesh, @@ -186,7 +687,10 @@ namespace Elixir section.FirstIndex = (uint32_t)mesh.Indices.size(); section.IndexCount = (uint32_t)indices.size(); section.VertexOffset = (uint32_t)mesh.Vertices.size(); - section.MaterialIndex = 0; + section.MaterialIndex = + primitive.materialIndex && *primitive.materialIndex < asset.materials.size() + ? uint32_t(*primitive.materialIndex + 1) + : 0; section.LocalBounds = GetBounds(vertices); mesh.Vertices.insert( @@ -221,7 +725,6 @@ namespace Elixir { SStaticMeshData mesh; mesh.Name = path.stem().string(); - mesh.Materials.push_back(CreateDefaultMaterials()[0]); auto data = fastgltf::GltfDataBuffer::FromPath(path); if (data.error() != fastgltf::Error::None) @@ -253,9 +756,11 @@ namespace Elixir return std::nullopt; } - const fastgltf::Asset& asset = loadResult.get(); + fastgltf::Asset& asset = loadResult.get(); bool hasBounds = false; + LoadMaterials(mesh, context, asset, path.parent_path()); + // Combine every glTF mesh into this single static mesh. for (const auto& assetMesh : asset.meshes) for (const auto& primitive : assetMesh.primitives) diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h index 0e75ad94..5ed252e2 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h @@ -10,6 +10,11 @@ namespace Elixir public: /** * @brief Combine all glTF meshes into one static mesh data object. + * + * The loader creates one Surface material for every glTF material. It + * imports metallic-roughness PBR factors and base-color, + * metallic-roughness, normal, occlusion, and emissive maps. + * * @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. diff --git a/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp b/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp index 02f13201..09f1020a 100644 --- a/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp +++ b/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include #include @@ -78,6 +80,66 @@ TEST(MaterialGraphTest, RoutesTextureAlphaToOpacity) EXPECT_NE(hlsl.find(".w"), 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, 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(1.000000"), std::string::npos); + EXPECT_NE(hlsl.find("surface.AmbientOcclusion ="), std::string::npos); +} + TEST(MaterialGraphTest, GeneratesExponentialRadialGradientForOpacity) { MaterialGraph graph; diff --git a/Shaders/Material/Material.ps.hlsl b/Shaders/Material/Material.ps.hlsl index 40a1f6b0..6f115f60 100644 --- a/Shaders/Material/Material.ps.hlsl +++ b/Shaders/Material/Material.ps.hlsl @@ -64,6 +64,7 @@ struct Surface float Roughness; float Opacity; float3 Emissive; + float AmbientOcclusion; }; static const uint NO_TEXTURE = 0xFFFFFFFFu; @@ -73,6 +74,15 @@ 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; @@ -117,14 +127,23 @@ 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; // __GRAPH_BODY__ float roughness = clamp(surface.Roughness, 0.045f, 1.0f); float3 F0 = lerp(0.04f.xxx, surface.BaseColor, surface.Metallic); + + 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; + N = normalize(mul(tangentNormal, float3x3(tangent, bitangent, N))); + float NdotV = saturate(dot(N, V)) + 1e-4f; - float3 diffuse = SampleIrradiance(N) * surface.BaseColor * (1.0f - surface.Metallic); + float ao = saturate(surface.AmbientOcclusion); + + float3 diffuse = SampleIrradiance(N) * surface.BaseColor * (1.0f - surface.Metallic) * ao; float3 R = reflect(-V, N); float3 fresnel = F0 + (max((1.0f - roughness).xxx, F0) - F0) * pow(saturate(1.0f - NdotV), 5.0f); @@ -137,7 +156,7 @@ float4 main(PSInput input) : SV_Target0 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; + color += (surface.BaseColor * (1.0f - surface.Metallic) + F0 * spec) * LightColor.rgb * LightColor.w * NdotL * ao; // Tone mapping color = ACESFilm(color); From 63c904c55c109a5a555accf83cc68cf353232635 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Tue, 22 Sep 2026 00:27:43 -0300 Subject: [PATCH 08/61] Simplify FlattenNormal documentation --- Elixir/Source/Engine/Materials/Nodes/FlattenNormal.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/Elixir/Source/Engine/Materials/Nodes/FlattenNormal.h b/Elixir/Source/Engine/Materials/Nodes/FlattenNormal.h index 3f7c935d..7bab18f5 100644 --- a/Elixir/Source/Engine/Materials/Nodes/FlattenNormal.h +++ b/Elixir/Source/Engine/Materials/Nodes/FlattenNormal.h @@ -14,7 +14,6 @@ namespace Elixir::Materials::Nodes class FlattenNormal final : public MaterialNode { public: - /** @brief Creates a tangent-space normal flattening operation. */ FlattenNormal() : MaterialNode({{ "Normal", @@ -29,10 +28,8 @@ namespace Elixir::Materials::Nodes EMaterialValueType::Float }}) {} - /** @brief Gets the stable identifier used by tools and serialization. */ std::string_view GetTypeName() const override { return "Material.FlattenNormal"; } - /** @brief Emits HLSL that blends the input normal toward the flat normal. */ SMaterialExpression Emit(const MaterialEmitContext& context) const override { const auto normal = context.Widen( From 16becc4305e145cd21cb4c1cb61d40a0563f449c Mon Sep 17 00:00:00 2001 From: MrChampz Date: Tue, 22 Sep 2026 02:02:04 -0300 Subject: [PATCH 09/61] Light static meshes with environments --- Dissolve/Dissolve.cmake | 3 +- Dissolve/Source/Dissolve.cpp | 20 +- Dissolve/Source/Dissolve.h | 4 + Dissolve/Source/Environment.cpp | 242 ++++++++++++++++++ Dissolve/Source/Environment.h | 58 +++++ .../Materials/Rendering/MaterialRenderScene.h | 6 + .../Engine/Materials/Rendering/Renderer.cpp | 42 ++- .../Engine/Materials/Rendering/Renderer.h | 44 ++-- .../Materials/Rendering/TextureRegistry.h | 54 ++-- .../Source/Engine/Mesh/StaticMeshRenderer.cpp | 36 ++- .../Source/Engine/Mesh/StaticMeshRenderer.h | 51 +++- Shaders/Material/Material.ps.hlsl | 41 ++- Shaders/Material/Surface.vs.hlsl | 3 - 13 files changed, 529 insertions(+), 75 deletions(-) create mode 100644 Dissolve/Source/Environment.cpp create mode 100644 Dissolve/Source/Environment.h diff --git a/Dissolve/Dissolve.cmake b/Dissolve/Dissolve.cmake index 0dfab6e6..5232a55c 100644 --- a/Dissolve/Dissolve.cmake +++ b/Dissolve/Dissolve.cmake @@ -58,6 +58,7 @@ endif() target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/Elixir/Source + ${CMAKE_SOURCE_DIR}/Elixir/Vendor/stb ) # Linking @@ -66,4 +67,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 9078962c..99d83a06 100644 --- a/Dissolve/Source/Dissolve.cpp +++ b/Dissolve/Source/Dissolve.cpp @@ -21,6 +21,7 @@ std::array, 2> m_ParticleSystemInstances; Ref graphMaterial; Ref mesh; +SStaticMeshLighting s_LightingSetup; Dissolve::Dissolve() { @@ -39,12 +40,29 @@ Dissolve::Dissolve() m_CameraController = CreateScope(60.0f, aspectRatio); const auto tex = TextureLoader::Load("./Assets/Bricks.png"); + 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(), GetMaterialSystem(), StaticMeshLoaderRegistry::GetGeometryPool() ); + s_LightingSetup = { + .Environment = { + .Environment = m_Environment->GetEnvironment(), + .Irradiance = m_Environment->GetIrradiance(), + .Prefiltered = m_Environment->GetPrefiltered(), + .Sampler = m_Environment->GetSampler(), + .Intensity = 1.0f, + .MaxLod = m_Environment->GetMaxLod(), + }, + }; + m_ParticleSystems[0] = GetAetherManager().LoadEffect("./Assets/VFX/FireAndFireworks.json"); EE_CORE_ASSERT( m_ParticleSystems[0], @@ -223,7 +241,7 @@ void Dissolve::Render(const Timestep frameTime) m_GraphicsContext->Clear(); - m_StaticMeshRenderer->BeginFrame(m_CameraController->GetCamera()); + m_StaticMeshRenderer->BeginFrame(m_CameraController->GetCamera(), s_LightingSetup); DrawGeometry(); m_StaticMeshRenderer->EndFrame(); diff --git a/Dissolve/Source/Dissolve.h b/Dissolve/Source/Dissolve.h index 52a18707..c79c2a46 100644 --- a/Dissolve/Source/Dissolve.h +++ b/Dissolve/Source/Dissolve.h @@ -2,6 +2,8 @@ #include +#include "Environment.h" + class Dissolve final : public Elixir::Application { public: @@ -17,6 +19,8 @@ class Dissolve final : public Elixir::Application private: void DrawGeometry(); + Scope m_Environment; + Scope m_StaticMeshRenderer; Scope m_CameraController; diff --git a/Dissolve/Source/Environment.cpp b/Dissolve/Source/Environment.cpp new file mode 100644 index 00000000..c1b5edd1 --- /dev/null +++ b/Dissolve/Source/Environment.cpp @@ -0,0 +1,242 @@ +#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; + } +} + +Elixir::Scope Environment::Load( + const Elixir::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 = Elixir::Scope(new Environment()); + const auto* graphicsContext = &context; + environment->m_Environment = Elixir::Texture2D::Create( + graphicsContext, + Elixir::EImageFormat::R32G32B32A32_SFLOAT, + static_cast(width), + static_cast(height), + pixels, + path.string() + ); + + const auto irradiance = BakeIrradiance(pixels, width, height); + environment->m_Irradiance = Elixir::Texture2D::Create( + graphicsContext, + Elixir::EImageFormat::R32G32B32A32_SFLOAT, + IrradianceWidth, + IrradianceHeight, + irradiance.data() + ); + + const auto prefiltered = BakePrefiltered(pixels, width, height); + environment->m_Prefiltered = Elixir::Texture2D::Create( + graphicsContext, + Elixir::EImageFormat::R32G32B32A32_SFLOAT, + PrefilterWidth, + PrefilterHeight * PrefilterLevels, + prefiltered.data() + ); + stbi_image_free(pixels); + + environment->m_Sampler = Elixir::SamplerBuilder() + .SetMagFilter(Elixir::ESamplerFilter::Linear) + .SetMinFilter(Elixir::ESamplerFilter::Linear) + .SetAddressModeU(Elixir::ESamplerAddressMode::Repeat) + .SetAddressModeV(Elixir::ESamplerAddressMode::ClampToEdge) + .SetAddressModeW(Elixir::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..64b1ba12 --- /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 Elixir::Scope Load( + const Elixir::GraphicsContext& context, + const std::filesystem::path& path + ); + + /** @brief Return the source HDR environment texture. */ + [[nodiscard]] const Elixir::Ref& GetEnvironment() const { return m_Environment; } + + /** @brief Return the diffuse irradiance texture derived from the environment. */ + [[nodiscard]] const Elixir::Ref& GetIrradiance() const { return m_Irradiance; } + + /** @brief Return the roughness-filtered reflection texture. */ + [[nodiscard]] const Elixir::Ref& GetPrefiltered() const { return m_Prefiltered; } + + /** @brief Return the sampler shared by the environment textures. */ + [[nodiscard]] const Elixir::Ref& GetSampler() const { return m_Sampler; } + + /** @brief Return the highest roughness level in the prefiltered texture. */ + [[nodiscard]] float GetMaxLod() const { return static_cast(PrefilterLevels - 1); } + + private: + Environment() = default; + + Elixir::Ref m_Environment; + Elixir::Ref m_Irradiance; + Elixir::Ref m_Prefiltered; + Elixir::Ref m_Sampler; +}; diff --git a/Elixir/Source/Engine/Materials/Rendering/MaterialRenderScene.h b/Elixir/Source/Engine/Materials/Rendering/MaterialRenderScene.h index 58c2db45..d42867e3 100644 --- a/Elixir/Source/Engine/Materials/Rendering/MaterialRenderScene.h +++ b/Elixir/Source/Engine/Materials/Rendering/MaterialRenderScene.h @@ -125,6 +125,12 @@ 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; diff --git a/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp b/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp index 23277534..89e667b0 100644 --- a/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp +++ b/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp @@ -258,6 +258,8 @@ namespace Elixir::Materials::Rendering .ExternalResources = { .ConstantBuffers = geometry->ConstantBuffers, .StorageBuffers = geometry->StorageBuffers, + .Textures = geometry->Textures, + .Samplers = geometry->Samplers, }, .MaterialBuffer = GetActiveMaterialBuffer(), .InitialPushConstants = std::span{ @@ -436,7 +438,6 @@ namespace Elixir::Materials::Rendering { state.ExternalResources.push_back({ .Name = std::string(binding.Name), - .Resource = binding.Buffer.get(), .Type = EDescriptorBindingType::ConstantBuffer, }); } @@ -450,7 +451,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 @@ -460,6 +460,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()) { @@ -468,29 +484,32 @@ 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")) @@ -502,7 +521,6 @@ namespace Elixir::Materials::Rendering 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 dd47bec8..015b16fd 100644 --- a/Elixir/Source/Engine/Materials/Rendering/Renderer.h +++ b/Elixir/Source/Engine/Materials/Rendering/Renderer.h @@ -92,33 +92,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. */ @@ -126,6 +130,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. @@ -133,7 +139,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() + ); } }; @@ -241,13 +252,14 @@ namespace Elixir::Materials::Rendering 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; diff --git a/Elixir/Source/Engine/Materials/Rendering/TextureRegistry.h b/Elixir/Source/Engine/Materials/Rendering/TextureRegistry.h index 5f10e82c..2dd50f81 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. * @@ -82,6 +54,32 @@ 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; + } + }; + Ref m_Textures; Ref m_Sampler; SResourceHandle m_FallbackTextureHandle; diff --git a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp index 8b05da6d..363c806c 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp +++ b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp @@ -18,11 +18,8 @@ namespace Elixir glm::mat4 ViewProj{ 1.0f }; glm::vec3 CameraPos{}; float Time = 0.0f; - uint32_t EnvIndex = UINT32_MAX; - uint32_t IrradianceIndex = UINT32_MAX; float EnvIntensity = 0.0f; float EnvMaxLod = 0.0f; - uint32_t PrefIndex = UINT32_MAX; uint32_t SceneColorIndex = UINT32_MAX; float ScreenWidth = 1.0f; float ScreenHeight = 1.0f; @@ -51,16 +48,29 @@ namespace Elixir m_FrameBuffer = UniformBuffer::Create(m_Context, sizeof(frameData), &frameData); } - void StaticMeshRenderer::BeginFrame(const Camera& camera) + void StaticMeshRenderer::BeginFrame( + const Camera& camera, + const SStaticMeshLighting& lighting + ) { const auto extent = m_Context->GetRenderTarget()->GetExtent(); + + const auto lightDirection = glm::normalize(lighting.DirectionalLightDirection); + const SSurfaceFrameData frameData{ .View = camera.GetViewMatrix(), .Proj = camera.GetProjectionMatrix(), .ViewProj = camera.GetViewProjectionMatrix(), .CameraPos = camera.GetPosition(), + .EnvIntensity = lighting.Environment.Intensity, + .EnvMaxLod = lighting.Environment.MaxLod, .ScreenWidth = (float)extent.Width, .ScreenHeight = (float)extent.Height, + .LightDirection = glm::vec4(lightDirection, 0.0f), + .LightColor = glm::vec4( + lighting.DirectionalLightColor, + lighting.DirectionalLightIntensity + ), }; m_FrameBuffer->UpdateData(&frameData, sizeof(frameData)); @@ -74,6 +84,24 @@ namespace Elixir .Name = "cbFrame", .Buffer = m_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, diff --git a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h index 65d07d2c..176c0d36 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h +++ b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include @@ -13,6 +15,49 @@ namespace Elixir 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 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; + }; + /** * @brief Collects static mesh surface draws for MaterialSystem. */ @@ -38,9 +83,13 @@ namespace Elixir * 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); + void BeginFrame( + const Camera& camera, + const SStaticMeshLighting& lighting + ); /** * @brief Record draw commands for one static mesh. diff --git a/Shaders/Material/Material.ps.hlsl b/Shaders/Material/Material.ps.hlsl index 6f115f60..ed9de20d 100644 --- a/Shaders/Material/Material.ps.hlsl +++ b/Shaders/Material/Material.ps.hlsl @@ -10,11 +10,8 @@ cbuffer cbFrame : register(b0) float4x4 ViewProj; float3 CameraPos; float Time; - uint EnvIndex; - uint IrradianceIndex; float EnvIntensity; float EnvMaxLod; - uint PrefIndex; uint SceneColorIndex; float ScreenWidth; float ScreenHeight; @@ -37,6 +34,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; @@ -92,16 +101,28 @@ 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 SampleEnv(float3 dir, 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 prefilterLevel = saturate(roughness) * EnvMaxLod; + const float prefilterLevels = EnvMaxLod + 1.0f; + + // The prefiltered texture is an equirectangular atlas. Each vertical block + // stores the GGX convolution for one roughness level. + float2 uv = DirToEquirect(dir); + uv.y = (uv.y + prefilterLevel) / prefilterLevels; + + return prefilteredTexture.SampleLevel( + environmentSampler, + uv, + 0 + ).rgb * EnvIntensity; } float3 ACESFilm(float3 x) @@ -134,6 +155,8 @@ float4 main(PSInput input) : SV_Target0 float roughness = clamp(surface.Roughness, 0.045f, 1.0f); float3 F0 = lerp(0.04f.xxx, 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; diff --git a/Shaders/Material/Surface.vs.hlsl b/Shaders/Material/Surface.vs.hlsl index 85ae8db7..4f5169b5 100644 --- a/Shaders/Material/Surface.vs.hlsl +++ b/Shaders/Material/Surface.vs.hlsl @@ -8,11 +8,8 @@ cbuffer cbFrame : register(b0) float4x4 ViewProj; float3 CameraPos; float Time; - uint EnvIndex; - uint IrradianceIndex; float EnvIntensity; float EnvMaxLod; - uint PrefIndex; uint SceneColorIndex; float ScreenWidth; float ScreenHeight; From abaea89b364add55e2bdb7074f0cb43e7d026fc4 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Tue, 22 Sep 2026 18:32:33 -0300 Subject: [PATCH 10/61] Support material transparency modes --- .../Engine/Aether/Effect/MaterialFactory.cpp | 8 +-- .../Engine/Aether/Rendering/Renderer.cpp | 17 ++++- .../Source/Engine/Aether/Rendering/Renderer.h | 5 +- .../Engine/Materials/DefaultMaterials.cpp | 23 +++++-- Elixir/Source/Engine/Materials/Material.cpp | 21 ++++++ Elixir/Source/Engine/Materials/Material.h | 40 +++++++++++ .../Engine/Materials/Rendering/FrameTable.cpp | 3 + .../Engine/Materials/Rendering/FrameTable.h | 6 ++ .../Rendering/MaterialRenderProxy.cpp | 8 ++- .../Materials/Rendering/MaterialRenderProxy.h | 8 +++ .../Materials/Rendering/MaterialRenderScene.h | 6 ++ .../Engine/Materials/Rendering/Renderer.cpp | 68 ++++++++++++++++--- .../Engine/Materials/Rendering/Renderer.h | 4 ++ .../Source/Engine/Mesh/StaticMeshRenderer.cpp | 8 ++- .../Source/Engine/Mesh/StaticMeshRenderer.h | 1 + .../Tests/Engine/Materials/MaterialTest.cpp | 30 ++++++++ Shaders/Material/Material.ps.hlsl | 8 +++ Shaders/Material/ParticleMesh.ps.hlsl | 12 +++- Shaders/Material/ParticleRibbon.ps.hlsl | 12 +++- Shaders/Material/ParticleSprite.ps.hlsl | 12 +++- 20 files changed, 268 insertions(+), 32 deletions(-) diff --git a/Elixir/Source/Engine/Aether/Effect/MaterialFactory.cpp b/Elixir/Source/Engine/Aether/Effect/MaterialFactory.cpp index 43aa3aaa..69863cc6 100644 --- a/Elixir/Source/Engine/Aether/Effect/MaterialFactory.cpp +++ b/Elixir/Source/Engine/Aether/Effect/MaterialFactory.cpp @@ -3,10 +3,7 @@ #include -#include -#include #include -#include #include #include #include @@ -35,9 +32,12 @@ namespace Elixir::Aether::Effect { const auto material = CreateRef(std::move(name)); - const auto result = material->SetUsage(GetMaterialUsage(renderMode), true); + auto result = material->SetUsage(GetMaterialUsage(renderMode), true); EE_CORE_ASSERT(result, "Particle material usage must be enabled.") + result = material->SetBlendMode(EMaterialBlendMode::Translucent); + EE_CORE_ASSERT(result, "Particle materials must enable translucent blending.") + MaterialGraph graph; const auto baseColor = graph.AddNode( 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/Materials/DefaultMaterials.cpp b/Elixir/Source/Engine/Materials/DefaultMaterials.cpp index 38525e83..bd97899a 100644 --- a/Elixir/Source/Engine/Materials/DefaultMaterials.cpp +++ b/Elixir/Source/Engine/Materials/DefaultMaterials.cpp @@ -14,13 +14,23 @@ 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); + + auto result = material->SetUsage(usage, true); EE_CORE_ASSERT(result, "Default material usage must be enabled.") + + if (blendMode != EMaterialBlendMode::Opaque) + { + result = material->SetBlendMode(blendMode); + EE_CORE_ASSERT(result, "Default material blend mode must be enabled.") + } + material->SetGraph(std::move(graph)); + return material; } @@ -58,7 +68,8 @@ namespace Elixir::Materials return MakeMaterial( "Engine.Materials.Defaults.ParticleSprite", EMaterialUsage::ParticleSprite, - std::move(graph) + std::move(graph), + EMaterialBlendMode::Translucent ); } @@ -72,7 +83,8 @@ namespace Elixir::Materials return MakeMaterial( "Engine.Materials.Defaults.ParticleRibbon", EMaterialUsage::ParticleRibbon, - std::move(graph) + std::move(graph), + EMaterialBlendMode::Translucent ); } @@ -86,7 +98,8 @@ namespace Elixir::Materials return MakeMaterial( "Engine.Materials.Defaults.ParticleMesh", EMaterialUsage::ParticleMesh, - std::move(graph) + std::move(graph), + EMaterialBlendMode::Translucent ); } } diff --git a/Elixir/Source/Engine/Materials/Material.cpp b/Elixir/Source/Engine/Materials/Material.cpp index 6fc5076a..8d3f5dc4 100644 --- a/Elixir/Source/Engine/Materials/Material.cpp +++ b/Elixir/Source/Engine/Materials/Material.cpp @@ -16,6 +16,27 @@ namespace Elixir::Materials ++m_Revision; } + bool Material::SetBlendMode(const EMaterialBlendMode mode) + { + if (m_BlendMode == mode) + return false; + + m_BlendMode = mode; + ++m_Revision; + return true; + } + + 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_AlphaCutoff = normalized; + ++m_Revision; + return true; + } + bool Material::SetUsage(const EMaterialUsage usage, const bool enabled) { const uint32_t mask = GetMaterialUsageMask(usage); diff --git a/Elixir/Source/Engine/Materials/Material.h b/Elixir/Source/Engine/Materials/Material.h index 0a377bbc..d8aaa713 100644 --- a/Elixir/Source/Engine/Materials/Material.h +++ b/Elixir/Source/Engine/Materials/Material.h @@ -31,6 +31,21 @@ namespace Elixir::Materials Count }; + /** + * @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. */ @@ -89,6 +104,29 @@ namespace Elixir::Materials */ const MaterialGraph& GetGraph() const { return m_Graph; } + /** + * @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 SetBlendMode(EMaterialBlendMode mode); + + /** @brief Returns the blend mode used by compatible material passes. */ + EMaterialBlendMode GetBlendMode() const { return m_BlendMode; } + + /** + * @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 SetAlphaCutoff(float cutoff); + + /** @brief Returns the opacity threshold used by masked material passes. */ + float GetAlphaCutoff() const { return m_AlphaCutoff; } + /** * @brief Enables or disables a material usage. * @param usage Usage to update. @@ -180,6 +218,8 @@ namespace Elixir::Materials MaterialGraph m_Graph; std::unordered_map m_Parameters; uint32_t m_UsageMask = 0; + EMaterialBlendMode m_BlendMode = EMaterialBlendMode::Opaque; + float m_AlphaCutoff = 0.5f; uint32_t m_Revision = 1; }; 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..bd7d4cc3 100644 --- a/Elixir/Source/Engine/Materials/Rendering/MaterialRenderProxy.cpp +++ b/Elixir/Source/Engine/Materials/Rendering/MaterialRenderProxy.cpp @@ -10,14 +10,18 @@ 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_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..99cdf5f8 100644 --- a/Elixir/Source/Engine/Materials/Rendering/MaterialRenderProxy.h +++ b/Elixir/Source/Engine/Materials/Rendering/MaterialRenderProxy.h @@ -60,10 +60,18 @@ 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 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; + 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 d42867e3..e13de5f0 100644 --- a/Elixir/Source/Engine/Materials/Rendering/MaterialRenderScene.h +++ b/Elixir/Source/Engine/Materials/Rendering/MaterialRenderScene.h @@ -152,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; diff --git a/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp b/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp index 89e667b0..bffd1a26 100644 --- a/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp +++ b/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp @@ -188,12 +188,20 @@ namespace Elixir::Materials::Rendering 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; @@ -204,6 +212,7 @@ namespace Elixir::Materials::Rendering const SBatchKey key{ .Pass = item.Pass, + .BlendMode = prepared.Proxy->GetBlendMode(), .GeometryIndex = item.GeometryIndex, .Indexed = item.IndexedDraw.has_value(), .Program = *program @@ -233,6 +242,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; @@ -243,6 +255,37 @@ 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(), + .GeometryIndex = item.GeometryIndex, + .Indexed = item.IndexedDraw.has_value(), + .Program = *program, + }, + .Items = { prepared }, + }); + } + for (const auto& batch : batches) { if (batch.Items.empty()) continue; @@ -353,7 +396,12 @@ namespace Elixir::Materials::Rendering return SPreparedPass{ .Shader = shader, - .Pipeline = GetPipeline(request.Pass, shader, request.Pipeline), + .Pipeline = GetPipeline( + request.Pass, + request.Material->GetBlendMode(), + shader, + request.Pipeline + ), }; } @@ -365,12 +413,14 @@ namespace Elixir::Materials::Rendering Ref Renderer::GetPipeline( const EMaterialPass pass, + const EMaterialBlendMode blendMode, const Ref& shader, const SPipelineRequest& request ) { const SPipelineKey key{ .Pass = pass, + .BlendMode = blendMode, .Shader = shader.get(), .VertexLayoutKey = request.VertexLayoutKey, }; @@ -385,34 +435,30 @@ namespace Elixir::Materials::Rendering builder.SetColorAttachmentFormat(EImageFormat::R8G8B8A8_SRGB); builder.SetDepthAttachmentFormat(EDepthStencilImageFormat::D32_SFLOAT); builder.SetBufferLayout(*request.VertexLayout); + builder.DisableBlending(); switch (pass) { case EMaterialPass::Surface: + case EMaterialPass::ParticleMesh: builder.SetCullMode(ECullMode::Back, EFrontFace::CounterClockwise); break; case EMaterialPass::ParticleSprite: - builder.SetCullMode(ECullMode::None, EFrontFace::CounterClockwise); - builder.EnableAlphaBlending(); - builder.DisableDepthTest(); - break; 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::Surface || pass == EMaterialPass::ParticleMesh) { info.DepthStencil.DepthTestEnable = true; - info.DepthStencil.DepthWriteEnable = true; + info.DepthStencil.DepthWriteEnable = blendMode != EMaterialBlendMode::Translucent; info.DepthStencil.DepthCompareOp = ECompareOp::LessOrEqual; } diff --git a/Elixir/Source/Engine/Materials/Rendering/Renderer.h b/Elixir/Source/Engine/Materials/Rendering/Renderer.h index 015b16fd..4b34c7dd 100644 --- a/Elixir/Source/Engine/Materials/Rendering/Renderer.h +++ b/Elixir/Source/Engine/Materials/Rendering/Renderer.h @@ -278,6 +278,7 @@ namespace Elixir::Materials::Rendering struct SPipelineKey { EMaterialPass Pass = EMaterialPass::ParticleSprite; + EMaterialBlendMode BlendMode = EMaterialBlendMode::Opaque; const Shader* Shader = nullptr; uint64_t VertexLayoutKey = 0; @@ -291,6 +292,7 @@ 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.VertexLayoutKey)); return hash; } @@ -321,6 +323,7 @@ namespace Elixir::Materials::Rendering struct SBatchKey { EMaterialPass Pass = EMaterialPass::ParticleSprite; + EMaterialBlendMode BlendMode = EMaterialBlendMode::Opaque; uint32_t GeometryIndex = UINT32_MAX; bool Indexed = false; SProgramKey Program; @@ -352,6 +355,7 @@ namespace Elixir::Materials::Rendering /** Returns a cached pipeline or creates one for the request. */ Ref GetPipeline( EMaterialPass pass, + EMaterialBlendMode blendMode, const Ref& shader, const SPipelineRequest& request ); diff --git a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp index 363c806c..e912b0f4 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp +++ b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp @@ -55,10 +55,12 @@ namespace Elixir { const auto extent = m_Context->GetRenderTarget()->GetExtent(); + m_View = camera.GetViewMatrix(); + const auto lightDirection = glm::normalize(lighting.DirectionalLightDirection); const SSurfaceFrameData frameData{ - .View = camera.GetViewMatrix(), + .View = m_View, .Proj = camera.GetProjectionMatrix(), .ViewProj = camera.GetViewProjectionMatrix(), .CameraPos = camera.GetPosition(), @@ -126,9 +128,13 @@ namespace Elixir 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{}, diff --git a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h index 176c0d36..2a4ffc87 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h +++ b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h @@ -117,6 +117,7 @@ namespace Elixir 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; }; diff --git a/Elixir/Tests/Engine/Materials/MaterialTest.cpp b/Elixir/Tests/Engine/Materials/MaterialTest.cpp index afc85ccc..36ddd174 100644 --- a/Elixir/Tests/Engine/Materials/MaterialTest.cpp +++ b/Elixir/Tests/Engine/Materials/MaterialTest.cpp @@ -8,6 +8,36 @@ using namespace Elixir; using namespace Elixir::Materials; using namespace Elixir::Materials::Nodes; +TEST(MaterialTest, StoresTransparencySettings) +{ + Material material("Transparency"); + + EXPECT_EQ(material.GetBlendMode(), EMaterialBlendMode::Opaque); + EXPECT_FLOAT_EQ(material.GetAlphaCutoff(), 0.5f); + + const auto initialRevision = material.GetRevision(); + EXPECT_TRUE(material.SetBlendMode(EMaterialBlendMode::Masked)); + EXPECT_EQ(material.GetBlendMode(), EMaterialBlendMode::Masked); + EXPECT_EQ(material.GetRevision(), initialRevision + 1); + + EXPECT_TRUE(material.SetAlphaCutoff(0.35f)); + EXPECT_FLOAT_EQ(material.GetAlphaCutoff(), 0.35f); + EXPECT_EQ(material.GetRevision(), initialRevision + 2); +} + +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; diff --git a/Shaders/Material/Material.ps.hlsl b/Shaders/Material/Material.ps.hlsl index ed9de20d..785a4aa1 100644 --- a/Shaders/Material/Material.ps.hlsl +++ b/Shaders/Material/Material.ps.hlsl @@ -26,6 +26,8 @@ struct CompiledMaterial { float4 Values[32]; uint TextureIndices[32]; + uint BlendMode; + float AlphaCutoff; }; [[vk::binding(2, 0)]] @@ -152,6 +154,12 @@ float4 main(PSInput input) : SV_Target0 // __GRAPH_BODY__ + static const uint MATERIAL_BLEND_MASK = 1u; + if (mat.BlendMode == MATERIAL_BLEND_MASK) + { + clip(surface.Opacity - mat.AlphaCutoff); + } + float roughness = clamp(surface.Roughness, 0.045f, 1.0f); float3 F0 = lerp(0.04f.xxx, surface.BaseColor, surface.Metallic); diff --git a/Shaders/Material/ParticleMesh.ps.hlsl b/Shaders/Material/ParticleMesh.ps.hlsl index 0b3208de..f90ef150 100644 --- a/Shaders/Material/ParticleMesh.ps.hlsl +++ b/Shaders/Material/ParticleMesh.ps.hlsl @@ -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)]] @@ -76,6 +78,12 @@ float4 main(PSInput input) : SV_Target0 // __GRAPH_BODY__ + static const uint MATERIAL_BLEND_MASK = 1u; + if (mat.BlendMode == MATERIAL_BLEND_MASK) + { + clip(surface.Opacity - mat.AlphaCutoff); + } + const float3 normal = normalize(input.Normal); const float3 lightDirection = normalize(LIGHT_DIRECTION); const float3 viewDirection = normalize(CameraPos - input.WorldPos); diff --git a/Shaders/Material/ParticleRibbon.ps.hlsl b/Shaders/Material/ParticleRibbon.ps.hlsl index d034e78f..b2af4249 100644 --- a/Shaders/Material/ParticleRibbon.ps.hlsl +++ b/Shaders/Material/ParticleRibbon.ps.hlsl @@ -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)]] @@ -76,6 +78,12 @@ float4 main(PSInput input) : SV_Target0 // __GRAPH_BODY__ + static const uint MATERIAL_BLEND_MASK = 1u; + if (mat.BlendMode == MATERIAL_BLEND_MASK) + { + clip(surface.Opacity - mat.AlphaCutoff); + } + //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); diff --git a/Shaders/Material/ParticleSprite.ps.hlsl b/Shaders/Material/ParticleSprite.ps.hlsl index 569063a8..896b44b5 100644 --- a/Shaders/Material/ParticleSprite.ps.hlsl +++ b/Shaders/Material/ParticleSprite.ps.hlsl @@ -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)]] @@ -71,5 +73,11 @@ float4 main(PSInput input) : SV_Target0 // __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 From 967ee383f1a20f66bcccf60dcddd296ae66a2bf0 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Tue, 22 Sep 2026 19:00:07 -0300 Subject: [PATCH 11/61] Import glTF transparency modes --- .../Platform/GLTF/GLTFStaticMeshLoader.cpp | 26 +++++++++++++++++++ .../Platform/GLTF/GLTFStaticMeshLoader.h | 3 ++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp index 37a33ad1..e11f4f2c 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp @@ -224,6 +224,24 @@ namespace Elixir return result; } + EMaterialBlendMode 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; + } + } + Ref CreateSurfaceMaterial( const fastgltf::Material& source, const size_t materialIndex, @@ -244,6 +262,10 @@ namespace Elixir const auto material = CreateRef(name); material->SetUsage(EMaterialUsage::Surface, true); + material->SetBlendMode(GetBlendMode(source.alphaMode)); + + if (source.alphaMode == fastgltf::AlphaMode::Mask) + material->SetAlphaCutoff(source.alphaCutoff); const auto& pbr = source.pbrData; const auto baseColorFactor = glm::vec4( @@ -373,9 +395,13 @@ namespace Elixir 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 (normalTexture) diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h index 5ed252e2..15699c70 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h @@ -11,7 +11,8 @@ namespace Elixir /** * @brief Combine all glTF meshes into one static mesh data object. * - * The loader creates one Surface material for every glTF material. It + * 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. * From 8c52a934aeace95f4ba015639829d1f161602d28 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Tue, 22 Sep 2026 22:32:10 -0300 Subject: [PATCH 12/61] Add material shading models --- .../Engine/Materials/Compilation/Compiler.cpp | 29 ++++++-- .../Engine/Materials/Compilation/Compiler.h | 8 ++- Elixir/Source/Engine/Materials/Material.cpp | 44 ++++++++----- Elixir/Source/Engine/Materials/Material.h | 66 ++++++++++++------- .../Source/Engine/Materials/MaterialGraph.cpp | 21 +++--- .../Source/Engine/Materials/MaterialGraph.h | 9 +++ .../Tests/Engine/Materials/MaterialTest.cpp | 19 +++++- Shaders/Material/Material.ps.hlsl | 47 +++++++++++-- 8 files changed, 180 insertions(+), 63 deletions(-) diff --git a/Elixir/Source/Engine/Materials/Compilation/Compiler.cpp b/Elixir/Source/Engine/Materials/Compilation/Compiler.cpp index 25373bdb..40eb99c6 100644 --- a/Elixir/Source/Engine/Materials/Compilation/Compiler.cpp +++ b/Elixir/Source/Engine/Materials/Compilation/Compiler.cpp @@ -151,13 +151,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; } @@ -201,7 +216,11 @@ namespace Elixir::Materials::Compilation std::ofstream out(pixelSourcePath, std::ios::binary); const auto graphHlsl = GenerateGraphHLSL(material.GetGraph(), *result.Material); - out << InjectBody(pixelHlsl, graphHlsl); + out << InjectBody( + pixelHlsl, + graphHlsl, + material.GetShadingModel() + ); } const fs::path dxc = FindDXC(); diff --git a/Elixir/Source/Engine/Materials/Compilation/Compiler.h b/Elixir/Source/Engine/Materials/Compilation/Compiler.h index 1da7f79c..b471a038 100644 --- a/Elixir/Source/Engine/Materials/Compilation/Compiler.h +++ b/Elixir/Source/Engine/Materials/Compilation/Compiler.h @@ -125,8 +125,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/Material.cpp b/Elixir/Source/Engine/Materials/Material.cpp index 8d3f5dc4..b7c89c1a 100644 --- a/Elixir/Source/Engine/Materials/Material.cpp +++ b/Elixir/Source/Engine/Materials/Material.cpp @@ -16,45 +16,55 @@ namespace Elixir::Materials ++m_Revision; } - bool Material::SetBlendMode(const EMaterialBlendMode mode) + bool Material::SetUsage(const EMaterialUsage usage, const bool enabled) { - if (m_BlendMode == mode) + const uint32_t mask = GetMaterialUsageMask(usage); + const uint32_t updatedMask = enabled + ? m_UsageMask | mask + : m_UsageMask & ~mask; + + if (updatedMask == m_UsageMask) return false; - m_BlendMode = mode; + m_UsageMask = updatedMask; ++m_Revision; return true; } - bool Material::SetAlphaCutoff(const float cutoff) + bool Material::SupportsUsage(const EMaterialUsage usage) const { - const auto normalized = std::min(std::max(0.0f, cutoff), 1.0f); - if (m_AlphaCutoff == normalized) + return (m_UsageMask & GetMaterialUsageMask(usage)) != 0; + } + + bool Material::SetShadingModel(EMaterialShadingModel model) + { + if (m_ShadingModel == model) return false; - m_AlphaCutoff = normalized; + m_ShadingModel = model; ++m_Revision; return true; } - bool Material::SetUsage(const EMaterialUsage usage, const bool enabled) + bool Material::SetBlendMode(const EMaterialBlendMode mode) { - const uint32_t mask = GetMaterialUsageMask(usage); - const uint32_t updatedMask = enabled - ? m_UsageMask | mask - : m_UsageMask & ~mask; - - if (updatedMask == m_UsageMask) + if (m_BlendMode == mode) return false; - m_UsageMask = updatedMask; + m_BlendMode = mode; ++m_Revision; return true; } - bool Material::SupportsUsage(const EMaterialUsage usage) const + bool Material::SetAlphaCutoff(const float cutoff) { - return (m_UsageMask & GetMaterialUsageMask(usage)) != 0; + const auto normalized = std::min(std::max(0.0f, cutoff), 1.0f); + if (m_AlphaCutoff == normalized) + return false; + + m_AlphaCutoff = normalized; + ++m_Revision; + return true; } bool Material::SetDefaultParameter(const std::string& name, const SMaterialParameter& value) diff --git a/Elixir/Source/Engine/Materials/Material.h b/Elixir/Source/Engine/Materials/Material.h index d8aaa713..c0372fef 100644 --- a/Elixir/Source/Engine/Materials/Material.h +++ b/Elixir/Source/Engine/Materials/Material.h @@ -31,9 +31,20 @@ namespace Elixir::Materials Count }; - /** - * @brief Defines how a material combines with the color target. - */ + /** @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. */ @@ -46,17 +57,13 @@ namespace Elixir::Materials Translucent, }; - /** - * @brief Defines the numeric value type used by a material. - */ + /** @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. */ @@ -104,6 +111,31 @@ namespace Elixir::Materials */ const MaterialGraph& GetGraph() const { return m_Graph; } + /** + * @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. + */ + bool SetUsage(EMaterialUsage usage, bool enabled); + + /** + * @brief Checks whether a material usage is supported. + * @param usage Usage to check. + * @return `true` if the usage is enabled. + */ + bool SupportsUsage(EMaterialUsage usage) const; + + /** + * @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 Sets how renderers combine this material with the color target. * @param mode Blend mode to use for every compatible material pass. @@ -127,21 +159,6 @@ namespace Elixir::Materials /** @brief Returns the opacity threshold used by masked material passes. */ float GetAlphaCutoff() const { return m_AlphaCutoff; } - /** - * @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. - */ - bool SetUsage(EMaterialUsage usage, bool enabled); - - /** - * @brief Checks whether a material usage is supported. - * @param usage Usage to check. - * @return `true` if the usage is enabled. - */ - bool SupportsUsage(EMaterialUsage usage) const; - /** * @brief Updates a parameter default value. * @param name Parameter name. @@ -218,6 +235,7 @@ namespace Elixir::Materials MaterialGraph m_Graph; std::unordered_map m_Parameters; uint32_t m_UsageMask = 0; + EMaterialShadingModel m_ShadingModel = EMaterialShadingModel::Lit; EMaterialBlendMode m_BlendMode = EMaterialBlendMode::Opaque; float m_AlphaCutoff = 0.5f; uint32_t m_Revision = 1; diff --git a/Elixir/Source/Engine/Materials/MaterialGraph.cpp b/Elixir/Source/Engine/Materials/MaterialGraph.cpp index ef231573..57e3effc 100644 --- a/Elixir/Source/Engine/Materials/MaterialGraph.cpp +++ b/Elixir/Source/Engine/Materials/MaterialGraph.cpp @@ -11,13 +11,16 @@ 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::AmbientOcclusion: return "AmbientOcclusion"; + 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::ClearCoat: return "ClearCoat"; + case EMaterialChannel::ClearCoatRoughness: return "ClearCoatRoughness"; + case EMaterialChannel::ClearCoatBottomNormal: return "ClearCoatBottomNormal"; } return "BaseColor"; @@ -31,7 +34,9 @@ namespace Elixir::Materials const bool isScalar = channel == EMaterialChannel::Metallic || channel == EMaterialChannel::Roughness || channel == EMaterialChannel::Opacity || - channel == EMaterialChannel::AmbientOcclusion; + channel == EMaterialChannel::AmbientOcclusion || + channel == EMaterialChannel::ClearCoat || + channel == EMaterialChannel::ClearCoatRoughness; if (isScalar) return expression.ValueType == EMaterialValueType::Float diff --git a/Elixir/Source/Engine/Materials/MaterialGraph.h b/Elixir/Source/Engine/Materials/MaterialGraph.h index 64cb4631..bf4158d5 100644 --- a/Elixir/Source/Engine/Materials/MaterialGraph.h +++ b/Elixir/Source/Engine/Materials/MaterialGraph.h @@ -29,6 +29,15 @@ namespace Elixir::Materials /** @brief Ambient-occlusion multiplier for indirect and direct lighting. */ AmbientOcclusion, + + /** @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. */ diff --git a/Elixir/Tests/Engine/Materials/MaterialTest.cpp b/Elixir/Tests/Engine/Materials/MaterialTest.cpp index 36ddd174..bbf53358 100644 --- a/Elixir/Tests/Engine/Materials/MaterialTest.cpp +++ b/Elixir/Tests/Engine/Materials/MaterialTest.cpp @@ -8,6 +8,19 @@ using namespace Elixir; using namespace Elixir::Materials; using namespace Elixir::Materials::Nodes; +TEST(MaterialTest, StoresSurfaceShadingModel) +{ + Material material("ClearCoated"); + + 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"); @@ -15,14 +28,14 @@ TEST(MaterialTest, StoresTransparencySettings) EXPECT_EQ(material.GetBlendMode(), EMaterialBlendMode::Opaque); EXPECT_FLOAT_EQ(material.GetAlphaCutoff(), 0.5f); - const auto initialRevision = material.GetRevision(); + const auto revision = material.GetRevision(); EXPECT_TRUE(material.SetBlendMode(EMaterialBlendMode::Masked)); EXPECT_EQ(material.GetBlendMode(), EMaterialBlendMode::Masked); - EXPECT_EQ(material.GetRevision(), initialRevision + 1); + EXPECT_EQ(material.GetRevision(), revision + 1); EXPECT_TRUE(material.SetAlphaCutoff(0.35f)); EXPECT_FLOAT_EQ(material.GetAlphaCutoff(), 0.35f); - EXPECT_EQ(material.GetRevision(), initialRevision + 2); + EXPECT_EQ(material.GetRevision(), revision + 2); } TEST(MaterialTest, ClampsAlphaCutoff) diff --git a/Shaders/Material/Material.ps.hlsl b/Shaders/Material/Material.ps.hlsl index 785a4aa1..89914b3c 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) @@ -76,6 +84,9 @@ struct Surface float Opacity; float3 Emissive; float AmbientOcclusion; + float ClearCoat; + float ClearCoatRoughness; + float3 ClearCoatBottomNormal; }; static const uint NO_TEXTURE = 0xFFFFFFFFu; @@ -151,6 +162,9 @@ float4 main(PSInput input) : SV_Target0 surface.Opacity = 1.0f; surface.Emissive = float3(0.0f, 0.0f, 0.0f); surface.AmbientOcclusion = 1.0f; + surface.ClearCoat = 0.0f; + surface.ClearCoatRoughness = 0.0f; + surface.ClearCoatBottomNormal = float3(0.0f, 0.0f, 1.0f); // __GRAPH_BODY__ @@ -160,6 +174,12 @@ float4 main(PSInput input) : SV_Target0 clip(surface.Opacity - mat.AlphaCutoff); } + +#if MATERIAL_SHADING_MODEL == MATERIAL_SHADING_MODEL_UNLIT + const float3 unlit = ACESFilm(surface.BaseColor + surface.Emissive); + return float4(unlit, surface.Opacity); +#endif + float roughness = clamp(surface.Roughness, 0.045f, 1.0f); float3 F0 = lerp(0.04f.xxx, surface.BaseColor, surface.Metallic); @@ -168,7 +188,14 @@ float4 main(PSInput input) : SV_Target0 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; - N = normalize(mul(tangentNormal, float3x3(tangent, bitangent, N))); + const float3x3 tangentBasis = float3x3(tangent, bitangent, N); + +#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 float NdotV = saturate(dot(N, V)) + 1e-4f; @@ -189,6 +216,18 @@ float4 main(PSInput input) : SV_Target0 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 * ao; +#if MATERIAL_SHADING_MODEL == MATERIAL_SHADING_MODEL_CLEAR_COAT + const float clearCoat = saturate(surface.ClearCoat); + if (clearCoat > 0.0f) + { + const float coatRoughness = clamp(surface.ClearCoatRoughness, 0.045f, 1.0f); + const float coatNdotV = saturate(dot(coatNormal, V)) + 1e-4f; + const float coatFresnel = clearCoat * (0.04f + 0.96f * pow(saturate(1.0f - coatNdotV), 5.0f)); + const float3 coatReflection = SampleEnv(reflect(-V, coatNormal), coatRoughness); + color = color * (1.0f - coatFresnel) + coatReflection * coatFresnel; + } +#endif + // Tone mapping color = ACESFilm(color); From 441be03092af0e3c996265c7697e61cdd91eb962 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Tue, 22 Sep 2026 23:03:23 -0300 Subject: [PATCH 13/61] Import glTF clear coat materials --- .../Platform/GLTF/GLTFStaticMeshLoader.cpp | 181 +++++++++++++++++- .../Platform/GLTF/GLTFStaticMeshLoader.h | 4 +- 2 files changed, 180 insertions(+), 5 deletions(-) diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp index e11f4f2c..2dff35f9 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp @@ -250,7 +250,10 @@ namespace Elixir const Ref& metallicRoughnessTexture, const Ref& normalTexture, const Ref& occlusionTexture, - const Ref& emissiveTexture + const Ref& emissiveTexture, + const Ref& clearCoatTexture, + const Ref& clearCoatRoughnessTexture, + const Ref& clearCoatNormalTexture ) { using namespace Materials; @@ -264,6 +267,9 @@ namespace Elixir material->SetUsage(EMaterialUsage::Surface, true); material->SetBlendMode(GetBlendMode(source.alphaMode)); + if (source.clearcoat) + material->SetShadingModel(EMaterialShadingModel::ClearCoat); + if (source.alphaMode == fastgltf::AlphaMode::Mask) material->SetAlphaCutoff(source.alphaCutoff); @@ -319,6 +325,35 @@ namespace Elixir source.occlusionTexture ? source.occlusionTexture->strength : 1.0f), }), "Could not define OcclusionStrength.") + 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( @@ -425,7 +460,12 @@ namespace Elixir graph.Connect(sample, flatten, 0); graph.Connect(scale, flatten, 1); - graph.SetChannel(EMaterialChannel::Normal, flatten); + graph.SetChannel( + source.clearcoat + ? EMaterialChannel::ClearCoatBottomNormal + : EMaterialChannel::Normal, + flatten + ); } if (occlusionTexture) @@ -457,6 +497,92 @@ namespace Elixir graph.SetChannel(EMaterialChannel::AmbientOcclusion, occlusion); } + if (source.clearcoat) + { + auto clearCoat = graph.AddNode( + "ClearCoatFactor", + EMaterialValueType::Float + ); + + if (clearCoatTexture) + { + EE_CORE_ASSERT(material->DefineParameter("ClearCoatTexture", { + .Kind = EMaterialParameterKind::Texture, + .DefaultValue = SMaterialParameter::MakeTexture(clearCoatTexture), + }), "Could not define ClearCoatTexture."); + + const auto sample = graph.AddNode( + "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 (clearCoatRoughnessTexture) + { + EE_CORE_ASSERT(material->DefineParameter("ClearCoatRoughnessTexture", { + .Kind = EMaterialParameterKind::Texture, + .DefaultValue = SMaterialParameter::MakeTexture( + clearCoatRoughnessTexture + ), + }), "Could not define ClearCoatRoughnessTexture."); + + const auto sample = graph.AddNode( + "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 (clearCoatNormalTexture) + { + EE_CORE_ASSERT(material->DefineParameter("ClearCoatNormalTexture", { + .Kind = EMaterialParameterKind::Texture, + .DefaultValue = SMaterialParameter::MakeTexture( + clearCoatNormalTexture + ), + }), "Could not define ClearCoatNormalTexture."); + + const auto sample = graph.AddNode( + "ClearCoatNormalTexture", + ETextureSampleType::Normal + ); + + const auto scale = graph.AddNode( + "ClearCoatNormalScale", + EMaterialValueType::Float + ); + + const auto flatten = graph.AddNode(); + graph.Connect(sample, flatten, 0); + graph.Connect(scale, flatten, 1); + + graph.SetChannel(EMaterialChannel::Normal, flatten); + } + } + material->SetGraph(std::move(graph)); return material; } @@ -482,6 +608,9 @@ namespace Elixir Ref normalTexture; Ref occlusionTexture; Ref emissiveTexture; + Ref clearCoatTexture; + Ref clearCoatRoughnessTexture; + Ref clearCoatNormalTexture; if (source.pbrData.baseColorTexture) { @@ -543,6 +672,45 @@ namespace Elixir ); } + if (source.clearcoat) + { + if (source.clearcoat->clearcoatTexture) + { + clearCoatTexture = LoadTexture( + context, + asset, + source.clearcoat->clearcoatTexture->textureIndex, + sourceDirectory, + EImageFormat::R8G8B8A8_UNORM, + textureCache + ); + } + + if (source.clearcoat->clearcoatRoughnessTexture) + { + clearCoatRoughnessTexture = LoadTexture( + context, + asset, + source.clearcoat->clearcoatRoughnessTexture->textureIndex, + sourceDirectory, + EImageFormat::R8G8B8A8_UNORM, + textureCache + ); + } + + if (source.clearcoat->clearcoatNormalTexture) + { + clearCoatNormalTexture = LoadTexture( + context, + asset, + source.clearcoat->clearcoatNormalTexture->textureIndex, + sourceDirectory, + EImageFormat::R8G8B8A8_UNORM, + textureCache + ); + } + } + mesh.Materials.push_back(CreateSurfaceMaterial( source, i, @@ -551,7 +719,10 @@ namespace Elixir metallicRoughnessTexture, normalTexture, occlusionTexture, - emissiveTexture + emissiveTexture, + clearCoatTexture, + clearCoatRoughnessTexture, + clearCoatNormalTexture )); } } @@ -763,7 +934,9 @@ namespace Elixir return std::nullopt; } - fastgltf::Parser parser; + fastgltf::Parser parser{ + fastgltf::Extensions::KHR_materials_clearcoat + }; auto loadResult = parser.loadGltf( data.get(), diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h index 15699c70..921abe1e 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h @@ -14,7 +14,9 @@ namespace Elixir * 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. + * 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. * * @param context Graphics context available for material resolution. * @param path Local glTF or GLB file path. From 3cfc4083549d94544b476e740dc2a2ed6308f29b Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 23 Sep 2026 01:30:00 -0300 Subject: [PATCH 14/61] Import glTF specular materials --- .../Source/Engine/Materials/MaterialGraph.cpp | 3 + .../Source/Engine/Materials/MaterialGraph.h | 6 + .../Platform/GLTF/GLTFStaticMeshLoader.cpp | 108 ++++++++++++++++++ .../Platform/GLTF/GLTFStaticMeshLoader.h | 2 + .../Engine/Materials/MaterialGraphTest.cpp | 28 +++++ Shaders/Material/Material.ps.hlsl | 12 +- 6 files changed, 157 insertions(+), 2 deletions(-) diff --git a/Elixir/Source/Engine/Materials/MaterialGraph.cpp b/Elixir/Source/Engine/Materials/MaterialGraph.cpp index 57e3effc..9c20bdf7 100644 --- a/Elixir/Source/Engine/Materials/MaterialGraph.cpp +++ b/Elixir/Source/Engine/Materials/MaterialGraph.cpp @@ -18,6 +18,8 @@ namespace Elixir::Materials 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"; @@ -35,6 +37,7 @@ namespace Elixir::Materials channel == EMaterialChannel::Roughness || channel == EMaterialChannel::Opacity || channel == EMaterialChannel::AmbientOcclusion || + channel == EMaterialChannel::Specular || channel == EMaterialChannel::ClearCoat || channel == EMaterialChannel::ClearCoatRoughness; diff --git a/Elixir/Source/Engine/Materials/MaterialGraph.h b/Elixir/Source/Engine/Materials/MaterialGraph.h index bf4158d5..e5ec8988 100644 --- a/Elixir/Source/Engine/Materials/MaterialGraph.h +++ b/Elixir/Source/Engine/Materials/MaterialGraph.h @@ -30,6 +30,12 @@ namespace Elixir::Materials /** @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, diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp index 2dff35f9..a0efba42 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp @@ -251,6 +251,8 @@ namespace Elixir const Ref& normalTexture, const Ref& occlusionTexture, const Ref& emissiveTexture, + const Ref& specularTexture, + const Ref& specularColorTexture, const Ref& clearCoatTexture, const Ref& clearCoatRoughnessTexture, const Ref& clearCoatNormalTexture @@ -325,6 +327,29 @@ namespace Elixir 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", { @@ -497,6 +522,57 @@ namespace Elixir graph.SetChannel(EMaterialChannel::AmbientOcclusion, occlusion); } + if (source.specular) + { + auto specular = graph.AddNode( + "SpecularFactor", + EMaterialValueType::Float + ); + + if (specularTexture) + { + EE_CORE_ASSERT(material->DefineParameter("SpecularTexture", { + .Kind = EMaterialParameterKind::Texture, + .DefaultValue = SMaterialParameter::MakeTexture(specularTexture), + }), "Could not define SpecularTexture."); + + const auto sample = graph.AddNode( + "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 (specularColorTexture) + { + EE_CORE_ASSERT(material->DefineParameter("SpecularColorTexture", { + .Kind = EMaterialParameterKind::Texture, + .DefaultValue = SMaterialParameter::MakeTexture(specularColorTexture), + }), "Could not define SpecularColorTexture."); + + const auto sample = graph.AddNode("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( @@ -608,6 +684,8 @@ namespace Elixir Ref normalTexture; Ref occlusionTexture; Ref emissiveTexture; + Ref specularTexture; + Ref specularColorTexture; Ref clearCoatTexture; Ref clearCoatRoughnessTexture; Ref clearCoatNormalTexture; @@ -672,6 +750,33 @@ namespace Elixir ); } + if (source.specular) + { + if (source.specular->specularTexture) + { + specularTexture = LoadTexture( + context, + asset, + source.specular->specularTexture->textureIndex, + sourceDirectory, + EImageFormat::R8G8B8A8_UNORM, + textureCache + ); + } + + if (source.specular->specularColorTexture) + { + specularColorTexture = LoadTexture( + context, + asset, + source.specular->specularColorTexture->textureIndex, + sourceDirectory, + EImageFormat::R8G8B8A8_SRGB, + textureCache + ); + } + } + if (source.clearcoat) { if (source.clearcoat->clearcoatTexture) @@ -720,6 +825,8 @@ namespace Elixir normalTexture, occlusionTexture, emissiveTexture, + specularTexture, + specularColorTexture, clearCoatTexture, clearCoatRoughnessTexture, clearCoatNormalTexture @@ -935,6 +1042,7 @@ namespace Elixir } fastgltf::Parser parser{ + fastgltf::Extensions::KHR_materials_specular | fastgltf::Extensions::KHR_materials_clearcoat }; diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h index 921abe1e..54a38ffe 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h @@ -17,6 +17,8 @@ namespace Elixir * 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. * * @param context Graphics context available for material resolution. * @param path Local glTF or GLB file path. diff --git a/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp b/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp index 09f1020a..83d362bc 100644 --- a/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp +++ b/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp @@ -80,6 +80,34 @@ TEST(MaterialGraphTest, RoutesTextureAlphaToOpacity) EXPECT_NE(hlsl.find(".w"), 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, GeneratesNormalTextureSamplingAndFlattening) { MaterialGraph graph; diff --git a/Shaders/Material/Material.ps.hlsl b/Shaders/Material/Material.ps.hlsl index 89914b3c..415abc85 100644 --- a/Shaders/Material/Material.ps.hlsl +++ b/Shaders/Material/Material.ps.hlsl @@ -84,6 +84,8 @@ struct Surface float Opacity; float3 Emissive; float AmbientOcclusion; + float Specular; + float3 SpecularColor; float ClearCoat; float ClearCoatRoughness; float3 ClearCoatBottomNormal; @@ -162,6 +164,8 @@ float4 main(PSInput input) : SV_Target0 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); @@ -174,14 +178,18 @@ float4 main(PSInput input) : SV_Target0 clip(surface.Opacity - mat.AlphaCutoff); } - #if MATERIAL_SHADING_MODEL == MATERIAL_SHADING_MODEL_UNLIT const float3 unlit = ACESFilm(surface.BaseColor + surface.Emissive); return float4(unlit, surface.Opacity); #endif float roughness = clamp(surface.Roughness, 0.045f, 1.0f); - float3 F0 = lerp(0.04f.xxx, surface.BaseColor, surface.Metallic); + + 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. From 4eccd7bdc63b79d7edc833e4ac72dbf7cdf7242c Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 23 Sep 2026 01:31:18 -0300 Subject: [PATCH 15/61] Preserve assertions in disabled builds --- Elixir/Source/Engine/Core/Core.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Elixir/Source/Engine/Core/Core.h b/Elixir/Source/Engine/Core/Core.h index df4a7541..6b3778c1 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) From 75b734e7741666def6dc8a2dad3b9ca5198c4154 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 23 Sep 2026 01:47:23 -0300 Subject: [PATCH 16/61] Light clear coat with directional lights --- Shaders/Material/Material.ps.hlsl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Shaders/Material/Material.ps.hlsl b/Shaders/Material/Material.ps.hlsl index 415abc85..38881b10 100644 --- a/Shaders/Material/Material.ps.hlsl +++ b/Shaders/Material/Material.ps.hlsl @@ -233,6 +233,18 @@ float4 main(PSInput input) : SV_Target0 const float coatFresnel = clearCoat * (0.04f + 0.96f * pow(saturate(1.0f - coatNdotV), 5.0f)); const float3 coatReflection = SampleEnv(reflect(-V, coatNormal), coatRoughness); color = color * (1.0f - coatFresnel) + coatReflection * coatFresnel; + + const float coatNdotL = saturate(dot(coatNormal, L)); + 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 coatSpecular = pow(coatNdotH, max(2.0f, (1.0f - coatRoughness) * 128.0f)); + const float coatDirectFresnel = clearCoat * (0.04f + 0.96f * pow(1.0f - coatVdotH, 5.0f)); + + color += coatDirectFresnel * coatSpecular * LightColor.rgb * LightColor.a * coatNdotL; + } } #endif From 5cc75e44dc72dc8066338e1170ef28ea46eba0de Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 23 Sep 2026 02:04:41 -0300 Subject: [PATCH 17/61] Support secondary mesh texture coordinates --- Elixir/Source/Engine/Materials/Nodes/Dot.h | 2 +- .../Source/Engine/Materials/Nodes/Fresnel.h | 4 ++-- .../Source/Engine/Materials/Nodes/OneMinus.h | 2 +- Elixir/Source/Engine/Materials/Nodes/Power.h | 2 +- .../Source/Engine/Materials/Nodes/Saturate.h | 2 +- Elixir/Source/Engine/Materials/Nodes/Sine.h | 2 +- .../Source/Engine/Materials/Nodes/TexCoord.h | 22 +++++++++++++++---- Elixir/Source/Engine/Mesh/StaticMesh.cpp | 9 ++++---- Elixir/Source/Engine/Mesh/StaticMesh.h | 3 +++ .../Platform/GLTF/GLTFStaticMeshLoader.cpp | 13 +++++++++-- .../Platform/GLTF/GLTFStaticMeshLoader.h | 2 ++ .../Engine/Materials/MaterialGraphTest.cpp | 14 ++++++++++++ Shaders/Material/Material.ps.hlsl | 1 + Shaders/Material/Surface.vs.hlsl | 11 ++++++---- 14 files changed, 68 insertions(+), 21 deletions(-) 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/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/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/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/Mesh/StaticMesh.cpp b/Elixir/Source/Engine/Mesh/StaticMesh.cpp index 74c80999..f2c7d9cb 100644 --- a/Elixir/Source/Engine/Mesh/StaticMesh.cpp +++ b/Elixir/Source/Engine/Mesh/StaticMesh.cpp @@ -61,10 +61,11 @@ namespace Elixir const BufferLayout& StaticMesh::GetVertexLayout() { static const BufferLayout layout = {{ - { EDataType::Vec3, "Position" }, - { EDataType::Vec3, "Normal" }, - { EDataType::Vec4, "Tangent" }, - { EDataType::Vec2, "TexCoord" }, + { EDataType::Vec3, "Position" }, + { EDataType::Vec3, "Normal" }, + { EDataType::Vec4, "Tangent" }, + { EDataType::Vec2, "TexCoord" }, + { EDataType::Vec2, "TexCoord1" }, }}; return layout; } diff --git a/Elixir/Source/Engine/Mesh/StaticMesh.h b/Elixir/Source/Engine/Mesh/StaticMesh.h index 6f28dde3..fb1f5885 100644 --- a/Elixir/Source/Engine/Mesh/StaticMesh.h +++ b/Elixir/Source/Engine/Mesh/StaticMesh.h @@ -23,6 +23,9 @@ namespace Elixir /** First texture coordinate channel. */ glm::vec2 TexCoord{}; + + /** Second texture coordinate channel. */ + glm::vec2 TexCoord1{}; }; /** @brief Defines an axis-aligned bound in mesh-local space. */ diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp index a0efba42..959dd122 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp @@ -282,12 +282,12 @@ namespace Elixir pbr.baseColorFactor.z(), pbr.baseColorFactor.w() ); - const auto emissiveFactor = glm::vec4( + 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, @@ -949,6 +949,14 @@ namespace Elixir } ); + 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 && @@ -1042,6 +1050,7 @@ namespace Elixir } fastgltf::Parser parser{ + fastgltf::Extensions::KHR_materials_emissive_strength | fastgltf::Extensions::KHR_materials_specular | fastgltf::Extensions::KHR_materials_clearcoat }; diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h index 54a38ffe..d3e26aad 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h @@ -19,6 +19,8 @@ namespace Elixir * 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. * * @param context Graphics context available for material resolution. * @param path Local glTF or GLB file path. diff --git a/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp b/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp index 83d362bc..80891bd6 100644 --- a/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp +++ b/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include using namespace Elixir; @@ -108,6 +109,19 @@ TEST(MaterialGraphTest, RoutesSpecularInputs) 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, GeneratesNormalTextureSamplingAndFlattening) { MaterialGraph graph; diff --git a/Shaders/Material/Material.ps.hlsl b/Shaders/Material/Material.ps.hlsl index 38881b10..d922aa44 100644 --- a/Shaders/Material/Material.ps.hlsl +++ b/Shaders/Material/Material.ps.hlsl @@ -71,6 +71,7 @@ struct PSInput float3 Normal : NORMAL0; float4 Tangent : TANGENT0; float2 TexCoord : TEXCOORD0; + float2 TexCoord1 : TEXCOORD1; float3 WorldPos : POSITION0; bool FrontFace : SV_IsFrontFace; }; diff --git a/Shaders/Material/Surface.vs.hlsl b/Shaders/Material/Surface.vs.hlsl index 4f5169b5..0c6183e3 100644 --- a/Shaders/Material/Surface.vs.hlsl +++ b/Shaders/Material/Surface.vs.hlsl @@ -28,10 +28,11 @@ MaterialPushConstants pc; struct VSInput { - float3 Position : POSITION0; - float3 Normal : NORMAL0; - float4 Tangent : TANGENT0; - float2 TexCoord : TEXCOORD0; + float3 Position : POSITION0; + float3 Normal : NORMAL0; + float4 Tangent : TANGENT0; + float2 TexCoord : TEXCOORD0; + float2 TexCoord1 : TEXCOORD1; }; struct VSOutput @@ -40,6 +41,7 @@ struct VSOutput float3 Normal : NORMAL0; float4 Tangent : TANGENT0; float2 TexCoord : TEXCOORD0; + float2 TexCoord1 : TEXCOORD1; float3 WorldPos : POSITION0; }; @@ -56,6 +58,7 @@ VSOutput main(VSInput input) 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 From 92ced7ba5a0fa5747484c7fc8791d028d0fe5810 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 23 Sep 2026 02:29:39 -0300 Subject: [PATCH 18/61] Support double-sided materials --- Elixir/Source/Engine/Materials/Material.cpp | 10 ++++++++++ Elixir/Source/Engine/Materials/Material.h | 11 +++++++++++ .../Materials/Rendering/MaterialRenderProxy.cpp | 1 + .../Materials/Rendering/MaterialRenderProxy.h | 4 ++++ .../Engine/Materials/Rendering/Renderer.cpp | 10 +++++++++- .../Source/Engine/Materials/Rendering/Renderer.h | 4 ++++ .../Source/Platform/GLTF/GLTFStaticMeshLoader.cpp | 1 + Elixir/Tests/Engine/Materials/MaterialTest.cpp | 13 +++++++++++++ .../Rendering/MaterialRenderProxyTest.cpp | 15 +++++++++++++++ 9 files changed, 68 insertions(+), 1 deletion(-) diff --git a/Elixir/Source/Engine/Materials/Material.cpp b/Elixir/Source/Engine/Materials/Material.cpp index b7c89c1a..4c923565 100644 --- a/Elixir/Source/Engine/Materials/Material.cpp +++ b/Elixir/Source/Engine/Materials/Material.cpp @@ -67,6 +67,16 @@ namespace Elixir::Materials return true; } + bool Material::SetDoubleSided(bool enabled) + { + if (m_DoubleSided == enabled) + return false; + + m_DoubleSided = enabled; + ++m_Revision; + return true; + } + bool Material::SetDefaultParameter(const std::string& name, const SMaterialParameter& value) { const auto it = m_Parameters.find(name); diff --git a/Elixir/Source/Engine/Materials/Material.h b/Elixir/Source/Engine/Materials/Material.h index c0372fef..6acd85f3 100644 --- a/Elixir/Source/Engine/Materials/Material.h +++ b/Elixir/Source/Engine/Materials/Material.h @@ -159,6 +159,16 @@ namespace Elixir::Materials /** @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. * @param name Parameter name. @@ -237,6 +247,7 @@ namespace Elixir::Materials uint32_t m_UsageMask = 0; EMaterialShadingModel m_ShadingModel = EMaterialShadingModel::Lit; EMaterialBlendMode m_BlendMode = EMaterialBlendMode::Opaque; + bool m_DoubleSided = false; float m_AlphaCutoff = 0.5f; uint32_t m_Revision = 1; }; diff --git a/Elixir/Source/Engine/Materials/Rendering/MaterialRenderProxy.cpp b/Elixir/Source/Engine/Materials/Rendering/MaterialRenderProxy.cpp index bd7d4cc3..3cb41468 100644 --- a/Elixir/Source/Engine/Materials/Rendering/MaterialRenderProxy.cpp +++ b/Elixir/Source/Engine/Materials/Rendering/MaterialRenderProxy.cpp @@ -21,6 +21,7 @@ namespace Elixir::Materials::Rendering 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 99cdf5f8..64a9027e 100644 --- a/Elixir/Source/Engine/Materials/Rendering/MaterialRenderProxy.h +++ b/Elixir/Source/Engine/Materials/Rendering/MaterialRenderProxy.h @@ -63,6 +63,9 @@ namespace Elixir::Materials::Rendering /** @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; } @@ -72,6 +75,7 @@ namespace Elixir::Materials::Rendering 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/Renderer.cpp b/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp index bffd1a26..e99e8b9c 100644 --- a/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp +++ b/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp @@ -213,6 +213,7 @@ 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 @@ -278,6 +279,7 @@ namespace Elixir::Materials::Rendering .Key = { .Pass = item.Pass, .BlendMode = prepared->Proxy->GetBlendMode(), + .DoubleSided = prepared->Proxy->IsDoubleSided(), .GeometryIndex = item.GeometryIndex, .Indexed = item.IndexedDraw.has_value(), .Program = *program, @@ -399,6 +401,7 @@ namespace Elixir::Materials::Rendering .Pipeline = GetPipeline( request.Pass, request.Material->GetBlendMode(), + request.Material->IsDoubleSided(), shader, request.Pipeline ), @@ -414,6 +417,7 @@ namespace Elixir::Materials::Rendering Ref Renderer::GetPipeline( const EMaterialPass pass, const EMaterialBlendMode blendMode, + const bool doubleSided, const Ref& shader, const SPipelineRequest& request ) @@ -421,6 +425,7 @@ namespace Elixir::Materials::Rendering const SPipelineKey key{ .Pass = pass, .BlendMode = blendMode, + .DoubleSided = doubleSided, .Shader = shader.get(), .VertexLayoutKey = request.VertexLayoutKey, }; @@ -441,7 +446,10 @@ namespace Elixir::Materials::Rendering { case EMaterialPass::Surface: case EMaterialPass::ParticleMesh: - builder.SetCullMode(ECullMode::Back, EFrontFace::CounterClockwise); + builder.SetCullMode( + doubleSided ? ECullMode::None : ECullMode::Back, + EFrontFace::CounterClockwise + ); break; case EMaterialPass::ParticleSprite: case EMaterialPass::ParticleRibbon: diff --git a/Elixir/Source/Engine/Materials/Rendering/Renderer.h b/Elixir/Source/Engine/Materials/Rendering/Renderer.h index 4b34c7dd..44a27873 100644 --- a/Elixir/Source/Engine/Materials/Rendering/Renderer.h +++ b/Elixir/Source/Engine/Materials/Rendering/Renderer.h @@ -279,6 +279,7 @@ namespace Elixir::Materials::Rendering { EMaterialPass Pass = EMaterialPass::ParticleSprite; EMaterialBlendMode BlendMode = EMaterialBlendMode::Opaque; + bool DoubleSided = false; const Shader* Shader = nullptr; uint64_t VertexLayoutKey = 0; @@ -293,6 +294,7 @@ 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; } @@ -324,6 +326,7 @@ namespace Elixir::Materials::Rendering { EMaterialPass Pass = EMaterialPass::ParticleSprite; EMaterialBlendMode BlendMode = EMaterialBlendMode::Opaque; + bool DoubleSided = false; uint32_t GeometryIndex = UINT32_MAX; bool Indexed = false; SProgramKey Program; @@ -356,6 +359,7 @@ namespace Elixir::Materials::Rendering Ref GetPipeline( EMaterialPass pass, EMaterialBlendMode blendMode, + bool doubleSided, const Ref& shader, const SPipelineRequest& request ); diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp index 959dd122..9e862710 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp @@ -268,6 +268,7 @@ namespace Elixir const auto material = CreateRef(name); material->SetUsage(EMaterialUsage::Surface, true); material->SetBlendMode(GetBlendMode(source.alphaMode)); + material->SetDoubleSided(source.doubleSided); if (source.clearcoat) material->SetShadingModel(EMaterialShadingModel::ClearCoat); diff --git a/Elixir/Tests/Engine/Materials/MaterialTest.cpp b/Elixir/Tests/Engine/Materials/MaterialTest.cpp index bbf53358..62a718f6 100644 --- a/Elixir/Tests/Engine/Materials/MaterialTest.cpp +++ b/Elixir/Tests/Engine/Materials/MaterialTest.cpp @@ -38,6 +38,19 @@ TEST(MaterialTest, StoresTransparencySettings) 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"); 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()); +} From 38a82cb2095e30f33bd5e4c68eb376431923353a Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 23 Sep 2026 12:56:31 -0300 Subject: [PATCH 19/61] Import glTF texture transforms --- Elixir/Source/Engine/Materials/Nodes/Append.h | 52 +++++ Elixir/Source/Engine/Materials/Nodes/Cosine.h | 22 ++ .../Materials/Nodes/UnaryOperationNode.h | 10 +- .../Platform/GLTF/GLTFStaticMeshLoader.cpp | 200 +++++++++++++++++- .../Platform/GLTF/GLTFStaticMeshLoader.h | 2 + .../Engine/Materials/MaterialGraphTest.cpp | 73 +++++++ 6 files changed, 343 insertions(+), 16 deletions(-) create mode 100644 Elixir/Source/Engine/Materials/Nodes/Append.h create mode 100644 Elixir/Source/Engine/Materials/Nodes/Cosine.h 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/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/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/Platform/GLTF/GLTFStaticMeshLoader.cpp b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp index 9e862710..42cb0401 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp @@ -6,12 +6,19 @@ #include #include #include +#include +#include #include #include +#include +#include #include #include #include #include +#include +#include +#include #include #include @@ -242,6 +249,136 @@ namespace Elixir } } + uint32_t 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 AddTextureSample( + MaterialGraph& graph, + Material& material, + const std::string_view parameterName, + const fastgltf::TextureInfo& source, + const Nodes::ETextureSampleType sampleType = Nodes::ETextureSampleType::Color + ) + { + using namespace Materials; + using namespace Materials::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 CreateSurfaceMaterial( const fastgltf::Material& source, const size_t materialIndex, @@ -394,7 +531,12 @@ namespace Elixir .DefaultValue = SMaterialParameter::MakeTexture(baseColorTexture), }), "Could not define BaseColorTexture."); - const auto texture = graph.AddNode("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); @@ -418,7 +560,13 @@ namespace Elixir .DefaultValue = SMaterialParameter::MakeTexture(metallicRoughnessTexture), }), "Could not define MetallicRoughnessTexture."); - const auto sample = graph.AddNode("MetallicRoughnessTexture"); + const auto sample = AddTextureSample( + graph, + *material, + "MetallicRoughnessTexture", + *pbr.metallicRoughnessTexture, + ETextureSampleType::LinearColor + ); const auto textureRoughness = graph.AddNode(1); graph.Connect(sample, textureRoughness, 0); @@ -449,7 +597,12 @@ namespace Elixir .DefaultValue = SMaterialParameter::MakeTexture(emissiveTexture), }), "Could not define EmissiveTexture."); - const auto sample = graph.AddNode("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); @@ -472,8 +625,11 @@ namespace Elixir .DefaultValue = SMaterialParameter::MakeTexture(normalTexture), }), "Could not define NormalTexture."); - const auto sample = graph.AddNode( + const auto sample = AddTextureSample( + graph, + *material, "NormalTexture", + *source.normalTexture, ETextureSampleType::Normal ); @@ -501,7 +657,13 @@ namespace Elixir .DefaultValue = SMaterialParameter::MakeTexture(occlusionTexture), }), "Could not define OcclusionTexture."); - const auto sample = graph.AddNode("OcclusionTexture"); + const auto sample = AddTextureSample( + graph, + *material, + "OcclusionTexture", + *source.occlusionTexture, + ETextureSampleType::LinearColor + ); const auto red = graph.AddNode(0); graph.Connect(sample, red, 0); @@ -537,8 +699,11 @@ namespace Elixir .DefaultValue = SMaterialParameter::MakeTexture(specularTexture), }), "Could not define SpecularTexture."); - const auto sample = graph.AddNode( + const auto sample = AddTextureSample( + graph, + *material, "SpecularTexture", + *source.specular->specularTexture, ETextureSampleType::LinearColor ); const auto alpha = graph.AddNode(3); @@ -562,7 +727,12 @@ namespace Elixir .DefaultValue = SMaterialParameter::MakeTexture(specularColorTexture), }), "Could not define SpecularColorTexture."); - const auto sample = graph.AddNode("SpecularColorTexture"); + const auto sample = AddTextureSample( + graph, + *material, + "SpecularColorTexture", + *source.specular->specularColorTexture + ); const auto multiply = graph.AddNode(); graph.Connect(specularColor, multiply, 0); @@ -588,8 +758,11 @@ namespace Elixir .DefaultValue = SMaterialParameter::MakeTexture(clearCoatTexture), }), "Could not define ClearCoatTexture."); - const auto sample = graph.AddNode( + const auto sample = AddTextureSample( + graph, + *material, "ClearCoatTexture", + *source.clearcoat->clearcoatTexture, ETextureSampleType::LinearColor ); const auto red = graph.AddNode(0); @@ -615,8 +788,11 @@ namespace Elixir ), }), "Could not define ClearCoatRoughnessTexture."); - const auto sample = graph.AddNode( + const auto sample = AddTextureSample( + graph, + *material, "ClearCoatRoughnessTexture", + *source.clearcoat->clearcoatRoughnessTexture, ETextureSampleType::LinearColor ); const auto green = graph.AddNode(1); @@ -642,8 +818,11 @@ namespace Elixir ), }), "Could not define ClearCoatNormalTexture."); - const auto sample = graph.AddNode( + const auto sample = AddTextureSample( + graph, + *material, "ClearCoatNormalTexture", + *source.clearcoat->clearcoatNormalTexture, ETextureSampleType::Normal ); @@ -1053,6 +1232,7 @@ namespace Elixir fastgltf::Parser parser{ fastgltf::Extensions::KHR_materials_emissive_strength | fastgltf::Extensions::KHR_materials_specular | + fastgltf::Extensions::KHR_texture_transform | fastgltf::Extensions::KHR_materials_clearcoat }; diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h index d3e26aad..6029487e 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h @@ -21,6 +21,8 @@ namespace Elixir * 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. * * @param context Graphics context available for material resolution. * @param path Local glTF or GLB file path. diff --git a/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp b/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp index 80891bd6..94e368d6 100644 --- a/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp +++ b/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp @@ -2,6 +2,10 @@ #include +#include +#include +#include +#include #include #include #include @@ -9,6 +13,8 @@ #include #include #include +#include +#include #include #include @@ -122,6 +128,73 @@ TEST(MaterialGraphTest, SelectsSecondStaticMeshTextureCoordinate) 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(mat.Values[2].x)"), std::string::npos); + EXPECT_NE(hlsl.find("sin(mat.Values[2].x)"), 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; From 376b4e589f9ebc9c760668b73e886cf66c506b7b Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 23 Sep 2026 16:39:40 -0300 Subject: [PATCH 20/61] Improve surface PBR specular lighting --- Shaders/Material/Material.ps.hlsl | 115 +++++++++++++++++++++++++----- 1 file changed, 96 insertions(+), 19 deletions(-) diff --git a/Shaders/Material/Material.ps.hlsl b/Shaders/Material/Material.ps.hlsl index d922aa44..c933b3e5 100644 --- a/Shaders/Material/Material.ps.hlsl +++ b/Shaders/Material/Material.ps.hlsl @@ -141,6 +141,56 @@ float3 SampleEnv(float3 dir, float roughness) ).rgb * EnvIntensity; } +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); +} + +float GeometrySchlickGGX(float NdotX, float roughness) +{ + const float roughnessPlusOne = roughness + 1.0f; + const float k = (roughnessPlusOne * roughnessPlusOne) * 0.125f; + return NdotX / max(NdotX * (1.0f - k) + k, 1e-7f); +} + +float GeometrySmith(float NdotV, float NdotL, float roughness) +{ + 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); +} + +/** + * 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; +} + float3 ACESFilm(float3 x) { const float a = 2.51f, b = 0.03f, c = 2.43f, d = 0.59f, e = 0.14f; @@ -206,24 +256,42 @@ float4 main(PSInput input) : SV_Target0 N = normalize(mul(tangentNormal, tangentBasis)); #endif + float3 R = reflect(-V, N); + float3 L = normalize(LightDirection.xyz); float NdotV = saturate(dot(N, V)) + 1e-4f; - + float NdotL = saturate(dot(N, L)); float ao = saturate(surface.AmbientOcclusion); - float3 diffuse = SampleIrradiance(N) * surface.BaseColor * (1.0f - surface.Metallic) * ao; - float3 R = reflect(-V, N); + // 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 = SampleEnv(R, roughness) * (F0 * environmentBRDF.x + environmentBRDF.y); - float3 fresnel = F0 + (max((1.0f - roughness).xxx, F0) - F0) * pow(saturate(1.0f - NdotV), 5.0f); - float3 specular = SampleEnv(R, roughness) * fresnel; + float3 color = (diffuseWeightIBL * diffuseIBL + specularIBL) * ao + surface.Emissive; - float3 color = diffuse + specular + 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)); - // Direcional light: Lambert diffuse + a simple spec. - float3 L = normalize(LightDirection.xyz); - 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 * ao; + 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; + + color += (diffuseWeightDirect * surface.BaseColor / 3.14159265359f + specularDirect) * + radiance * NdotL * ao; + } #if MATERIAL_SHADING_MODEL == MATERIAL_SHADING_MODEL_CLEAR_COAT const float clearCoat = saturate(surface.ClearCoat); @@ -231,20 +299,29 @@ float4 main(PSInput input) : SV_Target0 { const float coatRoughness = clamp(surface.ClearCoatRoughness, 0.045f, 1.0f); const float coatNdotV = saturate(dot(coatNormal, V)) + 1e-4f; - const float coatFresnel = clearCoat * (0.04f + 0.96f * pow(saturate(1.0f - coatNdotV), 5.0f)); - const float3 coatReflection = SampleEnv(reflect(-V, coatNormal), coatRoughness); - color = color * (1.0f - coatFresnel) + coatReflection * coatFresnel; - 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 = SampleEnv(reflect(-V, coatNormal), coatRoughness) * + (coatF0 * coatEnvBRDF.x + coatEnvBRDF.y); + + color = color * (1.0f - coatFresnel) + coatReflection * clearCoat; + 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 coatSpecular = pow(coatNdotH, max(2.0f, (1.0f - coatRoughness) * 128.0f)); - const float coatDirectFresnel = clearCoat * (0.04f + 0.96f * pow(1.0f - coatVdotH, 5.0f)); - color += coatDirectFresnel * coatSpecular * LightColor.rgb * LightColor.a * coatNdotL; + 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); + + color += coatSpecular * LightColor.rgb * LightColor.a * coatNdotL; } } #endif From 3c881808e5283f76d0ff9ec7f0333242b0b943d8 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 23 Sep 2026 16:54:20 -0300 Subject: [PATCH 21/61] Blend prefiltered environment levels --- Shaders/Material/Material.ps.hlsl | 32 +++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/Shaders/Material/Material.ps.hlsl b/Shaders/Material/Material.ps.hlsl index c933b3e5..50ec0883 100644 --- a/Shaders/Material/Material.ps.hlsl +++ b/Shaders/Material/Material.ps.hlsl @@ -124,15 +124,22 @@ float3 SampleIrradiance(float3 dir) ).rgb * EnvIntensity; } -float3 SampleEnv(float3 dir, float roughness) +float3 SamplePrefilteredLevel(float3 dir, uint level) { - const float prefilterLevel = saturate(roughness) * EnvMaxLod; - const float prefilterLevels = EnvMaxLod + 1.0f; + uint width = 0; + uint height = 0; + prefilteredTexture.GetDimensions(width, height); + + const float levelCount = EnvMaxLod + 1.0f; + const float blockTexelSize = levelCount / float(height); - // The prefiltered texture is an equirectangular atlas. Each vertical block - // stores the GGX convolution for one roughness level. + // 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 = (uv.y + prefilterLevel) / prefilterLevels; + + uv.y = clamp(uv.y, 0.5f * blockTexelSize, 1.0f - 0.5f * blockTexelSize); + uv.y = (float(level) + uv.y) / levelCount; return prefilteredTexture.SampleLevel( environmentSampler, @@ -141,6 +148,19 @@ float3 SampleEnv(float3 dir, float roughness) ).rgb * EnvIntensity; } +float3 SampleEnv(float3 dir, float roughness) +{ + const float prefilterLevel = saturate(roughness) * EnvMaxLod; + const uint lowerLevel = uint(floor(prefilterLevel)); + const uint upperLevel = min(lowerLevel + 1u, uint(EnvMaxLod)); + const float levelBlend = frac(prefilterLevel); + + const float3 lower = SamplePrefilteredLevel(dir, lowerLevel); + const float3 upper = SamplePrefilteredLevel(dir, upperLevel); + + return lerp(lower, upper, levelBlend); +} + float DistributionGGX(float NdotH, float roughness) { const float alpha = roughness * roughness; From 26f3213f41331418e75811dada4eaefdac09b44e Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 23 Sep 2026 17:37:08 -0300 Subject: [PATCH 22/61] Reduce specular aliasing --- Shaders/Material/Material.ps.hlsl | 34 ++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/Shaders/Material/Material.ps.hlsl b/Shaders/Material/Material.ps.hlsl index 50ec0883..002ecc5d 100644 --- a/Shaders/Material/Material.ps.hlsl +++ b/Shaders/Material/Material.ps.hlsl @@ -124,6 +124,15 @@ float3 SampleIrradiance(float3 dir) ).rgb * EnvIntensity; } +float3 SampleEnvironment(float3 dir) +{ + return environmentTexture.SampleLevel( + environmentSampler, + DirToEquirect(dir), + 0 + ).rgb * EnvIntensity; +} + float3 SamplePrefilteredLevel(float3 dir, uint level) { uint width = 0; @@ -148,17 +157,21 @@ float3 SamplePrefilteredLevel(float3 dir, uint level) ).rgb * EnvIntensity; } -float3 SampleEnv(float3 dir, float roughness) +float3 SampleSpecular(float3 dir, float roughness) { - const float prefilterLevel = saturate(roughness) * EnvMaxLod; + const float3 env = SampleEnvironment(dir); + + const float r = saturate(roughness); + const float prefilterLevel = r * EnvMaxLod; const uint lowerLevel = uint(floor(prefilterLevel)); const uint upperLevel = min(lowerLevel + 1u, uint(EnvMaxLod)); - const float levelBlend = frac(prefilterLevel); + const float blend = frac(prefilterLevel); const float3 lower = SamplePrefilteredLevel(dir, lowerLevel); const float3 upper = SamplePrefilteredLevel(dir, upperLevel); - - return lerp(lower, upper, levelBlend); + const float3 prefiltered = lerp(lower, upper, blend); + + return lerp(env, prefiltered, r); } float DistributionGGX(float NdotH, float roughness) @@ -276,6 +289,13 @@ float4 main(PSInput input) : SV_Target0 N = normalize(mul(tangentNormal, tangentBasis)); #endif + const float3 normalDerivativeX = ddx(N); + const float3 normalDerivativeY = ddy(N); + const float normalVariance = sqrt(dot(normalDerivativeX, normalDerivativeX) + + dot(normalDerivativeY, normalDerivativeY)); + + roughness = saturate(roughness + min(normalVariance * 0.7f, 0.4f)); + float3 R = reflect(-V, N); float3 L = normalize(LightDirection.xyz); float NdotV = saturate(dot(N, V)) + 1e-4f; @@ -289,7 +309,7 @@ float4 main(PSInput input) : SV_Target0 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 = SampleEnv(R, roughness) * (F0 * environmentBRDF.x + environmentBRDF.y); + const float3 specularIBL = SampleSpecular(R, roughness) * (F0 * environmentBRDF.x + environmentBRDF.y); float3 color = (diffuseWeightIBL * diffuseIBL + specularIBL) * ao + surface.Emissive; @@ -324,7 +344,7 @@ float4 main(PSInput input) : SV_Target0 const float3 coatF0 = 0.04f.xxx; const float coatFresnel = clearCoat * FresnelSchlick(coatNdotV, coatF0).x; const float2 coatEnvBRDF = EnvBRDFApprox(coatRoughness, coatNdotV); - const float3 coatReflection = SampleEnv(reflect(-V, coatNormal), coatRoughness) * + const float3 coatReflection = SampleSpecular(reflect(-V, coatNormal), coatRoughness) * (coatF0 * coatEnvBRDF.x + coatEnvBRDF.y); color = color * (1.0f - coatFresnel) + coatReflection * clearCoat; From 0e3f77fcfaa0fdc508f2f064be0e2165907ed50d Mon Sep 17 00:00:00 2001 From: MrChampz Date: Thu, 24 Sep 2026 11:31:22 -0300 Subject: [PATCH 23/61] Fix mesh material rendering --- Dissolve/Source/Dissolve.cpp | 16 +++-- Dissolve/Source/Environment.cpp | 31 ++++---- Dissolve/Source/Environment.h | 22 +++--- .../Source/Engine/Mesh/StaticMeshRenderer.cpp | 5 +- .../Source/Engine/Mesh/StaticMeshRenderer.h | 28 ++++++++ .../Graphics/SpirV/SpirVShaderBackend.cpp | 2 +- .../Graphics/Vulkan/VulkanGraphicsContext.cpp | 2 +- .../Source/Graphics/Vulkan/VulkanShader.cpp | 16 +++-- Elixir/Source/Graphics/Vulkan/VulkanShader.h | 3 +- .../Platform/GLTF/GLTFStaticMeshLoader.cpp | 6 +- .../Engine/Materials/MaterialGraphTest.cpp | 6 +- .../Rendering/TextureRegistryTest.cpp | 20 ------ Shaders/Material/Material.ps.hlsl | 72 +++++++++++++------ 13 files changed, 142 insertions(+), 87 deletions(-) delete mode 100644 Elixir/Tests/Engine/Materials/Rendering/TextureRegistryTest.cpp diff --git a/Dissolve/Source/Dissolve.cpp b/Dissolve/Source/Dissolve.cpp index 99d83a06..b3f1c90a 100644 --- a/Dissolve/Source/Dissolve.cpp +++ b/Dissolve/Source/Dissolve.cpp @@ -58,9 +58,15 @@ Dissolve::Dissolve() .Irradiance = m_Environment->GetIrradiance(), .Prefiltered = m_Environment->GetPrefiltered(), .Sampler = m_Environment->GetSampler(), - .Intensity = 1.0f, - .MaxLod = m_Environment->GetMaxLod(), + .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"); @@ -236,8 +242,8 @@ void Dissolve::Render(const Timestep frameTime) Application::Render(frameTime); m_CameraController->Update(frameTime); - auto& aether = GetAetherManager(); - aether.BeginFrame(frameTime); + //auto& aether = GetAetherManager(); + //aether.BeginFrame(frameTime); m_GraphicsContext->Clear(); @@ -245,7 +251,7 @@ void Dissolve::Render(const Timestep frameTime) DrawGeometry(); m_StaticMeshRenderer->EndFrame(); - aether.Render(m_CameraController->GetCamera()); + //aether.Render(m_CameraController->GetCamera()); } void Dissolve::OnEvent(Event& event) diff --git a/Dissolve/Source/Environment.cpp b/Dissolve/Source/Environment.cpp index c1b5edd1..51add99f 100644 --- a/Dissolve/Source/Environment.cpp +++ b/Dissolve/Source/Environment.cpp @@ -184,8 +184,8 @@ namespace } } -Elixir::Scope Environment::Load( - const Elixir::GraphicsContext& context, +Scope Environment::Load( + const GraphicsContext& context, const std::filesystem::path& path ) { @@ -199,11 +199,11 @@ Elixir::Scope Environment::Load( return nullptr; } - auto environment = Elixir::Scope(new Environment()); + auto environment = Scope(new Environment()); const auto* graphicsContext = &context; - environment->m_Environment = Elixir::Texture2D::Create( + environment->m_Environment = Texture2D::Create( graphicsContext, - Elixir::EImageFormat::R32G32B32A32_SFLOAT, + EImageFormat::R32G32B32A32_SFLOAT, static_cast(width), static_cast(height), pixels, @@ -211,30 +211,31 @@ Elixir::Scope Environment::Load( ); const auto irradiance = BakeIrradiance(pixels, width, height); - environment->m_Irradiance = Elixir::Texture2D::Create( + environment->m_Irradiance = Texture2D::Create( graphicsContext, - Elixir::EImageFormat::R32G32B32A32_SFLOAT, + EImageFormat::R32G32B32A32_SFLOAT, IrradianceWidth, IrradianceHeight, irradiance.data() ); const auto prefiltered = BakePrefiltered(pixels, width, height); - environment->m_Prefiltered = Elixir::Texture2D::Create( + environment->m_Prefiltered = Texture2D::Create( graphicsContext, - Elixir::EImageFormat::R32G32B32A32_SFLOAT, + EImageFormat::R32G32B32A32_SFLOAT, PrefilterWidth, PrefilterHeight * PrefilterLevels, prefiltered.data() ); stbi_image_free(pixels); - environment->m_Sampler = Elixir::SamplerBuilder() - .SetMagFilter(Elixir::ESamplerFilter::Linear) - .SetMinFilter(Elixir::ESamplerFilter::Linear) - .SetAddressModeU(Elixir::ESamplerAddressMode::Repeat) - .SetAddressModeV(Elixir::ESamplerAddressMode::ClampToEdge) - .SetAddressModeW(Elixir::ESamplerAddressMode::ClampToEdge) + 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) diff --git a/Dissolve/Source/Environment.h b/Dissolve/Source/Environment.h index 64b1ba12..dd54c391 100644 --- a/Dissolve/Source/Environment.h +++ b/Dissolve/Source/Environment.h @@ -28,31 +28,31 @@ class Environment final * @param path Path to an HDR equirectangular image. * @return The loaded environment, or null when the image cannot be loaded. */ - static Elixir::Scope Load( - const Elixir::GraphicsContext& context, + static Scope Load( + const GraphicsContext& context, const std::filesystem::path& path ); /** @brief Return the source HDR environment texture. */ - [[nodiscard]] const Elixir::Ref& GetEnvironment() const { return m_Environment; } + const Ref& GetEnvironment() const { return m_Environment; } /** @brief Return the diffuse irradiance texture derived from the environment. */ - [[nodiscard]] const Elixir::Ref& GetIrradiance() const { return m_Irradiance; } + const Ref& GetIrradiance() const { return m_Irradiance; } /** @brief Return the roughness-filtered reflection texture. */ - [[nodiscard]] const Elixir::Ref& GetPrefiltered() const { return m_Prefiltered; } + const Ref& GetPrefiltered() const { return m_Prefiltered; } /** @brief Return the sampler shared by the environment textures. */ - [[nodiscard]] const Elixir::Ref& GetSampler() const { return m_Sampler; } + const Ref& GetSampler() const { return m_Sampler; } /** @brief Return the highest roughness level in the prefiltered texture. */ - [[nodiscard]] float GetMaxLod() const { return static_cast(PrefilterLevels - 1); } + static float GetMaxLod() { return static_cast(PrefilterLevels - 1); } private: Environment() = default; - Elixir::Ref m_Environment; - Elixir::Ref m_Irradiance; - Elixir::Ref m_Prefiltered; - Elixir::Ref m_Sampler; + Ref m_Environment; + Ref m_Irradiance; + Ref m_Prefiltered; + Ref m_Sampler; }; diff --git a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp index e912b0f4..06178dbd 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp +++ b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp @@ -11,7 +11,7 @@ namespace Elixir { namespace { - struct SSurfaceFrameData + struct alignas(16) SSurfaceFrameData { glm::mat4 View{ 1.0f }; glm::mat4 Proj{ 1.0f }; @@ -21,8 +21,10 @@ namespace Elixir 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{}; }; @@ -66,6 +68,7 @@ namespace Elixir .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), diff --git a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h index 2a4ffc87..9bf7932c 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h +++ b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h @@ -42,6 +42,31 @@ namespace Elixir 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 { @@ -56,6 +81,9 @@ namespace Elixir /** Intensity multiplier of the directional light. */ float DirectionalLightIntensity = 2.2f; + + /** Surface lighting contribution to display for diagnostics. */ + ESurfaceDebugView DebugView = ESurfaceDebugView::Composite; }; /** 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/VulkanGraphicsContext.cpp b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp index ddeff100..31e08536 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp +++ b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp @@ -400,7 +400,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); } diff --git a/Elixir/Source/Graphics/Vulkan/VulkanShader.cpp b/Elixir/Source/Graphics/Vulkan/VulkanShader.cpp index dd4e3630..e2483093 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanShader.cpp +++ b/Elixir/Source/Graphics/Vulkan/VulkanShader.cpp @@ -228,10 +228,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 +289,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 +377,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 = {}; diff --git a/Elixir/Source/Graphics/Vulkan/VulkanShader.h b/Elixir/Source/Graphics/Vulkan/VulkanShader.h index c65928f1..e376ea90 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanShader.h +++ b/Elixir/Source/Graphics/Vulkan/VulkanShader.h @@ -82,10 +82,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/Platform/GLTF/GLTFStaticMeshLoader.cpp b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp index 42cb0401..e3b25d96 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp @@ -112,7 +112,7 @@ namespace Elixir std::visit( fastgltf::visitor{ - [](auto&) {}, + [](const auto&) {}, [&](const fastgltf::sources::URI& source) { const auto path = sourceDirectory / source.uri.fspath(); @@ -139,7 +139,7 @@ namespace Elixir format ); }, - [&](fastgltf::sources::BufferView& viewSource) + [&](const fastgltf::sources::BufferView& viewSource) { if (viewSource.bufferViewIndex >= asset.bufferViews.size()) { @@ -157,7 +157,7 @@ namespace Elixir auto& buffer = asset.buffers[view.bufferIndex]; std::visit( fastgltf::visitor{ - [](auto&) {}, + [](const auto&) {}, [&](const fastgltf::sources::Array& source) { texture = DecodeImage( diff --git a/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp b/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp index 94e368d6..8e65236e 100644 --- a/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp +++ b/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp @@ -188,8 +188,8 @@ TEST(MaterialGraphTest, GeneratesStaticTextureCoordinateTransform) }); EXPECT_NE(hlsl.find("input.TexCoord1"), std::string::npos); - EXPECT_NE(hlsl.find("cos(mat.Values[2].x)"), std::string::npos); - EXPECT_NE(hlsl.find("sin(mat.Values[2].x)"), 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); @@ -251,7 +251,7 @@ TEST(MaterialGraphTest, InterpolatesAmbientOcclusionFromOne) } }); - EXPECT_NE(hlsl.find("lerp(1.000000"), std::string::npos); + EXPECT_NE(hlsl.find("lerp("), std::string::npos); EXPECT_NE(hlsl.find("surface.AmbientOcclusion ="), std::string::npos); } 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/Shaders/Material/Material.ps.hlsl b/Shaders/Material/Material.ps.hlsl index 002ecc5d..7e8becb0 100644 --- a/Shaders/Material/Material.ps.hlsl +++ b/Shaders/Material/Material.ps.hlsl @@ -21,6 +21,7 @@ cbuffer cbFrame : register(b0) float EnvIntensity; float EnvMaxLod; uint SceneColorIndex; + uint DebugView; float ScreenWidth; float ScreenHeight; float4 LightDirection; @@ -93,6 +94,14 @@ struct Surface }; 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) { @@ -124,13 +133,17 @@ float3 SampleIrradiance(float3 dir) ).rgb * EnvIntensity; } -float3 SampleEnvironment(float3 dir) +float3 SampleEnvironment(float3 dir, float lod) { - return environmentTexture.SampleLevel( + float3 color = environmentTexture.SampleLevel( environmentSampler, DirToEquirect(dir), - 0 - ).rgb * EnvIntensity; + 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) @@ -159,9 +172,11 @@ float3 SamplePrefilteredLevel(float3 dir, uint level) float3 SampleSpecular(float3 dir, float roughness) { - const float3 env = SampleEnvironment(dir); - 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)); @@ -171,7 +186,7 @@ float3 SampleSpecular(float3 dir, float roughness) const float3 upper = SamplePrefilteredLevel(dir, upperLevel); const float3 prefiltered = lerp(lower, upper, blend); - return lerp(env, prefiltered, r); + return lerp(env, prefiltered, smoothstep(0.12f, 0.35f, r)); } float DistributionGGX(float NdotH, float roughness) @@ -289,13 +304,6 @@ float4 main(PSInput input) : SV_Target0 N = normalize(mul(tangentNormal, tangentBasis)); #endif - const float3 normalDerivativeX = ddx(N); - const float3 normalDerivativeY = ddy(N); - const float normalVariance = sqrt(dot(normalDerivativeX, normalDerivativeX) + - dot(normalDerivativeY, normalDerivativeY)); - - roughness = saturate(roughness + min(normalVariance * 0.7f, 0.4f)); - float3 R = reflect(-V, N); float3 L = normalize(LightDirection.xyz); float NdotV = saturate(dot(N, V)) + 1e-4f; @@ -311,7 +319,12 @@ float4 main(PSInput input) : SV_Target0 const float2 environmentBRDF = EnvBRDFApprox(roughness, NdotV); const float3 specularIBL = SampleSpecular(R, roughness) * (F0 * environmentBRDF.x + environmentBRDF.y); - float3 color = (diffuseWeightIBL * diffuseIBL + specularIBL) * ao + surface.Emissive; + 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) @@ -329,8 +342,10 @@ float4 main(PSInput input) : SV_Target0 const float3 diffuseWeightDirect = (1.0f - fresnelDirect) * (1.0f - surface.Metallic); const float3 radiance = LightColor.rgb * LightColor.a; - color += (diffuseWeightDirect * surface.BaseColor / 3.14159265359f + specularDirect) * - radiance * NdotL * ao; + directDiffuseContribution = diffuseWeightDirect * surface.BaseColor / + 3.14159265359f * radiance * NdotL * ao; + directSpecularContribution = specularDirect * radiance * NdotL * ao; + color += directDiffuseContribution + directSpecularContribution; } #if MATERIAL_SHADING_MODEL == MATERIAL_SHADING_MODEL_CLEAR_COAT @@ -347,7 +362,8 @@ float4 main(PSInput input) : SV_Target0 const float3 coatReflection = SampleSpecular(reflect(-V, coatNormal), coatRoughness) * (coatF0 * coatEnvBRDF.x + coatEnvBRDF.y); - color = color * (1.0f - coatFresnel) + coatReflection * clearCoat; + clearCoatContribution = coatReflection * clearCoat; + color = color * (1.0f - coatFresnel) + clearCoatContribution; if (coatNdotL > 0.0f) { @@ -361,13 +377,29 @@ float4 main(PSInput input) : SV_Target0 const float coatSpecular = coatDistribution * coatGeometry * coatFresnelDirect / max(4.0f * coatNdotV * coatNdotL, 1e-4f); - color += coatSpecular * LightColor.rgb * LightColor.a * coatNdotL; + 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; + // Tone mapping color = ACESFilm(color); return float4(color, surface.Opacity); -} \ No newline at end of file +} From 77ca2a0447b8e7424aa04a481f1ef38cfc279349 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Thu, 24 Sep 2026 15:17:30 -0300 Subject: [PATCH 24/61] Add texture mipmap generation --- Elixir/Source/Engine/Graphics/Image.h | 5 +- Elixir/Source/Engine/Graphics/Sampler.h | 2 +- Elixir/Source/Engine/Graphics/Texture.cpp | 191 ++++++++++++++---- Elixir/Source/Engine/Graphics/Texture.h | 71 ++++++- .../Source/Engine/Graphics/TextureLoader.cpp | 69 +++++-- Elixir/Source/Engine/Graphics/TextureLoader.h | 11 + Elixir/Source/Graphics/Vulkan/Utils.h | 16 +- Elixir/Source/Graphics/Vulkan/VulkanImage.cpp | 82 +++++++- Elixir/Source/Graphics/Vulkan/VulkanImage.h | 2 + .../Platform/GLTF/GLTFStaticMeshLoader.cpp | 128 +++++++----- Elixir/Tests/Engine/Graphics/ImageTest.cpp | 22 +- .../Tests/Graphics/Vulkan/VulkanImageTest.cpp | 27 +++ Shaders/Material/Material.ps.hlsl | 17 +- 13 files changed, 523 insertions(+), 120 deletions(-) diff --git a/Elixir/Source/Engine/Graphics/Image.h b/Elixir/Source/Engine/Graphics/Image.h index ef8fa0e9..5a7baf0b 100644 --- a/Elixir/Source/Engine/Graphics/Image.h +++ b/Elixir/Source/Engine/Graphics/Image.h @@ -195,6 +195,9 @@ namespace Elixir EImageUsage Usage; EImageLayout InitialLayout = EImageLayout::Undefined; SAllocationInfo AllocationInfo; + + /** @brief Generates lower mip levels from the supplied base level. */ + bool GenerateMipmaps = false; }; class ELIXIR_API Image @@ -378,4 +381,4 @@ namespace Elixir protected: DepthStencilImage(const GraphicsContext* context, const SImageCreateInfo& info); }; -} \ No newline at end of file +} 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/Texture.cpp b/Elixir/Source/Engine/Graphics/Texture.cpp index cfc369e9..f6abe161 100644 --- a/Elixir/Source/Engine/Graphics/Texture.cpp +++ b/Elixir/Source/Engine/Graphics/Texture.cpp @@ -10,42 +10,76 @@ namespace Elixir Ref Texture::Create( const GraphicsContext* context, - EImageFormat format, - uint32_t width, - const void* data, - const std::string& path + const STextureCreateInfo& info ) { + const auto imageInfo = CreateImageInfo(info); + switch (context->GetAPI()) { case EGraphicsAPI::Vulkan: - return CreateRef(context, format, width, data, path); + return CreateRef(context, imageInfo, info.Path); default: EE_CORE_ASSERT(false, "Unknown GraphicsAPI!") return nullptr; } } - SImageCreateInfo Texture::CreateImageInfo( + Ref Texture::Create( + const GraphicsContext* context, const EImageFormat format, const uint32_t width, const void* data, const std::string& path ) { + STextureCreateInfo info; + info.InitialData = data; + info.Format = format; + info.Width = width; + info.Path = path; + return Create(context, info); + } + + SImageCreateInfo Texture::CreateImageInfo( + const STextureCreateInfo& info + ) + { + auto usage = EImageUsage::Sampled | EImageUsage::TransferDst; + + if (info.GenerateMipmaps) + usage |= EImageUsage::TransferSrc; + return { - .InitialData = data, - .Width = width, + .InitialData = info.InitialData, + .Width = info.Width, .Type = EImageType::_1D, - .Format = format, - .Usage = EImageUsage::Sampled | EImageUsage::TransferDst, + .Format = info.Format, + .MipLevels = info.MipLevels, + .Usage = usage, .InitialLayout = EImageLayout::ShaderReadOnly, .AllocationInfo = { .RequiredFlags = EMemoryProperty::DeviceLocal - } + }, + .GenerateMipmaps = info.GenerateMipmaps, }; } + SImageCreateInfo Texture::CreateImageInfo( + const EImageFormat format, + const uint32_t width, + const void* data, + const std::string& path + ) + { + STextureCreateInfo info; + info.InitialData = data; + info.Format = format; + info.Width = width; + info.Path = path; + return CreateImageInfo(info); + } + Texture::Texture( const GraphicsContext* context, const EImageFormat format, @@ -71,44 +105,80 @@ namespace Elixir Ref Texture2D::Create( const GraphicsContext* context, - EImageFormat format, - uint32_t width, - uint32_t height, - const void* data, - const std::string& path + const STexture2DCreateInfo& info ) { + const auto imageInfo = CreateImageInfo(info); + switch (context->GetAPI()) { case EGraphicsAPI::Vulkan: - return CreateRef(context, format, width, height, data, path); + return CreateRef(context, imageInfo, info.Path); default: EE_CORE_ASSERT(false, "Unknown GraphicsAPI!") return nullptr; } } - SImageCreateInfo Texture2D::CreateImageInfo( + Ref Texture2D::Create( + const GraphicsContext* context, const EImageFormat format, const uint32_t width, const uint32_t height, - const void* data + const void* data, + const std::string& path ) { + 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 usage = EImageUsage::Sampled | EImageUsage::TransferDst; + + if (info.GenerateMipmaps) + usage |= EImageUsage::TransferSrc; + return { - .InitialData = data, - .Width = width, - .Height = height, + .InitialData = info.InitialData, + .Width = info.Width, + .Height = info.Height, .Type = EImageType::_2D, - .Format = format, - .Usage = EImageUsage::Sampled | EImageUsage::TransferDst, + .Format = info.Format, + .MipLevels = info.MipLevels, + .Usage = usage, .InitialLayout = EImageLayout::ShaderReadOnly, .AllocationInfo = { .RequiredFlags = EMemoryProperty::DeviceLocal - } + }, + .GenerateMipmaps = info.GenerateMipmaps, }; } + SImageCreateInfo Texture2D::CreateImageInfo( + const EImageFormat format, + const uint32_t width, + const uint32_t height, + const void* data + ) + { + 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, @@ -135,47 +205,84 @@ namespace Elixir 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 STexture3DCreateInfo& info ) { + const auto imageInfo = CreateImageInfo(info); + switch (context->GetAPI()) { case EGraphicsAPI::Vulkan: - return CreateRef(context, format, width, height, depth, data, path); + return CreateRef(context, imageInfo, info.Path); default: EE_CORE_ASSERT(false, "Unknown GraphicsAPI!") return nullptr; } } - SImageCreateInfo Texture3D::CreateImageInfo( + Ref Texture3D::Create( + const GraphicsContext* context, const EImageFormat format, const uint32_t width, const uint32_t height, const uint32_t depth, - const void* data + const void* data, + const std::string& path ) { + 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 STexture3DCreateInfo& info + ) + { + auto usage = EImageUsage::Sampled | EImageUsage::TransferDst; + + if (info.GenerateMipmaps) + usage |= EImageUsage::TransferSrc; + return { - .InitialData = data, - .Width = width, - .Height = height, - .Depth = depth, + .InitialData = info.InitialData, + .Width = info.Width, + .Height = info.Height, + .Depth = info.Depth, .Type = EImageType::_3D, - .Format = format, - .Usage = EImageUsage::Sampled | EImageUsage::TransferDst, + .Format = info.Format, + .MipLevels = info.MipLevels, + .Usage = usage, .InitialLayout = EImageLayout::ShaderReadOnly, .AllocationInfo = { .RequiredFlags = EMemoryProperty::DeviceLocal - } + }, + .GenerateMipmaps = info.GenerateMipmaps, }; } + SImageCreateInfo Texture3D::CreateImageInfo( + const EImageFormat format, + const uint32_t width, + const uint32_t height, + const uint32_t depth, + const void* data + ) + { + STexture3DCreateInfo info; + info.InitialData = data; + info.Format = format; + info.Width = width; + info.Height = height; + info.Depth = depth; + return CreateImageInfo(info); + } + Texture3D::Texture3D( const GraphicsContext* context, const EImageFormat format, @@ -198,4 +305,4 @@ namespace Elixir m_DebugName = "Texture3D[" + m_UUID.ToString() + "]"; #endif } -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/Graphics/Texture.h b/Elixir/Source/Engine/Graphics/Texture.h index be881586..6eff327d 100644 --- a/Elixir/Source/Engine/Graphics/Texture.h +++ b/Elixir/Source/Engine/Graphics/Texture.h @@ -4,6 +4,42 @@ namespace Elixir { + /** @brief Defines properties shared by every texture dimension. */ + struct STextureCreateInfo + { + /** Pixel data for the base mip level, or null for an empty texture. */ + const void* InitialData = nullptr; + + /** Pixel format of the texture. */ + EImageFormat Format = EImageFormat::Undefined; + + /** Width of the base mip level in texels. */ + uint32_t Width = 0; + + /** Number of mip levels allocated for the texture. */ + uint32_t MipLevels = 1; + + /** Generates lower mip levels from the base level during upload. */ + bool GenerateMipmaps = false; + + /** Source path retained for diagnostics and asset tracking. */ + std::string Path; + }; + + /** @brief Defines properties for a two-dimensional texture. */ + struct STexture2DCreateInfo : STextureCreateInfo + { + /** Height of the base mip level in texels. */ + uint32_t Height = 0; + }; + + /** @brief Defines properties for a three-dimensional texture. */ + struct STexture3DCreateInfo : STexture2DCreateInfo + { + /** Depth of the base mip level in texels. */ + uint32_t Depth = 0; + }; + class ELIXIR_API Texture : public Image { public: @@ -11,6 +47,12 @@ namespace Elixir [[nodiscard]] const std::string& GetPath() const { return m_Path; } + /** @brief Creates a one-dimensional texture from the supplied description. */ + static Ref Create( + const GraphicsContext* context, + const STextureCreateInfo& info + ); + static Ref Create( const GraphicsContext* context, EImageFormat format, @@ -19,6 +61,11 @@ namespace Elixir const std::string& path = "" ); + /** @brief Builds the image description used to create a one-dimensional texture. */ + static SImageCreateInfo CreateImageInfo( + const STextureCreateInfo& info + ); + static SImageCreateInfo CreateImageInfo( EImageFormat format, uint32_t width, @@ -50,6 +97,12 @@ namespace Elixir [[nodiscard]] uint32_t GetHeight() const { return m_Extent.Height; } + /** @brief Creates a two-dimensional texture from the supplied description. */ + static Ref Create( + const GraphicsContext* context, + const STexture2DCreateInfo& info + ); + static Ref Create( const GraphicsContext* context, EImageFormat format, @@ -59,6 +112,11 @@ namespace Elixir const std::string& path = "" ); + /** @brief Builds the image description used to create a two-dimensional texture. */ + static SImageCreateInfo CreateImageInfo( + const STexture2DCreateInfo& info + ); + static SImageCreateInfo CreateImageInfo( EImageFormat format, uint32_t width, @@ -89,6 +147,12 @@ namespace Elixir [[nodiscard]] uint32_t GetDepth() const { return m_Extent.Depth; } + /** @brief Creates a three-dimensional texture from the supplied description. */ + static Ref Create( + const GraphicsContext* context, + const STexture3DCreateInfo& info + ); + static Ref Create( const GraphicsContext* context, EImageFormat format, @@ -99,6 +163,11 @@ namespace Elixir const std::string& path = "" ); + /** @brief Builds the image description used to create a three-dimensional texture. */ + static SImageCreateInfo CreateImageInfo( + const STexture3DCreateInfo& info + ); + static SImageCreateInfo CreateImageInfo( EImageFormat format, uint32_t width, @@ -123,4 +192,4 @@ namespace Elixir const std::string& path = "" ); }; -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/Graphics/TextureLoader.cpp b/Elixir/Source/Engine/Graphics/TextureLoader.cpp index 2ca37a59..c38bf46c 100644 --- a/Elixir/Source/Engine/Graphics/TextureLoader.cpp +++ b/Elixir/Source/Engine/Graphics/TextureLoader.cpp @@ -7,19 +7,36 @@ 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 + uint32_t GetFullMipLevelCount(const uint32_t width, const uint32_t height) + { + uint32_t largestDimension = std::max(width, height); + uint32_t levelCount = 1; + + while (largestDimension > 1) + { + largestDimension >>= 1; + ++levelCount; + } + + return levelCount; + } + + 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,6 +52,16 @@ 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!") @@ -70,16 +97,18 @@ namespace Elixir EE_CORE_ASSERT(data, "Could not read texture data!") - TraceTextureInfo(pathStr, isHdr, format, channels); + auto createInfo = info; + createInfo.InitialData = data; + createInfo.Width = width; + createInfo.Height = height; + createInfo.Path = pathStr; + + if (createInfo.GenerateMipmaps) + createInfo.MipLevels = GetFullMipLevelCount(width, height); + + TraceTextureInfo(pathStr, isHdr, createInfo.Format, channels); - auto texture = Texture2D::Create( - s_Context, - format, - width, - height, - data, - pathStr - ); + auto texture = Texture2D::Create(s_Context, createInfo); texture->m_HDR = isHdr; EE_CORE_TRACE("Loaded texture: {0} [{1}].", pathStr, texture->GetUUID()) @@ -87,4 +116,4 @@ namespace Elixir 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/Graphics/Vulkan/Utils.h b/Elixir/Source/Graphics/Vulkan/Utils.h index 0d5647a2..ed33959b 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,9 @@ 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 ) { EE_PROFILE_ZONE_SCOPED() @@ -234,12 +240,12 @@ namespace Elixir::Vulkan blitRegion.srcSubresource.aspectMask = aspectMask; blitRegion.srcSubresource.baseArrayLayer = 0; blitRegion.srcSubresource.layerCount = 1; - blitRegion.srcSubresource.mipLevel = 0; + blitRegion.srcSubresource.mipLevel = srcMipLevel; blitRegion.dstSubresource.aspectMask = aspectMask; blitRegion.dstSubresource.baseArrayLayer = 0; blitRegion.dstSubresource.layerCount = 1; - blitRegion.dstSubresource.mipLevel = 0; + blitRegion.dstSubresource.mipLevel = dstMipLevel; VkBlitImageInfo2 blitInfo = {}; blitInfo.sType = VK_STRUCTURE_TYPE_BLIT_IMAGE_INFO_2; @@ -255,4 +261,4 @@ namespace Elixir::Vulkan vkCmdBlitImage2(cmd, &blitInfo); } } -} \ No newline at end of file +} diff --git a/Elixir/Source/Graphics/Vulkan/VulkanImage.cpp b/Elixir/Source/Graphics/Vulkan/VulkanImage.cpp index b42d967b..53f10a5c 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanImage.cpp +++ b/Elixir/Source/Graphics/Vulkan/VulkanImage.cpp @@ -270,6 +270,76 @@ namespace Elixir::Vulkan } } + template + void VulkanBaseImage::GenerateMipmaps( + const CommandBuffer* cmd, + const EImageLayout finalLayout + ) + { + const auto vkCmd = static_cast(cmd); + const auto vkFinalLayout = Converters::GetImageLayout(finalLayout); + const auto aspect = Converters::GetImageAspect(this->GetAspect()); + + uint32_t sourceWidth = this->GetWidth(); + uint32_t sourceHeight = this->GetExtent().Height; + uint32_t sourceDepth = this->GetExtent().Depth; + + for (uint32_t level = 1; level < this->GetMipLevels(); ++level) + { + const uint32_t destinationWidth = std::max(1u, sourceWidth >> 1u); + const uint32_t destinationHeight = std::max(1u, sourceHeight >> 1u); + const uint32_t destinationDepth = std::max(1u, sourceDepth >> 1u); + + CommandUtils::TransitionImage( + vkCmd->GetVulkanCommandBuffer(), + m_Image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + aspect, + level - 1, + 1 + ); + + CommandUtils::CopyImageToImage( + vkCmd->GetVulkanCommandBuffer(), + m_Image, + m_Image, + { sourceWidth, sourceHeight, sourceDepth }, + { destinationWidth, destinationHeight, destinationDepth }, + aspect, + VK_FILTER_LINEAR, + level - 1, + level + ); + + CommandUtils::TransitionImage( + vkCmd->GetVulkanCommandBuffer(), + m_Image, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + vkFinalLayout, + aspect, + level - 1, + 1 + ); + + sourceWidth = destinationWidth; + sourceHeight = destinationHeight; + sourceDepth = destinationDepth; + } + + CommandUtils::TransitionImage( + vkCmd->GetVulkanCommandBuffer(), + m_Image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + vkFinalLayout, + aspect, + this->GetMipLevels() - 1, + 1 + ); + + this->m_Layout = finalLayout; + } + template void VulkanBaseImage::InitImage(const SImageCreateInfo& info) { @@ -280,6 +350,10 @@ namespace Elixir::Vulkan cmd->Begin(); + EE_CORE_ASSERT( + !info.GenerateMipmaps || info.InitialData, + "Generated mipmaps require initial image data." + ) if (info.InitialData) { this->Transition(cmd.get(), EImageLayout::TransferDst); @@ -298,7 +372,11 @@ namespace Elixir::Vulkan SBufferImageCopy regions[] = { copyRegion }; this->CopyFrom(cmd, stagingBuffer, regions); - this->Transition(cmd.get(), info.InitialLayout); + if (info.GenerateMipmaps) + GenerateMipmaps(cmd.get(), info.InitialLayout); + else + this->Transition(cmd.get(), info.InitialLayout); + cmd->Flush(); stagingBuffer->Destroy(); @@ -398,4 +476,4 @@ namespace Elixir::Vulkan template class VulkanBaseImage; template class VulkanBaseImage; template class VulkanBaseImage; -} \ No newline at end of file +} diff --git a/Elixir/Source/Graphics/Vulkan/VulkanImage.h b/Elixir/Source/Graphics/Vulkan/VulkanImage.h index bc66b186..63113890 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanImage.h +++ b/Elixir/Source/Graphics/Vulkan/VulkanImage.h @@ -71,6 +71,8 @@ namespace Elixir::Vulkan void CreateImage(const SImageCreateInfo& info); void InitImage(const SImageCreateInfo& info); + /** Generates the missing mip levels after the base level upload. */ + void GenerateMipmaps(const CommandBuffer* cmd, EImageLayout finalLayout); void CreateImageView(); void CreateDescriptorInfo(); void UpdateSampler() override; diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp index e3b25d96..2179c45d 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp @@ -51,11 +51,25 @@ namespace Elixir target.Max = glm::max(target.Max, source.Max); } + uint32_t GetFullMipLevelCount(const uint32_t width, const uint32_t height) + { + uint32_t largestDimension = std::max(width, height); + uint32_t levelCount = 1; + + while (largestDimension > 1) + { + largestDimension >>= 1; + ++levelCount; + } + + return levelCount; + } + Ref DecodeImage( const GraphicsContext& context, const std::byte* bytes, const size_t size, - const EImageFormat format + const STexture2DCreateInfo& info ) { int width = 0; @@ -80,13 +94,15 @@ namespace Elixir return nullptr; } - const auto texture = Texture2D::Create( - &context, - format, - (uint32_t)width, - (uint32_t)height, - pixels - ); + auto createInfo = info; + createInfo.InitialData = pixels; + createInfo.Width = width; + createInfo.Height = height; + + if (createInfo.GenerateMipmaps) + createInfo.MipLevels = GetFullMipLevelCount(width, height); + + const auto texture = Texture2D::Create(&context, createInfo); stbi_image_free(pixels); @@ -97,8 +113,8 @@ namespace Elixir const GraphicsContext& context, fastgltf::Asset& asset, const size_t imageIndex, - const std::filesystem::path& sourceDirectory, - const EImageFormat format + const std::filesystem::path& dir, + const STexture2DCreateInfo& info ) { if (imageIndex >= asset.images.size()) @@ -115,11 +131,8 @@ namespace Elixir [](const auto&) {}, [&](const fastgltf::sources::URI& source) { - const auto path = sourceDirectory / source.uri.fspath(); - texture = TextureLoader::Load( - path, - format - ); + const auto path = dir / source.uri.fspath(); + texture = TextureLoader::Load(path, info); }, [&](const fastgltf::sources::Array& source) { @@ -127,7 +140,7 @@ namespace Elixir context, source.bytes.data(), source.bytes.size(), - format + info ); }, [&](const fastgltf::sources::Vector& source) @@ -136,7 +149,7 @@ namespace Elixir context, source.bytes.data(), source.bytes.size(), - format + info ); }, [&](const fastgltf::sources::BufferView& viewSource) @@ -164,7 +177,7 @@ namespace Elixir context, source.bytes.data() + view.byteOffset, view.byteLength, - format + info ); }, [&](const fastgltf::sources::Vector& source) @@ -173,7 +186,7 @@ namespace Elixir context, source.bytes.data() + view.byteOffset, view.byteLength, - format + info ); } }, @@ -191,14 +204,15 @@ namespace Elixir const GraphicsContext& context, fastgltf::Asset& asset, const size_t textureIndex, - const std::filesystem::path& sourceDirectory, - const EImageFormat format, + const std::filesystem::path& dir, + const STexture2DCreateInfo& info, std::unordered_map>& cache ) { const uint64_t cacheKey = - (uint64_t(textureIndex) << 1) | - (format == EImageFormat::R8G8B8A8_SRGB ? 1ull : 0ull); + (uint64_t(textureIndex) << 2) | + (info.Format == EImageFormat::R8G8B8A8_SRGB ? 1ull : 0ull) | + (info.GenerateMipmaps ? 2ull : 0ull); const auto existing = cache.find(cacheKey); if (existing != cache.end()) @@ -223,8 +237,8 @@ namespace Elixir context, asset, *texture.imageIndex, - sourceDirectory, - format + dir, + info ); cache.emplace(cacheKey, result); @@ -847,7 +861,7 @@ namespace Elixir SStaticMeshData& mesh, const GraphicsContext& context, fastgltf::Asset& asset, - const std::filesystem::path& sourceDirectory + const std::filesystem::path& dir ) { // Index zero is the fallback for primitives without a glTF material. @@ -872,60 +886,71 @@ namespace Elixir if (source.pbrData.baseColorTexture) { + STexture2DCreateInfo info; + info.Format = EImageFormat::R8G8B8A8_SRGB; baseColorTexture = LoadTexture( context, asset, source.pbrData.baseColorTexture->textureIndex, - sourceDirectory, - EImageFormat::R8G8B8A8_SRGB, + dir, + info, textureCache ); } if (source.pbrData.metallicRoughnessTexture) { + STexture2DCreateInfo info; + info.Format = EImageFormat::R8G8B8A8_UNORM; metallicRoughnessTexture = LoadTexture( context, asset, source.pbrData.metallicRoughnessTexture->textureIndex, - sourceDirectory, - EImageFormat::R8G8B8A8_UNORM, + dir, + info, textureCache ); } if (source.normalTexture) { + STexture2DCreateInfo info; + info.Format = EImageFormat::R8G8B8A8_UNORM; + info.GenerateMipmaps = true; normalTexture = LoadTexture( context, asset, source.normalTexture->textureIndex, - sourceDirectory, - EImageFormat::R8G8B8A8_UNORM, + dir, + info, textureCache ); } if (source.occlusionTexture) { + STexture2DCreateInfo info; + info.Format = EImageFormat::R8G8B8A8_UNORM; occlusionTexture = LoadTexture( context, asset, source.occlusionTexture->textureIndex, - sourceDirectory, - EImageFormat::R8G8B8A8_UNORM, + dir, + info, textureCache ); } if (source.emissiveTexture) { + STexture2DCreateInfo info; + info.Format = EImageFormat::R8G8B8A8_SRGB; emissiveTexture = LoadTexture( context, asset, source.emissiveTexture->textureIndex, - sourceDirectory, - EImageFormat::R8G8B8A8_SRGB, + dir, + info, textureCache ); } @@ -934,24 +959,28 @@ namespace Elixir { if (source.specular->specularTexture) { + STexture2DCreateInfo info; + info.Format = EImageFormat::R8G8B8A8_UNORM; specularTexture = LoadTexture( context, asset, source.specular->specularTexture->textureIndex, - sourceDirectory, - EImageFormat::R8G8B8A8_UNORM, + dir, + info, textureCache ); } if (source.specular->specularColorTexture) { + STexture2DCreateInfo info; + info.Format = EImageFormat::R8G8B8A8_SRGB; specularColorTexture = LoadTexture( context, asset, source.specular->specularColorTexture->textureIndex, - sourceDirectory, - EImageFormat::R8G8B8A8_SRGB, + dir, + info, textureCache ); } @@ -961,36 +990,43 @@ namespace Elixir { if (source.clearcoat->clearcoatTexture) { + STexture2DCreateInfo info; + info.Format = EImageFormat::R8G8B8A8_UNORM; clearCoatTexture = LoadTexture( context, asset, source.clearcoat->clearcoatTexture->textureIndex, - sourceDirectory, - EImageFormat::R8G8B8A8_UNORM, + dir, + info, textureCache ); } if (source.clearcoat->clearcoatRoughnessTexture) { + STexture2DCreateInfo info; + info.Format = EImageFormat::R8G8B8A8_UNORM; clearCoatRoughnessTexture = LoadTexture( context, asset, source.clearcoat->clearcoatRoughnessTexture->textureIndex, - sourceDirectory, - EImageFormat::R8G8B8A8_UNORM, + dir, + info, textureCache ); } if (source.clearcoat->clearcoatNormalTexture) { + STexture2DCreateInfo info; + info.Format = EImageFormat::R8G8B8A8_UNORM; + info.GenerateMipmaps = true; clearCoatNormalTexture = LoadTexture( context, asset, source.clearcoat->clearcoatNormalTexture->textureIndex, - sourceDirectory, - EImageFormat::R8G8B8A8_UNORM, + dir, + info, textureCache ); } diff --git a/Elixir/Tests/Engine/Graphics/ImageTest.cpp b/Elixir/Tests/Engine/Graphics/ImageTest.cpp index 4bf32df5..32b438d7 100644 --- a/Elixir/Tests/Engine/Graphics/ImageTest.cpp +++ b/Elixir/Tests/Engine/Graphics/ImageTest.cpp @@ -2,6 +2,7 @@ using namespace testing; #include +#include using namespace Elixir; TEST(ImageTest, Image_IsNotConstructibleAndAssignable) @@ -20,4 +21,23 @@ TEST(ImageTest, DepthStencilImage_IsNotConstructibleAndAssignable) 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 +} + +TEST(ImageTest, Texture2DCreateInfoBuildsAMipmapChain) +{ + STexture2DCreateInfo textureInfo; + textureInfo.Format = EImageFormat::R8G8B8A8_UNORM; + textureInfo.Width = 8; + textureInfo.Height = 4; + textureInfo.MipLevels = 4; + textureInfo.GenerateMipmaps = true; + + const auto imageInfo = Texture2D::CreateImageInfo(textureInfo); + + EXPECT_TRUE(imageInfo.GenerateMipmaps); + EXPECT_EQ(imageInfo.MipLevels, 4u); + EXPECT_EQ( + imageInfo.Usage, + EImageUsage::Sampled | EImageUsage::TransferSrc | EImageUsage::TransferDst + ); +} diff --git a/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp b/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp index 52aebb9d..9f2f3a66 100644 --- a/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp +++ b/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp @@ -5,10 +5,13 @@ using namespace testing; #include #include #include +#include #include #include "VulkanTestContext.h" +#include + using namespace Elixir; using namespace Elixir::Vulkan; @@ -122,6 +125,30 @@ TEST_F(VulkanImageTest, VulkanDepthStencilImage_DepthStencil) SUCCEED(); } +TEST_F(VulkanImageTest, VulkanTexture2DGeneratesMipmapChain) +{ + std::array pixels{}; + STexture2DCreateInfo info; + info.InitialData = pixels.data(); + info.Format = EImageFormat::R8G8B8A8_UNORM; + info.Width = 8; + info.Height = 4; + info.MipLevels = 4; + info.GenerateMipmaps = true; + + const auto texture = Texture2D::Create(Context, info); + + ASSERT_NE(texture, nullptr); + EXPECT_EQ(texture->GetMipLevels(), 4u); + EXPECT_EQ(texture->GetLayout(), EImageLayout::ShaderReadOnly); + EXPECT_EQ( + texture->GetUsage(), + EImageUsage::Sampled | EImageUsage::TransferSrc | EImageUsage::TransferDst + ); + + texture->Destroy(); +} + TEST_F(VulkanImageTest, VulkanImage_LayoutTransition) { SImageCreateInfo info = Image::CreateImageInfo(EImageFormat::R8G8B8A8_UNORM, 64); info.Usage = EImageUsage::Sampled | EImageUsage::TransferDst; diff --git a/Shaders/Material/Material.ps.hlsl b/Shaders/Material/Material.ps.hlsl index 7e8becb0..0e813680 100644 --- a/Shaders/Material/Material.ps.hlsl +++ b/Shaders/Material/Material.ps.hlsl @@ -220,6 +220,18 @@ float3 FresnelSchlickRoughness(float NdotV, float3 F0, float roughness) 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. * @@ -304,6 +316,8 @@ float4 main(PSInput input) : SV_Target0 N = normalize(mul(tangentNormal, tangentBasis)); #endif + roughness = FilterSpecularRoughness(roughness, N); + float3 R = reflect(-V, N); float3 L = normalize(LightDirection.xyz); float NdotV = saturate(dot(N, V)) + 1e-4f; @@ -352,7 +366,8 @@ float4 main(PSInput input) : SV_Target0 const float clearCoat = saturate(surface.ClearCoat); if (clearCoat > 0.0f) { - const float coatRoughness = clamp(surface.ClearCoatRoughness, 0.045f, 1.0f); + float coatRoughness = clamp(surface.ClearCoatRoughness, 0.045f, 1.0f); + coatRoughness = FilterSpecularRoughness(coatRoughness, coatNormal); const float coatNdotV = saturate(dot(coatNormal, V)) + 1e-4f; const float coatNdotL = saturate(dot(coatNormal, L)); From 5734d076b014dcf08ffc9f11d9c521742e5a00a5 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Thu, 24 Sep 2026 15:31:56 -0300 Subject: [PATCH 25/61] Remove redundant mipmap comment tag --- Elixir/Source/Engine/Graphics/Image.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Elixir/Source/Engine/Graphics/Image.h b/Elixir/Source/Engine/Graphics/Image.h index 5a7baf0b..eac86780 100644 --- a/Elixir/Source/Engine/Graphics/Image.h +++ b/Elixir/Source/Engine/Graphics/Image.h @@ -196,7 +196,7 @@ namespace Elixir EImageLayout InitialLayout = EImageLayout::Undefined; SAllocationInfo AllocationInfo; - /** @brief Generates lower mip levels from the supplied base level. */ + /** Generates lower mip levels from the supplied base level. */ bool GenerateMipmaps = false; }; From 0d8e95036a89944f15a06902bc83ac9876c06371 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Thu, 24 Sep 2026 15:58:28 -0300 Subject: [PATCH 26/61] Centralize texture mip level calculation --- Elixir/Source/Engine/Graphics/Image.cpp | 45 +++++++++++++------ Elixir/Source/Engine/Graphics/Image.h | 3 ++ Elixir/Source/Engine/Graphics/Texture.cpp | 12 +++-- .../Source/Engine/Graphics/TextureLoader.cpp | 17 ------- Elixir/Source/Graphics/Vulkan/VulkanImage.h | 2 + .../Platform/GLTF/GLTFStaticMeshLoader.cpp | 17 ------- Elixir/Tests/Engine/Graphics/ImageTest.cpp | 9 +++- 7 files changed, 53 insertions(+), 52 deletions(-) diff --git a/Elixir/Source/Engine/Graphics/Image.cpp b/Elixir/Source/Engine/Graphics/Image.cpp index bb78a5b9..74136ed6 100644 --- a/Elixir/Source/Engine/Graphics/Image.cpp +++ b/Elixir/Source/Engine/Graphics/Image.cpp @@ -10,23 +10,26 @@ namespace Elixir { using namespace Elixir::Graphics; - uint32_t CalculateBitsPerPixel(const Image* image) + namespace { - EE_PROFILE_ZONE_SCOPED() + uint32_t CalculateBitsPerPixel(const Image* image) + { + EE_PROFILE_ZONE_SCOPED() - const auto blockSize = Utils::GetFormatBlockSizeBits(image); - const auto blockExtent = Utils::GetFormatBlockExtent(image); - const uint32_t texels = blockExtent.x * blockExtent.y * blockExtent.z; + const auto blockSize = Utils::GetFormatBlockSizeBits(image); + const auto blockExtent = Utils::GetFormatBlockExtent(image); + const uint32_t texels = blockExtent.x * blockExtent.y * blockExtent.z; - return blockSize / texels; - } + return blockSize / texels; + } - size_t CalculateSize(const Image* image) - { - EE_PROFILE_ZONE_SCOPED() + size_t CalculateSize(const Image* image) + { + EE_PROFILE_ZONE_SCOPED() - const auto extent = image->GetExtent(); - return extent.Width * extent.Height * extent.Depth * image->GetBytesPerPixel(); + const auto extent = image->GetExtent(); + return extent.Width * extent.Height * extent.Depth * image->GetBytesPerPixel(); + } } /* Image */ @@ -175,6 +178,20 @@ namespace Elixir }; } + 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; + } + Image::Image(const GraphicsContext* context, const SImageCreateInfo& info) : m_GraphicsContext(context) { @@ -230,7 +247,7 @@ namespace Elixir ) { const auto imageFormat = Converters::GetImageFormat(format); - const auto usage = EImageUsage::Sampled | EImageUsage::DepthStencilAttachment; + constexpr auto usage = EImageUsage::Sampled | EImageUsage::DepthStencilAttachment; return { .Width = width, .Height = height, @@ -276,4 +293,4 @@ namespace Elixir // EImageUsage::ColorAttachment; // m_Aspect = Utils::CalculateImageAspect(m_Usage, m_Format); // } -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/Graphics/Image.h b/Elixir/Source/Engine/Graphics/Image.h index eac86780..ec3e5273 100644 --- a/Elixir/Source/Engine/Graphics/Image.h +++ b/Elixir/Source/Engine/Graphics/Image.h @@ -320,6 +320,9 @@ namespace Elixir const void* data = nullptr ); + /** Returns the number of mip levels from a base image extent. */ + static uint32_t GetFullMipLevelCount(const Extent3D& extent); + protected: Image(const GraphicsContext* context, const SImageCreateInfo& info); Image(const Image&) = delete; diff --git a/Elixir/Source/Engine/Graphics/Texture.cpp b/Elixir/Source/Engine/Graphics/Texture.cpp index f6abe161..abd6ff17 100644 --- a/Elixir/Source/Engine/Graphics/Texture.cpp +++ b/Elixir/Source/Engine/Graphics/Texture.cpp @@ -55,7 +55,9 @@ namespace Elixir .Width = info.Width, .Type = EImageType::_1D, .Format = info.Format, - .MipLevels = info.MipLevels, + .MipLevels = info.GenerateMipmaps + ? GetFullMipLevelCount({ info.Width, 1, 1 }) + : info.MipLevels, .Usage = usage, .InitialLayout = EImageLayout::ShaderReadOnly, .AllocationInfo = { @@ -154,7 +156,9 @@ namespace Elixir .Height = info.Height, .Type = EImageType::_2D, .Format = info.Format, - .MipLevels = info.MipLevels, + .MipLevels = info.GenerateMipmaps + ? GetFullMipLevelCount({ info.Width, info.Height, 1 }) + : info.MipLevels, .Usage = usage, .InitialLayout = EImageLayout::ShaderReadOnly, .AllocationInfo = { @@ -256,7 +260,9 @@ namespace Elixir .Depth = info.Depth, .Type = EImageType::_3D, .Format = info.Format, - .MipLevels = info.MipLevels, + .MipLevels = info.GenerateMipmaps + ? GetFullMipLevelCount({ info.Width, info.Height, info.Depth }) + : info.MipLevels, .Usage = usage, .InitialLayout = EImageLayout::ShaderReadOnly, .AllocationInfo = { diff --git a/Elixir/Source/Engine/Graphics/TextureLoader.cpp b/Elixir/Source/Engine/Graphics/TextureLoader.cpp index c38bf46c..b31fcb6b 100644 --- a/Elixir/Source/Engine/Graphics/TextureLoader.cpp +++ b/Elixir/Source/Engine/Graphics/TextureLoader.cpp @@ -11,20 +11,6 @@ namespace Elixir namespace { - uint32_t GetFullMipLevelCount(const uint32_t width, const uint32_t height) - { - uint32_t largestDimension = std::max(width, height); - uint32_t levelCount = 1; - - while (largestDimension > 1) - { - largestDimension >>= 1; - ++levelCount; - } - - return levelCount; - } - void TraceTextureInfo( const std::string& path, bool isHdr, @@ -103,9 +89,6 @@ namespace Elixir createInfo.Height = height; createInfo.Path = pathStr; - if (createInfo.GenerateMipmaps) - createInfo.MipLevels = GetFullMipLevelCount(width, height); - TraceTextureInfo(pathStr, isHdr, createInfo.Format, channels); auto texture = Texture2D::Create(s_Context, createInfo); diff --git a/Elixir/Source/Graphics/Vulkan/VulkanImage.h b/Elixir/Source/Graphics/Vulkan/VulkanImage.h index 63113890..1cf2e738 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanImage.h +++ b/Elixir/Source/Graphics/Vulkan/VulkanImage.h @@ -71,8 +71,10 @@ namespace Elixir::Vulkan void CreateImage(const SImageCreateInfo& info); void InitImage(const SImageCreateInfo& info); + /** Generates the missing mip levels after the base level upload. */ void GenerateMipmaps(const CommandBuffer* cmd, EImageLayout finalLayout); + void CreateImageView(); void CreateDescriptorInfo(); void UpdateSampler() override; diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp index 2179c45d..170d8f09 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp @@ -51,20 +51,6 @@ namespace Elixir target.Max = glm::max(target.Max, source.Max); } - uint32_t GetFullMipLevelCount(const uint32_t width, const uint32_t height) - { - uint32_t largestDimension = std::max(width, height); - uint32_t levelCount = 1; - - while (largestDimension > 1) - { - largestDimension >>= 1; - ++levelCount; - } - - return levelCount; - } - Ref DecodeImage( const GraphicsContext& context, const std::byte* bytes, @@ -99,9 +85,6 @@ namespace Elixir createInfo.Width = width; createInfo.Height = height; - if (createInfo.GenerateMipmaps) - createInfo.MipLevels = GetFullMipLevelCount(width, height); - const auto texture = Texture2D::Create(&context, createInfo); stbi_image_free(pixels); diff --git a/Elixir/Tests/Engine/Graphics/ImageTest.cpp b/Elixir/Tests/Engine/Graphics/ImageTest.cpp index 32b438d7..b7e0fcfd 100644 --- a/Elixir/Tests/Engine/Graphics/ImageTest.cpp +++ b/Elixir/Tests/Engine/Graphics/ImageTest.cpp @@ -29,7 +29,7 @@ TEST(ImageTest, Texture2DCreateInfoBuildsAMipmapChain) textureInfo.Format = EImageFormat::R8G8B8A8_UNORM; textureInfo.Width = 8; textureInfo.Height = 4; - textureInfo.MipLevels = 4; + textureInfo.MipLevels = 1; textureInfo.GenerateMipmaps = true; const auto imageInfo = Texture2D::CreateImageInfo(textureInfo); @@ -41,3 +41,10 @@ TEST(ImageTest, Texture2DCreateInfoBuildsAMipmapChain) EImageUsage::Sampled | EImageUsage::TransferSrc | EImageUsage::TransferDst ); } + +TEST(ImageTest, GetFullMipLevelCountUsesTheLargestDimension) +{ + EXPECT_EQ(Image::GetFullMipLevelCount({ 8, 4, 1 }), 4u); + EXPECT_EQ(Image::GetFullMipLevelCount({ 1, 1, 1 }), 1u); + EXPECT_EQ(Image::GetFullMipLevelCount({ 2, 8, 16 }), 5u); +} From 93fdbd6420b88a3442a43f3e2c1948508bd22081 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Sat, 26 Sep 2026 21:19:50 -0300 Subject: [PATCH 27/61] Refactor image and texture resources --- Dissolve/Source/Dissolve.cpp | 2 +- Elixir/Source/Engine/Core/Core.h | 9 +- .../Source/Engine/Graphics/GraphicsContext.h | 24 +- Elixir/Source/Engine/Graphics/GraphicsTypes.h | 9 +- Elixir/Source/Engine/Graphics/Image.cpp | 584 ++++++++++++++---- Elixir/Source/Engine/Graphics/Image.h | 413 ++++++++++--- .../Source/Engine/Graphics/SamplerBuilder.cpp | 4 +- .../Source/Engine/Graphics/SamplerBuilder.h | 34 +- Elixir/Source/Engine/Graphics/Texture.cpp | 248 ++------ Elixir/Source/Engine/Graphics/Texture.h | 270 +++++--- .../Source/Engine/Graphics/TextureLoader.cpp | 2 +- Elixir/Source/Graphics/Vulkan/Initializers.h | 6 +- Elixir/Source/Graphics/Vulkan/Utils.h | 7 +- .../Graphics/Vulkan/VulkanDescriptorPool.cpp | 4 +- .../Graphics/Vulkan/VulkanGraphicsContext.cpp | 39 +- Elixir/Source/Graphics/Vulkan/VulkanImage.cpp | 535 +++++----------- Elixir/Source/Graphics/Vulkan/VulkanImage.h | 127 ++-- .../Source/Graphics/Vulkan/VulkanShader.cpp | 4 +- .../Source/Graphics/Vulkan/VulkanTexture.cpp | 94 --- Elixir/Source/Graphics/Vulkan/VulkanTexture.h | 74 --- .../Graphics/Vulkan/VulkanTextureSet.cpp | 4 +- .../Platform/GLTF/GLTFStaticMeshLoader.cpp | 8 +- Elixir/Tests/Engine/Graphics/ImageTest.cpp | 141 ++++- .../Materials/Rendering/FrameTableTest.cpp | 24 +- .../Tests/Graphics/Vulkan/VulkanImageTest.cpp | 347 +++++++---- Shaders/Material/Material.ps.hlsl | 3 +- 26 files changed, 1658 insertions(+), 1358 deletions(-) delete mode 100644 Elixir/Source/Graphics/Vulkan/VulkanTexture.cpp delete mode 100644 Elixir/Source/Graphics/Vulkan/VulkanTexture.h diff --git a/Dissolve/Source/Dissolve.cpp b/Dissolve/Source/Dissolve.cpp index b3f1c90a..ec56ac44 100644 --- a/Dissolve/Source/Dissolve.cpp +++ b/Dissolve/Source/Dissolve.cpp @@ -129,7 +129,7 @@ Dissolve::Dissolve() EE_CORE_ERROR("Dissolve particle emitter 'FlameCore' was not found.") } - if (auto loaded = StaticMeshLoaderRegistry::Load("./Assets/Meshes/McLaren/scene.gltf")) + if (auto loaded = StaticMeshLoaderRegistry::Load("./Assets/Meshes/Porsche/scene.gltf")) mesh = std::move(*loaded); } diff --git a/Elixir/Source/Engine/Core/Core.h b/Elixir/Source/Engine/Core/Core.h index 6b3778c1..f3062370 100644 --- a/Elixir/Source/Engine/Core/Core.h +++ b/Elixir/Source/Engine/Core/Core.h @@ -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/Graphics/GraphicsContext.h b/Elixir/Source/Engine/Graphics/GraphicsContext.h index b87a804e..cb6726eb 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; @@ -61,7 +60,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 +71,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; @@ -112,10 +114,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 74136ed6..3a4b4a37 100644 --- a/Elixir/Source/Engine/Graphics/Image.cpp +++ b/Elixir/Source/Engine/Graphics/Image.cpp @@ -2,10 +2,11 @@ #include "Image.h" #include -#include #include #include +#include + namespace Elixir { using namespace Elixir::Graphics; @@ -14,25 +15,113 @@ namespace Elixir { uint32_t CalculateBitsPerPixel(const Image* image) { - EE_PROFILE_ZONE_SCOPED() + 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) + SNormalMapMipChain BuildNormalMapMipChain(const Image* image, const void* baseData) { - EE_PROFILE_ZONE_SCOPED() + 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) { @@ -116,48 +205,194 @@ 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; + } + + if (info.MipmapMode == EImageMipmapMode::SimpleAverage && info.MipLevels > 1) + info.Usage |= EImageUsage::TransferSrc; + + 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( @@ -167,130 +402,221 @@ namespace Elixir ) { return { - .InitialData = data, .Width = width, .Type = EImageType::_1D, .Format = format, .Usage = EImageUsage::Sampled, - .AllocationInfo = { - .RequiredFlags = EMemoryProperty::DeviceLocal - } + .InitialData = data, }; } - uint32_t Image::GetFullMipLevelCount(const Extent3D& extent) + Image::Image(const GraphicsContext* context, const SImageCreateInfo& info) + : 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) { - uint32_t largestDimension = std::max({ extent.Width, extent.Height, extent.Depth }); - uint32_t levelCount = 1; + RecalculateSize(); +#ifdef EE_DEBUG + m_DebugName = "Image[" + m_UUID.ToString() + "]"; +#endif + } - while (largestDimension > 1) + void Image::Initialize(const SImageCreateInfo& info) + { + SNormalMapMipChain upload; + const void* pixels = info.InitialData; + size_t uploadSize = GetSize(); + + if (info.MipmapMode == EImageMipmapMode::NormalMap) { - largestDimension >>= 1; - ++levelCount; + 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; + } - return levelCount; - } + 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); + } - Image::Image(const GraphicsContext* context, const SImageCreateInfo& info) - : m_GraphicsContext(context) - { - EE_PROFILE_ZONE_SCOPED() + pixels = upload.Data.data(); + uploadSize = upload.Data.size(); + } + else if (pixels) + { + SBufferImageCopy region{}; + region.ImageSubresource = { m_Aspect, 0, 0, m_ArrayLayers }; + region.ImageExtent = m_Extent; + upload.Regions.push_back(region); + } - 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); + if (pixels && (m_Aspect & EImageAspect::Depth) && (m_Aspect & EImageAspect::Stencil)) + { + EE_CORE_ERROR("Combined depth/stencil uploads require separate aspect regions.") + return; + } -#ifdef EE_DEBUG - m_DebugName = "Image[" + m_UUID.ToString() + "]"; -#endif + 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(); } - void Image::RecalculateSize() + void Image::Resize(const Ref& cmd, const Extent3D& extent) { - m_BitsPerPixel = CalculateBitsPerPixel(this); - m_Size = CalculateSize(this); - } + if (!extent.Width || !extent.Height || !extent.Depth) + { + EE_CORE_ERROR("Image dimensions must be greater than zero.") + return; + } - /* DepthStencilImage */ + if (extent.Width == m_Extent.Width && extent.Height == m_Extent.Height && + extent.Depth == m_Extent.Depth) + return; - Ref DepthStencilImage::Create( - const GraphicsContext* context, - EDepthStencilImageFormat format, - uint32_t width, - uint32_t height - ) - { - switch (context->GetAPI()) + if ((m_Type == EImageType::_1D && (extent.Height != 1 || extent.Depth != 1)) || + (m_Type == EImageType::_2D && extent.Depth != 1)) { - case EGraphicsAPI::Vulkan: - return CreateRef( - context, format, width, height + 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(); + info.InitialLayout = EImageLayout::TransferDst; + + const auto staging = Create(m_GraphicsContext, info); + if (!staging) + return; + + const auto originalLayout = m_Layout; + const bool preserve = originalLayout != EImageLayout::Undefined; + + if (preserve) + { + 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 ); - default: - EE_CORE_ASSERT(false, "Unknown GraphicsAPI!") - return nullptr; + + staging->Transition(cmd, EImageLayout::TransferSrc); + cmd->Flush(); } - } - SImageCreateInfo DepthStencilImage::CreateImageInfo( - const EDepthStencilImageFormat format, - const uint32_t width, - const uint32_t height - ) - { - const auto imageFormat = Converters::GetImageFormat(format); - constexpr 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 - } - }; - } + 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; - DepthStencilImage::DepthStencilImage( - const GraphicsContext* context, - const SImageCreateInfo& info - ) : Image(context, info) - { - EE_PROFILE_ZONE_SCOPED() + Destroy(); + m_Extent = extent; + m_MipLevels = info.MipLevels; + m_Layout = EImageLayout::Undefined; + RecalculateSize(); + CreateResource(info); - m_Size = CalculateSize(this); + 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 + ); -#ifdef EE_DEBUG - m_DebugName = "DepthStencilImage[" + m_UUID.ToString() + "]"; -#endif + Transition(cmd, originalLayout); + cmd->Flush(); + } } - /* 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); - // } + void Image::RecalculateSize() + { + m_BitsPerPixel = CalculateBitsPerPixel(this); + m_Size = GetMipSize(0) * m_ArrayLayers; + } } diff --git a/Elixir/Source/Engine/Graphics/Image.h b/Elixir/Source/Engine/Graphics/Image.h index ec3e5273..59dcef19 100644 --- a/Elixir/Source/Engine/Graphics/Image.h +++ b/Elixir/Source/Engine/Graphics/Image.h @@ -2,18 +2,13 @@ #include #include -#include + +#include namespace Elixir { class Image; - namespace Vulkan - { - template - class VulkanBaseImage; - } - enum class EImageLayout { Undefined, @@ -65,6 +60,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 +184,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,122 +194,353 @@ 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 the transfer flags needed for initialization. */ + EImageUsage Usage = EImageUsage::Sampled; + + /** Layout after initialization; Undefined with initial pixels resolves to General. */ EImageLayout InitialLayout = EImageLayout::Undefined; - SAllocationInfo AllocationInfo; - /** Generates lower mip levels from the supplied base level. */ - bool GenerateMipmaps = false; + /** 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; }; + /** + * @brief Owns a graphics image and coordinates initialization independently of its + * backend. + */ class ELIXIR_API Image { - 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 Resizes storage and scales each existing mip level, waiting for the copies + * to finish. + * + * Mip levels that do not fit the new extent are removed; no new levels are generated. + * + * @param cmd Command buffer used to record and synchronously submit the resize copies. + * @param extent New base-level dimensions in texels. + * @pre The image has TransferSrc and TransferDst usage, and cmd is not recording. + */ + void Resize(const Ref& cmd, 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 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; } + + /** @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; - [[nodiscard]] const Ref& GetSampler() const { return m_Sampler; } - void SetSampler(const Ref& sampler); + /** + * @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 { @@ -307,6 +550,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, @@ -314,22 +579,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 ); - /** Returns the number of mip levels from a base image extent. */ + /** @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 @@ -354,34 +635,28 @@ 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; + private: + // Creates storage and completes the requested upload after backend construction. + void Initialize(const SImageCreateInfo& info); - [[nodiscard]] virtual uint32_t GetHeight() const { return m_Extent.Height; } + // Allocates backend resources from the already resolved description. + virtual void CreateResource(const SImageCreateInfo& info) = 0; - static Ref Create( - const GraphicsContext* context, - 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; - static SImageCreateInfo CreateImageInfo( - EDepthStencilImageFormat format, - uint32_t width, - uint32_t height - ); - - 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; }; + } 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/Texture.cpp b/Elixir/Source/Engine/Graphics/Texture.cpp index abd6ff17..e091aa63 100644 --- a/Elixir/Source/Engine/Graphics/Texture.cpp +++ b/Elixir/Source/Engine/Graphics/Texture.cpp @@ -1,36 +1,21 @@ #include "epch.h" #include "Texture.h" -#include -#include - namespace Elixir { - /* Texture */ - - Ref Texture::Create( - const GraphicsContext* context, - const STextureCreateInfo& info - ) + Texture::Texture(Ref image, const STextureCreateInfo& info) + : m_Image(std::move(image)), m_Path(info.Path), m_HDR(info.HDR) { - const auto imageInfo = CreateImageInfo(info); + } - switch (context->GetAPI()) - { - case EGraphicsAPI::Vulkan: - return CreateRef(context, imageInfo, info.Path); - default: - EE_CORE_ASSERT(false, "Unknown GraphicsAPI!") - return nullptr; - } + Ref Texture::Create(const GraphicsContext* context, const STextureCreateInfo& info) + { + return Ref(new Texture(Image::Create(context, CreateImageInfo(info)), info)); } Ref Texture::Create( - const GraphicsContext* context, - const EImageFormat format, - const 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 ) { STextureCreateInfo info; @@ -41,37 +26,24 @@ namespace Elixir return Create(context, info); } - SImageCreateInfo Texture::CreateImageInfo( - const STextureCreateInfo& info - ) + SImageCreateInfo Texture::CreateImageInfo(const STextureCreateInfo& info) { - auto usage = EImageUsage::Sampled | EImageUsage::TransferDst; - - if (info.GenerateMipmaps) - usage |= EImageUsage::TransferSrc; - return { .InitialData = info.InitialData, .Width = info.Width, .Type = EImageType::_1D, .Format = info.Format, - .MipLevels = info.GenerateMipmaps - ? GetFullMipLevelCount({ info.Width, 1, 1 }) - : info.MipLevels, - .Usage = usage, + .MipmapMode = info.MipmapMode, + .MipLevels = info.MipLevels, + .Usage = EImageUsage::Sampled, .InitialLayout = EImageLayout::ShaderReadOnly, - .AllocationInfo = { - .RequiredFlags = EMemoryProperty::DeviceLocal - }, - .GenerateMipmaps = info.GenerateMipmaps, + .InitialMipData = info.InitialMipData, }; } SImageCreateInfo Texture::CreateImageInfo( - const EImageFormat format, - const uint32_t width, - const void* data, - const std::string& path + const EImageFormat format, const uint32_t width, + const void* data, const std::string& path ) { STextureCreateInfo info; @@ -82,53 +54,19 @@ namespace Elixir return CreateImageInfo(info); } - 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)) + Texture2D::Texture2D(Ref image, const STextureCreateInfo& info) + : Texture(std::move(image), info) { - EE_PROFILE_ZONE_SCOPED() - -#ifdef EE_DEBUG - m_DebugName = "Texture[" + m_UUID.ToString() + "]"; -#endif } - /* Texture2D */ - - Ref Texture2D::Create( - const GraphicsContext* context, - const STexture2DCreateInfo& info - ) + Ref Texture2D::Create(const GraphicsContext* context, const STexture2DCreateInfo& info) { - const auto imageInfo = CreateImageInfo(info); - - switch (context->GetAPI()) - { - case EGraphicsAPI::Vulkan: - return CreateRef(context, imageInfo, info.Path); - default: - EE_CORE_ASSERT(false, "Unknown GraphicsAPI!") - return nullptr; - } + return Ref(new Texture2D(Image::Create(context, CreateImageInfo(info)), info)); } Ref Texture2D::Create( - const GraphicsContext* context, - const EImageFormat format, - const uint32_t width, - const 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 ) { STexture2DCreateInfo info; @@ -140,39 +78,16 @@ namespace Elixir return Create(context, info); } - SImageCreateInfo Texture2D::CreateImageInfo( - const STexture2DCreateInfo& info - ) + SImageCreateInfo Texture2D::CreateImageInfo(const STexture2DCreateInfo& info) { - - auto usage = EImageUsage::Sampled | EImageUsage::TransferDst; - - if (info.GenerateMipmaps) - usage |= EImageUsage::TransferSrc; - - return { - .InitialData = info.InitialData, - .Width = info.Width, - .Height = info.Height, - .Type = EImageType::_2D, - .Format = info.Format, - .MipLevels = info.GenerateMipmaps - ? GetFullMipLevelCount({ info.Width, info.Height, 1 }) - : info.MipLevels, - .Usage = usage, - .InitialLayout = EImageLayout::ShaderReadOnly, - .AllocationInfo = { - .RequiredFlags = EMemoryProperty::DeviceLocal - }, - .GenerateMipmaps = info.GenerateMipmaps, - }; + 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 ) { STexture2DCreateInfo info; @@ -183,55 +98,20 @@ namespace Elixir 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 - ) + Ref Texture3D::Create(const GraphicsContext* context, const STexture3DCreateInfo& info) { - const auto imageInfo = CreateImageInfo(info); - - switch (context->GetAPI()) - { - case EGraphicsAPI::Vulkan: - return CreateRef(context, imageInfo, info.Path); - default: - EE_CORE_ASSERT(false, "Unknown GraphicsAPI!") - return nullptr; - } + return Ref(new Texture3D(Image::Create(context, CreateImageInfo(info)), info)); } Ref Texture3D::Create( - 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 + 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 ) { STexture3DCreateInfo info; @@ -244,40 +124,17 @@ namespace Elixir return Create(context, info); } - SImageCreateInfo Texture3D::CreateImageInfo( - const STexture3DCreateInfo& info - ) + SImageCreateInfo Texture3D::CreateImageInfo(const STexture3DCreateInfo& info) { - auto usage = EImageUsage::Sampled | EImageUsage::TransferDst; - - if (info.GenerateMipmaps) - usage |= EImageUsage::TransferSrc; - - return { - .InitialData = info.InitialData, - .Width = info.Width, - .Height = info.Height, - .Depth = info.Depth, - .Type = EImageType::_3D, - .Format = info.Format, - .MipLevels = info.GenerateMipmaps - ? GetFullMipLevelCount({ info.Width, info.Height, info.Depth }) - : info.MipLevels, - .Usage = usage, - .InitialLayout = EImageLayout::ShaderReadOnly, - .AllocationInfo = { - .RequiredFlags = EMemoryProperty::DeviceLocal - }, - .GenerateMipmaps = info.GenerateMipmaps, - }; + auto image = Texture2D::CreateImageInfo(info); + image.Type = EImageType::_3D; + image.Depth = info.Depth; + return image; } SImageCreateInfo Texture3D::CreateImageInfo( - const EImageFormat format, - const uint32_t width, - const uint32_t height, - const uint32_t depth, - const void* data + const EImageFormat format, const uint32_t width, const uint32_t height, + const uint32_t depth, const void* data ) { STexture3DCreateInfo info; @@ -288,27 +145,4 @@ namespace Elixir info.Depth = depth; return CreateImageInfo(info); } - - 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) - { - EE_PROFILE_ZONE_SCOPED() - -#ifdef EE_DEBUG - m_DebugName = "Texture3D[" + m_UUID.ToString() + "]"; -#endif - } } diff --git a/Elixir/Source/Engine/Graphics/Texture.h b/Elixir/Source/Engine/Graphics/Texture.h index 6eff327d..a25833f9 100644 --- a/Elixir/Source/Engine/Graphics/Texture.h +++ b/Elixir/Source/Engine/Graphics/Texture.h @@ -4,55 +4,121 @@ namespace Elixir { - /** @brief Defines properties shared by every texture dimension. */ + /** @brief Defines image properties and asset metadata shared by texture dimensions. */ struct STextureCreateInfo { - /** Pixel data for the base mip level, or null for an empty texture. */ - const void* InitialData = nullptr; - /** Pixel format of the texture. */ EImageFormat Format = EImageFormat::Undefined; - - /** Width of the base mip level in texels. */ + + /** Base-level width in texels. */ uint32_t Width = 0; - - /** Number of mip levels allocated for the texture. */ + + /** Number of supplied mip levels when using LeaveExistingMips. */ uint32_t MipLevels = 1; - - /** Generates lower mip levels from the base level during upload. */ - bool GenerateMipmaps = false; - - /** Source path retained for diagnostics and asset tracking. */ + + /** 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 properties for a two-dimensional texture. */ + /** @brief Defines a two-dimensional texture asset. */ struct STexture2DCreateInfo : STextureCreateInfo { - /** Height of the base mip level in texels. */ + /** Base-level height in texels. */ uint32_t Height = 0; }; - /** @brief Defines properties for a three-dimensional texture. */ + /** @brief Defines a three-dimensional texture asset. */ struct STexture3DCreateInfo : STexture2DCreateInfo { - /** Depth of the base mip level in texels. */ + /** Base-level depth in texels. */ uint32_t Depth = 0; }; - class ELIXIR_API Texture : public Image + /** + * @brief Owns asset metadata and a shared image resource, independently of the + * graphics backend. + */ + class ELIXIR_API Texture { - public: - ~Texture() override = default; + 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(); } + + /** @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(); } - [[nodiscard]] const std::string& GetPath() const { return m_Path; } + /** @brief Returns the base-level height in texels. */ + uint32_t GetHeight() const { return m_Image->GetHeight(); } - /** @brief Creates a one-dimensional texture from the supplied description. */ + /** @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. + */ 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, @@ -60,12 +126,22 @@ namespace Elixir const void* data = nullptr, const std::string& path = "" ); - - /** @brief Builds the image description used to create a one-dimensional texture. */ - static SImageCreateInfo CreateImageInfo( - const STextureCreateInfo& info - ); - + + /** + * @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, @@ -73,36 +149,39 @@ 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; } - - /** @brief Creates a two-dimensional texture from the supplied description. */ + 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. + */ 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, @@ -112,11 +191,20 @@ namespace Elixir const std::string& path = "" ); - /** @brief Builds the image description used to create a two-dimensional texture. */ - static SImageCreateInfo CreateImageInfo( - const STexture2DCreateInfo& info - ); + /** + * @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, @@ -124,35 +212,35 @@ 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 texture from the supplied description. */ + /** + * @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. + */ 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, @@ -163,11 +251,21 @@ namespace Elixir const std::string& path = "" ); - /** @brief Builds the image description used to create a three-dimensional texture. */ - static SImageCreateInfo CreateImageInfo( - const STexture3DCreateInfo& info - ); + /** + * @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, @@ -176,20 +274,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); }; } diff --git a/Elixir/Source/Engine/Graphics/TextureLoader.cpp b/Elixir/Source/Engine/Graphics/TextureLoader.cpp index b31fcb6b..3be42d8e 100644 --- a/Elixir/Source/Engine/Graphics/TextureLoader.cpp +++ b/Elixir/Source/Engine/Graphics/TextureLoader.cpp @@ -88,11 +88,11 @@ namespace Elixir createInfo.Width = width; createInfo.Height = height; createInfo.Path = pathStr; + createInfo.HDR = isHdr; TraceTextureInfo(pathStr, isHdr, createInfo.Format, channels); auto texture = Texture2D::Create(s_Context, createInfo); - texture->m_HDR = isHdr; EE_CORE_TRACE("Loaded texture: {0} [{1}].", pathStr, texture->GetUUID()) stbi_image_free(data); 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 ed33959b..5f5ebfc7 100644 --- a/Elixir/Source/Graphics/Vulkan/Utils.h +++ b/Elixir/Source/Graphics/Vulkan/Utils.h @@ -220,7 +220,8 @@ namespace Elixir::Vulkan const VkImageAspectFlags aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, const VkFilter filter = VK_FILTER_LINEAR, const uint32_t srcMipLevel = 0, - const uint32_t dstMipLevel = 0 + const uint32_t dstMipLevel = 0, + const uint32_t layerCount = 1 ) { EE_PROFILE_ZONE_SCOPED() @@ -239,12 +240,12 @@ namespace Elixir::Vulkan blitRegion.srcSubresource.aspectMask = aspectMask; blitRegion.srcSubresource.baseArrayLayer = 0; - blitRegion.srcSubresource.layerCount = 1; + blitRegion.srcSubresource.layerCount = layerCount; blitRegion.srcSubresource.mipLevel = srcMipLevel; blitRegion.dstSubresource.aspectMask = aspectMask; blitRegion.dstSubresource.baseArrayLayer = 0; - blitRegion.dstSubresource.layerCount = 1; + blitRegion.dstSubresource.layerCount = layerCount; blitRegion.dstSubresource.mipLevel = dstMipLevel; VkBlitImageInfo2 blitInfo = {}; 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 31e08536..b9c9356f 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 @@ -487,22 +487,27 @@ namespace Elixir void VulkanGraphicsContext::CreateRenderTargets() { - 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 - ); - depthStencilInfo.Usage = depthStencilInfo.Usage | EImageUsage::TransferSrc | EImageUsage::TransferDst; - m_DepthStencilRenderTarget = CreateRef(this, depthStencilInfo); + 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() diff --git a/Elixir/Source/Graphics/Vulkan/VulkanImage.cpp b/Elixir/Source/Graphics/Vulkan/VulkanImage.cpp index 53f10a5c..bed858a1 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanImage.cpp +++ b/Elixir/Source/Graphics/Vulkan/VulkanImage.cpp @@ -1,295 +1,227 @@ #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(); - } + return dynamic_cast(image); } - const VulkanBaseImageBase* TryToGetVulkanImage(const Image* image) + VkImage TryToGetVulkanImageHandle(const Image* image) { - if (!image) return nullptr; - return TryToGetVulkanImageImpl(image); + const auto* vkImage = TryToGetVulkanImage(image); + return vkImage ? vkImage->GetVulkanImage() : VK_NULL_HANDLE; } - VkImage TryToGetVulkanImageHandle(const Image* image) + VulkanImage::VulkanImage(const GraphicsContext* context, const SImageCreateInfo& info) + : Image(context, info), + m_Context((const VulkanGraphicsContext*)context) {} + + VulkanImage::~VulkanImage() { - if (!image) return VK_NULL_HANDLE; - return TryToGetVulkanImageHandleImpl(image); + Destroy(); } - /* VulkanBaseImage */ - - template - void VulkanBaseImage::Destroy() + void VulkanImage::Destroy() { - EE_PROFILE_ZONE_SCOPED() + if (m_ImageView) + vkDestroyImageView(m_Context->GetDevice(), m_ImageView, nullptr); - if (!IsValid()) return; + if (m_Image) + vmaDestroyImage(m_Context->GetAllocator(), m_Image, m_Allocation); - 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 = {}; } - template - void VulkanBaseImage::Resize(const Ref& cmd, const Extent3D extent) + void VulkanImage::CreateResource(const SImageCreateInfo& info) { - 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) + if (info.MipmapMode == EImageMipmapMode::SimpleAverage && info.MipLevels > 1) + { + VkFormatProperties properties{}; + vkGetPhysicalDeviceFormatProperties( + m_Context->GetGPU(), + Converters::GetFormat(info.Format), + &properties + ); + + constexpr VkFormatFeatureFlags required = VK_FORMAT_FEATURE_BLIT_SRC_BIT | + VK_FORMAT_FEATURE_BLIT_DST_BIT | + VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT; + + 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_Context->GetAllocator(), &imageInfo, &allocInfo, &m_Image, &m_Allocation, nullptr + )); + + if (!m_DebugName.empty()) + vmaSetAllocationName(m_Context->GetAllocator(), m_Allocation, m_DebugName.c_str()); + + constexpr auto viewUsage = EImageUsage::Sampled | + EImageUsage::Storage | + EImageUsage::ColorAttachment | + EImageUsage::DepthStencilAttachment | + EImageUsage::InputAttachment; + + if (HasAnyFlags(m_Usage, viewUsage)) + { + const auto viewInfo = Initializers::ImageViewCreateInfo(this); + VK_CHECK_RESULT( + vkCreateImageView( + m_Context->GetDevice(), + &viewInfo, + nullptr, + &m_ImageView + ) + ); + } + + m_DescriptorInfo = {}; + m_DescriptorInfo.imageLayout = Converters::GetImageLayout(m_Layout); + m_DescriptorInfo.imageView = m_ImageView; } - template - void VulkanBaseImage::Transition(const CommandBuffer* cmd, const EImageLayout layout) + void VulkanImage::Transition(const CommandBuffer* cmd, const EImageLayout layout) { - EE_PROFILE_ZONE_SCOPED() + if (!IsValid()) + { + EE_CORE_ERROR("Cannot transition an image without native storage.") + return; + } - const auto vk_Cmd = static_cast(cmd); + if (layout == m_Layout) + return; + if (layout == EImageLayout::Undefined || layout == EImageLayout::PreInitialized) + { + EE_CORE_ERROR("Cannot transition an image into an initial-only layout.") + return; + } + + const auto* vkCmd = static_cast(cmd); CommandUtils::TransitionImage( - vk_Cmd->GetVulkanCommandBuffer(), + vkCmd->GetVulkanCommandBuffer(), m_Image, - Converters::GetImageLayout(this->GetLayout()), + Converters::GetImageLayout(m_Layout), Converters::GetImageLayout(layout), - Converters::GetImageAspect(this->GetAspect()) + Converters::GetImageAspect(m_Aspect) ); - this->m_Layout = layout; + m_Layout = layout; + m_DescriptorInfo.imageLayout = Converters::GetImageLayout(layout); } - template - void VulkanBaseImage::Copy( - const CommandBuffer* cmd, - Image* dst, - const Extent3D& srcExtent, - const Extent3D& dstExtent + void VulkanImage::CopyMip( + const CommandBuffer* cmd, Image* dst, + const Extent3D& srcExtent, const Extent3D& dstExtent, + const uint32_t level ) { - EE_PROFILE_ZONE_SCOPED() + const auto* vkCmd = static_cast(cmd); + const auto target = TryToGetVulkanImageHandle(dst); - const auto vk_Cmd = static_cast(cmd); - const auto vk_Dst = TryToGetVulkanImageHandle(dst); - - EE_CORE_ASSERT(vk_Dst != nullptr, "Invalid destination image!") - EE_CORE_ASSERT(vk_Dst != VK_NULL_HANDLE, "Invalid destination image!") + 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; + } - // 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); + const bool depthStencil = (m_Aspect & EImageAspect::Depth) || + (m_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 - ); + for (const auto aspect : { EImageAspect::Color, EImageAspect::Depth, EImageAspect::Stencil }) + { + if (!(m_Aspect & aspect)) + continue; - dst->m_Extent = dstExtent; + 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 + ); + } } - template - void VulkanBaseImage::CopyFrom( - const CommandBuffer* cmd, - const Buffer* src, + void VulkanImage::CopyFrom( + const CommandBuffer* cmd, const Buffer* src, const std::span regions ) { - 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 (!IsValid()) { - SBufferImageCopy defaultRegion[] = { - SBufferImageCopy::Default(this->GetExtent()) - }; - copyRegions = std::span(defaultRegion); + EE_CORE_ERROR("Cannot upload to an image without native storage.") + return; } - std::vector imageCopies(copyRegions.size()); + const auto* vkCmd = static_cast(cmd); + const auto source = TryToGetVulkanBuffer(src); + EE_CORE_ASSERT(source != VK_NULL_HANDLE, "Invalid source buffer!") + + 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, imageCopies.begin(), Converters::GetBufferImageCopy + copyRegions, + copies.begin(), + Converters::GetBufferImageCopy ); vkCmdCopyBufferToImage( - vk_Cmd->GetVulkanCommandBuffer(), vk_Src, m_Image, - Converters::GetImageLayout(this->GetLayout()), imageCopies.size(), - imageCopies.data() + vkCmd->GetVulkanCommandBuffer(), + source, + m_Image, + Converters::GetImageLayout(m_Layout), + uint32_t(copies.size()), + copies.data() ); } - template - VulkanBaseImage::VulkanBaseImage( - const GraphicsContext* context, - const SImageCreateInfo& info - ) : Base(context, info), VulkanBaseImageBase() + void VulkanImage::GenerateMipmaps(const CommandBuffer* cmd, const EImageLayout finalLayout) { - EE_PROFILE_ZONE_SCOPED() - - m_GraphicsContext = static_cast(context); - - CreateImage(info); - InitImage(info); - CreateImageView(); - CreateDescriptorInfo(); - } - - template - void VulkanBaseImage::CreateImage(const SImageCreateInfo& info) - { - EE_PROFILE_ZONE_SCOPED() - - const uint32_t queueFamily = m_GraphicsContext->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_Image, - &m_Allocation, - nullptr - ) - ); - - if (!this->m_DebugName.empty()) + if (!IsValid()) { - vmaSetAllocationName( - m_GraphicsContext->GetAllocator(), - m_Allocation, - this->m_DebugName.c_str() - ); + EE_CORE_ERROR("Cannot generate mipmaps for an image without native storage.") + return; } - } - template - void VulkanBaseImage::GenerateMipmaps( - const CommandBuffer* cmd, - const EImageLayout finalLayout - ) - { const auto vkCmd = static_cast(cmd); - const auto vkFinalLayout = Converters::GetImageLayout(finalLayout); - const auto aspect = Converters::GetImageAspect(this->GetAspect()); - - uint32_t sourceWidth = this->GetWidth(); - uint32_t sourceHeight = this->GetExtent().Height; - uint32_t sourceDepth = this->GetExtent().Depth; + const auto aspect = Converters::GetImageAspect(m_Aspect); + const auto targetLayout = Converters::GetImageLayout(finalLayout); - for (uint32_t level = 1; level < this->GetMipLevels(); ++level) + for (uint32_t level = 1; level < m_MipLevels; ++level) { - const uint32_t destinationWidth = std::max(1u, sourceWidth >> 1u); - const uint32_t destinationHeight = std::max(1u, sourceHeight >> 1u); - const uint32_t destinationDepth = std::max(1u, sourceDepth >> 1u); - CommandUtils::TransitionImage( vkCmd->GetVulkanCommandBuffer(), m_Image, @@ -304,176 +236,37 @@ namespace Elixir::Vulkan vkCmd->GetVulkanCommandBuffer(), m_Image, m_Image, - { sourceWidth, sourceHeight, sourceDepth }, - { destinationWidth, destinationHeight, destinationDepth }, + Converters::GetExtent3D(GetMipExtent(level - 1)), + Converters::GetExtent3D(GetMipExtent(level)), aspect, VK_FILTER_LINEAR, level - 1, - level + level, + m_ArrayLayers ); CommandUtils::TransitionImage( vkCmd->GetVulkanCommandBuffer(), m_Image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - vkFinalLayout, + targetLayout, aspect, level - 1, 1 ); - - sourceWidth = destinationWidth; - sourceHeight = destinationHeight; - sourceDepth = destinationDepth; } CommandUtils::TransitionImage( vkCmd->GetVulkanCommandBuffer(), m_Image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - vkFinalLayout, + targetLayout, aspect, - this->GetMipLevels() - 1, + m_MipLevels - 1, 1 ); - this->m_Layout = finalLayout; + m_Layout = finalLayout; + m_DescriptorInfo.imageLayout = targetLayout; } - - template - void VulkanBaseImage::InitImage(const SImageCreateInfo& info) - { - EE_PROFILE_ZONE_SCOPED() - - auto cmd = m_GraphicsContext->GetUploadCommandBuffer(); - auto vk_Cmd = std::static_pointer_cast(cmd); - - cmd->Begin(); - - EE_CORE_ASSERT( - !info.GenerateMipmaps || info.InitialData, - "Generated mipmaps require initial image data." - ) - if (info.InitialData) - { - this->Transition(cmd.get(), EImageLayout::TransferDst); - - const auto stagingBuffer = StagingBuffer::Create( - m_GraphicsContext, - this->GetSize(), - info.InitialData - ); - - SBufferImageCopy copyRegion = {}; - copyRegion.ImageSubresource.AspectMask = this->GetAspect(); - copyRegion.ImageSubresource.LayerCount = this->GetArrayLayers(); - copyRegion.ImageExtent = this->GetExtent(); - - SBufferImageCopy regions[] = { copyRegion }; - this->CopyFrom(cmd, stagingBuffer, regions); - - if (info.GenerateMipmaps) - GenerateMipmaps(cmd.get(), info.InitialLayout); - else - this->Transition(cmd.get(), info.InitialLayout); - - cmd->Flush(); - - stagingBuffer->Destroy(); - } - else - { - this->Transition(cmd.get(), info.InitialLayout); - cmd->Flush(); - } - } - - template - void VulkanBaseImage::CreateImageView() - { - EE_PROFILE_ZONE_SCOPED() - - const auto viewInfo = Initializers::ImageViewCreateInfo(this); - VK_CHECK_RESULT( - vkCreateImageView( - m_GraphicsContext->GetDevice(), - &viewInfo, - nullptr, - &m_ImageView - ) - ); - } - - template - void VulkanBaseImage::CreateDescriptorInfo() - { - EE_PROFILE_ZONE_SCOPED() - m_DescriptorInfo = VkDescriptorImageInfo{}; - m_DescriptorInfo.imageLayout = Converters::GetImageLayout(this->GetLayout()); - m_DescriptorInfo.imageView = m_ImageView; - } - - 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! - } - - /* VulkanImage */ - - VulkanImage::VulkanImage( - const GraphicsContext* context, - const EImageFormat format, - const uint32_t width, - const void* data - ) : VulkanImage(context, CreateImageInfo(format, width, data)) {} - - VulkanImage::VulkanImage(const GraphicsContext* context, const SImageCreateInfo& info) - : VulkanBaseImage(context, info) - { - EE_PROFILE_ZONE_SCOPED() - - // TODO: Set a default sampler - // auto sampler = SamplerBuilder() - // .SetMagFilter(ESamplerFilter::Nearest) - // .SetMinFilter(ESamplerFilter::Nearest) - // .Build(); - - // SetSampler(sampler); - } - - VulkanImage::~VulkanImage() - { - EE_PROFILE_ZONE_SCOPED() - VulkanBaseImage::Destroy(); - } - - /* VulkanDepthStencilImage */ - - VulkanDepthStencilImage::VulkanDepthStencilImage( - const GraphicsContext* context, - const EDepthStencilImageFormat format, - const uint32_t width, - const uint32_t height - ) : VulkanDepthStencilImage(context, CreateImageInfo(format, width, height)) {} - - VulkanDepthStencilImage::VulkanDepthStencilImage( - const GraphicsContext* context, - const SImageCreateInfo& info - ) - : VulkanBaseImage(context, info) - { - } - - VulkanDepthStencilImage::~VulkanDepthStencilImage() - { - EE_PROFILE_ZONE_SCOPED() - VulkanBaseImage::Destroy(); - } - - template class VulkanBaseImage; - template class VulkanBaseImage; - template class VulkanBaseImage; } diff --git a/Elixir/Source/Graphics/Vulkan/VulkanImage.h b/Elixir/Source/Graphics/Vulkan/VulkanImage.h index 1cf2e738..6dfb8145 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanImage.h +++ b/Elixir/Source/Graphics/Vulkan/VulkanImage.h @@ -1,116 +1,93 @@ #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; - - protected: - VulkanBaseImageBase() = default; - }; - - const VulkanBaseImageBase* TryToGetVulkanImage(const Image* image); + /** Returns the Vulkan backend, or null when the image does not use Vulkan. */ + ELIXIR_API const VulkanImage* 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); + + /** @brief Releases the native image and view. */ + ~VulkanImage() override; - using Image::Resize; - virtual void Resize(const Ref& cmd, Extent3D extent) 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; 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; } - VulkanBaseImage& operator=(const VulkanBaseImage&) = delete; - VulkanBaseImage& operator=(VulkanBaseImage&&) = delete; + /** @brief Returns the view spanning every mip level and layer. */ + VkImageView GetVulkanImageView() const { return m_ImageView; } - 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); + private: + // Creates native storage and its view from the resolved description. + void CreateResource(const SImageCreateInfo& info) override; - /** Generates the missing mip levels after the base level upload. */ - void GenerateMipmaps(const CommandBuffer* cmd, EImageLayout finalLayout); + // Generates simple-average mip levels with linear image blits. + void GenerateMipmaps(const CommandBuffer* cmd, EImageLayout finalLayout) override; - void CreateImageView(); - void CreateDescriptorInfo(); - void UpdateSampler() 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; - 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 e2483093..30d7d18a 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanShader.cpp +++ b/Elixir/Source/Graphics/Vulkan/VulkanShader.cpp @@ -447,8 +447,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/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 index 170d8f09..8beaa125 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp @@ -193,9 +193,9 @@ namespace Elixir ) { const uint64_t cacheKey = - (uint64_t(textureIndex) << 2) | + (uint64_t(textureIndex) << 3) | (info.Format == EImageFormat::R8G8B8A8_SRGB ? 1ull : 0ull) | - (info.GenerateMipmaps ? 2ull : 0ull); + (static_cast(info.MipmapMode) << 1); const auto existing = cache.find(cacheKey); if (existing != cache.end()) @@ -899,7 +899,7 @@ namespace Elixir { STexture2DCreateInfo info; info.Format = EImageFormat::R8G8B8A8_UNORM; - info.GenerateMipmaps = true; + info.MipmapMode = EImageMipmapMode::NormalMap; normalTexture = LoadTexture( context, asset, @@ -1003,7 +1003,7 @@ namespace Elixir { STexture2DCreateInfo info; info.Format = EImageFormat::R8G8B8A8_UNORM; - info.GenerateMipmaps = true; + info.MipmapMode = EImageMipmapMode::NormalMap; clearCoatNormalTexture = LoadTexture( context, asset, diff --git a/Elixir/Tests/Engine/Graphics/ImageTest.cpp b/Elixir/Tests/Engine/Graphics/ImageTest.cpp index b7e0fcfd..23cfdef4 100644 --- a/Elixir/Tests/Engine/Graphics/ImageTest.cpp +++ b/Elixir/Tests/Engine/Graphics/ImageTest.cpp @@ -1,50 +1,149 @@ #include -using namespace testing; #include #include + using namespace Elixir; -TEST(ImageTest, Image_IsNotConstructibleAndAssignable) +namespace { - EXPECT_FALSE(std::is_constructible_v); + class ImageMetadata final : public Image + { + public: + explicit ImageMetadata(const SImageCreateInfo& info) : Image(nullptr, info) {} + void Destroy() override {} + void Transition(const CommandBuffer*, EImageLayout) 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_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_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); + 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, Texture2DCreateInfoBuildsAMipmapChain) +TEST(ImageTest, TextureMappingDoesNotResolveMipmapsOrUploadUsage) { STexture2DCreateInfo textureInfo; textureInfo.Format = EImageFormat::R8G8B8A8_UNORM; textureInfo.Width = 8; textureInfo.Height = 4; - textureInfo.MipLevels = 1; - textureInfo.GenerateMipmaps = true; - + textureInfo.MipLevels = 99; + textureInfo.MipmapMode = EImageMipmapMode::NormalMap; const auto imageInfo = Texture2D::CreateImageInfo(textureInfo); - - EXPECT_TRUE(imageInfo.GenerateMipmaps); - EXPECT_EQ(imageInfo.MipLevels, 4u); - EXPECT_EQ( - imageInfo.Usage, - EImageUsage::Sampled | EImageUsage::TransferSrc | EImageUsage::TransferDst - ); + 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, GetFullMipLevelCountUsesTheLargestDimension) +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) +{ + 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{ + .InitialData = pixels.data(), + .Width = 8, .Height = 4, + .Format = EImageFormat::R8G8B8A8_UNORM, + .MipmapMode = EImageMipmapMode::NormalMap, + .MipLevels = 4, + .AllocationInfo = { .RequiredFlags = EMemoryProperty::HostVisible } + }; + 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/Rendering/FrameTableTest.cpp b/Elixir/Tests/Engine/Materials/Rendering/FrameTableTest.cpp index e7f405f4..276fc228 100644 --- a/Elixir/Tests/Engine/Materials/Rendering/FrameTableTest.cpp +++ b/Elixir/Tests/Engine/Materials/Rendering/FrameTableTest.cpp @@ -16,29 +16,7 @@ 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, {}) {} }; } diff --git a/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp b/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp index 9f2f3a66..3386beae 100644 --- a/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp +++ b/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp @@ -1,13 +1,9 @@ #include -using namespace testing; -#include -#include -#include -#include #include #include - +#include +#include #include "VulkanTestContext.h" #include @@ -15,7 +11,7 @@ using namespace testing; using namespace Elixir; using namespace Elixir::Vulkan; -class VulkanImageTest : public Test +class VulkanImageTest : public testing::Test { protected: static void SetUpTestSuite() @@ -23,174 +19,259 @@ 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->GetMipLevels(), 1u); EXPECT_EQ(image->GetUsage(), EImageUsage::Sampled); EXPECT_EQ(image->GetLayout(), EImageLayout::Undefined); - EXPECT_EQ(image->GetAspect(), EImageAspect::Color); - image->Destroy(); - SUCCEED(); -} - -TEST_F(VulkanImageTest, VulkanDepthStencilImage_DepthOnly) -{ - 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); - - image->Destroy(); - SUCCEED(); + EXPECT_FALSE(image->IsValid()); + EXPECT_NO_THROW(image->Destroy()); + EXPECT_EQ(TryToGetVulkanImageHandle(nullptr), VK_NULL_HANDLE); } -TEST_F(VulkanImageTest, VulkanDepthStencilImage_DepthStencil) +TEST_F(VulkanImageTest, DepthImagesUseTheSameBackend) { - const auto image = DepthStencilImage::Create( - Context, - EDepthStencilImageFormat::D32_SFLOAT_S8_UINT, - 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 * 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); - - image->Destroy(); - SUCCEED(); + 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); + } } -TEST_F(VulkanImageTest, VulkanTexture2DGeneratesMipmapChain) +TEST_F(VulkanImageTest, TextureOwnsAnInitializedImageAndMetadata) { - std::array pixels{}; + 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 = 4; - info.GenerateMipmaps = true; + 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()); +} - const auto texture = Texture2D::Create(Context, info); +TEST_F(VulkanImageTest, NormalMapMipsAreGeneratedByImageAndUploaded) +{ + const std::array pixels{ + 255, 128, 128, 20, 0, 127, 128, 40, + 128, 255, 128, 60, 127, 0, 255, 80 + }; + SImageCreateInfo info{ + .InitialData = pixels.data(), + .Width = 2, .Height = 2, + .Format = EImageFormat::R8G8B8A8_UNORM, + .MipmapMode = EImageMipmapMode::NormalMap, + .MipLevels = 99, + .Usage = EImageUsage::Sampled | EImageUsage::TransferSrc, + .InitialLayout = EImageLayout::ShaderReadOnly + }; + 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)); +} - ASSERT_NE(texture, nullptr); - EXPECT_EQ(texture->GetMipLevels(), 4u); - EXPECT_EQ(texture->GetLayout(), EImageLayout::ShaderReadOnly); - EXPECT_EQ( - texture->GetUsage(), - EImageUsage::Sampled | EImageUsage::TransferSrc | EImageUsage::TransferDst - ); +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()))); +} - texture->Destroy(); +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, { + .InitialData = pixels.data(), + .Width = 2, .Height = 2, + .Format = EImageFormat::R8G8B8A8_UNORM, + .MipmapMode = EImageMipmapMode::SimpleAverage, + .ArrayLayers = 2, + .InitialLayout = EImageLayout::ShaderReadOnly, + }); + 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, { + .InitialData = pixels.data(), + .Width = 4, .Height = 4, + .Format = EImageFormat::R8G8B8A8_UNORM, + .MipmapMode = EImageMipmapMode::SimpleAverage, + .InitialLayout = EImageLayout::ShaderReadOnly, + }); + image->Resize(Context->GetUploadCommandBuffer(), { 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, { + .InitialData = pixels.data(), .Width = 4, .Height = 4, + .Format = EImageFormat::R8G8B8A8_UNORM, + .Usage = EImageUsage::Sampled | EImageUsage::TransferSrc, + .InitialLayout = EImageLayout::TransferSrc, + }); + 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/Material/Material.ps.hlsl b/Shaders/Material/Material.ps.hlsl index 0e813680..91091bbe 100644 --- a/Shaders/Material/Material.ps.hlsl +++ b/Shaders/Material/Material.ps.hlsl @@ -366,8 +366,7 @@ float4 main(PSInput input) : SV_Target0 const float clearCoat = saturate(surface.ClearCoat); if (clearCoat > 0.0f) { - float coatRoughness = clamp(surface.ClearCoatRoughness, 0.045f, 1.0f); - coatRoughness = FilterSpecularRoughness(coatRoughness, coatNormal); + float coatRoughness = saturate(surface.ClearCoatRoughness); const float coatNdotV = saturate(dot(coatNormal, V)) + 1e-4f; const float coatNdotL = saturate(dot(coatNormal, L)); From 3d887d7c302f8751cd0fc75c064048f92c43cbaf Mon Sep 17 00:00:00 2001 From: MrChampz Date: Sat, 26 Sep 2026 22:29:53 -0300 Subject: [PATCH 28/61] Apply glTF node transforms to static meshes --- .../Platform/GLTF/GLTFStaticMeshLoader.cpp | 114 +++++++++++++++++- .../Platform/GLTF/GLTFStaticMeshLoader.h | 2 + 2 files changed, 111 insertions(+), 5 deletions(-) diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp index 8beaa125..e84d2cb8 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp @@ -23,6 +23,7 @@ #include #include +#include #include namespace Elixir @@ -51,6 +52,48 @@ namespace Elixir target.Max = glm::max(target.Max, source.Max); } + glm::mat4 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; + } + + bool ApplyLocalTransform( + std::vector& vertices, + 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 = glm::normalize(normalTransform * vertex.Normal); + + glm::vec3 tangent = linearTransform * glm::vec3(vertex.Tangent); + tangent -= vertex.Normal * glm::dot(vertex.Normal, tangent); + vertex.Tangent = { + glm::normalize(tangent), + vertex.Tangent.w * handedness + }; + } + + return true; + } + Ref DecodeImage( const GraphicsContext& context, const std::byte* bytes, @@ -1043,12 +1086,14 @@ namespace Elixir fastgltf::Category::Buffers | fastgltf::Category::Images | fastgltf::Category::Textures | - fastgltf::Category::Materials; + fastgltf::Category::Materials | + fastgltf::Category::Scenes; static void LoadPrimitive( SStaticMeshData& mesh, const fastgltf::Asset& asset, const fastgltf::Primitive& primitive, + const glm::mat4& localTransform, bool& hasBounds ) { @@ -1194,6 +1239,9 @@ namespace Elixir return; } + if (!ApplyLocalTransform(vertices, localTransform)) + return; + SStaticMeshSection section; section.FirstIndex = (uint32_t)mesh.Indices.size(); section.IndexCount = (uint32_t)indices.size(); @@ -1229,6 +1277,27 @@ namespace Elixir mesh.Sections.push_back(std::move(section)); } + static void LoadNodeMesh( + SStaticMeshData& mesh, + const fastgltf::Asset& asset, + const fastgltf::Node& node, + const glm::mat4& localTransform, + bool& hasBounds + ) + { + 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(mesh, asset, primitive, localTransform, hasBounds); + } + std::optional GLTFStaticMeshLoader::Load( const GraphicsContext& context, std::filesystem::path path @@ -1277,10 +1346,45 @@ namespace Elixir LoadMaterials(mesh, context, asset, path.parent_path()); - // Combine every glTF mesh into this single static mesh. - for (const auto& assetMesh : asset.meshes) - for (const auto& primitive : assetMesh.primitives) - LoadPrimitive(mesh, asset, primitive, hasBounds); + 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 std::nullopt; + } + + fastgltf::iterateSceneNodes( + asset, + sceneIndex, + fastgltf::math::fmat4x4{}, + [&](const fastgltf::Node& node, const fastgltf::math::fmat4x4& transform) + { + LoadNodeMesh(mesh, asset, node, ToGlmMatrix(transform), hasBounds); + } + ); + } + else + { + if (asset.nodes.empty()) + { + for (const auto& assetMesh : asset.meshes) + for (const auto& primitive : assetMesh.primitives) + LoadPrimitive(mesh, asset, primitive, glm::mat4{ 1.0f }, hasBounds); + } + else + { + for (const auto& node : asset.nodes) + LoadNodeMesh( + mesh, + asset, + node, + ToGlmMatrix(fastgltf::getTransformMatrix(node)), + hasBounds + ); + } + } if (mesh.Sections.empty()) { diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h index 6029487e..e3aa8477 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.h @@ -23,6 +23,8 @@ namespace Elixir * 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. From 1dfed2f9aa7073311691439de44215c6e9804521 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Sun, 27 Sep 2026 00:35:05 -0300 Subject: [PATCH 29/61] Refactor glTF loading and frame material tables --- .../Engine/Materials/Rendering/Renderer.cpp | 60 +- .../Engine/Materials/Rendering/Renderer.h | 14 +- .../Platform/GLTF/GLTFStaticMeshLoader.cpp | 986 +++++++++--------- 3 files changed, 561 insertions(+), 499 deletions(-) diff --git a/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp b/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp index e99e8b9c..5ff55531 100644 --- a/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp +++ b/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp @@ -48,6 +48,31 @@ namespace Elixir::Materials::Rendering "Material rendering requires BeginFrame for the current graphics frame." ) + 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)); + + 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(); @@ -72,11 +97,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; } @@ -132,7 +155,10 @@ namespace Elixir::Materials::Rendering return UINT32_MAX; } - Renderer::SPreparedRenderScene Renderer::PrepareScene(const SPreparedScene& scene) + Renderer::SPreparedRenderScene Renderer::PrepareScene( + const SPreparedScene& scene, + FrameTable& materialTable + ) { SPreparedRenderScene prepared{ .Scene = scene.Scene, @@ -140,20 +166,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; @@ -164,15 +181,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; } @@ -181,9 +189,7 @@ namespace Elixir::Materials::Rendering const SPreparedRenderScene& scene ) { - SRenderResult result{ - .MaterialCount = scene.MaterialCount, - }; + SRenderResult result{}; if (!cmd || !scene.Scene) return result; diff --git a/Elixir/Source/Engine/Materials/Rendering/Renderer.h b/Elixir/Source/Engine/Materials/Rendering/Renderer.h index 44a27873..c21b64b2 100644 --- a/Elixir/Source/Engine/Materials/Rendering/Renderer.h +++ b/Elixir/Source/Engine/Materials/Rendering/Renderer.h @@ -70,7 +70,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. */ @@ -199,7 +199,7 @@ 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 materialCapacity Maximum unique materials supported by one frame. * @pre All arguments are valid for the renderer lifetime. */ Renderer( @@ -308,12 +308,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. */ @@ -340,8 +339,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( diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp index e84d2cb8..2652ec75 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp @@ -30,76 +30,105 @@ namespace Elixir { namespace { - SStaticMeshBounds 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 ExpandBounds(SStaticMeshBounds& target, const SStaticMeshBounds& source) - { - target.Min = glm::min(target.Min, source.Min); - target.Max = glm::max(target.Max, source.Max); - } + 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::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; - glm::mat4 ToGlmMatrix(const fastgltf::math::fmat4x4& source) + class MaterialLoader final { - 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]; + public: + MaterialLoader( + const GraphicsContext& context, + fastgltf::Asset& asset, + std::filesystem::path directory, + std::string_view meshName + ); - return result; - } + std::vector> Load(); - bool ApplyLocalTransform( - std::vector& vertices, - const glm::mat4& localTransform - ) - { - const glm::mat3 linearTransform{ localTransform }; - const float determinant = glm::determinant(linearTransform); - if (determinant == 0.0f) + private: + struct SMaterialTextures { - EE_CORE_WARN("glTF node has a singular local transform; skipping its primitive.") - return false; - } + Ref BaseColor; + Ref MetallicRoughness; + Ref Normal; + Ref Occlusion; + Ref Emissive; + Ref Specular; + Ref SpecularColor; + Ref ClearCoat; + Ref ClearCoatRoughness; + Ref ClearCoatNormal; + }; - const glm::mat3 normalTransform = glm::transpose(glm::inverse(linearTransform)); - const float handedness = determinant < 0.0f ? -1.0f : 1.0f; + Ref DecodeImage( + const std::byte* bytes, + size_t size, + const STexture2DCreateInfo& info + ) const; - for (auto& vertex : vertices) - { - vertex.Position = glm::vec3(localTransform * glm::vec4(vertex.Position, 1.0f)); - vertex.Normal = glm::normalize(normalTransform * vertex.Normal); + Ref LoadImage( + size_t imageIndex, + const STexture2DCreateInfo& info + ) const; - glm::vec3 tangent = linearTransform * glm::vec3(vertex.Tangent); - tangent -= vertex.Normal * glm::dot(vertex.Normal, tangent); - vertex.Tangent = { - glm::normalize(tangent), - vertex.Tangent.w * handedness - }; - } + Ref LoadTexture( + size_t textureIndex, + const STexture2DCreateInfo& info + ); - return true; - } + 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; + }; - Ref DecodeImage( + 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; @@ -128,42 +157,38 @@ namespace Elixir createInfo.Width = width; createInfo.Height = height; - const auto texture = Texture2D::Create(&context, createInfo); + const auto texture = Texture2D::Create(&m_Context, createInfo); stbi_image_free(pixels); return texture; } - Ref LoadImage( - const GraphicsContext& context, - fastgltf::Asset& asset, + Ref MaterialLoader::LoadImage( const size_t imageIndex, - const std::filesystem::path& dir, const STexture2DCreateInfo& info - ) + ) const { - if (imageIndex >= asset.images.size()) + if (imageIndex >= m_Asset.images.size()) { EE_CORE_WARN("glTF material references an invalid image index.") return nullptr; } Ref texture; - auto& image = asset.images[imageIndex]; + auto& image = m_Asset.images[imageIndex]; std::visit( fastgltf::visitor{ [](const auto&) {}, [&](const fastgltf::sources::URI& source) { - const auto path = dir / source.uri.fspath(); + const auto path = m_Directory / source.uri.fspath(); texture = TextureLoader::Load(path, info); }, [&](const fastgltf::sources::Array& source) { texture = DecodeImage( - context, source.bytes.data(), source.bytes.size(), info @@ -172,7 +197,6 @@ namespace Elixir [&](const fastgltf::sources::Vector& source) { texture = DecodeImage( - context, source.bytes.data(), source.bytes.size(), info @@ -180,27 +204,26 @@ namespace Elixir }, [&](const fastgltf::sources::BufferView& viewSource) { - if (viewSource.bufferViewIndex >= asset.bufferViews.size()) + if (viewSource.bufferViewIndex >= m_Asset.bufferViews.size()) { EE_CORE_WARN("glTF image references an invalid buffer view.") return; } - const auto& view = asset.bufferViews[viewSource.bufferViewIndex]; - if (view.bufferIndex >= asset.buffers.size()) + 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 = asset.buffers[view.bufferIndex]; + auto& buffer = m_Asset.buffers[view.bufferIndex]; std::visit( fastgltf::visitor{ [](const auto&) {}, [&](const fastgltf::sources::Array& source) { texture = DecodeImage( - context, source.bytes.data() + view.byteOffset, view.byteLength, info @@ -209,7 +232,6 @@ namespace Elixir [&](const fastgltf::sources::Vector& source) { texture = DecodeImage( - context, source.bytes.data() + view.byteOffset, view.byteLength, info @@ -226,13 +248,9 @@ namespace Elixir return texture; } - Ref LoadTexture( - const GraphicsContext& context, - fastgltf::Asset& asset, + Ref MaterialLoader::LoadTexture( const size_t textureIndex, - const std::filesystem::path& dir, - const STexture2DCreateInfo& info, - std::unordered_map>& cache + const STexture2DCreateInfo& info ) { const uint64_t cacheKey = @@ -240,19 +258,19 @@ namespace Elixir (info.Format == EImageFormat::R8G8B8A8_SRGB ? 1ull : 0ull) | (static_cast(info.MipmapMode) << 1); - const auto existing = cache.find(cacheKey); - if (existing != cache.end()) + const auto existing = m_TextureCache.find(cacheKey); + if (existing != m_TextureCache.end()) { return existing->second; } - if (textureIndex >= asset.textures.size()) + if (textureIndex >= m_Asset.textures.size()) { EE_CORE_WARN("glTF material references an invalid texture index.") return nullptr; } - const auto& texture = asset.textures[textureIndex]; + const auto& texture = m_Asset.textures[textureIndex]; if (!texture.imageIndex) { EE_CORE_WARN("glTF texture does not reference an image.") @@ -260,18 +278,15 @@ namespace Elixir } const auto result = LoadImage( - context, - asset, *texture.imageIndex, - dir, info ); - cache.emplace(cacheKey, result); + m_TextureCache.emplace(cacheKey, result); return result; } - EMaterialBlendMode GetBlendMode(const fastgltf::AlphaMode mode) + EMaterialBlendMode MaterialLoader::GetBlendMode(const fastgltf::AlphaMode mode) { using namespace Materials; @@ -289,7 +304,7 @@ namespace Elixir } } - uint32_t GetTextureCoordinateIndex(const fastgltf::TextureInfo& source) + uint32_t MaterialLoader::GetTextureCoordinateIndex(const fastgltf::TextureInfo& source) { const size_t index = source.transform && source.transform->texCoordIndex ? *source.transform->texCoordIndex @@ -305,16 +320,16 @@ namespace Elixir return 0; } - uint32_t AddTextureSample( + uint32_t MaterialLoader::AddTextureSample( MaterialGraph& graph, Material& material, const std::string_view parameterName, const fastgltf::TextureInfo& source, - const Nodes::ETextureSampleType sampleType = Nodes::ETextureSampleType::Color + const Nodes::ETextureSampleType sampleType ) { using namespace Materials; - using namespace Materials::Nodes; + using namespace Nodes; const auto sample = graph.AddNode( std::string(parameterName), @@ -419,27 +434,17 @@ namespace Elixir return sample; } - Ref CreateSurfaceMaterial( + Ref MaterialLoader::CreateSurfaceMaterial( const fastgltf::Material& source, const size_t materialIndex, - const std::string_view meshName, - const Ref& baseColorTexture, - const Ref& metallicRoughnessTexture, - const Ref& normalTexture, - const Ref& occlusionTexture, - const Ref& emissiveTexture, - const Ref& specularTexture, - const Ref& specularColorTexture, - const Ref& clearCoatTexture, - const Ref& clearCoatRoughnessTexture, - const Ref& clearCoatNormalTexture - ) + const SMaterialTextures& textures + ) const { using namespace Materials; - using namespace Materials::Nodes; + using namespace Nodes; const std::string name = source.name.empty() - ? std::string(meshName) + ".Material." + std::to_string(materialIndex) + ? std::string(m_MeshName) + ".Material." + std::to_string(materialIndex) : std::string(source.name); const auto material = CreateRef(name); @@ -564,11 +569,11 @@ namespace Elixir EMaterialValueType::Float4 ); - if (baseColorTexture) + if (textures.BaseColor) { EE_CORE_ASSERT(material->DefineParameter("BaseColorTexture", { .Kind = EMaterialParameterKind::Texture, - .DefaultValue = SMaterialParameter::MakeTexture(baseColorTexture), + .DefaultValue = SMaterialParameter::MakeTexture(textures.BaseColor), }), "Could not define BaseColorTexture."); const auto texture = AddTextureSample( @@ -593,11 +598,11 @@ namespace Elixir EMaterialValueType::Float ); - if (metallicRoughnessTexture) + if (textures.MetallicRoughness) { EE_CORE_ASSERT(material->DefineParameter("MetallicRoughnessTexture", { .Kind = EMaterialParameterKind::Texture, - .DefaultValue = SMaterialParameter::MakeTexture(metallicRoughnessTexture), + .DefaultValue = SMaterialParameter::MakeTexture(textures.MetallicRoughness), }), "Could not define MetallicRoughnessTexture."); const auto sample = AddTextureSample( @@ -630,11 +635,11 @@ namespace Elixir EMaterialValueType::Float3 ); - if (emissiveTexture) + if (textures.Emissive) { EE_CORE_ASSERT(material->DefineParameter("EmissiveTexture", { .Kind = EMaterialParameterKind::Texture, - .DefaultValue = SMaterialParameter::MakeTexture(emissiveTexture), + .DefaultValue = SMaterialParameter::MakeTexture(textures.Emissive), }), "Could not define EmissiveTexture."); const auto sample = AddTextureSample( @@ -658,11 +663,11 @@ namespace Elixir graph.SetChannel( EMaterialChannel::Opacity, opacity); graph.SetChannel(EMaterialChannel::Emissive, emissive); - if (normalTexture) + if (textures.Normal) { EE_CORE_ASSERT(material->DefineParameter("NormalTexture", { .Kind = EMaterialParameterKind::Texture, - .DefaultValue = SMaterialParameter::MakeTexture(normalTexture), + .DefaultValue = SMaterialParameter::MakeTexture(textures.Normal), }), "Could not define NormalTexture."); const auto sample = AddTextureSample( @@ -690,11 +695,11 @@ namespace Elixir ); } - if (occlusionTexture) + if (textures.Occlusion) { EE_CORE_ASSERT(material->DefineParameter("OcclusionTexture", { .Kind = EMaterialParameterKind::Texture, - .DefaultValue = SMaterialParameter::MakeTexture(occlusionTexture), + .DefaultValue = SMaterialParameter::MakeTexture(textures.Occlusion), }), "Could not define OcclusionTexture."); const auto sample = AddTextureSample( @@ -732,11 +737,11 @@ namespace Elixir EMaterialValueType::Float ); - if (specularTexture) + if (textures.Specular) { EE_CORE_ASSERT(material->DefineParameter("SpecularTexture", { .Kind = EMaterialParameterKind::Texture, - .DefaultValue = SMaterialParameter::MakeTexture(specularTexture), + .DefaultValue = SMaterialParameter::MakeTexture(textures.Specular), }), "Could not define SpecularTexture."); const auto sample = AddTextureSample( @@ -760,11 +765,11 @@ namespace Elixir EMaterialValueType::Float3 ); - if (specularColorTexture) + if (textures.SpecularColor) { EE_CORE_ASSERT(material->DefineParameter("SpecularColorTexture", { .Kind = EMaterialParameterKind::Texture, - .DefaultValue = SMaterialParameter::MakeTexture(specularColorTexture), + .DefaultValue = SMaterialParameter::MakeTexture(textures.SpecularColor), }), "Could not define SpecularColorTexture."); const auto sample = AddTextureSample( @@ -791,11 +796,11 @@ namespace Elixir EMaterialValueType::Float ); - if (clearCoatTexture) + if (textures.ClearCoat) { EE_CORE_ASSERT(material->DefineParameter("ClearCoatTexture", { .Kind = EMaterialParameterKind::Texture, - .DefaultValue = SMaterialParameter::MakeTexture(clearCoatTexture), + .DefaultValue = SMaterialParameter::MakeTexture(textures.ClearCoat), }), "Could not define ClearCoatTexture."); const auto sample = AddTextureSample( @@ -819,12 +824,12 @@ namespace Elixir EMaterialValueType::Float ); - if (clearCoatRoughnessTexture) + if (textures.ClearCoatRoughness) { EE_CORE_ASSERT(material->DefineParameter("ClearCoatRoughnessTexture", { .Kind = EMaterialParameterKind::Texture, .DefaultValue = SMaterialParameter::MakeTexture( - clearCoatRoughnessTexture + textures.ClearCoatRoughness ), }), "Could not define ClearCoatRoughnessTexture."); @@ -849,12 +854,12 @@ namespace Elixir // In glTF, clearCoatNormalTexture belongs to the external // clear-coat layer. - if (clearCoatNormalTexture) + if (textures.ClearCoatNormal) { EE_CORE_ASSERT(material->DefineParameter("ClearCoatNormalTexture", { .Kind = EMaterialParameterKind::Texture, .DefaultValue = SMaterialParameter::MakeTexture( - clearCoatNormalTexture + textures.ClearCoatNormal ), }), "Could not define ClearCoatNormalTexture."); @@ -883,44 +888,26 @@ namespace Elixir return material; } - void LoadMaterials( - SStaticMeshData& mesh, - const GraphicsContext& context, - fastgltf::Asset& asset, - const std::filesystem::path& dir - ) + std::vector> MaterialLoader::Load() { // Index zero is the fallback for primitives without a glTF material. - mesh.Materials.push_back(CreateDefaultMaterials()[0]); + std::vector> materials; + materials.push_back(CreateDefaultMaterials()[0]); - std::unordered_map> textureCache; - mesh.Materials.reserve(asset.materials.size() + 1); + materials.reserve(m_Asset.materials.size() + 1); - for (size_t i = 0; i < asset.materials.size(); ++i) + for (size_t i = 0; i < m_Asset.materials.size(); ++i) { - const auto& source = asset.materials[i]; - Ref baseColorTexture; - Ref metallicRoughnessTexture; - Ref normalTexture; - Ref occlusionTexture; - Ref emissiveTexture; - Ref specularTexture; - Ref specularColorTexture; - Ref clearCoatTexture; - Ref clearCoatRoughnessTexture; - Ref clearCoatNormalTexture; + const auto& source = m_Asset.materials[i]; + SMaterialTextures textures; if (source.pbrData.baseColorTexture) { STexture2DCreateInfo info; info.Format = EImageFormat::R8G8B8A8_SRGB; - baseColorTexture = LoadTexture( - context, - asset, + textures.BaseColor = LoadTexture( source.pbrData.baseColorTexture->textureIndex, - dir, - info, - textureCache + info ); } @@ -928,13 +915,9 @@ namespace Elixir { STexture2DCreateInfo info; info.Format = EImageFormat::R8G8B8A8_UNORM; - metallicRoughnessTexture = LoadTexture( - context, - asset, + textures.MetallicRoughness = LoadTexture( source.pbrData.metallicRoughnessTexture->textureIndex, - dir, - info, - textureCache + info ); } @@ -943,13 +926,9 @@ namespace Elixir STexture2DCreateInfo info; info.Format = EImageFormat::R8G8B8A8_UNORM; info.MipmapMode = EImageMipmapMode::NormalMap; - normalTexture = LoadTexture( - context, - asset, + textures.Normal = LoadTexture( source.normalTexture->textureIndex, - dir, - info, - textureCache + info ); } @@ -957,13 +936,9 @@ namespace Elixir { STexture2DCreateInfo info; info.Format = EImageFormat::R8G8B8A8_UNORM; - occlusionTexture = LoadTexture( - context, - asset, + textures.Occlusion = LoadTexture( source.occlusionTexture->textureIndex, - dir, - info, - textureCache + info ); } @@ -971,13 +946,9 @@ namespace Elixir { STexture2DCreateInfo info; info.Format = EImageFormat::R8G8B8A8_SRGB; - emissiveTexture = LoadTexture( - context, - asset, + textures.Emissive = LoadTexture( source.emissiveTexture->textureIndex, - dir, - info, - textureCache + info ); } @@ -987,13 +958,9 @@ namespace Elixir { STexture2DCreateInfo info; info.Format = EImageFormat::R8G8B8A8_UNORM; - specularTexture = LoadTexture( - context, - asset, + textures.Specular = LoadTexture( source.specular->specularTexture->textureIndex, - dir, - info, - textureCache + info ); } @@ -1001,13 +968,9 @@ namespace Elixir { STexture2DCreateInfo info; info.Format = EImageFormat::R8G8B8A8_SRGB; - specularColorTexture = LoadTexture( - context, - asset, + textures.SpecularColor = LoadTexture( source.specular->specularColorTexture->textureIndex, - dir, - info, - textureCache + info ); } } @@ -1018,13 +981,9 @@ namespace Elixir { STexture2DCreateInfo info; info.Format = EImageFormat::R8G8B8A8_UNORM; - clearCoatTexture = LoadTexture( - context, - asset, + textures.ClearCoat = LoadTexture( source.clearcoat->clearcoatTexture->textureIndex, - dir, - info, - textureCache + info ); } @@ -1032,13 +991,9 @@ namespace Elixir { STexture2DCreateInfo info; info.Format = EImageFormat::R8G8B8A8_UNORM; - clearCoatRoughnessTexture = LoadTexture( - context, - asset, + textures.ClearCoatRoughness = LoadTexture( source.clearcoat->clearcoatRoughnessTexture->textureIndex, - dir, - info, - textureCache + info ); } @@ -1047,351 +1002,450 @@ namespace Elixir STexture2DCreateInfo info; info.Format = EImageFormat::R8G8B8A8_UNORM; info.MipmapMode = EImageMipmapMode::NormalMap; - clearCoatNormalTexture = LoadTexture( - context, - asset, + textures.ClearCoatNormal = LoadTexture( source.clearcoat->clearcoatNormalTexture->textureIndex, - dir, - info, - textureCache + info ); } } - mesh.Materials.push_back(CreateSurfaceMaterial( - source, - i, - mesh.Name, - baseColorTexture, - metallicRoughnessTexture, - normalTexture, - occlusionTexture, - emissiveTexture, - specularTexture, - specularColorTexture, - clearCoatTexture, - clearCoatRoughnessTexture, - clearCoatNormalTexture - )); + materials.push_back(CreateSurfaceMaterial(source, i, textures)); } + + return materials; } - } - 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::Scenes; - - static void LoadPrimitive( - SStaticMeshData& mesh, - const fastgltf::Asset& asset, - const fastgltf::Primitive& primitive, - const glm::mat4& localTransform, - bool& hasBounds - ) - { - if (primitive.type != fastgltf::PrimitiveType::Triangles) + class Loader final { - EE_CORE_WARN( - "Skipping primitive in mesh '{}': only triangles are supported.", - mesh.Name - ) - return; - } + public: + Loader(const GraphicsContext& context, std::filesystem::path path); - const auto position = primitive.findAttribute("POSITION"); + std::optional Load(); - if (position == primitive.attributes.end() || - position->accessorIndex >= asset.accessors.size()) + 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, + const glm::mat4& localTransform + ); + + 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) { - EE_CORE_WARN("Skipping primitive in mesh '{}': POSITION is missing.", mesh.Name) - return; + 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; } - const auto& positionAccessor = asset.accessors[position->accessorIndex]; - if (positionAccessor.count > std::numeric_limits::max()) + void Loader::ExpandBounds( + SStaticMeshBounds& target, + const SStaticMeshBounds& source + ) { - EE_CORE_WARN( - "Skipping primitive in mesh '{}': it exceeds the vertex limit.", - mesh.Name - ) - return; + 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; } - std::vector vertices(positionAccessor.count); - fastgltf::iterateAccessorWithIndex( - asset, - positionAccessor, - [&vertices](const auto value, const size_t index) + bool Loader::ApplyLocalTransform( + std::vector& vertices, + const glm::mat4& localTransform + ) + { + const glm::mat3 linearTransform{ localTransform }; + const float determinant = glm::determinant(linearTransform); + if (determinant == 0.0f) { - vertices[index].Position = { value.x(), value.y(), value.z() }; + EE_CORE_WARN("glTF node has a singular local transform; skipping its primitive.") + return false; } - ); - const auto loadAttribute = [&]( - const std::string_view name, - auto&& assign + 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 = glm::normalize(normalTransform * vertex.Normal); + + glm::vec3 tangent = linearTransform * glm::vec3(vertex.Tangent); + tangent -= vertex.Normal * glm::dot(vertex.Normal, tangent); + vertex.Tangent = { + glm::normalize(tangent), + vertex.Tangent.w * handedness + }; + } + + return true; + } + + void Loader::LoadPrimitive( + const fastgltf::Primitive& primitive, + const glm::mat4& localTransform ) { - const auto attribute = primitive.findAttribute(name); + const auto& asset = *m_Asset; + auto& mesh = m_Mesh; - if (attribute == primitive.attributes.end() || - attribute->accessorIndex >= asset.accessors.size()) + 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& accessor = asset.accessors[attribute->accessorIndex]; - if (accessor.count != vertices.size()) + const auto& positionAccessor = asset.accessors[position->accessorIndex]; + if (positionAccessor.count > std::numeric_limits::max()) { EE_CORE_WARN( - "Ignoring {} in primitive of mesh '{}': its count differs from POSITION.", - name, + "Skipping primitive in mesh '{}': it exceeds the vertex limit.", mesh.Name ) return; } - fastgltf::iterateAccessorWithIndex( + std::vector vertices(positionAccessor.count); + fastgltf::iterateAccessorWithIndex( asset, - accessor, - [&assign](const auto value, const size_t index) + positionAccessor, + [&vertices](const auto value, const size_t index) { - assign(value, index); + vertices[index].Position = { value.x(), value.y(), value.z() }; } ); - }; - loadAttribute.operator()( - "NORMAL", - [&vertices](const auto value, const size_t index) + const auto loadAttribute = [&]( + const std::string_view name, + auto&& assign + ) { - vertices[index].Normal = { value.x(), value.y(), value.z() }; - } - ); + const auto attribute = primitive.findAttribute(name); - loadAttribute.operator()( - "TANGENT", - [&vertices](const auto value, const size_t index) - { - vertices[index].Tangent = { value.x(), value.y(), value.z(), value.w() }; - } - ); + if (attribute == primitive.attributes.end() || + attribute->accessorIndex >= asset.accessors.size()) + { + return; + } - loadAttribute.operator()( - "TEXCOORD_0", - [&vertices](const auto value, const size_t index) - { - vertices[index].TexCoord = { value.x(), value.y() }; - } - ); + 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; + } - loadAttribute.operator()( - "TEXCOORD_1", - [&vertices](const auto value, const size_t index) - { - vertices[index].TexCoord1 = { value.x(), value.y() }; - } - ); + fastgltf::iterateAccessorWithIndex( + asset, + accessor, + [&assign](const auto value, const size_t index) + { + assign(value, index); + } + ); + }; - std::vector indices; + loadAttribute.operator()( + "NORMAL", + [&vertices](const auto value, const size_t index) + { + vertices[index].Normal = { value.x(), value.y(), value.z() }; + } + ); - if (primitive.indicesAccessor && - *primitive.indicesAccessor < asset.accessors.size()) - { - const auto& accessor = asset.accessors[*primitive.indicesAccessor]; + loadAttribute.operator()( + "TANGENT", + [&vertices](const auto value, const size_t index) + { + vertices[index].Tangent = { value.x(), value.y(), value.z(), value.w() }; + } + ); - if (accessor.count > std::numeric_limits::max()) - { - EE_CORE_WARN( - "Skipping primitive in mesh '{}': it exceeds the index limit.", - mesh.Name - ) - return; - } + loadAttribute.operator()( + "TEXCOORD_0", + [&vertices](const auto value, const size_t index) + { + vertices[index].TexCoord = { value.x(), value.y() }; + } + ); - indices.resize(accessor.count); - fastgltf::iterateAccessorWithIndex( - asset, - accessor, - [&indices](const uint32_t value, const size_t index) + loadAttribute.operator()( + "TEXCOORD_1", + [&vertices](const auto value, const size_t index) { - indices[index] = value; + vertices[index].TexCoord1 = { value.x(), value.y() }; } ); - } - 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; - } + std::vector indices; - if (!ApplyLocalTransform(vertices, 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 (!hasBounds) - { - mesh.LocalBounds = section.LocalBounds; - hasBounds = true; - } - else - { - ExpandBounds(mesh.LocalBounds, section.LocalBounds); - } + if (primitive.indicesAccessor && + *primitive.indicesAccessor < asset.accessors.size()) + { + const auto& accessor = asset.accessors[*primitive.indicesAccessor]; - mesh.Sections.push_back(std::move(section)); - } + if (accessor.count > std::numeric_limits::max()) + { + EE_CORE_WARN( + "Skipping primitive in mesh '{}': it exceeds the index limit.", + mesh.Name + ) + return; + } - static void LoadNodeMesh( - SStaticMeshData& mesh, - const fastgltf::Asset& asset, - const fastgltf::Node& node, - const glm::mat4& localTransform, - bool& hasBounds - ) - { - if (!node.meshIndex) - 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 (*node.meshIndex >= asset.meshes.size()) - { - EE_CORE_WARN("glTF node references an invalid mesh index.") - return; - } + if (vertices.empty() || indices.empty()) + { + EE_CORE_WARN("Skipping empty primitive in mesh '{}'.", mesh.Name) + return; + } - for (const auto& primitive : asset.meshes[*node.meshIndex].primitives) - LoadPrimitive(mesh, asset, primitive, localTransform, hasBounds); - } + if (!ApplyLocalTransform(vertices, localTransform)) + return; - std::optional GLTFStaticMeshLoader::Load( - const GraphicsContext& context, - std::filesystem::path path - ) const - { - SStaticMeshData mesh; - mesh.Name = path.stem().string(); + 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()) + ); - auto data = fastgltf::GltfDataBuffer::FromPath(path); - if (data.error() != fastgltf::Error::None) - { - EE_CORE_ERROR( - "Failed to open glTF file '{}': {}.", - path.string(), - fastgltf::getErrorMessage(data.error()) - ) - return std::nullopt; - } + mesh.Indices.insert( + mesh.Indices.end(), + std::make_move_iterator(indices.begin()), + std::make_move_iterator(indices.end()) + ); - fastgltf::Parser parser{ - fastgltf::Extensions::KHR_materials_emissive_strength | - fastgltf::Extensions::KHR_materials_specular | - fastgltf::Extensions::KHR_texture_transform | - fastgltf::Extensions::KHR_materials_clearcoat - }; + if (!m_HasBounds) + { + mesh.LocalBounds = section.LocalBounds; + m_HasBounds = true; + } + else + { + ExpandBounds(mesh.LocalBounds, section.LocalBounds); + } - auto loadResult = parser.loadGltf( - data.get(), - path.parent_path(), - GLTF_OPTIONS, - GLTF_CATEGORIES - ); + mesh.Sections.push_back(std::move(section)); + } - if (loadResult.error() != fastgltf::Error::None) + void Loader::LoadNodeMesh( + const fastgltf::Node& node, + const glm::mat4& localTransform + ) { - EE_CORE_ERROR( - "Failed to parse glTF file '{}': {}.", - path.string(), - fastgltf::getErrorMessage(loadResult.error()) - ) - return std::nullopt; - } + const auto& asset = *m_Asset; - fastgltf::Asset& asset = loadResult.get(); - bool hasBounds = false; + if (!node.meshIndex) + return; - LoadMaterials(mesh, context, asset, path.parent_path()); + if (*node.meshIndex >= asset.meshes.size()) + { + EE_CORE_WARN("glTF node references an invalid mesh index.") + return; + } - if (!asset.scenes.empty()) + 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() { - const size_t sceneIndex = asset.defaultScene.value_or(0); - if (sceneIndex >= asset.scenes.size()) + auto data = fastgltf::GltfDataBuffer::FromPath(m_Path); + if (data.error() != fastgltf::Error::None) { - EE_CORE_ERROR("glTF default scene index is invalid.") - return std::nullopt; + EE_CORE_ERROR( + "Failed to open glTF file '{}': {}.", + m_Path.string(), + fastgltf::getErrorMessage(data.error()) + ) + return false; } - fastgltf::iterateSceneNodes( - asset, - sceneIndex, - fastgltf::math::fmat4x4{}, - [&](const fastgltf::Node& node, const fastgltf::math::fmat4x4& transform) - { - LoadNodeMesh(mesh, asset, node, ToGlmMatrix(transform), hasBounds); - } + 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; } - else + + bool Loader::LoadGeometry() { - if (asset.nodes.empty()) + const auto& asset = *m_Asset; + + if (!asset.scenes.empty()) { - for (const auto& assetMesh : asset.meshes) - for (const auto& primitive : assetMesh.primitives) - LoadPrimitive(mesh, asset, primitive, glm::mat4{ 1.0f }, hasBounds); + 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 { - for (const auto& node : asset.nodes) - LoadNodeMesh( - mesh, - asset, - node, - ToGlmMatrix(fastgltf::getTransformMatrix(node)), - hasBounds - ); + if (asset.nodes.empty()) + { + for (const auto& assetMesh : asset.meshes) + for (const auto& primitive : assetMesh.primitives) + LoadPrimitive(primitive, glm::mat4{ 1.0f }); + } + else + { + for (const auto& node : asset.nodes) + LoadNodeMesh( + node, + ToGlmMatrix(fastgltf::getTransformMatrix(node)) + ); + } } + + return true; } - if (mesh.Sections.empty()) + std::optional Loader::Load() { - EE_CORE_ERROR("No valid static meshes were found in '{}'.", path.string()) - return std::nullopt; + 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); } + } - return mesh; + std::optional GLTFStaticMeshLoader::Load( + const GraphicsContext& context, + std::filesystem::path path + ) const + { + return Loader(context, std::move(path)).Load(); } } From 213215e5a47af106f26f6dc5baaa981be5a3fb57 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Sun, 27 Sep 2026 02:50:37 -0300 Subject: [PATCH 30/61] Add HDR bloom post processing --- Dissolve/Source/Dissolve.cpp | 2 +- Elixir/Source/Engine/Aether/Manager.cpp | 9 +- Elixir/Source/Engine/Aether/Manager.h | 4 +- Elixir/Source/Engine/Core/Application.cpp | 38 +++- Elixir/Source/Engine/Core/Application.h | 11 ++ .../Engine/Graphics/GraphicsContext.cpp | 10 + .../Source/Engine/Graphics/GraphicsContext.h | 11 +- .../Source/Engine/Graphics/PostProcessor.cpp | 186 ++++++++++++++++++ Elixir/Source/Engine/Graphics/PostProcessor.h | 73 +++++++ Elixir/Source/Engine/Graphics/Shader/Shader.h | 10 +- .../Engine/Materials/MaterialSystem.cpp | 5 +- .../Source/Engine/Materials/MaterialSystem.h | 9 +- .../Engine/Materials/Rendering/Renderer.cpp | 14 +- .../Engine/Materials/Rendering/Renderer.h | 13 +- .../Graphics/Vulkan/VulkanGraphicsContext.cpp | 31 +-- .../Graphics/Vulkan/VulkanGraphicsContext.h | 2 +- .../Source/Graphics/Vulkan/VulkanShader.cpp | 39 +++- Elixir/Source/Graphics/Vulkan/VulkanShader.h | 5 + Shaders/Material/Material.ps.hlsl | 12 +- Shaders/PostProcessBloom.ps.hlsl | 62 ++++++ Shaders/PostProcessBloom.vs.hlsl | 18 ++ Shaders/PostProcessToneMap.ps.hlsl | 44 +++++ Shaders/PostProcessToneMap.vs.hlsl | 18 ++ 23 files changed, 580 insertions(+), 46 deletions(-) create mode 100644 Elixir/Source/Engine/Graphics/PostProcessor.cpp create mode 100644 Elixir/Source/Engine/Graphics/PostProcessor.h create mode 100644 Shaders/PostProcessBloom.ps.hlsl create mode 100644 Shaders/PostProcessBloom.vs.hlsl create mode 100644 Shaders/PostProcessToneMap.ps.hlsl create mode 100644 Shaders/PostProcessToneMap.vs.hlsl diff --git a/Dissolve/Source/Dissolve.cpp b/Dissolve/Source/Dissolve.cpp index ec56ac44..58add6d8 100644 --- a/Dissolve/Source/Dissolve.cpp +++ b/Dissolve/Source/Dissolve.cpp @@ -251,7 +251,7 @@ void Dissolve::Render(const Timestep frameTime) DrawGeometry(); m_StaticMeshRenderer->EndFrame(); - //aether.Render(m_CameraController->GetCamera()); + aether.Render(m_CameraController->GetCamera(), m_SceneTarget); } void Dissolve::OnEvent(Event& event) 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/Core/Application.cpp b/Elixir/Source/Engine/Core/Application.cpp index de939476..c4e1aff7 100644 --- a/Elixir/Source/Engine/Core/Application.cpp +++ b/Elixir/Source/Engine/Core/Application.cpp @@ -12,7 +12,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -36,8 +38,10 @@ 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 = CreateScope(m_GraphicsContext.get(), m_ShaderLoader.get()); TextureLoader::Initialize(m_GraphicsContext.get()); FontManager::Initialize(m_GraphicsContext.get()); @@ -204,8 +208,10 @@ namespace Elixir m_GraphicsContext->RenderFrame([this, frameTime]() { m_MaterialSystem->BeginFrame(); + m_GraphicsContext->Clear(m_SceneTarget); Render(frameTime); - m_MaterialSystem->RenderFrame(); + m_MaterialSystem->RenderFrame(m_SceneTarget); + m_PostProcessor->Apply(m_SceneTarget, m_GraphicsContext->GetRenderTarget()); m_GUIManager->Render(); }); @@ -221,6 +227,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); @@ -281,4 +288,33 @@ namespace Elixir return false; } + + bool Application::OnFramebufferResize(const FramebufferResizeEvent& event) + { + const auto& extent = event.GetExtent(); + if (extent.Width == 0 || extent.Height == 0 || !m_SceneTarget) + return false; + + m_SceneTarget->Resize( + m_GraphicsContext->GetUploadCommandBuffer(), + { 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..fec94765 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; + Scope m_PostProcessor; + Timer m_Timer; FrameProfiler m_Profiler; 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 cb6726eb..d620699f 100644 --- a/Elixir/Source/Engine/Graphics/GraphicsContext.h +++ b/Elixir/Source/Engine/Graphics/GraphicsContext.h @@ -33,7 +33,15 @@ namespace Elixir virtual void DrainRenderQueue() = 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; @@ -106,6 +114,7 @@ namespace Elixir } private: + virtual void ClearImage(const Ref& image) = 0; virtual void CreateRenderTargets() = 0; protected: diff --git a/Elixir/Source/Engine/Graphics/PostProcessor.cpp b/Elixir/Source/Engine/Graphics/PostProcessor.cpp new file mode 100644 index 00000000..8b527922 --- /dev/null +++ b/Elixir/Source/Engine/Graphics/PostProcessor.cpp @@ -0,0 +1,186 @@ +#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 + ) : m_Context(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_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->BindConstantBuffer("cbPostProcess", m_FrameBuffer); + m_BloomShader->BindSampler("postProcessSampler", m_Sampler); + m_ToneMapShader->BindConstantBuffer("cbPostProcess", m_FrameBuffer); + m_ToneMapShader->BindSampler("postProcessSampler", m_Sampler); + } + + 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(); + EnsureBloomTarget(extent); + + 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, + }; + m_FrameBuffer->UpdateData(&frameData, sizeof(frameData)); + + const SRenderingInfo bloomRenderingInfo{ + .ColorAttachment = m_BloomTarget, + .RenderArea = extent, + }; + + const auto bloomCommandBuffer = m_Context->GetSecondaryCommandBuffer(); + bloomCommandBuffer->Begin(bloomRenderingInfo); + + 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); + + m_ToneMapShader->BindImage("sceneTarget", sceneTarget); + m_ToneMapShader->BindImage("bloomTarget", m_BloomTarget); + + const SRenderingInfo toneMapRenderingInfo{ + .ColorAttachment = renderTarget, + .RenderArea = extent, + }; + + const auto toneMapCommandBuffer = m_Context->GetSecondaryCommandBuffer(); + toneMapCommandBuffer->Begin(toneMapRenderingInfo); + 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); + } + + void PostProcessor::EnsureBloomTarget(const Extent3D& extent) + { + if (m_BloomTarget && HasEqualExtent(m_BloomTarget->GetExtent(), extent)) + return; + + if (m_BloomTarget) + { + m_BloomTarget->Resize(m_Context->GetUploadCommandBuffer(), extent); + return; + } + + 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.") + } +} diff --git a/Elixir/Source/Engine/Graphics/PostProcessor.h b/Elixir/Source/Engine/Graphics/PostProcessor.h new file mode 100644 index 00000000..03e68a93 --- /dev/null +++ b/Elixir/Source/Engine/Graphics/PostProcessor.h @@ -0,0 +1,73 @@ +#pragma once + +#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. + * @pre context and shaderLoader remain valid for this object's lifetime. + */ + PostProcessor(const GraphicsContext* context, const ShaderLoader* shaderLoader); + + /** + * @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: + // Creates or resizes the HDR image that stores the blurred bloom contribution. + void EnsureBloomTarget(const Extent3D& extent); + + const GraphicsContext* m_Context = nullptr; + Ref m_BloomTarget; + Ref m_BloomShader; + Ref m_ToneMapShader; + Ref m_BloomPipeline; + Ref m_ToneMapPipeline; + Ref m_Sampler; + Ref m_FrameBuffer; + SPostProcessSettings m_Settings; + }; +} 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/Materials/MaterialSystem.cpp b/Elixir/Source/Engine/Materials/MaterialSystem.cpp index be86e744..17ca5774 100644 --- a/Elixir/Source/Engine/Materials/MaterialSystem.cpp +++ b/Elixir/Source/Engine/Materials/MaterialSystem.cpp @@ -28,9 +28,10 @@ namespace Elixir::Materials m_SubmittedScenes.push_back(std::move(scene)); } - SRenderResult MaterialSystem::RenderFrame() + SRenderResult MaterialSystem::RenderFrame(const Ref& sceneTarget) { EE_CORE_ASSERT(m_IsCollectingFrame, "Material rendering requires BeginFrame.") + EE_CORE_ASSERT(sceneTarget, "Material rendering requires a scene target.") std::vector scenes; scenes.reserve(m_SubmittedScenes.size()); @@ -39,7 +40,7 @@ namespace Elixir::Materials scenes.push_back(PrepareScene(scene)); m_IsCollectingFrame = false; - return m_Renderer->RenderFrame(scenes); + return m_Renderer->RenderFrame(scenes, sceneTarget); } SPreparedScene MaterialSystem::PrepareScene(const MaterialRenderScene& scene) diff --git a/Elixir/Source/Engine/Materials/MaterialSystem.h b/Elixir/Source/Engine/Materials/MaterialSystem.h index b35770da..64ed5d7a 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 { @@ -52,10 +56,11 @@ namespace Elixir::Materials /** * @brief Records all scenes submitted for the current graphics frame. + * @param sceneTarget Color target that receives the material scene. * @return Counts of prepared materials, batches, and draws. * @pre BeginFrame was called for the current graphics frame. */ - SRenderResult RenderFrame(); + SRenderResult RenderFrame(const Ref& sceneTarget); private: /** diff --git a/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp b/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp index 5ff55531..7dcabba3 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 @@ -38,11 +39,16 @@ namespace Elixir::Materials::Rendering m_Textures.BeginFrame(m_CurrentFrameNumber); } - SRenderResult Renderer::RenderFrame(std::span scenes) + SRenderResult Renderer::RenderFrame( + const std::span scenes, + const Ref& sceneTarget + ) { SRenderResult result{}; if (scenes.empty()) return result; + EE_CORE_ASSERT(sceneTarget, "Material rendering requires a scene target.") + EE_CORE_ASSERT( m_CurrentFrameNumber == m_Context->GetFrameNumber(), "Material rendering requires BeginFrame for the current graphics frame." @@ -74,10 +80,10 @@ namespace Elixir::Materials::Rendering result.MaterialCount = materialTable.GetCount(); const auto cmd = m_Context->GetSecondaryCommandBuffer(); - const auto extent = m_Context->GetRenderTarget()->GetExtent(); + const auto extent = sceneTarget->GetExtent(); const SRenderingInfo renderingInfo{ - .ColorAttachment = m_Context->GetRenderTarget(), + .ColorAttachment = sceneTarget, .DepthStencilAttachment = m_Context->GetDepthStencilRenderTarget(), .RenderArea = extent, }; @@ -443,7 +449,7 @@ namespace Elixir::Materials::Rendering builder.SetShader(shader); builder.SetInputTopology(EPrimitiveTopology::TriangleList); builder.SetPolygonMode(EPolygonMode::Fill); - builder.SetColorAttachmentFormat(EImageFormat::R8G8B8A8_SRGB); + builder.SetColorAttachmentFormat(EImageFormat::R16G16B16A16_SFLOAT); builder.SetDepthAttachmentFormat(EDepthStencilImageFormat::D32_SFLOAT); builder.SetBufferLayout(*request.VertexLayout); builder.DisableBlending(); diff --git a/Elixir/Source/Engine/Materials/Rendering/Renderer.h b/Elixir/Source/Engine/Materials/Rendering/Renderer.h index c21b64b2..1aedbe8f 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 @@ -213,9 +218,13 @@ namespace Elixir::Materials::Rendering /** * @brief Records all resolved material scenes for the current frame. * @param scenes Scenes with material proxies resolved by MaterialSystem. + * @param sceneTarget Color target that receives the recorded scene. * @return Counts of materials, batches, and draw commands recorded. */ - SRenderResult RenderFrame(std::span scenes); + SRenderResult RenderFrame( + std::span scenes, + const Ref& sceneTarget + ); /** * @brief Returns the program key for a material pass. diff --git a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp index b9c9356f..bab790da 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp +++ b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp @@ -187,21 +187,6 @@ namespace Elixir m_ClearColor = {{ color.r, color.g, color.b, color.a }}; } - void VulkanGraphicsContext::Clear() - { - EE_PROFILE_ZONE_SCOPED() - - const auto range = Initializers::ImageSubresourceRange(EImageAspect::Color); - vkCmdClearColorImage( - m_MainCommandBuffer->GetVulkanCommandBuffer(), - TryToGetVulkanImage(m_RenderTarget.get())->GetVulkanImage(), - VK_IMAGE_LAYOUT_GENERAL, - &m_ClearColor, - 1, - &range - ); - } - void VulkanGraphicsContext::Resize(const Extent2D extent) { EE_PROFILE_ZONE_SCOPED() @@ -485,6 +470,22 @@ namespace Elixir m_SwapchainRecreateRequested = false; } + void VulkanGraphicsContext::ClearImage(const Ref& image) + { + 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 + ); + } + void VulkanGraphicsContext::CreateRenderTargets() { const SImageCreateInfo colorInfo{ diff --git a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.h b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.h index 2662ec5e..847d4673 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.h +++ b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.h @@ -62,7 +62,6 @@ namespace Elixir::Vulkan void DrainRenderQueue() override; void SetClearColor(const glm::vec4& color) override; - void Clear() override; void Resize(Extent2D extent) override; @@ -103,6 +102,7 @@ namespace Elixir::Vulkan void DestroySwapchain(); void RecreateSwapchain(); + void ClearImage(const Ref& image) override; void CreateRenderTargets() override; void ResetFrameUsageState(); diff --git a/Elixir/Source/Graphics/Vulkan/VulkanShader.cpp b/Elixir/Source/Graphics/Vulkan/VulkanShader.cpp index 30d7d18a..1b74d2cb 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanShader.cpp +++ b/Elixir/Source/Graphics/Vulkan/VulkanShader.cpp @@ -157,6 +157,18 @@ namespace Elixir::Vulkan 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)) + { + if (m_DescriptorSets.Set(*binding, DescriptorValue{ image })) + m_Images[*binding] = 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)) @@ -414,7 +426,9 @@ namespace Elixir::Vulkan [this, &writes, binding = change.Key](const auto& value) { using Value = std::decay_t; - if constexpr (std::is_same_v>) + if constexpr (std::is_same_v>) + writes.push_back(GetWriteDescriptorSet(binding, value.get())); + else if constexpr (std::is_same_v>) writes.push_back(GetWriteDescriptorSet(binding, value.get())); else writes.push_back(GetWriteDescriptorSet(binding, value)); @@ -433,6 +447,29 @@ namespace Elixir::Vulkan }); } + 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 diff --git a/Elixir/Source/Graphics/Vulkan/VulkanShader.h b/Elixir/Source/Graphics/Vulkan/VulkanShader.h index e376ea90..86a67cee 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; @@ -48,6 +49,9 @@ namespace Elixir::Vulkan void ApplyPendingDescriptorState(); + VkWriteDescriptorSet GetWriteDescriptorSet( + SShaderBinding binding, const Image* image + ) const; VkWriteDescriptorSet GetWriteDescriptorSet( SShaderBinding binding, const Texture* texture ) const; @@ -69,6 +73,7 @@ namespace Elixir::Vulkan ) const; using DescriptorValue = std::variant< + Ref, Ref, Ref, Ref, diff --git a/Shaders/Material/Material.ps.hlsl b/Shaders/Material/Material.ps.hlsl index 91091bbe..e79d8c07 100644 --- a/Shaders/Material/Material.ps.hlsl +++ b/Shaders/Material/Material.ps.hlsl @@ -251,12 +251,6 @@ float2 EnvBRDFApprox(float roughness, float NdotV) return float2(-1.04f, 1.04f) * a004 + coefficients.zw; } -float3 ACESFilm(float3 x) -{ - 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)); -} - float4 main(PSInput input) : SV_Target0 { CompiledMaterial mat = materials[pc.MaterialIndex]; @@ -290,8 +284,7 @@ float4 main(PSInput input) : SV_Target0 } #if MATERIAL_SHADING_MODEL == MATERIAL_SHADING_MODEL_UNLIT - const float3 unlit = ACESFilm(surface.BaseColor + surface.Emissive); - return float4(unlit, surface.Opacity); + return float4(surface.BaseColor + surface.Emissive, surface.Opacity); #endif float roughness = clamp(surface.Roughness, 0.045f, 1.0f); @@ -412,8 +405,5 @@ float4 main(PSInput input) : SV_Target0 else if (DebugView == SURFACE_DEBUG_CLEAR_COAT) color = clearCoatContribution; - // Tone mapping - color = ACESFilm(color); - return float4(color, surface.Opacity); } diff --git a/Shaders/PostProcessBloom.ps.hlsl b/Shaders/PostProcessBloom.ps.hlsl new file mode 100644 index 00000000..0b948f8a --- /dev/null +++ b/Shaders/PostProcessBloom.ps.hlsl @@ -0,0 +1,62 @@ +[[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; +}; + +float3 ExtractBloom(const float3 color) +{ + const float brightness = max(color.r, max(color.g, color.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 color * 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..cd356a2e --- /dev/null +++ b/Shaders/PostProcessToneMap.ps.hlsl @@ -0,0 +1,44 @@ +[[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; +}; + +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 scene = sceneTarget.Sample(postProcessSampler, input.UV).rgb; + const float3 bloom = bloomTarget.Sample(postProcessSampler, input.UV).rgb; + 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; +} From 36b6ed8d904354ba2e098ea30f562bc27155418b Mon Sep 17 00:00:00 2001 From: MrChampz Date: Sun, 27 Sep 2026 18:13:35 -0300 Subject: [PATCH 31/61] Bind material renderer to scene target --- Elixir/Source/Engine/Core/Application.cpp | 3 ++- .../Source/Engine/Materials/MaterialSystem.cpp | 12 ++++++++---- Elixir/Source/Engine/Materials/MaterialSystem.h | 5 +++-- .../Engine/Materials/Rendering/Renderer.cpp | 16 ++++++++-------- .../Source/Engine/Materials/Rendering/Renderer.h | 9 ++++----- .../Tests/Engine/Graphics/FrameSlotStateTest.cpp | 2 +- 6 files changed, 26 insertions(+), 21 deletions(-) diff --git a/Elixir/Source/Engine/Core/Application.cpp b/Elixir/Source/Engine/Core/Application.cpp index c4e1aff7..f237b4a5 100644 --- a/Elixir/Source/Engine/Core/Application.cpp +++ b/Elixir/Source/Engine/Core/Application.cpp @@ -51,6 +51,7 @@ namespace Elixir m_MaterialSystem = CreateScope( m_GraphicsContext.get(), m_ShaderLoader.get(), + m_SceneTarget, SMaterialSystemConfig{ .InitialFrameCapacity = 256 } ); @@ -210,7 +211,7 @@ namespace Elixir m_MaterialSystem->BeginFrame(); m_GraphicsContext->Clear(m_SceneTarget); Render(frameTime); - m_MaterialSystem->RenderFrame(m_SceneTarget); + m_MaterialSystem->RenderFrame(); m_PostProcessor->Apply(m_SceneTarget, m_GraphicsContext->GetRenderTarget()); m_GUIManager->Render(); }); diff --git a/Elixir/Source/Engine/Materials/MaterialSystem.cpp b/Elixir/Source/Engine/Materials/MaterialSystem.cpp index 17ca5774..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() { @@ -28,10 +33,9 @@ namespace Elixir::Materials m_SubmittedScenes.push_back(std::move(scene)); } - SRenderResult MaterialSystem::RenderFrame(const Ref& sceneTarget) + SRenderResult MaterialSystem::RenderFrame() { EE_CORE_ASSERT(m_IsCollectingFrame, "Material rendering requires BeginFrame.") - EE_CORE_ASSERT(sceneTarget, "Material rendering requires a scene target.") std::vector scenes; scenes.reserve(m_SubmittedScenes.size()); @@ -40,7 +44,7 @@ namespace Elixir::Materials scenes.push_back(PrepareScene(scene)); m_IsCollectingFrame = false; - return m_Renderer->RenderFrame(scenes, sceneTarget); + return m_Renderer->RenderFrame(scenes); } SPreparedScene MaterialSystem::PrepareScene(const MaterialRenderScene& scene) diff --git a/Elixir/Source/Engine/Materials/MaterialSystem.h b/Elixir/Source/Engine/Materials/MaterialSystem.h index 64ed5d7a..1829cc59 100644 --- a/Elixir/Source/Engine/Materials/MaterialSystem.h +++ b/Elixir/Source/Engine/Materials/MaterialSystem.h @@ -34,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. @@ -41,6 +42,7 @@ namespace Elixir::Materials MaterialSystem( const GraphicsContext* context, const ShaderLoader* shaderLoader, + const Ref& sceneTarget, SMaterialSystemConfig config ); @@ -56,11 +58,10 @@ namespace Elixir::Materials /** * @brief Records all scenes submitted for the current graphics frame. - * @param sceneTarget Color target that receives the material scene. * @return Counts of prepared materials, batches, and draws. * @pre BeginFrame was called for the current graphics frame. */ - SRenderResult RenderFrame(const Ref& sceneTarget); + SRenderResult RenderFrame(); private: /** diff --git a/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp b/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp index 7dcabba3..f31ee8cf 100644 --- a/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp +++ b/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp @@ -10,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." @@ -39,15 +42,12 @@ namespace Elixir::Materials::Rendering m_Textures.BeginFrame(m_CurrentFrameNumber); } - SRenderResult Renderer::RenderFrame( - const std::span scenes, - const Ref& sceneTarget - ) + SRenderResult Renderer::RenderFrame(const std::span scenes) { SRenderResult result{}; if (scenes.empty()) return result; - EE_CORE_ASSERT(sceneTarget, "Material rendering requires a scene target.") + EE_CORE_ASSERT(m_SceneTarget, "Material rendering requires a scene target.") EE_CORE_ASSERT( m_CurrentFrameNumber == m_Context->GetFrameNumber(), @@ -80,10 +80,10 @@ namespace Elixir::Materials::Rendering result.MaterialCount = materialTable.GetCount(); const auto cmd = m_Context->GetSecondaryCommandBuffer(); - const auto extent = sceneTarget->GetExtent(); + const auto extent = m_SceneTarget->GetExtent(); const SRenderingInfo renderingInfo{ - .ColorAttachment = sceneTarget, + .ColorAttachment = m_SceneTarget, .DepthStencilAttachment = m_Context->GetDepthStencilRenderTarget(), .RenderArea = extent, }; @@ -449,7 +449,7 @@ namespace Elixir::Materials::Rendering builder.SetShader(shader); builder.SetInputTopology(EPrimitiveTopology::TriangleList); builder.SetPolygonMode(EPolygonMode::Fill); - builder.SetColorAttachmentFormat(EImageFormat::R16G16B16A16_SFLOAT); + builder.SetColorAttachmentFormat(m_SceneTarget->GetFormat()); builder.SetDepthAttachmentFormat(EDepthStencilImageFormat::D32_SFLOAT); builder.SetBufferLayout(*request.VertexLayout); builder.DisableBlending(); diff --git a/Elixir/Source/Engine/Materials/Rendering/Renderer.h b/Elixir/Source/Engine/Materials/Rendering/Renderer.h index 1aedbe8f..96e2e9bd 100644 --- a/Elixir/Source/Engine/Materials/Rendering/Renderer.h +++ b/Elixir/Source/Engine/Materials/Rendering/Renderer.h @@ -204,11 +204,13 @@ namespace Elixir::Materials::Rendering /** * @brief Creates a material renderer. * @param context Graphics context used to create pipelines. + * @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 ); @@ -218,13 +220,9 @@ namespace Elixir::Materials::Rendering /** * @brief Records all resolved material scenes for the current frame. * @param scenes Scenes with material proxies resolved by MaterialSystem. - * @param sceneTarget Color target that receives the recorded scene. * @return Counts of materials, batches, and draw commands recorded. */ - SRenderResult RenderFrame( - std::span scenes, - const Ref& sceneTarget - ); + SRenderResult RenderFrame(std::span scenes); /** * @brief Returns the program key for a material pass. @@ -384,6 +382,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/Tests/Engine/Graphics/FrameSlotStateTest.cpp b/Elixir/Tests/Engine/Graphics/FrameSlotStateTest.cpp index e383967b..ae1c3ed9 100644 --- a/Elixir/Tests/Engine/Graphics/FrameSlotStateTest.cpp +++ b/Elixir/Tests/Engine/Graphics/FrameSlotStateTest.cpp @@ -19,7 +19,6 @@ namespace Elixir void RenderFrame(std::function) override {} void DrainRenderQueue() 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 +26,7 @@ namespace Elixir Extent3D GetSwapchainExtent() const override { return {}; } private: + void ClearImage(const Ref&) override {} void CreateRenderTargets() override {} }; From a2a09a74f455502f8941f824ca3cdd4ecd671c6a Mon Sep 17 00:00:00 2001 From: MrChampz Date: Sun, 27 Sep 2026 18:17:57 -0300 Subject: [PATCH 32/61] Refine rendering setup --- Dissolve/Source/Dissolve.cpp | 6 +++--- .../Source/Engine/Materials/MaterialSystem.h | 4 +++- .../Engine/Materials/Rendering/Renderer.cpp | 9 +++++++-- Elixir/Source/Graphics/Vulkan/VulkanImage.cpp | 18 ++++++++++++++---- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/Dissolve/Source/Dissolve.cpp b/Dissolve/Source/Dissolve.cpp index 58add6d8..df69940d 100644 --- a/Dissolve/Source/Dissolve.cpp +++ b/Dissolve/Source/Dissolve.cpp @@ -129,7 +129,7 @@ Dissolve::Dissolve() EE_CORE_ERROR("Dissolve particle emitter 'FlameCore' was not found.") } - if (auto loaded = StaticMeshLoaderRegistry::Load("./Assets/Meshes/Porsche/scene.gltf")) + if (auto loaded = StaticMeshLoaderRegistry::Load("./Assets/Meshes/McLaren/scene.gltf")) mesh = std::move(*loaded); } @@ -242,8 +242,8 @@ void Dissolve::Render(const Timestep frameTime) Application::Render(frameTime); m_CameraController->Update(frameTime); - //auto& aether = GetAetherManager(); - //aether.BeginFrame(frameTime); + auto& aether = GetAetherManager(); + aether.BeginFrame(frameTime); m_GraphicsContext->Clear(); diff --git a/Elixir/Source/Engine/Materials/MaterialSystem.h b/Elixir/Source/Engine/Materials/MaterialSystem.h index 1829cc59..de566410 100644 --- a/Elixir/Source/Engine/Materials/MaterialSystem.h +++ b/Elixir/Source/Engine/Materials/MaterialSystem.h @@ -76,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/Rendering/Renderer.cpp b/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp index f31ee8cf..4e22d9bc 100644 --- a/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp +++ b/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp @@ -445,12 +445,17 @@ 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(m_SceneTarget->GetFormat()); - builder.SetDepthAttachmentFormat(EDepthStencilImageFormat::D32_SFLOAT); + builder.SetColorAttachmentFormat(colorFormat); + builder.SetDepthAttachmentFormat(depthFormat); builder.SetBufferLayout(*request.VertexLayout); builder.DisableBlending(); diff --git a/Elixir/Source/Graphics/Vulkan/VulkanImage.cpp b/Elixir/Source/Graphics/Vulkan/VulkanImage.cpp index bed858a1..ba193c54 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanImage.cpp +++ b/Elixir/Source/Graphics/Vulkan/VulkanImage.cpp @@ -68,12 +68,22 @@ namespace Elixir::Vulkan 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_Context->GetAllocator(), &imageInfo, &allocInfo, &m_Image, &m_Allocation, nullptr - )); + VK_CHECK_RESULT( + vmaCreateImage( + m_Context->GetAllocator(), + &imageInfo, &allocInfo, + &m_Image, + &m_Allocation, + nullptr + ) + ); if (!m_DebugName.empty()) - vmaSetAllocationName(m_Context->GetAllocator(), m_Allocation, m_DebugName.c_str()); + vmaSetAllocationName( + m_Context->GetAllocator(), + m_Allocation, + m_DebugName.c_str() + ); constexpr auto viewUsage = EImageUsage::Sampled | EImageUsage::Storage | From a07791a7725ff663711763b8ccc55f332a28c12f Mon Sep 17 00:00:00 2001 From: MrChampz Date: Tue, 29 Sep 2026 18:17:28 -0300 Subject: [PATCH 33/61] Serialize image resize operations on render thread --- Elixir/Source/Engine/Core/Application.cpp | 19 ++-- Elixir/Source/Engine/Core/Application.h | 3 + .../Source/Engine/Graphics/GraphicsContext.h | 9 ++ Elixir/Source/Engine/Graphics/Image.cpp | 81 +++++++++++++-- Elixir/Source/Engine/Graphics/Image.h | 35 +++++-- .../Source/Engine/Graphics/PostProcessor.cpp | 2 +- .../Graphics/Vulkan/VulkanGraphicsContext.cpp | 98 ++++++++++++++++--- .../Graphics/Vulkan/VulkanGraphicsContext.h | 5 +- .../Platform/GLTF/GLTFStaticMeshLoader.cpp | 1 + .../Engine/Graphics/FrameSlotStateTest.cpp | 3 + .../Tests/Graphics/Vulkan/VulkanImageTest.cpp | 2 +- 11 files changed, 217 insertions(+), 41 deletions(-) diff --git a/Elixir/Source/Engine/Core/Application.cpp b/Elixir/Source/Engine/Core/Application.cpp index f237b4a5..a492594c 100644 --- a/Elixir/Source/Engine/Core/Application.cpp +++ b/Elixir/Source/Engine/Core/Application.cpp @@ -292,13 +292,18 @@ namespace Elixir bool Application::OnFramebufferResize(const FramebufferResizeEvent& event) { - const auto& extent = event.GetExtent(); - if (extent.Width == 0 || extent.Height == 0 || !m_SceneTarget) - return false; - - m_SceneTarget->Resize( - m_GraphicsContext->GetUploadCommandBuffer(), - { extent.Width, extent.Height, 1 } + // 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, + extent = event.GetExtent() + ]() + { + context->WaitDeviceIdle(); + context->Resize(extent); + sceneTarget->Resize({ extent.Width, extent.Height, 1 }); + } ); return false; diff --git a/Elixir/Source/Engine/Core/Application.h b/Elixir/Source/Engine/Core/Application.h index fec94765..da9d42e7 100644 --- a/Elixir/Source/Engine/Core/Application.h +++ b/Elixir/Source/Engine/Core/Application.h @@ -1,5 +1,8 @@ #pragma once +#include +#include + #include #include #include diff --git a/Elixir/Source/Engine/Graphics/GraphicsContext.h b/Elixir/Source/Engine/Graphics/GraphicsContext.h index d620699f..557b507d 100644 --- a/Elixir/Source/Engine/Graphics/GraphicsContext.h +++ b/Elixir/Source/Engine/Graphics/GraphicsContext.h @@ -32,6 +32,15 @@ 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 Reports whether the caller is the rendering thread for this context. */ + virtual bool IsRenderThread() const = 0; + virtual void SetClearColor(const glm::vec4& color) = 0; /** @brief Clears the context-owned LDR render target with the configured clear color. */ diff --git a/Elixir/Source/Engine/Graphics/Image.cpp b/Elixir/Source/Engine/Graphics/Image.cpp index 3a4b4a37..b3308b70 100644 --- a/Elixir/Source/Engine/Graphics/Image.cpp +++ b/Elixir/Source/Engine/Graphics/Image.cpp @@ -528,7 +528,80 @@ namespace Elixir cmd->Flush(); } - void Image::Resize(const Ref& cmd, const Extent3D& extent) + void Image::Resize(const Extent3D& extent) + { + if (m_GraphicsContext->IsRenderThread()) + { + m_GraphicsContext->WaitDeviceIdle(); + 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.") + } + } + + bool Image::ResizeAndWait(const Extent3D& extent) + { + return m_GraphicsContext->RunRenderTaskAndWait( + [self = shared_from_this(), extent]() + { + self->m_GraphicsContext->WaitDeviceIdle(); + self->ResizeOnRenderThread( + self->m_GraphicsContext->GetUploadCommandBuffer(), + extent + ); + } + ); + } + + void Image::RecalculateSize() + { + m_BitsPerPixel = CalculateBitsPerPixel(this); + m_Size = GetMipSize(0) * m_ArrayLayers; + } + + void Image::ApplyPendingResize() + { + while (true) + { + std::optional extent; + { + std::scoped_lock lock(m_ResizeMutex); + extent.swap(m_PendingResize); + if (!extent) + { + m_ResizeQueued = false; + return; + } + } + + m_GraphicsContext->WaitDeviceIdle(); + ResizeOnRenderThread(m_GraphicsContext->GetUploadCommandBuffer(), *extent); + } + } + + void Image::ResizeOnRenderThread(const Ref& cmd, const Extent3D& extent) { if (!extent.Width || !extent.Height || !extent.Depth) { @@ -613,10 +686,4 @@ namespace Elixir cmd->Flush(); } } - - void Image::RecalculateSize() - { - m_BitsPerPixel = CalculateBitsPerPixel(this); - m_Size = GetMipSize(0) * m_ArrayLayers; - } } diff --git a/Elixir/Source/Engine/Graphics/Image.h b/Elixir/Source/Engine/Graphics/Image.h index 59dcef19..783968dd 100644 --- a/Elixir/Source/Engine/Graphics/Image.h +++ b/Elixir/Source/Engine/Graphics/Image.h @@ -3,12 +3,16 @@ #include #include +#include +#include #include namespace Elixir { class Image; + namespace Vulkan { class VulkanGraphicsContext; } + enum class EImageLayout { Undefined, @@ -258,7 +262,7 @@ namespace Elixir * @brief Owns a graphics image and coordinates initialization independently of its * backend. */ - class ELIXIR_API Image + class ELIXIR_API Image : public std::enable_shared_from_this { public: virtual ~Image() = default; @@ -270,16 +274,17 @@ namespace Elixir virtual void Destroy() = 0; /** - * @brief Resizes storage and scales each existing mip level, waiting for the copies - * to finish. - * - * Mip levels that do not fit the new extent are removed; no new levels are generated. - * - * @param cmd Command buffer used to record and synchronously submit the resize copies. + * @brief Queues an image resize and returns without waiting when called off the rendering thread. * @param extent New base-level dimensions in texels. - * @pre The image has TransferSrc and TransferDst usage, and cmd is not recording. */ - void Resize(const Ref& cmd, const Extent3D& extent); + void Resize(const Extent3D& extent); + + /** + * @brief Resizes the image and waits until the operation completes. + * @param extent New base-level dimensions in texels. + * @return True when the resize task was accepted by the graphics context. + */ + bool ResizeAndWait(const Extent3D& extent); /** * @brief Records a layout transition for all mip levels and layers. @@ -639,7 +644,13 @@ namespace Elixir const GraphicsContext* m_GraphicsContext; + std::mutex m_ResizeMutex; + std::optional m_PendingResize; + bool m_ResizeQueued = false; + private: + friend class Vulkan::VulkanGraphicsContext; + // Creates storage and completes the requested upload after backend construction. void Initialize(const SImageCreateInfo& info); @@ -657,6 +668,12 @@ namespace Elixir const Extent3D& dstExtent, uint32_t level ) = 0; + + // Applies queued resize requests on the rendering thread. + void ApplyPendingResize(); + + // Resizes storage and scales existing mip levels on the rendering thread. + void ResizeOnRenderThread(const Ref& cmd, const Extent3D& extent); }; } diff --git a/Elixir/Source/Engine/Graphics/PostProcessor.cpp b/Elixir/Source/Engine/Graphics/PostProcessor.cpp index 8b527922..ae72df9c 100644 --- a/Elixir/Source/Engine/Graphics/PostProcessor.cpp +++ b/Elixir/Source/Engine/Graphics/PostProcessor.cpp @@ -167,7 +167,7 @@ namespace Elixir if (m_BloomTarget) { - m_BloomTarget->Resize(m_Context->GetUploadCommandBuffer(), extent); + m_BloomTarget->Resize(extent); return; } diff --git a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp index bab790da..f9b3a13b 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp +++ b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp @@ -17,6 +17,7 @@ #include "GLFW/glfw3.h" #include +#include namespace Elixir { @@ -24,6 +25,29 @@ namespace Elixir using namespace Vulkan::Converters; using namespace SpirV; + namespace + { + 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; + }; + } + template void LogError(vkb::Result result, std::string preMessage = "") { @@ -130,12 +154,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 +169,7 @@ namespace Elixir m_FrameSemaphore.acquire(); - m_Executor->Enqueue(EThreadName::Rendering, [this, callback]() + if (!EnqueueRenderTask([this, callback]() { if (!Prepare()) { @@ -163,7 +184,10 @@ namespace Elixir Present(); m_FrameSemaphore.release(); - }); + })) + { + m_FrameSemaphore.release(); + } } void VulkanGraphicsContext::DrainRenderQueue() @@ -181,6 +205,57 @@ namespace Elixir ResetFrameUsageState(); } + bool VulkanGraphicsContext::EnqueueRenderTask(std::function task) const + { + if (!task || !m_AcceptingFrames.load()) + return false; + + m_Executor->Enqueue(EThreadName::Rendering, [this, task = std::move(task)]() + { + SRenderThreadScope scope(this); + task(); + }); + return true; + } + + bool VulkanGraphicsContext::RunRenderTaskAndWait(std::function task) const + { + if (!task) + return false; + + 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::IsRenderThread() const + { + return s_RenderThreadContext == this; + } + void VulkanGraphicsContext::SetClearColor(const glm::vec4& color) { EE_PROFILE_ZONE_SCOPED() @@ -196,8 +271,8 @@ namespace Elixir m_SwapchainRecreateRequested = true; const auto cmd = GetUploadCommandBuffer(); - m_RenderTarget->Resize(cmd, extent); - m_DepthStencilRenderTarget->Resize(cmd, extent); + m_RenderTarget->ResizeOnRenderThread(cmd, extent); + m_DepthStencilRenderTarget->ResizeOnRenderThread(cmd, extent); } Ref VulkanGraphicsContext::GetSecondaryCommandBuffer() const @@ -665,9 +740,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 847d4673..f7cec7d7 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.h +++ b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.h @@ -60,6 +60,9 @@ 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 IsRenderThread() const override; void SetClearColor(const glm::vec4& color) override; @@ -111,8 +114,6 @@ namespace Elixir::Vulkan void Submit(); void Present(); - bool HandleFramebufferResize(const FramebufferResizeEvent& event); - bool m_IsInitialized = false; #ifdef EE_DEBUG diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp index 2652ec75..5010e478 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp @@ -40,6 +40,7 @@ namespace Elixir fastgltf::Category::Images | fastgltf::Category::Textures | fastgltf::Category::Materials | + fastgltf::Category::Nodes | fastgltf::Category::Scenes; constexpr auto GLTF_EXTENSIONS = diff --git a/Elixir/Tests/Engine/Graphics/FrameSlotStateTest.cpp b/Elixir/Tests/Engine/Graphics/FrameSlotStateTest.cpp index ae1c3ed9..a12fbfa1 100644 --- a/Elixir/Tests/Engine/Graphics/FrameSlotStateTest.cpp +++ b/Elixir/Tests/Engine/Graphics/FrameSlotStateTest.cpp @@ -18,6 +18,9 @@ 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; } void SetClearColor(const glm::vec4&) override {} void Resize(Extent2D) override {} Ref GetSecondaryCommandBuffer() const override { return nullptr; } diff --git a/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp b/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp index 3386beae..5899986d 100644 --- a/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp +++ b/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp @@ -245,7 +245,7 @@ TEST_F(VulkanImageTest, ResizePreservesContentAndClampsTheAllocatedMipCount) .MipmapMode = EImageMipmapMode::SimpleAverage, .InitialLayout = EImageLayout::ShaderReadOnly, }); - image->Resize(Context->GetUploadCommandBuffer(), { 2, 2, 1 }); + ASSERT_TRUE(image->ResizeAndWait({ 2, 2, 1 })); EXPECT_EQ(image->GetWidth(), 2u); EXPECT_EQ(image->GetMipLevels(), 2u); EXPECT_EQ(image->GetSize(), 16u); From d99d6cca708d36ad0f5c827c98c85e5fd3a6086c Mon Sep 17 00:00:00 2001 From: MrChampz Date: Tue, 29 Sep 2026 19:27:48 -0300 Subject: [PATCH 34/61] Defer resized image destruction by frame lifetime --- Elixir/Source/Engine/Core/Application.cpp | 4 +- Elixir/Source/Engine/Graphics/Image.cpp | 3 - Elixir/Source/Engine/Graphics/Image.h | 4 - .../Graphics/Vulkan/VulkanGraphicsContext.cpp | 131 ++++++++++++++---- .../Graphics/Vulkan/VulkanGraphicsContext.h | 8 +- Elixir/Source/Graphics/Vulkan/VulkanImage.cpp | 13 +- 6 files changed, 119 insertions(+), 44 deletions(-) diff --git a/Elixir/Source/Engine/Core/Application.cpp b/Elixir/Source/Engine/Core/Application.cpp index a492594c..633af875 100644 --- a/Elixir/Source/Engine/Core/Application.cpp +++ b/Elixir/Source/Engine/Core/Application.cpp @@ -294,13 +294,13 @@ namespace Elixir { // 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([ + m_GraphicsContext->EnqueueRenderTask( + [ context = m_GraphicsContext.get(), sceneTarget = m_SceneTarget, extent = event.GetExtent() ]() { - context->WaitDeviceIdle(); context->Resize(extent); sceneTarget->Resize({ extent.Width, extent.Height, 1 }); } diff --git a/Elixir/Source/Engine/Graphics/Image.cpp b/Elixir/Source/Engine/Graphics/Image.cpp index b3308b70..093dcf88 100644 --- a/Elixir/Source/Engine/Graphics/Image.cpp +++ b/Elixir/Source/Engine/Graphics/Image.cpp @@ -532,7 +532,6 @@ namespace Elixir { if (m_GraphicsContext->IsRenderThread()) { - m_GraphicsContext->WaitDeviceIdle(); ResizeOnRenderThread(m_GraphicsContext->GetUploadCommandBuffer(), extent); return; } @@ -566,7 +565,6 @@ namespace Elixir return m_GraphicsContext->RunRenderTaskAndWait( [self = shared_from_this(), extent]() { - self->m_GraphicsContext->WaitDeviceIdle(); self->ResizeOnRenderThread( self->m_GraphicsContext->GetUploadCommandBuffer(), extent @@ -596,7 +594,6 @@ namespace Elixir } } - m_GraphicsContext->WaitDeviceIdle(); ResizeOnRenderThread(m_GraphicsContext->GetUploadCommandBuffer(), *extent); } } diff --git a/Elixir/Source/Engine/Graphics/Image.h b/Elixir/Source/Engine/Graphics/Image.h index 783968dd..e932f21f 100644 --- a/Elixir/Source/Engine/Graphics/Image.h +++ b/Elixir/Source/Engine/Graphics/Image.h @@ -11,8 +11,6 @@ namespace Elixir { class Image; - namespace Vulkan { class VulkanGraphicsContext; } - enum class EImageLayout { Undefined, @@ -649,8 +647,6 @@ namespace Elixir bool m_ResizeQueued = false; private: - friend class Vulkan::VulkanGraphicsContext; - // Creates storage and completes the requested upload after backend construction. void Initialize(const SImageCreateInfo& info); diff --git a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp index f9b3a13b..706a3ac7 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp +++ b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp @@ -46,41 +46,64 @@ namespace Elixir private: const VulkanGraphicsContext* m_PreviousContext; }; - } - template - void LogError(vkb::Result result, std::string preMessage = "") - { - if (!result) + struct SRetiredImage { - const auto message = preMessage + result.error().message(); - EE_CORE_FATAL(message) + 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!") @@ -202,6 +225,9 @@ namespace Elixir m_Executor->ShutdownRenderPool(); WaitDeviceIdle(); + for (auto& frame : m_Frames) + frame.DeletionQueue.Flush(); + ResetFrameUsageState(); } @@ -270,9 +296,8 @@ namespace Elixir m_SwapchainExtent = extent; m_SwapchainRecreateRequested = true; - const auto cmd = GetUploadCommandBuffer(); - m_RenderTarget->ResizeOnRenderThread(cmd, extent); - m_DepthStencilRenderTarget->ResizeOnRenderThread(cmd, extent); + m_RenderTarget->Resize(extent); + m_DepthStencilRenderTarget->Resize(extent); } Ref VulkanGraphicsContext::GetSecondaryCommandBuffer() const @@ -297,6 +322,60 @@ namespace Elixir VK_CHECK_RESULT(vkDeviceWaitIdle(m_Device)); } + 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() diff --git a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.h b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.h index f7cec7d7..bacd46ad 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.h +++ b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.h @@ -74,6 +74,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()]; } @@ -149,7 +155,7 @@ namespace Elixir::Vulkan Scope m_CommandPoolManager; Ref m_MainCommandBuffer; - 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 ba193c54..e0135027 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanImage.cpp +++ b/Elixir/Source/Graphics/Vulkan/VulkanImage.cpp @@ -30,16 +30,13 @@ namespace Elixir::Vulkan void VulkanImage::Destroy() { - if (m_ImageView) - vkDestroyImageView(m_Context->GetDevice(), m_ImageView, nullptr); + 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); - if (m_Image) - vmaDestroyImage(m_Context->GetAllocator(), m_Image, m_Allocation); - - m_Image = VK_NULL_HANDLE; - m_ImageView = VK_NULL_HANDLE; - m_Allocation = VK_NULL_HANDLE; m_DescriptorInfo = {}; + + m_Context->RetireImage(imageView, image, allocation); } void VulkanImage::CreateResource(const SImageCreateInfo& info) From e747acaf5f9e22002b037b691be1e5b351a5e235 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 30 Sep 2026 00:55:18 -0300 Subject: [PATCH 35/61] Fix post-process target synchronization --- Elixir/Source/Engine/Core/Application.cpp | 53 +++++++++++--- Elixir/Source/Engine/Core/Application.h | 7 ++ .../Source/Engine/GUI/Renderer/Renderer.cpp | 23 ++++-- Elixir/Source/Engine/GUI/Renderer/Renderer.h | 3 +- .../Source/Engine/Graphics/GraphicsContext.h | 11 +++ Elixir/Source/Engine/Graphics/Image.cpp | 35 ++++----- Elixir/Source/Engine/Graphics/Image.h | 20 +++--- .../Source/Engine/Graphics/PostProcessor.cpp | 71 +++++++++++-------- Elixir/Source/Engine/Graphics/PostProcessor.h | 16 +++-- .../Graphics/Vulkan/VulkanGraphicsContext.cpp | 25 +++++++ .../Graphics/Vulkan/VulkanGraphicsContext.h | 3 + Elixir/Source/Graphics/Vulkan/VulkanImage.cpp | 19 +++++ Elixir/Source/Graphics/Vulkan/VulkanImage.h | 7 ++ .../Source/Graphics/Vulkan/VulkanShader.cpp | 64 ++++++++++++++--- Elixir/Source/Graphics/Vulkan/VulkanShader.h | 21 +++++- .../Engine/Graphics/FrameSlotStateTest.cpp | 2 + .../Tests/Graphics/Vulkan/VulkanImageTest.cpp | 7 +- Shaders/PostProcessBloom.ps.hlsl | 18 ++++- Shaders/PostProcessToneMap.ps.hlsl | 23 +++++- 19 files changed, 329 insertions(+), 99 deletions(-) diff --git a/Elixir/Source/Engine/Core/Application.cpp b/Elixir/Source/Engine/Core/Application.cpp index 633af875..79284f19 100644 --- a/Elixir/Source/Engine/Core/Application.cpp +++ b/Elixir/Source/Engine/Core/Application.cpp @@ -41,7 +41,11 @@ namespace Elixir CreateSceneTarget(m_GraphicsContext->GetSwapchainExtent()); m_ShaderLoader = CreateScope(m_GraphicsContext.get()); - m_PostProcessor = CreateScope(m_GraphicsContext.get(), m_ShaderLoader.get()); + m_PostProcessor = CreateScope( + m_GraphicsContext.get(), + m_ShaderLoader.get(), + m_SceneTarget->GetExtent() + ); TextureLoader::Initialize(m_GraphicsContext.get()); FontManager::Initialize(m_GraphicsContext.get()); @@ -294,19 +298,46 @@ namespace Elixir { // 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, - extent = event.GetExtent() - ]() + { + std::scoped_lock lock(m_FramebufferResizeMutex); + m_PendingFramebufferExtent = event.GetExtent(); + if (m_FramebufferResizeQueued) + return false; + + m_FramebufferResizeQueued = true; + } + + if (!m_GraphicsContext->EnqueueRenderTask([this]() + { + ApplyPendingFramebufferResizes(); + })) + { + std::scoped_lock lock(m_FramebufferResizeMutex); + m_FramebufferResizeQueued = false; + } + + return false; + } + + void Application::ApplyPendingFramebufferResizes() + { + while (true) + { + std::optional extent; { - context->Resize(extent); - sceneTarget->Resize({ extent.Width, extent.Height, 1 }); + std::scoped_lock lock(m_FramebufferResizeMutex); + extent.swap(m_PendingFramebufferExtent); + if (!extent) + { + m_FramebufferResizeQueued = false; + return; + } } - ); - return false; + m_GraphicsContext->Resize(*extent); + m_SceneTarget->Resize({ extent->Width, extent->Height, 1 }); + m_PostProcessor->Resize({ extent->Width, extent->Height, 1 }); + } } void Application::CreateSceneTarget(const Extent3D& extent) diff --git a/Elixir/Source/Engine/Core/Application.h b/Elixir/Source/Engine/Core/Application.h index da9d42e7..0608a10c 100644 --- a/Elixir/Source/Engine/Core/Application.h +++ b/Elixir/Source/Engine/Core/Application.h @@ -96,6 +96,13 @@ namespace Elixir bool m_Minimized = false; private: + // Applies the newest framebuffer resize request on the rendering thread. + void ApplyPendingFramebufferResizes(); + + std::mutex m_FramebufferResizeMutex; + std::optional m_PendingFramebufferExtent; + bool m_FramebufferResizeQueued = false; + static Application* s_Application; }; 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.h b/Elixir/Source/Engine/Graphics/GraphicsContext.h index 557b507d..a9e63e76 100644 --- a/Elixir/Source/Engine/Graphics/GraphicsContext.h +++ b/Elixir/Source/Engine/Graphics/GraphicsContext.h @@ -41,6 +41,17 @@ namespace Elixir /** @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; /** @brief Clears the context-owned LDR render target with the configured clear color. */ diff --git a/Elixir/Source/Engine/Graphics/Image.cpp b/Elixir/Source/Engine/Graphics/Image.cpp index 093dcf88..82434a1b 100644 --- a/Elixir/Source/Engine/Graphics/Image.cpp +++ b/Elixir/Source/Engine/Graphics/Image.cpp @@ -360,8 +360,7 @@ namespace Elixir info.InitialLayout = EImageLayout::General; } - if (info.MipmapMode == EImageMipmapMode::SimpleAverage && info.MipLevels > 1) - info.Usage |= EImageUsage::TransferSrc; + info.Usage |= EImageUsage::TransferSrc | EImageUsage::TransferDst; if (!context) { @@ -530,7 +529,7 @@ namespace Elixir void Image::Resize(const Extent3D& extent) { - if (m_GraphicsContext->IsRenderThread()) + if (m_GraphicsContext->IsRenderThread() && !m_GraphicsContext->IsFrameRecording()) { ResizeOnRenderThread(m_GraphicsContext->GetUploadCommandBuffer(), extent); return; @@ -560,19 +559,6 @@ namespace Elixir } } - bool Image::ResizeAndWait(const Extent3D& extent) - { - return m_GraphicsContext->RunRenderTaskAndWait( - [self = shared_from_this(), extent]() - { - self->ResizeOnRenderThread( - self->m_GraphicsContext->GetUploadCommandBuffer(), - extent - ); - } - ); - } - void Image::RecalculateSize() { m_BitsPerPixel = CalculateBitsPerPixel(this); @@ -594,7 +580,10 @@ namespace Elixir } } - ResizeOnRenderThread(m_GraphicsContext->GetUploadCommandBuffer(), *extent); + ResizeOnRenderThread( + m_GraphicsContext->GetUploadCommandBuffer(), + *extent + ); } } @@ -624,17 +613,17 @@ namespace Elixir } auto info = GetCreateInfo(); - info.InitialLayout = EImageLayout::TransferDst; - - const auto staging = Create(m_GraphicsContext, info); - if (!staging) - return; - 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); diff --git a/Elixir/Source/Engine/Graphics/Image.h b/Elixir/Source/Engine/Graphics/Image.h index e932f21f..cefb72b0 100644 --- a/Elixir/Source/Engine/Graphics/Image.h +++ b/Elixir/Source/Engine/Graphics/Image.h @@ -237,7 +237,7 @@ namespace Elixir /** Number of array layers; volume images require one layer. */ uint32_t ArrayLayers = 1; - /** Requested uses; Image adds the transfer flags needed for initialization. */ + /** 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. */ @@ -272,18 +272,11 @@ namespace Elixir virtual void Destroy() = 0; /** - * @brief Queues an image resize and returns without waiting when called off the rendering thread. + * @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 Resizes the image and waits until the operation completes. - * @param extent New base-level dimensions in texels. - * @return True when the resize task was accepted by the graphics context. - */ - bool ResizeAndWait(const Extent3D& extent); - /** * @brief Records a layout transition for all mip levels and layers. * @param cmd Recording command buffer that receives the transition. @@ -298,6 +291,13 @@ namespace Elixir */ 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. * @@ -668,7 +668,7 @@ namespace Elixir // Applies queued resize requests on the rendering thread. void ApplyPendingResize(); - // Resizes storage and scales existing mip levels on the rendering thread. + // Recreates storage on the rendering thread. void ResizeOnRenderThread(const Ref& cmd, const Extent3D& extent); }; diff --git a/Elixir/Source/Engine/Graphics/PostProcessor.cpp b/Elixir/Source/Engine/Graphics/PostProcessor.cpp index ae72df9c..aeaa6f7f 100644 --- a/Elixir/Source/Engine/Graphics/PostProcessor.cpp +++ b/Elixir/Source/Engine/Graphics/PostProcessor.cpp @@ -47,7 +47,8 @@ namespace Elixir PostProcessor::PostProcessor( const GraphicsContext* context, - const ShaderLoader* shaderLoader + const ShaderLoader* shaderLoader, + const Extent3D& extent ) : m_Context(context) { EE_CORE_ASSERT(m_Context, "PostProcessor requires a graphics context.") @@ -82,6 +83,36 @@ namespace Elixir m_BloomShader->BindSampler("postProcessSampler", m_Sampler); m_ToneMapShader->BindConstantBuffer("cbPostProcess", m_FrameBuffer); 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( @@ -97,7 +128,10 @@ namespace Elixir ) const auto extent = sceneTarget->GetExtent(); - EnsureBloomTarget(extent); + EE_CORE_ASSERT( + m_BloomTarget && HasEqualExtent(m_BloomTarget->GetExtent(), extent), + "Post-process targets must be resized before Apply." + ) const SPostProcessFrameData frameData{ .InverseSceneSize = { @@ -120,6 +154,8 @@ namespace Elixir 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({{ @@ -135,9 +171,6 @@ namespace Elixir bloomCommandBuffer->End(); m_Context->EnqueueSecondaryCommandBuffer(bloomCommandBuffer); - m_ToneMapShader->BindImage("sceneTarget", sceneTarget); - m_ToneMapShader->BindImage("bloomTarget", m_BloomTarget); - const SRenderingInfo toneMapRenderingInfo{ .ColorAttachment = renderTarget, .RenderArea = extent, @@ -145,6 +178,11 @@ namespace Elixir 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), @@ -160,27 +198,4 @@ namespace Elixir m_Context->EnqueueSecondaryCommandBuffer(toneMapCommandBuffer); } - void PostProcessor::EnsureBloomTarget(const Extent3D& extent) - { - if (m_BloomTarget && HasEqualExtent(m_BloomTarget->GetExtent(), extent)) - return; - - if (m_BloomTarget) - { - m_BloomTarget->Resize(extent); - return; - } - - 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.") - } } diff --git a/Elixir/Source/Engine/Graphics/PostProcessor.h b/Elixir/Source/Engine/Graphics/PostProcessor.h index 03e68a93..0d4e9a57 100644 --- a/Elixir/Source/Engine/Graphics/PostProcessor.h +++ b/Elixir/Source/Engine/Graphics/PostProcessor.h @@ -38,9 +38,20 @@ namespace Elixir * @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); + 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. @@ -57,9 +68,6 @@ namespace Elixir const SPostProcessSettings& GetSettings() const { return m_Settings; } private: - // Creates or resizes the HDR image that stores the blurred bloom contribution. - void EnsureBloomTarget(const Extent3D& extent); - const GraphicsContext* m_Context = nullptr; Ref m_BloomTarget; Ref m_BloomShader; diff --git a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp index 706a3ac7..34a7779d 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp +++ b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp @@ -322,6 +322,29 @@ 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, @@ -734,6 +757,7 @@ namespace Elixir VK_CHECK_RESULT(result); m_MainCommandBuffer->Begin(); + m_IsFrameRecording = true; m_RenderTarget->Transition(m_MainCommandBuffer, EImageLayout::General); @@ -790,6 +814,7 @@ namespace Elixir swapchain.RenderSemaphore, frame.RenderFence ); + m_IsFrameRecording = false; m_CommandPoolManager->RecycleCommandBuffer(m_MainCommandBuffer); diff --git a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.h b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.h index bacd46ad..72ac04bd 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.h +++ b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.h @@ -63,6 +63,8 @@ namespace Elixir::Vulkan bool EnqueueRenderTask(std::function task) const override; bool RunRenderTaskAndWait(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; @@ -154,6 +156,7 @@ namespace Elixir::Vulkan Scope m_CommandPoolManager; Ref m_MainCommandBuffer; + bool m_IsFrameRecording = false; mutable std::vector m_Frames; VkClearColorValue m_ClearColor; diff --git a/Elixir/Source/Graphics/Vulkan/VulkanImage.cpp b/Elixir/Source/Graphics/Vulkan/VulkanImage.cpp index e0135027..884cd703 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanImage.cpp +++ b/Elixir/Source/Graphics/Vulkan/VulkanImage.cpp @@ -104,6 +104,7 @@ namespace Elixir::Vulkan m_DescriptorInfo = {}; m_DescriptorInfo.imageLayout = Converters::GetImageLayout(m_Layout); m_DescriptorInfo.imageView = m_ImageView; + ++m_ResourceGeneration; } void VulkanImage::Transition(const CommandBuffer* cmd, const EImageLayout layout) @@ -136,6 +137,24 @@ namespace Elixir::Vulkan m_DescriptorInfo.imageLayout = Converters::GetImageLayout(layout); } + void VulkanImage::Barrier(const CommandBuffer* cmd) + { + if (!IsValid()) + { + EE_CORE_ERROR("Cannot synchronize an image without native storage.") + return; + } + + const auto* vkCmd = static_cast(cmd); + CommandUtils::TransitionImage( + vkCmd->GetVulkanCommandBuffer(), + m_Image, + Converters::GetImageLayout(m_Layout), + Converters::GetImageLayout(m_Layout), + Converters::GetImageAspect(m_Aspect) + ); + } + void VulkanImage::CopyMip( const CommandBuffer* cmd, Image* dst, const Extent3D& srcExtent, const Extent3D& dstExtent, diff --git a/Elixir/Source/Graphics/Vulkan/VulkanImage.h b/Elixir/Source/Graphics/Vulkan/VulkanImage.h index 6dfb8145..c8f299f0 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanImage.h +++ b/Elixir/Source/Graphics/Vulkan/VulkanImage.h @@ -41,6 +41,9 @@ namespace Elixir::Vulkan */ 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; /** @brief Records a buffer upload; an empty region list copies the base level of all layers. @@ -64,6 +67,9 @@ namespace Elixir::Vulkan /** @brief Returns the view spanning every mip level and layer. */ VkImageView GetVulkanImageView() const { return m_ImageView; } + /** @brief Returns the generation of the current native image allocation. */ + uint64_t GetVulkanResourceGeneration() const { return m_ResourceGeneration; } + /** @brief Returns the current view and layout; samplers are bound separately. */ const VkDescriptorImageInfo& GetVulkanDescriptorInfo() const { return m_DescriptorInfo; } @@ -87,6 +93,7 @@ namespace Elixir::Vulkan VkImageView m_ImageView = VK_NULL_HANDLE; VkDescriptorImageInfo m_DescriptorInfo{}; VmaAllocation m_Allocation = VK_NULL_HANDLE; + uint64_t m_ResourceGeneration = 0; const VulkanGraphicsContext* m_Context; }; diff --git a/Elixir/Source/Graphics/Vulkan/VulkanShader.cpp b/Elixir/Source/Graphics/Vulkan/VulkanShader.cpp index 1b74d2cb..c743c624 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanShader.cpp +++ b/Elixir/Source/Graphics/Vulkan/VulkanShader.cpp @@ -13,6 +13,15 @@ namespace Elixir::Vulkan { + namespace + { + uint64_t GetResourceGeneration(const Ref& image) + { + const auto vkImage = TryToGetVulkanImage(image.get()); + return vkImage ? vkImage->GetVulkanResourceGeneration() : 0; + } + } + VulkanShader::VulkanShader(const GraphicsContext* context, SShaderCreateInfo&& info) : Shader(context, std::move(info)), m_DescriptorSets(*context) @@ -149,8 +158,14 @@ 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 + } } + ); return; } @@ -161,8 +176,14 @@ namespace Elixir::Vulkan { if (const auto binding = GetShaderBinding(name)) { - if (m_DescriptorSets.Set(*binding, DescriptorValue{ image })) - m_Images[*binding] = image; + m_Images[*binding] = image; + m_DescriptorSets.Set( + *binding, + DescriptorValue{ SImageDescriptorValue{ + image, + GetResourceGeneration(image) + } } + ); return; } @@ -415,6 +436,8 @@ namespace Elixir::Vulkan void VulkanShader::ApplyPendingDescriptorState() { + RefreshImageDescriptorState(); + m_DescriptorSets.ApplyPendingState([this](auto&, const auto changes) { std::vector writes; @@ -426,10 +449,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())); - else 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)); }, @@ -447,6 +470,31 @@ namespace Elixir::Vulkan }); } + void VulkanShader::RefreshImageDescriptorState() + { + for (const auto& [binding, image] : m_Images) + { + m_DescriptorSets.Set( + binding, + DescriptorValue{ SImageDescriptorValue{ + image, + GetResourceGeneration(image) + } } + ); + } + + for (const auto& [binding, texture] : m_Textures) + { + m_DescriptorSets.Set( + binding, + DescriptorValue{ STextureDescriptorValue{ + texture, + texture ? GetResourceGeneration(texture->GetImage()) : 0 + } } + ); + } + } + VkWriteDescriptorSet VulkanShader::GetWriteDescriptorSet( const SShaderBinding binding, const Image* image diff --git a/Elixir/Source/Graphics/Vulkan/VulkanShader.h b/Elixir/Source/Graphics/Vulkan/VulkanShader.h index 86a67cee..930703de 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanShader.h +++ b/Elixir/Source/Graphics/Vulkan/VulkanShader.h @@ -48,6 +48,7 @@ namespace Elixir::Vulkan void CreatePipelineLayout(); void ApplyPendingDescriptorState(); + void RefreshImageDescriptorState(); VkWriteDescriptorSet GetWriteDescriptorSet( SShaderBinding binding, const Image* image @@ -72,9 +73,25 @@ namespace Elixir::Vulkan const Ref& buffer ) const; + struct SImageDescriptorValue + { + Ref Image; + uint64_t ResourceGeneration = 0; + + bool operator==(const SImageDescriptorValue&) const = default; + }; + + struct STextureDescriptorValue + { + Ref Texture; + uint64_t ResourceGeneration = 0; + + bool operator==(const STextureDescriptorValue&) const = default; + }; + using DescriptorValue = std::variant< - Ref, - Ref, + SImageDescriptorValue, + STextureDescriptorValue, Ref, Ref, Ref, diff --git a/Elixir/Tests/Engine/Graphics/FrameSlotStateTest.cpp b/Elixir/Tests/Engine/Graphics/FrameSlotStateTest.cpp index a12fbfa1..1471397b 100644 --- a/Elixir/Tests/Engine/Graphics/FrameSlotStateTest.cpp +++ b/Elixir/Tests/Engine/Graphics/FrameSlotStateTest.cpp @@ -21,6 +21,8 @@ namespace Elixir 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 Resize(Extent2D) override {} Ref GetSecondaryCommandBuffer() const override { return nullptr; } diff --git a/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp b/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp index 5899986d..05ff9899 100644 --- a/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp +++ b/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp @@ -77,7 +77,10 @@ TEST_F(VulkanImageTest, CreatesAndDestroysAnImage) EXPECT_EQ(image->GetType(), EImageType::_1D); EXPECT_EQ(image->GetFormat(), EImageFormat::R8G8B8A8_SRGB); EXPECT_EQ(image->GetMipLevels(), 1u); - EXPECT_EQ(image->GetUsage(), EImageUsage::Sampled); + EXPECT_EQ( + image->GetUsage(), + EImageUsage::Sampled | EImageUsage::TransferSrc | EImageUsage::TransferDst + ); EXPECT_EQ(image->GetLayout(), EImageLayout::Undefined); image->Destroy(); EXPECT_FALSE(image->IsValid()); @@ -245,7 +248,7 @@ TEST_F(VulkanImageTest, ResizePreservesContentAndClampsTheAllocatedMipCount) .MipmapMode = EImageMipmapMode::SimpleAverage, .InitialLayout = EImageLayout::ShaderReadOnly, }); - ASSERT_TRUE(image->ResizeAndWait({ 2, 2, 1 })); + 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); diff --git a/Shaders/PostProcessBloom.ps.hlsl b/Shaders/PostProcessBloom.ps.hlsl index 0b948f8a..b2b031cf 100644 --- a/Shaders/PostProcessBloom.ps.hlsl +++ b/Shaders/PostProcessBloom.ps.hlsl @@ -22,14 +22,28 @@ struct PSInput 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 float brightness = max(color.r, max(color.g, color.b)); + 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 color * contribution / max(brightness, 1e-4f); + return sanitizedColor * contribution / max(brightness, 1e-4f); } float4 main(PSInput input) : SV_Target0 diff --git a/Shaders/PostProcessToneMap.ps.hlsl b/Shaders/PostProcessToneMap.ps.hlsl index cd356a2e..81562736 100644 --- a/Shaders/PostProcessToneMap.ps.hlsl +++ b/Shaders/PostProcessToneMap.ps.hlsl @@ -25,6 +25,15 @@ struct PSInput 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; @@ -37,8 +46,18 @@ float3 ACESFilm(const float3 color) float4 main(PSInput input) : SV_Target0 { - const float3 scene = sceneTarget.Sample(postProcessSampler, input.UV).rgb; - const float3 bloom = bloomTarget.Sample(postProcessSampler, input.UV).rgb; + 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); } From 81fa4b874712b8408bcc7d70723e1f5570dcfd76 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 30 Sep 2026 01:30:40 -0300 Subject: [PATCH 36/61] Fix graphics resource lifetime issues --- .../Source/Engine/Graphics/GraphicsContext.h | 17 +++++++ .../Source/Engine/Graphics/PostProcessor.cpp | 15 ++++-- Elixir/Source/Engine/Graphics/PostProcessor.h | 3 +- Elixir/Source/Engine/Graphics/Texture.cpp | 15 ++++-- Elixir/Source/Engine/Graphics/Texture.h | 3 ++ Elixir/Source/Engine/Mesh/GeometryPool.cpp | 20 +++++++- Elixir/Source/Engine/Mesh/GeometryPool.h | 12 +++-- .../Engine/Mesh/StaticMeshLoaderRegistry.cpp | 1 + .../Source/Engine/Mesh/StaticMeshRenderer.cpp | 13 +++-- .../Source/Engine/Mesh/StaticMeshRenderer.h | 3 +- .../Graphics/Vulkan/VulkanGraphicsContext.cpp | 51 +++++++++++++++++++ .../Graphics/Vulkan/VulkanGraphicsContext.h | 1 + Elixir/Tests/Engine/Graphics/ImageTest.cpp | 19 +++++++ 13 files changed, 153 insertions(+), 20 deletions(-) diff --git a/Elixir/Source/Engine/Graphics/GraphicsContext.h b/Elixir/Source/Engine/Graphics/GraphicsContext.h index a9e63e76..cebf7c15 100644 --- a/Elixir/Source/Engine/Graphics/GraphicsContext.h +++ b/Elixir/Source/Engine/Graphics/GraphicsContext.h @@ -38,6 +38,23 @@ namespace Elixir /** @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; diff --git a/Elixir/Source/Engine/Graphics/PostProcessor.cpp b/Elixir/Source/Engine/Graphics/PostProcessor.cpp index aeaa6f7f..bc53c32e 100644 --- a/Elixir/Source/Engine/Graphics/PostProcessor.cpp +++ b/Elixir/Source/Engine/Graphics/PostProcessor.cpp @@ -49,7 +49,8 @@ namespace Elixir const GraphicsContext* context, const ShaderLoader* shaderLoader, const Extent3D& extent - ) : m_Context(context) + ) : 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.") @@ -72,16 +73,17 @@ namespace Elixir ); constexpr SPostProcessFrameData frameData{}; - m_FrameBuffer = UniformBuffer::Create(m_Context, sizeof(frameData), &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->BindConstantBuffer("cbPostProcess", m_FrameBuffer); m_BloomShader->BindSampler("postProcessSampler", m_Sampler); - m_ToneMapShader->BindConstantBuffer("cbPostProcess", m_FrameBuffer); m_ToneMapShader->BindSampler("postProcessSampler", m_Sampler); EE_CORE_ASSERT( @@ -144,7 +146,10 @@ namespace Elixir .BloomIntensity = m_Settings.BloomIntensity, .Exposure = m_Settings.Exposure, }; - m_FrameBuffer->UpdateData(&frameData, sizeof(frameData)); + 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, diff --git a/Elixir/Source/Engine/Graphics/PostProcessor.h b/Elixir/Source/Engine/Graphics/PostProcessor.h index 0d4e9a57..e3372b3d 100644 --- a/Elixir/Source/Engine/Graphics/PostProcessor.h +++ b/Elixir/Source/Engine/Graphics/PostProcessor.h @@ -1,5 +1,6 @@ #pragma once +#include #include namespace Elixir @@ -75,7 +76,7 @@ namespace Elixir Ref m_BloomPipeline; Ref m_ToneMapPipeline; Ref m_Sampler; - Ref m_FrameBuffer; + FrameSlotState> m_FrameBuffers; SPostProcessSettings m_Settings; }; } diff --git a/Elixir/Source/Engine/Graphics/Texture.cpp b/Elixir/Source/Engine/Graphics/Texture.cpp index e091aa63..c3b407a9 100644 --- a/Elixir/Source/Engine/Graphics/Texture.cpp +++ b/Elixir/Source/Engine/Graphics/Texture.cpp @@ -10,7 +10,10 @@ namespace Elixir Ref Texture::Create(const GraphicsContext* context, const STextureCreateInfo& info) { - return Ref(new Texture(Image::Create(context, CreateImageInfo(info)), info)); + const auto image = Image::Create(context, CreateImageInfo(info)); + if (!image) return nullptr; + + return Ref(new Texture(image, info)); } Ref Texture::Create( @@ -61,7 +64,10 @@ namespace Elixir Ref Texture2D::Create(const GraphicsContext* context, const STexture2DCreateInfo& info) { - return Ref(new Texture2D(Image::Create(context, CreateImageInfo(info)), info)); + const auto image = Image::Create(context, CreateImageInfo(info)); + if (!image) return nullptr; + + return Ref(new Texture2D(image, info)); } Ref Texture2D::Create( @@ -105,7 +111,10 @@ namespace Elixir Ref Texture3D::Create(const GraphicsContext* context, const STexture3DCreateInfo& info) { - return Ref(new Texture3D(Image::Create(context, CreateImageInfo(info)), info)); + const auto image = Image::Create(context, CreateImageInfo(info)); + if (!image) return nullptr; + + return Ref(new Texture3D(image, info)); } Ref Texture3D::Create( diff --git a/Elixir/Source/Engine/Graphics/Texture.h b/Elixir/Source/Engine/Graphics/Texture.h index a25833f9..80baca27 100644 --- a/Elixir/Source/Engine/Graphics/Texture.h +++ b/Elixir/Source/Engine/Graphics/Texture.h @@ -105,6 +105,7 @@ namespace Elixir * 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, @@ -167,6 +168,7 @@ namespace Elixir * @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, @@ -225,6 +227,7 @@ namespace Elixir * @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, diff --git a/Elixir/Source/Engine/Mesh/GeometryPool.cpp b/Elixir/Source/Engine/Mesh/GeometryPool.cpp index bca6faab..f4173ea7 100644 --- a/Elixir/Source/Engine/Mesh/GeometryPool.cpp +++ b/Elixir/Source/Engine/Mesh/GeometryPool.cpp @@ -9,7 +9,8 @@ namespace Elixir const GraphicsContext& context, const SGeometryPoolConfig config ) : m_VertexCapacity(config.VertexCapacity), - m_IndexCapacity(config.IndexCapacity) + m_IndexCapacity(config.IndexCapacity), + m_GraphicsContext(context) { EE_CORE_ASSERT(m_VertexCapacity > 0, "GeometryPool vertex capacity must be greater than 0.") EE_CORE_ASSERT(m_IndexCapacity > 0, "GeometryPool index capacity must be greater than 0.") @@ -136,6 +137,21 @@ namespace Elixir } void GeometryPool::Free(const SHandle handle) + { + if (!handle.IsValid() || handle.Index >= m_Slots.size()) + return; + + const SGeometryPoolSlot& slot = m_Slots[handle.Index]; + if (!slot.Allocated || slot.Generation != handle.Generation) + return; + + m_GraphicsContext.DeferResourceRelease([this, handle]() + { + FreeCompleted(handle); + }); + } + + void GeometryPool::FreeCompleted(const SHandle handle) { if (!handle.IsValid() || handle.Index >= m_Slots.size()) return; @@ -240,4 +256,4 @@ namespace Elixir freeRanges.erase(next); } } -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/Mesh/GeometryPool.h b/Elixir/Source/Engine/Mesh/GeometryPool.h index 64d0db49..cd33740e 100644 --- a/Elixir/Source/Engine/Mesh/GeometryPool.h +++ b/Elixir/Source/Engine/Mesh/GeometryPool.h @@ -1,13 +1,12 @@ #pragma once #include +#include #include #include namespace Elixir { - class GraphicsContext; - /** @brief Identifies a contiguous geometry range in GeometryPool buffers. */ struct SGeometry { @@ -27,7 +26,7 @@ namespace Elixir /** * @brief Owns shared GPU buffers used by static meshes. * - * Geometry remains valid until Free receives its matching handle. + * Geometry remains valid until all submitted draws using it complete. */ class ELIXIR_API GeometryPool final { @@ -74,9 +73,12 @@ namespace Elixir bool Allocated = false; }; - /** @brief Release a geometry range when its handle is valid. */ + /** @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. */ + void FreeCompleted(SHandle handle); + /** @brief Check whether this allocation still identifies live pool geometry. */ bool IsValid(const SHandle& handle) const; @@ -111,5 +113,7 @@ namespace Elixir Ref m_IndexBuffer; std::vector m_Geometries; std::vector m_Generations; + + const GraphicsContext& m_GraphicsContext; }; } diff --git a/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp b/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp index 8fef7c1e..22ad664b 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp +++ b/Elixir/Source/Engine/Mesh/StaticMeshLoaderRegistry.cpp @@ -32,6 +32,7 @@ namespace Elixir void StaticMeshLoaderRegistry::Shutdown() { s_Loader.reset(); + s_GeometryPool.reset(); s_GraphicsContext = nullptr; } diff --git a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp index 06178dbd..5583a263 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp +++ b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.cpp @@ -42,12 +42,16 @@ namespace Elixir const GeometryPool& geometryPool ) : m_MaterialSystem(materialSystem), m_GeometryPool(geometryPool), - m_Context(context) + m_Context(context), + m_FrameBuffers(*context) { EE_CORE_ASSERT(m_Context, "StaticMeshRenderer requires a graphics context") constexpr SSurfaceFrameData frameData; - m_FrameBuffer = UniformBuffer::Create(m_Context, sizeof(frameData), &frameData); + m_FrameBuffers.ForEach([this, &frameData](Ref& frameBuffer) + { + frameBuffer = UniformBuffer::Create(m_Context, sizeof(frameData), &frameData); + }); } void StaticMeshRenderer::BeginFrame( @@ -77,7 +81,8 @@ namespace Elixir lighting.DirectionalLightIntensity ), }; - m_FrameBuffer->UpdateData(&frameData, sizeof(frameData)); + const auto& frameBuffer = m_FrameBuffers.GetCurrent(); + frameBuffer->UpdateData(&frameData, sizeof(frameData)); m_Scene = {}; m_GeometryIndex = m_Scene.AddGeometry({ @@ -87,7 +92,7 @@ namespace Elixir }, .ConstantBuffers = {{ .Name = "cbFrame", - .Buffer = m_FrameBuffer, + .Buffer = frameBuffer, }}, .Textures = { { diff --git a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h index 9bf7932c..497dceb3 100644 --- a/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h +++ b/Elixir/Source/Engine/Mesh/StaticMeshRenderer.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -139,7 +140,6 @@ namespace Elixir Ref GetDefaultInstance(const Ref& material); MaterialSystem& m_MaterialSystem; - Ref m_FrameBuffer; const GeometryPool& m_GeometryPool; Rendering::MaterialRenderScene m_Scene; @@ -148,5 +148,6 @@ namespace Elixir glm::mat4 m_View{ 1.0f }; const GraphicsContext* m_Context = nullptr; + FrameSlotState> m_FrameBuffers; }; } diff --git a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp index 34a7779d..b9f3811d 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp +++ b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp @@ -27,6 +27,15 @@ namespace Elixir namespace { + struct SDeferredResourceRelease + { + 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 @@ -277,6 +286,48 @@ namespace Elixir 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; diff --git a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.h b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.h index 72ac04bd..718e8fe4 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.h +++ b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.h @@ -62,6 +62,7 @@ namespace Elixir::Vulkan 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; diff --git a/Elixir/Tests/Engine/Graphics/ImageTest.cpp b/Elixir/Tests/Engine/Graphics/ImageTest.cpp index 23cfdef4..842d39dc 100644 --- a/Elixir/Tests/Engine/Graphics/ImageTest.cpp +++ b/Elixir/Tests/Engine/Graphics/ImageTest.cpp @@ -13,6 +13,7 @@ namespace 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; } @@ -56,6 +57,24 @@ TEST(ImageTest, TextureMappingDoesNotResolveMipmapsOrUploadUsage) 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); From 434bcf89ee57a740668174ab67f964b042354838 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 30 Sep 2026 01:47:22 -0300 Subject: [PATCH 37/61] Fix glTF material and geometry import --- .../Source/Engine/Graphics/TextureLoader.cpp | 19 +- .../Engine/Materials/Nodes/ScaleNormal.h | 45 ++++ .../Platform/GLTF/GLTFStaticMeshLoader.cpp | 198 ++++++++++++++++-- .../Engine/Materials/MaterialGraphTest.cpp | 28 +++ 4 files changed, 269 insertions(+), 21 deletions(-) create mode 100644 Elixir/Source/Engine/Materials/Nodes/ScaleNormal.h diff --git a/Elixir/Source/Engine/Graphics/TextureLoader.cpp b/Elixir/Source/Engine/Graphics/TextureLoader.cpp index 3be42d8e..bfd041a2 100644 --- a/Elixir/Source/Engine/Graphics/TextureLoader.cpp +++ b/Elixir/Source/Engine/Graphics/TextureLoader.cpp @@ -51,9 +51,11 @@ namespace Elixir { 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(); @@ -81,7 +83,15 @@ 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; @@ -93,7 +103,8 @@ namespace Elixir TraceTextureInfo(pathStr, isHdr, createInfo.Format, channels); auto texture = Texture2D::Create(s_Context, createInfo); - EE_CORE_TRACE("Loaded texture: {0} [{1}].", pathStr, texture->GetUUID()) + if (texture) + EE_CORE_TRACE("Loaded texture: {0} [{1}].", pathStr, texture->GetUUID()) stbi_image_free(data); 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/Platform/GLTF/GLTFStaticMeshLoader.cpp b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp index 5010e478..04676694 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp @@ -12,10 +12,10 @@ #include #include #include -#include #include #include #include +#include #include #include #include @@ -49,6 +49,20 @@ namespace Elixir 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: @@ -184,7 +198,14 @@ namespace Elixir [](const auto&) {}, [&](const fastgltf::sources::URI& source) { - const auto path = m_Directory / source.uri.fspath(); + 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) @@ -684,15 +705,15 @@ namespace Elixir EMaterialValueType::Float ); - const auto flatten = graph.AddNode(); - graph.Connect(sample, flatten, 0); - graph.Connect(scale, flatten, 1); + const auto scaledNormal = graph.AddNode(); + graph.Connect(sample, scaledNormal, 0); + graph.Connect(scale, scaledNormal, 1); graph.SetChannel( source.clearcoat ? EMaterialChannel::ClearCoatBottomNormal : EMaterialChannel::Normal, - flatten + scaledNormal ); } @@ -877,11 +898,11 @@ namespace Elixir EMaterialValueType::Float ); - const auto flatten = graph.AddNode(); - graph.Connect(sample, flatten, 0); - graph.Connect(scale, flatten, 1); + const auto scaledNormal = graph.AddNode(); + graph.Connect(sample, scaledNormal, 0); + graph.Connect(scale, scaledNormal, 1); - graph.SetChannel(EMaterialChannel::Normal, flatten); + graph.SetChannel(EMaterialChannel::Normal, scaledNormal); } } @@ -1045,8 +1066,13 @@ namespace Elixir 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; @@ -1090,8 +1116,86 @@ namespace Elixir 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 ) { @@ -1109,16 +1213,25 @@ namespace Elixir for (auto& vertex : vertices) { vertex.Position = glm::vec3(localTransform * glm::vec4(vertex.Position, 1.0f)); - vertex.Normal = glm::normalize(normalTransform * vertex.Normal); + 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 = { - glm::normalize(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; } @@ -1272,7 +1385,19 @@ namespace Elixir return; } - if (!ApplyLocalTransform(vertices, localTransform)) + 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; @@ -1404,11 +1529,50 @@ namespace Elixir } else { + std::vector childNodes(asset.nodes.size()); for (const auto& node : asset.nodes) - LoadNodeMesh( - node, - ToGlmMatrix(fastgltf::getTransformMatrix(node)) - ); + { + 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)); + } } } diff --git a/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp b/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp index 8e65236e..ef24a5c1 100644 --- a/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp +++ b/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -222,6 +223,33 @@ TEST(MaterialGraphTest, GeneratesNormalTextureSamplingAndFlattening) 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; From 2c9c5ce58f5a61ba52c5900aac2b45279baaa966 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 30 Sep 2026 01:51:26 -0300 Subject: [PATCH 38/61] Release Dissolve resources before context teardown --- Dissolve/Source/Dissolve.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dissolve/Source/Dissolve.cpp b/Dissolve/Source/Dissolve.cpp index df69940d..42eae81d 100644 --- a/Dissolve/Source/Dissolve.cpp +++ b/Dissolve/Source/Dissolve.cpp @@ -222,6 +222,11 @@ Dissolve::Dissolve() Dissolve::~Dissolve() { m_StaticMeshRenderer.reset(); + mesh.reset(); + s_LightingSetup = {}; + graphMaterial.reset(); + m_ParticleSystemInstances = {}; + m_ParticleSystems = {}; } void Dissolve::OnGUI(const Timestep frameTime) From f0af417926140fe1cc38e1aa698007b17f9ad6e7 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 30 Sep 2026 01:58:28 -0300 Subject: [PATCH 39/61] Fix particle material blending --- Dissolve/Source/Dissolve.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Dissolve/Source/Dissolve.cpp b/Dissolve/Source/Dissolve.cpp index 42eae81d..9b75455e 100644 --- a/Dissolve/Source/Dissolve.cpp +++ b/Dissolve/Source/Dissolve.cpp @@ -89,6 +89,10 @@ Dissolve::Dissolve() graphMaterial->SetUsage(EMaterialUsage::ParticleSprite, true), "Dissolve graph material must enable ParticleSprite usage." ) + EE_CORE_ASSERT( + graphMaterial->SetBlendMode(EMaterialBlendMode::Translucent), + "Dissolve graph material must enable translucent blending." + ) EE_CORE_ASSERT(graphMaterial->DefineParameter("Tint", { .Kind = EMaterialParameterKind::Value, @@ -145,6 +149,10 @@ Dissolve::Dissolve() ribbonMaterial->SetUsage(EMaterialUsage::ParticleMesh, true), "Ribbon material must enable ParticleRibbon usage." ) + EE_CORE_ASSERT( + ribbonMaterial->SetBlendMode(EMaterialBlendMode::Translucent), + "Ribbon material must enable translucent blending." + ) EE_CORE_ASSERT(ribbonMaterial->DefineParameter("Tint", { .Kind = EMaterialParameterKind::Value, From ef942472fc9b7ed896ccd64369d7362fb01be140 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 30 Sep 2026 16:32:08 -0300 Subject: [PATCH 40/61] Fix Windows material renderer build --- Elixir/Source/Engine/Materials/Rendering/Renderer.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Elixir/Source/Engine/Materials/Rendering/Renderer.h b/Elixir/Source/Engine/Materials/Rendering/Renderer.h index 96e2e9bd..4f8bb6bd 100644 --- a/Elixir/Source/Engine/Materials/Rendering/Renderer.h +++ b/Elixir/Source/Engine/Materials/Rendering/Renderer.h @@ -7,6 +7,8 @@ #include #include +#include + namespace Elixir { class Image; From 4f79eb373104e8e9f365cb783eafcd5f8ddbbb03 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 30 Sep 2026 16:48:03 -0300 Subject: [PATCH 41/61] Add variant to precompiled header --- Elixir/Source/Engine/Materials/Rendering/Renderer.h | 2 -- Elixir/Source/epch.h | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Elixir/Source/Engine/Materials/Rendering/Renderer.h b/Elixir/Source/Engine/Materials/Rendering/Renderer.h index 4f8bb6bd..96e2e9bd 100644 --- a/Elixir/Source/Engine/Materials/Rendering/Renderer.h +++ b/Elixir/Source/Engine/Materials/Rendering/Renderer.h @@ -7,8 +7,6 @@ #include #include -#include - namespace Elixir { class Image; 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 From 8d20e25b6c0573c6af855ad2ef2e42ce35618359 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 30 Sep 2026 17:03:25 -0300 Subject: [PATCH 42/61] Fix Windows texture build --- Elixir/Source/Engine/Graphics/Texture.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Elixir/Source/Engine/Graphics/Texture.cpp b/Elixir/Source/Engine/Graphics/Texture.cpp index c3b407a9..c68d7980 100644 --- a/Elixir/Source/Engine/Graphics/Texture.cpp +++ b/Elixir/Source/Engine/Graphics/Texture.cpp @@ -32,7 +32,6 @@ namespace Elixir SImageCreateInfo Texture::CreateImageInfo(const STextureCreateInfo& info) { return { - .InitialData = info.InitialData, .Width = info.Width, .Type = EImageType::_1D, .Format = info.Format, @@ -40,6 +39,7 @@ namespace Elixir .MipLevels = info.MipLevels, .Usage = EImageUsage::Sampled, .InitialLayout = EImageLayout::ShaderReadOnly, + .InitialData = info.InitialData, .InitialMipData = info.InitialMipData, }; } From a37691609eb6f28f8d0312fbac54ed3617347d72 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 30 Sep 2026 17:28:32 -0300 Subject: [PATCH 43/61] Fix Windows glTF loader build --- Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp index 04676694..d3c6f606 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp @@ -1,6 +1,8 @@ #include "epch.h" #include "GLTFStaticMeshLoader.h" +#include + #include #include #include From 6d66eb1e7aaa8a8d65b701223954dd906113bc4f Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 30 Sep 2026 17:56:45 -0300 Subject: [PATCH 44/61] Fix Windows image test build --- Elixir/Tests/Engine/Graphics/ImageTest.cpp | 4 ++-- Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Elixir/Tests/Engine/Graphics/ImageTest.cpp b/Elixir/Tests/Engine/Graphics/ImageTest.cpp index 842d39dc..d65e1c70 100644 --- a/Elixir/Tests/Engine/Graphics/ImageTest.cpp +++ b/Elixir/Tests/Engine/Graphics/ImageTest.cpp @@ -122,12 +122,12 @@ TEST(ImageTest, AllocationDescriptionDoesNotKeepUploadDataOrGenerationRequests) { std::array pixels{}; SImageCreateInfo info{ - .InitialData = pixels.data(), .Width = 8, .Height = 4, .Format = EImageFormat::R8G8B8A8_UNORM, .MipmapMode = EImageMipmapMode::NormalMap, .MipLevels = 4, - .AllocationInfo = { .RequiredFlags = EMemoryProperty::HostVisible } + .AllocationInfo = { .RequiredFlags = EMemoryProperty::HostVisible }, + .InitialData = pixels.data(), }; ImageMetadata image(info); const auto allocation = image.GetCreateInfo(); diff --git a/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp b/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp index 05ff9899..f1eee5bb 100644 --- a/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp +++ b/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp @@ -140,13 +140,13 @@ TEST_F(VulkanImageTest, NormalMapMipsAreGeneratedByImageAndUploaded) 128, 255, 128, 60, 127, 0, 255, 80 }; SImageCreateInfo info{ - .InitialData = pixels.data(), .Width = 2, .Height = 2, .Format = EImageFormat::R8G8B8A8_UNORM, .MipmapMode = EImageMipmapMode::NormalMap, .MipLevels = 99, .Usage = EImageUsage::Sampled | EImageUsage::TransferSrc, - .InitialLayout = EImageLayout::ShaderReadOnly + .InitialLayout = EImageLayout::ShaderReadOnly, + .InitialData = pixels.data(), }; const auto image = Image::Create(Context, info); EXPECT_EQ(image->GetMipLevels(), 2u); From 74e7899ae8d3cfcb0e3adaaddd756ec29b8b4601 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 30 Sep 2026 19:48:56 -0300 Subject: [PATCH 45/61] Fix Windows Dissolve and Vulkan test build --- Dissolve/Dissolve.cmake | 1 + Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Dissolve/Dissolve.cmake b/Dissolve/Dissolve.cmake index 5232a55c..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 diff --git a/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp b/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp index f1eee5bb..728c5173 100644 --- a/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp +++ b/Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp @@ -191,12 +191,12 @@ TEST_F(VulkanImageTest, SimpleAverageHandlesEveryArrayLayer) std::fill_n(pixels.begin(), 16, 40); std::fill_n(pixels.begin() + 16, 16, 80); const auto image = Image::Create(Context, { - .InitialData = pixels.data(), .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))); @@ -242,11 +242,11 @@ TEST_F(VulkanImageTest, ResizePreservesContentAndClampsTheAllocatedMipCount) std::array pixels; pixels.fill(80); const auto image = Image::Create(Context, { - .InitialData = pixels.data(), .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); @@ -261,10 +261,11 @@ TEST_F(VulkanImageTest, CopyDoesNotChangeDestinationExtent) { std::array pixels{}; const auto source = Image::Create(Context, { - .InitialData = pixels.data(), .Width = 4, .Height = 4, + .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, From bd1fcf63d943ab9fa64877f7006656730677ba22 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 30 Sep 2026 20:14:37 -0300 Subject: [PATCH 46/61] Keep submitted frames alive after failed present --- Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp index b9f3811d..0bdab6c4 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp +++ b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp @@ -883,7 +883,8 @@ namespace Elixir 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; } From 459ad6108154bffdc5b483a5020f28dbcd6f8cde Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 30 Sep 2026 20:22:37 -0300 Subject: [PATCH 47/61] Refresh descriptors after image layout changes --- .../Source/Graphics/Vulkan/VulkanShader.cpp | 22 +++++++++++++++---- Elixir/Source/Graphics/Vulkan/VulkanShader.h | 2 ++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Elixir/Source/Graphics/Vulkan/VulkanShader.cpp b/Elixir/Source/Graphics/Vulkan/VulkanShader.cpp index c743c624..b6e9ab0b 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanShader.cpp +++ b/Elixir/Source/Graphics/Vulkan/VulkanShader.cpp @@ -20,6 +20,14 @@ namespace Elixir::Vulkan 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) @@ -163,7 +171,9 @@ namespace Elixir::Vulkan *binding, DescriptorValue{ STextureDescriptorValue{ texture, - texture ? GetResourceGeneration(texture->GetImage()) : 0 + texture ? GetResourceGeneration(texture->GetImage()) : 0, + texture ? GetDescriptorImageLayout(texture->GetImage()) + : VK_IMAGE_LAYOUT_UNDEFINED } } ); return; @@ -181,7 +191,8 @@ namespace Elixir::Vulkan *binding, DescriptorValue{ SImageDescriptorValue{ image, - GetResourceGeneration(image) + GetResourceGeneration(image), + GetDescriptorImageLayout(image) } } ); return; @@ -478,7 +489,8 @@ namespace Elixir::Vulkan binding, DescriptorValue{ SImageDescriptorValue{ image, - GetResourceGeneration(image) + GetResourceGeneration(image), + GetDescriptorImageLayout(image) } } ); } @@ -489,7 +501,9 @@ namespace Elixir::Vulkan binding, DescriptorValue{ STextureDescriptorValue{ texture, - texture ? GetResourceGeneration(texture->GetImage()) : 0 + texture ? GetResourceGeneration(texture->GetImage()) : 0, + texture ? GetDescriptorImageLayout(texture->GetImage()) + : VK_IMAGE_LAYOUT_UNDEFINED } } ); } diff --git a/Elixir/Source/Graphics/Vulkan/VulkanShader.h b/Elixir/Source/Graphics/Vulkan/VulkanShader.h index 930703de..3028ebab 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanShader.h +++ b/Elixir/Source/Graphics/Vulkan/VulkanShader.h @@ -77,6 +77,7 @@ namespace Elixir::Vulkan { Ref Image; uint64_t ResourceGeneration = 0; + VkImageLayout ImageLayout = VK_IMAGE_LAYOUT_UNDEFINED; bool operator==(const SImageDescriptorValue&) const = default; }; @@ -85,6 +86,7 @@ namespace Elixir::Vulkan { Ref Texture; uint64_t ResourceGeneration = 0; + VkImageLayout ImageLayout = VK_IMAGE_LAYOUT_UNDEFINED; bool operator==(const STextureDescriptorValue&) const = default; }; From c7cde340e49ebecb93024a1689824d9ce455e56e Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 30 Sep 2026 21:16:08 -0300 Subject: [PATCH 48/61] Keep deferred geometry frees alive --- Elixir/Source/Engine/Mesh/GeometryPool.cpp | 82 +++++++++++++--------- Elixir/Source/Engine/Mesh/GeometryPool.h | 32 ++++++--- 2 files changed, 70 insertions(+), 44 deletions(-) diff --git a/Elixir/Source/Engine/Mesh/GeometryPool.cpp b/Elixir/Source/Engine/Mesh/GeometryPool.cpp index f4173ea7..08df804f 100644 --- a/Elixir/Source/Engine/Mesh/GeometryPool.cpp +++ b/Elixir/Source/Engine/Mesh/GeometryPool.cpp @@ -8,21 +8,23 @@ namespace Elixir GeometryPool::GeometryPool( const GraphicsContext& context, const SGeometryPoolConfig config - ) : m_VertexCapacity(config.VertexCapacity), - m_IndexCapacity(config.IndexCapacity), + ) : m_State(CreateRef(SGeometryPoolState{ + .VertexCapacity = config.VertexCapacity, + .IndexCapacity = config.IndexCapacity, + })), m_GraphicsContext(context) { - EE_CORE_ASSERT(m_VertexCapacity > 0, "GeometryPool vertex capacity must be greater than 0.") - EE_CORE_ASSERT(m_IndexCapacity > 0, "GeometryPool index capacity must be greater than 0.") + 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_VertexCapacity * sizeof(SStaticMeshVertex) + m_State->VertexCapacity * sizeof(SStaticMeshVertex) ); m_IndexBuffer = DynamicIndexBuffer::Create( &context, - m_IndexCapacity * sizeof(uint32_t), + m_State->IndexCapacity * sizeof(uint32_t), nullptr, EIndexType::UInt32 ); @@ -52,9 +54,9 @@ namespace Elixir const uint32_t indexCount = (uint32_t)data.Indices.size(); const auto vertexOffset = AllocateRange( - m_FreeVertexRanges, - m_NextVertexOffset, - m_VertexCapacity, + m_State->FreeVertexRanges, + m_State->NextVertexOffset, + m_State->VertexCapacity, vertexCount ); if (!vertexOffset) @@ -64,15 +66,15 @@ namespace Elixir } const auto indexOffset = AllocateRange( - m_FreeIndexRanges, - m_NextIndexOffset, - m_IndexCapacity, + m_State->FreeIndexRanges, + m_State->NextIndexOffset, + m_State->IndexCapacity, indexCount ); if (!indexOffset) { FreeRange( - m_FreeVertexRanges, + m_State->FreeVertexRanges, { .Offset = *vertexOffset, .Count = vertexCount, @@ -95,18 +97,18 @@ namespace Elixir ); uint32_t slotIndex; - if (!m_FreeSlots.empty()) + if (!m_State->FreeSlots.empty()) { - slotIndex = m_FreeSlots.back(); - m_FreeSlots.pop_back(); + slotIndex = m_State->FreeSlots.back(); + m_State->FreeSlots.pop_back(); } else { - slotIndex = (uint32_t)m_Slots.size(); - m_Slots.push_back({}); + slotIndex = (uint32_t)m_State->Slots.size(); + m_State->Slots.push_back({}); } - SGeometryPoolSlot& slot = m_Slots[slotIndex]; + SGeometryPoolSlot& slot = m_State->Slots[slotIndex]; slot.Geometry = { .VertexOffset = *vertexOffset, .IndexOffset = *indexOffset, @@ -126,10 +128,10 @@ namespace Elixir const SGeometry* GeometryPool::Get(const SHandle handle) const { - if (!handle.IsValid() || handle.Index >= m_Slots.size()) + if (!handle.IsValid() || handle.Index >= m_State->Slots.size()) return nullptr; - const SGeometryPoolSlot& slot = m_Slots[handle.Index]; + const SGeometryPoolSlot& slot = m_State->Slots[handle.Index]; if (!slot.Allocated || slot.Generation != handle.Generation) return nullptr; @@ -138,30 +140,34 @@ namespace Elixir void GeometryPool::Free(const SHandle handle) { - if (!handle.IsValid() || handle.Index >= m_Slots.size()) + if (!handle.IsValid() || handle.Index >= m_State->Slots.size()) return; - const SGeometryPoolSlot& slot = m_Slots[handle.Index]; + const SGeometryPoolSlot& slot = m_State->Slots[handle.Index]; if (!slot.Allocated || slot.Generation != handle.Generation) return; - m_GraphicsContext.DeferResourceRelease([this, handle]() + const Ref state = m_State; + m_GraphicsContext.DeferResourceRelease([state, handle]() { - FreeCompleted(handle); + FreeCompleted(*state, handle); }); } - void GeometryPool::FreeCompleted(const SHandle handle) + void GeometryPool::FreeCompleted( + SGeometryPoolState& state, + const SHandle handle + ) { - if (!handle.IsValid() || handle.Index >= m_Slots.size()) + if (!handle.IsValid() || handle.Index >= state.Slots.size()) return; - SGeometryPoolSlot& slot = m_Slots[handle.Index]; + SGeometryPoolSlot& slot = state.Slots[handle.Index]; if (!slot.Allocated || slot.Generation != handle.Generation) return; FreeRange( - m_FreeVertexRanges, + state.FreeVertexRanges, { .Offset = slot.Geometry.VertexOffset, .Count = slot.Geometry.VertexCount, @@ -169,7 +175,7 @@ namespace Elixir ); FreeRange( - m_FreeIndexRanges, + state.FreeIndexRanges, { .Offset = slot.Geometry.IndexOffset, .Count = slot.Geometry.IndexCount, @@ -179,15 +185,23 @@ namespace Elixir slot.Allocated = false; ++slot.Generation; slot.Geometry = {}; - m_FreeSlots.push_back(handle.Index); + 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 < m_Slots.size() && - m_Slots[handle.Index].Allocated && - m_Slots[handle.Index].Generation == handle.Generation; + handle.Index < state.Slots.size() && + state.Slots[handle.Index].Allocated && + state.Slots[handle.Index].Generation == handle.Generation; } std::optional GeometryPool::AllocateRange( diff --git a/Elixir/Source/Engine/Mesh/GeometryPool.h b/Elixir/Source/Engine/Mesh/GeometryPool.h index cd33740e..ce6fc7c0 100644 --- a/Elixir/Source/Engine/Mesh/GeometryPool.h +++ b/Elixir/Source/Engine/Mesh/GeometryPool.h @@ -73,15 +73,35 @@ namespace Elixir 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. */ - void FreeCompleted(SHandle handle); + 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. @@ -99,15 +119,7 @@ namespace Elixir SGeometryPoolRange range ); - uint32_t m_VertexCapacity = 0; - uint32_t m_IndexCapacity = 0; - uint32_t m_NextVertexOffset = 0; - uint32_t m_NextIndexOffset = 0; - - std::vector m_FreeVertexRanges; - std::vector m_FreeIndexRanges; - std::vector m_Slots; - std::vector m_FreeSlots; + Ref m_State; Ref m_VertexBuffer; Ref m_IndexBuffer; From 07db484272a9b5c1795a54640d3a53af144ffc12 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 30 Sep 2026 22:46:29 -0300 Subject: [PATCH 49/61] Simplify framebuffer resize handling --- Elixir/Source/Engine/Core/Application.cpp | 53 +++++++---------------- Elixir/Source/Engine/Core/Application.h | 12 +---- 2 files changed, 16 insertions(+), 49 deletions(-) diff --git a/Elixir/Source/Engine/Core/Application.cpp b/Elixir/Source/Engine/Core/Application.cpp index 79284f19..b775f655 100644 --- a/Elixir/Source/Engine/Core/Application.cpp +++ b/Elixir/Source/Engine/Core/Application.cpp @@ -41,7 +41,7 @@ namespace Elixir CreateSceneTarget(m_GraphicsContext->GetSwapchainExtent()); m_ShaderLoader = CreateScope(m_GraphicsContext.get()); - m_PostProcessor = CreateScope( + m_PostProcessor = CreateRef( m_GraphicsContext.get(), m_ShaderLoader.get(), m_SceneTarget->GetExtent() @@ -296,50 +296,27 @@ namespace Elixir 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 + ]() { - std::scoped_lock lock(m_FramebufferResizeMutex); - m_PendingFramebufferExtent = event.GetExtent(); - if (m_FramebufferResizeQueued) - return false; - - m_FramebufferResizeQueued = true; - } - - if (!m_GraphicsContext->EnqueueRenderTask([this]() - { - ApplyPendingFramebufferResizes(); - })) - { - std::scoped_lock lock(m_FramebufferResizeMutex); - m_FramebufferResizeQueued = false; - } + context->Resize(extent); + sceneTarget->Resize({ extent.Width, extent.Height, 1 }); + postProcessor->Resize({ extent.Width, extent.Height, 1 }); + }); return false; } - void Application::ApplyPendingFramebufferResizes() - { - while (true) - { - std::optional extent; - { - std::scoped_lock lock(m_FramebufferResizeMutex); - extent.swap(m_PendingFramebufferExtent); - if (!extent) - { - m_FramebufferResizeQueued = false; - return; - } - } - - m_GraphicsContext->Resize(*extent); - m_SceneTarget->Resize({ extent->Width, extent->Height, 1 }); - m_PostProcessor->Resize({ extent->Width, extent->Height, 1 }); - } - } - void Application::CreateSceneTarget(const Extent3D& extent) { m_SceneTarget = Image::Create(m_GraphicsContext.get(), { diff --git a/Elixir/Source/Engine/Core/Application.h b/Elixir/Source/Engine/Core/Application.h index 0608a10c..194d5fe9 100644 --- a/Elixir/Source/Engine/Core/Application.h +++ b/Elixir/Source/Engine/Core/Application.h @@ -1,8 +1,5 @@ #pragma once -#include -#include - #include #include #include @@ -83,7 +80,7 @@ namespace Elixir Scope m_AetherManager; - Scope m_PostProcessor; + Ref m_PostProcessor; Timer m_Timer; FrameProfiler m_Profiler; @@ -96,13 +93,6 @@ namespace Elixir bool m_Minimized = false; private: - // Applies the newest framebuffer resize request on the rendering thread. - void ApplyPendingFramebufferResizes(); - - std::mutex m_FramebufferResizeMutex; - std::optional m_PendingFramebufferExtent; - bool m_FramebufferResizeQueued = false; - static Application* s_Application; }; From ba5ba6b3f4017a31f6326e60ccd348a1e48b3e08 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 30 Sep 2026 23:21:21 -0300 Subject: [PATCH 50/61] Use materials for particle appearance --- Shaders/Material/ParticleMesh.ps.hlsl | 3 +-- Shaders/Material/ParticleMesh.vs.hlsl | 5 +---- Shaders/Material/ParticleRibbon.ps.hlsl | 13 ++----------- Shaders/Material/ParticleRibbon.vs.hlsl | 5 +---- Shaders/Material/ParticleSprite.ps.hlsl | 3 +-- 5 files changed, 6 insertions(+), 23 deletions(-) diff --git a/Shaders/Material/ParticleMesh.ps.hlsl b/Shaders/Material/ParticleMesh.ps.hlsl index f90ef150..bc2afd29 100644 --- a/Shaders/Material/ParticleMesh.ps.hlsl +++ b/Shaders/Material/ParticleMesh.ps.hlsl @@ -42,7 +42,6 @@ struct PSInput float4 ClipPos : SV_POSITION; float3 WorldPos : POSITION0; float3 Normal : NORMAL0; - float4 Color : COLOR0; float2 TexCoord : TEXCOORD0; }; @@ -95,4 +94,4 @@ float4 main(PSInput input) : SV_Target0 (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..23acb07b 100644 --- a/Shaders/Material/ParticleMesh.vs.hlsl +++ b/Shaders/Material/ParticleMesh.vs.hlsl @@ -64,7 +64,6 @@ struct VSInput float4 VelocityAge : TEXCOORD0; float4 Transform : TEXCOORD1; float4 TangentRibbonId : TANGENT; - float4 Color : COLOR; float4 Metadata : TEXCOORD2; }; @@ -73,7 +72,6 @@ struct VSOutput float4 ClipPos : SV_POSITION; float3 WorldPos : POSITION0; float3 Normal : NORMAL0; - float4 Color : COLOR0; float2 TexCoord : TEXCOORD0; }; @@ -115,8 +113,7 @@ 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; return output; -} \ No newline at end of file +} diff --git a/Shaders/Material/ParticleRibbon.ps.hlsl b/Shaders/Material/ParticleRibbon.ps.hlsl index b2af4249..334b17fc 100644 --- a/Shaders/Material/ParticleRibbon.ps.hlsl +++ b/Shaders/Material/ParticleRibbon.ps.hlsl @@ -42,7 +42,6 @@ MaterialPushConstants pc; struct PSInput { float4 ClipPos : SV_POSITION; - float4 Color : COLOR0; float2 TexCoord : TEXCOORD0; nointerpolation float Valid : TEXCOORD1; }; @@ -84,13 +83,5 @@ float4 main(PSInput input) : SV_Target0 clip(surface.Opacity - mat.AlphaCutoff); } - //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; - -// 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..47e5b8d0 100644 --- a/Shaders/Material/ParticleRibbon.vs.hlsl +++ b/Shaders/Material/ParticleRibbon.vs.hlsl @@ -50,7 +50,6 @@ PushConstants pc; struct VSOutput { float4 ClipPos : SV_POSITION; - float4 Color : COLOR0; float2 TexCoord : TEXCOORD0; nointerpolation float Valid : TEXCOORD1; }; @@ -190,7 +189,6 @@ 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; return output; @@ -268,9 +266,8 @@ VSOutput main(uint vertexId : SV_VertexID) VSOutput output; output.ClipPos = mul(ViewProj, float4(center + side * (width * 0.5 * sideSign), 1.0)); - output.Color = useEndParticle ? endParticle.Color : startParticle.Color; output.TexCoord = 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/Material/ParticleSprite.ps.hlsl b/Shaders/Material/ParticleSprite.ps.hlsl index 896b44b5..88af4efa 100644 --- a/Shaders/Material/ParticleSprite.ps.hlsl +++ b/Shaders/Material/ParticleSprite.ps.hlsl @@ -40,7 +40,6 @@ MaterialPushConstants pc; struct PSInput { float4 ClipPos : SV_POSITION; - float4 Color : COLOR; float2 TexCoord : TEXCOORD0; }; @@ -80,4 +79,4 @@ float4 main(PSInput input) : SV_Target0 } return float4(surface.BaseColor + surface.Emissive, surface.Opacity); -} \ No newline at end of file +} From b9640cdfe76a53339fe662d77d08753306a5a9b5 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 30 Sep 2026 23:25:01 -0300 Subject: [PATCH 51/61] Retry particle allocation after replacement failure --- Elixir/Source/Engine/Aether/Simulation/Simulator.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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()); From de60a4d016417395a54153859a28e70c948f991a Mon Sep 17 00:00:00 2001 From: MrChampz Date: Wed, 30 Sep 2026 23:41:31 -0300 Subject: [PATCH 52/61] Support Fresnel in particle materials --- Shaders/Aether/Sprite.vs.hlsl | 4 +++- Shaders/Material/ParticleMesh.ps.hlsl | 11 ++++++----- Shaders/Material/ParticleRibbon.ps.hlsl | 7 ++++++- Shaders/Material/ParticleRibbon.vs.hlsl | 5 ++++- Shaders/Material/ParticleSprite.ps.hlsl | 7 ++++++- 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/Shaders/Aether/Sprite.vs.hlsl b/Shaders/Aether/Sprite.vs.hlsl index 8c463f04..06a49606 100644 --- a/Shaders/Aether/Sprite.vs.hlsl +++ b/Shaders/Aether/Sprite.vs.hlsl @@ -35,6 +35,7 @@ struct VSOutput float4 ClipPos : SV_POSITION; float4 Color : COLOR; float2 TexCoord : TEXCOORD0; + float3 ViewPos : POSITION0; }; // TODO: TEMP approximation, replace by a better solution! @@ -68,6 +69,7 @@ VSOutput main(VSInput input, uint vertexId : SV_VertexID) output.ClipPos = mul(Proj, float4(viewPos, 1.0f)); output.Color = input.Color; output.TexCoord = normalizedPos; + output.ViewPos = viewPos; return output; -} \ No newline at end of file +} diff --git a/Shaders/Material/ParticleMesh.ps.hlsl b/Shaders/Material/ParticleMesh.ps.hlsl index bc2afd29..1eca48c5 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. +// Surface fields using planar UVs, time, material values and textures. [[vk::binding(0, 0)]] cbuffer cbFrame : register(b0) @@ -75,6 +75,9 @@ float4 main(PSInput input) : SV_Target0 surface.Opacity = 1.0f; surface.Emissive = float3(0.0f, 0.0f, 0.0f); + const float3 N = normalize(input.Normal); + const float3 V = normalize(CameraPos - input.WorldPos); + // __GRAPH_BODY__ static const uint MATERIAL_BLEND_MASK = 1u; @@ -83,12 +86,10 @@ float4 main(PSInput input) : SV_Target0 clip(surface.Opacity - mat.AlphaCutoff); } - const float3 normal = normalize(input.Normal); 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); diff --git a/Shaders/Material/ParticleRibbon.ps.hlsl b/Shaders/Material/ParticleRibbon.ps.hlsl index 334b17fc..44dfc36b 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. +// Surface fields using UV, material values and textures. [[vk::binding(0, 0)]] cbuffer cbFrame : register(b0) @@ -44,6 +44,7 @@ struct PSInput float4 ClipPos : SV_POSITION; float2 TexCoord : TEXCOORD0; nointerpolation float Valid : TEXCOORD1; + float3 WorldPos : POSITION0; }; struct Surface @@ -75,6 +76,10 @@ 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__ static const uint MATERIAL_BLEND_MASK = 1u; diff --git a/Shaders/Material/ParticleRibbon.vs.hlsl b/Shaders/Material/ParticleRibbon.vs.hlsl index 47e5b8d0..410a06cb 100644 --- a/Shaders/Material/ParticleRibbon.vs.hlsl +++ b/Shaders/Material/ParticleRibbon.vs.hlsl @@ -52,6 +52,7 @@ struct VSOutput float4 ClipPos : SV_POSITION; float2 TexCoord : TEXCOORD0; nointerpolation float Valid : TEXCOORD1; + float3 WorldPos : POSITION0; }; float3 SafeNormalize(float3 value, float3 fallback) @@ -191,6 +192,7 @@ VSOutput EmptyVertex() output.ClipPos = float4(0.0, 0.0, 0.0, 1.0); output.TexCoord = float2(0.5, 0.0); output.Valid = 0.0; + output.WorldPos = float3(0.0, 0.0, 0.0); return output; } @@ -265,7 +267,8 @@ 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.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; diff --git a/Shaders/Material/ParticleSprite.ps.hlsl b/Shaders/Material/ParticleSprite.ps.hlsl index 88af4efa..eeb97e9f 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. +// Surface fields using UV, time, material values and textures. [[vk::binding(0, 0)]] cbuffer cbFrame : register(b0) @@ -41,6 +41,7 @@ struct PSInput { float4 ClipPos : SV_POSITION; float2 TexCoord : TEXCOORD0; + float3 ViewPos : POSITION0; }; struct Surface @@ -70,6 +71,10 @@ 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; From 8329c6b6e6c839e602ab20b56b806d9172e1a529 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Thu, 1 Oct 2026 02:54:55 -0300 Subject: [PATCH 53/61] Unify particle material usage --- Dissolve/Source/Dissolve.cpp | 12 +- .../Engine/Aether/Effect/MaterialFactory.cpp | 18 +-- .../Engine/Aether/Effect/MaterialFactory.h | 18 +-- .../Engine/Aether/Effect/MaterialResolver.cpp | 4 +- .../Engine/Graphics/Shader/ShaderLoader.cpp | 4 +- .../Engine/Materials/Compilation/Compiler.cpp | 77 +++++---- .../Engine/Materials/Compilation/Compiler.h | 33 ++-- .../Engine/Materials/DefaultMaterials.cpp | 40 +---- Elixir/Source/Engine/Materials/Material.h | 10 +- .../Engine/Materials/Rendering/Renderer.cpp | 30 +++- .../Engine/Materials/Rendering/Renderer.h | 3 + .../Aether/Effect/MaterialResolverTest.cpp | 6 +- Elixir/Tests/Engine/Aether/SystemTest.cpp | 37 ++++- .../Compilation/CompilationCacheTest.cpp | 6 +- .../Materials/Compilation/CompilerTest.cpp | 16 +- .../Materials/MaterialProxyCacheTest.cpp | 2 +- .../Engine/Materials/MaterialRegistryTest.cpp | 4 +- .../Materials/Rendering/FrameTableTest.cpp | 2 +- .../Materials/Rendering/RendererTest.cpp | 6 +- Shaders/Aether/Mesh.ps.hlsl | 33 ---- Shaders/Aether/Mesh.vs.hlsl | 119 -------------- Shaders/Aether/Ribbon.ps.hlsl | 19 --- Shaders/Aether/Ribbon.vs.hlsl | 146 ------------------ Shaders/Aether/Sprite.ps.hlsl | 32 ---- Shaders/Material/ParticleMesh.ps.hlsl | 3 +- Shaders/Material/ParticleMesh.vs.hlsl | 6 +- Shaders/Material/ParticleRibbon.ps.hlsl | 3 +- Shaders/Material/ParticleRibbon.vs.hlsl | 5 +- Shaders/Material/ParticleSprite.ps.hlsl | 3 +- .../ParticleSprite.vs.hlsl} | 32 ++-- Shaders/Shaders.cmake | 13 +- 31 files changed, 184 insertions(+), 558 deletions(-) delete mode 100644 Shaders/Aether/Mesh.ps.hlsl delete mode 100644 Shaders/Aether/Mesh.vs.hlsl delete mode 100644 Shaders/Aether/Ribbon.ps.hlsl delete mode 100644 Shaders/Aether/Ribbon.vs.hlsl delete mode 100644 Shaders/Aether/Sprite.ps.hlsl rename Shaders/{Aether/Sprite.vs.hlsl => Material/ParticleSprite.vs.hlsl} (61%) diff --git a/Dissolve/Source/Dissolve.cpp b/Dissolve/Source/Dissolve.cpp index 9b75455e..54168425 100644 --- a/Dissolve/Source/Dissolve.cpp +++ b/Dissolve/Source/Dissolve.cpp @@ -86,8 +86,8 @@ Dissolve::Dissolve() graphMaterial = CreateRef("DissolveGraph"); EE_CORE_ASSERT( - graphMaterial->SetUsage(EMaterialUsage::ParticleSprite, true), - "Dissolve graph material must enable ParticleSprite usage." + graphMaterial->SetUsage(EMaterialUsage::Particle, true), + "Dissolve graph material must enable Particle usage." ) EE_CORE_ASSERT( graphMaterial->SetBlendMode(EMaterialBlendMode::Translucent), @@ -142,12 +142,8 @@ Dissolve::Dissolve() const auto ribbonMaterial = CreateRef("RibbonEnergy"); EE_CORE_ASSERT( - ribbonMaterial->SetUsage(EMaterialUsage::ParticleRibbon, true), - "Ribbon material must enable ParticleRibbon usage." - ) - EE_CORE_ASSERT( - ribbonMaterial->SetUsage(EMaterialUsage::ParticleMesh, true), - "Ribbon material must enable ParticleRibbon usage." + ribbonMaterial->SetUsage(EMaterialUsage::Particle, true), + "Ribbon material must enable Particle usage." ) EE_CORE_ASSERT( ribbonMaterial->SetBlendMode(EMaterialBlendMode::Translucent), diff --git a/Elixir/Source/Engine/Aether/Effect/MaterialFactory.cpp b/Elixir/Source/Engine/Aether/Effect/MaterialFactory.cpp index 69863cc6..8bbfd3b6 100644 --- a/Elixir/Source/Engine/Aether/Effect/MaterialFactory.cpp +++ b/Elixir/Source/Engine/Aether/Effect/MaterialFactory.cpp @@ -12,27 +12,14 @@ 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)); - auto result = material->SetUsage(GetMaterialUsage(renderMode), true); + auto result = material->SetUsage(EMaterialUsage::Particle, true); EE_CORE_ASSERT(result, "Particle material usage must be enabled.") result = material->SetBlendMode(EMaterialBlendMode::Translucent); @@ -58,8 +45,7 @@ namespace Elixir::Aether::Effect ); graph.SetChannel(EMaterialChannel::Emissive, emissive); - if (renderMode == Core::EParticleRenderMode::Sprite && - !desc.BaseColorTexturePath.empty()) + if (!desc.BaseColorTexturePath.empty()) { constexpr auto texParam = "BaseColorTexture"; const auto tex = TextureLoader::Load(desc.BaseColorTexturePath); 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..1b98db42 100644 --- a/Elixir/Source/Engine/Aether/Effect/MaterialResolver.cpp +++ b/Elixir/Source/Engine/Aether/Effect/MaterialResolver.cpp @@ -28,7 +28,7 @@ namespace Elixir::Aether::Effect material = m_Registry.Find(name); if (!material) { - material = CreateMaterial(name, emitter->GetRenderMode(), *desc); + material = CreateMaterial(name, *desc); if (!m_Registry.Register(material)) { EE_CORE_ERROR("Aether material '{}' could not be registered.", name) @@ -38,7 +38,7 @@ namespace Elixir::Aether::Effect } else { - material = m_Registry.GetDefault(GetMaterialUsage(emitter->GetRenderMode())); + material = m_Registry.GetDefault(EMaterialUsage::Particle); } emitter->SetMaterial(material); 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/Materials/Compilation/Compiler.cpp b/Elixir/Source/Engine/Materials/Compilation/Compiler.cpp index 40eb99c6..55cfe4cc 100644 --- a/Elixir/Source/Engine/Materials/Compilation/Compiler.cpp +++ b/Elixir/Source/Engine/Materials/Compilation/Compiler.cpp @@ -130,20 +130,12 @@ namespace Elixir::Materials::Compilation if (!result) return result; } - if (material.SupportsUsage(EMaterialUsage::ParticleSprite)) + if (material.SupportsUsage(EMaterialUsage::Particle)) { result = CompileParticleSprite(loader, material, std::move(result)); if (!result) return result; - } - - if (material.SupportsUsage(EMaterialUsage::ParticleRibbon)) - { 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; } @@ -273,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; } @@ -290,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."; @@ -318,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) { diff --git a/Elixir/Source/Engine/Materials/Compilation/Compiler.h b/Elixir/Source/Engine/Materials/Compilation/Compiler.h index b471a038..f3f2bcaf 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. */ @@ -37,13 +46,13 @@ namespace Elixir::Materials::Compilation /** @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. */ @@ -60,21 +69,21 @@ namespace Elixir::Materials::Compilation } /** - * @brief Gets the shader for a material usage. - * @param usage Material usage. - * @return The matching 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::Surface: + case EMaterialShaderVariant::Surface: return SurfaceShader; - case EMaterialUsage::ParticleSprite: + 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; diff --git a/Elixir/Source/Engine/Materials/DefaultMaterials.cpp b/Elixir/Source/Engine/Materials/DefaultMaterials.cpp index bd97899a..0dfe5311 100644 --- a/Elixir/Source/Engine/Materials/DefaultMaterials.cpp +++ b/Elixir/Source/Engine/Materials/DefaultMaterials.cpp @@ -48,7 +48,7 @@ namespace Elixir::Materials ); } - Ref CreateDefaultSpriteMaterial() + Ref CreateDefaultParticleMaterial() { MaterialGraph graph; @@ -66,38 +66,8 @@ namespace Elixir::Materials graph.SetChannel(EMaterialChannel::Opacity, opacity); return MakeMaterial( - "Engine.Materials.Defaults.ParticleSprite", - EMaterialUsage::ParticleSprite, - std::move(graph), - EMaterialBlendMode::Translucent - ); - } - - Ref CreateDefaultRibbonMaterial() - { - MaterialGraph graph; - - const auto checkerboard = graph.AddNode(8.0f); - graph.SetChannel(EMaterialChannel::BaseColor, checkerboard); - - return MakeMaterial( - "Engine.Materials.Defaults.ParticleRibbon", - EMaterialUsage::ParticleRibbon, - std::move(graph), - EMaterialBlendMode::Translucent - ); - } - - Ref CreateDefaultMeshMaterial() - { - MaterialGraph graph; - - const auto checkerboard = graph.AddNode(8.0f); - graph.SetChannel(EMaterialChannel::BaseColor, checkerboard); - - return MakeMaterial( - "Engine.Materials.Defaults.ParticleMesh", - EMaterialUsage::ParticleMesh, + "Engine.Materials.Defaults.Particle", + EMaterialUsage::Particle, std::move(graph), EMaterialBlendMode::Translucent ); @@ -108,9 +78,7 @@ namespace Elixir::Materials { return { CreateDefaultSurfaceMaterial(), - CreateDefaultSpriteMaterial(), - CreateDefaultRibbonMaterial(), - CreateDefaultMeshMaterial() + CreateDefaultParticleMaterial() }; } } diff --git a/Elixir/Source/Engine/Materials/Material.h b/Elixir/Source/Engine/Materials/Material.h index 6acd85f3..f695768f 100644 --- a/Elixir/Source/Engine/Materials/Material.h +++ b/Elixir/Source/Engine/Materials/Material.h @@ -18,14 +18,8 @@ namespace Elixir::Materials /** Material for surface geometry. */ Surface = 0, - /** Material for particle sprites. */ - ParticleSprite, - - /** Material for particle ribbons. */ - ParticleRibbon, - - /** Material for particle meshes. */ - ParticleMesh, + /** Material for every particle geometry mode. */ + Particle, /** Number of supported usages. */ Count diff --git a/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp b/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp index 4e22d9bc..0329f469 100644 --- a/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp +++ b/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp @@ -127,7 +127,7 @@ namespace Elixir::Materials::Rendering if (!compiled || !compiled->SupportsUsage(usage)) return std::nullopt; - const auto& shader = compiled->GetShader(usage); + const auto& shader = compiled->GetShader(GetShaderVariant(pass)); if (!shader) return std::nullopt; @@ -139,13 +139,31 @@ namespace Elixir::Materials::Rendering switch (pass) { case EMaterialPass::Surface: return EMaterialUsage::Surface; - case EMaterialPass::ParticleSprite: return EMaterialUsage::ParticleSprite; - case EMaterialPass::ParticleRibbon: return EMaterialUsage::ParticleRibbon; - case EMaterialPass::ParticleMesh: return EMaterialUsage::ParticleMesh; + 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) @@ -394,7 +412,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; diff --git a/Elixir/Source/Engine/Materials/Rendering/Renderer.h b/Elixir/Source/Engine/Materials/Rendering/Renderer.h index 96e2e9bd..77395158 100644 --- a/Elixir/Source/Engine/Materials/Rendering/Renderer.h +++ b/Elixir/Source/Engine/Materials/Rendering/Renderer.h @@ -253,6 +253,9 @@ 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 { diff --git a/Elixir/Tests/Engine/Aether/Effect/MaterialResolverTest.cpp b/Elixir/Tests/Engine/Aether/Effect/MaterialResolverTest.cpp index a8bb1bed..ac320a52 100644 --- a/Elixir/Tests/Engine/Aether/Effect/MaterialResolverTest.cpp +++ b/Elixir/Tests/Engine/Aether/Effect/MaterialResolverTest.cpp @@ -28,12 +28,12 @@ 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_TRUE(sprite.GetMaterial()->GetParent()->SupportsUsage(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)); } diff --git a/Elixir/Tests/Engine/Aether/SystemTest.cpp b/Elixir/Tests/Engine/Aether/SystemTest.cpp index 5a051060..2a6a023d 100644 --- a/Elixir/Tests/Engine/Aether/SystemTest.cpp +++ b/Elixir/Tests/Engine/Aether/SystemTest.cpp @@ -127,10 +127,10 @@ 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)); + ASSERT_TRUE(material->SetUsage(EMaterialUsage::Particle, true)); ASSERT_TRUE(material->DefineParameter("Tint", { .Kind = EMaterialParameterKind::Value, .ValueType = EMaterialValueType::Float4, @@ -149,7 +149,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 + EMaterialUsage::Particle )); EXPECT_EQ(first.Emitters[0].Material, instance); EXPECT_FLOAT_EQ(first.Emitters[0].Material->GetVector("Tint").x, 0.25f); @@ -167,7 +167,7 @@ TEST(SystemTest, CompilePublishesParticleSpriteMaterialInstance) TEST(SystemTest, CompileSnapshotsParticleRibbonMaterialForRenderData) { const auto material = CreateRef("Particle ribbon"); - ASSERT_TRUE(material->SetUsage(EMaterialUsage::ParticleRibbon, true)); + ASSERT_TRUE(material->SetUsage(EMaterialUsage::Particle, true)); const auto system = CreateRef("Ribbon material snapshot contract"); auto& emitter = system->AddEmitter("Ribbon", 8, 0.0f); @@ -181,14 +181,35 @@ 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 + EMaterialUsage::Particle + )); +} + +TEST(SystemTest, CompilePreservesParticleMaterialWhenRenderModeChanges) +{ + const auto material = CreateRef("Particle material"); + ASSERT_TRUE(material->SetUsage(EMaterialUsage::Particle, true)); + + 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_TRUE(compiled.Emitters[0].Material->GetParent()->SupportsUsage( + EMaterialUsage::Particle )); } TEST(SystemTest, CompileSnapshotsParticleMeshMaterialForRenderData) { const auto material = CreateRef("Particle mesh"); - material->SetUsage(EMaterialUsage::ParticleMesh, true); + material->SetUsage(EMaterialUsage::Particle, true); const auto system = CreateRef("Mesh material"); auto& emitter = system->AddEmitter("Mesh", 8, 0.0f); @@ -202,7 +223,7 @@ 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 + EMaterialUsage::Particle )); } @@ -216,6 +237,6 @@ 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 + EMaterialUsage::Particle )); } diff --git a/Elixir/Tests/Engine/Materials/Compilation/CompilationCacheTest.cpp b/Elixir/Tests/Engine/Materials/Compilation/CompilationCacheTest.cpp index 25072c78..6d93979b 100644 --- a/Elixir/Tests/Engine/Materials/Compilation/CompilationCacheTest.cpp +++ b/Elixir/Tests/Engine/Materials/Compilation/CompilationCacheTest.cpp @@ -11,7 +11,7 @@ TEST(CompilationCacheTest, ReusesACompiledMaterialUntilTheSourceRevisionChanges) CompilationCache cache{ nullptr }; const auto material = CreateRef("Cache test"); - ASSERT_TRUE(material->SetUsage(EMaterialUsage::ParticleSprite, true)); + ASSERT_TRUE(material->SetUsage(EMaterialUsage::Particle, true)); const auto first = cache.GetOrCompile(material); const auto second = cache.GetOrCompile(material); @@ -19,11 +19,11 @@ TEST(CompilationCacheTest, ReusesACompiledMaterialUntilTheSourceRevisionChanges) ASSERT_TRUE(first); EXPECT_EQ(first, second); - ASSERT_TRUE(material->SetUsage(EMaterialUsage::ParticleRibbon, true)); + ASSERT_TRUE(material->SetUsage(EMaterialUsage::Surface, true)); const auto rebuilt = cache.GetOrCompile(material); ASSERT_TRUE(rebuilt); EXPECT_NE(first, rebuilt); - EXPECT_TRUE(rebuilt->SupportsUsage(EMaterialUsage::ParticleRibbon)); + EXPECT_TRUE(rebuilt->SupportsUsage(EMaterialUsage::Surface)); } diff --git a/Elixir/Tests/Engine/Materials/Compilation/CompilerTest.cpp b/Elixir/Tests/Engine/Materials/Compilation/CompilerTest.cpp index 9c7f4d6e..a302ffbf 100644 --- a/Elixir/Tests/Engine/Materials/Compilation/CompilerTest.cpp +++ b/Elixir/Tests/Engine/Materials/Compilation/CompilerTest.cpp @@ -36,27 +36,23 @@ TEST(CompilerTest, PreservesEnabledRendererUsages) { const auto material = CreateRef("Material"); ASSERT_TRUE(material->SetUsage(EMaterialUsage::Surface, true)); - ASSERT_TRUE(material->SetUsage(EMaterialUsage::ParticleSprite, true)); - ASSERT_TRUE(material->SetUsage(EMaterialUsage::ParticleRibbon, true)); - ASSERT_TRUE(material->SetUsage(EMaterialUsage::ParticleMesh, true)); + ASSERT_TRUE(material->SetUsage(EMaterialUsage::Particle, true)); const auto result = Compiler::Build(*material); ASSERT_TRUE(result); EXPECT_TRUE(result.Material->SupportsUsage(EMaterialUsage::Surface)); - EXPECT_TRUE(result.Material->SupportsUsage(EMaterialUsage::ParticleSprite)); - EXPECT_TRUE(result.Material->SupportsUsage(EMaterialUsage::ParticleRibbon)); - EXPECT_TRUE(result.Material->SupportsUsage(EMaterialUsage::ParticleMesh)); + EXPECT_TRUE(result.Material->SupportsUsage(EMaterialUsage::Particle)); } TEST(CompilerTest, DoesNotAliasParticleUsageShadersToSurfaceShader) { SCompiledMaterial material; - const auto& surfaceShader = material.GetShader(EMaterialUsage::Surface); - 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); diff --git a/Elixir/Tests/Engine/Materials/MaterialProxyCacheTest.cpp b/Elixir/Tests/Engine/Materials/MaterialProxyCacheTest.cpp index 66d3a925..fdc9206d 100644 --- a/Elixir/Tests/Engine/Materials/MaterialProxyCacheTest.cpp +++ b/Elixir/Tests/Engine/Materials/MaterialProxyCacheTest.cpp @@ -76,7 +76,7 @@ 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()->SetUsage(EMaterialUsage::Particle, true)); 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 a7d54b61..8845a088 100644 --- a/Elixir/Tests/Engine/Materials/MaterialRegistryTest.cpp +++ b/Elixir/Tests/Engine/Materials/MaterialRegistryTest.cpp @@ -11,9 +11,7 @@ TEST(MaterialRegistryTest, RegistersAndFindsDefaultMaterials) for (const auto usage : { EMaterialUsage::Surface, - EMaterialUsage::ParticleSprite, - EMaterialUsage::ParticleRibbon, - EMaterialUsage::ParticleMesh + EMaterialUsage::Particle }) { const auto& material = registry.GetDefault(usage); diff --git a/Elixir/Tests/Engine/Materials/Rendering/FrameTableTest.cpp b/Elixir/Tests/Engine/Materials/Rendering/FrameTableTest.cpp index 276fc228..3e4e9f6f 100644 --- a/Elixir/Tests/Engine/Materials/Rendering/FrameTableTest.cpp +++ b/Elixir/Tests/Engine/Materials/Rendering/FrameTableTest.cpp @@ -23,7 +23,7 @@ namespace TEST(FrameTableTest, DeduplicatesAProxyAndPreservesItsValues) { auto material = CreateRef("Particle material"); - ASSERT_TRUE(material->SetUsage(EMaterialUsage::ParticleSprite, true)); + ASSERT_TRUE(material->SetUsage(EMaterialUsage::Particle, true)); ASSERT_TRUE(material->DefineParameter("Tint", { .Kind = EMaterialParameterKind::Value, .ValueType = EMaterialValueType::Float4, 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/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/ParticleMesh.ps.hlsl b/Shaders/Material/ParticleMesh.ps.hlsl index 1eca48c5..d6edfb8b 100644 --- a/Shaders/Material/ParticleMesh.ps.hlsl +++ b/Shaders/Material/ParticleMesh.ps.hlsl @@ -1,4 +1,4 @@ -// Template for EMaterialUsage::ParticleMesh. The generated graph body writes +// Shader variant for EMaterialUsage::Particle. The generated graph body writes // Surface fields using planar UVs, time, material values and textures. [[vk::binding(0, 0)]] @@ -43,6 +43,7 @@ struct PSInput float3 WorldPos : POSITION0; float3 Normal : NORMAL0; float2 TexCoord : TEXCOORD0; + float2 TexCoord1 : TEXCOORD1; }; static const float3 LIGHT_DIRECTION = float3(-0.45, 0.8, 0.55); diff --git a/Shaders/Material/ParticleMesh.vs.hlsl b/Shaders/Material/ParticleMesh.vs.hlsl index 23acb07b..3ec808a2 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,6 +73,7 @@ struct VSOutput float3 WorldPos : POSITION0; float3 Normal : NORMAL0; float2 TexCoord : TEXCOORD0; + float2 TexCoord1 : TEXCOORD1; }; float Hash01(uint x) @@ -114,6 +115,7 @@ VSOutput main(VSInput input) output.ClipPos = mul(ViewProj, float4(output.WorldPos, 1.0)); output.Normal = normalize(mul((float3x3)pc.WorldTransform, localNormal)); output.TexCoord = input.LocalPos.xy * 0.5f + 0.5f; + output.TexCoord1 = output.TexCoord; return output; } diff --git a/Shaders/Material/ParticleRibbon.ps.hlsl b/Shaders/Material/ParticleRibbon.ps.hlsl index 44dfc36b..5ed38023 100644 --- a/Shaders/Material/ParticleRibbon.ps.hlsl +++ b/Shaders/Material/ParticleRibbon.ps.hlsl @@ -1,4 +1,4 @@ -// Template for EMaterialUsage::ParticleRibbon. The generated graph body writes +// Shader variant for EMaterialUsage::Particle. The generated graph body writes // Surface fields using UV, material values and textures. [[vk::binding(0, 0)]] @@ -44,6 +44,7 @@ struct PSInput float4 ClipPos : SV_POSITION; float2 TexCoord : TEXCOORD0; nointerpolation float Valid : TEXCOORD1; + float2 TexCoord1 : TEXCOORD2; float3 WorldPos : POSITION0; }; diff --git a/Shaders/Material/ParticleRibbon.vs.hlsl b/Shaders/Material/ParticleRibbon.vs.hlsl index 410a06cb..629c3ec3 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 { @@ -52,6 +52,7 @@ struct VSOutput float4 ClipPos : SV_POSITION; float2 TexCoord : TEXCOORD0; nointerpolation float Valid : TEXCOORD1; + float2 TexCoord1 : TEXCOORD2; float3 WorldPos : POSITION0; }; @@ -192,6 +193,7 @@ VSOutput EmptyVertex() output.ClipPos = float4(0.0, 0.0, 0.0, 1.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); return output; } @@ -271,6 +273,7 @@ VSOutput main(uint vertexId : SV_VertexID) 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; return output; } diff --git a/Shaders/Material/ParticleSprite.ps.hlsl b/Shaders/Material/ParticleSprite.ps.hlsl index eeb97e9f..3b5aa224 100644 --- a/Shaders/Material/ParticleSprite.ps.hlsl +++ b/Shaders/Material/ParticleSprite.ps.hlsl @@ -1,4 +1,4 @@ -// Template for EMaterialUsage::ParticleSprite. The generated graph body writes +// Shader variant for EMaterialUsage::Particle. The generated graph body writes // Surface fields using UV, time, material values and textures. [[vk::binding(0, 0)]] @@ -41,6 +41,7 @@ struct PSInput { float4 ClipPos : SV_POSITION; float2 TexCoord : TEXCOORD0; + float2 TexCoord1 : TEXCOORD1; float3 ViewPos : POSITION0; }; diff --git a/Shaders/Aether/Sprite.vs.hlsl b/Shaders/Material/ParticleSprite.vs.hlsl similarity index 61% rename from Shaders/Aether/Sprite.vs.hlsl rename to Shaders/Material/ParticleSprite.vs.hlsl index 06a49606..901bf15b 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,44 +35,37 @@ struct VSInput struct VSOutput { - float4 ClipPos : SV_POSITION; - float4 Color : COLOR; - float2 TexCoord : TEXCOORD0; - float3 ViewPos : POSITION0; + float4 ClipPos : SV_POSITION; + float2 TexCoord : TEXCOORD0; + float2 TexCoord1 : TEXCOORD1; + float3 ViewPos : POSITION0; }; -// 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; - 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 ) From 2e2024df4541adc4b6c53501f8c600bf4e805d6e Mon Sep 17 00:00:00 2001 From: MrChampz Date: Thu, 1 Oct 2026 02:55:09 -0300 Subject: [PATCH 54/61] Tidy Vulkan presentation handling --- Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp index 0bdab6c4..df4e097c 100644 --- a/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp +++ b/Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp @@ -879,7 +879,6 @@ namespace Elixir presentInfo.pImageIndices = &m_CurrentSwapchainImageIndex; const auto result = vkQueuePresentKHR(m_GraphicsQueue, &presentInfo); - if (result == VK_ERROR_OUT_OF_DATE_KHR) { m_SwapchainRecreateRequested = true; From dcb510ccdbdef364b4668183effee79fbb81b4f4 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Thu, 1 Oct 2026 13:54:42 -0300 Subject: [PATCH 55/61] fix(material): reject cyclic material graphs --- .../Source/Engine/Materials/MaterialGraph.cpp | 38 +++++++++++++++++++ .../Source/Engine/Materials/MaterialGraph.h | 6 +++ .../Tests/Engine/Materials/MaterialTest.cpp | 16 ++++++++ 3 files changed, 60 insertions(+) diff --git a/Elixir/Source/Engine/Materials/MaterialGraph.cpp b/Elixir/Source/Engine/Materials/MaterialGraph.cpp index 9c20bdf7..44ed85df 100644 --- a/Elixir/Source/Engine/Materials/MaterialGraph.cpp +++ b/Elixir/Source/Engine/Materials/MaterialGraph.cpp @@ -108,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 e5ec8988..49b4ec87 100644 --- a/Elixir/Source/Engine/Materials/MaterialGraph.h +++ b/Elixir/Source/Engine/Materials/MaterialGraph.h @@ -96,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/Tests/Engine/Materials/MaterialTest.cpp b/Elixir/Tests/Engine/Materials/MaterialTest.cpp index 62a718f6..f7bc4ad8 100644 --- a/Elixir/Tests/Engine/Materials/MaterialTest.cpp +++ b/Elixir/Tests/Engine/Materials/MaterialTest.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include @@ -84,6 +85,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"); From 8c51232a30d64704dd647b1526f856408a3b312d Mon Sep 17 00:00:00 2001 From: MrChampz Date: Thu, 1 Oct 2026 14:07:50 -0300 Subject: [PATCH 56/61] test(material): cover unconnected unary nodes --- .../Tests/Engine/Materials/MaterialGraphTest.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp b/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp index ef24a5c1..021c06f6 100644 --- a/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp +++ b/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -15,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -72,6 +74,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; From adc3b625ea0fe65c91b486e4e8b6c313e4757e46 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Thu, 1 Oct 2026 14:12:05 -0300 Subject: [PATCH 57/61] fix(material): light particle mesh normals --- Shaders/Material/ParticleMesh.ps.hlsl | 29 ++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/Shaders/Material/ParticleMesh.ps.hlsl b/Shaders/Material/ParticleMesh.ps.hlsl index d6edfb8b..e7f134a4 100644 --- a/Shaders/Material/ParticleMesh.ps.hlsl +++ b/Shaders/Material/ParticleMesh.ps.hlsl @@ -76,11 +76,38 @@ float4 main(PSInput input) : SV_Target0 surface.Opacity = 1.0f; surface.Emissive = float3(0.0f, 0.0f, 0.0f); - const float3 N = normalize(input.Normal); + const float3 geometricNormal = normalize(input.Normal); + float3 N = geometricNormal; const float3 V = normalize(CameraPos - input.WorldPos); // __GRAPH_BODY__ + 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) { From affe755245d046d91ed4b6f1570dc001e2ebf23f Mon Sep 17 00:00:00 2001 From: MrChampz Date: Thu, 1 Oct 2026 16:53:30 -0300 Subject: [PATCH 58/61] fix(material): retire unused texture bindings --- .../Engine/Materials/Rendering/Renderer.cpp | 9 +++- .../Materials/Rendering/TextureRegistry.cpp | 46 +++++++++++++++++-- .../Materials/Rendering/TextureRegistry.h | 20 +++++++- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp b/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp index 0329f469..94339b64 100644 --- a/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp +++ b/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp @@ -45,7 +45,6 @@ namespace Elixir::Materials::Rendering 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.") @@ -54,6 +53,12 @@ namespace Elixir::Materials::Rendering "Material rendering requires BeginFrame for the current graphics frame." ) + if (scenes.empty()) + { + m_Textures.EndFrame(); + return result; + } + FrameTable materialTable{ m_MaterialCapacity, m_Textures.GetFallbackIndex(), @@ -69,6 +74,8 @@ namespace Elixir::Materials::Rendering for (const auto& scene : scenes) preparedScenes.push_back(PrepareScene(scene, materialTable)); + m_Textures.EndFrame(); + if (!materialTable.GetData().empty()) { GetActiveMaterialBuffer()->UpdateData( 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 2dd50f81..79fbd0f6 100644 --- a/Elixir/Source/Engine/Materials/Rendering/TextureRegistry.h +++ b/Elixir/Source/Engine/Materials/Rendering/TextureRegistry.h @@ -27,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. * @@ -80,13 +87,22 @@ namespace Elixir::Materials::Rendering } }; + /** @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 +} From de12cf2b4ef01b1624bbff009d732c0b0edf5726 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Thu, 1 Oct 2026 18:12:47 -0300 Subject: [PATCH 59/61] Add particle color material support --- Assets/VFX/RibbonGarden.json | 2 +- Assets/VFX/RibbonVortex.json | 2 +- Dissolve/Source/Dissolve.cpp | 55 ++++--------------- .../Engine/Aether/Effect/MaterialFactory.cpp | 29 +++++++--- .../Engine/Materials/Compilation/Compiler.cpp | 6 +- .../Engine/Materials/Compilation/Compiler.h | 15 ++--- .../Engine/Materials/DefaultMaterials.cpp | 23 +++++--- Elixir/Source/Engine/Materials/Material.cpp | 25 ++------- Elixir/Source/Engine/Materials/Material.h | 46 +++++----------- Elixir/Source/Engine/Materials/MaterialNode.h | 4 ++ Elixir/Source/Engine/Materials/Nodes/Color.h | 36 ++++++++++++ .../Engine/Materials/Rendering/Renderer.cpp | 2 +- .../Platform/GLTF/GLTFStaticMeshLoader.cpp | 3 +- .../Aether/Effect/MaterialResolverTest.cpp | 2 +- Elixir/Tests/Engine/Aether/SystemTest.cpp | 36 ++++++------ .../Compilation/CompilationCacheTest.cpp | 8 +-- .../Materials/Compilation/CompilerTest.cpp | 9 +-- .../Engine/Materials/MaterialGraphTest.cpp | 11 ++++ .../Materials/MaterialProxyCacheTest.cpp | 5 +- .../Engine/Materials/MaterialRegistryTest.cpp | 2 +- .../Tests/Engine/Materials/MaterialTest.cpp | 22 ++++++++ .../Materials/Rendering/FrameTableTest.cpp | 3 +- Shaders/Material/ParticleMesh.ps.hlsl | 1 + Shaders/Material/ParticleMesh.vs.hlsl | 3 + Shaders/Material/ParticleRibbon.ps.hlsl | 1 + Shaders/Material/ParticleRibbon.vs.hlsl | 3 + Shaders/Material/ParticleSprite.ps.hlsl | 1 + Shaders/Material/ParticleSprite.vs.hlsl | 2 + 28 files changed, 192 insertions(+), 165 deletions(-) create mode 100644 Elixir/Source/Engine/Materials/Nodes/Color.h 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/Source/Dissolve.cpp b/Dissolve/Source/Dissolve.cpp index 54168425..e134c9af 100644 --- a/Dissolve/Source/Dissolve.cpp +++ b/Dissolve/Source/Dissolve.cpp @@ -1,5 +1,6 @@ #include "Dissolve.h" +#include "Engine/Materials/Nodes/Color.h" #include "Engine/Materials/Nodes/Parameter.h" #include @@ -84,11 +85,7 @@ Dissolve::Dissolve() { MaterialGraph graph; - graphMaterial = CreateRef("DissolveGraph"); - EE_CORE_ASSERT( - graphMaterial->SetUsage(EMaterialUsage::Particle, true), - "Dissolve graph material must enable Particle usage." - ) + graphMaterial = CreateRef("DissolveGraph", EMaterialUsage::Particle); EE_CORE_ASSERT( graphMaterial->SetBlendMode(EMaterialBlendMode::Translucent), "Dissolve graph material must enable translucent blending." @@ -140,11 +137,7 @@ Dissolve::Dissolve() { MaterialGraph graph1; - const auto ribbonMaterial = CreateRef("RibbonEnergy"); - EE_CORE_ASSERT( - ribbonMaterial->SetUsage(EMaterialUsage::Particle, true), - "Ribbon material must enable Particle usage." - ) + const auto ribbonMaterial = CreateRef("RibbonEnergy", EMaterialUsage::Particle); EE_CORE_ASSERT( ribbonMaterial->SetBlendMode(EMaterialBlendMode::Translucent), "Ribbon material must enable translucent blending." @@ -153,34 +146,20 @@ Dissolve::Dissolve() 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 }), - }), "") - - EE_CORE_ASSERT(ribbonMaterial->DefineParameter("Albedo", { - .Kind = EMaterialParameterKind::Texture, - .DefaultValue = SMaterialParameter::MakeTexture(tex), + .DefaultValue = SMaterialParameter::MakeVector({ 1.0f, 1.0f, 1.0f, 1.0f }), }), "") - 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); @@ -188,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(); diff --git a/Elixir/Source/Engine/Aether/Effect/MaterialFactory.cpp b/Elixir/Source/Engine/Aether/Effect/MaterialFactory.cpp index 8bbfd3b6..104e2627 100644 --- a/Elixir/Source/Engine/Aether/Effect/MaterialFactory.cpp +++ b/Elixir/Source/Engine/Aether/Effect/MaterialFactory.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include namespace Elixir::Aether::Effect @@ -17,12 +18,12 @@ namespace Elixir::Aether::Effect const SMaterialDescription& desc ) { - const auto material = CreateRef(std::move(name)); - - auto result = material->SetUsage(EMaterialUsage::Particle, true); - EE_CORE_ASSERT(result, "Particle material usage must be enabled.") + const auto material = CreateRef( + std::move(name), + EMaterialUsage::Particle + ); - result = material->SetBlendMode(EMaterialBlendMode::Translucent); + const auto result = material->SetBlendMode(EMaterialBlendMode::Translucent); EE_CORE_ASSERT(result, "Particle materials must enable translucent blending.") MaterialGraph graph; @@ -45,6 +46,20 @@ namespace Elixir::Aether::Effect ); graph.SetChannel(EMaterialChannel::Emissive, emissive); + 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"; @@ -59,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/Materials/Compilation/Compiler.cpp b/Elixir/Source/Engine/Materials/Compilation/Compiler.cpp index 55cfe4cc..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,13 +124,13 @@ namespace Elixir::Materials::Compilation auto result = Build(material); if (!result) return result; - if (material.SupportsUsage(EMaterialUsage::Surface)) + if (material.GetUsage() == EMaterialUsage::Surface) { result = CompileSurface(loader, material, std::move(result)); if (!result) return result; } - if (material.SupportsUsage(EMaterialUsage::Particle)) + if (material.GetUsage() == EMaterialUsage::Particle) { result = CompileParticleSprite(loader, material, std::move(result)); if (!result) return result; diff --git a/Elixir/Source/Engine/Materials/Compilation/Compiler.h b/Elixir/Source/Engine/Materials/Compilation/Compiler.h index f3f2bcaf..40ed96dd 100644 --- a/Elixir/Source/Engine/Materials/Compilation/Compiler.h +++ b/Elixir/Source/Engine/Materials/Compilation/Compiler.h @@ -40,8 +40,8 @@ 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; @@ -58,15 +58,8 @@ namespace Elixir::Materials::Compilation /** @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 shader for a compiled material variant. diff --git a/Elixir/Source/Engine/Materials/DefaultMaterials.cpp b/Elixir/Source/Engine/Materials/DefaultMaterials.cpp index 0dfe5311..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 { @@ -18,14 +21,11 @@ namespace Elixir::Materials const EMaterialBlendMode blendMode = EMaterialBlendMode::Opaque ) { - const auto material = CreateRef(std::move(name)); - - auto result = material->SetUsage(usage, true); - EE_CORE_ASSERT(result, "Default material usage must be enabled.") + const auto material = CreateRef(std::move(name), usage); if (blendMode != EMaterialBlendMode::Opaque) { - result = material->SetBlendMode(blendMode); + const auto result = material->SetBlendMode(blendMode); EE_CORE_ASSERT(result, "Default material blend mode must be enabled.") } @@ -56,14 +56,23 @@ namespace Elixir::Materials glm::vec4{ 1.0f, 1.0f, 1.0f, 0.0f }, EMaterialValueType::Float3 ); - graph.SetChannel(EMaterialChannel::BaseColor, baseColor); + 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 ); - graph.SetChannel(EMaterialChannel::Opacity, opacity); + 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.Particle", diff --git a/Elixir/Source/Engine/Materials/Material.cpp b/Elixir/Source/Engine/Materials/Material.cpp index 4c923565..9830658a 100644 --- a/Elixir/Source/Engine/Materials/Material.cpp +++ b/Elixir/Source/Engine/Materials/Material.cpp @@ -16,26 +16,6 @@ namespace Elixir::Materials ++m_Revision; } - bool Material::SetUsage(const EMaterialUsage usage, const bool enabled) - { - const uint32_t mask = GetMaterialUsageMask(usage); - const uint32_t updatedMask = enabled - ? m_UsageMask | mask - : m_UsageMask & ~mask; - - if (updatedMask == m_UsageMask) - return false; - - m_UsageMask = updatedMask; - ++m_Revision; - return true; - } - - bool Material::SupportsUsage(const EMaterialUsage usage) const - { - return (m_UsageMask & GetMaterialUsageMask(usage)) != 0; - } - bool Material::SetShadingModel(EMaterialShadingModel model) { if (m_ShadingModel == model) @@ -146,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 f695768f..74c56955 100644 --- a/Elixir/Source/Engine/Materials/Material.h +++ b/Elixir/Source/Engine/Materials/Material.h @@ -8,9 +8,9 @@ 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 @@ -71,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. @@ -105,20 +109,8 @@ namespace Elixir::Materials */ const MaterialGraph& GetGraph() const { return m_Graph; } - /** - * @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. - */ - bool SetUsage(EMaterialUsage usage, bool enabled); - - /** - * @brief Checks whether a material usage is supported. - * @param usage Usage to check. - * @return `true` if the usage is enabled. - */ - bool SupportsUsage(EMaterialUsage usage) const; + /** @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. @@ -222,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; } @@ -238,7 +227,7 @@ 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; @@ -246,13 +235,4 @@ namespace Elixir::Materials 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/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/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/Rendering/Renderer.cpp b/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp index 94339b64..9ad66ff8 100644 --- a/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp +++ b/Elixir/Source/Engine/Materials/Rendering/Renderer.cpp @@ -131,7 +131,7 @@ 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(GetShaderVariant(pass)); diff --git a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp index d3c6f606..20f64a77 100644 --- a/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp +++ b/Elixir/Source/Platform/GLTF/GLTFStaticMeshLoader.cpp @@ -471,8 +471,7 @@ namespace Elixir ? std::string(m_MeshName) + ".Material." + std::to_string(materialIndex) : std::string(source.name); - const auto material = CreateRef(name); - material->SetUsage(EMaterialUsage::Surface, true); + const auto material = CreateRef(name, EMaterialUsage::Surface); material->SetBlendMode(GetBlendMode(source.alphaMode)); material->SetDoubleSided(source.doubleSided); diff --git a/Elixir/Tests/Engine/Aether/Effect/MaterialResolverTest.cpp b/Elixir/Tests/Engine/Aether/Effect/MaterialResolverTest.cpp index ac320a52..c8f87d62 100644 --- a/Elixir/Tests/Engine/Aether/Effect/MaterialResolverTest.cpp +++ b/Elixir/Tests/Engine/Aether/Effect/MaterialResolverTest.cpp @@ -30,7 +30,7 @@ TEST(MaterialResolverTest, CreatesAuthoredMaterialsAndUsesUsageDefaults) ASSERT_TRUE(sprite.GetMaterial()); EXPECT_NE(sprite.GetMaterial()->GetParent(), registry.GetDefault(EMaterialUsage::Particle)); - EXPECT_TRUE(sprite.GetMaterial()->GetParent()->SupportsUsage(EMaterialUsage::Particle)); + EXPECT_EQ(sprite.GetMaterial()->GetParent()->GetUsage(), EMaterialUsage::Particle); ASSERT_TRUE(ribbon.GetMaterial()); EXPECT_EQ(ribbon.GetMaterial()->GetParent(), registry.GetDefault(EMaterialUsage::Particle)); diff --git a/Elixir/Tests/Engine/Aether/SystemTest.cpp b/Elixir/Tests/Engine/Aether/SystemTest.cpp index 2a6a023d..4734c7b4 100644 --- a/Elixir/Tests/Engine/Aether/SystemTest.cpp +++ b/Elixir/Tests/Engine/Aether/SystemTest.cpp @@ -129,8 +129,7 @@ TEST(SystemTest, FindsNamedEmitterForMaterialPublication) TEST(SystemTest, CompilePublishesParticleMaterialInstance) { - const auto material = CreateRef("Particle tint"); - ASSERT_TRUE(material->SetUsage(EMaterialUsage::Particle, 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, CompilePublishesParticleMaterialInstance) ASSERT_EQ(first.Emitters.size(), 1); ASSERT_TRUE(first.Emitters[0].Material); - EXPECT_TRUE(first.Emitters[0].Material->GetParent()->SupportsUsage( - EMaterialUsage::Particle - )); + 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, CompilePublishesParticleMaterialInstance) TEST(SystemTest, CompileSnapshotsParticleRibbonMaterialForRenderData) { - const auto material = CreateRef("Particle ribbon"); - ASSERT_TRUE(material->SetUsage(EMaterialUsage::Particle, 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,15 @@ TEST(SystemTest, CompileSnapshotsParticleRibbonMaterialForRenderData) ASSERT_EQ(compiled.Emitters.size(), 1); ASSERT_TRUE(compiled.Emitters[0].Material); - EXPECT_TRUE(compiled.Emitters[0].Material->GetParent()->SupportsUsage( + EXPECT_EQ( + compiled.Emitters[0].Material->GetParent()->GetUsage(), EMaterialUsage::Particle - )); + ); } TEST(SystemTest, CompilePreservesParticleMaterialWhenRenderModeChanges) { - const auto material = CreateRef("Particle material"); - ASSERT_TRUE(material->SetUsage(EMaterialUsage::Particle, true)); + const auto material = CreateRef("Particle material", EMaterialUsage::Particle); const auto system = CreateRef("Particle render mode contract"); auto& emitter = system->AddEmitter("Emitter", 8, 0.0f); @@ -201,15 +197,15 @@ TEST(SystemTest, CompilePreservesParticleMaterialWhenRenderModeChanges) ASSERT_EQ(compiled.Emitters.size(), 1); EXPECT_EQ(compiled.Emitters[0].RenderMode, EParticleRenderMode::Ribbon); EXPECT_EQ(compiled.Emitters[0].Material, instance); - EXPECT_TRUE(compiled.Emitters[0].Material->GetParent()->SupportsUsage( + EXPECT_EQ( + compiled.Emitters[0].Material->GetParent()->GetUsage(), EMaterialUsage::Particle - )); + ); } TEST(SystemTest, CompileSnapshotsParticleMeshMaterialForRenderData) { - const auto material = CreateRef("Particle mesh"); - material->SetUsage(EMaterialUsage::Particle, true); + const auto material = CreateRef("Particle mesh", EMaterialUsage::Particle); const auto system = CreateRef("Mesh material"); auto& emitter = system->AddEmitter("Mesh", 8, 0.0f); @@ -222,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( + EXPECT_EQ( + compiled.Emitters[0].Material->GetParent()->GetUsage(), EMaterialUsage::Particle - )); + ); } TEST(SystemTest, CompileAssignsTheDefaultMaterialWhenNoneIsExplicit) @@ -236,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( + EXPECT_EQ( + compiled.Emitters[0].Material->GetParent()->GetUsage(), EMaterialUsage::Particle - )); + ); } diff --git a/Elixir/Tests/Engine/Materials/Compilation/CompilationCacheTest.cpp b/Elixir/Tests/Engine/Materials/Compilation/CompilationCacheTest.cpp index 6d93979b..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::Particle, 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::Surface, 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::Surface)); + 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 a302ffbf..1a36831d 100644 --- a/Elixir/Tests/Engine/Materials/Compilation/CompilerTest.cpp +++ b/Elixir/Tests/Engine/Materials/Compilation/CompilerTest.cpp @@ -32,17 +32,14 @@ TEST(CompilerTest, AssignsStableSlotsByParameterKindAndName) EXPECT_EQ(result.Material->Parameters[1].Slot, 0); } -TEST(CompilerTest, PreservesEnabledRendererUsages) +TEST(CompilerTest, CompilesOnlyTheSelectedRendererUsage) { - const auto material = CreateRef("Material"); - ASSERT_TRUE(material->SetUsage(EMaterialUsage::Surface, true)); - ASSERT_TRUE(material->SetUsage(EMaterialUsage::Particle, true)); + const auto material = CreateRef("Material", EMaterialUsage::Particle); const auto result = Compiler::Build(*material); ASSERT_TRUE(result); - EXPECT_TRUE(result.Material->SupportsUsage(EMaterialUsage::Surface)); - EXPECT_TRUE(result.Material->SupportsUsage(EMaterialUsage::Particle)); + EXPECT_EQ(result.Material->GetUsage(), EMaterialUsage::Particle); } TEST(CompilerTest, DoesNotAliasParticleUsageShadersToSurfaceShader) diff --git a/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp b/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp index 021c06f6..0148ffe4 100644 --- a/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp +++ b/Elixir/Tests/Engine/Materials/MaterialGraphTest.cpp @@ -20,6 +20,7 @@ #include #include #include +#include using namespace Elixir; using namespace Elixir::Materials; @@ -102,6 +103,16 @@ 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; diff --git a/Elixir/Tests/Engine/Materials/MaterialProxyCacheTest.cpp b/Elixir/Tests/Engine/Materials/MaterialProxyCacheTest.cpp index fdc9206d..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::Particle, 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 8845a088..7f8feede 100644 --- a/Elixir/Tests/Engine/Materials/MaterialRegistryTest.cpp +++ b/Elixir/Tests/Engine/Materials/MaterialRegistryTest.cpp @@ -17,7 +17,7 @@ TEST(MaterialRegistryTest, RegistersAndFindsDefaultMaterials) 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 f7bc4ad8..713ef099 100644 --- a/Elixir/Tests/Engine/Materials/MaterialTest.cpp +++ b/Elixir/Tests/Engine/Materials/MaterialTest.cpp @@ -4,6 +4,7 @@ #include #include #include +#include using namespace Elixir; using namespace Elixir::Materials; @@ -13,6 +14,7 @@ TEST(MaterialTest, StoresSurfaceShadingModel) { Material material("ClearCoated"); + EXPECT_EQ(material.GetUsage(), EMaterialUsage::Surface); EXPECT_EQ(material.GetShadingModel(), EMaterialShadingModel::Lit); const auto revision = material.GetRevision(); @@ -118,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 3e4e9f6f..ee80a4d9 100644 --- a/Elixir/Tests/Engine/Materials/Rendering/FrameTableTest.cpp +++ b/Elixir/Tests/Engine/Materials/Rendering/FrameTableTest.cpp @@ -22,8 +22,7 @@ namespace TEST(FrameTableTest, DeduplicatesAProxyAndPreservesItsValues) { - auto material = CreateRef("Particle material"); - ASSERT_TRUE(material->SetUsage(EMaterialUsage::Particle, true)); + auto material = CreateRef("Particle material", EMaterialUsage::Particle); ASSERT_TRUE(material->DefineParameter("Tint", { .Kind = EMaterialParameterKind::Value, .ValueType = EMaterialValueType::Float4, diff --git a/Shaders/Material/ParticleMesh.ps.hlsl b/Shaders/Material/ParticleMesh.ps.hlsl index e7f134a4..f26a8a29 100644 --- a/Shaders/Material/ParticleMesh.ps.hlsl +++ b/Shaders/Material/ParticleMesh.ps.hlsl @@ -44,6 +44,7 @@ struct PSInput float3 Normal : NORMAL0; float2 TexCoord : TEXCOORD0; float2 TexCoord1 : TEXCOORD1; + float4 Color : COLOR; }; static const float3 LIGHT_DIRECTION = float3(-0.45, 0.8, 0.55); diff --git a/Shaders/Material/ParticleMesh.vs.hlsl b/Shaders/Material/ParticleMesh.vs.hlsl index 3ec808a2..5996c91d 100644 --- a/Shaders/Material/ParticleMesh.vs.hlsl +++ b/Shaders/Material/ParticleMesh.vs.hlsl @@ -64,6 +64,7 @@ struct VSInput float4 VelocityAge : TEXCOORD0; float4 Transform : TEXCOORD1; float4 TangentRibbonId : TANGENT; + float4 Color : COLOR; float4 Metadata : TEXCOORD2; }; @@ -74,6 +75,7 @@ struct VSOutput float3 Normal : NORMAL0; float2 TexCoord : TEXCOORD0; float2 TexCoord1 : TEXCOORD1; + float4 Color : COLOR; }; float Hash01(uint x) @@ -116,6 +118,7 @@ VSOutput main(VSInput input) output.Normal = normalize(mul((float3x3)pc.WorldTransform, localNormal)); output.TexCoord = input.LocalPos.xy * 0.5f + 0.5f; output.TexCoord1 = output.TexCoord; + output.Color = input.Color; return output; } diff --git a/Shaders/Material/ParticleRibbon.ps.hlsl b/Shaders/Material/ParticleRibbon.ps.hlsl index 5ed38023..98bc7d33 100644 --- a/Shaders/Material/ParticleRibbon.ps.hlsl +++ b/Shaders/Material/ParticleRibbon.ps.hlsl @@ -46,6 +46,7 @@ struct PSInput nointerpolation float Valid : TEXCOORD1; float2 TexCoord1 : TEXCOORD2; float3 WorldPos : POSITION0; + float4 Color : COLOR; }; struct Surface diff --git a/Shaders/Material/ParticleRibbon.vs.hlsl b/Shaders/Material/ParticleRibbon.vs.hlsl index 629c3ec3..39b2ed4e 100644 --- a/Shaders/Material/ParticleRibbon.vs.hlsl +++ b/Shaders/Material/ParticleRibbon.vs.hlsl @@ -54,6 +54,7 @@ struct VSOutput nointerpolation float Valid : TEXCOORD1; float2 TexCoord1 : TEXCOORD2; float3 WorldPos : POSITION0; + float4 Color : COLOR; }; float3 SafeNormalize(float3 value, float3 fallback) @@ -195,6 +196,7 @@ VSOutput EmptyVertex() 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; } @@ -274,6 +276,7 @@ VSOutput main(uint vertexId : SV_VertexID) 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; } diff --git a/Shaders/Material/ParticleSprite.ps.hlsl b/Shaders/Material/ParticleSprite.ps.hlsl index 3b5aa224..c1573e16 100644 --- a/Shaders/Material/ParticleSprite.ps.hlsl +++ b/Shaders/Material/ParticleSprite.ps.hlsl @@ -43,6 +43,7 @@ struct PSInput float2 TexCoord : TEXCOORD0; float2 TexCoord1 : TEXCOORD1; float3 ViewPos : POSITION0; + float4 Color : COLOR; }; struct Surface diff --git a/Shaders/Material/ParticleSprite.vs.hlsl b/Shaders/Material/ParticleSprite.vs.hlsl index 901bf15b..9464128f 100644 --- a/Shaders/Material/ParticleSprite.vs.hlsl +++ b/Shaders/Material/ParticleSprite.vs.hlsl @@ -39,6 +39,7 @@ struct VSOutput float2 TexCoord : TEXCOORD0; float2 TexCoord1 : TEXCOORD1; float3 ViewPos : POSITION0; + float4 Color : COLOR; }; float MaxAxisScale(float4x4 transform) @@ -67,5 +68,6 @@ VSOutput main(VSInput input, uint vertexId : SV_VertexID) output.TexCoord = normalizedPos; output.TexCoord1 = normalizedPos; output.ViewPos = viewPos; + output.Color = input.Color; return output; } From 2c90626b60a5ee288c351e5ce3b18a1b1c93eee2 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Thu, 1 Oct 2026 18:19:41 -0300 Subject: [PATCH 60/61] fix(aether): refresh authored effect materials --- .../Engine/Aether/Effect/MaterialResolver.cpp | 31 ++++++++++++++----- .../Engine/Materials/MaterialRegistry.cpp | 11 +++++++ .../Engine/Materials/MaterialRegistry.h | 7 +++++ .../Aether/Effect/MaterialResolverTest.cpp | 23 ++++++++++++++ 4 files changed, 65 insertions(+), 7 deletions(-) diff --git a/Elixir/Source/Engine/Aether/Effect/MaterialResolver.cpp b/Elixir/Source/Engine/Aether/Effect/MaterialResolver.cpp index 1b98db42..060ac5bc 100644 --- a/Elixir/Source/Engine/Aether/Effect/MaterialResolver.cpp +++ b/Elixir/Source/Engine/Aether/Effect/MaterialResolver.cpp @@ -14,21 +14,33 @@ 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 (const auto& assigned = emitter->GetMaterial(); assigned + && assigned->GetParent()->GetName() != name) + continue; + material = m_Registry.Find(name); - if (!material) + const auto refreshed = CreateMaterial(name, *desc); + if (material) { - material = CreateMaterial(name, *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,6 +50,11 @@ namespace Elixir::Aether::Effect } else { + // A caller may replace a default instance before creating a + // SystemInstance. Do not overwrite that explicit choice. + if (emitter->GetMaterial()) + continue; + material = m_Registry.GetDefault(EMaterialUsage::Particle); } 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/Tests/Engine/Aether/Effect/MaterialResolverTest.cpp b/Elixir/Tests/Engine/Aether/Effect/MaterialResolverTest.cpp index c8f87d62..258b2e56 100644 --- a/Elixir/Tests/Engine/Aether/Effect/MaterialResolverTest.cpp +++ b/Elixir/Tests/Engine/Aether/Effect/MaterialResolverTest.cpp @@ -37,3 +37,26 @@ TEST(MaterialResolverTest, CreatesAuthoredMaterialsAndUsesUsageDefaults) 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() + ); +} From bd6c7d76e0a8451768ca4a4e829ab405dc4593c5 Mon Sep 17 00:00:00 2001 From: MrChampz Date: Thu, 1 Oct 2026 18:21:06 -0300 Subject: [PATCH 61/61] fix(aether): track resolver material ownership --- .../Engine/Aether/Effect/MaterialResolver.cpp | 7 +++---- Elixir/Source/Engine/Aether/Emitter.h | 21 ++++++++++++++++++- .../Aether/Effect/MaterialResolverTest.cpp | 18 ++++++++++++++++ 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/Elixir/Source/Engine/Aether/Effect/MaterialResolver.cpp b/Elixir/Source/Engine/Aether/Effect/MaterialResolver.cpp index 060ac5bc..8f344fb0 100644 --- a/Elixir/Source/Engine/Aether/Effect/MaterialResolver.cpp +++ b/Elixir/Source/Engine/Aether/Effect/MaterialResolver.cpp @@ -22,8 +22,7 @@ namespace Elixir::Aether::Effect // Preserve a caller-selected material, but recreate the material // previously generated for this emitter when its asset data changes. - if (const auto& assigned = emitter->GetMaterial(); assigned - && assigned->GetParent()->GetName() != name) + if (emitter->GetMaterial() && !emitter->HasResolvedMaterial()) continue; material = m_Registry.Find(name); @@ -52,13 +51,13 @@ namespace Elixir::Aether::Effect { // A caller may replace a default instance before creating a // SystemInstance. Do not overwrite that explicit choice. - if (emitter->GetMaterial()) + 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/Tests/Engine/Aether/Effect/MaterialResolverTest.cpp b/Elixir/Tests/Engine/Aether/Effect/MaterialResolverTest.cpp index 258b2e56..37eb42c1 100644 --- a/Elixir/Tests/Engine/Aether/Effect/MaterialResolverTest.cpp +++ b/Elixir/Tests/Engine/Aether/Effect/MaterialResolverTest.cpp @@ -60,3 +60,21 @@ TEST(MaterialResolverTest, RefreshesAuthoredMaterialsWhenDescriptionsChange) 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); +}