How to make a Photon Mapper : Photons everywhere

Bleed Color and Caustics.
Bleed Color and Caustics.

Hello,

It has been a long time since I posted anything on this blog, I am so sorry about it.
I will try to diversify my blog, I’ll talk about C++, Rendering (always <3), and Qt, a framework I love.

So, in this last part, we will talk about photons.

What exactly are Photons ?

A photon is a quantum of light. It carries the light straightforwardly on the medium. Thanks to it, we can see objects etc.

How could we transform photons in a “visible value” like RGB color ?

We saw that the eyes only “see” the radiance !
So we have to transform our photons in radiance.

From physic of photons

We know that one photon have for energy :

\displaystyle{}E_{\lambda}={h\nu}=\frac{hc}{\lambda}

where \lambda is the wavelength in nm and E in Joules.
Say we have n_\lambda photons of E_{\lambda} each.
We can lay the luminous energy right now :

\displaystyle{Q_{\lambda}=n_{\lambda}E{\lambda}}

The luminous flux is just the temporal derivative about the luminous energy :

\displaystyle{\phi_{\lambda}=\frac{dQ_{\lambda}}{dt}}

The idea is great, but we have a luminous flux function of the wavelength, but the radiance is waiting for a general luminous flux
So, we want to have a general flux which is the integral over all wavelengths in the visible spectrum of the \lambda luminous flux.

\displaystyle{\phi=\int_{380}^{750}d\phi_{\lambda}=\int_{380}^{750}\frac{\partial \phi_{\lambda}}{\partial\lambda}d\lambda}

Now, we have the radiance

\displaystyle{L=\frac{d^2 \phi}{cos \theta dAd\omega}=\frac{d^2(\int_{380}^{750}\frac{\partial \phi_{\lambda}}{\partial \lambda}d\lambda)}{cos(\theta)dAd\omega}=\int_{380}^{750}\frac{d^3\phi_{\lambda}}{cos(\theta)dAd\omega d\lambda}d\lambda}

Using the rendering equation, we get two forms :

\displaystyle{L^O=\int_{380}^{750}\int_{\Omega^+}fr(\mathbf{x}, \omega_i,\omega_o,\lambda)\frac{d^3\phi_{\lambda}}{dAd\lambda}d\lambda}
\displaystyle{\int_{\Omega^+}fr(\mathbf{x}, \omega_i,\omega_o)\frac{d^2\phi}{dA}}

The first one take care about dispersion since the second doesn’t.
In this post, I am not going to use the first one, but I could write an article about it latter.

Let’s make our Photon Mapper

What do we need ?

We need a Light which emits photons, so we could add a function “emitPhotons” .

/**
 * @brief      Interface for a light
 */
class AbstractLight {
public:
    AbstractLight(glm::vec3 const &flux);

    virtual glm::vec3 getIrradiance(glm::vec3 const &position, glm::vec3 const &normal) = 0;

    virtual void emitPhotons(std::size_t number) = 0;

    virtual ~AbstractLight() = default;

protected:
    glm::vec3 mTotalFlux;
};

We also need a material which bounces photons :

class AbstractMaterial {
public:
    AbstractMaterial(float albedo);
    
    virtual glm::vec3 getReflectedRadiance(Ray const &ray, AbstractShape const &shape) = 0;
    
    virtual void bouncePhoton(Photon const &photon, AbstractShape const &shape) = 0;
    virtual ~AbstractMaterial() = default;
    
    float albedo;
protected:
    virtual float brdf(glm::vec3 const &ingoing, glm::vec3 const &outgoing, glm::vec3 const &normal) = 0;
};

Obviously, we also need a structure for our photons. This structure should be able to store photons and compute irradiance at a given position.

class AbstractPhotonMap {
public:
    AbstractPhotonMap() = default;

    virtual glm::vec3 gatherIrradiance(glm::vec3 position, glm::vec3 normal, float radius) = 0;
    virtual void addPhoton(Photon const &photon) = 0;
    virtual void clear() = 0;

    virtual ~AbstractPhotonMap() = default;
private:
};

How could we do this ?

Photon emitting is really easy :

void SpotLight::emitPhotons(std::size_t number) {
    Photon photon;

    photon.flux = mTotalFlux / (float)number;
    photon.position = mPosition;

    for(auto i(0u); i < number; ++i) {
        vec3 directionPhoton;
        do
            directionPhoton = Random::random.getSphereDirection();
        while(dot(directionPhoton, mDirection) < mCosCutoff);

        photon.direction = directionPhoton;
        tracePhoton(photon);
    }
}

We divide the total flux by the number of photons and we compute a random direction, then we could trace the photon

Bouncing a photon depends on your material :

void UniformLambertianMaterial::bouncePhoton(const Photon &_photon, const AbstractShape &shape) {
    Photon photon = _photon;

    float xi = Random::random.xi();
    float d = brdf(vec3(), vec3(), vec3());

    if(photon.recursionDeep > 0) {
        // Photon is absorbed
        if(xi > d) {
            World::world.addPhoton(_photon);
            return;
        }
    }

    if(++photon.recursionDeep > MAX_BOUNCES)
        return;

    photon.flux *= color;
    photon.direction = Random::random.getHemisphereDirection(shape.getNormal(photon.position));
    tracePhoton(photon);
}

To take care about conservation of energy, we play Russian roulette.
Obviously, to take care about conservation of energy, we have to modify the direct lighting as well ^^.

vec3 UniformLambertianMaterial::getReflectedRadiance(Ray const &ray, AbstractShape const &shape) {
    vec3 directLighting = getIrradianceFromDirectLighting(ray.origin, shape.getNormal(ray.origin));
    float f = brdf(vec3(), vec3(), vec3());

    return color * (1.f - f) * f * (directLighting + World::world.gatherIrradiance(ray.origin, shape.getNormal(ray.origin), 0.5f));
}

Finally, we need to compute the irradiance at a given position :
It is only :

\displaystyle{E=\sum \frac {\phi}{\pi r^2}}

So we could easily write :

vec3 SimplePhotonMap::gatherIrradiance(glm::vec3 position, glm::vec3 normal, float radius) {
    float radiusSquare = radius * radius;
    vec3 irradiance;
    for(auto &photon : mPhotons)
        if(dot(photon.position - position, photon.position - position) < radiusSquare)
            if(dot(photon.direction, normal) < 0.0)
                irradiance += photon.flux;

    return irradiance / ((float)M_PI * radiusSquare);
}

To have shadows, you could emit shadow photons like this :

void traceShadowPhoton(const Photon &_photon) {
    Photon photon = _photon;
    Ray ray(photon.position + photon.direction * RAY_EPSILON, photon.direction);

    photon.flux = -photon.flux;

    auto nearest = World::world.findNearest(ray);

    while(get<0>(nearest) != nullptr) {
        ray.origin += ray.direction * get<1>(nearest);
        photon.position = ray.origin;

        if(dot(ray.direction, get<0>(nearest)->getNormal(ray.origin)) < 0.f)
            World::world.addPhoton(photon);

        ray.origin += RAY_EPSILON * ray.direction;

        nearest = World::world.findNearest(ray);
    }
}

That’s all ! If you have any question, please, let me know !

Comments

Leave a Reply