|
@@ -215,44 +215,13 @@
|
|
}
|
|
}
|
|
|
|
|
|
#ifdef WEBGL2
|
|
#ifdef WEBGL2
|
|
- const vec3 PCFSamplers[32] = vec3[32](
|
|
|
|
- vec3(0.06407013, 0.05409927, 0.),
|
|
|
|
- vec3(0.7366577, 0.5789394, 0.),
|
|
|
|
- vec3(-0.6270542, -0.5320278, 0.),
|
|
|
|
- vec3(-0.4096107, 0.8411095, 0.),
|
|
|
|
- vec3(0.6849564, -0.4990818, 0.),
|
|
|
|
- vec3(-0.874181, -0.04579735, 0.),
|
|
|
|
- vec3(0.9989998, 0.0009880066, 0.),
|
|
|
|
- vec3(-0.004920578, -0.9151649, 0.),
|
|
|
|
- vec3(0.1805763, 0.9747483, 0.),
|
|
|
|
- vec3(-0.2138451, 0.2635818, 0.),
|
|
|
|
- vec3(0.109845, 0.3884785, 0.),
|
|
|
|
- vec3(0.06876755, -0.3581074, 0.),
|
|
|
|
- vec3(0.374073, -0.7661266, 0.),
|
|
|
|
- vec3(0.3079132, -0.1216763, 0.),
|
|
|
|
- vec3(-0.3794335, -0.8271583, 0.),
|
|
|
|
- vec3(-0.203878, -0.07715034, 0.),
|
|
|
|
- vec3(0.5912697, 0.1469799, 0.),
|
|
|
|
- vec3(-0.88069, 0.3031784, 0.),
|
|
|
|
- vec3(0.5040108, 0.8283722, 0.),
|
|
|
|
- vec3(-0.5844124, 0.5494877, 0.),
|
|
|
|
- vec3(0.6017799, -0.1726654, 0.),
|
|
|
|
- vec3(-0.5554981, 0.1559997, 0.),
|
|
|
|
- vec3(-0.3016369, -0.3900928, 0.),
|
|
|
|
- vec3(-0.5550632, -0.1723762, 0.),
|
|
|
|
- vec3(0.925029, 0.2995041, 0.),
|
|
|
|
- vec3(-0.2473137, 0.5538505, 0.),
|
|
|
|
- vec3(0.9183037, -0.2862392, 0.),
|
|
|
|
- vec3(0.2469421, 0.6718712, 0.),
|
|
|
|
- vec3(0.3916397, -0.4328209, 0.),
|
|
|
|
- vec3(-0.03576927, -0.6220032, 0.),
|
|
|
|
- vec3(-0.04661255, 0.7995201, 0.),
|
|
|
|
- vec3(0.4402924, 0.3640312, 0.)
|
|
|
|
- );
|
|
|
|
-
|
|
|
|
// Shadow PCF kernel size 1 with a single tap (lowest quality)
|
|
// Shadow PCF kernel size 1 with a single tap (lowest quality)
|
|
- float computeShadowWithPCF1(vec4 vPositionFromLight, sampler2DShadow shadowSampler, float darkness, float frustumEdgeFalloff)
|
|
|
|
|
|
+ float computeShadowWithPCF1(vec4 vPositionFromLight, float depthMetric, sampler2DShadow shadowSampler, float darkness, float frustumEdgeFalloff)
|
|
{
|
|
{
|
|
|
|
+ if (depthMetric > 1.0 || depthMetric < 0.0) {
|
|
|
|
+ return 1.0;
|
|
|
|
+ }
|
|
|
|
+
|
|
vec3 clipSpace = vPositionFromLight.xyz / vPositionFromLight.w;
|
|
vec3 clipSpace = vPositionFromLight.xyz / vPositionFromLight.w;
|
|
vec3 uvDepth = vec3(0.5 * clipSpace.xyz + vec3(0.5));
|
|
vec3 uvDepth = vec3(0.5 * clipSpace.xyz + vec3(0.5));
|
|
|
|
|
|
@@ -264,8 +233,12 @@
|
|
// Shadow PCF kernel 3*3 in only 4 taps (medium quality)
|
|
// Shadow PCF kernel 3*3 in only 4 taps (medium quality)
|
|
// This uses a well distributed taps to allow a gaussian distribution covering a 3*3 kernel
|
|
// This uses a well distributed taps to allow a gaussian distribution covering a 3*3 kernel
|
|
// https://mynameismjp.wordpress.com/2013/09/10/shadow-maps/
|
|
// https://mynameismjp.wordpress.com/2013/09/10/shadow-maps/
|
|
- float computeShadowWithPCF3(vec4 vPositionFromLight, sampler2DShadow shadowSampler, vec2 shadowMapSizeAndInverse, float darkness, float frustumEdgeFalloff)
|
|
|
|
|
|
+ float computeShadowWithPCF3(vec4 vPositionFromLight, float depthMetric, sampler2DShadow shadowSampler, vec2 shadowMapSizeAndInverse, float darkness, float frustumEdgeFalloff)
|
|
{
|
|
{
|
|
|
|
+ if (depthMetric > 1.0 || depthMetric < 0.0) {
|
|
|
|
+ return 1.0;
|
|
|
|
+ }
|
|
|
|
+
|
|
vec3 clipSpace = vPositionFromLight.xyz / vPositionFromLight.w;
|
|
vec3 clipSpace = vPositionFromLight.xyz / vPositionFromLight.w;
|
|
vec3 uvDepth = vec3(0.5 * clipSpace.xyz + vec3(0.5));
|
|
vec3 uvDepth = vec3(0.5 * clipSpace.xyz + vec3(0.5));
|
|
|
|
|
|
@@ -298,8 +271,12 @@
|
|
// Shadow PCF kernel 5*5 in only 9 taps (high quality)
|
|
// Shadow PCF kernel 5*5 in only 9 taps (high quality)
|
|
// This uses a well distributed taps to allow a gaussian distribution covering a 5*5 kernel
|
|
// This uses a well distributed taps to allow a gaussian distribution covering a 5*5 kernel
|
|
// https://mynameismjp.wordpress.com/2013/09/10/shadow-maps/
|
|
// https://mynameismjp.wordpress.com/2013/09/10/shadow-maps/
|
|
- float computeShadowWithPCF5(vec4 vPositionFromLight, sampler2DShadow shadowSampler, vec2 shadowMapSizeAndInverse, float darkness, float frustumEdgeFalloff)
|
|
|
|
|
|
+ float computeShadowWithPCF5(vec4 vPositionFromLight, float depthMetric, sampler2DShadow shadowSampler, vec2 shadowMapSizeAndInverse, float darkness, float frustumEdgeFalloff)
|
|
{
|
|
{
|
|
|
|
+ if (depthMetric > 1.0 || depthMetric < 0.0) {
|
|
|
|
+ return 1.0;
|
|
|
|
+ }
|
|
|
|
+
|
|
vec3 clipSpace = vPositionFromLight.xyz / vPositionFromLight.w;
|
|
vec3 clipSpace = vPositionFromLight.xyz / vPositionFromLight.w;
|
|
vec3 uvDepth = vec3(0.5 * clipSpace.xyz + vec3(0.5));
|
|
vec3 uvDepth = vec3(0.5 * clipSpace.xyz + vec3(0.5));
|
|
|
|
|
|
@@ -334,12 +311,147 @@
|
|
return computeFallOff(shadow, clipSpace.xy, frustumEdgeFalloff);
|
|
return computeFallOff(shadow, clipSpace.xy, frustumEdgeFalloff);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ const vec3 PoissonSamplers32[64] = vec3[64](
|
|
|
|
+ vec3(0.06407013, 0.05409927, 0.),
|
|
|
|
+ vec3(0.7366577, 0.5789394, 0.),
|
|
|
|
+ vec3(-0.6270542, -0.5320278, 0.),
|
|
|
|
+ vec3(-0.4096107, 0.8411095, 0.),
|
|
|
|
+ vec3(0.6849564, -0.4990818, 0.),
|
|
|
|
+ vec3(-0.874181, -0.04579735, 0.),
|
|
|
|
+ vec3(0.9989998, 0.0009880066, 0.),
|
|
|
|
+ vec3(-0.004920578, -0.9151649, 0.),
|
|
|
|
+ vec3(0.1805763, 0.9747483, 0.),
|
|
|
|
+ vec3(-0.2138451, 0.2635818, 0.),
|
|
|
|
+ vec3(0.109845, 0.3884785, 0.),
|
|
|
|
+ vec3(0.06876755, -0.3581074, 0.),
|
|
|
|
+ vec3(0.374073, -0.7661266, 0.),
|
|
|
|
+ vec3(0.3079132, -0.1216763, 0.),
|
|
|
|
+ vec3(-0.3794335, -0.8271583, 0.),
|
|
|
|
+ vec3(-0.203878, -0.07715034, 0.),
|
|
|
|
+ vec3(0.5912697, 0.1469799, 0.),
|
|
|
|
+ vec3(-0.88069, 0.3031784, 0.),
|
|
|
|
+ vec3(0.5040108, 0.8283722, 0.),
|
|
|
|
+ vec3(-0.5844124, 0.5494877, 0.),
|
|
|
|
+ vec3(0.6017799, -0.1726654, 0.),
|
|
|
|
+ vec3(-0.5554981, 0.1559997, 0.),
|
|
|
|
+ vec3(-0.3016369, -0.3900928, 0.),
|
|
|
|
+ vec3(-0.5550632, -0.1723762, 0.),
|
|
|
|
+ vec3(0.925029, 0.2995041, 0.),
|
|
|
|
+ vec3(-0.2473137, 0.5538505, 0.),
|
|
|
|
+ vec3(0.9183037, -0.2862392, 0.),
|
|
|
|
+ vec3(0.2469421, 0.6718712, 0.),
|
|
|
|
+ vec3(0.3916397, -0.4328209, 0.),
|
|
|
|
+ vec3(-0.03576927, -0.6220032, 0.),
|
|
|
|
+ vec3(-0.04661255, 0.7995201, 0.),
|
|
|
|
+ vec3(0.4402924, 0.3640312, 0.),
|
|
|
|
+
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.),
|
|
|
|
+ vec3(0., 0., 0.)
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ const vec3 PoissonSamplers64[64] = vec3[64](
|
|
|
|
+ vec3(-0.613392, 0.617481, 0.),
|
|
|
|
+ vec3(0.170019, -0.040254, 0.),
|
|
|
|
+ vec3(-0.299417, 0.791925, 0.),
|
|
|
|
+ vec3(0.645680, 0.493210, 0.),
|
|
|
|
+ vec3(-0.651784, 0.717887, 0.),
|
|
|
|
+ vec3(0.421003, 0.027070, 0.),
|
|
|
|
+ vec3(-0.817194, -0.271096, 0.),
|
|
|
|
+ vec3(-0.705374, -0.668203, 0.),
|
|
|
|
+ vec3(0.977050, -0.108615, 0.),
|
|
|
|
+ vec3(0.063326, 0.142369, 0.),
|
|
|
|
+ vec3(0.203528, 0.214331, 0.),
|
|
|
|
+ vec3(-0.667531, 0.326090, 0.),
|
|
|
|
+ vec3(-0.098422, -0.295755, 0.),
|
|
|
|
+ vec3(-0.885922, 0.215369, 0.),
|
|
|
|
+ vec3(0.566637, 0.605213, 0.),
|
|
|
|
+ vec3(0.039766, -0.396100, 0.),
|
|
|
|
+ vec3(0.751946, 0.453352, 0.),
|
|
|
|
+ vec3(0.078707, -0.715323, 0.),
|
|
|
|
+ vec3(-0.075838, -0.529344, 0.),
|
|
|
|
+ vec3(0.724479, -0.580798, 0.),
|
|
|
|
+ vec3(0.222999, -0.215125, 0.),
|
|
|
|
+ vec3(-0.467574, -0.405438, 0.),
|
|
|
|
+ vec3(-0.248268, -0.814753, 0.),
|
|
|
|
+ vec3(0.354411, -0.887570, 0.),
|
|
|
|
+ vec3(0.175817, 0.382366, 0.),
|
|
|
|
+ vec3(0.487472, -0.063082, 0.),
|
|
|
|
+ vec3(-0.084078, 0.898312, 0.),
|
|
|
|
+ vec3(0.488876, -0.783441, 0.),
|
|
|
|
+ vec3(0.470016, 0.217933, 0.),
|
|
|
|
+ vec3(-0.696890, -0.549791, 0.),
|
|
|
|
+ vec3(-0.149693, 0.605762, 0.),
|
|
|
|
+ vec3(0.034211, 0.979980, 0.),
|
|
|
|
+ vec3(0.503098, -0.308878, 0.),
|
|
|
|
+ vec3(-0.016205, -0.872921, 0.),
|
|
|
|
+ vec3(0.385784, -0.393902, 0.),
|
|
|
|
+ vec3(-0.146886, -0.859249, 0.),
|
|
|
|
+ vec3(0.643361, 0.164098, 0.),
|
|
|
|
+ vec3(0.634388, -0.049471, 0.),
|
|
|
|
+ vec3(-0.688894, 0.007843, 0.),
|
|
|
|
+ vec3(0.464034, -0.188818, 0.),
|
|
|
|
+ vec3(-0.440840, 0.137486, 0.),
|
|
|
|
+ vec3(0.364483, 0.511704, 0.),
|
|
|
|
+ vec3(0.034028, 0.325968, 0.),
|
|
|
|
+ vec3(0.099094, -0.308023, 0.),
|
|
|
|
+ vec3(0.693960, -0.366253, 0.),
|
|
|
|
+ vec3(0.678884, -0.204688, 0.),
|
|
|
|
+ vec3(0.001801, 0.780328, 0.),
|
|
|
|
+ vec3(0.145177, -0.898984, 0.),
|
|
|
|
+ vec3(0.062655, -0.611866, 0.),
|
|
|
|
+ vec3(0.315226, -0.604297, 0.),
|
|
|
|
+ vec3(-0.780145, 0.486251, 0.),
|
|
|
|
+ vec3(-0.371868, 0.882138, 0.),
|
|
|
|
+ vec3(0.200476, 0.494430, 0.),
|
|
|
|
+ vec3(-0.494552, -0.711051, 0.),
|
|
|
|
+ vec3(0.612476, 0.705252, 0.),
|
|
|
|
+ vec3(-0.578845, -0.768792, 0.),
|
|
|
|
+ vec3(-0.772454, -0.090976, 0.),
|
|
|
|
+ vec3(0.504440, 0.372295, 0.),
|
|
|
|
+ vec3(0.155736, 0.065157, 0.),
|
|
|
|
+ vec3(0.391522, 0.849605, 0.),
|
|
|
|
+ vec3(-0.620106, -0.328104, 0.),
|
|
|
|
+ vec3(0.789239, -0.419965, 0.),
|
|
|
|
+ vec3(-0.545396, 0.538133, 0.),
|
|
|
|
+ vec3(-0.178564, -0.596057, 0.)
|
|
|
|
+ );
|
|
|
|
+
|
|
// PCSS
|
|
// PCSS
|
|
// This helps to achieve a contact hardening effect on the shadow
|
|
// This helps to achieve a contact hardening effect on the shadow
|
|
// It uses 16 Taps for search and a 32 PCF taps in a randomly rotating poisson sampling disc.
|
|
// It uses 16 Taps for search and a 32 PCF taps in a randomly rotating poisson sampling disc.
|
|
// This is heavily inspired from http://developer.download.nvidia.com/shaderlibrary/docs/shadow_PCSS.pdf
|
|
// This is heavily inspired from http://developer.download.nvidia.com/shaderlibrary/docs/shadow_PCSS.pdf
|
|
// and http://developer.download.nvidia.com/whitepapers/2008/PCSS_Integration.pdf
|
|
// and http://developer.download.nvidia.com/whitepapers/2008/PCSS_Integration.pdf
|
|
- float computeShadowWithPCSS(vec4 vPositionFromLight, float depthMetric, sampler2D depthSampler, sampler2DShadow shadowSampler, float shadowMapSizeInverse, float lightSizeUV, float darkness, float frustumEdgeFalloff)
|
|
|
|
|
|
+ float computeShadowWithPCSS(vec4 vPositionFromLight, float depthMetric, sampler2D depthSampler, sampler2DShadow shadowSampler, float shadowMapSizeInverse, float lightSizeUV, float darkness, float frustumEdgeFalloff, int searchTapCount, int pcfTapCount, vec3[64] poissonSamplers)
|
|
{
|
|
{
|
|
if (depthMetric > 1.0 || depthMetric < 0.0) {
|
|
if (depthMetric > 1.0 || depthMetric < 0.0) {
|
|
return 1.0;
|
|
return 1.0;
|
|
@@ -351,8 +463,8 @@
|
|
float blockerDepth = 0.0;
|
|
float blockerDepth = 0.0;
|
|
float sumBlockerDepth = 0.0;
|
|
float sumBlockerDepth = 0.0;
|
|
float numBlocker = 0.0;
|
|
float numBlocker = 0.0;
|
|
- for (int i = 0; i < 16; i++) {
|
|
|
|
- blockerDepth = texture(depthSampler, uvDepth.xy + (lightSizeUV * shadowMapSizeInverse * PCFSamplers[i].xy)).r;
|
|
|
|
|
|
+ for (int i = 0; i < searchTapCount; i ++) {
|
|
|
|
+ blockerDepth = texture(depthSampler, uvDepth.xy + (lightSizeUV * shadowMapSizeInverse * PoissonSamplers32[i].xy)).r;
|
|
if (blockerDepth < depthMetric) {
|
|
if (blockerDepth < depthMetric) {
|
|
sumBlockerDepth += blockerDepth;
|
|
sumBlockerDepth += blockerDepth;
|
|
numBlocker++;
|
|
numBlocker++;
|
|
@@ -364,27 +476,49 @@
|
|
}
|
|
}
|
|
float avgBlockerDepth = sumBlockerDepth / numBlocker;
|
|
float avgBlockerDepth = sumBlockerDepth / numBlocker;
|
|
|
|
|
|
- // float penumbraRatio = (depthMetric - avgBlockerDepth) / avgBlockerDepth;
|
|
|
|
- // Do not dividing by z despite being physically incorrect looks better due to the limited kernel size.
|
|
|
|
|
|
+ // Offset preventing aliasing on contact.
|
|
float AAOffset = shadowMapSizeInverse * 10.;
|
|
float AAOffset = shadowMapSizeInverse * 10.;
|
|
|
|
+ // Do not dividing by z despite being physically incorrect looks better due to the limited kernel size.
|
|
|
|
+ // float penumbraRatio = (depthMetric - avgBlockerDepth) / avgBlockerDepth;
|
|
float penumbraRatio = ((depthMetric - avgBlockerDepth) + AAOffset);
|
|
float penumbraRatio = ((depthMetric - avgBlockerDepth) + AAOffset);
|
|
float filterRadius = penumbraRatio * lightSizeUV * shadowMapSizeInverse;
|
|
float filterRadius = penumbraRatio * lightSizeUV * shadowMapSizeInverse;
|
|
|
|
|
|
- float random = getRand(gl_FragCoord.xy);
|
|
|
|
|
|
+ float random = getRand(vPositionFromLight.xy);
|
|
float rotationAngle = random * 3.1415926;
|
|
float rotationAngle = random * 3.1415926;
|
|
vec2 rotationVector = vec2(cos(rotationAngle), sin(rotationAngle));
|
|
vec2 rotationVector = vec2(cos(rotationAngle), sin(rotationAngle));
|
|
|
|
|
|
float shadow = 0.;
|
|
float shadow = 0.;
|
|
- for (int i = 0; i < 32; i++) {
|
|
|
|
- vec3 offset = PCFSamplers[i];
|
|
|
|
|
|
+ for (int i = 0; i < pcfTapCount; i++) {
|
|
|
|
+ vec3 offset = poissonSamplers[i];
|
|
// Rotated offset.
|
|
// Rotated offset.
|
|
offset = vec3(offset.x * rotationVector.x - offset.y * rotationVector.y, offset.y * rotationVector.x + offset.x * rotationVector.y, 0.);
|
|
offset = vec3(offset.x * rotationVector.x - offset.y * rotationVector.y, offset.y * rotationVector.x + offset.x * rotationVector.y, 0.);
|
|
shadow += texture2D(shadowSampler, uvDepth + offset * filterRadius);
|
|
shadow += texture2D(shadowSampler, uvDepth + offset * filterRadius);
|
|
}
|
|
}
|
|
- shadow /= 32.;
|
|
|
|
|
|
+ shadow /= float(pcfTapCount);
|
|
|
|
|
|
|
|
+ // Blocker distance falloff
|
|
|
|
+ shadow = mix(shadow, 1., depthMetric - avgBlockerDepth);
|
|
|
|
+
|
|
|
|
+ // Apply darkness
|
|
shadow = shadow * (1. - darkness) + darkness;
|
|
shadow = shadow * (1. - darkness) + darkness;
|
|
|
|
+
|
|
|
|
+ // Apply light frustrum fallof
|
|
return computeFallOff(shadow, clipSpace.xy, frustumEdgeFalloff);
|
|
return computeFallOff(shadow, clipSpace.xy, frustumEdgeFalloff);
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ float computeShadowWithPCSS16(vec4 vPositionFromLight, float depthMetric, sampler2D depthSampler, sampler2DShadow shadowSampler, float shadowMapSizeInverse, float lightSizeUV, float darkness, float frustumEdgeFalloff)
|
|
|
|
+ {
|
|
|
|
+ return computeShadowWithPCSS(vPositionFromLight, depthMetric, depthSampler, shadowSampler, shadowMapSizeInverse, lightSizeUV, darkness, frustumEdgeFalloff, 16, 16, PoissonSamplers32);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ float computeShadowWithPCSS32(vec4 vPositionFromLight, float depthMetric, sampler2D depthSampler, sampler2DShadow shadowSampler, float shadowMapSizeInverse, float lightSizeUV, float darkness, float frustumEdgeFalloff)
|
|
|
|
+ {
|
|
|
|
+ return computeShadowWithPCSS(vPositionFromLight, depthMetric, depthSampler, shadowSampler, shadowMapSizeInverse, lightSizeUV, darkness, frustumEdgeFalloff, 16, 32, PoissonSamplers32);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ float computeShadowWithPCSS64(vec4 vPositionFromLight, float depthMetric, sampler2D depthSampler, sampler2DShadow shadowSampler, float shadowMapSizeInverse, float lightSizeUV, float darkness, float frustumEdgeFalloff)
|
|
|
|
+ {
|
|
|
|
+ return computeShadowWithPCSS(vPositionFromLight, depthMetric, depthSampler, shadowSampler, shadowMapSizeInverse, lightSizeUV, darkness, frustumEdgeFalloff, 32, 64, PoissonSamplers64);
|
|
|
|
+ }
|
|
#endif
|
|
#endif
|
|
#endif
|
|
#endif
|