Add 3DGS D3D12 MVS bootstrap and PLY loader

This commit is contained in:
2026-04-13 00:36:50 +08:00
parent 6f876678f5
commit 64212a53c7
8 changed files with 1534 additions and 0 deletions

View File

@@ -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 "$<TARGET_FILE_DIR:xc_3dgs_d3d12_mvs>"
)
add_custom_command(TARGET xc_3dgs_d3d12_mvs POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_CURRENT_SOURCE_DIR}/room.ply"
"$<TARGET_FILE_DIR:xc_3dgs_d3d12_mvs>/room.ply"
)

View File

@@ -0,0 +1,89 @@
#pragma once
#include <windows.h>
#include <memory>
#include <string>
#include <vector>
#include <wrl/client.h>
#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<XCEngine::RHI::D3D12ResourceView> m_gaussianPositionView;
std::unique_ptr<XCEngine::RHI::D3D12ResourceView> m_gaussianOtherView;
std::unique_ptr<XCEngine::RHI::D3D12ResourceView> m_gaussianShView;
std::unique_ptr<XCEngine::RHI::D3D12ResourceView> m_gaussianColorView;
std::vector<Microsoft::WRL::ComPtr<ID3D12Resource>> 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

View File

@@ -0,0 +1,46 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <string>
#include <vector>
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<std::byte> positionData;
std::vector<std::byte> otherData;
std::vector<std::byte> colorData;
std::vector<std::byte> 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

472
MVS/3DGS-D3D12/src/App.cpp Normal file
View File

@@ -0,0 +1,472 @@
#include "XC3DGSD3D12/App.h"
#include <d3d12.h>
#include <dxgi1_4.h>
#include <filesystem>
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<DWORD>(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<int>(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<CREATESTRUCTW*>(lParam);
app = reinterpret_cast<App*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
} else {
app = reinterpret_cast<App*>(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<uint64_t>(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<uint64_t>(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<uint64_t>(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<ID3D12Resource> 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<D3D12ResourceView*>(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<D3D12ResourceView*>(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<D3D12ResourceView*>(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<uint32_t>(Format::R32G32B32A32_Float);
m_gaussianColorView.reset(static_cast<D3D12ResourceView*>(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<float>(m_width), static_cast<float>(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

View File

@@ -0,0 +1,572 @@
#include "XC3DGSD3D12/GaussianPlyLoader.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstring>
#include <fstream>
#include <limits>
#include <sstream>
#include <string_view>
#include <unordered_map>
#include <vector>
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<PlyProperty> 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<Float3, GaussianSplatRuntimeData::kShCoefficientCount> 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<float>(value);
return true;
}
case PlyPropertyType::UInt8: {
uint8_t value = 0;
std::memcpy(&value, propertyPtr, sizeof(uint8_t));
outValue = static_cast<float>(value);
return true;
}
default:
return false;
}
}
bool BuildPropertyMap(
const PlyHeader& header,
std::unordered_map<std::string_view, const PlyProperty*>& 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<std::string_view, const PlyProperty*>& 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<float>::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<float>(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<uint32_t>(saturate(packedRotation.x) * 1023.5f);
const uint32_t y = static_cast<uint32_t>(saturate(packedRotation.y) * 1023.5f);
const uint32_t z = static_cast<uint32_t>(saturate(packedRotation.z) * 1023.5f);
const uint32_t w = static_cast<uint32_t>(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<uint32_t, 2> 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<uint32_t, 2> 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 <typename T>
void WriteValue(std::vector<std::byte>& bytes, size_t offset, const T& value) {
std::memcpy(bytes.data() + offset, &value, sizeof(T));
}
void WriteFloat3(std::vector<std::byte>& 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<std::byte>& 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<std::string_view, const PlyProperty*>& 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<float, GaussianSplatRuntimeData::kShCoefficientCount * 3> 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<std::string_view, const PlyProperty*> propertyMap;
if (!BuildPropertyMap(header, propertyMap, outErrorMessage)) {
return false;
}
outData.splatCount = header.vertexCount;
outData.colorTextureWidth = GaussianSplatRuntimeData::kColorTextureWidth;
outData.colorTextureHeight =
std::max<uint32_t>(1u, (header.vertexCount + outData.colorTextureWidth - 1u) / outData.colorTextureWidth);
outData.colorTextureHeight = (outData.colorTextureHeight + 15u) / 16u * 16u;
outData.positionData.resize(static_cast<size_t>(header.vertexCount) * GaussianSplatRuntimeData::kPositionStride);
outData.otherData.resize(static_cast<size_t>(header.vertexCount) * GaussianSplatRuntimeData::kOtherStride);
outData.colorData.resize(
static_cast<size_t>(outData.colorTextureWidth) *
static_cast<size_t>(outData.colorTextureHeight) *
GaussianSplatRuntimeData::kColorStride);
outData.shData.resize(static_cast<size_t>(header.vertexCount) * GaussianSplatRuntimeData::kShStride);
outData.boundsMin = {
std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::infinity(),
};
outData.boundsMax = {
-std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::infinity(),
};
std::vector<std::byte> vertexBytes(header.vertexStride);
for (uint32_t splatIndex = 0; splatIndex < header.vertexCount; ++splatIndex) {
input.read(reinterpret_cast<char*>(vertexBytes.data()), static_cast<std::streamsize>(vertexBytes.size()));
if (input.gcount() != static_cast<std::streamsize>(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<size_t>(splatIndex) * GaussianSplatRuntimeData::kPositionStride;
WriteFloat3(outData.positionData, positionOffset, splat.position);
const size_t otherOffset = static_cast<size_t>(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<size_t>(splatIndex) * GaussianSplatRuntimeData::kShStride;
for (uint32_t coefficientIndex = 0; coefficientIndex < GaussianSplatRuntimeData::kShCoefficientCount; ++coefficientIndex) {
const size_t coefficientOffset = shOffset + static_cast<size_t>(coefficientIndex) * sizeof(float) * 3u;
WriteFloat3(outData.shData, coefficientOffset, splat.sh[coefficientIndex]);
}
const uint32_t textureIndex = SplatIndexToTextureIndex(splatIndex);
const size_t colorOffset = static_cast<size_t>(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

View File

@@ -0,0 +1,39 @@
#include <windows.h>
#include <shellapi.h>
#include <string>
#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<unsigned int>(_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();
}

View File

@@ -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”这条路是通的。