#define NOMINMAX #include #include #include "../RenderingIntegrationImageAssert.h" #include "../RenderingIntegrationMain.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "../../../RHI/integration/fixtures/RHIIntegrationFixture.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace XCEngine::Components; using namespace XCEngine::Rendering; using namespace XCEngine::Resources; using namespace XCEngine::RHI; using namespace XCEngine::RHI::Integration; using namespace RenderingIntegrationTestUtils; namespace { constexpr const char* kD3D12Screenshot = "nahida_preview_scene_d3d12.ppm"; constexpr uint32_t kFrameWidth = 1280; constexpr uint32_t kFrameHeight = 720; constexpr int kWarmupFrames = 45; enum class DiagnosticMode { Toon, NoShadows, ForwardLit, Unlit }; DiagnosticMode GetDiagnosticMode() { const char* value = std::getenv("XC_NAHIDA_DIAG_MODE"); if (value == nullptr) { return DiagnosticMode::Toon; } const std::string mode(value); if (mode == "toon" || mode == "original") { return DiagnosticMode::Toon; } if (mode == "no_shadows") { return DiagnosticMode::NoShadows; } if (mode == "forward_lit") { return DiagnosticMode::ForwardLit; } if (mode == "unlit") { return DiagnosticMode::Unlit; } return DiagnosticMode::Toon; } const char* GetDiagnosticModeName(DiagnosticMode mode) { switch (mode) { case DiagnosticMode::Toon: return "toon"; case DiagnosticMode::NoShadows: return "no_shadows"; case DiagnosticMode::ForwardLit: return "forward_lit"; case DiagnosticMode::Unlit: return "unlit"; default: return "unknown"; } } const char* GetGoldenFileName(DiagnosticMode mode) { switch (mode) { case DiagnosticMode::Toon: return "GT.ppm"; case DiagnosticMode::NoShadows: return "GT.no_shadows.ppm"; case DiagnosticMode::ForwardLit: return "GT.forward_lit.ppm"; case DiagnosticMode::Unlit: return "GT.unlit.ppm"; default: return "GT.ppm"; } } constexpr const char* kToonShaderPath = "Assets/Shaders/Toon.shader"; constexpr const char* kNahidaTextureRoot = "Assets/Models/nahida/"; XCEngine::Containers::String RemapMaterialPropertyName( const Material& targetMaterial, const XCEngine::Containers::String& sourceName) { if (targetMaterial.HasProperty(sourceName)) { return sourceName; } if (sourceName == XCEngine::Containers::String("_BaseMap") && targetMaterial.HasProperty("_MainTex")) { return XCEngine::Containers::String("_MainTex"); } if (sourceName == XCEngine::Containers::String("_MainTex") && targetMaterial.HasProperty("_BaseMap")) { return XCEngine::Containers::String("_BaseMap"); } if (sourceName == XCEngine::Containers::String("_Color") && targetMaterial.HasProperty("_BaseColor")) { return XCEngine::Containers::String("_BaseColor"); } if (sourceName == XCEngine::Containers::String("_BaseColor") && targetMaterial.HasProperty("_Color")) { return XCEngine::Containers::String("_Color"); } return XCEngine::Containers::String(); } bool IsNahidaCharacterRenderable(const GameObject* gameObject) { if (gameObject == nullptr) { return false; } const std::string& name = gameObject->GetName(); return name.rfind("Body_Mesh", 0) == 0 || name == "Brow" || name == "EyeStar" || name == "Face" || name == "Face_Eye"; } std::string BuildNahidaTextureAssetPath(const char* fileName) { return std::string(kNahidaTextureRoot) + fileName; } void SetMaterialFloatIfPresent(Material* material, const char* name, float value) { if (material == nullptr || name == nullptr || !material->HasProperty(name)) { return; } material->SetFloat(name, value); } void SetMaterialFloat4IfPresent(Material* material, const char* name, const XCEngine::Math::Vector4& value) { if (material == nullptr || name == nullptr || !material->HasProperty(name)) { return; } material->SetFloat4(name, value); } void SetMaterialTexturePath(Material* material, const char* name, const char* fileName) { if (material == nullptr || name == nullptr || fileName == nullptr || !material->HasProperty(name)) { return; } const std::string assetPath = BuildNahidaTextureAssetPath(fileName); const ResourceHandle texture = ResourceManager::Get().Load(assetPath.c_str()); if (texture.Get() != nullptr) { material->SetTexture(name, texture); return; } material->SetTextureAssetRef(name, AssetRef(), XCEngine::Containers::String(assetPath.c_str())); } void ApplyNahidaSharedToonDefaults(Material* material) { if (material == nullptr) { return; } material->ClearKeywords(); material->ClearTags(); material->SetRenderStateOverrideEnabled(false); SetMaterialFloat4IfPresent(material, "_BaseColor", XCEngine::Math::Vector4(1.0f, 1.0f, 1.0f, 1.0f)); SetMaterialFloatIfPresent(material, "_Cutoff", 0.5f); SetMaterialFloatIfPresent(material, "_IsDay", 1.0f); SetMaterialFloatIfPresent(material, "_DoubleSided", 0.0f); SetMaterialFloat4IfPresent(material, "_LightDirectionMultiplier", XCEngine::Math::Vector4(1.0f, 0.5f, 1.0f, 0.0f)); SetMaterialFloatIfPresent(material, "_ShadowOffset", 0.1f); SetMaterialFloatIfPresent(material, "_ShadowSmoothness", 0.4f); SetMaterialFloat4IfPresent(material, "_ShadowColor", XCEngine::Math::Vector4(1.1f, 1.1f, 1.1f, 1.0f)); SetMaterialFloatIfPresent(material, "_UseCustomMaterialType", 0.0f); SetMaterialFloatIfPresent(material, "_CustomMaterialType", 1.0f); SetMaterialFloatIfPresent(material, "_UseEmission", 0.0f); SetMaterialFloatIfPresent(material, "_EmissionIntensity", 0.2f); SetMaterialFloatIfPresent(material, "_UseNormalMap", 0.0f); SetMaterialFloatIfPresent(material, "_IsFace", 0.0f); SetMaterialFloat4IfPresent(material, "_FaceDirection", XCEngine::Math::Vector4(0.0f, 0.0f, 1.0f, 0.0f)); SetMaterialFloatIfPresent(material, "_FaceShadowOffset", 0.0f); SetMaterialFloat4IfPresent(material, "_FaceBlushColor", XCEngine::Math::Vector4(1.0f, 0.72156864f, 0.69803923f, 1.0f)); SetMaterialFloatIfPresent(material, "_FaceBlushStrength", 0.0f); SetMaterialFloatIfPresent(material, "_UseSpecular", 0.0f); SetMaterialFloatIfPresent(material, "_SpecularSmoothness", 5.0f); SetMaterialFloatIfPresent(material, "_NonmetallicIntensity", 0.3f); SetMaterialFloatIfPresent(material, "_MetallicIntensity", 8.0f); SetMaterialFloatIfPresent(material, "_UseRim", 0.0f); SetMaterialFloatIfPresent(material, "_RimOffset", 5.0f); SetMaterialFloatIfPresent(material, "_RimThreshold", 0.5f); SetMaterialFloatIfPresent(material, "_RimIntensity", 0.5f); SetMaterialFloatIfPresent(material, "_UseSmoothNormal", 0.0f); SetMaterialFloatIfPresent(material, "_OutlineWidth", 1.6f); SetMaterialFloat4IfPresent(material, "_OutlineWidthParams", XCEngine::Math::Vector4(0.0f, 6.0f, 0.1f, 0.6f)); SetMaterialFloatIfPresent(material, "_OutlineZOffset", 0.1f); SetMaterialFloat4IfPresent(material, "_ScreenOffset", XCEngine::Math::Vector4(0.0f, 0.0f, 0.0f, 0.0f)); SetMaterialFloat4IfPresent(material, "_OutlineColor", XCEngine::Math::Vector4(0.5176471f, 0.35686275f, 0.34117648f, 1.0f)); SetMaterialFloat4IfPresent(material, "_OutlineColor2", XCEngine::Math::Vector4(0.3529412f, 0.3529412f, 0.3529412f, 1.0f)); SetMaterialFloat4IfPresent(material, "_OutlineColor3", XCEngine::Math::Vector4(0.47058824f, 0.47058824f, 0.5647059f, 1.0f)); SetMaterialFloat4IfPresent(material, "_OutlineColor4", XCEngine::Math::Vector4(0.5176471f, 0.35686275f, 0.34117648f, 1.0f)); SetMaterialFloat4IfPresent(material, "_OutlineColor5", XCEngine::Math::Vector4(0.35f, 0.35f, 0.35f, 1.0f)); } void ApplyNahidaSurfaceRecipe( Material* material, const char* baseMapFile, const char* lightMapFile, const char* normalMapFile, const char* shadowRampFile, bool doubleSided, bool useSmoothNormal, float outlineZOffset, const XCEngine::Math::Vector4* outlineColor = nullptr) { if (material == nullptr) { return; } ApplyNahidaSharedToonDefaults(material); SetMaterialTexturePath(material, "_BaseMap", baseMapFile); SetMaterialTexturePath(material, "_LightMap", lightMapFile); SetMaterialTexturePath(material, "_NormalMap", normalMapFile); SetMaterialTexturePath(material, "_ShadowRamp", shadowRampFile); SetMaterialTexturePath(material, "_MetalMap", "Avatar_Tex_MetalMap.png"); SetMaterialFloatIfPresent(material, "_UseEmission", 1.0f); SetMaterialFloatIfPresent(material, "_UseNormalMap", 1.0f); SetMaterialFloatIfPresent(material, "_UseRim", 1.0f); SetMaterialFloatIfPresent(material, "_UseSpecular", 1.0f); SetMaterialFloatIfPresent(material, "_UseSmoothNormal", useSmoothNormal ? 1.0f : 0.0f); SetMaterialFloatIfPresent(material, "_DoubleSided", doubleSided ? 1.0f : 0.0f); if (outlineZOffset != 0.0f) { SetMaterialFloatIfPresent(material, "_OutlineZOffset", outlineZOffset); } if (outlineColor != nullptr) { SetMaterialFloat4IfPresent(material, "_OutlineColor", *outlineColor); } material->EnableKeyword("_EMISSION"); material->EnableKeyword("_NORMAL_MAP"); material->EnableKeyword("_SPECULAR"); material->EnableKeyword("_RIM"); if (doubleSided) { MaterialRenderState renderState = material->GetRenderState(); renderState.cullMode = MaterialCullMode::None; material->SetRenderState(renderState); } } void ApplyNahidaFaceRecipe(Material* material, bool eyebrowVariant) { if (material == nullptr) { return; } ApplyNahidaSharedToonDefaults(material); SetMaterialTexturePath(material, "_BaseMap", "Avatar_Loli_Catalyst_Nahida_Tex_Face_Diffuse.png"); SetMaterialTexturePath(material, "_FaceLightMap", "Avatar_Loli_Tex_FaceLightmap.png"); SetMaterialTexturePath(material, "_FaceShadow", "Avatar_Tex_Face_Shadow.png"); SetMaterialTexturePath(material, "_ShadowRamp", "Avatar_Loli_Catalyst_Nahida_Tex_Body_Shadow_Ramp.png"); SetMaterialFloatIfPresent(material, "_IsFace", 1.0f); SetMaterialFloatIfPresent(material, "_UseCustomMaterialType", 1.0f); SetMaterialFloatIfPresent(material, "_UseRim", 1.0f); SetMaterialFloat4IfPresent( material, "_FaceDirection", XCEngine::Math::Vector4(-0.0051237254f, -0.11509954f, -0.99334073f, 0.0f)); if (eyebrowVariant) { SetMaterialFloatIfPresent(material, "_OutlineWidth", 0.0f); SetMaterialFloat4IfPresent( material, "_BaseColor", XCEngine::Math::Vector4(0.9764706f, 0.80103135f, 0.76164705f, 1.0f)); } else { SetMaterialFloatIfPresent(material, "_FaceBlushStrength", 0.3f); SetMaterialFloatIfPresent(material, "_OutlineZOffset", 0.5f); } material->EnableKeyword("_IS_FACE"); material->EnableKeyword("_RIM"); } void ApplyNahidaToonRecipe(Material* material, const GameObject* owner) { if (material == nullptr || owner == nullptr) { return; } const std::string& name = owner->GetName(); if (name == "Body_Mesh0") { const XCEngine::Math::Vector4 outlineColor(0.2784314f, 0.18039216f, 0.14901961f, 1.0f); ApplyNahidaSurfaceRecipe( material, "Avatar_Loli_Catalyst_Nahida_Tex_Hair_Diffuse.png", "Avatar_Loli_Catalyst_Nahida_Tex_Hair_Lightmap.png", "Avatar_Loli_Catalyst_Nahida_Tex_Hair_Normalmap.png", "Avatar_Loli_Catalyst_Nahida_Tex_Hair_Shadow_Ramp.png", false, true, 0.0f, &outlineColor); return; } if (name == "Body_Mesh1") { ApplyNahidaSurfaceRecipe( material, "Avatar_Loli_Catalyst_Nahida_Tex_Body_Diffuse.png", "Avatar_Loli_Catalyst_Nahida_Tex_Body_Lightmap.png", "Avatar_Loli_Catalyst_Nahida_Tex_Body_Normalmap.png", "Avatar_Loli_Catalyst_Nahida_Tex_Body_Shadow_Ramp.png", false, true, 0.0f); return; } if (name == "Body_Mesh2") { ApplyNahidaSurfaceRecipe( material, "Avatar_Loli_Catalyst_Nahida_Tex_Body_Diffuse.png", "Avatar_Loli_Catalyst_Nahida_Tex_Body_Lightmap.png", "Avatar_Loli_Catalyst_Nahida_Tex_Body_Normalmap.png", "Avatar_Loli_Catalyst_Nahida_Tex_Body_Shadow_Ramp.png", true, true, 0.5f); return; } if (name == "Body_Mesh3") { ApplyNahidaSurfaceRecipe( material, "Avatar_Loli_Catalyst_Nahida_Tex_Hair_Diffuse.png", "Avatar_Loli_Catalyst_Nahida_Tex_Hair_Lightmap.png", "Avatar_Loli_Catalyst_Nahida_Tex_Hair_Normalmap.png", "Avatar_Loli_Catalyst_Nahida_Tex_Hair_Shadow_Ramp.png", true, false, 0.5f); return; } if (name == "Brow") { ApplyNahidaFaceRecipe(material, true); return; } if (name == "EyeStar" || name == "Face" || name == "Face_Eye") { ApplyNahidaFaceRecipe(material, false); } } std::unordered_set GetIsolationObjectNames() { std::unordered_set result; const char* value = std::getenv("XC_NAHIDA_DIAG_ONLY"); if (value == nullptr) { return result; } std::stringstream stream(value); std::string token; while (std::getline(stream, token, ',')) { const size_t first = token.find_first_not_of(" \t\r\n"); if (first == std::string::npos) { continue; } const size_t last = token.find_last_not_of(" \t\r\n"); result.emplace(token.substr(first, last - first + 1)); } return result; } std::string DescribeVertexAttributes(VertexAttribute attributes) { std::string result; const auto appendFlag = [&result](const char* name) { if (!result.empty()) { result += "|"; } result += name; }; if (HasVertexAttribute(attributes, VertexAttribute::Position)) { appendFlag("Position"); } if (HasVertexAttribute(attributes, VertexAttribute::Normal)) { appendFlag("Normal"); } if (HasVertexAttribute(attributes, VertexAttribute::Tangent)) { appendFlag("Tangent"); } if (HasVertexAttribute(attributes, VertexAttribute::Bitangent)) { appendFlag("Bitangent"); } if (HasVertexAttribute(attributes, VertexAttribute::Color)) { appendFlag("Color"); } if (HasVertexAttribute(attributes, VertexAttribute::UV0)) { appendFlag("UV0"); } if (HasVertexAttribute(attributes, VertexAttribute::UV1)) { appendFlag("UV1"); } return result; } void DumpMeshDiagnostics(const char* label, const Mesh* mesh) { if (mesh == nullptr) { std::printf("[NahidaDiag] %s mesh=null\n", label); return; } const XCEngine::Math::Bounds& bounds = mesh->GetBounds(); std::printf( "[NahidaDiag] %s meshPath=%s vertices=%u indices=%u stride=%u attrs=%s center=(%.4f, %.4f, %.4f) extents=(%.4f, %.4f, %.4f) sections=%zu\n", label, mesh->GetPath().CStr(), mesh->GetVertexCount(), mesh->GetIndexCount(), mesh->GetVertexStride(), DescribeVertexAttributes(mesh->GetVertexAttributes()).c_str(), bounds.center.x, bounds.center.y, bounds.center.z, bounds.extents.x, bounds.extents.y, bounds.extents.z, mesh->GetSections().Size()); if (mesh->GetVertexStride() != sizeof(StaticMeshVertex) || mesh->GetVertexCount() == 0) { return; } const auto* vertices = static_cast(mesh->GetVertexData()); XCEngine::Math::Vector2 uvMin( std::numeric_limits::max(), std::numeric_limits::max()); XCEngine::Math::Vector2 uvMax( std::numeric_limits::lowest(), std::numeric_limits::lowest()); for (uint32_t vertexIndex = 0; vertexIndex < mesh->GetVertexCount(); ++vertexIndex) { uvMin.x = std::min(uvMin.x, vertices[vertexIndex].uv0.x); uvMin.y = std::min(uvMin.y, vertices[vertexIndex].uv0.y); uvMax.x = std::max(uvMax.x, vertices[vertexIndex].uv0.x); uvMax.y = std::max(uvMax.y, vertices[vertexIndex].uv0.y); } const StaticMeshVertex& firstVertex = vertices[0]; std::printf( "[NahidaDiag] %s firstVertex pos=(%.4f, %.4f, %.4f) normal=(%.4f, %.4f, %.4f) tangent=(%.4f, %.4f, %.4f) bitangent=(%.4f, %.4f, %.4f) uv0=(%.4f, %.4f) uv1=(%.4f, %.4f) color=(%.4f, %.4f, %.4f, %.4f)\n", label, firstVertex.position.x, firstVertex.position.y, firstVertex.position.z, firstVertex.normal.x, firstVertex.normal.y, firstVertex.normal.z, firstVertex.tangent.x, firstVertex.tangent.y, firstVertex.tangent.z, firstVertex.bitangent.x, firstVertex.bitangent.y, firstVertex.bitangent.z, firstVertex.uv0.x, firstVertex.uv0.y, firstVertex.uv1.x, firstVertex.uv1.y, firstVertex.color.x, firstVertex.color.y, firstVertex.color.z, firstVertex.color.w); std::printf( "[NahidaDiag] %s uvRange min=(%.4f, %.4f) max=(%.4f, %.4f)\n", label, uvMin.x, uvMin.y, uvMax.x, uvMax.y); } void DumpMaterialDiagnostics(const char* label, Material* material) { if (material == nullptr) { std::printf("[NahidaDiag] %s material=null\n", label); return; } std::printf( "[NahidaDiag] %s materialPath=%s shader=%s constants=%zu textures=%u keywords=%u renderQueue=%d\n", label, material->GetPath().CStr(), material->GetShader() != nullptr ? material->GetShader()->GetPath().CStr() : "", material->GetConstantLayout().Size(), material->GetTextureBindingCount(), material->GetKeywordCount(), material->GetRenderQueue()); std::printf( "[NahidaDiag] %s props _BaseColor=(%.3f, %.3f, %.3f, %.3f) _ShadowColor=(%.3f, %.3f, %.3f, %.3f) _UseNormalMap=%.3f _UseSpecular=%.3f _UseEmission=%.3f _UseRim=%.3f _IsFace=%.3f _UseCustomMaterialType=%.3f\n", label, material->GetFloat4("_BaseColor").x, material->GetFloat4("_BaseColor").y, material->GetFloat4("_BaseColor").z, material->GetFloat4("_BaseColor").w, material->GetFloat4("_ShadowColor").x, material->GetFloat4("_ShadowColor").y, material->GetFloat4("_ShadowColor").z, material->GetFloat4("_ShadowColor").w, material->GetFloat("_UseNormalMap"), material->GetFloat("_UseSpecular"), material->GetFloat("_UseEmission"), material->GetFloat("_UseRim"), material->GetFloat("_IsFace"), material->GetFloat("_UseCustomMaterialType")); for (uint32_t keywordIndex = 0; keywordIndex < material->GetKeywordCount(); ++keywordIndex) { std::printf("[NahidaDiag] %s keyword[%u]=%s\n", label, keywordIndex, material->GetKeyword(keywordIndex).CStr()); } for (uint32_t bindingIndex = 0; bindingIndex < material->GetTextureBindingCount(); ++bindingIndex) { const ResourceHandle textureHandle = material->GetTextureBindingTexture(bindingIndex); Texture* texture = textureHandle.Get(); std::printf( "[NahidaDiag] %s texture[%u] name=%s path=%s loaded=%d resolved=%s\n", label, bindingIndex, material->GetTextureBindingName(bindingIndex).CStr(), material->GetTextureBindingPath(bindingIndex).CStr(), texture != nullptr ? 1 : 0, texture != nullptr ? texture->GetPath().CStr() : ""); } } void DumpSceneDiagnostics(Scene& scene) { RenderSceneExtractor extractor; RenderSceneData sceneData = extractor.Extract(scene, nullptr, kFrameWidth, kFrameHeight); std::printf( "[NahidaDiag] extracted cameraPos=(%.3f, %.3f, %.3f) visibleItems=%zu additionalLights=%u\n", sceneData.cameraData.worldPosition.x, sceneData.cameraData.worldPosition.y, sceneData.cameraData.worldPosition.z, sceneData.visibleItems.size(), sceneData.lighting.additionalLightCount); for (size_t visibleIndex = 0; visibleIndex < sceneData.visibleItems.size(); ++visibleIndex) { const VisibleRenderItem& item = sceneData.visibleItems[visibleIndex]; const char* objectName = item.gameObject != nullptr ? item.gameObject->GetName().c_str() : ""; const char* meshPath = item.mesh != nullptr ? item.mesh->GetPath().CStr() : ""; const char* materialPath = item.material != nullptr ? item.material->GetPath().CStr() : ""; std::printf( "[NahidaDiag] visible[%zu] object=%s section=%u hasSection=%d materialIndex=%u queue=%d mesh=%s material=%s\n", visibleIndex, objectName, item.sectionIndex, item.hasSection ? 1 : 0, item.materialIndex, item.renderQueue, meshPath, materialPath); } } std::filesystem::path GetRepositoryRoot() { return std::filesystem::path(__FILE__).parent_path().parent_path().parent_path().parent_path().parent_path(); } std::filesystem::path GetProjectRoot() { return GetRepositoryRoot() / "project"; } std::filesystem::path GetScenePath() { return GetProjectRoot() / "Assets" / "Scenes" / "NahidaPreview.xc"; } std::filesystem::path GetAssimpDllPath() { return GetRepositoryRoot() / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll"; } class NahidaPreviewSceneTest : public RHIIntegrationFixture { protected: void SetUp() override; void TearDown() override; void RenderFrame() override; private: void InitializeProjectResources(); void PreloadSceneResources(); void PumpSceneLoads(std::chrono::milliseconds timeout); void TouchSceneMaterialsAndTextures(); void ApplyIsolationFilter(); void ApplyDiagnosticOverrides(); void DumpTargetDiagnostics(); RHIResourceView* GetCurrentBackBufferView(); Material* CreateOverrideMaterial(const Material* sourceMaterial, const char* shaderPath, const GameObject* owner); HMODULE m_assimpModule = nullptr; std::unique_ptr m_scene; std::unique_ptr m_sceneRenderer; std::vector> m_overrideMaterials; std::vector m_backBufferViews; RHITexture* m_depthTexture = nullptr; RHIResourceView* m_depthView = nullptr; }; void NahidaPreviewSceneTest::SetUp() { RHIIntegrationFixture::SetUp(); const std::filesystem::path assimpDllPath = GetAssimpDllPath(); ASSERT_TRUE(std::filesystem::exists(assimpDllPath)) << assimpDllPath.string(); m_assimpModule = LoadLibraryW(assimpDllPath.wstring().c_str()); ASSERT_NE(m_assimpModule, nullptr); InitializeProjectResources(); const std::filesystem::path scenePath = GetScenePath(); ASSERT_TRUE(std::filesystem::exists(scenePath)) << scenePath.string(); m_sceneRenderer = std::make_unique(); m_scene = std::make_unique("NahidaPreview"); m_scene->Load(scenePath.string()); ASSERT_NE(m_scene->Find("Preview Camera"), nullptr); ASSERT_NE(m_scene->Find("NahidaUnityModel"), nullptr); ASSERT_NE(m_scene->Find("Body_Mesh0"), nullptr); ASSERT_NE(m_scene->Find("Face"), nullptr); PreloadSceneResources(); auto* bodyMeshObject = m_scene->Find("Body_Mesh0"); ASSERT_NE(bodyMeshObject, nullptr); auto* bodyMeshFilter = bodyMeshObject->GetComponent(); ASSERT_NE(bodyMeshFilter, nullptr); Mesh* bodyMesh = bodyMeshFilter->GetMesh(); ASSERT_NE(bodyMesh, nullptr); EXPECT_TRUE(HasVertexAttribute(bodyMesh->GetVertexAttributes(), VertexAttribute::Color)); EXPECT_TRUE(HasVertexAttribute(bodyMesh->GetVertexAttributes(), VertexAttribute::UV1)); ApplyIsolationFilter(); ApplyDiagnosticOverrides(); DumpTargetDiagnostics(); TextureDesc depthDesc = {}; depthDesc.width = kFrameWidth; depthDesc.height = kFrameHeight; depthDesc.depth = 1; depthDesc.mipLevels = 1; depthDesc.arraySize = 1; depthDesc.format = static_cast(Format::D24_UNorm_S8_UInt); depthDesc.textureType = static_cast(XCEngine::RHI::TextureType::Texture2D); depthDesc.sampleCount = 1; depthDesc.sampleQuality = 0; depthDesc.flags = 0; m_depthTexture = GetDevice()->CreateTexture(depthDesc); ASSERT_NE(m_depthTexture, nullptr); ResourceViewDesc depthViewDesc = {}; depthViewDesc.format = static_cast(Format::D24_UNorm_S8_UInt); depthViewDesc.dimension = ResourceViewDimension::Texture2D; depthViewDesc.mipLevel = 0; m_depthView = GetDevice()->CreateDepthStencilView(m_depthTexture, depthViewDesc); ASSERT_NE(m_depthView, nullptr); m_backBufferViews.resize(2, nullptr); } void NahidaPreviewSceneTest::TearDown() { m_sceneRenderer.reset(); if (m_depthView != nullptr) { m_depthView->Shutdown(); delete m_depthView; m_depthView = nullptr; } if (m_depthTexture != nullptr) { m_depthTexture->Shutdown(); delete m_depthTexture; m_depthTexture = nullptr; } for (RHIResourceView*& backBufferView : m_backBufferViews) { if (backBufferView != nullptr) { backBufferView->Shutdown(); delete backBufferView; backBufferView = nullptr; } } m_backBufferViews.clear(); m_scene.reset(); ResourceManager& manager = ResourceManager::Get(); manager.UnloadAll(); manager.SetResourceRoot(""); manager.Shutdown(); if (m_assimpModule != nullptr) { FreeLibrary(m_assimpModule); m_assimpModule = nullptr; } RHIIntegrationFixture::TearDown(); } void NahidaPreviewSceneTest::InitializeProjectResources() { ResourceManager& manager = ResourceManager::Get(); manager.Initialize(); manager.SetResourceRoot(GetProjectRoot().string().c_str()); } void NahidaPreviewSceneTest::PreloadSceneResources() { ASSERT_NE(m_scene, nullptr); PumpSceneLoads(std::chrono::milliseconds(4000)); } void NahidaPreviewSceneTest::PumpSceneLoads(std::chrono::milliseconds timeout) { ResourceManager& manager = ResourceManager::Get(); const auto deadline = std::chrono::steady_clock::now() + timeout; do { TouchSceneMaterialsAndTextures(); manager.UpdateAsyncLoads(); if (!manager.IsAsyncLoading()) { TouchSceneMaterialsAndTextures(); manager.UpdateAsyncLoads(); if (!manager.IsAsyncLoading()) { break; } } std::this_thread::sleep_for(std::chrono::milliseconds(5)); } while (std::chrono::steady_clock::now() < deadline); } void NahidaPreviewSceneTest::TouchSceneMaterialsAndTextures() { ASSERT_NE(m_scene, nullptr); const std::vector meshFilters = m_scene->FindObjectsOfType(); for (MeshFilterComponent* meshFilter : meshFilters) { if (meshFilter == nullptr) { continue; } Mesh* mesh = meshFilter->GetMesh(); if (mesh == nullptr) { continue; } for (Material* material : mesh->GetMaterials()) { if (material == nullptr) { continue; } for (uint32_t bindingIndex = 0; bindingIndex < material->GetTextureBindingCount(); ++bindingIndex) { material->GetTextureBindingTexture(bindingIndex); } } } const std::vector meshRenderers = m_scene->FindObjectsOfType(); for (MeshRendererComponent* meshRenderer : meshRenderers) { if (meshRenderer == nullptr) { continue; } for (size_t materialIndex = 0; materialIndex < meshRenderer->GetMaterialCount(); ++materialIndex) { Material* material = meshRenderer->GetMaterial(materialIndex); if (material == nullptr) { continue; } for (uint32_t bindingIndex = 0; bindingIndex < material->GetTextureBindingCount(); ++bindingIndex) { material->GetTextureBindingTexture(bindingIndex); } } } } void NahidaPreviewSceneTest::ApplyIsolationFilter() { ASSERT_NE(m_scene, nullptr); const std::unordered_set isolatedObjects = GetIsolationObjectNames(); if (isolatedObjects.empty()) { return; } std::printf("[NahidaDiag] isolation="); bool first = true; for (const std::string& name : isolatedObjects) { std::printf("%s%s", first ? "" : ",", name.c_str()); first = false; } std::printf("\n"); const std::vector meshRenderers = m_scene->FindObjectsOfType(); for (MeshRendererComponent* meshRenderer : meshRenderers) { if (meshRenderer == nullptr || meshRenderer->GetGameObject() == nullptr) { continue; } GameObject* gameObject = meshRenderer->GetGameObject(); const bool keep = isolatedObjects.find(gameObject->GetName()) != isolatedObjects.end(); gameObject->SetActive(keep); } } Material* NahidaPreviewSceneTest::CreateOverrideMaterial( const Material* sourceMaterial, const char* shaderPath, const GameObject* owner) { if (sourceMaterial == nullptr || shaderPath == nullptr) { return nullptr; } const ResourceHandle shader = ResourceManager::Get().Load(shaderPath); if (shader.Get() == nullptr) { return nullptr; } auto material = std::make_unique(); IResource::ConstructParams params = {}; params.name = XCEngine::Containers::String("NahidaDiagnosticMaterial"); params.path = XCEngine::Containers::String(("memory://nahida/" + std::string(shaderPath)).c_str()); params.guid = ResourceGUID::Generate(params.path); material->Initialize(params); material->SetShader(shader); material->SetRenderQueue(sourceMaterial->GetRenderQueue()); if (std::string(shaderPath) == kToonShaderPath) { ApplyNahidaToonRecipe(material.get(), owner); } else { if (sourceMaterial->HasRenderStateOverride()) { material->SetRenderState(sourceMaterial->GetRenderState()); } for (uint32_t tagIndex = 0; tagIndex < sourceMaterial->GetTagCount(); ++tagIndex) { material->SetTag(sourceMaterial->GetTagName(tagIndex), sourceMaterial->GetTagValue(tagIndex)); } for (uint32_t keywordIndex = 0; keywordIndex < sourceMaterial->GetKeywordCount(); ++keywordIndex) { material->EnableKeyword(sourceMaterial->GetKeyword(keywordIndex)); } for (const MaterialProperty& property : sourceMaterial->GetProperties()) { const XCEngine::Containers::String targetName = RemapMaterialPropertyName(*material, property.name); if (targetName.Empty()) { continue; } switch (property.type) { case MaterialPropertyType::Float: material->SetFloat(targetName, property.value.floatValue[0]); break; case MaterialPropertyType::Float2: material->SetFloat2( targetName, XCEngine::Math::Vector2(property.value.floatValue[0], property.value.floatValue[1])); break; case MaterialPropertyType::Float3: material->SetFloat3( targetName, XCEngine::Math::Vector3( property.value.floatValue[0], property.value.floatValue[1], property.value.floatValue[2])); break; case MaterialPropertyType::Float4: material->SetFloat4( targetName, XCEngine::Math::Vector4( property.value.floatValue[0], property.value.floatValue[1], property.value.floatValue[2], property.value.floatValue[3])); break; case MaterialPropertyType::Int: material->SetInt(targetName, property.value.intValue[0]); break; case MaterialPropertyType::Bool: material->SetBool(targetName, property.value.boolValue); break; case MaterialPropertyType::Texture: case MaterialPropertyType::Cubemap: case MaterialPropertyType::Int2: case MaterialPropertyType::Int3: case MaterialPropertyType::Int4: default: break; } } for (uint32_t bindingIndex = 0; bindingIndex < sourceMaterial->GetTextureBindingCount(); ++bindingIndex) { const XCEngine::Containers::String sourceName = sourceMaterial->GetTextureBindingName(bindingIndex); const XCEngine::Containers::String targetName = RemapMaterialPropertyName(*material, sourceName); if (targetName.Empty()) { continue; } const ResourceHandle texture = sourceMaterial->GetTextureBindingTexture(bindingIndex); if (texture.Get() != nullptr) { material->SetTexture(targetName, texture); continue; } const AssetRef textureRef = sourceMaterial->GetTextureBindingAssetRef(bindingIndex); const XCEngine::Containers::String texturePath = sourceMaterial->GetTextureBindingPath(bindingIndex); if (textureRef.IsValid() || !texturePath.Empty()) { material->SetTextureAssetRef(targetName, textureRef, texturePath); } } } m_overrideMaterials.push_back(std::move(material)); return m_overrideMaterials.back().get(); } void NahidaPreviewSceneTest::ApplyDiagnosticOverrides() { ASSERT_NE(m_scene, nullptr); const DiagnosticMode mode = GetDiagnosticMode(); std::printf("[NahidaDiag] diagnosticMode=%s\n", GetDiagnosticModeName(mode)); if (mode == DiagnosticMode::NoShadows) { const std::vector lights = m_scene->FindObjectsOfType(); for (LightComponent* light : lights) { if (light != nullptr) { light->SetCastsShadows(false); } } return; } const char* shaderPath = nullptr; switch (mode) { case DiagnosticMode::Toon: shaderPath = "Assets/Shaders/Toon.shader"; break; case DiagnosticMode::ForwardLit: shaderPath = "builtin://shaders/forward-lit"; break; case DiagnosticMode::Unlit: shaderPath = "builtin://shaders/unlit"; break; case DiagnosticMode::NoShadows: default: return; } const std::vector meshRenderers = m_scene->FindObjectsOfType(); for (MeshRendererComponent* meshRenderer : meshRenderers) { if (meshRenderer == nullptr || !IsNahidaCharacterRenderable(meshRenderer->GetGameObject())) { continue; } auto* meshFilter = meshRenderer->GetGameObject()->GetComponent(); Mesh* mesh = meshFilter != nullptr ? meshFilter->GetMesh() : nullptr; const size_t materialCount = std::max( meshRenderer->GetMaterialCount(), mesh != nullptr ? static_cast(mesh->GetMaterials().Size()) : 0u); for (size_t materialIndex = 0; materialIndex < materialCount; ++materialIndex) { const Material* sourceMaterial = nullptr; if (materialIndex < meshRenderer->GetMaterialCount()) { sourceMaterial = meshRenderer->GetMaterial(materialIndex); } if (sourceMaterial == nullptr && mesh != nullptr && materialIndex < mesh->GetMaterials().Size()) { sourceMaterial = mesh->GetMaterial(static_cast(materialIndex)); } if (sourceMaterial == nullptr || sourceMaterial->GetShader() == nullptr) { continue; } Material* overrideMaterial = CreateOverrideMaterial(sourceMaterial, shaderPath, meshRenderer->GetGameObject()); if (overrideMaterial != nullptr) { meshRenderer->SetMaterial(materialIndex, overrideMaterial); } } } } void NahidaPreviewSceneTest::DumpTargetDiagnostics() { ASSERT_NE(m_scene, nullptr); DumpSceneDiagnostics(*m_scene); const char* const targetObjects[] = { "Body_Mesh0", "Body_Mesh1", "Body_Mesh2", "Body_Mesh3", "Brow", "EyeStar", "Face", "Face_Eye" }; for (const char* objectName : targetObjects) { GameObject* gameObject = m_scene->Find(objectName); if (gameObject == nullptr) { std::printf("[NahidaDiag] object=%s missing\n", objectName); continue; } auto* meshFilter = gameObject->GetComponent(); auto* meshRenderer = gameObject->GetComponent(); std::printf( "[NahidaDiag] object=%s materialCount=%zu active=%d\n", objectName, meshRenderer != nullptr ? meshRenderer->GetMaterialCount() : 0u, gameObject->IsActiveInHierarchy() ? 1 : 0); DumpMeshDiagnostics(objectName, meshFilter != nullptr ? meshFilter->GetMesh() : nullptr); if (meshRenderer == nullptr) { continue; } for (size_t materialIndex = 0; materialIndex < meshRenderer->GetMaterialCount(); ++materialIndex) { std::string label = std::string(objectName) + "#mat" + std::to_string(materialIndex); DumpMaterialDiagnostics(label.c_str(), meshRenderer->GetMaterial(materialIndex)); } } } RHIResourceView* NahidaPreviewSceneTest::GetCurrentBackBufferView() { const int backBufferIndex = GetCurrentBackBufferIndex(); if (backBufferIndex < 0) { return nullptr; } if (static_cast(backBufferIndex) >= m_backBufferViews.size()) { m_backBufferViews.resize(static_cast(backBufferIndex) + 1, nullptr); } if (m_backBufferViews[backBufferIndex] == nullptr) { ResourceViewDesc viewDesc = {}; viewDesc.format = static_cast(Format::R8G8B8A8_UNorm); viewDesc.dimension = ResourceViewDimension::Texture2D; viewDesc.mipLevel = 0; m_backBufferViews[backBufferIndex] = GetDevice()->CreateRenderTargetView(GetCurrentBackBuffer(), viewDesc); } return m_backBufferViews[backBufferIndex]; } void NahidaPreviewSceneTest::RenderFrame() { ASSERT_NE(m_scene, nullptr); ASSERT_NE(m_sceneRenderer, nullptr); TouchSceneMaterialsAndTextures(); ResourceManager::Get().UpdateAsyncLoads(); RHICommandList* commandList = GetCommandList(); ASSERT_NE(commandList, nullptr); commandList->Reset(); RenderSurface surface(kFrameWidth, kFrameHeight); surface.SetColorAttachment(GetCurrentBackBufferView()); surface.SetDepthAttachment(m_depthView); RenderContext renderContext = {}; renderContext.device = GetDevice(); renderContext.commandList = commandList; renderContext.commandQueue = GetCommandQueue(); renderContext.backendType = GetBackendType(); ASSERT_TRUE(m_sceneRenderer->Render(*m_scene, nullptr, renderContext, surface)); commandList->Close(); void* commandLists[] = { commandList }; GetCommandQueue()->ExecuteCommandLists(1, commandLists); } TEST_P(NahidaPreviewSceneTest, RenderNahidaPreviewScene) { RHICommandQueue* commandQueue = GetCommandQueue(); RHISwapChain* swapChain = GetSwapChain(); ASSERT_NE(commandQueue, nullptr); ASSERT_NE(swapChain, nullptr); const DiagnosticMode diagnosticMode = GetDiagnosticMode(); const char* goldenFileName = GetGoldenFileName(diagnosticMode); for (int frameIndex = 0; frameIndex <= kWarmupFrames; ++frameIndex) { if (frameIndex > 0) { commandQueue->WaitForPreviousFrame(); } BeginRender(); RenderFrame(); if (frameIndex >= kWarmupFrames) { commandQueue->WaitForIdle(); ASSERT_TRUE(TakeScreenshot(kD3D12Screenshot)); const PpmImage image = LoadPpmImage(kD3D12Screenshot); ASSERT_EQ(image.width, kFrameWidth); ASSERT_EQ(image.height, kFrameHeight); const std::filesystem::path gtPath = ResolveRuntimePath(goldenFileName); if (!std::filesystem::exists(gtPath)) { GTEST_SKIP() << goldenFileName << " missing, screenshot captured for manual review: " << kD3D12Screenshot; } ASSERT_TRUE(CompareWithGoldenTemplate(kD3D12Screenshot, goldenFileName, 10.0f)); break; } swapChain->Present(0, 0); } } } // namespace INSTANTIATE_TEST_SUITE_P(D3D12, NahidaPreviewSceneTest, ::testing::Values(RHIType::D3D12)); GTEST_API_ int main(int argc, char** argv) { return RunRenderingIntegrationTestMain(argc, argv); }