Skip to content

Commit cae6b90

Browse files
Merge pull request #5799 from kwvanderlinde/performance/5798-fow-performance
Improve fog rendering performance
2 parents 8ea6a87 + b426f64 commit cae6b90

13 files changed

Lines changed: 163 additions & 126 deletions

File tree

src/main/java/net/rptools/maptool/client/tool/PointerTool.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1952,9 +1952,11 @@ private boolean validateMove(
19521952
ExposedAreaMetaData meta = zone.getExposedAreaMetaData(token.getExposedAreaGUID());
19531953
tokenFog.add(meta.getExposedAreaHistory());
19541954

1955-
// Jamz: Allow a token without site to move within the current PlayerView
1955+
// Jamz: Allow a token without sight to move within the current PlayerView
19561956
if (!token.getHasSight()) {
1957-
tokenFog.add(renderer.getZoneView().getVisibleArea(new PlayerView(Role.PLAYER)));
1957+
var view = new PlayerView(Role.PLAYER);
1958+
var visibleArea = renderer.getZoneView().getVisibility(view).visibleArea();
1959+
tokenFog.add(visibleArea);
19581960
}
19591961
}
19601962

src/main/java/net/rptools/maptool/client/ui/zone/PlayerView.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ public class PlayerView {
3131
/**
3232
* Creates a player view that does not use token views.
3333
*
34-
* <p>Calling `isUsingTokenView()` on the new player view will return {@code false} and {@link
35-
* #getTokens()} should not be called.
34+
* <p>Calling {@link #isUsingTokenView()} on the new player view will return {@code false} and
35+
* {@link #getTokens()} should not be called.
3636
*
3737
* @param role The player role for the view.
3838
*/
@@ -45,8 +45,8 @@ public PlayerView(Player.Role role) {
4545
/**
4646
* Creates a player view for a token view.
4747
*
48-
* <p>Calling `isUsingTokenView()` on the new player view will return {@code false} and {@link
49-
* #getTokens()} can be called to retrieve the list of tokens.
48+
* <p>Calling {@link #isUsingTokenView()} on the new player view will return {@code true} and
49+
* {@link #getTokens()} can be called to retrieve the list of tokens.
5050
*
5151
* @param role The player role for the view.
5252
*/

src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java

Lines changed: 127 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.stream.Collectors;
2626
import java.util.stream.Stream;
2727
import javax.annotation.Nonnull;
28+
import net.rptools.lib.CodeTimer;
2829
import net.rptools.maptool.client.AppUtil;
2930
import net.rptools.maptool.client.MapTool;
3031
import net.rptools.maptool.client.ui.zone.Illumination.LumensLevel;
@@ -139,11 +140,57 @@ private void addLightSourceToken(Token token, Set<Player.Role> roles) {
139140
*/
140141
private final Map<PlayerView, Illumination> illuminationsPerView = new HashMap<>();
141142

142-
/** Map the PlayerView to its exposed area. */
143-
private final Map<PlayerView, Area> exposedAreaMap = new HashMap<>();
143+
/**
144+
* Holds the visibility information for a {@link PlayerView}.
145+
*
146+
* <p>The {@link #visibleArea()} and {@link #exposedArea()} are the fundamental areas that
147+
* describe the view's visibility and fog of war. The {@link #clearArea()} and {@link
148+
* #softFogArea()} are derived from these based on the zone's configuration.
149+
*
150+
* <p>When {@link Zone#hasFog()} returns {@code false}, the {@link #exposedArea()} is meaningless.
151+
* In this case, the area will be empty and should not be used. Because {@link #clearArea()} and
152+
* {@link #softFogArea()} derive from {@link #exposedArea()}, these are also meaningless and will
153+
* be set to the empty area.
154+
*
155+
* <p>Because of the way Fog of War has to be rendered, {@link #softFogArea()} may not be very
156+
* intuitive. Keep this rendering logic in mind when thinking about these areas:
157+
*
158+
* <ol>
159+
* <li>Start by assuming everything is covered in hard FoW.
160+
* <li>Use {@link #softFogArea()} to carve out part of the hard FoW.
161+
* <li>Use {@link #clearArea()} to carve out part of the soft FoW and hard Fow.
162+
* </ol>
163+
*
164+
* @param visibleArea The combined visible area of all tokens in the view.
165+
* @param exposedArea The combined exposed area of all tokens in the view.
166+
* @param softFogArea The area that is clear of hard FoW. Typically, the same as {@link
167+
* #exposedArea()}, unless vision is off in which case it is empty.
168+
* @param clearArea The area that is clear of soft FoW and hard Fow. Typically, the intersection
169+
* of {@link #visibleArea()} and {@link #exposedArea()}, unless vision if off in which case it
170+
* is just {@link #exposedArea()}.
171+
*/
172+
public record Visibility(
173+
@Nonnull Area visibleArea,
174+
@Nonnull Area exposedArea,
175+
@Nonnull Area softFogArea,
176+
@Nonnull Area clearArea) {
177+
178+
/**
179+
* Creates a visibility record for when the zone does not have fog enabled.
180+
*
181+
* <p>The {@link #exposedArea()}, {@link #softFogArea()}, and {@link #clearArea()} will all be
182+
* set to empty areas.
183+
*
184+
* @param visibleArea The visible area for the view.
185+
* @return A visibility record that only contains the visible area.
186+
*/
187+
public static @Nonnull Visibility withoutFog(@Nonnull Area visibleArea) {
188+
return new Visibility(visibleArea, new Area(), new Area(), new Area());
189+
}
190+
}
144191

145-
/** Map the PlayerView to its visible area. */
146-
private final Map<PlayerView, Area> visibleAreaMap = new HashMap<>();
192+
/** Map the PlayerView to its visible area and exposed area. */
193+
private final Map<PlayerView, Visibility> visibilityMap = new HashMap<>();
147194

148195
// endregion
149196

@@ -169,57 +216,80 @@ public ZoneView(Zone zone) {
169216
new MapToolEventBus().getMainEventBus().register(this);
170217
}
171218

172-
public Area getExposedArea(PlayerView view) {
173-
Area exposed = exposedAreaMap.get(view);
174-
175-
if (exposed == null) {
176-
boolean combinedView =
177-
!isUsingVision()
178-
|| MapTool.isPersonalServer()
179-
|| !MapTool.getServerPolicy().isUseIndividualFOW()
180-
|| view.isGMView();
181-
182-
if (view.isUsingTokenView() || combinedView) {
183-
exposed = zone.getExposedArea(view);
184-
} else {
185-
// Not a token-specific view, but we are using Individual FoW. So we build up all the owned
186-
// tokens' exposed areas to build the soft FoW. Note that not all owned tokens may still
187-
// have sight (so weren't included in the PlayerView), but could still have previously
188-
// exposed areas.
189-
exposed = new Area();
190-
for (Token tok : zone.getTokensForLayers(Zone.Layer::supportsVision)) {
191-
if (!AppUtil.playerOwns(tok)) {
192-
continue;
193-
}
194-
ExposedAreaMetaData meta = zone.getExposedAreaMetaData(tok.getExposedAreaGUID());
195-
Area exposedArea = meta.getExposedAreaHistory();
196-
exposed.add(new Area(exposedArea));
219+
private @Nonnull Area calculateExposedArea(PlayerView view) {
220+
boolean combinedView =
221+
!isUsingVision()
222+
|| MapTool.isPersonalServer()
223+
|| !MapTool.getServerPolicy().isUseIndividualFOW()
224+
|| view.isGMView();
225+
226+
@Nonnull Area exposed;
227+
if (view.isUsingTokenView() || combinedView) {
228+
exposed = zone.getExposedArea(view);
229+
} else {
230+
// Not a token-specific view, but we are using Individual FoW. So we build up all the owned
231+
// tokens' exposed areas to build the soft FoW. Note that not all owned tokens may still
232+
// have sight (so weren't included in the PlayerView), but could still have previously
233+
// exposed areas.
234+
exposed = new Area();
235+
for (Token tok : zone.getTokensForLayers(Zone.Layer::supportsVision)) {
236+
if (!AppUtil.playerOwns(tok)) {
237+
continue;
197238
}
239+
ExposedAreaMetaData meta = zone.getExposedAreaMetaData(tok.getExposedAreaGUID());
240+
Area exposedArea = meta.getExposedAreaHistory();
241+
exposed.add(new Area(exposedArea));
198242
}
199-
200-
exposedAreaMap.put(view, exposed);
201243
}
202244
return exposed;
203245
}
204246

205-
/**
206-
* Calculate the visible area of the view, cache it in visibleAreaMap, and return it
207-
*
208-
* <p>The visible area is calculated for each token in the view. The token's visible area is its
209-
* vision obstructed by topology and restricted to the illuminated portions of the map.
210-
*
211-
* @param view the PlayerView
212-
* @return the visible area
213-
*/
214-
public @Nonnull Area getVisibleArea(PlayerView view) {
215-
return visibleAreaMap.computeIfAbsent(
247+
private @Nonnull Area calculateVisibleArea(PlayerView view) {
248+
final var visibleArea = new Area();
249+
getTokensForView(view).map(token -> this.getVisibleArea(token, view)).forEach(visibleArea::add);
250+
return visibleArea;
251+
}
252+
253+
public @Nonnull Visibility getVisibility(PlayerView view) {
254+
return visibilityMap.computeIfAbsent(
216255
view,
217256
view2 -> {
218-
final var visibleArea = new Area();
219-
getTokensForView(view2)
220-
.map(token -> this.getVisibleArea(token, view2))
221-
.forEach(visibleArea::add);
222-
return visibleArea;
257+
var timer = CodeTimer.get();
258+
var tokenCount = view.isUsingTokenView() ? view.getTokens().size() : 0;
259+
260+
timer.start("ZoneView.getVisibility(%d tokens)-getVisibleArea", tokenCount);
261+
var visibleArea = calculateVisibleArea(view2);
262+
timer.stop("ZoneView.getVisibility(%d tokens)-getVisibleArea", tokenCount);
263+
264+
if (!zone.hasFog()) {
265+
// Exposed and clear areas are meaningless when not using fog of war.
266+
return Visibility.withoutFog(visibleArea);
267+
}
268+
269+
timer.start("ZoneView.getVisibility(%d tokens)-getExposedArea", tokenCount);
270+
var exposedArea = calculateExposedArea(view2);
271+
timer.stop("ZoneView.getVisibility(%d tokens)-getExposedArea", tokenCount);
272+
273+
/*
274+
* Hard FOW is cleared by exposed areas. The exposed area itself has two regions: the
275+
* visible area (rendered clear) and the soft FOW area (rendered translucent). But if
276+
* vision is off, treat the entire exposed area as clear with no soft FOW.
277+
*/
278+
279+
timer.start("ZoneView.getVisibility(%d tokens)-getClearArea", tokenCount);
280+
Area softFogArea;
281+
Area clearArea;
282+
if (isUsingVision()) {
283+
softFogArea = exposedArea;
284+
clearArea = new Area(visibleArea);
285+
clearArea.intersect(softFogArea);
286+
} else {
287+
softFogArea = new Area();
288+
clearArea = exposedArea;
289+
}
290+
timer.stop("ZoneView.getVisibility(%d tokens)-getClearArea", tokenCount);
291+
292+
return new Visibility(visibleArea, exposedArea, softFogArea, clearArea);
223293
});
224294
}
225295

@@ -722,8 +792,8 @@ public Collection<DrawableLight> getDrawableLights(PlayerView view) {
722792
}
723793

724794
/**
725-
* Clear the vision caches (@link #tokenVisionCachePerView}, {@link #visibleAreaMap}), fog cache
726-
* ({@link #exposedAreaMap}), and illumination caches ({@link #illuminationModels}.
795+
* Clear the vision caches (@link #tokenVisionCachePerView}, {@link #visibilityMap}), and
796+
* illumination caches ({@link #illuminationModels}.
727797
*
728798
* <p>Needs to be called whenever topology changes, fog is edited, or map vision settings are
729799
* changed. These are all external factors that directly affect vision and illumination. In the
@@ -739,14 +809,13 @@ public void flush() {
739809

740810
tokenVisionCachePerView.clear();
741811
illuminationsPerView.clear();
742-
exposedAreaMap.clear();
743-
visibleAreaMap.clear();
812+
flushFog();
744813

745814
flushLights();
746815
}
747816

748817
public void flushFog() {
749-
exposedAreaMap.clear();
818+
visibilityMap.clear();
750819
}
751820

752821
private void flushLights() {
@@ -756,8 +825,8 @@ private void flushLights() {
756825

757826
/**
758827
* Flush the ZoneView cache of the token. Remove token from {@link #tokenVisionCachePerView}, and
759-
* {@link #illuminationModels}. Can clear {@link #tokenVisionCachePerView}, {@link
760-
* #visibleAreaMap}, and {@link #exposedAreaMap} depending on the token.
828+
* {@link #illuminationModels}. Can clear {@link #tokenVisionCachePerView}, and {@link
829+
* #visibilityMap} depending on the token.
761830
*
762831
* @param token the token to flush.
763832
*/
@@ -776,14 +845,12 @@ public void flush(Token token) {
776845
contributedPersonalLightsByToken.remove(token.getId());
777846
tokenVisionCachePerView.clear();
778847
illuminationsPerView.clear();
779-
exposedAreaMap.clear();
780-
visibleAreaMap.clear();
848+
flushFog();
781849
drawableLights.clear();
782850
} else if (token.getHasSight()) {
783851
contributedPersonalLightsByToken.remove(token.getId());
784852
illuminationsPerView.clear();
785-
exposedAreaMap.clear();
786-
visibleAreaMap.clear();
853+
flushFog();
787854
drawableLights.clear();
788855
}
789856

@@ -884,16 +951,15 @@ private void onTokensChanged(TokensChanged event) {
884951

885952
/**
886953
* Update {@link #lightSourceMap} with the light sources of the tokens, and clear {@link
887-
* #visibleAreaMap} and {@link #exposedAreaMap} if one of the tokens has sight.
954+
* #visibilityMap} if one of the tokens has sight.
888955
*
889956
* @param tokens the list of tokens
890957
*/
891958
private void processTokenAddChangeEvent(List<Token> tokens) {
892959
updateLightSourcesFromTokens(tokens);
893960

894961
if (tokens.stream().anyMatch(Token::getHasSight)) {
895-
exposedAreaMap.clear();
896-
visibleAreaMap.clear();
962+
flushFog();
897963
}
898964

899965
if (tokens.stream().anyMatch(Token::hasAnyMaskTopology)) {

src/main/java/net/rptools/maptool/client/ui/zone/ZoneViewModel.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ private void updateViewport() {
347347

348348
/** Updates {@link #visibleArea} based on {@link #playerView}. */
349349
private void updateVisibleArea() {
350-
visibleArea = zoneView.getVisibleArea(playerView);
350+
visibleArea = zoneView.getVisibility(playerView).visibleArea();
351351
}
352352

353353
/** Updates {@link #playerView}. */

src/main/java/net/rptools/maptool/client/ui/zone/gdx/GdxRenderer.java

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,8 @@ private boolean prerender(PlayerView view) {
688688
timer.start("calcs-1");
689689
timer.start("ZoneRenderer-getVisibleArea");
690690
if (visibleScreenArea == null) {
691-
visibleScreenArea = zoneCache.getZoneView().getVisibleArea(viewModel.getPlayerView());
691+
visibleScreenArea =
692+
zoneCache.getZoneView().getVisibility(viewModel.getPlayerView()).visibleArea();
692693
}
693694
timer.stop("ZoneRenderer-getVisibleArea");
694695

@@ -855,24 +856,9 @@ private void renderFog(PlayerView view) {
855856

856857
var zoneView = zoneCache.getZoneView();
857858

858-
timer.start("renderFog-visibleArea");
859-
Area visibleArea = zoneView.getVisibleArea(view);
860-
timer.stop("renderFog-visibleArea");
861-
862-
timer.start("renderFog-combined(%d)", view.isUsingTokenView() ? view.getTokens().size() : 0);
863-
Area exposedArea = zoneView.getExposedArea(view);
864-
timer.stop("renderFog-combined(%d)", view.isUsingTokenView() ? view.getTokens().size() : 0);
865-
866-
Area softFogArea;
867-
Area clearArea;
868-
if (zoneView.isUsingVision()) {
869-
softFogArea = exposedArea;
870-
clearArea = new Area(visibleArea);
871-
clearArea.intersect(softFogArea);
872-
} else {
873-
softFogArea = new Area();
874-
clearArea = exposedArea;
875-
}
859+
var visibility = zoneView.getVisibility(view);
860+
Area softFogArea = visibility.softFogArea();
861+
Area clearArea = visibility.clearArea();
876862

877863
timer.start("renderFog");
878864
ScreenUtils.clear(Color.CLEAR);
@@ -1105,7 +1091,8 @@ private void showBlockedMoves(PlayerView view, Set<SelectionSet> movementSet) {
11051091
|| zoneCache
11061092
.getZoneRenderer()
11071093
.getZoneView()
1108-
.getVisibleArea(view)
1094+
.getVisibility(view)
1095+
.visibleArea()
11091096
.intersects(tokenRectangle);
11101097
}
11111098
} else {

0 commit comments

Comments
 (0)