Browse Source

Make volumetric light effect strength server controllable

- Make volumetric light effect strength server controllable
- Separate volumetric and bloom shader pipeline
- Require bloom to be enable, scale godrays with bloom
Lars 6 months ago
parent
commit
e0d4a9d575

+ 4 - 0
builtin/settingtypes.txt

@@ -627,6 +627,10 @@ bloom_strength_factor (Bloom Strength Factor) float 1.0 0.1 10.0
 #    Requires: shaders, enable_bloom
 bloom_radius (Bloom Radius) float 1 0.1 8
 
+#    Set to true to enable volumetric lighting effect (a.k.a. "Godrays").
+#
+#    Requires: shaders, enable_bloom
+enable_volumetric_lighting (Volumetric lighting) bool false
 
 [*Audio]
 

+ 0 - 93
client/shaders/extract_bloom/opengl_fragment.glsl

@@ -1,28 +1,13 @@
 #define rendered texture0
-#define depthmap texture2
 
 struct ExposureParams {
 	float compensationFactor;
 };
 
 uniform sampler2D rendered;
-uniform sampler2D depthmap;
-
 uniform mediump float bloomStrength;
 uniform ExposureParams exposureParams;
 
-uniform vec3 sunPositionScreen;
-uniform float sunBrightness;
-uniform vec3 moonPositionScreen;
-uniform float moonBrightness;
-
-uniform vec3 dayLight;
-#ifdef ENABLE_DYNAMIC_SHADOWS
-uniform vec3 v_LightDirection;
-#else
-const vec3 v_LightDirection = vec3(0.0, -1.0, 0.0);
-#endif
-
 #ifdef GL_ES
 varying mediump vec2 varTexCoord;
 #else
@@ -33,80 +18,6 @@ centroid varying vec2 varTexCoord;
 varying float exposure; // linear exposure factor, see vertex shader
 #endif
 
-const float far = 1000.;
-const float near = 1.;
-float mapDepth(float depth)
-{
-	return min(1., 1. / (1.00001 - depth) / far);
-}
-
-float noise(vec3 uvd) {
-	return fract(dot(sin(uvd * vec3(13041.19699, 27723.29171, 61029.77801)), vec3(73137.11101, 37312.92319, 10108.89991)));
-}
-
-float sampleVolumetricLight(vec2 uv, vec3 lightVec, float rawDepth)
-{
-	lightVec = 0.5 * lightVec / lightVec.z + 0.5;
-	const float samples = 30.;
-	float result = texture2D(depthmap, uv).r < 1. ? 0.0 : 1.0;
-	float bias = noise(vec3(uv, rawDepth));
-	vec2 samplepos;
-	for (float i = 1.; i < samples; i++) {
-		samplepos = mix(uv, lightVec.xy, (i + bias) / samples);
-		if (min(samplepos.x, samplepos.y) > 0. && max(samplepos.x, samplepos.y) < 1.)
-			result += texture2D(depthmap, samplepos).r < 1. ? 0.0 : 1.0;
-	}
-	return result / samples;
-}
-
-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.))));
-}
-
-vec3 applyVolumetricLight(vec3 color, vec2 uv, float rawDepth)
-{
-	vec3 lookDirection = normalize(vec3(uv.x * 2. - 1., uv.y * 2. - 1., rawDepth));
-	vec3 lightSourceTint = vec3(1.0, 0.98, 0.4);
-
-	const float boost = 4.0;
-	float brightness = 0.;
-	vec3 sourcePosition = vec3(-1., -1., -1);
-
-	if (sunPositionScreen.z > 0. && sunBrightness > 0.) {
-		brightness = sunBrightness;
-		sourcePosition = sunPositionScreen;
-	}
-	else if (moonPositionScreen.z > 0. && moonBrightness > 0.) {
-		lightSourceTint = vec3(0.4, 0.9, 1.);
-		brightness = moonBrightness * 0.05;
-		sourcePosition = moonPositionScreen;
-	}
-
-	float cameraDirectionFactor = pow(clamp(dot(sourcePosition, vec3(0., 0., 1.)), 0.0, 0.7), 2.5);
-	float viewAngleFactor = pow(max(0., dot(sourcePosition, lookDirection)), 8.);
-
-	float lightFactor = brightness * sampleVolumetricLight(uv, sourcePosition, rawDepth) *
-			(0.05 * cameraDirectionFactor + 0.95 * viewAngleFactor);
-
-	color = mix(color, boost * getDirectLightScatteringAtGround(v_LightDirection) * dayLight, lightFactor);
-
-	// if (sunPositionScreen.z < 0.)
-	// 	color.rg += 1. - clamp(abs((2. * uv.xy - 1.) - sunPositionScreen.xy / sunPositionScreen.z) * 1000., 0., 1.);
-	// if (moonPositionScreen.z < 0.)
-	// 	color.rg += 1. - clamp(abs((2. * uv.xy - 1.) - moonPositionScreen.xy / moonPositionScreen.z) * 1000., 0., 1.);
-	return color;
-}
-
 void main(void)
 {
 	vec2 uv = varTexCoord.st;
@@ -120,9 +31,5 @@ void main(void)
 	color *= exposure;
 #endif
 
-	float rawDepth = texture2D(depthmap, uv).r;
-
-	color = applyVolumetricLight(color, uv, rawDepth);
-
 	gl_FragColor = vec4(color, 1.0); // force full alpha to avoid holes in the image.
 }

+ 0 - 1
client/shaders/second_stage/opengl_fragment.glsl

@@ -126,7 +126,6 @@ void main(void)
 #endif
 	}
 
-
 #ifdef ENABLE_BLOOM
 	color = applyBloom(color, uv);
 #endif

+ 115 - 0
client/shaders/volumetric_light/opengl_fragment.glsl

@@ -0,0 +1,115 @@
+#define rendered texture0
+#define depthmap texture1
+
+uniform sampler2D rendered;
+uniform sampler2D depthmap;
+
+uniform vec3 sunPositionScreen;
+uniform float sunBrightness;
+uniform vec3 moonPositionScreen;
+uniform float moonBrightness;
+
+uniform lowp float volumetricLightStrength;
+
+uniform vec3 dayLight;
+#ifdef ENABLE_DYNAMIC_SHADOWS
+uniform vec3 v_LightDirection;
+#else
+const vec3 v_LightDirection = vec3(0.0, -1.0, 0.0);
+#endif
+
+#ifdef GL_ES
+varying mediump vec2 varTexCoord;
+#else
+centroid varying vec2 varTexCoord;
+#endif
+
+const float far = 1000.;
+float mapDepth(float depth)
+{
+	return min(1., 1. / (1.00001 - depth) / far);
+}
+
+float noise(vec3 uvd) {
+	return fract(dot(sin(uvd * vec3(13041.19699, 27723.29171, 61029.77801)), vec3(73137.11101, 37312.92319, 10108.89991)));
+}
+
+float sampleVolumetricLight(vec2 uv, vec3 lightVec, float rawDepth)
+{
+	lightVec = 0.5 * lightVec / lightVec.z + 0.5;
+	const float samples = 30.;
+	float result = texture2D(depthmap, uv).r < 1. ? 0.0 : 1.0;
+	float bias = noise(vec3(uv, rawDepth));
+	vec2 samplepos;
+	for (float i = 1.; i < samples; i++) {
+		samplepos = mix(uv, lightVec.xy, (i + bias) / samples);
+		if (min(samplepos.x, samplepos.y) > 0. && max(samplepos.x, samplepos.y) < 1.)
+			result += texture2D(depthmap, samplepos).r < 1. ? 0.0 : 1.0;
+	}
+	return result / samples;
+}
+
+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.))));
+}
+
+vec3 applyVolumetricLight(vec3 color, vec2 uv, float rawDepth)
+{
+	vec3 lookDirection = normalize(vec3(uv.x * 2. - 1., uv.y * 2. - 1., rawDepth));
+
+	const float boost = 4.0;
+	float brightness = 0.;
+	vec3 sourcePosition = vec3(-1., -1., -1);
+
+	if (sunPositionScreen.z > 0. && sunBrightness > 0.) {
+		brightness = sunBrightness;
+		sourcePosition = sunPositionScreen;
+	}
+	else if (moonPositionScreen.z > 0. && moonBrightness > 0.) {
+		brightness = moonBrightness * 0.05;
+		sourcePosition = moonPositionScreen;
+	}
+
+	float cameraDirectionFactor = pow(clamp(dot(sourcePosition, vec3(0., 0., 1.)), 0.0, 0.7), 2.5);
+	float viewAngleFactor = pow(max(0., dot(sourcePosition, lookDirection)), 8.);
+
+	float lightFactor = brightness * sampleVolumetricLight(uv, sourcePosition, rawDepth) *
+			(0.05 * cameraDirectionFactor + 0.95 * viewAngleFactor);
+
+	color = mix(color, boost * getDirectLightScatteringAtGround(v_LightDirection) * dayLight, lightFactor);
+
+	// a factor of 5 tested well
+	color *= volumetricLightStrength * 5.0;
+
+	// if (sunPositionScreen.z < 0.)
+	// 	color.rg += 1. - clamp(abs((2. * uv.xy - 1.) - sunPositionScreen.xy / sunPositionScreen.z) * 1000., 0., 1.);
+	// if (moonPositionScreen.z < 0.)
+	// 	color.rg += 1. - clamp(abs((2. * uv.xy - 1.) - moonPositionScreen.xy / moonPositionScreen.z) * 1000., 0., 1.);
+	return color;
+}
+
+void main(void)
+{
+	vec2 uv = varTexCoord.st;
+	vec3 color = texture2D(rendered, uv).rgb;
+	// translate to linear colorspace (approximate)
+	color = pow(color, vec3(2.2));
+
+	if (volumetricLightStrength > 0.0) {
+		float rawDepth = texture2D(depthmap, uv).r;
+
+		color = applyVolumetricLight(color, uv, rawDepth);
+	}
+
+	gl_FragColor = vec4(color, 1.0); // force full alpha to avoid holes in the image.
+}

+ 12 - 0
client/shaders/volumetric_light/opengl_vertex.glsl

@@ -0,0 +1,12 @@
+#ifdef GL_ES
+varying mediump vec2 varTexCoord;
+#else
+centroid varying vec2 varTexCoord;
+#endif
+
+
+void main(void)
+{
+	varTexCoord.st = inTexCoord0.st;
+	gl_Position = inVertexPosition;
+}

+ 3 - 0
doc/lua_api.md

@@ -8036,6 +8036,9 @@ child will follow movement and rotation of that bone.
         * `speed_dark_bright` set the speed of adapting to bright light (default: `1000.0`)
         * `speed_bright_dark` set the speed of adapting to dark scene (default: `1000.0`)
         * `center_weight_power` set the power factor for center-weighted luminance measurement (default: `1.0`)
+      * `volumetric_light`: is a table that controls volumetric light (a.k.a. "godrays")
+        * `strength`: sets the strength of the volumetric light effect from 0 (off, default) to 1 (strongest)
+           * This value has no effect on clients who have the "Volumetric Lighting" or "Bloom" shaders disabled.
 
 * `get_lighting()`: returns the current state of lighting for the player.
     * Result is a table with the same fields as `light_definition` in `set_lighting`.

+ 43 - 37
src/client/game.cpp

@@ -404,10 +404,12 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter
 	CachedPixelShaderSetting<float> m_bloom_radius_pixel;
 	float m_bloom_radius;
 	CachedPixelShaderSetting<float> m_saturation_pixel;
+	bool m_volumetric_light_enabled;
 	CachedPixelShaderSetting<float, 3> m_sun_position_pixel;
 	CachedPixelShaderSetting<float> m_sun_brightness_pixel;
 	CachedPixelShaderSetting<float, 3> m_moon_position_pixel;
 	CachedPixelShaderSetting<float> m_moon_brightness_pixel;
+	CachedPixelShaderSetting<float> m_volumetric_light_strength_pixel;
 
 public:
 	void onSettingsChange(const std::string &name)
@@ -469,7 +471,8 @@ public:
 		m_sun_position_pixel("sunPositionScreen"),
 		m_sun_brightness_pixel("sunBrightness"),
 		m_moon_position_pixel("moonPositionScreen"),
-		m_moon_brightness_pixel("moonBrightness")
+		m_moon_brightness_pixel("moonBrightness"),
+		m_volumetric_light_strength_pixel("volumetricLightStrength")
 	{
 		g_settings->registerChangedCallback("enable_fog", settingsCallback, this);
 		g_settings->registerChangedCallback("exposure_compensation", settingsCallback, this);
@@ -483,6 +486,7 @@ public:
 		m_bloom_intensity = g_settings->getFloat("bloom_intensity", 0.01f, 1.0f);
 		m_bloom_strength = RenderingEngine::BASE_BLOOM_STRENGTH * g_settings->getFloat("bloom_strength_factor", 0.1f, 10.0f);
 		m_bloom_radius = g_settings->getFloat("bloom_radius", 0.1f, 8.0f);
+		m_volumetric_light_enabled = g_settings->getBool("enable_volumetric_lighting") && m_bloom_enabled;
 	}
 
 	~GameGlobalShaderConstantSetter()
@@ -588,49 +592,52 @@ public:
 		float saturation = m_client->getEnv().getLocalPlayer()->getLighting().saturation;
 		m_saturation_pixel.set(&saturation, services);
 
-		// Map directional light to screen space
-		auto camera_node = m_client->getCamera()->getCameraNode();
-		core::matrix4 transform = camera_node->getProjectionMatrix();
-		transform *= camera_node->getViewMatrix();
+		if (m_volumetric_light_enabled) {
+			// Map directional light to screen space
+			auto camera_node = m_client->getCamera()->getCameraNode();
+			core::matrix4 transform = camera_node->getProjectionMatrix();
+			transform *= camera_node->getViewMatrix();
 
-		if (m_sky->getSunVisible()) {
-			v3f sun_position = camera_node->getAbsolutePosition() + 
-					10000. * m_sky->getSunDirection();
-			transform.transformVect(sun_position);
-			sun_position.normalize();
+			if (m_sky->getSunVisible()) {
+				v3f sun_position = camera_node->getAbsolutePosition() +
+						10000. * m_sky->getSunDirection();
+				transform.transformVect(sun_position);
+				sun_position.normalize();
 
-			float sun_position_array[3] = { sun_position.X, sun_position.Y, sun_position.Z};
-			m_sun_position_pixel.set(sun_position_array, services);
+				float sun_position_array[3] = { sun_position.X, sun_position.Y, sun_position.Z};
+				m_sun_position_pixel.set(sun_position_array, services);
 
-			float sun_brightness = rangelim(107.143f * m_sky->getSunDirection().dotProduct(v3f(0.f, 1.f, 0.f)), 0.f, 1.f);
-			m_sun_brightness_pixel.set(&sun_brightness, services);
-		}
-		else {
-			float sun_position_array[3] = { 0.f, 0.f, -1.f };
-			m_sun_position_pixel.set(sun_position_array, services);
+				float sun_brightness = rangelim(107.143f * m_sky->getSunDirection().dotProduct(v3f(0.f, 1.f, 0.f)), 0.f, 1.f);
+				m_sun_brightness_pixel.set(&sun_brightness, services);
+			} else {
+				float sun_position_array[3] = { 0.f, 0.f, -1.f };
+				m_sun_position_pixel.set(sun_position_array, services);
 
-			float sun_brightness = 0.f;
-			m_sun_brightness_pixel.set(&sun_brightness, services);
-		}
+				float sun_brightness = 0.f;
+				m_sun_brightness_pixel.set(&sun_brightness, services);
+			}
 
-		if (m_sky->getMoonVisible()) {
-			v3f moon_position = camera_node->getAbsolutePosition() + 
-					10000. * m_sky->getMoonDirection();
-			transform.transformVect(moon_position);
-			moon_position.normalize();
+			if (m_sky->getMoonVisible()) {
+				v3f moon_position = camera_node->getAbsolutePosition() +
+						10000. * m_sky->getMoonDirection();
+				transform.transformVect(moon_position);
+				moon_position.normalize();
 
-			float moon_position_array[3] = { moon_position.X, moon_position.Y, moon_position.Z};
-			m_moon_position_pixel.set(moon_position_array, services);
+				float moon_position_array[3] = { moon_position.X, moon_position.Y, moon_position.Z};
+				m_moon_position_pixel.set(moon_position_array, services);
 
-			float moon_brightness = rangelim(107.143f * m_sky->getMoonDirection().dotProduct(v3f(0.f, 1.f, 0.f)), 0.f, 1.f);
-			m_moon_brightness_pixel.set(&moon_brightness, services);
-		}
-		else {
-			float moon_position_array[3] = { 0.f, 0.f, -1.f };
-			m_moon_position_pixel.set(moon_position_array, services);
+				float moon_brightness = rangelim(107.143f * m_sky->getMoonDirection().dotProduct(v3f(0.f, 1.f, 0.f)), 0.f, 1.f);
+				m_moon_brightness_pixel.set(&moon_brightness, services);
+			}
+			else {
+				float moon_position_array[3] = { 0.f, 0.f, -1.f };
+				m_moon_position_pixel.set(moon_position_array, services);
 
-			float moon_brightness = 0.f;
-			m_moon_brightness_pixel.set(&moon_brightness, services);
+				float moon_brightness = 0.f;
+				m_moon_brightness_pixel.set(&moon_brightness, services);
+			}
+			float volumetric_light_strength = m_client->getEnv().getLocalPlayer()->getLighting().volumetric_light_strength;
+			m_volumetric_light_strength_pixel.set(&volumetric_light_strength, services);
 		}
 	}
 
@@ -3089,7 +3096,6 @@ void Game::handleClientEvent_SetSky(ClientEvent *event, CameraOrientation *cam)
 	else
 		sky->setFogStart(rangelim(g_settings->getFloat("fog_start"), 0.0f, 0.99f));
 
-
 	delete event->set_sky;
 }
 

+ 24 - 12
src/client/render/secondstage.cpp

@@ -120,8 +120,9 @@ RenderStep *addPostProcessing(RenderPipeline *pipeline, RenderStep *previousStep
 	static const u8 TEXTURE_EXPOSURE_1 = 3;
 	static const u8 TEXTURE_EXPOSURE_2 = 4;
 	static const u8 TEXTURE_FXAA = 5;
-	static const u8 TEXTURE_BLOOM_DOWN = 10;
-	static const u8 TEXTURE_BLOOM_UP = 20;
+	static const u8 TEXTURE_VOLUME = 6;
+	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.
@@ -130,6 +131,7 @@ RenderStep *addPostProcessing(RenderPipeline *pipeline, RenderStep *previousStep
 	const bool enable_auto_exposure = g_settings->getBool("enable_auto_exposure");
 	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;
 
 	if (enable_ssaa) {
 		u16 ssaa_scale = MYMAX(2, g_settings->getU16("fsaa"));
@@ -160,9 +162,9 @@ RenderStep *addPostProcessing(RenderPipeline *pipeline, RenderStep *previousStep
 
 		v2f downscale = scale * 0.5;
 		for (u8 i = 0; i < MIPMAP_LEVELS; i++) {
-			buffer->setTexture(TEXTURE_BLOOM_DOWN + i, downscale, std::string("downsample") + std::to_string(i), color_format);
+			buffer->setTexture(TEXTURE_SCALE_DOWN + i, downscale, std::string("downsample") + std::to_string(i), color_format);
 			if (enable_bloom)
-				buffer->setTexture(TEXTURE_BLOOM_UP + i, downscale, std::string("upsample") + std::to_string(i), color_format);
+				buffer->setTexture(TEXTURE_SCALE_UP + i, downscale, std::string("upsample") + std::to_string(i), color_format);
 			downscale *= 0.5;
 		}
 
@@ -171,20 +173,30 @@ RenderStep *addPostProcessing(RenderPipeline *pipeline, RenderStep *previousStep
 
 			// get bright spots
 			u32 shader_id = client->getShaderSource()->getShader("extract_bloom", TILE_MATERIAL_PLAIN, NDT_MESH);
-			RenderStep *extract_bloom = pipeline->addStep<PostProcessingStep>(shader_id, std::vector<u8> { TEXTURE_COLOR, TEXTURE_EXPOSURE_1, TEXTURE_DEPTH });
+			RenderStep *extract_bloom = pipeline->addStep<PostProcessingStep>(shader_id, std::vector<u8> { source, TEXTURE_EXPOSURE_1 });
 			extract_bloom->setRenderSource(buffer);
 			extract_bloom->setRenderTarget(pipeline->createOwned<TextureBufferOutput>(buffer, TEXTURE_BLOOM));
 			source = TEXTURE_BLOOM;
 		}
 
+		if (enable_volumetric_light) {
+			buffer->setTexture(TEXTURE_VOLUME, scale, "volume", color_format);
+
+			shader_id = client->getShaderSource()->getShader("volumetric_light", TILE_MATERIAL_PLAIN, NDT_MESH);
+			auto volume = pipeline->addStep<PostProcessingStep>(shader_id, std::vector<u8> { source, TEXTURE_DEPTH });
+			volume->setRenderSource(buffer);
+			volume->setRenderTarget(pipeline->createOwned<TextureBufferOutput>(buffer, TEXTURE_VOLUME));
+			source = TEXTURE_VOLUME;
+		}
+
 		// downsample
 		shader_id = client->getShaderSource()->getShader("bloom_downsample", TILE_MATERIAL_PLAIN, NDT_MESH);
 		for (u8 i = 0; i < MIPMAP_LEVELS; i++) {
 			auto step = pipeline->addStep<PostProcessingStep>(shader_id, std::vector<u8> { source });
 			step->setRenderSource(buffer);
 			step->setBilinearFilter(0, true);
-			step->setRenderTarget(pipeline->createOwned<TextureBufferOutput>(buffer, TEXTURE_BLOOM_DOWN + i));
-			source = TEXTURE_BLOOM_DOWN + i;
+			step->setRenderTarget(pipeline->createOwned<TextureBufferOutput>(buffer, TEXTURE_SCALE_DOWN + i));
+			source = TEXTURE_SCALE_DOWN + i;
 		}
 	}
 
@@ -193,19 +205,19 @@ RenderStep *addPostProcessing(RenderPipeline *pipeline, RenderStep *previousStep
 		// upsample
 		shader_id = client->getShaderSource()->getShader("bloom_upsample", TILE_MATERIAL_PLAIN, NDT_MESH);
 		for (u8 i = MIPMAP_LEVELS - 1; i > 0; i--) {
-			auto step = pipeline->addStep<PostProcessingStep>(shader_id, std::vector<u8> { u8(TEXTURE_BLOOM_DOWN + i - 1), source });
+			auto step = pipeline->addStep<PostProcessingStep>(shader_id, std::vector<u8> { u8(TEXTURE_SCALE_DOWN + i - 1), source });
 			step->setRenderSource(buffer);
 			step->setBilinearFilter(0, true);
 			step->setBilinearFilter(1, true);
-			step->setRenderTarget(pipeline->createOwned<TextureBufferOutput>(buffer, u8(TEXTURE_BLOOM_UP + i - 1)));
-			source = TEXTURE_BLOOM_UP + i - 1;
+			step->setRenderTarget(pipeline->createOwned<TextureBufferOutput>(buffer, u8(TEXTURE_SCALE_UP + i - 1)));
+			source = TEXTURE_SCALE_UP + i - 1;
 		}
 	}
 
 	// Dynamic Exposure pt2
 	if (enable_auto_exposure) {
 		shader_id = client->getShaderSource()->getShader("update_exposure", TILE_MATERIAL_PLAIN, NDT_MESH);
-		auto update_exposure = pipeline->addStep<PostProcessingStep>(shader_id, std::vector<u8> { TEXTURE_EXPOSURE_1, u8(TEXTURE_BLOOM_DOWN + MIPMAP_LEVELS - 1) });
+		auto update_exposure = pipeline->addStep<PostProcessingStep>(shader_id, std::vector<u8> { TEXTURE_EXPOSURE_1, u8(TEXTURE_SCALE_DOWN + MIPMAP_LEVELS - 1) });
 		update_exposure->setBilinearFilter(1, true);
 		update_exposure->setRenderSource(buffer);
 		update_exposure->setRenderTarget(pipeline->createOwned<TextureBufferOutput>(buffer, TEXTURE_EXPOSURE_2));
@@ -228,7 +240,7 @@ RenderStep *addPostProcessing(RenderPipeline *pipeline, RenderStep *previousStep
 
 	// final merge
 	shader_id = client->getShaderSource()->getShader("second_stage", TILE_MATERIAL_PLAIN, NDT_MESH);
-	PostProcessingStep *effect = pipeline->createOwned<PostProcessingStep>(shader_id, std::vector<u8> { final_stage_source, TEXTURE_BLOOM_UP, TEXTURE_EXPOSURE_2 });
+	PostProcessingStep *effect = pipeline->createOwned<PostProcessingStep>(shader_id, std::vector<u8> { final_stage_source, TEXTURE_SCALE_UP, TEXTURE_EXPOSURE_2 });
 	pipeline->addStep(effect);
 	if (enable_ssaa)
 		effect->setBilinearFilter(0, true);

+ 4 - 0
src/client/shader.cpp

@@ -770,6 +770,10 @@ ShaderInfo ShaderSource::generateShader(const std::string &name,
 	if (g_settings->getBool("debanding"))
 		shaders_header << "#define ENABLE_DITHERING 1\n";
 
+	if (g_settings->getBool("enable_volumetric_lighting")) {
+		shaders_header << "#define VOLUMETRIC_LIGHT 1\n";
+	}
+
 	shaders_header << "#line 0\n"; // reset the line counter for meaningful diagnostics
 
 	std::string common_header = shaders_header.str();

+ 3 - 0
src/client/sky.h

@@ -120,6 +120,9 @@ public:
 	void setFogStart(float fog_start) { m_sky_params.fog_start = fog_start; }
 	float getFogStart() const { return m_sky_params.fog_start; }
 
+	void setVolumetricLightStrength(float volumetric_light_strength) { m_sky_params.volumetric_light_strength = volumetric_light_strength; }
+	float getVolumetricLightStrength() const { return m_sky_params.volumetric_light_strength; }
+
 private:
 	aabb3f m_box;
 	video::SMaterial m_materials[SKY_MATERIAL_COUNT];

+ 1 - 0
src/defaultsettings.cpp

@@ -271,6 +271,7 @@ void set_default_settings()
 	settings->setDefault("bloom_strength_factor", "1.0");
 	settings->setDefault("bloom_intensity", "0.05");
 	settings->setDefault("bloom_radius", "1");
+	settings->setDefault("enable_volumetric_lighting", "false");
 
 	// Effects Shadows
 	settings->setDefault("enable_dynamic_shadows", "false");

+ 1 - 0
src/lighting.h

@@ -53,4 +53,5 @@ struct Lighting
 	AutoExposure exposure;
 	float shadow_intensity {0.0f};
 	float saturation {1.0f};
+	float volumetric_light_strength {0.0f};
 };

+ 2 - 0
src/network/clientpackethandler.cpp

@@ -1804,4 +1804,6 @@ void Client::handleCommand_SetLighting(NetworkPacket *pkt)
 				>> lighting.exposure.speed_bright_dark
 				>> lighting.exposure.center_weight_power;
 	}
+	if (pkt->getRemainingBytes() >= 4)
+		*pkt >> lighting.volumetric_light_strength;
 }

+ 12 - 1
src/script/lua_api/l_object.cpp

@@ -2495,7 +2495,14 @@ int ObjectRef::l_set_lighting(lua_State *L)
 			lighting.exposure.center_weight_power = getfloatfield_default(L, -1, "center_weight_power", lighting.exposure.center_weight_power);
 		}
 		lua_pop(L, 1); // exposure
-	}
+
+		lua_getfield(L, 2, "volumetric_light");
+		if (lua_istable(L, -1)) {
+			getfloatfield(L, -1, "strength", lighting.volumetric_light_strength);
+			lighting.volumetric_light_strength = rangelim(lighting.volumetric_light_strength, 0.0f, 1.0f);
+		}
+		lua_pop(L, 1); // volumetric_light
+}
 
 	getServer(L)->setLighting(player, lighting);
 	return 0;
@@ -2533,6 +2540,10 @@ int ObjectRef::l_get_lighting(lua_State *L)
 	lua_pushnumber(L, lighting.exposure.center_weight_power);
 	lua_setfield(L, -2, "center_weight_power");
 	lua_setfield(L, -2, "exposure");
+	lua_newtable(L); // "volumetric_light"
+	lua_pushnumber(L, lighting.volumetric_light_strength);
+	lua_setfield(L, -2, "strength");
+	lua_setfield(L, -2, "volumetric_light");
 	return 1;
 }
 

+ 2 - 0
src/server.cpp

@@ -1919,6 +1919,8 @@ 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;
+
 	Send(&pkt);
 }
 

+ 1 - 0
src/skyparams.h

@@ -46,6 +46,7 @@ struct SkyboxParams
 	float body_orbit_tilt { INVALID_SKYBOX_TILT };
 	s16 fog_distance { -1 };
 	float fog_start { -1.0f };
+	float volumetric_light_strength { 0.0f };
 };
 
 struct SunParams