So one thing I would like to add is some visual indication for the light object.
There are a variety of ways we could do this.
We could render a spherical model using the pipeline we currently have.
Or More advanced methods might render light sources using some sort of post-processing
effect.
However I would like to take this opportunity to add a second rendering system to the engine,
and to introduce a topic known as billboards.
A billboard is a flat 2D object embedded in the 3D world, but has the additional property
that it is always facing the camera.
No matter how the camera moves or rotates, the billboard will always face the camera
to some extent.
Show billboard not rotating, and then a billboard rotating, also screen shot the alternative
solution
It’s like if someones watching you, and no matter where you move or look, they turn
to face you.
Billboards can be used for a wide range of things, such as placing health bars above
the heads of enemies, or for representing objects that don’t have a physical opaque
surface, things such as clouds, grass, smoke or particles.
There are a few variations on how exactly a billboard can work based on how it aligns
itself to the view.
Screen aligned billboards align themselves to the camera’s viewing plane.
This means that the billboard's rotation is dependent on the camera’s rotation but not
it’s positioning.
An alternative method is viewpoint oriented billboards.
With this style of billboard it will always orient itself to directly face the camera
and therefore is affected by both the rotation and position of the camera.
There are even more options than just the two I mention here, axis aligned billboards
are commonly used for displaying trees, or for example variations displaying a fixed
size regardless of distance.
Definitely check out the additional resources provided for a more complete description of
this topic.
For this tutorial we will be using screen aligned billboards as they are relatively
simple to implement and work well for small symmetrical objects like point lights.
The billboard will be pinned to a single point in 3D world space, and will have a width and
height.
Each vertex position of the billboard will be calculated in a vertex shader.
This is accomplished by first starting at the center point, and then displacing each
vertex by the camera’s up and right vector.
This will create a flat object made up of two triangles, always aligned to face the
camera.
Ok let’s get to coding.
So to render a billboard we need to be able to extract the camera up and right vector
in world space within the vertex shader.
Currently we have our projection and view matrix combined, so in the app implementation
I’m going to separate these matrices.
And then below in the render loop, set the ubo projection field to just the camera projection,
and the ubo view to the camera view.
Now because we’ve changed the format of our uniform buffer, we need to update the
definitions in the simple shader files to match.
In the vertex shader I’ll break the projection view matrix into projection and view fields,
and will also change the calculation for the gl position value to use these separate matrices.
Don’t forget here that matrix multiplication order matters, so you must have a projection
times view and not the other way around.
This is probably the last time I’ll mention this.
I’ll copy the updated ubo structure definition, and navigate to my fragment shader to also
update the definition there.
So we haven’t changed any of the program behaviour, but as a quick sanity check let's
make sure that everything is still working as before.
Build and run your program, making sure to recompile your shaders, but by now that should
really be an automatic build step.
Ok so everythings still working.
Now to render the light source we’re going to create a second independent rendering system!
Within my project src directory I’m going to create a new folder called systems.
The build system we created in the previous tutorial should automatically detect any src
code even within subdirectories.
This will make structuring the project easy.
As the tutorials continue I’ll reorganize the project bit by bit.
For now though I’ll just move the simple render system to the systems folder, and then
duplicate these files to act as a starting point for the point light system.
After renaming the two files, navigate to the point light system header and rename all
occurrences of the class to PointLightSystem.
I’m also going to change the renderGameObjects function to just be called render as I think
this makes a bit more sense in this context.
For some reason,I had to close my vscode project and re-open to get the detection for my include
paths to be properly picked up by intellisense.
I’m not sure as to why that was exactly, probably something to do with creating the
new folder, so..
Just you may need to do that as well.
Ok now in the point light implementation, I need to update my initial include statement.
We can remove the push constant struct definition here as that’s only relevant to our simple
render system, and then replace each occurrence of simple render system with the new classes
name.
The constructor and destructor are both good as is.
I’m going to just comment out the code for the push constant range.
We don’t have one yet, but in an upcoming tutorial we will be adding this back in.
So in the pipeline layout info, let’s also update the pushcontant fields to use a count
of 0 and a nullptr for the ranges.
In the create pipeline function, we will be using new shader files specific to this system.
So update the shader path files to use point light dot vert and dot frag.
The render game objects function can be renamed to just render, and then we’re still going
to bind the systems pipeline and descriptor sets just as before, but all of this code
for drawing model objects can be removed.
We’re going to use a similar trick to what we had when drawing your first triangle.
To draw the light, we can use a single vk command draw that will draw 6 vertices.
The billboard will be a square object made up of two triangles, so 3 vertices for the
first triangle and 3 more for triangle number 2.
The vertex positions will be hardcoded into the point light vertex file, as I’ll demonstrate
in just a moment.
In your shaders directory create new files for point light dot vert and point light dot
frag.
Starting with the vertex file, we specify the glsl version being used as version 450,
and then I have provided a paste bin link with the vertex position offsets that can
be copied into here.
These values will be used to create the billboard object by offsetting each vertex position
from the point lights location, in the x and y camera space directions.
Next I’m going to declare a vertex shader output value at location 0, so we can access
the linearly interpolated offset within the fragment shader.
Following that we have the same ubo struct definition as used by the simple render system
shaders.
For the engine, I’m structuring things so that the global ubo object will be the same
for every shader we create.
Where inputs can start to vary between shaders will be by adding additional shader specific
descriptor sets.
Then I’m going to declare a constant value specifying the size of the billboard object
called Light_radius.
We can make this a push constant in a future tutorial, but for now we’ll stick with just
using a fixed value.
Finally we need to write the main function.
To start we set the fragOffset output value.
We’re going to use the same trick as we did in the early tutorials by using the special
variable gl vertex index which holds the index for the vertex currently being processed.
Then we need to extract the camera up and right vectors from the view matrix, these
will be in world space.
The 3 elements of the first row of the view matrix make up the camera right vector, and
the first 3 elements of the second row make up the camera up vector.
Using these we can calculate the vertex position in world space.
We start at the light position which is already in world space, and then apply the x and y
offsets by the camera vectors.
And as the last step we compute the final gl position by applying the projection and
view matrices to the position in world space.
Before moving on there are a couple things I’d like to point out.
If you are concerned about the extra matrix multiplication we now need to perform, a possible
optimization is to also pass in the pre-computed projection view matrix in the global ubo.
But I don’t think this would at least currently make any measurable performance difference.
Next there’s an alternative method for computing the final position by applying the offsets
to the position in camera space, rather than doing the initial computation using the position
in world space.
It might be a good exercise to try and come up with that yourself.
I really can’t emphasize enough how beneficial it is for anyone working on computer graphics
to gain a really good understanding of these transformations and sequence of spaces.
I’ll show the other possible solution after we get things compiling.
Ok, the fragment shader implementation is really easy.
We specify the version of glsl.
Then we need to declare the fragment shader inputs, so at location 0 we have the fragOffset
value.
The output is the same as the simple shaders, with a vec4 outcolor at location 0.
Then we have the same global ubo struct so copy and paste that in, and finally our main
function is as simple as it gets, for now we’ll just set the outColor to equal a vec4
with our light color and an opacity of 1
Ok almost finished, in the first app implementation I need to update the include statement to
look into the systems directory, and I’ll also include the point light system.
Then below where we have the simple render system initialization I will create a point
light system.
And in the render loop I’ll call point light system dot render, passing in the frame’s
information.
And that’s it, we can build and run, and now if you zoom a little back you should be
able to see the point light object being rendered.
Note how as the camera rotates, the billboard always rotates to face the camera directly
on.
I think a circular light would look a lot better, and one way to do this easily is to
discard any pixels that are outside the point light's radius.
We can do this by using the fragOffset input in the fragment shader.
We first calculate the distance the fragment is offset from the lights position.
Note that based on how we set up our offsets, the fragOffset x and y values will always
be in the range -1 to 1, no matter how large the billboard is made.
So if the distance is greater than 1, we discard.
Discard is a special word that can only be used within fragment shaders.
It tells the pipeline to throw away this fragment and return, this means the outcolor will never
be set for this fragment.
If you run your code again the light should now appear spherical.
We can make the light object larger or smaller by changing the value of the light radius
constant in the vertex shader.
Now would also be a good time to try calculating the vertex positions using the alternative
method I mentioned before.
And for those interested, here’s my solution.
Ok this tutorials already a bit longer than I would have liked but there’s one final
thing I’d like to do, and that’s to fix these validation layer messages.
We’re getting these warnings because the point light pipeline expects there to be the
same vertex attributes that we use in the simple shader to be present.
Any of the shader programs we create that require model objects will want to use these
values, but that isn’t really the case for the point light system.
So before finishing today, I would like to make these values optional.
In the pipeline header file, I will adjust the pipeline config info struct to contain
vector fields for both the binding descriptions and attribute descriptions.
Then when a pipeline is created, rather than fetching these values statically from the
model class, we can use the values provided by the config info.
I’ll also mark these variables as references, this way we avoid causing an unnecessary vector
copy operation.
In the default pipeline config info I will set the default values for the binding and
attribute descriptions to use the static values fetched from the model class.
This way the simple render system will continue to work without any additional changes, and
using the model's values is what we will likely want in most cases, so I think they make sense
as the default value.
And finally, for the point light system we can tell it to not use these descriptions
since it is a special case.
So in the implementation of point light create pipeline, call the clear function on the pipeline
configs attribute description \\ and binding description vectors.
If you run your code again the validation errors should now be gone!
And we’re finished, stay tuned for the final few tutorials on point lights which will be
coming out soon.
Thank you for watching and keep on coding.
Cheers!