ID:2714778
 
(See the best response by Lummox JR.)
Hello.

Problem description:

I am trying to create a lighting engine for the /vg/ branch of SS13.

The system works by using a PLANE_MASTER assigned for lighting.
On that plane master, we have a dark backdrop.
This dark backdrop is illuminated by a dark plane with very low transparency.

On that dark plane, we draw several light objects of various shapes.

This works fine for white light sources. But when we have light sources of different colours, they tend to mix badly and aggressively shift to white light.

That's because we draw everything - notably, we generated our light overlays - using BLEND_ADD. When two light sources L1 and L2 of different RGB values collide, the result is :

R(L1+L2) = R(L1)+R(L2)
G(L1+L2) = G(L1)+G(L2)
B(L1+L2) = B(L1)+B(L2)
A(L1+L2) = A(L1)+A(L2)

So if L1 and L2 are not purely monochromatic, the result of two identical lights overlapping is not the same light with brighter intensity, but a whiter light with brighter intensity.

Ideally we want:

R(L1+L2) = sqrt(R(L1)*R(L2))
G(L1+L2) = sqrt(G(L1)*G(L2))
B(L1+L2) = sqrt(B(L1)*B(L2))
A(L1+L2) = A(L1)+A(L2)

Or any sort of averaging for the colour and addition for the alpha/intensity.

I have not found a way to make this possible.

Code:

Our planes:

/obj/abstract/screen/plane
name = ""
screen_loc = "CENTER"
blend_mode = BLEND_MULTIPLY
layer = 1

/obj/abstract/screen/plane/New(var/client/C)
..()
if(istype(C))
C.screen += src
verbs.Cut()

/obj/abstract/screen/plane/master
icon = 'icons/mob/screen1.dmi'
appearance_flags = NO_CLIENT_COLOR | PLANE_MASTER | RESET_TRANSFORM | RESET_COLOR | RESET_ALPHA
color = LIGHTING_PLANEMASTER_COLOR // Completely black.
plane = LIGHTING_PLANE
mouse_opacity = 0

//poor inheritance shitcode
/obj/abstract/screen/backdrop
blend_mode = BLEND_OVERLAY
icon = 'icons/mob/screen1.dmi'
icon_state = "black"
layer = BACKGROUND_LAYER
screen_loc = "CENTER"
plane = LIGHTING_PLANE

/obj/abstract/screen/backdrop/New(var/client/C)
..()
if(istype(C)) C.screen += src
var/matrix/M = matrix()
M.Scale(world.view*3)
transform = M
verbs.Cut()


/obj/abstract/screen/plane/dark
blend_mode = BLEND_ADD
mouse_opacity = 0
plane = LIGHTING_PLANE // Just below the master plane.
icon = 'icons/lighting/over_dark.dmi'
alpha = 10
appearance_flags = RESET_TRANSFORM | RESET_COLOR | RESET_ALPHA
var/list/alphas = list()
var/colours = null // will animate() to that colour next check_dark_vision()

/obj/abstract/screen/plane/dark/New()
..()
var/matrix/M = matrix()
M.Scale(world.view*2.2)
transform = M


Our light object:

/atom/movable/light
appearance_flags = KEEP_TOGETHER
plane = LIGHTING_PLANE


Casting a light consists of adding together semi-transparent objects which are then assigned to the atom's overlays.

So far playing with RESET_COLOR has not helped.

Thank you for your help.

I realised an even better blending mode would take into account the relative alphas of the icons for colour mixing. Something like:

a1 = A(L1)/(A(L1)+A(L2))
a2 = A(L2)/(A(L1)+A(L2))

R(L1+L2) = sqrt(a1*R(L1)*a2*R(L2))
G(L1+L2) = sqrt(a1*G(L1)*a2*G(L2))
B(L1+L2) = sqrt(a1*B(L1)*a2*B(L2))
A(L1+L2) = A(L1)+A(L2)

I am actually not sure if this is possible given BYOND's current features, but as a special new BLEND_MODE it would be extremely satisfying.
Something doesn't sound right here, because the additive behavior you're describing is incorrect.

If you have two orange lights with alpha values of 85 and 170, you should more or less get a full 255 alpha of orange, except for any rounding error. That's not at all what the formulas you posted for additive blending describe, and it's not how the blending is set in the renderer. The alpha of the colors does matter when it comes to how they're blended.

There is also no physical justification for the sqrt() formulas. Those make no sense in any circumstance.

It sounds to me like you're seeing some rendering artifacts that would be consistent with your lighting plane backdrop not actually being present, or not having full alpha.
Thank you for your answer.

You're right that geometric averaging isn't probably the best. Any form of averaging would work, really.

I think it's best shown by pictures. The additive relations are mostly me guessing, but this is more or less what I see when trying.

We'll start with warm red lights: "#FA644B" (rgb(250, 100, 75)), with an alpha going from 255 at the centre to close to 1 at the edge of the screen.

It's best to only look at the centre of the image as there's some artifacts/other things going on at the edge and the centre shows my problem clearly, I think.

https://imgur.com/a/MeCSDJQ - this is one, two, and 4 red lights.

As you can see, the centre gets progressively whiter as more lights are spawned, and the edges are still red, but brighter.

In the degenerate case of a lot of individual red lights, almost the entire spot meant to be illuminated is fully white. The little corona effect here is due to slightly different positions of the red lights.

https://imgur.com/a/hHOUWC9

Now if we change the flare on the pictures to be mono-chromatically red ("#FF0000"), we get this:

https://imgur.com/a/Hsjvp31

With again, one, two, and four light objects.

I should add that, for some effects, we actually spawn two light atoms tied to the same light source. If we perform the following operation:
        var/list/RGB = rgb2num(light_color)
color = rgb(round(RGB[1]/2), round(RGB[2]/2), round(RGB[3]/2))


On both atoms, we get a light spot that is indistinguishable from a single atom with
color = light_color
.

Our backdrop is indeed not fully dark, as we want some players to be able to see through darkness.
However, even making it fully dark does not fix the issue.

I wrote this in developer help rather than feature requests/bug reports as I thought it was user error on my part and not seeing a way to blend light icon properly.
Best response
Try drawing only the lighting backdrop, and change your map control's background color to something crazy like magenta, for test purposes.

I think what you'll find is that the backdrop is not opaque.

Edit: I forgot you can also get some JSON output to debug the map. Just do .debug profile mapicons in your input control and it should save a JSON file to your cfg directory under your user data. You should be able to locate the lighting plane master and backdrop in that.
Well you might be onto something, thanks.

The plane master - on which everything is drawn - has the color matrix:

LIGHTING_PLANEMASTER_COLOR list(null,null,null,"#0000","#000f")


As this is a project I took from someone else (and I can no longer contact them), I am not quite sure what it is.
As I understand it it's a color matrix in list form, with the first three rows being the default. But I don't get what they meant with two last values.

I tried "#000000", "list(null, null, null, null, null)",
#define LIGHTING_PLANEMASTER_COLOR list(1, 0, 0, 0,\
0, 1, 0, 0,\
0, 0, 1, 0,\
0, 0, 0, 0,\
0, 0, 0, 1)
, etc, to no avail.
That color matrix basically turns the entire alpha channel to full blast. The only reason to have that is if your lighting plane doesn't actually have a backdrop. So you should get rid of that since it's useless.

All that matters is your backdrop color. It's looking as though the backdrop is either not there or not opaque, and there's the problem.

Mind you the backdrop is NOT the plane master itself. It is a separate object or an overlay. Usually you'd just take a black or dary gray icon, and scale it up with a transform so it covers the whole viewport.
Thanks for the very helpful response.

As it turns out it was indeed invisible. Having its layer set to BACKGROUND_LAYER made it so it didn't render.

Now that it's rendering, though, it's blacking out everything.
Oh well, have to work with that.
Use BACKGROUND_LAYER+1. That should solve everything.
Unfortunately that didn't do it. Neither did BACKGROUND_LAYER + LIGHTING_LAYER, etc.
I think we're back at the start with the problem of mixing.

I can, for 100% sure, tell you that this backdrop exists, because if I set it to a normal layer, it blacks out everything; but if I take an individual light atom, changes its layer to something like EFFECTS_LAYER+1, it *does* pierces it while others can't.

So it exists, and you can draw light over it.

But if I take two of those light atoms, give them EFFECTS_LAYER, well, they don't mix well and they still shift to white.
Can you show me where your backdrop is defined? That might make some things clearer.

Also, have you tried getting that JSON output I mentioned? It will tell you where the backdrop is in relation to the render and why.
Sure, the backdrop is:

/obj/abstract/screen/backdrop
blend_mode = BLEND_OVERLAY
icon = 'icons/mob/screen1.dmi'
icon_state = "black"
layer = BACKGROUND_LAYER
screen_loc = "CENTER"
plane = LIGHTING_PLANE

/obj/abstract/screen/backdrop/New(var/client/C)
..()
if(istype(C)) C.screen += src
var/matrix/M = matrix()
M.Scale(world.view*3)
transform = M
verbs.Cut()


It is added to a mob on Login() via:

/mob/Login()
// ...
backdrop = new(client)
// ...


I've noticed we add it once with += and once with |= (after). It's useless but I doubt that's it.

The JSON we get from the command is not valid, somehow. What I did to make sure I get the correct minimal form is to stand with a basic mob on top of basic turfs.

In it, the backdrop is: https://pastebin.com/1GeugJwn

It looks like sometimes you have things of the form
"appearance_flags":0"KEEP_TOGETHER"
in it, which doesn't pass my JSON decoder.
First, definitely switch to BACKGROUND_LAYER+1 for good. You don't want a base layer of 0; that'll just cause confusion.

The malformed JSON is because you're not using the current version for the client. That was fixed several versions ago.
I am running the latest version, 514.1566. The JSON is still malformed, I need to change every instance of 0"PLANE_MASTER with ["PLANE_MASTER".

Here is what we get:

https://pastebin.com/j9kBJcMD

It's still quite a lot. But basically the backdrop is here.

          "layer": 2,
"plane": 14,
"subplane": -63,


I notice the debug JSON doesn't say the blend mode of the backdrop, but I assume it doesn't matter since we want it to be an overlay.

On that "plane": 14, the lighting plane, we only have the opaque backdrop, a semi-transparent plane drawn on top at very low transparency, and the master plane itself.

I did notice that our light objects have a fractional layer. Maybe that's the problem.
The layer we have on the server-side is still a whole number.
Oh, I see the JSON issue now. If you post a bug report for that I can fix it for the next version.

You're correct that the blend_mode is being omitted for BLEND_OVERLAY.

The only thing I see in that output that's odd, besides that you're still using that color matrix for the lighting plane (you should ditch that), is your lights have layers of 0 and 1. It'd be a good idea to give them normal layers, but that shouldn't be interfering with any drawing. The plane and subplane are sorted first, so those override any layering issues that should otherwise exist.
http://www.byond.com/forum/post/2715227 is the bug report for the JSON decoder.

As for the lights, they were not included in that debug output as the goal was to show the backdrop exists and is indeed in the plane we use.

The thing with a layer of 0 was something else.

The JSON output for a light atom as as follows.

https://pastebin.com/Ek27Utaa

          "layer": 0.999371,
"plane": 14,
"subplane": 0,


I assume it has a fractional layer because of microlayers mentioned in the DM reference.

Maybe the issue here is that they have a subplane of 0? All our light atoms have matching planes, layers, and subplanes.

They still shift to white when blending together. As demonstrated in the pictures of my second post, a monochromatic red light + a monochromatic red light results in a redder light, but two rgb(250, 100, 75) lights blend together into something that's much closer to white light than orange-red.

Again, please note also that the colour of our light atoms are "#7d3225", that is, rgb(125, 50, 37). That's because we spawn two of them.

They combine into an atom whose colour is almost undistinguishable from rgb(250, 100, 75). If that is not proof enough that the renderer is adding the individual R values, I don't know what is.
Maybe it'd help if you sent me a stripped-down test case.
I will see what I can do.