Fix NanoVDB volume loading and rendering
This commit is contained in:
319
engine/assets/builtin/shaders/volumetric.shader
Normal file
319
engine/assets/builtin/shaders/volumetric.shader
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
694
engine/src/Rendering/Passes/BuiltinVolumetricPass.cpp
Normal file
694
engine/src/Rendering/Passes/BuiltinVolumetricPass.cpp
Normal 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
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user