aboutsummaryrefslogtreecommitdiff
path: root/client/shaders/nodes_shader
diff options
context:
space:
mode:
authorLiso <anlismon@gmail.com>2021-06-06 18:51:21 +0200
committerGitHub <noreply@github.com>2021-06-06 18:51:21 +0200
commitc47313db65f968559711ac1b505ef341a9872017 (patch)
tree63d9b1b2be512918e2361d96e4fb52ff1ec3f9de /client/shaders/nodes_shader
parent46f42e15c41cf4ab23c5ff4cd8a7d99d94d10d7b (diff)
downloadminetest-c47313db65f968559711ac1b505ef341a9872017.tar.gz
minetest-c47313db65f968559711ac1b505ef341a9872017.tar.bz2
minetest-c47313db65f968559711ac1b505ef341a9872017.zip
Shadow mapping render pass (#11244)
Co-authored-by: x2048 <codeforsmile@gmail.com>
Diffstat (limited to 'client/shaders/nodes_shader')
-rw-r--r--client/shaders/nodes_shader/opengl_fragment.glsl431
-rw-r--r--client/shaders/nodes_shader/opengl_vertex.glsl59
2 files changed, 483 insertions, 7 deletions
diff --git a/client/shaders/nodes_shader/opengl_fragment.glsl b/client/shaders/nodes_shader/opengl_fragment.glsl
index b58095063..43a8b1f25 100644
--- a/client/shaders/nodes_shader/opengl_fragment.glsl
+++ b/client/shaders/nodes_shader/opengl_fragment.glsl
@@ -7,7 +7,22 @@ uniform vec3 eyePosition;
// The cameraOffset is the current center of the visible world.
uniform vec3 cameraOffset;
uniform float animationTimer;
+#ifdef ENABLE_DYNAMIC_SHADOWS
+ // shadow texture
+ uniform sampler2D ShadowMapSampler;
+ // shadow uniforms
+ uniform vec3 v_LightDirection;
+ uniform float f_textureresolution;
+ uniform mat4 m_ShadowViewProj;
+ uniform float f_shadowfar;
+ varying float normalOffsetScale;
+ varying float adj_shadow_strength;
+ varying float cosLight;
+ varying float f_normal_length;
+#endif
+
+varying vec3 vNormal;
varying vec3 vPosition;
// World position in the visible world (i.e. relative to the cameraOffset.)
// This can be used for many shader effects without loss of precision.
@@ -22,10 +37,388 @@ varying mediump vec2 varTexCoord;
centroid varying vec2 varTexCoord;
#endif
varying vec3 eyeVec;
+varying float nightRatio;
const float fogStart = FOG_START;
const float fogShadingParameter = 1.0 / ( 1.0 - fogStart);
+
+
+#ifdef ENABLE_DYNAMIC_SHADOWS
+const float bias0 = 0.9;
+const float zPersFactor = 0.5;
+const float bias1 = 1.0 - bias0 + 1e-6;
+
+vec4 getPerspectiveFactor(in vec4 shadowPosition)
+{
+
+ float pDistance = length(shadowPosition.xy);
+ float pFactor = pDistance * bias0 + bias1;
+
+ shadowPosition.xyz *= vec3(vec2(1.0 / pFactor), zPersFactor);
+
+ return shadowPosition;
+}
+
+// assuming near is always 1.0
+float getLinearDepth()
+{
+ return 2.0 * f_shadowfar / (f_shadowfar + 1.0 - (2.0 * gl_FragCoord.z - 1.0) * (f_shadowfar - 1.0));
+}
+
+vec3 getLightSpacePosition()
+{
+ vec4 pLightSpace;
+ // some drawtypes have zero normals, so we need to handle it :(
+ #if DRAW_TYPE == NDT_PLANTLIKE
+ pLightSpace = m_ShadowViewProj * vec4(worldPosition, 1.0);
+ #else
+ float offsetScale = (0.0057 * getLinearDepth() + normalOffsetScale);
+ pLightSpace = m_ShadowViewProj * vec4(worldPosition + offsetScale * normalize(vNormal), 1.0);
+ #endif
+ pLightSpace = getPerspectiveFactor(pLightSpace);
+ return pLightSpace.xyz * 0.5 + 0.5;
+}
+// custom smoothstep implementation because it's not defined in glsl1.2
+// https://docs.gl/sl4/smoothstep
+float mtsmoothstep(in float edge0, in float edge1, in float x)
+{
+ float t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
+ return t * t * (3.0 - 2.0 * t);
+}
+
+#ifdef COLORED_SHADOWS
+
+// c_precision of 128 fits within 7 base-10 digits
+const float c_precision = 128.0;
+const float c_precisionp1 = c_precision + 1.0;
+
+float packColor(vec3 color)
+{
+ return floor(color.b * c_precision + 0.5)
+ + floor(color.g * c_precision + 0.5) * c_precisionp1
+ + floor(color.r * c_precision + 0.5) * c_precisionp1 * c_precisionp1;
+}
+
+vec3 unpackColor(float value)
+{
+ vec3 color;
+ color.b = mod(value, c_precisionp1) / c_precision;
+ color.g = mod(floor(value / c_precisionp1), c_precisionp1) / c_precision;
+ color.r = floor(value / (c_precisionp1 * c_precisionp1)) / c_precision;
+ return color;
+}
+
+vec4 getHardShadowColor(sampler2D shadowsampler, vec2 smTexCoord, float realDistance)
+{
+ vec4 texDepth = texture2D(shadowsampler, smTexCoord.xy).rgba;
+
+ float visibility = step(0.0, realDistance - texDepth.r);
+ vec4 result = vec4(visibility, vec3(0.0,0.0,0.0));//unpackColor(texDepth.g));
+ if (visibility < 0.1) {
+ visibility = step(0.0, realDistance - texDepth.b);
+ result = vec4(visibility, unpackColor(texDepth.a));
+ }
+ return result;
+}
+
+#else
+
+float getHardShadow(sampler2D shadowsampler, vec2 smTexCoord, float realDistance)
+{
+ float texDepth = texture2D(shadowsampler, smTexCoord.xy).r;
+ float visibility = step(0.0, realDistance - texDepth);
+ return visibility;
+}
+
+#endif
+
+
+#if SHADOW_FILTER == 2
+ #define PCFBOUND 3.5
+ #define PCFSAMPLES 64.0
+#elif SHADOW_FILTER == 1
+ #define PCFBOUND 1.5
+ #if defined(POISSON_FILTER)
+ #define PCFSAMPLES 32.0
+ #else
+ #define PCFSAMPLES 16.0
+ #endif
+#else
+ #define PCFBOUND 0.0
+ #if defined(POISSON_FILTER)
+ #define PCFSAMPLES 4.0
+ #else
+ #define PCFSAMPLES 1.0
+ #endif
+#endif
+#ifdef COLORED_SHADOWS
+float getHardShadowDepth(sampler2D shadowsampler, vec2 smTexCoord, float realDistance)
+{
+ vec4 texDepth = texture2D(shadowsampler, smTexCoord.xy);
+ float depth = max(realDistance - texDepth.r, realDistance - texDepth.b);
+ return depth;
+}
+#else
+float getHardShadowDepth(sampler2D shadowsampler, vec2 smTexCoord, float realDistance)
+{
+ float texDepth = texture2D(shadowsampler, smTexCoord.xy).r;
+ float depth = realDistance - texDepth;
+ return depth;
+}
+#endif
+
+float getBaseLength(vec2 smTexCoord)
+{
+ float l = length(2.0 * smTexCoord.xy - 1.0); // length in texture coords
+ return bias1 / (1.0 / l - bias0); // return to undistorted coords
+}
+
+float getDeltaPerspectiveFactor(float l)
+{
+ return 0.1 / (bias0 * l + bias1); // original distortion factor, divided by 10
+}
+
+float getPenumbraRadius(sampler2D shadowsampler, vec2 smTexCoord, float realDistance, float multiplier)
+{
+ // Return fast if sharp shadows are requested
+ if (SOFTSHADOWRADIUS <= 1.0)
+ return SOFTSHADOWRADIUS;
+
+ vec2 clampedpos;
+ float texture_size = 1.0 / (2048 /*f_textureresolution*/ * 0.5);
+ float y, x;
+ float depth = 0.0;
+ float pointDepth;
+ float maxRadius = SOFTSHADOWRADIUS * 5.0 * multiplier;
+
+ float baseLength = getBaseLength(smTexCoord);
+ float perspectiveFactor;
+ float bound = clamp(PCFBOUND * (1 - baseLength), 0.5, PCFBOUND);
+ int n = 0;
+
+ for (y = -bound; y <= bound; y += 1.0)
+ for (x = -bound; x <= bound; x += 1.0) {
+ clampedpos = vec2(x,y);
+ perspectiveFactor = getDeltaPerspectiveFactor(baseLength + length(clampedpos) * texture_size * maxRadius);
+ clampedpos = clampedpos * texture_size * perspectiveFactor * maxRadius * perspectiveFactor + smTexCoord.xy;
+
+ pointDepth = getHardShadowDepth(shadowsampler, clampedpos.xy, realDistance);
+ if (pointDepth > -0.01) {
+ depth += pointDepth;
+ n += 1;
+ }
+ }
+
+ depth = depth / n;
+
+ depth = pow(clamp(depth, 0.0, 1000.0), 1.6) / 0.001;
+ return max(0.5, depth * maxRadius);
+}
+
+#ifdef POISSON_FILTER
+const vec2[64] poissonDisk = vec2[64](
+ vec2(0.170019, -0.040254),
+ vec2(-0.299417, 0.791925),
+ vec2(0.645680, 0.493210),
+ vec2(-0.651784, 0.717887),
+ vec2(0.421003, 0.027070),
+ vec2(-0.817194, -0.271096),
+ vec2(-0.705374, -0.668203),
+ vec2(0.977050, -0.108615),
+ vec2(0.063326, 0.142369),
+ vec2(0.203528, 0.214331),
+ vec2(-0.667531, 0.326090),
+ vec2(-0.098422, -0.295755),
+ vec2(-0.885922, 0.215369),
+ vec2(0.566637, 0.605213),
+ vec2(0.039766, -0.396100),
+ vec2(0.751946, 0.453352),
+ vec2(0.078707, -0.715323),
+ vec2(-0.075838, -0.529344),
+ vec2(0.724479, -0.580798),
+ vec2(0.222999, -0.215125),
+ vec2(-0.467574, -0.405438),
+ vec2(-0.248268, -0.814753),
+ vec2(0.354411, -0.887570),
+ vec2(0.175817, 0.382366),
+ vec2(0.487472, -0.063082),
+ vec2(0.355476, 0.025357),
+ vec2(-0.084078, 0.898312),
+ vec2(0.488876, -0.783441),
+ vec2(0.470016, 0.217933),
+ vec2(-0.696890, -0.549791),
+ vec2(-0.149693, 0.605762),
+ vec2(0.034211, 0.979980),
+ vec2(0.503098, -0.308878),
+ vec2(-0.016205, -0.872921),
+ vec2(0.385784, -0.393902),
+ vec2(-0.146886, -0.859249),
+ vec2(0.643361, 0.164098),
+ vec2(0.634388, -0.049471),
+ vec2(-0.688894, 0.007843),
+ vec2(0.464034, -0.188818),
+ vec2(-0.440840, 0.137486),
+ vec2(0.364483, 0.511704),
+ vec2(0.034028, 0.325968),
+ vec2(0.099094, -0.308023),
+ vec2(0.693960, -0.366253),
+ vec2(0.678884, -0.204688),
+ vec2(0.001801, 0.780328),
+ vec2(0.145177, -0.898984),
+ vec2(0.062655, -0.611866),
+ vec2(0.315226, -0.604297),
+ vec2(-0.780145, 0.486251),
+ vec2(-0.371868, 0.882138),
+ vec2(0.200476, 0.494430),
+ vec2(-0.494552, -0.711051),
+ vec2(0.612476, 0.705252),
+ vec2(-0.578845, -0.768792),
+ vec2(-0.772454, -0.090976),
+ vec2(0.504440, 0.372295),
+ vec2(0.155736, 0.065157),
+ vec2(0.391522, 0.849605),
+ vec2(-0.620106, -0.328104),
+ vec2(0.789239, -0.419965),
+ vec2(-0.545396, 0.538133),
+ vec2(-0.178564, -0.596057)
+);
+
+#ifdef COLORED_SHADOWS
+
+vec4 getShadowColor(sampler2D shadowsampler, vec2 smTexCoord, float realDistance)
+{
+ vec2 clampedpos;
+ vec4 visibility = vec4(0.0);
+ float radius = getPenumbraRadius(shadowsampler, smTexCoord, realDistance, 1.5); // scale to align with PCF
+ if (radius < 0.1) {
+ // we are in the middle of even brightness, no need for filtering
+ return getHardShadowColor(shadowsampler, smTexCoord.xy, realDistance);
+ }
+
+ float baseLength = getBaseLength(smTexCoord);
+ float perspectiveFactor;
+
+ float texture_size = 1.0 / (f_textureresolution * 0.5);
+ int samples = int(clamp(PCFSAMPLES * (1 - baseLength) * (1 - baseLength), 1, PCFSAMPLES));
+ int init_offset = int(floor(mod(((smTexCoord.x * 34.0) + 1.0) * smTexCoord.y, 64.0-samples)));
+ int end_offset = int(samples) + init_offset;
+
+ for (int x = init_offset; x < end_offset; x++) {
+ clampedpos = poissonDisk[x];
+ perspectiveFactor = getDeltaPerspectiveFactor(baseLength + length(clampedpos) * texture_size * radius);
+ clampedpos = clampedpos * texture_size * perspectiveFactor * radius * perspectiveFactor + smTexCoord.xy;
+ visibility += getHardShadowColor(shadowsampler, clampedpos.xy, realDistance);
+ }
+
+ return visibility / samples;
+}
+
+#else
+
+float getShadow(sampler2D shadowsampler, vec2 smTexCoord, float realDistance)
+{
+ vec2 clampedpos;
+ float visibility = 0.0;
+ float radius = getPenumbraRadius(shadowsampler, smTexCoord, realDistance, 1.5); // scale to align with PCF
+ if (radius < 0.1) {
+ // we are in the middle of even brightness, no need for filtering
+ return getHardShadow(shadowsampler, smTexCoord.xy, realDistance);
+ }
+
+ float baseLength = getBaseLength(smTexCoord);
+ float perspectiveFactor;
+
+ float texture_size = 1.0 / (f_textureresolution * 0.5);
+ int samples = int(clamp(PCFSAMPLES * (1 - baseLength) * (1 - baseLength), 1, PCFSAMPLES));
+ int init_offset = int(floor(mod(((smTexCoord.x * 34.0) + 1.0) * smTexCoord.y, 64.0-samples)));
+ int end_offset = int(samples) + init_offset;
+
+ for (int x = init_offset; x < end_offset; x++) {
+ clampedpos = poissonDisk[x];
+ perspectiveFactor = getDeltaPerspectiveFactor(baseLength + length(clampedpos) * texture_size * radius);
+ clampedpos = clampedpos * texture_size * perspectiveFactor * radius * perspectiveFactor + smTexCoord.xy;
+ visibility += getHardShadow(shadowsampler, clampedpos.xy, realDistance);
+ }
+
+ return visibility / samples;
+}
+
+#endif
+
+#else
+/* poisson filter disabled */
+
+#ifdef COLORED_SHADOWS
+
+vec4 getShadowColor(sampler2D shadowsampler, vec2 smTexCoord, float realDistance)
+{
+ vec2 clampedpos;
+ vec4 visibility = vec4(0.0);
+ float radius = getPenumbraRadius(shadowsampler, smTexCoord, realDistance, 1.0);
+ if (radius < 0.1) {
+ // we are in the middle of even brightness, no need for filtering
+ return getHardShadowColor(shadowsampler, smTexCoord.xy, realDistance);
+ }
+
+ float baseLength = getBaseLength(smTexCoord);
+ float perspectiveFactor;
+
+ float texture_size = 1.0 / (f_textureresolution * 0.5);
+ float y, x;
+ float bound = clamp(PCFBOUND * (1 - baseLength), 0.5, PCFBOUND);
+ int n = 0;
+
+ // basic PCF filter
+ for (y = -bound; y <= bound; y += 1.0)
+ for (x = -bound; x <= bound; x += 1.0) {
+ clampedpos = vec2(x,y); // screen offset
+ perspectiveFactor = getDeltaPerspectiveFactor(baseLength + length(clampedpos) * texture_size * radius / bound);
+ clampedpos = clampedpos * texture_size * perspectiveFactor * radius * perspectiveFactor / bound + smTexCoord.xy; // both dx,dy and radius are adjusted
+ visibility += getHardShadowColor(shadowsampler, clampedpos.xy, realDistance);
+ n += 1;
+ }
+
+ return visibility / n;
+}
+
+#else
+float getShadow(sampler2D shadowsampler, vec2 smTexCoord, float realDistance)
+{
+ vec2 clampedpos;
+ float visibility = 0.0;
+ float radius = getPenumbraRadius(shadowsampler, smTexCoord, realDistance, 1.0);
+ if (radius < 0.1) {
+ // we are in the middle of even brightness, no need for filtering
+ return getHardShadow(shadowsampler, smTexCoord.xy, realDistance);
+ }
+
+ float baseLength = getBaseLength(smTexCoord);
+ float perspectiveFactor;
+
+ float texture_size = 1.0 / (f_textureresolution * 0.5);
+ float y, x;
+ float bound = clamp(PCFBOUND * (1 - baseLength), 0.5, PCFBOUND);
+ int n = 0;
+
+ // basic PCF filter
+ for (y = -bound; y <= bound; y += 1.0)
+ for (x = -bound; x <= bound; x += 1.0) {
+ clampedpos = vec2(x,y); // screen offset
+ perspectiveFactor = getDeltaPerspectiveFactor(baseLength + length(clampedpos) * texture_size * radius / bound);
+ clampedpos = clampedpos * texture_size * perspectiveFactor * radius * perspectiveFactor / bound + smTexCoord.xy; // both dx,dy and radius are adjusted
+ visibility += getHardShadow(shadowsampler, clampedpos.xy, realDistance);
+ n += 1;
+ }
+
+ return visibility / n;
+}
+
+#endif
+
+#endif
+#endif
+
#if ENABLE_TONE_MAPPING
/* Hable's UC2 Tone mapping parameters
@@ -58,6 +451,8 @@ vec4 applyToneMapping(vec4 color)
}
#endif
+
+
void main(void)
{
vec3 color;
@@ -74,9 +469,41 @@ void main(void)
#endif
color = base.rgb;
-
vec4 col = vec4(color.rgb * varColor.rgb, 1.0);
+#ifdef ENABLE_DYNAMIC_SHADOWS
+ float shadow_int = 0.0;
+ vec3 shadow_color = vec3(0.0, 0.0, 0.0);
+ vec3 posLightSpace = getLightSpacePosition();
+
+ float distance_rate = (1 - pow(clamp(2.0 * length(posLightSpace.xy - 0.5),0.0,1.0), 20.0));
+ float f_adj_shadow_strength = max(adj_shadow_strength-mtsmoothstep(0.9,1.1, posLightSpace.z ),0.0);
+
+ if (distance_rate > 1e-7) {
+
+#ifdef COLORED_SHADOWS
+ vec4 visibility = getShadowColor(ShadowMapSampler, posLightSpace.xy, posLightSpace.z);
+ shadow_int = visibility.r;
+ shadow_color = visibility.gba;
+#else
+ shadow_int = getShadow(ShadowMapSampler, posLightSpace.xy, posLightSpace.z);
+#endif
+ shadow_int *= distance_rate;
+ shadow_int *= 1.0 - nightRatio;
+
+
+ }
+
+ if (f_normal_length != 0 && cosLight < 0.0) {
+ shadow_int = clamp(1.0-nightRatio, 0.0, 1.0);
+ }
+
+ shadow_int = 1.0 - (shadow_int * f_adj_shadow_strength);
+
+ col.rgb = mix(shadow_color,col.rgb,shadow_int)*shadow_int;
+ // col.r = 0.5 * clamp(getPenumbraRadius(ShadowMapSampler, posLightSpace.xy, posLightSpace.z, 1.0) / SOFTSHADOWRADIUS, 0.0, 1.0) + 0.5 * col.r;
+#endif
+
#if ENABLE_TONE_MAPPING
col = applyToneMapping(col);
#endif
@@ -94,6 +521,6 @@ void main(void)
- fogShadingParameter * length(eyeVec) / fogDistance, 0.0, 1.0);
col = mix(skyBgColor, col, clarity);
col = vec4(col.rgb, base.a);
-
+
gl_FragColor = col;
}
diff --git a/client/shaders/nodes_shader/opengl_vertex.glsl b/client/shaders/nodes_shader/opengl_vertex.glsl
index 1a4840d35..d316930b2 100644
--- a/client/shaders/nodes_shader/opengl_vertex.glsl
+++ b/client/shaders/nodes_shader/opengl_vertex.glsl
@@ -1,5 +1,4 @@
uniform mat4 mWorld;
-
// Color of the light emitted by the sun.
uniform vec3 dayLight;
uniform vec3 eyePosition;
@@ -8,6 +7,7 @@ uniform vec3 eyePosition;
uniform vec3 cameraOffset;
uniform float animationTimer;
+varying vec3 vNormal;
varying vec3 vPosition;
// World position in the visible world (i.e. relative to the cameraOffset.)
// This can be used for many shader effects without loss of precision.
@@ -24,13 +24,38 @@ varying mediump vec2 varTexCoord;
#else
centroid varying vec2 varTexCoord;
#endif
-varying vec3 eyeVec;
+#ifdef ENABLE_DYNAMIC_SHADOWS
+ // shadow uniforms
+ uniform vec3 v_LightDirection;
+ uniform float f_textureresolution;
+ uniform mat4 m_ShadowViewProj;
+ uniform float f_shadowfar;
+ uniform float f_shadow_strength;
+ uniform float f_timeofday;
+ varying float cosLight;
+ varying float normalOffsetScale;
+ varying float adj_shadow_strength;
+ varying float f_normal_length;
+#endif
+
+varying vec3 eyeVec;
+varying float nightRatio;
// Color of the light emitted by the light sources.
const vec3 artificialLight = vec3(1.04, 1.04, 1.04);
const float e = 2.718281828459;
const float BS = 10.0;
+#ifdef ENABLE_DYNAMIC_SHADOWS
+// custom smoothstep implementation because it's not defined in glsl1.2
+// https://docs.gl/sl4/smoothstep
+float mtsmoothstep(in float edge0, in float edge1, in float x)
+{
+ float t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
+ return t * t * (3.0 - 2.0 * t);
+}
+#endif
+
float smoothCurve(float x)
{
@@ -86,6 +111,9 @@ float snoise(vec3 p)
#endif
+
+
+
void main(void)
{
varTexCoord = inTexCoord0.st;
@@ -136,10 +164,9 @@ void main(void)
gl_Position = mWorldViewProj * inVertexPosition;
#endif
-
vPosition = gl_Position.xyz;
-
eyeVec = -(mWorldView * inVertexPosition).xyz;
+ vNormal = inVertexNormal;
// Calculate color.
// Red, green and blue components are pre-multiplied with
@@ -152,7 +179,7 @@ void main(void)
vec4 color = inVertexColor;
#endif
// The alpha gives the ratio of sunlight in the incoming light.
- float nightRatio = 1.0 - color.a;
+ nightRatio = 1.0 - color.a;
color.rgb = color.rgb * (color.a * dayLight.rgb +
nightRatio * artificialLight.rgb) * 2.0;
color.a = 1.0;
@@ -164,4 +191,26 @@ void main(void)
0.07 * brightness);
varColor = clamp(color, 0.0, 1.0);
+
+#ifdef ENABLE_DYNAMIC_SHADOWS
+ vec3 nNormal = normalize(vNormal);
+ cosLight = dot(nNormal, -v_LightDirection);
+ float texelSize = 767.0 / f_textureresolution;
+ float slopeScale = clamp(1.0 - abs(cosLight), 0.0, 1.0);
+ normalOffsetScale = texelSize * slopeScale;
+
+ if (f_timeofday < 0.2) {
+ adj_shadow_strength = f_shadow_strength * 0.5 *
+ (1.0 - mtsmoothstep(0.18, 0.2, f_timeofday));
+ } else if (f_timeofday >= 0.8) {
+ adj_shadow_strength = f_shadow_strength * 0.5 *
+ mtsmoothstep(0.8, 0.83, f_timeofday);
+ } else {
+ adj_shadow_strength = f_shadow_strength *
+ mtsmoothstep(0.20, 0.25, f_timeofday) *
+ (1.0 - mtsmoothstep(0.7, 0.8, f_timeofday));
+ }
+ f_normal_length = length(vNormal);
+#endif
+
}