refactor: route builtin outline pass through shader assets

This commit is contained in:
2026-04-02 20:18:39 +08:00
parent 1f29dfd611
commit 9ce779da43
7 changed files with 591 additions and 145 deletions

View File

@@ -3,6 +3,7 @@
#include <XCEngine/Core/Math/Color.h> #include <XCEngine/Core/Math/Color.h>
#include <XCEngine/Core/Math/Vector3.h> #include <XCEngine/Core/Math/Vector3.h>
#include <XCEngine/Core/Math/Vector4.h> #include <XCEngine/Core/Math/Vector4.h>
#include <XCEngine/Core/Asset/ResourceHandle.h>
#include <XCEngine/Rendering/RenderContext.h> #include <XCEngine/Rendering/RenderContext.h>
#include <XCEngine/Rendering/RenderSurface.h> #include <XCEngine/Rendering/RenderSurface.h>
@@ -12,6 +13,7 @@
#include <XCEngine/RHI/RHIPipelineLayout.h> #include <XCEngine/RHI/RHIPipelineLayout.h>
#include <XCEngine/RHI/RHIPipelineState.h> #include <XCEngine/RHI/RHIPipelineState.h>
#include <XCEngine/RHI/RHIResourceView.h> #include <XCEngine/RHI/RHIResourceView.h>
#include <XCEngine/Resources/Shader/Shader.h>
#include <array> #include <array>
#include <cstdint> #include <cstdint>
@@ -62,6 +64,7 @@ private:
RHI::RHIDescriptorSet* m_constantSet = nullptr; RHI::RHIDescriptorSet* m_constantSet = nullptr;
RHI::RHIDescriptorPool* m_texturePool = nullptr; RHI::RHIDescriptorPool* m_texturePool = nullptr;
RHI::RHIDescriptorSet* m_textureSet = nullptr; RHI::RHIDescriptorSet* m_textureSet = nullptr;
Resources::ResourceHandle<Resources::Shader> m_builtinObjectIdOutlineShader;
}; };
} // namespace Passes } // namespace Passes

View File

@@ -25,6 +25,7 @@ Containers::String GetBuiltinPrimitiveMeshPath(BuiltinPrimitiveType primitiveTyp
Containers::String GetBuiltinDefaultPrimitiveMaterialPath(); Containers::String GetBuiltinDefaultPrimitiveMaterialPath();
Containers::String GetBuiltinForwardLitShaderPath(); Containers::String GetBuiltinForwardLitShaderPath();
Containers::String GetBuiltinObjectIdShaderPath(); Containers::String GetBuiltinObjectIdShaderPath();
Containers::String GetBuiltinObjectIdOutlineShaderPath();
Containers::String GetBuiltinInfiniteGridShaderPath(); Containers::String GetBuiltinInfiniteGridShaderPath();
Containers::String GetBuiltinDefaultPrimitiveTexturePath(); Containers::String GetBuiltinDefaultPrimitiveTexturePath();

View File

@@ -13,6 +13,7 @@
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <unordered_map> #include <unordered_map>
#include <unordered_set>
#include <vector> #include <vector>
namespace XCEngine { namespace XCEngine {
@@ -48,6 +49,32 @@ Containers::String NormalizeArtifactPathString(const Containers::String& path) {
return ToContainersString(fs::path(path.CStr()).lexically_normal().generic_string()); 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<std::string>& seenPaths,
std::vector<fs::path>& 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) { std::string TrimCopy(const std::string& text) {
const auto begin = std::find_if_not(text.begin(), text.end(), [](unsigned char ch) { const auto begin = std::find_if_not(text.begin(), text.end(), [](unsigned char ch) {
return std::isspace(ch) != 0; return std::isspace(ch) != 0;
@@ -69,6 +96,91 @@ std::string ToLowerCopy(std::string text) {
return text; return text;
} }
std::vector<std::string> SplitWhitespaceTokens(const std::string& text) {
std::vector<std::string> tokens;
std::istringstream stream(text);
std::string token;
while (stream >> token) {
tokens.push_back(token);
}
return tokens;
}
std::vector<fs::path> CollectObjDeclaredDependencyPaths(const fs::path& sourcePath) {
std::vector<fs::path> dependencies;
std::unordered_set<std::string> 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<unsigned char>(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<fs::path> CollectMtlDeclaredDependencyPaths(const fs::path& mtlPath) {
std::vector<fs::path> dependencies;
std::unordered_set<std::string> 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<std::string> 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 EscapeField(const std::string& value) {
std::string escaped; std::string escaped;
escaped.reserve(value.size()); escaped.reserve(value.size());
@@ -589,6 +701,16 @@ void AssetDatabase::LoadArtifactDB() {
record.sourceFileSize = static_cast<Core::uint64>(std::stoull(fields[9])); record.sourceFileSize = static_cast<Core::uint64>(std::stoull(fields[9]));
record.sourceWriteTime = fields.size() > 10 ? static_cast<Core::uint64>(std::stoull(fields[10])) : 0; record.sourceWriteTime = fields.size() > 10 ? static_cast<Core::uint64>(std::stoull(fields[10])) : 0;
record.mainLocalID = fields.size() > 11 ? static_cast<LocalID>(std::stoull(fields[11])) : kMainAssetLocalID; record.mainLocalID = fields.size() > 11 ? static_cast<LocalID>(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<Core::uint64>(std::stoull(fields[index + 2]));
dependency.writeTime = static_cast<Core::uint64>(std::stoull(fields[index + 3]));
if (!dependency.path.Empty()) {
record.dependencies.push_back(dependency);
}
}
if (!record.assetGuid.IsValid() || record.artifactKey.Empty()) { if (!record.assetGuid.IsValid() || record.artifactKey.Empty()) {
continue; continue;
@@ -604,7 +726,7 @@ void AssetDatabase::SaveArtifactDB() const {
return; 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) { for (const auto& [guid, record] : m_artifactsByGuid) {
output << EscapeField(ToStdString(record.artifactKey)) << '\t' output << EscapeField(ToStdString(record.artifactKey)) << '\t'
<< EscapeField(ToStdString(record.assetGuid.ToString())) << '\t' << EscapeField(ToStdString(record.assetGuid.ToString())) << '\t'
@@ -617,7 +739,14 @@ void AssetDatabase::SaveArtifactDB() const {
<< EscapeField(ToStdString(record.metaHash)) << '\t' << EscapeField(ToStdString(record.metaHash)) << '\t'
<< record.sourceFileSize << '\t' << record.sourceFileSize << '\t'
<< record.sourceWriteTime << '\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; shouldRewriteMeta = true;
} }
if (outRecord.importerVersion != kCurrentImporterVersion) {
outRecord.importerVersion = kCurrentImporterVersion;
shouldRewriteMeta = true;
}
const auto duplicateGuidIt = m_sourcesByGuid.find(outRecord.guid); const auto duplicateGuidIt = m_sourcesByGuid.find(outRecord.guid);
if (duplicateGuidIt != m_sourcesByGuid.end() && if (duplicateGuidIt != m_sourcesByGuid.end() &&
@@ -880,7 +1013,8 @@ bool AssetDatabase::ShouldReimport(const SourceAssetRecord& sourceRecord,
artifactRecord->sourceHash != sourceRecord.sourceHash || artifactRecord->sourceHash != sourceRecord.sourceHash ||
artifactRecord->metaHash != sourceRecord.metaHash || artifactRecord->metaHash != sourceRecord.metaHash ||
artifactRecord->sourceFileSize != sourceRecord.sourceFileSize || artifactRecord->sourceFileSize != sourceRecord.sourceFileSize ||
artifactRecord->sourceWriteTime != sourceRecord.sourceWriteTime; artifactRecord->sourceWriteTime != sourceRecord.sourceWriteTime ||
!AreDependenciesCurrent(artifactRecord->dependencies);
} }
bool AssetDatabase::ImportAsset(const SourceAssetRecord& sourceRecord, bool AssetDatabase::ImportAsset(const SourceAssetRecord& sourceRecord,
@@ -1058,6 +1192,7 @@ bool AssetDatabase::ImportTextureAsset(const SourceAssetRecord& sourceRecord,
outRecord.sourceFileSize = sourceRecord.sourceFileSize; outRecord.sourceFileSize = sourceRecord.sourceFileSize;
outRecord.sourceWriteTime = sourceRecord.sourceWriteTime; outRecord.sourceWriteTime = sourceRecord.sourceWriteTime;
outRecord.mainLocalID = kMainAssetLocalID; outRecord.mainLocalID = kMainAssetLocalID;
outRecord.dependencies.clear();
return true; return true;
} }
@@ -1072,7 +1207,9 @@ bool AssetDatabase::ImportMaterialAsset(const SourceAssetRecord& sourceRecord,
} }
Material* material = static_cast<Material*>(result.resource); Material* material = static_cast<Material*>(result.resource);
const Containers::String artifactKey = BuildArtifactKey(sourceRecord); std::vector<ArtifactDependencyRecord> dependencies;
CollectMaterialDependencies(*material, dependencies);
const Containers::String artifactKey = BuildArtifactKey(sourceRecord, dependencies);
const Containers::String artifactDir = BuildArtifactDirectory(artifactKey); const Containers::String artifactDir = BuildArtifactDirectory(artifactKey);
const Containers::String mainArtifactPath = const Containers::String mainArtifactPath =
NormalizePathString(fs::path(artifactDir.CStr()) / "main.xcmat"); NormalizePathString(fs::path(artifactDir.CStr()) / "main.xcmat");
@@ -1105,6 +1242,7 @@ bool AssetDatabase::ImportMaterialAsset(const SourceAssetRecord& sourceRecord,
outRecord.sourceFileSize = sourceRecord.sourceFileSize; outRecord.sourceFileSize = sourceRecord.sourceFileSize;
outRecord.sourceWriteTime = sourceRecord.sourceWriteTime; outRecord.sourceWriteTime = sourceRecord.sourceWriteTime;
outRecord.mainLocalID = kMainAssetLocalID; outRecord.mainLocalID = kMainAssetLocalID;
outRecord.dependencies = std::move(dependencies);
return true; return true;
} }
@@ -1118,7 +1256,9 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
} }
Mesh* mesh = static_cast<Mesh*>(result.resource); Mesh* mesh = static_cast<Mesh*>(result.resource);
const Containers::String artifactKey = BuildArtifactKey(sourceRecord); std::vector<ArtifactDependencyRecord> dependencies;
CollectModelDependencies(sourceRecord, *mesh, dependencies);
const Containers::String artifactKey = BuildArtifactKey(sourceRecord, dependencies);
const Containers::String artifactDir = BuildArtifactDirectory(artifactKey); const Containers::String artifactDir = BuildArtifactDirectory(artifactKey);
const Containers::String mainArtifactPath = NormalizePathString(fs::path(artifactDir.CStr()) / "main.xcmesh"); 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.sourceFileSize = sourceRecord.sourceFileSize;
outRecord.sourceWriteTime = sourceRecord.sourceWriteTime; outRecord.sourceWriteTime = sourceRecord.sourceWriteTime;
outRecord.mainLocalID = kMainAssetLocalID; outRecord.mainLocalID = kMainAssetLocalID;
outRecord.dependencies = std::move(dependencies);
return true; return true;
} }
Containers::String AssetDatabase::BuildArtifactKey(const SourceAssetRecord& sourceRecord) const { Containers::String AssetDatabase::BuildArtifactKey(
const AssetDatabase::SourceAssetRecord& sourceRecord,
const std::vector<AssetDatabase::ArtifactDependencyRecord>& dependencies) const {
Containers::String signature = sourceRecord.guid.ToString(); Containers::String signature = sourceRecord.guid.ToString();
signature += ":"; signature += ":";
signature += sourceRecord.importerName; 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 += Containers::String(std::to_string(sourceRecord.sourceFileSize).c_str());
signature += ":"; signature += ":";
signature += Containers::String(std::to_string(sourceRecord.sourceWriteTime).c_str()); 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(); 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<AssetDatabase::ArtifactDependencyRecord>& 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<AssetDatabase::ArtifactDependencyRecord>& outDependencies) const {
outDependencies.clear();
std::unordered_set<std::string> seenDependencyPaths;
std::vector<fs::path> 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<fs::path> mtlPaths = CollectObjDeclaredDependencyPaths(normalizedSourcePath);
for (const fs::path& mtlPath : mtlPaths) {
AddUniqueDependencyPath(mtlPath, seenDependencyPaths, candidatePaths);
const std::vector<fs::path> 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<AssetDatabase::ArtifactDependencyRecord>& outDependencies) const {
outDependencies.clear();
std::unordered_set<std::string> 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 { Containers::String AssetDatabase::BuildArtifactDirectory(const Containers::String& artifactKey) const {
if (artifactKey.Length() < 2) { if (artifactKey.Length() < 2) {
return Containers::String("Library/Artifacts/00/invalid"); return Containers::String("Library/Artifacts/00/invalid");

View File

@@ -1,11 +1,14 @@
#include "Rendering/Passes/BuiltinObjectIdOutlinePass.h" #include "Rendering/Passes/BuiltinObjectIdOutlinePass.h"
#include "Core/Asset/ResourceManager.h"
#include "Debug/Logger.h"
#include "Rendering/ObjectIdEncoding.h" #include "Rendering/ObjectIdEncoding.h"
#include "Rendering/Detail/ShaderVariantUtils.h"
#include "RHI/RHICommandList.h" #include "RHI/RHICommandList.h"
#include "RHI/RHIDevice.h" #include "RHI/RHIDevice.h"
#include "Resources/BuiltinResources.h"
#include <algorithm> #include <algorithm>
#include <cstring>
namespace XCEngine { namespace XCEngine {
namespace Rendering { namespace Rendering {
@@ -13,109 +16,73 @@ namespace Passes {
namespace { namespace {
const char kBuiltinObjectIdOutlineHlsl[] = R"( const Resources::ShaderPass* FindObjectIdOutlineCompatiblePass(
cbuffer OutlineConstants : register(b0) { const Resources::Shader& shader,
float4 gViewportSizeAndTexelSize; Resources::ShaderBackend backend) {
float4 gOutlineColor; const Resources::ShaderPass* outlinePass = shader.FindPass("ObjectIdOutline");
float4 gSelectedInfo; if (outlinePass != nullptr &&
float4 gSelectedObjectColors[256]; ::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 { if (shader.GetPassCount() > 0 &&
float4 position : SV_POSITION; ::XCEngine::Rendering::Detail::ShaderPassHasGraphicsVariants(shader, shader.GetPasses()[0].name, backend)) {
}; return &shader.GetPasses()[0];
}
VSOutput MainVS(uint vertexId : SV_VertexID) { return nullptr;
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) { RHI::GraphicsPipelineDesc CreatePipelineDesc(
const int2 maxCoord = int2( RHI::RHIType backendType,
max((int)gViewportSizeAndTexelSize.x - 1, 0), RHI::RHIPipelineLayout* pipelineLayout,
max((int)gViewportSizeAndTexelSize.y - 1, 0)); const Resources::Shader& shader,
return clamp(pixelCoord, int2(0, 0), maxCoord); const Containers::String& passName) {
RHI::GraphicsPipelineDesc pipelineDesc = {};
pipelineDesc.pipelineLayout = pipelineLayout;
pipelineDesc.topologyType = static_cast<uint32_t>(RHI::PrimitiveTopologyType::Triangle);
pipelineDesc.renderTargetCount = 1;
pipelineDesc.renderTargetFormats[0] = static_cast<uint32_t>(RHI::Format::R8G8B8A8_UNorm);
pipelineDesc.depthStencilFormat = static_cast<uint32_t>(RHI::Format::Unknown);
pipelineDesc.sampleCount = 1;
pipelineDesc.rasterizerState.fillMode = static_cast<uint32_t>(RHI::FillMode::Solid);
pipelineDesc.rasterizerState.cullMode = static_cast<uint32_t>(RHI::CullMode::None);
pipelineDesc.rasterizerState.frontFace = static_cast<uint32_t>(RHI::FrontFace::CounterClockwise);
pipelineDesc.rasterizerState.depthClipEnable = true;
pipelineDesc.blendState.blendEnable = true;
pipelineDesc.blendState.srcBlend = static_cast<uint32_t>(RHI::BlendFactor::SrcAlpha);
pipelineDesc.blendState.dstBlend = static_cast<uint32_t>(RHI::BlendFactor::InvSrcAlpha);
pipelineDesc.blendState.srcBlendAlpha = static_cast<uint32_t>(RHI::BlendFactor::One);
pipelineDesc.blendState.dstBlendAlpha = static_cast<uint32_t>(RHI::BlendFactor::InvSrcAlpha);
pipelineDesc.blendState.blendOp = static_cast<uint32_t>(RHI::BlendOp::Add);
pipelineDesc.blendState.blendOpAlpha = static_cast<uint32_t>(RHI::BlendOp::Add);
pipelineDesc.blendState.colorWriteMask = static_cast<uint8_t>(RHI::ColorWriteMask::All);
pipelineDesc.depthStencilState.depthTestEnable = false;
pipelineDesc.depthStencilState.depthWriteEnable = false;
pipelineDesc.depthStencilState.depthFunc = static_cast<uint32_t>(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 } // namespace
void BuiltinObjectIdOutlinePass::Shutdown() { void BuiltinObjectIdOutlinePass::Shutdown() {
@@ -225,6 +192,26 @@ bool BuiltinObjectIdOutlinePass::CreateResources(const RenderContext& renderCont
m_device = renderContext.device; m_device = renderContext.device;
m_backendType = renderContext.backendType; m_backendType = renderContext.backendType;
m_builtinObjectIdOutlineShader = Resources::ResourceManager::Get().Load<Resources::Shader>(
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] = {}; RHI::DescriptorSetLayoutBinding setBindings[2] = {};
setBindings[0].binding = 0; setBindings[0].binding = 0;
@@ -287,47 +274,12 @@ bool BuiltinObjectIdOutlinePass::CreateResources(const RenderContext& renderCont
return false; return false;
} }
RHI::GraphicsPipelineDesc pipelineDesc = {}; m_pipelineState = m_device->CreatePipelineState(
pipelineDesc.pipelineLayout = m_pipelineLayout; CreatePipelineDesc(
pipelineDesc.topologyType = static_cast<uint32_t>(RHI::PrimitiveTopologyType::Triangle); m_backendType,
pipelineDesc.renderTargetCount = 1; m_pipelineLayout,
pipelineDesc.renderTargetFormats[0] = static_cast<uint32_t>(RHI::Format::R8G8B8A8_UNorm); *m_builtinObjectIdOutlineShader.Get(),
pipelineDesc.depthStencilFormat = static_cast<uint32_t>(RHI::Format::Unknown); outlinePass->name));
pipelineDesc.sampleCount = 1;
pipelineDesc.rasterizerState.fillMode = static_cast<uint32_t>(RHI::FillMode::Solid);
pipelineDesc.rasterizerState.cullMode = static_cast<uint32_t>(RHI::CullMode::None);
pipelineDesc.rasterizerState.frontFace = static_cast<uint32_t>(RHI::FrontFace::CounterClockwise);
pipelineDesc.rasterizerState.depthClipEnable = true;
pipelineDesc.blendState.blendEnable = true;
pipelineDesc.blendState.srcBlend = static_cast<uint32_t>(RHI::BlendFactor::SrcAlpha);
pipelineDesc.blendState.dstBlend = static_cast<uint32_t>(RHI::BlendFactor::InvSrcAlpha);
pipelineDesc.blendState.srcBlendAlpha = static_cast<uint32_t>(RHI::BlendFactor::One);
pipelineDesc.blendState.dstBlendAlpha = static_cast<uint32_t>(RHI::BlendFactor::InvSrcAlpha);
pipelineDesc.blendState.blendOp = static_cast<uint32_t>(RHI::BlendOp::Add);
pipelineDesc.blendState.blendOpAlpha = static_cast<uint32_t>(RHI::BlendOp::Add);
pipelineDesc.blendState.colorWriteMask = static_cast<uint8_t>(RHI::ColorWriteMask::All);
pipelineDesc.depthStencilState.depthTestEnable = false;
pipelineDesc.depthStencilState.depthWriteEnable = false;
pipelineDesc.depthStencilState.depthFunc = static_cast<uint32_t>(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);
if (m_pipelineState == nullptr || !m_pipelineState->IsValid()) { if (m_pipelineState == nullptr || !m_pipelineState->IsValid()) {
DestroyResources(); DestroyResources();
return false; return false;
@@ -375,6 +327,7 @@ void BuiltinObjectIdOutlinePass::DestroyResources() {
m_device = nullptr; m_device = nullptr;
m_backendType = RHI::RHIType::D3D12; m_backendType = RHI::RHIType::D3D12;
m_builtinObjectIdOutlineShader.Reset();
} }
} // namespace Passes } // namespace Passes

View File

@@ -26,6 +26,7 @@ constexpr const char* kBuiltinTexturePrefix = "builtin://textures/";
constexpr const char* kBuiltinDefaultPrimitiveMaterialPath = "builtin://materials/default-primitive"; constexpr const char* kBuiltinDefaultPrimitiveMaterialPath = "builtin://materials/default-primitive";
constexpr const char* kBuiltinForwardLitShaderPath = "builtin://shaders/forward-lit"; constexpr const char* kBuiltinForwardLitShaderPath = "builtin://shaders/forward-lit";
constexpr const char* kBuiltinObjectIdShaderPath = "builtin://shaders/object-id"; 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* kBuiltinInfiniteGridShaderPath = "builtin://shaders/infinite-grid";
constexpr const char* kBuiltinDefaultPrimitiveTexturePath = "builtin://textures/default-primitive-albedo"; constexpr const char* kBuiltinDefaultPrimitiveTexturePath = "builtin://textures/default-primitive-albedo";
constexpr float kPi = 3.14159265358979323846f; 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"( const char kBuiltinInfiniteGridHlsl[] = R"(
cbuffer GridConstants : register(b0) { cbuffer GridConstants : register(b0) {
float4x4 gViewProjectionMatrix; float4x4 gViewProjectionMatrix;
@@ -1125,6 +1229,41 @@ Shader* BuildBuiltinInfiniteGridShader(const Containers::String& path) {
return shader; 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) { Material* BuildDefaultPrimitiveMaterial(const Containers::String& path) {
auto* material = new Material(); auto* material = new Material();
IResource::ConstructParams params; IResource::ConstructParams params;
@@ -1231,6 +1370,10 @@ Containers::String GetBuiltinObjectIdShaderPath() {
return Containers::String(kBuiltinObjectIdShaderPath); return Containers::String(kBuiltinObjectIdShaderPath);
} }
Containers::String GetBuiltinObjectIdOutlineShaderPath() {
return Containers::String(kBuiltinObjectIdOutlineShaderPath);
}
Containers::String GetBuiltinInfiniteGridShaderPath() { Containers::String GetBuiltinInfiniteGridShaderPath() {
return Containers::String(kBuiltinInfiniteGridShaderPath); return Containers::String(kBuiltinInfiniteGridShaderPath);
} }
@@ -1327,6 +1470,8 @@ LoadResult CreateBuiltinShaderResource(const Containers::String& path) {
shader = BuildBuiltinForwardLitShader(path); shader = BuildBuiltinForwardLitShader(path);
} else if (path == GetBuiltinObjectIdShaderPath()) { } else if (path == GetBuiltinObjectIdShaderPath()) {
shader = BuildBuiltinObjectIdShader(path); shader = BuildBuiltinObjectIdShader(path);
} else if (path == GetBuiltinObjectIdOutlineShaderPath()) {
shader = BuildBuiltinObjectIdOutlineShader(path);
} else if (path == GetBuiltinInfiniteGridShaderPath()) { } else if (path == GetBuiltinInfiniteGridShaderPath()) {
shader = BuildBuiltinInfiniteGridShader(path); shader = BuildBuiltinInfiniteGridShader(path);
} else { } else {

View File

@@ -22,6 +22,7 @@
#include "../../../RHI/integration/fixtures/RHIIntegrationFixture.h" #include "../../../RHI/integration/fixtures/RHIIntegrationFixture.h"
#include <memory> #include <memory>
#include <cstdlib>
#include <vector> #include <vector>
using namespace XCEngine::Components; using namespace XCEngine::Components;
@@ -319,5 +320,8 @@ GTEST_API_ int main(int argc, char** argv) {
Logger::Get().SetMinimumLevel(LogLevel::Debug); Logger::Get().SetMinimumLevel(LogLevel::Debug);
testing::InitGoogleTest(&argc, argv); 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);
} }

View File

@@ -33,6 +33,7 @@ TEST(ShaderLoader, CanLoad) {
EXPECT_TRUE(loader.CanLoad("test.hlsl")); EXPECT_TRUE(loader.CanLoad("test.hlsl"));
EXPECT_TRUE(loader.CanLoad(GetBuiltinForwardLitShaderPath())); EXPECT_TRUE(loader.CanLoad(GetBuiltinForwardLitShaderPath()));
EXPECT_TRUE(loader.CanLoad(GetBuiltinObjectIdShaderPath())); EXPECT_TRUE(loader.CanLoad(GetBuiltinObjectIdShaderPath()));
EXPECT_TRUE(loader.CanLoad(GetBuiltinObjectIdOutlineShaderPath()));
EXPECT_TRUE(loader.CanLoad(GetBuiltinInfiniteGridShaderPath())); EXPECT_TRUE(loader.CanLoad(GetBuiltinInfiniteGridShaderPath()));
EXPECT_FALSE(loader.CanLoad("test.txt")); EXPECT_FALSE(loader.CanLoad("test.txt"));
EXPECT_FALSE(loader.CanLoad("test.png")); EXPECT_FALSE(loader.CanLoad("test.png"));
@@ -154,6 +155,31 @@ TEST(ShaderLoader, LoadBuiltinInfiniteGridShaderBuildsD3D12Variants) {
delete shader; delete shader;
} }
TEST(ShaderLoader, LoadBuiltinObjectIdOutlineShaderBuildsD3D12Variants) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinObjectIdOutlineShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(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) { TEST(ShaderLoader, ResourceManagerLazilyLoadsBuiltinForwardLitShader) {
ResourceManager& manager = ResourceManager::Get(); ResourceManager& manager = ResourceManager::Get();
manager.Shutdown(); manager.Shutdown();
@@ -167,6 +193,10 @@ TEST(ShaderLoader, ResourceManagerLazilyLoadsBuiltinForwardLitShader) {
ASSERT_TRUE(objectIdShaderHandle.IsValid()); ASSERT_TRUE(objectIdShaderHandle.IsValid());
ASSERT_NE(objectIdShaderHandle->FindPass("ObjectId"), nullptr); ASSERT_NE(objectIdShaderHandle->FindPass("ObjectId"), nullptr);
ResourceHandle<Shader> objectIdOutlineShaderHandle = manager.Load<Shader>(GetBuiltinObjectIdOutlineShaderPath());
ASSERT_TRUE(objectIdOutlineShaderHandle.IsValid());
ASSERT_NE(objectIdOutlineShaderHandle->FindPass("ObjectIdOutline"), nullptr);
ResourceHandle<Shader> infiniteGridShaderHandle = manager.Load<Shader>(GetBuiltinInfiniteGridShaderPath()); ResourceHandle<Shader> infiniteGridShaderHandle = manager.Load<Shader>(GetBuiltinInfiniteGridShaderPath());
ASSERT_TRUE(infiniteGridShaderHandle.IsValid()); ASSERT_TRUE(infiniteGridShaderHandle.IsValid());
ASSERT_NE(infiniteGridShaderHandle->FindPass("InfiniteGrid"), nullptr); ASSERT_NE(infiniteGridShaderHandle->FindPass("InfiniteGrid"), nullptr);