diff --git a/gl/glsl_common.cpp b/gl/glsl_common.cpp index a52050e1..1037795c 100644 --- a/gl/glsl_common.cpp +++ b/gl/glsl_common.cpp @@ -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[]; }; diff --git a/gl/ubo.h b/gl/ubo.h index 38c30b2b..487554f1 100644 --- a/gl/ubo.h +++ b/gl/ubo.h @@ -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. diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index 59a1596b..7d9fa598 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -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( 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(light_i); m_lights_lit = static_cast(light_i - 1); light_data.lights_count = 1 + std::min(static_cast(light_i - 1), static_cast(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; diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index e6ad2065..a75c651d 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -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 diff --git a/shaders/light_common.glsl b/shaders/light_common.glsl index 37ddf50b..4df80ad9 100644 --- a/shaders/light_common.glsl +++ b/shaders/light_common.glsl @@ -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); diff --git a/shaders/mat_sunlessnormalmap.frag b/shaders/mat_sunlessnormalmap.frag index 0b3fc7f6..1e9e2ac7 100644 --- a/shaders/mat_sunlessnormalmap.frag +++ b/shaders/mat_sunlessnormalmap.frag @@ -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) { diff --git a/shaders/mat_sunlessnormalmap_specgloss.frag b/shaders/mat_sunlessnormalmap_specgloss.frag index 21cc9f6e..b19e9e4d 100644 --- a/shaders/mat_sunlessnormalmap_specgloss.frag +++ b/shaders/mat_sunlessnormalmap_specgloss.frag @@ -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) { diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index e81c70f0..71995935 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -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 ); diff --git a/utilities/Globals.h b/utilities/Globals.h index 933a4206..9dd768e7 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -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;