A Better Lookup TableBy tweaking the optical length lookup table just a little, it can be used for both optical length calculations (one to the light source and one to the camera), it can tell you what the density is at any point, and it can tell you when a ray enters the shadow of the planet. This leaves you with one integral which, as long as your lookup is precise enough, 3 or 4 samples seems to be sufficient to make it look good. I've only implemented a prototype so far, but it runs in real-time with 100% of the calculations done on the CPU and using a fairly sloppy brute-force rendering method. It runs between 50-100 FPS on my system, and it should be easy to make it go a good bit faster. The lookup table I generate is a bit different from the one described in the publication. It fits nicely into a 2D texture map in case someone wants to try to use it in vertex/pixel shaders. It uses two channels for Rayleigh scattering, and two channels for Mie scattering. For all channels, the horizontal axis represents the height of a sample point, and the vertical axis represents the angle of a sample ray from vertical. The angle goes all the way to 180 degrees (straight down) because viewpoints above the ground can see farther than 90 degrees at the horizon. There are two ways to interpret the result if the ray intersects the planet on its way to the outer edge of the atmosphere. If you want the optical legth to the sun (or other light source), then the optical length should be zero because it means the sample point is in the shadow of the planet. If you want the optical length to the camera, then you have to pretend like the ray didn't hit the planet. This is because many triangles at the horizon have one or two vertices behind the planet, and making those vertices black really messes up your horizon. Channel 1 - If the ray hits the planet, this value is 0. Otherwise, this value contains the Rayleigh scale density for this height.
The lookup table is generated in CPixelBuffer::MakeOpticalDepthBuffer(), and it is commented. I hope this description and the comments make it clear what it's doing. Using the Lookup TableEven though the use of this table seemed intuitive when I first came up with the idea, I ran into a surprising number of problems implementing it. Like Sir Robin exclaiming "Hey! That's easy.", I was snatched off my fake horse (so to speak). Luckily I landed on something soft and was able to work out all of the problems. Unfortunately I still haven't figured out how to avoid the outer integral without creating a 4-D lookup table. That 4-D lookup table would also be less flexible, and I'd need a different one for each planet or moon that had an atmosphere. Since I can't get around it, this means I have to trace the ray from the camera to each vertex, break it up into sample rays and calculate the scattering across each sample ray. For each sample ray, calculate the center point and determine its height and the angle to the light source. Perform a lookup in the table using that height and angle. If channel 1 is 0, then this sample is in shadow and you can skip this sample ray and move on to the next one. Otherwise store the value of channel 1 as the atmospheric density at the sample point, and store the value of channel 2 as the optical depth from the sample ray to the light source. Now it's time to determine the optical length to the camera for the sample ray, and this is where things get a bit tricky. It's easy if the camera is in space because it works just like the light source. Calculate the height of the sample point and the angle to the camera, perform a lookup in the table, and add channel 2 to the optical depth you stored from the first lookup. When the camera is inside the atmosphere, the lookup table breaks. This is because it stores optical depths from one point inside the atmosphere to the top of the atmosphere. It does not store optical depths from one point inside the atmosphere to another point inside the atmosphere. To complicate matters a bit more, when a point is above the camera the angle to the camera points down, putting you in the wrong spot in your lookup buffer. The answer to the first problem is to do two lookups (one for the sample point and one for the camera), and subtract one from the other. The answer to the second problem is to reverse the ray direction for both lookups when the target vertex is higher than the camera. To keep the code clean, when the camera is in space, you can treat it like it's at the top of the atmosphere and like it's always above any vertex being rendered in the atmosphere. The pseudo-code looks something like this:
if(camera is above vertex)
camera_lookup = lookup(camera_height, angle to camera);
else
camera_lookup = lookup(camera_height, angle from camera);
for(each sample ray)
{
light_source_lookup = lookup(sample_height, angle to light source);
density = light_source_lookup(channel 1);
if(density > 0)
{
density *= sample_ray_length;
optical_depth = light_source_lookup(channel 2);
if(camera is above vertex)
{
sample_point_lookup = lookup(sample_height, angle to camera);
optical_depth += sample_point_lookup(channel 2) - camera_lookup(channel 2);
}
else
{
sample_point_lookup = lookup(sample_height, angle from camera);
optical_depth += camera_lookup(channel 2) - sample_point_lookup(channel 2);
}
calculate_scattering_attenuation;
}
}
calculate_vertex_color;
This figure illustrates a few sample camera points and vertices, with one ray broken up into three sample rays. The orange lines indicate the rays that must be traversed for scattering calculations and the ray direction for calculating angles. The blue lines indicate the part you have to subtract from the first lookup in the optical depth table. For the ray broken into sample rays, the blue line is only shown for the first sample point. The red lines pointing to each sample point make it easier to see the different heights and angles for each sample point. The green lines going from each sample point make it easier to see how the height and angle affects the optical depth to the light source. The real code for the pseudo-code above is in CGameEngine::SetColor(), and it is commented. I hope the pseudo-code and figure make it clear what the code is doing. If not, I'm sure I'll hear about it on the forums. ;-) |
|