Formalize renderer material contracts and harden backpack import

This commit is contained in:
2026-04-08 04:27:21 +08:00
parent 7be3b2cc45
commit 6113ed92b0
18 changed files with 534 additions and 326 deletions

View File

@@ -323,21 +323,6 @@ BuiltinDepthStylePassBase::ResolvedShaderPass BuiltinDepthStylePassBase::Resolve
}
}
if (!shaderHasExplicitBuiltinMetadata &&
ownerMaterial != nullptr &&
ownerMaterial->HasLegacyShaderPassHint()) {
const Resources::ShaderPass* explicitPass = shader->FindPass(ownerMaterial->GetLegacyShaderPassHint());
if (explicitPass != nullptr &&
::XCEngine::Rendering::Detail::ShaderPassHasGraphicsVariants(
*shader,
explicitPass->name,
backend,
keywordSet) &&
tryAcceptPass(*explicitPass)) {
return true;
}
}
return false;
};
@@ -474,7 +459,8 @@ RHI::RHIPipelineState* BuiltinDepthStylePassBase::GetOrCreatePipelineState(
}
PipelineStateKey pipelineKey = {};
pipelineKey.renderState = ResolveEffectiveRenderState(resolvedShaderPass.pass, material);
pipelineKey.renderState =
BuildStaticPipelineRenderStateKey(ResolveEffectiveRenderState(resolvedShaderPass.pass, material));
pipelineKey.shader = resolvedShaderPass.shader;
pipelineKey.passName = resolvedShaderPass.passName;
pipelineKey.keywordSignature = ::XCEngine::Rendering::Detail::BuildShaderKeywordSignature(keywordSet);
@@ -714,6 +700,8 @@ bool BuiltinDepthStylePassBase::DrawVisibleItem(
visibleItem.gameObject->GetName().c_str()).CStr());
return false;
}
const Resources::MaterialRenderState effectiveRenderState =
ResolveEffectiveRenderState(resolvedShaderPass.pass, material);
PassLayoutKey passLayoutKey = {};
passLayoutKey.shader = resolvedShaderPass.shader;
@@ -764,38 +752,12 @@ bool BuiltinDepthStylePassBase::DrawVisibleItem(
};
MaterialConstantPayloadView materialConstants = ResolveSchemaMaterialConstantPayload(material);
FallbackPerMaterialConstants fallbackMaterialConstants = {};
if (!materialConstants.IsValid()) {
const BuiltinForwardMaterialData materialData = BuildBuiltinForwardMaterialData(material);
fallbackMaterialConstants.baseColorFactor = materialData.baseColorFactor;
fallbackMaterialConstants.alphaCutoffParams = Math::Vector4(
materialData.alphaCutoff,
0.0f,
0.0f,
0.0f);
static const Resources::MaterialConstantFieldDesc kFallbackMaterialConstantFields[] = {
{
Containers::String("_BaseColor"),
Resources::MaterialPropertyType::Float4,
0,
sizeof(Math::Vector4),
sizeof(Math::Vector4)
},
{
Containers::String("_Cutoff"),
Resources::MaterialPropertyType::Float,
sizeof(Math::Vector4),
sizeof(float),
sizeof(Math::Vector4)
}
};
materialConstants.data = &fallbackMaterialConstants;
materialConstants.size = sizeof(fallbackMaterialConstants);
materialConstants.layout = {
kFallbackMaterialConstantFields,
2,
sizeof(fallbackMaterialConstants)
};
if (passLayout->material.IsValid() && !materialConstants.IsValid()) {
Debug::Logger::Get().Error(
Debug::LogCategory::Rendering,
(Containers::String("BuiltinDepthStylePassBase requires a schema-backed material constant payload for ") +
visibleItem.gameObject->GetName().c_str()).CStr());
return false;
}
RHI::RHIResourceView* baseColorTextureView = ResolveTextureView(visibleItem);
@@ -879,6 +841,8 @@ bool BuiltinDepthStylePassBase::DrawVisibleItem(
passLayout->pipelineLayout);
}
ApplyDynamicRenderState(effectiveRenderState, *commandList);
if (visibleItem.hasSection) {
const Containers::Array<Resources::MeshSection>& sections = visibleItem.mesh->GetSections();
if (visibleItem.sectionIndex >= sections.Size()) {

View File

@@ -51,20 +51,6 @@ const Resources::ShaderPass* FindCompatibleSurfacePass(
}
}
if (!shaderHasExplicitBuiltinMetadata &&
material != nullptr &&
material->HasLegacyShaderPassHint()) {
const Resources::ShaderPass* explicitPass = shader.FindPass(material->GetLegacyShaderPassHint());
if (explicitPass != nullptr &&
::XCEngine::Rendering::Detail::ShaderPassHasGraphicsVariants(
shader,
explicitPass->name,
backend,
keywordSet)) {
return explicitPass;
}
}
if (shaderHasExplicitBuiltinMetadata) {
return nullptr;
}
@@ -303,7 +289,8 @@ RHI::RHIPipelineState* BuiltinForwardPipeline::GetOrCreatePipelineState(
}
PipelineStateKey pipelineKey = {};
pipelineKey.renderState = ResolveEffectiveRenderState(resolvedShaderPass.pass, material);
pipelineKey.renderState =
BuildStaticPipelineRenderStateKey(ResolveEffectiveRenderState(resolvedShaderPass.pass, material));
pipelineKey.shader = resolvedShaderPass.shader;
pipelineKey.passName = resolvedShaderPass.passName;
pipelineKey.keywordSignature = ::XCEngine::Rendering::Detail::BuildShaderKeywordSignature(keywordSet);
@@ -682,6 +669,8 @@ bool BuiltinForwardPipeline::DrawVisibleItem(
if (resolvedShaderPass.shader == nullptr || resolvedShaderPass.pass == nullptr) {
return false;
}
const Resources::MaterialRenderState effectiveRenderState =
ResolveEffectiveRenderState(resolvedShaderPass.pass, material);
PassLayoutKey passLayoutKey = {};
passLayoutKey.shader = resolvedShaderPass.shader;
@@ -705,38 +694,11 @@ bool BuiltinForwardPipeline::DrawVisibleItem(
}
MaterialConstantPayloadView materialConstants = ResolveSchemaMaterialConstantPayload(material);
FallbackPerMaterialConstants fallbackMaterialConstants = {};
if (!materialConstants.IsValid()) {
const BuiltinForwardMaterialData materialData = BuildBuiltinForwardMaterialData(material);
fallbackMaterialConstants.baseColorFactor = materialData.baseColorFactor;
fallbackMaterialConstants.alphaCutoffParams = Math::Vector4(
materialData.alphaCutoff,
0.0f,
0.0f,
0.0f);
static const Resources::MaterialConstantFieldDesc kFallbackMaterialConstantFields[] = {
{
Containers::String("_BaseColor"),
Resources::MaterialPropertyType::Float4,
0,
sizeof(Math::Vector4),
sizeof(Math::Vector4)
},
{
Containers::String("_Cutoff"),
Resources::MaterialPropertyType::Float,
sizeof(Math::Vector4),
sizeof(float),
sizeof(Math::Vector4)
}
};
materialConstants.data = &fallbackMaterialConstants;
materialConstants.size = sizeof(fallbackMaterialConstants);
materialConstants.layout = {
kFallbackMaterialConstantFields,
2,
sizeof(fallbackMaterialConstants)
};
if (passLayout->material.IsValid() && !materialConstants.IsValid()) {
Debug::Logger::Get().Error(
Debug::LogCategory::Rendering,
"BuiltinForwardPipeline requires a schema-backed material constant payload");
return false;
}
if (passLayout->descriptorSetCount > 0) {
@@ -807,6 +769,8 @@ bool BuiltinForwardPipeline::DrawVisibleItem(
passLayout->pipelineLayout);
}
ApplyDynamicRenderState(effectiveRenderState, *commandList);
if (visibleItem.hasSection) {
const Containers::Array<Resources::MeshSection>& sections = visibleItem.mesh->GetSections();
if (visibleItem.sectionIndex >= sections.Size()) {

View File

@@ -15,6 +15,7 @@
#include <assimp/postprocess.h>
#include <assimp/scene.h>
#include <algorithm>
#include <cctype>
#include <cstring>
#include <filesystem>
#include <fstream>
@@ -43,6 +44,13 @@ struct TextureImportContext {
TextureLoader textureLoader;
std::unordered_map<std::string, Texture*> textureCache;
std::vector<Texture*> ownedTextures;
struct ObjMaterialTextureFallbacks {
std::filesystem::path baseColorPath;
std::filesystem::path normalPath;
std::filesystem::path specularPath;
};
bool objMaterialFallbacksInitialized = false;
std::unordered_map<std::string, ObjMaterialTextureFallbacks> objMaterialFallbacks;
};
Math::Bounds ComputeBounds(const std::vector<StaticMeshVertex>& vertices) {
@@ -117,6 +125,38 @@ Containers::String ToContainersString(const std::string& value) {
return Containers::String(value.c_str());
}
std::string ToLowerCopy(const std::string& value) {
std::string lowered = value;
std::transform(lowered.begin(), lowered.end(), lowered.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
return lowered;
}
std::string TrimCopy(const std::string& value) {
const auto begin = std::find_if_not(value.begin(), value.end(), [](unsigned char ch) {
return std::isspace(ch) != 0;
});
if (begin == value.end()) {
return std::string();
}
const auto end = std::find_if_not(value.rbegin(), value.rend(), [](unsigned char ch) {
return std::isspace(ch) != 0;
}).base();
return std::string(begin, end);
}
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;
}
Containers::String BuildSubResourcePath(const Containers::String& sourcePath,
const char* category,
Core::uint32 index,
@@ -171,6 +211,20 @@ Containers::String NormalizePathString(const std::filesystem::path& path) {
return Containers::String(path.lexically_normal().generic_string().c_str());
}
std::filesystem::path ResolveSourcePathAbsolute(const Containers::String& sourcePath) {
std::filesystem::path resolvedPath(sourcePath.CStr());
if (resolvedPath.is_absolute()) {
return resolvedPath.lexically_normal();
}
const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot();
if (!resourceRoot.Empty()) {
return (std::filesystem::path(resourceRoot.CStr()) / resolvedPath).lexically_normal();
}
return resolvedPath.lexically_normal();
}
Containers::String ResolveArtifactDependencyPath(const Containers::String& dependencyPath,
const Containers::String& ownerArtifactPath) {
if (dependencyPath.Empty()) {
@@ -390,6 +444,140 @@ Texture* LoadMaterialTexture(const aiMaterial& assimpMaterial,
return nullptr;
}
enum class ObjMaterialTextureSemantic {
BaseColor,
Normal,
Specular
};
void InitializeObjMaterialTextureFallbacks(TextureImportContext& context) {
if (context.objMaterialFallbacksInitialized) {
return;
}
context.objMaterialFallbacksInitialized = true;
const std::filesystem::path sourcePath = ResolveSourcePathAbsolute(context.sourcePath);
if (ToLowerCopy(sourcePath.extension().string()) != ".obj") {
return;
}
std::ifstream objInput(sourcePath);
if (!objInput.is_open()) {
return;
}
std::vector<std::filesystem::path> mtlPaths;
const std::filesystem::path sourceDirectory = sourcePath.parent_path();
std::string line;
while (std::getline(objInput, 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)) {
mtlPaths.push_back((sourceDirectory / token).lexically_normal());
}
}
for (const std::filesystem::path& mtlPath : mtlPaths) {
std::ifstream mtlInput(mtlPath);
if (!mtlInput.is_open()) {
continue;
}
const std::filesystem::path mtlDirectory = mtlPath.parent_path();
std::string currentMaterialName;
while (std::getline(mtlInput, line)) {
const std::string trimmed = TrimCopy(line);
if (trimmed.empty() || trimmed[0] == '#') {
continue;
}
const std::vector<std::string> tokens = SplitWhitespaceTokens(trimmed);
if (tokens.empty()) {
continue;
}
const std::string keyword = ToLowerCopy(tokens.front());
if (keyword == "newmtl") {
currentMaterialName = TrimCopy(trimmed.substr(tokens.front().size()));
continue;
}
if (currentMaterialName.empty() || tokens.size() < 2) {
continue;
}
const std::string& textureToken = tokens.back();
if (textureToken.empty() || textureToken[0] == '-') {
continue;
}
TextureImportContext::ObjMaterialTextureFallbacks& fallbacks =
context.objMaterialFallbacks[currentMaterialName];
const std::filesystem::path resolvedTexturePath =
(mtlDirectory / textureToken).lexically_normal();
if (keyword == "map_kd") {
fallbacks.baseColorPath = resolvedTexturePath;
} else if (keyword == "map_bump" || keyword == "bump" || keyword == "norm") {
fallbacks.normalPath = resolvedTexturePath;
} else if (keyword == "map_ks") {
fallbacks.specularPath = resolvedTexturePath;
}
}
}
}
Texture* LoadObjMaterialTextureFallback(const aiMaterial& assimpMaterial,
ObjMaterialTextureSemantic semantic,
const TextureImportSettings& settings,
TextureImportContext& context) {
InitializeObjMaterialTextureFallbacks(context);
aiString assimpName;
if (assimpMaterial.Get(AI_MATKEY_NAME, assimpName) != AI_SUCCESS ||
assimpName.length == 0) {
return nullptr;
}
const auto fallbackIt = context.objMaterialFallbacks.find(std::string(assimpName.C_Str()));
if (fallbackIt == context.objMaterialFallbacks.end()) {
return nullptr;
}
const TextureImportContext::ObjMaterialTextureFallbacks& fallbacks = fallbackIt->second;
const std::filesystem::path* fallbackPath = nullptr;
switch (semantic) {
case ObjMaterialTextureSemantic::BaseColor:
fallbackPath = &fallbacks.baseColorPath;
break;
case ObjMaterialTextureSemantic::Normal:
fallbackPath = &fallbacks.normalPath;
break;
case ObjMaterialTextureSemantic::Specular:
fallbackPath = &fallbacks.specularPath;
break;
default:
break;
}
if (fallbackPath == nullptr || fallbackPath->empty()) {
return nullptr;
}
return LoadExternalTexture(*fallbackPath, settings, context);
}
bool HasMaterialTexture(
const aiMaterial& assimpMaterial,
std::initializer_list<aiTextureType> textureTypes) {
@@ -438,15 +626,21 @@ void ImportMaterialTextures(const aiMaterial& assimpMaterial,
Material& material,
TextureImportContext& context) {
auto assignTexture = [&](const char* propertyName,
ObjMaterialTextureSemantic semantic,
std::initializer_list<aiTextureType> textureTypes) {
const TextureImportSettings settings = BuildMaterialTextureImportSettings(propertyName);
Texture* texture = LoadMaterialTexture(assimpMaterial, textureTypes, settings, context);
if (texture == nullptr) {
texture = LoadObjMaterialTextureFallback(assimpMaterial, semantic, settings, context);
}
if (texture != nullptr) {
material.SetTexture(Containers::String(propertyName), ResourceHandle<Texture>(texture));
}
};
assignTexture("_MainTex", { aiTextureType_BASE_COLOR, aiTextureType_DIFFUSE });
assignTexture("_MainTex",
ObjMaterialTextureSemantic::BaseColor,
{ aiTextureType_BASE_COLOR, aiTextureType_DIFFUSE });
}
Material* ImportSingleMaterial(const aiMaterial& assimpMaterial,