2026-04-11 18:45:49 +08:00
|
|
|
#define NOMINMAX
|
|
|
|
|
#include <windows.h>
|
|
|
|
|
|
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
|
|
|
|
|
|
#include "../RenderingIntegrationImageAssert.h"
|
|
|
|
|
#include "../RenderingIntegrationMain.h"
|
|
|
|
|
|
|
|
|
|
#include <XCEngine/Components/MeshFilterComponent.h>
|
|
|
|
|
#include <XCEngine/Components/MeshRendererComponent.h>
|
|
|
|
|
#include <XCEngine/Components/LightComponent.h>
|
|
|
|
|
#include <XCEngine/Core/Asset/ResourceManager.h>
|
|
|
|
|
#include <XCEngine/Rendering/Extraction/RenderSceneExtractor.h>
|
|
|
|
|
#include <XCEngine/Rendering/Execution/SceneRenderer.h>
|
|
|
|
|
#include <XCEngine/Rendering/RenderContext.h>
|
|
|
|
|
#include <XCEngine/Rendering/RenderSurface.h>
|
|
|
|
|
#include <XCEngine/Resources/Material/Material.h>
|
|
|
|
|
#include <XCEngine/Resources/Mesh/Mesh.h>
|
|
|
|
|
#include <XCEngine/RHI/RHITexture.h>
|
|
|
|
|
#include <XCEngine/Scene/Scene.h>
|
|
|
|
|
|
|
|
|
|
#include "../../../RHI/integration/fixtures/RHIIntegrationFixture.h"
|
|
|
|
|
|
|
|
|
|
#include <chrono>
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <cstdio>
|
|
|
|
|
#include <cstdlib>
|
|
|
|
|
#include <filesystem>
|
|
|
|
|
#include <memory>
|
|
|
|
|
#include <limits>
|
|
|
|
|
#include <sstream>
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <thread>
|
|
|
|
|
#include <unordered_map>
|
|
|
|
|
#include <unordered_set>
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
|
|
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 {
|
2026-04-13 21:09:40 +08:00
|
|
|
Toon,
|
2026-04-11 18:45:49 +08:00
|
|
|
NoShadows,
|
|
|
|
|
ForwardLit,
|
|
|
|
|
Unlit
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
DiagnosticMode GetDiagnosticMode() {
|
|
|
|
|
const char* value = std::getenv("XC_NAHIDA_DIAG_MODE");
|
|
|
|
|
if (value == nullptr) {
|
2026-04-13 21:09:40 +08:00
|
|
|
return DiagnosticMode::Toon;
|
2026-04-11 18:45:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const std::string mode(value);
|
2026-04-13 21:09:40 +08:00
|
|
|
if (mode == "toon" || mode == "original") {
|
|
|
|
|
return DiagnosticMode::Toon;
|
|
|
|
|
}
|
2026-04-11 18:45:49 +08:00
|
|
|
if (mode == "no_shadows") {
|
|
|
|
|
return DiagnosticMode::NoShadows;
|
|
|
|
|
}
|
|
|
|
|
if (mode == "forward_lit") {
|
|
|
|
|
return DiagnosticMode::ForwardLit;
|
|
|
|
|
}
|
|
|
|
|
if (mode == "unlit") {
|
|
|
|
|
return DiagnosticMode::Unlit;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 21:09:40 +08:00
|
|
|
return DiagnosticMode::Toon;
|
2026-04-11 18:45:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const char* GetDiagnosticModeName(DiagnosticMode mode) {
|
|
|
|
|
switch (mode) {
|
2026-04-13 21:09:40 +08:00
|
|
|
case DiagnosticMode::Toon: return "toon";
|
2026-04-11 18:45:49 +08:00
|
|
|
case DiagnosticMode::NoShadows: return "no_shadows";
|
|
|
|
|
case DiagnosticMode::ForwardLit: return "forward_lit";
|
|
|
|
|
case DiagnosticMode::Unlit: return "unlit";
|
|
|
|
|
default: return "unknown";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 22:14:02 +08:00
|
|
|
const char* GetGoldenFileName(DiagnosticMode mode) {
|
|
|
|
|
switch (mode) {
|
2026-04-13 21:09:40 +08:00
|
|
|
case DiagnosticMode::Toon: return "GT.ppm";
|
2026-04-11 22:14:02 +08:00
|
|
|
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";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 21:09:40 +08:00
|
|
|
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> texture = ResourceManager::Get().Load<Texture>(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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 18:45:49 +08:00
|
|
|
std::unordered_set<std::string> GetIsolationObjectNames() {
|
|
|
|
|
std::unordered_set<std::string> 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<const StaticMeshVertex*>(mesh->GetVertexData());
|
|
|
|
|
XCEngine::Math::Vector2 uvMin(
|
|
|
|
|
std::numeric_limits<float>::max(),
|
|
|
|
|
std::numeric_limits<float>::max());
|
|
|
|
|
XCEngine::Math::Vector2 uvMax(
|
|
|
|
|
std::numeric_limits<float>::lowest(),
|
|
|
|
|
std::numeric_limits<float>::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(
|
2026-04-11 20:16:49 +08:00
|
|
|
"[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",
|
2026-04-11 18:45:49 +08:00
|
|
|
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,
|
2026-04-11 20:16:49 +08:00
|
|
|
firstVertex.uv0.y,
|
|
|
|
|
firstVertex.uv1.x,
|
|
|
|
|
firstVertex.uv1.y,
|
|
|
|
|
firstVertex.color.x,
|
|
|
|
|
firstVertex.color.y,
|
|
|
|
|
firstVertex.color.z,
|
|
|
|
|
firstVertex.color.w);
|
2026-04-11 18:45:49 +08:00
|
|
|
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() : "<null>",
|
|
|
|
|
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<Texture> 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() : "<null>");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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() : "<null>";
|
|
|
|
|
const char* meshPath = item.mesh != nullptr ? item.mesh->GetPath().CStr() : "<null>";
|
|
|
|
|
const char* materialPath = item.material != nullptr ? item.material->GetPath().CStr() : "<null>";
|
|
|
|
|
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();
|
2026-04-13 21:09:40 +08:00
|
|
|
Material* CreateOverrideMaterial(const Material* sourceMaterial, const char* shaderPath, const GameObject* owner);
|
2026-04-11 18:45:49 +08:00
|
|
|
|
|
|
|
|
HMODULE m_assimpModule = nullptr;
|
|
|
|
|
std::unique_ptr<Scene> m_scene;
|
|
|
|
|
std::unique_ptr<SceneRenderer> m_sceneRenderer;
|
|
|
|
|
std::vector<std::unique_ptr<Material>> m_overrideMaterials;
|
|
|
|
|
std::vector<RHIResourceView*> 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<SceneRenderer>();
|
|
|
|
|
m_scene = std::make_unique<Scene>("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();
|
2026-04-11 20:16:49 +08:00
|
|
|
|
|
|
|
|
auto* bodyMeshObject = m_scene->Find("Body_Mesh0");
|
|
|
|
|
ASSERT_NE(bodyMeshObject, nullptr);
|
|
|
|
|
auto* bodyMeshFilter = bodyMeshObject->GetComponent<MeshFilterComponent>();
|
|
|
|
|
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));
|
|
|
|
|
|
2026-04-11 18:45:49 +08:00
|
|
|
ApplyIsolationFilter();
|
|
|
|
|
ApplyDiagnosticOverrides();
|
|
|
|
|
DumpTargetDiagnostics();
|
|
|
|
|
|
|
|
|
|
TextureDesc depthDesc = {};
|
|
|
|
|
depthDesc.width = kFrameWidth;
|
|
|
|
|
depthDesc.height = kFrameHeight;
|
|
|
|
|
depthDesc.depth = 1;
|
|
|
|
|
depthDesc.mipLevels = 1;
|
|
|
|
|
depthDesc.arraySize = 1;
|
|
|
|
|
depthDesc.format = static_cast<uint32_t>(Format::D24_UNorm_S8_UInt);
|
|
|
|
|
depthDesc.textureType = static_cast<uint32_t>(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<uint32_t>(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<MeshFilterComponent*> meshFilters = m_scene->FindObjectsOfType<MeshFilterComponent>();
|
|
|
|
|
for (MeshFilterComponent* meshFilter : meshFilters) {
|
|
|
|
|
if (meshFilter == nullptr) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 21:09:40 +08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-11 18:45:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const std::vector<MeshRendererComponent*> meshRenderers = m_scene->FindObjectsOfType<MeshRendererComponent>();
|
|
|
|
|
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<std::string> 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<MeshRendererComponent*> meshRenderers = m_scene->FindObjectsOfType<MeshRendererComponent>();
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 21:09:40 +08:00
|
|
|
Material* NahidaPreviewSceneTest::CreateOverrideMaterial(
|
|
|
|
|
const Material* sourceMaterial,
|
|
|
|
|
const char* shaderPath,
|
|
|
|
|
const GameObject* owner) {
|
2026-04-11 18:45:49 +08:00
|
|
|
if (sourceMaterial == nullptr || shaderPath == nullptr) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 21:09:40 +08:00
|
|
|
const ResourceHandle<Shader> shader = ResourceManager::Get().Load<Shader>(shaderPath);
|
|
|
|
|
if (shader.Get() == nullptr) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 18:45:49 +08:00
|
|
|
auto material = std::make_unique<Material>();
|
|
|
|
|
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);
|
2026-04-13 21:09:40 +08:00
|
|
|
material->SetShader(shader);
|
|
|
|
|
material->SetRenderQueue(sourceMaterial->GetRenderQueue());
|
|
|
|
|
if (std::string(shaderPath) == kToonShaderPath) {
|
|
|
|
|
ApplyNahidaToonRecipe(material.get(), owner);
|
|
|
|
|
} else {
|
|
|
|
|
if (sourceMaterial->HasRenderStateOverride()) {
|
|
|
|
|
material->SetRenderState(sourceMaterial->GetRenderState());
|
|
|
|
|
}
|
2026-04-11 18:45:49 +08:00
|
|
|
|
2026-04-13 21:09:40 +08:00
|
|
|
for (uint32_t tagIndex = 0; tagIndex < sourceMaterial->GetTagCount(); ++tagIndex) {
|
|
|
|
|
material->SetTag(sourceMaterial->GetTagName(tagIndex), sourceMaterial->GetTagValue(tagIndex));
|
|
|
|
|
}
|
2026-04-11 18:45:49 +08:00
|
|
|
|
2026-04-13 21:09:40 +08:00
|
|
|
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;
|
|
|
|
|
}
|
2026-04-11 18:45:49 +08:00
|
|
|
|
2026-04-13 21:09:40 +08:00
|
|
|
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> 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-11 18:45:49 +08:00
|
|
|
|
|
|
|
|
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<LightComponent*> lights = m_scene->FindObjectsOfType<LightComponent>();
|
|
|
|
|
for (LightComponent* light : lights) {
|
|
|
|
|
if (light != nullptr) {
|
|
|
|
|
light->SetCastsShadows(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 21:09:40 +08:00
|
|
|
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;
|
|
|
|
|
}
|
2026-04-11 18:45:49 +08:00
|
|
|
|
|
|
|
|
const std::vector<MeshRendererComponent*> meshRenderers = m_scene->FindObjectsOfType<MeshRendererComponent>();
|
|
|
|
|
for (MeshRendererComponent* meshRenderer : meshRenderers) {
|
2026-04-13 21:09:40 +08:00
|
|
|
if (meshRenderer == nullptr || !IsNahidaCharacterRenderable(meshRenderer->GetGameObject())) {
|
2026-04-11 18:45:49 +08:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 21:09:40 +08:00
|
|
|
auto* meshFilter = meshRenderer->GetGameObject()->GetComponent<MeshFilterComponent>();
|
|
|
|
|
Mesh* mesh = meshFilter != nullptr ? meshFilter->GetMesh() : nullptr;
|
|
|
|
|
const size_t materialCount = std::max(
|
|
|
|
|
meshRenderer->GetMaterialCount(),
|
|
|
|
|
mesh != nullptr ? static_cast<size_t>(mesh->GetMaterials().Size()) : 0u);
|
|
|
|
|
for (size_t materialIndex = 0; materialIndex < materialCount; ++materialIndex) {
|
|
|
|
|
const Material* sourceMaterial = nullptr;
|
|
|
|
|
if (materialIndex < meshRenderer->GetMaterialCount()) {
|
|
|
|
|
sourceMaterial = meshRenderer->GetMaterial(materialIndex);
|
2026-04-11 18:45:49 +08:00
|
|
|
}
|
2026-04-13 21:09:40 +08:00
|
|
|
if (sourceMaterial == nullptr && mesh != nullptr && materialIndex < mesh->GetMaterials().Size()) {
|
|
|
|
|
sourceMaterial = mesh->GetMaterial(static_cast<uint32_t>(materialIndex));
|
2026-04-11 18:45:49 +08:00
|
|
|
}
|
2026-04-13 21:09:40 +08:00
|
|
|
if (sourceMaterial == nullptr || sourceMaterial->GetShader() == nullptr) {
|
|
|
|
|
continue;
|
2026-04-11 18:45:49 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-13 21:09:40 +08:00
|
|
|
Material* overrideMaterial = CreateOverrideMaterial(sourceMaterial, shaderPath, meshRenderer->GetGameObject());
|
2026-04-11 18:45:49 +08:00
|
|
|
if (overrideMaterial != nullptr) {
|
|
|
|
|
meshRenderer->SetMaterial(materialIndex, overrideMaterial);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void NahidaPreviewSceneTest::DumpTargetDiagnostics() {
|
|
|
|
|
ASSERT_NE(m_scene, nullptr);
|
|
|
|
|
|
|
|
|
|
DumpSceneDiagnostics(*m_scene);
|
|
|
|
|
|
|
|
|
|
const char* const targetObjects[] = {
|
|
|
|
|
"Body_Mesh0",
|
2026-04-13 21:09:40 +08:00
|
|
|
"Body_Mesh1",
|
|
|
|
|
"Body_Mesh2",
|
|
|
|
|
"Body_Mesh3",
|
2026-04-11 18:45:49 +08:00
|
|
|
"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<MeshFilterComponent>();
|
|
|
|
|
auto* meshRenderer = gameObject->GetComponent<MeshRendererComponent>();
|
|
|
|
|
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<size_t>(backBufferIndex) >= m_backBufferViews.size()) {
|
|
|
|
|
m_backBufferViews.resize(static_cast<size_t>(backBufferIndex) + 1, nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_backBufferViews[backBufferIndex] == nullptr) {
|
|
|
|
|
ResourceViewDesc viewDesc = {};
|
|
|
|
|
viewDesc.format = static_cast<uint32_t>(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);
|
2026-04-11 22:14:02 +08:00
|
|
|
const DiagnosticMode diagnosticMode = GetDiagnosticMode();
|
|
|
|
|
const char* goldenFileName = GetGoldenFileName(diagnosticMode);
|
2026-04-11 18:45:49 +08:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
2026-04-11 22:14:02 +08:00
|
|
|
const std::filesystem::path gtPath = ResolveRuntimePath(goldenFileName);
|
2026-04-11 18:45:49 +08:00
|
|
|
if (!std::filesystem::exists(gtPath)) {
|
2026-04-11 22:14:02 +08:00
|
|
|
GTEST_SKIP() << goldenFileName << " missing, screenshot captured for manual review: " << kD3D12Screenshot;
|
2026-04-11 18:45:49 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-11 22:14:02 +08:00
|
|
|
ASSERT_TRUE(CompareWithGoldenTemplate(kD3D12Screenshot, goldenFileName, 10.0f));
|
2026-04-11 18:45:49 +08:00
|
|
|
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);
|
|
|
|
|
}
|