mirror of
https://github.com/luanti-org/luanti.git
synced 2025-06-27 16:36:03 +00:00
Remove volumetrics stuff and some changes
This commit is contained in:
parent
da1a688493
commit
1bf1d0b2ff
21 changed files with 311 additions and 857 deletions
|
@ -510,9 +510,10 @@ water_wave_length (Waving liquids wavelength) float 20.0 0.1
|
|||
# Requires: shaders, enable_waving_water
|
||||
water_wave_speed (Waving liquids wave speed) float 5.0
|
||||
|
||||
# When enabled, liquid reflections are simulated.
|
||||
# When enabled, crude liquid reflections are simulated.
|
||||
# When waving liquids are enabled, the wave parameters will affect these reflections.
|
||||
#
|
||||
# Requires: shaders, enable_waving_water, enable_dynamic_shadows
|
||||
# Requires: shaders, enable_dynamic_shadows
|
||||
enable_water_reflections (Liquid reflections) bool false
|
||||
|
||||
[**Dynamic shadows]
|
||||
|
@ -684,39 +685,25 @@ bloom_strength_factor (Bloom Strength Factor) float 1.0 0.1 10.0
|
|||
# Requires: shaders, enable_post_processing, enable_bloom
|
||||
bloom_radius (Bloom Radius) float 1 0.1 8
|
||||
|
||||
[**Volumetric Effects]
|
||||
|
||||
# Set to true to enable volumetric lighting effect (a.k.a. "Godrays").
|
||||
#
|
||||
# Requires: shaders, enable_post_processing, enable_bloom
|
||||
enable_volumetric_lighting (Volumetric Lighting) bool false
|
||||
|
||||
# Set to true to render clouds as scatter volumes.
|
||||
#
|
||||
# Requires: shaders, enable_post_processing, enable_clouds
|
||||
enable_volumetric_clouds (Volumetric Clouds) bool false
|
||||
|
||||
# Higher values reduce visual quality and may introduce artifacts, but improve performance of volumetric cloud/aurora effects.
|
||||
# Range: from 1 to 4, default: 2
|
||||
#
|
||||
# Requires: shaders, enable_post_processing
|
||||
volumetrics_undersampling (Volumetric Undersampling) int 2 1 4
|
||||
|
||||
# Set to true to render volumetric auroras ("Northern Lights").
|
||||
#
|
||||
# Requires: shaders, enable_post_processing
|
||||
enable_volumetric_auroras (Volumetric Auroras) bool false
|
||||
|
||||
[**Other Effects]
|
||||
|
||||
# Makes the color of light fog more saturated.
|
||||
#
|
||||
# Requires: shaders
|
||||
enable_tinted_fog (Tinted fog) bool false
|
||||
|
||||
# Apply bump maps to nodes based on their textures. It is recommended to use a tilted sun orbit to go with this (Sky Body Orbit Tilt).
|
||||
#
|
||||
# Requires: shaders, enable_dynamic_shadows
|
||||
enable_bumpmaps (Bump maps) bool false
|
||||
|
||||
[**Other Effects]
|
||||
|
||||
# Simulate translucency when looking at foliage in the sunlight.
|
||||
# It is recommended to use this with the leaves style set to fancy.
|
||||
#
|
||||
# Requires: shaders, enable_dynamic_shadows
|
||||
enable_translucent_foliage (Translucent foliage) bool false
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
#define cloudsTexture texture0
|
||||
#define sceneTexture texture1
|
||||
#define depthmap texture2
|
||||
|
||||
uniform sampler2D cloudsTexture;
|
||||
uniform sampler2D sceneTexture;
|
||||
uniform sampler2D depthmap;
|
||||
uniform vec2 texelSize0;
|
||||
|
||||
uniform vec3 dayLight;
|
||||
uniform vec3 cloudColor;
|
||||
|
||||
varying vec2 screenspaceCoordinate;
|
||||
varying vec3 relativePosition;
|
||||
varying vec3 viewDirection;
|
||||
varying vec3 sunTint;
|
||||
varying float auroraFactor;
|
||||
|
||||
uniform vec3 cameraOffset;
|
||||
uniform vec3 cameraPosition;
|
||||
|
||||
uniform float cameraNear;
|
||||
uniform float cameraFar;
|
||||
|
||||
uniform float cloudHeight;
|
||||
uniform float cloudThickness;
|
||||
|
||||
float getDepth(vec2 screenspacePosition) {
|
||||
float depth = texture2D(depthmap, screenspacePosition * 0.5 + 0.5).r;
|
||||
return cameraNear * cameraFar / (cameraFar + depth * (cameraNear - cameraFar));
|
||||
}
|
||||
|
||||
vec4 sampleClouds(vec2 uv) {
|
||||
vec4 cloudsKey = texture2D(cloudsTexture, uv);
|
||||
|
||||
//const vec3 darkColor = vec3(0.05, 0.1, 0.2);
|
||||
vec3 darkColor = vec3(0.2, 0.4, 0.8) * dayLight;
|
||||
const vec3 auroraDark = vec3(0., 0.5, 0.5);
|
||||
const vec3 auroraBright = vec3(0., 0.5, .0);
|
||||
|
||||
//const vec3 auroraDark = vec3(0.);
|
||||
//const vec3 auroraBright = vec3(0.);
|
||||
|
||||
return vec4(
|
||||
mix(auroraDark, auroraBright, cloudsKey.b) * cloudsKey.b * auroraFactor +
|
||||
cloudsKey.r * cloudColor * (darkColor * max(0., 1. - cloudsKey.g) + dayLight * sunTint * cloudsKey.g),
|
||||
cloudsKey.r);
|
||||
}
|
||||
|
||||
vec4 getClouds(vec2 uv) {
|
||||
#if (VOLUMETRICS_UNDERSAMPLING <= 1)
|
||||
return sampleClouds(uv);
|
||||
#else
|
||||
return
|
||||
sampleClouds(uv - texelSize0 * vec2(-1.0, -1.0)) / 9.0 +
|
||||
sampleClouds(uv - texelSize0 * vec2( 0.0, -1.0)) / 9.0 +
|
||||
sampleClouds(uv - texelSize0 * vec2( 1.0, -1.0)) / 9.0 +
|
||||
sampleClouds(uv - texelSize0 * vec2(-1.0, 0.0)) / 9.0 +
|
||||
sampleClouds(uv - texelSize0 * vec2( 0.0, 0.0)) / 9.0 +
|
||||
sampleClouds(uv - texelSize0 * vec2( 1.0, 0.0)) / 9.0 +
|
||||
sampleClouds(uv - texelSize0 * vec2(-1.0, 1.0)) / 9.0 +
|
||||
sampleClouds(uv - texelSize0 * vec2( 0.0, 1.0)) / 9.0 +
|
||||
sampleClouds(uv - texelSize0 * vec2( 1.0, 1.0)) / 9.0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void main(void)
|
||||
{
|
||||
vec3 viewVec = normalize(relativePosition);
|
||||
|
||||
vec3 position = cameraOffset + cameraPosition;
|
||||
|
||||
float depth = getDepth(screenspaceCoordinate) / normalize(viewDirection).z;
|
||||
float bottomPlaneIntersect = max((cloudHeight - cameraPosition.y) / viewVec.y, 0.);
|
||||
float topPlaneIntersect = max((cloudHeight + cloudThickness - cameraPosition.y) / viewVec.y, 0.);
|
||||
float minPlane = min(bottomPlaneIntersect, topPlaneIntersect);
|
||||
|
||||
vec4 sceneColor = texture2D(sceneTexture, screenspaceCoordinate * 0.5 + 0.5);
|
||||
|
||||
if (depth > minPlane) {
|
||||
vec4 finalColor = getClouds(screenspaceCoordinate * 0.5 + 0.5);
|
||||
|
||||
gl_FragColor = vec4(sceneColor.rgb * (1. - finalColor.a) + finalColor.rgb, 1.);
|
||||
} else {
|
||||
gl_FragColor = sceneColor;
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
uniform mat4 mCameraProjInv;
|
||||
uniform mat4 mCameraView;
|
||||
uniform vec3 v_LightDirection;
|
||||
uniform float f_timeofday;
|
||||
|
||||
varying vec3 relativePosition;
|
||||
varying vec3 viewDirection;
|
||||
varying vec2 screenspaceCoordinate;
|
||||
varying vec3 sunTint;
|
||||
varying float auroraFactor;
|
||||
|
||||
vec3 getDirectLightScatteringAtGround(vec3 v_LightDirection)
|
||||
{
|
||||
// Based on talk at 2002 Game Developers Conference by Naty Hoffman and Arcot J. Preetham
|
||||
const float beta_r0 = 1e-5; // Rayleigh scattering beta
|
||||
|
||||
// These factors are calculated based on expected value of scattering factor of 1e-5
|
||||
// for Nitrogen at 532nm (green), 2e25 molecules/m3 in atmosphere
|
||||
const vec3 beta_r0_l = vec3(3.3362176e-01, 8.75378289198826e-01, 1.95342379700656) * beta_r0; // wavelength-dependent scattering
|
||||
|
||||
const float atmosphere_height = 15000.; // height of the atmosphere in meters
|
||||
// sun/moon light at the ground level, after going through the atmosphere
|
||||
return exp(-beta_r0_l * atmosphere_height / (1e-5 - dot(v_LightDirection, vec3(0., 1., 0.))));
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
void main(void)
|
||||
{
|
||||
vec4 p = mCameraProjInv * inVertexPosition;
|
||||
viewDirection = p.xyz / p.w;
|
||||
relativePosition = (p.xyz / p.w) * mat3(mCameraView);
|
||||
screenspaceCoordinate = inVertexPosition.xy;
|
||||
|
||||
auroraFactor = 1. - mtsmoothstep(0.13, 0.15, f_timeofday) * mtsmoothstep(0.87, 0.85, f_timeofday);
|
||||
|
||||
float tintFactor = mtsmoothstep(0.21, 0.24, f_timeofday) * mtsmoothstep(0.793, 0.753, f_timeofday);
|
||||
|
||||
sunTint = mix(vec3(1.0), getDirectLightScatteringAtGround(v_LightDirection), tintFactor);
|
||||
|
||||
gl_Position = inVertexPosition;
|
||||
}
|
|
@ -59,7 +59,7 @@ varying highp vec3 eyeVec;
|
|||
varying float nightRatio;
|
||||
|
||||
#ifdef ENABLE_DYNAMIC_SHADOWS
|
||||
#if ((defined(MATERIAL_WAVING_LIQUID) && defined(ENABLE_WATER_REFLECTIONS) && ENABLE_WAVING_WATER) || defined(ENABLE_BUMPMAPS))
|
||||
#if ((defined(MATERIAL_WAVING_LIQUID) && defined(ENABLE_WATER_REFLECTIONS)) || defined(ENABLE_BUMPMAPS))
|
||||
vec4 perm(vec4 x)
|
||||
{
|
||||
return mod(((x * 34.0) + 1.0) * x, 289.0);
|
||||
|
@ -450,7 +450,7 @@ void main(void)
|
|||
float fx0y0 = texture2D(baseTexture, uv).r;
|
||||
float fx1y0 = texture2D(baseTexture, uv + vec2(dr.x, 0.0)).r;
|
||||
float fx0y1 = texture2D(baseTexture, uv + vec2(0.0, dr.y)).r;
|
||||
vec2 gradient = 0.2 * vec2((fx1y0 - fx0y0) / dr.x, (fx0y1 - fx0y0) / dr.y) + 0.1 * gnoise(vec3(2.0 * uv / texelSize0, 0.0)).xy;
|
||||
vec2 gradient = 0.1 * vec2((fx1y0 - fx0y0) / dr.x, (fx0y1 - fx0y0) / dr.y) + 0.05 * gnoise(vec3(2.0 * uv / texelSize0, 0.0)).xy;
|
||||
// Compute a set of orthogonal basis vectors representing the node's surface plane.
|
||||
vec3 orth1 = normalize(cross(vNormal, mix(vec3(0.0, -1.0, 0.0), vec3(0.0, 0.0, -1.0), step(0.9, abs(vNormal.y)))));
|
||||
vec3 orth2 = normalize(cross(vNormal, orth1));
|
||||
|
@ -529,7 +529,14 @@ void main(void)
|
|||
vec3 viewVec = normalize(worldPosition + cameraOffset - cameraPosition);
|
||||
|
||||
// Water reflections
|
||||
#if (defined(MATERIAL_WAVING_LIQUID) && defined(ENABLE_WATER_REFLECTIONS) && ENABLE_WAVING_WATER)
|
||||
#if (defined(MATERIAL_WAVING_LIQUID) && defined(ENABLE_WATER_REFLECTIONS))
|
||||
|
||||
#if !ENABLE_WAVING_WATER
|
||||
#define WATER_WAVE_HEIGHT 0.5
|
||||
#define WATER_WAVE_SPEED 5.0
|
||||
#define WATER_WAVE_LENGTH 10.0
|
||||
#endif
|
||||
|
||||
vec3 wavePos = worldPosition * vec3(2.0, 0.0, 2.0);
|
||||
float off = animationTimer * WATER_WAVE_SPEED * 10.0;
|
||||
wavePos.x /= WATER_WAVE_LENGTH * 3.0;
|
||||
|
@ -560,7 +567,8 @@ void main(void)
|
|||
#if (defined(ENABLE_NODE_SPECULAR) && !defined(MATERIAL_WAVING_LIQUID))
|
||||
// Apply specular to blocks.
|
||||
if (dot(v_LightDirection, vNormal) < 0.0) {
|
||||
float intensity = 2.0 * (1.0 - (base.r * varColor.r));
|
||||
// This intensity is a placeholder and should be replaced by proper specular maps.
|
||||
float intensity = 4.0 * min(1.0, length(varColor.rgb * base.rgb));
|
||||
const float specular_exponent = 5.0;
|
||||
const float fresnel_exponent = 4.0;
|
||||
|
||||
|
@ -588,6 +596,8 @@ void main(void)
|
|||
// Note: clarity = (1 - fogginess)
|
||||
float clarity = clamp(fogShadingParameter
|
||||
- fogShadingParameter * length(eyeVec) / fogDistance, 0.0, 1.0);
|
||||
|
||||
#ifdef ENABLE_TINTED_FOG
|
||||
float fogColorMax = max(max(fogColor.r, fogColor.g), fogColor.b);
|
||||
// Prevent zero division.
|
||||
if (fogColorMax < 0.0000001) fogColorMax = 1.0;
|
||||
|
@ -595,6 +605,10 @@ void main(void)
|
|||
// For this to not make the fog color artificially dark we need to normalize using the
|
||||
// fog color's brightest value. We then blend our base color with this to make the fog.
|
||||
col = mix(fogColor * pow(fogColor / fogColorMax, vec4(2.0 * clarity)), col, clarity);
|
||||
#else
|
||||
col = mix(fogColor, col, clarity);
|
||||
#endif
|
||||
|
||||
col = vec4(col.rgb, base.a);
|
||||
|
||||
gl_FragData[0] = col;
|
||||
|
|
|
@ -21,7 +21,6 @@ uniform float animationTimer;
|
|||
uniform vec4 CameraPos;
|
||||
uniform float xyPerspectiveBias0;
|
||||
uniform float xyPerspectiveBias1;
|
||||
uniform vec3 shadow_tint;
|
||||
|
||||
varying float adj_shadow_strength;
|
||||
varying float cosLight;
|
||||
|
@ -433,8 +432,8 @@ void main(void)
|
|||
// calculate fragment color from components:
|
||||
col.rgb =
|
||||
adjusted_night_ratio * col.rgb + // artificial light
|
||||
sunTint * (1.0 - adjusted_night_ratio) * ( // natural light
|
||||
col.rgb * (1.0 - shadow_int * (1.0 - shadow_color) * (1.0 - shadow_tint)) + // filtered texture color
|
||||
(1.0 - adjusted_night_ratio) * ( // natural light
|
||||
col.rgb * (1.0 - shadow_int * (1.0 - shadow_color)) + // filtered texture color
|
||||
dayLight * shadow_color * shadow_int); // reflected filtered sunlight/moonlight
|
||||
}
|
||||
#endif
|
||||
|
@ -450,13 +449,9 @@ void main(void)
|
|||
// Note: clarity = (1 - fogginess)
|
||||
float clarity = clamp(fogShadingParameter
|
||||
- fogShadingParameter * length(eyeVec) / fogDistance, 0.0, 1.0);
|
||||
float fogColorMax = max(max(fogColor.r, fogColor.g), fogColor.b);
|
||||
// Prevent zero division.
|
||||
if (fogColorMax < 0.0000001) fogColorMax = 1.0;
|
||||
// For high clarity (light fog) we tint the fog color.
|
||||
// For this to not make the fog color artificially dark we need to normalize using the
|
||||
// fog color's brightest value. We then blend our base color with this to make the fog.
|
||||
col = mix(fogColor * pow(fogColor / fogColorMax, vec4(2.0 * clarity)), col, clarity);
|
||||
|
||||
col = mix(fogColor, col, clarity);
|
||||
|
||||
col = vec4(col.rgb, base.a);
|
||||
|
||||
gl_FragData[0] = col;
|
||||
|
|
22
client/shaders/soft_clouds/opengl_fragment.glsl
Normal file
22
client/shaders/soft_clouds/opengl_fragment.glsl
Normal file
|
@ -0,0 +1,22 @@
|
|||
uniform lowp vec4 fogColor;
|
||||
uniform float fogDistance;
|
||||
uniform float fogShadingParameter;
|
||||
varying highp vec3 eyeVec;
|
||||
|
||||
varying lowp vec4 varColor;
|
||||
varying lowp vec3 normal;
|
||||
uniform vec3 v_LightDirection;
|
||||
|
||||
void main(void)
|
||||
{
|
||||
float brightness = max(dot(-v_LightDirection, normal), 0.0);
|
||||
vec4 col = varColor;
|
||||
|
||||
col.rgb *= brightness;
|
||||
|
||||
float clarity = clamp(fogShadingParameter
|
||||
- fogShadingParameter * length(eyeVec) / fogDistance, 0.0, 1.0);
|
||||
col.rgb = mix(fogColor.rgb, col.rgb, clarity);
|
||||
|
||||
gl_FragColor = col;
|
||||
}
|
24
client/shaders/soft_clouds/opengl_vertex.glsl
Normal file
24
client/shaders/soft_clouds/opengl_vertex.glsl
Normal file
|
@ -0,0 +1,24 @@
|
|||
uniform lowp vec4 materialColor;
|
||||
|
||||
varying lowp vec4 varColor;
|
||||
|
||||
varying highp vec3 eyeVec;
|
||||
|
||||
varying lowp vec3 normal;
|
||||
|
||||
void main(void)
|
||||
{
|
||||
gl_Position = mWorldViewProj * inVertexPosition;
|
||||
|
||||
#ifdef GL_ES
|
||||
vec4 color = inVertexColor.bgra;
|
||||
#else
|
||||
vec4 color = inVertexColor;
|
||||
#endif
|
||||
|
||||
color *= materialColor;
|
||||
varColor = color;
|
||||
normal = inVertexNormal;
|
||||
|
||||
eyeVec = -(mWorldView * inVertexPosition).xyz;
|
||||
}
|
|
@ -1,214 +0,0 @@
|
|||
#define depthmap texture0
|
||||
#define noiseTexture texture1
|
||||
#define noiseTextureCoarse texture2
|
||||
|
||||
#define PROBING_ITERATIONS 30
|
||||
#define ITERATIONS 50
|
||||
#define LIGHT_ITERATIONS 3
|
||||
#define AURORA_ITERATIONS 80
|
||||
|
||||
// See clouds.cpp
|
||||
#define CLOUD_SIZE 640.0
|
||||
|
||||
uniform sampler2D depthmap;
|
||||
uniform sampler2D noiseTexture;
|
||||
uniform sampler2D noiseTextureCoarse;
|
||||
|
||||
uniform vec2 texelSize0;
|
||||
|
||||
uniform float cloudHeight;
|
||||
uniform float cloudThickness;
|
||||
uniform float cloudDensity;
|
||||
|
||||
varying vec3 relativePosition;
|
||||
varying vec3 viewDirection;
|
||||
uniform vec3 cameraOffset;
|
||||
uniform vec3 cameraPosition;
|
||||
|
||||
uniform float cameraNear;
|
||||
uniform float cameraFar;
|
||||
|
||||
uniform vec2 cloudOffset;
|
||||
uniform float cloudRadius;
|
||||
|
||||
varying vec2 screenspaceCoordinate;
|
||||
varying float sunStrength;
|
||||
|
||||
uniform float fogDistance;
|
||||
uniform float fogShadingParameter;
|
||||
|
||||
uniform vec3 v_LightDirection;
|
||||
|
||||
uniform float animationTimer;
|
||||
|
||||
// Derived From http://alex.vlachos.com/graphics/Alex_Vlachos_Advanced_VR_Rendering_GDC2015.pdf
|
||||
// and https://www.shadertoy.com/view/MslGR8 (5th one starting from the bottom)
|
||||
// NOTE: `frag_coord` is in pixels (i.e. not normalized UV).
|
||||
float screenSpaceDither(highp vec2 frag_coord)
|
||||
{
|
||||
// Iestyn's RGB dither (7 asm instructions) from Portal 2 X360, slightly modified for VR.
|
||||
highp float dither = dot(vec2(171.0, 231.0), frag_coord);
|
||||
dither = fract(dither / 103.0);
|
||||
|
||||
return dither;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
float toLinearDepth(float depth)
|
||||
{
|
||||
return cameraNear * cameraFar / (cameraFar + depth * (cameraNear - cameraFar));
|
||||
}
|
||||
|
||||
float getDepth(vec2 screenspacePosition)
|
||||
{
|
||||
return texture2D(depthmap, screenspacePosition * 0.5 + 0.5).r;
|
||||
}
|
||||
|
||||
float noise(vec3 p)
|
||||
{
|
||||
float y = floor(p.y);
|
||||
float f1 = texture2D(noiseTexture, p.xz / 256. + y * 0.2).r;
|
||||
float f2 = texture2D(noiseTexture, p.xz / 256. + y * 0.2 + 0.2).r;
|
||||
return mix(f1, f2, fract(p.y));
|
||||
}
|
||||
|
||||
float fnoise(vec3 p)
|
||||
{
|
||||
return noise(p * 4.) * 0.5 + noise(p * 8.) * 0.25;
|
||||
}
|
||||
|
||||
float fnoise3(vec3 p)
|
||||
{
|
||||
return noise(p * 4.) * 0.5 + noise(p * 8.) * 0.25 + noise(p * 16.) * 0.125;
|
||||
}
|
||||
|
||||
float getAuroraDensity(vec3 position)
|
||||
{
|
||||
float density = pow(max(0., 1. - 10. * abs(fnoise3(vec3(position.x * 0.25, animationTimer, position.z * 0.25)) - 0.5)), 4.);
|
||||
return 0.7 * density * mtsmoothstep(0.0, 0.05, position.y - 1.) * pow(1. - mtsmoothstep(0.05, 2.0, position.y - 1.), 4.);
|
||||
}
|
||||
|
||||
float getCoarse(vec3 position) {
|
||||
return texture2D(noiseTextureCoarse, (position.xz - cloudOffset) * 0.5 / CLOUD_SIZE / cloudRadius).r;
|
||||
}
|
||||
|
||||
float getDensity(vec3 position)
|
||||
{
|
||||
float density = texture2D(noiseTextureCoarse, (position.xz - cloudOffset) * 0.5 / CLOUD_SIZE / cloudRadius).r *
|
||||
mtsmoothstep(0.0, cloudThickness * 0.2, position.y - cloudHeight) *
|
||||
(1.0 - mtsmoothstep(cloudThickness * 0.5, cloudThickness, position.y - cloudHeight));
|
||||
|
||||
density = max(0., density - 0.5 * fnoise(position * 0.005));
|
||||
|
||||
return 0.04 * density;
|
||||
}
|
||||
|
||||
float getBrightness(vec3 position, float lightDistance)
|
||||
{
|
||||
float density = 0.;
|
||||
for (int i = 1; i <= LIGHT_ITERATIONS; i++) {
|
||||
vec3 rayPosition = position - v_LightDirection * lightDistance * float(i) / float(LIGHT_ITERATIONS);
|
||||
|
||||
density += getDensity(rayPosition) * float(lightDistance) / float(LIGHT_ITERATIONS);
|
||||
}
|
||||
return exp(-density);
|
||||
}
|
||||
|
||||
float blend(float A, float B, float alphaA, float alphaB)
|
||||
{
|
||||
float alphaC = alphaA + (1. - alphaA) * alphaB;
|
||||
return (alphaA * A + (1. - alphaA) * alphaB * B) / alphaC;
|
||||
}
|
||||
|
||||
void main(void)
|
||||
{
|
||||
vec3 viewVec = normalize(relativePosition);
|
||||
|
||||
vec3 position = cameraOffset + cameraPosition;
|
||||
|
||||
float depth = toLinearDepth(getDepth(screenspaceCoordinate)) / normalize(viewDirection).z;
|
||||
float bottomPlaneIntersect = clamp((cloudHeight - cameraPosition.y) / viewVec.y, 0., 4. * fogDistance);
|
||||
float topPlaneIntersect = clamp((cloudHeight + cloudThickness - cameraPosition.y) / viewVec.y, 0., 4. * fogDistance);
|
||||
|
||||
#if (VOLUMETRICS_UNDERSAMPLING <= 1)
|
||||
bottomPlaneIntersect = min(depth, bottomPlaneIntersect);
|
||||
topPlaneIntersect = min(depth, topPlaneIntersect);
|
||||
#else
|
||||
if ((bottomPlaneIntersect > depth + 5.0) != (topPlaneIntersect > depth + 5.0)) {
|
||||
bottomPlaneIntersect = min(depth, bottomPlaneIntersect);
|
||||
topPlaneIntersect = min(depth, topPlaneIntersect);
|
||||
}
|
||||
#endif
|
||||
|
||||
float startDepth = min(bottomPlaneIntersect, topPlaneIntersect);
|
||||
float endDepth = max(bottomPlaneIntersect, topPlaneIntersect);
|
||||
|
||||
float bias = screenSpaceDither(gl_FragCoord.xy + animationTimer * 2400.0);
|
||||
|
||||
vec3 color = vec3(0.);
|
||||
|
||||
float density = 0.;
|
||||
|
||||
float auroraStartDepth = min(max(0., 1.0 / viewVec.y), 8.);
|
||||
float auroraEndDepth = min(max(0., 3.0 / viewVec.y), 8.);
|
||||
float rawDepth = getDepth(screenspaceCoordinate);
|
||||
|
||||
if (auroraEndDepth - auroraStartDepth > 0.1 && rawDepth >= 1.0) {
|
||||
for (int i = 0; i < AURORA_ITERATIONS; i++) {
|
||||
vec3 rayPosition = viewVec * (auroraStartDepth + (auroraEndDepth - auroraStartDepth) * (float(i) + bias) / float(AURORA_ITERATIONS));
|
||||
|
||||
float localDensity = getAuroraDensity(rayPosition);
|
||||
|
||||
localDensity *= 1.0 - mtsmoothstep(4.0, 8.0, length(rayPosition));
|
||||
|
||||
density += localDensity;
|
||||
}
|
||||
}
|
||||
|
||||
color.b = density * (auroraEndDepth - auroraStartDepth) / float(AURORA_ITERATIONS);
|
||||
|
||||
float sunlightContribution = 0.;
|
||||
float alpha = 0.;
|
||||
float outScatter = 2. * (dot(v_LightDirection, viewVec) * 0.5 + 0.5);
|
||||
float forwardScatter = 1. + 2. * pow(min(dot(v_LightDirection, viewVec), 0.), 4.);
|
||||
density = 0.;
|
||||
|
||||
float fogDepth = min(4. * fogDistance, startDepth + 2000.);
|
||||
endDepth = min(endDepth, fogDepth);
|
||||
|
||||
float dx = (endDepth - startDepth) / float(ITERATIONS);
|
||||
float lightDistance = cloudThickness * 0.5;
|
||||
|
||||
if (endDepth - startDepth > 0.1) {
|
||||
for (int i = 0; i < ITERATIONS; i++) {
|
||||
vec3 rayPosition = cameraPosition + viewVec * (startDepth + (endDepth - startDepth) * (float(i) + bias) / float(ITERATIONS));
|
||||
|
||||
float localDensity = getDensity(rayPosition) * dx;
|
||||
|
||||
if (localDensity < 0.0001) continue;
|
||||
|
||||
float clarity = clamp(fogShadingParameter - fogShadingParameter * length(rayPosition - cameraPosition) / (fogDepth), 0.0, 1.0);
|
||||
float outScatterContribution = exp(-0.5 * outScatter * localDensity);
|
||||
float brightness = getBrightness(rayPosition, lightDistance) * forwardScatter * outScatterContribution * sunStrength + (1. - outScatterContribution);
|
||||
sunlightContribution = blend(sunlightContribution, brightness, 1. - exp(-density), 1. - exp(-localDensity));
|
||||
alpha = blend(alpha, clarity, 1. - exp(-density), 1. - exp(-localDensity));
|
||||
|
||||
density += localDensity;
|
||||
|
||||
if (density > 10.0) break;
|
||||
}
|
||||
}
|
||||
|
||||
color.r = (1. - exp(-density)) * alpha;
|
||||
color.g = sunlightContribution;
|
||||
color.b *= exp(-density);
|
||||
|
||||
gl_FragColor = vec4(color, 1.0);
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
uniform mat4 mCameraProjInv;
|
||||
uniform mat4 mCameraView;
|
||||
uniform float f_timeofday;
|
||||
|
||||
varying vec3 relativePosition;
|
||||
varying vec3 viewDirection;
|
||||
|
||||
varying vec2 screenspaceCoordinate;
|
||||
varying float sunStrength;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void main(void)
|
||||
{
|
||||
screenspaceCoordinate = inVertexPosition.xy;
|
||||
vec4 p = mCameraProjInv * inVertexPosition;
|
||||
viewDirection = p.xyz / p.w;
|
||||
relativePosition = (p.xyz / p.w) * mat3(mCameraView);
|
||||
|
||||
if (f_timeofday < 0.21) {
|
||||
sunStrength =
|
||||
(1.0 - mtsmoothstep(0.18, 0.21, f_timeofday));
|
||||
} else if (f_timeofday >= 0.793) {
|
||||
sunStrength =
|
||||
mtsmoothstep(0.793, 0.823, f_timeofday);
|
||||
} else {
|
||||
sunStrength =
|
||||
mtsmoothstep(0.21, 0.26, f_timeofday) *
|
||||
(1.0 - mtsmoothstep(0.743, 0.793, f_timeofday));
|
||||
}
|
||||
|
||||
gl_Position = inVertexPosition;
|
||||
}
|
|
@ -35,7 +35,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
#include "gameparams.h"
|
||||
#include "script/common/c_types.h" // LuaError
|
||||
#include "util/numeric.h"
|
||||
#include "clouds.h"
|
||||
|
||||
#ifdef SERVER
|
||||
#error Do not include in server builds
|
||||
|
@ -373,9 +372,6 @@ public:
|
|||
Camera* getCamera () { return m_camera; }
|
||||
scene::ISceneManager *getSceneManager();
|
||||
|
||||
Clouds* getClouds() { return m_clouds; }
|
||||
void setClouds(Clouds* clouds) { m_clouds = clouds; }
|
||||
|
||||
// IGameDef interface
|
||||
IItemDefManager* getItemDefManager() override;
|
||||
const NodeDefManager* getNodeDefManager() override;
|
||||
|
@ -501,7 +497,6 @@ private:
|
|||
ELoginRegister m_allow_login_or_register = ELoginRegister::Any;
|
||||
Camera *m_camera = nullptr;
|
||||
Minimap *m_minimap = nullptr;
|
||||
Clouds *m_clouds = nullptr;
|
||||
|
||||
// Server serialization version
|
||||
u8 m_server_ser_ver;
|
||||
|
|
|
@ -28,7 +28,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
#include "settings.h"
|
||||
#include <cmath>
|
||||
|
||||
class Clouds;
|
||||
scene::ISceneManager *g_menucloudsmgr = nullptr;
|
||||
Clouds *g_menuclouds = nullptr;
|
||||
|
||||
|
@ -44,7 +43,7 @@ Clouds::Clouds(scene::ISceneManager* mgr, IShaderSource *ssrc,
|
|||
s32 id,
|
||||
u32 seed
|
||||
):
|
||||
scene::ISceneNode(g_settings->getBool("enable_volumetric_clouds") ? nullptr : mgr->getRootSceneNode(), mgr, id),
|
||||
scene::ISceneNode(mgr->getRootSceneNode(), mgr, id),
|
||||
m_seed(seed)
|
||||
{
|
||||
assert(ssrc);
|
||||
|
@ -54,7 +53,8 @@ Clouds::Clouds(scene::ISceneManager* mgr, IShaderSource *ssrc,
|
|||
m_material.FogEnable = true;
|
||||
m_material.AntiAliasing = video::EAAM_SIMPLE;
|
||||
if (m_enable_shaders) {
|
||||
auto sid = ssrc->getShader("cloud_shader", TILE_MATERIAL_ALPHA);
|
||||
bool shaded_clouds = g_settings->getBool("soft_clouds") && g_settings->getBool("enable_dynamic_shadows");
|
||||
auto sid = ssrc->getShader(shaded_clouds ? "soft_clouds" : "cloud_shader", TILE_MATERIAL_ALPHA);
|
||||
m_material.MaterialType = ssrc->getShaderInfo(sid).material;
|
||||
} else {
|
||||
m_material.MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL;
|
||||
|
@ -107,14 +107,6 @@ void Clouds::updateMesh()
|
|||
std::floor(center_of_drawing_in_noise_f.Y / cloud_size)
|
||||
);
|
||||
|
||||
// The world position of the integer center point of drawing in the noise
|
||||
v2f world_center_of_drawing_in_noise_f = v2f(
|
||||
center_of_drawing_in_noise_i.X * cloud_size,
|
||||
center_of_drawing_in_noise_i.Y * cloud_size
|
||||
) + m_origin;
|
||||
|
||||
m_noise_position = world_center_of_drawing_in_noise_f - ((float)m_cloud_radius_i + 0.5f) * cloud_size;
|
||||
|
||||
// Only update mesh if it has moved enough, this saves lots of GPU buffer uploads.
|
||||
constexpr float max_d = 5 * BS;
|
||||
|
||||
|
@ -137,6 +129,12 @@ void Clouds::updateMesh()
|
|||
|
||||
const u32 num_faces_to_draw = is3D() ? 6 : 1;
|
||||
|
||||
// The world position of the integer center point of drawing in the noise
|
||||
v2f world_center_of_drawing_in_noise_f = v2f(
|
||||
center_of_drawing_in_noise_i.X * cloud_size,
|
||||
center_of_drawing_in_noise_i.Y * cloud_size
|
||||
) + m_origin;
|
||||
|
||||
// Colors with primitive shading
|
||||
|
||||
video::SColorf c_top_f(m_color);
|
||||
|
@ -166,7 +164,7 @@ void Clouds::updateMesh()
|
|||
|
||||
// Read noise
|
||||
|
||||
m_grid.resize(m_cloud_radius_i * 2 * m_cloud_radius_i * 2);
|
||||
std::vector<bool> grid(m_cloud_radius_i * 2 * m_cloud_radius_i * 2);
|
||||
|
||||
for(s16 zi = -m_cloud_radius_i; zi < m_cloud_radius_i; zi++) {
|
||||
u32 si = (zi + m_cloud_radius_i) * m_cloud_radius_i * 2 + m_cloud_radius_i;
|
||||
|
@ -174,7 +172,7 @@ void Clouds::updateMesh()
|
|||
for (s16 xi = -m_cloud_radius_i; xi < m_cloud_radius_i; xi++) {
|
||||
u32 i = si + xi;
|
||||
|
||||
m_grid[i] = gridFilled(
|
||||
grid[i] = gridFilled(
|
||||
xi + center_of_drawing_in_noise_i.X,
|
||||
zi + center_of_drawing_in_noise_i.Y
|
||||
);
|
||||
|
@ -213,24 +211,100 @@ void Clouds::updateMesh()
|
|||
|
||||
u32 i = GETINDEX(xi, zi, m_cloud_radius_i);
|
||||
|
||||
if (!m_grid[i])
|
||||
if (!grid[i])
|
||||
continue;
|
||||
|
||||
v2f p0 = v2f(xi,zi)*cloud_size + world_center_of_drawing_in_noise_f;
|
||||
|
||||
video::S3DVertex v[4] = {
|
||||
video::S3DVertex(0,0,0, 0,0,0, c_top, 0, 1),
|
||||
video::S3DVertex(0,0,0, 0,0,0, c_top, 1, 1),
|
||||
video::S3DVertex(0,0,0, 0,0,0, c_top, 1, 0),
|
||||
video::S3DVertex(0,0,0, 0,0,0, c_top, 0, 0)
|
||||
};
|
||||
|
||||
const f32 rx = cloud_size / 2.0f;
|
||||
// if clouds are flat, the top layer should be at the given height
|
||||
const f32 ry = is3D() ? m_params.thickness * BS : 0.0f;
|
||||
const f32 rz = cloud_size / 2;
|
||||
|
||||
bool soft_clouds_enabled = g_settings->getBool("soft_clouds");
|
||||
bool shaded_clouds_enabled = soft_clouds_enabled && g_settings->getBool("enable_dynamic_shadows") && g_settings->getBool("enable_3d_clouds");
|
||||
|
||||
v3f pos(p0.X, m_params.height * BS, p0.Y);
|
||||
|
||||
if (shaded_clouds_enabled) {
|
||||
bool neighbours[4] = {false, false, false, false};
|
||||
if (INAREA(xi - 1, zi, m_cloud_radius_i))
|
||||
neighbours[0] = grid[GETINDEX(xi - 1, zi, m_cloud_radius_i)];
|
||||
if (INAREA(xi, zi - 1, m_cloud_radius_i))
|
||||
neighbours[1] = grid[GETINDEX(xi, zi - 1, m_cloud_radius_i)];
|
||||
if (INAREA(xi + 1, zi, m_cloud_radius_i))
|
||||
neighbours[2] = grid[GETINDEX(xi + 1, zi, m_cloud_radius_i)];
|
||||
if (INAREA(xi, zi + 1, m_cloud_radius_i))
|
||||
neighbours[3] = grid[GETINDEX(xi, zi + 1, m_cloud_radius_i)];
|
||||
|
||||
video::S3DVertex v[32];
|
||||
v[ 0].Pos = core::vector3df( -rx, ry, -rz);
|
||||
v[ 1].Pos = core::vector3df(-0.75 * rx, ry, -rz);
|
||||
v[ 2].Pos = core::vector3df( 0.75 * rx, ry, -rz);
|
||||
v[ 3].Pos = core::vector3df( rx, ry, -rz);
|
||||
v[ 4].Pos = core::vector3df( -rx, ry, -0.75 * rz);
|
||||
v[ 5].Pos = core::vector3df(-0.75 * rx, ry, -0.75 * rz);
|
||||
v[ 6].Pos = core::vector3df( 0.75 * rx, ry, -0.75 * rz);
|
||||
v[ 7].Pos = core::vector3df( rx, ry, -0.75 * rz);
|
||||
v[ 8].Pos = core::vector3df( -rx, ry, 0.75 * rz);
|
||||
v[ 9].Pos = core::vector3df(-0.75 * rx, ry, 0.75 * rz);
|
||||
v[10].Pos = core::vector3df( 0.75 * rx, ry, 0.75 * rz);
|
||||
v[11].Pos = core::vector3df( rx, ry, 0.75 * rz);
|
||||
v[12].Pos = core::vector3df( -rx, ry, rz);
|
||||
v[13].Pos = core::vector3df(-0.75 * rx, ry, rz);
|
||||
v[14].Pos = core::vector3df( 0.75 * rx, ry, rz);
|
||||
v[15].Pos = core::vector3df( rx, ry, rz);
|
||||
|
||||
// These normals are intentionally left unnormalized
|
||||
if (neighbours[0]) {
|
||||
v[ 0].Normal.X -= 1.0;
|
||||
v[ 4].Normal.X -= 1.0;
|
||||
v[ 8].Normal.X -= 1.0;
|
||||
v[12].Normal.X -= 1.0;
|
||||
}
|
||||
if (neighbours[1]) {
|
||||
v[0].Normal.Z -= 1.0;
|
||||
v[1].Normal.Z -= 1.0;
|
||||
v[2].Normal.Z -= 1.0;
|
||||
v[3].Normal.Z -= 1.0;
|
||||
}
|
||||
if (neighbours[2]) {
|
||||
v[ 3].Normal.X += 1.0;
|
||||
v[ 7].Normal.X += 1.0;
|
||||
v[11].Normal.X += 1.0;
|
||||
v[15].Normal.X += 1.0;
|
||||
}
|
||||
if (neighbours[3]) {
|
||||
v[12].Normal.Z += 1.0;
|
||||
v[13].Normal.Z += 1.0;
|
||||
v[14].Normal.Z += 1.0;
|
||||
v[15].Normal.Z += 1.0;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
v[i + 32] = v[i];
|
||||
v[i + 32].Pos.Y = -ry;
|
||||
|
||||
v[i].Normal = core::vector3df(0.0, 1.0, 0.0);
|
||||
v[i + 32].Normal = core::vector3df(0.0, -1.0, 0.0);
|
||||
}
|
||||
|
||||
for (video::S3DVertex& vertex : v) {
|
||||
vertex.Color = shadow.toSColor();
|
||||
vertex.Pos += pos;
|
||||
}
|
||||
|
||||
//top
|
||||
|
||||
}
|
||||
else {
|
||||
video::S3DVertex v[4] = {
|
||||
video::S3DVertex(0,0,0, 0,0,0, c_top, 0, 1),
|
||||
video::S3DVertex(0,0,0, 0,0,0, c_top, 1, 1),
|
||||
video::S3DVertex(0,0,0, 0,0,0, c_top, 1, 0),
|
||||
video::S3DVertex(0,0,0, 0,0,0, c_top, 0, 0)
|
||||
};
|
||||
|
||||
for (u32 i = 0; i < num_faces_to_draw; i++)
|
||||
{
|
||||
switch (i)
|
||||
|
@ -247,7 +321,7 @@ void Clouds::updateMesh()
|
|||
case 1: // back
|
||||
if (INAREA(xi, zi - 1, m_cloud_radius_i)) {
|
||||
u32 j = GETINDEX(xi, zi - 1, m_cloud_radius_i);
|
||||
if (m_grid[j])
|
||||
if (grid[j])
|
||||
continue;
|
||||
}
|
||||
if (soft_clouds_enabled) {
|
||||
|
@ -256,7 +330,8 @@ void Clouds::updateMesh()
|
|||
}
|
||||
v[2].Color = c_bottom;
|
||||
v[3].Color = c_bottom;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
for (video::S3DVertex& vertex : v) {
|
||||
vertex.Color = c_side_1;
|
||||
vertex.Normal.set(0, 0, -1);
|
||||
|
@ -270,7 +345,7 @@ void Clouds::updateMesh()
|
|||
case 2: //right
|
||||
if (INAREA(xi + 1, zi, m_cloud_radius_i)) {
|
||||
u32 j = GETINDEX(xi + 1, zi, m_cloud_radius_i);
|
||||
if (m_grid[j])
|
||||
if (grid[j])
|
||||
continue;
|
||||
}
|
||||
if (soft_clouds_enabled) {
|
||||
|
@ -294,7 +369,7 @@ void Clouds::updateMesh()
|
|||
case 3: // front
|
||||
if (INAREA(xi, zi + 1, m_cloud_radius_i)) {
|
||||
u32 j = GETINDEX(xi, zi + 1, m_cloud_radius_i);
|
||||
if (m_grid[j])
|
||||
if (grid[j])
|
||||
continue;
|
||||
}
|
||||
if (soft_clouds_enabled) {
|
||||
|
@ -303,7 +378,8 @@ void Clouds::updateMesh()
|
|||
}
|
||||
v[2].Color = c_bottom;
|
||||
v[3].Color = c_bottom;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
for (video::S3DVertex& vertex : v) {
|
||||
vertex.Color = c_side_1;
|
||||
vertex.Normal.set(0, 0, -1);
|
||||
|
@ -317,7 +393,7 @@ void Clouds::updateMesh()
|
|||
case 4: // left
|
||||
if (INAREA(xi - 1, zi, m_cloud_radius_i)) {
|
||||
u32 j = GETINDEX(xi - 1, zi, m_cloud_radius_i);
|
||||
if (m_grid[j])
|
||||
if (grid[j])
|
||||
continue;
|
||||
}
|
||||
if (soft_clouds_enabled) {
|
||||
|
@ -326,7 +402,8 @@ void Clouds::updateMesh()
|
|||
}
|
||||
v[2].Color = c_bottom;
|
||||
v[3].Color = c_bottom;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
for (video::S3DVertex& vertex : v) {
|
||||
vertex.Color = c_side_2;
|
||||
vertex.Normal.set(-1, 0, 0);
|
||||
|
@ -349,14 +426,13 @@ void Clouds::updateMesh()
|
|||
break;
|
||||
}
|
||||
|
||||
v3f pos(p0.X, m_params.height * BS, p0.Y);
|
||||
|
||||
for (video::S3DVertex& vertex : v) {
|
||||
vertex.Pos += pos;
|
||||
vertices.push_back(vertex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mb->setDirty(scene::EBT_VERTEX);
|
||||
|
||||
const u32 quad_count = mb->getVertexCount() / 4;
|
||||
|
@ -430,40 +506,13 @@ void Clouds::render()
|
|||
cloud_full_radius*1.2, fog_density, fog_pixelfog, fog_rangefog);
|
||||
}
|
||||
|
||||
//#if 0
|
||||
driver->drawMeshBuffer(m_meshbuffer.get());
|
||||
//#endif
|
||||
|
||||
// Restore fog settings
|
||||
driver->setFog(fog_color, fog_type, fog_start, fog_end, fog_density,
|
||||
fog_pixelfog, fog_rangefog);
|
||||
}
|
||||
|
||||
void Clouds::renderDepth() {
|
||||
if (m_params.density <= 0.0f)
|
||||
return; // no need to do anything
|
||||
|
||||
video::IVideoDriver* driver = SceneManager->getVideoDriver();
|
||||
|
||||
updateMesh();
|
||||
|
||||
// Update position
|
||||
{
|
||||
v2f off_origin = m_origin - m_mesh_origin;
|
||||
v3f rel(off_origin.X, 0, off_origin.Y);
|
||||
rel -= intToFloat(m_camera_offset, BS);
|
||||
setPosition(rel);
|
||||
updateAbsolutePosition();
|
||||
}
|
||||
|
||||
video::SMaterial material = m_material;
|
||||
material.MaterialType = video::EMT_SOLID;
|
||||
material.ZWriteEnable = video::EZW_ON;
|
||||
|
||||
driver->setTransform(video::ETS_WORLD, AbsoluteTransformation);
|
||||
driver->setMaterial(material);
|
||||
driver->drawMeshBuffer(m_meshbuffer.get());
|
||||
}
|
||||
|
||||
void Clouds::step(float dtime)
|
||||
{
|
||||
m_origin = m_origin + dtime * BS * m_params.speed;
|
||||
|
|
|
@ -60,8 +60,6 @@ public:
|
|||
|
||||
virtual void render();
|
||||
|
||||
void renderDepth();
|
||||
|
||||
virtual const aabb3f &getBoundingBox() const
|
||||
{
|
||||
return m_box;
|
||||
|
@ -101,10 +99,6 @@ public:
|
|||
invalidateMesh();
|
||||
}
|
||||
|
||||
float getDensity() const {
|
||||
return m_params.density;
|
||||
}
|
||||
|
||||
void setColorBright(video::SColor color_bright)
|
||||
{
|
||||
m_params.color_bright = color_bright;
|
||||
|
@ -132,10 +126,6 @@ public:
|
|||
invalidateMesh();
|
||||
}
|
||||
|
||||
float getHeight() const {
|
||||
return m_params.height;
|
||||
}
|
||||
|
||||
void setSpeed(v2f speed)
|
||||
{
|
||||
m_params.speed = speed;
|
||||
|
@ -150,24 +140,10 @@ public:
|
|||
invalidateMesh();
|
||||
}
|
||||
|
||||
float getThickness() const {
|
||||
return m_params.thickness;
|
||||
}
|
||||
|
||||
bool isCameraInsideCloud() const { return m_camera_inside_cloud; }
|
||||
|
||||
const video::SColor getColor() const { return m_color.toSColor(); }
|
||||
|
||||
bool getGrid(int x, int y)
|
||||
{
|
||||
int index = x + y * m_cloud_radius_i * 2;
|
||||
if (index < 0 || index >= m_grid.size())
|
||||
return false;
|
||||
return m_grid[index];
|
||||
}
|
||||
|
||||
v2f getCloudOffset() const { return m_noise_position; }
|
||||
|
||||
private:
|
||||
void updateBox()
|
||||
{
|
||||
|
@ -204,7 +180,6 @@ private:
|
|||
u16 m_cloud_radius_i;
|
||||
u32 m_seed;
|
||||
v3f m_camera_pos;
|
||||
v2f m_noise_position;
|
||||
|
||||
v3s16 m_camera_offset;
|
||||
bool m_camera_inside_cloud = false;
|
||||
|
@ -212,7 +187,4 @@ private:
|
|||
bool m_enable_shaders, m_enable_3d;
|
||||
video::SColorf m_color = video::SColorf(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
CloudParams m_params;
|
||||
|
||||
std::vector<bool> m_grid;
|
||||
|
||||
};
|
||||
|
|
|
@ -435,8 +435,6 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter
|
|||
CachedPixelShaderSetting<float> m_moon_brightness_pixel{"moonBrightness"};
|
||||
CachedPixelShaderSetting<float>
|
||||
m_volumetric_light_strength_pixel{"volumetricLightStrength"};
|
||||
CachedPixelShaderSetting<float, 3>
|
||||
m_volumetric_cloud_color{"cloudColor"};
|
||||
|
||||
static constexpr std::array<const char*, 5> SETTING_CALLBACKS = {
|
||||
"exposure_compensation",
|
||||
|
@ -575,23 +573,6 @@ public:
|
|||
video::SColorf artificial_light = lighting.artificial_light_color;
|
||||
m_artificial_light.set(artificial_light, services);
|
||||
|
||||
// TODO: settings
|
||||
Clouds* clouds = m_client->getClouds();
|
||||
if (clouds && g_settings->getBool("enable_volumetric_clouds")) {
|
||||
float cloud_height = clouds->getHeight() * 10.0f;
|
||||
m_cloud_height_pixel.set(&cloud_height, services);
|
||||
float cloud_thickness = clouds->getThickness() * 10.0f;
|
||||
m_cloud_thickness_pixel.set(&cloud_thickness, services);
|
||||
float cloud_density = clouds->getDensity();
|
||||
m_cloud_density_pixel.set(&cloud_density, services);
|
||||
v2f cloud_offset = clouds->getCloudOffset();
|
||||
m_cloud_offset_pixel.set(cloud_offset, services);
|
||||
float cloud_radius = g_settings->getU16("cloud_radius");
|
||||
m_cloud_radius_pixel.set(&cloud_radius, services);
|
||||
video::SColor cloud_color = clouds->getColor();
|
||||
m_volumetric_cloud_color.set(cloud_color, services);
|
||||
}
|
||||
|
||||
if (m_volumetric_light_enabled) {
|
||||
// Map directional light to screen space
|
||||
auto camera_node = m_client->getCamera()->getCameraNode();
|
||||
|
@ -1588,7 +1569,6 @@ bool Game::createClient(const GameStartData &start_data)
|
|||
*/
|
||||
if (m_cache_enable_clouds)
|
||||
clouds = make_irr<Clouds>(smgr, shader_src, -1, rand());
|
||||
client->setClouds(clouds.get());
|
||||
|
||||
/* Skybox
|
||||
*/
|
||||
|
@ -4334,7 +4314,7 @@ void Game::updateClouds(float dtime)
|
|||
camera_node_position.Y = camera_node_position.Y + camera_offset.Y * BS;
|
||||
camera_node_position.Z = camera_node_position.Z + camera_offset.Z * BS;
|
||||
this->clouds->update(camera_node_position, this->sky->getCloudColor());
|
||||
if (this->clouds->isCameraInsideCloud() && this->fogEnabled() && !g_settings->getBool("enable_volumetric_clouds")) {
|
||||
if (this->clouds->isCameraInsideCloud() && this->fogEnabled()) {
|
||||
// If camera is inside cloud and fog is enabled, use cloud's colors as sky colors.
|
||||
video::SColor clouds_dark = this->clouds->getColor().getInterpolated(
|
||||
video::SColor(255, 0, 0, 0), 0.9);
|
||||
|
|
|
@ -113,19 +113,6 @@ void TextureBuffer::swapTextures(u8 texture_a, u8 texture_b)
|
|||
m_textures[texture_b] = temp;
|
||||
}
|
||||
|
||||
void TextureBuffer::setTextureImage(u8 id, video::IImage* image) {
|
||||
assert(m_definitions[id].valid);
|
||||
|
||||
auto &definition = m_definitions[id];
|
||||
|
||||
if (m_textures[id]) m_driver->removeTexture(m_textures[id]);
|
||||
|
||||
m_textures[id] = m_driver->addTexture(definition.name.c_str(), image);
|
||||
definition.fixed_size = true;
|
||||
definition.size = image->getDimension();
|
||||
definition.dirty = false;
|
||||
}
|
||||
|
||||
bool TextureBuffer::ensureTexture(video::ITexture **texture, const TextureDefinition& definition, PipelineContext &context)
|
||||
{
|
||||
bool modify;
|
||||
|
@ -195,8 +182,6 @@ void TextureBufferOutput::activate(PipelineContext &context)
|
|||
if (!render_target)
|
||||
render_target = driver->addRenderTarget();
|
||||
|
||||
if (disable_clear) m_clear = false;
|
||||
|
||||
core::array<video::ITexture *> textures;
|
||||
core::dimension2du size(0, 0);
|
||||
for (size_t i = 0; i < texture_map.size(); i++) {
|
||||
|
|
|
@ -190,7 +190,6 @@ public:
|
|||
TextureBufferOutput(TextureBuffer *buffer, const std::vector<u8> &texture_map, u8 depth_stencil);
|
||||
virtual ~TextureBufferOutput() override;
|
||||
void activate(PipelineContext &context) override;
|
||||
void disableClearing() { disable_clear = true; }
|
||||
private:
|
||||
static const u8 NO_DEPTH_TEXTURE = 255;
|
||||
|
||||
|
@ -199,7 +198,6 @@ private:
|
|||
u8 depth_stencil { NO_DEPTH_TEXTURE };
|
||||
video::IRenderTarget* render_target { nullptr };
|
||||
video::IVideoDriver* driver { nullptr };
|
||||
bool disable_clear = false;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -24,116 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
#include "client/shader.h"
|
||||
#include "client/tile.h"
|
||||
#include "settings.h"
|
||||
#include "noise.h"
|
||||
|
||||
// TODO: Probably this could still be improved, having a step for this seems silly
|
||||
class NoiseStep : public RenderStep {
|
||||
public:
|
||||
NoiseStep(TextureBuffer* buffer, u8 id, u32 size) :
|
||||
buffer(buffer), id(id), size(size)
|
||||
{
|
||||
}
|
||||
|
||||
void setRenderSource(RenderSource* _source) override {}
|
||||
|
||||
void setRenderTarget(RenderTarget* _target) override {}
|
||||
|
||||
void reset(PipelineContext& context) override {}
|
||||
|
||||
void run(PipelineContext& context) override
|
||||
{
|
||||
if (!needs_run) return;
|
||||
|
||||
needs_run = false;
|
||||
|
||||
video::IImage* noise_image = context.device->getVideoDriver()->createImage(video::ECF_A8R8G8B8, core::dimension2du(256, 256));
|
||||
PseudoRandom random;
|
||||
for (u32 i = 0; i < size * size; ++i) {
|
||||
noise_image->setPixel(i % size, i / size, video::SColor(0, random.next() % 256, 0, 0));
|
||||
}
|
||||
buffer->setTextureImage(id, noise_image);
|
||||
noise_image->drop();
|
||||
}
|
||||
|
||||
private:
|
||||
u32 size;
|
||||
u8 id;
|
||||
TextureBuffer* buffer = nullptr;
|
||||
bool needs_run = true;
|
||||
};
|
||||
|
||||
class CloudDensityStep : public RenderStep {
|
||||
public:
|
||||
CloudDensityStep(TextureBuffer* buffer, u8 id, Clouds* clouds) :
|
||||
buffer(buffer), id(id), clouds(clouds)
|
||||
{
|
||||
}
|
||||
|
||||
void setRenderSource(RenderSource* _source) override {}
|
||||
|
||||
void setRenderTarget(RenderTarget* _target) override {}
|
||||
|
||||
void reset(PipelineContext& context) override {}
|
||||
|
||||
void run(PipelineContext& context) override
|
||||
{
|
||||
u16 cloud_radius = g_settings->getU16("cloud_radius");
|
||||
if (cloud_radius < 1) cloud_radius = 1;
|
||||
|
||||
video::IImage* image = context.device->getVideoDriver()->createImage(video::ECF_A8R8G8B8, core::dimension2du(8 * cloud_radius, 8 * cloud_radius));
|
||||
|
||||
for (int x = 0; x < 2 * cloud_radius; ++x) {
|
||||
for (int y = 0; y < 2 * cloud_radius; ++y) {
|
||||
bool isFilled = clouds->getGrid(x, y);
|
||||
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
int xp = x * 4 + i % 4;
|
||||
int yp = y * 4 + i / 4;
|
||||
|
||||
image->setPixel(xp, yp, video::SColor(255, isFilled * 255, 0, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buffer->setTextureImage(id, image);
|
||||
|
||||
image->drop();
|
||||
}
|
||||
|
||||
private:
|
||||
Clouds* clouds = nullptr;
|
||||
u8 id = 0;
|
||||
TextureBuffer* buffer = nullptr;
|
||||
};
|
||||
|
||||
class CloudDepthStep : public RenderStep {
|
||||
public:
|
||||
CloudDepthStep(Clouds* clouds) :
|
||||
clouds(clouds)
|
||||
{
|
||||
}
|
||||
|
||||
void setRenderSource(RenderSource* _source) override {}
|
||||
|
||||
void setRenderTarget(RenderTarget* _target) override
|
||||
{
|
||||
target = _target;
|
||||
}
|
||||
|
||||
void reset(PipelineContext& context) override {}
|
||||
|
||||
void run(PipelineContext& context) override
|
||||
{
|
||||
if (target)
|
||||
target->activate(context);
|
||||
|
||||
clouds->renderDepth();
|
||||
}
|
||||
|
||||
private:
|
||||
Clouds* clouds = nullptr;
|
||||
RenderTarget* target = nullptr;
|
||||
};
|
||||
|
||||
PostProcessingStep::PostProcessingStep(u32 _shader_id, const std::vector<u8> &_texture_map) :
|
||||
shader_id(_shader_id), texture_map(_texture_map)
|
||||
|
@ -175,6 +65,7 @@ void PostProcessingStep::run(PipelineContext &context)
|
|||
if (target)
|
||||
target->activate(context);
|
||||
|
||||
// attach the shader
|
||||
material.MaterialType = context.client->getShaderSource()->getShaderInfo(shader_id).material;
|
||||
|
||||
auto driver = context.device->getVideoDriver();
|
||||
|
@ -205,17 +96,6 @@ void PostProcessingStep::setBilinearFilter(u8 index, bool value)
|
|||
material.TextureLayers[index].MagFilter = value ? video::ETMAGF_LINEAR : video::ETMAGF_NEAREST;
|
||||
}
|
||||
|
||||
void PostProcessingStep::setWrapRepeat(u8 index, bool value) {
|
||||
assert(index < video::MATERIAL_MAX_TEXTURES);
|
||||
material.TextureLayers[index].TextureWrapU = value ? video::ETC_REPEAT : video::ETC_CLAMP_TO_EDGE;
|
||||
material.TextureLayers[index].TextureWrapV = value ? video::ETC_REPEAT : video::ETC_CLAMP_TO_EDGE;
|
||||
}
|
||||
|
||||
void PostProcessingStep::disableDepthTest() {
|
||||
material.ZBuffer = video::ECFN_DISABLED;
|
||||
material.ZWriteEnable = video::EZW_OFF;
|
||||
}
|
||||
|
||||
RenderStep *addPostProcessing(RenderPipeline *pipeline, RenderStep *previousStep, v2f scale, Client *client)
|
||||
{
|
||||
auto buffer = pipeline->createOwned<TextureBuffer>();
|
||||
|
@ -241,13 +121,8 @@ RenderStep *addPostProcessing(RenderPipeline *pipeline, RenderStep *previousStep
|
|||
static const u8 TEXTURE_EXPOSURE_2 = 4;
|
||||
static const u8 TEXTURE_FXAA = 5;
|
||||
static const u8 TEXTURE_VOLUME = 6;
|
||||
static const u8 TEXTURE_CLOUDS_1 = 7;
|
||||
static const u8 TEXTURE_CLOUDS_2 = 8;
|
||||
static const u8 TEXTURE_CLOUD_DENSITY = 9;
|
||||
static const u8 TEXTURE_NOISE = 10;
|
||||
static const u8 TEXTURE_NOISE_COARSE = 11;
|
||||
static const u8 TEXTURE_SCALE_DOWN = 20;
|
||||
static const u8 TEXTURE_SCALE_UP = 30;
|
||||
static const u8 TEXTURE_SCALE_DOWN = 10;
|
||||
static const u8 TEXTURE_SCALE_UP = 20;
|
||||
|
||||
// Super-sampling is simply rendering into a larger texture.
|
||||
// Downscaling is done by the final step when rendering to the screen.
|
||||
|
@ -257,8 +132,6 @@ RenderStep *addPostProcessing(RenderPipeline *pipeline, RenderStep *previousStep
|
|||
const bool enable_ssaa = antialiasing == "ssaa";
|
||||
const bool enable_fxaa = antialiasing == "fxaa";
|
||||
const bool enable_volumetric_light = g_settings->getBool("enable_volumetric_lighting") && enable_bloom;
|
||||
// TODO: Proper constraints
|
||||
const bool enable_volumetric_clouds = g_settings->getBool("enable_volumetric_clouds") && client->getClouds();
|
||||
|
||||
if (enable_ssaa) {
|
||||
u16 ssaa_scale = MYMAX(2, g_settings->getU16("fsaa"));
|
||||
|
@ -279,55 +152,11 @@ RenderStep *addPostProcessing(RenderPipeline *pipeline, RenderStep *previousStep
|
|||
// Number of mipmap levels of the bloom downsampling texture
|
||||
const u8 MIPMAP_LEVELS = 4;
|
||||
|
||||
|
||||
// post-processing stage
|
||||
|
||||
u8 source = TEXTURE_COLOR;
|
||||
|
||||
u8 final_color_source = TEXTURE_COLOR;
|
||||
|
||||
if (enable_volumetric_clouds) {
|
||||
const u16 cloud_radius = g_settings->getU16("cloud_radius");
|
||||
|
||||
buffer->setTexture(TEXTURE_NOISE, core::dimension2du(256, 256), "noise", color_format);
|
||||
pipeline->addStep<NoiseStep>(buffer, TEXTURE_NOISE, 256);
|
||||
|
||||
buffer->setTexture(TEXTURE_NOISE_COARSE, core::dimension2du(cloud_radius * 8, cloud_radius * 8), "noise_coarse", color_format);
|
||||
pipeline->addStep<CloudDensityStep>(buffer, TEXTURE_NOISE_COARSE, client->getClouds());
|
||||
|
||||
u32 undersampling = core::clamp(g_settings->getU32("volumetrics_undersampling"), (u32)1, (u32)4);
|
||||
|
||||
buffer->setTexture(TEXTURE_CLOUDS_1, scale / (float)undersampling, "clouds_1", color_format, /*clear:*/ true);
|
||||
buffer->setTexture(TEXTURE_CLOUDS_2, scale, "clouds_2", color_format);
|
||||
buffer->setTexture(TEXTURE_CLOUD_DENSITY, scale, "cloud_density", color_format);
|
||||
|
||||
shader_id = client->getShaderSource()->getShader("volumetric_clouds", TILE_MATERIAL_PLAIN, NDT_MESH);
|
||||
PostProcessingStep *volumetric_clouds = pipeline->addStep<PostProcessingStep>(shader_id, std::vector<u8> { TEXTURE_DEPTH, TEXTURE_NOISE, TEXTURE_NOISE_COARSE });
|
||||
volumetric_clouds->setRenderSource(buffer);
|
||||
volumetric_clouds->setRenderTarget(pipeline->createOwned<TextureBufferOutput>(buffer, TEXTURE_CLOUDS_1));
|
||||
volumetric_clouds->setBilinearFilter(1, true);
|
||||
volumetric_clouds->setBilinearFilter(2, true);
|
||||
volumetric_clouds->setWrapRepeat(1, true);
|
||||
volumetric_clouds->setWrapRepeat(2, true);
|
||||
volumetric_clouds->disableDepthTest();
|
||||
|
||||
source = TEXTURE_CLOUDS_1;
|
||||
|
||||
shader_id = client->getShaderSource()->getShader("clouds_merge", TILE_MATERIAL_PLAIN, NDT_MESH);
|
||||
PostProcessingStep* blend_clouds = pipeline->addStep<PostProcessingStep>(shader_id, std::vector<u8> { TEXTURE_CLOUDS_1, TEXTURE_COLOR, TEXTURE_DEPTH });
|
||||
blend_clouds->setRenderSource(buffer);
|
||||
blend_clouds->setRenderTarget(pipeline->createOwned<TextureBufferOutput>(buffer, TEXTURE_CLOUDS_2));
|
||||
blend_clouds->setBilinearFilter(0, true);
|
||||
blend_clouds->disableDepthTest();
|
||||
|
||||
CloudDepthStep* cloud_depth = pipeline->addStep<CloudDepthStep>(client->getClouds());
|
||||
TextureBufferOutput* cloud_depth_output = pipeline->createOwned<TextureBufferOutput>(buffer, std::vector<u8>{ TEXTURE_COLOR }, TEXTURE_DEPTH);
|
||||
cloud_depth_output->disableClearing();
|
||||
cloud_depth->setRenderTarget(cloud_depth_output);
|
||||
|
||||
source = TEXTURE_CLOUDS_2;
|
||||
final_color_source = TEXTURE_CLOUDS_2;
|
||||
}
|
||||
|
||||
// common downsampling step for bloom or autoexposure
|
||||
if (enable_bloom || enable_auto_exposure) {
|
||||
|
||||
|
@ -395,14 +224,14 @@ RenderStep *addPostProcessing(RenderPipeline *pipeline, RenderStep *previousStep
|
|||
}
|
||||
|
||||
// FXAA
|
||||
u8 final_stage_source = final_color_source;
|
||||
u8 final_stage_source = TEXTURE_COLOR;
|
||||
|
||||
if (enable_fxaa) {
|
||||
final_stage_source = TEXTURE_FXAA;
|
||||
|
||||
buffer->setTexture(TEXTURE_FXAA, scale, "fxaa", color_format);
|
||||
shader_id = client->getShaderSource()->getShader("fxaa", TILE_MATERIAL_PLAIN);
|
||||
PostProcessingStep* effect = pipeline->createOwned<PostProcessingStep>(shader_id, std::vector<u8> { final_color_source });
|
||||
PostProcessingStep *effect = pipeline->createOwned<PostProcessingStep>(shader_id, std::vector<u8> { TEXTURE_COLOR });
|
||||
pipeline->addStep(effect);
|
||||
effect->setBilinearFilter(0, true);
|
||||
effect->setRenderSource(buffer);
|
||||
|
|
|
@ -49,13 +49,6 @@ public:
|
|||
* @param value true to enable the bilinear filter, false to disable
|
||||
*/
|
||||
void setBilinearFilter(u8 index, bool value);
|
||||
|
||||
void setWrapRepeat(u8 index, bool value);
|
||||
|
||||
void setColor(video::SColor color);
|
||||
|
||||
void disableDepthTest();
|
||||
|
||||
private:
|
||||
u32 shader_id;
|
||||
std::vector<u8> texture_map;
|
||||
|
|
|
@ -681,6 +681,9 @@ ShaderInfo ShaderSource::generateShader(const std::string &name,
|
|||
shaders_header << "#define ENABLE_WAVING_PLANTS " << g_settings->getBool("enable_waving_plants") << "\n";
|
||||
shaders_header << "#define ENABLE_TONE_MAPPING " << g_settings->getBool("tone_mapping") << "\n";
|
||||
|
||||
if (g_settings->getBool("enable_tinted_fog"))
|
||||
shaders_header << "#define ENABLE_TINTED_FOG 1\n";
|
||||
|
||||
if (g_settings->getBool("enable_dynamic_shadows")) {
|
||||
shaders_header << "#define ENABLE_DYNAMIC_SHADOWS 1\n";
|
||||
if (g_settings->getBool("shadow_map_color"))
|
||||
|
@ -738,10 +741,6 @@ ShaderInfo ShaderSource::generateShader(const std::string &name,
|
|||
shaders_header << "#define VOLUMETRIC_LIGHT 1\n";
|
||||
}
|
||||
|
||||
if (g_settings->getBool("enable_volumetric_clouds")) {
|
||||
shaders_header << "#define VOLUMETRICS_UNDERSAMPLING " << g_settings->getU32("volumetrics_undersampling") << '\n';
|
||||
}
|
||||
|
||||
shaders_header << "#line 0\n"; // reset the line counter for meaningful diagnostics
|
||||
|
||||
std::string common_header = shaders_header.str();
|
||||
|
|
|
@ -339,11 +339,11 @@ void set_default_settings()
|
|||
settings->setDefault("bloom_intensity", "0.05");
|
||||
settings->setDefault("bloom_radius", "1");
|
||||
settings->setDefault("enable_volumetric_lighting", "false");
|
||||
settings->setDefault("enable_volumetric_clouds", "false");
|
||||
settings->setDefault("enable_bumpmaps", "false");
|
||||
settings->setDefault("enable_water_reflections", "false");
|
||||
settings->setDefault("enable_translucent_foliage", "false");
|
||||
settings->setDefault("enable_node_specular", "false");
|
||||
settings->setDefault("enable_tinted_fog", "false");
|
||||
|
||||
// Effects Shadows
|
||||
settings->setDefault("enable_dynamic_shadows", "false");
|
||||
|
|
|
@ -1817,8 +1817,8 @@ void Client::handleCommand_SetLighting(NetworkPacket *pkt)
|
|||
}
|
||||
if (pkt->getRemainingBytes() >= 4)
|
||||
*pkt >> lighting.volumetric_light_strength;
|
||||
if (pkt->getRemainingBytes() >= 4)
|
||||
*pkt >> lighting.artificial_light_color;
|
||||
if (pkt->getRemainingBytes() >= 4)
|
||||
*pkt >> lighting.shadow_tint;
|
||||
if (pkt->getRemainingBytes() >= 4)
|
||||
*pkt >> lighting.artificial_light_color;
|
||||
}
|
||||
|
|
|
@ -1861,9 +1861,7 @@ void Server::SendSetLighting(session_t peer_id, const Lighting &lighting)
|
|||
<< lighting.exposure.speed_bright_dark
|
||||
<< lighting.exposure.center_weight_power;
|
||||
|
||||
pkt << lighting.volumetric_light_strength << lighting.shadow_tint;
|
||||
|
||||
pkt << lighting.artificial_light_color;
|
||||
pkt << lighting.volumetric_light_strength << lighting.shadow_tint << lighting.artificial_light_color;
|
||||
|
||||
Send(&pkt);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue