Drawing transparent objects

We now have the necessary functions to compute the blending of colors with different parameters. The functions we implemented in the last part are included with all cases in the current rasterizer.

For brevity, we will only have a quick look at the full write_fragment method with blending included. The blending code is basically the same as the one from the last section. The main difference is that we support multiple output buffers.

/**
 * Writes a number of output colors and
 *  depth to the pipeline's framebuffer.
 * Might apply depth test/write and
 *  blending operations
 * @param {Pipeline} pipeline The pipeline to use
 * @param {AbstractMat} frag_coord The
 *  fragment coordinate
 * @param {Object<AbstractMat>} colors A map
 *  containing the colors per output buffer
 */
write_fragment(pipeline, frag_coord, colors) {
  const px = floor(subvec(frag_coord, 0, 2));

  const depth_options = pipeline.depth_options;
  const blend_options = pipeline.blend_options;

  const depth = pipeline.framebuffer.depth_buffer;
  // depth test
  if (depth_options.enable_depth_test &&
      !!depth &&
      frag_coord.at(2) 
      > depth.at(px.at(0), px.at(1)).at(0)) {
    return;
  }

  if (depth_options.enable_depth_write &&
      !!depth) {
    depth.set(
      v32.from([frag_coord.at(2)]),
       px.at(0), px.at(1)
    );
  }

  const frames = pipeline.framebuffer.color_buffers;

  for (let i in colors) {
    const frame = frames[i];
    if (!frame) {
      continue;
    }
    if (blend_options.enabled) {
      const blended_color = this.blend_colors(
          colors[i], 
          frame.at(px.at(0), px.at(1)),
          blend_options.constant_color,
          blend_options.source_function,
          blend_options.destination_function,
          blend_options.blend_equation);
      frame.set(blended_color, px.at(0), px.at(1));
    } else {
      frame.set(colors[i], px.at(0), px.at(1));
    }
  }
}

The last part to our basic transparency rendering is handling the drawing itself. This isn't part of the rasterizer itself, so we have to do it from the outside. Luckily, our rasterizer is able to do all the required operations (besides one).

As mentioned before, the way to draw transparent objects is to utilize the painter algorithm, that is we draw from the back to the front. Note There are other common methods, for example approximate Order independent transparency, but we won't cover that here.

Since we can look through transparent objects, we can't just use the depth buffer, since that would discard all objects behind the closest one. So the transparent objects will not write into the depth buffer.

But we also have opaque objects, which need the depth buffer. And these opaque objects may even occlude transparent ones. So what to do?

First, we draw all opaque objects as usual. This will result in an opaque image with a filled depth buffer. Then we disable writing to the depth buffer, but keeping the depth test activated. This makes it so, that occluded transparent objects stay occluded. After that we enable blending. Finally we draw the transparent objects from the back to the front.

How do we sort the transparent objects? The answer is: It depends. There are different ways to sort and all of them have some issues. In general, we won't ever be able to correctly sort objects without breaking them apart. 3 triangles can overlap each other in a circular fashion. Even two triangles can intersect each other, making a correct sorting impossible.

The simple solution, that works well enough in practice is: Just take the centers (or some other point) of each object and sort by how far those are to the camera. We won't even go down to the level of triangles.

How far to the camera can easily be determined by the zz-value in camera space. So we can compute the center of geometry for each object (the mean of its vertices), transform it by the model view matrix and then sort by zz. We just have to keep in mind, that the camera is looking in the negative zz direction, so the farther an object is away, the smaller ("more negative") zz will get.

Let us implement that! The floor plane and one cube are transparent now. Also, another transparent "glass" pane is put in the front, so you can really see the issue when running the unchanged exercise. Solution is below:

Exercise:

Solution:

See the solution

We can now draw transparent 3D objects together with opaque ones! The last issue that we cover may be hard to see. When we draw a single object, the order in which the triangles are drawing is dependent on the order in which they are specified in the vertex array. So we might actually get a similar effect as with different objects, but within one! Depending on how the triangles are sorted, this might display as weird and inconsistent color changes.

We could of course do the same with each triangle that we did with the objects, but this would be a lot of additional processing. Also, as mentioned before, intersections and overlaps will still be an issue.

So we can't really solve this issue easily. But we can do something to mitigate the issue, which might also accelerate the drawing process a bit: Culling!