Greetings, fellow readers of this webpage! Welcome to the 8th tutorial of my OpenGL4 tutorial series! This one is about multitexturing. It's december, year 2018, at the time of writing this article, and looking out of the window at snowy and Christmas time scenery here in Slovakia, I have decided to create a Christmas/Winter themed multitexturing tutorial . We will create a nice winter scene with snow, houses and a pavement that runs in between them, partially covered with snow. I hope that I have caught your attention and that you will keep reading!
Multitexturing is simply a technique to apply multiple textures on a object at once. In the previous tutorial, we have been using just a single texture for the rendering. But this time, we will actually use three textures to create an effect of a pavement. One texture is snow, second is pavement and third one will be path of the pavement itself. To explain it straightforward, just look at the following picture:
We will simply combine snow and pavement texture in a way depicted in the path texture . The idea is relativily simple, more difficult is to implement it. But don't worry, it's actually nothing insanely difficult. We just have to bind all three textures at once to three different texture units and then write shader program, that can apply the appropriate logic and combine snow and pavement.
If you remember from the previous tutorial, I have mentioned texture units and that you don't have to care now, that it will be always zero. Well, this time this will change. Texture unit is simply said a slot for one texture and one sampler - in different texture units, you can use different textures (images) and samplers. And because for the purposes of this tutorial we need three textures, we will need three texture units. It's that simple! Similarly, in fragment shaders, we will have to use three different samplers, one for each of the textures (or one for each of the texture units used, because each texture is bound to its texture unit). Let's just have a look at the fragment shader now:
#version 440 core
layout(location = 0) out vec4 outputColor;
smooth in vec3 ioVertexColor;
smooth in vec2 ioVertexTexCoord;
uniform sampler2D snowSampler;
uniform sampler2D pathSampler;
uniform sampler2D pavementSampler;
void main()
{
vec4 snowTexel = texture(snowSampler, ioVertexTexCoord);
vec4 pathTexel = texture(pathSampler, ioVertexTexCoord / 20.0f);
vec4 pavementTexel = texture(pavementSampler, ioVertexTexCoord);
float pathWeight = pathTexel.r;
float snowWeight = 1.0-pathStrength;
outputColor = pavementTexel*pathWeight + snowTexel*snowWeight;
}
As you can see, there are now three samplers - snowSampler, pathSampler and pavementSampler. Later in the code, we fetch the texel of each texture. You might notice division by 20.0f by the path texture. The reason is, that we are binding texture along the ground 20 times, but as for path, we simply want to apply path texture over whole ground just once. So we go from UV coordinates in range (0.0, 0.0) - (20.0, 20.0) to UV coordinates (0.0, 0.0) - (1.0, 1.0). When we have all texels, we can calculate the weights of each texture.
The weight of path texture - pathWeight - is red color of path texture. You might ask - why red? Path is anyway grayscale texture. So in this case, it does not even matter, if we take red, green or blue, our path texture is anyway stored as RGB PNG image. This could have been optimized, if we would save the path texture as 8-bit PNG (which is entirely possible, I was just too lazy to google it and mspaint does not support this ). Furthermore, weight of snow texture - snowWeight - is simply complement to the weight of path, so that's why it is 1.0-pathWeight. Having both weights, we now can combine colors of both snow texture and pavement texture using their respective weights and this will give us the desired result !
The shader code is now fine and explained, but how to set those samplers, so that they use the right textures? First of all, I will mention, that I have decided to go for the highest quality of textures - that means, I am just using bilinear filtering for magnification and trilinear filtering for minification. Knowing this, we can conclude, that we only have to create one single sampler and use it everywhere. Furthermore, we will now have two shader programs. One is for rendering the ground using the logic described above and another is for rendering houses, which don't have any kind of special logic, only mapping a single texture. Let's have a look at the code in the renderScene() function first:
void OpenGLWindow::renderScene()
{
// ...
groundProgram["matrices.projectionMatrix"] = getProjectionMatrix();
groundProgram["matrices.viewMatrix"] = camera.getViewMatrix();
// Render ground
groundProgram["matrices.modelMatrix"] = glm::mat4(1.0);
// Setup snow texture
snowTexture.bind(0);
mainSampler.bind(0);
groundProgram["snowSampler"] = 0;
// Setup path texture
pathTexture.bind(1);
mainSampler.bind(1);
groundProgram["pathSampler"] = 1;
// Setup pavement texture
pavementTexture.bind(2);
mainSampler.bind(2);
groundProgram["pavementSampler"] = 2;
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
// ...
}
You can see, that we are calling the bind methods of texture and samplers with an integer parameter. With this integer parameter, we simply say, what texture unit do we want to use this texture / sampler with. Afterwards, we have to set our sampler2D variables in the shader program as well with that exact integer value - which texture unit should the sampler2D take texels from. And that's it, nothing less, nothing more!
One question, that might come to your mind now is - how many texture units can I use at once? Is 10 too much? Can I even use hundred textures? Fortunately, there is a way to ask OpenGL about it, like: "Hey OpenGL, can you tell me, how many texture units does my graphics card support?" And the OpenGL tell you just that . For instance, my current GPU GeForce GTX 1070 Ti has 32 texture units, but so does GeForce on my (gaming) laptop, which is lot less powerful. So it might be, that 32 is standard amount for quite some time and I think it's more than enough.
Anyways, I have created a static function in the Texture class, that asks exactly this and it stores the result even, so that next time, the query does not happen again (because it also takes some time, caching the result is faster). Let's have a look at this function then:
int Texture::getNumTextureImageUnits()
{
static std::once_flag queryOnceFlag;
static int maxTextureUnits;
std::call_once(queryOnceFlag, []() {glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &maxTextureUnits); });
return maxTextureUnits;
}
Wait, what are those strange constructs there? What I want to achieve is, that this query for the number of texture units will be called exactly once and only first time somebody requests it. This std::once_flag thingy is a C++ standard object to ensure, that something gets called only once. Moreover, it ensures thread safety, so even if you would somehow call this from two different threads, it makes sure only one will perform this function. So we simply use this flag, to call this function only ONCE - first time somebody wants to find amount of the texture units of his hardware .
This is what has been achieved today:
I would say that the result is pretty impressive! It looks pretty realistic and by playing with that texture, you can control, how much snow is on the pavement! And in the end, the algorithm itself is really simple ! I really hope you have enjoyed this tutorial and because it's 18th of December 2018 at the time of finishing this tutorial (which is also my birthday BTW, the actual celebration is tomorrow ). I also wish you Merry Christmas full of joy and time with your close ones!
Download 3.67 MB (1176 downloads)