2026-03-26 20:43:17 +08:00
|
|
|
#include <gtest/gtest.h>
|
|
|
|
|
|
|
|
|
|
#include <XCEngine/Components/CameraComponent.h>
|
2026-04-10 21:49:53 +08:00
|
|
|
#include <XCEngine/Components/GaussianSplatRendererComponent.h>
|
2026-03-26 20:43:17 +08:00
|
|
|
#include <XCEngine/Components/GameObject.h>
|
2026-04-01 00:41:56 +08:00
|
|
|
#include <XCEngine/Components/LightComponent.h>
|
2026-03-26 20:43:17 +08:00
|
|
|
#include <XCEngine/Components/MeshFilterComponent.h>
|
|
|
|
|
#include <XCEngine/Components/MeshRendererComponent.h>
|
2026-04-08 20:12:14 +08:00
|
|
|
#include <XCEngine/Components/VolumeRendererComponent.h>
|
2026-03-26 20:43:17 +08:00
|
|
|
#include <XCEngine/Core/Asset/IResource.h>
|
2026-04-01 00:41:56 +08:00
|
|
|
#include <XCEngine/Core/Math/Color.h>
|
2026-04-08 20:12:14 +08:00
|
|
|
#include <XCEngine/Core/Math/Bounds.h>
|
2026-04-01 00:41:56 +08:00
|
|
|
#include <XCEngine/Core/Math/Quaternion.h>
|
2026-03-26 20:43:17 +08:00
|
|
|
#include <XCEngine/Core/Math/Vector3.h>
|
2026-04-05 22:02:52 +08:00
|
|
|
#include <XCEngine/Rendering/Builtin/BuiltinPassContract.h>
|
2026-04-05 22:14:17 +08:00
|
|
|
#include <XCEngine/Rendering/Materials/RenderMaterialResolve.h>
|
|
|
|
|
#include <XCEngine/Rendering/Materials/RenderMaterialStateUtils.h>
|
2026-04-05 21:53:35 +08:00
|
|
|
#include <XCEngine/Rendering/Extraction/RenderSceneExtractor.h>
|
2026-04-08 19:18:07 +08:00
|
|
|
#include <XCEngine/RHI/RHIBuffer.h>
|
2026-04-10 21:49:53 +08:00
|
|
|
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
|
2026-03-27 11:56:23 +08:00
|
|
|
#include <XCEngine/Resources/Material/Material.h>
|
2026-03-26 20:43:17 +08:00
|
|
|
#include <XCEngine/Resources/Mesh/Mesh.h>
|
2026-04-02 16:26:20 +08:00
|
|
|
#include <XCEngine/Resources/Shader/Shader.h>
|
2026-04-02 17:13:53 +08:00
|
|
|
#include <XCEngine/Resources/Texture/Texture.h>
|
2026-04-08 20:12:14 +08:00
|
|
|
#include <XCEngine/Resources/Volume/VolumeField.h>
|
2026-03-27 11:56:23 +08:00
|
|
|
#include <XCEngine/Scene/Scene.h>
|
2026-03-26 20:43:17 +08:00
|
|
|
|
2026-04-10 21:49:53 +08:00
|
|
|
#include <cstring>
|
2026-04-08 19:18:07 +08:00
|
|
|
#include <string>
|
|
|
|
|
|
2026-03-26 20:43:17 +08:00
|
|
|
using namespace XCEngine::Components;
|
2026-03-27 11:56:23 +08:00
|
|
|
using namespace XCEngine::Core;
|
2026-03-26 20:43:17 +08:00
|
|
|
using namespace XCEngine::Math;
|
|
|
|
|
using namespace XCEngine::Rendering;
|
|
|
|
|
using namespace XCEngine::Resources;
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
2026-04-08 19:18:07 +08:00
|
|
|
class MockRenderBuffer final : public XCEngine::RHI::RHIBuffer {
|
|
|
|
|
public:
|
|
|
|
|
explicit MockRenderBuffer(uint64_t size = 512u, uint32_t stride = 16u)
|
|
|
|
|
: m_size(size)
|
|
|
|
|
, m_stride(stride) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void* Map() override { return nullptr; }
|
|
|
|
|
void Unmap() override {}
|
|
|
|
|
void SetData(const void*, size_t, size_t) override {}
|
|
|
|
|
uint64_t GetSize() const override { return m_size; }
|
|
|
|
|
XCEngine::RHI::BufferType GetBufferType() const override { return XCEngine::RHI::BufferType::Storage; }
|
|
|
|
|
void SetBufferType(XCEngine::RHI::BufferType) override {}
|
|
|
|
|
uint32_t GetStride() const override { return m_stride; }
|
|
|
|
|
void SetStride(uint32_t stride) override { m_stride = stride; }
|
|
|
|
|
void* GetNativeHandle() override { return nullptr; }
|
|
|
|
|
XCEngine::RHI::ResourceStates GetState() const override { return XCEngine::RHI::ResourceStates::Common; }
|
|
|
|
|
void SetState(XCEngine::RHI::ResourceStates) override {}
|
|
|
|
|
const std::string& GetName() const override { return m_name; }
|
|
|
|
|
void SetName(const std::string& name) override { m_name = name; }
|
|
|
|
|
void Shutdown() override {}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
uint64_t m_size = 0;
|
|
|
|
|
uint32_t m_stride = 0;
|
|
|
|
|
std::string m_name;
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-26 20:43:17 +08:00
|
|
|
Mesh* CreateTestMesh(const char* path) {
|
|
|
|
|
auto* mesh = new Mesh();
|
|
|
|
|
IResource::ConstructParams params = {};
|
|
|
|
|
params.name = "TestMesh";
|
|
|
|
|
params.path = path;
|
|
|
|
|
params.guid = ResourceGUID::Generate(path);
|
|
|
|
|
mesh->Initialize(params);
|
|
|
|
|
return mesh;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 11:56:23 +08:00
|
|
|
Mesh* CreateSectionedTestMesh(const char* path, std::initializer_list<uint32_t> materialIds) {
|
|
|
|
|
Mesh* mesh = CreateTestMesh(path);
|
|
|
|
|
uint32_t startIndex = 0;
|
|
|
|
|
for (uint32_t materialId : materialIds) {
|
|
|
|
|
MeshSection section = {};
|
|
|
|
|
section.baseVertex = 0;
|
|
|
|
|
section.vertexCount = 3;
|
|
|
|
|
section.startIndex = startIndex;
|
|
|
|
|
section.indexCount = 3;
|
|
|
|
|
section.materialID = materialId;
|
|
|
|
|
mesh->AddSection(section);
|
|
|
|
|
startIndex += 3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return mesh;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 04:27:21 +08:00
|
|
|
Material* CreateTestMaterial(const char* path, int32_t renderQueue) {
|
2026-03-27 11:56:23 +08:00
|
|
|
auto* material = new Material();
|
|
|
|
|
IResource::ConstructParams params = {};
|
|
|
|
|
params.name = "TestMaterial";
|
|
|
|
|
params.path = path;
|
|
|
|
|
params.guid = ResourceGUID::Generate(path);
|
|
|
|
|
material->Initialize(params);
|
|
|
|
|
material->SetRenderQueue(renderQueue);
|
|
|
|
|
return material;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 20:12:14 +08:00
|
|
|
VolumeField* CreateTestVolumeField(const char* path) {
|
|
|
|
|
auto* volumeField = new VolumeField();
|
|
|
|
|
IResource::ConstructParams params = {};
|
|
|
|
|
params.name = "TestVolume";
|
|
|
|
|
params.path = path;
|
|
|
|
|
params.guid = ResourceGUID::Generate(path);
|
|
|
|
|
volumeField->Initialize(params);
|
|
|
|
|
|
|
|
|
|
const unsigned char payload[8] = { 1, 2, 3, 4, 5, 6, 7, 8 };
|
|
|
|
|
EXPECT_TRUE(volumeField->Create(
|
|
|
|
|
VolumeStorageKind::NanoVDB,
|
|
|
|
|
payload,
|
|
|
|
|
sizeof(payload),
|
|
|
|
|
Bounds(Vector3::Zero(), Vector3(2.0f, 2.0f, 2.0f)),
|
|
|
|
|
Vector3(0.25f, 0.25f, 0.25f)));
|
|
|
|
|
return volumeField;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 21:49:53 +08:00
|
|
|
GaussianSplat* CreateTestGaussianSplat(const char* path) {
|
|
|
|
|
auto* gaussianSplat = new GaussianSplat();
|
|
|
|
|
IResource::ConstructParams params = {};
|
|
|
|
|
params.name = "TestGaussianSplat";
|
|
|
|
|
params.path = path;
|
|
|
|
|
params.guid = ResourceGUID::Generate(path);
|
|
|
|
|
gaussianSplat->Initialize(params);
|
|
|
|
|
|
|
|
|
|
GaussianSplatMetadata metadata = {};
|
|
|
|
|
metadata.splatCount = 1u;
|
|
|
|
|
|
|
|
|
|
XCEngine::Containers::Array<GaussianSplatSection> sections;
|
|
|
|
|
sections.Resize(1);
|
|
|
|
|
sections[0].type = GaussianSplatSectionType::Positions;
|
|
|
|
|
sections[0].format = GaussianSplatSectionFormat::VectorFloat32;
|
|
|
|
|
sections[0].dataOffset = 0u;
|
|
|
|
|
sections[0].dataSize = sizeof(GaussianSplatPositionRecord);
|
|
|
|
|
sections[0].elementCount = 1u;
|
|
|
|
|
sections[0].elementStride = sizeof(GaussianSplatPositionRecord);
|
|
|
|
|
|
|
|
|
|
XCEngine::Containers::Array<XCEngine::Core::uint8> payload;
|
|
|
|
|
payload.Resize(sizeof(GaussianSplatPositionRecord));
|
|
|
|
|
const GaussianSplatPositionRecord positionRecord = { Vector3(0.0f, 0.0f, 0.0f) };
|
|
|
|
|
std::memcpy(payload.Data(), &positionRecord, sizeof(positionRecord));
|
|
|
|
|
|
|
|
|
|
EXPECT_TRUE(gaussianSplat->CreateOwned(metadata, std::move(sections), std::move(payload)));
|
|
|
|
|
return gaussianSplat;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 13:13:42 +08:00
|
|
|
Texture* CreateTestTexture(const char* path, TextureType type) {
|
|
|
|
|
auto* texture = new Texture();
|
|
|
|
|
IResource::ConstructParams params = {};
|
|
|
|
|
params.name = path;
|
|
|
|
|
params.path = path;
|
|
|
|
|
params.guid = ResourceGUID::Generate(path);
|
|
|
|
|
texture->Initialize(params);
|
|
|
|
|
|
|
|
|
|
const uint32 arraySize =
|
|
|
|
|
(type == TextureType::TextureCube || type == TextureType::TextureCubeArray) ? 6u : 1u;
|
|
|
|
|
const unsigned char pixels[6 * 4] = {
|
|
|
|
|
255, 255, 255, 255,
|
|
|
|
|
255, 255, 255, 255,
|
|
|
|
|
255, 255, 255, 255,
|
|
|
|
|
255, 255, 255, 255,
|
|
|
|
|
255, 255, 255, 255,
|
|
|
|
|
255, 255, 255, 255
|
|
|
|
|
};
|
|
|
|
|
EXPECT_TRUE(texture->Create(
|
|
|
|
|
1u,
|
|
|
|
|
1u,
|
|
|
|
|
1u,
|
|
|
|
|
1u,
|
|
|
|
|
type,
|
|
|
|
|
TextureFormat::RGBA8_UNORM,
|
|
|
|
|
pixels,
|
|
|
|
|
static_cast<size_t>(arraySize) * 4u,
|
|
|
|
|
arraySize));
|
|
|
|
|
return texture;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 20:43:17 +08:00
|
|
|
TEST(RenderSceneExtractor_Test, SelectsHighestDepthPrimaryCameraAndVisibleObjects) {
|
|
|
|
|
Scene scene("RenderScene");
|
|
|
|
|
|
|
|
|
|
GameObject* cameraObjectA = scene.CreateGameObject("CameraA");
|
|
|
|
|
auto* cameraA = cameraObjectA->AddComponent<CameraComponent>();
|
|
|
|
|
cameraA->SetPrimary(true);
|
|
|
|
|
cameraA->SetDepth(0.0f);
|
|
|
|
|
|
|
|
|
|
GameObject* cameraObjectB = scene.CreateGameObject("CameraB");
|
|
|
|
|
auto* cameraB = cameraObjectB->AddComponent<CameraComponent>();
|
|
|
|
|
cameraB->SetPrimary(true);
|
|
|
|
|
cameraB->SetDepth(5.0f);
|
|
|
|
|
cameraObjectB->GetTransform()->SetLocalPosition(Vector3(2.0f, 3.0f, 4.0f));
|
|
|
|
|
|
|
|
|
|
GameObject* visibleObject = scene.CreateGameObject("VisibleQuad");
|
|
|
|
|
visibleObject->GetTransform()->SetLocalPosition(Vector3(1.0f, 2.0f, 3.0f));
|
|
|
|
|
auto* meshFilter = visibleObject->AddComponent<MeshFilterComponent>();
|
|
|
|
|
visibleObject->AddComponent<MeshRendererComponent>();
|
|
|
|
|
Mesh* visibleMesh = CreateTestMesh("Meshes/visible.mesh");
|
|
|
|
|
meshFilter->SetMesh(visibleMesh);
|
|
|
|
|
|
|
|
|
|
GameObject* hiddenObject = scene.CreateGameObject("HiddenQuad");
|
|
|
|
|
hiddenObject->SetActive(false);
|
|
|
|
|
auto* hiddenMeshFilter = hiddenObject->AddComponent<MeshFilterComponent>();
|
|
|
|
|
hiddenObject->AddComponent<MeshRendererComponent>();
|
|
|
|
|
Mesh* hiddenMesh = CreateTestMesh("Meshes/hidden.mesh");
|
|
|
|
|
hiddenMeshFilter->SetMesh(hiddenMesh);
|
|
|
|
|
|
|
|
|
|
RenderSceneExtractor extractor;
|
|
|
|
|
const RenderSceneData sceneData = extractor.Extract(scene, nullptr, 1280, 720);
|
|
|
|
|
|
|
|
|
|
ASSERT_TRUE(sceneData.HasCamera());
|
|
|
|
|
EXPECT_EQ(sceneData.camera, cameraB);
|
|
|
|
|
EXPECT_EQ(sceneData.cameraData.viewportWidth, 1280u);
|
|
|
|
|
EXPECT_EQ(sceneData.cameraData.viewportHeight, 720u);
|
|
|
|
|
EXPECT_EQ(sceneData.cameraData.worldPosition, Vector3(2.0f, 3.0f, 4.0f));
|
|
|
|
|
|
2026-03-27 11:56:23 +08:00
|
|
|
ASSERT_EQ(sceneData.visibleItems.size(), 1u);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleItems[0].gameObject, visibleObject);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleItems[0].mesh, visibleMesh);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleItems[0].localToWorld.GetTranslation(), Vector3(1.0f, 2.0f, 3.0f));
|
|
|
|
|
EXPECT_FALSE(sceneData.visibleItems[0].hasSection);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleItems[0].renderQueue, static_cast<int32_t>(MaterialRenderQueue::Geometry));
|
2026-03-26 20:43:17 +08:00
|
|
|
|
|
|
|
|
meshFilter->ClearMesh();
|
|
|
|
|
hiddenMeshFilter->ClearMesh();
|
|
|
|
|
delete visibleMesh;
|
|
|
|
|
delete hiddenMesh;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(RenderSceneExtractor_Test, OverrideCameraTakesPriority) {
|
|
|
|
|
Scene scene("OverrideScene");
|
|
|
|
|
|
|
|
|
|
GameObject* primaryObject = scene.CreateGameObject("PrimaryCamera");
|
|
|
|
|
auto* primaryCamera = primaryObject->AddComponent<CameraComponent>();
|
|
|
|
|
primaryCamera->SetPrimary(true);
|
|
|
|
|
primaryCamera->SetDepth(10.0f);
|
|
|
|
|
|
|
|
|
|
GameObject* overrideObject = scene.CreateGameObject("OverrideCamera");
|
|
|
|
|
auto* overrideCamera = overrideObject->AddComponent<CameraComponent>();
|
|
|
|
|
overrideCamera->SetPrimary(false);
|
|
|
|
|
overrideCamera->SetDepth(-1.0f);
|
|
|
|
|
|
|
|
|
|
RenderSceneExtractor extractor;
|
|
|
|
|
const RenderSceneData sceneData = extractor.Extract(scene, overrideCamera, 640, 480);
|
|
|
|
|
|
|
|
|
|
ASSERT_TRUE(sceneData.HasCamera());
|
|
|
|
|
EXPECT_EQ(sceneData.camera, overrideCamera);
|
|
|
|
|
EXPECT_NE(sceneData.camera, primaryCamera);
|
|
|
|
|
EXPECT_EQ(sceneData.cameraData.viewportWidth, 640u);
|
|
|
|
|
EXPECT_EQ(sceneData.cameraData.viewportHeight, 480u);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 00:41:56 +08:00
|
|
|
TEST(RenderSceneExtractor_Test, ExtractsBrightestDirectionalLightAsMainLight) {
|
|
|
|
|
Scene scene("LightingScene");
|
|
|
|
|
|
|
|
|
|
GameObject* cameraObject = scene.CreateGameObject("Camera");
|
|
|
|
|
auto* camera = cameraObject->AddComponent<CameraComponent>();
|
|
|
|
|
camera->SetPrimary(true);
|
|
|
|
|
|
|
|
|
|
GameObject* fillLightObject = scene.CreateGameObject("FillLight");
|
|
|
|
|
auto* fillLight = fillLightObject->AddComponent<LightComponent>();
|
|
|
|
|
fillLight->SetLightType(LightType::Directional);
|
|
|
|
|
fillLight->SetColor(Color(0.2f, 0.4f, 0.8f, 1.0f));
|
|
|
|
|
fillLight->SetIntensity(0.5f);
|
|
|
|
|
|
|
|
|
|
GameObject* pointLightObject = scene.CreateGameObject("PointLight");
|
|
|
|
|
auto* pointLight = pointLightObject->AddComponent<LightComponent>();
|
|
|
|
|
pointLight->SetLightType(LightType::Point);
|
|
|
|
|
pointLight->SetIntensity(10.0f);
|
|
|
|
|
|
|
|
|
|
GameObject* mainLightObject = scene.CreateGameObject("MainLight");
|
|
|
|
|
auto* mainLight = mainLightObject->AddComponent<LightComponent>();
|
|
|
|
|
mainLight->SetLightType(LightType::Directional);
|
|
|
|
|
mainLight->SetColor(Color(1.0f, 0.8f, 0.6f, 1.0f));
|
|
|
|
|
mainLight->SetIntensity(2.5f);
|
2026-04-04 20:03:23 +08:00
|
|
|
mainLight->SetCastsShadows(true);
|
2026-04-01 00:41:56 +08:00
|
|
|
mainLightObject->GetTransform()->SetLocalRotation(
|
|
|
|
|
Quaternion::LookRotation(Vector3(-0.3f, -1.0f, -0.2f).Normalized()));
|
|
|
|
|
|
|
|
|
|
RenderSceneExtractor extractor;
|
|
|
|
|
const RenderSceneData sceneData = extractor.Extract(scene, nullptr, 800, 600);
|
|
|
|
|
|
|
|
|
|
ASSERT_TRUE(sceneData.HasCamera());
|
|
|
|
|
ASSERT_TRUE(sceneData.lighting.HasMainDirectionalLight());
|
|
|
|
|
EXPECT_FLOAT_EQ(sceneData.lighting.mainDirectionalLight.intensity, 2.5f);
|
|
|
|
|
EXPECT_EQ(sceneData.lighting.mainDirectionalLight.color.r, 1.0f);
|
|
|
|
|
EXPECT_EQ(sceneData.lighting.mainDirectionalLight.color.g, 0.8f);
|
|
|
|
|
EXPECT_EQ(sceneData.lighting.mainDirectionalLight.color.b, 0.6f);
|
2026-04-04 20:03:23 +08:00
|
|
|
EXPECT_TRUE(sceneData.lighting.mainDirectionalLight.castsShadows);
|
2026-04-01 00:41:56 +08:00
|
|
|
EXPECT_EQ(
|
|
|
|
|
sceneData.lighting.mainDirectionalLight.direction,
|
|
|
|
|
mainLightObject->GetTransform()->GetForward().Normalized() * -1.0f);
|
2026-04-05 15:55:48 +08:00
|
|
|
|
|
|
|
|
ASSERT_EQ(sceneData.lighting.additionalLightCount, 2u);
|
|
|
|
|
EXPECT_EQ(sceneData.lighting.additionalLights[0].type, RenderLightType::Directional);
|
|
|
|
|
EXPECT_FLOAT_EQ(sceneData.lighting.additionalLights[0].intensity, 0.5f);
|
|
|
|
|
EXPECT_EQ(sceneData.lighting.additionalLights[1].type, RenderLightType::Point);
|
|
|
|
|
EXPECT_FLOAT_EQ(sceneData.lighting.additionalLights[1].intensity, 10.0f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(RenderSceneExtractor_Test, ExtractsAdditionalLightsWithoutMainDirectionalAndFiltersByLightCullingMask) {
|
|
|
|
|
Scene scene("AdditionalLightsScene");
|
|
|
|
|
|
|
|
|
|
GameObject* cameraObject = scene.CreateGameObject("Camera");
|
|
|
|
|
auto* camera = cameraObject->AddComponent<CameraComponent>();
|
|
|
|
|
camera->SetPrimary(true);
|
|
|
|
|
camera->SetCullingMask(1u << 3);
|
|
|
|
|
|
|
|
|
|
GameObject* hiddenSpotObject = scene.CreateGameObject("HiddenSpot");
|
|
|
|
|
hiddenSpotObject->SetLayer(0);
|
|
|
|
|
auto* hiddenSpot = hiddenSpotObject->AddComponent<LightComponent>();
|
|
|
|
|
hiddenSpot->SetLightType(LightType::Spot);
|
|
|
|
|
hiddenSpot->SetIntensity(100.0f);
|
|
|
|
|
hiddenSpot->SetRange(50.0f);
|
|
|
|
|
hiddenSpot->SetSpotAngle(60.0f);
|
|
|
|
|
hiddenSpotObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, 1.0f));
|
|
|
|
|
|
|
|
|
|
GameObject* visiblePointObject = scene.CreateGameObject("VisiblePoint");
|
|
|
|
|
visiblePointObject->SetLayer(3);
|
|
|
|
|
auto* visiblePoint = visiblePointObject->AddComponent<LightComponent>();
|
|
|
|
|
visiblePoint->SetLightType(LightType::Point);
|
|
|
|
|
visiblePoint->SetColor(Color(0.9f, 0.2f, 0.1f, 1.0f));
|
|
|
|
|
visiblePoint->SetIntensity(4.0f);
|
|
|
|
|
visiblePoint->SetRange(12.0f);
|
|
|
|
|
visiblePointObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, 4.0f));
|
|
|
|
|
|
|
|
|
|
GameObject* visibleSpotObject = scene.CreateGameObject("VisibleSpot");
|
|
|
|
|
visibleSpotObject->SetLayer(3);
|
|
|
|
|
auto* visibleSpot = visibleSpotObject->AddComponent<LightComponent>();
|
|
|
|
|
visibleSpot->SetLightType(LightType::Spot);
|
|
|
|
|
visibleSpot->SetColor(Color(0.1f, 0.8f, 0.4f, 1.0f));
|
|
|
|
|
visibleSpot->SetIntensity(3.0f);
|
|
|
|
|
visibleSpot->SetRange(8.0f);
|
|
|
|
|
visibleSpot->SetSpotAngle(48.0f);
|
|
|
|
|
visibleSpotObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, 2.0f));
|
|
|
|
|
visibleSpotObject->GetTransform()->SetLocalRotation(
|
|
|
|
|
Quaternion::LookRotation(Vector3(0.0f, 0.0f, -1.0f)));
|
|
|
|
|
|
|
|
|
|
RenderSceneExtractor extractor;
|
|
|
|
|
const RenderSceneData sceneData = extractor.Extract(scene, nullptr, 800, 600);
|
|
|
|
|
|
|
|
|
|
EXPECT_FALSE(sceneData.lighting.HasMainDirectionalLight());
|
|
|
|
|
ASSERT_TRUE(sceneData.lighting.HasAdditionalLights());
|
|
|
|
|
ASSERT_EQ(sceneData.lighting.additionalLightCount, 2u);
|
|
|
|
|
|
|
|
|
|
const RenderAdditionalLightData& spotLight = sceneData.lighting.additionalLights[0];
|
|
|
|
|
EXPECT_EQ(spotLight.type, RenderLightType::Spot);
|
|
|
|
|
EXPECT_EQ(spotLight.color.g, 0.8f);
|
|
|
|
|
EXPECT_EQ(spotLight.position, Vector3(0.0f, 0.0f, 2.0f));
|
|
|
|
|
EXPECT_EQ(spotLight.direction, Vector3(0.0f, 0.0f, 1.0f));
|
|
|
|
|
EXPECT_FLOAT_EQ(spotLight.range, 8.0f);
|
|
|
|
|
EXPECT_FLOAT_EQ(spotLight.spotAngle, 48.0f);
|
|
|
|
|
|
|
|
|
|
const RenderAdditionalLightData& pointLightData = sceneData.lighting.additionalLights[1];
|
|
|
|
|
EXPECT_EQ(pointLightData.type, RenderLightType::Point);
|
|
|
|
|
EXPECT_EQ(pointLightData.color.r, 0.9f);
|
|
|
|
|
EXPECT_EQ(pointLightData.position, Vector3(0.0f, 0.0f, 4.0f));
|
|
|
|
|
EXPECT_FLOAT_EQ(pointLightData.range, 12.0f);
|
|
|
|
|
EXPECT_FLOAT_EQ(pointLightData.spotAngle, 0.0f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(RenderSceneExtractor_Test, LimitsAdditionalLightsToBoundedCountUsingStableTieBreakOrder) {
|
|
|
|
|
Scene scene("AdditionalLightCapScene");
|
|
|
|
|
|
|
|
|
|
GameObject* cameraObject = scene.CreateGameObject("Camera");
|
|
|
|
|
auto* camera = cameraObject->AddComponent<CameraComponent>();
|
|
|
|
|
camera->SetPrimary(true);
|
|
|
|
|
|
|
|
|
|
for (uint32_t index = 0; index < RenderLightingData::kMaxAdditionalLightCount + 2u; ++index) {
|
|
|
|
|
GameObject* lightObject = scene.CreateGameObject("PointLight" + std::to_string(index));
|
|
|
|
|
auto* light = lightObject->AddComponent<LightComponent>();
|
|
|
|
|
light->SetLightType(LightType::Point);
|
|
|
|
|
light->SetIntensity(1.0f);
|
|
|
|
|
light->SetRange(16.0f);
|
|
|
|
|
light->SetColor(Color(static_cast<float>(index), 0.0f, 0.0f, 1.0f));
|
|
|
|
|
lightObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, 4.0f));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RenderSceneExtractor extractor;
|
|
|
|
|
const RenderSceneData sceneData = extractor.Extract(scene, nullptr, 800, 600);
|
|
|
|
|
|
|
|
|
|
ASSERT_FALSE(sceneData.lighting.HasMainDirectionalLight());
|
|
|
|
|
ASSERT_EQ(sceneData.lighting.additionalLightCount, RenderLightingData::kMaxAdditionalLightCount);
|
|
|
|
|
for (uint32_t index = 0; index < RenderLightingData::kMaxAdditionalLightCount; ++index) {
|
|
|
|
|
EXPECT_EQ(sceneData.lighting.additionalLights[index].type, RenderLightType::Point);
|
|
|
|
|
EXPECT_FLOAT_EQ(sceneData.lighting.additionalLights[index].color.r, static_cast<float>(index));
|
|
|
|
|
}
|
2026-04-01 00:41:56 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-01 01:42:06 +08:00
|
|
|
TEST(RenderSceneExtractor_Test, FiltersVisibleItemsByCameraCullingMask) {
|
|
|
|
|
Scene scene("CullingMaskScene");
|
|
|
|
|
|
|
|
|
|
GameObject* cameraObject = scene.CreateGameObject("Camera");
|
|
|
|
|
auto* camera = cameraObject->AddComponent<CameraComponent>();
|
|
|
|
|
camera->SetPrimary(true);
|
|
|
|
|
camera->SetCullingMask(1u << 5);
|
|
|
|
|
|
|
|
|
|
GameObject* visibleObject = scene.CreateGameObject("VisibleLayer5");
|
|
|
|
|
visibleObject->SetLayer(5);
|
|
|
|
|
auto* visibleMeshFilter = visibleObject->AddComponent<MeshFilterComponent>();
|
|
|
|
|
visibleObject->AddComponent<MeshRendererComponent>();
|
|
|
|
|
Mesh* visibleMesh = CreateTestMesh("Meshes/layer5.mesh");
|
|
|
|
|
visibleMeshFilter->SetMesh(visibleMesh);
|
|
|
|
|
|
|
|
|
|
GameObject* hiddenObject = scene.CreateGameObject("HiddenLayer0");
|
|
|
|
|
hiddenObject->SetLayer(0);
|
|
|
|
|
auto* hiddenMeshFilter = hiddenObject->AddComponent<MeshFilterComponent>();
|
|
|
|
|
hiddenObject->AddComponent<MeshRendererComponent>();
|
|
|
|
|
Mesh* hiddenMesh = CreateTestMesh("Meshes/layer0.mesh");
|
|
|
|
|
hiddenMeshFilter->SetMesh(hiddenMesh);
|
|
|
|
|
|
|
|
|
|
RenderSceneExtractor extractor;
|
|
|
|
|
const RenderSceneData sceneData = extractor.Extract(scene, nullptr, 800, 600);
|
|
|
|
|
|
|
|
|
|
ASSERT_EQ(sceneData.visibleItems.size(), 1u);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleItems[0].gameObject, visibleObject);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleItems[0].mesh, visibleMesh);
|
|
|
|
|
|
|
|
|
|
visibleMeshFilter->ClearMesh();
|
|
|
|
|
hiddenMeshFilter->ClearMesh();
|
|
|
|
|
delete visibleMesh;
|
|
|
|
|
delete hiddenMesh;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 20:12:14 +08:00
|
|
|
TEST(RenderSceneExtractor_Test, ExtractsVisibleVolumesAndSortsByRenderQueue) {
|
|
|
|
|
Scene scene("VolumeScene");
|
|
|
|
|
|
|
|
|
|
GameObject* cameraObject = scene.CreateGameObject("Camera");
|
|
|
|
|
auto* camera = cameraObject->AddComponent<CameraComponent>();
|
|
|
|
|
camera->SetPrimary(true);
|
|
|
|
|
camera->SetCullingMask(1u << 0);
|
|
|
|
|
|
|
|
|
|
GameObject* opaqueVolumeObject = scene.CreateGameObject("OpaqueVolume");
|
|
|
|
|
opaqueVolumeObject->SetLayer(0);
|
|
|
|
|
opaqueVolumeObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, 2.0f));
|
|
|
|
|
auto* opaqueVolumeRenderer = opaqueVolumeObject->AddComponent<VolumeRendererComponent>();
|
|
|
|
|
VolumeField* opaqueVolume = CreateTestVolumeField("Volumes/opaque.nvdb");
|
|
|
|
|
Material* opaqueMaterial = CreateTestMaterial(
|
|
|
|
|
"Materials/opaque_volume.mat",
|
|
|
|
|
static_cast<int32_t>(MaterialRenderQueue::Geometry));
|
|
|
|
|
opaqueVolumeRenderer->SetVolumeField(opaqueVolume);
|
|
|
|
|
opaqueVolumeRenderer->SetMaterial(opaqueMaterial);
|
|
|
|
|
|
|
|
|
|
GameObject* transparentVolumeObject = scene.CreateGameObject("TransparentVolume");
|
|
|
|
|
transparentVolumeObject->SetLayer(0);
|
|
|
|
|
transparentVolumeObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, 8.0f));
|
|
|
|
|
auto* transparentVolumeRenderer = transparentVolumeObject->AddComponent<VolumeRendererComponent>();
|
|
|
|
|
VolumeField* transparentVolume = CreateTestVolumeField("Volumes/transparent.nvdb");
|
|
|
|
|
Material* transparentMaterial = CreateTestMaterial(
|
|
|
|
|
"Materials/transparent_volume.mat",
|
|
|
|
|
static_cast<int32_t>(MaterialRenderQueue::Transparent));
|
|
|
|
|
transparentVolumeRenderer->SetVolumeField(transparentVolume);
|
|
|
|
|
transparentVolumeRenderer->SetMaterial(transparentMaterial);
|
|
|
|
|
|
|
|
|
|
GameObject* hiddenVolumeObject = scene.CreateGameObject("HiddenVolume");
|
|
|
|
|
hiddenVolumeObject->SetLayer(2);
|
|
|
|
|
auto* hiddenVolumeRenderer = hiddenVolumeObject->AddComponent<VolumeRendererComponent>();
|
|
|
|
|
VolumeField* hiddenVolume = CreateTestVolumeField("Volumes/hidden.nvdb");
|
|
|
|
|
Material* hiddenMaterial = CreateTestMaterial(
|
|
|
|
|
"Materials/hidden_volume.mat",
|
|
|
|
|
static_cast<int32_t>(MaterialRenderQueue::Geometry));
|
|
|
|
|
hiddenVolumeRenderer->SetVolumeField(hiddenVolume);
|
|
|
|
|
hiddenVolumeRenderer->SetMaterial(hiddenMaterial);
|
|
|
|
|
|
|
|
|
|
RenderSceneExtractor extractor;
|
|
|
|
|
const RenderSceneData sceneData = extractor.Extract(scene, nullptr, 800, 600);
|
|
|
|
|
|
|
|
|
|
ASSERT_TRUE(sceneData.HasCamera());
|
|
|
|
|
EXPECT_TRUE(sceneData.visibleItems.empty());
|
|
|
|
|
ASSERT_EQ(sceneData.visibleVolumes.size(), 2u);
|
|
|
|
|
|
|
|
|
|
EXPECT_EQ(sceneData.visibleVolumes[0].gameObject, opaqueVolumeObject);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleVolumes[0].volumeRenderer, opaqueVolumeRenderer);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleVolumes[0].volumeField, opaqueVolume);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleVolumes[0].material, opaqueMaterial);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleVolumes[0].renderQueue, static_cast<int32_t>(MaterialRenderQueue::Geometry));
|
|
|
|
|
EXPECT_EQ(sceneData.visibleVolumes[0].localToWorld.GetTranslation(), Vector3(0.0f, 0.0f, 2.0f));
|
|
|
|
|
|
|
|
|
|
EXPECT_EQ(sceneData.visibleVolumes[1].gameObject, transparentVolumeObject);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleVolumes[1].volumeRenderer, transparentVolumeRenderer);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleVolumes[1].volumeField, transparentVolume);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleVolumes[1].material, transparentMaterial);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleVolumes[1].renderQueue, static_cast<int32_t>(MaterialRenderQueue::Transparent));
|
|
|
|
|
EXPECT_EQ(sceneData.visibleVolumes[1].localToWorld.GetTranslation(), Vector3(0.0f, 0.0f, 8.0f));
|
|
|
|
|
|
|
|
|
|
opaqueVolumeRenderer->ClearVolumeField();
|
|
|
|
|
opaqueVolumeRenderer->ClearMaterial();
|
|
|
|
|
transparentVolumeRenderer->ClearVolumeField();
|
|
|
|
|
transparentVolumeRenderer->ClearMaterial();
|
|
|
|
|
hiddenVolumeRenderer->ClearVolumeField();
|
|
|
|
|
hiddenVolumeRenderer->ClearMaterial();
|
|
|
|
|
delete opaqueVolume;
|
|
|
|
|
delete opaqueMaterial;
|
|
|
|
|
delete transparentVolume;
|
|
|
|
|
delete transparentMaterial;
|
|
|
|
|
delete hiddenVolume;
|
|
|
|
|
delete hiddenMaterial;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 21:49:53 +08:00
|
|
|
TEST(RenderSceneExtractor_Test, ExtractsVisibleGaussianSplatsAndSortsByRenderQueueAndDistance) {
|
|
|
|
|
Scene scene("GaussianSplatScene");
|
|
|
|
|
|
|
|
|
|
GameObject* cameraObject = scene.CreateGameObject("Camera");
|
|
|
|
|
auto* camera = cameraObject->AddComponent<CameraComponent>();
|
|
|
|
|
camera->SetPrimary(true);
|
|
|
|
|
camera->SetCullingMask(1u << 0);
|
|
|
|
|
|
|
|
|
|
GameObject* opaqueSplatObject = scene.CreateGameObject("OpaqueGaussianSplat");
|
|
|
|
|
opaqueSplatObject->SetLayer(0);
|
|
|
|
|
opaqueSplatObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, 2.0f));
|
|
|
|
|
auto* opaqueSplatRenderer = opaqueSplatObject->AddComponent<GaussianSplatRendererComponent>();
|
|
|
|
|
GaussianSplat* opaqueGaussianSplat = CreateTestGaussianSplat("GaussianSplats/opaque_room.xcgsplat");
|
|
|
|
|
Material* opaqueMaterial = CreateTestMaterial(
|
|
|
|
|
"Materials/opaque_gaussian_splat.mat",
|
|
|
|
|
static_cast<int32_t>(MaterialRenderQueue::Geometry));
|
|
|
|
|
opaqueSplatRenderer->SetGaussianSplat(opaqueGaussianSplat);
|
|
|
|
|
opaqueSplatRenderer->SetMaterial(opaqueMaterial);
|
|
|
|
|
|
|
|
|
|
GameObject* farTransparentObject = scene.CreateGameObject("FarTransparentGaussianSplat");
|
|
|
|
|
farTransparentObject->SetLayer(0);
|
|
|
|
|
farTransparentObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, 8.0f));
|
|
|
|
|
auto* farTransparentRenderer = farTransparentObject->AddComponent<GaussianSplatRendererComponent>();
|
|
|
|
|
GaussianSplat* farTransparentGaussianSplat = CreateTestGaussianSplat("GaussianSplats/far_transparent_room.xcgsplat");
|
|
|
|
|
Material* transparentMaterial = CreateTestMaterial(
|
|
|
|
|
"Materials/transparent_gaussian_splat.mat",
|
|
|
|
|
static_cast<int32_t>(MaterialRenderQueue::Transparent));
|
|
|
|
|
farTransparentRenderer->SetGaussianSplat(farTransparentGaussianSplat);
|
|
|
|
|
farTransparentRenderer->SetMaterial(transparentMaterial);
|
|
|
|
|
|
|
|
|
|
GameObject* nearTransparentObject = scene.CreateGameObject("NearTransparentGaussianSplat");
|
|
|
|
|
nearTransparentObject->SetLayer(0);
|
|
|
|
|
nearTransparentObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, 4.0f));
|
|
|
|
|
auto* nearTransparentRenderer = nearTransparentObject->AddComponent<GaussianSplatRendererComponent>();
|
|
|
|
|
GaussianSplat* nearTransparentGaussianSplat = CreateTestGaussianSplat("GaussianSplats/near_transparent_room.xcgsplat");
|
|
|
|
|
nearTransparentRenderer->SetGaussianSplat(nearTransparentGaussianSplat);
|
|
|
|
|
nearTransparentRenderer->SetMaterial(transparentMaterial);
|
|
|
|
|
|
|
|
|
|
GameObject* hiddenSplatObject = scene.CreateGameObject("HiddenGaussianSplat");
|
|
|
|
|
hiddenSplatObject->SetLayer(2);
|
|
|
|
|
auto* hiddenSplatRenderer = hiddenSplatObject->AddComponent<GaussianSplatRendererComponent>();
|
|
|
|
|
GaussianSplat* hiddenGaussianSplat = CreateTestGaussianSplat("GaussianSplats/hidden_room.xcgsplat");
|
|
|
|
|
Material* hiddenMaterial = CreateTestMaterial(
|
|
|
|
|
"Materials/hidden_gaussian_splat.mat",
|
|
|
|
|
static_cast<int32_t>(MaterialRenderQueue::Geometry));
|
|
|
|
|
hiddenSplatRenderer->SetGaussianSplat(hiddenGaussianSplat);
|
|
|
|
|
hiddenSplatRenderer->SetMaterial(hiddenMaterial);
|
|
|
|
|
|
|
|
|
|
RenderSceneExtractor extractor;
|
|
|
|
|
const RenderSceneData sceneData = extractor.Extract(scene, nullptr, 800, 600);
|
|
|
|
|
|
|
|
|
|
ASSERT_TRUE(sceneData.HasCamera());
|
|
|
|
|
EXPECT_TRUE(sceneData.visibleItems.empty());
|
|
|
|
|
EXPECT_TRUE(sceneData.visibleVolumes.empty());
|
|
|
|
|
ASSERT_EQ(sceneData.visibleGaussianSplats.size(), 3u);
|
|
|
|
|
|
|
|
|
|
EXPECT_EQ(sceneData.visibleGaussianSplats[0].gameObject, opaqueSplatObject);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleGaussianSplats[0].gaussianSplatRenderer, opaqueSplatRenderer);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleGaussianSplats[0].gaussianSplat, opaqueGaussianSplat);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleGaussianSplats[0].material, opaqueMaterial);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleGaussianSplats[0].renderQueue, static_cast<int32_t>(MaterialRenderQueue::Geometry));
|
|
|
|
|
EXPECT_EQ(sceneData.visibleGaussianSplats[0].localToWorld.GetTranslation(), Vector3(0.0f, 0.0f, 2.0f));
|
|
|
|
|
|
|
|
|
|
EXPECT_EQ(sceneData.visibleGaussianSplats[1].gameObject, farTransparentObject);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleGaussianSplats[1].gaussianSplatRenderer, farTransparentRenderer);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleGaussianSplats[1].gaussianSplat, farTransparentGaussianSplat);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleGaussianSplats[1].material, transparentMaterial);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleGaussianSplats[1].renderQueue, static_cast<int32_t>(MaterialRenderQueue::Transparent));
|
|
|
|
|
EXPECT_EQ(sceneData.visibleGaussianSplats[1].localToWorld.GetTranslation(), Vector3(0.0f, 0.0f, 8.0f));
|
|
|
|
|
|
|
|
|
|
EXPECT_EQ(sceneData.visibleGaussianSplats[2].gameObject, nearTransparentObject);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleGaussianSplats[2].gaussianSplatRenderer, nearTransparentRenderer);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleGaussianSplats[2].gaussianSplat, nearTransparentGaussianSplat);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleGaussianSplats[2].material, transparentMaterial);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleGaussianSplats[2].renderQueue, static_cast<int32_t>(MaterialRenderQueue::Transparent));
|
|
|
|
|
EXPECT_EQ(sceneData.visibleGaussianSplats[2].localToWorld.GetTranslation(), Vector3(0.0f, 0.0f, 4.0f));
|
|
|
|
|
|
|
|
|
|
opaqueSplatRenderer->ClearGaussianSplat();
|
|
|
|
|
opaqueSplatRenderer->ClearMaterial();
|
|
|
|
|
farTransparentRenderer->ClearGaussianSplat();
|
|
|
|
|
farTransparentRenderer->ClearMaterial();
|
|
|
|
|
nearTransparentRenderer->ClearGaussianSplat();
|
|
|
|
|
nearTransparentRenderer->ClearMaterial();
|
|
|
|
|
hiddenSplatRenderer->ClearGaussianSplat();
|
|
|
|
|
hiddenSplatRenderer->ClearMaterial();
|
|
|
|
|
|
|
|
|
|
delete opaqueGaussianSplat;
|
|
|
|
|
delete farTransparentGaussianSplat;
|
|
|
|
|
delete nearTransparentGaussianSplat;
|
|
|
|
|
delete hiddenGaussianSplat;
|
|
|
|
|
delete opaqueMaterial;
|
|
|
|
|
delete transparentMaterial;
|
|
|
|
|
delete hiddenMaterial;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 11:56:23 +08:00
|
|
|
TEST(RenderSceneExtractor_Test, ExtractsSectionLevelVisibleItemsAndSortsByRenderQueue) {
|
|
|
|
|
Scene scene("SectionScene");
|
|
|
|
|
|
|
|
|
|
GameObject* cameraObject = scene.CreateGameObject("Camera");
|
|
|
|
|
auto* camera = cameraObject->AddComponent<CameraComponent>();
|
|
|
|
|
camera->SetPrimary(true);
|
|
|
|
|
|
|
|
|
|
GameObject* renderObject = scene.CreateGameObject("MultiSectionMesh");
|
|
|
|
|
renderObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, 5.0f));
|
|
|
|
|
auto* meshFilter = renderObject->AddComponent<MeshFilterComponent>();
|
|
|
|
|
auto* meshRenderer = renderObject->AddComponent<MeshRendererComponent>();
|
|
|
|
|
|
|
|
|
|
Mesh* mesh = CreateSectionedTestMesh("Meshes/sectioned.mesh", { 1u, 0u });
|
|
|
|
|
Material* opaqueMaterial = CreateTestMaterial(
|
|
|
|
|
"Materials/opaque.mat",
|
2026-04-08 04:27:21 +08:00
|
|
|
static_cast<int32_t>(MaterialRenderQueue::Geometry));
|
2026-03-27 11:56:23 +08:00
|
|
|
Material* transparentMaterial = CreateTestMaterial(
|
|
|
|
|
"Materials/transparent.mat",
|
2026-04-08 04:27:21 +08:00
|
|
|
static_cast<int32_t>(MaterialRenderQueue::Transparent));
|
2026-03-27 11:56:23 +08:00
|
|
|
|
|
|
|
|
meshFilter->SetMesh(mesh);
|
|
|
|
|
meshRenderer->SetMaterial(0, opaqueMaterial);
|
|
|
|
|
meshRenderer->SetMaterial(1, transparentMaterial);
|
|
|
|
|
|
|
|
|
|
RenderSceneExtractor extractor;
|
|
|
|
|
const RenderSceneData sceneData = extractor.Extract(scene, nullptr, 800, 600);
|
|
|
|
|
|
|
|
|
|
ASSERT_TRUE(sceneData.HasCamera());
|
|
|
|
|
ASSERT_EQ(sceneData.visibleItems.size(), 2u);
|
|
|
|
|
|
|
|
|
|
EXPECT_TRUE(sceneData.visibleItems[0].hasSection);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleItems[0].sectionIndex, 1u);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleItems[0].materialIndex, 0u);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleItems[0].material, opaqueMaterial);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleItems[0].renderQueue, static_cast<int32_t>(MaterialRenderQueue::Geometry));
|
|
|
|
|
|
|
|
|
|
EXPECT_TRUE(sceneData.visibleItems[1].hasSection);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleItems[1].sectionIndex, 0u);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleItems[1].materialIndex, 1u);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleItems[1].material, transparentMaterial);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleItems[1].renderQueue, static_cast<int32_t>(MaterialRenderQueue::Transparent));
|
|
|
|
|
|
|
|
|
|
meshRenderer->ClearMaterials();
|
|
|
|
|
meshFilter->ClearMesh();
|
|
|
|
|
delete opaqueMaterial;
|
|
|
|
|
delete transparentMaterial;
|
|
|
|
|
delete mesh;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(RenderSceneExtractor_Test, SortsOpaqueFrontToBackAndTransparentBackToFront) {
|
|
|
|
|
Scene scene("QueueSortScene");
|
|
|
|
|
|
|
|
|
|
GameObject* cameraObject = scene.CreateGameObject("Camera");
|
|
|
|
|
auto* camera = cameraObject->AddComponent<CameraComponent>();
|
|
|
|
|
camera->SetPrimary(true);
|
|
|
|
|
|
|
|
|
|
auto createRenderable = [&](const char* name, float z, int32_t renderQueue) -> Mesh* {
|
|
|
|
|
GameObject* object = scene.CreateGameObject(name);
|
|
|
|
|
object->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, z));
|
|
|
|
|
auto* meshFilter = object->AddComponent<MeshFilterComponent>();
|
|
|
|
|
auto* meshRenderer = object->AddComponent<MeshRendererComponent>();
|
|
|
|
|
Mesh* mesh = CreateTestMesh(name);
|
2026-04-08 04:27:21 +08:00
|
|
|
Material* material = CreateTestMaterial(name, renderQueue);
|
2026-03-27 11:56:23 +08:00
|
|
|
meshFilter->SetMesh(mesh);
|
|
|
|
|
meshRenderer->SetMaterial(0, material);
|
|
|
|
|
return mesh;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Mesh* opaqueFarMesh = createRenderable("OpaqueFar", 10.0f, static_cast<int32_t>(MaterialRenderQueue::Geometry));
|
|
|
|
|
Mesh* transparentNearMesh = createRenderable("TransparentNear", 2.0f, static_cast<int32_t>(MaterialRenderQueue::Transparent));
|
|
|
|
|
Mesh* opaqueNearMesh = createRenderable("OpaqueNear", 2.0f, static_cast<int32_t>(MaterialRenderQueue::Geometry));
|
|
|
|
|
Mesh* transparentFarMesh = createRenderable("TransparentFar", 10.0f, static_cast<int32_t>(MaterialRenderQueue::Transparent));
|
|
|
|
|
|
|
|
|
|
RenderSceneExtractor extractor;
|
|
|
|
|
const RenderSceneData sceneData = extractor.Extract(scene, nullptr, 800, 600);
|
|
|
|
|
|
|
|
|
|
ASSERT_EQ(sceneData.visibleItems.size(), 4u);
|
|
|
|
|
EXPECT_STREQ(sceneData.visibleItems[0].gameObject->GetName().c_str(), "OpaqueNear");
|
|
|
|
|
EXPECT_STREQ(sceneData.visibleItems[1].gameObject->GetName().c_str(), "OpaqueFar");
|
|
|
|
|
EXPECT_STREQ(sceneData.visibleItems[2].gameObject->GetName().c_str(), "TransparentFar");
|
|
|
|
|
EXPECT_STREQ(sceneData.visibleItems[3].gameObject->GetName().c_str(), "TransparentNear");
|
|
|
|
|
|
|
|
|
|
auto cleanupObject = [](GameObject* object) {
|
|
|
|
|
auto* meshFilter = object->GetComponent<MeshFilterComponent>();
|
|
|
|
|
auto* meshRenderer = object->GetComponent<MeshRendererComponent>();
|
|
|
|
|
Material* material = meshRenderer != nullptr ? meshRenderer->GetMaterial(0) : nullptr;
|
|
|
|
|
if (meshRenderer != nullptr) {
|
|
|
|
|
meshRenderer->ClearMaterials();
|
|
|
|
|
}
|
|
|
|
|
if (meshFilter != nullptr) {
|
|
|
|
|
meshFilter->ClearMesh();
|
|
|
|
|
}
|
|
|
|
|
delete material;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
cleanupObject(scene.Find("OpaqueFar"));
|
|
|
|
|
cleanupObject(scene.Find("TransparentNear"));
|
|
|
|
|
cleanupObject(scene.Find("OpaqueNear"));
|
|
|
|
|
cleanupObject(scene.Find("TransparentFar"));
|
|
|
|
|
|
|
|
|
|
delete opaqueFarMesh;
|
|
|
|
|
delete transparentNearMesh;
|
|
|
|
|
delete opaqueNearMesh;
|
|
|
|
|
delete transparentFarMesh;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 19:26:08 +08:00
|
|
|
TEST(RenderSceneExtractor_Test, FallsBackToEmbeddedMeshMaterialsWhenRendererHasNoExplicitSlots) {
|
|
|
|
|
Scene scene("EmbeddedMaterialScene");
|
|
|
|
|
|
|
|
|
|
GameObject* cameraObject = scene.CreateGameObject("Camera");
|
|
|
|
|
auto* camera = cameraObject->AddComponent<CameraComponent>();
|
|
|
|
|
camera->SetPrimary(true);
|
|
|
|
|
|
|
|
|
|
GameObject* renderObject = scene.CreateGameObject("EmbeddedBackpack");
|
|
|
|
|
auto* meshFilter = renderObject->AddComponent<MeshFilterComponent>();
|
|
|
|
|
auto* meshRenderer = renderObject->AddComponent<MeshRendererComponent>();
|
|
|
|
|
|
|
|
|
|
Mesh* mesh = CreateSectionedTestMesh("Meshes/embedded.mesh", { 0u });
|
|
|
|
|
Material* embeddedMaterial = CreateTestMaterial(
|
|
|
|
|
"Materials/embedded.mat",
|
2026-04-08 04:27:21 +08:00
|
|
|
static_cast<int32_t>(MaterialRenderQueue::Transparent));
|
2026-03-28 19:26:08 +08:00
|
|
|
mesh->AddMaterial(embeddedMaterial);
|
|
|
|
|
meshFilter->SetMesh(mesh);
|
|
|
|
|
meshRenderer->ClearMaterials();
|
|
|
|
|
|
|
|
|
|
RenderSceneExtractor extractor;
|
|
|
|
|
const RenderSceneData sceneData = extractor.Extract(scene, nullptr, 800, 600);
|
|
|
|
|
|
|
|
|
|
ASSERT_EQ(sceneData.visibleItems.size(), 1u);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleItems[0].material, embeddedMaterial);
|
|
|
|
|
EXPECT_EQ(sceneData.visibleItems[0].renderQueue, static_cast<int32_t>(MaterialRenderQueue::Transparent));
|
|
|
|
|
EXPECT_EQ(ResolveMaterial(sceneData.visibleItems[0]), embeddedMaterial);
|
|
|
|
|
|
|
|
|
|
meshFilter->ClearMesh();
|
|
|
|
|
delete mesh;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 04:59:49 +08:00
|
|
|
TEST(RenderMaterialUtility_Test, NullMaterialsDoNotMatchBuiltinSurfacePasses) {
|
|
|
|
|
EXPECT_FALSE(MatchesBuiltinPass(nullptr, BuiltinMaterialPass::ForwardLit));
|
|
|
|
|
EXPECT_FALSE(MatchesBuiltinPass(nullptr, BuiltinMaterialPass::Unlit));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(RenderMaterialUtility_Test, MaterialsWithoutShaderMetadataDoNotMatchBuiltinSurfacePasses) {
|
2026-03-27 11:56:23 +08:00
|
|
|
Material noMetadataMaterial;
|
2026-04-08 04:59:49 +08:00
|
|
|
EXPECT_FALSE(MatchesBuiltinPass(&noMetadataMaterial, BuiltinMaterialPass::ForwardLit));
|
|
|
|
|
EXPECT_FALSE(MatchesBuiltinPass(&noMetadataMaterial, BuiltinMaterialPass::Unlit));
|
2026-04-02 16:26:20 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 04:27:21 +08:00
|
|
|
TEST(RenderMaterialUtility_Test, ShaderPassMetadataSupportsUnlitDepthAndObjectId) {
|
2026-04-02 16:26:20 +08:00
|
|
|
Material unlitMaterial;
|
2026-04-08 04:27:21 +08:00
|
|
|
auto* unlitShader = new Shader();
|
|
|
|
|
ShaderPass unlitPass = {};
|
|
|
|
|
unlitPass.name = "Unlit";
|
|
|
|
|
unlitShader->AddPass(unlitPass);
|
|
|
|
|
unlitMaterial.SetShader(ResourceHandle<Shader>(unlitShader));
|
2026-04-02 16:26:20 +08:00
|
|
|
EXPECT_TRUE(MatchesBuiltinPass(&unlitMaterial, BuiltinMaterialPass::Unlit));
|
|
|
|
|
EXPECT_FALSE(MatchesBuiltinPass(&unlitMaterial, BuiltinMaterialPass::ForwardLit));
|
|
|
|
|
|
|
|
|
|
Material depthMaterial;
|
2026-04-08 04:27:21 +08:00
|
|
|
auto* depthShader = new Shader();
|
|
|
|
|
ShaderPass depthPass = {};
|
|
|
|
|
depthPass.name = "DepthOnly";
|
|
|
|
|
depthShader->AddPass(depthPass);
|
|
|
|
|
depthMaterial.SetShader(ResourceHandle<Shader>(depthShader));
|
2026-04-02 16:26:20 +08:00
|
|
|
EXPECT_TRUE(MatchesBuiltinPass(&depthMaterial, BuiltinMaterialPass::DepthOnly));
|
|
|
|
|
EXPECT_FALSE(MatchesBuiltinPass(&depthMaterial, BuiltinMaterialPass::Unlit));
|
|
|
|
|
|
|
|
|
|
Material objectIdMaterial;
|
2026-04-08 04:27:21 +08:00
|
|
|
auto* objectIdShader = new Shader();
|
|
|
|
|
ShaderPass objectIdPass = {};
|
|
|
|
|
objectIdPass.name = "ObjectId";
|
|
|
|
|
objectIdShader->AddPass(objectIdPass);
|
|
|
|
|
objectIdMaterial.SetShader(ResourceHandle<Shader>(objectIdShader));
|
2026-04-02 16:26:20 +08:00
|
|
|
EXPECT_TRUE(MatchesBuiltinPass(&objectIdMaterial, BuiltinMaterialPass::ObjectId));
|
|
|
|
|
EXPECT_FALSE(MatchesBuiltinPass(&objectIdMaterial, BuiltinMaterialPass::ForwardLit));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(RenderMaterialUtility_Test, ShaderPassMetadataCanDriveBuiltinPassMatching) {
|
|
|
|
|
Material material;
|
|
|
|
|
auto* shader = new Shader();
|
|
|
|
|
|
|
|
|
|
ShaderPass forwardPass = {};
|
|
|
|
|
forwardPass.name = "ForwardLit";
|
|
|
|
|
shader->AddPass(forwardPass);
|
|
|
|
|
|
|
|
|
|
ResourceHandle<Shader> shaderHandle(shader);
|
|
|
|
|
material.SetShader(shaderHandle);
|
|
|
|
|
|
|
|
|
|
EXPECT_TRUE(MatchesBuiltinPass(&material, BuiltinMaterialPass::ForwardLit));
|
|
|
|
|
EXPECT_FALSE(MatchesBuiltinPass(&material, BuiltinMaterialPass::Unlit));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 13:13:42 +08:00
|
|
|
TEST(RenderMaterialUtility_Test, CanonicalBuiltinPassMetadataMatchesFinalColorAndPostProcessPasses) {
|
|
|
|
|
ShaderPass finalColorPass = {};
|
|
|
|
|
finalColorPass.name = "FinalColor";
|
|
|
|
|
EXPECT_TRUE(ShaderPassMatchesBuiltinPass(finalColorPass, BuiltinMaterialPass::FinalColor));
|
|
|
|
|
|
|
|
|
|
ShaderPass postProcessPass = {};
|
|
|
|
|
postProcessPass.name = "ColorScale";
|
|
|
|
|
EXPECT_TRUE(ShaderPassMatchesBuiltinPass(postProcessPass, BuiltinMaterialPass::PostProcess));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(RenderMaterialUtility_Test, LegacyBuiltinPassAliasesDoNotMatchCanonicalBuiltinPasses) {
|
|
|
|
|
ShaderPass forwardAliasByName = {};
|
|
|
|
|
forwardAliasByName.name = "ForwardBase";
|
|
|
|
|
EXPECT_FALSE(ShaderPassMatchesBuiltinPass(forwardAliasByName, BuiltinMaterialPass::ForwardLit));
|
|
|
|
|
|
|
|
|
|
ShaderPassTagEntry forwardAliasTag = {};
|
|
|
|
|
forwardAliasTag.name = "LightMode";
|
|
|
|
|
forwardAliasTag.value = "ForwardBase";
|
|
|
|
|
ShaderPass forwardAliasByTag = {};
|
|
|
|
|
forwardAliasByTag.name = "Default";
|
|
|
|
|
forwardAliasByTag.tags.PushBack(forwardAliasTag);
|
|
|
|
|
EXPECT_FALSE(ShaderPassMatchesBuiltinPass(forwardAliasByTag, BuiltinMaterialPass::ForwardLit));
|
|
|
|
|
|
|
|
|
|
ShaderPassTagEntry finalColorAliasTag = {};
|
|
|
|
|
finalColorAliasTag.name = "LightMode";
|
|
|
|
|
finalColorAliasTag.value = "FinalOutput";
|
|
|
|
|
ShaderPass finalColorAliasPass = {};
|
|
|
|
|
finalColorAliasPass.name = "Default";
|
|
|
|
|
finalColorAliasPass.tags.PushBack(finalColorAliasTag);
|
|
|
|
|
EXPECT_FALSE(ShaderPassMatchesBuiltinPass(finalColorAliasPass, BuiltinMaterialPass::FinalColor));
|
|
|
|
|
|
|
|
|
|
ShaderPassTagEntry postProcessAliasTag = {};
|
|
|
|
|
postProcessAliasTag.name = "LightMode";
|
|
|
|
|
postProcessAliasTag.value = "PostProcess";
|
|
|
|
|
ShaderPass postProcessAliasPass = {};
|
|
|
|
|
postProcessAliasPass.name = "Default";
|
|
|
|
|
postProcessAliasPass.tags.PushBack(postProcessAliasTag);
|
|
|
|
|
EXPECT_FALSE(ShaderPassMatchesBuiltinPass(postProcessAliasPass, BuiltinMaterialPass::PostProcess));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 16:26:20 +08:00
|
|
|
TEST(RenderMaterialUtility_Test, ExplicitShaderPassMetadataDisablesImplicitForwardFallback) {
|
|
|
|
|
Material material;
|
|
|
|
|
auto* shader = new Shader();
|
|
|
|
|
|
|
|
|
|
ShaderPass unlitPass = {};
|
|
|
|
|
unlitPass.name = "Unlit";
|
|
|
|
|
shader->AddPass(unlitPass);
|
|
|
|
|
|
|
|
|
|
ResourceHandle<Shader> shaderHandle(shader);
|
|
|
|
|
material.SetShader(shaderHandle);
|
|
|
|
|
|
|
|
|
|
EXPECT_FALSE(MatchesBuiltinPass(&material, BuiltinMaterialPass::ForwardLit));
|
|
|
|
|
EXPECT_TRUE(MatchesBuiltinPass(&material, BuiltinMaterialPass::Unlit));
|
2026-03-27 11:56:23 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 04:27:21 +08:00
|
|
|
TEST(RenderMaterialUtility_Test, ShaderMetadataOverridesConflictingMaterialTags) {
|
2026-04-06 18:24:10 +08:00
|
|
|
Material material;
|
|
|
|
|
auto* shader = new Shader();
|
|
|
|
|
|
|
|
|
|
ShaderPass forwardPass = {};
|
|
|
|
|
forwardPass.name = "ForwardLit";
|
|
|
|
|
shader->AddPass(forwardPass);
|
|
|
|
|
|
|
|
|
|
material.SetShader(ResourceHandle<Shader>(shader));
|
|
|
|
|
material.SetTag("LightMode", "DepthOnly");
|
|
|
|
|
|
|
|
|
|
EXPECT_TRUE(MatchesBuiltinPass(&material, BuiltinMaterialPass::ForwardLit));
|
|
|
|
|
EXPECT_FALSE(MatchesBuiltinPass(&material, BuiltinMaterialPass::ShadowCaster));
|
|
|
|
|
EXPECT_FALSE(MatchesBuiltinPass(&material, BuiltinMaterialPass::DepthOnly));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 04:59:49 +08:00
|
|
|
TEST(RenderMaterialUtility_Test, MaterialTagsDoNotOverrideMissingShaderMetadata) {
|
2026-04-06 18:24:10 +08:00
|
|
|
Material material;
|
|
|
|
|
auto* shader = new Shader();
|
|
|
|
|
|
|
|
|
|
ShaderPass defaultPass = {};
|
|
|
|
|
defaultPass.name = "Default";
|
|
|
|
|
shader->AddPass(defaultPass);
|
|
|
|
|
|
|
|
|
|
material.SetShader(ResourceHandle<Shader>(shader));
|
2026-04-08 04:27:21 +08:00
|
|
|
material.SetTag("LightMode", "ShadowCaster");
|
2026-04-06 18:24:10 +08:00
|
|
|
|
2026-04-08 04:59:49 +08:00
|
|
|
EXPECT_FALSE(MatchesBuiltinPass(&material, BuiltinMaterialPass::ForwardLit));
|
2026-04-08 04:27:21 +08:00
|
|
|
EXPECT_FALSE(MatchesBuiltinPass(&material, BuiltinMaterialPass::ShadowCaster));
|
2026-04-06 18:24:10 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 04:59:49 +08:00
|
|
|
TEST(RenderMaterialUtility_Test, ShadersWithoutBuiltinMetadataDoNotMatchBuiltinSurfacePasses) {
|
2026-04-06 18:24:10 +08:00
|
|
|
Material material;
|
|
|
|
|
auto* shader = new Shader();
|
|
|
|
|
|
|
|
|
|
ShaderPass defaultPass = {};
|
|
|
|
|
defaultPass.name = "Default";
|
|
|
|
|
shader->AddPass(defaultPass);
|
|
|
|
|
|
|
|
|
|
material.SetShader(ResourceHandle<Shader>(shader));
|
|
|
|
|
|
2026-04-08 04:59:49 +08:00
|
|
|
EXPECT_FALSE(MatchesBuiltinPass(&material, BuiltinMaterialPass::ForwardLit));
|
2026-04-06 18:24:10 +08:00
|
|
|
EXPECT_FALSE(MatchesBuiltinPass(&material, BuiltinMaterialPass::Unlit));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 04:27:21 +08:00
|
|
|
TEST(RenderMaterialUtility_Test, MaterialsWithoutFormalShaderMetadataResolveBuiltinDefaultsOnly) {
|
|
|
|
|
Material material;
|
|
|
|
|
material.SetFloat4("baseColor", Vector4(0.2f, 0.4f, 0.6f, 0.8f));
|
|
|
|
|
material.SetFloat("_Cutoff", 0.3f);
|
|
|
|
|
material.SetFloat("opacity", 0.35f);
|
2026-04-02 17:13:53 +08:00
|
|
|
|
|
|
|
|
Texture* baseColorTexture = new Texture();
|
|
|
|
|
IResource::ConstructParams textureParams = {};
|
|
|
|
|
textureParams.name = "AliasBaseColor";
|
|
|
|
|
textureParams.path = "Textures/alias_base_color.texture";
|
|
|
|
|
textureParams.guid = ResourceGUID::Generate(textureParams.path);
|
|
|
|
|
baseColorTexture->Initialize(textureParams);
|
2026-04-08 04:27:21 +08:00
|
|
|
material.SetTexture("_BaseColorTexture", ResourceHandle<Texture>(baseColorTexture));
|
2026-04-02 17:13:53 +08:00
|
|
|
|
2026-04-08 04:27:21 +08:00
|
|
|
EXPECT_EQ(ResolveBuiltinBaseColorFactor(&material), Vector4(1.0f, 1.0f, 1.0f, 1.0f));
|
|
|
|
|
EXPECT_FLOAT_EQ(ResolveBuiltinAlphaCutoff(&material), 0.5f);
|
|
|
|
|
EXPECT_EQ(ResolveBuiltinBaseColorTexture(&material), nullptr);
|
|
|
|
|
EXPECT_FALSE(ResolveSchemaMaterialConstantPayload(&material).IsValid());
|
2026-04-02 17:13:53 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 00:01:31 +08:00
|
|
|
TEST(RenderMaterialUtility_Test, ResolvesBuiltinForwardMaterialContractFromShaderSemanticMetadata) {
|
|
|
|
|
auto* shader = new Shader();
|
|
|
|
|
|
|
|
|
|
ShaderPropertyDesc colorProperty = {};
|
|
|
|
|
colorProperty.name = "TintColor";
|
|
|
|
|
colorProperty.displayName = "Tint";
|
|
|
|
|
colorProperty.type = ShaderPropertyType::Color;
|
|
|
|
|
colorProperty.semantic = "BaseColor";
|
|
|
|
|
shader->AddProperty(colorProperty);
|
|
|
|
|
|
|
|
|
|
ShaderPropertyDesc textureProperty = {};
|
|
|
|
|
textureProperty.name = "AlbedoMap";
|
|
|
|
|
textureProperty.displayName = "Albedo";
|
|
|
|
|
textureProperty.type = ShaderPropertyType::Texture2D;
|
|
|
|
|
textureProperty.semantic = "BaseColorTexture";
|
|
|
|
|
shader->AddProperty(textureProperty);
|
|
|
|
|
|
2026-04-06 21:05:50 +08:00
|
|
|
ShaderPropertyDesc cutoffProperty = {};
|
|
|
|
|
cutoffProperty.name = "AlphaClipThreshold";
|
|
|
|
|
cutoffProperty.displayName = "Alpha Clip";
|
|
|
|
|
cutoffProperty.type = ShaderPropertyType::Range;
|
|
|
|
|
cutoffProperty.defaultValue = "0.5";
|
|
|
|
|
cutoffProperty.semantic = "AlphaCutoff";
|
|
|
|
|
shader->AddProperty(cutoffProperty);
|
|
|
|
|
|
2026-04-03 00:01:31 +08:00
|
|
|
Material material;
|
|
|
|
|
material.SetShader(ResourceHandle<Shader>(shader));
|
|
|
|
|
material.SetFloat4("TintColor", Vector4(0.3f, 0.5f, 0.7f, 0.9f));
|
2026-04-06 21:05:50 +08:00
|
|
|
material.SetFloat("AlphaClipThreshold", 0.61f);
|
2026-04-03 00:01:31 +08:00
|
|
|
|
|
|
|
|
Texture* texture = new Texture();
|
|
|
|
|
IResource::ConstructParams textureParams = {};
|
|
|
|
|
textureParams.name = "SemanticTexture";
|
|
|
|
|
textureParams.path = "Textures/semantic_base_color.texture";
|
|
|
|
|
textureParams.guid = ResourceGUID::Generate(textureParams.path);
|
|
|
|
|
texture->Initialize(textureParams);
|
|
|
|
|
material.SetTexture("AlbedoMap", ResourceHandle<Texture>(texture));
|
|
|
|
|
|
|
|
|
|
EXPECT_EQ(ResolveBuiltinBaseColorFactor(&material), Vector4(0.3f, 0.5f, 0.7f, 0.9f));
|
|
|
|
|
EXPECT_EQ(ResolveBuiltinBaseColorTexture(&material), texture);
|
2026-04-06 21:05:50 +08:00
|
|
|
EXPECT_FLOAT_EQ(ResolveBuiltinAlphaCutoff(&material), 0.61f);
|
2026-04-03 00:01:31 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 16:49:30 +08:00
|
|
|
TEST(RenderMaterialUtility_Test, ResolvesBuiltinForwardMaterialContractFromShaderSemanticDefaults) {
|
|
|
|
|
auto* shader = new Shader();
|
|
|
|
|
|
|
|
|
|
ShaderPropertyDesc colorProperty = {};
|
|
|
|
|
colorProperty.name = "TintColor";
|
|
|
|
|
colorProperty.displayName = "Tint";
|
|
|
|
|
colorProperty.type = ShaderPropertyType::Color;
|
|
|
|
|
colorProperty.defaultValue = "(0.11,0.22,0.33,0.44)";
|
|
|
|
|
colorProperty.semantic = "BaseColor";
|
|
|
|
|
shader->AddProperty(colorProperty);
|
|
|
|
|
|
2026-04-06 21:05:50 +08:00
|
|
|
ShaderPropertyDesc cutoffProperty = {};
|
|
|
|
|
cutoffProperty.name = "_Cutoff";
|
|
|
|
|
cutoffProperty.type = ShaderPropertyType::Range;
|
|
|
|
|
cutoffProperty.defaultValue = "0.37";
|
|
|
|
|
cutoffProperty.semantic = "AlphaCutoff";
|
|
|
|
|
shader->AddProperty(cutoffProperty);
|
|
|
|
|
|
2026-04-03 16:49:30 +08:00
|
|
|
Material material;
|
|
|
|
|
material.SetShader(ResourceHandle<Shader>(shader));
|
|
|
|
|
|
|
|
|
|
EXPECT_EQ(ResolveBuiltinBaseColorFactor(&material), Vector4(0.11f, 0.22f, 0.33f, 0.44f));
|
2026-04-06 21:05:50 +08:00
|
|
|
EXPECT_FLOAT_EQ(ResolveBuiltinAlphaCutoff(&material), 0.37f);
|
2026-04-03 16:49:30 +08:00
|
|
|
EXPECT_EQ(ResolveBuiltinBaseColorTexture(&material), nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 13:13:42 +08:00
|
|
|
TEST(RenderMaterialUtility_Test, ResolvesBuiltinSkyboxMaterialContractFromShaderSemanticMetadata) {
|
|
|
|
|
auto* shader = new Shader();
|
|
|
|
|
|
|
|
|
|
ShaderPropertyDesc tintProperty = {};
|
|
|
|
|
tintProperty.name = "SkyTintColor";
|
|
|
|
|
tintProperty.type = ShaderPropertyType::Color;
|
|
|
|
|
tintProperty.semantic = "Tint";
|
|
|
|
|
shader->AddProperty(tintProperty);
|
|
|
|
|
|
|
|
|
|
ShaderPropertyDesc exposureProperty = {};
|
|
|
|
|
exposureProperty.name = "ExposureControl";
|
|
|
|
|
exposureProperty.type = ShaderPropertyType::Float;
|
|
|
|
|
exposureProperty.semantic = "Exposure";
|
|
|
|
|
shader->AddProperty(exposureProperty);
|
|
|
|
|
|
|
|
|
|
ShaderPropertyDesc rotationProperty = {};
|
|
|
|
|
rotationProperty.name = "SkyYawDegrees";
|
|
|
|
|
rotationProperty.type = ShaderPropertyType::Float;
|
|
|
|
|
rotationProperty.semantic = "Rotation";
|
|
|
|
|
shader->AddProperty(rotationProperty);
|
|
|
|
|
|
|
|
|
|
ShaderPropertyDesc panoramicProperty = {};
|
|
|
|
|
panoramicProperty.name = "SkyPanorama";
|
|
|
|
|
panoramicProperty.type = ShaderPropertyType::Texture2D;
|
|
|
|
|
panoramicProperty.semantic = "SkyboxPanoramicTexture";
|
|
|
|
|
shader->AddProperty(panoramicProperty);
|
|
|
|
|
|
|
|
|
|
Material panoramicMaterial;
|
|
|
|
|
panoramicMaterial.SetShader(ResourceHandle<Shader>(shader));
|
|
|
|
|
panoramicMaterial.SetFloat4("SkyTintColor", Vector4(0.2f, 0.3f, 0.4f, 1.0f));
|
|
|
|
|
panoramicMaterial.SetFloat("ExposureControl", 1.35f);
|
|
|
|
|
panoramicMaterial.SetFloat("SkyYawDegrees", 27.0f);
|
|
|
|
|
Texture* panoramicTexture = CreateTestTexture("Textures/sky_panorama.texture", TextureType::Texture2D);
|
|
|
|
|
panoramicMaterial.SetTexture("SkyPanorama", ResourceHandle<Texture>(panoramicTexture));
|
|
|
|
|
|
|
|
|
|
EXPECT_EQ(ResolveSkyboxTint(&panoramicMaterial), Vector4(0.2f, 0.3f, 0.4f, 1.0f));
|
|
|
|
|
EXPECT_FLOAT_EQ(ResolveSkyboxExposure(&panoramicMaterial), 1.35f);
|
|
|
|
|
EXPECT_FLOAT_EQ(ResolveSkyboxRotationDegrees(&panoramicMaterial), 27.0f);
|
|
|
|
|
EXPECT_EQ(ResolveSkyboxPanoramicTexture(&panoramicMaterial), panoramicTexture);
|
|
|
|
|
EXPECT_EQ(ResolveSkyboxCubemapTexture(&panoramicMaterial), nullptr);
|
|
|
|
|
EXPECT_EQ(ResolveSkyboxTextureMode(&panoramicMaterial), BuiltinSkyboxTextureMode::Panoramic);
|
|
|
|
|
|
|
|
|
|
auto* cubemapShader = new Shader();
|
|
|
|
|
ShaderPropertyDesc cubemapProperty = {};
|
|
|
|
|
cubemapProperty.name = "EnvironmentCube";
|
|
|
|
|
cubemapProperty.type = ShaderPropertyType::TextureCube;
|
|
|
|
|
cubemapProperty.semantic = "SkyboxTexture";
|
|
|
|
|
cubemapShader->AddProperty(cubemapProperty);
|
|
|
|
|
|
|
|
|
|
Material cubemapMaterial;
|
|
|
|
|
cubemapMaterial.SetShader(ResourceHandle<Shader>(cubemapShader));
|
|
|
|
|
Texture* cubemapTexture = CreateTestTexture("Textures/sky_cube.texture", TextureType::TextureCube);
|
|
|
|
|
cubemapMaterial.SetTexture("EnvironmentCube", ResourceHandle<Texture>(cubemapTexture));
|
|
|
|
|
|
|
|
|
|
EXPECT_EQ(ResolveSkyboxPanoramicTexture(&cubemapMaterial), nullptr);
|
|
|
|
|
EXPECT_EQ(ResolveSkyboxCubemapTexture(&cubemapMaterial), cubemapTexture);
|
|
|
|
|
EXPECT_EQ(ResolveSkyboxTextureMode(&cubemapMaterial), BuiltinSkyboxTextureMode::Cubemap);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(RenderMaterialUtility_Test, SkyboxMaterialDoesNotUseLegacyPropertyNameFallbacksWithoutSemanticMetadata) {
|
|
|
|
|
Material material;
|
|
|
|
|
material.SetFloat4("_Tint", Vector4(0.8f, 0.7f, 0.6f, 1.0f));
|
|
|
|
|
material.SetFloat("_Exposure", 2.0f);
|
|
|
|
|
material.SetFloat("_Rotation", 45.0f);
|
|
|
|
|
|
|
|
|
|
Texture* panoramicTexture = CreateTestTexture("Textures/legacy_panorama.texture", TextureType::Texture2D);
|
|
|
|
|
Texture* cubemapTexture = CreateTestTexture("Textures/legacy_cube.texture", TextureType::TextureCube);
|
|
|
|
|
material.SetTexture("_MainTex", ResourceHandle<Texture>(panoramicTexture));
|
|
|
|
|
material.SetTexture("_Tex", ResourceHandle<Texture>(cubemapTexture));
|
|
|
|
|
|
|
|
|
|
EXPECT_EQ(ResolveSkyboxTint(&material), Vector4::One());
|
|
|
|
|
EXPECT_FLOAT_EQ(ResolveSkyboxExposure(&material), 1.0f);
|
|
|
|
|
EXPECT_FLOAT_EQ(ResolveSkyboxRotationDegrees(&material), 0.0f);
|
|
|
|
|
EXPECT_EQ(ResolveSkyboxPanoramicTexture(&material), nullptr);
|
|
|
|
|
EXPECT_EQ(ResolveSkyboxCubemapTexture(&material), nullptr);
|
|
|
|
|
EXPECT_EQ(ResolveSkyboxTextureMode(&material), BuiltinSkyboxTextureMode::None);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 16:49:30 +08:00
|
|
|
TEST(RenderMaterialUtility_Test, ExposesSchemaDrivenMaterialConstantPayload) {
|
|
|
|
|
auto* shader = new Shader();
|
|
|
|
|
|
|
|
|
|
ShaderPropertyDesc colorProperty = {};
|
|
|
|
|
colorProperty.name = "_BaseColor";
|
|
|
|
|
colorProperty.type = ShaderPropertyType::Color;
|
|
|
|
|
colorProperty.defaultValue = "(0.25,0.5,0.75,1.0)";
|
|
|
|
|
colorProperty.semantic = "BaseColor";
|
|
|
|
|
shader->AddProperty(colorProperty);
|
|
|
|
|
|
2026-04-06 21:05:50 +08:00
|
|
|
ShaderPropertyDesc cutoffProperty = {};
|
|
|
|
|
cutoffProperty.name = "_Cutoff";
|
|
|
|
|
cutoffProperty.type = ShaderPropertyType::Range;
|
|
|
|
|
cutoffProperty.defaultValue = "0.6";
|
|
|
|
|
cutoffProperty.semantic = "AlphaCutoff";
|
|
|
|
|
shader->AddProperty(cutoffProperty);
|
|
|
|
|
|
2026-04-03 16:49:30 +08:00
|
|
|
Material material;
|
|
|
|
|
material.SetShader(ResourceHandle<Shader>(shader));
|
|
|
|
|
|
|
|
|
|
const MaterialConstantPayloadView payload = ResolveSchemaMaterialConstantPayload(&material);
|
|
|
|
|
ASSERT_TRUE(payload.IsValid());
|
2026-04-06 21:05:50 +08:00
|
|
|
ASSERT_EQ(payload.size, 32u);
|
2026-04-03 16:49:30 +08:00
|
|
|
ASSERT_TRUE(payload.layout.IsValid());
|
2026-04-06 21:05:50 +08:00
|
|
|
ASSERT_EQ(payload.layout.count, 2u);
|
|
|
|
|
EXPECT_EQ(payload.layout.size, 32u);
|
2026-04-03 16:49:30 +08:00
|
|
|
EXPECT_EQ(payload.layout.fields[0].name, "_BaseColor");
|
|
|
|
|
EXPECT_EQ(payload.layout.fields[0].offset, 0u);
|
|
|
|
|
EXPECT_EQ(payload.layout.fields[0].size, 16u);
|
|
|
|
|
EXPECT_EQ(payload.layout.fields[0].alignedSize, 16u);
|
2026-04-06 21:05:50 +08:00
|
|
|
EXPECT_EQ(payload.layout.fields[1].name, "_Cutoff");
|
|
|
|
|
EXPECT_EQ(payload.layout.fields[1].offset, 16u);
|
|
|
|
|
EXPECT_EQ(payload.layout.fields[1].size, 4u);
|
|
|
|
|
EXPECT_EQ(payload.layout.fields[1].alignedSize, 16u);
|
2026-04-03 16:49:30 +08:00
|
|
|
|
|
|
|
|
const float* values = static_cast<const float*>(payload.data);
|
|
|
|
|
EXPECT_FLOAT_EQ(values[0], 0.25f);
|
|
|
|
|
EXPECT_FLOAT_EQ(values[1], 0.5f);
|
|
|
|
|
EXPECT_FLOAT_EQ(values[2], 0.75f);
|
|
|
|
|
EXPECT_FLOAT_EQ(values[3], 1.0f);
|
2026-04-06 21:05:50 +08:00
|
|
|
const float* cutoffValues = static_cast<const float*>(payload.data) + 4;
|
|
|
|
|
EXPECT_FLOAT_EQ(cutoffValues[0], 0.6f);
|
2026-04-03 16:49:30 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 21:49:53 +08:00
|
|
|
TEST(RenderMaterialUtility_Test, BuildsBuiltinDepthStylePayloadFromMaterialSemantics) {
|
|
|
|
|
auto* shader = new Shader();
|
|
|
|
|
|
|
|
|
|
ShaderPropertyDesc colorProperty = {};
|
|
|
|
|
colorProperty.name = "_BaseColor";
|
|
|
|
|
colorProperty.type = ShaderPropertyType::Color;
|
|
|
|
|
colorProperty.defaultValue = "(1,1,1,1)";
|
|
|
|
|
colorProperty.semantic = "BaseColor";
|
|
|
|
|
shader->AddProperty(colorProperty);
|
|
|
|
|
|
|
|
|
|
ShaderPropertyDesc cutoffProperty = {};
|
|
|
|
|
cutoffProperty.name = "_Cutoff";
|
|
|
|
|
cutoffProperty.type = ShaderPropertyType::Range;
|
|
|
|
|
cutoffProperty.defaultValue = "0.5";
|
|
|
|
|
cutoffProperty.semantic = "AlphaCutoff";
|
|
|
|
|
shader->AddProperty(cutoffProperty);
|
|
|
|
|
|
|
|
|
|
Material material;
|
|
|
|
|
material.SetShader(ResourceHandle<Shader>(shader));
|
|
|
|
|
material.SetFloat4("_BaseColor", Vector4(0.2f, 0.4f, 0.6f, 0.8f));
|
|
|
|
|
material.SetFloat("_Cutoff", 0.35f);
|
|
|
|
|
|
|
|
|
|
BuiltinDepthStyleMaterialConstants constants = {};
|
|
|
|
|
MaterialConstantFieldDesc layout[2] = {};
|
|
|
|
|
const MaterialConstantPayloadView payload =
|
|
|
|
|
ResolveBuiltinDepthStyleMaterialConstantPayload(&material, constants, layout);
|
|
|
|
|
|
|
|
|
|
ASSERT_TRUE(payload.IsValid());
|
|
|
|
|
EXPECT_EQ(payload.size, sizeof(BuiltinDepthStyleMaterialConstants));
|
|
|
|
|
ASSERT_TRUE(payload.layout.IsValid());
|
|
|
|
|
EXPECT_EQ(payload.layout.count, 2u);
|
|
|
|
|
EXPECT_EQ(payload.layout.fields[0].name, "gBaseColorFactor");
|
|
|
|
|
EXPECT_EQ(payload.layout.fields[1].name, "gAlphaCutoffParams");
|
|
|
|
|
|
|
|
|
|
const auto* typedConstants = static_cast<const BuiltinDepthStyleMaterialConstants*>(payload.data);
|
|
|
|
|
ASSERT_NE(typedConstants, nullptr);
|
|
|
|
|
EXPECT_EQ(typedConstants->baseColorFactor, Vector4(0.2f, 0.4f, 0.6f, 0.8f));
|
|
|
|
|
EXPECT_EQ(typedConstants->alphaCutoffParams, Vector4(0.35f, 0.0f, 0.0f, 0.0f));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(RenderMaterialUtility_Test, BuildsBuiltinDepthStylePayloadDefaultsWithoutMaterial) {
|
|
|
|
|
BuiltinDepthStyleMaterialConstants constants = {};
|
|
|
|
|
MaterialConstantFieldDesc layout[2] = {};
|
|
|
|
|
const MaterialConstantPayloadView payload =
|
|
|
|
|
ResolveBuiltinDepthStyleMaterialConstantPayload(nullptr, constants, layout);
|
|
|
|
|
|
|
|
|
|
ASSERT_TRUE(payload.IsValid());
|
|
|
|
|
EXPECT_EQ(payload.size, sizeof(BuiltinDepthStyleMaterialConstants));
|
|
|
|
|
|
|
|
|
|
const auto* typedConstants = static_cast<const BuiltinDepthStyleMaterialConstants*>(payload.data);
|
|
|
|
|
ASSERT_NE(typedConstants, nullptr);
|
|
|
|
|
EXPECT_EQ(typedConstants->baseColorFactor, Vector4::One());
|
|
|
|
|
EXPECT_EQ(typedConstants->alphaCutoffParams, Vector4(0.5f, 0.0f, 0.0f, 0.0f));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 04:27:21 +08:00
|
|
|
TEST(RenderMaterialUtility_Test, DoesNotUseOpacityFallbackWithoutFormalShaderSemanticMetadata) {
|
2026-04-02 17:13:53 +08:00
|
|
|
Material material;
|
|
|
|
|
material.SetFloat("opacity", 0.35f);
|
2026-04-08 04:27:21 +08:00
|
|
|
EXPECT_EQ(ResolveBuiltinBaseColorFactor(&material), Vector4(1.0f, 1.0f, 1.0f, 1.0f));
|
2026-04-02 17:13:53 +08:00
|
|
|
|
|
|
|
|
material.SetFloat4("baseColor", Vector4(0.9f, 0.8f, 0.7f, 0.6f));
|
2026-04-08 04:27:21 +08:00
|
|
|
EXPECT_EQ(ResolveBuiltinBaseColorFactor(&material), Vector4(1.0f, 1.0f, 1.0f, 1.0f));
|
2026-04-02 17:13:53 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 19:18:07 +08:00
|
|
|
TEST(RenderMaterialUtility_Test, ResolvesRuntimeStructuredBufferIntoBufferViewMetadata) {
|
|
|
|
|
Material material;
|
|
|
|
|
MockRenderBuffer buffer(1024u, 32u);
|
|
|
|
|
MaterialBufferBindingViewDesc viewDesc = {};
|
|
|
|
|
viewDesc.firstElement = 2u;
|
|
|
|
|
viewDesc.elementCount = 6u;
|
|
|
|
|
material.SetBuffer("VolumeNodes", &buffer, viewDesc);
|
|
|
|
|
|
|
|
|
|
BuiltinPassResourceBindingDesc binding = {};
|
|
|
|
|
binding.name = "VolumeNodes";
|
|
|
|
|
binding.semantic = BuiltinPassResourceSemantic::MaterialBuffer;
|
|
|
|
|
binding.resourceType = ShaderResourceType::StructuredBuffer;
|
|
|
|
|
binding.location = { 2u, 1u };
|
|
|
|
|
|
|
|
|
|
MaterialBufferResourceView resolvedView = {};
|
|
|
|
|
ASSERT_TRUE(TryResolveMaterialBufferResourceView(&material, binding, resolvedView));
|
|
|
|
|
EXPECT_EQ(resolvedView.buffer, &buffer);
|
|
|
|
|
EXPECT_EQ(resolvedView.viewType, XCEngine::RHI::ResourceViewType::ShaderResource);
|
|
|
|
|
EXPECT_EQ(resolvedView.viewDesc.dimension, XCEngine::RHI::ResourceViewDimension::StructuredBuffer);
|
|
|
|
|
EXPECT_EQ(resolvedView.viewDesc.firstElement, 2u);
|
|
|
|
|
EXPECT_EQ(resolvedView.viewDesc.elementCount, 6u);
|
|
|
|
|
EXPECT_EQ(resolvedView.viewDesc.structureByteStride, 32u);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 12:18:04 +08:00
|
|
|
TEST(RenderMaterialUtility_Test, MapsMaterialRenderStateToRhiDescriptors) {
|
|
|
|
|
Material material;
|
|
|
|
|
MaterialRenderState renderState;
|
|
|
|
|
renderState.cullMode = MaterialCullMode::Back;
|
|
|
|
|
renderState.blendEnable = true;
|
|
|
|
|
renderState.srcBlend = MaterialBlendFactor::SrcAlpha;
|
|
|
|
|
renderState.dstBlend = MaterialBlendFactor::InvSrcAlpha;
|
|
|
|
|
renderState.srcBlendAlpha = MaterialBlendFactor::One;
|
|
|
|
|
renderState.dstBlendAlpha = MaterialBlendFactor::InvSrcAlpha;
|
|
|
|
|
renderState.blendOp = MaterialBlendOp::Add;
|
|
|
|
|
renderState.blendOpAlpha = MaterialBlendOp::Subtract;
|
|
|
|
|
renderState.colorWriteMask = 0x7;
|
|
|
|
|
renderState.depthTestEnable = true;
|
|
|
|
|
renderState.depthWriteEnable = false;
|
|
|
|
|
renderState.depthFunc = MaterialComparisonFunc::LessEqual;
|
2026-04-08 04:27:21 +08:00
|
|
|
renderState.depthBiasFactor = 1.25f;
|
|
|
|
|
renderState.depthBiasUnits = 4;
|
|
|
|
|
renderState.stencil.enabled = true;
|
|
|
|
|
renderState.stencil.reference = 9;
|
|
|
|
|
renderState.stencil.readMask = 0x3F;
|
|
|
|
|
renderState.stencil.writeMask = 0x1F;
|
|
|
|
|
renderState.stencil.front.func = MaterialComparisonFunc::Equal;
|
|
|
|
|
renderState.stencil.front.failOp = MaterialStencilOp::Replace;
|
|
|
|
|
renderState.stencil.front.passOp = MaterialStencilOp::IncrWrap;
|
|
|
|
|
renderState.stencil.front.depthFailOp = MaterialStencilOp::DecrSat;
|
|
|
|
|
renderState.stencil.back.func = MaterialComparisonFunc::NotEqual;
|
|
|
|
|
renderState.stencil.back.failOp = MaterialStencilOp::Invert;
|
|
|
|
|
renderState.stencil.back.passOp = MaterialStencilOp::DecrWrap;
|
|
|
|
|
renderState.stencil.back.depthFailOp = MaterialStencilOp::Zero;
|
2026-03-27 12:18:04 +08:00
|
|
|
material.SetRenderState(renderState);
|
|
|
|
|
|
2026-04-07 09:49:21 +08:00
|
|
|
const MaterialRenderState effectiveRenderState = ResolveEffectiveRenderState(nullptr, &material);
|
|
|
|
|
const XCEngine::RHI::RasterizerDesc rasterizerState = BuildRasterizerState(effectiveRenderState);
|
|
|
|
|
const XCEngine::RHI::BlendDesc blendState = BuildBlendState(effectiveRenderState);
|
|
|
|
|
const XCEngine::RHI::DepthStencilStateDesc depthStencilState = BuildDepthStencilState(effectiveRenderState);
|
2026-03-27 12:18:04 +08:00
|
|
|
|
|
|
|
|
EXPECT_EQ(rasterizerState.cullMode, static_cast<uint32_t>(XCEngine::RHI::CullMode::Back));
|
|
|
|
|
EXPECT_EQ(rasterizerState.frontFace, static_cast<uint32_t>(XCEngine::RHI::FrontFace::CounterClockwise));
|
2026-04-08 04:27:21 +08:00
|
|
|
EXPECT_FLOAT_EQ(rasterizerState.slopeScaledDepthBias, 1.25f);
|
|
|
|
|
EXPECT_EQ(rasterizerState.depthBias, 4);
|
2026-03-27 12:18:04 +08:00
|
|
|
|
|
|
|
|
EXPECT_TRUE(blendState.blendEnable);
|
|
|
|
|
EXPECT_EQ(blendState.srcBlend, static_cast<uint32_t>(XCEngine::RHI::BlendFactor::SrcAlpha));
|
|
|
|
|
EXPECT_EQ(blendState.dstBlend, static_cast<uint32_t>(XCEngine::RHI::BlendFactor::InvSrcAlpha));
|
|
|
|
|
EXPECT_EQ(blendState.srcBlendAlpha, static_cast<uint32_t>(XCEngine::RHI::BlendFactor::One));
|
|
|
|
|
EXPECT_EQ(blendState.dstBlendAlpha, static_cast<uint32_t>(XCEngine::RHI::BlendFactor::InvSrcAlpha));
|
|
|
|
|
EXPECT_EQ(blendState.blendOp, static_cast<uint32_t>(XCEngine::RHI::BlendOp::Add));
|
|
|
|
|
EXPECT_EQ(blendState.blendOpAlpha, static_cast<uint32_t>(XCEngine::RHI::BlendOp::Subtract));
|
|
|
|
|
EXPECT_EQ(blendState.colorWriteMask, 0x7);
|
|
|
|
|
|
|
|
|
|
EXPECT_TRUE(depthStencilState.depthTestEnable);
|
|
|
|
|
EXPECT_FALSE(depthStencilState.depthWriteEnable);
|
|
|
|
|
EXPECT_EQ(depthStencilState.depthFunc, static_cast<uint32_t>(XCEngine::RHI::ComparisonFunc::LessEqual));
|
2026-04-08 04:27:21 +08:00
|
|
|
EXPECT_TRUE(depthStencilState.stencilEnable);
|
|
|
|
|
EXPECT_EQ(depthStencilState.stencilReadMask, 0x3F);
|
|
|
|
|
EXPECT_EQ(depthStencilState.stencilWriteMask, 0x1F);
|
|
|
|
|
EXPECT_EQ(depthStencilState.front.func, static_cast<uint32_t>(XCEngine::RHI::ComparisonFunc::Equal));
|
|
|
|
|
EXPECT_EQ(depthStencilState.front.failOp, static_cast<uint32_t>(XCEngine::RHI::StencilOp::Replace));
|
|
|
|
|
EXPECT_EQ(depthStencilState.front.passOp, static_cast<uint32_t>(XCEngine::RHI::StencilOp::Incr));
|
|
|
|
|
EXPECT_EQ(depthStencilState.front.depthFailOp, static_cast<uint32_t>(XCEngine::RHI::StencilOp::DecrSat));
|
|
|
|
|
EXPECT_EQ(depthStencilState.back.func, static_cast<uint32_t>(XCEngine::RHI::ComparisonFunc::NotEqual));
|
|
|
|
|
EXPECT_EQ(depthStencilState.back.failOp, static_cast<uint32_t>(XCEngine::RHI::StencilOp::Invert));
|
|
|
|
|
EXPECT_EQ(depthStencilState.back.passOp, static_cast<uint32_t>(XCEngine::RHI::StencilOp::Decr));
|
|
|
|
|
EXPECT_EQ(depthStencilState.back.depthFailOp, static_cast<uint32_t>(XCEngine::RHI::StencilOp::Zero));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(RenderMaterialUtility_Test, PipelineStateKeyStripsDynamicStencilReference) {
|
|
|
|
|
MaterialRenderState renderState = {};
|
|
|
|
|
renderState.stencil.enabled = true;
|
|
|
|
|
renderState.stencil.reference = 12;
|
|
|
|
|
renderState.stencil.front.func = MaterialComparisonFunc::Always;
|
|
|
|
|
renderState.stencil.back.func = MaterialComparisonFunc::Always;
|
|
|
|
|
|
|
|
|
|
const MaterialRenderState keyState = BuildStaticPipelineRenderStateKey(renderState);
|
|
|
|
|
EXPECT_TRUE(keyState.stencil.enabled);
|
|
|
|
|
EXPECT_EQ(keyState.stencil.reference, 0u);
|
|
|
|
|
EXPECT_EQ(keyState.stencil.front.func, MaterialComparisonFunc::Always);
|
|
|
|
|
EXPECT_EQ(keyState.stencil.back.func, MaterialComparisonFunc::Always);
|
2026-03-27 12:18:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-07 09:49:21 +08:00
|
|
|
TEST(RenderMaterialUtility_Test, ShaderPassFixedFunctionStateIsUsedBeforeLegacyMaterialOverride) {
|
|
|
|
|
ShaderPass shaderPass = {};
|
|
|
|
|
shaderPass.name = "ForwardLit";
|
|
|
|
|
shaderPass.hasFixedFunctionState = true;
|
|
|
|
|
shaderPass.fixedFunctionState.cullMode = MaterialCullMode::Back;
|
|
|
|
|
shaderPass.fixedFunctionState.blendEnable = true;
|
|
|
|
|
shaderPass.fixedFunctionState.srcBlend = MaterialBlendFactor::SrcAlpha;
|
|
|
|
|
shaderPass.fixedFunctionState.dstBlend = MaterialBlendFactor::InvSrcAlpha;
|
|
|
|
|
shaderPass.fixedFunctionState.srcBlendAlpha = MaterialBlendFactor::SrcAlpha;
|
|
|
|
|
shaderPass.fixedFunctionState.dstBlendAlpha = MaterialBlendFactor::InvSrcAlpha;
|
|
|
|
|
shaderPass.fixedFunctionState.depthWriteEnable = false;
|
|
|
|
|
shaderPass.fixedFunctionState.depthFunc = MaterialComparisonFunc::LessEqual;
|
|
|
|
|
|
|
|
|
|
Material material;
|
|
|
|
|
MaterialRenderState effectiveState = ResolveEffectiveRenderState(&shaderPass, &material);
|
|
|
|
|
EXPECT_EQ(effectiveState.cullMode, MaterialCullMode::Back);
|
|
|
|
|
EXPECT_TRUE(effectiveState.blendEnable);
|
|
|
|
|
EXPECT_FALSE(effectiveState.depthWriteEnable);
|
|
|
|
|
EXPECT_EQ(effectiveState.depthFunc, MaterialComparisonFunc::LessEqual);
|
|
|
|
|
|
|
|
|
|
MaterialRenderState legacyOverride = {};
|
|
|
|
|
legacyOverride.cullMode = MaterialCullMode::Front;
|
|
|
|
|
legacyOverride.depthWriteEnable = true;
|
|
|
|
|
material.SetRenderState(legacyOverride);
|
|
|
|
|
|
|
|
|
|
effectiveState = ResolveEffectiveRenderState(&shaderPass, &material);
|
|
|
|
|
EXPECT_EQ(effectiveState.cullMode, MaterialCullMode::Front);
|
|
|
|
|
EXPECT_FALSE(effectiveState.blendEnable);
|
|
|
|
|
EXPECT_TRUE(effectiveState.depthWriteEnable);
|
|
|
|
|
EXPECT_EQ(effectiveState.depthFunc, MaterialComparisonFunc::Less);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 14:13:26 +08:00
|
|
|
TEST(RenderMaterialUtility_Test, ShaderPassQueueTagResolvesAuthoringRenderQueue) {
|
2026-04-07 09:49:21 +08:00
|
|
|
ShaderPass shaderPass = {};
|
|
|
|
|
shaderPass.tags.PushBack({ "Queue", "Transparent" });
|
|
|
|
|
|
|
|
|
|
int32 renderQueue = 0;
|
|
|
|
|
EXPECT_TRUE(TryResolveShaderPassRenderQueue(shaderPass, renderQueue));
|
|
|
|
|
EXPECT_EQ(renderQueue, static_cast<int32>(MaterialRenderQueue::Transparent));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 20:43:17 +08:00
|
|
|
} // namespace
|