With this it is now possible to enable atlasing of shadow maps, which solves the existing limitation of 4 lights in a scene. This is done by grouping the rendering of shadow maps, that currently are drawn into their own images for each light, into one or several big textures. This was done because the openGL and webGL version Armory targets do not support dynamic indexing of shadowMapSamplers, meaning that the index that access an array of shadow maps has to be know by the compiler before hand so it can be unrolled into if/else branching. By instead simply using a big shadow map texture and moving the dynamic part to other types of array that are allowed dynamic indexing like vec4 and mat4, this limitation was solved. The premise was simple enough for the shader part, but for the Haxe part, managing and solving where lights shadow maps should go in a shadow map can be tricky. So to keep track and solve this, ShadowMapAtlas and ShadowMapTile were created. These classes have the minimally required logic to solve the basic features needed for this problem: defining some kind of abstraction to prevent overlapping of shadowmaps, finding available space, assigning such space efficiently, locking and freeing this space, etc. This functionality it is used by drawShadowMapAtlas(), which is a modified version of drawShadowMap(). Shadow map atlases are represented with perfectly balanced 4-ary trees, where each tree of the previous definition represents a "tile" or slice that results from dividing a square that represents the image into 4 slices or sub-images. The root of this "tile" it's a reference to the tile-slice, and this tile is divided in 4 slices, and the process is repeated depth-times. If depth is 1, slices are kept at just the initial 4 tiles of max size, which is the default size of the shadow map. #arm_shadowmap_atlas_lod allows controlling if code to support more depth levels is added or not when compiling. the tiles that populate atlases tile trees are simply a data structure that contains a reference to the light they are linked to, inner subtiles in case LOD is enabled, coordinates to where this tile starts in the atlas that go from 0 to Shadow Map Size, and a reference to a linked tile for LOD. This simple definition allows tiles having a theoretically small memory footprint, but in turn this simplicity might make some functionality that might be responsibility of tiles (for example knowing if they are overlapping) a responsibility of the ones that utilizes tiles instead. This decision may complicate maintenance so it is to be revised in future iterations of this feature.
325 lines
10 KiB
GLSL
Executable file
325 lines
10 KiB
GLSL
Executable file
#ifndef _SHADOWS_GLSL_
|
|
#define _SHADOWS_GLSL_
|
|
|
|
#include "compiled.inc"
|
|
|
|
#ifdef _CSM
|
|
uniform vec4 casData[shadowmapCascades * 4 + 4];
|
|
#endif
|
|
|
|
#ifdef _SMSizeUniform
|
|
uniform vec2 smSizeUniform;
|
|
#endif
|
|
|
|
#ifdef _ShadowMap
|
|
#ifdef _Clusters
|
|
#ifdef _ShadowMapAtlas
|
|
uniform vec4 pointLightDataArray[maxLightsCluster * 6];
|
|
#endif
|
|
#endif
|
|
#endif
|
|
|
|
#ifdef _ShadowMapAtlas
|
|
// https://www.khronos.org/registry/OpenGL/specs/gl/glspec20.pdf // p:168
|
|
// https://www.gamedev.net/forums/topic/687535-implementing-a-cube-map-lookup-function/5337472/
|
|
vec2 sampleCube(vec3 dir, out int faceIndex) {
|
|
vec3 dirAbs = abs(dir);
|
|
float ma;
|
|
vec2 uv;
|
|
if(dirAbs.z >= dirAbs.x && dirAbs.z >= dirAbs.y) {
|
|
faceIndex = dir.z < 0.0 ? 5 : 4;
|
|
ma = 0.5 / dirAbs.z;
|
|
uv = vec2(dir.z < 0.0 ? -dir.x : dir.x, -dir.y);
|
|
}
|
|
else if(dirAbs.y >= dirAbs.x) {
|
|
faceIndex = dir.y < 0.0 ? 3 : 2;
|
|
ma = 0.5 / dirAbs.y;
|
|
uv = vec2(dir.x, dir.y < 0.0 ? -dir.z : dir.z);
|
|
}
|
|
else {
|
|
faceIndex = dir.x < 0.0 ? 1 : 0;
|
|
ma = 0.5 / dirAbs.x;
|
|
uv = vec2(dir.x < 0.0 ? dir.z : -dir.z, -dir.y);
|
|
}
|
|
// downscale uv a little to hide seams
|
|
return uv * 0.9976 * ma + 0.5;
|
|
}
|
|
#endif
|
|
|
|
float PCF(sampler2DShadow shadowMap, const vec2 uv, const float compare, const vec2 smSize) {
|
|
float result = texture(shadowMap, vec3(uv + (vec2(-1.0, -1.0) / smSize), compare));
|
|
result += texture(shadowMap, vec3(uv + (vec2(-1.0, 0.0) / smSize), compare));
|
|
result += texture(shadowMap, vec3(uv + (vec2(-1.0, 1.0) / smSize), compare));
|
|
result += texture(shadowMap, vec3(uv + (vec2(0.0, -1.0) / smSize), compare));
|
|
result += texture(shadowMap, vec3(uv, compare));
|
|
result += texture(shadowMap, vec3(uv + (vec2(0.0, 1.0) / smSize), compare));
|
|
result += texture(shadowMap, vec3(uv + (vec2(1.0, -1.0) / smSize), compare));
|
|
result += texture(shadowMap, vec3(uv + (vec2(1.0, 0.0) / smSize), compare));
|
|
result += texture(shadowMap, vec3(uv + (vec2(1.0, 1.0) / smSize), compare));
|
|
return result / 9.0;
|
|
}
|
|
|
|
float lpToDepth(vec3 lp, const vec2 lightProj) {
|
|
lp = abs(lp);
|
|
float zcomp = max(lp.x, max(lp.y, lp.z));
|
|
zcomp = lightProj.x - lightProj.y / zcomp;
|
|
return zcomp * 0.5 + 0.5;
|
|
}
|
|
|
|
float PCFCube(samplerCubeShadow shadowMapCube, const vec3 lp, vec3 ml, const float bias, const vec2 lightProj, const vec3 n) {
|
|
const float s = shadowmapCubePcfSize; // TODO: incorrect...
|
|
float compare = lpToDepth(lp, lightProj) - bias * 1.5;
|
|
ml = ml + n * bias * 20;
|
|
#ifdef _InvY
|
|
ml.y = -ml.y;
|
|
#endif
|
|
float result = texture(shadowMapCube, vec4(ml, compare));
|
|
result += texture(shadowMapCube, vec4(ml + vec3(s, s, s), compare));
|
|
result += texture(shadowMapCube, vec4(ml + vec3(-s, s, s), compare));
|
|
result += texture(shadowMapCube, vec4(ml + vec3(s, -s, s), compare));
|
|
result += texture(shadowMapCube, vec4(ml + vec3(s, s, -s), compare));
|
|
result += texture(shadowMapCube, vec4(ml + vec3(-s, -s, s), compare));
|
|
result += texture(shadowMapCube, vec4(ml + vec3(s, -s, -s), compare));
|
|
result += texture(shadowMapCube, vec4(ml + vec3(-s, s, -s), compare));
|
|
result += texture(shadowMapCube, vec4(ml + vec3(-s, -s, -s), compare));
|
|
return result / 9.0;
|
|
}
|
|
|
|
#ifdef _ShadowMapAtlas
|
|
// transform "out-of-bounds" coordinates to the correct face/coordinate system
|
|
vec2 transformOffsetedUV(const int faceIndex, out int newFaceIndex, vec2 uv) {
|
|
if (uv.x < 0.0) {
|
|
if (faceIndex == 0) { // X+
|
|
newFaceIndex = 4; // Z+
|
|
}
|
|
else if (faceIndex == 1) { // X-
|
|
newFaceIndex = 5; // Z-
|
|
}
|
|
else if (faceIndex == 2) { // Y+
|
|
newFaceIndex = 1; // X-
|
|
}
|
|
else if (faceIndex == 3) { // Y-
|
|
newFaceIndex = 1; // X-
|
|
}
|
|
else if (faceIndex == 4) { // Z+
|
|
newFaceIndex = 1; // X-
|
|
}
|
|
else { // Z-
|
|
newFaceIndex = 0; // X+
|
|
}
|
|
uv = vec2(1.0 + uv.x, uv.y);
|
|
}
|
|
else if (uv.x > 1.0) {
|
|
if (faceIndex == 0) { // X+
|
|
newFaceIndex = 5; // Z-
|
|
}
|
|
else if (faceIndex == 1) { // X-
|
|
newFaceIndex = 4; // Z+
|
|
}
|
|
else if (faceIndex == 2) { // Y+
|
|
newFaceIndex = 0; // X+
|
|
}
|
|
else if (faceIndex == 3) { // Y-
|
|
newFaceIndex = 0; // X+
|
|
}
|
|
else if (faceIndex == 4) { // Z+
|
|
newFaceIndex = 0; // X+
|
|
}
|
|
else { // Z-
|
|
newFaceIndex = 1; // X-
|
|
}
|
|
uv = vec2(1.0 - uv.x, uv.y);
|
|
}
|
|
else if (uv.y < 0.0) {
|
|
if (faceIndex == 0) { // X+
|
|
newFaceIndex = 2; // Y+
|
|
}
|
|
else if (faceIndex == 1) { // X-
|
|
newFaceIndex = 2; // Y+
|
|
}
|
|
else if (faceIndex == 2) { // Y+
|
|
newFaceIndex = 5; // Z-
|
|
}
|
|
else if (faceIndex == 3) { // Y-
|
|
newFaceIndex = 4; // Z+
|
|
}
|
|
else if (faceIndex == 4) { // Z+
|
|
newFaceIndex = 2; // Y+
|
|
}
|
|
else { // Z-
|
|
newFaceIndex = 2; // Y+
|
|
}
|
|
uv = vec2(uv.x, 1.0 + uv.y);
|
|
}
|
|
else if (uv.y > 1.0) {
|
|
if (faceIndex == 0) { // X+
|
|
newFaceIndex = 3; // Y-
|
|
}
|
|
else if (faceIndex == 1) { // X-
|
|
newFaceIndex = 3; // Y-
|
|
}
|
|
else if (faceIndex == 2) { // Y+
|
|
newFaceIndex = 4; // Z+
|
|
}
|
|
else if (faceIndex == 3) { // Y-
|
|
newFaceIndex = 5; // Z-
|
|
}
|
|
else if (faceIndex == 4) { // Z+
|
|
newFaceIndex = 3; // Y-
|
|
}
|
|
else { // Z-
|
|
newFaceIndex = 3; // Y-
|
|
}
|
|
uv = vec2(uv.x, 1.0 - uv.y);
|
|
} else {
|
|
newFaceIndex = faceIndex;
|
|
}
|
|
// cover corner cases too
|
|
return uv;
|
|
}
|
|
|
|
float PCFFakeCube(sampler2DShadow shadowMap, const vec3 lp, vec3 ml, const float bias, const vec2 lightProj, const vec3 n, const int index) {
|
|
const vec2 smSize = smSizeUniform; // TODO: incorrect...
|
|
const float compare = lpToDepth(lp, lightProj) - bias * 1.5;
|
|
ml = ml + n * bias * 20;
|
|
|
|
int faceIndex = 0;
|
|
const int lightIndex = index * 6;
|
|
const vec2 uv = sampleCube(ml, faceIndex);
|
|
|
|
vec4 pointLightTile = pointLightDataArray[lightIndex + faceIndex]; // x: tile X offset, y: tile Y offset, z: tile size relative to atlas
|
|
float result = texture(shadowMap, vec3(pointLightTile.z * uv + pointLightTile.xy, compare));
|
|
// soft shadowing
|
|
int newFaceIndex = 0;
|
|
vec2 uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(-1.0, 0.0) / smSize)));
|
|
pointLightTile = pointLightDataArray[lightIndex + newFaceIndex];
|
|
result += texture(shadowMap, vec3(pointLightTile.z * uvtiled + pointLightTile.xy, compare));
|
|
|
|
uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(-1.0, 1.0) / smSize)));
|
|
pointLightTile = pointLightDataArray[lightIndex + newFaceIndex];
|
|
result += texture(shadowMap, vec3(pointLightTile.z * uvtiled + pointLightTile.xy, compare));
|
|
|
|
uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(0.0, -1.0) / smSize)));
|
|
pointLightTile = pointLightDataArray[lightIndex + newFaceIndex];
|
|
result += texture(shadowMap, vec3(pointLightTile.z * uvtiled + pointLightTile.xy, compare));
|
|
|
|
uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(-1.0, -1.0) / smSize)));
|
|
pointLightTile = pointLightDataArray[lightIndex + newFaceIndex];
|
|
result += texture(shadowMap, vec3(pointLightTile.z * uvtiled + pointLightTile.xy, compare));
|
|
|
|
uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(0.0, 1.0) / smSize)));
|
|
pointLightTile = pointLightDataArray[lightIndex + newFaceIndex];
|
|
result += texture(shadowMap, vec3(pointLightTile.z * uvtiled + pointLightTile.xy, compare));
|
|
|
|
uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(1.0, -1.0) / smSize)));
|
|
pointLightTile = pointLightDataArray[lightIndex + newFaceIndex];
|
|
result += texture(shadowMap, vec3(pointLightTile.z * uvtiled + pointLightTile.xy, compare));
|
|
|
|
uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(1.0, 0.0) / smSize)));
|
|
pointLightTile = pointLightDataArray[lightIndex + newFaceIndex];
|
|
result += texture(shadowMap, vec3(pointLightTile.z * uvtiled + pointLightTile.xy, compare));
|
|
|
|
uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(1.0, 1.0) / smSize)));
|
|
pointLightTile = pointLightDataArray[lightIndex + newFaceIndex];
|
|
result += texture(shadowMap, vec3(pointLightTile.z * uvtiled + pointLightTile.xy, compare));
|
|
|
|
return result / 9.0;
|
|
}
|
|
#endif
|
|
|
|
float shadowTest(sampler2DShadow shadowMap, const vec3 lPos, const float shadowsBias) {
|
|
#ifdef _SMSizeUniform
|
|
vec2 smSize = smSizeUniform;
|
|
#else
|
|
const vec2 smSize = shadowmapSize;
|
|
#endif
|
|
if (lPos.x < 0.0 || lPos.y < 0.0 || lPos.x > 1.0 || lPos.y > 1.0) return 1.0;
|
|
return PCF(shadowMap, lPos.xy, lPos.z - shadowsBias, smSize);
|
|
}
|
|
|
|
#ifdef _CSM
|
|
mat4 getCascadeMat(const float d, out int casi, out int casIndex) {
|
|
const int c = shadowmapCascades;
|
|
|
|
// Get cascade index
|
|
// TODO: use bounding box slice selection instead of sphere
|
|
const vec4 ci = vec4(float(c > 0), float(c > 1), float(c > 2), float(c > 3));
|
|
// int ci;
|
|
// if (d < casData[c * 4].x) ci = 0;
|
|
// else if (d < casData[c * 4].y) ci = 1 * 4;
|
|
// else if (d < casData[c * 4].z) ci = 2 * 4;
|
|
// else ci = 3 * 4;
|
|
// Splits
|
|
vec4 comp = vec4(
|
|
float(d > casData[c * 4].x),
|
|
float(d > casData[c * 4].y),
|
|
float(d > casData[c * 4].z),
|
|
float(d > casData[c * 4].w));
|
|
casi = int(min(dot(ci, comp), c));
|
|
|
|
// Get cascade mat
|
|
casIndex = casi * 4;
|
|
|
|
return mat4(
|
|
casData[casIndex ],
|
|
casData[casIndex + 1],
|
|
casData[casIndex + 2],
|
|
casData[casIndex + 3]);
|
|
|
|
// if (casIndex == 0) return mat4(casData[0], casData[1], casData[2], casData[3]);
|
|
// ..
|
|
}
|
|
|
|
float shadowTestCascade(sampler2DShadow shadowMap, const vec3 eye, const vec3 p, const float shadowsBias) {
|
|
#ifdef _SMSizeUniform
|
|
vec2 smSize = smSizeUniform;
|
|
#else
|
|
const vec2 smSize = shadowmapSize * vec2(shadowmapCascades, 1.0);
|
|
#endif
|
|
const int c = shadowmapCascades;
|
|
float d = distance(eye, p);
|
|
|
|
int casi;
|
|
int casIndex;
|
|
mat4 LWVP = getCascadeMat(d, casi, casIndex);
|
|
|
|
vec4 lPos = LWVP * vec4(p, 1.0);
|
|
lPos.xyz /= lPos.w;
|
|
|
|
float visibility = 1.0;
|
|
if (lPos.w > 0.0) visibility = PCF(shadowMap, lPos.xy, lPos.z - shadowsBias, smSize);
|
|
|
|
// Blend cascade
|
|
// https://github.com/TheRealMJP/Shadows
|
|
const float blendThres = 0.15;
|
|
float nextSplit = casData[c * 4][casi];
|
|
float splitSize = casi == 0 ? nextSplit : nextSplit - casData[c * 4][casi - 1];
|
|
float splitDist = (nextSplit - d) / splitSize;
|
|
if (splitDist <= blendThres && casi != c - 1) {
|
|
int casIndex2 = casIndex + 4;
|
|
mat4 LWVP2 = mat4(
|
|
casData[casIndex2 ],
|
|
casData[casIndex2 + 1],
|
|
casData[casIndex2 + 2],
|
|
casData[casIndex2 + 3]);
|
|
|
|
vec4 lPos2 = LWVP2 * vec4(p, 1.0);
|
|
lPos2.xyz /= lPos2.w;
|
|
float visibility2 = 1.0;
|
|
if (lPos2.w > 0.0) visibility2 = PCF(shadowMap, lPos2.xy, lPos2.z - shadowsBias, smSize);
|
|
|
|
float lerpAmt = smoothstep(0.0, blendThres, splitDist);
|
|
return mix(visibility2, visibility, lerpAmt);
|
|
}
|
|
return visibility;
|
|
|
|
// Visualize cascades
|
|
// if (ci == 0) albedo.rgb = vec3(1.0, 0.0, 0.0);
|
|
// if (ci == 4) albedo.rgb = vec3(0.0, 1.0, 0.0);
|
|
// if (ci == 8) albedo.rgb = vec3(0.0, 0.0, 1.0);
|
|
// if (ci == 12) albedo.rgb = vec3(1.0, 1.0, 0.0);
|
|
}
|
|
#endif
|
|
|
|
#endif
|