OpenGL Tutorial 4 (QS) – Loading Textures & Animating Images

Image files such as JPG and PNG, first need to be loaded into your game or application by using an image loader such as STBI, which reads the image data. We then instruct OpenGL to generate a new texture, which can then be bound and formatted as a texture image by passing the image data to glTexImage2D(GL_TEXTURE_2D...)

The GL_TEXTURE_2D target is the one that makes texture (image) coordinates refer/correspond to the X, Y axes of the image normally, e.g. [0, 0] is the bottom-left, and [1, 1] is the top-right. Therefore, we can control and manipulated how an image maps onto a given mesh face, to produce different results.

It’s surprisingly easy to distort images in a variety of different ways, thereby producing basic special effects. By modifying the otherwise straightforward [0, 1] linear mapping of the image as applied to a given mesh face, we can easily produce: scrolling, stretching, and twisting-like distortions, as demonstrated in the following video.

Source code: C++ from... main.cpp

#include <glad/glad.h> // GLAD: https://github.com/Dav1dde/glad ... GLAD 2 also works via the web-service: https://gen.glad.sh/ (leaving all checkbox options unchecked)
#include <GLFW/glfw3.h>
 
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
 
#include <iostream>
#include <fstream> // Used in "shader_configure.h" to read the shader text files.
 
#include "shader_configure.h" // Used to create the shaders.
 
unsigned int load_texture_image(const charfile_name); // Function prototype
 
int main()
{
	// (1) GLFW: Initialise & Configure
	// -----------------------------------------
	if (!glfwInit())
		exit(EXIT_FAILURE);
 
	glfwWindowHint(GLFW_SAMPLES, 4); // Anti-aliasing
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
	glfwWindowHint(GLFW_OPENGL_PROFILEGLFW_OPENGL_CORE_PROFILE);	
	
	const GLFWvidmodemode = glfwGetVideoMode(glfwGetPrimaryMonitor());
 
	int monitor_width = mode->width; // Monitor's width.
	int monitor_height = mode->height;
 
	int window_width = (int)(monitor_width * 0.65f); // Window size will be 50% the monitor's size.
	int window_height = (int)(monitor_height * 0.65f); // Cast is simply to silence the compiler warning.
 
	GLFWwindowwindow = glfwCreateWindow(window_widthwindow_height"Loading Images - Scrolling Texture Coordinates"NULLNULL);
	// GLFWwindow* window = glfwCreateWindow(window_width, window_height, "Loading Images – Scrolling Texture Coordinates", glfwGetPrimaryMonitor(), NULL); // Full Screen Mode ("Alt" + "F4" to Exit!)
 
	if (!window)
	{
		glfwTerminate();
		exit(EXIT_FAILURE);
	}
 
	glfwMakeContextCurrent(window); // Set the window to be used and then centre that window on the monitor. 
	glfwSetWindowPos(window, (monitor_width - window_width) / 2, (monitor_height - window_height) / 2);
 
	glfwSwapInterval(1); // Set VSync rate 1:1 with monitor's refresh rate.
	
	// (2) GLAD: Load OpenGL Function Pointers
	// -------------------------------------------------------
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) // For GLAD 2 use the following instead: gladLoadGL(glfwGetProcAddress)
	{
		glfwTerminate();
		exit(EXIT_FAILURE);
	}
 
	glEnable(GL_MULTISAMPLE); // Anti-aliasing
	glEnable(GL_BLEND); // GL_BLEND for OpenGL transparency which is further set within the fragment shader.
 
	// Source image (1st argument; the one being rendered) = alpha, e.g., 0.45... Destination image (2nd argument; already in the buffer) = 1 - source (1 - 0.45 = 0.55)
	// -------------------------------------------------------------------------------------------------- https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glBlendFunc.xhtml
	glBlendFunc(GL_SRC_ALPHAGL_ONE_MINUS_SRC_ALPHA); 
 
	// (3) Compile Shaders Read from Text Files
	// ------------------------------------------------------
	const charvert_shader = "../../Shaders/shader_glsl.vert";
	const charfrag_shader = "../../Shaders/shader_glsl.frag";
 
	Shader main_shader(vert_shaderfrag_shader);
	main_shader.use();	
 
	// (4) Declare the Rectangle & Triangle Vertex Position & Texture Coordinate Values
	// ----------------------------------------------------------------------------------------------------------
	float scale = 1.0f; // 0.5... To make the rectangle completely fill the display window set this to 1
 
	float rectangle_triangle_vertices[] = // (Typically type: GLfloat is used)
	{
		// Note:  "Being able to store the vertex attributes for that vertex only once is very economical, as a vertex's attribute...
		// data is generally around 32 bytes, while indices are usually 2-4 bytes in size." (Hence, the next tutorial uses: glDrawElements())
		
		// Rectangle vertices (Texture Coordinates: 0,0 = bottom left... 1,1 = top right)
		//-------------------------
		-1.0f * scale, 1.0f * scale, 0.0f,		0.0f, 1.0f,  // left top ......... 0 // Draw this rectangle's six vertices 1st.
		-1.0f * scale, -1.0f * scale, 0.0f,		0.0f, 0.0f,  // left bottom ... 1 // Use NDC coordinates [-1, +1] to completely fill the display window.
		 1.0f * scale, 1.0f * scale, 0.0f,		1.0f, 1.0f,  // right top ....... 2
 
		 1.0f * scale,  1.0f * scale, 0.0f,		1.0f, 1.0f,   // right top ........ 3		
		-1.0f * scale,  -1.0f * scale, 0.0f,	0.0f, 0.0f,  // left bottom ..... 4		
		 1.0f * scale, -1.0f * scale, 0.0f,		1.0f, 0.0f,  // right bottom ... 5
 
		 // Triangle vertices (Drawing this 2nd allows some of the rectangle's fragment colour to be added to this triangle via transparency)
		 // ----------------------
		 0.0f, 0.75f, 0.0f,		0.5f, 1.0f,  // middle top ... 0
		-0.75f, -0.75f, 0.0f,	0.0f, 0.0f,  // left bottom ... 1
		 0.75f,  -0.75f, 0.0f,	1.0f, 0.0f  // right bottom .. 2		 
	};
 
	/* int width, height;
	glfwGetFramebufferSize(window, &width, &height); // Uncomment this block to adjust the effective display area.
	glViewport(-200, -112, int(width + 400), int(height + 225)); */
 
	// (5) Store the Rectangle & Triangle Vertex Data in Buffer Objects Ready for Drawing
	// -------------------------------------------------------------------------------------------------------------
	unsigned int VAOVBO// Buffer handles. (Typically type: GLuint is used)	
 
	glGenVertexArrays(1, &VAO);
	glGenBuffers(1, &VBO);	
 
	glBindVertexArray(VAO); // Binding this VAO 1st causes the following VBO to become associated with this VAO.
 
	glBindBuffer(GL_ARRAY_BUFFERVBO); // ......................... Address operator not required for arrays.
	glBufferData(GL_ARRAY_BUFFERsizeof(rectangle_triangle_vertices), rectangle_triangle_verticesGL_STATIC_DRAW);
 
	// Void pointer below is for legacy reasons. Two possible meanings: "offset for buffer objects" & "address for client state arrays"
	glEnableVertexAttribArray(0);
	glVertexAttribPointer(0, 3, GL_FLOATGL_FALSE, 5 * sizeof(float), (void*)0); 
 
	glEnableVertexAttribArray(1);
	glVertexAttribPointer(1, 2, GL_FLOATGL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
		   
	glBindVertexArray(0); 	// Unbind VAO
 
	// (6) Load & Bind Image Files & Set Shader's Uniform Samplers
	// --------------------------------------------------------------------------------
	// unsigned int image_1 = load_texture_image("multi_colour_rectangle_mipmaps.jpg");
 
	unsigned int image_1 = load_texture_image("image_dry_ground.jpg");
	// unsigned int image_2 = load_texture_image("image_bird_eagle.png");
 
	// unsigned int image_1 = load_texture_image("image_green_leaves.jpg");
	unsigned int image_2 = load_texture_image("insect_beetle_orange_spread.png");
	
	glActiveTexture(GL_TEXTURE0);	
	glBindTexture(GL_TEXTURE_2Dimage_1);
 
	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2Dimage_2);
 
	glUniform1i(glGetUniformLocation(main_shader.ID, "image_1"), 0);	
	glUniform1i(glGetUniformLocation(main_shader.ID, "image_2"), 1);
 
	// (7) Optional: Check Maximum Textures Allowed... https://www.khronos.org/opengl/wiki/Shader#Resource_limitations
	// ---------------------------------------------------------------
	GLint max_textures;
	glGetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &max_textures);
	std::cout << "The total number of texture image units that can be used (All active programs stages) = " << max_textures << "\n";
	
	glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &max_textures);	
	std::cout << "The maximum number of texture image units that the sampler can use (Fragment shader) = " << max_textures << "\n";
	
	// (8) Enter the Main-Loop
	// --------------------------------
	unsigned int scroll_texture_loc = glGetUniformLocation(main_shader.ID, "scroll_texture");
	float scroll_horizontally = 0;
	float scroll_vertically = 0;
	int direction_x = 1;
	int direction_y = 1;
 
	while (!glfwWindowShouldClose(window)) // Main-Loop
	{
		// (9) Scroll Values Used to Scroll the Image in Fragment Shader
		// --------------------------------------------------------------------------------
		scroll_horizontally += (1.0f / 100.0f) * direction_x;
		scroll_vertically += (1.0f / 100.0f) * direction_y;
 
		if (std::abs(scroll_horizontally) > 0.9f) // For: 1 / 120, then 0.5 (not 1, because of std::abs) = Direction changes every 2 seconds for 60Hz monitor.
			direction_x = -direction_x;
 
		if (std::abs(scroll_vertically) > 0.9f)
			direction_y = -direction_y;
 
		glUniform2f(scroll_texture_locscroll_horizontallyscroll_vertically); // Pass scroll values to fragment shader.
 
		// (10) Clear the Screen & Draw Both Shapes
		// --------------------------------------------------------
		glClearColor(0.35f, 0.6f, 0.8f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);
 
		glBindVertexArray(VAO); // Not necessary for this simple case of using only one VAO. Although, it's being unbound at initialisation just above.
		glDrawArrays(GL_TRIANGLES, 0, sizeof(rectangle_triangle_vertices) / sizeof(rectangle_triangle_vertices[0]));
		glBindVertexArray(0);
 
		glfwSwapBuffers(window);
		glfwPollEvents();		
	}
 
	// (11) Exit the Application
	// --------------------------------
	glDeleteProgram(main_shader.ID); // This OpenGL function call is talked about in: shader_configure.h
 
	/* glfwDestroyWindow(window) // Call this function to destroy a specific window */	
	glfwTerminate(); // Destroys all remaining windows and cursors, restores modified gamma ramps, and frees resources.
 
	exit(EXIT_SUCCESS); // Function call: exit() is a C/C++ function that performs various tasks to help clean up resources.
}
 
unsigned int load_texture_image(const charfile_name)
{
	stbi_set_flip_vertically_on_load(1); // Without calling this function, the image is upside-down.
 
	int widthheightnum_components;		
	unsigned charimage_data = stbi_load(file_name, &width, &height, &num_components, 0);
 
	unsigned int textureID;
	glGenTextures(1, &textureID);
 
	if (image_data)
	{
		GLenum format{};
 
		if (num_components == 1)
			format = GL_RED;
		else if (num_components == 3)
			format = GL_RGB;
		else if (num_components == 4)
			format = GL_RGBA;		
 
		glBindTexture(GL_TEXTURE_2DtextureID);
		glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // Recommended by NVIDIA Rep: https://devtalk.nvidia.com/default/topic/875205/opengl/how-does-gl_unpack_alignment-work-/
		
		glTexImage2D(GL_TEXTURE_2D, 0, formatwidthheight, 0, formatGL_UNSIGNED_BYTEimage_data);
		glGenerateMipmap(GL_TEXTURE_2D);
 
		// https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glTexParameter.xhtml
		// ----------------------------------------------------------------------------------------------------------------
		glTexParameteri(GL_TEXTURE_2DGL_TEXTURE_WRAP_SGL_MIRRORED_REPEAT); // GL_REPEAT... GL_MIRRORED_REPEAT... GL_CLAMP_TO_EDGE... GL_CLAMP_TO_BORDER.
		glTexParameteri(GL_TEXTURE_2DGL_TEXTURE_WRAP_TGL_MIRRORED_REPEAT);
 
		// float border_colour[] = {0.45, 0.55, 0.95};
		// glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border_colour); // For above when using: GL_CLAMP_TO_BORDER		
 
		// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
		// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2DGL_TEXTURE_MIN_FILTERGL_LINEAR); // GL_NEAREST... GL_LINEAR... GL_NEAREST_MIPMAP_NEAREST (See above link for full list)
		glTexParameteri(GL_TEXTURE_2DGL_TEXTURE_MAG_FILTERGL_LINEAR); // GL_NEAREST or GL_LINEAR.
 
		stbi_image_free(image_data);
		std::cout << "Image loaded OK\n\n";
	}
	else
	{		
		stbi_image_free(image_data);
		std::cout << "Image failed to load\n\n";
	}	
	return textureID;
}

Source code: C++ from... shader_configure.h

#pragma once // Instead of using include guards.
 
class Shader
{
public:
	GLuint ID; // Public Program ID.
 
	// Constructor
	// ---------------
	Shader(const charvert_pathconst charfrag_path)
	{
		char character;
 
		std::string vert_string;
		std::string frag_string;
 
		std::ifstream vert_stream;
		std::ifstream frag_stream;
 
		// Read vertex shader text file
		// ------------------------------------
		vert_stream.open(vert_path); // I decided not to implement: Exception handling try catch method.
 
		if (vert_stream.is_open()) // Note: There are various other methods for accessing the stream, i.e., vert_stream.get() is just one option.
		{
			while (vert_stream.get(character)) // Loop getting single characters until EOF (value false) is returned. 
				vert_string += character// "The first signature returns the character read, or the end-of-file value (EOF) if no characters are available in the stream..."
 
			vert_stream.close();
			std::cout << "File: " << vert_path << " opened successfully.\n\n";
		}
		else
			std::cout << "ERROR!... File: " << vert_path << " could not be opened.\n\n";
 
		// Read fragment shader text file
		// ----------------------------------------
		frag_stream.open(frag_path);
 
		if (frag_stream.is_open())
		{
			while (frag_stream.get(character))
				frag_string += character;
 
			frag_stream.close();
			std::cout << "File: " << frag_path << " opened successfully.\n\n";
		}
		else
			std::cout << "ERROR!... File: " << frag_path << " could not be opened.\n\n";
 
		std::cout << vert_string << "\n\n"// Output the shader files to display in the console window.
		std::cout << frag_string << "\n\n";
 
		const charvert_pointer = vert_string.c_str();
		const charfrag_pointer = frag_string.c_str();
 
		// Compile shaders
		// ----------------------
		GLuint vert_shadfrag_shad// Declare in here locally. Being attached to the public Program ID allows the shaders to be used publicly.
 
		// Create vertex shader
		// ---------------------------
		vert_shad = glCreateShader(GL_VERTEX_SHADER);
		glShaderSource(vert_shad, 1, &vert_pointerNULL);
		glCompileShader(vert_shad);
		Check_Shaders_Program(vert_shad"vert_shader");
 
		// Create fragment shader
		// -------------------------------
		frag_shad = glCreateShader(GL_FRAGMENT_SHADER);
		glShaderSource(frag_shad, 1, &frag_pointerNULL);
		glCompileShader(frag_shad);
		Check_Shaders_Program(frag_shad"frag_shader");
 
		// Create shader program
		// ------------------------------
		ID = glCreateProgram();
		glAttachShader(ID, vert_shad); // This also avoids deletion via: glDeleteShader(vert_shad) as called below.
		glAttachShader(ID, frag_shad);
		glLinkProgram(ID);
		Check_Shaders_Program(ID, "shader_program");
 
		// Note: Flagging the program object for deletion before calling "glUseProgram" would accidentally stop the program installation of the rendering state	
		// ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
		glDeleteShader(vert_shad); // Flag shader object for automatic deletion (freeing memory) when no longer attached to a program object...
		glDeleteShader(frag_shad); // ... program object is deleted (glDeleteProgram ) within: main() when the application ends.
 
		// glUseProgram(ID); // Typically this is called within: main() to select individual shaders that have been created. 
		// glDeleteProgram(ID); // Alternatively the program object can be deleted here after 1st calling:  glUseProgram(ID)
	}
 
	// Activate the shader
	// -------------------------
	void use()
	{
		glUseProgram(ID); // Function called from within main() to select an individual shader to be used.
	}
 
private:
	// Check shader compilations and program object for linking errors
	// -------------------------------------------------------------------------------------
	void Check_Shaders_Program(GLuint type, std::string name)
	{
		int success;
		int error_log_size;
		char info_log[1000]; // 1000 characters max. Typically it's less than 500 even for multiple shader errors.
 
		if (name == "vert_shader" || name == "frag_shader")
		{
			glGetShaderiv(typeGL_COMPILE_STATUS, &success);
			if (!success)
			{
				glGetShaderInfoLog(type, 1024, &error_log_sizeinfo_log);
				std::cout << "\n--- Shader Compilation Error: " << name << "\n\n" << info_log << "\n" << "Error Log Number of Characters: " << error_log_size << "\n\n";
			}
		}
		else // "shader_program"
		{
			glGetProgramiv(typeGL_LINK_STATUS, &success);
			if (!success)
			{
				glGetProgramInfoLog(type, 1024, &error_log_sizeinfo_log);
				std::cout << "\n--- Program Link Error: " << name << "\n\n" << info_log << "\n" << "Error Log Number of Characters: " << error_log_size << "\n";
			}
		}
	}
};

Source code: GLSL from... shader_glsl.vert (Vertex shader)

#version 420 core
 
layout (location = 0) in vec3 aPos;	 // Attribute data: vertex(s) X, Y, Z position via VBO set up on the CPU side.
layout (location = 1) in vec2 aTexCoord;
 
flat out vec2 shape_xy_pos_flat; // Vertex position coordinates passed to the fragment shader.
out vec2 shape_xy_pos_varying; // Vertex position coordinates passed as interpolated per-vertex.
out vec2 texture_coordinates;
 
void main()
{
	shape_xy_pos_flat = vec2(aPos.x, aPos.y); // Send values that will not be interpolated in the fragment shader.
	shape_xy_pos_varying = vec2(aPos.x, aPos.y); // These values will be interpolated in the fragment shader.
 
	texture_coordinates = aTexCoord;
		
	// https://www.khronos.org/opengl/wiki/Vertex_Post-Processing
	gl_Position = vec4(aPos, 1.0); // Output to vertex stream for the "Vertex Post-Processing" stage.
}

Source code: GLSL from... shader_glsl.frag (Fragment shader)

#version 420 core
 
out vec4 fragment_colour;
 
// Must be the exact same name as declared in the vertex shader
// -----------------------------------------------------------------------------------
flat in vec2 shape_xy_pos_flat; // Vertex position coordinates received that are not being interpolated.
in vec2 shape_xy_pos_varying; // Vertex position coordinates received as interpolated per-vertex.
in vec2 texture_coordinates;
 
uniform sampler2D image_1;
uniform sampler2D image_2;
uniform vec2 scroll_texture;
 
mat4 transformation_matrix(vec3 axis, float angle); // Function prototype.
 
void main() // For texture() ... 0,0 = Image bottom left... 1,1 = Top right.
{
	if (abs(shape_xy_pos_flat.y) == 0.75) // Draw triangle
	{
		// Drawing using scrolling X value
		// -----------------------------------------
		fragment_colour = texture(image_2, vec2((texture_coordinates.x + scroll_texture.x), texture_coordinates.y));
 
		// Beetle spots alpha = 0.40 as set in Photoshop
		// ------------------------------------------------------------
		if (fragment_colour.a < 0.40) // Use < 0.40 to execute the else-statement (The background is 100% transparent as set in Photoshop)
			fragment_colour = vec4(0.5, abs(shape_xy_pos_varying.x), abs(shape_xy_pos_varying.y), 0.39); // Set green and blue to varying XY values.
		else	
		{		
			if (fragment_colour.a <  0.41) // Discarding also makes the beetle's spots 100% transparent like when using < 41 above.
				// discard;
				fragment_colour = vec4(0, 0, 1, 0.65); // Manually set the beetle's spot colour.
		}
 
		// Uncommenting the below overrides the above
		// --------------------------------------------------------------
		//		mat4 coords_rot_mat = transformation_matrix(vec3(0.0, 0.0, 1.0), scroll_texture.y * shape_xy_pos_varying.x);  // Use to rotate and stretch the image.
		//		vec2 new_coords = vec2(mat3(coords_rot_mat) * vec3(texture_coordinates, 0.0));
		//
		//		new_coords.y += scroll_texture.y * 1.5;
		//
		//		fragment_colour = texture(image_2, new_coords);
		//
		//		if (fragment_colour.a < 0.41)
		//		{
		//			if (fragment_colour.a != 0)
		//				fragment_colour = vec4(0, abs(new_coords.x * 2), abs(new_coords.y), 0.95); // Manually set the beetle's spot colour.
		//			else
		//				fragment_colour = vec4(0.5, abs(shape_xy_pos_varying.x), abs(shape_xy_pos_varying.y), 0.35);
		//		}
 
		//		if (new_coords.x > 1 || new_coords.x < 0)
		//		{
		//			fragment_colour.r = abs(new_coords.x - floor(new_coords.x));
		//			fragment_colour.g = abs(new_coords.y - floor(new_coords.y));
		//			fragment_colour.b = 1.0;
		//		}
		//
		//		if (new_coords.y > 1 || new_coords.y < 0)
		//		{
		//			fragment_colour.r = abs(new_coords.y - floor(new_coords.y)) * 1.25;
		//			fragment_colour.g = abs(new_coords.x - floor(new_coords.x));
		//			fragment_colour.b = 0.65;
		//		}
		
		// discard; // Used to completely disable drawing the triangle.
	}
	else // Draw rectangle
	{		
		// fragment_colour = vec4(vec3(texture(image_1, vec2(texture_coordinates.x + scroll_texture.x, texture_coordinates.y))), 1.0); // Drawing using scrolling X value.
		// fragment_colour = vec4(vec3(texture(image_1, vec2(texture_coordinates.x, texture_coordinates.y + scroll_texture.y))), 1.0); // Drawing using scrolling Y value.
		// fragment_colour = vec4(vec3(texture(image_1, vec2(texture_coordinates.x + scroll_texture.x, texture_coordinates.y + scroll_texture.y))), 1.0); // Drawing using scrolling X and Y value.
			
		// Uncommenting the below overrides the above
		// --------------------------------------------------------------
		// mat4 coords_rot_mat = transformation_matrix(vec3(0.0, 0.0, 1.0), scroll_texture.x); // Use to only rotate the image.
		mat4 coords_rot_mat = transformation_matrix(vec3(0.0, 0.0, 1.0), scroll_texture.x * shape_xy_pos_varying.y); // Use to rotate and stretch the image.
		
		vec2 new_coords = vec2(mat3(coords_rot_mat) * vec3(texture_coordinates, 0.0));
		
		new_coords.y += scroll_texture.y;
				
		fragment_colour = vec4(vec3(texture(image_1, new_coords)), 1.0);		
 
		// Set border colour for texture coordinates outside [0, 1]
		// ------------------------------------------------------------------------
		// float new_x_coord = texture_coordinates.x + scroll_texture.x; // Produces straight horizontal and vertical borders. 
		// float new_y_coord = texture_coordinates.y + scroll_texture.y;
		
		float new_x_coord = new_coords.x; // Produces curved borders that follow the shape of the distortion.
		float new_y_coord = new_coords.y;
						
		if (new_x_coord > 1 || new_x_coord < 0)
		{
			fragment_colour.r = abs(new_x_coord - floor(new_x_coord));
			fragment_colour.g = abs(new_y_coord - floor(new_y_coord));		
			fragment_colour.b = 0.35;
		}
										
		if (new_y_coord > 1 || new_y_coord < 0)
		{
			fragment_colour.r = abs(new_y_coord - floor(new_y_coord));
			fragment_colour.g = abs(new_x_coord - floor(new_x_coord));			
			fragment_colour.b = 0.55;
		}	
	}
}
 
mat4 transformation_matrix(vec3 axis, float angle) // http://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/ ... http://www.songho.ca/opengl/gl_anglestoaxes.html
{
    axis = normalize(axis); // Axis to rotate around // DO NOT Normalize (Else it crashes) Zero vectors, i.e., when all three values are: 0... (0, 0, 0)
 
    float s = sin(angle);  // Angle in radians.
    float c = cos(angle); // Cosine of the angle [-1.0, 1.0]
    float oc = 1.0 - c;      // Range [0, 2]
 
    return mat4 
    (			
	 oc * axis.x * axis.x + c,                    oc * axis.x * axis.y - axis.z * s,	 oc * axis.z * axis.x + axis.y * s,      0.0,	
         oc * axis.x * axis.y + axis.z * s,      oc * axis.y * axis.y + c,                      oc * axis.y * axis.z - axis.x * s,       0.0,
         oc * axis.z * axis.x - axis.y * s,       oc * axis.y * axis.z + axis.x * s,        oc * axis.z * axis.z + c,                    0.0,
         0.0,                                                    0.0,                                                       0.0,                                                    1.0
    );
}