ID:1102834
 
Lighting in BYOND has always been a long sought after feature, and a very difficult technical feat in BYOND games. In my opinion, the guy who got it right was Forum_Account, but his library is far from perfect. After a month of deep analysis and experimenting, I've come up with a version of the library that is as fast as it can be in BYOND. It requires massive modifications to the original code, along with several "injects" of extra code in your project in order to function efficiently. In this article, I will show you how to make his library as efficient and as fast as possible.

First thing's first, we need to fix some problems with his library. His library forgets to do two things: what happens when lights (or their owners) die? and we should update the location of a mobile light when its owner moves.

light
Del()
off()
apply()
lighting.lights -= src
..()
atom
Del()
if(light)
del(light)
..()


Now we make sure lights stay with their owners

atom/movable
Move()
. = ..()
if(.)
if(light)
light.loc = src.loc
light.should_wake = 1


Next, we tackle the efficiency problem. F_A's library constantly updates lights, even lights that don't need to be updated. He did shave off the extra calculations that won't be necessary (so if a light hasn't changed, it doesn't recalculate it's effect) but what he failed to do was to leave out unnecessary updates. If a light hasn't changed, we don't need to check it or even try it. Instead, we should keep track of which lights do need updates, and only update those. We will handle this with a variable: should_wake. should_wake will tell us if a light should be considered for processing during this frame.

light
var/tmp/should_wake = 1
proc/loop() //overwriting the original light.loop()
if(!owner) del(src) //we handle the possibility of stranded lights
if(mobile)
var/opx = owner.x
var/opy = owner.y

if(opx != __x || opy != __y)
__x = opx
__y = opy
changed = 1
if(changed) //changed, process effect
apply()
else //no change
should_wake = 0 //stop waking up


Ok, the next thing we need to accomplish is sorting through the lights and only updating the ones who have should_wake equal to 1. We'll do this by looking at what clients are logged in, and calculating which light each client can see, take the lights that are within that view, and check only those for should_wake. This way, only lights that clients can see are capable of doing anything. We don't need to process or even try to process lights that no one will ever see!

var/list/players = list() //this will keep track of players logged in

mob/player
Login()
..()
players += src
src.light = new(src, 3)

Logout()
players -= src //always do this, even if switching mobs
if(key) //disconnecting
del(src)

//set LIGHT_RANGE to how far away from clients you want lights to "become active"
//for a square view size, set it to half your width
//for a rectangle view size with client.perspective = EDGE_PERSPECTIVE,
// set it to your height (ie: 15x11, set it to 11)
//and feel free to experiment!
//results will vary with each different client.perspective setting

var/const/LIGHT_RANGE = 8
Lighting
//lets rewrite Lighting.loop()

proc/loop() //this completely replaces the Lighting.loop() code in the library
while(src)
sleep(world.tick_lag)
if(players.len <= 0) continue //no players logged in, bail!

if(!states)
if(!icon)
del(src)
CRASH("The global var lighting.icon must be set")

var/list/l = icon_states(icon)
states = l.len ** 0.25
if(__ambient != ambient)
for(var/light/l in lights)
l.ambient(ambient)
__ambient = ambient

//in the following lines, you're going to see the use of the colon operator
//we do this because a big world is going to have a lot of objects and we know that
//within the lists that we're searching, there will only ever be the object we expect
//so we can bypass type checking by omitting the type paths and using colon
//operators instead of the period operator
for(var/p in players)
for(var/light/l in range(p, LIGHT_RANGE))
if(l.should_wake)
l.loop()
l.should_wake = 0
for(var/s in changed)
s:icon_state = "[s:c1:lum][s:c2:lum][s:c3:lum][s:lum]"
s:changed = 0
changed.Cut()


If you want to make a change to a light, you need to make sure that you set should_wake to 1 so that it will be processed.

Lights that only change the area around it once (like a static light on the wall) are only ever processed ONCE.

If a client is logged in and standing still, it will only ever process the lights it can see, once.

I've tested this on my machine up to about 50,000 moving light sources (spread out over two 64x64 maps) with a view size of 15x11, and the CPU doesn't go over 50%. That is some serious performance (and a lot of moving things). Try doing that with the library out of the box! The secret is in processing only what we need to process, and only processing lights that clients can see!

Bonus:

Let's make the light engine also support opacity! We'll implement our own, but we'll call it opaque.

atom
var
// we don't use the opacity var because we don't
// want to use BYOND's built-in opacity system,
// we want to create our own effects.
opaque = 0
turf
var
canshade = 1 //by default, all turfs can shade, but if you want a void that ignores shading, set to 0

//here we speed up shading init a little
var
shading/null_shading = new(null, null, 0)
shading
proc
init()

// get references to its neighbors
c1 = locate(/shading) in get_step(src, SOUTH) //these get_step calls are new!
c2 = locate(/shading) in get_step(src, SOUTHWEST)
c3 = locate(/shading) in get_step(src, WEST)

u1 = locate(/shading) in get_step(src, EAST)
u2 = locate(/shading) in get_step(src, NORTHEAST)
u3 = locate(/shading) in get_step(src, NORTH)

// some of these vars will be null around the edge of the
// map, so in that case we set them to the global null_shading
// instance so we don't constantly have to check if these
// vars are null before referencing them.
if(!c1) c1 = null_shading
if(!c2) c2 = null_shading
if(!c3) c3 = null_shading

if(!u1) u1 = null_shading
if(!u2) u2 = null_shading
if(!u3) u3 = null_shading


Now add this to the first line of shading.lum()

shading
proc
lum(l)
if(!loc:canshade) return //can't shade this, so don't!
//rest of code here...


And finally, the code that does all the work... light.effect()
This was taken directly from the F_A's demo!

light
proc
effect()

var/list/L = list()

for(var/shading/s in range(radius, src))

// if we already have a value for it, skip it
if(!isnull(L[s])) continue

// if its the center we handle this case specially to
// avoid some division by zero errors later. also, we
// know we can always see the center so we don't need
// to check.
if(s.x == x && s.y == y)
var/lum = lum(s)
if(lum > 0)
L[s] = lum

continue

// determine if s can be seen from src...

// find the normalized vector pointing from src to s
var/dx = (s.x - x)
var/dy = (s.y - y)
var/d = sqrt(dx * dx + dy * dy)

if(d > 0)
dx /= d
dy /= d

// we'll use this vector to trace a line from src to s
// and check each tile along the way for opaque objects
var/tx = x + dx + 0.5
var/ty = y + dy + 0.5

// we use a limited number of iterations
for(var/i = 1 to radius)

var/turf/t = locate(round(tx), round(ty), z)

// in case we went off the edge of the map
if(!t) break

// if we don't have an illumination value, compute one
if(!L[t.shading])
var/lum = lum(t.shading)
if(lum > 0)
L[t.shading] = lum

// if it's opaque, stop tracing the line
if(t.opaque) break
//if(t.opaque(owner))
// break

// if we've reached s, stop tracing the line
if(t.shading == s) break

// continue tracing the line
tx += dx
ty += dy

return L


Now I realize this is a lot of changes to the library, and I know its probably hard to follow this article and make all of the changes successfully. So if there is a big enough demand for it, I will consider posting a PASTE BIN of the library in full so its plug and play.

Cheers, and happy lighting.
The half-of-your-view thing should work for games where the client's perspective is MOB_PERSPECTIVE (the default). If others, e.g. EDGE_PERSPECTIVE and EYE_PERSPECTIVE, were applied, things will go wrong so I believe you should mention that.
In response to Jemai1
Jemai1 wrote:
The half-of-your-view thing should work for games where the client's perspective is MOB_PERSPECTIVE (the default). If others, e.g. EDGE_PERSPECTIVE and EYE_PERSPECTIVE, were applied, things will go wrong so I believe you should mention that.

Done deal.
Another thing I noticed is that it disregards light sources that are not in-range to your player but may however affect the lighting around you. This may or may not be what you want.

Tip: You can use range(client) to get the atoms in your client's screen.
In response to Jemai1
Jemai1 wrote:
Another thing I noticed is that it disregards light sources that are not in-range to your player but may however affect the lighting around you. This may or may not be what you want.

Tip: You can use range(client) to get the atoms in your client's screen.

Yeah, this is to make it faster. The more lights that you allow into that list, the more that can possibly process. If you have any lights with a radius > LIGHT_RANGE, the player will "see them turn on" as he approaches them.

This is a minor price to pay for speed. Especially in a server-side lighting system.

As for the tip, I did not know that, but by providing a number (such as 11) and having a centered view @ 15x11, there's at least a distance of 5 or so tiles away off screen that are being checked for lights as intended.
You can still have that as an argument, i.e. range(LIGHT_RANGE,client), just so that you won't have to worry for perspective stuffs.
There is no reason to have a light updating loop.
Instead, just create a single function for updating a light and call it to change the light's attributes: location, brightness, radius, etc.

There's also other things that f_a doesn't do quite optimally in his library, like his example for shadowcasting relies on an extremely inefficient algorithm.
There exists algorithm for this that is an order of magnitude faster, like what I use in my lighting library ;)
In response to D4RK3 54B3R
D4RK3 54B3R wrote:
There is no reason to have a light updating loop.
Instead, just create a single function for updating a light and call it to change the light's attributes: location, brightness, radius, etc.

There's also other things that f_a doesn't do quite optimally in his library, like his example for shadowcasting relies on an extremely inefficient algorithm.
There exists algorithm for this that is an order of magnitude faster, like what I use in my lighting library ;)

No offense, because I bought your lighting library. But it crashes after about 10 or so minutes, and leaves lots of glitch shading everywhere. Especially on multi-player. It isn't stable, and can't handle 50,000 light sources like I have gotten this one to do.

Also, I could care less if you think this is inefficient because in my tests it performs as if its not even processing. With a view of 15x11, each client can only see a maximum of 165 light sources... so even at maximum lights per client, you'll scale very far.

If you have a better opacity calculation, lets see it?
I just discovered another optimization, and that is the loop iterations per server tick. We can cut this down considerably by providing a wait time that is just slightly under the delay between two successive moves by the fastest move speed. What I mean by this is, its impossible for a moving light to need an update between two moves of the fastest movement speed in the game. Because of this, we can wait a lot longer in between processing frames!

I'll provide an example that shows how to do it using my Smooth Tile Movement library, which provides a core system for movement speed of all atoms.

var/const/fastest_speed = 10
Lighting
var/loop_delay

New()
loop_delay = (world.icon_size * world.tick_lag) / fastest_speed
spawn(1) loop()
proc/loop()
while(src)
sleep(light_delay)
...//rest of the code here


In this example, with a tile size of 64 pixels and a FPS of 40, the fastest anything can ever move in the game is one tile per 16 milliseconds (1.6 "units" times 10 milliseconds for sleep()). So instead of waiting 0.25 at a fps of 40 (world.tick_lag), we are waiting about 6.4 times longer in between evaluating lights, which gives us a lot more breathing room in server processing per tick cycle.

I should note that doing it this way means the light loop will only update when it reaches the end of the delay, so if a change occurs in between this delay, the effect will be delay. The good news is, this amount of time is so small that isn't noticeable especially for a server sided light system.
In response to FIREking
FIREking wrote:
No offense, because I bought your lighting library. But it crashes after about 10 or so minutes, and leaves lots of glitch shading everywhere. Especially on multi-player. It isn't stable, and can't handle 50,000 light sources like I have gotten this one to do.

If you had such poor performance under my library, either you were using it wrong or you have an outdated build.

I have released countless games, singleplayer and multiplayer, that use it and do not have the performance problems that you have mentioned. To name a few:


Also, I could care less if you think this is inefficient because in my tests it performs as if its not even processing. With a view of 15x11, each client can only see a maximum of 165 light sources... so even at maximum lights per client, you'll scale very far.

If you have a better opacity calculation, lets see it?

Recursive Shadowcasting.
http://roguebasin.roguelikedevelopment.org/ index.php?title=FOV_using_recursive_shadowcasting

It is like a floodfill that evaluates the opacity of each turf only once, unlike Forum_account's simple but inefficient algorithm.
Forum_account's algorithm goes through every turf in a line to the tile it is computing light for. This means that it will go through the same turfs multiple times for opacity evaluation.
Would you mind creating a package of F_a's library with all of your changes already made? It would make life quite a lot easier, but if not I can take some time later this week to follow your original post myself. :)
In response to D4RK3 54B3R
D4RK3 54B3R wrote:
FIREking wrote:
No offense, because I bought your lighting library. But it crashes after about 10 or so minutes, and leaves lots of glitch shading everywhere. Especially on multi-player. It isn't stable, and can't handle 50,000 light sources like I have gotten this one to do.

If you had such poor performance under my library, either you were using it wrong or you have an outdated build.

I have released countless games, singleplayer and multiplayer, that use it and do not have the performance problems that you have mentioned. To name a few:


Also, I could care less if you think this is inefficient because in my tests it performs as if its not even processing. With a view of 15x11, each client can only see a maximum of 165 light sources... so even at maximum lights per client, you'll scale very far.

If you have a better opacity calculation, lets see it?

Recursive Shadowcasting.
http://roguebasin.roguelikedevelopment.org/ index.php?title=FOV_using_recursive_shadowcasting

It is like a floodfill that evaluates the opacity of each turf only once, unlike Forum_account's simple but inefficient algorithm.
Forum_account's algorithm goes through every turf in a line to the tile it is computing light for. This means that it will go through the same turfs multiple times for opacity evaluation.
I'll try to dig up what negative information I found. Perhaps you can send me an updated build? Page me.

I'll look into the Recursive Shadowcasting as well. For what its worth, I don't use his library in such a way that allows for mobile objects to have opacity, only turfs.
In response to TheLionsRoar
TheLionsRoar wrote:
Would you mind creating a package of F_a's library with all of your changes already made? It would make life quite a lot easier, but if not I can take some time later this week to follow your original post myself. :)

Sure!
In response to FIREking
FIREking wrote:
I'll look into the Recursive Shadowcasting as well. For what its worth, I don't use his library in such a way that allows for mobile objects to have opacity, only turfs.

Yeah that's not what the shadowcasting is for. Mobile objects that are opaque are a pain in the butt because they have to force lightsource updates. Can cause some severe slowdowns if you're not careful with how you implement them.

The shadowcasting essentially allows you to have more lightsources with larger radii that cast shadows because it is plain faster.
In response to D4RK3 54B3R
D4RK3 54B3R wrote:
FIREking wrote:
I'll look into the Recursive Shadowcasting as well. For what its worth, I don't use his library in such a way that allows for mobile objects to have opacity, only turfs.

Yeah that's not what the shadowcasting is for. Mobile objects that are opaque are a pain in the butt because they have to force lightsource updates. Can cause some severe slowdowns if you're not careful with how you implement them.

The shadowcasting essentially allows you to have more lightsources with larger radii that cast shadows because it is plain faster.

Reading the article for the first time, I get lost around the middle where it talks about switching to a new section after finding a blocker. Its a little bit over my head for now. I want to understand it better, but I'm just not that good with LOS or FOV stuff. Even the one I tried implementing in my Java game used the trace a line method. I'm not sure I understand how all of this only happens once, either. Or do you mean, once per light source?
Here's a modular paste bin version of the library with the optimizations.

http://pastebin.com/nLY4xHUM
To make this even faster, you can remove all of the del() calls and switch it over to letting the garbage collector handle deletion. I'll post how later by editing this post.

Edit:

To do this, all you need to do is make sure that when you need to "rid the world of a specific light" that you remove all references to it.

light/proc/Die() //call this to remove a light from the game
dead = 1
if(src.light) //how did the light get a light?
src.light.Die()
src.light = null
should_wake = 0 //stop auto processing this light
off() //negate its effect
apply()
lighting.lights -= src
src.owner = null
src.loc = null

light/proc/Del()
if(!dead)
CRASH("light.Del() not called by the garbage collector!")
..()


Here we have some debugging information that will help you find instances in your code where del(light) is being called. You should replace all of these with light.Die(). This way, the garbage collector will delete objects that aren't needed over time which is exactly what you want in a single threaded application.
Nice work FIREKing. Good to see some optimization going on. Shame that FA seems to have disappeared, though.
In response to Jmurph
Jmurph wrote:
Nice work FIREKing. Good to see some optimization going on. Shame that FA seems to have disappeared, though.

I agree I'm loving what Fireking is doing as it's turning out to be the fastest lighting library so far. I love D4RK3's library and it's amazing, but it can be a bit slow at times (I may not have the latest version though.)

This seems to be great for lighting when you have a CPU budget to worry about, which most games do.

I'm also kind of saddened though as it's true, Forum_Account kind of disappeared from BYOND and I'm worried that something may have happened to him.

Regardless, the fact that Fireking is working on this and has also started poking his head into my get_ library, I'm kind of excited for what results it can produce.
Cloud Magic wrote:
FA ragequit. Nothing happened to him other than his own cockiness getting the better of him.

When was this?
Page: 1 2