From 6d45c243f85942b20dab58753e735ec89a68f710 Mon Sep 17 00:00:00 2001 From: x2048 Date: Fri, 6 Jan 2023 22:33:25 +0100 Subject: [PATCH] Add dynamic exposure correction (#12959) * Add uniform for frame delta time * Adjust exposure in logarithmic (EV) space * Add network support and LUA API * Add testing mod --- android/native/jni/Android.mk | 1 + builtin/settingtypes.txt | 17 ++- .../extract_bloom/opengl_fragment.glsl | 13 +- .../shaders/extract_bloom/opengl_vertex.glsl | 8 + .../shaders/second_stage/opengl_fragment.glsl | 11 +- .../shaders/second_stage/opengl_vertex.glsl | 13 ++ .../update_exposure/opengl_fragment.glsl | 75 ++++++++++ .../update_exposure/opengl_vertex.glsl | 11 ++ doc/lua_api.txt | 9 ++ games/devtest/mods/lighting/init.lua | 140 ++++++++++++++++++ games/devtest/mods/lighting/mod.conf | 2 + games/devtest/mods/util_commands/init.lua | 14 -- src/CMakeLists.txt | 1 + src/client/clientenvironment.cpp | 11 +- src/client/clientenvironment.h | 2 + src/client/game.cpp | 41 +++-- src/client/render/pipeline.cpp | 20 +++ src/client/render/pipeline.h | 44 ++++-- src/client/render/secondstage.cpp | 55 +++++-- src/client/renderingengine.cpp | 2 +- src/client/shader.cpp | 3 + src/client/shader.h | 37 +++++ src/defaultsettings.cpp | 3 +- src/lighting.cpp | 29 ++++ src/lighting.h | 28 ++++ src/network/clientpackethandler.cpp | 8 + src/network/networkprotocol.h | 7 + src/script/lua_api/l_object.cpp | 26 ++++ src/server.cpp | 7 + 29 files changed, 567 insertions(+), 71 deletions(-) create mode 100644 client/shaders/update_exposure/opengl_fragment.glsl create mode 100644 client/shaders/update_exposure/opengl_vertex.glsl create mode 100644 games/devtest/mods/lighting/init.lua create mode 100644 games/devtest/mods/lighting/mod.conf create mode 100644 src/lighting.cpp diff --git a/android/native/jni/Android.mk b/android/native/jni/Android.mk index cd9326d4a..b2a8b3d6a 100644 --- a/android/native/jni/Android.mk +++ b/android/native/jni/Android.mk @@ -194,6 +194,7 @@ LOCAL_SRC_FILES := \ ../../src/itemdef.cpp \ ../../src/itemstackmetadata.cpp \ ../../src/light.cpp \ + ../../src/lighting.cpp \ ../../src/log.cpp \ ../../src/main.cpp \ ../../src/map.cpp \ diff --git a/builtin/settingtypes.txt b/builtin/settingtypes.txt index d3a817126..25f75fbbe 100644 --- a/builtin/settingtypes.txt +++ b/builtin/settingtypes.txt @@ -450,12 +450,17 @@ shadow_sky_body_orbit_tilt (Sky Body Orbit Tilt) float 0.0 -60.0 60.0 [**Post processing] -# Set the exposure compensation factor. -# This factor is applied to linear color value -# before all other post-processing effects. -# Value of 1.0 (default) means no exposure compensation. -# Range: from 0.1 to 10.0 -exposure_factor (Exposure Factor) float 1.0 0.1 10.0 +# Set the exposure compensation in EV units. +# Value of 0.0 (default) means no exposure compensation. +# Range: from -1 to 1.0 +exposure_compensation (Exposure compensation) float 0.0 -1.0 1.0 + +# Enable automatic exposure correction +# When enabled, the post-processing engine will +# automatically adjust to the brightness of the scene, +# simulating the behavior of human eye. +enable_auto_exposure (Enable Automatic Exposure) bool false + [**Bloom] diff --git a/client/shaders/extract_bloom/opengl_fragment.glsl b/client/shaders/extract_bloom/opengl_fragment.glsl index af320f9ab..45f5e9c6f 100644 --- a/client/shaders/extract_bloom/opengl_fragment.glsl +++ b/client/shaders/extract_bloom/opengl_fragment.glsl @@ -1,8 +1,12 @@ #define rendered texture0 +struct ExposureParams { + float compensationFactor; +}; + uniform sampler2D rendered; -uniform mediump float exposureFactor; uniform mediump float bloomStrength; +uniform ExposureParams exposureParams; #ifdef GL_ES varying mediump vec2 varTexCoord; @@ -10,6 +14,7 @@ varying mediump vec2 varTexCoord; centroid varying vec2 varTexCoord; #endif +varying float exposure; void main(void) { @@ -18,10 +23,6 @@ void main(void) // translate to linear colorspace (approximate) color = pow(color, vec3(2.2)); - // Scale colors by luminance to amplify bright colors - // in SDR textures. - float luminance = dot(color, vec3(0.213, 0.515, 0.072)); - luminance *= luminance; - color *= luminance * exposureFactor * bloomStrength; + color *= pow(2., exposure) * exposureParams.compensationFactor * bloomStrength; gl_FragColor = vec4(color, 1.0); // force full alpha to avoid holes in the image. } diff --git a/client/shaders/extract_bloom/opengl_vertex.glsl b/client/shaders/extract_bloom/opengl_vertex.glsl index 12692c296..479ae1079 100644 --- a/client/shaders/extract_bloom/opengl_vertex.glsl +++ b/client/shaders/extract_bloom/opengl_vertex.glsl @@ -1,11 +1,19 @@ +#define exposureMap texture1 + +uniform sampler2D exposureMap; + #ifdef GL_ES varying mediump vec2 varTexCoord; #else centroid varying vec2 varTexCoord; #endif +varying float exposure; + void main(void) { + exposure = texture2D(exposureMap, vec2(0.5)).r; + varTexCoord.st = inTexCoord0.st; gl_Position = inVertexPosition; } diff --git a/client/shaders/second_stage/opengl_fragment.glsl b/client/shaders/second_stage/opengl_fragment.glsl index 09b49ef0e..2ff58aa42 100644 --- a/client/shaders/second_stage/opengl_fragment.glsl +++ b/client/shaders/second_stage/opengl_fragment.glsl @@ -1,9 +1,14 @@ #define rendered texture0 #define bloom texture1 +struct ExposureParams { + float compensationFactor; +}; + uniform sampler2D rendered; uniform sampler2D bloom; -uniform mediump float exposureFactor; + +uniform ExposureParams exposureParams; uniform lowp float bloomIntensity; uniform lowp float saturation; @@ -13,6 +18,8 @@ varying mediump vec2 varTexCoord; centroid varying vec2 varTexCoord; #endif +varying float exposure; + #ifdef ENABLE_BLOOM vec4 applyBloom(vec4 color, vec2 uv) @@ -80,7 +87,7 @@ void main(void) if (uv.x > 0.5 || uv.y > 0.5) #endif { - color.rgb *= exposureFactor; + color.rgb *= exposure * exposureParams.compensationFactor; } diff --git a/client/shaders/second_stage/opengl_vertex.glsl b/client/shaders/second_stage/opengl_vertex.glsl index 12692c296..7c121f6d1 100644 --- a/client/shaders/second_stage/opengl_vertex.glsl +++ b/client/shaders/second_stage/opengl_vertex.glsl @@ -1,11 +1,24 @@ +#define exposureMap texture2 + +uniform sampler2D exposureMap; + #ifdef GL_ES varying mediump vec2 varTexCoord; #else centroid varying vec2 varTexCoord; #endif +varying float exposure; + void main(void) { +#ifdef ENABLE_AUTO_EXPOSURE + exposure = texture2D(exposureMap, vec2(0.5)).r; + exposure = pow(2., exposure); +#else + exposure = 1.0; +#endif + varTexCoord.st = inTexCoord0.st; gl_Position = inVertexPosition; } diff --git a/client/shaders/update_exposure/opengl_fragment.glsl b/client/shaders/update_exposure/opengl_fragment.glsl new file mode 100644 index 000000000..dfed8f0d8 --- /dev/null +++ b/client/shaders/update_exposure/opengl_fragment.glsl @@ -0,0 +1,75 @@ +#define exposure texture0 +#define screen texture1 + +struct ExposureParams { + float luminanceMin; + float luminanceMax; + float exposureCorrection; + float luminanceKey; + float speedDarkBright; + float speedBrightDark; + float centerWeightPower; + float compensationFactor; +}; + +uniform sampler2D exposure; +uniform sampler2D screen; + +#ifdef ENABLE_BLOOM +uniform float bloomStrength; +#else +const float bloomStrength = 1.0; +#endif +uniform ExposureParams exposureParams; +uniform float animationTimerDelta; + + +const vec3 luminanceFactors = vec3(0.213, 0.715, 0.072); + +float getLuminance(vec3 color) +{ + return dot(color, luminanceFactors); +} + +void main(void) +{ + float previousExposure = texture2D(exposure, vec2(0.5, 0.5)).r; + + vec3 averageColor = vec3(0.); + float n = 0.; + + // Scan the screen with center-weighting and sample average color + for (float _x = 0.1; _x < 0.9; _x += 0.17) { + float x = pow(_x, exposureParams.centerWeightPower); + for (float _y = 0.1; _y < 0.9; _y += 0.17) { + float y = pow(_y, exposureParams.centerWeightPower); + averageColor += texture2D(screen, vec2(0.5 + 0.5 * x, 0.5 + 0.5 * y)).rgb; + averageColor += texture2D(screen, vec2(0.5 + 0.5 * x, 0.5 - 0.5 * y)).rgb; + averageColor += texture2D(screen, vec2(0.5 - 0.5 * x, 0.5 + 0.5 * y)).rgb; + averageColor += texture2D(screen, vec2(0.5 - 0.5 * x, 0.5 - 0.5 * y)).rgb; + n += 4.; + } + } + + float luminance = getLuminance(averageColor); + luminance /= n; + + luminance /= pow(2., previousExposure) * bloomStrength * exposureParams.compensationFactor; // compensate for the configurable factors + + luminance = clamp(luminance, exposureParams.luminanceMin, exposureParams.luminanceMax); + + // From https://media.contentapi.ea.com/content/dam/eacom/frostbite/files/course-notes-moving-frostbite-to-pbr-v2.pdf + // 1. EV100 = log2(luminance * S / K) where S = 100, K = 0.125 = log2(luminance) + 3 + // 2. Lmax = 1.2 * 2 ^ (EV100 - EC) + // => Lmax = 1.2 * 2^3 * luminance / 2^EC = 9.6 * luminance / 2^EC + // 3. exposure = 1 / Lmax + // => exposure = 2^EC / (9.6 * luminance) + float wantedExposure = exposureParams.exposureCorrection - log(luminance)/0.693147180559945 - 3.263034405833794; + + if (wantedExposure < previousExposure) + wantedExposure = mix(wantedExposure, previousExposure, exp(-animationTimerDelta * exposureParams.speedDarkBright)); // dark -> bright + else + wantedExposure = mix(wantedExposure, previousExposure, exp(-animationTimerDelta * exposureParams.speedBrightDark)); // bright -> dark + + gl_FragColor = vec4(vec3(wantedExposure), 1.); +} diff --git a/client/shaders/update_exposure/opengl_vertex.glsl b/client/shaders/update_exposure/opengl_vertex.glsl new file mode 100644 index 000000000..12692c296 --- /dev/null +++ b/client/shaders/update_exposure/opengl_vertex.glsl @@ -0,0 +1,11 @@ +#ifdef GL_ES +varying mediump vec2 varTexCoord; +#else +centroid varying vec2 varTexCoord; +#endif + +void main(void) +{ + varTexCoord.st = inTexCoord0.st; + gl_Position = inVertexPosition; +} diff --git a/doc/lua_api.txt b/doc/lua_api.txt index a35adca60..fb6bc9df8 100644 --- a/doc/lua_api.txt +++ b/doc/lua_api.txt @@ -7472,6 +7472,15 @@ child will follow movement and rotation of that bone. * `shadows` is a table that controls ambient shadows * `intensity` sets the intensity of the shadows from 0 (no shadows, default) to 1 (blackness) * This value has no effect on clients who have the "Dynamic Shadows" shader disabled. + * `exposure` is a table that controls automatic exposure. + The basic exposure factor equation is `e = 2^exposure_correction / clamp(luminance, 2^luminance_min, 2^luminance_max)` + * `luminance_min` set the lower luminance boundary to use in the calculation + * `luminance_max` set the upper luminance boundary to use in the calculation + * `exposure_correction` correct observed exposure by the given EV value + * `speed_dark_bright` set the speed of adapting to bright light + * `speed_bright_dark` set the speed of adapting to dark scene + * `center_weight_power` set the power factor for center-weighted luminance measurement + * `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`. * `respawn()`: Respawns the player using the same mechanism as the death screen, diff --git a/games/devtest/mods/lighting/init.lua b/games/devtest/mods/lighting/init.lua new file mode 100644 index 000000000..5fb0f68fe --- /dev/null +++ b/games/devtest/mods/lighting/init.lua @@ -0,0 +1,140 @@ +local lighting_sections = { + {n = "shadows", d = "Shadows", + entries = { + { n = "intensity", d = "Shadow Intensity", min = 0, max = 1 } + } + }, + { + n = "exposure", d = "Exposure", + entries = { + {n = "luminance_min", d = "Minimum Luminance", min = -10, max = 10}, + {n = "luminance_max", d = "Maximum Luminance", min = -10, max = 10}, + {n = "exposure_correction", d = "Exposure Correction", min = -10, max = 10}, + {n = "speed_dark_bright", d = "Bright light adaptation speed", min = -10, max = 10, type="log2"}, + {n = "speed_bright_dark", d = "Dark scene adaptation speed", min = -10, max = 10, type="log2"}, + {n = "center_weight_power", d = "Power factor for center-weighting", min = 0.1, max = 10}, + } + } +} + +local function dump_lighting(lighting) + local result = "{\n" + local section_count = 0 + for _,section in ipairs(lighting_sections) do + section_count = section_count + 1 + + local parameters = section.entries or {} + local state = lighting[section.n] or {} + + result = result.." "..section.n.." = {\n" + + local count = 0 + for _,v in ipairs(parameters) do + count = count + 1 + result = result.." "..v.n.." = "..(math.floor(state[v.n] * 1000)/1000) + if count < #parameters then + result = result.."," + end + result = result.."\n" + end + + result = result.." }" + + if section_count < #lighting_sections then + result = result.."," + end + result = result.."\n" + end + result = result .."}" + return result +end + +minetest.register_chatcommand("set_lighting", { + params = "", + description = "Tune lighting parameters", + func = function(player_name, param) + local player = minetest.get_player_by_name(player_name); + if not player then return end + + local lighting = player:get_lighting() + local exposure = lighting.exposure or {} + + local form = { + "formspec_version[2]", + "size[15,30]", + "position[0.99,0.15]", + "anchor[1,0]", + "padding[0.05,0.1]", + "no_prepend[]" + }; + + local line = 1 + for _,section in ipairs(lighting_sections) do + local parameters = section.entries or {} + local state = lighting[section.n] or {} + + table.insert(form, "label[1,"..line..";"..section.d.."]") + line = line + 1 + + for _,v in ipairs(parameters) do + table.insert(form, "label[2,"..line..";"..v.d.."]") + table.insert(form, "scrollbaroptions[min=0;max=1000;smallstep=10;largestep=100;thumbsize=10]") + local value = state[v.n] + if v.type == "log2" then + value = math.log(value or 1) / math.log(2) + end + local sb_scale = math.floor(1000 * (math.max(v.min, value or 0) - v.min) / (v.max - v.min)) + table.insert(form, "scrollbar[2,"..(line+0.7)..";12,1;horizontal;"..section.n.."."..v.n..";"..sb_scale.."]") + line = line + 2.7 + end + + line = line + 1 + end + + minetest.show_formspec(player_name, "lighting", table.concat(form)) + local debug_value = dump_lighting(lighting) + local debug_ui = player:hud_add({type="text", position={x=0.1, y=0.3}, scale={x=1,y=1}, alignment = {x=1, y=1}, text=debug_value, number=0xFFFFFF}) + player:get_meta():set_int("lighting_hud", debug_ui) + end +}) + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= "lighting" then return end + + if not player then return end + + local hud_id = player:get_meta():get_int("lighting_hud") + + if fields.quit then + player:hud_remove(hud_id) + player:get_meta():set_int("lighting_hud", -1) + return + end + + local lighting = player:get_lighting() + for _,section in ipairs(lighting_sections) do + local parameters = section.entries or {} + + local state = (lighting[section.n] or {}) + lighting[section.n] = state + + for _,v in ipairs(parameters) do + + if fields[section.n.."."..v.n] then + local event = minetest.explode_scrollbar_event(fields[section.n.."."..v.n]) + if event.type == "CHG" then + local value = v.min + (v.max - v.min) * (event.value / 1000); + if v.type == "log2" then + value = math.pow(2, value); + end + state[v.n] = value; + end + end + end + end + + local debug_value = dump_lighting(lighting) + player:hud_change(hud_id, "text", debug_value) + + player:set_lighting(lighting) +end) \ No newline at end of file diff --git a/games/devtest/mods/lighting/mod.conf b/games/devtest/mods/lighting/mod.conf new file mode 100644 index 000000000..83bbbef0c --- /dev/null +++ b/games/devtest/mods/lighting/mod.conf @@ -0,0 +1,2 @@ +name = lighting +description = UI to control and debug lighting parameters diff --git a/games/devtest/mods/util_commands/init.lua b/games/devtest/mods/util_commands/init.lua index 4702cc556..48cd47f10 100644 --- a/games/devtest/mods/util_commands/init.lua +++ b/games/devtest/mods/util_commands/init.lua @@ -210,20 +210,6 @@ minetest.register_chatcommand("dump_item", { end, }) --- shadow control -minetest.register_on_joinplayer(function (player) - player:set_lighting({shadows={intensity = 0.33}}) -end) - -core.register_chatcommand("set_shadow", { - params = "", - description = "Set shadow parameters of current player.", - func = function(player_name, param) - local shadow_intensity = tonumber(param) - minetest.get_player_by_name(player_name):set_lighting({shadows = { intensity = shadow_intensity} }) - end -}) - core.register_chatcommand("set_saturation", { params = "", description = "Set the saturation for current player.", diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b4f457340..afe008395 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -375,6 +375,7 @@ set(common_SRCS itemdef.cpp itemstackmetadata.cpp light.cpp + lighting.cpp log.cpp main.cpp map.cpp diff --git a/src/client/clientenvironment.cpp b/src/client/clientenvironment.cpp index 0070fa82f..d9b88eb4a 100644 --- a/src/client/clientenvironment.cpp +++ b/src/client/clientenvironment.cpp @@ -531,8 +531,13 @@ void ClientEnvironment::updateFrameTime(bool is_paused) { // if paused, m_frame_time_pause_accumulator increases by dtime, // otherwise, m_frame_time increases by dtime - if (is_paused) + if (is_paused) { + m_frame_dtime = 0; m_frame_time_pause_accumulator = porting::getTimeMs() - m_frame_time; - else - m_frame_time = porting::getTimeMs() - m_frame_time_pause_accumulator; + } + else { + auto new_frame_time = porting::getTimeMs() - m_frame_time_pause_accumulator; + m_frame_dtime = new_frame_time - MYMAX(m_frame_time, m_frame_time_pause_accumulator); + m_frame_time = new_frame_time; + } } diff --git a/src/client/clientenvironment.h b/src/client/clientenvironment.h index 87820fbe8..f5d46deb5 100644 --- a/src/client/clientenvironment.h +++ b/src/client/clientenvironment.h @@ -144,6 +144,7 @@ public: void updateFrameTime(bool is_paused); u64 getFrameTime() const { return m_frame_time; } + u64 getFrameTimeDelta() const { return m_frame_dtime; } private: ClientMap *m_map; @@ -158,5 +159,6 @@ private: std::list m_player_names; v3s16 m_camera_offset; u64 m_frame_time = 0; + u64 m_frame_dtime = 0; u64 m_frame_time_pause_accumulator = 0; }; diff --git a/src/client/game.cpp b/src/client/game.cpp index 3f76d2e05..cf0117046 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -414,6 +414,8 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter CachedPixelShaderSetting m_fog_distance; CachedVertexShaderSetting m_animation_timer_vertex; CachedPixelShaderSetting m_animation_timer_pixel; + CachedVertexShaderSetting m_animation_timer_delta_vertex; + CachedPixelShaderSetting m_animation_timer_delta_pixel; CachedPixelShaderSetting m_day_light; CachedPixelShaderSetting m_star_color; CachedPixelShaderSetting m_eye_position_pixel; @@ -427,8 +429,8 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter CachedPixelShaderSetting m_texture3; CachedPixelShaderSetting m_texel_size0; std::array m_texel_size0_values; - CachedPixelShaderSetting m_exposure_factor_pixel; - float m_user_exposure_factor; + CachedStructPixelShaderSetting m_exposure_params_pixel; + float m_user_exposure_compensation; bool m_bloom_enabled; CachedPixelShaderSetting m_bloom_intensity_pixel; float m_bloom_intensity; @@ -443,8 +445,8 @@ public: { if (name == "enable_fog") m_fog_enabled = g_settings->getBool("enable_fog"); - if (name == "exposure_factor") - m_user_exposure_factor = g_settings->getFloat("exposure_factor", 0.1f, 10.0f); + if (name == "exposure_compensation") + m_user_exposure_compensation = g_settings->getFloat("exposure_compensation", -1.0f, 1.0f); if (name == "bloom_intensity") m_bloom_intensity = g_settings->getFloat("bloom_intensity", 0.01f, 1.0f); if (name == "bloom_strength_factor") @@ -470,6 +472,8 @@ public: m_fog_distance("fogDistance"), m_animation_timer_vertex("animationTimer"), m_animation_timer_pixel("animationTimer"), + m_animation_timer_delta_vertex("animationTimerDelta"), + m_animation_timer_delta_pixel("animationTimerDelta"), m_day_light("dayLight"), m_star_color("starColor"), m_eye_position_pixel("eyePosition"), @@ -482,20 +486,24 @@ public: m_texture2("texture2"), m_texture3("texture3"), m_texel_size0("texelSize0"), - m_exposure_factor_pixel("exposureFactor"), + m_exposure_params_pixel("exposureParams", + std::array { + "luminanceMin", "luminanceMax", "exposureCorrection", + "speedDarkBright", "speedBrightDark", "centerWeightPower", "compensationFactor" + }), m_bloom_intensity_pixel("bloomIntensity"), m_bloom_strength_pixel("bloomStrength"), m_bloom_radius_pixel("bloomRadius"), m_saturation_pixel("saturation") { g_settings->registerChangedCallback("enable_fog", settingsCallback, this); - g_settings->registerChangedCallback("exposure_factor", settingsCallback, this); + g_settings->registerChangedCallback("exposure_compensation", settingsCallback, this); g_settings->registerChangedCallback("bloom_intensity", settingsCallback, this); g_settings->registerChangedCallback("bloom_strength_factor", settingsCallback, this); g_settings->registerChangedCallback("bloom_radius", settingsCallback, this); g_settings->registerChangedCallback("saturation", settingsCallback, this); m_fog_enabled = g_settings->getBool("enable_fog"); - m_user_exposure_factor = g_settings->getFloat("exposure_factor", 0.1f, 10.0f); + m_user_exposure_compensation = g_settings->getFloat("exposure_compensation", -1.0f, 1.0f); m_bloom_enabled = g_settings->getBool("enable_bloom"); 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); @@ -546,6 +554,10 @@ public: m_animation_timer_vertex.set(&animation_timer_f, services); m_animation_timer_pixel.set(&animation_timer_f, services); + float animation_timer_delta_f = (float)m_client->getEnv().getFrameTimeDelta() / 100000.f; + m_animation_timer_delta_vertex.set(&animation_timer_delta_f, services); + m_animation_timer_delta_pixel.set(&animation_timer_delta_f, services); + float eye_position_array[3]; v3f epos = m_client->getEnv().getLocalPlayer()->getEyePosition(); epos.getAs3Values(eye_position_array); @@ -577,10 +589,17 @@ public: m_texel_size0.set(m_texel_size0_values.data(), services); - float exposure_factor = m_user_exposure_factor; - if (std::isnan(exposure_factor)) - exposure_factor = 1.0f; - m_exposure_factor_pixel.set(&exposure_factor, services); + const AutoExposure &exposure_params = m_client->getEnv().getLocalPlayer()->getLighting().exposure; + std::array exposure_buffer = { + std::pow(2.0f, exposure_params.luminance_min), + std::pow(2.0f, exposure_params.luminance_max), + exposure_params.exposure_correction, + exposure_params.speed_dark_bright, + exposure_params.speed_bright_dark, + exposure_params.center_weight_power, + powf(2.f, m_user_exposure_compensation) + }; + m_exposure_params_pixel.set(exposure_buffer.data(), services); if (m_bloom_enabled) { m_bloom_intensity_pixel.set(&m_bloom_intensity, services); diff --git a/src/client/render/pipeline.cpp b/src/client/render/pipeline.cpp index 13898f8a4..cc275a7ef 100644 --- a/src/client/render/pipeline.cpp +++ b/src/client/render/pipeline.cpp @@ -101,6 +101,16 @@ void TextureBuffer::reset(PipelineContext &context) RenderSource::reset(context); } +void TextureBuffer::swapTextures(u8 texture_a, u8 texture_b) +{ + assert(m_definitions[texture_a].valid && m_definitions[texture_b].valid); + + video::ITexture *temp = m_textures[texture_a]; + m_textures[texture_a] = m_textures[texture_b]; + m_textures[texture_b] = temp; +} + + bool TextureBuffer::ensureTexture(video::ITexture **texture, const TextureDefinition& definition, PipelineContext &context) { bool modify; @@ -230,6 +240,16 @@ void SetRenderTargetStep::run(PipelineContext &context) step->setRenderTarget(target); } +SwapTexturesStep::SwapTexturesStep(TextureBuffer *_buffer, u8 _texture_a, u8 _texture_b) + : buffer(_buffer), texture_a(_texture_a), texture_b(_texture_b) +{ +} + +void SwapTexturesStep::run(PipelineContext &context) +{ + buffer->swapTextures(texture_a, texture_b); +} + RenderSource *RenderPipeline::getInput() { return &m_input; diff --git a/src/client/render/pipeline.h b/src/client/render/pipeline.h index 354624102..bfdef2931 100644 --- a/src/client/render/pipeline.h +++ b/src/client/render/pipeline.h @@ -53,7 +53,7 @@ struct PipelineContext /** * Base object that can be owned by RenderPipeline - * + * */ class RenderPipelineObject { @@ -74,7 +74,7 @@ public: virtual u8 getTextureCount() = 0; /** - * Get a texture by index. + * Get a texture by index. * Returns nullptr is the texture does not exist. */ virtual video::ITexture *getTexture(u8 index) = 0; @@ -119,7 +119,7 @@ public: /** * Configure fixed-size texture for the specific index - * + * * @param index index of the texture * @param size width and height of the texture in pixels * @param height height of the texture in pixels @@ -130,7 +130,7 @@ public: /** * Configure relative-size texture for the specific index - * + * * @param index index of the texture * @param scale_factor relation of the texture dimensions to the screen dimensions * @param name unique name of the texture @@ -141,6 +141,7 @@ public: virtual u8 getTextureCount() override { return m_textures.size(); } virtual video::ITexture *getTexture(u8 index) override; virtual void reset(PipelineContext &context) override; + void swapTextures(u8 texture_a, u8 texture_b); private: static const u8 NO_DEPTH_TEXTURE = 255; @@ -193,7 +194,7 @@ private: /** * Allows remapping texture indicies in another RenderSource. - * + * * @note all unmapped indexes are passed through to the underlying render source. */ class RemappingSource : RenderSource @@ -205,7 +206,7 @@ public: /** * Maps texture index to a different index in the dependent source. - * + * * @param index texture index as requested by the @see RenderStep. * @param target_index matching texture index in the underlying @see RenderSource. */ @@ -250,7 +251,7 @@ public: virtual u8 getTextureCount() override; /** - * Get a texture by index. + * Get a texture by index. * Returns nullptr is the texture does not exist. */ virtual video::ITexture *getTexture(u8 index) override; @@ -288,14 +289,14 @@ class RenderStep : virtual public RenderPipelineObject public: /** * Assigns render source to this step. - * + * * @param source source of rendering information */ virtual void setRenderSource(RenderSource *source) = 0; /** * Assigned render target to this step. - * + * * @param target render target to send output to. */ virtual void setRenderTarget(RenderTarget *target) = 0; @@ -319,7 +320,7 @@ public: /** * Dynamically changes render target of another step. - * + * * This allows re-running parts of the pipeline with different outputs */ class SetRenderTargetStep : public TrivialRenderStep @@ -332,9 +333,24 @@ private: RenderTarget *target; }; +/** + * Swaps two textures in the texture buffer. + * + */ +class SwapTexturesStep : public TrivialRenderStep +{ +public: + SwapTexturesStep(TextureBuffer *buffer, u8 texture_a, u8 texture_b); + virtual void run(PipelineContext &context) override; +private: + TextureBuffer *buffer; + u8 texture_a; + u8 texture_b; +}; + /** * Render Pipeline provides a flexible way to execute rendering steps in the engine. - * + * * RenderPipeline also implements @see RenderStep, allowing for nesting of the pipelines. */ class RenderPipeline : public RenderStep @@ -342,7 +358,7 @@ class RenderPipeline : public RenderStep public: /** * Add a step to the end of the pipeline - * + * * @param step reference to a @see RenderStep implementation. */ RenderStep *addStep(RenderStep *step) @@ -353,9 +369,9 @@ public: /** * Capture ownership of a dynamically created @see RenderStep instance. - * + * * RenderPipeline will delete the instance when the pipeline is destroyed. - * + * * @param step reference to the instance. * @return RenderStep* value of the 'step' parameter. */ diff --git a/src/client/render/secondstage.cpp b/src/client/render/secondstage.cpp index ebc7e7411..395a0fe6b 100644 --- a/src/client/render/secondstage.cpp +++ b/src/client/render/secondstage.cpp @@ -115,10 +115,14 @@ RenderStep *addPostProcessing(RenderPipeline *pipeline, RenderStep *previousStep static const u8 TEXTURE_COLOR = 0; static const u8 TEXTURE_DEPTH = 1; static const u8 TEXTURE_BLOOM = 2; + static const u8 TEXTURE_EXPOSURE_1 = 3; + static const u8 TEXTURE_EXPOSURE_2 = 4; static const u8 TEXTURE_BLOOM_DOWN = 10; static const u8 TEXTURE_BLOOM_UP = 20; buffer->setTexture(TEXTURE_COLOR, scale, "3d_render", color_format); + buffer->setTexture(TEXTURE_EXPOSURE_1, core::dimension2du(1,1), "exposure_1", color_format); + buffer->setTexture(TEXTURE_EXPOSURE_2, core::dimension2du(1,1), "exposure_2", color_format); buffer->setTexture(TEXTURE_DEPTH, scale, "3d_depthmap", depth_format); // attach buffer to the previous step @@ -127,30 +131,40 @@ RenderStep *addPostProcessing(RenderPipeline *pipeline, RenderStep *previousStep // shared variables u32 shader_id; + // Number of mipmap levels of the bloom downsampling texture + const u8 MIPMAP_LEVELS = 4; + + const bool enable_bloom = g_settings->getBool("enable_bloom"); + const bool enable_auto_exposure = g_settings->getBool("enable_auto_exposure"); + // post-processing stage - // set up bloom - if (g_settings->getBool("enable_bloom")) { + u8 source = TEXTURE_COLOR; - buffer->setTexture(TEXTURE_BLOOM, scale, "bloom", color_format); + // common downsampling step for bloom or autoexposure + if (enable_bloom || enable_auto_exposure) { - const u8 MIPMAP_LEVELS = 4; v2f downscale = scale * 0.5; for (u8 i = 0; i < MIPMAP_LEVELS; i++) { - buffer->setTexture(TEXTURE_BLOOM_DOWN + i, downscale, std::string("bloom_down") + std::to_string(i), color_format); - buffer->setTexture(TEXTURE_BLOOM_UP + i, downscale, std::string("bloom_up") + std::to_string(i), color_format); + buffer->setTexture(TEXTURE_BLOOM_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); downscale *= 0.5; } - // get bright spots - u32 shader_id = client->getShaderSource()->getShader("extract_bloom", TILE_MATERIAL_PLAIN, NDT_MESH); - RenderStep *extract_bloom = pipeline->addStep(shader_id, std::vector { TEXTURE_COLOR }); - extract_bloom->setRenderSource(buffer); - extract_bloom->setRenderTarget(pipeline->createOwned(buffer, TEXTURE_BLOOM)); + if (enable_bloom) { + buffer->setTexture(TEXTURE_BLOOM, scale, "bloom", color_format); + + // get bright spots + u32 shader_id = client->getShaderSource()->getShader("extract_bloom", TILE_MATERIAL_PLAIN, NDT_MESH); + RenderStep *extract_bloom = pipeline->addStep(shader_id, std::vector { TEXTURE_COLOR, TEXTURE_EXPOSURE_1 }); + extract_bloom->setRenderSource(buffer); + extract_bloom->setRenderTarget(pipeline->createOwned(buffer, TEXTURE_BLOOM)); + source = TEXTURE_BLOOM; + } // downsample shader_id = client->getShaderSource()->getShader("bloom_downsample", TILE_MATERIAL_PLAIN, NDT_MESH); - u8 source = TEXTURE_BLOOM; for (u8 i = 0; i < MIPMAP_LEVELS; i++) { auto step = pipeline->addStep(shader_id, std::vector { source }); step->setRenderSource(buffer); @@ -158,7 +172,9 @@ RenderStep *addPostProcessing(RenderPipeline *pipeline, RenderStep *previousStep step->setRenderTarget(pipeline->createOwned(buffer, TEXTURE_BLOOM_DOWN + i)); source = TEXTURE_BLOOM_DOWN + i; } + } + if (enable_bloom) { // upsample shader_id = client->getShaderSource()->getShader("bloom_upsample", TILE_MATERIAL_PLAIN, NDT_MESH); for (u8 i = MIPMAP_LEVELS - 1; i > 0; i--) { @@ -171,11 +187,24 @@ RenderStep *addPostProcessing(RenderPipeline *pipeline, RenderStep *previousStep } } + if (enable_auto_exposure) { + shader_id = client->getShaderSource()->getShader("update_exposure", TILE_MATERIAL_PLAIN, NDT_MESH); + auto update_exposure = pipeline->addStep(shader_id, std::vector { TEXTURE_EXPOSURE_1, u8(TEXTURE_BLOOM_DOWN + MIPMAP_LEVELS - 1) }); + update_exposure->setBilinearFilter(1, true); + update_exposure->setRenderSource(buffer); + update_exposure->setRenderTarget(pipeline->createOwned(buffer, TEXTURE_EXPOSURE_2)); + } + // final post-processing shader_id = client->getShaderSource()->getShader("second_stage", TILE_MATERIAL_PLAIN, NDT_MESH); - PostProcessingStep *effect = pipeline->createOwned(shader_id, std::vector { TEXTURE_COLOR, TEXTURE_BLOOM_UP }); + PostProcessingStep *effect = pipeline->createOwned(shader_id, std::vector { TEXTURE_COLOR, TEXTURE_BLOOM_UP, TEXTURE_EXPOSURE_2 }); pipeline->addStep(effect); effect->setBilinearFilter(1, true); // apply filter to the bloom effect->setRenderSource(buffer); + + if (enable_auto_exposure) { + pipeline->addStep(buffer, TEXTURE_EXPOSURE_1, TEXTURE_EXPOSURE_2); + } + return effect; } diff --git a/src/client/renderingengine.cpp b/src/client/renderingengine.cpp index ec7a05338..a58b0efe6 100644 --- a/src/client/renderingengine.cpp +++ b/src/client/renderingengine.cpp @@ -56,7 +56,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #endif RenderingEngine *RenderingEngine::s_singleton = nullptr; -const float RenderingEngine::BASE_BLOOM_STRENGTH = 8.0f; +const float RenderingEngine::BASE_BLOOM_STRENGTH = 1.0f; static gui::GUISkin *createSkin(gui::IGUIEnvironment *environment, diff --git a/src/client/shader.cpp b/src/client/shader.cpp index da3da8ab1..ccecb22c3 100644 --- a/src/client/shader.cpp +++ b/src/client/shader.cpp @@ -784,6 +784,9 @@ ShaderInfo ShaderSource::generateShader(const std::string &name, shaders_header << "#define ENABLE_BLOOM_DEBUG 1\n"; } + if (g_settings->getBool("enable_auto_exposure")) + shaders_header << "#define ENABLE_AUTO_EXPOSURE 1\n"; + shaders_header << "#line 0\n"; // reset the line counter for meaningful diagnostics std::string common_header = shaders_header.str(); diff --git a/src/client/shader.h b/src/client/shader.h index 8f1ba1e41..33c3e45e3 100644 --- a/src/client/shader.h +++ b/src/client/shader.h @@ -121,6 +121,43 @@ public: CachedShaderSetting(name, false){} }; +template +class CachedStructShaderSetting { + const char *m_name; + T m_sent[count]; + bool has_been_set = false; + std::array m_fields; +public: + CachedStructShaderSetting(const char *name, std::array &&fields) : + m_name(name), m_fields(std::move(fields)) + {} + + void set(const T value[count], video::IMaterialRendererServices *services) + { + if (cache && has_been_set && std::equal(m_sent, m_sent + count, value)) + return; + + for (std::size_t i = 0; i < count; i++) { + std::string uniform_name = std::string(m_name) + "." + m_fields[i]; + + if (is_pixel) + services->setPixelShaderConstant(services->getPixelShaderConstantID(uniform_name.c_str()), value + i, 1); + else + services->setVertexShaderConstant(services->getVertexShaderConstantID(uniform_name.c_str()), value + i, 1); + } + + if (cache) { + std::copy(value, value + count, m_sent); + has_been_set = true; + } + } +}; + +template +using CachedStructVertexShaderSetting = CachedStructShaderSetting; + +template +using CachedStructPixelShaderSetting = CachedStructShaderSetting; /* ShaderSource creates and caches shaders. diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp index 66a7411c4..e9ee8281d 100644 --- a/src/defaultsettings.cpp +++ b/src/defaultsettings.cpp @@ -278,7 +278,8 @@ void set_default_settings() settings->setDefault("water_wave_speed", "5.0"); settings->setDefault("enable_waving_leaves", "false"); settings->setDefault("enable_waving_plants", "false"); - settings->setDefault("exposure_factor", "1.0"); + settings->setDefault("exposure_compensation", "0.0"); + settings->setDefault("enable_auto_exposure", "false"); settings->setDefault("enable_bloom", "false"); settings->setDefault("enable_bloom_debug", "false"); settings->setDefault("bloom_strength_factor", "1.0"); diff --git a/src/lighting.cpp b/src/lighting.cpp new file mode 100644 index 000000000..b19d47772 --- /dev/null +++ b/src/lighting.cpp @@ -0,0 +1,29 @@ +/* +Minetest +Copyright (C) 2021 x2048, Dmitry Kostenko + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "lighting.h" + +AutoExposure::AutoExposure() + : luminance_min(-3.f), + luminance_max(-3.f), + exposure_correction(0.0f), + speed_dark_bright(1000.f), + speed_bright_dark(1000.f), + center_weight_power(1.f) +{} diff --git a/src/lighting.h b/src/lighting.h index 6c837568b..9c4211605 100644 --- a/src/lighting.h +++ b/src/lighting.h @@ -19,10 +19,38 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once + +/** + * Parameters for automatic exposure compensation + * + * Automatic exposure compensation uses the following equation: + * + * wanted_exposure = 2^exposure_correction / clamp(observed_luminance, 2^luminance_min, 2^luminance_max) + * + */ +struct AutoExposure +{ + /// @brief Minimum boundary for computed luminance + float luminance_min; + /// @brief Maximum boundary for computed luminance + float luminance_max; + /// @brief Luminance bias. Higher values make the scene darker, can be negative. + float exposure_correction; + /// @brief Speed of transition from dark to bright scenes + float speed_dark_bright; + /// @brief Speed of transition from bright to dark scenes + float speed_bright_dark; + /// @brief Power value for center-weighted metering. Value of 1.0 measures entire screen uniformly + float center_weight_power; + + AutoExposure(); +}; + /** Describes ambient light settings for a player */ struct Lighting { + AutoExposure exposure; float shadow_intensity {0.0f}; float saturation {1.0f}; }; diff --git a/src/network/clientpackethandler.cpp b/src/network/clientpackethandler.cpp index 829b1c3e6..0e6256356 100644 --- a/src/network/clientpackethandler.cpp +++ b/src/network/clientpackethandler.cpp @@ -1766,4 +1766,12 @@ void Client::handleCommand_SetLighting(NetworkPacket *pkt) *pkt >> lighting.shadow_intensity; if (pkt->getRemainingBytes() >= 4) *pkt >> lighting.saturation; + if (pkt->getRemainingBytes() >= 24) { + *pkt >> lighting.exposure.luminance_min + >> lighting.exposure.luminance_max + >> lighting.exposure.exposure_correction + >> lighting.exposure.speed_dark_bright + >> lighting.exposure.speed_bright_dark + >> lighting.exposure.center_weight_power; + } } diff --git a/src/network/networkprotocol.h b/src/network/networkprotocol.h index 01e65ef68..4e50ef533 100644 --- a/src/network/networkprotocol.h +++ b/src/network/networkprotocol.h @@ -831,6 +831,13 @@ enum ToClientCommand /* f32 shadow_intensity f32 saturation + exposure parameters + f32 luminance_min + f32 luminance_max + f32 exposure_correction + f32 speed_dark_bright + f32 speed_bright_dark + f32 center_weight_power */ TOCLIENT_NUM_MSG_TYPES = 0x64, diff --git a/src/script/lua_api/l_object.cpp b/src/script/lua_api/l_object.cpp index 0a3e05907..fc2c1254b 100644 --- a/src/script/lua_api/l_object.cpp +++ b/src/script/lua_api/l_object.cpp @@ -2297,8 +2297,20 @@ int ObjectRef::l_set_lighting(lua_State *L) getfloatfield(L, -1, "intensity", lighting.shadow_intensity); } lua_pop(L, 1); // shadows + getfloatfield(L, -1, "saturation", lighting.saturation); + lua_getfield(L, 2, "exposure"); + if (lua_istable(L, -1)) { + lighting.exposure.luminance_min = getfloatfield_default(L, -1, "luminance_min", lighting.exposure.luminance_min); + lighting.exposure.luminance_max = getfloatfield_default(L, -1, "luminance_max", lighting.exposure.luminance_max); + lighting.exposure.exposure_correction = getfloatfield_default(L, -1, "exposure_correction", lighting.exposure.exposure_correction); + lighting.exposure.speed_dark_bright = getfloatfield_default(L, -1, "speed_dark_bright", lighting.exposure.speed_dark_bright); + lighting.exposure.speed_bright_dark = getfloatfield_default(L, -1, "speed_bright_dark", lighting.exposure.speed_bright_dark); + lighting.exposure.center_weight_power = getfloatfield_default(L, -1, "center_weight_power", lighting.exposure.center_weight_power); + } + lua_pop(L, 1); // exposure + getServer(L)->setLighting(player, lighting); return 0; } @@ -2321,6 +2333,20 @@ int ObjectRef::l_get_lighting(lua_State *L) lua_setfield(L, -2, "shadows"); lua_pushnumber(L, lighting.saturation); lua_setfield(L, -2, "saturation"); + lua_newtable(L); // "exposure" + lua_pushnumber(L, lighting.exposure.luminance_min); + lua_setfield(L, -2, "luminance_min"); + lua_pushnumber(L, lighting.exposure.luminance_max); + lua_setfield(L, -2, "luminance_max"); + lua_pushnumber(L, lighting.exposure.exposure_correction); + lua_setfield(L, -2, "exposure_correction"); + lua_pushnumber(L, lighting.exposure.speed_dark_bright); + lua_setfield(L, -2, "speed_dark_bright"); + lua_pushnumber(L, lighting.exposure.speed_bright_dark); + lua_setfield(L, -2, "speed_bright_dark"); + lua_pushnumber(L, lighting.exposure.center_weight_power); + lua_setfield(L, -2, "center_weight_power"); + lua_setfield(L, -2, "exposure"); return 1; } diff --git a/src/server.cpp b/src/server.cpp index ee08b45fd..a1171f404 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -1866,6 +1866,13 @@ void Server::SendSetLighting(session_t peer_id, const Lighting &lighting) pkt << lighting.shadow_intensity; pkt << lighting.saturation; + pkt << lighting.exposure.luminance_min + << lighting.exposure.luminance_max + << lighting.exposure.exposure_correction + << lighting.exposure.speed_dark_bright + << lighting.exposure.speed_bright_dark + << lighting.exposure.center_weight_power; + Send(&pkt); }