I am trying to implement a simple path tracer with next-event estimation in Java. The general idea is to trace a ray through the scene as usual (using a cosine distribution to determine the next random direction at every bounce), while also taking the direct illumination into account at every bounce. I think I have most pieces of the puzzle available, but I can't fit them together correctly.

Here is how I shade matte objects: add the direct illumination to the radiance coming from a random ray (chosen according to a cosine distribution), the latter multiplied by the BRDF.

public RGBSpectrum shadePath(Shading shading, HybridPathTracer tracer, int length) {
    cosinusSampler sampler = new cosinusSampler(1);
    Point sample = sampler.computeSample();

    Vector w = shading.getIntersectionPoint().getNormal();
    Random PRNG = new Random();
    Vector r = new Vector(PRNG.nextDouble(), PRNG.nextDouble(), PRNG.nextDouble()).normalize();
    Vector v = r.cross(w);
    Vector u = v.cross(w);
    Vector wi = u.scale(sample.x).add(v.scale(sample.y)).add(w.scale(sample.z));
    Ray ray = new Ray(shading.getIntersectionPoint().toPoint(shading.getRay()), wi);
    RGBSpectrum reflection = tracer.trace(ray, length + 1);
    RGBSpectrum brdf = shading.getColour().scale(getDiffuseReflection());
    return shade(shading).add(brdf.multiply(reflection));

When a ray reaches an emissive material, black is returned, unless the path length is equal to 1:

public RGBSpectrum shadePath(Shading shading, HybridPathTracer tracer, int length) {
    Ray ray = shading.getRay();
    Vector n = shading.getIntersectionPoint().getNormal();

    if (length == 1 && n.dot(ray.direction) < 0) {
        return getTexture().getColour();
    } else {
        return RGBSpectrum.BLACK;

I read Progressive Path Tracing with Explicit Light Sampling | Computer Graphics and thought that I was using the same idea in my code, but I must be doing something wrong, because my images are getting too bright when the maximal path length increases (see the examples for maximal length 2, 4 and 6 underneath). What is the problem?

Maximal length 2 Maximal length 4 Maximal length 6

Note: I know I should also consider some edge cases for specular surfaces, but maybe it's better to ignore those first and focus on matte objects. (I think I have prepared my code for specular surfaces anyway.) My shade function for direct illumination of matte objects should be fine, but here it is for reference:

public RGBSpectrum shade(Shading shading) {
    RGBSpectrum colour = shading.getColour();
    double L = 0;
    final Point intersection = shading.getIntersectionPoint().toPoint(shading.getRay());

    for (Light light : shading.getScene().getLights()) {
        double l = 0;

        for (Point sample : light.computeSamples()) {
            Vector wi = sample.subtract(intersection);
            double lengthSquared = wi.lengthSquared();
            wi = wi.normalize();
            double cos = shading.getIntersectionPoint().getNormal().dot(wi);

            if (cos > 0) {
                Ray shadowRay = new Ray(intersection, wi);

                if (!light.inShadow(shadowRay, shading.getScene())) {
                    if (light instanceof PointLight) {
                        double denom = 4 * Math.PI * lengthSquared;
                        l += light.getPower() * cos / denom;
                    } else if (light instanceof RectangularLight) {
                        Rectangle rectangle = ((RectangularLight) light).getRectangle();
                        double cosp = -wi.dot(rectangle.getNormal());
                        l += light.getPower() * cos * cosp * rectangle.getArea() / lengthSquared;

        L += l / light.getSamplesNumber();

    return colour.scale(getAmbientReflection() + L * getDiffuseReflection());

I noticed three potential problems. First, this bit of code looks suspicious:

Vector r = new Vector(PRNG.nextDouble(), PRNG.nextDouble(), PRNG.nextDouble()).normalize();
Vector v = r.cross(w);
Vector u = v.cross(w);
Vector wi = u.scale(sample.x).add(v.scale(sample.y)).add(w.scale(sample.z));

The vectors v and u are not normalized, so the construction of wi will be distorted. (Also, using a random vector r to construct the basis is really more work than necessary; you might take a look at this paper for a faster way of doing that.)

Second, you have a getAmbientReflection() in your direct lighting code—that should not be there for path tracing, where "ambient" light is properly calculated by the recursive trace.

Finally, I think the result of your direct illumination path ought to be scaled by $1/π$. This is because the Lambert BRDF is equal to $c_d/π$ where $c_d$ is the diffuse color. The direct illumination path needs to evaluate the full BRDF, including that $1/π$ factor, using the irradiance sample coming in from the light source.

However, the cosine-weighted recursive trace does not need a $1/π$, as it is cancelled out by the cosine PDF. That one should just use $c_d$, like you have it.

You can check that the $1/π$ factors are correct by checking that the average brightness is equal with and without next-event estimation (keeping the path length the same).

BTW, note that the image getting somewhat brighter as you increase the path length is to be expected, as longer paths = more bounces = more illumination. (Russian-roulette path termination fixes this.) Longer paths will also give stronger global illumination / "color-bleeding" effects. That seems to be what's happening in your images—look at the color of the shadow under the sphere; it gets progressively more influenced by the color of the walls and sphere as you increase the path length.

If this is undesirable, try reducing the diffuse reflection factors of all the materials in the scene; they might be unrealistically high, and cutting them down will reduce the bounce light and color-bleeding.


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.