17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-18 12:39:18 +02:00

Light the scene with lamps of models, vehicle compartments and the cab lamp in the GL 4.6 renderer

This commit is contained in:
2026-10-16 00:07:08 +02:00
parent 5f365a00c5
commit fd98d4eea5
12 changed files with 859 additions and 29 deletions

View File

@@ -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"

View File

@@ -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

View File

@@ -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),

View File

@@ -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

View File

@@ -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 <algorithm>
#include <array>
#include <chrono>
#include <limits>
#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<float>(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<int>(std::floor(Camera.x / scene::EU07_SECTIONSIZE + scene::EU07_REGIONSIDESECTIONCOUNT / 2))};
auto const cameraz{static_cast<int>(std::floor(Camera.z / scene::EU07_SECTIONSIZE + scene::EU07_REGIONSIDESECTIONCOUNT / 2))};
auto const reach{static_cast<int>(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<double, std::milli>(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<double>(Global.ZoomFactor)) / Global.fDistanceFactor};
if ((distancesquared < Instance->m_rangesquaredmin) || (distancesquared >= Instance->m_rangesquaredmax))
{
return;
}
Instance->RaAnimate(Framestamp);
Instance->RaPrepare();
TSubModel::fSquareDist = static_cast<float>(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<std::uint32_t>(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<TModel3d *, 2> 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<std::uintptr_t>(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<std::uint32_t>(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 &section : 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<float>(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<TSubModel const *> 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<void const *>(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<float>::max()};
glm::vec3 max{std::numeric_limits<float>::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<float>::max()};
glm::vec3 max{std::numeric_limits<float>::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<unsigned>(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);
}

View File

@@ -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 <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
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<light> 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<TAnimModel *> 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 &section(TModel3d *Model, TSubModel *Section, gfx::geometrybank_manager const &Geometry);
section_data const &model_bounds(TModel3d *Model, gfx::geometrybank_manager const &Geometry);
std::vector<light> m_lights;
light m_cablamp;
bool m_cablampon{false};
std::unordered_map<TModel3d const *, bool> m_modellights; // whether the model has light points
std::unordered_map<scene::basic_cell const *, cell_data> m_cells;
std::unordered_map<TSubModel const *, section_data> m_sections; // compartments, by their submodel
std::unordered_map<TModel3d const *, section_data> 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
};

View File

@@ -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<int>( 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 &params{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<unsigned>(gl::MAX_LIGHTS - 1));
ImGui::Text("headlights %u, %u over capacity of %u", m_lights_lit, m_lights_dropped, static_cast<unsigned>(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<uint32_t>(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<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_count = 1 + std::min<uint32_t>(m_lights_lit, static_cast<uint32_t>(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");

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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 );

View File

@@ -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;