To be clear, I really do mean volumetric lighting; not only calculating which area is lit, but also realistically lighting the volume of "air" that the light hits on the way.

Like this:

enter image description here

I've tried are 2 approaches: a pre-rendered 2D image and a 3D cone. The 2D image always looks flat, no matter how it is rotated in world space. The cone on the other hand is next to impossible to control when it comes to fade distance, doesn't look soft (smooth) and is expensive to compute.

Is there a better way?

  • 1
    \$\begingroup\$ You can try rendering depth and detecting which pixels cover some the cone volume. This is like old fog volumes pretty much from like quake tech. The idea is to cast a ray from viewer into the scene and add up how much fog you pass. If done properly this can be like adding and subtracting intervals of penetration and exit. The limitation is that fog volumes can't be overlapping. \$\endgroup\$ – RandyGaul Jun 6 '14 at 6:38

(Someone else can write a more in-depth answer since these steps require a bit of non-trivial math, but since there's no answer and this question is probably important for some I'll post). There are few ways depending on your scene and how modern you want to go.

You can either render a fullscreen quad naively or render a 2D billboarded rectangle covering the cone's area. In your pixel shader you'd pass in the cone position, direction (both in world space), and color. For every rasterized pixel you'd create a world space ray by creating a camera space ray and transforming by the camera matrix into world space. Then use that ray to intersect with cone based on the uniform cone data you passed in. Extract the front and back face intersection points and calculate the difference. This is your depth. If you only have a backface point then your camera is inside so use the ray's starting position and calculate the difference.

(Note this works even if you're looking into the cone at the light source. In the end it'll appear almost opaque depending on your weights since the depth values are large).

You then need to use https://en.wikipedia.org/wiki/Transmittance#Beer.E2.80.93Lambert_law. A common mistake is to try to use alpha blending equations with the depth value in someway. If you try using alpha transparency blending you'll notice it produces very unrealistic results. This is very noticeable if you have overlapping cones since there's no formula using alpha blending to treat two mediums as one. Using Lambert's law solves this allowing for multiple overlapping beams to be rendered as you would expect. Just to be very clear you cannot get a realistic result using alpha blending or additive blending techniques. This might be acceptable though for some though. I'm going to use the word depth from here on out, but realize that with lambert the light is going through a medium. If you support multiple different mediums you'll need to pass that in and use it as a weight to the depth. I'm assuming air here everywhere thus some constant absorption weight.

Note that what you'll end up with a kind of murky cone shape that looks like a transparent cone and not a beam. A cone light's intensity will be concentrated at the center of the beam and fall off radially. To take this into consideration there's a few methods. The first is you can sample the distance to the center of the beam starting from the front intersection point and stepping a few times to the back intersection point. At each step you calculate the minimum distance to the light ray and then take the average of all samples. The second approach is to solve the integral for the average distance directly. (I'm not sure the latter has a closed form, but it might). Both approaches give you the average distance to the center of the beam of light.

You'll also need a second average distance which is the average distance to the light position. You can sample along the ray from the front to the back intersection point or solve the integral like before. (This one should have a closed form solution). Lights fall off distance squared so you can optionally use this to weight the intensity if you want the beam to fall off over distance along the light ray. (You can use a weight like secondDistance / maxDistance or square it if you want, but linear falloff sometimes looks better). In any case we need this average for the beam falloff away from the ray direction. (Since it's a cone).

The first distance value can be used with the second to weight the intensity which indicates how close the intersecting ray is to the center of the light's beam along the ray. For the second distance calculate the maximum radius of the cone at that distance. Using this a linear weight can be created that looks like: 1 - (secondDistance / radius). You can use this or a distance squared variant and combine it with the firstDistance weight by multiplying. (You can combine these in many ways to produce different cone properties. You can even run the firstDistance through a max function to make the core of the beam evenly lit like in the image).

Using these two weights you'll end up with a realistic volumetric cone of light probably identical to the image above.

I haven't explained how to handle overlapping or intersecting cones though. I did explain not to use additive blending though. The issue with a forward approach of rendering the cones is you lack the depth information of the previously rendered cone. You can use multiple render targets and render the depth additively such that the next cone, rendered front to back can sample for the the depth information and calculate the opacity weight left from the lambert calculations. (Again this assumes every cone passed through is going through a medium of constant absorption. If that's not the case then you can store a weighted depth value based on absorption and use that instead).

Handling intersecting cones is not simple, but with modern hardware it's very doable. There's an ordered independent transparency (OIT) algorithm that uses append buffers (a-buffers) where instead of rendering front to back all the fragment data is stored in a linked list data structure in the GPU. AMD wrote the initial basics I believe. Instead of storing just the color you'd render each cone and store the color, front face depth z value, and back face depth z value, and optionally the medium's absorption if you're using it. (In reality you'd be storing a linked list of elements with the structure { z:float, color:float3, absorption:float, front:bool } where the front and back faces are separate). Then after rendering all the cones into the OIT linked list in a second pass sort the linked lists then perform the Lambert law blending. (For overlapping z ranges take the average of the colors and add the absorption weights).

I should point out that this works for any volumetric object, not just cones. If you don't want to mathematically compute the front and back faces with the linked list approach you can just render the front and back face meshes and in the pixel shader perform the linked list insertions with the corresponding z values.

I hope I didn't make a mistake in this explanation. I used a technique nearly identical to this in a project a long time ago for a voxel test with volumetrically rendered voxels. I have a simple blending function here: http://ideone.com/PVXdB if the wikipedia article was confusing. In that one volume is a function of depth and absorption.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.