[MIRROR] Lighting source refactor (Tiny) [MDB IGNORE] (#19370)

* Lighting source refactor (Tiny) (#73284)

## About The Pull Request

I'm doing two things here. Let's get the boring bit out of the way.

Lighting source updates do three distinct things, and those things were
all in one proc.
I've split that one proc into three, with the first two feeding into the
third.

Second, more interesting thing.

An annoying aspect of our lighting system is the math we use for
calculating luminosity is hardcoded.
This means that we can't have subtypes that are angled, or that have
squared falloff, etc. All has to look the same.
This sucks, and it shows.

It has to be, goes the thinking, because we need very fast lookups that
OOP cannot provide.
We can't bog down the main equation with fluff, because the main
equation needs to be really speedy.

The thing about this equation is the only variants on a turf to turf
basis is exactly how far turfs are from the center.
So what if, instead of doing the math in our corner worker loop, we
build lookup tables to match our current source's state.
The tables, like a heatmap, could encode the lighting of any point along
the line.

This is actually faster then doing the math each time, because the list
generation can be cached.
It also means we've pulled the part we want to override out of hotcode.
It's cheap to override now, and a complex subtype, with angles and such
would have no impact on the typical usage.

So the code's faster, easier to read, and more extensible.
And we can do stuff like squared falloff for some lights in future
without breaking others.

Winning!

## Why It's Good For The Game

Winning

* Lighting source refactor (Tiny)

---------

Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com>
This commit is contained in:
SkyratBot
2023-02-17 01:08:28 +01:00
committed by GitHub
parent fe9057484b
commit 32c31b8fc0

View File

@@ -139,17 +139,23 @@
// This exists so we can cache the vars used in this macro, and save MASSIVE time :)
// Most of this is saving off datum var accesses, tho some of it does actually cache computation
// You will NEED to call this before you call APPLY_CORNER
#define SETUP_CORNERS_CACHE(lighting_source) \
var/_turf_x = lighting_source.pixel_turf.x; \
var/_turf_y = lighting_source.pixel_turf.y; \
var/_turf_z = lighting_source.pixel_turf.z; \
var/_range_divisor = max(1, lighting_source.light_range); \
var/_light_power = lighting_source.light_power; \
var/_applied_lum_r = lighting_source.applied_lum_r; \
var/_applied_lum_g = lighting_source.applied_lum_g; \
var/_applied_lum_b = lighting_source.applied_lum_b; \
var/_lum_r = lighting_source.lum_r; \
var/_lum_g = lighting_source.lum_g; \
#define SETUP_CORNERS_CACHE(lighting_source) \
var/_turf_x = lighting_source.pixel_turf.x; \
var/_turf_y = lighting_source.pixel_turf.y; \
var/_turf_z = lighting_source.pixel_turf.z; \
var/list/_sheet = get_sheet(); \
var/list/_multiz_sheet = list(); \
if(!!GET_LOWEST_STACK_OFFSET(source_turf.z)) { \
_multiz_sheet = get_sheet(multiz = TRUE); \
} \
var/_range_offset = CEILING(lighting_source.light_range, 1) + 0.5 + 2; \
var/_multiz_offset = SSmapping.max_plane_offset + 1; \
var/_light_power = lighting_source.light_power; \
var/_applied_lum_r = lighting_source.applied_lum_r; \
var/_applied_lum_g = lighting_source.applied_lum_g; \
var/_applied_lum_b = lighting_source.applied_lum_b; \
var/_lum_r = lighting_source.lum_r; \
var/_lum_g = lighting_source.lum_g; \
var/_lum_b = lighting_source.lum_b;
#define SETUP_CORNERS_REMOVAL_CACHE(lighting_source) \
@@ -157,10 +163,9 @@
var/_applied_lum_g = lighting_source.applied_lum_g; \
var/_applied_lum_b = lighting_source.applied_lum_b;
#define LUM_FALLOFF(C) (1 - CLAMP01(sqrt((C.x - _turf_x) ** 2 + (C.y - _turf_y) ** 2 + LIGHTING_HEIGHT) / _range_divisor))
// You may notice we still use squares here even though there are three components
// Because z diffs are so functionally small, cubes and cube roots are too aggressive
#define LUM_FALLOFF_MULTIZ(C) (1 - CLAMP01(sqrt((C.x - _turf_x) ** 2 + (C.y - _turf_y) ** 2 + abs(C.z - _turf_z) ** 2 + LIGHTING_HEIGHT) / _range_divisor))
// Read out of our sources light sheet, a map of offsets -> the luminosity to use
#define LUM_FALLOFF(C) _sheet[C.x - _turf_x + _range_offset][C.y - _turf_y + _range_offset]
#define LUM_FALLOFF_MULTIZ(C) _multiz_sheet[C.z - _turf_z + _multiz_offset][C.x - _turf_x + _range_offset][C.y - _turf_y + _range_offset]
// Macro that applies light to a new corner.
// It is a macro in the interest of speed, yet not having to copy paste it.
@@ -193,6 +198,67 @@
. * _applied_lum_b \
);
/// Returns a list of lists, indexed with ints, that can be read to get the lighting multiplier at any one point
/// If the requested sheet is multiz, this will be 3 lists deep, first handling z level then x and y
/// otherwise it's just two, x then y
/datum/light_source/proc/get_sheet(multiz = FALSE)
var/list/static/key_to_sheet = list()
var/range = max(1, light_range);
var/key = "[range]-[multiz]"
var/list/hand_back = key_to_sheet[key]
if(!hand_back)
if(multiz)
hand_back = generate_sheet_multiz(range)
else
hand_back = generate_sheet(range)
key_to_sheet[key] = hand_back
return hand_back
/// Returns a list of lists that encodes the light falloff of our source
/// Takes anything that impacts our generation as input
/// This function should be "pure", no side effects or reads from the source object
/datum/light_source/proc/generate_sheet(range, z_level = 0)
var/list/encode = list()
var/bound_range = CEILING(range, 1) + 1
// Corners are placed at 0.5 offsets
// We need our coords to reflect that
for(var/x in (-bound_range - 0.5) to (bound_range + 0.5))
var/list/row = list()
for(var/y in (-bound_range - 0.5) to (bound_range + 0.5))
row += falloff_at_coord(x, y, z_level, range)
encode += list(row)
return encode
/// Returns a THREE dimensional list of lists that encodes the lighting falloff of our source
/// Takes anything that impacts our generation as input
/// This function should be "pure", no side effects or reads from the passed object
/datum/light_source/proc/generate_sheet_multiz(range)
var/list/encode = list()
var/z_range = SSmapping.max_plane_offset // Let's just be safe yeah?
for(var/z in -z_range to z_range)
var/list/sheet = generate_sheet(range, z)
encode += list(sheet)
return encode
/// Takes x y and z offsets from the source as input, alongside our source's range
/// Returns a value between 0 and 1, 0 being dark on that tile, 1 being fully lit
/datum/light_source/proc/falloff_at_coord(x, y, z, range)
var/_range_divisor = max(1, range)
// You may notice we use squares here even though there are three components
// Because z diffs are so functionally small, cubes and cube roots are too aggressive
return 1 - CLAMP01(sqrt(x ** 2 + y ** 2 + z ** 2 + LIGHTING_HEIGHT) / _range_divisor)
/proc/read_sheet(list/sheet, x, y, offset, z, z_offset)
var/list/working = sheet
var/offset_x = x + offset
var/offset_y = y + offset
var/offset_z = z + z_offset
if(z)
working = sheet[offset_z]
var/list/line = working[offset_x]
var/word = line[offset_y]
return word
/// This is the define used to calculate falloff.
/datum/light_source/proc/remove_lum()
SETUP_CORNERS_REMOVAL_CACHE(src)
@@ -239,13 +305,15 @@
insert_into[draw_from.lighting_corner_SW] = 0; \
insert_into[draw_from.lighting_corner_NW] = 0;
/datum/light_source/proc/update_corners()
/// Refreshes our lighting source to match its parent atom
/// Returns TRUE if an update is needed, FALSE otherwise
/datum/light_source/proc/refresh_values()
var/update = FALSE
var/atom/source_atom = src.source_atom
if (QDELETED(source_atom))
qdel(src)
return
return FALSE
if (source_atom.light_power != light_power)
light_power = source_atom.light_power
@@ -261,7 +329,7 @@
if (!light_range || !light_power)
qdel(src)
return
return FALSE
if (isturf(top_atom))
if (source_turf != top_atom)
@@ -281,7 +349,7 @@
if (!isturf(source_turf))
if (applied)
remove_lum()
return
return FALSE
if (light_range && light_power && !applied)
update = TRUE
@@ -294,55 +362,73 @@
else if (applied_lum_r != lum_r || applied_lum_g != lum_g || applied_lum_b != lum_b)
update = TRUE
// If we need to update, well, update
if (update)
needs_update = LIGHTING_CHECK_UPDATE
applied = TRUE
else if (needs_update == LIGHTING_CHECK_UPDATE)
return //nothing's changed
return TRUE
// Otherwise, go off the needs_update var. If it requires an update provide one, otherwise we're kosher
if (needs_update == LIGHTING_CHECK_UPDATE)
return FALSE //nothing's changed
return TRUE
/// Returns a list of lighting corners this source impacts
/datum/light_source/proc/impacted_corners()
var/list/datum/lighting_corner/corners = list()
if (!source_turf)
return list()
if (source_turf)
var/uses_multiz = !!GET_LOWEST_STACK_OFFSET(source_turf.z)
var/oldlum = source_turf.luminosity
source_turf.luminosity = CEILING(light_range, 1)
if(uses_multiz)
for(var/turf/T in view(CEILING(light_range, 1), source_turf))
if(IS_OPAQUE_TURF(T))
continue
INSERT_CORNERS(corners, T)
var/oldlum = source_turf.luminosity
source_turf.luminosity = CEILING(light_range, 1)
var/turf/below = SSmapping.get_turf_below(T)
var/turf/previous = T
while(below)
// If we find a non transparent previous, end
if(!istransparentturf(previous))
break
if(IS_OPAQUE_TURF(below))
// If we're opaque but the tile above us is transparent, then we should be counted as part of the potential "space"
// Of this corner
break
// Now we do lighting things to it
INSERT_CORNERS(corners, below)
// ANNND then we add the one below it
previous = below
below = SSmapping.get_turf_below(below)
var/turf/above = SSmapping.get_turf_above(T)
while(above)
// If we find a non transparent turf, end
if(!istransparentturf(above) || IS_OPAQUE_TURF(above))
break
INSERT_CORNERS(corners, above)
above = SSmapping.get_turf_above(above)
else // Yes I know this could be acomplished with an if in the for loop, but it's fukin lighting code man
for(var/turf/T in view(CEILING(light_range, 1), source_turf))
if(IS_OPAQUE_TURF(T))
continue
INSERT_CORNERS(corners, T)
var/uses_multiz = !!GET_LOWEST_STACK_OFFSET(source_turf.z)
if(!uses_multiz) // Yes I know this could be acomplished with an if in the for loop, but it's fukin lighting code man
for(var/turf/T in view(CEILING(light_range, 1), source_turf))
if(IS_OPAQUE_TURF(T))
continue
INSERT_CORNERS(corners, T)
source_turf.luminosity = oldlum
return corners
for(var/turf/T in view(CEILING(light_range, 1), source_turf))
if(IS_OPAQUE_TURF(T))
continue
INSERT_CORNERS(corners, T)
var/turf/below = SSmapping.get_turf_below(T)
var/turf/previous = T
while(below)
// If we find a non transparent previous, end
if(!istransparentturf(previous))
break
if(IS_OPAQUE_TURF(below))
// If we're opaque but the tile above us is transparent, then we should be counted as part of the potential "space"
// Of this corner
break
// Now we do lighting things to it
INSERT_CORNERS(corners, below)
// ANNND then we add the one below it
previous = below
below = SSmapping.get_turf_below(below)
var/turf/above = SSmapping.get_turf_above(T)
while(above)
// If we find a non transparent turf, end
if(!istransparentturf(above) || IS_OPAQUE_TURF(above))
break
INSERT_CORNERS(corners, above)
above = SSmapping.get_turf_above(above)
source_turf.luminosity = oldlum
return corners
/datum/light_source/proc/update_corners()
if(!refresh_values())
return
var/list/datum/lighting_corner/corners = impacted_corners()
SETUP_CORNERS_CACHE(src)
var/list/datum/lighting_corner/new_corners = (corners - src.effect_str)