diff --git a/engine/include/XCEngine/Rendering/Passes/BuiltinObjectIdOutlinePass.h b/engine/include/XCEngine/Rendering/Passes/BuiltinObjectIdOutlinePass.h index 2558b8a5..a50ec088 100644 --- a/engine/include/XCEngine/Rendering/Passes/BuiltinObjectIdOutlinePass.h +++ b/engine/include/XCEngine/Rendering/Passes/BuiltinObjectIdOutlinePass.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -12,6 +13,7 @@ #include #include #include +#include #include #include @@ -62,6 +64,7 @@ private: RHI::RHIDescriptorSet* m_constantSet = nullptr; RHI::RHIDescriptorPool* m_texturePool = nullptr; RHI::RHIDescriptorSet* m_textureSet = nullptr; + Resources::ResourceHandle m_builtinObjectIdOutlineShader; }; } // namespace Passes diff --git a/engine/include/XCEngine/Resources/BuiltinResources.h b/engine/include/XCEngine/Resources/BuiltinResources.h index 650e9238..e87cc526 100644 --- a/engine/include/XCEngine/Resources/BuiltinResources.h +++ b/engine/include/XCEngine/Resources/BuiltinResources.h @@ -25,6 +25,7 @@ Containers::String GetBuiltinPrimitiveMeshPath(BuiltinPrimitiveType primitiveTyp Containers::String GetBuiltinDefaultPrimitiveMaterialPath(); Containers::String GetBuiltinForwardLitShaderPath(); Containers::String GetBuiltinObjectIdShaderPath(); +Containers::String GetBuiltinObjectIdOutlineShaderPath(); Containers::String GetBuiltinInfiniteGridShaderPath(); Containers::String GetBuiltinDefaultPrimitiveTexturePath(); diff --git a/engine/src/Core/Asset/AssetDatabase.cpp b/engine/src/Core/Asset/AssetDatabase.cpp index 7bcd7a1c..f9e247d1 100644 --- a/engine/src/Core/Asset/AssetDatabase.cpp +++ b/engine/src/Core/Asset/AssetDatabase.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include namespace XCEngine { @@ -48,6 +49,32 @@ Containers::String NormalizeArtifactPathString(const Containers::String& path) { return ToContainersString(fs::path(path.CStr()).lexically_normal().generic_string()); } +bool IsProjectRelativePath(const fs::path& path) { + const std::string generic = path.generic_string(); + return !generic.empty() && + generic != "." && + generic != ".." && + generic.rfind("../", 0) != 0; +} + +void AddUniqueDependencyPath(const fs::path& path, + std::unordered_set& seenPaths, + std::vector& outPaths) { + if (path.empty()) { + return; + } + + const fs::path normalizedPath = path.lexically_normal(); + const std::string key = normalizedPath.generic_string(); + if (key.empty()) { + return; + } + + if (seenPaths.insert(key).second) { + outPaths.push_back(normalizedPath); + } +} + std::string TrimCopy(const std::string& text) { const auto begin = std::find_if_not(text.begin(), text.end(), [](unsigned char ch) { return std::isspace(ch) != 0; @@ -69,6 +96,91 @@ std::string ToLowerCopy(std::string text) { return text; } +std::vector SplitWhitespaceTokens(const std::string& text) { + std::vector tokens; + std::istringstream stream(text); + std::string token; + while (stream >> token) { + tokens.push_back(token); + } + return tokens; +} + +std::vector CollectObjDeclaredDependencyPaths(const fs::path& sourcePath) { + std::vector dependencies; + std::unordered_set seenPaths; + + std::ifstream input(sourcePath); + if (!input.is_open()) { + return dependencies; + } + + const fs::path sourceDirectory = sourcePath.parent_path(); + std::string line; + while (std::getline(input, line)) { + const std::string trimmed = TrimCopy(line); + if (trimmed.empty() || trimmed[0] == '#') { + continue; + } + + const std::string lowered = ToLowerCopy(trimmed); + if (lowered.rfind("mtllib", 0) != 0 || + (trimmed.size() > 6 && std::isspace(static_cast(trimmed[6])) == 0)) { + continue; + } + + const std::string remainder = TrimCopy(trimmed.substr(6)); + for (const std::string& token : SplitWhitespaceTokens(remainder)) { + AddUniqueDependencyPath(sourceDirectory / token, seenPaths, dependencies); + } + } + + return dependencies; +} + +std::vector CollectMtlDeclaredDependencyPaths(const fs::path& mtlPath) { + std::vector dependencies; + std::unordered_set seenPaths; + + std::ifstream input(mtlPath); + if (!input.is_open()) { + return dependencies; + } + + const fs::path sourceDirectory = mtlPath.parent_path(); + std::string line; + while (std::getline(input, line)) { + const std::string trimmed = TrimCopy(line); + if (trimmed.empty() || trimmed[0] == '#') { + continue; + } + + const std::vector tokens = SplitWhitespaceTokens(trimmed); + if (tokens.size() < 2) { + continue; + } + + const std::string keyword = ToLowerCopy(tokens.front()); + const bool isTextureDirective = + keyword.rfind("map_", 0) == 0 || + keyword == "bump" || + keyword == "disp" || + keyword == "decal" || + keyword == "refl" || + keyword == "norm"; + if (!isTextureDirective) { + continue; + } + + const std::string& textureToken = tokens.back(); + if (!textureToken.empty() && textureToken[0] != '-') { + AddUniqueDependencyPath(sourceDirectory / textureToken, seenPaths, dependencies); + } + } + + return dependencies; +} + std::string EscapeField(const std::string& value) { std::string escaped; escaped.reserve(value.size()); @@ -589,6 +701,16 @@ void AssetDatabase::LoadArtifactDB() { record.sourceFileSize = static_cast(std::stoull(fields[9])); record.sourceWriteTime = fields.size() > 10 ? static_cast(std::stoull(fields[10])) : 0; record.mainLocalID = fields.size() > 11 ? static_cast(std::stoull(fields[11])) : kMainAssetLocalID; + for (size_t index = 12; index + 3 < fields.size(); index += 4) { + ArtifactDependencyRecord dependency; + dependency.path = ToContainersString(fields[index + 0]); + dependency.hash = ToContainersString(fields[index + 1]); + dependency.fileSize = static_cast(std::stoull(fields[index + 2])); + dependency.writeTime = static_cast(std::stoull(fields[index + 3])); + if (!dependency.path.Empty()) { + record.dependencies.push_back(dependency); + } + } if (!record.assetGuid.IsValid() || record.artifactKey.Empty()) { continue; @@ -604,7 +726,7 @@ void AssetDatabase::SaveArtifactDB() const { return; } - output << "# artifactKey\tassetGuid\timporter\tversion\ttype\tartifactDir\tmainArtifact\tsourceHash\tmetaHash\tsize\twriteTime\tmainLocalID\n"; + output << "# artifactKey\tassetGuid\timporter\tversion\ttype\tartifactDir\tmainArtifact\tsourceHash\tmetaHash\tsize\twriteTime\tmainLocalID\t(depPath\tdepHash\tdepSize\tdepWriteTime)...\n"; for (const auto& [guid, record] : m_artifactsByGuid) { output << EscapeField(ToStdString(record.artifactKey)) << '\t' << EscapeField(ToStdString(record.assetGuid.ToString())) << '\t' @@ -617,7 +739,14 @@ void AssetDatabase::SaveArtifactDB() const { << EscapeField(ToStdString(record.metaHash)) << '\t' << record.sourceFileSize << '\t' << record.sourceWriteTime << '\t' - << record.mainLocalID << '\n'; + << record.mainLocalID; + for (const ArtifactDependencyRecord& dependency : record.dependencies) { + output << '\t' << EscapeField(ToStdString(dependency.path)) + << '\t' << EscapeField(ToStdString(dependency.hash)) + << '\t' << dependency.fileSize + << '\t' << dependency.writeTime; + } + output << '\n'; } } @@ -714,6 +843,10 @@ bool AssetDatabase::EnsureMetaForPath(const fs::path& sourcePath, } shouldRewriteMeta = true; } + if (outRecord.importerVersion != kCurrentImporterVersion) { + outRecord.importerVersion = kCurrentImporterVersion; + shouldRewriteMeta = true; + } const auto duplicateGuidIt = m_sourcesByGuid.find(outRecord.guid); if (duplicateGuidIt != m_sourcesByGuid.end() && @@ -880,7 +1013,8 @@ bool AssetDatabase::ShouldReimport(const SourceAssetRecord& sourceRecord, artifactRecord->sourceHash != sourceRecord.sourceHash || artifactRecord->metaHash != sourceRecord.metaHash || artifactRecord->sourceFileSize != sourceRecord.sourceFileSize || - artifactRecord->sourceWriteTime != sourceRecord.sourceWriteTime; + artifactRecord->sourceWriteTime != sourceRecord.sourceWriteTime || + !AreDependenciesCurrent(artifactRecord->dependencies); } bool AssetDatabase::ImportAsset(const SourceAssetRecord& sourceRecord, @@ -1058,6 +1192,7 @@ bool AssetDatabase::ImportTextureAsset(const SourceAssetRecord& sourceRecord, outRecord.sourceFileSize = sourceRecord.sourceFileSize; outRecord.sourceWriteTime = sourceRecord.sourceWriteTime; outRecord.mainLocalID = kMainAssetLocalID; + outRecord.dependencies.clear(); return true; } @@ -1072,7 +1207,9 @@ bool AssetDatabase::ImportMaterialAsset(const SourceAssetRecord& sourceRecord, } Material* material = static_cast(result.resource); - const Containers::String artifactKey = BuildArtifactKey(sourceRecord); + std::vector dependencies; + CollectMaterialDependencies(*material, dependencies); + const Containers::String artifactKey = BuildArtifactKey(sourceRecord, dependencies); const Containers::String artifactDir = BuildArtifactDirectory(artifactKey); const Containers::String mainArtifactPath = NormalizePathString(fs::path(artifactDir.CStr()) / "main.xcmat"); @@ -1105,6 +1242,7 @@ bool AssetDatabase::ImportMaterialAsset(const SourceAssetRecord& sourceRecord, outRecord.sourceFileSize = sourceRecord.sourceFileSize; outRecord.sourceWriteTime = sourceRecord.sourceWriteTime; outRecord.mainLocalID = kMainAssetLocalID; + outRecord.dependencies = std::move(dependencies); return true; } @@ -1118,7 +1256,9 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord, } Mesh* mesh = static_cast(result.resource); - const Containers::String artifactKey = BuildArtifactKey(sourceRecord); + std::vector dependencies; + CollectModelDependencies(sourceRecord, *mesh, dependencies); + const Containers::String artifactKey = BuildArtifactKey(sourceRecord, dependencies); const Containers::String artifactDir = BuildArtifactDirectory(artifactKey); const Containers::String mainArtifactPath = NormalizePathString(fs::path(artifactDir.CStr()) / "main.xcmesh"); @@ -1196,10 +1336,13 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord, outRecord.sourceFileSize = sourceRecord.sourceFileSize; outRecord.sourceWriteTime = sourceRecord.sourceWriteTime; outRecord.mainLocalID = kMainAssetLocalID; + outRecord.dependencies = std::move(dependencies); return true; } -Containers::String AssetDatabase::BuildArtifactKey(const SourceAssetRecord& sourceRecord) const { +Containers::String AssetDatabase::BuildArtifactKey( + const AssetDatabase::SourceAssetRecord& sourceRecord, + const std::vector& dependencies) const { Containers::String signature = sourceRecord.guid.ToString(); signature += ":"; signature += sourceRecord.importerName; @@ -1213,9 +1356,176 @@ Containers::String AssetDatabase::BuildArtifactKey(const SourceAssetRecord& sour signature += Containers::String(std::to_string(sourceRecord.sourceFileSize).c_str()); signature += ":"; signature += Containers::String(std::to_string(sourceRecord.sourceWriteTime).c_str()); + for (const ArtifactDependencyRecord& dependency : dependencies) { + signature += ":dep:"; + signature += dependency.path; + signature += ":"; + signature += dependency.hash; + signature += ":"; + signature += Containers::String(std::to_string(dependency.fileSize).c_str()); + signature += ":"; + signature += Containers::String(std::to_string(dependency.writeTime).c_str()); + } return HashStringToAssetGUID(signature).ToString(); } +Containers::String AssetDatabase::NormalizeDependencyPath(const fs::path& path) const { + const fs::path normalizedPath = path.lexically_normal(); + if (normalizedPath.empty()) { + return Containers::String(); + } + + if (normalizedPath.is_absolute() && !m_projectRoot.Empty()) { + std::error_code ec; + const fs::path relativePath = fs::relative(normalizedPath, fs::path(m_projectRoot.CStr()), ec); + if (!ec && IsProjectRelativePath(relativePath)) { + return ToContainersString(relativePath.generic_string()); + } + } + + return ToContainersString(normalizedPath.generic_string()); +} + +fs::path AssetDatabase::ResolveDependencyPath(const Containers::String& path) const { + if (path.Empty()) { + return fs::path(); + } + + fs::path dependencyPath(path.CStr()); + if (dependencyPath.is_absolute()) { + return dependencyPath.lexically_normal(); + } + + return (fs::path(m_projectRoot.CStr()) / dependencyPath).lexically_normal(); +} + +bool AssetDatabase::CaptureDependencyRecord(const fs::path& path, + AssetDatabase::ArtifactDependencyRecord& outRecord) const { + const Containers::String normalizedPath = NormalizeDependencyPath(path); + if (normalizedPath.Empty()) { + return false; + } + + outRecord = ArtifactDependencyRecord(); + outRecord.path = normalizedPath; + + const fs::path resolvedPath = ResolveDependencyPath(normalizedPath); + if (!resolvedPath.empty() && fs::exists(resolvedPath)) { + outRecord.hash = ComputeFileHash(resolvedPath); + outRecord.fileSize = GetFileSizeValue(resolvedPath); + outRecord.writeTime = GetFileWriteTimeValue(resolvedPath); + } + + return true; +} + +bool AssetDatabase::AreDependenciesCurrent( + const std::vector& dependencies) const { + for (const AssetDatabase::ArtifactDependencyRecord& dependency : dependencies) { + if (dependency.path.Empty()) { + return false; + } + + const fs::path resolvedPath = ResolveDependencyPath(dependency.path); + if (resolvedPath.empty() || !fs::exists(resolvedPath)) { + if (!dependency.hash.Empty() || + dependency.fileSize != 0 || + dependency.writeTime != 0) { + return false; + } + continue; + } + + const Core::uint64 currentFileSize = GetFileSizeValue(resolvedPath); + const Core::uint64 currentWriteTime = GetFileWriteTimeValue(resolvedPath); + if (currentFileSize != dependency.fileSize || + currentWriteTime != dependency.writeTime) { + return false; + } + + if (ComputeFileHash(resolvedPath) != dependency.hash) { + return false; + } + } + + return true; +} + +bool AssetDatabase::CollectModelDependencies(const AssetDatabase::SourceAssetRecord& sourceRecord, + const Mesh& mesh, + std::vector& outDependencies) const { + outDependencies.clear(); + + std::unordered_set seenDependencyPaths; + std::vector candidatePaths; + + const fs::path sourcePath = fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr(); + const fs::path normalizedSourcePath = sourcePath.lexically_normal(); + if (ToLowerCopy(normalizedSourcePath.extension().string()) == ".obj") { + const std::vector mtlPaths = CollectObjDeclaredDependencyPaths(normalizedSourcePath); + for (const fs::path& mtlPath : mtlPaths) { + AddUniqueDependencyPath(mtlPath, seenDependencyPaths, candidatePaths); + const std::vector texturePaths = CollectMtlDeclaredDependencyPaths(mtlPath); + for (const fs::path& texturePath : texturePaths) { + AddUniqueDependencyPath(texturePath, seenDependencyPaths, candidatePaths); + } + } + } + + for (Texture* texture : mesh.GetTextures()) { + if (texture == nullptr || texture->GetPath().Empty()) { + continue; + } + + const std::string texturePath = ToStdString(texture->GetPath()); + if (texturePath.find('#') != std::string::npos) { + continue; + } + + AddUniqueDependencyPath(fs::path(texturePath), seenDependencyPaths, candidatePaths); + } + + const std::string sourcePathKey = normalizedSourcePath.generic_string(); + for (const fs::path& candidatePath : candidatePaths) { + if (candidatePath.generic_string() == sourcePathKey) { + continue; + } + + ArtifactDependencyRecord dependency; + if (CaptureDependencyRecord(candidatePath, dependency)) { + outDependencies.push_back(dependency); + } + } + + return true; +} + +bool AssetDatabase::CollectMaterialDependencies( + const Material& material, + std::vector& outDependencies) const { + outDependencies.clear(); + + std::unordered_set seenDependencyPaths; + for (Core::uint32 bindingIndex = 0; bindingIndex < material.GetTextureBindingCount(); ++bindingIndex) { + const Containers::String texturePath = material.GetTextureBindingPath(bindingIndex); + if (texturePath.Empty()) { + continue; + } + + ArtifactDependencyRecord dependency; + if (!CaptureDependencyRecord(ResolveDependencyPath(texturePath), dependency)) { + continue; + } + + const std::string dependencyKey = ToStdString(dependency.path); + if (seenDependencyPaths.insert(dependencyKey).second) { + outDependencies.push_back(dependency); + } + } + + return true; +} + Containers::String AssetDatabase::BuildArtifactDirectory(const Containers::String& artifactKey) const { if (artifactKey.Length() < 2) { return Containers::String("Library/Artifacts/00/invalid"); diff --git a/engine/src/Rendering/Passes/BuiltinObjectIdOutlinePass.cpp b/engine/src/Rendering/Passes/BuiltinObjectIdOutlinePass.cpp index 7e55bea3..3eb5803d 100644 --- a/engine/src/Rendering/Passes/BuiltinObjectIdOutlinePass.cpp +++ b/engine/src/Rendering/Passes/BuiltinObjectIdOutlinePass.cpp @@ -1,11 +1,14 @@ #include "Rendering/Passes/BuiltinObjectIdOutlinePass.h" +#include "Core/Asset/ResourceManager.h" +#include "Debug/Logger.h" #include "Rendering/ObjectIdEncoding.h" +#include "Rendering/Detail/ShaderVariantUtils.h" #include "RHI/RHICommandList.h" #include "RHI/RHIDevice.h" +#include "Resources/BuiltinResources.h" #include -#include namespace XCEngine { namespace Rendering { @@ -13,109 +16,73 @@ namespace Passes { namespace { -const char kBuiltinObjectIdOutlineHlsl[] = R"( -cbuffer OutlineConstants : register(b0) { - float4 gViewportSizeAndTexelSize; - float4 gOutlineColor; - float4 gSelectedInfo; - float4 gSelectedObjectColors[256]; -}; +const Resources::ShaderPass* FindObjectIdOutlineCompatiblePass( + const Resources::Shader& shader, + Resources::ShaderBackend backend) { + const Resources::ShaderPass* outlinePass = shader.FindPass("ObjectIdOutline"); + if (outlinePass != nullptr && + ::XCEngine::Rendering::Detail::ShaderPassHasGraphicsVariants(shader, outlinePass->name, backend)) { + return outlinePass; + } -Texture2D gObjectIdTexture : register(t0); + const Resources::ShaderPass* editorOutlinePass = shader.FindPass("EditorObjectIdOutline"); + if (editorOutlinePass != nullptr && + ::XCEngine::Rendering::Detail::ShaderPassHasGraphicsVariants(shader, editorOutlinePass->name, backend)) { + return editorOutlinePass; + } -struct VSOutput { - float4 position : SV_POSITION; -}; + if (shader.GetPassCount() > 0 && + ::XCEngine::Rendering::Detail::ShaderPassHasGraphicsVariants(shader, shader.GetPasses()[0].name, backend)) { + return &shader.GetPasses()[0]; + } -VSOutput MainVS(uint vertexId : SV_VertexID) { - static const float2 positions[3] = { - float2(-1.0, -1.0), - float2(-1.0, 3.0), - float2( 3.0, -1.0) - }; - - VSOutput output; - output.position = float4(positions[vertexId], 0.0, 1.0); - return output; + return nullptr; } -int2 ClampPixelCoord(int2 pixelCoord) { - const int2 maxCoord = int2( - max((int)gViewportSizeAndTexelSize.x - 1, 0), - max((int)gViewportSizeAndTexelSize.y - 1, 0)); - return clamp(pixelCoord, int2(0, 0), maxCoord); +RHI::GraphicsPipelineDesc CreatePipelineDesc( + RHI::RHIType backendType, + RHI::RHIPipelineLayout* pipelineLayout, + const Resources::Shader& shader, + const Containers::String& passName) { + RHI::GraphicsPipelineDesc pipelineDesc = {}; + pipelineDesc.pipelineLayout = pipelineLayout; + pipelineDesc.topologyType = static_cast(RHI::PrimitiveTopologyType::Triangle); + pipelineDesc.renderTargetCount = 1; + pipelineDesc.renderTargetFormats[0] = static_cast(RHI::Format::R8G8B8A8_UNorm); + pipelineDesc.depthStencilFormat = static_cast(RHI::Format::Unknown); + pipelineDesc.sampleCount = 1; + + pipelineDesc.rasterizerState.fillMode = static_cast(RHI::FillMode::Solid); + pipelineDesc.rasterizerState.cullMode = static_cast(RHI::CullMode::None); + pipelineDesc.rasterizerState.frontFace = static_cast(RHI::FrontFace::CounterClockwise); + pipelineDesc.rasterizerState.depthClipEnable = true; + + pipelineDesc.blendState.blendEnable = true; + pipelineDesc.blendState.srcBlend = static_cast(RHI::BlendFactor::SrcAlpha); + pipelineDesc.blendState.dstBlend = static_cast(RHI::BlendFactor::InvSrcAlpha); + pipelineDesc.blendState.srcBlendAlpha = static_cast(RHI::BlendFactor::One); + pipelineDesc.blendState.dstBlendAlpha = static_cast(RHI::BlendFactor::InvSrcAlpha); + pipelineDesc.blendState.blendOp = static_cast(RHI::BlendOp::Add); + pipelineDesc.blendState.blendOpAlpha = static_cast(RHI::BlendOp::Add); + pipelineDesc.blendState.colorWriteMask = static_cast(RHI::ColorWriteMask::All); + + pipelineDesc.depthStencilState.depthTestEnable = false; + pipelineDesc.depthStencilState.depthWriteEnable = false; + pipelineDesc.depthStencilState.depthFunc = static_cast(RHI::ComparisonFunc::Always); + + const Resources::ShaderBackend backend = ::XCEngine::Rendering::Detail::ToShaderBackend(backendType); + if (const Resources::ShaderStageVariant* vertexVariant = + shader.FindVariant(passName, Resources::ShaderType::Vertex, backend)) { + ::XCEngine::Rendering::Detail::ApplyShaderStageVariant(*vertexVariant, pipelineDesc.vertexShader); + } + if (const Resources::ShaderStageVariant* fragmentVariant = + shader.FindVariant(passName, Resources::ShaderType::Fragment, backend)) { + ::XCEngine::Rendering::Detail::ApplyShaderStageVariant(*fragmentVariant, pipelineDesc.fragmentShader); + } + + return pipelineDesc; } -float4 LoadObjectId(int2 pixelCoord) { - return gObjectIdTexture.Load(int3(ClampPixelCoord(pixelCoord), 0)); -} - -bool IsSelectedObject(float4 objectIdColor) { - if (objectIdColor.a <= 0.0) { - return false; - } - - const int selectedCount = min((int)gSelectedInfo.x, 256); - [loop] - for (int i = 0; i < selectedCount; ++i) { - const float4 selectedColor = gSelectedObjectColors[i]; - if (all(abs(objectIdColor - selectedColor) <= float4( - 0.0025, - 0.0025, - 0.0025, - 0.0025))) { - return true; - } - } - - return false; -} - -float4 MainPS(VSOutput input) : SV_TARGET { - const int2 pixelCoord = int2(input.position.xy); - const bool debugSelectionMask = gSelectedInfo.y > 0.5; - const bool centerSelected = IsSelectedObject(LoadObjectId(pixelCoord)); - - if (debugSelectionMask) { - return centerSelected ? float4(1.0, 1.0, 1.0, 1.0) : float4(0.0, 0.0, 0.0, 1.0); - } - - if (centerSelected) { - discard; - } - - const int outlineWidth = max((int)gSelectedInfo.z, 1); - float outline = 0.0; - [loop] - for (int y = -2; y <= 2; ++y) { - [loop] - for (int x = -2; x <= 2; ++x) { - if (x == 0 && y == 0) { - continue; - } - - const float distancePixels = length(float2((float)x, (float)y)); - if (distancePixels > outlineWidth) { - continue; - } - - if (!IsSelectedObject(LoadObjectId(pixelCoord + int2(x, y)))) { - continue; - } - - const float weight = saturate(1.0 - ((distancePixels - 1.0) / max((float)outlineWidth, 1.0))); - outline = max(outline, weight); - } - } - - if (outline <= 0.001) { - discard; - } - - return float4(gOutlineColor.rgb, gOutlineColor.a * outline); -} -)"; - } // namespace void BuiltinObjectIdOutlinePass::Shutdown() { @@ -225,6 +192,26 @@ bool BuiltinObjectIdOutlinePass::CreateResources(const RenderContext& renderCont m_device = renderContext.device; m_backendType = renderContext.backendType; + m_builtinObjectIdOutlineShader = Resources::ResourceManager::Get().Load( + Resources::GetBuiltinObjectIdOutlineShaderPath()); + if (!m_builtinObjectIdOutlineShader.IsValid()) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "BuiltinObjectIdOutlinePass failed to load builtin object-id-outline shader resource"); + DestroyResources(); + return false; + } + + const Resources::ShaderBackend backend = ::XCEngine::Rendering::Detail::ToShaderBackend(m_backendType); + const Resources::ShaderPass* outlinePass = + FindObjectIdOutlineCompatiblePass(*m_builtinObjectIdOutlineShader.Get(), backend); + if (outlinePass == nullptr) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "BuiltinObjectIdOutlinePass could not resolve a valid ObjectIdOutline shader pass"); + DestroyResources(); + return false; + } RHI::DescriptorSetLayoutBinding setBindings[2] = {}; setBindings[0].binding = 0; @@ -287,47 +274,12 @@ bool BuiltinObjectIdOutlinePass::CreateResources(const RenderContext& renderCont return false; } - RHI::GraphicsPipelineDesc pipelineDesc = {}; - pipelineDesc.pipelineLayout = m_pipelineLayout; - pipelineDesc.topologyType = static_cast(RHI::PrimitiveTopologyType::Triangle); - pipelineDesc.renderTargetCount = 1; - pipelineDesc.renderTargetFormats[0] = static_cast(RHI::Format::R8G8B8A8_UNorm); - pipelineDesc.depthStencilFormat = static_cast(RHI::Format::Unknown); - pipelineDesc.sampleCount = 1; - - pipelineDesc.rasterizerState.fillMode = static_cast(RHI::FillMode::Solid); - pipelineDesc.rasterizerState.cullMode = static_cast(RHI::CullMode::None); - pipelineDesc.rasterizerState.frontFace = static_cast(RHI::FrontFace::CounterClockwise); - pipelineDesc.rasterizerState.depthClipEnable = true; - - pipelineDesc.blendState.blendEnable = true; - pipelineDesc.blendState.srcBlend = static_cast(RHI::BlendFactor::SrcAlpha); - pipelineDesc.blendState.dstBlend = static_cast(RHI::BlendFactor::InvSrcAlpha); - pipelineDesc.blendState.srcBlendAlpha = static_cast(RHI::BlendFactor::One); - pipelineDesc.blendState.dstBlendAlpha = static_cast(RHI::BlendFactor::InvSrcAlpha); - pipelineDesc.blendState.blendOp = static_cast(RHI::BlendOp::Add); - pipelineDesc.blendState.blendOpAlpha = static_cast(RHI::BlendOp::Add); - pipelineDesc.blendState.colorWriteMask = static_cast(RHI::ColorWriteMask::All); - - pipelineDesc.depthStencilState.depthTestEnable = false; - pipelineDesc.depthStencilState.depthWriteEnable = false; - pipelineDesc.depthStencilState.depthFunc = static_cast(RHI::ComparisonFunc::Always); - - pipelineDesc.vertexShader.source.assign( - kBuiltinObjectIdOutlineHlsl, - kBuiltinObjectIdOutlineHlsl + std::strlen(kBuiltinObjectIdOutlineHlsl)); - pipelineDesc.vertexShader.sourceLanguage = RHI::ShaderLanguage::HLSL; - pipelineDesc.vertexShader.entryPoint = L"MainVS"; - pipelineDesc.vertexShader.profile = L"vs_5_0"; - - pipelineDesc.fragmentShader.source.assign( - kBuiltinObjectIdOutlineHlsl, - kBuiltinObjectIdOutlineHlsl + std::strlen(kBuiltinObjectIdOutlineHlsl)); - pipelineDesc.fragmentShader.sourceLanguage = RHI::ShaderLanguage::HLSL; - pipelineDesc.fragmentShader.entryPoint = L"MainPS"; - pipelineDesc.fragmentShader.profile = L"ps_5_0"; - - m_pipelineState = m_device->CreatePipelineState(pipelineDesc); + m_pipelineState = m_device->CreatePipelineState( + CreatePipelineDesc( + m_backendType, + m_pipelineLayout, + *m_builtinObjectIdOutlineShader.Get(), + outlinePass->name)); if (m_pipelineState == nullptr || !m_pipelineState->IsValid()) { DestroyResources(); return false; @@ -375,6 +327,7 @@ void BuiltinObjectIdOutlinePass::DestroyResources() { m_device = nullptr; m_backendType = RHI::RHIType::D3D12; + m_builtinObjectIdOutlineShader.Reset(); } } // namespace Passes diff --git a/engine/src/Resources/BuiltinResources.cpp b/engine/src/Resources/BuiltinResources.cpp index 0199c297..dd826aed 100644 --- a/engine/src/Resources/BuiltinResources.cpp +++ b/engine/src/Resources/BuiltinResources.cpp @@ -26,6 +26,7 @@ constexpr const char* kBuiltinTexturePrefix = "builtin://textures/"; constexpr const char* kBuiltinDefaultPrimitiveMaterialPath = "builtin://materials/default-primitive"; constexpr const char* kBuiltinForwardLitShaderPath = "builtin://shaders/forward-lit"; constexpr const char* kBuiltinObjectIdShaderPath = "builtin://shaders/object-id"; +constexpr const char* kBuiltinObjectIdOutlineShaderPath = "builtin://shaders/object-id-outline"; constexpr const char* kBuiltinInfiniteGridShaderPath = "builtin://shaders/infinite-grid"; constexpr const char* kBuiltinDefaultPrimitiveTexturePath = "builtin://textures/default-primitive-albedo"; constexpr float kPi = 3.14159265358979323846f; @@ -309,6 +310,109 @@ void main() { } )"; +const char kBuiltinObjectIdOutlineHlsl[] = R"( +cbuffer OutlineConstants : register(b0) { + float4 gViewportSizeAndTexelSize; + float4 gOutlineColor; + float4 gSelectedInfo; + float4 gSelectedObjectColors[256]; +}; + +Texture2D gObjectIdTexture : register(t0); + +struct VSOutput { + float4 position : SV_POSITION; +}; + +VSOutput MainVS(uint vertexId : SV_VertexID) { + static const float2 positions[3] = { + float2(-1.0, -1.0), + float2(-1.0, 3.0), + float2( 3.0, -1.0) + }; + + VSOutput output; + output.position = float4(positions[vertexId], 0.0, 1.0); + return output; +} + +int2 ClampPixelCoord(int2 pixelCoord) { + const int2 maxCoord = int2( + max((int)gViewportSizeAndTexelSize.x - 1, 0), + max((int)gViewportSizeAndTexelSize.y - 1, 0)); + return clamp(pixelCoord, int2(0, 0), maxCoord); +} + +float4 LoadObjectId(int2 pixelCoord) { + return gObjectIdTexture.Load(int3(ClampPixelCoord(pixelCoord), 0)); +} + +bool IsSelectedObject(float4 objectIdColor) { + if (objectIdColor.a <= 0.0) { + return false; + } + + const int selectedCount = min((int)gSelectedInfo.x, 256); + [loop] + for (int i = 0; i < selectedCount; ++i) { + const float4 selectedColor = gSelectedObjectColors[i]; + if (all(abs(objectIdColor - selectedColor) <= float4( + 0.0025, + 0.0025, + 0.0025, + 0.0025))) { + return true; + } + } + + return false; +} + +float4 MainPS(VSOutput input) : SV_TARGET { + const int2 pixelCoord = int2(input.position.xy); + const bool debugSelectionMask = gSelectedInfo.y > 0.5; + const bool centerSelected = IsSelectedObject(LoadObjectId(pixelCoord)); + + if (debugSelectionMask) { + return centerSelected ? float4(1.0, 1.0, 1.0, 1.0) : float4(0.0, 0.0, 0.0, 1.0); + } + + if (centerSelected) { + discard; + } + + const int outlineWidth = max((int)gSelectedInfo.z, 1); + float outline = 0.0; + [loop] + for (int y = -2; y <= 2; ++y) { + [loop] + for (int x = -2; x <= 2; ++x) { + if (x == 0 && y == 0) { + continue; + } + + const float distancePixels = length(float2((float)x, (float)y)); + if (distancePixels > outlineWidth) { + continue; + } + + if (!IsSelectedObject(LoadObjectId(pixelCoord + int2(x, y)))) { + continue; + } + + const float weight = saturate(1.0 - ((distancePixels - 1.0) / max((float)outlineWidth, 1.0))); + outline = max(outline, weight); + } + } + + if (outline <= 0.001) { + discard; + } + + return float4(gOutlineColor.rgb, gOutlineColor.a * outline); +} +)"; + const char kBuiltinInfiniteGridHlsl[] = R"( cbuffer GridConstants : register(b0) { float4x4 gViewProjectionMatrix; @@ -1125,6 +1229,41 @@ Shader* BuildBuiltinInfiniteGridShader(const Containers::String& path) { return shader; } +Shader* BuildBuiltinObjectIdOutlineShader(const Containers::String& path) { + auto* shader = new Shader(); + IResource::ConstructParams params; + params.name = Containers::String("Builtin Object Id Outline"); + params.path = path; + params.guid = ResourceGUID::Generate(path); + params.memorySize = 0; + shader->Initialize(params); + + const Containers::String passName("ObjectIdOutline"); + shader->SetPassTag(passName, "LightMode", "ObjectIdOutline"); + + AddBuiltinShaderStageVariant( + *shader, + passName, + ShaderType::Vertex, + ShaderLanguage::HLSL, + ShaderBackend::D3D12, + kBuiltinObjectIdOutlineHlsl, + "MainVS", + "vs_5_0"); + AddBuiltinShaderStageVariant( + *shader, + passName, + ShaderType::Fragment, + ShaderLanguage::HLSL, + ShaderBackend::D3D12, + kBuiltinObjectIdOutlineHlsl, + "MainPS", + "ps_5_0"); + + shader->m_memorySize = CalculateBuiltinShaderMemorySize(*shader); + return shader; +} + Material* BuildDefaultPrimitiveMaterial(const Containers::String& path) { auto* material = new Material(); IResource::ConstructParams params; @@ -1231,6 +1370,10 @@ Containers::String GetBuiltinObjectIdShaderPath() { return Containers::String(kBuiltinObjectIdShaderPath); } +Containers::String GetBuiltinObjectIdOutlineShaderPath() { + return Containers::String(kBuiltinObjectIdOutlineShaderPath); +} + Containers::String GetBuiltinInfiniteGridShaderPath() { return Containers::String(kBuiltinInfiniteGridShaderPath); } @@ -1327,6 +1470,8 @@ LoadResult CreateBuiltinShaderResource(const Containers::String& path) { shader = BuildBuiltinForwardLitShader(path); } else if (path == GetBuiltinObjectIdShaderPath()) { shader = BuildBuiltinObjectIdShader(path); + } else if (path == GetBuiltinObjectIdOutlineShaderPath()) { + shader = BuildBuiltinObjectIdOutlineShader(path); } else if (path == GetBuiltinInfiniteGridShaderPath()) { shader = BuildBuiltinInfiniteGridShader(path); } else { diff --git a/tests/Rendering/integration/textured_quad_scene/main.cpp b/tests/Rendering/integration/textured_quad_scene/main.cpp index 982814ee..05c022c4 100644 --- a/tests/Rendering/integration/textured_quad_scene/main.cpp +++ b/tests/Rendering/integration/textured_quad_scene/main.cpp @@ -22,6 +22,7 @@ #include "../../../RHI/integration/fixtures/RHIIntegrationFixture.h" #include +#include #include using namespace XCEngine::Components; @@ -319,5 +320,8 @@ GTEST_API_ int main(int argc, char** argv) { Logger::Get().SetMinimumLevel(LogLevel::Debug); testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); + const int result = RUN_ALL_TESTS(); + Logger::Get().Shutdown(); + std::fflush(nullptr); + std::_Exit((result == 0 && !testing::UnitTest::GetInstance()->Failed()) ? 0 : 1); } diff --git a/tests/Resources/Shader/test_shader_loader.cpp b/tests/Resources/Shader/test_shader_loader.cpp index 018d6713..2cdc674a 100644 --- a/tests/Resources/Shader/test_shader_loader.cpp +++ b/tests/Resources/Shader/test_shader_loader.cpp @@ -33,6 +33,7 @@ TEST(ShaderLoader, CanLoad) { EXPECT_TRUE(loader.CanLoad("test.hlsl")); EXPECT_TRUE(loader.CanLoad(GetBuiltinForwardLitShaderPath())); EXPECT_TRUE(loader.CanLoad(GetBuiltinObjectIdShaderPath())); + EXPECT_TRUE(loader.CanLoad(GetBuiltinObjectIdOutlineShaderPath())); EXPECT_TRUE(loader.CanLoad(GetBuiltinInfiniteGridShaderPath())); EXPECT_FALSE(loader.CanLoad("test.txt")); EXPECT_FALSE(loader.CanLoad("test.png")); @@ -154,6 +155,31 @@ TEST(ShaderLoader, LoadBuiltinInfiniteGridShaderBuildsD3D12Variants) { delete shader; } +TEST(ShaderLoader, LoadBuiltinObjectIdOutlineShaderBuildsD3D12Variants) { + ShaderLoader loader; + LoadResult result = loader.Load(GetBuiltinObjectIdOutlineShaderPath()); + ASSERT_TRUE(result); + ASSERT_NE(result.resource, nullptr); + + Shader* shader = static_cast(result.resource); + ASSERT_NE(shader, nullptr); + ASSERT_TRUE(shader->IsValid()); + + const ShaderPass* pass = shader->FindPass("ObjectIdOutline"); + ASSERT_NE(pass, nullptr); + ASSERT_EQ(pass->variants.Size(), 2u); + ASSERT_EQ(pass->tags.Size(), 1u); + EXPECT_EQ(pass->tags[0].name, "LightMode"); + EXPECT_EQ(pass->tags[0].value, "ObjectIdOutline"); + + EXPECT_NE(shader->FindVariant("ObjectIdOutline", ShaderType::Vertex, ShaderBackend::D3D12), nullptr); + EXPECT_NE(shader->FindVariant("ObjectIdOutline", ShaderType::Fragment, ShaderBackend::D3D12), nullptr); + EXPECT_EQ(shader->FindVariant("ObjectIdOutline", ShaderType::Vertex, ShaderBackend::OpenGL), nullptr); + EXPECT_EQ(shader->FindVariant("ObjectIdOutline", ShaderType::Fragment, ShaderBackend::OpenGL), nullptr); + + delete shader; +} + TEST(ShaderLoader, ResourceManagerLazilyLoadsBuiltinForwardLitShader) { ResourceManager& manager = ResourceManager::Get(); manager.Shutdown(); @@ -167,6 +193,10 @@ TEST(ShaderLoader, ResourceManagerLazilyLoadsBuiltinForwardLitShader) { ASSERT_TRUE(objectIdShaderHandle.IsValid()); ASSERT_NE(objectIdShaderHandle->FindPass("ObjectId"), nullptr); + ResourceHandle objectIdOutlineShaderHandle = manager.Load(GetBuiltinObjectIdOutlineShaderPath()); + ASSERT_TRUE(objectIdOutlineShaderHandle.IsValid()); + ASSERT_NE(objectIdOutlineShaderHandle->FindPass("ObjectIdOutline"), nullptr); + ResourceHandle infiniteGridShaderHandle = manager.Load(GetBuiltinInfiniteGridShaderPath()); ASSERT_TRUE(infiniteGridShaderHandle.IsValid()); ASSERT_NE(infiniteGridShaderHandle->FindPass("InfiniteGrid"), nullptr);