Sunday, April 30, 2017

1000 Dynamic Lights

It's been over a month since my last post. For the past month, I've been working on a number of smaller unrelated topics, so I'll probably add a few shorter length posts in quick succession. Recent work has been on the subjects of large numbers of dynamic lights, the game mode freeze gun, instanced placement of complex 3D models in tiled terrain mode, and tiled terrain animated clouds.

The first topic is dynamic light sources. I've discussed my dynamic light implementation in 3DWorld before. Lights are managed on the CPU, then the visible lights are sent to the GPU each frame as a group of 1D and 2D textures. These textures encode the world space position of each light source grouped spatially, so that the GPU fragment shader can efficiently determine which lights affect which pixels. Every fragment (similar to a pixel) looks up its position in the lights list texture using the {x,y} position of the fragment. Then, the contribution of each light in the list corresponding to that grid entry is added to the fragment. Typical texture/grid size is 128x128.

This has been working well for a long time. I've been experimenting with 100 dynamic light sources in scenes and have posted screenshots of these in my planar reflection posts such as here and here. I recently experimented to see how many lights I could add to a scene while achieving realtime (60+ FPS) rendering. After various low-level optimizations on the CPU side, I managed to get up to 1000 lights for many scenes. Here are some screenshots of the office building scene with dynamic light sources at night.

Office building scene viewed from outside, at night, with 1000 dynamic, floating, colored light sources.

Back of the office building where the only lighting comes from 1000 dynamic light sources, at 67 FPS.

Keep in mind that all of these glowing spheres are moving, colliding with the scene, and casting light. In addition, they cast shadows from the sun and moon. However, I've created these images on a moonless night, so that there are no other light sources. All lighting comes from dynamic lights. I normally get around 60 FPS (frames per second) with reflections enabled and 90 FPS without.

Here is a view from inside the building lobby. There are a few dozen lights visible in this room alone. I've enabled reflections for the floor so that the glowing sphere reflections are visible.

A room densely filled with light sources, which also reflect off the floor.

Here is a screenshot from a larger room that contains around 100 light sources. The building is huge! This is just one of 4 floors (+ basement), all full of lights.

Another room containing over 100 light sources, with floor reflections.

The max number of lights is actually 1024, because I encode light indices as 10-bit values within the lookup textures. I used to run into problems with this limit when there were a large number of reflected laser beams in the scene during gameplay. I previously had implemented laser beam light using a series of small point light sources along the beam path. This quickly adds up when there are multiple beams crossing an entire floor of the building. Each beam segment could have over a hundred point lights. Now I'm using analytical line light sources for laser beams, which are directly rasterized into the lighting lookup texture. One beam segment is only one light source.

I also increased the number of lights in the Sponza atrium scene from 100 to 200. This scene is much smaller (~8x), so the lights are packed together more closely. They have the same light radius in a smaller space. Each pixel has contributions from 10-20 light sources. 1000 lights is possible, but overkill. The frame rate drops and the walls appear a very bright white.

Sponza atrium scene with 200 dynamic colored light sources and floor reflections.

These images are also taken at night, where the glowing spheres and fires are the only light sources. Here is the same scene from a different view on the lower level, with smooth reflective floors enabled. There are no shadows for these light sources, so they shine through walls.

Sponza scene lower level, with floating lights and reflective floor. The fire and burning embers also cast light.

Note that the fire on the right of the image also emits dynamic light, with approximate shadows. In addition, it throws out glowing embers, which also emit light. You can see three of these on the floor, reflecting their red glow on the smooth marble.

Here are two older screenshots of the Sponza scene with fewer lights.

Sponza scene at night, lit by only dynamic lights.

Sponza scene at night, with reflections on the floor.

I've also implemented dynamic cube map shadows for moving light sources. It's limited to a small number of lights, currently only 9 (64 textures, 6 textures per cube face/light). This is determined by shader size and uniform variable count. I could increase the texture array size for my new system/GPU, but I also want this to work properly on older systems. For now, I have added a reasonable cap on shadowed lights.

Here is a screenshot showing two shadow casting lights near the stairs in the office building. If you look closely, you can see some shadows cast by the red light on the stairs and support columns.

Two shadow casting, moving lights. Shadows can be seen on the stairs for the red light on the right.

The approach I'm using for local lighting is similar to a standard tiled forward approach, also known as Forward+. Some reference articles and examples can be found here, here, and here. This is an alternative approach to deferred shading. 3DWorld was originally written using a forward rendering pipeline because I wanted to have a lot of transparency effects. Also, deferred shading wasn't very common back when I started writing 3DWorld in 2001, and I've never gotten around to implementing it. The tiled forward approach works well in my engine so I've continued to use it.

However, I'm doing it a nonstandard way. I came up with my system years ago, before deferred shading and tiled forward rendering were well known. This is one of the early application of GPU shaders to replace the fixed function pipeline in 3DWorld. Instead of using screen space tiles, I use world space tiles in the XY plane. This is the floorplan or map view of the building; I use up=Z in 3DWorld. In the past few weeks, I attempted to implement screen space lighting tiles in the standard way. I thought it might yield better performance, but my experiments showed that this was not the case. Why is that? Part of the problem is that it takes more work to transform world space light positions into screen space, but that's a minor issue. Maybe I haven't optimized it properly. But there's more.

3DWorld's "ground mode" is intended to be a first person shooter environment. The player is usually standing on the ground/floor, unless they have the flight powerup or are in spectate mode. This means that the view direction is often in the XY plane. A common case is to be looking down a hallway that has lights on the sides. Using my world space light tiles, these lights will be in their own texture tiles. The subdivision system effectively splits them up so that pixels along the view direction (along the hallway) will see different tiles at different distance/depth. On the other hand, if screen space tiles are used, the rows of lights will all stack up behind each other at different depths, which means that many of them will contribute to the same pixel. Screen space tiles appear to be slower in this situation.

Maybe I'm doing something wrong here? Or maybe my system just works better for my type of scenes? I suspect that if the lights are small and dense enough, the screen space system works better. For example, if there are 100 lights that all fit within a small area of a single XY tile in 3DWorld, they would all contribute to each pixel. The tile subdivision system is ineffective. But if these were screen space tiles, there would still be subdivision. I guess this case doesn't come up much in 3DWorld because there are no sources of small, dense lights. Explosion particles and shrapnel, the smallest light casting objects, use a system of grouping of multiple smaller lights into a single larger virtual light to work around this problem. World space tiles sized on the order of a typical light source's radius work well in most cases.

Some tiled forward systems use 3D tiles (screen X, screen Y, and depth/Z). [I've read about this before, but I can't seem to find the original article.] That would give better resolution along the hallway, fixing this issue. It would seem to guarantee that no matter what the view vector and light placement is, the tiles always split up light sources that are far apart from each other in world space. The only pathologically bad case is where many light sources are close enough to each other that their spheres of influence overlap. Of course, I could add a 3D texture/array of world space tiles to my system as well. I have experimented with that before, but it turns out the performance is worse. 3D texture lookups are slower than 2D texture lookups, the math is more complex (and slower), and I need to use more total tiles. This increases memory usage and CPU => GPU transfer costs as well.

As far as I can tell, the dynamic lighting system I've implemented in 3DWorld works well. It scales to 1000 lights for a large scene, with no problems. If I ever need more than 1024 lights, the system needs to change. For now, this seems to be sufficient for even my most demanding scenes.

No comments:

Post a Comment