From 64212a53c720dea7ec07bd157115c99d1df4cf1f Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Mon, 13 Apr 2026 00:36:50 +0800 Subject: [PATCH] Add 3DGS D3D12 MVS bootstrap and PLY loader --- MVS/3DGS-D3D12/CMakeLists.txt | 71 +++ MVS/3DGS-D3D12/include/XC3DGSD3D12/App.h | 89 +++ .../include/XC3DGSD3D12/GaussianPlyLoader.h | 46 ++ MVS/3DGS-D3D12/src/App.cpp | 472 +++++++++++++++ MVS/3DGS-D3D12/src/GaussianPlyLoader.cpp | 572 ++++++++++++++++++ MVS/3DGS-D3D12/src/main.cpp | 39 ++ .../3DGS-D3D12最小可行系统计划_2026-04-12.md | 245 ++++++++ ...3DGS渲染路径对齐参考实现修复计划_过期归档_2026-04-12.md} | 0 8 files changed, 1534 insertions(+) create mode 100644 MVS/3DGS-D3D12/CMakeLists.txt create mode 100644 MVS/3DGS-D3D12/include/XC3DGSD3D12/App.h create mode 100644 MVS/3DGS-D3D12/include/XC3DGSD3D12/GaussianPlyLoader.h create mode 100644 MVS/3DGS-D3D12/src/App.cpp create mode 100644 MVS/3DGS-D3D12/src/GaussianPlyLoader.cpp create mode 100644 MVS/3DGS-D3D12/src/main.cpp create mode 100644 docs/plan/3DGS-D3D12最小可行系统计划_2026-04-12.md rename docs/{plan/3DGS渲染路径对齐参考实现修复计划_2026-04-11.md => used/3DGS渲染路径对齐参考实现修复计划_过期归档_2026-04-12.md} (100%) diff --git a/MVS/3DGS-D3D12/CMakeLists.txt b/MVS/3DGS-D3D12/CMakeLists.txt new file mode 100644 index 00000000..80f5e72d --- /dev/null +++ b/MVS/3DGS-D3D12/CMakeLists.txt @@ -0,0 +1,71 @@ +cmake_minimum_required(VERSION 3.20) + +project(XC3DGSD3D12MVS LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +get_filename_component(XCENGINE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE) +set(XCENGINE_BUILD_DIR "${XCENGINE_ROOT}/build") +set(XCENGINE_INCLUDE_DIR "${XCENGINE_ROOT}/engine/include") +set(XCENGINE_LIBRARY_DEBUG "${XCENGINE_BUILD_DIR}/engine/Debug/XCEngine.lib") + +if(NOT EXISTS "${XCENGINE_LIBRARY_DEBUG}") + message(FATAL_ERROR "Prebuilt XCEngine library was not found: ${XCENGINE_LIBRARY_DEBUG}") +endif() + +add_library(XCEngine STATIC IMPORTED GLOBAL) +set_target_properties(XCEngine PROPERTIES + IMPORTED_CONFIGURATIONS "Debug;Release;RelWithDebInfo;MinSizeRel" + IMPORTED_LOCATION_DEBUG "${XCENGINE_LIBRARY_DEBUG}" + IMPORTED_LOCATION_RELEASE "${XCENGINE_LIBRARY_DEBUG}" + IMPORTED_LOCATION_RELWITHDEBINFO "${XCENGINE_LIBRARY_DEBUG}" + IMPORTED_LOCATION_MINSIZEREL "${XCENGINE_LIBRARY_DEBUG}" +) + +add_executable(xc_3dgs_d3d12_mvs + WIN32 + src/main.cpp + src/App.cpp + src/GaussianPlyLoader.cpp + include/XC3DGSD3D12/App.h + include/XC3DGSD3D12/GaussianPlyLoader.h +) + +target_include_directories(xc_3dgs_d3d12_mvs PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${XCENGINE_INCLUDE_DIR} +) + +target_compile_definitions(xc_3dgs_d3d12_mvs PRIVATE + UNICODE + _UNICODE + NOMINMAX + WIN32_LEAN_AND_MEAN +) + +if(MSVC) + target_compile_options(xc_3dgs_d3d12_mvs PRIVATE /utf-8) +endif() + +target_link_libraries(xc_3dgs_d3d12_mvs PRIVATE + XCEngine + d3d12 + dxgi + dxguid + d3dcompiler + winmm + delayimp + bcrypt + opengl32 +) + +set_target_properties(xc_3dgs_d3d12_mvs PROPERTIES + VS_DEBUGGER_WORKING_DIRECTORY "$" +) + +add_custom_command(TARGET xc_3dgs_d3d12_mvs POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_CURRENT_SOURCE_DIR}/room.ply" + "$/room.ply" +) diff --git a/MVS/3DGS-D3D12/include/XC3DGSD3D12/App.h b/MVS/3DGS-D3D12/include/XC3DGSD3D12/App.h new file mode 100644 index 00000000..748e45f1 --- /dev/null +++ b/MVS/3DGS-D3D12/include/XC3DGSD3D12/App.h @@ -0,0 +1,89 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include "XC3DGSD3D12/GaussianPlyLoader.h" +#include "XCEngine/RHI/RHIEnums.h" +#include "XCEngine/RHI/RHITypes.h" +#include "XCEngine/RHI/D3D12/D3D12CommandAllocator.h" +#include "XCEngine/RHI/D3D12/D3D12Buffer.h" +#include "XCEngine/RHI/D3D12/D3D12CommandList.h" +#include "XCEngine/RHI/D3D12/D3D12CommandQueue.h" +#include "XCEngine/RHI/D3D12/D3D12DescriptorHeap.h" +#include "XCEngine/RHI/D3D12/D3D12Device.h" +#include "XCEngine/RHI/D3D12/D3D12ResourceView.h" +#include "XCEngine/RHI/D3D12/D3D12SwapChain.h" +#include "XCEngine/RHI/D3D12/D3D12Texture.h" + +namespace XC3DGSD3D12 { + +class App { +public: + App(); + ~App(); + + bool Initialize(HINSTANCE instance, int showCommand); + int Run(); + void SetFrameLimit(unsigned int frameLimit); + void SetGaussianScenePath(std::wstring scenePath); + void SetSummaryPath(std::wstring summaryPath); + const std::wstring& GetLastErrorMessage() const; + +private: + static constexpr int kBackBufferCount = 2; + static constexpr int kDefaultWidth = 1280; + static constexpr int kDefaultHeight = 720; + + static LRESULT CALLBACK StaticWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam); + LRESULT WindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam); + + bool RegisterWindowClass(HINSTANCE instance); + bool CreateMainWindow(HINSTANCE instance, int showCommand); + bool LoadGaussianScene(); + bool InitializeRhi(); + bool InitializeGaussianGpuResources(); + void ShutdownGaussianGpuResources(); + void Shutdown(); + void RenderFrame(); + + HWND m_hwnd = nullptr; + HINSTANCE m_instance = nullptr; + int m_width = kDefaultWidth; + int m_height = kDefaultHeight; + bool m_running = false; + bool m_isInitialized = false; + bool m_hasRenderedAtLeastOneFrame = false; + unsigned int m_frameLimit = 0; + unsigned int m_renderedFrameCount = 0; + std::wstring m_gaussianScenePath = L"room.ply"; + std::wstring m_summaryPath; + std::wstring m_lastErrorMessage; + GaussianSplatRuntimeData m_gaussianSceneData; + XCEngine::RHI::D3D12Buffer m_gaussianPositionBuffer; + XCEngine::RHI::D3D12Buffer m_gaussianOtherBuffer; + XCEngine::RHI::D3D12Buffer m_gaussianShBuffer; + XCEngine::RHI::D3D12Texture m_gaussianColorTexture; + std::unique_ptr m_gaussianPositionView; + std::unique_ptr m_gaussianOtherView; + std::unique_ptr m_gaussianShView; + std::unique_ptr m_gaussianColorView; + std::vector> m_gaussianUploadBuffers; + + XCEngine::RHI::D3D12Device m_device; + XCEngine::RHI::D3D12CommandQueue m_commandQueue; + XCEngine::RHI::D3D12SwapChain m_swapChain; + XCEngine::RHI::D3D12CommandAllocator m_commandAllocator; + XCEngine::RHI::D3D12CommandList m_commandList; + XCEngine::RHI::D3D12Texture m_depthStencil; + XCEngine::RHI::D3D12DescriptorHeap m_rtvHeap; + XCEngine::RHI::D3D12DescriptorHeap m_dsvHeap; + XCEngine::RHI::D3D12ResourceView m_rtvs[kBackBufferCount]; + XCEngine::RHI::D3D12ResourceView m_dsv; +}; + +} // namespace XC3DGSD3D12 diff --git a/MVS/3DGS-D3D12/include/XC3DGSD3D12/GaussianPlyLoader.h b/MVS/3DGS-D3D12/include/XC3DGSD3D12/GaussianPlyLoader.h new file mode 100644 index 00000000..673e6528 --- /dev/null +++ b/MVS/3DGS-D3D12/include/XC3DGSD3D12/GaussianPlyLoader.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace XC3DGSD3D12 { + +struct Float3 { + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; +}; + +struct GaussianSplatRuntimeData { + static constexpr uint32_t kColorTextureWidth = 2048; + static constexpr uint32_t kPositionStride = sizeof(float) * 3; + static constexpr uint32_t kOtherStride = sizeof(uint32_t) + sizeof(float) * 3; + static constexpr uint32_t kColorStride = sizeof(float) * 4; + static constexpr uint32_t kShCoefficientCount = 15; + static constexpr uint32_t kShStride = sizeof(float) * 3 * 16; + + uint32_t splatCount = 0; + uint32_t colorTextureWidth = kColorTextureWidth; + uint32_t colorTextureHeight = 0; + Float3 boundsMin = {}; + Float3 boundsMax = {}; + std::vector positionData; + std::vector otherData; + std::vector colorData; + std::vector shData; +}; + +bool LoadGaussianSceneFromPly( + const std::filesystem::path& filePath, + GaussianSplatRuntimeData& outData, + std::string& outErrorMessage); + +bool WriteGaussianSceneSummary( + const std::filesystem::path& filePath, + const GaussianSplatRuntimeData& data, + std::string& outErrorMessage); + +} // namespace XC3DGSD3D12 diff --git a/MVS/3DGS-D3D12/src/App.cpp b/MVS/3DGS-D3D12/src/App.cpp new file mode 100644 index 00000000..8b54e45c --- /dev/null +++ b/MVS/3DGS-D3D12/src/App.cpp @@ -0,0 +1,472 @@ +#include "XC3DGSD3D12/App.h" + +#include +#include + +#include + +namespace XC3DGSD3D12 { + +using namespace XCEngine::RHI; + +namespace { +constexpr wchar_t kWindowClassName[] = L"XC3DGSD3D12WindowClass"; +constexpr wchar_t kWindowTitle[] = L"XC 3DGS D3D12 MVS - Phase 1"; +constexpr float kClearColor[4] = { 0.08f, 0.12f, 0.18f, 1.0f }; + +std::filesystem::path GetExecutableDirectory() { + std::wstring pathBuffer; + pathBuffer.resize(MAX_PATH); + const DWORD pathLength = GetModuleFileNameW(nullptr, pathBuffer.data(), static_cast(pathBuffer.size())); + pathBuffer.resize(pathLength); + return std::filesystem::path(pathBuffer).parent_path(); +} + +std::filesystem::path ResolveNearExecutable(const std::wstring& path) { + const std::filesystem::path inputPath(path); + if (inputPath.is_absolute()) { + return inputPath; + } + return GetExecutableDirectory() / inputPath; +} +} + +App::App() = default; + +App::~App() { + Shutdown(); +} + +bool App::Initialize(HINSTANCE instance, int showCommand) { + m_instance = instance; + m_lastErrorMessage.clear(); + + if (!LoadGaussianScene()) { + return false; + } + + if (!RegisterWindowClass(instance)) { + m_lastErrorMessage = L"Failed to register the Win32 window class."; + return false; + } + + if (!CreateMainWindow(instance, showCommand)) { + m_lastErrorMessage = L"Failed to create the main window."; + return false; + } + + if (!InitializeRhi()) { + if (m_lastErrorMessage.empty()) { + m_lastErrorMessage = L"Failed to initialize the D3D12 RHI objects."; + } + return false; + } + + if (!InitializeGaussianGpuResources()) { + return false; + } + + m_isInitialized = true; + m_running = true; + return true; +} + +void App::SetFrameLimit(unsigned int frameLimit) { + m_frameLimit = frameLimit; +} + +void App::SetGaussianScenePath(std::wstring scenePath) { + m_gaussianScenePath = std::move(scenePath); +} + +void App::SetSummaryPath(std::wstring summaryPath) { + m_summaryPath = std::move(summaryPath); +} + +const std::wstring& App::GetLastErrorMessage() const { + return m_lastErrorMessage; +} + +int App::Run() { + MSG message = {}; + while (m_running) { + while (PeekMessage(&message, nullptr, 0, 0, PM_REMOVE)) { + if (message.message == WM_QUIT) { + m_running = false; + break; + } + + TranslateMessage(&message); + DispatchMessage(&message); + } + + if (!m_running) { + break; + } + + RenderFrame(); + + ++m_renderedFrameCount; + if (m_frameLimit > 0 && m_renderedFrameCount >= m_frameLimit) { + m_running = false; + PostMessageW(m_hwnd, WM_CLOSE, 0, 0); + } + } + + return static_cast(message.wParam); +} + +LRESULT CALLBACK App::StaticWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { + App* app = nullptr; + if (message == WM_NCCREATE) { + CREATESTRUCTW* createStruct = reinterpret_cast(lParam); + app = reinterpret_cast(createStruct->lpCreateParams); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(app)); + } else { + app = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + } + + if (app != nullptr) { + return app->WindowProc(hwnd, message, wParam, lParam); + } + + return DefWindowProcW(hwnd, message, wParam, lParam); +} + +LRESULT App::WindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { + switch (message) { + case WM_CLOSE: + DestroyWindow(hwnd); + return 0; + case WM_DESTROY: + PostQuitMessage(0); + return 0; + default: + return DefWindowProcW(hwnd, message, wParam, lParam); + } +} + +bool App::RegisterWindowClass(HINSTANCE instance) { + WNDCLASSEXW windowClass = {}; + windowClass.cbSize = sizeof(WNDCLASSEXW); + windowClass.style = CS_HREDRAW | CS_VREDRAW; + windowClass.lpfnWndProc = &App::StaticWindowProc; + windowClass.hInstance = instance; + windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW); + windowClass.lpszClassName = kWindowClassName; + return RegisterClassExW(&windowClass) != 0; +} + +bool App::CreateMainWindow(HINSTANCE instance, int showCommand) { + RECT windowRect = { 0, 0, m_width, m_height }; + AdjustWindowRect(&windowRect, WS_OVERLAPPEDWINDOW, FALSE); + + m_hwnd = CreateWindowExW( + 0, + kWindowClassName, + kWindowTitle, + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, + CW_USEDEFAULT, + windowRect.right - windowRect.left, + windowRect.bottom - windowRect.top, + nullptr, + nullptr, + instance, + this); + + if (m_hwnd == nullptr) { + return false; + } + + ShowWindow(m_hwnd, showCommand); + UpdateWindow(m_hwnd); + return true; +} + +bool App::LoadGaussianScene() { + std::string errorMessage; + const std::filesystem::path scenePath = ResolveNearExecutable(m_gaussianScenePath); + if (!LoadGaussianSceneFromPly(scenePath, m_gaussianSceneData, errorMessage)) { + m_lastErrorMessage.assign(errorMessage.begin(), errorMessage.end()); + return false; + } + + if (!m_summaryPath.empty()) { + const std::filesystem::path summaryPath = ResolveNearExecutable(m_summaryPath); + if (!WriteGaussianSceneSummary(summaryPath, m_gaussianSceneData, errorMessage)) { + m_lastErrorMessage.assign(errorMessage.begin(), errorMessage.end()); + return false; + } + } + + return true; +} + +bool App::InitializeRhi() { + RHIDeviceDesc deviceDesc = {}; + deviceDesc.adapterIndex = 0; + deviceDesc.enableDebugLayer = false; + deviceDesc.enableGPUValidation = false; + + if (!m_device.Initialize(deviceDesc)) { + m_lastErrorMessage = L"Failed to initialize the XCEngine D3D12 device."; + return false; + } + + ID3D12Device* device = m_device.GetDevice(); + IDXGIFactory4* factory = m_device.GetFactory(); + + if (!m_commandQueue.Initialize(device, CommandQueueType::Direct)) { + m_lastErrorMessage = L"Failed to initialize the direct command queue."; + return false; + } + + if (!m_swapChain.Initialize(factory, m_commandQueue.GetCommandQueue(), m_hwnd, m_width, m_height, kBackBufferCount)) { + m_lastErrorMessage = L"Failed to initialize the swap chain."; + return false; + } + + if (!m_depthStencil.InitializeDepthStencil(device, m_width, m_height)) { + m_lastErrorMessage = L"Failed to initialize the depth stencil texture."; + return false; + } + + if (!m_rtvHeap.Initialize(device, DescriptorHeapType::RTV, kBackBufferCount)) { + m_lastErrorMessage = L"Failed to initialize the RTV descriptor heap."; + return false; + } + + if (!m_dsvHeap.Initialize(device, DescriptorHeapType::DSV, 1)) { + m_lastErrorMessage = L"Failed to initialize the DSV descriptor heap."; + return false; + } + + for (int index = 0; index < kBackBufferCount; ++index) { + D3D12Texture& backBuffer = m_swapChain.GetBackBuffer(index); + D3D12_RENDER_TARGET_VIEW_DESC renderTargetDesc = + D3D12ResourceView::CreateRenderTargetDesc(Format::R8G8B8A8_UNorm, D3D12_RTV_DIMENSION_TEXTURE2D); + m_rtvs[index].InitializeAsRenderTarget(device, backBuffer.GetResource(), &renderTargetDesc, &m_rtvHeap, index); + } + + D3D12_DEPTH_STENCIL_VIEW_DESC depthStencilDesc = + D3D12ResourceView::CreateDepthStencilDesc(Format::D24_UNorm_S8_UInt, D3D12_DSV_DIMENSION_TEXTURE2D); + m_dsv.InitializeAsDepthStencil(device, m_depthStencil.GetResource(), &depthStencilDesc, &m_dsvHeap, 0); + + if (!m_commandAllocator.Initialize(device, CommandQueueType::Direct)) { + m_lastErrorMessage = L"Failed to initialize the command allocator."; + return false; + } + + if (!m_commandList.Initialize(device, CommandQueueType::Direct, m_commandAllocator.GetCommandAllocator())) { + m_lastErrorMessage = L"Failed to initialize the command list."; + return false; + } + + return true; +} + +bool App::InitializeGaussianGpuResources() { + ID3D12Device* device = m_device.GetDevice(); + ID3D12GraphicsCommandList* commandList = m_commandList.GetCommandList(); + + m_commandAllocator.Reset(); + m_commandList.Reset(); + + const D3D12_RESOURCE_STATES shaderResourceState = + D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; + + m_gaussianPositionBuffer.SetStride(4); + if (!m_gaussianPositionBuffer.InitializeWithData( + device, + commandList, + m_gaussianSceneData.positionData.data(), + static_cast(m_gaussianSceneData.positionData.size()), + shaderResourceState)) { + m_lastErrorMessage = L"Failed to upload the Gaussian position buffer."; + return false; + } + + m_gaussianOtherBuffer.SetStride(4); + if (!m_gaussianOtherBuffer.InitializeWithData( + device, + commandList, + m_gaussianSceneData.otherData.data(), + static_cast(m_gaussianSceneData.otherData.size()), + shaderResourceState)) { + m_lastErrorMessage = L"Failed to upload the Gaussian other buffer."; + return false; + } + + m_gaussianShBuffer.SetStride(4); + if (!m_gaussianShBuffer.InitializeWithData( + device, + commandList, + m_gaussianSceneData.shData.data(), + static_cast(m_gaussianSceneData.shData.size()), + shaderResourceState)) { + m_lastErrorMessage = L"Failed to upload the Gaussian SH buffer."; + return false; + } + + D3D12_RESOURCE_DESC colorTextureDesc = {}; + colorTextureDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + colorTextureDesc.Alignment = 0; + colorTextureDesc.Width = m_gaussianSceneData.colorTextureWidth; + colorTextureDesc.Height = m_gaussianSceneData.colorTextureHeight; + colorTextureDesc.DepthOrArraySize = 1; + colorTextureDesc.MipLevels = 1; + colorTextureDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT; + colorTextureDesc.SampleDesc.Count = 1; + colorTextureDesc.SampleDesc.Quality = 0; + colorTextureDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; + colorTextureDesc.Flags = D3D12_RESOURCE_FLAG_NONE; + + Microsoft::WRL::ComPtr colorUploadBuffer; + if (!m_gaussianColorTexture.InitializeFromData( + device, + commandList, + colorTextureDesc, + TextureType::Texture2D, + m_gaussianSceneData.colorData.data(), + m_gaussianSceneData.colorData.size(), + m_gaussianSceneData.colorTextureWidth * GaussianSplatRuntimeData::kColorStride, + &colorUploadBuffer)) { + m_lastErrorMessage = L"Failed to upload the Gaussian color texture."; + return false; + } + m_gaussianUploadBuffers.push_back(colorUploadBuffer); + + ResourceViewDesc rawBufferViewDesc = {}; + rawBufferViewDesc.dimension = ResourceViewDimension::RawBuffer; + + m_gaussianPositionView.reset(static_cast(m_device.CreateShaderResourceView(&m_gaussianPositionBuffer, rawBufferViewDesc))); + if (!m_gaussianPositionView) { + m_lastErrorMessage = L"Failed to create the Gaussian position SRV."; + return false; + } + + m_gaussianOtherView.reset(static_cast(m_device.CreateShaderResourceView(&m_gaussianOtherBuffer, rawBufferViewDesc))); + if (!m_gaussianOtherView) { + m_lastErrorMessage = L"Failed to create the Gaussian other SRV."; + return false; + } + + m_gaussianShView.reset(static_cast(m_device.CreateShaderResourceView(&m_gaussianShBuffer, rawBufferViewDesc))); + if (!m_gaussianShView) { + m_lastErrorMessage = L"Failed to create the Gaussian SH SRV."; + return false; + } + + ResourceViewDesc textureViewDesc = {}; + textureViewDesc.dimension = ResourceViewDimension::Texture2D; + textureViewDesc.format = static_cast(Format::R32G32B32A32_Float); + m_gaussianColorView.reset(static_cast(m_device.CreateShaderResourceView(&m_gaussianColorTexture, textureViewDesc))); + if (!m_gaussianColorView) { + m_lastErrorMessage = L"Failed to create the Gaussian color texture SRV."; + return false; + } + + m_commandList.Close(); + void* commandLists[] = { &m_commandList }; + m_commandQueue.ExecuteCommandLists(1, commandLists); + m_commandQueue.WaitForIdle(); + m_gaussianUploadBuffers.clear(); + + return true; +} + +void App::ShutdownGaussianGpuResources() { + m_gaussianColorView.reset(); + m_gaussianShView.reset(); + m_gaussianOtherView.reset(); + m_gaussianPositionView.reset(); + m_gaussianUploadBuffers.clear(); + m_gaussianColorTexture.Shutdown(); + m_gaussianShBuffer.Shutdown(); + m_gaussianOtherBuffer.Shutdown(); + m_gaussianPositionBuffer.Shutdown(); +} + +void App::Shutdown() { + if (!m_isInitialized && m_hwnd == nullptr) { + return; + } + + m_running = false; + + if (m_commandQueue.GetCommandQueue() != nullptr) { + m_commandQueue.WaitForIdle(); + } + ShutdownGaussianGpuResources(); + m_commandList.Shutdown(); + m_commandAllocator.Shutdown(); + m_dsv.Shutdown(); + for (D3D12ResourceView& rtv : m_rtvs) { + rtv.Shutdown(); + } + m_dsvHeap.Shutdown(); + m_rtvHeap.Shutdown(); + m_depthStencil.Shutdown(); + m_swapChain.Shutdown(); + m_commandQueue.Shutdown(); + m_device.Shutdown(); + + if (m_hwnd != nullptr) { + DestroyWindow(m_hwnd); + m_hwnd = nullptr; + } + + if (m_instance != nullptr) { + UnregisterClassW(kWindowClassName, m_instance); + m_instance = nullptr; + } + + m_isInitialized = false; +} + +void App::RenderFrame() { + if (m_hasRenderedAtLeastOneFrame) { + m_commandQueue.WaitForPreviousFrame(); + } + + m_commandAllocator.Reset(); + m_commandList.Reset(); + + const int currentBackBufferIndex = m_swapChain.GetCurrentBackBufferIndex(); + D3D12Texture& backBuffer = m_swapChain.GetBackBuffer(currentBackBufferIndex); + + m_commandList.TransitionBarrier(backBuffer.GetResource(), ResourceStates::Present, ResourceStates::RenderTarget); + + const CPUDescriptorHandle rtvCpuHandle = m_rtvHeap.GetCPUDescriptorHandle(currentBackBufferIndex); + const CPUDescriptorHandle dsvCpuHandle = m_dsvHeap.GetCPUDescriptorHandle(0); + const D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = { rtvCpuHandle.ptr }; + const D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle = { dsvCpuHandle.ptr }; + + m_commandList.SetRenderTargetsHandle(1, &rtvHandle, &dsvHandle); + + const Viewport viewport = { 0.0f, 0.0f, static_cast(m_width), static_cast(m_height), 0.0f, 1.0f }; + const Rect scissorRect = { 0, 0, m_width, m_height }; + m_commandList.SetViewport(viewport); + m_commandList.SetScissorRect(scissorRect); + + m_commandList.ClearRenderTargetView(rtvHandle, kClearColor, 0, nullptr); + m_commandList.ClearDepthStencilView( + dsvHandle, + D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, + 1.0f, + 0, + 0, + nullptr); + + m_commandList.TransitionBarrier(backBuffer.GetResource(), ResourceStates::RenderTarget, ResourceStates::Present); + m_commandList.Close(); + + void* commandLists[] = { &m_commandList }; + m_commandQueue.ExecuteCommandLists(1, commandLists); + m_swapChain.Present(1, 0); + + m_hasRenderedAtLeastOneFrame = true; +} + +} // namespace XC3DGSD3D12 diff --git a/MVS/3DGS-D3D12/src/GaussianPlyLoader.cpp b/MVS/3DGS-D3D12/src/GaussianPlyLoader.cpp new file mode 100644 index 00000000..0e563d83 --- /dev/null +++ b/MVS/3DGS-D3D12/src/GaussianPlyLoader.cpp @@ -0,0 +1,572 @@ +#include "XC3DGSD3D12/GaussianPlyLoader.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace XC3DGSD3D12 { + +namespace { + +constexpr float kSHC0 = 0.2820948f; + +enum class PlyPropertyType { + None, + Float32, + Float64, + UInt8, +}; + +struct PlyProperty { + std::string name; + PlyPropertyType type = PlyPropertyType::None; + uint32_t offset = 0; + uint32_t size = 0; +}; + +struct PlyHeader { + uint32_t vertexCount = 0; + uint32_t vertexStride = 0; + std::vector properties; +}; + +struct Float4 { + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + float w = 0.0f; +}; + +struct RawGaussianSplat { + Float3 position = {}; + Float3 dc0 = {}; + std::array sh = {}; + float opacity = 0.0f; + Float3 scale = {}; + Float4 rotation = {}; +}; + +std::string TrimTrailingCarriageReturn(std::string line) { + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + return line; +} + +uint32_t PropertyTypeSize(PlyPropertyType type) { + switch (type) { + case PlyPropertyType::Float32: + return 4; + case PlyPropertyType::Float64: + return 8; + case PlyPropertyType::UInt8: + return 1; + default: + return 0; + } +} + +bool ParsePropertyType(const std::string& token, PlyPropertyType& outType) { + if (token == "float") { + outType = PlyPropertyType::Float32; + return true; + } + if (token == "double") { + outType = PlyPropertyType::Float64; + return true; + } + if (token == "uchar") { + outType = PlyPropertyType::UInt8; + return true; + } + + outType = PlyPropertyType::None; + return false; +} + +bool ParsePlyHeader(std::ifstream& input, PlyHeader& outHeader, std::string& outErrorMessage) { + std::string line; + if (!std::getline(input, line)) { + outErrorMessage = "Failed to read PLY magic line."; + return false; + } + + if (TrimTrailingCarriageReturn(line) != "ply") { + outErrorMessage = "Input file is not a valid PLY file."; + return false; + } + + bool sawFormat = false; + std::string currentElement; + + while (std::getline(input, line)) { + line = TrimTrailingCarriageReturn(line); + if (line == "end_header") { + break; + } + + if (line.empty()) { + continue; + } + + std::istringstream stream(line); + std::string token; + stream >> token; + + if (token == "comment") { + continue; + } + + if (token == "format") { + std::string formatName; + std::string version; + stream >> formatName >> version; + if (formatName != "binary_little_endian") { + outErrorMessage = "Only binary_little_endian PLY files are supported."; + return false; + } + sawFormat = true; + continue; + } + + if (token == "element") { + stream >> currentElement; + if (currentElement == "vertex") { + stream >> outHeader.vertexCount; + } + continue; + } + + if (token == "property" && currentElement == "vertex") { + std::string typeToken; + std::string name; + stream >> typeToken >> name; + + PlyPropertyType propertyType = PlyPropertyType::None; + if (!ParsePropertyType(typeToken, propertyType)) { + outErrorMessage = "Unsupported PLY vertex property type: " + typeToken; + return false; + } + + PlyProperty property; + property.name = name; + property.type = propertyType; + property.offset = outHeader.vertexStride; + property.size = PropertyTypeSize(propertyType); + outHeader.vertexStride += property.size; + outHeader.properties.push_back(property); + } + } + + if (!sawFormat) { + outErrorMessage = "PLY header is missing a valid format declaration."; + return false; + } + + if (outHeader.vertexCount == 0) { + outErrorMessage = "PLY file does not contain any vertex data."; + return false; + } + + if (outHeader.vertexStride == 0 || outHeader.properties.empty()) { + outErrorMessage = "PLY vertex layout is empty."; + return false; + } + + return true; +} + +bool ReadPropertyAsFloat( + const std::byte* vertexBytes, + const PlyProperty& property, + float& outValue) { + const std::byte* propertyPtr = vertexBytes + property.offset; + switch (property.type) { + case PlyPropertyType::Float32: { + std::memcpy(&outValue, propertyPtr, sizeof(float)); + return true; + } + case PlyPropertyType::Float64: { + double value = 0.0; + std::memcpy(&value, propertyPtr, sizeof(double)); + outValue = static_cast(value); + return true; + } + case PlyPropertyType::UInt8: { + uint8_t value = 0; + std::memcpy(&value, propertyPtr, sizeof(uint8_t)); + outValue = static_cast(value); + return true; + } + default: + return false; + } +} + +bool BuildPropertyMap( + const PlyHeader& header, + std::unordered_map& outMap, + std::string& outErrorMessage) { + outMap.clear(); + outMap.reserve(header.properties.size()); + + for (const PlyProperty& property : header.properties) { + const auto [it, inserted] = outMap.emplace(property.name, &property); + if (!inserted) { + outErrorMessage = "Duplicate PLY vertex property found: " + property.name; + return false; + } + } + + return true; +} + +bool RequireProperty( + const std::unordered_map& propertyMap, + std::string_view name, + const PlyProperty*& outProperty, + std::string& outErrorMessage) { + const auto iterator = propertyMap.find(name); + if (iterator == propertyMap.end()) { + outErrorMessage = "Missing required PLY property: " + std::string(name); + return false; + } + + outProperty = iterator->second; + return true; +} + +Float3 Min(const Float3& a, const Float3& b) { + return { + std::min(a.x, b.x), + std::min(a.y, b.y), + std::min(a.z, b.z), + }; +} + +Float3 Max(const Float3& a, const Float3& b) { + return { + std::max(a.x, b.x), + std::max(a.y, b.y), + std::max(a.z, b.z), + }; +} + +float Dot(const Float4& a, const Float4& b) { + return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w; +} + +Float4 NormalizeSwizzleRotation(const Float4& wxyz) { + const float lengthSquared = Dot(wxyz, wxyz); + if (lengthSquared <= std::numeric_limits::epsilon()) { + return { 0.0f, 0.0f, 0.0f, 1.0f }; + } + + const float inverseLength = 1.0f / std::sqrt(lengthSquared); + return { + wxyz.y * inverseLength, + wxyz.z * inverseLength, + wxyz.w * inverseLength, + wxyz.x * inverseLength, + }; +} + +Float4 PackSmallest3Rotation(Float4 rotation) { + const Float4 absoluteRotation = { + std::fabs(rotation.x), + std::fabs(rotation.y), + std::fabs(rotation.z), + std::fabs(rotation.w), + }; + + int largestIndex = 0; + float largestValue = absoluteRotation.x; + if (absoluteRotation.y > largestValue) { + largestIndex = 1; + largestValue = absoluteRotation.y; + } + if (absoluteRotation.z > largestValue) { + largestIndex = 2; + largestValue = absoluteRotation.z; + } + if (absoluteRotation.w > largestValue) { + largestIndex = 3; + largestValue = absoluteRotation.w; + } + + if (largestIndex == 0) { + rotation = { rotation.y, rotation.z, rotation.w, rotation.x }; + } else if (largestIndex == 1) { + rotation = { rotation.x, rotation.z, rotation.w, rotation.y }; + } else if (largestIndex == 2) { + rotation = { rotation.x, rotation.y, rotation.w, rotation.z }; + } + + const float sign = rotation.w >= 0.0f ? 1.0f : -1.0f; + const float invSqrt2 = std::sqrt(2.0f) * 0.5f; + const Float3 encoded = { + (rotation.x * sign * std::sqrt(2.0f)) * 0.5f + 0.5f, + (rotation.y * sign * std::sqrt(2.0f)) * 0.5f + 0.5f, + (rotation.z * sign * std::sqrt(2.0f)) * 0.5f + 0.5f, + }; + + (void)invSqrt2; + return { encoded.x, encoded.y, encoded.z, static_cast(largestIndex) / 3.0f }; +} + +uint32_t EncodeQuatToNorm10(const Float4& packedRotation) { + const auto saturate = [](float value) { + return std::clamp(value, 0.0f, 1.0f); + }; + + const uint32_t x = static_cast(saturate(packedRotation.x) * 1023.5f); + const uint32_t y = static_cast(saturate(packedRotation.y) * 1023.5f); + const uint32_t z = static_cast(saturate(packedRotation.z) * 1023.5f); + const uint32_t w = static_cast(saturate(packedRotation.w) * 3.5f); + return x | (y << 10) | (z << 20) | (w << 30); +} + +Float3 LinearScale(const Float3& logarithmicScale) { + return { + std::fabs(std::exp(logarithmicScale.x)), + std::fabs(std::exp(logarithmicScale.y)), + std::fabs(std::exp(logarithmicScale.z)), + }; +} + +Float3 SH0ToColor(const Float3& dc0) { + return { + dc0.x * kSHC0 + 0.5f, + dc0.y * kSHC0 + 0.5f, + dc0.z * kSHC0 + 0.5f, + }; +} + +float Sigmoid(float value) { + return 1.0f / (1.0f + std::exp(-value)); +} + +std::array DecodeMorton2D16x16(uint32_t value) { + value = (value & 0xFFu) | ((value & 0xFEu) << 7u); + value &= 0x5555u; + value = (value ^ (value >> 1u)) & 0x3333u; + value = (value ^ (value >> 2u)) & 0x0F0Fu; + return { value & 0xFu, value >> 8u }; +} + +uint32_t SplatIndexToTextureIndex(uint32_t index) { + const std::array morton = DecodeMorton2D16x16(index); + const uint32_t widthInBlocks = GaussianSplatRuntimeData::kColorTextureWidth / 16u; + index >>= 8u; + const uint32_t x = (index % widthInBlocks) * 16u + morton[0]; + const uint32_t y = (index / widthInBlocks) * 16u + morton[1]; + return y * GaussianSplatRuntimeData::kColorTextureWidth + x; +} + +template +void WriteValue(std::vector& bytes, size_t offset, const T& value) { + std::memcpy(bytes.data() + offset, &value, sizeof(T)); +} + +void WriteFloat3(std::vector& bytes, size_t offset, const Float3& value) { + WriteValue(bytes, offset + 0, value.x); + WriteValue(bytes, offset + 4, value.y); + WriteValue(bytes, offset + 8, value.z); +} + +void WriteFloat4(std::vector& bytes, size_t offset, float x, float y, float z, float w) { + WriteValue(bytes, offset + 0, x); + WriteValue(bytes, offset + 4, y); + WriteValue(bytes, offset + 8, z); + WriteValue(bytes, offset + 12, w); +} + +bool ReadGaussianSplat( + const std::byte* vertexBytes, + const std::unordered_map& propertyMap, + RawGaussianSplat& outSplat, + std::string& outErrorMessage) { + const PlyProperty* property = nullptr; + + auto readFloat = [&](std::string_view name, float& outValue) -> bool { + if (!RequireProperty(propertyMap, name, property, outErrorMessage)) { + return false; + } + return ReadPropertyAsFloat(vertexBytes, *property, outValue); + }; + + if (!readFloat("x", outSplat.position.x) || + !readFloat("y", outSplat.position.y) || + !readFloat("z", outSplat.position.z) || + !readFloat("f_dc_0", outSplat.dc0.x) || + !readFloat("f_dc_1", outSplat.dc0.y) || + !readFloat("f_dc_2", outSplat.dc0.z) || + !readFloat("opacity", outSplat.opacity) || + !readFloat("scale_0", outSplat.scale.x) || + !readFloat("scale_1", outSplat.scale.y) || + !readFloat("scale_2", outSplat.scale.z) || + !readFloat("rot_0", outSplat.rotation.x) || + !readFloat("rot_1", outSplat.rotation.y) || + !readFloat("rot_2", outSplat.rotation.z) || + !readFloat("rot_3", outSplat.rotation.w)) { + if (outErrorMessage.empty()) { + outErrorMessage = "Failed to read required Gaussian splat PLY properties."; + } + return false; + } + + std::array shRaw = {}; + for (uint32_t index = 0; index < shRaw.size(); ++index) { + const std::string propertyName = "f_rest_" + std::to_string(index); + if (!readFloat(propertyName, shRaw[index])) { + if (outErrorMessage.empty()) { + outErrorMessage = "Failed to read SH rest coefficients from PLY."; + } + return false; + } + } + + for (uint32_t coefficientIndex = 0; coefficientIndex < GaussianSplatRuntimeData::kShCoefficientCount; ++coefficientIndex) { + outSplat.sh[coefficientIndex] = { + shRaw[coefficientIndex + 0], + shRaw[coefficientIndex + GaussianSplatRuntimeData::kShCoefficientCount], + shRaw[coefficientIndex + GaussianSplatRuntimeData::kShCoefficientCount * 2], + }; + } + + return true; +} + +void LinearizeGaussianSplat(RawGaussianSplat& splat) { + const Float4 normalizedQuaternion = NormalizeSwizzleRotation(splat.rotation); + const Float4 packedQuaternion = PackSmallest3Rotation(normalizedQuaternion); + splat.rotation = packedQuaternion; + splat.scale = LinearScale(splat.scale); + splat.dc0 = SH0ToColor(splat.dc0); + splat.opacity = Sigmoid(splat.opacity); +} + +} // namespace + +bool LoadGaussianSceneFromPly( + const std::filesystem::path& filePath, + GaussianSplatRuntimeData& outData, + std::string& outErrorMessage) { + outData = {}; + outErrorMessage.clear(); + + std::ifstream input(filePath, std::ios::binary); + if (!input.is_open()) { + outErrorMessage = "Failed to open PLY file: " + filePath.string(); + return false; + } + + PlyHeader header; + if (!ParsePlyHeader(input, header, outErrorMessage)) { + return false; + } + + std::unordered_map propertyMap; + if (!BuildPropertyMap(header, propertyMap, outErrorMessage)) { + return false; + } + + outData.splatCount = header.vertexCount; + outData.colorTextureWidth = GaussianSplatRuntimeData::kColorTextureWidth; + outData.colorTextureHeight = + std::max(1u, (header.vertexCount + outData.colorTextureWidth - 1u) / outData.colorTextureWidth); + outData.colorTextureHeight = (outData.colorTextureHeight + 15u) / 16u * 16u; + + outData.positionData.resize(static_cast(header.vertexCount) * GaussianSplatRuntimeData::kPositionStride); + outData.otherData.resize(static_cast(header.vertexCount) * GaussianSplatRuntimeData::kOtherStride); + outData.colorData.resize( + static_cast(outData.colorTextureWidth) * + static_cast(outData.colorTextureHeight) * + GaussianSplatRuntimeData::kColorStride); + outData.shData.resize(static_cast(header.vertexCount) * GaussianSplatRuntimeData::kShStride); + + outData.boundsMin = { + std::numeric_limits::infinity(), + std::numeric_limits::infinity(), + std::numeric_limits::infinity(), + }; + outData.boundsMax = { + -std::numeric_limits::infinity(), + -std::numeric_limits::infinity(), + -std::numeric_limits::infinity(), + }; + + std::vector vertexBytes(header.vertexStride); + for (uint32_t splatIndex = 0; splatIndex < header.vertexCount; ++splatIndex) { + input.read(reinterpret_cast(vertexBytes.data()), static_cast(vertexBytes.size())); + if (input.gcount() != static_cast(vertexBytes.size())) { + outErrorMessage = + "Unexpected end of file while reading Gaussian splat vertex " + std::to_string(splatIndex) + "."; + return false; + } + + RawGaussianSplat splat; + if (!ReadGaussianSplat(vertexBytes.data(), propertyMap, splat, outErrorMessage)) { + return false; + } + + LinearizeGaussianSplat(splat); + + outData.boundsMin = Min(outData.boundsMin, splat.position); + outData.boundsMax = Max(outData.boundsMax, splat.position); + + const size_t positionOffset = static_cast(splatIndex) * GaussianSplatRuntimeData::kPositionStride; + WriteFloat3(outData.positionData, positionOffset, splat.position); + + const size_t otherOffset = static_cast(splatIndex) * GaussianSplatRuntimeData::kOtherStride; + const uint32_t packedRotation = EncodeQuatToNorm10(splat.rotation); + WriteValue(outData.otherData, otherOffset, packedRotation); + WriteFloat3(outData.otherData, otherOffset + sizeof(uint32_t), splat.scale); + + const size_t shOffset = static_cast(splatIndex) * GaussianSplatRuntimeData::kShStride; + for (uint32_t coefficientIndex = 0; coefficientIndex < GaussianSplatRuntimeData::kShCoefficientCount; ++coefficientIndex) { + const size_t coefficientOffset = shOffset + static_cast(coefficientIndex) * sizeof(float) * 3u; + WriteFloat3(outData.shData, coefficientOffset, splat.sh[coefficientIndex]); + } + + const uint32_t textureIndex = SplatIndexToTextureIndex(splatIndex); + const size_t colorOffset = static_cast(textureIndex) * GaussianSplatRuntimeData::kColorStride; + WriteFloat4(outData.colorData, colorOffset, splat.dc0.x, splat.dc0.y, splat.dc0.z, splat.opacity); + } + + return true; +} + +bool WriteGaussianSceneSummary( + const std::filesystem::path& filePath, + const GaussianSplatRuntimeData& data, + std::string& outErrorMessage) { + outErrorMessage.clear(); + + std::ofstream output(filePath, std::ios::binary | std::ios::trunc); + if (!output.is_open()) { + outErrorMessage = "Failed to open summary output file: " + filePath.string(); + return false; + } + + output << "splat_count=" << data.splatCount << '\n'; + output << "color_texture_width=" << data.colorTextureWidth << '\n'; + output << "color_texture_height=" << data.colorTextureHeight << '\n'; + output << "bounds_min=" << data.boundsMin.x << "," << data.boundsMin.y << "," << data.boundsMin.z << '\n'; + output << "bounds_max=" << data.boundsMax.x << "," << data.boundsMax.y << "," << data.boundsMax.z << '\n'; + output << "position_bytes=" << data.positionData.size() << '\n'; + output << "other_bytes=" << data.otherData.size() << '\n'; + output << "color_bytes=" << data.colorData.size() << '\n'; + output << "sh_bytes=" << data.shData.size() << '\n'; + + return output.good(); +} + +} // namespace XC3DGSD3D12 diff --git a/MVS/3DGS-D3D12/src/main.cpp b/MVS/3DGS-D3D12/src/main.cpp new file mode 100644 index 00000000..d2394380 --- /dev/null +++ b/MVS/3DGS-D3D12/src/main.cpp @@ -0,0 +1,39 @@ +#include +#include + +#include + +#include "XC3DGSD3D12/App.h" + +int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int showCommand) { + XC3DGSD3D12::App app; + + int argumentCount = 0; + LPWSTR* arguments = CommandLineToArgvW(GetCommandLineW(), &argumentCount); + for (int index = 1; index + 1 < argumentCount; ++index) { + if (std::wstring(arguments[index]) == L"--frame-limit") { + app.SetFrameLimit(static_cast(_wtoi(arguments[index + 1]))); + ++index; + } else if (std::wstring(arguments[index]) == L"--scene") { + app.SetGaussianScenePath(arguments[index + 1]); + ++index; + } else if (std::wstring(arguments[index]) == L"--summary-file") { + app.SetSummaryPath(arguments[index + 1]); + ++index; + } + } + if (arguments != nullptr) { + LocalFree(arguments); + } + + if (!app.Initialize(instance, showCommand)) { + const std::wstring message = + app.GetLastErrorMessage().empty() + ? L"Failed to initialize XC 3DGS D3D12 MVS." + : app.GetLastErrorMessage(); + MessageBoxW(nullptr, message.c_str(), L"Initialization Error", MB_OK | MB_ICONERROR); + return -1; + } + + return app.Run(); +} diff --git a/docs/plan/3DGS-D3D12最小可行系统计划_2026-04-12.md b/docs/plan/3DGS-D3D12最小可行系统计划_2026-04-12.md new file mode 100644 index 00000000..178cc41e --- /dev/null +++ b/docs/plan/3DGS-D3D12最小可行系统计划_2026-04-12.md @@ -0,0 +1,245 @@ +# 3DGS-D3D12 最小可行系统计划 + +日期:2026-04-12 + +## 1. 文档定位 + +这份计划用于指导 `D:\Xuanchi\Main\XCEngine\mvs\3DGS-D3D12` 的最小可行系统落地。 + +当前任务边界已经明确: + +1. 只允许在 `mvs/3DGS-D3D12` 目录内开发与新增文件。 +2. `engine` 代码只能引用,禁止修改。 +3. 图形抽象层只能使用现有 `engine` 提供的 `RHI` 接口。 +4. 本轮目标是先把 3DGS 的最小可行系统跑通,而不是把它正式并入引擎主线。 +5. 本轮明确不引入 `chunk` 机制。 + +因此,这份计划不是“继续修补引擎内已有的 GaussianSplat 路径”,而是“在 MVS 目录内重新搭一条干净的、可验证的、无 chunk 的 3DGS-D3D12 最小链路”。 + +## 2. 参考实现与职责拆分 + +本轮只参考两条现有实现,各自承担不同职责: + +### 2.1 `mvs/3DGS Unity Renderer` + +用途: + +1. 参考 `.ply` 的读取方式。 +2. 参考如何从 PLY 属性中提取 3DGS 所需原始数据。 +3. 参考资源准备阶段的数据布局与字段含义。 + +注意: + +1. 这里只借鉴导入与数据准备思路。 +2. 不照搬 Unity Editor 资产工作流。 +3. 不引入 chunk。 + +### 2.2 `mvs/3DGS-Unity` + +用途: + +1. 参考“干净的无 chunk 渲染路径”。 +2. 参考 `prepare -> sort -> draw -> composite` 的最小闭环。 +3. 参考 3DGS 在屏幕空间椭圆展开、混合与合成时的核心着色器语义。 + +注意: + +1. 这里重点参考渲染过程,而不是 Unity 的宿主框架。 +2. 不把 Unity 的 `ScriptableRenderPass`、资产导入器、Inspector 等编辑器逻辑带入本轮实现。 + +## 3. 首轮目标 + +首轮只完成以下闭环: + +1. 在 `mvs/3DGS-D3D12` 内部加载 `room.ply`。 +2. 将 PLY 中的高斯数据转换为无 chunk 的运行时缓冲。 +3. 通过现有 `RHI` 完成 D3D12 路径下的最小渲染。 +4. 输出一张稳定可观察的结果图,证明房间场景已经被正确绘制。 + +首轮验收标准: + +1. 程序能独立编译与运行。 +2. 不依赖修改 `engine` 才能成立。 +3. 渲染结果不再是纯黑、纯白或明显错误的撕裂图。 +4. 渲染链路中不存在任何 chunk 数据结构、chunk 缓冲或 chunk 可见性阶段。 + +## 4. 非目标 + +本轮明确不做: + +1. 不并入 editor。 +2. 不接入引擎现有 Renderer 主线。 +3. 不做 OpenGL / Vulkan 多后端对齐。 +4. 不做正式资源缓存格式。 +5. 不做 chunk、cluster、LOD、streaming 等优化层。 +6. 不做 compute 之外的额外架构扩展。 +7. 不为迎合当前 MVS 去修改 `engine` 的 `RHI`、Renderer、Resources。 + +## 5. 约束与执行原则 + +### 5.1 目录约束 + +本轮新增内容应尽量收敛在 `mvs/3DGS-D3D12` 内,例如: + +1. `src/`:程序入口、渲染器、相机、数据上传。 +2. `include/`:本地头文件。 +3. `shaders/`:本地 HLSL 或中间 shader 资源。 +4. `assets/`:本地测试资源或生成物描述。 +5. `third_party/`:仅当 MVS 自己确实需要额外小型依赖时使用。 + +### 5.2 RHI 使用原则 + +1. 只调用现有 `engine` 的 `RHI` 公共接口。 +2. 如果发现最小系统缺失某项能力,优先在 MVS 内通过更简单的组织方式规避。 +3. 若确实被 `RHI` 能力边界阻塞,先记录问题并汇报,不允许直接改 `engine`。 + +### 5.3 数据路径原则 + +1. 整个系统只保留 positions、other、color、SH 等无 chunk 基础数据。 +2. 不生成 chunk header。 +3. 不上传 chunk buffer。 +4. 不做 visible chunk 标记。 +5. 不保留任何为了兼容旧 chunk 方案而加的临时分支。 + +## 6. 技术路线 + +### Phase 1:梳理 3DGS-D3D12 的骨架与构建入口 + +目标: + +1. 确认 `mvs/3DGS-D3D12` 当前是否只有 `room.ply`。 +2. 建立最小的可执行工程骨架。 +3. 打通对 `engine` 中 `RHI` 的引用与链接。 + +任务: + +1. 规划 `CMakeLists.txt` 与目录结构。 +2. 建立窗口、设备、交换链、命令提交、离屏或在屏渲染的最小入口。 +3. 跑通一个“清屏可显示”的基础版本。 + +验收: + +1. `3DGS-D3D12` 可独立编译。 +2. 程序能启动并输出基础画面。 + +### Phase 2:实现无 chunk 的 PLY 读取与运行时数据打包 + +目标: + +1. 参考 `mvs/3DGS Unity Renderer`,在 MVS 内部完成 PLY 读取。 +2. 输出适合渲染阶段直接上传的高斯原始数组。 + +任务: + +1. 解析 PLY header 与顶点属性。 +2. 提取 position、rotation、scale、opacity、color/SH 等字段。 +3. 明确字段的排列顺序、类型与归一化方式。 +4. 生成 MVS 本地 `GaussianSplatSceneData`。 + +验收: + +1. `room.ply` 可被成功读取。 +2. 点数量、字段长度、边界盒等基础统计合理。 +3. 整条导入链中完全没有 chunk 概念。 + +### Phase 3:对齐无 chunk 的 GPU 数据布局与上传 + +目标: + +1. 把导入结果上传到 GPU。 +2. 数据布局尽量贴近 `mvs/3DGS-Unity` 的渲染输入。 + +任务: + +1. 设计 positions / other / color / SH 的 GPU buffer。 +2. 建立与着色器绑定一致的 SRV/UAV 视图。 +3. 为排序与绘制准备 index、distance、view-data 等工作缓冲。 + +验收: + +1. GPU 侧所有关键缓冲都能正确创建。 +2. 每个绑定槽位与 shader 语义一一对应。 +3. 不包含 chunk buffer / visible chunk buffer。 + +### Phase 4:先打通 prepare 与 sort + +目标: + +1. 先验证“高斯数据被相机看到并被正确排序”。 +2. 在 draw 之前把中间结果可视化或可检查化。 + +任务: + +1. 参考 `mvs/3DGS-Unity` 的 prepare 语义,计算每个 splat 的 view-space 信息。 +2. 生成排序距离。 +3. 跑通最小排序路径。 +4. 必要时增加调试输出,用于检查 order / distance / 计数是否异常。 + +验收: + +1. prepare 结果不是全零或明显错误。 +2. sort 输出索引顺序稳定。 + +### Phase 5:对齐 draw 与 composite + +目标: + +1. 参考 `mvs/3DGS-Unity` 的“干净无 chunk 渲染路径”把核心画面先画出来。 + +任务: + +1. 对齐 Gaussian splat draw shader 的主要输入输出语义。 +2. 对齐椭圆展开、覆盖范围与透明混合约定。 +3. 建立 accumulation target。 +4. 建立 composite pass,把 accumulation 结果合回最终颜色。 + +验收: + +1. 输出图中能看出 `room.ply` 对应的房间结构。 +2. 不出现整屏纯黑、纯白、随机撕裂。 + +### Phase 6:收口与验证 + +目标: + +1. 固化最小系统结果,形成稳定基线。 + +任务: + +1. 规范运行命令与资源路径。 +2. 输出一张固定命名的结果图作为检查基线。 +3. 清理调试残留与临时分支。 +4. 补一份 MVS 内局部说明文档。 + +验收: + +1. 代码、资源、着色器都收敛在 `mvs/3DGS-D3D12`。 +2. 运行方式清晰。 +3. 基线截图稳定。 + +## 7. 关键风险 + +### 7.1 PLY 属性语义与当前样例不一致 + +如果 `room.ply` 的字段命名、顺序或编码方式与参考加载器假设不一致,最先要修的是导入器映射,不是渲染器。 + +### 7.2 RHI 能力与参考实现存在接口差 + +如果 Unity 参考依赖的某些资源绑定或 compute 流程不能直接一比一照搬,本轮优先在 `mvs/3DGS-D3D12` 内部重排执行方式,而不是去动 `engine`。 + +### 7.3 排序与混合契约不一致 + +3DGS 最容易出错的不是“有没有画出来”,而是排序方向、alpha 累积与 composite 契约是否一致。本轮必须把这三者当成同一个问题处理,禁止分开打补丁。 + +## 8. 本轮完成后的下一步 + +当 `mvs/3DGS-D3D12` 的无 chunk 最小系统跑通后,下一步才有意义讨论: + +1. 是否把这条路径收编进引擎正式 Renderer。 +2. 是否把 PLY 导入升级成正式资源导入器。 +3. 是否在引擎层补 structured buffer / compute / renderer pass 抽象。 +4. 是否接入 editor 与资产缓存。 + +在这之前,所有工作都应服务于一个目标: + +先在 `mvs/3DGS-D3D12` 内证明“无 chunk 的 3DGS-D3D12 + 现有 RHI”这条路是通的。 diff --git a/docs/plan/3DGS渲染路径对齐参考实现修复计划_2026-04-11.md b/docs/used/3DGS渲染路径对齐参考实现修复计划_过期归档_2026-04-12.md similarity index 100% rename from docs/plan/3DGS渲染路径对齐参考实现修复计划_2026-04-11.md rename to docs/used/3DGS渲染路径对齐参考实现修复计划_过期归档_2026-04-12.md