OpenGL: Shader、GLSL和Uniform

一、Qt UI调用OpenGL控件功能
    1、如果需要从paintGL()以外位置触发重新绘制(典型示例是使用计时器设置场景动画),则应调用Widget的update()函数来安排更新。
    2、在paintGL()、resizeGL()、initializeGL()里调用标准OpenGL API函数,Widget的OpenGL上下文将自动变为当前。
    3、但在其它函数里调用标准OpenGL API函数,则必须首先调用makeCurrent(),最后调用doneCurrent()。
    4、在paintGL()以外的地方调用OpenGL的绘制函数,没有意义,因为绘制图像会被paintGL()覆盖。
    5、VAO、VBO、EBO、Shader、ShaderProgram创建对象后,也是需要删除回收的。
       glDeleteVertexArrays()
       glDeleteBuffers()
       glDeleteShader()
       glDeleteProgram()

二、着色器
    1、着色器(Shader)是运行在GPU上的小程序。
    2、这些小程序为图形渲染管线的某个特定部分而运行。
    3、着色器是一种把输入转化为输出的程序。
    4、着色器是一种非常独立的程序,因为它们之间不能互相通信,唯一的沟通只有通过输入和输出。


三、GLSL
    1、着色器是用GLSL写的,GLSL是一种类C语言。
    2、GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。
    3、着色器结构:
    

  #version version_number           //声明版本
  in type in_variable_name;         //输入变量1
  in type in_variable_name;         //输入变量2...

  out type out_variable_name;       //输出变量

  uniform type uniform_name;        //uniform

  int main()                        //main函数
  {
       // 处理输入并进行一些图形操作
       ...
       // 输出处理过的结果到输出变量
       out_variable_name = weird_stuff_we_processed;
  }


    4、在顶点着色器中,每个输入变量也叫顶点属性(Vertex Attribute),属性如位置、颜色、纹理...。
    5、开发者能声明的顶点属性有上限,一般由硬件决定,OpenGL确保至少有16个包含4分量的顶点属性可用,通过glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &n)可查询上限。

四、数据类型
    1、GLSL包含基本数据类型: int 、 float 、 double 、 uint 、 bool。
    2、GLSL包含两种容器类型: 向量(Vector)和矩阵(Matrix)。
    3、GLSL的向量是一个可以包含2、3或4个分量的容器,分量的类型可以是基本数据类型的任意一个。
       vecn               包含n个float分量的默认向量
       bvecn              包含n个bool分量的向量
       ivecn              包含n个int分量的向量
       uvecn              包含n个uint分量的向量
       dvecn              包含n个double分量的向量
    4、获取向量的分量方式:
       * 通过vec.x、vec.y、vec.z、vec.w获取向量的第1、2、3、4个分量。
       * 通过vec.r、vec.g、vec.b、vec.a获取代表颜色的向量的第1、2、3、4个分量。
       * 通过vec.s、vec.t、vec.p、vec.q获取代表纹理的向量的第1、2、3、4个分量。
       * 通过重组(Swizzling),如:
         vec2 someVec;
         vec4 differentVec = someVec.xyxx;
         vec3 anotherVec = differentVec.zyw;
         vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
         vec2 vect = vec2(0.5, 0.7);
         vec4 result = vec4(vect, 0.0, 0.0);
         vec4 otherResult = vec4(result.xyz, 1.0);


五、输入与输出
    1、着色器虽是各自独立的小程序,但又是渲染管线整体的一部分,因此着色器有输入和输出,数据才能交流与传递。
    2、GLSL用in关键字设定输入,用out关键字设定输出。
    3、顶点着色器中, 用layout(location = 0),把顶点着色器链接到顶点数据,从顶点数据中直接接收输入。
    4、片段着色器中,需要一个vec4颜色输出变量,如果没有定义输出颜色,OpenGL会把物体渲染为黑色(或白色)。
    5、若开发者打算从一个着色器向另一个着色器发送数据,则:
       在发送着色器中声明一个输出变量;
       在接收着色器中声明一个输入变量;
       这两个变量类型和名字都一样,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了。
    6、例如: 从顶点着色器给片段着色器发送数据
       顶点着色器:
      

 #version 330 core
 layout (location = 0) in vec3 aPos;   // 位置变量的属性位置值为0

 out vec4 vertexColor;                 // 为片段着色器指定一个颜色输出

 void main()
 {
      gl_Position = vec4(aPos, 1.0);          // 注意我们如何把一个vec3作为vec4的构造器的参数
      vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色
 }

       片段着色器:
    

  #version 330 core
  out vec4 FragColor;

  in vec4 vertexColor;   // 从顶点着色器传来的输入变量(名称相同、类型相同)

  void main()
  {
       FragColor = vertexColor;
  }

六、Uniform
    1、Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式。
    2、Uniform是全局的(Global),Uniform变量在着色器中必须独一无二。
    3、Uniform变量值在着色器程序中不可被改变(测试改了会渲染成白色),只有在CPU应用中可被重置或更新。
    4、可以在着色器中添加uniform关键字至类型和变量名前来声明一个GLSL的uniform变量。
    5、例如: 从CPU应用中向着色器发送数据
       顶点着色器
      

#version 330 core

layout(location = 0) in vec3 aPos;

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


       片段着色器
      

 #version 330 core
       
 out vec4 FragColor;

 uniform vec4 ourColor;       //在OpenGL程序代码中设定这个变量

 void main()
 {
     FragColor = ourColor;
 }


    6、注意,如果在着色器中声明了一个uniform变量,却在着色器中没有用过它,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致错误。
    7、在CPU应用中,得先通过glGetUniformLocation(shaderProgram, "ourColor")找到着色器中uniform变量的位置值,返回-1表示没有找到位置值。
    8、得到uniform变量的位置值后,可以通过glUniform4f()函数设置uniform变量值。
    9、注意,必须在设置uniform变量值之前,先使用着色器程序(glUseProgram()),因为它是在当前激活的着色器程序中设置uniform的。
    10、glUniform()函数有后缀,用来标识设定的uniform的类型。
       f      函数需要一个float值
       i      函数需要一个int值
       ui     函数需要一个unsigned int值
       3f     函数需要三个float值
       fv     函数需要一个float向量/数组值
       ......
    11、uniform对于设置一个在渲染迭代中会改变的属性是一个非常有用的工具。

七、更多属性
    1、我们可以把颜色数据加入到顶点数据中,传入到VBO。
    2、运行代码,会发现一个大调色盘,这是片段插值(Fragment Interpolation)的结果。
       当渲染一个矩形时,光栅化阶段通常会造成比原指定顶点更多的片段。
       光栅化阶段会根据每个片段在矩形上相对位置,插值(Interpolate)所有片段着色器的输入变量。
       比如一个线段,上面的端点是绿色,下面的端点是蓝色,如果一个片段着色器在线段的70%位置(从下到上)运行,则它的颜色输入变量会被插值为30%蓝+70%绿。
    3、片段插值会被应用到片段着色器的所有输入属性上。

八、代码

//MyShader.h

#pragma once

#include 


class MyShader : public QOpenGLFunctions_4_5_Core
{
public:
	MyShader(const std::string & vtShaderPath, const std::string & fmShaderPath);
	~MyShader();

	void Use();

	void SetBool(const std::string & name, bool value);
	void SetInt(const std::string & name, int value);
	void SetFloat3(const std::string & name, float value1, float value2, float value3);

private:
	void CheckCompileErrors(unsigned int id, std::string type);

private:
	unsigned int m_shaderProgramID;
};




//MyShader.cpp

#include "MyShader.h"

#include 
#include 
#include 


MyShader::MyShader(const std::string & vtShaderPath, const std::string & fmShaderPath)
{
	std::string vtShaderCode;
	std::string fmShaderCode;

	std::ifstream vtShaderFile;
	std::ifstream fmShaderFile;

	//确保ifstream对象可以抛异常
	vtShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
	fmShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);

	try
	{
		vtShaderFile.open(vtShaderPath.c_str());
		fmShaderFile.open(fmShaderPath.c_str());

		std::stringstream vtShaderStrStream;
		std::stringstream fmShaderStrStream;

		vtShaderStrStream << vtShaderFile.rdbuf();
		fmShaderStrStream << fmShaderFile.rdbuf();

		vtShaderFile.close();
		fmShaderFile.close();

		vtShaderCode = vtShaderStrStream.str();
		fmShaderCode = fmShaderStrStream.str();
	}
	catch (std::ifstream::failure & e)
	{
		std::cout << "ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ: " << e.what() << std::endl;
	}

	initializeOpenGLFunctions();

	unsigned int vertexShaderID, fragmentShaderID;
	const char * vtShaderCodeStr = vtShaderCode.c_str();
	const char * fmShaderCodeStr = fmShaderCode.c_str();

	//顶点着色器
	vertexShaderID = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(vertexShaderID, 1, &vtShaderCodeStr, NULL);
	glCompileShader(vertexShaderID);
	CheckCompileErrors(vertexShaderID, "VERTEX");

	//片段着色器
	fragmentShaderID = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fragmentShaderID, 1, &fmShaderCodeStr, NULL);
	glCompileShader(fragmentShaderID);
	CheckCompileErrors(fragmentShaderID, "FRAGMENT");

	//着色器程序
	m_shaderProgramID = glCreateProgram();
	glAttachShader(m_shaderProgramID, vertexShaderID);
	glAttachShader(m_shaderProgramID, fragmentShaderID);
	glLinkProgram(m_shaderProgramID);
	CheckCompileErrors(m_shaderProgramID, "PROGRAM");

	//删除着色器
	glDeleteShader(vertexShaderID);
	glDeleteShader(fragmentShaderID);
}


MyShader::~MyShader()
{
	glDeleteProgram(m_shaderProgramID);
}


void MyShader::Use()
{
	glUseProgram(m_shaderProgramID);
}


void MyShader::SetBool(const std::string & name, bool value) 
{
	glUniform1i(glGetUniformLocation(m_shaderProgramID, name.c_str()), (int)value);
}


void MyShader::SetInt(const std::string & name, int value) 
{
	glUniform1i(glGetUniformLocation(m_shaderProgramID, name.c_str()), value);
}


void MyShader::SetFloat3(const std::string & name, float value1, float value2, float value3)
{
	glUniform3f(glGetUniformLocation(m_shaderProgramID, name.c_str()), value1, value2, value3);
}


void MyShader::CheckCompileErrors(unsigned int id, std::string type)
{
	int success = 0;
	char infoLog[512] = { 0 };

	if (type != "PROGRAM")
	{
		glGetShaderiv(id, GL_COMPILE_STATUS, &success);
		if (!success)
		{
			glGetShaderInfoLog(id, 512, NULL, infoLog);
			std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
		}
	}
	else
	{
		glGetProgramiv(id, GL_LINK_STATUS, &success);
		if (!success)
		{
			glGetProgramInfoLog(id, 512, NULL, infoLog);
			std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
		}
	}
}




//MyOpenGLWidget.h

#pragma once

//需要两个头文件
#include 
#include 
#include "MyShader.h"


class MyOpenGLWidget : public QOpenGLWidget, QOpenGLFunctions_4_5_Core
{
	Q_OBJECT

public:
	MyOpenGLWidget(QWidget * parent = nullptr);
	~MyOpenGLWidget();

	enum Shape { None, Rect, Circle, Triangle };

	void DrawShape(Shape shape);
	void WireFrame(bool bWire);

protected:
	void initializeGL() override;
	void resizeGL(int w, int h) override;
	void paintGL() override;

private:
	unsigned int VAO;
	unsigned int VBO;
	unsigned int EBO;

	float m_ramValue = 0.0f;

	Shape m_shape = None;

	MyShader * m_shaderProgram = nullptr;
};



//MyOpenGLWidget.cpp

#include "MyOpenGLWidget.h"
#include 


MyOpenGLWidget::MyOpenGLWidget(QWidget * parent) : QOpenGLWidget(parent)
{
}


MyOpenGLWidget::~MyOpenGLWidget()
{
	makeCurrent();
	glDeleteBuffers(1, &VBO);
	glDeleteBuffers(1, &EBO);
	glDeleteVertexArrays(1, &VAO);
	delete m_shaderProgram;
	doneCurrent();
}


void MyOpenGLWidget::DrawShape(Shape shape)
{
	//触发重新绘制
	m_shape = shape;
	update();
}


void MyOpenGLWidget::WireFrame(bool bWire)
{
	makeCurrent();
	if (bWire)
	{
		glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
	}
	else
	{
		glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
	}
	doneCurrent();

	update();
}


void MyOpenGLWidget::initializeGL()
{
	initializeOpenGLFunctions();

	//创建VAO、VBO、EBO,并赋予ID
	glGenVertexArrays(1, &VAO);
	glGenBuffers(1, &VBO);
	glGenBuffers(1, &EBO);

	//绑定VAO、VBO、EBO对象
	glBindVertexArray(VAO);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);  //注意, 因为VAO已经绑定,此句会被VAO的最后一个指针偷偷记录下来, 指向EBO。
	
	//顶点数据
	float vertices[] = 
	{
		//位置                //颜色
		 0.5f,  0.5f,  0.0f,  1.0f, 0.0f, 0.0f,   // 右上角
		 0.5f, -0.5f,  0.0f,  0.0f, 1.0f, 0.0f,   // 右下角
		-0.5f, -0.5f,  0.0f,  0.0f, 0.0f, 1.0f,   // 左下角
		-0.5f,  0.5f,  0.0f,  0.3f, 0.5f, 0.8f,   // 左上角
	};
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

	//索引数据
	unsigned int indices[] = 
	{
		0, 1, 3, // 第一个三角形
		1, 2, 3  // 第二个三角形
	};
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

	//配置VAO位置属性指针
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)0);
	glEnableVertexAttribArray(0);
	//配置VAO颜色属性指针
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)(3 * sizeof(float)));
	glEnableVertexAttribArray(1);

	//解绑VAO、VBO、EBO
	glBindVertexArray(0);
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);   //注意,因为VAO已经解绑了,所以此句不会被VAO记录下来,VAO的最后一个指针依然指向EBO,这样好处是绘制前,只要再绑定VAO即可,EBO、VBO都不用再绑定。

	//(1) 顶点着色器向片段着色器传递数据
	//(2) 从CPU应用中向GPU着色器传递数据
	//(3) 从顶点数据发送更多属性数据到GPU
	m_shaderProgram = new MaLanShader("VertexShaderCode.txt", "FragmentShaderCode.txt");
}


void MyOpenGLWidget::resizeGL(int w, int h)
{
}


//绘制都是在此函数中,其它地方都是触发绘制
void MyOpenGLWidget::paintGL()
{
	glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT);

	glBindVertexArray(VAO);
	m_shaderProgram->Use();

	//(2) 从CPU应用中向GPU着色器传递数据
	/*if (m_ramValue >= 1.0f)
	{
		m_ramValue = 0.0f;
	}
	m_ramValue += 0.1f;
	m_shaderProgram->SetFloat3("ourColor", 0.0f, m_ramValue, 0.0f);*/

	switch (m_shape)
	{
	case MaLanOpenGLWidget::None:
		break;
	case MaLanOpenGLWidget::Rect:
	{
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
		break;
	}
	case MaLanOpenGLWidget::Circle:
		break;
	case MaLanOpenGLWidget::Triangle:
		break;
	default:
		break;
	}
}




//MainWindow.h

#pragma once

#include 


QT_BEGIN_NAMESPACE
namespace Ui { class MainWindowClass; };
QT_END_NAMESPACE


class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget * parent = nullptr);
    ~MainWindow();

private:
	void ActDrawRectTriggered(bool checked);
	void ActClearTriggered(bool checked);
	void ActWireFrameTriggered(bool checked);

private:
    Ui::MainWindowClass * ui;
};




//MainWindow.cpp

#include "MainWindow.h"
#include "ui_MainWindow.h"


MainWindow::MainWindow(QWidget * parent): QMainWindow(parent), ui(new Ui::MainWindowClass())
{
    ui->setupUi(this);
	setCentralWidget(ui->openGLWidget);

	connect(ui->actDrawRect, &QAction::triggered, this, &MainWindow::ActDrawRectTriggered);
	connect(ui->actClear, &QAction::triggered, this, &MainWindow::ActClearTriggered);
	connect(ui->actWireFrame, &QAction::triggered, this, &MainWindow::ActWireFrameTriggered);
}


MainWindow::~MainWindow()
{
    delete ui;
}


void MainWindow::ActDrawRectTriggered(bool checked)
{
	ui->openGLWidget->DrawShape(MaLanOpenGLWidget::Rect);
}


void MainWindow::ActClearTriggered(bool checked)
{
	ui->openGLWidget->DrawShape(MaLanOpenGLWidget::None);
}


void MainWindow::ActWireFrameTriggered(bool checked)
{
	ui->openGLWidget->WireFrame(checked);
}



九、VertexShaderCode.txt内容

1、顶点着色器向片段着色器传递数据

#version 330 core

layout(location = 0) in vec3 aPos;
out vec4 vertexColor;

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

2、从CPU应用中向GPU着色器传递数据

#version 330 core

layout(location = 0) in vec3 aPos;

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

3、从顶点数据发送更多属性数据到GPU

#version 330 core

layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aColor;

out vec3 ourColor;

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

十、FragmentShaderCode.txt内容

1、顶点着色器向片段着色器传递数据

#version 330 core

in vec4 vertexColor;
out vec4 FragColor;

void main()
{
    FragColor = vertexColor;
}

2、从CPU应用中向GPU着色器传递数据

#version 330 core

out vec4 FragColor;

uniform vec4 ourColor;

void main()
{
    FragColor = ourColor;
}

3、从顶点数据发送更多属性数据到GPU

#version 330 core

in vec3 ourColor;
out vec4 FragColor;

void main()
{
    FragColor = vec4(ourColor, 1.0);
}

你可能感兴趣的:(OpenGL+Qt,计算机视觉,图形渲染)