Add 3DGS D3D12 MVS bootstrap and PLY loader
This commit is contained in:
71
MVS/3DGS-D3D12/CMakeLists.txt
Normal file
71
MVS/3DGS-D3D12/CMakeLists.txt
Normal 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"
|
||||
)
|
||||
89
MVS/3DGS-D3D12/include/XC3DGSD3D12/App.h
Normal file
89
MVS/3DGS-D3D12/include/XC3DGSD3D12/App.h
Normal 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
|
||||
46
MVS/3DGS-D3D12/include/XC3DGSD3D12/GaussianPlyLoader.h
Normal file
46
MVS/3DGS-D3D12/include/XC3DGSD3D12/GaussianPlyLoader.h
Normal 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
472
MVS/3DGS-D3D12/src/App.cpp
Normal 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
|
||||
572
MVS/3DGS-D3D12/src/GaussianPlyLoader.cpp
Normal file
572
MVS/3DGS-D3D12/src/GaussianPlyLoader.cpp
Normal 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
|
||||
39
MVS/3DGS-D3D12/src/main.cpp
Normal file
39
MVS/3DGS-D3D12/src/main.cpp
Normal 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();
|
||||
}
|
||||
245
docs/plan/3DGS-D3D12最小可行系统计划_2026-04-12.md
Normal file
245
docs/plan/3DGS-D3D12最小可行系统计划_2026-04-12.md
Normal 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”这条路是通的。
|
||||
Reference in New Issue
Block a user