好久没写过GL了,正好来发发教程
要求
给出兔兔的顶点坐标与三角面索引,需要实现:
- 绘制模型,冯氏光照
- 模型及视角移动
- 鼠标点选高亮某三角面
总体思路 & 坑点
For 可以自己实现OpenGL编写的朋友
导入模型 :给出的数据为顶点坐标,没有法线信息,需要求解法线,在我的实现中对每一三角面根据三点坐标求解了法线,没有进行法线插值,这会使得兔兔表面不够圆润,并且需要 NumFace * 3 大小的VBO,较浪费空间。按道理可以对每一点求解法线,使用该点与邻接点的向量进行加权平均,具体可以参考链接 Weighted Vertex Normals
光照模型:没什么特别的,Blinn-Phong或者Phong的Shader
模型及视角移动:也没什么特别的,取下帧间鼠标Δ值和键盘按键变model view矩阵就行了
点选高亮:这个还蛮有趣的,想了个办法,应该不是最优解,用一个drawcall绘制一张每个面颜色都是该面索引值 / 总面数的RT,然后readBack一下鼠标位置的颜色,拿到高亮面的顶点,这里我直接再加了一个drawcall画这三角,应该是多余了。
实现细节
For 不怎么熟悉OpenGL的朋友
配置OpenGL环境
- 使用GLFW初始化窗口
1 | GLFW is a lightweight utility library for use with OpenGL. GLFW stands for Graphics Library Framework. It provides programmers with the ability to create and manage windows and OpenGL contexts, as well as handle joystick, keyboard and mouse input. |
一般使用GLFW作为跨平台的窗口工具,在本例中就作为创建OpenGL绘制窗口,处理鼠标键盘事件的API。
- 使用GLAD链接OpenGL API
可以简单认为,虽然各个平台都支持OpenGL绘制,但一般都仅仅是提供了按OpenGL标准所实现的二进制链接库,如WIndows下默认静态链接库会有opengl32.lib,但仍然需要一个第三方库去在运行时加载这一dll,提供一个符合标准的c++头文件,并且将二进制库中的实现加载到对应的函数API上。
1 | void Application::Run() |
处理输入
- 将所有点与索引输入,按面顺序排序,每个点有7个float的长度:
- 位置:vec3
- 法线:vec3
- 面编号(归一化):float
1 | void Application::Init() { |
- 求面法线:求两向量叉积,实现为化简过后的过程。
1 | void calcNormal(float* v1, float* v2, float* v3, float* nor) { |
初始化Shader
本实现中使用了三个Shader,分别用于绘制面索引,光照模型,高亮三角
1
2
3
4
5void Application::InitShader() {
mShaders["BlinnPhong"] = new Shader("BlinnPhong");
mShaders["FaceIndex"] = new Shader("FaceIndex");
mShaders["postprocess"] = new Shader("postprocess");
}从文件读入Shader代码(这里有个bug,在文件尾会读入几个乱码字符,这里在shader最后手动加换行暂时删掉了乱码)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28Shader::Shader(const std::string& shaderName)
{
const std::string vertShaderName = shaderName + ".vert";
const std::string fragShaderName = shaderName + ".frag";
std::ifstream vertFile(vertShaderName);
std::ifstream fragFile(fragShaderName);
if (!vertFile || !fragFile) {
assert(false);
}
vertFile.seekg(0, std::ios::end);
int length = vertFile.tellg();
GLchar* vertexShaderCode = new GLchar[length];
vertFile.seekg(0, std::ios::beg);
vertFile.read(vertexShaderCode, length);
while (vertexShaderCode[length - 1] != '\n') { //TODO
vertexShaderCode[length - 1] = 0;
length--;
}
fragFile.seekg(0, std::ios::end);
length = fragFile.tellg();
GLchar* fragmentShaderCode = new GLchar[length];
fragFile.seekg(0, std::ios::beg);
fragFile.read(fragmentShaderCode, length);
while (fragmentShaderCode[length - 1] != '\n') { //TODO
fragmentShaderCode[length - 1] = 0;
length--;
}编译Shader代码
1 | GLuint VertexShader = glCreateShader(GL_VERTEX_SHADER); |
- 传入uniform 以及 绑定program
1 | // Check location |
初始化顶点缓冲
- 主顶点缓冲,包含所有顶点信息
1 |
|
- 高亮三角缓冲,仅三个点,使用Dynamic Draw声明该数据会实时变化,令GPU对数据存储进行优化
1 | glGenVertexArrays(1, &CoveredVAO); |
初始化FrameBuffers
- 一个FrameBuffer表示一组Render Target(可以理解为绘制到的一张窗口大小的图片)的集合,包含若干color attachment以及一个depth-stencil attachment,本例中为了绘制FaceIndex使用一个R32F的纹理作为RT绑定。
1 | glGenFramebuffers(1, &FaceIndexFBO); // generate frame buffer |
主绘制流程
- 处理鼠标输入
1 | void Application::HandleMouse() { |
绘制顶点索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20void Application::Render() {
HandleMouse();
glEnable(GL_CULL_FACE); // NOTICE : Must Enable Cull Face to ensure NOT rendering back face of the BUNNY !!!
glEnable(GL_DEPTH_TEST);
glBindVertexArray(vao);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glm::mat4 projection = glm::perspective((float)glm::radians(45.0), 800.0f / 600.0f, 0.1f, 100.0f);
glm::mat4 model = glm::scale(glm::mat4(1.0f), glm::vec3(5.0f));
glm::mat4 view = glm::lookAt(viewPos, viewPos + viewDir, viewUp);
mShaders["FaceIndex"]->Bind();
// set Uniforms
mShaders["FaceIndex"]->SetMat("projection", projection);
mShaders["FaceIndex"]->SetMat("model", model);
mShaders["FaceIndex"]->SetMat("view", view);
glBindFramebuffer(GL_FRAMEBUFFER, FaceIndexFBO);
glDrawArrays(GL_TRIANGLES, 0, numFaces * 3);顶点索引 Shader 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23//FaceIndex.vert
#version 330 core
layout(location = 0) in vec3 in_position;
layout(location = 1) in vec3 in_normal;
layout(location = 2) in float in_index;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
out vec3 thePosition;
out vec3 theNormal;
flat out float theIndex; // NOTICE: Index should NOT be interpolated
void main() {
vec4 v = vec4(in_position,1.0);
gl_Position = projection * view * model * v;
thePosition = vec3(model * v);
theNormal = normalize(vec3(model * vec4(in_normal,0)));
theIndex = in_index;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14//FaceIndex.frag
#version 430
out vec4 daColor;
in vec3 thePosition;
in vec3 theNormal;
flat in float theIndex;
void main()
{
daColor = vec4(theIndex,0,0,0);
}
回读鼠标位置的颜色(索引值)
1
2
3
4
5
6
7float res[4];
GLint viewport[4];
glGetIntegerv(GL_VIEWPORT, viewport);
glReadPixels((GLint)MouseX, viewport[3] - MouseY, 1, 1, GL_RED, GL_FLOAT, &res);
// y is flipped in OpenGL screen coordinate system
// res[0] is the face index picked by mouse
绘制冯氏光照模型
1
2
3
4
5
6
7
8
9
10mShaders["BlinnPhong"]->Bind();
mShaders["BlinnPhong"]->SetMat("projection", projection);
mShaders["BlinnPhong"]->SetMat("model", model);
mShaders["BlinnPhong"]->SetMat("view", view);
mShaders["BlinnPhong"]->SetVec("CameraPosition", glm::vec3(0, 0, -20.0f));
mShaders["BlinnPhong"]->SetVec("LightPosition", glm::vec3(-50.0f, 10.0f, -20.0f));
glBindFramebuffer(GL_FRAMEBUFFER, 0); // Bind the default framebuffer (render to screen)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glDrawArrays(GL_TRIANGLES, 0, numFaces * 3);冯氏光照Shader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21//BlinnPhong.vert
#version 330 core
layout(location = 0) in vec3 in_position;
layout(location = 1) in vec3 in_normal;
layout(location = 2) in float in_index;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
out vec3 thePosition;
out vec3 theNormal;
void main() {
vec4 v = vec4(in_position,1.0);
gl_Position = projection * view * model * v;
thePosition = vec3(model * v);
theNormal = normalize(vec3(model * vec4(in_normal,0)));
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32#version 430
out vec4 daColor;
in vec3 thePosition;
in vec3 theNormal;
uniform vec3 CameraPosition;
uniform vec3 LightPosition;
void main()
{
float finalBrightness = 0.0;
float ambient = 0.05;
//diiffuse
vec3 lightDir = normalize(LightPosition - thePosition);
float diff = max(dot(lightDir,theNormal),0.0);
//specular
vec3 viewDir = normalize(CameraPosition - thePosition);
vec3 reflectDir = reflect(-lightDir,theNormal);
float spec = 0.0;
vec3 halfwayDir = normalize(lightDir + viewDir);
spec = pow(max(dot(theNormal,halfwayDir),0.0),32.0);
float specular = 0.3 * spec;
finalBrightness = ambient + diff + specular;
vec3 f = vec3(finalBrightness);
vec3 test = min(f,vec3(1.0));
daColor = vec4(test, 1.0);
}
绘制高亮三角形
高亮面的编号为 res[0] * numFaces
1
DrawCoverVertices(res[0] * numFaces, projection, model, view);
获取高亮面的三个点坐标
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19void Application::DrawCoverVertices(int index, glm::mat4& projection, glm::mat4& model, glm::mat4& view)
{
if (index < 0 || index >= numFaces) {
return;
}
int a = index * 3 + 0;
int b = index * 3 + 1;
int c = index * 3 + 2;
CoveredVertices[0] = sortedVertices[a * 7 + 0];
CoveredVertices[1] = sortedVertices[a * 7 + 1];
CoveredVertices[2] = sortedVertices[a * 7 + 2];
CoveredVertices[3] = sortedVertices[b * 7 + 0];
CoveredVertices[4] = sortedVertices[b * 7 + 1];
CoveredVertices[5] = sortedVertices[b * 7 + 2];
CoveredVertices[6] = sortedVertices[c * 7 + 0];
CoveredVertices[7] = sortedVertices[c * 7 + 1];
CoveredVertices[8] = sortedVertices[c * 7 + 2];绘制高亮面 (关闭depth test 从而与之前的有光照的兔兔叠加)
1
2
3
4
5
6
7
8
9
10
11glDisable(GL_DEPTH_TEST); // NOTICE: Disable depth test to cover last result
glDisable(GL_CULL_FACE);
glBindVertexArray(CoveredVAO);
glBufferSubData(GL_ARRAY_BUFFER, 0, 9 * sizeof(float), CoveredVertices);
mShaders["postprocess"]->Bind();
mShaders["postprocess"]->SetMat("projection", projection);
mShaders["postprocess"]->SetMat("model", model);
mShaders["postprocess"]->SetMat("view", view);
glBindVertexArray(CoveredVAO);
glDrawArrays(GL_TRIANGLES, 0, 9);绘制高亮面Shader
1
2
3
4
5
6
7
8
9
10
11
12//postprocess.vert
#version 330 core
layout(location = 0) in vec3 in_position;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main() {
vec4 v = vec4(in_position,1.0);
gl_Position = projection * view * model * v;
}1
2
3
4
5
6
7
8
9//postprocess.frag
#version 430
out vec4 daColor;
void main()
{
daColor = vec4(1.0,0,0,1.0);
}
加一个简单延迟,锁定在三十帧左右(也可以直接用glfw的垂直同步)
1 | DrawCoverVertices(res[0] * numFaces, projection, model, view); |
- 附上一个Application与Shader的声明
1 | class Application { |
1 | class Shader { |
实现效果
碎碎念
主要难点的地方还是实现拾取,这里用了三个drawcall显然应该是多余的,可优化的地方还很多
给别人讲图形学是真的非常难,概念冗杂不说,还一环套一环,必须全部搞懂才能实现一个非常简单的东西
评论