From 42e2e1b8f23d72e63b38ccbd8757c159a611646a Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sat, 4 Apr 2026 01:00:36 +0800 Subject: [PATCH] Add unlit rendering integration coverage --- docs/plan/Shader与Material系统下一阶段计划.md | 11 + tests/Rendering/CMakeLists.txt | 1 + tests/Rendering/integration/CMakeLists.txt | 1 + .../integration/unlit_scene/CMakeLists.txt | 63 ++++ .../integration/unlit_scene/main.cpp | 325 ++++++++++++++++++ 5 files changed, 401 insertions(+) create mode 100644 tests/Rendering/integration/unlit_scene/CMakeLists.txt create mode 100644 tests/Rendering/integration/unlit_scene/main.cpp diff --git a/docs/plan/Shader与Material系统下一阶段计划.md b/docs/plan/Shader与Material系统下一阶段计划.md index 861cb48e..cff645fc 100644 --- a/docs/plan/Shader与Material系统下一阶段计划.md +++ b/docs/plan/Shader与Material系统下一阶段计划.md @@ -384,6 +384,17 @@ Unity-like Shader Authoring (.shader) - `rendering_integration_unlit_scene` - `rendering_integration_object_id_scene` +当前进展(`2026-04-04`): + +- 已完成:`rendering_integration_unlit_scene` + - 显式使用 builtin `unlit` shader + `Unlit` pass,覆盖 `BuiltinForwardPipeline` 的 `ForwardLit / Unlit` pass 选择路径 + - 复用 `textured_quad_scene` 构图做最小差异回归,验证 shader/material 新 contract 不改变既有画面输出 +- 已验证:`rendering_integration_unlit_scene` + - D3D12:通过 + - OpenGL:通过 + - Vulkan:通过 +- 下一步:补 `rendering_integration_object_id_scene` 或等价的 object-id integration coverage,完成本阶段收口 + ## 7. 当前阶段明确不做 下一阶段不应把范围扩散到下面这些方向: diff --git a/tests/Rendering/CMakeLists.txt b/tests/Rendering/CMakeLists.txt index 87393569..57aa2fe7 100644 --- a/tests/Rendering/CMakeLists.txt +++ b/tests/Rendering/CMakeLists.txt @@ -13,6 +13,7 @@ add_custom_target(rendering_unit_test_targets add_custom_target(rendering_integration_tests DEPENDS rendering_integration_textured_quad_scene + rendering_integration_unlit_scene rendering_integration_backpack_scene rendering_integration_backpack_lit_scene rendering_integration_camera_stack_scene diff --git a/tests/Rendering/integration/CMakeLists.txt b/tests/Rendering/integration/CMakeLists.txt index c4cb8c30..236a2a6d 100644 --- a/tests/Rendering/integration/CMakeLists.txt +++ b/tests/Rendering/integration/CMakeLists.txt @@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.15) project(XCEngine_RenderingIntegrationTests) add_subdirectory(textured_quad_scene) +add_subdirectory(unlit_scene) add_subdirectory(backpack_scene) add_subdirectory(backpack_lit_scene) add_subdirectory(camera_stack_scene) diff --git a/tests/Rendering/integration/unlit_scene/CMakeLists.txt b/tests/Rendering/integration/unlit_scene/CMakeLists.txt new file mode 100644 index 00000000..67f46288 --- /dev/null +++ b/tests/Rendering/integration/unlit_scene/CMakeLists.txt @@ -0,0 +1,63 @@ +cmake_minimum_required(VERSION 3.15) + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + +project(rendering_integration_unlit_scene) + +set(ENGINE_ROOT_DIR ${CMAKE_SOURCE_DIR}/engine) +set(PACKAGE_DIR ${CMAKE_SOURCE_DIR}/mvs/OpenGL/package) + +get_filename_component(PROJECT_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../.. ABSOLUTE) + +find_package(Vulkan QUIET) + +add_executable(rendering_integration_unlit_scene + main.cpp + ${CMAKE_SOURCE_DIR}/tests/RHI/integration/fixtures/RHIIntegrationFixture.cpp + ${PACKAGE_DIR}/src/glad.c +) + +target_include_directories(rendering_integration_unlit_scene PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/tests/RHI/integration/fixtures + ${ENGINE_ROOT_DIR}/include + ${PACKAGE_DIR}/include + ${PROJECT_ROOT_DIR}/engine/src +) + +target_link_libraries(rendering_integration_unlit_scene PRIVATE + d3d12 + dxgi + d3dcompiler + winmm + opengl32 + XCEngine + GTest::gtest +) + +if(TARGET Vulkan::Vulkan) + target_link_libraries(rendering_integration_unlit_scene PRIVATE Vulkan::Vulkan) + target_compile_definitions(rendering_integration_unlit_scene PRIVATE XCENGINE_SUPPORT_VULKAN) +endif() + +target_compile_definitions(rendering_integration_unlit_scene PRIVATE + UNICODE + _UNICODE + XCENGINE_SUPPORT_OPENGL + XCENGINE_SUPPORT_D3D12 +) + +add_custom_command(TARGET rendering_integration_unlit_scene POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_SOURCE_DIR}/tests/RHI/integration/compare_ppm.py + $/ + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_SOURCE_DIR}/tests/Rendering/integration/textured_quad_scene/GT.ppm + $/GT.ppm + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${ENGINE_ROOT_DIR}/third_party/renderdoc/renderdoc.dll + $/ +) + +include(GoogleTest) +gtest_discover_tests(rendering_integration_unlit_scene) diff --git a/tests/Rendering/integration/unlit_scene/main.cpp b/tests/Rendering/integration/unlit_scene/main.cpp new file mode 100644 index 00000000..899be514 --- /dev/null +++ b/tests/Rendering/integration/unlit_scene/main.cpp @@ -0,0 +1,325 @@ +#include + +#include "../RenderingIntegrationMain.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../../RHI/integration/fixtures/RHIIntegrationFixture.h" + +#include +#include +#include + +using namespace XCEngine::Components; +using namespace XCEngine::Debug; +using namespace XCEngine::Math; +using namespace XCEngine::Rendering; +using namespace XCEngine::Resources; +using namespace XCEngine::RHI; +using namespace XCEngine::RHI::Integration; + +namespace { + +constexpr const char* kD3D12Screenshot = "unlit_scene_d3d12.ppm"; +constexpr const char* kOpenGLScreenshot = "unlit_scene_opengl.ppm"; +constexpr const char* kVulkanScreenshot = "unlit_scene_vulkan.ppm"; +constexpr uint32_t kFrameWidth = 1280; +constexpr uint32_t kFrameHeight = 720; + +Mesh* CreateQuadMesh() { + auto* mesh = new Mesh(); + IResource::ConstructParams params = {}; + params.name = "UnlitQuad"; + params.path = "Tests/Rendering/UnlitQuad.mesh"; + params.guid = ResourceGUID::Generate(params.path); + mesh->Initialize(params); + + StaticMeshVertex vertices[4] = {}; + vertices[0].position = Vector3(-1.0f, -1.0f, 0.0f); + vertices[0].uv0 = Vector2(0.0f, 1.0f); + vertices[1].position = Vector3(-1.0f, 1.0f, 0.0f); + vertices[1].uv0 = Vector2(0.0f, 0.0f); + vertices[2].position = Vector3(1.0f, -1.0f, 0.0f); + vertices[2].uv0 = Vector2(1.0f, 1.0f); + vertices[3].position = Vector3(1.0f, 1.0f, 0.0f); + vertices[3].uv0 = Vector2(1.0f, 0.0f); + + const uint32_t indices[6] = { 0, 1, 2, 2, 1, 3 }; + mesh->SetVertexData( + vertices, + sizeof(vertices), + 4, + sizeof(StaticMeshVertex), + VertexAttribute::Position | VertexAttribute::UV0); + mesh->SetIndexData(indices, sizeof(indices), 6, true); + + MeshSection section = {}; + section.baseVertex = 0; + section.vertexCount = 4; + section.startIndex = 0; + section.indexCount = 6; + section.materialID = 0; + mesh->AddSection(section); + + return mesh; +} + +Texture* CreateCheckerTexture() { + auto* texture = new Texture(); + IResource::ConstructParams params = {}; + params.name = "Checker"; + params.path = "Tests/Rendering/Checker.texture"; + params.guid = ResourceGUID::Generate(params.path); + texture->Initialize(params); + + const unsigned char pixels[16] = { + 255, 32, 32, 255, + 32, 255, 32, 255, + 32, 32, 255, 255, + 255, 255, 32, 255 + }; + texture->Create( + 2, + 2, + 1, + 1, + XCEngine::Resources::TextureType::Texture2D, + XCEngine::Resources::TextureFormat::RGBA8_UNORM, + pixels, + sizeof(pixels)); + return texture; +} + +Material* CreateMaterial(Texture* texture) { + auto* material = new Material(); + IResource::ConstructParams params = {}; + params.name = "QuadUnlitMaterial"; + params.path = "Tests/Rendering/QuadUnlit.material"; + params.guid = ResourceGUID::Generate(params.path); + material->Initialize(params); + material->SetShader(ResourceManager::Get().Load(GetBuiltinUnlitShaderPath())); + material->SetShaderPass("Unlit"); + material->SetTexture("_MainTex", ResourceHandle(texture)); + return material; +} + +const char* GetScreenshotFilename(RHIType backendType) { + switch (backendType) { + case RHIType::D3D12: + return kD3D12Screenshot; + case RHIType::Vulkan: + return kVulkanScreenshot; + case RHIType::OpenGL: + default: + return kOpenGLScreenshot; + } +} + +int GetComparisonThreshold(RHIType backendType) { + return backendType == RHIType::D3D12 ? 0 : 5; +} + +class UnlitSceneTest : public RHIIntegrationFixture { +protected: + void SetUp() override; + void TearDown() override; + void RenderFrame() override; + +private: + RHIResourceView* GetCurrentBackBufferView(); + + std::unique_ptr mScene; + std::unique_ptr mSceneRenderer; + std::vector mBackBufferViews; + RHITexture* mDepthTexture = nullptr; + RHIResourceView* mDepthView = nullptr; + Mesh* mMesh = nullptr; + Material* mMaterial = nullptr; + Texture* mTexture = nullptr; +}; + +void UnlitSceneTest::SetUp() { + RHIIntegrationFixture::SetUp(); + + mSceneRenderer = std::make_unique(); + mScene = std::make_unique("UnlitScene"); + + mMesh = CreateQuadMesh(); + mTexture = CreateCheckerTexture(); + mMaterial = CreateMaterial(mTexture); + + GameObject* cameraObject = mScene->CreateGameObject("MainCamera"); + auto* camera = cameraObject->AddComponent(); + camera->SetPrimary(true); + camera->SetClearColor(XCEngine::Math::Color(0.08f, 0.08f, 0.10f, 1.0f)); + + GameObject* quadObject = mScene->CreateGameObject("Quad"); + quadObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, 3.0f)); + auto* meshFilter = quadObject->AddComponent(); + auto* meshRenderer = quadObject->AddComponent(); + meshFilter->SetMesh(ResourceHandle(mMesh)); + meshRenderer->SetMaterial(0, ResourceHandle(mMaterial)); + + TextureDesc depthDesc = {}; + depthDesc.width = kFrameWidth; + depthDesc.height = kFrameHeight; + depthDesc.depth = 1; + depthDesc.mipLevels = 1; + depthDesc.arraySize = 1; + depthDesc.format = static_cast(Format::D24_UNorm_S8_UInt); + depthDesc.textureType = static_cast(XCEngine::RHI::TextureType::Texture2D); + depthDesc.sampleCount = 1; + depthDesc.sampleQuality = 0; + depthDesc.flags = 0; + mDepthTexture = GetDevice()->CreateTexture(depthDesc); + ASSERT_NE(mDepthTexture, nullptr); + + ResourceViewDesc depthViewDesc = {}; + depthViewDesc.format = static_cast(Format::D24_UNorm_S8_UInt); + depthViewDesc.dimension = ResourceViewDimension::Texture2D; + depthViewDesc.mipLevel = 0; + mDepthView = GetDevice()->CreateDepthStencilView(mDepthTexture, depthViewDesc); + ASSERT_NE(mDepthView, nullptr); + + mBackBufferViews.resize(2, nullptr); +} + +void UnlitSceneTest::TearDown() { + mSceneRenderer.reset(); + + if (mDepthView != nullptr) { + mDepthView->Shutdown(); + delete mDepthView; + mDepthView = nullptr; + } + + if (mDepthTexture != nullptr) { + mDepthTexture->Shutdown(); + delete mDepthTexture; + mDepthTexture = nullptr; + } + + for (RHIResourceView*& backBufferView : mBackBufferViews) { + if (backBufferView != nullptr) { + backBufferView->Shutdown(); + delete backBufferView; + backBufferView = nullptr; + } + } + mBackBufferViews.clear(); + + mScene.reset(); + + delete mMaterial; + mMaterial = nullptr; + delete mMesh; + mMesh = nullptr; + delete mTexture; + mTexture = nullptr; + + RHIIntegrationFixture::TearDown(); +} + +RHIResourceView* UnlitSceneTest::GetCurrentBackBufferView() { + const int backBufferIndex = GetCurrentBackBufferIndex(); + if (backBufferIndex < 0) { + return nullptr; + } + + if (static_cast(backBufferIndex) >= mBackBufferViews.size()) { + mBackBufferViews.resize(static_cast(backBufferIndex) + 1, nullptr); + } + + if (mBackBufferViews[backBufferIndex] == nullptr) { + ResourceViewDesc viewDesc = {}; + viewDesc.format = static_cast(Format::R8G8B8A8_UNorm); + viewDesc.dimension = ResourceViewDimension::Texture2D; + viewDesc.mipLevel = 0; + mBackBufferViews[backBufferIndex] = GetDevice()->CreateRenderTargetView(GetCurrentBackBuffer(), viewDesc); + } + + return mBackBufferViews[backBufferIndex]; +} + +void UnlitSceneTest::RenderFrame() { + ASSERT_NE(mScene, nullptr); + ASSERT_NE(mSceneRenderer, nullptr); + + RHICommandList* commandList = GetCommandList(); + ASSERT_NE(commandList, nullptr); + + commandList->Reset(); + + RenderSurface surface(kFrameWidth, kFrameHeight); + surface.SetColorAttachment(GetCurrentBackBufferView()); + surface.SetDepthAttachment(mDepthView); + + RenderContext renderContext = {}; + renderContext.device = GetDevice(); + renderContext.commandList = commandList; + renderContext.commandQueue = GetCommandQueue(); + renderContext.backendType = GetBackendType(); + + ASSERT_TRUE(mSceneRenderer->Render(*mScene, nullptr, renderContext, surface)); + + commandList->Close(); + void* commandLists[] = { commandList }; + GetCommandQueue()->ExecuteCommandLists(1, commandLists); +} + +TEST_P(UnlitSceneTest, RenderUnlitScene) { + RHICommandQueue* commandQueue = GetCommandQueue(); + RHISwapChain* swapChain = GetSwapChain(); + const int targetFrameCount = 30; + const char* screenshotFilename = GetScreenshotFilename(GetBackendType()); + const int comparisonThreshold = GetComparisonThreshold(GetBackendType()); + + for (int frameCount = 0; frameCount <= targetFrameCount; ++frameCount) { + if (frameCount > 0) { + commandQueue->WaitForPreviousFrame(); + } + + BeginRender(); + RenderFrame(); + + if (frameCount >= targetFrameCount) { + commandQueue->WaitForIdle(); + ASSERT_TRUE(TakeScreenshot(screenshotFilename)); + ASSERT_TRUE(CompareWithGoldenTemplate(screenshotFilename, "GT.ppm", static_cast(comparisonThreshold))); + break; + } + + swapChain->Present(0, 0); + } +} + +} // namespace + +INSTANTIATE_TEST_SUITE_P(D3D12, UnlitSceneTest, ::testing::Values(RHIType::D3D12)); +INSTANTIATE_TEST_SUITE_P(OpenGL, UnlitSceneTest, ::testing::Values(RHIType::OpenGL)); +#if defined(XCENGINE_SUPPORT_VULKAN) +INSTANTIATE_TEST_SUITE_P(Vulkan, UnlitSceneTest, ::testing::Values(RHIType::Vulkan)); +#endif + +GTEST_API_ int main(int argc, char** argv) { + return RunRenderingIntegrationTestMain(argc, argv); +}