OpenGL Tutorial 7 (Quick Start) – Model Loading – Saving Binary Files

The object (.obj) and material (.mtl) file extensions are types of text files – used to store 3D model vertex data. It takes a significant amount of time to parse an object file that contains millions of vertices... or more likely, there would be several models totalling millions of vertices.

The ASSIMP 3D model loading import library parses (reads and organises into data structures) text files in order to deliver (import) various data into our game or application, including vertex position values, normal vectors, texture coordinates, and more.

However, there’s no need to parse model text files during every program run (but it is indeed required to re-parse the text files of any model that has been modified in 3D modelling software and then re-exported as an updated model). Therefore, models can be loaded much faster by saving and reading model data as binary files, as demonstrated in the following video...


Code updated: 24-06-2022 (Now able to load and draw multiple models without sampler images getting overridden)

Code updated: 03-09-2022

  1. Class: load_meshes_combine.h: uniform calls have been moved from configure_draw_calls() to each of the draw-call functions (as an additional fix to the above sampler issue).

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"
 
// OpenGL Mathematics(GLM) ... https://github.com/g-truc/glm/blob/master/manual.md
// ------------------------------------
// GLM Headers
// ------------------
#include <glm/glm.hpp> // Include all GLM core.
	// #include <glm/ext.hpp> // Include all GLM extensions.
#include <glm/gtc/matrix_transform.hpp> // Specific extensions.
#include <glm/gtc/type_ptr.hpp>
 
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
 
#include <vector>
#include <iostream>
#include <fstream> // Used in "shader_configure.h" to read the shader text files.
 
#include "shader_configure.h" // Used to create the shaders.
#include "load_meshes_binary.h"
 
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.75f); // Window size will be 65% the monitor's size...
	int window_height = (int)(monitor_height * 0.75f); // ... Cast is simply to silence the compiler warning.
 
	GLFWwindowwindow = glfwCreateWindow(window_widthwindow_height"Assimp Model Loading - Combining Meshes & Save Binary"NULLNULL);
	// GLFWwindow* window = glfwCreateWindow(window_width, window_height, "Assimp Model Loading - Combining Meshes & Save Binary", 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_DEPTH_TEST); // Enabling depth testing allows rear faces of 3D objects to be hidden behind front faces.
	glEnable(GL_MULTISAMPLE); // Anti-aliasing
	glEnable(GL_BLEND); // GL_BLEND for OpenGL transparency which is further set within the fragment shader. 
	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();	
	
	unsigned int view_matrix_loc = glGetUniformLocation(main_shader.ID, "view");
	unsigned int projection_matrix_loc = glGetUniformLocation(main_shader.ID, "projection");
	unsigned int camera_position_loc = glGetUniformLocation(main_shader.ID, "camera_position");
 
	glm::vec3 camera_position(0.0f, 10.0f, 15.0f); // -Z is into the screen.		
	glm::vec3 camera_target(0.0f, 0.0f, 0.0f);
	glm::vec3 camera_up(0.0f, 1.0f, 0.0f);
 
	glUniform3f(camera_position_loccamera_position.x, camera_position.y, camera_position.z);
 
	glm::mat4 view = glm::lookAt(camera_positioncamera_targetcamera_up);
	glUniformMatrix4fv(view_matrix_loc, 1, GL_FALSE, glm::value_ptr(view)); // Uniform: Transfer view matrix to vertex shader.
 
	glm::mat4 projection = glm::perspective(glm::radians(55.0f), (float)window_width / (float)window_height, 0.1f, 100.0f);
	glUniformMatrix4fv(projection_matrix_loc, 1, GL_FALSE, glm::value_ptr(projection));
 
	// (4) Enter the Main-Loop
	// --------------------------------
	srand((unsigned)time(NULL)); // Initialise random seed.
 
	float x_spin = 1.0f / (rand() % 10 + 1); // Generate random number between 1 and 10
	float y_spin = 1.0f / (rand() % 10 + 1);
	float z_spin = 1.0f / (rand() % 10 + 1);
	float spin_speed = (float)(rand() % 5 + 1); // Cast is simply to silence the compiler warning.
 
	float spin_vary = 0.0f;
	int spin_dir = 1;
 
	glm::mat4 spinning_mat(1.0f);
 
	unsigned int animate_loc = glGetUniformLocation(main_shader.ID, "animate");	
 
	// https://www.turbosquid.com/Search/3D-Models/free/commercial (Free Models)
	// ----------------------------------------------------------------------------------------	
	// https://www.youtube.com/watch?v=-bCeNzgiJ8I (Steamworks Development video – Cass Everitt & John McDonald from NVIDIA)
	// ----------------------------------------------------------------
	// Model model("model_testing.obj", main_shader, 2); // Draw method: 0 = multiple meshes... 1 = combined (5 VBOs) ... 2 = combined (1 VBO)
	Model black_smith("black_smith.obj"main_shader, 0);
	Model helicopter("The_Beast_Helicopter.obj"main_shader, 1);
	Model aeroplane("Plane_CAP_232.obj"main_shader, 2);
 
	while (!glfwWindowShouldClose(window)) // Main-Loop
	{
		// (5) Randomise the Model's Spinning Speed & Axis
		// ------------------------------------------------------------------	
		spin_vary += 0.05f * spin_dir;
 
		if (spin_vary > 6 || spin_vary < 0)
		{
			spin_dir = -spin_dir// Reverse the spinning direction.
 
			x_spin = 1.0f / (rand() % 10 + 1);
			y_spin = 1.0f / (rand() % 10 + 1);
			z_spin = 1.0f / (rand() % 10 + 1);
			spin_speed = (float)(rand() % 50 + 1) / 20;			
		}
		
		spinning_mat = glm::rotate(spinning_mat, glm::radians(spin_speed), glm::normalize(glm::vec3(x_spiny_spinz_spin)));
		glUniformMatrix4fv(animate_loc, 1, GL_FALSE, glm::value_ptr(spinning_mat)); // Pass rotation matrix to vertex shader.
 
		// (6) Clear the Screen & Draw Model Meshes
		// ---------------------------------------------------------
		glClearColor(0.30f, 0.55f, 0.65f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		
		black_smith.process_draw_calls(); // Draw either multiple meshes; combined using 5VBOs, or combined using 1VB0.
		helicopter.process_draw_calls();
		aeroplane.process_draw_calls();
		
		glfwSwapBuffers(window);
		glfwPollEvents();
	}
 
	// (7) 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.
}

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::ifstream vert_stream;
		std::ifstream frag_stream;
 
		std::string vert_string;
		std::string frag_string;		
 
		// 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 << "\n   File: " << vert_path << " opened successfully.\n";
		}
		else
			std::cout << "\n   ERROR!... File: " << vert_path << " could not be opened.\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: C++ from... load_meshes_binary.h

#pragma once // Instead of using include guards.
 
class Model
{
private:
	Assimp::Importer importer; // https://assimp-docs.readthedocs.io/en/v5.1.0/ ... (An older Assimp website: http://assimp.sourceforge.net/lib_html/index.html)
	const aiScene* scene = nullptr;
	aiNode* root_node = nullptr// Only being used in the: load_model_cout_console() function.	
	
	struct Mesh
	{
		unsigned int VAO, VBO1, VBO2, VBO3, EBO; // Buffer handles (Typically type: GLuint is used)
		
		std::vector<glm::vec3> vert_positions;
		std::vector<glm::vec3> vert_normals;
		std::vector<glm::vec2> tex_coords;
		std::vector<unsigned int> vert_indices;
		unsigned int mesh_num; // Add a uniform if wanting to use mesh_num via: draw_multiple_meshes()  
		unsigned int tex_handle;		
	};
	struct Texture
	{
		unsigned int tex_handle;
		unsigned int sampler_location;
		std::string image_name;
	};
	// ------------------------------
	struct Meshes_5VBO_Combined
	{
		unsigned int VAO, VBO1, VBO2, VBO3, VBO4, VBO5, EBO;		
		std::vector<Texture> texture_list;
 
		std::vector<glm::vec3> vert_positions;
		std::vector<glm::vec3> vert_normals;
		std::vector<glm::vec2> tex_coords;
		std::vector<unsigned int> vert_indices;
		std::vector<unsigned int> mesh_num;
		std::vector<unsigned int> sampler_array_pos;		
	};
	// ------------------------------
	struct Vertex
	{
		glm::vec3 vert_position;
		glm::vec3 vert_normal;
		glm::vec2 tex_coords;
		unsigned int mesh_num;
		unsigned int sampler_array_pos;
	} ;
	struct Meshes_1VBO_Combined
	{
		unsigned int VAO, VBO, EBO;
		std::vector<Texture> texture_list;
 
		std::vector<unsigned int> vert_indices;
		std::vector<Vertex> vertex_data;
	};
	// ------------------------------
	bool binary_version_found;
 
	int skip;
	double time;
	double prev_time;	
	unsigned int draw_method;
	unsigned int num_meshes;	
 
	std::vector<Mesh> mesh_list;
	std::vector<Texture> texture_list;
	Meshes_1VBO_Combined meshes_1VBO_combined;
	Meshes_5VBO_Combined meshes_5VBO_combined;
 
	Shader& shader;
	unsigned int draw_multiple_meshes_sampler_pos0_val;
	unsigned int rendering_multiple_meshes_loc; // Uniform required because multiple meshes (draw method 0) doesn't pass mesh number via shader input attribute.
	unsigned int meshes_combined_loc;
 
public:
	Model(const charmodel_pathShadermain_shaderunsigned int draw_method) : shader(main_shader// Constructor
	{		
		skip = 0;
		time = 0;
		prev_time = 0;		
		this->draw_method = draw_method;
		// ---------------------------------------------
		configure_draw_calls(model_path);
	}
 
	void process_draw_calls()
	{
		if (draw_method == 0)
			draw_multiple_meshes();
		if (draw_method == 1)
			draw_meshes_combined_5VBO();
		if (draw_method == 2)
			draw_meshes_combined_1VBO();
	}
 
private:
	void configure_draw_calls(std::string model_path)
	{
		rendering_multiple_meshes_loc = glGetUniformLocation(shader.ID, "rendering_multiple_meshes");
		meshes_combined_loc = glGetUniformLocation(shader.ID, "meshes_combined");
 
		if (draw_method == 0)
		{
			load_model(model_path); // Uncomment only one of these two load model functions.
			// load_model_cout_console(model_path);
 
			draw_multiple_meshes_sampler_pos0_val = glGetUniformLocation(shader.ID, "images[0]");
		}
 
		if (draw_method == 1)
		{
			binary_version_found = look_for_model_5VBO(model_path);
 
			if (binary_version_found)
			{
				std::cout << "\n   Binary model found (5VBO) Path: " << model_path;
				std::cout << "\n   ********************************************************\n\n";
 
				// Measure Loading Time (5VBO)
				// ----------------------------------------
				double time_before_binary = glfwGetTime();
				read_model_files_5VBO(model_path);
				std::cout << "   Time taken to read binary files (5VBO): " << glfwGetTime() - time_before_binary << "\n\n";
 
				double time_before_textures = glfwGetTime();
				load_file_name_images(meshes_5VBO_combined.texture_list);
				std::cout << "\n   Time taken to load textures (5VBO): " << glfwGetTime() - time_before_textures << "\n\n";
 
				populate_sampler_array_setup(meshes_5VBO_combined.texture_list); // Includes calls to: glGetUniformLocation()
			}
			else
			{
				std::cout << "\n   Did not find usable binary model (5VBO) Path: " << model_path;
				std::cout << "\n   **********************************************************************\n\n";
 
				load_model(model_path);	// Load model via Assimp.
					// load_model_cout_console(model_path);
 
				combine_meshes_5VBO(); // Vertex data as... vector lists.
				write_model_files_5VBO(model_path);
 
				populate_sampler_array_setup(texture_list);
			}
			set_buffer_data_combined_5VBO(); // Set up: VAO, VBO and EBO.			
		}
 
		if (draw_method == 2)
		{
			binary_version_found = look_for_model_1VBO(model_path);
 
			if (binary_version_found)
			{
				std::cout << "\n   Binary model found (1VBO) Path: " << model_path;
				std::cout << "\n   ********************************************************\n\n";
 
				// Measure Loading Time (1VBO)
				// ----------------------------------------
				double time_before_binary = glfwGetTime();
				read_model_files_1VBO(model_path);
				std::cout << "   Time taken to read binary files (1VBO): " << glfwGetTime() - time_before_binary << "\n\n";
 
				double time_before_textures = glfwGetTime();
				load_file_name_images(meshes_1VBO_combined.texture_list);
				std::cout << "\n   Time taken to load textures (1VBO): " << glfwGetTime() - time_before_textures << "\n\n";
 
				populate_sampler_array_setup(meshes_1VBO_combined.texture_list); // Includes calls to: glGetUniformLocation()
			}
			else
			{
				std::cout << "\n   Did not find usable binary model (1VBO) Path: " << model_path;
				std::cout << "\n   **********************************************************************\n\n";
 
				load_model(model_path); // Load model via Assimp.
					// load_model_cout_console(model_path);
 
				combine_meshes_1VBO(); // Vertex data as... struct lists.
				write_model_files_1VBO(model_path);
 
				populate_sampler_array_setup(texture_list);
			}
			set_buffer_data_combined_1VBO(); // Set up: VAO, VBO and EBO.			
		}
	}
 
	void load_model(std::string model_path)
	{
		// http://assimp.sourceforge.net/lib_html/postprocess_8h.html (See: aiPostProcessSteps) (Flag options)			
		scene = importer.ReadFile(model_pathaiProcess_JoinIdenticalVertices | aiProcess_Triangulate | aiProcess_FlipUVs);
		// scene = importer.ReadFile(model_path, NULL | aiProcess_FlipUVs);
 
		if (!scene || !scene->mRootNode || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE)
			std::cout << "Assimp importer.ReadFile (Error) -- " << importer.GetErrorString() << "\n";
		else
		{
			num_meshes = scene->mNumMeshes;
			mesh_list.resize(num_meshes);
 
			aiMeshmesh{};
			unsigned int total_num_vertices = 0;
			unsigned int total_num_indices = 0;
 
			// (1) Loop through all the model's meshes
			// -----------------------------------------------------
			for (unsigned int i = 0; i < num_meshes; ++i)
			{
				mesh_list[i].mesh_num = i// Fed in to vertex shader as an input attribute (Used to identify and transform meshes independently of one another)
				mesh = scene->mMeshes[i]; // http://assimp.sourceforge.net/lib_html/structai_mesh.html				
 
				total_num_vertices += mesh->mNumVertices;
				// std::cout << "   Mesh: " << i << " --- mesh[i].mNumVertices: " << mesh->mNumVertices << "\n";
 
				aiMaterialmaterial = scene->mMaterials[mesh->mMaterialIndex]; // http://assimp.sourceforge.net/lib_html/structai_material.html	
				
				unsigned int tex_count = 0; // This loop will only run once (i.e. there's only 1 texture per mesh)
				for (; tex_count < material->GetTextureCount(aiTextureType_DIFFUSE); ++tex_count// Also, only using: aiTextureType_DIFFUSE.
				{
					aiString string;
					material->GetTexture(aiTextureType_DIFFUSEtex_count, &string);	 // Acquire the name of the image file to be loaded.			
 
					// (2) Load mesh [i]'s texture if not already loaded
					// ---------------------------------------------------------------
					int already_loaded = is_image_loaded(string.C_Str()); // Returns -1 if texture Not already loaded, otherwise returns Existing texture handle.
 
					if (already_loaded == -1) // Image not yet loaded so now attempt to load it.
					{
						bool load_success = false;
						unsigned int texture_handle = load_texture_image(string.C_Str(), load_success); // Notice the UPPER case "C_Str()"
 
						if (load_success// Although do nothing if the image fails to load.
						{
							Texture texture;
							texture.image_name = string.C_Str();
							texture.tex_handle = texture_handle;
							texture_list.push_back(texture);
 
							mesh_list[i].tex_handle = texture_handle;							
						}
						std::cout << "\n";
					}
					else
						mesh_list[i].tex_handle = already_loaded// Assign existing texture handle.
				}
				if (tex_count == 0)
				{
					mesh_list[i].tex_handle = 0;
					// std::cout << "   material->GetTexture(...) No image has been applied to this mesh\n";
				}
 
				// (3) Loop through all mesh [i]'s vertices
				// ---------------------------------------------------
				for (unsigned int i2 = 0; i2 < mesh->mNumVertices; ++i2)
				{
					glm::vec3 position{};
					position.x = mesh->mVertices[i2].x;
					position.y = mesh->mVertices[i2].y;
					position.z = mesh->mVertices[i2].z;
					mesh_list[i].vert_positions.push_back(position);
 
					if (mesh->HasNormals())
					{
						glm::vec3 normal{};
						normal.x = mesh->mNormals[i2].x;
						normal.y = mesh->mNormals[i2].y;
						normal.z = mesh->mNormals[i2].z;
						mesh_list[i].vert_normals.push_back(normal);
					}
					else
						mesh_list[i].vert_normals.push_back(glm::vec3(0.0f, 0.0f, 0.0f));
 
					if (mesh->HasTextureCoords(0)) // Only slot [0] is in question.
					{
						glm::vec2 tex_coords{};
						tex_coords.x = mesh->mTextureCoords[0][i2].x;
						tex_coords.y = mesh->mTextureCoords[0][i2].y;
						mesh_list[i].tex_coords.push_back(tex_coords);
					}
					else
						mesh_list[i].tex_coords.push_back(glm::vec2(0.0f, 0.0f));
				}
				// (4) Loop through all mesh [i]'s Indices
				// --------------------------------------------------
				for (unsigned int i3 = 0; i3 < mesh->mNumFaces; ++i3)
				{
					for (unsigned int i4 = 0; i4 < mesh->mFaces[i3].mNumIndices; ++i4)
						mesh_list[i].vert_indices.push_back(mesh->mFaces[i3].mIndices[i4]);
					total_num_indices += mesh->mFaces[i3].mNumIndices;
				}
				set_buffer_data(i); // Set up: VAO, VBO and EBO.
			}			
			std::cout << "\n   Assimp... total_num_vertices: " << total_num_vertices << "\n";
			std::cout << "   Assimp... total_num_indices: " << total_num_indices << "\n\n";
		}
	}
 
	void load_model_cout_console(std::string model_path)
	{
		// http://assimp.sourceforge.net/lib_html/postprocess_8h.html (See: aiPostProcessSteps) (Flag options)			
		scene = importer.ReadFile(model_pathaiProcess_JoinIdenticalVertices | aiProcess_Triangulate | aiProcess_FlipUVs);
		// scene = importer.ReadFile(model_path, aiProcess_Triangulate | aiProcess_FlipUVs);
 
		// Briefly looking at the node structure
		// ------------------------------------------------
		if (!scene || !scene->mRootNode || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE)
			std::cout << "Assimp importer.ReadFile (Error) -- " << importer.GetErrorString() << "\n";
		else
		{
			num_meshes = scene->mNumMeshes;
			mesh_list.resize(num_meshes);
 
			std::cout << "\n   Start of Assimp Loading Meshes & Analysis";
			std::cout << "\n   -----------------------------------------";
 
			root_node = scene->mRootNode;
 
			std::cout << "\n   node->mNumMeshes: " << root_node->mNumMeshes;
			std::cout << "\n   node->mName.C_Str(): " << root_node->mName.C_Str();
			std::cout << "\n\n   node->mNumChildren: " << root_node->mNumChildren;
			// ------------------------------------------------------------------------------------------
			for (unsigned int i = 0; i < root_node->mNumChildren; ++i)
			{
				std::cout << "\n   node->mChildren[i]->mName.C_Str(): " << root_node->mChildren[i]->mName.C_Str();
				std::cout << "\n   node->mChildren[i]->mNumMeshes: " << root_node->mChildren[i]->mNumMeshes;
			}
			std::cout << "\n\n   scene->HasMaterials(): " << scene->HasMaterials();
			// ------------------------------------------------------------------------------------------
			for (unsigned int i = 0; i < scene->mNumMaterials; ++i)
				std::cout << "\n   scene->mMaterials[i]->GetName(): " << scene->mMaterials[i]->GetName().C_Str();
 
			std::cout << "\n\n   scene->HasTextures(): " << scene->HasTextures();
 
			aiMeshmesh{};
 
			int total_num_indices = 0;
 
			// (1) Loop through all the model's meshes
			// -----------------------------------------------------
			std::cout << "\n   scene->mNumMeshes: " << num_meshes;
			std::cout << "\n   ********************\n";
			// ---------------------------------------------------------
			for (unsigned int i = 0; i < num_meshes; ++i// In this case... scene->mNumMeshes = node->mChildren[i]->mNumMeshes
			{
				mesh_list[i].mesh_num = i// Fed in to vertex shader as an input attribute (Used to identify and transform meshes independently of one another)
				mesh = scene->mMeshes[i]; // http://assimp.sourceforge.net/lib_html/structai_mesh.html
 
				std::cout << "\n\n   mesh->mMaterialIndex: " << mesh->mMaterialIndex;
				std::cout << "\n   ----------------------- ";
				std::cout << "\n   mesh->mName.C_Str(): " << mesh->mName.C_Str();
 
				aiMaterialmaterial = scene->mMaterials[mesh->mMaterialIndex]; // http://assimp.sourceforge.net/lib_html/structai_material.html
 
				std::cout << "\n\n   material->GetTexture(aiTextureType_DIFFUSE, tex_count, &string): " << material->GetTextureCount(aiTextureType_DIFFUSE);
				std::cout << "\n   material->GetTexture(aiTextureType_SPECULAR, tex_count, &string): " << material->GetTextureCount(aiTextureType_SPECULAR);
				std::cout << "\n   material->GetTexture(aiTextureType_AMBIENT, tex_count, &string): " << material->GetTextureCount(aiTextureType_AMBIENT) << "\n\n";
 
				unsigned int tex_count = 0; // This loop will only run once (i.e. there's only 1 texture per mesh)
				for (; tex_count < material->GetTextureCount(aiTextureType_DIFFUSE); ++tex_count// The above std::cout reveals that only using: aiTextureType_DIFFUSE
				{
					aiString string;
					material->GetTexture(aiTextureType_DIFFUSEtex_count, &string); // Acquire the name of the image file to be loaded.
					std::cout << "   material->GetTexture(aiTextureType_DIFFUSE, tex_count, &string): " << string.C_Str() << "\n\n";
 
					// (2) Load mesh [i]'s texture if not already loaded
					// ---------------------------------------------------------------
					int already_loaded = is_image_loaded(string.C_Str()); // Returns -1 if texture Not already loaded, otherwise returns Existing texture handle.
					std::cout << "   Loading Image\n";
 
					if (already_loaded == -1) // Image not yet loaded.
					{
						bool load_complete = false;
						unsigned int texture_handle = load_texture_image(string.C_Str(), load_complete); // Notice the UPPER case "C_Str()"
 
						if (load_complete// Although do nothing if the image fails to load.
						{
							Texture texture;
							texture.image_name = string.C_Str();
							texture.tex_handle = texture_handle;
							texture_list.push_back(texture);
 
							mesh_list[i].tex_handle = texture_handle;							
						}
					}
					else // Assign existing texture handle.
					{
						std::string edited = string.C_Str();
						std::size_t position = edited.find_last_of("\\");
 
						std::cout << "   Image file: " << edited.substr(position + 1) << " (is already loaded)";
						mesh_list[i].tex_handle = already_loaded;
					}
				}
				if (tex_count == 0)
				{
					mesh_list[i].tex_handle = 0;
					// std::cout << "   material->GetTexture(...) No image has been applied to this mesh\n\n";
				}
				else
					std::cout << "\n\n";
 
				for (unsigned int slot = 0; slot < AI_MAX_NUMBER_OF_TEXTURECOORDS; ++slot)
					std::cout << "   mesh->HasTextureCoords(" << slot << "): " << mesh->HasTextureCoords(slot) << "\n";
 
				std::cout << "\n   Mesh index: " << i << " (mesh->mNumVertices: " << mesh->mNumVertices << ")";
				std::cout << "\n   ------------------------------------- ";
 
				// (3) Loop through all mesh [i]'s vertices
				// ---------------------------------------------------
				for (unsigned int i2 = 0; i2 < mesh->mNumVertices; ++i2)
				{
					glm::vec3 position{};
					position.x = mesh->mVertices[i2].x;
					position.y = mesh->mVertices[i2].y;
					position.z = mesh->mVertices[i2].z;
					mesh_list[i].vert_positions.push_back(position);
 
					std::cout << "\n   Count: " << i2;
					std::cout << "\n   mesh->mVertices[" << i2 << "].x: " << position.x;
					std::cout << "\n   mesh->mVertices[" << i2 << "].y: " << position.y;
					std::cout << "\n   mesh->mVertices[" << i2 << "].z: " << position.z;
 
					if (mesh->HasNormals())
					{
						glm::vec3 normal{};
						normal.x = mesh->mNormals[i2].x;
						normal.y = mesh->mNormals[i2].y;
						normal.z = mesh->mNormals[i2].z;
						mesh_list[i].vert_normals.push_back(normal);
						std::cout << "\n   mesh->mNormals[" << i2 << "] X: " << normal.x << " Y: " << normal.y << " Z: " << normal.z;
					}
					else
						mesh_list[i].vert_normals.push_back(glm::vec3(0.0f, 0.0f, 0.0f));
 
					if (mesh->HasTextureCoords(0)) // Above for loop: AI_MAX_NUMBER_OF_TEXTURECOORDS reveals that only slot [0] is in question.
					{
						glm::vec2 tex_coords{};
						tex_coords.x = mesh->mTextureCoords[0][i2].x;
						tex_coords.y = mesh->mTextureCoords[0][i2].y;
						mesh_list[i].tex_coords.push_back(tex_coords);
						std::cout << "\n   mesh->mTextureCoords[0][" << i2 << "] X: " << tex_coords.x << " Y: " << tex_coords.y;
					}
					else
						mesh_list[i].tex_coords.push_back(glm::vec2(0.0f, 0.0f));
				}
				std::cout << "\n\n   mesh->mNumFaces: " << mesh->mNumFaces << "\n";
				std::cout << "   ------------------ ";
 
				// (4) Loop through all mesh [i]'s Indices
				// --------------------------------------------------
				for (unsigned int i3 = 0; i3 < mesh->mNumFaces; ++i3)
				{
					std::cout << "\n";
					for (unsigned int i4 = 0; i4 < mesh->mFaces[i3].mNumIndices; ++i4)
					{
						std::cout << "   mesh->mFaces[" << i3 << "].mIndices[" << i4 << "]: " << mesh->mFaces[i3].mIndices[i4] << "\n";
						mesh_list[i].vert_indices.push_back(mesh->mFaces[i3].mIndices[i4]);
						++total_num_indices;
					}
				}
				std::cout << "\n   Total number of indices: " << total_num_indices;
				std::cout << "\n   **************************\n";
				total_num_indices = 0;
 
				set_buffer_data(i); // Set up: VAO, VBO and EBO.
			}
			// Look to see if each mesh's texture handle corresponds correctly to the loaded image
			// ----------------------------------------------------------------------------------------------------------------
			std::cout << "\n   Look to see if each mesh's texture handle corresponds correctly to the loaded image";
			std::cout << "\n   -----------------------------------------------------------------------------------\n";
			if (texture_list.size() > 0)
				for (unsigned int i = 0; i < texture_list.size(); ++i)
				{
					std::cout << "   image_list[" << i << "].imageID: " << texture_list[i].tex_handle << "... image_list[" << i << "].image_name: " << texture_list[i].image_name << "\n";
 
					for (unsigned int i2 = 0; i2 < num_meshes; ++i2)
						if (texture_list[i].tex_handle == mesh_list[i2].tex_handle)
							std::cout << "   mesh_list[" << i2 << "].tex_handle: " << mesh_list[i2].tex_handle << "\n";
					std::cout << "\n";
				}
			else
				std::cout << "   ***** No images have been loaded\n\n";
		}
	}
 
	void load_file_name_images(std::vector<Texture>& texture_list)
	{
		if (texture_list.size() > 0)
		{
			for (unsigned int i = 0; i < texture_list.size(); ++i)
			{
				bool load_success = false;
				unsigned int texture_handle = load_texture_image(texture_list[i].image_name, load_success);
 
				if (load_success)
				{
					texture_list[i].tex_handle = texture_handle// Parameter: "& texture_list" received by reference and changed here.
					std::cout << " --- load_file_name_images() Successful: " << texture_list[i].image_name << "\n";
				}
				else
					std::cout << " --- load_file_name_images() Failed: " << texture_list[i].image_name << "\n";
			}
		}
		else
			std::cout << "   ***** No images have been loaded\n\n";
	}
 
	void populate_sampler_array_setup(std::vector<Texture>& texture_list)
	{
		for (unsigned int i = 0; i < texture_list.size(); ++i)
		{
			std::string index_num = std::to_string(i);
			std::string sampler_name = "images[" + index_num + "]";
 
			// Sampler location set below is received here via texture list (by reference)
			// ------------------------------------------------------------------------------------------------
			texture_list[i].sampler_location = glGetUniformLocation(shader.ID, sampler_name.c_str());
		}
	}
 
	void combine_meshes_5VBO()
	{		
		std::cout << "   Analysing combined mesh data\n";
		std::cout << "   ----------------------------";
 
		unsigned int offset = 0;
		for (unsigned int i = 0; i < num_meshes; ++i// Combine multiple mesh data into 1 set of vectors.
		{
			// A straightforward copy of the positions, normals and texture coordinates
			// ------------------------------------------------------------------------------------------------
			meshes_5VBO_combined.vert_positions.insert(meshes_5VBO_combined.vert_positions.end(), mesh_list[i].vert_positions.begin(), mesh_list[i].vert_positions.end());
			meshes_5VBO_combined.vert_normals.insert(meshes_5VBO_combined.vert_normals.end(), mesh_list[i].vert_normals.begin(), mesh_list[i].vert_normals.end());
			meshes_5VBO_combined.tex_coords.insert(meshes_5VBO_combined.tex_coords.end(), mesh_list[i].tex_coords.begin(), mesh_list[i].tex_coords.end());			
 
			for (unsigned int i2 = 0; i2 < mesh_list[i].vert_positions.size(); ++i2// 1 mesh number & 1 sampler position per vertex.
			{
				meshes_5VBO_combined.mesh_num.push_back(mesh_list[i].mesh_num); // Use for identifying and transforming meshes independently.
 
				bool match_found = false;
				for (unsigned int i3 = 0; i3 < texture_list.size(); ++i3)
					if (texture_list[i3].tex_handle == mesh_list[i].tex_handle) // Compare mesh handle to texture list handle.
					{
						match_found = true;
						meshes_5VBO_combined.sampler_array_pos.push_back(i3);
					}
				if (!match_found// Note: without assigning some/any pos here in case any of the model meshes don't have a texture applied, then the size of this "sampler_array_pos" results in...
					meshes_5VBO_combined.sampler_array_pos.push_back(0); // ...being too small, and from testing, not surprisingly, the sampler array pos then no longer corresponds correctly.
			}
			for (unsigned int i2 = 0; i2 < mesh_list[i].vert_indices.size(); ++i2// Offset by the total number of vertices in previous meshes.
				meshes_5VBO_combined.vert_indices.push_back(mesh_list[i].vert_indices[i2] + offset);				
						
			offset += (unsigned int)mesh_list[i].vert_positions.size(); // Unsigned 32 bit is still over 2 billion.
			// std::cout << "\n   Indices offset: " << offset;
		}				
		std::cout << "\n\n   Meshes combined into vector lists\n";
		std::cout << "   ---------------------------------\n";
		std::cout << "   meshes_5VBO_combined.vert_positions.size(): " << meshes_5VBO_combined.vert_positions.size() << "\n";
		std::cout << "   meshes_5VBO_combined.vert_normals.size(): " << meshes_5VBO_combined.vert_normals.size() << "\n";
		std::cout << "   meshes_5VBO_combined.tex_coords.size(): " << meshes_5VBO_combined.tex_coords.size() << "\n";
		std::cout << "   meshes_5VBO_combined.mesh_num.size(): " << meshes_5VBO_combined.mesh_num.size() << "\n";
		std::cout << "   meshes_5VBO_combined.vert_indices.size(): " << meshes_5VBO_combined.vert_indices.size() << "\n\n";
	}
 
	void combine_meshes_1VBO()
	{	
		std::cout << "   Analysing combined mesh data\n";
		std::cout << "   ----------------------------";
 
		Vertex data{};
		data.sampler_array_pos = 0; // Set pos to 0 for when mesh has no texture (See note further down: "data.sampler_array_pos = i3; // Unlike for...")
		unsigned int offset = 0;
 
		for (unsigned int i = 0; i < num_meshes; ++i)
		{			
			for (unsigned int i2 = 0; i2 < mesh_list[i].vert_positions.size(); ++i2// Add attribute values for every vertex entry.
			{
				data.mesh_num = mesh_list[i].mesh_num; // Use for identifying and transforming meshes independently.
 
				data.vert_position = mesh_list[i].vert_positions[i2];
				data.vert_normal = mesh_list[i].vert_normals[i2];
				data.tex_coords = mesh_list[i].tex_coords[i2];
 
				for (unsigned int i3 = 0; i3 < texture_list.size(); ++i3)
					if (texture_list[i3].tex_handle == mesh_list[i].tex_handle) // Compare mesh handle to texture list handle.
						data.sampler_array_pos = i3// Unlike for "meshes_5VBO_combined.sampler_array_pos.push_back(0)", set pos to 0 further up, for if mesh has no texture.	
 
				meshes_1VBO_combined.vertex_data.push_back(data); // Add vertex to meshes combined. 	
			}
			for (unsigned int i2 = 0; i2 < mesh_list[i].vert_indices.size(); ++i2// Offset by the total number of vertices in previous meshes.			
				meshes_1VBO_combined.vert_indices.push_back(mesh_list[i].vert_indices[i2] + offset);
			
			offset += (unsigned int)mesh_list[i].vert_positions.size(); // Unsigned 32 bit is still over 2 billion.
			// std::cout << "\n   Indices offset: " << offset;
		}	
		std::cout << "\n\n   Meshes combined into struct lists\n";
		std::cout << "   ---------------------------------\n";
		std::cout << "   meshes_1VBO_combined.vertex_data.size(): " << meshes_1VBO_combined.vertex_data.size() << "\n\n";
	}
 
	void set_buffer_data(unsigned int index)
	{
		glGenVertexArrays(1, &mesh_list[index].VAO);
		glGenBuffers(1, &mesh_list[index].VBO1);
		glGenBuffers(1, &mesh_list[index].VBO2);
		glGenBuffers(1, &mesh_list[index].VBO3);
		glGenBuffers(1, &mesh_list[index].EBO);
 
		glBindVertexArray(mesh_list[index].VAO);		
 
		// Vertex Positions
		// ---------------------
		glBindBuffer(GL_ARRAY_BUFFER, mesh_list[index].VBO1);
		glBufferData(GL_ARRAY_BUFFERsizeof(glm::vec3) * mesh_list[index].vert_positions.size(), &mesh_list[index].vert_positions[0], GL_STATIC_DRAW);
		
		glEnableVertexAttribArray(0); // Void pointer below is for legacy reasons. Two possible meanings: "offset for buffer objects" & "address for client state arrays"
		glVertexAttribPointer(0, 3, GL_FLOATGL_FALSE, 3 * sizeof(float), (void*)0);
		
		// Vertex Normals
		// --------------------
		glBindBuffer(GL_ARRAY_BUFFER, mesh_list[index].VBO2);
		glBufferData(GL_ARRAY_BUFFERsizeof(glm::vec3) * mesh_list[index].vert_normals.size(), &mesh_list[index].vert_normals[0], GL_STATIC_DRAW);
 
		glEnableVertexAttribArray(1);
		glVertexAttribPointer(1, 3, GL_FLOATGL_FALSE, 3 * sizeof(float), (void*)0);
 
		// Texture Coordinates
		// ---------------------------
		glBindBuffer(GL_ARRAY_BUFFER, mesh_list[index].VBO3);
		glBufferData(GL_ARRAY_BUFFERsizeof(glm::vec2) * mesh_list[index].tex_coords.size(), &mesh_list[index].tex_coords[0], GL_STATIC_DRAW);
 
		glEnableVertexAttribArray(2);
		glVertexAttribPointer(2, 2, GL_FLOATGL_FALSE, 2 * sizeof(float), (void*)0);
		
		// Indices for: glDrawElements()
		// ---------------------------------------
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh_list[index].EBO);
		glBufferData(GL_ELEMENT_ARRAY_BUFFERsizeof(unsigned int) * mesh_list[index].vert_indices.size(), &mesh_list[index].vert_indices[0], GL_STATIC_DRAW);
 
		glBindVertexArray(0); // Unbind VAO
	}
 
	void set_buffer_data_combined_5VBO()
	{
		glGenVertexArrays(1, &meshes_5VBO_combined.VAO);
		glGenBuffers(1, &meshes_5VBO_combined.VBO1); // Alternative to using 5 separate VBOs... see function: set_buffer_data_combined_1VBO()
		glGenBuffers(1, &meshes_5VBO_combined.VBO2);
		glGenBuffers(1, &meshes_5VBO_combined.VBO3);
		glGenBuffers(1, &meshes_5VBO_combined.VBO4);
		glGenBuffers(1, &meshes_5VBO_combined.VBO5);
		glGenBuffers(1, &meshes_5VBO_combined.EBO);
 
		glBindVertexArray(meshes_5VBO_combined.VAO);
 
		// Vertex Positions
		// ---------------------
		glBindBuffer(GL_ARRAY_BUFFER, meshes_5VBO_combined.VBO1);
		glBufferData(GL_ARRAY_BUFFERsizeof(glm::vec3) * meshes_5VBO_combined.vert_positions.size(), &meshes_5VBO_combined.vert_positions[0], GL_STATIC_DRAW);
 
		glEnableVertexAttribArray(0); // Void pointer below is for legacy reasons. Two possible meanings: "offset for buffer objects" & "address for client state arrays"
		glVertexAttribPointer(0, 3, GL_FLOATGL_FALSE, 3 * sizeof(float), (void*)0);
 
		// Vertex Normals
		// --------------------
		glBindBuffer(GL_ARRAY_BUFFER, meshes_5VBO_combined.VBO2);
		glBufferData(GL_ARRAY_BUFFERsizeof(glm::vec3) * meshes_5VBO_combined.vert_normals.size(), &meshes_5VBO_combined.vert_normals[0], GL_STATIC_DRAW);
 
		glEnableVertexAttribArray(1);
		glVertexAttribPointer(1, 3, GL_FLOATGL_FALSE, 3 * sizeof(float), (void*)0);
 
		// Texture Coordinates
		// ---------------------------
		glBindBuffer(GL_ARRAY_BUFFER, meshes_5VBO_combined.VBO3);
		glBufferData(GL_ARRAY_BUFFERsizeof(glm::vec2) * meshes_5VBO_combined.tex_coords.size(), &meshes_5VBO_combined.tex_coords[0], GL_STATIC_DRAW);
 
		glEnableVertexAttribArray(2);
		glVertexAttribPointer(2, 2, GL_FLOATGL_FALSE, 2 * sizeof(float), (void*)0);
 
		// Mesh Number
		// -------------------
		glBindBuffer(GL_ARRAY_BUFFER, meshes_5VBO_combined.VBO4);
		glBufferData(GL_ARRAY_BUFFERsizeof(unsigned int) * meshes_5VBO_combined.mesh_num.size(), &meshes_5VBO_combined.mesh_num[0], GL_STATIC_DRAW);
 
		glEnableVertexAttribArray(3);
		glVertexAttribIPointer(3, 1, GL_UNSIGNED_INT, 0, (void*)0); // (Notice the "I")
 
		// Sampler Array Position
		// ------------------------------
		glBindBuffer(GL_ARRAY_BUFFER, meshes_5VBO_combined.VBO5);
		glBufferData(GL_ARRAY_BUFFERsizeof(unsigned int) * meshes_5VBO_combined.sampler_array_pos.size(), &meshes_5VBO_combined.sampler_array_pos[0], GL_STATIC_DRAW);
 
		glEnableVertexAttribArray(4);
		glVertexAttribIPointer(4, 1, GL_UNSIGNED_INT, 0, (void*)0); // (Notice the "I")
 
		// Indices for: glDrawElements()
		// ---------------------------------------
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, meshes_5VBO_combined.EBO);
		glBufferData(GL_ELEMENT_ARRAY_BUFFERsizeof(unsigned int) * meshes_5VBO_combined.vert_indices.size(), &meshes_5VBO_combined.vert_indices[0], GL_STATIC_DRAW);
 
		glBindVertexArray(0); // Unbind VAO
	}
 
	void set_buffer_data_combined_1VBO()
	{
		glGenVertexArrays(1, &meshes_1VBO_combined.VAO);
		glGenBuffers(1, &meshes_1VBO_combined.VBO);
		glGenBuffers(1, &meshes_1VBO_combined.EBO);
 
		glBindVertexArray(meshes_1VBO_combined.VAO);
 
		// Vertex Positions
		// ---------------------
		glBindBuffer(GL_ARRAY_BUFFER, meshes_1VBO_combined.VBO);
		glBufferData(GL_ARRAY_BUFFER, meshes_1VBO_combined.vertex_data.size() * sizeof(Vertex), &meshes_1VBO_combined.vertex_data[0], GL_STATIC_DRAW);
 
		glEnableVertexAttribArray(0); // Void pointer below is for legacy reasons. Two possible meanings: "offset for buffer objects" & "address for client state arrays"
		glVertexAttribPointer(0, 3, GL_FLOATGL_FALSEsizeof(Vertex), (void*)0);		
 
		// Vertex Normals
		// --------------------
		glEnableVertexAttribArray(1);
		glVertexAttribPointer(1, 3, GL_FLOATGL_FALSEsizeof(Vertex), (void*)offsetof(Vertex, vert_normal));
 
		// Texture Coordinates
		// ---------------------------
		glEnableVertexAttribArray(2);
		glVertexAttribPointer(2, 2, GL_FLOATGL_FALSEsizeof(Vertex), (void*)offsetof(Vertex, tex_coords));
 
		// Mesh Number
		// -------------------
		glEnableVertexAttribArray(3);
		glVertexAttribIPointer(3, 1, GL_UNSIGNED_INTsizeof(Vertex), (void*)offsetof(Vertex, mesh_num)); // Notice the "I"
 
		// Sampler Array Position
		// ------------------------------	
		glEnableVertexAttribArray(4);
		glVertexAttribIPointer(4, 1, GL_UNSIGNED_INTsizeof(Vertex), (void*)offsetof(Vertex, sampler_array_pos)); // Notice the "I"
		
		// Indices for: glDrawElements()
		// ---------------------------------------
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, meshes_1VBO_combined.EBO);
		glBufferData(GL_ELEMENT_ARRAY_BUFFERsizeof(unsigned int) * meshes_1VBO_combined.vert_indices.size(), &meshes_1VBO_combined.vert_indices[0], GL_STATIC_DRAW);	
 
		glBindVertexArray(0); // Unbind VAO		
	}
 
	int is_image_loaded(std::string image_path)
	{		
		for (unsigned int i = 0; i < texture_list.size(); ++i)	
			if (image_path.compare(texture_list[i].image_name) == 0)
				return texture_list[i].tex_handle;
		return -1;
	}
 
	unsigned int load_texture_image(std::string image_pathboolload_complete)
	{
		// stbi_set_flip_vertically_on_load(1); // Call this function if the image is upside-down.		
 
		std::size_t position = image_path.find_last_of("\\");
		image_path = "Images\\" + image_path.substr(position + 1);		
 
		int widthheightnum_components;
		unsigned charimage_data = stbi_load(image_path.c_str(), &width, &height, &num_components, 0);
 
		unsigned int tex_handle;
		glGenTextures(1, &tex_handle);		
 
		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_2Dtex_handle);
			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_2DGL_TEXTURE_MIN_FILTERGL_LINEAR_MIPMAP_LINEAR);
				// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
				// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_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.
 
			load_complete = true;
			stbi_image_free(image_data);			
			std::cout << "   Image loaded OK: " << image_path;
		}
		else
		{
			load_complete = false;
			stbi_image_free(image_data);
			std::cout << "   Image failed to load: " << image_path;
		}
		return tex_handle;
	}	
 
	void draw_multiple_meshes()
	{
		glUniform1i(meshes_combined_loc, 0);
		glUniform1i(draw_multiple_meshes_sampler_pos0_val, 0); // Make sure sampler array is at position 0: image[0] used in fragment shader, is set to 0.
 
		glActiveTexture(GL_TEXTURE0);
 
		unsigned int i = 0;
		for (; i < num_meshes; ++i)
		{
			glUniform1i(rendering_multiple_meshes_loc, i);
 
			glBindTexture(GL_TEXTURE_2D, mesh_list[i].tex_handle); // Bind texture for the current mesh.	
 
			glBindVertexArray(mesh_list[i].VAO);
			glDrawElements(GL_TRIANGLES, (GLsizei)mesh_list[i].vert_indices.size(), GL_UNSIGNED_INT, 0);
			glBindVertexArray(0);
		}
		show_FPS("Drawing via: draw_multiple_meshes()"i); // Passing "i" which is the number of draw calls.
	}
 
	void draw_meshes_combined_5VBO()
	{
		glUniform1i(meshes_combined_loc, 1); // If-statement in fragment shader.
		glUniform1i(rendering_multiple_meshes_loc, -1);
 
		if (binary_version_found)
			populate_sampler_array(meshes_5VBO_combined.texture_list);
		else
			populate_sampler_array(texture_list);	
 
		glBindVertexArray(meshes_5VBO_combined.VAO);
		glDrawElements(GL_TRIANGLES, (GLsizei)meshes_5VBO_combined.vert_indices.size(), GL_UNSIGNED_INT, 0);
		glBindVertexArray(0);
 
		show_FPS("Drawing via: draw_meshes_combined_5VBO()", 1);
	}
 
	void draw_meshes_combined_1VBO()
	{
		glUniform1i(meshes_combined_loc, 1); // If-statement in fragment shader.
		glUniform1i(rendering_multiple_meshes_loc, -1);
 
		if (binary_version_found)
			populate_sampler_array(meshes_1VBO_combined.texture_list);
		else
			populate_sampler_array(texture_list);	
 
		glBindVertexArray(meshes_1VBO_combined.VAO);
		glDrawElements(GL_TRIANGLES, (GLsizei)meshes_1VBO_combined.vert_indices.size(), GL_UNSIGNED_INT, 0);
		glBindVertexArray(0);
 
		show_FPS("Drawing via: draw_meshes_combined_1VBO()", 1);
	}
 
	void populate_sampler_array(std::vector<Texturetexture_list)
	{
		for (unsigned int i = 0; i < texture_list.size(); ++i)
		{
			glActiveTexture(GL_TEXTURE0 + i);
			glBindTexture(GL_TEXTURE_2Dtexture_list[i].tex_handle);
 
			glUniform1i(texture_list[i].sampler_location, i);
		}
	}
 
	void show_FPS(const chardisplay_draw_methodunsigned int num_draw_calls)
	{
		prev_time = time;
		time = glfwGetTime();
		float delta_time = (float)(time - prev_time);	
 
		++skip; // Avoids std::cout slowing down the program too much. 
		if (skip == 60) // 60 Is simply so that for VSync 60FPS it displays here once per second (Any number is OK)
		{
			skip = 0;
			std::cout << "   FPS: " << ((1.0f / 60) / delta_time) * 60 << " --- " << display_draw_method 
				<< " --- Number of draw calls: " << num_draw_calls << "\n";
		}
	}
 
	bool look_for_model_5VBO(std::string read_path)
	{		
		bool file_not_found = false;
 
		std::ifstream vert_data// Read data.
 
		std::size_t position = read_path.find_last_of("\\");
		std::string model_name = read_path.substr(position + 1);
		
		vert_data.open("Model Data 5VBO/" + model_name + "_vert_positions.bin", std::ios::in);
		std::cout << "   Detecting: "  << model_name + "_vert_positions.bin: " << vert_data.is_open() << "\n";
		if (!vert_data.is_open()) { file_not_found = true; }
		vert_data.close();
 
		vert_data.open("Model Data 5VBO/" + model_name + "_vert_normals.bin", std::ios::in);
		std::cout << "   Detecting: "  << model_name + "_vert_normals.bin: " << vert_data.is_open() << "\n";
		if (!vert_data.is_open()) { file_not_found = true; }
		vert_data.close();
 
		vert_data.open("Model Data 5VBO/" + model_name + "_tex_coords.bin", std::ios::in);
		std::cout << "   Detecting: "  << model_name + "_tex_coords.bin: " << vert_data.is_open() << "\n";
		if (!vert_data.is_open()) { file_not_found = true; }
		vert_data.close();
 
		vert_data.open("Model Data 5VBO/" + model_name + "_vert_indices.bin", std::ios::in);
		std::cout << "   Detecting: "  << model_name + "_vert_indices.bin: " << vert_data.is_open() << "\n";
		if (!vert_data.is_open()) { file_not_found = true; }
		vert_data.close();
 
		vert_data.open("Model Data 5VBO/" + model_name + "_mesh_num.bin", std::ios::in);
		std::cout << "   Detecting: "  << model_name + "_mesh_num.bin: " << vert_data.is_open() << "\n";
		if (!vert_data.is_open()) { file_not_found = true; }
		vert_data.close();
 
		vert_data.open("Model Data 5VBO/" + model_name + "_sampler_array_pos.bin", std::ios::in);
		std::cout << "   Detecting: "  << model_name + "_sampler_array_pos.bin: " << vert_data.is_open() << "\n";
		if (!vert_data.is_open()) { file_not_found = true; }
		vert_data.close();
 
		vert_data.open("Model Data 5VBO/" + model_name + "_image_names.txt", std::ios::in);
		std::cout << "   Detecting: "  << model_name + "_image_names.txt: " << vert_data.is_open() << "\n";
		if (!vert_data.is_open()) { file_not_found = true; }
		vert_data.close();
		
		if (file_not_found)
			return false;
 
		return true;
	}
 
	void write_model_files_5VBO(std::string write_path)
	{
		std::string file_name;
		std::ofstream vert_data// Write data.
 
		std::size_t position = write_path.find_last_of("\\");
		std::string model_name = write_path.substr(position + 1);
 
		file_name = "Model Data 5VBO/" + model_name + "_vert_positions.bin";
		vert_data.open(file_name, std::ios::out | std::ios::binary);
		vert_data.write(reinterpret_cast<char*>(&meshes_5VBO_combined.vert_positions[0]), meshes_5VBO_combined.vert_positions.size() * 3 * sizeof(float));
		vert_data.close();		
 
		file_name = "Model Data 5VBO/" + model_name + "_vert_normals.bin";
		vert_data.open(file_name, std::ios::out | std::ios::binary);
		vert_data.write(reinterpret_cast<char*>(&meshes_5VBO_combined.vert_normals[0]), meshes_5VBO_combined.vert_normals.size() * 3 * sizeof(float));
		vert_data.close();
 
		file_name = "Model Data 5VBO/" + model_name + "_tex_coords.bin";
		vert_data.open(file_name, std::ios::out | std::ios::binary);
		vert_data.write(reinterpret_cast<char*>(&meshes_5VBO_combined.tex_coords[0]), meshes_5VBO_combined.tex_coords.size() * 2 * sizeof(float));
		vert_data.close();
 
		file_name = "Model Data 5VBO/" + model_name + "_vert_indices.bin";
		vert_data.open(file_name, std::ios::out | std::ios::binary);
		vert_data.write(reinterpret_cast<char*>(&meshes_5VBO_combined.vert_indices[0]), meshes_5VBO_combined.vert_indices.size() * sizeof(unsigned int));
		vert_data.close();
 
		file_name = "Model Data 5VBO/" + model_name + "_mesh_num.bin";
		vert_data.open(file_name, std::ios::out | std::ios::binary);
		vert_data.write(reinterpret_cast<char*>(&meshes_5VBO_combined.mesh_num[0]), meshes_5VBO_combined.mesh_num.size() * sizeof(unsigned int));
		vert_data.close();
 
		file_name = "Model Data 5VBO/" + model_name + "_sampler_array_pos.bin";
		vert_data.open(file_name, std::ios::out | std::ios::binary);
		vert_data.write(reinterpret_cast<char*>(&meshes_5VBO_combined.sampler_array_pos[0]), meshes_5VBO_combined.sampler_array_pos.size() * sizeof(unsigned int));
		vert_data.close();		
		
		std::string names_to_file;
		for (unsigned int i = 0; i < texture_list.size(); ++i)
		{
			names_to_file += texture_list[i].image_name;
			names_to_file += "\n";			
		}		
			// file_name = "Model Data 5VBO/" + model_name + "_image_names.bin";
			// vert_data.open(file_name, std::ios::out | std::ios::binary);
		file_name = "Model Data 5VBO/" + model_name + "_image_names.txt";
		vert_data.open(file_name, std::ios::out);
		vert_data.write(reinterpret_cast<char*>(&names_to_file[0]), names_to_file.size() * sizeof(char));
		vert_data.close();
	}
 
	void read_model_files_5VBO(std::string read_path)
	{
		std::string file_name;
		std::ifstream vert_data// Read data.
		size_t vector_bytes_size = 0;
		size_t vector_index_size = 0;
 
		std::size_t position = read_path.find_last_of("\\");
		std::string model_name = read_path.substr(position + 1);
		file_name = model_name;
 
		// -------------------
 
		file_name = "Model Data 5VBO/" + model_name + "_vert_positions.bin";
		vert_data.open(file_name, std::ios::in | std::ios::binary | std::ios::ate);		
		
		vector_bytes_size = vert_data.tellg();
		vector_index_size = vector_bytes_size / (3 * sizeof(float));		
 
		meshes_5VBO_combined.vert_positions.resize(vector_index_size);
 
		vert_data.seekg(std::ios::beg);
		vert_data.read(reinterpret_cast<char*>(&meshes_5VBO_combined.vert_positions[0]), vector_bytes_size);
		vert_data.close();	
 
		// -------------------
 
		file_name = "Model Data 5VBO/" + model_name + "_vert_normals.bin";
		vert_data.open(file_name, std::ios::in | std::ios::binary | std::ios::ate);
 
		vector_bytes_size = vert_data.tellg();
		vector_index_size = vector_bytes_size / (3 * sizeof(float));
 
		meshes_5VBO_combined.vert_normals.resize(vector_index_size);
 
		vert_data.seekg(std::ios::beg);
		vert_data.read(reinterpret_cast<char*>(&meshes_5VBO_combined.vert_normals[0]), vector_bytes_size);
		vert_data.close();
 
		// -------------------
 
		file_name = "Model Data 5VBO/" + model_name + "_tex_coords.bin";
		vert_data.open(file_name, std::ios::in | std::ios::binary | std::ios::ate);
 
		vector_bytes_size = vert_data.tellg();
		vector_index_size = vector_bytes_size / (2 * sizeof(float));
 
		meshes_5VBO_combined.tex_coords.resize(vector_index_size);
 
		vert_data.seekg(std::ios::beg);
		vert_data.read(reinterpret_cast<char*>(&meshes_5VBO_combined.tex_coords[0]), vector_bytes_size);
		vert_data.close();
 
		// -------------------
 
		file_name = "Model Data 5VBO/" + model_name + "_vert_indices.bin";
		vert_data.open(file_name, std::ios::in | std::ios::binary | std::ios::ate);
 
		vector_bytes_size = vert_data.tellg();
		vector_index_size = vector_bytes_size / sizeof(unsigned int);
 
		meshes_5VBO_combined.vert_indices.resize(vector_index_size);
 
		vert_data.seekg(std::ios::beg);
		vert_data.read(reinterpret_cast<char*>(&meshes_5VBO_combined.vert_indices[0]), vector_bytes_size);
		vert_data.close();	
 
		// -------------------
 
		file_name = "Model Data 5VBO/" + model_name + "_mesh_num.bin";
		vert_data.open(file_name, std::ios::in | std::ios::binary | std::ios::ate);
 
		vector_bytes_size = vert_data.tellg();
		vector_index_size = vector_bytes_size / sizeof(unsigned int);
 
		meshes_5VBO_combined.mesh_num.resize(vector_index_size);
 
		vert_data.seekg(std::ios::beg);
		vert_data.read(reinterpret_cast<char*>(&meshes_5VBO_combined.mesh_num[0]), vector_bytes_size);
		vert_data.close();	
 
		// -------------------
 
		file_name = "Model Data 5VBO/" + model_name + "_sampler_array_pos.bin";
		vert_data.open(file_name, std::ios::in | std::ios::binary | std::ios::ate);
 
		vector_bytes_size = vert_data.tellg();
		vector_index_size = vector_bytes_size / sizeof(unsigned int);
 
		meshes_5VBO_combined.sampler_array_pos.resize(vector_index_size);
 
		vert_data.seekg(std::ios::beg);
		vert_data.read(reinterpret_cast<char*>(&meshes_5VBO_combined.sampler_array_pos[0]), vector_bytes_size);
		vert_data.close();		
 
		// -------------------		
 
			// file_name = "Model Data 5VBO/" + model_name + "_image_names.bin";
			// vert_data.open(file_name, std::ios::in | std::ios::binary | std::ios::ate);
		file_name = "Model Data 5VBO/" + model_name + "_image_names.txt";
		vert_data.open(file_name, std::ios::in | std::ios::ate);
 
		vector_bytes_size = vert_data.tellg();
		vector_index_size = vector_bytes_size / sizeof(char);
 
		std::string names_from_file;
		names_from_file.resize(vector_index_size);		
 
		vert_data.seekg(std::ios::beg);
		vert_data.read(reinterpret_cast<char*>(&names_from_file[0]), vector_bytes_size);
		vert_data.close();
 
		const charall_names = names_from_file.c_str();
 
		std::string single_name;
		for (unsigned int i = 0; i < names_from_file.size(); ++i)
		{		
			if (*all_names != '\n')
				single_name += *all_names;
			else
			{
				Texture texture;
				texture.image_name = single_name;
				texture.tex_handle = 0; // Temporary value. Gets overridden in: load_file_name_images()
 
				meshes_5VBO_combined.texture_list.push_back(texture);				
				single_name.clear();
			}			
			++all_names;
		}
		/*for (unsigned int i = 0; i < meshes_5VBO_combined.texture_list.size(); ++i)		
			std::cout << "\n Retrieved names: " << meshes_5VBO_combined.texture_list[i].image_name << "\n";*/
	}
 
	bool look_for_model_1VBO(std::string read_path)
	{
		bool file_not_found = false;
 
		std::ifstream vert_data// Read data.
 
		std::size_t position = read_path.find_last_of("\\");
		std::string model_name = read_path.substr(position + 1);
 
		vert_data.open("Model Data 1VBO/" + model_name + "_vertex_data.bin", std::ios::in);
		std::cout << "   Detecting: "  << model_name + "_vertex_data.bin: " << vert_data.is_open() << "\n";
		if (!vert_data.is_open()) { file_not_found = true; }
		vert_data.close();		
 
		vert_data.open("Model Data 1VBO/" + model_name + "_vert_indices.bin", std::ios::in);
		std::cout << "   Detecting: "  << model_name + "_vert_indices.bin: " << vert_data.is_open() << "\n";
		if (!vert_data.is_open()) { file_not_found = true; }
		vert_data.close();	
 
		vert_data.open("Model Data 1VBO/" + model_name + "_image_names.txt", std::ios::in);
		std::cout << "   Detecting: "  << model_name + "_image_names.txt: " << vert_data.is_open() << "\n";
		if (!vert_data.is_open()) { file_not_found = true; }
		vert_data.close();
 
		if (file_not_found)
			return false;
 
		return true;
	}
 
	void write_model_files_1VBO(std::string write_path)
	{
		std::string file_name;
		std::ofstream vert_data// Write data.
 
		std::size_t position = write_path.find_last_of("\\");
		std::string model_name = write_path.substr(position + 1);		
 
		file_name = "Model Data 1VBO/" + model_name + "_vertex_data.bin";
		vert_data.open(file_name, std::ios::out | std::ios::binary);
		vert_data.write(reinterpret_cast<char*>(&meshes_1VBO_combined.vertex_data[0]), meshes_1VBO_combined.vertex_data.size() * sizeof(Vertex));
		vert_data.close();
 
		file_name = "Model Data 1VBO/" + model_name + "_vert_indices.bin";
		vert_data.open(file_name, std::ios::out | std::ios::binary);
		vert_data.write(reinterpret_cast<char*>(&meshes_1VBO_combined.vert_indices[0]), meshes_1VBO_combined.vert_indices.size() * sizeof(unsigned int));
		vert_data.close();
 
		std::string names_to_file;
		for (unsigned int i = 0; i < texture_list.size(); ++i)
		{
			names_to_file += texture_list[i].image_name;
			names_to_file += "\n";			
		}
			// file_name = "Model Data 1VBO/" + model_name + "_image_names.bin";
			// vert_data.open(file_name, std::ios::out | std::ios::binary);
		file_name = "Model Data 1VBO/" + model_name + "_image_names.txt";
		vert_data.open(file_name, std::ios::out);
		vert_data.write(reinterpret_cast<char*>(&names_to_file[0]), names_to_file.size() * sizeof(char));
		vert_data.close();
	}
 
	void read_model_files_1VBO(std::string read_path)
	{
		std::string file_name;
		std::ifstream vert_data// Read data.
		size_t vector_bytes_size = 0;
		size_t vector_index_size = 0;
 
		std::size_t position = read_path.find_last_of("\\");
		std::string model_name = read_path.substr(position + 1);
		file_name = model_name;
 
		// -------------------
 
		file_name = "Model Data 1VBO/" + model_name + "_vertex_data.bin";
		vert_data.open(file_name, std::ios::in | std::ios::binary | std::ios::ate);	
 
		vector_bytes_size = vert_data.tellg();
		vector_index_size = vector_bytes_size / (3 * sizeof(float));
 
		meshes_1VBO_combined.vertex_data.resize(vector_index_size);
 
		vert_data.seekg(std::ios::beg);
		vert_data.read(reinterpret_cast<char*>(&meshes_1VBO_combined.vertex_data[0]), vector_bytes_size);
		vert_data.close();		
 
		// -------------------
	
		file_name = "Model Data 1VBO/" + model_name + "_vert_indices.bin";
		vert_data.open(file_name, std::ios::in | std::ios::binary | std::ios::ate);
 
		vector_bytes_size = vert_data.tellg();
		vector_index_size = vector_bytes_size / sizeof(unsigned int);
 
		meshes_1VBO_combined.vert_indices.resize(vector_index_size);
 
		vert_data.seekg(std::ios::beg);
		vert_data.read(reinterpret_cast<char*>(&meshes_1VBO_combined.vert_indices[0]), vector_bytes_size);
		vert_data.close();	
 
		// -------------------		
 
			// file_name = "Model Data 1VBO/" + model_name + "_image_names.bin";
			// vert_data.open(file_name, std::ios::in | std::ios::binary | std::ios::ate);
		file_name = "Model Data 1VBO/" + model_name + "_image_names.txt";
		vert_data.open(file_name, std::ios::in | std::ios::ate);
 
		vector_bytes_size = vert_data.tellg();
		vector_index_size = vector_bytes_size / sizeof(char);
 
		std::string names_from_file;
		names_from_file.resize(vector_index_size);
 
		vert_data.seekg(std::ios::beg);
		vert_data.read(reinterpret_cast<char*>(&names_from_file[0]), vector_bytes_size);
		vert_data.close();
 
		const charall_names = names_from_file.c_str();
 
		std::string single_name;
		for (unsigned int i = 0; i < names_from_file.size(); ++i)
		{
			if (*all_names != '\n')
				single_name += *all_names;
			else
			{
				Texture texture;
				texture.image_name = single_name;
				texture.tex_handle = 0; // Temporary value. Gets overridden in: load_file_name_images()
 
				meshes_1VBO_combined.texture_list.push_back(texture);
				single_name.clear();
			}
			++all_names;
		}
	}
};

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 vec3 aNormal;
layout (location = 2) in vec2 aTexCoord;
layout (location = 3) in unsigned int aMeshNum; // Use this for identifying and transforming meshes independently.
layout (location = 4) in unsigned int aSamplerPos; // Pass this to the fragment shader.
 
out vec3 vert_pos_varying; // Vertex position coordinates passed to the fragment shader as interpolated per-vertex.
out vec3 vert_pos_transformed; // Transformed cube vertex position coordinates also passed as interpolated.
out vec3 vertex_normal;
out vec2 texture_coordinates;
flat out unsigned int mesh_number; // Can be used to identify and transform meshes independently of one another.
flat out unsigned int sampler_array_pos;
 
uniform int rendering_multiple_meshes;
 
uniform mat4 view;
uniform mat4 projection;
uniform mat4 animate;
 
void main()
{
	if (rendering_multiple_meshes == -1)
		mesh_number = aMeshNum; // Receive mesh number via input attribute.
	else
		mesh_number = rendering_multiple_meshes; // For draw method option 0... Receive mesh number via uniform.
 
	vert_pos_varying = aPos; // Send aPos vertex position values to fragment shader, which can be used as colour values instead of using texture images.
	vert_pos_transformed = vec3(animate * vec4(aPos, 1.0)); // Send transformed position values, which are used for the lighting effects.			
 
	texture_coordinates = aTexCoord;
	mesh_number = aMeshNum;
	sampler_array_pos = aSamplerPos;
 
	mat3 normal_matrix = transpose(inverse(mat3(animate)));
	vertex_normal = normal_matrix * aNormal;
	
	if (length(vertex_normal) > 0)
		vertex_normal = normalize(vertex_normal); // Never try to normalise zero vectors (0,0,0)
		
	// https://www.khronos.org/opengl/wiki/Vertex_Post-Processing
	gl_Position = projection * view * animate * 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
// -----------------------------------------------------------------------------------
in vec3 vert_pos_varying; // Vertex position coordinates received from the fragment shader as interpolated per-vertex.
in vec3 vert_pos_transformed; // Transformed cube vertex position coordinates also received as interpolated.
in vec3 vertex_normal;
in vec2 texture_coordinates;
flat in unsigned int mesh_number; // Can be used to set the fragment colours of meshes independently of one another.
flat in unsigned int sampler_array_pos; // Used to select the correct image from the images[] array.
 
uniform bool meshes_combined;
uniform sampler2D images[32]; // Array of sampler images.
uniform vec3 camera_position; // -Z is into the screen... camera_position is set in main() on CPU side.
 
void main()
{
	// This initial fragment colour is simply overridden via the Phong lighting "fragment_colour = " further down (But is used via "fragment... +=")
	// ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
	// fragment_colour = vec4(abs(vert_pos_varying) / 1.25, 1.0);
	// fragment_colour = vec4(abs(vert_pos_transformed) / 2.95, 1.0); // 3.75
	
	vec3 view_direction = normalize(camera_position - vert_pos_transformed);
 
	vec3 light_position = vec3(0.0, 5.0, 0.0); // A position used as a light source acts as a point light (Not a directional light)
	vec3 light_direction = normalize(vec3(light_position - vert_pos_transformed));
	
	unsigned int index = 0;
	// --------------------------
	if (meshes_combined)
		index = sampler_array_pos;
	
	vec4 image_colour = texture(images[index], texture_coordinates);
 
	float ambient_factor = 0.55; // Intensity multiplier.
	vec4 ambient_result = vec4(ambient_factor * image_colour.rgb, 1.0);
 
	// Perpendicular vectors dot product = 0
	// Parallel vectors in same direction dot product = 1
	// Parallel vectors in opposite direction dot product = -1
	// ----------------------------------------------------------------------
	float diffuse_factor = 0.75;
	float diffuse_angle = max(dot(light_direction, vertex_normal), -0.05); // [-1.0 to 0] down to -1 results in darker lighting past 90 degrees.
	vec4 diffuse_result =  vec4(diffuse_factor * diffuse_angle * image_colour.rgb, 1.0);	
		
	vec3 specular_colour = vec3(0.5, 0.5, 0.5);
	vec3 reflect_direction = normalize(reflect(-light_direction, vertex_normal)); // Light direction is negated here.
	float specular_strength = pow(max(dot(view_direction, reflect_direction), 0), 32);
	vec4 specular_result = vec4(specular_colour * specular_strength, 1.0);
 
	fragment_colour = ambient_result + diffuse_result + specular_result;
		// fragment_colour = ambient_result;
		// fragment_colour = diffuse_result;
		// fragment_colour = specular_result;
		// fragment_colour = ambient_result + diffuse_result;
		// fragment_colour = ambient_result + specular_result;
	
	// fragment_colour = image_colour; // Enable this to have no lighting effects.
	
	// Comment all the above "fragment_colour =" if using these
	// -----------------------------------------------------------------------------
	// fragment_colour += diffuse_result; // Adds image texture to the initial vert_pos... varying or transformed colour values.
	// fragment_colour += specular_result;
}