cnq3/code/renderer/shaders/crp/vl_frustum_injection_nanovdb.hlsl
myT afc81437c3 added NanoVDB support
- added the foundation for a GPU particle system
- reworked volumetric particle injection
2024-07-02 02:06:15 +02:00

398 lines
13 KiB
HLSL

/*
===========================================================================
Copyright (C) 2024 Gian 'myT' Schellenbaum
This file is part of Challenge Quake 3 (CNQ3).
Challenge Quake 3 is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of the License,
or (at your option) any later version.
Challenge Quake 3 is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Challenge Quake 3. If not, see <https://www.gnu.org/licenses/>.
===========================================================================
*/
// volumetric lighting: inject a NanoVDB volume into the material textures
//#define PREVIEW_MODE 1
// preview mode is a faster and less complex version of the shader
// that doesn't crash the AMD/Intel shader compilers
// this shader considers smoke to be fully isotropic (g = 0)
// in "Creating the Atmospheric World of Red Dead Redemption 2" by Fabian Bauer,
// g is 0.1 with strong backscatter for smoke
// RDR2 uses a phase function with multiple octaves of HG
// it fakes backscattering for a specific extinction range
// the phase function returns the max. of the sum of octaves and the backscatter
#include "common.hlsli"
#include "scene_view.h.hlsli"
#include "light_grid.h.hlsli"
#include "vl_common.h.hlsli"
#include "vl_nanovdb.hlsli"
cbuffer RootConstants
{
float4 packedTransform[4];
float2 packedTransform2;
uint nanovdbBufferIndex;
uint blackbodyTextureIndex;
LightGridRC lightGridRC;
uint materialTextureAIndex;
uint materialTextureBIndex;
uint materialTextureCIndex;
uint scatterExtTextureIndex;
uint frustumVisTextureIndex;
uint densityGridByteOffset;
uint flamesGridByteOffset;
uint densityGridByteOffset2;
uint flamesGridByteOffset2;
uint linearInterpolation;
uint accurateOverlapTest;
uint ambientAngularCoverage;
float densityExtinctionScale;
float densityAlbedo;
float flamesEmissionScale;
float flamesTemperatureScale;
float stepScale; // for super-sampling froxel average
float transStepScale; // for under-sampling ambient transmittance
float t;
}
static const float SqrtOneThird = sqrt(1.0 / 3.0);
#if PREVIEW_MODE
static const float3 Dirs[4] =
{
float3(-SqrtOneThird, -SqrtOneThird, SqrtOneThird),
float3(SqrtOneThird, SqrtOneThird, SqrtOneThird),
float3(-SqrtOneThird, SqrtOneThird, SqrtOneThird),
float3(SqrtOneThird, -SqrtOneThird, SqrtOneThird)
};
#else
static const float3 Dirs[6 + 8] =
{
float3(1, 0, 0),
float3(-1, 0, 0),
float3(0, 1, 0),
float3(0, -1, 0),
float3(0, 0, 1),
float3(0, 0, -1),
float3(-SqrtOneThird, -SqrtOneThird, -SqrtOneThird),
float3(-SqrtOneThird, -SqrtOneThird, SqrtOneThird),
float3(-SqrtOneThird, SqrtOneThird, -SqrtOneThird),
float3(-SqrtOneThird, SqrtOneThird, SqrtOneThird),
float3(SqrtOneThird, -SqrtOneThird, -SqrtOneThird),
float3(SqrtOneThird, -SqrtOneThird, SqrtOneThird),
float3(SqrtOneThird, SqrtOneThird, -SqrtOneThird),
float3(SqrtOneThird, SqrtOneThird, SqrtOneThird)
};
#endif
float3 BlackbodyColor(float temperatureK, Texture2D blackbodyTexture, SamplerState blackbodySampler)
{
const float minT = 800;
const float maxT = 12000;
float t = saturate((temperatureK - minT) / (maxT - minT));
float3 emission = blackbodyTexture.SampleLevel(blackbodySampler, float2(t, 0.5), 0.0).rgb;
return emission;
}
// Stefan-Boltzmann law
float BlackbodyRadiation(float temperatureK)
{
const float sigma = 5.670373e-8;
float T = temperatureK;
float T2 = T * T;
float T4 = T2 * T2;
float radiation = (T4 * sigma) / PI;
return radiation;
}
float3 BlackbodyEmission(float temperatureK, Texture2D blackbodyTexture, SamplerState blackbodySampler)
{
const float scale = 1.0e-6;
float3 color = BlackbodyColor(temperatureK, blackbodyTexture, blackbodySampler);
float radiation = BlackbodyRadiation(temperatureK);
float colorBrightness = Brightness(color);
float3 result = (color * radiation * scale) / colorBrightness;
return result;
}
struct SampleRequest
{
pnanovdb_buf_t buffer;
uint gridByteOffset;
float3 froxelPosition;
int3 froxelIndex;
float3 textureSize;
float3 froxelSize;
Transform transform;
SceneView scene;
bool ambientLight;
int3 froxelId;
LightGrid lightGrid;
};
#if PREVIEW_MODE
SampleResult SampleFroxel(SampleRequest request)
{
SampleResult result = CreateSampleResult();
if(request.gridByteOffset == 0)
{
// no grid requested
return result;
}
Grid grid = GetGrid(request.buffer, request.gridByteOffset);
float3 indexF = grid.WorldToIndex(request.froxelPosition, request.transform);
bool overlaps = IsInRange(indexF, grid.bboxMin, grid.bboxMax);
if(!overlaps)
{
// no grid/froxel intersection
return result;
}
result = grid.GetFroxelAverage(request.scene, request.froxelPosition, request.froxelSize, request.transform);
if(!request.ambientLight)
{
// no ambient light requested
return result;
}
const float inScatterScale = 2.0;
const float stepScale = 8.0;
const uint dirCount = 4;
float jitterScale = 0.5 * Hash3To1(request.froxelId);
float extScale = densityExtinctionScale;
float trans = 0.0;
[unroll]
for(uint i = 0; i < dirCount; i++)
{
float3 dir = Dirs[i];
float3 step = dir * stepScale;
trans += grid.RaymarchTransmittance(request.froxelPosition + step * jitterScale, step, request.transform, extScale);
}
result.inScatteredLight = request.scene.ambientColor * (inScatterScale * trans * request.scene.ambientIntensity);
return result;
}
#else
SampleResult SampleFroxel(SampleRequest request)
{
SampleResult result = CreateSampleResult();
if(request.gridByteOffset == 0)
{
// no grid requested
return result;
}
Grid grid = GetGrid(request.buffer, request.gridByteOffset);
bool overlaps;
if(accurateOverlapTest != 0u)
{
overlaps = grid.OverlapsFroxel(request.scene, request.froxelIndex, request.textureSize, request.transform);
}
else
{
float3 indexF = grid.WorldToIndex(request.froxelPosition, request.transform);
overlaps = IsInRange(indexF, grid.bboxMin, grid.bboxMax);
}
if(!overlaps)
{
// no grid/froxel intersection
return result;
}
result = grid.GetFroxelAverage(request.scene, request.froxelPosition, request.froxelSize, request.transform);
if(!request.ambientLight)
{
// no ambient light requested
return result;
}
float3x3 rotation = RandomRotationMatrix3x3(float3(request.froxelId));
float jitterScale = 0.5 * Hash3To1(request.froxelId);
float stepScale = transStepScale;
uint dirCount = ambientAngularCoverage != 0 ? 14 : 6;
float localWeight = ambientAngularCoverage != 0 ? 1.75 : 0.75;
if(lightGridRC.isAvailable)
{
LightGridSample ambient = request.lightGrid.SampleAtPosition(request.froxelPosition);
float extScale = densityExtinctionScale;
float3 lightDir = ambient.GetLightDirection();
float3 dirTransStep = lightDir * stepScale;
float dirTrans = grid.RaymarchTransmittance(request.froxelPosition + dirTransStep * jitterScale, dirTransStep, request.transform, extScale);
float3 globalColor = float3(0, 0, 0);
for(uint i = 0; i < dirCount; i++)
{
float3 dir = mul(rotation, Dirs[i]);
float scale = stepScale;
float3 step = dir * stepScale;
LightGridSample sample = request.lightGrid.SampleAtPosition(request.froxelPosition + dir * scale * 0.5);
float trans = grid.RaymarchTransmittance(request.froxelPosition + step * jitterScale, step, request.transform, extScale);
float3 color = ColorAtBrightness(sample.GetGlobalColor(), 0.5);
globalColor += color * trans;
}
float3 cameraDir = normalize(request.froxelPosition - request.scene.cameraPosition);
float localScale = dot(-cameraDir, lightDir) * 0.5 + 0.5; // wraps around
float3 localColor = ColorAtBrightness(ambient.GetLocalColor(), 0.5) * localScale * dirTrans;
float3 color = (globalColor + localColor * localWeight) / (float(dirCount) + localWeight);
result.inScatteredLight = color * request.scene.ambientIntensity;
}
else
{
float extScale = densityExtinctionScale;
float trans = 0.0;
for(uint i = 0; i < dirCount; i++)
{
float3 dir = Dirs[i];
float3 step = dir * stepScale;
trans += grid.RaymarchTransmittance(request.froxelPosition + step * jitterScale, step, request.transform, extScale);
}
trans /= float(dirCount);
result.inScatteredLight = request.scene.ambientColor * (trans * request.scene.ambientIntensity);
}
return result;
}
#endif
[numthreads(4, 4, 4)]
void cs(uint3 id : SV_DispatchThreadID)
{
RWTexture3D<float4> materialTextureA = ResourceDescriptorHeap[materialTextureAIndex];
uint3 textureSize = GetTextureSize(materialTextureA);
if(any(id >= textureSize))
{
return;
}
RWTexture2D<uint> frustumVisTexture = ResourceDescriptorHeap[frustumVisTextureIndex];
uint furthestVisibleFroxelZIndex = frustumVisTexture[id.xy];
if(id.z > furthestVisibleFroxelZIndex)
{
return;
}
SceneView scene = GetSceneView();
LightGrid lightGrid;
if(lightGridRC.isAvailable)
{
lightGrid = GetLightGrid(lightGridRC);
}
pnanovdb_buf_t nanovdbBuffer = ResourceDescriptorHeap[nanovdbBufferIndex];
Texture2D blackbodyTexture = ResourceDescriptorHeap[blackbodyTextureIndex];
SamplerState blackbodySampler = SamplerDescriptorHeap[scene.linearClampSamplerIndex];
RWTexture3D<float4> materialTextureB = ResourceDescriptorHeap[materialTextureBIndex];
RWTexture3D<float> materialTextureC = ResourceDescriptorHeap[materialTextureCIndex];
RWTexture3D<float4> scatterExtTexture = ResourceDescriptorHeap[scatterExtTextureIndex];
float3 textureSizeF = float3(textureSize);
float3 froxelPosition = scene.FroxelIndexToWorldSpace(int3(id), textureSizeF);
float3 froxelSize = scene.FroxelMaxDimensions(id, textureSizeF);
Transform transform = DecodeTransform(packedTransform, packedTransform2);
transform.stepSize *= stepScale;
#if !PREVIEW_MODE
if(linearInterpolation != 0u)
{
SampleRequest r;
r.buffer = nanovdbBuffer;
r.froxelIndex = id;
r.froxelPosition = froxelPosition;
r.froxelSize = froxelSize;
r.gridByteOffset = densityGridByteOffset;
r.scene = scene;
r.textureSize = textureSizeF;
r.transform = transform;
r.lightGrid = lightGrid;
r.froxelId = id;
r.ambientLight = true;
SampleResult extResult1 = SampleFroxel(r);
r.gridByteOffset = densityGridByteOffset2;
SampleResult extResult2 = SampleFroxel(r);
r.ambientLight = false;
r.gridByteOffset = flamesGridByteOffset;
SampleResult emResult1 = SampleFroxel(r);
r.gridByteOffset = flamesGridByteOffset2;
SampleResult emResult2 = SampleFroxel(r);
if(extResult1.sum > 0.0 || extResult2.sum > 0.0)
{
float extinction1 = (extResult1.sum / float(extResult1.maxSampleCount));
float extinction2 = (extResult2.sum / float(extResult2.maxSampleCount));
float extinction = lerp(extinction1, extinction2, t) * densityExtinctionScale;
float scatter = extinction * densityAlbedo;
float absorption = extinction - scatter;
float coverage1 = float(extResult1.sampleCount) / float(extResult1.maxSampleCount);
float coverage2 = float(extResult2.sampleCount) / float(extResult2.maxSampleCount);
float coverage = lerp(coverage1, coverage2, t);
float3 inScattered = lerp(extResult1.inScatteredLight, extResult2.inScatteredLight, t);
materialTextureA[id] += float4(scatter.xxx, absorption);
materialTextureC[id] += coverage;
scatterExtTexture[id] += float4(scatter * inScattered, 0);
}
if(emResult1.sum > 0.0 || emResult2.sum > 0.0)
{
float Tnorm1 = emResult1.sum / float(emResult1.maxSampleCount);
float Tnorm2 = emResult2.sum / float(emResult2.maxSampleCount);
float T = lerp(Tnorm1, Tnorm2, t) * flamesTemperatureScale;
float3 emission = BlackbodyEmission(T, blackbodyTexture, blackbodySampler) * flamesEmissionScale;
materialTextureB[id] += float4(emission, 0.0);
}
}
else
#endif
{
SampleRequest r;
r.buffer = nanovdbBuffer;
r.froxelIndex = id;
r.froxelPosition = froxelPosition;
r.froxelSize = froxelSize;
r.gridByteOffset = densityGridByteOffset;
r.scene = scene;
r.textureSize = textureSizeF;
r.transform = transform;
r.lightGrid = lightGrid;
r.froxelId = id;
r.ambientLight = true;
SampleResult extResult = SampleFroxel(r);
r.gridByteOffset = flamesGridByteOffset;
r.ambientLight = false;
SampleResult emResult = SampleFroxel(r);
if(extResult.sum > 0.0)
{
float extinction = (extResult.sum / float(extResult.maxSampleCount)) * densityExtinctionScale;
float scatter = extinction * densityAlbedo;
float absorption = extinction - scatter;
float coverage = float(extResult.sampleCount) / float(extResult.maxSampleCount);
materialTextureA[id] += float4(scatter.xxx, absorption);
materialTextureC[id] += coverage;
scatterExtTexture[id] += float4(scatter * extResult.inScatteredLight, 0);
}
if(emResult.sum > 0.0)
{
float Tnorm = emResult.sum / float(emResult.maxSampleCount);
float T = Tnorm * flamesTemperatureScale;
float3 emission = BlackbodyEmission(T, blackbodyTexture, blackbodySampler) * flamesEmissionScale;
materialTextureB[id] += float4(emission, 0.0);
}
}
}