playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
629 lines (495 loc) • 23.1 kB
JavaScript
var clusteredLight_default = (
/* glsl */
`
// include this before shadow / cookie code
uniform highp usampler2D clusterWorldTexture;
uniform highp sampler2D lightsTexture;
// TODO: when VSM shadow is supported, it needs to use sampler2D in webgl2
uniform sampler2DShadow shadowAtlasTexture;
uniform sampler2D cookieAtlasTexture;
uniform int clusterMaxCells;
// number of lights in the cluster structure
uniform int numClusteredLights;
// width of the cluster texture
uniform int clusterTextureWidth;
uniform vec3 clusterCellsCountByBoundsSize;
uniform vec3 clusterBoundsMin;
uniform vec3 clusterBoundsDelta;
uniform ivec3 clusterCellsDot;
uniform ivec3 clusterCellsMax;
uniform vec2 shadowAtlasParams;
// structure storing light properties of a clustered light. Vectors and scalars are interleaved
// so each vec3 packs with an adjacent 4-byte field into a 16-byte slot, minimising padding for
// compilers that don't reorder struct members.
struct ClusterLightData {
// world space position
vec3 position;
// light index in the lights texture
int lightIndex;
// world space direction (spot light only)
vec3 direction;
// area light shape
uint shape;
// color
vec3 color;
// 0.0 if the light doesn't cast shadows
float shadowIntensity;
// range of the light
float range;
// compressed biases, two half-floats stored in a float
float biasesData;
// intensity of the cookie
float cookieIntensity;
// true for spot lights
bool isSpot;
// light follow mode
bool falloffModeLinear;
// light mask (mutually exclusive)
bool isDynamic;
bool isLightmapped;
};
// Spot light cone angles, decoded on demand only when the light is a spot light.
struct ClusterLightSpotData {
float innerConeAngleCos;
float outerConeAngleCos;
};
// Area light dimensions and orientation, decoded on demand only for non-punctual lights.
struct ClusterLightAreaData {
vec3 halfWidth;
vec3 halfHeight;
};
// Shadow bias parameters, decoded on demand only when the light casts shadows.
struct ClusterLightShadowData {
float shadowBias;
float shadowNormalBias;
};
// Note: on some devices (tested on Pixel 3A XL), this matrix when stored inside the light struct has lower precision compared to
// when stored outside, so we store it outside to avoid spot shadow flickering. This might need to be done to other / all members
// of the structure if further similar issues are observed. See https://github.com/KhronosGroup/WebGL/issues/3351
// shadow (spot light only) / cookie projection matrix
mat4 lightProjectionMatrix;
// NOTE: On some Samsung devices, these values can suffer precision / corruption issues when stored
// as members of ClusterLightData. Keep them as module-scope temporaries instead. See issue #7800.
uint clusterLightData_flags; // 32bit of flags
float clusterLightData_anglesData; // compressed angles, two half-floats stored in a float
uint clusterLightData_colorBFlagsData; // blue color component and angle flags (as uint for efficient bit operations)
vec4 sampleLightTextureF(int lightIndex, int index) {
return texelFetch(lightsTexture, ivec2(index, lightIndex), 0);
}
ClusterLightData decodeClusterLightCore(int lightIndex) {
ClusterLightData clusterLightData;
// light index
clusterLightData.lightIndex = lightIndex;
// sample data encoding half-float values into 32bit uints
vec4 halfData = sampleLightTextureF(lightIndex, {CLUSTER_TEXTURE_COLOR_ANGLES_BIAS});
// store values needed by later decode steps (anglesData / colorBFlagsData live outside the
// struct due to Samsung precision issues - see #7800)
clusterLightData_anglesData = halfData.z;
clusterLightData.biasesData = halfData.w;
clusterLightData_colorBFlagsData = floatBitsToUint(halfData.y);
// decompress color half-floats
vec2 colorRG = unpackHalf2x16(floatBitsToUint(halfData.x));
vec2 colorB_flags = unpackHalf2x16(clusterLightData_colorBFlagsData);
clusterLightData.color = vec3(colorRG, colorB_flags.x) * {LIGHT_COLOR_DIVIDER};
// position and range, full floats
vec4 lightPosRange = sampleLightTextureF(lightIndex, {CLUSTER_TEXTURE_POSITION_RANGE});
clusterLightData.position = lightPosRange.xyz;
clusterLightData.range = lightPosRange.w;
// spot direction & flags data
vec4 lightDir_Flags = sampleLightTextureF(lightIndex, {CLUSTER_TEXTURE_DIRECTION_FLAGS});
// spot light direction
clusterLightData.direction = lightDir_Flags.xyz;
// 32bit flags (kept outside the struct, see #7800)
clusterLightData_flags = floatBitsToUint(lightDir_Flags.w);
clusterLightData.isSpot = (clusterLightData_flags & (1u << 30u)) != 0u;
clusterLightData.shape = (clusterLightData_flags >> 28u) & 0x3u;
clusterLightData.falloffModeLinear = (clusterLightData_flags & (1u << 27u)) == 0u;
clusterLightData.shadowIntensity = float((clusterLightData_flags >> 0u) & 0xFFu) / 255.0;
clusterLightData.cookieIntensity = float((clusterLightData_flags >> 8u) & 0xFFu) / 255.0;
clusterLightData.isDynamic = (clusterLightData_flags & (1u << 22u)) != 0u;
clusterLightData.isLightmapped = (clusterLightData_flags & (1u << 21u)) != 0u;
return clusterLightData;
}
ClusterLightSpotData decodeClusterLightSpot() {
// decompress spot light angles
uint angleFlags = (clusterLightData_colorBFlagsData >> 16u) & 0xFFFFu; // Extract upper 16 bits as integer
vec2 angleValues = unpackHalf2x16(floatBitsToUint(clusterLightData_anglesData));
float innerVal = angleValues.x;
float outerVal = angleValues.y;
// decode based on flags (branch-free)
float innerIsVersine = float(angleFlags & 1u); // bit 0: inner angle format
float outerIsVersine = float((angleFlags >> 1u) & 1u); // bit 1: outer angle format
return ClusterLightSpotData(
mix(innerVal, 1.0 - innerVal, innerIsVersine),
mix(outerVal, 1.0 - outerVal, outerIsVersine)
);
}
vec3 decodeClusterLightOmniAtlasViewport(int lightIndex) {
return sampleLightTextureF(lightIndex, {CLUSTER_TEXTURE_PROJ_MAT_0}).xyz;
}
ClusterLightAreaData decodeClusterLightAreaData(int lightIndex) {
return ClusterLightAreaData(
sampleLightTextureF(lightIndex, {CLUSTER_TEXTURE_AREA_DATA_WIDTH}).xyz,
sampleLightTextureF(lightIndex, {CLUSTER_TEXTURE_AREA_DATA_HEIGHT}).xyz
);
}
mat4 decodeClusterLightProjectionMatrixData(int lightIndex) {
// shadow matrix
vec4 m0 = sampleLightTextureF(lightIndex, {CLUSTER_TEXTURE_PROJ_MAT_0});
vec4 m1 = sampleLightTextureF(lightIndex, {CLUSTER_TEXTURE_PROJ_MAT_1});
vec4 m2 = sampleLightTextureF(lightIndex, {CLUSTER_TEXTURE_PROJ_MAT_2});
vec4 m3 = sampleLightTextureF(lightIndex, {CLUSTER_TEXTURE_PROJ_MAT_3});
return mat4(m0, m1, m2, m3);
}
ClusterLightShadowData decodeClusterLightShadowData(float biasesData) {
// shadow biases
vec2 biases = unpackHalf2x16(floatBitsToUint(biasesData));
return ClusterLightShadowData(biases.x, biases.y);
}
vec4 decodeClusterLightCookieData() {
// extract channel mask from flags
uint cookieFlags = (clusterLightData_flags >> 23u) & 0x0Fu; // 4bits, each bit enables a channel
vec4 mask = vec4(uvec4(cookieFlags) & uvec4(1u, 2u, 4u, 8u));
return step(1.0, mask); // Normalize to 0.0 or 1.0
}
void evaluateLight(
ClusterLightData light,
vec3 worldNormal,
vec3 viewDir,
vec3 reflectionDir,
vec3 clearcoatReflectionDir,
float gloss,
vec3 specularity,
vec3 geometricNormal,
mat3 tbn,
vec3 iridescenceFresnel,
vec3 clearcoat_worldNormal,
float clearcoat_gloss,
float sheen_gloss,
float iridescence_intensity
) {
vec3 cookieAttenuation = vec3(1.0);
float diffuseAttenuation = 1.0;
float falloffAttenuation = 1.0;
// evaluate omni part of the light
vec3 lightDirW = evalOmniLight(light.position);
vec3 lightDirNormW = normalize(lightDirW);
// distance attenuation
if (light.shape != {LIGHTSHAPE_PUNCTUAL}) { // area light
// area lights
ClusterLightAreaData areaData = decodeClusterLightAreaData(light.lightIndex);
// handle light shape
if (light.shape == {LIGHTSHAPE_RECT}) {
calcRectLightValues(light.position, areaData.halfWidth, areaData.halfHeight);
} else if (light.shape == {LIGHTSHAPE_DISK}) {
calcDiskLightValues(light.position, areaData.halfWidth, areaData.halfHeight);
} else { // sphere
calcSphereLightValues(light.position, areaData.halfWidth, areaData.halfHeight);
}
falloffAttenuation = getFalloffWindow(light.range, lightDirW);
} else
{ // punctual light
if (light.falloffModeLinear)
falloffAttenuation = getFalloffLinear(light.range, lightDirW);
else
falloffAttenuation = getFalloffInvSquared(light.range, lightDirW);
}
if (falloffAttenuation > 0.00001) {
if (light.shape != {LIGHTSHAPE_PUNCTUAL}) { // area light
// handle light shape
if (light.shape == {LIGHTSHAPE_RECT}) {
diffuseAttenuation = getRectLightDiffuse(worldNormal, viewDir, lightDirW, lightDirNormW) * 16.0;
} else if (light.shape == {LIGHTSHAPE_DISK}) {
diffuseAttenuation = getDiskLightDiffuse(worldNormal, viewDir, lightDirW, lightDirNormW) * 16.0;
} else { // sphere
diffuseAttenuation = getSphereLightDiffuse(worldNormal, viewDir, lightDirW, lightDirNormW) * 16.0;
}
} else
{
falloffAttenuation *= getLightDiffuse(worldNormal, viewDir, lightDirNormW);
}
// spot light falloff
if (light.isSpot) {
ClusterLightSpotData spotData = decodeClusterLightSpot();
falloffAttenuation *= getSpotEffect(light.direction, spotData.innerConeAngleCos, spotData.outerConeAngleCos, lightDirNormW);
}
if (falloffAttenuation > 0.00001) {
// shadow / cookie
if (light.shadowIntensity > 0.0 || light.cookieIntensity > 0.0) {
vec3 omniAtlasViewport = vec3(0.0);
// shared shadow / cookie data depends on light type
if (light.isSpot) {
lightProjectionMatrix = decodeClusterLightProjectionMatrixData(light.lightIndex);
} else {
omniAtlasViewport = decodeClusterLightOmniAtlasViewport(light.lightIndex);
}
float shadowTextureResolution = shadowAtlasParams.x;
float shadowEdgePixels = shadowAtlasParams.y;
// cookie
if (light.cookieIntensity > 0.0) {
vec4 cookieChannelMask = decodeClusterLightCookieData();
if (light.isSpot) {
cookieAttenuation = getCookie2DClustered(TEXTURE_PASS(cookieAtlasTexture), lightProjectionMatrix, vPositionW, light.cookieIntensity, cookieChannelMask);
} else {
cookieAttenuation = getCookieCubeClustered(TEXTURE_PASS(cookieAtlasTexture), lightDirW, light.cookieIntensity, cookieChannelMask, shadowTextureResolution, shadowEdgePixels, omniAtlasViewport);
}
}
// shadow
if (light.shadowIntensity > 0.0) {
ClusterLightShadowData shadowData = decodeClusterLightShadowData(light.biasesData);
vec4 shadowParams = vec4(shadowTextureResolution, shadowData.shadowNormalBias, shadowData.shadowBias, 1.0 / light.range);
if (light.isSpot) {
// spot shadow
vec3 shadowCoord = getShadowCoordPerspZbufferNormalOffset(lightProjectionMatrix, shadowParams, geometricNormal);
float shadow = getShadowSpotClusteredPCF1(SHADOWMAP_PASS(shadowAtlasTexture), shadowCoord, shadowParams);
float shadow = getShadowSpotClusteredPCF3(SHADOWMAP_PASS(shadowAtlasTexture), shadowCoord, shadowParams);
float shadow = getShadowSpotClusteredPCF5(SHADOWMAP_PASS(shadowAtlasTexture), shadowCoord, shadowParams);
float shadow = getShadowSpotClusteredPCSS(SHADOWMAP_PASS(shadowAtlasTexture), shadowCoord, shadowParams);
falloffAttenuation *= mix(1.0, shadow, light.shadowIntensity);
} else {
// omni shadow
vec3 dir = normalOffsetPointShadow(shadowParams, light.position, lightDirW, lightDirNormW, geometricNormal); // normalBias adjusted for distance
float shadow = getShadowOmniClusteredPCF1(SHADOWMAP_PASS(shadowAtlasTexture), shadowParams, omniAtlasViewport, shadowEdgePixels, dir);
float shadow = getShadowOmniClusteredPCF3(SHADOWMAP_PASS(shadowAtlasTexture), shadowParams, omniAtlasViewport, shadowEdgePixels, dir);
float shadow = getShadowOmniClusteredPCF5(SHADOWMAP_PASS(shadowAtlasTexture), shadowParams, omniAtlasViewport, shadowEdgePixels, dir);
falloffAttenuation *= mix(1.0, shadow, light.shadowIntensity);
}
}
}
}
// diffuse / specular / clearcoat
if (light.shape != {LIGHTSHAPE_PUNCTUAL}) { // area light
// area light diffuse
{
vec3 areaDiffuse = (diffuseAttenuation * falloffAttenuation) * light.color * cookieAttenuation;
areaDiffuse = mix(areaDiffuse, vec3(0), dLTCSpecFres);
// area light diffuse - it does not mix diffuse lighting into specular attenuation
dDiffuseLight += areaDiffuse;
}
// specular and clear coat are material settings and get included by a define based on the material
// area light specular
float areaLightSpecular;
if (light.shape == {LIGHTSHAPE_RECT}) {
areaLightSpecular = getRectLightSpecular(worldNormal, viewDir);
} else if (light.shape == {LIGHTSHAPE_DISK}) {
areaLightSpecular = getDiskLightSpecular(worldNormal, viewDir);
} else { // sphere
areaLightSpecular = getSphereLightSpecular(worldNormal, viewDir);
}
dSpecularLight += dLTCSpecFres * areaLightSpecular * falloffAttenuation * light.color * cookieAttenuation;
// area light specular clear coat
float areaLightSpecularCC;
if (light.shape == {LIGHTSHAPE_RECT}) {
areaLightSpecularCC = getRectLightSpecular(clearcoat_worldNormal, viewDir);
} else if (light.shape == {LIGHTSHAPE_DISK}) {
areaLightSpecularCC = getDiskLightSpecular(clearcoat_worldNormal, viewDir);
} else { // sphere
areaLightSpecularCC = getSphereLightSpecular(clearcoat_worldNormal, viewDir);
}
ccSpecularLight += ccLTCSpecFres * areaLightSpecularCC * falloffAttenuation * light.color * cookieAttenuation;
} else
{ // punctual light
// punctual light diffuse
{
vec3 punctualDiffuse = falloffAttenuation * light.color * cookieAttenuation;
punctualDiffuse = mix(punctualDiffuse, vec3(0), specularity);
dDiffuseLight += punctualDiffuse;
}
// specular and clear coat are material settings and get included by a define based on the material
vec3 halfDir = normalize(-lightDirNormW + viewDir);
// specular
dSpecularLight +=
getLightSpecular(halfDir, reflectionDir, worldNormal, viewDir, lightDirNormW, gloss, tbn) * falloffAttenuation * light.color * cookieAttenuation *
getFresnel(
dot(viewDir, halfDir),
gloss,
specularity
, iridescenceFresnel,
iridescence_intensity
);
dSpecularLight += getLightSpecular(halfDir, reflectionDir, worldNormal, viewDir, lightDirNormW, gloss, tbn) * falloffAttenuation * light.color * cookieAttenuation * specularity;
ccSpecularLight += getLightSpecular(halfDir, clearcoatReflectionDir, clearcoat_worldNormal, viewDir, lightDirNormW, clearcoat_gloss, tbn) * falloffAttenuation * light.color * cookieAttenuation * getFresnelCC(dot(viewDir, halfDir));
ccSpecularLight += getLightSpecular(halfDir, clearcoatReflectionDir, clearcoat_worldNormal, viewDir, lightDirNormW, clearcoat_gloss, tbn) * falloffAttenuation * light.color * cookieAttenuation;
sSpecularLight += getLightSpecularSheen(halfDir, worldNormal, viewDir, lightDirNormW, sheen_gloss) * falloffAttenuation * light.color * cookieAttenuation;
}
}
// Write to global attenuation values (for lightmapper)
dAtten = falloffAttenuation;
dLightDirNormW = lightDirNormW;
}
void evaluateClusterLight(
int lightIndex,
vec3 worldNormal,
vec3 viewDir,
vec3 reflectionDir,
vec3 clearcoatReflectionDir,
float gloss,
vec3 specularity,
vec3 geometricNormal,
mat3 tbn,
vec3 iridescenceFresnel,
vec3 clearcoat_worldNormal,
float clearcoat_gloss,
float sheen_gloss,
float iridescence_intensity
) {
// decode core light data from textures
ClusterLightData clusterLightData = decodeClusterLightCore(lightIndex);
// evaluate light if it uses accepted light mask
bool acceptLightMask = clusterLightData.isDynamic;
bool acceptLightMask = clusterLightData.isLightmapped;
if (acceptLightMask)
evaluateLight(
clusterLightData,
worldNormal,
viewDir,
reflectionDir,
clearcoatReflectionDir,
gloss,
specularity,
geometricNormal,
tbn,
iridescenceFresnel,
clearcoat_worldNormal,
clearcoat_gloss,
sheen_gloss,
iridescence_intensity
);
}
void addClusteredLights(
vec3 worldNormal,
vec3 viewDir,
vec3 reflectionDir,
vec3 clearcoatReflectionDir,
float gloss,
vec3 specularity,
vec3 geometricNormal,
mat3 tbn,
vec3 iridescenceFresnel,
vec3 clearcoat_worldNormal,
float clearcoat_gloss,
float sheen_gloss,
float iridescence_intensity
) {
// skip if no lights (index 0 is reserved for 'no light')
if (numClusteredLights <= 1)
return;
// world space position to 3d integer cell cordinates in the cluster structure
ivec3 cellCoords = ivec3(floor((vPositionW - clusterBoundsMin) * clusterCellsCountByBoundsSize));
// no lighting when cell coordinate is out of range
if (!(any(lessThan(cellCoords, ivec3(0))) || any(greaterThanEqual(cellCoords, clusterCellsMax)))) {
// cell index (mapping from 3d cell coordinates to linear memory)
int cellIndex = cellCoords.x * clusterCellsDot.x + cellCoords.y * clusterCellsDot.y + cellCoords.z * clusterCellsDot.z;
// convert cell index to uv coordinates
int clusterV = cellIndex / clusterTextureWidth;
int clusterU = cellIndex - clusterV * clusterTextureWidth;
// loop over maximum number of light cells
for (int lightCellIndex = 0; lightCellIndex < clusterMaxCells; lightCellIndex++) {
// using a single channel texture with data in red channel
uint lightIndex = texelFetch(clusterWorldTexture, ivec2(clusterU + lightCellIndex, clusterV), 0).x;
if (lightIndex == 0u)
break;
evaluateClusterLight(
int(lightIndex),
worldNormal,
viewDir,
reflectionDir,
clearcoatReflectionDir,
gloss,
specularity,
geometricNormal,
tbn,
iridescenceFresnel,
clearcoat_worldNormal,
clearcoat_gloss,
sheen_gloss,
iridescence_intensity
);
}
}
}
`
);
export {
clusteredLight_default as default
};