diff --git a/CMakeLists.txt b/CMakeLists.txt index da5ee1e2..7f58ced9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -337,6 +337,7 @@ if (WITH_OPENGL_MODERN) "rendering/opengl46/opengl46sky.cpp" "rendering/opengl46/opengl46exposure.cpp" "rendering/opengl46/opengl46ao.cpp" + "rendering/opengl46/opengl46locallights.cpp" "rendering/opengl46/opengl46precipitation.cpp" "rendering/opengl46/opengl46particles.cpp" "rendering/opengl46/opengl46terrain.cpp" diff --git a/docs/clustered-lighting.md b/docs/clustered-lighting.md index 3a5535e4..347d5c80 100644 --- a/docs/clustered-lighting.md +++ b/docs/clustered-lighting.md @@ -1,6 +1,6 @@ # Clustered lighting -How the OpenGL 4.6 renderer (`rendering/opengl46/`) gives the shaders the lights of vehicle headlights. +How the OpenGL 4.6 renderer (`rendering/opengl46/`) gives the shaders the lights of vehicle headlights and lamps. For the settings and what changed for players see [opengl46-renderer.md](opengl46-renderer.md). ## Idea @@ -29,13 +29,13 @@ The UBO numbers go together with the scene (0), model (1) and instance (3) block space of their own. Layouts of the structures are checked with `static_assert`s on sizes and offsets in `gl/ubo.h`, and have to stay equal to the blocks in `gl/glsl_common.cpp`. -Sizes: `MAX_LIGHTS` = 256 (the sun and 255 headlights), `MAX_LIGHTS_PER_CLUSTER` = 64. The buffers have a +Sizes: `MAX_LIGHTS` = 1024 (the sun and 1023 headlights and lamps), `MAX_LIGHTS_PER_CLUSTER` = 64. The buffers have a fixed size, so changing the resolution, the field of view or the draw range only makes the grid parameters and boxes be recomputed. `gl::light_data.lights_total` is the number of lights written to `lights[]`, which the culling reads. `lights_count` is the length of the plain loop over the lights (the sun and the first `dynamiclights` headlights) used -when the lights are not clustered. `lights_clustered` is set per pass and tells the shading which one to use. +when the lights are not clustered; then only headlights are written, from slot 1 on. `lights_clustered` is set per pass and tells the shading which one to use. ## Frame order @@ -44,9 +44,11 @@ In `Render_pass()` for the color pass of a view (`opengl46_renderer.cpp`): 1. `Update_Clusters(vp)`: `cluster_grid::update()` compares the projection and near / far plane with those the boxes were built for. When they differ, `cluster_build.comp` recomputes the boxes (one invocation per cluster, 54 work groups of 64). This is the "clusters" timer. -2. `Update_Lights(...)`: sorts the lights of the scene (`simulation::Lights`) by distance squared over - intensity, skips the lights that are off or farther than 1 km, writes each lit headlight to `lights[]` - in view space, and uploads the header and the part of the array in use. +2. `Update_Lights(...)`: with clusters, writes the cab lamp first (slot 1), when it is on. Then it sorts the lights of + the scene (`simulation::Lights`) by distance squared over intensity, skips the lights that are off or farther than + 1 km and writes each lit headlight to `lights[]` in view space. With clusters the lamps of models and vehicles follow + (`opengl46_local_lights`, in their order of importance), those whose sphere is in the view frustum of the pass, while + there is room. Then it uploads the header and the part of the array in use. 3. `Cull_Lights(vp)`: the CPU zeroes the counters, then `cluster_cull.comp` runs (one invocation per cluster, 54 work groups of 64): it tests every light against the box of its cluster and writes the indices of the ones that reach it. Then `glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT)`. This is the @@ -81,6 +83,26 @@ reverse-Z with the far plane at infinity, so the `znear` / `zfar` of the pass bo cluster (`ndc.xy` times `w` at the near and far side of the slice) map back to view space through the inverse of the corresponding 3x3 part of the projection. The box is the min and max of the corners. +## Lamps + +`opengl46_local_lights` (`rendering/opengl46/opengl46locallights.*`) gathers the other lights once per frame, in +`Render()`, before the views are drawn, within `gfx.lights.range` of the camera: + +- light points (`TP_FREESPOTLIGHT`) of scenery models: the instances on the translucent lists of the cells around the camera + whose model has light points (that list is kept per cell and made anew when the list of the cell changes, and every frame + while the editor is on). For each, `RaAnimate()` and `RaPrepare()` set the state of the shared model, as the render does, + and the submodel tree is gone through the way `Render_Alpha(TSubModel *)` goes through it (visibility, lod ranges, + transforms, animations). A point which is lit (emission above 0 and the light level below its `fLight`) becomes a spot light + along its -z axis, with the cone of its glare; +- the same for vehicles within the range, after `ABuLittleUpdate()`, with their main model, attachments and low poly interior, + and a point light in the middle of each lit section of the low poly interior which has no light points of its own; +- the cab lamp: a point light under the top of the bounds of the cab model, when the cab is drawn and its lamp is on. + +Lamps are `POINT` or `SPOT` lights. The shading (`calc_local_light()`) lets them fall off with the square of the +distance (no nearer than 10 cm) and to nothing at their range, which is written as `quadratic` = 1 / range^2 and +which the culling takes as the end of the light. A spot light fades from `in_cutoff` to `out_cutoff` (cosines of the half +angles); the culling tests a cone only when it is narrower than a half of the space. + ## Culling `cluster_cull.comp` handles one cluster per invocation and goes through `lights[1 .. lights_total)`. @@ -122,9 +144,8 @@ guarantee any storage blocks in the vertex and geometry stages. The work group size in the shaders and the number of groups dispatched both come from `CLUSTER_*_GROUP_SIZE`. - Check the shaders with `ci_shadervalidator` (see [opengl46-renderer.md](opengl46-renderer.md)); CI does it for changes in `shaders/` and `gl/`. -- Only vehicle headlights feed `lights[]` today. The culling already knows `SPOT`, `POINT` and `DIR`, but - `add_headlights()` runs `calc_headlights()` for every listed light, so a new kind of light also needs - its own branch there. +- A new kind of light needs its branch in `add_headlights()` (today `HEADLIGHTS` goes to `calc_headlights()`, the others + to `calc_local_light()`) and in `light_range()` / `light_reaches_box()` of `cluster_cull.comp`. ## Not covered diff --git a/docs/opengl46-renderer.md b/docs/opengl46-renderer.md index 0528bfe0..c547a9c2 100644 --- a/docs/opengl46-renderer.md +++ b/docs/opengl46-renderer.md @@ -96,6 +96,40 @@ before the shading (a depth prepass), which is planned later. | `gfx.postfx.ao.bounce` | `0.3` | 0..1: albedo of the surfaces assumed for the light that comes back after more bounces (brightens the occlusion a little) | | `gfx.postfx.ao.quality slices steps` | `3 4` | directions searched around each pixel (1..8) and samples on each side of a direction (1..16) | +### Lamps + +With the clustered lights on, lamps light their surroundings, besides the sun and the vehicle headlights. Without the +clustered lights (`gfx.lights.clustered no`) none of this happens and the cab is lit as before. The switches and +scales can also be changed in the **Light clusters** debug window. + +- **Light points of models** (`freespotlight` submodels: street and platform lamps, signals, lamps of vehicles) are taken + the way the experimental renderer takes them. A point which is visible, has self-illumination (`SelfIllum` `true`, or a + threshold the light level is below) shines as a spot light along the -z axis of its submodel, in the cone of its glare + (`FalloffAngle`, full brightness within `HotspotAngle`), in its colour; `hotspotpower` scales it. Lamps of scenery + models switched with `Light_On` / `Light_Off` (`lights` of the model node, events) turn their light points on and off + with them. Nothing in the models has to change; a light point without self-illumination stays a glare only. +- **Lit compartments of vehicles** (the `cab`, `corridor`/`korytarz` and `compartment`/`przedzial` sections of the low + poly interior) give a point light in the middle of the section, in the colour of the interior light, unless the + section holds light points of its own (these light it then, through its self-illumination). +- **The cab lamp** of the occupied cab is a point light under the top of the cab model. Before, it added to the ambient + light of the whole cab and weakened the shadows of the sun in it; now a share of that ambient light is kept for the light + which bounces around the cab (`gfx.lights.cab.bounce`), and the sun shadows stay. + +The lamps of models and compartments fade in from the light level at which scenery lamps switch on (`ls_Dark`) to full +darkness; in daylight they are not gathered at all. None of these lights casts shadows yet, so they also light what is +behind walls within their reach; that reach is kept short for compartments and the cab. + +| Entry | Default | Meaning | +|---|---|---| +| `gfx.lights.scenery yes/no` | `yes` | light points of scenery models light their surroundings | +| `gfx.lights.vehicles yes/no` | `yes` | light points of vehicles and their lit compartments light their surroundings | +| `gfx.lights.cab yes/no` | `yes` | the cab lamp is a point light; `no`: it adds to the ambient light of the cab, as before | +| `gfx.lights.freespot.intensity` | `10` | brightness of a light point (the experimental renderer uses the same default) | +| `gfx.lights.compartment.intensity` | `1.0` | brightness of the light of a lit compartment | +| `gfx.lights.cab.intensity` | `1.5` | brightness of the cab lamp | +| `gfx.lights.cab.bounce` | `0.3` | 0..1: share of the former ambient light of the cab lamp kept with the lamp on | +| `gfx.lights.range` | `300` | distance from the camera the lamps of models and vehicles are gathered from, metres | + ### Changed option `dynamiclights` (0..7) used to limit the number of headlight sets that light the surroundings. It is still @@ -104,7 +138,8 @@ read, but it only takes effect with `gfx.lights.clustered no`. ## What looks different from the previous renderer - **No limit of 7 lit headlights.** All lit headlights within 1 km take part in the lighting (each lit end - of a vehicle counts separately), up to 255 in the buffer and 64 reaching any one part of the view. Beyond + of a vehicle counts separately), up to 1023 lights in the buffer (headlights, lamps and the cab lamp) and 64 reaching + any one part of the view. Beyond these capacities the farthest and dimmest lights are dropped, and the statistics show how many. - **Outside the beam texture a headlight adds nothing.** Before, the edge of the texture was stretched beyond the beam, which could leave a faint glow around vehicles. This also applies with @@ -127,7 +162,10 @@ editor goes through the first `dynamiclights` headlights, the reflections throug time, and `cluster lists:` the size of the lists and the number of dropped (cluster, light) pairs (`overflow`, with its peak). A non-zero overflow means some cluster held more than 64 lights. - **Light clusters** window (in debug mode, menu *Windows*, item *Headlight config* opens the renderer debug - windows): the switch for clustered lights, the heat map, the grid state and the same counters. + windows): the switch for clustered lights, the heat map, the grid state and the same counters, and the switches and + scales of the lamps with their counts. Shift+F12 shows the lamps in the `local lights:` row: how many were gathered (from + scenery models, vehicles and compartments, and the cab lamp), how many were placed in the light buffer of the main view + (those that may light something in view) and did not fit, and the cpu time of gathering them. - **Sky and exposure** window (next to the Light clusters window): the switches and scales above, the share of the atmosphere, the colours of the sun, ambient light and fog in use, and the measured luminance and exposure. Shift+F12 shows the GPU time of `atmosphere:` (look-up tables, only when the sun moved or the weather changed), diff --git a/gl/ubo.h b/gl/ubo.h index 23f897ad..6b0802f5 100644 --- a/gl/ubo.h +++ b/gl/ubo.h @@ -169,7 +169,7 @@ namespace gl // capacity of light_data.lights; slot 0 is the sun. a buffer size, not a limit of the shaders: they take the // number of lights from the header - const size_t MAX_LIGHTS = 256; + const size_t MAX_LIGHTS = 1024; // contents of light_ssbo: global lighting header followed by the light array struct light_data diff --git a/rendering/opengl46/opengl46locallights.cpp b/rendering/opengl46/opengl46locallights.cpp new file mode 100644 index 00000000..41d12410 --- /dev/null +++ b/rendering/opengl46/opengl46locallights.cpp @@ -0,0 +1,520 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" + +#include "rendering/opengl46/opengl46locallights.h" + +#include +#include +#include +#include + +#include "utilities/Globals.h" +#include "utilities/utilities.h" +#include "scene/scene.h" +#include "model/AnimModel.h" +#include "model/Model3d.h" +#include "vehicle/DynObj.h" +#include "vehicle/Train.h" +#include "simulation/simulation.h" +#include "rendering/geometrybank.h" + +namespace +{ +// a light ends where its contribution falls under this. it is above the limit of the culling of the clusters, as there are +// many more of these lights than headlights, and each of them lights only its surroundings +float const LIGHT_THRESHOLD{0.01f}; +// no light of a model reaches farther than this, metres +float const LIGHT_RANGE_MAX{60.f}; +// the self-illumination threshold of the lamps of scenery models (Light_On, ls_Dark) and full darkness: between them +// the lights of models and compartments fade in, as they would be lost in the daylight anyway +float const LIGHT_DUSK_LUMINANCE{0.325f}; +float const LIGHT_DAY_LUMINANCE{1.0f}; + +glm::vec3 to_linear(glm::vec3 const &Color) +{ + return glm::pow(glm::max(Color, glm::vec3{0.f}), glm::vec3{2.2f}); +} + +float max_component(glm::vec3 const &Color) +{ + return std::max(Color.r, std::max(Color.g, Color.b)); +} + +// distance at which a light of the colour falls under the threshold with the square of the distance +float light_range(glm::vec3 const &Color) +{ + return std::min(std::sqrt(max_component(Color) / LIGHT_THRESHOLD), LIGHT_RANGE_MAX); +} + +// model space transform of the parent of the submodel, from the static transforms of the submodels above it; false if the +// submodel is not in the tree +bool find_parent_transform(TSubModel const *Submodel, TSubModel const *Target, glm::mat4 const &Parent, glm::mat4 &Result) +{ + for (auto const *submodel{Submodel}; submodel != nullptr; submodel = submodel->Next) + { + if (submodel == Target) + { + Result = Parent; + return true; + } + auto transform{Parent}; + if ((submodel->iFlags & 0xC000) && (submodel->fMatrix != nullptr)) + { + transform *= glm::make_mat4(submodel->fMatrix->readArray()); + } + if (find_parent_transform(submodel->Child, Target, transform, Result)) + { + return true; + } + } + return false; +} + +// bounds of the geometry of the submodel, of its children and, with Siblings, of the submodels after it, in the space of Transform +void extend_bounds(TSubModel const *Submodel, glm::mat4 const &Transform, gfx::geometrybank_manager const &Geometry, bool const Siblings, glm::vec3 &Min, glm::vec3 &Max, + bool &Lightpoints) +{ + for (auto const *submodel{Submodel}; submodel != nullptr; submodel = (Siblings ? submodel->Next : nullptr)) + { + auto transform{Transform}; + if ((submodel->iFlags & 0xC000) && (submodel->fMatrix != nullptr)) + { + transform *= glm::make_mat4(submodel->fMatrix->readArray()); + } + if (submodel->eType == TP_FREESPOTLIGHT) + { + Lightpoints = true; + } + else if ((submodel->eType < TP_ROTATOR) && (submodel->m_geometry.handle != null_handle)) + { + for (auto const &vertex : Geometry.vertices(submodel->m_geometry.handle)) + { + auto const point{glm::vec3{transform * glm::vec4{vertex.position, 1.f}}}; + Min = glm::min(Min, point); + Max = glm::max(Max, point); + } + } + extend_bounds(submodel->Child, transform, Geometry, true, Min, Max, Lightpoints); + } +} +} // namespace + +void opengl46_local_lights::gather(glm::dvec3 const &Camera, scene::basic_region &Region, gfx::geometrybank_manager const &Geometry, unsigned int const Framestamp) +{ + auto const start{std::chrono::steady_clock::now()}; + + m_lights.clear(); + m_cablampon = false; + m_scenerycount = m_vehiclecount = m_compartmentcount = 0; + + // the editor adds, moves and removes instances in the cells. it can also do it in the frame it is turned off in + auto const rebuild{EditorModeFlag || m_editorseen}; + m_editorseen = EditorModeFlag; + + if (Global.gfx_lights_clustered) + { + if (Global.gfx_lights_cab) + { + gather_cab_lamp(Geometry); + } + + // lamps are lost in the daylight; this also spares the work of gathering them in the middle of the day + auto const darkness{1.f - glm::smoothstep(LIGHT_DUSK_LUMINANCE, LIGHT_DAY_LUMINANCE, static_cast(Global.fLuminance))}; + auto const range{Global.gfx_lights_range}; + + if ((darkness > 0.f) && (range > 0.f) && Global.gfx_lights_scenery) + { + auto const freespotbrightness{Global.gfx_lights_freespot_intensity * darkness}; + // sections of the region within the range, the same way the render picks them + auto const camerax{static_cast(std::floor(Camera.x / scene::EU07_SECTIONSIZE + scene::EU07_REGIONSIDESECTIONCOUNT / 2))}; + auto const cameraz{static_cast(std::floor(Camera.z / scene::EU07_SECTIONSIZE + scene::EU07_REGIONSIDESECTIONCOUNT / 2))}; + auto const reach{static_cast(std::ceil(range / scene::EU07_SECTIONSIZE))}; + for (int row = std::max(0, cameraz - reach); row <= std::min(scene::EU07_REGIONSIDESECTIONCOUNT - 1, cameraz + reach); ++row) + { + for (int column = std::max(0, camerax - reach); column <= std::min(scene::EU07_REGIONSIDESECTIONCOUNT - 1, camerax + reach); ++column) + { + auto *section{Region.m_sections[row * scene::EU07_REGIONSIDESECTIONCOUNT + column]}; + if (section == nullptr) + { + continue; + } + for (auto &cell : section->m_cells) + { + if ((false == cell.m_active) || (glm::length(cell.m_area.center - Camera) > range + cell.m_area.radius)) + { + continue; + } + for (auto *instance : cell_instances(cell, rebuild).instances) + { + gather_instance(instance, Camera, freespotbrightness, Framestamp); + } + } + } + } + } + + if ((darkness > 0.f) && (range > 0.f) && Global.gfx_lights_vehicles) + { + auto const freespotbrightness{Global.gfx_lights_freespot_intensity * darkness}; + auto const compartmentbrightness{Global.gfx_lights_compartment_intensity * darkness}; + for (auto *vehicle : simulation::Vehicles.sequence()) + { + if (glm::length(vehicle->GetPosition() - Camera) > range + vehicle->radius()) + { + continue; + } + gather_vehicle(vehicle, Camera, Geometry, freespotbrightness, compartmentbrightness); + } + } + + // the most important lights first: when the light buffer or the list of a cluster fills up, the others are left out + std::sort(std::begin(m_lights), std::end(m_lights), [&Camera](light const &Left, light const &Right) { + return glm::length2(Left.position - Camera) / std::max(max_component(Left.color), 1e-6f) < glm::length2(Right.position - Camera) / std::max(max_component(Right.color), 1e-6f); + }); + } + + auto const elapsed{std::chrono::duration(std::chrono::steady_clock::now() - start).count()}; + m_gathertime = (m_gathertime == 0.0 ? elapsed : m_gathertime * 0.95 + elapsed * 0.05); +} + +void opengl46_local_lights::reset() +{ + m_lights.clear(); + m_cablampon = false; + m_modellights.clear(); + m_cells.clear(); + m_sections.clear(); + m_models.clear(); + m_scenerycount = m_vehiclecount = m_compartmentcount = m_placed = m_dropped = 0; +} + +void opengl46_local_lights::gather_instance(TAnimModel *Instance, glm::dvec3 const &Camera, float const Brightness, unsigned int const Framestamp) +{ + if ((false == Instance->m_visible) || (Instance->pModel == nullptr)) + { + return; + } + auto const offset{Instance->location() - Camera}; + if (glm::length(offset) > Global.gfx_lights_range + Instance->radius()) + { + return; + } + // the same distance the render picks the lods and the range of the instance by + auto const distancesquared{glm::length2(offset / static_cast(Global.ZoomFactor)) / Global.fDistanceFactor}; + if ((distancesquared < Instance->m_rangesquaredmin) || (distancesquared >= Instance->m_rangesquaredmax)) + { + return; + } + + Instance->RaAnimate(Framestamp); + Instance->RaPrepare(); + TSubModel::fSquareDist = static_cast(distancesquared); + + auto const count{m_lights.size()}; + auto const placement{glm::translate(glm::dmat4{1.0}, Instance->location()) * glm::dmat4{Instance->rotation_scale()}}; + walk(Instance->pModel->Root, glm::mat4{1.f}, placement, Brightness); + m_scenerycount += static_cast(m_lights.size() - count); +} + +void opengl46_local_lights::gather_vehicle(TDynamicObject *Vehicle, glm::dvec3 const &Camera, gfx::geometrybank_manager const &Geometry, float const Freespotbrightness, + float const Compartmentbrightness) +{ + if ((false == Global.render_cab) && (Global.pCamera.m_owner == Vehicle)) + { + // the render leaves the vehicle out as well + return; + } + // light points: the models with any are gone through after the setup the render makes before it draws the vehicle + std::array models{Vehicle->mdModel, Vehicle->mdLowPolyInt}; + auto const haslightpoints = [this](TModel3d *Model) { return (Model != nullptr) && has_light_points(Model); }; + if (std::any_of(std::begin(models), std::end(models), haslightpoints) || std::any_of(std::begin(Vehicle->mdAttachments), std::end(Vehicle->mdAttachments), haslightpoints)) + { + TSubModel::iInstance = reinterpret_cast(Vehicle); + TSubModel::iVariantSeed = Vehicle->variant_seed(); + auto const squaredistance{glm::length2(glm::vec3{Vehicle->vPosition - Camera} / Global.ZoomFactor)}; + Vehicle->ABuLittleUpdate(squaredistance); + + auto const placement{glm::translate(glm::dmat4{1.0}, Vehicle->vPosition) * Vehicle->mMatrix}; + auto const count{m_lights.size()}; + auto const walkmodel = [&](TModel3d *Model) { + if (haslightpoints(Model)) + { + TSubModel::fSquareDist = squaredistance; + walk(Model->Root, glm::mat4{1.f}, placement, Freespotbrightness); + } + }; + walkmodel(Vehicle->mdModel); + for (auto *attachment : Vehicle->mdAttachments) + { + walkmodel(attachment); + } + walkmodel(Vehicle->mdLowPolyInt); + m_vehiclecount += static_cast(m_lights.size() - count); + + if (Vehicle->btnOn) + { + Vehicle->TurnOff(); // the state of the shared model goes back to the default, as after the render + } + } + + // lit compartments, the way TDynamicObject::Update() works out their self-illumination: cab sections come first and are lit at + // the level of their lamp, the others at the level of the compartment lights + if ((Vehicle->mdLowPolyInt != nullptr) && (Compartmentbrightness > 0.f)) + { + auto const owncab{m_cablampon && (simulation::Train != nullptr) && (simulation::Train->Dynamic() == Vehicle)}; + auto cabsection{true}; + for (auto const §ion : Vehicle->Sections) + { + auto const &name{section.compartment->pName}; + cabsection &= (name.size() >= 4) && name.starts_with("cab"); + auto const level{section.light_level * (cabsection ? 1.f : Vehicle->MoverParameters->CompartmentLights.intensity)}; + if ((level <= 0.f) || (Global.fLuminance > section.compartment->fLight) || (section.compartment->iVisible == 0)) + { + continue; + } + if (cabsection && owncab) + { + // the lamp of the occupied cab is a light of its own + continue; + } + auto const &data{this->section(Vehicle->mdLowPolyInt, section.compartment, Geometry)}; + if (data.has_light_points || (data.extent == glm::vec3{0.f})) + { + continue; + } + light compartmentlight; + compartmentlight.position = Vehicle->GetWorldPosition(glm::dvec3{data.center}); + compartmentlight.color = to_linear(Vehicle->InteriorLight) * (level * Compartmentbrightness); + // the light goes out of the windows a little; there are no shadows to keep it in, so it doesn't reach far + compartmentlight.range = std::clamp(glm::length(data.extent) * 1.5f, 2.f, std::min(light_range(compartmentlight.color), 15.f)); + m_lights.emplace_back(compartmentlight); + ++m_compartmentcount; + } + } +} + +void opengl46_local_lights::gather_cab_lamp(gfx::geometrybank_manager const &Geometry) +{ + // the cab is drawn, Render_cab() conditions + if (FreeFlyModeFlag || (false == Global.render_cab) || (simulation::Train == nullptr)) + { + return; + } + auto const *vehicle{simulation::Train->Dynamic()}; + if ((vehicle == nullptr) || (false == vehicle->bDisplayCab) || (vehicle->mdKabina == nullptr) || (vehicle->mdKabina == vehicle->mdModel)) + { + return; + } + if (vehicle->InteriorLightLevel <= 0.f) + { + return; + } + // as bright as the ambient light it used to add to the cab: lost in the daylight + auto const luminance{Global.fLuminance * (vehicle->fShade > 0.0f ? vehicle->fShade : 1.0f)}; + auto const level{vehicle->InteriorLightLevel * std::clamp(1.25f - static_cast(luminance), 0.f, 1.f)}; + if (level <= 0.f) + { + return; + } + auto const &bounds{model_bounds(vehicle->mdKabina, Geometry)}; + if (bounds.extent == glm::vec3{0.f}) + { + return; + } + // under the ceiling, in the middle of the cab + auto const lamp{glm::vec3{bounds.center.x, std::max(bounds.center.y, bounds.center.y + bounds.extent.y - 0.25f), bounds.center.z}}; + m_cablamp = light{}; + m_cablamp.position = vehicle->GetWorldPosition(glm::dvec3{lamp}); + m_cablamp.color = to_linear(vehicle->InteriorLight) * (level * Global.gfx_lights_cab_intensity); + // within the cab: with no shadows the lamp would light the track below and the body of the vehicle around it + m_cablamp.range = std::clamp(glm::length(bounds.extent) * 1.25f, 2.5f, 8.f); + m_cablampon = (max_component(m_cablamp.color) > 0.f); +} + +void opengl46_local_lights::walk(TSubModel *Submodel, glm::mat4 const &Transform, glm::dmat4 const &Placement, float const Brightness) +{ + // visibility, lod ranges, transforms and animations as in opengl46_renderer::Render_Alpha(TSubModel *) + for (auto *submodel{Submodel}; submodel != nullptr; submodel = submodel->Next) + { + if ((submodel->iVisible == 0) || (TSubModel::fSquareDist < submodel->fSquareMinDist) || (TSubModel::fSquareDist >= submodel->fSquareMaxDist)) + { + continue; + } + auto transform{Transform}; + if (submodel->iFlags & 0xC000) + { + if (submodel->fMatrix != nullptr) + { + transform *= glm::make_mat4(submodel->fMatrix->readArray()); + } + if (submodel->b_aAnim != TAnimType::at_None) + { + submodel->RaAnimation(transform, submodel->b_aAnim); + } + } + if (submodel->eType == TP_FREESPOTLIGHT) + { + add_light_point(*submodel, transform, Placement, Brightness); + } + if (submodel->Child != nullptr) + { + walk(submodel->Child, transform, Placement, Brightness); + } + } +} + +void opengl46_local_lights::add_light_point(TSubModel const &Submodel, glm::mat4 const &Transform, glm::dmat4 const &Placement, float const Brightness) +{ + // lit points: with self-illumination, past its threshold. the compartments of vehicles set it with their lights + if ((Submodel.f4Emision.a <= 0.f) || (Global.fLuminance >= Submodel.fLight) || (Submodel.fVisible <= 0.f)) + { + return; + } + auto const diffuse{Submodel.DiffuseOverride.r < 0.f ? // -1 indicates no override + glm::vec3{Submodel.f4Diffuse} : + Submodel.DiffuseOverride}; + // hotspotpower of the model; this renderer keeps it apart from the colour + auto const multiplier{Submodel.diffuseMultiplier > 0.f ? Submodel.diffuseMultiplier : 1.f}; + auto const color{to_linear(diffuse) * (Brightness * multiplier * Submodel.fVisible * std::min(Submodel.f4Emision.a, 1.f))}; + if (max_component(color) <= LIGHT_THRESHOLD) + { + return; + } + + light lightpoint; + lightpoint.position = glm::dvec3{Placement * glm::dvec4{glm::dvec3{Transform[3]}, 1.0}}; + auto const axis{glm::vec3{glm::dmat3{Placement} * glm::dvec3{-glm::vec3{Transform[2]}}}}; + lightpoint.direction = (glm::length2(axis) > 0.f ? glm::normalize(axis) : glm::vec3{0.f, -1.f, 0.f}); + lightpoint.color = color; + // the cone of the glare: where the point is seen from is where it shines. the glare goes from no brightness at the falloff + // angle to full at the hotspot angle, which some models have the wrong way round + lightpoint.cos_outer = std::clamp(Submodel.fCosFalloffAngle, -1.f, 0.999f); + lightpoint.cos_inner = std::clamp(std::max(Submodel.fCosHotspotAngle, lightpoint.cos_outer + 0.05f), lightpoint.cos_outer + 0.001f, 1.f); + lightpoint.range = light_range(color); + m_lights.emplace_back(lightpoint); +} + +bool opengl46_local_lights::has_light_points(TModel3d *Model) +{ + auto lookup{m_modellights.find(Model)}; + if (lookup != m_modellights.end()) + { + return lookup->second; + } + auto found{false}; + std::vector pending{Model->Root}; + while ((false == found) && (false == pending.empty())) + { + auto const *submodel{pending.back()}; + pending.pop_back(); + for (; submodel != nullptr; submodel = submodel->Next) + { + if (submodel->eType == TP_FREESPOTLIGHT) + { + found = true; + break; + } + if (submodel->Child != nullptr) + { + pending.emplace_back(submodel->Child); + } + } + } + m_modellights.emplace(Model, found); + return found; +} + +opengl46_local_lights::cell_data const &opengl46_local_lights::cell_instances(scene::basic_cell &Cell, bool const Rebuild) +{ + // light points are drawn in the translucent phase, so the instances which have them are on the translucent list of the cell + auto &instances{Cell.m_instancetranslucent}; + auto &data{m_cells[&Cell]}; + if (Rebuild || (data.source_size != instances.size()) || (data.source_data != static_cast(instances.data()))) + { + data.instances.clear(); + for (auto *instance : instances) + { + if ((instance->pModel != nullptr) && has_light_points(instance->pModel)) + { + data.instances.emplace_back(instance); + } + } + data.source_size = instances.size(); + data.source_data = instances.data(); + } + return data; +} + +opengl46_local_lights::section_data const &opengl46_local_lights::section(TModel3d *Model, TSubModel *Section, gfx::geometrybank_manager const &Geometry) +{ + auto lookup{m_sections.find(Section)}; + if (lookup != m_sections.end()) + { + return lookup->second; + } + section_data data; + glm::mat4 transform{1.f}; + if (find_parent_transform(Model->Root, Section, glm::mat4{1.f}, transform)) + { + glm::vec3 min{std::numeric_limits::max()}; + glm::vec3 max{std::numeric_limits::lowest()}; + extend_bounds(Section, transform, Geometry, false, min, max, data.has_light_points); + if (min.x <= max.x) + { + data.center = 0.5f * (min + max); + data.extent = 0.5f * (max - min); + } + } + return m_sections.emplace(Section, data).first->second; +} + +opengl46_local_lights::section_data const &opengl46_local_lights::model_bounds(TModel3d *Model, gfx::geometrybank_manager const &Geometry) +{ + auto lookup{m_models.find(Model)}; + if (lookup != m_models.end()) + { + return lookup->second; + } + section_data data; + glm::vec3 min{std::numeric_limits::max()}; + glm::vec3 max{std::numeric_limits::lowest()}; + extend_bounds(Model->Root, glm::mat4{1.f}, Geometry, true, min, max, data.has_light_points); + if (min.x <= max.x) + { + data.center = 0.5f * (min + max); + data.extent = 0.5f * (max - min); + } + return m_models.emplace(Model, data).first->second; +} + +std::string opengl46_local_lights::info() const +{ + return "local lights: " + std::to_string(m_lights.size()) + " (scenery " + std::to_string(m_scenerycount) + ", vehicles " + std::to_string(m_vehiclecount) + ", compartments " + + std::to_string(m_compartmentcount) + ")" + (m_cablampon ? " + cab lamp" : "") + ", placed " + std::to_string(m_placed) + ", over capacity " + std::to_string(m_dropped) + + ", gather " + to_string(m_gathertime, 2) + " ms"; +} + +void opengl46_local_lights::draw_debug_ui() +{ + ImGui::TextUnformatted("local lights (need clustered lights)"); + ImGui::Checkbox("light points of scenery models", &Global.gfx_lights_scenery); + ImGui::Checkbox("light points and compartments of vehicles", &Global.gfx_lights_vehicles); + ImGui::Checkbox("cab lamp as a light", &Global.gfx_lights_cab); + ImGui::SliderFloat("light point brightness", &Global.gfx_lights_freespot_intensity, 0.f, 50.f); + ImGui::SliderFloat("compartment brightness", &Global.gfx_lights_compartment_intensity, 0.f, 10.f); + ImGui::SliderFloat("cab lamp brightness", &Global.gfx_lights_cab_intensity, 0.f, 10.f); + ImGui::SliderFloat("cab ambient kept", &Global.gfx_lights_cab_bounce, 0.f, 1.f); + ImGui::SliderFloat("range, m", &Global.gfx_lights_range, 0.f, 1000.f); + ImGui::Text("gathered %u: scenery %u, vehicles %u, compartments %u%s", static_cast(m_lights.size()), m_scenerycount, m_vehiclecount, m_compartmentcount, + m_cablampon ? ", cab lamp" : ""); + ImGui::Text("in the light buffer %u, over capacity %u", m_placed, m_dropped); + ImGui::Text("gather %.2f ms (cpu)", m_gathertime); +} diff --git a/rendering/opengl46/opengl46locallights.h b/rendering/opengl46/opengl46locallights.h new file mode 100644 index 00000000..63db7c1b --- /dev/null +++ b/rendering/opengl46/opengl46locallights.h @@ -0,0 +1,125 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include + +namespace scene +{ +class basic_region; +class basic_cell; +} // namespace scene +namespace gfx +{ +class geometrybank_manager; +} +class TModel3d; +class TSubModel; +class TAnimModel; +class TDynamicObject; + +// lights of the opengl 4.6 renderer other than the sun and the vehicle headlights, which go to the light clusters: +// - the light points (freespot lights) of scenery models and of vehicles, taken the way the experimental renderer takes them: +// a point which is visible, has self-illumination (emission) and is past its self-illumination threshold shines as a spot +// light along the -z axis of its submodel, with the cone of its glare. lamps of scenery models switched with Light_On / Light_Off +// and the self-illumination of vehicle compartments (TDynamicObject::Sections) work through the same state +// - the lit compartments of vehicles (low poly interior sections) without light points of their own: a point light in the middle +// - the lamp of the occupied cab: a point light under the top of the cab model, in place of the ambient light it used to add +// gathered once per frame around the camera, written to the light buffer by opengl46_renderer::Update_Lights() +class opengl46_local_lights +{ + public: + struct light + { + glm::dvec3 position; // world space + glm::vec3 direction{0.f, -1.f, 0.f}; // world space, unit; axis of the cone + glm::vec3 color; // linear, the brightness included + float cos_outer{-1.f}; // cosine of the half angle of the cone; not above 0: the light is not limited to a cone + float cos_inner{-1.f}; // cosine of the half angle within which the light is at its full brightness; above cos_outer + float range{0.f}; // distance at which the light ends, metres + }; + + opengl46_local_lights() = default; + opengl46_local_lights(const opengl46_local_lights &) = delete; + opengl46_local_lights &operator=(const opengl46_local_lights &) = delete; + + // collects the lights within Global.gfx_lights_range of Camera, for the frame about to be drawn. it sets the state of the + // shared models of the instances and vehicles it goes through, as the render does before it draws each of them. + // Geometry: the geometry the models were loaded to, for the bounds of compartments and cabs; Framestamp: the number of the frame + void gather(glm::dvec3 const &Camera, scene::basic_region &Region, gfx::geometrybank_manager const &Geometry, unsigned int const Framestamp); + // forgets the lights and everything kept about the scene, which is about to be replaced (a scenery being loaded) + void reset(); + // the lights of models and vehicles, the most important (closest and brightest) first + std::vector const &lights() const + { + return m_lights; + } + // the lamp of the occupied cab, when it is on and the cab is drawn; nullptr otherwise + light const *cab_lamp() const + { + return m_cablampon ? &m_cablamp : nullptr; + } + // counts of the last pass which wrote the lights to the light buffer: placed, and left out for the lack of room + void placed(std::uint32_t const Placed, std::uint32_t const Dropped) + { + m_placed = Placed; + m_dropped = Dropped; + } + // line for the statistics of the debug panel + std::string info() const; + // controls and counters for the debug window + void draw_debug_ui(); + + private: + struct section_data + { + glm::vec3 center{0.f}; // middle of the bounds, model space + glm::vec3 extent{0.f}; // half the size of the bounds + bool has_light_points{false}; // the section holds light points, which light it in place of the one made for it + }; + struct cell_data + { + std::vector instances; // instances of models with light points + // what the list was made from: when the instance list of the cell changes, the list is made anew + std::size_t source_size{0}; + void const *source_data{nullptr}; + }; + + // lights of the light points of the submodel tree, gone through the way the render goes through it. Transform: model space of + // Submodel's parent, Placement: model space to world space, Brightness: of a light point of white colour + void walk(TSubModel *Submodel, glm::mat4 const &Transform, glm::dmat4 const &Placement, float const Brightness); + void add_light_point(TSubModel const &Submodel, glm::mat4 const &Transform, glm::dmat4 const &Placement, float const Brightness); + void gather_instance(TAnimModel *Instance, glm::dvec3 const &Camera, float const Brightness, unsigned int const Framestamp); + void gather_vehicle(TDynamicObject *Vehicle, glm::dvec3 const &Camera, gfx::geometrybank_manager const &Geometry, float const Freespotbrightness, float const Compartmentbrightness); + void gather_cab_lamp(gfx::geometrybank_manager const &Geometry); + bool has_light_points(TModel3d *Model); + cell_data const &cell_instances(scene::basic_cell &Cell, bool const Rebuild); + section_data const §ion(TModel3d *Model, TSubModel *Section, gfx::geometrybank_manager const &Geometry); + section_data const &model_bounds(TModel3d *Model, gfx::geometrybank_manager const &Geometry); + + std::vector m_lights; + light m_cablamp; + bool m_cablampon{false}; + std::unordered_map m_modellights; // whether the model has light points + std::unordered_map m_cells; + std::unordered_map m_sections; // compartments, by their submodel + std::unordered_map m_models; // bounds of cab models + bool m_editorseen{false}; // the editor was on in the last frame: the instance lists of the cells may have changed + // statistics + std::uint32_t m_scenerycount{0}; + std::uint32_t m_vehiclecount{0}; + std::uint32_t m_compartmentcount{0}; + std::uint32_t m_placed{0}; + std::uint32_t m_dropped{0}; + double m_gathertime{0.0}; // milliseconds, smoothed +}; diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index 8cbafb86..e7d8213d 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -617,6 +617,14 @@ bool opengl46_renderer::Render() // used, the directional part of the ambient light m_fogcolor = Global.FogColor; m_sky.update(m_sunlight, m_fogcolor, simulation::Environment.skydome(), simulation::Environment.sun().getDirection(), simulation::Environment.sun().getAngle()); + // lamps of models and vehicles around the camera and the cab lamp, for the views of the frame to take into their light buffers + if ((false == Global.gfx_skiprendering) && (simulation::Region != nullptr)) + m_locallights.gather(Global.pCamera.Pos, *simulation::Region, m_geometry, m_framestamp); + } + else + { + // a scenery is being loaded: the cells, instances and models the lights were taken from may be gone + m_locallights.reset(); } // generate new frame opengl_texture::reset_unit_cache(); @@ -739,7 +747,8 @@ void opengl46_renderer::SwapBuffers() + "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), " - + ( Global.gfx_lights_clustered ? "clustered" : "first " + std::to_string( std::max( Global.DynamicLightCount, 0 ) ) ); + + ( Global.gfx_lights_clustered ? "clustered" : "first " + std::to_string( std::max( Global.DynamicLightCount, 0 ) ) ) + + "\n" + m_locallights.info(); 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 ) @@ -778,13 +787,15 @@ void opengl46_renderer::draw_debug_ui() auto const ¶ms{clusters->params()}; ImGui::Text("grid %u x %u x %u, %s", gl::CLUSTER_GRID_X, gl::CLUSTER_GRID_Y, gl::CLUSTER_GRID_Z, params.valid ? "built" : "none (orthographic view)"); ImGui::Text("near %.3f m, far %.1f m", params.znear, params.zfar); - ImGui::Text("lights %u (sun excluded), %u over capacity of %u", m_lights_lit, m_lights_dropped, static_cast(gl::MAX_LIGHTS - 1)); + ImGui::Text("headlights %u, %u over capacity of %u", m_lights_lit, m_lights_dropped, static_cast(gl::MAX_LIGHTS - 1)); if (auto const cullstats = clusters->stats()) { ImGui::Text("index list %u of %u", cullstats->index_count, gl::CLUSTER_COUNT * gl::MAX_LIGHTS_PER_CLUSTER); ImGui::Text("overflow: %u lights dropped from full clusters (limit %u each), peak %u", cullstats->overflow_count, gl::MAX_LIGHTS_PER_CLUSTER, cullstats->overflow_peak); } } + ImGui::Separator(); + m_locallights.draw_debug_ui(); } ImGui::End(); @@ -1019,7 +1030,8 @@ void opengl46_renderer::Render_pass(viewport_config &vp, rendermode const Mode) glDebug("render opaque cab"); model_ubs.future = glm::mat4(); auto const *vehicle{ simulation::Train->Dynamic() }; - if( vehicle->InteriorLightLevel > 0.f ) { + // the lamp lighting the shadows of the sun, unless it is a light of its own + if( ( vehicle->InteriorLightLevel > 0.f ) && ( false == m_cablamplit ) ) { setup_shadow_color( glm::min( colors::white, m_shadowcolor + glm::vec4( vehicle->InteriorLight * vehicle->InteriorLightLevel, 1.f ) ) ); } Render_cab( vehicle, vehicle->InteriorLightLevel, false ); @@ -1052,7 +1064,7 @@ void opengl46_renderer::Render_pass(viewport_config &vp, rendermode const Mode) { model_ubs.future = glm::mat4(); auto *vehicle { simulation::Train->Dynamic() }; - if( vehicle->InteriorLightLevel > 0.f ) { + if( ( vehicle->InteriorLightLevel > 0.f ) && ( false == m_cablamplit ) ) { setup_shadow_color( glm::min( colors::white, m_shadowcolor + glm::vec4( vehicle->InteriorLight * vehicle->InteriorLightLevel, 1.f ) ) ); } if (Global.Overcast > 1.0f) @@ -3978,8 +3990,10 @@ bool opengl46_renderer::Render_cab(TDynamicObject const *Dynamic, float const Li auto const old_ambient { light_data.ambient }; auto const luminance { Global.fLuminance * ( Dynamic->fShade > 0.0f ? Dynamic->fShade : 1.0f ) }; if( Lightlevel > 0.f ) { - // crude way to light the cabin, until we have something more complete in place - light_data.ambient += packed_vec3( ( Dynamic->InteriorLight * Lightlevel ) * std::clamp( 1.25f - (float)luminance, 0.f, 1.f ) ); + // crude way to light the cabin, until we have something more complete in place. with the lamp of the cab among the + // lights a share of it is kept for the light which bounces around the cab + auto const share { m_cablamplit ? Global.gfx_lights_cab_bounce : 1.f }; + light_data.ambient += packed_vec3( ( Dynamic->InteriorLight * Lightlevel ) * std::clamp( 1.25f - (float)luminance, 0.f, 1.f ) * share ); upload_light_header(); } @@ -6039,6 +6053,18 @@ void opengl46_renderer::Update_Lights(light_array &Lights, bool const Headlights size_t light_i = 1; m_lights_dropped = 0; + // the lights other than the headlights are only taken from the lists of the clusters. the lamp of the occupied cab goes first: + // it is next to the camera, and it is not to be the one left out of a full cluster + auto const locallights{Headlights && Clustered}; + m_cablamplit = false; + if( locallights ) { + if( auto const *lamp{ m_locallights.cab_lamp() } ) { + Write_Local_Light( light_data.lights[ light_i++ ], *lamp, camera, mv ); + m_cablamplit = true; + } + } + auto const headlightsstart{light_i}; + for (auto const &scenelight : Lights.data) { if (!Headlights || scenelight.intensity == 0.f) { @@ -6137,11 +6163,28 @@ void opengl46_renderer::Update_Lights(light_array &Lights, bool const Headlights ++light_i; } + m_lights_lit = static_cast(light_i - headlightsstart); + // lamps of models and vehicles, the most important first, those which may be seen lighting something in this view + if( locallights ) { + uint32_t placed{ 0 }; + uint32_t dropped{ 0 }; + for( auto const &local : m_locallights.lights() ) { + if( false == m_renderpass.pass_camera.visible( scene::bounding_area{ local.position, local.range } ) ) { + continue; + } + if( light_i >= gl::MAX_LIGHTS ) { + ++dropped; + continue; + } + Write_Local_Light( light_data.lights[ light_i++ ], local, camera, mv ); + ++placed; + } + m_locallights.placed( placed, dropped ); + } // every light is there for the clusters to be culled from, while the shading loop over the lights keeps to the number - // of them it always went through + // of headlights it always went through (without the clusters only the headlights are written, from the first slot on) 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_count = 1 + std::min(m_lights_lit, static_cast(std::max(Global.DynamicLightCount, 0))); light_data.lights_clustered = Clustered ? 1 : 0; light_data.env_ggx = ( Global.gfx_envmap_ggx && m_envmap_bound ) ? 1 : 0; // fill sunlight data @@ -6169,6 +6212,29 @@ void opengl46_renderer::Update_Lights(light_array &Lights, bool const Headlights light_ssbo->update(&light_data, 0, offsetof(gl::light_data, lights) + sizeof(gl::light_element) * light_i); } +void opengl46_renderer::Write_Local_Light(gl::light_element &Light, opengl46_local_lights::light const &Source, glm::dvec3 const &Camera, glm::mat4 const &View) +{ + // a cone wider than a half of the space is no limit for the culling, but the shading still fades the light out of it + Light.type = gl::light_element::SPOT; + Light.pos = View * glm::vec4(glm::vec3{Source.position - Camera}, 1.0f); + Light.dir = glm::normalize(glm::vec3{View * glm::vec4(Source.direction, 0.0f)}); + Light.in_cutoff = Source.cos_inner; + Light.out_cutoff = Source.cos_outer; + if (Source.cos_outer <= -1.f) + { + // all around + Light.type = gl::light_element::POINT; + } + Light.color = Source.color; + Light.intensity = 1.0f; + Light.ambient = 0.0f; + // the shading takes the light to end at 1 / sqrt(quadratic) + Light.linear = 0.0f; + Light.quadratic = 1.0f / sq(std::max(Source.range, 0.01f)); + Light.headlight_projection = glm::mat4{1.0f}; + Light.headlight_weights = glm::vec4{0.0f}; +} + void opengl46_renderer::Update_Clusters(viewport_config &vp, glm::ivec2 const &Targetsize) { glDebug("Update_Clusters"); diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index dbb22cc6..17ac82d6 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -35,6 +35,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/opengl46/opengl46sky.h" #include "rendering/opengl46/opengl46exposure.h" #include "rendering/opengl46/opengl46ao.h" +#include "rendering/opengl46/opengl46locallights.h" // bare-bones render controller, in lack of anything better yet class opengl46_renderer : public gfx_renderer { @@ -351,8 +352,11 @@ class opengl46_renderer : public gfx_renderer { void Render_Alpha_text(TSubModel *Submodel); // characters of a text display // 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 + // 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. + // with Headlights and Clustered the cab lamp (first) and the lamps of models and vehicles (after the headlights) are written as well void Update_Lights(light_array &Lights, bool const Headlights = true, bool const Clustered = false); + // writes a light of a model or a vehicle, or the cab lamp, to the slot of the light buffer, in the view space of the pass + void Write_Local_Light(gl::light_element &Light, opengl46_local_lights::light const &Source, glm::dvec3 const &Camera, glm::mat4 const &View); // brings the light cluster grid of the viewport up to date with the projection of the color pass being drawn. // Targetsize is the size in pixels of what the pass draws to, which gl_FragCoord of the shading counts in void Update_Clusters(viewport_config &vp, glm::ivec2 const &Targetsize); @@ -481,6 +485,10 @@ class opengl46_renderer : public gfx_renderer { // headlights placed in the light buffer by the last Update_Lights(), and those which are on but did not fit in it uint32_t m_lights_lit = 0; uint32_t m_lights_dropped = 0; + // lights of models and vehicles and the cab lamp, gathered once per frame for the light clusters + opengl46_local_lights m_locallights; + // the last Update_Lights() placed the cab lamp in the light buffer: the cab is lit by it, not by an addition to the ambient light + bool m_cablamplit = false; double m_precipitationrotation; diff --git a/shaders/cluster_cull.comp b/shaders/cluster_cull.comp index f487a470..1754a171 100644 --- a/shaders/cluster_cull.comp +++ b/shaders/cluster_cull.comp @@ -44,8 +44,9 @@ float light_range(light_s light) float denominator = b + sqrt(b * b - 4.0 * a * c); return (denominator > 1e-12) ? min(-2.0 * c / denominator, UNLIMITED_RANGE) : UNLIMITED_RANGE; } - // point and spot lights fall off with the square of the distance - return sqrt(k); + // point and spot lights fall off with the square of the distance; the shading ends them at 1 / sqrt(quadratic) where it is set + float range = sqrt(k); + return (light.quadratic > 0.0) ? min(range, inversesqrt(light.quadratic)) : range; } bool sphere_touches_box(vec3 center, float radius, vec3 lower, vec3 upper) @@ -103,8 +104,9 @@ bool light_reaches_box(light_s light, float range, vec3 lower, vec3 upper) if (light.type == LIGHT_HEADLIGHTS) return box_in_headlight_beam(light.headlight_projection, center, extent); - if (light.type == LIGHT_SPOT) - // the cone is tested against the sphere around the box, which lets some of the corners through + if ((light.type == LIGHT_SPOT) && (light.out_cutoff > 0.0)) + // the cone is tested against the sphere around the box, which lets some of the corners through. a cone wider than + // a half of the space is taken as the whole sphere return sphere_touches_cone(center - light.pos, length(extent), normalize(light.dir), light.out_cutoff); return true; diff --git a/shaders/light_common.glsl b/shaders/light_common.glsl index 72da5e63..357c2a5f 100644 --- a/shaders/light_common.glsl +++ b/shaders/light_common.glsl @@ -229,6 +229,29 @@ vec2 calc_headlights(light_s light, vec3 fragnormal, vec3 dpdx, vec3 dpdy) return part * atten * lightintensity; } +// closest distance (squared) the light of a lamp is computed for: nearer the inverse square would grow without a limit +const float LOCAL_LIGHT_MIN_DISTANCE2 = 0.01; + +// light of a lamp (point or spot light: the lamps of models and vehicles, the cab lamp) reaching the fragment, x the diffuse +// and y the specular part. it falls off with the square of the distance, and smoothly to nothing at the range of the light, +// 1 / sqrt(quadratic), past which the culling of the clusters does not take it. a spot light fades from its inner cone to the +// outer one +vec2 calc_local_light(light_s light, vec3 fragnormal) +{ + vec3 tolight = light.pos - f_pos.xyz; + float distance2 = dot(tolight, tolight); + vec3 light_dir = tolight * inversesqrt(max(distance2, 1e-8)); + float edge = clamp(1.0 - distance2 * distance2 * (light.quadratic * light.quadratic), 0.0, 1.0); + float atten = edge * edge / max(distance2, LOCAL_LIGHT_MIN_DISTANCE2); + if (light.type == LIGHT_SPOT) + atten *= smoothstep(light.out_cutoff, light.in_cutoff, dot(-light_dir, normalize(light.dir))); + if (atten <= 0.0) + return vec2(0.0); + vec2 part = calc_light(light_dir, fragnormal); + // the highlight of a tiny light on a polished surface is kept within what the image can take + return vec2(part.x, min(part.y, 16.0) * part.x) * atten; +} + // 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 @@ -240,9 +263,9 @@ uint fragment_cluster() 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 +// adds the light of the headlights and lamps 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 +// lights[] otherwise, which are headlights only vec3 add_headlights(vec3 fragcolor, vec3 fragnormal) { // worked out before the loops, which the fragments of a quad do not leave together @@ -255,7 +278,7 @@ vec3 add_headlights(vec3 fragcolor, vec3 fragnormal) 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); + vec2 part = (light.type == LIGHT_HEADLIGHTS) ? calc_headlights(light, fragnormal, dpdx, dpdy) : calc_local_light(light, fragnormal); fragcolor += light.color * (part.x + part.y * param[1].y) * light.intensity; } } diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index 863e111d..d1738b37 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -1593,6 +1593,14 @@ global_settings::ConfigParse_gfx_sky( cParser &Parser, std::string_view const To Parser >> gfx_postfx_ao_slices >> gfx_postfx_ao_steps; gfx_postfx_ao_slices = std::clamp( gfx_postfx_ao_slices, 1, 8 ); gfx_postfx_ao_steps = std::clamp( gfx_postfx_ao_steps, 1, 16 ); } + else if( Token == "gfx.lights.scenery" ) { readswitch( gfx_lights_scenery ); } + else if( Token == "gfx.lights.vehicles" ) { readswitch( gfx_lights_vehicles ); } + else if( Token == "gfx.lights.cab" ) { readswitch( gfx_lights_cab ); } + else if( Token == "gfx.lights.freespot.intensity" ) { readvalue( gfx_lights_freespot_intensity, 0.f, 1000.f ); } + else if( Token == "gfx.lights.compartment.intensity" ) { readvalue( gfx_lights_compartment_intensity, 0.f, 100.f ); } + else if( Token == "gfx.lights.cab.intensity" ) { readvalue( gfx_lights_cab_intensity, 0.f, 100.f ); } + else if( Token == "gfx.lights.cab.bounce" ) { readvalue( gfx_lights_cab_bounce, 0.f, 1.f ); } + else if( Token == "gfx.lights.range" ) { readvalue( gfx_lights_range, 0.f, 2000.f ); } else { return false; } return true; @@ -1859,6 +1867,14 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "gfx.postfx.ao.strength", gfx_postfx_ao_strength ); export_as_text( Output, "gfx.postfx.ao.bounce", gfx_postfx_ao_bounce ); Output << "gfx.postfx.ao.quality " << gfx_postfx_ao_slices << " " << gfx_postfx_ao_steps << "\n"; + export_as_text( Output, "gfx.lights.scenery", gfx_lights_scenery ); + export_as_text( Output, "gfx.lights.vehicles", gfx_lights_vehicles ); + export_as_text( Output, "gfx.lights.cab", gfx_lights_cab ); + export_as_text( Output, "gfx.lights.freespot.intensity", gfx_lights_freespot_intensity ); + export_as_text( Output, "gfx.lights.compartment.intensity", gfx_lights_compartment_intensity ); + export_as_text( Output, "gfx.lights.cab.intensity", gfx_lights_cab_intensity ); + export_as_text( Output, "gfx.lights.cab.bounce", gfx_lights_cab_bounce ); + export_as_text( Output, "gfx.lights.range", gfx_lights_range ); export_as_text( Output, "python.enabled", python_enabled ); export_as_text( Output, "python.threadedupload", python_threadedupload ); export_as_text( Output, "python.uploadmain", python_uploadmain ); diff --git a/utilities/Globals.h b/utilities/Globals.h index c935b105..a8e68e74 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -332,6 +332,16 @@ struct global_settings { float gfx_postfx_ao_bounce { 0.3f }; // albedo of the surfaces assumed for the light coming back after more bounces int gfx_postfx_ao_slices { 3 }; // directions searched around each pixel int gfx_postfx_ao_steps { 4 }; // samples on each side of a direction + // local lights of the opengl 4.6 renderer, besides the sun and the vehicle headlights (debug window "Light clusters"). + // they go through the light clusters, so they need gfx_lights_clustered + bool gfx_lights_scenery { true }; // light points (freespot lights) of scenery models light their surroundings + bool gfx_lights_vehicles { true }; // light points of vehicles and their lit compartments light their surroundings + bool gfx_lights_cab { true }; // the lamp of the occupied cab is a point light instead of an addition to the ambient light + float gfx_lights_freespot_intensity { 10.f }; // brightness of a light point of a model + float gfx_lights_compartment_intensity { 1.f }; // brightness of the light of a lit vehicle compartment + float gfx_lights_cab_intensity { 1.5f }; // brightness of the cab lamp + float gfx_lights_cab_bounce { 0.3f }; // share of the former ambient light of the cab lamp kept, for the light which bounces around the cab + float gfx_lights_range { 300.f }; // distance from the camera the lights of models and vehicles are taken from, metres float ui_fontsize = 13.0f; float ui_scale = 1.0f;