17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-28 16:57:29 +01:00

Use clustered light lists in forward shading

This commit is contained in:
2026-10-15 16:16:53 +02:00
parent 6aa6e505e8
commit 9f2ae071a6
9 changed files with 117 additions and 51 deletions

View File

@@ -64,8 +64,10 @@ void gl::glsl_common_setup()
uint lights_total;
vec3 fog_color;
// lights the shading loop goes through
// lights the shading loop goes through when the lights are not clustered
uint lights_count;
// 1 if the shading takes the lights from the list of the cluster of a fragment, see cluster_ubo
uint lights_clustered;
light_s lights[];
};

View File

@@ -168,8 +168,13 @@ namespace gl
uint32_t lights_total;
packed_vec3 fog_color;
// number of lights (the sun included) the shading loop over lights[] goes through
// number of lights (the sun included) the shading loop over lights[] goes through when the lights are not clustered
uint32_t lights_count;
// 1 if the shading takes the lights of a fragment from the list of its cluster (lights_count is not used then), 0 if not.
// set per pass: the cluster grid exists only for the color pass of a perspective view
uint32_t lights_clustered;
// lights[] begins at a multiple of 16 bytes
uint32_t padding[3];
light_element lights[MAX_LIGHTS];
};
@@ -178,8 +183,9 @@ namespace gl
static_assert(offsetof(light_data, lights_total) == 12, "bad layout of light_data");
static_assert(offsetof(light_data, fog_color) == 16, "bad layout of light_data");
static_assert(offsetof(light_data, lights_count) == 28, "bad layout of light_data");
static_assert(offsetof(light_data, lights) == 32, "bad layout of light_data");
static_assert(sizeof(light_data) == 32 + sizeof(light_element) * MAX_LIGHTS, "bad size of light_data");
static_assert(offsetof(light_data, lights_clustered) == 32, "bad layout of light_data");
static_assert(offsetof(light_data, lights) == 48, "bad layout of light_data");
static_assert(sizeof(light_data) == 48 + sizeof(light_element) * MAX_LIGHTS, "bad size of light_data");
// light clusters (see gl/cluster_grid.h): the view frustum is cut into CLUSTER_GRID_X x CLUSTER_GRID_Y screen
// tiles and CLUSTER_GRID_Z depth slices, spaced exponentially between the near and far plane of the pass.

View File

@@ -694,8 +694,9 @@ void opengl46_renderer::SwapBuffers()
+ " lines: " + to_string( m_colorpass.draw_stats.lines, 7 ) + "\n"
+ "particles: " + to_string( m_colorpass.draw_stats.particles, 7 )
+ "\nlights: " + to_string( static_cast<int>( m_lights_lit ), 7 )
+ " (" + std::to_string( m_lights_dropped ) + " over capacity)";
if( auto const cullstats = ( m_viewports.empty() ? std::nullopt : m_viewports.front()->clusters->stats() ) ) {
+ " (" + std::to_string( m_lights_dropped ) + " over capacity), "
+ ( Global.gfx_lights_clustered ? "clustered" : "first " + std::to_string( std::max( Global.DynamicLightCount, 0 ) ) );
if( auto const cullstats = ( m_viewports.empty() || !( Global.gfx_lights_clustered || Global.gfx_lights_clusterheatmap ) ? std::nullopt : m_viewports.front()->clusters->stats() ) ) {
m_debugstatstext +=
"\ncluster lists: " + std::to_string( cullstats->index_count )
+ ", overflow " + std::to_string( cullstats->overflow_count )
@@ -726,6 +727,7 @@ void opengl46_renderer::draw_debug_ui()
if (ImGui::Begin("Light clusters"))
{
ImGui::Checkbox("clustered lights (off: the first dynamiclights headlights)", &Global.gfx_lights_clustered);
ImGui::Checkbox("heatmap", &Global.gfx_lights_clusterheatmap);
if (auto const &clusters{m_viewports.front()->clusters})
{
@@ -926,9 +928,17 @@ void opengl46_renderer::Render_pass(viewport_config &vp, rendermode const Mode)
future = glm::translate(mv, -glm::vec3(vehicle->get_future_movement())) * glm::inverse(mv);
}
Update_Clusters( vp );
Update_Lights( simulation::Lights );
Cull_Lights( vp );
// the cluster grid and its lists of lights are kept up only while something reads them: the shading or the heatmap.
// the lists exist for a perspective view; without them the shading goes through the first few lights, as does the pass
// the environment map is drawn in
auto const clusterlists{ Global.gfx_lights_clustered || Global.gfx_lights_clusterheatmap };
if( clusterlists ) {
Update_Clusters( vp );
}
Update_Lights( simulation::Lights, true, Global.gfx_lights_clustered && vp.clusters->valid() );
if( clusterlists ) {
Cull_Lights( vp );
}
glDebug("render environment");
@@ -1201,6 +1211,10 @@ if (!Global.gfx_skippipeline)
setup_env_map(m_empty_cubemap.get());
setup_matrices();
// the light buffer holds what the last color pass left in it, in the view space of its camera, while the face is drawn
// in its own. the lights go in anew; the map is a cache of the surroundings which lasts for seconds, so the headlights
// of the vehicles around are left out of it and the cost of the pass does not depend on how many of them are on
Update_Lights(simulation::Lights, false);
// render
// environment...
@@ -5612,6 +5626,7 @@ void opengl46_renderer::Preview_Color(TAnimModel *Instance, float const Squaredi
light_data.fog_color = glm::vec3{1.f};
light_data.lights_total = 1;
light_data.lights_count = 1;
light_data.lights_clustered = 0;
light_data.lights[0].type = gl::light_element::DIR;
light_data.lights[0].dir = modelview * glm::vec4(m_sunlight.direction, 0.0f);
light_data.lights[0].color = m_sunlight.diffuse;
@@ -5893,7 +5908,7 @@ bool opengl46_renderer::Render_Preview(TAnimModel *Instance, int const Size, int
return true;
}
void opengl46_renderer::Update_Lights(light_array &Lights)
void opengl46_renderer::Update_Lights(light_array &Lights, bool const Headlights, bool const Clustered)
{
glDebug("Update_Lights");
@@ -5901,15 +5916,18 @@ void opengl46_renderer::Update_Lights(light_array &Lights)
// arrange the light array from closest to farthest from current position of the camera
auto const camera = m_colorpass.pass_camera.position();
std::sort(
std::begin(Lights.data), std::end(Lights.data),
[&camera](light_array::light_record const &Left, light_array::light_record const &Right) {
// move lights which are off at the end...
if (Left.intensity == 0.f) { return false; }
if (Right.intensity == 0.f) { return true; }
// ...otherwise prefer closer and/or brigher light sources
return (glm::length2(camera - Left.position) / Left.intensity) < (glm::length2(camera - Right.position) / Right.intensity);
});
if (Headlights)
{
std::sort(
std::begin(Lights.data), std::end(Lights.data),
[&camera](light_array::light_record const &Left, light_array::light_record const &Right) {
// move lights which are off at the end...
if (Left.intensity == 0.f) { return false; }
if (Right.intensity == 0.f) { return true; }
// ...otherwise prefer closer and/or brigher light sources
return (glm::length2(camera - Left.position) / Left.intensity) < (glm::length2(camera - Right.position) / Right.intensity);
});
}
// set up helpers
glm::mat4 coordmove;
@@ -5931,7 +5949,7 @@ void opengl46_renderer::Update_Lights(light_array &Lights)
for (auto const &scenelight : Lights.data)
{
if (scenelight.intensity == 0.f) {
if (!Headlights || scenelight.intensity == 0.f) {
// all lights past this one are bound to be off
break;
}
@@ -6032,6 +6050,7 @@ void opengl46_renderer::Update_Lights(light_array &Lights)
light_data.lights_total = static_cast<uint32_t>(light_i);
m_lights_lit = static_cast<uint32_t>(light_i - 1);
light_data.lights_count = 1 + std::min<uint32_t>(static_cast<uint32_t>(light_i - 1), static_cast<uint32_t>(std::max(Global.DynamicLightCount, 0)));
light_data.lights_clustered = Clustered ? 1 : 0;
// fill sunlight data
light_data.ambient = m_sunlight.ambient * m_sunlight.factor;// *simulation::Environment.light_intensity();
light_data.lights[0].type = gl::light_element::DIR;

View File

@@ -341,7 +341,10 @@ class opengl46_renderer : public gfx_renderer {
bool Render_Alpha(TModel3d *Model, material_data const *Material, float const Squaredistance);
void Render_Alpha(TSubModel *Submodel);
void Render_Alpha_text(TSubModel *Submodel); // characters of a text display
void Update_Lights(light_array &Lights);
// writes the lighting of the pass about to be drawn to the light buffer: the sun, the ambient light and the fog, and the headlights of Lights
// unless Headlights is false, as in the pass which draws the environment map, where lights of vehicles would only get baked into the map.
// Clustered tells the shading to take the headlights from the lists of the clusters (see Cull_Lights()) and not to go through the first few
void Update_Lights(light_array &Lights, bool const Headlights = true, bool const Clustered = false);
// brings the light cluster grid of the viewport up to date with the projection of the color pass being drawn
void Update_Clusters(viewport_config &vp);
// assigns the lights set up by Update_Lights() to the clusters of the viewport; run after it, before the scene is drawn

View File

@@ -194,7 +194,10 @@ vec2 calc_dir_light(light_s light, vec3 fragnormal)
return calc_light(light_dir, fragnormal);
}
vec2 calc_headlights(light_s light, vec3 fragnormal)
// dpdx and dpdy are the change of the view space position of the fragment from a pixel to the next one. the beam texture is
// sampled with gradients worked out from them instead of the ones of the hardware: those are undefined where the fragments of
// a quad do not go through the same lights, which is what happens at the edges of the clusters
vec2 calc_headlights(light_s light, vec3 fragnormal, vec3 dpdx, vec3 dpdy)
{
vec4 headlightpos = light.headlight_projection * f_pos;
vec3 coords = headlightpos.xyz / headlightpos.w;
@@ -203,6 +206,10 @@ vec2 calc_headlights(light_s light, vec3 fragnormal)
return vec2(0.0);
if (coords.z < 0.0)
return vec2(0.0);
// the beam is what the projection takes in. beyond its sides the texture would be clamped to its edge, and near the projector
// (where a pixel spans many texels) its coarsest levels spill the average of the whole beam over the surroundings
if (coords.x < 0.0 || coords.x > 1.0 || coords.y < 0.0 || coords.y > 1.0)
return vec2(0.0);
vec3 light_dir = normalize(light.pos - f_pos.xyz);
// Tighter wrap (was +0.25): faces angled away from the headlight cone
@@ -212,11 +219,58 @@ vec2 calc_headlights(light_s light, vec3 fragnormal)
float distance = length(light.pos - f_pos.xyz);
float atten = 1.0f / (1.0f + light.linear * distance + light.quadratic * (distance * distance));
atten *= mix(1.0, 0.0, clamp((coords.z - 0.998) * 500.0, 0.0, 1.0));
vec3 lights = textureProj(headlightmap, headlightpos).rgb * light.headlight_weights.rgb;
vec2 beamcoords = headlightpos.xy / headlightpos.w;
vec4 dheadlightposdx = light.headlight_projection * vec4(dpdx, 0.0);
vec4 dheadlightposdy = light.headlight_projection * vec4(dpdy, 0.0);
vec2 dbeamdx = (dheadlightposdx.xy - beamcoords * dheadlightposdx.w) / headlightpos.w;
vec2 dbeamdy = (dheadlightposdy.xy - beamcoords * dheadlightposdy.w) / headlightpos.w;
vec3 lights = textureGrad(headlightmap, beamcoords, dbeamdx, dbeamdy).rgb * light.headlight_weights.rgb;
float lightintensity = max(max(lights.r, lights.g), lights.b);
return part * atten * lightintensity;
}
// index of the light cluster the fragment is in, see gl/cluster_grid.h: its screen tile and slice of depth. the depth is the clip
// space w, which is 1 / gl_FragCoord.w. a tile is kept in the grid, as the far edge of the render target and rounding
// right below it give one past the last
uint fragment_cluster()
{
ivec2 tile = ivec2(gl_FragCoord.xy * (vec2(CLUSTER_GRID_X, CLUSTER_GRID_Y) / cluster_viewport_size));
tile = clamp(tile, ivec2(0), ivec2(CLUSTER_GRID_X - 1, CLUSTER_GRID_Y - 1));
int slice = clamp(int(floor(cluster_slice_bias - log(gl_FragCoord.w) * cluster_slice_scale)), 0, CLUSTER_GRID_Z - 1);
return uint(tile.x + CLUSTER_GRID_X * (tile.y + CLUSTER_GRID_Y * slice));
}
// adds the light of the headlights which reach the fragment to its colour. these are the ones in the list of its cluster
// when the lights are clustered (the sun is not on the list, the callers take care of it), and the first lights_count - 1 of
// lights[] otherwise
vec3 add_headlights(vec3 fragcolor, vec3 fragnormal)
{
// worked out before the loops, which the fragments of a quad do not leave together
vec3 dpdx = dFdx(f_pos.xyz);
vec3 dpdy = dFdy(f_pos.xyz);
if (lights_clustered != 0U)
{
cluster_range_s range = cluster_ranges[fragment_cluster()];
for (uint i = 0U; i < range.count; i++)
{
light_s light = lights[cluster_light_indices[range.offset + i]];
vec2 part = calc_headlights(light, fragnormal, dpdx, dpdy);
fragcolor += light.color * (part.x + part.y * param[1].y) * light.intensity;
}
}
else
{
for (uint i = 1U; i < lights_count; i++)
{
light_s light = lights[i];
vec2 part = calc_headlights(light, fragnormal, dpdx, dpdy);
fragcolor += light.color * (part.x + part.y * param[1].y) * light.intensity;
}
}
return fragcolor;
}
// -----------------------------------------------------------------------
// Split-sum environment BRDF (Karis / UE4 analytic approximation).
//
@@ -312,12 +366,7 @@ vec3 apply_lights(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, float refl
// direct sun read as too hot/burnt; raise it for more punch.
fragcolor += lights[0].color * SUN_DIFFUSE_SCALE * (1.0 - shadow1) * diffuseamount;
for (uint i = 1U; i < lights_count; i++)
{
light_s light = lights[i];
vec2 part = calc_headlights(light, fragnormal);
fragcolor += light.color * (part.x + part.y * param[1].y) * light.intensity;
}
fragcolor = add_headlights(fragcolor, fragnormal);
float specularamount = sunlight.y * param[1].y * specularity * lights[0].intensity
* clamp(1.0 - shadowtone, 0.0, 1.0);

View File

@@ -66,22 +66,7 @@ vec3 apply_lights_sunless(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, fl
float specularamount = (sunlight.y * param[1].y * specularity) * lights[0].intensity;
glossiness = abs(param[1].w);
for (uint i = 1U; i < lights_count; i++)
{
light_s light = lights[i];
vec2 part = vec2(0.0);
// if (light.type == LIGHT_SPOT)
// part = calc_spot_light(light, fragnormal);
// else if (light.type == LIGHT_POINT)
// part = calc_point_light(light, fragnormal);
// else if (light.type == LIGHT_DIR)
// part = calc_dir_light(light, fragnormal);
// else if (light.type == LIGHT_HEADLIGHTS)
part = calc_headlights(light, fragnormal);
fragcolor += light.color * (part.x + part.y * param[1].y) * light.intensity;
}
fragcolor = add_headlights(fragcolor, fragnormal);
if (shadowtone < 1.0)
{

View File

@@ -76,12 +76,7 @@ vec3 apply_lights_sunless(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, fl
float specularamount = sunlight.y * param[1].y * specularity * lights[0].intensity;
for (uint i = 1U; i < lights_count; i++)
{
light_s light = lights[i];
vec2 part = calc_headlights(light, fragnormal);
fragcolor += light.color * (part.x + part.y * param[1].y) * light.intensity;
}
fragcolor = add_headlights(fragcolor, fragnormal);
if (shadowtone < 1.0)
{

View File

@@ -1421,6 +1421,11 @@ global_settings::ConfigParse_gfx( cParser &Parser, std::string_view const Token
Parser.getTokens(1);
Parser >> gfx_envmap_enabled;
}
else if (Token == "gfx.lights.clustered")
{
Parser.getTokens(1);
Parser >> gfx_lights_clustered;
}
else if (Token == "gfx.lights.clusterheatmap")
{
Parser.getTokens(1);
@@ -1759,6 +1764,7 @@ global_settings::export_as_text( std::ostream &Output ) const {
// TODO: export fpslimit
export_as_text( Output, "randomseed", Global.random_seed );
export_as_text( Output, "gfx.envmap.enabled", gfx_envmap_enabled );
export_as_text( Output, "gfx.lights.clustered", gfx_lights_clustered );
export_as_text( Output, "gfx.lights.clusterheatmap", gfx_lights_clusterheatmap );
export_as_text( Output, "gfx.postfx.motionblur.enabled", gfx_postfx_motionblur_enabled );
export_as_text( Output, "gfx.postfx.motionblur.shutter", gfx_postfx_motionblur_shutter );

View File

@@ -283,6 +283,7 @@ struct global_settings {
int gfx_framebuffer_height = -1;
bool gfx_shadowmap_enabled = true;
bool gfx_envmap_enabled = true;
bool gfx_lights_clustered = true; // headlights from the light lists of the clusters (no limit of lit headlights) or the first dynamiclights of them
bool gfx_lights_clusterheatmap = false; // debug view of the light clusters
bool gfx_postfx_motionblur_enabled = true;
float gfx_postfx_motionblur_shutter = 0.01f;