Hello coders,
So far up until now, everything we have rendered has been a solid and completely opaque object.
This means that none of the incoming light is allowed to pass through.
This corresponds to the alpha component for every fragment being equal to 1.
Well we do have one exception, and that’s the billboard we use for the point light objects.
In the case a fragment falls outside the light’s radius, we discard it.
This is mostly equivalent to having an alpha value of zero.
But what about all the other possible values in between 0 and 1.
Conceptually the alpha component says what percentage of the color to contribute to the
final image.
So for example, if we wanted to add a blue tinted window to the scene that lets 3/4 of
the light behind it pass through, then it should be as simple as rendering a quad with
its alpha component set to one quarter.
The final fragment color should be 25% the color of the window, plus 1 minus 25% the
color of whatever is behind the window.
The graphics pipeline also already provides a fixed function step to do this for us.
If you recall way back to tutorial 4, the final stage of the graphics pipeline is the
color blending stage.
This occurs automatically following the fragment shader, and is even configurable.
The color blending stage can perform two types of operations.
It can mix the previous and incoming color to produce a new mixed value,
Or it can combine the previous and incoming value using a bitwise operation.
So far this should all sound very straightforward, and you may be wondering why we just didn’t
enable color blending from the start.
And that’s because there is one pretty significant gotcha when working with transparency, the
order in which we render semi-transparent objects actually matters!
The reason for this can be a bit counter-intuitive, especially if you’ve spent any time using
art editing software where an image is broken up into a sequence of layers.
When working with layers, partial transparency isn’t an issue because we know the alpha
component for each pixel at each layer, and there is a well defined ordering of layers
from front to back.
But this is the wrong mental model to have for our pipeline based rendering.
Lets jump back to tutorial 4 for a quick review.
So how does this relate to semi-transparent objects?
Well the depth buffer can only store one depth value per pixel and doesn’t care about how
transparent an object is.
If the window is rendered before the vase object behind it, then the depth buffer will
be storing the window’s depth which is closer to the camera, therefore the vase fragments
get discarded.
One simple solution is to first render all the solid objects in the scene, followed by
every semi-transparent object.
However even this solution isn’t perfect and breaks down when multiple semi-transparent
objects overlap.
You could work around this by first sorting each semi-transparent object by its distance
from the camera, and then rendering in the order from back to front.
But what about if the camera moves, or a model moves, sorting every object by distance every
frame can require a lot of computation.
And even that will break down in certain scenarios.
It’s possible to overlap 3 triangles such that from the perspective of the camera, properly
ordering them becomes impossible and we’d need to order per fragment.
So unfortunately, there isn’t an easy solution that handles every case.
There do exist a class of techniques known as order independent transparency that solve
this complicated problem, but all of which have prerequisite topics we would need to
cover first.
So I guess that’s it, thank you for watching and?
Well not quite.
I would still like to demonstrate blending.
And if the only semi-transparent objects in the scene are the point lights, then this
is a simple case we can make work.
Point lights are billboards and always face the camera, therefore we avoid the case where
triangles can overlap, and we do not need to sort individual vertices, the only thing
that will affect the ordering is the position of the point light.
Ok so let’s get to coding.
First we will need to render all the solid objects in the scene, so in the first app
implementation file’s run function make sure your simple render system’s render
game objects function is being called before the point light systems render function.
Typically when rendering any semi-transparent objects you will want to first render all
your solid objects, then render any semi-transparent objects.
Next we’re going to head over to the pipeline header to add a helper function to enable
alpha blending.
First though, if you haven’t yet corrected this yourself, I have a mistake in the pipeline
config info struct.
We should be setting the constructor function equal to the default.
This is because whenever you edit any of the copy or move constructors you need to explicitly
default or implement the constructor.
Not doing so can cause errors when using other compilers.
Now, just below the default pipeline config info we will add a second static void function
called enable alpha blending that will also take a pipeline config info reference called
pipeline.
This will be a helper function to easily enable alpha blending for a pipeline config.
Then at the bottom of our pipeline implementation file we can add the implementation for this.
We already have an example setting the necessary fields for the color blend attachment in the
default function above, so scroll up and copy that chunk of code, and paste it into your
enable function.
To enable alpha blending we need to change a few of the fields here.
First we set the blend enable field to vk true.
You may be wondering why we don’t have this field always set to true for all pipelines
and that's because there is a performance cost to having it enabled, we need to perform
the blending operations.
The color write mask can remain unchanged, since we want to write the r, g, b and a components.
The next 6 fields correspond to variables in this equation, and determine how exactly
rgb values and alpha values are combined.
The src is the current value being output from the fragment shader, in our case we call
it the outColor.
The destination is whatever values exist at that fragment in our color attachment.
So before we’ve rendered anything, this will be equal to whatever we’ve set the
clear value of our render pass.
So in our case, when writing a color we want to use its alpha component as the src blend
factor, and one minus the src alpha as how much the existing value in the color attachment
contributes.
As long as we render solid objects first, and then semi-transparent objects from farthest
to closest, this method will work fine.
The color blend op should be vk blend op add since we want to add these values together.
And the last 3 fields for alpha blending don’t actually matter for our purposes here, since
we don’t check what the destination alpha is at any point.
So I’ve left the alpha blend fields just with their current values, you may want to
change these in the future though depending on what you’re doing.
Ok now moving on, lets enable alpha blending for just the point light system.
Head over to its implementation, and in its create pipeline function after we’ve set
the pipeline config to the default we can then enable blending using the helper function
we just created.
The last thing to do before running our code is to make use of the alpha component in our
point light fragment shader in a non-trivial way.
Currently we always output the value of one, but let's make it so that the alpha component
approaches 0 as the distance from the center of the point light approaches 1.
The cosine function can be a nice function for this since it’s continuous with zero
slope at its max and minimum.
We’ll need the pi constant to properly map the domain, so I’ll define a const float
pi variable as 3.1415926538 which is more than enough significant digits.
Next I’ll set the alpha value to use 0.5 times cos of my distance times pi plus one
in brackets with the cosine function.
This will have the effect of transforming the cosine function to this.
If we build and run, everything is looking pretty good but there's actually a small problem,
which to demonstrate I’ll need to first disable the rotation of my lights.
If we build and run again, when the red light overlaps the white light, this looks fine,
but if we flip our orientation we can see a clear problem when the white light overlaps
the red light.
This is because as I mentioned at the start of the video, ordering matters.
In this case the white light is being rendered first.
But when the red light is rendered, the depth buffer signals to the red fragments that they
are behind these semi-transparent fragments of the white light, so they get discarded.
I know what you may be thinking, that if we just change how the blending operations are
being performed we should be able to make things work, right?
But no, changing the blend operations just result in other problems and the only real
solutions are to sort all semi-transparent objects and render from back to front, or
to use an order-independent rendering technique which isn’t something we can do with the
vulkan topics I’ve covered so far.
So we’re going to do the former, we’ll make sure that the draw call for each light
is ordered by the light’s distance from the camera.
To start, we’ll add a helper function to the camera header to get its current position.
This can be easily accomplished now that we also are storing the inverse view matrix.
The first 3 values of the last column of the inverse view matrix correspond to the camera’s
position in world space.
We cast the last column to a vec3 in order to remove the 4th component.
Next in the point light system at the top of the file I’ll import the map header.
We’re going to use a map as a quick and simple way to sort our point light objects.
So below in the render function as an initial step we’ll sort the lights.
Create a map object with a float as the maps key and a game object id as the value.
The float key here will be the distance of the point light.
We iterate through each point using the same method as below.
Going through every game object and filtering out any game object that does not have the
point light component.
To calculate the distance we first get the camera's position and subtract the position
of the point light.
Then by using the dot product with itself we get the length of the offset vector.
Well technically we get the length squared, but that's good enough for our purposes since
we only care about the ordering and computing the square root would only add unnecessary
calculations.
We then add the game objects id to our sorted map.
Now below, instead of iterating through every game object, we will iterate through the sorted
map in reverse order, since we want to render the game objects from back to front.
To do so we’ll use a c++ iterator, going from r begin to r end.
We fetch the actual game object by using the object id from the sorted map to index into
the unordered map of all game objects.
Game objects are not copyable, that’s why we must do it this way using the game object
ids, rather than copying the actual game object to the sorted map.
And finally we can remove this filtering step as we know already that we only have point
lights at this point.
And that’s it, we can build and run and now regardless of the camera’s orientation,
the lights will be rendered in the correct order, so we will no longer see that artifact
when the white light overlaps the red.
This wraps up the point light series of videos and we’ll be moving on to textures next
as I know that's a topic many of you have been waiting for.
Feel free to experiment with how exactly you render the point light objects.
One thing we haven’t touched is adding some color variation.
If we pull out our cosine function, we can also make use of it when determining each
fragment's color.
One effect I particularly like is simply adding the cosine term to the lights rgb color, which
will make the center of each light white, and gradually transition to the actual lights
colors only near the edges.
Share any equations you come up with in the comments below or on our discord, I’d love
to see what you guys can come up with.
Maybe I could do something like a showcase in a future video.
Anyway thank you for watching and keep on coding!