OpenGLshader开发实战学习笔记:第二章 第一个着色器
1. 第一个着色器
openFrameworks 是一个开源的 C++ 工具包,旨在通过提供一个简单直观的框架来协助创作过程,帮助进行实验。
openFrameworks 被设计为一种通用的“胶水”,将多个常用的库整合在一起,包括:
-
图形:OpenGL、GLEW、GLUT、libtess2 和 cairo -
音频输入、输出与分析:rtAudio、PortAudio、OpenAL、Kiss FFT 或 FMOD -
字体:FreeType -
图像保存与加载:FreeImage -
视频播放与捕获:Quicktime、GStreamer 和 videoInput -
多种实用工具:Poco -
计算机视觉:OpenCV -
3D 模型加载:Assimp
代码被编写为高度跨兼容性。目前,openFrameworks支持五个操作系统(Windows、OSX、Linux、iOS、Android)和四个集成开发环境(XCode、Code::Blocks、Visual Studio 和 Eclipse)。API 的设计力求简洁且易于掌握。 openFrameworks 以 MIT 许可证发布。这赋予了每个人在任何环境下使用 openFrameworks 的自由:商业或非商业、公共或私人、开源或闭源。虽然许多 openFrameworks 用户以类似自由的方式将自己的作品回馈给社区,但这并非强制要求。 简而言之,openFrameworks 是一个让用代码创作变得更加容易的工具。 我们的关注点在于OpenGL开发,为了简化学习,将学习重点放在OpenGL上,因此使用openFrameworks来作为我们的开发环境。 原书使用的是openFrameWorks v0.10.0 版本
本学习笔记配套的代码在https://github.com/DragonBaySpark/practical_shader_development,如遇因Visual Studio 及openframeworks的目录原因造成配套的sln项目不能运行,可以自行新建项目,将src及bin/data目录拷贝过去即可
参考资料
1.1. 在windows上安装openFrameWorks
在Visual Studio 中设置常规项目和使用openFrameWorks的区别是是openFrameWorks 自带的项目生成器(project generator)应用程序。这个应用程序可以帮助我们创建一个新的openFrameWorks项目,而不需要手动设置项目的属性。
-
下载openFrameWorks v0.12.0 版本(目前最新版),解压到本地磁盘。(注:原书中采用的是v0.10.0版本,我在学习时采用了v0.12.0版本。如果遇到问题,可以将版本换回v0.10.0,这样与原书一致) -
打开projectGenerator/projectGenerator.exe,按照提示创建项目。 -
用Visual Studio 2022 打开项目,编译运行即可,无需手动设置项目属性。
注:创建的Visual Studio 项目中的项目属性中会有很多设置与openFrameWroks的目录有关,建议可以将openFrameWorks放在以下目录:D:\openframeworks\of_v0.12.0_vs_release


在Visual Studio 上打开的窗口中点击"确定"
解决方案中将包含2个项目,我们目前的重点在main.cpp和ofApp.cpp
不修改代码,直接编译运行,将得到如下的界面 
1.2. 创建第一个三角形
1.2.1. 运行结果

1.2.2. main.cpp
int main( ){
//Use ofGLFWWindowSettings for more options like multi-monitor fullscreen
ofGLWindowSettings settings;
settings.setSize(1024, 768);
settings.windowMode = OF_WINDOW; //can also be OF_FULLSCREEN
settings.setGLVersion(4, 1); //GL 版本设置为4.1
auto window = ofCreateWindow(settings);
ofRunApp(window, make_shared<ofApp>());
ofRunMainLoop();
}
1.2.3. ofApp.h
private:
// ofMesh 用于存储和管理三维模型的顶点、颜色、纹理坐标等数据
ofMesh mesh;
1.2.4. ofApp.cpp
void ofApp::setup(){
// 添加第一个顶点,位于坐标原点 (0, 0, 0)
mesh.addVertex(glm::vec3(0.0, 0.0, 0.0));
// 添加第二个顶点,位于坐标 (0, 768, 0)
mesh.addVertex(glm::vec3(0.0, 768.0f, 0.0));
// 添加第三个顶点,位于坐标 (1024, 768, 0)
mesh.addVertex(glm::vec3(1024.0f, 768.0f, 0.0));
}
void ofApp::draw(){
// 绘制网格,将之前定义的顶点连接起来形成图形
// 每帧都会重新绘制一次网格,每帧绘制三个顶点
mesh.draw();
}
1.2.5. 说明
代码中画的窗口为 1024*768 大小 ,其坐标系为:原点为左上角 (0,0) ,右下角为(1024,768) 
在OpenGL中,有各种坐标系,后面会一一说明。 这里先说明第一种:将屏幕像素坐标归一化到[-1,1] 之间 。这个归一化其实也存在一定的问题,这会造成屏幕的宽度和高度的比例不一致,导致图像变形。目前我们暂时不考虑这个问题,后续会解决这个问题。 
原始代码为
mesh.addVertex(glm::vec3(0.0, 0.0, 0.0));
mesh.addVertex(glm::vec3(0.0, 768.0f, 0.0));
mesh.addVertex(glm::vec3(1024.0f, 768.0f, 0.0));
更新后的代码如下:
mesh.addVertex(glm::vec3(-1.0, 1.0, 0.0));
mesh.addVertex(glm::vec3(-1.0, -1.0f, 0.0));
mesh.addVertex(glm::vec3(1.0, -1.0f, 0.0));
1.3. 第一个顶点着色器
运行结果 
着色器代码通常放在与C++代码分离的文件中,通常放置在与存储图像或网格等资源相同的文件夹中。 对于openFrameworks,资源目录为bin/data 通常着色器后缀规则:
-
顶点着色器: 后缀为.vert -
片元着色器: 后缀为.frag
顶点着色器的代码
// 指定GLSL版本为4.1
#version 410
// 声明输入变量:顶点位置(从顶点缓冲区接收)
in vec3 position;
// 主函数:每个顶点都会执行一次
void main()
{
// 将3D位置转换为齐次坐标系(4D)
// w分量设为1.0表示这是一个位置点(而不是方向向量)
gl_Position=vec4(position,1.0);
}
目前只介绍我们会用到的几个关键字
-
#version :指定着色器的版本号 -
in : 输入变量,从顶点着色器传递到片元着色器 -
out : 输出变量 -
void : 函数返回类型,无返回值 -
main : 主函数,程序的入口点 -
vec2 : 2D向量类型,包含2个浮点数分量,常用于表示2D坐标、纹理坐标(UV)等。分量可以用 x,y 或 s,t 访问 -
vec3 : 3D向量类型,包含3个浮点数分量,常用于表示3D位置、法线、颜色(RGB)等。分量可以用 x,y,z 或 r,g,b 访问 -
vec4 : 4D向量类型,包含4个浮点数分量,常用于表示4D位置、颜色(RGBA)等。分量可以用 x,y,z,w 或 r,g,b,a 访问 -
gl_Position : 内置变量,用于存储顶点的位置信息,在顶点着色器中设置该变量的值,将决定顶点在屏幕上的位置。
GLSL 语言参考资料
1.4. 第一个片元着色器
片元着色器的代码
#version 410
// 输出变量:片元颜色
out vec4 color;
// 主函数:每个片元都会执行一次
void main()
{
// 设置片元颜色为红色
color=vec4(1.0,0.0,0.0,1.0);
}
片元着色器中,我们只定义了一个输出变量 color,其类型为 vec4,表示4个浮点数分量,分别表示RGBA颜色值。其值设置为红色,因此,每个片元都会被渲染为红色。
需要修改 ofApp.cpp 中的代码
void ofApp::setup(){
mesh.addVertex(glm::vec3(-1.0, 1.0, 0.0));
mesh.addVertex(glm::vec3(-1.0, -1.0f, 0.0));
mesh.addVertex(glm::vec3(1.0, -1.0f, 0.0));
shader.load("first_vertex.vert", "first_fragment.frag");
}
//--------------------------------------------------------------
/**
* @brief 绘制场景的主要函数。
*
* 此函数负责使用指定的着色器绘制网格。
* 它调用着色器的 begin() 方法来激活着色器,
* 然后绘制网格,最后调用着色器的 end() 方法来停用着色器。
*/
void ofApp::draw(){
// 开始使用着色器,准备进行渲染
shader.begin();
// 绘制之前设置好的网格
mesh.draw();
// 结束使用着色器,完成渲染
shader.end();
}
使用顶点添加颜色
运行效果

插值
插值的基本概念 在渲染图形时,我们通常只定义了少量的顶点及其属性,例如顶点的位置、颜色、法线等。然而,在屏幕上最终显示的图形是由大量的片元(Fragment)组成的,这些片元需要根据顶点的属性来确定自身的属性值。插值就是用来计算这些位于顶点之间的片元属性值的方法。
为什么需要插值 以绘制一个三角形为例,我们只定义了三个顶点的属性,但三角形内部有无数个片元,每个片元都需要有自己的属性值(如颜色、纹理坐标等)才能正确渲染。通过插值,我们可以根据顶点的属性来计算出每个片元的属性值,从而实现平滑的过渡效果。
插值的工作原理 在GPU中,当进行渲染时,会根据三角形的三个顶点的属性来计算每个片元的属性值。具体来说,对于每个片元,GPU会考虑它所在的面(由三个顶点构成),并从这三个顶点获取属性数据,然后按照一定的权重混合这些数据,得到该片元的属性值。这个混合过程是线性的,也就是说,属性值在顶点之间是线性变化的。
示例 我们有一个三角形,三个顶点分别设置了不同的颜色(如红色、绿色、蓝色),在渲染这个三角形时,GPU会对每个片元进行插值计算。例如,对于三角形内部靠近红色顶点的片元,其颜色会更接近红色;而靠近绿色和蓝色顶点的片元,颜色会相应地偏向绿色和蓝色。通过这种方式,三角形内部的颜色会呈现出平滑的过渡效果。
顶点着色器和片元着色器中的插值
-
顶点着色器 :主要负责处理顶点的位置和属性,通常会将顶点的属性传递给片元着色器。 -
片元着色器 :接收经过插值后的属性值,并根据这些值来计算每个片元的最终颜色。 ![]()
顶点信息
在 OpenFrameworks 里, ofMesh 对象可用于存储和管理三维模型的顶点、颜色、纹理坐标等数据,这意味着每个顶点可以包含以下几类信息:
1. 位置信息
顶点的位置信息用 glm::vec3 类型来表示,它定义了顶点在三维空间里的位置。
mesh.addVertex(glm::vec3(0.0, 0.0, 0.0));
在顶点着色器中,通过 layout (location=0) in vec3 position; 来定义顶点位置信息的输入变量。
1.4.0.1. 颜色信息
顶点的颜色信息用 glm::vec4 类型来表示,它定义了顶点的 RGBA 颜色值。
mesh.addColor(glm::vec4(1.0, 0.0, 0.0, 1.0));
在顶点着色器中,通过 layout (location=1) in vec4 color; 来定义顶点颜色信息的输入变量。
1.4.0.2. 纹理坐标信息
纹理坐标信息用 glm::vec2 类型来表示,它定义了顶点在纹理图上的位置。
mesh.addTexCoord(glm::vec2(0.5, 0.5));
在顶点着色器中,通过 layout (location=2) in vec2 texCoord; 来定义纹理坐标信息的输入变量。
1.4.0.3. 法线信息
法线信息用 glm::vec3 类型来表示,它定义了顶点的法线向量。
mesh.addNormal(glm::vec3(0.0, 0.0, 1.0));
在顶点着色器中,通过 layout (location=3) in vec3 normal; 来定义法线信息的输入变量。
1.4.1. ofApp.cpp
void ofApp::setup()
{
// 添加三角形的第一个顶点,位于左上角 (-1, 1, 0)
mesh.addVertex(glm::vec3(-1.0, 1.0, 0.0));
// 添加三角形的第二个顶点,位于左下角 (-1, -1, 0)
mesh.addVertex(glm::vec3(-1.0, -1.0f, 0.0));
// 添加三角形的第三个顶点,位于右下角 (1, -1, 0)
mesh.addVertex(glm::vec3(1.0, -1.0f, 0.0));
// 为第一个顶点添加颜色,红色 (1, 0, 0)
mesh.addColor(ofFloatColor(1.0, 0.0, 0.0));
// 为第二个顶点添加颜色,绿色 (0, 1, 0)
mesh.addColor(ofFloatColor(0.0, 1.0, 0.0));
// 为第三个顶点添加颜色,蓝色 (0, 0, 1)
mesh.addColor(ofFloatColor(0.0, 0.0, 1.0));
// 加载顶点着色器和片段着色器
shader.load("first_vertex.vert", "first_fragment.frag");
}
1.4.2. first_vertex.vert
// 指定GLSL版本为4.1
#version 410
// 声明输入变量:顶点位置(从顶点缓冲区接收)
layout (location=0) in vec3 position;
layout (location=1) in vec4 color;
// 主函数:每个顶点都会执行一次
out vec4 fragCol;
void main()
{
// 将3D位置转换为齐次坐标系(4D)
// w分量设为1.0表示这是一个位置点(而不是方向向量)
gl_Position=vec4(position,1.0);
// 将颜色输出到片元着色器
fragCol=color;
}
1.4.3. first_fragment.frag
#version 410
in vec4 fragCol;
// 输出变量:片元颜色
out vec4 color;
// 主函数:每个片元都会执行一次
void main()
{
// 设置片元颜色为红色
//color=vec4(1.0,0.0,0.0,1.0);
color=fragCol;
}
1.5. Uniform变量
在 GLSL(OpenGL Shading Language)中,uniform 是一种特殊的变量类型,用于在着色器程序和应用程序之间传递数据。
1.5.1. Uniform 的特点
-
全局性: uniform 变量可以在顶点着色器和片段着色器中共享,而不需要通过 varying 传递。 -
只读性: 在着色器运行期间,uniform 的值是只读的,不能被修改。 -
固定值: uniform 的值在整个绘制调用期间保持不变,适用于所有顶点和片段。 -
外部设置: uniform 的值由应用程序(例如 OpenFrameworks 或 OpenGL 应用程序)通过 API 设置。
1.5.2. Uniform 的用途
uniform 变量通常用于传递以下类型的数据:
-
变换矩阵: 如模型矩阵(Model Matrix)、视图矩阵(View Matrix)、投影矩阵(Projection Matrix)。 -
光照信息: 如光源位置、颜色、强度等。 -
材质属性: 如漫反射颜色、镜面反射颜色、光泽度等。 -
时间或动画参数: 如时间戳、动画进度等。 -
纹理采样器: 如绑定到着色器的纹理单元。 -
其他全局参数: 如分辨率、摄像机参数等。
1.5.3. Uniform 的设置
-
c++代码中调用setUniform1f(), setUniform2f(), setUniform3f(), setUniform4f() 等函数设置uniform浮点数变量。 -
在着色器中使用 uniform 关键字声明变量。 -
c++代码和着色器中使用相同的变量名。 -
c++代码和着色器中使用相同的变量类型。
1.5.4. 将三角形的颜色设置为绿色
运行结果 
1.5.4.1. ofApp.cpp
void ofApp::draw(){
// 开始使用着色器,准备进行渲染
shader.begin();
// 设置着色器中的统一变量 "fragCol",将其值设置为一个四维向量,代表颜色 (RGBA)
// 这里的颜色为绿色(R=0.0, G=1.0, B=0.0),并且完全不透明(A=1.0)
shader.setUniform4f("fragCol",glm::vec4(0.0, 1.0, 0.0, 1.0));
// 绘制之前设置好的网格
mesh.draw();
// 结束使用着色器,完成渲染
shader.end();
}
1.5.4.2. vertex.vert
// 指定GLSL版本为4.1
#version 410
// 声明输入变量:顶点位置(从顶点缓冲区接收)
layout (location=0) in vec3 position;
// 主函数:每个顶点都会执行一次
void main()
{
// 将3D位置转换为齐次坐标系(4D)
// w分量设为1.0表示这是一个位置点(而不是方向向量)
gl_Position=vec4(position,1.0);
}
1.5.4.3. fragment.frag
#version 410
uniform vec4 fragCol;
// 输出变量:片元颜色
out vec4 color;
// 主函数:每个片元都会执行一次
void main()
{
// 设置片元颜色为红色
//color=vec4(1.0,0.0,0.0,1.0);
color=fragCol;
}
1.6. 常见问题
-
找不到dll
找不到libcrypto-3-x64.dll 和找不到FreeImage.dll
清理项目,重新编译。



浙公网安备 33010602011771号