My goal is to render a volumetric point light as explained in this book's chapter 10: FGED Rendering The book explains the math pretty well, but I'm not sure I understand what is the required setup for a volumetric point light halo to be visible. The book describes the following code snippet to calculate the brightness of the volumetric halo:

float R = 5.f; // Radius of the volumetric point light halo.
float R2 = R * R;
float recipR2 = 1.f / R2;
float recip3R2 = 1.f / (3.f * R2);
float normalizer = 3.f / (4.f * R);

// This shader renders a halo effect of radius R at the object-space position pobject.
// The object-space camera position c and view direction z are given by cameraPosition 
// and cameraView. The pixelCoord parameter specifies the viewport coordinates of the
// pixel being processed, and it is used to read from the depth buffer.
float CalculateHaloBrightness(float3 pobject, float2 pixelCoord)
    float3 vdir = cameraPosition - pobject;
    float v2 = dot(vdir, vdir);
    float p2 = dot(pobject, pobject);
    float pv = -dot(pobject, vdir);
    float m = sqrt(max(pv * pv - v2 * (p2 - R2), 0.0));

     // Read z0 from the depth buffer.
    float2 depth = texture(depthBuffer, pixelCoord);
    float t0 = 1.0 + depth / dot(cameraView, vdir);

     // Calculate clamped limits of integration.
    float t1 = clamp((pv - m) / v2, t0, 1.0);
    float t2 = clamp((pv + m) / v2, t0, 1.0);
    float u1 = t1 * t1;
    float u2 = t2 * t2;

     // Evaluate density integral, normalize, and square.
    float B = ((1.0 - p2 * recipR2) * (t2 - t1) + pv * recipR2 * (u2 - u1) - v2 * recip3R2 * (t2 * u2 - t1 * u1)) * normalizer;
    return (B * B * v2);

There's a few things that I don't understand:

  1. What is meant exactly by pobject, cameraPosition and cameraView being in object-space? Does it just mean that they all have to be in world-space for this function to work correctly?

  2. On what surface am I supposed to execute this pixel shader on? From what I read, it sounds like it has to be run on a cube centered at pobject but I'm not sure.

  3. Is a light scattering medium required, such as fog? It would make sense that it does, but it's not really mentioned in the book's explanation.

I realize this is a difficult question to answer, since I haven't included the entire explanation present in the book here. I assume the author would not like that. So I am hoping that maybe somebody here might roughly understand what's going on and point me in the right direction.

edit: I am almost there! Here's my current HLSL implementation:

ConstantBuffer<pass_data> cb_pass : register(b0);
Texture2D<float> Depth : register(t3);
SamplerState linear_wrap_sampler : register(s0);

float4 ps_main(vertex_out pin) : SV_Target
    float2 uv = pin.posh.xy / cb_pass.sceen_size;
    float depth = Depth.SampleLevel(linear_wrap_sampler, uv, 0.f);

    // Volumetric halo variables.
    float halo = 0.f;
    float halo_radius = cb_pass.radius;
    float halo_radius2 = halo_radius * halo_radius;
    float rcp_halo_radius2 = 1.f / halo_radius2;
    float rcp_3halo_radius2 = 1.f / (3.f * halo_radius2);
    float density_integral_normalizer = 3.f / (4.f * halo_radius);

    // Render volumetric halo.
    float4 world_space_pixel = float4(pin.posw, 1.f);
    float3 pobject = float3(mul(world_space_pixel, cb_pass.world_to_object).xyz);
    float3 cam_pos = mul(float4(cb_pass.eye_pos, 1.f), cb_pass.world_to_object).xyz;
    float3 vdir = cam_pos - pobject;
    float v2 = dot(vdir, vdir);
    float p2 = dot(pobject, pobject);
    float pv = -dot(pobject, vdir);
    float m = sqrt(max(pv * pv - v2 * (p2 - halo_radius2), 0.f));

    float3 cam_view = mul(float4(cb_pass.eye_forward, 0.f), cb_pass.world_to_object).xyz;
    float t0 = 1.f + depth / dot(cam_view, vdir);
    float t1 = clamp((pv - m) / v2, t0, 1.f);
    float t2 = clamp((pv + m) / v2, t0, 1.f);
    float u1 = t1 * t1;
    float u2 = t2 * t2;

    float B = ((1.f - p2 * rcp_halo_radius2) * (t2 - t1) + pv * rcp_halo_radius2 * (u2 - u1) - v2 * rcp_3halo_radius2 * (t2 * u2 - t1 * u1)) * density_integral_normalizer;
    halo = (B * B * v2);

    return float4(halo, halo, halo, 1.f);

The only problem remaining is that the halo only appears when the camera gets very close to the halo. When the camera is close enough it does show up correctly though. I noticed that scaling cb_pass.eye_forward will make the halo show up from further away but I'm not sure what is causing this.


1 Answer 1


Here are a few steps to (hopefully) help get you going:

Rendering the cube: For the geometry a simple cube is needed, so really the first step is just to get a cube rendering just like you would expect, I suggest placing the cube at the origin in both world and object space.

After you are happy with the way the cube is rendered, change the winding direction for triangles the easiest way to do that is just a quick call to the API. So if the API is set to render counter clockwise as the front face, just change it so the front face is clockwise. This makes the "interior" of the cube visible.

At this point you aren't actually trying to render the halo, just the interior of the cube. So the fragment shader should just return a color. Once that is all set up and you are happy with it, the next step will be the actual rendering of halo's and is where you would call the halo functions in the fragment shader.

Thinking about the object spaces: The cube itself is in "object space" normally there is a Model to World matrix that we multiply the cube by that puts it in world space. When the book refers to "object space" it is referring to the cube's object space. (I think that book actually covers object/world spaces pretty well you may want to look up the chapter that goes over this to help clarify the subject)

No fog is needed, a halo will render correctly without any special medium.

Alpha blending will need to be enabled for the final halo, but you can skip that until the basic outline of the halo is rendering correctly.

Edit: Here is some code to help with cameraPosition and cameraView

mat4 Camera2World = inverse(view_matrix); // The view matrix 
mat4 World2Object = inverse(object_matrix); // The object2world matrix
vec3 cameraPosition = (World2Object * Camara2World[3]).xyz;
vec3 cameraView = (World2Object * Camera2World[2]).xyz;

Note that this is using column major matrices.

This isn't the most efficient code, but hopefully it will help get you going.

  • $\begingroup$ Thank you so much for the explanation! After writing this post I did come to the conclusion that I was supposed to render a box "inside out" but the "inside out" part I wasn't sure what was meant, so your advice to flip the normals clarified that. I am also additively blending. Now at this point I have the box rendering at world position (0,0,0) with the normals flipped. I am using the camera view and position in world space for now just to be safe and since the box is at (0, 0, 0) I should be fine. (continued) $\endgroup$
    – mbl
    Oct 13, 2021 at 20:40
  • $\begingroup$ Now the last question I have is, what value am I supposed to pass to pobject? From the book explanation, it sounds like I need to pass the current pixel position of the pixel that the box shader is currently rendering. But reading the comment above the description of the function it says: "This shader renders a halo effect of radius R at the object-space position pobject." so it makes it sound like in my case, I should pass (0,0,0). I tried both and neither give the result of rendering the basic shape of the halo. $\endgroup$
    – mbl
    Oct 13, 2021 at 20:40
  • $\begingroup$ Also I should mention, if I pass (0,0,0) to pobject, the result I get seem to be correct in the case where the camera is inside the radius R. If the camera steps outside of this radius, nothing is rendered. $\endgroup$
    – mbl
    Oct 13, 2021 at 20:52
  • $\begingroup$ pobject is the position values of the object which is the cube that is being rendered. So compute gl_Position using the mvp matrix and pass the the pobject values down to the fragment shader. $\endgroup$
    – pmw1234
    Oct 13, 2021 at 21:30
  • $\begingroup$ I used the pixel world position and it seems to (almost) work! I updated my post with the details. $\endgroup$
    – mbl
    Oct 14, 2021 at 5:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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