playcanvas
Version:
PlayCanvas WebGL game engine
621 lines (485 loc) • 23.5 kB
JavaScript
var clusteredLightPS = /* wgsl */ `
// include this before shadow / cookie code
var clusterWorldTexture: texture_2d<f32>;
var lightsTexture: texture_2d<f32>;
// TODO: when VSM shadow is supported, it needs to use sampler2D in webgl2
var shadowAtlasTexture: texture_depth_2d;
var shadowAtlasTextureSampler: sampler_comparison;
var cookieAtlasTexture: texture_2d<f32>;
var cookieAtlasTextureSampler: sampler;
uniform clusterMaxCells: i32;
// 1.0 if clustered lighting can be skipped (0 lights in the clusters)
uniform clusterSkip: f32;
uniform clusterCellsCountByBoundsSize: vec3f;
uniform clusterTextureSize: vec3f;
uniform clusterBoundsMin: vec3f;
uniform clusterBoundsDelta: vec3f;
uniform clusterCellsDot: vec3f;
uniform clusterCellsMax: vec3f;
uniform shadowAtlasParams: vec2f;
// structure storing light properties of a clustered light
// it's sorted to have all vectors aligned to 4 floats to limit padding
struct ClusterLightData {
// 32bit of flags
flags: u32,
// area light sizes / orientation
halfWidth: vec3f,
isSpot: bool,
// area light sizes / orientation
halfHeight: vec3f,
// light index
lightIndex: i32,
// world space position
position: vec3f,
// area light shape
shape: u32,
// world space direction (spot light only)
direction: vec3f,
// light follow mode
falloffModeLinear: bool,
// color
color: vec3f,
// 0.0 if the light doesn't cast shadows
shadowIntensity: f32,
// atlas viewport for omni light shadow and cookie (.xy is offset to the viewport slot, .z is size of the face in the atlas)
omniAtlasViewport: vec3f,
// range of the light
range: f32,
// channel mask - one of the channels has 1, the others are 0
cookieChannelMask: vec4f,
// compressed biases, two haf-floats stored in a float
biasesData: f32,
// shadow bias values
shadowBias: f32,
shadowNormalBias: f32,
// compressed angles, two haf-floats stored in a float
anglesData: f32,
// spot light inner and outer angle cosine
innerConeAngleCos: f32,
outerConeAngleCos: f32,
// intensity of the cookie
cookieIntensity: f32,
// light mask
//float mask;
isDynamic: bool,
isLightmapped: bool
}
// 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.
// shadow (spot light only) / cookie projection matrix
var<private> lightProjectionMatrix: mat4x4f;
fn sampleLightTextureF(lightIndex: i32, index: i32) -> vec4f {
return textureLoad(lightsTexture, vec2<i32>(index, lightIndex), 0);
}
fn decodeClusterLightCore(clusterLightData: ptr<function, ClusterLightData>, lightIndex: f32) {
// light index
clusterLightData.lightIndex = i32(lightIndex);
// sample data encoding half-float values into 32bit uints
let halfData: vec4f = sampleLightTextureF(clusterLightData.lightIndex, {CLUSTER_TEXTURE_COLOR_ANGLES_BIAS});
// store floats we decode later as needed
clusterLightData.anglesData = halfData.z;
clusterLightData.biasesData = halfData.w;
// decompress color half-floats
let colorRG: vec2f = unpack2x16float(bitcast<u32>(halfData.x));
let colorB_: vec2f = unpack2x16float(bitcast<u32>(halfData.y));
clusterLightData.color = vec3f(colorRG, colorB_.x) * {LIGHT_COLOR_DIVIDER};
// position and range, full floats
let lightPosRange: vec4f = sampleLightTextureF(clusterLightData.lightIndex, {CLUSTER_TEXTURE_POSITION_RANGE});
clusterLightData.position = lightPosRange.xyz;
clusterLightData.range = lightPosRange.w;
// spot direction & flags data
let lightDir_Flags: vec4f = sampleLightTextureF(clusterLightData.lightIndex, {CLUSTER_TEXTURE_DIRECTION_FLAGS});
// spot light direction
clusterLightData.direction = lightDir_Flags.xyz;
// 32bit flags
let flags_uint: u32 = bitcast<u32>(lightDir_Flags.w);
clusterLightData.flags = flags_uint;
clusterLightData.isSpot = (flags_uint & (1u << 30u)) != 0u;
clusterLightData.shape = (flags_uint >> 28u) & 0x3u;
clusterLightData.falloffModeLinear = (flags_uint & (1u << 27u)) == 0u;
clusterLightData.shadowIntensity = f32((flags_uint >> 0u) & 0xFFu) / 255.0;
clusterLightData.cookieIntensity = f32((flags_uint >> 8u) & 0xFFu) / 255.0;
clusterLightData.isDynamic = (flags_uint & (1u << 22u)) != 0u;
clusterLightData.isLightmapped = (flags_uint & (1u << 21u)) != 0u;
}
fn decodeClusterLightSpot(clusterLightData: ptr<function, ClusterLightData>) {
// spot light cos angles
let angles: vec2f = unpack2x16float(bitcast<u32>(clusterLightData.anglesData));
clusterLightData.innerConeAngleCos = angles.x;
clusterLightData.outerConeAngleCos = angles.y;
}
fn decodeClusterLightOmniAtlasViewport(clusterLightData: ptr<function, ClusterLightData>) {
clusterLightData.omniAtlasViewport = sampleLightTextureF(clusterLightData.lightIndex, {CLUSTER_TEXTURE_PROJ_MAT_0}).xyz;
}
fn decodeClusterLightAreaData(clusterLightData: ptr<function, ClusterLightData>) {
clusterLightData.halfWidth = sampleLightTextureF(clusterLightData.lightIndex, {CLUSTER_TEXTURE_AREA_DATA_WIDTH}).xyz;
clusterLightData.halfHeight = sampleLightTextureF(clusterLightData.lightIndex, {CLUSTER_TEXTURE_AREA_DATA_HEIGHT}).xyz;
}
fn decodeClusterLightProjectionMatrixData(clusterLightData: ptr<function, ClusterLightData>) {
// shadow matrix
let m0: vec4f = sampleLightTextureF(clusterLightData.lightIndex, {CLUSTER_TEXTURE_PROJ_MAT_0});
let m1: vec4f = sampleLightTextureF(clusterLightData.lightIndex, {CLUSTER_TEXTURE_PROJ_MAT_1});
let m2: vec4f = sampleLightTextureF(clusterLightData.lightIndex, {CLUSTER_TEXTURE_PROJ_MAT_2});
let m3: vec4f = sampleLightTextureF(clusterLightData.lightIndex, {CLUSTER_TEXTURE_PROJ_MAT_3});
lightProjectionMatrix = mat4x4f(m0, m1, m2, m3);
}
fn decodeClusterLightShadowData(clusterLightData: ptr<function, ClusterLightData>) {
// shadow biases
let biases: vec2f = unpack2x16float(bitcast<u32>(clusterLightData.biasesData));
clusterLightData.shadowBias = biases.x;
clusterLightData.shadowNormalBias = biases.y;
}
fn decodeClusterLightCookieData(clusterLightData: ptr<function, ClusterLightData>) {
// extract channel mask from flags
let cookieFlags: u32 = (clusterLightData.flags >> 23u) & 0x0Fu; // 4bits, each bit enables a channel
let mask_uvec: vec4<u32> = vec4<u32>(cookieFlags) & vec4<u32>(1u, 2u, 4u, 8u);
clusterLightData.cookieChannelMask = step(vec4f(1.0), vec4f(mask_uvec)); // Normalize to 0.0 or 1.0
}
fn evaluateLight(
light: ptr<function, ClusterLightData>,
worldNormal: vec3f,
viewDir: vec3f,
reflectionDir: vec3f,
clearcoatReflectionDir: vec3f,
gloss: f32,
specularity: vec3f,
geometricNormal: vec3f,
tbn: mat3x3f,
iridescenceFresnel: vec3f,
clearcoat_worldNormal: vec3f,
clearcoat_gloss: f32,
sheen_gloss: f32,
iridescence_intensity: f32
) {
var cookieAttenuation: vec3f = vec3f(1.0);
var diffuseAttenuation: f32 = 1.0;
var falloffAttenuation: f32 = 1.0;
// evaluate omni part of the light
let lightDirW: vec3f = evalOmniLight(light.position);
let lightDirNormW: vec3f = normalize(lightDirW);
// distance attenuation
if (light.shape != {LIGHTSHAPE_PUNCTUAL}) { // area light
// area lights
decodeClusterLightAreaData(light);
// handle light shape
if (light.shape == {LIGHTSHAPE_RECT}) {
calcRectLightValues(light.position, light.halfWidth, light.halfHeight);
} else if (light.shape == {LIGHTSHAPE_DISK}) {
calcDiskLightValues(light.position, light.halfWidth, light.halfHeight);
} else { // sphere
calcSphereLightValues(light.position, light.halfWidth, light.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 = falloffAttenuation * getLightDiffuse(worldNormal, viewDir, lightDirNormW);
}
// spot light falloff
if (light.isSpot) {
decodeClusterLightSpot(light);
falloffAttenuation = falloffAttenuation * getSpotEffect(light.direction, light.innerConeAngleCos, light.outerConeAngleCos, lightDirNormW);
}
if (falloffAttenuation > 0.00001) {
// shadow / cookie
if (light.shadowIntensity > 0.0 || light.cookieIntensity > 0.0) {
// shared shadow / cookie data depends on light type
if (light.isSpot) {
decodeClusterLightProjectionMatrixData(light);
} else {
decodeClusterLightOmniAtlasViewport(light);
}
let shadowTextureResolution: f32 = uniform.shadowAtlasParams.x;
let shadowEdgePixels: f32 = uniform.shadowAtlasParams.y;
// cookie
if (light.cookieIntensity > 0.0) {
decodeClusterLightCookieData(light);
if (light.isSpot) {
// !!!!!!!!!!! TEXTURE_PASS likely needs sampler. Assuming cookieAtlasTextureSampler exists.
cookieAttenuation = getCookie2DClustered(cookieAtlasTexture, cookieAtlasTextureSampler, lightProjectionMatrix, vPositionW, light.cookieIntensity, light.cookieChannelMask);
} else {
// !!!!!!!!!!! TEXTURE_PASS likely needs sampler. Assuming cookieAtlasTextureSampler exists.
cookieAttenuation = getCookieCubeClustered(cookieAtlasTexture, cookieAtlasTextureSampler, lightDirW, light.cookieIntensity, light.cookieChannelMask, shadowTextureResolution, shadowEdgePixels, light.omniAtlasViewport);
}
}
// shadow
if (light.shadowIntensity > 0.0) {
decodeClusterLightShadowData(light);
let shadowParams: vec4f = vec4f(shadowTextureResolution, light.shadowNormalBias, light.shadowBias, 1.0 / light.range);
if (light.isSpot) {
// spot shadow
let shadowCoord: vec3f = getShadowCoordPerspZbufferNormalOffset(lightProjectionMatrix, shadowParams, geometricNormal);
// !!!!!!!!!!! SHADOWMAP_PASS needs texture and sampler_comparison.
// !!!!!!!!!!! Shadow functions need update for WGSL textureSampleCompare etc. Assuming these are handled in includes.
let shadow: f32 = getShadowSpotClusteredPCF1(shadowAtlasTexture, shadowAtlasTextureSampler, shadowCoord, shadowParams);
let shadow: f32 = getShadowSpotClusteredPCF3(shadowAtlasTexture, shadowAtlasTextureSampler, shadowCoord, shadowParams);
let shadow: f32 = getShadowSpotClusteredPCF5(shadowAtlasTexture, shadowAtlasTextureSampler, shadowCoord, shadowParams);
let shadow: f32 = getShadowSpotClusteredPCSS(shadowAtlasTexture, shadowAtlasTextureSampler, shadowCoord, shadowParams);
falloffAttenuation = falloffAttenuation * mix(1.0, shadow, light.shadowIntensity);
} else {
// omni shadow
let dir: vec3f = normalOffsetPointShadow(shadowParams, light.position, lightDirW, lightDirNormW, geometricNormal); // normalBias adjusted for distance
// !!!!!!!!!!! SHADOWMAP_PASS needs texture and sampler_comparison.
// !!!!!!!!!!! Shadow functions need update for WGSL textureSampleCompare etc. Assuming these are handled in includes.
let shadow: f32 = getShadowOmniClusteredPCF1(shadowAtlasTexture, shadowAtlasTextureSampler, shadowParams, light.omniAtlasViewport, shadowEdgePixels, dir);
let shadow: f32 = getShadowOmniClusteredPCF3(shadowAtlasTexture, shadowAtlasTextureSampler, shadowParams, light.omniAtlasViewport, shadowEdgePixels, dir);
let shadow: f32 = getShadowOmniClusteredPCF5(shadowAtlasTexture, shadowAtlasTextureSampler, shadowParams, light.omniAtlasViewport, shadowEdgePixels, dir);
falloffAttenuation = falloffAttenuation * mix(1.0, shadow, light.shadowIntensity);
}
}
}
}
// diffuse / specular / clearcoat
if (light.shape != {LIGHTSHAPE_PUNCTUAL}) { // area light
// area light diffuse
{
var areaDiffuse: vec3f = (diffuseAttenuation * falloffAttenuation) * light.color * cookieAttenuation;
areaDiffuse = mix(areaDiffuse, vec3f(0.0), dLTCSpecFres);
// area light diffuse - it does not mix diffuse lighting into specular attenuation
dDiffuseLight = dDiffuseLight + areaDiffuse;
}
// specular and clear coat are material settings and get included by a define based on the material
// area light specular
var areaLightSpecular: f32; // Use var because assigned in if/else
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 = dSpecularLight + dLTCSpecFres * areaLightSpecular * falloffAttenuation * light.color * cookieAttenuation;
// area light specular clear coat
var areaLightSpecularCC: f32; // Use var because assigned in if/else
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 = ccSpecularLight + ccLTCSpecFres * areaLightSpecularCC * falloffAttenuation * light.color * cookieAttenuation;
} else
{ // punctual light
// punctual light diffuse
{
var punctualDiffuse: vec3f = falloffAttenuation * light.color * cookieAttenuation;
punctualDiffuse = mix(punctualDiffuse, vec3f(0.0), specularity);
dDiffuseLight = dDiffuseLight + punctualDiffuse;
}
// specular and clear coat are material settings and get included by a define based on the material
let halfDir: vec3f = normalize(-lightDirNormW + viewDir);
// specular
dSpecularLight = dSpecularLight +
getLightSpecular(halfDir, reflectionDir, worldNormal, viewDir, lightDirNormW, gloss, tbn) * falloffAttenuation * light.color * cookieAttenuation *
getFresnel(
dot(viewDir, halfDir),
gloss,
specularity
, iridescenceFresnel,
iridescence_intensity
);
dSpecularLight = dSpecularLight + getLightSpecular(halfDir, reflectionDir, worldNormal, viewDir, lightDirNormW, gloss, tbn) * falloffAttenuation * light.color * cookieAttenuation * specularity;
ccSpecularLight = ccSpecularLight + getLightSpecular(halfDir, clearcoatReflectionDir, clearcoat_worldNormal, viewDir, lightDirNormW, clearcoat_gloss, tbn) * falloffAttenuation * light.color * cookieAttenuation * getFresnelCC(dot(viewDir, halfDir));
ccSpecularLight = ccSpecularLight + getLightSpecular(halfDir, clearcoatReflectionDir, clearcoat_worldNormal, viewDir, lightDirNormW, clearcoat_gloss, tbn) * falloffAttenuation * light.color * cookieAttenuation;
sSpecularLight = sSpecularLight + getLightSpecularSheen(halfDir, worldNormal, viewDir, lightDirNormW, sheen_gloss) * falloffAttenuation * light.color * cookieAttenuation;
}
}
// Write to global attenuation values (for lightmapper)
dAtten = falloffAttenuation;
dLightDirNormW = lightDirNormW;
}
fn evaluateClusterLight(
lightIndex: f32,
worldNormal: vec3f,
viewDir: vec3f,
reflectionDir: vec3f,
clearcoatReflectionDir: vec3f,
gloss: f32,
specularity: vec3f,
geometricNormal: vec3f,
tbn: mat3x3f,
iridescenceFresnel: vec3f,
clearcoat_worldNormal: vec3f,
clearcoat_gloss: f32,
sheen_gloss: f32,
iridescence_intensity: f32
) {
// decode core light data from textures
var clusterLightData: ClusterLightData;
decodeClusterLightCore(&clusterLightData, lightIndex);
// evaluate light if it uses accepted light mask
let acceptLightMask: bool = clusterLightData.isDynamic;
let acceptLightMask: bool = clusterLightData.isLightmapped;
if (acceptLightMask) {
evaluateLight(
&clusterLightData,
worldNormal,
viewDir,
reflectionDir,
clearcoatReflectionDir,
gloss,
specularity,
geometricNormal,
tbn,
iridescenceFresnel,
clearcoat_worldNormal,
clearcoat_gloss,
sheen_gloss,
iridescence_intensity
);
}
}
fn addClusteredLights(
worldNormal: vec3f,
viewDir: vec3f,
reflectionDir: vec3f,
clearcoatReflectionDir: vec3f,
gloss: f32,
specularity: vec3f,
geometricNormal: vec3f,
tbn: mat3x3f,
iridescenceFresnel: vec3f,
clearcoat_worldNormal: vec3f,
clearcoat_gloss: f32,
sheen_gloss: f32,
iridescence_intensity: f32
) {
// skip lights if no lights at all
if (uniform.clusterSkip > 0.5) {
return;
}
// world space position to 3d integer cell cordinates in the cluster structure
let cellCoords: vec3f = floor((vPositionW - uniform.clusterBoundsMin) * uniform.clusterCellsCountByBoundsSize);
// no lighting when cell coordinate is out of range
if (!(any(cellCoords < vec3f(0.0)) || any(cellCoords >= uniform.clusterCellsMax))) {
// cell index (mapping from 3d cell coordinates to linear memory)
let cellIndex: f32 = dot(uniform.clusterCellsDot, cellCoords);
// convert cell index to uv coordinates
let clusterV: f32 = floor(cellIndex * uniform.clusterTextureSize.y);
let clusterU: f32 = cellIndex - (clusterV * uniform.clusterTextureSize.x);
// loop over maximum number of light cells
for (var lightCellIndex: i32 = 0; lightCellIndex < uniform.clusterMaxCells; lightCellIndex = lightCellIndex + 1) {
// using a single channel texture with data in red channel
let lightIndexPacked: f32 = textureLoad(clusterWorldTexture, vec2<i32>(i32(clusterU) + lightCellIndex, i32(clusterV)), 0).r;
if (lightIndexPacked <= 0.0) {
break;
}
evaluateClusterLight(
lightIndexPacked * 255.0,
worldNormal,
viewDir,
reflectionDir,
clearcoatReflectionDir,
gloss,
specularity,
geometricNormal,
tbn,
iridescenceFresnel,
clearcoat_worldNormal,
clearcoat_gloss,
sheen_gloss,
iridescence_intensity
);
}
}
}`;
export { clusteredLightPS as default };