123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- /*
- * Copyright (c) Contributors to the Open 3D Engine Project.
- * For complete copyright and license terms please see the LICENSE at the root of this distribution.
- *
- * SPDX-License-Identifier: Apache-2.0 OR MIT
- *
- */
- #include <viewsrg_all.srgi>
- #include <Atom/Features/PBR/Hammersley.azsli>
- #include <Atom/RPI/Math.azsli>
- #define GROUP_DIM_X 16
- #define GROUP_DIM_Y 16
- // Padding of one side, e.g. 16x16 block + 2 pixel pad = 20x20 padded block
- // Intended to provide at least some LDS neighbours for boundary pixels
- #define PAD 2
- #define PAD2 (PAD * 2)
- // This size should be number of element in thread group + number of padding element
- #define SHARED_MEMORY_SIZE 400
- // Use 60% / 30% of total samples for relatively far / far objects
- #define SAMPLE_RATIO_1 0.3
- #define SAMPLE_RATIO_2 0.6
- #define NUMBER_OF_SAMPLE 200
- // How large the diffusion profile (filter) is on the screen in pixel, sampling rate of small filters will be reduced
- // to improve performance
- #define FOOTPRINT_SMALL 1
- #define FOOTPRINT_MEDIUM 4
- // Unit conversion
- #define MILLIMETER_TO_METER 1e-3
- #define METER_TO_MILLIMETER 1e3
- #define DEPTH_CLIP_THRESHOLD_SCALE (0.5 * MILLIMETER_TO_METER)
- ShaderResourceGroup PassSrg : SRG_PerPass
- {
- Texture2D<float4> m_diffuseTexture;
- Texture2D<float> m_linearDepthTexture;
- Texture2D<float3> m_scatterDistanceTexture;
-
- RWTexture2D<float4> m_outputTexture;
- // Passed by SubsurfaceScatterPass.cpp from cpu
- float2 m_screenSize;
- // float3 -> diffuse color
- // float -> linear depth
- groupshared float sColorR[SHARED_MEMORY_SIZE];
- groupshared float sColorG[SHARED_MEMORY_SIZE];
- groupshared float sColorB[SHARED_MEMORY_SIZE];
- groupshared float sDepth [SHARED_MEMORY_SIZE];
-
- void SetColorDepth(uint index, float3 color, float depth)
- {
- sColorR[index] = color.r;
- sColorG[index] = color.g;
- sColorB[index] = color.b;
- sDepth[index] = depth;
- }
-
- float3 GetRadiance(uint index)
- {
- return float3(sColorR[index], sColorG[index], sColorB[index]);
- }
-
- float GetDepth(uint index)
- {
- return sDepth[index];
- }
- }
- // --Return Monte-Carlo importance for the given kernel element, whose equation is :
- // DiffusionProfile(l, s) * l / DiffusionProfilePDF(r, minS)
- // -> (s / (8 * PI * l) * (exp(-s * l) + exp(-s * l / 3.0))) * l / (minS / (8 * PI) * (exp(-minS * r) + exp(-minS * r / 3.0)))
- // -> (exp(-s * l) + exp(-s * l / 3.0)) / (exp(-minS * r) + exp(-minS * r / 3.0))
- // --Smallest element of shape parameter 's' is used for PDF because we sampled from this single lobe during importance sampling.
- // --Since volume albedo A is a constant for all samples, thus been removed from equation of diffusion profile, which can be multiplied back later
- // --For more information about Burley's normalized diffusion please refer to:
- // Christensen H.P. Burley B. 2015, Approximate Reflectance Profiles for Efficient Subsurface Scattering
- // https://graphics.pixar.com/library/ApproxBSSRDF/paper.pdf
- float3 Kernel(float l, float r, float3 s, float minS)
- {
- return l > 0 ? (exp(-s * l) + exp(-s * l / 3.0)) / (exp(-minS * r) + exp(-minS * r / 3.0)) : float3(0.0, 0.0, 0.0);
- }
- // Helper function of inverse cdf approximation
- float G(float u)
- {
- return 4.0 * u * (2.0 * u + sqrt(1.0 + 4.0 * u * u)) + 1.0;
- }
- // Approximation of inverse CDF via curve fitting, surves as an excellent model for graphical computation
- float GetSampleInverseCDFApprox(float u, float s)
- {
- // complementary value of u
- float uc = 1.0 - u;
- float oneDivThree = 1.0 / 3.0;
- return 3.0 * log((1.0 + 1.0 / pow(G(uc), oneDivThree) + pow(G(uc), oneDivThree)) / (4.0 * uc)) / s;
- }
- // 1D Spherical fibonacci point set for uniformly sampling azimuth angle
- float SF(uint index)
- {
- return 2 * PI * index * 2 / (1 + sqrt(5));
- }
- // Cosine value of pseudo random (hammersley) angle theta,
- // which used to rotate the direction of diffusion profile, which is a 2d plane at surface point parallel to viewing plane,
- // to eliminate structured noise pattern introduced by low-discrepancy sequence, this value equivalent to:
- // theta = 2 * pi * hammersley(index)
- // cosTheta = cos(theta)
- static const float cosTheta[4] = { 1.0, -1.0, 6.123233995736766e-17, -1.8369701987210297e-16 };
- // Same as above but sine value instead
- static const float sinTheta[4] = { 0.0, 1.2246467991473532e-16, 1.0, -1.0 };
- // --Get low-discrepancy 2d sample using sample index 'index' and inverse scatter distance 's'
- // --Radius 'r' sampled from hammersley sequance (see Hammersley.azsli::GetVanDerCorputRadicalInverse() for more information)
- // --Planar angle 'phi' sampled from spherical fibonacci's azimuthal component
- // --Sample rotated using interleaved sample strategy to avoid obvious artifacts, which repeat a
- // 2x2 block sampling pattern (generated using 'gridIndex', which from 0-7 to select rotation angle) across the whole image to generate well distributed noise that can be easily filtered
- // by a 2x2 box filter afterwards
- void GetSample(in uint index, in uint gridIndex, in float s, out float3 sp)
- {
- float r = GetSampleInverseCDFApprox(GetVanDerCorputRadicalInverse(index), s);
- float x = r * cos(SF(index));
- float y = r * sin(SF(index));
- // Rotate samples to reduce visible pattern:
- sp = float3(x * cosTheta[gridIndex] - y * sinTheta[gridIndex],
- x * sinTheta[gridIndex] + y * cosTheta[gridIndex], r);
- }
- [numthreads(GROUP_DIM_X,GROUP_DIM_Y,1)]
- void MainCS(
- uint3 dispatchID : SV_DispatchThreadID,
- uint3 groupThreadID : SV_GroupThreadID,
- uint3 groupID : SV_GroupID,
- uint groupIndex : SV_GroupIndex )
- {
- // Input diffuse color of current pixel, alpha channel is matte mask identify which pixel needs convolution
- float4 colorX = PassSrg::m_diffuseTexture.Load(uint3(dispatchID.x, dispatchID.y, 0));
- // unpack strength factor and quality factor
- float2 factorAndQuality = frac(colorX.w / float2(65536, 256));
- float factor = factorAndQuality.x < 0.99 ? factorAndQuality.x : 1.0;
- float quality = factorAndQuality.y < 0.99 ? factorAndQuality.y : 1.0;
- //+---------------------------+
- //| shared memory allocation |
- //+---------------------------+
-
- // X/Y thread group dimension include pad on both sides
- uint dimXWithPad = GROUP_DIM_X + PAD2;
- // Pre integration of constants used to convert thread group id to
- // texture coordinates, to avoid duplicate calculation, don't have special meaning
- uint texelOffsetX = groupID.x * GROUP_DIM_X - PAD;
- uint texelOffsetY = groupID.y * GROUP_DIM_Y - PAD;
- // --Corresponding texel coordinate for each element in the (GROUP_DIM_X + PAD) x (GROUP_DIM_Y + PAD) shared memory
- // due to the existence of padding to avoid bank conflicts threads are offseted to
- // load pixels based on coordinates in shared memory, instead of with original groupThreadID.
- // --All threads will load first GROUP_DIM_X x GROUP_DIM_Y elements into shared memory start from
- // top left corner, then portion of threads will continuously load rest of elements
- // --Note: since shared memory ID use top left as origin but texture coordinates use bottom left
- // as origin, image patch stored in shared memory is flipped vertically, but it's fine as
- // our profile is radial symmetric
- uint3 texelIndex = uint3(
- texelOffsetX + (groupIndex % dimXWithPad),
- texelOffsetY + (groupIndex / dimXWithPad),
- 0
- );
- PassSrg::SetColorDepth(groupIndex,
- PassSrg::m_diffuseTexture.Load(texelIndex).rgb,
- PassSrg::m_linearDepthTexture.Load(texelIndex).r
- );
- uint totalLoadingThread = SHARED_MEMORY_SIZE - GROUP_DIM_X * GROUP_DIM_Y;
- if(groupIndex < totalLoadingThread)
- {
- // load rest pixels into shared memory
- uint index = GROUP_DIM_X * GROUP_DIM_Y + groupIndex;
- texelIndex = uint3(
- texelOffsetX + (index % dimXWithPad),
- texelOffsetY + (index / dimXWithPad),
- 0
- );
- PassSrg::SetColorDepth(index,
- PassSrg::m_diffuseTexture.Load(texelIndex).rgb,
- PassSrg::m_linearDepthTexture.Load(texelIndex).r
- );
- }
- // If current thread don't need further convolution then terminate and return
- // Case 1: factor < EPSILON, factor is zero so further computation is not necessary
- // Case 2: factor is nonzero but subsurface scattering is disabled at this pixel
- if(factor < EPSILON || colorX.w < 0)
- {
- PassSrg::m_outputTexture[dispatchID.xy] = float4(colorX.rgb, 0.0f);
- return;
- }
- GroupMemoryBarrierWithGroupSync();
- //+--------------------------------------+
- //| subsurface scattering preprocessing |
- //+--------------------------------------+
-
- // Flattened index of current pixel in shared memory
- uint2 sMemID = groupThreadID.xy + uint2(PAD, PAD);
- uint sMemIndex = sMemID.y * dimXWithPad + sMemID.x;
- // Inverse of scatter distance, symbol 's' is used to match with literatures
- float3 s = rcp(PassSrg::m_scatterDistanceTexture.Load(uint3(dispatchID.x, dispatchID.y, 0)).rgb + EPSILON);
- float minS = min(s.x, min(s.y, s.z));
-
- // Input view space linear depth of current pixel
- float2 texelSize = float2(1.0 / PassSrg::m_screenSize.x, 1.0 / PassSrg::m_screenSize.y);
-
- float depthX = PassSrg::GetDepth(sMemIndex);
-
- // Map radius sampled from profile (in millimeter) to distance on view plane (in pixel), consists of three steps
- // 'MILLIMETER_TO_METER' converts unit of radius on diffusion profile from millimeter to meter
- // '(ViewSrg::m_projectionMatrix[0][0] / depthX)' projects radius on profile to distance on viewing plane
- // 'texelSize' maps viewing plane distance to distance in pixel
- float profileToScreenX = (ViewSrg::m_projectionMatrix[0][0] / depthX) * MILLIMETER_TO_METER / texelSize.x;
- float profileToScreenY = (ViewSrg::m_projectionMatrix[1][1] / depthX) * MILLIMETER_TO_METER / texelSize.y;
- // This quantity controls the strength of depth based lod mechanism
- // 90% confidence interval, gives a roughly linear response with respect to input minS which more stable than true maximum radius
- float maxRadius = GetSampleInverseCDFApprox(0.9, minS);
- // Radius of diffusion profile on viewing plane in pixel, accounting for projection distortion
- // Use longer axis of ellipsoid as footprint
- uint maxFootPrint = max(maxRadius * profileToScreenX, maxRadius * profileToScreenY);
- // Determine how much sample will be used based on the depth of current pixel
- uint numSample = uint(NUMBER_OF_SAMPLE * quality * (maxFootPrint <= FOOTPRINT_MEDIUM ? maxFootPrint <= FOOTPRINT_SMALL ? SAMPLE_RATIO_1 : SAMPLE_RATIO_2 : 1.0));
- // Grid index used for interleaved sampling
- // range from 0-3 in a local 2x2 block as following shows:
- // -+---+---+-
- // | 0 | 1 |
- // -+---+---+-
- // | 2 | 3 |
- // -+---+---+-
- uint sampleGridIndex = (dispatchID.y % 2) * 2 + dispatchID.x % 2;
-
- //+------------------------------------+
- //| subsurface scattering convolution |
- //+------------------------------------+
-
- float3 totalSum = float3(0.0, 0.0, 0.0);
- float3 weightSum = float3(0.0, 0.0, 0.0);
- for(uint i = 0; i < numSample; ++i)
- {
- float3 sp;
- GetSample(i, sampleGridIndex, minS, sp);
- // Distance in pixel
- int u = round(sp.x * profileToScreenX);
- int v = round(sp.y * profileToScreenY);
-
- // Sampled neightbor's ID in shared memory
- uint2 nbrSMemID = sMemID + uint2(u, v);
-
- // Neighbor's depth
- float depthNbr = 0.0;
-
- // Neighbor's color
- float3 colorNbr = float3(0.0, 0.0, 0.0);
- // Fall back to global memory to fetch pixel information
- // ortherwise unable to render wide range scatter effect, e.g. (scatter distance > 1)
- if(nbrSMemID.x >= dimXWithPad || nbrSMemID.x < 0 || nbrSMemID.y >= (GROUP_DIM_Y + PAD2) || nbrSMemID.y < 0)
- {
- // Fall back to fetch from global memory
- uint3 nbrTexelIndex = uint3(dispatchID.x + u, dispatchID.y + v, 0);
- depthNbr = PassSrg::m_linearDepthTexture.Load(nbrTexelIndex).r;
- colorNbr = PassSrg::m_diffuseTexture.Load(nbrTexelIndex).rgb;
- }
- else
- {
- // Fetch depth and color from shared memory
- nbrSMemID = sMemID + uint2(u, v);
- uint nbrSMemIndex = nbrSMemID.y * dimXWithPad + nbrSMemID.x;
- depthNbr = PassSrg::GetDepth(nbrSMemIndex);
- colorNbr = PassSrg::GetRadiance(nbrSMemIndex);
- }
- // Skip invalid samples (i.e. sample out of valid region (depth = 0), totally black pixel - no energy spread out (color = 0))
- if(depthNbr > 0.0 && (colorNbr.x + colorNbr.y + colorNbr.z) > 0.0)
- {
- // Radial distance between current pixel and its neighbour
- float r = sp.z;
- float d = abs(depthX - depthNbr) * METER_TO_MILLIMETER;
- // Distance taking depth into account, in theory only surface points on convex objects can be computed in this way,
- // but in practice it works well as an approximation in all cases
- float surfaceDistance = sqrt(r * r + d * d);
-
- // As the sample is drawn from the distribution of color channel with longest scatter distance,
- // the pdf should follows the same rule as well for consistency
- float3 weight = Kernel(surfaceDistance, r, s, minS);
- totalSum += weight * colorNbr;
- weightSum += weight;
- }
- }
- // Force normalization with sum of weights
- float3 subsurfaceScatteringColor = totalSum / (weightSum + EPSILON);
- float3 outColor = lerp(colorX.rgb, subsurfaceScatteringColor, factor);
- // 1.0 in alpha channel will trigger 2x2 noise reduction filter in output merger to remove low discrepancy noises
- PassSrg::m_outputTexture[dispatchID.xy] = float4(outColor, 1.0);
- }
|