Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent seeing light through walls #26488

Merged
merged 17 commits into from
Nov 20, 2018

Conversation

jbytheway
Copy link
Contributor

@jbytheway jbytheway commented Nov 1, 2018

Summary

SUMMARY: Interface "Prevent seeing light through walls"

Purpose of change

There's a long-standing issue where walls that are lit from one side appear bright from the other side. This is strange and unintuitive.

I have a proof-of-concept solution, and wanted to share it for feedback before spending the time to polish it.

Describe the solution

First some pictures.
Before:
cata-lighting-before
After:
cata-lighting-after
Works with vehicle lights too:
cata-lighting-headlights
The algorithm is fairly simple. Rather than storing a single illumination value in the light map, when a tile is opaque (like a wall) we store four values: one for each quadrant of 2D space. Each value represents the illumination from that quadrant. When computing apparent visibility, we check which other tiles adjacent to the tile in question we can see, and count only those light values corresponding to the quadrants where we can see adjacent tiles.

This works pretty well in practice, the only small annoyance I've observed is that you get a 'looking around corners' kind of effect in some cases. This can be seen in the second image above: the wall to the left of the northern door appears bright because we can also see the tile to the NE of it, and it is being lit from that direction. But that's never giving away any information the player couldn't have deduced, so I think it's fine, and it doesn't look too weird.

Fixes #13356.

Performance

No formal measurements yet, but it seems fine in "normal" situations.
There are two pieces of code to consider:

  • The light casting code. When optimized it should be only one extra branch+array index per tile here, so that ought to be fine. Even in debug build I'd expect negligible overhead.
  • Calculating lighting levels. There will be a largish overhead when calculating lighting level for opaque tiles, but the number of opaque tiles it's possible to see at once is much less than the number of non-opaque ones, so I think this should be fine too.

The current memory layout is rather cache-unfriendly. If performance testing dictates, it may be wise to swap the array dimensions around.

Still to do

  • Update the tests (including adding tests verifying that walls are lit correctly in this way).
  • Update the 3D FOV code.
  • Test performance when many light sources exist.

Additional context

I'm aware there has been discussion of a new lighting algorithm (#23996). I think this change is entirely compatible with that new algorithm.

@jbytheway jbytheway changed the title [WIP] Prevent seeing light through walls [WIP][CR] Prevent seeing light through walls Nov 1, 2018
@kevingranade
Copy link
Member

This definitely looks like the right way to do it, I've been concerned that it was going to unduly impact performance, but I hadn't thought of only applying the 4 quadrant update to opaque tiles.

I agree this seems to mesh with all the optimizations I have planned for shadowcasting (both the dynamic lighting overhaul and refactoring the shadowcasting algorithm to be breadth-first).

@AMurkin
Copy link
Contributor

AMurkin commented Nov 2, 2018

The issue is #13356.

@ZhilkinSerg ZhilkinSerg added <Enhancement / Feature> New features, or enhancements on existing Info / User Interface Game - player communication, menus, etc. [C++] Changes (can be) made in C++. Previously named `Code` <Bugfix> This is a fix for a bug (or closes open issue) labels Nov 2, 2018
@jbytheway
Copy link
Contributor Author

The tests comparing various algorithms (shadowcasting_runoff, etc.) don't pass on master. Should they? I tried rolling back to when they were first added in 4353ec9 to see if they worked then, but that version requires libtap++, which I can't easily install.

@jbytheway
Copy link
Contributor Author

OK, I found a version that does pass (f48f788); bisecting.

@jbytheway
Copy link
Contributor Author

OK, so it broke in e858c00 slightly. It looks like it might have got worse at some point since then, but it's kind of subjective, so I'm not sure. In any case, I assume I should not be worrying about that test. (Should have paid more attention to the comment written above it, I suppose...)

src/map.h Outdated
return *std::max_element( values.begin(), values.end() );
}

friend inline four_quadrants operator*( const four_quadrants &l, const four_quadrants &r ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inline is redundant here. Functions defined inside the class definition are automatically inline.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that was only true of member functions, but it seems you're right; it applies to friend definitions also. Thanks.

@kevingranade
Copy link
Member

Sorry about the confusion, most of those tests are execution aids and migration validation rather then ongoing regression tests.

@jbytheway
Copy link
Contributor Author

jbytheway commented Nov 4, 2018

There's another oddity I'd like to understand. If we look at the following chunk of code from the visibility algorithm (this is from master, not my branch):

Cataclysm-DDA/src/map.cpp

Lines 5458 to 5471 in f99a963

// Then we just search for the light level in descending order.
if( apparent_light > LIGHT_SOURCE_BRIGHT || map_cache.sm[p.x][p.y] > 0.0 ) {
return LL_BRIGHT;
}
if( apparent_light > LIGHT_AMBIENT_LIT ) {
return LL_LIT;
}
if( apparent_light > cache.vision_threshold ) {
return LL_LOW;
} else {
return LL_BLANK;
}
// Is this ever supposed to happen?
return LL_DARK;

we see that it's choosing from a sequence of decreasing light levels.

There's one strangeness which is already called out in a comment in this code. It can never return LL_DARK.

However, there is more. Look at the definitions of the constants:

#define LIGHT_SOURCE_BRIGHT 10

#define LIGHT_AMBIENT_LIT 10.0

LIGHT_SOURCE_BRIGHT and LIGHT_AMBIENT_LIT are the same, so this function can also never return LL_LIT. Is that intentional?

@jbytheway
Copy link
Contributor Author

Tests written. Some bugs found and fixed. I believe it's functionally complete now. Still want to do some performance testing.

@jbytheway jbytheway changed the title [WIP][CR] Prevent seeing light through walls [CR] Prevent seeing light through walls Nov 6, 2018
@jbytheway
Copy link
Contributor Author

(Rebased to avoid build error about mismatched Makefile)

I think this is now ready. Of course, testing by others would be welcome.

Performance testing shows very little difference between the light casting algorithms' performance (up to ~1%):

castLight on four_quadrants (denominator 10) executed 1000000 times in 655537 microseconds.
castLight on floats (denominator 10) executed 1000000 times in 652451 microseconds.

castLight on four_quadrants (denominator 100) executed 1000000 times in 652528 microseconds.
castLight on floats (denominator 100) executed 1000000 times in 651208 microseconds.

castLight on four_quadrants (denominator 10) executed 1000000 times in 857010 microseconds.
castLight on floats (denominator 10) executed 1000000 times in 848121 microseconds.

castLight on four_quadrants (denominator 100) executed 1000000 times in 651661 microseconds.
castLight on floats (denominator 100) executed 1000000 times in 655883 microseconds.

castLight on four_quadrants (denominator 10) executed 1000000 times in 651561 microseconds.
castLight on floats (denominator 10) executed 1000000 times in 649798 microseconds.

castLight on four_quadrants (denominator 100) executed 1000000 times in 656125 microseconds.
castLight on floats (denominator 100) executed 1000000 times in 651559 microseconds.

castLight on four_quadrants (denominator 10) executed 1000000 times in 855901 microseconds.
castLight on floats (denominator 10) executed 1000000 times in 846490 microseconds.

castLight on four_quadrants (denominator 100) executed 1000000 times in 651638 microseconds.
castLight on floats (denominator 100) executed 1000000 times in 652841 microseconds.

castLight on four_quadrants (denominator 10) executed 1000000 times in 820022 microseconds.
castLight on floats (denominator 10) executed 1000000 times in 816416 microseconds.

castLight on four_quadrants (denominator 100) executed 1000000 times in 651293 microseconds.
castLight on floats (denominator 100) executed 1000000 times in 652072 microseconds.

As anecdotal evidence, I also tried burning down some buildings and the performance seems fine.

I used perf while doing so; it shows that generate_lightmap is a large part of the cost in such a situation. ~35% of samples are in map::build_map_cache and 30% in map::generate_lightmap. I think that's pretty much the same as on master (it's hard to construct a repeatable experiment for comparison).

My other reason for running perf was to verify that map::update_visibility_cache was not suddenly very expensive. Indeed it is not, clocking in at ~2% of samples.

@jbytheway jbytheway closed this Nov 7, 2018
@jbytheway jbytheway reopened this Nov 7, 2018
@jbytheway jbytheway force-pushed the no_light_through_walls branch 2 times, most recently from 36c4ebf to b581833 Compare November 7, 2018 20:56
@jbytheway
Copy link
Contributor Author

jbytheway commented Nov 7, 2018

Got a slightly concerning crash on one of the mingw builds on Travis (looks like a null pointer dereference). Not sure how to debug that. Is it possible to get the actual binary that was built there so I can interpret the backtrace?

@kevingranade
Copy link
Member

The container is destroyed, you could possibly stick a command in the travis.yml file to push it somewhere, other than that youd have to try and recreate the docker image travis uses.

@jbytheway jbytheway changed the title [CR] Prevent seeing light through walls [WIP][CR] Prevent seeing light through walls Nov 8, 2018
@jbytheway jbytheway force-pushed the no_light_through_walls branch 4 times, most recently from 87fecfb to cdb27a2 Compare November 9, 2018 20:25
@jbytheway jbytheway changed the title [WIP][CR] Prevent seeing light through walls [CR] Prevent seeing light through walls Nov 10, 2018
@jbytheway
Copy link
Contributor Author

OK, after some investigation it seems my test refactoring had exposed some flakiness in other tests. All the failures I've observed should be fixed now. The current test failures are the same vehicle power ones that are failing everywhere.

This was referenced Nov 15, 2018
It fits with other similar functions there and will need to share helper
functions with them.
New lighting model to resolve the issue whereby walls appear bright when
they are lit from the other side.

For each opaque tile (such as a wall) we store four different lighting
values, one for each quadrant from which the lighting may come.  Then,
when determining the lightness of such a tile, we consider only those
values for the quadrants where we can also observe an adjacent tile in
that quadrant.
Unlike the existing shadowcasting tests, these are tests of the full
lighting and visibility engine in combination.
Make the tests more robust and more understandable, and add some more
(including one to detect the bug just fixed in the casting algorithm).
Turns out that one shouldn't have floor adjacent to outside tiles,
because all tiles adjacent to an INDOORS terrain are considered indoors.
Make a struct to represent a test case, rather than ad hoc string
arrays.
Because the player's vision threshold is based on prior lighting levels,
it requires two iterations of the lighting algorithms to stabilize for
reliable test results.
For every vision test case, test every rotation and reflection of it to
verify that the code is isotropic.
We don't need to explain why we make something static constexpr.
Unfortunately the 3D FOV algorithm is slightly different from the 2D
one, so we can't easily check for equivalence in these tests.
Put the vision-related stats in a slightly more logical order (closer to
the order in which they are computed).
Looks like various additional info has crept into this window since the
size was last set, so bump it up a bit.
Use it in the two places we were formatting these objects.
An equivalence test and a microbenchmark.
Just disable the one case where it doesn't work properly.
To match snake_case convention.
@jbytheway
Copy link
Contributor Author

I've rebased and tidied up these changes after pulling out #26701 and #26728. I think it's as clean as it's going to get and good for review.

@jbytheway jbytheway changed the title [CR] Prevent seeing light through walls Prevent seeing light through walls Nov 19, 2018
@kevingranade kevingranade merged commit e45d527 into CleverRaven:master Nov 20, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
<Bugfix> This is a fix for a bug (or closes open issue) [C++] Changes (can be) made in C++. Previously named `Code` <Enhancement / Feature> New features, or enhancements on existing Info / User Interface Game - player communication, menus, etc.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants