Add GUI scale setting and volume contrast setting

GUI scale (#737):
- add display.gui-scale setting (0 - auto, 1..4 - fixed integer scale)
- auto mode picks the largest scale keeping at least 640x480 UI units
- apply scale in GUI (root size, camera, cursor, tooltips, scissors)
- HUD layout and ModelViewer use scaled coordinates
- add GUI Scale trackbar to display settings

Volume contrast (#399):
- add audio.volume-contrast setting (100% - unchanged sounds,
  0% - loud sounds are lowered to the level of quiet ones)
- loudness (RMS) of a sound is calculated once and cached
- add trackbar to audio settings and reset page
This commit is contained in:
loki5512344 2026-10-03 12:56:54 +00:00
parent dacebf94bc
commit 708c276787
14 changed files with 158 additions and 17 deletions

View file

@ -1,5 +1,6 @@
#include "audio.hpp"
#include <cmath>
#include <iostream>
#include <stdexcept>
#include <utility>
@ -25,6 +26,64 @@ namespace {
util::ObjectsKeeper objects_keeper {};
std::unique_ptr<InputDevice> input_device = nullptr;
static bool input_enabled = false;
/// @brief Volume contrast setting value (1.0 - no compression)
float volume_contrast = 1.0f;
/// @brief RMS level (0..1) that louder sounds are lowered to
/// when contrast is 0
constexpr float REFERENCE_RMS = 0.1f;
struct LoudnessEntry {
std::weak_ptr<PCM> pcm;
float rms;
};
std::unordered_map<const Sound*, LoudnessEntry> loudness_cache;
}
/// @brief Calculate root mean square level of PCM data
/// @return level in range [0.0, 1.0]
static float calc_rms(const PCM& pcm) {
double sum = 0.0;
size_t count = 0;
if (pcm.bitsPerSample == 16) {
count = pcm.data.size() / sizeof(int16_t);
auto samples = reinterpret_cast<const int16_t*>(pcm.data.data());
for (size_t i = 0; i < count; i++) {
double value = samples[i] / 32768.0;
sum += value * value;
}
} else {
count = pcm.data.size();
for (size_t i = 0; i < count; i++) {
double value = (static_cast<uint8_t>(pcm.data[i]) - 128) / 128.0;
sum += value * value;
}
}
return count ? static_cast<float>(std::sqrt(sum / count)) : 0.0f;
}
/// @brief Get volume multiplier for a sound according to volume contrast
/// setting. Loud sounds are lowered, quiet ones are never amplified
static float get_contrast_gain(const Sound* sound) {
if (volume_contrast >= 1.0f) {
return 1.0f;
}
auto pcm = sound->getPCM();
if (pcm == nullptr) {
return 1.0f;
}
float rms;
auto found = loudness_cache.find(sound);
if (found != loudness_cache.end() && found->second.pcm.lock() == pcm) {
rms = found->second.rms;
} else {
rms = calc_rms(*pcm);
loudness_cache[sound] = {pcm, rms};
}
if (rms <= REFERENCE_RMS) {
return 1.0f;
}
return std::pow(REFERENCE_RMS / rms, 1.0f - volume_contrast);
}
Channel::Channel(std::string name, bool effects)
@ -196,6 +255,9 @@ void audio::initialize(
audio::get_channel(channel.name)->setVolume(value * value);
}, true));
}
objects_keeper.keepAlive(settings.volumeContrast.observe([](auto value) {
volume_contrast = value;
}, true));
objects_keeper.keepAlive(settings.acousticEffects.observe([=](bool value) {
if (value) return;
backend->setAcoustics(audio::Acoustics {});
@ -395,7 +457,7 @@ speakerid_t audio::play(
speakerid_t id = nextId++;
speakers.try_emplace(id, std::move(speaker_ptr));
speaker->setPosition(position);
speaker->setVolume(volume);
speaker->setVolume(volume * get_contrast_gain(sound));
speaker->setPitch(pitch);
speaker->setLoop(loop);
speaker->setRelative(relative);

View file

@ -370,7 +370,7 @@ void Hud::update(bool visible) {
element.getNode()->setVisible(visible);
}
const auto& windowSize = engine.getWindow().getSize();
const auto windowSize = gui.getContainer()->getSize();
glm::vec2 caSize = contentAccessPanel->getSize();
contentAccessPanel->setVisible(inventoryView != nullptr && showContentPanel);
contentAccessPanel->setSize(glm::vec2(caSize.x, windowSize.y));
@ -626,7 +626,8 @@ void Hud::setDebug(bool flag) {
void Hud::draw(const DrawContext& ctx){
const auto& viewport = ctx.getViewport();
updateElementsPosition(viewport);
// UI layout is calculated in GUI units (window size / GUI scale)
updateElementsPosition(gui.getContainer()->getSize());
uicamera->setFov(viewport.y);
uicamera->setAspectRatio(viewport.x / static_cast<float>(viewport.y));
@ -653,7 +654,7 @@ void Hud::draw(const DrawContext& ctx){
}
}
void Hud::updateElementsPosition(const glm::uvec2& viewport) {
void Hud::updateElementsPosition(const glm::vec2& viewport) {
if (inventoryOpen) {
float caWidth = inventoryView && showContentPanel
? contentAccess->getSize().x
@ -698,7 +699,7 @@ void Hud::updateElementsPosition(const glm::uvec2& viewport) {
}
}
if (exchangeSlot != nullptr) {
exchangeSlot->setPos(input.getCursor().pos);
exchangeSlot->setPos(gui.getCursor().pos);
}
hotbarView->setPos(glm::vec2(viewport.x / 2, viewport.y - 65));
hotbarView->setSelected(player.getChosenSlot());

View file

@ -130,7 +130,7 @@ class Hud : public util::ObjectsKeeper {
std::shared_ptr<gui::InventoryView> createHotbar();
void processInput(bool visible);
void updateElementsPosition(const glm::uvec2& viewport);
void updateElementsPosition(const glm::vec2& viewport);
void updateHotbarControl();
void cleanup();

View file

@ -157,8 +157,12 @@ void DrawContext::setBlendMode(BlendMode mode) {
set_blend_mode(mode);
}
void DrawContext::setUiScale(float scale) {
uiScale = scale;
}
void DrawContext::setScissors(const glm::vec4& area) {
window.pushScissor(area);
window.pushScissor(area * uiScale);
scissorsCount++;
}

View file

@ -24,6 +24,7 @@ class DrawContext {
BlendMode blendMode = BlendMode::normal;
int scissorsCount = 0;
float lineWidth = 1.0f;
float uiScale = 1.0f;
std::array<const Bindable*, advanced_pipeline::TARGETS_COUNT> textures {};
public:
DrawContext(
@ -46,5 +47,9 @@ public:
void setBlendMode(BlendMode mode);
void setScissors(const glm::vec4& area);
void setLineWidth(float width);
/// @brief Set scale of UI coordinates (UI units -> window pixels).
/// Affects setScissors only
void setUiScale(float scale);
void useTexture(int target, const Bindable* texture);
};

View file

@ -88,13 +88,41 @@ void GUI::onAssetsLoad(Assets* assets) {
assets->store(rootDocument, "core:root");
}
/// @brief Minimal virtual window size used to choose the automatic scale
static constexpr glm::uvec2 AUTO_SCALE_MIN_SIZE {640, 480};
int GUI::calcScale(const glm::uvec2& viewport) const {
int value = engine.getSettings().display.guiScale.get();
if (value > 0) {
return value;
}
int result = 1;
while (result < 4 &&
viewport.x / (result + 1) >= AUTO_SCALE_MIN_SIZE.x &&
viewport.y / (result + 1) >= AUTO_SCALE_MIN_SIZE.y) {
result++;
}
return result;
}
int GUI::getScale() const {
return scale;
}
CursorState GUI::getCursor() const {
auto cursor = input.getCursor();
cursor.pos /= static_cast<float>(scale);
cursor.delta /= static_cast<float>(scale);
return cursor;
}
void GUI::resetTooltip() {
tooltipTimer = 0.0f;
tooltip->setVisible(false);
}
void GUI::updateTooltip(float delta) {
const auto& cursor = input.getCursor();
const auto cursor = getCursor();
if (hover == nullptr || !hover->isInside(cursor.pos)) {
return resetTooltip();
}
@ -229,7 +257,7 @@ void GUI::actFocused() {
focus->keyPressed(key);
}
const auto& cursor = input.getCursor();
const auto cursor = getCursor();
if (!cursor.locked) {
if (input.clicked(Mousecode::BUTTON_1) &&
(input.jclicked(Mousecode::BUTTON_1) || cursor.delta.x ||
@ -242,7 +270,8 @@ void GUI::actFocused() {
}
void GUI::act(float delta, const glm::uvec2& vp) {
container->setSize(vp);
scale = calcScale(vp);
container->setSize(glm::vec2(vp) / static_cast<float>(scale));
for (auto& pair : frames) {
pair.second->act(delta);
}
@ -250,7 +279,7 @@ void GUI::act(float delta, const glm::uvec2& vp) {
updateTooltip(delta);
const auto& cursor = input.getCursor();
const auto cursor = getCursor();
if (!cursor.locked && activeFrame) {
actMouse(*activeFrame, delta, cursor);
} else {
@ -278,8 +307,11 @@ void GUI::postAct() {
void GUI::draw(const DrawContext& pctx, Assets& assets) {
auto ctx = pctx.sub(batch2D.get());
ctx.setUiScale(static_cast<float>(scale));
auto& viewport = ctx.getViewport();
// viewport in UI units
const glm::vec2 viewport =
glm::vec2(ctx.getViewport()) / static_cast<float>(scale);
auto& page = menu->getCurrent();
if (page.panel) {
@ -289,9 +321,9 @@ void GUI::draw(const DrawContext& pctx, Assets& assets) {
panel->cropToContent();
}
}
menu->setPos((glm::vec2(viewport) - menu->getSize()) / 2.0f);
menu->setPos((viewport - menu->getSize()) / 2.0f);
uicamera->setFov(viewport.y);
uicamera->setAspectRatio(viewport.x / static_cast<float>(viewport.y));
uicamera->setAspectRatio(viewport.x / viewport.y);
auto uishader = assets.get<Shader>("ui");
uishader->use();

View file

@ -86,12 +86,18 @@ namespace gui {
std::unordered_map<std::string, std::shared_ptr<Frame>> frames;
vec2supplier cursorLocator;
/// @brief Current GUI scale (UI units -> window pixels)
int scale = 1;
float tooltipTimer = 0.0f;
float doubleClickTimer = 0.0f;
float doubleClickDelay = 0.5f;
bool doubleClicked = false;
bool debug = false;
int calcScale(const glm::uvec2& viewport) const;
CursorState getScaledCursor() const;
void actMouse(Frame& frame, float delta, const CursorState& cursor);
void actFocused();
void updateTooltip(float delta);
@ -125,6 +131,12 @@ namespace gui {
void postAct();
/// @brief Get current GUI scale: window pixels per one UI unit
int getScale() const;
/// @brief Get cursor state in UI coordinates
CursorState getCursor() const;
/// @brief Add element to the main container
/// @param node UI element
void add(std::shared_ptr<UINode> node);

View file

@ -57,7 +57,7 @@ void ModelViewer::act(float delta) {
grabbing = true;
}
if (grabbing && input.clicked(Mousecode::BUTTON_3)) {
auto cursor = input.getCursor();
auto cursor = gui.getCursor();
if (input.pressed(Keycode::LEFT_SHIFT)) {
center -= camera.right * (cursor.delta.x / size.x) * distance;
center += camera.up * (cursor.delta.y / size.y) * distance;

View file

@ -44,6 +44,7 @@ SettingsHandler::SettingsHandler(EngineSettings& settings) {
builder.add("volume-ui", &settings.audio.volumeUI);
builder.add("volume-ambient", &settings.audio.volumeAmbient);
builder.add("volume-music", &settings.audio.volumeMusic);
builder.add("volume-contrast", &settings.audio.volumeContrast);
builder.add("input-device", &settings.audio.inputDevice);
builder.add("acoustic-effects", &settings.audio.acousticEffects);
@ -55,6 +56,7 @@ SettingsHandler::SettingsHandler(EngineSettings& settings) {
builder.add("limit-fps-iconified", &settings.display.limitFpsIconified);
builder.add("window-mode", &settings.display.windowMode);
builder.add("adaptive-menu-fps", &settings.display.adaptiveFpsInMenu);
builder.add("gui-scale", &settings.display.guiScale);
builder.addSection("camera");
builder.add("sensitivity", &settings.camera.sensitivity);

View file

@ -17,6 +17,9 @@ struct AudioSettings {
NumberSetting volumeUI {1.0f, 0.0f, 1.0f, setting_format::percent};
NumberSetting volumeAmbient {1.0f, 0.0f, 1.0f, setting_format::percent};
NumberSetting volumeMusic {1.0f, 0.0f, 1.0f, setting_format::percent};
/// @brief Volume contrast: 100% - sounds are played as is,
/// 0% - loud sounds are lowered to the level of quiet ones ("night mode")
NumberSetting volumeContrast {1.0f, 0.0f, 1.0f, setting_format::percent};
StringSetting inputDevice {""};
@ -38,6 +41,8 @@ struct DisplaySettings {
FlagSetting limitFpsIconified {false};
/// @brief Adaptive framerate in menu (experimental)
FlagSetting adaptiveFpsInMenu {false};
/// @brief GUI scale factor (0 - auto, 1..4 - fixed integer scale)
IntegerSetting guiScale {0, 0, 4};
};
struct ChunksSettings {