Skip to content
Permalink
Browse files
Distribute shadow map update over multiple frames to reduce stutter (#…
…11422)

Reduces stutter and freezes when playing.

 * Maintains double SM and SM Color textures
 * Light frustum update triggers incremental generation of shadow map into secondary 'future' textures.
 * Every incremental update renders a portion of the shadow draw list (split equally).
 * After defined number of frames (currently, 4), 'future' and 'current' textures are swapped, and DirectionalLight 'commits' the new frustum to use when rendering shadows on screen.

Co-authored-by: sfan5 <sfan5@live.de>
  • Loading branch information
x2048 and sfan5 committed Jul 25, 2021
1 parent ff2d2a6 commit bf3acbf388406f736286d990adb5f35a9023c390
@@ -618,11 +618,11 @@ shadow_filters (Shadow filter quality) enum 1 0,1,2
# On true translucent nodes cast colored shadows. This is expensive.
shadow_map_color (Colored shadows) bool false


# Set the shadow update time, in seconds.
# Lower value means shadows and map updates faster, but it consumes more resources.
# Minimum value: 0.001; maximum value: 0.2
shadow_update_time (Map update time) float 0.2 0.001 0.2
# Spread a complete update of shadow map over given amount of frames.
# Higher values might make shadows laggy, lower values
# will consume more resources.
# Minimum value: 1; maximum value: 16
shadow_update_frames (Map shadows update frames) int 8 1 16

# Set the soft shadow radius size.
# Lower values mean sharper shadows, bigger values mean softer shadows.
@@ -197,7 +197,7 @@ float getPenumbraRadius(sampler2D shadowsampler, vec2 smTexCoord, float realDist
float pointDepth;
float maxRadius = SOFTSHADOWRADIUS * 5.0 * multiplier;

float bound = clamp(PCFBOUND * (1 - baseLength), 0.5, PCFBOUND);
float bound = clamp(PCFBOUND * (1 - baseLength), 0.0, PCFBOUND);
int n = 0;

for (y = -bound; y <= bound; y += 1.0)
@@ -304,7 +304,7 @@ vec4 getShadowColor(sampler2D shadowsampler, vec2 smTexCoord, float realDistance
float perspectiveFactor;

float texture_size = 1.0 / (f_textureresolution * 0.5);
int samples = int(clamp(PCFSAMPLES * (1 - baseLength) * (1 - baseLength), 1, PCFSAMPLES));
int samples = int(clamp(PCFSAMPLES * (1 - baseLength) * (1 - baseLength), PCFSAMPLES / 4, PCFSAMPLES));
int init_offset = int(floor(mod(((smTexCoord.x * 34.0) + 1.0) * smTexCoord.y, 64.0-samples)));
int end_offset = int(samples) + init_offset;

@@ -334,7 +334,7 @@ float getShadow(sampler2D shadowsampler, vec2 smTexCoord, float realDistance)
float perspectiveFactor;

float texture_size = 1.0 / (f_textureresolution * 0.5);
int samples = int(clamp(PCFSAMPLES * (1 - baseLength) * (1 - baseLength), 1, PCFSAMPLES));
int samples = int(clamp(PCFSAMPLES * (1 - baseLength) * (1 - baseLength), PCFSAMPLES / 4, PCFSAMPLES));
int init_offset = int(floor(mod(((smTexCoord.x * 34.0) + 1.0) * smTexCoord.y, 64.0-samples)));
int end_offset = int(samples) + init_offset;

@@ -370,7 +370,7 @@ vec4 getShadowColor(sampler2D shadowsampler, vec2 smTexCoord, float realDistance

float texture_size = 1.0 / (f_textureresolution * 0.5);
float y, x;
float bound = clamp(PCFBOUND * (1 - baseLength), 0.5, PCFBOUND);
float bound = clamp(PCFBOUND * (1 - baseLength), PCFBOUND / 2, PCFBOUND);
int n = 0;

// basic PCF filter
@@ -402,7 +402,7 @@ float getShadow(sampler2D shadowsampler, vec2 smTexCoord, float realDistance)

float texture_size = 1.0 / (f_textureresolution * 0.5);
float y, x;
float bound = clamp(PCFBOUND * (1 - baseLength), 0.5, PCFBOUND);
float bound = clamp(PCFBOUND * (1 - baseLength), PCFBOUND / 2, PCFBOUND);
int n = 0;

// basic PCF filter
@@ -636,7 +636,7 @@ void ClientMap::PrintInfo(std::ostream &out)
}

void ClientMap::renderMapShadows(video::IVideoDriver *driver,
const video::SMaterial &material, s32 pass)
const video::SMaterial &material, s32 pass, int frame, int total_frames)
{
bool is_transparent_pass = pass != scene::ESNRP_SOLID;
std::string prefix;
@@ -650,7 +650,23 @@ void ClientMap::renderMapShadows(video::IVideoDriver *driver,

MeshBufListList drawbufs;

int count = 0;
int low_bound = is_transparent_pass ? 0 : m_drawlist_shadow.size() / total_frames * frame;
int high_bound = is_transparent_pass ? m_drawlist_shadow.size() : m_drawlist_shadow.size() / total_frames * (frame + 1);

// transparent pass should be rendered in one go
if (is_transparent_pass && frame != total_frames - 1) {
return;
}

for (auto &i : m_drawlist_shadow) {
// only process specific part of the list & break early
++count;
if (count <= low_bound)
continue;
if (count > high_bound)
break;

v3s16 block_pos = i.first;
MapBlock *block = i.second;

@@ -705,6 +721,7 @@ void ClientMap::renderMapShadows(video::IVideoDriver *driver,
local_material.MaterialType = material.MaterialType;
local_material.BackfaceCulling = material.BackfaceCulling;
local_material.FrontfaceCulling = material.FrontfaceCulling;
local_material.BlendOperation = material.BlendOperation;
local_material.Lighting = false;
driver->setMaterial(local_material);

@@ -720,6 +737,12 @@ void ClientMap::renderMapShadows(video::IVideoDriver *driver,
}
}

// restore the driver material state
video::SMaterial clean;
clean.BlendOperation = video::EBO_ADD;
driver->setMaterial(clean); // reset material to defaults
driver->draw3DLine(v3f(), v3f(), video::SColor(0));

g_profiler->avg(prefix + "draw meshes [ms]", draw.stop(true));
g_profiler->avg(prefix + "vertices drawn [#]", vertex_count);
g_profiler->avg(prefix + "drawcalls [#]", drawcall_count);
@@ -125,7 +125,7 @@ class ClientMap : public Map, public scene::ISceneNode
void renderMap(video::IVideoDriver* driver, s32 pass);

void renderMapShadows(video::IVideoDriver *driver,
const video::SMaterial &material, s32 pass);
const video::SMaterial &material, s32 pass, int frame, int total_frames);

int getBackgroundBrightness(float max_d, u32 daylight_factor,
int oldvalue, bool *sunlight_seen_result);
@@ -609,7 +609,6 @@ struct GameRunData {
float jump_timer;
float damage_flash;
float update_draw_list_timer;
float update_shadows_timer;

f32 fog_range;

@@ -3881,10 +3880,8 @@ void Game::updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime,
changed much
*/
runData.update_draw_list_timer += dtime;
runData.update_shadows_timer += dtime;

float update_draw_list_delta = 0.2f;
bool draw_list_updated = false;

v3f camera_direction = camera->getDirection();
if (runData.update_draw_list_timer >= update_draw_list_delta
@@ -3894,18 +3891,10 @@ void Game::updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime,
runData.update_draw_list_timer = 0;
client->getEnv().getClientMap().updateDrawList();
runData.update_draw_list_last_cam_dir = camera_direction;
draw_list_updated = true;
}

if (ShadowRenderer *shadow = RenderingEngine::get_shadow_renderer()) {
update_draw_list_delta = shadow->getUpdateDelta();

if (m_camera_offset_changed ||
(runData.update_shadows_timer > update_draw_list_delta &&
(!draw_list_updated || shadow->getDirectionalLightCount() == 0))) {
runData.update_shadows_timer = 0;
updateShadows();
}
if (RenderingEngine::get_shadow_renderer()) {
updateShadows();
}

m_game_ui->update(*stats, client, draw_control, cam, runData.pointed_old, gui_chat_console, dtime);
@@ -4062,7 +4051,7 @@ void Game::updateShadows()
shadow->getDirectionalLight().setDirection(sun_pos);
shadow->setTimeOfDay(in_timeofday);

shadow->getDirectionalLight().update_frustum(camera, client);
shadow->getDirectionalLight().update_frustum(camera, client, m_camera_offset_changed);
}

/****************************************************************************
@@ -38,8 +38,8 @@ void DirectionalLight::createSplitMatrices(const Camera *cam)
float tanFovX = tanf(cam->getFovX() * 0.5f);

// adjusted frustum boundaries
float sfNear = shadow_frustum.zNear;
float sfFar = adjustDist(shadow_frustum.zFar, cam->getFovY());
float sfNear = future_frustum.zNear;
float sfFar = adjustDist(future_frustum.zFar, cam->getFovY());

// adjusted camera positions
v3f camPos2 = cam->getPosition();
@@ -87,14 +87,15 @@ void DirectionalLight::createSplitMatrices(const Camera *cam)
v3f eye_displacement = direction * vvolume;

// we must compute the viewmat with the position - the camera offset
// but the shadow_frustum position must be the actual world position
// but the future_frustum position must be the actual world position
v3f eye = frustumCenter - eye_displacement;
shadow_frustum.position = world_center - eye_displacement;
shadow_frustum.length = vvolume;
shadow_frustum.ViewMat.buildCameraLookAtMatrixLH(eye, frustumCenter, v3f(0.0f, 1.0f, 0.0f));
shadow_frustum.ProjOrthMat.buildProjectionMatrixOrthoLH(shadow_frustum.length,
shadow_frustum.length, -shadow_frustum.length,
shadow_frustum.length,false);
future_frustum.position = world_center - eye_displacement;
future_frustum.length = vvolume;
future_frustum.ViewMat.buildCameraLookAtMatrixLH(eye, frustumCenter, v3f(0.0f, 1.0f, 0.0f));
future_frustum.ProjOrthMat.buildProjectionMatrixOrthoLH(future_frustum.length,
future_frustum.length, -future_frustum.length,
future_frustum.length,false);
future_frustum.camera_offset = cam->getOffset();
}

DirectionalLight::DirectionalLight(const u32 shadowMapResolution,
@@ -104,23 +105,44 @@ DirectionalLight::DirectionalLight(const u32 shadowMapResolution,
farPlane(farValue), mapRes(shadowMapResolution), pos(position)
{}

void DirectionalLight::update_frustum(const Camera *cam, Client *client)
void DirectionalLight::update_frustum(const Camera *cam, Client *client, bool force)
{
should_update_map_shadow = true;
if (dirty && !force)
return;

float zNear = cam->getCameraNode()->getNearValue();
float zFar = getMaxFarValue();

///////////////////////////////////
// update splits near and fars
shadow_frustum.zNear = zNear;
shadow_frustum.zFar = zFar;
future_frustum.zNear = zNear;
future_frustum.zFar = zFar;

// update shadow frustum
createSplitMatrices(cam);
// get the draw list for shadows
client->getEnv().getClientMap().updateDrawListShadow(
getPosition(), getDirection(), shadow_frustum.length);
getPosition(), getDirection(), future_frustum.length);
should_update_map_shadow = true;
dirty = true;

// when camera offset changes, adjust the current frustum view matrix to avoid flicker
v3s16 cam_offset = cam->getOffset();
if (cam_offset != shadow_frustum.camera_offset) {
v3f rotated_offset;
shadow_frustum.ViewMat.rotateVect(rotated_offset, intToFloat(cam_offset - shadow_frustum.camera_offset, BS));
shadow_frustum.ViewMat.setTranslation(shadow_frustum.ViewMat.getTranslation() + rotated_offset);
shadow_frustum.camera_offset = cam_offset;
}
}

void DirectionalLight::commitFrustum()
{
if (!dirty)
return;

shadow_frustum = future_frustum;
dirty = false;
}

void DirectionalLight::setDirection(v3f dir)
@@ -144,6 +166,16 @@ const m4f &DirectionalLight::getProjectionMatrix() const
return shadow_frustum.ProjOrthMat;
}

const m4f &DirectionalLight::getFutureViewMatrix() const
{
return future_frustum.ViewMat;
}

const m4f &DirectionalLight::getFutureProjectionMatrix() const
{
return future_frustum.ProjOrthMat;
}

m4f DirectionalLight::getViewProjMatrix()
{
return shadow_frustum.ProjOrthMat * shadow_frustum.ViewMat;
@@ -34,6 +34,7 @@ struct shadowFrustum
core::matrix4 ProjOrthMat;
core::matrix4 ViewMat;
v3f position;
v3s16 camera_offset;
};

class DirectionalLight
@@ -47,7 +48,7 @@ class DirectionalLight

//DISABLE_CLASS_COPY(DirectionalLight)

void update_frustum(const Camera *cam, Client *client);
void update_frustum(const Camera *cam, Client *client, bool force = false);

// when set direction is updated to negative normalized(direction)
void setDirection(v3f dir);
@@ -59,6 +60,8 @@ class DirectionalLight
/// Gets the light's matrices.
const core::matrix4 &getViewMatrix() const;
const core::matrix4 &getProjectionMatrix() const;
const core::matrix4 &getFutureViewMatrix() const;
const core::matrix4 &getFutureProjectionMatrix() const;
core::matrix4 getViewProjMatrix();

/// Gets the light's far value.
@@ -88,6 +91,8 @@ class DirectionalLight

bool should_update_map_shadow{true};

void commitFrustum();

private:
void createSplitMatrices(const Camera *cam);

@@ -99,4 +104,6 @@ class DirectionalLight
v3f pos;
v3f direction{0};
shadowFrustum shadow_frustum;
shadowFrustum future_frustum;
bool dirty{false};
};

0 comments on commit bf3acbf

Please sign in to comment.