Fix NanoVDB volume loading and rendering

This commit is contained in:
2026-04-09 01:11:59 +08:00
parent b839fd98af
commit fde99a4d34
13 changed files with 628024 additions and 55 deletions

View File

@@ -0,0 +1,319 @@
Shader "Builtin Volumetric"
{
Properties
{
_Tint ("Tint", Color) = (1,1,1,1)
_DensityScale ("Density Scale", Float) = 0.2
_StepSize ("Step Size", Float) = 1.0
_MaxSteps ("Max Steps", Float) = 2000.0
_AmbientStrength ("Ambient Strength", Float) = 0.005
_LightDirection ("Light Direction", Vector) = (0.5, 0.8, 0.3, 0.0)
_LightSamples ("Light Samples", Float) = 8.0
}
HLSLINCLUDE
#define PNANOVDB_HLSL
#define PNANOVDB_ADDRESS_32
#include "../../../third_party/nanovdb/shaders/PNanoVDB.hlsl"
cbuffer PerObjectConstants
{
float4x4 gProjectionMatrix;
float4x4 gViewMatrix;
float4x4 gModelMatrix;
float4x4 gInverseModelMatrix;
float4 gCameraWorldPosition;
float4 gVolumeBoundsMin;
float4 gVolumeBoundsMax;
};
cbuffer MaterialConstants
{
float4 gVolumeTint;
float4 gDensityScale;
float4 gStepSize;
float4 gMaxSteps;
float4 gAmbientStrength;
float4 gLightDirection;
float4 gLightSamples;
};
StructuredBuffer<uint> VolumeData;
struct VSInput
{
float3 position : POSITION;
};
struct PSInput
{
float4 position : SV_POSITION;
float3 localPosition : TEXCOORD0;
};
float3 MapUnitCubeToVolumeBounds(float3 unitCubePosition)
{
const float3 uvw = saturate(unitCubePosition + float3(0.5, 0.5, 0.5));
return lerp(gVolumeBoundsMin.xyz, gVolumeBoundsMax.xyz, uvw);
}
PSInput MainVS(VSInput input)
{
PSInput output;
const float3 localPosition = MapUnitCubeToVolumeBounds(input.position);
const float4 worldPosition = mul(gModelMatrix, float4(localPosition, 1.0f));
output.position = mul(gProjectionMatrix, mul(gViewMatrix, worldPosition));
output.localPosition = localPosition;
return output;
}
bool IntersectAabb(
float3 rayOrigin,
float3 rayDirection,
float3 boxMin,
float3 boxMax,
out float tEnter,
out float tExit)
{
const float3 invDirection = 1.0f / max(abs(rayDirection), 1e-6f) * sign(rayDirection);
const float3 t0 = (boxMin - rayOrigin) * invDirection;
const float3 t1 = (boxMax - rayOrigin) * invDirection;
const float3 tMin3 = min(t0, t1);
const float3 tMax3 = max(t0, t1);
tEnter = max(max(tMin3.x, tMin3.y), tMin3.z);
tExit = min(min(tMax3.x, tMax3.y), tMax3.z);
return tExit >= tEnter && tExit > 0.0f;
}
bool IsPointInsideAabb(float3 samplePosition, float3 boxMin, float3 boxMax)
{
return all(samplePosition >= boxMin) && all(samplePosition <= boxMax);
}
struct NanoVolume
{
pnanovdb_grid_handle_t grid;
pnanovdb_readaccessor_t accessor;
};
void InitNanoVolume(out NanoVolume volume)
{
volume.grid.address.byte_offset = 0;
const pnanovdb_tree_handle_t tree = pnanovdb_grid_get_tree(VolumeData, volume.grid);
const pnanovdb_root_handle_t root = pnanovdb_tree_get_root(VolumeData, tree);
pnanovdb_readaccessor_init(volume.accessor, root);
}
float GetValueCoord(inout pnanovdb_readaccessor_t accessor, float3 position)
{
const pnanovdb_vec3_t p = position;
const pnanovdb_coord_t ijk = pnanovdb_hdda_pos_to_ijk(p);
const pnanovdb_address_t address =
pnanovdb_readaccessor_get_value_address(
PNANOVDB_GRID_TYPE_FLOAT,
VolumeData,
accessor,
ijk);
return pnanovdb_read_float(VolumeData, address);
}
uint GetDimCoord(inout pnanovdb_readaccessor_t accessor, float3 position)
{
const pnanovdb_vec3_t p = position;
const pnanovdb_coord_t ijk = pnanovdb_hdda_pos_to_ijk(p);
return pnanovdb_readaccessor_get_dim(
PNANOVDB_GRID_TYPE_FLOAT,
VolumeData,
accessor,
ijk);
}
bool GetHddaHit(
inout pnanovdb_readaccessor_t accessor,
inout float tMin,
float3 origin,
float3 direction,
float tMax,
out float valueAtHit)
{
const pnanovdb_vec3_t pOrigin = origin;
const pnanovdb_vec3_t pDirection = direction;
float tHit = 0.0f;
const bool hit = pnanovdb_hdda_tree_marcher(
PNANOVDB_GRID_TYPE_FLOAT,
VolumeData,
accessor,
pOrigin, tMin,
pDirection, tMax,
tHit,
valueAtHit);
tMin = tHit;
return hit;
}
float ComputeVolumetricShadow(
float3 localPosition,
float densityScale,
float3 localLightDirection,
float lightSamples,
inout pnanovdb_readaccessor_t accessor)
{
if (lightSamples < 1.0f) {
return 1.0f;
}
float shadow = 1.0f;
float stepSize = 1.0f;
[unroll]
for (int stepIndex = 0; stepIndex < 10; ++stepIndex) {
const float3 samplePosition = localPosition + stepSize * localLightDirection;
const float sigmaS =
max(GetValueCoord(accessor, samplePosition) * densityScale, 0.0f);
const float sigmaE = max(0.000001f, sigmaS) * 0.3f;
shadow *= exp(-sigmaE * stepSize);
stepSize *= 2.0f;
}
return shadow;
}
float4 MainPS(PSInput input) : SV_TARGET
{
const float3 boxMin = gVolumeBoundsMin.xyz;
const float3 boxMax = gVolumeBoundsMax.xyz;
const float densityScale = max(gDensityScale.x, 0.0f);
const float stepSize = max(gStepSize.x, 0.001f);
const int maxSteps = max((int)gMaxSteps.x, 1);
const float ambientStrength = max(gAmbientStrength.x, 0.0f);
const float lightSamples = max(gLightSamples.x, 0.0f);
const float3 cameraLocalPosition =
mul(gInverseModelMatrix, float4(gCameraWorldPosition.xyz, 1.0f)).xyz;
const float3 rayDirection = normalize(input.localPosition - cameraLocalPosition);
float tEnter = 0.0f;
float tExit = 0.0f;
if (!IntersectAabb(cameraLocalPosition, rayDirection, boxMin, boxMax, tEnter, tExit)) {
discard;
}
const bool cameraInside = IsPointInsideAabb(cameraLocalPosition, boxMin, boxMax);
const float surfaceT = cameraInside ? tExit : max(tEnter, 0.0f);
const float3 expectedSurfacePosition = cameraLocalPosition + rayDirection * surfaceT;
if (distance(expectedSurfacePosition, input.localPosition) > stepSize * 1.5f + 0.01f) {
discard;
}
float t = max(surfaceT, 0.01f);
const float marchEnd = tExit;
float hitValue = 0.0f;
NanoVolume volume;
InitNanoVolume(volume);
if (!GetHddaHit(volume.accessor, t, cameraLocalPosition, rayDirection, marchEnd, hitValue)) {
discard;
}
float skipDistance = 0.0f;
float3 integratedLight = 0.0f.xxx;
float transmittance = 1.0f;
float accumulatedDensity = 0.0f;
float3 localLightDirection =
mul((float3x3)gInverseModelMatrix, gLightDirection.xyz);
const float localLightLength = length(localLightDirection);
if (localLightLength > 0.0001f) {
localLightDirection /= localLightLength;
} else {
localLightDirection = normalize(float3(0.5f, 0.8f, 0.3f));
}
[loop]
for (int stepIndex = 0; stepIndex < maxSteps; ++stepIndex) {
if (t >= marchEnd) {
break;
}
float3 localPosition = cameraLocalPosition + rayDirection * t;
const uint dim = GetDimCoord(volume.accessor, localPosition);
if (dim > 1u) {
skipDistance = 15.0f;
t += skipDistance;
continue;
}
float density = GetValueCoord(volume.accessor, localPosition) * densityScale;
if (density < 0.01f) {
skipDistance = 5.0f;
t += skipDistance;
continue;
}
if (skipDistance > 0.0f) {
t -= skipDistance * 0.8f;
localPosition = cameraLocalPosition + rayDirection * t;
skipDistance = 0.0f;
}
const float sigmaS = density;
const float sigmaE = max(0.000001f, sigmaS);
accumulatedDensity += sigmaS;
const float shadow =
ComputeVolumetricShadow(
localPosition,
densityScale,
localLightDirection,
lightSamples,
volume.accessor);
const float3 S = sigmaS * shadow.xxx;
const float3 integratedSegment = (S - S * exp(-sigmaE * stepSize)) / sigmaE;
integratedLight += transmittance * integratedSegment;
transmittance *= exp(-sigmaE * stepSize);
if (accumulatedDensity > 1.0f) {
break;
}
if (transmittance < 0.05f) {
transmittance = 0.0f;
break;
}
t += stepSize;
}
const float3 ambientLight = ambientStrength.xxx;
float3 finalColor = (integratedLight + ambientLight) * accumulatedDensity;
finalColor *= gVolumeTint.rgb;
finalColor = pow(max(finalColor, 0.0f.xxx), 1.0f / 2.2f);
const float alpha = saturate(accumulatedDensity) * saturate(gVolumeTint.a);
if (alpha <= 0.001f) {
discard;
}
return float4(finalColor, alpha);
}
ENDHLSL
SubShader
{
Tags { "Queue" = "Transparent" }
Pass
{
Name "Volumetric"
Tags { "LightMode" = "Volumetric" }
Cull Off
ZWrite Off
ZTest LEqual
Blend SrcAlpha OneMinusSrcAlpha
HLSLPROGRAM
#pragma target 4.5
#pragma vertex MainVS
#pragma fragment MainPS
ENDHLSL
}
}
}

View File

@@ -15,7 +15,7 @@ constexpr Core::uint32 kMaterialArtifactSchemaVersion = 6;
constexpr Core::uint32 kMeshArtifactSchemaVersion = 2;
constexpr Core::uint32 kShaderArtifactSchemaVersion = 5;
constexpr Core::uint32 kUIDocumentArtifactSchemaVersion = 2;
constexpr Core::uint32 kVolumeFieldArtifactSchemaVersion = 1;
constexpr Core::uint32 kVolumeFieldArtifactSchemaVersion = 2;
struct TextureArtifactHeader {
char magic[8] = { 'X', 'C', 'T', 'E', 'X', '0', '1', '\0' };
@@ -131,15 +131,17 @@ struct UIDocumentArtifactDiagnosticHeader {
};
struct VolumeFieldArtifactHeader {
char magic[8] = { 'X', 'C', 'V', 'O', 'L', '0', '1', '\0' };
char magic[8] = { 'X', 'C', 'V', 'O', 'L', '0', '2', '\0' };
Core::uint32 schemaVersion = kVolumeFieldArtifactSchemaVersion;
Core::uint32 storageKind = 0;
Math::Vector3 boundsMin = Math::Vector3::Zero();
Math::Vector3 boundsMax = Math::Vector3::Zero();
Math::Vector3 voxelSize = Math::Vector3::Zero();
Core::int32 indexBoundsMin[3] = { 0, 0, 0 };
Core::int32 indexBoundsMax[3] = { 0, 0, 0 };
Core::uint32 gridType = 0;
Core::uint32 gridClass = 0;
Core::uint64 payloadSize = 0;
Core::uint32 reserved0 = 0;
Core::uint32 reserved1 = 0;
};
} // namespace Resources

View File

@@ -14,6 +14,28 @@ enum class VolumeStorageKind : Core::uint32 {
NanoVDB = 1
};
struct VolumeIndexBounds {
Core::int32 minX = 0;
Core::int32 minY = 0;
Core::int32 minZ = 0;
Core::int32 maxX = 0;
Core::int32 maxY = 0;
Core::int32 maxZ = 0;
bool operator==(const VolumeIndexBounds& other) const {
return minX == other.minX &&
minY == other.minY &&
minZ == other.minZ &&
maxX == other.maxX &&
maxY == other.maxY &&
maxZ == other.maxZ;
}
bool operator!=(const VolumeIndexBounds& other) const {
return !(*this == other);
}
};
class VolumeField : public IResource {
public:
VolumeField();
@@ -31,11 +53,18 @@ public:
const void* payload,
size_t payloadSize,
const Math::Bounds& bounds = Math::Bounds(),
const Math::Vector3& voxelSize = Math::Vector3::Zero());
const Math::Vector3& voxelSize = Math::Vector3::Zero(),
const VolumeIndexBounds& indexBounds = VolumeIndexBounds(),
Core::uint32 gridType = 0u,
Core::uint32 gridClass = 0u);
VolumeStorageKind GetStorageKind() const { return m_storageKind; }
const Math::Bounds& GetBounds() const { return m_bounds; }
const Math::Bounds& GetWorldBounds() const { return m_bounds; }
const Math::Vector3& GetVoxelSize() const { return m_voxelSize; }
const VolumeIndexBounds& GetIndexBounds() const { return m_indexBounds; }
Core::uint32 GetGridType() const { return m_gridType; }
Core::uint32 GetGridClass() const { return m_gridClass; }
const void* GetPayloadData() const { return m_payload.Data(); }
size_t GetPayloadSize() const { return m_payload.Size(); }
@@ -45,6 +74,9 @@ private:
VolumeStorageKind m_storageKind = VolumeStorageKind::Unknown;
Math::Bounds m_bounds;
Math::Vector3 m_voxelSize = Math::Vector3::Zero();
VolumeIndexBounds m_indexBounds = {};
Core::uint32 m_gridType = 0u;
Core::uint32 m_gridClass = 0u;
Containers::Array<Core::uint8> m_payload;
};

View File

@@ -379,6 +379,14 @@ bool WriteVolumeFieldArtifactFile(const fs::path& artifactPath, const VolumeFiel
header.boundsMin = volumeField.GetBounds().GetMin();
header.boundsMax = volumeField.GetBounds().GetMax();
header.voxelSize = volumeField.GetVoxelSize();
header.indexBoundsMin[0] = volumeField.GetIndexBounds().minX;
header.indexBoundsMin[1] = volumeField.GetIndexBounds().minY;
header.indexBoundsMin[2] = volumeField.GetIndexBounds().minZ;
header.indexBoundsMax[0] = volumeField.GetIndexBounds().maxX;
header.indexBoundsMax[1] = volumeField.GetIndexBounds().maxY;
header.indexBoundsMax[2] = volumeField.GetIndexBounds().maxZ;
header.gridType = volumeField.GetGridType();
header.gridClass = volumeField.GetGridClass();
header.payloadSize = static_cast<Core::uint64>(volumeField.GetPayloadSize());
output.write(reinterpret_cast<const char*>(&header), sizeof(header));

View File

@@ -89,7 +89,7 @@ void ResourceCache::OnZeroRefCount(ResourceGUID guid) {
void ResourceCache::Evict(size_t requiredBytes) {
size_t released = 0;
// Simple eviction: remove from end of LRU list
// ResourceManager owns resource lifetime. This cache only drops its LRU bookkeeping.
while (released < requiredBytes && m_lruOrder.Size() > 0) {
ResourceGUID guid = m_lruOrder.Back();
m_lruOrder.PopBack();
@@ -98,7 +98,6 @@ void ResourceCache::Evict(size_t requiredBytes) {
if (it != nullptr) {
m_memoryUsage -= it->memorySize;
released += it->memorySize;
it->resource->Release();
m_cache.Erase(guid);
}
}

View File

@@ -216,6 +216,11 @@ void ResourceManager::Unload(ResourceGUID guid) {
{
std::lock_guard lock(m_mutex);
auto* refCount = m_refCounts.Find(guid);
if (refCount != nullptr && *refCount > 0u) {
return;
}
auto* it = m_resourceCache.Find(guid);
if (it != nullptr) {
resource = *it;
@@ -233,22 +238,38 @@ void ResourceManager::Unload(ResourceGUID guid) {
void ResourceManager::UnloadAll() {
Containers::Array<IResource*> resourcesToRelease;
Containers::Array<ResourceGUID> guidsToRelease;
{
std::lock_guard lock(m_mutex);
const auto cachedResources = m_resourceCache.GetPairs();
resourcesToRelease.Reserve(cachedResources.Size());
guidsToRelease.Reserve(cachedResources.Size());
for (const auto& pair : cachedResources) {
if (pair.second != nullptr) {
resourcesToRelease.PushBack(pair.second);
if (pair.second == nullptr) {
continue;
}
const auto* refCount = m_refCounts.Find(pair.first);
if (refCount != nullptr && *refCount > 0u) {
continue;
}
guidsToRelease.PushBack(pair.first);
resourcesToRelease.PushBack(pair.second);
}
m_resourceCache.Clear();
m_cache.Clear();
m_refCounts.Clear();
m_guidToPath.Clear();
m_memoryUsage = 0;
for (const ResourceGUID& guid : guidsToRelease) {
auto* it = m_resourceCache.Find(guid);
if (it == nullptr || *it == nullptr) {
continue;
}
m_memoryUsage -= (*it)->GetMemorySize();
m_resourceCache.Erase(guid);
m_cache.Remove(guid);
m_guidToPath.Erase(guid);
}
}
for (IResource* resource : resourcesToRelease) {

View File

@@ -0,0 +1,694 @@
#include "Rendering/Passes/BuiltinVolumetricPass.h"
#include "Components/GameObject.h"
#include "Core/Asset/ResourceManager.h"
#include "Debug/Logger.h"
#include "RHI/RHICommandList.h"
#include "RHI/RHIDevice.h"
#include "Rendering/Builtin/BuiltinPassLayoutUtils.h"
#include "Rendering/Detail/ShaderVariantUtils.h"
#include "Rendering/FrameData/RenderSceneData.h"
#include "Rendering/FrameData/VisibleVolumeItem.h"
#include "Rendering/RenderSurface.h"
#include "Resources/BuiltinResources.h"
#include "Resources/Material/Material.h"
#include "Resources/Shader/Shader.h"
#include "Resources/Volume/VolumeField.h"
#include <algorithm>
#include <cstddef>
namespace XCEngine {
namespace Rendering {
namespace Passes {
namespace {
bool IsDepthFormat(RHI::Format format) {
return format == RHI::Format::D24_UNorm_S8_UInt ||
format == RHI::Format::D32_Float;
}
Resources::ShaderKeywordSet ResolvePassKeywordSet(
const RenderSceneData& sceneData,
const Resources::Material* material) {
return Resources::CombineShaderKeywordSets(
sceneData.globalShaderKeywords,
material != nullptr ? material->GetKeywordSet() : Resources::ShaderKeywordSet());
}
const Resources::ShaderPass* FindCompatibleVolumePass(
const Resources::Shader& shader,
const RenderSceneData& sceneData,
const Resources::Material* material,
Resources::ShaderBackend backend) {
const Resources::ShaderKeywordSet keywordSet = ResolvePassKeywordSet(sceneData, material);
for (const Resources::ShaderPass& shaderPass : shader.GetPasses()) {
if (ShaderPassMatchesBuiltinPass(shaderPass, BuiltinMaterialPass::Volumetric) &&
::XCEngine::Rendering::Detail::ShaderPassHasGraphicsVariants(
shader,
shaderPass.name,
backend,
keywordSet)) {
return &shaderPass;
}
}
return nullptr;
}
RHI::GraphicsPipelineDesc CreatePipelineDesc(
RHI::RHIType backendType,
RHI::RHIPipelineLayout* pipelineLayout,
const Resources::Shader& shader,
const Resources::ShaderPass& shaderPass,
const Containers::String& passName,
const Resources::ShaderKeywordSet& keywordSet,
const Resources::Material* material,
RHI::Format renderTargetFormat,
RHI::Format depthStencilFormat) {
RHI::GraphicsPipelineDesc pipelineDesc = {};
pipelineDesc.pipelineLayout = pipelineLayout;
pipelineDesc.topologyType = static_cast<uint32_t>(RHI::PrimitiveTopologyType::Triangle);
pipelineDesc.renderTargetCount = 1;
pipelineDesc.renderTargetFormats[0] = static_cast<uint32_t>(renderTargetFormat);
pipelineDesc.depthStencilFormat = static_cast<uint32_t>(depthStencilFormat);
pipelineDesc.sampleCount = 1;
pipelineDesc.inputLayout = BuiltinVolumetricPass::BuildInputLayout();
ApplyResolvedRenderState(&shaderPass, material, pipelineDesc);
const Resources::ShaderBackend backend = ::XCEngine::Rendering::Detail::ToShaderBackend(backendType);
if (const Resources::ShaderStageVariant* vertexVariant =
shader.FindVariant(passName, Resources::ShaderType::Vertex, backend, keywordSet)) {
::XCEngine::Rendering::Detail::ApplyShaderStageVariant(
shader.GetPath(),
shaderPass,
backend,
*vertexVariant,
pipelineDesc.vertexShader);
}
if (const Resources::ShaderStageVariant* fragmentVariant =
shader.FindVariant(passName, Resources::ShaderType::Fragment, backend, keywordSet)) {
::XCEngine::Rendering::Detail::ApplyShaderStageVariant(
shader.GetPath(),
shaderPass,
backend,
*fragmentVariant,
pipelineDesc.fragmentShader);
}
return pipelineDesc;
}
Math::Bounds ResolveVolumeBounds(const Resources::VolumeField* volumeField) {
if (volumeField == nullptr) {
return Math::Bounds(Math::Vector3::Zero(), Math::Vector3::One());
}
const Resources::VolumeIndexBounds& indexBounds = volumeField->GetIndexBounds();
const Math::Vector3 indexMin(
static_cast<float>(indexBounds.minX),
static_cast<float>(indexBounds.minY),
static_cast<float>(indexBounds.minZ));
const Math::Vector3 indexMax(
static_cast<float>(indexBounds.maxX),
static_cast<float>(indexBounds.maxY),
static_cast<float>(indexBounds.maxZ));
const Math::Vector3 indexSize = indexMax - indexMin;
if (indexSize.SqrMagnitude() > Math::EPSILON) {
Math::Bounds bounds;
bounds.SetMinMax(indexMin, indexMax);
return bounds;
}
const Math::Bounds bounds = volumeField->GetBounds();
const Math::Vector3 size = bounds.extents * 2.0f;
if (size.SqrMagnitude() <= Math::EPSILON) {
return Math::Bounds(Math::Vector3::Zero(), Math::Vector3::One());
}
return bounds;
}
} // namespace
BuiltinVolumetricPass::~BuiltinVolumetricPass() {
Shutdown();
}
const char* BuiltinVolumetricPass::GetName() const {
return "BuiltinVolumetricPass";
}
RHI::InputLayoutDesc BuiltinVolumetricPass::BuildInputLayout() {
RHI::InputLayoutDesc inputLayout = {};
RHI::InputElementDesc position = {};
position.semanticName = "POSITION";
position.semanticIndex = 0;
position.format = static_cast<uint32_t>(RHI::Format::R32G32B32_Float);
position.inputSlot = 0;
position.alignedByteOffset = static_cast<uint32_t>(offsetof(Resources::StaticMeshVertex, position));
inputLayout.elements.push_back(position);
RHI::InputElementDesc normal = {};
normal.semanticName = "NORMAL";
normal.semanticIndex = 0;
normal.format = static_cast<uint32_t>(RHI::Format::R32G32B32_Float);
normal.inputSlot = 0;
normal.alignedByteOffset = static_cast<uint32_t>(offsetof(Resources::StaticMeshVertex, normal));
inputLayout.elements.push_back(normal);
RHI::InputElementDesc texcoord = {};
texcoord.semanticName = "TEXCOORD";
texcoord.semanticIndex = 0;
texcoord.format = static_cast<uint32_t>(RHI::Format::R32G32_Float);
texcoord.inputSlot = 0;
texcoord.alignedByteOffset = static_cast<uint32_t>(offsetof(Resources::StaticMeshVertex, uv0));
inputLayout.elements.push_back(texcoord);
return inputLayout;
}
bool BuiltinVolumetricPass::Initialize(const RenderContext& context) {
return EnsureInitialized(context);
}
bool BuiltinVolumetricPass::Execute(const RenderPassContext& context) {
if (!context.renderContext.IsValid()) {
return false;
}
if (context.sceneData.visibleVolumes.empty()) {
return true;
}
const std::vector<RHI::RHIResourceView*>& colorAttachments = context.surface.GetColorAttachments();
if (colorAttachments.empty() || colorAttachments[0] == nullptr || context.surface.GetDepthAttachment() == nullptr) {
return false;
}
const Math::RectInt renderArea = context.surface.GetRenderArea();
if (renderArea.width <= 0 || renderArea.height <= 0) {
return false;
}
if (!EnsureInitialized(context.renderContext)) {
return false;
}
RHI::RHICommandList* commandList = context.renderContext.commandList;
RHI::RHIResourceView* renderTarget = colorAttachments[0];
commandList->SetRenderTargets(1, &renderTarget, context.surface.GetDepthAttachment());
const RHI::Viewport viewport = {
static_cast<float>(renderArea.x),
static_cast<float>(renderArea.y),
static_cast<float>(renderArea.width),
static_cast<float>(renderArea.height),
0.0f,
1.0f
};
const RHI::Rect scissorRect = {
renderArea.x,
renderArea.y,
renderArea.x + renderArea.width,
renderArea.y + renderArea.height
};
commandList->SetViewport(viewport);
commandList->SetScissorRect(scissorRect);
commandList->SetPrimitiveTopology(RHI::PrimitiveTopology::TriangleList);
for (const VisibleVolumeItem& visibleVolume : context.sceneData.visibleVolumes) {
DrawVisibleVolume(context.renderContext, context.surface, context.sceneData, visibleVolume);
}
return true;
}
void BuiltinVolumetricPass::Shutdown() {
DestroyResources();
}
bool BuiltinVolumetricPass::EnsureInitialized(const RenderContext& context) {
if (!context.IsValid()) {
return false;
}
if (m_device == context.device &&
m_backendType == context.backendType &&
m_builtinCubeMesh.IsValid()) {
return true;
}
DestroyResources();
return CreateResources(context);
}
bool BuiltinVolumetricPass::CreateResources(const RenderContext& context) {
m_device = context.device;
m_backendType = context.backendType;
m_builtinCubeMesh = Resources::ResourceManager::Get().Load<Resources::Mesh>(
Resources::GetBuiltinPrimitiveMeshPath(Resources::BuiltinPrimitiveType::Cube));
if (!m_builtinCubeMesh.IsValid()) {
Debug::Logger::Get().Error(
Debug::LogCategory::Rendering,
"BuiltinVolumetricPass failed to load builtin cube mesh resource");
DestroyResources();
return false;
}
return true;
}
void BuiltinVolumetricPass::DestroyResources() {
m_resourceCache.Shutdown();
for (auto& pipelinePair : m_pipelineStates) {
if (pipelinePair.second != nullptr) {
pipelinePair.second->Shutdown();
delete pipelinePair.second;
}
}
m_pipelineStates.clear();
for (auto& descriptorSetPair : m_dynamicDescriptorSets) {
DestroyOwnedDescriptorSet(descriptorSetPair.second.descriptorSet);
}
m_dynamicDescriptorSets.clear();
for (auto& passLayoutPair : m_passResourceLayouts) {
DestroyPassResourceLayout(passLayoutPair.second);
}
m_passResourceLayouts.clear();
m_builtinCubeMesh.Reset();
m_device = nullptr;
m_backendType = RHI::RHIType::D3D12;
}
BuiltinVolumetricPass::ResolvedShaderPass BuiltinVolumetricPass::ResolveVolumeShaderPass(
const RenderSceneData& sceneData,
const Resources::Material* material) const {
ResolvedShaderPass resolved = {};
if (material == nullptr || material->GetShader() == nullptr) {
return resolved;
}
const Resources::Shader* shader = material->GetShader();
const Resources::ShaderBackend backend = ::XCEngine::Rendering::Detail::ToShaderBackend(m_backendType);
if (const Resources::ShaderPass* shaderPass =
FindCompatibleVolumePass(*shader, sceneData, material, backend)) {
resolved.shader = shader;
resolved.pass = shaderPass;
resolved.passName = shaderPass->name;
}
return resolved;
}
BuiltinVolumetricPass::PassResourceLayout* BuiltinVolumetricPass::GetOrCreatePassResourceLayout(
const RenderContext& context,
const ResolvedShaderPass& resolvedShaderPass) {
if (resolvedShaderPass.shader == nullptr || resolvedShaderPass.pass == nullptr) {
return nullptr;
}
PassLayoutKey passLayoutKey = {};
passLayoutKey.shader = resolvedShaderPass.shader;
passLayoutKey.passName = resolvedShaderPass.passName;
const auto existing = m_passResourceLayouts.find(passLayoutKey);
if (existing != m_passResourceLayouts.end()) {
return &existing->second;
}
PassResourceLayout passLayout = {};
auto failLayout = [this, &passLayout](const char* message) -> PassResourceLayout* {
Debug::Logger::Get().Error(Debug::LogCategory::Rendering, message);
DestroyPassResourceLayout(passLayout);
return nullptr;
};
BuiltinPassResourceBindingPlan bindingPlan = {};
Containers::String bindingPlanError;
if (!TryBuildBuiltinPassResourceBindingPlan(*resolvedShaderPass.pass, bindingPlan, &bindingPlanError)) {
const Containers::String contextualError =
Containers::String("BuiltinVolumetricPass failed to resolve pass resource bindings for shader='") +
resolvedShaderPass.shader->GetPath() +
"', pass='" + resolvedShaderPass.passName +
"': " + bindingPlanError +
". Bindings: " + DescribeShaderResourceBindings(resolvedShaderPass.pass->resources);
return failLayout(contextualError.CStr());
}
if (!bindingPlan.perObject.IsValid()) {
return failLayout("BuiltinVolumetricPass requires a PerObject resource binding");
}
if (!bindingPlan.volumeField.IsValid()) {
return failLayout("BuiltinVolumetricPass requires a VolumeField structured-buffer binding");
}
Containers::String setLayoutError;
if (!TryBuildBuiltinPassSetLayouts(bindingPlan, passLayout.setLayouts, &setLayoutError)) {
return failLayout(setLayoutError.CStr());
}
passLayout.firstDescriptorSet = bindingPlan.firstDescriptorSet;
passLayout.descriptorSetCount = bindingPlan.descriptorSetCount;
passLayout.perObject = bindingPlan.perObject;
passLayout.material = bindingPlan.material;
passLayout.volumeField = bindingPlan.volumeField;
std::vector<RHI::DescriptorSetLayoutDesc> nativeSetLayouts(passLayout.setLayouts.size());
for (size_t setIndex = 0; setIndex < passLayout.setLayouts.size(); ++setIndex) {
nativeSetLayouts[setIndex] = passLayout.setLayouts[setIndex].layout;
}
RHI::RHIPipelineLayoutDesc pipelineLayoutDesc = {};
pipelineLayoutDesc.setLayouts = nativeSetLayouts.empty() ? nullptr : nativeSetLayouts.data();
pipelineLayoutDesc.setLayoutCount = static_cast<uint32_t>(nativeSetLayouts.size());
passLayout.pipelineLayout = context.device->CreatePipelineLayout(pipelineLayoutDesc);
if (passLayout.pipelineLayout == nullptr) {
return failLayout("BuiltinVolumetricPass failed to create pipeline layout from shader pass resources");
}
const auto result = m_passResourceLayouts.emplace(passLayoutKey, passLayout);
PassResourceLayout& storedPassLayout = result.first->second;
RefreshBuiltinPassSetLayouts(storedPassLayout.setLayouts);
return &storedPassLayout;
}
RHI::RHIPipelineState* BuiltinVolumetricPass::GetOrCreatePipelineState(
const RenderContext& context,
const RenderSurface& surface,
const RenderSceneData& sceneData,
const Resources::Material* material) {
const ResolvedShaderPass resolvedShaderPass = ResolveVolumeShaderPass(sceneData, material);
if (resolvedShaderPass.shader == nullptr || resolvedShaderPass.pass == nullptr) {
return nullptr;
}
PassResourceLayout* passLayout = GetOrCreatePassResourceLayout(context, resolvedShaderPass);
if (passLayout == nullptr || passLayout->pipelineLayout == nullptr) {
return nullptr;
}
const Resources::ShaderKeywordSet keywordSet = ResolvePassKeywordSet(sceneData, material);
const std::vector<RHI::RHIResourceView*>& colorAttachments = surface.GetColorAttachments();
const RHI::Format renderTargetFormat =
(!colorAttachments.empty() && colorAttachments[0] != nullptr)
? colorAttachments[0]->GetFormat()
: RHI::Format::Unknown;
const RHI::Format depthStencilFormat =
surface.GetDepthAttachment() != nullptr
? surface.GetDepthAttachment()->GetFormat()
: RHI::Format::Unknown;
PipelineStateKey pipelineKey = {};
pipelineKey.renderState =
BuildStaticPipelineRenderStateKey(ResolveEffectiveRenderState(resolvedShaderPass.pass, material));
pipelineKey.shader = resolvedShaderPass.shader;
pipelineKey.passName = resolvedShaderPass.passName;
pipelineKey.keywordSignature = ::XCEngine::Rendering::Detail::BuildShaderKeywordSignature(keywordSet);
pipelineKey.renderTargetCount = renderTargetFormat != RHI::Format::Unknown ? 1u : 0u;
pipelineKey.renderTargetFormat = static_cast<uint32_t>(renderTargetFormat);
pipelineKey.depthStencilFormat = static_cast<uint32_t>(depthStencilFormat);
const auto existing = m_pipelineStates.find(pipelineKey);
if (existing != m_pipelineStates.end()) {
return existing->second;
}
const RHI::GraphicsPipelineDesc pipelineDesc =
CreatePipelineDesc(
context.backendType,
passLayout->pipelineLayout,
*resolvedShaderPass.shader,
*resolvedShaderPass.pass,
resolvedShaderPass.passName,
keywordSet,
material,
renderTargetFormat,
depthStencilFormat);
RHI::RHIPipelineState* pipelineState = context.device->CreatePipelineState(pipelineDesc);
if (pipelineState == nullptr || !pipelineState->IsValid()) {
if (pipelineState != nullptr) {
pipelineState->Shutdown();
delete pipelineState;
}
return nullptr;
}
m_pipelineStates.emplace(pipelineKey, pipelineState);
return pipelineState;
}
bool BuiltinVolumetricPass::CreateOwnedDescriptorSet(
const BuiltinPassSetLayoutMetadata& setLayout,
OwnedDescriptorSet& descriptorSet) {
RHI::DescriptorPoolDesc poolDesc = {};
poolDesc.type = setLayout.heapType;
poolDesc.descriptorCount = CountBuiltinPassHeapDescriptors(setLayout.heapType, setLayout.bindings);
poolDesc.shaderVisible = setLayout.shaderVisible;
descriptorSet.pool = m_device->CreateDescriptorPool(poolDesc);
if (descriptorSet.pool == nullptr) {
return false;
}
descriptorSet.set = descriptorSet.pool->AllocateSet(setLayout.layout);
if (descriptorSet.set == nullptr) {
DestroyOwnedDescriptorSet(descriptorSet);
return false;
}
return true;
}
BuiltinVolumetricPass::CachedDescriptorSet* BuiltinVolumetricPass::GetOrCreateDynamicDescriptorSet(
const PassLayoutKey& passLayoutKey,
const PassResourceLayout& passLayout,
const BuiltinPassSetLayoutMetadata& setLayout,
Core::uint32 setIndex,
Core::uint64 objectId,
const Resources::Material* material,
const Resources::VolumeField* volumeField,
const MaterialConstantPayloadView& materialConstants,
RHI::RHIResourceView* volumeFieldView) {
DynamicDescriptorSetKey key = {};
key.passLayout = passLayoutKey;
key.setIndex = setIndex;
key.objectId = objectId;
key.material = material;
key.volumeField = volumeField;
CachedDescriptorSet& cachedDescriptorSet = m_dynamicDescriptorSets[key];
if (cachedDescriptorSet.descriptorSet.set == nullptr) {
if (!CreateOwnedDescriptorSet(setLayout, cachedDescriptorSet.descriptorSet)) {
return nullptr;
}
}
const Core::uint64 materialVersion = material != nullptr ? material->GetChangeVersion() : 0u;
if (setLayout.usesMaterial) {
if (!passLayout.material.IsValid() || passLayout.material.set != setIndex || !materialConstants.IsValid()) {
return nullptr;
}
if (cachedDescriptorSet.materialVersion != materialVersion) {
cachedDescriptorSet.descriptorSet.set->WriteConstant(
passLayout.material.binding,
materialConstants.data,
materialConstants.size);
}
}
if (setLayout.usesVolumeField) {
if (volumeFieldView == nullptr ||
!passLayout.volumeField.IsValid() ||
passLayout.volumeField.set != setIndex) {
return nullptr;
}
if (cachedDescriptorSet.volumeFieldView != volumeFieldView) {
cachedDescriptorSet.descriptorSet.set->Update(
passLayout.volumeField.binding,
volumeFieldView);
}
}
cachedDescriptorSet.materialVersion = materialVersion;
cachedDescriptorSet.volumeFieldView = volumeFieldView;
return &cachedDescriptorSet;
}
void BuiltinVolumetricPass::DestroyOwnedDescriptorSet(OwnedDescriptorSet& descriptorSet) {
if (descriptorSet.set != nullptr) {
descriptorSet.set->Shutdown();
delete descriptorSet.set;
descriptorSet.set = nullptr;
}
if (descriptorSet.pool != nullptr) {
descriptorSet.pool->Shutdown();
delete descriptorSet.pool;
descriptorSet.pool = nullptr;
}
}
void BuiltinVolumetricPass::DestroyPassResourceLayout(PassResourceLayout& passLayout) {
if (passLayout.pipelineLayout != nullptr) {
passLayout.pipelineLayout->Shutdown();
delete passLayout.pipelineLayout;
passLayout.pipelineLayout = nullptr;
}
passLayout.setLayouts.clear();
passLayout.firstDescriptorSet = 0;
passLayout.descriptorSetCount = 0;
passLayout.perObject = {};
passLayout.material = {};
passLayout.volumeField = {};
}
bool BuiltinVolumetricPass::DrawVisibleVolume(
const RenderContext& context,
const RenderSurface& surface,
const RenderSceneData& sceneData,
const VisibleVolumeItem& visibleVolume) {
if (m_builtinCubeMesh.Get() == nullptr ||
visibleVolume.gameObject == nullptr ||
visibleVolume.volumeField == nullptr ||
visibleVolume.material == nullptr ||
visibleVolume.volumeField->GetStorageKind() != Resources::VolumeStorageKind::NanoVDB) {
return false;
}
const RenderResourceCache::CachedMesh* cachedMesh =
m_resourceCache.GetOrCreateMesh(m_device, m_builtinCubeMesh.Get());
const RenderResourceCache::CachedVolumeField* cachedVolume =
m_resourceCache.GetOrCreateVolumeField(m_device, visibleVolume.volumeField);
if (cachedMesh == nullptr ||
cachedMesh->vertexBufferView == nullptr ||
cachedVolume == nullptr ||
cachedVolume->shaderResourceView == nullptr) {
return false;
}
const Resources::Material* material = visibleVolume.material;
const ResolvedShaderPass resolvedShaderPass = ResolveVolumeShaderPass(sceneData, material);
if (resolvedShaderPass.shader == nullptr || resolvedShaderPass.pass == nullptr) {
return false;
}
PassLayoutKey passLayoutKey = {};
passLayoutKey.shader = resolvedShaderPass.shader;
passLayoutKey.passName = resolvedShaderPass.passName;
PassResourceLayout* passLayout = GetOrCreatePassResourceLayout(context, resolvedShaderPass);
RHI::RHIPipelineState* pipelineState = GetOrCreatePipelineState(context, surface, sceneData, material);
if (passLayout == nullptr || pipelineState == nullptr) {
return false;
}
const Resources::MaterialRenderState effectiveRenderState =
ResolveEffectiveRenderState(resolvedShaderPass.pass, material);
const MaterialConstantPayloadView materialConstants = ResolveSchemaMaterialConstantPayload(material);
if (passLayout->material.IsValid() && !materialConstants.IsValid()) {
return false;
}
RHI::RHICommandList* commandList = context.commandList;
commandList->SetPipelineState(pipelineState);
RHI::RHIResourceView* vertexBuffers[] = { cachedMesh->vertexBufferView };
const uint64_t offsets[] = { 0u };
const uint32_t strides[] = { cachedMesh->vertexStride };
commandList->SetVertexBuffers(0, 1, vertexBuffers, offsets, strides);
if (cachedMesh->indexBufferView != nullptr) {
commandList->SetIndexBuffer(cachedMesh->indexBufferView, 0u);
}
const Math::Bounds volumeBounds = ResolveVolumeBounds(visibleVolume.volumeField);
const PerObjectConstants perObjectConstants = {
sceneData.cameraData.projection,
sceneData.cameraData.view,
visibleVolume.localToWorld.Transpose(),
visibleVolume.localToWorld.Inverse().Transpose(),
Math::Vector4(sceneData.cameraData.worldPosition, 1.0f),
Math::Vector4(volumeBounds.GetMin(), 0.0f),
Math::Vector4(volumeBounds.GetMax(), 0.0f)
};
if (passLayout->descriptorSetCount > 0u) {
std::vector<RHI::RHIDescriptorSet*> descriptorSets(passLayout->descriptorSetCount, nullptr);
for (Core::uint32 descriptorOffset = 0u; descriptorOffset < passLayout->descriptorSetCount; ++descriptorOffset) {
const Core::uint32 setIndex = passLayout->firstDescriptorSet + descriptorOffset;
if (setIndex >= passLayout->setLayouts.size()) {
return false;
}
const BuiltinPassSetLayoutMetadata& setLayout = passLayout->setLayouts[setIndex];
if (!(setLayout.usesPerObject || setLayout.usesMaterial || setLayout.usesVolumeField)) {
return false;
}
const Core::uint64 objectId =
(setLayout.usesPerObject && visibleVolume.gameObject != nullptr)
? visibleVolume.gameObject->GetID()
: 0u;
const Resources::Material* materialKey = setLayout.usesMaterial ? material : nullptr;
const Resources::VolumeField* volumeFieldKey =
setLayout.usesVolumeField ? visibleVolume.volumeField : nullptr;
CachedDescriptorSet* cachedDescriptorSet = GetOrCreateDynamicDescriptorSet(
passLayoutKey,
*passLayout,
setLayout,
setIndex,
objectId,
materialKey,
volumeFieldKey,
materialConstants,
cachedVolume->shaderResourceView);
if (cachedDescriptorSet == nullptr || cachedDescriptorSet->descriptorSet.set == nullptr) {
return false;
}
RHI::RHIDescriptorSet* descriptorSet = cachedDescriptorSet->descriptorSet.set;
if (setLayout.usesPerObject) {
if (!passLayout->perObject.IsValid() || passLayout->perObject.set != setIndex) {
return false;
}
descriptorSet->WriteConstant(
passLayout->perObject.binding,
&perObjectConstants,
sizeof(perObjectConstants));
}
descriptorSets[descriptorOffset] = descriptorSet;
}
commandList->SetGraphicsDescriptorSets(
passLayout->firstDescriptorSet,
passLayout->descriptorSetCount,
descriptorSets.data(),
passLayout->pipelineLayout);
}
ApplyDynamicRenderState(effectiveRenderState, *commandList);
if (cachedMesh->indexBufferView != nullptr && cachedMesh->indexCount > 0u) {
commandList->DrawIndexed(cachedMesh->indexCount, 1u, 0u, 0u, 0u);
} else if (cachedMesh->vertexCount > 0u) {
commandList->Draw(cachedMesh->vertexCount, 1u, 0u, 0u);
}
return true;
}
} // namespace Passes
} // namespace Rendering
} // namespace XCEngine

View File

@@ -17,7 +17,10 @@ bool VolumeField::Create(VolumeStorageKind storageKind,
const void* payload,
size_t payloadSize,
const Math::Bounds& bounds,
const Math::Vector3& voxelSize) {
const Math::Vector3& voxelSize,
const VolumeIndexBounds& indexBounds,
Core::uint32 gridType,
Core::uint32 gridClass) {
if (payload == nullptr || payloadSize == 0) {
return false;
}
@@ -25,6 +28,9 @@ bool VolumeField::Create(VolumeStorageKind storageKind,
m_storageKind = storageKind;
m_bounds = bounds;
m_voxelSize = voxelSize;
m_indexBounds = indexBounds;
m_gridType = gridType;
m_gridClass = gridClass;
m_payload.Resize(payloadSize);
std::memcpy(m_payload.Data(), payload, payloadSize);
m_isValid = true;

View File

@@ -4,6 +4,13 @@
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Resources/Volume/VolumeField.h>
#if defined(XCENGINE_HAS_NANOVDB)
#include <nanovdb/GridHandle.h>
#include <nanovdb/HostBuffer.h>
#include <nanovdb/io/IO.h>
#endif
#include <cmath>
#include <cstring>
#include <filesystem>
#include <fstream>
@@ -26,6 +33,9 @@ LoadResult CreateVolumeFieldResource(const Containers::String& path,
VolumeStorageKind storageKind,
const Math::Bounds& bounds,
const Math::Vector3& voxelSize,
const VolumeIndexBounds& indexBounds,
Core::uint32 gridType,
Core::uint32 gridClass,
const void* payload,
size_t payloadSize) {
auto* volumeField = new VolumeField();
@@ -37,7 +47,15 @@ LoadResult CreateVolumeFieldResource(const Containers::String& path,
params.memorySize = payloadSize;
volumeField->Initialize(params);
if (!volumeField->Create(storageKind, payload, payloadSize, bounds, voxelSize)) {
if (!volumeField->Create(
storageKind,
payload,
payloadSize,
bounds,
voxelSize,
indexBounds,
gridType,
gridClass)) {
delete volumeField;
return LoadResult(Containers::String("Failed to create volume field resource: ") + path);
}
@@ -45,7 +63,7 @@ LoadResult CreateVolumeFieldResource(const Containers::String& path,
return LoadResult(volumeField);
}
LoadResult LoadVolumeFieldArtifact(const Containers::String& path) {
std::filesystem::path ResolveVolumeFieldPath(const Containers::String& path) {
std::filesystem::path resolvedPath(path.CStr());
if (!resolvedPath.is_absolute() && !std::filesystem::exists(resolvedPath)) {
const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot();
@@ -54,6 +72,12 @@ LoadResult LoadVolumeFieldArtifact(const Containers::String& path) {
}
}
return resolvedPath.lexically_normal();
}
LoadResult LoadVolumeFieldArtifact(const Containers::String& path) {
const std::filesystem::path resolvedPath = ResolveVolumeFieldPath(path);
std::ifstream input(resolvedPath, std::ios::binary);
if (!input.is_open()) {
return LoadResult(Containers::String("Failed to read volume artifact: ") + path);
@@ -66,7 +90,7 @@ LoadResult LoadVolumeFieldArtifact(const Containers::String& path) {
}
const bool validHeader =
std::memcmp(header.magic, "XCVOL01", 7) == 0 &&
std::memcmp(header.magic, "XCVOL02", 7) == 0 &&
header.schemaVersion == kVolumeFieldArtifactSchemaVersion &&
header.payloadSize > 0;
if (!validHeader) {
@@ -83,14 +107,118 @@ LoadResult LoadVolumeFieldArtifact(const Containers::String& path) {
Math::Bounds bounds;
bounds.SetMinMax(header.boundsMin, header.boundsMax);
VolumeIndexBounds indexBounds = {};
indexBounds.minX = header.indexBoundsMin[0];
indexBounds.minY = header.indexBoundsMin[1];
indexBounds.minZ = header.indexBoundsMin[2];
indexBounds.maxX = header.indexBoundsMax[0];
indexBounds.maxY = header.indexBoundsMax[1];
indexBounds.maxZ = header.indexBoundsMax[2];
return CreateVolumeFieldResource(path,
static_cast<VolumeStorageKind>(header.storageKind),
bounds,
header.voxelSize,
indexBounds,
header.gridType,
header.gridClass,
payload.Data(),
payload.Size());
}
#if defined(XCENGINE_HAS_NANOVDB)
Math::Vector3 ToEngineVector3(const nanovdb::Vec3d& value) {
return Math::Vector3(
static_cast<float>(value[0]),
static_cast<float>(value[1]),
static_cast<float>(value[2]));
}
VolumeIndexBounds ToEngineIndexBounds(const nanovdb::CoordBBox& value) {
VolumeIndexBounds bounds = {};
bounds.minX = value.min()[0];
bounds.minY = value.min()[1];
bounds.minZ = value.min()[2];
bounds.maxX = value.max()[0];
bounds.maxY = value.max()[1];
bounds.maxZ = value.max()[2];
return bounds;
}
bool IsFiniteVector3(const Math::Vector3& value) {
return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z);
}
Math::Bounds BuildNanoVDBBounds(const nanovdb::GridMetaData& metadata) {
const Math::Vector3 minPoint = ToEngineVector3(metadata.worldBBox().min());
const Math::Vector3 maxPoint = ToEngineVector3(metadata.worldBBox().max());
if (!IsFiniteVector3(minPoint) || !IsFiniteVector3(maxPoint)) {
return Math::Bounds();
}
Math::Bounds bounds;
bounds.SetMinMax(
Math::Vector3(
std::min(minPoint.x, maxPoint.x),
std::min(minPoint.y, maxPoint.y),
std::min(minPoint.z, maxPoint.z)),
Math::Vector3(
std::max(minPoint.x, maxPoint.x),
std::max(minPoint.y, maxPoint.y),
std::max(minPoint.z, maxPoint.z)));
return bounds;
}
LoadResult LoadNanoVDBSourceFile(const Containers::String& path) {
const std::filesystem::path resolvedPath = ResolveVolumeFieldPath(path);
if (!std::filesystem::exists(resolvedPath)) {
return LoadResult(Containers::String("Failed to read file: ") + path);
}
try {
nanovdb::GridHandle<nanovdb::HostBuffer> handle =
nanovdb::io::readGrid<nanovdb::HostBuffer>(resolvedPath.string());
if (!handle || handle.data() == nullptr || handle.bufferSize() == 0u) {
return LoadResult(Containers::String("Failed to parse NanoVDB grid payload: ") + path);
}
const nanovdb::GridMetaData* metadata = handle.gridMetaData();
Math::Bounds bounds;
Math::Vector3 voxelSize = Math::Vector3::Zero();
VolumeIndexBounds indexBounds = {};
Core::uint32 gridType = 0u;
Core::uint32 gridClass = 0u;
if (metadata != nullptr && metadata->isValid()) {
bounds = BuildNanoVDBBounds(*metadata);
voxelSize = ToEngineVector3(metadata->voxelSize());
indexBounds = ToEngineIndexBounds(metadata->indexBBox());
gridType = static_cast<Core::uint32>(metadata->gridType());
gridClass = static_cast<Core::uint32>(metadata->gridClass());
if (!IsFiniteVector3(voxelSize)) {
voxelSize = Math::Vector3::Zero();
}
}
return CreateVolumeFieldResource(
path,
VolumeStorageKind::NanoVDB,
bounds,
voxelSize,
indexBounds,
gridType,
gridClass,
handle.data(),
static_cast<size_t>(handle.bufferSize()));
} catch (const std::exception& e) {
return LoadResult(
Containers::String("Failed to parse NanoVDB file: ") +
path +
" - " +
e.what());
}
}
#endif
} // namespace
VolumeFieldLoader::VolumeFieldLoader() = default;
@@ -121,17 +249,12 @@ LoadResult VolumeFieldLoader::Load(const Containers::String& path, const ImportS
return LoadVolumeFieldArtifact(path);
}
Containers::Array<Core::uint8> payload = ReadFileData(path);
if (payload.Empty()) {
return LoadResult(Containers::String("Failed to read file: ") + path);
}
return CreateVolumeFieldResource(path,
VolumeStorageKind::NanoVDB,
Math::Bounds(),
Math::Vector3::Zero(),
payload.Data(),
payload.Size());
#if defined(XCENGINE_HAS_NANOVDB)
return LoadNanoVDBSourceFile(path);
#else
return LoadResult(
Containers::String("NanoVDB source-file support is unavailable in this build: ") + path);
#endif
}
ImportSettings* VolumeFieldLoader::GetDefaultSettings() const {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,269 @@
#include <gtest/gtest.h>
#include "../RenderingIntegrationImageAssert.h"
#include "../RenderingIntegrationMain.h"
#include <XCEngine/Components/CameraComponent.h>
#include <XCEngine/Components/GameObject.h>
#include <XCEngine/Components/VolumeRendererComponent.h>
#include <XCEngine/Core/Asset/IResource.h>
#include <XCEngine/Core/Math/Color.h>
#include <XCEngine/Core/Math/Vector2.h>
#include <XCEngine/Core/Math/Vector3.h>
#include <XCEngine/Rendering/RenderContext.h>
#include <XCEngine/Rendering/RenderSurface.h>
#include <XCEngine/Rendering/Execution/SceneRenderer.h>
#include <XCEngine/Rendering/Extraction/RenderSceneExtractor.h>
#include <XCEngine/Resources/BuiltinResources.h>
#include <XCEngine/Resources/Material/Material.h>
#include <XCEngine/Resources/Shader/Shader.h>
#include <XCEngine/Resources/Volume/VolumeField.h>
#include <XCEngine/Resources/Volume/VolumeFieldLoader.h>
#include <XCEngine/RHI/RHITexture.h>
#include <XCEngine/Scene/Scene.h>
#include "../../../RHI/integration/fixtures/RHIIntegrationFixture.h"
#include <filesystem>
#include <memory>
#include <vector>
using namespace XCEngine::Components;
using namespace XCEngine::Math;
using namespace XCEngine::Rendering;
using namespace XCEngine::Resources;
using namespace XCEngine::RHI;
using namespace XCEngine::RHI::Integration;
namespace {
constexpr const char* kD3D12Screenshot = "volume_scene_d3d12.ppm";
constexpr uint32_t kFrameWidth = 1280;
constexpr uint32_t kFrameHeight = 720;
constexpr const char* kVolumeFixtureRelativePath = "Res/Volumes/cloud.nvdb";
Material* CreateVolumetricMaterial() {
auto* material = new Material();
IResource::ConstructParams params = {};
params.name = "VolumeMaterial";
params.path = "Tests/Rendering/VolumeScene/Volume.material";
params.guid = ResourceGUID::Generate(params.path);
material->Initialize(params);
material->SetShader(ResourceManager::Get().Load<Shader>(GetBuiltinVolumetricShaderPath()));
material->SetRenderQueue(MaterialRenderQueue::Transparent);
material->SetFloat4("_Tint", Vector4(1.0f, 1.0f, 1.0f, 1.0f));
material->SetFloat("_DensityScale", 0.2f);
material->SetFloat("_StepSize", 1.0f);
material->SetFloat("_MaxSteps", 2000.0f);
material->SetFloat("_AmbientStrength", 0.005f);
material->SetFloat4("_LightDirection", Vector4(0.5f, 0.8f, 0.3f, 0.0f));
material->SetFloat("_LightSamples", 8.0f);
return material;
}
const char* GetScreenshotFilename(RHIType backendType) {
switch (backendType) {
case RHIType::D3D12:
default:
return kD3D12Screenshot;
}
}
class VolumeSceneTest : public RHIIntegrationFixture {
protected:
void SetUp() override;
void TearDown() override;
void RenderFrame() override;
private:
RHIResourceView* GetCurrentBackBufferView();
std::unique_ptr<Scene> mScene;
std::unique_ptr<SceneRenderer> mSceneRenderer;
std::vector<RHIResourceView*> mBackBufferViews;
RHITexture* mDepthTexture = nullptr;
RHIResourceView* mDepthView = nullptr;
Material* mVolumeMaterial = nullptr;
VolumeField* mVolumeField = nullptr;
};
void VolumeSceneTest::SetUp() {
RHIIntegrationFixture::SetUp();
mSceneRenderer = std::make_unique<SceneRenderer>();
mScene = std::make_unique<Scene>("VolumeScene");
mVolumeMaterial = CreateVolumetricMaterial();
ASSERT_NE(mVolumeMaterial, nullptr);
const std::filesystem::path fixturePath =
RenderingIntegrationTestUtils::ResolveRuntimePath(kVolumeFixtureRelativePath);
ASSERT_TRUE(std::filesystem::exists(fixturePath)) << fixturePath.string();
VolumeFieldLoader volumeFieldLoader;
LoadResult volumeResult = volumeFieldLoader.Load(fixturePath.string().c_str());
ASSERT_TRUE(volumeResult);
ASSERT_NE(volumeResult.resource, nullptr);
mVolumeField = static_cast<VolumeField*>(volumeResult.resource);
GameObject* cameraObject = mScene->CreateGameObject("MainCamera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
camera->SetPrimary(true);
camera->SetFieldOfView(45.0f);
camera->SetNearClipPlane(0.1f);
camera->SetFarClipPlane(5000.0f);
camera->SetClearColor(XCEngine::Math::Color(0.03f, 0.04f, 0.06f, 1.0f));
cameraObject->GetTransform()->SetLocalPosition(Vector3(-10.0f, 300.0f, -1200.0f));
cameraObject->GetTransform()->LookAt(Vector3(-10.0f, 73.0f, 0.0f));
GameObject* volumeObject = mScene->CreateGameObject("CloudVolume");
auto* volumeRenderer = volumeObject->AddComponent<VolumeRendererComponent>();
volumeRenderer->SetVolumeField(mVolumeField);
volumeRenderer->SetMaterial(mVolumeMaterial);
volumeRenderer->SetCastShadows(false);
volumeRenderer->SetReceiveShadows(false);
TextureDesc depthDesc = {};
depthDesc.width = kFrameWidth;
depthDesc.height = kFrameHeight;
depthDesc.depth = 1;
depthDesc.mipLevels = 1;
depthDesc.arraySize = 1;
depthDesc.format = static_cast<uint32_t>(Format::D24_UNorm_S8_UInt);
depthDesc.textureType = static_cast<uint32_t>(XCEngine::RHI::TextureType::Texture2D);
depthDesc.sampleCount = 1;
depthDesc.sampleQuality = 0;
depthDesc.flags = 0;
mDepthTexture = GetDevice()->CreateTexture(depthDesc);
ASSERT_NE(mDepthTexture, nullptr);
ResourceViewDesc depthViewDesc = {};
depthViewDesc.format = static_cast<uint32_t>(Format::D24_UNorm_S8_UInt);
depthViewDesc.dimension = ResourceViewDimension::Texture2D;
depthViewDesc.mipLevel = 0;
mDepthView = GetDevice()->CreateDepthStencilView(mDepthTexture, depthViewDesc);
ASSERT_NE(mDepthView, nullptr);
mBackBufferViews.resize(2, nullptr);
RenderSceneExtractor extractor;
const RenderSceneData sceneData = extractor.Extract(*mScene, nullptr, kFrameWidth, kFrameHeight);
ASSERT_EQ(sceneData.visibleItems.size(), 0u);
ASSERT_EQ(sceneData.visibleVolumes.size(), 1u);
}
void VolumeSceneTest::TearDown() {
mSceneRenderer.reset();
if (mDepthView != nullptr) {
mDepthView->Shutdown();
delete mDepthView;
mDepthView = nullptr;
}
if (mDepthTexture != nullptr) {
mDepthTexture->Shutdown();
delete mDepthTexture;
mDepthTexture = nullptr;
}
for (RHIResourceView*& backBufferView : mBackBufferViews) {
if (backBufferView != nullptr) {
backBufferView->Shutdown();
delete backBufferView;
backBufferView = nullptr;
}
}
mBackBufferViews.clear();
mScene.reset();
delete mVolumeField;
mVolumeField = nullptr;
delete mVolumeMaterial;
mVolumeMaterial = nullptr;
RHIIntegrationFixture::TearDown();
}
RHIResourceView* VolumeSceneTest::GetCurrentBackBufferView() {
const int backBufferIndex = GetCurrentBackBufferIndex();
if (backBufferIndex < 0) {
return nullptr;
}
if (static_cast<size_t>(backBufferIndex) >= mBackBufferViews.size()) {
mBackBufferViews.resize(static_cast<size_t>(backBufferIndex) + 1, nullptr);
}
if (mBackBufferViews[backBufferIndex] == nullptr) {
ResourceViewDesc viewDesc = {};
viewDesc.format = static_cast<uint32_t>(Format::R8G8B8A8_UNorm);
viewDesc.dimension = ResourceViewDimension::Texture2D;
viewDesc.mipLevel = 0;
mBackBufferViews[backBufferIndex] = GetDevice()->CreateRenderTargetView(GetCurrentBackBuffer(), viewDesc);
}
return mBackBufferViews[backBufferIndex];
}
void VolumeSceneTest::RenderFrame() {
ASSERT_NE(mScene, nullptr);
ASSERT_NE(mSceneRenderer, nullptr);
RHICommandList* commandList = GetCommandList();
ASSERT_NE(commandList, nullptr);
commandList->Reset();
RenderSurface surface(kFrameWidth, kFrameHeight);
surface.SetColorAttachment(GetCurrentBackBufferView());
surface.SetDepthAttachment(mDepthView);
RenderContext renderContext = {};
renderContext.device = GetDevice();
renderContext.commandList = commandList;
renderContext.commandQueue = GetCommandQueue();
renderContext.backendType = GetBackendType();
ASSERT_TRUE(mSceneRenderer->Render(*mScene, nullptr, renderContext, surface));
commandList->Close();
void* commandLists[] = { commandList };
GetCommandQueue()->ExecuteCommandLists(1, commandLists);
}
TEST_P(VolumeSceneTest, RenderNanoVdbVolumeScene) {
RHICommandQueue* commandQueue = GetCommandQueue();
RHISwapChain* swapChain = GetSwapChain();
ASSERT_NE(swapChain, nullptr);
const int targetFrameCount = 30;
const char* screenshotFilename = GetScreenshotFilename(GetBackendType());
for (int frameCount = 0; frameCount <= targetFrameCount; ++frameCount) {
if (frameCount > 0) {
commandQueue->WaitForPreviousFrame();
}
BeginRender();
RenderFrame();
if (frameCount >= targetFrameCount) {
commandQueue->WaitForIdle();
ASSERT_TRUE(TakeScreenshot(screenshotFilename));
ASSERT_TRUE(CompareWithGoldenTemplate(screenshotFilename, "GT.ppm", 0.0f));
break;
}
swapChain->Present(0, 0);
}
}
} // namespace
INSTANTIATE_TEST_SUITE_P(D3D12, VolumeSceneTest, ::testing::Values(RHIType::D3D12));
GTEST_API_ int main(int argc, char** argv) {
return RunRenderingIntegrationTestMain(argc, argv);
}

View File

@@ -26,7 +26,10 @@ TEST(VolumeField, CreatePreservesPayloadAndMetadata) {
payload,
sizeof(payload),
bounds,
XCEngine::Math::Vector3(0.5f, 0.25f, 0.125f)));
XCEngine::Math::Vector3(0.5f, 0.25f, 0.125f),
VolumeIndexBounds{ -4, -5, -6, 7, 8, 9 },
1u,
2u));
EXPECT_TRUE(volumeField.IsValid());
EXPECT_EQ(volumeField.GetType(), ResourceType::VolumeField);
@@ -35,6 +38,9 @@ TEST(VolumeField, CreatePreservesPayloadAndMetadata) {
EXPECT_EQ(volumeField.GetBounds().GetMin(), XCEngine::Math::Vector3(-1.0f, -2.0f, -3.0f));
EXPECT_EQ(volumeField.GetBounds().GetMax(), XCEngine::Math::Vector3(4.0f, 5.0f, 6.0f));
EXPECT_EQ(volumeField.GetVoxelSize(), XCEngine::Math::Vector3(0.5f, 0.25f, 0.125f));
EXPECT_EQ(volumeField.GetIndexBounds(), (VolumeIndexBounds{ -4, -5, -6, 7, 8, 9 }));
EXPECT_EQ(volumeField.GetGridType(), 1u);
EXPECT_EQ(volumeField.GetGridClass(), 2u);
EXPECT_EQ(static_cast<const unsigned char*>(volumeField.GetPayloadData())[0], 1u);
EXPECT_GT(volumeField.GetMemorySize(), sizeof(VolumeField));
}

View File

@@ -3,33 +3,100 @@
#include <XCEngine/Core/Asset/AssetDatabase.h>
#include <XCEngine/Core/Asset/AssetRef.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/Math/Bounds.h>
#include <XCEngine/Core/Math/Vector3.h>
#include <XCEngine/Resources/Volume/VolumeField.h>
#include <XCEngine/Resources/Volume/VolumeFieldLoader.h>
#if defined(XCENGINE_HAS_NANOVDB)
#include <nanovdb/GridHandle.h>
#include <nanovdb/HostBuffer.h>
#include <nanovdb/io/IO.h>
#endif
#include <chrono>
#include <filesystem>
#include <fstream>
#include <thread>
#include <vector>
using namespace XCEngine::Resources;
namespace {
std::vector<unsigned char> MakeTestNanoVDBPayload() {
return {
0x4E, 0x56, 0x44, 0x42,
0x10, 0x20, 0x30, 0x40,
0x01, 0x03, 0x05, 0x07,
0xAA, 0xBB, 0xCC, 0xDD
};
struct GeneratedNanoVDBVolume {
size_t payloadSize = 0u;
XCEngine::Math::Bounds bounds;
XCEngine::Math::Vector3 voxelSize = XCEngine::Math::Vector3::Zero();
VolumeIndexBounds indexBounds = {};
XCEngine::Core::uint32 gridType = 0u;
XCEngine::Core::uint32 gridClass = 0u;
};
std::filesystem::path GetCloudVolumePath() {
return std::filesystem::path(XCENGINE_TEST_CLOUD_NVDB_PATH);
}
void WriteBinaryFile(const std::filesystem::path& path, const std::vector<unsigned char>& bytes) {
std::ofstream output(path, std::ios::binary | std::ios::trunc);
output.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
void ExpectVector3Near(
const XCEngine::Math::Vector3& actual,
const XCEngine::Math::Vector3& expected,
float epsilon = 1e-4f) {
EXPECT_NEAR(actual.x, expected.x, epsilon);
EXPECT_NEAR(actual.y, expected.y, epsilon);
EXPECT_NEAR(actual.z, expected.z, epsilon);
}
void ExpectIndexBoundsEq(
const VolumeIndexBounds& actual,
const VolumeIndexBounds& expected) {
EXPECT_EQ(actual.minX, expected.minX);
EXPECT_EQ(actual.minY, expected.minY);
EXPECT_EQ(actual.minZ, expected.minZ);
EXPECT_EQ(actual.maxX, expected.maxX);
EXPECT_EQ(actual.maxY, expected.maxY);
EXPECT_EQ(actual.maxZ, expected.maxZ);
}
#if defined(XCENGINE_HAS_NANOVDB)
XCEngine::Math::Vector3 ToVector3(const nanovdb::Vec3d& value) {
return XCEngine::Math::Vector3(
static_cast<float>(value[0]),
static_cast<float>(value[1]),
static_cast<float>(value[2]));
}
VolumeIndexBounds ToIndexBounds(const nanovdb::CoordBBox& value) {
VolumeIndexBounds bounds = {};
bounds.minX = value.min()[0];
bounds.minY = value.min()[1];
bounds.minZ = value.min()[2];
bounds.maxX = value.max()[0];
bounds.maxY = value.max()[1];
bounds.maxZ = value.max()[2];
return bounds;
}
GeneratedNanoVDBVolume ReadTestNanoVDBFileMetadata(const std::filesystem::path& path) {
nanovdb::GridHandle<nanovdb::HostBuffer> handle =
nanovdb::io::readGrid<nanovdb::HostBuffer>(path.string());
GeneratedNanoVDBVolume generated;
generated.payloadSize = static_cast<size_t>(handle.bufferSize());
const nanovdb::GridMetaData* metadata = handle.gridMetaData();
if (metadata != nullptr) {
generated.bounds.SetMinMax(
ToVector3(metadata->worldBBox().min()),
ToVector3(metadata->worldBBox().max()));
generated.voxelSize = ToVector3(metadata->voxelSize());
generated.indexBounds = ToIndexBounds(metadata->indexBBox());
generated.gridType = static_cast<XCEngine::Core::uint32>(metadata->gridType());
generated.gridClass = static_cast<XCEngine::Core::uint32>(metadata->gridClass());
}
return generated;
}
#endif
TEST(VolumeFieldLoader, GetResourceType) {
VolumeFieldLoader loader;
EXPECT_EQ(loader.GetResourceType(), ResourceType::VolumeField);
@@ -48,12 +115,14 @@ TEST(VolumeFieldLoader, LoadInvalidPath) {
EXPECT_FALSE(result);
}
TEST(VolumeFieldLoader, LoadSourceNanoVDBBlob) {
#if defined(XCENGINE_HAS_NANOVDB)
TEST(VolumeFieldLoader, LoadSourceNanoVDBGridPayload) {
namespace fs = std::filesystem;
const fs::path volumePath = fs::temp_directory_path() / "xc_volume_loader_source_test.nvdb";
const std::vector<unsigned char> bytes = MakeTestNanoVDBPayload();
WriteBinaryFile(volumePath, bytes);
const fs::path volumePath = GetCloudVolumePath();
ASSERT_TRUE(fs::exists(volumePath));
const GeneratedNanoVDBVolume generated = ReadTestNanoVDBFileMetadata(volumePath);
VolumeFieldLoader loader;
LoadResult result = loader.Load(volumePath.string().c_str());
@@ -62,13 +131,19 @@ TEST(VolumeFieldLoader, LoadSourceNanoVDBBlob) {
auto* volumeField = static_cast<VolumeField*>(result.resource);
EXPECT_EQ(volumeField->GetStorageKind(), VolumeStorageKind::NanoVDB);
EXPECT_EQ(volumeField->GetPayloadSize(), bytes.size());
EXPECT_EQ(static_cast<const unsigned char*>(volumeField->GetPayloadData())[0], bytes[0]);
EXPECT_EQ(volumeField->GetPayloadSize(), generated.payloadSize);
ExpectVector3Near(volumeField->GetBounds().GetMin(), generated.bounds.GetMin());
ExpectVector3Near(volumeField->GetBounds().GetMax(), generated.bounds.GetMax());
ExpectVector3Near(volumeField->GetVoxelSize(), generated.voxelSize);
ExpectIndexBoundsEq(volumeField->GetIndexBounds(), generated.indexBounds);
EXPECT_EQ(volumeField->GetGridType(), generated.gridType);
EXPECT_EQ(volumeField->GetGridClass(), generated.gridClass);
delete volumeField;
fs::remove(volumePath);
}
#endif
#if defined(XCENGINE_HAS_NANOVDB)
TEST(VolumeFieldLoader, AssetDatabaseCreatesVolumeArtifactAndReusesItWithoutReimport) {
namespace fs = std::filesystem;
using namespace std::chrono_literals;
@@ -76,10 +151,13 @@ TEST(VolumeFieldLoader, AssetDatabaseCreatesVolumeArtifactAndReusesItWithoutReim
const fs::path projectRoot = fs::temp_directory_path() / "xc_volume_library_cache_test";
const fs::path assetsDir = projectRoot / "Assets";
const fs::path volumePath = assetsDir / "cloud.nvdb";
const fs::path fixturePath = GetCloudVolumePath();
fs::remove_all(projectRoot);
fs::create_directories(assetsDir);
WriteBinaryFile(volumePath, MakeTestNanoVDBPayload());
ASSERT_TRUE(fs::exists(fixturePath));
fs::copy_file(fixturePath, volumePath, fs::copy_options::overwrite_existing);
const GeneratedNanoVDBVolume generated = ReadTestNanoVDBFileMetadata(fixturePath);
AssetDatabase database;
database.Initialize(projectRoot.string().c_str());
@@ -97,7 +175,13 @@ TEST(VolumeFieldLoader, AssetDatabaseCreatesVolumeArtifactAndReusesItWithoutReim
ASSERT_NE(artifactLoad.resource, nullptr);
auto* artifactVolume = static_cast<VolumeField*>(artifactLoad.resource);
EXPECT_EQ(artifactVolume->GetStorageKind(), VolumeStorageKind::NanoVDB);
EXPECT_EQ(artifactVolume->GetPayloadSize(), MakeTestNanoVDBPayload().size());
EXPECT_EQ(artifactVolume->GetPayloadSize(), generated.payloadSize);
ExpectVector3Near(artifactVolume->GetBounds().GetMin(), generated.bounds.GetMin());
ExpectVector3Near(artifactVolume->GetBounds().GetMax(), generated.bounds.GetMax());
ExpectVector3Near(artifactVolume->GetVoxelSize(), generated.voxelSize);
ExpectIndexBoundsEq(artifactVolume->GetIndexBounds(), generated.indexBounds);
EXPECT_EQ(artifactVolume->GetGridType(), generated.gridType);
EXPECT_EQ(artifactVolume->GetGridClass(), generated.gridClass);
delete artifactVolume;
const auto originalArtifactWriteTime = fs::last_write_time(firstResolve.artifactMainPath.CStr());
@@ -111,7 +195,13 @@ TEST(VolumeFieldLoader, AssetDatabaseCreatesVolumeArtifactAndReusesItWithoutReim
database.Shutdown();
fs::remove_all(projectRoot);
}
#else
TEST(VolumeFieldLoader, AssetDatabaseCreatesVolumeArtifactAndReusesItWithoutReimport) {
GTEST_SKIP() << "NanoVDB headers are unavailable in this build";
}
#endif
#if defined(XCENGINE_HAS_NANOVDB)
TEST(VolumeFieldLoader, ResourceManagerLoadsVolumeByAssetRefFromProjectAssets) {
namespace fs = std::filesystem;
@@ -121,11 +211,13 @@ TEST(VolumeFieldLoader, ResourceManagerLoadsVolumeByAssetRefFromProjectAssets) {
const fs::path projectRoot = fs::temp_directory_path() / "xc_volume_asset_ref_test";
const fs::path assetsDir = projectRoot / "Assets";
const fs::path volumePath = assetsDir / "cloud.nvdb";
const std::vector<unsigned char> bytes = MakeTestNanoVDBPayload();
const fs::path fixturePath = GetCloudVolumePath();
fs::remove_all(projectRoot);
fs::create_directories(assetsDir);
WriteBinaryFile(volumePath, bytes);
ASSERT_TRUE(fs::exists(fixturePath));
fs::copy_file(fixturePath, volumePath, fs::copy_options::overwrite_existing);
const GeneratedNanoVDBVolume generated = ReadTestNanoVDBFileMetadata(fixturePath);
manager.SetResourceRoot(projectRoot.string().c_str());
@@ -133,7 +225,13 @@ TEST(VolumeFieldLoader, ResourceManagerLoadsVolumeByAssetRefFromProjectAssets) {
const auto firstHandle = manager.Load<VolumeField>("Assets/cloud.nvdb");
ASSERT_TRUE(firstHandle.IsValid());
EXPECT_EQ(firstHandle->GetStorageKind(), VolumeStorageKind::NanoVDB);
EXPECT_EQ(firstHandle->GetPayloadSize(), bytes.size());
EXPECT_EQ(firstHandle->GetPayloadSize(), generated.payloadSize);
ExpectVector3Near(firstHandle->GetBounds().GetMin(), generated.bounds.GetMin());
ExpectVector3Near(firstHandle->GetBounds().GetMax(), generated.bounds.GetMax());
ExpectVector3Near(firstHandle->GetVoxelSize(), generated.voxelSize);
ExpectIndexBoundsEq(firstHandle->GetIndexBounds(), generated.indexBounds);
EXPECT_EQ(firstHandle->GetGridType(), generated.gridType);
EXPECT_EQ(firstHandle->GetGridClass(), generated.gridClass);
AssetRef assetRef;
ASSERT_TRUE(manager.TryGetAssetRef("Assets/cloud.nvdb", ResourceType::VolumeField, assetRef));
@@ -144,12 +242,23 @@ TEST(VolumeFieldLoader, ResourceManagerLoadsVolumeByAssetRefFromProjectAssets) {
const auto secondHandle = manager.Load<VolumeField>(assetRef);
ASSERT_TRUE(secondHandle.IsValid());
EXPECT_EQ(secondHandle->GetStorageKind(), VolumeStorageKind::NanoVDB);
EXPECT_EQ(secondHandle->GetPayloadSize(), bytes.size());
EXPECT_EQ(secondHandle->GetPayloadSize(), generated.payloadSize);
ExpectVector3Near(secondHandle->GetBounds().GetMin(), generated.bounds.GetMin());
ExpectVector3Near(secondHandle->GetBounds().GetMax(), generated.bounds.GetMax());
ExpectVector3Near(secondHandle->GetVoxelSize(), generated.voxelSize);
ExpectIndexBoundsEq(secondHandle->GetIndexBounds(), generated.indexBounds);
EXPECT_EQ(secondHandle->GetGridType(), generated.gridType);
EXPECT_EQ(secondHandle->GetGridClass(), generated.gridClass);
}
manager.SetResourceRoot("");
manager.Shutdown();
fs::remove_all(projectRoot);
}
#else
TEST(VolumeFieldLoader, ResourceManagerLoadsVolumeByAssetRefFromProjectAssets) {
GTEST_SKIP() << "NanoVDB headers are unavailable in this build";
}
#endif
} // namespace