2D Lighting and Shadows

Started by DrAgonmoray, October 11, 2013, 02:26:32

Previous topic - Next topic

DrAgonmoray

Hi there. I'm attempting to make lighting effects similar to these:
http://www.youtube.com/watch?v=W53rTHXM6yo
http://www.youtube.com/watch?v=fsbECSpwtig

Sadly, I have no idea how to accomplish this. I can draw triangles and quads and set their color, but beyond that I am lost.
I've been searching for days, but I just can't seem to find a place to learn how to do what I'm trying to accomplish.

I've decided that it would be best to do this using GLSL shaders. I successfully made vertex and fragment shaders, but I don't know what to do with them.

Sample code would be great, but even just a nudge in the right direction would be much appreciated.

quew8

Might I recommend http://www.lighthouse3d.com/tutorials/glsl-tutorial/. If you know about GLSL then you can just skip to the lighting stuff, otherwise it's all good.

DrAgonmoray

Quote from: quew8 on October 11, 2013, 16:53:37
Might I recommend http://www.lighthouse3d.com/tutorials/glsl-tutorial/. If you know about GLSL then you can just skip to the lighting stuff, otherwise it's all good.

Great! Thanks for the link. I've now gotten a light on the screen:


I think I've heard this be called a pixel shader... Anyway, I fill the screen with a black quad. Then the color for each pixel is calculated based on the color of the light and the distance from it. This is done in a fragment shader.

Now I would like to create a variable number of lights. I believe the way this should be done is by rendering each light by itself, then blending these renderings together (much like you would change the Blending Mode in an image editor)

Where would I look in order to accomplish something like this?

DrAgonmoray

OKAY! I've got multiple lights working...


Use this blending mode:
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);


For each light:
I set uniform variables which get passed to the shader. These are color, position, and radius.
Clear the background using black.
Render a quad that takes up the entire screen.

And somehow it works... I did it by accident :D

Now for shadows!

quew8

Another name for a fragment shader is a pixel shader since it runs once for each pixel in a primitive. So another name for this method is per-pixel lighting.

Personally I think it would be better to blend all the lights together in the shader and then you can do all the lights in a single render pass. I'm not sure exactly how you are doing it, but if the fragment colour starts as black (0, 0, 0) then you iterate over all the lights and add the light's "colour contribution" to the final colour. Probably finish off with a clamp of some kind.

For shadows, I use the method described here: http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping/#Basic_shadowmap or for 2D (although I've never actually done this) https://github.com/mattdesl/lwjgl-basics/wiki/2D-Pixel-Perfect-Shadows

DrAgonmoray

I wanted to do them all in a single pass, but I don't know how to pass something like an ArrayList of lights to a shader.

Using my current method, however, I have got some shadows:


Here's my process:

For each light:
- enable light shader
- glBlendFunc(GL_ONE, GL_ONE);
- Draw fullscreen quad with a light on it
- disable light shader
- For each block:
- - glBlendFunc(GL_ONE, GL_ZERO);
- - Draw shadow (black quad)

how can I fix this?




quew8

Well there's nothing like an array list in GLSL (it's like C without dynamic memory). However there are options - annoyingly I went through the various options in a lot of detail in a previous reply somewhere but now I can't find it anywhere. You seem smart though so I'll just briefly describe them:

1) Use an array of lights in the shader that is at long enough to hold as many lights as your game might need. This can actually be determined at runtime or at level load time by dynamically changing the source you pass to OpenGL. Also send your shader the number of lights in use and hence how many it needs to iterate through.

2) Fill a texture with light data rather than image data and give your shader this as a sampler. Then decode this information into usable data in the vertex shader. Textures are really the only way to get dynamic memory in GLSL.

As for your shadow problem. Let me get back to you.

quew8

OK, I am guessing that this misbehaviour is down to the way you blend the last pass (you probably guessed this too) it would be easier if I could see a bit more code - I'm having some real problems visualizing what your code is doing. A little debugging tip though - look at the result after various stages of the rendering. (Like after first light before shadows, after first light shadows etc.)

Also, I'm not sure that these shadows are something you want. They don't look realistic. If you shine a light onto a shadow, it doesn't change the colour of the shadow it gets rid of the shadow. Perhaps you could mess around with the way you draw the shadow but I doubt it. I have another wee suggestion:

You clearly have a really nice algorithm for working out where the shadows are in terms of one light but other lights mess it up. What I would do is: For each light draw into the stencil buffer where there ISN'T a shadow i.e. where the light falls. (You can draw directly into the stencil buffer by
1) For God sake making sure you have requested a stencil buffer
2) enabling stencil test
3) Setting the stencil function to GL_NEVER with the reference set to whatever value you want to draw.
4) Setting sfail param of the stencil operation to GL_REPLACE
5) For God sake making sure you have requested a stencil buffer again
6) Remebering to disable the stencil test afterwards
) and then after drawing all the lights, drawing black wherever there isn't a value written into the stencil test. (Ie set the stencil test to GL_NEQUAL to the reference value you gave glStencilFunc() before )

Sorry to spam all over your thread.

DrAgonmoray

Thank you so much for all your help - I really appreciate it.

I played around with blending modes and layers in Photoshop and I think I know how to fix my problem:
I need to render the lights, followed by the boxes and shadows. Then I need to "merge" these together and then blend the entire thing, rather than blending JUST the lights.
I think I need to use Frame Buffer Objects. I would render the stuff to the FBO, set the blending, then render the FBO to the canvas. I have no idea how to do this though c:

Also, I think these shadows are something I want. I don't see whats unrealistic about them, except for them being point lights.

Anyway, I'm trying to work with the stencil buffer as per your suggestion. Here's my code:

Display creation:
Display.create(new PixelFormat(0, 16, 1));


OpenGL setup:
glEnable(GL_STENCIL_TEST);
glClearStencil(0);

glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0, 800, 600, 0, 1, -1);
glMatrixMode(GL_MODELVIEW);


Rendering:
glClearColor (0, 0, 0, 0);
glClear (GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

for (Light light : lights) {
	glStencilFunc(GL_ALWAYS, 1, 1);
	glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
	glColor4f(0, 0, 0, 1);
	for (Block block : blocks) {
		//render block and its shadow
	}
	glStencilFunc(GL_EQUAL, 0, 1);
	glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);

	glUseProgram(shaderProgram);
	glEnable(GL_BLEND);
	glBlendFunc(GL_ONE, GL_ONE);
	//pass information to pixel shader
	glBegin(GL_QUADS); {
		glColor3f(1, 1, 1);
		glVertex2f(0, 0);
		glVertex2f(0, 600);
		glVertex2f(800, 600);
		glVertex2f(800, 0);
	}
	glEnd();
	glDisable(GL_BLEND);
	glUseProgram(0);


	glFlush();
	Display.update();
	Display.sync(60);
}


I'm getting this result:

As you can see, shadows are rendered, but they are unaffected by other lights. I'm not sure where to go from here

Again, thank you

DrAgonmoray

OMGITHINKITSWORKING.



Turns out, I had two more problems:

1) I wasn't clearing the stencil buffer after each light. Fix: glClear(GL_STENCIL_BUFFER_BIT);
2) I was still rendering the shadows to the screen, rather than just to the stencil buffer. Fix: glColorMask(false, false, false, false);

After I get things smoothed out, I'm going to be posting the source so hopefully people will learn from it. I have a feeling there are many improvements that could be made to mine though :3

quew8

OK, I was skeptical but that actually looks really awesome. Perhaps you could also post a video showing them at work dynamically. Ie. with the objects or the lights moving. I think it would be really cool. I'd love to know what kind of game your making here because you have the potential for some absolutely superb atmosphere in my opinion.

1) I should have thought to say that. My fault.
2) That's actually a much, much better solution than what I proposed. Would never have thought to do that. Nice one.

Definitely a good idea to post up some source if you're willing. In fact if you've got the time it might even be worth doing a little talk through of the method alongside the source in JGOs "Articles and Tutorials" section. If you haven't got the time for this and are willing I'd be happy to write it up myself (since I think I understand the method) of course giving you all credit.

Congrats again on a fabulous looking technique. One image and you blew me away.

@

Really nice. What a shame it sure doesn't work under gl11 (that's what you get with a 8 years old computer). Great work, like quew8 said you should record it in action.

DrAgonmoray

Thanks guys. I'll make a video soon, and also post the source with accompanying documentation.

I realized that the boxes themselves never actually get lit. Technically, its correct, but it might look better if the boxes themselves did get lit up a bit.
Before I was rendering a shadow for every face of the object, so the shadows would cover it up. Using the optimization described in this article,http://forums.tigsource.com/index.php?topic=8803.0 I was able to only render the necessary shadows, thereby giving a speed boost and also revealing the object.

Right now the objects are axis-aligned (you just give them x, y, width, height), but I'll probably implement Box2D and make some stuff move.

All I can say is, I'm actually quite proud of myself. :D

Fool Running

Quote from: DrAgonmoray on October 15, 2013, 00:20:12
Thanks guys. I'll make a video soon, and also post the source with accompanying documentation.
I'm also very interested in the technique you used. I look forward to seeing the source and video.  :o
Programmers will, one day, rule the world... and the world won't notice until its too late.Just testing the marquee option ;D

DrAgonmoray

Hey guys! I got a video made. It's a little bit lengthy (6ish minutes) but it has some pretty good stuff I think.

The reason it took so long is because I realized that I had kindof a major problem that I didn't notice before: if an object was in a shadow, it would still get lit up. Obviously if you're in a shadow, no light is reaching you. I thought I fixed this on Monday, and I realized I did, but Box2D was giving my 8 verticies for a square which messed everything up.

I also added directional lights which are pretty cool.

http://www.youtube.com/watch?v=nSf1MpsWKig

Here's a stripped-down version of the source. This doesn't have physics, directional lights, input, or colored boxes, but it should be enough to get somebody going. It's basically all the "hard" parts (which aren't so hard in retrospect!).

https://github.com/DrAgonmoray/BasicLighting/

PLEASE oh PLEASE help me make the code better. I still do not know OpenGL even REMOTELY well at all. There's bound to be things that can be improved. Here's a couple things off the top of my head which I think could be possible offenders:

  • glColorMask(false, false, false, false); used to stop rendering (so it only renders to stencil buffer). I feel like 1) this still renders, its just invisible 2) there's a better way
  • glBegin(GL_QUADS); etc. I'm pretty sure there's a newer and faster way to draw these things.

Also, the code isn't as efficient as it could be. A made a bunch of variables to try to make the code self explanatory. Obviously you could consolidate them if you wanted to.

thanks again everyone!