Sample scene with different PBR materials. |
This time we will talk about something really interesting and useful - Physically Based Rendering. Actually, I am not going to explain everything about this topic, as it is huge and probably there is a ton of information that already exists on the web. However, I'd like to share my experience with adding PBR to my engine and struggles that I ran into during the process.
But what is PBR? If you only know that it makes things fancy and realistic in the application, I will briefly give you an explanation. By this time you should realize that one of the main goals in computer graphics is to simulate "proper" light behavior. Somehow we want our objects in the scene to interact with light sources like in the real world. We want to shade them, have reflections and do many other complicated things. And it so happened that J.Kajiya introduced a general rendering equation in 1986:
And since then people have been trying to mimic and solve it for all kinds of purposes. Of course, games became 3D much later, but they also needed lights. People have also found partial solutions and approximations of that equation for real-time applications (i.e Lambert diffuse), but they were not 100% accurate. Just look at that nasty indefinite integral over the hemisphere above! How the heck we should compute it in real-time? This task seems to be impossible...
But approximations... They are everywhere in this world... Developers and researchers did not stop on Lambert and other simplified shading models, so that's why in the early 2000s, PBR shading model was introduced. In a way, it is another solution to that equation which takes some physical properties into consideration: conservation of energy, microsurface scattering and etc. So it is more "physical" and thus more accurate and computationally complicated. And fortunately, it was popularized for the real-time applications by companies like Epic Games. If you are not familiar with the concepts of radiance, irradiance, flux, BRDF you can read about them here. By the way, I have used the Cook-Torrance BRDF model, as it is very popular in rendering engines. (Read about it here)
So as you can see we have diffuse and specular components. We can separate this integral in two integrals and solve them separately. Of course, for direct lighting (from direct sources) it is not a big of a deal: we just compute a Lambert diffuse term for the left integral and specular term with all tricky but straightforward formulas: D for GGX Distribution, F - Shlick-Fresnel and G for Smith's Geometry. ( So if things are already unclear, make sure to read the theory! ) In general, this is just some Maths that can be calculated in our shader. However, the results are not satisfying :(
Why? Well, we are calculating radiance for direct lighting. But look at our integral again. It's about all directions (w) over the hemisphere. Pretty unrealistic to compute, I agree... But it doesn't mean that we do not want to try! Let's dive into a more complicated part of PBR implementation - IBL or Image Based Lighting.
Probably you have already realized that we would work with environment maps / cubemaps. That's a nice and relatively cheap way to calculate environment diffuse light. It is called an irradiance map. Basically, it is a pre-convoluted cubemap - for every direction over the hemisphere we take a sample in the cubemap. Fortunately, you can generate an irradiance map with a third-party tool, such as CubeMapGen by AMD, or you can precompute it by yourself in the code.
Moving next, we should deal with a specular part. It consists of 2 textures: radiance and integration maps. The second one is just a lookup texture for computing the BRDF, so you can download it from the Internet. However, the first one must consist of several pre-filtered mip-maps. Your shader will calculate an indirect specular with something like split-sum approximation. That's why for different roughness values on your material you would use different mip-maps of your cubemap. The screenshot below should give you an intuition.
The mip-map generation process required some additional code for my engine so I could load my irradiance map into a PBR shader with all mips at once. For example, here is my IBLRadianceMap class:
And that is it! I will leave my final PBR shader/effect for you to explore. I understand that there may be many improvements, but for now, I am really satisfied with the results. As a result, I can load fancy 4K textures and get the realism out of them:)
Links:
1) https://learnopengl.com/PBR/Theory
2) https://blog.selfshadow.com/publications/s2013-shading-course/
TOREAD: "Physically Based Rendering: From Theory to Implementation" by G.Humphreys and M. Pharr
That's how our BRDF radiance equation looks now. D, F, G terms are explained in the link above. |
So as you can see we have diffuse and specular components. We can separate this integral in two integrals and solve them separately. Of course, for direct lighting (from direct sources) it is not a big of a deal: we just compute a Lambert diffuse term for the left integral and specular term with all tricky but straightforward formulas: D for GGX Distribution, F - Shlick-Fresnel and G for Smith's Geometry. ( So if things are already unclear, make sure to read the theory! ) In general, this is just some Maths that can be calculated in our shader. However, the results are not satisfying :(
Why? Well, we are calculating radiance for direct lighting. But look at our integral again. It's about all directions (w) over the hemisphere. Pretty unrealistic to compute, I agree... But it doesn't mean that we do not want to try! Let's dive into a more complicated part of PBR implementation - IBL or Image Based Lighting.
Probably you have already realized that we would work with environment maps / cubemaps. That's a nice and relatively cheap way to calculate environment diffuse light. It is called an irradiance map. Basically, it is a pre-convoluted cubemap - for every direction over the hemisphere we take a sample in the cubemap. Fortunately, you can generate an irradiance map with a third-party tool, such as CubeMapGen by AMD, or you can precompute it by yourself in the code.
Moving next, we should deal with a specular part. It consists of 2 textures: radiance and integration maps. The second one is just a lookup texture for computing the BRDF, so you can download it from the Internet. However, the first one must consist of several pre-filtered mip-maps. Your shader will calculate an indirect specular with something like split-sum approximation. That's why for different roughness values on your material you would use different mip-maps of your cubemap. The screenshot below should give you an intuition.
Image taken from https://polycount.com/discussion/139342/introducing-lys-open-beta |
The mip-map generation process required some additional code for my engine so I could load my irradiance map into a PBR shader with all mips at once. For example, here is my IBLRadianceMap class:
And that is it! I will leave my final PBR shader/effect for you to explore. I understand that there may be many improvements, but for now, I am really satisfied with the results. As a result, I can load fancy 4K textures and get the realism out of them:)
Links:
1) https://learnopengl.com/PBR/Theory
2) https://blog.selfshadow.com/publications/s2013-shading-course/
TOREAD: "Physically Based Rendering: From Theory to Implementation" by G.Humphreys and M. Pharr