Files
XCEngine/tests/Rendering/unit/test_render_scene_extractor.cpp

1311 lines
59 KiB
C++
Raw Normal View History

#include <gtest/gtest.h>
#include <XCEngine/Components/CameraComponent.h>
#include <XCEngine/Components/GaussianSplatRendererComponent.h>
#include <XCEngine/Components/GameObject.h>
#include <XCEngine/Components/LightComponent.h>
#include <XCEngine/Components/MeshFilterComponent.h>
#include <XCEngine/Components/MeshRendererComponent.h>
2026-04-08 20:12:14 +08:00
#include <XCEngine/Components/VolumeRendererComponent.h>
#include <XCEngine/Core/Asset/IResource.h>
#include <XCEngine/Core/Math/Color.h>
2026-04-08 20:12:14 +08:00
#include <XCEngine/Core/Math/Bounds.h>
#include <XCEngine/Core/Math/Quaternion.h>
#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>
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
#include <XCEngine/Resources/Material/Material.h>
#include <XCEngine/Resources/Mesh/Mesh.h>
#include <XCEngine/Resources/Shader/Shader.h>
#include <XCEngine/Resources/Texture/Texture.h>
2026-04-08 20:12:14 +08:00
#include <XCEngine/Resources/Volume/VolumeField.h>
#include <XCEngine/Scene/Scene.h>
#include <cstring>
2026-04-08 19:18:07 +08:00
#include <string>
using namespace XCEngine::Components;
using namespace XCEngine::Core;
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;
};
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;
}
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;
}
Material* CreateTestMaterial(const char* path, int32_t renderQueue) {
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;
}
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;
}
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;
}
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));
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));
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);
}
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);
mainLight->SetCastsShadows(true);
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);
EXPECT_TRUE(sceneData.lighting.mainDirectionalLight.castsShadows);
EXPECT_EQ(
sceneData.lighting.mainDirectionalLight.direction,
mainLightObject->GetTransform()->GetForward().Normalized() * -1.0f);
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 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;
}
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;
}
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",
static_cast<int32_t>(MaterialRenderQueue::Geometry));
Material* transparentMaterial = CreateTestMaterial(
"Materials/transparent.mat",
static_cast<int32_t>(MaterialRenderQueue::Transparent));
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);
Material* material = CreateTestMaterial(name, renderQueue);
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",
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;
}
TEST(RenderMaterialUtility_Test, NullMaterialsDoNotMatchBuiltinSurfacePasses) {
EXPECT_FALSE(MatchesBuiltinPass(nullptr, BuiltinMaterialPass::ForwardLit));
EXPECT_FALSE(MatchesBuiltinPass(nullptr, BuiltinMaterialPass::Unlit));
}
TEST(RenderMaterialUtility_Test, MaterialsWithoutShaderMetadataDoNotMatchBuiltinSurfacePasses) {
Material noMetadataMaterial;
EXPECT_FALSE(MatchesBuiltinPass(&noMetadataMaterial, BuiltinMaterialPass::ForwardLit));
EXPECT_FALSE(MatchesBuiltinPass(&noMetadataMaterial, BuiltinMaterialPass::Unlit));
}
TEST(RenderMaterialUtility_Test, ShaderPassMetadataSupportsUnlitDepthAndObjectId) {
Material unlitMaterial;
auto* unlitShader = new Shader();
ShaderPass unlitPass = {};
unlitPass.name = "Unlit";
unlitShader->AddPass(unlitPass);
unlitMaterial.SetShader(ResourceHandle<Shader>(unlitShader));
EXPECT_TRUE(MatchesBuiltinPass(&unlitMaterial, BuiltinMaterialPass::Unlit));
EXPECT_FALSE(MatchesBuiltinPass(&unlitMaterial, BuiltinMaterialPass::ForwardLit));
Material depthMaterial;
auto* depthShader = new Shader();
ShaderPass depthPass = {};
depthPass.name = "DepthOnly";
depthShader->AddPass(depthPass);
depthMaterial.SetShader(ResourceHandle<Shader>(depthShader));
EXPECT_TRUE(MatchesBuiltinPass(&depthMaterial, BuiltinMaterialPass::DepthOnly));
EXPECT_FALSE(MatchesBuiltinPass(&depthMaterial, BuiltinMaterialPass::Unlit));
Material objectIdMaterial;
auto* objectIdShader = new Shader();
ShaderPass objectIdPass = {};
objectIdPass.name = "ObjectId";
objectIdShader->AddPass(objectIdPass);
objectIdMaterial.SetShader(ResourceHandle<Shader>(objectIdShader));
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));
}
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));
}
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));
}
TEST(RenderMaterialUtility_Test, ShaderMetadataOverridesConflictingMaterialTags) {
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));
}
TEST(RenderMaterialUtility_Test, MaterialTagsDoNotOverrideMissingShaderMetadata) {
Material material;
auto* shader = new Shader();
ShaderPass defaultPass = {};
defaultPass.name = "Default";
shader->AddPass(defaultPass);
material.SetShader(ResourceHandle<Shader>(shader));
material.SetTag("LightMode", "ShadowCaster");
EXPECT_FALSE(MatchesBuiltinPass(&material, BuiltinMaterialPass::ForwardLit));
EXPECT_FALSE(MatchesBuiltinPass(&material, BuiltinMaterialPass::ShadowCaster));
}
TEST(RenderMaterialUtility_Test, ShadersWithoutBuiltinMetadataDoNotMatchBuiltinSurfacePasses) {
Material material;
auto* shader = new Shader();
ShaderPass defaultPass = {};
defaultPass.name = "Default";
shader->AddPass(defaultPass);
material.SetShader(ResourceHandle<Shader>(shader));
EXPECT_FALSE(MatchesBuiltinPass(&material, BuiltinMaterialPass::ForwardLit));
EXPECT_FALSE(MatchesBuiltinPass(&material, BuiltinMaterialPass::Unlit));
}
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);
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);
material.SetTexture("_BaseColorTexture", ResourceHandle<Texture>(baseColorTexture));
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());
}
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);
ShaderPropertyDesc cutoffProperty = {};
cutoffProperty.name = "AlphaClipThreshold";
cutoffProperty.displayName = "Alpha Clip";
cutoffProperty.type = ShaderPropertyType::Range;
cutoffProperty.defaultValue = "0.5";
cutoffProperty.semantic = "AlphaCutoff";
shader->AddProperty(cutoffProperty);
Material material;
material.SetShader(ResourceHandle<Shader>(shader));
material.SetFloat4("TintColor", Vector4(0.3f, 0.5f, 0.7f, 0.9f));
material.SetFloat("AlphaClipThreshold", 0.61f);
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);
EXPECT_FLOAT_EQ(ResolveBuiltinAlphaCutoff(&material), 0.61f);
}
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);
ShaderPropertyDesc cutoffProperty = {};
cutoffProperty.name = "_Cutoff";
cutoffProperty.type = ShaderPropertyType::Range;
cutoffProperty.defaultValue = "0.37";
cutoffProperty.semantic = "AlphaCutoff";
shader->AddProperty(cutoffProperty);
Material material;
material.SetShader(ResourceHandle<Shader>(shader));
EXPECT_EQ(ResolveBuiltinBaseColorFactor(&material), Vector4(0.11f, 0.22f, 0.33f, 0.44f));
EXPECT_FLOAT_EQ(ResolveBuiltinAlphaCutoff(&material), 0.37f);
EXPECT_EQ(ResolveBuiltinBaseColorTexture(&material), nullptr);
}
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);
}
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);
ShaderPropertyDesc cutoffProperty = {};
cutoffProperty.name = "_Cutoff";
cutoffProperty.type = ShaderPropertyType::Range;
cutoffProperty.defaultValue = "0.6";
cutoffProperty.semantic = "AlphaCutoff";
shader->AddProperty(cutoffProperty);
Material material;
material.SetShader(ResourceHandle<Shader>(shader));
const MaterialConstantPayloadView payload = ResolveSchemaMaterialConstantPayload(&material);
ASSERT_TRUE(payload.IsValid());
ASSERT_EQ(payload.size, 32u);
ASSERT_TRUE(payload.layout.IsValid());
ASSERT_EQ(payload.layout.count, 2u);
EXPECT_EQ(payload.layout.size, 32u);
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);
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);
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);
const float* cutoffValues = static_cast<const float*>(payload.data) + 4;
EXPECT_FLOAT_EQ(cutoffValues[0], 0.6f);
}
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));
}
TEST(RenderMaterialUtility_Test, DoesNotUseOpacityFallbackWithoutFormalShaderSemanticMetadata) {
Material material;
material.SetFloat("opacity", 0.35f);
EXPECT_EQ(ResolveBuiltinBaseColorFactor(&material), Vector4(1.0f, 1.0f, 1.0f, 1.0f));
material.SetFloat4("baseColor", Vector4(0.9f, 0.8f, 0.7f, 0.6f));
EXPECT_EQ(ResolveBuiltinBaseColorFactor(&material), Vector4(1.0f, 1.0f, 1.0f, 1.0f));
}
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);
}
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;
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;
material.SetRenderState(renderState);
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);
EXPECT_EQ(rasterizerState.cullMode, static_cast<uint32_t>(XCEngine::RHI::CullMode::Back));
EXPECT_EQ(rasterizerState.frontFace, static_cast<uint32_t>(XCEngine::RHI::FrontFace::CounterClockwise));
EXPECT_FLOAT_EQ(rasterizerState.slopeScaledDepthBias, 1.25f);
EXPECT_EQ(rasterizerState.depthBias, 4);
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));
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);
}
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);
}
TEST(RenderMaterialUtility_Test, ShaderPassQueueTagResolvesAuthoringRenderQueue) {
ShaderPass shaderPass = {};
shaderPass.tags.PushBack({ "Queue", "Transparent" });
int32 renderQueue = 0;
EXPECT_TRUE(TryResolveShaderPassRenderQueue(shaderPass, renderQueue));
EXPECT_EQ(renderQueue, static_cast<int32>(MaterialRenderQueue::Transparent));
}
} // namespace