I am trying to implement for research purposes a path tracer, but so far but results are not so good and I will explain you why. The general idea before getting to the code:I am working on paths that are generated before sampling them. I mean that the first step of my algorithm consists in calculating a path for a certain pixel (x,y). This path will perform some bounces within the scene and, if terminates on the light, will be considered valid, which means that I can calculate its contribution for the pixel (x,y) which otherwise will be black.But first, my aim is to reach an acceptable image with a few number of SPP (around 16-32 spp). This is because I am following the code of a target framework, easy implemented, that manages to reach such good results in few steps (explained later). The following is the  target image that I want to reach, produced by the target framework:It's rendered with 16SPP. The core code behind it is quite straightforward. It just implements the rendering equation and it is shown and commented below:Vec Sample(vec3 O, vec3 D, int depth)//oriding, direction, depth of the recursive step{  vec3 color(0, 0, 0);  float t;  if (checkIfIntersectSomething(D, t))  {    vec3 I = O + t * D; //get to the intersection point on the object    vec3 diffuse = getMaterial(I);    vec3 L = vec3(-1 + Rand(2.0f), 20, 9 + Rand(2.0f)) - I; //(-1,20,9) is Hard-code of the light position, and I add Rand(2.0f) on X and Z axis    //so that I have an area light instead of a point light    L = normalize(L);    float ndotl = dot(I.getNormal(), L); //the closer the dotProdutc is to 1.0, the more the light and surface face each other    if (ndotl > 0)    {        if (!checkRayLightIntersection(L)) {            float dist = distFromLight(I);            color += diffuse * ndotl * vec3(1000.0f, 1000.0f, 850.0f) * (1.0f / (dist * dist));        }    }    // russian roulette    float Psurvival = CLAMP((diffuse.r + diffuse.g + diffuse.b) * 0.33333f, 0.2f, 0.8f);    // continue random walk     float rand = Rand(1.0f);    if (depth < 10 && rand < Psurvival)    {        //Besides russian roulette, I also do another weight, because rays that go towards the horizon will bring back very little energy        //so I make a random distribution that favours those rays who are close to the normal of the hit point, this is DiffuseReflectionCosineWeighted(). It creates a Random bounce but proportional to N dot R        vec3 R = DiffuseReflectionCosineWeighted(I.getNormal());//there is a weight        float prob = 1.0;        float cosTheta = fabs(dot(I.getNormal(), R));        if (cosTheta > 1e-6) prob = cosTheta / M_PI;        color += diffuse * Sample(I + R * EPSILON, R, depth + 1) * (1.0f / Psurvival); //the cosTheta() of the attenuation of the rendering equation gets simplified with the cosTheta of the prob        //the PI of the prob gets simplified with the BRDF where we are using the ideal BRDF = diffuse/PI    }  }return color;}The DiffuseReflectionCosineWeighted implies that we have to apply a PDF to our recursive step. This PDF is cosTheta/PI and we have to divide for it in:color += diffuse * Sample(I + R * EPSILON, R, depth + 1) * (1.0f / Psurvival);You can't see it in the code because it gets simplified witht the cosTheta attenuation term of the rendering equation and PI of the diffuse BRDF where my BRDF is indeed diffuse/PI because we want to consider only DIFFUSE objects.(For any question about the code do not hesitate to ask.)I tried to emulate this code with the difference that I don't calculate the path at the moment I want to calculate the color of a pixel (like the target framework does above), but I calculate a path beforehand:My generatePath() method indeed tracks the path into the scene and checks all the vertices it hits. The checkIfRayIntersectSomething(t) method you will see used, it's just a pseudo method implemented in my framework and that I omit posting cause of its length. I use it to check if my ray hits something in the scene, if it does, it update the t with the distance to that object. NOTE: the light is not considered an object itself. Hence, I also have a checkRayLightIntersection(hitLightPoint) which checks the intersection with the light, if there is any, the hitLightPoint is updated with the point on the light I have been hitting.The light is a 2D surface of area 2x2 placed at the same position (-1,20,9), as the target framework does.First the definition of a couple of struct to store some info:struct PathVert {   vec3 p; vec3 n; //hit point and normal of the surface hit };struct Path {   PathVert verts[MAX_DEPTH]; //maxDepth is 15 for now   int vertCount;   int x, y; //which pixel this path is referring to};And now the main code:bool GeneratePath(int x, int y, Path &path){   path.x = x;   path.y = y;   path.vertCount = 0;   vec3 P = renderData.p1 + renderData.dx * ((float)(x) + Rand(1)) +  renderData.dy * ((float)(y) + Rand(1));   vec3 O = renderData.E + vec3(Rand(0.4f) - 0.2f, Rand(0.4f) - 0.2f, Rand(0.4f) - 0.2f);   vec3 D = normalize(P - O); //direction of the first ray, the one from the camera towards the pixel we are considering   for (int depth = 1; depth <= MAXDEPTH; depth++){    float t;    Vec hitLightPoint;    PathVert vert;    if (!checkIfRayIntersectSomething(t)){        //we didn't find any object.. but we still may have found the light which is an object non represented in the scene        //the depth check avoids me rendering the light as a white plane        if (depth > 1 && checkRayLightIntersection(O, D, hitLightPoint)){            //update the vertex since we realized it's the light            vert.p = hitLightPoint;            vert.n = Vec(0, -1, 0);//cause the light is pointing down            path.verts[depth - 1] = vert;            path.vertCount++;            return true; //light hit, path completed            }        return false; //nothing hit, path non valid    }    //otherwise I got a hit into the scene    vert.p = O + D * t; //reach the hitPoint    vert.n = methodToFindTheNormal();    vert.color = CalculateColor(vert.p); //according to the material properties (only diffuse objects so far)    path.verts[depth - 1] = vert;    path.vertCount++;    //since I have the light, and a path terminates when it hits the light, I have to check out also if my ray hits this light,    //and if does, I have to check whether it first hits the light or the object just calculated above    //moreover with the depth > 1 check, I avoid again rendering the light which otherwise would be visible as a white plane    if (depth > 1 && checkRayLightIntersection(O, D, hitLightPoint)){        float distFromObj = length(vert.p - O);        float distFromLight = length(hitLightPoint - O);        if (distFromLight < distFromObj){            //update the vertex since we realized it's the light            vert.p = hitLightPoint;            vert.n = Vec(0, -1, 0);            vert.color = Vec(1, 1, 1);// TODO light color? or light emission?            path.verts[depth - 1] = vert;            return true; //light hit, path completed        }    }    if (depth == MAXDEPTH) return false;       Vec newDir = BSDFDiffuseReflectionCosineWeighted(vert.n, D);//explained later       D = newDir;       O = vert.p;   }   return false;}The BSDFDiffuseReflectionCosineWeighted() just calculates the new direction like in the target framework, tested and working. What remains last is the Sample method which calculates the final color of the pixel using the path calculated right above:Vec Sampling(Path &path){   Vec color(1, 1, 1);   for (int vert = 0; vert < path.vertCount - 1; vert++) { //considers the last vertex as the light      const PathVert &currVert = path.verts[vert];      const PathVert &nextVert = path.verts[vert + 1];      Vec wo = (nextVert.p - currVert.p).norm();      double cosTheta = fabs(wo.dot(currVert.n));      float PDF = cosTheta/PI;      if (cosTheta <= 1e-6) return Vec();      //considering only DIFFUSE objects      color = color.mult(currVert.color * (cosTheta / M_PI) / PDF);   }   return color.mult(Vec(10.0f, 10.0f, 10.0f)); //multiplication for the light emission?}The problem is that the image is really dark, you can see how small the light is, but somehow in the target framework it works correctly while in mine:This is because not many rays reach the light, so many paths are not considered valid. If instead I make my light broad (20x20) results are better but I lose the shadows:I am sure that less rays reach the light cause I have been calculating them in both cases: small area light and big area light. I also tried to compensate the dark image by increasing the light contribution when reached by the path, but it brings to white pixels (for paths that reach the light) and the black ones (cause of invalid path) keep remaining black, so not a good approach. It seems that (as far as my understanding arrives) the target framework applies direct lighting, that's why such a good image BUT according to theory direct lighting only decrease variance, reducing the noise.. which means that using or not direct lighting shouldn't change the resulting image so much. Is there a way according to you to fix this problem? Thanks in advance.
How to compensate low amount of rays reaching the light in a Path Tracer
c++;lighting;shadow;pathtracing;global illumination