您最多选择25个主题
主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
466 行
18 KiB
466 行
18 KiB
// =============== Convolves transmitted radiance with the Disney diffusion profile ================
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
// Definitions
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
// #pragma enable_d3d11_debug_symbols
|
|
|
|
// Tweak parameters.
|
|
#define SSS_BILATERAL_FILTER 1
|
|
#define SSS_USE_LDS_CACHE 1
|
|
#define SSS_ENABLE_NEAR_FIELD 0 // Greatly increases the number of samples. Comes at a high cost.
|
|
#define SSS_SAMPLE_TEST_HTILE 0 // Potential optimization. YMMV.
|
|
#define SSS_USE_TANGENT_PLANE 0 // Improves the accuracy of the approximation(0 -> 1st order). High cost. Does not work with back-facing normals.
|
|
#define SSS_CLAMP_ARTIFACT 0 // Reduces bleeding. Use with SSS_USE_TANGENT_PLANE.
|
|
#define SSS_DEBUG_LOD 0
|
|
#define SSS_DEBUG_NORMAL_VS 0
|
|
|
|
// Do not modify these.
|
|
#include "../../../ShaderPass/ShaderPass.cs.hlsl"
|
|
#define SHADERPASS SHADERPASS_SUBSURFACE_SCATTERING
|
|
#define MILLIMETERS_PER_METER 1000
|
|
#define CENTIMETERS_PER_METER 100
|
|
#define GROUP_SIZE_1D 16
|
|
#define GROUP_SIZE_2D (GROUP_SIZE_1D * GROUP_SIZE_1D)
|
|
#define TEXTURE_CACHE_BORDER 2
|
|
#define TEXTURE_CACHE_SIZE_1D (GROUP_SIZE_1D + 2 * TEXTURE_CACHE_BORDER)
|
|
|
|
// Check for support of typed UAV loads from FORMAT_R16G16B16A16_FLOAT.
|
|
// TODO: query the format support more precisely.
|
|
#if !(defined(SHADER_API_PSSL) || defined(SHADER_API_XBOXONE))
|
|
#define USE_INTERMEDIATE_BUFFER
|
|
#endif
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
// Included headers
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
#include "../../../../Core/ShaderLibrary/Packing.hlsl"
|
|
#include "../../../../Core/ShaderLibrary/SpaceFillingCurves.hlsl"
|
|
#include "../../../ShaderVariables.hlsl"
|
|
#define UNITY_MATERIAL_LIT
|
|
#include "../../../Material/Material.hlsl"
|
|
#include "../../../Lighting/LightDefinition.cs.hlsl"
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
// Inputs & outputs
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
float4 _WorldScales[SSS_N_PROFILES]; // Size of the world unit in meters (only the X component is used)
|
|
float4 _FilterKernels[SSS_N_PROFILES][SSS_N_SAMPLES_NEAR_FIELD]; // XY = near field, ZW = far field; 0 = radius, 1 = reciprocal of the PDF
|
|
|
|
DECLARE_GBUFFER_TEXTURE(_GBufferTexture); // Contains the albedo and SSS parameters
|
|
TEXTURE2D(_DepthTexture); // Z-buffer
|
|
TEXTURE2D(_HTile); // DXGI_FORMAT_R8_UINT is not supported by Unity
|
|
TEXTURE2D(_IrradianceSource); // Includes transmitted light
|
|
|
|
#ifdef USE_INTERMEDIATE_BUFFER
|
|
RW_TEXTURE2D(float4, _CameraFilteringTexture); // Target texture
|
|
#else
|
|
RW_TEXTURE2D(float4, _CameraColorTexture); // Target texture
|
|
#endif
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
// Implementation
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
// 6656 bytes used. It appears that the reserved LDS space must be a multiple of 512 bytes.
|
|
#if SSS_USE_LDS_CACHE
|
|
groupshared float4 textureCache[TEXTURE_CACHE_SIZE_1D * TEXTURE_CACHE_SIZE_1D]; // {irradiance, linearDepth}
|
|
#endif
|
|
groupshared bool processGroup;
|
|
|
|
bool StencilTest(int2 pixelCoord, float stencilRef)
|
|
{
|
|
bool passedStencilTest;
|
|
|
|
#if SSS_SAMPLE_TEST_HTILE
|
|
int2 tileCoord = pixelCoord / 8;
|
|
|
|
// Perform the stencil test (reject at the tile rate).
|
|
passedStencilTest = stencilRef == LOAD_TEXTURE2D(_HTile, tileCoord).r;
|
|
|
|
[branch] if (passedStencilTest)
|
|
#else
|
|
// It is extremely uncommon for individual samples to fail the HTile test.
|
|
// Unfortunately, our copy of HTile does not allow to accept at the tile rate.
|
|
// Therefore, we choose not to perform the HiS test here.
|
|
#endif
|
|
{
|
|
// Unfortunately, our copy of HTile does not allow to accept at the tile rate.
|
|
// Therefore, we have to additionally perform the stencil test at the pixel rate.
|
|
// We check the tagged irradiance buffer to avoid an extra stencil texture fetch.
|
|
passedStencilTest = TestLightingForSSS(LOAD_TEXTURE2D(_IrradianceSource, pixelCoord).rgb);
|
|
}
|
|
|
|
return passedStencilTest;
|
|
}
|
|
|
|
#if SSS_USE_LDS_CACHE
|
|
float4 LoadSampleFromCacheMemory(int2 cacheCoord)
|
|
{
|
|
return textureCache[Mad24(TEXTURE_CACHE_SIZE_1D, cacheCoord.y, cacheCoord.x)];
|
|
}
|
|
#endif
|
|
|
|
float4 LoadSampleFromVideoMemory(int2 pixelCoord)
|
|
{
|
|
float3 irradiance = LOAD_TEXTURE2D(_IrradianceSource, pixelCoord).rgb;
|
|
float depth = LOAD_TEXTURE2D(_DepthTexture, pixelCoord).r;
|
|
|
|
return float4(irradiance, LinearEyeDepth(depth, _ZBufferParams));
|
|
}
|
|
|
|
// Returns {irradiance, linearDepth}.
|
|
float4 LoadSample(int2 pixelCoord, int2 cacheAnchor)
|
|
{
|
|
int2 cacheCoord = pixelCoord - cacheAnchor;
|
|
bool isInCache = max((uint)cacheCoord.x, (uint)cacheCoord.y) < TEXTURE_CACHE_SIZE_1D;
|
|
|
|
#if SSS_USE_LDS_CACHE
|
|
[branch] if (isInCache)
|
|
{
|
|
return LoadSampleFromCacheMemory(cacheCoord);
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
float stencilRef = STENCILLIGHTINGUSAGE_SPLIT_LIGHTING;
|
|
|
|
[branch] if (StencilTest(pixelCoord, stencilRef))
|
|
{
|
|
return LoadSampleFromVideoMemory(pixelCoord);
|
|
}
|
|
else
|
|
{
|
|
return float4(0, 0, 0, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Computes the value of the integrand in polar coordinates: f(r, s) = r * R(r, s).
|
|
// f(r, s) = (Exp[-r * s] + Exp[-r * s / 3]) * (s / (8 * Pi))
|
|
// We can drop the constant (s / (8 * Pi)) due to the subsequent weight renormalization.
|
|
float3 DisneyProfilePolar(float r, float3 S)
|
|
{
|
|
#if 0
|
|
float3 expOneThird = exp(((-1.0 / 3.0) * r) * S);
|
|
#else
|
|
// Help the compiler.
|
|
float k = (-1.0 / 3.0) * LOG2_E;
|
|
float3 p = (k * r) * S;
|
|
float3 expOneThird = exp2(p);
|
|
#endif
|
|
return expOneThird + expOneThird * expOneThird * expOneThird;
|
|
}
|
|
|
|
// Computes f(r, s)/p(r, s), s.t. r = sqrt(xy^2 + z^2).
|
|
// Rescaling of the PDF is handled by 'totalWeight'.
|
|
float3 ComputeBilateralWeight(float xy2, float z, float mmPerUnit, float3 S, float rcpPdf)
|
|
{
|
|
#if (SSS_BILATERAL_FILTER == 0)
|
|
z = 0;
|
|
#endif
|
|
|
|
#if SSS_USE_TANGENT_PLANE
|
|
// Both 'xy2' and 'z' require conversion to millimeters.
|
|
float r = sqrt(xy2 + z * z) * mmPerUnit;
|
|
#else
|
|
// Only 'z' requires conversion to millimeters.
|
|
float r = sqrt(xy2 + (z * mmPerUnit) * (z * mmPerUnit));
|
|
#endif
|
|
|
|
#if SSS_CLAMP_ARTIFACT
|
|
return saturate(DisneyProfilePolar(r, S) * rcpPdf);
|
|
#else
|
|
return DisneyProfilePolar(r, S) * rcpPdf;
|
|
#endif
|
|
}
|
|
|
|
void EvaluateSample(uint i, uint n, uint profileID, uint iR, uint iP, float2 centerCoord, int2 cacheAnchor,
|
|
float3 shapeParam, float3 centerPosVS, float mmPerUnit, float2 pixelsPerMm,
|
|
float3 tangentX, float3 tangentY, float4x4 projMatrix,
|
|
inout float3 totalIrradiance, inout float3 totalWeight)
|
|
{
|
|
float r = _FilterKernels[profileID][i][iR];
|
|
// The relative sample position is known at the compile time.
|
|
float phi = SampleDiskFibonacci(i, n).y;
|
|
float2 vec = r * float2(cos(phi), sin(phi));
|
|
|
|
// Compute the screen-space position and the squared distance (in mm) in the image plane.
|
|
int2 position; float xy2;
|
|
|
|
#if SSS_USE_TANGENT_PLANE
|
|
float3 relPosVS = vec.x * tangentX + vec.y * tangentY;
|
|
float3 positionVS = centerPosVS + relPosVS;
|
|
float4 positionCS = mul(projMatrix, float4(positionVS, 1));
|
|
float2 positionSS = ComputeScreenSpacePosition(positionCS);
|
|
|
|
position = (int2)(positionSS * _ScreenSize.xy);
|
|
xy2 = dot(relPosVS.xy, relPosVS.xy);
|
|
#else
|
|
position = (int2)(centerCoord + vec * pixelsPerMm);
|
|
xy2 = r * r;
|
|
#endif
|
|
|
|
float4 textureSample = LoadSample(position, cacheAnchor);
|
|
float3 irradiance = textureSample.rgb;
|
|
|
|
// Check the results of the stencil test.
|
|
if (TestLightingForSSS(irradiance))
|
|
{
|
|
// Apply bilateral weighting.
|
|
float linearDepth = textureSample.a;
|
|
float z = linearDepth - centerPosVS.z;
|
|
float p = _FilterKernels[profileID][i][iP];
|
|
float3 w = ComputeBilateralWeight(xy2, z, mmPerUnit, shapeParam, p);
|
|
|
|
totalIrradiance += w * irradiance;
|
|
totalWeight += w;
|
|
}
|
|
else
|
|
{
|
|
// The irradiance is 0. This could happen for 2 reasons.
|
|
// Most likely, the surface fragment does not have an SSS material.
|
|
// Alternatively, our sample comes from a region without any geometry.
|
|
// Our blur is energy-preserving, so 'centerWeight' should be set to 0.
|
|
// We do not terminate the loop since we want to gather the contribution
|
|
// of the remaining samples (e.g. in case of hair covering skin).
|
|
}
|
|
}
|
|
|
|
void StoreResult(uint2 pixelCoord, float3 irradiance)
|
|
{
|
|
#ifdef USE_INTERMEDIATE_BUFFER
|
|
_CameraFilteringTexture[pixelCoord] = float4(irradiance, 1);
|
|
#else
|
|
_CameraColorTexture[pixelCoord] += float4(irradiance, 0);
|
|
#endif
|
|
}
|
|
|
|
#pragma kernel SubsurfaceScattering
|
|
|
|
[numthreads(GROUP_SIZE_2D, 1, 1)]
|
|
void SubsurfaceScattering(uint2 groupId : SV_GroupID,
|
|
uint groupThreadId : SV_GroupThreadID)
|
|
{
|
|
// Note: any factor of 64 is a suitable wave size for our algorithm.
|
|
uint waveIndex = WaveReadFirstLane(groupThreadId / 64);
|
|
uint laneIndex = groupThreadId % 64;
|
|
uint quadIndex = laneIndex / 4;
|
|
|
|
// Arrange threads in the Morton order to optimally match the memory layout of GCN tiles.
|
|
uint mortonCode = groupThreadId;
|
|
uint2 localCoord = DecodeMorton2D(mortonCode);
|
|
uint2 tileAnchor = groupId * GROUP_SIZE_1D;
|
|
uint2 pixelCoord = tileAnchor + localCoord;
|
|
int2 cacheAnchor = (int2)tileAnchor - TEXTURE_CACHE_BORDER;
|
|
uint2 cacheCoord = localCoord + TEXTURE_CACHE_BORDER;
|
|
float stencilRef = STENCILLIGHTINGUSAGE_SPLIT_LIGHTING;
|
|
|
|
[branch] if (groupThreadId == 0)
|
|
{
|
|
// Check whether the thread group needs to perform any work.
|
|
float s00 = LOAD_TEXTURE2D(_HTile, 2 * groupId + uint2(0, 0)).r;
|
|
float s10 = LOAD_TEXTURE2D(_HTile, 2 * groupId + uint2(1, 0)).r;
|
|
float s01 = LOAD_TEXTURE2D(_HTile, 2 * groupId + uint2(0, 1)).r;
|
|
float s11 = LOAD_TEXTURE2D(_HTile, 2 * groupId + uint2(1, 1)).r;
|
|
|
|
// Perform the stencil test (reject at the tile rate).
|
|
processGroup = (stencilRef == s00 || stencilRef == s10 || stencilRef == s01 || stencilRef == s11);
|
|
}
|
|
|
|
// Wait for the LDS.
|
|
GroupMemoryBarrierWithGroupSync();
|
|
|
|
[branch] if (!processGroup) { return; }
|
|
|
|
float3 centerIrradiance = 0;
|
|
float centerDepth = 0;
|
|
float4 cachedValue = 0;
|
|
|
|
bool passedStencilTest = StencilTest((int2)pixelCoord, stencilRef);
|
|
|
|
[branch] if (passedStencilTest)
|
|
{
|
|
centerIrradiance = LOAD_TEXTURE2D(_IrradianceSource, pixelCoord).rgb;
|
|
centerDepth = LOAD_TEXTURE2D(_DepthTexture, pixelCoord).r;
|
|
cachedValue = float4(centerIrradiance, LinearEyeDepth(centerDepth, _ZBufferParams));
|
|
}
|
|
|
|
#if SSS_USE_LDS_CACHE
|
|
// Populate the central region of the LDS cache.
|
|
textureCache[Mad24(TEXTURE_CACHE_SIZE_1D, cacheCoord.y, cacheCoord.x)] = cachedValue;
|
|
|
|
uint numBorderQuadsPerWave = TEXTURE_CACHE_SIZE_1D / 2 - 1;
|
|
uint halfCacheWidthInQuads = TEXTURE_CACHE_SIZE_1D / 4;
|
|
|
|
[branch] if (quadIndex < numBorderQuadsPerWave)
|
|
{
|
|
// Fetch another texel into the LDS.
|
|
uint2 startQuad = halfCacheWidthInQuads * uint2(waveIndex & 1, waveIndex >> 1);
|
|
|
|
uint2 quadCoord;
|
|
|
|
// The traversal order is such that the quad's X coordinate is monotonically increasing.
|
|
// Note: the compiler can heavily optimize the code below, as the switch is scalar,
|
|
// and there are very few unique values due to the symmetry.
|
|
switch (waveIndex)
|
|
{
|
|
case 0:
|
|
quadCoord.x = max(0, (int)(quadIndex - (halfCacheWidthInQuads - 1)));
|
|
quadCoord.y = max(0, (int)((halfCacheWidthInQuads - 1) - quadIndex));
|
|
break;
|
|
case 1:
|
|
quadCoord.x = min(quadIndex, halfCacheWidthInQuads - 1);
|
|
quadCoord.y = max(0, (int)(quadIndex - (halfCacheWidthInQuads - 1)));
|
|
break;
|
|
case 2:
|
|
quadCoord.x = max(0, (int)(quadIndex - (halfCacheWidthInQuads - 1)));
|
|
quadCoord.y = min(quadIndex, halfCacheWidthInQuads - 1);
|
|
break;
|
|
default: // 3
|
|
quadCoord.x = min(quadIndex, halfCacheWidthInQuads - 1);
|
|
quadCoord.y = min(halfCacheWidthInQuads - 1, 2 * (halfCacheWidthInQuads - 1) - quadIndex);
|
|
break;
|
|
}
|
|
|
|
uint2 cacheCoord2 = 2 * (startQuad + quadCoord) + uint2(laneIndex & 1, (laneIndex >> 1) & 1);
|
|
int2 pixelCoord2 = (int2)(tileAnchor + cacheCoord2) - TEXTURE_CACHE_BORDER;
|
|
float4 cachedValue2 = 0;
|
|
|
|
[branch] if (StencilTest(pixelCoord2, stencilRef))
|
|
{
|
|
cachedValue2 = LoadSampleFromVideoMemory(pixelCoord2);
|
|
}
|
|
|
|
// Populate the border region of the LDS cache.
|
|
textureCache[Mad24(TEXTURE_CACHE_SIZE_1D, cacheCoord2.y, cacheCoord2.x)] = cachedValue2;
|
|
}
|
|
|
|
// Wait for the LDS.
|
|
GroupMemoryBarrierWithGroupSync();
|
|
#endif
|
|
|
|
bool isOffScreen = pixelCoord.x >= (uint)_ScreenSize.x || pixelCoord.y >= (uint)_ScreenSize.y;
|
|
|
|
[branch] if (!passedStencilTest || isOffScreen) { return; }
|
|
|
|
PositionInputs posInput = GetPositionInput(pixelCoord, _ScreenSize.zw);
|
|
|
|
float3 unused;
|
|
|
|
// The result of the stencil test allows us to statically determine the material type (SSS).
|
|
BSDFData bsdfData;
|
|
FETCH_GBUFFER(gbuffer, _GBufferTexture, pixelCoord);
|
|
DECODE_FROM_GBUFFER(gbuffer, MATERIALFEATUREFLAGS_LIT_SSS, bsdfData, unused);
|
|
|
|
int profileID = bsdfData.subsurfaceProfile;
|
|
float distScale = bsdfData.subsurfaceRadius;
|
|
float3 shapeParam = _ShapeParams[profileID].rgb;
|
|
float maxDistance = _ShapeParams[profileID].a;
|
|
|
|
// Reconstruct the view-space position corresponding to the central sample.
|
|
float2 centerPosSS = posInput.positionSS;
|
|
float2 cornerPosSS = centerPosSS + 0.5 * _ScreenSize.zw;
|
|
float3 centerPosVS = ComputeViewSpacePosition(centerPosSS, centerDepth, _InvProjMatrix);
|
|
float3 cornerPosVS = ComputeViewSpacePosition(cornerPosSS, centerDepth, _InvProjMatrix);
|
|
|
|
// Rescaling the filter is equivalent to inversely scaling the world.
|
|
float mmPerUnit = MILLIMETERS_PER_METER * (_WorldScales[profileID].x / distScale);
|
|
float unitsPerMm = rcp(mmPerUnit);
|
|
|
|
// Compute the view-space dimensions of the pixel as a quad projected onto geometry.
|
|
float2 unitsPerPixel = 2 * abs(cornerPosVS.xy - centerPosVS.xy);
|
|
float2 pixelsPerMm = rcp(unitsPerPixel) * unitsPerMm;
|
|
|
|
// We perform point sampling. Therefore, we can avoid the cost
|
|
// of filtering if we stay within the bounds of the current pixel.
|
|
// We use the value of 1 instead of 0.5 as an optimization.
|
|
// N.b.: our LoD selection algorithm is the same regardless of
|
|
// whether we integrate over the tangent plane or not, since we
|
|
// don't want the orientation of the tangent plane to create
|
|
// divergence of execution across the warp.
|
|
float maxDistInPixels = maxDistance * max(pixelsPerMm.x, pixelsPerMm.y);
|
|
|
|
float3 albedo = ApplyDiffuseTexturingMode(bsdfData);
|
|
|
|
[branch] if (distScale == 0 || maxDistInPixels < 1)
|
|
{
|
|
#if SSS_DEBUG_LOD
|
|
StoreResult(pixelCoord, float3(0, 0, 1));
|
|
#else
|
|
StoreResult(pixelCoord, albedo * centerIrradiance);
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
float4x4 viewMatrix, projMatrix;
|
|
GetLeftHandedViewSpaceMatrices(viewMatrix, projMatrix);
|
|
|
|
// Compute the tangent frame in view space.
|
|
float3 normalVS = mul((float3x3)viewMatrix, bsdfData.normalWS);
|
|
float3 tangentX = GetLocalFrame(normalVS)[0] * unitsPerMm;
|
|
float3 tangentY = GetLocalFrame(normalVS)[1] * unitsPerMm;
|
|
|
|
#if SSS_DEBUG_NORMAL_VS
|
|
// We expect the normal to be front-facing.
|
|
float3 viewDirVS = normalize(centerPosVS);
|
|
if (dot(normalVS, viewDirVS) >= 0)
|
|
{
|
|
StoreResult(pixelCoord, float3(1, 1, 1));
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
// Use more samples for SS regions larger than 5x5 pixels (rotated by 45 degrees).
|
|
bool useNearFieldKernel = SSS_ENABLE_NEAR_FIELD && maxDistInPixels > SSS_LOD_THRESHOLD;
|
|
|
|
#if SSS_DEBUG_LOD
|
|
StoreResult(pixelCoord, useNearFieldKernel ? float3(1, 0, 0) : float3(0.5, 0.5, 0);
|
|
return;
|
|
#endif
|
|
|
|
// Compute the indices used to access the individual components of the float4 of the kernel.
|
|
uint iR = useNearFieldKernel ? 0 : 2; // radius
|
|
uint iP = useNearFieldKernel ? 1 : 3; // rcp(pdf)
|
|
|
|
float centerRadius = _FilterKernels[profileID][0][iR];
|
|
float centerRcpPdf = _FilterKernels[profileID][0][iP];
|
|
float3 centerWeight = DisneyProfilePolar(centerRadius, shapeParam) * centerRcpPdf;
|
|
|
|
// Accumulate filtered irradiance and bilateral weights (for renormalization).
|
|
float3 totalIrradiance = centerWeight * centerIrradiance;
|
|
float3 totalWeight = centerWeight;
|
|
|
|
int i, n; // Declare once to avoid the warning from the Unity shader compiler.
|
|
|
|
[unroll]
|
|
for (i = 1, n = SSS_N_SAMPLES_FAR_FIELD; i < n; i++)
|
|
{
|
|
// Integrate over the image or tangent plane in the view space.
|
|
EvaluateSample(i, n, profileID, iR, iP, pixelCoord + 0.5, cacheAnchor,
|
|
shapeParam, centerPosVS, mmPerUnit, pixelsPerMm,
|
|
tangentX, tangentY, projMatrix,
|
|
totalIrradiance, totalWeight);
|
|
}
|
|
|
|
[branch] if (!useNearFieldKernel)
|
|
{
|
|
StoreResult(pixelCoord, albedo * totalIrradiance / totalWeight);
|
|
return;
|
|
}
|
|
|
|
[unroll]
|
|
for (i = SSS_N_SAMPLES_FAR_FIELD, n = SSS_N_SAMPLES_NEAR_FIELD; i < n; i++)
|
|
{
|
|
// Integrate over the image or tangent plane in the view space.
|
|
EvaluateSample(i, n, profileID, iR, iP, pixelCoord + 0.5, cacheAnchor,
|
|
shapeParam, centerPosVS, mmPerUnit, pixelsPerMm,
|
|
tangentX, tangentY, projMatrix,
|
|
totalIrradiance, totalWeight);
|
|
}
|
|
|
|
StoreResult(pixelCoord, albedo * totalIrradiance / totalWeight);
|
|
}
|