[OpenGL learning notes] computer graphics ③ - shader [GLSL Uniform color triangle color changing square]


🍲 😋

Magic square ☁️

In the previous section, we drew a dark green triangle. In this section, we implement "code encapsulation" and draw color positive angles and color changing triangles.
Previous article address link: [OpenGL learning notes] computer graphics ② - [rendering pipeline vertex shader slice shader VAO VBO].

0. Preview of achievements:

           

  ● color triangle on the left and color changing square on the right.


1, Preliminary understanding of shaders:


  ● basically, a shader is just a program that converts input into output. Shaders are also very independent programs because they cannot communicate with each other; The only communication between them is through input and output.

  ● shaders are written in a C-like language called GLSL (full name OpenGL Shading Language). GLSL is tailored for graphical computing and contains some useful features for vector and matrix operations.

  ● the shader always starts with a version declaration, followed by input and output variables, uniform and main functions. The entry point of each shader is the main function, in which we process all input variables and output the results to the output variables. Uniform will be explained later.

  ● the template of a typical shader is:

#version version_number

in vector_type in_variable_name;
out vector_type out_variable_name;

uniform type uniform_name;

void main()
{
  // Process input and perform some graphical operations
  ...
  // Send the processed results to the output variable
  out_variable_name = Processed results;
}



  ● the vector in GLSL is a container that can contain 1, 2, 3 or 4 components, and the component type can be any of the previous default basic types. They can be in the following form (n represents the number of components):

typemeaning
vec+nVector containing n float components
bvec+nVector containing n bool components
ivec+nVector containing n int components
uvec+nVector containing n unsigned int components
dvec+nVector containing n double components

  ◆ we use "vec+n" most of the time, because float is enough to meet most requirements.

  ◆ the component of a vector can be obtained by vec.x, where x refers to the first component of the vector. We can use ". X,. y,. z and. w" respectively to obtain their first, second, third and fourth components. GLSL also allows you to use rgba for colors or stpq for texture coordinates to access the same components.



  ◆ take the chestnut of a slice shader: (environment VS2010)


2, Vertex / slice shader (text file txt)


  ● relationship between vertex shader and slice shader:
    generally, we first obtain the color at the input through the vertex shader as the vertex attribute, then transfer it to the slice shader for processing, and finally output it.

  ● although shaders are independent applets, they are all part of a whole. GLSL defines the in and out keywords specifically for this purpose. Each shader uses these two keywords to set the input and output, and as long as an output variable matches the input of the next shader stage, it will be passed on. But there is a difference between vertex and clip shaders.

  ● the vertex shader should receive a special form of input. The input of the vertex shader is special in that it receives input directly from vertex data. In order to define how vertex data should be managed, we use the metadata location to specify the input variables. Writing format: layout (location = 0). The vertex shader needs to provide an additional layout ID for its input so that we can link it to vertex data.

  ◆ for example, when rendering color triangles, our vertex shader (text file txt) is written as follows:

// The file name is "shader_v.txt"
#version 330 core 							//  Version 3.30
layout(location = 0) in vec3 position;		// The property location value of the location variable is 0
layout(location = 1) in vec3 color;			// The attribute position value of the color variable is 1
out vec3 ourColor;							// Color output
void main()
{
	gl_Position = vec4(position, 1.0f);		// Core function (location information assignment)
	ourColor = color;
} 


  ● the other is the fragment shader, which requires a vec4 color output variable, because the fragment shader needs to generate a final output color.

  ● if we intend to send data from one shader to another, we must declare an output in the sender and a similar input in the receiver. When the type and name are the same, OpenGL will link the two variables together and send data between them (this is done when linking program objects). To show how this works, we will slightly change the shader in the previous tutorial to let the vertex shader determine the color for the fragment shader.

  ◆ for example, when rendering color triangles, our slice shader (text file txt) is written as follows:

// The file name is "shader_f.txt"
#version 330 core 		//  Version 3.30
in vec3 ourColor;		// Input (3D) color vector
out vec4 FragColor;     // Output to a (4-dimensional) vector FragColor composed of four floating-point numbers
void main()
{
	FragColor = vec4(ourColor, 1.0f);	// Core function (color information assignment)
}

3, Create our own shader class (Shader.h)


  ● the shaders mentioned above are single and independent. Now we want to create a shader class and combine them organically.

  ◆ the template of a typical shader class is as follows

#pragma once 	//  To avoid the same header file being include d multiple times
#include<string>
#include<fstream>
#include<sstream>
#include<iostream>
using namespace std;

// Our own shader
class Shader
{
private:
	GLuint vertex, fragment;	// Vertex shader and slice shader 
public:
	GLuint Program;				// The ID of the shader program

	// Constructor (shader constructor)
	Shader( const GLchar *vertexPath, const GLchar *fragmentPath )		// These are the paths of the vertex / fragment shader GLSL passed in by the main function
	{
		// Variable definition of file reading series
		......

		// Exception handling mechanism: ensure that ifstream objects can throw exceptions:
		......

		try
		{
			// Open file
			......

			// Read the buffered contents of the file into the data stream
			......

			// Turn off the file processor
			......

			// Convert data stream to string
			......

		} catch( ifstream::failure e  ){	// Output in case of exception
			cout<<"ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ"<<endl;
		}

		/* Converts a string of type string to a char array type */
		......

		/* Vertex Shader  */
		......

		/* Fragment Shader  */
		......

		/* Shader program */
		......
	}

	// Deconstructor (destructor of shader)
	~Shader()
	{
		glDetachShader(this->Program, vertex);
		glDetachShader(this->Program, fragment);
		glDeleteShader(vertex);
		glDeleteShader(fragment);
		glDeleteProgram(this->Program);
	}
	
	// Called when rendering
	void Use()
	{
		glUseProgram(this->Program);
	}
};

  ● supplement to the details of the code: (this general shader is used for the color triangles and color changing squares to be rendered later)

#pragma once 	//  To avoid the same header file being include d multiple times
#include<string>
#include<fstream>
#include<sstream>
#include<iostream>
using namespace std;

// Our own shader
class Shader
{
private:
	GLuint vertex, fragment;	// Vertex shader and slice shader 
public:
	GLuint Program;				// The ID of the shader program

	// Constructor (shader constructor)
	Shader( const GLchar *vertexPath, const GLchar *fragmentPath )
	{
		// Variable definition of file reading series
		string vertexCode;
		string fragmentCode;
		ifstream vShaderFile;
		ifstream fShaderFile;

		// Exception handling mechanism: ensure that ifstream objects can throw exceptions:
		vShaderFile.exceptions(ifstream::badbit);
		fShaderFile.exceptions(ifstream::badbit);

		try
		{
			// Open file
			vShaderFile.open(vertexPath);
			fShaderFile.open(fragmentPath);
			stringstream vShaderStream, fShaderStream;

			// Read the buffered contents of the file into the data stream
			vShaderStream << vShaderFile.rdbuf();
			fShaderStream << fShaderFile.rdbuf();

			// Turn off the file processor
			vShaderFile.close();
			fShaderFile.close();

			// Convert data stream to string
			vertexCode = vShaderStream.str();
			fragmentCode = fShaderStream.str();

		} catch( ifstream::failure e  ){	// Output in case of exception
			cout<<"ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ"<<endl;
		}

		/* Converts a string of type string to a char array type */
		const GLchar *vShaderCode = vertexCode.c_str();
		const GLchar *fShaderCode = fragmentCode.c_str();

		/* Vertex Shader  */
		vertex = glCreateShader(GL_VERTEX_SHADER);				// Create vertex shader object
		glShaderSource(vertex, 1, &vShaderCode, NULL);			// Pass in the contents of the vertex shader
		glCompileShader(vertex);								// Compiling vertex shaders
		GLint flag;												// Used to judge whether the compilation is successful
		GLchar infoLog[512];				
		glGetShaderiv(vertex, GL_COMPILE_STATUS, &flag); // Get compilation status
		if( !flag )
		{
			glGetShaderInfoLog(vertex, 512, NULL, infoLog);    
			cout<<"ERROR::SHADER::VERTEX::COMPILATION_FAILED\n"<<infoLog<<endl;
		}

		/* Fragment Shader  */
		fragment = glCreateShader(GL_FRAGMENT_SHADER);			// Create a slice shader object
		glShaderSource(fragment, 1, &fShaderCode, NULL);		// Pass in the contents of the slice shader
		glCompileShader(fragment);								// Compiling vertex shaders
		glGetShaderiv(fragment, GL_COMPILE_STATUS, &flag);		// Get compilation status
		if( !flag )
		{
			glGetShaderInfoLog(fragment, 512, NULL, infoLog);	 
			cout<<"ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n"<<infoLog<<endl;
		}

		/* Shader program */
		this->Program = glCreateProgram();
		glAttachShader(this->Program, vertex);
		glAttachShader(this->Program, fragment);
		glLinkProgram(this->Program);
		if( !flag )
		{
			glGetProgramInfoLog(this->Program, 512, NULL, infoLog);    
			cout<<"ERROR::SHADER::PROGRAM::LINKING_FAILED\n"<<infoLog<<endl;
		}
		// Delete shaders, they are already linked to our program and are no longer needed
		glDeleteShader(vertex);		
		glDeleteShader(fragment);
	}

	// Deconstructor (destructor)
	~Shader()
	{
		glDetachShader(this->Program, vertex);
		glDetachShader(this->Program, fragment);
		glDeleteShader(vertex);
		glDeleteShader(fragment);
		glDeleteProgram(this->Program);
	}

	void Use()
	{
		glUseProgram(this->Program);
	}
};

4, Draw color triangle (main function)


  ● the conventional drawing process (output a triangle) is as follows:
    ◆ step 1: import the corresponding library
    ◆ step 2: write vertex position and color
    ◆ step 3: write vertex shader
    ◆ step 4: write slice shader (also known as fragment shader)
    ◆ step 5: write shader program
    ◆ step 6: set link vertex attributes
    ◆ step 7: set vertex buffer object (VBO) [usually appears together with the following VAO]
    ◆ step 8: set vertex array object (VAO) (also known as vertex array object)
    ◆ step 9: draw triangle

  ● however, because we have written the steps "step 2, step 3, step 4, step 5 and step 6" into the corresponding "Shader.h", "shader_v.txt" and "shader_f.txt" [as shown in the above figure] and have been encapsulated, we only need to write the remaining steps in the main function:

/* Import corresponding library */
#include <iostream>
using namespace std;
#define GLEW_STATIC	
#include <glew.h>	
#include <glfw3.h> 
#include "Shader.h"

/* Write each vertex position and color */
GLfloat vertices_1[] = 
{	// position				// color
	0.0f, 0.5f, 0.0f,		1.0f, 0.0f, 0.0f,		// Top vertex (red)
	-0.5f, -0.5f, 0.0f,		0.0f, 1.0f, 0.0f,		// Left vertex (green)
	0.5f, -0.5f, 0.0f,		0.0f, 0.0f, 1.0f		// Right vertex (blue)
};

const GLint WIDTH = 800, HEIGHT = 600;		// Length and width of the window

int main()
{
	/* initialization */
	glfwInit();
	GLFWwindow* window_1 = glfwCreateWindow(WIDTH, HEIGHT, "A Beautiful Triangle", nullptr, nullptr);
	int screenWidth_1, screenHeight_1;
	glfwGetFramebufferSize(window_1, &screenWidth_1, &screenHeight_1);
	glfwMakeContextCurrent(window_1);
	glewInit();

	/* Pass in the shader text we set ourselves */
	Shader ourShader = Shader("shader_v.txt", "shader_f.txt");	// Relative path

	/* Set vertex buffer object (VBO) + set vertex array object (VAO)  */
	GLuint VAO, VBO;					
	glGenVertexArrays(1, &VAO);		
	glGenBuffers(1, &VBO);			
	glBindVertexArray(VAO);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);	
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_1), vertices_1, GL_STATIC_DRAW);	

	/* Set linked vertex attributes */
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(GLfloat), (GLvoid*)0);
	glEnableVertexAttribArray(0);	// Channel 0 open
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(GLfloat), (GLvoid*)(3*sizeof(GLfloat)));
	glEnableVertexAttribArray(1);	// Channel 1 open

	// Draw loop draw loop
	while (!glfwWindowShouldClose(window_1))
	{
		glViewport(0, 0, screenWidth_1, screenHeight_1);
		glfwPollEvents();
		glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);

		/*  Step 9: draw triangle */
		ourShader.Use();					// Graphics rendering
		glBindVertexArray(VAO);				// Bind VAO
		glDrawArrays(GL_TRIANGLES, 0, 3);	// Draw a triangle 3 times from the 0 th vertex
		glBindVertexArray(0);				// Unbind

		glfwSwapBuffers(window_1);
	}

	glDeleteVertexArrays(1, &VAO);			// Release resources	
	glDeleteBuffers(1, &VBO);
	glfwTerminate();						// end
	return 0;
}

  ● operation results:


5, Index buffer object (EBO)


  ● if we want to draw a square, we can think of two triangles. EBO will be used at this time.


1. The relationship and difference between VBO, VAO and EBO

  ● relationship and difference between VBO, VAO and EBO: ⭐ ️ ⭐ ️
   ① vertex buffer object VBO is a memory buffer opened up in the memory space of the graphics card, which is used to store various attribute information of vertices, such as vertex coordinates, vertex normal vector, vertex color data, etc. During rendering, various attribute data of vertices can be taken directly from VBO. Because VBO is in video memory rather than memory, there is no need to transfer data from CPU, so the processing efficiency is higher.
    therefore, it can be understood that VBO is a storage area in video memory, which can maintain a large amount of vertex attribute information. And you can open up many vbos. Each VBO has its unique identification ID in OpenGL. This ID corresponds to the video memory address of the specific VBO. Through this ID, you can access the data in the specific VBO.


   ② VAO is a state combination that saves all vertex data attributes. It stores the format of vertex data and the reference of VBO object required by vertex data.
    because VBO saves the vertex attribute information of a model, you need to bind all the information of vertices before drawing the model every time. When the amount of data is large, it becomes very troublesome to repeat such actions. VAO can store all these configurations in one object. Each time you draw a model, you only need to bind the VAO object.
    in addition, VAO itself does not store the relevant attribute data of vertices. This information is stored in VBO. VAO is equivalent to a reference to many vbos, and some vbos are combined together as an object for unified management.

   ③ the index buffer object EBO is equivalent to the concept of vertex array in OpenGL. It is to solve the problem of repeated calls of the same vertex, reduce the waste of memory space and improve the execution efficiency. When duplicate vertices need to be used, the vertex is called through the vertex position index, instead of repeatedly recording and calling the duplicate vertex information.
    the content stored in EBO is the index of vertex position. EBO is similar to VBO. It is also a memory buffer in video memory, but EBO saves the index of vertex.


2,EBO

  ● VAO and VBO have been explained in detail in the previous section. Let's talk about EBO.

  ● first, if we want to draw a square now, we need 4 vertices. In the main function, we give the position information of the four vertices. They are "stitched" together with the index indices array of vertex positions. The schematic diagram is as follows:

  ● codes are as follows:

/* Write each vertex position */
GLfloat vertices_1[] =
{
	//position					
	0.5f, 0.5f, 0.0f,			// top right		0
	0.5f, -0.5f, 0.0f,			// bottom right		1
	-0.5f, -0.5f, 0.0f,			// bottom left		2
	-0.5f, 0.5f, 0.0f,			// top left			3
};

/* The connection information of the four vertices is given */
GLuint indices_1[] =
{
	0, 1, 3,		// Vertices with sequence numbers 0, 1 and 3 are combined into a triangle
	1, 2, 3			// Vertices numbered 1, 2 and 3 are combined into a triangle
};

  ● after creating VAO and VBO, create EBO and bind it, and store the index in EBO with glBufferData (with GL_ELEMENT_ARRAY_BUFFER as the parameter):

GLuint EBO;
glGenBuffers(1, &EBO);						// Bind EBO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);	// Use the glBindBuffer function to bind the newly created index buffer object to GL_ELEMENT_ARRAY_BUFFER target
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices_1), indices_1, GL_STATIC_DRAW); // GL_STATIC_DRAW: static drawing (because it needs to be read frequently)

  ● when drawing the model by EBO binding vertex index, you need to use glDrawElements instead of glDrawArrays:

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);				// Bind EBO
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);	// Draw two triangles, starting from the 0th vertex, a total of 6 times (in the order of 0,1,3,1,2,3)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);				// Unbound EBO

  ◆ description of glDrawElements() function:
     ① first parameter: drawing mode
     ② second parameter: the number of vertices drawn
     ③ the third parameter: the data type of the index
     ④ fourth parameter: optional offset setting in EBO


6, Uniform


  ● to draw a square whose color changes with time, the input of shader changes with time. Previous knowledge can not meet this point, so Uniform needs to be used.

  ● uniform is a way to send data from applications in the CPU to shaders in the GPU, but uniform is somewhat different from vertex attributes. First, uniform is global. Global means that the uniform variable must be unique in each shader program object, and it can be accessed by any shader of the shader program at any stage. Second, no matter what you set the uniform value to, uniform will keep their data until they are reset or updated.

  ● modified vertex shader:

#version 330 core 							//  Version 3.30
layout(location = 0) in vec3 position;		// The property location value of the location variable is 0
void main()
{
	gl_Position = vec4(position, 1.0f);		// Core function (location information assignment)
} 

  ● modified slice shader:

#version 330 core  		//  Version 3.30
out vec4 FragColor;     // The output is a vector RGB+aerfa composed of four floating-point numbers
uniform vec4 time;		// Set this variable in OpenGL program code (uniform: real-time variable representation)
void main()
{
	FragColor = time;	// Color changes with time
}

  ● "Shader.h" header file without modification (the same as color triangle).


  ● the uniform is still empty. We haven't added any data to it yet. First, you need to use the glGetUniformLocation() function to find the index (i.e. position value) of the uniform attribute in the shader. When we get the uniform index, we can update its value with the glUniform... () correlation function.

float time = glfwGetTime();						// Get time (seconds to run)
float redValue = sin(time) / 2.0f + 0.5f;		// Red numerical calculation, range [0,1]
float greenValue = 1 - redValue;				// Green numerical calculation, range [0.1]. And meet "redValue + greenValue = 1"
int vertexColorLocation = glGetUniformLocation(ourShader.Program, "time");	// Index found for 'time'
glUniform4f(vertexColorLocation, redValue, greenValue, 0.0f, 1.0f );		// Update color

  ◆ supplementary note: because OpenGL is a C library at its core, it does not support type overloading. When the function parameters are different, it is necessary to define a new function. The glUniform function is a typical example, which has a specific suffix. When the ID is set to the type of uniform, the possible suffixes are:

suffixmeaning
n+fThe function requires an n float as its value
n+iThe function requires an n int as its value
n+uiThe function requires n unsigned int s as its value
fvThe function requires a float vector / array as its value

7, Draw color changing square (main function)


  ● everything is ready except Dongfeng:

/* Import corresponding library */
#include <iostream>
using namespace std;
#define GLEW_STATIC	
#include<glew.h>	
#include<glfw3.h> 
#include"Shader.h"

/* Write each vertex position */
GLfloat vertices_1[] =
{
	//position					
	0.5f, 0.5f, 0.0f,			// top right		0
	0.5f, -0.5f, 0.0f,			// bottom right		1
	-0.5f, -0.5f, 0.0f,			// bottom left		2
	-0.5f, 0.5f, 0.0f,			// top left			3
};

/* The connection information of the four vertices is given */
GLuint indices_1[] =
{
	0, 1, 3,		// Vertices with sequence numbers 0, 1 and 3 are combined into a triangle
	1, 2, 3			// Vertices numbered 1, 2 and 3 are combined into a triangle
};

const GLint WIDTH = 600, HEIGHT = 600;		// Square window

int main()
{
	glfwInit();
	GLFWwindow* window_1 = glfwCreateWindow(WIDTH, HEIGHT, "Learn OpenGL Triangle test", nullptr, nullptr);
	int screenWidth_1, screenHeight_1;
	glfwGetFramebufferSize(window_1, &screenWidth_1, &screenHeight_1);
	glfwMakeContextCurrent(window_1);
	glewInit();

	/* Pass in the shader text we set ourselves */
	Shader ourShader = Shader("shader_v.txt", "shader_f.txt");		// Relative path

	/* Set vertex buffer object (VBO) + set vertex array object (VAO)  */
	GLuint VAO, VBO;				
	glGenVertexArrays(1, &VAO);		
	glGenBuffers(1, &VBO);			
	glBindVertexArray(VAO);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);	
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_1), vertices_1, GL_STATIC_DRAW);	

	/* Set linked vertex attributes */
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(GLfloat), (GLvoid*)0);
	glEnableVertexAttribArray(0);	// Channel 0 open

	/* Set index buffer object	*/
	GLuint EBO;
	glGenBuffers(1, &EBO);		
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices_1), indices_1, GL_STATIC_DRAW); 

	// Draw loop draw loop
	while (!glfwWindowShouldClose(window_1))
	{
		// Viewport + time 
		glViewport(0, 0, screenWidth_1, screenHeight_1);
		glfwPollEvents();

		// Render + clear color buffer
		glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);

		/*  Drawing graphics */
		ourShader.Use();
		float time = glfwGetTime();						// Get time
		float redValue = sin(time) / 2.0f + 0.5f;		// Red numerical calculation, range [0,1]
		float greenValue = 1 - redValue;				// Green numerical calculation, range [0.1]. And meet "redValue + greenValue = 1"
		int vertexColorLocation = glGetUniformLocation(ourShader.Program, "time");
		glUniform4f(vertexColorLocation, redValue, greenValue, 0.0f, 1.0f );

		glBindVertexArray(VAO);									// Bind VAO
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);				// Bind EBO
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);	// Draw two triangles, starting from the 0th vertex, a total of 6 times
		glBindVertexArray(0);									// Unbound VAO
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);				// Unbound EBO

		// Swap buffer
		glfwSwapBuffers(window_1);
	}

	glDeleteVertexArrays(1, &VAO);	// Release resources
	glDeleteBuffers(1, &VBO);
	glDeleteBuffers(1, &EBO);
	glfwTerminate();				// end
	return 0;
}

  ● operation results:


8, Summary (overall mind map)


  ● drawing process (output a triangle):
    ◆ step 1: import the corresponding library
    ◆ step 2: write vertex attributes
    ◆ step 3: write vertex shader
    ◆ step 4: write slice shader (also known as fragment shader)
    ◆ step 5: write shader program
    ◆ step 6: set link vertex attributes
    ◆ step 7: set vertex buffer object (VBO)
    ◆ step 8: set vertex array object (VAO)
    ◆ step 9: set index buffer object (EBO)
    ◆ step 10: draw triangle

  ● encapsulate the steps of "step 2, step 3, step 4, step 5 and step 6" in the corresponding "Shader.h", "shader_v.txt" and "shader_f.txt". Then in the main function, write the remaining steps.

5, Refer to appendix:

[1] Learnopungl CN - shaders
Link: https://learnopengl-cn.github.io/01%20Getting%20started/05%20Shaders/.

[2] OpenGL graphics rendering pipeline, VBO, VAO, EBO concepts and use cases
Link: https://blog.csdn.net/weixin_30735745/article/details/95616490.

Previous article address link: [OpenGL learning notes] computer graphics ② - [rendering pipeline vertex shader slice shader VAO VBO].

⭐️ ⭐️

Tags: OpenGL

Posted on Thu, 23 Sep 2021 00:43:02 -0400 by jobe1