Chapter 2-Texture Loading?

One cannot simply make a game with flat, white triangles... Unless you can, in which case send me a link

Fall in, everyone! Today we're going to be learning about writing a texture loader for our game.

We all know what textures are. A Texture is a 2D image, be it a .png, .gif, or .bmp. They're what provide color and detail to the Godly Pointy Points that make up a scene in OpenGL. Hooah?

Before we get started with shaders, I'm going to show how to make a basic .png texture loader. Ordinarily, most OpenGL tutorials show how to load textures through external APIs such as Slick2D, STB, DevIL, or what have you. However, the caveat of this is by using an external API you don't fully understand HOW the texture loading process works.

So let's get started. Right now we'll be creating a class in our tutorials.render package named Texture. For this class, we'll need to import the following:

Texture.java
import java.io.File;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.util.HashMap;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL30;
import org.lwjgl.stb.STBImage;
import org.lwjgl.system.MemoryStack;

Going into the class, we'll first start with making a static Hashmap for containing all of our Texture names and IDs.

Texture.java
private static HashMap<String, Integer> idMap = new HashMap<String, Integer>();

Now, the way OpenGL, and as a result LWJGL, load textures is by storing the texture data in the GPU and assigning it an ID. When the texture is loaded like this, OpenGL doesn't store any information about it beyond the dimensions and RGBA values. In order to let us load the texture with the file name, we use this HashMap in order to store the name and location of the texture in the game's files, and use that as a key to get the relevant ID. Nifty, huh?

For this class, we'll be including a static method called loadTexture, with an input of String texture.

First thing's first, we'll need to create a try ... catch block in the case of the texture file not being found.

Texture.java
public static int loadTexture(String texture){
    try (MemoryStack stack = MemoryStack.stackPush()){
    
    }catch(Exception e){
        e.printStackTrace();
    }
}

What this does is, in the event that a texture file can't be loaded, the terminal returns a warning saying which one it is.

Now, we'll need to check and see if the resource is already loaded. This prevents situations where a texture gets loaded multiple times, and is totally not something I had to learn the hard way when I first started using this method. Honest.

Texture.java
if(idMap.contains(texture)){
    return idMap.get("res/"+resourceName);
}

If you've used HashMaps, you'll know that a Value can be stored by multiple Keys, but you can't have duplicate Keys. This has two purposes of 1.) Preventing issues due to the same Key being added twice, and 2.) Save time and memory by not storing the same texture more than once.

If you don't include this if statement, demons will literally fly out of your computer and eat your soul when the same Key is entered twice.

Now, let's get to loading some cool textures! After that if statement, we can begin the texture loading process:

Texture.java
int width;
int height;
ByteBuffer buffer;
try (MemoryStack stack = MemoryStack.stackPush()){
	IntBuffer w = stack.mallocInt(1);
	IntBuffer h = stack.mallocInt(1);
	IntBuffer channels = stack.mallocInt(1);
		  
	URL url = Texture.class.getResource("res/"+resourceName);
	File file = new File("res/"+resourceName);
	String filePath = file.getAbsolutePath();
	buffer = STBImage.stbi_load(filePath, w, h, channels, 4);
	if(buffer ==null) {
		  throw new Exception("Can't load file "+resourceName+" "+STBImage.stbi_failure_reason());
	}
	width = w.get();
	height = h.get();
		  
	int id = GL11.glGenTextures();
	idMap.put(resourceName, id);
	GL11.glBindTexture(GL11.GL_TEXTURE_2D, id);
	GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
		  
	GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, width, height, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buffer);

	GL30.glGenerateMipmap(GL11.GL_TEXTURE_2D);
	STBImage.stbi_image_free(buffer);
	return id;
} catch(Exception e) {
	e.printStackTrace();
}
return 0;

Unfortunately, this method operates on the STB library. I'm not too sure how the library works, so I can't give much explanation on the options. I will update this as I learn more about it.

Now we're done with the Texture class, but we can't really do anything with it just yet. Going back into the MeshLoader class, let's make some changes to the createMesh(...) method.

MeshLoader.java
public Mesh createMesh(float[] positions, float[] UVs, int[] indices){
		int vao = genVAO();
		storeData(0,3,positions);
		storeData(1,2,UVs);
		bindIndices(indices);
		GL30.glBindVertexArray(0);
		return new Mesh(vao,indices.length);
}

Now, when we create a Mesh, we pass along the UV coordinates, which represent the area on the texture the Mesh is using. When we use storeData(...) a second time, we tell it that the data it's receiving is to be interpreted as a 2D array, not a 3D one.

"So, NOW are we ready?" you ask? And to that I say, "Hey, don't talk back to me... Also, no."

Unfortunately, at present we only have part of the puzzle solved. In the next chapter, we'll learn about creating a simple Vertex and Fragment Shader, as well as going into more details about what shaders are and how they work.

Last updated