OpenGL Tutorial 3 (QS) – Orbiting Spinning Cubes

To change the position, rotation, or size of 3D models as rendered in graphics programming, their vertex positional values are multiplied by different transformation matrices. Translating vertex X, Y, Z positional values requires simply adding or subtracting the movement amount. Similarly, scaling requires simply multiplying vertex positional values by a scaling value.

However, rotation is appreciably more complicated, because even for rotation around a single axis, then per vertex, it requires multiplying its X, Y, Z values by the sine or cosine of the angle, about which the other two axes rotate, the results of which are then added accordingly – increasing or cancelling by varying amounts to produce the new X, Y or Z value.

Conveniently, the OpenGL maths library (GLM) includes all the transformation matrices we need; therefore we don’t need to derive them ourselves. The program demonstrated in the video below makes use of... glm::translate, glm::rotate and glm::scale, to produce one stationary spinning cube and five orbiting spinning cubes.

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> 
 
// 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 <iostream>
#include <fstream> // Used in "shader_configure.h" to read the shader text files. 
#include <sstream>

#include "shader_configure.h" // Used to create the shaders.
 
// Function Prototypes
// ---------------------------
void update_orbit(glm::mat4orbit_mat, glm::vec3orbit_vec, glm::vec3 constorbit_axisfloat angle, glm::vec3orbit_pos);
void update_norm_vec(glm::mat4orbit_mat, glm::mat4spinning_mat, glm::vec3face);
void draw_cube(unsigned VAOunsigned cube_indices_bytesunsigned orbit_locfloatorbit_mat);																										
 
int main()
{
	// (1) GLFW: Initialise & Configure
	// -----------------------------------------
	if (!glfwInit())
		exit(EXIT_FAILURE);
 
	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 75% 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"Spinning Cubes Shining Light Beams – Matrix Transformations"NULLNULL);
	// GLFWwindow* window = glfwCreateWindow(window_width, window_height, "Spinning Cubes Shining Light Beams", 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.	
 
	// (3) Compile Shaders Read from Text Files
	// ------------------------------------------------------
	const charvert_shader = "../../Shaders/shader_glsl.vert";
	const charfrag_shader = "../../Shaders/shader_glsl.frag";
	
	Shader main_shader(vert_shaderfrag_shader);
	main_shader.use();	
 
	// (4) Declare the Cube's Vertex Position Values
	// -----------------------------------------------------------
	float cube_vertices[] =
	{
		// Note:  "Being able to store the vertex attributes for that vertex only once is very economical, as a vertex's attribute...
		// data is generally around 32 bytes, while indices are usually 2-4 bytes in size." (Hence, glDrawElements() further down)
		
		-1.0f, -1.0f,  1.0f, // left, bottom, rear ..... 0
		-1.0f, -1.0f, -1.0f, // left, bottom, front .... 1
		-1.0f,  1.0f,  1.0f, // left, top, rear ............ 2
		-1.0f,  1.0f,  -1.0f, // left, top, front .......... 3
 
		1.0f, -1.0f,  1.0f,  // right, bottom, rear .... 4
		1.0f, -1.0f, -1.0f,  // right, bottom, front ... 5
		1.0f,  1.0f,  1.0f,  // right, top, rear ........... 6
		1.0f,  1.0f, -1.0f   // right, top, front ......... 7
	};
 
	// glDrawArrays() vs glDrawElements()
	// ----------------------------------------------
	// Typical bytes being used here via using glDrawElements() = 8 * 32 + 36 * 3 = 364 bytes.
	// If we were using glDrawArrays(), then: 36 vertices (instead of the 8 above) * typically 32 bytes = 1152 bytes.
	// Saving 28 vertices: 28 * 32 = 896 bytes, but at the expense of 36 indices x typically 3 bytes = 108 bytes.
	// Therefore the final saving then becomes: 896 - 108 = 788 bytes, which is a 68.4% reduction.
 
	unsigned int cube_indices[] = // Drawing 6 faces = 12 triangles.
	{
		1,0,5, 5,0,4,     5,4,7, 7,4,6,     0,2,4, 4,2,6,     3,2,1, 1,2,0,     3,1,7, 7,1,5,     2,3,6, 6,3,7
	};
 
	// (5) Store the Cube's Vertex Data in Buffer Objects Ready for Drawing
	// ------------------------------------------------------------------------------------------
	unsigned int VAOVBOEBO;
	
	glGenVertexArrays(1, &VAO);
	glGenBuffers(1, &VBO);
	glGenBuffers(1, &EBO);
 
	glBindVertexArray(VAO); // Binding this VAO 1st causes the following VBO and EBO to become associated with this VAO. 
 
	glBindBuffer(GL_ARRAY_BUFFERVBO);
	glBufferData(GL_ARRAY_BUFFERsizeof(cube_vertices), cube_verticesGL_STATIC_DRAW);		
 
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFEREBO);					
	glBufferData(GL_ELEMENT_ARRAY_BUFFERsizeof(cube_indices), cube_indicesGL_STATIC_DRAW);
	
	glEnableVertexAttribArray(0);
 
	// Void pointer 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); 
																												
	glBindVertexArray(0);	// Unbind VAO
 
	// (6) Declare & Set Up the Various Matrices... https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glUniform.xhtml
	// -------------------------------------------------------
	unsigned int model_num_loc = glGetUniformLocation(main_shader.ID, "model_num"); // Accessed directly in both shaders.
 
	// Used only in the vertex shader
	// ----------------------------------------
	unsigned int view_matrix_loc = glGetUniformLocation(main_shader.ID, "view"); 
	unsigned int projection_matrix_loc = glGetUniformLocation(main_shader.ID, "projection");
	unsigned int scaling_mat_loc = glGetUniformLocation(main_shader.ID, "scaling_mat");
	unsigned int spinning_mat_loc = glGetUniformLocation(main_shader.ID, "spinning_mat");
	unsigned int orbiting_cube_loc = glGetUniformLocation(main_shader.ID, "orbiting_cube");
	unsigned int outer_cube_loc = glGetUniformLocation(main_shader.ID, "outer_cube");
 
	// Used only in the fragment shader
	// --------------------------------------------
	unsigned int orbit_pos_loc = glGetUniformLocation(main_shader.ID, "orbit_pos");
	unsigned int face_dir_loc = glGetUniformLocation(main_shader.ID, "face_dir");
 
	glm::vec3 cameraPos(0.0f, 0.0f, 20.0f);
	glm::vec3 camTarget(0.0f, 0.0f, 0.0f);
	glm::vec3 cameraUp(0.0f, 1.0f, 0.0f);
	glm::mat4 view = glm::lookAt(cameraPoscamTargetcameraUp);
	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));
 
	glm::mat4 scaling_mat(1.0f); // Initialise to identity matrix.
	scaling_mat = glm::scale(scaling_mat, glm::vec3(0.5f, 0.5f, 0.5f));
	glUniformMatrix4fv(scaling_mat_loc, 1, GL_FALSE, glm::value_ptr(scaling_mat));
 
	glm::mat4 outer_cube(1.0f); // Initialise to identity matrix.
	outer_cube = glm::scale(outer_cube, glm::vec3(20.0f, 20.0f, 20.0f));
	outer_cube = glm::rotate(outer_cube, glm::radians(45.0f), glm::vec3(0, 1, 0)); // Rotation axes are typically recommended to be normalised.
	outer_cube = glm::rotate(outer_cube, glm::radians(75.0f), glm::vec3(1, 0, 1)); // https://glm.g-truc.net/0.9.4/api/a00151.html#gaacb9cbe8f93a8fef9dc3e25559df19c0		
 
	glm::mat4 spinning_mat(1.0f); // Initialise to identity matrix.
	glm::mat4 orbit_1_mat(1.0f);
	glm::mat4 orbit_2_mat(1.0f);
	glm::mat4 orbit_3_mat(1.0f);
	glm::mat4 orbit_4_mat(1.0f);
	glm::mat4 orbit_5_mat(1.0f);	
 
	glm::vec3 orbit_1_vec = glm::vec3(-4.5f, 0.0f, 0.0f);
	orbit_1_mat = glm::translate(orbit_1_matorbit_1_vec); // Translate to initial orbit position.
 
	glm::vec3 orbit_2_vec = glm::vec3(0.0f, 4.5f, 0.0f);
	orbit_2_mat = glm::translate(orbit_2_mat-orbit_2_vec);
 
	glm::vec3 orbit_3_vec = glm::vec3(0.0f, 0.0f, 4.5f);
	orbit_3_mat = glm::translate(orbit_3_mat-orbit_3_vec);
 
	glm::vec3 orbit_4_vec = glm::vec3(3.5f, 0.0f, 3.5f);
	orbit_4_mat = glm::translate(orbit_4_mat-orbit_4_vec);
 
	glm::vec3 orbit_5_vec = glm::vec3(-3.5f, 0.0f, 3.5f);
	orbit_5_mat = glm::translate(orbit_5_mat-orbit_5_vec);
 
	// (7) 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::vec3 face_dir[6]{};
	glm::vec3 orbit_pos[6]{};
 
	while (!glfwWindowShouldClose(window)) // Main-Loop
	{
		// (8) Randomise the Cube 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() % 5 + 1);
		}			
		spinning_mat = glm::rotate(spinning_mat, glm::radians(spin_speed), glm::normalize(glm::vec3(x_spiny_spinz_spin)));
			
		// (9) Calculate the Transformation Matrices for the Orbiting Cubes (Typically classes would be used once things start to get complicated)
		// -------------------------------------------------------------------------------------
		// (Temporarily moving back to centre (0, 0, 0) before rotating, and then returning along a different path)
		orbit_1_mat = glm::translate(orbit_1_mat-orbit_1_vec); 
		orbit_1_mat = glm::rotate(orbit_1_mat, glm::radians(0.5f), glm::vec3(0, 0, 1)); // Perpendicular axis to orbit.
		orbit_1_mat = glm::translate(orbit_1_matorbit_1_vec);
		orbit_pos[0] = orbit_1_mat * glm::vec4(0, 0, 0, 1); // Used to calculate an accurate line-of-site for specular-type lighting in fragment shader.		
		
		// std::cout << "Orbit position: " << orbit_pos[0].x << " --- " << orbit_pos[0].y << " --- " << orbit_pos[0].z << "\n";
 
		// (The order of multiplying matrices together matters)	
		glm::mat4 transform = orbit_1_mat * spinning_mat// Spin 1st... Apply orbit 2nd... Notice right to left (i.e., 1st operation is on the right)
		
		// (Cannot use "spinning_mat" directly because the "orbit_xx_mat"... translate >> rotate >> translate... also changes the rotation)		
		glm::mat3 norm_corr_matrix = glm::transpose(glm::inverse(glm::mat3(transform))); 
		face_dir[0] = glm::normalize(norm_corr_matrix * glm::vec3(1, 0, 0)); // Arbitrarily simply selecting the right face.
 
		// (Normalising not actually required unless scaling is involved)	
		/* std::cout << " --- Normalised length: " << glm::length(norm_corr_matrix * glm::vec3(1, 0, 0)) << " --- Length without normalising: " 
					<< glm::length(glm::mat3(transform) * glm::vec3(1, 0, 0)) << "\n\n"; */
 
		// (The above breakdown is for illustration purposes, i.e., the remaining orbit positions [1] to [5] below, all call the relevant functions)
		// -----------------------------------------------------------------
		update_orbit(orbit_2_matorbit_2_vec, glm::vec3(1, 0, 0), 0.5f, orbit_pos[1]);
		update_norm_vec(orbit_2_matspinning_matface_dir[1]);	
 
		update_orbit(orbit_3_matorbit_3_vec, glm::vec3(0, 1, 0), 0.5f, orbit_pos[2]);
		update_norm_vec(orbit_3_matspinning_matface_dir[2]);
 
		update_orbit(orbit_4_matorbit_4_vec, glm::vec3(1, 0, -1), 0.5f, orbit_pos[3]);
		update_norm_vec(orbit_4_matspinning_matface_dir[3]);
 
		update_orbit(orbit_5_matorbit_5_vec, glm::vec3(-1, 0, -1), -0.5f, orbit_pos[4]);
		update_norm_vec(orbit_5_matspinning_matface_dir[4]);		
 
		orbit_pos[5] = glm::vec3(0, 0, 0); // This is the centre stationary spinning cube.
		face_dir[5] = glm::mat3(spinning_mat* glm::vec3(1, 0, 0);		
 
		glUniform3fv(face_dir_loc, 6, glm::value_ptr(face_dir[0])); // Uniform: Transfer matrices to array in fragment shader.
		glUniform3fv(orbit_pos_loc, 6, glm::value_ptr(orbit_pos[0]));
 
		// (10) Finally Clear the Screen & Draw Everything
		// --------------------------------------------------------------
		glClearColor(0.75f, 0.55f, 0.35f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
		glUniform1i(model_num_loc, 1); // This is the very large outer cube that doesn't spin.
		draw_cube(VAOsizeof(cube_indices), outer_cube_loc, glm::value_ptr(outer_cube));
 
		glUniform1i(model_num_loc, 2); // This is the centre stationary spinning cube.
		draw_cube(VAOsizeof(cube_indices), spinning_mat_loc, glm::value_ptr(spinning_mat));
 
		glUniform1i(model_num_loc, 3); // These are all the small, orbiting spinning cubes.		
		draw_cube(VAOsizeof(cube_indices), orbiting_cube_loc, glm::value_ptr(orbit_1_mat));
		draw_cube(VAOsizeof(cube_indices), orbiting_cube_loc, glm::value_ptr(orbit_2_mat));
		draw_cube(VAOsizeof(cube_indices), orbiting_cube_loc, glm::value_ptr(orbit_3_mat));
		draw_cube(VAOsizeof(cube_indices), orbiting_cube_loc, glm::value_ptr(orbit_4_mat));
		draw_cube(VAOsizeof(cube_indices), orbiting_cube_loc, glm::value_ptr(orbit_5_mat));
 
		glfwSwapBuffers(window);
		glfwPollEvents();
	}
 
	// (11) Exit the Application
	// --------------------------------
	glDeleteProgram(main_shader.ID); // This OpenGL function call is talked about in: shader_configure.h
 
	/* glfwDestroyWindow(window) // Call this function to destroy a specific window */
	glfwTerminate(); // Destroys all remaining windows and cursors, restores modified gamma ramps, and frees resources.
 
	exit(EXIT_SUCCESS); // Function call: exit() is a C/C++ function that performs various tasks to help clean up resources.
}
 
void update_orbit(glm::mat4orbit_mat, glm::vec3orbit_vec, glm::vec3 constorbit_axisfloat angle, glm::vec3orbit_pos)
{
	orbit_mat = glm::translate(orbit_matorbit_vec);
	orbit_mat = glm::rotate(orbit_mat, glm::radians(angle), orbit_axis);
	orbit_mat = glm::translate(orbit_mat-orbit_vec);
 
	// Not transferring all the positions, most noticeably causes all the smaller cubes, own face dot, to display on the larger centre spinning cube.
	// -------------------------------------------
	orbit_pos = orbit_mat * glm::vec4(0, 0, 0, 1); // Transferred to the fragment shader in the main() while loop.
}
 
void update_norm_vec(glm::mat4orbit_mat, glm::mat4spinning_mat, glm::vec3face)
{
	glm::mat4 transform = orbit_mat * spinning_mat;
	glm::mat3 norm_corr_matrix = glm::transpose(glm::inverse(glm::mat3(transform)));
	face = glm::normalize(norm_corr_matrix * glm::vec3(1, 0, 0)); // Arbitrarily simply selecting the right face.
}
 
void draw_cube(unsigned VAOunsigned cube_indices_bytesunsigned orbit_locfloatorbit_mat)
{
	glUniformMatrix4fv(orbit_loc, 1, GL_FALSEorbit_mat);
 
	glBindVertexArray(VAO); // Not necessary for this simple case of using only one VAO. Although, it's being unbound at initialisation further up.
	glDrawElements(GL_TRIANGLEScube_indices_bytes / sizeof(float), GL_UNSIGNED_INT, 0);
	glBindVertexArray(0);
}

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

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

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

#version 420 core
 
layout (location = 0) in vec3 aPos; // Attribute data: vertex(s) X, Y, Z position via VBO set up on the CPU side.
// layout (location = 1) in vec3 aNormal; // Not being used.
 
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.
 
 // Uniform variables passed from the CPU side
 // ----------------------------------------------------------
uniform int model_num; // This particular uniform is declared both here in the vertex shader and also within the fragment shader.
 
uniform mat4 view;
uniform mat4 projection;
 
uniform mat4 scaling_mat;
uniform mat4 spinning_mat;
uniform mat4 orbiting_cube;
uniform mat4 outer_cube;
 
void main()
{
	vert_pos_varying = aPos; // Send aPos vertex position values to the fragment shader, which are used as the initial colour values.
 
	mat4 mat_trans;
 
	if (model_num == 1)
		mat_trans = outer_cube; // Outer cube
 
	if (model_num == 2)	
		mat_trans = spinning_mat; // Centre cube
	
	if (model_num == 3)
		mat_trans = orbiting_cube * spinning_mat * scaling_mat;	// Orbiting cubes (Right-to-left multiplication order)
 
	// Notes for typical (Not being implemented for this demo) lighting via normal vectors
	// --------------------------------------------------------------------------------------------------------------
	// mat3 norm_corr_matrix;
	// norm_corr_matrix = transpose(inverse(mat3(mat_trans)));
	// ----------------------------------------------------------------------------
	// 1) aNormal: Rotation only (i.e., no translations) transformations don't require transforming via "norm_corr_matrix".
	// 2) If transformations involve scaling then normal vectors require normalising.
	// -----------------------------------------------------------------------------------------------------
	// norm_vec = norm_corr_matrix * aNormal;
	// norm_vec = normalize(norm_vec); (It doesn't do any harm to simply normalise them anyway) (Never try to normalise 0,0,0 vectors)
 
	vert_pos_transformed = vec3(mat_trans * vec4(aPos, 1.0)); // Send transformed position values, which are used for the lighting effects.
		
	// https://www.khronos.org/opengl/wiki/Vertex_Post-Processing
	gl_Position = projection * view * mat_trans * 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;
 
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.
 
 // Uniform variables passed from the CPU side
 // ----------------------------------------------------------
uniform int model_num; // This particular uniform is declared both here in the fragment shader and also within the vertex shader.
uniform vec3 orbit_pos[6];
uniform vec3 face_dir[6];
 
void main()
{
	vec3 colour_rgb;
 
	colour_rgb.x = abs(vert_pos_varying.x);
	colour_rgb.y = abs(vert_pos_varying.y);
	colour_rgb.z = abs(vert_pos_varying.z);	
 
	// Calculating the spatial direction from each cube's centre to the interpolated vertex position being rasterised... in 3D/World space
	// --------------------------------------------------------------------------------------------------------------------------------------------------------------------------
	vec3 light_dir_1 = normalize(orbit_pos[0] - vert_pos_transformed); // Each cube's light is actually being allowed to shine/pass through all the other cubes.
	vec3 light_dir_2 = normalize(orbit_pos[1] - vert_pos_transformed);
	vec3 light_dir_3 = normalize(orbit_pos[2] - vert_pos_transformed);
	vec3 light_dir_4 = normalize(orbit_pos[3] - vert_pos_transformed);
	vec3 light_dir_5 = normalize(orbit_pos[4] - vert_pos_transformed);
	vec3 light_dir_6 = normalize(orbit_pos[5] - vert_pos_transformed);
 
	int spec_intensity = 64; // Lower values result in shining a larger spot size onto other cube's faces (Values 6 to 128 work best)
 
	// Dot on 1 side... Calculating the angular difference between each cube's rotational face direction and the above spatial direction (and using some power of this dot product)
	// ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
	float spec_a = pow(max(dot(face_dir[0], light_dir_1), 0), spec_intensity); // face_dir[i] have all been normalised on the CPU side.
	float spec_b = pow(max(dot(face_dir[1], light_dir_2), 0), spec_intensity); // For perpendicular vectors the dot product = 0
	float spec_c = pow(max(dot(face_dir[2], light_dir_3), 0), spec_intensity); // ... and for parallel vectors in the same direction the dot product = 1
	float spec_d = pow(max(dot(face_dir[3], light_dir_4), 0), spec_intensity); // ... and for parallel vectors in the opposite direction the dot product = -1
	float spec_e = pow(max(dot(face_dir[4], light_dir_5), 0), spec_intensity); // max() because... "The result is undefined if x < 0 or if x = 0 and y <= 0"
	float spec_f = pow(max(dot(face_dir[5], light_dir_6), 0), spec_intensity); // ... https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/pow.xhtml
 
	// Dot on 2 sides
	// -------------------
	//	float spec_a = pow(abs(dot(face_dir[0], light_dir_1)), spec_intensity);
	//	float spec_b = pow(abs(dot(face_dir[1], light_dir_2)), spec_intensity);
	//	float spec_c = pow(abs(dot(face_dir[2], light_dir_3)), spec_intensity);
	//	float spec_d = pow(abs(dot(face_dir[3], light_dir_4)), spec_intensity);
	//	float spec_e = pow(abs(dot(face_dir[4], light_dir_5)), spec_intensity);
	//	float spec_f = pow(abs(dot(face_dir[5], light_dir_6)), spec_intensity);
 
	// "A numeric literal that uses a decimal is by default of type float. To create a float literal from an integer value, use the suffix f or F as in C/C++"
	// ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
	vec3 col_a = vec3(102.0 / 255,   0.0 / 255,       102.0 / 255); // Set individual specular colours here: OpenGL RGB [0, 1] = Actual RGB / 255
	vec3 col_b = vec3(51.0 / 255,     102.0 / 255,   204.0 / 255);
	vec3 col_c = vec3(153.0 / 255,   237.0 / 255,   0.0 / 255);
	vec3 col_d = vec3(170.0 / 255,   102.0 / 255,   0.0 / 255);
	vec3 col_e = vec3(204.0 / 255,   0.0 / 255,       255.0 / 255);
	vec3 col_f = vec3(255.0 / 255,    80.0 / 255,     80.0 / 255);
 
	vec3 specular_light = col_a * spec_a + col_b * spec_b + col_c * spec_c + col_d * spec_d + col_e * spec_e + col_f * spec_f;	
	
	colour_rgb += specular_light * 2.5; // Apply the main specular light beam effect with intensity.
 
	if (model_num == 1)
		colour_rgb *= 0.5; // Colour intensity of outer cube.
	else
		colour_rgb *= 0.6; // Colour intensity of all spinning cubes.
 
	if (length(orbit_pos[0] - vert_pos_transformed) < 0.75) // Targeting only a close distance from the cube's centre... Purple
		if (spec_a > 0.015) // Set the dot size on cube face based on the specular value.
			colour_rgb = col_a * 1.25; // Set dot colour and intensity on cube face.
 
	if (length(orbit_pos[1] - vert_pos_transformed) < 0.75) // orbit_pos[0] to [4] are the orbiting cubes... Blue
		if (spec_b > 0.015)		// Only 0.75 required above because orbiting cubes VBO vertices are scaled smaller in vertex shader to 0.5 from centre.
			colour_rgb = col_b * 1.0;
 
	if (length(orbit_pos[2] - vert_pos_transformed) < 0.75) // Green
		if (spec_c > 0.015)		
			colour_rgb = col_c * 1.0;
 
	if (length(orbit_pos[3] - vert_pos_transformed) < 0.75) // Orange
		if (spec_d > 0.015)	
			colour_rgb = col_d * 1.0;
 
	if (length(orbit_pos[4] - vert_pos_transformed) < 0.75) // Pink
		if (spec_e > 0.015)
			colour_rgb = col_e * 1.0;
 
	if (length(orbit_pos[5] - vert_pos_transformed) < 1.25) // [5] = The centre stationary spinning cube... Red
		if (spec_f > 0.015)
			colour_rgb = col_f * 1.0;
 
	// Uncomment to enable ribbon pattern
	// -------------------------------------------------
	//	if (abs(vert_pos_varying.x) < 0.1)
	//		colour_rgb = col_d * 1.0;
	//
	//	if (abs(vert_pos_varying.y) < 0.1)
	//		colour_rgb = col_e * 1.0;
 
	fragment_colour = vec4(colour_rgb, 1.0); // The resulting fragment colour that will be displayed.
}