OpenGL Tutorial 2 (QS) – Drawing Shapes & Transparency

To draw 3D objects in OpenGL, we need to specify vertex position values, which are simply points in 3D space just like in real life, e.g. taking your kitchen’s centre to be: X, Y, Z (0, 0, 0), your microwave oven might be 3 metres to the left, 1.5 metres forwards, and 0.25 metres upwards (-3.0, -1.5, 0.25).

Drawing 2D shapes is easier than drawing 3D objects because we can just use OpenGL’s normalised device coordinates (NDC) directly, which are X, Y, Z in the range [-1, 1], without first needing to multiply the vertex position values by a perspective projection matrix, followed by perspective division to arrive at NDC space.

However, we do still need a vertex shader and a fragment shader in order to render vertex data, regardless of whether drawing in 2D or 3D. But programming the shaders requires only minimal GLSL shader code, as demonstrated in the following “Hello triangle/rectangle” (with transparency) video...

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

#include <glad/glad.h> // GLAD: https://github.com/Dav1dde/glad ... GLAD 2 also works via the web-service: https://gen.glad.sh/ (leaving all checkbox options unchecked)
#include <GLFW/glfw3.h>
 
#include <iostream>
#include <fstream> // Used in "shader_configure.h" to read the shader text files.
 
#include "shader_configure.h" // Used to create the shaders.
 
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.5f); // Window size will be 50% the monitor's size...
	int window_height = (int)(monitor_height * 0.5f); // ... Cast is simply to silence the compiler warning.
 
	GLFWwindowwindow = glfwCreateWindow(window_widthwindow_height"Drawing Basic Shapes – Buffer Objects & Shaders"NULLNULL);
	// GLFWwindow* window = glfwCreateWindow(window_width, window_height, "Drawing Basic Shapes - Buffer Objects & Shaders", 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_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();
 
	float scale = 0.5f; // To make the rectangle completely fill the display window set this to 1
 
	// (4) Declare the Rectangle & Triangle Vertex Position Values
	// ------------------------------------------------------------------------------
	float rectangle_triangle_vertices[] = // (Typically type: GLfloat is used)
	{
		// Note:  "Being able to store the vertex attributes for that vertex only once is very economical, as a vertex's attribute...
		// data is generally around 32 bytes, while indices are usually 2-4 bytes in size." (Hence, the next tutorial uses: glDrawElements())
		
		// Rectangle vertices
		//-------------------------
		-1.0f * scale, 1.0f * scale, 0.0f,   // left top .......... 0 // Draw this rectangle's six vertices 1st.
		-1.0f * scale, -1.0f * scale, 0.0f,  // left bottom ... 1 // Use NDC coordinates [-1, +1]
		 1.0f * scale, 1.0f * scale, 0.0f,   // right top ........ 2
 
		  1.0f * scale,  1.0f * scale, 0.0f,   // right top ......... 3	
		 -1.0f * scale, -1.0f * scale, 0.0f,  // left bottom ..... 4
		  1.0f * scale, -1.0f * scale, 0.0f,  // right bottom ... 5		
		
		  // Triangle vertices (Drawing this 2nd allows some of the rectangle's fragment colour to be added to this triangle via transparency)
		  // ----------------------
		  0.0f, 0.75f, 0.0f,      // middle top ... 0
		 -0.75f, -0.75f, 0.0f,  // left bottom ... 1
		  0.75f,  -0.75f, 0.0f  // right bottom .. 2
	};
 
	/* int width, height;
	glfwGetFramebufferSize(window, &width, &height); // Uncomment this block to adjust the effective display area.
	glViewport(0, 0, int(width * 0.65f), int(height * 0.65f)); */
 
	// (5) Store the Rectangle & Triangle Vertex Data in Buffer Objects Ready for Drawing
	// -------------------------------------------------------------------------------------------------------------
	unsigned int VAOVBO// Buffer handles. (Typically type: GLuint is used)	
 
	glGenVertexArrays(1, &VAO);
	glGenBuffers(1, &VBO);	
 
	glBindVertexArray(VAO); // Binding this VAO 1st causes the following VBO to become associated with this VAO.
 
	glBindBuffer(GL_ARRAY_BUFFERVBO); // ......................... Address operator not required for arrays.
	glBufferData(GL_ARRAY_BUFFERsizeof(rectangle_triangle_vertices), rectangle_triangle_verticesGL_STATIC_DRAW);
 
	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
 
	// (7) Enter the Main-Loop
	// --------------------------------
	while (!glfwWindowShouldClose(window)) // Main-Loop
	{
		// (8) Clear the Screen & Draw Both Shapes
		// ------------------------------------------------------
		glClearColor(0.4f, 0.6f, 0.8f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);
 
		glBindVertexArray(VAO); // Not necessary for this simple case of using only one VAO. Although, it's being unbound at initialisation just above.
		glDrawArrays(GL_TRIANGLES, 0, sizeof(rectangle_triangle_vertices) / sizeof(rectangle_triangle_vertices[0]));
		glBindVertexArray(0);
 
		glfwSwapBuffers(window);
		glfwPollEvents();
	}
 
	// (9) 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 << "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.
 
flat out vec2 shape_xy_pos_flat; // Vertex position coordinates passed to the fragment shader.
out vec2 shape_xy_pos_varying; // Vertex position coordinates passed as interpolated per-vertex.
 
void main()
{
	shape_xy_pos_flat = vec2(aPos.x, aPos.y); // Send values that will not be interpolated in the fragment shader.
	shape_xy_pos_varying = vec2(aPos.x, aPos.y); // These values will be interpolated in the fragment shader.
		
	// https://www.khronos.org/opengl/wiki/Vertex_Post-Processing
	gl_Position = vec4(aPos, 1.0); // Output to vertex stream for the "Vertex Post-Processing" stage.
}

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

#version 420 core
 
out vec4 fragment_colour;
 
// Must be the exact same name as declared in the vertex shader
// -----------------------------------------------------------------------------------
flat in vec2 shape_xy_pos_flat; // Vertex position coordinates received that are not being interpolated.
in vec2 shape_xy_pos_varying; // Vertex position coordinates received as interpolated per-vertex.
 
void main()
{
	// RGB [0, 1] = RGB / 255
	// -----------------------------
	vec4 rgba = vec4(abs(shape_xy_pos_varying.x), abs(shape_xy_pos_varying.y), 0.5, 1.0); // Set red and green to varying XY values.
 
	// Triangle vertex XY values are all 0.75 (so let's just check the X value)
	// ------------------------------------------------
	if (abs(shape_xy_pos_flat.x) == 0.75) // ...................................... Transparency 0.45
		rgba = vec4(0.5, abs(shape_xy_pos_varying.x), abs(shape_xy_pos_varying.y), 0.45); // Set green and blue to varying XY values.
 
	fragment_colour = rgba; // The pixel colour that will be displayed on the monitor's screen.
}