View Issue Details

IDProjectCategoryView StatusLast Update
0005765The Dark ModCodingpublic29.09.2021 14:58
ReporterMirceaKitsune Assigned To 
Status newResolutionopen 
Product VersionSVN 
Summary0005765: Using visportals to remove inaccessible rooms from a light's perspective before preforming calculations
DescriptionFirst I'd like to explain exactly what I'm suggesting, why, and how it would work. The idea emerged from this thread, an experiment in attaching ambient lights to standard light sources as a means of implementing cheap estimated irradiance (bounce lighting), something I'm close to succeeding at only running into a handful of engine limitations.

The issue: Let's say you have an ambient light that doesn't cast shadows, located in a typical brush room containing portal openings that lead inside (doors, windows, etc). The origin of this light is located in the room but its radius box exceeds it and is poking through the walls into the outside world or a neighboring room. With the current functionality, as long as the light is being rendered to the player and the GPU is aware of it, it will always affect the room it's poking in as well. There are two separate issues with this, each its own reason why I'm proposing the suggestion I'll get to shortly:

1 - If you want an ambient light that doesn't unrealistically leak through walls when its box pokes through them, there's currently no satisfactory solution. Shadows are the only thing that keeps lights from leaking between rooms, without shadows lights that intersect walls illuminate both sides. Currently this makes my attempt to implement proper irradiance lamps impossible despite getting beautiful and promising results otherwise.

2 - Although you don't see normal lights leaking through walls thanks to shadows, even standard lights are affected by having no visleaf based culling, due to resources being wasted to calculate lighting and shadowing on brushes and entities behind walls the light can't travel to. Let's say you have a torch placed on the exterior wall of a building, its light not intersecting any door or window leading into that building... on the other side of this wall just 16 units behind the torch, there's a shelf full of books and bottles and other little details: You'll never see the light of the torch poking into the room, but the light is aware of the shelf and its objects because they're within its radius, the engine wastes precious resources calculating them against that light for no reason. Such scenarios likely translate to a FPS loss in all FM's, every map has gas lamps or candles against walls which poke through the wall and touch entities there... given how heavy light calculations are fixing this would likely bring a noticeable performance improvement.

There's an obscure spawnarg to prevent this which I initially hoped would help me fix my bounce lights: On the light entity you can set "areaLock" "origin" to prevent your light from affecting any room but the one it's located in. This however falls in the other extreme: The light cuts off in front of every portal surface, causing illumination to inexplicably disappear in a sharp line whenever it meets a visportal... not only will the light not shine through any door or window, but even portals between walls used to optimize rooms will cause the ugly cutoffs.

This brings us to my proposal, which is essentially a smarter and more selective version of what arealock does: The light renders in the room where its origin is present, as well as other rooms it intersects BUT only if its bounding box is also intersecting at least one open portal leading to that other room. This would ensure the light operates only in spaces it's logically able to access, without leaking or bothering to calculate anything outside, while also not cutting off in front of open portals it should go through. While the important thing is to allow this as a spawnarg for ambient lights so we can plug absurd wall leaks, I'd suggest enabling it on all lights for the performance improvement obtained by ignoring polygons in places the light intersects but can't actually reach. Needless to say this is disabled for lights covering the entire map like fog and ambient_world.

I've attached a screenshot to exemplify the exact functionality I'm suggesting. It depicts three rooms connected by visportal doors and a light in the rightmost one: The rooms painted in green represent rooms the light can affect and act in, whereas the room in red represents a room the light would ignore. The reason the middle room is affected is because the light touches the visportal leading from its own room into it... the leftmost room is not because, although the light's bounding box pokes inside, it does not also touch the visportal leading from the middle room to it. The outside of the entire three-room cube would also be ignored by the light, only the middle room and the rightmost room where it's located exist from the light's perspective.
Additional InformationThe implementation from a technical perspective: We'll start from the behavior of the existing "arealock origin" as this does what we want initially, meaning anything apart from the visleaf of the light's origin is ignored. Before the light decides what to calculate, we first loop through all the visportals of this room. If we detect a portal intersects the box of the light and it's an active / open portal, we take the decision to include the room behind this portal in the calculation, that room and its contents now exist to the light. Now we loop through all visportals in this other room: If we detect one of them intersects the box of our light, we include the room it leads to as the third room in the light's existence. The process repeats until the visportals of all rooms touched by the light's box have been scanned. Once this scan is complete, the light can begin lighting surfaces and casting shadows, using only surfaces in the rooms that were included.

The extra calculation should be ridiculously cheap on the CPU: It only involves checking if a 2D surface intersects a 3D cube. Most rooms typically have less than 10 portals, and each portal is almost always a rectangular face so 4 vertices to check... even an electric calculator for 90's should be able to do this per cycle without any lag. It gets even cheaper if we can break out of the loop once we know a room is open so we don't scan other portals leading to that same room. Not to mention the scan only needs to be redone when the light moves, for static lights it's done once at map start... with the exception of doors opening and closing their portals, that will need to trigger a rescan on all lights touching the door upon open / close.

Here is some code to help with the implementation, it's what I typically do to detect intersections from vertices. An engine developer will need to properly convert that to C++ grammar and the variables the engine works with while correcting any mistakes I might have made there. The only assumption this makes is that we know the radius of the light as well as the vertices of its room's visportals. You'd essentially run this function in a loop for each portal.

// self.mins is the starting position of the light's bounding box (bottom-left-back vertice)
// self.maxs is the ending position of the light's bounding box (top-right-front vertice)
// portal_vertices is an array containing the positions of all the vertices of a visportal surface
bool light:check_intersection(vec3d[] portal_vertices)
    // generate a virtual bounding box encompassing the vertices of the portal face
    float portal_min_x, portal_min_y, portal_min_z = 0;
    float portal_max_x, portal_max_y, portal_max_z = 999999;
    for(int i = 0; i < length(portal_vertices); i++)
        vec3d v = portal_vertices[i];
        if(v.x > portal_min_x)
            portal_min_x = v.x;
        if(v.x < portal_max_x)
            portal_max_x = v.x;
        if(v.y > portal_min_y)
            portal_min_y = v.y;
        if(v.y < portal_max_y)
            portal_max_y = v.y;
        if(v.z > portal_min_z)
            portal_min_z = v.z;
        if(v.z < portal_max_z)
            portal_max_z = v.z;

    // check if the boundaries of the portal intersect that of the light
    if(portal_min_x <= self.maxs.x && portal_max_x >= self.mins.x)
    if(portal_min_y <= self.maxs.y && portal_max_y >= self.mins.y)
    if(portal_min_z <= self.maxs.z && portal_max_z >= self.mins.z)
        return true;
    return false;
Tagsengine, graphics, lighting, vis, visportals




28.09.2021 00:07


Screenshot_20210927_213327.png (210,599 bytes)   
Screenshot_20210927_213327.png (210,599 bytes)   


28.09.2021 02:28

reporter   ~0014375

Rarely touched C++ in my life and trying to now still makes my head spin: Especially as I'm not familiar with the engine code it's unlikely I could patch this on my own. But I looked on Github at how arealock does what it's doing: To my ridiculously untrained eye which might be missing something obvious, it looks like this should be an easy one for those with actual experience.

Visleafs seem to be identified by an ID as I hoped, referenced there as areanum. You seem to tell the engine you want that light to appear in an area by using AddLightRefToArea(light, area) to put it there: This is only done once at the moment, the question is if you can make that call multiple times or give it an array to specify a list of rooms, and I'm assuming that code does execute each frame? If yes that's prolly where this change goes: Just needs a way to loop through visportals per room and see which rooms they lead to in order, can't begin to imagine how that's done but if anyone can this is hopefully solvable.

Obviously there should be a spawnarg to enable it per light, perhaps another value for the existing arealock so we don't create a new one? In this case atdm:light_base should be given this spawnarg in the defs... ambient_world and fog make exceptions for obvious reasons, but torch and candle and lamp lights should safely be able to use it for optimization without any visual or gameplay difference.


29.09.2021 03:13

administrator   ~0014376

I don't like the proposal.
It is the same kind of hack as areaLock, with the same issues which make it not applicable to dynamic lights.

And even for static lights: it is not good that a torch on the wall lights through the wall when a doorways is at a distance of few meters.
And when static light does not have a door in its radius, then mapper can put areaLock on it.

We are not here to ship a game tomorrow with a fixed number of levels.
Hacks accumulate and make everything more complicated in future.


29.09.2021 12:39

reporter   ~0014377

I have an alternative to this idea them. Essentially the same thing except per-brush, independent of portals and the vis system. The light loops through all worldspawn brushes it intersects preforming the same intersection check: If that brush or a set of overlapping brushes block that direction without leaving any cracks, so that no vertice / edge / face of the light's box reaches past that brush and the brush fully stops it, the light is cut and discards everything behind those brushes.... obviously if the brush in cause is using a non-transparent texture and we include closed portal doors here.

This might in fact be even better, as it would also account for walls not tied to a visportal. And it should be about as cheap on the CPU to do per frame... obviously it would only be done for brushes reached by the light, a few simple shapes typically cubes / rectangles with 8 vertices. It would provide the same two benefits for my suggestion: We could keep local ambient lights from leaking through walls, whereas using it on all lights would help them discard unneeded data and optimize performance. This couldn't be called a hack either, it would be a controlled common sense optimization that should always work.

As Cabalistic pointed out, it can (kind of) be seen as duplicating the functionality of shadows, albeit it doesn't feel anything like shadows to me: It's a way of chopping the light before we calculate any lighting and shadows to discard what that light doesn't need to see... think of the light as a normal cubic brush in Darkradiant, then using the brush cut tool to cut 2D planes across it so it doesn't go through other brushes. What I'm suggesting here is NOT volumetric projection as we don't project anything: We'd only cut the light's box to a convex shape when it can't get past a set of surfaces before deciding what gets included in its calculations.


29.09.2021 13:04

reporter   ~0014378

Visualization for this idea. In 2D, same concept applied in 3D for any brush patterns.
light_cutting.png (117,605 bytes)   
light_cutting.png (117,605 bytes)   


29.09.2021 14:57

administrator   ~0014379

Last edited: 29.09.2021 14:58

"The light loops through all worldspawn brushes it intersects"
     This has to be done on CPU and it already too bad for performance.
"If that brush or a set of overlapping brushes block that direction without leaving any cracks"
     This is very hard to detect. In fact, it requires constructing BSP tree and checking volumetric connectivity.
     Such algorithms are used in dmap, and you must know it is not "realtime" at all.

Issue History

Date Modified Username Field Change
28.09.2021 00:07 MirceaKitsune New Issue
28.09.2021 00:07 MirceaKitsune Tag Attached: engine
28.09.2021 00:07 MirceaKitsune Tag Attached: graphics
28.09.2021 00:07 MirceaKitsune Tag Attached: lighting
28.09.2021 00:07 MirceaKitsune Tag Attached: vis
28.09.2021 00:07 MirceaKitsune Tag Attached: visportals
28.09.2021 00:07 MirceaKitsune File Added: Screenshot_20210927_213327.png
28.09.2021 02:28 MirceaKitsune Note Added: 0014375
29.09.2021 03:13 stgatilov Note Added: 0014376
29.09.2021 12:39 MirceaKitsune Note Added: 0014377
29.09.2021 13:04 MirceaKitsune Note Added: 0014378
29.09.2021 13:04 MirceaKitsune File Added: light_cutting.png
29.09.2021 14:57 stgatilov Note Added: 0014379
29.09.2021 14:58 stgatilov Note Edited: 0014379