From 76c96199e17ac07b36e0862adec5188d73fddcb1 Mon Sep 17 00:00:00 2001 From: turanszkij Date: Tue, 27 Feb 2018 17:22:58 +0000 Subject: [PATCH] voxel GI quality update; added cornell-box model for better testing --- WickedEngine/globals.hlsli | 4 +- WickedEngine/lightingHF.hlsli | 17 ++++-- WickedEngine/voxelConeTracingHF.hlsli | 77 ++++++++++++++------------ WickedEngine/wiRenderer.cpp | 3 +- WickedEngine/wiRenderer.h | 2 +- WickedEngine/wiVersion.cpp | 2 +- models/CornellBox/CornellBox.wimf | Bin 0 -> 89278 bytes 7 files changed, 60 insertions(+), 45 deletions(-) create mode 100644 models/CornellBox/CornellBox.wimf diff --git a/WickedEngine/globals.hlsli b/WickedEngine/globals.hlsli index 82062132c..8904a8870 100644 --- a/WickedEngine/globals.hlsli +++ b/WickedEngine/globals.hlsli @@ -59,14 +59,14 @@ CBUFFER(WorldCB, CBSLOT_RENDERER_WORLD) float3 g_xWorld_Fog; // Fog Start,End,Height float g_xWorld_SpecularAA; float g_xWorld_VoxelRadianceDataSize; + float g_xWorld_VoxelRadianceDataSize_Inverse; uint g_xWorld_VoxelRadianceDataRes; float g_xWorld_VoxelRadianceDataRes_Inverse; uint g_xWorld_VoxelRadianceDataMIPs; uint g_xWorld_VoxelRadianceConeTracingQuality; - uint g_xWorld_VoxelRadianceConeTracingQuality_Inverse; + float g_xWorld_VoxelRadianceConeTracingQuality_Inverse; float g_xWorld_VoxelRadianceFalloff; bool g_xWorld_VoxelRadianceReflectionsEnabled; - float xPadding1_WorldCB; float3 g_xWorld_VoxelRadianceDataCenter; bool g_xWorld_AdvancedRefractions; uint3 g_xWorld_EntityCullingTileCount; diff --git a/WickedEngine/lightingHF.hlsli b/WickedEngine/lightingHF.hlsli index 4934e574c..c38ea2091 100644 --- a/WickedEngine/lightingHF.hlsli +++ b/WickedEngine/lightingHF.hlsli @@ -704,10 +704,19 @@ inline void VoxelRadiance(in Surface surface, inout float3 diffuse, inout float3 { [branch]if (g_xWorld_VoxelRadianceDataRes != 0) { - float3 diff = (surface.P - g_xWorld_VoxelRadianceDataCenter) / g_xWorld_VoxelRadianceDataRes / g_xWorld_VoxelRadianceDataSize; - float3 uvw = diff * float3(0.5f, -0.5f, 0.5f) + 0.5f; - diff = abs(diff); - float blend = pow(saturate(max(diff.x, max(diff.y, diff.z))), 4); + // Convert world space surface pos to voxel grid space: + float3 voxelSpacePos = surface.P - g_xWorld_VoxelRadianceDataCenter; + // offset by half a voxel in the direction of the surface normal (try to avoid self-occlusion) + voxelSpacePos += surface.N * 0.5f; + // normalize voxel space pos to [-1, 1]: + voxelSpacePos *= g_xWorld_VoxelRadianceDataSize_Inverse; + voxelSpacePos *= g_xWorld_VoxelRadianceDataRes_Inverse; + // calculate uvw coords into voxel texture: + float3 uvw = voxelSpacePos * float3(0.5f, -0.5f, 0.5f) + 0.5f; + + // determine blending factor (we will blend out voxel GI on grid edges): + voxelSpacePos = saturate(abs(voxelSpacePos)); + float blend = pow(max(voxelSpacePos.x, max(voxelSpacePos.y, voxelSpacePos.z)), 4); float4 radiance = ConeTraceRadiance(texture_voxelradiance, uvw, surface.N); diffuse += lerp(radiance.rgb, 0, blend); diff --git a/WickedEngine/voxelConeTracingHF.hlsli b/WickedEngine/voxelConeTracingHF.hlsli index a504180c1..69301fd05 100644 --- a/WickedEngine/voxelConeTracingHF.hlsli +++ b/WickedEngine/voxelConeTracingHF.hlsli @@ -3,63 +3,64 @@ #include "globals.hlsli" static const float3 CONES[] = { - float3(0.355512, -0.709318, -0.102371), - float3(0.534186, 0.71511, -0.115167), - float3(-0.87866, 0.157139, -0.115167), - float3(0.140679, -0.475516, -0.0639818), - float3(-0.0796121, 0.158842, -0.677075), - float3(-0.0759516, -0.101676, -0.483625), - float3(0.12493, -0.0223423, -0.483625), - float3(-0.0720074, 0.243395, -0.967251), - float3(-0.207641, 0.414286, 0.187755), - float3(-0.277332, -0.371262, 0.187755), - float3(0.63864, -0.114214, 0.262857), - float3(-0.184051, 0.622119, 0.262857), - float3(0.110007, -0.219486, 0.435574), - float3(0.235085, 0.314707, 0.696918), - float3(-0.290012, 0.0518654, 0.522688), - float3(0.0975089, -0.329594, 0.609803) + float3(0.57735, 0.57735, 0.57735), + float3(0.57735, -0.57735, -0.57735), + float3(-0.57735, 0.57735, -0.57735), + float3(-0.57735, -0.57735, 0.57735), + float3(-0.903007, -0.182696, -0.388844), + float3(-0.903007, 0.182696, 0.388844), + float3(0.903007, -0.182696, 0.388844), + float3(0.903007, 0.182696, -0.388844), + float3(-0.388844, -0.903007, -0.182696), + float3(0.388844, -0.903007, 0.182696), + float3(0.388844, 0.903007, -0.182696), + float3(-0.388844, 0.903007, 0.182696), + float3(-0.182696, -0.388844, -0.903007), + float3(0.182696, 0.388844, -0.903007), + float3(-0.182696, 0.388844, 0.903007), + float3(0.182696, -0.388844, 0.903007) }; inline float4 ConeTraceRadiance(in Texture3D voxels, in float3 uvw, in float3 N) { - const uint numCones = clamp(g_xWorld_VoxelRadianceConeTracingQuality, 1, 16); - - // Cone tracing: float4 radiance = 0; - for (uint cone = 0; cone < numCones; ++cone) + + for (uint cone = 0; cone < g_xWorld_VoxelRadianceConeTracingQuality; ++cone) // quality is between 1 and 16 cones { // try to approximate a hemisphere from random points inside a sphere: float3 coneVec = CONES[cone]; // if point on sphere is facing below normal (so it's located on bottom hemisphere), put it on the opposite hemisphere instead: coneVec *= dot(coneVec, N) < 0 ? -1 : 1; - // try to avoid cone extending below horizon by offsetting a bit by normal: - coneVec += N * 0.1f; coneVec *= g_xWorld_VoxelRadianceDataRes_Inverse * float3(1, -1, 1); - float4 accumulation = 0; + float3 color = 0; + float alpha = 0; + float3 tc = uvw; uint i = 0; - while (accumulation.a < 1 && !any(tc - saturate(tc))) + while (alpha < 1 && !any(tc - saturate(tc))) { - float mip = 0.7 * i; + float mip = 0.8 * i; tc += coneVec * (1 + mip); float4 sam = voxels.SampleLevel(sampler_linear_clamp, tc, mip); - accumulation.a += sam.a; - accumulation.rgb += sam.rgb * sam.a; + + float a = 1 - alpha; + color += a * sam.rgb; + alpha += a * sam.a; if (mip >= (float)g_xWorld_VoxelRadianceDataMIPs) break; ++i; } - float searchDist = length(uvw - tc) * g_xWorld_VoxelRadianceDataRes * g_xWorld_VoxelRadianceFalloff; - accumulation.a /= searchDist; - radiance += accumulation; + + radiance += float4(color, alpha); } - radiance /= numCones; + + // final radiance is average of all the cones radiances + radiance *= g_xWorld_VoxelRadianceConeTracingQuality_Inverse; radiance.a = saturate(radiance.a); return max(0, radiance); @@ -69,24 +70,28 @@ inline float4 ConeTraceReflection(in Texture3D voxels, in float3 uvw, in { float aperture = pow(roughness, 4); float3 coneVec = reflect(-V, N) / g_xWorld_VoxelRadianceDataRes * float3(1, -1, 1); - float4 accumulation = 0; float3 tc = uvw + coneVec; float NdotV = saturate(dot(N, V)); coneVec *= lerp(0.25, 4, pow(1 - NdotV, 8)); + float3 color = 0; + float alpha = 0; + // TODO: this can result in huge loops if ray enters empty space. // Simply limiting iteration count is not good enough solution because most reflections won't be found that way. // Need to have at least some form of binary search. Maybe we could reuse lower mip to find where to backtrace? uint i = 0; - while (accumulation.a < 1 && !any(tc - saturate(tc))) + while (alpha < 1 && !any(tc - saturate(tc))) { float mip = aperture * i; tc += coneVec * (1 + mip); float4 sam = voxels.SampleLevel(sampler_linear_clamp, tc, mip); - accumulation.a += sam.a; - accumulation.rgb += sam.rgb * sam.a; + + float a = 1 - alpha; + color += a * sam.rgb; + alpha += a * sam.a; if (mip >= (float)g_xWorld_VoxelRadianceDataMIPs) { @@ -96,7 +101,7 @@ inline float4 ConeTraceReflection(in Texture3D voxels, in float3 uvw, in ++i; } - return float4(max(0, accumulation.rgb), saturate(accumulation.a)); + return float4(max(0, color), saturate(alpha)); } #endif \ No newline at end of file diff --git a/WickedEngine/wiRenderer.cpp b/WickedEngine/wiRenderer.cpp index 323415f13..692c24194 100644 --- a/WickedEngine/wiRenderer.cpp +++ b/WickedEngine/wiRenderer.cpp @@ -6293,10 +6293,11 @@ void wiRenderer::UpdateWorldCB(GRAPHICSTHREAD threadID) value.mZenith = world.zenith; value.mSpecularAA = SPECULARAA; value.mVoxelRadianceDataSize = voxelSceneData.voxelsize; + value.mVoxelRadianceDataSize_Inverse = 1.0f / (float)value.mVoxelRadianceDataSize; value.mVoxelRadianceDataRes = GetVoxelRadianceEnabled() ? (UINT)voxelSceneData.res : 0; value.mVoxelRadianceDataRes_Inverse = 1.0f / (float)value.mVoxelRadianceDataRes; value.mVoxelRadianceDataMIPs = voxelSceneData.mips; - value.mVoxelRadianceDataConeTracingQuality = voxelSceneData.coneTracingQuality; + value.mVoxelRadianceDataConeTracingQuality = max(min(voxelSceneData.coneTracingQuality, 16), 1); value.mVoxelRadianceDataConeTracingQuality_Inverse = 1.0f / (float)value.mVoxelRadianceDataConeTracingQuality; value.mVoxelRadianceDataFalloff = voxelSceneData.falloff; value.mVoxelRadianceReflectionsEnabled = voxelSceneData.reflectionsEnabled; diff --git a/WickedEngine/wiRenderer.h b/WickedEngine/wiRenderer.h index 0a6218736..18133e71a 100644 --- a/WickedEngine/wiRenderer.h +++ b/WickedEngine/wiRenderer.h @@ -133,6 +133,7 @@ public: XMFLOAT3 mFog; float mSpecularAA; float mVoxelRadianceDataSize; + float mVoxelRadianceDataSize_Inverse; UINT mVoxelRadianceDataRes; float mVoxelRadianceDataRes_Inverse; UINT mVoxelRadianceDataMIPs; @@ -140,7 +141,6 @@ public: float mVoxelRadianceDataConeTracingQuality_Inverse; float mVoxelRadianceDataFalloff; BOOL mVoxelRadianceReflectionsEnabled; - float pad1; XMFLOAT3 mVoxelRadianceDataCenter; BOOL mAdvancedRefractions; XMUINT3 mEntityCullingTileCount; diff --git a/WickedEngine/wiVersion.cpp b/WickedEngine/wiVersion.cpp index 73a57b82c..d4969f420 100644 --- a/WickedEngine/wiVersion.cpp +++ b/WickedEngine/wiVersion.cpp @@ -9,7 +9,7 @@ namespace wiVersion // minor features, major updates const int minor = 16; // minor bug fixes, alterations, refactors, updates - const int revision = 8; + const int revision = 9; long GetVersion() diff --git a/models/CornellBox/CornellBox.wimf b/models/CornellBox/CornellBox.wimf new file mode 100644 index 0000000000000000000000000000000000000000..99dd8f56f63b24d3c3eb44f7c3c271861323f5b5 GIT binary patch literal 89278 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 literal 0 HcmV?d00001