1
0
mirror of https://github.com/luanti-org/luanti.git synced 2025-11-19 08:05:20 +01:00

Dynamic shadows with the ogles2 driver on OpenGL ES 3.0+ (#16661)

This commit is contained in:
grorp
2025-11-17 17:55:13 +01:00
committed by GitHub
parent ac0ebf39ad
commit fcd96e9244
22 changed files with 97 additions and 70 deletions

View File

@@ -355,10 +355,10 @@ local function check_requirements(name, requires, context)
return true
end
local video_driver = core.get_active_driver()
local touch_support = core.irrlicht_device_supports_touch()
local touch_controls = core.settings:get("touch_controls")
local touch_interaction_style = core.settings:get("touch_interaction_style")
local shadows_support = core.driver_supports_shadows()
local special = {
android = PLATFORM == "Android",
desktop = PLATFORM ~= "Android",
@@ -367,9 +367,8 @@ local function check_requirements(name, requires, context)
-- be used, so we show settings for both.
touchscreen = touch_support and (touch_controls == "auto" or core.is_yes(touch_controls)),
keyboard_mouse = not touch_support or (touch_controls == "auto" or not core.is_yes(touch_controls)),
opengl = (video_driver == "opengl" or video_driver == "opengl3"),
gles = video_driver:sub(1, 5) == "ogles",
touch_interaction_style_tap = touch_interaction_style ~= "buttons_crosshair",
shadows_support = shadows_support,
}
for req_key, req_value in pairs(requires) do

View File

@@ -69,7 +69,7 @@ end
return {
query_text = "Shadows",
requires = {
opengl = true,
shadows_support = true,
},
context = "client",
get_formspec = function(self, avail_w)

View File

@@ -91,8 +91,9 @@
# * The value of a boolean setting, such as enable_dynamic_shadows
# * An engine-defined value:
# * desktop / android
# * touch_support
# * touchscreen / keyboard_mouse
# * opengl / gles
# * shadows_support
# * You can negate any requirement by prepending with !
# * The "keyboard_mouse" requirement is automatically added to settings with the
# "key" type.
@@ -702,60 +703,60 @@ water_wave_speed (Waving liquids wave speed) float 5.0
# Set to true to enable Shadow Mapping.
#
# Requires: opengl
# Requires: shadows_support
enable_dynamic_shadows (Dynamic shadows) bool false
# Set the shadow strength gamma.
# Adjusts the intensity of in-game dynamic shadows.
# Lower value means lighter shadows, higher value means darker shadows.
#
# Requires: enable_dynamic_shadows, opengl
# Requires: enable_dynamic_shadows, shadows_support
shadow_strength_gamma (Shadow strength gamma) float 1.0 0.1 10.0
# Maximum distance to render shadows.
#
# Requires: enable_dynamic_shadows, opengl
# Requires: enable_dynamic_shadows, shadows_support
shadow_map_max_distance (Shadow map max distance in nodes to render shadows) float 140.0 10.0 1000.0
# Texture size to render the shadow map on.
# This must be a power of two.
# Bigger numbers create better shadows but it is also more expensive.
#
# Requires: enable_dynamic_shadows, opengl
# Requires: enable_dynamic_shadows, shadows_support
shadow_map_texture_size (Shadow map texture size) int 2048 128 8192
# Sets shadow texture quality to 32 bits.
# On false, 16 bits texture will be used.
# This can cause much more artifacts in the shadow.
#
# Requires: enable_dynamic_shadows, opengl
# Requires: enable_dynamic_shadows, shadows_support
shadow_map_texture_32bit (Shadow map texture in 32 bits) bool true
# Define shadow filtering quality.
# This simulates the soft shadows effect by applying a PCF or Poisson disk
# but also uses more resources.
#
# Requires: enable_dynamic_shadows, opengl
# Requires: enable_dynamic_shadows, shadows_support
shadow_filters (Shadow filter quality) enum 1 0,1,2
# Enable colored shadows for transculent nodes.
# This is expensive.
#
# Requires: enable_dynamic_shadows, opengl
# Requires: enable_dynamic_shadows, shadows_support
shadow_map_color (Colored shadows) bool false
# Set the soft shadow radius size.
# Lower values mean sharper shadows, bigger values mean softer shadows.
# Minimum value: 1.0; maximum value: 15.0
#
# Requires: enable_dynamic_shadows, opengl
# Requires: enable_dynamic_shadows, shadows_support
shadow_soft_radius (Soft shadow radius) float 5.0 1.0 15.0
# Set the default tilt of Sun/Moon orbit in degrees.
# Games may change orbit tilt via API.
# Value of 0 means no tilt / vertical orbit.
#
# Requires: enable_dynamic_shadows, opengl
# Requires: enable_dynamic_shadows, shadows_support
shadow_sky_body_orbit_tilt (Sky Body Orbit Tilt) float 0.0 -60.0 60.0
[**Post Processing]
@@ -2083,14 +2084,14 @@ post_processing_texture_bits (Color depth for post-processing texture) enum 16 8
# Enable Poisson disk filtering.
# On true uses Poisson disk to make "soft shadows". Otherwise uses PCF filtering.
#
# Requires: enable_dynamic_shadows, opengl
# Requires: enable_dynamic_shadows, shadows_support
shadow_poisson_filter (Poisson filtering) bool true
# Spread a complete update of the shadow map over a given number of frames.
# Higher values might make shadows laggy, lower values
# will consume more resources.
#
# Requires: enable_dynamic_shadows, opengl
# Requires: enable_dynamic_shadows, shadows_support
shadow_update_frames (Map shadows update frames) int 16 1 32
# Set to true to render debugging breakdown of the bloom effect.

View File

@@ -317,7 +317,7 @@ vec4 getShadowColor(sampler2D shadowsampler, vec2 smTexCoord, float realDistance
int samples = (1 + 1 * int(SOFTSHADOWRADIUS > 1.0)) * PCFSAMPLES; // scale max samples for the soft shadows
samples = int(clamp(pow(4.0 * radius + 1.0, 2.0), 1.0, float(samples)));
int init_offset = int(floor(mod(((smTexCoord.x * 34.0) + 1.0) * smTexCoord.y, 64.0-samples)));
int init_offset = int(floor(mod(((smTexCoord.x * 34.0) + 1.0) * smTexCoord.y, 64.0-float(samples))));
int end_offset = int(samples) + init_offset;
for (int x = init_offset; x < end_offset; x++) {
@@ -325,7 +325,7 @@ vec4 getShadowColor(sampler2D shadowsampler, vec2 smTexCoord, float realDistance
visibility += getHardShadowColor(shadowsampler, clampedpos.xy, realDistance);
}
return visibility / samples;
return visibility / float(samples);
}
#else
@@ -344,7 +344,7 @@ float getShadow(sampler2D shadowsampler, vec2 smTexCoord, float realDistance)
int samples = (1 + 1 * int(SOFTSHADOWRADIUS > 1.0)) * PCFSAMPLES; // scale max samples for the soft shadows
samples = int(clamp(pow(4.0 * radius + 1.0, 2.0), 1.0, float(samples)));
int init_offset = int(floor(mod(((smTexCoord.x * 34.0) + 1.0) * smTexCoord.y, 64.0-samples)));
int init_offset = int(floor(mod(((smTexCoord.x * 34.0) + 1.0) * smTexCoord.y, 64.0-float(samples))));
int end_offset = int(samples) + init_offset;
for (int x = init_offset; x < end_offset; x++) {
@@ -352,7 +352,7 @@ float getShadow(sampler2D shadowsampler, vec2 smTexCoord, float realDistance)
visibility += getHardShadow(shadowsampler, clampedpos.xy, realDistance);
}
return visibility / samples;
return visibility / float(samples);
}
#endif
@@ -511,8 +511,8 @@ void main(void)
// Apply self-shadowing when light falls at a narrow angle to the surface
// Cosine of the cut-off angle.
const float self_shadow_cutoff_cosine = 0.035;
if (f_normal_length != 0 && cosLight < self_shadow_cutoff_cosine) {
shadow_int = max(shadow_int, 1 - clamp(cosLight, 0.0, self_shadow_cutoff_cosine)/self_shadow_cutoff_cosine);
if (f_normal_length != 0.0 && cosLight < self_shadow_cutoff_cosine) {
shadow_int = max(shadow_int, 1.0 - clamp(cosLight, 0.0, self_shadow_cutoff_cosine)/self_shadow_cutoff_cosine);
shadow_color = mix(vec3(0.0), shadow_color, min(cosLight, self_shadow_cutoff_cosine)/self_shadow_cutoff_cosine);
#if (MATERIAL_TYPE == TILE_MATERIAL_WAVING_LEAVES || MATERIAL_TYPE == TILE_MATERIAL_WAVING_PLANTS)

View File

@@ -162,8 +162,8 @@ float getPenumbraRadius(sampler2D shadowsampler, vec2 smTexCoord, float realDist
// conversion factor from shadow depth to blur radius
float depth_to_blur = f_shadowfar / SOFTSHADOWRADIUS / xyPerspectiveBias0;
if (depth > 0.0 && f_normal_length > 0.0)
// 5 is empirical factor that controls how fast shadow loses sharpness
sharpness_factor = clamp(5 * depth * depth_to_blur, 0.0, 1.0);
// 5.0 is empirical factor that controls how fast shadow loses sharpness
sharpness_factor = clamp(5.0 * depth * depth_to_blur, 0.0, 1.0);
depth = 0.0;
float world_to_texture = xyPerspectiveBias1 / perspective_factor / perspective_factor
@@ -257,7 +257,7 @@ vec4 getShadowColor(sampler2D shadowsampler, vec2 smTexCoord, float realDistance
int samples = (1 + 1 * int(SOFTSHADOWRADIUS > 1.0)) * PCFSAMPLES; // scale max samples for the soft shadows
samples = int(clamp(pow(4.0 * radius + 1.0, 2.0), 1.0, float(samples)));
int init_offset = int(floor(mod(((smTexCoord.x * 34.0) + 1.0) * smTexCoord.y, 64.0-samples)));
int init_offset = int(floor(mod(((smTexCoord.x * 34.0) + 1.0) * smTexCoord.y, 64.0-float(samples))));
int end_offset = int(samples) + init_offset;
for (int x = init_offset; x < end_offset; x++) {
@@ -265,7 +265,7 @@ vec4 getShadowColor(sampler2D shadowsampler, vec2 smTexCoord, float realDistance
visibility += getHardShadowColor(shadowsampler, clampedpos.xy, realDistance);
}
return visibility / samples;
return visibility / float(samples);
}
#else
@@ -284,7 +284,7 @@ float getShadow(sampler2D shadowsampler, vec2 smTexCoord, float realDistance)
int samples = (1 + 1 * int(SOFTSHADOWRADIUS > 1.0)) * PCFSAMPLES; // scale max samples for the soft shadows
samples = int(clamp(pow(4.0 * radius + 1.0, 2.0), 1.0, float(samples)));
int init_offset = int(floor(mod(((smTexCoord.x * 34.0) + 1.0) * smTexCoord.y, 64.0-samples)));
int init_offset = int(floor(mod(((smTexCoord.x * 34.0) + 1.0) * smTexCoord.y, 64.0-float(samples))));
int end_offset = int(samples) + init_offset;
for (int x = init_offset; x < end_offset; x++) {
@@ -292,7 +292,7 @@ float getShadow(sampler2D shadowsampler, vec2 smTexCoord, float realDistance)
visibility += getHardShadow(shadowsampler, clampedpos.xy, realDistance);
}
return visibility / samples;
return visibility / float(samples);
}
#endif
@@ -426,8 +426,8 @@ void main(void)
// Apply self-shadowing when light falls at a narrow angle to the surface
// Cosine of the cut-off angle.
const float self_shadow_cutoff_cosine = 0.14;
if (f_normal_length != 0 && cosLight < self_shadow_cutoff_cosine) {
shadow_int = max(shadow_int, 1 - clamp(cosLight, 0.0, self_shadow_cutoff_cosine)/self_shadow_cutoff_cosine);
if (f_normal_length != 0.0 && cosLight < self_shadow_cutoff_cosine) {
shadow_int = max(shadow_int, 1.0 - clamp(cosLight, 0.0, self_shadow_cutoff_cosine)/self_shadow_cutoff_cosine);
shadow_color = mix(vec3(0.0), shadow_color, min(cosLight, self_shadow_cutoff_cosine)/self_shadow_cutoff_cosine);
}

View File

@@ -3,7 +3,7 @@
#else
uniform sampler2D baseTexture;
#endif
varying vec4 tPos;
VARYING_ vec4 tPos;
CENTROID_ VARYING_ mediump vec2 varTexCoord;
CENTROID_ VARYING_ float varTexLayer; // actually int

View File

@@ -1,6 +1,6 @@
uniform mat4 LightMVP; // world matrix
uniform vec4 CameraPos; // camera position
varying vec4 tPos;
VARYING_ vec4 tPos;
uniform float xyPerspectiveBias0;
uniform float xyPerspectiveBias1;

View File

@@ -3,13 +3,13 @@
#else
uniform sampler2D baseTexture;
#endif
varying vec4 tPos;
VARYING_ vec4 tPos;
CENTROID_ VARYING_ mediump vec2 varTexCoord;
CENTROID_ VARYING_ float varTexLayer; // actually int
#ifdef COLORED_SHADOWS
varying vec3 varColor;
VARYING_ vec3 varColor;
// c_precision of 128 fits within 7 base-10 digits
const float c_precision = 128.0;

View File

@@ -1,8 +1,8 @@
uniform mat4 LightMVP; // world matrix
uniform vec4 CameraPos;
varying vec4 tPos;
VARYING_ vec4 tPos;
#ifdef COLORED_SHADOWS
varying vec3 varColor;
VARYING_ vec3 varColor;
#endif
uniform float xyPerspectiveBias0;

View File

@@ -4,11 +4,7 @@ uniform sampler2D ShadowMapClientMapTraslucent;
#endif
uniform sampler2D ShadowMapSamplerdynamic;
#ifdef GL_ES
varying mediump vec2 varTexCoord;
#else
centroid varying vec2 varTexCoord;
#endif
CENTROID_ VARYING_ mediump vec2 varTexCoord;
void main()
{

View File

@@ -1,8 +1,4 @@
#ifdef GL_ES
varying mediump vec2 varTexCoord;
#else
centroid varying vec2 varTexCoord;
#endif
CENTROID_ VARYING_ mediump vec2 varTexCoord;
void main()
{

View File

@@ -133,6 +133,11 @@ enum E_VIDEO_DRIVER_FEATURE
//! Support for 2D array textures.
EVDF_TEXTURE_2D_ARRAY,
//! Support for floating-point textures as framebuffer color attachments.
// (The formats are a. color-renderable and b. part of the required framebuffer formats)
// (This refers to at least ECF_R16F, ECF_R32F, ECF_A16B16G16R16F and ECF_A32B32G32R32F)
EVDF_RENDER_TO_FLOAT_TEXTURE,
//! Only used for counting the elements of this enum
EVDF_COUNT
};

View File

@@ -580,6 +580,8 @@ bool COpenGLExtensionHandler::queryFeature(E_VIDEO_DRIVER_FEATURE feature) const
return FeatureAvailable[IRR_NV_depth_clamp] || FeatureAvailable[IRR_ARB_depth_clamp];
case EVDF_TEXTURE_MULTISAMPLE:
return (Version >= 302) || FeatureAvailable[IRR_ARB_texture_multisample];
case EVDF_RENDER_TO_FLOAT_TEXTURE:
return Version >= 300;
default:
return false;

View File

@@ -76,6 +76,8 @@ public:
return TextureMultisampleSupported;
case EVDF_TEXTURE_2D_ARRAY:
return Texture2DArraySupported;
case EVDF_RENDER_TO_FLOAT_TEXTURE:
return RenderToFloatTextureSupported;
default:
return false;
};
@@ -178,6 +180,7 @@ public:
bool TextureMultisampleSupported = false;
bool Texture2DArraySupported = false;
bool KHRDebugSupported = false;
bool RenderToFloatTextureSupported = false;
u32 MaxLabelLength = 0;
};

View File

@@ -69,10 +69,11 @@ void COpenGL3Driver::initFeatures()
LODBiasSupported = true;
BlendMinMaxSupported = true;
TextureMultisampleSupported = true;
Texture2DArraySupported = Version.Major >= 3 || queryExtension("GL_EXT_texture_array");
Texture2DArraySupported = true;
KHRDebugSupported = isVersionAtLeast(4, 6) || queryExtension("GL_KHR_debug");
if (KHRDebugSupported)
MaxLabelLength = GetInteger(GL.MAX_LABEL_LENGTH);
RenderToFloatTextureSupported = true;
// COGLESCoreExtensionHandler::Feature
static_assert(MATERIAL_MAX_TEXTURES <= 16, "Only up to 16 textures are guaranteed");

View File

@@ -43,6 +43,7 @@ void COpenGLES2Driver::initFeatures()
if (Version.Major >= 3) {
// NOTE floating-point formats may not be suitable for render targets.
// See also EVDF_RENDER_TO_FLOAT_TEXTURE.
TextureFormats[ECF_A1R5G5B5] = {GL_RGB5_A1, GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1, CColorConverter::convert_A1R5G5B5toR5G5B5A1};
TextureFormats[ECF_R5G6B5] = {GL_RGB565, GL_RGB, GL_UNSIGNED_SHORT_5_6_5};
TextureFormats[ECF_R8G8B8] = {GL_RGB8, GL_RGB, GL_UNSIGNED_BYTE};
@@ -126,6 +127,7 @@ void COpenGLES2Driver::initFeatures()
KHRDebugSupported = queryExtension("GL_KHR_debug");
if (KHRDebugSupported)
MaxLabelLength = GetInteger(GL.MAX_LABEL_LENGTH);
RenderToFloatTextureSupported = isVersionAtLeast(3, 2)|| queryExtension("GL_EXT_color_buffer_float");
// COGLESCoreExtensionHandler::Feature
static_assert(MATERIAL_MAX_TEXTURES <= 8, "Only up to 8 textures are guaranteed");

View File

@@ -35,7 +35,6 @@ ShadowRenderer::ShadowRenderer(IrrlichtDevice *device, Client *client) :
m_shadow_map_texture_32bit = g_settings->getBool("shadow_map_texture_32bit");
m_shadow_map_colored = g_settings->getBool("shadow_map_color");
m_shadow_samples = g_settings->getS32("shadow_filters");
m_map_shadow_update_frames = g_settings->getS16("shadow_update_frames");
m_screen_quad = new ShadowScreenQuad();
@@ -525,10 +524,6 @@ void ShadowRenderer::renderShadowObjects(
} // end for caster shadow nodes
}
void ShadowRenderer::mixShadowsQuad()
{
}
void ShadowRenderer::createShaders()
{
auto *shdsrc = m_client->getShaderSource();
@@ -586,9 +581,7 @@ std::unique_ptr<ShadowRenderer> createShadowRenderer(IrrlichtDevice *device, Cli
return nullptr;
// disable if unsupported
// See also checks in builtin/mainmenu/settings/dlg_settings.lua
const video::E_DRIVER_TYPE type = device->getVideoDriver()->getDriverType();
if (type != video::EDT_OPENGL && type != video::EDT_OPENGL3) {
if (!ShadowRenderer::isSupported(device)) {
warningstream << "Shadows: disabled dynamic shadows due to being unsupported" << std::endl;
g_settings->setBool("enable_dynamic_shadows", false);
return nullptr;
@@ -598,3 +591,19 @@ std::unique_ptr<ShadowRenderer> createShadowRenderer(IrrlichtDevice *device, Cli
shadow_renderer->initialize();
return shadow_renderer;
}
bool ShadowRenderer::isSupported(IrrlichtDevice *device)
{
auto driver = device->getVideoDriver();
const video::E_DRIVER_TYPE type = driver->getDriverType();
v2s32 glver = driver->getLimits().GLVersion;
if (type != video::EDT_OPENGL && type != video::EDT_OPENGL3 &&
!(type == video::EDT_OGLES2 && glver.X >= 3))
return false;
if (!driver->queryFeature(video::EVDF_RENDER_TO_FLOAT_TEXTURE))
return false;
return true;
}

View File

@@ -85,7 +85,6 @@ public:
void setShadowIntensity(float shadow_intensity);
void setShadowTint(video::SColor shadow_tint) { m_shadow_tint = shadow_tint; }
s32 getShadowSamples() const { return m_shadow_samples; }
float getShadowStrength() const { return m_shadows_enabled ? m_shadow_strength : 0.0f; }
video::SColor getShadowTint() const { return m_shadow_tint; }
float getTimeOfDay() const { return m_time_day; }
@@ -93,6 +92,8 @@ public:
f32 getPerspectiveBiasXY() { return m_perspective_bias_xy; }
f32 getPerspectiveBiasZ() { return m_perspective_bias_z; }
static bool isSupported(IrrlichtDevice *device);
private:
video::ITexture *getSMTexture(const std::string &shadow_map_name,
video::ECOLOR_FORMAT texture_format,
@@ -102,7 +103,6 @@ private:
scene::E_SCENE_NODE_RENDER_PASS pass =
scene::ESNRP_SOLID);
void renderShadowObjects(video::ITexture *target, DirectionalLight &light);
void mixShadowsQuad();
void updateSMTextures();
void disable();
@@ -127,7 +127,6 @@ private:
float m_shadow_map_max_distance;
u32 m_shadow_map_texture_size;
float m_time_day;
int m_shadow_samples;
bool m_shadow_map_texture_32bit;
bool m_shadows_enabled;
bool m_shadows_supported;

View File

@@ -336,15 +336,24 @@ void set_default_settings()
// Effects Shadows
settings->setDefault("enable_dynamic_shadows", "false");
settings->setDefault("shadow_strength_gamma", "1.0");
settings->setDefault("shadow_map_max_distance", "140.0");
settings->setDefault("shadow_map_texture_size", "2048");
settings->setDefault("shadow_map_texture_32bit", "true");
settings->setDefault("shadow_map_color", "false");
settings->setDefault("shadow_filters", "1");
settings->setDefault("shadow_poisson_filter", "true");
settings->setDefault("shadow_update_frames", "16");
settings->setDefault("shadow_soft_radius", "5.0");
settings->setDefault("shadow_sky_body_orbit_tilt", "0.0");
#ifndef __ANDROID__
// equivalent to "Medium" preset
// see "shadows_component.lua"
settings->setDefault("shadow_map_max_distance", "140.0");
settings->setDefault("shadow_map_texture_size", "2048");
settings->setDefault("shadow_filters", "1");
#else
// equivalent to "Low" preset
settings->setDefault("shadow_map_max_distance", "93.0");
settings->setDefault("shadow_map_texture_size", "1024");
settings->setDefault("shadow_filters", "0");
#endif
settings->setDefault("shadow_map_texture_32bit", "true");
settings->setDefault("shadow_map_color", "false");
// Input
settings->setDefault("invert_mouse", "false");

View File

@@ -6,6 +6,7 @@
#include "l_menu_common.h"
#include "client/renderingengine.h"
#include "client/shadows/dynamicshadowsrender.h"
#include "gettext.h"
#include "lua_api/l_internal.h"
@@ -28,6 +29,13 @@ int ModApiMenuCommon::l_get_active_driver(lua_State *L)
}
int ModApiMenuCommon::l_driver_supports_shadows(lua_State *L)
{
lua_pushboolean(L, ShadowRenderer::isSupported(RenderingEngine::get_raw_device()));
return 1;
}
int ModApiMenuCommon::l_irrlicht_device_supports_touch(lua_State *L)
{
lua_pushboolean(L, RenderingEngine::get_raw_device()->supportsTouchEvents());
@@ -47,6 +55,7 @@ void ModApiMenuCommon::Initialize(lua_State *L, int top)
{
API_FCT(gettext);
API_FCT(get_active_driver);
API_FCT(driver_supports_shadows);
API_FCT(irrlicht_device_supports_touch);
API_FCT(normalize_keycode);
}

View File

@@ -12,6 +12,7 @@ class ModApiMenuCommon: public ModApiBase
private:
static int l_gettext(lua_State *L);
static int l_get_active_driver(lua_State *L);
static int l_driver_supports_shadows(lua_State *L);
static int l_irrlicht_device_supports_touch(lua_State *L);
static int l_normalize_keycode(lua_State *L);

View File

@@ -14,20 +14,15 @@ rm -rf "$worldpath"
mkdir -p "$worldpath/worldmods"
# enable a lot of visual effects so we can catch shader errors and other obvious bugs
opts1=(
opts=(
screen_w=384 screen_h=256 fps_max=5
active_block_range=1 viewing_range=40 helper_mode=devtest
opengl_debug=true mip_map=true enable_waving_{leaves,plants,water}=true
antialiasing=ssaa node_highlighting=halo
)
opts2=(
enable_{auto_exposure,bloom,dynamic_shadows,translucent_foliage,volumetric_lighting,water_reflections}=true
shadow_map_color=true
)
printf '%s\n' "${opts1[@]}" "${clientconf:-}" >"$conf_client"
if ! grep -q 'video_driver *= *ogles2' "$conf_client"; then # no shadows on GLES
printf '%s\n' "${opts2[@]}" >>"$conf_client"
fi
printf '%s\n' "${opts[@]}" "${clientconf:-}" >"$conf_client"
ln -s "$dir/helper_mod" "$worldpath/worldmods/"