Textures in OpenGL

If you haven’t already, please read the Hello Triangle tutorial to understand the OpenGL pipeline and on the Nintendo 64 using Libdragon.

The key to drawing a texture on a surface is to do three things:

  • Set up OpenGL and Libdragon to work with textures
  • Upload the texture to TMEM
  • Set the texture coordinates on the model

For this example, we’ll be using a cube with a texture on it. Here is the full code, but we’ll break down the relevant parts to loading textures bit by bit.

Working through the code

Full source code
#include <libdragon.h>
#include <math.h>
#include <stdlib.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/gl_integration.h>

int main(void)
{
	// Initialise the various systems
	display_init(RESOLUTION_320x240, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_RESAMPLE);
	dfs_init(DFS_DEFAULT_LOCATION);
	rdpq_init();
	gl_init();

	// Setup
	float aspect_ratio = (float)display_get_width() / (float)display_get_height();
	float near_plane = 1.0f;
	float far_plane = 20.0f;

	// Set the viewing area
	glMatrixMode(GL_PROJECTION);
	glEnable(GL_DEPTH_TEST);
	glLoadIdentity();
	glFrustum(
		-near_plane*aspect_ratio,
		near_plane*aspect_ratio,
		-near_plane,
		near_plane,
		near_plane,
		far_plane
		);

	sprite_t* brick_sprite = sprite_load("rom:/images/brick.rgba16.sprite");
	float x = 0;
	float y = 0;
	float z = 0;

	// Main loop
	while(1) {
		// Start a new frame
		// Get the frame buffer
		surface_t* disp;
		surface_t *zbuf = display_get_zbuf();
		while(!(disp = display_try_get()));

		// Attach the buffers to the RDP
		rdpq_attach_clear(disp, zbuf);

		// Draw plain background
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		rdpq_set_mode_fill(RGBA32(128, 128, 128, 0));
		rdpq_fill_rectangle(0, 0, display_get_width(), display_get_height());

		// Start OpenGL context
		gl_context_begin();

			// Set the camera's position
			glMatrixMode(GL_MODELVIEW);
			glLoadIdentity();
			gluLookAt(
				0, 0, 10.0,
				0, 0, 0,
				0, 1, 0
				);

			// Settings for the texture
			glEnable(GL_RDPQ_TEXTURING_N64);
			glTexSizeN64(16, 16);

			// Upload the sprite
			rdpq_set_mode_standard();
			rdpq_mode_filter(FILTER_BILINEAR);
			rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY);
			rdpq_sprite_upload(
				TILE0,
				brick_sprite,
				&(rdpq_texparms_t){
					.s.repeats = REPEAT_INFINITE,
					.t.repeats = REPEAT_INFINITE
				});

			glPushMatrix();
				// Rotate the cube
				glRotatef(x, 1.0f, 0.0f, 0.0f);
				glRotatef(y, 0.0f, 1.0f, 0.0f);
				glRotatef(z, 0.0f, 0.0f, 1.0f);

				const float size = 1.0f;

				glBegin(GL_QUADS);

					// Front face
					glTexCoord2f(0.0f, 0.0f); glVertex3f(-size, -size,  size);
					glTexCoord2f(1.0f, 0.0f); glVertex3f( size, -size,  size);
					glTexCoord2f(1.0f, 1.0f); glVertex3f( size,  size,  size);
					glTexCoord2f(0.0f, 1.0f); glVertex3f(-size,  size,  size);

					// Back face
					glTexCoord2f(1.0f, 0.0f); glVertex3f(-size, -size, -size);
					glTexCoord2f(1.0f, 1.0f); glVertex3f(-size,  size, -size);
					glTexCoord2f(0.0f, 1.0f); glVertex3f( size,  size, -size);
					glTexCoord2f(0.0f, 0.0f); glVertex3f( size, -size, -size);

					// Left face
					glTexCoord2f(0.0f, 0.0f); glVertex3f(-size, -size, -size);
					glTexCoord2f(1.0f, 0.0f); glVertex3f(-size, -size,  size);
					glTexCoord2f(1.0f, 1.0f); glVertex3f(-size,  size,  size);
					glTexCoord2f(0.0f, 1.0f); glVertex3f(-size,  size, -size);

					// Right face
					glTexCoord2f(0.0f, 0.0f); glVertex3f( size, -size,  size);
					glTexCoord2f(1.0f, 0.0f); glVertex3f( size, -size, -size);
					glTexCoord2f(1.0f, 1.0f); glVertex3f( size,  size, -size);
					glTexCoord2f(0.0f, 1.0f); glVertex3f( size,  size,  size);

					// Top face
					glTexCoord2f(0.0f, 1.0f); glVertex3f(-size,  size,  size);
					glTexCoord2f(0.0f, 0.0f); glVertex3f( size,  size,  size);
					glTexCoord2f(1.0f, 0.0f); glVertex3f( size,  size, -size);
					glTexCoord2f(1.0f, 1.0f); glVertex3f(-size,  size, -size);

					// Bottom face
					glTexCoord2f(1.0f, 1.0f); glVertex3f(-size, -size, -size);
					glTexCoord2f(0.0f, 1.0f); glVertex3f( size, -size, -size);
					glTexCoord2f(0.0f, 0.0f); glVertex3f( size, -size,  size);
					glTexCoord2f(1.0f, 0.0f); glVertex3f(-size, -size,  size);

				glEnd();

			glPopMatrix();

		// We're done texturing, disable the settings
		glDisable(GL_RDPQ_TEXTURING_N64);

		// Does nothing for now, but keep it in case
		gl_context_end();

		// Send frame buffer to display (TV)
		rdpq_detach_show();
		x += 1.0;
		y += 0.5;
		y += 0.75;
	}
}

Set up OpenGL and Libdragon

The first step is to set up OpenGL and Libdragon to properly load the textures. Let’s focus on these lines:

// This sets the flag so that OpenGL knows to start drawing textures from TMEM
glEnable(GL_RDPQ_TEXTURING_N64);
// This tells OpenGL the size of the texture. Useful for scaling the texture properly
// If excluded, it will stretch a 1x1 pixel over the whole surface
glTexSizeN64(16, 16);

// After we're done drawing textured polygons, switch it off if you want to use another mode
glDisable(GL_RDPQ_TEXTURING_N64);

Uploading the texture

When it comes to uploading the texture, we do it the same way that we do with other hardware graphics.

// Set the RDP mode settings
rdpq_set_mode_standard();
rdpq_mode_filter(FILTER_BILINEAR);
rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY);

// Upload the sprite
rdpq_sprite_upload(
	TILE0,
	brick_sprite,
	&(rdpq_texparms_t){
		.s.repeats = REPEAT_INFINITE,
		.t.repeats = REPEAT_INFINITE
	});

Uploading a sprite will always upload the whole sprite, which might be tough considering that you can only use larger RGBA textures (16/32-bit). This is useful if you’re uploading a whole sprite for mipmapping.

You can also use surfaces, which allows finer control over spritesheets:

rdpq_set_mode_standard();
rdpq_mode_filter(FILTER_BILINEAR);
rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY);
rdpq_tex_upload_sub(
	0,
	&brick_surface,
	&(rdpq_texparms_t){
		.s.repeats = REPEAT_INFINITE,
		.t.repeats = REPEAT_INFINITE
	},
	0, 0,
	16, 16
);

Just remember that you need to upload to TMEM, not use one of the higher-level functions that draw to the screen like rdpq_sprite_blit().

Set the texture coordinates

The final step takes place when you’re drawing the model itself. You need to assign a texture coordinate to each vertex. In the cube example we’re using six squares, but for simplicity I’ll only show one:

float size = 1.0f;

glBegin(GL_QUADS);

	// Front face
	glTexCoord2f(0.0f, 0.0f); glVertex3f(-size, -size,  size);
	glTexCoord2f(1.0f, 0.0f); glVertex3f( size, -size,  size);
	glTexCoord2f(1.0f, 1.0f); glVertex3f( size,  size,  size);
	glTexCoord2f(0.0f, 1.0f); glVertex3f(-size,  size,  size);

glEnd();

As you can see, I’ve set the texture coordinates for each side to zero or one. This means that the texture is stretched to each corner of the square face. You can mess around with this to distort the texture in different ways if you’d like.

Summary

If you’ve got the hang of drawing textures using the RDPQ, this should be easy. The most complicated part is getting the texture coordinates set up correctly on your model. Just remember to set up Libdragon and OpenGL correctly, and then upload the texture to TMEM first.

Extra: waterfall effect

You can now add in some special effects like a waterfall by shifting the texture coordinates each frame:

glTexCoord2f(0.0f + (j%16)/16.0, 0.0f); glVertex3f(-size, -size,  size);
glTexCoord2f(1.0f + (j%16)/16.0, 0.0f); glVertex3f( size, -size,  size);
glTexCoord2f(1.0f + (j%16)/16.0, 1.0f); glVertex3f( size,  size,  size);
glTexCoord2f(0.0f + (j%16)/16.0, 1.0f); glVertex3f(-size,  size,  size);

j++;

Search

Subscribe to the mailing list

Random posts