1. 程式人生 > >新版OpenGL學習入門(二)——繪製圖形

新版OpenGL學習入門(二)——繪製圖形

教程連結:你好,三角形

這一章學的東西超級多,學完也算基本入門啦

那就從最基礎的開始吧

 

頂點輸入

首先是座標軸,它是高中數學學的直角座標系的座標軸,理解特別簡單。

對應的數值需要在-1和1之間,大概類似百分比吧,最後的f代表浮點數。

和頂點對應的是頂點緩衝物件VBO,先是建立一個unsigned int來儲存id,然後建立頂點緩衝物件,再是繫結緩衝物件

最後把頂點資料緩衝進去

這裡glBufferData最後一個引數是顯示卡管理給定資料的模式

  • GL_STATIC_DRAW         資料幾乎不會改變
  • GL_DYNAMIC_DRAW     資料會改變很多
  • GL_STREAM_DRAW       資料每次繪製都會改變
//頂點座標x,y,z,需要在-1到1之間,標準數學座標軸
//如果是2d的話z變為0
float vertices[] = {
	-0.5f, -0.5f, 0.0f,
	 0.5f, -0.5f, 0.0f,
	 0.0f,  0.5f, 0.0f,
};

unsigned int VBO;                      //VBO頂點緩衝物件的id
glGenBuffers(1, &VBO);                 //生成頂點緩衝物件,給VBO這個id。型別為GL_ARRAY_BUFFER
glBindBuffer(GL_ARRAY_BUFFER, VBO);    //把新建的緩衝繫結到GL_ARRAY_BUFFER

//把頂點資料複製到緩衝的記憶體中
//顯示卡管理給定資料的模式:GL_STATIC_DRAW資料幾乎不會改變,GL_DYNAMIC_DRAW資料會改變很多,GL_STREAM_DRAW資料每次繪製都會改變
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);    //頂點緩衝物件,傳送資料大小,傳送的實際資料,顯示卡管理給定資料的模式

 

頂點著色器

著色器相關的在後面有詳細介紹,這裡主要是為了能夠編譯使用

頂點著色器

著色器程式碼

這是一個非常基礎的GLSL頂點著色器

#version 330 core
layout (location = 0) in vec3 aPos;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

在著色器的使用過程中,一般是另建立文件來使用的,因此在這邊為了簡單使用,用字串記錄

*注意這個是寫在main函式之前的

// GLSL頂點著色器,一般來說著色器會寫在其它頁面
const char *vertexShaderSource = "#version 330 core\n"      //GLSL版本,與OpenGL版本對應
	"layout (location = 0) in vec3 aPos;\n"
	"void main()\n"
	"{\n"
	"   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"  //可以把vec3的資料看作vec4構造器的引數,最後一個是透視除法(?)
	"}\0";

編譯頂點著色器

這個和VBO類似,先是著色器id,然後建立著色器,再編譯著色器,最後判斷是否成功

// 編譯著色器
unsigned int vertexShader;                                       //建立頂點著色器物件
vertexShader = glCreateShader(GL_VERTEX_SHADER);                 //建立頂點著色器
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);      //要編譯的著色器,傳遞的原始碼字串數量,頂點著色器真正的原始碼,(未知)
glCompileShader(vertexShader);
//判斷編譯是否成功
int sucess;                                                      //獲取著色器編譯是否成功的引數
char infoLog[512];                                               //如果編譯失敗獲取失敗的內容
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &sucess);         //檢查編譯是否成功
if (!sucess)                                                     //如果編譯失敗,輸出資訊
{
	glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
	std::cout << "ERROR::SHADER::VERTEX::COMPLIATION_FILED\n" << infoLog << std::endl;
}

 

片段著色器

這個幾乎和頂點著色器一模一樣

著色器程式碼

原始碼

#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
} 

寫在同一個.cpp下的話

// 片段著色器同上
const char *fragmentShaderSource = "#version 330 core\n"
	"out vec4 FragColor;\n"
	"void main()\n"
	"{\n"
	"   FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"        //RGB色,最後一個alpha透明度
	"}\n\0";

編譯片段著色器

這裡比上面少了兩行報錯內容的宣告

// 編譯片段著色器,原理基本同上
unsigned int fragmentShader;                                     //建立片段著色器物件                     
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);             //建立頂點著色器
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);  //要編譯的著色器,傳遞的原始碼字串數量,頂點著色器真正的原始碼,(未知)
glCompileShader(fragmentShader);
//判斷編譯是否成功
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success)                                                    //判斷編譯是否成功
{
	glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
	std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
}

 

著色器程式

著色器程式是類似載入頂點著色器和片段著色器

程式碼一如既往的相似hhhh

順便最後因為著色器程式全部get啦,那麼頂點著色器和片段著色器就不需要留著資料了

其實從某種意義上感覺片段著色器和頂點著色器對於著色器程式做的是一個封裝處理,這樣易於管理

// 著色器程式
unsigned int shaderProgram;                                      //建立著色器程式物件
shaderProgram = glCreateProgram();                               //建立著色器程式
glAttachShader(shaderProgram, vertexShader);                     //把頂點著色器附加到著色器程式
glAttachShader(shaderProgram, fragmentShader);                   //把片段著色器附加到著色器程式
glLinkProgram(shaderProgram);                                    //對附加的著色器連結到著色器程式
// 檢查連結是否成功
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
	glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
	std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
// 刪除不需要的著色器
glDeleteShader(vertexShader);                      
glDeleteShader(fragmentShader);

最後如果要使用著色器的話,需要再while迴圈函式裡寫

glUseProgram(shaderProgram);             //啟用程式物件

 

連結頂點屬性

它主要是告訴編譯器如何解析頂點一類的

//設定頂點屬性(對應頂點著色器中layout(location=0)),頂點屬性大小(vec3對應大小為3),指定資料的型別
//是否被標準化(?和是否有符號有關),步長(每個頂點的間隔),強制型別轉換+起始點的偏移量
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);  //用來解析頂點
glEnableVertexAttribArray(0);        //以頂點屬性位置值為引數,啟用頂點屬性

 

頂點陣列物件(VAO)

 

VAO是用來記錄頂點屬性的

unsigned int VAO;
glGenVertexArrays(1, &VAO);

glBindVertexArray(VAO);

原始碼有把VAO和VBO合在一起,所以來一個合起來的版本吧

unsigned int VAO, VBO;                 //VAO頂點陣列物件,VBO頂點緩衝物件的id
glGenVertexArrays(1, &VAO);            //生成頂點陣列物件,給VAO這個id
glGenBuffers(1, &VBO);                 //生成頂點緩衝物件,給VBO這個id。型別為GL_ARRAY_BUFFER

glBindVertexArray(VAO);                //繫結VAO

glBindBuffer(GL_ARRAY_BUFFER, VBO);    //把新建的緩衝繫結到GL_ARRAY_BUFFER
//把頂點資料複製到緩衝的記憶體中
//顯示卡管理給定資料的模式:GL_STATIC_DRAW資料幾乎不會改變,GL_DYNAMIC_DRAW資料會改變很多,GL_STREAM_DRAW資料每次繪製都會改變
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);    //頂點緩衝物件,傳送資料大小,傳送的實際資料,顯示卡管理給定資料的模式
	

//設定頂點屬性(對應頂點著色器中layout(location=0)),頂點屬性大小(vec3對應大小為3),指定資料的型別
//是否被標準化(?和是否有符號有關),步長(每個頂點的間隔),強制型別轉換+起始點的偏移量
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);  //用來解析頂點
glEnableVertexAttribArray(0);        //以頂點屬性位置值為引數,啟用頂點屬性

 

索引緩衝物件(EBO)

EBO又名IBO,可以設定頂點如何生成三角形

float vertices[] = {
    0.5f, 0.5f, 0.0f,   // 右上角
    0.5f, -0.5f, 0.0f,  // 右下角
    -0.5f, -0.5f, 0.0f, // 左下角
    -0.5f, 0.5f, 0.0f   // 左上角
};

unsigned int indices[] = { // 注意索引從0開始! 
    0, 1, 3, // 第一個三角形
    1, 2, 3  // 第二個三角形
};

unsigned int EBO;
glGenBuffers(1, &EBO);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

又是整合版的

unsigned int VAO, VBO, EBO;            //VAO頂點陣列物件,VBO頂點緩衝物件,EBO索引緩衝物件
glGenVertexArrays(1, &VAO);            //生成頂點陣列物件,給VAO這個id
glGenBuffers(1, &VBO);                 //生成頂點緩衝物件,給VBO這個id。型別為GL_ARRAY_BUFFER
glGenBuffers(1, &EBO);                 //生成索引緩衝物件,給EBO這個id。型別為GL_ELEMENT_ARRAY_BUFFER

glBindVertexArray(VAO);                //繫結VAO

glBindBuffer(GL_ARRAY_BUFFER, VBO);    //把VBO繫結到緩衝區GL_ARRAY_BUFFER
//把頂點資料複製到緩衝的記憶體中
//顯示卡管理給定資料的模式:GL_STATIC_DRAW資料幾乎不會改變,GL_DYNAMIC_DRAW資料會改變很多,GL_STREAM_DRAW資料每次繪製都會改變
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);    //頂點緩衝物件,傳送資料大小,傳送的實際資料,顯示卡管理給定資料的模式

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);     //把EBO繫結到緩衝區GL_ELEMENT_ARRAY_BUFFER
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);  //設定緩衝區型別

	

//設定頂點屬性(對應頂點著色器中layout(location=0)),頂點屬性大小(vec3對應大小為3),指定資料的型別
//是否被標準化(?和是否有符號有關),步長(每個頂點的間隔),強制型別轉換+起始點的偏移量
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);  //用來解析頂點
glEnableVertexAttribArray(0);        //以頂點屬性位置值為引數,啟用頂點屬性


//解綁防止意外更改
// note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
glBindBuffer(GL_ARRAY_BUFFER, 0);
// You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other
// VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
glBindVertexArray(0);

 

生成圖形

最後只要在while裡面執行一下就好啦

生成三角形

 

這裡是用glDrawArrays執行的

glUseProgram(shaderProgram);             //啟用程式物件
glBindVertexArray(VAO);                  //每次需要重新繫結一下VAO
glDrawArrays(GL_TRIANGLES, 0, 3);        //設定繪製三角形,有3個頂點

生成矩形

生成矩形需要通過兩個三角形來繪製

OpenGL這裡給人的感覺好蠢。。為什麼不能直接生成矩形,想哭。。

glUseProgram(shaderProgram);             //啟用程式物件
glBindVertexArray(VAO);                  //每次需要重新繫結一下VAO
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

 

 程式碼總彙

#include <glad/glad.h>
#include <glfw3.h>

#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);      //視窗回撥函式
void processInput(GLFWwindow *window);  //輸入控制

// settings 初始化設定
const unsigned int SCR_WIDTH = 800;     //初始寬度
const unsigned int SCR_HEIGHT = 600;    //初始高度

// GLSL頂點著色器,一般來說著色器會寫在其它頁面
const char *vertexShaderSource = "#version 330 core\n"      //GLSL版本,與OpenGL版本對應
	"layout (location = 0) in vec3 aPos;\n"
	"void main()\n"
	"{\n"
	"   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"  //可以把vec3的資料看作vec4構造器的引數,最後一個是透視除法(?)
	"}\0";

// 片段著色器同上,這個是管理顏色的
const char *fragmentShaderSource = "#version 330 core\n"
	"out vec4 FragColor;\n"
	"void main()\n"
	"{\n"
	"   FragColor = vec4(1.0f, 0.5f, 0.8f, 1.0f);\n"        //RGB色,最後一個alpha透明度
	"}\n\0";



int main()
{
	// glfw: initialize and configure   不需要做任何改動
	glfwInit();                                             //初始化GLFW
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);          //主版本號為3
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);          //次版本號為3
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);   //核心模式

#ifdef __APPLE__
	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);    // 對於OS X
#endif


	// glfw 視窗物件   不需要改動
	GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);   //視窗:寬、高、名稱。
	if (window == NULL)    //確保正確建立視窗
	{
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);                                       //設定視窗上下文為當前執行緒
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);    //告訴glfw視窗大小會根據改變


	// glad: load all OpenGL function pointers  載入系統相關的OpenGL函式指標地址的函式   不需要改動
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}


	// 編譯頂點著色器
	unsigned int vertexShader;                                       //建立頂點著色器物件
	vertexShader = glCreateShader(GL_VERTEX_SHADER);                 //建立頂點著色器
	glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);      //要編譯的著色器,傳遞的原始碼字串數量,頂點著色器真正的原始碼,(未知)
	glCompileShader(vertexShader);
	// 判斷編譯是否成功
	int success;                                                     //獲取著色器編譯是否成功的引數
	char infoLog[512];                                               //如果編譯失敗獲取失敗的內容
	glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);        //檢查編譯是否成功
	if (!success)                                                    //如果編譯失敗,輸出資訊
	{
		glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::VERTEX::COMPLIATION_FILED\n" << infoLog << std::endl;
	}

	// 編譯片段著色器,原理基本同上
	unsigned int fragmentShader;                                     //建立片段著色器物件                     
	fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);             //建立頂點著色器
	glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);  //要編譯的著色器,傳遞的原始碼字串數量,頂點著色器真正的原始碼,(未知)
	glCompileShader(fragmentShader);
	// 判斷編譯是否成功
	glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
	if (!success)                                                    //判斷編譯是否成功
	{
		glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
	}

	// 著色器程式
	unsigned int shaderProgram;                                      //建立著色器程式物件
	shaderProgram = glCreateProgram();                               //建立著色器程式
	glAttachShader(shaderProgram, vertexShader);                     //把頂點著色器附加到著色器程式
	glAttachShader(shaderProgram, fragmentShader);                   //把片段著色器附加到著色器程式
	glLinkProgram(shaderProgram);                                    //對附加的著色器連結到著色器程式
	// 檢查連結是否成功
	glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
	if (!success) {
		glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
	}
	// 刪除不需要的著色器
	glDeleteShader(vertexShader);                      
	glDeleteShader(fragmentShader);





	//頂點座標x,y,z,需要在-1到1之間,標準數學座標軸
	//如果是2d的話z變為0
	float vertices[] = {                   //儲存的點是x,y,z;x,y,z這樣迴圈的
		 0.5f,  0.5f, 0.0f,
		 0.5f, -0.5f, 0.0f,
		-0.5f, -0.5f, 0.0f,
		-0.5f,  0.5f, 0.0f
	};
	unsigned int indices[] = {             //設定頂點練成三角形
		0, 1, 3,
		1, 2, 3
	};

	unsigned int VAO, VBO, EBO;            //VAO頂點陣列物件,VBO頂點緩衝物件,EBO索引緩衝物件
	glGenVertexArrays(1, &VAO);            //生成頂點陣列物件,給VAO這個id
	glGenBuffers(1, &VBO);                 //生成頂點緩衝物件,給VBO這個id。型別為GL_ARRAY_BUFFER
	glGenBuffers(1, &EBO);                 //生成索引緩衝物件,給EBO這個id。型別為GL_ELEMENT_ARRAY_BUFFER

	glBindVertexArray(VAO);                //繫結VAO

	glBindBuffer(GL_ARRAY_BUFFER, VBO);    //把VBO繫結到緩衝區GL_ARRAY_BUFFER
	//把頂點資料複製到緩衝的記憶體中
	//顯示卡管理給定資料的模式:GL_STATIC_DRAW資料幾乎不會改變,GL_DYNAMIC_DRAW資料會改變很多,GL_STREAM_DRAW資料每次繪製都會改變
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);    //頂點緩衝物件,傳送資料大小,傳送的實際資料,顯示卡管理給定資料的模式

	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);     //把EBO繫結到緩衝區GL_ELEMENT_ARRAY_BUFFER
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);  //設定緩衝區型別

	

	//設定頂點屬性(對應頂點著色器中layout(location=0)),頂點屬性大小(vec3對應大小為3),指定資料的型別
	//是否被標準化(?和是否有符號有關),步長(每個頂點的間隔),強制型別轉換+起始點的偏移量
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);  //用來解析頂點
	glEnableVertexAttribArray(0);        //以頂點屬性位置值為引數,啟用頂點屬性


	//解綁防止意外更改
	// note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	// You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other
	// VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
	glBindVertexArray(0);


	// render loop  渲染迴圈,這樣可以保持一直執行
	while (!glfwWindowShouldClose(window))
	{
		// 輸入
		processInput(window);

		//渲染指令
		glClearColor(0.1f, 0.3f, 0.3f, 1.0f);    //設定清空螢幕所用的顏色,每次迴圈重新渲染,因此需要清空,也因此這是螢幕的背景色。RGB色
		glClear(GL_COLOR_BUFFER_BIT);            //清楚顏色緩衝


		glUseProgram(shaderProgram);             //啟用程式物件
		glBindVertexArray(VAO);                  //每次需要重新繫結一下VAO
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);    //6個頂點

		// 檢查並呼叫事件、交換緩衝
		glfwSwapBuffers(window);      //交換顏色緩衝
		glfwPollEvents();             //檢查事件觸發
	}


	// glfw: terminate, clearing all previously allocated GLFW resources.
	glfwTerminate();     //終止glfw
	return 0;
}

// 輸入控制
void processInput(GLFWwindow *window)
{
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)    //判斷是否按下Esc
		glfwSetWindowShouldClose(window, true);               //如果時Esc,那麼glfw需要關閉視窗
}


// glfw: whenever the window size changed (by OS or user resize) this callback function executes 根據視窗大小改變顯示大小
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	// make sure the viewport matches the new window dimensions; note that width and 
	// height will be significantly larger than specified on retina displays.
	glViewport(0, 0, width, height);    //視窗左下角的座標x、y
}