https://learnopengl.com/Getting-started/Hello-Triangle
梗概:在OpenGL里面我们面对的是三维空间,但是显示器屏幕和窗口是二维的像素排列,所以OpenGL里面我们大部分工作就是把三维坐标变成适合你的显示器的二位像素,这个变换过程在OpenGL里由图像流水线实现。图像流水线可以被分为两大块:首先把三维坐标转换为二维坐标,再把二维坐标变成颜色像素。
流水线被分成几个步骤,每个步骤的输入都依赖于上个步骤的输出。每个步骤有明确的目的并且能平行运行,也因为它们能并行的特点,现在的显卡(Graphics Cards)有上千个处理核心能在图像流水线的每一步中通过在图像处理器(Graphics Processing Unit GPU)上运行小程序快速处理你的数据。这些小程序就叫做shader(着色器)。shader好听,以后不译了。
其中一些shader是可识别的以便于开发者开发自己的shader来代替已有的默认shader,这能让我们更好的控制流水线的具体过程,而且因为shader在GPU上面运行,就可以省内存CPU时间。
蓝色部分是我们可以自由发挥的部分。
传入顶点和顶点属性:vertex attributes,传入顶点数据之后要告诉OpenGL用什么方式来组合这些顶点:GL_POINTS,GL_TRIANGLES,GL_LINE_STRIP,...这是vertex shader干的事情,shape assembly组装过后geometry shader可以加新点,但是我们这门课基本用不到这个。然后会生成fragments,在fragment shader运行之前,会进行裁剪clipping,裁剪会把视口范围外的片段丢弃。OpenGL渲染一个像素所需要的所有数据就是一个fragment。
A fragment in OpenGL is all the data required for OpenGL to render a single pixel.
fragment shader就是用来计算一个像素最后的颜色的,这就是使用所有高级的OpenGL效果的地方。最后通过指定的深度值(遮挡效果)和alpha值等等形成最终的图像。
输入点数据(比如我们之前作业写的off文件提供点的数据),点的坐标都是在[-1,1]范围中(normalized device coordinates)
输入的点的坐标会通过视口变换viewport transform(glViewport)变成显示器空间的坐标,我们要把点的数据通过VBO(vertex buffer objects)存在显卡里,用这个缓存的好处就在于我们能一次送一大批数据到显卡里,从内存送数据到GPU里相对较慢,所以我们尽量送一次就多送一点,OpenGL里面每个东西都有一个独特的名字(ID)
float vertices[]={
...
...
...
};
unsigned int VBO;
glGenBuffers(1,&VBO);
glBindBuffer(GL_ARRAY_BUFFER,VBO);//从此我们对buffer的操作都是对VBO的操作
glBindBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);//专门用来给当前绑定缓存赋值的函数
//GL_STATIC_DRAW: the data will most likely not change at all or //very rarely.
//GL_DYNAMIC_DRAW: the data is likely to change a lot.
//GL_STREAM_DRAW: the data will change every time it is drawn.
//一个简单的vertex_shader,什么操作都没有进行直接把输入的数据输出,
//一般情况下不是这样,因为一般情况下的数据坐标都不是NDC,之前童老师
//非常善良的给了我们NDC,但是当没有遇到如此善良的老师时,我们在
//vertex_shader里要做的第一件事情就是把坐标转换为VDC
#version 330 core
layout(location=0)in vec3 aPos;
void main()
{
gl_Position=vec4(aPos.x,aPos.y,aPos.z,1.0);//通过赋给gl_Position位置来确定vertex_shader的输出
}
//编译使用shader_object
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::COMPILATION_FAILED\n"<<infoLog<<std::endl;
}
//一个简单的fragment_shader
#version 330 core
out vec4 FragColor;//输出量
void main()
{
FragColor=vec4(1.0f,0.5f,0.2f,1.0f);//后面的四个数是RGBA值,
//在[0.0,1.0]之间
}
//编译一个fragment_shader
unsigned int fragmentShader;
fragmentShader=glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader,1,&fragmentShaderSource,NULL);
glCompileShader(fragmentShader);
//编译成功两个shader_oobjects之后,就把shader_objects和
//shader_program_objects link起来
//当我们把shaders连接进program里面时,它把每个shader的输出与下一个
//shader的输入连接起来,如果你的input和output不匹配就会发生
//linking_errors
//创建一个program
unsigned int shaderProgram;
shaderProgram=glCreateProgram();
glAttachShader(shaderProgram,vertexShader);
glAttachShader(shaderProgram,fragmentShader);
glLinkProgram(shaderProgram);
//检验是否连接成功
glGetProgramiv(shaderProgram,GL_LINK_STATUS,&success);
if(!sucess){
glGetProgramInfoLog(shaderProgram,512,NULL,infoLog);
std::cout<<"ERROR::SHADER::VERTEX::COMPILATION_FAILED\n"<<infoLog<<std::endl;
}
//使用program
glUseProgram(shaderProgram);
//一旦连接成功,我们就不再需要之前的shader_objects了
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
//Linking Vertex Attributes,告诉OpenGL应该如何翻译点的数据
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*sizeof(float),(void*)0);//参数解释:
//1.说明我们想识别的点属性,之前我们在vertex shader里面写了
//layout(location=0),把vertex attribute的位置设置为0,所以我们把数
//据传到0
//2.点属性的size,vertex shader里写的是vec3所以我们这里写3
//3.数据类型
//4.是否需要数据被normalize
//5.stride
//6.offset
//因为在使用glVertexAttribPointer之前,VBO仍然和GL_ARRAY_BUFFER绑
//定,多以vertex attribute 0仍然和VBO里的vertex data一样
glEnableVertexAttribArray(0);
//vertex array object(VAO)能够绑定所有vertex attribute
//创建VAO
unsigned int VAO;
glGenVertexArrays(1,&VAO);
glBindBuffer(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);
glVertexAttribPointer(0,3,GL_FLOAT,GL_FLOAT,3*sizeof(float),(void*)0);
glEnableVertexAttribArray(0);
glUseProgram(shaderProgram);
glBindVertexArray(VAO)
glDrawArrays(GL_TRIANGLES,0,3);//someOpenGlFunctionThatDrawsOurTriangle();
//EBO(element buffer objects)存储下标indices
float vertices[]={
...
...
...
}
unsigned int indices[]={
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);
....
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,EBO);
glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0);
绑定EBO的时候不能解绑VAO,因为下标是和顶点对应的。
glBindVertexArray(VAO);//要用VAO里面的数据了
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,sizeof(indices),indices,GL_STATIC_DRAW);
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*sizeof(float),(void*)0);
glEnableVertexAttribArray(0);
...
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElement(GL_TRIANGLES,6,GL_UNSIGNED_INT,0);
glBindVertexArray(0);
//glPolygonMode(GL_FRONT_AND_BACK,GL_FILL/GL_LINE);
GLSL(OpenGL shading language)
语言设计是针对vector和矩阵运算的
一个shader一般以版本号声明开头,然后定义inputs和outputs,uniforms,main()函数
#version version_number
in type in_variable_name;
in type in_variable_name;
out type out_variable_name;
uniform type uniform_name;
void main() {
// process input(s) and do some weird graphics stuff
...
// output processed stuff to output variable
out_variable_name = weird_stuff_we_processed;
}
input的量就是vertex_attributes,能够声明的vertex_attributes的数量是有限的
int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
一般情况下我们会得到16,基本足够使用了
GLSL里面支持的数据类型有int,float,double,uint,bool,容器类型有:vectors and matrices
vectors里面有
vecn
: the default vector ofn
floats.bvecn
: a vector ofn
booleans.ivecn
: a vector ofn
integers.uvecn
: a vector ofn
unsigned integers.dvecn
: a vector ofn
double components.
n=1,2,3,4
因为我们一般用float型数据,所以vecn就够用了,
vec.x/.y/.z/.w获取第一、二、三、四个元素
也可以用vec.r/g/b/a->colors
vec.s/t/p/q->textures
vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;//swizzling
vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);
vertex shader:
inputs:从vertex data里面获取数据,指定vertex data的位置元数据,所以shader可以从CPU里面识别vertex attributes
layout(location=0)
outputs:vec4 color output variable
类型相同、名字一样的inputs和outputs就是连接在一起的
Vertex shader
#version 330 core
layout (location = 0) in vec3 aPos; // the position variable has attribute position 0
out vec4 vertexColor; // specify a color output to the fragment shader
void main()
{
gl_Position = vec4(aPos, 1.0); // see how we directly give a vec3 to vec4's constructor
vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // set the output variable to a dark-red color
}
Fragment shader
#version 330 core
out vec4 FragColor;
in vec4 vertexColor; // the input variable from the vertex shader (same name and same type)
void main()
{
FragColor = vertexColor;
}
上面就是用vertex_color决定了frag_color
Uniforms:就是一种将数据从CPU的application中传递到GPU的shader的方法,而且对于会在迭代中改变的attributes或者是在application和shaders里面交换数据会比较方便,uniform可以看作是一种更加灵活好用的in
uniforms是全局的,它的值可以被shader program中的任意一个着色器,不管是vertex shader还是fragment shader在任意一个阶段获取,而且直到uniform的值被改变之前,uniform保存它的值
//fragment_shader
#version 330 core
out vec4 FragColor;
uniform vec4 ourColor;
// we set this variable in the OpenGL code.
void main() {
FragColor = ourColor;
}
#uniform的就是大家的#
如果定义的uniform的值到处都没有用到,编译的时候就会被抹掉,会导致出错
给uniform传入数据:
首先要指定uniform attribute的位置
float timeValue=glfwGetTime();
float greenValue=(sin(timeValue)/2.0f)+0.5f;
int vertexColorLocation=glGetUniformLocation(shaderProgram,"ourColor");//这个不一定要在glUseProgram前面使用
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation,0.0f,greenValue,0.0f,1.0f);要想更新uniform的数据,得先使用program
//OpenGL不支持重载(因为是inputs match outputs,step by step么?),所以glUniform4f后面要有f后缀
f
: the function expects afloat
as its valuei
: the function expects anint
as its valueui
: the function expects anunsigned int
as its value3f
: the function expects 3float
s as its valuefv
: the function expects afloat
vector/array as its value
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES,0,3);
多属性:
float vertices[] = {
// positions // colors
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left
0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // top
};
vertex shader:
#version 330 core
layout (location = 0) in vec3 aPos; // the position variable has attribute position 0
layout (location = 1) in vec3 aColor; // the color variable has attribute position 1
out vec3 ourColor; // output a color to the fragment shader
void main()
{
gl_Position = vec4(aPos, 1.0);
ourColor = aColor; // set ourColor to the input color we got from the vertex data
}
fragment shader:
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
void main()
{
FragColor=vec4(ourColor,1.0);
}
在点坐标的基础上加了点的颜色,就要重新告诉OpenGL怎么读vertex data
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,6*sizeof(float),(void*)0);
glEnableVertexAttribArray(0);
glVertexAttribArray(1,3,GL_FLOAT,GL_FALSE,6*sizeof(float),(void*)(3*sizeof(float)));//stride相当于一套attribute有几个数据
glEnableVertexAttribArray(1);
#ifndef SHADER_H
#define SHADER_H
#include <glad/glad.h> // include glad to get all the required OpenGL headers
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
class Shader
{
public:
// the program ID
unsigned int ID;
// constructor reads and builds the shader
Shader(const GLchar* vertexPath, const GLchar* fragmentPath);
// use/activate the shader
void use();
// utility uniform functions
void setBool(const std::string &name, bool value) const;
void setInt(const std::string &name, int value) const;
void setFloat(const std::string &name, float value) const;
};
#endif
Shader(const char* vertexPath, const char* fragmentPath) {
// 1. retrieve the vertex/fragment source code from filePath
std::string vertexCode;
std::string fragmentCode;
std::ifstream vShaderFile;
std::ifstream fShaderFile;
// ensure ifstream objects can throw exceptions:
vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
try {
// open files
vShaderFile.open(vertexPath);
fShaderFile.open(fragmentPath);
std::stringstream vShaderStream, fShaderStream;
// read file's buffer contents into streams
vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();
// close file handlers
vShaderFile.close();
fShaderFile.close();
// convert stream into string
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
}
catch(std::ifstream::failure e)
{
std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
}
const char* vShaderCode = vertexCode.c_str();
const char* fShaderCode = fragmentCode.c_str();
[...]
然后编译shaders
void use()
{
glUseProgram(ID);
}
//相当于uniform的重载了
void setBool(const std::string &name, bool value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
}
void setInt(const std::string &name, int value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
}
void setFloat(const std::string &name, float value) const
{
glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}
Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs");
...
while(...) {
ourShader.use();
ourShader.setFloat("someUniform", 1.0f);
DrawStuff();
}
在第十次作业中,要求把之前在兼容模式下写的读off网格改成核心模式下,主要改变有:
1.创建了shader.h,Shader class,主要在里面实现vertex shader和fragment shader的编译实现,主要是参考learnOpenGL上面给的代码
2.调用老师给的创建矩阵的头文件vmath.h来写旋转、平移、放缩变化,要注意矩阵乘的顺序:projection*view*model,伸缩和平移老师推荐在projection里面实现,但是我其实是写在view矩阵的Lookat里面,但是老师这一次有点小失误的是给的rotate代码有点问题,没有标准化,导致旋转的时候会变形,从观察鼠标移动距离与形变的关系发现了这一点,把老师的代码改过来了,或者直接调用glm库也可以。
3.shaders里面加上了uniform量:用来赋矩阵,uniform很适合存需要实时更新的量,之前理解错了以为坐标量也需要用uniform传进去,但是其实想想uniform能够存的就那么点数据就知道不太可能,而且已经用VAO,VBO搞定了点坐标的数据,这里是自己有点混淆了,只有坐标和颜色结构还是比较简单的,后面加上texture估计会变复杂。
总体来说刚刚进入核心模式就已经觉得很有趣了,shaders的概念都非常新颖,代码看起来也更加舒服,不过这只是刚刚进了门,囫囵的把大致框架搞定了,具体的技术问题还得慢慢思考体会啦!