OpenGL Tutorial 10 (QS) – Interpolation & Extrapolation – VSync & Timers

If you want your game to display smoothly on all PC monitors, but also provide a consistent gameplay experience with respect to the physics routines, then you cannot...

  1. Achieve smooth animation by simply allowing your physics routines to run at the VSync refresh rate, because that will cause the game the run faster or slower depending on the monitor.
  2. Overcome issue 1 above by using an ordinary timer, because that will cause frame stuttering or frame skipping.

Instead, within the main loop… you need to either interpolate or extrapolate the positional, rotational, and scaling values of the game objects during each main loop cycle, such as buildings, trees, people, etc, and then use those values for rendering, and only update the actual/master copies of those values when it’s time for the physics routines to execute once again.

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/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>
 
#include <thread>
#include <atomic>
#include <mutex>
 
#include "load_model_meshes.h"
#include "shader_configure.h" // Used to create the shaders.
 
#include "interp_extrap_1_thread.h"
#include "interp_extrap_2_threads.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.5f); // Window size will be 50% 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"Interpolation & Extrapolation - Using 1 or 2 Threads"NULLNULL);
	// GLFWwindow* window = glfwCreateWindow(window_width, window_height, "Interpolation & Extrapolation - Using 1 or 2 Threads", 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);	
 
	// (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 shader(vert_shaderfrag_shader);
	shader.use();
 
	// (4) Set Camera Parameters
	// -----------------------------------
	unsigned int view_matrix_loc = glGetUniformLocation(shader.ID, "view");
	unsigned int projection_matrix_loc = glGetUniformLocation(shader.ID, "projection");
	unsigned int camera_position_loc = glGetUniformLocation(shader.ID, "camera_position");
 
	glm::vec3 camera_position(0.0f, 0.0f, 20.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(35.0f), (float)window_width / (float)window_height, 3.0f, 50.0f);
	glUniformMatrix4fv(projection_matrix_loc, 1, GL_FALSE, glm::value_ptr(projection));	
 
	// (5) Enter the Main-Loop
	// --------------------------------
	srand((unsigned)time(NULL)); // Initialise random seed.	
	
	Interp_1_Thread one_thread(windowshader);
 
	/*std::atomic_bool thread_running = true;
	Interp_2_Threads two_threads(window, shader, thread_running);
	std::thread thread_object;
	two_threads.start_physics_thread(thread_object);*/
	
	while (!glfwWindowShouldClose(window)) // Main-Loop
	{
		one_thread.process_time();
		// two_threads.main_render_thread();
	}
 
	// (6) Exit the Application
	// ------------------------------
	/*thread_running = false;
	thread_object.join();*/
 
	glDeleteProgram(shader.ID);
 
	/* 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_model_meshes.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 tex_handle;
	};	
 
	struct Texture
	{
	unsigned int textureID;
		std::string image_name;
	};
 
public:
	unsigned int num_meshes;
	std::vector<Mesh> mesh_list;
	std::vector<Texture> texture_list;
 
	Model(const charmodel_path// Constructor
	{
		// http://assimp.sourceforge.net/lib_html/postprocess_8h.html (See: aiPostProcessSteps) (Flag options)
		
			// scene = importer.ReadFile(model_path, aiProcess_JoinIdenticalVertices | aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_CalcTangentSpace);
		scene = importer.ReadFile(model_pathaiProcess_JoinIdenticalVertices | aiProcess_Triangulate | aiProcess_FlipUVs);
			// scene = importer.ReadFile(model_path, aiProcess_JoinIdenticalVertices | aiProcess_FlipUVs);
			// scene = importer.ReadFile(model_path, aiProcess_Triangulate | aiProcess_FlipUVs);
			// scene = importer.ReadFile(model_path, NULL);			
 
		    load_model(); // Uncomment only one of these two load model functions.
			// load_model_cout_console();
	}
 
private:	
	void load_model()
	{
		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{};		
			int indices_offset = 0; // Not being used yet... i.e. indices_offset += mesh->mNumVertices; is commented further down.
 
			// (1) Loop through all the model's meshes
			// -----------------------------------------------------
			for (unsigned int i = 0; i < num_meshes; ++i)
			{
				mesh = scene->mMeshes[i]; // http://assimp.sourceforge.net/lib_html/structai_mesh.html				
 
				aiMaterialmaterial = scene->mMaterials[mesh->mMaterialIndex]; // http://assimp.sourceforge.net/lib_html/structai_material.html			
 
				// This loop will only run once (i.e. there's only 1 texture per mesh)
				for (unsigned int tex_count = 0; 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);
 
						if (load_success// Although do nothing if the image fails to load.
						{
							Texture texture;
							texture.image_name = string.C_Str();
							texture.textureID = texture_handle;
 
							texture_list.push_back(texture);
							mesh_list[i].tex_handle = texture_handle;
						}
					}
					else	
						mesh_list[i].tex_handle = already_loaded// Assign existing texture handle.
				}
				// (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] + indices_offset);									
				
				// indices_offset += mesh->mNumVertices; // Disabled for tutorial: Model Loading (Part 1 of 3)
 
				set_buffer_data(i); // Set up: VAO, VBO and EBO.
			}			
		}
	}	
 
	void load_model_cout_console()
	{
		// 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\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;
			int indices_offset = 0; // Not being used yet... i.e. indices_offset += mesh->mNumVertices; is commented further down.
 
			// (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 = 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;
				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);
 
						if (load_complete// Although do nothing if the image fails to load.
						{
							Texture texture;
							texture.image_name = string.C_Str();
							texture.textureID = 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)
					std::cout << "   material->GetTexture(aiTextureType_DIFFUSE, tex_count, &string): No image has been applied to this mesh\n\n";
				else
					std::cout << "\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] + indices_offset);
						++total_num_indices;
					}
				}
				std::cout << "\n   Total number of indices: " << total_num_indices;
				std::cout << "\n   **************************";
				total_num_indices = 0;
 
				// indices_offset += mesh->mNumVertices; // Disabled for tutorial: Model Loading (Part 1 of 3)
 
				std::cout << "\n   Indices offset (Total 'mesh->mNumVertices' so far): " << indices_offset;
				std::cout << "\n   *****************************************************\n\n";
 
				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
			// ----------------------------------------------------------------------------------------------------------------
			if (texture_list.size() > 0)
				for (unsigned int i = 0; i < texture_list.size(); ++i)
				{
					std::cout << "   image_list[" << i << "].imageID: " << texture_list[i].textureID << "... image_list[" << i << "].image_name: " << texture_list[i].image_name << "\n";
 
					for (unsigned int i2 = 0; i2 < num_meshes; ++i2)
						if (texture_list[i].textureID == 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";			
		}
	}
 
	void set_buffer_data(unsigned int index)
	{
		glGenVertexArrays(1, &mesh_list[index].VAO);
		glGenBuffers(1, &mesh_list[index].VBO1); // Alternative to using 3 separate VBOs, instead use only 1 VBO and set glVertexAttribPointer's offset...
		glGenBuffers(1, &mesh_list[index].VBO2); // like was done in tutorial 3... Orbiting spinning cubes.
		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
	}
 
	int is_image_loaded(std::string file_name)
	{		
		for (unsigned int i = 0; i < texture_list.size(); ++i)	
			if (file_name.compare(texture_list[i].image_name) == 0)
				return texture_list[i].textureID;
		return -1;
	}
 
	unsigned int load_texture_image(std::string file_nameboolload_complete)
	{
		// stbi_set_flip_vertically_on_load(1); // Call this function if the image is upside-down.		
 
		std::size_t position = file_name.find_last_of("\\");
		file_name = "Images\\" + file_name.substr(position + 1);		
 
		int widthheightnum_components;
		unsigned charimage_data = stbi_load(file_name.c_str(), &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_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: " << file_name << "\n";
		}
		else
		{
			load_complete = false;
			stbi_image_free(image_data);
			std::cout << "   Image failed to load: " << file_name << "\n";
		}
		return textureID;
	}
};

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

#pragma once
 
class Interp_1_Thread
{
private:
	GLFWwindow* window;
	Shader& shader;
 
	Model model; // model.obj... The_Beast_Helicopter.obj... Plane_CAP_232.obj
	
	unsigned int position_loc;
	unsigned int rotation_loc;
 
	// Simulating having a faster/slower monitor by disabling VSync (glfwSwapInterval(0)) in the constructor and using this timer instead
	// ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
	int timer_FPS_turn_vsync_off = 140; // Alternatively don't use a timer and instead set: glfwSwapInterval(1) 2, 3 etc... instead of 0
	float timer_FPS_time = 1.0f / timer_FPS_turn_vsync_off;
	float timer_delta_time = 0;
	double timer_prev_time = 0;
	int draw_calls_count = 0;
 
	// Physics Related Variables
	// ---------------------------------
	glm::vec3 object_position = glm::vec3(0, 0, 0);
	float translation_step_val = 0.1f;
	int translation_dir = 1;
 
	int physics_FPS = 45;
	float physics_FPS_time = 1.0f / physics_FPS;
	double curr_target_time = 0;
	double overlap_point = 0;
	int physics_FPS_count = 0;
	
	glm::mat4 spinning_mat = glm::mat4(1.0f);
	glm::mat4 spinning_mat_copy = glm::mat4(1.0f);
 
	float spin_speed_factor = 0.1f;
	float spin_speed = (float)(rand() % 50 + 1) * spin_speed_factor;
 
	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_vary = 0.0f;
	int spin_dir = 1;
 
public:
	Interp_1_Thread(GLFWwindowwindowShadershader) : shader(shader), model("Object & Material Files\\The_Beast_Helicopter.obj")
	{
		glfwSwapInterval(1); // *** Set VSync rate 1:1 with monitor's refresh rate (both my monitors have a VSync of 60Hz) ***
 
		this->window = window;
		position_loc = glGetUniformLocation(shader.ID, "position");
		rotation_loc = glGetUniformLocation(shader.ID, "rotation");
	}
 
	void process_time() // Called from: main.cpp
	{
		double total_elapsed_time = glfwGetTime();
		if (overlap_point == 0) // Set initial target values to eliminate unwanted skip when the program run 1st starts.
		{						
			curr_target_time = total_elapsed_time;
			spinning_mat = glm::rotate(spinning_mat, glm::radians(spin_speed), glm::normalize(glm::vec3(x_spin, y_spin, z_spin)));
		}
		timer_delta_time += (float)(total_elapsed_time - timer_prev_time);
		timer_prev_time = total_elapsed_time;
 
		// if (timer_delta_time > timer_FPS_time) // *** Comment this line if setting the swap interval value higher than 0 (glfwSwapInterval(1) 2, 3 etc ***
		{			
			// timer_delta_time -= timer_FPS_time; // = 0 would discard the remainder, whereas -= doesn't.						
			
			// Interpolation Extrapolation Start
			// -------------------------------------------
			int num_cycles = (int)((total_elapsed_time - curr_target_time) / physics_FPS_time); // For-loop below runs only when this: subtraction / physics_FPS_time is > 1				
				// std::cout << "\n\n   total_elapsed_time - curr_target_time = " << total_elapsed_time - curr_target_time << " / physics_FPS_time (" << physics_FPS_time << ") = " << num_cycles;
 
			for (int i = 0; i < num_cycles; ++i// Example: physics FPS 120... Monitor VSync (enabled) 60 FPS... Results in 2 physics cycles per monitor refresh.
				process_physics(i);
 
			float physics_time_just_ran = num_cycles * physics_FPS_time;
 
			overlap_point = curr_target_time + physics_time_just_ran;
			curr_target_time += physics_time_just_ran// Increase target time by total physics time just ran (target time never exceeds "total elapsed time")
			
			float overlap_time = (float)(total_elapsed_time - overlap_point); // Time passed since the last known target time increment.
				// std::cout << "\n   overlap_time: " << overlap_time;
 
			float interp_extrap_time = overlap_time / physics_FPS_time; // Overlap time as a percentage [0, 1] of physics FPS time.
			// ------------------------------------------
			// Interpolation Extrapolation End
		
			draw_object(interp_extrap_time);
		}
	}
 
private:
	void process_physics(int physics_loop_index)
	{		
		// std::cout << "\n   Index: " << physics_loop_index;
		std::cout << "\n\n   New Physics Frame";
 
		++physics_FPS_count;
		draw_calls_count = 0; // Reset draw calls count when a new physics cycle is executed.
 
		double time = glfwGetTime();		
 
		// std::cout << "\n   physics_loop_index: " << physics_loop_index << " --- physics_FPS_count: " << physics_FPS_count << " --- glfwGetTime(): " << time
			// << " ---  physics_FPS_count / glfwGetTime() = " << physics_FPS_count / time;
	
		object_position.x += translation_step_val * translation_dir;  // *** 1st: update position (updating after reversing results in a render skipping visual artifact) ***
		// Note: if this translation direction happened to be some significant change in direction... then extrapolating beyond the current position could result in a visual artifact.
		// Alternatively... interpolate from the previous physics value like is being done for the rotation.
 
		if (std::abs(object_position.x) > 5.75f) // 2nd: reverse direction only after having 1st applied the updated position (to catch up with extrapolated/rendered position)
			translation_dir = -translation_dir;
 
		// object_position.x += translation_step_val * translation_dir; // *** Comment the above same line... if testing to see render this skipping visual artifact ***
 
		// Randomise the Model's Spinning Speed & Axis
		// -------------------------------------------------------------
		spin_vary += 0.05f * spin_dir;
 
		if (spin_vary > 3.0f || 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) * spin_speed_factor;
		}
		spinning_mat_copy = spinning_mat;
		spinning_mat = glm::rotate(spinning_mat, glm::radians(spin_speed), glm::normalize(glm::vec3(x_spin, y_spin, z_spin)));
	}
 
	void draw_object(float interp_extrap_time)
	{
		++draw_calls_count;
 
		// Position: Extrapolate, i.e. translate beyond physics current position... rendering is ahead (adds to current physics position)
		// ----------------------------------------------------------------------------------------------------------------------------------------------------------------
		// float render_xpos = object_position.x; // No extrapolation.
		float render_xpos = object_position.x + (translation_step_val * translation_dir * interp_extrap_time); // With extrapolation.
		glm::vec3 render_pos(render_xpos, object_position.y, object_position.z);
 
		std::cout << "\n   draw_calls_count: " << draw_calls_count << " --- interp_extrap_time: " << interp_extrap_time << " --- object_position.x: " << object_position.x << " --- render_xpos: " << render_xpos;
		// std::cout << "\n   draw_calls_count: " << draw_calls_count << " --- interp_extrap_time: " << interp_extrap_time << " --- spin_speed: " << spin_speed << " --- spin_speed * interp_extrap_time: " << spin_speed * interp_extrap_time;
		 
		glm::mat4 object_matrix(1.0f);
		object_matrix = glm::translate(object_matrixrender_pos);
		glUniformMatrix4fv(position_loc, 1, GL_FALSE, glm::value_ptr(object_matrix));
 
		// Rotation: Extrapolate, i.e. rotate beyond physics matrix current rotation amount... rendering is ahead (adds to the current physics rotation matrix)
		// ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
		// glm::mat4 spinning_mat_render = spinning_mat; // No extrapolation.
		// glm::mat4 spinning_mat_render = glm::rotate(spinning_mat, glm::radians(spin_speed * interp_extrap_time), glm::normalize(glm::vec3(x_spin, y_spin, z_spin))); // With extrapolation.
		// glUniformMatrix4fv(rotation_loc, 1, GL_FALSE, glm::value_ptr(spinning_mat_render));
 
		// Rotation: Interpolate, i.e. rotate towards physics matrix current rotation amount... rendering is behind (adds to the previous physics rotation matrix)
		// ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
		// glm::mat4 spinning_mat_render = spinning_mat_copy; // No interpolation.
		glm::mat4 spinning_mat_render = glm::rotate(spinning_mat_copy, glm::radians(spin_speed * interp_extrap_time), glm::normalize(glm::vec3(x_spin, y_spin, z_spin))); // With interpolation.
		glUniformMatrix4fv(rotation_loc, 1, GL_FALSE, glm::value_ptr(spinning_mat_render));
 
		// Clear the Screen & Draw Model Meshes
		// -----------------------------------------------------
		glClearColor(0.30f, 0.55f, 0.65f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
		for (unsigned int i = 0; i < model.num_meshes; ++i)
		{
			glBindTexture(GL_TEXTURE_2D, model.mesh_list[i].tex_handle); // Bind texture for the current mesh.	
 
			glBindVertexArray(model.mesh_list[i].VAO);
			glDrawElements(GL_TRIANGLES, (GLsizei)model.mesh_list[i].vert_indices.size(), GL_UNSIGNED_INT, 0);
			glBindVertexArray(0);
		}
		glfwSwapBuffers(window);
		glfwPollEvents();		
	}
};

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

#pragma once
 
class Interp_2_Threads
{
private:
	GLFWwindow* window;
	Shader& shader;
	
	std::mutex mutex_toggle;
	std::atomic_bool& thread_running;
 
	Model model; // model_testing.obj... The_Beast_Helicopter.obj... Plane_CAP_232.obj
 
	unsigned int position_loc;
	unsigned int rotation_loc;
 
	// Simulating having a faster/slower monitor by disabling VSync (glfwSwapInterval(0)) in the constructor and using this timer instead
	// ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
	int timer_FPS_turn_vsync_off = 60; // Alternatively don't use a timer and instead set: glfwSwapInterval(1) 2, 3 etc... instead of 0
	float timer_FPS_time = 1.0f / timer_FPS_turn_vsync_off;
	float timer_delta_time = 0;
	double timer_prev_time = 0;
 
	// Physics Related Variables
	// ---------------------------------
	int physics_FPS = 25;	
	float physics_FPS_time = 1.0f / physics_FPS;
	float thread_precision_time_margin = 0.05f;
	float thread_precision_time_scaling = 1;
	double thread_time_last = 0;	
	bool physics_running = false;
	
	glm::vec3 object_position = glm::vec3(0, 0, 0);
	glm::mat4 position_matrix = glm::mat4(1.0f);
	float translation_step_val = 0.12f;
	int translation_dir = 1;
 
	glm::mat4 spinning_mat = glm::mat4(1.0f);
	glm::mat4 spinning_mat_copy = glm::mat4(1.0f);
	glm::mat4 spinning_mat_render = glm::mat4(1.0f);
 
	float spin_speed_factor = 0.07f;
	float spin_speed = (float)(rand() % 50 + 1) * spin_speed_factor;
 
	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_vary = 0.0f;
	int spin_dir = 1;
 
public:
	Interp_2_Threads(GLFWwindowwindowShadershader, std::atomic_boolthread_running) : shader(shader), thread_running(thread_running), model("Object & Material Files\\Plane_CAP_232.obj")
	{
		glfwSwapInterval(1); // *** Set VSync rate 1:1 with monitor's refresh rate ***
 
		this->window = window;
		position_loc = glGetUniformLocation(shader.ID, "position");
		rotation_loc = glGetUniformLocation(shader.ID, "rotation");
	}
 
	void start_physics_thread(std::threadthread)
	{
		thread = std::thread(&Interp_2_Threads::physics_threadthis); // Use: std::ref() as a 3rd argument if you want to pass a reference to a thread.
	}
		
	void main_render_thread()
	{
		double total_elapsed_time = glfwGetTime();
			// std::cout << "\n   total_elapsed_time: " << total_elapsed_time;
 
		timer_delta_time += (float)(total_elapsed_time - timer_prev_time);
		timer_prev_time = total_elapsed_time;
 
		// if (timer_delta_time > timer_FPS_time) // *** Comment this line if setting the swap interval value higher than 0 (glfwSwapInterval(1) 2, 3 etc ***
		{
			// timer_delta_time -= timer_FPS_time; // = 0 would discard the remainder, whereas -= doesn't.
 
			mutex_toggle.lock(); // Lock start.
 
					double time_inside_lock = glfwGetTime();
						// std::cout << "\n   time_inside_lock: " << time_inside_lock;
 
					float delta_physics_time = (float)((time_inside_lock - thread_time_last)) * thread_precision_time_scaling;
						// std::cout << "\n   delta_physics_time: " << delta_physics_time << " --- time_inside_lock: " << time_inside_lock << " --- thread_time_last: " << thread_time_last;
 
					float interp_extrap_time = delta_physics_time / physics_FPS_time; // Time as a percentage [0, 1] of physics FPS time.
						// std::cout << "\n   interp_extrap_time: " << interp_extrap_time;				
					
					// Position: Extrapolate, i.e. translate beyond physics current position... rendering is ahead (adds to current physics position)
					// ----------------------------------------------------------------------------------------------------------------------------------------------------------------
					// float render_xpos = object_position.x; // No extrapolation.
					float render_xpos = object_position.x + (translation_step_val * translation_dir * interp_extrap_time); // With extrapolation.
					glm::vec3 render_pos = glm::vec3(render_xpos, object_position.y, object_position.z);
						// std::cout << "\n   interp_extrap_time: " << interp_extrap_time << " --- object_position.x: " << object_position.x << " --- render_xpos: " << render_xpos;
 
					position_matrix = glm::mat4(1.0f);
					position_matrix = glm::translate(position_matrix, render_pos);
					
					// Rotation: Extrapolate, i.e. rotate beyond physics matrix current rotation amount... rendering is ahead (adds to the current physics rotation matrix)
					// ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
					// spinning_mat_render = spinning_mat; // No extrapolation.
					// spinning_mat_render = glm::rotate(spinning_mat, glm::radians(spin_speed * interp_extrap_time), glm::normalize(glm::vec3(x_spin, y_spin, z_spin))); // With extrapolation.
 
					// Rotation: Interpolate, i.e. head towards physics matrix current rotation amount... rendering is behind (adds to the previous physics rotation matrix)
					// --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
					// spinning_mat_render = spinning_mat_copy; // No interpolation.
					spinning_mat_render = glm::rotate(spinning_mat_copy, glm::radians(spin_speed * interp_extrap_time), glm::normalize(glm::vec3(x_spin, y_spin, z_spin))); // With interpolation.
 
					// for (int i = 0; i < 5; ++i) { std::cout << "a" << i << " "; }	std::cout << "\n";
			
					bool physics_started = physics_running; // Using mutex lock instead of... atomic_bool... or else the 1st render cycle could be missed.
 
			mutex_toggle.unlock(); // Lock end.			
		
			if (physics_started// Bool set in: process_physics() ... called from: physics_thread()
				draw_object(); // Process rendering after the mutex lock.
		}
	}
	
private:
	void physics_thread()
	{
		float delta_time = 0;
		double previous_time = 0;
 
		while (thread_running)
		{
			// std::this_thread::sleep_for(std::chrono::microseconds(100)); // Optional (significantly reduces the CPU load, but can cause stuttering or skipping)
 
			double total_elapsed_time = glfwGetTime();
 
			delta_time += (float)(total_elapsed_time - previous_time);
			previous_time = total_elapsed_time;
 
			if (delta_time > physics_FPS_time)
			{
				delta_time -= physics_FPS_time; // By using "-=" instead of setting to: 0, it then precisely maintains this physics cycle rate.
 
				mutex_toggle.lock(); // Lock start.
 
						double thread_time_previous = thread_time_last;
						thread_time_last = glfwGetTime();
 
						float thread_cycle_time = (float)(thread_time_last - thread_time_previous);
						thread_precision_time_scaling = physics_FPS_time / thread_cycle_time;
							// std::cout << "\n   thread_cycle_time: " << thread_cycle_time << " --- thread_precision_time_scaling: " << thread_precision_time_scaling;
 
						// if (thread_precision_time_scaling > 1 - thread_precision_time_margin && thread_precision_time_scaling < 1 + thread_precision_time_margin) // ***
							process_physics();
 
						// Thread timing precision for my PC only becomes an issue if the FPS physics rate is very low... Example 1) 10 FPS (0.1) results in: 0.100967... Example 2) 5 FPS (0.2) results in: 0.19869
 
						// for (int i = 0; i < 5; ++i) { std::cout << "b" << i << " "; }	std::cout << "\n"; // *** Comment the above "thread_precision_time_scaling" if-statement line... if uncommenting this line ***
 
				mutex_toggle.unlock(); // Lock end.
			}			
		}
	}
 
	void process_physics()
	{
		physics_running = true;
 
		object_position.x += translation_step_val * translation_dir; // *** 1st: update position (updating after reversing results in a render bouncing visual artifact) ***
		// Note: if this translation direction happened to be some significant change in direction... then the extrapolate "beyond physics current position" could result in a visual artifact.
		// Alternatively... interpolate from the previous physics value like is being done for the rotation.
 
		if (std::abs(object_position.x) > 7) // 2nd: reverse direction only after having 1st applied the updated position (to catch up with interpolated/rendered position)
			translation_dir = -translation_dir;
 
		// object_position.x += translation_step_val * translation_dir; // *** Comment the above same line... if testing to see render bouncing visual artifact ***
 
		// Randomise the Model's Spinning Speed & Axis
		// -------------------------------------------------------------
		spin_vary += 0.05f * spin_dir;
 
		if (spin_vary > 2.0f || 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) * spin_speed_factor;
		}
		spinning_mat_copy = spinning_mat;
		spinning_mat = glm::rotate(spinning_mat, glm::radians(spin_speed), glm::normalize(glm::vec3(x_spin, y_spin, z_spin)));
	}
 
	void draw_object()
	{		
		glUniformMatrix4fv(position_loc, 1, GL_FALSE, glm::value_ptr(position_matrix));			
		glUniformMatrix4fv(rotation_loc, 1, GL_FALSE, glm::value_ptr(spinning_mat_render));
 
		// Clear the Screen & Draw Model Meshes
		// -----------------------------------------------------
		glClearColor(0.30f, 0.55f, 0.65f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
		for (unsigned int i = 0; i < model.num_meshes; ++i)
		{
			glBindTexture(GL_TEXTURE_2D, model.mesh_list[i].tex_handle); // Bind texture for the current mesh.	
 
			glBindVertexArray(model.mesh_list[i].VAO);
			glDrawElements(GL_TRIANGLES, (GLsizei)model.mesh_list[i].vert_indices.size(), GL_UNSIGNED_INT, 0);
			glBindVertexArray(0);
		}
		glfwSwapBuffers(window);
		glfwPollEvents();
	}
};

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;
 
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;
 
uniform mat4 view;
uniform mat4 projection;
uniform mat4 rotation;
uniform mat4 position;
 
int animate = 3;
 
void main()
{
	mat4 anim_mat = mat4(1.0);
 
	if (animate == 1)	
		anim_mat = position;
	if (animate == 2)
		anim_mat = rotation;
	if (animate == 3)
		anim_mat = position * rotation;
 
	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(anim_mat * vec4(aPos, 1.0)); // Send transformed position values, which are used for the lighting effects.			
 
	texture_coordinates = aTexCoord;
 
	mat3 normal_matrix = transpose(inverse(mat3(anim_mat)));
	vertex_normal = normal_matrix * aNormal;
	
	if (length(vertex_normal) > 0)
		vertex_normal = normalize(vertex_normal); //Never try to normalise zero vectors (0,0,0)		
 
	gl_Position = projection * view * anim_mat * vec4(aPos, 1.0);
}

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;
 
uniform sampler2D image;
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); // Subtraction order results in vertex to camera direction: correct for the already calculated reflection direction.
 
	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)); // Subtraction order: correct for normals... but incorrect for calculating the reflection direction.
	
	vec4 image_colour = texture(image, texture_coordinates);
 
	float ambient_factor = 0.25; // 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.0); // [-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;
}