Jump to content
  • Advertisement

Calculating Irradiance Map

This topic is 810 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

I am trying to calculate irradiance map based on this article: http://www.codinglabs.net/article_physically_based_rendering.aspx

I understand the idea, you must place an hemisphere over the normal, then you need to sum every incoming radiance. Then write the radiance back at the corresponding pixel in the cube map. However I didn't understand how author iterated over the hemisphere and the direct translation from hlsl code to glsl didn't worked as I expected. It smooth the original cube map a lot. As if I dropped resolution from 1024px to 32px.


I changed the hemisphere iteration as below:


    normal = normalize(normal);
    vec3 up = vec3(0.0, 1.0, 0.0);
    vec3 right = normalize(cross(normal, up));
    int index = 0;
    vec3 irradiance = vec3(0.0 ,0.0 ,0.0);
    for (float longi = 0.0; longi <= 90.0; longi += 3.0)
     	mat4 trl = rotationMatrix(right, radians(longi));
        for (float azi = 0.0; azi <= 360.0; azi += 3.0)
         	mat4 tra = rotationMatrix(normal, radians(azi));
            vec3 sampleVec = (tra * trl * vec4(normal, 1.0)).xyz;
            irradiance += texture(iChannel0, sampleVec).rgb * dot(sampleVec, normal);

    fragColor = vec4((PI * irradiance / float(index)), 1.0);

Generated irradiance map seems to me too bright. Also I don't understand why we are averaging the summed radiance by dividing it to "index" ? Aren't we after the total incoming radiance to a point ?

Here is the link to the shader.



Edited by afraidofdark

Share this post

Link to post
Share on other sites
6 hours ago, afraidofdark said:

I didn't understand how author iterated over the hemisphere


Now I understand how the author traversed over the hemisphere. However there is still a problem. In monte carlo integration, you consider the integration out put as the average of random samples. This is why we divide the sum by "index". However, this part isn't clear to me yet. If I want to approximate x^2 integral over 0 to 5 period, does this method suggest me to take for example 100 random number in that interval and average them, and expect this outcome as my integration result ?

Share this post

Link to post
Share on other sites
37 minutes ago, afraidofdark said:

If I want to approximate x^2 integral over 0 to 5 period, does this method suggest me to take for example 100 random number in that interval and average them

I have lived enough enlightenment today thanks to scratchapixel ! I've seen the light ! (Literally) Since I used a cube map to look up incoming radiance, I tie my hands to use an approximation of the actual integral. Because for incoming radiance, I don't have a function which I can integrate over spherical coordinates !

Share this post

Link to post
Share on other sites

So the full monte carlo formula is essentially (1 / NumSamples) * Sum(f(x) / p(x), NumSamples), where f(x) is the function you're integrating, and p(x) is probability distribution function (PDF) evaluated for x. The PDF is basically the likelihood of a particular sample being chosen. For "uniform" sampling schemes where samples are distributed evenly across the whole domain (for instance, the surface of a sphere) the PDF is the same for all samples, and so you can pull it out of the sum.

Now for integrating irradiance for a point with normal N, you'll need integrate over the surface of the hemisphere that surrounds N. Uniformly sampling a hemisphere oriented around Z = 1 (the canonical "upper" hemisphere) is pretty simple:

// Returns a direction on the hemisphere around z = 1
Float3 SampleDirectionHemisphere(float u1, float u2)
    float z = u1;
    float r = std::sqrt(std::max(0.0f, 1.0f - z * z));
    float phi = 2 * Pi * u2;
    float x = r * std::cos(phi);
    float y = r * std::sin(phi);

    return Float3(x, y, z);

As input you need u1 and u2, which are two random variables in the range [0, 1]. These can be generated from a random number generator like rand(), or can be generated by a sequence that produces more a optimal sample distribution that avoids clustering (this is known as Quasi-Monte Carlo sampling).

To make this work for our case, we need to sample the hemisphere surrounding the surface normal. This means we need a transformation that can go from tangent space -> world space. If you're doing normal mapping then you probably already have such a matrix, and if not you can build one by starting with the normal as your Z basis and then generating a perpendicular vector to use as the X basis (after that you can use a cross product to generate the Y basis). Then you just transform the hemisphere sample direction by this matrix, and you now have a world space direction.Now if you recall from earlier, we need to divide samples by the PDF of each sample. For uniform hemisphere sampling the PDF is constant for all samples, and it ends up being one over the surface area of a unit hemisphere (1 / (2 * Pi)). So our algorithm ends up looking like this:

float3 irradiance = 0.0f;
for(uint i = 0; i < NumSamples; ++i)
    float u1 = RandomFloat();
    float u2 = RandomFloat();
    float3 sampleDirInTangentSpace = SampleDirectionHemisphere(u1, u2);
    float3 sampleDirInWorldSpace = mul(sampleDirInTangentSpace, tangentToWorld);
    float3 radiance = SampleEnvironment(sampleDirInWorldSpace);
    irradiance += radiance * saturate(sampleDirInWorldSpace, normalInWorldSpace);

float hemispherePDF = 1.0f / (2 * Pi);
irradiance /= (NumSamples * hemispherePDF);

Just be aware that if you use this result to light a surface using a Lambertian BRDF, make sure that you include the 1 / Pi term that's part of that BRDF. A lot of people like to bake the 1/ Pi into their light sources which is fine, you just have to be careful to make sure that you're doing it somewhere otherwise your diffuse lighting can be too bright.

Once you have this working, you can improve the quality by being smarter about choosing your sample directions. The first way to do that is to use QMC techniques like I mentioned earlier, for instance using Halton sequence or using stratified sampling. You can also just pick values that are evenly spaced out over the [0, 1] domain, which is basically stratified sampling without any jitter (this is essentially what you're doing in the code you posted above). The other way to is to importance sample your function by choosing samples that match the shape of the function being sampled. For irradiance, the common way to do this is to choose sample directions that are proportional to the cosine of the angle between that direction and the surface normal.

Share this post

Link to post
Share on other sites

  • Advertisement

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!