So far, I haven't used textures in my voxel engine. At least not, to texture voxels. With this post, and the linked video, that will change. Although, I don't plan on using textures that much. Or maybe I will... It depends. Anyway, how do we implement textures into a voxel renderer? And more importantly, how do we connect it with greedy meshing?
Using textures with normal meshes, that do not use greedy algorithms, is quite easy. Every voxel in our mesh has faces with the exact dimensions of 1 by 1. We can then use the normal of the face to determine the uv coordinate, or we could make a list of uvs for the faces. This would kinda look like this:
Using a predefined array:
This is self-explanatory, as you define an uv coordinate for every vertex position, before you generate the mesh. You then fetch this uv coordinate while generating the mesh.
vec2[ ] uvs = //predefined;
vec2 uv = uvs[currentVertexIndex];
Using the normal vector:
We can use the fact, that the normal vectors of our voxel faces can only face into six directions, meaning in the direction of the three coordinate axis and their respective reverse counterpart (in positive and negative direction). We then only have to check, which coordinate of our normal vector is not equal to 0, and we can then use the vertex offset from the voxel position, to determine the uv coordinate
vec3 normal;
vec2 uv;
vec3 offset;
if(normal.x != 0){
uv = new vec2(offset.y, offset.z);
}else if(normal.y != 0){
uv = new vec2(offset.x, offset.z);
}else if(normal.z != 0){
uv = new vec2(offset.x, offset.y);
}
This method can be tweaked to fit what we want.
Now we can generate uv coordinates for our voxel meshes. How do we get the shader to render it? We could use separate textures, that we could then choose using another index attribute that we could store in the mesh. This however would be very stupid, as we want a big variety of textures that all look different. If we bind every texture as a separate image, not only can we (in the case of OpenGL) only bind 32 textures, this would also be a nightmare to manage in the shadercode and application code. The system would, on top of that, not be very flexible if we want to add more textures.
To solve these problems, we can use a texture atlas to store every texture we need in one big image. A texture atlas is a collection of images, which all have in our case the same size. If we then want one specific texture, we then only have to know what index it has in the x- and y-axis of the image.
An example would be:
//retrieve the image that is the 3rd from the left and 4th from the
//bottom (column 3, row 4)
uv = new vec2( 3 * (textureSize.x / atlasSize.x),
4 * (textureSize.y / atlasSize.y));
Where atlasSize is the size of the whole atlas image, and textureSize is the size of one single texture.
If the atlas we are using consists of textures that resemble terrain, one can then see how we could use this, to texture our voxels
Two texture atlase used is another old game I made called "MagicGame"
Putting it all together, we can use what we did before to get the normalized uv coordinates for every voxel face and use it to get the texture we want on this particular face.
vec2 uvRaw = getUV(); // The uv coordinates we got using the normal or a predefined array
int column = 4;
int row = 3;
vec2 oneTextureSize = (textureSize.x / atlasSize.x);
vec2 uv= new vec2( column * oneTextureSize.x + oneTextureSize.x * uvRaw.x,
row * oneTextureSize.y + oneTextureSize.y * uvRaw.y);
Greedy meshing is a way of generating a voxel mesh by combining faces that have the same attributes and are touching each other. We can use this to reduce the number of vertices and triangles that the GPU has to draw each frame. This speeds the rendering process up and gives better frame rates, at the cost of calculation time that is used to generate the greedy mesh.
I will not go into detail with how greedy meshes are generated, as this article's topic is the texturing of these greedy meshes. In the future, I may write an article about how my greedy meshing algorithm works. If this article exists, it will be linked here:
Sorry, this article is not written yet :(
All we have to know right now, is that the greedy meshes will, for each vertex, output its respective normal, position, and other properties, while also returning the start position of the quad it is in. That way, we can calculate an offset from the vertices to the start of the quad, which will display the size of the quad we are currently on.
An example of a greedy mesh in comparison with a normal mesh.
Texturing a normal voxel mesh is working fine. If we want to texture a greedy mesh, we have to think a little bit harder. If we would just use the system to generate uv coordinates that we used previously, we would get messed up results. The texture would be stretched over the whole face, to fit its size. For the following examples, I will use textures from the old minecraft texture atlas. These textures are only for the examples and will not be included in the editor.
As you can see on the top image, this is most certainly not what we want. What we want is the bottom image, where the texture is tiled not stretched, to fit the size of the faces. We could just account for that, by using the size of the quad that we got from the greedy mesher directly. This would increase our uv coordinates so that the textures are not stretched. But we would then get another textured mesh that we do not want. This time, other textures we do not want are visible on the faces. And as you can see on the second image, this is also not our desired output.
Summarizing, we not only want our textures to tile to keep the size of the textures, but we also want the respective texture to repeat over the whole face. To achieve this, we have to use the mod operator. For everyone who does not know what the mod operator is and does, I will explain it really quick, or you can read the Wikipedia article about it.
Basically, mod gives you the number that remains, if you divide one number by another one. For example, if you divide 4 by 3, the result would be 1,33333. This result means, that the 3 can fit 1,3333 times into 4. Or, if we write it differently, 3 can fit 1 and 1/3 times into 4. And mod gives you the result, if you multiply 3 with the fraction 1/3, which is 1. In other words, if the first number is less than the second number, it will give you back the first number. If the first number however is bigger than the first number, it will wrap around. This means, the result will never be greater than the second number.
This diagram is a graphical Representation of the mod operator in action. As you can see, all values above 5 wrap around to 5. We can use that to our advantage for the uv coordinates.
The first thing we have to do, to make the repeating tiles work, is to define a base uv coordinate for each vertex. This will serve an the, well... base of the texture in the atlas, that we want to sample from. We can do this, by using the same row-column method we did above:
vec2 oneTextureSize = (textureSize.x / atlasSize.x);
vec2 uvBase = new vec2(column * oneTextureSize.x, row * oneTextureSize.y);
This base is then stored as a two-dimensional vector for every vertex. Secondly, we have to tell the shader, how many times we want the texture to be sampled on the quad. That can be achieved by using the size of the greedy quads that we got from the meshing algorithm, and combining it with the method of getting the normalized uv coordinates from the normal vector. After that, we only have to multiply it by the size of one texture to get the amount of textures we want.
vec2 quadSize;
vec3 normal;
vec2 uvOffset;
if(normal.x != 0){
uvOffset = new vec2(quadSize.y * oneTextureSize.x , quadSize.z * oneTextureSize.y );
}else if(normal.y != 0){
uvOffset = new vec2(quadSize.x * oneTextureSize.x , quadSize.z * oneTextureSize.y );
}else if(normal.z != 0){
uvOffset = new vec2(quadSize.x * oneTextureSize.x , quadSize.y * oneTextureSize.y );
}
The uvOffset can then be stored together with the uvBase in a four-dimensional vector. If we want to sample the texture in the shader, using the uv coordinates we just computed, we just have to add the base and the offset together, and we will have the normal uv coordinates that we usually use again. Only this time, the textures are not stretched. Although they are not repeating yet (we have the second image again).
And this is where our mod operator comes into play. We can just take the mod of the uvOffset and the size of one texture in the fragment shader, to wrap around the size of one texture:
vec2 uvWrap= new vec2( mod( uvOffset.x, oneTextureSize.x), mod( uvOffset.y, oneTextureSize.y));
vec2 uvFinal= uvBase + uvWrap;
Remember, we have to do this operation in the fragment shader, as we want to wrap every pixel (fragment) and not every vertex. If we would wrap the uv coordinates in the vertex shader, we would get the stretched texture again. If we then use the uvFinal as uv coordinate in texture(atlas, uv) we will get the results shown in the image on the right and in my YouTube video which is shown below.
Streched uv coordinates for the greedy mesh.
Tiled uv coordinates for the freedy mesh.
Correct uv coordinates for the greedy mesh
I hope, this explanation is useful to some of you, and I could help with your projects. If you have any questions, you can ask me in the comments on my YouTube video, or you can use the Google form below.
All graphical illustrations were made using Blender.